Pythonでword2vecを自在に操って高次元ベクトルを可視化

標準

word2vecで色々な言葉をベクトルに出来たのは良いものの、それを一切活用できない宝の持ち腐れ状態だったのでpythonで色々といじくれるように頑張ってみました。

word2vecをpythonでいじれる環境を作る

依存するパッケージをpipでインストールします。

sudo -H pip install -I numpy scipy gensim matplotlib sklearn

python仕様に辞書を作り直す

以前作ったモデルファイルはバイナリになっていてpythonで使うことが出来ないので(多分)、それも含めてもう一度学習をしないといけなくなりました。ので、今回はpythonでコーパスから学習するpythonのコードを。

# -*- coding:utf-8 -*-
from gensim.models import word2vec

files = word2vec.Text8Corpus('tweets.txt')

#size 次元数 コーパスに収録されている語数に応じて増やすと良いらしい
#min_count この回数未満登場する単語は無視される
#window 文脈判断 この前後の語数だけ一つの文脈だと判断します
#sample 単語の無視 この頻度だけ単語を無視します

model = word2vec.Word2Vec(files, size=200, min_count=10, window=10, sample=1e-3)
model.save("tweets-train-200.model")

これでモデルファイルが生成されます。binaryオプションはつけちゃだめです。コーパスの量によって処理の時間はかかりますが、私の場合は4,000万以上のツイート集を利用したので処理に4時間ほどかかりました。(Bash on Windowsを利用しました)

(ここまでの環境はUbuntuでしたが、ここからの環境はWindowsです。)

実際にベクトルを見てみる

無事に学習が終わったので、実際に単語のベクトルを見てみます。

# -*- coding:utf-8 -*-
from gensim.models import word2vec
import numpy as np
import sys

target = sys.argv[1]

model = word2vec.Word2Vec.load("tweets-train-200.model")
try:
    target_model = model[target]
except:
    #辞書に収録されてないときはゼロベクトルを代入
    target_model = [0]
    raise

#配列形式で出力
print(np.array2string(target_model, separator=', ', formatter={'float_kind': lambda x: '{: .4f}'.format(x)}))

#普通に標準出力(指数表示)
print(target_model)

これでコマンドラインから引数に調べたい単語を添えると結果が標準出力になります。

> python view_vec.py アニメ

[-3.0598, -1.0856,  0.5501,  0.3603,  6.1139, -0.8803,  1.1255, -1.6247,
  2.4462,  0.8340, -1.7479, -0.8233,  4.1820, -2.8125,  1.5961,  0.0054,
  2.7226,  0.2497,  1.0567, -1.6948, -2.7111, -2.0759,  1.5670,  1.9177,
 -2.1502,  4.3175,  0.1491,  2.5644,  2.2666, -1.4657,  2.9952,  0.9764,
 -1.1360,  0.1766,  1.9482,  2.1181, -1.3740, -7.5991,  2.0472, -1.2783,
 -0.7213,  3.8815, -1.4831, -0.9652,  2.2482,  2.2893,  3.3173,  4.9056,
 -0.4750,  0.4818, -0.4340, -0.0209, -1.6915, -1.1480, -1.7376,  3.0522,
 -1.2222, -1.2791, -1.5357,  3.8215, -0.9456, -0.2643, -0.0183, -2.4773,
  1.4308, -0.8442, -0.7839,  2.2442,  2.0562,  3.6583,  0.7669, -0.1983,
  1.9040, -0.4223, -0.3367, -3.7683,  0.9073, -2.0037,  7.4025,  0.7309,
  1.6203, -0.3519, -0.3557,  3.9803,  2.0642,  0.8938, -2.8267, -2.5608,
 -3.1776,  1.1625, -0.7042, -0.9658,  4.8135,  0.9713,  4.2898,  3.2817,
 -3.8686,  1.8089,  0.8076, -1.7774,  2.5647,  0.4590,  0.0024,  1.5371,
 -2.8700,  0.3400, -0.7222,  2.9524,  0.9778,  0.3452,  5.9796,  3.4122,
  3.2135,  1.0425, -3.4586, -1.3652, -2.2248,  2.8334, -0.2026,  0.4311,
 -4.9417,  1.5280, -0.5980,  0.2509, -0.2174, -0.0101,  2.7904,  3.4795,
 -1.0065, -1.2637,  3.2353,  2.8361,  1.3290, -0.6255,  4.8891, -1.0721,
  3.9757,  1.3872,  2.8494, -2.4399, -2.2528,  3.7583, -2.9619,  0.1122,
  2.6769,  1.6758,  3.1519, -2.9497, -1.6210,  4.9706,  1.6269,  0.8466,
  1.3961,  0.1470, -0.6976, -0.1300, -1.1262,  1.3919,  0.9148, -1.0835,
 -5.0799,  1.2272,  0.2235,  1.0859, -2.9946,  3.3173, -3.7431, -0.4212,
 -4.8388, -1.1162,  0.7835, -0.2319, -3.7270, -1.8361, -0.6881, -1.1337,
  0.5921, -3.0593, -0.5235,  0.1439,  1.5064, -0.3918, -1.0260, -3.3447,
  2.0733, -1.2811, -0.0204,  3.2155,  1.7667, -1.5619,  0.7945,  4.1375,
 -1.8093, -2.2561, -3.6757,  0.9597, -1.2544, -3.7155,  3.7733,  0.7932]

