建立可分類圖片的簡易網站

1. 事前準備

在本程式碼研究室中,您將瞭解如何使用 TensorFlow Serving (REST 和 gRPC) 從網站執行圖片分類推論。

必要條件

  • 具備網頁開發基本知識,例如 HTML 和 JavaScript
  • 具備 TensorFlow 機器學習基本知識,例如訓練和部署
  • 終端機和 Docker 的基本知識

課程內容

  • 如何在 TensorFlow Hub 尋找預先訓練的圖片分類模型。
  • 如何建構簡單的網站,並透過 TensorFlow Serving (REST 和 gRPC) 使用下載的圖片分類模型進行預測。
  • 如何在 UI 中算繪偵測結果。

軟硬體需求

2. 做好準備

如要下載本程式碼研究室的程式碼,請按照下列步驟操作:

  1. 前往這個 GitHub 存放區
  2. 依序點選「Code」>「Download zip」,下載這個程式碼研究室的所有程式碼。

a72f2bb4caa9a96.png

  1. 將下載的 ZIP 檔案解壓縮,解壓縮後會產生 codelabs 根資料夾,內含所有需要的資源。

在本程式碼研究室中,您只需要存放區中 TFServing/ImageClassificationWeb 子目錄的檔案,其中包含兩個資料夾:

  • starter 資料夾包含本程式碼研究室的範例程式碼。
  • finished 資料夾包含完成的程式碼,適用於完成的範例應用程式。

3. 安裝依附元件

如要安裝依附元件,請按照下列步驟操作:

  • 在終端機中,前往 starter 資料夾,然後安裝必要的 NPM 套件:
npm install

4. 執行範例網站

使用 Web Server for Chrome 載入 TFServing/ImageClassificationWeb/starter/dist/index.html 檔案:

  1. 在 Chrome 的網址列中輸入 Chrome://apps/,然後在應用程式清單中找出「Web Server for Chrome」
  2. 啟動 Web Server for Chrome,然後選擇 TFServing/ImageClassificationWeb/starter/dist/ 資料夾。
  3. 按一下「Web Server」切換鈕啟用網路伺服器,然後在瀏覽器中前往 http://localhost:8887/

f7b43cd44ebf1f1b.png

執行及探索網站

現在應該會看到網站。使用者介面相當簡單:使用者可以分類貓咪圖片,並透過 REST 或 gRPC 將資料傳送至後端。後端會對圖片進行圖片分類,並將分類結果傳回網站,網站會顯示結果。

837d97a27c59a0b3.png

如果您點選「分類」,系統不會有任何反應,因為目前還無法與後端通訊。

5. 使用 TensorFlow Serving 部署圖片分類模型

圖片分類是常見的機器學習工作,會根據圖片的主要內容,將圖片分類到預先定義的類別。以下是花朵分類的範例:

a6da16b4a7665db0.png

TensorFlow Hub 提供多種預先訓練的圖片分類模型。在本程式碼研究室中,您會使用熱門的 Inception v3 模型。

如要使用 TensorFlow Serving 部署圖片分類模型,請按照下列步驟操作:

  1. 下載 Inception v3 模型檔案。
  2. 使用 7-Zip 等解壓縮工具,將下載的 .tar.gz 檔案解壓縮。
  3. 建立 inception_v3 資料夾,然後在其中建立 123 子資料夾。
  4. 將解壓縮的 variables 資料夾和 saved_model.pb 檔案放入 123 子資料夾。

您可以將 inception_v3 資料夾視為 SavedModel 資料夾。123 是版本號碼範例。如有需要,可以選擇其他號碼。

資料夾結構應如下圖所示:

21a8675ac8d31907.png

啟動 TensorFlow Serving

  • 在終端機中,使用 Docker 啟動 TensorFlow Serving,但請將 PATH/TO/SAVEDMODEL 替換為電腦上 inception_v3 資料夾的絕對路徑。
