Кому нужен джин в бутылке, если с Flask можно делать прогнозы?

Автор сценария: Магнус Нермарк, Альф Кальмар и Адриан Сандакер.

Вы когда-нибудь хотели начать заниматься машинным обучением? В наши дни он используется почти повсюду, и потенциальные возможности применения кажутся почти бесконечными. Однако иногда бывает сложно сделать первый шаг в новый технологический ландшафт.

Машинное обучение - обширная и часто сложная область, но, к счастью, начать ее использовать совсем не сложно!

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

Если вам уже интересен результат, вы можете посмотреть демоверсию приложения по ссылке ниже. Модель, используемая для классификации изображений, обучена распознавать объекты, поэтому попробуйте направить камеру на объект во время тестирования. Вы также должны убедиться, что никакие другие приложения не используют вашу камеру перед запуском демонстрации, иначе браузер не сможет получить видеопоток.

Демо: https://react-flask-keras-app.herokuapp.com/

Примечание.. Загрузка приложения по указанной выше ссылке может занять минуту, так как оно переходит в спящий режим через час без трафика. Также стоит отметить, что версия, запущенная по указанной выше ссылке, использует другую модель для классификации (MobileNetV2), чем приведенный ниже код (ResNet50), из-за ограничений памяти на платформе бесплатного хостинга. Однако общий опыт остается прежним, и модель, используемая ниже, должна обеспечить еще лучшие результаты! ⚡

Схема проекта

Давайте быстро посмотрим, как будет создано приложение.

На бэкэнде мы будем использовать облегченный фреймворк Python Flask в сочетании с мощной библиотекой машинного обучения Keras, чтобы настроить простой REST API, способный классифицировать изображения. Чтобы упростить задачу, мы будем использовать предварительно обученную модель машинного обучения. Это избавляет нас от большого количества работы, поскольку нам не нужно самостоятельно подготавливать данные и обучать модель.

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

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

Приложение будет иметь следующую структуру:

project-folder 
│
└───backend
│   │   app.py
│   │   classifier.py
│   │   reverseProxy.py
│
└───frontend
│   │   .env
│   │   package.json
│   │   ...
│   └───src
│       │   App.js
│       │   ...
│       └─── classifier
│            │   index.jsx

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

Если вы хотите продолжить, у вас должно быть установлено следующее программное обеспечение:

  • Python (64-разрядная версия) версии от 3.5 до 3.8.
  • Версия узла 10.14 или выше.

Вы также должны быть знакомы с pip и npm. Имея некоторый опыт работы с Python и React, вам будет очень полезно, но, надеюсь, вы сможете следовать за ним независимо от вашего уровня опыта 👩‍💻👨‍💻

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

Настройка простого приложения Flask:

Первое, что нам нужно сделать, это установить в наш проект библиотеку Flask.

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

Начните с создания каталога для хранения файлов проекта.

Откройте этот каталог в терминале и выполните следующую команду

pip install flask

Теперь мы готовы создать наше приложение Flask.

Создайте папку с именем «backend» в каталоге проекта и создайте внутри файл с именем app.py со следующим содержимым:

# backend/app.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
    return "Hello from FLASK"

Приведенный выше код - это все, что нам нужно для создания очень простого приложения Flask. Рекомендуется запустить его сейчас, чтобы убедиться, что все работает правильно.

Снова откройте терминал и введите flask run.

Вот и все!

Теперь ваше приложение flask готово и ожидает по адресу http: // localhost: 5000 /.

Когда бэкэнд запущен и работает, давайте теперь обратим наше внимание на интерфейс.

Настройка внешнего интерфейса

Для простоты мы будем использовать Create React App для создания нашего внешнего интерфейса. Нет смысла каждый раз изобретать велосипед, и это избавит нас от большого количества лишней работы.

Перейдите в корневую папку проекта и выполните приведенную ниже команду.

npx create-react-app frontend

При использовании npx нам не нужно устанавливать приложение create-response-app локально, так как node будет извлекать и выполнять пакет из реестра npm.

Вышеупомянутая команда создаст новое приложение React внутри каталога с именем «frontend » рядом с внутренним каталогом. В зависимости от вашей системы этот процесс может занять несколько минут.

Когда приложение create-response-app завершает свою работу, перейдите во вновь созданную папку внешнего интерфейса и запустите приложение, чтобы убедиться, что все работает.

