Edited at

「進撃の巨人」に一番似てる文章は?〜word2vec入門〜


前提

Web系会社で社内勉強会を実施したので、その内容を記事に落とし込みました。

そこまで自然言語処理強くないので、ざっくりなイメージで理解して頂けると幸いです。


対象者


  • 理論よりも、とにかくword2vec実装したい!って方

  • 環境構築がめんどくさい方

  • タイトルの答えが気になる方


個人的に「進撃の巨人」に一番似てる文章は?

色々考えたのですが、まず単語ごとに区切って一番近しい言葉を当てはめることにしました。

進撃と言えば...巨人と言えば...と考え、個人的な答えとして思いついたものは...

急襲のノッポさん

MLstudygroup02.pptx.png

ノッポさんじゃないことを祈りながら、実装していきたいと思います。


環境


  • Google Colaboratory

チュートリアルは@tomo_makes様の記事がとても分かりやすかったです。

https://qiita.com/tomo_makes/items/f70fe48c428d3a61e131#%E8%A9%A6%E3%81%97%E6%96%B9

Python3.6でも問題なく動きましたが、環境構築さえめんどくさいと思うので、Colaboratoryを使用しました。


実装


Colabとword2vec実装

Google ColaboratoryにGoogle Driveをマウントします。

from google.colab import drive

drive.mount('/content/drive')

Google Drive配下の特定フォルダに移動

(事前にフォルダを作成する必要有。私の場合はMy Drive配下に「MLstudygroup02」というフォルダを作成した)

%cd /content/drive/My Drive/MLstudygroup02

gensimをimportします

「gensim」とは、文章を単語ごとで分割したり、コーパスを作成することが出来るPythonのライブラリ(らしい)

from gensim.models.word2vec import Word2Vec

import gensim

学習済みモデル(コーパス)をダウンロードして、特定フォルダ配下に配置する。(私の場合、「MLstudygroup02」配下に.modelを配置)

コーパスとは、簡単に記載すると大量のテキストデータ。今回はwikipediaのコーパス。以下からダウンロード。

http://aial.shiroyagi.co.jp/2017/02/japanese-word2vec-model-builder/

modelW_path = './word2vec_models/word2vec.gensim.model'

model = Word2Vec.load(modelW_path)

wikipedia以外のコーパスを使用したい場合は、探せば結構出てきます。

自分で作成したい!という方は以下記事が参考になると思います。

上記処理でコーパスを読むことが出来たので、単語をベクトル変換出来るか確認してみます。

# 「単語」→model→ベクトル(数値)のイメージ

# 「愛」という単語が50次元のベクトルに変換
print(len(model.wv["愛"]))
# 「愛」という単語をベクトル化した結果
model.wv["愛"]

# 出力結果
# 50
# array([ 0.09289702, -0.16302316, -0.08176763, -0.29827002, 0.05170078,
# 0.07736144, -0.06452437, 0.19822665, -0.11941547, -0.11159643,
# 0.03224859, 0.03042056, -0.09065174, -0.1677992 , -0.19054233,
# 0.10354111, 0.02630192, -0.06666993, -0.06296805, 0.00500843,
# 0.26934028, 0.05273635, 0.0192258 , 0.2924312 , -0.23919497,
# 0.02317964, -0.21278766, -0.01392282, 0.24962738, 0.11264788,
# 0.05772769, 0.20941015, -0.01239212, -0.1256235 , -0.19794041,
# 0.1267719 , -0.12306885, 0.01006295, 0.08548331, -0.08936502,
# -0.05429656, -0.09757583, 0.10338967, 0.13714872, 0.23966707,
# 0.02216845, 0.02270923, 0.32569838, -0.0311841 , -0.00150117],
# dtype=float32)

次に、「愛」という単語はどの単語に似ているのか類似度を表示します。

sim_do = model.wv.most_similar(positive=["愛"], topn=9)

# リスト化されているので、見やすいように整形
print(*[" ".join([v, str("{:.5f}".format(s))]) for v, s in sim_do], sep="\n")

# 出力結果
# 恋 0.82252
# 愛する人 0.78936
# 心 0.78868
# 慈愛 0.78840
# いのち 0.78585
# 永遠 0.78321
# 絆 0.77754
# 天国 0.76998
# 哀しみ 0.76375

