[RecBole] 女子に、乙女向けアニメを適切に推薦できるのか？！~漢の挑戦編~

## 世の中には2種類のレコメンドしかない
こんな名言を聞いたことあるでしょうか？

世の中には2種類の男しかいない「俺か、俺以外か」

僕も思うんです。

世の中には2種類のレコメンドしかない「機械学習を使うか、それ以外か」
## でも、推薦の「手法」は100種類以上ある
※ 推薦アルゴリズムの論文を見ているとそのアルゴリズムを指して「手法」と呼んでいるようです。

この手法の種類は100を超えており、真に最適な推薦の手法とは何であるのかわからないような状態となっております。

その為、自分たちで構築するよりも既製品のAPIを使ったり、SaaSを使ったりと外部に構築された推薦システムを使うことがしばしばあります。

例)
- [algolia recommendations API](https://www.algolia.com/products/recommendations/)
- [Google Recommendations AI](https://cloud.google.com/recommendations?hl=ja)
- [Amazon Personalize](https://aws.amazon.com/jp/personalize/)

## アナタが選んだレコメンド方法で本当に最適な「推薦」が行われているか評価してみたくはないでしょうか？
これが、今回の記事のテーマです。

実は「手法」ごとの評価方法やツールは既に存在しています。
クックパッドさんの記事が参考になるので、リンクを張っておきます。

https://techlife.cookpad.com/entry/2021/11/04/090000

**とはいえですよ？**
**システム的に仮に評価したとしても、本当に価値があるかどうかって最終的に人間が評価するしかなくないですか？？**

というわけで自称アニオタの私が、アニメリストの中から実際に推薦を行ってみました。

> ※ それを元にあーでもないこーでもないと考える記事となります。

## 元データ (https://myanimelist.net/)

Kaggleからアニメデータを引っ張ってきました。
myanimelistというサイトのユーザレビューデータをインタラクションデータとして利用していきます。

https://www.kaggle.com/datasets/marlesson/myanimelist-dataset-animes-profiles-reviews


## インタラクションデータとは？
「インタラクションデータ」とは「ユーザがアイテムに対してアクションした(行動を起こした)データ」となります。
機械学習レコメンドではどの手法を選んでも必ずこのデータが必要となりますので覚えておいて損は無さそうです。


手法によって必要なデータが異なり例えば、Knowledge-Aware Networkでは概念の要素の「先頭」「関連性」「末尾」の識別子を利用することで文字と文字の関係値を図る為の「ナレッジデータ」が必要となってきます。

## やらないこと
今回、Context-awareのような**アイテムとユーザの詳細データを必要とする手法では推薦しておりません。**
クックパッドさんの記事にもありますが、最終的にインタラクションオンリーの推薦の方が精度が高いという話があるためです。
(~~データのパースとその学習が面倒くさいからです~~)

## 実際に推薦していくぜ☆☆☆

### Kaggleからダウンロードしたアニメのレビューデータを学習用に整形

> ※今回の記事にPEP 8の概念は出てこないよ！ごめんね！

otsukaInter変数は大塚自身のアニメレビュー結果をマージする為の配列です。

```python
import csv
import time

file = "otsuka-anime.inter"
otsukaUid = 80909090 ## ダウンロードしたレビューデータに存在しない大塚専用ユーザ
otsukaRating = 10 ## 評価値。結構高いです。
otsukaInter = [
    [
        otsukaUid,
        269,  # BLEACH
        otsukaRating,
    ],
    [
        otsukaUid,
        37450,  # 青春ブタ野郎はバニーガール先輩の夢を見ない
        otsukaRating,
    ],
    [
        otsukaUid,
        1887,  # らき☆すた
        otsukaRating,
    ],
]

i = 0
with open("reviews.csv") as f: ## ダウンロードしたCSV
    uid_col = 0
    iid_col = 2
    rating_col = 4
    reader = csv.reader(f)
    for row in reader:
        if i == 0:
            with open(file, "w") as record_file:
                record_file.write(
                    "user_id:token\titem_id:token\trating:float\ttimestamp:float\n"
                )
        else:
            with open(file, "a") as record_file:
                record_file.write(
                    "%s\t%s\t%s\t%s\n"
                    % (row[uid_col], row[iid_col], row[rating_col], time.time())
                )
        i += 1

uid_col = 0
iid_col = 1
rating_col = 2
for row in otsukaInter:
    with open(file, "a") as record_file:
        record_file.write(
            "%s\t%s\t%s\t%s\n"
            % (row[uid_col], row[iid_col], row[rating_col], time.time())
        )
```

ざっくり生成されたデータの中身はこんな感じです。
```text
user_id:token   item_id:token   rating:float    timestamp:float
255938  34096   8       1701317310.8277361
259117  34599   10      1701317310.828107
```

RecBoleではAtomic File(~~tsv~~)と呼ばれるデータフォーマットに変換する必要があるのでそれに沿っています。

### RecBoleの設定ファイル

```yaml:configs.yaml
use_gpu: True,
field_separator: "\t"
metrics: ['Recall', 'MRR', 'NDCG', 'Hit', 'Precision']
checkpoint_dir: "/hogehoge"
data_path: "/hogehoge"
dataset: "anime-review"
USER_ID_FIELD: user_id
ITEM_ID_FIELD: item_id
RATING_FIELD: rating
TIME_FIELD: timestamp
load_col:
    inter: [user_id, item_id, rating, timestamp]
unused_col:
    inter: [timestamp]
eval_args:
    mode: full,
    group_by: user,
    order: TO,
    split: {'RS': [0.8, 0.1, 0.1]} # 学習:検証:テスト
epochs: 50, ## default: 300
stopping_step: 10, ## default: 10
train_batch_size: 4096,
eval_batch_size: 4096000,
save_dataset: True,
save_dataloaders: True
```

### 評価指標(metrics)の解説

- recall: 一度ユーザが関連したアイテムがもう一度推薦されている割合
- mrr: レコメンド上位のノイズの少なさ
- precision: 推薦アイテムのうち関連アイテムの割合
- hits: 正解のアイテムを一つ以上含むレコメンドリストを作成できた割合
- ndcg: 予測のDCGから実際のDCGを割った値

### 精度が高いと噂のRecVAEモデルちゃんに推薦してもらう

#### RecVAEモデル自体の評価結果

|手法|recall|mrr|ndcg|hit|precision|
|---|---|---|---|---|---|
|RecVAE|1|1|1|1|0.1|

高すぎて怖いくらいですね。
言わずもがなですが、モデルの評価ツールは[RecBole](https://recbole.io/)を使っています。

#### アニメの推薦に使用したプログラム

```python
from recbole.quick_start import load_data_and_model
from recbole.data.interaction import Interaction
import torch
import numpy as np

device = 'cuda:0' ## use_gpuなので
maxItem=10 ## 推薦最大数 (今回は上位からN個)

_, model, dataset, _, _, _ = load_data_and_model(
    model_file="RecVAE.pth",
)

otsuka_uid='80909090' ## レビューデータに存在しない大塚専用ユーザ

uid_field=dataset.uid_field
iid_field=dataset.iid_field

uid_list = dataset.token2id(uid_field, [otsuka_uid])
input_inter = Interaction({'user_id': uid_list})

model = model.to(device)
input_inter = input_inter.to(device)

with torch.no_grad():
  try:
    scores = model.full_sort_predict(input_inter)
  except NotImplementedError:
    input_inter = input_inter.repeat(dataset.item_num)
    input_inter.update(dataset.get_item_feature().repeat(len(input_inter)))
    input_inter = input_inter.to(device)
    scores = model.predict(input_inter)

topk_score, topk_iid_list  = torch.topk(scores, maxItem)
rec_items = dataset.id2token(iid_field, topk_iid_list.cpu().numpy())

print(rec_items) ## 推薦アイテムの出力
```

### 推薦(実行)結果

上から1~5位
※ 6位以下省略

1. 甲鉄城のカバネリ
2. 君の名は
3. SHUFFLE!
4. 僕は友達が少ない
5. ソードアートオンライン

### ... 一旦考察 ...

「僕は友達が少ない」と「甲鉄城のカバネリ」はたしかに気にはなっていた。
それはそう。
SHUFFLE!はそもそも名前も知らなかったが系統的には好きだ。
「君の名は」と「ソードアートオンライン」は既に見ている。

...このリストは大塚への最適化ではなくアニオタ全員が好きなアニメを教えてくれているだけなのでは無いだろうか？ (偏見)

(もっと白黒スッキリとした結果が欲しかった大塚は思いついた。)

**そうだ！乙女向けアニメが好きな人のインタラクションデータを元にすればいいのだ！**

乙女はきっと「[えむえむっ!](https://amzn.asia/d/1WkAhrO)」(推薦8位)を見たりしないから出てきた時点でおかしいと断定できる！(偏見)

※「えむえむっ！」とは女性に罵倒されたり嗜虐されたりすると気持ち良くなってしまうという体質に目覚めてしまった主人公の話

## 仕切り直して乙女向けにパーソナルデータをレビューに混ぜていく

```python
import csv
import time

file = "otome-anime.inter"
otomeUid = 90909090 ## レビューデータに存在しない乙女
otomeRating = 10
otomeInter = [
    [
        otomeUid,
        39533,  # ギヴン https://given-anime.com/
        otomeRating,
    ],
    [
        otomeUid,
        37597,  # 抱かれたい男1位に脅されています。
        otomeRating,
    ],
    [
        otomeUid,
        22673,  # 黒子のバスケ 2nd SEASON NG集
        otomeRating,
    ],
]


i = 0
with open("reviews.csv") as f:
    uid_col = 0
    iid_col = 2
    rating_col = 4
    reader = csv.reader(f)
    for row in reader:
        if i == 0:
            with open(file, "w") as record_file:
                record_file.write(
                    "user_id:token\titem_id:token\trating:float\ttimestamp:float\n"
                )
        else:
            with open(file, "a") as record_file:
                record_file.write(
                    "%s\t%s\t%s\t%s\n"
                    % (row[uid_col], row[iid_col], row[rating_col], time.time())
                )
        i += 1

uid_col = 0
iid_col = 1
rating_col = 2
for row in otomeInter:
    with open(file, "a") as record_file:
        record_file.write(
            "%s\t%s\t%s\t%s\n"
            % (row[uid_col], row[iid_col], row[rating_col], time.time())
        )
```


## 実際に推薦していくわよ☆☆☆

### 精度が高いと噂のRecVAEちゃんに(ry

|手法|recall|mrr|ndcg|hit|precision|
|---|---|---|---|---|---|
|RecVAE|1|1|1|1|0.1|

データ3件足した程度ですし変わらないですね。
相変わらず高い数値。


### 乙女向け推薦結果 - RecVAE

上から1~5位
※ 6位以下省略

1. 斬魔大聖デモンベイン
2. 哀しみのベラドンナ
3. 魔法少女まどか☆マギカ
4. マージナルプリンス〜月桂樹の王子達〜
5. 乃木坂春香の秘密 ふぃな～れ♪

### 一旦ふむふむ(考察)タイム

「斬魔大聖デモンベイン」と初めましてでした( ᵕ̤ᴗᵕ̤ )<コンニチハ、ドチラサマデスカ

そして、「乃木坂春香の秘密 ふぃな～れ♪」は画像検索すると布面積が大分無いというかそもそも布が無い状態なのだが、これは乙女向け推薦としては心配になってしまう。

[マージナルプリンス](https://www.at-x.com/program/detail/2043)は乙女向けといって過言ではないと言い切れると思います。(独断と偏見)

それ以外の4件の精度がどうなんだろう？と思うレベルなので、次は手法を変えてみようと思います。

## 古くから存在するBPR手法

### モデルの評価値

|手法|recall|mrr|ndcg|hit|precision|
|---|---|---|---|---|---|
|BPR|0.9918|0.9444|0.9561|0.9918|0.0992|

RecVAEと比較するとそこまでいい結果ではないですね。

### 乙女向け推薦結果 - BPR

上から1~6位
※ 7位以下省略

1. Re:ゼロから始める異世界生活
2. Air
3. ブラック・ブレット
4. サマーウォーズ
5. SHIROBAKO
6. カーニヴァル

### 考察タイム

6位ではあるものの乙女向けの[カーニヴァル](https://karneval-anime.com/)が入っており、他のラインナップ的にもまずまずの精度と言えるのではないかなと思います。

## 有名なItemKNNも試してみる

### モデルの評価値

|手法|recall|mrr|ndcg|hit|precision|
|---|---|---|---|---|---|
|ItemKNN|0.0383|0.0164|0.0215|0.0383|0.0038|

これ以上低くなるのか？と言えるくらい低い評価値ですね。。。

### 乙女向け推薦結果 - ItemKNN

上から1~8位
※ 9位以下省略

1. 攻殻機動隊 2.0
2. かぐや姫の物語
3. コードギアス 反逆のルルーシュ
4. CLANNAD 〜AFTER STORY〜
5. メイドインアビス
6. ハイキュー!! セカンドシーズン
7. 鋼の錬金術師 FULLMETAL ALCHEMIST
8. のだめカンタービレ ファイナル 峰と清良の再会

### ふむふむ
評価値は今回最低だったものの6位に[ハイキュー](https://haikyu.jp/)も入っており、精度もそこまで悪いといった印象は受けません。
ランク6位までとするならRecVAEと比較した時にネガティブ要素的にどっちも同じくらいなので変わらないなという印象です。

## 考察とまとめ
乙女系アニメを見る人はトラブルダークネスを見ないだろうの推論から、うたの☆プリンスさまっ♪やFree!で推薦結果がいっぱいになるかと思いきやそうでもない結果となりましたね。


システムの評価はRecVAEが1位でしたが、推薦された作品のネガティブ(H)度が乙女視点だと高い為、評価(数値)だけを見て推薦手法を決めてしまうのは、判断として怖いなと思いました。

逆に他の推薦手法が大きくズレているかというとズレていない為、レコメンドの実運用へ投入する際には**数値だけで判断せずきちんとエンドユーザ様の視点に立って推薦結果の中身まで確認してみる**のが大事なのかもしれないですね。

もう一つの気付きとしては
- インタラクションデータはある程度ユーザの属性ごとに分割しておく
- インタラクションデータはユーザの属性ごとにフィルターできる

のどちらかを対応すると「きゃー‼︎のびたさんのエッchiー！」を乙女に推薦してしまうみたいなことがそもそも無いため、もっと推薦精度が上がるのかな？と思いました。

ここまで読んで頂き、ありがとうございました！

## 採用PR
弊社ではマイクロサービス化を推進頂くバックエンドエンジニアを募集中となります。
会員登録なしで、応募資格まで見れます。
ぜひ、ご一読ください！

https://hrmos.co/pages/vivion/jobs/0000226

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307359/2000f749-3e82-7a47-b1c4-4444e237f79c.png" width=45%>