Делаем сервис по распознаванию изображений с помощью TensorFlow Serving

в 12:00, , рубрики: bazel, deep learning, flask, neural networks, ods, production, python, TensorFlow, tensorflow serving, Блог компании Open Data Science, машинное обучение

image
Всегда наступает то самое время, когда обученную модель нужно выпускать в production. Для этого часто приходится писать велосипеды в виде оберток библиотек машинного обучения. Но если Ваша модель реализована на Tensorflow, то у меня для Вас хорошая новость — велосипед писать не придется, т.к. можно использовать Tensorflow Serving.

В данной статье мы рассмотрим как использовать Tensorflow Serving для быстрого создания производительного сервиса по распознаванию изображений.

Tensorflow Serving — система для развертывания Tensorflow-моделей с такими возможностями как:

  • автоматический батчинг;
  • горячая замена моделей и версионирование;
  • возможность обработки параллельных запросов.

Дополнительным плюсом является возможность перегнать модель из Keras в Tensorflow-модель и задеплоить через Serving (если конечно в Keras используется Tensorflow бэкенд).

Как работает Tensorflow Serving

Делаем сервис по распознаванию изображений с помощью TensorFlow Serving - 2

Основной частью Tensorflow Serving является сервер моделей (Model Server).

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

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

  • Запустить выполнение модели для этого запроса.
  • Объединить несколько запросов в батч и провести вычисление для всего батча, если соответствующая опция (флаг --enable_batching) активирована при запуске. Обработка батчами является более эффективной (особенно на GPU), поэтому эта функция позволяет увеличить количество обрабатываемых запросов на единицу времени.
  • Поставить запрос в очередь, если на текущий момент вычислительные ресурсы заняты.

Как уже было упомянуто ранее Tensorflow Serving поддерживает горячую замену моделей. Сервер моделей постоянно сканирует указанный при запуске путь на наличии новых моделей и при нахождение новой версии автоматически загружает эту версию. Это позволяет выкладывать новые версии моделей без необходимости остановки сервера моделей.

Таким образом, Tensorflow Serving имеет достаточный функционал для полноценной работы в production. Поэтому использование таких подходов, как создание собственной обертки над моделью, выглядит неоправданно, т.к. Tensorflow Serving предлагает те же возможности и даже больше без необходимости писать и поддерживать самописные решения.

Установка

Сборка Tensorflow Serving наверное самая сложная часть использования этого инструмента. В принципе ничего сложно нет, но есть несколько подводных граблей. Именно про них я и расскажу в этом разделе.

Для сборки используется система сборки bazel.

Установка Tensorflow Serving описана на официальном сайте https://tensorflow.github.io/serving/setup. Я не буду расписывать подробно каждый шаг, а расскажу о проблемах, которые могут возникнуть при выполнении установки.

Со всеми шагами до конфигурации Tensorflow (./configure) не должно возникнуть проблем.

При конфигурации Tensorflow почти для всех параметров можно оставлять дефолтные значения. Но если вы выберете установку с CUDA, то конфигуратор спросит версию cuDNN. Надо вводить полную версию cuDNN (в моем случае 5.1.5).

Доходим до сборки (bazel build tensorflow_serving/...).

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

bazel build -c opt --copt=-mavx --copt=-mavx2 --copt=-mfma --copt=-mfpmath=both --copt=-msse4.2 tensorflow_serving/...

Проверьте, что все эти оптимизации доступны вашему процессору. У меня процессор не поддерживает AVX2 и FMA поэтому я собирал следующей командой:

bazel build -c opt --copt=-mavx --copt=-mfpmath=both --copt=-msse4.2 tensorflow_serving/...

По дефолту сборка Tensorflow потребляет много памяти, поэтому если у Вас ее не слишком много, то надо ограничить потребление ресурсов. Сделать это можно следующим флагом --local_resources availableRAM,availableCPU,availableIO (RAM in MB, CPU in cores, available I/O (1.0 being average workstation), например, --local_resources 2048,.5,1.0).

