newsmode
search
Меню
arrow_back Назад

Basics – Hamel’s Blog - Hamel Husain

auto_awesomeКраткое саммари

Заметки Hamel Husain о развёртывании моделей через TensorFlow Serving на примере трансформера для классификации отзывов IMDB. Автор обучает простую модель (657 758 параметров, точность ~86% на валидации), сохраняет её в формате SavedModel с двумя версиями и показывает, как запускать Docker-контейнер tensorflow/serving с монтированием каталога моделей. Описано версионирование моделей через конфигурационные файлы (model_config_list), позволяющее отдавать несколько версий одновременно и опрашивать конфиг каждые 60 секунд. Приведены примеры запросов через REST API (POST на /v1/models/model:predict) и gRPC с использованием Protocol Buffers, которые эффективнее для крупных нагрузок. Результаты обоих способов сравниваются с прямым вызовом model.predict через np.allclose и совпадают с точностью до 1e-4.

These notes use code from here and this tutorial on tf serving.

В этих заметках используется код отсюда и этот туториал по tf serving.

Create The Model

Создаём модель

I didn’t want to use an existing model file from a tfserving tutorial, so I’m creating a new model from scratch.

Я не хотел использовать готовый файл модели из туториала по tfserving, поэтому создаю новую модель с нуля.

import tensorflow as tf from tensorflow import keras from tensorflow.keras import layers import os from train import get_model vocab_size = 20000 # Only consider the top 20k words maxlen = 200 # Only consider the first 200 words of each movie review embed_dim = 32 # Embedding size for each token num_heads = 2 # Number of attention heads ff_dim = 32 # Hidden layer size in feed forward network inside transformer

import tensorflow as tf from tensorflow import keras from tensorflow.keras import layers import os from train import get_model vocab_size = 20000 # Берём только топ-20k слов maxlen = 200 # Берём только первые 200 слов каждого отзыва embed_dim = 32 # Размер эмбеддинга для каждого токена num_heads = 2 # Число attention-голов ff_dim = 32 # Размер скрытого слоя в feed forward сети внутри трансформера

(x_train, y_train), (x_val, y_val) = keras.datasets.imdb.load_data(num_words=vocab_size) print(len(x_train), "Training sequences") print(len(x_val), "Validation sequences") x_train = keras.preprocessing.sequence.pad_sequences(x_train, maxlen=maxlen) x_val = keras.preprocessing.sequence.pad_sequences(x_val, maxlen=maxlen)

(x_train, y_train), (x_val, y_val) = keras.datasets.imdb.load_data(num_words=vocab_size) print(len(x_train), "Training sequences") print(len(x_val), "Validation sequences") x_train = keras.preprocessing.sequence.pad_sequences(x_train, maxlen=maxlen) x_val = keras.preprocessing.sequence.pad_sequences(x_val, maxlen=maxlen)

25000 Training sequences 25000 Validation sequences

25000 Training sequences 25000 Validation sequences

model = get_model(maxlen=maxlen, vocab_size=vocab_size, embed_dim=embed_dim, num_heads=num_heads, ff_dim=ff_dim)

model = get_model(maxlen=maxlen, vocab_size=vocab_size, embed_dim=embed_dim, num_heads=num_heads, ff_dim=ff_dim)

You should be careful to specify dtype properly for the input layer, so that the tfserving api validation will work properly. Like this:

Следует аккуратно указывать dtype для входного слоя, чтобы валидация API tfserving работала корректно. Вот так:

inputs = layers.Input(shape=(maxlen,), dtype='int32')

inputs = layers.Input(shape=(maxlen,), dtype='int32')

model.summary()

model.summary()

Model: "model" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_1 (InputLayer) [(None, 200)] 0 token_and_position_embeddin (None, 200, 32) 646400 g (TokenAndPositionEmbeddin g) transformer_block (Transfor (None, 200, 32) 10656 merBlock) global_average_pooling1d (G (None, 32) 0 lobalAveragePooling1D) dropout_2 (Dropout) (None, 32) 0 dense_2 (Dense) (None, 20) 660 dropout_3 (Dropout) (None, 20) 0 dense_3 (Dense) (None, 2) 42 ================================================================= Total params: 657,758 Trainable params: 657,758 Non-trainable params: 0 _________________________________________________________________

