面向機(jī)器智能的TensorFlow實(shí)踐:產(chǎn)品環(huán)境中模型的部署
在了解如何利用TesnsorFlow構(gòu)建和訓(xùn)練各種模型——從基本的機(jī)器學(xué)習(xí)模型到復(fù)雜的深度學(xué)習(xí)網(wǎng)絡(luò)后,我們就要考慮如何將訓(xùn)練好的模型投入于產(chǎn)品,以使其能夠?yàn)槠渌麘?yīng)用所用,本文對此將進(jìn)行詳細(xì)介紹。文章節(jié)選自《面向機(jī)器智能的TensorFlow實(shí)踐》第7章。
本文將創(chuàng)建一個(gè)簡單的Web App,使用戶能夠上傳一幅圖像,并對其運(yùn)行Inception模型,實(shí)現(xiàn)圖像的自動(dòng)分類。
搭建TensorFlow服務(wù)開發(fā)環(huán)境
Docker鏡像
TensorFlow服務(wù)是用于構(gòu)建允許用戶在產(chǎn)品中使用我們提供的模型的服務(wù)器的工具。在開發(fā)過程中,使用該工具的方法有兩種:手工安裝所有的依賴項(xiàng)和工具,并從源碼開始構(gòu)建;或利用Docker鏡像。這里準(zhǔn)備使用后者,因?yàn)樗菀住⒏蓛?,同時(shí)允許在其他不同于Linux的環(huán)境中進(jìn)行開發(fā)。
如果不了解Docker鏡像,不妨將其想象為一個(gè)輕量級的虛擬機(jī)鏡像,但它在運(yùn)行時(shí)不需要以在其中運(yùn)行完整的操作系統(tǒng)為代價(jià)。如果尚未安裝Docker,請?jiān)陂_發(fā)機(jī)中安裝它,點(diǎn)擊查看具體安裝步驟(https://docs.docker.com/engine/installation/)。
為了使用Docker鏡像,還可利用筆者提供的文件(https://github.com/tensorflow/serving/blob/master/tensorflow_serving/tools/docker/Dockerfile.devel),它是一個(gè)用于在本地創(chuàng)建鏡像的配置文件。要使用該文件,可使用下列命令:
- docker build --pull -t $USER/tensorflow-serving-devel
- https://raw.githubusercontent.com/tensorflow/serving/master/
- tensorflow_serving/tools/docker/Dockerfile.devel
請注意,執(zhí)行上述命令后,下載所有的依賴項(xiàng)可能需要一段較長的時(shí)間。
上述命令執(zhí)行完畢后,為了使用該鏡像運(yùn)行容器,可輸入下列命令:
- docker run -v $HOME:/mnt/home -p 9999:9999 -it $USER/
- tensorflow-serving-devel
該命令執(zhí)行后會(huì)將你的home目錄加載到容器的/mnt/home路徑中,并允許在其中的一個(gè)終端下工作。這是非常有用的,因?yàn)槟憧墒褂米约浩玫腎DE或編輯器直接編輯代碼,同時(shí)在運(yùn)行構(gòu)建工具時(shí)僅使用該容器。它還會(huì)開放端口9999,使你可從自己的主機(jī)中訪問它,并供以后將要構(gòu)建的服務(wù)器使用。
鍵入exit命令可退出該容器終端,使其停止運(yùn)行,也可利用上述命令在需要的時(shí)候啟動(dòng)它。
Bazel工作區(qū)
由于TensorFlow服務(wù)程序是用C++編寫的,因此在構(gòu)建時(shí)應(yīng)使用Google的Bazel構(gòu)建工具。我們將從最近創(chuàng)建的容器內(nèi)部運(yùn)行Bazel。
Bazel在代碼級管理著第三方依賴項(xiàng),而且只要它們也需要用Bazel構(gòu)建,Bazel便會(huì)自動(dòng)下載和構(gòu)建它們。為了定義我們的項(xiàng)目將支持哪些第三方依賴項(xiàng),必須在項(xiàng)目庫的根目錄下定義一個(gè)WORKSPACE文件。
我們需要的依賴項(xiàng)是TensorFlow服務(wù)庫。在我們的例子中,TensorFlow模型庫包含了Inception模型的代碼。
不幸的是,在撰寫本書時(shí),TensorFlow服務(wù)尚不支持作為Git庫通過Bazel直接引用,因此必須在項(xiàng)目中將它作為一個(gè)Git的子模塊包含進(jìn)去:
- # 在本地機(jī)器上
- mkdir ~/serving_example
- cd ~/serving_example
- git init
- git submodule add https://github.com/tensorflow/serving.git
- tf_serving
- git.submodule update - -init - -recursive
下面利用WORKSPACE文件中的local_repository規(guī)則將第三方依賴項(xiàng)定義為在本地存儲(chǔ)的文件。此外,還需利用從項(xiàng)目中導(dǎo)入的tf_workspace規(guī)則對TensorFlow的依賴項(xiàng)初始化:
- # Bazel WORKSPACE文件
- workspace(name = "serving")
- local_repository(
- name = "tf_serving",
- path = _workspace_dir__ + "/tf_serving",
- local_repository(
- name = "org_tensorflow",
- path = _workspace_dir__ + "/tf_serving/tensorflow",
- )
- load('//tf_serving/tensorflow/tensorflow:workspace.bzl',
- 'tf_workspace')
- tf_workspace("tf_serving/tensorflow/", "@org_tensorflow")
- bind(
- name = "libssl",
- actual = "@boringssl_git//:ssl",
- )
- bind(
- name = "zlib",
- actual = "@zlib_archive//:zlib"
- )
- # 僅當(dāng)導(dǎo)入inception 模型時(shí)需要
- local_repository(
- name = "inception_model",
- path = __workspace_dir__ + "/tf_serving/tf_models/
- inception”,
- )
- 最后,需要從容器內(nèi)為Tensorflow運(yùn)行./configure:
- # 在Docker容器中
- cd /mnt/home/serving_example/tf_serving/tensorflow
- ./configure
導(dǎo)出訓(xùn)練好的模型
一旦模型訓(xùn)練完畢并準(zhǔn)備進(jìn)行評估,便需要將數(shù)據(jù)流圖及其變量值導(dǎo)出,以使其可為產(chǎn)品所用。
模型的數(shù)據(jù)流圖應(yīng)當(dāng)與其訓(xùn)練版本有所區(qū)分,因?yàn)樗仨殢恼嘉环邮蛰斎?,并對其進(jìn)行單步推斷以計(jì)算輸出。對于Inception模型這個(gè)例子,以及對于任意一般圖像識(shí)別模型,我們希望輸入是一個(gè)表示了JPEG編碼的圖像字符串,這樣就可輕易地將它傳送到消費(fèi)App中。這與從TFRecord文件讀取訓(xùn)練輸入頗為不同。
定義輸入的一般形式如下:
- def convert_external_inputs (external_x):
- #將外部輸入變換為推斷所需的輸入格式
- def inference(x):
- #從原始模型中……
- external_x = tf.placeholder(tf.string)
- x = convert_external_inputs(external_x)
- y = inference(x)
在上述代碼中,為輸入定義了占位符,并調(diào)用了一個(gè)函數(shù)將用占位符表示的外部輸入轉(zhuǎn)換為原始推斷模型所需的輸入格式。例如,我們需要將JPEG字符串轉(zhuǎn)換為Inception模型所需的圖像格式。最后,調(diào)用原始模型推斷方法,依據(jù)轉(zhuǎn)換后的輸入得到推斷結(jié)果。
例如,對于Inception模型,應(yīng)當(dāng)有下列方法:
- import tensorflow as tf
- from tensorflow_serving.session_bundle import exporter
- from inception import inception_model
- def convert_external_inputs (external_x)
- # 將外部輸入變換為推斷所需的輸入格式
- # 將圖像字符串轉(zhuǎn)換為一個(gè)各分量位于[0,1]內(nèi)的像素張量
- image =
- tf.image.convert_image_dtype(tf.image.decode_jpeg(external_x,
- channels=3), tf.float32)
- # 對圖像尺寸進(jìn)行縮放,使其符合模型期望的寬度和高度
- images = tf.image.resize_bilinear(tf.expand_dims(image,
- 0),[299,299])
- # 將像素值變換到模型所要求的區(qū)間[-1,1]內(nèi)
- images =tf.mul(tf.sub(image,0.5),2)
- return images
- def inference(images):
- logits, _ = inception_model.inference(images, 1001)
- return logits
這個(gè)推斷方法要求各參數(shù)都被賦值。我們將從一個(gè)訓(xùn)練檢查點(diǎn)恢復(fù)這些參數(shù)值。你可能還記得,在前面的章節(jié)中,我們周期性地保存模型的訓(xùn)練檢查點(diǎn)文件。那些文件中包含了當(dāng)時(shí)學(xué)習(xí)到的參數(shù),因此當(dāng)出現(xiàn)異常時(shí),訓(xùn)練進(jìn)展不會(huì)受到影響。
訓(xùn)練結(jié)束時(shí),最后一次保存的訓(xùn)練檢查點(diǎn)文件中將包含最后更新的模型參數(shù),這正是我們希望在產(chǎn)品中使用的版本。
要恢復(fù)檢查點(diǎn)文件,可使用下列代碼:
- saver = tf.train.Saver()
- with tf.Session() as sess:
- # 從訓(xùn)練檢查點(diǎn)文件恢復(fù)各交量
- ckpt = tf.train.get_checkpoint_state(sys.argv[1])
- if ckpt and ckpt.model_checkpoint_path:
- saver.restore(sess, sys.argv[1])+”/”+
- ckpt.model_checkpoint_path)
- else:
- print(“Checkpoint file not found”)
- raise SystemExit
對于Inception模型,可從下列鏈接下載一個(gè)預(yù)訓(xùn)練的檢查點(diǎn)文件:http://download.tensorflow.org/models/image/imagenet/inception-v3-2016-03-01.tar.gz。
- # 在docker容器中
- cd/tmp
- curl -O http://download.tensorflow.org/models/image/imagenet/
- inception-v3-2016-03-01.tar.gz
- tar –xzf inception-v3-2016-03-01.tar.gz
最后,利用tensorflow_serving.session_bundle.exporter.Exporter類將模型導(dǎo)出。我們通過傳入一個(gè)保存器實(shí)例創(chuàng)建了一個(gè)它的實(shí)例。然后,需要利用exporter.classification_signature方法創(chuàng)建該模型的簽名。該簽名指定了什么是input_tensor以及哪些是輸出張量。輸出由classes_tensor構(gòu)成,它包含了輸出類名稱列表以及模型分配給各類別的分值(或概率)的socres_tensor。通常,在一個(gè)包含的類別數(shù)相當(dāng)多的模型中,應(yīng)當(dāng)通過配置指定僅返回tf.nn.top_k所選擇的那些類別,即按模型分配的分?jǐn)?shù)按降序排列后的前K個(gè)類別。
最后一步是應(yīng)用這個(gè)調(diào)用了exporter.Exporter.init方法的簽名,并通過export方法導(dǎo)出模型,該方法接收一個(gè)輸出路徑、一個(gè)模型的版本號(hào)和會(huì)話對象。
- Scores, class_ids=tf.nn.top_k(y,NUM_CLASS_TO_RETURN)
- #為了簡便起見,我們將僅返回類別ID,應(yīng)當(dāng)另外對它們命名
- classes =
- tf.contrib.lookup.index_to_string(tf.to_int64(class_ids)
- mapping=tf.constant([str(i) for i in range(1001)]))
- model_exporter = exporter.Exporter(saver)
- signature = exporter.classification_signature(
- input_tensor=external_x, classes_tensor=classes,
- scores_tensor=scores)
- model_exporter.init(default_graph_signature=signature,
- init_op=tf.initialize_all_tables())
- model_exporter.export(sys.argv[1]+ "/export"
- tf.constant(time.time()), sess)
由于對Exporter類代碼中自動(dòng)生成的代碼存在依賴,所以需要在Docker容器內(nèi)部使用bazel運(yùn)行我們的導(dǎo)出器。
為此,需要將代碼保存到之前啟動(dòng)的bazel工作區(qū)內(nèi)的exporter.py中。此外,還需要一個(gè)帶有構(gòu)建規(guī)則的BUILD文件,類似于下列內(nèi)容:
- # BUILD文件
- py_binary(
- name = "export",
- srcs =[
- “export.py”,
- ],
- deps = [
- “//tensorflow_serving/session_bundle:exporter”,
- “@org_tensorflow//tensorflow:tensorflow_py”,
- #僅在導(dǎo)出 inception模型時(shí)需
- “@inception_model//inception”,
- ],
- )
然后,可在容器中通過下列命令運(yùn)行導(dǎo)出器:
- # 在Docker容器中
- cd /mnt/home/serving_example
它將依據(jù)可從/tmp/inception-v3中提取到的檢查點(diǎn)文件在/tmp/inception-v3/{current_timestamp}/ 中創(chuàng)建導(dǎo)出器。
注意,首次運(yùn)行它時(shí)需要花費(fèi)一些時(shí)間,因?yàn)樗仨氁獙ensorFlow進(jìn)行編譯。
定義服務(wù)器接口
接下來需要為導(dǎo)出的模型創(chuàng)建一個(gè)服務(wù)器。
TensorFlow服務(wù)使用gRPC協(xié)議(gRPC是一種基于HTTP/2的二進(jìn)制協(xié)議)。它支持用于創(chuàng)建服務(wù)器和自動(dòng)生成客戶端存根的各種語言。由于TensorFlow是基于C++的,所以需要在其中定義自己的服務(wù)器。幸運(yùn)的是,服務(wù)器端代碼比較簡短。
為了使用gRPS,必須在一個(gè)protocol buffer中定義服務(wù)契約,它是用于gRPC的IDL(接口定義語言)和二進(jìn)制編碼。下面來定義我們的服務(wù)。前面的導(dǎo)出一節(jié)曾提到,我們希望服務(wù)有一個(gè)能夠接收一個(gè)JPEG編碼的待分類的圖像字符串作為輸入,并可返回一個(gè)依據(jù)分?jǐn)?shù)排列的由推斷得到的類別列表。
這樣的服務(wù)應(yīng)定義在一個(gè)classification_service.proto文件中,類似于:
- syntax = "proto3";
- message ClassificationRequest {
- // JPEG 編碼的圖像字符串
- bytes input = 1;
- };
- message ClassificationResponse{
- repeated ClassificationClass classes = 1;
- };
- message ClassificationClass {
- string name = 1;
- float score = 2;
- }
可對能夠接收一幅圖像,或一個(gè)音頻片段或一段文字的任意類型的服務(wù)使用同一個(gè)接口。
為了使用像數(shù)據(jù)庫記錄這樣的結(jié)構(gòu)化輸入,需要修改ClassificationRequest消息。例如,如果試圖為Iris數(shù)據(jù)集構(gòu)建分類服務(wù),則需要如下編碼:
- message ClassificationRequest {
- float petalWidth = 1;
- float petaHeight = 2;
- float petalWidth = 3;
- float petaHeight = 4;
- }
這個(gè)proto文件將由proto編譯器轉(zhuǎn)換為客戶端和服務(wù)器相應(yīng)的類定義。為了使用protobuf編譯器,必須為BUILD文件添加一條新的規(guī)則,類似于:
- load("@protobuf//:protobuf.bzl", "cc_proto_library")
- cc_proto_library(
- name="classification_service_proto",
- srcs=["classification_service.proto"],
- cc_libs = ["@protobuf//:protobuf"],
- protoc="@protobuf//:protoc",
- default_runtime="@protobuf//:protobuf",
- use_grpc_plugin=1
- )
請注意位于上述代碼片段中最上方的load。它從外部導(dǎo)入的protobuf庫中導(dǎo)入了cc_proto_library規(guī)則定義。然后,利用它為proto文件定義了一個(gè)構(gòu)建規(guī)則。利用bazel build :classification_service_proto可運(yùn)行該構(gòu)建,并通過bazel-genfiles/classification_service.grpc.pb.h檢查結(jié)果:
- …
- class ClassificationService {
- ...
- class Service : public ::grpc::Service {
- public:
- Service();
- virtual ~Service();
- virtual ::grpc::Status classify(::grpc::ServerContext*
- context, const ::ClassificationRequest*
- request, ::ClassificationResponse* response);
- };
按照推斷邏輯,ClassificationService::Service是必須要實(shí)現(xiàn)的接口。我們也可通過檢查bazel-genfiles/classification_service.pb.h查看request和response消息的定義:
- …
- class ClassificationRequest :
- public ::google::protobuf::Message {
- ...
- const ::std::string& input() const;
- void set_input(const ::std::string& value);
- ...
- }
- class ClassificationResponse :
- public ::google::protobuf::Message {
- ...
- const ::ClassificationClass& classes() const;
- void set_allocated_classes(::ClassificationClass*
- classes);
- ...
- }
- class ClassificationClass :
- public ::google::protobuf::Message {
- ...
- const ::std::string& name() const;
- void set_name(const ::std::string& value);
- float score() const;
- void set_score(float value);
- ...
- }
可以看到,proto定義現(xiàn)在變成了每種類型的C++類接口。它們的實(shí)現(xiàn)也是自動(dòng)生成的,這樣便可直接使用它們。
實(shí)現(xiàn)推斷服務(wù)器
為實(shí)現(xiàn)ClassificationService::Service,需要加載導(dǎo)出模型并對其調(diào)用推斷方法。這可通過一個(gè)SessionBundle對象來實(shí)現(xiàn),該對象是從導(dǎo)出的模型創(chuàng)建的,它包含了一個(gè)帶有完全加載的數(shù)據(jù)流圖的TF會(huì)話對象,以及帶有定義在導(dǎo)出工具上的分類簽名的元數(shù)據(jù)。
為了從導(dǎo)出的文件路徑創(chuàng)建SessionBundle對象,可定義一個(gè)便捷函數(shù),以處理這個(gè)樣板文件:
- #include <iostream>
- #include <memory>
- #include <string>
- #include <grpc++/grpc++.h>
- #include "classification_service.grpc.pb.h"
- #include "tensorflow_serving/servables/tensorflow/
- session_bundle_factory.h"
- using namespace std;
- using namespace tensorflow::serving;
- using namespace grpc;
- unique_ptr<SessionBundle> createSessionBundle(const string&
- pathToExportFiles) {
- SessionBundleConfig session_bundle_config =
- SessionBundleConfig();
- unique_ptr<SessionBundleFactory> bundle_factory;
- SessionBundleFactory::Create(session_bundle_config,
- &bundle_factory);
- unique_ptr<SessionBundle> sessionBundle;
- bundle_factory-
- >CreateSessionBundle(pathToExportFiles, &sessionBundle);
- return sessionBundle;
- }
在這段代碼中,我們利用了一個(gè)SessionBundleFactory類創(chuàng)建了SessionBundle對象,并將其配置為從pathToExportFiles指定的路徑中加載導(dǎo)出的模型。最后返回一個(gè)指向所創(chuàng)建的SessionBundle實(shí)例的unique指針。
接下來需要定義服務(wù)的實(shí)現(xiàn)—ClassificationServiceImpl,該類將接收SessionBundle實(shí)例作為參數(shù),以在推斷中使用:
- class ClassificationServiceImpl final : public
- ClassificationService::Service {
- private:
- unique_ptr<SessionBundle> sessionBundle;
- public:
- ClassificationServiceImpl(unique_ptr<SessionBundle>
- sessionBundle) :
- sificationServiceImpl(unique_ptr<Sessi
- Status classify(ServerContext* context, const
- ClassificationRequest* request,
- ClassificationResponse* response)
- override {
- // 加載分類簽名
- ClassificationSignature signature;
- const tensorflow::Status signatureStatus =
- GetClassificationSignature(sessionBundle-
- >meta_graph_def, &signature);
- if (!signatureStatus.ok()) {
- return Status(StatusCode::INTERNAL,
- signatureStatus.error_message());
- }
- // 將 protobuf 輸入變換為推斷輸入張量
- tensorflow::Tensor
- input(tensorflow::DT_STRING, tensorflow::TensorShape());
- input.scalar<string>()() = request->input();
- vector<tensorflow::Tensor> outputs;
- //運(yùn)行推斷
- const tensorflow::Status inferenceStatus =
- sessionBundle->session->Run(
- {{signature.input().tensor_name(),
- input}},
- {signature.classes().tensor_name(),
- signature.scores().tensor_name()},
- {},
- &outputs);
- if (!inferenceStatus.ok()) {
- return Status(StatusCode::INTERNAL,
- inferenceStatus.error_message());
- }
- //將推斷輸出張量變換為protobuf輸出
- for (int i = 0; i <
- outputs[0].vec<string>().size(); ++i) {
- ClassificationClass
- *classificationClass = response->add_classes();
- classificationClass-
- >set_name(outputs[0].flat<string>()(i));
- classificationClass-
- >set_score(outputs[1].flat<float>()(i));
- }
- return Status::OK;
- }
- };
classify方法的實(shí)現(xiàn)包含了4個(gè)步驟:
- 利用GetClassificationSignature函數(shù)加載存儲(chǔ)在模型導(dǎo)出元數(shù)據(jù)中的Classification-Signature。這個(gè)簽名指定了輸入張量的(邏輯)名稱到所接收的圖像的真實(shí)名稱以及數(shù)據(jù)流圖中輸出張量的(邏輯)名稱到對其獲得推斷結(jié)果的映射。
- 將JPEG編碼的圖像字符串從request參數(shù)復(fù)制到將被進(jìn)行推斷的張量。
- 運(yùn)行推斷。它從sessionBundle獲得TF會(huì)話對象,并運(yùn)行一次,同時(shí)傳入輸入和輸出張量的推斷。
- 從輸出張量將結(jié)果復(fù)制到由ClassificationResponse消息指定的形狀中的response輸出參數(shù)并格式化。
最后一段代碼是設(shè)置gRPC服務(wù)器并創(chuàng)建ClassificationServiceImpl實(shí)例(用Session-Bundle對象進(jìn)行配置)的樣板代碼。
- int main(int argc, char** argv) {
- if (argc < 3) {
- cerr << "Usage: server <port> /path/to/export/files" <<
- endl;
- return 1;
- }
- const string serverAddress(string("0.0.0.0:") +
- argv[1]);
- const string pathToExportFile (argv[2]) ;
- unique_ptr<SessionBundle> sessionBundle =
- createSessionBundle(pathToExportFiles);
- const string serverAddres
- classificationServiceImpl(move(sessionBundle));
- ServerBuilder builder;
- builder. AddListeningPort(serverAddress,
- grpc::InsecureServerCredentials());
- builder.RegisterService(&classificationServiceImpl);
- unique_ptr<Server> server = builder.BuildAndStart();
- cout << "Server listening on " << serverAddress << endl;
- server->Wait();
- return 0;
- }
為了編譯這段代碼,需要在BUILD文件中為其定義一條規(guī)則:
- cc_binary(
- name = "server",
- srcs = [
- "server.cc",
- ],
- deps = [
- ":classification_service_proto",
- "@tf_serving//tensorflow_serving/servables/
- tensorflow:session_bundle_factory",
- "@grpc//:grpc++",
- ],
- )
借助這段代碼,便可通過命令bazel run :server 9999 /tmp/inception-v3/export/{timestamp}從容器中運(yùn)行推斷服務(wù)器。
客戶端應(yīng)用
由于gRPC是基于HTTP/2的,將來可能會(huì)直接從瀏覽器調(diào)用基于gRPC的服務(wù),但除非主流的瀏覽器支持所需的HTTP/2特性,且谷歌發(fā)布瀏覽器端的JavaScript gRPC客戶端程序,從webapp訪問推斷服務(wù)都應(yīng)當(dāng)通過服務(wù)器端的組件進(jìn)行。
接下來將基于BaseHTTPServer搭建一個(gè)簡單的Python Web服務(wù)器,BaseHTTPServer將處理上載的圖像文件,并將其發(fā)送給推斷服務(wù)進(jìn)行處理,再將推斷結(jié)果以純文本形式返回。
為了將圖像發(fā)送到推斷服務(wù)器進(jìn)行分類,服務(wù)器將以一個(gè)簡單的表單對GET請求做出響應(yīng)。所使用的代碼如下:
- From BaseHTTPServer import HTTPServer,BaseHTTPRequestHandler
- import cgi
- import classification_service_pb2
- From grpc.beta import implementations
- class ClientApp (BaseHTTPRequestHandler);
- def do_GET(self):
- self.respond_form()
- def respond_form(self, response=""):
- form = """
- <html><body>
- <h1>Image classification service</h1>
- <form enctype="multipart/form-data" method="post">
- <div>Image: <input type="file" name="file"
- accept="image/jpeg"></div>
- <div><input type="submit" value="Upload"></div>
- </form>
- %s
- </body></html>
- """
- response = form % response
- self.send_response(200)
- self.send_header("Content-type", "text/html")
- self.send_header("Content-length", len(response))
- self.end_headers()
- self.wfile.write(response)
為了從Web App服務(wù)器調(diào)用推斷功能,需要ClassificationService相應(yīng)的Python protocol buffer客戶端。為了生成它,需要運(yùn)行Python的protocol buffer編譯器:
- pip install grpcio cython grpcio-tools
- python -m grpc.tools.protoc -I. --python_out=. --
- grpc_python_out=. classification_service.proto
它將生成包含了用于調(diào)用服務(wù)的stub的classification_service_pb2.py文件。
服務(wù)器接收到POST請求后,將對發(fā)送的表單進(jìn)行解析,并用它創(chuàng)建一個(gè)Classification-Request對象。然后為這個(gè)分類服務(wù)器設(shè)置一個(gè)channel,并將請求提交給它。最后,它會(huì)將分類響應(yīng)渲染為HTML,并送回給用戶。
- def do_POST(self):
- form = cgi.FieldStorage(
- fp=self.rfile,
- headers=self.headers,
- environ={
- 'REQUEST_METHOD': 'POST',
- 'CONTENT_TYPE': self.headers['Content-Type'],
- })
- request =
- classification_service_pb2.ClassificationRequest()
- request.input = form['file'].file.read()
- channel =
- implementations.insecure_channel("127.0.0.1", 9999)
- stub =
- classification_service_pb2.beta_create_ClassificationService_stub(channel)
- response = stub.classify(request, 10) # 10 secs
- timeout
- self.respond_form("<div>Response: %s</div>" %
- response)
為了運(yùn)行該服務(wù)器,可從該容器外部使用命令python client.py。然后,用瀏覽器導(dǎo)航到http://localhost:8080來訪問其UI。請上傳一幅圖像并查看推斷結(jié)果如何。
產(chǎn)品準(zhǔn)備
在結(jié)束本文內(nèi)容之前,我們還將學(xué)習(xí)如何將分類服務(wù)器應(yīng)用于產(chǎn)品中。
首先,將編譯后的服務(wù)器文件復(fù)制到一個(gè)容器內(nèi)的永久位置,并清理所有的臨時(shí)構(gòu)建文件:
- #在容器內(nèi)部
- mkdir /opt/classification_server
- cd /mnt/home/serving_example
- cp -R bazel-bin/. /opt/classification_server
- bazel clean
現(xiàn)在,在容器外部,我們必須將其狀態(tài)提交給一個(gè)新的Docker鏡像,基本含義是創(chuàng)建一個(gè)記錄其虛擬文件系統(tǒng)變化的快照。
- #在容器外部
- docker ps
- #獲取容器ID
- docker commit <container id>
這樣,便可將圖像推送到自己偏好的docker服務(wù)云中,并對其進(jìn)行服務(wù)。
本文小結(jié)
在本文中,我們學(xué)習(xí)了如何將訓(xùn)練好的模型用于服務(wù)、如何將它們導(dǎo)出,以及如何構(gòu)建可運(yùn)行這些模型的快速、輕量級服務(wù)器;還學(xué)習(xí)了當(dāng)給定了從其他App使用TensorFlow模型的完整工具集后,如何創(chuàng)建使用這些模型的簡單Web App。