- 原文地址:Serverless Machine Learning With TensorFlow.js
- 原文做者:jamesthom
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:wzasd
- 校對者:haiyang-tju
在之前的博客中,我講解了如何使用 TensorFlow.js 在 Node.js 上來運行在本地圖像中進行的視覺識別。TensorFlow.js 是來自 Google 的開源機器學習庫中的 JavaScript 版本。html
當我將本地的 Node.js 腳本跑通,個人下一個想法就是將其轉換成爲無服務功能。我將會在 IBM Cloud Functions(Apache OpenWhisk)運行此功能並將腳本轉換成本身的用於視覺識別的微服務。前端
看起來很簡單,對吧?它只是一個 JavaScript 庫?所以,解壓它而後咱們進入正題… 啊哈 👊;node
將圖像分類腳本轉換並運行在無服務架構環境中具備如下挑戰:android
其中有一些問題會比其它問題更具備挑戰性!讓咱們在解釋如何使用 Apache OpenWhisk 中的 Docker support 來解決每一個問題以前,咱們先看一下每一個問題的細節部分。ios
TensorFlow.js 庫不包括在 Apache OpenWhisk 提供的 Node.js 運行時的庫git
外部庫能夠經過從zip文件中部署應用程序的方式導入到運行時時。zip 文件中包含自定義文件夾 node_modules
被提取到運行時中。Zip 文件的大小最大限制爲 48 MB。github
使用 TensorFlow.js 庫須要運行命令 npm install
這裏會出現第一個問題……即生成的 node_modules
文件夾大小爲 175MB。😱docker
查看該文件夾的內容,tfjs-node
模塊編譯一個 135M 的本地共享庫(libtensorflow.so
)。這意味着,在這個神奇的 48 MB 限制規則下,沒有多少 JavaScript 能夠縮小到限制要求以得到這些外部依賴。👎數據庫
本地共享庫 libtensorflow.so
必須使用平臺運行時來進行編譯。在本地運行 npm install
會自動編譯針對主機平臺的機器依賴項。本地環境可能使用不一樣的 CPU 體系結構(Mac 與 Linux)或連接到無服務運行時中不可用的共享庫。apache
TensorFlow 模型文件須要在 Node.js 中從文件系統進行加載。無服務運行時確實在運行時環境中提供臨時文件系統。zip 部署文件中的相關文件在調用前會自動解壓縮到此環境中。在無服務功能的生命週期以外,沒有對該文件系統的外部訪問。
MobileNet 模型文件有 16MB。若是這些文件包含在部署包中,則其他的應用程序源代碼將會留下 32MB 的大小。雖然模型文件足夠小,能夠包含在 zip 文件中,可是 TensorFlow.js 庫呢?這是這篇文章的結尾嗎?沒那麼快…。
Apache OpenWhisk 對自定義運行時的支持爲全部這些問題提供了簡單的解決方案!
Apache OpenWhisk 使用 Docker 容器做爲無服務功能(操做)的運行時環境。全部的平臺運行時的鏡像都在 Docker Hub 發佈,容許開發人員在本地啓動這些環境。
開發人員也能夠在建立操做的時候自定義運行映像。這些鏡像必須在 Docker Hub 上公開。自定義運行時必須公開平臺用於調用相同的 HTTP API。
將平臺運行時的映像用做父映像可使構建自定義運行時變得簡單。用戶能夠在 Docker 構建期間運行命令以安裝其餘庫和其餘依賴項。父映像已包含具備 Http API 服務處理平臺請求的源文件。
如下是 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
目錄中。Dockerfile
。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
複製代碼
ibmcloud fn action invoke classify -r -p image $(base64 panda.jpg)
複製代碼
{
"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 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。