Хабрахабр

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

Друзья, в конце марта мы запускаем новый поток по курсу «Data Scientist». И прямо сейчас начинаем делиться с вами полезным материалом по курсу.

Введение

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

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

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

Содержание

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

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

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

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

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

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

Что такое API?

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

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

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

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

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

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

Есть еще Django, Falcon, Hug и множество других, о которых в данной статье не говорится. Внимание: Flask — это не единственный сетевой фреймворк для этих целей. Например, для 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! Ура! Поскольку у вас уже есть некоторый опыт в выполнении этих простых шагов, мы сможем создать сетевые конечные точки, которые могут быть доступны локально.

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

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

  • Для начала займемся соревнованием по машинному обучению 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, которое поможет в прототипировании продукта и сделает его действительно функциональным, но чтобы отправить его в продакшн, необходимо внести несколько корректив, которые уже не входят в сферу изучения машинного обучения. В этой статье мы прошли лишь половину пути, создав рабочее API, которое выдает прогнозы, и стали на шаг ближе к интеграции ML решений непосредственно в разрабатываемые приложения.

Есть несколько вещей, о которых нельзя забывать, в процессе создания 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-летним опытом — Александр Никитин. Вот такой получился материал.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть