В этой статье я опишу процесс развертывания модели машинного обучения с использованием 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.