分類問題を機械学習で解く

今回の目的

理論とか細かいことは後回し、取り敢えず実行してみて、機械学習がどのようなものか感じをつかむ。

分類問題とは

データを複数のクラスに分類すること。

今回は、機械学習の手法の一つである教師あり学習を使って分類する。教師あり学習とは、予め用意された問題と正解の傾向を学習することで、未知の問題に対する正解を推測する手法をいう。

必要なもの

  • データセット
  • 分類器 (データを分類する機械学習モデル)
    • k-近傍法(k-NN)
    • 決定木
    • サポートベクターマシン(SVM)
    • ロジスティック回帰など

データの傾向を学習させる必要があるため、目的に合わせたデータセットを事前に用意する。分類器はライブラリとして既に実装されているものを利用する。

機械学習を試すとき、まずはデータを準備することが一つのハードルになるが、scikit-learnにはいくつかの標準的なデータセットが付属しているので、自分で用意しなくても試すことが出来る。

今回はsklearn.datasetsのload_iris(アヤメの花のデータセット)を使用する。がくや花びらの大きさとアヤメの種類がデータに含まれていて、がくや花びらの大きさからアヤメの種類を予測する。

Pythonで実装

from sklearn.datasets import load_iris

iris = load_iris()
# データの内容を確認する
iris
{'DESCR': '(データの説明は省略)',
 'data': array([[5.1, 3.5, 1.4, 0.2],
        [4.9, 3. , 1.4, 0.2],
        [4.7, 3.2, 1.3, 0.2],
        [4.6, 3.1, 1.5, 0.2],
        [5. , 3.6, 1.4, 0.2],
        (中略)
        [5.9, 3. , 5.1, 1.8]]),
 'feature_names': ['sepal length (cm)', # がくの長さ
  'sepal width (cm)',                   # がくの幅
  'petal length (cm)',                  # 花びらの長さ
  'petal width (cm)'],                  # 花びらの幅
 'filename': '/usr/local/lib/python3.6/dist-packages/sklearn/datasets/data/iris.csv',
 # アヤメの種類 0: 'setosa', 1: 'versicolor', 2: 'virginica'
 'target': array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
        2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
        2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]),
 'target_names': array(['setosa', 'versicolor', 'virginica'], dtype='<U10')} 

この後のデータの解析を行いやすくするため、pandasのDataFrameに変換する。

import pandas as pd

# DataFrameに変換する
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['target'] = iris.target_names[iris.target]

# 先頭5行を表示する
df.head()
sepal length (cm)sepal width (cm)petal length (cm)petal width (cm)target
05.13.51.40.2setosa
14.93.01.40.2setosa
24.73.21.30.2setosa
34.63.11.50.2setosa
45.03.61.40.2setosa

ざっとデータの内容を確認する。欠損値もないしそのまま使えるように既に整えられている。

df.info()

RangeIndex: 150 entries, 0 to 149
Data columns (total 5 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   sepal length (cm)  150 non-null    float64
 1   sepal width (cm)   150 non-null    float64
 2   petal length (cm)  150 non-null    float64
 3   petal width (cm)   150 non-null    float64
 4   target             150 non-null    object 
dtypes: float64(4), object(1)
memory usage: 6.0+ KB
df.describe()
sepal length (cm)sepal width (cm)petal length (cm)petal width (cm)
count150150150150
mean5.8433333.0573333.7581.199333
std0.8280660.4358661.7652980.762238
min4.3210.1
25%5.12.81.60.3
50%5.834.351.3
75%6.43.35.11.8
max7.94.46.92.5
df['target'].value_counts()

virginica     50
setosa        50
versicolor    50
Name: target, dtype: int64

次に説明変数と目的変数に分ける。
説明変数とは目的変数を説明する変数のこと。これをもとに予測する。
目的変数とは予測したい変数のこと。

# 説明変数
X = df.drop('target', axis=1)
# 目的変数
y = df['target']

さらに、トレーニング用データセットとテスト用データセットに分ける。未知の値について予測する性能をテストするため、テスト用のデータはトレーニングで使用してはいけない。

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)

