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

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

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

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

Все необходимые модули можно найти в файле 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()

Разработанная здесь модель не принимает матрицы $28 x 28$, предоставляемые модулем 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)

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

Модель классификации, в данном случае многослойный персептрон (MLP), была построена с использованием модуля Tensorflow:

mlp = Sequential([     
    Input(shape=(28,28)),     
    Flatten(),     
    Dense(128, activation='relu'),     
    Dropout(0.15),     
    Dense(128, activation='relu'),     
    Dropout(0.1),     
    Dense(10, activation='softmax') 
])  
mlp.compile(     
    loss='categorical_crossentropy',     
    optimizer='adam',     
    metrics=['acc'] 
)

Модель классификации, в данном случае многослойный персептрон (MLP), была построена с использованием модуля Tensorflow:

Графическое представление предлагаемой модели MLP показано на рисунке ниже.

Затем модель была адаптирована к данным с использованием двух обратных вызовов: EarlyStopping, который останавливает процесс обучения, когда потери при проверке модели не улучшаются, и ReduceLROnPlateau, который отслеживает потери при проверке и снижает скорость обучения, когда она достигает плато.

history = mlp.fit(     
    x_train, y_train,     
    validation_split=0.1,     
    batch_size=64,     
    epochs=2000,     
    shuffle=True,     
    callbacks=[         
        EarlyStopping(monitor='val_loss', patience=5),             
        ReduceLROnPlateau(monitor='val_loss', factor=0.1, patience=3, min_lr=1e-7)     
] )

На рисунке ниже показаны функция потерь и значения точности предлагаемой модели классификации на этапе обучения. Функция потерь была успешно минимизирована со значениями, близкими к нулю для обучающего набора, и низкими значениями для проверочного набора, что указывает на успех на этом этапе. Кривая точности изображает аналогичный сценарий, причем обе кривые превышают 90%.

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

|     Class    | Precision | Recall | F1-Score | Samples ||:------------:|:---------:|:------:|:--------:|:-------:|
|       0      |    0.99   |  0.99  |   0.99   |   980   |
|       1      |    0.99   |  0.99  |   0.99   |   1135  |
|       2      |    0.98   |  0.98  |   0.98   |   1032  |
|       3      |    0.98   |  0.99  |   0.98   |   1010  |
|       4      |    0.99   |  0.98  |   0.98   |   982   |
|       5      |    0.99   |  0.98  |   0.98   |   892   |
|       6      |    0.99   |  0.99  |   0.99   |   958   |
|       7      |    0.98   |  0.99  |   0.98   |   1028  |
|       8      |    0.98   |  0.98  |   0.98   |   974   |
|       9      |    0.98   |  0.98  |   0.98   |   1009  |
|              |           |        |          |         |
|   Accuracy   |           |        |   0.98   |  10000  |
|   Macro avg  |    0.98   |  0.98  |   0.98   |  10000  |
| Weighted avg |    0.98   |  0.98  |   0.98   |  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) 
})   
# ROC curve 
predict_proba = mlp.predict(x_test) 
wandb.sklearn.plot_roc(y_test, predict_proba, np.unique(y_train))  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')

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

Включение маршрута вывода в API

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

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

# Define a GET on the specified endpoint. 
@app.post('/predict_nn/') 
async def predict_nn(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 neural network 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 = load_model('path')      
    predictions = [ float(pred) for pred  in model.predict(input)[0] ]          
    
     return {          
         'result': 1,         
         'error': '',         
         'data': predictions    
     }

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

Развертывание в 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

Сравнение результатов классификатора с деревом решений

Классификатор дерева решений использовался для решения той же задачи классификации в предыдущей публикации, которая доступна в разделе Использование данных MNSIT для обучения и развертывания модели классификации. В этом разделе исследуются различия между предсказаниями каждой модели.

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

MLP достиг точности 98 % по сравнению с 87 % дерева решений. Реальное влияние этого 11%-го улучшения можно увидеть при сравнении матрицы путаницы, где становится ясно, что прогнозы MLP имеют более высокое качество.

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

Рекомендации

  • Орельен Жерон. Практика машинного обучения с помощью Scikit-Learn, Keras и TensorFlow. "[Связь]"