Программа магистратуры в области электротехники и вычислительной техники
Факультет вычислительной техники и автоматизации
EEC1509 — Машинное обучение

Этот репозиторий представляет собой набор тестов для навыков, полученных на уроке Основы машинного обучения и деревья решений, для развертывания модели классификации в общедоступном наборе данных MNIST.

Основная цель данной работы — развернуть модель с помощью модуля FastAPI, создать API и тесты. Тесты API будут включены в структуру CI/CD с использованием GitHub Actions. Живой API будет развернут с помощью Heroku. Веса и смещения будут использоваться для управления и отслеживания всех артефактов.

Настройка среды

Все необходимые модули можно найти в файле requirements.txt и установить, выполнив:

pip install -r requirements.txt

Набор данных

База данных рукописных цифр MNIST имеет обучающий набор из 60 000 примеров и тестовый набор из 10 000 примеров. Цифры были нормализованы по размеру и центрированы на изображении фиксированного размера. Это хорошая база данных для людей, которые хотят попробовать методы обучения и методы распознавания образов на реальных данных, затрачивая минимум усилий на предварительную обработку и форматирование.

Разработка модели

Самые сложные части были сделаны за нас благодаря библиотеке Keras и Национальному институту стандартов и технологий (NIST of MNIST). Информация собрана и готова к обработке.

from tensorflow.keras.datasets import mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()

Разработанная здесь модель не принимает матрицы 28x28, предоставляемые модулем Keras, поэтому требуется выравнивание данных. Этот процесс не идеален, поскольку он может скрыть информацию о соседних пикселях, но его достаточно для построения базовой модели.

Затем полученные данные будут сглажены для получения нескольких векторов длиной 784 (28 * 28):

def reshape(array: np.array) -> np.array:
    """The samples in the input array are faltered."""
    samples, w, h = array.shape
    return array.reshape((samples, w * h))

x_train = reshape(x_train)
x_test = reshape(x_test)

Примеры распределений в исследованных наборах изображены ниже:

Модель классификации, в данном случае Дерево решений, затем может быть применена к данным следующим образом:

clf = DecisionTreeClassifier(max_depth=10, random_state=42)
clf.fit(x_train, y_train)

Чтобы следить за эффективностью экспериментов по машинному обучению, проект пометил определенные выходные данные этапа конвейера данных как метрики. Принятые здесь метрики: точность, f1, точность, полнота. Результаты для обученной модели показаны ниже.

|              | Precision | Recall | F1-Score | Samples |
| ------------ | --------- | ------ | -------- | ------- |
|       0      |    0.91   |  0.94  |   0.92   |   980   |
|       1      |    0.95   |  0.96  |   0.95   |   1135  |
|       2      |    0.85   |  0.84  |   0.84   |   1032  |
|       3      |    0.82   |  0.84  |   0.83   |   1010  |
|       4      |    0.86   |  0.85  |   0.86   |   982   |
|       5      |    0.84   |  0.80  |   0.82   |   892   |
|       6      |    0.91   |  0.87  |   0.89   |   958   |
|       7      |    0.90   |  0.88  |   0.89   |   1028  |
|       8      |    0.80   |  0.81  |   0.80   |   974   |
|       9      |    0.81   |  0.86  |   0.83   |   1009  |
|   accuracy   |           |        |   0.87   |  10000  |
|   macro avg  |    0.87   |  0.86  |   0.86   |  10000  |
| weighted avg |    0.87   |  0.87  |   0.87   |  10000  |

Матрица путаницы классификаций модели показана ниже.

Эта базовая модель может быть улучшена в будущем, а модель логического вывода сохранена на платформе Weights & Biases для замены в реальном времени в окончательном приложении.

Сохранение модели в Weights & Biases

Используя следующий код, этот тренировочный прогон можно сохранить на платформе Weights & Biases для отслеживания эксперимента:

run = wandb.init(project='proj_mnist', job_type='train')
print('Evaluating Model...')
fbeta = fbeta_score(y_test, y_pred, beta=1, zero_division=1, average='weighted')
precision = precision_score(y_test, y_pred, zero_division=1, average='weighted')
recall = recall_score(y_test, y_pred, zero_division=1, average='weighted')
acc = accuracy_score(y_test, y_pred)
print(f'Accuracy: {acc}')
print(f'Precision: {precision}')
print(f'Recall: {recall}')
print(f'F1: {fbeta}')
run.summary['Acc'] = acc
run.summary['Precision'] = precision
run.summary['Recall'] = recall
run.summary['F1'] = fbeta
print('Uploading confusion matrix...')
run.log({
    'confusion_matrix': wandb.Image(fig)
})
wandb.sklearn.plot_classifier(
    clf, 
    x_train, x_test,
    y_train, y_test, y_pred,
    clf.predict_proba(x_test),
    np.unique(y_train), 
    model_name='Baseline_model'
)
wandb.sklearn.plot_summary_metrics(
    clf,
    x_train, y_train,
    x_test, y_test
)
# ROC curve
predict_proba = clf.predict_proba(x_test)
wandb.sklearn.plot_roc(y_test, predict_proba, np.unique(y_train))
run.finish()

Затем модель логического вывода можно сохранить на платформе и позже получить через API.

run = wandb.init(project='proj_mnist', job_type='model')
artifact_model = 'model_export'
print('Dumping model to disk...')
joblib.dump(clf, artifact_model)
artifact = wandb.Artifact(
    artifact_model,
    type='inference_artifact',
    description='Model for inference'
)
print('Logging model artifact...')
artifact.add_file(artifact_model)
run.log_artifact(artifact)
run.finish()

Введение в FastAPI

FastAPI – это современная платформа API, возможности которой в значительной степени зависят от подсказок типов.

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

Начать работу так же просто, как написать файл main.py, содержащий:

from fastapi import FastAPI
# Instantiate the app.
app = FastAPI()
# Define a GET on the specified endpoint.
@app.get('/')
async def say_hello():
    return {'greeting': 'Hello World!'}

Для запуска приложения можно использовать uvicorn в оболочке: uvicorn source.main:app --reload.

Uvicorn — это реализация веб-сервера ASGI (интерфейс асинхронного шлюза сервера) для Python.

По умолчанию наше приложение будет доступно локально по адресу http://127.0.0.1:8000. --reload позволяет вам вносить изменения в свой код и мгновенно развертывать их без перезапуска uvicorn. Для дальнейшего чтения отлично написана Документация по FastAPI, ознакомьтесь с ней!

Когда пользователь делает запрос на получение в корневом маршруте для этого проекта, будет отображаться статическая страница. FastAPI уже поддерживает этот вариант использования, и пример кода можно изменить, чтобы он соответствовал следующим требованиям:

from fastapi.staticfiles import StaticFiles
from fastapi import FastAPI
# Instantiate the app.
app = FastAPI()
# Servers a static page for requests to the root endpoint.
app.mount('/', StaticFiles(directory='./source/static', html = True), name='static')

Домашняя страница приложения показана ниже.

С помощью декоратора app.post в API можно включить новый маршрут POST, который будет получать захват изображения числа, нарисованного пользователем на холсте, и возвращать вероятности классификации для каждого класса.

Платформа Weights & Biases будет использоваться для получения модели классификации. Это может быть конкретная версия самой последней версии, обеспечивающая постоянную интеграцию любых изменений в модель в приложение.

artifact_model_name = 'proj_mnist/model_export:latest'
run = wandb.init(project='proj_mnist', job_type='api')
# Instantiate the app.
app = FastAPI()
# Define a GET on the specified endpoint.
@app.post('/predict/')
async def predict(drawing_data: str) -> dict:
    """
        Receive the base64 encoding of an image 
        containing a handwritten digit and make 
        predictions using an ml model.
        
        Args:
            drawing_data (str): Base64 encoding of an image.
       Returns:
            dict: JSON response containing the predictions
    """
    # Convert data in url to numpy array
    img_str = re.search(r'base64,(.*)', drawing_data.replace(' ', '+')).group(1)
    img_bytes = io.BytesIO(base64.b64decode(img_str))
    img = Image.open(img_bytes)
    
    # Normalize pixel values
    input = np.array(img)[:, :, 0:1].reshape((1, 28*28)) / 255.0
model_export_path = run.use_artifact(artifact_model_name).file()
    clf = joblib.load(model_export_path)
predictions = clf.predict_proba(input)[0]
    
    return { 
        'result': 1,
        'error': '',
        'data': list(predictions)
    }
app.mount('/', StaticFiles(directory='./source/static', html = True), name='static')

С ответом прогнозируемого маршрута можно отобразить график, отображающий вероятности для каждого прогноза.

Развертывание в Heroku

Чтобы развернуть разработанный код на Heroku, выполните следующие действия.

  1. Зарегистрируйтесь бесплатно и испытайте Heroku.
  2. Теперь пришло время создать новое приложение. Очень важно подключить приложение к нашему репозиторию Github и включить автоматическое развертывание.
  3. Установите Heroku CLI, следуя инструкциям.
  4. Войдите в героку с помощью терминала
heroku login

5. В корневой папке проекта проверьте уже созданные проекты heroku.

heroku apps

6. Проверьте правильность сборки:

heroku buildpacks --app proj-mnist

7. При необходимости обновите билдпак:

heroku buildpacks:set heroku/python --app proj-mnist

8. Когда вы запускаете сценарий в автоматизированной среде, вы можете управлять Wandb с помощью переменных среды, установленных до запуска сценария или внутри него. Настройте доступ к Wandb на Heroku, если используете интерфейс командной строки:

heroku config:set WANDB_API_KEY=xxx --app proj-mnist

9. Инструкции по запуску приложения содержатся в файле Procfile, который находится на самом высоком уровне каталога вашего проекта. Создайте файл Procfile с помощью:

web: uvicorn source.api.main:app --host=0.0.0.0 --port=${PORT:-5000}

10. Настройте удаленный репозиторий для Heroku:

heroku git:remote --app proj-mnist

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

git push heroku main

12. Проверьте запуск удаленных файлов:

heroku run bash --app proj-mnist

13. Если все предыдущие шаги были выполнены успешно, после открытия вы увидите сообщение ниже: https://proj-mnist.herokuapp.com/.

14. В целях отладки всякий раз, когда вы можете получить самые последние журналы вашего приложения, используйте команду heroku logs:

heroku logs

Первоначально опубликовано на https://github.com/xarmison/proj_mnist