CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: gensim で学習済み単語ベクトル表現を扱ってみる

Python で自然言語処理を扱うためのパッケージのひとつに gensim がある。 今回は、gensim で学習済み単語ベクトル表現 (Pre-trained Word Vectors) を使った Word Embedding を試してみた。 Word Embedding というのは単語 (Word) をベクトル表現の特徴量にする手法のこと。 ごく単純な One-Hot Encoding から、ニューラルネットワークの学習時の重みを元にしたものまで様々な手法が知られている。

今回は、その中でも Facebook の公開している fastText と呼ばれる学習済み単語ベクトル表現を使うことにした。 学習に使われているコーパスは Web クローラのデータと Wikipedia があるようだけど、とりあえずクローラの方にしてみる。 詳細については以下を参照のこと。

fasttext.cc

使った環境は次のとおり。

$ sw_vers           
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G3020
$ python -V             
Python 3.7.7

下準備

下準備として、まずは公開されている学習済み単語ベクトル表現をダウンロードしておく。 次の URL からは Web クローラで収集した日本語コーパスを使って、単語を 300 次元のベクトルに埋め込むためのモデルが得られる。 このファイルは 1GB ほどあるので割と時間がかかる。

$ wget https://dl.fbaipublicfiles.com/fasttext/vectors-crawl/cc.ja.300.vec.gz

なお、fastText の Python パッケージを使って、次のようにしても良い。 この機能は今のところリポジトリの HEAD にしか入っていないけど、次のリリースでは含まれるはず。

$ pip install -U git+https://github.com/facebookresearch/fastText.git
$ python -c "from fasttext import util; util.download_model('ja')"

そして、gensim パッケージをインストールしておく。

$ pip install gensim

インストールできたら、Python の REPL を起動しておく。

$ python

gensim から fastText の学習済み単語ベクトル表現を使う

はじめに gensim をインポートする。

>>> import gensim

先ほどダウンロードしたファイルを gensim で読み込む。 これで単語をベクトル表現に変換するためのモデルができる。 なお、この操作にはかなり時間がかかるので気長に待つ必要がある。 だいたい 10 分くらい 1

>>> model = gensim.models.KeyedVectors.load_word2vec_format('cc.ja.300.vec.gz', binary=False)
>>> model
<gensim.models.keyedvectors.Word2VecKeyedVectors object at 0x127797310>

このモデルのざっくりした使い方は以下のドキュメントに記載されている。 ただ、ソースコードと見比べていくとイマイチ更新が追いついていない雰囲気も伺える。

radimrehurek.com

モデルの読み込みが終わったら、早速単語をベクトル表現にしてみよう。 試しに「猫」という単語をベクトル表現にすると、次のようになる。 モデルには辞書ライクなインターフェースがあるので、ブラケットを使ってアクセスする。

>>> model['猫']
array([-2.618e-01, -7.520e-02, -1.930e-02,  2.088e-01, -3.005e-01,
        1.936e-01, -1.561e-01, -3.540e-02,  1.220e-01,  2.718e-01,
        7.460e-02,  1.356e-01,  2.299e-01,  1.851e-01, -2.684e-01,
...(snip)...
       -9.260e-02, -8.890e-02,  1.143e-01, -3.381e-01, -1.913e-01,
        8.160e-02, -4.420e-02, -2.405e-01, -2.170e-02, -1.062e-01,
        3.230e-02, -2.380e-02,  1.860e-02, -2.750e-02, -1.900e-01],
      dtype=float32)

確認すると、この単語ベクトルはちゃんと 300 次元ある。

>>> model['猫'].shape
(300,)

学習した単語のボキャブラリーには vocab という名前でアクセスできる。 このモデルでは 200 万の単語を学習しているようだ。

>>> list(model.vocab.keys())[:10]
['の', '、', '。', 'に', 'は', 'が', 'を', 'て', 'た', 'で']
>>> len(model.vocab.keys())
2000000

most_similar() メソッドを使うと、特定の単語に似ている単語が得られる。

>>> from pprint import pprint
>>> pprint(model.most_similar('猫', topn=10))
[('ネコ', 0.8059155941009521),
 ('ねこ', 0.7272598147392273),
 ('子猫', 0.720253586769104),
 ('仔猫', 0.7062687873840332),
 ('ニャンコ', 0.7058036923408508),
 ('野良猫', 0.7030349969863892),
 ('犬', 0.6505385041236877),
 ('ミケ', 0.6356303691864014),
 ('野良ねこ', 0.6340526342391968),
 ('飼猫', 0.6265145540237427)]

単語の類似度は similarity() メソッドで得られる。 たとえば、「猫」と「犬」の方が「猫」と「人」よりも単語としては似ているようだ。

>>> model.similarity('猫', '犬')
0.65053856
>>> model.similarity('猫', '人')
0.23371725

ところで、Word2vec なんかだと、まるで単語の意味を理解しているかのように単語ベクトル間の足し引きができるとかよく言われている。 このモデルではどんなもんだろうか。

ありがちな例として「王様」から「男」を抜いて「女」を足してみよう。 女王になるみたいな説明、よく見る。

>>> new_vec = model['王様'] - model['男'] + model['女']

similar_by_vector() メソッドを使って得られたベクトルに似ている単語を確認してみる。

>>> pprint(model.similar_by_vector(new_vec))
[('王様', 0.8916897773742676),
 ('女王', 0.527921199798584),
 ('ラジオキッズ', 0.5255386829376221),
 ('王さま', 0.5226017236709595),
 ('王妃', 0.5000214576721191),
 ('裸', 0.487439900636673),
 ('タプチム', 0.4832267761230469),
 ('アンナ・レオノーウェンズ', 0.4807651937007904),
 ('ゲムケン', 0.48058977723121643),
 ('お姫様', 0.4792743921279907)]

最も似ている単語は変換前の単語ということになってしまった。 変換前の単語は取り除くことにするのかな? だとしたら、次に「女王」が得られているけど。

そもそも元の単語ベクトルに類似した単語は次のとおり。

>>> pprint(model.similar_by_word('王様'))
[('王さま', 0.5732064247131348),
 ('ラジオキッズ', 0.5712020993232727),
 ('女王', 0.5381238460540771),
 ('ゲムケン', 0.5354433059692383),
 ('裸', 0.5288593769073486),
 ('ブランチ', 0.5283758640289307),
 ('王', 0.5277552604675293),
 ('タプチム', 0.5203500986099243),
 ('乃女', 0.5120265483856201),
 ('王妃', 0.5055921077728271)]

学習済み単語ベクトル表現の読み込みが遅い問題について

一般的に、公開されている学習済み単語ベクトル表現は、特定の言語や環境に依存しないフォーマットになっているようだ。 そのため、gensim に限らず読み込む際には結構重たいパース処理が必要になっている。 この点は、一般的な機械学習において CSV などのデータを読み込むときのテクニックを応用すれば高速化が見込めそうだ。

具体的には Python であれば Pickle を使って直列化・非直列化する。 まずは pickle パッケージを読み込む。

>>> import pickle

ファイル名を指定してモデルをストレージに直列化する。

>>> with open('gensim-kvecs.cc.ja.300.vec.pkl', mode='wb') as fp:
...     pickle.dump(model, fp)
... 

直列化したら、あとは使いたいタイミングで非直列化するだけ。 ストレージのスループット次第だけど、それでも 10 分待たされるようなことはないはず。

>>> with open('gensim-kvecs.cc.ja.300.vec.pkl', mode='rb') as fp:
...     model = pickle.load(fp)
... 

ただし、直列化されない内部的なキャッシュもあるようで一発目の呼び出しではちょっとモタつく。 とはいえ、もちろん 10 分とか待たされるようなことはない。

いじょう。


  1. 今の実装には明確な処理のボトルネックがあって、それを解消すると 1/3 くらいの時間に短縮できる。