{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Light FM Approximate Nearest Neighbours (ANN) example\n", "\n", "In this notebook, we'll be building a hybrid LightFM model on the example movielens dataset. \n", "\n", "We'll then store the item and user vectors, and build an ANN index over these for fast querying at scale. This technique can produce < 50ms response-times across millions of items.\n", "\n", "We'll look at two popular ANN libraries, Annoy and NMSlib, and compare their performance.\n", "\n", "Dependencies:\n", "- Annoy - https://github.com/spotify/annoy\n", "- NMSLIB - https://github.com/searchivarius/nmslib/tree/master/python_bindings\n", "\n", "Firstly, let's import the data and build the Light FM model, as per the example in the docs:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/Users/lester/anaconda2/lib/python2.7/site-packages/lightfm/_lightfm_fast.py:9: UserWarning: LightFM was compiled without OpenMP support. Only a single thread will be used.\n", " warnings.warn('LightFM was compiled without OpenMP support. '\n" ] } ], "source": [ "%matplotlib inline\n", "import numpy as np\n", "import pandas as pd\n", "from sklearn.metrics import pairwise_distances\n", "import time\n", "from lightfm.datasets import fetch_movielens\n", "\n", "movielens = fetch_movielens()" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "('test', , (943, 1682))\n", "('item_features', , (1682, 1682))\n", "('train', , (943, 1682))\n", "('item_labels', , (1682,))\n", "('item_feature_labels', , (1682,))\n" ] } ], "source": [ "for key, value in movielens.items():\n", " print(key, type(value), value.shape)" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "collapsed": true }, "outputs": [], "source": [ "train = movielens['train']\n", "test = movielens['test']" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Precision: train 0.73, test 0.10.\n", "AUC: train 0.97, test 0.91.\n" ] } ], "source": [ "from lightfm import LightFM\n", "from lightfm.evaluation import precision_at_k\n", "from lightfm.evaluation import auc_score\n", "\n", "model = LightFM(learning_rate=0.05, loss='warp', no_components=64, item_alpha=0.001)\n", "\n", "model.fit_partial(train, item_features=movielens['item_features'], epochs=20 )\n", "\n", "train_precision = precision_at_k(model, train, k=10).mean()\n", "test_precision = precision_at_k(model, test, k=10).mean()\n", "\n", "train_auc = auc_score(model, train).mean()\n", "test_auc = auc_score(model, test).mean()\n", "\n", "print('Precision: train %.2f, test %.2f.' % (train_precision, test_precision))\n", "print('AUC: train %.2f, test %.2f.' % (train_auc, test_auc))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can get the item embeddings by simply multiplying the item features by the models item feature embeddings. Even better, LightFM has a performant built-in function to retrieve these embeddings givin a set of features." ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": true }, "outputs": [], "source": [ "_, item_embeddings = model.get_item_representations(movielens['item_features'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now let's make an annoy index for item to item querying:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from annoy import AnnoyIndex\n", "\n", "factors = item_embeddings.shape[1] # Length of item vector that will be indexed\n", "annoy_idx = AnnoyIndex(factors) \n", "for i in range(item_embeddings.shape[0]):\n", " v = item_embeddings[i]\n", " annoy_idx.add_item(i, v)\n", "\n", "annoy_idx.build(10) # 10 trees\n", "annoy_idx.save('movielens_item_Annoy_idx.ann')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And query the index for similar movies:" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def nearest_movies_annoy(movie_id, index, n=10, print_output=True):\n", " nn = index.get_nns_by_item(movie_id, 10)\n", " if print_output:\n", " print('Closest to %s : \\n' % movielens['item_labels'][movie_id])\n", " titles = [movielens['item_labels'][i] for i in nn]\n", " if print_output:\n", " print(\"\\n\".join(titles))" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Closest to Nightmare Before Christmas, The (1993) : \n", "\n", "Nightmare Before Christmas, The (1993)\n", "Beauty and the Beast (1991)\n", "Fantasia (1940)\n", "Aladdin (1992)\n", "Jurassic Park (1993)\n", "Sword in the Stone, The (1963)\n", "Snow White and the Seven Dwarfs (1937)\n", "Lion King, The (1994)\n", "Princess Bride, The (1987)\n", "Monty Python and the Holy Grail (1974)\n" ] } ], "source": [ "nearest_movies_annoy(90, annoy_idx)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Not bad, and it's very quick:" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The slowest run took 5.72 times longer than the fastest. This could mean that an intermediate result is being cached.\n", "100000 loops, best of 3: 15.4 µs per loop\n" ] } ], "source": [ "%%timeit\n", "nearest_movies_annoy(90, annoy_idx, print_output=False)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "How about NMSlib?" ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "collapsed": true }, "outputs": [], "source": [ "import nmslib\n", "\n", "# initialize a new nmslib index, using a HNSW index on Cosine Similarity\n", "nms_idx = nmslib.init(method='hnsw', space='cosinesimil')\n", "nms_idx.addDataPointBatch(item_embeddings)\n", "nms_idx.createIndex(print_progress=True)" ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def nearest_movies_nms(movie_id, index, n=10, print_output=True):\n", " nn = index.knnQuery(item_embeddings[movie_id], k=10)\n", " if print_output == True:\n", " print('Closest to %s : \\n' % movielens['item_labels'][movie_id])\n", " titles = [movielens['item_labels'][i] for i in nn[0]]\n", " if print_output == True:\n", " print(\"\\n\".join(titles))" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Closest to Nightmare Before Christmas, The (1993) : \n", "\n", "Nightmare Before Christmas, The (1993)\n", "Beauty and the Beast (1991)\n", "Fantasia (1940)\n", "Aladdin (1992)\n", "Star Trek: The Wrath of Khan (1982)\n", "Heavy Metal (1981)\n", "Jurassic Park (1993)\n", "Aristocats, The (1970)\n", "Blade Runner (1982)\n", "Bram Stoker's Dracula (1992)\n" ] } ], "source": [ "nearest_movies_nms(90, nms_idx, n=10)" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The slowest run took 4.93 times longer than the fastest. This could mean that an intermediate result is being cached.\n", "10000 loops, best of 3: 26.7 µs per loop\n" ] } ], "source": [ "%%timeit \n", "nearest_movies_nms(90, nms_idx, n=10, print_output=False)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

Again, good and maybe a little better, and also very fast. Now we'll do an example showing how we can do user recommendations using a neat trick outlined by the Xbox recomendations team: https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/XboxInnerProduct.pdf . Thanks to Ben Fredrickson https://github.com/benfred/ for bringing this one to light.

\n", "\n", "

Basically we add a nomalizing factor to each item vector - making their distances equal with each other. Then when we query with a user vector, we add a 0 to the end, and the result is proportional to the inner producct of the user and item vectors. This is a sneaky way to do an aproximate maximum inner product search." ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "collapsed": true }, "outputs": [], "source": [ "norms = np.linalg.norm(item_embeddings, axis=1)\n", "max_norm = norms.max()\n", "extra_dimension = np.sqrt(max_norm ** 2 - norms ** 2)\n", "norm_data = np.append(item_embeddings, extra_dimension.reshape(norms.shape[0], 1), axis=1)\n", "\n", "#First an Annoy index:\n", "\n", "user_factors = norm_data.shape[1]\n", "annoy_member_idx = AnnoyIndex(user_factors) # Length of item vector that will be indexed\n", "\n", "for i in range(norm_data.shape[0]):\n", " v = norm_data[i]\n", " annoy_member_idx.add_item(i, v)\n", " \n", "annoy_member_idx.build(10)\n", "\n", "# Now an NMS index\n", "\n", "nms_member_idx = nmslib.init(method='hnsw', space='cosinesimil')\n", "nms_member_idx.addDataPointBatch(norm_data)\n", "nms_member_idx.createIndex(print_progress=True)" ] }, { "cell_type": "code", "execution_count": 18, "metadata": { "collapsed": true }, "outputs": [], "source": [ "# Define our user vectors\n", "\n", "_, user_embeddings = model.get_user_representations()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Now we can query as per the standard LightFM example" ] }, { "cell_type": "code", "execution_count": 21, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def sample_recommendation(user_ids, model, data, n_items=10, print_output=True):\n", " n_users, n_items = data['train'].shape\n", "\n", " for user_id in user_ids:\n", " known_positives = data['item_labels'][data['train'].tocsr()[user_id].indices]\n", " top_items = [data['item_labels'][i] for i in annoy_member_idx.get_nns_by_vector(np.append(user_embeddings[user_id], 0), 50)]\n", " if print_output == True:\n", " print(\"User %s\" % user_id)\n", " print(\" Known positives:\")\n", "\n", " for x in known_positives[:3]:\n", " print(\" %s\" % x)\n", "\n", " print(\" Recommended:\")\n", "\n", " for x in top_items[:3]:\n", " print(\" %s\" % x)" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "User 3\n", " Known positives:\n", " Seven (Se7en) (1995)\n", " Indiana Jones and the Last Crusade (1989)\n", " Contact (1997)\n", " Recommended:\n", " Cop Land (1997)\n", " Air Force One (1997)\n", " Contact (1997)\n", "User 25\n", " Known positives:\n", " Toy Story (1995)\n", " Twelve Monkeys (1995)\n", " Dead Man Walking (1995)\n", " Recommended:\n", " Men in Black (1997)\n", " Toy Story (1995)\n", " Rock, The (1996)\n", "User 450\n", " Known positives:\n", " Kolya (1996)\n", " Devil's Own, The (1997)\n", " Contact (1997)\n", " Recommended:\n", " Hoodlum (1997)\n", " Excess Baggage (1997)\n", " G.I. Jane (1997)\n" ] } ], "source": [ "sample_recommendation([3,25,450], model, movielens, print_output=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Compare this to the original, non-ANN version here:" ] }, { "cell_type": "code", "execution_count": 23, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def sample_recommendation_original(model, data, user_ids, print_output=True):\n", "\n", " n_users, n_items = data['train'].shape\n", "\n", " for user_id in user_ids:\n", " known_positives = data['item_labels'][data['train'].tocsr()[user_id].indices]\n", " scores = model.predict(user_id, np.arange(n_items))\n", " top_items = data['item_labels'][np.argsort(-scores)]\n", " if print_output == True:\n", " print(\"User %s\" % user_id)\n", " print(\" Known positives:\")\n", "\n", " for x in known_positives[:3]:\n", " print(\" %s\" % x)\n", "\n", " print(\" Recommended:\")\n", "\n", " for x in top_items[:3]:\n", " print(\" %s\" % x)" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "User 3\n", " Known positives:\n", " Seven (Se7en) (1995)\n", " Indiana Jones and the Last Crusade (1989)\n", " Contact (1997)\n", " Recommended:\n", " Contact (1997)\n", " Air Force One (1997)\n", " Cop Land (1997)\n", "User 25\n", " Known positives:\n", " Toy Story (1995)\n", " Twelve Monkeys (1995)\n", " Dead Man Walking (1995)\n", " Recommended:\n", " Men in Black (1997)\n", " Toy Story (1995)\n", " Rock, The (1996)\n", "User 450\n", " Known positives:\n", " Kolya (1996)\n", " Devil's Own, The (1997)\n", " Contact (1997)\n", " Recommended:\n", " Hoodlum (1997)\n", " G.I. Jane (1997)\n", " Cop Land (1997)\n" ] } ], "source": [ "sample_recommendation_original(model, movielens, [3, 25, 450])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So pretty similar!\n", "\n", "## But how fast are they, and do they scale?\n", "\n", "Here are the two functions compared on a single prediction:" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "100 loops, best of 3: 4.56 ms per loop\n" ] } ], "source": [ "%%timeit\n", "sample_recommendation_original(model, movielens, [3, 25, 450], print_output=False)" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "100 loops, best of 3: 2.88 ms per loop\n" ] } ], "source": [ "%%timeit\n", "sample_recommendation([3,25,450], model, movielens, print_output=False)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So this is a bit faster, but at these volumes it probably doesn't justify the extra effort. Where these libraries provide value is against very large numbers of vectors. So let's see how the ANN libraries stack-up versus the Scikit-Learn pairwise distance function, which can also be used for (exact) nearest neighbour searches:" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[Text(0,0.5,u'Time (ms)'), Text(0.5,0,u'Numer of vectors')]" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/png": "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\n", "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "def time_nearest_neighbours_methods(item_embeddings):\n", " \n", " # Pairwise distances nearest neighbours\n", " start = time.time()\n", " distances = pairwise_distances(item_embeddings[0].reshape(1, -1), Y=item_embeddings)\n", " top_items = distances.argsort()[:10]\n", " end = time.time()\n", " pairwise_elapsed = end - start\n", " \n", " #NMS lib\n", " nms_idx = nmslib.init(method='hnsw', space='cosinesimil')\n", " nms_idx.addDataPointBatch(item_embeddings)\n", " nms_idx.createIndex(print_progress=False)\n", " start = time.time()\n", " nms_idx.knnQuery(item_embeddings[0])[0]\n", " end = time.time()\n", " nms_elapsed = end - start\n", " \n", " #Annoy\n", " annoy_idx = AnnoyIndex(item_embeddings.shape[1])\n", " for i in range(item_embeddings.shape[0]):\n", " v = item_embeddings[i]\n", " annoy_idx.add_item(i, v)\n", " annoy_idx.build(10) # 10 trees\n", " \n", " start = time.time()\n", " annoy_idx.get_nns_by_vector(item_embeddings[0], 10)\n", " end = time.time() \n", " annoy_elapsed = end - start\n", " \n", " return {'pairwise_elapsed': pairwise_elapsed, 'nms_elapsed': nms_elapsed, 'annoy_elapsed' : annoy_elapsed}\n", "\n", "pairwise_timings = []\n", "nms_timings = []\n", "annoy_timings = []\n", "\n", "for i in range (1, 100, 10):\n", " loop_item_embeddings = np.repeat(item_embeddings, i, axis=0)\n", " results = time_nearest_neighbours_methods(loop_item_embeddings)\n", " pairwise_timings.append(results['pairwise_elapsed'])\n", " nms_timings.append(results['nms_elapsed'])\n", " annoy_timings.append(results['annoy_elapsed'])\n", " \n", "timings_df = pd.DataFrame({'Pairwise_dist':pairwise_timings, \\\n", " 'NMS': nms_timings, \\\n", " 'Annoy': annoy_timings}, \\\n", " index=np.arange(10)*item_embeddings.shape[0])\n", "\n", "import matplotlib.pyplot as plt\n", "fig, axes = plt.subplots(nrows=1, ncols=2)\n", "ax = timings_df.plot(figsize=(14,4), title='Time to compute nearest neighbours vs. number of vectors', ax=axes[0])\n", "ax.set(xlabel='Numer of vectors', ylabel='Time (ms)')\n", "ax2 = timings_df.loc[:, 'Annoy':'NMS'].plot(figsize=(14,4), title='Time to compute nearest neighbours vs. number of vectors', ax=axes[1])\n", "ax2.set(xlabel='Numer of vectors', ylabel='Time (ms)')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As you can see, both NMSLib and Annoy are a lot faster than the pairwise distance method with a larger numbers of vectors. In fact at these volumes, we can't really see any response-time increase at all as we add more vectors.\n", "\n", "## So how does using an ANN library affect quality?\n", "\n", "To get an idea, let's use our Annoy and NMS indexes to calculate predictions, and then evaluate their 'precision at K' versus the built-in predict method from LightFM." ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "lightfm_pak = precision_at_k(model, test, k=10).mean()\n", "\n", "def annoy_precision_at_k(user_ids, k=10):\n", " test_csr = test.tocsr()\n", " paks = []\n", " for user_id in user_ids:\n", " test_interactions = test_csr[user_id].indices\n", " if len(test_interactions) > 0:\n", " recommendations = annoy_member_idx.get_nns_by_vector(np.append(user_embeddings[user_id], 0), k)\n", " hits = len(set(test_interactions).intersection(recommendations))\n", " pak = hits / (len(test_interactions) * 1.0)\n", " paks.append(pak)\n", " return np.array(paks).mean()\n", "\n", "annoy_pak = annoy_precision_at_k(np.arange(test.shape[0]))\n", "\n", "def nms_precision_at_k(user_ids, k=10):\n", " test_csr = test.tocsr()\n", " paks = []\n", " for user_id in user_ids:\n", " test_interactions = test_csr[user_id].indices\n", " if len(test_interactions) > 0:\n", " recommendations = nms_member_idx.knnQuery(np.append(user_embeddings[user_id], 0), k=10)[0]\n", " hits = len(set(test_interactions).intersection(recommendations))\n", " pak = hits / (len(test_interactions) * 1.0)\n", " paks.append(pak)\n", " return np.array(paks).mean()\n", "\n", "nms_pak = nms_precision_at_k(np.arange(test.shape[0]))\n", "\n", "plt.bar(['LightFm', 'Annoy', 'NMSLib'], [lightfm_pak, annoy_pak, nms_pak])\n", "plt.title('Precision at K=10, ANN vs. exact')\n", "plt.ylabel('Precision at k=10')\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## So, should you use one of these libraries?\n", "\n", "On a smaller dataset, it's not worth the hassle and loss of accuracy for a small increase in speed. However, if you need to respond in < 100ms, then your vector set can only grow so large before you'll need to start thinking about one of these libraries. \n", "\n", "Annoy has the advantage of saving to a memory map, and so playing well with multiple processes in a production environment\n", "\n", "NMSlib outperforms Annoy in terms of accuracy, but I also found that constructing indexes over a large dataset was slower. This becomes a problem if you have to rebuild your index frequently because of fast moving product catalogues etc. " ] } ], "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.0" } }, "nbformat": 4, "nbformat_minor": 1 }