GloVeで単語ベクトルを得る

単語ベクトル化モデルの一つであるGloVeを試してみます。

GloVe

GloVeは単語のベクトル表現を得る手法の一つで、Word2Vecの後発となります。論文はこちらです。

nlp.stanford.edu

Word2Vec (skip-gram with negative sampling: SGNS) では各単語から周辺単語を予測するというタスクをニューラルネットワークで解くことによって単語ベクトルを得ますが、GloVeではコーパス全体から得られる単語間の共起行列を持ち込んだ最適化関数 (重み付き最小二乗法) で学習します。

  • 単語iと単語jの共起行列が  logX_{ij}
  •  w_i^T\tilde{w_j} は、次元削減された単語ベクトル (factorization matrix)
  •  b_i,\tilde{b_j} は、それぞれ  w_i, \tilde{w_j} のバイアス
  • 関数  f は重みをつけるためのもので、コーパス全体で頻出するワード (助詞や指示語など) の重みを低くします


J=\sum_{i,j=1}^V f(X_{ij})(w_i^T\tilde{w_j} + b_i + \tilde{b_j} - logX_{ij})^2

理論面はきちんと勉強できていませんので、改めて整理します。

GloVeでベクトル化する

それでは実際にGloVeの実装を使って、単語をベクトル化していきます。

github.com

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

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

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

コーパスの作成

最初にコーパスを作ります。日本語へ適用した場合の性質を見てみたいので、ここではサイズが小さくてかつ青空文庫でダウンロードできるデカルトの省察からコーパスを作ります。

import re
import urllib.request
import zipfile

# zipファイルのダウンロード
urllib.request.urlretrieve('https://www.aozora.gr.jp/cards/001029/files/43291_ruby_20925.zip', '43291_ruby_20925.zip')
with zipfile.ZipFile('./43291_ruby_20925.zip') as zip_file:
    zip_file.extractall('.')
    
# テキストファイルから文字列を抽出
with open('./meditationes.txt', 'r', encoding='shift_jis') as f:
    text = f.read()

# 本文を抽出 (ヘッダとフッタを削除)
text = ''.join(text.split('\n')[23:346]).replace(' ', '')
# 注釈 ([]) とルビ (《》) を削除
text = re.sub(r'([.*?])|(《.*?》)', '', text)
# 神の存在、及び人間の霊魂と肉体との区別を論証する、第一哲学についての省察 ...

さらにJanomeで形態素解析することで、単語を抽出します。名詞・動詞・副詞・形容詞の原型に限定しました。

# pip install Janome
from janome.analyzer import Analyzer
from janome.charfilter import UnicodeNormalizeCharFilter
from janome.tokenizer import Tokenizer
from janome.tokenfilter import POSKeepFilter, ExtractAttributeFilter

analyzer = Analyzer(
    [UnicodeNormalizeCharFilter()],
    Tokenizer(),
    [POSKeepFilter(['名詞', '動詞', '形容詞', '副詞']), ExtractAttributeFilter('base_form')]
)

tokens = [token for token in analyzer.analyze(text)]
# ['神', '存在', '人間', '霊魂', '肉体', ... ]

単語ごとに' 'で区切った1行の文書として./input.txtに保存します。これをコーパスとして使います。

with open('./input.txt', 'w') as f:
    f.write(' '.join(tokens))

GloVeでベクトル化

先程得られた実行ファイルを使って、コーパスから単語ベクトルを作ります。

必要なのは4つのコマンドです。

  • vocab_countで辞書を生成 (vocab.txt)
    • -min-countで、出現頻度の低い単語を足切り (ここでは2未満の単語はベクトル化しない)
  • cooccurで共起行列を生成 (cooccurrence)
    • -window-sizeで、いくつの周辺単語の共起をカウントするか指定する (ここでは前後15単語)
  • shuffle (cooccurrence_shuffleを出力、論文内でも記述が無く、おそらく実装都合のもの)
  • gloveでベクトル化 (vectors.txt, vectors.bin)
    • -vector-sizeで、ベクトルの次元数を指定
    • -iterで、イテレーション回数を指定
$ ./glove/build/vocab_count -min-count 2 -verbose 2 < input.txt > vocab.txt
$ ./glove/build/cooccur -memory 4 -vocab-file vocab.txt -verbose 2 -window-size 15 < input.txt > cooccurrence
$ ./glove/build/shuffle -memory 4 -verbose 2 < cooccurrence.txt > cooccurrence_shuffle
$ ./glove/build/glove -save-file vectors -threads 2 -input-file cooccurrence_shuffle -x-max 10 -iter 10 -vector-size 50 -binary 2 -vocab-file vocab.txt -verbose 2

