こんにちは。takapy(@takapy0210)です。
コンピュータで自然言語を扱う場合は、単語(文書)を何らかの形で数値表現に変換する必要があります。
単語の分散表現を得る方法の一つとして、gensimのword2vecを使うことはよくあると思うのですが、本日はGloVeを用いて単語の分散表現を計算してみます。
GloVeのダウンロード&ビルド
READMEの手順に従ってビルドを行います。
./glove/build
に実行形式ファイルがいくつか生成されます。
$ git clone http://github.com/stanfordnlp/glove
$ cd glove && make
使用するcorpus
今回は日本語の検証したいため、食べログから取得した新宿のラーメン屋の口コミデータを用いることにします。
Mecabを用いて口コミを形態素解析し単語を抽出します。今回は名詞・動詞・形容詞の原型に限定しました。
例えば
今日のランチは、凄い行列のステーキ屋さんの横にあったこちらのお店セルフオーダー制ということで、わたくしにはバッチリな注文環境調子にのって王道のダイエットは明日から(笑)ラーメン半チャーハン餃子替え玉(笑)かたさはばりかたで餃子は普通ですチャーハンは油っぽくて、ジャンキーな嫌いでない味ラーメンは濃くが足りなく物足りない印象でした。もっと濃くがあるのが好み、高菜がなかったのもものたりない。次は考えます
という口コミは、下記のようになります。
今日 ランチ 凄い 行列 ステーキ屋 さん 横 ある こちら 店 セルフ オーダー 制 こと わたくし 注文 環境 調子 のる 王道 ダイエットは明日から 笑 ラーメン チャーハン 餃子 替え玉 笑 かた 左派 ばり かた 餃子 普通 チャーハン 油 っぽい Junky 嫌い 味 ラーメン 濃い 足りる 物足りない 印象 濃い ある の 好む 高菜 ない の ものたりない 次 考える
スクレイピングのやり方は下記を参考にしていただければと思います。
上記のような文書を、改行で結合してcorpus.txt
というファイルで保存します。
ファイル構成は下記のようにしておきます。
Gloveで計算
冒頭でダウンロードした実行ファイルを使ってコーパスから単語ベクトルを生成します。
必要なのは下記4つのコマンドです。
vocab_countで辞書を生成 (vocab.txt)
- min-countで、出現頻度の低い単語を足切り (ここでは5未満の単語はベクトル化しない)
cooccurで共起行列を生成 (cooccurrence)
- window-sizeで、いくつの周辺単語の共起をカウントするか指定する (ここでは前後5単語)
shuffle (cooccurrence_shuffleを出力、論文内でも記述が無く、おそらく実装都合のもの)
gloveでベクトル化 (vectors.txt, vectors.bin)
- vector-sizeで、ベクトルの次元数を指定
- iterで、イテレーション回数を指定
実行例
$ ls
corpus.txt glove
$ ./glove/build/vocab_count -min-count 5 -verbose 1 < corpus.txt > vocab.txt BUILDING VOCABULARY Truncating vocabulary at min count 5. Using vocabulary of size 12794.
$ ./glove/build/cooccur -memory 4 -vocab-file vocab.txt -verbose 1 -window-size 5 < corpus.txt > cooccurrence COUNTING COOCCURRENCES window size: 5 context: symmetric Merging cooccurrence files: processed 3859102 lines.
$ ./glove/build/shuffle -memory 4 -verbose 1 < cooccurrence > cooccurrence_shuffle SHUFFLING COOCCURRENCES array size: 255013683 Merging temp files: processed 3859102 lines.
$ ./glove/build/glove -save-file vectors -threads 2 -input-file cooccurrence_shuffle -x-max 10 -iter 5 -vector-size 100 -binary 2 -vocab-file vocab.txt -verbose 1 TRAINING MODEL Read 3859102 lines. vector size: 100 vocab size: 12794 x_max: 10.000000 alpha: 0.750000 06/23/19 - 10:23.56PM, iter: 001, cost: 0.095663 06/23/19 - 10:23.57PM, iter: 002, cost: 0.070735 06/23/19 - 10:23.59PM, iter: 003, cost: 0.061182 06/23/19 - 10:24.00PM, iter: 004, cost: 0.053757 06/23/19 - 10:24.02PM, iter: 005, cost: 0.047510
ここまで実行することで、下記ファイルが生成されます。
- vocab.txt(辞書)
- cooccurrence
- cooccurrence_shuffle
- vectors.txt
- vectors.bin
vectors.txtには、各単語に紐づく分散表現が格納されています。
ラーメン 0.673250 -0.513771 -0.475880 -0.096685 0.264802 -0.272522 0.005046 -0.775419 -0.786791 0.528882 1.336813 1.003489 0.267365 0.986350 -0.342269 0.888036 0.428048 -0.791635 -0.461346 0.828478 -0.488368 -0.925032 1.028225 -1.123553 -0.331316 -0.072964 1.307135 -0.160825 0.993804 -0.058459 -1.377232 0.096985 0.577326 -0.006944 0.683655 0.436561 -0.381796 -0.829013 1.048190 0.536892 0.241616 0.514627 0.286292 0.274792 -0.016219 0.849626 0.129554 -0.520040 0.771759 0.230136 -0.877305 0.228084 -1.308605 -0.788791 0.935787 0.290390 0.203867 -1.195877 -0.607174 -0.421747 -0.336211 -0.036497 -0.194382 1.363402 -0.504111 0.747341 0.137524 -0.876247 0.007305 -0.224895 1.095122 0.702427 -0.684584 -0.499054 -1.400971 1.558607 -0.871038 1.306472 0.963653 0.081811 0.041142 0.577321 0.020814 -0.682070 -0.559259 -1.052202 -1.318521 0.276529 -0.334324 -0.945115 -0.603959 0.363780 -0.617557 -1.297032 0.092984 -0.233712 -0.612274 0.213086 -0.603416 1.217969
味 -1.464609 -0.228089 0.000087 ・・・
GloVeの分散表現を取得する
作られたベクトルファイルvectors.txt
をpandas/numpyで読み込んで、単語の分散表現を取得します。
# 単語ラベルをインデックスにしてDataFrameで読み込む vectors = pd.read_csv('../data/features/vectors.txt', delimiter=' ', index_col=0, header=None) vector = vectors.loc['ラーメン',:].values # array([ 0.67325 , -0.513771, -0.47588 , -0.096685, 0.264802, -0.272522, # 0.005046, -0.775419, -0.786791, 0.528882, 1.336813, 1.003489, # 0.267365, 0.98635 , -0.342269, 0.888036, 0.428048, -0.791635, # -0.461346, 0.828478, -0.488368, -0.925032, 1.028225, -1.123553, # -0.331316, -0.072964, 1.307135, -0.160825, 0.993804, -0.058459, # -1.377232, 0.096985, 0.577326, -0.006944, 0.683655, 0.436561, # -0.381796, -0.829013, 1.04819 , 0.536892, 0.241616, 0.514627, # 0.286292, 0.274792, -0.016219, 0.849626, 0.129554, -0.52004 , # 0.771759, 0.230136, -0.877305, 0.228084, -1.308605, -0.788791, # 0.935787, 0.29039 , 0.203867, -1.195877, -0.607174, -0.421747, # -0.336211, -0.036497, -0.194382, 1.363402, -0.504111, 0.747341, # 0.137524, -0.876247, 0.007305, -0.224895, 1.095122, 0.702427, # -0.684584, -0.499054, -1.400971, 1.558607, -0.871038, 1.306472, # 0.963653, 0.081811, 0.041142, 0.577321, 0.020814, -0.68207 , # -0.559259, -1.052202, -1.318521, 0.276529, -0.334324, -0.945115, # -0.603959, 0.36378 , -0.617557, -1.297032, 0.092984, -0.233712, # -0.612274, 0.213086, -0.603416, 1.217969])
このままでもコサイン類似度を計算することはできるのですが、慣れているgensimを利用して読み込み、類似単語を表示してみます。
gensimを用いてGloVeで計算したモデルを読み込む
gensimのKeyedVectorsにロードして、gensimで提供されているAPIを使って楽に取り扱えるようにしてみます。
しかし、上で出力されたvectors.txt
をそのままKeyedVectorsでロードすることはできません。1行目に単語数
次元数
の記述が必要です。
そこで、1行目に単語数
次元数
を追加したファイル (gensim_vectors.txt) を新たに準備します。
with open('../data/features/vectors.txt', 'r') as original, open('../data/features/gensim_vectors.txt', 'w') as transformed: vocab_count = vectors.shape[0] # 単語数 size = vectors.shape[1] # 次元数 transformed.write(f'{vocab_count} {size}\n') transformed.write(original.read()) # 2行目以降はそのまま出力
load_word2vec_formatメソッドでロードします。
from gensim.models import KeyedVectors glove_vectors = KeyedVectors.load_word2vec_format('./gensim_vectors.txt', binary=False)
あとはgensimで提供されているAPIが利用できます!
print("Vocabulary size:", len(glove_vectors.wv.vocab)) # Vocabulary size: 12795
glove_vectors.wv.most_similar(positive=['ラーメン']) # [('味噌ラーメン', 0.7279496788978577), # ('出会える', 0.6777999997138977), # ('ここ', 0.6596914529800415), # ('業界', 0.6555453538894653), # ('思う', 0.6497626304626465), # ('69点', 0.643233060836792), # ('すごい', 0.6417592763900757), # ('以外', 0.641024112701416), # ('ノーマル', 0.6402268409729004), # ('笑', 0.6401883363723755)] glove_vectors.wv.most_similar(positive=['魚介']) # [('豚骨', 0.9047324061393738), # ('動物', 0.90399569272995), # ('濃厚', 0.8746256828308105), # ('あっさり', 0.8694295883178711), # ('ベース', 0.8567452430725098), # ('ダシ', 0.8536779880523682), # ('系', 0.8491894006729126), # ('どろどろ', 0.8370369672775269), # ('ダブル', 0.823868453502655), # ('豚骨魚介', 0.8174031972885132)]
今回は比較的小さいコーパスで計算したので結果はイマイチですが、それなりに計算できています。
gensimのword2vecで計算した結果と比較する
最後にgensimのword2Vecを用いて今回と同様のコーパスを学習させた結果と比較させてみます。
word2vecのオプションは下記を設定しています。
model = word2vec.Word2Vec(data, size=100, window=5, hs=1, min_count=5, sg=1, iter=5, workers=cpu_count )
GloVe
glove_vectors.wv.most_similar(positive=['ラーメン']) # [('味噌ラーメン', 0.7279496788978577), # ('出会える', 0.6777999997138977), # ('ここ', 0.6596914529800415), # ('業界', 0.6555453538894653), # ('思う', 0.6497626304626465), # ('69点', 0.643233060836792), # ('すごい', 0.6417592763900757), # ('以外', 0.641024112701416), # ('ノーマル', 0.6402268409729004), # ('笑', 0.6401883363723755)] glove_vectors.wv.most_similar(positive=['魚介']) # [('豚骨', 0.9047324061393738), # ('動物', 0.90399569272995), # ('濃厚', 0.8746256828308105), # ('あっさり', 0.8694295883178711), # ('ベース', 0.8567452430725098), # ('ダシ', 0.8536779880523682), # ('系', 0.8491894006729126), # ('どろどろ', 0.8370369672775269), # ('ダブル', 0.823868453502655), # ('豚骨魚介', 0.8174031972885132)]
word2vec
model.wv.most_similar(positive=['ラーメン']) # [('バターコーンラーメン', 0.7198267579078674), # ('味噌ラーメン', 0.7033755779266357), # ('730', 0.661577045917511), # ('しょうゆラーメン', 0.6528247594833374), # ('560円', 0.6478167772293091), # ('つけ麺', 0.6475899815559387), # ('醤油ラーメン', 0.64621502161026), # ('のしお', 0.6430776119232178), # ('コク味', 0.641629695892334), # ('絡麺', 0.6399456262588501)] model.wv.most_similar(positive=['魚介']) # [('動物', 0.8495888710021973), # ('濃厚', 0.7789713144302368), # ('マタオマ', 0.7709181308746338), # ('豚骨魚介', 0.7476851344108582), # ('節', 0.7337124347686768), # ('シャバシャバ', 0.7289061546325684), # ('鰹', 0.7187755703926086), # ('ダシ', 0.7185323238372803), # ('豚骨', 0.7179540991783142), # ('ビター', 0.7052069902420044)]
上記だけ見るとword2vecの方が良い結果になっているように感じます。
最後に
GloVeには、カウントベースやword2vecと比べて
- 学習が速く
- 大きなコーパスに対応可能で
- 小さなコーパス、小さなベクトルでもパフォーマンスがいい
という長所もあるようなので、解決したい課題によって使い分けていきたいです。