![](https://habrastorage.org/getpro/habr/upload_files/6f0/217/d53/6f0217d53cd81b852e589d83310b3df7.jpg)
Данный туториал пошагово разбирает процесс создания веб-приложения для определения тональности текста на основе NLP-модели.
Мы будем использовать модель из библиотеки Hugging Face Hub, но описанный подход подойдет для любой задачи машинного обучения.
План:
-
Загрузка и подготовка модели машинного обучения для использования в веб-сервисе.
-
Создание веб-сервиса с помощью FastAPI.
-
Изучение пользовательского интерфейса FastAPI для удобного ручного тестирования и демонстрации работы приложения.
-
Написание автоматических тестов с помощью библиотеки pytest.
-
Запуск приложения в Docker-контейнере.
Код доступен на GitHub.
0. Организация кода. Разделяем код ML и код приложения
Будем придерживаться следующей структуры:
![](https://habrastorage.org/getpro/habr/upload_files/2bd/7eb/6ec/2bd7eb6ec177495d5821c75c1fedb0a6.png)
Разделение пакетов 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‑запросы на указанном порту. Информацию о запуске приложения мы увидим в терминале:
![](https://habrastorage.org/getpro/habr/upload_files/749/9a8/866/7499a88664737bc9ee7c257114b9e460.png)
![](https://habrastorage.org/getpro/habr/upload_files/66b/257/11e/66b25711e361ec6ca684e4d533014f1e.png)
Откроем эту ссылку в браузере и увидим то самое сообщение, которое мы указали, когда начинали писать приложение на FastAPI.
FastAPI дополнительно предоставляет очень удобный интерфейс для отправки запросов. Он доступен, если в строке браузера добавить /docs
:
![](https://habrastorage.org/getpro/habr/upload_files/c58/468/203/c58468203f308386723c76a9663e8201.png)
Здесь можно руками потыкать приложение. Нажав на «Try it out», можно вводить любые входные данные и проверять, как отрабатывает приложение:
![](https://habrastorage.org/getpro/habr/upload_files/d4e/ff0/bed/d4eff0bedd0297d6336b78b35b99ddd8.png)
Использование виртуального окружения удобно для разработки и тестирования приложения на локальной машине. Далее обсудим, как запускать приложения в 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.
ссылка на оригинал статьи https://habr.com/ru/articles/729380/
Добавить комментарий