この前はtf-idfとwikipedia仕込みのword2vecを組み合わせる事で、文書ベクトルを3次元空間にマッピングして可視化しました。
今回は単語の加減算を使用して、概念を使用した検索機能を作ってみたいと思います。
word2vecおさらい
理屈については過去に書いているのでそちらをご参照。
word2vecの大きな特徴として、概念の足し算・引き算が可能だという点があげられます。
例えばこんな感じです。
(出典:https://www.aclweb.org/anthology/N13-1090)
分散表現になっていれば、中身はただの固定長のベクトルなので、ベクトルの足し算引き算が可能です。 各単語ベクトルが単語の意味を反映したベクトルになっているので、ベクトルの足し算引き算がそのまま概念の足し算引き算になっているということになります。
gensimで実際にやってみるとこんな感じです。
まあ、女王っぽいのが上に出ているんで、期待通りにはなっているんでしょう。
livedoor ニュースコーパスへ適用
前回やった文書の分散表現でも、中身はword2vecの足し算で文書を表現しています。 ということは、検索したい概念の総和のベクトルを用意してやれば、概念的に近い座標を指し示すことになります。 そして、その近傍にある点(文書)を調べれば、関係する文書がヒットすることが期待できます。
ということでやってみました。
手順
流れは大体こんな感じを想定しています。
- 学習済みword2vecを読み込み
- scikit-learnを使用してtf-idf値を算出
- tf-idf * embeddingで文書ベクトルを算出
- 検索
- 検索ワードの総和計算・正規化して、検索語から作成されるベクトルを取得
- NGTを使用して近傍の文書を表示(※)
※ : pureなgensimでは学習済みモデルを使用し、かつ文書だけをヒットさせる検索メソッドが見当たらなかったので、NGTを使用します。 もっと簡単な方法があったら教えてほしいです。
実装
全体感としてはこんな感じになるのかなと思います。
環境構築
まずはNGTのインストールをしてみます。
ubuntu 18.04のdockerでインストールしようとしたんですが、何故かpipではインストールがうまく行かなかったので、 泣く泣く普通にインストールしました。。。
事象としてはインストールはうまく言ったんですが、下記でいきなりエラーが出ました。
from ngt import base as ngt
まあ、zipファイルダウンロードしてcmakeからビルドしたらインストールできましたし、docker imageは公開しましたので、使いたい方はどうぞ。
ディレクトリ階層はこんな感じにしてます。
gensimの学習済みモデルとsudachiの辞書等はよしなにダウンロードして配置してください。
下準備
ElasticSearchにlivedoor ニュースコーパスをインデックスしていきます。
$ python3 register.py
一応こんな感じで確認できます。
$ python3 register.py ... 省略 ... movie-enter-6568559.txt movie-enter-6332419.txt movie-enter-6316975.txt movie-enter-6477901.txt movie-enter-5966725.txt
文書ベクトルの算出
gensimとscikit-learnを使用して文書ベクトルを出すところまでは、基本的に前回と変わらないです。
形態素解析はこんな感じです。
実際の学習はこんな感じですかね。
中身の確認
中身の確認をしてみます。 Embedding Projectorって実はオリジナルのファイルも表示できるんですね。
こんな感じになりました。
ローカルで動かす意味なくなってきました。。。 前回までの苦労はなんだったのか。。。
近傍検索
さて、新しいのはここからで、単語ベクトルの足し算引き算を考えていきます。
まずは、yahooさんの記事に書いてあるとおりに実行してみます。
普通に動きますね。
中身をちょっと見ていくと、ngtでは高次元のベクトルを登録し、それにインデックスを貼って保持するようですね。 これがngtのエンティティベクトルに該当しているようです。
上のコードではインデックス登録したものを外部出力して保存しています。 ちなみにこんな感じのが生成されます。
w2vIndex/ ├── grp ├── obj ├── prf └── tre
ベクトル計算によって検索を行う際には、このインデックスを再度読み込んで使用するようです。
実際のベクトルの加減算はこの辺でやっていますね。
vector = word_vectors[u'[ヤマハ]'] normalized_vector = vector / numpy.linalg.norm(vector)
今回はこの計算を応用して、単語ではなく近傍にある文書を探索します。
まず、gensimで読み込んでるembedding vectorを単語ベクトル(A)と文書ベクトル(B)の2種類、NGTで文書ベクトル(C)を読み込みます。 そしたら、gensimの単語ベクトル(A)だけを使用して、適当に正規化したベクトルを計算してあげます。 算出されたベクトルをNGT(C)に問い合わせて近い文書のインデックスを受け取ります。 そのインデックスをgensimの文書ベクトル(B)に問い合わせることで、似た文書を検索します。
ややこしいですが、イメージはこんな感じ。
書いてみるとこんな感じです。
検索はこんな感じでしょうか?
$ python3 search.py > result $ cat result [Unnamed: 0 topic-news 0 topic-news-5955656.txt Name: 2789, dtype: object, Unnamed: 0 it-life-hack 0 it-life-hack-6480343.txt Name: 6151, dtype: object, Unnamed: 0 it-life-hack 0 it-life-hack-6473542.txt Name: 4785, dtype: object, Unnamed: 0 dokujo-tsushin 0 dokujo-tsushin-5745435.txt Name: 2996, dtype: object, Unnamed: 0 topic-news 0 topic-news-6288304.txt Name: 2832, dtype: object, Unnamed: 0 kaden-channel 0 kaden-channel-6066216.txt Name: 6273, dtype: object, Unnamed: 0 peachy 0 peachy-4596063.txt Name: 3803, dtype: object, Unnamed: 0 kaden-channel 0 kaden-channel-6754072.txt Name: 411, dtype: object, Unnamed: 0 kaden-channel 0 kaden-channel-6128294.txt Name: 3455, dtype: object, Unnamed: 0 peachy 0 peachy-4352879.txt Name: 853, dtype: object, Unnamed: 0 kaden-channel 0 kaden-channel-6679992.txt Name: 4974, dtype: object]
良し悪しはわかりませんが、とりあえずなんか出ましたね。
(追記)中身見てみましたが、結構頓珍漢ですね。ベクトルの計算方法が雑すぎるのか、主観的に見て結構違う文書にヒットしてしまいました。この辺の精度については今度ちゃんと直したいと思います。
作ったもの
こちらに置いときました。
感想
今回はEmbedding空間を使用した、近傍探索による文書検索をやってみました。 普通の全文検索エンジンとは若干違うかと思いますし、相関関係も可視化できるので面白いですね。 というか、ElasticSearchを使いたいがために無理やり使いましたが、この使いかたに意味ないっすね。 集計処理とかしないならRDBを使うメリットもないですが、単にドキュメントを一箇所に集めるだけならElasticSearchすらいらないかもしれないですね。
この手の分析系のデータベースって普通何が使われるんでしょうね?今度時間あるときに調べてみたいと思います。
あと、機械学習とは関係ないんですが、nlp関係でもMacbookだと性能的に厳しくなってきました。 あんまりイイやつ使ってないというのもありますが、趣味でやるにはMacBookだとそろそろしんどいのかもしれないですね。。。