Word2vecで遊んでみた

今年の大きめの仕事が一段落ついた(本当は来月までやる)こともあり、時間があったのでちょっと触ってみましたというエントリーです。

年末ぐらいからWord2vecを使ってみたいなあとは思っていたのですが、なかなか手をつけられませんでしたがようやく手をつけることができました。

Word2vecとは…

en.wikipedia.org

ということです。よくわかっていない…

なんとなく大量のテキストデータを解析し、各単語の意味をベクトル表現化し、コーパス内の単語同士の意味の近さを求めたり、 単語同士の意味を演算ができるようにするものなのかなと思います。ここでいう意味というのは一般的な言葉で言う意味とは少し違う 概念(概念的な近さ?)だと思ったほうがいいのかもしれません。ちなみにコーパスはテキストや発話を大規模に集めてデータベース化した言語資料されたものをいうようです

とはいっても、まずは触ってみたほうがいいので

f:id:ueponx:20190210115552p:plain

って感じで使ってみることにします。以下にとてもよいエントリーがあったので参考にしました。

【参考】 qiita.com

参考にしたエントリーは青空文庫に入っているデータを取得しコーパスとして使用していますが、そんな高尚なことにあまり興味がないので、今回は平成仮面ライダー20周年というメモリアルイヤーでもあるので、Wikipediaにある平成仮面ライダーの情報をコーパスとして取得していろいろ遊んで見ようと思います。

Word2vecの使い方は以下のような手順になると思います。

  1. コーパスとなるデータの取得(大規模なテキストデータ)
  2. 取得されたテキストデータの形態素解析を行い単語単位で取得
  3. Word2vecの処理を行い、単語のベクトル化
  4. ベクトル化されたデータを元に行いたい演算を行う

これが簡単な手順になるかと思います。 単語のベクトル化はかなりの計算量があるのでRaspberryPiなどではかなり時間がかかる処理になります。可能であればPCやクラウドのマシンパワーで なんとかしたほうがいいかなと思います。参考にしたエントリーではIBM Cloudを使用しているのでかなり理にかなった感じの処理になっているかなと思います。 ただJupyter Notebook経由でのテキストデータの処理に関しては少し癖があるように感じたので、自分は手元にあるPCを使用しました。 (ベクトル化されたデータは保存して再利用可能なので別の処理系で演算することは可能です。)

では進めてみます。

今回は

  • テキストの取得処理に関しては…requestsBeautifulSoup
  • 形態素解析には…janome
  • ベクトル化には…word2vec

を使用しています。インストールされていない場合にはpipでモジュールのインストールを行いましょう。

$ pip install requests
$ pip install beautifulsoup4
$ pip install janome
$ pip install word2vec

Wikipediaからデータを取得する

ホントはこういうやり方はあまりオススメしないのですが(Wikipediaに不要なアクセスが増えるので本来であればDBをローカルに持ってくるのが望ましい) この処理は機械的にやるのではなく、ちょっと使う程度で控えましょう。

requestsモジュールでデータを引っ張ってきたHTMLファイル内にある

タグの中身をBeautifulSoupで取得します。

【getWikipedia.py】

import requests
from bs4 import BeautifulSoup

link = "https://ja.wikipedia.org/wiki/"
keyword = ["仮面ライダークウガ", "仮面ライダーアギト", "仮面ライダー龍騎", "仮面ライダー555", "仮面ライダー剣",
           "仮面ライダー響鬼", "仮面ライダーカブト", "仮面ライダー電王", "仮面ライダーキバ", "仮面ライダーディケイド",
           "仮面ライダーW", "仮面ライダーオーズ/OOO", "仮面ライダーフォーゼ", "仮面ライダーウィザード", "仮面ライダー鎧武/ガイム",
           "仮面ライダードライブ", "仮面ライダーゴースト", "仮面ライダーエグゼイド", "仮面ライダービルド", "仮面ライダージオウ"]

