Files
FYS-STK4155/doc/Programs/BoltzmannMachines/rbm.py
T
Morten Hjorth-Jensen 24234ed5a5 added rbms
2025-05-30 09:29:56 +02:00

206 lines
11 KiB
Python

# RBM Class Implementation
import numpy as np
class RBM:
def __init__(self, n_visible, n_hidden, learning_rate=0.1):
"""
Initialize the RBM with given number of visible and hidden units.
Weights are initialized to small random values, biases to zero.
"""
self.n_visible = n_visible # number of visible units
self.n_hidden = n_hidden # number of hidden units
self.learning_rate = learning_rate
# Initialize weight matrix (dimensions: n_visible x n_hidden) with small random values
# Using a normal distribution with mean 0 and standard deviation 0.1 for initialization
self.weights = np.random.normal(loc=0.0, scale=0.1, size=(n_visible, n_hidden))
# Initialize biases for visible and hidden units as zero vectors
self.visible_bias = np.zeros(n_visible)
self.hidden_bias = np.zeros(n_hidden)
def _sigmoid(self, x):
"""Sigmoid activation function to convert energies to probabilities (0 to 1)."""
return 1.0 / (1.0 + np.exp(-x))
def sample_hidden(self, visible):
"""
Sample hidden state given a visible state.
- visible: array-like of shape (n_visible,) with binary values (0 or 1).
- Returns: numpy array of shape (n_hidden,) with sampled binary hidden states.
"""
# Ensure input is a numpy array
visible = np.array(visible)
# Compute activations of hidden units: W^T * visible + hidden_bias
hidden_activations = visible.dot(self.weights) + self.hidden_bias # shape (n_hidden,)
# Convert activations to probabilities via sigmoid
hidden_probs = self._sigmoid(hidden_activations)
# Sample each hidden unit (1 with probability = hidden_probs)
hidden_states = (np.random.rand(self.n_hidden) < hidden_probs).astype(np.int_)
return hidden_states
def sample_visible(self, hidden):
"""
Sample visible state given a hidden state.
- hidden: array-like of shape (n_hidden,) with binary values (0 or 1).
- Returns: numpy array of shape (n_visible,) with sampled binary visible states.
"""
hidden = np.array(hidden)
# Compute activations of visible units: W * hidden + visible_bias
visible_activations = hidden.dot(self.weights.T) + self.visible_bias # shape (n_visible,)
# Convert activations to probabilities via sigmoid
visible_probs = self._sigmoid(visible_activations)
# Sample each visible unit (1 with probability = visible_probs)
visible_states = (np.random.rand(self.n_visible) < visible_probs).astype(np.int_)
return visible_states
def train(self, data, epochs=10, batch_size=1, k=1):
"""
Train the RBM using Contrastive Divergence (CD-k).
Parameters:
- data: array-like of shape (num_samples, n_visible), containing the training data (values 0 or 1).
- epochs: number of epochs (full passes through the dataset) to train.
- batch_size: number of samples per mini-batch for updates (default 1 for stochastic CD).
- k: number of Gibbs sampling steps for CD-k (default 1, which is the standard CD-1).
"""
data = np.array(data)
num_samples = data.shape[0]
batch_size = min(batch_size, num_samples) # ensure batch_size is valid
for epoch in range(epochs):
# Shuffle the training data at the start of each epoch
indices = np.random.permutation(num_samples)
data_shuffled = data[indices]
# Loop over batches of the training data
for i in range(0, num_samples, batch_size):
batch = data_shuffled[i:i+batch_size]
# === Positive phase ===
# Compute hidden probabilities for the batch (forward pass)
pos_hidden_activations = batch.dot(self.weights) + self.hidden_bias # shape: (batch_size, n_hidden)
pos_hidden_probs = self._sigmoid(pos_hidden_activations) # p(h_j=1 | v)
# Sample hidden states from these probabilities
pos_hidden_states = (np.random.rand(batch.shape[0], self.n_hidden) < pos_hidden_probs).astype(np.int_)
# Compute positive associations (correlation between v and h) as batch.T * pos_hidden_probs
pos_associations = batch.T.dot(pos_hidden_probs) # shape: (n_visible, n_hidden)
# === Negative phase (reconstruction) ===
# Initialize the negative phase using the sampled hidden states from the positive phase
neg_hidden_states = pos_hidden_states
neg_hidden_probs = None
neg_visible_states = None
# Perform k full Gibbs sampling steps (hidden->visible->hidden repeated k times)
for step in range(k):
# Visible probabilities given hidden
neg_visible_activations = neg_hidden_states.dot(self.weights.T) + self.visible_bias # (batch_size, n_visible)
neg_visible_probs = self._sigmoid(neg_visible_activations) # p(v_i=1 | h)
# Sample visible states from these probabilities
neg_visible_states = (np.random.rand(batch.shape[0], self.n_visible) < neg_visible_probs).astype(np.int_)
# Hidden probabilities given the reconstructed visible
neg_hidden_activations = neg_visible_states.dot(self.weights) + self.hidden_bias # (batch_size, n_hidden)
neg_hidden_probs = self._sigmoid(neg_hidden_activations) # p(h_j=1 | v_recon)
# Sample hidden states for the next step (if more Gibbs steps remain)
if step < k - 1:
neg_hidden_states = (np.random.rand(batch.shape[0], self.n_hidden) < neg_hidden_probs).astype(np.int_)
# After k steps, we have neg_visible_states (sampled reconstruction) and neg_hidden_probs (probabilities at hidden layer)
# === Update weights and biases ===
# Compute negative associations (using reconstructed visible states and hidden probabilities)
neg_associations = neg_visible_states.T.dot(neg_hidden_probs) # shape: (n_visible, n_hidden)
# Update weights: increase by learning_rate * (positive associations - negative associations) normalized by batch size
self.weights += self.learning_rate * (pos_associations - neg_associations) / batch.shape[0]
# Update biases: adjust towards the data (positive phase) and away from reconstructions (negative phase)
self.visible_bias += self.learning_rate * np.mean(batch - neg_visible_states, axis=0)
self.hidden_bias += self.learning_rate * np.mean(pos_hidden_probs - neg_hidden_probs, axis=0)
# (Optionally, one could compute reconstruction error here to monitor training progress)
def reconstruct(self, visible):
"""
Reconstruct a given visible vector through one forward-pass to hidden and one backward-pass to visible.
- visible: array-like of shape (n_visible,) with binary values.
- Returns: tuple (reconstructed_visible_probabilities, reconstructed_visible_state).
"""
visible = np.array(visible)
# Forward pass: visible -> hidden
hidden_probs = self._sigmoid(visible.dot(self.weights) + self.hidden_bias)
hidden_state = (np.random.rand(self.n_hidden) < hidden_probs).astype(np.int_)
# Backward pass: hidden -> visible
recon_visible_activations = hidden_state.dot(self.weights.T) + self.visible_bias
recon_visible_probs = self._sigmoid(recon_visible_activations)
recon_visible_state = (np.random.rand(self.n_visible) < recon_visible_probs).astype(np.int_)
return recon_visible_probs, recon_visible_state
def get_parameters(self):
"""
Get the learned parameters of the RBM.
Returns a tuple: (weight_matrix, visible_bias_vector, hidden_bias_vector).
"""
return self.weights, self.visible_bias, self.hidden_bias
def gibbs_sample(self, num_steps=1, initial_visible=None):
"""
Generate a sample from the learned RBM by running Gibbs sampling.
- num_steps: number of Gibbs sampling iterations to perform.
- initial_visible: optional initial visible state to start the chain (if None, a random visible state is used).
- Returns: tuple (visible_state, hidden_state) after the Gibbs sampling steps.
"""
# If no initial visible state provided, start with a random binary visible vector
if initial_visible is None:
visible_state = (np.random.rand(self.n_visible) < 0.5).astype(np.int_)
else:
visible_state = np.array(initial_visible).astype(np.int_)
hidden_state = None
# Iterate Gibbs sampling for the specified number of steps
for step in range(num_steps):
# Sample hidden state given current visible state
hidden_probs = self._sigmoid(visible_state.dot(self.weights) + self.hidden_bias)
hidden_state = (np.random.rand(self.n_hidden) < hidden_probs).astype(np.int_)
# Sample visible state given current hidden state
visible_probs = self._sigmoid(hidden_state.dot(self.weights.T) + self.visible_bias)
visible_state = (np.random.rand(self.n_visible) < visible_probs).astype(np.int_)
# After the final step, we have a new visible_state; sample a final hidden_state for completeness
hidden_probs = self._sigmoid(visible_state.dot(self.weights) + self.hidden_bias)
hidden_state = (np.random.rand(self.n_hidden) < hidden_probs).astype(np.int_)
return visible_state, hidden_state
# Example usage of the RBM class
# 1. Create a synthetic dataset (binary patterns)
# Here we use patterns of length 4. For example:
# Pattern A = [1, 1, 0, 0] and Pattern B = [0, 0, 1, 1], repeated multiple times.
data = [[1, 1, 0, 0]] * 50 + [[0, 0, 1, 1]] * 50 # 100 samples total (50 of each pattern)
# 2. Initialize the RBM with 4 visible units and 2 hidden units
rbm = RBM(n_visible=4, n_hidden=2, learning_rate=0.1)
# 3. Train the RBM on the dataset for a certain number of epochs
rbm.train(data, epochs=50, batch_size=10, k=1) # using CD-1 (k=1) and mini-batches of size 10
# 4. After training, retrieve the learned weights and biases
weights, vis_bias, hid_bias = rbm.get_parameters()
print("Learned weight matrix:\n", weights)
print("Learned visible biases:", vis_bias)
print("Learned hidden biases:", hid_bias)
# 5. Reconstruct an example from the training data using the trained RBM
test_visible = np.array([1, 1, 0, 0]) # input pattern A
recon_probs, recon_state = rbm.reconstruct(test_visible)
print("\nOriginal visible input:", test_visible)
print("Reconstructed visible probabilities:", recon_probs)
print("Reconstructed visible sample:", recon_state)
# 6. Generate a new sample from the model using Gibbs sampling
visible_sample, hidden_sample = rbm.gibbs_sample(num_steps=5) # start from a random visible state
print("\nNew sampled visible state (after Gibbs sampling):", visible_sample)
print("Corresponding hidden state:", hidden_sample)