これが成功なのかよく分かりませんが、一応ベクトルの生成には成功しているのがわかります。

次元圧縮をしてみる

200次元ベクトルを人間が理解するのは到底不可能なので、ここでは誰でも簡単に理解できる2次元まで次元を圧縮します。
主成分分析(PCA;Principal Component Analysis)と呼ばれる手法を使うのですが、こういう話は全然分からないのでソフト任せで適当にやってもらいます。

# -*- coding:utf-8 -*-

from gensim.models import word2vec
import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA

model = word2vec.Word2Vec.load("tweets-train-200.model")

words = []
words.append("りんご")
words.append("バナナ")

length = len(words)
data = []

j = 0
while j < length:
    data.append(model[words[j]])
    j += 1

pca = PCA(n_components=2)
pca.fit(data)
data_pca= pca.transform(data)

length_data = len(data_pca)

i = 0
while i < length_data:
    #配列形式に整形
    print(np.array2string(data_pca[i], separator=', ', formatter={'float_kind': lambda x: '{: .4f}'.format(x)}))
    i += 1

これでコマンドから実行すると

> python pca_sample.py

[-12.5588, -0.0000]
[ 12.5588,  0.0000]

いい感じに2次元に圧縮できています。では次はもうちょっと応用して座標平面にプロットしてみます。

座標平面にプロットして相関を調べる

MatPlotLibを使って次は座標平面にプロットしてみます。

# -*- coding:utf-8 -*-

from gensim.models import word2vec
import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA

model = word2vec.Word2Vec.load("tweets-train-200.model")

#調べる情報を配列に収納
capital_name = []
capital_name.append(["前橋市","群馬県","Maebashi","Gumma"])
capital_name.append(["横浜市","神奈川県","Yokohama","Kanagawa"])
capital_name.append(["甲府市","山梨県","Kohu","Yamanashi"])
capital_name.append(["金沢市","石川県","Kanazawa","Ishikawa"])
capital_name.append(["名古屋市","愛知県","Nagoya","Aichi"])
capital_name.append(["津市","三重県","Tsu","Mie"])
capital_name.append(["大津市","滋賀県","Ohtsu","Shiga"])
capital_name.append(["神戸市","兵庫県","Kobe","Hyogo"])
capital_name.append(["松江市","島根県","Matsue","Shimane"])
capital_name.append(["高松市","香川県","Takamatsu","Kagawa"])
capital_name.append(["松山市","愛媛県","Matsuyama","Ehime"])
capital_name.append(["那覇市","沖縄県","Naha","Okinawa"])

length = len(capital_name)
data = []

j = 0
while j < length:
    print(capital_name[j][1])
    data.append(model[capital_name[j][0]])
    data.append(model[capital_name[j][1]])
    j += 1

pca = PCA(n_components=2)
pca.fit(data)
data_pca= pca.transform(data)

length_data = len(data_pca)

