[譯] 基於 TensorFlow.js 的無服務架構機器學習

之前的博客中,我講解了如何使用 TensorFlow.js 在 Node.js 上來運行在本地圖像中進行的視覺識別。TensorFlow.js 是來自 Google 的開源機器學習庫中的 JavaScript 版本。html

當我將本地的 Node.js 腳本跑通,個人下一個想法就是將其轉換成爲無服務功能。我將會在 IBM Cloud FunctionsApache OpenWhisk)運行此功能並將腳本轉換成本身的用於視覺識別的微服務。前端

使用 TensorFlow.js 實現的無服務功能

看起來很簡單,對吧?它只是一個 JavaScript 庫?所以,解壓它而後咱們進入正題… 啊哈 👊;node

將圖像分類腳本轉換並運行在無服務架構環境中具備如下挑戰:android

  • TensorFlow.js 庫須要在運行時加載。
  • 必須根據平臺體系結構對庫文件的本地綁定進行編譯。
  • 須要從文件系統來加載模型文件。

其中有一些問題會比其它問題更具備挑戰性!讓咱們在解釋如何使用 Apache OpenWhisk 中的 Docker support 來解決每一個問題以前,咱們先看一下每一個問題的細節部分。ios

挑戰

TensorFlow.js 庫

TensorFlow.js 庫不包括在 Apache OpenWhisk 提供的 Node.js 運行時的庫git

外部庫能夠經過從zip文件中部署應用程序的方式導入到運行時時。zip 文件中包含自定義文件夾 node_modules 被提取到運行時中。Zip 文件的大小最大限制爲 48 MBgithub

庫大小

使用 TensorFlow.js 庫須要運行命令 npm install 這裏會出現第一個問題……即生成的 node_modules 文件夾大小爲 175MB。😱docker

查看該文件夾的內容,tfjs-node 模塊編譯一個 135M 的本地共享庫libtensorflow.so)。這意味着,在這個神奇的 48 MB 限制規則下,沒有多少 JavaScript 能夠縮小到限制要求以得到這些外部依賴。👎數據庫

本地依賴

本地共享庫 libtensorflow.so 必須使用平臺運行時來進行編譯。在本地運行 npm install 會自動編譯針對主機平臺的機器依賴項。本地環境可能使用不一樣的 CPU 體系結構(Mac 與 Linux)或連接到無服務運行時中不可用的共享庫。apache

MobileNet 模型文件

TensorFlow 模型文件須要在 Node.js 中從文件系統進行加載。無服務運行時確實在運行時環境中提供臨時文件系統。zip 部署文件中的相關文件在調用前會自動解壓縮到此環境中。在無服務功能的生命週期以外,沒有對該文件系統的外部訪問。

MobileNet 模型文件有 16MB。若是這些文件包含在部署包中,則其他的應用程序源代碼將會留下 32MB 的大小。雖然模型文件足夠小,能夠包含在 zip 文件中,可是 TensorFlow.js 庫呢?這是這篇文章的結尾嗎?沒那麼快…。

Apache OpenWhisk 對自定義運行時的支持爲全部這些問題提供了簡單的解決方案!

自定義運行時

Apache OpenWhisk 使用 Docker 容器做爲無服務功能(操做)的運行時環境。全部的平臺運行時的鏡像都在 Docker Hub 發佈,容許開發人員在本地啓動這些環境。

開發人員也能夠在建立操做的時候自定義運行映像。這些鏡像必須在 Docker Hub 上公開。自定義運行時必須公開平臺用於調用相同的 HTTP API

將平臺運行時的映像用做父映像可使構建自定義運行時變得簡單。用戶能夠在 Docker 構建期間運行命令以安裝其餘庫和其餘依賴項。父映像已包含具備 Http API 服務處理平臺請求的源文件。

TensorFlow.js 運行時

如下是 Node.js 操做運行時的 Docker 構建文件,其中包括其它 TensorFlow.js 依賴項。

FROM openwhisk/action-nodejs-v8:latest

RUN npm install @tensorflow/tfjs @tensorflow-models/mobilenet @tensorflow/tfjs-node jpeg-js

COPY mobilenet mobilenet
複製代碼

openwhisk/action-nodejs-v8:latest 是 OpenWhisk 發佈的安裝了 Node.js 運行時的映像。

在構建過程當中使用 npm install 安裝 TensorFlow 庫和其餘依賴項。在構建過程當中安裝庫 @tensorflow/tfjs-node 的本地依賴項,能夠自動對應平臺進行編譯。

因爲我正在構建一個新的運行時,我還將 MobileNet 模型文件添加到鏡像中。雖然不是絕對必要,但從運行 zip 文件中刪除它們能夠減小部署時間。

想跳過下一步嗎?使用這個鏡像 jamesthomas/action-nodejs-v8:tfjs 而不是本身來建立的。

構建運行時

以前的博客中,我展現瞭如何從公共庫下載模型文件。

  • 下載 MobileNet 模型的一個版本並將全部文件放在 mobilenet 目錄中。
  • 複製 Docker 構建文件到本地,並將其命名爲 Dockerfile
  • 運行 Docker build command 生成本地映像。
