Перенос модели машинного обучения в production

Перенос модели машинного обучения в production

Весь код и конфиги из этой статьи доступны в репозитория GitHub.


Pipeline для построения моделей машинного обучения часто заканчивается на этапе оценки качества модели: вы достигли приемлемой точности и на этом все.

Кроме этого, возможно, вы еще построите красивые графики для вашей статьи/блога или же для вашей внутренней документации.
Замечание: конечно от machine learning engineer не всегда требуется доведение модели до production. И даже если это нужно, эта часто задача делегируется системному администратору.


Однако в настоящее время многие исследователи/инженеры несут (возможно моральную) ответственность за построение полного pipeline: от построения моделей до доведения их до production. Университетский проект это, личным эксперимент или же коммерческий продукт, демонстрация его работы является отличным способом заинтересовать широкую аудиторию. Немногие люди будут прилагать дополнительные усилия для исследования модели или продукта, результаты которого достаточно сложно воспроизвести.

В этой статье мы собираемся вместе пройти через этот pipeline. Предполагается, что вы уже создали модель машинного обучения, используя ваш любимый deep learning framework (scikit-learn, Keras, Tensorflow, PyTorch и тд). Теперь вы хотите продемонстрировать его миру, правда только через API.

Мы рассмотрим инфраструктуру основанную на Python фреймворках и на серверах Linux. Она будет включать:
  • Anaconda: для управления установкой пакетов и создания изолированной среды Python 3. 
  • Keras: высокоуровневый API для нейронных сетей, который способен работать поверх TensorFlow, CNTK или Theano
  • Flask: минималистическый python framework для создания RESTful API. Замечание: встроенный сервер Flask не подходит для развертывания в production, так как обслуживает только один запрос по умолчанию. Этот сервер в большей степени предназначен для более простой отладки
  • nginx: стабильный веб-сервер, который реализует такой функционал, как балансировка нагрузки, конфигурация SSL и т. д. 
  • uWSGI: высоко конфигурируемый WSGI сервер(Web Server Gateway Interface), который позволяет нескольким worker'ам одновременно обслуживать несколько запросов. 
  • systemd: система init, используемая в нескольких дистрибутивах Linux для управления системными процессами после загрузки. 
Nginx будет нашим интерфейсом к Интернету, и он будет обрабатывать запросы клиентов. Nginx имеет встроенную поддержку протокола uWSGI, и они обмениваются данными через Unix-сокеты. В свою очередь, сервер uWSGI будет ссылаться на вызываемый объект в нашем приложении Flask напрямую. Таким образом запросы и будут обслуживаться.


Несколько заметок в начале этого руководства:
  • Большинство компонентов, приведенных выше, могут быть легко заменены эквивалентными, практически без изменений в остальных элементах выстроенного pipeline. Например, Keras можно легко заменить PyTorch, Flask можно легко заменить Bottle и тд. 
  • Когда мы говорим о переносе в production мы говорим не о enterprise масштабах огромной компании. Цель состоит в том, чтобы сделать все возможное в рамках одного сервера с большим количеством процессорных ядер и большим количеством оперативной памяти.

Настройка окружения

Для начала нам нужно установить пакеты systemd и nginx:
sudo apt-get install systemd nginx 

Затем мы должны установить Anaconda, следуя инструкциям на официальном сайте, которые состоят в загрузке исполняемого файла, его запуске и добавлении Anaconda в PATH вашей системы. Ниже мы предположим, что Anaconda установлена в домашнем каталоге.

Затем создадим изолированную среду Anaconda из файла environment.yml. Вот как выглядит этот файл (он уже содержит несколько фреймворков, которые мы будем использовать):
name: production_ml_env
channels:
  - conda-forge
dependencies:
- python=3.6
- keras
- flask
- uwsgi
- numpy
- pip
- pip:
  - uwsgitop

Для создания среды мы запускаем следующее:
conda env create --file environment.yml 

Для активировация полученной среду, мы делаем:
source activate production_ml_env 

К настоящему времени у нас есть Keras, flask, uwsgi, uwsgitop и т. д. Итак, мы готовы начать.


Построение веб-приложения на Flask

В рамках этого урока мы не будем глубоко погружаться в то, как создавать свою ML модель. Вместо этого мы адаптируем пример классификации текстовых новостей, используя датасет Reuters newswire, идущий в поставке с Keras. Это код для построения классификатора:
'''Trains and evaluate a simple MLP
on the Reuters newswire topic classification task.
'''
from __future__ import print_function
import os
import numpy as np
import keras
from keras.datasets import reuters
from keras.models import Sequential
from keras.layers import Dense, Dropout, Activation
from keras.preprocessing.text import Tokenizer
from keras.callbacks import ModelCheckpoint

MODEL_DIR = './models'

max_words = 1000
batch_size = 32
epochs = 5

print('Loading data...')
(x_train, y_train), (x_test, y_test) = reuters.load_data(num_words=max_words,
                                                         test_split=0.2)
print(len(x_train), 'train sequences')
print(len(x_test), 'test sequences')

num_classes = np.max(y_train) + 1
print(num_classes, 'classes')

print('Vectorizing sequence data...')
tokenizer = Tokenizer(num_words=max_words)
x_train = tokenizer.sequences_to_matrix(x_train, mode='binary')
x_test = tokenizer.sequences_to_matrix(x_test, mode='binary')
print('x_train shape:', x_train.shape)
print('x_test shape:', x_test.shape)

print('Convert class vector to binary class matrix '
      '(for use with categorical_crossentropy)')
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)
print('y_train shape:', y_train.shape)
print('y_test shape:', y_test.shape)

print('Building model...')
model = Sequential()
model.add(Dense(512, input_shape=(max_words,)))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(num_classes))
model.add(Activation('softmax'))

model.compile(loss='categorical_crossentropy',
              optimizer='adam',
              metrics=['accuracy'])

if not os.path.exists(''):
    os.makedirs(MODEL_DIR)

mcp = ModelCheckpoint(os.path.join(MODEL_DIR, 'reuters_model.hdf5'), monitor="val_acc",
                      save_best_only=True)

history = model.fit(x_train, y_train,
                    batch_size=batch_size,
                    epochs=epochs,
                    verbose=1,
                    validation_split=0.1,
                    callbacks=[mcp])

score = model.evaluate(x_test, y_test,
                       batch_size=batch_size, verbose=1)
print('Test score:', score[0])
print('Test accuracy:', score[1])

Для воспроизведения настроек, которые мы здесь используем, просто запустите следующие команды при обучении модели. Это позволит обучаться без GPU - на CPU:
export CUDA_VISIBLE_DEVICES=-1 
KERAS_BACKEND=theano python build_classifier.py 

Это создаст сериализованный файл  reuters_model.hdf5 из обученной модели в папке models. Теперь мы готовы использовать модель через Flask на порту 4444. В приведенном ниже коде мы предоставляем единственную точку входа для REST запроса: /predict, которая представлена в виде запроса GET, где текст для классификации передается в качестве параметра. Возвращенный JSON имеет форму {"prediction": "N"}, где N - целое число, обозначающее предсказанный класс.
from flask import Flask
from flask import request
from keras.models import load_model
from keras.datasets import reuters
from keras.preprocessing.text import Tokenizer, text_to_word_sequence
from flask import jsonify
import os

MODEL_DIR = './models'

max_words = 1000

app = Flask(__name__)

print("Loading model")
model = load_model(os.path.join(MODEL_DIR, 'reuters_model.hdf5'))
# we need the word index to map words to indices
word_index = reuters.get_word_index()
tokenizer = Tokenizer(num_words=max_words)


def preprocess_text(text):
    word_sequence = text_to_word_sequence(text)
    indices_sequence = [[word_index[word] if word in word_index else 0 for word in word_sequence]]
    x = tokenizer.sequences_to_matrix(indices_sequence, mode='binary')
    return x


@app.route('/predict', methods=['GET'])
def predict():
    try:
        text = request.args.get('text')
        x = preprocess_text(text)
        y = model.predict(x)
        predicted_class = y[0].argmax(axis=-1)
        print(predicted_class)
        return jsonify({'prediction': str(predicted_class)})
    except:
        response = jsonify({'error': 'problem predicting'})
        response.status_code = 400
        return response


if __name__ == "__main__":
    app.run(host='0.0.0.0', port=4444)

Чтобы запустить Flask сервер приложений, мы выполняем:
python app.py 

Вы можете протестировать его с помощью любого REST клиента(например, Postman) или просто перейдя по этому URL-адресу в своем веб-браузере (замените your_server_url URL-адресом вашего сервера):
http://your_server_url:4444/pred?text=this is a news sample text about sports and football in specific 


И вы получите ответ
{ 
  "class": "11" 
} 


Настройка uWSGI сервера

Теперь мы готовы масштабировать наш сервер приложений. uWSGI будет ключевым игроком здесь. Он связывается с нашим Flask приложением, вызывая app в файле app.py. uWSGI включает в себя большое количество функций распараллеливания, которыми мы и воспользуемся. Его конфигурационный файл выглядит следующим образом:
[uwsgi]
# placeholders that you have to change
my_app_folder = /home/harkous/Development/production_ml
my_user = harkous

socket = %(my_app_folder)/production_ml.sock
chdir = %(my_app_folder)
file = app.py
callable = app

# environment variables
env = CUDA_VISIBLE_DEVICES=-1
env = KERAS_BACKEND=theano
env = PYTHONPATH=%(my_app_folder):$PYTHONPATH

master = true
processes = 5
# allows nginx (and all users) to read and write on this socket
chmod-socket = 666
# remove the socket when the process stops
vacuum = true

# loads your application one time per worker
# will very probably consume more memory,
# but will run in a more consistent and clean environment.
lazy-apps = true

uid = %(my_user)
gid = %(my_user)

# uWSGI will kill the process instead of reloading it
die-on-term = true
# socket file for getting stats about the workers
stats = %(my_app_folder)/stats.production_ml.sock

# Scaling the server with the Cheaper subsystem

# set cheaper algorithm to use, if not set default will be used
cheaper-algo = spare
# minimum number of workers to keep at all times
cheaper = 5
# number of workers to spawn at startup
cheaper-initial = 5
# maximum number of workers that can be spawned
workers = 50
# how many workers should be spawned at a time
cheaper-step = 3

Нам нужно изменить параметр my_app_folder, чтобы быть папкой вашего собственного каталога приложений, а параметр my_user - вашим собственным именем пользователя. В зависимости от ваших потребностей и местоположений файлов вам может потребоваться изменить/добавить другие параметры.

Одним из важных разделов в uwsgi.ini является часть, в которой мы используем Cheaper subsystem в uWSGI, которая позволяет нам запускать несколько workers параллельно, чтобы обслуживать несколько параллельных запросов. Это одна из интересных особенностей uWSGI, где динамическое масштабирование вверх и вниз возможно с помощью нескольких параметров. При вышеуказанной конфигурации мы будем иметь как минимум 5 workers в любое время. При увеличении нагрузки проще будет выделять 3 дополнительных worker за раз, пока все запросы не найдут свой worker. Максимальное количество workers выше установлено в 50.

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

Связывание uWSGI и nginx

Если мы начнем работать с uWSGI сейчас (мы это сделаем немного позже), оно позаботится о вызове приложения из файла app.py, и мы получим все возможности масштабирования, которые он предоставляет. Но мы хотим получить запросы REST из Интернета и передать их в приложение Flask через uWSGI. Для этого мы будем настраивать nginx.
Вот простой конфигурационный файл для nginx. Конечно, nginx можно дополнительно использовать для настройки SSL или для статических файлов, но это выходит за рамки этой статьи.
server {
    listen 4444;
    # change this to your server name or IP
    server_name YOUR_SERVER_NAME_OR_IP;

    location / {
        include         uwsgi_params;
        # change this to the location of the uWSGI socket file (set in uwsgi.ini)
        uwsgi_pass      unix:/home/harkous/Development/production_ml/production_ml.sock;
    }
}

Мы размещаем этот файл в /etc/nginx/sites-available/nginx_production_ml (для этого вам понадобится sudo доступ). Затем, чтобы включить эту конфигурацию nginx, мы связываем ее с sites-enabled директорией:
sudo ln -s /etc/nginx/sites-available/nginx_production_ml /etc/nginx/sites-enabled

Затем перезапускаем nginx:
sudo service nginx restart 

Настройка службы systemd

Наконец, мы запустим ранее настроенный сервер uWSGI. Однако, чтобы гарантировать, что наш сервер не умрет навсегда после перезапуска системы или неожиданных сбоев, мы запустим ее как службу systemd. Вот наш конфигурационный файл службы, который мы размещаем в каталоге /etc/systemd/system, используя:
sudo vi /etc/systemd/system/production_ml.service

[Unit]
Description=uWSGI instance to serve production_ml service

[Service]
User=harkous
Group=harkous
WorkingDirectory=/home/harkous/Development/production_ml/
ExecStart=/home/harkous/anaconda3/envs/production_ml_env/bin/uwsgi --ini /home/harkous/Development/production_ml/uwsgi.ini
Restart=on-failure