「愛」と「恋」は似ている。というのは何となく理解出来るのですが、「愛」と「哀しみ」が似ているというのはエモさありますね。

次に、2つの単語の類似度を求めたいと思います。

similarity = model.wv.similarity(w1="うどん", w2="蕎麦")

print(similarity)

similarity = model.wv.similarity(w1="うどん", w2="香川")
print(similarity)

similarity = model.wv.similarity(w1="うどん", w2="アンパンマン")
print(similarity)

# 出力結果
# 0.9250555
# 0.38046315
# 0.28074443

「うどん」と「蕎麦」が似ているのは当たり前ですが、「うどん」と「香川」が似ていないのは意外ですね。

次に、似ていない(真逆の単語)を求めたいと思います。

sim_do = model.wv.most_similar(negative=["赤"], topn=5)

print(*[" ".join([v, str("{:.5f}".format(s))]) for v, s in sim_do], sep="\n")

# 出力結果
# ウッセイ 0.47948
# 自活 0.46734
# 傾注 0.46145
# 慈善 0.46126
# 船齢 0.45573

「赤」の真逆と言えば、「黒」や「青」を想像しますが、自然言語処理では全く関係のない単語が出力されます。

「ウッセイ」って何!?!??って単語が出力されます。正直初めて耳にした単語です。

モデルをチューニングすると、しっかり真逆っぽい言葉が出力されるみたいです。

@youwht様の「赤の他人」の対義語は「白い恋人」記事がとても良かったので、興味ある方は是非。

https://qiita.com/youwht/items/f21325ff62603e8664e6

次に、単語を足したり引いたりして、「王様」-「男性」+「女性」=?を求めてみたいと思います。

sim_do = model.wv.most_similar(positive = ["王様", "女性"], negative=["男性"], topn=5)

print(*[" ".join([v, str("{:.5f}".format(s))]) for v, s in sim_do], sep="\n")

# 出力結果
# お姫様 0.85313
# 花嫁 0.83918
# 野獣 0.83155
# 魔女 0.82982
# 乙女 0.82356

結構それっぽい値が出てきましたね!

word2vecのチュートリアルとして「王様」-「男性」+「女性」=「女王」という記事を良く見るのですが、これはコーパスがwikipediaコーパスだった為、出力結果が異なると考えられます。

コーパスが違うと出力結果が異なる例としては、以下のようなパターンが考えられます。

# 例

# ダイエット文章をコーパス化した場合
sim_do = model.wv.most_similar(positive=["ハンバーグ"], topn=2)
print(*[" ".join([v, str("{:.5f}".format(s))]) for v, s in sim_do], sep="\n")

# 出力結果
# 豆腐 0.87611
# おから 0.76542

# 食べログ文章をコーパス化した場合
sim_do = model.wv.most_similar(positive=["ハンバーグ"], topn=2)
print(*[" ".join([v, str("{:.5f}".format(s))]) for v, s in sim_do], sep="\n")

# 出力結果
# ステーキ 0.66544
# 肉汁 0.62121

出力結果が大きく異なる為、コーパス選びはとても重要な作業となります。

恐らくTwitter文章のコーパス等あれば、「うどん」と「香川」は似てそうな気がしますね。

今までは単語のみの類似度を求めてきましたが、次は文章の類似度を調べてみましょう。

sim_do = model.wv.most_similar(positive=["犬も歩けば棒に当たる"], topn=9)

print(*[" ".join([v, str("{:.5f}".format(s))]) for v, s in sim_do], sep="\n")

# 出力結果
# KeyError: "word '犬も歩けば棒に当たる' not in vocabulary"

エラー内容として、そのような単語は学習されていません。というエラーが出力されています。

コーパスは単語ごとで区切られており、文章単位で学習していない為だと考えられます。


本題

上記のエラーを解消する為、入力文章を形態素解析し、文章をこれ以上分割出来ない最小単位に分割する必要があります。

形態素解析ツールは「Mecab」や「Janome」や「nagisa」や「COTOHA API」等、様々なツールがありますが、今回は導入の楽さを考慮して「nagisa」を使用します。