分類器にはサポートベクターマシンを使用する。scikit-learnサポートベクターマシンを実装したライブラリがあるので、それをそのまま使用する。

from sklearn.svm import SVC

svc = SVC()
# 問題と正解の傾向を学習させ、学習済みモデルを作成する
svc.fit(X_train, y_train)

学習済みモデルが作成出来たら、テストデータを使ってアヤメの種類を予測してみる。

svc.predict(X_test)

array(['virginica', 'versicolor', 'setosa', 'virginica', 'setosa',
       'virginica', 'setosa', 'versicolor', 'versicolor', 'versicolor',
       'virginica', 'versicolor', 'versicolor', 'versicolor',
       'versicolor', 'setosa', 'versicolor', 'versicolor', 'setosa',
       'setosa', 'virginica', 'versicolor', 'setosa', 'setosa',
       'virginica', 'setosa', 'setosa', 'versicolor', 'versicolor',
       'setosa', 'virginica', 'versicolor', 'setosa', 'virginica',
       'virginica', 'versicolor', 'setosa', 'virginica'], dtype=object)

どのくらい正解しているのかは下記で確認出来る。

svc.score(X_test, y_test)

0.9736842105263158

97%正しいアヤメの種類を予測出来ている。

まとめ

上記はかなり単純な例です。このように高い率で予測出来ているのは、使用しやすく既にデータが整備されていたためです。本来データには欠損値であったり、ノイズであったり、そもそも予測するための説明変数が不足していたりしていますので、まず学習で使用するデータの作成に時間がかかります。
また、分類器も今回はサポートベクターマシンを使用しましたが、最適な分類器ではないかもしれません。どの分類器が最適か探すこともあるでしょうし、そのパラメーターのチューニングも必要になるかもしれません。
これらを詰めていき正解率を上げていく作業はなかなか楽しいです。興味があればさらに詳しく調べてみてください。

Recommendation Systemを考える

以下の機械学習の勉強の過程で小説の特徴を抽出することは出来た。ここからユーザーにおすすめの小説を推薦するにはどのような仕組みが必要なのか考える。

小説を読もうの累計ランキングをDoc2Vecで解析する その5

Recommendation System(推薦システム)とは

過去の行動をもとに、あるアイテムを好む可能性を予測するシステム。
例えば、Netflixでは、映画を見たり、投票したりするような過去の行動に応じて、新しい映画を提案している。
新しいアイテムを推奨するという点が重要となる。

・ユーザーベースの推薦システム

自分の経験と他の人の経験を組み合わせて推薦する。ユーザ間の類似度を計算し、類似度の高いユーザーが好む作品の中でまだ見ていない作品を推薦する。

このシステムを実現するには複数のユーザー情報が必要になるため、今回は不採用。

・アイテムベースの推薦システム

類似のユーザを見つけるのではなく、映画や物などのアイテムを比較する。アイテム間の類似度を計算する。

・どのような推薦システムを作成するか

ユーザーが入力した小説に似ている小説を推薦するシステムは以前作成したDoc2Vecのモデルを使用すれば実現可能である。しかし、このシステムではユーザーが毎回小説を入力しなければいけない点が煩わしい。

例えば、最近投稿された小説の中から自分の好みに合う小説があったら、自動的に通知が来るようなシステムであると良い。

このシステムは以前作成したモデルのみでは実現不可能であり、さらに自分の好みを学習させる必要がある。これまでと違い教師あり学習になるので、どのような手法があるか調べるところから始めることにする。

Recommendation Systemを考える その2へ続く。

GreenMail Ver. 1.6 Migration

GreenMailを1.6にバージョンアップするとAssertionErrorが発生する場合は、 以下の依存関係が悪さをしているので、

    <dependency>
      <groupId>com.sun.mail</groupId>
      <artifactId>javax.mail</artifactId>
    </dependency>

