TensorFlow Recommendersはいいぞってこと！

# はじめに

レコメンドシステムにはさまざまなアルゴリズムが存在します。
- コンテンツベース
- 協調フィルタリング
- Matrix Factorization
- Factorization Machines
- ニューラルネットベースモデル

今回は、そのなかでもニューラルネットベースでのレコメンドシステム開発ライブラリ TensorFlow Recommenders の紹介をします。

TensorFlow Recommenders の使い方を公式のクイックスタートを自分の解釈で噛み砕いて説明します。

# TensorFlow Recommenders

## なにしてるの？

レコメンドシステムのタスクとして大きく二つあります。
- 全商品の中から、ユーザに適している商品を抽出する(Retrieval)
- 抽出した商品にランキングをすることで並び替える(Ranking)

TensorFlow Recommenders ではこれらのタスクを簡単に実装することができます。

## クイックスタートと解説

MovieLens 100Kデータセットを使用してレコメンドシステムを作成します。

### データセット用意

```bash
pip install -q tensorflow-recommenders
pip install -q --upgrade tensorflow-datasets
```

```python
from typing import Dict, Text

import numpy as np
import tensorflow as tf

import tensorflow_datasets as tfds
import tensorflow_recommenders as tfrs
```

```python
# Ratings data.
ratings = tfds.load('movielens/100k-ratings', split="train")
# Features of all the available movies.
movies = tfds.load('movielens/100k-movies', split="train")

# Select the basic features.
ratings = ratings.map(lambda x: {
    "movie_title": x["movie_title"],
    "user_id": x["user_id"]
})
movies = movies.map(lambda x: x["movie_title"])
```

まぁ、データ読んでるだけです。

### モデル構築

今回はニューラルネットベースの Matrix Factorization を実装します。

```python
user_ids_vocabulary = tf.keras.layers.StringLookup(mask_token=None)
user_ids_vocabulary.adapt(ratings.map(lambda x: x["user_id"]))

movie_titles_vocabulary = tf.keras.layers.StringLookup(mask_token=None)
movie_titles_vocabulary.adapt(movies)
```

こちらはユーザIDと映画IDの文字列を入力にできるように数値にしてくれる変換してくれる関数になります。

```python
class MovieLensModel(tfrs.Model):
  # We derive from a custom base class to help reduce boilerplate. Under the hood,
  # these are still plain Keras Models.

  def __init__(
      self,
      user_model: tf.keras.Model,
      movie_model: tf.keras.Model,
      task: tfrs.tasks.Retrieval):
    super().__init__()

    # Set up user and movie representations.
    self.user_model = user_model
    self.movie_model = movie_model

    # Set up a retrieval task.
    self.task = task

  def compute_loss(self, features: Dict[Text, tf.Tensor], training=False) -> tf.Tensor:
    # Define how the loss is computed.

    user_embeddings = self.user_model(features["user_id"])
    movie_embeddings = self.movie_model(features["movie_title"])

    return self.task(user_embeddings, movie_embeddings)
```

このモデルでは大きく二つのことをしています。
- ユーザと映画を疎な行列から密な埋め込みベクトルに変換する(user_model,movie_model)
- それぞれ変換した埋め込みベクトルからロスを計算している(task)

```python
# Define user and movie models.
user_model = tf.keras.Sequential([
    user_ids_vocabulary,
    tf.keras.layers.Embedding(user_ids_vocabulary.vocab_size(), 64)
])
movie_model = tf.keras.Sequential([
    movie_titles_vocabulary,
    tf.keras.layers.Embedding(movie_titles_vocabulary.vocab_size(), 64)
])

# Define your objectives.
task = tfrs.tasks.Retrieval(metrics=tfrs.metrics.FactorizedTopK(
    movies.batch(128).map(movie_model)
  )
)
```

次に先ほどの定義した埋め込みモデルとロスの設定を行います。
user_modelとmovie_modelではそれぞれ、ユーザIDと映画IDを疎な行列に変換して、その後に埋め込み層で密なベクトルに変換を行います。

ロスは候補の中からk個おすすめ商品を取り出した時に、実際の上位k個の実際に高評価された商品がどれくらい含まれているのかを算出しています。

```python
# Create a retrieval model.
model = MovieLensModel(user_model, movie_model, task)
model.compile(optimizer=tf.keras.optimizers.Adagrad(0.5))

# Train for 3 epochs.
model.fit(ratings.batch(4096), epochs=3)

# Use brute-force search to set up retrieval using the trained representations.
index = tfrs.layers.factorized_top_k.BruteForce(model.user_model)
index.index_from_dataset(
    movies.batch(100).map(lambda title: (title, model.movie_model(title))))

# Get some recommendations.
_, titles = index(np.array(["42"]))
print(f"Top 3 recommendations for user 42: {titles[0, :3]}")
```

最後に学習し、おすすめ商品の出力を行います。
予測では、ユーザの埋め込みベクトルから、総当たりしておすすめの商品をk件おすすめ映画を出力してくれています。
ここの総当たりですが、ScaNNなど別アルゴリズムも存在するため、それぞれのタスクにあった物を選択すれば良いと思います。

# おわりに

### 感想
- ニューラルネットでのレコメンドシステムのめんどくさいところを一行でできちゃうのイイね
- ロス周りもう少しいろいろいじってみたいなー
- TensorFlowはこのような問題にそったライブラリを提供してくれるから好き

# 引用
https://www.tensorflow.org/recommenders/examples/quickstart