PyLearn NLP 02: Word 2 Vec その1

日大生産工 大前佑斗

Word2Vecとは、word to vectorの略語で、その名の通り、ワードをベクトルに変換する仕組みを意味します。googleによって開発されたアルゴリズムで、いろいろなところで使用されています。今回は、w2vを用いて、単語と単語の類似度を算出する方法、単語から単語を引き算したり、足し算したりする方法を学びます。

学習済みモデルのダウンロード

w2vは、大規模な文章データでモデルを学習させる必要があります。ここでは、すでに学習済みのモデルがあるので、それを使用します。学習済みモデルは色々と公開されていますが、Qiita@Hironsanが作成したものを使用します。以下からDownload Word Vectorsを選択し、ダウンロードしてください。

(落とせない場合はこちら

ダウンロードしたら、以下コードを打ち込み、学習済みモデルを取得します。

In [7]:
# *** 50秒くらいかかります。連打しないこと ***
import gensim
model = gensim.models.KeyedVectors.load_word2vec_format('model.vec', binary=False)

指定した単語と類似している単語の検出

次に、学習済みモデルmodelを使用して、指定した単語の類似度を測ってみましょう。例として、「りんご」と類似する単語を「5」個取り出してみます。これは、以下のように記述すればokです。

In [8]:
useword = "りんご" # 検出対象のワード
num = 10 # 上位何個の単語を検出するか
v = model[useword] # model.wv[useword]はv4で消えるので、この記法
wd = model.most_similar( [ v ], [], num)

print(wd)
[('りんご', 1.0), ('リンゴ', 0.650419294834137), ('イチゴ', 0.5795953273773193), ('ブルーベリー', 0.5524570345878601), ('サクランボ', 0.5325688719749451), ('プルーン', 0.5178399085998535), ('夕張メロン', 0.5152283906936646), ('みかん', 0.5119045972824097), ('さくらんぼ', 0.507516622543335), ('苺', 0.5053956508636475)]

似ている単語が出てきました。数字がセットになっていることがわかります。この数字は類似度を表しており、最大1を取ります。一番上にある単語「りんご」は、完全に一致しているので、類似度が最大となります。続いて「リンゴ、イチゴ、ブルーベリー...」と続きます。

なお、i番目に似ている単語とその類似度は、以下のように書けばアクセスできます。

In [9]:
i = 4
print("単語: ", wd[i][0])
print("類似度: ", wd[i][1])
単語:  サクランボ
類似度:  0.5325688719749451

ちなみに、modelに登録されていない単語は、類似度を測れません。例えば、以下のコードはエラーが出ます。

In [13]:
# v = model["りんごん"]

このように新たな単語を登録したいときは、そのワードが入った文章をいくつも用意し、モデルを学習させる必要があります。これについては、次回以降で解説します。 

指定した2つの単語同士の類似度を算出

続いて、指定した2つの単語同士の類似度を求めてみます。これは、similarity関数を利用します。

In [14]:
w1 = "りんご"
w2 = "みかん"
w3 = "ラジオ"

d12 = model.similarity(w1, w2)
d13 = model.similarity(w1, w3)
d23 = model.similarity(w2, w3)

print("「" + w1 + "」と「" + w2 + "」の類似度: " + str(d12))
print("「" + w1 + "」と「" + w3 + "」の類似度: " + str(d13))
print("「" + w2 + "」と「" + w3 + "」の類似度: " + str(d23))
「りんご」と「みかん」の類似度: 0.51190466
「りんご」と「ラジオ」の類似度: 0.14930405
「みかん」と「ラジオ」の類似度: 0.17428736

果物同士は似ていて、ラジオとは似ていないようです。 graphvizを使用すると、この関係を可視化できます。これについては、http://int-info.com/PyLearn/PyLearnTIPS01.html を参照してくださ。

In [24]:
from graphviz import Graph

nG = Graph(format="pdf") # 拡張子の設定 (pdf/pngなど)
nG.attr("node", shape="square", color="black")

# エッジの定義
nG.edge(w1, w2, label=str(round(d12, 3)), penwidth=str(d12*10))
nG.edge(w1, w3, label=str(round(d13, 3)), penwidth=str(d13*10))
nG.edge(w2, w3, label=str(round(d23, 3)), penwidth=str(d23*10))

# ノードの定義
nG.node(w1, shape="circle", color="blue", fixedsize="True", width="0.7", height="0.7")
nG.node(w2, shape="circle", color="blue", fixedsize="True", width="0.7", height="0.7")
nG.node(w3, shape="circle", color="blue", fixedsize="True", width="0.7", height="0.7")

# 描画
nG.render("ngraphs")
display(nG)
%3 りんご りんご みかん みかん りんご--みかん 0.512 ラジオ ラジオ りんご--ラジオ 0.149 みかん--ラジオ 0.174

こんな感じになりました。類似しているほど、パスを太くしています。これだけだとつまらないですが、単語数が多くなると面白そうですね。

単語同士の演算

続いて、単語同士の演算について考えてみます。 複数の単語同士を足したり引いたりしたい場合は、足したい単語をmost_similar関数のpositiveに、引きたい単語をnegative引数に与えればokです。複数入れてもokです。掛け算や割り算はありません。

In [71]:
# 人間 - 知性?
t = model.most_similar(positive=['人間'], negative=['知性'])
print(t)
# -> 人間から知性を抜くと、動物になるようです。
[('タヌキ', 0.2672618627548218), ('隠亡', 0.26263779401779175), ('妖怪', 0.2605975270271301), ('猫', 0.25559699535369873), ('もぐっ', 0.254338800907135), ('ヒグマ', 0.2529761791229248), ('ぶつ', 0.2525160312652588), ('かくう', 0.25100135803222656), ('ねずみ', 0.2475045919418335), ('ざくろ', 0.24692127108573914)]
In [72]:
# 女 + 王 ?
t = model.most_similar(positive=['女', '王'], negative=[])
print(t)
# -> 女性を王様にすると、王妃や王女になるようです。
[('王妃', 0.6784960627555847), ('王女', 0.6658340692520142), ('妃', 0.6655517816543579), ('男', 0.6475381255149841), ('娘', 0.621396541595459), ('后', 0.5808514356613159), ('太子', 0.5691717863082886), ('国王', 0.5656334757804871), ('王族', 0.564300000667572), ('マリア・アマーリア', 0.5629106760025024)]
In [74]:
# サラリーマン + 夢 ?
t = model.most_similar(positive=['サラリーマン', '夢'], negative=[])
print(t)
# サラリーマンが夢を持つと、フリーターや貧乏になるようです...
[('夢見', 0.5900476574897766), ('夢見る', 0.5714670419692993), ('フリーター', 0.5640970468521118), ('OL', 0.5604824423789978), ('貧乏', 0.5415534973144531), ('あこがれ', 0.5379558801651001), ('幸せ', 0.5321822762489319), ('憧れ', 0.5274276733398438), ('中年', 0.5272011160850525), ('青春', 0.5238356590270996)]
In [75]:
# 大人 - 年齢?
t = model.most_similar(positive=['大人'], negative=['年齢'])
print(t)
# -> 大人から年齢を引くと、子供になるようです。
[('小人', 0.3122234344482422), ('こども', 0.2979876399040222), ('軻比', 0.27476775646209717), ('ここら', 0.2729908525943756), ('跋', 0.25244349241256714), ('放題', 0.2490951418876648), ('一日中', 0.24668405950069427), ('ちょっとした', 0.2459249496459961), ('おとな', 0.24353891611099243), ('またたび', 0.24327655136585236)]
In [82]:
# 人間 + 労働 + 残業?
t = model.most_similar(positive=['人間', '労働', '残業'], negative=[])
print(t)
# 人間が働いて残業をすると、賃金になるようです。
[('賃金', 0.6605127453804016), ('正社員', 0.5944187641143799), ('職場', 0.5885380506515503), ('就業', 0.5760812759399414), ('従業', 0.5467520952224731), ('雇用', 0.5461084842681885), ('労務', 0.5426047444343567), ('搾取', 0.5410510897636414), ('プレカリアート', 0.5390125513076782), ('非現業', 0.5375046730041504)]

色々例を出しましたが、多くの場合、おかしい結果が返ってきます。モデルの学習をより良くするともう少しそれらしい回答が期待できますが、限界もあるでしょう。自然言語の難しさを表していると言えます。

学習済みモデルを利用した簡単なアプリケーション

形態素解析とmecabを利用して、簡単なアプリケーションを作ってみます。

  • 機能:

    • 入力した文章に対して、名詞と動詞を似た別の単語に置き換え、新たな文章を生成する。
  • アルゴリズム:

    • 文書をキーボードから入力させる。
    • 入力された文章を、形態素解析により品詞分解する。
    • 名詞と動詞を検出する。
    • 検出された語句について、w2vで類語を取り出す。
    • 取り出された類語を、名詞と動詞があった位置に代入する。
    • これをランダムに行い、様々な文書を生成する。
In [115]:
#  50秒くらいかかるので、一回のみ実行
import MeCab
import gensim
import random
model = gensim.models.KeyedVectors.load_word2vec_format('model.vec', binary=False)
In [122]:
# テキスト入力
print("")
print("*** 入力テキスト ***")
text = input()

# 品詞情報の取得
Info01, Info02 = [], []
Word = []

tag = MeCab.Tagger("-Ochasen")
node = tag.parseToNode(text)

while node:
    Info01.append(node.feature.split(",")[0]) # 品詞情報1
    Info02.append(node.feature.split(",")[1]) # 品詞情報2
    Word.append(node.surface.split(",")[0]) # 単語
    node=node.next

# 解析対象を抜き取り
TargetWord = []
TargetNumber = []
for i in range(len(Word)):
    
    # 検出したい品詞を決定(名詞、動詞、形容詞)
    # if Info01[i] == "名詞" or Info01[i] == "動詞" or Info01[i] == "形容詞":
    if Info01[i] == "名詞" or Info01[i] == "動詞":
    # if Info01[i] == "名詞":
        TargetWord.append(Word[i])
        TargetNumber.append(i)

wv = []
for i in range(len(TargetWord)):
    wv.append(model[TargetWord[i]])
    
    
# 単語の類似度の検索
SimWord = []
GetWord = []
NumGetWord = 5 # 検出したい単語数
for i in range(len(TargetWord)):
    SimWord.append(model.most_similar( [ wv[i] ], [], NumGetWord))
    
    temp_txt = ""
    for j in range(1, NumGetWord):
        temp_txt = temp_txt + SimWord[i][j][0] + " / "
    GetWord.append(temp_txt)
    
# 純粋な単語のみを取り出し
PureWord = []
for i in range(0, len(GetWord)):
    PureWord.append(GetWord[i].split("/"))
    
# 自動文章生成
GenerationTxtList = []
NumGenerate = 5 # <- 生成文章の数
for m in range(0, NumGenerate):
    GenerationTxt = ""
    k=0
    for i in range(0, len(Word)):

        # 置き換え単語の場合、置き換え
        changeFlag = 0
        for j in range(0, len(TargetNumber)):
                if i == TargetNumber[j]:
                    r = random.randint(0, len(PureWord[k])-2) # <- 乱数生成
                    GenerationTxt = GenerationTxt + PureWord[k][r]
                    k=k+1
                    changeFlag = 1
                    break

        # 置き換えではない場合、そのまま結合
        if changeFlag == 0:
            GenerationTxt = GenerationTxt + Word[i]
    
    # 生成文章をappend
    GenerationTxtList.append(GenerationTxt)

# 生成テキストの表示
print("")
print(" *** 生成テキスト ***")
for i in range(0, len(GenerationTxtList)):
    print(GenerationTxtList[i])
    
    
# 類似用語の出力
print("")
print("*** 検出単語一覧 ***")
for i in range(0, len(TargetWord)):
    print("")
    print("第"+str(i)+"単語: " + TargetWord[i])
    print(GetWord[i])
*** 入力テキスト ***
私がその絵を見たとき、心に衝撃が走った。

 *** 生成テキスト ***
 貴方 がその画 を み た 時 、 こころ に 感銘 が 走り た。
 あの がその 絵画 を み た際 、 こころ に 動揺 が 通っ た。
 わたし がその 下絵 を 見る た 時 、 愛情 に 感銘 が 走ら た。
 貴方 がその画 を み た際 、 愛情 に 感銘 が走る た。
あなた がその画 を見受け た 時 、 愛情 に 動揺 が 走ら た。

*** 検出単語一覧 ***

第0単語: 私
あなた / 貴方 / わたし / あの / 

第1単語: 絵
画 / 下絵 / 絵画 / 挿絵 / 

第2単語: 見
見受け / み / 見る / られる / 

第3単語: とき
際 / ので / 時 / すると / 

第4単語: 心
好奇 / 愛情 / こころ / 奥底 / 

第5単語: 衝撃
ショック / 感銘 / 受け止め / 動揺 / 

第6単語: 走っ
走る / 走り / 走ら / 通っ / 

てきとうに作ってみましたが、結構おもしろい結果になりました。Webアプリ化するといいかもしれませんね。

本日は、すでに学習されているモデルを利用して、w2vの威力を体験してみました。次回は実際に自分でモデルを作る方法などを学習していきます。