Машинное обучение в 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 tag mnist-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 tag mnist-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 с большим количеством функций и возможностей я, вероятно, буду экспериментировать и писать о них, так что следите за новостями!