Edited at

【Python】🍜ラーメンガチ勢によるガチ勢のためのWord2vec(食べログ口コミコーパスの威力を検証してみた)🍜


1.簡単な概要

この記事では都内ラーメン屋の食べログ口コミを使ってWord2vecでモデル構築するをやり方を解説していきます。

私自身🍜が大好きで昔は年間100杯以上食べ歩いてきた自称ラーメンガチ勢です。しかしながら、直近の健康診断にひっかかり、医者からドクターストップをかけられてしまいました。。。

行き場をなくしたラーメン熱を発散すべく機械学習でラーメンレコメンド(隠れた名店をレコメンドで発掘)に挑戦してみることにしました。

今回は、スクレイピングで取得した口コミを学習データとしてWord2vecでモデル構築を行います。

世の中に食べログ口コミデータでモデリングする記事はいくつかありましたが、

ラーメン屋の"名店の口コミのみ"でモデル構築

をやっている前例が見当たらなかったのでやってみました。

本記事ではword2vecでおなじみの

「王様」ー「男」+「女」=「女王」

をラーメンバージョンでやってみたいと思います。

ゴールとしては、

「ラーメン」+「豚骨」+「ヤサイタワー」=「二郎」

みたいになればいいなと。

↓こんなイメージです。

二郎_new.jpg


2.はじめに

今回は、「【Python】ラーメンガチ勢による〇〇」シリーズ第2弾です。

第1弾でスクレイピングしたデータを用いてモデル構築していきます。

第1弾:【Python】ラーメンガチ勢によるガチ勢のための食べログスクレイピング

第2弾: 本記事

第3弾:【Python】ラーメンガチ勢が自然言語処理を学んで食べログの口コミから隠れた名店を探してみた。(coming soon)


3.Word2vecとは?

そもそもword2vecを使うと何が良いのか?について簡単に解説します。

word2vecは、大量のテキストデータを解析して、各単語の意味をベクトル表現化する手法です。単語をベクトル化することで、

単語間の意味の近さを計算

単語間の意味を足したり引いたり

ということが可能になります。

って、どういうこと?

私のような初学者のために少し説明しますと、例えば代表的なラーメンチェーン店『一風堂』、『俺流塩ラーメン』をベクトルで表現すると

一風堂:(0.4,0.1,0.9,0.4)

俺流塩ラーメン:(0.5,0.2,0.3,0.4)

となったとします。

この二つのベクトルのコサイン類似度を計算します。

コサイン類似度は三角関数の普通のコサインの通り、1に近ければ類似しており、0に近ければ類似していないと解釈できます。

参考:http://www.cse.kyoto-su.ac.jp/~g0846020/keywords/cosinSimilarity.html

『一風堂』ベクトルと『俺流塩ラーメン』ベクトルのコサイン類似度を計算すると0.83となり、類似度が高いといえます。この技術を応用するとラーメン屋Aに近いラーメン屋Bをレコメンドすることが可能となります。

次に、単語間の意味を足したり引いたりすることもできます。

今度は『豚骨』『塩』ベクトルも用意してみました。

一風堂:(0.4,0.1,0.9,0.4)

俺流塩ラーメン:(0.5,0.2,0.3,0.4)

豚骨:(0.1,0.0,0.8,0.2)

塩:(0.2,0.1,0.2,0.3)

先ほど『一風堂』 と 『俺流塩ラーメン』の類似度が高かったことから、

『一風堂』 ベクトルから 『豚骨』ベクトル成分を抜いて、変わりに『塩』ベクトル成分を加えると、

『一風堂』 - 『豚骨』 + 『塩』 ≒ 『俺流塩ラーメン』

といった計算ができます。

詳細なアルゴリズムは、下記が参考になるかと思います。

https://deepage.net/bigdata/machine_learning/2016/09/02/word2vec_power_of_word_vector.html


4.モデル構築までの流れ

Word2vecでモデル構築する手順をざっくり。全体像がわかると理解が早いです。

初学者の方は、下記の記事で自然言語処理用語を予習していただけるとスムーズです。

参考:https://qiita.com/yura/items/6c1481ca652d3d131e47


①MeCabのインストール

MeCabとは日本語の形態素解析器のことです。

参考:https://qiita.com/menon/items/f041b7c46543f38f78f7