docker pull tensorflow/serving

docker run -it --rm -p 8500:8500 -p 8501:8501 -v "PATH/TO/SAVEDMODEL:/models/inception" -e MODEL_NAME=inception tensorflow/serving

Docker 會先自動下載 TensorFlow Serving 映像檔,這需要一分鐘。TensorFlow Serving 隨即會啟動。記錄應如下列程式碼片段所示:

2022-02-25 06:01:12.513231: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:206] Restoring SavedModel bundle.
2022-02-25 06:01:12.585012: I external/org_tensorflow/tensorflow/core/platform/profile_utils/cpu_utils.cc:114] CPU Frequency: 3000000000 Hz
2022-02-25 06:01:13.395083: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:190] Running initialization op on SavedModel bundle at path: /models/inception/123
2022-02-25 06:01:13.837562: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:277] SavedModel load for tags { serve }; Status: success: OK. Took 1928700 microseconds.
2022-02-25 06:01:13.877848: I tensorflow_serving/servables/tensorflow/saved_model_warmup_util.cc:59] No warmup data file found at /models/inception/123/assets.extra/tf_serving_warmup_requests
2022-02-25 06:01:13.929844: I tensorflow_serving/core/loader_harness.cc:87] Successfully loaded servable version {name: inception version: 123}
2022-02-25 06:01:13.985848: I tensorflow_serving/model_servers/server_core.cc:486] Finished adding/updating models
2022-02-25 06:01:13.985987: I tensorflow_serving/model_servers/server.cc:367] Profiler service is enabled
2022-02-25 06:01:13.988994: I tensorflow_serving/model_servers/server.cc:393] Running gRPC ModelServer at 0.0.0.0:8500 ...
[warn] getaddrinfo: address family for nodename not supported
2022-02-25 06:01:14.033872: I tensorflow_serving/model_servers/server.cc:414] Exporting HTTP/REST API at:localhost:8501 ...
[evhttp_server.cc : 245] NET_LOG: Entering the event loop ...

6. 設定 Envoy Proxy

目前 TensorFlow Serving 不會設定 Access-Control-Allow-Origin 標頭,因此基於安全考量,瀏覽器會封鎖前端 JavaScript 對 TensorFlow Serving 的要求。如要解決這個問題,您需要使用 Proxy (例如 Envoy),將 JavaScript 的要求 Proxy 至 TensorFlow Serving 後端。

啟動 Envoy

  • 在終端機中,使用 Docker 下載 Envoy 映像檔並啟動 Envoy,但請將 PATH/TO/ENVOY-CUSTOM.YAML 預留位置替換為 starter 資料夾中 envoy-custom.yaml 檔案的絕對路徑。
docker pull envoyproxy/envoy-dev:fd3e8370ddb7a96634c192d1461516e6de1d1797

docker run --add-host host.docker.internal:host-gateway --rm -it -p 9901:9901 -p 8000:8000 -p 8080:8080 -v PATH/TO/ENVOY-CUSTOM.YAML:/envoy-custom.yaml envoyproxy/envoy-dev:fd3e8370ddb7a96634c192d1461516e6de1d1797 -c /envoy-custom.yaml

Docker 會先自動下載 Envoy 映像檔。隨後 Envoy 應會啟動。記錄應如下列程式碼片段所示:

[2022-03-02 07:51:48.563][1][info][main] [source/server/server.cc:436]   response trailer map: 152 bytes: grpc-message,grpc-status
[2022-03-02 07:51:48.681][1][info][main] [source/server/server.cc:772] runtime: {}
[2022-03-02 07:51:48.682][1][info][admin] [source/server/admin/admin.cc:134] admin address: 0.0.0.0:9901
[2022-03-02 07:51:48.683][1][info][config] [source/server/configuration_impl.cc:127] loading tracing configuration
[2022-03-02 07:51:48.683][1][info][config] [source/server/configuration_impl.cc:87] loading 0 static secret(s)
[2022-03-02 07:51:48.683][1][info][config] [source/server/configuration_impl.cc:93] loading 2 cluster(s)
[2022-03-02 07:51:48.687][1][info][config] [source/server/configuration_impl.cc:97] loading 2 listener(s)
[2022-03-02 07:51:48.694][1][info][config] [source/server/configuration_impl.cc:109] loading stats configuration
[2022-03-02 07:51:48.696][1][info][main] [source/server/server.cc:868] starting main dispatch loop
[2022-03-02 07:51:48.881][1][info][runtime] [source/common/runtime/runtime_impl.cc:446] RTDS has finished initialization
[2022-03-02 07:51:48.881][1][info][upstream] [source/common/upstream/cluster_manager_impl.cc:207] cm init: all clusters initialized
[2022-03-02 07:51:48.881][1][info][main] [source/server/server.cc:849] all clusters initialized. initializing init manager
[2022-03-02 07:51:48.881][1][info][config] [source/server/listener_manager_impl.cc:784] all dependencies initialized. starting workers
[2022-03-02 07:51:48.902][1][warning][main] [source/server/server.cc:747] there is no configured limit to the number of allowed active connections. Set a limit via the runtime key overload.global_downstream_max_connections

7. 透過 REST 將網站連結至 TensorFlow

後端現在已準備就緒,您可以將用戶端要求傳送至 TensorFlow Serving,對圖片進行分類。您可以透過兩種方式將要求傳送至 TensorFlow Serving:

  • REST
  • gRPC

透過 REST 傳送要求及接收回應

透過 REST 傳送及接收要求時,只需完成三個簡單步驟:

  1. 建立 REST 要求。
  2. 將 REST 要求傳送至 TensorFlow Serving。
  3. 從 REST 回應中擷取預測結果,並顯示結果。

您可以在 src/index.js 檔案中完成這些步驟。

建立 REST 要求

目前 classify_img() 函式不會將 REST 要求傳送至 TensorFlow Serving。您必須先實作這個 REST 分支,才能建立 REST 要求:

if (radioButtons[0].checked) {
    console.log('Using REST');
    // TODO: Add code to send a REST request to TensorFlow Serving.

} 

TensorFlow Serving 預期會收到包含所用 Inception v3 模型圖片張量的 POST 要求,因此您需要將圖片中每個像素的 RGB 值擷取到陣列中,然後將陣列包裝在 JSON 中,也就是要求酬載。

  • 將這段程式碼新增至 REST 分支:
//Create the REST request.
let imgTensor = new Array();
let pixelArray = new Array();
context.drawImage(img, 0, 0);
for(let i=0; i<inputImgHeight; i++) {
    pixelArray[i] = new Array();
    for (let j=0; j<inputImgWidth; j++) {
        pixelArray[i][j] = new Array();
        pixelArray[i][j].push(context.getImageData(i, j, 1, 1).data[0]/255); 
        pixelArray[i][j].push(context.getImageData(i, j, 1, 1).data[1]/255); 
        pixelArray[i][j].push(context.getImageData(i, j, 1, 1).data[2]/255); 
    }
}
imgTensor.push(pixelArray);

const RESTURL = 'http://localhost:8000/v1/models/inception:predict';        
let xhr = new XMLHttpRequest();
xhr.open('POST', RESTURL);
xhr.setRequestHeader('Content-Type', 'application/json;charset=utf-8;');
let data = JSON.stringify({
    instances: imgTensor
});    
xhr.onload = () => {

}
xhr.onerror = () => {
    console.log('REST request error');
}

將 REST 要求傳送至 TensorFlow Serving

現在可以傳送要求。

  • 在 REST 分支中,於上述程式碼後方加入以下程式碼:
// Send the REST request.
xhr.send(data);

處理 TensorFlow Serving 的 REST 回應

