手把手:我的深度學習模型訓練好了,然後要做啥?

大資料文摘發表於2018-01-28

本文講的是如何快速而不求完美地部署一個訓練好的機器學習模型並應用到實際中。如果你已經成功地使用諸如Tensorflow或Caffe這樣的框架訓練好了一個機器學習模型,現在你正在試圖讓這個模型能夠快速的演示,那麼讀這篇文章就對了。

閱讀時長: 10-15分鐘

使用前檢查清單

  • 檢查tensorflow的安裝

  • 從 stdin 執行線上分類

  • 在本地執行分類

  • 把分類器放到硬編碼(hardcoded)的代理

  • 把分類器放到有服務發現(service discovery)的代理

  • 用一個偽DNS呼叫分類器

機器學習的實際應用

當我們第一次進入Hive的機器學習空間時,針對我們的實際應用場景,我們已經擁有了數百萬張準確標記的影像,這些影像使我們能夠在一週之內,從頭開始訓練最先進的深度卷積神經網路影像分類模型(即隨機權重)。然而,在更典型的應用場景中,影像的數量級通常只有數百幅,這種情況下,我建議微調現有的模型。比如,https://www.tensorflow.org/tutorials/image_retraining有一個關於如何微調Imagenet模型(在1.2M影像上訓練1000個類別)以對花進行分類的樣本資料集(3647個影像, 5個類別)。

上面的Tensorflow教程簡要而言,是在安裝bazel和tensorflow之後,需要執行以下程式碼,用大約30分鐘的來建模,5分鐘來訓練:

(
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,則可以使用以下預構建的Docker映象:

sudo docker run -it --net=host liubowei/simple-ml-serving:latest /bin/bash

>>> cat test.sh && bash test.sh

這將進入容器內部的互動式shell中並執行上述命令; 如果你願意的話,也可以按照容器內的其餘部分進行操作。

現在,tensorflow已經將模型資訊儲存到/tmp/output_graph.pb和/tmp/output_labels.txt中,這些作為命令列引數傳遞給label_image.py指令碼。Google的image_recognition教程也連結到另一個指令碼,但是這裡我們仍將使用label_image.py。

將本地執行轉換為線上執行(Tensorflow)

如果我們只想接受來自標準輸入的檔名,每行一個,我們就可以很容易地進行“線上”執行:

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

然而,從效能的角度來看這樣糟糕透了—— 每一個輸入都要重新載入神經網路權重,整個Tensorflow框架和python本身!

當然可以改進。先修改label_image.py 指令碼。對我而言,這個指令碼的位置在:

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:  for line in sys.stdin:
142:    run_graph(load_image(line), labels, FLAGS.input_layer, FLAGS.output_layer,
142:        FLAGS.num_top_predictions)

原因在於用with tf.Session()構建對話。Tensorflow本質上是在每次呼叫run_graph時將所有的計算載入到記憶體中。一旦開始嘗試在GPU上進行運算,這一點就會變得很明顯——可以看到GPU記憶體使用隨著Tensorflow載入和解除安裝GPU的模型引數而上下波動。據我所知,這種結構並不存在於Caffe或Pytorch框架中。

解決方法是把with命令去掉,傳遞一個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)

如果你執行完這一段,你會發現每張圖只需要大約0.1秒,對於線上應用來說已經夠快了。

將本地執行轉換為線上執行(其他ML框架)

Caffe使用net.forward程式碼,很容易被放入一個可呼叫的框架中:see http://nbviewer.jupyter.org/github/BVLC/caffe/blob/master/examples/00-classification.ipynb

Mxnet也是非常獨特的:它實際上已經準備好了面向大眾的伺服器程式碼。

部署

我們的計劃是,將這些程式碼包裝到一個Flask應用程式中。如果你沒有聽說Flask,簡單解釋一下,Flask是一個非常輕量級的Python Web框架,它允許你以最少的工作啟動一個http api伺服器。

作為一個快速參考,這裡是一個Flask應用程式,它接收包含多部分表單資料的POST請求:

#!/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)

這裡是如何將相應的FLASK應用程式連線到上面的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)

模型部署至此看起來還是相當不錯的。除了一點——需要FlASK和Tensorflow完全同步——Flask按照接收的順序一次處理一個請求,並且Tensorflow在進行影像分類時完全佔用執行緒。

速度瓶頸可能還是在實際的計算工作中,所以升級Flask包裝程式碼沒有太多的意義。現在,也許這個程式碼足以處理你的負載。

有兩種顯而易見的方法可以擴大請求的通量:通過增加工人數量來橫向放大,這在下一節將會介紹,或者通過使用GPU和批處理邏輯來縱向擴充套件。實現後者需要一個能夠一次處理多個待處理請求的web伺服器,並決定是否繼續等待更大的批處理或將其傳送到Tensorflow圖形執行緒進行分類,對於這個Flask應用程式是非常不適合的。有兩種可能性:使用Twisted + Klein來保留Python程式碼,或者如果你更喜歡一流的事件迴圈支援,並且能夠連線到非Python ML框架(如Torch),則可以使用Node.js + ZeroMQ。

擴充套件:負載平衡和服務發現

那麼,假設現在你只有一臺伺服器來部署模型,由於它太慢了,或者我們的負載變得太高了,此時你想要啟動更多伺服器——如何在每個伺服器上分配請求?

常規的方法是新增一個代理層,也許是haproxy或nginx,它能夠平衡後端伺服器之間的負載,同時向客戶端呈現一個統一的介面。為了在本節稍後使用,以下是執行基本Node.js負載均衡器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}`))

為了自動檢測後端伺服器的數量和位置,人們通常使用“服務發現”工具,該工具可能與負載平衡器捆綁在一起,或者是分開的。一些知名例子的是Consul和Zookeeper。設定和學習使用它們不在本文的討論範圍之內,所以我使用了一個非常基本的,通過node.js服務發現包seport實現的代理。

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}`)

然而,當應用於機器學習時,這個設定遇到了頻寬問題。

每秒幾十到幾百張影像,這個系統就會成為網路頻寬的瓶頸。在目前的設定中,所有的資料都必須通過我們的單個seaport 主節點,這也是呈現給客戶端的端點。

為了解決這個問題,我們需要我們的客戶端不要訪問http://127.0.0.1:12480這個端點,而是要在後端伺服器之間通過自動輪換來訪問。如果你懂網路,一定會想:這不就是DNS乾的活嘛!

但是,設定自定義的DNS伺服器已經超出了本文的範圍。相反,通過更改客戶端以遵循兩步“手動DNS”協議,我們可以重新使用我們的基礎版的seaport 代理來實現客戶端直接連線到其伺服器的“點對點”協議:

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"

結論和進一步閱讀

至此你的系統應該可以進入實際應用了,但它總是要發展的。本指南中未涉及幾個重要的主題:

新硬體上的自動部署和設定。

o   值得注意的工具包括Openstack / VMware(如果您使用的是自己的硬體),Chef / Puppet(用於安裝Docker並處理網路路由)以及Docker(用於安裝Tensorflow,Python等)。

o   如果你在雲端,Kubernetes或Marathon / Mesos也很棒

模型版本管理

o   一開始手動管理不難。

o   Tensorflow Serving是一個很好的工具,可以非常徹底地處理這個問題,以及批處理和整體部署。 缺點是設定和編寫客戶端程式碼有點難,另外不支援Caffe / PyTorch。

如何將機器學習程式碼從Matlab中遷移出來。

o   在生產階段不要用Matlab

GPU驅動,Cuda,CUDNN

o   使用nvidia-docker,試試其它的線上Dockfiles。

後處理層。

o   一旦你在生產中得到了一些不同的ML模型,你可能會開始想要混合和匹配不同的用例——只有在模型B不確定的情況下才執行模型A,在Caffe中執行模型C並將結果傳遞給模型D在Tensorflow 等等。

相關文章