②Mecabの辞書をNEologdに変更

NEologdとはMeCab用のシステム辞書のことを指し、Web上から得た新語に対応しています。

初学者の躓きやすい点は、MeCabインストール後に辞書をデフォルトからNEologdに変更するところです。誰しもが乗り越える壁ですので、先人のqiita記事を参考にするなどしてクリアしてください!(私は半日ぐらい費やしました笑)

参考:https://qiita.com/menon/items/f041b7c46543f38f78f7

※2019年3月時点では、mecab-python3 0.996.1ではparseToNodeが単語ではなく文章全体が出る不具合が報告されています。mecab-python3のバージョンを下げて対応する必要があるみたいです。

pip uninstall mecab-python3

pip install mecab-python3==0.7

https://teratail.com/questions/164787


③コーパスを作成

コーパスとは、自然言語の文章を構造化してまとめたものを指します。

口コミデータをMecabで分かち書きしてコーパスを作成します。

これが、モデリングするための学習データとなります。

今回使用する口コミデータは前回スクレイピングで取得したものです。

review.jpg


④モデル構築

gensimを使ってWord2Vecを実装します。

コーパスをWord2vecに噛ませるだけです。

詳しくはソースコードをご覧ください。


5.ソースコード

import numpy as np

import pandas as pd
import pickle
from gensim.models import word2vec
import MeCab

tagger = MeCab.Tagger('-Owakati -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd')#タグはMeCab.Tagger(neologd辞書)を使用

tagger.parse('')
def tokenize_ja(text, lower):
node = tagger.parseToNode(str(text))
while node:
if lower and node.feature.split(',')[0] in ["名詞","形容詞"]:#分かち書きで取得する品詞を指定
yield node.surface.lower()
node = node.next
def tokenize(content, token_min_len, token_max_len, lower):
return [
str(token) for token in tokenize_ja(content, lower)
if token_min_len <= len(token) <= token_max_len and not token.startswith('_')
]

#学習データの読み込み

df = pd.read_csv('../output/tokyo_ramen_review.csv')
df_ramen = df.groupby(['store_name','score','review_cnt'])['review'].apply(list).apply(' '.join).reset_index().sort_values('score', ascending=False)

#コーパス作成
wakati_ramen_text = []
for i in df_ramen['review']:
txt = tokenize(i, 2, 10000, True)
wakati_ramen_text.append(txt)
np.savetxt("../work/ramen_corpus.txt", wakati_ramen_text,fmt='%s', delimiter=',')
# モデル作成
word2vec_ramen_model = word2vec.Word2Vec(wakati_ramen_text,sg=1,size=100, window=5,min_count=5,iter=100,workers=3)
#sg(0: CBOW, 1: skip-gram),size(ベクトルの次元数),window(学習に使う前後の単語数),min_count(n回未満登場する単語を破棄),iter(トレーニング反復回数)

# モデルのセーブ
word2vec_ramen_model.save("../model/word2vec_ramen_model.model")

これでモデル構築は完了です!


6.○○に最も近い単語は△△

いよいよ、ここからが本番です!

では、早速 most_similar()で、ある単語に最も近しい単語を調べてみます。

食べログ口コミコーパスmodelの "威力" を実感いただくために、一般的なwikipediaコーパスでモデル構築したものと比較していきます。

↓wikipediaコーパスの作り方↓

https://qiita.com/kenta1984/items/93b64768494f971edf86

では、最初に始球式的な感じで投入するワードは、こちら!



$\huge{「山岸」}$

つけ麺の生みの親であり、つけ麺界のレジェンドofレジェンドである東池袋大勝軒元店主「山岸」さん。敬意を込めて、指名させていただきました。

では、まずはwikipediaコーパスmodelに投入します。

wikipediaにおける「山岸」に最も近しい単語が表示されます。


wikipedia_model

# モデルのロード

wikipedia_model = word2vec.Word2Vec.load("../model/full_wikipedia.model")
wikipedia_model.most_similar("山岸")
>>>
[('潤史', 0.4181003272533417),
('範宏', 0.4008517265319824),
('工藤', 0.39781779050827026),
('六車', 0.3838202953338623),
('林部', 0.3719743490219116),
('岡元', 0.3716839551925659),
('吉沢', 0.36764824390411377),
('加藤', 0.36382099986076355),
('斎藤', 0.362610787153244),
('木島', 0.36078932881355286)]

