{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#hide\n",
    "!pip install -Uqq fastbook\n",
    "import fastbook\n",
    "fastbook.setup_book()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#hide\n",
    "from fastbook import *"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Collaborative Filtering Deep Dive"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## A First Look at the Data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from fastai.collab import *\n",
    "from fastai.tabular.all import *\n",
    "path = untar_data(URLs.ML_100k)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ratings = pd.read_csv(path/'u.data', delimiter='\\t', header=None,\n",
    "                      names=['user','movie','rating','timestamp'])\n",
    "ratings.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "last_skywalker = np.array([0.98,0.9,-0.9])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "user1 = np.array([0.9,0.8,-0.6])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "(user1*last_skywalker).sum()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "casablanca = np.array([-0.99,-0.3,0.8])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "(user1*casablanca).sum()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Learning the Latent Factors"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Creating the DataLoaders"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "movies = pd.read_csv(path/'u.item',  delimiter='|', encoding='latin-1',\n",
    "                     usecols=(0,1), names=('movie','title'), header=None)\n",
    "movies.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ratings = ratings.merge(movies)\n",
    "ratings.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dls = CollabDataLoaders.from_df(ratings, item_name='title', bs=64)\n",
    "dls.show_batch()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dls.classes"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "n_users  = len(dls.classes['user'])\n",
    "n_movies = len(dls.classes['title'])\n",
    "n_factors = 5\n",
    "\n",
    "user_factors = torch.randn(n_users, n_factors)\n",
    "movie_factors = torch.randn(n_movies, n_factors)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "one_hot_3 = one_hot(3, n_users).float()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "user_factors.t() @ one_hot_3"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "user_factors[3]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Collaborative Filtering from Scratch"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Example:\n",
    "    def __init__(self, a): self.a = a\n",
    "    def say(self,x): return f'Hello {self.a}, {x}.'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ex = Example('Sylvain')\n",
    "ex.say('nice to meet you')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class DotProduct(Module):\n",
    "    def __init__(self, n_users, n_movies, n_factors):\n",
    "        self.user_factors = Embedding(n_users, n_factors)\n",
    "        self.movie_factors = Embedding(n_movies, n_factors)\n",
    "        \n",
    "    def forward(self, x):\n",
    "        users = self.user_factors(x[:,0])\n",
    "        movies = self.movie_factors(x[:,1])\n",
    "        return (users * movies).sum(dim=1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x,y = dls.one_batch()\n",
    "x.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model = DotProduct(n_users, n_movies, 50)\n",
    "learn = Learner(dls, model, loss_func=MSELossFlat())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "learn.fit_one_cycle(5, 5e-3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class DotProduct(Module):\n",
    "    def __init__(self, n_users, n_movies, n_factors, y_range=(0,5.5)):\n",
    "        self.user_factors = Embedding(n_users, n_factors)\n",
    "        self.movie_factors = Embedding(n_movies, n_factors)\n",
    "        self.y_range = y_range\n",
    "        \n",
    "    def forward(self, x):\n",
    "        users = self.user_factors(x[:,0])\n",
    "        movies = self.movie_factors(x[:,1])\n",
    "        return sigmoid_range((users * movies).sum(dim=1), *self.y_range)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model = DotProduct(n_users, n_movies, 50)\n",
    "learn = Learner(dls, model, loss_func=MSELossFlat())\n",
    "learn.fit_one_cycle(5, 5e-3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class DotProductBias(Module):\n",
    "    def __init__(self, n_users, n_movies, n_factors, y_range=(0,5.5)):\n",
    "        self.user_factors = Embedding(n_users, n_factors)\n",
    "        self.user_bias = Embedding(n_users, 1)\n",
    "        self.movie_factors = Embedding(n_movies, n_factors)\n",
    "        self.movie_bias = Embedding(n_movies, 1)\n",
    "        self.y_range = y_range\n",
    "        \n",
    "    def forward(self, x):\n",
    "        users = self.user_factors(x[:,0])\n",
    "        movies = self.movie_factors(x[:,1])\n",
    "        res = (users * movies).sum(dim=1, keepdim=True)\n",
    "        res += self.user_bias(x[:,0]) + self.movie_bias(x[:,1])\n",
    "        return sigmoid_range(res, *self.y_range)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model = DotProductBias(n_users, n_movies, 50)\n",
    "learn = Learner(dls, model, loss_func=MSELossFlat())\n",
    "learn.fit_one_cycle(5, 5e-3)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Weight Decay"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x = np.linspace(-2,2,100)\n",
    "a_s = [1,2,5,10,50] \n",
    "ys = [a * x**2 for a in a_s]\n",
    "_,ax = plt.subplots(figsize=(8,6))\n",
    "for a,y in zip(a_s,ys): ax.plot(x,y, label=f'a={a}')\n",
    "ax.set_ylim([0,5])\n",
    "ax.legend();"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model = DotProductBias(n_users, n_movies, 50)\n",
    "learn = Learner(dls, model, loss_func=MSELossFlat())\n",
    "learn.fit_one_cycle(5, 5e-3, wd=0.1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Creating Our Own Embedding Module"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class T(Module):\n",
    "    def __init__(self): self.a = torch.ones(3)\n",
    "\n",
    "L(T().parameters())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class T(Module):\n",
    "    def __init__(self): self.a = nn.Parameter(torch.ones(3))\n",
    "\n",
    "L(T().parameters())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class T(Module):\n",
    "    def __init__(self): self.a = nn.Linear(1, 3, bias=False)\n",
    "\n",
    "t = T()\n",
    "L(t.parameters())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "type(t.a.weight)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def create_params(size):\n",
    "    return nn.Parameter(torch.zeros(*size).normal_(0, 0.01))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class DotProductBias(Module):\n",
    "    def __init__(self, n_users, n_movies, 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.movie_factors = create_params([n_movies, n_factors])\n",
    "        self.movie_bias = create_params([n_movies])\n",
    "        self.y_range = y_range\n",
    "        \n",
    "    def forward(self, x):\n",
    "        users = self.user_factors[x[:,0]]\n",
    "        movies = self.movie_factors[x[:,1]]\n",
    "        res = (users*movies).sum(dim=1)\n",
    "        res += self.user_bias[x[:,0]] + self.movie_bias[x[:,1]]\n",
    "        return sigmoid_range(res, *self.y_range)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model = DotProductBias(n_users, n_movies, 50)\n",
    "learn = Learner(dls, model, loss_func=MSELossFlat())\n",
    "learn.fit_one_cycle(5, 5e-3, wd=0.1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Interpreting Embeddings and Biases"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "movie_bias = learn.model.movie_bias.squeeze()\n",
    "idxs = movie_bias.argsort()[:5]\n",
    "[dls.classes['title'][i] for i in idxs]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "idxs = movie_bias.argsort(descending=True)[:5]\n",
    "[dls.classes['title'][i] for i in idxs]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "g = ratings.groupby('title')['rating'].count()\n",
    "top_movies = g.sort_values(ascending=False).index.values[:1000]\n",
    "top_idxs = tensor([learn.dls.classes['title'].o2i[m] for m in top_movies])\n",
    "movie_w = learn.model.movie_factors[top_idxs].cpu().detach()\n",
    "movie_pca = movie_w.pca(3)\n",
    "fac0,fac1,fac2 = movie_pca.t()\n",
    "idxs = list(range(50))\n",
    "X = fac0[idxs]\n",
    "Y = fac2[idxs]\n",
    "plt.figure(figsize=(12,12))\n",
    "plt.scatter(X, Y)\n",
    "for i, x, y in zip(top_movies[idxs], X, Y):\n",
    "    plt.text(x,y,i, color=np.random.rand(3)*0.7, fontsize=11)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Using fastai.collab"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "learn = collab_learner(dls, n_factors=50, y_range=(0, 5.5))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "learn.fit_one_cycle(5, 5e-3, wd=0.1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "learn.model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "movie_bias = learn.model.i_bias.weight.squeeze()\n",
    "idxs = movie_bias.argsort(descending=True)[:5]\n",
    "[dls.classes['title'][i] for i in idxs]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Embedding Distance"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "movie_factors = learn.model.i_weight.weight\n",
    "idx = dls.classes['title'].o2i['Silence of the Lambs, The (1991)']\n",
    "distances = nn.CosineSimilarity(dim=1)(movie_factors, movie_factors[idx][None])\n",
    "idx = distances.argsort(descending=True)[1]\n",
    "dls.classes['title'][idx]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Bootstrapping a Collaborative Filtering Model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Deep Learning for Collaborative Filtering"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "embs = get_emb_sz(dls)\n",
    "embs"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "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)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model = CollabNN(*embs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "learn = Learner(dls, model, loss_func=MSELossFlat())\n",
    "learn.fit_one_cycle(5, 5e-3, wd=0.01)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "learn = collab_learner(dls, use_nn=True, y_range=(0, 5.5), layers=[100,50])\n",
    "learn.fit_one_cycle(5, 5e-3, wd=0.1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "@delegates(TabularModel)\n",
    "class EmbeddingNN(TabularModel):\n",
    "    def __init__(self, emb_szs, layers, **kwargs):\n",
    "        super().__init__(emb_szs, layers=layers, n_cont=0, out_sz=1, **kwargs)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Sidebar: kwargs and Delegates"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### End sidebar"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Conclusion"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Questionnaire"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1. What problem does collaborative filtering solve?\n",
    "1. How does it solve it?\n",
    "1. Why might a collaborative filtering predictive model fail to be a very useful recommendation system?\n",
    "1. What does a crosstab representation of collaborative filtering data look like?\n",
    "1. Write the code to create a crosstab representation of the MovieLens data (you might need to do some web searching!).\n",
    "1. What is a latent factor? Why is it \"latent\"?\n",
    "1. What is a dot product? Calculate a dot product manually using pure Python with lists.\n",
    "1. What does `pandas.DataFrame.merge` do?\n",
    "1. What is an embedding matrix?\n",
    "1. What is the relationship between an embedding and a matrix of one-hot-encoded vectors?\n",
    "1. Why do we need `Embedding` if we could use one-hot-encoded vectors for the same thing?\n",
    "1. What does an embedding contain before we start training (assuming we're not using a pretained model)?\n",
    "1. Create a class (without peeking, if possible!) and use it.\n",
    "1. What does `x[:,0]` return?\n",
    "1. Rewrite the `DotProduct` class (without peeking, if possible!) and train a model with it.\n",
    "1. What is a good loss function to use for MovieLens? Why? \n",
    "1. What would happen if we used cross-entropy loss with MovieLens? How would we need to change the model?\n",
    "1. What is the use of bias in a dot product model?\n",
    "1. What is another name for weight decay?\n",
    "1. Write the equation for weight decay (without peeking!).\n",
    "1. Write the equation for the gradient of weight decay. Why does it help reduce weights?\n",
    "1. Why does reducing weights lead to better generalization?\n",
    "1. What does `argsort` do in PyTorch?\n",
    "1. Does sorting the movie biases give the same result as averaging overall movie ratings by movie? Why/why not?\n",
    "1. How do you print the names and details of the layers in a model?\n",
    "1. What is the \"bootstrapping problem\" in collaborative filtering?\n",
    "1. How could you deal with the bootstrapping problem for new users? For new movies?\n",
    "1. How can feedback loops impact collaborative filtering systems?\n",
    "1. When using a neural network in collaborative filtering, why can we have different numbers of factors for movies and users?\n",
    "1. Why is there an `nn.Sequential` in the `CollabNN` model?\n",
    "1. 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?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Further Research\n",
    "\n",
    "1. 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!)\n",
    "1. 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.\n",
    "1. 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).\n",
    "1. Create a model for MovieLens that works with cross-entropy loss, and compare it to the model in this chapter."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "jupytext": {
   "split_at_heading": true
  },
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
