Deep Learning for Collaborative Filtering

To turn our architecture into a deep learning model, the first step is to take the results of the embedding lookup and concatenate those activations together. This gives us a matrix which we can then pass through linear layers and nonlinearities in the usual way.

Since we’ll be concatenating the embeddings, rather than taking their dot product, the two embedding matrices can have different sizes (i.e., different numbers of latent factors). fastai has a function get_emb_sz that returns recommended sizes for embedding matrices for your data, based on a heuristic that fast.ai has found tends to work well in practice:

In [ ]:

  1. embs = get_emb_sz(dls)
  2. embs

Out[ ]:

  1. [(944, 74), (1635, 101)]

Let’s implement this class:

In [ ]:

  1. class CollabNN(Module):
  2. def __init__(self, user_sz, item_sz, y_range=(0,5.5), n_act=100):
  3. self.user_factors = Embedding(*user_sz)
  4. self.item_factors = Embedding(*item_sz)
  5. self.layers = nn.Sequential(
  6. nn.Linear(user_sz[1]+item_sz[1], n_act),
  7. nn.ReLU(),
  8. nn.Linear(n_act, 1))
  9. self.y_range = y_range
  10. def forward(self, x):
  11. embs = self.user_factors(x[:,0]),self.item_factors(x[:,1])
  12. x = self.layers(torch.cat(embs, dim=1))
  13. return sigmoid_range(x, *self.y_range)

And use it to create a model:

In [ ]:

  1. model = CollabNN(*embs)

CollabNN creates our Embedding layers in the same way as previous classes in this chapter, except that we now use the embs sizes. self.layers is identical to the mini-neural net we created in <> for MNIST. Then, in forward, we apply the embeddings, concatenate the results, and pass this through the mini-neural net. Finally, we apply sigmoid_range as we have in previous models.

Let’s see if it trains:

In [ ]:

  1. learn = Learner(dls, model, loss_func=MSELossFlat())
  2. learn.fit_one_cycle(5, 5e-3, wd=0.01)
epochtrain_lossvalid_losstime
00.9401040.95978600:15
10.8939430.90522200:14
20.8655910.87523800:14
30.8001770.86746800:14
40.7602550.86745500:14

fastai provides this model in fastai.collab if you pass use_nn=True in your call to collab_learner (including calling get_emb_sz for you), and it lets you easily create more layers. For instance, here we’re creating two hidden layers, of size 100 and 50, respectively:

In [ ]:

  1. learn = collab_learner(dls, use_nn=True, y_range=(0, 5.5), layers=[100,50])
  2. learn.fit_one_cycle(5, 5e-3, wd=0.1)
epochtrain_lossvalid_losstime
01.0027470.97239200:16
10.9269030.92234800:16
20.8771600.89340100:16
30.8383340.86504000:16
40.7816660.86493600:16

learn.model is an object of type EmbeddingNN. Let’s take a look at fastai’s code for this class:

In [ ]:

  1. @delegates(TabularModel)
  2. class EmbeddingNN(TabularModel):
  3. def __init__(self, emb_szs, layers, **kwargs):
  4. super().__init__(emb_szs, layers=layers, n_cont=0, out_sz=1, **kwargs)

Wow, that’s not a lot of code! This class inherits from TabularModel, which is where it gets all its functionality from. In __init__ it calls the same method in TabularModel, passing n_cont=0 and out_sz=1; other than that, it only passes along whatever arguments it received.

Sidebar: kwargs and Delegates

EmbeddingNN includes **kwargs as a parameter to __init__. In Python **kwargs in a parameter list means “put any additional keyword arguments into a dict called kwargs. And **kwargs in an argument list means “insert all key/value pairs in the kwargs dict as named arguments here”. This approach is used in many popular libraries, such as matplotlib, in which the main plot function simply has the signature plot(*args, **kwargs). The plot documentation says “The kwargs are Line2D properties” and then lists those properties.

We’re using **kwargs in EmbeddingNN to avoid having to write all the arguments to TabularModel a second time, and keep them in sync. However, this makes our API quite difficult to work with, because now Jupyter Notebook doesn’t know what parameters are available. Consequently things like tab completion of parameter names and pop-up lists of signatures won’t work.

fastai resolves this by providing a special @delegates decorator, which automatically changes the signature of the class or function (EmbeddingNN in this case) to insert all of its keyword arguments into the signature.

End sidebar

Although the results of EmbeddingNN are a bit worse than the dot product approach (which shows the power of carefully constructing an architecture for a domain), it does allow us to do something very important: we can now directly incorporate other user and movie information, date and time information, or any other information that may be relevant to the recommendation. That’s exactly what TabularModel does. In fact, we’ve now seen that EmbeddingNN is just a TabularModel, with n_cont=0 and out_sz=1. So, we’d better spend some time learning about TabularModel, and how to use it to get great results! We’ll do that in the next chapter.