標籤:

如何科學地「學習」中餐菜譜

大概過年的時候圍觀了一篇這樣的文章: food2vec - Augmented cooking withmachine intelligence,講了一個把食物向量化的一個應用,還挺好玩的。

大天朝物產豐富,在吃的方面絕對要比世界人民領先好幾個檔次,當然也是可以搞一搞這個的。

數據預處理

我先找到一個叫《菜譜大全》的文本文件,處理了一下,命名為「recipe.txt」。

涉及中文字元處理,先把所有的字元串轉成Unicode:

from __future__ import unicode_literals

中文文件的編碼向來是個大坑,這裡我把文件轉成了比較通用的UTF-8格式編碼。可以用codecs模塊指定編碼格式來讀取文件:

import codecswith codecs.open("recipe.txt", encoding="utf8") as f: raw_data = f.read().strip().split("
")

我對數據做了一些簡單的預處理,現在的數據長這個樣子:

for line in raw_data[:2]: print line【菜名】板栗燒雞【所屬菜系】東北菜【特點】雞肉酥爛,板栗香甜,時令佳肴,美味可口【原料】活雌雞或閹雞1隻,約重1500~2000克,菜油(或化豬油)100克,豆瓣25克,老薑50克,大蔥10克,白糖或冰糖25克,花椒、料酒、醬油、精鹽、味精、八角等適量。【製作過程】將雞宰殺、拔毛、剖腹去內臟洗凈,把雞頭、翅膀和腳至脛部切下,然後將雞對剖開,將雞肉斬成長3厘米、寬2厘米的長方塊,把雞頭、翅膀和腳也斬成3厘米的段。鍋置旺火上,下100克菜油燒熱,然後將雞塊入熱油鍋中爆炒,待雞肉變硬時,加入料酒及姜塊、豆瓣、花椒,炒至水分漸干溢出香味時,即摻入適量水,放入少量鹽、醬油和白糖、八角等。加蓋燜燒至六七成熟時,再加入板栗同燒15分鐘左右即可。起鍋時加入蔥段及味精,有少量湯汁為宜。【菜名】鴛鴦戲飛龍【所屬菜系】東北菜【特點】【原料】飛龍肉200克,雞脯肉50克,口蘑、蛋清、火腿、菜心適量。【製作過程】1.飛龍肉切薄片,用蛋清糊上漿,下開水鍋汆透撈出;2.用150克蛋清攪成蛋泡糊,雞脯肉製成茸,加在蛋泡糊中拌勻,倒在模子中成鴛鴦形,用紅、綠辣椒飾嘴、眼及翅膀,上籠蒸熟取出;3.飛龍片、火腿片、鮮蘑、油菜心下雞清湯中燒開撈入碗中,余湯燒開撇凈浮沫,調好味後倒入湯碗,再放入蒸好的鴛鴦即成。

每一行是一個數據,包括菜名、菜系、特點、原料、做法等細節。

看一眼有多少個菜譜:

print len(raw_data)4577

先來定義一個函數處理每一條數據,每行數據規律性還挺明顯的,這裡我用正則表達式提取相關信息:

import redef process_single_data(line): pattern = r"【菜名】([^【]*)【所屬菜系】([^【]*)【特點】([^【]*).*【原料】([^【]*).*【製作過程】([^【]*).*" match = re.search(pattern, line) if match: return {"菜名": match.group(1), "菜系": match.group(2), "特點": match.group(3), "原料": match.group(4), "過程": match.group(5)} print "處理下列數據出現錯誤:", line

隨便處理一行試試:

sample =process_single_data(raw_data[1105])for k in sample: print k, sample[k]特點 菜名 香露全雞過程 1.將雞治凈,從背部剖開,再橫切3刀,雞腹向上放入燉缽,鋪上火腿片、香菇,加入調料、雞湯。2.缽內放入盛有高粱酒、丁香的小杯,加蓋封嚴,蒸2小時後取出缽內小杯即成。菜系 閩菜原料 肥嫩母雞1隻,水發香菇2朵,火腿肉2片,高粱酒50克,雞湯750克,丁香子5粒

對所有的數據進行處理:

data = map(process_single_data, raw_data)

在建模之前,我們先看看數據的分布。

涉及畫圖,我們先導入必要的畫圖工具:

import numpy as npimport matplotlib.pyplot as plt%matplotlib inline

而且需要解決不顯示中文的問題:

plt.title("中文")plt.show()

顯示不出中文是因為它找不到合適的字體去顯示。

為此,我們可以使用FontProperties指定字體的路徑來生成一個字體對象,然後畫圖的時候指定字體。

我個人比較喜歡簡單粗暴做法:直接找字體文件,放在當前文件夾用,比如宋體:

from matplotlib.font_manager import FontPropertiesfont_song = FontProperties(fname="SIMSUN.TTC")plt.title("中文", fontproperties=font_song)plt.show()

菜系統計

先看看有哪些菜系和數目,計數這種任務交給Counter就好了:

from collections import Counterdata_tps = [s["菜系"] for s in data]tps_cts = Counter(data_tps)for k, v in tps_cts.items(): print k, v滬菜 87浙江菜 1114韓國 310湘菜 75江蘇菜 249滿漢全席 44微波爐菜 85日本料理 11海派菜 19魯菜 285川菜 352全部 1323雲南菜 18淮陽菜 34東北菜 52閩菜 114法國名菜 53京菜 123粵菜 195其他西餐 34

然後畫個圖:

from matplotlib import cm_, ax =plt.subplots(figsize=(8, 8))ax.pie([v for k, v in tps_cts.items()], labels=[k for k, v in tps_cts.items()], colors=cm.Vega20.colors)ax.axis("equal")for t in ax.texts: t.set_font_properties(font_song)plt.show()

這裡,類別為全部大概是家常菜的意思吧(我猜的)。

食材統計

食材統計是個比較麻煩的事情,先隨便看看食材的數據長什麼樣子:

for i in range(0, 4000, 400): print data[i]["原料"]活雌雞或閹雞1隻,約重1500~2000克,菜油(或化豬油)100克,豆瓣25克,老薑50克,大蔥10克,白糖或冰糖25克,花椒、料酒、醬油、精鹽、味精、八角等適量。燕窩(40克)、椰子(1個,750克)、味粉(15克)、雞湯(250克)、精鹽(少許)、小蘇打粉(少許)活鱔魚350克。蝦仁50克、熟火腿20克、豬腰片50克、水發冬菇20克,水發蝦米10克、熟筍片30克…蝦子5克。豬肉湯750克、紹酒15克、蔥15克、姜7.5克、鹽5克、味精3克、白鬍椒粉1.5克。光鴨(1隻,1500克左右)、蔥段(20克)、薑片(20克)、桂皮(20克)、茴香(13克)、紅米(8克)、黃酒、冰糖(130克)、白醬、鹽、麻油節瓜600克(約1斤),草菇50克(約1兩半),蝦仁100克(約2兩半),蟹肉75克(約2兩),蛋白1隻,上湯400毫升,姜1片,蔥1條。鹽1茶匙,糖1/4茶匙,粟粉、酒各1茶匙,水1湯匙,胡椒粉少許。嫩母雞(約1.25公斤左右)1隻,胡蘿蔔100克,芹菜50克,蔥頭10O克,精鹽10克,香葉半片,胡椒6粒,雞清湯250毫升,生菜油500(罕耗100克),奶油15克,雞油15克,紅白菜200克,酸黃瓜100克,鮮西紅柿100克,紙花2個,蘿蔔花2個,生菜葉10克。水發魷魚(325克)、京蔥花(40克)、大蒜頭片(10片)、黃酒(12.5克)、醋(少許)、鹽(6克)、味精(少許)、醬瓜米(少許)、菱粉(45克)、清湯(150克)。鮮魷魚肉12兩(約480克),西芹一條。西芹調味料:鹽1/8茶匙,麻油少許。魷魚調味料:鹽1/4茶匙,胡椒粉少許。椒麻汁料:花椒粒1茶匙,姜茸2茶匙,青蔥茸2湯匙,糖2/3茶匙,醋,麻油各1茶匙,生抽,開水各2湯匙。原料:麵條500克,菠菜或小白菜300克,青椒(甜)3個,榨菜25克。蔥10克,蒜1頭,醬油10克,香醋5克,精鹽3克,味精2克,香油15克,高湯適量。主料扁豆150克,雞油50克。調料料酒25克,味精5克,濕澱粉15克,雞湯150克。

為了搞到食材,我們可以想辦法去掉裡面的一些關鍵詞,比如多少克,少許,越重以及標點符號:

def process_single_ingredient(line): # 標點符號替換為空格 line = re.sub("[。,、/…~~:;:%]", " ", line) + " " line = re.sub("([^(]*)", " ", line) # 阿拉伯數字替換為空格 line = re.sub("d+S* ", " ", line) # 漢語數字替換為空格 line = re.sub("[一二兩三四五六七八九十幾半]+S* ", " ", line) # 關鍵詞替換為空格 for s in ["少許", "適量", "或", "等", "重", "約", "各", "原料", "調味料", "主料", "輔料", "調料", "用料", "和", "及"]: line = re.sub(s, " ", line) # 字母替換為空格 line = re.sub(" w+ ", " ", line) line = re.sub(" +", " ", line).strip() return linefor i in range(0, 4000, 400): print process_single_ingredient(data[i]["原料"])活雌雞 閹雞 菜油 豆瓣 老薑 大蔥 白糖 冰糖 花椒 料酒 醬油 精鹽 味精燕窩 椰子 味粉 雞湯 精鹽 小蘇打粉活鱔魚 蝦仁 熟火腿 豬腰片 水發冬菇 水發蝦米 熟筍片 蝦子 豬肉湯 紹酒 蔥 姜 鹽 味精 白鬍椒粉光鴨 蔥段 薑片 桂皮 茴香 紅米 黃酒 冰糖 白醬 鹽 麻油節瓜 草菇 蝦仁 蟹肉 蛋白 上湯 姜 蔥 鹽 糖 粟粉 酒 水 胡椒粉嫩母雞 胡蘿蔔 芹菜 蔥頭 精鹽 香葉 胡椒 雞清湯 生菜油 奶油 雞油 紅白菜 酸黃瓜 鮮西紅柿 紙花 蘿蔔花 生菜葉水發魷魚 京蔥花 大蒜頭片 黃酒 醋 鹽 味精 醬瓜米 菱粉 清湯鮮魷魚肉 西芹 西芹 鹽 麻油 魷魚 鹽 胡椒粉 椒麻汁料 花椒粒 姜茸 青蔥茸 糖 醋 麻油 生抽 開水麵條 菠菜 小白菜 青椒 榨菜 蔥 蒜 醬油 香醋 精鹽 味精 香油 高湯扁豆 雞油 料酒 味精 濕澱粉 雞湯

這樣結果就看起來不錯的樣子了,我們來做一下統計:

data_ings = map(lambda x: process_single_ingredient(x["原料"]), data)data_ings_all = " ".join(data_ings).split()ings_cts = Counter(data_ings_all)

不出意外的話,排在前面的原料應該是各種調味品:

for k, v in ings_cts.items(): if v > 200: print k, v醬油 1288花椒 252雞蛋清 299雞湯 399料酒 1048薑末 284水澱粉 254味精 2049姜 799麻油 351香油 460醋 434紹酒 808精鹽 1520胡椒粉 801濕澱粉 619白糖 1140芝麻油 434蔥 948鹽 1452植物油 274油 277雞蛋 506熟豬油 325澱粉 387蔥段 237清湯 280豬油 225黃酒 243花生油 410糖 401麵粉 261

食材預測

有了食材,我們可以按照那篇英文文章的想法,用一個食材預測的任務來得到食物的Embedding向量。具體做法為:用菜譜中的每個食材,來預測菜譜中的其它食材。

我們現在的食材有:

print len(ings_cts)5598

我們現在去除那些只出現了次數比較少的食材:

for k inings_cts.keys(): if ings_cts[k] <= 5: ings_cts.pop(k)

剩下的有:

print len(ings_cts)737

然後將它們從1開始ID化:

word_idx = {k: idx+1 for idx, k in enumerate(ings_cts)}idx_word = {idx+1: k for idx, k in enumerate(ings_cts)}

接下來將數據ID化:

data_ings_idx = map(lambda x: [word_idx.get(t, 0) for t in x.split()], data_ings)

模型構造

有了數據集,模型也就不難構造了:

這裡,我們使用keras來構造我們的Embedding模型:

import kerasUsing TensorFlow backend.

因為使用的是tensorflow的後端,而且是GPU版本的,我需要設置一下GPU選項,免得我的小程序什麼都沒做就吃掉所有的GPU顯存(坑爹的tensorflow能不能不要認為這些卡都是我一個人在用!):

import keras.backend as Kif K.backend() == "tensorflow": config = K.tf.ConfigProto() config.gpu_options.allow_growth = True session = K.tf.Session(config=config) K.set_session(session)

模型其實很簡單,我們先構造一個N×D的Embedding矩陣,其中N是我的食材數目,D是我需要的食材向量的維度,那麼N種食材就對應與這N個D維向量了,在Embedding的結果上加一個sigmoid輸出的全連接層預測其它食材。輸入維度為1,代表食材的種類,輸出長度為N,代表這個食材與其它食材相關的概率。

現在來構造這個模型:

from keras.models import Sequentialfrom keras.layers import Embedding, Dense, Flattenfrom keras.regularizers import l2 n_words = len(word_idx) + 1model = Sequential()model.add(Embedding(n_words, 50, input_length=1, activity_regularizer=l2(0.01)))model.add(Flatten())model.add(Dense(n_words, activation="sigmoid", activity_regularizer=l2(0.01)))model.compile(optimizer="adam", loss="binary_crossentropy", metrics=["acc"])

構造訓練集:

data_x = []data_y = []for ings_idxs in data_ings_idx: if len(ings_idxs) == 1: continue for idx in set(ings_idxs): y = np.zeros(n_words) data_x.append(idx) y[list(set(ings_idxs) - set([idx]))] = 1 data_y.append(y) data_x =np.array(data_x)data_y =np.array(data_y)

來隨機化一下數據:

rd_idx = np.arange(len(data_x))np.random.shuffle(rd_idx)

訓練模型:

hist = model.fit(data_x[rd_idx], data_y[rd_idx], validation_split=0.1, verbose=0, epochs=5)

訓練完成,我們拿到這些Embedding的向量:

emb = model.get_weights()[0]

算個距離度量,並看看一些跟食材最接近的都是什麼:

from scipy.spatial import distancedist = distance.squareform(distance.pdist(emb, "cosine"))print idx_word[np.argsort(dist[word_idx["鹽"]])[1]]print idx_word[np.argsort(dist[word_idx["雞湯"]])[1]]print idx_word[np.argsort(dist[word_idx["西紅柿"]])[1]]醬油雞蛋高湯

看起來還是蠻有意思的,然而並看不出什麼靠譜的結論。

菜系預測

我們也可以用我們的數據來進行菜系預測,比如想像成一個序列預測問題,用一個輸入序列來預測最終的菜系,序列處理可以使用一個RNN,比如LSTM,GRU等。

這裡我們先處理一下序列,如果使用「做法」序列當輸入,好像這些序列有點太長了,因為最長的序列大概有1000個字,我們退而取其次,用「原料」序列當作輸入。

為此,我們可以先去掉原料中沒什麼用的部分,比如標點符號數字字母等:

def process_rnn(line): # 標點符號 line = re.sub("[。,、/…~~:;:%()]", "", line) # 阿拉伯數字替換為空格 line = re.sub("[dw]", "", line) return linedata_rnn =[process_rnn(i["原料"]) for i in data]

做一下相關統計,去掉所有隻出現過一次的字:

word_cts = Counter("".join(data_rnn))for k, v inword_cts.items(): if v == 1: word_cts.pop(k) print len(word_cts)1105

將這些字和菜系都ID化,方便我們構造數據集:

word_idx = {k: idx+1 for idx, k in enumerate(word_cts)}idx_word = {idx+1: k for idx, k in enumerate(word_cts)}tps_idx = {k: idx for idx, k in enumerate(tps_cts)}idx_tps = {idx: k for idx, k in enumerate(tps_cts)}

將數據ID化:

data_rnn_idx = map(lambda x: [word_idx.get(t, 0) for t in x], data_rnn)data_tps_idx = np.array(map(lambda x: tps_idx[x], data_tps))

將RNN相關的模塊導入:

from keras.layers import LSTM, GRUfrom keras.preprocessing.sequence import pad_sequences

原料序列一般是不等長的,我們用pad_sequences讓它們等長,方便訓練:

data_rnn_idx = np.array(pad_sequences(data_rnn_idx))

接下來就是構造模型:

n_seq = len(data_rnn_idx[0])n_words = len(word_idx) + 1n_tps = len(tps_idx) rnn_model =Sequential()rnn_model.add(Embedding(n_words,64,input_length=n_seq))rnn_model.add(GRU(64))rnn_model.add(Dense(128, activation="relu", name="feat"))rnn_model.add(Dense(n_tps,activation="softmax"))rnn_model.compile(optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["acc"])

這裡用的是GRU的RNN模型,並拿到序列的最後一個輸出,在之後加上一層全連接和一層Softmax分類。

訓練這個模型:

hist = rnn_model.fit(data_rnn_idx, data_tps_idx, epochs=50, verbose=0)

準確率為:

print hist.history["acc"][-1]0.90364867817347605

我們現在把最後一層的特徵拿出來:

from keras.models import Model feat_model = Model(inputs=rnn_model.input,outputs=rnn_model.get_layer("feat").output)data_feat = feat_model.predict(data_rnn_idx)

做一個TSNE降到2維:

from sklearn.manifold import TSNEdata_feat_vis = TSNE(n_components=2, init=pca).fit_transform(data_feat)

可視化一下:

_, ax =plt.subplots(figsize=(15, 12))for tp, idx in tps_idx.items(): ax.scatter(data_feat_vis[data_tps_idx==idx,0], data_feat_vis[data_tps_idx==idx,1], c=cm.Vega20.colors[idx], label=tp)ax.set_xlim(-20, 40)ax.set_ylim(-20, 20)ax.set_xticks([])ax.set_yticks([])ax.legend(loc=0)ltext = ax.get_legend().get_texts()for t in ltext: t.set_font_properties(font_song) t.set_fontsize("xx-large")plt.show()

一本正經地分析一下這張圖:

  • 韓國菜明顯風格跟天朝有差異

  • 浙江菜的隔壁是滬菜和江蘇菜,江蘇菜的隔壁是魯菜

  • 微波爐菜(不知道什麼鬼)混入了全部菜系之中

  • 閩菜和粵菜相鄰

  • 粵菜的點分布的還挺開的(大概廣東人什麼都吃?)

  • 湘菜跟川菜也挺近的

剩下的請恕我老眼昏花看不清楚,從地理上看,還是有點意思的。

當然了,說到底這個任務都是我硬生生造出來的,並沒什麼用,而且,我也根本不懂怎麼做菜,權當我是胡說八道好了,畢竟:

人生吶,就該一本正經地胡說八道。

原文發表於微信公眾號:lijin_echo

微信文章鏈接:如何科學地「學習」中餐菜譜

推薦閱讀:

大數據人的職業生涯規劃分享要點
用python建立房價預測模型|python數據分析建模實例
用戶畫像學習
新征程——2017年數據分析實踐具體計劃
2017年3D列印行業大數據報告,3D列印品牌數據分析

TAG:數據分析 |