В этой статье я опишу процесс развертывания модели машинного обучения с использованием FastAPI в Docker. Основная цель этого проекта — узнать о создании и запуске веб-сайта вывода машинного обучения в Docker.

Контекст,

Модель, используемая в этом проекте, представляет собой регрессионную модель для прогнозирования возраста морского ушка. Я построил ее с использованием алгоритма многослойного персептрона из Scikit Learn. Кстати, я построил эту модель для завершения проекта третьего курса колледжа, а теперь решил применить ее, изучая машинное обучение в производстве.

Я буду использовать Fast API, чтобы создать веб-сайт, закрепить его и развернуть в облаке Heroku (первый уровень бесплатного пользования). Да, я знаю, я могу упростить его, используя Streamlit, разместить на Github и развернуть с помощью Streamlit (конечно, это бесплатно). Но это не поможет мне изучить Docker, я хочу изучить Docker.

Последний контекст: почему я выбрал Fast API? Ну, потому что это Python Framework, и в последнее время он пользуется хорошей ажиотажем😁.

Я разделю эту статью на три раздела: написание кода вывода и веб-сайта, как его докеризовать и как развернуть в Heroku. Хорошо, давайте начнем.

1. Вывод и код сайта

Первое, что я делаю, это выбираю структуру проекта, которую буду использовать. Потратив полчаса на поиск в Google, я нашел вот такой шаблон:

├── my_project
│   ├── app
│   │   ├── venv
│   │   ├── static
│   │   |  ├── style.css
│   │   ├── templates
│   │   |  ├── index.html
│   │   |  ├── prediction.html
│   │   ├── main.py
│   │   ├── mlp_abalone_age_prediction-0.1.0.sav

Я пишу все выводы и код маршрутизации веб-сайта в файле main.py из-за простоты этого проекта. Если код вывода довольно сложен, мы можем записать его в другой файл Python. Папка venv — это виртуальная среда проекта, я просто использую ее для локального тестирования приложения и игнорирую ее при докеризации.

В процессе разработки я обычно устанавливаю каждую из необходимых библиотек отдельно и перечисляю ее в текстовом файле с названием «requirements.txt», это все библиотеки, которые я использовал.

anyio==3.7.0
certifi==2023.5.7
click==8.1.3
colorama==0.4.6
dnspython==2.3.0
email-validator==2.0.0.post2
exceptiongroup==1.1.1
fastapi==0.98.0
h11==0.14.0
httpcore==0.17.2
httptools==0.5.0
httpx==0.24.1
idna==3.4
itsdangerous==2.1.2
Jinja2==3.1.2
joblib==1.2.0
MarkupSafe==2.1.3
numpy==1.25.0
orjson==3.9.1
python-dotenv==1.0.0
python-multipart==0.0.6
PyYAML==6.0
scikit-learn==1.1.3
scipy==1.11.0
sniffio==1.3.0
starlette==0.27.0
threadpoolctl==3.1.0
typing-extensions==4.6.3
ujson==5.8.0
uvicorn==0.22.0
watchfiles==0.19.0
websockets==11.0.3

Хватит говорить о зависимостях, давайте напишем код вывода и маршрутизации на main.py.

from joblib import load
from pathlib import Path
from fastapi import FastAPI, Request, Form
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

# The model version, in case we update the model in the future
__version__ = "0.1.0"
BASE_DIR = Path(__file__).resolve(strict=True).parent
# open and load model file
with open(f"{BASE_DIR}/mlp_abalone_age_prediction-{__version__}.sav", "rb") as f:
    model = load(f)
# inference function
def mlp_predict(abalone_features):
    """
    abalone_features : List of seven abalone's features
    """
    predicted_age = model.predict([abalone_features])
    if predicted_age[0] <= 0:
        return f"There is an issue with the input data."
    return predicted_age[0]

app = FastAPI()

# Mount the "static" folder to serve CSS and other static files
app.mount(
    "/static", StaticFiles(directory=f"{BASE_DIR}/static"), name="static")

# Mount the "templates" folder to load HTML templates
templates = Jinja2Templates(directory=f"{BASE_DIR}/templates")

@app.get("/", response_class=HTMLResponse)
def read_root(request: Request):
    return templates.TemplateResponse("index.html", {"request": request, })

