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

小説を読もう累計ランキングをDoc2Vecで解析して、色々遊んでみる。
Doc2Vecを使って小説家になろうで自分好みの小説を見つけたい話」を参考にさせて頂いた。

(9/2 追記: 下記のコードを修正してGitHub Gistに載せました。=> 小説を読もうの累計ランキングをDoc2Vecで解析する その5)

小説のスクレイピング

Google Colaboratoryを使ってプログラムを作成していくため、まずは諸々必要なものをインストールする。

!apt install aptitude swig
!aptitude install mecab libmecab-dev mecab-ipadic-utf8 git make curl xz-utils file -y
!pip install mecab-python3 unidic-lite
!git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git
!echo yes | mecab-ipadic-neologd/bin/install-mecab-ipadic-neologd -n -a

前準備として、形態素解析を行い文章を単語毎に分ける必要がある。その時日本語辞書が必要になるが、今回はmecab-ipadic-NEologdを使うことにした。小説を読もうの文章には新しい表現が多いと思われるため、多数のWeb上の言語資源から得た新語を追加することでカスタマイズされているこの辞書が最適と判断した。

以下のコードで実際にスクレイピングし、文章を取得し形態素解析を行ってからGoogle Driveに保存する。(Google Driveをマウントする方法はこちらを参照)

import requests
import subprocess
import MeCab
from bs4 import BeautifulSoup
from time import sleep

# 本文をダウンロード
def novel_text_dler(url):
  headers = {
    'User-Agent':
    'Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko'
  }
  r = requests.get(url, headers=headers)
  r.encoding = r.apparent_encoding
  soup =  BeautifulSoup(r.text)
  honbun = soup.find_all('div', class_='novel_view')
  novel = ''
  for text in honbun:
    novel += text.text
  sleep(1)
  return novel

# 形態素解析
def keitaiso(text):
  cmd = 'echo `mecab-config --dicdir`"/mecab-ipadic-neologd"'
  path = (subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True).communicate()[0]).decode('utf-8')
  tagger = MeCab.Tagger('-d {0}'.format(path))
  tagger.parse('')
  node = tagger.parseToNode(text)
  word = ''
  pre_feature = ''
  while node:
    # 名詞、形容詞、動詞、形容動詞であるか判定
    HANTEI = "名詞" in node.feature
    HANTEI = "形容詞" in node.feature or HANTEI
    HANTEI = "動詞" in node.feature or HANTEI
    HANTEI = "形容動詞" in node.feature or HANTEI
    # 以下に該当する場合は除外(ストップワード)
    HANTEI = (not "代名詞" in node.feature) and HANTEI
    HANTEI = (not "助動詞" in node.feature) and HANTEI
    HANTEI = (not "非自立" in node.feature) and HANTEI
    HANTEI = (not "数" in node.feature) and HANTEI
    HANTEI = (not "人名" in node.feature) and HANTEI
    if HANTEI:
      if ("名詞接続" in pre_feature and "名詞" in node.feature) or ("接尾" in node.feature):
        word += '{0}'.format(node.surface)
      else:
        word += ' {0}'.format(node.surface)
      #print('{0} {1}'.format(node.surface, node.feature))
    pre_feature = node.feature
    node = node.next
  return word[1:]

# 累計ランキングTop50の1から5話を取得
def novel_total_50():
  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)
  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(50):
      link = rank_index[rank].find('a')
      url = link.get('href')
      # 後でわかりやすいようにURLと小説のタイトルを設定
      f.write(url + '\t' + link.get_text() + '\t')
      for chapter in range(5):
        f.write(keitaiso(novel_text_dler(url + str(chapter + 1) + '/')))
      f.write('\n')

novel_total_50()

学習済みモデルを作成する

準備が出来たら、Doc2Vecを実行し学習済みモデルを作成する。

from gensim.models.doc2vec import Doc2Vec, TaggedDocument

# 空白で単語を区切り、改行で文書を区切っているテキストデータ
with open('drive/My Drive/Colab Notebooks/syosetu/novel_datas.txt', 'r') as f:
  # 文書ごとに単語を分割してリストにする
  trainings = [TaggedDocument(words = data.split('\t')[2].split(), tags=[i]) for i, data in enumerate(f)]

# 学習の実行
m = Doc2Vec(documents=trainings, dm=1, vector_size=200, window=8, min_alpha=1e-4, min_count=5, sample=1e-3, workers=4, epochs=40)
# モデルを保存
m.save('drive/My Drive/Colab Notebooks/syosetu/doc2vec.model')

学習済みモデルを検証する

学習済みモデルが作成出来たのでこれを使って色々遊んでみる。

似ている小説を探す

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)]

最初の3つの小説のタイトルは、以下の通り。
 27:モンスターがあふれる世界になったので、好きに生きたいと思います
 11:蜘蛛ですが、なにか?
 42:即死チートが最強すぎて、異世界のやつらがまるで相手にならないんですが。

そもそも、類似度が高くても0.32なので似ている小説はないと見た方がよさそう。
もう少し特徴のある小説を選んで検索してみる。

# 28番目の小説に似ている小説は?(28番目の小説のタイトルは、「公爵令嬢の嗜み」)
print(m.docvecs.most_similar(28))