Inception v3 模型會傳回圖片屬於預先定義類別的機率陣列。預測成功後,您應在 UI 中輸出最可能的類別。

您會實作 onload() 監聽器來處理回應。

xhr.onload = () => {

}
  • 將這段程式碼新增至 onload() 監聽器:
// Process the REST response.
const response = JSON.parse(xhr.responseText);
const maxIndex = argmax(response['predictions'][0])
document.getElementById('category').textContent = 'Predicted category: ' + maxIndex;

現在,接聽程式會從回應中擷取預測機率,找出最有可能的物件類別,並在 UI 中顯示結果。

開始執行

  1. 在終端機中前往 starter 資料夾,然後使用 webpack 將所有 JavaScript 檔案組合成單一檔案,以便嵌入 dist/index.html 檔案:
npm install -g npx
npm install --save-dev webpack
npx webpack
  1. 在瀏覽器中重新整理 http://localhost:8887/,然後按一下「REST」>「Classify」

網站會顯示預測類別「286」,這會對應至 ImageNet 資料集中的「Egyptian Cat」標籤。

c865a93b9b58335d.png

8. 透過 gRPC 將網站連結至 TensorFlow Serving

除了 REST,TensorFlow Serving 也支援 gRPC

b6f4449c2c850b0e.png

gRPC 是一種高效能的現代化開放原始碼遠端程序呼叫 (RPC) 架構,可在任何環境中執行。可有效連結資料中心內和跨資料中心的服務,並支援負載平衡、追蹤、健康狀態檢查和驗證功能。實務上,gRPC 的效能比 REST 更出色。

使用 gRPC 傳送要求及接收回應

簡單四步驟:

  1. 選用:產生 gRPC 用戶端虛設常式程式碼。
  2. 建立 gRPC 要求。
  3. 將 gRPC 要求傳送至 TensorFlow Serving。
  4. 從 gRPC 回應中擷取預測結果,並顯示在 UI 中。

您會在 src/index.js 檔案中完成這些步驟。

選用:產生 gRPC 用戶端存根程式碼

如要搭配 TensorFlow Serving 使用 gRPC,請按照 gRPC 工作流程操作。如要瞭解詳情,請參閱 gRPC 說明文件

a9d0e5cb543467b4.png

TensorFlow Serving 和 TensorFlow 會為您定義 .proto 檔案。在 TensorFlow 和 TensorFlow Serving 2.8 中,需要下列 .proto 檔案:

tensorflow/core/example/example.proto
tensorflow/core/example/feature.proto
tensorflow/core/protobuf/struct.proto
tensorflow/core/protobuf/saved_object_graph.proto
tensorflow/core/protobuf/saver.proto
tensorflow/core/protobuf/trackable_object_graph.proto
tensorflow/core/protobuf/meta_graph.proto
tensorflow/core/framework/node_def.proto
tensorflow/core/framework/attr_value.proto
tensorflow/core/framework/function.proto
tensorflow/core/framework/types.proto
tensorflow/core/framework/tensor_shape.proto
tensorflow/core/framework/full_type.proto
tensorflow/core/framework/versions.proto
tensorflow/core/framework/op_def.proto
tensorflow/core/framework/graph.proto
tensorflow/core/framework/tensor.proto
tensorflow/core/framework/resource_handle.proto
tensorflow/core/framework/variable.proto

tensorflow_serving/apis/inference.proto
tensorflow_serving/apis/classification.proto
tensorflow_serving/apis/predict.proto
tensorflow_serving/apis/regression.proto
tensorflow_serving/apis/get_model_metadata.proto
tensorflow_serving/apis/input.proto
tensorflow_serving/apis/prediction_service.proto
tensorflow_serving/apis/model.proto
  • 在終端機中,前往 starter/src/proto/ 資料夾並產生存根:
bash generate_grpc_stub_js.sh

建立 gRPC 要求

