単語ベクトル化モデルの一つであるGloVeを試してみます。
GloVe
GloVeは単語のベクトル表現を得る手法の一つで、Word2Vecの後発となります。論文はこちらです。
Word2Vec (skip-gram with negative sampling: SGNS) では各単語から周辺単語を予測するというタスクをニューラルネットワークで解くことによって単語ベクトルを得ますが、GloVeではコーパス全体から得られる単語間の共起行列を持ち込んだ最適化関数 (重み付き最小二乗法) で学習します。
- 単語iと単語jの共起行列が
は、次元削減された単語ベクトル (factorization matrix)
は、それぞれ
のバイアス
- 関数
は重みをつけるためのもので、コーパス全体で頻出するワード (助詞や指示語など) の重みを低くします
理論面はきちんと勉強できていませんので、改めて整理します。
GloVeでベクトル化する
それでは実際にGloVeの実装を使って、単語をベクトル化していきます。
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)]