當咱們經過深度學習完成模型訓練後,有時但願能將模型落地於生產,能開發API接口被終端調用,這就涉及了模型的部署工做。Modelarts支持對tensorflow,mxnet,pytorch等模型的部署和在線預測,這裏老山介紹下tensorflow的模型部署。html
模型部署的工做其實是將模型預測函數搬到了線上,一般一個典型的模型預測流程以下圖所示:python
模型部署時,咱們須要作的事情以下:git
用戶的輸入輸出使用config.json文件來定義;github
預處理模塊和後處理模塊customize_service.py經過複寫TfServingBaseService模塊的相關函數來實現;json
tensorflow模型須要改寫成savedModel模型;api
tensorflow模型自己做爲一個黑盒子,不需關心也沒法關心,也就是說你沒法在服務啓動後對計算圖增長節點了。安全
因爲模型部署是在模型預測的基礎上從新定義的,因此若是模型已經寫好了預測的函數,咱們就很方便的經過改寫程序來進行模型部署工做。session
下面便介紹下在modelarts部署bert模型的流程。app
本文部署的模型是在華爲雲 ModelArts-Lab AI實戰營第七期的基礎上的,請你們重走一遍案例,但務必記得,與案例不一樣的是,開發環境請選擇Tensorflow 1.8,中間有段安裝tensorflow 1.11的代碼也請跳過。這是由於在本文成文時,modelarts暫時只支持tensorflow 1.8的模型。本章節的全部代碼都在modelarts的notebook上完成。less
執行完案例後,在./ner/output路徑下有訓練模型的結果,但這模型還不能直接用於Tensorflow Serving,咱們必須先把他轉成savedModel模型。
咱們先找到./ner/src/terminal_predict.py文件,直接找到預測用的主函數(爲了閱讀方便,略去了不關注的代碼)。
def predict_online(): global graph with graph.as_default(): # ...bala bala nosense # ------用戶輸入 sentence = str(input()) # nosence again # ------------前處理 sentence = tokenizer.tokenize(sentence) input_ids, input_mask, segment_ids, label_ids = convert(sentence) feed_dict = {input_ids_p: input_ids, input_mask_p: input_mask} # run session get current feed_dict result # -------------使用模型(黑盒子) pred_ids_result = sess.run([pred_ids], feed_dict) # -------------後處理 pred_label_result = convert_id_to_label(pred_ids_result, id2label) result = strage_combined_link_org_loc(sentence, pred_label_result[0]) # 輸出被這個函數封裝了 # something useless
在模型使用這塊,程序使用了sess這個tf.Session的實例做爲全局變量調用,在程序很少的執行代碼中,能夠看到主要是作了重構模型計算圖。
graph = tf.get_default_graph() with graph.as_default(): print("going to restore checkpoint") #sess.run(tf.global_variables_initializer()) # -----定義了模型的兩個輸出張量 input_ids_p = tf.placeholder(tf.int32, [batch_size, max_seq_length], name="input_ids") input_mask_p = tf.placeholder(tf.int32, [batch_size, max_seq_length], name="input_mask") bert_config = modeling.BertConfig.from_json_file(os.path.join(bert_dir, 'bert_config.json')) # ----定義了模型的輸出張量,因爲後處理只用到pred_ids,其餘無論 (total_loss, logits, trans, pred_ids) = create_model( bert_config=bert_config, is_training=False, input_ids=input_ids_p, input_mask=input_mask_p, segment_ids=None, labels=None, num_labels=num_labels, use_one_hot_embeddings=False, dropout_rate=1.0) saver = tf.train.Saver() saver.restore(sess, tf.train.latest_checkpoint(model_dir))
既然sess在可執行代碼中幫咱們構建好了,咱們源代碼一行不動,直接引用就能夠生成savedModel模型
from ner.src.terminal_predict import * export_path = './model' builder = tf.saved_model.builder.SavedModelBuilder(export_path) # 將輸入張量與名稱掛鉤 signature_inputs = { 'input_ids': tf.saved_model.utils.build_tensor_info(input_ids_p), 'input_mask': tf.saved_model.utils.build_tensor_info(input_mask_p), } # 將輸出張量與名稱掛鉤 signature_outputs = { 'pred_ids':tf.saved_model.utils.build_tensor_info(pred_ids), } # 簽名定義?不懂就往下看輸出結果 classification_signature_def = tf.saved_model.signature_def_utils.build_signature_def( inputs=signature_inputs, outputs=signature_outputs, method_name=tf.saved_model.signature_constants.PREDICT_METHOD_NAME) builder.add_meta_graph_and_variables( sess, [tf.saved_model.tag_constants.SERVING], signature_def_map={ 'root': classification_signature_def }, ) builder.save()
這樣就在export_path路徑下生成了savedModel模型,模型文件以下
model ├── saved_model.pb ├── variables │ ├── variables.index │ └── variables.data-00000-of-00001
生成模型後咱們能夠進行預測,來判斷模型是否正確
首先咱們輸出signature_def
sess = tf.Session() meta_graph_def = tf.saved_model.loader.load(sess, [tf.saved_model.tag_constants.SERVING], export_dir) signature = meta_graph_def.signature_def signature['root']
signature_def:
inputs { key: "input_ids" value { name: "input_ids:0" dtype: DT_INT32 tensor_shape { dim { size: 1 } dim { size: 128 } } } } inputs { key: "input_mask" value { name: "input_mask:0" dtype: DT_INT32 tensor_shape { dim { size: 1 } dim { size: 128 } } } } outputs { key: "pred_ids" value { name: "ReverseSequence_1:0" dtype: DT_INT32 tensor_shape { dim { size: 1 } dim { size: 128 } } } } method_name: "tensorflow/serving/predict"
可見這個簽名定義了模型的輸入輸出格式的信息。因爲模型上線封裝後,沒法獲取具體節點張量,因此輸入輸出就用節點的名稱來替代,也就是裏面的key值。
接下來,咱們寫個預測函數,來看看結果
# 直接照抄 def convert(line): feature = convert_single_example(0, line, label_list, max_seq_length, tokenizer, 'p') input_ids = np.reshape([feature.input_ids],(batch_size, max_seq_length)) input_mask = np.reshape([feature.input_mask],(batch_size, max_seq_length)) segment_ids = np.reshape([feature.segment_ids],(batch_size, max_seq_length)) label_ids =np.reshape([feature.label_ids],(batch_size, max_seq_length)) return input_ids, input_mask, segment_ids, label_ids # 基本照抄,改變了輸出,變成dict def strage_combined_link_org_loc_2(tokens, tags): def print_output(data, type): line = [] for i in data: line.append(i.word) return [i.word for i in data] params = None eval = Result(params) if len(tokens) > len(tags): tokens = tokens[:len(tags)] person, loc, org = eval.get_result(tokens, tags) return {'LOC': print_output(loc, 'LOC'), 'PER': print_output(person, 'PER'), 'ORG': print_output(org, 'ORG'),} # 線下調用模型的函數,得本身寫,不過測試完就扔掉了 def predict(f1, f2): x1_tensor_name = signature['root'].inputs['input_ids'].name x2_tensor_name = signature['root'].inputs['input_mask'].name y1_tensor_name = signature['root'].outputs['pred_ids'].name x1 = sess.graph.get_tensor_by_name(x1_tensor_name) x2 = sess.graph.get_tensor_by_name(x2_tensor_name) y1 = sess.graph.get_tensor_by_name(y1_tensor_name) y1 = sess.run(y1, feed_dict={x1:f1,x2:f2}) return y1 # 輸入 sentence = '中國男籃與委內瑞拉隊在北京五棵松體育館展開小組賽最後一場比賽的爭奪,趙繼偉12分4助攻3搶斷、易建聯11分8籃板、周琦8分7籃板2蓋帽。' # 前處理 input_ids, input_mask, segment_ids, label_ids = convert(sentence) # 調用模型 y1 = predict(input_ids, input_mask) # 後處理 pred_label_result = convert_id_to_label([y1], id2label) result = strage_combined_link_org_loc_2(sentence, pred_label_result[0]) # 輸出 result
out:
{'LOC': ['北京五棵松體育館'], 'ORG': ['中國男籃', '委內瑞拉隊'], 'PER': ['趙繼偉', '易建聯', '周琦']}
以上就線下預測的模塊。線上預測大致相似,但仍還須要少許的代碼更改,以及無用的代碼塊剔除。
config.json文件生成
config.json編寫可查看規範,其中apis中以json scheme定義了用戶輸入輸出方式,也是最頭疼的地方。老山看來,apis部分描述的做用大於對程序的實際影響,若是你自己熟悉程序的輸入方式,徹底能夠定義最外層便可,無須對內部仔細定義。dependencies模塊除非須要特定版本或是真的用了些不常見的工程,不然能夠不寫。
config.json
{ "model_algorithm": "bert_ner", "model_type": "TensorFlow", "runtime": "python3.6", "apis": [ { "procotol": "http", "url": "/", "method": "post", "request": { "Content-type": "multipart/form-data", "data": { "type": "object", "properties": { "sentence": { "type": "string" } } } }, "response": { "Content-type": "applicaton/json", "data": { "type": "object", "properties": { } } } } ] }
這裏規範了輸入必須是{"sentence":"須要輸入的句子"}這麼個格式。
customize_service.py生成
這個模塊定義了預處理和後處理,重要性不可謂不重要。一樣能夠找到規範,這個也是你會花費最多時間去反覆修改的程序。這裏老山講一下幾點經驗,方便你們參考:
程序經過新建TfServingBaseService的子類來重寫_preprocess和_postprocess函數;
若是是.py文件,正常引用即是;若是是其餘文件,在類內經過self.model_path得到路徑;
若是用後處理後須要用到前處理的變量,把該變量變成類的屬性(如今由於都是同步的,若是之後加入異步功能,這樣簡單的處理方法有可能會引發線程安全問題);
引用其餘.py文件時,命名請儘可能刁鑽,如(utils.py -> utils_.py, config.py -> config_.py),避免和服務自己的模塊重名;
程序儘可能剪枝,一些無關程序就刪除把;
customize_service.py
from __future__ import absolute_import from __future__ import division from __future__ import print_function import tensorflow as tf import os import numpy as np import json from model_service.tfserving_model_service import TfServingBaseService import tokenization from utils_ import convert_, convert_id_to_label, strage_combined_link_org_loc from config_ import do_lower_case, id2label class BertPredictService(TfServingBaseService): def _preprocess(self, data): tokenizer = tokenization.FullTokenizer( vocab_file=os.path.join(self.model_path, 'vocab.txt'), do_lower_case=do_lower_case) sentence = data['sentence'] # 把sentence保存在類中,方便後處理時調用 self.sentence = sentence input_ids, input_mask, *_ = convert_(sentence, tokenizer) feed = {'input_ids': input_ids.astype(np.int32), 'input_mask': input_mask.astype(np.int32)} print("feed:", feed) return feed def _postprocess(self, data): pred_ids = data['pred_ids'] pred_label_result = convert_id_to_label([pred_ids], id2label) result = strage_combined_link_org_loc(self.sentence, pred_label_result[0]) return result
這裏引用了bert自帶的tokenization模塊,config_模塊裏面都是些常量,utils_模塊基本就是把terminal_predict.py裏的模型相關的全刪除掉,改吧改吧弄出來的,這裏再也不贅述了。
在部署以前,必須安裝規範存儲在obs中,此次老山存儲的目錄以下。
obs-name └── ocr └── model ├── config.json ├── config_.py ├── customize_service.py ├── saved_model.pb ├── tokenization.py ├── utils_.py ├── variables │ ├── variables.data-00000-of-00001 │ └── variables.index └── vocab.txt
在modelarts控制檯上左側導航欄選擇模型管理 -> 模型列表,在中間的模型列表中選擇導入
在導入模型頁面上,修改名稱,在元模型來源選擇從OBS中選擇,選擇元模型的路徑後,點擊馬上建立。
返回模型列表,等待模型狀態變成正常
在模型列表中選擇建立的模型,選擇部署
部署會須要2-3分鐘不等的時間,等待部署成功後,點擊服務的名稱,進入在線服務的頁面
在線服務頁面選擇預測標籤欄,輸入預測代碼:{"sentence":"中國男籃與委內瑞拉隊在北京五棵松體育館展開小組賽最後一場比賽的爭奪,趙繼偉12分4助攻3搶斷、易建聯11分8籃板、周琦8分7籃板2蓋帽。"},點擊預測,在返回結果處能夠看到結果與以前模型測試結果相同。
在調用指南標籤頁中給出了服務的API接口地址
官方文檔介紹瞭如何使用Postman和curl調用API接口,你們自行查閱,老山這個給出的是如何使用python來調用API。
首先是選擇認證方式。一個是AK/SK認證,也就是每次調用都直接使用AK/SK來請求,無疑要對AK/SK進行加密,這意味着基本上不折騰的方式就是使用官方的模塊。另一種是X-Auth-Token認證,有時效,每次使用X-Auth-Token調用請求便可,但在獲取X-Auth-Token時請求結構體中要明文方式輸入帳戶和密碼,安全性上還值得商榷。但這裏天然是選用第二種方法,相對靈活些。
首先是請求X-Auth-Token
import requests import json url = "https://iam.cn-north-1.myhuaweicloud.com/v3/auth/tokens" headers = {"Content-Type":"application/json"} data = { "auth": { "identity": { "methods": ["password"], "password": { "user": { "name": "your-username", # 帳戶 "password": "your-password", # 密碼 "domain": { "name": "your-domainname:normally equal to your-username" #域帳戶,普通帳戶這裏就仍是填帳戶 } } } }, "scope": { "project": { "name": "cn-north-1" } } } } data = json.dumps(data) r = requests.post(url, data = data, headers = headers) print(r.headers['X-Subject-Token'])
data具體參數基本上就是帳號和密碼,具體細節可參考官網。
程序最後得到的即是X-Auth-Token認證碼。得到認證碼後即可進行預測了。
config.py
X_Auth_Token = "MIIZpAYJKoZIhvcNAQcCoIIZlTCC..." # 前面獲取的X-Auth-Token值 url = "https://39ae62200d7f439eaae44c7cabccf5de.apig..." #在調用指南頁面獲取的url值
predict.py
import requests from config import url, X_Auth_Token import json def bertService(sentence): data = {"sentence":sentence} data = json.dumps(data) headers = {"content-type": "application/json", 'X-Auth-Token': X_Auth_Token} response = requests.request("POST", url, data = data, headers=headers) return response.text if __name__ == "__main__": print(bertService('中國男籃與委內瑞拉隊在北京五棵松體育館展開小組賽最後一場比賽的爭奪,趙繼偉12分4助攻3搶斷、易建聯11分8籃板、周琦8分7籃板2蓋帽。'))
輸出結果:
{"LOC": ["北京五棵松體育館"], "PER": ["趙繼偉", "易建聯", "周琦"], "ORG": ["中國男籃", "委內瑞拉隊"]}
當不須要使用服務時,請點擊在線服務頁面右上角的中止,以免產生沒必要要的費用。
做者:山找海味