{
  "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": [
              "<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>1</td>\n",
              "      <td>1</td>\n",
              "      <td>4.0</td>\n",
              "      <td>964982703</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>1</th>\n",
              "      <td>1</td>\n",
              "      <td>3</td>\n",
              "      <td>4.0</td>\n",
              "      <td>964981247</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>2</th>\n",
              "      <td>1</td>\n",
              "      <td>6</td>\n",
              "      <td>4.0</td>\n",
              "      <td>964982224</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>3</th>\n",
              "      <td>1</td>\n",
              "      <td>47</td>\n",
              "      <td>5.0</td>\n",
              "      <td>964983815</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>4</th>\n",
              "      <td>1</td>\n",
              "      <td>50</td>\n",
              "      <td>5.0</td>\n",
              "      <td>964982931</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>...</th>\n",
              "      <td>...</td>\n",
              "      <td>...</td>\n",
              "      <td>...</td>\n",
              "      <td>...</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>100831</th>\n",
              "      <td>610</td>\n",
              "      <td>166534</td>\n",
              "      <td>4.0</td>\n",
              "      <td>1493848402</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>100832</th>\n",
              "      <td>610</td>\n",
              "      <td>168248</td>\n",
              "      <td>5.0</td>\n",
              "      <td>1493850091</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>100833</th>\n",
              "      <td>610</td>\n",
              "      <td>168250</td>\n",
              "      <td>5.0</td>\n",
              "      <td>1494273047</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>100834</th>\n",
              "      <td>610</td>\n",
              "      <td>168252</td>\n",
              "      <td>5.0</td>\n",
              "      <td>1493846352</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>100835</th>\n",
              "      <td>610</td>\n",
              "      <td>170875</td>\n",
              "      <td>3.0</td>\n",
              "      <td>1493846415</td>\n",
              "    </tr>\n",
              "  </tbody>\n",
              "</table>\n",
              "<p>100836 rows × 4 columns</p>\n",
              "</div>"
            ],
            "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": [
              "<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",
              "      <th>user</th>\n",
              "      <th>movie</th>\n",
              "    </tr>\n",
              "  </thead>\n",
              "  <tbody>\n",
              "    <tr>\n",
              "      <th>0</th>\n",
              "      <td>1</td>\n",
              "      <td>1</td>\n",
              "      <td>4.0</td>\n",
              "      <td>964982703</td>\n",
              "      <td>0</td>\n",
              "      <td>0</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>1</th>\n",
              "      <td>1</td>\n",
              "      <td>3</td>\n",
              "      <td>4.0</td>\n",
              "      <td>964981247</td>\n",
              "      <td>0</td>\n",
              "      <td>1</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>2</th>\n",
              "      <td>1</td>\n",
              "      <td>6</td>\n",
              "      <td>4.0</td>\n",
              "      <td>964982224</td>\n",
              "      <td>0</td>\n",
              "      <td>2</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>3</th>\n",
              "      <td>1</td>\n",
              "      <td>47</td>\n",
              "      <td>5.0</td>\n",
              "      <td>964983815</td>\n",
              "      <td>0</td>\n",
              "      <td>3</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>4</th>\n",
              "      <td>1</td>\n",
              "      <td>50</td>\n",
              "      <td>5.0</td>\n",
              "      <td>964982931</td>\n",
              "      <td>0</td>\n",
              "      <td>4</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>...</th>\n",
              "      <td>...</td>\n",
              "      <td>...</td>\n",
              "      <td>...</td>\n",
              "      <td>...</td>\n",
              "      <td>...</td>\n",
              "      <td>...</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>100831</th>\n",
              "      <td>610</td>\n",
              "      <td>166534</td>\n",
              "      <td>4.0</td>\n",
              "      <td>1493848402</td>\n",
              "      <td>609</td>\n",
              "      <td>3120</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>100832</th>\n",
              "      <td>610</td>\n",
              "      <td>168248</td>\n",
              "      <td>5.0</td>\n",
              "      <td>1493850091</td>\n",
              "      <td>609</td>\n",
              "      <td>2035</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>100833</th>\n",
              "      <td>610</td>\n",
              "      <td>168250</td>\n",
              "      <td>5.0</td>\n",
              "      <td>1494273047</td>\n",
              "      <td>609</td>\n",
              "      <td>3121</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>100834</th>\n",
              "      <td>610</td>\n",
              "      <td>168252</td>\n",
              "      <td>5.0</td>\n",
              "      <td>1493846352</td>\n",
              "      <td>609</td>\n",
              "      <td>1392</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>100835</th>\n",
              "      <td>610</td>\n",
              "      <td>170875</td>\n",
              "      <td>3.0</td>\n",
              "      <td>1493846415</td>\n",
              "      <td>609</td>\n",
              "      <td>2873</td>\n",
              "    </tr>\n",
              "  </tbody>\n",
              "</table>\n",
              "<p>100836 rows × 6 columns</p>\n",
              "</div>"
            ],
            "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": "iVBORw0KGgoAAAANSUhEUgAAAY4AAAEWCAYAAABxMXBSAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjIsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+WH4yJAAAgAElEQVR4nO3deXxU5dnw8d+VzCSTkAUIAYEAIRBUREWJKCJ7bREXtPVBrbgr2tbavm1t9enyPO3z+L5t7WJdWpGqrbWKu+K+gSwKSEBkX8MWtoSwJCF7cr1/nBMYYhIyMJMzSa7v5zOfzJxznzPXDMxcc933OfcRVcUYY4xpqRivAzDGGNO2WOIwxhgTEkscxhhjQmKJwxhjTEgscRhjjAmJJQ5jjDEhscRhTASJyD9E5H9b2HariHztZPdjTKRZ4jDGGBMSSxzGGGNCYonDdHhuF9G9IrJCRA6LyJMi0kNE3hWREhH5SES6BLW/QkRWi8hBEflERE4PWneOiCxzt3sBCDR4rstEZLm77WcictYJxnyHiGwSkf0iMktEernLRUT+LCIFIlIsIitFZIi7bpKIrHFj2ykiPzmhN8x0eJY4jHF8C7gYGARcDrwL/CeQjvM5uQdARAYBzwM/dNe9A7wpInEiEge8DvwL6Aq85O4Xd9tzgKeAO4E0YDowS0TiQwlURMYD/w+YAvQEtgEz3dVfB0a7ryPVbVPkrnsSuFNVk4EhwOxQnteYepY4jHE8oqp7VXUnMB9YrKpfqGoF8BpwjtvuGuBtVf1QVauBPwAJwIXABYAfeEhVq1X1ZWBJ0HNMA6ar6mJVrVXVfwKV7nahuB54SlWXqWolcD8wQkQygWogGTgNEFVdq6q73e2qgcEikqKqB1R1WYjPawxgicOYenuD7pc38jjJvd8L5xc+AKpaB+wAervrduqxM4duC7rfD/ix2011UEQOAn3c7ULRMIZSnKqit6rOBh4FHgMKROQJEUlxm34LmARsE5G5IjIixOc1BrDEYUyoduEkAMAZU8D58t8J7AZ6u8vq9Q26vwN4QFU7B90SVfX5k4yhE07X104AVX1YVYcBg3G6rO51ly9R1clAd5wutRdDfF5jAEscxoTqReBSEZkgIn7gxzjdTZ8BC4Ea4B4R8YvIN4HhQdvOAO4SkfPdQexOInKpiCSHGMPzwC0iMtQdH/m/OF1rW0XkPHf/fuAwUAHUuWMw14tIqtvFVgzUncT7YDowSxzGhEBV1wNTgUeAfTgD6ZerapWqVgHfBG4G9uOMh7watG0ucAdOV9IBYJPbNtQYPgJ+CbyCU+UMAK51V6fgJKgDON1ZRcCD7robgK0iUgzchTNWYkzIxC7kZIwxJhRWcRhjjAmJJQ5jjDEhscRhjDEmJJY4jDHGhMTndQCtoVu3bpqZmel1GMYY06YsXbp0n6qmN1zeIRJHZmYmubm5XodhjDFtiohsa2y5dVUZY4wJiSUOY4wxIbHEYYwxJiQdYoyjMdXV1eTn51NRUeF1KBEVCATIyMjA7/d7HYoxpp3osIkjPz+f5ORkMjMzOXYy0/ZDVSkqKiI/P5/+/ft7HY4xpp3osF1VFRUVpKWltdukASAipKWltfuqyhjTujps4gDaddKo1xFeozGmdXXoxHE8h8qqKCqt9DoMY4yJKpY4mnGwvJo9hyqoqQ3/9W4OHjzIX//615C3mzRpEgcPHgx7PMYY01KWOJrRPTmeWlWKDleFfd9NJY6amppmt3vnnXfo3Llz2OMxxpiW6rBHVbVEQpyPlICffaWVdEuKIzYmfHn2vvvuY/PmzQwdOhS/308gEKBLly6sW7eODRs2cOWVV7Jjxw4qKir4wQ9+wLRp04Cj06eUlpZyySWXcNFFF/HZZ5/Ru3dv3njjDRISEsIWozHGNMYSB/DrN1ezZldxo+vqVCmvqiXOF4M/tuWJY3CvFP7r8jOaXP/b3/6WVatWsXz5cj755BMuvfRSVq1adeSw2aeeeoquXbtSXl7Oeeedx7e+9S3S0tKO2cfGjRt5/vnnmTFjBlOmTOGVV15h6tSpLY7RGGNOhHVVHUeMCLExQnUExjmCDR8+/JhzLR5++GHOPvtsLrjgAnbs2MHGjRu/sk3//v0ZOnQoAMOGDWPr1q0RjdEYY8AqDoBmKwOAw5U1bC4spWdqgPTkQERi6NSp05H7n3zyCR999BELFy4kMTGRsWPHNnouRnx8/JH7sbGxlJeXRyQ2Y4wJZhVHC3SK95EU76OwpIq6Og3LPpOTkykpKWl03aFDh+jSpQuJiYmsW7eORYsWheU5jTEmHKziaKEeKQE2F5ZSdLiK9OT4429wHGlpaYwcOZIhQ4aQkJBAjx49jqybOHEijz/+OKeffjqnnnoqF1xwwUk/nzHGhIuohucXdDTLycnRhhdyWrt2LaeffnpI+9lcWEpVTR2n9kgmJqbtnJF9Iq/VGGNEZKmq5jRcbl1VIeiRHE91bR37y8J/XocxxrQVEU0cIjJRRNaLyCYRua+JNlNEZI2IrBaR59xl/URkmYgsd5ffFdT+E3efy91b90i+hmCd4n10ivNRWFJJXQeo1IwxpjERG+MQkVjgMeBiIB9YIiKzVHVNUJts4H5gpKoeCEoCu4ERqlopIknAKnfbXe7661W11S8iLiJ0T4lny77DHDhcRVrSyY91GGNMWxPJimM4sElV81S1CpgJTG7Q5g7gMVU9AKCqBe7fKlWtn10wPsJxhiQp3keiVR3GmA4skl/IvYEdQY/z3WXBBgGDRORTEVkkIhPrV4hIHxFZ4e7jd0HVBsDTbjfVL6WJecNFZJqI5IpIbmFhYXheEW7VkRxPVW0dB8uqw7ZfY4xpK7z+Je8DsoGxwHXADBHpDKCqO1T1LGAgcJOI1B+ver2qngmMcm83NLZjVX1CVXNUNSc9PT2sQScHfCT4YykoqaAjHJVmjDHBIpk4dgJ9gh5nuMuC5QOzVLVaVbcAG3ASyRFupbEKJ0mgqjvdvyXAczhdYq3KGesIUFVz4lXHiU6rDvDQQw9RVlZ2QtsaY8zJimTiWAJki0h/EYkDrgVmNWjzOk61gYh0w+m6yhORDBFJcJd3AS4C1ouIz22HiPiBy3CSSqtLCfgI+GMpKKk8oarDEocxpq2K2FFVqlojIncD7wOxwFOqulpEfgPkquosd93XRWQNUAvcq6pFInIx8EcRUUCAP6jqShHpBLzvJo1Y4CNgRqReQ3Pqxzq27y/jUHk1nRPjQto+eFr1iy++mO7du/Piiy9SWVnJVVddxa9//WsOHz7MlClTyM/Pp7a2ll/+8pfs3buXXbt2MW7cOLp168acOXMi9AqNMaZxEZ1yRFXfAd5psOxXQfcV+JF7C27zIXBWI/s7DAwLe6Dv3gd7Voa8WSrKwKpaEFB/LELQOP0pZ8Ilv21y2+Bp1T/44ANefvllPv/8c1SVK664gnnz5lFYWEivXr14++23AWcOq9TUVP70pz8xZ84cunXrFnLMxhhzsrweHG/TBCHOF0NdHdSexOSHH3zwAR988AHnnHMO5557LuvWrWPjxo2ceeaZfPjhh/zsZz9j/vz5pKamhjF6Y4w5MTbJITRbGRxPrCo795YiAtndk2ji6OBmqSr3338/d95551fWLVu2jHfeeYdf/OIXTJgwgV/96leN7MEYY1qPVRwnqX6so6K6luKK5q8XHix4WvVvfOMbPPXUU5SWlgKwc+dOCgoK2LVrF4mJiUydOpV7772XZcuWfWVbY4xpbVZxhEHnRD97S2IoKK4gJeBrUdURPK36JZdcwre//W1GjBgBQFJSEs8++yybNm3i3nvvJSYmBr/fz9/+9jcApk2bxsSJE+nVq5cNjhtjWp1Nqx4m+w9Xkn+gnMy0TqQk+MO233CwadWNMSfCplWPsM6JccTFxpzweR3GGNNWWOIIkxgR0pPjKauqobSy5WMdxhjT1nToxBHuyqBLpzj8sTEUFEdP1REtcRhj2o8OmzgCgQBFRUVh/WKtrzoOV9VwuLI2bPs9UapKUVERgUDA61CMMe1Ihz2qKiMjg/z8fMI55To4X9b7iis5uMtJIl4LBAJkZGR4HYYxph3psInD7/fTv3//iOz7s/l5/O/ba3nprhGcl9k1Is9hjDFe6bBdVZF0/fn9SOsUx8Mfb/Q6FGOMCTtLHBGQEBfLHaOzmL9xH19sP+B1OMYYE1aWOCJk6gX96Jzo55HZm7wOxRhjwsoSR4Qkxfu4/aL+zF5XwMr8Q16HY4wxYWOJI4JuvDCTlICPR2bbWIcxpv2wxBFBKQE/t4zszwdr9rJ2d7HX4RhjTFhY4oiwW0f2Jynex6M21mGMaScscURYaqKfmy7sxzurdrNhr11DwxjT9lniaAW3XZRFgj/Wqg5jTLtgiaMVdO0Uxw0j+vHWil1sLiz1OhxjjDkpljhayR2jsojzxfDYHKs6jDFtmyWOVtItKZ7rz+/HG8t3sa3osNfhGGPMCbPE0YruHJ1FbIzw1zmbvQ7FGGNOWEQTh4hMFJH1IrJJRO5ros0UEVkjIqtF5Dl3WT8RWSYiy93ldwW1HyYiK919PiwiEsnXEE7dUwJcd14fXlmWz479ZV6HY4wxJyRiiUNEYoHHgEuAwcB1IjK4QZts4H5gpKqeAfzQXbUbGKGqQ4HzgftEpJe77m/AHUC2e5sYqdcQCXeNHUCMCI/PtarDGNM2RbLiGA5sUtU8Va0CZgKTG7S5A3hMVQ8AqGqB+7dKVSvdNvH1cYpITyBFVRepc+m+Z4ArI/gawq5nagJX52TwUm4+uw+Vex2OMcaELJKJozewI+hxvrss2CBgkIh8KiKLRORI9SAifURkhbuP36nqLnf7/OPss377aSKSKyK54b7K38n6zpgB1KkyfW6e16EYY0zIvB4c9+F0N40FrgNmiEhnAFXdoapnAQOBm0SkRyg7VtUnVDVHVXPS09PDHPbJ6dM1kW+e25vnPt9OQXGF1+EYY0xIIpk4dgJ9gh5nuMuC5QOzVLVaVbcAG3ASyRFupbEKGOVuH3wB7cb22SZ8b9xAauuU6fOs6jDGtC2RTBxLgGwR6S8iccC1wKwGbV7HqTYQkW44XVd5IpIhIgnu8i7ARcB6Vd0NFIvIBe7RVDcCb0TwNURMv7ROTB7ai38v3sa+0srjb2CMMVEiYolDVWuAu4H3gbXAi6q6WkR+IyJXuM3eB4pEZA0wB7hXVYuA04HFIvIlMBf4g6qudLf5LvB3YBOwGXg3Uq8h0r43biCVNXXMmG9VhzGm7RDn4KT2LScnR3Nzc70Oo1H3PP8FH63dy4KfjadrpzivwzHGmCNEZKmq5jRc7vXgeId39/iBlFXV8tSCLV6HYowxLWKJw2ODeiQz6cxT+MdnWzlUVu11OMYYc1yWOKLA3eOyKa2s4enPrOowxkQ/SxxRYHCvFC4e3IOnFmyhpMKqDmNMdLPEESXuGZ9NcUUNzyzc5nUoxhjTLEscUeLMjFTGnZrOjPl5lFbWeB2OMcY0yRJHFPn+hGwOllXz7CKrOowx0csSRxQ5t28XRmV3Y8a8PMqrar0OxxhjGmWJI8rcMyGbosNV/HuxVR3GmOhkiSPKnJfZlRFZaUyfl0dFtVUdxpjoY4kjCn1/wkAKSyp5YcmO4zc2xphWZokjCo3ISuO8zC48PnczlTVWdRhjoosljigkInx/fDa7D1Xw8tL8429gjDGtyBJHlBqV3Y2hfTrz1zmbqa6t8zocY4w5whJHlBIRfjAhm50Hy3ltWZu8yKExpp2yxBHFxp6azpm9U3l0ziZqrOowxkQJSxxRzBnrGMj2/WXM+nKX1+EYYwxgiSPqXTy4B6edksyjszdRW9f+r9ZojIl+ljiinIhwz4Rs8vYd5q0VVnUYY7xniaMNmHjGKWR3T+LR2Zuos6rDGOMxSxxtQEyMcPf4gWwsKOW91Xu8DscY08FZ4mgjLjurF1ndOvHwxxut6jDGeMoSRxsRGyN8b9xA1u0p4aO1e70OxxjTgVniaEMmD+1F366JPDx7I6pWdRhjvBHRxCEiE0VkvYhsEpH7mmgzRUTWiMhqEXnOXTZURBa6y1aIyDVB7f8hIltEZLl7GxrJ1xBNfLEx3D1uIKt2FvPJ+kKvwzHGdFARSxwiEgs8BlwCDAauE5HBDdpkA/cDI1X1DOCH7qoy4EZ32UTgIRHpHLTpvao61L0tj9RriEZXndub3p0T+MvHVnUYY7wRyYpjOLBJVfNUtQqYCUxu0OYO4DFVPQCgqgXu3w2qutG9vwsoANIjGGub4Y+N4bvjBrB8x0EWbNrndTjGmA4okomjNxB8JaJ8d1mwQcAgEflURBaJyMSGOxGR4UAcsDlo8QNuF9afRSS+sScXkWkikisiuYWF7atb5+phGfRMDfCXj6zqMMa0Pq8Hx31ANjAWuA6YEdwlJSI9gX8Bt6hq/Sx/9wOnAecBXYGfNbZjVX1CVXNUNSc9vX0VK/G+WO4aM4DcbQdYmFfkdTjGmA4mkoljJ9An6HGGuyxYPjBLVatVdQuwASeRICIpwNvAz1V1Uf0GqrpbHZXA0zhdYh3ONef1oXtyPI98vMnrUIwxHUwkE8cSIFtE+otIHHAtMKtBm9dxqg1EpBtO11We2/414BlVfTl4A7cKQUQEuBJYFcHXELUC/limjc5iYV4RS7bu9zocY0wHErHEoao1wN3A+8Ba4EVVXS0ivxGRK9xm7wNFIrIGmINztFQRMAUYDdzcyGG3/xaRlcBKoBvwv5F6DdHu+vP70S0pjoc/3uh1KMaYDkQ6wuBqTk6O5ubmeh1GRDw+dzO/fXcdr373Qs7t28XrcIwx7YiILFXVnIbLvR4cNyfphgv60SXRzyNWdRhjWokljjauU7yP20dlMWd9ISvzD3kdjjGmA7DE0Q7cOKIfKQEfD8+2qsMYE3mWONqB5ICfWy/qz4dr9rJmV7HX4Rhj2jlLHO3ELRf2Jynex6NzrOowxkSWJY52IjXRz80XZvLuqj1s2FvidTjGmHbMEkc7cttF/Unwx/LobDub3BgTOZY42pEuneK4YUQ/3lyxi82FpV6HY4xpp1qUOETkByKSIo4nRWSZiHw90sGZ0N0xKot4XwyPzbGqwxgTGS2tOG5V1WLg60AX4AbgtxGLypywbknxXH9+P95YvottRYe9DscY0w61NHGI+3cS8C9VXR20zESZO0dnERsj/HXO5uM3NsaYELU0cSwVkQ9wEsf7IpIM1B1nG+OR7ikBvj28L68sy2fH/jKvwzHGtDMtTRy3AfcB56lqGeAHbolYVOak3TkmixgR/jbXqg5jTHi1NHGMANar6kERmQr8ArCJkaJYz9QE/iMng5dyd7DrYLnX4Rhj2pGWJo6/AWUicjbwY5zrfz8TsahMWHxn7ABUYbpVHcaYMGpp4qhR58Idk4FHVfUxIDlyYZlwyOiSyLfOzeD5JTsoKK7wOhxjTDvR0sRRIiL34xyG+7aIxOCMc5go991xA6itU6bPy/M6FGNMO9HSxHENUIlzPsceIAN4MGJRmbDpl9aJyUN78e/F29hXWul1OMaYdqBFicNNFv8GUkXkMqBCVW2Mo4343riBVNXUMWO+VR3GmJPX0ilHpgCfA/8BTAEWi8jVkQzMhM+A9CQuO6sX/1q4jf2Hq7wOxxjTxrW0q+rnOOdw3KSqNwLDgV9GLiwTbnePH0h5dS1PLdjidSjGmDaupYkjRlULgh4XhbCtiQKDeiQzaUhP/vHZVg6VVXsdjjGmDWvpl/97IvK+iNwsIjcDbwPvRC6sKLH6dVjxElS2jwsj3T1+IKWVNTz1qVUdxpgT52tJI1W9V0S+BYx0Fz2hqq9FLqwosfQfkDcHYuMh+2I44yoY9A2Ib5unsJzeM4WvD+7B059u4bZR/UkJ2BHVxpjQtbi7SVVfUdUfubcWJQ0RmSgi60Vkk4jc10SbKSKyRkRWi8hz7rKhIrLQXbZCRK4Jat9fRBa7+3xBROJa+hpCNvVVuPV9yLkF8nPhldvgwYEw83pY+TJUtr2LJX1/fDbFFTU889lWr0MxxrRR4pwQ3sRKkRKgsQYCqKqmNLNtLLABuBjIB5YA16nqmqA22cCLwHhVPSAi3VW1QEQGufvfKCK9gKXA6e5cWS8Cr6rqTBF5HPhSVf/W3IvMycnR3Nzc5pocX10d7FgMq1+DNW9A6R7wBY5WItnfgPikk3uOVnLrP5awbPsBFvxsPEnxLSo6jTEdkIgsVdWchsubrThUNVlVUxq5JTeXNFzDgU2qmqeqVcBMnClLgt0BPKaqB9znK3D/blDVje79XUABkC4iAowHXna3/ydw5XHiCI+YGOg3Aib9Hn60Fm55F869EXZ8Di/f6lQiL9wAq16Fqui+gNL3xw/kYFk1zy7a5nUoxpg2KJI/N3sDO4Ie5wPnN2gzCEBEPgVigf9W1feCG4jIcCAOZ2LFNOCgqtYE7bN3Y08uItOAaQB9+/Y9qRfyFTEx0O9C5zbxt7B9Eax53alE1s4CXwIM+joMvtIZE4nrFN7nP0nn9O3CqOxuzJiXx00jMkmIi/U6JGNMG+L1IbU+IBsYC1wHzBCRzvUrRaQn8C/gFlUN6cJRqvqEquaoak56enoYQ24gJhYyR8KkB51K5Oa34ZypsG0hvHwL/H4AvHiT08UVRZXIDyZkU3S4in8vtqrDGBOaSFYcO4E+QY8z3GXB8oHFqloNbBGRDTiJZImIpOAc9vtzVV3kti8COouIz606Gtund2JiIfMi53bJ72DbZ0crkTWvgz/RqUAGXwnZX4e4RM9CzcnsyoisNKbPy2PqBf0I+K3qMMa0TCQrjiVAtnsUVBxwLTCrQZvXcaoNRKQbTtdVntv+NeAZVa0fz8Cd2n0OUD/dyU3AGxF8DScuJhb6j4JL/wg/Xg83vQlnXwdbF8BLN8GDA+Clm52kUuXN5V3vmZBNYUklLyzZcfzGxhjjavaoqpPeucgk4CGc8YunVPUBEfkNkKuqs9zB7j8CE4Fa4AH3aKmpwNPA6qDd3ayqy0UkC2egvSvwBTBVVZud9jUsR1WFS10tbPvUPTprFpTtA38npxI54yrnKC1/QquEoqpMmb6QHfvLmfvTscT7rOowxhzV1FFVEU0c0SKqEkew2pqjSWTtm0eTyKkTnSQy8GsRTyLzNxZyw5Of88BVQ7j+/H4RfS5jTNtiiSMaE0ew2hrYtiAoiRRBXBIMCk4igbA/raryzb99RkFxJXN+MpY4n9fHSxhjosUJncdhWlGsD7LGwuV/gR9vgBtehzOvhs2z4YXrnTGRV26HdW9DdfguAysi3DM+m50Hy3nti/yw7dcY035ZxRHtamtg6zxnwsW1b0L5fohLhlMvgTOuhAETTroSUVWuePRTDpVXM/vHY/DF2u8JY4xVHG1XrA8GjIcrHoafbHDmzxpyFWz6EGZ+2zlj/dVpsO4dqDmxS8OKCN8fP5Dt+8t4Y/muML8AY0x7YxVHW1VbDVvmHq1EKg5CfAqcOsmtRMaDL77Fu1NVJj28gMrqWj780RhiYySCwRtj2gKrONqbWL8zYD75Ubh3E0x9BQZfARveg+evdSuRO2H9ey2qRJyxjoHk7TvMWyus6jDGNM0qjvampgq2zHOOzlr3lluJpMJpk5yjs7LGga/xmejr6pSJf5mHKrz/w9HEWNVhTIdmFUdH4YuD7K/BlY/BTzbC9S/D6ZfB+nfguSnwh4Hw2ndgwwdOkgkSEyPcPT6bjQWlvLd6j0cvwBgT7azi6ChqqiDvE2fOrLVvQeUhCKTCaZc5lUj/MeCLo7ZOufjPc4mLjeGde0ZZ1WFMB2YnAHb0xBGspsq5JO7q153zQo4kkcvhjCt57dAA/s/La3nihmF8/YxTvI7WGOORphKHXf6tI/LFOXNjDfqGM3C+eY5bicyC5c9yZaAzcUnDWPDeRi4+9btIE2MixpiOySoOc1RNpXOm+urXqVrzFnE1pVTHdcZ/hlOJ0H+MczSXMaZDsK4qSxwhqa4s5+d/eJhJMQsZo7lIVQkkdIHTL3euJ9J/tCURY9o566oyIfHHJ3D2167l5teG8K+bHmWUrHAO8V31Gix7BhK6OkdrnXEVZI52znA3xnQIVnGYJlXW1DL2wU/o3TmBl+4agYg4Eyxu/thJIuvfhapSSEw7enRW5ihLIsa0E1ZxmJDF+2L5ztgB/OqN1SzMK+LCAd2cCRVPu9S5VZfDpo+dgfVVr8Cyf0JiN2dW37OugV7ngNjhvMa0N1ZxmGZVVNcy+vdzyErvxMxpI5puWF0Omz6ClS85lUhtFXQb5CSQs66Bzn2a3tYYE5XszHFzQgL+WO4cM4BFefv5fMv+phv6E5yB8ynPOGesX/4Xpwtr9v/AQ0PgH5fBsn9BRXHrBW+MiQirOMxxlVfVMur3szm9Zwr/uu380DY+sBVWvAhfzoT9m8EXcGbwPftaZwZfOzLLmKhlFYc5YQlxsdwxKov5G/exbPuB0DbukgljfgrfXwq3fwzn3OBMffLcFPjjafDuz2DnMugAP2CMaS+s4jAtcriyhot+N5uhfTrz9C3DT25nNVXOeMiKmc6077WVQeMhU6Bz3/AEbYw5KVZxmJPSKd7H7aOymLO+kJX5h05uZ744Z5r3Kc84VzW8/C/O0Viz/wceOhOevtQ5V6TiJJ/HGBMRljhMi904oh8pAR8Pz94Yvp0mdIZhN8Ot78IPvoRxv4DSPTDr+/CHQfDSzbDhfeeKh8aYqGCJw7RYcsDPrRf158M1e1mzKwJHR3XJhDH3wt25cPtsOPdG56JUNh5iTFSJaOIQkYkisl5ENonIfU20mSIia0RktYg8F7T8PRE5KCJvNWj/DxHZIiLL3dvQSL4Gc6xbLuxPcryPR+eEsepoSAQyhsGkB+HH6+G6mZA5EnKfhhnj4LHhMO8PcHB75GIwxjQpYmeOi0gs8BhwMZAPLBGRWaq6JqhNNnA/MFJVD4hI96BdPAgkAnc2svt7VfXlSMVumpaa6OfmkZk8MnsTG/aWMKhHcmSfMNYPp17i3MoPOmepf/mCMx4y+3+g30Vw9jUweLJzTRFjTMRFsuIYDmxS1TxVrQJmApMbtLkDeExVDwCoakH9CqYRyNoAABajSURBVFX9GCiJYHzmBN06sj+d4mJ5dPam1n3iloyHrH/PxkPam7o6KFgHXzwLcx+EjR/ZiaQei+RcVb2BHUGP84GGZ48NAhCRT4FY4L9V9b0W7PsBEfkV8DFwn6pWNmwgItOAaQB9+9rhneHUpVMcN4zIZPq8zdwzIZuB3ZM8CCLTGQ8Z/RNn3GPFTGe+rNWvOUdoDfmWU4n0Otfmy2prinfDzlzYuRTyc2HXcqhq8BtSYqDHGdB3BPS9wPmb0subeDugiJ3HISJXAxNV9Xb38Q3A+ap6d1Cbt4BqYAqQAcwDzlTVg+76scBPVPWyoG16AnuAOOAJYLOq/qa5WOw8jvDbV1rJRb+bzaQhPfnTNVEyzFRb7Zwf8uVMd74sOz8k6lWWwK4vnCSxcynkL4WSXc66GD+cMgR6D4PeOc7flJ5Ou20LYftCyF8C1WVO+879jk0k6afaj4aT5MXsuDuB4JntMtxlwfKBxapaDWwRkQ1ANrCkqZ2q6m73bqWIPA38JHwhm5bqlhTP1PP78fRnW7lnQjaZ3Tp5HVIj4yFvwAobD4katTVQsCaomlgKhesA98dr1yznIIj6JHHKmc5szA1ljXVu4PxY2LMSti+C7Z85U/6vmOmsS+jqJhE3kfQc6pxDZE5aJCsOH7ABmICTMJYA31bV1UFtJgLXqepNItIN+AIYqqpF7vqxNFJxqOpuERHgz0CFqjZ6xFY9qzgio6C4glG/n8Pkob34/dVnex1O0w5sg5UvOoPqRRvd+bIugbOuhYETbL6sSFB1jnrbmet0Je5c6nQ51ZQ76xO6QkZOUDVxLiR2Dc/z7s9zqpH6qmT/ZmedL+A8V30i6XOe/YA4Dk8uHSsik4CHcMYvnlLVB0TkN0Cuqs5yv/z/CEwEaoEHVHWmu+184DQgCSgCblPV90VkNpAOCLAcuEtVS5uLwxJH5Pz3rNU8u2gbc34ylj5dE70Op3mqsGuZ05W16hUoK7LxkHApP3A0QdTfDhc663wB6Hm2myTcW5fM1nuvSwvcimSRk0h2fwlaa+MkLWDXHLfEERG7D5Uz5vefcHVOBv/3qjO9DqflGhsPSct2EshZ19h4SHNqKmHPqqNdTjuXQlH9EXbijC30HuZUEb1znC/naKrqKkud2OsTyY4lUH3YWddwnKTbIIjpuOdJW+KwxBExP39tJS/m7mDuvePo1TnB63BCFzwesu1TZ1m/kc7U7x19PEQVija7CcJNFHtWOhfqAkg6xe1ycpNEr6Ft7/06ZpzE7d6qr5Y6+DiJJQ5LHBGTf6CMsQ9+wvXn9+XXk4d4Hc7JaXI85BoY+LXo+uUcCaWFxyaJnUuPTjbp7+RcDjhj2NGxiZRe7a97L3icZPtCJ6HUV1S+gPPa+47oEOMkljgscUTUfa+s4NUvdrLgp+PontLIkTBtzZHxkBdg1cvtczykqszp7w9OFPXTuEgMdD/j2CSRfirExHobs1eaGidBoMcQ6Nc+x0kscVjiiKjtRWWM++MnXHNeH/538hBiYtr4l2qw2mrY5B7mue6dtjkeUlcLheuPTRJ717hffkBqX6e7qf5Ip55nQ1wUHGIdraoOOycn1lclx4yT9D1akbTxcRJLHJY4Iu7+V1fy/OfbyerWiWmjs7jq3N7E+9rZL9SmxkPOugbOuDJ6ui0O7QxKEsuck+yq3IMP41OPTRK9h0FS9+b3Z5pXWwN73XGSbZ85fw+7MygldIE+7jhJvwvb1DiJJQ5LHBFXU1vHe6v38PjczazaWUx6cjy3juzPt8/vS2pCOxwbaDgeEhvvjIecfW3rjodUFB979vXOpVDinicb43dOpOs97Gii6Dqgzf4CbjOOjJMEDbh/ZZzkAuh7YVSPk1jisMTRalSVzzYX8fjczczfuI+keB/fPr8vt47szymp7WD8o6FGx0PSnPGQs651ft2Hazykthr2rnYTxDKnoihcz9GzrwccmyR6DGn87GvT+koLYceio1VJw3GSvkFVSZSMk1jisMThiVU7D/HEvDzeWrGL2BjhyqG9uXNMFgO7R3g6dq80Nx5y5hTo0q/l+1KFg9ucvvT6JLH7S6ipcNYnph2dniNjmDNgH46zr03rODJOsujovFv13YlHxkncqsSjcRJLHJY4PLVjfxl/n5/HC7k7qKiu42un9+CuMVnkZLbjL7qKQ854yJcvwLYFzrL68ZDBk51p4oOV7Q86+9odwC4rctb5Ak7feH2S6D3MOVmtrR/ZZY4KHiepnzKlsXGSviOc82V88REPyRKHJY6oUFRayTMLt/HPhVs5WFZNTr8u3DlmABNO696+jsRq6OB2WPGiM6i+b8PR8ZDew5yTz3bmOn3iwNGzr3OODmJ3H9z+zyExx/rKOMkiZywNGoyTjICM8776QyQMLHFY4ogqZVU1vLhkBzPmb2HnwXIGdk9i2ugsrhzamzhfOx64VXUGsle8ACtfhrJ9kNzz6NFNGTlOZRFI8TpSE41KC2HH4qMD7ru/hLoavjJO0ncEpPY+6aezxGGJIyrV1Nbx9srdPD43j7W7i+mREs9tF/XnuuF9SQ6081/YtdXO4b1J6V5HYtqqqsNfvT5Jw3GScf/pTCp5AixxWOKIaqrKvI37mD53M59tLiI54GPqBf245cLM9nEmujGtobYG9q46tnvrrvknfJ6OJQ5LHG3GivyDTJ+bx7urduOLieGb5/bmjtFZDEj34BK1xrRlqid1AIUlDkscbc7WfYeZMT+Pl5bmU11bx9cH9+DOMQM4t28Xr0MzpkOwxGGJo80qLKnkmYVbeWbhNg6VVzO8f1fuGpPFuFO7I3Y4qjERY4nDEkebd7iyhplLdvDk/Dx2Harg1B7JTBudxeVn92rfR2IZ4xFLHJY42o3q2jre/HIX0+fmsX5vCT1TA9x2UX+uHd6XpHif1+EZ025Y4rDE0e6oKp+sL+TxuZtZvGU/KQEfN47I5KYLM0lPjvxZtca0d5Y4LHG0a19sP8D0uXm8v2YP/tgYrh6WwbRRWWR2s2tKGHOiLHFY4ugQNheW8vf5ebyydCfVdXVcMuQU7hw9gLP7hH86BmPaO0scljg6lILiCp7+bCvPLtpGSUUNI7LSuHNMFmMGpduRWMa0kCUOSxwdUklFNc9/vp0nF2xhb3Elp52SzF1jBnDpWT3xx9qRWMY0xxKHJY4OraqmjteX7+SJeXlsKiild+cEbh/Vn2vO60NinB2JZUxjmkocEf3JJSITRWS9iGwSkfuaaDNFRNaIyGoReS5o+XsiclBE3mrQvr+ILHb3+YKItI2L9xpPxflimJLThw9+OJq/35hDz9QAv35zDRf+djZ/+nADRaWVXodoTJsRsYpDRGKBDcDFQD6wBLhOVdcEtckGXgTGq+oBEemuqgXuuglAInCnql4WtM2LwKuqOlNEHge+VNW/NReLVRymMblb9/P43Dw+WruXgN9JLLdflEXftESvQzMmKnhRcQwHNqlqnqpWATOByQ3a3AE8pqoHAOqThnv/Y6AkuLE4o5rjgZfdRf8EroxM+Ka9y8nsyt9vyuGjH43mirN78fzn2xn7hzl8//kvWLXzkNfhGRO1Ipk4egM7gh7nu8uCDQIGicinIrJIRCYeZ59pwEFVrWlmn8aEZGD3ZH5/9dnM/+l47hiVxZx1BVz2yAKm/n0xCzbuoyOMAxoTCq8PK/EB2cBY4DpghoiE5YB7EZkmIrkikltYWBiOXZp27pTUAPdPOp1P7xvPzyaexvq9JUx9cjGXPbKAWV/uoqa2zusQjYkKkUwcO4E+QY8z3GXB8oFZqlqtqltwxkSym9lnEdBZROoPg2lsnwCo6hOqmqOqOenpdoU103KpCX6+M3YA8386jt9+80zKq2q55/kvGPfHT3hm4VbKq2q9DtEYT0UycSwBst2joOKAa4FZDdq8jlNtICLdcLqu8praoTp9BnOAq91FNwFvhDdsYxwBfyzXDu/LRz8aw+NTh5HWKZ5fvbGakb+bzV8+2siBw1Veh2iMJyJ6HoeITAIeAmKBp1T1ARH5DZCrqrPcwe4/AhOBWuABVZ3pbjsfOA1Iwqk0blPV90UkC2egvSvwBTBVVZs9ltKOqjLhoKp8vmU/0+flMXtdAQn+WK45rw+3j+pPRhc7Esu0P3YCoCUOE0br95Qwfd5mZi3fhQKXn9WTaaMHMLhXitehGRM2ljgscZgI2HWwnCcXbOH5z7dTVlXL6EHp3DUmixFZaTYnlmnzLHFY4jARdKismmcXb+PpT7ewr7SKszNSuXPMAL5xxinExlgCMW2TJQ5LHKYVVFTX8sqyfGbMy2NrURn90hK5Y1QWVw/LIOCP9To8Y0JiicMSh2lFtXXK+6v38PjczazIP0S3pDhuvjCTGy7IJDXR73V4xrSIJQ5LHMYDqsrCvCKmz81j7oZCEuNiuW54X267qD+9Oid4HZ4xzbLEYYnDeGzNrmKemLeZN1fsRoArhvbi+vP70qdLImlJ8TYWYqKOJQ5LHCZK7NhfxpMLtvDCkh2UVztnoccIdO0UT/fkeLqnuH+TA0fup7uP05PjbazEtBpLHJY4TJQ5cLiKxVv2U1haSWFxBQUlle6tgoLiSvaVVlLXyMczJeCje0ogKKEcTTLBCSYl4LNDgs1JaSpx2KXPjPFIl05xTBxySpPra+uU/YernERSUklhceXR+26SWbb9AAXFlVTWfHUCxnhfjFuxBNzkEpRYgqqarp3irJusjauorqWkooaSimqK3b8lFTUUl1dz6Vk9SQ6E94AMSxzGRKnYGCHd/bI/o5l2qkpxRQ2FwUklKMkUFFeysaCUTzfto7ii5ivbx8YIaZ3ivppkUgKkJ8Uf010W77NusnCrqa1zv/RrKK6opjjoSz94eX0yOPrYTRTlNVQ1M3PzsH5dLHEYY44lIqQm+ElN8DOwe3KzbSuqa91qxekOKyw9NsnsOVTBivxDFB2upLFe7M6J/mMqlyP3G3SdJcV3jG6yujqltOrYL/mSoC//+gRwTBXQ4Eu/fpyrOYlxsaQE/CQHfCQHfHTtFEe/tE5HHqcE/KQEfCQH/KQkOH+T3cfdk+PD/rotcRjTgQT8sfTpmkifrs1PylhTW+d2kx1NMvX367vJPt+yn8KSykZ/7Sb4Y48Z5E8PHo9JOVrVdEmMI8ajbjJVpby6luLyY7t4Gnb1NNYFVL+utKqm0QQbLM4XE/TF7iMlwU/P1ADJ8f4jj+u/5Ou//JMDPlLd5UnxPnyxXl866ViWOIwxX+GLjXG+4FMCQGqT7VSVQ+XVQeMuwUmmkoLiCtbuKWbehkpKKr/aTeaLEbod0x3WYNDfTTLdkuKJ8x375VlZc/RLv+Ev+WN/6Tfd1VPb2NEHDeJLDvoyTwn46ds18Zhf9ilBv/qPtEs4Wh20x+49SxzGmBMmInROjKNzYhyDejTfTVZWVXOkWikorjwyJlN/yz9QzhfbD1LUxHVOunaKIzng43BlDcUVNVQ1ckDAsbFBUrzvSBdPSsDPKSkBsrsf+yv/6Je+8zg1qKsnwR/bIbrcQmWJwxjTKhLjfPRL89EvrVOz7apr6ygqrWq0i6ykooakY37hH/ulH9y/nxTn86wbrL2zxGGMiSr+2BhOSQ1wSmrA61BME6JrxMUYY0zUs8RhjDEmJJY4jDHGhMQShzHGmJBY4jDGGBMSSxzGGGNCYonDGGNMSCxxGGOMCUmHuJCTiBQC205w827AvjCGEy4WV2gsrtBYXKFpr3H1U9X0hgs7ROI4GSKS29gVsLxmcYXG4gqNxRWajhaXdVUZY4wJiSUOY4wxIbHEcXxPeB1AEyyu0FhcobG4QtOh4rIxDmOMMSGxisMYY0xILHEYY4wJiSUOl4hMFJH1IrJJRO5rZH28iLzgrl8sIplREtfNIlIoIsvd2+2tENNTIlIgIquaWC8i8rAb8woROTfSMbUwrrEicijovfpVK8XVR0TmiMgaEVktIj9opE2rv2ctjKvV3zMRCYjI5yLypRvXrxtp0+qfxxbG1eqfx6DnjhWRL0TkrUbWhff9UtUOfwNigc1AFhAHfAkMbtDmu8Dj7v1rgReiJK6bgUdb+f0aDZwLrGpi/STgXUCAC4DFURLXWOAtD/5/9QTOde8nAxsa+Xds9feshXG1+nvmvgdJ7n0/sBi4oEEbLz6PLYmr1T+PQc/9I+C5xv69wv1+WcXhGA5sUtU8Va0CZgKTG7SZDPzTvf8yMEEifxX7lsTV6lR1HrC/mSaTgWfUsQjoLCI9oyAuT6jqblVd5t4vAdYCvRs0a/X3rIVxtTr3PSh1H/rdW8OjeFr989jCuDwhIhnApcDfm2gS1vfLEoejN7Aj6HE+X/0AHWmjqjXAISAtCuIC+JbbvfGyiPSJcEwt0dK4vTDC7Wp4V0TOaO0nd7sIzsH5tRrM0/esmbjAg/fM7XZZDhQAH6pqk+9XK34eWxIXePN5fAj4KVDXxPqwvl+WONq+N4FMVT0L+JCjvyrMVy3DmXvnbOAR4PXWfHIRSQJeAX6oqsWt+dzNOU5cnrxnqlqrqkOBDGC4iAxpjec9nhbE1eqfRxG5DChQ1aWRfq56ljgcO4HgXwYZ7rJG24iID0gFiryOS1WLVLXSffh3YFiEY2qJlryfrU5Vi+u7GlT1HcAvIt1a47lFxI/z5fxvVX21kSaevGfHi8vL98x9zoPAHGBig1VefB6PG5dHn8eRwBUishWnO3u8iDzboE1Y3y9LHI4lQLaI9BeROJzBo1kN2swCbnLvXw3MVnekycu4GvSDX4HTT+21WcCN7pFCFwCHVHW310GJyCn1/boiMhzn/3/Ev2zc53wSWKuqf2qiWau/Zy2Jy4v3TETSRaSzez8BuBhY16BZq38eWxKXF59HVb1fVTNUNRPnO2K2qk5t0Cys75fvRDdsT1S1RkTuBt7HOZLpKVVdLSK/AXJVdRbOB+xfIrIJZwD22iiJ6x4RuQKoceO6OdJxicjzOEfbdBORfOC/cAYKUdXHgXdwjhLaBJQBt0Q6phbGdTXwHRGpAcqBa1sh+YPzi/AGYKXbPw7wn0DfoNi8eM9aEpcX71lP4J8iEouTqF5U1be8/jy2MK5W/zw2JZLvl005YowxJiTWVWWMMSYkljiMMcaExBKHMcaYkFjiMMYYExJLHMYYY0JiicOYKCfODLVfmfHUGK9Y4jDGGBMSSxzGhImITHWv17BcRKa7E+KVisif3es3fCwi6W7boSKyyJ0M7zUR6eIuHygiH7mTCi4TkQHu7pPcSfPWici/W2FmZmOaZInDmDAQkdOBa4CR7iR4tcD1QCecs3fPAObinM0O8AzwM3cyvJVBy/8NPOZOKnghUD/tyDnAD4HBONdnGRnxF2VME2zKEWPCYwLOhHZL3GIgAWfq7TrgBbfNs8CrIpIKdFbVue7yfwIviUgy0FtVXwNQ1QoAd3+fq2q++3g5kAksiPzLMuarLHEYEx4C/FNV7z9mocgvG7Q70Tl+KoPu12KfXeMh66oyJjw+Bq4Wke4AItJVRPrhfMaudtt8G1igqoeAAyIyyl1+AzDXvQpfvohc6e4jXkQSW/VVGNMC9qvFmDBQ1TUi8gvgAxGJAaqB7wGHcS748wucrqtr3E1uAh53E0MeR2fDvQGY7s5sWg38Ryu+DGNaxGbHNSaCRKRUVZO8jsOYcLKuKmOMMSGxisMYY0xIrOIwxhgTEkscxhhjQmKJwxhjTEgscRhjjAmJJQ5jjDEh+f8X8bdnwv8YvwAAAABJRU5ErkJggg==\n",
            "text/plain": [
              "<Figure size 432x288 with 1 Axes>"
            ]
          },
          "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": []
    }
  ]
}