@Haruki314様の「COTOHA API」と「Mecab」の比較記事( https://qiita.com/Haruki314/items/799fa25173f2edf92982

「nagisa」をインストールした後、importします

%pip install nagisa

import nagisa

文章を入力し、形態素解析出来ているか確認します。

# 文章を入力

text = '進撃の巨人'
# 文章を形態素解析
words_tagging = nagisa.tagging(text).words
print(words_tagging)

# 出力結果
# ['進撃', 'の', '巨人']

助詞や助動詞も変換してしまうと文章としておかしくなる可能性が高いので、変換する品詞としては「名詞」・「形状詞(多分形容動詞?)」・「形容詞」・「動詞」あたりに絞って、変換します。

# 「名詞」、「形状詞」、「形容詞」、「動詞」のみを抽出

words_extract = nagisa.extract(text, extract_postags=['名詞', "形状詞", "形容詞", "動詞"]).words
# ユニークな単語を排除
# 「早く早く仕事を終わらせる」等の文章の場合、「早く」が2回抽出される為
words_extract = list(dict.fromkeys(words_extract))
print(words_extract)

# 出力結果
# ['進撃', '巨人']

「Python」等の、辞書に登録されていない単語を調べようとすると、エラーが出力される為、登録されていない単語を変換する場合は原文を返すメソッドを定義します。

def postive(word):

try:
return model.wv.most_similar(positive=[word], topn=1)[0][0]
except:
return word

ようやくですが、「進撃の巨人」に一番似ている文章を調べます!!!!!!

処理としては冗長な部分があるので、綺麗なコードではありません...

# 名詞、形状詞、形容詞、動詞をFor文で回す

for words_ext in words_extract:
# 形態素解析したものをindexと単語に分けてFor文で回す
for i, word_tag in enumerate(words_tagging):
# 原文と同じ単語の場合、似ている単語に置換
if words_ext == word_tag:
words_tagging[i] = postive(words_ext)

# リストを繋げて文字列化
print("".join(words_tagging))

# 出力結果
# 進軍の江夏豊

進軍の江夏豊

まさかの江夏豊...巨人軍じゃなくて阪神の人や...!!!いっそのことノッポさんの方が近い説

wikipediaコーパスだと巨人 = 江夏豊になりましたが、漫画コーパスだと、もしかしたら違う結果になるかもしれないですね!


補足

ちなみに、他にも似てる文章を調べると以下のような結果になりました!


  • 「足をすくわれる」 → 「片足を引っ張られる」

  • 「火に油を注ぐ」 → 「たいまつに粉を注ぎ込む」

  • 「狐の嫁入り」 → 「狸の遊女」

  • 「鴨が葱を背負って来る」 → 「栗が筍を投げ飛ばして行く」

  • 「天空の城ラピュタ」 → 「天上の居城マレフィセント」

  • 「紅の豚」 → 「緋のヒツジ」

  • 「千と千尋の神隠し」 → 「万と静香の化け猫」

  • 「猫の恩返し」 → 「ネコの感謝」

  • 「ハウルの動く城」 → 「ドクター・ストレンジの動かす居城」

  • 「崖の上のポニョ」 → 「断崖の輪郭のライサンダー」

  • 「モアナと伝説の海」 → 「ガブリエラと伝承の海へ」

  • 「美女と野獣」 → 「美少年と狩人」

  • 「赤の他人」 → 「青の他者」

  • 「プラダを着た悪魔」 → 「グッチを羽織ったサタン」

また、KeyErrorを出力させる為に、以下コードを実行してみたのですが、まさかのエラーが出ないという結果になりました...(wikipediaのコーパスに「進撃の巨人」が入っているという事案)

sim_do = model.wv.most_similar(positive=["進撃の巨人"], topn=5)

print(*[" ".join([v, str("{:.5f}".format(s))]) for v, s in sim_do], sep="\n")

# 出力結果
# フラガール 0.85298
# ガールズ&パンツァー 0.85175
# アイドルマスターシリーズ 0.84194
# HEROES 0.83748
# ガンダムSEED 0.83154

本当に似ている文章は「フラガール」説


感想

とにかく初心者が手を動かして楽しめるような内容にしました。

次はコーパスを作って違うものを作ってみようと思います。

上にあげた「赤の他人を白い恋人」や「食べログ自然言語処理」や「三国志自然言語処理」等とても面白かったので、興味ある方は是非是非!