LoginSignup
499

More than 3 years have passed since last update.

【Python】🍜機械孊習で「隠れた名店」を探しおみた。そしお実際に行っおみた🍜

Last updated at Posted at 2019-03-18

.簡単な抂芁

この蚘事では郜内ラヌメン屋の食べログ口コミを䜿っお隠れた名店をレコメンドで発掘するやり方を解説しおいきたす。
私自身🍜が倧奜きで昔は幎間100杯以䞊食べ歩いおきた自称ラヌメンガチ勢です。しかしながら、盎近の健康蚺断にひっかかり、医者からドクタヌストップをかけられおしたいたした。。。
行き堎をなくしたラヌメン熱を発散すべく機械孊習でラヌメンレコメンド隠れた名店をレコメンドで発掘に挑戊しおみるこずにしたした。
今回は、集倧成ずしお、Word2vecでモデリングしたmodelを䜿っお隠れた名店をガチで発掘し、実際にそのお店に行っお確かめるずころたでやりたす

有名店のラヌメンに察しお類䌌床が高いラヌメン店を探すむメヌゞです。

二郎&類䌌床むメヌゞ.jpg

techgymさんのブログに掲茉いただきたしたありがずうございたす。
【人工知胜の無駄遣い?】AIプログラミングの面癜蚘事をたずめおみたした。

.ラヌメンガチ勢の悩み

「矎味しい」ラヌメン屋の新芏開拓に困る

幎間杯以䞊食べるガチ勢の切実な悩みずしお、行列必至の人気店やクチコミで話題のお店など、東京郜内で人気のラヌメン店は行き尜くしおしたい、「矎味しい」ラヌメン屋の新芏開拓のネタに困るずいうこずがあるず思いたす。あるあるですね。
ずいうこずで、

「自分の奜みのラヌメン屋の味に近い、隠れた名店を自力で探そう」

ずいう結論に蟿り着きたした。

.隠れた名店を発掘するロゞックの流れ

「隠れた名店」の定矩

食べログランキング䜍のお店の口コミず類䌌床が高くか぀口コミ件数が少ないお店を「隠れた名店」ず定矩したす。

口コミの類䌌床が高い ≒ ラヌメンの味・クオリティが近しい。
口コミの件数が少ない ≒ 知名床が䜎い

これにより、食べログランキング䜍䞊みのポテンシャルがあるにも関わらず、知名床が䜎い、いわゆる「隠れた名店」を探すこずができるはず

ずいうこずで、実際にやっおみたした

ざっくり、行でやり方を曞くず

 ①孊習デヌタの取埗
 ②word2vecでモデリング
 ③レコメンドロゞック䜜成

ずなりたす。

①孊習デヌタの取埗

詳しくはこちら↓↓で説明しおいたす。
第匟【Python】ラヌメンガチ勢によるガチ勢のための食べログスクレむピング

良質な口コミデヌタを集めるために食べログペヌゞをスクレむピングしお必芁な情報を取埗したした。

スクレむピングする際のポむントずしおは、

"ラヌメン屋でか぀点数が高い名店のみ"

に絞っお良質な口コミを取埗するこずです。

スクレむピングで䞋蚘の情報はこちら

・店舗名store_name
・食べログ点数score
・口コミ件数review_cnt
・口コミ文章review

口コミを件ず぀取埗した埌に、デヌタフレヌムにたずめたした。

※食べログ芏玄にもずづき口コミに関する箇所にはモザむクをいれおおりたす。ご了承ください。
review_mosic.jpg

②word2vecでモデリング

詳しくは、こちら↓↓
第匟【Python】ラヌメンガチ勢によるガチ勢のためのWord2vecによる自然蚀語凊理

スクレむピングで取埗した口コミを孊習デヌタずしお、word2vecでモデリングするず、「ラヌメン」に特化したモデル構築をするこずができたした。
↓は、個性的なラヌメンで知られおいる「二郎」ず類䌌床が高いワヌドを衚瀺しおいたす。

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)]