Model: "model" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_1 (InputLayer) [(None, 200)] 0 token_and_position_embeddin (None, 200, 32) 646400 g (TokenAndPositionEmbeddin g) transformer_block (Transfor (None, 200, 32) 10656 merBlock) global_average_pooling1d (G (None, 32) 0 lobalAveragePooling1D) dropout_2 (Dropout) (None, 32) 0 dense_2 (Dense) (None, 20) 660 dropout_3 (Dropout) (None, 20) 0 dense_3 (Dense) (None, 2) 42 ================================================================= Total params: 657,758 Trainable params: 657,758 Non-trainable params: 0 _________________________________________________________________

Train Model

Обучение модели

model.compile(optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"]) history = model.fit( x_train, y_train, batch_size=32, epochs=2, validation_data=(x_val, y_val) )

model.compile(optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"]) history = model.fit( x_train, y_train, batch_size=32, epochs=2, validation_data=(x_val, y_val) )

Epoch 1/2 782/782 [==============================] - 49s 58ms/step - loss: 0.3977 - accuracy: 0.8056 - val_loss: 0.2856 - val_accuracy: 0.8767 Epoch 2/2 782/782 [==============================] - 19s 24ms/step - loss: 0.1962 - accuracy: 0.9258 - val_loss: 0.3261 - val_accuracy: 0.8608

Epoch 1/2 782/782 [==============================] - 49s 58ms/step - loss: 0.3977 - accuracy: 0.8056 - val_loss: 0.2856 - val_accuracy: 0.8767 Epoch 2/2 782/782 [==============================] - 19s 24ms/step - loss: 0.1962 - accuracy: 0.9258 - val_loss: 0.3261 - val_accuracy: 0.8608

Save Model

Сохранение модели

You can serialize your tensorflow models to a SavedModel format using tf.saved_model.save(...). This format is documented here. We are saving two versions of the model in order to discuss features of how TF Serving can serve multiple model versions.

Модели tensorflow можно сериализовать в формат SavedModel с помощью tf.saved_model.save(...). Формат описан здесь. Мы сохраняем две версии модели, чтобы обсудить возможности TF Serving по отдаче нескольких версий модели.

!rm -rf ./model

!rm -rf ./model

def save_model(model_version, model_dir="./model"): model_export_path = f"{model_dir}/{model_version}" tf.saved_model.save( model, export_dir=model_export_path, ) print(f"SavedModel files: {os.listdir(model_export_path)}") save_model(model_version=1) save_model(model_version=2)

def save_model(model_version, model_dir="./model"): model_export_path = f"{model_dir}/{model_version}" tf.saved_model.save( model, export_dir=model_export_path, ) print(f"SavedModel files: {os.listdir(model_export_path)}") save_model(model_version=1) save_model(model_version=2)

WARNING:absl:Found untraced functions such as embedding_layer_call_fn, embedding_layer_call_and_return_conditional_losses, embedding_1_layer_call_fn, embedding_1_layer_call_and_return_conditional_losses, multi_head_attention_layer_call_fn while saving (showing 5 of 26). These functions will not be directly callable after loading.

WARNING:absl:Found untraced functions such as embedding_layer_call_fn, embedding_layer_call_and_return_conditional_losses, embedding_1_layer_call_fn, embedding_1_layer_call_and_return_conditional_losses, multi_head_attention_layer_call_fn while saving (showing 5 of 26). These functions will not be directly callable after loading.

INFO:tensorflow:Assets written to: ./model/1/assets

INFO:tensorflow:Assets written to: ./model/1/assets

INFO:tensorflow:Assets written to: ./model/1/assets

INFO:tensorflow:Assets written to: ./model/1/assets

SavedModel files: ['fingerprint.pb', 'variables', 'assets', 'saved_model.pb']

SavedModel files: ['fingerprint.pb', 'variables', 'assets', 'saved_model.pb']

WARNING:absl:Found untraced functions such as embedding_layer_call_fn, embedding_layer_call_and_return_conditional_losses, embedding_1_layer_call_fn, embedding_1_layer_call_and_return_conditional_losses, multi_head_attention_layer_call_fn while saving (showing 5 of 26). These functions will not be directly callable after loading.

WARNING:absl:Found untraced functions such as embedding_layer_call_fn, embedding_layer_call_and_return_conditional_losses, embedding_1_layer_call_fn, embedding_1_layer_call_and_return_conditional_losses, multi_head_attention_layer_call_fn while saving (showing 5 of 26). These functions will not be directly callable after loading.

INFO:tensorflow:Assets written to: ./model/2/assets

INFO:tensorflow:Assets written to: ./model/2/assets

INFO:tensorflow:Assets written to: ./model/2/assets

INFO:tensorflow:Assets written to: ./model/2/assets

SavedModel files: ['fingerprint.pb', 'variables', 'assets', 'saved_model.pb']

SavedModel files: ['fingerprint.pb', 'variables', 'assets', 'saved_model.pb']

Model versioning is done by saving your model into a directory with an integer. By default, the directory with the highest integer will be served. You can change this with config files.

Версионирование модели делается путём сохранения её в каталог с целым числом. По умолчанию обслуживаться будет каталог с наибольшим числом. Это можно изменить с помощью конфигурационных файлов.

!ls model/

!ls model/

1 2

1 2

Validate the API Schema

Валидация схемы API

The output of the below command will show the input schema and shape, as well as the output shape of the API we will create with tfserving.

Вывод приведённой ниже команды покажет схему и форму входа, а также форму выхода API, которое мы создадим в tfserving.

Thie below flags are mostly boilerplate. I don’t know what signature really means just yet.

Флаги ниже — это в основном шаблонные настройки. Я пока не до конца понимаю, что на самом деле означает signature.

!saved_model_cli show --dir ./model/2 --tag_set serve --signature_def serving_default

!saved_model_cli show --dir ./model/2 --tag_set serve --signature_def serving_default

The given SavedModel SignatureDef contains the following input(s): inputs['input_1'] tensor_info: dtype: DT_INT32 shape: (-1, 200) name: serving_default_input_1:0 The given SavedModel SignatureDef contains the following output(s): outputs['dense_3'] tensor_info: dtype: DT_FLOAT shape: (-1, 2) name: StatefulPartitionedCall:0 Method name is: tensorflow/serving/predict

The given SavedModel SignatureDef contains the following input(s): inputs['input_1'] tensor_info: dtype: DT_INT32 shape: (-1, 200) name: serving_default_input_1:0 The given SavedModel SignatureDef contains the following output(s): outputs['dense_3'] tensor_info: dtype: DT_FLOAT shape: (-1, 2) name: StatefulPartitionedCall:0 Method name is: tensorflow/serving/predict

Launch the docker container

Запуск Docker-контейнера

The TFServing docs really want you to use docker. But you can use the CLI tensorflow_model_server instead, which is what is packaged in the Docker container. This is what their docs say:

Документация TFServing настойчиво рекомендует использовать docker. Но вместо этого можно использовать CLI tensorflow_model_server, который как раз и упакован в Docker-контейнер. Вот что говорит их документация:

The easiest and most straight-forward way of using TensorFlow Serving is with Docker images. We highly recommend this route unless you have specific needs that are not addressed by running in a container.

Самый простой и прямой способ использования TensorFlow Serving — через Docker-образы. Мы настоятельно рекомендуем этот путь, если только у вас нет специфических требований, которые не покрываются запуском в контейнере.

TIP: This is also the easiest way to get TensorFlow Serving working with GPU support.

СОВЕТ: Это также самый простой способ заставить TensorFlow Serving работать с поддержкой GPU.

It worth looking at The Dockerfile for TFServing:

Стоит взглянуть на Dockerfile для TFServing:

ENV MODEL_BASE_PATH=/models RUN mkdir -p ${MODEL_BASE_PATH} # The only required piece is the model name in order to differentiate endpoints ENV MODEL_NAME=model RUN echo '#!/bin/bash \n\n\ tensorflow_model_server --port=8500 --rest_api_port=8501 \ --model_name=${MODEL_NAME} --model_base_path=${MODEL_BASE_PATH}/${MODEL_NAME} \ "$@"' > /usr/bin/tf_serving_entrypoint.sh \ && chmod +x /usr/bin/tf_serving_entrypoint.sh

ENV MODEL_BASE_PATH=/models RUN mkdir -p ${MODEL_BASE_PATH} # Единственная обязательная вещь — имя модели, чтобы различать endpoints ENV MODEL_NAME=model RUN echo '#!/bin/bash \n\n\ tensorflow_model_server --port=8500 --rest_api_port=8501 \ --model_name=${MODEL_NAME} --model_base_path=${MODEL_BASE_PATH}/${MODEL_NAME} \ "$@"' > /usr/bin/tf_serving_entrypoint.sh \ && chmod +x /usr/bin/tf_serving_entrypoint.sh


this means that it is looking in /models/model by default. We can consider this when mounting the local model directory into the container.

это означает, что по умолчанию он ищет в /models/model. Это нужно учитывать при монтировании локальной директории модели в контейнер.

Suppose my local model is located at /home/hamel/hamel/notes/serving/tfserving/model. This is how you would run the Docker container:

Допустим, моя локальная модель находится по пути /home/hamel/hamel/notes/serving/tfserving/model. Вот как тогда запускать Docker-контейнер:

docker run -p 8500:8500 \ --mount type=bind,source=/home/hamel/hamel/notes/serving/tfserving/model,target=/models/model \ --net=host -t tensorflow/serving

docker run -p 8500:8500 \ --mount type=bind,source=/home/hamel/hamel/notes/serving/tfserving/model,target=/models/model \ --net=host -t tensorflow/serving

TFServing on a GPU

TFServing на GPU

See the note on using GPUs in TF Serving.

См. заметку об использовании GPU в TF Serving.

However, it probably only makes sense to enable the GPU if you are going to enable batching, or if a single prediction are GPU intensive (like Stable Diffusion)

Однако включать GPU имеет смысл, вероятно, только если вы собираетесь включить батчинг или если одно предсказание само по себе требует много ресурсов GPU (как Stable Diffusion).

Testing the API

Тестирование API

According to the documentation we can see the status of our model like this:

Согласно документации, статус нашей модели можно посмотреть так:

GET http://host:port/v1/models/${MODEL_NAME}, which for us is:

GET http://host:port/v1/models/${MODEL_NAME}, что для нас выглядит так:

curl https://localhost:8501/v1/models/model

curl https://localhost:8501/v1/models/model

! curl http://localhost:8501/v1/models/model

! curl http://localhost:8501/v1/models/model

{ "model_version_status": [ { "version": "2", "state": "AVAILABLE", "status": { "error_code": "OK", "error_message": "" } } ] }

{ "model_version_status": [ { "version": "2", "state": "AVAILABLE", "status": { "error_code": "OK", "error_message": "" } } ] }

Note how this shows the highest version number by default. You can access different model versions through different endpoints and supplying the right config files.

Обратите внимание, что по умолчанию отображается наибольший номер версии. Доступ к разным версиям модели можно получить через разные endpoints и подсунув подходящие конфигурационные файлы.

Model Versioning

Версионирование модели

Models that you save into the directory have a version number, for example our model is saved at home/hamel/hamel/notes/serving/tfserving/model with directories with versions 1 and 2.

Модели, которые вы сохраняете в каталог, имеют номер версии — например, наша модель сохранена по пути home/hamel/hamel/notes/serving/tfserving/model с подкаталогами версий 1 и 2.

!ls /home/hamel/hamel/notes/serving/tfserving/model

!ls /home/hamel/hamel/notes/serving/tfserving/model

1 2

1 2

By default, TF Serving will always serve the model with the highest version number. However, you can change that with a model server config. You can also serve multiple versions of a model, add labels to models, etc. This is probably one of the most useful aspects of TF Serving. Here are some configs that allow you to serve multiple versions at the same time:

По умолчанию TF Serving всегда отдаёт модель с наибольшим номером версии. Однако это можно изменить с помощью конфигурации сервера моделей. Можно также отдавать сразу несколько версий модели, навешивать метки на модели и т. д. Это, наверное, одна из самых полезных особенностей TF Serving. Вот несколько конфигов, позволяющих отдавать несколько версий одновременно:

%%writefile ./model/models.config model_config_list { config { name: 'model' base_path: '/models/model/' model_platform: 'tensorflow' model_version_policy: {all: {}} } }

%%writefile ./model/models.config model_config_list { config { name: 'model' base_path: '/models/model/' model_platform: 'tensorflow' model_version_policy: {all: {}} } }

Overwriting ./model/models.config

Overwriting ./model/models.config

If you wanted to specify specific models to serve, you could name the versions instead of specifying all like this:

Если нужно задать конкретные модели для отдачи, можно указать имена версий вместо all, как здесь:

%%writefile ./model/models-specific.config model_config_list { config { name: 'model' base_path: '/models/model/' model_platform: 'tensorflow' model_version_policy { specific { versions: 1 versions: 2 } } } }

%%writefile ./model/models-specific.config model_config_list { config { name: 'model' base_path: '/models/model/' model_platform: 'tensorflow' model_version_policy { specific { versions: 1 versions: 2 } } } }

Overwriting ./model/models-specific.config

Overwriting ./model/models-specific.config

To read the config files, we need to pass these additional flags when running the container:

Чтобы сервер читал конфигурационные файлы, при запуске контейнера нужно передать дополнительные флаги:

docker run \ --mount type=bind,source=/home/hamel/hamel/notes/serving/tfserving/model,target=/models/model \ --net=host \ -t tensorflow/serving \ --model_config_file=/models/model/models-specific.config \ --model_config_file_poll_wait_seconds=60

docker run \ --mount type=bind,source=/home/hamel/hamel/notes/serving/tfserving/model,target=/models/model \ --net=host \ -t tensorflow/serving \ --model_config_file=/models/model/models-specific.config \ --model_config_file_poll_wait_seconds=60

The flag --model_config_file_poll_wait_seconds=60 tells the server to check for a new config file at the path every 60 seconds. This is optional but likely a good idea so you can change your config file without rebooting the server.

Флаг --model_config_file_poll_wait_seconds=60 указывает серверу проверять наличие нового конфигурационного файла по этому пути каждые 60 секунд. Это необязательно, но, скорее всего, хорошая идея, чтобы можно было менять конфиг без перезагрузки сервера.

To access a specific version of the model, you would make a request to

Чтобы обратиться к конкретной версии модели, нужно отправить запрос на

http://host:port/v1/models/${MODEL_NAME}[/versions/${VERSION}|/labels/${LABEL}]:predict. For example, for version 1 the endpoint would be http://localhost:8501/v1/models/model/versions/1:predict.

http://host:port/v1/models/${MODEL_NAME}[/versions/${VERSION}|/labels/${LABEL}]:predict. Например, для версии 1 endpoint будет http://localhost:8501/v1/models/model/versions/1:predict.

If you did not care about the version, and just wanted the highest version we can use the general endpoint without the version which will serve the highest version by default:

Если версия не важна и нужна просто наибольшая, можно использовать общий endpoint без указания версии — он по умолчанию отдаёт наибольшую версию:

http://localhost:8501/v1/models/model:predict

http://localhost:8501/v1/models/model:predict

We can test that all of these version is avialable to serve like so:

Проверить, что все версии доступны для отдачи, можно так:

! curl http://localhost:8501/v1/models/model/versions/2

! curl http://localhost:8501/v1/models/model/versions/2

{ "model_version_status": [ { "version": "2", "state": "AVAILABLE", "status": { "error_code": "OK", "error_message": "" } } ] }

{ "model_version_status": [ { "version": "2", "state": "AVAILABLE", "status": { "error_code": "OK", "error_message": "" } } ] }

! curl http://localhost:8501/v1/models/model/versions/1

! curl http://localhost:8501/v1/models/model/versions/1

{ "model_version_status": [ { "version": "1", "state": "AVAILABLE", "status": { "error_code": "OK", "error_message": "" } } ] }

{ "model_version_status": [ { "version": "1", "state": "AVAILABLE", "status": { "error_code": "OK", "error_message": "" } } ] }

TF Serving doesn’t make all versions available by default, only the latest one (with the highest number). You have to supply a config file if you want multiple versions to be made available at once. You probably should use labels to make URLs consistent in production scenarios.

TF Serving по умолчанию не делает все версии доступными — только последнюю (с наибольшим номером). Чтобы сразу было доступно несколько версий, нужно указать конфигурационный файл. В продакшене, вероятно, стоит использовать метки, чтобы URL оставались единообразными.

Make a prediction request

Запрос на предсказание

REST

REST

Time to make a prediction request. We will first try the REST API, which says the api endpoint is as follows: Note that v1 is just a hardcoded thing that has to do with the version of tfServing, not the version of the model:

Пора сделать запрос на предсказание. Сначала попробуем REST API; согласно документации, endpoint выглядит так. Обратите внимание, что v1 здесь — просто захардкоженная версия самого tfServing, а не версия модели:

POST http://host:port/v1/models/${MODEL_NAME}[/versions/${VERSION}|/labels/${LABEL}]:predict

POST http://host:port/v1/models/${MODEL_NAME}[/versions/${VERSION}|/labels/${LABEL}]:predict

import json, requests import numpy as np sample_data = x_val[:2, :] data = json.dumps( {"signature_name": "serving_default", "instances": sample_data.tolist()} ) url = "http://localhost:8501/v1/models/model:predict" # this would be "http://localhost:8501/v1/models/model/versions/1:predict" for version 1 def predict_rest(json_data, url): json_response = requests.post(url, data=json_data) response = json.loads(json_response.text) rest_outputs = np.array(response["predictions"]) return rest_outputs rest_outputs = predict_rest(data, url)

import json, requests import numpy as np sample_data = x_val[:2, :] data = json.dumps( {"signature_name": "serving_default", "instances": sample_data.tolist()} ) url = "http://localhost:8501/v1/models/model:predict" # для версии 1 было бы "http://localhost:8501/v1/models/model/versions/1:predict" def predict_rest(json_data, url): json_response = requests.post(url, data=json_data) response = json.loads(json_response.text) rest_outputs = np.array(response["predictions"]) return rest_outputs rest_outputs = predict_rest(data, url)

rest_outputs

rest_outputs

array([[0.94086391, 0.05913605], [0.00317052, 0.99682945]])

array([[0.94086391, 0.05913605], [0.00317052, 0.99682945]])

model_outputs = model.predict(sample_data)

model_outputs = model.predict(sample_data)

1/1 [==============================] - 0s 210ms/step

1/1 [==============================] - 0s 210ms/step

Let’s compare this to our model’s output. It’s close enough :)

Сравним это с выходом нашей модели. Достаточно близко :)

