Chapter 8 - Collaborative Filtering Deep Dive
Deep Learning For Coders with fastai & Pytorch- Collaborative Filtering Deep Dive - Recommender systems works differently than classic DL classifiers. They are mostly used for known data, no prediction expected based on previously unknown data like bear classifier do. Yes, there is a generalization process but still, all data is known by the model. What is not known is latent factors at the beginning of the training. The model learn these latent factors and the recommender is ready.
- Exploring the data
- How model learn about our preferences
- How to create Dataloaders
- More PyTorch Less Python
- Collaborative Filtering from Scratch
- Weight Decay (or L2 regularization)
- Interpreting Embeddings and Biases
- Bootstrapping a Collaborative Filtering Model
- Deep Learning for Collaborative Filtering
This my daughter at the IKEA very close to our home.
import fastbook
fastbook.setup_book()
from fastbook import *
%config Completer.use_jedi = False
Collaborative filtering modules:
from fastai.collab import *
from fastai.tabular.all import*
Downloading and extracting data from the URL list
path = untar_data(URLs.ML_100k)
Giving columns names and readind first five rows.
ratings = pd.read_csv(path/'u.data',delimiter = '\t', header= None, engine='python',names=['user','movie','rating','timestamp'])
ratings.head()
How to recommend movies. Assume the movie has three properties, scince fiction(ness), action, old(ness).
Last skywalker is a sci-fi, and action and not old.
last_skywalker = np.array([0.98,0.9,-0.9])
And a user who likes sci-fi and action movies and not so old movies would like this.
user1= np.array([.9,.8,-.6])
If we multiply these two vectors and sum it. We get:
(user1*last_skywalker).sum()
this our matching score, it is a positive value that shows there is a match between the movie and the user1
casablanka= np.array([-.99,-.33,.8])
(user1*casablanka).sum()
this is low at this time. There is no match.
We can pick arbitrary number of parameters for the array. Above, we use three. That could be much more of them. We call them Latent Factors. We start training with random parameters and learn from the ratings given by users.
movies = pd.read_csv(path/'u.item', delimiter='|', engine= 'python',header=None,encoding='latin1', usecols=(0,1),names=('movie','title'))
movies.head()
Let's bring ratings and movies together. (movie id will be the key parameter)
ratings=ratings.merge(movies)
ratings.head()
For Dataloaders, we use CollabDataLoaders
this Dataloader use first column for the user and second one for the item
, in our situation we should change the default one because our item
will be title
.
dls=CollabDataLoaders.from_df(ratings, item_name='title',bs=64)
dls.show_batch()
dls.classes['user'][:15]
dls.classes['title'][:15]
n_users = len(dls.classes['user'])
n_movies = len(dls.classes['title'])
n_factors = 5
user_factors = torch.randn(n_users,n_factors)
movie_factors = torch.randn(n_movies, n_factors)
one_hot(0,5)
one_hot_3 = one_hot(3,n_users).float()
one_hot_3[:10]
and multiply by users_factors(matrix multiplication)
user_factors.t() @ one_hot_3
This might look a bit daunting but it is not. Basically we want utilize pytorch more and python less. PyTorch very good at matrix multiplication, python is not. With this matrix multiplication we can access every index of the latent factor tensor in one move. Otherwise we would have use regular python loop and index which is very very slow.
This is Python version:
user_factors[3]
This is same. Great.
At this point there is a section regarding OOP if you want to learn OOP the check the original book page 260 (3rd release) or the course notebook
class DotProduct(Module):
def __init__(self, n_users, n_movies, n_factors):
self.user_factors = Embedding(n_users, n_factors)
self.movie_factors = Embedding(n_movies, n_factors)
def forward(self, x):
users = self.user_factors(x[:,0])
movies = self.movie_factors(x[:,1])
return (users * movies).sum(dim=1)
x
is merged df (it became part of the dls) from above so first column is user id and the second is movie id. check this part:
python
ratings=ratings.merge(movies)
ratings.head()
x,y = dls.one_batch()
x.shape
x[0]
first one is user id and the second is movie.
y[0]
must be the rating.
model = DotProduct(n_users, n_movies, 50)
learn = Learner(dls, model, loss_func=MSELossFlat())
learn.fit_one_cycle(5, 5e-3)
not bad but we can force our model to make predictions into range 0-5
class DotProduct(Module):
def __init__(self, n_users, n_movies, n_factors, y_range=(0,5.5)):
self.user_factors = Embedding(n_users, n_factors)
self.movie_factors = Embedding(n_movies, n_factors)
self.y_range = y_range
def forward(self, x):
users = self.user_factors(x[:,0])
movies = self.movie_factors(x[:,1])
return sigmoid_range((users * movies).sum(dim=1), *self.y_range)
The dls has values in this range as dependent variables (ratings) and there is a special method in the fastai(I assume) for that.
doc(sigmoid_range)
model = DotProduct(n_users, n_movies, 50)
learn = Learner(dls, model, loss_func=MSELossFlat())
learn.fit_one_cycle(5, 5e-3)
A little bit better results.
Sometimes a user could give low (or high) ratings based on his/her subjective preference even the others thinks that is a very good movie. Let's add a net parameter for that is bias
. Bias effects all other parameters in negative or positive way.
class DotProductBias(Module):
def __init__(self, n_users, n_movies, n_factors, y_range=(0,5.5)):
self.user_factors = Embedding(n_users, n_factors)
self.user_bias = Embedding(n_users, 1)
self.movie_factors = Embedding(n_movies, n_factors)
self.movie_bias = Embedding(n_movies, 1)
self.y_range = y_range
def forward(self, x):
users = self.user_factors(x[:,0])
movies = self.movie_factors(x[:,1])
res = (users * movies).sum(dim=1, keepdim=True)
res += self.user_bias(x[:,0]) + self.movie_bias(x[:,1])
return sigmoid_range(res, *self.y_range)
model = DotProductBias(n_users, n_movies, 50)
learn = Learner(dls, model, loss_func=MSELossFlat())
learn.fit_one_cycle(5, 5e-3)
And the training loss goes down faster and faster, but valid loss not so.
from the book:
Weight decay, or L2 regularization, consists in adding to your loss function the sum of all the weights squared. Why do that? Because when we compute the gradients, it will add a contribution to them that will encourage the weights to be as small as possible.**
Why would it prevent overfitting? The idea is that the larger the coefficients are, the sharper canyons we will have in the loss function. If we take the basic example of a parabola, So, letting our model learn high parameters might cause it to fit all the data points in the training set with an overcomplex function that has very sharp changes, which will lead to overfitting. Limiting our weights from growing too much is going to hinder the training of the model, but it will yield a state where it generalizes better. Going back to the theory briefly, weight decay (or just In practice, though, it would be very inefficient (and maybe numerically unstable) to compute that big sum and add it to the loss. If you remember a little bit of high school math, you might recall that the derivative of In practice, since Not so good traing loss but at this time validation loss is far better. There is no pararameters, by its definition parameters must be trainable. from the book: To tell and Very similiar results. Lowest biases in the model. from the book: Think about what this means. What it's saying is that for each of these movies, even when a user is very well matched to its latent factors (which, as we will see in a moment, tend to represent things like level of action, age of movie, and so forth), they still generally don't like it. We could have simply sorted the movies directly by their average rating, but looking at the learned bias tells us something much more interesting. It tells us not just whether a movie is of a kind that people tend not to enjoy watching, but that people tend not to like watching it even if it is of a kind that they would otherwise enjoy! By the same token, here are the movies with the highest bias: from the book: So, for instance, even if you don't normally enjoy detective movies, you might enjoy LA Confidential! It is not quite so easy to directly interpret the embedding matrices. There are just too many factors for a human to look at. But there is a technique that can pull out the most important underlying directions in such a matrix, called principal component analysis (PCA). We will not be going into this in detail in this book, because it is not particularly important for you to understand to be a deep learning practitioner, but if you are interested then we suggest you check out the fast.ai course Computational Linear Algebra for Coders. < Lets try changing X axis. Very interesting to study changes. Same thing with fastai similar results. Basically it means if two movies has similar latent factors.(embedding vector)
This is the movie very similar latent factors with Silence of the lambs. read the all section from the original book at page 270 (3rd release) or the course notebook First fastai could make a recommendation for right embedding sizes(latent factors). above is possibble(again) with collab_learner with one step. just use from the book: 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.y = a * (x**2)
, the larger a
is, the more narrow the parabola is (<wd
) is a parameter that controls that sum of squares we add to our loss (assuming parameters
is a tensor of all parameters):loss_with_wd = loss + wd * (parameters**2).sum()
p**2
with respect to p
is 2*p
, so adding that big sum to our loss is exactly the same as doing:parameters.grad += wd * 2 * parameters
wd
is a parameter that we choose, we can just make it twice as big, so we don't even need the *2
in this equation. To use weight decay in fastai, just pass wd
in your call to fit
or fit_one_cycle
:x = np.linspace(-2,2,100)
a_s = [1,2,5,10,50]
ys = [a * x**2 for a in a_s]
_,ax = plt.subplots(figsize=(8,6))
for a,y in zip(a_s,ys): ax.plot(x,y, label=f'a={a}')
ax.set_ylim([0,5])
ax.legend();
model = DotProductBias(n_users, n_movies, 50)
learn = Learner(dls, model, loss_func=MSELossFlat())
learn.fit_one_cycle(5, 5e-3, wd=0.1)
class T(Module):
def __init__(self): self.a = torch.ones(3)
L(T().parameters())
type(torch.ones(3)[0])
Module
that we want to treat a tensor as a parameter, we have to wrap it in the nn.Parameter
class. This class doesn't actually add any functionality (other than automatically calling requires_grad_
for us). It's only used as a "marker" to show what to include in parameters
:class T(Module):
def __init__(self): self.a = nn.Parameter(torch.ones(3))
L(T().parameters())
class T(Module):
def __init__(self): self.a = nn.Linear(1, 3, bias=False)
t = T()
L(t.parameters())
type(t.a.weight)
type(t.a.weight.data)
def create_params(size):
return nn.Parameter(torch.zeros(*size).normal_(0, 0.01))
doc(create_params)
class DotProductBias(Module):
def __init__(self, n_users, n_movies, n_factors, y_range=(0,5.5)):
self.user_factors = create_params([n_users, n_factors])
self.user_bias = create_params([n_users])
self.movie_factors = create_params([n_movies, n_factors])
self.movie_bias = create_params([n_movies])
self.y_range = y_range
def forward(self, x):
users = self.user_factors[x[:,0]]
movies = self.movie_factors[x[:,1]]
res = (users*movies).sum(dim=1)
res += self.user_bias[x[:,0]] + self.movie_bias[x[:,1]]
return sigmoid_range(res, *self.y_range)
model = DotProductBias(n_users, n_movies, 50)
learn = Learner(dls, model, loss_func=MSELossFlat())
learn.fit_one_cycle(5, 5e-3, wd=0.1)
movie_bias=learn.model.movie_bias.squeeze()
idxs=movie_bias.argsort()[0:5]
[dls.classes['title'][i] for i in idxs]
idxs = movie_bias.argsort(descending=True)[:5]
[dls.classes['title'][i] for i in idxs]
g = ratings.groupby('title')['rating'].count()
top_movies = g.sort_values(ascending=False).index.values[:1000]
top_idxs = tensor([learn.dls.classes['title'].o2i[m] for m in top_movies])
movie_w = learn.model.movie_factors[top_idxs].cpu().detach()
movie_pca = movie_w.pca(3)
fac0,fac1,fac2 = movie_pca.t()
idxs = list(range(50))
X = fac0[idxs]
Y = fac2[idxs]
plt.figure(figsize=(12,12))
plt.scatter(X, Y)
for i, x, y in zip(top_movies[idxs], X, Y):
plt.text(x,y,i, color=np.random.rand(3)*0.7, fontsize=11)
plt.show()
g = ratings.groupby('title')['rating'].count()
top_movies = g.sort_values(ascending=False).index.values[:1000]
top_idxs = tensor([learn.dls.classes['title'].o2i[m] for m in top_movies])
movie_w = learn.model.movie_factors[top_idxs].cpu().detach()
movie_pca = movie_w.pca(3)
fac0,fac1,fac2 = movie_pca.t()
idxs = list(range(50))
X = fac1[idxs]
Y = fac2[idxs]
plt.figure(figsize=(12,12))
plt.scatter(X, Y)
for i, x, y in zip(top_movies[idxs], X, Y):
plt.text(x,y,i, color=np.random.rand(3)*0.7, fontsize=11)
plt.show()
collab_learner
learn = collab_learner(dls, n_factors=50, y_range=(0, 5.5))
learn.fit_one_cycle(5, 5e-3, wd=0.1)
learn.model
movie_bias = learn.model.i_bias.weight.squeeze()
idxs = movie_bias.argsort(descending=True)[:5]
[dls.classes['title'][i] for i in idxs]
movie_factors = learn.model.i_weight.weight
idx = dls.classes['title'].o2i['Silence of the Lambs, The (1991)']
distances = nn.CosineSimilarity(dim=1)(movie_factors, movie_factors[idx][None])
idx = distances.argsort(descending=True)[1]
dls.classes['title'][idx]
embs = get_emb_sz(dls)
embs
class CollabNN(Module):
def __init__(self, user_sz, item_sz, y_range=(0,5.5), n_act=100):
self.user_factors = Embedding(*user_sz)
self.item_factors = Embedding(*item_sz)
self.layers = nn.Sequential(
nn.Linear(user_sz[1]+item_sz[1], n_act),
nn.ReLU(),
nn.Linear(n_act, 1))
self.y_range = y_range
def forward(self, x):
embs = self.user_factors(x[:,0]),self.item_factors(x[:,1])
x = self.layers(torch.cat(embs, dim=1))
return sigmoid_range(x, *self.y_range)
model = CollabNN(*embs)
learn = Learner(dls, model, loss_func=MSELossFlat())
learn.fit_one_cycle(5, 5e-3, wd=0.01)
use_nn=True
.learn = collab_learner(dls, use_nn=True, y_range=(0, 5.5), layers=[100,50])
learn.fit_one_cycle(5, 5e-3, wd=0.1)