ML на основе нейросетей открывает для программного обеспечения новые возможности в области логического вывода. Как правило, ML-модели выполняются в облаке, а это означает, что для классификации или прогнозирования необходимо отправить данные по сети внешнему сервису.
Производительность таких решений сильно зависит от пропускной способности сети и задержки. Кроме того, отправка данных внешнему сервису может привести к проблемам с конфиденциальностью. В этой статье демонстрируется возможность переноса ИИ из облачной среды на периферию. Чтобы продемонстрировать ML с использованием периферийных ресурсов, мы будем использовать API-интерфейсы Arm NN для классификации изображений мусора с веб-камеры, подключённой к компьютеру Raspberry Pi, который покажет результаты классификации.
Arm NN и Arm Compute Library – это библиотеки с открытым исходным кодом для оптимизации машинного обучения на процессорах Arm и оконечных устройствах Интернета вещей. Рабочие вычисления машинного обучения могут выполняться целиком на оконечных устройствах, что позволяет сложному программному обеспечению с поддержкой ИИ работать практически где угодно, даже без доступа к сети.
На практике это означает, что вы можете обучать свои модели, используя самые популярные платформы машинного обучения. Затем можно загружать модели и оптимизировать их для быстрого получения логических выводов на своём оконечном устройстве, устраняя проблемы с сетью и конфиденциальностью.
Что нам нужно для создания устройства и приложения?
- Устройство Raspberry Pi. Урок проверен на устройствах Pi 2, Pi 3 и Pi 4 модели B.
- Карту MicroSD.
- Модуль камеры USB или MIPI для Raspberry Pi.
- Чтобы создать собственную библиотеку Arm NN, также потребуется хост-компьютер Linux или компьютер с установленной виртуальной средой Linux.
- Стекло, бумагу, картон, пластик, металл или любой другой мусор, который Raspberry Pi поможет вам классифицировать.
Конфигурация устройства
Я использовал Raspberry Pi 4 модель B с четырёхъядерным процессором ARM Cortex A72, встроенной памятью объёмом 1 ГБ, картой MicroSD объёмом 8 ГБ, аппаратным ключом WiFi и USB-камерой (Microsoft HD-3000).
Перед включением устройства необходимо установить ОС Raspbian на карту MicroSD, как описано в руководстве по настройке Raspberry Pi. Чтобы облегчить установку, я использовал установщик NOOBS.
Затем я загрузил Raspberry Pi, сконфигурировал Raspbian и настроил систему удалённого доступа к рабочему столу VNC для удалённого доступа к моему компьютеру Raspberry Pi. Подробные инструкции по настройке VNC можно найти на странице VNC (Virtual Network Computing) сайта RaspberryPi.org.
После настройки оборудования я занялся программным обеспечением. Данное решение состоит из трёх компонентов:
- camera.hpp реализует вспомогательные методы захвата изображений с веб-камеры;
- ml.hpp содержит методы загрузки модели машинного обучения и классификации изображений мусора на основе входных данных с камеры;
- main.cpp содержит метод main, который объединяет указанные выше компоненты. Это точка входа в приложение.
Все эти компоненты обсуждаются ниже. Всё, что вы видите здесь, было создано с помощью редактора Geany, который по умолчанию устанавливается вместе с ОС Raspbian.
Настройка камеры
Для получения изображений с веб-камеры я использовал библиотеку OpenCV с открытым исходным кодом для компьютерного зрения. Эта библиотека предоставляет удобный интерфейс для захвата и обработки изображений. Один и тот же API-интерфейс легко использовать для различных приложений и устройств, от Интернета вещей до мобильных устройств и настольных ПК.
Самый простой способ включить OpenCV в свои приложения Raspbian для Интернета вещей – установить пакет libopencv-dev с помощью программы apt-get:
sudo apt-get update
sudo apt-get upgrade
sudo apt-get install libopencv-dev
После загрузки и установки пакетов можно приступать к захвату изображений с веб-камеры. Я начал с реализации двух методов: grabFrame и showFrame (см. camera.hpp в сопутствующем коде):
Mat grabFrame()
{
// Open default camera
VideoCapture cap(0);
// If camera was open, get the frame
Mat frame;
if (cap.isOpened())
{
cap >> frame;
imwrite("image.jpg", frame);
}
else
{
printf("No valid camera\n");
}
return frame;
}
void showFrame(Mat frame)
{
if (!frame.empty())
{
imshow("Image", frame);
waitKey(0);
}
}
Первый метод, grabFrame, открывает веб-камеру по умолчанию (индекс 0) и захватывает один кадр. Обратите внимание, что интерфейс C++ OpenCV для представления изображений использует класс Mat, поэтому grabFrame возвращает объекты этого типа. Доступ к необработанным данным изображения можно получить, считав элемент данных класса Mat.
Второй метод, showFrame, используется для отображения захваченного изображения. С этой целью в showFrame для создания окна, в котором отображается изображение, используется метод imshow из библиотеки OpenCV. Затем вызывается метод waitKey, чтобы окно изображения отображалось до тех пор, пока пользователь не нажмёт клавишу на клавиатуре. Время ожидания было указано с помощью параметра waitKey. Здесь использовалось бесконечное время ожидания, представленное значением 0.
Для тестирования указанных выше методов я вызвал их в методе main (main.cpp):
int main()
{
// Grab image
Mat frame = grabFrame();
// Display image
showFrame(frame);
return 0;
}
Для создания приложения я использовал команду g++ и связал библиотеки OpenCV посредством pkg-config:
g++ main.cpp -o trashClassifier 'pkg-config --cflags --libs opencv'
После этого я запустил приложение, чтобы захватить одно изображение:
Набор данных о мусоре и обучение модели
Модель классификации TensorFlow была обучена на основе набора данных, созданного Гэри Тунгом (Gary Thung) и доступного в его репозитории Github trashnet.
При обучении модели я следовал процедуре из учебного руководства по классификации изображений TensorFlow. Однако я обучал модель на изображениях размером 256?192 пикселя – это половина ширины и высоты исходных изображений из набора данных о мусоре. Вот из чего состоит наша модель:
# Building the model
model = Sequential()
# 3 convolutional layers
model.add(Conv2D(32, (3, 3), input_shape = (IMG_HEIGHT, IMG_WIDTH, 3)))
model.add(Activation("relu"))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Conv2D(64, (3, 3)))
model.add(Activation("relu"))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Conv2D(64, (3, 3)))
model.add(Activation("relu"))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.25))
# 2 hidden layers
model.add(Flatten())
model.add(Dense(128))
model.add(Activation("relu"))
model.add(Dense(128))
model.add(Activation("relu"))
# The output layer with 6 neurons, for 6 classes
model.add(Dense(6))
model.add(Activation("softmax"))
Модель достигла точности около 83 %. С помощью преобразователя tf.lite.TFLiteConverter мы преобразовали её в формат TensorFlow Lite – trash_model.tflite.
converter = tf.lite.TFLiteConverter.from_keras_model_file('model.h5')
model = converter.convert()
file = open('model.tflite' , 'wb')
file.write(model)
Настройка пакета средств разработки Arm NN
Следующий шаг – подготовка пакета средств разработки (SDK) Arm NN. При создании библиотеки Arm NN для Raspberry Pi можно последовать учебному руководству Cross-compile Arm NN and Tensorflow for the Raspberry Pi компании Arm или выполнить автоматический сценарий из репозитория Github Tool-Solutions компании Arm для кросс-компиляции пакета средств разработки. Двоичный tar-файл Arm NN 19.08 для Raspberry Pi можно найти на GitHub.
Независимо от выбранного подхода скопируйте полученный tar-файл (armnn-dist) в Raspberry Pi. В этом случае я использую VNC для передачи файлов между моим ПК для разработки и Raspberry Pi.
Затем задайте переменную среды LD_LIBRARY_PATH. Она должна указывать на подпапку
armnn/lib
в armnn-dist
:export LD_LIBRARY_PATH=/home/pi/armnn-dist/armnn/lib
Здесь я предполагаю, что armnn-dist находится в папке home/pi.
Использование Arm NN для получения логических выводов на основе машинного обучения на устройстве
Загрузка меток вывода модели
Для интерпретации выходных данных модели необходимо использовать метки вывода модели. В нашем коде мы создаём строковый вектор для хранения 6 классов.
const std::vector<std::string> modelOutputLabels = {"cardboard", "glass", "metal", "paper", "plastic", "trash"};
Загрузка и предварительная обработка входного изображения
Изображения необходимо предварительно обработать, прежде чем модель сможет использовать их в качестве входных данных. Используемый нами метод предварительной обработки зависит от платформы, модели или типа данных модели.
На входе нашей модели находится слой Conversion 2D (преобразование 2D) с идентификатором «conv2d_input». Её выход – слой активации функции Softmax с идентификатором «activation_5/Softmax». Свойства модели извлекаются с помощью Tensorboard, инструмента визуализации, предоставленного в TensorFlow для проверки модели.
const std::string inputName = "conv2d_input";
const std::string outputName = "activation_5/Softmax";
const unsigned int inputTensorWidth = 256;
const unsigned int inputTensorHeight = 192;
const unsigned int inputTensorBatchSize = 32;
const armnn::DataLayout inputTensorDataLayout = armnn::DataLayout::NHWC;
Обратите внимание, что размер пакета, используемого для обучения, равен 32, поэтому для обучения и проверки необходимо предоставить не менее 32 изображений.
Следующий код загружает и предварительно обрабатывает изображение, захватываемое камерой:
// Load and preprocess input image
const std::vector<TContainer> inputDataContainers =
{ PrepareImageTensor<uint8_t>("image.jpg" ,
inputTensorWidth, inputTensorHeight,
normParams,
inputTensorBatchSize,
inputTensorDataLayout) } ;
Вся логика, связанная с загрузкой модели машинного обучения и выполнением прогнозов, содержится в файле ml.hpp.
Создание синтаксического анализатора и загрузка сети
Следующий шаг при работе с Armn NN – создание объекта синтаксического анализатора, который будет использоваться для загрузки файла сети. В Arm NN есть синтаксические анализаторы для файлов моделей различных типов, включая TFLite, ONNX, Caffe и т. д. Синтаксические анализаторы обрабатывают создание базового графа Arm NN, поэтому вам не нужно создавать граф модели вручную.
В этом примере используется модель TFLite, поэтому мы создаём синтаксический анализатор TfLite для загрузки модели, используя указанный путь.
Наиболее важный метод в ml.hpp – это loadModelAndPredict. Сначала он создаёт синтаксический анализатор модели TensorFlow:
// Import the TensorFlow model.
// Note: use CreateNetworkFromBinaryFile for .tflite files.
armnnTfLiteParser::ITfLiteParserPtr parser =
armnnTfLiteParser::ITfLiteParser::Create();
armnn::INetworkPtr network =
parser->CreateNetworkFromBinaryFile("trash_model.tflite");
Затем вызывается метод armnnTfLiteParser::ITfLiteParser::Create, синтаксический анализатор используется для загрузки файла trash_model.tflite.
После анализа модели создаются привязки к слоям с помощью метода GetNetworkInputBindingInfo/GetNetworkOutputBindingInfo:
// Find the binding points for the input and output nodes
const size_t subgraphId = 0;
armnnTfParser::BindingPointInfo inputBindingInfo =
parser->GetNetworkInputBindingInfo(subgraphId, inputName);
armnnTfParser::BindingPointInfo outputBindingInfo =
parser->GetNetworkOutputBindingInfo(subgraphId, outputName);
Для получения выходных данных модели необходимо подготовить контейнер. Размер выходного тензора равен количеству меток вывода модели. Это реализовано следующим образом:
// Output tensor size is equal to the number of model output labels
const unsigned int outputNumElements = modelOutputLabels.size();
std::vector<TContainer> outputDataContainers = { std::vector<uint8_t>(outputNumElements)};
Выбор внутренних интерфейсов, создание среды выполнения и оптимизация модели
Необходимо оптимизировать сеть и загрузить её на вычислительное устройство. Пакет средств разработки Arm NN поддерживает внутренние интерфейсы оптимизированного выполнения на центральных процессорах Arm, графических процессорах Mali и устройствах DSP. Внутренние интерфейсы идентифицируются строкой, которая должна быть уникальной для всех выходных интерфейсов. Можно указать один или несколько внутренних интерфейсов в порядке предпочтения.
В нашем коде Arm NN определяет, какие уровни поддерживаются внутренним интерфейсом. Сначала проверяется центральный процессор. Если один или несколько уровней невозможно выполнить на центральном процессоре, сначала осуществляется возврат к эталонной реализации.
Указав список внутренних интерфейсов, можно создать среду выполнения и оптимизировать сеть в контексте среды выполнения. Внутренние интерфейсы могут выбрать реализацию оптимизаций, характерных для внутренних интерфейсов. Arm NN разбивает граф на подграфы на основе внутренних интерфейсов, вызывает функцию оптимизации подграфов для каждого из них и заменяет соответствующий подграф в исходном графе его оптимизированной версией, когда это возможно.
Как только это сделано, LoadNetwork создаёт характерные для внутреннего интерфейса рабочие нагрузки для слоёв, а также характерную для внутреннего интерфейса фабрику рабочих нагрузок и вызывает её для создания рабочих нагрузок. Входное изображение сворачивается с тензором типа const и ограничивается входным тензором.
// Optimize the network for a specific runtime compute
// device, e.g. CpuAcc, GpuAcc
armnn::IRuntime::CreationOptions options;
armnn::IRuntimePtr runtime = armnn::IRuntime::Create(options);
armnn::IOptimizedNetworkPtr optNet = armnn::Optimize(*network,
{armnn::Compute::CpuAcc, armnn::Compute::CpuRef},
runtime->GetDeviceSpec());
Механизм логического вывода в пакете Arm NN SDK предоставляет мост между существующими платформами нейронных сетей и центральными процессорами Arm Cortex-A, графическими процессорами Arm Mali и устройствами DSP. При получении логических выводов на основе машинного обучения с помощью Arm NN SDK алгоритмы машинного обучения оптимизируются для используемого оборудования.
После оптимизации сеть загружается в среду выполнения:
// Load the optimized network onto the runtime device
armnn::NetworkId networkIdentifier;
runtime->LoadNetwork(networkIdentifier, std::move(optNet));
Затем выполните прогнозы с помощью метода EnqueueWorkload:
// Predict
armnn::Status ret = runtime->EnqueueWorkload(networkIdentifier,
armnnUtils::MakeInputTensors(inputBindings, inputDataContainers),
armnnUtils::MakeOutputTensors(outputBindings, outputDataContainers));
На последнем шаге получаем результат прогнозирования.
std::vector<uint8_t> output = boost::get<std::vector<uint8_t>>(outputDataContainers[0]);
size_t labelInd = std::distance(output.begin(), std::max_element(output.begin(), output.end()));
std::cout << "Prediction: ";
std::cout << modelOutputLabels[labelInd] << std::endl;
В приведённом выше примере единственный аспект, который связан с платформой машинного обучения, – это та часть, в которой загружается модель и настраиваются привязки. Всё остальное не зависит от платформы машинного обучения. Таким образом, вы можете легко переключаться между различными моделями машинного обучения без изменения других частей своего приложения.
Объединение всех компонентов и создание приложения
Наконец, я собрал все компоненты вместе в методе main (main.cpp):
#include "camera.hpp"
#include "ml.hpp"
int main()
{
// Grab frame from the camera
grabFrame(true);
// Load ML model and predict
loadModelAndPredict();
return 0;
}
Обратите внимание, что у метода grabFrame есть дополнительный параметр. Если этот параметр имеет значение true, изображение камеры преобразуется в оттенки серого с изменением размеров до 256?192 пикселей в соответствии с входным форматом модели машинного обучения, а затем преобразованное изображение передаётся методу loadModelAndPredict.
Для создания приложения требуется использовать команду g++ и связать библиотеку OpenCV и пакет Arm NN SDK:
g++ main.cpp -o trashClassifier 'pkg-config --cflags --libs opencv' -I/home/pi/armnn-dist/armnn/include -I/home/pi/armnn-dist/boost/include -L/home/pi/armnn-dist/armnn/lib -larmnn -lpthread -linferenceTest -lboost_system -lboost_filesystem -lboost_program_options -larmnnTfLiteParser -lprotobuf
Опять же, я предполагаю, что пакет Arm NN SDK находится в папке home/pi/armnn-dist. Запустите приложение и сделайте снимок какого-нибудь картона.
pi@raspberrypi:~/ $ ./trashClassifier
ArmNN v20190800
Running network...
Prediction: cardboard
Если во время выполнения приложения отображается сообщение «error while loading shared libraries: libarmnn.so: cannot open shared object file: No such file or directory» (ошибка при загрузке общих библиотек: libarmnn.so, не удаётся открыть общий объектный файл: нет такого файла или каталога), убедитесь, что ваша переменная среды LD_LIBRARY_PATH задана правильно.
Данное приложение также можно улучшить, реализовав запуск захвата и распознавания изображений по внешнему сигналу. Для этого требуется изменить метод loadAndPredict в модуле ml.hpp и отделить загрузку модели от прогнозирования (логического вывода). А если хотите прокачать себя в Data Science, Machine Learning или поднять уровень уже имеющихся знаний — приходите учиться, будет сложно, но интересно.
Узнайте подробности, как получить Level Up по навыкам и зарплате или востребованную профессию с нуля, пройдя онлайн-курсы SkillFactory со скидкой 40% и промокодом HABR, который даст еще +10% скидки на обучение:
- Профессия Data Scientist
- Профессия Data Analyst
- Курс по Data Engineering
- Другие профессии и курсыПРОФЕССИИ
- Профессия Java-разработчик
- Профессия QA-инженер на JAVA
- Профессия Frontend-разработчик
- Профессия Этичный хакер
- Профессия C++ разработчик
- Профессия Разработчик игр на Unity
- Профессия Веб-разработчик
- Профессия iOS-разработчик с нуля
- Профессия Android-разработчик с нуля
КУРСЫ
- Курс по Machine Learning
- Курс «Математика и Machine Learning для Data Science»
- Курс «Machine Learning и Deep Learning»
- Курс «Python для веб-разработки»
- Курс «Алгоритмы и структуры данных»
- Курс по аналитике данных
- Курс по DevOps