果たして、これはレジェンド「山岸」さんに近しいワードといえるでしょうか。もちろん否です。

一般的なwikipediaコーパスmodelでは、レジェンド「山岸」さんと関連性のない単語が並びました。

続いて、私が丹精を込めて作った食べログ口コミコーパスmodelに投入してみます・・・


word2vec_ramen_model

# モデルのロード

word2vec_ramen_model =word2vec.Word2Vec.load("../model/word2vec_ramen_model.model")
word2vec_ramen_model.most_similar("山岸")
>>>
[('東池袋大勝軒', 0.6612457036972046),
('東池袋', 0.6129428744316101),
('山岸一雄', 0.5511749982833862),
('大勝軒', 0.5429054498672485),
('滝野川', 0.5334186553955078),
('もりそば', 0.5152110457420349),
('丸信', 0.5020366907119751),
('マスター', 0.48960864543914795),
('代々木上原', 0.48622703552246094),
('1955å¹´', 0.4847238063812256)]



"$\huge{キタコレ!}$"

これは文句なしでレジェンド「山岸さん」に関連する大勝軒のワードが並びました!さすがですね。

東池袋大勝軒:https://tabelog.com/tokyo/A1305/A130501/13045828/

次に「二郎」についてもwikipediaコーパスmodelと食べログ口コミコーパスmodelで結果を比較してみます。


wikipedia_model

# モデルのロード

wikipedia_model = word2vec.Word2Vec.load("../model/full_wikipedia.model")
wikipedia_model.most_similar("二郎")
>>>
[('一郎', 0.4573158621788025),
('次郎', 0.44129377603530884),
('三郎', 0.43840569257736206),
('柴柳', 0.3729743957519531),
('四郎', 0.33052968978881836),
('太田喜', 0.33035555481910706),
('太郎', 0.32737094163894653),
('國領', 0.3245748281478882),
('江神', 0.3236039876937866),
('信一郎', 0.31906336545944214)]

決して間違いというわけではないのですが・・・

私が求めている「二郎」は、


word2vec_ramen_model

# モデルのロード

word2vec_ramen_model=word2vec.Word2Vec.load("../model/word2vec_ramen_model.model")
word2vec_ramen_model.most_similar("二郎")
>>>
[('ラーメン二郎', 0.7518627643585205),
('二郎系', 0.7041865587234497),
('インスパイア', 0.6942269802093506),
('上野毛', 0.6394986510276794),
('メグジ', 0.6040332317352295),
('ヤサイ', 0.5899537205696106),
('乳化', 0.5867205858230591),
('直系', 0.5784134268760681),
('英二', 0.5678684711456299),
('一之江', 0.567740261554718)]



"$\huge{まさにこれですよ!}$"

解説すると、

「上野毛」はラーメン二郎上野毛店を指し、「メグチ」はラーメン二郎目黒店のことです。

「英二」も二郎系のお店ですね。

ラーメン英二:https://tabelog.com/tokyo/A1326/A132602/13164704/

このように比較すると食べログ口コミで作ったコーパスの有能さが際立つ結果となりました!


7.足し算、引き算にチャレンジ

ここからはクイズ形式で進めていきます。

類似度が一番高いワードを予想してみてください。