@app.post("/predict", response_class=HTMLResponse, )
def predict(request: Request, length: float = Form(...), diameter: float = Form(...), height: float = Form(...), whole_weight: float = Form(...), shucked_weight: float = Form(...), viscera_weight: float = Form(...), shell_weight: float = Form(...)):  # sentence: str = Form(...)
    abalone_features = [length, diameter, height, whole_weight,
                        shucked_weight, viscera_weight, shell_weight]
    prediction = mlp_predict(abalone_features)
    prediction = round(prediction, 2)
    return templates.TemplateResponse(
        "prediction.html",
        {"request": request, "prediction": prediction},
    )

Код вывода прост: импортируйте файл модели, и мы сможем использовать его в функции вывода. Файл модели создается на основе конвейера функций предварительной обработки и обученной модели MLP, поэтому мы можем напрямую использовать необработанные входные данные. Кстати, я не проверяю здесь ввод, я сделаю это в HTML-форме, чтобы получить правильный тип входных данных.

На этом веб-сайте есть только два маршрута: индексный маршрут по умолчанию «/» будет показывать форму ввода, а маршрут «/predict» используется для отображения результата прогноза.

Следующий код, который я напишу, — это файл index.html, который подключается к маршруту «/» по умолчанию и отображает форму ввода. Вот html-код.

<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" type="text/css" href="{{ url_for('static', path='/css/style.css') }}" />
  </head>
  <body>
    <h1>Abalone's Age Prediction App</h1>
    <p>Please fill in all of the Abalone's physical features measurements below & press the predict button:</p>
<div class="form-container">
      <form action="/predict" method="post">
        <label for="length">Length / Longest shell measurement (in milimeter / mm) :</label>
        <input type="number" min="0.0001" max="2" step="0.0001" name="length" id="length" placeholder="ex: 0.455" required /> <br />
        <br />
        <label for="diameter">Diameter / perpendicular to length (in milimeter / mm) :</label>
        <input type="number" min="0.0001" max="2" step="0.0001" name="diameter" id="diameter" placeholder="ex: 0.365" required /> <br />
        <br />
        <label for="height">Height / with meat in shell (in milimeter / mm) :</label>
        <input type="number" min="0.0001" max="2" step="0.0001" name="height" id="height" placeholder="ex: 0.095" required /> <br />
        <br />
        <label for="whole_weight">Whole Weight/ whole abalone (in grams / g) :</label>
        <input type="number" min="0.0001" max="2" step="0.0001" name="whole_weight" id="whole_weight" placeholder="ex: 0.5140" required /> <br />
        <br />
        <label for="shucked_weight">Shucked weight / weight of meat (in grams / g) :</label>
        <input type="number" min="0.0001" max="2" step="0.0001" name="shucked_weight" id="shucked_weight" placeholder="ex: 0.2245" required /> <br />
        <br />
        <label for="viscera_weight">Viscera weight / gut weight (after bleeding) (in grams / g):</label>
        <input type="number" min="0.0001" max="2" step="0.0001" name="viscera_weight" id="viscera_weight" placeholder="ex: 0.1010" required /> <br />
        <br />
        <label for="shell_weight">Shell weight / after being dried (in grams / g) :</label>
        <input type="number" min="0.0001" max="2" step="0.0001" name="shell_weight" id="shell_weight" placeholder="ex: 0.15" required /> <br />
        <br />
        <button type="submit">Predict Abalone's Age</button>
      </form>
    </div>
  </body>
</html>

Для прогнозирования модели требуется 7 числовых функций. Чтобы убедиться, что пользователь вводит правильные данные для каждого объекта, я установил тип по умолчанию в виде числа непосредственно в каждое поле ввода, а также минимальное, максимальное и ступенчатое значения для удобства пользователя.

Кнопка отправки вызовет функцию вывода после того, как все поля ввода будут правильно заполнены, и перенаправит пользователя на страницу прогноза. Я не показываю здесь Prediction.html и другие коды, но если вам интересно, я оставляю ссылку на репозиторий Github для этого проекта в конце этой статьи.

Процесс локальной разработки завершен, давайте попробуем его запустить. На самом деле, я всегда запускаю веб-сайт вместе с процессом кодирования, кодирую небольшую функцию — запускаю ее снова и снова. Вот как выглядит сайт (приложение FastAPI по умолчанию работает на 127.0.0.1:8000).

2. Докеризируйте наш сайт и запустите его локально.

