Машинное обучение в Akash DeCloud (часть 3/3): развертывание приложений машинного обучения в децентрализованном облаке
В Часть 1 моей серии из трех статей, посвященных машинному обучению в Akash Network, мы развернули в Akash полную среду Jupyter с ядром Python и установленным TensorFlow и использовали ее. обучить сверточную нейронную сеть (CNN) распознаванию рукописных цифр на наборе данных MNIST. В Часть 2 мы связали модель с TensorFlow Serving, чтобы предоставить REST API для вывода модели, и показали, что мы можем разместить это и на Akash.
В этой заключительной части мы расширим наш проект и развернем веб-приложение для Akash, которое использует наш REST API для классификации цифр, нарисованных пользователем. Вот краткий обзор:
Есть много способов добиться этого, и в этом руководстве я покажу вам одно распространенное решение производственного уровня — разработку внешнего интерфейса с помощью React, написание API для обработки данных и запросов к нашей модели, обслуживаемой TensorFlow Serving, с использованием Flask, используйте Gunicorn в качестве интерфейса шлюза веб-сервера (WSGI) и используйте Nginx как для обслуживания внешнего интерфейса, так и для обратного проксирования запросов между интерфейсом и серверной частью модели.
Предпосылки
В этом руководстве показано, как создать два образа Docker, доступные здесь и здесь под именем пользователя wlouie1
. Образ Docker для обслуживания моделей TensorFlow из Часть 2 находится здесь. Однако если вы хотите создать их самостоятельно, вам потребуется установить Docker и иметь учетную запись на Docker Hub.
Обратите внимание, что для самостоятельного развертывания в Akash вам потребуется выполнить первоначальную настройку Akash, описанную в разделе Предварительные требования моего Руководства по развертыванию приложений в Akash DeCloud.
Код для всех трех частей этой серии находится в этом репозитории GitHub, и мы сосредоточимся на файлах в каталогах flask
и webapp
. Если вы еще этого не сделали, клонируйте репозиторий, запустив:
git clone https://github.com/wlouie1/mnist-app.git cd mnist-app/
Архитектурный обзор
На приведенной ниже диаграмме описана архитектура развертывания:
Когда клиент впервые посещает и запрашивает страницу, Nginx обслуживает интерфейсные ресурсы React. Когда клиент рисует цифру и нажимает кнопку «Предсказать», обратный прокси-сервер Nginx отправляет запрос на предсказание с полезной нагрузкой изображения в Gunicorn, который передает его во Flask. Flask обрабатывает изображение и отправляет запрос на прогнозирование в TensorFlow Serve. Результирующий прогнозирующий ответ в конечном итоге возвращается к клиенту и обрабатывается React для обновления представления.
Мы уже создали образ Docker для обслуживания моделей TensorFlow в Часть 2 (также доступно здесь). Мы создадим два дополнительных образа Docker, один из которых инкапсулирует серверы Flask и Gunicorn, а другой инкапсулирует веб-сервер Nginx и интерфейсные ресурсы React. После того, как образы Docker готовы, развертывание всего этого в Akash становится тривиальной задачей!
Приложение React и образ веб-сервера Nginx
Реагировать на внешний интерфейс
Таким образом, интерфейс состоит из нескольких компонентов React. Детали этих компонентов выходят за рамки этого руководства, но я хочу указать на этот фрагмент кода в компоненте PredictionDisplay под webapp/src/components/PredictionDisplay/PredictionDisplay.js
:
const handlePredict = (e) => { // Convert canvas to image data uri const canvas = document.querySelector('.digit-canvas'); const dataURL = canvas.toDataURL(); // Use the REST API exposed by our Flask server to ask for // predictions. The Flask server preprocesses the given // image, and sends it to the model hosted by the // Tensorflow Server. The Tensorflow Server replies with // predictions, which makes it back here to the front end. const params = { method: "POST", cache: "no-cache", headers: new Headers({ "content-type": "application/json" }), body: JSON.stringify(dataURL) }; // Our Flask server sits behind Gunicorn, which is reverse // proxied by a proper web server Nginx. // The reverse proxy also relieves CORS issues. const hostname = window.location.hostname; const url = `http://${hostname}/mnist/predict/`; fetch(url, params) .then((res) => res.json()) .then((result) => { const preds = result.predictions[0]; setPredictions(preds); }); };
Здесь все, что пользователь рисует на холсте, преобразуется в URI данных, который представляет собой кодировку изображения с основанием 64. Запрос POST, содержащий эту закодированную строку в качестве полезной нагрузки, отправляется в Nginx с помощью Javascript fetch, который направляется на серверы Gunicorn/Flask/TensorFlow. Внешний интерфейс обновляет представление с ответом прогноза, когда он становится доступным.
Конфигурация Nginx
Nginx — это наш веб-сервер, который обрабатывает как запросы для внешнего интерфейса React, так и для вывода модели путем обратного проксирования на наши серверы Gunicorn/Flask/TensorFlow и действует как балансировщик нагрузки для приложения. В каталоге webapp
вы должны увидеть файлы конфигурации Nginx. В общем файле конфигурации nginx.conf
:
# The number of worker processes; recommended value is the number of # cores that are being used by your server worker_processes 1; # Parameters that affect connection processing. events { # Maximum number of simultaneous connections that can be opened by a worker process worker_connections 1024; } # Parameters for how Nginx should handle HTTP web traffic http { # Include the file defining file types to be supported include /etc/nginx/mime.types; # Timeout for keep-alive connections with the client keepalive_timeout 90; # Include additional parameters include /etc/nginx/conf.d/*.conf; }
мы указываем 1 рабочий процесс, максимум 1024 соединения на рабочий процесс и включение дополнительного файла конфигурации для конкретного приложения, project.conf
:
server { # Port the app should run at listen 80; # Location of app files to serve root /app; # If the request leads to app files, serve them, # otherwise proxy the request to Gunicorn/Flask server location / { try_files $uri $uri/ @proxy; } # Gunicorn/Flask server reverse proxy location @proxy { proxy_pass http://flaskserver:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }
Здесь мы указываем расположение наших интерфейсных файлов React и то, что веб-сервер должен прослушивать порт 80. Мы также указываем, что если запрос на интерфейсные ресурсы React действителен, обслуживать их; в противном случае направьте запрос на наш сервер Gunicorn/Flask. Обратите внимание, что мы можем добиться этого с помощью прокси-сервера, переходящего на http://flaskserver:5000, потому что Gunicorn предоставляет API Flask через порт 5000, а наш файл Docker Compose (и SDL развертывания Akash) в последующих разделах использует псевдоним нашего контейнера Gunicorn/Flask Docker. адрес как flaskserver
.
Контейнеризация с помощью Docker
Внутри каталога webapp
вы должны увидеть файл с именем Dockerfile
со следующим содержимым:
# Multi-stage Build # 1) Build React app # 2) Nginx for serving the app, and acting as reverse proxy for Gunicorn/Flask # Build React app (stage name "builder") FROM node:14.15.1-alpine AS builder WORKDIR /app ENV PATH ./node_modules/.bin:$PATH COPY package.json package-lock.json ./ RUN npm install COPY . ./ RUN npm run build # Nginx for serving and reverse proxy FROM nginx:alpine WORKDIR /app # Copy static assets from builder stage COPY --from=builder /app/build . # Copy and replace with our configuration files RUN rm /etc/nginx/nginx.conf COPY nginx.conf /etc/nginx/ RUN rm /etc/nginx/conf.d/default.conf COPY project.conf /etc/nginx/conf.d/ # Run nginx with global directives and daemon off ENTRYPOINT ["nginx", "-g", "daemon off;"]
Как определено, наш образ Docker выполняет многоэтапную сборку, которая сначала создает приложение React, а затем запускает сервер Nginx.
Убедитесь, что ваш рабочий каталог — это каталог webapp
, и создайте образ Docker (мы назовем его mnist-app
и пометим как 1.0
) с помощью:
docker build -t mnist-app
:1.0 .
Отправка образа в реестр Docker
Если у вас есть учетная запись Docker Hub с именем пользователя USERNAME
, создайте репозиторий с именем mnist-app
, а затем пометьте и отправьте образ с помощью:
docker tagmnist-app
:1.0 USERNAME/mnist-app
:1.0 docker push USERNAME/mnist-app
:1.0
Колба и изображение Gunicorn
Колба
Flask — это популярная среда Python для создания веб-приложений и API. Мы будем использовать Flask для предоставления прогнозируемого REST API. Если вы заглянете внутрь каталога flask
, то увидите определение нашего Flask REST API в app.py
:
import requests import json from flask import Flask, jsonify, request import base64 from io import BytesIO from PIL import Image, ImageOps import numpy as np server = Flask(__name__) @server.route('/mnist/predict/', methods=['POST']) def mnist_predict(): data_url = request.get_json() content = data_url.split(';')[1] image_encoded = content.split(',')[1].encode('utf-8') # Transform image into something our model expects: # (28, 28, 1) dimensional array, grayscale values # between 0 and 1. image_bytes = BytesIO(base64.b64decode(image_encoded)) im = Image.open(image_bytes) im = ImageOps.grayscale(im) im = im.resize((28, 28)) im_array = np.array(im).reshape((28, 28, 1)) / 255. # Make POST request to Tensorflow Serving to get prediction. # Wrap the prediction as response of this API. r = requests.post( 'http://tfserver:8501/v1/models/mnist:predict', json={ 'instances': [im_array.tolist()] } ) return jsonify(r.json())
Получив URI данных, представляющий изображение, нарисованное пользователем во внешнем интерфейсе, Flask считывает его как изображение и обрабатывает в формате, ожидаемом нашей моделью CNN: одномерная матрица 28 x 28 x со значениями оттенков серого между 0 и 1. Затем Flask отправляет запрос POST на конечную точку REST TensorFlow Serving с обработанной полезной нагрузкой. Ответ-прогноз в конечном итоге возвращается во внешний интерфейс для визуальных обновлений.
Гуникорн
Наш Flask API выполняет код Python, поэтому ему требуется WSGI для обработки соединений с веб-сервером, таким как Nginx. Хотя Flask имеет встроенный WSGI, он небезопасен и неэффективен, поэтому его не следует использовать в производственной среде. Вместо этого мы будем использовать Gunicorn как правильный WSGI.
Как вы можете видеть в wsgi.py
в каталоге flask
, настроить Gunicorn очень просто. Нам просто нужен скрипт, который запускает наш сервер Flask для прослушивания порта 5000, и затем мы можем направить Gunicorn к этому скрипту в нашем Dockerfile (показанном в следующем разделе):
from app import server if __name__ == '__main__': server.run(host='0.0.0.0', port=5000)
Контейнеризация с помощью Docker
Внутри каталога flask
вы должны увидеть файл с именем Dockerfile
со следующим содержимым:
FROM python:3.8-buster WORKDIR /flask COPY requirements.txt requirements.txt RUN pip install -r requirements.txt EXPOSE 5000 COPY . . CMD ["gunicorn", "-w", "1", "-b", "0.0.0.0:5000", "wsgi:server"]
Как определено, наш образ Docker начинается с базового образа Python, устанавливает соответствующие пакеты в соответствии с requirements.txt
и при запуске инициализирует сервер Gunicorn с 1 рабочим потоком на порту 5000, который запускает наш сервер Flask (используя server
из wsgi.py
).
Убедитесь, что ваш рабочий каталог — это каталог flask
, и создайте образ Docker (мы назовем его mnist-flask-serve
) с помощью:
docker build -t mnist-flask-serve
:1.0 .
Отправка образа в реестр Docker
Если у вас есть учетная запись Docker Hub с именем пользователя USERNAME
, создайте репозиторий с именем mnist-flask-serve
, а затем пометьте и отправьте образ с помощью:
docker tagmnist-flask-serve
:1.0 USERNAME/mnist-flask-serve
:1.0 docker push USERNAME/mnist-flask-serve
:1.0
Локальное тестирование с использованием Docker Compose
Перед развертыванием в Akash давайте протестируем приложение, собрав вместе все соответствующие образы Docker, которые мы создали с помощью Docker Compose. Внутри корневого каталога репозитория GitHub вы должны увидеть файл Docker Compose с именем docker-compose.yml
со следующим содержимым:
version: '3.8' services: tfserver: image: wlouie1/mnist-tf-serve:1.0 ports: - '8501:8501' flaskserver: image: wlouie1/mnist-flask-serve:1.0 ports: - '5000:5000' depends_on: - tfserver web: image: wlouie1/mnist-app:1.0 ports: - '80:80' depends_on: - flaskserver
Если вы следовали приведенным выше разделам и загрузили свои собственные образы Docker в Docker Hub, замените wlouie1
на свой USERNAME
, чтобы использовать их. Как видите, файл Docker Compose указан в соответствии с нашей архитектурной схемой выше.
Для проверки убедитесь, что ваш рабочий каталог является корневым каталогом репозитория (каталог с файлом docker-compose.yml
), и запустите контейнер с помощью docker-compose:
docker-compose up --build
Перейдите к http://localhost:80
, и вы должны увидеть функциональное приложение для распознавания рукописных цифр!
Развертывание в Акаше
Полные инструкции по развертыванию в Akash см. в моем предыдущем Руководстве по развертыванию приложений в Akash DeCloud. По сути, вам просто нужно создать следующий файл SDL (назовите его deploy.yml
), заменить USERNAME
своим именем пользователя Docker Hub и выполнить пару команд, как описано в руководстве по развертыванию. Обратите внимание, как легко написать SDL после написания файла Docker Compose!
--- version: "2.0" services: tfserver: image: USERNAME/mnist-tf-serve:1.0 expose: - port: 8501 flaskserver: image: USERNAME/mnist-flask-serve:1.0 expose: - port: 5000 depends-on: - tfserver web: image: USERNAME/mnist-app:1.0 expose: - port: 80 as: 80 to: - global: true depends-on: - flaskserver profiles: compute: tfserver: resources: cpu: units: 0.2 memory: size: 512Mi storage: size: 512Mi flaskserver: resources: cpu: units: 0.2 memory: size: 256Mi storage: size: 1Gi web: resources: cpu: units: 0.1 memory: size: 256Mi storage: size: 512Mi placement: westcoast: attributes: organization: ovrclk.com signedBy: anyOf: - "akash1vz375dkt0c60annyp6mkzeejfq0qpyevhseu05" pricing: tfserver: denom: uakt amount: 1000 flaskserver: denom: uakt amount: 1000 web: denom: uakt amount: 1000 deployment: tfserver: westcoast: profile: tfserver count: 1 flaskserver: westcoast: profile: flaskserver count: 1 web: westcoast: profile: web count: 1
Для меня детали доступа к аренде:
{ "services": { "flaskserver": { "name": "flaskserver", "available": 1, "total": 1, "uris": null, "observed-generation": 0, "replicas": 0, "updated-replicas": 0, "ready-replicas": 0, "available-replicas": 0 }, "tfserver": { "name": "tfserver", "available": 1, "total": 1, "uris": null, "observed-generation": 0, "replicas": 0, "updated-replicas": 0, "ready-replicas": 0, "available-replicas": 0 }, "web": { "name": "web", "available": 1, "total": 1, "uris": [ "95hp0ohflteo177k9pqkhcsv50.provider5.akashdev.net" ], "observed-generation": 0, "replicas": 0, "updated-replicas": 0, "ready-replicas": 0, "available-replicas": 0 } }, "forwarded-ports": {} }
что означает, что я могу получить доступ к приложению по адресу http://95hp0ohflteo177k9pqkhcsv50.provider5.akashdev.net. Ваша ссылка под uris
вашей информации об аренде, вероятно, отличается от моей.
Перейдите по своей ссылке и получайте удовольствие, рисуя цифры!
Вывод
На этом завершается моя серия из трех статей о развертывании примера проекта машинного обучения, от обучения модели до размещения веб-приложения, использующего модель, и все это на Akash. Проект Akash все еще довольно молод, поэтому в будущем можно ожидать больше возможностей и дальнейшего упрощения рабочих процессов. По мере развития платформы Akash с большим количеством функций и возможностей я, вероятно, буду экспериментировать и писать о них, так что следите за новостями!