fastTextのベクトルの類似検索でYAHOOのNGTを使う。
fasttextにWikipedia全文を食べさせて遊んでいた(だいたい以下の記事の内容)。
「パリ - フランス + 日本」とかそういうやつ(だいたい以下の記事の内容)。
1 0.044094 -0.020766 -0.043709 -0.28123 0.13417 0.1673 0.05862 0.22077 -0.12497 0.077398 -0.022736 ... 2 0.1059 -0.050905 -0.10932 -0.22583 0.15278 0.15993 0.034985 0.12569 -0.10522 0.09798 -0.026379 ... 3 0.010506 -0.23203 -0.083458 -0.26819 0.14723 0.2554 -0.032926 0.029112 -0.24196 0.063793 ... ...
↑
fasttextで学習させるとこういうファイルが吐かれる。
最初の整数が単語に割り振ったトークンでそれからfloatが次元数分、ここでは300個並んでいる。
それが約92万行。学習はCPUで2時間くらいで終わる。
92万もあるとファイルサイズが2GBくらいになるのでパソコンでは全力で並列化とかしても読み込みに1分くらいかかってしまう。
さらに、対象のベクトルと全件の類似度を計算するみたいなことをやるのでパソコンでは全力で並列化しても1分くらいかかってしまう。
これだと気軽に使えないし、例によってslack botにするなどしたいのでさくらのVPSの安いやつでも数秒で計算が終わって欲しい。
たぶんアプローチとしては、読み込みも類似計算もこのベクトル空間内で近しいものだけ行うようにインデックス化すればいいんだと思うけど
それは大変だなーとか思ってググっているとすごいツールがあった。
NGT(Neighborhood Graph and Tree)
簡単にいうとfastextが吐いたような1行に1ベクトルみたいなファイルをインデックス化して、それを使って高速に空間内の距離の検索をしてくれるコマンドラインツール。
インストールは書いてある通りにやれば簡単に出来ると思う。 注意点としては
- ubuntu, centosなどのLinuxでは動いたけどMacではインストールはできてもコマンド引数が認識されないみたいなエラーが出て動かなかった。
- githubから最新版をクローンしてくると動かなかったのでおとなしくタグが打ってあるやつを使うといい。
前処理
最初にNGTにベクトル放り込んでインデックス化してもらう。
そのためにfasttextが吐いたやつの下処理をする。
import numpy as np def sorted_vectors(vec_path): vec_file = open(vec_path) # 最初の行はヘッダなので捨てる vec_file.readline() rows = [] for token, vec in (row.strip().split(maxsplit=1) for row in vec_file): # なぜかトークンではないものが混じることがある。 try: rows.append((int(token), vec)) except ValueError: continue # 行数とトークンが揃っててほしいので、トークンでソートする。 rows.sort(key=lambda x: x[0]) # 1からの連番になることを確認 for idx, row in enumerate(rows, 1): assert idx == row[0] yield from (row[1] for row in rows) def prepare_for_ngt(vec_path, dist_vec): dist = open(dist_vec, 'w') for row in sorted_vectors(vec_path): vec = np.array(row.split(), dtype='float32') # ノルムで割ってベクトルを正規化。 vec /= np.linalg.norm(vec) row = list(vec) row = ' '.join(map(str, row)) dist.write('{}\n'.format(row))
正規化をしているのは結果をコサイン類似度と揃えたかったから。 techblog.yahoo.co.jp
NGTは距離しか扱えないため、コサイン類似度と同じ検索結果を得るために事前にベクトルのノルムが1となるように正規化してユークリッド距離を使う方法か、
距離関数はcreate
の引数で選択できる。
デフォルトだとユークリッド距離。
これにfasttextが吐いたファイルを与えると1行に1つのベクトルで、トークンでソートされているファイルが出来上がる。
prepare_for_ngt('model.vec', 'ngt_model.vec')
そして以下のコマンドを叩く。
-d
は次元数,index
は出力されるディレクトリ名。
$ ngt create -d 300 index ngt_model.vec
詳しいオプションは→https://research-lab.yahoo.co.jp/software/ngt/command.html#create
この所用時間はコア数に応じて数十分~1時間くらい。
もちろん出力されたものは使いまわせる。
検索
$ ngt search index target.vec
taget.vec
は検索したいベクトルのファイルで、形式はcreate
に使ったファイルと同じ。
Query No.1 Rank ID Distance 1 野球 0 2 ソフトボール 0.682129 3 プロ野球 0.708267 4 サッカー 0.739252 5 軟式野球 0.754248 6 少年野球 0.755179 7 バスケットボール 0.765375 8 高校野球 0.788223 9 リトルリーグ 0.788254 10 アマチュア野球 0.794894 11 アメリカンフットボール 0.799319 12 ホッケー 0.805228 13 ベースボール 0.822246 14 ラグビー 0.822764 15 アメフト 0.823869 16 硬式野球 0.832693 17 バスケ 0.834052 18 大学野球 0.835599 19 卓球 0.835896 20 投手 0.836089
結果がこういう形で表示される(本当に正しいかわかりづらかったのでトークンを単語に変換している)。
indexに放り込んだベクトルから近い順に返ってくる。
さくらのVPSの下から2番目のやつでも3秒くらいで返ってくる。
まとめ
このツールは、界隈では自分が紹介するまでもなく有名で、かつ使われているのかもしれないけれどリリースが結構前な割には(商用利用出来るようになったのは去年だけど)そんなに情報が多くなかったので、ただ使ってみただけだけれども書いてみた。
多次元ベクトルのインデックス化は明らかに僕のできる範囲を超えているので感動してしまった。ごちそうさまです。
あと一応試せるように使用したモデルデータを置いておきます。
これは冒頭のqiitaの記事の過程で得られるものだけど。
model.vec.gz - Google ドライブ
wiki__all.vocab.gz - Google ドライブ
おわり