使用scikitlearn、NLTK、Docker、Flask和Heroku構建食譜推薦API

2021-02-07 磐創AI


作者 | Jackmleitch 

編譯 | VK 

來源 | Towards Data Science

我的想法是:給你一張配料表,我能做什麼不同的食譜?也就是說,我可以用我公寓裡的食物做什麼食譜?

首先,如果你想看到我的API(或使用它!)請按照以下步驟進行操作:

https://whats-cooking-recommendation.herokuapp.com/-如果你在美國

https://whatscooking-deployment.herokuapp.com/-如果你在歐洲

我為缺乏美觀道歉,在某個時候,當我有時間去做的時候,我會構建一個更好的應用程式。

在我關於這個項目的第一篇博客文章中,我回顧了我是如何為這個項目收集數據的。數據是烹飪食譜和相應的配料。從那以後,我添加了更多的食譜,所以我們現在總共有4647個。請隨意使用這個數據集,你可以在我的Github上找到它:https://github.com/jackmleitch/Whatscooking-

這篇文章將著重於對數據進行預處理,構建推薦系統,最後使用Flask和Heroku部署模型。

建立推薦系統的過程如下:

首先對數據集進行清理和解析,然後從數據中提取數字特徵,在此基礎上應用相似度函數來尋找已知食譜的配料與最終用戶給出的配料之間的相似度。最後根據相似度得分,得到最佳推薦食譜。

與本系列的第一篇文章不同,本文不是關於我使用的工具的教程,但它將描述我如何構建系統以及為什麼我會做出這樣的決定。雖然,代碼注釋在我看來很好地解釋了一些事情。與大多數項目一樣,我的目標是創建最簡單的模型,以使工作達到我想要的標準。

構建食譜推薦API成分的預處理與解析

為了理解手頭的任務,讓我們看一個例子。Jamie Oliver網站上的美味「Gennaro's classic spaghetti carbonara」食譜需要以下配料:

這裡有很多冗餘信息;例如,重量和相關度量不會為食譜的矢量編碼增加意義。如果說有什麼區別的話,這將使區分食譜變得更加困難。所以我們需要把那些東西處理掉。在谷歌上快速搜索後,我找到了一個維基百科頁面,裡面有一個標準烹飪指標的列表,比如丁香、克(g)、茶匙等等。在我的配料分析器中刪除所有這些詞效果非常好。

我們還想從我們的成分中去掉停用詞。在NLP中,「停止詞」是指一種語言中最常見的詞。例如,句子「learning about what stop words are」變成了「learning stop words」。NLTK為我們提供了一種簡單的方法來刪除(大部分)這些單詞。

食材中還有一些對我們沒用的詞——這些詞在食譜中很常見。例如,油在大多數食譜中都有使用,而且在食譜之間幾乎沒有區別。而且,大多數人家裡都有油,所以每次使用API都要寫油,這既麻煩又毫無意義。

簡單地刪除最常見的單詞似乎非常有效,所以我這樣做了。奧卡姆剃刀原則…為了得到最常見的詞彙,我們可以執行:

import nltk
vocabulary = nltk.FreqDist()

# 我已經做好了原料的預處理
for ingredients in recipe_df['ingredients']:
    ingredients = ingredients.split()
    vocabulary.update(ingredients)
    
for word, frequency in vocabulary.most_common(200):
    print(f'{word};{frequency}')

不過,我們還有最後一個障礙要克服。當我們試圖從配料表中刪除這些「垃圾」詞時,如果同一個詞有不同的變體,會發生什麼情況?

如果我們想去掉「pound」這個詞的每一個出現,但是食譜中的配料卻寫著「pounds」怎麼辦?幸運的是,有一個相當簡單的解決方法:詞形還原和詞幹還原。詞幹還原和詞形還原都會產生詞根變化詞的詞根形式,區別在於詞幹還原的結果可能不是一個真正的單詞,而詞形還原的結果是一個實際的單詞。

儘管詞形還原通常比較慢,但我選擇使用這種技術,因為我知道實際單詞對調試和可視化非常有用。當用戶向API提供成分時,我們也會將這些單詞詞形還原

