食べログに肉の希少部位を教えてもらう話

シニアデータサイエンティストの富田です。
この記事はAdvent Calendar13日の投稿です。
なんか面白いこと書けと言われたので面白いこと書きます。

## 背景

この記事を読むような読者さんはご存知かと思いますが、食べログはレビュアーさんからの評価を元に計算された点数で検索することができまして、グルメな方々が美味しいと判断するお店を簡単に調べることができます。例えば、彼女さんをおしゃれなお店に連れて行くとためのお店探しも食べログを使えば楽々検索できます。

で、おしゃれなお店を探してみると焼肉のお店が出てくることがありまして、女性も連れて行ける焼肉が最近確かに増えて来て良さそうに見えるのですが、何も考えずにそのようなお店を選んでしまうと一つ問題が出て来ます。お店に行ってみてメニューを開いてみますと、肉の聞いたことない部位が出てくるわけです。みすじ、エンピツとか。何よそれってなりません？

ここで肉の希少部位が何の肉なんだよーとかさっと答えられたらカッコイイじゃないですか。彼女さんの好感度がぐんぐん上昇しそうです。

なので、食べログることにしました。

## 問題設定

希少部位はどこか有名な部位の中のさらに一部であることが多いので、最終的には有名な部位、あるいは、体のどの部分であるというのが出力されることを目標とします。
例えば、「ザブトン」という部位は「肩ロース」の中から取り出される希少部位で、肩にある部位ですので、「ザブトン」を入力したら「肩ロース」や「肩」が出力されることを目指します。

## 手法の方針

ザブトン => 肩ロース という推移関係を捉えて、推移関係を適用できるのであれば、部位に対する場所が特定出来そうです。そこで、もはや古くなってしまった手法ではありますが、推移関係を捉えられるword2vecを採用してみることにします。

## 下ごしらえ

焼肉ジャンルの店舗の口コミを抽出し、口コミ本文を形態素解析して部位の名前が含まれるワード群を抽出します。(4~5年前のコードを再利用しているのでMeCab+ipadicでやってますが、今ならMeCab + mecab-ipadic-NEologdを使うのが良さそうです)
単に形態素解析を行うだけだと、部位の名前や出力したいワードまで分割されてしまう可能性があるため、形容詞 + 名詞など、名詞で終わる語句を一部結合して出力します。ここら辺は部位の名前がそれなりの精度で抽出出来れば十分です。

以下のような、ワード群が取れたと仮定して以降の話をします。
(どなたの口コミなのか特定できないように部分だけ抽出させていただいております)

```
...和牛 部位 、 肩 希少 部位 ザブトン ！ 脂 最高 た...
... 入る た 肉 和牛 ザブトン 肩ロース 芯 部分 希少部位 は もっと 霜降り が..
```

## 調理

CBOW(word2vecの手法の一つ)で学習します。


```python:train.py
import gensim
import sys

# 直接読み込んで渡すとメモリを食うので、iteratorで渡す
class SentencesFromFile:
  def __init__(self, path):
    self.path = path 
 
  def __iter__(self):
    for line in open(self.path):
      yield line.split()
 
input_path = sys.argv[1]
model_path = sys.argv[2]
sentences = SentencesFromFile(input_path)
model = gensim.models.Word2Vec(sentences)
model.save(model_path)
```

実行

```bash
$ python train.py input_data model
```

「例としてあげる希少部位の名前」 - 「例としてあげる部位グループの名前」 + 「調べたい希少部位」とすれば推移関係を計算することが出来ます。
今回は以下の設定で実行指定してみました。
例としてあげる希少部位の名前:「ザブトン」
例の部位グループの名前:「肩ロース」

```python:test.py
import gensim
import sys

# モデルを引数で指定
model_path = sys.argv[1]

# モデルのロード
model = gensim.models.word2vec.Word2Vec.load(model_path)

# 希少部位の場所を出力する関数
def teach_group(word, parent_word, child_word):
  # 見つから無かった場合はその旨を出力
  if word not in model.wv:
    print("not found")
    return
  out = model.wv.most_similar(positive=[parent_word, word], negative=[child_word], topn=3)
  for x in out:
    print(word + " => " + x[0] + " " + str(x[1]))

# 部位の名前 -> 場所
groups = {
  "ザブトン": "肩ロース",
  "サンカク": "肩バラ肉",
  "ミスジ": "肩甲骨",
  "トウガラシ": "肩甲骨",
  "リブロース": "ロース",
  "エンピツ": "リブロース",
  "カイノミ": "バラ肉",
  "ヒウチ": "内股"
}

for child_word, parent_word in groups.items():
  print(child_word + ":" + parent_word)
  teach_group(child_word, parent_word="肩ロース", child_word="ザブトン")
```

実行

```bash
$ python test.py input_data model
ザブトン:肩ロース
ザブトン => ショルダー 0.9025213122367859
ザブトン => ランプ 0.8409810066223145
ザブトン => モモ 0.8318709135055542
サンカク:肩バラ肉
サンカク => ショルダー 0.8480710983276367
サンカク => 肩肉 0.8426247835159302
サンカク => モモ 0.8292331695556641
ミスジ:肩甲骨
ミスジ => ショルダー 0.8833603858947754
ミスジ => モモ 0.8254925012588501
ミスジ => ランプ 0.8164725303649902
トウガラシ:肩甲骨
トウガラシ => ショルダー 0.7658345699310303
トウガラシ => 肩肉 0.7233084440231323
トウガラシ => もも肉 0.7228221893310547
リブロース:ロース
リブロース => ショルダー 0.839434027671814
リブロース => もも肉 0.7863738536834717
リブロース => モモ肉 0.7844791412353516
エンピツ:リブロース
エンピツ => 肩肉 0.819837749004364
エンピツ => ムネ 0.7850074768066406
エンピツ => モモ 0.7765495181083679
カイノミ:バラ肉
カイノミ => ショルダー 0.8563748598098755
カイノミ => モモ 0.8255313038825989
カイノミ => 肩肉 0.7984086275100708
ヒウチ:内股
ヒウチ => 肩肉 0.8613795042037964
ヒウチ => ショルダー 0.8531433343887329
ヒウチ => モモ 0.8186233639717102
```

候補には出て来ているものの、最も似ているワードには部位の場所が正しく出ていたり、出ていなかったりします。

## エラー分析

口コミを眺めていると、ただ、部位を列挙しているだけの口コミがちらほらみられ、
そのような口コミがノイズになって正しく推定出来ていない可能性が考えられます。(推測ですが、食べた部位をそのまま記載していると考えられます。)
今回のタスクではノイズとなるような書き方をしている口コミを除くことで性能が向上すると思われます。

以下はヒウチの例です。

```
# ヒウチがモモ肉であることに言及している口コミ
... ヒウチ モモ肉 一部 で 、 牛 頭 から   ほど しか とれる ...
# ただ部位を列挙しているだけの口コミ
...限定肉 ミスジ イチボ ヒウチ  円 は 絶対 安い メニュー た だけ...
```

## 最後に

焼肉の部位に対する場所の特定を、食べログのデータを用いて試みました。
部位の場所は候補には上がって来たものの、部位を列挙する表現の口コミがノイズとなり、うまく当てることが出来ませんでした。
好感度は上がりませんでした。残念です。


次のAdvent Calendarは@ham0215 さんです。