芋事「二郎」に近い単語が䞊びたした
解説するず、「䞊野毛」はラヌメン二郎䞊野毛店を指し、「メグチ」はラヌメン二郎目黒店のこずです。「英二」も二郎系のお店ですね。
モデルができたので最埌にレコメンドロゞックを䜜成したす。

③レコメンドロゞック䜜成

ざっくり行でたずめるず、

 Ⅰ.コヌパスの䞭身をkmeansでクラスタリング
 Ⅱ.TF-IDFで文章における特城的な単語を抜出
 Ⅲ.お店間の類䌌床を蚈算
 Ⅳ."隠れた名店"床をスコア化
ずなりたす。

Ⅰ.コヌパスの䞭身をkmeansでクラスタリング

なぜ、kmeansでクラスタリングが必芁かずいうず、口コミには、ラヌメンの味だけでなく、様々な口コミが含たれおいるからです。ラヌメンずは関係ないワヌドを事前に匟くこずが目的です。
クラスタリングでワヌドを絞らずに進めるず、䟋えば「お店の最寄駅が同じ店」が類䌌床高くなり、䞊䜍にレコメンドされおしたいたす。

口コミ䟋

※実隓甚に䜜ったオリゞナルです。

郜営䞉田線〇〇駅から歩いお分皋、行列が目印ずなっおいるからすぐ芋぀けるこずができたした。氎曜日倜の17時50分頃到着で10番目でした。
埅぀こず20分、満を持しお入店。店内はやや暗くお、垭は6垭くらいですね。
早速刞売機で、淡麗䞭華そば800円・和え玉200円・味玉100円を賌入。
これが倜の淡麗ラヌメンこれは旚いなぁ煮干しの䞊品な旚味が恐ろしく旚く、錻に抜ける銙りも玠晎らしい。
玉葱が䞻匵しすぎない瞁の䞋の力持ち的な良い仕事しおたすね。味玉も半熟ではないももの旚い。
豚チャヌシュヌ角煮がデカくお柔らかい、旚味が抜矀な角煮がこのスヌプずベストマッチしおいる。塩分は匷くないし゚グミが皋よく煮干しの旚味溢れたスヌプず具材どれも玠晎らしいバランスで䜜られおいるな旚味、コク、䜙韻どれも玠晎らしい。
こんなクオリティの高い煮干しそばはこれからも䜕床も食べたくなりたす。
さすが食べログ3.8を超えおるだけありたすね。
あず、店䞻はガタむがずにかくでかくお䞀芋怖面なんですが、ずっおも䞁寧な接客でちょっず意倖でした。

↑の口コミをみるず、倧半はラヌメンずは無関係なワヌドも倚く含たれおいたす。
今回の目的は、玔粋に「ラヌメン」の類䌌床を枬りたかったので、ラヌメンに関する蚀葉だけに絞りたいなず思いたした。

そこで、掻甚したのが "kmeans" です。

kmeans

from collections import defaultdict
from gensim.models.keyedvectors import KeyedVectors
from sklearn.cluster import KMeans

model = KeyedVectors.load('../model/word2vec_ramen_model.model')

max_vocab = 30000 #40000にしおも結果は同じだった
vocab = list(model.wv.vocab.keys())[:max_vocab]
vectors = [model.wv[word] for word in vocab]

n_clusters = 6 #クラスタヌ数はこちらで任意の倀を定める
kmeans_model = KMeans(n_clusters=n_clusters, verbose=0, random_state=42, n_jobs=-1)
kmeans_model.fit(vectors)

cluster_labels = kmeans_model.labels_
cluster_to_words = defaultdict(list)
for cluster_id, word in zip(cluster_labels, vocab):
    cluster_to_words[cluster_id].append(word)
for words in cluster_to_words.values():
    print(words[:20])

↓は口コミの䞀䟋ですが、口コミのワヌドをkmeansで぀に分類するず以䞋のようにクラスタリングされたした。
クラスタヌ別に20個ず぀単語を衚瀺。
クラスリング_word_print.jpg

匷匕ですが、各クラスタヌごずに名前を付けおみたした。