我們可以把這些都放在一個函數component_parser中,以及其他一些標準的預處理:去掉標點符號,使所有內容都小寫,統一編碼。

def ingredient_parser(ingredients):

    # 量度和常用詞(已被詞形還原)
    measures = ['teaspoon', 't', 'tsp.', 'tablespoon', 'T', ...]
    words_to_remove = ['fresh', 'oil', 'a', 'red', 'bunch', ...]
    
    # 將成分列表從字符串轉換為列表
    if isinstance(ingredients, list):
       ingredients = ingredients
    else:
       ingredients = ast.literal_eval(ingredients)
       
    # 我們首先去掉所有的標點符號
    translator = str.maketrans('', '', string.punctuation)
    
    # 初始化nltk的lemmatizer
    lemmatizer = WordNetLemmatizer()
ingred_list = []
    for i in ingredients:
        i.translate(translator)
        
        # 我們用連字符和空格分開
        items = re.split(' |-', i)
        
        # 把所有內容都改成小寫
        items = [word for word in items if word.isalpha()]
        
        # 小寫
        items = [word.lower() for word in items]
        
        # 統一編碼
        items = [unidecode.unidecode(word) for word in items]
        
        # 詞形還原,這樣我們可以比較
        items = [lemmatizer.lemmatize(word) for word in items]
        
        # 刪除停用詞
        stop_words = set(corpus.stopwords.words('english'))
        items = [word for word in items if word not in stop_words]
        
        # #避免測量單詞/短語, 例如. heaped teaspoon
        items = [word for word in items if word not in measures]
        
        # 刪除常見的簡單詞彙
        items = [word for word in items if word not in words_to_remove]
        if items:
           ingred_list.append(' '.join(items))
           ingred_list = ' '.join(ingred_list)
    return ingred_list

當我們分析「Gennaro’s classic spaghetti carbonara』」的成分時,我們得到:egg yolk parmesan cheese pancetta spaghetti garlic。太好了,太棒了!

使用lambda函數,很容易解析所有成分。

recipe_df = pd.read_csv(config.RECIPES_PATH)
recipe_df['ingredients_parsed'] = recipe_df['ingredients'].apply(lambda x: ingredient_parser(x))
df = recipe_df.dropna()
df.to_csv(config.PARSED_PATH, index=False)

提取特徵

我們現在需要對每個文檔(食譜成分)進行編碼,和以前一樣,簡單的模型非常有效。

在進行NLP時,最基本的模型之一就是詞袋。這就需要創建一個巨大的稀疏矩陣來存儲我們語料庫中所有單詞對應的數量(所有文檔,即每個食譜的所有成分)。scikitlearn的countVector有一個很好的實現。

詞袋執行得不錯,但TF-IDF(術語頻率反向文檔頻率)執行得稍差,所以我們選擇了這個。我不打算詳細介紹tf-idf是如何工作的,因為它與博客無關。與往常一樣,scikitlearn有一個很好的實現:TfidfVectorizer。然後,我用pickle保存了模型和編碼,因為每次使用API時重新訓練模型都會使它非常緩慢。

import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
import pickle
import config

# 加載解析的食譜數據集
df_recipes = pd.read_csv(config.PARSED_PATH)

# Tfidf需要unicode或string類型
df_recipes['ingredients_parsed'] = df_recipes.ingredients_parsed.values.astype('U')

# TF-IDF特徵提取程序
tfidf = TfidfVectorizer()
tfidf.fit(df_recipes['ingredients_parsed'])
tfidf_recipe = tfidf.transform(df_recipes['ingredients_parsed'])

# 保存tfidf模型和編碼
with open(config.TFIDF_MODEL_PATH, "wb") as f:
     pickle.dump(tfidf, f)
     
with open(config.TFIDF_ENCODING_PATH, "wb") as f:
     pickle.dump(tfidf_recipe, f)

推薦系統

該應用程式僅由文本數據組成,並且沒有可用的評分類型,因此不能使用矩陣分解方法,如基於SVD和基於相關係數的方法。

我們使用基於內容的過濾,使我們能夠根據用戶提供的屬性(成分)向人們推薦食譜。為了度量文檔之間的相似性,我使用了餘弦相似性。我也嘗試過使用Spacy和KNN,但是餘弦相似性在性能(和易用性)方面獲得了勝利。

