{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.7.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":35725,"databundleVersionId":3597961,"sourceType":"competition"}],"dockerImageVersionId":30203,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"from fastai.collab import *\nfrom fastai.tabular.all import *\nfrom os.path import expanduser, join, dirname, basename\nset_seed(42)","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:44:03.282594Z","iopub.execute_input":"2024-08-01T15:44:03.282908Z","iopub.status.idle":"2024-08-01T15:44:06.069484Z","shell.execute_reply.started":"2024-08-01T15:44:03.282835Z","shell.execute_reply":"2024-08-01T15:44:06.068582Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# NOTE! This is just a copy of the original [collaborative-filtering-deep-dive](https://www.kaggle.com/code/jhoward/collaborative-filtering-deep-dive). \n# **Please upvote the original [collaborative-filtering-deep-dive](https://www.kaggle.com/code/jhoward/collaborative-filtering-deep-dive).** ","metadata":{}},{"cell_type":"markdown","source":"> *This is chapter 8 of the book [Practical Deep Learning for Coders](https://www.amazon.com/Deep-Learning-Coders-fastai-PyTorch/dp/1492045527), provided courtesy of O'Reilly Media. The full book is [available as Jupyter Notebooks](https://github.com/fastai/fastbook/). A free course that covers the book is [available here](https://course.fast.ai/).*\n>\n> Click **Copy and Edit** in the top right to run the code in this notebook yourself.","metadata":{}},{"cell_type":"markdown","source":"## Code needed for the following Lesson-8a","metadata":{}},{"cell_type":"code","source":"path = \"/kaggle/input/fmi-su-recommender-system-hw-22022/\"","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:44:06.071579Z","iopub.execute_input":"2024-08-01T15:44:06.072523Z","iopub.status.idle":"2024-08-01T15:44:06.078203Z","shell.execute_reply.started":"2024-08-01T15:44:06.072477Z","shell.execute_reply":"2024-08-01T15:44:06.077275Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"path = Path(path)","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:44:06.079643Z","iopub.execute_input":"2024-08-01T15:44:06.080358Z","iopub.status.idle":"2024-08-01T15:44:06.094344Z","shell.execute_reply.started":"2024-08-01T15:44:06.080314Z","shell.execute_reply":"2024-08-01T15:44:06.093230Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"ratings = pd.read_csv(path/'ratings_train.csv', index_col=0)\nratings.head()             ","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:44:06.097239Z","iopub.execute_input":"2024-08-01T15:44:06.097981Z","iopub.status.idle":"2024-08-01T15:44:07.197048Z","shell.execute_reply.started":"2024-08-01T15:44:06.097940Z","shell.execute_reply":"2024-08-01T15:44:07.195957Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"books = pd.read_csv(path/'books.csv')\nbooks.head()","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:44:07.198505Z","iopub.execute_input":"2024-08-01T15:44:07.199063Z","iopub.status.idle":"2024-08-01T15:44:08.129407Z","shell.execute_reply.started":"2024-08-01T15:44:07.199022Z","shell.execute_reply":"2024-08-01T15:44:08.128343Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"ratings = pd.merge(ratings, books, on='Book_Id', how='inner')\nratings.head()","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:44:08.130580Z","iopub.execute_input":"2024-08-01T15:44:08.130864Z","iopub.status.idle":"2024-08-01T15:44:08.816668Z","shell.execute_reply.started":"2024-08-01T15:44:08.130838Z","shell.execute_reply":"2024-08-01T15:44:08.815676Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"dls = CollabDataLoaders.from_df(ratings, item_name='Title', bs=64)\ndls.show_batch()","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:44:08.818062Z","iopub.execute_input":"2024-08-01T15:44:08.818473Z","iopub.status.idle":"2024-08-01T15:44:12.331650Z","shell.execute_reply.started":"2024-08-01T15:44:08.818433Z","shell.execute_reply":"2024-08-01T15:44:12.330706Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"n_users  = len(dls.classes['User_Id'])\nn_books = len(dls.classes['Title'])\nn_factors = 5\n\nuser_factors = torch.randn(n_users, n_factors)\nbook_factors = torch.randn(n_books, n_factors)","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:44:12.332999Z","iopub.execute_input":"2024-08-01T15:44:12.333319Z","iopub.status.idle":"2024-08-01T15:44:12.350852Z","shell.execute_reply.started":"2024-08-01T15:44:12.333290Z","shell.execute_reply":"2024-08-01T15:44:12.350085Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# --- START OF Lesson-8 video --------","metadata":{}},{"cell_type":"markdown","source":"### Creating Our Own Embedding Module","metadata":{}},{"cell_type":"markdown","source":"So far, we've used `Embedding` without thinking about how it really works. Let's re-create `DotProductBias` *without* using this class. We'll need a randomly initialized weight matrix for each of the embeddings. We have to be careful, however. Recall from <<chapter_mnist_basics>> that optimizers require that they can get all the parameters of a module from the module's `parameters` method. However, this does not happen fully automatically. If we just add a tensor as an attribute to a `Module`, it will not be included in `parameters`:","metadata":{}},{"cell_type":"code","source":"class T(Module):\n    def __init__(self): \n        self.a = torch.ones(3)\n\nL(T().parameters())","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:44:12.352208Z","iopub.execute_input":"2024-08-01T15:44:12.352534Z","iopub.status.idle":"2024-08-01T15:44:12.359649Z","shell.execute_reply.started":"2024-08-01T15:44:12.352506Z","shell.execute_reply":"2024-08-01T15:44:12.358727Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"To tell `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`:","metadata":{}},{"cell_type":"code","source":"class T(Module):\n    def __init__(self): \n        self.a = nn.Parameter(torch.ones(3))\n\nL(T().parameters())","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:44:12.378875Z","iopub.execute_input":"2024-08-01T15:44:12.379189Z","iopub.status.idle":"2024-08-01T15:44:12.387806Z","shell.execute_reply.started":"2024-08-01T15:44:12.379160Z","shell.execute_reply":"2024-08-01T15:44:12.386660Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# to view docs without source code\n?nn.Parameter\n# or\nnn.Parameter?","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:44:12.388921Z","iopub.execute_input":"2024-08-01T15:44:12.389578Z","iopub.status.idle":"2024-08-01T15:44:12.462593Z","shell.execute_reply.started":"2024-08-01T15:44:12.389545Z","shell.execute_reply":"2024-08-01T15:44:12.461292Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"All PyTorch modules use `nn.Parameter` for any trainable parameters, which is why we haven't needed to explicitly use this wrapper up until now:","metadata":{}},{"cell_type":"code","source":"# to view docs WITH source code\n??nn.Parameter\n# or\nnn.Parameter??","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:44:12.463884Z","iopub.execute_input":"2024-08-01T15:44:12.464171Z","iopub.status.idle":"2024-08-01T15:44:12.495250Z","shell.execute_reply.started":"2024-08-01T15:44:12.464144Z","shell.execute_reply":"2024-08-01T15:44:12.494312Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"class T(Module):\n    def __init__(self): \n        # NOTE! bias=False. Try bias=True and see what happens\n        self.a = nn.Linear(1, 3, bias=False)\n        # NOTE! Try bias=True and see what happens\n#         self.a = nn.Linear(1, 3, bias=True)\n\nt = T()\nL(t.parameters())","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:44:12.496516Z","iopub.execute_input":"2024-08-01T15:44:12.496866Z","iopub.status.idle":"2024-08-01T15:44:12.506451Z","shell.execute_reply.started":"2024-08-01T15:44:12.496829Z","shell.execute_reply":"2024-08-01T15:44:12.505552Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"type(t.a.weight)","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:44:12.507576Z","iopub.execute_input":"2024-08-01T15:44:12.507954Z","iopub.status.idle":"2024-08-01T15:44:12.514870Z","shell.execute_reply.started":"2024-08-01T15:44:12.507914Z","shell.execute_reply":"2024-08-01T15:44:12.513812Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"We can create a tensor as a parameter, with random initialization, like so:","metadata":{}},{"cell_type":"code","source":"def create_params(size):\n    return nn.Parameter(torch.zeros(*size).normal_(0, 0.01))","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:44:12.516047Z","iopub.execute_input":"2024-08-01T15:44:12.516337Z","iopub.status.idle":"2024-08-01T15:44:12.525373Z","shell.execute_reply.started":"2024-08-01T15:44:12.516310Z","shell.execute_reply":"2024-08-01T15:44:12.524497Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Let's use this to create `DotProductBias` again, but without `Embedding`:","metadata":{}},{"cell_type":"code","source":"class DotProductBias(Module):\n    def __init__(self, n_users, n_books, n_factors, y_range=(0,5.5)):\n        self.user_factors = create_params([n_users, n_factors])\n        self.user_bias = create_params([n_users])\n        self.book_factors = create_params([n_books, n_factors])\n        self.book_bias = create_params([n_books])\n        self.y_range = y_range\n        \n    def forward(self, x):\n        users = self.user_factors[x[:,0]]\n        books = self.book_factors[x[:,1]]\n        res = (users*books).sum(dim=1)\n        res += self.user_bias[x[:,0]] + self.book_bias[x[:,1]]\n        return sigmoid_range(res, *self.y_range)","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:44:12.526608Z","iopub.execute_input":"2024-08-01T15:44:12.526971Z","iopub.status.idle":"2024-08-01T15:44:12.536461Z","shell.execute_reply.started":"2024-08-01T15:44:12.526936Z","shell.execute_reply":"2024-08-01T15:44:12.535530Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Then let's train it again to check we get around the same results we saw in the previous section:","metadata":{}},{"cell_type":"code","source":"n_factors = 50\n# n_factors = 3  # Try this\nmodel = DotProductBias(n_users, n_books, n_factors=n_factors)\n# model = DotProductBias(n_users, n_movies, 50)\nlearn = Learner(dls, model, loss_func=MSELossFlat())\nlearn.fit_one_cycle(5, 5e-3, wd=0.1)","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:44:12.537834Z","iopub.execute_input":"2024-08-01T15:44:12.538235Z","iopub.status.idle":"2024-08-01T15:51:14.533469Z","shell.execute_reply.started":"2024-08-01T15:44:12.538198Z","shell.execute_reply":"2024-08-01T15:51:14.532496Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"model.book_bias","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:51:14.534621Z","iopub.execute_input":"2024-08-01T15:51:14.534905Z","iopub.status.idle":"2024-08-01T15:51:14.557361Z","shell.execute_reply.started":"2024-08-01T15:51:14.534878Z","shell.execute_reply":"2024-08-01T15:51:14.556429Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Now, let's take a look at what our model has learned.","metadata":{}},{"cell_type":"markdown","source":"## Interpreting Embeddings and Biases","metadata":{}},{"cell_type":"markdown","source":"Our model is already useful, in that it can provide us with movie recommendations for our users—but it is also interesting to see what parameters it has discovered. The easiest to interpret are the biases. Here are the movies with the lowest values in the bias vector:","metadata":{}},{"cell_type":"code","source":"book_bias = learn.model.book_bias.squeeze()\nidxs = book_bias.argsort()[:5]\n[dls.classes['Title'][i] for i in idxs]","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:51:14.558557Z","iopub.execute_input":"2024-08-01T15:51:14.560247Z","iopub.status.idle":"2024-08-01T15:51:14.574316Z","shell.execute_reply.started":"2024-08-01T15:51:14.560217Z","shell.execute_reply":"2024-08-01T15:51:14.573422Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"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:","metadata":{}},{"cell_type":"code","source":"idxs = book_bias.argsort(descending=True)[:5]\n[dls.classes['Title'][i] for i in idxs]","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:51:14.575627Z","iopub.execute_input":"2024-08-01T15:51:14.576082Z","iopub.status.idle":"2024-08-01T15:51:14.584206Z","shell.execute_reply.started":"2024-08-01T15:51:14.576044Z","shell.execute_reply":"2024-08-01T15:51:14.583270Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"So, for instance, even if you don't normally enjoy detective movies, you might enjoy *LA Confidential*!\n\nIt 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](https://github.com/fastai/numerical-linear-algebra). Here's what our movies look like based on two of the strongest PCA components.","metadata":{}},{"cell_type":"code","source":"g = ratings.groupby('Title')['Rating'].count()\ntop_books = g.sort_values(ascending=False).index.values[:1000]\ntop_idxs = tensor([learn.dls.classes['Title'].o2i[m] for m in top_books])\nbook_w = learn.model.book_factors[top_idxs].cpu().detach()\nbook_pca = book_w.pca(3)\nfac0,fac1,fac2 = book_pca.t()\nidxs = list(range(50))\nX = fac0[idxs]\nY = fac2[idxs]\nplt.figure(figsize=(12,12))\nplt.scatter(X, Y)\nfor i, x, y in zip(top_books[idxs], X, Y):\n    plt.text(x,y,i, color=np.random.rand(3)*0.7, fontsize=11)\nplt.show()","metadata":{"hide_input":true,"execution":{"iopub.status.busy":"2024-08-01T15:51:14.585430Z","iopub.execute_input":"2024-08-01T15:51:14.585762Z","iopub.status.idle":"2024-08-01T15:51:15.880755Z","shell.execute_reply.started":"2024-08-01T15:51:14.585734Z","shell.execute_reply":"2024-08-01T15:51:15.879820Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"We can see here that the model seems to have discovered a concept of *classic* versus *pop culture* movies, or perhaps it is *critically acclaimed* that is represented here.","metadata":{}},{"cell_type":"markdown","source":"> j: No matter how many models I train, I never stop getting moved and surprised by how these randomly initialized bunches of numbers, trained with such simple mechanics, manage to discover things about my data all by themselves. It almost seems like cheating, that I can create code that does useful things without ever actually telling it how to do those things!","metadata":{}},{"cell_type":"markdown","source":"We defined our model from scratch to teach you what is inside, but you can directly use the fastai library to build it. We'll look at how to do that next.","metadata":{}},{"cell_type":"code","source":"# Very interesting!\n# Try to set n_factors = 3 and rerun the above cells. ","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:51:15.881961Z","iopub.execute_input":"2024-08-01T15:51:15.882257Z","iopub.status.idle":"2024-08-01T15:51:15.886215Z","shell.execute_reply.started":"2024-08-01T15:51:15.882229Z","shell.execute_reply":"2024-08-01T15:51:15.885316Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Using fastai.collab","metadata":{}},{"cell_type":"markdown","source":"We can create and train a collaborative filtering model using the exact structure shown earlier by using fastai's `collab_learner`:","metadata":{}},{"cell_type":"code","source":"learn = collab_learner(dls, n_factors=50, y_range=(0, 5.5))","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:51:15.887324Z","iopub.execute_input":"2024-08-01T15:51:15.887640Z","iopub.status.idle":"2024-08-01T15:51:16.134883Z","shell.execute_reply.started":"2024-08-01T15:51:15.887612Z","shell.execute_reply":"2024-08-01T15:51:16.134010Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Remember to look for a good lr\nlearn.lr_find()","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:51:16.136301Z","iopub.execute_input":"2024-08-01T15:51:16.136759Z","iopub.status.idle":"2024-08-01T15:51:18.825770Z","shell.execute_reply.started":"2024-08-01T15:51:16.136718Z","shell.execute_reply":"2024-08-01T15:51:18.824811Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"learn.fit_one_cycle(5, 5e-3, wd=0.1)","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:51:18.826819Z","iopub.execute_input":"2024-08-01T15:51:18.827082Z","iopub.status.idle":"2024-08-01T15:57:56.942129Z","shell.execute_reply.started":"2024-08-01T15:51:18.827056Z","shell.execute_reply":"2024-08-01T15:57:56.941167Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"The names of the layers can be seen by printing the model:","metadata":{}},{"cell_type":"code","source":"learn.model","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:57:56.943441Z","iopub.execute_input":"2024-08-01T15:57:56.943842Z","iopub.status.idle":"2024-08-01T15:57:56.950377Z","shell.execute_reply.started":"2024-08-01T15:57:56.943803Z","shell.execute_reply":"2024-08-01T15:57:56.949418Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"We can use these to replicate any of the analyses we did in the previous section—for instance:","metadata":{}},{"cell_type":"code","source":"book_bias = learn.model.i_bias.weight.squeeze()\nidxs = book_bias.argsort(descending=True)[:5]\n[dls.classes['Title'][i] for i in idxs]","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:57:56.951695Z","iopub.execute_input":"2024-08-01T15:57:56.952004Z","iopub.status.idle":"2024-08-01T15:57:56.964563Z","shell.execute_reply.started":"2024-08-01T15:57:56.951962Z","shell.execute_reply":"2024-08-01T15:57:56.963608Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Another interesting thing we can do with these learned embeddings is to look at _distance_.","metadata":{}},{"cell_type":"code","source":"collab_learner??","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:57:56.970626Z","iopub.execute_input":"2024-08-01T15:57:56.970909Z","iopub.status.idle":"2024-08-01T15:57:56.990555Z","shell.execute_reply.started":"2024-08-01T15:57:56.970882Z","shell.execute_reply":"2024-08-01T15:57:56.989591Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"EmbeddingDotBias??","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:57:56.991753Z","iopub.execute_input":"2024-08-01T15:57:56.992096Z","iopub.status.idle":"2024-08-01T15:57:57.026285Z","shell.execute_reply.started":"2024-08-01T15:57:56.992062Z","shell.execute_reply":"2024-08-01T15:57:57.025457Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Embedding Distance","metadata":{}},{"cell_type":"markdown","source":"On a two-dimensional map we can calculate the distance between two coordinates using the formula of Pythagoras: $\\sqrt{x^{2}+y^{2}}$ (assuming that *x* and *y* are the distances between the coordinates on each axis). For a 50-dimensional embedding we can do exactly the same thing, except that we add up the squares of all 50 of the coordinate distances.\n\nIf there were two movies that were nearly identical, then their embedding vectors would also have to be nearly identical, because the users that would like them would be nearly exactly the same. There is a more general idea here: movie similarity can be defined by the similarity of users that like those movies. And that directly means that the distance between two movies' embedding vectors can define that similarity. We can use this to find the most similar movie to *Silence of the Lambs*:","metadata":{}},{"cell_type":"code","source":"movie_factors = learn.model.i_weight.weight\nidx = dls.classes['Title'].o2i['Silence of the Lambs, The (1991)']\ndistances = nn.CosineSimilarity(dim=1)(book_factors, book_factors[idx][None])\nidx = distances.argsort(descending=True)[1]\ndls.classes['Title'][idx]","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:57:57.027433Z","iopub.execute_input":"2024-08-01T15:57:57.028039Z","iopub.status.idle":"2024-08-01T15:57:57.073520Z","shell.execute_reply.started":"2024-08-01T15:57:57.028002Z","shell.execute_reply":"2024-08-01T15:57:57.072597Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Now that we have succesfully trained a model, let's see how to deal with the situation where we have no data for a user. How can we make recommendations to new users?","metadata":{}},{"cell_type":"markdown","source":"## Bootstrapping a Collaborative Filtering Model","metadata":{}},{"cell_type":"markdown","source":"The biggest challenge with using collaborative filtering models in practice is the *bootstrapping problem*. The most extreme version of this problem is when you have no users, and therefore no history to learn from. What products do you recommend to your very first user?\n\nBut even if you are a well-established company with a long history of user transactions, you still have the question: what do you do when a new user signs up? And indeed, what do you do when you add a new product to your portfolio? There is no magic solution to this problem, and really the solutions that we suggest are just variations of *use your common sense*. You could assign new users the mean of all of the embedding vectors of your other users, but this has the problem that that particular combination of latent factors may be not at all common (for instance, the average for the science-fiction factor may be high, and the average for the action factor may be low, but it is not that common to find people who like science-fiction without action). Better would probably be to pick some particular user to represent *average taste*.\n\nBetter still is to use a tabular model based on user meta data to construct your initial embedding vector. When a user signs up, think about what questions you could ask them that could help you to understand their tastes. Then you can create a model where the dependent variable is a user's embedding vector, and the independent variables are the results of the questions that you ask them, along with their signup metadata. We will see in the next section how to create these kinds of tabular models. (You may have noticed that when you sign up for services such as Pandora and Netflix, they tend to ask you a few questions about what genres of movie or music you like; this is how they come up with your initial collaborative filtering recommendations.)","metadata":{}},{"cell_type":"markdown","source":"One thing to be careful of is that a small number of extremely enthusiastic users may end up effectively setting the recommendations for your whole user base. This is a very common problem, for instance, in movie recommendation systems. People that watch anime tend to watch a whole lot of it, and don't watch very much else, and spend a lot of time putting their ratings on websites. As a result, anime tends to be heavily overrepresented in a lot of *best ever movies* lists. In this particular case, it can be fairly obvious that you have a problem of representation bias, but if the bias is occurring in the latent factors then it may not be obvious at all.\n\nSuch a problem can change the entire makeup of your user base, and the behavior of your system. This is particularly true because of positive feedback loops. If a small number of your users tend to set the direction of your recommendation system, then they are naturally going to end up attracting more people like them to your system. And that will, of course, amplify the original representation bias. This type of bias has a natural tendency to be amplified exponentially. You may have seen examples of company executives expressing surprise at how their online platforms rapidly deteriorated in such a way that they expressed values at odds with the values of the founders. In the presence of these kinds of feedback loops, it is easy to see how such a divergence can happen both quickly and in a way that is hidden until it is too late.\n\nIn a self-reinforcing system like this, we should probably expect these kinds of feedback loops to be the norm, not the exception. Therefore, you should assume that you will see them, plan for that, and identify up front how you will deal with these issues. Try to think about all of the ways in which feedback loops may be represented in your system, and how you might be able to identify them in your data. In the end, this is coming back to our original advice about how to avoid disaster when rolling out any kind of machine learning system. It's all about ensuring that there are humans in the loop; that there is careful monitoring, and a gradual and thoughtful rollout.","metadata":{}},{"cell_type":"markdown","source":"Our dot product model works quite well, and it is the basis of many successful real-world recommendation systems. This approach to collaborative filtering is known as *probabilistic matrix factorization* (PMF). Another approach, which generally works similarly well given the same data, is deep learning.","metadata":{}},{"cell_type":"markdown","source":"## Deep Learning for Collaborative Filtering","metadata":{}},{"cell_type":"markdown","source":"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.\n\nSince 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:","metadata":{}},{"cell_type":"code","source":"embs = get_emb_sz(dls)\nembs","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:57:57.074636Z","iopub.execute_input":"2024-08-01T15:57:57.074913Z","iopub.status.idle":"2024-08-01T15:57:57.081082Z","shell.execute_reply.started":"2024-08-01T15:57:57.074886Z","shell.execute_reply":"2024-08-01T15:57:57.080090Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Let's implement this class:","metadata":{}},{"cell_type":"code","source":"class CollabNN(Module):\n    def __init__(self, user_sz, item_sz, y_range=(0,5.5), n_act=100):\n        self.user_factors = Embedding(*user_sz)\n        self.item_factors = Embedding(*item_sz)\n        self.layers = nn.Sequential(\n            nn.Linear(user_sz[1]+item_sz[1], n_act),\n            nn.ReLU(),\n            nn.Linear(n_act, 1))\n        self.y_range = y_range\n        \n    def forward(self, x):\n        embs = self.user_factors(x[:,0]),self.item_factors(x[:,1])\n        x = self.layers(torch.cat(embs, dim=1))\n        return sigmoid_range(x, *self.y_range)","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:57:57.082393Z","iopub.execute_input":"2024-08-01T15:57:57.082714Z","iopub.status.idle":"2024-08-01T15:57:57.091113Z","shell.execute_reply.started":"2024-08-01T15:57:57.082688Z","shell.execute_reply":"2024-08-01T15:57:57.090317Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"And use it to create a model:","metadata":{}},{"cell_type":"code","source":"model = CollabNN(*embs)","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:57:57.092381Z","iopub.execute_input":"2024-08-01T15:57:57.092792Z","iopub.status.idle":"2024-08-01T15:57:59.939367Z","shell.execute_reply.started":"2024-08-01T15:57:57.092752Z","shell.execute_reply":"2024-08-01T15:57:59.938307Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"`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 <<chapter_mnist_basics>> 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.\n\nLet's see if it trains:","metadata":{}},{"cell_type":"code","source":"learn = Learner(dls, model, loss_func=MSELossFlat())\nlearn.fit_one_cycle(5, 5e-3, wd=0.01)","metadata":{"execution":{"iopub.status.busy":"2024-08-01T15:57:59.940737Z","iopub.execute_input":"2024-08-01T15:57:59.941045Z","iopub.status.idle":"2024-08-01T16:28:57.817583Z","shell.execute_reply.started":"2024-08-01T15:57:59.941016Z","shell.execute_reply":"2024-08-01T16:28:57.816601Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"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:","metadata":{}},{"cell_type":"code","source":"learn = collab_learner(dls, use_nn=True, y_range=(0, 5.5), layers=[100,50])\nlearn.fit_one_cycle(5, 5e-3, wd=0.1)","metadata":{"execution":{"iopub.status.busy":"2024-08-01T16:28:57.818912Z","iopub.execute_input":"2024-08-01T16:28:57.819229Z","iopub.status.idle":"2024-08-01T17:01:13.517662Z","shell.execute_reply.started":"2024-08-01T16:28:57.819199Z","shell.execute_reply":"2024-08-01T17:01:13.516610Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"`learn.model` is an object of type `EmbeddingNN`. Let's take a look at fastai's code for this class:","metadata":{}},{"cell_type":"code","source":"@delegates(TabularModel)\nclass EmbeddingNN(TabularModel):\n    def __init__(self, emb_szs, layers, **kwargs):\n        super().__init__(emb_szs, layers=layers, n_cont=0, out_sz=1, **kwargs)","metadata":{"execution":{"iopub.status.busy":"2024-08-01T17:01:13.519082Z","iopub.execute_input":"2024-08-01T17:01:13.519682Z","iopub.status.idle":"2024-08-01T17:01:13.526445Z","shell.execute_reply.started":"2024-08-01T17:01:13.519642Z","shell.execute_reply":"2024-08-01T17:01:13.525248Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"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.","metadata":{}},{"cell_type":"markdown","source":"### Sidebar: kwargs and Delegates","metadata":{}},{"cell_type":"markdown","source":"`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](https://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.plot) says \"The `kwargs` are `Line2D` properties\" and then lists those properties.\n\nWe'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.\n\nfastai 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.","metadata":{}},{"cell_type":"markdown","source":"### End sidebar","metadata":{}},{"cell_type":"markdown","source":"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.","metadata":{}},{"cell_type":"markdown","source":"## Conclusion","metadata":{}},{"cell_type":"markdown","source":"For our first non-computer vision application, we looked at recommendation systems and saw how gradient descent can learn intrinsic factors or biases about items from a history of ratings. Those can then give us information about the data. \n\nWe also built our first model in PyTorch. We will do a lot more of this in the next section of the book, but first, let's finish our dive into the other general applications of deep learning, continuing with tabular data.","metadata":{}},{"cell_type":"markdown","source":"## Questionnaire","metadata":{}},{"cell_type":"markdown","source":"1. What problem does collaborative filtering solve?\n1. How does it solve it?\n1. Why might a collaborative filtering predictive model fail to be a very useful recommendation system?\n1. What does a crosstab representation of collaborative filtering data look like?\n1. Write the code to create a crosstab representation of the MovieLens data (you might need to do some web searching!).\n1. What is a latent factor? Why is it \"latent\"?\n1. What is a dot product? Calculate a dot product manually using pure Python with lists.\n1. What does `pandas.DataFrame.merge` do?\n1. What is an embedding matrix?\n1. What is the relationship between an embedding and a matrix of one-hot-encoded vectors?\n1. Why do we need `Embedding` if we could use one-hot-encoded vectors for the same thing?\n1. What does an embedding contain before we start training (assuming we're not using a pretained model)?\n1. Create a class (without peeking, if possible!) and use it.\n1. What does `x[:,0]` return?\n1. Rewrite the `DotProduct` class (without peeking, if possible!) and train a model with it.\n1. What is a good loss function to use for MovieLens? Why? \n1. What would happen if we used cross-entropy loss with MovieLens? How would we need to change the model?\n1. What is the use of bias in a dot product model?\n1. What is another name for weight decay?\n1. Write the equation for weight decay (without peeking!).\n1. Write the equation for the gradient of weight decay. Why does it help reduce weights?\n1. Why does reducing weights lead to better generalization?\n1. What does `argsort` do in PyTorch?\n1. Does sorting the movie biases give the same result as averaging overall movie ratings by movie? Why/why not?\n1. How do you print the names and details of the layers in a model?\n1. What is the \"bootstrapping problem\" in collaborative filtering?\n1. How could you deal with the bootstrapping problem for new users? For new movies?\n1. How can feedback loops impact collaborative filtering systems?\n1. When using a neural network in collaborative filtering, why can we have different numbers of factors for movies and users?\n1. Why is there an `nn.Sequential` in the `CollabNN` model?\n1. What kind of model should we use if we want to add metadata about users and items, or information such as date and time, to a collaborative filtering model?","metadata":{}},{"cell_type":"markdown","source":"### Further Research\n\n1. Take a look at all the differences between the `Embedding` version of `DotProductBias` and the `create_params` version, and try to understand why each of those changes is required. If you're not sure, try reverting each change to see what happens. (NB: even the type of brackets used in `forward` has changed!)\n1. Find three other areas where collaborative filtering is being used, and find out what the pros and cons of this approach are in those areas.\n1. Complete this notebook using the full MovieLens dataset, and compare your results to online benchmarks. See if you can improve your accuracy. Look on the book's website and the fast.ai forum for ideas. Note that there are more columns in the full dataset—see if you can use those too (the next chapter might give you ideas).\n1. Create a model for MovieLens that works with cross-entropy loss, and compare it to the model in this chapter.","metadata":{}},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]}]}