submission.csv? Нет, спасибо!

Это продолжение моей предыдущей статьи:

От записной книжки Jupyter к скриптам

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

Даже с формой скрипта нам по-прежнему требуется изменить конфигурацию и запустить скрипт. Это нормально для соревнований Kaggle, потому что все, что вам нужно, это просто submission.csv, но вы, вероятно, не хотите сидеть за компьютером 24/7 и нажимать Run всякий раз, когда пользователи отправляют вам запрос на прогноз 🙁

В этой статье мы обсудим, как использовать модели, которые мы построили в прошлый раз, и создать API-интерфейсы прогнозирования для обслуживания моделей с помощью FastAPI!

Для тех, кто занимается ML / DL, мы говорим о FastAPI, а НЕ fast.ai !!!

Справочная информация: FastAPI

В экосистеме Python есть много фреймворков для API, я изначально думал использовать Flask. Но я впечатлен тем, насколько простой и интуитивно понятный [и быстрый, как следует из названия] FastAPI, и люблю опробовать его в этом мини-проекте!

Рим был построен не за один день, FastAPI многому научился из предыдущих фреймворков, таких как Django, Flask, APIStar, я не могу объяснить лучше, чем сам создатель, и эта статья великолепна!

Скучная, но необходимая установка

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

* Специалисты по CS всегда говорят принцип единой ответственности вместо того, чтобы говорить «не объединяйте код с разными функциональными возможностями», в следующий раз, возможно, вы можете сказать «мы должны следовать принципу единой ответственности!»

Прежде всего, давайте обновим файл requirements.txt новыми пакетами, как мы упоминали в прошлый раз, мы должны указать точную версию, чтобы другие могли воспроизвести работу!

# for last article
pytest==6.0.1
pandas==1.0.1
Click==7.0
scikit-learn==0.22.1
black==19.10b0
isort==4.3.21
PyYAML==5.2
# for FastAPI
fastapi==0.61.0
uvicorn==0.11.8
chardet==3.0.4

После этого нам нужно снова установить requirements.txt в conda env [потому что у нас есть новые пакеты]

# You can skip the line below if you have created conda env
conda create - name YOU_CHANGE_THIS python=3.7 -y
conda activate YOU_CHANGE_THIS
pip install –r requirements.txt

План игры

Давайте подумаем о том, что происходит. Нам нужна конечная точка API для прогнозирования, а точнее, если пользователи дают нам ввод, нам нужно использовать модель для прогнозирования и возврата прогнозов.

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

Ниже псевдокод:

# Load trained model
trained_model = load_model(model_path)
# Let's create a API that can receive user request
api = CreateAPI()
# If user send us the request to `predict` endpoint
when user sends request to `api`.`predict`:
    input = api[`predict`].get(input) # get input
    prediction = trained_model(input) # apply model
    return prediction                 # return prediction

Это хорошо для счастливого течения! Но мы должны НИКОГДА не доверять пользователю, просто спросите себя, будете ли вы когда-нибудь читать руководство пользователя в своей повседневной жизни?

Например, мы ожидаем от пользователя {‘a’: 1, ‘b’: 2, ‘c’: 3}, но можем получить:

  • Неправильный порядок {‘b’: 2, ‘a’: 1, ‘c’: 3} или
  • Неверный ключ {‘a’: 1, ‘b’: 2, ‘d’: 3} или
  • Отсутствует ключ {‘a’: 1, ‘b’: 2} или
  • Отрицательное значение {‘a’: -1, ‘b’: 2, ‘c’: 3} или
  • Неверный тип {‘a’: «HELLO WORLD», ‘b’: 2, ‘c’: 3} или
  • и т. д. и т. д.

Это фатально для нашего API, потому что наша модель не знает, как на это реагировать. Нам нужно ввести некоторые структуры ввода, чтобы защитить нас! Следовательно, мы должны обновить наш псевдокод!

# Define input schema
input_schema = {......}
# Load trained model
trained_model = load_model(model_path)
# Let's create a API that can receive user request
api = CreateAPI()
# If user send us the request to `predict` endpoint
when user sends request to `api`.`predict`:
    input = api[`predict`].get(input) # get input           
    transformed_input = apply(input_schema, input)
    if not transformed_input.valid(): return Error
    prediction = trained_model(transformed_input) # apply model
    return prediction                 # return prediction

Код

Теперь мне нравится! Давайте переведем их с помощью FastAPI по частям!

Схема ввода

Кажется, много строк, но все то же самое, как вы можете догадаться, мы определяем класс под названием `Sample`, который определяет каждый предиктор как float и больше, чем [gt] ноль!

Загрузить модель

Затем загружаем обученную модель, хммм, что такое «Predictor»? это просто настраиваемый класс, который обертывает модель различными методами, поэтому мы можем вызывать метод вместо реализации логики на сервере API

Создайте сервер API

Затем мы создаем API с помощью FastAPI …… псевдокод - это уже почти код

предсказать конечную точку

Это выглядит сложно, но они очень простые

Вместо того, чтобы говорить «когда пользователь отправляет запрос на` api`.`predict` »