i = 0
j = 0
while i < length_data:
    #点プロット
    plt.plot(data_pca[i][0], data_pca[i][1], ms=5.0, zorder=2 ,marker="x")
    plt.plot(data_pca[i+1][0], data_pca[i+1][1],ms=5.0, zorder=2 ,marker="x")

    #線プロット
    plt.plot((data_pca[i][0], data_pca[i+1][0]),(data_pca[i][1],data_pca[i+1][1]),c="b",linewidth=0.5,zorder=1,linestyle="--")

    #文字プロット
    plt.annotate(capital_name[j][2],(data_pca[i][0], data_pca[i][1]),size=7)
    plt.annotate(capital_name[j][3],(data_pca[i+1][0], data_pca[i+1][1]),size=7)

    j += 1
    i += 2

plt.show()

これで
県名(x)——-県庁所在地名(x)
と言った感じに線で結んだ図を出力できて、容易にその関係を把握することが出来ます。
(上記コードはmatplotlibに2バイト文字入力すると文字化けするから適当に対処したコードです。後にこの文字化けは回避できると学習しました。)

実際に実行すると以下のような図が出力されます。

集中してて文字は読みにくいところありますが、見事県庁所在地名と県名の間のベクトルの相関を観測することが出来ました。

おまけ程度に国名と首都名の間での相関も調べてみました。

県名の場合とは違ってちょっとバラつきました。サンプル数が少なかったんでしょうか。今回はこの相関が偶然じゃないことを示すために、「豚肉」-「鶏肉」の関係も調べて図に組み込まれています。

座標平面にプロットして分類してみる

前項で一定の関係にある単語間には一定の相関がある(つまり、引き算すると一定のベクトルが抽出できる)ことがわかったので、次は色々な単語のベクトルを調べてジャンルごとに分類できているのかを調べてみます。

# -*- coding:utf-8 -*-

from gensim.models import word2vec
import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA

model = word2vec.Word2Vec.load("tweets-train-200.model")

words = []
words.append(["生物","r"])
words.append(["数学","r"])
words.append(["現代文","r"])
words.append(["世界史","r"])
words.append(["物理","r"])
words.append(["豚肉","c"])
words.append(["鶏肉","c"])
words.append(["キャベツ","c"])
words.append(["チーズ","c"])
words.append(["牛乳","c"])
words.append(["卵","c"])
words.append(["白菜","c"])
words.append(["じゃがいも","c"])
words.append(["ニンジン","c"])
words.append(["国土交通省","y"])
words.append(["厚生労働省","y"])
words.append(["外務省","y"])
words.append(["総務省","y"])
words.append(["バス停","m"])
words.append(["電車","m"])
words.append(["新幹線","m"])
words.append(["バス","m"])
words.append(["タクシー","m"])
words.append(["車","m"])
words.append(["自転車","m"])

length = len(words)
data = []

j = 0
while j < length:
    data.append(model[words[j][0]])
    j += 1

pca = PCA(n_components=2)
pca.fit(data)
data_pca= pca.transform(data)

length_data = len(data_pca)

i = 0
while i < length_data:
    #点プロット
    plt.plot(data_pca[i][0], data_pca[i][1], ms=5.0, zorder=2, marker="x", color=words[i][1])

    #文字プロット
    plt.annotate(words[i][0], (data_pca[i][0], data_pca[i][1]), size=7)

    i += 1

plt.show()

今回は適当に思いついた単語をジャンルごとに人間の観点で分類し(r,c,y,m…はカラーコードです)、それを機械も正しく分類できるのかを試します。

実際に実行すると以下のような図が出力されます。

いい感じに色ごとに単語が集まってくれました。完璧ですね!

おしまい

今回が初めてのpythonコーディングだったんですけど、意外と書きやすくて面白いなと思いました。今回学んだ仕組みを応用すれば、受け取った単語がどのような分類に所属するのかを機械が判定する、なんてこともできるんじゃないかなと思いました。いつかやってみたい。

投稿者プロフィール

yoshipc
コンピューター関連を得意としています。PHPが専門です(尚、技量はお察し)。このブログとMastodonのインスタンスを運営・管理しています。よろしくお願いいたします。

コメントを残す

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