[Install]
WantedBy=multi-user.target

Затем стартуем сервис с:
sudo systemctl start production_ml.service 

Чтобы разрешить запуск этого сервиса при перезагрузке устройства:
sudo systemctl enable production_ml.service 

На этом этапе наш сервис должно успешно стартовать. В случае обновления каких-либо конфигов, мы должны перезапустить его:
sudo systemctl restart production_ml.service 

Мониторинг сервиса

Чтобы следить за сервисом и видеть нагрузку на одного worker, мы можем использовать uwsgitop. В uwsgi.ini мы уже настроили сокет статистики в нашей папке приложения. Чтобы просмотреть статистику, выполните следующую команду в этой папке:
uwsgitop stats.production_ml.sock 

Вот пример worker'ов в действии, с дополнительными worker'ами, которые уже были созданы. Чтобы имитировать такую большую нагрузку на вашем сервере, вы можете добавить time.sleep(3) в код прогнозирования.


Один из способов отправки параллельных запросов на ваш сервер - использовать curl (не забудьте заменить YOUR_SERVER_NAME_OR_IP на URL вашего сервера или IP-адрес.
#!/usr/bin/env bash
url="http://YOUR_SERVER_NAME_OR_IP:4444/predict?text=this%20is%20a%20news%20sample%20text%20about%20sports,%20and%20football%20in%20specific" # add more URLs here

for i in {0..10}
do
   # run the curl job in the background so we can start another job
   # and disable the progress bar (-s)
   echo "fetching $url"
   curl $url -s &
done
wait #wait for all background jobs to terminate

Чтобы отслеживать журнал самого приложения, мы можем использовать journalctl:
sudo journalctl -u production_ml.service -f 

Ваш вывод должен выглядеть следующим образом:


Финальные замечания

Если вы достигли этого этапа и ваша приложение успешно запущено, эта статья достигла свою цель. Некоторые дополнения заслуживают упоминания на данном этапе:
  • Чтобы эта статья была достаточно общей, мы использовали режим lazy-apps в uwsgi, который загружает приложение один раз для каждого worker. Согласно документации, для загрузки этого потребуется время O(n) (где n - количество workers). Это также, возможно, потребует большего объема памяти, но приводет к чистому окружению для каждого worker'а. По умолчанию uWSGI загружает все приложение по-разному. Он начинается с одного процесса; то он несколько раз разворачивается для дополнительных работников. Это приводит к увеличению экономии памяти. Однако это не очень хорошо работает со всеми структурами ML. Например, бэкэнд TensorFlow в Keras не работает без режима ленивых приложений (например, проверьте это, это и это). Лучше всего было бы попробовать сначала без lazy-apps = true и перейти на него, если вы столкнетесь с подобными проблемами. 
  • Параметры приложения Flask: поскольку uWSGI вызывает app как исполняемое, параметры самого приложения не должны передаваться через командную строку. Вам лучше использовать файл конфигурации с подобными configparser для чтения таких параметров. 
  • Масштабирование на нескольких серверах. В приведенном выше руководстве не обсуждается случай с несколькими серверами. К счастью, это может быть достигнуто без значительных изменений в нашей настройке. Используя функцию балансировки нагрузки в nginx, вы можете настроить несколько машин, каждый с настройкой uWSGI, описанной выше. Затем вы можете настроить nginx для маршрутизации запросов на разные серверы. nginx поставляется с несколькими методами для распределения нагрузки, начиная от простого циклического масштабирования и заканчивая учетом количества подключений или средней задержки. 
  • Выбор порта: в приведенном выше руководстве используется порт 4444 для иллюстрации. Вы можете изменить этот порт для ваших условий. Убедитесь, что вы открыли эти порты в брандмауэре или попросите администраторов вашего учреждения сделать это. 
  • Привилегии сокета: мы предоставляем доступ для всех пользователей. Не стесняйтесь также настраивать привелегии для своих целей и запускать сервер с другими группами привилегий. Но убедитесь, что ваши nginx и uWSGI могут по-прежнему успешно контактировать друг с другом после ваших изменений. 

Ссылки на использованные материалы

  • https://hackernoon.com/a-guide-to-scaling-machine-learning-models-in-production-aa8831163846



















Комментарии

Популярные сообщения из этого блога

Подготовка данных для алгоритмов машинного обучения

Выбор метрики в машинном обучении

Встречайте церковь «Путь будущего»