npm start

Теперь вы сможете найти свое свежее новое приложение для внешнего интерфейса, работающее через webpack-dev-server по адресу http: // localhost: 3000.

Круто, интерфейсное приложение живо!

На данный момент серверная часть работает на порту 5000, а интерфейсная часть - на порту 3000.

В дальнейшем было бы неплохо получать доступ к приложению только через наш сервер Flask и вместо этого обслуживать любые статические файлы с webpack-dev-server, перенаправляя их через Flask. Это не строго необходимо для создания нашего приложения, но улучшит общий опыт разработчика, а также поможет упростить взаимодействие между интерфейсом и серверной частью.

Прежде чем мы это сделаем, давайте сначала установим некоторые переменные среды, чтобы подготовить наше веб-приложение для обслуживания через Flask.

Создайте файл с именем .env в корне каталога внешнего интерфейса и вставьте следующие две строки:

WDS_SOCKET_PORT=3000
BROWSER=none

Первая строка указывает webpack-dev-серверу уведомлять порт 3000 о горячей перезагрузке обновлений вместо стандартного window.location.port.

Это позволяет выполнять горячую перезагрузку при запуске приложения через flask на другом порту, поскольку позволяет webpack-dev-серверу уведомлять сам о любых изменениях кода и запускать обновление приложения.

Вторая строка просто гарантирует, что новое окно браузера не откроется при запуске webpack-dev-server.

Перезапустите приложение, чтобы убедиться, что файл .env используется. Если новое окно браузера не запускается, все должно быть в порядке.

На этом этапе мы создали backend-сервер, который возвращает простой ответ при просмотре, и frontend-сервер, отображающий простой компонент React.

Теперь мы их соединим!

Загрузка компонента React через Flask

Как упоминалось выше, мы хотим, чтобы наш Flask-сервер был основной точкой входа при разработке приложения. Итак, как мы можем заставить Flask-server взаимодействовать с webpack-dev-server для получения статических файлов и контента во время разработки?

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

Начните с установки пакета «запросы» с помощью pip

pip install requests

Затем создайте новый файл с именем reverseProxy.py внутри нашей внутренней папки и добавьте следующее содержимое:

Приведенный выше код был первоначально разработан Эндрю Хайндманом. Вся заслуга в этом прекрасном простом решении принадлежит ему!

Каждый раз, когда вызывается функция proxyRequest, она открывает новый запрос к указанному хосту и пути и возвращает результат в качестве ответа.

Следующий шаг - вернуться в наше приложение Flask и перенаправить любые запросы по нашему корневому пути на webpack-dev-server.

Для этого нам нужно немного расширить наш код в файле app.py.

И так, что здесь происходит?

Чтобы упростить разработку, мы позволим приложению работать в двух разных режимах. Мы используем переменную среды под названием FLASK_ENV, чтобы указать, какой режим следует использовать.

Если значение FLASK_ENV установлено на «разработка», все запросы, поступающие в приложение, теперь будут перенаправляться через обратный прокси-сервер. Затем прокси получит запрошенный контент и файлы с webpack-dev-server, а затем вернет все через сервер Flask на порт 5000.

Если режим не указан, сервер будет использовать структуру шаблона по умолчанию во Flask и обслуживать статические ресурсы из папки static. По сути, это будет «производственный» -режим.

Теперь вам может быть интересно - как мы используем переменную среды FLASK_ENV?

Что ж, теперь пора добавить нашу конфигурацию для разработки и производства в наш файл package.json, расположенный внутри папки внешнего интерфейса. Таким образом, нам не нужно записывать каждую команду каждый раз, когда мы хотим запустить приложение.

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

Вернитесь в папку внешнего интерфейса и выполните следующую команду:

npm install --save-dev cross-env concurrently

Затем измените файл package.json с помощью следующих скриптов:

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

Последним шагом на этом этапе является повторный запуск приложения с использованием нового сценария.

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

npm run start:server-dev

Теперь вы должны увидеть, как Flask-server и webpack-dev-server запускаются бок о бок в одном сеансе терминала.

И вуаля! Теперь наше приложение должно быть доступно по адресу http: // localhost: 5000 /.

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

Теперь, когда интерфейс и серверная часть связаны, давайте поработаем над машинным обучением!

Всемогущий классификатор

