Recommendation Systemを考える その2

Recommendation Systemを考えるの続き。

更新履歴:2021/03/19 データ処理に誤りがあったため訂正

初めに

小説をユーザーに推薦するかしないかで分けたいので、これは分類問題となる。分類問題についてはこちらで解説している。

分類するために、作者、関連作品やタグ付けも参照した方が精度が良くなるかもしれないが、今回はDoc2Vecのベクトル情報のみで行ってみる。Doc2Vec で作成した文書ベクトルを教師あり学習の説明変数として使用する。

文書ベクトルを教師あり学習の説明変数とした分類問題ではRandom Forestが有効という論文を見たので、まずは Random Forest を試してみる。また、今回使用するDoc2Vecの文書ベクトルは次元数が100ほどあるため、事前に次元圧縮を行うことも検討する。

最初は、このモデルを正しく評価するために、ユーザーの好みかの様なあいまいな指標で分類するのではなく、機械的に分類できる指標にする。

分類の実施

データセットの作成

以前作成した学習済みモデルを読み込み、データセットを作成する。

from gensim.models.doc2vec import Doc2Vec

MODEL_DATA_PATH = 'drive/My Drive/Colab Notebooks/syosetu/doc2vec_100.model'

m = Doc2Vec.load(MODEL_DATA_PATH)
vectors_list = [m.docvecs[n] for n in range(len(m.docvecs))]
import pandas as pd

MODEL_TITLES_CSV_PATH = 'drive/My Drive/Colab Notebooks/syosetu/novel_titles.csv'

df = pd.read_csv(MODEL_TITLES_CSV_PATH, header=None, names=['url', 'title', 'tag'])
df = df.drop(columns=['url'])
df['vectors'] = vectors_list

df.head(2)
titletagvectors
0転生したらスライムだった件R15 残酷な描写あり 異世界転生 スライム チート[5.356541, 1.455931, 1.0426528, 1.1359314, 3.9…
1無職転生 - 異世界行ったら本気だす –R15 残酷な描写あり 異世界転生[0.010144993, -12.32704, 2.8596005, 6.5284176,…

タグを見て機械的に分類したいため、どんなタグが存在するのか最初に確認する。

import collections
import itertools

tag = collections.Counter(list(itertools.chain.from_iterable(df['tag'].str.split().tolist())))
sorted(tag.items(), key=lambda pair: pair[1], reverse=True)[:20]
[('R15', 233),
 ('残酷な描写あり', 215),
 ('異世界転生', 98),
 ('異世界', 87),
 ('異世界転移', 86),
 ('チート', 84),
 ('ファンタジー', 80),
 ('魔法', 73),
 ('男主人公', 65),
 ('冒険', 63),
 ('主人公最強', 59),
 ('書籍化', 53),
 ('ハーレム', 48),
 ('成り上がり', 43),
 ('ほのぼの', 33),
 ('ハッピーエンド', 32),
 ('女主人公', 31),
 ('恋愛', 30),
 ('オリジナル戦記', 30),
 ('転生', 28)]

2極化しやすそうな、男主人公、女主人公の小説を抽出することにする。

df_m = df[df['tag'].str.contains('男主人公')]
df_m['female'] = 0
df_f = df[df['tag'].str.contains('女主人公|女性視点|悪役令嬢|後宮|少女マンガ')]
df_f['female'] = 1

上記の抽出方法の場合、女主人公と分類した小説の中に男主人公の小説が混じっている可能性がある。

df_f[df_f['tag'].str.contains('男主人公')]
titletagvectors
47乙女ゲー世界はモブに厳しい世界ですR15 残酷な描写あり 異世界転生 乙女ゲーム 悪役令嬢 冒険 人工知能 男主人公 学園 …[6.775977, 4.3698134, -1.9109731, 9.731645, -0…
74【Web版】町人Aは悪役令嬢をどうしても救いたい【本編完結済み】R15 残酷な描写あり 異世界転生 乙女ゲーム 悪役令嬢 オリジナル戦記 男主人公 西洋 …[2.9941628, 1.892081, 1.9338807, 4.0745897, 6….

上記は男主人公の小説と思われるため、女主人公の小説の一覧から除外する。

df_f = df_f.drop([47, 74])
df = pd.concat([df_m, df_f])
df['female'].value_counts()

0 67
1 51
Name: female, dtype: int64

男主人公の小説の方が数が多いため、女主人公の小説の数と合わせる。

#男主人公の一覧
df_0 = df[df['female'] == 0]
#女主人公の一覧
df_1 = df[df['female'] == 1]

df_0 = df_0.sample(n=len(df_1), random_state=0)

df = pd.concat([df_0, df_1])
機械学習の実施

データセットの作成が出来たので、 Random Forest を用いて、分類を実施してみる。

X = df['vectors']
y = df['female']

X_array = np.array([np.array(v) for v in X])
y_array = np.array([i for i in y])
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X_array, y_array, random_state=0)
from sklearn.ensemble import RandomForestClassifier

random_forest = RandomForestClassifier(random_state=0)
random_forest.fit(X_train, y_train)

random_forest.score(X_test, y_test)

0.8076923076923077

random_forest.predict(X_test)

array([0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0,
0, 1, 0, 0])

試しに、ロジスティック回帰でも実施してみる。

from sklearn.linear_model import LogisticRegression

logreg = LogisticRegression()
logreg.fit(X_train, y_train)

logreg.score(X_test, y_test)

0.9230769230769231

logreg.predict(X_test)

array([0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0])

ロジスティック回帰の方が良いスコアになった。しかし、過学習を起こしている可能性もある。

まとめ

Random Forest が一番良い結果になると予想していたが、違った。
データセットは大体問題なさそうなので、この問題を解くにはどの分類器が適しているのか、また最適なパラメーターを探す作業は次回行いたいと思う。

複数の機械学習アルゴリズムを比較するへ続く。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください