机器学习项目中,一个模型训练好了之后,总要部署到服务器上去。以前我们项目采用的方式都是以离线学习为主,不要求数据处理的实时性。比如凌晨开始调用前一天的数据,跑出模型结果,存到数据库,早上上班后就可以运用模型结果了。但有些时候,要求数据能够实时给出模型结果,即一条新的数据一旦生成,立刻输入到模型中运算,得到的结果立刻返回,这叫在线学习。我们在亚马逊上买东西,通常会看到提示为“你可能也喜欢这些东西”,这些就是在线学习的结果。
这次我们的项目客户也提出了类似需求,这是一个投诉工单文本分类模型,需求是希望一个投诉工单生成后,立刻能够知道模型结果。坦白讲,这类需求我们项目组之前也没实现过,一开始也有点懵逼。好在公司团队肥肠强大,在咨询了其他项目组的一些大牛后,明白可以采用WebAPI的方式调取模型,实时获取模型结果。既然这周的博客任务还没完成,那就把这个项目经验写下来吧。。。
首先什么是API,如果你之前没有任何这方面的了解,你可能咋一看API的定义会一脸懵逼(比如我)。其实通俗的讲,可以将API比作一个窗口,你要通过这个窗口去实现一些功能,但你不用去管这些功能怎么实现的,你只要按照请求提交一些东西到窗口里,窗口另一边就会返回相应的东西。 比如去肯德基买汉堡,你不用管汉堡怎么做的,你只需按照要求,把钱递进去,说我要汉堡,肯德基就把香喷喷的汉堡递给你了。这就是API。按照这个定义,我们可以发现,函数就是一个API,我们按照函数的定义输入参数,函数就返回相应的结果,而我们不用管函数内部怎么实现的。我们日常打开网页的方式也是调用API,我们只需要输入网址,浏览器就会呈现相应的内容,我们不用管后台服务器端是如何渲染这个网页的。可以这么理解,通过一个url地址来实现API调用的,就叫WebAPI。
而我们现在的目的一样,调用方只需要负责将实时将数据,通过我们开发好的Web API传输过来,就可以调用模型,进而得到模型结果。在Web传输中,有两个重要概念,叫请求和响应,即,客户端向服务器发出请求,服务器向客户端返回响应。模型需要的输入数据,跟随着请求一起传输过来,模型的输出结果,通过响应回传给客户端。
更具体的,在我们的模型接口中(接口即API),一个请求,包含请求头(head)和请求体(body),请求头中包含目标url(这个url是开发接口的人定义的)和请求的方法(主要是get、post,差别后面讲);请求体中包含接口定义的输入数据(通常就是模型的输入项)。类似地,响应也包含响应头和响应体,我们要返回给模型的结果,就在响应体中。
具体的Web相关的理论知识,就不介绍太多了,一时半会说不完,说太多反而形成劝退的效果。(其实现在天冷,我盘腿蜷缩在沙发上码子,懒得查资料又怕记忆有偏差,所以懒得说了……)
那么具体来说我们要怎么做呢,我们要定义一个url,将这个url绑定一个函数,这个函数可以接收数据并调用模型。我们这个url交给调用方,告诉他,你通过这个url把数据传输给。一定他们调用了这个url——比如最简单的,在浏览器输入这个url地址,那么在服务器这边,一旦发现url被调用,就会触发我们定义的函数执行,这样就实现了接口调用。
怎么开发接口呢,Python的Flask库可以很方便的开发接口。我们要做的事情有:
- 定义一个url,并绑定一个函数,接下来是函数的处理逻辑。
- 函数获取数据,并判断数据类型是否争取,如果不对,我们就不方便进行下一步处理。
- 校验输入数据的字段是否齐全,比如模型需要10个输入字段,你只给了9个,那可不行。
- 调用模型运算
- 返回模型结果。
说得再多,不如直接看代码,我把注释写在代码里(我懒癌犯了)。注意,你别看我下面代码这么长,其实这是我在测试阶段一股脑都写上去了,真实的场景可能不需要这么多的,一个接口也不需要这么多代码,简单的可能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') # 建立日志文件,保存结果,这是个人习惯,不用管。
app = Flask(__name__) # 1.定义个一个Flask实例,__name__指定了程序主模块的名字,
Flask以此决定程序的根目录。即如果flask要从一个相对路径获取资源,根目录就是这个主
模块所在的目录。如果你不清楚,直接写__name__就可以了。
app.config['JSON_AS_ASCII'] = False #如果你返回的json数据数乱码,那么这个设定可以
帮助你。
class InputData(Form):#我们要对模型输入的数据,这个类定义了检查数据的方式。
# accept_content表示对输入数据中的 accept_content字段进行规范;StringField要求字
段必须是文本;validators参数指定检查器,可以有多个检查器,都放在[]里;DataRequired
意思是这个字段是必须的,其中的message表示如果没有这个输入这个数据,会提示什么文
字。
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脚本的内容,这个是用requests库调用api的方式。这句代码中:
requests.post表示执行post方法,对应的requests.get表示执行get方法;
http://127.0.0.1:5000/complaint就是我们指定的url,127.0.0.1是本机地址,是在本机测试
API时使用的,真正部署时,需要填服务器的API地址。5000是默认端口,也可以自己指
定。complaint是自己的定义,也可以取其他名字;json = data_json中,json表示数据必须以
json格式输入,这是我们开发接口时限定的条件,也可以限定其他格式。data_json中就是模
型需要的输入数据,为json格式。
# requests.post('http://127.0.0.1:5000/complaint', json = data_json)
@app.route('/complaint', methods=['post']) #这个装饰器定义了url的路径,并指定了调用方
法必须是post
def predition(): #这是核心程序,当调用上述url时,就会执行这个函数。
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函数,来处理输入数据不符合要求
时要怎么办。
else:
data = request.get_json() # 获取json数据
try:
data = json.loads(data) #转成字典,这是我这边调用模型要求输入必须字典格式。
except:
return bad_request() #如果无法转成字典,说明请求中的数据不是json格式,不符合
要求。
#json解析成字典,再作为关键字参数传输进去,验证数据,这步是必须的,必须转成
字典。
input = InputData(**data)
if not input.validate(): #input.validate()执行验证,如果验证通过,返回True
#如果没通过验证,input.errors返回错误信息,我们将错误信息返回给调用方,让他
们进行调整。
return jsonify(input.errors)
else:
#运行模型,测试阶段,如果模型出错,会返回测试数据给调用方,方便他们根据测
试数据继续开发。这是我自己的个性化需求,因为我们的开发人员,需要获取响应数据,再
执行其他开发工作。他们不在乎返回是什么,只要有返回就行了。如果因为我们模型的问
题,导致无法返回数据,会影响他们的开发工作。所以我这边定义模型如果出错,返回测试
结果,同时我记录出错数据和错误信息,再进行debug。
try:
data = input_column_to_eng(data) #将英文key转为中文,这是我的模型文件要求
的
pre_result = predict(data) #执行模型运算
log.info("request data \n:{0}".format(data)) #记录输出结果到日志文件
except: #测试期间,运行错误返回错误值,并记录错误原因,错误数据
log.error("=*20模型出错:", exc_info=True)
with open('cannot_predict.txt', 'a') as file: #记录出错数据
file.write(str(data) + '\n\n')
pre_result = {'测试数据':'测试结果'
}
pre_result_code = result_to_code(pre_result) # 转码输出结果,不用管
return jsonify(pre_result_code) #将模型结果,用json格式发送出去。
#一下装饰器,分别定义了各种接口调用错误的处理方式。要了解这些错误,需要去了解一
下相关的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
好了,大概就这样。其实开发web接口,Flask中有个插件叫FlaskRestful,是专门开发API用的,而Flask是用来开发网页的,开发接口只是他顺便的功能之一而已。我也用FlaskRestful写了个接口,下次看有没有必要找个机会发出来。。
这篇有点水,想到哪写到哪,很多点还没写清楚,历时一个半小时,就为了赶在12点前发出去。怎么说呢,不打脸比较重要吧。。12月的福州太冷了,瑟瑟发抖躲被窝去。。。