Теперь мы создадим конечную точку в нашем приложении Flask, которая может получать изображения и возвращать классификацию. Для этого мы собираемся использовать Keras, который представляет собой фреймворк машинного обучения (ML) на Python. Модель классификатора будет создана с использованием предварительно обученной модели машинного обучения, созданной с помощью ResNet50 и ImageNet.

Сначала установите необходимые пакеты:

pip install tensorflow
pip install pillow

Затем создайте файл с именем classifier.py в внутренней папке. Импортируйте необходимые пакеты и инициируйте модель со следующими строками.

Получение прогноза

Теперь у нас есть модель, которую мы можем использовать для получения прогнозов.

Модель ожидает, что входные данные (изображения) будут в следующем формате: (224, 224, 3). Это означает, что изображение необходимо предварительно обработать, чтобы модель могла его правильно прочитать. Когда это будет сделано, мы можем использовать прогнозируемый метод, доступный в нашей модели, чтобы получить прогноз.

Что такое прогноз

Итак, теперь у нас есть модель, способная делать прогнозы на изображениях. Аккуратный!

Модель возвращает прогноз в виде огромной матрицы, которая, к сожалению, сама по себе не имеет особого смысла. Чтобы люди могли понять предсказание, сначала необходимо декодировать матрицу предсказания. Используя метко названную вспомогательную функцию decode_predictions от Keras, мы можем легко это сделать!

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

Создайте Flask API

Теперь мы создадим конечную точку на сервере Flask, способную получать изображения и возвращать прогноз.

Сначала импортируйте только что созданный модуль классификатора в файл app.py:

from classifier import classifyImage

Затем создайте маршрут с именем /classify. Это путь, по которому API будет получать изображения из внешнего интерфейса.

Внутри маршрута мы получаем изображение из запроса, прежде чем передать его нашей модели для прогноза.

Вы также должны добавить метод «запроса» к импорту из Flask в верхней части файла.

from flask import Flask, render_template, request

Вот и все!

Теперь у нас есть API, способный получать изображения и возвращать прогноз от ResNet50.

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

Вернемся к интерфейсу! 🚀

Создание интерфейса для нашего классификатора

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

Все функции будут заключены в один компонент React.

Компонент классификатора должен будет сделать следующее:

  • Отображение прямой видеотрансляции с камеры нашего устройства
  • Делайте снимки через равные промежутки времени
  • Отправьте изображение на классификацию и покажите результат

Это может показаться большим трудом, но благодаря магии современных веб-API это на самом деле не так уж сложно. Все необходимое доступно прямо здесь, в браузере.

Начните с создания нового файла для компонента классификатора и настройте внутри базовый каркас компонента.

В настоящее время компонент просто возвращает тег заголовка и пустой div, в котором мы будем создавать пользовательский интерфейс. Ничего особенного здесь пока не происходит.

Мы также настроили несколько ref-объектов вверху, которые будут использоваться для ссылки на элементы HTML DOM позже. Мы также собираемся хранить изображения, которые мы снимаем, в imageRef вместо использования состояния компонента. Процесс захвата изображения будет происходить в фоновом режиме, и мы хотим выполнить повторный рендеринг только тогда, когда у нас есть новый результат классификации изображений для представления.

Наконец, мы также подготовили несколько хуков useEffect на будущее.

Прежде чем двигаться дальше, импортируйте и визуализируйте компонент где-нибудь в приложении. Хорошее место внутри основного файла App.js.

Примечание. Вам нужно будет обновить путь импорта в строке 2, если ваш путь отличается от того, который использовался здесь. В приведенном выше коде предполагается, что ваш компонент находится по пути frontend/src/classifier/index.jsx.

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

Отображение видеопотока

Начните с добавления простого видеоэлемента внутри существующего div-элемента и подключите его к videoRef.

<div>
  <video ref={videoRef} />
</div>

Этот элемент видео будет отвечать за отображение канала камеры на странице.

Затем обратите внимание на самый верхний хук useEffect.

Внутри мы настроим функцию для получения доступа к камере устройства. Мы хотим запрашивать доступ к камере сразу после монтирования компонента, поэтому мы используем ловушку useEffect без чего-либо в массиве зависимостей для однократного запуска кода при монтировании компонента. Мы объявим функцию внутри хука, так как она не будет использоваться где-либо еще в компоненте.

