fastTextを使って単語じゃないものの分散表現を獲得する

※　2017/1/16に追記しました

[Retty Advent Calendar](http://qiita.com/advent-calendar/2016/retty)で穴が空きそうになったとき用に記事用意してたんですが、ちゃんとみんな埋めてくれたみたいです。良かった。
で、用意した記事が無駄になってももったいないので普通の記事として公開しちゃいます。

皆さん[fastText](https://github.com/facebookresearch/fastText)って知ってますか？
Facebookが公開している自然言語処理用のツールです。GPU使わないのに超速いのでありがたく使ってます。
単語の分散表現を学習させたり文章の分類とかができるんですが、分散表現の学習の仕組みって語彙にID振ってone-hot vectorにして、それを次元圧縮してるんですよね？（適当）
じゃあ、ID列で表せる何かならなんでも分散表現にできるんじゃね？って思いません？
思いついたらやってみましょう。

# 用意するもの

- [fastText](https://github.com/facebookresearch/fastText)
- Rettyユーザーのお店詳細ページの閲覧履歴

# やること

Rettyのお店詳細ページの閲覧履歴からお店の分散表現を獲得できるか試す。

# やってみた

まずは1行に1ユーザーの閲覧したお店のIDが時系列順にスペース区切りに並んだファイルを用意します。
こんな感じです

```:visit_history.dat
100000742124 100000706532 100000733147 100000004915 100001262703 100001215921 ...
100000008314 100000699913 100000052680 100000798310 100001211986 ...
...
※内容は適当です。
```

で、これをそのままfastTextに渡すだけです。

```bash
fasttext skipgram -input visit_history.dat -output output  -epoch 300
```

こうすると、カレントディレクトリに

```
output.bin 
output.vec
```

ができます。output.vecが各お店の分散表現になってます。中身は

```
お店の数 ベクトルの次元数
<お店ID> <ベクトル値>,...
<お店ID> <ベクトル値>,...
...
```

て感じです。

ではこの結果を使って各お店に近い店を探してみましょう。

```py
import numpy as np

n_result = 100

# read data
with open('output.vec', 'r') as f:
    ss = f.readline().split()
    n_vocab, n_units = int(ss[0]), int(ss[1])
    store2index = {}
    index2store = {}
    w = np.empty((n_vocab, n_units), dtype=np.float32)
    for i, line in enumerate(f):
        ss = line.split()
        store = ss[0]
        store2index[store] = i
        index2store[i] = store
        w[i] = np.array([float(s) for s in ss[1:]], dtype=np.float32)

# normalize
s = np.sqrt((w * w).sum(1))
w /= s.reshape((s.shape[0], 1))

# calc similarity
stores = store2index.keys()
try:
    for store in stores:
        v = w[store2index[store]]
        similarity = w.dot(v)
        count = 0
        for i in (-similarity).argsort():
            if np.isnan(similarity[i]):
                continue
            if index2store[i] == store:
                continue
            print 'https://retty.me/restaurant/{}'.format(store),"https://retty.me/restaurant/{}".format(index2store[i]), similarity[i]
            count += 1
            if count == n_result:
                break
except EOFError:
    pass
```

結果の一例を挙げると、

|お店1|お店2|類似度|
|-------|-------|---|
| [ミディ・アプレミディ](https://retty.me/restaurant/100000019960) | [然花抄院 京都室町本店](https://retty.me/restaurant/100000034434) | 0.999405 |

両方とも京都の烏丸御池あたりのスイーツなお店ですね。

|お店1|お店2|類似度|
|-------|-------|---|
| [Fish Market丸秀](https://retty.me/restaurant/100001179152) | [サンパチキッチン 今泉店](https://retty.me/restaurant/100000827768) | 0.996479 |

どっちも福岡市のイタリアンですね。

|お店1|お店2|類似度|
|-------|-------|---|
| [バー タツミ](https://retty.me/restaurant/100000019967) | [シャンパーニュバー ポンポンヌ](https://retty.me/restaurant/100000858635) | 0.998517 |

広尾駅あたりのバーですね。

たまに小樽と東京の店が近かったりとか変なのも出ちゃいますが結構うまくいきそうです。

# まとめ

これで口コミが少ない店でも分析できる！
いつからfastTextが自然言語処理用だと錯覚していた？

# ※ 追記 2017/1/16

fastTextは語尾の変化を考慮するという情報がはてブやらTwitterやらでコメントされてました。
お店IDが近いもの同士が語尾の変化しただけの似てる語と見なされちゃうんでは？それが結果に影響を与えないかな？ってことのようです。

全然気にせず使ってたので、もしかしたらそれが東京と小樽の店が近いと判定されたりすることがあった原因かもと思い検証してみました。

## 何をやったか

お店IDをmd5でハッシュ化した上で上記手順で分散表現を獲得させました。
ハッシュ化すれば近いIDも見た目上はバラバラになるだろうという目論見です。

こんな感じ

```py
import hashlib
print hashlib.md5("100000008314").hexdigest() # ==> '8e3e38774abe5c64c3658dcb82f26a8a'
print hashlib.md5("100000008315").hexdigest() # ==> '7f85b7d13425a28ee6ca17d2649fe8e4'
```

これで
- 前回と結果がガラリと変わらないか
- 前回おかしかった結果が改善されるか
を確認しました。

### 前回と結果が全然違ったりしてない？

#### ミディ・アプレミディに一番類似度が高いお店

前回

|お店1|お店2|類似度|
|-------|-------|---|
| [ミディ・アプレミディ](https://retty.me/restaurant/100000019960) | [然花抄院 京都室町本店](https://retty.me/restaurant/100000034434) | 0.999405 |

今回

|お店1|お店2|類似度|
|-------|-------|---|
| [ミディ・アプレミディ](https://retty.me/restaurant/100000019960) | [然花抄院 京都室町本店](https://retty.me/restaurant/100000034434) | 0.785348 |

これは変わらなかったみたいです。が、距離が少々離れたみたいです。前のは近すぎる気はしてたので健全になった気がします。

#### Fish Market丸秀に一番類似度が高いお店

前回

|お店1|お店2|類似度|
|-------|-------|---|
| [Fish Market丸秀](https://retty.me/restaurant/100001179152) | [サンパチキッチン 今泉店](https://retty.me/restaurant/100000827768) | 0.996479 |

今回

|お店1|お店2|類似度|
|-------|-------|---|
| [Fish Market丸秀](https://retty.me/restaurant/100001179152) | [Quad](https://retty.me/restaurant/100000054770) | 0.799881 |

これは変わりました。どちらも福岡のイタリアンです。
ちなみに前回一番近かった[サンパチキッチン 今泉店](https://retty.me/restaurant/100000827768)は類似度0.794777で二位でした

#### バー タツミに一番類似度が高いお店

前回

|お店1|お店2|類似度|
|-------|-------|---|
| [バー タツミ](https://retty.me/restaurant/100000019967) | [シャンパーニュバー ポンポンヌ](https://retty.me/restaurant/100000858635) | 0.998517 |

今回

|お店1|お店2|類似度|
|-------|-------|---|
| [バー タツミ](https://retty.me/restaurant/100000019967) | [シャンパーニュバー ポンポンヌ](https://retty.me/restaurant/100000858635) | 0.628401 |

これは変わりませんでした。ですが、やはり距離は離れてます。

### 前回おかしかった結果は改善した？


#### 赤坂うまや 博多に一番類似度が高いお店

前回

|お店1|お店2|類似度|
|-------|-------|---|
| [赤坂うまや 博多](https://retty.me/restaurant/100000053009) | [霧乃個室 蒸し屋清郎](https://retty.me/restaurant/100000000876) | 0.998709 |

今回

|お店1|お店2|類似度|
|-------|-------|---|
| [赤坂うまや 博多](https://retty.me/restaurant/100000053009) | [すず 博多駅筑紫口店](https://retty.me/restaurant/100001263824) | 0.710382 |

前回は博多の居酒屋と渋谷の居酒屋が一番近いってことになってしまってましたが、今回はどちらも博多の居酒屋になりました！

#### 鉄板バーグ RUN Mっつんに一番類似度が高いお店

前回

|お店1|お店2|類似度|
|-------|-------|---|
| [鉄板バーグ RUN Mっつん](https://retty.me/restaurant/100000852792) | [ソレントの窯焼き](https://retty.me/restaurant/100000852791) | 0.998609 |

今回

|お店1|お店2|類似度|
|-------|-------|---|
| [鉄板バーグ RUN Mっつん](https://retty.me/restaurant/100000852792) | [MARUZEN Cafe 京都店](https://retty.me/restaurant/100001256372) | 0.771792 |

前回は京都のハンバーグ屋と静岡のイタリアンが一番近いってことになってしまってましたが、どちらも京都になりました！
ハンバーグ屋とハヤシライスが売りのカフェ、どっちも洋食だしまあ近い…か？
前回の結果はIDが隣同士なのであからさまに語尾だけ変わった同じ語と判断されちゃった感じですね…

## 検証結果

- 前回問題なかったところについては基本的に同じような結果になるようです。距離が離れるようになりましたが、前の結果は近すぎたように思うので健全になったと思われます。
- 前回おかしかった部分がかなり改善された雰囲気です。

全体的に改善したように思えます。これは語尾の変化が考慮されているってところがかなり影響あった気がしますね。

指摘してくれてた方達に感謝！