Глубокое обучение с Keras, Redis, Flask и Apache в производственной среде

задняя часть
Глубокое обучение с Keras, Redis, Flask и Apache в производственной среде

Это 6-й день моего участия в ноябрьском испытании обновлений, подробности о событии:Вызов последнего обновления 2021 г.

Сегодня мы покажем, как использовать Keras, Redis, Flask и Apache для глубокого обучения в продакшене.

Структура проекта

keras-complete-rest-api
├── helpers.py
├── jemma.png
├── keras_rest_api_app.wsgi
├── run_model_server.py
├── run_web_server.py
├── settings.py
├── simple_request.py
└── stress_test.py

Документация объясняет:

  • run_web_server.py содержит весь код нашего веб-сервера Flask — Apache загрузит его при запуске нашего веб-приложения для глубокого обучения.
  • run_model_server.py будет:
    • Загрузите нашу модель Keras с диска
    • Постоянно опрашивать Redis на наличие новых изображений для классификации
    • Классифицировать изображения (пакетная обработка для повышения эффективности)
    • Запишите результаты вывода обратно в Redis, чтобы их можно было вернуть клиенту через Flask.
  • settings.py содержит все настройки на основе Python для нашего производственного сервиса глубокого обучения, такие как информация о хосте/порте Redis, настройки классификации изображений, имена очередей изображений и т. д.
  • helpers.py содержит служебные функции (т. е. кодировку base64), которые будут использовать как run_web_server.py, так и run_model_server.py.
  • keras_rest_api_app.wsgi содержит наши настройки WSGI, поэтому мы можем обслуживать приложение Flask с нашего сервера Apache.
  • simple_request.py можно использовать для программного использования результатов нашей службы API глубокого обучения.
  • jemma.png — это фотография моего бигля. Мы будем использовать ее в качестве примера изображения при вызове REST API, чтобы убедиться, что он действительно работает.
  • Наконец, мы будем использовать stress_test.py, чтобы нагрузить наш сервер и измерить классификацию изображений.

У нас есть конечная точка /predict на сервере Flask. Этот метод находится в run_web_server.py и при необходимости вычисляет классификацию входного изображения. Предварительная обработка изображений также выполняется в run_web_server.py.

Чтобы подготовить наше серверное производство, я взял функцию процесса классификации из одного сценария с прошлой недели и поместил ее в run_model_server.py. Этот скрипт очень важен, потому что он загрузит нашу модель Keras и захватит изображения из очереди изображений в Redis для классификации. Результаты записываются обратно в Redis (конечная точка /predict и соответствующие функции в run_web_server.py отслеживают Redis для отправки результатов обратно клиенту).

Но если мы не знаем возможностей и ограничений сервера REST API для глубокого обучения, что в нем хорошего?

В stress_test.py мы тестируем наш сервер. Мы сделаем это, запустив 500 одновременных потоков, которые отправят наши изображения на сервер для параллельной классификации. Я рекомендую запустить его на локальном хосте сервера, чтобы начать, а затем запустить его с внешнего клиента.

Создание нашего веб-приложения для глубокого обучения

img

Рисунок 1: Схема потока данных сервера REST API глубокого обучения, созданного с помощью Python, Keras, Redis и Flask.

Почти каждая строка кода, используемая в этом проекте, взята из нашей предыдущей статьи «Создание масштабируемого REST API для глубокого обучения».

Установка и конфигурация

# initialize Redis connection settings
REDIS_HOST = "localhost"
REDIS_PORT = 6379
REDIS_DB = 0
# initialize constants used to control image spatial dimensions and
# data type
IMAGE_WIDTH = 224
IMAGE_HEIGHT = 224
IMAGE_CHANS = 3
IMAGE_DTYPE = "float32"
# initialize constants used for server queuing
IMAGE_QUEUE = "image_queue"
BATCH_SIZE = 32
SERVER_SLEEP = 0.25
CLIENT_SLEEP = 0.25

