手把手:我的深度學(xué)習(xí)模型訓(xùn)練好了,然后要做啥?
大數(shù)據(jù)文摘作品
編譯:姜范波、云舟
本文講的是如何快速而不求***地部署一個(gè)訓(xùn)練好的機(jī)器學(xué)習(xí)模型并應(yīng)用到實(shí)際中。如果你已經(jīng)成功地使用諸如Tensorflow或Caffe這樣的框架訓(xùn)練好了一個(gè)機(jī)器學(xué)習(xí)模型,現(xiàn)在你正在試圖讓這個(gè)模型能夠快速的演示,那么讀這篇文章就對(duì)了。
使用前檢查清單
- 檢查tensorflow的安裝
- 從 stdin 運(yùn)行在線分類
- 在本地運(yùn)行分類
- 把分類器放到硬編碼(hardcoded)的代理
- 把分類器放到有服務(wù)發(fā)現(xiàn)(service discovery)的代理
- 用一個(gè)偽DNS調(diào)用分類器
機(jī)器學(xué)習(xí)的實(shí)際應(yīng)用
當(dāng)我們***次進(jìn)入Hive的機(jī)器學(xué)習(xí)空間時(shí),針對(duì)我們的實(shí)際應(yīng)用場(chǎng)景,我們已經(jīng)擁有了數(shù)百萬張準(zhǔn)確標(biāo)記的圖像,這些圖像使我們能夠在一周之內(nèi),從頭開始訓(xùn)練***進(jìn)的深度卷積神經(jīng)網(wǎng)絡(luò)圖像分類模型(即隨機(jī)權(quán)重)。然而,在更典型的應(yīng)用場(chǎng)景中,圖像的數(shù)量級(jí)通常只有數(shù)百幅,這種情況下,我建議微調(diào)現(xiàn)有的模型。比如,
https://www.tensorflow.org/tutorials/image_retraining有一個(gè)關(guān)于如何微調(diào)Imagenet模型(在1.2M圖像上訓(xùn)練1000個(gè)類別)以對(duì)花進(jìn)行分類的樣本數(shù)據(jù)集(3647個(gè)圖像, 5個(gè)類別)。
上面的Tensorflow教程簡(jiǎn)要而言,是在安裝bazel和tensorflow之后,需要運(yùn)行以下代碼,用大約30分鐘的來建模,5分鐘來訓(xùn)練:
- (
- cd "$HOME" && \
- curl -O http://download.tensorflow.org/example_images/flower_photos.tgz && \
- tar xzf flower_photos.tgz ;
- ) && \
- bazel build tensorflow/examples/image_retraining:retrain \
- tensorflow/examples/image_retraining:label_image \
- && \
- bazel-bin/tensorflow/examples/image_retraining/retrain \
- --image_dir "$HOME"/flower_photos \
- --how_many_training_steps=200
- && \
- bazel-bin/tensorflow/examples/image_retraining/label_image \
- --graph=/tmp/output_graph.pb \
- --labels=/tmp/output_labels.txt \
- --output_layer=final_result:0 \
- --image=$HOME/flower_photos/daisy/21652746_cc379e0eea_m.jpg
或者,如果你安裝了Docker,則可以使用以下預(yù)構(gòu)建的Docker鏡像:
- sudo docker run -it --net=host liubowei/simple-ml-serving:latest /bin/bash
- >>> cat test.sh && bash test.sh
這將進(jìn)入容器內(nèi)部的交互式shell中并運(yùn)行上述命令; 如果你愿意的話,也可以按照容器內(nèi)的其余部分進(jìn)行操作。
現(xiàn)在,tensorflow已經(jīng)將模型信息保存到/tmp/output_graph.pb和/tmp/output_labels.txt中,這些作為命令行參數(shù)傳遞給label_image.py腳本。Google的image_recognition教程也鏈接到另一個(gè)腳本,但是這里我們?nèi)詫⑹褂胠abel_image.py。
將本地運(yùn)行轉(zhuǎn)換為在線運(yùn)行(Tensorflow)
如果我們只想接受來自標(biāo)準(zhǔn)輸入的文件名,每行一個(gè),我們就可以很容易地進(jìn)行“在線”運(yùn)行:
- while read line ; do
- bazel-bin/tensorflow/examples/image_retraining/label_image \
- --graph=/tmp/output_graph.pb --labels=/tmp/output_labels.txt \
- --output_layer=final_result:0 \
- --image="$line" ;
- done
然而,從性能的角度來看這樣糟糕透了—— 每一個(gè)輸入都要重新加載神經(jīng)網(wǎng)絡(luò),權(quán)重,整個(gè)Tensorflow框架和python本身!
當(dāng)然可以改進(jìn)。先修改label_image.py 腳本。對(duì)我而言,這個(gè)腳本的位置在:
- in bazel-bin/tensorflow/examples/image_retraining/label_image.runfiles/org_tensorflow/tensorflow/examples/image_retraining/label_image.py.
修改如下:
- 141: run_graph(image_data, labels, FLAGS.input_layer, FLAGS.output_layer,
- 142: FLAGS.num_top_predictions)141: for line in sys.stdin:
修改后馬上快了很多,但這還不是***。
- 141: run_graph(image_data, labels, FLAGS.input_layer, FLAGS.output_layer,
- 142: FLAGS.num_top_predictions)141: for line in sys.stdin:
原因在于用with tf.Session()構(gòu)建對(duì)話。Tensorflow本質(zhì)上是在每次調(diào)用run_graph時(shí)將所有的計(jì)算加載到內(nèi)存中。一旦開始嘗試在GPU上進(jìn)行運(yùn)算,這一點(diǎn)就會(huì)變得很明顯——可以看到GPU內(nèi)存使用隨著Tensorflow加載和卸載GPU的模型參數(shù)而上下波動(dòng)。據(jù)我所知,這種結(jié)構(gòu)并不存在于Caffe或Pytorch框架中。
解決方法是把with命令去掉,傳遞一個(gè)sess變量到run_graph:
- def run_graph(image_data, labels, input_layer_name, output_layer_name,
- num_top_predictions, sess):
- # Feed the image_data as input to the graph.
- # predictions will contain a two-dimensional array, where one
- # dimension represents the input image count, and the other has
- # predictions per class
- softmax_tensor = sess.graph.get_tensor_by_name(output_layer_name)
- predictions, = sess.run(softmax_tensor, {input_layer_name: image_data})
- # Sort to show labels in order of confidence
- top_k = predictions.argsort()[-num_top_predictions:][::-1]
- for node_id in top_k:
- human_string = labels[node_id]
- score = predictions[node_id]
- print('%s (score = %.5f)' % (human_string, score))
- return [ (labels[node_id], predictions[node_id].item()) for node_id in top_k ] # numpy floats are not json serializable, have to run item
- ...
- with tf.Session() as sess:
- for line in sys.stdin:
- run_graph(load_image(line), labels, FLAGS.input_layer, FLAGS.output_layer,
- FLAGS.num_top_predictions, sess)
如果你運(yùn)行完這一段,你會(huì)發(fā)現(xiàn)每張圖只需要大約0.1秒,對(duì)于在線應(yīng)用來說已經(jīng)夠快了。
將本地運(yùn)行轉(zhuǎn)換為在線運(yùn)行(其他ML框架)
Caffe使用net.forward代碼,很容易被放入一個(gè)可調(diào)用的框架中:
see http://nbviewer.jupyter.org/github/BVLC/caffe/blob/master/examples/00-classification.ipynb
Mxnet也是非常獨(dú)特的:它實(shí)際上已經(jīng)準(zhǔn)備好了面向大眾的服務(wù)器代碼。
部署
我們的計(jì)劃是,將這些代碼包裝到一個(gè)Flask應(yīng)用程序中。如果你沒有聽說Flask,簡(jiǎn)單解釋一下,F(xiàn)lask是一個(gè)非常輕量級(jí)的Python Web框架,它允許你以最少的工作啟動(dòng)一個(gè)http api服務(wù)器。
作為一個(gè)快速參考,這里是一個(gè)Flask應(yīng)用程序,它接收包含多部分表單數(shù)據(jù)的POST請(qǐng)求:
- #!/usr/bin/env python
- # usage: python echo.py to launch the server ; and then in another session, do
- # curl -v -XPOST 127.0.0.1:12480 -F "data=@./image.jpg"
- from flask import Flask, request
- app = Flask(__name__)
- @app.route('/', methods=['POST'])
- def classify():
- try:
- data = request.files.get('data').read()
- print repr(data)[:1000]
- return data, 200
- except Exception as e:
- return repr(e), 500
- app.run(host='127.0.0.1',port=12480)
這里是如何將相應(yīng)的FLASK應(yīng)用程序連接到上面的run_graph:
- And here is the corresponding flask app hooked up to run_graph above:
- #!/usr/bin/env python
- # usage: bash tf_classify_server.sh
- from flask import Flask, request
- import tensorflow as tf
- import label_image as tf_classify
- import json
- app = Flask(__name__)
- FLAGS, unparsed = tf_classify.parser.parse_known_args()
- labels = tf_classify.load_labels(FLAGS.labels)
- tf_classify.load_graph(FLAGS.graph)
- sess = tf.Session()
- @app.route('/', methods=['POST'])
- def classify():
- try:
- data = request.files.get('data').read()
- result = tf_classify.run_graph(data, labels, FLAGS.input_layer, FLAGS.output_layer, FLAGS.num_top_predictions, sess)
- return json.dumps(result), 200
- except Exception as e:
- return repr(e), 500
- app.run(host='127.0.0.1',port=12480)
模型部署至此看起來還是相當(dāng)不錯(cuò)的。除了一點(diǎn)——需要FlASK和Tensorflow完全同步——Flask按照接收的順序一次處理一個(gè)請(qǐng)求,并且Tensorflow在進(jìn)行圖像分類時(shí)完全占用線程。
速度瓶頸可能還是在實(shí)際的計(jì)算工作中,所以升級(jí)Flask包裝代碼沒有太多的意義?,F(xiàn)在,也許這個(gè)代碼足以處理你的負(fù)載。
有兩種顯而易見的方法可以擴(kuò)大請(qǐng)求的通量:通過增加工人數(shù)量來橫向放大,這在下一節(jié)將會(huì)介紹,或者通過使用GPU和批處理邏輯來縱向擴(kuò)展。實(shí)現(xiàn)后者需要一個(gè)能夠一次處理多個(gè)待處理請(qǐng)求的web服務(wù)器,并決定是否繼續(xù)等待更大的批處理或?qū)⑵浒l(fā)送到Tensorflow圖形線程進(jìn)行分類,對(duì)于這個(gè)Flask應(yīng)用程序是非常不適合的。有兩種可能性:使用Twisted + Klein來保留Python代碼,或者如果你更喜歡***的事件循環(huán)支持,并且能夠連接到非Python ML框架(如Torch),則可以使用Node.js + ZeroMQ。
擴(kuò)展:負(fù)載平衡和服務(wù)發(fā)現(xiàn)
那么,假設(shè)現(xiàn)在你只有一臺(tái)服務(wù)器來部署模型,由于它太慢了,或者我們的負(fù)載變得太高了,此時(shí)你想要啟動(dòng)更多服務(wù)器——如何在每個(gè)服務(wù)器上分配請(qǐng)求?
常規(guī)的方法是添加一個(gè)代理層,也許是haproxy或nginx,它能夠平衡后端服務(wù)器之間的負(fù)載,同時(shí)向客戶端呈現(xiàn)一個(gè)統(tǒng)一的接口。為了在本節(jié)稍后使用,以下是運(yùn)行基本Node.js負(fù)載均衡器http代理的一些示例代碼:
- // Usage : node basic_proxy.js WORKER_PORT_0,WORKER_PORT_1,...
- const worker_ports = process.argv[2].split(',')
- if (worker_ports.length === 0) { console.err('missing worker ports') ; process.exit(1) }
- const proxy = require('http-proxy').createProxyServer({})
- proxy.on('error', () => console.log('proxy error'))
- let i = 0
- require('http').createServer((req, res) => {
- proxy.web(req,res, {target: 'http://localhost:' + worker_ports[ (i++) % worker_ports.length ]})
- }).listen(12480)
- console.log(`Proxying localhost:${12480} to [${worker_ports.toString()}]`)
- // spin up the ML workers
- const { exec } = require('child_process')
- worker_ports.map(port => exec(`/bin/bash ./tf_classify_server.sh ${port}`))
為了自動(dòng)檢測(cè)后端服務(wù)器的數(shù)量和位置,人們通常使用“服務(wù)發(fā)現(xiàn)”工具,該工具可能與負(fù)載平衡器捆綁在一起,或者是分開的。一些知名例子的是Consul和Zookeeper。設(shè)置和學(xué)習(xí)使用它們不在本文的討論范圍之內(nèi),所以我使用了一個(gè)非?;镜?,通過node.js服務(wù)發(fā)現(xiàn)包seport實(shí)現(xiàn)的代理。
Proxy代碼:
- // Usage : node seaport_proxy.js
- const seaportServer = require('seaport').createServer()
- seaportServer.listen(12481)
- const proxy = require('http-proxy').createProxyServer({})
- proxy.on('error', () => console.log('proxy error'))
- let i = 0
- require('http').createServer((req, res) => {
- seaportServer.get('tf_classify_server', worker_ports => {
- const this_port = worker_ports[ (i++) % worker_ports.length ].port
- proxy.web(req,res, {target: 'http://localhost:' + this_port })
- })
- }).listen(12480)
- console.log(`Seaport proxy listening on ${12480} to '${'tf_classify_server'}' servers registered to ${12481}`)
Worker代碼:
- // Usage : node tf_classify_server.js
- const port = require('seaport').connect(12481).register('tf_classify_server')
- console.log(`Launching tf classify worker on ${port}`)
- require('child_process').exec(`/bin/bash ./tf_classify_server.sh ${port}`)
然而,當(dāng)應(yīng)用于機(jī)器學(xué)習(xí)時(shí),這個(gè)設(shè)置遇到了帶寬問題。
每秒幾十到幾百?gòu)垐D像,這個(gè)系統(tǒng)就會(huì)成為網(wǎng)絡(luò)帶寬的瓶頸。在目前的設(shè)置中,所有的數(shù)據(jù)都必須通過我們的單個(gè)seaport 主節(jié)點(diǎn),這也是呈現(xiàn)給客戶端的端點(diǎn)。
為了解決這個(gè)問題,我們需要我們的客戶端不要訪問http://127.0.0.1:12480這個(gè)端點(diǎn),而是要在后端服務(wù)器之間通過自動(dòng)輪換來訪問。如果你懂網(wǎng)絡(luò),一定會(huì)想:這不就是DNS干的活嘛!
但是,設(shè)置自定義的DNS服務(wù)器已經(jīng)超出了本文的范圍。相反,通過更改客戶端以遵循兩步“手動(dòng)DNS”協(xié)議,我們可以重新使用我們的基礎(chǔ)版的seaport 代理來實(shí)現(xiàn)客戶端直接連接到其服務(wù)器的“點(diǎn)對(duì)點(diǎn)”協(xié)議:
Proxy代碼:
- // Usage : node p2p_proxy.js
- const seaportServer = require('seaport').createServer()
- seaportServer.listen(12481)
- let i = 0
- require('http').createServer((req, res) => {
- seaportServer.get('tf_classify_server', worker_ports => {
- const this_port = worker_ports[ (i++) % worker_ports.length ].port
- res.end(`${this_port}
- `)
- })
- }).listen(12480)
- console.log(`P2P seaport proxy listening on ${12480} to 'tf_classify_server' servers registered to ${12481}`)(Worker 代碼同上)
Client代碼:
- curl -v -XPOST localhost:`curl localhost:12480` -F"data=@$HOME/flower_photos/daisy/21652746_cc379e0eea_m.jpg"
結(jié)論和進(jìn)一步閱讀
至此你的系統(tǒng)應(yīng)該可以進(jìn)入實(shí)際應(yīng)用了,但它總是要發(fā)展的。本指南中未涉及幾個(gè)重要的主題:
1. 新硬件上的自動(dòng)部署和設(shè)置。
- 值得注意的工具包括Openstack / VMware(如果您使用的是自己的硬件),Chef / Puppet(用于安裝Docker并處理網(wǎng)絡(luò)路由)以及Docker(用于安裝Tensorflow,Python等)。
- 如果你在云端,Kubernetes或Marathon / Mesos也很棒
2. 模型版本管理
- 一開始手動(dòng)管理不難。
- Tensorflow Serving是一個(gè)很好的工具,可以非常徹底地處理這個(gè)問題,以及批處理和整體部署。 缺點(diǎn)是設(shè)置和編寫客戶端代碼有點(diǎn)難,另外不支持Caffe / PyTorch。
3. 如何將機(jī)器學(xué)習(xí)代碼從Matlab中遷移出來。
- 在生產(chǎn)階段不要用Matlab
4. GPU驅(qū)動(dòng),Cuda,CUDNN
- 使用nvidia-docker,試試其它的在線Dockfiles。
5. 后處理層。
- 一旦你在生產(chǎn)中得到了一些不同的ML模型,你可能會(huì)開始想要混合和匹配不同的用例——只有在模型B不確定的情況下才運(yùn)行模型A,在Caffe中運(yùn)行模型C并將結(jié)果傳遞給模型D在Tensorflow 等等。
來源:
https://thehive.ai/blog/simple-ml-serving?utm_campaign=Revue%20newsletter&utm_medium=Newsletter&utm_source=The%20Wild%20Week%20in%20AI
【本文是51CTO專欄機(jī)構(gòu)大數(shù)據(jù)文摘的原創(chuàng)譯文,微信公眾號(hào)“大數(shù)據(jù)文摘( id: BigDataDigest)”】