TensorFlow機器學習模型快速部署指南

本文經機器之心(微信公眾號:almosthuman2014)授權轉載,禁止二次轉載。

GitHub 地址:github.com/hiveml/simpl

其中包含的條目有:

  • 檢查 TensorFlow 安裝:github.com/hiveml/simpl
  • 利用 stdin 運行在線分類:github.com/hiveml/simpl
  • 在本地主機上運行在線分類:github.com/hiveml/simpl
  • 將分類器放在硬編碼代理器後面:github.com/hiveml/simpl
  • 將分類器放在可實現服務發現的代理器後面:github.com/hiveml/simpl
  • 利用偽 DN 啟用分類器:github.com/hiveml/simpl

生產環境中的機器學習

第一次進入 Hive 的機器學習空間,我們就已經擁有數百萬個真值標註圖像,這可以讓我們在一周時間內從頭訓練(即隨機權重)適用於特定使用案例的頂尖深度卷積圖像分類模型。更典型的 ML 用例通常基於數百個圖像,這種情況我推薦大家對現有模型進行微調。例如,tensorflow.org/tutorial 頁面上有如何微調 ImageNet 模型對花樣本數據集(3647 張圖像,5 個類別)進行分類的教程。

安裝 Bazel 和 TensorFlow 後,你需要運行以下代碼,構建大約需要 30 分鐘,訓練需要 5 分鐘:

(ncd "$HOME" && ncurl -O http://download.tensorflow.org/example_images/flower_photos.tgz && ntar xzf flower_photos.tgz ;n) && nbazel build tensorflow/examples/image_retraining:retrain n tensorflow/examples/image_retraining:label_image n&& nbazel-bin/tensorflow/examples/image_retraining/retrain n--image_dir "$HOME"/flower_photos n--how_many_training_steps=200n&& nbazel-bin/tensorflow/examples/image_retraining/label_image n--graph=/tmp/output_graph.pb n--labels=/tmp/output_labels.txt n--output_layer=final_result:0 n--image=$HOME/flower_photos/daisy/21652746_cc379e0eea_m.jpgn

或者,如果你有 Docker,可以使用預製 Docker 圖像,

sudo docker run -it --net=host liubowei/simple-ml-serving:latest /bin/bashnn>>> cat test.sh && bash test.shn

進入容器中的互動式 shell,運行以上命令。你也可以閱讀下文,在容器中按照下文說明進行操作。

現在,TensorFlow 將模型信息保存至/tmp/output_graph.pb 和 /tmp/output_labels.txt,二者作為命令行參數被輸入至 label_image.py (github.com/tensorflow/t) 腳本。谷歌的圖像識別教程也與另一個腳本(github.com/tensorflow/m)有關,但是在這個例子中,我們將繼續使用 label_image.py。

將單點推斷轉換成在線推斷(TensorFlow)

如果我們只想接受標準輸入的文件名,一行一個,則我們可以輕鬆實現「在線」推斷:

while read line ; donbazel-bin/tensorflow/examples/image_retraining/label_image n--graph=/tmp/output_graph.pb --labels=/tmp/output_labels.txt n--output_layer=final_result:0 n--image="$line" ;ndonen

如果以性能為出發點來看,這太糟糕了:我們需要為每個輸入樣本重新載入神經網路、權重、整個 TensorFlow 架構和 Python!

我們當然可以做得更好。讓我們從編輯 label_image.py script 開始。它的地址為 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,n142: FLAGS.num_top_predictions)n

改為:

141: for line in sys.stdin:n142: run_graph(load_image(line), labels, FLAGS.input_layer, FLAGS.output_layer,n142: FLAGS.num_top_predictions)n

這樣速度快多了,但是仍然不是最好!

原因在於第 100 行的 with tf.Session() as sess 構造。本質上,TensorFlow 在每次啟用 run_graph 時,將所有計算載入至內存中。如果你試著在 GPU 上執行推斷時就會明顯發現這一現象,你會看到 GPU 內存隨著 TensorFlow 在 GPU 上載入和卸載模型參數而升降。據我所知,該構造在其他 ML 框架如 Caffe 或 PyTorch 中不存在。

解決方案是去掉 with 語句,向 run_graph 添加 sess 變數:

def run_graph(image_data, labels, input_layer_name, output_layer_name,n num_top_predictions, sess):n# Feed the image_data as input to the graph.n# predictions will contain a two-dimensional array, where onen# dimension represents the input image count, and the other hasn# predictions per classn softmax_tensor = sess.graph.get_tensor_by_name(output_layer_name)n predictions, = sess.run(softmax_tensor, {input_layer_name: image_data})n# Sort to show labels in order of confidencen top_k = predictions.argsort()[-num_top_predictions:][::-1]nfor node_id in top_k:n human_string = labels[node_id]n score = predictions[node_id]nprint(%s (score = %.5f) % (human_string, score))nreturn [ (labels[node_id], predictions[node_id].item()) for node_id in top_k ] # numpy floats are not json serializable, have to run itemnn...nnwith tf.Session() as sess:nfor line in sys.stdin:n run_graph(load_image(line), labels, FLAGS.input_layer, FLAGS.output_layer,n FLAGS.num_top_predictions, sess)n