從數學上講,餘弦相似性度量兩個向量之間夾角的餘弦。我選擇使用這種相似性度量,即使兩個相似的文檔以歐幾裡德距離相距甚遠(由於文檔的大小),它們可能仍然朝向更近的方向。

例如,如果用戶輸入了大量的配料,而只有前半部分與食譜匹配,理論上,我們仍然應該得到一個很好的食譜匹配。在餘弦相似性中,角度越小,餘弦相似度越高:所以我們試圖最大化這個分數。

from sklearn.metrics.pairwise import cosine_similarity
from sklearn.feature_extraction.text import TfidfVectorizer
import pickle
import config
from ingredient_parser import ingredient_parser

# 加載tdidf模型和編碼
with open(config.TFIDF_ENCODING_PATH, 'rb') as f:
     tfidf_encodings = pickle.load(f)
with open(config.TFIDF_MODEL_PATH, "rb") as f:
     tfidf = pickle.load(f)
     
# 使用ingredient_parser分析配料
try:
    ingredients_parsed = ingredient_parser(ingredients)
except:
    ingredients_parsed = ingredient_parser([ingredients])
    
# 使用我們預訓練的tfidf模型對輸入成分進行編碼
ingredients_tfidf = tfidf.transform([ingredients_parsed])

# 計算實際食譜和測試食譜之間的餘弦相似性
cos_sim = map(lambda x: cosine_similarity(ingredients_tfidf, x), tfidf_encodings)
scores = list(cos_sim)

然後,我編寫了一個函數get_recommendations,對這些分數進行排名,並輸出一個pandas數據框,其中包含前N個菜譜的所有細節。

def get_recommendations(N, scores):
    # 加載食譜數據集
    df_recipes = pd.read_csv(config.PARSED_PATH)
    
    # 對分數排序,得到N個最高的分數
    top = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)[:N]
    
    # 在dataframe中創建建議
    recommendation = pd.DataFrame(columns = ['recipe', 'ingredients', 'score', 'url'])
    
    count = 0
    for i in top:
        recommendation.at[count, 'recipe'] = title_parser(df_recipes['recipe_name'][i])
        
        recommendation.at[count, 'ingredients'] = ingredient_parser_final(df_recipes['ingredients'][i])
        
        recommendation.at[count, 'url'] = df_recipes['recipe_urls'][i]
        recommendation.at[count, 'score'] = "{:.3f}".format(float(scores[i]))
        
        count += 1
    return recommendation

值得注意的是,沒有具體的方法來評估模型的性能,所以我不得不手動評估這些建議。不過,老實說,這真的很有趣…我還發現了很多新的食譜!

到目前為止,我冰箱/櫥櫃裡的一些東西是:碎牛肉、義大利麵、番茄面醬、培根、洋蔥、西葫蘆和奶酪。推薦系統的建議是:

{ "ingredients" : "1 (15 ounce) can tomato sauce, 1 (8 ounce) package uncooked pasta shells, 1 large zucchini - peeled and cubed, 1 teaspoon dried basil, 1 teaspoon dried oregano, 1/2 cup white sugar, 1/2 medium onion, finely chopped, 1/4 cup grated Romano cheese, 1/4 cup olive oil, 1/8 teaspoon crushed red pepper flakes, 2 cups water, 3 cloves garlic, minced",

  "recipe" : "Zucchini and Shells",  
  
  "score: "0.760",
 
  "url":"https://www.allrecipes.com/recipe/88377/zucchini-and-shells/"
}

聽起來不錯-最好去做飯!

創建一個API來部署模型使用Flask

那麼,我如何為最終用戶提供我所構建的模型呢?我創建了一個API,可以用來輸入成分,然後根據這些成分輸出前5個食譜建議。為了構建這個API,我使用了Flask,它是一個微web服務框架。

# app.py
from flask import Flask, jsonify, request
import json, requests, pickle
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from ingredient_parser import ingredient_parser
import config, rec_sys

app = Flask(__name__)
@app.route('/', methods=["GET"])
def hello():
    # 這是我們API的主頁
    # 它可以通過http://127.0.0.1:5000/訪問
    return HELLO_HTML
    
HELLO_HTML = """
     <html><body>
         <h1>Welcome to my api: Whatscooking!</h1>
         <p>Please add some ingredients to the url to receive recipe recommendations.
            You can do this by appending "/recipe?ingredients= Pasta Tomato ..." to the current url.
         <br>Click <a href="/recipe?ingredients= pasta tomato onion">here</a> for an example when using the ingredients: pasta, tomato and onion.
     </body></html>
     """
     
@app.route('/recipe', methods=["GET"])
def recommend_recipe():
    # 可以通過http://127.0.0.1:5000/recipe訪問
    ingredients = request.args.get('ingredients')
    recipe = rec_sys.RecSys(ingredients)
    
    # 我們需要將輸出轉換為JSON。
    response = {}
    count = 0    
    for index, row in recipe.iterrows():
        response[count] = {
                            'recipe': str(row['recipe']),
                            'score': str(row['score']),
                            'ingredients': str(row['ingredients']),
                            'url': str(row['url'])
                          }
                          
        count += 1
        
    return jsonify(response)
if __name__ == "__main__":
   app.run(host="0.0.0.0", debug=True)

我們可以通過運行命令python app.py來啟動,API將在本地主機上的埠5000上啟動。我們可以通過訪問http://192.168.1.51:5000/recipe?ingredients=%20pasta%20tomato%20onion獲取關於義大利麵、番茄和洋蔥的食譜推薦。

將Flask API部署到Heroku

如果使用Github,將flaskapi部署到Heroku非常容易!首先,我在我的項目文件夾中創建了一個沒有擴展名的Procfile文件。你只需在該文件中輸入:

web: gunicorn app:app

下一步是創建一個名為requirements.txt的文件,它包含了我在這個項目中使用的所有python庫。

如果你在虛擬環境中工作(我使用conda),可以使用pip freeze > requirements.txt,確保你在正確的工作目錄中運行,否則它會將文件保存到其他地方。

現在我所要做的就是將更改提交到Github存儲庫中,然後按照上面的部署步驟進行操作https://dashboard.heroku.com/apps。如果你想試用或使用我的API,請訪問:

https://whats-cooking-recommendation.herokuapp.com/-如果你在美國

https://whatscooking-deployment.herokuapp.com/-如果你在歐洲

Docker

我們現在已經到了這樣一個階段,我對我構建的模型感到滿意,所以我希望能夠將我的模型分發給其他人,以便他們也能使用它。

我已經把我的整個項目上傳到Github,但這還不夠。僅僅因為代碼在我的計算機上工作並不意味著它將在其他人的計算機上工作。

如果當我分發代碼時,我複製我的計算機,這樣我就知道它會工作了,那將是非常棒的。現在最流行的方法之一就是使用Docker容器。我做的第一件事是創建一個名為Dockerfile的docker文件(它沒有擴展名)。簡單地說,docker文件告訴我們如何構建環境,並包含用戶可以在命令行中調用的所有命令來組裝映像。

# 包括從何處獲取映像(作業系統)
FROM ubuntu:18.04

MAINTAINER Jack Leitch 'jackmleitch@gmail.com'

