Build a Restricted Boltzmann Machine (RBM) as a recommendation system for Movie Review | Towards Data Science

This article is Part 2 of how to build a Restricted Boltzmann Machine (RBM) as a recommendation system.
In Part 1, we focus on data processing, and here the focus is on model creation. What you will learn is how to create an RBM model from scratch. It is split into 3 parts.
Model buildingModel trainingModel testing
Model building
Model training
Model testing
📣📣 This is a technical-driven article. Now let’s begin the journey 🏃‍♀️🏃‍♂️.
Model building
Model building
Essentially, RBM is a probabilistic graphical model.
To build the model architecture, we will create a class for RBM. In the class, define all parameters for RBM, including the number of hidden nodes, the weights, and bias for the probability of the visible nodes and the hidden node.
There are 4 functions, 1st is to initialize the class, 2nd function is to sample the probabilities of hidden nodes given visible nodes, and 3rd function is to sample the probabilities of visible nodes given hidden nodes, the final one is to train the model.
1.1 __init__ function
Inside the __init__ function, we will initialize all parameters that need to be optimized. Note, nv and nh are the numbers of visible nodes and the number of hidden nodes, respectively. W is the weights for the visible nodes and hidden nodes. We use a normal distribution with mean 0 and variance 1 to initialize weights and bias. a is the bias for the probability of hidden nodes given visible node, and b is the bias for the probability of visible nodes given hidden nodes. Note we added a dimension for the batch because the function we will use in Pytorch cannot accept vectors with only 1 dimension.
class RBM():    def __init__(self, nv, nh):        self.W = torch.randn(nh, nv)        self.a = torch.randn(1, nh)        self.b = torch.randn(1, nb)
1.2 Hidden node sampling function
This function is about sampling hidden nodes given the probabilities of visible nodes. Why do we need this? Because we need the probabilities to sample the activation of the hidden nodes.
Suppose we have 100 hidden nodes, this function will sample the activation of the hidden nodes, namely activating them based on certain probability p_h_given_v. p_h_given_v is the probability of hidden nodes equal to one (activated) given the values of v.
Note the function takes argument x, which is the value of visible nodes. We use v to calculate the probability of hidden nodes. Remember, the probability of h given v (p_h_given_v) is the sigmoid activation of v. Thus, we multiply the value of visible nodes with the weights, plus the bias of the hidden nodes. We expanded the dimension for bias a to have the same dimension as wx, so that bias is added to each line of wx.
def sample_h(self, x):    wx = torch.mm(x, self.W.t())    activation = wx + self.a.expand(wx)    p_h_given_v =torch.sigmoid(activation)    reutrn p_h_given_v, torch.bernoulli(p_h_given_v)
Note what is returned is p_h_given_v, and the sampled hidden nodes. Here, we are making a Bernoulli RBM, as we are predicting a binary outcome, that is, users like or not like a movie. Assuming there are 100 hidden nodes, p_h_given_v is a vector of 100 elements, with each element as the probability of each hidden node being activated, given the values of visible nodes (namely, movie ratings by a user). But the question is how to activate the hidden nodes? Here we use Bernoulli sampling. Suppose, for a hidden node, its probability in p_h_given_v is 70%. We take a random number between 0 and 1. If it is below 70%, we will not activate the hidden node. By repeating Bernoulli sampling for all hidden nodes in p_h_given_v, we get a vector of zeros and ones with one corresponding to hidden nodes to be activated.
This is the first function we need for Gibbs sampling ✨✨.
1.3 Visible node sampling function
Following the same logic, we create the function to sample visible nodes. Given the values of hidden nodes (1 or 0, activated or not), we estimate the probabilities of visible nodes p_v_given_h, which is the probabilities of each visible node equal to 1 (being activated).
Since there are 1682 movies and thus1682 visible nodes, we have a vector of 1682 probabilities, each corresponding to visible node equal to one, given the activation of the hidden nodes. We use Bernoulli sampling to decide if this visible node will be sampled or not. In the end, the function returns probabilities of visible nodes p_v_given_h, and a vector of ones and zeros with one corresponding to visible nodes to be activated.
def sample_v(self, y):    wy = torch.mm(y, self.W)    activation = wy + self.b.expand(wy)    p_v_given_h =torch.sigmoid(activation)    reutrn p_v_given_h, torch.bernoulli(p_v_given_h)
1.4 Contrastive divergence function
RBM is an energy-based model which means we need to minimize the energy function.
The energy function depends on the weights of the model, and thus we need to optimize the weights. On the other hand, RBM can be taken as a probabilistic graphical model, which requires maximizing the log-likelihood of the training set. Obviously, for any neural network, to minimize the energy or maximize the log-likelihood, we need to compute the gradient. Here we use Contrastive Divergence to approximate the likelihood gradient.
Contrastive divergence is about approximating the log-likelihood gradient. Instead of direct computation of gradient which requires heavy computation resources, we approximate the gradient. During training, we adjust the weights in the direction of minimizing energy. Similar to minimizing loss function through gradient descent where we update the weights to minimize the loss, the only difference is we approximate the gradient using an algorithm, Contrastive Divergence.
Specifically, we start with input vector v0, based on the probability of p_h_given_v, we sample the first set of hidden nodes at the first iteration and use these sampled hidden nodes to sample visible nodes v1 with p_v_given_h. Repeat this process K times, and that is all about k-step Contrastive Divergence.
In this function, we will update the weights, the bias for visible nodes, and for hidden nodes using the algorithm outlined in this paper. I strongly recommend this RBM paper if you like a more in-depth understanding.
Inside the function, v0 is the input vector containing the ratings of all movies by a user. vk is the visible nodes obtained after k samplings from visible nodes to hidden nodes. ph0 is the vector of probabilities of hidden node equal to one at the first iteration given v0. phk is the probabilities of hidden nodes given visible nodes vk at the kth iteration.
def train(self, v0, vk, ph0, phk):    self.W += torch.mm(v0.t(), ph0) — torch.mm(vk.t(), phk)    self.b += torch.sum((v0 — vk), 0)    self.a += torch.sum((ph0 — phk), 0)
1.5 RBM object creation
To initialize the RBM, we create an object of RBM class. First, we need the number of visible nodes, which is the number of total movies. The number of hidden nodes corresponds to the number of features we want to detect from the movies. It is hard to tell the optimal number of features. But this parameter is tunable, so we start with 100. We also define the batch size, which is the number of observations in a batch we use to update the weights. Again we start with 100.
nv = len(training_set[0]) nh = 100 batch_size = 100 rbm = RBM(nv, nh)
2. Model training
Congratulations if you made through Part 1 as that is the most difficult part 👍👍. Now let’s train the RBM model.
We set nb_epoch as 10 to start with. For each epoch, all observations will go into the network and update the weights after each batch passed through the network. In the end, we get final visible nodes with new ratings for the movies which were not rated originally. Inside each batch, we will make the k steps contrastive divergence to predict the visible nodes after k steps of random walks. Thus, we will have 3 for loops, one for epoch iteration and one for batch iteration, and a final one for contrastive divergence.
For the loss function, we will measure the difference between the predicted ratings and the real ratings in the training set. There are a few options, including RMSE which is the root of the mean of the square difference between the predicted ratings and the real ratings, and the absolute difference between the predicted ratings and the real ratings. We will take an absolute difference here.
Inside the batch loop, we have input vector vk, which will be updated through contrastive divergence and as the output of Gibbs sampling after k steps of a random walk. But at the start, vk is the input batch of all ratings of the users in a batch. v0 is the target which will be compared with predictions, which are the ratings that were rated already by the users in the batch. ph0 is the initial probabilities of hidden nodes given visible nodes v0.
Inside the contrastive divergence loop, we will make the Gibbs sampling. Basically, it consists of making Gibbs chain which is several round trips from the visible nodes to the hidden nodes. In each round, visible nodes are updated to get a good prediction. Starting from the visible nodes vk, we sample the hidden nodes with a Bernoulli sampling. At the end of 10 random walks, we get the 10th sampled visible nodes. Note, we will not train RBM on ratings that were -1 which are not existing as real rating at the beginning.
With v0, vk, ph0, phk, we can apply the train function to update the weights and biases. Eventually, the probabilities that are most relevant to the movie features will get the largest weights, leading to correct predictions. At the end of each batch, we log the training loss. Again, we only record the loss on ratings that were existent.
nb_epoch = 10for epoch in range(1, nb_epoch+1):    train_loss = 0    s = 0.    for id_user in range(0, nb_users — batch_size, 100):        vk = training_set[id_user: id_user+batch_size]        v0 = training_set[id_user: id_user+batch_size]        ph0, = rbm.sample_h(v0)        for k in range(10):            _, hk = rbm.sample_h(vk)            _, vk = rbm.sample_v(hk)            vk[v0<0] = v0[v0<0]        phk, _ = rbm.sample_h(vk)       rbm.train(v0, vk, ph0, phk)       train_loss += torch.mean(torch.abs(v0[v0>0]-vk[v0>0]))       s += 1 print(‘epoch: ‘+str(epoch)+’ loss: ‘+str(train_loss/s))
After 10 epoch iteration of training, we got a loss of 0.15. Quite a decent accuracy ✌✌.
3. Model testing
Compared to the training loops, we remove the epoch iteration and batch iteration. We will loop each observation through the RBM and make a prediction one by one, accumulating the loss for each prediction.
Note below, we use the training_set as the input to activate the RBM, the same training set used to train the RBM. But the difference is that in the testing stage, we did not remove ratings which were not rated by the user originally, because these are unknown inputs for a model for testing purpose. Also notice, we did not perform 10 steps of random walks as in the training stage. This is because for testing to obtain the best prediction, 1 step is better than 10 iterations.
test_loss = 0s = 0.for id_user in range(0, nb_users):    v_input = training_set[id_user: id_user+1]    v_target = test_set[id_user: id_user+1]    if len(v_target(v_target>=0)):        _, h = rbm.sample_h(v_input)        _, v_input = rbm.sample_v(h)        test_loss += torch.mean(torch.abs(v_target[v_target>0]-                                          v_input[v_target>0]))         s += 1print(‘test loss: ‘ +str(test_loss/s))
Great. We obtained a loss of 0.16, close to the training loss, indicating a minor over-fitting.
That’s all. Hopefully, this gives a sense of how to create an RBM as a recommendation system. If you need the source code, visit my Github page 🤞🤞.