もう既に山ほど解説記事が出回っていて、あまつさえそれを利用したwebサービスまで出てきてしまっているword2vecですが、うちの現場でも流行っているのでせっかくなので僕もやってみようと思い立ったのでした。
word2vecそのものについては昨年来大量にブログやら何やらの記事が出回っているので、詳細な説明は割愛します。例えばPFIの海野さんのslideshare(Statistical Semantic入門 ~分布仮説からword2vecまで~)なんかは非常に分かりやすいかと思います。
要するにword2vecって何よ
Recurrent Neural Network(再帰型ニューラルネットワーク)で、単語同士のつながり(というか共起関係)に基づいて単語同士の関係性をベクトル化(定量化)し、これを100次元とか200次元に圧縮して表現するもの。。。みたいです(汗)*1。
※以下のようにご指摘のコメントをいただきました。
word2vecで実装されている言語モデルは再帰型ニューラルネットワークではない普通のニューラルネットワーク(というかロジスティック回帰)ですよ。
http://arxiv.org/pdf/1301.3781.pdf
学習が高速なうえ精度もニューラルネットワークと同じぐらいだったのでロジスティック回帰が使われているようです。ということで、皆様ご注意を。。。
単語の特徴どころかその意味構造までもがベクトル化されているので、意味の足し算や引き算すら可能。有名な例では"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.860283613205 急 0.85419178009 冠 0.845700502396 唯 0.826245903969 杜 0.813594639301 ども 0.796917557716 春 0.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.805280506611 恋 0.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.592361450195 甲 0.590470671654 作り 0.588110208511 霧 0.574778556824 倒し 0.570192337036 近づく 0.558950543404 特殊 0.555250644684 髪の毛 0.553275585175 わり 0.546983122826 緑 0.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.68687659502 某 0.679705262184 つきあい 0.678078770638 次第 0.676117956638 麦酒 0.663680911064 株 0.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