# 自動按Y
RUN apt-get update && apt-get install -y \
    git \
    curl \
    ca-certificates \
    python3 \
    python3-pip \
    sudo \
    && rm -rf /var/lib/apt/lists/*
    
# 設置工作目錄
WORKDIR /app

# 將currect目錄中的所有內容複製到app目錄中。
ADD . /app

# 安裝所有要求
RUN pip3 install -r requirements.txt

# 下載wordnet作為它用來詞形還原
RUN python3 -c "import nltk; nltk.download('wordnet')"

# CMD在容器啟動後執行
CMD ["python3", "app.py"]

一旦我創建了docker文件,我就需要構建我的容器—這很簡單。

旁註:如果你這樣做,確保你所有的文件路徑(我把我的放在一個config.py文件中)不是特定於你的計算機,因為docker就像一個虛擬機,包含它自己的文件系統,例如,你可以放./input/df_recipes.csv。

docker build -f Dockerfile -t whatscooking:api

在任何機器上啟動API(!),我們現在要做的就是(假設你已經下載了docker容器):

docker run -p 5000:5000 -d whatscooking:api

如果你想親自檢查容器,這裡有一個連結到我的Docker Bub:https://hub.docker.com/repository/docker/jackmleitch/whatscooking。你可以通過以下方式拖動圖像:

docker pull jackmleitch/whatscooking:api

接下來的計劃是使用Streamlit構建一個更好的API接口。

相關焦點

  • 用scikit-learn解救蔡徐坤
    監督控制辱罵和攻擊性言論,原本是幾行代碼就可以輕鬆解決的事情。本文介紹一個自動檢測仇恨言論系統——使用scikit-learn構建,並通過Heroku上的Docker進行部署。作為線上留言板或評論區的監管員,在網上評論開始出現攻擊性、和辱罵性言語時能得到快速提醒。及時採取控制措施。工作流程只需四步:1.
  • 使用Scikit-learn 理解隨機森林
    雷鋒網按:本文為 AI 研習社編譯的技術博客,原標題 Random forest interpretation with scikit-learn,作者 ando。我的一些代碼包正在做相關工作,然而,大多數隨機森林算法包(包括 scikit-learn)並沒有給出預測過程的樹路徑。因此 sklearn 的應用需要一個補丁來展現這些路徑。
  • python機器學習之使用scikit-learn庫
    引言數據分析由一連串的步驟組成,對於其中預測模型的創建和驗證這一步,我們使用scikit-learn這個功能強大的庫來完成。scikit-learning庫python庫scikit-learn整合了多種機器學習算法。
  • 使用Flask構建簡單的RESTful服務
    我們現在的一個項目是使用Django來構建,說來也是基於技術擴展的考慮,我對於Django裡面大而全的一些組件還是持有保守態度
  • scikit-learn中的自動模型選擇和複合特徵空間
    使用scikit-learn管道自動組合文本和數字數據有時,機器學習模型的可能配置即使沒有上千種,也有數百種,這使得手工找到最佳配置的可能性變得不可能,因此自動化是必不可少的。在處理複合特徵空間時尤其如此,在複合特徵空間中,我們希望對數據集中的不同特徵應用不同的轉換。
  • 機器學習也能套模版:在線選擇模型和參數,一鍵生成demo
    現在,有一個Web應用程式,可以生成用於機器學習的模板代碼(demo),目前支持PyTorch和scikit-learn。同時,對於初學者來說,這也是一個非常好的工具。在模版中學習機器學習的代碼,可以少走一些彎路。
  • CDA承接的全球頂級機器學習Scikit-learn 中文社區上線啦!
    (注:scikit-learn的官網是www.scikit-learn.org,CDA承接的中文社區網址是www.scikit-learn.org.cn,這同時也標誌著CDA與全球頂級深度學習和機器學習框架更進一步融合,CDA認證更加得到全球頂級技術框架的認可!
  • 遊戲夜讀|Scikit-learn的2018自述
    R和Python的交誼舞曾幾何時,數據分析入門的一大討論就是R和Python的選擇。當統計學家連上了網際網路的那刻起,這種選擇一直就存在,只是主演們在換。上一代的主角,一方是R和S。大家選擇R是因為它是免費的解決方案,又能用,還有一批人維護更新,而且下載即可。概括起來,就是「開源大法好」。另一方是Matlab和Python。
  • Docker(二):Dockerfile 使用介紹
    將當前目錄做為構建上下文時,可以像下面這樣使用docker build命令構建鏡像:docker build .Sending build context to Docker daemon 6.51 MB...說明:構建會在 Docker 後臺守護進程(daemon)中執行,而不是 CLI中。
  • Python粉都應該知道的開源機器學習框架:Scikit-learn入門指南
    安裝和運行Scikit-learn如前所述,Scikit-learn需要NumPy和SciPy等其他包的支持,因此在安裝Scikit-learn之前需要提前安裝一些支持包,具體列表和教程可以查看Scikit-learn的官方文檔: http://scikit-learn.org/stable/install.html ,以下僅列出Python、NumPy和SciPy等三個必備包的安裝說明
  • 如何使用 Docker 高效部署 Node 應用 - 51CTO.COM
    再稍微複雜一點點的 Node 應用可以查看山月的項目 whoami[5]: 一個最簡化的 serverless 與 dockerize 示例。$ apk  在帶有編譯過程的鏡像構建中,源文件與構建工具都會造成空間的浪費。藉助鏡像的「多階段構建」可以高效利用空間。Go App 與 FE App 的構建也遵循此規則。 多階段構建 Go 應用[6] 多階段構建前端應用[7]在構建 Node 應用鏡像時,第一層鏡像用以構造 node_modules。
  • Flask 插件學習系列:Restful
    在基礎框架之外,Flask 擁有豐富的擴展(Extension) 來其擴充功能,這些擴展有的來自官方,有的來自第三方。這一系列會給大家介紹一些 Flask 常用的擴展及其使用方法。Flask 擴展你可以在 Flask 的官網上尋找你想要的擴展,每個擴展都有其文檔連結和 Github 上的源碼連結。擴展可以通過"pip install"來安裝。
  • 不要輕易使用 Alpine 鏡像來構建 Docker 鏡像,有坑!
    大多數 Java 鏡像都提供了 JDK 和 JRE 兩種標籤,因此可以在多階段構建的 build 階段使用 JDK 作為基礎鏡像,run 階段使用 JRE 作為基礎鏡像。這種情況就必須從源碼開始構建!最後一種情況最不推薦使用 Alpine 作為基礎鏡像,不但不能減小體積,可能還會適得其反,因為你需要安裝編譯器、依賴庫、頭文件等等。。。更重要的是,構建時間會很長,效率低下。如果非要考慮多階段構建,就更複雜了,你得搞清楚如何將所有的依賴編譯成二進位文件,想想就頭大。
  • docker的第一階段總結
    docker實例默認使用的是私有的網段,需要訪問裡面服務時候通過映射埠出來,但是如果想讓實例使用物理IP,比如我們辦公網想當做虛擬機來使用,就可以用macvlan網絡來實現。針對私有倉庫的管理可以使用vmware提供的harbor來做,安裝也是很docker化,下載源碼之後利用docker-compose編排工具進行全自動化編排,因為裡面包含各個需要互聯的docker實例,所以需要docker-compose.yml這個配置文件來進行編排自動化管理。
  • 在NLP中結合文本和數字特徵進行機器學習
    應用於自然語言處理的機器學習數據通常包含文本和數字輸入。例如,當您通過twitter或新聞構建一個模型來預測產品未來的銷售時,在考慮文本的同時考慮過去的銷售數據、訪問者數量、市場趨勢等將會更有效。您不會僅僅根據新聞情緒來預測股價的波動,而是會利用它來補充基於經濟指標和歷史價格的模型。
  • docker下高並發和高可用之docker swarm使用
    ,操作步驟參考Linux下安裝和使用Docker安裝完,使用命令sudo systemctl start docker啟動docker,再通過命令docker version查看docker版本信息利用docker swarm 命令來指定其中一臺虛擬機為docker的Manager管理機docker swarm init --advertise-addr
  • 基於 Flask 提供 RESTful Web 服務
    > (點擊上方公眾號,可快速關注)來源:FunHacks連結:funhacks.net/2016/08/21/restful_api_with_flask
  • 使用Gitea+Drone來搭建自己的輕量級CI/CD自動構建平臺
    我們使用Gitea搭建了自己的Git版本控制系統,可以用來管理自己的代碼還需要一個自動構建工具來解放生產力,這裡我推薦使用Drone來搭建CI/CD持續集成,持續部署平臺 為什麼選用Gitea+Drone呢?
  • 雲計算核心技術Docker教程:docker-compose build/pull命令介紹
    Docker Compose是用於定義和運行多容器 Docker 應用程式的工具。通過 Compose,您可以使用 YML 文件來配置應用程式需要的所有服務。然後,使用一個命令,就可以從 YML 文件配置中創建並啟動所有服務。
  • 雲計算核心技術Docker教程:Docker Compose的restart和rm命令詳解
    Docker-Compose restart命令可以重新啟動所有已停止並正在運行的服務,Docker-Compose rm命令可以刪除已經停止的容器,如果服務在運行,需要先docker-compose stop 停止容器。