答えがすぐ見えてしまうは、ご愛嬌ということで(^^;


①初級編

「ラーメン」 + 「北海道」 = 「???」

北海道のご当地ラーメンといったらもちろん!

word2vec_ramen_model.most_similar(positive=[u"ラーメン",u"北海道"])

>>>
[('味噌ラーメン', 0.5989491939544678),
('らーめん', 0.5617096424102783),
('札幌', 0.5409911870956421),
('たぐ', 0.5338827967643738),
('ラーメン屋', 0.5107439756393433),
('醤油ラーメン', 0.5045751929283142),
('こちら', 0.5027260780334473),
('東京', 0.49508172273635864),
('全日空', 0.4923136830329895),
('カリィ', 0.48750197887420654)]

正解は味噌ラーメンです。


②中級編

「ラーメン」 + 「北極」 = 「???」

激辛ラーメンの定番といえば、これ!

真冬に食べても超汗だくになる強烈なラーメンです。

word2vec_ramen_model.most_similar(positive=[u"ラーメン",u"北極"])

>>>
[('中本', 0.5654767751693726),
('これ', 0.5430104732513428),
('17回', 0.5270459651947021),
('激辛', 0.5181963443756104),
('walker', 0.5173792243003845),
('辛さ', 0.5158922672271729),
('辛い', 0.5034847259521484),
('挑戦', 0.49608075618743896),
('権威', 0.48732683062553406),
('420g', 0.4867885708808899)]

そうです、正解は「中本」でした!

「北極」といえば「中本」と反射的に答えられたあなたはガチ勢の素質ありです。

蒙古タンメン中本:https://tabelog.com/tokyo/A1322/A132203/13004380/

スクリーンショット 2019-03-04 1.38.54.png

ちなみに、北極ラーメンから「激辛」を取り除いてみると・・・

「ラーメン」 + 「北極」 ー 「激辛」 = 「???」

word2vec_ramen_model.most_similar(positive=[u"ラーメン",u"北極"], negative=[u"激辛"])

>>>
[('ラーメン屋', 0.5003402233123779),
('ラーメン店', 0.492368221282959),
('17回', 0.4728690981864929),
('award', 0.4705893397331238),
('人気', 0.47028398513793945),
('激戦', 0.463814914226532),
('ジョー', 0.441331684589386),
('カリィ', 0.44119763374328613),
('成立', 0.4396098256111145),
('専門店', 0.4389714002609253)]

なんと、普通のラーメン屋に成り下がってしまいました(´・ω・`)


③上級編

「六厘舎」 - 「東京ラーメンストリート」 + 「松戸」 = 「???」

私は、3時間並びました。

word2vec_ramen_model.most_similar(positive=[u"六厘舎",u"松戸"], negative=[u"東京ラーメンストリート"])

>>>
[('富田', 0.46717607975006104),
('平井', 0.4340380132198334),
('選手権', 0.38690489530563354),
('圧倒的', 0.3818015456199646),
('入れ替え', 0.3796331286430359),
('開幕', 0.3761281669139862),
('ドロ', 0.3747977018356323),
('プロデュース', 0.3694424629211426),
('からい', 0.3633619248867035),
('最強', 0.3624943792819977)]

正解は「富田」でした!

「正式名称は"とみ田"でしょ」というツッコミが入りそうですね。

解説:つけ麺界のカリスマ的存在「六厘舎」@東京ラーメンストリートから、「東京ラーメンストリート」ベクトルを引いて「松戸」ベクトルを加えると、松戸におけるつけ麺界のカリスマ的存在「とみ田」となります。

中華蕎麦 とみ田:https://tabelog.com/chiba/A1203/A120302/12000422/

↑の結果がちょっと微妙なのは、今回のコーパスは東京ラーメンの口コミのみで作成しているため、とみ田@松戸についてのデータが足りなかったのだと思われます。


8.まとめ

今回は名店ラーメン屋の口コミデータを使用してword2vecで遊んでみました。

名店の口コミならではの"エッジ"の効いたモデルができて興味深い結果を得ることができましたが、モデルの良し悪しはらーめんガチ勢にしか判定できないのが悩みどころです。

記事の冒頭で、

「ラーメン」+「豚骨」+「ヤサイタワー」=「二郎」

になるのでは?と推測しましたが、全然うまくいきませんでした。原因は、「二郎」は「ラーメン」にも「豚骨」にも類似していなかったためです。


「二郎はラーメンではなく、二郎という食べ物である。」


という名言は、本当にそうなのかもしれませんね。

以上。

次回は、今回作成したラーメンモデルを応用して、隠れた名店をレコメンドで発掘にチャレンジします。


9.仲間を探しています

余談ですが、私の隣の席で一緒に機械学習タスクをガシガシ取り組める素敵な方を探しております。新興であるスマートロックというハードウェアの上で、これまでにない多様なAI・機械学習の可能性を模索しませんか?

AI・機械学習・バイオメトリクスエンジニア

またBitkeyでは、AI・機械学習領域に限らず様々なポジションで一緒に働ける仲間を募集しています。もし興味がある方はお気軽にご一報ください。

募集ポジション一覧