Хорошо, веб-сайт успешно работает локально на интерпретаторе Python на компьютере. Следующий шаг — преобразование файлов и кодов веб-сайта в образы Docker. Для этого нам нужно сначала установить Docker на наш компьютер. Мы можем следовать инструкциям по установке на официальном сайте Docker по этой ссылке Установить Docker Desktop в Windows | Документы Докера. Однако установить Docker на компьютер с Windows немного сложно (по крайней мере, для меня).

Чтобы закрепить этот проект, нам нужен дополнительный файл, который мы должны разместить на одном уровне с папкой приложения.

├── my_project
│   ├── app
│   │   ├── venv
│   │   ├── static
│   │   |  ├── style.css
│   │   ├── templates
│   │   |  ├── index.html
│   │   |  ├── prediction.html
│   │   ├── main.py
│   │   ├── mlp_abalone_age_prediction-0.1.0.sav
│   ├── Dockerfile
│   ├── .dockerignore
│   ├── requirements.txt
│   ├── __init__.py

Dockerfile — это файл, который определяет, как Docker должен создавать образ приложения, .dockerignore — это список файлов или папок, которые не будут включены в образы, файл require.txt будет содержать всю библиотеку, которую должен установить Docker. для запуска приложения.

Чтобы создать Dockerfile, мы можем следовать инструкциям из нашего основного фреймворка, в данном случае я следую инструкциям FastAPI по этой ссылке FastAPI в контейнерах — Docker — FastAPI (tiangolo.com). Вот как выглядит Dockerfile этого проекта.

# we used the official python image
FROM python:3.9

# define the working directory of the docker image
WORKDIR /code

# copying file to the working directory
COPY ./requirements.txt /code/requirements.txt

# install library on the working directory
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

# copying all of the necessary file and folder to the working directory
COPY ./app /code/app

# command about how to run the app
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]

Чтобы создать образ Docker для этого приложения, используйте эту команду на терминале. Убедитесь, что вы запускаете ее в том же каталоге, где находится Dockerfile.

docker build -t abalone-age-prediction-fastapi .

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

Теперь попробуем сделать контейнер из образа «abalone-age-prediction-fastapi» и запустить его.

docker run -d --name abalone-container-v1 -p 8082:80 abalone-age
-prediction-fastapi

«8082:80» предназначен для настройки порта, который будет использоваться, 80 — это порт для контейнера докера, а 8082 — это порт нашего локального компьютера, который будет прослушивать или привязываться к «abalone-container-v1». . Я выбираю 8082, потому что в настоящее время он свободен на моем компьютере. Вы можете выбрать любой другой порт, просто убедитесь, что он в настоящее время не используется другим приложением.

Хорошо, теперь посмотрим, как выглядит приложение при запуске через докер. Просто посетите http://localhost:your_port, в моем случае это http://localhost:8082.

На терминале используйте команду «docker ps», чтобы увидеть все активные/работающие контейнеры в нашей системе Docker.

Следующим шагом является развертывание контейнера приложения в Heroku, остановите локальный контейнер с помощью команды «docker stop your-container-name».

3. Разверните наш веб-сайт в Heroku.

Существуют различные способы развертывания приложения в Heroku через Docker. В этой статье я использую Github для хранения файла приложения, который будет связан с Heroku.

Первым делом запустите «git init» на терминале my_project для инициализации нашего проекта, который мы отправим в Heroku.

По-прежнему на терминале my_project создайте файл «heroku.yml», который расскажет Heroku о том, как собрать приложение. Вот код файла Heroku.yml.

build:
  docker:
    web: Dockerfile

Затем создайте файл .gitignore, чтобы перечислить все файлы или папки, которые Heroku не нужны для создания приложения.

Запустите эту команду на терминале my_project, чтобы зафиксировать все файлы.

git add .
git commit -m "files for heroku deploy via docker"

Теперь убедитесь, что на вашем компьютере установлена ​​учетная запись Heroku и интерфейс командной строки Heroku. Запустите «вход в Heroku» на терминале, это откроет новую вкладку для входа в нашу учетную запись Heroku.

После входа в систему давайте создадим наш проект Heroku с помощью терминала с помощью этой команды. Вы должны использовать уникальное имя, доступное на Heroku.

 heroku create abaloge-age-predcition

Упс, я только что понял. Чтобы создать приложение (даже бесплатный уровень), мне нужно добавить номер кредитной карты для подтверждения платежа Heroku. У меня сейчас нет кредитной карты, но в следующий раз я найду другой способ использовать ее.

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

Все файлы этого проекта можно увидеть в Github Repo.