Мы говорим: «Привет, приложение, если люди отправляют« GET запрос »для« прогноз », запустите функцию pred_item, мы ожидаем, что ввод будет соответствовать схеме, которую мы определили в« Sample »»

pred_item только преобразует входную форму, передает в обученную модель и возвращает прогноз, простую функцию Python

Если вы хотите узнать больше о методах HTTP-запроса

Но вы можете спросить: эй! Одна строчка отсутствует !!! Где проверка ввода? Что делать, если пользователи указывают неправильный тип данных / ключ или пропускают поле?

Что ж …… .Помните, что мы определили класс Sample для входной схемы? Fast API автоматически проверяет его для нас в соответствии со схемой, и нам не нужно об этом заботиться !!! Это экономит много мозгов и много строк кода для создания надежного и хорошо протестированного API!

Попробуй использовать

# At project root, we can run this
# --reload is for development, API server autorefresh
# when you change the code
uvicorn prediction_api.main:app --reload

Вы должны увидеть это, сервер API теперь работает на «http://127.0.0.1:8000»!

Есть разные способы поэкспериментировать с API, в зависимости от вашей среды: вы можете использовать запросы в Python или cURL в командной строке. Кстати, есть удобный инструмент под названием Почтальон, попробуйте его, это очень интуитивно понятный и удобный инструмент для API!

Мы будем использовать запросы Python для следующих примеров, вы можете увидеть их в этом блокноте [иногда помогает Jupyter 😎]

В приведенном ниже примере используется допустимый ввод: ДА! 😍 Мы сделали это! Конечная точка возвращает прогноз !!!

payload = {
    "fixed_acidity": 10.5,
    "volatile_acidity": 0.51,
    "citric_acid": 0.64,
    "residual_sugar": 2.4,
    "chlorides": 0.107,
    "free_sulfur_dioxide": 6.0,
    "total_sulfur_dioxide": 15.0,
    "density": 0.9973,
    "pH": 3.09,
    "sulphates": 0.66,
    "alcohol": 11.8,
}
result = requests.get("http://127.0.0.1:8000/predict", data = json.dumps(payload))
print(result.json())
Output
{'prediction': 1, 'utc_ts': 1597537570, 'model': 'RandomForestClassifier'}

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

payload = {
    "volatile_acidity": 0.51,
    "citric_acid": 0.64,
    "residual_sugar": 2.4,
    "chlorides": 0.107,
    "free_sulfur_dioxide": 6.0,
    "total_sulfur_dioxide": 15.0,
    "density": 0.9973,
    "pH": 3.09,
    "sulphates": 0.66,
    "alcohol": 11.8,
}
result = requests.get("http://127.0.0.1:8000/predict", data = json.dumps(payload))
print(result.json())
Output
{'detail': [{'loc': ['body', 'fixed_acidity'], 'msg': 'field required', 'type': 'value_error.missing'}]}

Ради интереса я также реализовал update_model PUT API для обмена моделями, например, изначально мы использовали Random Forest, я обновил его до Gradient Boosting☺️

result = requests.put("http://127.0.0.1:8000/update_model")
print(result.json())
Output
{'old_model': 'RandomForestClassifier', 'new_model': 'GradientBoostingClassifier', 'utc_ts': 1597537156}

Автоматически созданный документ

Одной из замечательных функций FastAPI является автоматическое документирование. Просто перейдите по адресу http://127.0.0.1:8000/docs#/, и вы получите интерактивный и мощный документ API из коробки! Настолько интуитивно понятный, что мне не нужно вдаваться в подробности

Еще раз посетить pytest

Я не могу переоценить важность модульного тестирования, оно проверяет, что функции выполняют то, что мы ожидаем от них, так что вы не сможете случайно что-то сломать!

Но если я постараюсь пройти все тесты, это будет слишком скучно и долго. Я планирую здесь поделиться некоторыми областями, которые я буду бездумно тестировать, и некоторыми [возможно, полезными] статьями. Затем я расскажу о функции pytest, называемой параметризованным модульным тестом, и некоторых вариантах тестирования в pytest. Самый простой способ мотивировать себя изучать модульное тестирование - это попытаться реорганизовать предыдущий код. Чем больше, тем лучше!

Модульное тестирование

Всякий раз, когда вам было трудно написать / понять модульные тесты, вам, вероятно, сначала нужно пересмотреть структуру кода. Ниже приведены 4 области, которые я рассмотрю бездумно:

  1. Входные данные: размер [например: df.shape], тип [например: str], диапазон значений [например: - / 0 / +]
  2. Выходные данные: размер [например: df.shape], тип [например: str], диапазон значений [например: - / 0 / +]
  3. Сравните: вывод и ожидаемый результат
  4. После отладки не допустить, чтобы это повторилось снова

Например, я очень много внимания уделяю выходному размеру, типу и диапазону значений, указанным ниже. Это кажется простым, но если вы измените какой-либо выходной формат, он напомнит вам, какие форматы ожидаются!

Некоторые статьи БЮР:

Модульное тестирование для специалистов по данным

Как выполнить модульное тестирование кода машинного обучения [глубокое обучение]

Параметризованный модульный тест

Предположим, у вас есть 100 фиктивных данных [аннотация от D_i, i: 1..100], и вы хотите запустить один и тот же модульный тест для каждого из них, как вы это сделаете?

Решение с использованием грубой силы

def test_d1():
    assert some_operation(D_1)
def test_d2():
    assert some_operation(D_2)
def test_d3():
    assert some_operation(D_3)
......
def test_d100():
    assert some_operation(D_100)

Но если вам нужно изменить some_operation, вам нужно изменить его 100 раз LOL ……… Хотя вы можете сделать это как служебную функцию, это затрудняет чтение и делает тесты очень длинными.

Может быть, для цикла лучше?

def test_d():
    for D in [D_1, D_2, D_3, ..., D_100]:
        assert some_operation(D)

Но вы не можете точно знать, какие тесты не пройдут, потому что все эти 100 тестов находятся в одном тесте.

pytest предлагает нам функцию под названием параметризация

@pytest.mark.parametrize("test_object", [D_1, D_2, ..., D_100])
def test_d(test_object):
    assert some_operation(test_object)

Распространенные параметры pytest

pytest FOLDER

В прошлый раз, когда мы упоминали, мы можем просто запустить `pytest` в командной строке, и pytest найдет ВСЕ тесты в самой папке. Но иногда мы можем не захотеть запускать все модульные тесты во время разработки [возможно, некоторые тесты занимают много времени, но не связаны с вашими текущими задачами]

В этом случае вы можете просто запустить pytest FOLDER, например: `pytest. / Scripts` или` pytest. / Prediction_api` в демонстрации.

параллельный pytest

Иногда ваши тестовые примеры слишком тяжелы, может быть хорошей идеей запустить их параллельно! Вы можете установить pytest-xdist и заменить pytest на py.test в своей команде, например: py.test -n 4

pytest -v

Это личный вкус, я предпочитаю подробный вывод и вижу зеленый ПРОШЕЛ ✅, чтобы начать свой день

Подробнее читайте в материалах ниже:

Https://docs.pytest.org/en/stable/

Https://www.guru99.com/pytest-tutorial.html#5

Наконец, я надеюсь, что вам понравится это 1-минутное видео на Youtube, как и мне 😆

Выводы

Yooo✋ мы создали API прогнозирования, который использует нашу модель, теперь пользователи могут отправлять запросы и получать прогнозы без участия человека, это упрощает реальность [пропускная способность, задержка, управление моделью, аутентификация, AB-тестирование и т. Д.] но это идея!

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

В заключение мы:

a. Update conda env [requirements.txt]
b. Brainstorm pseudocode and convert to code [FastAPI, uvicorn]
c. Utilize API [cURL, requests, Postman]
d. Talk about Auto-generated documents by FastAPI
e. Some pytest techniques [parallel, parameterized, -v]

Дерево файлов ниже, чтобы показать этапы разработки

.
├── notebook
│   ├── prediction-of-quality-of-wine.ipynb
│   └── prediction_API_test.ipynb           [c] <-consume API
├── prediction_api
│   ├── __init__.py
│   ├── api_utility.py                      [b] <-wrap up methods
│   ├── main.py                             [b] <-modify demo
│   ├── mock_data.py                        [e] <-Unit test
│   ├── test_api_utility.py                 [e] <-Unit test
│   └── test_main.py                        [e] <-Unit test
├── requirements.txt                        [a] <-FastAPI doc
.
.
.

НО (опять же, плохие новости обычно начинаются с НО) они все еще находятся на моем локальном компьютере.

Хотя нам не нужно сидеть сложа руки и нажимать «Выполнить», пользовательские запросы не могут достигать конечных точек API. Даже если они это сделают, это означает, что я не могу закрыть свой Macbook, это означает, что я не могу масштабироваться, если есть много входящих запросов прогнозов😱 !!!

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

Но сначала нам также нужно убедиться, что код там работает нормально! Как?

Краткий ответ: докер

В сторону:

Хотя я не пробовал, есть стартап под названием Cortex, который фокусируется на платформе API машинного обучения с открытым исходным кодом, и они также используют FastAPI под капотом!

К настоящему времени вы должны быть в состоянии понять их руководство, короче говоря, они решают многие проблемы производственного уровня за кулисами, такие как последовательное обновление, вывод модели DL, интеграция с AWS, автоматическое масштабирование и т. Д.… [Это DevOps проблемы? Или, может быть, более интересный термин: MLOps]

Но с точки зрения пользователя [он же вы], они развертывают API с помощью декларативного yml [аналогично тому, как мы настраиваем модель в предыдущей статье], имеют класс предиктора [аналогичный нашему классу Predictor], trainer.py [аналогичный train.py в последней статье]

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

ИЛИ вы можете пометить мое репо!

ИЛИ мой LinkedIn [Добро пожаловать, но, пожалуйста, оставьте несколько слов, чтобы указать, что вы не зомби]!