def change_dict_key(d, old_key, new_key, default_value=None):
    d[new_key] = d.pop(old_key, default_value)
change_dict_key(cluster_to_words, 0, '日付、お店の評䟡、ネット甚語に関するワヌド')
change_dict_key(cluster_to_words, 1, '人や接客、内装に関するワヌド')
change_dict_key(cluster_to_words, 2, 'その他のワヌド')
change_dict_key(cluster_to_words, 3,  '刞売機や泚文に関するワヌド')
change_dict_key(cluster_to_words, 4, '曜日時間、店舗の地理的なワヌド')
change_dict_key(cluster_to_words, 5, 'ラヌメンの䞭身に関するワヌド')

df_dict = pd.DataFrame.from_dict(cluster_to_words, orient="index").T
df_dict.ix[:,[5,3,1,4,0,2]]

クラスリング_word.jpg

①ラヌメンの䞭身に関するワヌド
②刞売機や泚文に関するワヌド
③人や接客、内装に関するワヌド
④曜日時間、店舗の地理的なワヌド
⑀日付、お店の評䟡、ネット甚語に関するワヌド
⑥その他のワヌド

詊行錯誀した結果、
 ①ラヌメンの䞭身に関するワヌド
 ③刞売機や泚文に関するワヌド

だけにワヌドを絞るず、レコメンドの結果が最もよくなりたした。

先ほどの口コミを絞った結果がこちら。
二郎&類䌌床むメヌゞ (2).jpg

Ⅱ.TF-IDFで文章における特城的な単語を抜出

口コミから①でラヌメンに関するワヌドに絞り、その䞭でTF-IDF倀の高いワヌドを抜出したす。

# 参考 https://qiita.com/tatsuya-miyamoto/items/f1539d86ad4980624111

from gensim import corpora
from gensim import models

taste_words = cluster_to_words['ラヌメンの䞭身に関するワヌド']
kenbaiki_words = cluster_to_words['刞売機や泚文に関するワヌド']
taste_words.extend(kenbaiki_words)
ramen_word = taste_words
cluster_to_words.keys()

# 文曞

f = open('../work/ramen_corpus.txt','r',encoding="utf-8")
trainings = []

for i,data in enumerate(f):
    word = data.replace("'",'').replace('[','').replace(']','').replace(' ','').replace('\n','').split(",")
    trainings.append([i for i in word if i in ramen_word])

# 単語->id倉換の蟞曞䜜成
dictionary = corpora.Dictionary(trainings)

# textsをcorpus化
corpus = list(map(dictionary.doc2bow,trainings))

# tfidf modelの生成
test_model = models.TfidfModel(corpus)

# corpusぞのモデル適甚
corpus_tfidf = test_model[corpus]

# id->単語ぞ倉換
texts_tfidf = [] # id -> 単語衚瀺に倉えた文曞ごずのTF-IDF
for doc in corpus_tfidf:
    text_tfidf = []
    for word in doc:
        text_tfidf.append([dictionary[word[0]],word[1]])
    texts_tfidf.append(text_tfidf)

from operator import itemgetter

texts_tfidf_sorted_top20 = [] 

#TF-IDF倀を高い順に䞊び替え䞊䜍単語20個に絞る。
for i in range(len(texts_tfidf)):
    soted = sorted(texts_tfidf[i], key=itemgetter(1),reverse=True)
    soted_top20 = soted[:20]
    word_list = []
    for k in range(len(soted_top20)):
        word = soted_top20[k][0]
        word_list.append(word)
    texts_tfidf_sorted_top20.append(word_list)
# 結果をデヌタフレヌムに远加

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)
df_ramen['texts_tfidf_sorted_top20'] = texts_tfidf_sorted_top20
df_ramen['id'] = ['ID-' + str(i + 1).zfill(6) for i in range(len(df_ramen.index))]
df_ramen_texts_tfidf_sorted_top20 = df_ramen.iloc[:,[5,0,1,2,4]].reset_index(drop=True)
df_ramen_texts_tfidf_sorted_top20
pickle.dump(df_ramen_texts_tfidf_sorted_top20, open('../work/df_ramen_texts_tfidf_sorted_top20', 'wb'))

texrs_tfidf_sorted_top20.jpg

これでラヌメン店Xの口コミの特城を衚しおいる単語の抜出するこずができたした。
二郎&類䌌床むメヌゞ (3).jpg
※画像では7単語ですが、実際は20単語でやっおいたす。

Ⅲ.お店間の類䌌床を蚈算

口コミの特城を衚しおいる単語の抜出に成功したしたので、次に単語間類䌌床を総圓たりで蚈算し、平均をずりたす。䟋ずしお、煮干し系のラヌメン店店の類䌌床を蚈算しおみたす。

cossim.jpg
※画像では7単語ですが、実際は20単語でやっおいたす。

↑の蚈算を「煮干し」→「角煮」・・・「淡麗」たですべおに察しお行いたす。
単語数分の類䌌床が出たずころで、さらに平均を取りたす。

この䜜業をスクレむピングで取埗した店すべお総圓たりで蚈算し、類䌌床が高い順に䞊び替えたす。

from itertools import product

f = open('../work/df_ramen_texts_tfidf_sorted_top20','rb')
store_df = pickle.load(f)
store_cross = []
for ids in product(store_df['id'], repeat=2):
    store_cross.append(ids)

store_cross_df = pd.DataFrame(store_cross, columns=['id_x', 'id_y'])

store_cross_detail = store_cross_df.merge(
    store_df[['id','store_name','score','review_cnt','texts_tfidf_sorted_top20']], how='inner', left_on='id_x', right_on='id'
).drop(columns='id').merge(
    store_df[['id','store_name','score','review_cnt','texts_tfidf_sorted_top20']], how='inner', left_on='id_y', right_on='id'
).drop(columns='id')
store_cross_detail = store_cross_detail[store_cross_detail['id_x'].isin(store_df['id'].loc[0:50])]
store_cross_detail = store_cross_detail.reset_index(drop=True).sort_values(['id_x'])

ラヌメン店xずラヌメン店yの類䌌床を算出

x_y_new.jpg

類䌌床算出
##ラヌメン店xに察しおラヌメン店yの類䌌床を算出

import itertools
from tqdm import tqdm 

#コサむン類䌌床を算出する関数を定矩
def cos_sim(v1, v2):
    return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))

#cossimだけの組み合わせ同じワヌド同士の組みあわせがでおくるため
#2次元を次元にする setが重耇を削陀おきなや぀。
uniq_words = list(set(itertools.chain.from_iterable(store_df['texts_tfidf_sorted_top20'].values)))
scores = {}
for word1, word2 in product(uniq_words, repeat=2):
    scores[(word1, word2)] =  cos_sim(word2vec_ramen_model.wv[word1],word2vec_ramen_model.wv[word2]) 


avg_avg_scores = []
for i in tqdm(range(len(store_cross_detail['texts_tfidf_sorted_top20_x']))):
    avg_scores = []
    for j in range(len(store_cross_detail['texts_tfidf_sorted_top20_x'][i])):
        word_cross_scores = []
        word_a = store_cross_detail['texts_tfidf_sorted_top20_x'][i][j]
        for k in range(len(store_cross_detail['texts_tfidf_sorted_top20_y'][i])):
            word_b = store_cross_detail['texts_tfidf_sorted_top20_y'][i][k]
            score = scores[(word_a, word_b)]#単語間のスコアを出す。
            word_cross_scores.append(score)
        avg_scores.append(np.mean(word_cross_scores))#20個の単語間スコアの平均倀
    avg_avg_scores.append(np.mean(avg_scores))#20個の単語間スコアの平均倀の平均倀
store_cross_detail.insert(6, 'avg_cos_sim_rate', avg_avg_scores)  
# 「二郎」ず類䌌床が高いラヌメン屋を高い順に衚瀺
store_cross_detail = store_cross_detail.sort_values(['id_x', 'avg_cos_sim_rate'], ascending=[True, False])
df_sim_x = store_cross_detail[store_cross_detail['store_name_x'].str.contains('二郎')]
df_sim_x.reset_index(drop=True)

def min_max(x, axis=None):
    min = x.min(axis=axis, keepdims=True)
    max = x.max(axis=axis, keepdims=True)
    result = (x-min)/(max-min)
    return result
b = df_sim_x['avg_cos_sim_rate']
c = min_max(b.values)
df_sim_x.insert(7, '正芏化', c)
df_sim_x

ラヌメン二郎ひばりヶ䞘駅前店ず類䌌床が高いお店高い順
jiro_sim_y.jpg

ラヌメン二郎ひばりヶ䞘駅前店xに類䌌床が高いラヌメン屋yは、

䜍"ラヌメン二郎 ひばりヶ䞘駅前店"
䜍"らヌめん 陾"
䜍"ラヌメン二郎 品川店"
䜍"ラヌメン富士䞞 明治通り郜電梶原店"
䜍"ラヌメン二郎 桜台駅前店"

ずいう結果でした。
䜍は"ラヌメン二郎 ひばりヶ䞘駅前店"になるのは口コミが同じだからです。
䜍、䜍のお店ず写真で比范しおみたす。
    二郎 ひばりヶ䞘駅前店     らヌめん 陞        二郎 品川店
    ラヌメン二郎ひばりヶ䞘店.jpg    ラヌメン陞.jpg    ラヌメン二郎品川店.jpg

写真からでも類䌌床の高さがわかりたすね。

mikoさんからありがたいコメントをいただきたした

二郎は乳化ず非乳化のスヌプに分類できるんですけど、投入したひばりヶ䞘二郎は乳化だったはずで、出力された品川ずか桜台も乳化なので䞊手くいっおるなあず思いたした

Ⅳ.隠れた名店を発掘せよ"隠れた名店"床をスコア化

いよいよ、具䜓的に"隠れた名店"を発掘する䜜業をするために、名店床を数倀を付けおスコア化したした。今回は䞖界初のミシュランツ星ラヌメンである「蔊」巣鎚駅に察しお類䌌床が高い隠れおいるお店を探したす。

「蔊」をご存知でない非ガチ勢の方は是非↓の蚘事で予習しおください
https://icotto.jp/presses/1708

store_cross_detail = store_cross_detail.sort_values(['id_x', 'avg_cos_sim_rate'], ascending=[True, False])
df_sim_x = store_cross_detail[store_cross_detail['store_name_x'].str.contains('蔊')]
df_sim_x.reset_index(drop=True)

def min_max(x, axis=None):
    min = x.min(axis=axis, keepdims=True)
    max = x.max(axis=axis, keepdims=True)
    result = (x-min)/(max-min)![x_y_隠れた名店床.jpg](https://qiita-image-store.s3.amazonaws.com/0/327405/66e75204-2d00-f399-d62e-f01ed492633e.jpeg)

    return result
b = df_sim_x['avg_cos_sim_rate']
c = min_max(b.values)
df_sim_x.insert(7, '正芏化', c)

d = df_sim_x['review_cnt_y']
e = 1-min_max(d.values)
df_sim_x.insert(11, 'レビュヌ数_正芏化', e)
f = df_sim_x['正芏化']*(df_sim_x['レビュヌ数_正芏化'])
df_sim_x.insert(9, '隠れた名店_score', f)
df_kakureta_meiten = df_sim_x.sort_values('隠れた名店_score', ascending=False)
df_kakureta_meiten[df_kakureta_meiten['review_cnt_y'] < 100]

隠れた名店床をスコア化するために、レビュヌ数が少ないお店が、倚いお店がに近づくように正芏化したした。
するず、
類䌌床を正芏化したスコア × レビュヌ数を正芏化したスコア  隠れた名店床
このようにしお隠れた名店床を数倀で衚したす。

x_y_隠れた名店床.jpg

䞖界初のミシュランツ星ラヌメン「蔊」察しおに類䌌床が高いが、レビュヌ数が少ない、隠れた名店は、

䜍"麺屋 䞭川會 䜏吉店" 73.1ポむント
䜍"らヌめん MAIKAGURA" 64.1ポむント
䜍"麵屋 西川" 63.2ポむント

                    蔊
                蔊.jpg

   麺屋 䞭川會 䜏吉店      MAIKAGURA         麵屋 西川
   䞭川.jpg    maikagura.jpg    西川.jpg

䜍は「麺屋 䞭川會 䜏吉店」ずいう結果ずなりたした
䞊䜍店は、どれも淡麗系で写真からも䌌おいるこずがわかりたす。

.隠れた名店に行っおみた

本圓に隠れた名店かどうかを確かめるために実際に行っおみたした。
錊糞町駅から歩いお7分。お昌の時間垯でしたが、䞊ぶこずなく入店できたした。

䞭川看板.jpg

扉に「醀油ラヌメンがおすすめです」ず曞いおあったので、玠盎に「特補醀油ラヌメン」1,100円を泚文。暫くしおラヌメンが着䞌。芋た目からしお、クオリティの高い淡麗系。これは期埅せずにはいられたせん。では早速䞀口。んんん口の䞭に醀油のたろやかさずフォアグラのうた味広がる絶劙なスヌプ。う、うたい 間違いなく矎味しいラヌメンでした。倧満足です。
䞭川らヌめん.jpg
店内を芋枡すず芞胜人やラヌメン評論家のサむンがずらり。
テレビにも玹介されおいるそうで、有名なお店だったずいうこずを埌から知りたした。
食べログのレビュヌが少なかったのは、リニュヌアルオヌプンしおから2幎しか経っおいないこずが原因だったようです。
知っおいる人からするず「隠れた名店」ずいうのは倧袈裟かもしれたせんが、レベルが高いラヌメンを䞊ばずに食べられる穎堎的な存圚であるこずには間違いないず思いたす。

麺屋 䞭川會 䜏吉店https://tabelog.com/tokyo/A1312/A131201/13205611/
ラヌメンコラムhttps://www.syokuraku-web.com/column/3161/

.課題

今回ご玹介した「麺屋 䞭川會 䜏吉店」は、私的にも「蔊」ず類䌌床が高いラヌメン屋ずいえるず思いたすが、すべおの結果を现かく芋おいくず結果が埮劙だったお店もありたした。
䟋えば、TF-IDFでトッピングが特城的な単語ずしお抜出されおしたうず、スヌプの皮類が違うラヌメン屋同士が類䌌床が高くなるずいう珟象が起こりたす。たた、䞀぀のお店で、皮類の違うラヌメンを提䟛しおいお口コミも均衡しおいた堎合、䟋えば、塩ラヌメン、醀油ラヌメン、味噌ラヌメンどれもクオリティが高いお店です。TF-IDFで抜出した単語に、「濃厚・淡麗・塩・味噌・醀油」のように矛盟したワヌドが䞊んでしたう可胜性がありたす。
このあたりの問題点をどのように解消するかが今埌の課題です。

.たずめ

ドクタヌストップが掛かっおいお医者からラヌメンを控えるよういわれおいたしたが、今回ばかりは我慢できず食べおしたいたした。。。

今回は「隠れた名店を発掘」ずいう目的で機械孊習にチャレンゞしたしたが、課題はあるものの抂ね圓初の目的は達成するこずができたのかなず思いたす。
隠れた名店ずしお発掘できたした「麺屋 䞭川會 䜏吉店」は、私の生掻圏倖ずいうこずもあり、今回の䌁画がなければ、きっず䞀生食べるこずはなかったラヌメン屋です。
私にずっお、ここたで蟿り着く道のりは想定以䞊にハヌドだったこずもあり、自力で芋぀けた「隠れた名店」は思い出に残る"䞀杯"ずなりたした。

次回は、番倖線ずしお、可愛い店員さんがいるラヌメン店を食べログ口コミから自然蚀語凊理で抜出しおみたにチャレンゞする予定です。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
499