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

LeviDing發表於2019-02-27

以前的部落格中,我講解了如何使用 TensorFlow.js 在 Node.js 上來執行在本地影像中進行的視覺識別。TensorFlow.js 是來自 Google 的開源機器學習庫中的 JavaScript 版本。

當我將本地的 Node.js 指令碼跑通,我的下一個想法就是將其轉換成為無服務功能。我將會在 IBM Cloud FunctionsApache OpenWhisk)執行此功能並將指令碼轉換成自己的用於視覺識別的微服務。

[譯] 基於 TensorFlow.js 的無服務架構機器學習
使用 TensorFlow.js 實現的無服務功能

看起來很簡單,對吧?它只是一個 JavaScript 庫?因此,解壓它然後我們進入正題… 啊哈 ?;

將影像分類指令碼轉換並執行在無服務架構環境中具有以下挑戰:

  • TensorFlow.js 庫需要在執行時載入。
  • 必須根據平臺體系結構對庫檔案的本地繫結進行編譯。
  • 需要從檔案系統來載入模型檔案。

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

挑戰

TensorFlow.js 庫

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

外部庫可以通過從zip檔案中部署應用程式的方式匯入到執行時時。zip 檔案中包含自定義資料夾 node_modules 被提取到執行時中。Zip 檔案的大小最大限制為 48 MB

庫大小

使用 TensorFlow.js 庫需要執行命令 npm install 這裡會出現第一個問題……即生成的 node_modules 資料夾大小為 175MB。?

檢視該資料夾的內容,tfjs-node 模組編譯一個 135M 的本地共享庫libtensorflow.so)。這意味著,在這個神奇的 48 MB 限制規則下,沒有多少 JavaScript 可以縮小到限制要求以獲得這些外部依賴。?

本地依賴

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

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

測試

[譯] 基於 TensorFlow.js 的無服務架構機器學習
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 次會產生以下結果。

熱啟用

[譯] 基於 TensorFlow.js 的無服務架構機器學習
熱啟用的表現結果

在熱啟動環境中,分類處理的平均耗時為 316 毫秒。檢視耗時資料,將 Base64 編碼的 JPEG 轉換為輸入張量大約需要 100 毫秒。執行模型進行分類任務的耗時為 200-250 毫秒。

冷啟用

[譯] 基於 TensorFlow.js 的無服務架構機器學習
冷啟用的表現結果

使用冷環境時,分類處理大的平均耗時 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前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章