以下の様に変更すれば良い。

    <dependency>
      <groupId>com.sun.mail</groupId>
      <artifactId>jakarta.mail</artifactId>
    </dependency>

Ver. 1.6から依存関係にあるjavamailが変更された。

小説を読もうの累計ランキングをDoc2Vecで解析する その5

小説を読もうの累計ランキングをDoc2Vecで解析する その4の続き。

形態素解析をする前に正規化処理を追加した。それ以外には類似文章の検索結果が良くなるように、文章数と1文章当たりの文字数、次元数などを調整した。
その代わり類似単語の検索については悪化しているように感じる。

追記(2020/09/16):
指定したURLの小説の類似小説を表示する関数を追加した。

修正版 (2020/09/16):

TensorflowからNVIDIAのGPUを使用する(Windows)

使用しているGPUがCUDAに対応していることを確認する

https://developer.nvidia.com/cuda-gpus#compute
上記サイトの一覧に自分のGPUがあることを確認する。

Build Tools for Visual Studio 2019をインストールする

https://visualstudio.microsoft.com/ja/downloads/
上記のページにアクセスし、Visual Studio 2019 のツールからBuild Tools for Visual Studio 2019をダウンロードする。
以下のC++ Build Toolsにチェックを入れてインストールする。それ以外はデフォルトで良い。

CUDA Toolkitをインストールする

https://developer.nvidia.com/cuda-toolkit-archive
上記のページにアクセスし、CUDA Toolkitをダウンロードする。
必ずTensorflowが動作保証しているバージョン(https://www.tensorflow.org/install/gpu)をダウンロードすること。最新バージョンではDLLが見つからないなどエラーが発生し、動作しない可能性が高い。
今回はTensorflow 2.3.0を使用するため、CUDA Toolkit 10.1 update2 (Aug 2019)をダウンロードする。

インストーラーを起動して以下のオプションを指定する。それ以外はデフォルトで良い。

cuDNNをインストールする

https://developer.nvidia.com/cudnn
上記のページにアクセスし、cuDNNをダウンロードする。(ダウンロードは無料だが、ユーザー登録が必要)
今回はTensorflow 2.3.0、CUDA Toolkit 10.1を使用するため、cuDNN v7.6.5 (November 5th, 2019), for CUDA 10.1をダウンロードする。

ダウンロードしたファイルを解凍し、CUDAをインストールしたフォルダに上書きする。

動作確認

Tensorflowを実行し以下の通りGPUが読み込まれていることを確認できた。

2020-08-18 19:54:11.658397: I tensorflow/stream_executor/platform/default/dso_loader.cc:48] Successfully opened dynamic library nvcuda.dll
2020-08-18 19:54:11.895033: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1716] Found device 0 with properties: 
pciBusID: 0000:01:00.0 name: GeForce GTX 1070 computeCapability: 6.1
coreClock: 1.7715GHz coreCount: 15 deviceMemorySize: 8.00GiB deviceMemoryBandwidth: 238.66GiB/s
2020-08-18 19:54:11.895468: I tensorflow/stream_executor/platform/default/dso_loader.cc:48] Successfully opened dynamic library cudart64_101.dll
2020-08-18 19:54:11.924128: I tensorflow/stream_executor/platform/default/dso_loader.cc:48] Successfully opened dynamic library cublas64_10.dll
2020-08-18 19:54:11.955523: I tensorflow/stream_executor/platform/default/dso_loader.cc:48] Successfully opened dynamic library cufft64_10.dll
2020-08-18 19:54:11.962251: I tensorflow/stream_executor/platform/default/dso_loader.cc:48] Successfully opened dynamic library curand64_10.dll
2020-08-18 19:54:11.989806: I tensorflow/stream_executor/platform/default/dso_loader.cc:48] Successfully opened dynamic library cusolver64_10.dll
2020-08-18 19:54:12.003706: I tensorflow/stream_executor/platform/default/dso_loader.cc:48] Successfully opened dynamic library cusparse64_10.dll
2020-08-18 19:54:15.670393: I tensorflow/stream_executor/platform/default/dso_loader.cc:48] Successfully opened dynamic library cudnn64_7.dll
2020-08-18 19:54:15.670725: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1858] Adding visible gpu devices: 0