Если вы хотите собрать Tensorflow Serving с поддержкой GPU, то надо добавить флаг --config=cuda. Получится примерно такая команда.

bazel build -c opt --copt=-mavx --copt=-mfpmath=both --copt=-msse4.2 --config=cuda tensorflow_serving/...

При сборке может возникнуть следующая ошибка.

Текст ошибки

ERROR: no such target '@org_tensorflow//third_party/gpus/crosstool:crosstool': target 'crosstool' not declared in package 'third_party/gpus/crosstool' defined by /home/movchan/.cache/bazel/_bazel_movchan/835a50f8a234772a7d7dac38871b88e9/external/org_tensorflow/third_party/gpus/crosstool/BUILD.

Чтобы исправить эту ошибку, надо в файле tools/bazel.rc заменить @org_tensorflow//third_party/gpus/crosstool на @local_config_cuda//crosstool:toolchain

Еще может появиться следующая ошибка.

Текст ошибки

ERROR: /home/movchan/.cache/bazel/_bazel_movchan/835a50f8a234772a7d7dac38871b88e9/external/org_tensorflow/tensorflow/contrib/nccl/BUILD:23:1: C++ compilation of rule '@org_tensorflow//tensorflow/contrib/nccl:python/ops/_nccl_ops.so' failed: crosstool_wrapper_driver_is_not_gcc failed: error executing command external/local_config_cuda/crosstool/clang/bin/crosstool_wrapper_driver_is_not_gcc -U_FORTIFY_SOURCE '-D_FORTIFY_SOURCE=1' -fstack-protector -fPIE -Wall -Wunused-but-set-parameter ... (remaining 80 argument(s) skipped): com.google.devtools.build.lib.shell.BadExitStatusException: Process exited with status 1. In file included from external/org_tensorflow/tensorflow/conERROR: /home/movchan/.cache/bazel/_bazel_movchan/835a50f8a234772a7d7dac38871b88e9/external/org_tensorflow/tensorflow/contrib/nccl/BUILD:23:1: C++ compilation of rule '@org_tensorflow//tensorflow/contrib/nccl:python/ops/_nccl_ops.so' failed: crosstool_wrapper_driver_is_not_gcc failed: error executing command external/local_config_cuda/crosstool/clang/bin/crosstool_wrapper_driver_is_not_gcc -U_FORTIFY_SOURCE '-D_FORTIFY_SOURCE=1' -fstack-protector -fPIE -Wall -Wunused-but-set-parameter ... (remaining 80 argument(s) skipped): com.google.devtools.build.lib.shell.BadExitStatusException: Process exited with status 1. In file included from external/org_tensorflow/tensorflow/contrib/nccl/kernels/nccl_manager.cc:15:0: external/org_tensorflow/tensorflow/contrib/nccl/kernels/nccl_manager.h:23:44: fatal error: external/nccl_archive/src/nccl.h: No such file or directory compilation terminated.

Чтобы ее исправить надо удалить префикс /external/nccl_archive в строчке #include "external/nccl_archive/src/nccl.h" в следующих файлах:
tensorflow/tensorflow/contrib/nccl/kernels/nccl_ops.cc tensorflow/tensorflow/contrib/nccl/kernels/nccl_manager.h

Ура! Собрали наконец!

Экспорт модели

Экспорт модели из Tensorflow подробно описан на https://tensorflow.github.io/serving/serving_basic в разделе "Train And Export TensorFlow Model".

Для экспорта используется класс SavedModelBuilder. Я же использую Keras для тренировки Tensorflow-моделей, т.ч. я опишу процесс экспорта модели из Keras в Serving с помощью этого модуля.

Код экспорта ResNet-50, обученного на ImageNet.

import os
import tensorflow as tf
from keras.applications.resnet50 import ResNet50
from keras.preprocessing import image
from keras.applications.resnet50 import preprocess_input, decode_predictions
from tensorflow.contrib.session_bundle import exporter
import keras.backend as K

# устанавливаем режим в test time.
K.set_learning_phase(0)

# создаем модель и загружаем веса
model = ResNet50(weights='imagenet')

sess = K.get_session()

# задаем путь сохранения модели и версию модели
export_path_base = './model'
export_version = 1

export_path = os.path.join(
  tf.compat.as_bytes(export_path_base),
  tf.compat.as_bytes(str(export_version)))
print('Exporting trained model to', export_path)
builder = tf.saved_model.builder.SavedModelBuilder(export_path)

# создаем входы и выходы из тензоров
model_input = tf.saved_model.utils.build_tensor_info(model.input)
model_output = tf.saved_model.utils.build_tensor_info(model.output)

# создаем сигнатуру для предсказания, в которой устанавливаем входы и выходы модели
prediction_signature = (
  tf.saved_model.signature_def_utils.build_signature_def(
      inputs={'images': model_input},
      outputs={'scores': model_output},
      method_name=tf.saved_model.signature_constants.PREDICT_METHOD_NAME))

# добавляем сигнатуры к SavedModelBuilder
legacy_init_op = tf.group(tf.tables_initializer(), name='legacy_init_op')
builder.add_meta_graph_and_variables(
  sess, [tf.saved_model.tag_constants.SERVING],
  signature_def_map={
      'predict':
          prediction_signature,
  },
  legacy_init_op=legacy_init_op)

builder.save()

Вместо 'images' и 'scores' при установке входов и выходов можно указать любые названия. Эти названия будут использоваться далее.
Если модель имеет несколько входов и/или выходов, то нужно указать это в tf.saved_model.signature_def_utils.build_signature_def. Для этого нужно использовать model.inputs и model.outputs. Тогда код установки входов и выходов будет выглядеть следующим образом:

# создаем входы и выходы из тензоров
model_input = tf.saved_model.utils.build_tensor_info(model.inputs[0])
model_output = tf.saved_model.utils.build_tensor_info(model.outputs[0])
model_aux_input = tf.saved_model.utils.build_tensor_info(model.inputs[1])
model_aux_output = tf.saved_model.utils.build_tensor_info(model.outputs[1])

# создаем сигнатуру для предсказания
prediction_signature = (
  tf.saved_model.signature_def_utils.build_signature_def(
      inputs={'images': model_input, 'aux_input': model_aux_input},
      outputs={'scores': model_output, 'aux_output': model_aux_output},
      method_name=tf.saved_model.signature_constants.PREDICT_METHOD_NAME))

Еще стоит заметить, что в signature_def_map указываются все доступные методы (сигнатуры), которых может быть больше чем 1. В примере выше добавлен только один метод — predict. Название метода будет использоваться позже.

Запуск сервера моделей

Запуск сервера моделей осуществляется следующей командой:

./bazel-bin/tensorflow_serving/model_servers/tensorflow_model_server --enable_batching --port=9001 --model_name=resnet50 --model_base_path=/home/movchan/ml/serving_post/model

Рассмотрим, что означают флаги в данной команде.

  • enable_batching — флаг активации автоматического батчинга, позволяет Tensorflow Serving объединять запросы в батчи для более эффективной обработки.
  • port — порт, который модель будет прослушивать.
  • model_name — имя модели (будет использоваться далее).
  • model_base_path — путь до модели (туда, куда вы ее сохранили на предыдущем шаге).

Использование Tensorflow Serving из python

Для начала поставим пакет grpcio через pip.

sudo pip3 install grpcio

Вообще по туториалу на официальном сайте предлагается собирать python-скрипты через bazel. Но мне эта идея не нравится, т.ч. я нашел другой способ.

Для использования python API можно скопировать (сделать софтлинк) директорию bazel-bin/tensorflow_serving/example/inception_client.runfiles/tf_serving/tensorflow_serving. Там содержится все необходимое для работы python API. Я обычно просто копирую в директорию, в которой лежит скрипт, использующий это API.

Рассмотрим пример использования python API.

import numpy as np
from grpc.beta import implementations
from tensorflow_serving.apis import predict_pb2
from tensorflow_serving.apis import prediction_service_pb2

# Создаем канал и заглушку для запроса к Serving

host = '127.0.0.1'
port = 9001

channel = implementations.insecure_channel(host, port)
stub = prediction_service_pb2.beta_create_PredictionService_stub(channel)

# Создаем запрос

request = predict_pb2.PredictRequest()

# Указываем имя модели, которое было указано при запуске сервера (флаг model_name)

request.model_spec.name = 'resnet50'

# Указываем имя метода, которое было указано при экспорте модели (см. signature_def_map). 

request.model_spec.signature_name = 'predict'

# Копируем входные данные. Названия входов такие же как при экспорте модели.

request.inputs['images'].CopyFrom(
    tf.contrib.util.make_tensor_proto(image, shape=image.shape))

# Выполняем запрос. Второй параметр - timeout.

result = stub.Predict(request, 10.0)

# Извлекаем результаты. Названия выходов такие же как при экспорте модели.

prediction = np.array(result.outputs['scores'].float_val)

Полный код примера использования python API

import time
import sys
import tensorflow as tf
import numpy as np
from grpc.beta import implementations
from tensorflow_serving.apis import predict_pb2
from tensorflow_serving.apis import prediction_service_pb2
from keras.preprocessing import image
from keras.applications.resnet50 import preprocess_input, decode_predictions

def preprocess_image(img_path):
    img = image.load_img(img_path, target_size=(224, 224))
    x = image.img_to_array(img)
    x = np.expand_dims(x, axis=0)
    x = preprocess_input(x)
    return x

def get_prediction(host, port, img_path):
    image = preprocess_image(img_path)

    start_time = time.time()

    channel = implementations.insecure_channel(host, port)
    stub = prediction_service_pb2.beta_create_PredictionService_stub(channel)
    request = predict_pb2.PredictRequest()
    request.model_spec.name = 'resnet50'
    request.model_spec.signature_name = 'predict'

    request.inputs['images'].CopyFrom(
        tf.contrib.util.make_tensor_proto(image, shape=image.shape))

    result = stub.Predict(request, 10.0)
    prediction = np.array(result.outputs['scores'].float_val)

    return prediction, (time.time()-start_time)*1000.

if __name__ == "__main__":
    if len(sys.argv) != 4:
        print ('usage: serving_test.py <host> <port> <img_path>')
        print ('example: serving_test.py 127.0.0.1 9001 ~/elephant.jpg')
        exit()

    host = sys.argv[1]
    port = int(sys.argv[2])
    img_path = sys.argv[3]

    for i in range(10):
        prediction, elapsed_time = get_prediction(host, port, img_path)
        if i == 0:
            print('Predicted:', decode_predictions(np.atleast_2d(prediction), top=3)[0])
        print('Elapsed time:', elapsed_time, 'ms')

Сравним скорость работы Tensorflow Serving c Keras-версией.

Код на Keras

import sys
import time
from keras.applications.resnet50 import ResNet50
from keras.preprocessing import image
from keras.applications.resnet50 import preprocess_input, decode_predictions
import numpy as np

def preprocess_image(img_path):
    img = image.load_img(img_path, target_size=(224, 224))
    x = image.img_to_array(img)
    x = np.expand_dims(x, axis=0)
    x = preprocess_input(x)
    return x

def get_prediction(model, img_path):
    image = preprocess_image(img_path)

    start_time = time.time()
    prediction = model.predict(image)

    return prediction, (time.time()-start_time)*1000.

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print ('usage: keras_test.py <img_path>')
        print ('example: keras_test.py ~/elephant.jpg')
        exit()

    img_path = sys.argv[1]

    model = ResNet50(weights='imagenet')

    for i in range(10):
        prediction, elapsed_time = get_prediction(model, img_path)
        if i == 0:
            print('Predicted:', decode_predictions(np.atleast_2d(prediction), top=3)[0])
        print('Elapsed time:', elapsed_time, 'ms')

Все замеры производились на CPU.

Для тестирования возьмем эту фотографию кота с Pexels.com, которую я нашел через https://everypixel.com.

Делаем сервис по распознаванию изображений с помощью TensorFlow Serving - 3

Keras

Predicted: [('n02127052', 'lynx', 0.59509182), ('n02128385', 'leopard', 0.050437182), ('n02123159', 'tiger_cat', 0.049577814)]
Elapsed time: 419.47126388549805 ms
Elapsed time: 125.33354759216309 ms
Elapsed time: 122.70569801330566 ms
Elapsed time: 122.8172779083252 ms
Elapsed time: 122.3604679107666 ms
Elapsed time: 116.24360084533691 ms
Elapsed time: 116.51420593261719 ms
Elapsed time: 113.5416030883789 ms
Elapsed time: 112.34736442565918 ms
Elapsed time: 110.09907722473145 ms

Serving

Predicted: [('n02127052', 'lynx', 0.59509176015853882), ('n02128385', 'leopard', 0.050437178462743759), ('n02123159', 'tiger_cat', 0.049577809870243073)]
Elapsed time: 117.71702766418457 ms
Elapsed time: 75.67715644836426 ms
Elapsed time: 72.94225692749023 ms
Elapsed time: 71.62714004516602 ms
Elapsed time: 71.4271068572998 ms
Elapsed time: 74.54872131347656 ms
Elapsed time: 70.8014965057373 ms
Elapsed time: 70.94025611877441 ms
Elapsed time: 70.58024406433105 ms
Elapsed time: 68.82333755493164 ms

Как видно, Serving работает даже быстрее, чем версия на Keras. Это будет еще заметнее при большом количестве запросов.

Реализация REST API к Tensorflow Serving через Flask

Сначала установим Flask.

sudo pip3 install flask

Полный код REST-сервиса

from flask import Flask
from flask import request
from flask import jsonify
import tensorflow as tf
from grpc.beta import implementations
from tensorflow_serving.apis import predict_pb2
from tensorflow_serving.apis import prediction_service_pb2
from keras.preprocessing import image
from keras.applications.resnet50 import preprocess_input, decode_predictions
import numpy as np

application = Flask(__name__)

host = '127.0.0.1'
port = 9001

def preprocess_image(img):
    img = image.load_img(img, target_size=(224, 224))
    x = image.img_to_array(img)
    x = np.expand_dims(x, axis=0)
    x = preprocess_input(x)
    return x

def get_prediction(img):
    image = preprocess_image(img)

    channel = implementations.insecure_channel(host, port)
    stub = prediction_service_pb2.beta_create_PredictionService_stub(channel)
    request = predict_pb2.PredictRequest()
    request.model_spec.name = 'resnet50'
    request.model_spec.signature_name = 'predict'

    request.inputs['images'].CopyFrom(
        tf.contrib.util.make_tensor_proto(image, shape=image.shape))

    result = stub.Predict(request, 10.0)
    prediction = np.array(result.outputs['scores'].float_val)

    return decode_predictions(np.atleast_2d(prediction), top=3)[0]

@application.route('/predict', methods=['POST'])
def predict():
    if request.files.get('data'):
        img = request.files['data']
        resp = get_prediction(img)
        response = jsonify(resp)
        return response
    else:
        return jsonify({'status': 'error'})

if __name__ == "__main__":
    application.run()

Запустим сервис.

python3 serving_service.py

Протестируем сервис. Отправим запрос через curl.

curl '127.0.0.1:5000/predict' -X POST -F "data=@./cat.jpeg"

Получаем ответ следующего вида.

[ [ "n02127052", "lynx", 0.5950918197631836 ], [ "n02128385", "leopard", 0.05043718218803406 ], [ "n02123159", "tiger_cat", 0.04957781359553337 ] ]

Замечательно! Оно работает!

Заключение

В данной статье мы рассмотрели как можно использовать Tensorflow Serving для деплоймента моделей в production. Также рассмотрели как можно реализовать простой REST-сервис на Flask, обращающийся к серверу моделей.

Ссылки

Официальный сайт Tensorflow Serving
Код всех скриптов статьи

Автор: movchan74

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js