如何用Flaks開發(fā)Web API實現(xiàn)機(jī)器學(xué)習(xí)模型的實時調(diào)用

? 機(jī)器學(xué)習(xí)項目中,一個模型訓(xùn)練好了之后,總要部署到服務(wù)器上去。以前我們項目采用的方式都是以離線學(xué)習(xí)為主,不要求數(shù)據(jù)處理的實時性。比如凌晨開始調(diào)用前一天的數(shù)據(jù),跑出模型結(jié)果,存到數(shù)據(jù)庫,早上上班后就可以運(yùn)用模型結(jié)果了。但有些時候,要求數(shù)據(jù)能夠?qū)崟r給出模型結(jié)果,即一條新的數(shù)據(jù)一旦生成,立刻輸入到模型中運(yùn)算,得到的結(jié)果立刻返回,這叫在線學(xué)習(xí)。我們在亞馬遜上買東西,通常會看到提示為“你可能也喜歡這些東西”,這些就是在線學(xué)習(xí)的結(jié)果。

? 這次我們的項目客戶也提出了類似需求,這是一個投訴工單文本分類模型,需求是希望一個投訴工單生成后,立刻能夠知道模型結(jié)果。坦白講,這類需求我們項目組之前也沒實現(xiàn)過,一開始也有點懵逼。好在公司團(tuán)隊肥腸強(qiáng)大,在咨詢了其他項目組的一些大牛后,明白可以采用WebAPI的方式調(diào)取模型,實時獲取模型結(jié)果。既然這周的博客任務(wù)還沒完成,那就把這個項目經(jīng)驗寫下來吧。。。

? 首先什么是API,如果你之前沒有任何這方面的了解,你可能咋一看API的定義會一臉懵逼(比如我)。其實通俗的講,可以將API比作一個窗口,你要通過這個窗口去實現(xiàn)一些功能,但你不用去管這些功能怎么實現(xiàn)的,你只要按照請求提交一些東西到窗口里,窗口另一邊就會返回相應(yīng)的東西。 比如去肯德基買漢堡,你不用管漢堡怎么做的,你只需按照要求,把錢遞進(jìn)去,說我要漢堡,肯德基就把香噴噴的漢堡遞給你了。這就是API。按照這個定義,我們可以發(fā)現(xiàn),函數(shù)就是一個API,我們按照函數(shù)的定義輸入?yún)?shù),函數(shù)就返回相應(yīng)的結(jié)果,而我們不用管函數(shù)內(nèi)部怎么實現(xiàn)的。我們?nèi)粘4蜷_網(wǎng)頁的方式也是調(diào)用API,我們只需要輸入網(wǎng)址,瀏覽器就會呈現(xiàn)相應(yīng)的內(nèi)容,我們不用管后臺服務(wù)器端是如何渲染這個網(wǎng)頁的??梢赃@么理解,通過一個url地址來實現(xiàn)API調(diào)用的,就叫WebAPI。

? 而我們現(xiàn)在的目的一樣,調(diào)用方只需要負(fù)責(zé)將實時將數(shù)據(jù),通過我們開發(fā)好的Web API傳輸過來,就可以調(diào)用模型,進(jìn)而得到模型結(jié)果。在Web傳輸中,有兩個重要概念,叫請求和響應(yīng),即,客戶端向服務(wù)器發(fā)出請求,服務(wù)器向客戶端返回響應(yīng)。模型需要的輸入數(shù)據(jù),跟隨著請求一起傳輸過來,模型的輸出結(jié)果,通過響應(yīng)回傳給客戶端。

? 更具體的,在我們的模型接口中(接口即API),一個請求,包含請求頭(head)和請求體(body),請求頭中包含目標(biāo)url(這個url是開發(fā)接口的人定義的)和請求的方法(主要是get、post,差別后面講);請求體中包含接口定義的輸入數(shù)據(jù)(通常就是模型的輸入項)。類似地,響應(yīng)也包含響應(yīng)頭和響應(yīng)體,我們要返回給模型的結(jié)果,就在響應(yīng)體中。

? 具體的Web相關(guān)的理論知識,就不介紹太多了,一時半會說不完,說太多反而形成勸退的效果。(其實現(xiàn)在天冷,我盤腿蜷縮在沙發(fā)上碼子,懶得查資料又怕記憶有偏差,所以懶得說了……)

? 那么具體來說我們要怎么做呢,我們要定義一個url,將這個url綁定一個函數(shù),這個函數(shù)可以接收數(shù)據(jù)并調(diào)用模型。我們這個url交給調(diào)用方,告訴他,你通過這個url把數(shù)據(jù)傳輸給。一定他們調(diào)用了這個url——比如最簡單的,在瀏覽器輸入這個url地址,那么在服務(wù)器這邊,一旦發(fā)現(xiàn)url被調(diào)用,就會觸發(fā)我們定義的函數(shù)執(zhí)行,這樣就實現(xiàn)了接口調(diào)用。

? 怎么開發(fā)接口呢,Python的Flask庫可以很方便的開發(fā)接口。我們要做的事情有:

  1. 定義一個url,并綁定一個函數(shù),接下來是函數(shù)的處理邏輯。
  2. 函數(shù)獲取數(shù)據(jù),并判斷數(shù)據(jù)類型是否爭取,如果不對,我們就不方便進(jìn)行下一步處理。
  3. 校驗輸入數(shù)據(jù)的字段是否齊全,比如模型需要10個輸入字段,你只給了9個,那可不行。
  4. 調(diào)用模型運(yùn)算
  5. 返回模型結(jié)果。

? 說得再多,不如直接看代碼,我把注釋寫在代碼里(我懶癌犯了)。注意,你別看我下面代碼這么長,其實這是我在測試階段一股腦都寫上去了,真實的場景可能不需要這么多的,一個接口也不需要這么多代碼,簡單的可能10幾行就夠了。

from flask import Flask, jsonify, request  
from wtforms import StringField, Form
from wtforms.validators import InputRequired,DataRequired
import json
from model import *  #這個model是我自己的模型文件,你們并沒有,不用管它。


log = my_log(path='log', log_file='web_api_request.log')  # 建立日志文件,保存結(jié)果,這是個人習(xí)慣,不用管。

app = Flask(__name__) # 1.定義個一個Flask實例,__name__指定了程序主模塊的名字,
Flask以此決定程序的根目錄。即如果flask要從一個相對路徑獲取資源,根目錄就是這個主
模塊所在的目錄。如果你不清楚,直接寫__name__就可以了。
app.config['JSON_AS_ASCII'] = False #如果你返回的json數(shù)據(jù)數(shù)亂碼,那么這個設(shè)定可以
幫助你。

class InputData(Form):#我們要對模型輸入的數(shù)據(jù),這個類定義了檢查數(shù)據(jù)的方式。
    # accept_content表示對輸入數(shù)據(jù)中的 accept_content字段進(jìn)行規(guī)范;StringField要求字
段必須是文本;validators參數(shù)指定檢查器,可以有多個檢查器,都放在[]里;DataRequired
意思是這個字段是必須的,其中的message表示如果沒有這個輸入這個數(shù)據(jù),會提示什么文
字。
    accept_content = StringField(validators=[DataRequired(message='沒有accept_content')])
    category_lv1 = StringField(validators=[DataRequired(message='沒有輸入category_lv1')])
    category_lv2 = StringField(validators=[DataRequired(message='沒有輸入category_lv2')])
    category_lv3 = StringField(validators=[DataRequired(message='沒有輸入category_lv3')])

#下面這句這不是API腳本的內(nèi)容,這個是用requests庫調(diào)用api的方式。這句代碼中:
requests.post表示執(zhí)行post方法,對應(yīng)的requests.get表示執(zhí)行g(shù)et方法;
http://127.0.0.1:5000/complaint就是我們指定的url,127.0.0.1是本機(jī)地址,是在本機(jī)測試
API時使用的,真正部署時,需要填服務(wù)器的API地址。5000是默認(rèn)端口,也可以自己指
定。complaint是自己的定義,也可以取其他名字;json = data_json中,json表示數(shù)據(jù)必須以
json格式輸入,這是我們開發(fā)接口時限定的條件,也可以限定其他格式。data_json中就是模
型需要的輸入數(shù)據(jù),為json格式。
# requests.post('http://127.0.0.1:5000/complaint', json = data_json)

@app.route('/complaint', methods=['post']) #這個裝飾器定義了url的路徑,并指定了調(diào)用方
法必須是post
def predition(): #這是核心程序,當(dāng)調(diào)用上述url時,就會執(zhí)行這個函數(shù)。
    log.info("get a request:referer{0} user_agent{1}".format(request.referrer, request.user_agent)) #記錄每一次請求者的信息
    if not request.is_json : #判斷輸入是否json格式
        return bad_request() #我們定義了一個bad_request函數(shù),來處理輸入數(shù)據(jù)不符合要求
時要怎么辦。
    else:
        data = request.get_json()  # 獲取json數(shù)據(jù)
        try:
            data = json.loads(data)  #轉(zhuǎn)成字典,這是我這邊調(diào)用模型要求輸入必須字典格式。
        except:
            return bad_request() #如果無法轉(zhuǎn)成字典,說明請求中的數(shù)據(jù)不是json格式,不符合
