Друзья, в конце марта мы запускаем новый поток по курсу «Data Scientist». И прямо сейчас начинаем делиться с вами полезным материалом по курсу.
Введение
Вспоминая ранний опыт своего увлечения машинным обучением (ML) могу сказать, что много усилий уходило на построение действительно хорошей модели. Я советовался с экспертами в этой области, чтобы понять, как улучшить свою модель, думал о необходимых функциях, пытался убедиться, что все предлагаемые ими советы учтены. Но все же я столкнулся с проблемой.
Как же внедрить модель в реальный проект? Идей на этот счет у меня не было. Вся литература, которую я изучал до этого момента, фокусировалась только на улучшении моделей. Я не видел следующего шага в их развитии.
Именно поэтому я сейчас пишу это руководство. Мне хочется, чтобы вы столкнулись с той проблемой, с которой столкнулся я в свое время, но смогли достаточно быстро ее решить. К концу этой статьи я покажу вам как реализовать модель машинного обучения используя фреймворк Flask на Python.
Содержание
- Варианты реализации моделей машинного обучения.
- Что такое API?
- Установка среды для Python и базовые сведения о Flask.
- Создание модели машинного обучения.
- Сохранения модели машинного обучения: Сериализация и Десериализация.
- Создание 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'x80x03]qx00(Kx01Xx04x00x00x00hereqx01K{Xx06x00x00x00walkerqx02e.'
Затем мы снова выгрузим законсервированный объект:
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-летним опытом — Александр Никитин.
Автор: MaxRokatansky