for word in keyword:
    with requests.get(link + word) as response:
        # responseはhtmlのformatになっている
        html = response.text
        soup = BeautifulSoup(html, "lxml")
        # <p>タグを取得
        p_tags = soup.find_all('p')
        print(p_tags)
        print('----')

以下のような感じデータが取得されます。

> python getWikipedia.py
[<p class="mw-empty-elt">
</p>, <p><b>仮面ライダークウガ</b>』(かめんライダークウガ)は、<a href="/wiki/2000%E5%B9%B4" title="2000年">2000</a><a href="/wiki/%E5%B9%B3%E6%88%90" title="平成">平成</a>12年)<a href="/wiki/1%E6%9C%8830%E6%97%A5" title="1月30日">130</a>から<a href="/wiki/2001%E5%B9%B4" title="2001年">2001</a>(平成13年)<a href="/wiki/1%E6%9C%8821%E6%97%A5" title="1月21日">121</a>まで、<a href="/wiki/%E3%83%86%E3%83%AC%E3%83%93%E6%9C%9D%E6%97%A5" title="テレビ朝日">テレビ朝日</a>系で毎週日曜8:00 - 8:30<a href="/wiki/%E6%97%A5%E6%9C%AC%E6%A8%99%E6%BA%96%E6%99%82" title="日本標準時">JST</a>)に全49話が放映された、<a href="/wiki/%E6%9D%B1%E6%98%A0" title="東映">東映</a>制作の<a href="/wiki/%E7%89%B9%E6%92%AE%E3%83%86%E3%83%AC%E3%83%93%E7%95%AA%E7%B5%84%E4%B8%80%E8%A6%A7" title="特撮テレビ番組一覧">特撮テレビドラマ</a>作品。

(中略)

</p>, <p>監督は本編チーフ助監督の大峯靖弘が、台本は東映プロデューサーの白倉伸一郎がそれぞれ担当した<sup class="reference" id="cite_ref-U16344_141-3"><a href="#cite_note-U16344-141">[84]</a></sup>。大峯はチーフ助監督としてスケジュール管理も行っていたことから、本編撮影の合間を縫って短時間で撮るという体制であった<sup class="reference" id="cite_ref-U16344_141-4"><a href="#cite_note-U16344-141">[84]</a></sup>。第4.5話の飯島寛騎と瀬戸利樹は別日に撮影しており、編集で共演しているように演出している<sup class="reference" id="cite_ref-U16344_141-5"><a href="#cite_note-U16344-141">[84]</a></sup>。第13.5話で使用しているユルセンのぬいぐるみは大峯の私物である<sup class="reference" id="cite_ref-U16344_141-6"><a href="#cite_note-U16344-141">[84]</a></sup></p>]
----

HTMLのタグが結構はいっているのでもう少しクレンジングをしたほうがいいのかなと思いますが今回はこのまま使用します。

続いては形態素解析を行います。

janomeによる形態素解析

取得されたデータをjanomeモジュールを使用して形態素解析を行います。

janomeに関しては以前のエントリーで触れているので参考にします。

【参考】 uepon.hatenadiary.com uepon.hatenadiary.com

先程、取得できたデータを形態素解析していきます。 形態素解析を行った結果はpwiki.txtというテキストファイルに入れて、以降はWebアクセスなしでも後続の処理ができるようにしています。

【getWiki2wakachi.py】

from janome.tokenizer import Tokenizer
import requests
from bs4 import BeautifulSoup
import codecs

link = "https://ja.wikipedia.org/wiki/"
keyword = ["仮面ライダークウガ", "仮面ライダーアギト", "仮面ライダー龍騎", "仮面ライダー555", "仮面ライダー剣",
           "仮面ライダー響鬼", "仮面ライダーカブト", "仮面ライダー電王", "仮面ライダーキバ", "仮面ライダーディケイド",
           "仮面ライダーW", "仮面ライダーオーズ/OOO", "仮面ライダーフォーゼ", "仮面ライダーウィザード", "仮面ライダー鎧武/ガイム",
           "仮面ライダードライブ", "仮面ライダーゴースト", "仮面ライダーエグゼイド", "仮面ライダービルド", "仮面ライダージオウ"]

corpus = []
wordslist = []
t = Tokenizer()
for word in keyword:
    with requests.get(link + word) as response:
        # responseはhtmlのformatになっている
        html = response.text
        soup = BeautifulSoup(html, "lxml")
        # <p>タグを取得
        p_tags = soup.find_all('p')
        for p in p_tags:
            tokens = t.tokenize(p.text)
            for token in tokens:
                # print("表層形:",token.surface,"\n"
                #       "品詞:",token.part_of_speech.split(',')[0],"\n"
                #       "品詞細分類1:",token.part_of_speech.split(',')[1],"\n"
                #       "品詞細分類2:",token.part_of_speech.split(',')[2],"\n"
                #       "品詞細分類3:",token.part_of_speech.split(',')[3],"\n"
                #       "活用型:",token.infl_type,"\n"
                #       "活用形:",token.infl_form,"\n"
                #       "原形:",token.base_form,"\n"
                #       "読み:",token.reading,"\n"
                #       "発音:",token.phonetic)
                word = ''
                if (token.part_of_speech.split(',')[0] in ['代名詞', '名詞', '固有名詞', '動詞', '形容詞', '形容動詞']) and \
                    (token.part_of_speech.split(',')[1] not in ['数','自立', 'サ変接続', '非自立', '接尾', '副詞可能']) and \
                    (token.base_form not in ['これら', 'せる', 'これ']):
                            word = token.base_form
                            wordslist.append(word)
        corpus.append(wordslist)
        # print(corpus)

with codecs.open("pwiki.txt", "w", "utf-8") as f:
    f.write(str(corpus))

プログラム内で以下のような処理を行っていますがが、明らかに不要(解析が難しい数詞や接尾などの単語)やで代名詞のこれなどの単語に関しては コーパスに入らないようにしています。

                word = ''
                if (token.part_of_speech.split(',')[0] in ['代名詞', '名詞', '固有名詞', '動詞', '形容詞', '形容動詞']) and \
                    (token.part_of_speech.split(',')[1] not in ['数','自立', 'サ変接続', '非自立', '接尾', '副詞可能']) and \
                    (token.base_form not in ['これら', 'せる', 'これ']):
                            word = token.base_form
                            wordslist.append(word)

テキストデータによってはこのような処理も必要かと思います。出来上がったファイルを見ると

【pwiki.txt】

仮面 ライダークウガ ライダークウガ 平成 月 平成 月 テレビ朝日 JST 東映 特撮 テレビ ドラマ 作品
A New Hero A New Legend 英雄 伝説 OP 最後 タイトル 左上 右
テレビ シリーズ 仮面ライダー BLACK RX テレビ シリーズ 仮面ライダー J 仮面ライダー 作品 平成 仮面ライダー シリーズ クウガ 漢字 空 我 名 漢字 名前 石森 プロ 社長 小野寺 章 クワガタ 語感
昭和 仮面ライダー シリーズ 昭和 ライダー 世界 昭和 ライダー オマージュ 台詞 随所 注 昭和 ライダー 違い 仮面ライダー 怪人 人間 世界 悪 秘密 結社 注 劇 仮面ライダー 名称 人間 医療 技術 臓器 人間 東映 人間 影 主人公 平成 ライダー シリーズ
作品 特撮 ヒーロー 番組 新た 試み 随所 身近 現実 特撮 ヒーロー 番組 グロンギ 独自 言語 文化 クウガ 警察 技 作 劇 スポット 回 周囲 人々 社会 ヒーロー 悪 過程 ヒーロー ドラマ 視点 一般 ドラマ 視点 エピソード 基本 エピソード スタイル スタイル 作品
商業 ベルト 人気 好成績 ドラマ 作 劇 シーン 回 月 クウガ 最終 形態 アルティメットフォーム 雄介 幻影 月 最終 直前 作中 注 出番 逆 スポンサー 玩具 会社 形態 アメイジングマイティ 商品 販促 番組 異例 め 最終 A パート B パート 間 CM ED 主役 ヒーロー シーン 主人公 五代 雄介 出番 注

(以下略)

いい感じで出力されています。

形態素解析を行ったあとのコーパスからWord2vecのモデルを生成する

pwiki.txtを使用してモデルを生成します。

モデルの生成はword2vecモジュールのgensim.modelsを使用します。 Word2vecを使用すると意外とたくさんWarningが発生するので、問題がなさそうだったら冒頭にwarningsモジュールをimportして抑止すると見やすくなります。

【wakachi2model.py】

import warnings
warnings.filterwarnings(action='ignore', category=UserWarning, module='gensim')
warnings.filterwarnings(action='ignore', category=FutureWarning)
from gensim.models import word2vec
import codecs

corpus = word2vec.LineSentence("pwiki.txt")
model = word2vec.Word2Vec(corpus, size=100 ,min_count=5,window=5,iter=100)
print(model)
model.save("pwiki.model")

これでモデル(pwiki.model)が生成されました。このモデルファイルがあれば再度データの取得など行わず、すぐに演算を行うことができます。

モデルを使用して演算を行う

以下のようにすれば類似度のランキングや2つのキーワードの類似度を見ることができます。

【oprateWord2vec.py】

import warnings
warnings.filterwarnings(action='ignore', category=UserWarning, module='gensim')
warnings.filterwarnings(action='ignore', category=FutureWarning)
from gensim.models import word2vec

model = word2vec.Word2Vec.load("pwiki.model")

# ドライバーと類似している単語を見る
print('ドライバーと関連する単語ベスト10')
similar_words = model.wv.most_similar(positive=[u"ドライバー"], topn=10)
for key,value in similar_words:
    print('{}\t\t{:.2f}'.format(key, value))

print('-----')
# 2つの単語の類似度を計算
similarity = model.wv.similarity(w1=u"クウガ", w2=u"アギト")
print('クウガとアギトの類似度=>' + str(similarity))

similarity = model.wv.similarity(w1=u"ショッカー", w2=u"ディケイド")
print('ショッカーとディケイドの類似度=>' + str(similarity))

similarity = model.wv.similarity(w1=u"ジオウ", w2=u"ディケイド")
print('ジオウとディケイドの類似度=>' + str(similarity))

このような演算を行うと以下の様な結果が得られます。

Word2Vec(vocab=1819, size=100, alpha=0.025)
ドライバーと関連する単語ベスト10
凌     0.98
ヘルヘイム     0.96
戒斗      0.96
ゲネシスドライバー     0.96
戦     0.96
アーマードライダー     0.95
森     0.95
紋     0.95
不能      0.94
果実      0.94
-----
クウガとアギトの類似度=>0.890038
ショッカーとディケイドの類似度=>0.70078796
ジオウとディケイドの類似度=>0.8640115

上記のような値がでました。

考察

  • ドライバーという単語に近い意味を持つ単語は鎧武ってこと?…人数が多くてドライバーの種類も多かったということなんでしょうか?
  • クウガとアギトの類似度は結構高いようです…ストーリーでもそういう雰囲気はありましたね~
  • ショッカーとディケイドは近いけどそこまでではない?…アポロガイストあたりの影響かな?
  • ジオウとディケイドの類似度はまあ高い…お祭りライダーだからかも?ディケイドがジオウにでているのもあるかな。

なんとなく面白いデータが取れたような気がします。こういうのを考えてみるのもわりと面白いですねえ。

今回のコードをまとめたものを以下においておきます。

import warnings
warnings.filterwarnings(action='ignore', category=UserWarning, module='gensim')
warnings.filterwarnings(action='ignore', category=FutureWarning)
from gensim.models import word2vec
from janome.tokenizer import Tokenizer
import requests
from bs4 import BeautifulSoup
import codecs

# https://www.pytry3g.com/entry/gensim-word2vec-tutorial

link = "https://ja.wikipedia.org/wiki/"
keyword = ["仮面ライダークウガ", "仮面ライダーアギト", "仮面ライダー龍騎", "仮面ライダー555", "仮面ライダー剣",
           "仮面ライダー響鬼", "仮面ライダーカブト", "仮面ライダー電王", "仮面ライダーキバ", "仮面ライダーディケイド",
           "仮面ライダーW", "仮面ライダーオーズ/OOO", "仮面ライダーフォーゼ", "仮面ライダーウィザード", "仮面ライダー鎧武/ガイム",
           "仮面ライダードライブ", "仮面ライダーゴースト", "仮面ライダーエグゼイド", "仮面ライダービルド", "仮面ライダージオウ"]

corpus = []
t = Tokenizer()
for word in keyword:
    with requests.get(link + word) as response:
        print(link + word)
        # responseはhtmlのformatになっている
        html = response.text
        soup = BeautifulSoup(html, "lxml")
        # <p>タグを取得
        p_tags = soup.find_all('p')
        for p in p_tags:
            r = []
            tokens = t.tokenize(p.text)
            for token in tokens:
                # print("表層形:",token.surface,"\n"
                #       "品詞:",token.part_of_speech.split(',')[0],"\n"
                #       "品詞細分類1:",token.part_of_speech.split(',')[1],"\n"
                #       "品詞細分類2:",token.part_of_speech.split(',')[2],"\n"
                #       "品詞細分類3:",token.part_of_speech.split(',')[3],"\n"
                #       "活用型:",token.infl_type,"\n"
                #       "活用形:",token.infl_form,"\n"
                #       "原形:",token.base_form,"\n"
                #       "読み:",token.reading,"\n"
                #       "発音:",token.phonetic)
                word = ''
                if (token.part_of_speech.split(',')[0] in ['代名詞', '名詞', '固有名詞', '動詞', '形容詞', '形容動詞']) and \
                    (token.part_of_speech.split(',')[1] not in ['数','自立', 'サ変接続', '非自立', '接尾', '副詞可能']) and \
                    (token.base_form not in ['これら', 'せる', 'これ']):
                            r.append(token.base_form)
            rl = (' '.join(r)).strip()
            corpus.append(rl)
            # print(corpus)

with codecs.open("pwiki.txt", "w", "utf-8") as f:
    f.write("\n".join(corpus))

corpus = word2vec.LineSentence("pwiki.txt")
model = word2vec.Word2Vec(corpus, size=100 ,min_count=3,window=5,iter=30)
print(model)
model.save("pwiki.model")

model = word2vec.Word2Vec.load("pwiki.model")

# ドライバーと類似している単語を見る
print('ドライバーと関連する単語ベスト10')
similar_words = model.wv.most_similar(positive=[u"ドライバー"], topn=10)
for key,value in similar_words:
    print('{}\t\t{:.2f}'.format(key, value))

print('-----')
# 2つの単語の類似度を計算
similarity = model.wv.similarity(w1=u"クウガ", w2=u"アギト")
print('クウガとアギトの類似度=>' + str(similarity))

similarity = model.wv.similarity(w1=u"ショッカー", w2=u"ディケイド")
print('ショッカーとディケイドの類似度=>' + str(similarity))

similarity = model.wv.similarity(w1=u"ジオウ", w2=u"ディケイド")
print('ジオウとディケイドの類似度=>' + str(similarity))
/* -----codeの行番号----- */