Movie Recommendation System
By: Janine B
Tags: Movies, Neural Networks, Data Science, School Project, Ut Austin

By Janine Bariuan, Kevin Chen, Bibartan Jha, Sudeep Narala, Rooshi Patidar, and Diana Shao

Introduction

For this project, our team decided to try and create a movie recommendation system that utilizes multiple datasets to output a list of movies recommended for the user. We found some datasets online, including the MovieLens 100K Dataset and Movie Poster dataset that we eventually used for our final model. By using these datasets, combined with scraped data, and implementing two different models, we were able to create a thorough movie recommendation system.

Follow along with our Github repo here.

Datasets

There were two existing datasets that we used: the MovieLens 100K Dataset and the Movie Poster Dataset.

MovieLens 100K Dataset

The Movielens 100K Dataset comprises of 100,000 ratings for 1,600+ movies made by 900+ users. The official website can be found here.

We used the items (movies), ratings, and users datasets.

Movie Poster Dataset

This dataset takes all of the movies found in the MovieLens 100K Dataset and provides a URL to the poster image for that movie. The link to the official dataset can be found here.

We used the movie_poster.csv file.

Pre-Processing

MovieLens 100K Dataset

Since the MovieLens dataset consists of thousands of ratings and movies, we wanted to find any trends that occurred in the data. Therefore, we could then transform the dataset according to this information.

One of the first things we wanted to see was the frequency of the number of ratings for movies.

We found that there was a high number of movies that were only given one rating, which could only be seen as noise when trying to create rating predictions. To adjust this, we removed all of the movies from the ratings data that had less than 15 total ratings from users.

Other steps we took included:

Removing the unix timestamp from the dataframes, as it was not a factor in making recommendations

Fixed training dataframe for Matrix Factorization model

0-indexing the data to be more consistent

And changing the user_id and movie_id columns to integer types to make it easier to work with the data

Movie Poster Dataset

With this dataset, we pre-processed the training set X_train by encoding the poster images retrieved from the movie posters dataset. For this purpose, we used a pre-processing function within the Keras library called “applications.vgg19.preprocess_input()”. Since the VGG19 CNN model uses the ImageNet dataset as weights, this pre-processing function aligns the images encoded within X_train to better suit ImageNet by converting the images from RGB to BGR and zero-centering the color channels with respect to ImageNet.

This pre-processing technique enabled us to better optimize our CNN and KNN models. The main evaluation metric we used to test the accuracy of our models is the mean distance from all the neighbors for each query. The smaller the mean distance, the greater the accuracy of the models.

The evaluation metric for models will be discussed in more detail in the “Movie Posters Model” section below.

The two graphs below show the “mean distance from neighbors” results of the first 100 images. These results are derived from the same VGG19 model (using the same parameters). The top graph is derived from the model prior to applying pre-processing to the X_train data, and the bottom graph is from after applying pre-processing.

Prior to pre-processing

After pre-processing

As evidenced by the graphs above, the mean distances from neighbors for the first 100 posters is much higher prior to pre-processing than after pre-processing. This validates that the pre-processing function optimized the CNN and KNN models.

Scraping

We decided to increase the amount and scope of our data by scraping some overall movie ratings on IMDB and Rotten Tomatoes and factor those into our model. We scraped all available ratings for the movies listed in the MovieLens 100K dataset.

IMDB

For the IMDB scraping, we used the requests library (a simple HTTP library for Python) to access the IMDB search page and query by the movie title.

We found that the format of the titles in the MovieLens dataset were not suitable for searching, so we removed the year in parentheses to get better results.

Then, once we had the response page in HTML format, we used the BeautifulSoup library to easily parse through the page and find the search results. We then iterated through the results until the result year matched the query’s year, and opened the respective url using the urlopen method from the urllib library.

Finally, it was a matter of using BeautifulSoup again to find the specific ratings on the IMDB movie page. Since IMDB has two types of ratings, a user rating and a metascore (critic rating), we attempted to scrape both, if available. Some movies weren’t popular enough to garner a metascore, so those were just represented as NaN. Additionally, some movies couldn’t be found at all, which turned out to mostly bedue to mismatched years or movies with alternate names. These were added into the dataset manually by searching IMDB or Google and inputting them by hand into the .csv file.

Here is a visual of the resulting columns that were created from the IMDB scraping:

Scraped IMDB Data

Rotten Tomatoes

For the Rotten Tomatoes scraping, we followed a similar process as with IMDB. We utilized the requests library and a public API endpoint of Rotten Tomatoes to query the search results of their database and find a movie title at a specific year. We encountered similar problems with the MovieLens format, but likewise removed special characters and extraneous elements of the movie titles as we did when working with IMDB.

Rotten Tomatoes’ API allowed us to work with a JSON format of the movie results, so finding the URL for a specific movie was as simple as navigating the JSON. We then were able to use BeautifulSoup at that URL to parse for the Rotten Tomatoes critic and audience scores easily.

We, however, ran into the issue of having sent too many requests to the Rotten Tomatoes API and subsequently had some of our IPs blacklisted from their server, causing us to be unable to even visit their web page regularly. We ended up deciding that having 2 more ratings in addition to the ratings that we already had from IMDB and users would not produce too much of a difference that merited using a VPN to gain access and continue scraping. Instead, we decided to focus more on other parts of our system.

System Overview

Above is a high level overview of our recommendation system. It begins with the input query made by the user who wants to receive recommendations. This input gets used in our first model that makes predicted ratings and recommendations based off of movie poster information. This output from the first model was then formatted to a new dataframe that holds the recommended movies and their predicted ratings for this new user. This data, in addition to the original user input, gets used in the matrix factorization model to get user and movie embeddings. From this model, a final output of the top 5 recommended movies is made. Each model and the input query is explained in more detail in the following sections.

Input Query

Our recommendation system works for a single user, based on the ratings of previous movies they have watched. This input was obtained using this Google Form, listing 15 movies and asking the user to rate each one from 1–5, 1 for hating the movie and 5 for loving it.

The movies listed on the Google Form were chosen from the top 15 most-rated movies in the dataset that we found. We chose this so there would be a more accurate prediction since these movies would appear in the (user, movie) embedding space more frequently.

It would also allow for a consistent list that users would rate, and would not add to the noise we removed if a user inputted their own favorite movies that has a low amount of ratings in the existing dataset.

We then transformed the input query into a dictionary of the movie ID’s for those top 15 movies, with the rating of the user.