vectors.txtは以下のように、1列目が単語ラベル、2列目以降がベクトルとなっています。

する 0.821036 -0.767963 0.173905 -1.520309 0.974474 0.516971 -0.905228 0.454891 0.759307 0.265375 -0.075784 0.535552 0.288605 -1.316993 -0.012559 -1.067366 -0.840541 0.042517 1.565274 -0.208433 -1.157825 1.280726 -0.874233 0.239113 -0.047998 0.300433 -0.785599 0.228769 -0.067241 -1.164181 -1.075916 -0.410294 -0.525001 0.309162 -0.522551 -1.094749 0.352643 0.157778 -0.395058 0.489368 0.144088 -1.184835 0.090272 0.048608 1.166587 -1.310016 -0.179411 0.451254 0.048985 0.206744
私 1.077732 -0.197421 0.158596 -1.215650 0.603018 -0.416900 -0.240335 -0.229747 0.497587 0.726050 -0.685833 0.304997 0.025257 -1.270019 0.117946 -0.820217 -1.074720 -0.251358 1.141868 -0.266699 -1.171293 0.236756 -0.899527 0.295187 -0.390448 0.432460 0.517610 0.523386 -0.012453 -0.166734 -1.654589 0.189975 -0.705832 0.434905 0.071865 -0.490482 0.746462 0.633038 -0.828486 0.306500 -0.047694 -0.760142 -0.170331 0.489952 0.957421 -0.903183 -0.070720 0.450352 0.165593 0.084121
こと 0.803995 -0.283790 -0.806350 -1.189181 0.367095 0.138987 0.039391 -0.141035 0.746949 0.676415 -0.684094 0.234107 0.723286 -0.853208 -0.421323 -0.547982 -0.630406 0.520100 1.511528 0.477472 -1.140159 0.218199 -0.874279 -0.031568 0.008137 0.625755 0.165821 0.212576 -0.022433 -0.677801 -1.592565 -0.456095 -0.199207 0.648881 0.391887 -0.248479 0.334491 0.716378 -0.922525 -0.100026 0.312525 -0.417347 -0.103911 0.243988 0.601623 -0.585152 -0.542268 0.113903 0.301077 -0.147555
...

ベクトルを読み込む

作られたベクトルファイル vectors.txt をpandas/numpyで読み込みます。

import pandas as pd
import numpy as np

# 単語ラベルをインデックスにしてDataFrameで読み込む
vectors = pd.read_csv('./vectors.txt', delimiter=' ', index_col=0, header=None)

vector = vectors.loc['人間',:].values
# array([ 0.118479, -0.161301, -0.11967 , -0.386339, -0.151804,  0.092074,
#         0.027781,  0.07296 ,  0.114962,  0.184768, -0.151259, -0.009416,
#         0.072511, -0.330517,  0.118942, -0.015616, -0.304021, -0.052112,
#         0.307647, -0.084063, -0.330387,  0.043164, -0.280033,  0.178405,
#        -0.118228,  0.144567, -0.001227,  0.122085,  0.016187, -0.025795,
#        -0.275177, -0.086059, -0.093451,  0.104058,  0.222139, -0.093288,
#         0.106345,  0.239498, -0.283105, -0.015229,  0.209854, -0.270562,
#        -0.115171, -0.011286,  0.242488, -0.124451, -0.064112,  0.008188,
#         0.125374, -0.065054])

gensimで読み込む

numpyで表現されていますのでコサイン類似度などの計算はできますが、gensimのKeyedVectorsにロードして、提供されているAPIを使って楽に取り扱えるようにします。

上で出力された vectors.txt をそのままKeyedVectorsでロードできません。1行目に単語数 次元数の記述が必要です。
そこで、1行目に単語数 次元数を追加したファイル (gensim_vectors.txt) を新たに準備します。

with open('./vectors.txt', 'r') as original, open('./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)

例えば"人間"と最もベクトルが近い単語は以下で計算できます。

glove_vectors.most_similar('人間')
# [('精神', 0.9610371589660645),
#  ('身体', 0.9233092069625854),
#  ('見る', 0.913536548614502),
#  ('最後', 0.9134587049484253),
#  ('いま', 0.9112191200256348),
#  ('言う', 0.9044301509857178),
#  ('かよう', 0.902915358543396),
#  ('かえって', 0.9026569128036499),
#  ('できる', 0.9021652936935425),
#  ('よう', 0.9006799459457397)]