Pythonの実行環境構築(Windows)

Windows上でPythonの実行環境を構築する。

  1. PleiadesからPythonのFull Editionパッケージをダウンロードし任意のフォルダに解凍する。
  2. pipの公式サイトからget-pip.pyをダウンロードし任意のフォルダに保存する。
  3. Pleiadesに同封されているPythonでget-pip.pyを実行する。 <PLEIADES_INSTALL_DIR>\python\3\python get-pip.py
  4. Tensorflowなどインストールしたい場合には、eclipseの「ウィンドウ(W) > 設定(P) > PyDev > インタープリーター > Pythonインタープリーター > Python3」を開き「Manage with pip」ボタンをクリックし、以下の画面の様に入力実行する。

TensorflowでNVidiaのGPUを使用する場合は別途CUDA Toolkitなどのインストールが必要になるはずで、それについては後で試してみる。

小説を読もうの累計ランキングをDoc2Vecで解析する その4

小説を読もうの累計ランキングをDoc2Vecで解析する その3の続き。

今回はこれまで解析した特徴を表にプロットして可視化してみる。2次元の表にプロットするために、t-SNEを使用し次元を削減する。

t-SNEによる次元圧縮

import numpy as np
import matplotlib.pyplot as plt
from gensim.models.doc2vec import Doc2Vec
from sklearn.manifold import TSNE

# 学習済みモデルを読み込む
m = Doc2Vec.load('drive/My Drive/Colab Notebooks/syosetu/doc2vec.model')
weights = []
for i in range(0, len(m.docvecs)):
  weights.append(m.docvecs[i].tolist())
weights_tuple = tuple(weights)
X = np.vstack(weights_tuple)

# t-SNEで次元圧縮する
tsne_model = TSNE(n_components=2, random_state=0, verbose=2)
np.set_printoptions(suppress=True)
t_sne = tsne_model.fit_transform(X)

# クラスタリング済みのデータを読み込む
with open('drive/My Drive/Colab Notebooks/syosetu/novel_cluster.csv', 'r') as f:
  reader = csv.reader(f)
  clustered = np.array([row for row in reader])
  clustered = clustered.astype(np.dtype(int).type)
  clustered = clustered[np.argsort(clustered[:, 0])]
  clustered = clustered.T[1]

# グラフ描画
fig, ax = plt.subplots(figsize=(10, 10), facecolor='w', edgecolor='k')

# Set Color map
cmap = plt.get_cmap('Dark2')

for i in range(t_sne.shape[0]):
  cval = cmap(clustered[i] / 4)
  ax.scatter(t_sne[i][0], t_sne[i][1], marker='.', color=cval)
  ax.annotate(i, xy=(t_sne[i][0], t_sne[i][1]), color=cval)
plt.show()

実行すると以下のようなグラフになった。満遍なく分布しているが同じクラスターの小説についてはある程度まとまって分布しているように見える。

続いて3次元のグラフを描画してくれる良いページがあるので使ってみる。

Embedding projector – visualization of high-dimensional data

上記のサイトで次元圧縮を行ってくれるので、こちらはTSV形式のデータを用意するだけで良い。

!pip install gensim torch tensorboardX

from torch import FloatTensor
from gensim.models import KeyedVectors
from tensorboardX import SummaryWriter

m = KeyedVectors.load('drive/My Drive/Colab Notebooks/syosetu/doc2vec.model')
weights = []
labels = []
for i in range(0, len(m.docvecs)):
  weights.append(m.docvecs[i].tolist())
  labels.append(m.docvecs.index_to_doctag(i))

# DEBUG: visualize vectors up to 1000
weights = weights[:1000]
labels = labels[:1000]

writer = SummaryWriter()
writer.add_embedding(FloatTensor(weights), metadata=labels)

実行すると「/content/runs/実行日付等/00000/default」にtensors.tsvとmetadata.tsvファイルが保存される。このファイルをEmbedding Projectorのページにアップロードする。
このままではどの点がどの小説を表しているかわかりずらいので、metadata.tsvを加工してタイトルが表示されるようにする。

とても見やすいけれども、第1から第3主成分までの累積寄与率が14%ほどしかないので、PCAにて次元圧縮したこのグラフは残念ながら殆どあてにならない。PCA以外にも先ほど使用したt-SNEを使用することも可能。

まとめ

もう少しきれいなグラフが描けるように、ライブラリの使い方から勉強しなおす。

小説を読もうの累計ランキングをDoc2Vecで解析する その5へ続く。

小説を読もうの累計ランキングをDoc2Vecで解析する その3

小説を読もうの累計ランキングをDoc2Vecで解析する その2の続き。

前回累積ランキングのクラスターを目視で確認をしたが、それぞれの特徴を的確に捉えることは出来なかった。今回はクラスターの特徴をTF-IDFを使って抽出してみる。

TF-IDFで分析する

今回もscikit-learnのライブラリを使用する。

import csv
from sklearn.feature_extraction.text import TfidfVectorizer

# 小説の本文を読み込む
with open('drive/My Drive/Colab Notebooks/syosetu/novel_datas.txt', 'r') as f:
  novels = [[i, data.split('\t')[2]] for i, data in enumerate(f)]

docs = ['','','','','','']

# クラスタリング結果を読み込む
with open('drive/My Drive/Colab Notebooks/syosetu/novel_cluster.csv', 'r') as f:
  reader = csv.reader(f)
  for row in reader:
    doc = novels[int(row[0])][1]
    #doc = ' '.join(set(doc.split())) #同一タイトル内で重複削除
    # クラスター毎に本文を纏める
    docs[int(row[1]) - 1] += ' {0}'.format(doc)

# tf-idfの計算
vectorizer = TfidfVectorizer(max_df=0.90, max_features=100)
# 文書全体の90%以上で出現する単語は無視する
# 且つ、出現上位100までの単語で計算する
X = vectorizer.fit_transform(docs)
#print('feature_names:', vectorizer.get_feature_names())

words = vectorizer.get_feature_names()
for doc_id, vec in zip(range(len(docs)), X.toarray()):
  print('doc_id:', doc_id + 1)
  for w_id, tfidf in sorted(enumerate(vec), key=lambda x: x[1], reverse=True)[:20]:
    lemma = words[w_id]
    print('\t{0:s}: {1:f}'.format(lemma, tfidf))

実行結果は以下の通りとなった。

doc_id: 1	
	蔵人: 0.364076
	魔物: 0.334649
	ベルグリフ: 0.296258
	魔王: 0.271560
	アンジェリン: 0.264133
	直継: 0.237363
	ガリウス: 0.205239
	魔術: 0.199051
	lv: 0.197992
	夜霧: 0.196315
	マイン: 0.194641
	パーティ: 0.190183
	身体: 0.161838
	女神: 0.156352
	ハンター: 0.127054
	魔族: 0.118583
	メンバー: 0.107892
	探索者: 0.106833
	公爵: 0.104819
	バス: 0.093903
doc_id: 2	
	魔物: 0.409209
	ダリヤ: 0.387583
	dp: 0.376927
	ポーション: 0.287506
	lv: 0.255560
	lv: 0.232489
	クマ: 0.227815
	ゴブリン: 0.226827
	聖女: 0.159725
	薬草: 0.142533
	魔石: 0.131330
	兄さん: 0.124231
	契約: 0.119544
	討伐: 0.111881
	加護: 0.104218
	hp: 0.102685
	ボックス: 0.094060
	金貨: 0.082761
	騎士団: 0.082761
	パン: 0.079863
doc_id: 3	
	リアム: 0.810088
	ドロップ: 0.416766
	導書: 0.335092
	領地: 0.152304
	hp: 0.102806
	殿下: 0.101409
	スケルトン: 0.076152
	mp: 0.049499
	皇帝: 0.044091
	キャラ: 0.041884
	王子: 0.030461
	兄さん: 0.017636
	冒険: 0.011423
	母さん: 0.011423
	クマ: 0.010291
	報酬: 0.007615
	経験値: 0.007615
	付与: 0.003808
	契約: 0.003808
	採取: 0.003808
doc_id: 4	
	名無し: 0.424656
	スバル: 0.420209
	コタロー: 0.326830
	真昼: 0.262535
	鑑定: 0.215285
	耐性: 0.200477
	ニート: 0.193944
	ボーナス: 0.187787
	魔物: 0.174278
	ポーション: 0.166195
	lv: 0.163557
	ガチャ: 0.158615
	ゴブリン: 0.158331
	lv: 0.155643
	プレイヤー: 0.132132
	盗賊: 0.129854
	身体: 0.105934
	ユニーク: 0.091012
	初期: 0.088848
	キャラ: 0.084291
doc_id: 5	
	リーシェ: 0.737429
	王子: 0.384356
	アルノルト: 0.342548
	殿下: 0.235180
	学院: 0.227594
	公爵: 0.123912
	皇帝: 0.118855
	バス: 0.103287
	先生: 0.096089
	剣士: 0.085170
	訓練: 0.076434
	母さん: 0.056780
	学校: 0.054596
	聖女: 0.053105
	身体: 0.048044
	それなり: 0.043677
	キャラ: 0.034941
	所属: 0.034941
	騎士団: 0.030574
	メンバー: 0.028390
doc_id: 6	
	魔物: 0.419558
	lv: 0.318543
	素材: 0.269150
	幼女: 0.265192
	魔術: 0.261251
	レイ: 0.259408
	身体: 0.239465
	マイン: 0.205894
	ゴブリン: 0.205821
	魔石: 0.199376
	爺さん: 0.186030
	転移: 0.178751
	万能: 0.166240
	それなり: 0.112806
	銀貨: 0.112292
	採取: 0.104890
	おじさん: 0.103126
	金貨: 0.096973
	美女: 0.094994
	訓練: 0.094994

形態素解析をしたときに残ってしまった人名がいくつか含まれてしまっているが今回は無視する。
また、予想はしていたがサンプル数が少ないことと、似ている内容が多いため特徴がわかり難くなってしまった。それでもある程度の特徴はつかむことが出来た。

doc_id: 1
  戦闘、戦争中心の話?
doc_id: 2
  ダンジョン中心の話?
doc_id: 3
  領地経営、内政中心の話?
doc_id: 4
  ファンタジーだけど現実要素強めの話?
doc_id: 5
  乙女ゲーム、内政中心の話?
doc_id: 6
  戦闘、戦争は少なそう。まったり系、生産系の話?

まとめ

教師無しであるにもかかわらず、ちゃんと分類出来るのは面白い。最終的にやりたいことは自分が読みたいと思う小説を機械学習で探すことなので、今回の知見を後に生かせれば良いと思う。

小説を読もうの累計ランキングをDoc2Vecで解析する その4へ続く。

小説を読もうの累計ランキングをDoc2Vecで解析する その2

小説を読もうの累計ランキングをDoc2Vecで解析するの続き。

前回学習済みモデルによって小説間の類似度を見ることが出来た。今度はクラスタリングを行う。

前回のモデルを改善する

次に進む前に少しスクレイピングの処理を修正する。1から5話まで取得する様にしていたが、作品によって1話当たりの文字数にかなり差があるので、単語数を基準に本文を取得するように変更する。またサンプルをTop 100(これでもかなり少ない)まで増やす。

