ユーザｘ著者データで書籍レコメンドエンジンの精度が上がった話

# 検証の経緯
業務でMatrixFactorizationベースの書籍レコメンドエンジンを構築することになり、様々な手法を試していた。
当初はユーザｘ書籍のデータで学習させたが、ユーザｘ著者のデータを使用したモデルのほうが精度が高まった。
よって、備忘録として検証結果をここに残したい。

# 検証環境
#### 利用データ：
公開されている書籍の評価データセット。以下からダウンロードが可能である。
 　Book-Crossing Dataset
 　　http://www2.informatik.uni-freiburg.de/~cziegler/BX/

#### 検証手法：
Pythonを言語に使用。Keras (Tensorflow)で上記のデータに対してMatrixFactorizationを利用し、ユーザの評価を予測。

# 検証の流れ
### データの読み込み
利用するデータセットは以下の通り。
・BX-Books.csv：書籍の詳細情報が記載されたデータ
・BX-Book-Ratings.csv：ユーザの書籍に対する評価データ

``` recommend_keras_mf.py
import os, codecs, gc
import pandas as pd
import codecs

input_dir = "../input/"

# make dataframe from items data
col_name = ["ISBN", "Title", "Author", "Year", "Publisher", "URL-S", "URL-M", "URL-L"]
with codecs.open(input_dir + "BX-Books.csv", "r", "utf8", "ignore") as file:
    item = pd.read_csv(file, delimiter=";", names=col_name, skiprows=1, converters={"Year" : str})

# make dataframe from rating data
with codecs.open(input_dir + "BX-Book-Ratings.csv", "r", "utf8", "ignore") as file:
    rating = pd.read_csv(file, delimiter=";")
```

### データ前処理
書籍情報と評価をISBNで連結し一つのデータセットへまとめた後に、nullを除外。
データセットのカラムを【ユーザID、ISBN、著者、評価】のみに。

```recommend_keras_mf.py
# join dataframe item & rating
data = pd.merge(rating, item, how='left', on='ISBN')

# drop nan & select user-ID, ISBN, Author, Book-Rating
data.dropna(inplace=True)
data = data[data.Year.str.contains(pat='\d', regex=True)].iloc[:, [0, 1, 4, 2]]
```

### データ整形・分割
ユーザの評価が10段階あり、回帰予測の難易度が高い。よって、ビニングで評価を4段階に変換。
また、データセットをユーザｘ著者、ユーザｘ書籍に分割した。また、各データセットの評価を
ユーザと著者（書籍）でグルーピングし、平均とした。

```recommend_keras_mf.py
# binning raw_ratings
data["Book-Rating"] = data["Book-Rating"].apply(lambda x : 0 if x == 0 else (1 if x in [1,2,3,4]  else (2 if x in[5, 6, 7] else 3)))

# make user x author rating dataset
# calc rating by user and author 
data_by_author = data.groupby(['User-ID', 'Author'])["Book-Rating"].agg(['mean']).reset_index()
data_by_author.sort_values(by=['User-ID', 'Author'], inplace=True)
data_by_author.columns = ["userID", "author", "raw_ratings"]

# make user x isbn rating dataset
# calc rating by user and author 
data_by_isbn = data.groupby(['User-ID', 'ISBN'])["Book-Rating"].agg(['mean']).reset_index()
data_by_isbn.sort_values(by=['User-ID', 'ISBN'], inplace=True)
data_by_isbn.columns = ["userID", "isbn", "raw_ratings"]
```

### Keras用にデータの型を変換
Kerasは文字列を読み込むことができない。よって、UserIDと著者名をcategory値に変換。

```recommend_keras_mf.py
# convert ID and Author to category
data_by_author["user_category"] = data_by_author.userID.astype('category').cat.codes.values
data_by_author["author_category"] = data_by_author.author.astype('category').cat.codes.values
data_by_isbn["user_category"] = data_by_isbn.userID.astype('category').cat.codes.values
data_by_isbn["isbn_category"] = data_by_isbn.isbn.astype('category').cat.codes.values

# convert raw_ratings to int
data_by_author.raw_ratings = data_by_author.raw_ratings.astype("int")
data_by_isbn.raw_ratings = data_by_isbn.raw_ratings.astype("int")
```

