Created
May 30, 2019 17:20
-
-
Save bmbagley/e3aa3e69c64f4c60deb867b3e873d2aa to your computer and use it in GitHub Desktop.
Recommendation engine with FastAI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"cells": [ | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"# Create a Collaborative Filtering Model using MovieLense datasets & Predicting 2 new movies\n", | |
"\n", | |
"The techniques used here are to employ the FastAi library to build a simple Recommendation Engine and prediction for users. For additional information refer to the FastAi library." | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 1, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"from fastai.collab import * " | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"## Import a sample of the MovieLense Ratings dataset" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 2, | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"data": { | |
"text/html": [ | |
"<div>\n", | |
"<style scoped>\n", | |
" .dataframe tbody tr th:only-of-type {\n", | |
" vertical-align: middle;\n", | |
" }\n", | |
"\n", | |
" .dataframe tbody tr th {\n", | |
" vertical-align: top;\n", | |
" }\n", | |
"\n", | |
" .dataframe thead th {\n", | |
" text-align: right;\n", | |
" }\n", | |
"</style>\n", | |
"<table border=\"1\" class=\"dataframe\">\n", | |
" <thead>\n", | |
" <tr style=\"text-align: right;\">\n", | |
" <th></th>\n", | |
" <th>userId</th>\n", | |
" <th>movieId</th>\n", | |
" <th>rating</th>\n", | |
" <th>timestamp</th>\n", | |
" </tr>\n", | |
" </thead>\n", | |
" <tbody>\n", | |
" <tr>\n", | |
" <th>0</th>\n", | |
" <td>73</td>\n", | |
" <td>1097</td>\n", | |
" <td>4.0</td>\n", | |
" <td>1255504951</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>1</th>\n", | |
" <td>561</td>\n", | |
" <td>924</td>\n", | |
" <td>3.5</td>\n", | |
" <td>1172695223</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>2</th>\n", | |
" <td>157</td>\n", | |
" <td>260</td>\n", | |
" <td>3.5</td>\n", | |
" <td>1291598691</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>3</th>\n", | |
" <td>358</td>\n", | |
" <td>1210</td>\n", | |
" <td>5.0</td>\n", | |
" <td>957481884</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>4</th>\n", | |
" <td>130</td>\n", | |
" <td>316</td>\n", | |
" <td>2.0</td>\n", | |
" <td>1138999234</td>\n", | |
" </tr>\n", | |
" </tbody>\n", | |
"</table>\n", | |
"</div>" | |
], | |
"text/plain": [ | |
" userId movieId rating timestamp\n", | |
"0 73 1097 4.0 1255504951\n", | |
"1 561 924 3.5 1172695223\n", | |
"2 157 260 3.5 1291598691\n", | |
"3 358 1210 5.0 957481884\n", | |
"4 130 316 2.0 1138999234" | |
] | |
}, | |
"execution_count": 2, | |
"metadata": {}, | |
"output_type": "execute_result" | |
} | |
], | |
"source": [ | |
"path=untar_data(URLs.ML_SAMPLE)\n", | |
"ratings = pd.read_csv(path/'ratings.csv')\n", | |
"ratings.head()" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 141, | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": [ | |
"6031\n" | |
] | |
} | |
], | |
"source": [ | |
"print(len(ratings))" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"### Build a dataset from the dataframe" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 3, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"# Create embeddings layer\n", | |
"data = CollabDataBunch.from_df(ratings)" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"### Build a collaborative filtering model\n", | |
"Uses 50 latent factors for both the user and ratings, and increase accuracy by defining the range of ratings with a EmbeddingDotBias model" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 4, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"# Create Collaborative Filtering Learner\n", | |
"learn = collab_learner(data, n_factors=50, y_range=(0.,5.))" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"Find a learning rate" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 5, | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"data": { | |
"text/html": [], | |
"text/plain": [ | |
"<IPython.core.display.HTML object>" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
}, | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": [ | |
"LR Finder is complete, type {learner_name}.recorder.plot() to see the graph.\n" | |
] | |
}, | |
{ | |
"data": { | |
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAZgAAAEKCAYAAAAvlUMdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4yLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvOIA7rQAAIABJREFUeJzt3Xd8V9X9+PHXOzshISQkgZBBgDBlE1DEAWqte9XdOmut1bba1tZu+62/Wq2tta4qddRaR1XcA1yggGAYhhlGSCBAQggji+zk/fvj80FDzOZzPyN5Px+Pz4PP595z730fMt4595x7jqgqxhhjjKcF+ToAY4wxvZMlGGOMMY6wBGOMMcYRlmCMMcY4whKMMcYYR1iCMcYY4whLMMYYYxxhCcYYY4wjLMEYY4xxRIivA/CkhIQEzcjI8HUYxhgTMFatWrVPVROdOHevSjAZGRmsXLnS12EYY0zAEJEdTp3bbpEZY4xxhCUYY4wxjrAEY4wxxhGWYIwxxjjCEowxxhhHWIIxxhjjCEswxhhjHGEJxhiHLdu2n1U7Dvg6DGO8zhKMMQ4qq67nxv+s5ObnVlPX2OTrcIzxKkswxjjon59so7KukZKKOt7IKfJ1OMZ4lSUYYxxSUlHLM59t58IpKYxN7s/cT/NpblZfh2WM11iCMcYhD328lcYm5SenjeKmk4eTt7eKjzft9XVYXVJSUcunW0p9HYYJcJZgjHHAjv2HeDF7J1fMSCd9YBRnTUgmZUAkj3+6zdehdaq2oYmrn8zm6qeyeXJJga/DMQHMEowxDnjgw62EBAs/OiUTgNDgIG44cRgrth/0+xFl//fWRjaXVDJtaBx3vb2R5z53bLJd08tZgjHGwzbtqeD1nN1ce/wwkvpHfLn9sulpDIgK5bFP8n0YXcfeXFPEC9mF/GD2CF743nGcOiaJ37y2nldW7fJ1aCYAOZZgRCRNRBaKSK6IbBCRW9spN1tEctxlPmmxfbuIrHPvs0VeTMD464ItRIeHcNPJw4/YHhUWwtXHDeWDjSXk7a3yUXTt277vEL9+dR1T0wfw02+MIiwkiEe+PZUTRybwi1fW8NYaGwVnusfJBccagZ+p6moRiQFWicgHqrrxcAERGQA8CpyhqoUiktTqHHNUdZ+DMRrjUZ9uKeXD3BJ+/s3RDIgK+9r+a47P4PFP8/n5K2uYmh5HWEgQYcFBpMRFct6kIUSEBvsgaqhrbOKHL6wmOEh48IophAa7/vaMCA1m7lVZXPN0Nrf9L4dmVc6fnOKTGE3gcSzBqGoxUOx+XykiuUAKsLFFsSuBV1W10F0uMIbYGNOGusYm/vDmBjIGRnHDicPaLDMwOpybZ2fyzLLtbC2poq6xiYYm19Dlv8zfxHWzhnHVzKH0jwj1YuRw3/zNrN9dwdyrppEaF3XEvsiwYJ66djo3PLOC2/6XQ11DM5dOT/NqfCYweWXJZBHJAKYAn7faNQoIFZFFQAzwD1X9j3ufAu+LiAKPq+pcb8RqTE89uaSA/H2HePq66YSHtN8SufW0kdx62sgvPzc3K9nbD/Doom3ct2Azjy3axnWzMrj1tFEEB4njca/acZAnlxbw7WPTOf2YwW2WiQ4P4elrZ/D9/67iF/PWUl3fyLWz2k6ixhzmeIIRkWhgHnCbqla0cf1pwKlAJLBMRJar6hZglqoWuW+bfSAim1T10zbOfyNwI0B6erqTVTGmXUVlNTz0UR7fGDeIOaNb3+ntWFCQcNzwgRw3fCDrd5fz6KI8Hvw4j4raRu48dxwiziWZusYm7pi3luT+EfzyzDEdlo0MC+ZfV0/jR89/wR/e2kh1QxM/OHmEo/GZwOboKDIRCcWVXJ5T1VfbKLILmK+qh9x9LZ8CkwBUtcj9717gNWBGW9dQ1bmqmqWqWYmJiU5Uw5hO/emdXJpV+f05447qPONTYnn029O44YRh/Puz7fxrsbMjzh7+OI+8vVX86aIJxHThtlx4SDCPfHsq500awl/mb+ZHL3xBeXWDozGawOXkKDIBngRyVfX+doq9AZwoIiEiEgUcC+SKSD/3wABEpB9wOrDeqViNORpL8/bxzrpibp6dSVp8VOcHdMGvzxrLOROTufvdTbyRs9sj52xtY1EF/1y0jYumpHSr1RUaHMTfL5vMz785mvnr93DGPz7lszwbi2O+zslbZLOAq4B1IpLj3vZrIB1AVR9T1VwRmQ+sBZqBJ1R1vYgMB15zN71DgOdVdb6DsRrTI/WNzdz55gbS46P4fqthyUcjKEj426WTKK2s4/aX15AYE87xIxI8dv7GpmbumLeWAVGh/K4Hra7gIOGWOZmcODKB217M4conPue6WRmMHxJLXWMzdY1NNCtcNCWFuH5fH01n+gZR7T2T72VlZenKlfbIjPGef3y4lb9/uIWnr53OnDHd63vpivKaBi557DOKy2q551sTOXticrtlVZVtpYf4MLeEJVv3cdn0NM6dNKTNso8uyuMv8zfzyJVTOzxnV1TXN3L3u7n8d3nh1/Z9/6Th/OqssUd1fuMsEVmlqlmOnNsSjDE9s7WkkrMeXMyZ45N58Iopjl2nqKyGm/67irW7yjlrwmD+eP54EqLDAVdSydlZxrvrivkwdy8F+w4BEBsZSk19Ey/ceCzThsYfcb6Fm/by3WdWcMb4wTxy5VSPddIXldXQ2KSEh7qe7fnJSzls2VPJkjtOIcgLo+FMz1iC6SJLMKa7NhZV8PKqndxxxphuPeTY3Kxc8vgytpVW8eFPT/7yF75TGpuambs4nwc+2Ep0RAg/O30UOw/U8PbaInYdrCE0WJg5IoFvjE3ilLGD6BcWzPmPLOVQXRNv/nAWQwZEArB+dzmXPr6M4Yn9+N+NM+kX7txd8te/2M1t/8vh5ZtmMj0jvvMDjE84mWC88hyMMf7qrrc3six/P2XVDdx/6aQu/zX/XHYhq3Yc5K+XTHI8uQCEBAdx8+xMvjF2ELe/vIbfvLaekCBhVmYCt546ktOPGUxs5JGjwJ64OosLH/2M7/1nJS/fNJOy6gau//cKBkSG8tQ10x1NLgDfGDeIiNAg3swpsgTTR1mCMX3Wul3lLMvfz9jk/rz2xW7Gp8Ty3RO+/vBgfWMzocHyZfIpLq/h3vc2cUJmAt+a6t1pU0YOimHeD45nxfaDjBkc02EH+shBMTx0xRSuf2YFP/lfDjv2V1NT38TLP5h5xCScTukXHsKpYwfx7rpi7jx3HCHBNrduX2MJxvRZ/1qcT3R4CC/eeBw/f3kNd7+by9jBMRyf6RqtVV7dwF/f38xzn+9gQFQYowZFM3pQDLnFlTQ2N3P3hRN88pBhSHAQM0cM7FLZOWOS+OUZY/jze5sICRKevm46Ywb3dzjCr5w3aQjvrC1m6bb9nDzKnlPrayzBmD5p18Fq3llXzPWzMoiNDOX+yyZzwSNLueX51bz5wxNYlr+fe97bRFl1PZdMS0MENpdU8sqqXRyqb+K3Z48lfaBnnnlx2o0nDaexWRmRGM2JI737S3726ERiIkJ4M6fIEkwfZAnG9ElPL92OANe559OKDg9h7lXTOP+RpZx2/yfUNTa7Ftw6/1jGDfnqL35VZf+hegYG0LMdIq5nVnwhPCSYM44ZzPz1e6htGO+z2aKNb9hNUdPnlNc08GJ2IedOGvLl6CqA4YnRPHTFFIYl9OO+iyfy8vdnHpFcwPXLOiE63Obf6obzJg+hsq6RRZttsvS+xlowps95IbuQQ/VNbU6pP3t0ErO7OVml6djM4QNJiA7jjZwizhh/dA91msBiLRjTp9Q3NvP00gJOyEzgmCGxvg6nTwgJDuLsCcl8tGkvlbU2MWZfYi0Y06u9vHInr+fsJiQoiNDgIKrqGiipqOMvF0/ydWh9ynmTh/DMsh0s2FDCxdNSfR2O8RJrwZhe61BdI3e9vZH80kOUVddTVFbDgUP1nD0hmZNGem7iSNO5qelxDE/ox5NLCuhNs4eYjlkLxvRaL6/cSUVtI09fN4NpQ+N8HU6fJiLcPCeT219ew4e5e/nGuEG+Dsl4gbVgTK/U1Kw8tXQ7U9MHWHLxE+dPHkJ6fBQPfrTVWjF9hCUY0yu9v2EPhQeq+d6JnlujxRyd0OAgbpkzgnW7y1m0udTX4RgvsARjeqV/Lc4nPT6K048Z7OtQTAsXTkklZUAk/7BWTJ9gCcb0Oqt2HGR1YRnXz8og2NYh8SthIUHcPGcEOTvLWLzVllnu7SzBmF7nicX59I8I4ZKsNF+HYtpw8bRUkmMjrC+mD7AEY3qVwv3VLNiwh28fN9Tx9U5Mz4SHBPOD2SNYueMgy7bt93U4xkGOJRgRSRORhSKSKyIbROTWdsrNFpEcd5lPWmw/Q0Q2i0ieiPzSqThN7/LU0gKCg4Rrj8/wdSimA5dmpTG4fwS3PL+aDzeW+Doc4xAnWzCNwM9UdSxwHHCLiIxrWUBEBgCPAuep6jHAJe7twcAjwJnAOOCK1sca01pjUzOv5+zmjPHJDPLCglqm5yJCg3n+e8cyZEAkN/xnJXe+sZ7ahiZfh2U8zLEEo6rFqrra/b4SyAVaL/93JfCqqha6yx2ebnUGkKeq+apaD7wInO9UrKZ3yC44QFl1A2dPsJFjgWB4YjSv3nw8N5wwjGeW7eCCR5ayfne5r8MyHuSVPhgRyQCmAJ+32jUKiBORRSKySkSudm9PAXa2KLeLrycnY44wf8MeIkKDOMkWtgoY4SHB/PaccTx93XRKK+s456ElnP/wEp5dvoPyGpsYM9A5nmBEJBqYB9ymqhWtdocA04CzgW8CvxORUUBbY0vbHG4iIjeKyEoRWVlaag9v9VXNzcqCDXs4eVQiUWHWuR9o5oxO4sOfnszvzhlHXWMzv3t9PTP+9CG/e3099Y3Nvg7P9JCjP4kiEooruTynqq+2UWQXsE9VDwGHRORTYJJ7e8sxpqlAUVvXUNW5wFyArKwsG/PYR63ZVUZJRR3ftAcrA1ZcvzC+e8Iwrp+VwYaiCp77fAfPLt9B4YFqHvvONCLDbDXMQOPkKDIBngRyVfX+doq9AZwoIiEiEgUci6uvZgUwUkSGiUgYcDnwplOxmsA3f8MeQoKEU8fYJIqBTkQYnxLLny+ayD0XTWDx1lK+8+TnlFfbLbNA4+QtslnAVcAp7mHIOSJylojcJCI3AahqLjAfWAtkA0+o6npVbQR+CCzAlXBeUtUNDsZqApiqsmD9HmaOGEhsVKivwzEedPmMdB6+ciprd5Vx2dxl7K2s9XVIphscu0Wmqktouy+ldbn7gPva2P4u8K4DoZleZktJFdv3V/O9k2xiy97orAnJxESE8P1nV3H53OW8d+uJhIfY7bJAYE/ym4A3f/0eRLA1RnqxE0cm8vCVU8gvPcRLK3Z2foDxC5ZgTMBbsGEP09LjSIqxhyt7szmjk8gaGscjC7fZQ5kBwhKMCSg19U00N381WLBwfzUbiys4Y7yNHuvtRITbThvFnopaXlpprZhAYA8MmIBRuL+asx9aTEx4COdMGsK5E4ewPN81WaINT+4bZmUOZHpGHI8szOPSrDQiQq0vxp9ZC8YEhKZm5Scv5QAwJrk/Ty8t4NyHl3DP/E2MS+5PWnyUjyM03iAi/OS0UZRU1PFidqGvwzGdsBaMCQiPfbKNVTsO8sBlk7lgSgpl1fUs2LCHBRtKuHCKzSLUl8wcMZAZw+J5dNE2Lp+Rbq0YP2YtGOP31u8u54EPt3D2xGTOnzwEgAFRYVw2PZ2nrp3OuZOG+DhC402HWzF7K+t4wVoxfs1aMMav1TY08ZP/5RAXFcafLhiPa4II09fNHDGQY4fF8+BHWynYd4jhCf0YkRTN6MExNprQj1iCMX7tvgWb2bq3imeun8GAqDBfh2P8yB/OO4bfvLaO11bvprKuEYCwkCDm3XQ8E1JjfRydAUswxo99UXiQJ5cUcNVxQznZpuA3rYxN7s+rN89CVSmtqiOvpIobn13FU0sL+Ptlk30dnsH6YIyfam5W/vDWRpJiwrnjzDG+Dsf4MREhKSaC4zMTuHhaKu+sLaa0ss7XYRkswRg/9doXu1mzs4w7zhhDdLg1tE3XfOe4odQ3NfO/Fdb57w8swRi/U1XXyL3zNzEpbYANQTbdkpkUzYkjE/jv8kIam2yhMl+zBGP8zqML89hbWced544jKMhGjZnuuXpmBnsqavlgY4mvQ+nzLMEYv1K4v5onFhdw0ZQUpqbH+TocE4BOGZNEyoBI/rNsh69D6fMswRi/8qd3NxISLPziDOvYNz0THCRcNXMoy/L3s6Wk0tfh9GmWYIxfUFX+uWgbCzaUcMucTAbH2sNypucuy0ojPCSI/yzb7utQ+jRLMLiGxNY12voSvlLb0MRPX1rDvfM3cc7EZG44cZivQzIBLq5fGOdNGsKrq3dTUdvg63D6rD6fYKrqGjnlb4t4ckmBr0Ppk/ZW1HLZ3OW89sVufvaNUTx0xRRbDtd4xHeOG0p1fRPz1+3xdSh9Vp9PMNHhIQyOjeDF7J1HLGRlnLdy+wHOe3gpW0sqeew70/jRqSNtrjHjMRNTY0mOjeDjTXt9HUqf5ViCEZE0EVkoIrkiskFEbm2jzGwRKReRHPfr9y32bReRde7tK52KE+CKGekUHqjms237nbyMcWtoauZv72/m0seXERoivHLT8bYipfE4EWHOmCSW5O2jvtGeifEFJx+RbgR+pqqrRSQGWCUiH6jqxlblFqvqOe2cY46q7nMwRgDOGD+YuKhQns/ewQkjE5y+XJ+WX1rFbf/LYe2uci6Zlsrvzx1HTESor8MyvdSc0Uk8/3khK7YfYFam/Wx7m2MtGFUtVtXV7veVQC7gl49lh4cEc/G0VN7fUGJzGDnos237OPvBJRQeqObRb0/lvksmWXIxjpqVOZCwkCAW2m0yn/BKH4yIZABTgM/b2D1TRNaIyHsickyL7Qq8LyKrROTGDs59o4isFJGVpaWlPY7x8hnpNDYrr6za1eNzmI79/YMtxPcLY/6tJ3HWhGRfh2P6gKiwEI4bPpCPN1uC8QXHE4yIRAPzgNtUtaLV7tXAUFWdBDwEvN5i3yxVnQqcCdwiIie1dX5VnauqWaqalZjY8yndRyRGc+yweF7ILrTOfgdsKCpnxfaDXDcrw55xMV51yuhE8ksPsWP/IV+H0uc4mmBEJBRXcnlOVV9tvV9VK1S1yv3+XSBURBLcn4vc/+4FXgNmOBkrwJXHWme/U55dtoPI0GAumZbm61BMH3PKmEEANprMB5wcRSbAk0Cuqt7fTpnB7nKIyAx3PPtFpJ97YAAi0g84HVjvVKyHffMYV2e/rfPtWWXV9byes5sLpqQQG2V9Lsa70gdGMTyxHws39/wWuukZJ0eRzQKuAtaJSI5726+BdABVfQy4GPiBiDQCNcDlqqoiMgh4zZ17QoDnVXW+g7ECEBEazLempvLvz7ZTWllHYky405fsE15auZPahmaunjnU16GYPuqU0Un8Z/kOqusbiQqz9YW8xbH/aVVdAnT41JyqPgw83Mb2fGCSQ6F16PIZ6TyxpICXVu7kljmZvgihV2lqVp5dvoMZw+IZm9zf1+GYPuqUMUk8saSApXn7+ca4Qb4Op8/o80/yt5aZFM2c0Yk89PFWm4nVAxZu2svOAzVce3yGr0MxfVhWRjzR4SHWD+NllmDacO/FE4kOD+GW51ZTXd/o63AC2jPLtjO4f4T91Wh8KiwkiBNHJrBo815UbZSot1iCaUNSTAQPXDaFvNIqfv/GBl+HE7C2lVaxeOs+vn1sOqHB9q1mfGvO6CSKy2vZtMfuTHiL/dS344SRCfxwTiavrNrFvF7y8GVtQ9tLEtQ2NPHUkgLOf3gJ//o03yPPATU2NfPndzcRGixcPiP9qM9nzNGaPdr1nNzirTaazFtsOEUHbj11JNkFB/jt6+uZlBZLZlKMr0PqkcamZn73xgZeyC7kmCH9OWtCMmeOH0xqXBQvr9rJQx/lsaeilrT4SP70bi4f5pbw10smkRYf1aPrqSq/fm0dH+aW8PtzxtloPOMXkvpHkBYfyZqd5b4Opc+wFkwHQoKDePCKKUSFBfOzl9b49b3bxqZmauq/3kKpbWji5udW80J2IedPHkJYSBD3LdjMKX/7hGl3fcBvXlvPkAERPH/DsXz68zncd/FENhRVcOY/FvPSyp3drrOqcve7uby0chc/PiWT60+wxcOM/5iYOoA1u8p8HUafYS2YTgzqH8EvzhjNHfPWsXDz3i+fCvY3v39zA6+s2sV5k4ZwzcwMJqTGUlHbwPeeWcnnBQe489xxXDfL9cu+uLyG+ev3sGZnGedPSWH2qMQv12G5JCuN44YP5PaX1/CLV9byf29uIDMpmhFJ0YxMiuFb01JIiml/qpdHF23jX4sLuGbmUH7yjVFeqbsxXTUpNZZ31hazv6qOgdHWsnaa+PNf5d2VlZWlK1d6fumYhqZm5vx1EfH9wnjjlll+tyjW7rIaTv7LQjKToik8UE11fRNT0gdQU9/EttIq/nbpZM6bNKRb52xuVt5cU0TOzjLy9laRt7eKPRW1TBsaxys3zWzz/+DF7EJ++eo6Lpg8hPsvnUxQkH/9PxmzPH8/l89dztPXTWfO6CRfh+MXRGSVqmY5cW5rwXRBaHAQPzolkzvmrWPR5lLmjPGvb8x/fZqPCDx17XSiI0KYt2oXzy7bQUlFLU9dO50TR3Z/EtCgIOGCKSlcMOWrFRb+t6KQO+at4+21xZzbKmHtPFDNH97awIkjE7jvkkmWXIxfGp8Siwis2VlmCcYLrA+miy6amkpqXCQPfLjFJ30xZdX1bY4C21dVxwvZhVw4JYUhAyLpHxHKdbOG8eFPT2bFb0/rUXJpz8XT0hib3J973tt0RCyqym9fX0+wCPd+a6INSTZ+Kzo8hMzEaNbuso5+b7DfBF10uBWzZlc5i7w8aV5tQxPnPLSEcx5aQnlNwxH7nlpSQH1TM98/ecQR24OCxONzLgUHCb87eyy7y2p4amnBl9vfWlvMJ1tKuf2boxkyINKj1zTG0yalDWDtrjK/HrTTW1iC6QZftWKe/7yQXQdryC+t4gf/XfXl+uIVtQ08u2wHZ44fzIjEaK/EcnxmAqeNHcSjC7dRWllHWXU9f3xrA5NSY7l6ZoZXYjDmaExKjWVfVT1F5bW+DqXXswTTDaHBQfxwjrsVs8WzrZhtpVU0NDV/bXt1fSOPLspj5vCB/OXiSXy2bT+/eW0dqsqzy3ZQWdfIzbO9Oynnr88aQ21DE/d/sIV73tvEweoG7r5oAsHW72ICwMTUAQCs3WnDlZ1mnfzddNHUVB5emMeDH231SCdhcXkN/++dXN5ZW8yZ4wfzyJVTj+gg//dn29lXVc/jV41m2tA4Cg9U8+BHWxkcG8EL2YWcNCqR8SmxRx1HdwxPjObqmRk8/VkBqvD9k4ZzzBDvxmBMT41JjiE0WMjZVcaZtnS3o6wF001hIUHccMIwvigsY0NRzzsKG5qamfvpNk792yd8uLGE08YO4r31e3jgo61flimvaeDxT/I5ZUwS04bGAfCT00Zy4ZQUHvo4j31V9dwye0R7l3DUj0/NJDYylLT4SG49baRPYjCmJ8JDghmb3J+19kS/46wF0wMXTknlz+9t4sXsndx1Qff/cq+qa+SSx5aRW1zBqWOSuPPcY0iLj+QXr6zlwY+2MjIpmnMnDeHJxfmU1zTw0xYPLIoI93xrAger6wkSYcaweE9WrcsGRIXx6g+OJyosxBZwMgFnYmosb3xRRHOz2pB6B9lvhh6IjQrlrAnJvP7Fbn591lgiw4K7dfySrfvILa7gLxdP5NKsr9ao/38Xjmf7/kPc/vIa+keG8uSSAs6aMPhrt8DCQ4L593UzUFWfPvQ53EsDC4zxtImpA/jv8kLy9x0iM8m+j53SpVtkIjJCRMLd72eLyI9FZICzofm3y6enUVnXyNtri7p9bHbBAcJDgrhgcsoR28NDgvnnd6aREB3ONU9lU9PQdETrpTV/m1HAmEAxOc3d0W/zkjmqq30w84AmEckEngSGAc87FlUAmDEsnuGJ/Xhxxc5uH5u9fT9T0+MIC/n6f39CdDhPXJNFv7BgLp6WGrAzOBvjz0YkRhMVFmwPXDqsqwmmWVUbgQuBB1T1J0CHwy9EJE1EFopIrohsEJFb2ygzW0TKRSTH/fp9i31niMhmEckTkV92p1LeICJcPj2NVTsOdmtp5YraBjYWVXTYdzI2uT9Lf3kKf75ooidCNca0EhwkjE+JJceGKjuqqwmmQUSuAK4B3nZvC+3kmEbgZ6o6FjgOuEVExrVRbrGqTna//gggIsHAI8CZwDjginaO9alvTU0lNFh4MbvrrZhVOw7SrHBsJ53zA6LC7LkSYxw0KTWWjcUVXz64bDyvqwnmOmAm8CdVLRCRYcB/OzpAVYtVdbX7fSWQC6R0dEwLM4A8Vc1X1XrgReD8Lh7rNQOjwzl93GBe/WJXu6tFtpZdcICQIGFKepzD0RljOjIxdQD1jc3dugNhuqdLCUZVN6rqj1X1BRGJA2JU9Z6uXkREMoApwOdt7J4pImtE5D0ROca9LQVo2SzYRdeTk1ddPiONsuoGFmzY06Xy2QUHmJga2+2RZ8YYz5rkfqLfFiBzTldHkS0Skf4iEg+sAZ4Wkfu7eGw0rkECt6lqRavdq4GhqjoJeAh4/fBhbZyqzcm/RORGEVkpIitLS72/1vasEQmkxUfyQnZhp2Vr6ptYu6uMGcMGeiEyY0xH0uIjiY0MZUNR619LxlO6eoss1p0cLgKeVtVpwGmdHSQiobiSy3Oq+mrr/apaoapV7vfvAqEikoCrxZLWomgq0OZ4YFWdq6pZqpqVmOi5qem7KihIuOq4oSzPP8C764o7LPvFzoM0NGmn/S/GGOeJCCMS+5FfWuXrUHqtriaYEBFJBi7lq07+DonrIY0ngVxVbbO1IyKD3eUQkRnuePYDK4CRIjJMRMKAy4E3uxir1103axiT0gbwq1fXUVRW02657IIDBAlMy7D+F2P8wfDEaPJLD/k6jF6rqwnmj8ACYJuqrhCR4cDWTo6ZBVwFnNJiGPJZInKTiNzkLnMxsF5E1gAPAperSyPwQ/c6ox7dAAAXtUlEQVQ1c4GXVHVDN+vmNaHBQfzjssk0NjXz05dyaGpueyr/7IIDjBvSn/4RnQ3AM8Z4w/DEfuytrKOytqHzwqbbujRVjKq+DLzc4nM+8K1OjllC230pLcs8DDzczr53gXe7Ep8/yEjoxx/OO4afv7KWxz/d9rUp9Osbm1ldeJArZwz1UYTGmNaGJ7imickvPcSktD49OYkjutrJnyoir4nIXhEpEZF5IpLqdHCB5uJpqZw9MZn739/CmlYPcK3bXU5tQ7PPJqc0xnxdZlI/APL3WT+ME7p6i+xpXH0gQ3ANF37Lvc20ICLcfcEEkmLC+fGLX1C4v/rLfdkFBwCYbv0vxviN9Ph+BAeJ9cM4pKsJJlFVn1bVRvfr34D3h2wFgNioUB66cgoHDtVz1oOLeWXVLlSV7IL9jEyKZmB0uK9DNMa4hYUEkRYXaQnGIV1NMPtE5DsiEux+fQfXaC/ThmlD45l/20mMG9Kf219eww+f/4KV2w/a7TFj/NDwxGi22VBlR3Q1wVyPa4jyHqAY1+iv65wKqjdIGRDJC987jl+cMZoFG/ZQWddoCcYYPzQisR8F+w7R3M7oT9NzXR1FVgic13KbiNwGPOBEUL1FcJBw8+xMTsxM5JVVOzl17CBfh2SMaWV4YjR1jc3sLqshLT7K1+H0KkezouVPsQTTJRNSY5mQ2v2llY0xzhuecHgk2SFLMB7W1VtkbbG55I0xAe/w0t/b9lo/jKcdTYKxG5bGmICXEB1GTESIPQvjgA5vkYlIJW0nEgEiHYnIGGO8yDXppc1J5oQOE4yq2oLwxpheb3hiP5bm7fN1GL3O0dwiM8aYXmFEYjQlFXVU1TX6OpRexRKMMabPOzySrMBuk3mUJRhjTJ83Isk9q7J19HuUJRhjTJ83dGAUQWJDlT3NEowxps8LDwkmNS6KbfvsFpknWYIxxhhcI8lsqLJnWYIxxhhcI8kK9lXZpJceZAnGGGNwtWBqG5opKq/xdSi9hmMJRkTSRGShiOSKyAYRubWDstNFpElELm6xrUlEctyvN52K0xhjAIYnuEeS2W0yjzma2ZQ70wj8TFVXi0gMsEpEPlDVjS0LiUgwcC+woNXxNao62cH4jDHmSyMS3bMql1Zx0ihbsNcTHGvBqGqxqq52v68EcoGUNor+CJgH7HUqFmOM6UxiTDgx4SFssxaMx3ilD0ZEMoApwOettqcAFwKPtXFYhIisFJHlInKB40EaY/o0EWF4ki2f7ElO3iIDQESicbVQblPVila7HwDuUNUmka8tL5OuqkUiMhz4WETWqeq2Ns5/I3AjQHp6uucrYIzpM0YmRfPJllJfh9FrONqCEZFQXMnlOVV9tY0iWcCLIrIduBh49HBrRVWL3P/mA4twtYC+RlXnqmqWqmYlJtp9U2NMz2UmRVNaWUd5dYOvQ+myBRv28OiiPFT9b3i1k6PIBHgSyFXV+9sqo6rDVDVDVTOAV4CbVfV1EYkTkXD3eRKAWcDGts5hjDGeMtI9J1leaaWPI+m6t9YU8UJ2IW3cBfI5J2+RzQKuAtaJSI5726+BdABVbavf5bCxwOMi0owrCd7TevSZMcZ42sgk1xJYW0uqmDY03sfRdM3mPZWMHtTf12G0ybEEo6pLcK182dXy17Z4/xkwwYGwjDGmXSlxkUSEBrE1QCa9rGtsIn/fIU4/ZpCvQ2mTPclvjDFuwUGu5ZPzAiTBbNt7iKZmZfRg/2zBWIIxxpgWMpMCJ8FsLnENzB0z2D9Xt7cEY4wxLYxMimZ3WQ2HAmD55E17KgkNFoa5V+T0N5ZgjDGmhUx3R38gPHC5eU8lIxKjCQ32z1/l/hmVMcb4yMhBrqHKW0sCI8H46+0xsARjjDFHGBofRWiw+P1IsvLqBorLa/22gx8swRhjzBFCgoMYltCPvL3+/bDl5hJXfNaCMcaYADIyKcbvR5Jt3uMaQTbaEowxxgSOzKRoCg9UU9vQ5OtQ2rVpTyUxESEkx0b4OpR2WYIxxphWRg6Kpln9e3XLwx38/jgH2WGWYIwxppVM96SXW/20H0ZV2VxS6de3x8ASjDHGfM2whH4ECWzz036YovJaKmsb/XoEGViCMcaYrwkPCSZjYD+/Hap8uIPfn0eQgSUYY4xpU2ZStN8mmE17XLfuRg2yBGOMMQFn5KBotu87RENTs69D+ZrNeyoZEhtBbGSor0PpkCUYY4xpQ2ZSNI3Nyo79/jeSbPMe/+/gB0swxhjTpparW/qThqZmtpVW+X0HP1iCMcaYNo1IjEYEv+uHyS89REOT+n0HP1iCMcaYNkWGBZMaF+l3CebwHGR9+haZiKSJyEIRyRWRDSJyawdlp4tIk4hc3GLbNSKy1f26xqk4jTGmPSOTYtiyx78etty8p4IQ99LO/i7EwXM3Aj9T1dUiEgOsEpEPVHVjy0IiEgzcCyxosS0euBPIAtR97JuqetDBeI0x5ghjBsfw6ZZS6hubCQvxjxs+W0qqGJbQz2/i6YhjEapqsaqudr+vBHKBlDaK/giYB+xtse2bwAeqesCdVD4AznAqVmOMacuY5P40NqtfrW6562AN6fFRvg6jS7ySAkUkA5gCfN5qewpwIfBYq0NSgJ0tPu+i7eRkjDGOGevu58gtrvBxJF8pLq8heYD/zqDckuMJRkSicbVQblPV1l+lB4A7VLX1nNhtTQ+q7Zz/RhFZKSIrS0tLjz5gY4xxO3wrapOf9MPU1DdRVt1Acmykr0PpEif7YBCRUFzJ5TlVfbWNIlnAi+7pphOAs0SkEVeLZXaLcqnAorauoapzgbkAWVlZbSYhY4zpiZDgIEYNivabFkxReQ0AQwKkBeNYghFX1ngSyFXV+9sqo6rDWpT/N/C2qr7u7uS/W0Ti3LtPB37lVKzGGNOeMYP788kW/7g7UlxWCxAwLRgnb5HNAq4CThGRHPfrLBG5SURu6uhAVT0A3AWscL/+6N5mjDFeNWZwDKWVdeyrqvN1KF+1YAIkwTjWglHVJbTdl9Je+WtbfX4KeMrDYRljTLeMS3ZNybKpuJITRob7NJaiMleCGRTr2zi6yv8HUhtjjA8dfmJ+0x7f98MUl9WSEB1OeEiwr0PpEkswxhjTgYHR4STFhJNb7PuRZEXlNQHTwQ+WYIwxplNjkvv7xUiy4vLagOl/AUswxhjTqbHJMeTtrfLp4mOqSnFZ4DxkCZZgjDGmU2MH96e+qZmCfb5bfKyitpFD9U3WgjHGmN5kTLLvp4wpdg9RthaMMcb0IsMTogkNFp929AfaQ5ZgCcYYYzoVFhJEZlKMT4cqB9o0MWAJxhhjumTs4Bg2+bgFExwkJMVYgjHGmF5lTHIMeypqOXio3ifXLyqvYVBMOMFBXZ4gxecswRhjTBeMGeyaMibXR7fJispqSB4QOP0vYAnGGGO6ZGyLOcl8obi8luTYwLk9BpZgjDGmSxJjwkmIDvNJR7+qup7itxaMMcb0TmMG9/fJUOX9h+qpb2y2FowxxvRW41Ni2bSngtqG1qu8OysQn4EBSzDGGNNlM4bF0dCkfFFY5tXrHn4GJsVukRljTO80bWg8IpBd4N0FdovLAm+aGLAEY4wxXRYbGcroQTGs2O7lBFNeS1hIEAP7hXn1ukfLEowxxnTDscPiWV140KtT9xe5hyiLBM5DlmAJxhhjumX6sHiq65vYUOS94crFZTUBN4IMHEwwIpImIgtFJFdENojIrW2UOV9E1opIjoisFJETWuxrcm/PEZE3nYrTGGO6Y0ZGPAArvNgPE2grWR7mZAumEfiZqo4FjgNuEZFxrcp8BExS1cnA9cATLfbVqOpk9+s8B+M0xpguS+ofQcbAKLK91A/T1KzsqagNuA5+cDDBqGqxqq52v68EcoGUVmWqVFXdH/sBijHG+LnpGfGs2H6A5mbnf2XtraylqVkD7hkY8FIfjIhkAFOAz9vYd6GIbALewdWKOSzCfdtsuYhc0MG5b3SXW1laWurhyI0x5utmDIunrLqBvNIqx69V5H7IMpDWgTnM8QQjItHAPOA2Vf1ar5iqvqaqY4ALgLta7EpX1SzgSuABERnR1vlVda6qZqlqVmJiogM1MMaYI80Y5uqH+dwL/TBfLpVsLZgjiUgoruTynKq+2lFZVf0UGCEiCe7PRe5/84FFuFpAxhjjc+nxUQzqH+6Vjv7D08RYJ38L4hqw/SSQq6r3t1Mm010OEZkKhAH7RSRORMLd2xOAWcBGp2I1xpjuEBGmZ8STXXCAr7qRnVFUXkNUWDD9I0McvY4TnIx4FnAVsE5Ectzbfg2kA6jqY8C3gKtFpAGoAS5TVRWRscDjItKMKwneo6qWYIwxfmPGsHjeXlvMroM1pMVHOXad4rLAfMgSHEwwqroE6PB/RFXvBe5tY/tnwASHQjPGmKN2uB8mu+CAowlmx4HqgFsH5jB7kt8YY3pgVFIMsZGhjs5LtmrHAXKLKzh5VGAOYLIEY4wxPRAUJEzPiHN0ZuWHPs4jLiqUK49Nd+waTrIEY4wxPTQrM4H8fYdYv7vc4+det6ucRZtLueHE4USFBV4HP1iCMcaYHrtoair9woJ5YnG+x8/98MKt9I8I4eqZQz1+bm+xBGOMMT0UGxnKZdPTeXtt8ZcPRHrC5j2VLNhQwrWzhhETEeqx83qbJRhjjDkK183KoFmVfy/d7rFzPrIwj35hwVx3fIbHzukLlmCMMeYopMVHceaEZJ7PLqSqrvGoz5dfWsXba4v4zsyhxAXYCpatWYIxxpij9L0Th1NZ28j/Vuw8qvOoKv/4aCuhwUHccMJwD0XnO5ZgjDHmKE1OG8D0jDieWlJAY4ullPdX1fHO2uIutWy2lFRy2dzlvJFTxDXHZ5AYE+5kyF4RmGPfjDHGz9xw4nC+/+wqFmwo4fgRA5m7OJ9nPttOdX0TQ2IjuOuC8Zw6dtDXjquub+TBj/J4YnE+/cJDuPvCCVw+Pc0HNfA8SzDGGOMBp40dxNCBUfy/dzZSUdNAdUMT504cwpnjB/P3D7fw3WdWcvaEZO48dxwKLM3bx9K8/XyypZR9VXVcPC2VX505hoHRgd9yOcwSjDHGeEBwkHDz7BHcMW8dZ09M5tZTRzJqUAwAp44dxNxPt/Hgx3l8sLGEevdttLioUGaOGMi1xw/7cm6z3kScnmram7KysnTlypW+DsMY04eV1zQQG9n2syv5pVU8vXQ7afGRHD8igXHJ/QkK8u0sySKyyr24o8dZC8YYYzyoveQCMDwxmrsuGO/FaHzLRpEZY4xxhCUYY4wxjrAEY4wxxhGWYIwxxjjCEowxxhhHWIIxxhjjCEswxhhjHGEJxhhjjCN61ZP8IlIK7Gi1ORZovWB2620tP3f2PgHYdxRhthVPV8t0ty6tPx9+35vq0vL90dTnaOrS3j77Pvtqm31tuhZrZ2Wc+NqMVtWYzsPuAVXt1S9gbmfbWn7u7D2w0tPxdLVMd+vSQR16TV08VZ+jqYt9n3X8fWZfm977tens1Rdukb3VhW1vdfO9p+Ppapnu1qX157faKdNT/lCXrsbRmaOpS3v77PvMM+xr0/F2X35tOtSrbpF5g4isVIcmhvO23lQX6F316U11gd5Vn95UF3C2Pn2hBeNpc30dgAf1prpA76pPb6oL9K769Ka6gIP1sRaMMcYYR1gLxhhjjCP6dIIRkadEZK+IrO/BsdNEZJ2I5InIgyIiLfb9SEQ2i8gGEfmLZ6NuNx6P10VE/iAiu0Ukx/06y/ORtxuTI18b9/7bRURFJMFzEXcYjxNfm7tEZK376/K+iAzxfORtxuNEXe4TkU3u+rwmIgM8H3m7MTlRn0vcP/vNIuJ4X83R1KGd810jIlvdr2tabO/w56pNTg1PC4QXcBIwFVjfg2OzgZmAAO8BZ7q3zwE+BMLdn5MCuC5/AG7vLV8b9740YAGu56USArUuQP8WZX4MPBbAdTkdCHG/vxe4N5C/z4CxwGhgEZDlr3Vwx5fRals8kO/+N879Pq6j+nb06tMtGFX9FDjQcpuIjBCR+SKySkQWi8iY1seJSDKuH/Bl6vqf/w9wgXv3D4B7VLXOfY29ztbCxaG6+IyD9fk78AvAa52PTtRFVStaFO2Hl+rjUF3eV9VGd9HlQKqztfiKQ/XJVdXN3ojffb0e1aEd3wQ+UNUDqnoQ+AA4o6e/J/p0gmnHXOBHqjoNuB14tI0yKcCuFp93ubcBjAJOFJHPReQTEZnuaLQdO9q6APzQfeviKRGJcy7ULjmq+ojIecBuVV3jdKBdcNRfGxH5k4jsBL4N/N7BWDvjie+zw67H9dexL3myPr7SlTq0JQXY2eLz4Xr1qL4hXbxonyAi0cDxwMstbi+Gt1W0jW2H/4IMwdW0PA6YDrwkIsPdWd9rPFSXfwJ3uT/fBfwN1y8Arzva+ohIFPAbXLdjfMpDXxtU9TfAb0TkV8APgTs9HGqnPFUX97l+AzQCz3kyxu7wZH18paM6iMh1wK3ubZnAuyJSDxSo6oW0X68e1dcSzJGCgDJVndxyo4gEA6vcH9/E9Yu3ZTM+FShyv98FvOpOKNki0oxr7qJSJwNvw1HXRVVLWhz3L+BtJwPuxNHWZwQwDFjj/qFLBVaLyAxV3eNw7K154vuspeeBd/BBgsFDdXF3Jp8DnOrtP8Za8fTXxhfarAOAqj4NPA0gIouAa1V1e4siu4DZLT6n4uqr2UVP6ut0B5S/v4AMWnSOAZ8Bl7jfCzCpneNW4GqlHO7wOsu9/Sbgj+73o3A1NyVA65LcosxPgBcD+WvTqsx2vNTJ79DXZmSLMj8CXgngupwBbAQSvfn95fT3GV7q5O9pHWi/k78A112YOPf7+K7Ut824fPEF9ZcX8AJQDDTgytDfxfVX7nxgjfub/vftHJsFrAe2AQ/z1UOrYcB/3ftWA6cEcF2eBdYBa3H91Zbsjbo4VZ9WZbbjvVFkTnxt5rm3r8U1r1RKANclD9cfYjnul1dGxDlYnwvd56oDSoAF/lgH2kgw7u3Xu78mecB1ndW3o5c9yW+MMcYRNorMGGOMIyzBGGOMcYQlGGOMMY6wBGOMMcYRlmCMMcY4whKM6dVEpMrL13tCRMZ56FxN4poteb2IvNXZLMMiMkBEbvbEtY3xBBumbHo1EalS1WgPni9Ev5qY0VEtYxeRZ4AtqvqnDspnAG+r6nhvxGdMZ6wFY/ocEUkUkXkissL9muXePkNEPhORL9z/jnZvv1ZEXhaRt4D3RWS2iCwSkVfEtY7Jc4fXxnBvz3K/r3JPSLlGRJaLyCD39hHuzytE5I9dbGUt46tJO6NF5CMRWS2u9TnOd5e5BxjhbvXc5y77c/d11orI/3nwv9GYTlmCMX3RP4C/q+p04FvAE+7tm4CTVHUKrtmJ725xzEzgGlU9xf15CnAbMA4YDsxq4zr9gOWqOgn4FPhei+v/w339Tudzcs+DdSqu2RQAaoELVXUqrvWH/uZOcL8EtqnqZFX9uYicDowEZgCTgWkiclJn1zPGU2yyS9MXnQaMazHTbH8RiQFigWdEZCSumWJDWxzzgaq2XHMjW1V3AYhIDq65oJa0uk49X00Qugr4hvv9TL5aS+N54K/txBnZ4tyrcK3NAa65oO52J4tmXC2bQW0cf7r79YX7czSuhPNpO9czxqMswZi+KAiYqao1LTeKyEPAQlW90N2fsajF7kOtzlHX4n0Tbf8sNehXnZztlelIjapOFpFYXInqFuBBXOu/JALTVLVBRLYDEW0cL8CfVfXxbl7XGI+wW2SmL3of1/opAIjI4WnNY4Hd7vfXOnj95bhuzQFc3llhVS3HtSzy7SISiivOve7kMgcY6i5aCcS0OHQBcL17fRBEJEVEkjxUB2M6ZQnG9HZRIrKrxeunuH5ZZ7k7vjfiWmIB4C/An0VkKRDsYEy3AT8VkWwgGSjv7ABV/QLXzLiX41qQK0tEVuJqzWxyl9kPLHUPa75PVd/HdQtumYisA17hyARkjKNsmLIxXuZeXbNGVVVELgeuUNXzOzvOmEBjfTDGeN804GH3yK8yfLQMtTFOsxaMMcYYR1gfjDHGGEdYgjHGGOMISzDGGGMcYQnGGGOMIyzBGGOMcYQlGGOMMY74/5ARbBU6q7onAAAAAElFTkSuQmCC\n", | |
"text/plain": [ | |
"<Figure size 432x288 with 1 Axes>" | |
] | |
}, | |
"metadata": { | |
"needs_background": "light" | |
}, | |
"output_type": "display_data" | |
} | |
], | |
"source": [ | |
"learn.lr_find()\n", | |
"learn.recorder.plot(skip_end=15)" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"Train the model & save" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 6, | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"data": { | |
"text/html": [ | |
"<table border=\"1\" class=\"dataframe\">\n", | |
" <thead>\n", | |
" <tr style=\"text-align: left;\">\n", | |
" <th>epoch</th>\n", | |
" <th>train_loss</th>\n", | |
" <th>valid_loss</th>\n", | |
" <th>time</th>\n", | |
" </tr>\n", | |
" </thead>\n", | |
" <tbody>\n", | |
" <tr>\n", | |
" <td>0</td>\n", | |
" <td>2.449789</td>\n", | |
" <td>1.975176</td>\n", | |
" <td>00:01</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <td>1</td>\n", | |
" <td>1.138332</td>\n", | |
" <td>0.637853</td>\n", | |
" <td>00:01</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <td>2</td>\n", | |
" <td>0.760696</td>\n", | |
" <td>0.616741</td>\n", | |
" <td>00:01</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <td>3</td>\n", | |
" <td>0.636746</td>\n", | |
" <td>0.604001</td>\n", | |
" <td>00:01</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <td>4</td>\n", | |
" <td>0.586490</td>\n", | |
" <td>0.604377</td>\n", | |
" <td>00:01</td>\n", | |
" </tr>\n", | |
" </tbody>\n", | |
"</table>" | |
], | |
"text/plain": [ | |
"<IPython.core.display.HTML object>" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
} | |
], | |
"source": [ | |
"# Train\n", | |
"learn.fit_one_cycle(5, 5e-3, wd=0.1)" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 7, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"learn.save('collab_20190506')" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 8, | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"data": { | |
"text/plain": [ | |
"CollabLearner(data=TabularDataBunch;\n", | |
"\n", | |
"Train: LabelList (4825 items)\n", | |
"x: CollabList\n", | |
"userId 73; movieId 1097; ,userId 561; movieId 924; ,userId 157; movieId 260; ,userId 358; movieId 1210; ,userId 130; movieId 316; \n", | |
"y: FloatList\n", | |
"4.0,3.5,3.5,5.0,2.0\n", | |
"Path: .;\n", | |
"\n", | |
"Valid: LabelList (1206 items)\n", | |
"x: CollabList\n", | |
"userId 313; movieId 589; ,userId 262; movieId 593; ,userId 518; movieId 1198; ,userId 452; movieId 457; ,userId 30; movieId 2858; \n", | |
"y: FloatList\n", | |
"3.5,5.0,5.0,4.0,5.0\n", | |
"Path: .;\n", | |
"\n", | |
"Test: None, model=EmbeddingDotBias(\n", | |
" (u_weight): Embedding(101, 50)\n", | |
" (i_weight): Embedding(101, 50)\n", | |
" (u_bias): Embedding(101, 1)\n", | |
" (i_bias): Embedding(101, 1)\n", | |
"), opt_func=functools.partial(<class 'torch.optim.adam.Adam'>, betas=(0.9, 0.99)), loss_func=FlattenedLoss of MSELoss(), metrics=[], true_wd=True, bn_wd=True, wd=0.01, train_bn=True, path=PosixPath('.'), model_dir='models', callback_fns=[functools.partial(<class 'fastai.basic_train.Recorder'>, add_time=True, silent=False)], callbacks=[], layer_groups=[Sequential(\n", | |
" (0): Embedding(101, 50)\n", | |
" (1): Embedding(101, 50)\n", | |
" (2): Embedding(101, 1)\n", | |
" (3): Embedding(101, 1)\n", | |
")], add_time=True, silent=None)" | |
] | |
}, | |
"execution_count": 8, | |
"metadata": {}, | |
"output_type": "execute_result" | |
} | |
], | |
"source": [ | |
"# Predict Rating\n", | |
"learn.load('collab_20190506')" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"### Output sample predictions from the model" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 131, | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"data": { | |
"text/html": [ | |
"<div>\n", | |
"<style scoped>\n", | |
" .dataframe tbody tr th:only-of-type {\n", | |
" vertical-align: middle;\n", | |
" }\n", | |
"\n", | |
" .dataframe tbody tr th {\n", | |
" vertical-align: top;\n", | |
" }\n", | |
"\n", | |
" .dataframe thead th {\n", | |
" text-align: right;\n", | |
" }\n", | |
"</style>\n", | |
"<table border=\"1\" class=\"dataframe\">\n", | |
" <thead>\n", | |
" <tr style=\"text-align: right;\">\n", | |
" <th></th>\n", | |
" <th>userId</th>\n", | |
" <th>movieId</th>\n", | |
" <th>rating</th>\n", | |
" <th>predictions</th>\n", | |
" </tr>\n", | |
" </thead>\n", | |
" <tbody>\n", | |
" <tr>\n", | |
" <th>0</th>\n", | |
" <td>73</td>\n", | |
" <td>1097</td>\n", | |
" <td>4.0</td>\n", | |
" <td>4.06</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>1</th>\n", | |
" <td>561</td>\n", | |
" <td>924</td>\n", | |
" <td>3.5</td>\n", | |
" <td>3.82</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>2</th>\n", | |
" <td>157</td>\n", | |
" <td>260</td>\n", | |
" <td>3.5</td>\n", | |
" <td>3.88</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>3</th>\n", | |
" <td>358</td>\n", | |
" <td>1210</td>\n", | |
" <td>5.0</td>\n", | |
" <td>4.46</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>4</th>\n", | |
" <td>130</td>\n", | |
" <td>316</td>\n", | |
" <td>2.0</td>\n", | |
" <td>3.08</td>\n", | |
" </tr>\n", | |
" </tbody>\n", | |
"</table>\n", | |
"</div>" | |
], | |
"text/plain": [ | |
" userId movieId rating predictions\n", | |
"0 73 1097 4.0 4.06\n", | |
"1 561 924 3.5 3.82\n", | |
"2 157 260 3.5 3.88\n", | |
"3 358 1210 5.0 4.46\n", | |
"4 130 316 2.0 3.08" | |
] | |
}, | |
"execution_count": 131, | |
"metadata": {}, | |
"output_type": "execute_result" | |
} | |
], | |
"source": [ | |
"temp = ratings.iloc[:5,:3]\n", | |
"# Predict rating for first 5 rows\n", | |
"preds_small = [round(float(learn.predict(rw.iloc[:2])[1]),2) for indx,rw in ratings.iloc[:5].iterrows()]\n", | |
"temp['predictions'] = preds_small\n", | |
"temp.head()" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"---\n", | |
"# Predictions of Unscored Movies by User" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 146, | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": [ | |
"CPU times: user 8 ms, sys: 0 ns, total: 8 ms\n", | |
"Wall time: 9.38 ms\n" | |
] | |
} | |
], | |
"source": [ | |
"%%time\n", | |
"# Find missing combinations of users and item ratings\n", | |
"rate_long = ratings.pivot(index='userId',columns='movieId',values='rating')\\\n", | |
" .stack(dropna=False)\\\n", | |
" .reset_index()\n", | |
"rate_long.columns = ['userId', 'movieId', 'score']" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 147, | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": [ | |
"CPU times: user 47.7 s, sys: 144 ms, total: 47.9 s\n", | |
"Wall time: 47.9 s\n" | |
] | |
} | |
], | |
"source": [ | |
"%%time\n", | |
"# Predict all user/movie ratings (known and unknown)\n", | |
"preds = [round(float(learn.predict(rw.iloc[:2])[1]),2) for indx,rw in rate_long.iterrows()]\n", | |
"rate_long['predictions'] = preds" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 148, | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"data": { | |
"text/html": [ | |
"<div>\n", | |
"<style scoped>\n", | |
" .dataframe tbody tr th:only-of-type {\n", | |
" vertical-align: middle;\n", | |
" }\n", | |
"\n", | |
" .dataframe tbody tr th {\n", | |
" vertical-align: top;\n", | |
" }\n", | |
"\n", | |
" .dataframe thead th {\n", | |
" text-align: right;\n", | |
" }\n", | |
"</style>\n", | |
"<table border=\"1\" class=\"dataframe\">\n", | |
" <thead>\n", | |
" <tr style=\"text-align: right;\">\n", | |
" <th></th>\n", | |
" <th>userId</th>\n", | |
" <th>movieId</th>\n", | |
" <th>score</th>\n", | |
" <th>predictions</th>\n", | |
" </tr>\n", | |
" </thead>\n", | |
" <tbody>\n", | |
" <tr>\n", | |
" <th>0</th>\n", | |
" <td>15</td>\n", | |
" <td>1</td>\n", | |
" <td>2.0</td>\n", | |
" <td>3.33</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>1</th>\n", | |
" <td>15</td>\n", | |
" <td>10</td>\n", | |
" <td>3.0</td>\n", | |
" <td>2.84</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>2</th>\n", | |
" <td>15</td>\n", | |
" <td>32</td>\n", | |
" <td>4.0</td>\n", | |
" <td>3.55</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>3</th>\n", | |
" <td>15</td>\n", | |
" <td>34</td>\n", | |
" <td>3.0</td>\n", | |
" <td>2.92</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>4</th>\n", | |
" <td>15</td>\n", | |
" <td>39</td>\n", | |
" <td>2.5</td>\n", | |
" <td>3.13</td>\n", | |
" </tr>\n", | |
" </tbody>\n", | |
"</table>\n", | |
"</div>" | |
], | |
"text/plain": [ | |
" userId movieId score predictions\n", | |
"0 15 1 2.0 3.33\n", | |
"1 15 10 3.0 2.84\n", | |
"2 15 32 4.0 3.55\n", | |
"3 15 34 3.0 2.92\n", | |
"4 15 39 2.5 3.13" | |
] | |
}, | |
"execution_count": 148, | |
"metadata": {}, | |
"output_type": "execute_result" | |
} | |
], | |
"source": [ | |
"rate_long.head()" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"## Output top 2 movie recommendations by user\n", | |
"Recommendations of Movies that were not scored by the user" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 149, | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"data": { | |
"text/html": [ | |
"<div>\n", | |
"<style scoped>\n", | |
" .dataframe tbody tr th:only-of-type {\n", | |
" vertical-align: middle;\n", | |
" }\n", | |
"\n", | |
" .dataframe tbody tr th {\n", | |
" vertical-align: top;\n", | |
" }\n", | |
"\n", | |
" .dataframe thead th {\n", | |
" text-align: right;\n", | |
" }\n", | |
"</style>\n", | |
"<table border=\"1\" class=\"dataframe\">\n", | |
" <thead>\n", | |
" <tr style=\"text-align: right;\">\n", | |
" <th></th>\n", | |
" <th>userId</th>\n", | |
" <th>movieId</th>\n", | |
" <th>predictions</th>\n", | |
" </tr>\n", | |
" </thead>\n", | |
" <tbody>\n", | |
" <tr>\n", | |
" <th>0</th>\n", | |
" <td>15</td>\n", | |
" <td>595</td>\n", | |
" <td>3.23</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>1</th>\n", | |
" <td>17</td>\n", | |
" <td>1197</td>\n", | |
" <td>4.23</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>2</th>\n", | |
" <td>17</td>\n", | |
" <td>1196</td>\n", | |
" <td>4.31</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>3</th>\n", | |
" <td>19</td>\n", | |
" <td>4973</td>\n", | |
" <td>4.21</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>4</th>\n", | |
" <td>19</td>\n", | |
" <td>4226</td>\n", | |
" <td>4.21</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>5</th>\n", | |
" <td>23</td>\n", | |
" <td>1214</td>\n", | |
" <td>3.99</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>6</th>\n", | |
" <td>23</td>\n", | |
" <td>58559</td>\n", | |
" <td>4.09</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>7</th>\n", | |
" <td>30</td>\n", | |
" <td>4973</td>\n", | |
" <td>4.55</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>8</th>\n", | |
" <td>30</td>\n", | |
" <td>1136</td>\n", | |
" <td>4.57</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>9</th>\n", | |
" <td>48</td>\n", | |
" <td>1221</td>\n", | |
" <td>4.31</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>10</th>\n", | |
" <td>48</td>\n", | |
" <td>318</td>\n", | |
" <td>4.33</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>11</th>\n", | |
" <td>56</td>\n", | |
" <td>858</td>\n", | |
" <td>4.38</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>12</th>\n", | |
" <td>56</td>\n", | |
" <td>1221</td>\n", | |
" <td>4.43</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>13</th>\n", | |
" <td>73</td>\n", | |
" <td>1073</td>\n", | |
" <td>4.11</td>\n", | |
" </tr>\n", | |
" <tr>\n", | |
" <th>14</th>\n", | |
" <td>73</td>\n", | |
" <td>4973</td>\n", | |
" <td>4.50</td>\n", | |
" </tr>\n", | |
" </tbody>\n", | |
"</table>\n", | |
"</div>" | |
], | |
"text/plain": [ | |
" userId movieId predictions\n", | |
"0 15 595 3.23\n", | |
"1 17 1197 4.23\n", | |
"2 17 1196 4.31\n", | |
"3 19 4973 4.21\n", | |
"4 19 4226 4.21\n", | |
"5 23 1214 3.99\n", | |
"6 23 58559 4.09\n", | |
"7 30 4973 4.55\n", | |
"8 30 1136 4.57\n", | |
"9 48 1221 4.31\n", | |
"10 48 318 4.33\n", | |
"11 56 858 4.38\n", | |
"12 56 1221 4.43\n", | |
"13 73 1073 4.11\n", | |
"14 73 4973 4.50" | |
] | |
}, | |
"execution_count": 149, | |
"metadata": {}, | |
"output_type": "execute_result" | |
} | |
], | |
"source": [ | |
"max_preds = rate_long[rate_long['score'].isnull()]\\\n", | |
" .sort_values(['userId','predictions'],ascending=False)\\\n", | |
" .groupby('userId',sort=False)\\\n", | |
" .head(2)\\\n", | |
" .drop(columns=['score'])\\\n", | |
" .sort_values(['userId'])\\\n", | |
" .reset_index(drop=True)\n", | |
"max_preds.head(15)" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": {}, | |
"outputs": [], | |
"source": [] | |
} | |
], | |
"metadata": { | |
"kernelspec": { | |
"display_name": "Python 3", | |
"language": "python", | |
"name": "python3" | |
}, | |
"language_info": { | |
"codemirror_mode": { | |
"name": "ipython", | |
"version": 3 | |
}, | |
"file_extension": ".py", | |
"mimetype": "text/x-python", | |
"name": "python", | |
"nbconvert_exporter": "python", | |
"pygments_lexer": "ipython3", | |
"version": "3.6.7" | |
} | |
}, | |
"nbformat": 4, | |
"nbformat_minor": 2 | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment