Руководство по развертыванию моделей машинного обучения в рабочей среде в качестве API с помощью Flask

Автор оригинала: https://www.analyticsvidhya.com/blog/author/guest-blog/
  • Перевод
Друзья, в конце марта мы запускаем новый поток по курсу «Data Scientist». И прямо сейчас начинаем делиться с вами полезным материалом по курсу.

Введение

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

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



Именно поэтому я сейчас пишу это руководство. Мне хочется, чтобы вы столкнулись с той проблемой, с которой столкнулся я в свое время, но смогли достаточно быстро ее решить. К концу этой статьи я покажу вам как реализовать модель машинного обучения используя фреймворк Flask на Python.

Содержание

  1. Варианты реализации моделей машинного обучения.
  2. Что такое API?
  3. Установка среды для Python и базовые сведения о Flask.
  4. Создание модели машинного обучения.
  5. Сохранения модели машинного обучения: Сериализация и Десериализация.
  6. Создание API с использованием Flask.

Варианты реализации моделей машинного обучения.

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

К примеру, большинство ML специалистов используют R или Python для своих научных исканий. Однако потребителями этих моделей будут инженеры-программисты, которые используют совсем другой стек технологий. Есть два варианта, которыми можно решить эту проблему:

Вариант 1: Переписать весь код на том языке, с которым работают инженеры-разработчики. Звучит в какой-то степени логично, однако необходимо большое количество сил и времени, чтобы тиражировать разработанные модели. По итогу это получается просто тратой времени. Большинство языков, как например JavaScript, не имеют удобных библиотек для работы с ML. Поэтому будет достаточно рациональным решением этот вариант не использовать.

Вариант 2: Использовать API. Сетевые API решили проблему работы с приложениями на разных языках. Если фронт-энд разработчику необходимо использовать вашу модель машинного обучения, чтобы на ее основе создать веб-приложение, им нужно всего лишь получить URL конечного сервера, обсуживающего API.

Что такое API?

Если говорить простыми словами, то API (Application Programming Interface) – это своеобразный договор между двумя программами, говорящий, что если пользовательская программа предоставляет входные данные в определенном формате, то программа разработчика (API) пропускает их через себя и выдает необходимые пользователю выходные данные.

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


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

Например, одним из таких поставщиков API является Google со своим Google Vision API.

Все, что необходимо сделать разработчику, это просто вызвать REST (Representational State Transfer) API с помощью SDK, предоставляемой Google. Посмотрите, что можно сделать используя Google Vision API.

Звучит замечательно, не так ли? В этой статье мы разберемся, как создать свое собственное API с использованием Flask, фреймворка на Python.

Внимание: Flask — это не единственный сетевой фреймворк для этих целей. Есть еще Django, Falcon, Hug и множество других, о которых в данной статье не говорится. Например, для R есть пакет, который называется plumber

Установка среды для Python и базовые сведения о Flask.

1) Создание виртуальной среды с использованием Anaconda. Если вам необходимо создать свою виртуальную среду для Python и сохранить необходимое состояние зависимостей, то Anaconda предлагает для этого хорошие решения. Дальше будет вестись работа с командной строкой.

  • Здесь вы найдете установщик miniconda для Python;
  • wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh
  • bash Miniconda3-latest-Linux-x86_64.sh
  • Следуйте последовательности вопросов.
  • source .bashrc
  • Если вы введете: conda, то сможете увидеть список доступных команд и помощь.
  • Чтобы создать новую среду, введите: conda create --name <environment-name> python=3.6
  • Следуйте шагам, которые вам будет предложено сделать и в конце введите: source activate <environment-name>
  • Установите необходимые пакеты Python. Самые важные это flask и gunicorn.

2) Мы попробуем создать свое простое «Hello world» приложение на Flask с использованием gunicorn.

  • Откройте свой любимый текстовый редактор и создайте в папке файл hello-world.py
  • Напишите следующий код:

"""Filename: hello-world.py
  """

  from flask import Flask

  app = Flask(__name__)

  @app.route('/users/<string:username>')
  def hello_world(username=None):

      return("Hello {}!".format(username))

  • Сохраните файл и вернитесь к терминалу.
  • Для запуска API выполните в терминале: gunicorn --bind 0.0.0.0:8000 hello-world:app
  • Если получите следующее, то вы на правильном пути:



  • В браузере введите следующее: https://localhost:8000/users/any-name



Ура! Вы написали свою первую программу на Flask! Поскольку у вас уже есть некоторый опыт в выполнении этих простых шагов, мы сможем создать сетевые конечные точки, которые могут быть доступны локально.

Используя Flask мы можем оборачивать наши модели и использовать их в качестве Web API. Если мы хотим создавать более сложные сетевые приложения (например, на JavaScript), то нам нужно добавить некоторые изменения.

Создание модели машинного обучения.

  • Для начала займемся соревнованием по машинному обучению Loan Prediction Competition. Основная цель состоит в том, чтобы настроить предобработку пайплайна (pre-processing pipeline) и создать ML модели для облегчения задачи прогнозирования во время развертывания.

import os 
import json
import numpy as np
import pandas as pd
from sklearn.externals import joblib
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.ensemble import RandomForestClassifier

from sklearn.pipeline import make_pipeline

import warnings
warnings.filterwarnings("ignore")

  • Сохраняем датасет в папке:

!ls /home/pratos/Side-Project/av_articles/flask_api/data/

test.csv  training.csv

data = pd.read_csv('../data/training.csv')

list(data.columns)

['Loan_ID',
 'Gender',
 'Married',
 'Dependents',
 'Education',
 'Self_Employed',
 'ApplicantIncome',
 'CoapplicantIncome',
 'LoanAmount',
 'Loan_Amount_Term',
 'Credit_History',
 'Property_Area',
 'Loan_Status']

data.shape


(614, 13)
ul>
Находим null/Nan значения в столбцах:

for _ in data.columns:
    print("The number of null values in:{} == {}".format(_, data[_].isnull().sum()))


The number of null values in:Loan_ID == 0
The number of null values in:Gender == 13
The number of null values in:Married == 3
The number of null values in:Dependents == 15
The number of null values in:Education == 0
The number of null values in:Self_Employed == 32
The number of null values in:ApplicantIncome == 0
The number of null values in:CoapplicantIncome == 0
The number of null values in:LoanAmount == 22
The number of null values in:Loan_Amount_Term == 14
The number of null values in:Credit_History == 50
The number of null values in:Property_Area == 0
The number of null values in:Loan_Status == 0

  • Следующим шагом создаем датасеты для обучения и тестирования:

red_var = ['Gender','Married','Dependents','Education','Self_Employed','ApplicantIncome','CoapplicantIncome',\
            'LoanAmount','Loan_Amount_Term','Credit_History','Property_Area']

X_train, X_test, y_train, y_test = train_test_split(data[pred_var], data['Loan_Status'], \
                                                    test_size=0.25, random_state=42)

  • Чтобы убедиться, что все шаги предобработки (pre-processing) выполнены верно даже после того как мы провели эксперименты, и мы не упустили ничего во время прогнозирования, мы создадим собственный оценщик на Scikit-learn для предобработки (pre-processing Scikit-learn estimator).

Чтобы понять как мы его создали, прочитайте следующее.

from sklearn.base import BaseEstimator, TransformerMixin

class PreProcessing(BaseEstimator, TransformerMixin):
    """Custom Pre-Processing estimator for our use-case
    """

    def __init__(self):
        pass

    def transform(self, df):
        """Regular transform() that is a help for training, validation & testing datasets
           (NOTE: The operations performed here are the ones that we did prior to this cell)
        """
        pred_var = ['Gender','Married','Dependents','Education','Self_Employed','ApplicantIncome',\
                    'CoapplicantIncome','LoanAmount','Loan_Amount_Term','Credit_History','Property_Area']
        
        df = df[pred_var]
        
        df['Dependents'] = df['Dependents'].fillna(0)
        df['Self_Employed'] = df['Self_Employed'].fillna('No')
        df['Loan_Amount_Term'] = df['Loan_Amount_Term'].fillna(self.term_mean_)
        df['Credit_History'] = df['Credit_History'].fillna(1)
        df['Married'] = df['Married'].fillna('No')
        df['Gender'] = df['Gender'].fillna('Male')
        df['LoanAmount'] = df['LoanAmount'].fillna(self.amt_mean_)
        
        gender_values = {'Female' : 0, 'Male' : 1} 
        married_values = {'No' : 0, 'Yes' : 1}
        education_values = {'Graduate' : 0, 'Not Graduate' : 1}
        employed_values = {'No' : 0, 'Yes' : 1}
        property_values = {'Rural' : 0, 'Urban' : 1, 'Semiurban' : 2}
        dependent_values = {'3+': 3, '0': 0, '2': 2, '1': 1}
        df.replace({'Gender': gender_values, 'Married': married_values, 'Education': education_values, \
                    'Self_Employed': employed_values, 'Property_Area': property_values, \
                    'Dependents': dependent_values}, inplace=True)
        
        return df.as_matrix()

    def fit(self, df, y=None, **fit_params):
        """Fitting the Training dataset & calculating the required values from train
           e.g: We will need the mean of X_train['Loan_Amount_Term'] that will be used in
                transformation of X_test
        """
        
        self.term_mean_ = df['Loan_Amount_Term'].mean()
        self.amt_mean_ = df['LoanAmount'].mean()
        return self 

  • Конвертируем y_train и y_test в np.array:

y_train = y_train.replace({'Y':1, 'N':0}).as_matrix()
y_test = y_test.replace({'Y':1, 'N':0}).as_matrix()

Создадим пайплайн чтобы убедиться, что все шаги предобработки, которые мы делаем это работа оценщика scikit-learn.

pipe = make_pipeline(PreProcessing(),
                    RandomForestClassifier())

pipe

Pipeline(memory=None,
     steps=[('preprocessing', PreProcessing()), ('randomforestclassifier', RandomForestClassifier(bootstrap=True, class_weight=None, criterion='gini',
            max_depth=None, max_features='auto', max_leaf_nodes=None,
            min_impurity_decrease=0.0, min_impurity_split=None,
            min_samples_leaf=1, min_samples_split=2,
            min_weight_fraction_leaf=0.0, n_estimators=10, n_jobs=1,
            oob_score=False, random_state=None, verbose=0,
            warm_start=False))])

Для поиска подходящих гипер-параметров (степень для полиномиальных объектов и альфа для ребра) сделаем поиск по сетке (Grid Search):

  • Определяем param_grid:

param_grid = {"randomforestclassifier__n_estimators" : [10, 20, 30],
             "randomforestclassifier__max_depth" : [None, 6, 8, 10],
             "randomforestclassifier__max_leaf_nodes": [None, 5, 10, 20], 
             "randomforestclassifier__min_impurity_split": [0.1, 0.2, 0.3]}

  • Запускам поиск по сетке:

grid = GridSearchCV(pipe, param_grid=param_grid, cv=3)

  • Подгоняем обучающие данные для оценщика пайплайна (pipeline estimator):

grid.fit(X_train, y_train)

GridSearchCV(cv=3, error_score='raise',
       estimator=Pipeline(memory=None,
     steps=[('preprocessing', PreProcessing()), ('randomforestclassifier', RandomForestClassifier(bootstrap=True, class_weight=None, criterion='gini',
            max_depth=None, max_features='auto', max_leaf_nodes=None,
            min_impurity_decrease=0.0, min_impu..._jobs=1,
            oob_score=False, random_state=None, verbose=0,
            warm_start=False))]),
       fit_params=None, iid=True, n_jobs=1,
       param_grid={'randomforestclassifier__n_estimators': [10, 20, 30], 'randomforestclassifier__max_leaf_nodes': [None, 5, 10, 20], 'randomforestclassifier__min_impurity_split': [0.1, 0.2, 0.3], 'randomforestclassifier__max_depth': [None, 6, 8, 10]},
       pre_dispatch='2*n_jobs', refit=True, return_train_score=True,
       scoring=None, verbose=0)

  • Посмотрим, какой параметр выбрал поиск по сетке:

print("Best parameters: {}".format(grid.best_params_))

Best parameters: {'randomforestclassifier__n_estimators': 30, 'randomforestclassifier__max_leaf_nodes': 20, 'randomforestclassifier__min_impurity_split': 0.2, 'randomforestclassifier__max_depth': 8}

  • Подсчитаем:

print("Validation set score: {:.2f}".format(grid.score(X_test, y_test)))

Validation set score: 0.79

  • Загрузим тестовый набор:

test_df = pd.read_csv('../data/test.csv', encoding="utf-8-sig")
test_df = test_df.head()

grid.predict(test_df)

array([1, 1, 1, 1, 1])

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

Сохранение модели машинного обучения: Сериализация и Десериализация.

«В computer science, в контексте хранение данных, сериализация – это процесс перевода структур данных или состояний объекта в хранимый формат (например, файл или буфер памяти) и воссоздания позже в этой же или другой среде компьютера.»


В Python консервация (pickling) – это стандартный способ хранение объектов и позже получения их в исходном состоянии. Чтобы звучало понятнее, приведу простой пример:

list_to_pickle = [1, 'here', 123, 'walker']

#Pickling the list
import pickle

list_pickle = pickle.dumps(list_to_pickle)

list_pickle

b'\x80\x03]q\x00(K\x01X\x04\x00\x00\x00hereq\x01K{X\x06\x00\x00\x00walkerq\x02e.'

Затем мы снова выгрузим законсервированный объект:

loaded_pickle = pickle.loads(list_pickle)

loaded_pickle

[1, 'here', 123, 'walker']

Мы можем сохранять законсервированные объекты в файл и использовать их. Этот метод похож на создание .rda файлов, как в программировании на R, например.

Заметка: Некоторым может не понравиться такой способ консервации для сериализации. Альтернативой может стать h5py.

У нас имеется пользовательский класс (Class), который нам нужно импортировать пока идет обучение (training), поэтому мы будем использовать модуль dill для упаковки оценщика класса (Class) с объектом сетки.

Желательно создать отдельный файл training.py, содержащий весь код для обучния модели. (Пример можно посмотреть здесь).

  • Устанавливаем dill

!pip install dill

Requirement already satisfied: dill in /home/pratos/miniconda3/envs/ordermanagement/lib/python3.5/site-packages

import dill as pickle
filename = 'model_v1.pk'

with open('../flask_api/models/'+filename, 'wb') as file:
	pickle.dump(grid, file)

Модель сохранится в выбранной выше директории. Как только модель законсервирована, ее можно обернуть в Flask wrapper. Однако перед этим нужно убедиться, что законсервированный файл работает. Давайте загрузим его обратно и сделаем прогноз:

with open('../flask_api/models/'+filename ,'rb') as f:
    loaded_model = pickle.load(f)

loaded_model.predict(test_df)

array([1, 1, 1, 1, 1])

Поскольку мы выполнили шаги предобрабоки для того, чтобы вновь поступившие данные были частью пайплайна, нам просто нужно запустить predict(). Используя библиотеку scikit-learn достаточно просто работать с пайплайнами. Оценщики и пайплайны берегут ваше время и нервы, даже если первоначальная реализация кажется дикой.

Создание API с использованием Flask

Давайте сохраним структуру папок максимально простой:



В создании wrapper функции apicall() есть три важные части:

  • Получение request данных (для которых будет делаться прогноз);
  • Загрузка законсервированного оценщика;
  • Перевод наших прогнозов в формат JSON и получение ответа status code: 200;

HTTP сообщения создаются из заголовка и тела. В общем случае основное содержимое тела передается в формате JSON. Мы будем отправлять (POST url-endpoint/) поступающие данные как пакет для получения прогнозов.

Заметка: Вы можете отправлять обычный текст, XML, cvs или картинку напрямую для взаимозаменяемости формата, однако предпочтительнее в нашем случае использовать именно JSON.

"""Filename: server.py
"""

import os
import pandas as pd
from sklearn.externals import joblib
from flask import Flask, jsonify, request

app = Flask(__name__)

@app.route('/predict', methods=['POST'])
def apicall():
    """API Call

    Pandas dataframe (sent as a payload) from API Call
    """
    try:
        test_json = request.get_json()
        test = pd.read_json(test_json, orient='records')

        #To resolve the issue of TypeError: Cannot compare types 'ndarray(dtype=int64)' and 'str'
        test['Dependents'] = [str(x) for x in list(test['Dependents'])]

        #Getting the Loan_IDs separated out
        loan_ids = test['Loan_ID']

    except Exception as e:
        raise e

    clf = 'model_v1.pk'

    if test.empty:
        return(bad_request())
    else:
        #Load the saved model
        print("Loading the model...")
        loaded_model = None
        with open('./models/'+clf,'rb') as f:
            loaded_model = pickle.load(f)

        print("The model has been loaded...doing predictions now...")
        predictions = loaded_model.predict(test)

        """Add the predictions as Series to a new pandas dataframe
                                OR
           Depending on the use-case, the entire test data appended with the new files
        """
        prediction_series = list(pd.Series(predictions))

        final_predictions = pd.DataFrame(list(zip(loan_ids, prediction_series)))

        """We can be as creative in sending the responses.
           But we need to send the response codes as well.
        """
        responses = jsonify(predictions=final_predictions.to_json(orient="records"))
        responses.status_code = 200

        return (responses)

После выполнения, введите: gunicorn --bind 0.0.0.0:8000 server:app
Давайте сгенерируем данные для прогнозирования и очередь для локального запуска API по адресу https:0.0.0.0:8000/predict

import json
import requests

"""Setting the headers to send and accept json responses
"""
header = {'Content-Type': 'application/json', \
                  'Accept': 'application/json'}

"""Reading test batch
"""
df = pd.read_csv('../data/test.csv', encoding="utf-8-sig")
df = df.head()

"""Converting Pandas Dataframe to json
"""
data = df.to_json(orient='records')

data

'[{"Loan_ID":"LP001015","Gender":"Male","Married":"Yes","Dependents":"0","Education":"Graduate","Self_Employed":"No","ApplicantIncome":5720,"CoapplicantIncome":0,"LoanAmount":110.0,"Loan_Amount_Term":360.0,"Credit_History":1.0,"Property_Area":"Urban"},{"Loan_ID":"LP001022","Gender":"Male","Married":"Yes","Dependents":"1","Education":"Graduate","Self_Employed":"No","ApplicantIncome":3076,"CoapplicantIncome":1500,"LoanAmount":126.0,"Loan_Amount_Term":360.0,"Credit_History":1.0,"Property_Area":"Urban"},{"Loan_ID":"LP001031","Gender":"Male","Married":"Yes","Dependents":"2","Education":"Graduate","Self_Employed":"No","ApplicantIncome":5000,"CoapplicantIncome":1800,"LoanAmount":208.0,"Loan_Amount_Term":360.0,"Credit_History":1.0,"Property_Area":"Urban"},{"Loan_ID":"LP001035","Gender":"Male","Married":"Yes","Dependents":"2","Education":"Graduate","Self_Employed":"No","ApplicantIncome":2340,"CoapplicantIncome":2546,"LoanAmount":100.0,"Loan_Amount_Term":360.0,"Credit_History":null,"Property_Area":"Urban"},{"Loan_ID":"LP001051","Gender":"Male","Married":"No","Dependents":"0","Education":"Not Graduate","Self_Employed":"No","ApplicantIncome":3276,"CoapplicantIncome":0,"LoanAmount":78.0,"Loan_Amount_Term":360.0,"Credit_History":1.0,"Property_Area":"Urban"}]'

"""POST <url>/predict
"""
resp = requests.post("http://0.0.0.0:8000/predict", \
                    data = json.dumps(data),\
                    headers= header)

resp.status_code

200

"""The final response we get is as follows:
"""
resp.json()

{'predictions': '[{"0":"LP001015","1":1},{...

Заключение

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

Есть несколько вещей, о которых нельзя забывать, в процессе создания API:

  • Создание качественного API из спагетти-кода вещь почти невозможная, поэтому применяйте свои знания в области машинного обучения, чтобы создать полезное и удобное API.
  • Попробуйте использовать контроль версий для моделей и кода API. Помните о том, что Flask не обеспечивает поддержку средств контроля версий. Сохранение и отслеживание ML моделей – это сложная задача, найдите удобный для себя способ. Здесь есть статья, которая рассказывает о том, как это делать.
  • В связи со спецификой scikit-learn моделей, необходимо удостовериться что оценщик и код для обучения лежат рядом (в случае использования пользовательского оценщика для предобработки или иной подобной задачи). Таким образом законсервированная модель будет иметь рядом с собой оценщик класса.

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

Код и пояснения для этой статьи

Полезные источники:

[1] Don’t Pickle your data.
[2] Building Scikit Learn compatible transformers.
[3] Using jsonify in Flask.
[4] Flask-QuickStart.

Вот такой получился материал. Подписывайтесь на нас, если понравилась публикация, а также записывайтесь на бесплатный открытый вебинар по теме: «Метрические алгоритмы классификации», который уже 12 марта проведет разработчик и data scientist с 5-летним опытом — Александр Никитин.
OTUS. Онлайн-образование
Цифровые навыки от ведущих экспертов

Комментарии 1

    0
    Если уж речь про заведение модели в продакшен, то наверно неправильно при каждом запросе загружать модель с диска. Нужно переписать код server.py так чтобы при инициализации сразу загружалась модель, а уже при запросе только делались предикшены от нее.

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое