ギークなエンジニアを目指す男

機械学習系の知識を蓄えようとするブログ

GloVeを使って単語の分散表現を取得する

f:id:taxa_program:20190623231314p:plain

こんにちは。takapy(@takapy0210)です。

コンピュータで自然言語を扱う場合は、単語(文書)を何らかの形で数値表現に変換する必要があります。

単語の分散表現を得る方法の一つとして、gensimのword2vecを使うことはよくあると思うのですが、本日はGloVeを用いて単語の分散表現を計算してみます。

GloVeのダウンロード&ビルド

READMEの手順に従ってビルドを行います。
./glove/buildに実行形式ファイルがいくつか生成されます。

$ git clone http://github.com/stanfordnlp/glove
$ cd glove && make

github.com

使用するcorpus

今回は日本語の検証したいため、食べログから取得した新宿のラーメン屋の口コミデータを用いることにします。

Mecabを用いて口コミを形態素解析し単語を抽出します。今回は名詞・動詞・形容詞の原型に限定しました。

例えば

今日のランチは、凄い行列のステーキ屋さんの横にあったこちらのお店セルフオーダー制ということで、わたくしにはバッチリな注文環境調子にのって王道のダイエットは明日から(笑)ラーメン半チャーハン餃子替え玉(笑)かたさはばりかたで餃子は普通ですチャーハンは油っぽくて、ジャンキーな嫌いでない味ラーメンは濃くが足りなく物足りない印象でした。もっと濃くがあるのが好み、高菜がなかったのもものたりない。次は考えます

という口コミは、下記のようになります。

今日 ランチ 凄い 行列 ステーキ屋 さん 横 ある こちら 店 セルフ オーダー 制 こと わたくし 注文 環境 調子 のる 王道 ダイエットは明日から 笑 ラーメン チャーハン 餃子 替え玉 笑 かた 左派 ばり かた 餃子 普通 チャーハン 油 っぽい Junky 嫌い 味 ラーメン 濃い 足りる 物足りない 印象 濃い ある の 好む 高菜 ない の ものたりない 次 考える

スクレイピングのやり方は下記を参考にしていただければと思います。

www.takapy.work

上記のような文書を、改行で結合してcorpus.txtというファイルで保存します。

ファイル構成は下記のようにしておきます。

f:id:taxa_program:20190623220939p:plain

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と比べて

  • 学習が速く
  • 大きなコーパスに対応可能で
  • 小さなコーパス、小さなベクトルでもパフォーマンスがいい

という長所もあるようなので、解決したい課題によって使い分けていきたいです。