В settings.py вы сможете изменить параметры подключения к серверу, размер изображения + тип данных и очередь сервера.

# import the necessary packages
import numpy as np
import base64
import sys
def base64_encode_image(a):
	# base64 encode the input NumPy array
	return base64.b64encode(a).decode("utf-8")
def base64_decode_image(a, dtype, shape):
	# if this is Python 3, we need the extra step of encoding the
	# serialized NumPy string as a byte object
	if sys.version_info.major == 3:
		a = bytes(a, encoding="utf-8")
	# convert the string to a NumPy array using the supplied data
	# type and target shape
	a = np.frombuffer(base64.decodestring(a), dtype=dtype)
	a = a.reshape(shape)
	# return the decoded image
	return a

Файл helpers.py содержит две функции — одну для кодирования base64 и одну для декодирования.

Кодировка необходима, чтобы мы могли сериализовать + хранить наши изображения в Redis. Опять же, декодирование необходимо, чтобы мы могли десериализовать изображение в формат массива NumPy перед предварительной обработкой.

Веб-сервер глубокого обучения

# import the necessary packages
from tensorflow.keras.preprocessing.image import img_to_array
from tensorflow.keras.applications.resnet50 import preprocess_input
from PIL import Image
import numpy as np
import settings
import helpers
import flask
import redis
import uuid
import time
import json
import io
# initialize our Flask application and Redis server
app = flask.Flask(__name__)
db = redis.StrictRedis(host=settings.REDIS_HOST,
	port=settings.REDIS_PORT, db=settings.REDIS_DB)
def prepare_image(image, target):
	# if the image mode is not RGB, convert it
	if image.mode != "RGB":
		image = image.convert("RGB")
	# resize the input image and preprocess it
	image = image.resize(target)
	image = img_to_array(image)
	image = np.expand_dims(image, axis=0)
	image = preprocess_input(image)
	# return the processed image
	return image
@app.route("/")
def homepage():
	return "Welcome to the PyImageSearch Keras REST API!"
@app.route("/predict", methods=["POST"])
def predict():
	# initialize the data dictionary that will be returned from the
	# view
	data = {"success": False}
	# ensure an image was properly uploaded to our endpoint
	if flask.request.method == "POST":
		if flask.request.files.get("image"):
			# read the image in PIL format and prepare it for
			# classification
			image = flask.request.files["image"].read()
			image = Image.open(io.BytesIO(image))
			image = prepare_image(image,
				(settings.IMAGE_WIDTH, settings.IMAGE_HEIGHT))
			# ensure our NumPy array is C-contiguous as well,
			# otherwise we won't be able to serialize it
			image = image.copy(order="C")
			# generate an ID for the classification then add the
			# classification ID + image to the queue
			k = str(uuid.uuid4())
			image = helpers.base64_encode_image(image)
			d = {"id": k, "image": image}
			db.rpush(settings.IMAGE_QUEUE, json.dumps(d))
			# keep looping until our model server returns the output
			# predictions
			while True:
				# attempt to grab the output predictions
				output = db.get(k)
				# check to see if our model has classified the input
				# image
				if output is not None:
					# add the output predictions to our data
					# dictionary so we can return it to the client
					output = output.decode("utf-8")
					data["predictions"] = json.loads(output)
					# delete the result from the database and break
					# from the polling loop
					db.delete(k)
					break
				# sleep for a small amount to give the model a chance
				# to classify the input image
				time.sleep(settings.CLIENT_SLEEP)
			# indicate that the request was a success
			data["success"] = True
	# return the data dictionary as a JSON response
	return flask.jsonify(data)
# for debugging purposes, it's helpful to start the Flask testing
# server (don't use this for production
if __name__ == "__main__":
	print("* Starting web service...")
	app.run()

В run_web_server.py вы увидите predict функцию, связанную с нашей конечной точкой REST API /predict.

Функция прогнозирования отправляет закодированное изображение в очередь Redis, а затем выполняет цикл/опрос до тех пор, пока не получит данные прогноза с сервера модели. Затем мы кодируем данные в JSON и инструктируем Flask отправить данные обратно клиенту.

Сервер модели глубокого обучения

# import the necessary packages
from tensorflow.keras.applications import ResNet50
from tensorflow.keras.applications.resnet50 import decode_predictions
import numpy as np
import settings
import helpers
import redis
import time
import json
# connect to Redis server
db = redis.StrictRedis(host=settings.REDIS_HOST,
	port=settings.REDIS_PORT, db=settings.REDIS_DB)
def classify_process():
	# load the pre-trained Keras model (here we are using a model
	# pre-trained on ImageNet and provided by Keras, but you can
	# substitute in your own networks just as easily)
	print("* Loading model...")
	model = ResNet50(weights="imagenet")
	print("* Model loaded")
	# continually pool for new images to classify
	while True:
		# attempt to grab a batch of images from the database, then
		# initialize the image IDs and batch of images themselves
		queue = db.lrange(settings.IMAGE_QUEUE, 0,
			settings.BATCH_SIZE - 1)
		imageIDs = []
		batch = None
		# loop over the queue
		for q in queue:
			# deserialize the object and obtain the input image
			q = json.loads(q.decode("utf-8"))
			image = helpers.base64_decode_image(q["image"],
				settings.IMAGE_DTYPE,
				(1, settings.IMAGE_HEIGHT, settings.IMAGE_WIDTH,
					settings.IMAGE_CHANS))
			# check to see if the batch list is None
			if batch is None:
				batch = image
			# otherwise, stack the data
			else:
				batch = np.vstack([batch, image])
			# update the list of image IDs
			imageIDs.append(q["id"])
		# check to see if we need to process the batch
		if len(imageIDs) > 0:
			# classify the batch
			print("* Batch size: {}".format(batch.shape))
			preds = model.predict(batch)
			results = decode_predictions(preds)
			# loop over the image IDs and their corresponding set of
			# results from our model
			for (imageID, resultSet) in zip(imageIDs, results):
				# initialize the list of output predictions
				output = []
				# loop over the results and add them to the list of
				# output predictions
				for (imagenetID, label, prob) in resultSet:
					r = {"label": label, "probability": float(prob)}
					output.append(r)
				# store the output predictions in the database, using
				# the image ID as the key so we can fetch the results
				db.set(imageID, json.dumps(output))
			# remove the set of images from our queue
			db.ltrim(settings.IMAGE_QUEUE, len(imageIDs), -1)
		# sleep for a small amount
		time.sleep(settings.SERVER_SLEEP)
# if this is the main thread of execution start the model server
# process
if __name__ == "__main__":
	classify_process()

Файл run_model_server.py содержит нашу функцию classify_process. Эта функция загружает нашу модель, а затем выполняет прогнозы для пакета изображений. Этот процесс лучше всего выполнять на графическом процессоре, но можно использовать и центральный процессор.

В этом примере для простоты мы будем использовать ResNet50, предварительно обученный на наборе данных ImageNet. Вы можете изменить classify_process, чтобы использовать свои собственные модели глубокого обучения.

Конфигурация WSGI

# add our app to the system path
import sys
sys.path.insert(0, "/var/www/html/keras-complete-rest-api")
# import the application and away we go...
from run_web_server import app as application

Файл keras_rest_api_app.wsgi — это новый компонент нашего REST API глубокого обучения. Этот файл конфигурации WSGI добавляет каталог нашего сервера в системный путь и импортирует веб-приложение для запуска всего. Мы указываем на этот файл в файле настроек сервера Apache /etc/apache2/sites-available/000-default.conf, который будет рассмотрен позже в этом сообщении блога.

испытание давлением

# import the necessary packages
from threading import Thread
import requests
import time
# initialize the Keras REST API endpoint URL along with the input
# image path
KERAS_REST_API_URL = "http://localhost/predict"
IMAGE_PATH = "jemma.png"
# initialize the number of requests for the stress test along with
# the sleep amount between requests
NUM_REQUESTS = 500
SLEEP_COUNT = 0.05
def call_predict_endpoint(n):
	# load the input image and construct the payload for the request
	image = open(IMAGE_PATH, "rb").read()
	payload = {"image": image}
	# submit the request
	r = requests.post(KERAS_REST_API_URL, files=payload).json()
	# ensure the request was sucessful
	if r["success"]:
		print("[INFO] thread {} OK".format(n))
	# otherwise, the request failed
	else:
		print("[INFO] thread {} FAILED".format(n))
# loop over the number of threads
for i in range(0, NUM_REQUESTS):
	# start a new thread to call the API
	t = Thread(target=call_predict_endpoint, args=(i,))
	t.daemon = True
	t.start()
	time.sleep(SLEEP_COUNT)
# insert a long sleep so we can wait until the server is finished
# processing the images
time.sleep(300)

Наш скрипт stress_test.py поможет нам протестировать сервер и определить его ограничения. Я всегда рекомендую нагрузочное тестирование вашего сервера REST API глубокого обучения, чтобы вы знали, нужно ли (и, что более важно, когда) добавлять дополнительные графические процессоры, процессоры или оперативную память. Этот скрипт запускает NUM_REQUESTS потоков и POST-запросов к конечной точке /predict. Это зависит от нашего веб-приложения Flask.

Скомпилируйте и установите Redis

Redis — это эффективная база данных в памяти, которая будет действовать как наш брокер очередей/сообщений. Получить и установить Redis очень просто:

$ wget http://download.redis.io/redis-stable.tar.gz
$ tar xvzf redis-stable.tar.gz
$ cd redis-stable
$ make
$ sudo make install

Создайте виртуальную среду Python для глубокого обучения

Установите дополнительные пакеты:

$ workon dl4cv
$ pip install flask
$ pip install gevent
$ pip install requests
$ pip install redis

Установить веб-сервер Apache

Можно использовать и другие веб-серверы, такие как nginx, но, поскольку у меня больше опыта работы с Apache (и, следовательно, я в целом лучше знаком с Apache), я буду использовать Apache для этого примера. Apache можно установить:

$ sudo apt-get install apache2

Если вы создали виртуальную среду с помощью Python 3, вам потребуется установить модули Python 3 WSGI + Apache:

$ sudo apt-get install libapache2-mod-wsgi-py3
$ sudo a2enmod wsgi

Чтобы убедиться, что Apache установлен, откройте браузер и введите IP-адрес веб-сервера. Если вы не видите заставку сервера, убедитесь, что порты 80 и 5000 открыты. В моем случае IP-адрес моего сервера — 54.187.46.215 (у вас будет другой). Вбив это в браузере, я вижу:

img

Симлинк вашего приложения Flask + Deep Learning

По умолчанию Apache обслуживает контент из /var/www/html. Я рекомендую создать символическую ссылку из /var/www/html в веб-приложение Flask. Я загрузил свое приложение для глубокого обучения + Flask в основной каталог в каталоге с именем keras-complete-rest-api:

$ ls ~
keras-complete-rest-api

Я могу сделать символическую ссылку на /var/www/html с помощью:

$ cd /var/www/html/
$ sudo ln -s ~/keras-complete-rest-api keras-complete-rest-api

Обновите конфигурацию Apache, чтобы она указывала на приложение Flask.

Чтобы настроить Apache так, чтобы он указывал на наше приложение Flask, нам нужно отредактировать /etc/apache2/sites-available/000-default.conf. Откройте в своем любимом текстовом редакторе (здесь я буду использовать vi):

$ sudo vi /etc/apache2/sites-available/000-default.conf

Укажите конфигурацию WSGIPythonHome (путь к каталогу bin Python) и WSGIPythonPath (путь к каталогу пакетов сайта Python) в верхней части файла:

WSGIPythonHome /home/ubuntu/.virtualenvs/keras_flask/bin
WSGIPythonPath /home/ubuntu/.virtualenvs/keras_flask/lib/python3.5/site-packages
<VirtualHost *:80>
	...
</VirtualHost>

В Ubuntu 18.04 вам может понадобиться изменить первую строку на:

WSGIPythonHome /home/ubuntu/.virtualenvs/keras_flask

Поскольку в этом примере мы используем виртуальную среду Python (я назвал свой keras_flask ), мы предоставляем виртуальной среде Python пути к каталогам bin и site-packages. Затем в теле после ServerAdmin и DocumentRoot добавьте:

<VirtualHost *:80>
	...
	
	WSGIDaemonProcess keras_rest_api_app threads=10
	WSGIScriptAlias / /var/www/html/keras-complete-rest-api/keras_rest_api_app.wsgi
	
	<Directory /var/www/html/keras-complete-rest-api>
		WSGIProcessGroup keras_rest_api_app
		WSGIApplicationGroup %{GLOBAL}
		Order deny,allow
		Allow from all
	</Directory>
	
	...
</VirtualHost>

Библиотека Symlink CUDA (опционально, только GPU)

Если вы используете GPU для глубокого обучения и хотите воспользоваться преимуществами CUDA (почему бы и вам), к сожалению, Apache не знает о библиотеках CUDA *.so в /usr/local/cuda/lib64.

Я не уверен, что это «самый правильный» способ указать Apache, где находятся эти библиотеки CUDA, но «полностью взломанное» решение состоит в том, чтобы символизировать все файлы из /usr/local/cuda/lib64 в /usr/lib :

$ cd /usr/lib
$ sudo ln -s /usr/local/cuda/lib64/* ./

Перезапустите веб-сервер Apache.

После редактирования файла конфигурации Apache и, при необходимости, символической ссылки на библиотеку глубокого обучения CUDA обязательно перезапустите сервер Apache:

$ sudo service apache2 restart

Протестируйте веб-сервер Apache + конечную точку глубокого обучения

Чтобы проверить правильность настройки Apache для обслуживания приложений Flask + Deep Learning, обновите веб-браузер:

img

Теперь вы должны увидеть текст «Добро пожаловать в PyImageSearch Keras REST API!» в вашем браузере. Как только вы достигнете этого этапа, ваше приложение глубокого обучения Flask должно быть готово. Тем не менее, если у вас возникнут какие-либо проблемы, обязательно обратитесь к следующему разделу…

Совет: если вы столкнулись с проблемами, следите за журналом ошибок Apache.

Я много лет использую Python + веб-фреймворки, такие как Flask и Django, и все еще получаю ошибки при правильной настройке среды. Хотя я бы хотел, чтобы был пуленепробиваемый способ убедиться, что все прошло гладко, правда в том, что на этом пути могут возникнуть некоторые проблемы. Хорошей новостью является то, что WSGI регистрирует события Python (включая сбои) в журнале сервера. В Ubuntu журналы сервера Apache находятся в /var/log/apache2/:

$ ls /var/log/apache2
access.log error.log other_vhosts_access.log

При отладке я часто открываю запущенный терминал:

$ tail -f /var/log/apache2/error.log

... так что я вижу вторую ошибку. Используйте журнал ошибок, чтобы помочь вам запустить Flask на вашем сервере.

Запустите свой сервер модели глубокого обучения

Ваш сервер Apache уже должен быть запущен. Если нет, вы можете запустить его:

$ sudo service apache2 start

Затем вам нужно запустить магазин Redis:

$ redis-server

и запустите сервер модели Keras в отдельном терминале:

$ python run_model_server.py
* Loading model...
...
* Model loaded

Оттуда попробуйте отправить образец изображения в службу API глубокого обучения:

$ curl -X POST -F image=@jemma.png 'http://localhost/predict'
{
  "predictions": [
    {
      "label": "beagle", 
      "probability": 0.9461532831192017
    }, 
    {
      "label": "bluetick", 
      "probability": 0.031958963721990585
    }, 
    {
      "label": "redbone", 
      "probability": 0.0066171870566904545
    }, 
    {
      "label": "Walker_hound", 
      "probability": 0.003387963864952326
    }, 
    {
      "label": "Greater_Swiss_Mountain_dog", 
      "probability": 0.0025766845792531967
    }
  ], 
  "success": true
}

Если все прошло хорошо, вы должны получить отформатированные выходные данные JSON от сервера моделей API глубокого обучения с прогнозами классов и вероятностями.

img

Стресс-тест вашего REST API для глубокого обучения

Конечно, это всего лишь пример. Давайте проведем стресс-тестирование нашего REST API для глубокого обучения. Откройте другой терминал и выполните следующие команды:

$ python stress_test.py 
[INFO] thread 3 OK
[INFO] thread 0 OK
[INFO] thread 1 OK
...
[INFO] thread 497 OK
[INFO] thread 499 OK
[INFO] thread 498 OK

В выводе run_model_server.py вы увидите следующие строки, зарегистрированные в терминале:

* Batch size: (4, 224, 224, 3)
* Batch size: (9, 224, 224, 3)
* Batch size: (9, 224, 224, 3)
* Batch size: (8, 224, 224, 3)
...
* Batch size: (2, 224, 224, 3)
* Batch size: (10, 224, 224, 3)
* Batch size: (7, 224, 224, 3)

Даже при новом запросе каждые 0,05 секунды размер нашего пакета никогда не превышает ~10-12 изображений в пакете. Наш модельный сервер может легко справляться с нагрузкой, не напрягаясь, и может легко масштабироваться сверх этого. Если вы перегружаете сервер (вероятно, размер вашего пакета слишком велик, а вашему графическому процессору не хватает памяти с сообщением об ошибке), вам следует остановить сервер и использовать интерфейс командной строки Redis для очистки очереди:

$ redis-cli
> FLUSHALL

Оттуда вы можете настроить параметры в settings.py и /etc/apache2/sites-available/000-default.conf. Затем вы можете перезапустить сервер.

Рекомендации по развертыванию собственных моделей глубокого обучения в рабочей среде

Один из лучших советов, который я могу дать, — держать ваши данные, особенно сервер Redis, рядом с графическим процессором. Возможно, вы захотите развернуть гигантский сервер Redis с сотнями ГБ ОЗУ для обработки нескольких очередей изображений и обслуживания нескольких машин с графическим процессором. Проблема здесь будет заключаться в задержке ввода-вывода и сетевых издержках.

Предполагая, что изображения 224 x 224 x 3 представлены в виде массивов float32, размер пакета из 32 изображений будет составлять ~ 19 МБ данных. Это означает, что для каждого пакетного запроса с сервера модели Redis потребуется получить 19 МБ данных и отправить их на сервер. При быстром переключении это не имеет большого значения, но вам следует рассмотреть возможность запуска сервера модели и Redis на одном сервере, чтобы данные находились рядом с графическим процессором.

Суммировать

В сегодняшней записи блога мы узнали, как развернуть модель глубокого обучения в рабочей среде с помощью Keras, Redis, Flask и Apache. Большинство инструментов, которые мы здесь используем, взаимозаменяемы. Вы можете заменить TensorFlow или PyTorch на Keras. Вы можете использовать Django вместо Flask. Nginx можно заменить на Apache.

Единственный инструмент, который я бы не рекомендовал отключать, — это Redis. Redis, пожалуй, лучшее решение для хранения данных в оперативной памяти. Если у вас нет особых причин не использовать Redis, я рекомендую использовать Redis для операций с очередями. Наконец, мы провели стресс-тестирование REST API глубокого обучения.

Мы отправили в общей сложности 500 запросов на классификацию изображений на наш сервер с задержкой 0,05 секунды между каждым запросом — наш сервер не был подготовлен (размер пакета CNN никогда не превышал ~ 37%).