すると今度は以下のような結果が表示された。
[(35, 0.5070601105690002), (46, 0.35368871688842773), (47, 0.34039080142974854), (15, 0.32830286026000977), (7, 0.30432310700416565), (5, 0.2750416100025177), (8, 0.24726390838623047), (38, 0.23762762546539307), (12, 0.21315187215805054), (17, 0.20340490341186523)]

最初の3つの小説のタイトルは、以下の通り。
35:乙女ゲームの破滅フラグしかない悪役令嬢に転生してしまった…
46:(´・ω・`)最強勇者はお払い箱→魔王になったらずっと俺の無双ターン
47:転生した大聖女は、聖女であることをひた隠す

今度は類似度0.5の小説が出てきたが、タイトルを見る限りかなり似ている気がする。それぞれの小説のタグも「R15 異世界転生 悪役令嬢 転生」、「異世界転生 乙女ゲーム 悪役令嬢 転生 悪役 魔法 逆ハー(性別問わず) 犬とは犬猿の仲」となっているため、小説の内容も近いものと思われる。

似ている単語を探す

# 魔法に似た単語は?
print(m.wv.most_similar('魔法'))

以下のような実行結果になった。
[(‘使える’, 0.5868769884109497), (‘基礎式’, 0.5487741827964783), (‘古代’, 0.5475476384162903), (‘全属性’, 0.5122957229614258), (‘氷’, 0.5110565423965454), (‘基礎’, 0.5063040256500244), (‘失われ’, 0.5040603280067444), (‘火炎’, 0.4972696006298065), (‘初級’, 0.4933454394340515), (‘トーチ’, 0.48877549171447754)]

類似度が大体0.5程度の単語が出て来ているが、出てきた理由が良くわからない単語も含まれている。これは魔法という単語が小説を読もう内で広範囲に使われているせいだろうか。

# スライムに似た単語は?
print(m.wv.most_similar('スライム'))

今度は以下のような実行結果になった。
[(‘マザースライム’, 0.7109331488609314), (‘消化’, 0.7005775570869446), (‘栄養’, 0.7005000114440918), (‘キャタピラー’, 0.6742416620254517), (‘魔獣’, 0.6534003019332886), (‘グリーン’, 0.6487153768539429), (‘スティッキースライム’, 0.6390945911407471), (‘野生’, 0.6230173707008362), (‘食べさせ’, 0.609244167804718), (‘分裂’, 0.5920956134796143)]

こちらはかなり良好な実行結果になっていると思われる。

さらにDoc2Vecは文字の足し算引き算をすることが出来る。

# 魔法に水を足す
print(m.wv.most_similar(positive=['魔法', '水']))
# 魔法から最強を引く
print(m.wv.most_similar(positive=['魔法'], negative=['最強']))

それぞれ、実行結果は以下の通り。
[(‘氷’, 0.6695120334625244), (‘出せる’, 0.6237022280693054), (‘火’, 0.6203511953353882), (‘風’, 0.5940117239952087), (‘水魔法’, 0.5882148146629333), (‘雷’, 0.5841056108474731), (‘風魔法’, 0.5786388516426086), (‘属性’, 0.5733253955841064), (‘SS’, 0.5640972852706909), (‘唱え’, 0.563363254070282)]

[(‘氷’, 0.5235071182250977), (‘電気’, 0.4570558965206146), (‘トーチ’, 0.4391556680202484), (‘指先’, 0.4334547519683838), (‘唱え’, 0.4241408109664917), (‘おー’, 0.40032967925071716), (‘イメージ’, 0.388439416885376), (‘驚く’, 0.3821325898170471), (‘ゴブリン・ソーサラー’, 0.3783293664455414), (‘魔炎’, 0.3754980266094208)]

魔法に水を足した場合と、最強を引いた場合の最も類似度の高い単語が同じ「氷」になり面白い結果になった。

未学習の小説のベクトル化

学習していない小説も、学習済みモデルを使用してベクトル化することが出来る。これを使うと学習済み小説の中から似ている小説を探すことが出来る。

# 本来は小説の本文を形態素解析するべきだが、あえて少ない文字で検索してみる
x = m.infer_vector(keitaiso('主人公最弱 ヒロイン最強'))
print(m.docvecs.most_similar([x]))

実行結果は以下の通り。
[(29, 0.3471960127353668), (26, 0.3468911945819855), (42, 0.34553420543670654), (48, 0.33736804127693176), (6, 0.3323182463645935), (32, 0.3290942907333374), (27, 0.3264150023460388), (38, 0.3221890926361084), (19, 0.30004316568374634), (17, 0.29801973700523376)]

最初の3つの小説のタイトルは、以下の通り。
29:望まぬ不死の冒険者
26:魔王様の街づくり!~最強のダンジョンは近代都市~
42:即死チートが最強すぎて、異世界のやつらがまるで相手にならないんですが。

「望まぬ不死の冒険者」は最初のうちは主人公が弱かったはず。5話までしかスクレイピングしていないため、このような結果になったと思われる。「魔王様の街づくり!」も成り上がり系だから同様と思われる。「即死チート」はよくわからず。さすがにサンプル不足のため、検索機能として使うには厳しい実行結果となった。もう少しサンプルを増やせば変わってくるのではないだろうか。

まとめ

今回はかなり少ない小説の数で実行したにも関わらず、それなりに面白い結果を得ることが出来た。もっとサンプル数を増やせば実行結果も安定してくるのではないだろうか。

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

コメントを残す

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