はじめに
Groonga Advent Calendar 2015の11日目の記事です。
GroongaはC/C++で書かれた高速な国産の全文検索エンジンです。
word2vecは、Googleが研究評価用に作った単語の特徴をベクトルで表現しニューラルネットモデルで教師なし学習をさせるツールです。
単語を文脈も考慮させたベクトルで表現しニューラルネットで学習することで、単語の意味的な足し引きができているんじゃないか( kingからman引いてwoman足したらqueenがでてきましたよとか。)ということで少し前に結構流行ったようです。
また、word2vecの作者が簡易的にsentenceのベクトル表現を使えるようにしたword2vecのコードもあるようです。
普段、Groongaを使ったそこそこの規模のデータベースをもっているので、Groongaに格納されたデータからword2vec/sentence2vecで簡単に学習させることができ、また、Groongaから単語ベクトルの演算ができるプラグインを作成しました*1。
naoa/groonga-word2vec · GitHub
dump_to_train_fileコマンド Groongaのカラムから学習ファイル生成
Groongaのカラムからword2vecで学習できるように整形してファイルに出力します。
形態素解析やノーマライズ、記号削除などの前処理がオプション指定で行えるようになっています。
sentence_vectorsオプションを使うとdoc_id:とGroongaのテーブルの_idが紐付けられて出力されます。これにより後のベクトル演算時に元のテーブルと関連付ける事が可能です。
また、column名の後にカッコでカテゴリなど特定のカラムに格納されたタグ等に任意のラベルを付与できます。 たとえば、category:カテゴリAやcompany:会社名Aなどの形で学習させることで類似するカテゴリや会社のみを抽出することができます。
実行例
dump_to_train_file docs companies_[company:],categories_[category:],title/,body/@$ \ --filter 'year >= 2010' --sentence_vectors 1
会社名とカテゴリは、スペースを_でつなげてフレーズ化してラベルをつけ、TitleとBodyは形態素解析、記号削除などをおこなっています。また--filter
でテーブルの検索結果のみを出力することができます。
word2vec_trainコマンド word2vecコマンドを実行して学習
これはオプションを少しいじって実行パスにあるword2vecコマンドを実行するだけです。 このプラグインをインストールすると上記のsentence vector追記版のword2vecが自動的にbindirにインストールされます*2。 学習はGroongaを介して実行しなくても構いません。
word2vec_train --window 20 --cbow 0 --threads 12 --iter 10 --sentence_vectors 1
word2vec_distanceコマンド ベクトル距離を演算
word2vecで学習させたバイナリファイルを読み込み、単語ベクトルを演算します。初回のみバイナリファイル読み込んでをメモリに展開するため、少し時間がかかります。Groongaを常駐でサーバ実行している場合は一度読みこめば2度目からは素早く計算することができます。同時に複数のモデルファイルを読み込むことができます(20個まで)。
word2vecに付属しているdistanceコマンドをベースに以下を改良しています。
- スペースと+/- で単語ベクトルの演算可
- offset,limit,thresholdなどページ制御のためのオプション
- sentence_vectorの場合、元のテーブルをひも付けてカラム出力、ソート
- 高速化のため語彙表を配列ではなくPatricia Trieで保持
元々のdistance.cは語彙表から一致する単語を探すのに線形探索をしていますが、Patricia Trieを使うことによりマッチ時間の短縮が見込めます。
doc_id:や、category:など特定のラベル付けしたもののみを取得するような場合、Patricia Trieで非常に高速な前方一致検索が可能です。
以下の例では、2sec近くかかっていたのが0.01sec以下で類似カテゴリが取得可能となっています。
- 改良前(全ワード+正規表現"company:.*"で絞込)
> word2vec_distance "company:apple" --is_phrase 1 --limit 5 --offset 0 \ --n_sort 5 --white_term_filter "company:.*" --output_pretty yes [ [ 0, 1449782162.00164, 2.34969544410706 ], [ [ 5 ], [ [ "_key", "ShortText" ], [ "_value", "Float" ] ], [ "company:google technology holdings", 0.644274830818176 ], [ "company:research in motion", 0.641874372959137 ], [ "company:htc", 0.63908725976944 ], [ "company:lenovo (singapore) pte", 0.63323575258255 ], [ "company:blackberry", 0.622487127780914 ] ] ]
- 改良後(パトリシアトライで"company:"に前方一致)
> word2vec_distance "company:apple" --is_phrase 1 --limit 5 --offset 0 \ --n_sort 5 --prefix_filter "company:" --output_pretty yes [ [ 0, 1449781678.28364, 0.00766372680664062 ], [ [ 5 ], [ [ "_key", "ShortText" ], [ "_value", "Float" ] ], [ "company:google technology holdings", 0.644274830818176 ], [ "company:research in motion", 0.641874372959137 ], [ "company:htc", 0.63908725976944 ], [ "company:lenovo (singapore) pte", 0.63323575258255 ], [ "company:blackberry", 0.622487127780914 ] ] ]
このようにGroongaは全文検索だけでなくPatricia Trie、Double Array、HashのCライブラリとしても有用に使うことができます。今回のケースでは更新をしないので、Marisa Trieが使えたらより省メモリで構築できてよいかもしれません。
word2vec実行例
会社名のラベルをつけて類似会社名のみを取得してみます。
> word2vec_distance "company:トヨタ自動車株式会社" --limit 10 --offset 0 \ --n_sort 10 --prefix_filter "company:" --output_pretty yes [ [ 0, 1449815073.42663, 0.138718605041504 ], [ [ 5 ], [ [ "_key", "ShortText" ], [ "_value", "Float" ] ], [ "company:日産自動車株式会社", 0.911168932914734 ], [ "company:三菱自動車工業株式会社", 0.891977429389954 ], [ "company:富士重工業株式会社", 0.870425820350647 ], [ "company:ダイハツ工業株式会社", 0.858096361160278 ], [ "company:本田技研工業株式会社", 0.839616179466248 ] ] ]
類似しているぽい会社名が取得できました。
ベクトルの加減算も自由に行えます。
> word2vec_distance "company:トヨタ自動車株式会社 - company:日産自動車株式会社 + company:グーグル_インコーポレイテッド" \ --limit 5 --n_sort 5 --output_pretty yes [ [ 0, 1449815137.37063, 0.501566171646118 ], [ [ 5 ], [ [ "_key", "ShortText" ], [ "_value", "Float" ] ], [ "company:グーグル__インコーポレイテッド", 0.87317681312561 ], [ "company:グーグル・インコーポレーテッド", 0.868086099624634 ], [ "company:ヤフー!_インコーポレイテッド", 0.836208581924438 ], [ "company:アリババ・グループ・ホールディング・リミテッド", 0.827649772167206 ], [ "company:マイクロソフト_コーポレーション", 0.825306117534637 ] ] ]
あ、表記ゆれが結構あるな。。
会社名の総数は単語の語彙数よりは数が少ないのでそこそこ高速に検索できています。最初にカテゴリーかなにかで分類すれば、結構高速に得られますね。
sentence2vec実行例
dump_to_train_file Entries title,tag,tags --sentence_vectors 1 word2vec_train --min_count 1 --cbow 1 --sentence_vectors 1 word2vec_distance "doc_id:2" --sentence_vectors 1 --table Entries --column _id,title,tag [ [ 0, 0.0, 0.0 ], [ [ 2 ], [ [ "_id", "UInt32" ], [ "title", "ShortText" ], [ "tag", "Tags" ] ], [ 3, "Database", "Server" ], [ 1, "FulltextSearch", "Library" ] ] ]
doc_idを指定することにより(たぶん)類似する文書の取得でき、そのカラムを直接取得することができます。
実際の実行例のせようと思ってたのですが、min_countオプションを指定するのを忘れてdoc_idが除去されてしまいました。min_conutのデフォルトは5で出現数が5に満たない語彙は捨てられます。sentence vectorの場合は、--min_count 1にして除外されないようにしないといけませんね。実験結果はそのうち載せようと思います。
試してみました。
Groongaからword2vecを使って類似文書を取得してみる - CreateField Blog
おわりに
Groongaからword2vecを簡単に使うためのプラグインを紹介しました。 GroongaはMySQLでテーブル構築やデータ管理ができるMroongaや、PosrgreSQLからGroongaのインデックスを使うことができるPGroongaが開発されています。これらを使えば簡単にGroongaのテーブルを作ることができ、このプラグインを使えばword2vecを簡単に試すことができます。私はMroongaからこのプラグインを利用しています。GroongaやMySQLにデータがあって、わざわざ整形したりするのが面倒という場合はこのプラグインを使ってみてはいかがでしょうか。
word2vecと似たようなものでGloVeというのもあるみたいです。
この発表では編集距離ベースに誤記の表記揺れを抽出した例を紹介しましたが、word2vecをうまく使えば、略語や言い換えなど意味的な表記揺れもある程度取得可能かもしれませんね*3。