# 累計ランキングをTop100まで取得
def novel_total():
  headers = {
    'User-Agent':
    'Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko'
  }
  r = requests.get('http://yomou.syosetu.com/rank/list/type/total_total/', headers=headers, timeout=20)
  r.encoding = r.apparent_encoding
  soup =  BeautifulSoup(r.text)
  rank_index = soup.find_all('div', class_='rank_h')
  sleep(1)
  with open('drive/My Drive/Colab Notebooks/syosetu/novel_datas.txt', 'w') as f:
    for rank in range(100):
      link = rank_index[rank].find('a')
      url = link.get('href')
      title = link.get_text()
      # 後でわかりやすいようにURLと小説のタイトルを設定
      f.write('{0}\t{1}\t'.format(url, title))
      print('rank:{0} title:{1}'.format(rank + 1, title))
      chapter = 0
      word_count = 0
      while word_count < 5000 and chapter < 40:
        # 単語が5000未満かつ、40話以下の間繰り返し取得する
        try:
          words = keitaiso(novel_text_dler('{0}{1}/'.format(url, chapter + 1)))
        except (HTTPError, URLError) as e:
          print(e)
          break
        except socket.timeout as e:
          print(e)
          continue
        else:
          f.write(words)
          print('chapter:{0}'.format(chapter + 1))
          word_count += len(words.split())
        chapter += 1
      f.write('\n')

モデルを作成するパラメーターも少し変更する。

# 学習の実行
m = Doc2Vec(documents=trainings, dm=1, vector_size=400, min_count=4, workers=4, epochs=40)

実行結果を確認する。

m = Doc2Vec.load('drive/My Drive/Colab Notebooks/syosetu/doc2vec.model')
# 0番目の小説に似ている小説は?(0番目の小説のタイトルは、「転生したらスライムだった件」)
print(m.docvecs.most_similar(0))

前回:
[(27, 0.3229605555534363), (11, 0.27810564637184143), (42, 0.24812708795070648), (13, 0.2365787774324417), (29, 0.22865955531597137), (33, 0.21636559069156647), (22, 0.19751328229904175), (20, 0.18333640694618225), (36, 0.1763637661933899), (25, 0.1638755202293396)]
今回:
[(27, 0.2789984941482544), (13, 0.26930510997772217), (60, 0.2584960460662842), (48, 0.25848516821861267), (68, 0.24882027506828308), (33, 0.24246476590633392), (49, 0.23103246092796326), (52, 0.22929105162620544), (94, 0.22420653700828552), (12, 0.22121790051460266)]

一番目が「27:モンスターがあふれる世界になったので、好きに生きたいと思います」であるのは変更なしであるが、二番目には「13:異世界のんびり農家」が出て来た。チートぐらいしか共通点はなさそう。試しにwindowをデフォルトの8から16に増やしてみる。

するとこのようになった。
[(60, 0.3200589418411255), (52, 0.3115108609199524), (27, 0.31150585412979126), (49, 0.2996276319026947), (13, 0.299365371465683), (18, 0.2926834225654602), (97, 0.2818679213523865), (12, 0.2745039761066437), (29, 0.2639138996601105), (68, 0.2628819942474365)]

一番目は「60:聖者無双 ~サラリーマン、異世界で生き残るために歩む道~」、二番目は「52:進化の実~知らないうちに勝ち組人生~」となった。今度は「残酷な描写あり」、「異世界」、「ハーレム」、「チート」と共通点は多そうなのでこちらを採用する。

クラスター分析する

今回も「Doc2Vecを使って小説家になろうで自分好みの小説を見つけたい話」を参考にさせて頂いた。

これらは便利な関数が用意されているので、それらに値を渡すだけでクラスタリングから図の作成まで行える。

import numpy as np
import matplotlib.pyplot as plt
from gensim.models.doc2vec import Doc2Vec
from scipy.cluster.hierarchy import linkage, fcluster, dendrogram
from matplotlib.font_manager import FontProperties

