Данный туториал пошагово разбирает процесс создания веб-приложения для определения тональности текста на основе NLP-модели.
Мы будем использовать модель из библиотеки Hugging Face Hub, но описанный подход подойдет для любой задачи машинного обучения.
План:
Загрузка и подготовка модели машинного обучения для использования в веб-сервисе.
Создание веб-сервиса с помощью FastAPI.
Изучение пользовательского интерфейса FastAPI для удобного ручного тестирования и демонстрации работы приложения.
Написание автоматических тестов с помощью библиотеки pytest.
Запуск приложения в Docker-контейнере.
Код доступен на GitHub.
0. Организация кода. Разделяем код ML и код приложения
Будем придерживаться следующей структуры:
Разделение пакетов ml и app помогает организовать код проекта более логично и удобно для его дальнейшей поддержки и развития.
ml содержит код для работы с моделью машинного обучения.
app содержит код для запуска веб-приложения.
Кроме этого, в проекте есть другие важные файлы и директории, такие как:
tests: содержит скрипты для тестирования кода. В рамках проекта мы также будем отдельно тестировать ml-код и приложение.
setup.py: содержит информацию о пакете и его зависимостях.
requirements-dev.txt и requirements.txt: это списки зависимостей для локальной разработки и запуска приложения соответственно.
Dockerfile: содержит инструкции для создания Docker-контейнера.
1. Загрузка и подготовка модели машинного обучения
Очень полезная практика оформить ML-код так, чтобы с ним можно было работать как с черным ящиком. Позже сервис будет получать всю ML-логику через функцию load_model
.
В зависимости от вашей задачи, load_model
будет включать:
всю логику работы с признаками и препроцессинг,
загрузку модели и необходимых артефактов из хранилища,
инференс модели,
постпроцессинг предсказаний,
...
Начнем с загрузки модели. В нашем примере загрузим модель cointegrated/rubert-tiny-sentiment-balanced
из Hugging Face Hub:
from transformers import pipeline
model_hf = pipeline("sentiment-analysis", model="cointegrated/rubert-tiny-sentiment-balanced")
Опишем формат, который будет возвращать модель и с которым будет позже работать сервис. Для этого удобно использовать dataclass
:
from dataclasses import dataclass
@dataclass
class SentimentPrediction:
"""Class representing a sentiment prediction result."""
label: str
score: float
Теперь главное: model
- функция, которую будет вызывать сервис, чтобы получить предсказания. Она содержит всю необходимую логику с моделью, пре и пост-процессингом данных. В нашем случае model_hf
- уже пайплайн, который содержит препроцессинг текста и токенизацию, инференс модели и постпроцессинг предсказаний. Мы только оставим предсказания лучшего класса и вернем ответ в виде экземпляра класса SentimentPrediction
:
def model(text: str) -> SentimentPrediction:
pred = model_hf(text)
pred_best_class = pred[0]
return SentimentPrediction(
label=pred_best_class["label"],
score=pred_best_class["score"],
)
Теперь код, связанный с ML закончен. На этом шаге еще полезно вспомнить, что мы использовали константы при загрузке модели из HF.
Любые константы лучше выносить в отдельные конфиги, чтобы:
иметь быстрый доступ ко всем параметрам модели,
удобно настраивать параметры модели, не залезая в код.
В нашем случае для конфигурации будем использовать YAML-файл config.yaml
:
task: sentiment-analysis
model: cointegrated/rubert-tiny-sentiment-balanced
Тогда скрипт получения модели model.py
будет выглядеть следующим образом:
from dataclasses import dataclass
from pathlib import Path
import yaml
from transformers import pipeline
# load config file
config_path = Path(__file__).parent / "config.yaml"
with open(config_path, "r") as file:
config = yaml.load(file, Loader=yaml.FullLoader)
@dataclass
class SentimentPrediction:
"""Class representing a sentiment prediction result."""
label: str
score: float
def load_model():
"""Load a pre-trained sentiment analysis model.
Returns:
model (function): A function that takes a text input and returns a SentimentPrediction object.
"""
model_hf = pipeline(config["task"], model=config["model"], device=-1)
def model(text: str) -> SentimentPrediction:
pred = model_hf(text)
pred_best_class = pred[0]
return SentimentPrediction(
label=pred_best_class["label"],
score=pred_best_class["score"],
)
return model
Мы также добавили device=-1
, чтобы модель запускалась на CPU.
2. Пишем приложение на FastAPI
Простейшее приложение на FastAPI выглядит так:
from fastapi import FastAPI
app = FastAPI()
# create a route
@app.get("/")
def index():
return {"text": "Sentiment Analysis"}
Но, оно пока ничего не умеет делать, в частности, ничего не знает о модели, которую мы подготовили в пакете ml
. Добавим загрузку модели во время старта приложения:
from ml.model import load_model
model = None
# Register the function to run during startup
@app.on_event("startup")
def startup_event():
global model
model = load_model()
Теперь осталось добавить предсказание модели. Для начала определим формат ответа SentimentResponse
. Используем pydantic
для валидации выходных данных:
from pydantic import BaseModel
class SentimentResponse(BaseModel):
text: str
sentiment_label: str
sentiment_score: float
Мы будем возращать:
text
- исходный текст,sentiment_label
- название класса, который предсказала модель,sentiment_score
- значение скора предсказания.
Напишем GET-запрос для получения предсказания по заданному тексту. Благодаря тому, что model
хранит всю логику модели внутри себя, нам достаточно передать ей сырой текст. Вспомним, что model
возвращает предсказание в виде объекта класса SentimentPrediction
, который мы задали ранее в пакете ml
. Далее формируем ответ согласно заданному формату SentimentResponse
.
# Your FastAPI route handlers go here
@app.get("/predict")
def predict_sentiment(text: str):
sentiment = model(text)
response = SentimentResponse(
text=text,
sentiment_label=sentiment.label,
sentiment_score=sentiment.score,
)
return response
На этом приложение готово! Весь код app.py
занял 40 строк:
from fastapi import FastAPI
from pydantic import BaseModel
from ml.model import load_model
model = None
app = FastAPI()
class SentimentResponse(BaseModel):
text: str
sentiment_label: str
sentiment_score: float
# create a route
@app.get("/")
def index():
return {"text": "Sentiment Analysis"}
# Register the function to run during startup
@app.on_event("startup")
def startup_event():
global model
model = load_model()
# Your FastAPI route handlers go here
@app.get("/predict")
def predict_sentiment(text: str):
sentiment = model(text)
response = SentimentResponse(
text=text,
sentiment_label=sentiment.label,
sentiment_score=sentiment.score,
)
return response
3. Настраиваем окружение и запускаем тесты на ML-код
Для локального запуска в процессе разработки удобно использовать виртуальные окружения venv
. Внутри виртуальных окружений можно устанавливать и использовать необходимые пакеты и библиотеки без влияния на глобальное окружение Python на системе. Создадим и активируем виртуальное окружение:
# Create a virtual environment
python3.11 -m venv env
# Activate the virtual environment
source env/bin/activate
Теперь соберем питоновский пакет, описанный в setup.py
, с зависимостями из requirements.txt
. Это означает, что мы создадим пакет, который будет включать в себя весь код, описанный в нашем репозитории, а также все необходимые библиотеки, указанные в файлах setup.py
.
# Install/upgrade dependencies
pip install -U -e .
Следующий этап - тестирование ML-кода. Тесты помогают выявлять ошибки в коде на ранних этапах разработки, предотвращать появление новых ошибок при внесении изменений в код, а также сокращать время и затраты на тестирование вручную.
Для тестирования будет использовать библиотеку pytest
. Чтобы установить эту библиотеку и другие зависимости, которые необходимы только для разработки, а не для использования проекта в продакшн-среде, мы указали их в файле requirements-dev.txt
. Информация о зависимостях также прописана в файле setup.py
, поэтому для их установки мы можем использовать команду:
pip install -U -e .[dev]
Тесты на код машинного обучения хранятся в test_ml.py
. В данном примере у нас 3 теста, которые проверяют, корректно ли модель определяет положительную, отрицательную и нейтральную тональность в тексте:
import pytest
from ml.model import SentimentPrediction, load_model
@pytest.fixture(scope="function")
def model():
# Load the model once for each test function
return load_model()
@pytest.mark.parametrize(
"text, expected_label",
[
("очень плохо", "negative"),
("очень хорошо", "positive"),
("по-разному", "neutral"),
],
)
def test_sentiment(model, text: str, expected_label: str):
model_pred = model(text)
assert isinstance(model_pred, SentimentPrediction)
assert model_pred.label == expected_label
Библиотека pytest
предоставляет удобный и интуитивно понятный синтаксис для написания тестов. Здесь мы использовали:
фикстуры - позволяют задавать начальные условия для тестов. В нашем случае, фикстура
model
загружает модель перед началом каждого тестового запуска.параметризация - задает различные значения для тестовых параметров, уменьшая необходимость в дублировании кода.
В данном случае, тест проверяет, что модель верно определяет тональность текста, и для каждого параметра (text
, expected_label
) проверяет соответствующее значение предсказания модели. Если значение не соответствует ожидаемому результату, тест выдает ошибку.
Для запуска тестов нужно воспользоваться командой:
pytest tests/test_ml.py
4. Запуск приложения и удобный интерфейс FastAPI
С использованием uvicorn
, мы можем запустить наше приложение и обрабатывать входящие HTTP-запросы. Для запуска приложения с помощью uvicorn
выполним следующую команду:
# Run app
uvicorn app.app:app --host 0.0.0.0 --port 8080
где app.app
- это путь к файлу с нашим приложением, app
- имя экземпляра приложения, --host
- параметр, указывающий IP-адрес, на котором будет запущен сервер (в данном случае 0.0.0.0), и --port
- параметр, указывающий порт, на котором будет запущен сервер (в данном случае 8080).
После выполнения этой команды, uvicorn
запустит наше приложение и начнет принимать входящие HTTP-запросы на указанном порту. Информацию о запуске приложения мы увидим в терминале:
Откроем эту ссылку в браузере и увидим то самое сообщение, которое мы указали, когда начинали писать приложение на FastAPI.
FastAPI дополнительно предоставляет очень удобный интерфейс для отправки запросов. Он доступен, если в строке браузера добавить /docs
:
Здесь можно руками потыкать приложение. Нажав на "Try it out", можно вводить любые входные данные и проверять, как отрабатывает приложение:
Использование виртуального окружения удобно для разработки и тестирования приложения на локальной машине. Далее обсудим, как запускать приложения в Docker-контейнере.
5. Запуск приложения в Docker-контейнере и тестирование приложения
Docker дает возможность упаковать приложение и запускать его на любой машине. Некоторые из его преимуществ:
Абстракция от хост-системы: Docker-контейнер позволяет упаковать приложение со всеми зависимостями и настройками в единый образ, который может быть запущен на любой машине, где установлен Docker.
Изоляция: запуск приложения в Docker-контейнере обеспечивает изоляцию от других процессов и приложений на хост-машине, что уменьшает риск взаимодействия с другими приложениями и позволяет управлять ресурсами контейнера.
Управление зависимостями: Docker-контейнер позволяет явно определить все зависимости и версии, необходимые для запуска приложения.
Для начального ознакомления с Docker подойдет их страница. Здесь же есть ссылки на инструкции, как установить Docker на разные системы.
Начнем с Dockerfile. Dockerfile - это текстовый файл, который содержит инструкции по созданию образа Docker. Он используется для автоматической сборки образа Docker, который включает все необходимые зависимости, настройки и код для запуска приложения в изолированном контейнере. Наш Dockerfile:
FROM python:3.11
COPY requirements.txt requirements-dev.txt setup.py /workdir/
COPY app/ /workdir/app/
COPY ml/ /workdir/ml/
WORKDIR /workdir
RUN pip install -U -e .
# Run the application
CMD ["uvicorn", "app.app:app", "--host", "0.0.0.0", "--port", "80"]
Первая инструкция указывает, что мы хотим использовать готовый образ Python версии 3.11 как основу для создания нашего образа.
Затем мы копируем весь рабочий код в рабочую директорию /workdir/ внутри контейнера.
Строка
WORKDIR /workdir
устанавливает рабочую директорию для последующих команд в Dockerfile. Это означает, что все следующие команды в Dockerfile будут выполняться относительно этой директории.Далее собираем пакет, по аналогии с тем, как делали в виртуальном окружении.
В последней строке указывается команда, которая будет выполнена при запуске контейнера: запуск приложения с помощью
uvicorn
на порту 80.
Создаем новый Docker-образ с именем ml-app
, используя Dockerfile, находящийся в текущей директории:
docker build -t ml-app .
После того, как образ собрался, командой
docker run -p 80:80 ml-app
запускаем контейнер из образа ml-app
и привязывает порт 80 внутри контейнера к порту 80 на хосте.
Контейнер будет запущен и доступен по адресу http://localhost:80 в браузере. Приложение также можно протестировать вручную, используя UI от FastAPI, как описано в предыдущем пункте.
Мы также можем написать несколько тестов для тестирования приложения, запущенного в контейнере. Будем использовать те же примеры, которые мы использовали для ML-кода. Только теперь будем отправлять HTTP-запросы в сервис, поднятый в контейнере, используя библиотеку requests
.
import pytest
import requests
@pytest.mark.parametrize(
"input_text, expected_label",
[
("очень плохо", "negative"),
("очень хорошо", "positive"),
("по-разному", "neutral"),
],
)
def test_sentiment(input_text: str, expected_label: str):
response = requests.get("http://0.0.0.0/predict/", params={"text": input_text})
assert response.json()["text"] == input_text
assert response.json()["sentiment_label"] == expected_label
Для запуска тестов можно использовать созданное нами ранее виртуальное окружение env
, так как там уже стоит библиотека pytest
. Тогда в другом терминале, не останавливая запущенный контейнер, запустим тесты, предварительно активировав окружение env
:
source env/bin/activate
pytest tests/test_app.py
deactivate
Заключение
В этом туториале мы создали веб-приложение для определения тональности текста с помощью FastAPI.
Мы также коснулись важных аспектов при разработке приложения: организация кода, тестирование, конфигурация, запуск приложения в Docker-контейнере.
Описанный подход может быть использован для любой задачи машинного обучения.
Код приложения доступен на GitHub и его можно использовать как отправную точку для создания собственного веб-сервиса.
Следующую статью планирую написать про запуск ML-пайплайна с помощью Airflow. А пока подписывайтесь на мой телеграм-канал. Там будут анонсы новых статей, а также советы для работы и более короткие мысли по DS/ML/AI.