大きめの自然言語のデータを扱うのに、word2vecがとても便利です。いろいろ使い道があると思いますが、私はよく表記ゆれの吸収のレイヤとして使用します。
実際に活用する場面では、「このキーワードに近い単語」というより、「近い単語をすべて使って集計する◯◯の数字」といったものが出てきます。例えば、「xx という単語と、それに近い単語を含んだページをみたユーザ数」とかです。このリクエストに応えるために、例えば
- 事前にDBから自然言語のデータを抜き出して前処理し、word2vecのモデルデータを作成しておく
- 単語を受け取って、word2vecで近い単語のリストを取り出す
- 2.のリストを使って、DBでユーザ数を集計
といったフローが発生します。2.と3.が何回も発生するのでこの作業の効率化を考えます。DB上で「word2vecで近い単語のリストを取り出す」ができるようになると、SQL叩くだけになるので効率化できそうな気がします。
準備
gensim の Word2Vecを使用して、事前にモデルデータを作成しておきます。また、以下のようなスクリプトでtsvとして出力できるようにしておきます。モデルパラメータ(次元)の変更に強いように、縦持ちでデータを持つことにします。
import gensim
from gensim.models.word2vec import Word2Vec
FORMAT="%s\t%d\t%f"
if __name__ == '__main__':
model = Word2Vec.load(...)
for word in model.vocab:
vector = model[word]
for idx,value in enumerate(vector):
print (FORMAT % (word, idx, value)).encode('utf-8')
また、モデルを格納するテーブルを以下のように定義します。ここではpostgresを使用することにします。
create table word2vec (
keyword varchar(30) not null,
idx int not null,
value double precision not null
);
一時ファイルを作らず、psqlにパイプでデータを流すと楽チンです。
python ./dump.py | psql -c "\copy word2vec from pstdin"
クエリ
word2vecでは単語間の近さを計算することができます。幾つか種類があると思いますが、ここでは最も基本的なコサイン類似度を使ってみようと思います。
with targets as (
select 'man'::text as keyword
),
config as (
select
-- 1キーワードあたりいくつの関連キーワードを取り出すか?
10 as maximum_num_related_keywords_by_target
),
-- 以降は計算処理
similarity as( -- コサイン類似度の計算
select
targets.keyword as target_keyword,
other_kv.keyword as related_keyword,
sum(target_kv.value * other_kv.value) / sqrt(sum(target_kv.value * target_kv.value) * sum(other_kv.value * other_kv.value)) as cosine_similarity
from
targets
inner join word2vec target_kv on targets.keyword = target_kv.keyword
left outer join word2vec other_kv on target_kv.idx = other_kv.idx
group by
targets.keyword,
other_kv.keyword
),
ranked_similarity as ( -- コサイン類似度に基づいた順位付け
select
target_keyword,
related_keyword,
cosine_similarity,
DENSE_RANK() over (partition by target_keyword order by cosine_similarity desc) as related_keyword_rank
from
similarity
)
-- 出力
select
target_keyword,
related_keyword,
cosine_similarity
from
ranked_similarity
cross join config
where
related_keyword_rank <= maximum_num_related_keywords_by_target
order by
target_keyword, cosine_similarity desc
結果
googleの公開している word2vecのサンプル (の、githubのクローン)で使用されているデータ(text8)で動作確認をしてみます。 gensimでは以下のような出力が得られます。
>>> for k,v in model.most_similar(positive=['man']):
... print k,v
...
woman 0.567330121994
girl 0.547901391983
daredevil 0.504863023758
young 0.481207370758
immortal 0.473287910223
spider 0.47292047739
story 0.444978773594
beast 0.444529414177
himself 0.443396568298
bride 0.440255999565
同じモデルで、query.sqlを実行してみると以下のような出力が得られます。入力した単語(man)が含まれていること以外は、単語の並びやsimilarityの値が同様のものを得られていることがわかります。このクエリで得られた related_keyword
を使用して、さらに別のテーブルへjoinして各種集計を行うことができます。
target_keyword | related_keyword | cosine_similarity
----------------+-----------------+-------------------
man | man | 1
man | woman | 0.567330055211273
man | girl | 0.54790106993787
man | daredevil | 0.504863090926911
man | young | 0.481207192597222
man | immortal | 0.473287587423905
man | spider | 0.472920298213122
man | story | 0.44497852578098
man | beast | 0.444529328298847
man | himself | 0.44339640098128
まとめ
word2vecの類似単語推測をSQLで行いました。いくつか課題があると思っていて、何かアイデアがあったらコメントいただけるとありがたいです。
- 含まれている単語数によりますが、そこそこ計算時間がかかります。この記事ではセットアップの手間を省くためpostgresで行いました。ログデータなどを溜め込んでいるビッグデータ対応なサービスや製品だとこのアプローチは有効なように思います。
- 学習部分は(DBの)外でやるので、できればSQLでも学習できるとさらに楽できそうです。