{ "nbformat": 4, "nbformat_minor": 0, "metadata": { "colab": { "name": "recommendation_nn.ipynb", "provenance": [], "collapsed_sections": [] }, "kernelspec": { "name": "python3", "display_name": "Python 3" }, "language_info": { "name": "python" } }, "cells": [ { "cell_type": "markdown", "metadata": { "id": "fw3fbJCt5124" }, "source": [ "# 推薦システムのコード例\n", "- 参考: [Collaborative Filtering for Movie Recommendations](https://keras.io/examples/structured_data/collaborative_filtering_movielens/) by Keras例題\n", "- 全体の流れ\n", " - データセットの用意\n", " - 学習用データ・検証用データに分割\n", " - モデル構築\n", " - 学習\n", " - 学習過程の観察\n", " - top-N推薦" ] }, { "cell_type": "markdown", "metadata": { "id": "9rymVz_vo_jv" }, "source": [ "## 環境構築" ] }, { "cell_type": "code", "metadata": { "id": "L4ebGTz3ky_D" }, "source": [ "import pandas as pd\n", "import numpy as np\n", "from zipfile import ZipFile\n", "import tensorflow as tf\n", "from tensorflow import keras\n", "from tensorflow.keras import layers\n", "from pathlib import Path\n", "import matplotlib.pyplot as plt" ], "execution_count": 1, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "yhnKe2YNpDhx" }, "source": [ "## データセットの用意\n", "- [MovieLens](https://grouplens.org/datasets/movielens/)の小データセットをダウンロード。\n", "- pd.read_csvで ratings.csv を DataFrame として読み込む。" ] }, { "cell_type": "code", "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 487 }, "id": "v2XQTRSQk0bC", "outputId": "5a8aba45-d01f-475a-8333-f93b1759c810" }, "source": [ "# Download the actual data from http://files.grouplens.org/datasets/movielens/ml-latest-small.zip\"\n", "# Use the ratings.csv file\n", "movielens_data_file_url = (\n", " \"http://files.grouplens.org/datasets/movielens/ml-latest-small.zip\"\n", ")\n", "movielens_zipped_file = keras.utils.get_file(\n", " \"ml-latest-small.zip\", movielens_data_file_url, extract=False\n", ")\n", "keras_datasets_path = Path(movielens_zipped_file).parents[0]\n", "movielens_dir = keras_datasets_path / \"ml-latest-small\"\n", "\n", "# Only extract the data the first time the script is run.\n", "if not movielens_dir.exists():\n", " with ZipFile(movielens_zipped_file, \"r\") as zip:\n", " # Extract files\n", " print(\"Extracting all the files now...\")\n", " zip.extractall(path=keras_datasets_path)\n", " print(\"Done!\")\n", "\n", "ratings_file = movielens_dir / \"ratings.csv\"\n", "df = pd.read_csv(ratings_file)\n", "df" ], "execution_count": 2, "outputs": [ { "output_type": "stream", "text": [ "Downloading data from http://files.grouplens.org/datasets/movielens/ml-latest-small.zip\n", "983040/978202 [==============================] - 0s 0us/step\n", "Extracting all the files now...\n", "Done!\n" ], "name": "stdout" }, { "output_type": "execute_result", "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
userIdmovieIdratingtimestamp
0114.0964982703
1134.0964981247
2164.0964982224
31475.0964983815
41505.0964982931
...............
1008316101665344.01493848402
1008326101682485.01493850091
1008336101682505.01494273047
1008346101682525.01493846352
1008356101708753.01493846415
\n", "

100836 rows × 4 columns

\n", "
" ], "text/plain": [ " userId movieId rating timestamp\n", "0 1 1 4.0 964982703\n", "1 1 3 4.0 964981247\n", "2 1 6 4.0 964982224\n", "3 1 47 5.0 964983815\n", "4 1 50 5.0 964982931\n", "... ... ... ... ...\n", "100831 610 166534 4.0 1493848402\n", "100832 610 168248 5.0 1493850091\n", "100833 610 168250 5.0 1494273047\n", "100834 610 168252 5.0 1493846352\n", "100835 610 170875 3.0 1493846415\n", "\n", "[100836 rows x 4 columns]" ] }, "metadata": { "tags": [] }, "execution_count": 2 } ] }, { "cell_type": "markdown", "metadata": { "id": "il7FHiD9pd5O" }, "source": [ "## データ前処理1:連番振り直し\n", "userId, movieIDは整数がラベルとして振られているが、欠番が存在する。このままでは扱いづらいため番号を振り直し。" ] }, { "cell_type": "code", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "pZ4_ulSyk6-B", "outputId": "5d695208-668b-41ba-8586-716eb55ea130" }, "source": [ "user_ids = df[\"userId\"].unique().tolist()\n", "user2user_encoded = {x: i for i, x in enumerate(user_ids)}\n", "userencoded2user = {i: x for i, x in enumerate(user_ids)}\n", "movie_ids = df[\"movieId\"].unique().tolist()\n", "movie2movie_encoded = {x: i for i, x in enumerate(movie_ids)}\n", "movie_encoded2movie = {i: x for i, x in enumerate(movie_ids)}\n", "df[\"user\"] = df[\"userId\"].map(user2user_encoded)\n", "df[\"movie\"] = df[\"movieId\"].map(movie2movie_encoded)\n", "\n", "num_users = len(user2user_encoded)\n", "num_movies = len(movie_encoded2movie)\n", "df[\"rating\"] = df[\"rating\"].values.astype(np.float32)\n", "# min and max ratings will be used to normalize the ratings later\n", "min_rating = min(df[\"rating\"])\n", "max_rating = max(df[\"rating\"])\n", "\n", "print(\n", " \"Number of users: {}, Number of Movies: {}, Min rating: {}, Max rating: {}\".format(\n", " num_users, num_movies, min_rating, max_rating\n", " )\n", ")" ], "execution_count": 3, "outputs": [ { "output_type": "stream", "text": [ "Number of users: 610, Number of Movies: 9724, Min rating: 0.5, Max rating: 5.0\n" ], "name": "stdout" } ] }, { "cell_type": "code", "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 419 }, "id": "yl_aCO4kpPRX", "outputId": "89fc5fc5-8aa5-49a6-f835-2404e4ade2b2" }, "source": [ "df" ], "execution_count": 4, "outputs": [ { "output_type": "execute_result", "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
userIdmovieIdratingtimestampusermovie
0114.096498270300
1134.096498124701
2164.096498222402
31475.096498381503
41505.096498293104
.....................
1008316101665344.014938484026093120
1008326101682485.014938500916092035
1008336101682505.014942730476093121
1008346101682525.014938463526091392
1008356101708753.014938464156092873
\n", "

100836 rows × 6 columns

\n", "
" ], "text/plain": [ " userId movieId rating timestamp user movie\n", "0 1 1 4.0 964982703 0 0\n", "1 1 3 4.0 964981247 0 1\n", "2 1 6 4.0 964982224 0 2\n", "3 1 47 5.0 964983815 0 3\n", "4 1 50 5.0 964982931 0 4\n", "... ... ... ... ... ... ...\n", "100831 610 166534 4.0 1493848402 609 3120\n", "100832 610 168248 5.0 1493850091 609 2035\n", "100833 610 168250 5.0 1494273047 609 3121\n", "100834 610 168252 5.0 1493846352 609 1392\n", "100835 610 170875 3.0 1493846415 609 2873\n", "\n", "[100836 rows x 6 columns]" ] }, "metadata": { "tags": [] }, "execution_count": 4 } ] }, { "cell_type": "markdown", "metadata": { "id": "btnjSrFrp7ss" }, "source": [ "## データ前処理2:レーティングを正規化\n", "元の評価値は0〜5の範囲を取る。これを学習しやすくするため0〜1の範囲に調整。" ] }, { "cell_type": "code", "metadata": { "id": "-Xv4nB8_k-LD" }, "source": [ "df = df.sample(frac=1, random_state=42)\n", "x = df[[\"user\", \"movie\"]].values\n", "# Normalize the targets between 0 and 1. Makes it easy to train.\n", "y = df[\"rating\"].apply(lambda x: (x - min_rating) / (max_rating - min_rating)).values\n" ], "execution_count": 5, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "Ux0iuJQYqlpv" }, "source": [ "学習用データと検証用データに分割" ] }, { "cell_type": "code", "metadata": { "id": "cl5r0qN4qhUj" }, "source": [ "# Assuming training on 90% of the data and validating on 10%.\n", "train_indices = int(0.9 * df.shape[0])\n", "x_train, x_val, y_train, y_val = (\n", " x[:train_indices],\n", " x[train_indices:],\n", " y[:train_indices],\n", " y[train_indices:],\n", ")" ], "execution_count": 6, "outputs": [] }, { "cell_type": "code", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "0QFDc3_2pybG", "outputId": "34393f37-bf4f-4ba2-94e0-4ba0a6df45b4" }, "source": [ "print(x_train[0].shape)\n", "print(x_train[0])\n", "print(y_train[0])" ], "execution_count": 7, "outputs": [ { "output_type": "stream", "text": [ "(2,)\n", "[ 431 4730]\n", "0.8888888888888888\n" ], "name": "stdout" } ] }, { "cell_type": "markdown", "metadata": { "id": "7DjuIeucqpYZ" }, "source": [ "## モデル構築\n", "word2vecでも用いるembeddingレイヤーを用いてモデルを構築している。「ユーザ x レーティング」をtf.tensordotで演算してるだけのシンプルなモデル。" ] }, { "cell_type": "code", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "53wKsC8hlBvv", "outputId": "d0370738-938c-4749-8abb-e90a87196fab" }, "source": [ "EMBEDDING_SIZE = 50\n", "\n", "\n", "class RecommenderNet(keras.Model):\n", " def __init__(self, num_users, num_movies, embedding_size, **kwargs):\n", " super(RecommenderNet, self).__init__(**kwargs)\n", " self.num_users = num_users\n", " self.num_movies = num_movies\n", " self.embedding_size = embedding_size\n", " self.user_embedding = layers.Embedding(\n", " num_users,\n", " embedding_size,\n", " embeddings_initializer=\"he_normal\",\n", " embeddings_regularizer=keras.regularizers.l2(1e-6),\n", " )\n", " self.user_bias = layers.Embedding(num_users, 1)\n", " self.movie_embedding = layers.Embedding(\n", " num_movies,\n", " embedding_size,\n", " embeddings_initializer=\"he_normal\",\n", " embeddings_regularizer=keras.regularizers.l2(1e-6),\n", " )\n", " self.movie_bias = layers.Embedding(num_movies, 1)\n", "\n", " def call(self, inputs):\n", " user_vector = self.user_embedding(inputs[:, 0])\n", " user_bias = self.user_bias(inputs[:, 0])\n", " movie_vector = self.movie_embedding(inputs[:, 1])\n", " movie_bias = self.movie_bias(inputs[:, 1])\n", " dot_user_movie = tf.tensordot(user_vector, movie_vector, 2)\n", " # Add all the components (including bias)\n", " x = dot_user_movie + user_bias + movie_bias\n", " # The sigmoid activation forces the rating to between 0 and 1\n", " return tf.nn.sigmoid(x)\n", "\n", "\n", "model = RecommenderNet(num_users, num_movies, EMBEDDING_SIZE)\n", "model.compile(\n", " loss=tf.keras.losses.BinaryCrossentropy(), optimizer=keras.optimizers.Adam(lr=0.001)\n", ")" ], "execution_count": 8, "outputs": [ { "output_type": "stream", "text": [ "/usr/local/lib/python3.7/dist-packages/tensorflow/python/keras/optimizer_v2/optimizer_v2.py:375: UserWarning: The `lr` argument is deprecated, use `learning_rate` instead.\n", " \"The `lr` argument is deprecated, use `learning_rate` instead.\")\n" ], "name": "stderr" } ] }, { "cell_type": "markdown", "metadata": { "id": "M5zABGrU6Jy6" }, "source": [ "## 学習" ] }, { "cell_type": "code", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "htHvjYItlEmE", "outputId": "83d57ca8-93b6-4223-95fb-cc4af2d04c87" }, "source": [ "history = model.fit(\n", " x=x_train,\n", " y=y_train,\n", " batch_size=64,\n", " epochs=5,\n", " verbose=1,\n", " validation_data=(x_val, y_val),\n", ")" ], "execution_count": 9, "outputs": [ { "output_type": "stream", "text": [ "Epoch 1/5\n", "1418/1418 [==============================] - 12s 7ms/step - loss: 0.6370 - val_loss: 0.6206\n", "Epoch 2/5\n", "1418/1418 [==============================] - 10s 7ms/step - loss: 0.6135 - val_loss: 0.6168\n", "Epoch 3/5\n", "1418/1418 [==============================] - 10s 7ms/step - loss: 0.6082 - val_loss: 0.6126\n", "Epoch 4/5\n", "1418/1418 [==============================] - 11s 7ms/step - loss: 0.6071 - val_loss: 0.6150\n", "Epoch 5/5\n", "1418/1418 [==============================] - 10s 7ms/step - loss: 0.6078 - val_loss: 0.6123\n" ], "name": "stdout" } ] }, { "cell_type": "markdown", "metadata": { "id": "bSzUU_z66N-U" }, "source": [ "## 学習履歴のグラフ化" ] }, { "cell_type": "code", "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 295 }, "id": "5_gPhufGlGEE", "outputId": "fea6c81f-7cb6-4653-9dc3-01a0dcdce306" }, "source": [ "plt.plot(history.history[\"loss\"])\n", "plt.plot(history.history[\"val_loss\"])\n", "plt.title(\"model loss\")\n", "plt.ylabel(\"loss\")\n", "plt.xlabel(\"epoch\")\n", "plt.legend([\"train\", \"test\"], loc=\"upper left\")\n", "plt.show()" ], "execution_count": 10, "outputs": [ { "output_type": "display_data", "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "tags": [], "needs_background": "light" } } ] }, { "cell_type": "markdown", "metadata": { "id": "7oHeirss6TFL" }, "source": [ "## 上位N件の推薦\n" ] }, { "cell_type": "code", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "5XpK2qVMlRAc", "outputId": "df1a2b84-fd7c-4da3-ce89-bebe6fd902c2" }, "source": [ "movie_df = pd.read_csv(movielens_dir / \"movies.csv\")\n", "\n", "# Let us get a user and see the top recommendations.\n", "user_id = df.userId.sample(1).iloc[0]\n", "\n", "# 視聴済み映画リスト。\n", "movies_watched_by_user = df[df.userId == user_id]\n", "\n", "# 未視聴映画リスト。\n", "# not演算、重複排除。前処理で用意した movie2movie_encoded で前処理し直し。\n", "movies_not_watched = movie_df[\n", " ~movie_df[\"movieId\"].isin(movies_watched_by_user.movieId.values)\n", "][\"movieId\"]\n", "movies_not_watched = list(\n", " set(movies_not_watched).intersection(set(movie2movie_encoded.keys()))\n", ")\n", "movies_not_watched = [[movie2movie_encoded.get(x)] for x in movies_not_watched]\n", "\n", "# モデルで予測するためのデータ整形。\n", "user_encoder = user2user_encoded.get(user_id)\n", "user_movie_array = np.hstack(\n", " ([[user_encoder]] * len(movies_not_watched), movies_not_watched)\n", ")\n", "\n", "# 学習したモデルで予測。上位10件の映画idを取得。\n", "ratings = model.predict(user_movie_array).flatten()\n", "top_ratings_indices = ratings.argsort()[-10:][::-1]\n", "recommended_movie_ids = [\n", " movie_encoded2movie.get(movies_not_watched[x][0]) for x in top_ratings_indices\n", "]\n", "\n", "# 視聴済み映画のうち上位5件を出力。\n", "print(\"Showing recommendations for user: {}\".format(user_id))\n", "print(\"====\" * 9)\n", "print(\"Movies with high ratings from user\")\n", "print(\"----\" * 8)\n", "top_movies_user = (\n", " movies_watched_by_user.sort_values(by=\"rating\", ascending=False)\n", " .head(5)\n", " .movieId.values\n", ")\n", "movie_df_rows = movie_df[movie_df[\"movieId\"].isin(top_movies_user)]\n", "for row in movie_df_rows.itertuples():\n", " print(row.title, \":\", row.genres)\n", "\n", "# 推薦候補上位10件を出力。\n", "print(\"----\" * 8)\n", "print(\"Top 10 movie recommendations\")\n", "print(\"----\" * 8)\n", "recommended_movies = movie_df[movie_df[\"movieId\"].isin(recommended_movie_ids)]\n", "for row in recommended_movies.itertuples():\n", " print(row.title, \":\", row.genres)" ], "execution_count": 11, "outputs": [ { "output_type": "stream", "text": [ "Showing recommendations for user: 174\n", "====================================\n", "Movies with high ratings from user\n", "--------------------------------\n", "French Kiss (1995) : Action|Comedy|Romance\n", "Ace Ventura: Pet Detective (1994) : Comedy\n", "Jurassic Park (1993) : Action|Adventure|Sci-Fi|Thriller\n", "Tombstone (1993) : Action|Drama|Western\n", "Batman (1989) : Action|Crime|Thriller\n", "--------------------------------\n", "Top 10 movie recommendations\n", "--------------------------------\n", "Braveheart (1995) : Action|Drama|War\n", "Taxi Driver (1976) : Crime|Drama|Thriller\n", "Godfather, The (1972) : Crime|Drama\n", "Reservoir Dogs (1992) : Crime|Mystery|Thriller\n", "Star Wars: Episode V - The Empire Strikes Back (1980) : Action|Adventure|Sci-Fi\n", "Princess Bride, The (1987) : Action|Adventure|Comedy|Fantasy|Romance\n", "Raiders of the Lost Ark (Indiana Jones and the Raiders of the Lost Ark) (1981) : Action|Adventure\n", "Lawrence of Arabia (1962) : Adventure|Drama|War\n", "Apocalypse Now (1979) : Action|Drama|War\n", "Goodfellas (1990) : Crime|Drama\n" ], "name": "stdout" } ] }, { "cell_type": "code", "metadata": { "id": "bfeU2egI8oUO" }, "source": [ "" ], "execution_count": 11, "outputs": [] } ] }