word2vecの類似単語推測をSQLで行う

大きめの自然言語のデータを扱うのに、word2vecがとても便利です。いろいろ使い道があると思いますが、私はよく表記ゆれの吸収のレイヤとして使用します。

実際に活用する場面では、「このキーワードに近い単語」というより、「近い単語をすべて使って集計する◯◯の数字」といったものが出てきます。例えば、「xx という単語と、それに近い単語を含んだページをみたユーザ数」とかです。このリクエストに応えるために、例えば

1. 事前にDBから自然言語のデータを抜き出して前処理し、word2vecのモデルデータを作成しておく
2. 単語を受け取って、word2vecで近い単語のリストを取り出す
3. 2.のリストを使って、DBでユーザ数を集計

といったフローが発生します。2.と3.が何回も発生するのでこの作業の効率化を考えます。DB上で「word2vecで近い単語のリストを取り出す」ができるようになると、SQL叩くだけになるので効率化できそうな気がします。

# 準備

gensim の Word2Vecを使用して、事前にモデルデータを作成しておきます。また、以下のようなスクリプトでtsvとして出力できるようにしておきます。モデルパラメータ(次元)の変更に強いように、縦持ちでデータを持つことにします。

```python:dump.py
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を使用することにします。

```postgres
create table word2vec (
  keyword varchar(30) not null,
  idx int not null,
  value double precision not null
);
```

一時ファイルを作らず、psqlにパイプでデータを流すと楽チンです。

```bash
python ./dump.py | psql -c "\copy word2vec from pstdin"
```

# クエリ

word2vecでは単語間の近さを計算することができます。幾つか種類があると思いますが、ここでは最も基本的なコサイン類似度を使ってみようと思います。

```sql:query.sql

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のサンプル](https://code.google.com/archive/p/word2vec/) (の、[githubのクローン](https://github.com/dav/word2vec))で使用されているデータ(text8)で動作確認をしてみます。 gensimでは以下のような出力が得られます。

```python
>>> 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でも学習できるとさらに楽できそうです。