前文:三分鍾快速上手TensorFlow 2.0 (中)——常用模塊和模型的部署
TensorFlow 模型導出
使用 SavedModel 完整導出模型
不僅包含參數的權值,還包含計算的流程(即計算圖)
tf.saved_model.save(model, "保存的目標文件夾名稱")
model = tf.saved_model.load("保存的目標文件夾名稱")
tf.keras.Model 類建立的 Keras 模型,其需要導出到 SavedModel 格式的方法(比如 call )都需要使用 @tf.function 修飾
tf.keras.Model 類建立的 Keras 模型 model ,使用 SavedModel 載入后將無法使用 model() 直接進行推斷,而需要使用 model.call()
import tensorflow as tf from zh.model.utils import MNISTLoader num_epochs = 1 batch_size = 50 learning_rate = 0.001 model = tf.keras.models.Sequential([ tf.keras.layers.Flatten(), tf.keras.layers.Dense(100, activation=tf.nn.relu), tf.keras.layers.Dense(10), tf.keras.layers.Softmax() ]) data_loader = MNISTLoader() model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss=tf.keras.losses.sparse_categorical_crossentropy, metrics=[tf.keras.metrics.sparse_categorical_accuracy] ) model.fit(data_loader.train_data, data_loader.train_label, epochs=num_epochs, batch_size=batch_size) tf.saved_model.save(model, "saved/1")
import tensorflow as tf from zh.model.utils import MNISTLoader batch_size = 50 model = tf.saved_model.load("saved/1") data_loader = MNISTLoader() sparse_categorical_accuracy = tf.keras.metrics.SparseCategoricalAccuracy() num_batches = int(data_loader.num_test_data // batch_size) for batch_index in range(num_batches): start_index, end_index = batch_index * batch_size, (batch_index + 1) * batch_size y_pred = model(data_loader.test_data[start_index: end_index]) sparse_categorical_accuracy.update_state(y_true=data_loader.test_label[start_index: end_index], y_pred=y_pred) print("test accuracy: %f" % sparse_categorical_accuracy.result())
class MLP(tf.keras.Model): def __init__(self): super().__init__() self.flatten = tf.keras.layers.Flatten() self.dense1 = tf.keras.layers.Dense(units=100, activation=tf.nn.relu) self.dense2 = tf.keras.layers.Dense(units=10) @tf.function def call(self, inputs): # [batch_size, 28, 28, 1] x = self.flatten(inputs) # [batch_size, 784] x = self.dense1(x) # [batch_size, 100] x = self.dense2(x) # [batch_size, 10] output = tf.nn.softmax(x) return output model = MLP()
y_pred = model.call(data_loader.test_data[start_index: end_index])
Keras Sequential save 方法
是基於 keras 的 Sequential 構建了多層的卷積神經網絡,並進行訓練
curl -LO https://raw.githubusercontent.com/keras-team/keras/master/examples/mnist_cnn.py
model.save('mnist_cnn.h5')
python mnist_cnn.py
執行過程會比較久,執行結束后,會在當前目錄產生 mnist_cnn.h5 文件(HDF5 格式),就是 keras 訓練后的模型,其中已經包含了訓練后的模型結構和權重等信息。
在服務器端,可以直接通過 keras.models.load_model("mnist_cnn.h5") 加載,然后進行推理;在移動設備需要將 HDF5 模型文件轉換為 TensorFlow Lite 的格式,然后通過相應平台的 Interpreter 加載,然后進行推理。
TensorFlow Serving(服務器端部署模型)
安裝
# 添加Google的TensorFlow Serving源 echo "deb [arch=amd64] http://storage.googleapis.com/tensorflow-serving-apt stable tensorflow-model-server tensorflow-model-server-universal" | sudo tee /etc/apt/sources.list.d/tensorflow-serving.list # 添加gpg key curl https://storage.googleapis.com/tensorflow-serving-apt/tensorflow-serving.release.pub.gpg | sudo apt-key add -
sudo apt-get update
sudo apt-get install tensorflow-model-server
curl 設置代理的方式為 -x 選項或設置 http_proxy 環境變量,即 export http_proxy=http://代理服務器IP:端口 或 curl -x http://代理服務器IP:端口 URL apt-get 設置代理的方式為 -o 選項,即 sudo apt-get -o Acquire::http::proxy="http://代理服務器IP:端口" ...
模型部署
tensorflow_model_server \ --rest_api_port=端口號(如8501) \ --model_name=模型名 \ --model_base_path="SavedModel格式模型的文件夾絕對地址(不含版本號)"
支持熱更新模型,其典型的模型文件夾結構如下:
/saved_model_files /1 # 版本號為1的模型文件 /assets /variables saved_model.pb ... /N # 版本號為N的模型文件 /assets /variables saved_model.pb
上面 1~N 的子文件夾代表不同版本號的模型。當指定 --model_base_path 時,只需要指定根目錄的 絕對地址 (不是相對地址)即可。例如,如果上述文件夾結構存放在 home/snowkylin 文件夾內,則 --model_base_path 應當設置為 home/snowkylin/saved_model_files (不附帶模型版本號)。TensorFlow Serving 會自動選擇版本號最大的模型進行載入。
tensorflow_model_server \ --rest_api_port=8501 \ --model_name=MLP \ --model_base_path="/home/.../.../saved" # 文件夾絕對地址根據自身情況填寫,無需加入版本號
MLP 的模型名在 8501 端口進行部署,可以直接使用以上命令
class MLP(tf.keras.Model): ... @tf.function(input_signature=[tf.TensorSpec([None, 28, 28, 1], tf.float32)]) def call(self, inputs): ...
@tf.function 修飾,還要在修飾時指定 input_signature 參數,以顯式說明輸入的形狀。該參數傳入一個由 tf.TensorSpec 組成的列表,指定每個輸入張量的形狀和類型
[None, 28, 28, 1] 的四維張量( None表示第一維即 Batch Size 的大小不固定),此時我們可以將模型的 call 方法做出上面的修飾
model = MLP() ... tf.saved_model.save(model, "saved_with_signature/1", signatures={"call": model.call})
tf.saved_model.save 導出時,需要通過 signature 參數提供待導出的函數的簽名(Signature)
call 這一簽名來調用 model.call方法時,我們可以在導出時傳入 signature 參數,以 dict 的鍵值對形式告知導出的方法對應的簽名
tensorflow_model_server \ --rest_api_port=8501 \ --model_name=MLP \ --model_base_path="/home/.../.../saved_with_signature" # 修改為自己模型的絕對地址
在客戶端調用以 TensorFlow Serving 部署的模型
支持以 gRPC 和 RESTful API 調用以 TensorFlow Serving 部署的模型。這里主要介紹較為通用的 RESTful API 方法。
RESTful API 以標准的 HTTP POST 方法進行交互,請求和回復均為 JSON 對象。為了調用服務器端的模型,我們在客戶端向服務器發送以下格式的請求:
服務器 URI: http://服務器地址:端口號/v1/models/模型名:predict
請求內容:
{
"signature_name": "需要調用的函數簽名(Sequential模式不需要)", "instances": 輸入數據 }
回復為:
{
"predictions": 返回值 }
import json import numpy as np import requests from zh.model.utils import MNISTLoader data_loader = MNISTLoader() data = json.dumps({ "instances": data_loader.test_data[0:3].tolist() }) headers = {"content-type": "application/json"} json_response = requests.post( 'http://localhost:8501/v1/models/MLP:predict', data=data, headers=headers) predictions = np.array(json.loads(json_response.text)['predictions']) print(np.argmax(predictions, axis=-1)) print(data_loader.test_label[0:10])
import json import numpy as np import requests from zh.model.utils import MNISTLoader data_loader = MNISTLoader() data = json.dumps({ "signature_name": "call", "instances": data_loader.test_data[0:10].tolist() }) headers = {"content-type": "application/json"} json_response = requests.post( 'http://localhost:8501/v1/models/MLP:predict', data=data, headers=headers) predictions = np.array(json.loads(json_response.text)['predictions']) print(np.argmax(predictions, axis=-1)) print(data_loader.test_label[0:10])
const Jimp = require('jimp') const superagent = require('superagent') const url = 'http://localhost:8501/v1/models/MLP:predict' const getPixelGrey = (pic, x, y) => { const pointColor = pic.getPixelColor(x, y) const { r, g, b } = Jimp.intToRGBA(pointColor) const gray = +(r * 0.299 + g * 0.587 + b * 0.114).toFixed(0) return [ gray / 255 ] } const getPicGreyArray = async (fileName) => { const pic = await Jimp.read(fileName) const resizedPic = pic.resize(28, 28) const greyArray = [] for ( let i = 0; i< 28; i ++ ) { let line = [] for (let j = 0; j < 28; j ++) { line.push(getPixelGrey(resizedPic, j, i)) } console.log(line.map(_ => _ > 0.3 ? ' ' : '1').join(' ')) greyArray.push(line) } return greyArray } const evaluatePic = async (fileName) => { const arr = await getPicGreyArray(fileName) const result = await superagent.post(url) .send({ instances: [arr] }) result.body.predictions.map(res => { const sortedRes = res.map((_, i) => [_, i]) .sort((a, b) => b[0] - a[0]) console.log(`我們猜這個數字是${sortedRes[0][1]},概率是${sortedRes[0][0]}`) }) } evaluatePic('test_pic_tag_5.png')
npm install jimp 和 npm install superagent 安裝)
TensorFlow Lite(移動端部署模型)
目前 TFLite 只提供了推理功能,在服務器端進行訓練后,經過如下簡單處理即可部署到邊緣設備上。
模型轉換
模型轉換:由於邊緣設備計算等資源有限,使用 TensorFlow 訓練好的模型,模型太大、運行效率比較低,不能直接在移動端部署,需要通過相應工具進行轉換成適合邊緣設備的格式。
轉換方式有兩種:Float 格式和 Quantized 格式
針對 Float 格式的,先使用命令行工具 tflite_convert,在終端執行如下命令:
tflite_convert -h
usage: tflite_convert [-h] --output_file OUTPUT_FILE (--saved_model_dir SAVED_MODEL_DIR | --keras_model_file KERAS_MODEL_FILE) --output_file OUTPUT_FILE Full filepath of the output file. --saved_model_dir SAVED_MODEL_DIR Full path of the directory containing the SavedModel. --keras_model_file KERAS_MODEL_FILE Full filepath of HDF5 file containing tf.Keras model.
TF2.0 支持兩種模型導出方法和格式 SavedModel 和 Keras Sequential。
tflite_convert --saved_model_dir=saved/1 --output_file=mnist_savedmodel.tflite
tflite_convert --keras_model_file=mnist_cnn.h5 --output_file=mnist_sequential.tflite
Android 部署
邊緣設備部署:本節以 android 為例,簡單介紹如何在 android 應用中部署轉化后的模型,完成 Mnist 圖片的識別。
buildscript { repositories { maven { url 'https://maven.aliyun.com/nexus/content/repositories/google' } maven { url 'https://maven.aliyun.com/nexus/content/repositories/jcenter' } } dependencies { classpath 'com.android.tools.build:gradle:3.5.1' } } allprojects { repositories { maven { url 'https://maven.aliyun.com/nexus/content/repositories/google' } maven { url 'https://maven.aliyun.com/nexus/content/repositories/jcenter' } } }
build.gradle 中的 maven 源 google() 和 jcenter() 分別替換為國內鏡像
android { aaptOptions { noCompress "tflite" // 編譯apk時,不壓縮tflite文件 } } dependencies { implementation 'org.tensorflow:tensorflow-lite:1.14.0' }
app/build.gradle 添加信息
其中,
-
aaptOptions設置 tflite 文件不壓縮,確保后面 tflite 文件可以被 Interpreter 正確加載。 -
org.tensorflow:tensorflow-lite的最新版本號可以在這里查詢 https://bintray.com/google/tensorflow/tensorflow-lite
設置好后,sync 和 build 整個工程,如果 build 成功說明,配置成功。
添加 tflite 文件到 assets 文件夾
在 app 目錄先新建 assets 目錄,並將 mnist_savedmodel.tflite 文件保存到 assets 目錄。重新編譯 apk,檢查新編譯出來的 apk 的 assets 文件夾是否有 mnist_cnn.tflite 文件。
點擊菜單 Build->Build APK (s) 觸發 apk 編譯,apk 編譯成功點擊右下角的 EventLog。點擊最后一條信息中的 analyze 鏈接,會觸發 apk analyzer 查看新編譯出來的 apk,若在 assets 目錄下存在 mnist_savedmodel.tflite ,則編譯打包成功,如下:
assets
|__mnist_savedmodel.tflite
/** Memory-map the model file in Assets. */ private MappedByteBuffer loadModelFile(Activity activity) throws IOException { AssetFileDescriptor fileDescriptor = activity.getAssets().openFd(mModelPath); FileInputStream inputStream = new FileInputStream(fileDescriptor.getFileDescriptor()); FileChannel fileChannel = inputStream.getChannel(); long startOffset = fileDescriptor.getStartOffset(); long declaredLength = fileDescriptor.getDeclaredLength(); return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength); }
mnist_savedmodel.tflite 文件加載到 memory-map 中,作為 Interpreter 實例化的輸入
mTFLite = new Interpreter(loadModelFile(activity));
getAssets() 打開。
MappedByteBuffer 直接作為 Interpreter 的輸入, mTFLite ( Interpreter )就是轉換后模型的運行載體。
//Float模型相關參數 // com/dpthinker/mnistclassifier/model/FloatSavedModelConfig.java protected void setConfigs() { setModelName("mnist_savedmodel.tflite"); setNumBytesPerChannel(4); setDimBatchSize(1); setDimPixelSize(1); setDimImgWeight(28); setDimImgHeight(28); setImageMean(0); setImageSTD(255.0f); } // 初始化 // com/dpthinker/mnistclassifier/classifier/BaseClassifier.java private void initConfig(BaseModelConfig config) { this.mModelConfig = config; this.mNumBytesPerChannel = config.getNumBytesPerChannel(); this.mDimBatchSize = config.getDimBatchSize(); this.mDimPixelSize = config.getDimPixelSize(); this.mDimImgWidth = config.getDimImgWeight(); this.mDimImgHeight = config.getDimImgHeight(); this.mModelPath = config.getModelName(); }
使用 MNIST test 測試集中的圖片作為輸入,mnist 圖像大小 28*28,單像素
// 將輸入的Bitmap轉化為Interpreter可以識別的ByteBuffer // com/dpthinker/mnistclassifier/classifier/BaseClassifier.java protected ByteBuffer convertBitmapToByteBuffer(Bitmap bitmap) { int[] intValues = new int[mDimImgWidth * mDimImgHeight]; scaleBitmap(bitmap).getPixels(intValues, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight()); ByteBuffer imgData = ByteBuffer.allocateDirect( mNumBytesPerChannel * mDimBatchSize * mDimImgWidth * mDimImgHeight * mDimPixelSize); imgData.order(ByteOrder.nativeOrder()); imgData.rewind(); // Convert the image toFloating point. int pixel = 0; for (int i = 0; i < mDimImgWidth; ++i) { for (int j = 0; j < mDimImgHeight; ++j) { //final int val = intValues[pixel++]; int val = intValues[pixel++]; mModelConfig.addImgValue(imgData, val); //添加把Pixel數值轉化並添加到ByteBuffer } } return imgData; } // mModelConfig.addImgValue定義 // com/dpthinker/mnistclassifier/model/FloatSavedModelConfig.java public void addImgValue(ByteBuffer imgData, int val) { imgData.putFloat(((val & 0xFF) - getImageMean()) / getImageSTD()); }
convertBitmapToByteBuffer 的輸出即為模型運行的輸入。
privateFloat[][] mLabelProbArray = newFloat[1][10];
定義一個 1*10 的多維數組,因為我們只有 10 個 label
運行結束后,每個二級元素都是一個 label 的概率。
mTFLite.run(imgData, mLabelProbArray);
mLabelProbArray 的內容就是各個 label 識別的概率。對他們進行排序,找出 Top 的 label 並界面呈現給用戶.
View.OnClickListener() 觸發 "image/*" 類型的 Intent.ACTION_GET_CONTENT ,進而獲取設備上的圖片(只支持 MNIST 標准圖片)。然后,通過 RadioButtion 的選擇情況,確認加載哪種轉換后的模型,並觸發真正分類操作。
Quantization 模型轉換
在 TF1.0 上,可以使用命令行工具轉換 Quantized 模型。在筆者嘗試的情況看在 TF2.0 上,命令行工具目前只能轉換為 Float 模型,Python API 只能轉換為 Quantized 模型。
import tensorflow as tf converter = tf.lite.TFLiteConverter.from_saved_model('saved/1') converter.optimizations = [tf.lite.Optimize.DEFAULT] tflite_quant_model = converter.convert() open("mnist_savedmodel_quantized.tflite", "wb").write(tflite_quant_model)
mnist_savedmodel_quantized.tflite
在 TF2.0 上,提供了新的一步到位的工具 visualize.py ,直接轉換為 html 文件,除了模型結構,還有更清晰的關鍵信息總結。
visualize.py 目前看應該還是開發階段,使用前需要先從 github 下載最新的 TensorFlow 和 FlatBuffers 源碼,並且兩者要在同一目錄,因為 visualize.py 源碼中是按兩者在同一目錄寫的調用路徑。
git clone git@github.com:tensorflow/tensorflow.git
git clone git@github.com:google/flatbuffers.git
編譯 FlatBuffers:(筆者使用的 Mac,其他平台請大家自行配置,應該不麻煩)
-
下載 cmake:執行
brew install cmake -
設置編譯環境:在
FlatBuffers的根目錄,執行cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release -
編譯:在
FlatBuffers的根目錄,執行make
編譯完成后,會在跟目錄生成 flatc,這個可執行文件是 visualize.py 運行所依賴的。
python visualize.py mnist_savedmodel_quantized.tflite mnist_savedmodel_quantized.html
跟 Float 模型對比,Input/Output 格式是一致的,所以可以復用 Float 模型 Android 部署過程中的配置。
// Quantized模型相關參數 // com/dpthinker/mnistclassifier/model/QuantSavedModelConfig.java public class QuantSavedModelConfig extends BaseModelConfig { @Override protected void setConfigs() { setModelName("mnist_savedmodel_quantized.tflite"); setNumBytesPerChannel(4); setDimBatchSize(1); setDimPixelSize(1); setDimImgWeight(28); setDimImgHeight(28); setImageMean(0); setImageSTD(255.0f); } @Override public void addImgValue(ByteBuffer imgData, int val) { imgData.putFloat(((val & 0xFF) - getImageMean()) / getImageSTD()); } }
https://github.com/snowkylin/tensorflow-handbook/tree/master/source/android
TensorFlow.js
TensorFlow 的 JavaScript 版本,支持 GPU 硬件加速,可以運行在 Node.js 或瀏覽器環境中。它不但支持完全基於 JavaScript 從頭開發、訓練和部署模型,也可以用來運行已有的 Python 版 TensorFlow 模型,或者基於現有的模型進行繼續訓練。
基於 TensorFlow.js 1.0,向大家簡單地介紹如何基於 ES6 的 JavaScript 進行 TensorFlow.js 的開發
相關代碼,使用說明,和訓練好的模型文件及參數,都可以在作者的 GitHub 上找到。地址: https://github.com/huan/tensorflow-handbook-javascript
瀏覽器中進行機器學習,相對比與服務器端來講,將擁有以下四大優勢:
-
不需要安裝軟件或驅動(打開瀏覽器即可使用);
-
可以通過瀏覽器進行更加方便的人機交互;
-
可以通過手機瀏覽器,調用手機硬件的各種傳感器(如:GPS、電子羅盤、加速度傳感器、攝像頭等);
-
用戶的數據可以無需上傳到服務器,在本地即可完成所需操作。
Move Mirror 地址:https://experiments.withgoogle.com/move-mirror
Move Mirror 所使用的 PoseNet 地址:https://github.com/tensorflow/tfjs-models/tree/master/posenet
環境配置
在瀏覽器中使用 TensorFlow.js
<html>
<head>
<script src="http://unpkg.com/@tensorflow/tfjs/dist/tf.min.js"></script>
在 Node.js 中使用 TensorFlow.js
首先需要按照 NodeJS.org 官網的說明,完成安裝最新版本的 Node.js 。然后,完成以下四個步驟即可完成配置:
$ node --verion v10.5.0 $ npm --version 6.4.1
$ mkdir tfjs
$ cd tfjs
# 初始化項目管理文件 package.json $ npm init -y # 安裝 tfjs 庫,純 JavaScript 版本 $ npm install @tensorflow/tfjs # 安裝 tfjs-node 庫,C Binding 版本 $ npm install @tensorflow/tfjs-node # 安裝 tfjs-node-gpu 庫,支持 CUDA GPU 加速 $ npm install @tensorflow/tfjs-node-gpu
$ node > require('@tensorflow/tfjs').version { 'tfjs-core': '1.3.1', 'tfjs-data': '1.3.1', 'tfjs-layers': '1.3.1', 'tfjs-converter': '1.3.1', tfjs: '1.3.1' } >
tfjs-core, tfjs-data, tfjs-layers 和 tfjs-converter 的輸出信息,那么就說明環境配置沒有問題了。
import * as tf from '@tensorflow/tfjs' console.log(tf.version.tfjs) // Output: 1.3.1
import 是 JavaScript ES6 版本新開始擁有的新特性。粗略可以認為等價於 require。比如:import * as tf from '@tensorflow/tfjs' 和 const tf = require('@tensorflow/tfjs') 對上面的示例代碼是等價的。
在微信小程序中使用 TensorFlow.js
首先要在小程序管理后台的 “設置 - 第三方服務 - 插件管理” 中添加插件。開發者可登錄小程序管理后台,通過 appid _wx6afed118d9e81df9_ 查找插件並添加。本插件無需申請,添加后可直接使用。
例子可以看 TFJS Mobilenet: 物體識別小程序
有興趣的讀者可以前往 NEXT 學院,進行后續深度學習。課程地址:https://ke.qq.com/course/428263
模型部署
通過 TensorFlow.js 加載 Python 模型
$ pip install tensorflowjs
使用細節,可以通過 --help 參數查看程序幫助:
$ tensorflowjs_converter --help
以 MobilenetV1 為例,看一下如何對模型文件進行轉換操作,並將可以被 TensorFlow.js 加載的模型文件,存放到 /mobilenet/tfjs_model 目錄下。
tensorflowjs_converter \ --input_format=tf_saved_model \ --output_node_names='MobilenetV1/Predictions/Reshape_1' \ --saved_model_tags=serve \ /mobilenet/saved_model \ /mobilenet/tfjs_model
轉換完成的模型,保存為了兩類文件:
model.json:模型架構
group1-shard*of*:模型參數
舉例來說,我們對 MobileNet v2 轉換出來的文件,如下:
/mobilenet/tfjs_model/model.json /mobilenet/tfjs_model/group1-shard1of5 … /mobilenet/tfjs_model/group1-shard5of5
$ npm install @tensorflow/tfjs
import * as tf from '@tensorflow/tfjs' const MODEL_URL = '/mobilenet/tfjs_model/model.json' const model = await tf.loadGraphModel(MODEL_URL) const cat = document.getElementById('cat') model.execute(tf.browser.fromPixels(cat))
使用 TensorFlow.js 模型庫
模型庫 GitHub 地址:https://github.com/tensorflow/tfjs-models,其中模型分類包括圖像識別、語音識別、人體姿態識別、物體識別、文字分類等。
在程序內使用模型 API 時要提供 modelUrl 的參數,可以指向谷歌中國的鏡像服務器。
谷歌雲的 base url 是 https://storage.googleapis.com
中國鏡像的 base url 是 https://www.gstaticcnapps.cn
模型的 url path 是一致的。以 posenet 模型為例:
-
谷歌雲地址是:https://storage.googleapis.com/tfjs-models/savedmodel/posenet/mobilenet/float/050/model-stride16.json
-
中國鏡像地址是:https://www.gstaticcnapps.cn/tfjs-models/savedmodel/posenet/mobilenet/float/050/model-stride16.json
在瀏覽器中使用 MobileNet 進行攝像頭物體識別
<head>
<script src="https://unpkg.com/@tensorflow/tfjs"></script>
<script src="https://unpkg.com/@tensorflow-models/mobilenet"> </script>
</head>
<video width=400 height=300></video> <p></p> <img width=400 height=300 />
<video>,用來顯示我們截取特定幀的 <img>,和用來顯示檢測文字結果的 <p>:
const video = document.querySelector('video') const image = document.querySelector('img') const status = document.querySelector("p") const canvas = document.createElement('canvas') const ctx = canvas.getContext('2d') let model
<video>, <img>, <p> 三個 HTML 元素,canvas 和 ctx 用來做從攝像頭獲取視頻流數據的中轉存儲。model 將用來存儲我們從網絡上加載的 MobileNet:
async function main () { status.innerText = "Model loading..." model = await mobilenet.load() status.innerText = "Model is loaded!" const stream = await navigator.mediaDevices.getUserMedia({ video: true }) video.srcObject = stream await video.play() canvas.width = video.videoWidth canvas.height = video.videoHeight refresh() }
<video> 這個 HTML 元素上,最后觸發 refresh() 函數,進行定期刷新操作
async function refresh(){ ctx.drawImage(video, 0,0) image.src = canvas.toDataURL('image/png') await model.load() const predictions = await model.classify(image) const className = predictions[0].className const percentage = Math.floor(100 * predictions[0].probability) status.innerHTML = percentage + '%' + ' ' + className setTimeout(refresh, 100) }
<html>
<head>
<script src="https://unpkg.com/@tensorflow/tfjs"></script>
<script src="https://unpkg.com/@tensorflow-models/mobilenet"> </script>
</head>
<video width=400 height=300></video>
<p></p>
<img width=400 height=300 />
<script>
const video = document.querySelector('video')
const image = document.querySelector('img')
const status = document.querySelector("p")
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
let model
main()
async function main () {
status.innerText = "Model loading..."
model = await mobilenet.load()
status.innerText = "Model is loaded!"
const stream = await navigator.mediaDevices.getUserMedia({ video: true })
video.srcObject = stream
await video.play()
canvas.width = video.videoWidth
canvas.height = video.videoHeight
refresh()
}
async function refresh(){
ctx.drawImage(video, 0,0)
image.src = canvas.toDataURL('image/png')
await model.load()
const predictions = await model.classify(image)
const className = predictions[0].className
const percentage = Math.floor(100 * predictions[0].probability)
status.innerHTML = percentage + '%' + ' ' + className
setTimeout(refresh, 100)
}
</script>
</html>
TensorFlow.js 模型訓練 *
與 TensorFlow Serving 和 TensorFlow Lite 不同,TensorFlow.js 不僅支持模型的部署和推斷,還支持直接在 TensorFlow.js 中進行模型訓練、
基礎章節中,我們已經用 Python 實現過,針對某城市在 2013-2017 年的房價的任務,通過對該數據進行線性回歸,即使用線性模型
來擬合上述數據,此處
和
是待求的參數。
下面我們改用 TensorFlow.js 來實現一個 JavaScript 版本。
const xsRaw = tf.tensor([2013, 2014, 2015, 2016, 2017]) const ysRaw = tf.tensor([12000, 14000, 15000, 16500, 17500]) // 歸一化 const xs = xsRaw.sub(xsRaw.min()) .div(xsRaw.max().sub(xsRaw.min())) const ys = ysRaw.sub(ysRaw.min()) .div(ysRaw.max().sub(ysRaw.min()))
const a = tf.scalar(Math.random()).variable() const b = tf.scalar(Math.random()).variable() // y = a * x + b. const f = (x) => a.mul(x).add(b) const loss = (pred, label) => pred.sub(label).square().mean() const learningRate = 1e-3 const optimizer = tf.train.sgd(learningRate) // 訓練模型 for (let i = 0; i < 10000; i++) { optimizer.minimize(() => loss(f(xs), ys)) } // 預測 console.log(`a: ${a.dataSync()}, b: ${b.dataSync()}`) const preds = f(xs).dataSync() const trues = ys.arraySync() preds.forEach((pred, i) => { console.log(`x: ${i}, pred: ${pred.toFixed(2)}, true: ${trues[i].toFixed(2)}`) })
loss() 計算損失; 使用 optimizer.minimize() 自動更新模型參數。
=>)來簡化函數的聲明和書寫
dataSync()同步函數
<html>
<head>
<script src="http://unpkg.com/@tensorflow/tfjs/dist/tf.min.js"></script>
<script>
const xsRaw = tf.tensor([2013, 2014, 2015, 2016, 2017])
const ysRaw = tf.tensor([12000, 14000, 15000, 16500, 17500])
// 歸一化
const xs = xsRaw.sub(xsRaw.min())
.div(xsRaw.max().sub(xsRaw.min()))
const ys = ysRaw.sub(ysRaw.min())
.div(ysRaw.max().sub(ysRaw.min()))
const a = tf.scalar(Math.random()).variable()
const b = tf.scalar(Math.random()).variable()
// y = a * x + b.
const f = (x) => a.mul(x).add(b)
const loss = (pred, label) => pred.sub(label).square().mean()
const learningRate = 1e-3
const optimizer = tf.train.sgd(learningRate)
// 訓練模型
for (let i = 0; i < 10000; i++) {
optimizer.minimize(() => loss(f(xs), ys))
}
// 預測
console.log(`a: ${a.dataSync()}, b: ${b.dataSync()}`)
const preds = f(xs).dataSync()
const trues = ys.arraySync()
preds.forEach((pred, i) => {
console.log(`x: ${i}, pred: ${pred.toFixed(2)}, true: ${trues[i].toFixed(2)}`)
})
</script>
</head>
</html>
TensorFlow.js 性能對比
基於 MobileNet 的評測
與 TensorFlow Lite 代碼基准相比,手機瀏覽器中的 TensorFlow.js 在 IPhoneX 上的運行時間為基准的 1.2 倍,在 Pixel3 上運行的時間為基准的 1.8 倍。
與 Python 代碼基准相比,瀏覽器中的 TensorFlow.js 在 CPU 上的運行時間為基准的 1.7 倍,在 GPU (WebGL) 上運行的時間為基准的 3.8 倍。
與 Python 代碼基准相比,Node.js 的 TensorFlow.js 在 CPU 上的運行時間與基准相同,在 GPU(CUDA) 上運行的時間是基准的 1.6 倍。
TensorFlow 分布式訓練
TensorFlow 在 tf.distribute.Strategy 中為我們提供了若干種分布式策略,使得我們能夠更高效地訓練模型。
單機多卡訓練: MirroredStrategy
strategy = tf.distribute.MirroredStrategy()
可以在參數中指定設備,如:
strategy = tf.distribute.MirroredStrategy(devices=["/gpu:0", "/gpu:1"])
即指定只使用第 0、1 號 GPU 參與分布式策略。
with strategy.scope(): # 模型構建代碼
import tensorflow as tf import tensorflow_datasets as tfds num_epochs = 5 batch_size_per_replica = 64 learning_rate = 0.001 strategy = tf.distribute.MirroredStrategy() print('Number of devices: %d' % strategy.num_replicas_in_sync) # 輸出設備數量 batch_size = batch_size_per_replica * strategy.num_replicas_in_sync # 載入數據集並預處理 def resize(image, label): image = tf.image.resize(image, [224, 224]) / 255.0 return image, label # 使用 TensorFlow Datasets 載入貓狗分類數據集,詳見“TensorFlow Datasets數據集載入”一章 dataset = tfds.load("cats_vs_dogs", split=tfds.Split.TRAIN, as_supervised=True) dataset = dataset.map(resize).shuffle(1024).batch(batch_size) with strategy.scope(): model = tf.keras.applications.MobileNetV2() model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=learning_rate), loss=tf.keras.losses.sparse_categorical_crossentropy, metrics=[tf.keras.metrics.sparse_categorical_accuracy] ) model.fit(dataset, epochs=num_epochs)
多機訓練: MultiWorkerMirroredStrategy
將 MirroredStrategy 更換為適合多機訓練的 MultiWorkerMirroredStrategy 即可。不過,由於涉及到多台計算機之間的通訊,還需要進行一些額外的設置。具體而言,需要設置環境變量 TF_CONFIG
os.environ['TF_CONFIG'] = json.dumps({ 'cluster': { 'worker': ["localhost:20000", "localhost:20001"] }, 'task': {'type': 'worker', 'index': 0} })
TF_CONFIG 由 cluster 和 task 兩部分組成:
-
cluster說明了整個多機集群的結構和每台機器的網絡地址(IP + 端口號)。對於每一台機器,cluster的值都是相同的; -
task說明了當前機器的角色。例如,{'type': 'worker', 'index': 0}說明當前機器是cluster中的第 0 個 worker(即localhost:20000)。每一台機器的task值都需要針對當前主機進行分別的設置。
請在各台機器上均注意防火牆的設置,尤其是需要開放與其他主機通信的端口。如上例的 0 號 worker 需要開放 20000 端口,1 號 worker 需要開放 20001 端口。
import tensorflow as tf import tensorflow_datasets as tfds import os import json num_epochs = 5 batch_size_per_replica = 64 learning_rate = 0.001 num_workers = 2 os.environ['TF_CONFIG'] = json.dumps({ 'cluster': { 'worker': ["localhost:20000", "localhost:20001"] }, 'task': {'type': 'worker', 'index': 0} }) strategy = tf.distribute.experimental.MultiWorkerMirroredStrategy() batch_size = batch_size_per_replica * num_workers def resize(image, label): image = tf.image.resize(image, [224, 224]) / 255.0 return image, label dataset = tfds.load("cats_vs_dogs", split=tfds.Split.TRAIN, as_supervised=True) dataset = dataset.map(resize).shuffle(1024).batch(batch_size) with strategy.scope(): model = tf.keras.applications.MobileNetV2() model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=learning_rate), loss=tf.keras.losses.sparse_categorical_crossentropy, metrics=[tf.keras.metrics.sparse_categorical_accuracy] ) model.fit(dataset, epochs=num_epochs)
在所有機器性能接近的情況下,訓練時長與機器的數目接近於反比關系。
使用TPU訓練TensorFlow模型
TPU 代表 Tensor Processing Unit (張量處理單元)
免費 TPU:Google Colab
最方便使用 TPU 的方法,就是使用 Google 的 Colab ,不但通過瀏覽器訪問直接可以用,而且還免費。
在 Google Colab 的 Notebook 界面中,打開界面中,打開主菜單 Runtime ,然后選擇 Change runtime type,會彈出 Notebook settings 的窗口。選擇里面的 Hardware accelerator為 TPU 就可以了。
import os import pprint import tensorflow as tf if 'COLAB_TPU_ADDR' not in os.environ: print('ERROR: Not connected to a TPU runtime') else: tpu_address = 'grpc://' + os.environ['COLAB_TPU_ADDR'] print ('TPU address is', tpu_address) with tf.Session(tpu_address) as session: devices = session.list_devices() print('TPU devices:') pprint.pprint(devices)
Cloud TPU
在 Google Cloud 上,我們可以購買所需的 TPU 資源,用來按需進行機器學習訓練。為了使用 Cloud TPU ,需要在 Google Cloud Engine 中啟動 VM 並為 VM 請求 Cloud TPU 資源。請求完成后,VM 就可以直接訪問分配給它專屬的 Cloud TPU了。
Source: TPUs for Developers
在使用 Cloud TPU 時,為了免除繁瑣的驅動安裝,我們可以通過直接使用 Google Cloud 提供的 VM 操作系統鏡像。
TPU 基礎使用
在 TPU 上進行 TensorFlow 分布式訓練的核心API是 tf.distribute.TPUStrategy ,可以簡單幾行代碼就實現在 TPU 上的分布式訓練,同時也可以很容易的遷移到 GPU單機多卡、多機多卡的環境。
resolver = tf.distribute.resolver.TPUClusterResolver( tpu='grpc://' + os.environ['COLAB_TPU_ADDR']) tf.config.experimental_connect_to_host(resolver.master()) tf.tpu.experimental.initialize_tpu_system(resolver) strategy = tf.distribute.experimental.TPUStrategy(resolver)
以下使用 Fashion MNIST 分類任務展示 TPU 的使用方式。本小節的源代碼可以在 https://github.com/huan/tensorflow-handbook-tpu 找到。
更方便的是在 Google Colab 上直接打開本例子的 Jupyter 直接運行,地址:https://colab.research.google.com/github/huan/tensorflow-handbook-tpu/blob/master/tensorflow-handbook-tpu-example.ipynb (推薦)
import tensorflow as tf import numpy as np import os (x_train, y_train), (x_test, y_test) = tf.keras.datasets.fashion_mnist.load_data() # add empty color dimension x_train = np.expand_dims(x_train, -1) x_test = np.expand_dims(x_test, -1) def create_model(): model = tf.keras.models.Sequential() model.add(tf.keras.layers.Conv2D(64, (3, 3), input_shape=x_train.shape[1:])) model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2), strides=(2,2))) model.add(tf.keras.layers.Activation('relu')) model.add(tf.keras.layers.Flatten()) model.add(tf.keras.layers.Dense(10)) model.add(tf.keras.layers.Activation('softmax')) return model resolver = tf.distribute.resolver.TPUClusterResolver( tpu='grpc://' + os.environ['COLAB_TPU_ADDR']) tf.config.experimental_connect_to_host(resolver.master()) tf.tpu.experimental.initialize_tpu_system(resolver) strategy = tf.distribute.experimental.TPUStrategy(resolver) with strategy.scope(): model = create_model() model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3), loss=tf.keras.losses.sparse_categorical_crossentropy, metrics=[tf.keras.metrics.sparse_categorical_accuracy]) model.fit( x_train.astype(np.float32), y_train.astype(np.float32), epochs=5, steps_per_epoch=60, validation_data=(x_test.astype(np.float32), y_test.astype(np.float32)), validation_freq=5 )