Здесь запрос разрешения камеры отправляется пользователю путем вызова метода getUserMedia. Если этот запрос одобрен, вызов getUserMedia преобразуется в объект MediaStream, который затем устанавливается как srcObject в нашем videoRef. Последний связан с элементом ‹video›, определенным в блоке возврата, поэтому этот простой поток соединяет поток от камеры с элементом видео. Теперь нам остается только запустить видеопоток, когда видео-элемент будет готов.

Настройте для этого небольшую функцию:

const playCameraStream = () => {
  if (videoRef.current) {
    videoRef.current.play();
  }
};

Затем убедитесь, что он вызывается после срабатывания события onCanPlay на видеоэлементе.

<video ref={videoRef} onCanPlay={() => playCameraStream()} />

Это запускает поток с использованием метода воспроизведения, доступного через videoRef.

Бум - теперь у нас есть прямой видеопоток! 🎥

Снова откройте приложение, чтобы убедиться, что все работает. Если вы тестируете устройство Apple, например iPhone, вам может потребоваться добавить атрибут playsinline к видео, чтобы оно отображалось правильно.

Теперь перейдем к захвату изображений из этого видеопотока.

Захват изображения

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

Начните с добавления элемента холста в блок возврата и подключите его к canvasRef.

<canvas ref={canvasRef} hidden></canvas>

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

Давайте добавим функцию, которую мы можем вызывать каждый раз, когда мы хотим захватить новое изображение.

Начнем с получения контекста рисования холста с помощью свойства canvasRef. Затем мы берем высоту и ширину видеоэлемента. Эти значения будут использоваться для установки размеров нашего холста, чтобы изображение было нарисовано с правильным размером.

Затем мы вызываем функцию drawImage в контексте холста. Первый параметр - это изображение, которое мы хотим нарисовать. Здесь мы используем videoRef, чтобы вырвать изображение из видеопотока. Затем мы передаем координаты осей x и y того места, где мы хотим начать рисование.

Мы хотим заполнить весь холст нашим изображением, поэтому мы передаем 0 для обоих значений, чтобы начать рисование в верхнем левом углу. Наконец, мы передаем высоту и ширину, которые хотим использовать при рисовании, что опять же является высотой и шириной видео.

Затем, когда изображение нарисовано на холсте, мы вызываем функцию toBlob и сохраняем полученный большой двоичный объект в переменной imageRef, которую затем можем отправить в API для получения прогноза.

Все это мы сделаем во втором хуке useEffect, так что давайте углубимся в него.

Получение прогноза из API

Второй хук useEffect будет отвечать за создание снимка и его отправку в API каждую секунду.

Внутри крючка все будет происходить так:

Давайте пройдемся по этому коду.

Мы начинаем с вызова функции captureImageFromCamera для обновления текущего изображения, хранящегося в imageRef. Каждый раз, когда вызывается эта функция, необработанное изображение обновляется, чтобы отразить кадр видеопотока в этот момент времени.

После обновления изображения мы выполняем POST-запрос, используя fetch ​​-API, с изображением, заключенным в объект FormData в качестве тела запроса по нашему запросу. Если запрос выполнен успешно, состояние компонента обновляется с помощью прикрепленного текста прогноза. Если что-то не удается, вместо этого мы устанавливаем общее сообщение об ошибке.

Поскольку все здесь аккуратно заключено в функцию setInterval, этот процесс теперь будет происходить каждую секунду.

Теперь нам просто нужно показать результат прогноза пользователю.

Добавьте элемент абзаца, чтобы отобразить результат внутри блока возврата.

<p>Currently seeing: {result}</p>

Идите вперед и откройте приложение, и вы увидите, что классификатор постоянно обновляет результат прогноза каждый раз, когда приходит новый ответ от API.

Это почти кажется немного волшебным. ✨

Подведение итогов

…вот и все!

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

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

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

Вы также можете немного подправить интерфейс, чтобы обеспечить удобство работы с пользователем, текущий дизайн, в конце концов, немного… минимален. 😬

Есть какие-нибудь безумные идеи? Стройте их!

Это простое маленькое приложение можно расширить множеством способов. Теперь все в ваших руках.

Если вы хотите проверить полный код проекта, здесь есть репозиторий git с готовым проектом:

Https://github.com/sopra-steria-norge/react-flask-keras-app