読者です 読者をやめる 読者になる 読者になる

六本木で働くデータサイエンティストのブログ

元祖「銀座で働くデータサイエンティスト」です / 道玄坂→銀座→東京→六本木

青空文庫のデータを使って、遅ればせながらword2vecと戯れてみた

Python テキストマイニング 機械学習

もう既に山ほど解説記事が出回っていて、あまつさえそれを利用したwebサービスまで出てきてしまっているword2vecですが、うちの現場でも流行っているのでせっかくなので僕もやってみようと思い立ったのでした。


word2vecそのものについては昨年来大量にブログやら何やらの記事が出回っているので、詳細な説明は割愛します。例えばPFIの海野さんのslideshare(Statistical Semantic入門 ~分布仮説からword2vecまで~)なんかは非常に分かりやすいかと思います。


要するにword2vecって何よ


Recurrent Neural Network(再帰ニューラルネットワーク)で、単語同士のつながり(というか共起関係)に基づいて単語同士の関係性をベクトル化(定量化)し、これを100次元とか200次元に圧縮して表現するもの。。。みたいです(汗)*1

※以下のようにご指摘のコメントをいただきました。

id:lufetch

word2vecで実装されている言語モデル再帰ニューラルネットワークではない普通のニューラルネットワーク(というかロジスティック回帰)ですよ。
http://arxiv.org/pdf/1301.3781.pdf
学習が高速なうえ精度もニューラルネットワークと同じぐらいだったのでロジスティック回帰が使われているようです。

ということで、皆様ご注意を。。。

f:id:TJO:20140619150536p:plain

単語の特徴どころかその意味構造までもがベクトル化されているので、意味の足し算や引き算すら可能。有名な例では"king" + "woman" - "man" = "queen"になるとか、はたまた("good" + "best") / 2 = "better"とか。こうして意味構造自体を定量的に扱えるというのがword2vecの最大の利点です。


ちなみにword2vecに関連するMikolovの発表、昨年僕も参加したNIPS2013にあったみたいですー。そうだったんだー知らなかったわーあっはっは(棒*2


実際にPythonで走らせてみる


ということで、今回は久しぶりにPythonを使った記事です。gensimパッケージを使いますよ、ということでオリジナルはこちらを参照のこと。

インストールするだけなら簡単です。easy_installでgensimを入れて、その後Cythonをpipで入れます。

$ easy_install -U gensim
$ pip install cython

Cythonを入れる理由は簡単で、これを入れないとword2vecが遅いままというか、入れると70倍にスピードアップするからです。ちなみにcythonはeasy_installでは入らないっぽいので要注意*3


環境が整ったら、学習データを用意します。word2vecは特に形態素解析する必要もなく、単に分かち書きしたテキストさえあれば大丈夫です*4。ということで、MeCabを使います。MeCab-Pythonバインディングの入れ方の記事はそこら中に転がっているので、ここでは割愛します。で、text.txtを元データとして用意したら以下のような感じで分かち書きしましょう。

$ mecab -Owakati text.txt -o data.txt

ここまで来たら後は普通にPythonの対話環境のもとでダラダラやるだけ。遅ればせながら最近になって僕もIPython Notebookを使うようになりました、ということで立ち上げておきます。

$ ipython notebook

後は基本的には以下のようにやれば走ります。gensim.modelsからword2vecをインポートし、分かち書きしたテキストデータからコーパスを作り、それをRNNで学習してsize次元に圧縮してやるだけです。

from gensim.models import word2vec
data = word2vec.Text8Corpus('data.txt')
model = word2vec.Word2Vec(data, size=200)

out=model.most_similar(positive=[u'▲▲▲▲'])
for x in out:
    print x[0],x[1]

most_similarメソッドでコサイン類似度に基づく共起性ランクが出せて、こいつこそが「意味の足し引き」が可能なベクトルです。ここまで来たら後はpositive引数に足したいもの、negative引数に引きたいものを入れるだけで好き放題遊べます。


青空文庫のデータで遊んでみる


で、学習データはスクレイピングの下手な僕があちこちからよく分からない日本語コーパス的なデータを集めてくるよりはオープンなデータセットを用いた方がいいかなと思いまして、青空文庫のデータを利用することにしました。


ここでは、芥川龍之介の『羅生門』『蜘蛛の糸』『杜子春』『鼻』、夏目漱石の前後期三部作『三四郎』『それから』『門』『彼岸過迄』『行人』『こゝろ』、ロマン・ロラン『ジャン・クリストフ』(豊島与志雄訳)の3セットを用いてみました。


まず、「人間」という語に対する共起ベクトルのリストを見てみましょう。

# 芥川龍之介
out=model1.most_similar(positive=[u'人間'])
for x in out:
    print x[0],x[1]

金持 0.8602836132050.854191780090.8457005023960.8262459039690.813594639301
ども 0.7969175577160.788514018059
ませ 0.776528775692
もの 0.768471717834
なくなっ 0.768149554729

# 夏目漱石
out=model2.most_similar(positive=[u'人間'])
for x in out:
    print x[0],x[1]

人間らしく 0.760541915894
性質 0.743081927299
矛盾 0.736667990685
必要 0.7341837883
消極 0.73138743639
財産 0.727823257446
関係 0.722252130508
種類 0.721520721912
世間 0.719589948654
横着 0.71786904335

# ロマン・ロラン
out=model3.most_similar(positive=[u'人間'])
for x in out:
    print x[0],x[1]

民主 0.762652754784
自由 0.746543884277
空想 0.742990076542
人物 0.741820216179
絶対 0.737374663353
懐疑 0.73071205616
賢明 0.730664670467
民衆 0.72896873951
天才 0.72097915411
独創 0.71560716629


作家ごとの特徴が出てますね。ところで、先にも書いたようにword2vecは単語の意味同士で足し引きなどベクトルとみなした演算ができます。試しにこういうことをやってみると。。。

# 夏目漱石

out=model2.most_similar(positive=[u'人生',u'結婚'])
for x in out:
    print x[0],x[1]
意見 0.856792092323
信念 0.819839835167
同情 0.816481769085
希望 0.8052805066110.80394500494
親類 0.79625415802
変化 0.78347492218
人格 0.780082583427
自身 0.777028143406
学問 0.76985013485

out=model2.most_similar(positive=[u'人生'],negative=[u'結婚'])
for x in out:
    print x[0],x[1]

状況 0.5923614501950.590470671654
作り 0.5881102085110.574778556824
倒し 0.570192337036
近づく 0.558950543404
特殊 0.555250644684
髪の毛 0.553275585175
わり 0.5469831228260.545182168484
自身 0.777028143406
学問 0.76985013485


人生に結婚を足すと漱石は何やら難しくなるようですが、人生から結婚を引くとコサイン類似度の低いものだらけで何も残ってない気もしますw


夏目漱石ロマン・ロランのデータは割と語彙が豊富なので、ちょっとモデル推定のパラメータを変えてもう一度試してみます。min_count(出現頻度の低いものをカットする)とか、window(前後の単語を拾う際の窓の広さを決める)あたりをいじると良さそうです。

# 夏目漱石
model2_1=word2vec.Word2Vec(data2,size=200,min_count=10,window=10)
out=model2_1.most_similar(positive=[u'人生',u'結婚'])
for x in out:
    print x[0],x[1]

主張 0.818065524101
意見 0.804816007614
尊敬 0.795175909996
反対 0.790998935699
責任 0.783528029919
事情 0.77575802803
研究 0.767453432083
社会 0.763743638992
自白 0.763557672501
当人 0.762744665146

out=model2_1.most_similar(positive=[u'人生'],negative=[u'結婚'])
for x in out:
    print x[0],x[1]

夜中 0.716477274895
用談 0.706492185593
詰め 0.692175507545
所々 0.686876595020.679705262184
つきあい 0.678078770638
次第 0.676117956638
麦酒 0.6636809110640.663213014603
刺激 0.662095069885

# ロマン・ロラン
model3_1=word2vec.Word2Vec(data3,size=200,min_count=20,window=15)
out=model3_1.most_similar(positive=[u'人生'])
for x in out:
    print x[0],x[1]
天才 0.690837740898
真理 0.685137271881
意志 0.681576728821
絶対 0.66373282671
行為 0.662181377411
欲する 0.651502549648
神聖 0.644471049309
罪悪 0.640765547752
自然 0.63612562418
犠牲 0.632806062698


色々微妙に変わった感がありますね。ちなみにうちの現場のNLPer氏に言わせると「window変えただけでだいぶ変わる」らしいので、その辺を頑張ってチューニングした方が良いかもしれません。。。


そうそう、肝心の実ビジネスでの使い方について。要は「単語単位で何かしら取り出したいんだけど、意味的関係を踏まえた上で取り出したい場合」に使えるってことですね。実はそういうケースって限定的ながら確実に存在するものなんだけど、ここではさすがに書けないので興味のある人は勉強会やセミナーの席上などで僕を捕まえて聞いてください、ってことでw

*1:これはうちの現場のNLPerに質問したりして得た僕の理解なので合ってる自信皆無です(滝汗)

*2:ってかこんなホットなテーマしれっとポスターに紛れ込んでるんじゃねぇよボケ

*3:GitHubから持ってくるケースではpipでないと言うこと聞かないっぽいです

*4:この点英語だと猛烈に楽なんだけど。。。