與 REST 要求類似,您會在 gRPC 分支中建立 gRPC 要求.

if (connectionMode[picker.selectedRow(inComponent: 0)] == "REST") {

}
else {
    print("Using gRPC")
    // TODO: Add code to send a gRPC request to TensorFlow Serving.
    
}
  • 將下列程式碼新增至 gRPC 分支:
// Create the gRPC request.
const PredictModule = require('./proto/generated/tensorflow_serving/apis/predict_pb.js');
const PredictionServiceClientModule = require('./proto/generated/tensorflow_serving/apis/prediction_service_grpc_web_pb.js');
const ModelModule = require('./proto/generated/tensorflow_serving/apis/model_pb.js');
const TensorModule = require('./proto/generated/tensorflow/core/framework/tensor_pb.js');

const GPRCURL = 'http://localhost:8080';
const stub = new PredictionServiceClientModule.PredictionServiceClient(GPRCURL);

const modelSpec = new ModelModule.ModelSpec();
modelSpec.setName('inception');

const tensorProto = new TensorModule.TensorProto();
const tensorShapeProto = new TensorModule.TensorShapeProto();

const batchDim = (new TensorModule.TensorShapeProto.Dim()).setSize(1);
const heightDim = (new TensorModule.TensorShapeProto.Dim()).setSize(inputImgHeight);
const widthDim = (new TensorModule.TensorShapeProto.Dim()).setSize(inputImgWidth);
const channelDim = (new TensorModule.TensorShapeProto.Dim()).setSize(3);

tensorShapeProto.setDimList([batchDim, heightDim, widthDim, channelDim]);

tensorProto.setDtype(proto.tensorflow.DataType.DT_FLOAT);
tensorProto.setTensorShape(tensorShapeProto);
context.drawImage(img, 0, 0);
for(let i=0; i<inputImgHeight; i++) {
    for (let j=0; j<inputImgWidth; j++) {
        tensorProto.addFloatVal(context.getImageData(i, j, 1, 1).data[0]/255); 
        tensorProto.addFloatVal(context.getImageData(i, j, 1, 1).data[1]/255); 
        tensorProto.addFloatVal(context.getImageData(i, j, 1, 1).data[2]/255); 
    }
}

const predictionServiceRequest = new PredictModule.PredictRequest();
predictionServiceRequest.setModelSpec(modelSpec);
predictionServiceRequest.getInputsMap().set('inputs', tensorProto);

將 gRPC 要求傳送至 TensorFlow Serving

現在可以傳送要求。

  • 在先前程式碼片段的 gRPC 分支中,緊接在程式碼後方加入下列程式碼:
// Send the gRPC request.
stub.predict(predictionServiceRequest, {}, function(err, response) {
    // TODO: Add code to process the response.
});

處理 TensorFlow Serving 的 gRPC 回應

最後,您會實作上述回呼函式來處理回應。

  • 將這段程式碼新增至前一個程式碼片段中的函式主體:
// Process the gRPC response.
if (err) {
    console.log(err.code);
    console.log(err.message);
} 
else {
    const maxIndex = argmax(response.getOutputsMap().get('logits').getFloatValList());
    document.getElementById('category').textContent = 'Predicted category: ' + maxIndex;
}

現在,接聽程式會從回應中擷取預測機率,找出最有可能的物件類別,並在 UI 中顯示結果。

開始執行

  1. 在終端機中,使用 webpack 將所有 JavaScript 檔案組合成單一檔案,以便嵌入 index.html 檔案:
npx webpack
  1. 在瀏覽器中重新整理 http://localhost:8887/
  2. 依序按一下「gRPC」>「Classify」

網站會顯示 286 的預測類別,該類別會對應至 ImageNet 資料集中的 Egyptian Cat 標籤。

9. 恭喜

您已使用 TensorFlow Serving,為網站新增圖片分類功能!

瞭解詳情