Building a Recommender System, Part 2
This post explores an technique for collaborative filtering which uses latent factor models, a which naturally generalizes to deep learning approaches. Our approach will be implemented using Tensorflow and Keras.
By Matthew Mahowald, Open Data Group
In a previous post, we looked at neighborhood-based methods for building recommender systems. This post explores an alternative technique for collaborative filtering using latent factor models. The technique we’ll use naturally generalizes to deep learning approaches (such as autoencoders), so we’ll also implement our approach using Tensorflow and Keras.
First, let’s load in this data:
Let’s take a quick look at the top 20 most-viewed files:
|2858||American Beauty (1999)||Comedy|Drama|
|260||Star Wars: Episode IV - A New Hope (1977)||Action|Adventure|Fantasy|Sci-Fi|
|1196||Star Wars: Episode V - The Empire Strikes Back…||Action|Adventure|Drama|Sci-Fi|War|
|1210||Star Wars: Episode VI - Return of the Jedi (1983)||Action|Adventure|Romance|Sci-Fi|War|
|480||Jurassic Park (1993)||Action|Adventure|Sci-Fi|
|2028||Saving Private Ryan (1998)||Action|Drama|War|
|589||Terminator 2: Judgment Day (1991)||Action|Sci-Fi|Thriller|
|2571||Matrix, The (1999)||Action|Sci-Fi|Thriller|
|1270||Back to the Future (1985)||Comedy|Sci-Fi|
|593||Silence of the Lambs, The (1991)||Drama|Thriller|
|1580||Men in Black (1997)||Action|Adventure|Comedy|Sci-Fi|
|1198||Raiders of the Lost Ark (1981)||Action|Adventure|
|2762||Sixth Sense, The (1999)||Thriller|
|2396||Shakespeare in Love (1998)||Comedy|Romance|
|1197||Princess Bride, The (1987)||Action|Adventure|Comedy|Romance|
|527||Schindler’s List (1993)||Drama|War|
|1617||L.A. Confidential (1997)||Crime|Film-Noir|Mystery|Thriller|
|1265||Groundhog Day (1993)||Comedy|Romance|
Collaborative filtering models typically work best when each item has a decent number of ratings. Let’s restrict to only the 500 most popular films (as determined by number of ratings). We’ll also reindex by
Next, as mentioned in the previous post, we should normalize our rating data. We create an adjusted rating by subtracting off the overall mean rating, the mean rating for each item, and then the mean rating for each user.
This produces a “preference rating” defined by
The intuition for is that means that user ’s rating for item is exactly what we would guess if all we knew was the average overall ratings, item ratings, and user ratings. Any values above or below 0 indicate deviations in preference from this baseline. To distinguish from the raw rating , I’ll refer to the former as the user’s preference for item and the latter as the user’s rating of item .
Let’s build the preference data using ratings for the 500 most popular films:
The output of this block of code is two objects:
prefs, which is a dataframe of preferences indexed by
pref_matrix, which is a matrix whose th entry corresponds to the rating user gives movie (i.e. the columns are movies and each row is a user). In cases where the user hasn’t rated the item, this matrix will have a
The maximum and minimum preferences in this data are 3.923 and -4.643, respectively. Next, we’ll build an actual model.
Latent-factor collaborative filtering
At this stage, we’ve constructed a matrix (called
pref_matrix in the Python code above). The idea behind latent-factor collaborative filtering models is that each user’s preferences can be predicted by a small number of latent factors (usually much smaller than the overall number of items available):
Latent factor models thus require answering two related questions:
- For a given user , what are the corresponding latent factors ?
- For a given collection of latent factors, what is the function , i.e., what is the relationship between the latent factors and a user’s preferences for each item?
One approach to this problem is to attempt to solve for both the ’s and ’s by making the simplifying assumption that each of these functions is linear:
Taken over all items and users, this can be re-written as a linear algebra problem problem: find matrices and such that
where is the matrix of preferences, is the linear transformation that projects a user’s preferences onto latent variable space, and is the linear transformation that reconstructs the user’s ratings from that user’s representation in latent variable space.
The product will be a square matrix. However, by choosing a number of latent variables strictly less than the number of items, this product will necessarily not be full rank. In essence, we are solving for and such that the product best approximates the identity transformation on the preferences matrix . Our intuition (and hope) is that this will reconstruct accurate preferences for each user. (We will tune our loss function to ensure that this is in fact the case.)
As advertised, we’ll be building our model in Keras + Tensorflow so that we’re well-positioned for any future generalization to deep learning approaches. This is also a natural approach to the type of problem we’re solving: the expression
can be thought of as describing a two-layer dense neural network whose layers are defined by and and whose activation function is just the identity map (i.e. the function ).
First, let’s import the packages we’ll need and set the encoding dimension (the number of latent variables) we want for this model.
Next, define the model itself as a composition of an “encoding” layer (projection onto latent variable space) and a “decoding” layer (recovery of preferences from latent variable representation). The recommender model itself is just a composition of these two layers.
Custom loss functions
At this point, we could train our model directly to just reproduce its inputs (this is essentially a very simple autoencoder). However, we’re actually interested in picking and that correctly fill in missing values. We can do this through a careful application of masking and a custom loss function.
prefs_matrix currently consists largely of NaNs—in fact, there’s only one zero value in the whole dataset:
prefs_matrix, we can fill any missing values with zeros. This is a reasonable choice because we’ve already performed some normalization of the ratings, so 0 represents our naive guess for a user’s preference for a given item. Then, to create training data, use
prefs_matrix as the target and selectively mask nonzero elements in
prefs_matrix to create the input (“forgetting” that particular user-item preference). We can then build a loss function which strongly penalizes incorrectly guessing the “forgotten” values, i.e., one which is trained to construct novel ratings from known ratings. Here’s our function:
By default, the loss this returns is a 20%-80% weighted sum of the overall MSE and the MSE of just the missing ratings. This loss function requires the input (with missing preferences), the predicted preferences, and the true preferences.
At least as of the date of this post, Keras and TensorFlow don’t currently support custom loss functions with three inputs (other frameworks, such as PyTorch, do). We can get around this fact by introducing a “dummy” loss function and a simple wrapper model. Loss functions in Keras require only two inputs, so this dummy function will ignore the “true” values.
Next, our wrapper model. The idea here is to use a lambda layer (‘
loss’) to apply our custom loss function (
'lambda_mse'), and then use our custom loss function for the actual optimization. Using Keras’s functional API makes it very easy to wrap the recommender we already defined with this simple wrapper model.
To generate training data for our model, we’ll start with the preferences matrix
pref_matrix and randomly mask (i.e. set to 0) a certain fraction of the known ratings for each user. Structuring this as a generator allows us to make an essentially unlimited collection of training data (though in each case, the output is constrained to be drawn from the same fixed set of known ratings). Here’s the generator function:
Let’s check that this generator’s masking functionality is working correctly:
To complete the story, we’ll define a training function that calls this generator and allows us to set a few other parameters (number of epochs, early stopping, etc):
Recall that and are and dimensional matrices, respectively, so this model has parameters. A good rule of thumb with linear models is to have at least 10 observations per parameter, meaning we’d like to see 250,000 individual user ratings vectors during training. We don’t have nearly enough users for that, though, so for this tutorial, we’ll skimp by quite a bit—let’s settle for a maximum of 12,500 observations (stopping the model earlier if loss doesn’t improve).
The output of this training process (at least on my machine) gives a loss of 0.6321, which means that on average we’re within about 0.7901 units of a user’s true preference when we haven’t seen it before (recall that this loss is 80% from unknown preferences, and 20% from the knowns). Preferences in our data range between -4.64 and 3.92, so this is not too shabby!
To generate a prediction with our model, we have to call the
recommender model we trained earlier after normalizing the ratings along the various dimensions. Let’s assume that the input to our predict function will be a dataframe indexed by (
userid), and with a single column named
Let’s test it out! Here’s some sample ratings for a single fake user, who really likes Star Wars and Jurassic Park and doesn’t like much else:
|260||4.008329||Star Wars: Episode IV - A New Hope (1977)|
|1198||3.942005||Raiders of the Lost Ark (1981)|
|1196||3.860034||Star Wars: Episode V - The Empire Strikes Back…|
|1148||3.716259||Wrong Trousers, The (1993)|
|904||3.683811||Rear Window (1954)|
|2019||3.654374||Seven Samurai (The Magnificent Seven) (Shichin…|
|913||3.639756||Maltese Falcon, The (1941)|
|318||3.637150||Shawshank Redemption, The (1994)|
|745||3.619762||Close Shave, A (1995)|
|908||3.608473||North by Northwest (1959)|
Interestingly, even though the user gave Star Wars a 5 as input, the model only predicts a rating of 4.08 for Star Wars. But it does recommend the Empire Strikes Back and Raiders of the Lost Ark, which seem like reasonable recommendations for those preferences.
Now let’s reverse this user’s ratings for Star Wars and Jurassic Park, and see how the ratings change:
|2019||3.532214||Seven Samurai (The Magnificent Seven) (Shichin…|
|50||3.489284||Usual Suspects, The (1995)|
|2858||3.480124||American Beauty (1999)|
|745||3.466157||Close Shave, A (1995)|
|1148||3.415981||Wrong Trousers, The (1993)|
|1197||3.415527||Princess Bride, The (1987)|
|527||3.386785||Schindler’s List (1993)|
|750||3.342154||Dr. Strangelove or: How I Learned to Stop Worr…|
|1207||3.335204||To Kill a Mockingbird (1962)|
Note that Seven Samurai features prominently in both lists. In fact, Seven Samurai has the highest average rating of any film in this dataset (at 4.56), and looking at the top 20 or top 50 recommended films for both users has even more common films showing up that happen to be very highly rated overall.
Conclusions and further reading
The latent factor representation we’ve built can also be thought of as defining an embedding of items into some lower-dimensional space, as opposed to an embedding of users. This lets us do some interesting things—for example, we can compare distances between each item’s vector representation to understand how similar or different two films are. Let’s compare Star Wars against The Empire Strikes Back and American Beauty:
Note that 33 is the column index corresponding to Star Wars (different from its
movieid of 260), 144 is the column index corresponding to Empire Strikes Back, and 401 is the column index of American Beauty.
Comparing the distances, we see that with a distance of 0.209578, Star Wars and Empire Strikes Back are much closer in latent factor space than Star Wars and American Beauty are.
With a little bit of further work, it’s also possible to answer other questions in latent factor space like “which film is least similar to Star Wars?”
Variations on this type of technique lead to autoencoder-based recommender systems. For futher reading, there’s also a family of related models known as matrix factorization models, which can incorporate both item and user features as well as the raw ratings.
- Building a Recommender System
- K-Means Clustering: Unsupervised Learning for Recommender Systems
- Building Recommender systems with Azure Machine Learning service