Rettyのクラフトビール担当兼エンジニアのbokenekoです。
doc2vecを使って実験的なリコメンデーションシステムを作ってみたのでその手法を紹介します。
doc2vec
doc2vecはword2vecの進化系です。word2vecはある単語はその周りにどんな単語が現れやすいかでその単語の意味を捉えようとしますが、doc2vecはそこにさらに文脈を加味するように学習します。
例えば、「私はxxxを飼っている」という文章のxxxには「犬」とか「猫」とかが入るので「犬」も「猫」も似た意味を持つのだろうというのがword2vecの考え方です。
ですが、もしこの文章が犬の話の小説のものであれば「猫」よりも「犬」が圧倒的に出やすくなりますし、SM小説の一節なら...まあ出やすい単語が変わるのは分かっていただけるかと思います。
つまり文章の文脈によって単語の出やすさが変わるので、どんな単語が使われているかということからその文章がどんな文章かを学習できる、というのがdoc2vecなわけです。
詳しいことはまあDistributed Representations of Sentences and Documentsとかを読んでください。
正直私も斜め読みなんで間違った解釈してるかもしれません。
口コミのdoc2vec
口コミをdoc2vecにかけてやればその口コミの分散表現が学習できます。
gensimにdoc2vecがあるのでそれを使ってみましょう。
理屈は置いといてまずは試してみるってのが簡単にできるのはほんとありがたいです。
# -*- coding:utf-8 -*-
from gensim.models.doc2vec import Doc2Vec
from gensim.models.doc2vec import TaggedDocument
import MeCab
import csv
mt = MeCab.Tagger()
reports = []
with open("reports.tsv") as f:
# reports.tsvには一行に口コミID,口コミがtab区切りで保存されている
reader = csv.reader(f, delimiter="\t")
for report_id, report in reader:
words = []
node = mt.parseToNode(report)
while node:
if len(node.surface) > 0:
words.append(node.surface)
node = node.next
# wordsが口コミの単語のリスト,tagsには口コミIDを指定
reports.append(TaggedDocument(words=words, tags=[report_id]))
model = Doc2Vec(documents=reports, size=128, window=8, min_count=5, workers=8)
model.save("doc2vec.model")
これで口コミを128次元のベクトルとして学習できます。
学習した口コミベクトルは以下のようにして確認できます。
# -*- coding:utf-8 -*-
from gensim.models.doc2vec import Doc2Vec
model = Doc2Vec.load("doc2vec.model")
sample_report_id = .... # 分散表現を確認したい口コミID
report_vector = model.docvecs[sample_report_id]
リコメンデーション
さて、これで口コミのベクトルを学習できたわけですが、これでどうリコメンデーションをするのかと言いますと、まずあるユーザーの口コミベクトル全ての加算平均をそのユーザーを表すベクトルだと考えます。
同じようにある店舗の口コミベクトル全ての加算平均をその店舗を表すベクトルだと考えます。
こうして作ったユーザーベクトルと店舗ベクトルを使って
- ユーザーに近い店舗 => ユーザーへのおすすめ店舗
- ユーザーに近いユーザー => 似たユーザー。Rettyでのフォロー候補のリコメンドとかに使えるかな
- 店舗に近いユーザー => その店舗を好むであろうユーザー。PUSHの対象とかに使えるかな
- 店舗に近い店舗 => Rettyの店舗ページの似たお店欄に出すとか
ということができるんではないかなーと考えました。
ただの仮説なのでうまくいくかはやってみなくちゃわからないってことで早速やってみましょう。
ngt
ベクトルの近傍をスピーディーに計算するためにngtを使いました。
※ 前見たらライセンスが商用不可だった気がするののですが、今はApache License, Version 2.0になってたのでもう商用OKみたいです。今回のシステムは今のところ社内だけの実験的なものなので商用ではないですが、実運用に回すことになっても大丈夫そう。
使い方としては、以下のような1行に1ベクトルの各次元をの値をtab区切りにしたものをユーザーと店舗で用意します。
-0.32609 0.0670668 -0.0722714 -0.0738026 0.0177741 ....
...
0.0385331 0.0978981 -0.0495091 -0.182571 0.0538142 ...
...
これは上記のdoc2vecの結果を書き出して作ります。
そして以下のコマンドでDBを作成します。
$ ngt create -d 128 users users.tsv
$ ngt create -d 128 restaurants restaurants.tsv
すると以下のようなディレクトリができてるはずです。
<cur dir>
|--restaurants
| |-- grp
| |-- obj
| |-- prf
| |-- tre
|
|--users
|-- grp
|-- obj
|-- prf
|-- tre
これで準備は完了です。
検索は以下のようにします。
# ユーザーを検索する場合
$ ngt search -n 10 users search_query.tsv
# 店舗を検索する場合
$ ngt search -n 10 restaurants search_query.tsv
# search_query.tsvはターゲットとなるベクトルを1行に1ベクトルの各次元をの値をtab区切りにしたものとして書かれている。
# 私に近い店舗を探すなら、search_query.tsvには私のユーザーベクトルを書く。
ユーザーに近い店舗
ではまずは私を実験台にして私におすすめの店を私が口コミを書いていない店に限定してリコメンドしてみます。
順位 | url |
---|---|
1 | シャムロック バイ アボット チョイス |
2 | クラフトヘッズ |
3 | アボットチョイス 渋谷店 |
4 | スワンレイク パブ エド |
5 | クラフトビアマーケット 神保町店 |
6 | クーパーエールズ |
7 | 馬車道タップルーム |
8 | 8taps |
9 | バンガロー |
10 | ザ・シャノンズ |
私はRettyのクラフトビール担当を名乗ってますが、まあ見事にビールの店ばっかりですね。
確かに私好みなのでうまくいってる気がします。
特にクラフトヘッズは口コミ書いてないですがもうずいぶん通ってます。なら口コミ書けよって話なんですがね。
さらに言えば、この中で行ったことないの馬車道タップルームだけです。
口コミ書くのサボってごめんね:P
店舗に近い店舗
上で出たクラフトヘッズは私の好きな店なので、その店に近い店を探してみましょう。
順位 | url |
---|---|
1 | The Griffon |
2 | グッドビア ファウセッツ |
3 | ビアパブカムデン |
4 | ビーボ! ビア アンド ダイニングバー |
5 | ウォータリングホール |
6 | ブリュードッグ 六本木 |
7 | 中目黒 タップルーム |
8 | TAP STAND |
9 | 目黒リパブリック |
10 | ブルゴンディセ ヘイメル |
うん、ビールの店ばっかですね。ちなみに全部行ったことあります。
ちゃんとビールのお店がリコメンドできているようです。
ユーザーに近いユーザー
Retty社員以外のユーザーさん出しちゃうのはアレなのでここはスキップ。
ちなみに私に近いユーザーさん出すと確かにビール狂な方々が出てきました。
店舗に近いユーザー
ここもユーザーさん出ちゃうんでスキップしときます。
まとめ
口コミのdoc2vecの加算平均でユーザー・店舗を表すベクトルを得られるかな?という仮説のもとにやってみましたが、結構うまく行きそうです。
今回単語を取り出すのにmecabを使いましたが、sentencepieceがなかなか面白そうなので、今度はこっち使ってやってみようと思ってます。
P.S.
上では口コミベクトルの加算平均をユーザーベクトル・店舗ベクトルにしていると書きましたが、実はもうちょっと工夫してます。
最初はただの加算平均だったんですが、社内で試してもらったところ昔の趣味が反映されすぎると言われました。なので最近の口コミほど重みをつけるようにすると、今度は趣味の店行き尽くしていて最近は趣味の店以外の投稿が多いという方に趣味に沿った店が出てこないと言われて...と諸々あったので微調整を繰り返しています。上記結果はその微調整後の結果です。