docker build -t tfjs .
複製代碼
docker tag tfjs <USERNAME>/action-nodejs-v8:tfjs
複製代碼

用你本身的 Docker Hub 用戶名替換 <USERNAME>

docker push <USERNAME>/action-nodejs-v8:tfjs
複製代碼

一旦 Docker Hub 上的鏡像可用,就可使用該運行時映像建立操做。😎

示例代碼

此代碼將圖像分類實現爲 OpenWhisk 操做。使用事件參數上的 image 屬性將圖像文件做爲 Base64 編碼的字符串提供。分類結果做爲響應中的 results 屬性返回。

const tf = require('@tensorflow/tfjs')
const mobilenet = require('@tensorflow-models/mobilenet');
require('@tensorflow/tfjs-node')

const jpeg = require('jpeg-js');

const NUMBER_OF_CHANNELS = 3
const MODEL_PATH = 'mobilenet/model.json'

let mn_model

const memoryUsage = () => {
  let used = process.memoryUsage();
  const values = []
  for (let key in used) {
    values.push(`${key}=${Math.round(used[key] / 1024 / 1024 * 100) / 100} MB`);
  }

  return `memory used: ${values.join(', ')}`
}

const logTimeAndMemory = label => {
  console.timeEnd(label)
  console.log(memoryUsage())
}

const decodeImage = source => {
  console.time('decodeImage');
  const buf = Buffer.from(source, 'base64')
  const pixels = jpeg.decode(buf, true);
  logTimeAndMemory('decodeImage')
  return pixels
}

const imageByteArray = (image, numChannels) => {
  console.time('imageByteArray');
  const pixels = image.data
  const numPixels = image.width * image.height;
  const values = new Int32Array(numPixels * numChannels);

  for (let i = 0; i < numPixels; i++) {
    for (let channel = 0; channel < numChannels; ++channel) {
      values[i * numChannels + channel] = pixels[i * 4 + channel];
    }
  }

  logTimeAndMemory('imageByteArray')
  return values
}

const imageToInput = (image, numChannels) => {
  console.time('imageToInput');
  const values = imageByteArray(image, numChannels)
  const outShape = [image.height, image.width, numChannels];
  const input = tf.tensor3d(values, outShape, 'int32');

  logTimeAndMemory('imageToInput')
  return input
}

const loadModel = async path => {
  console.time('loadModel');
  const mn = new mobilenet.MobileNet(1, 1);
  mn.path = `file://${path}`
  await mn.load()
  logTimeAndMemory('loadModel')
  return mn
}

async function main (params) {
  console.time('main');
  console.log('prediction function called.')
  console.log(memoryUsage())

  console.log('loading image and model...')

  const image = decodeImage(params.image)
  const input = imageToInput(image, NUMBER_OF_CHANNELS)

  if (!mn_model) {
    mn_model = await loadModel(MODEL_PATH)
  }

  console.time('mn_model.classify');
  const predictions = await mn_model.classify(input);
  logTimeAndMemory('mn_model.classify')

  console.log('classification results:', predictions);

  // free memory from TF-internal libraries from input image
  input.dispose()
  logTimeAndMemory('main')

  return { results: predictions }
}
複製代碼

緩存加載的模型

無服務的平臺按需初始化運行環境用以處理調用。一旦運行環境被建立,他將會對從新調用有一些限制。

應用程序能夠經過使用全局變量來維護跨請求的狀態來利用此方式。這一般用於已打開的數據庫緩存方式或存儲從外部系統加載的初始化數據。

我使用這種模式來緩存 MobileNet 模型用於分類任務。在冷調用期間,模型從文件系統加載並存儲在全局變量中。而後,熱調用就會利用這個已存在的全局變量來處理進一步的請求,從而跳過模型的再次加載過程。

緩存模型能夠減小熱調用分類的時間(從而下降成本)。

內存泄漏

能夠經過最簡化的修改從 IBM Cloud Functions 上的博客文章來運行 Node.js 腳本。不幸的是,性能測試顯示處理函數中存在內存泄漏。😢

在 Node.js 上閱讀更多關於 TensorFlow.js 如何工做的信息,揭示了這個問題...

TensorFlow.js 的 Node.js 擴展使用本地 C++ 庫在 CPU 或 GPU 引擎上計算 Tensors。爲應用程序顯式釋放它或進程退出以前,將保留爲本機庫中的 Tensor 對象分配的內存。TensorFlow.js 在各個對象上提供 dispose 方法以釋放分配的內存。 還有一個 tf.tidy 方法能夠自動清理幀內全部已分配的對象。

檢查代碼,每一個請求都會從圖像建立圖像張量做爲模型的輸入。在從請求處理程序返回以前,這些生成的張量對象並未被銷燬。這意味着本地內存會無限增加。在返回以前添加顯式的 dispose 調用以釋放這些對象能夠修復該問題

分析和性能

執行代碼記錄了分類處理過程當中不一樣階段的內存使用和時間消耗。