assert np.allclose(rest_outputs, model_outputs, rtol=1e-4)

assert np.allclose(rest_outputs, model_outputs, rtol=1e-4)

gRPC

gRPC

  • The payload format for grpc uses Protocol Buffers which are compressed better than JSON, which might make latency lower. This makes a difference for higher payload sizes, like images.
  • gRPC has some kind of bi-directional streaming whereas REST is just a response/request model. I don’t know what this means.
  • gRPC uses a newer HTTP protocol than REST. I don’t know what this means.
  • Формат полезной нагрузки для grpc использует Protocol Buffers, которые сжимаются лучше, чем JSON, что может уменьшить задержку. Это даёт эффект при больших размерах payload, например для изображений. У gRPC есть какая-то двунаправленная потоковая передача, тогда как REST — это просто модель ответ/запрос. Я не знаю, что это значит. gRPC использует более новый протокол HTTP, чем REST. Я не знаю, что это значит.

    import grpc # Create a channel that will be connected to the gRPC port of the container channel = grpc.insecure_channel("localhost:8500")

    import grpc # Создаём канал, который будет подключён к gRPC-порту контейнера channel = grpc.insecure_channel("localhost:8500")

    from tensorflow_serving.apis import predict_pb2, prediction_service_pb2_grpc stub = prediction_service_pb2_grpc.PredictionServiceStub(channel)

    from tensorflow_serving.apis import predict_pb2, prediction_service_pb2_grpc stub = prediction_service_pb2_grpc.PredictionServiceStub(channel)

    # Get the serving_input key loaded_model = tf.saved_model.load(model_export_path) input_name = list( loaded_model.signatures["serving_default"].structured_input_signature[1].keys() )[0]

    # Получаем ключ serving_input loaded_model = tf.saved_model.load(model_export_path) input_name = list( loaded_model.signatures["serving_default"].structured_input_signature[1].keys() )[0]

    input_name

    input_name

    'input_1'

    'input_1'

    def predict_grpc(data, input_name, stub): # Create a gRPC request made for prediction request = predict_pb2.PredictRequest() # Set the name of the model, for this use case it is "model" request.model_spec.name = "model" # Set which signature is used to format the gRPC query # here the default one "serving_default" request.model_spec.signature_name = "serving_default" # Set the input as the data # tf.make_tensor_proto turns a TensorFlow tensor into a Protobuf tensor request.inputs[input_name].CopyFrom(tf.make_tensor_proto(data)) # Send the gRPC request to the TF Server result = stub.Predict(request) return result sample_data = tf.convert_to_tensor(x_val[:2, :], dtype='int32') grpc_outputs = predict_grpc(sample_data, input_name, stub)

    def predict_grpc(data, input_name, stub): # Создаём gRPC-запрос для предсказания request = predict_pb2.PredictRequest() # Указываем имя модели — в данном случае "model" request.model_spec.name = "model" # Указываем, какая сигнатура используется для формирования gRPC-запроса # здесь — дефолтная "serving_default" request.model_spec.signature_name = "serving_default" # Подставляем входные данные # tf.make_tensor_proto превращает тензор TensorFlow в Protobuf-тензор request.inputs[input_name].CopyFrom(tf.make_tensor_proto(data)) # Отправляем gRPC-запрос на TF Server result = stub.Predict(request) return result sample_data = tf.convert_to_tensor(x_val[:2, :], dtype='int32') grpc_outputs = predict_grpc(sample_data, input_name, stub)

    Inspect the gRPC response

    Разбираем gRPC-ответ

    We can see all the fields that the gRPC response has. In this situation, the name of the final layer of our model will be the key that containst the predictions, which is dense_3 in this case.

    Можно увидеть все поля, которые содержит gRPC-ответ. В нашей ситуации имя последнего слоя модели будет тем ключом, под которым лежат предсказания, — в данном случае это dense_3.

    grpc_outputs

    grpc_outputs

    outputs { key: "dense_3" value { dtype: DT_FLOAT tensor_shape { dim { size: 2 } dim { size: 2 } } float_val: 0.9408639073371887 float_val: 0.059136051684617996 float_val: 0.0031705177389085293 float_val: 0.9968294501304626 } } model_spec { name: "model" version { value: 2 } signature_name: "serving_default" }

    outputs { key: "dense_3" value { dtype: DT_FLOAT tensor_shape { dim { size: 2 } dim { size: 2 } } float_val: 0.9408639073371887 float_val: 0.059136051684617996 float_val: 0.0031705177389085293 float_val: 0.9968294501304626 } } model_spec { name: "model" version { value: 2 } signature_name: "serving_default" }

    We can also get the name of the last layer of the model like this:

    Имя последнего слоя модели можно получить и так:

    loaded_model.signatures["serving_default"].structured_outputs

    loaded_model.signatures["serving_default"].structured_outputs

    {'dense_3': TensorSpec(shape=(None, 2), dtype=tf.float32, name='dense_3')}

    {'dense_3': TensorSpec(shape=(None, 2), dtype=tf.float32, name='dense_3')}

    Reshaping the Response

    Изменение формы ответа

    shape = [x.size for x in grpc_outputs.outputs['dense_3'].tensor_shape.dim] grpc_preds = np.reshape(grpc_outputs.outputs['dense_3'].float_val, shape) grpc_preds

    shape = [x.size for x in grpc_outputs.outputs['dense_3'].tensor_shape.dim] grpc_preds = np.reshape(grpc_outputs.outputs['dense_3'].float_val, shape) grpc_preds

    array([[0.94086391, 0.05913605], [0.00317052, 0.99682945]])

    array([[0.94086391, 0.05913605], [0.00317052, 0.99682945]])

    The predictions are close enough. I am not sure why they wouldn’t be exactly the same.

    Предсказания достаточно близки. Не знаю, почему они не совпадают в точности.

    assert np.allclose(model_outputs, grpc_preds,rtol=1e-4)

    assert np.allclose(model_outputs, grpc_preds,rtol=1e-4)