要求。
        #json解析成字典,再作為關(guān)鍵字參數(shù)傳輸進(jìn)去,驗證數(shù)據(jù),這步是必須的,必須轉(zhuǎn)成
字典。
        input = InputData(**data) 
        if not input.validate(): #input.validate()執(zhí)行驗證,如果驗證通過,返回True
            #如果沒通過驗證,input.errors返回錯誤信息,我們將錯誤信息返回給調(diào)用方,讓他
們進(jìn)行調(diào)整。
            return jsonify(input.errors) 
        else:
            #運(yùn)行模型,測試階段,如果模型出錯,會返回測試數(shù)據(jù)給調(diào)用方,方便他們根據(jù)測
試數(shù)據(jù)繼續(xù)開發(fā)。這是我自己的個性化需求,因為我們的開發(fā)人員,需要獲取響應(yīng)數(shù)據(jù),再
執(zhí)行其他開發(fā)工作。他們不在乎返回是什么,只要有返回就行了。如果因為我們模型的問
題,導(dǎo)致無法返回數(shù)據(jù),會影響他們的開發(fā)工作。所以我這邊定義模型如果出錯,返回測試
結(jié)果,同時我記錄出錯數(shù)據(jù)和錯誤信息,再進(jìn)行debug。
            try: 
                data = input_column_to_eng(data) #將英文key轉(zhuǎn)為中文,這是我的模型文件要求
的
                pre_result = predict(data) #執(zhí)行模型運(yùn)算
                log.info("request data \n:{0}".format(data)) #記錄輸出結(jié)果到日志文件
            except: #測試期間,運(yùn)行錯誤返回錯誤值,并記錄錯誤原因,錯誤數(shù)據(jù)
                log.error("=*20模型出錯:", exc_info=True)
                with open('cannot_predict.txt', 'a') as file:  #記錄出錯數(shù)據(jù)
                    file.write(str(data) + '\n\n')
                pre_result = {'測試數(shù)據(jù)':'測試結(jié)果'
                              }
            pre_result_code = result_to_code(pre_result) # 轉(zhuǎn)碼輸出結(jié)果,不用管
            return jsonify(pre_result_code) #將模型結(jié)果,用json格式發(fā)送出去。

#一下裝飾器,分別定義了各種接口調(diào)用錯誤的處理方式。要了解這些錯誤,需要去了解一
下相關(guān)的web知識。
@app.errorhandler(400)
def bad_request(error=None):
    message = {
        'status':400,
        'message':'Bad request:Please check your request, is it json type?'
    }
    resp = jsonify(message)
    resp.status_code = 400
    return resp


@app.errorhandler(404)
def not_found(e):
    message = {
        'status':404,
        'message':'Notfound: please check your url'
    }
    resp = jsonify(message)
    resp.status_code = 404
    return resp


@app.errorhandler(405)
def Method_error(e):
    message = {
        'status':405,
        'message':'Method not allow: please make sure your method is POST'
    }
    resp = jsonify(message)
    resp.status_code = 405
    return resp


@app.errorhandler(500)
def serve_error(e):
    message = {
        'status':500,
        'message':'Internal serve error: Try again , or ask API developer for help.'
    }
    resp = jsonify(message)
    resp.status_code = 500
    return resp

? 好了,大概就這樣。其實開發(fā)web接口,F(xiàn)lask中有個插件叫FlaskRestful,是專門開發(fā)API用的,而Flask是用來開發(fā)網(wǎng)頁的,開發(fā)接口只是他順便的功能之一而已。我也用FlaskRestful寫了個接口,下次看有沒有必要找個機(jī)會發(fā)出來。。

? 這篇有點水,想到哪寫到哪,很多點還沒寫清楚,歷時一個半小時,就為了趕在12點前發(fā)出去。怎么說呢,不打臉比較重要吧。。12月的福州太冷了,瑟瑟發(fā)抖躲被窩去。。。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架,建立于...
    Hsinwong閱讀 22,942評論 1 92
  • 關(guān)于Mongodb的全面總結(jié) MongoDB的內(nèi)部構(gòu)造《MongoDB The Definitive Guide》...
    中v中閱讀 32,309評論 2 89
  • 去年有段時間得空,就把谷歌GAE的API權(quán)威指南看了一遍,收獲頗豐,特別是在自己幾乎獨立開發(fā)了公司的云數(shù)據(jù)中心之后...
    騎單車的勛爵閱讀 21,116評論 0 41
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,666評論 1 32
  • 有多少人在對你虎視眈眈,你沒有資格閉上眼睛。 我鄙視那些看到別人一點點進(jìn)步心里焦躁不安卻始終沒勇氣沒決心付出行動的...
    我是趙小趙閱讀 220評論 0 1

友情鏈接更多精彩內(nèi)容