代碼地址:github.com/hiveml/simpl

運行後,你會發現每張圖像花費時間約為 0.1 秒,這樣的速度快到可以在線使用了。

將單點推斷轉換成在線推斷(其他 ML 框架)

  • Caffe 使用其 net.forward 代碼,詳見:nbviewer.jupyter.org/gi
  • Mxnet 也很獨特:它實際上已開源可用的推斷伺服器代碼:github.com/awslabs/mxne

部署

計劃是將代碼封裝進 Flask app。Flask 是一個輕量級 Python 網頁框架,允許用極少的工作運行 http api 伺服器。

作為快速推斷,下列 Flask app 接受 multipart/form-data 的 POST 請求:

#!/usr/bin/env pythonn# usage: python echo.py to launch the server ; and then in another session, don# curl -v -XPOST 127.0.0.1:12480 -F "data=@./image.jpg"nfrom flask import Flask, requestnapp = Flask(__name__)n@app.route(/, methods=[POST])ndef classify():ntry:n data = request.files.get(data).read()nprint repr(data)[:1000]nreturn data, 200nexcept Exception as e:nreturn repr(e), 500napp.run(host=127.0.0.1,port=12480)n

下面是對應的 Flask app,可連接上文提到的 run_graph:

And here is the corresponding flask app hooked up to run_graph above:nn#!/usr/bin/env pythonn# usage: bash tf_classify_server.shnfrom flask import Flask, requestnimport tensorflow as tfnimport label_image as tf_classifynimport jsonnapp = Flask(__name__)nFLAGS, unparsed = tf_classify.parser.parse_known_args()nlabels = tf_classify.load_labels(FLAGS.labels)ntf_classify.load_graph(FLAGS.graph)nsess = tf.Session()n@app.route(/, methods=[POST])ndef classify():ntry:n data = request.files.get(data).read()n result = tf_classify.run_graph(data, labels, FLAGS.input_layer, FLAGS.output_layer, FLAGS.num_top_predictions, sess)nreturn json.dumps(result), 200nexcept Exception as e:nreturn repr(e), 500napp.run(host=127.0.0.1,port=12480)n

看起來還不錯,除了 Flask 和 TensorFlow 完全同步以外:執行圖像分類時,Flask 按照接收請求的順序一次處理一個請求,而 TensorFlow 完全佔用線程。

如上所述,速度的瓶頸可能仍然在於實際計算量,因此升級 Flask 封裝器代碼沒有太大意義。或許該代碼足以處理載入。有兩個明顯的方式可以擴大請求吞吐量:通過增加工作線程的數量來水平擴大請求吞吐量(下一節將講述),或利用 GPU 和批邏輯(batching logic)垂直擴大請求吞吐量。後者的實現要求網頁伺服器一次處理多個掛起請求,並決定是否等待較大批次還是將其發送至 TensorFlow 圖線程進行分類,對此 Flask app 完全不適合。兩種方式使用 Twisted + Klein 用 Python 寫代碼;如果你偏好第一類事件循環支持,並希望能夠連接到非 Python ML 框架如 Torch,則需要使用 Node.js + ZeroMQ。

擴展:負載平衡和服務發現

現在我們已經有一個模型可用的伺服器,但是它可能太慢,或我們的負載太高。我們想運行更多此類伺服器,那麼我們應該怎樣在多個伺服器上對其進行分布呢?普通方法是添加一個代理層,可以是 haproxy 或 nginx,可以平衡後端伺服器之間的負載,同時向用戶呈現一個統一的界面。下面是運行初級 Node.js 負載平衡器 http proxy 的示例代碼:

// Usage : node basic_proxy.js WORKER_PORT_0,WORKER_PORT_1,...nconst worker_ports = process.argv[2].split(,)nif (worker_ports.length === 0) { console.err(missing worker ports) ; process.exit(1) }nnconst proxy = require(http-proxy).createProxyServer({})nproxy.on(error, () => console.log(proxy error))nnlet i = 0nrequire(http).createServer((req, res) => {n proxy.web(req,res, {target: http://localhost: + worker_ports[ (i++) % worker_ports.length ]})n}).listen(12480)nconsole.log(`Proxying localhost:${12480} to [${worker_ports.toString()}]`)nn// spin up the ML workersnconst { exec } = require(child_process)nworker_ports.map(port => exec(`/bin/bash ./tf_classify_server.sh ${port}`))n