# 階層型クラスタリングの実施
def hierarchical_clustering(emb, threshold):
  # ウォード法 x ユークリッド距離
  linkage_result = linkage(emb, method='ward', metric='euclidean')
  # クラスタ分けするしきい値を決める
  threshold_distance = threshold * np.max(linkage_result[:, 2])
  # クラスタリング結果の値を取得
  clustered = fcluster(linkage_result, threshold_distance, criterion='distance')
  print("end clustering.")
  return linkage_result, threshold_distance, clustered

# 階層型クラスタリングの可視化
def plot_dendrogram(linkage_result, doc_labels, threshold):
  fp = FontProperties(fname=r'drive/My Drive/Colab Notebooks/IPAexfont00301/ipaexg.ttf')
  plt.figure(figsize=(16, 8), facecolor='w', edgecolor='k')
  dendrogram(linkage_result, labels=doc_labels, color_threshold=threshold)
  plt.title('Dendrogram', fontproperties=fp)
  plt.xticks(fontsize=10)
  print('end plot.')
  plt.savefig('drive/My Drive/Colab Notebooks/syosetu/novel_hierarchy.png')

# 階層型クラスタリング結果の保存
def save_cluster(doc_index, clustered):
  doc_cluster = np.array([doc_index, clustered])
  doc_cluster = doc_cluster.T
  doc_cluster = doc_cluster.astype(np.dtype(int).type)
  doc_cluster = doc_cluster[np.argsort(doc_cluster[:, 1])]
  np.savetxt('drive/My Drive/Colab Notebooks/syosetu/novel_cluster.csv', doc_cluster, delimiter=',', fmt='%.0f')
  print('save cluster.')

m = Doc2Vec.load('drive/My Drive/Colab Notebooks/syosetu/doc2vec.model')
vectors_list = [m.docvecs[n] for n in range(len(m.docvecs))]

threshold = 0.8
linkage_result, threshold, clustered = hierarchical_clustering(emb=vectors_list, threshold=threshold)
plot_dendrogram(linkage_result=linkage_result, doc_labels=list(range(100)), threshold=threshold)
save_cluster(list(range(100)), clustered)

実行すると以下のような画像が表示される。

100タイトルの小説が6つに分類された。

クラスター分析結果を検証する

グラフを見る限りそれぞれの小説の類似度は高いとは言えず満遍なく分布している。念のため小説の特徴にあった分類がされているか目視で確認する。(機械的に確認する処理の作成は次回)3、5のクラスターは比較的特徴がわかりやすい。

633レベル1だけどユニークスキルで最強です
513乙女ゲー世界はモブに厳しい世界です
533俺は星間国家の悪徳領主!
393没落予定の貴族だけど、暇だったから魔法を極めてみた
343貴族転生~恵まれた生まれから最強の力を得る
825転生王女は今日も旗を叩き折る。
885甘く優しい世界で生きるには
795今度は絶対に邪魔しませんっ!
155謙虚、堅実をモットーに生きております!
105一億年ボタンを連打した俺は、気付いたら最強になっていた~落第剣士の学院無双~
355乙女ゲームの破滅フラグしかない悪役令嬢に転生してしまった…
65陰の実力者になりたくて!【web版】
15ありふれた職業で世界最強
595ループ7回目の悪役令嬢は、元敵国で自由気ままな花嫁(人質)生活を満喫する
285公爵令嬢の嗜み
855黒の魔王

クラスター3の特徴は「異世界 貴族 中世 SF 最強 男主人公」だろうか。クラスター5は「異世界 乙女ゲーム 女主人公 成り上がり」だろうか。

まとめ

それなりに意味のあるまとまりになっている気がするがなかなか厳しい。次回はその他のクラスターについても特徴を検証してみる。

小説を読もうの累計ランキングをDoc2Vecで解析する その3に続く。