### Kerasのトレーニング関数を定義
データセットを学習用とテスト用に分割。学習データで5回のクロスバリデーションを実施。学習済みモデルでテストデータで予測。

```recommend_keras_mf.py
from keras.callbacks import EarlyStopping, TerminateOnNaN
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split

# define callback
echeck = EarlyStopping(monitor='val_loss', patience=0, verbose=0, mode='auto')
ncheck = TerminateOnNaN()

# define training
def train_keras(model, X, y):
  k = 5
  for i in range(k):
    print("===========Round" + str(i) + " Start===========" )
    train_x, test_x, train_y, test_y = train_test_split(X, y, test_size=0.1, random_state=i)
    model.fit([train_x.iloc[:, 0], train_x.iloc[:, 1]],  train_y,  epochs=10, validation_split=0.2,
              callbacks=[echeck, ncheck], verbose=0)
    model.evaluate([test_x.iloc[:, 0], test_x.iloc[:, 1]], test_y, verbose=1)
    pred = model.predict([test_x.iloc[:, 0], test_x.iloc[:, 1]])
    print(np.sqrt(mean_squared_error(test_y, pred)))
```

### MatrixFactorizationのネットワークを定義
精度をRMSEで評価するよう設定。FunctionalAPIでユーザと著者（書籍）をインプット。次元削減を行った後に結合させている。

```recommend_keras_mf.py
# define metrics
from keras import backend as K
def rmse(y_true, y_pred):
    return K.sqrt(K.mean(K.square(y_pred - y_true), axis=-1)) 

# make network for Keras MF
def build_model():
    # another network
    another_input = keras.layers.Input(shape=[1], name='another')
    another_embedding = keras.layers.Embedding(n_another + 1, n_latent_factors, name='another-Embedding')(another_input)
    another_vec = keras.layers.Flatten(name='flatten_another')(another_embedding)
    another_vec = keras.layers.Dropout(0.2)(another_vec)

    # user network
    user_input = keras.layers.Input(shape=[1],name='User')
    user_embedding = keras.layers.Embedding(n_author + 1, n_latent_factors, name='user-Embedding')(user_input)
    user_vec = keras.layers.Flatten(name='flatten_users')(user_embedding)
    user_vec = keras.layers.Dropout(0.2)(user_vec)

    # concat author and user
    concat_vec = keras.layers.concatenate([another_vec, user_vec], axis=-1)
    concat_vec = keras.layers.Dropout(0.2)(concat_vec)

    # full-connected
    dense4 = keras.layers.Dense(4, name='FullyConnected1', activation='relu')(concat_vec)
    result = keras.layers.Dense(1, activation='relu',name='Activation')(dense4)
    model = keras.Model([user_input, another_input], result)
    model.compile(optimizer='Adagrad', loss='mse', metrics=[rmse])
    
    return model
```

### 検証
まずは、ユーザｘ著者データで検証。

```recommend_keras_mf.py
def author_train(model):
    n_users, n_another = len(data_by_author.user_category.unique()), len(data_by_author.author_category.unique())
    n_latent_factors = 3
    X = data_by_author.drop(['userID', 'author', 'raw_ratings'], axis=1)
    y = data_by_author.raw_ratings
    train_keras(model, X, y)

model = build_model()
author_train(model)
```
上記の結果はRMSE：1.02となった。一方、ユーザｘ書籍データで検証。

```recommend_keras_mf.py
def isdn_train():
    n_users, n_another = len(data_by_isbn.user_category.unique()), len(data_by_isbn.isbn_category.unique())
    n_latent_factors = 3
    
    X = data_by_isbn.drop(['userID', 'isbn', 'raw_ratings'], axis=1)
    y = data_by_isbn.raw_ratings
    
    model = build_model()
    train_keras(model, X, y)
model = build_model()
isdn_train(model)
```
上記の結果はRMSE：1.04となり、ユーザｘ著者データでのレコメンドエンジンのRMSE精度が上回ったことがわかる。

# 今後の取り組み
現在、ある書籍に対して別書籍を推奨するシステムが大半を占めている。しかし、上記の結果からユーザがクリックした
書籍の著者をインプットにし、著者や著者が執筆した書籍をレコメンドすることで精度が上がるかもしれない。

こちらはサイトに実装はしていないが、試していきたいケースだと考えている。