為了自動檢測後端伺服器的數量和地址,人們通常使用一個「服務發現」工具,它可能和負載平衡器捆綁在一起,也可能分開。一些有名的工具,如 Consul 和 Zookeeper。設置並學習如何使用此類工具超出了本文範疇,因此,我使用 node.js 服務發現包 seaport 推斷了一個非常初級的代理。代理代碼:

// Usage : node seaport_proxy.jsnconst seaportServer = require(seaport).createServer()nseaportServer.listen(12481)nconst proxy = require(http-proxy).createProxyServer({})nproxy.on(error, () => console.log(proxy error))nnlet i = 0nrequire(http).createServer((req, res) => {n seaportServer.get(tf_classify_server, worker_ports => {n const this_port = worker_ports[ (i++) % worker_ports.length ].portn proxy.web(req,res, {target: http://localhost: + this_port })n})n}).listen(12480)nconsole.log(`Seaport proxy listening on ${12480} to ${tf_classify_server} servers registered to ${12481}`)n

工作線程代碼:

// Usage : node tf_classify_server.jsnconst port = require(seaport).connect(12481).register(tf_classify_server)nconsole.log(`Launching tf classify worker on ${port}`)nrequire(child_process).exec(`/bin/bash ./tf_classify_server.sh ${port}`)n

但是,在應用到機器學習時,這個配置會遇到帶寬問題。

系統如果每秒鐘處理數十、數百張圖片,它就會卡在系統帶寬上。在目前的裝配上,所有的數據需要通過我們的單個 seaport 主機,也是面向客戶端的單個端點。

為了解決這個問題,我們需要客戶不點擊單個端點:127.0.0.1:12480,而是在後端伺服器間自動旋轉來點擊。如果你懂網路架構,這聽起來更像是 DNS 的活。

但是,配置定製的 DNS 伺服器不在本文的討論範圍。把客戶端代碼改編遵循成 2 階「手動 DNS」協議就行,我們能重複使用基本的 seaport proxy 來實現「端對端的」協議,其中客戶能直接連接到伺服器:

代理代碼:

// Usage : node p2p_proxy.jsnconst seaportServer = require(seaport).createServer()nseaportServer.listen(12481)nnlet i = 0nrequire(http).createServer((req, res) => {n seaportServer.get(tf_classify_server, worker_ports => {n const this_port = worker_ports[ (i++) % worker_ports.length ].portn res.end(`${this_port}n`)n})n}).listen(12480)nconsole.log(`P2P seaport proxy listening on ${12480} to tf_classify_server servers registered to ${12481}`)n

(worker code 和上面一樣)

客戶端代碼:

curl -v -XPOST localhost:`curl localhost:12480` -F"data=@$HOME/flower_photos/daisy/21652746_cc379e0eea_m.jpg"n

結論與拓展閱讀

這個時候,你應該上手做點什麼,但這肯定也不是不會過時的技術。在此文章中,還有很多重要的主題沒被覆蓋到:

  • 在新硬體上的自動開發與裝配
  • 在自己的硬體上,值得關注的工具包括 Openstack/VMware,還有安裝 Docker、管理網路路徑的 Chef/Puppet,安裝 TensorFlow、Python 等等的 Docker。
  • 在雲端,Kubernetes 或者 Marathon/Mesos 都非常棒
  • 模型版本管理
  • 一開始手動管理模型不是很難
  • TensorFlow Serving 是處理這個問題的不錯工具,還有批處理和整體部署,非常徹底。缺點是有點難以配置,也難以編寫客戶端代碼,此外還不支持 Caffe/PyTorch。
  • 如何從 Matlab 遷移機器學習代碼?
  • 在開發產品中不要用 Matlab(譯者註:僅代表作者觀點)。
  • GPU 驅動、Cuda、CUDNN
  • 使用英偉達容器並嘗試尋找一些在線 Dorckerfiles
  • 後處理層。一旦你在開發產品過程中找到一些不同的機器學習模型,你可能想要混合這些模型,並為不同的使用案例匹配不同的模型——也就是模型 B 沒結果跑模型 A,在 Caffe 上跑模型 C,並把結果傳送到 TensorFlow 上跑的模型 D,等等。

原文鏈接:thehive.ai/blog/simple-

  • 原 文:Step-by-step Guide to Deploying Deep Learning Models
  • 譯 文:機器之心
  • 作 者:李亞洲 譯

更多文章:SDK.CN - 中國領先的開發者服務平台

推薦閱讀:

CS 294: Deep Reinforcement Learning:IRL
O'Reilly人工智慧大會將於2018年4月10-13日來到北京:講師徵集開始
Logistic回歸的CPU並行化及其 Batch Gradient Ascent 的實現
機器學習分類判別方法(二):Support Vector Machines前言—拉格朗日乘子與KKT條件
Metropolis Hasting演算法如何推導出Gibbs Sampling?

TAG:TensorFlow | 机器学习 |