はじめに
はじめまして。SIerのおっさんです。
SIerで働いておりましたが今後に漠然とした不安を感じ、この春から給付金制度を利用してAidemyさんのデータ分析講座を受講しています。最終成果物のテーマを決めるのにかなり悩みましたが、子供といっしょに楽しめそうなアニメの「レコメンド」にしました。本格的に機械学習の勉強をすることは初めてでまとまっていない部分も多々あるかと思いますが、温かい目で読んでいただけますと幸いです。
目次
1.レコメンドとは
2.データ項目説明
3.候補生成(コンテンツ特徴量の作成)
4.アニメEmbeddingベクトルの可視化
5.リランク
・ユーザー特徴量の作成
・学習データセットの作成
・モデルの定義と学習
・推論
6.分析
7.リランクその2(ユーザー特徴量次元削減による精度改善)
8.まとめ
1.レコメンドとは
「レコメンド」とは簡単に言うと「おすすめシステム」のことです。
普段インターネットをやっているとAmazon、Netflix、Spotifyなどで
「この商品を購入したお客様はこちらも一緒にお買い上げいただいてます」
「あなたにおすすめの動画一覧」
など見かけることも多いかと思います。
これは「レコメンド機能(レコメンドエンジン)」と呼ばれるもので、近年ではインターネット上の様々なサービスで使われています。
今回はこのレコメンド機能を作ってみたいと思います。
尚、プログラミングはすべてGoogle colaboratory上で行っております。
2.データ項目説明
レコメンドを実装する上で、元となるデータは欠かせません。
企業であれば自社のWEBサイトで蓄積されていく顧客データを元にレコメンドをつくることができますが、残念ながら私はそのようなデータは持ち合わせておりません。
そこで今回はKaggleで公開されている「Anime Recommendation Database 2020」というデータセットを利用していきたいと思います。
・「Anime Recommendation Database 2020」
myanimelist.net で蓄積された 325,772 人のユーザーと 17,562 のアニメからなるデータセットです。下記の5つのファイルで構成されています。
ファイル名 | ファイルの概要説明 | データ件数 |
---|---|---|
Anime.csv | アニメのマスタ情報(タイトル、ジャンル、話数、放送日など) | 17,562 |
Anime_with_ synopsis.csv |
アニメのあらすじ。 | 16,214 |
Animelist.csv | ユーザーが視聴しているアニメとその評価点数のリスト。(視聴途中のものや評価をつけていないアニメも含まれる。) | 109,224,747 |
rating_complete.csv | ユーザーが完全に視聴したアニメとその評価点数のリスト。(視聴途中のものや評価をつけなかったアニメは含まれない) | 57,633,278 |
watch_status.csv | 視聴状況のマスタ(現在視聴中、視聴完了、視聴保留中、視聴中断、視聴予定) | 5 |
・データ読込
まずは必要なデータを読み込んでいきます。
表に記載の通り非常に件数の多いデータなので、処理に必要となるanime.csv、rating_complete.csvの2つだけを読み込みます。
import pandas as pd
df_anime = pd.read_csv('/content/drive/MyDrive/anime/anime.csv')
df_rating = pd.read_csv('/content/drive/MyDrive/anime/rating_complete.csv')
df_anime.head(5)
試しにanime.csvの最初の5行を表示してみます。
カウボーイビバップがIDの1番目、2番目に来ているのが興味深いですね。
海外でも人気があるようなので、データの作者の並々ならぬこだわりがあるのかもしれません。
ファイルレイアウトは下記のようになっています。
■ Anime.csv
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 17562 entries, 0 to 17561
Data columns (total 35 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 MAL_ID 17562 non-null int64 アニメのMyAnimelistID
1 Name 17562 non-null object アニメのフルネーム
2 Score 17562 non-null object アニメの平均スコア
3 Genres 17562 non-null object このアニメのジャンルのコンマ区切りリスト
4 English name 17562 non-null object アニメの英語でのフルネーム
5 Japanese name 17562 non-null object アニメの日本でのフルネーム
6 Type 17562 non-null object テレビ、映画、OVAなど
7 Episodes 17562 non-null object 章の数、話数
8 Aired 17562 non-null object 放送日
9 Premiered 17562 non-null object 初演、シーズンプレミア
10 Producers 17562 non-null object 製作者のコンマ区切りリスト
11 Licensors 17562 non-null object ライセンサーのコンマ区切りリスト
12 Studios 17562 non-null object 制作スタジオのコンマ区切りリスト
13 Source 17562 non-null object 原作(マンガ、ライトノベル、本など)
14 Duration 17562 non-null object エピソードごとのアニメの長さ
15 Rating 17562 non-null object 評価(R指定など)
16 Ranked 17562 non-null object ランク付け
17 Popularity 17562 non-null int64 リストに追加したユーザーの数に基づく人気度数
18 Members 17562 non-null int64 「グループ」に所属しているユーザー数
19 Favorites 17562 non-null int64 「お気に入り」に登録しているユーザー数
20 Watching 17562 non-null int64 アニメを見ているユーザーの数
21 Completed 17562 non-null int64 視聴完了したユーザーの数
22 On-Hold 17562 non-null int64 視聴保留にしているユーザーの数
23 Dropped 17562 non-null int64 アニメをドロップしたユーザーの数
24 Plan to Watch 17562 non-null int64 アニメを観る予定のユーザー数
25 Score-10 17562 non-null object 10点を獲得したユーザーの数
26 Score-9 17562 non-null object 9点を獲得したユーザーの数
27 Score-8 17562 non-null object 8点を獲得したユーザーの数
28 Score-7 17562 non-null object 7点を獲得したユーザーの数
29 Score-6 17562 non-null object 6点を獲得したユーザーの数
30 Score-5 17562 non-null object 5点を獲得したユーザーの数
31 Score-4 17562 non-null object 4点を獲得したユーザーの数
32 Score-3 17562 non-null object 3点を獲得したユーザーの数
33 Score-2 17562 non-null object 2点を獲得したユーザーの数
34 Score-1 17562 non-null object 1点を獲得したユーザーの数
dtypes: int64(9), object(26)
memory usage: 4.7+ MB
■ rating_complete.csv
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 57633278 entries, 0 to 57633277
Data columns (total 3 columns):
# Column Dtype
--- ------ -----
0 user_id int64 ユーザーID
1 anime_id int64 このユーザーが評価したアニメのID。
2 rating int64 このユーザーが割り当てた評価。
dtypes: int64(3)
memory usage: 1.3 GB
これらのデータを使って進めていきたいと思います。
3.候補生成(コンテンツ特徴量の作成)
「カウボーイビバップをご覧になったあなたへ」を表示したいとき、どのようにすればおすすめアニメの候補が得られるでしょうか?
カウボーイビバップを過去に見た人たちが高く評価している他のアニメを集めてくれば、なんとなく候補が得られそうです。
そこで、まずは読み込んだデータの中にあるユーザーの評価(rating)を活用して、各アニメの特徴量を作成していきます。
いろいろな手法がありますが、ここではNMF(Nonnegative Matrix Factorization: 非負値行列因子分解)という手法を用いたいと思います。
・NMF(Nonnegative Matrix Factorization)
NMFとは「負の値が含まれていない行列の掛け算形式で、元の行列を分解する」手法です。
数式で表すと、分解前の行列をXとしたときには、
X≅WH
となるような非負値行列WとHを発見することに相当します。
(≅は「およそ等しい」という意味の記号です。)
ソース:https://qiita.com/nozma/items/d8dafe4e938c43fb7ad1
今回の要素を適用すると以下のようなイメージになります。
X・・・Nがuser_id、pがanime_idの行列。中身はユーザーがつけたアニメのrating(評価)
W・・・各ユーザーに対応した長さrのベクトル
H・・・各コンテンツに対応した長さrのベクトル
NMFを用いることで、設定したrの長さのベクトル(つまり特徴量)が取得できます。
そして、これらのベクトルの類似度を計算することで、どれだけ似ているかを算出することができます。
「カウボーイビバップをご覧になったあなたへ」を表示したいときは、カウボーイビバップと類似度が高いものを候補として選べばよいということになります。
import numpy as np
from scipy.sparse import csr_matrix
from pandas.api.types import CategoricalDtype
from sklearn.decomposition import NMF
# user_id, anime_idのユニークかつ昇順にソートされた配列を作成
user_id_unique = list(sorted(df_rating['user_id'].unique()))
anime_id_unique = list(sorted(df_rating['anime_id'].unique()))
# ratingの配列を作成
score = df_rating['rating'].tolist()
# user_id列をuser_id_uniqueをラベルとしたカテゴリ変数にキャスト
row = df_rating['user_id'].astype(
CategoricalDtype(categories=user_id_unique)).cat.codes
# anime_id列をanime_id_uniqueをラベルとしたカテゴリ変数にキャスト
col = df_rating['anime_id'].astype(
CategoricalDtype(categories=anime_id_unique)).cat.codes
# 行(user_id)と列(anime_id)の疎行列X を作成
X = csr_matrix((score, (row, col)), shape=(len(user_id_unique), len(anime_id_unique)))
# NMFのモデルを作成しデータを学習させる(n_componentsは何次元に次元削減するのかの指定)
nmf = NMF(n_components=10, init='random', max_iter=30)
W = nmf.fit_transform(X)
H = nmf.components_
・コサイン類似度
各アニメのベクトルからコサイン類似度を算出します。
2つのベクトルの類似度を求める際によく使われる手法です。
{\cos( \vec{q}, \vec{d} ) = \frac{ \vec{q} \cdot \vec{d}}{ |\vec{q}| |\vec{d}|} = \frac{ \vec{q} }{ |\vec{q}| }\cdot\frac{ \vec{d}}{ |\vec{d}| } = \frac{\sum_{i=1}^{|V|} q_i d_i}{\sqrt{\sum_{i=1}^{|V|} q_i^2} \cdot \sqrt{\sum_{i=1}^{|V|} d_i^2}}
}
参考:
http://www.cse.kyoto-su.ac.jp/~g0846020/keywords/cosinSimilarity.html
https://qiita.com/Qiitaman/items/fa393d93ce8e61a857b1
# アニメごとにループしながら特徴量を取得、他のアニメとのコサイン類似度を求めて配列に格納する
target_anime_id_list = [1]
def get_mf_candidate(target_anime_id_list, limit=20):
df_result_list = []
for anime_id_sub in target_anime_id_list:
# Hを転置。アニメごとの10個のベクトル(特徴量)を取得
anime_vec_sub = H.T[anime_id_unique.index(anime_id_sub),:]
# 類似度を格納する配列を準備
similar_temp = []
for anime_num_sim in range(len(anime_id_unique)):
# コサイン類似度の算出
cos_sim = np.dot(anime_vec_sub, H.T[anime_num_sim, :]) / (np.linalg.norm(anime_vec_sub) * np.linalg.norm(H.T[anime_num_sim, :]))
similar_temp.append(cos_sim)
df_result = pd.DataFrame({'score':similar_temp}, index=anime_id_unique).sort_values('score', ascending=False)
df_result_list.append(pd.merge(df_result, df_anime[['MAL_ID', 'Japanese name']],
left_index=True, right_on='MAL_ID').set_index('MAL_ID').drop(index=anime_id_sub).iloc[:limit,:])
return df_result_list
試しにカウボーイビバップについて各アニメとのコサイン類似度を算出し、類似度の高い方から20件をランキングすると下記のようになりました。
get_mf_candidate([1],20)
[ score Japanese name
MAL_ID
1 1.000000 カウボーイビバップ
33 0.947848 剣風伝奇ベルセルク
205 0.917112 サムライチャンプルー
1292 0.872859 アフロサムライ
227 0.866054 フリクリ
30 0.863220 新世紀エヴァンゲリオン
160 0.860621 今、そこにいる僕
467 0.855296 攻殻機動隊 STAND ALONE COMPLEX
387 0.855074 灰羽連盟
1566 0.854283 攻殻機動隊 STAND ALONE COMPLEX Solid State Society
114 0.851687 魁!! クロマティ高校
770 0.851668 ペイル・コクーン
801 0.851325 攻殻機動隊 S.A.C. 2nd GIG
3089 0.849332 スカイ・クロラ
2164 0.843086 電脳コイル
81 0.841772 機動戦士ガンダム 第08MS小隊
4672 0.840325 攻殻機動隊2.0
2001 0.837236 天元突破グレンラガン
4106 0.837072 トライガン
47 0.836964 AKIRA(アキラ)]
まぁまぁそれっぽい結果になってますね。(分かりやすいようにカウボーイビバップ自身も入れています。自分自身は類似度1.00です。)
「カウボーイビバップをご覧になったあなたへ」には、このアニメたちが左が順番に表示されます。
4.アニメEmbeddingベクトルの可視化
ここで一旦、これまで作成してきたアニメのベクトルを可視化してみます。
アニメのベクトルは先ほどのNMFで得た結果のHの方に入っています。
まずこれをKMeansを用いて10のグループにクラスタリングします。
その後、PCAで2次元に削減し、可視化していきます。
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA
# データを標準化
H_T_norm = (H.T - H.T.mean(axis=0)) / H.T.std(axis=0)
# KMeansでクラスタリング
km = KMeans(n_clusters=10)
H_km = km.fit_predict(H_T_norm)
df_analysis = pd.DataFrame(H_km, index=anime_id_unique, columns=['cluster'])
df_analysis = df_analysis.astype({'cluster':'object'})
# 主成分分析のインスタンスを生成。
pca = PCA(n_components=2)
# データを学習し、変換する。
H_pca = pca.fit_transform(H_T_norm)
df_analysis = df_analysis.assign(pca_0=H_pca[:,0], pca_1=H_pca[:,1])
df_analysis = pd.merge(df_analysis, df_anime[['MAL_ID', 'Japanese name']],
left_index=True, right_on='MAL_ID').set_index('MAL_ID')
可視化のライブラリはplotlyを使います。
# plotlyで可視化する
import plotly.express as px
fig = px.scatter(df_analysis, x='pca_0', y='pca_1',
hover_name="Japanese name", log_x=False, color='cluster')
fig.update_traces(textposition='top center')
fig.update_layout(
title_text='Anime Recommendation Database 2020'
)
fig.show()
この点のひとつひとつがアニメで、クラスタリングされ色分けされています。
カウボーイビバップは中央の下の方にいますね。
カウボーイビバップと同じ3番のクラスタにいるアニメが類似度が高いものといえます。
5.リランク
これまではアニメをベースとした特徴量から候補を生成してきました。
しかし、現状ではアニメの特徴量だけですので「カウボーイビバップをご覧になったあなたへ」にはどのユーザーにも同じ並び順で候補が並んでしまうことになります。
ここでパーソナライズという考え方が出てきます。
ユーザーの特徴量を用いて個人に最適な順に並べ替え、ランキングの精度を高めていく方法です。
「年齢」「性別」などパーソナライズにも様々なやり方がありますが、今回はユーザーそのものの情報をcsvの項目に持っていません。(ユーザーの情報はuser_idのみです。)
そこでアニメのマスタ情報に持っている「ジャンル」を用いてパーソナライズします。
「ユーザーがこれまでにどのジャンルをよく見ているか」という情報からユーザーの特徴量を生成し、先ほどの候補一覧を再ランキング(リランク)していきます。
・ジャンルリストの取得
ジャンルについて集計していきたいので、まずはユニークなジャンル一覧が欲しいです。ジャンルはマスタやコードでもっていないので、データの中から集めていきます。
anime.csvのカラム「Genres」にはジャンル名がカンマ区切りで格納されています。
(例)カウボーイビバップの場合
Action,Adventure,Comedy,Dorama,SciFi,Space
全データのGenresをすべて集めて配列に格納し、重複を排除して、ジャンルのユニークなリストを取得します。
df_anime['Genres'].str.split(', ')
genre_list = []
for list_temp in df_anime['Genres'].str.split(', '):
genre_list = genre_list + list_temp
genre_set = set(genre_list)
genre_set
{'Action', 'Adventure', 'Cars', 'Comedy', 'Dementia', 'Demons', 'Drama', 'Ecchi', 'Fantasy', 'Game', 'Harem', 'Hentai', 'Historical', 'Horror', 'Josei', 'Kids', 'Magic', 'Martial Arts', 'Mecha', 'Military', 'Music', 'Mystery', 'Parody', 'Police', 'Psychological', 'Romance', 'Samurai', 'School', 'Sci-Fi', 'Seinen', 'Shoujo', 'Shoujo Ai', 'Shounen', 'Shounen Ai', 'Slice of Life', 'Space', 'Sports', 'Super Power', 'Supernatural', 'Thriller', 'Unknown', 'Vampire', 'Yaoi', 'Yuri'}
こんなにたくさんジャンルがあるんですね・・・。
ジャンルに日本語が多く含まれていることが、アニメ文化における日本の影響力の大きさを物語っています。ジャンルの中で分かりにくそうなものを以下にピックアップして概説してみました。
ジャンル名 | 説明 | 数 | 例 |
---|---|---|---|
Dementia | 直訳は「認知症」だが、病的な意味合いではなく「見た人の脳を混乱させるような不思議な世界観」といった意味のようだ。 | 512 | 新世紀エヴァンゲリオン、フリクリ、Serial Experiments lain、ブギーポップは笑わない、パーフェクトブルー、パプリカ、妄想代理人 |
Demons | デーモン(悪魔)が指すように、何かしら人外のものが出現するアニメのようだ | 501 | 剣風伝奇ベルセルク、幻想魔伝 最遊記、犬夜叉、金色のガッシュベル!!、3×3EYES、幽☆遊☆白書、美少女戦士セーラームーン、スレイヤーズ |
Ecchi | 「エッチ」という響きからも推察されるように、わりと軽めのエッチな場面の登場するアニメに振られているジャンルのよう。 | 767 | 電影少女 、ちょびっツ、魔法先生 ネギま!、ラブひな、いちご100%、一騎当千、まほろまてぃっく |
Hentai | 18禁アニメや18禁ゲームのアニメ化など、Ecchiよりは重めの変態系アニメとお見受けします。 | 1348 | Piaキャロットへようこそ!!、同窓会、下級生、その他多数 |
Harem | 男性主人公1人を大勢の女性キャラが取り囲むという形態のアニメ | 399 | 藍より青し、魔法先生 ネギま!、ラブひな、いちご100% |
Josei | 少女まんがを原作としているアニメ(と思われる) | 97 | ハチミツとクローバー、まっすぐにいこう、のだめカンタービレ、ちはやふる |
Seinen | 青年誌のまんがを原作としているアニメ(に見える) | 830 | モンスター、剣風伝奇ベルセルク、ああっ女神さまっ、ローゼンメイデン、ギャラリーフェイク |
Shoujo | 少女まんがを原作としているアニメ?「Josei」とかぶってそうだが、「Josei」よりは若めの年齢層(小中学生?)を対象としたアニメが多いように見える。 | 760 | 赤ずきんチャチャ、花より男子、フルーツバスケット、ふしぎ遊戯、神風怪盗ジャンヌ、彼氏彼女の事情、カードキャプターさくら |
Slice of Life | 学園生活など日常を切り取った場面をベースに繰り広げられるアニメ。 | 1914 | ハチミツとクローバー、彼氏彼女の事情、ラブひな、らんま1/2、ボーイズ・ビー、ごくせん |
Shounen Ai | 読んで字の如し | 100 | 【略】 |
Shoujo Ai | 読んで字の如し | 79 | 【略】 |
Yaoi | 読んで字の如し | 42 | 【略】 |
Yuri | 読んで字の如し | 32 | 【略】 |
ジャンルを特徴量として取得したいので、one-hotエンコーディングしてジャンルごとに1項目に置き換えます
df_anime_genre = df_anime
for genre in sorted(genre_set):
df_anime_genre['genre-' + genre] = df_anime['Genres'].str.contains(genre)
・ユーザー特徴量の作成
ユーザーがどのジャンルを視聴しているのかをカウントします。
またその合計を正規化します。(全体の合計で割ることで割合が分かるようになります。)
# ユーザーごとのrating情報と、先ほどone-hotエンコーディングしたアニメのマスタ情報をマージ
df_dataset = pd.merge(df_rating.sample(n=1000000, random_state=0),
df_anime_genre, left_on='anime_id', right_on='MAL_ID')
# 必要な情報(ジャンル情報のみ)に項目をしぼってユーザーごとにグルーピングして集計する。
df_dataset_user = df_dataset.set_index('user_id').iloc[:,38:]
df_dataset_user = df_dataset_user.groupby(level=0).sum()
# user特徴量の正規化。どのユーザーがどのジャンルを多く見てるのかが割合で示される。
df_dataset_user = (df_dataset_user.T/df_dataset_user.T.sum()).T
df_dataset_user
最大値が1で、それぞれジャンルごとに割合が示されているのが分かります。
user_id=2の人はコメディ好きのようですね。
user_id=7の人はエッチなジャンルをよくご覧になっているようです。
・学習データセットの作成
算出してきたユーザーの特徴量とアニメの特徴量をマージし、ひとつのDataFrameにします。
また必要に応じて、欠損値を含むデータへの対処もしていきます。
今回のデータにはNaNを含むデータが40件程度存在しました。NaNが含まれるとうまく処理できません。幸いデータ数が少なく大きな影響はないため、あらかじめNaNを含んだ行を落としておきます。
# df_dataset_animeのindexとカラム名を設定。
df_dataset_anime = pd.DataFrame(H.T, index=anime_id_unique,
columns=[f'MF-{i}' for i in range(H.shape[0])])
# それぞれ算出してきたuserの特徴量とanimeの特徴量をマージし、ひとつのDataFrameにする。
df_dataset = df_dataset[['user_id','anime_id','rating']]
df_dataset = pd.merge(df_dataset, df_dataset_user, left_on='user_id', right_index=True)
df_dataset = pd.merge(df_dataset, df_dataset_anime, left_on='anime_id', right_index=True)
# 一部、NaNが含まれている項目があるためdropnaで行を落とす。
df_dataset = df_dataset.dropna(how='any')
データの下準備ができたら、X(説明変数)とY(目的変数)に分けていきます。
# X(説明変数)とY(目的変数)に分けて、学習の準備をする
# Xは学習には不要な'user_id','anime_id', 'rating'を落としたもの
df_X = df_dataset.drop(columns=['user_id','anime_id','rating'])
# Yは評価結果
series_Y = df_dataset['rating']
・モデルの定義と学習
学習データセットができたので、機械学習モデルを定義して実際に学習させていきます
今回は非常にポピュラーな「ランダムフォレスト」という手法を使っています。
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error
#データを学習データとテストデータに分割する
X_train, X_test, y_train, y_test = train_test_split(df_X, series_Y, shuffle=True, train_size=0.8, random_state=0)
#モデルの定義
model = RandomForestRegressor(n_estimators=20, max_depth=4, random_state=0, n_jobs=-1, verbose=1, min_samples_split=3, min_samples_leaf=3, max_features='log2')
#モデルの学習
model.fit(X_train, y_train)
#予測を行い、精度を計測する
pred_train = model.predict(X_train)
pred_test = model.predict(X_test)
print(mean_squared_error(y_train.values, pred_train, squared=False))
print(mean_squared_error(y_test.values, pred_test, squared=False))
精度は
train 1.6215739968095482
test 1.6235540960211639
と低めの結果となりました。
ランダムサーチやグリッドサーチ、optuna等を駆使してハイパーパラメータのチューニングをしてみましたが、そこまで劇的な改善が見られませんでした。。。
精度はそこまで高くないですが、過学習は発生していないので一旦これで進めてみようと思います。
・推論
モデルができたのでいよいよ推論してみます。
これまでに見たことがあるアニメをいくつかチョイスし、モデルに渡しておすすめのアニメ一覧を推論させます。アニメは配列で何個でも渡せるのですが、ひとまず結果の精度が分かりやすいように「風の谷のナウシカ」で推論してみたいと思います。
アニメのタイトルから検索し、IDを調べます。
search_word = '風の谷のナウシカ'
df_anime[(df_anime['Japanese name'].str.contains(search_word))
| (df_anime['English name'].str.contains(search_word)) ]
風の谷のナウシカのIDは572でした。
IDをモデルに渡して候補生成、リランクし、レコメンドを表示します。
MAL_ID_list = [572]
df_candidate_list = get_mf_candidate(MAL_ID_list,50)
df_candidate_concat = pd.concat(df_candidate_list, axis=0)
df_candidate_concat = df_candidate_concat.reset_index()
df_candidate_concat = df_candidate_concat.groupby('MAL_ID', group_keys=False).apply(lambda x: x.loc[x.score.idxmax()]).drop(columns='MAL_ID')
pd.set_option('display.max_rows', 500)
df_candidate_concat.sort_values('score', ascending=False)
df_predict_contents = df_candidate_concat
df_predict_contents = pd.merge(df_predict_contents, df_dataset_anime, left_on='MAL_ID', right_index=True).sort_values('score', ascending=False)
df_predict_user = df_anime_genre
df_predict_user = df_predict_user[df_predict_user['MAL_ID'].isin(MAL_ID_list)].iloc[:,36:]
arr_predict_user = df_predict_user.sum()
arr_predict_user = arr_predict_user/arr_predict_user.sum()
df_predict = df_predict_contents.copy(deep=True)
for i,v in arr_predict_user.items():
df_predict[i] = v
df_predict_result = df_predict.copy(deep=True)
df_predict_result['rerank_score'] = model.predict(df_predict.drop(columns=['Japanese name','score']))
df_predict_result[['Japanese name','score','rerank_score']].sort_values('rerank_score', ascending=False)
推論の結果、このようなランキングとなりました。
うーん。。。なんとも言えない結果ですねw
「蟲師」が9位にランクインしてますね。
確かに「王蟲」と蟲の字が被ってはいますが、アニメとしてはあまり類似点がないような・・・。
ちなみに
scoreはアニメの特徴量のみを用いた評価点。
rerank_scoreはユーザーの特徴量を用いた評価点です。
rerank_scoreを見るとほとんど同じような値になってしまっていることが分かります。
6.分析
リランクの精度を上げるため、データを可視化しながら原因を分析していきます。
・feature importance
feature importanceで特徴量の重要度を比較してみます。
df_fi = pd.DataFrame(model.feature_importance(),
index=df_X.columns, columns=['feature_importance'])
df_fi.plot.bar(figsize=(16, 9))
向かって左側がユーザー特徴量(genre-XXXXX)、右側がアニメの特徴量(MF-X)です。
アニメの特徴量は大きく、ユーザーの特徴量は小さくなっており、
かなり偏りがあることが分かります。
・ヒストグラム
ヒストグラムでユーザーデータがどの程度複雑に入っているのかを確認してみます。
df_dataset['rating'].hist()
そもそも評価点自体が偏っているのが分かります。
ほとんどが6~10点をつけており、5点以下の低い点数をつけるひとはあまりいないようです。
同様にユーザーが見たジャンルのカウントにどれくらいばらつきがあるかをヒストグラムで確認します。
df_dataset.set_index('user_id').iloc[:,38:].groupby(level=0).count().iloc[:,0].hist(bins=30)
ほとんどのユーザーは1ジャンル(1作品)しか見てないですね。
多くても2,3ジャンルです。
7.リランクその2(ユーザー特徴量次元削減による精度改善)
分析結果から以下の方針で再度リランクをしてみようと思います。
- ユーザー特徴量にゼロが多く密度が低いため、次元削減して密度を高める。
- アニメの特徴量に偏っている傾向があるので、アニメの特徴量は逆に次元を増やして密度を薄める。
ジャンルは全部で44個もあります。つまり44次元のユーザー特徴量ということです。
これを10次元に減らします。可視化をしたときと同じ方式でPCAを使います。
from sklearn.decomposition import PCA
# 主成分分析のインスタンスを生成。
pca = PCA(n_components=10)
# データを学習し、変換する。
user_pca = pca.fit_transform(df_dataset_user)
df_dataset_user = pd.DataFrame(user_pca, index=df_dataset_user.index, columns=[f'user_genre_pca_{i}' for i in range(10)])
また、「3.候補生成」でアニメの特徴量を作成する時に次元指定を10次元にしていましたが、これを15次元に変更してみます。
# NMFのモデルを作成しデータを学習させる(n_componentsは何次元に次元削減するのかの指定)
nmf = NMF(n_components=15, init='random', max_iter=30)
W = nmf.fit_transform(X)
H = nmf.components_
以降、手順としては「5.リランク」と同じことをやっていきます。
・ユーザー特徴量の作成
・学習データセットの作成
・モデルの定義と学習
・推論
feature importanceの結果は下記のようになりました。
全体的に要素がシンプルになり、ユーザー特徴量に重きが置かれるようになりました。
また、精度は
train 1.4419250735955922
test 1.4736103072904003
まで上がりました。
さて、いよいよ推論結果です。
全体的にジブリ作品が増えているのと、rerank_scoreが前よりはバラけているので推論の精度としては上がってそうですが、1位がまさかの「パンティ&ストッキングwithガーターベルト」という結果になりました。
私はこのアニメを見たことがなく、タイトルの感じから「もしや・・・」と思ったのですが、ネットで調べるとどうやらここに掲載できるアニメのようですので少しホッとしました。
自分が作ったAIの激推しなので一度見てみたいと思います。
(ナウシカと類似しているかどうかはさておき)
せっかくなので子供に過去に見たことがある好きなアニメを3つ選択してもらい
そこから推論してみました。
- 僕のヒーローアカデミア →31964
- 約束のネバーランド →37779
- 地縛少年花子くん →39534
MAL_ID_list = [31964,37779,39534]
df_candidate_list = get_mf_candidate(MAL_ID_list,50)
df_candidate_concat = pd.concat(df_candidate_list, axis=0)
df_candidate_concat = df_candidate_concat.reset_index()
df_candidate_concat = df_candidate_concat.groupby('MAL_ID', group_keys=False).apply(lambda x: x.loc[x.score.idxmax()]).drop(columns='MAL_ID')
pd.set_option('display.max_rows', 500)
df_candidate_concat.sort_values('score', ascending=False)
df_predict_contents = df_candidate_concat
df_predict_contents = pd.merge(df_predict_contents, df_dataset_anime, left_on='MAL_ID', right_index=True).sort_values('score', ascending=False)
df_predict_user = df_anime_genre
df_predict_user = df_predict_user[df_predict_user['MAL_ID'].isin(MAL_ID_list)].iloc[:,36:]
arr_predict_user = df_predict_user.sum()
arr_predict_user = arr_predict_user/arr_predict_user.sum()
df_predict = df_predict_contents.copy(deep=True)
for i,v in arr_predict_user.items():
df_predict[i] = v
df_predict_result = df_predict.copy(deep=True)
df_predict_result['rerank_score'] = model.predict(df_predict.drop(columns=['Japanese name','score']))
df_predict_result[['Japanese name','score','rerank_score']].sort_values('rerank_score', ascending=False)
「パンティ&ストッキングwithガーターベルト」がランクインしてこないことを祈ります。
説明がめんどくさいので・・・。
結果は・・・
なかなか手堅いところをチョイスしてきてる気がしますね。
やれば出来るヤツではないか。
この後しばらく子供と遊んで盛り上がりました。
8.まとめ
今回はアニメのデータセットを使ったレコメンド機能に挑戦してみました。
全くの初心者ですのでAidemyのチューターさんに教えていただきながら、なんとか形にすることができました。ひとつひとつ丁寧に指導してくださったチューターさんにこの場を借りて感謝申し上げます。おかげで楽しく機械学習の勉強をすることができ、普段何気なく目にして使っている「レコメンド」がどのようにして作られているのか仕組みを理解することができました。本当にいろいろな場所で機械学習が活用されているんですね。
時間も限られていたので決して精度の高いレコメンド機能とは言えませんでしたが、これを最初の一歩として機械学習の勉強に取り組んでいければと思います。
今後は今回作ったものを土台として、下記のようなことにもチャレンジしてみたいです。
・ モデルの精度を上げる。
・ 勾配ブースティング決定木(XGBoost、LightGBM)など他の手法でもやってみる。
・ WEB画面を作って操作できるようにしてみる。
・ アニメ以外(映画、音楽、ECの商品)のレコメンドに応用してみる。
最後まで読んでいただきありがとうございました。