更新履歴: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)
title | tag | vectors | |
---|---|---|---|
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('男主人公')]
title | tag | vectors | |
---|---|---|---|
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 が一番良い結果になると予想していたが、違った。
データセットは大体問題なさそうなので、この問題を解くにはどの分類器が適しているのか、また最適なパラメーターを探す作業は次回行いたいと思う。