記錄內存使用狀況能夠容許我修改分配給該功能的最大內存,以得到最佳性能和成本。Node.js 提供標準庫 API 來檢索當前進程的內存使用狀況。記錄這些值容許我檢查不一樣階段的內存使用狀況。

分類過程當中的不一樣任務的耗時,也就是模型加載,圖像分類等不一樣任務,這可讓我深刻了解到與其它方法相比這裏的分類方法的效率。Node.js 有一個標準庫 API,可使用計時器將時間消耗進行記錄和打印到控制檯。

例子

部署代碼

ibmcloud fn action create classify --docker <IMAGE_NAME> index.js
複製代碼

使用自定義運行時的公共 Docker Hub 映像標識符替換 <IMAGE_NAME>。若是你並無構建它,請使用 jamesthomas/action-nodejs-v8:tfjs

測試

wget http://bit.ly/2JYSal9 -O panda.jpg
複製代碼
  • 使用 Base64 編碼圖像做爲調用方法的輸入參數。
ibmcloud fn action invoke classify -r -p image $(base64 panda.jpg)
複製代碼
  • 返回的 JSON 消息包含分類機率。🐼🐼🐼
{
  "results":  [{
    className: 'giant panda, panda, panda bear, coon bear',
    probability: 0.9993536472320557
  }]
}
複製代碼

激活的細節

  • 檢索上次激活的日誌記錄輸出以顯示性能數據。
ibmcloud fn activation logs --last
複製代碼

分析和內存使用詳細信息記錄到 stdout

prediction function called.
memory used: rss=150.46 MB, heapTotal=32.83 MB, heapUsed=20.29 MB, external=67.6 MB
loading image and model...
decodeImage: 74.233ms
memory used: rss=141.8 MB, heapTotal=24.33 MB, heapUsed=19.05 MB, external=40.63 MB
imageByteArray: 5.676ms
memory used: rss=141.8 MB, heapTotal=24.33 MB, heapUsed=19.05 MB, external=45.51 MB
imageToInput: 5.952ms
memory used: rss=141.8 MB, heapTotal=24.33 MB, heapUsed=19.06 MB, external=45.51 MB
mn_model.classify: 274.805ms
memory used: rss=149.83 MB, heapTotal=24.33 MB, heapUsed=20.57 MB, external=45.51 MB
classification results: [...]
main: 356.639ms
memory used: rss=144.37 MB, heapTotal=24.33 MB, heapUsed=20.58 MB, external=45.51 MB

複製代碼

main 是處理程序的總耗時。mn_model.classify 是圖像分類的耗時。冷啓動請求打印了一條帶有模型加載時間的額外日誌消息,loadModel:394.547ms

性能結果

對冷激活和熱激活(使用 256 MB 內存)調用 classify 動做 1000 次會產生如下結果。

熱激活

熱激活的表現結果

在熱啓動環境中,分類處理的平均耗時爲 316 毫秒。查看耗時數據,將 Base64 編碼的 JPEG 轉換爲輸入張量大約須要 100 毫秒。運行模型進行分類任務的耗時爲 200-250 毫秒。

冷激活

冷激活的表現結果

使用冷環境時,分類處理大的平均耗時 1260 毫秒。這些請求會因初始化新的運行時容器和從文件系統加載模型而受到限制。這兩項任務都須要大約 400 毫秒的時間。

在 Apache OpenWhisk 中使用自定義運行時映像的一個缺點是缺乏預熱容器。預熱是指在該容器在須要使用以前啓動運行時容器,以減小冷啓動的時間消耗。

分類成本

IBM Cloud Functions 提供了一個每個月 400,000 GB/s 流量的免費等級。每秒時間內的調用額外收費爲 $0.000017 每 GB 的內存佔用。執行時間四捨五入到最接近的 100 毫秒。

若是全部激活都是熱激活的,那麼用戶能夠在免費等級內使用 256MB 存儲佔用和每個月執行超過 4,000,000 個分類。一旦超出免費等級範圍,大約 600,000 次額外調用的花費才 $1 多一點。

若是全部激活都是冷激活的,那麼用戶能夠在免費等級內使用 256MB 存儲佔用和每個月執行超過 1,2000,000 個分類。一旦超出免費等級範圍,大約 180,000 次額外調用的花費爲 $1。

結論

TensorFlow.js 爲 JavaScript 開發人員帶來了深度學習的力量。使用預先訓練的模型和 TensorFlow.js 庫,能夠輕鬆地以最少的工做量和代碼擴展具備複雜機器學習任務的 JavaScript 應用程序。

獲取本地腳原本運行圖像分類相對簡單,但轉換爲無服務器功能帶來了更多挑戰!Apache OpenWhisk 將最大應用程序大小限制爲 50MB,本機庫依賴項遠大於此限制。

幸運的是,Apache OpenWhisk 的自定義運行時支持使咱們可以解決全部這些問題。經過使用本機依賴項和模型文件構建自定義運行時,能夠在平臺上使用這些庫,而無需將它們包含在部署包中。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索