Распознавание жестов — это технология, которая позволяет людям взаимодействовать с устройствами без физического нажатия кнопок или сенсорных экранов. Интерпретируя жесты человека, эта технология нашла свое применение в различных потребительских устройствах, включая смартфоны и игровые консоли. В основе распознавания жестов лежат два ключевых компонента: сенсор и программный алгоритм.
В этом примере используются измерения акселерометра MPU 6050 и машинное обучение (ML) для распознавания трех жестов рукой с помощью ESP32. Данные из сенсора распознаются на микроконтроллере и результат выводится в консоль в виде названия жеста и вероятности результата. Модель ML использует TensorFlow и Keras и обучается на выборке данных, представляющей три различных жеста: «circle» (окружность), «cross» (пересечение) и «pad» (поступательное движение).
Разработка проекта начнется с получения данных из акселерометра для построения набора жестов. Затем мы проектируем полносвязную нейронную сеть для распознавания жестов, и подключим модель в проекте ESP32.
В следующей части рассмотрим как настроить Bluetooth LE (BLE) на ESP32 и Android устройстве. Передадим квантированный набор ускорений сенсора по BLE. Настроим Модель ML для распознания жестов на Android.
Содержание
-
Акселерометр MPU6050
-
Краткое описание машинного обучения (ML)
-
ML на встраиваемых устройствах
-
TensorFlow Lite for Microcontrollers (TFML)
-
Подготовка модели
-
Реализация исходного кода распознавания жестов на ESP32
-
Tensorflow lite на Raspberry Pi
-
Отладка ESP32-S3
-
Заключение
-
Используемые источники
Акселерометр MPU6050
MPU-6050 — это Inertial Measurement Unit (IMU), который объединяет трехосевой акселерометр и трехосевые гироскопические датчики для измерения ускорений и угловой скорости тела. Это устройство существует на рынке уже давно, и благодаря своей низкой стоимости и высокой производительности оно по-прежнему является популярным выбором для проектов DIY на основе датчиков движения.
Рассмотрим следующую систему, состоящую из массы, прикрепленной к одному концу пружины.

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

По закону Гука противодействующая сила пружины
где F — это сила пружины, k — коэффициент жесткости, — смещение по оси z.
По второму закону Ньютона сила, приложенная к массе равна
где m — масса объекта, a — испытываемое ускорение.
Зная, что ускорение свободного падения , для равновесия приравняем взаимодействующие силы
Смещение пружины — это физическая величина, которую акселерометр фиксирует для измерения ускорения.
Сенсор представляет собой MEMS (micro-electromechanical systems) систему, объединяющие в себе взаимосвязанные механические и электрические компоненты микронных размеров.
Краткое описание I2C
Контакты SDA и SCL IMU MPU-6050 используются для связи с микроконтроллером через последовательный протокол связи I2C.

Как видно из схемы, есть две сигнальные линии (SCL и SDA) для подключения вторичных устройств. SCL — это тактовый сигнал, вырабатываемый первичным устройством, и используется всеми устройствами I2C для выборки битов, передаваемых по шине данных. Как первичное, так и вторичное устройства могут передавать данные по шине SDA. MPU-6050 поддерживает максимальную частоту SCL 400 кГц. Подтягивающие резисторы (Rpullup) необходимы, поскольку устройство I2C может поставлять сигнал только до низкого уровня (логический уровень 0). В нашем случае подтягивающие резисторы не нужны, поскольку они интегрированы в модуль MPU-6050. С точки зрения протокола связи первичное устройство всегда начинает связь, передавая следующие биты.
-
Start bit: переход SDA HIGH в LOW в течение SCL HIGH.
-
7-bit address: это 7-битный адрес целевого вторичного устройства.
-
Stop bit: это 1 бит, который указывает на желание первичного устройства либо прочитать данные, либо записать данные на вторичное устройство. Когда стоп-бит установлен в логический LOW, это означает намерение первичного устройства записать данные на вторичное устройство (режим записи). И наоборот, когда стоп-бит установлен в логический HIGH уровень, это указывает на намерение первичного устройства считать данные со вторичного устройства (режим чтения). Стоп-бит также известен как бит чтения/записи (R/W).

Подключение сенсора
Для подключения сенсора MPU-6050 к ESP32 достаточно подать питание на пины VCC и GND, и соединить шины SDA, SCL с пинами микроконтроллера.

Программирование ESP32 с использованием драйвера MPU 6050
Драйверы периферийных устройств предлагают абстрактные интерфейсы, не зависящие от микросхемы. Каждое периферийное устройство имеет общий заголовочный файл (например, gpio.h), что избавляет от необходимости решать вопросы поддержки, связанные с различными чипами. Для установки компонента драйвера MPU 6050 необходимо выполнить команду в терминале ESP-IDF:
idf.py add-dependency "espressif/mpu6050^1.2.0"
После чего компонент добавится в зависимости манифест-файла idfcomponent.yml
## IDF Component Manager Manifest File dependencies: espressif/mpu6050: "^1.2.0"
Заголовочный файл MPU6050.h, содержащий основные функции работы с сенсором появится в папке include. Его необходимо подключить с исходном файле.
Пример инициализации, чтения и записи в I2C в ESP-IDF можно посмотреть в репозитории или в IDF examples.
В качестве сигналов SDA и SCL можно использовать пины общего назначения. Я выбрал GPIO4, GPIO 5. Частоту шины указал 100000 Hz.
#define I2C_MASTER_SCL_IO 4 /* gpio number for I2C master clock */ #define I2C_MASTER_SDA_IO 5 /* gpio number for I2C master data */ #define I2C_MASTER_NUM I2C_NUM_0 /* I2C port number for master dev */ #define I2C_MASTER_FREQ_HZ 100000 /* I2C master clock frequency */
Затем, необходимо сконфигурировать шину I2C
static void i2c_bus_init(void) { i2c_config_t conf; conf.mode = I2C_MODE_MASTER; conf.sda_io_num = (gpio_num_t)I2C_MASTER_SDA_IO; conf.sda_pullup_en = GPIO_PULLUP_ENABLE; conf.scl_io_num = (gpio_num_t)I2C_MASTER_SCL_IO; conf.scl_pullup_en = GPIO_PULLUP_ENABLE; conf.master.clk_speed = I2C_MASTER_FREQ_HZ; conf.clk_flags = I2C_SCLK_SRC_FLAG_FOR_NOMAL; esp_err_t ret = i2c_param_config(I2C_MASTER_NUM, &conf); ret = i2c_driver_install(I2C_MASTER_NUM, conf.mode, 0, 0, 0); }
Далее, создаем указатель static mpu6050_handle_t mpu6050 и инициализируем сенсор MPU6050
static mpu6050_handle_t mpu6050 = NULL; static void i2c_sensor_mpu6050_init(void) { esp_err_t ret; i2c_bus_init(); mpu6050 = mpu6050_create(I2C_MASTER_NUM, MPU6050_I2C_ADDRESS); ret = mpu6050_config(mpu6050, ACCE_FS_4G, GYRO_FS_500DPS); ret = mpu6050_wake_up(mpu6050); }
Описание функции mpu6050_config
Set accelerometer and gyroscope full scale range Parameters: sensor – object handle of mpu6050 acce_fs – accelerometer full scale range gyro_fs – gyroscope full scale range Returns: ESP_OK Success - ESP_FAIL Fail
Для тестирования шины I2C, и вывода адреса ведомого устройства можно использовать следующую функцию
static void i2c_scan() { printf("Scanning...\n"); for (uint8_t addr = 1; addr < 127; addr++) { i2c_cmd_handle_t cmd = i2c_cmd_link_create(); i2c_master_start(cmd); i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_WRITE, true); i2c_master_stop(cmd); esp_err_t err = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, 1000 / portTICK_PERIOD_MS); i2c_cmd_link_delete(cmd); if (err == ESP_OK) { printf("Device found at address 0x%02X\n", addr); } } }
В нашем примере будем считывать ускорения сенсора по трём осям. Для этого вызываем функцию mpu6050_get_acce(sensor, &acce);, предварительно инициализируем структуру mpu6050_acce_value_t acce; sensor — это указатель mpu6050_handle_t sensor. Значения ускорений получаем как acce.acce_x, acce.acce_y, acce.acce_z
mpu6050_acce_value_t acce; mpu6050_get_acce(sensor, &acce); ESP_LOGI(TAG, "acce_x:%.2f, acce_y:%.2f, acce_z:%.2f\n", acce.acce_x, acce.acce_y, acce.acce_z);
Краткое описание машинного обучения (ML)
Машинное обучение по своей сути представляет собой процесс построения моделей, способных выявлять закономерности в данных. В отличие от классического программирования, где разработчики прописывают жесткие правила, определяющие работу программ, здесь алгоритмы самостоятельно анализируют данные и находят в них скрытые зависимости.
Чтобы наглядно продемонстрировать этот принцип, представим ситуацию: наша компания занимается организацией переездов и хочет разработать систему для оценки стоимости услуги. В традиционном программировании можно было бы написать набор условий вида:
if num_bedrooms == 2 and num_bathrooms == 2: estimate = 1500 elif num_bedrooms == 3 and sq_ft > 2000: estimate = 2500
Однако такой подход быстро усложняется по мере добавления новых факторов: количество крупных предметов, число коробок с одеждой, наличие хрупких вещей и прочие нюансы. Запрашивать всю эту информацию у клиентов заранее может оказаться не только трудоемким, но и оттолкнуть потенциальных заказчиков от завершения процесса оценки. Вместо этого можно обучить модель на исторических данных о переездах, чтобы она самостоятельно предсказывала стоимость услуги, анализируя параметры предыдущих клиентов.
Искусственные нейронные сети представляют собой метод машинного обучения, в котором множество слоев, состоящих из нейронов, обрабатывают информацию и передают её дальше, пока на выходе не будет получен конечный результат. Хотя их устройство нельзя напрямую сопоставить с биологическими нейронами мозга, нейросети часто сравнивают с человеческим мышлением из-за их способности выявлять закономерности в данных и формировать предсказания на их основе.
Когда нейросеть содержит более одного скрытого слоя, она классифицируется как глубокая (deep learning). Однако независимо от визуального представления, модели машинного обучения — это математические функции, которые можно реализовать с помощью численных вычислительных пакетов.

Разработчики, работающие в области машинного обучения, как правило, не пишут алгоритмы с нуля, а используют специализированные библиотеки, предоставляющие удобные API для построения моделей. Одним из популярных решений является фреймворк TensorFlow, разработанный Google, с акцентом на глубокое обучение. Для удобства работы применяется API Keras — высокоуровневый инструмент для создания нейронных сетей.
Помимо TensorFlow и Keras, существуют альтернативные фреймворки, такие как scikit-learn, XGBoost и PyTorch. Они обеспечивают мощные инструменты для обработки данных и реализации как простых, так и сложных моделей. Интересно, что с развитием технологий машинное обучение становится все более доступным — теперь модели можно выражать даже на языке SQL. Например, инструмент BigQuery ML, позволяющий совмещать подготовку данных и создание моделей в рамках SQL-запросов.
Нейросети с единственным входным и выходным слоями относятся к классу линейных моделей. Эти алгоритмы строят прогнозы, используя линейные зависимости между переменными. В то же время деревья решений представляют другой вид моделей, в которых данные разделяются на множество ветвей, каждая из которых соответствует определенному исходу.
Другим важным типом моделей являются кластеризационные алгоритмы. Они анализируют данные, выявляют схожие группы и объединяют объекты в кластеры, основываясь на обнаруженных закономерностях.
Все задачи машинного обучения можно разделить на два типа:
-
Обучение с учителем (supervised learning) — модели обучаются на размеченных данных, где каждой записи заранее присвоен правильный ответ. Например, к изображению кошки прикреплена метка «кошка», а к информации о младенце — вес при рождении. После анализа примеров модель должна научиться классифицировать новые данные.
-
Обучение без учителя (unsupervised learning) — в этом случае модель сама ищет скрытые зависимости в данных, не имея заранее заданных меток. Это может быть кластеризация, уменьшение размерности данных или поиск ассоциативных закономерностей.
На практике подавляющее большинство моделей, применяемых в индустрии, относится к обучению с учителем.
В рамках обучения с учителем задачи подразделяются на два класса:
-
Классификация (classification) — модель относит данные к заранее известной категории. Например, определяет породу животного по изображению или распознает дефекты на производственной линии.
-
Регрессия (regression) — предсказывает непрерывные числовые значения. К примеру, модель может оценивать стоимость недвижимости, прогнозировать доход компании или предсказывать продолжительность поездки.
Таким образом, машинное обучение предлагает мощные инструменты для автоматизации процессов анализа данных и формирования предсказаний, что делает его ключевой технологией в современной аналитике и разработке интеллектуальных систем.
ML на встраиваемых устройствах
Одной из главных причин популярности встраиваемых устройств, таких как микроконтроллеры и одноплатные компьютеры является их широкое распространение в различных сферах: автомобилестроение, бытовая электроника, системы здравоохранения, промышленные системы автоматизации, телекоммуникации и многое другое.
Присутствие интернета вещей (IoT) способствует дальнейшему росту популярности встраиваемых устройств.
Современные встраиваемые устройства обладают достаточно высокой вычислительной мощностью и энергоэффективностью. Они позволяют запускать алгоритмы машинного обучения при минимальном потреблении энергии, что делает их особенно полезными для работы от батарей.
Представьте, что у вас есть станок, который производит детали. Иногда он выходит из строя, и его ремонт обходится дорого. А что, если можно было бы предсказывать поломки заранее и останавливать работу до того, как произойдет серьезное повреждение? Для этого можно собирать данные о работе станка — например, скорость производства, температуру и уровень вибрации. Возможно, определенная комбинация этих параметров сигнализирует о надвигающейся неисправности. Но как это определить?
Именно такие задачи и решает машинное обучение. По сути, это метод использования компьютеров для прогнозирования на основе накопленных данных. Мы собираем информацию о работе станка, затем обучаем компьютерную модель анализировать эти данные и делать прогнозы о будущем состоянии оборудования.
Вместо того чтобы разрабатывать алгоритм вручную, программист загружает данные в специальный алгоритм, который самостоятельно находит в них закономерности. В результате мы получаем модель — программу, способную делать прогнозы на основе входных данных. Этот процесс называется обучением. Когда модель уже обучена, мы можем использовать её для предсказаний, и этот процесс называется выводом (inference).
Глубокое обучение (DL) играет решающую роль в разработке интеллектуальных систем, способных самостоятельно анализировать данные и принимать решения. Однако для интеграции таких технологий в автономные устройства, работающие от аккумуляторов, необходимо учитывать их ограниченные вычислительные и энергетические ресурсы.
Локальные вычисления vs. облачные вычисления
Основными причинами по которым отдается предпочтение запуска ML локально можно назвать следующие.
1. Сокращение задержек
Передача данных в облако и обратно занимает время, что может оказать негативное влияние на приложения, требующие быстрого отклика (например, голосовые помощники, системы управления движением и т. д.).
2. Энергоэффективность
Отправка данных в облако требует затрат энергии, особенно при использовании беспроводной связи. Например, на плате Arduino Nano 33 BLE Sense:
-
Связь через Bluetooth потребляет 65 % энергии.
-
Вычисления на процессоре – 14 %.
-
Гироскоп – 2,5 %.
-
Микрофон – 2,4 %. Очевидно, что выполнение вычислений на процессоре устройства оказывается более энергоэффективным по сравнению с отправкой данных в облако.
3. Конфиденциальность
Облачные вычисления требуют передачи данных, что может представлять риски для конфиденциальности. В отличие от этого, локальная обработка данных на устройстве позволяет сохранять персональные данные в безопасности.
Основные этапы работы с глубоким обучением на примере
Чтобы создать модель глубокого обучения для прогнозирования отказов станка, нужно пройти несколько этапов.
-
Определение цели
Нужно сформулировать задачу, которую модель будет решать. В нашем случае мы хотим предсказывать неисправности станка. Это задача классификации: модель должна определять, работает ли станок в нормальном режиме или демонстрирует признаки неисправности. -
Сбор данных
Чтобы обучить модель, нужны данные. В нашем примере можно собирать показатели температуры, скорости производства и уровня вибрации. Однако важно отфильтровать нерелевантные данные — например, меню в столовой вряд ли влияет на работу оборудования. -
Проектирование архитектуры модели
Нужно выбрать подходящую структуру нейронной сети. Разные архитектуры лучше подходят для разных задач: например, сверточные сети хорошо работают с изображениями, а рекуррентные — с временными рядами. -
Обучение модели
На этом этапе модель анализирует собранные данные и учится находить закономерности. Для обучения используется процесс оптимизации, который находит лучшие параметры модели. -
Конвертация модели
Если модель будет работать на микроконтроллере или другом устройстве с ограниченными ресурсами, её нужно оптимизировать — например, уменьшить размер и упростить вычисления. -
Запуск вывода (inference)
После обучения модель можно применять на практике. Например, она будет получать новые данные от датчиков и прогнозировать возможные неисправности. -
Оценка и доработка
Важно проверить, насколько точны предсказания модели, и при необходимости внести корректировки, добавив новые данные или изменив параметры обучения.
Подготовка данных для обучения
Сбор данных
Количество данных, необходимое для обучения, зависит от сложности задачи и уровня шума в данных. Однако общий принцип таков: чем больше данных, тем лучше.
Важно собирать данные в разных условиях. Например, если температура станка зависит от сезона, в выборке должны быть данные как для лета, так и для зимы. Это поможет модели обобщать и правильно работать в любых ситуациях.
Чаще всего данные представляют собой временные ряды — измерения, записанные через регулярные промежутки времени. Например:
Источник данных |
Интервал измерения |
Пример значения |
---|---|---|
Скорость производства |
Каждые 2 минуты |
100 деталей |
Температура |
Каждую минуту |
30°C |
Вибрация (% от нормы) |
Каждые 10 секунд |
23% |
Метка состояния («норма» / «неисправность») |
Каждые 10 секунд |
норма |
Для обучения модели также нужны метки классов: какой набор данных соответствует нормальной работе, а какой указывает на неисправность. Это называется разметкой данных.
После сбора и разметки данных можно приступать к созданию и обучению модели машинного обучения.
Проектирование архитектуры модели
В глубоких нейронных сетях существует множество архитектур, каждая из которых предназначена для решения определённых задач. При разработке модели можно создать собственную архитектуру или использовать уже существующую, разработанную исследователями. Для многих распространённых задач доступны предобученные модели, которые можно бесплатно найти в открытом доступе.
Проектирование модели — это одновременно наука и искусство, а создание новых архитектур остаётся одной из ключевых областей исследований в машинном обучении. Фактически, новые архитектуры появляются буквально каждый день.
На практике можно начать с простой модели, состоящей из нескольких слоёв нейронов, а затем постепенно уточнять архитектуру в процессе итеративного обучения, пока не будет получен желаемый результат.
Представление данных в виде тензоров
Глубокие нейросети работают с входными и выходными данными в формате тензоров. В данном контексте тензор можно представить как многомерный массив чисел. Простые примеры тензоров:
-
Вектор — одномерный массив чисел (размерность 1D). Например:
[42, 35, 8, 643, 7]
-
Этот вектор имеет форму (5,), так как содержит пять элементов в одном измерении.
-
Матрица — двумерный массив (размерность 2D):
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Здесь форма (3, 3), так как матрица состоит из трёх строк и трёх столбцов.
-
Многомерный тензор (3D и более):
[[[10, 20, 30], [40, 50, 60], [70, 80, 90]], [[11, 21, 31], [41, 51, 61], [71, 81, 91]]]
-
В данном случае форма (2, 3, 3) — две матрицы размером 3×3.
-
Скаляр — отдельное число, которое является нулевым измерением (0D), например: 42
Преобразование временных рядов в признаки
В машинном обучении признаки (features) — это ключевые параметры, на основе которых обучается модель. Разные модели работают с разными типами признаков. Например, одна модель может принимать на вход всего одно число (скаляр), в то время как другая — многомерный массив пиксельных значений для анализа изображений.
В нашем случае мы используем такие параметры, как скорость производства, температура и вибрация. Однако в их сыром виде (разные интервалы временных рядов) они могут не подходить для подачи в нейросеть.
Оконное преобразование данных
Временные ряды в задачах прогнозирования могут содержать данные с разными интервалами измерений:
-
Производительность – раз в 2 минуты
-
Температура – раз в минуту
-
Вибрация – раз в 10 секунд
-
Метка состояния (норма/аномалия) – раз в 10 секунд
Проблема в том, что если модель анализирует только доступные данные в конкретный момент времени, она может потерять часть информации. Например, в определённый момент могут быть доступны только данные о вибрации, но не о температуре и производительности.
Решением является оконное агрегирование данных – объединение всех значений в заданном временном окне (например, 1 минута). Внутри окна мы усредняем значения и заполняем отсутствующие данные последними доступными показателями. Если хотя бы одна метка внутри окна указывает на аномалию, всё окно считается аномальным.
Пример окна:
-
Производительность → Среднее: 102
-
Температура → Среднее: 34°C
-
Вибрация → Среднее: 18%
-
Метка → «норма»
После этого значения временного окна представляются вектором: [102, 34, 0.18]
Этот вектор передаётся в модель для обучения или предсказания.
Нормализация входных данных
Для эффективного обучения нейросети входные данные должны быть масштабированы в один диапазон. Например, если одно значение измеряется в десятках, а другое – в долях, модель может неправильно интерпретировать их важность.
Один из методов нормализации – централизация значений:
-
Вычисляем среднее значение по признаку (например, температура = 103.8°C).
-
Вычитаем его из каждого измерения: [108, 104, 102, 103, 102] → [4.2, 0.2, -1.8, -0.8, -1.8]
Для изображений, хранящихся в виде 8-битных матриц (0–255), нормализация выполняется делением на 255, чтобы привести значения к диапазону [0,1].
Пример:
Исходное изображение (3×3 пикселя):
[[255 175 30] [0 45 24] [130 192 87]]
После нормализации:
[[1.0 0.69 0.12] [0.0 0.18 0.09] [0.51 0.75 0.34]]
Обучение модели
Обучение модели — это процесс, в ходе которого алгоритм учится выдавать корректные результаты на основе заданного набора входных данных. Для этого используется обучающая выборка, проходящая через модель, параметры которой (веса и смещения) постепенно корректируются, чтобы минимизировать ошибку предсказаний.
В процессе обучения данные подаются в модель, и её выход сравнивается с эталонными значениями. Для корректировки параметров используется метод обратного распространения ошибки (backpropagation), который итеративно изменяет веса и смещения так, чтобы модель лучше соответствовала ожидаемым результатам. Этот процесс повторяется в течение нескольких эпох — полных проходов через обучающий набор.
Завершают обучение, когда улучшение точности прекращается. Графики потерь (loss) и точности (accuracy) позволяют отслеживать динамику обучения. Потери показывают, насколько модель далека от правильного ответа, а точность отражает долю верных предсказаний. Идеальная модель имела бы нулевые потери и 100% точность, но в реальности это маловероятно.
Основные проблемы, возникающие при обучении моделей, — это недообучение (underfitting) и переобучение (overfitting).
-
Недообучение происходит, когда модель не может выявить закономерности в данных. Причины могут включать слишком простую архитектуру сети или недостаточное количество обучающих данных.
-
Переобучение возникает, когда модель слишком хорошо запоминает обучающую выборку, но плохо справляется с новыми данными. Например, если модель обучалась на изображениях собак, сделанных только на улице, она может ошибаться, классифицируя собаку, сфотографированную в помещении. Методы борьбы с переобучением включают регуляризацию, уменьшение размера модели и увеличение разнообразия обучающего набора данных.
Для оценки качества модели данные разделяют на три группы:
-
Обучающая выборка (training set) — используется непосредственно для обучения модели.
-
Валидационная выборка (validation set) — помогает контролировать процесс обучения и выявлять переобучение.
-
Тестовая выборка (test set) — проверяет, насколько хорошо модель справляется с новыми, ранее не встречавшимися данными.
Обычно данные делят в пропорции 60%-20%-20%. Анализ валидационных потерь помогает определить момент, когда модель начинает переобучаться. Если тестовая выборка показывает плохие результаты, значит, модель адаптировалась не только к обучающим, но и к валидационным данным, что требует пересмотра архитектуры или методики обучения.
Конвертация модели
Обученные модели, созданные в TensorFlow, изначально предназначены для работы на мощных серверах и настольных компьютерах. Однако для работы на микроконтроллерах необходимо конвертировать их в формат TensorFlow Lite.
Конвертация выполняется с помощью инструмента TensorFlow Lite Converter, который также применяет оптимизации, уменьшающие размер модели и повышающие скорость работы. Этот процесс быстр и не требует сложных настроек.
Запуск предсказаний (Inference)
После конвертации модель готова к внедрению в приложение. Используя библиотеку TensorFlow Lite for Microcontrollers, модель загружается и применяется для предсказаний.
На этом этапе входные данные, полученные от датчиков, должны быть предварительно обработаны так, чтобы соответствовать формату, на котором обучалась модель. После обработки данные передаются в модель, которая выполняет предсказание. Выходные данные обычно представляют собой вероятностные оценки классов. Например, для классификатора аномалий результатом будет оценка вероятности, с которой объект относится к категории «норма» или «аномалия».
Таким образом, успешное обучение модели включает в себя корректную настройку параметров, контроль за переобучением, оптимизацию структуры сети и адаптацию модели для работы на целевых устройствах.
TensorFlow Lite for Microcontrollers (TFML)
TensorFlow — это открытая библиотека машинного обучения от Google. Она была представлена в 2015 году и с тех пор стала одной из ведущих платформ для работы с нейросетями. TensorFlow ориентирован на использование в облачных сервисах и на мощных вычислительных платформах, таких как серверы и настольные ПК. Основной язык интерфейса — Python, а размер исполняемых файлов может достигать сотен мегабайт, что не является проблемой для облачной среды.
Однако такие характеристики делают TensorFlow малопригодным для мобильных устройств, где каждые несколько мегабайт в приложении могут негативно сказаться на его популярности. В 2017 году Google представил TensorFlow Lite — облегченную версию для мобильных платформ.
TensorFlow Lite: оптимизация для мобильных устройств
TensorFlow Lite (TFLite) был разработан для выполнения моделей машинного обучения на мобильных устройствах. Для сокращения размера библиотеки и уменьшения нагрузки на процессор в TFLite убраны некоторые возможности, такие как обучение моделей и поддержка всех форматов данных (например, double). Кроме того, библиотека поддерживает оптимизированные вычисления для процессоров Arm Cortex-A и использует API Neural Networks на Android для работы с аппаратными ускорителями.
Одним из ключевых преимуществ TensorFlow Lite является поддержка 8-битной квантизации, позволяющей уменьшить размер модели до 75% по сравнению с 32-битными представлениями. Это существенно ускоряет выполнение моделей и снижает требования к вычислительным ресурсам.
TensorFlow Lite для встраиваемых систем
Несмотря на успех TensorFlow Lite, его требования по объему памяти оставались слишком высокими для микроконтроллеров, где даже сотни килобайт могут быть критичными. В 2018 году Google представил TensorFlow Lite for Microcontrollers (TFLM) — ещё более облегченную версию, специально разработанную для встроенных систем.
Основные требования к TFLM:
-
Минимальный размер кода — библиотека должна помещаться в 20 КБ памяти.
-
Отсутствие зависимости от операционной системы — код не должен использовать системные вызовы.
-
Отказ от стандартных библиотек C и C++ — даже базовые функции вроде sprintf() могут занимать десятки килобайт.
-
Отсутствие аппаратной поддержки плавающей запятой — большинство микроконтроллеров не имеют встроенных FPU, поэтому модели должны работать на целых числах.
-
Запрет динамического выделения памяти — чтобы избежать фрагментации памяти при долгосрочной работе, библиотека использует заранее выделенный буфер.
Некоторые ограничения были сознательно оставлены без изменений ради совместимости с TensorFlow Lite:
-
Использование C++11 — несмотря на популярность C среди встраиваемых разработчиков, переход на C++ облегчает поддержку кода.
-
Ориентация на 32-битные процессоры — современные микроконтроллеры в основном используют 32-битную архитектуру, поэтому было решено отказаться от поддержки 16- и 8-битных чипов.
FlatBuffers — сериализация для встраиваемых систем
FlatBuffers — это библиотека сериализации, разработанная для приложений, где критически важна производительность. Благодаря своей архитектуре она идеально подходит для встраиваемых систем. Одним из главных преимуществ FlatBuffers является то, что ее представление в памяти в режиме выполнения совпадает с сериализованной формой. Это означает, что модели могут быть встроены непосредственно в флеш-память и использоваться без необходимости парсинга или копирования данных.
FlatBuffers основан на схеме, определяющей структуру данных для сериализации. Затем компилятор превращает эту схему в код на C++, который позволяет читать и записывать эти данные. В TensorFlow Lite схема расположена в файле tensorflow/lite/schema/schema.fbs, а сгенерированный C++-код хранится в tensorflow/lite/schema/schema_generated.h. Хотя можно было бы генерировать код при каждом новом билде, для упрощения портирования было принято решение хранить его в репозитории исходного кода.
Тем, кто хочет глубже разобраться в байтовом уровне формата, рекомендуется изучить внутреннюю документацию проекта FlatBuffers для C++ или C. Однако во многих случаях работа ведется через высокоуровневые интерфейсы, поэтому знание низкоуровневой структуры не всегда требуется.
Архитектура TFLM
Основные компоненты:
-
Модель – уже обученная нейросеть в формате .tflite.
-
Интерпретатор (TFLM Runtime) – выполняет инференс (запуск модели).
-
Kernels – оптимизированные реализации операций нейросетей (слои, активации, свёртки и т. д.).
-
Arena Allocator – выделяет память статически при запуске.
-
Драйвер ввода/вывода – взаимодействует с датчиками, микрофонами, камерами и т. д.

Этот репозиторий содержит компонент esp-tflite-micro и примеры, необходимые для использования Tensorflow Lite Micro на чипсетах Espressif с использованием платформы ESP-IDF.
Подготовка модели
Перед построением и обучением модели необходимо сделать выборку входных данных ускорений сенсора для определенных жестов. И сохранить результаты в файлы Circle.csv, Cross.csv, Pad.csv. Для этого необходимо подключить сенсор к микроконтроллеру, и загрузить тестовый код. А затем выполнить жест рукой в течении 2.5 s. Пример такой логики на ESP32 находится ниже в разделе реализации проекта для микроконтроллера. Однако можно использовать любой другой микроконтроллер для снятия ускорений сенсора MPU6050, главное чтобы частота опроса была 20 ms, и длительность одной выборки — 2.5 s. Также необходимо учитывать то, что расположение сенсора при снятии выборки должно быть таким же, как и предполагается в дальнейшем использовании.
В указанных ниже источниках используется другой подход для реализации подобного проекта — с помощью инструмента Edge Impulse data forwarder на Raspberry Pi Pico. Однако мне было интересно реализовать распознавание жестов на ESP32 с помощью Tensorflow lite.
Импорт библиотек:
import numpy as np import pandas as pd import tensorflow as tf from tensorflow import keras from sklearn.model_selection import train_test_split import matplotlib.pyplot as plt
-
NumPy и Pandas используются для работы с числовыми данными и таблицами.
-
TensorFlow и Keras предназначены для построения и обучения модели.
-
Scikit-learn помогает разделить данные на обучающую и тестовую выборку.
-
Matplotlib используется для визуализации результатов обучения.
Функция загрузки данных
def load_data(filenames, label): data = [] labels = [] for file in filenames: df = pd.read_csv(file, header=None) for i in range(len(df) - 125): # Оконное разбиение (2.5 с * 50 Гц) window = df.iloc[i:i+125].values.flatten() data.append(window) labels.append(label) return np.array(data), np.array(labels)
-
Читаем CSV-файл, содержащий данные с акселерометра.
-
Используем скользящее окно размером 125 значений (соответствует 2.5 секунды при частоте 50 Гц).
-
Каждое окно преобразуется в одномерный массив и добавляется в список обучающих данных.
-
Метки жестов добавляются в соответствующий список.
Маркирование жестов
gestures = {"circle": 0, "cross": 1, "pad": 2} data = [] labels = [] for gesture, label in gestures.items(): d, l = load_data([f"{gesture}.csv"], label) data.append(d) labels.append(l) data = np.vstack(data) labels = np.hstack(labels)
-
Определяем словарь жестов, где каждому присваивается числовая метка.
-
Загружаем данные для каждого жеста, используя load_data().
-
Объединяем массивы данных и меток в единые массивы data и labels.
Разделение данных на обучающую и тестовую выборку
X_train, X_test, y_train, y_test = train_test_split(data, labels, test_size=0.2, random_state=42)
-
Разбиваем выборку в соотношении 80% на обучение и 20% на тест.
-
random_state=42 позволяет повторять разбиение при каждом запуске кода.
Создание нейросетевой модели
model = keras.Sequential([ keras.layers.Dense(64, activation='relu', input_shape=(375,)), keras.layers.Dropout(0.2), keras.layers.Dense(32, activation='relu'), keras.layers.Dense(3, activation='softmax') ])
-
Первый Dense-слой: 64 нейрона, функция активации relu, входная размерность (375,) (125 точек по 3 оси акселерометра).
-
Dropout-слой: Убирает 20% случайных нейронов для уменьшения переобучения.
-
Второй Dense-слой: 32 нейрона, relu.
-
Выходной слой: 3 нейрона (по количеству классов), softmax для вероятностного выхода.
Компиляция и обучение модели
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy']) history = model.fit(X_train, y_train, epochs=20, batch_size=16, validation_data=(X_test, y_test))
-
Оптимизатор Adam: адаптивный градиентный спуск.
-
Функция потерь: sparse_categorical_crossentropy, так как классы представлены в виде целых чисел.
-
Метрика: accuracy (точность предсказаний).
-
Обучение: 20 эпох, размер мини-батча 16.
Сохранение обученной модели
model.save('model_keras.h5')
Сохраняем обученную модель в файл model_keras.h5, чтобы позже использовать её для предсказаний. Этот пункт не обязательный, если сразу конвертируем в .tflite.
Визуализация результатов обучения
plt.figure(figsize=(12, 5)) plt.subplot(1, 2, 1) plt.plot(history.history['loss'], label='Training Loss') plt.plot(history.history['val_loss'], label='Validation Loss') plt.xlabel('Epochs') plt.ylabel('Loss') plt.legend() plt.title('Loss during Training') plt.subplot(1, 2, 2) plt.plot(history.history['accuracy'], label='Training Accuracy') plt.plot(history.history['val_accuracy'], label='Validation Accuracy') plt.xlabel('Epochs') plt.ylabel('Accuracy') plt.legend() plt.title('Accuracy during Training') plt.show()
-
Визуализируем изменение функции потерь (loss) и точности (accuracy) во время обучения.
-
Используем subplot() для отображения двух графиков в одном окне.
-
Сравниваем тренировочные и валидационные показатели для контроля переобучения.

Конвертирование модели в tflite
Обученную модель в TensorFlow нужно конвертировать в .tflite. Это делается с помощью TensorFlow Lite Converter:
def representative_data_gen(): for i_value in tf.data.Dataset.from_tensor_slices(X_test).batch(1).take(100): i_value_f32 = tf.dtypes.cast(i_value, tf.float32) yield [i_value_f32] # Load model from model_keras.h5 file # model_keras= tf.keras.models.load_model('model_keras.h5') # Conversion to TFLite converter = tf.lite.TFLiteConverter.from_keras_model(model) converter.representative_dataset = tf.lite.RepresentativeDataset(representative_data_gen) # converter.optimizations = [tf.lite.Optimize.DEFAULT] # Input/output type is left as float32 converter.inference_input_type = tf.float32 converter.inference_output_type = tf.float32 tflite_model = converter.convert() # Save the model with open("gesture_model.tflite", "wb") as f: f.write(tflite_model)
В результате получим файл модели gesture_model.tflite. tflite файл можно непосредственно загружать и использовать на Android или одноплатный компьютер, как Raspberry Pi. Следует отметить, что при включенной опции tf.lite.Optimize.DEFAULT, но без квантования модель не будет работать на микроконтроллере.
Для использования модели на микроконтроллере ESP32 tflite файл необходимо конвертировать в заголовочный файл model.h с помощью утилиты XXD
xxd -i gesture_model.tflite > model.h
После этого в model.h создаётся массив типа:
const unsigned char gesture_model_tflite[] = { 0x12, 0x34, 0x56, ... };
Модель подключается как заголовок и передаётся в tflite::GetModel()
const tflite::Model* model = tflite::GetModel(gesture_model_tflite);
Реализация исходного кода распознавания жестов на ESP32
Для создания модели машинного обучения по распознаванию жестов на основе данных с MPU6050, необходимо предварительно собрать и подготовить данные. Для этого используем окно сбора данных 2.5 секунды и частоту дискретизации 50 Гц, что означает, что в каждом окне будет 125 измерений.
Характеристики сбора данных:
-
Сенсор MPU6050
-
Частота выборки: 50 Гц (интервал между измерениями ~20 мс)
-
Оконный метод: 2.5 секунды (125 записей на окно)
-
Формат данных: CSV-файл, содержащий три столбца (X, Y, Z ускорения) — данные копируются вручную из консоли и сохраняются в CSV-файл. Возможно автоматизировать процесс.
Создание проекта
Для создания проекта использовалась среда разработки Visual Studio Code с расширением Espressif, версия ESP-IDF — 4.4.5 Проект можно создавать с нуля или использовать доступный пример. Ранее я писал в статье более подробно про создание и настройку проектов для ESP32, а также пример устройства мобильной платформы.
Затем необходимо подключить к проекту зависимость esp-tflite-micro, выполнив команду в терминале ESP-IDF
idf.py add-dependency "esp-tflite-micro"
После чего в зависимостях появится строчка espressif/esp-tflite-micro: «*» .
Исходный код проекта ESP32 находится в этом репозитории.
Исходный код Tensorflow модели находится здесь.
Структура проекта
├── main │ ├── gesture.cc # Gesture recognition task using TFLM │ ├── gesture.h # Header file for gesture recognition │ ├── ble_provider.c # Bluetooth communication logic │ ├── ble_provider.h # Header file for BLE provider │ ├── mpu6050.c # MPU6050 sensor interface │ ├── model.h # TensorFlow Lite model (generated as xxd array) │ ├── main.c # Main entry point │ ├── CMakeLists.txt # Build configuration ├── include │ ├── mpu6050.h # Header file for MPU6050 sensor ├── sdkconfig.defaults.esp32s3 # ESP-IDF configuration file ├── idf_component.yml # ESP-IDF component configuration ├── CMakeLists.txt # Project build configuration ├── README.md
Выборка данных для обучения
Пример кода для платформы ESP-IDF, который собирает данные с MPU6050 и выводит их в консоль
void read_mpu6050_task(void *pvParameters) { mpu6050_handle_t mpu6050 = mpu6050_create(I2C_MASTER_NUM, MPU6050_I2C_ADDRESS); mpu6050_config(mpu6050, ACCEL_FS_2G, GYRO_FS_250DPS); for (int i = 0; i < SAMPLE_COUNT; i++) { mpu6050_accel_data_t accel; mpu6050_read_accel(mpu6050, &accel); printf("%.2f,%.2f,%.2f\n", accel.x, accel.y, accel.z); vTaskDelay(pdMS_TO_TICKS(20)); // 50 Гц } vTaskDelete(NULL); // Завершение задачи после 2.5 сек } void app_main(void) { i2c_master_init(); xTaskCreate(&read_mpu6050_task, "read_mpu6050_task", 4096, NULL, 5, NULL); }
-
Чтение данных: в цикле на 125 итераций (2.5 сек, 50 Гц) считываются значения ускорений по X, Y и Z.
-
Вывод данных: Результаты построчно выводятся в консоль.
-
Остановка задачи: после сбора 125 измерений задача завершается (vTaskDelete(NULL)).
-
Запуск следующей задачи — с помощью кнопки reset на ESP32.
Я сделал 8 выборок по 125 измерений для каждого жеста. Таким образом сформировал три CSV-файла c 1000 строками: circle.csv, cross.csv, pad.csv.
На следующем фото я изобразил, как записывал движения сенсора. Изначально MPU6050 был подключен к ESP32 с помощью гибких перемычек, затем я переместил все на макетную плату.

Пример исходного кода gesture.cc для запуска модели
#include #include #include "esp_log.h" #include "tensorflow/lite/micro/micro_mutable_op_resolver.h" #include "tensorflow/lite/micro/micro_interpreter.h" #include "tensorflow/lite/schema/schema_generated.h" #include "tensorflow/lite/core/c/common.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/queue.h" #include "mpu6050.h" #include "model.h" //Generated by xxd #include #include "gesture.h" #define TAG "GESTURE" #define SAMPLE_SIZE 125 // TensorFlow Lite constexpr int kTensorArenaSize = 120 * 1024; static uint8_t *tensor_arena; const tflite::Model* model = nullptr; tflite::MicroInterpreter* interpreter = nullptr; TfLiteTensor* model_input = nullptr; TfLiteTensor* output = nullptr; mpu6050_acce_value_t acce; /** * TensorFlow Lite initialization function */ void tflite_init() { if (tensor_arena == NULL) { tensor_arena = (uint8_t *) heap_caps_malloc(kTensorArenaSize, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); } if (tensor_arena == NULL) { ESP_LOGE(TAG, "Couldn't allocate memory of %d bytes\n", kTensorArenaSize); return; } ESP_LOGI(TAG, "Model loading..."); model = tflite::GetModel(gesture_model_tflite); if (model->version() != TFLITE_SCHEMA_VERSION) { ESP_LOGE(TAG, "Model version error! Expected %d, got %d", TFLITE_SCHEMA_VERSION, model->version()); return; } static tflite::MicroMutableOpResolver<6> resolver; resolver.AddFullyConnected(); resolver.AddRelu(); resolver.AddSoftmax(); static tflite::MicroInterpreter static_interpreter(model, resolver, tensor_arena, kTensorArenaSize); interpreter = &static_interpreter; if (interpreter->AllocateTensors() != kTfLiteOk) { ESP_LOGE(TAG, "Memory allocation error!"); return; } model_input = interpreter->input(0); output = interpreter->output(0); ESP_LOGI(TAG, "TensorFlow Lite has been initialized."); } /** * Function to read data from MPU6050 */ void collect_sensor_data(mpu6050_handle_t sensor, QueueHandle_t sensorQueue) { ESP_LOGI(TAG, "Data acquisition with MPU6050..."); for (int i = 0; i < SAMPLE_SIZE; i++) { mpu6050_get_acce(sensor, &acce); model_input->data.f[i * 3] = acce.acce_x; model_input->data.f[i * 3 + 1] = acce.acce_y; model_input->data.f[i * 3 + 2] = acce.acce_z; vTaskDelay(pdMS_TO_TICKS(20)); // 50 Hz (20 ms) } // Launching the model if (interpreter->Invoke() != kTfLiteOk) { ESP_LOGE(TAG, "Model execution error!"); return; } memcpy(&model_input->data.f[SAMPLE_SIZE * 3], output->data.f, 3 * sizeof(float)); portBASE_TYPE xStatus = xQueueSend(sensorQueue, &model_input->data.f, 0); if( xStatus != pdPASS ) { ESP_LOGE(TAG, "Could not send sensor data to the queue.\r\n"); } } /** * Main task */ extern "C" void gesture_predict(void *param) { MPUparams* mpu_params = (MPUparams *)param; mpu6050_handle_t sensor = mpu_params->sensor; QueueHandle_t sensorQueue = mpu_params->sensorQueue; tflite_init(); while (1) { collect_sensor_data(sensor, sensorQueue); } }
Этот код загружает модель, выделяет память, передаёт входные данные, выполняет инференс и выводит результат.
Чтобы загрузить модель необходимо выделить память для tensor_arena. Это можно сделать статически в стеке или динамически в куче. В большинстве примеров память выделяется динамически, тогда при создании задачи FreeRTOS ее размер относительно небольшой.
void tflite_init() { if (tensor_arena == NULL) { tensor_arena = (uint8_t *) heap_caps_malloc(kTensorArenaSize, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); } if (tensor_arena == NULL) { ESP_LOGE(TAG, "Couldn't allocate memory of %d bytes\n", kTensorArenaSize); return; }
Модель нейросети tflite::Model* model`
const tflite::Model* model — эта переменная содержит ссылку на модель, которая была загружена в память. Она указывает на объект tflite::Model, который интерпретирует бинарные данные модели.
model = tflite::GetModel(gesture_model_tflite); if (model->version() != TFLITE_SCHEMA_VERSION) { ESP_LOGE(TAG, "Model version error! Expected %d, got %d", TFLITE_SCHEMA_VERSION, model->version()); return; }
Интерпретатор tflite::MicroInterpreter* interpreter
Интерпретатор запускает модель и управляет входами/выходами. Это центральный компонент, который выполняет вычисления.
-
загружает модель tflite::Model.
-
использует резолвер для подключения нужных функций (например, Conv2D, FullyConnected).
-
выделяет память tensor_arena для хранения тензоров.
Пример создания интерпретатора:
interpreter = new tflite::MicroInterpreter(model, resolver, tensor_arena, kTensorArenaSize);
После настройки интерпретатора мы передаем в него входные данные и запускаем:
interpreter->Invoke();
Выходные данные можно получить так:
float* output = interpreter->output(0)->data.f;
где data.f .f — это типа данных float union, если модель оперирует другими типами данных, как int8_t, необходимо выбирать соответствующий тип.
Резолвер tflite::MicroMutableOpResolver
Этот объект регистрирует все доступные операции (слои нейросети), которые используются в модели. В TensorFlow Lite разные модели могут использовать разные операции. Например:
-
Свёрточная сеть использует Conv2D
-
Полносвязная сеть использует FullyConnected
-
Для активации может использоваться ReLU, Sigmoid или Softmax
Так как TFLM не поддерживает динамическую загрузку операций, их нужно заранее зарегистрировать в резолвере.
static tflite::MicroMutableOpResolver<6> resolver;
Число 6 в шаблоне указывает количество слоев модели сети.
Далее необходимо явно подключить требуемые слои к резолверу.
resolver.AddFullyConnected(); resolver.AddRelu(); resolver.AddSoftmax();
В TensorFlow Lite (TFL) модель загружается динамически, а необходимые операции (слои) автоматически подгружаются в память. Это возможно, потому что:
-
операции хранятся в виде динамически загружаемых библиотек;
-
устройство имеет достаточно памяти для их хранения;
-
интерпретатор сам определяет, какие операции нужны, и загружает только их.
Но в TensorFlow Lite Micro (TFLM) всё работает иначе:
-
операции хранятся в коде прошивки – каждая операция занимает место во Flash-памяти;
-
нужно вручную указывать слои – чтобы не перегружать микроконтроллер ненужными слоями.
Поэтому в TFLM мы используем MicroMutableOpResolver и вручную добавляем только те слои, которые реально используются в модели.
Для того, чтобы правильно настроить слои резолвера, необходимо узнать структуру tflite модели. Для этого можно воспользоваться ресурсом https://netron.app
В нашем примере модель gesture_model.tflite выглядит так.

-
resolver.AddFullyConnected() — используется в нейросетях для обработки входных данных в полносвязном формате.
-
resolver.AddRelu(); — Функция ReLU
— используется в скрытых слоях для улучшения сходимости.
-
resolver.AddSoftmax(); — Функция Softmax:
— преобразует числа в вероятности.
Набор всех методов подключаемых слоев находится в исходном файле micro_mutable_op_resolver.h
ReLU (Rectified Linear Unit) — это одна из самых популярных функций активации в нейросетях. Она очень простая, но эффективная: . То есть, если вход больше 0, то остаётся таким же, если вход меньше 0, то превращается в 0.
Softmax – это функция активации, которая превращает сырые предсказания — логиты в вероятности, получая экспоненту e каждого значения, а затем подвергая нормализации каждое e, то есть разделяя на их сумму, чтобы сумма всех экспонент равнялась единице. Логиты – это логарифм шансов, он лежит в пределах от минус- до плюс-бесконечности. Когда логиты отрицательны или обладают разными знаками, правильной нормализации выполнить не удастся. Возведение в степень решает эту проблему. Softmax обычно применяется в последнем слое нейросети, если мы решаем задачу классификации. Представь, что у нас есть три объекта, и модель выдала логиты на полносвязном слое:
Cat: 2.0 Dog: 1.0 Rabbit: 0.1
Применим Softmax к этим числам согласно формуле
Как видно из преобразования, мы получили вероятности логитов, сумма которых равна единице.
Обработка и предсказание
Ускорения сенсора считываются с помощью функции mpu6050_get_acce(sensor, &acce); модуля gesture.cc в цикле из 125 итерации с задержкой 20 мс vTaskDelay(pdMS_TO_TICKS(20));, что соответствует 2.5 секунды.
for (int i = 0; i < SAMPLE_SIZE; i++) { mpu6050_get_acce(sensor, &acce); model_input->data.f[i * 3] = acce.acce_x; model_input->data.f[i * 3 + 1] = acce.acce_y; model_input->data.f[i * 3 + 2] = acce.acce_z; vTaskDelay(pdMS_TO_TICKS(20)); // 50 Hz (20 ms) }
Значения ускорений передаются в структуру данных входного тензора model_input->data.f
Затем происходит вычисление модели с помощью метода interpreter->Invoke()
if (interpreter->Invoke() != kTfLiteOk) { ESP_LOGE(TAG, "Model execution error!"); return; }
Для передачи данных между задачами FreeRTOS я решил использовать очередь, в которую передаем указатель на структуру входных данных. В качестве обмена данными можно использовать и другие варианты и структуры, но такой способ в моем случае кажется наиболее удобным. Создаем в модуле main.c очередь. Ее размер равен размеру одного указателя float.
static QueueHandle_t sensorQueue; sensorQueue = xQueueCreate(1, sizeof(float) );
В основной функции void app_main(void) модуля main.c создаем задачи для функций:
-
gesture_predict — обработка сенсора и предсказание модели;
-
ble_loop — получение данных сенсора и предсказаний модели для дальнейшей отправки по BLE (описание BLE в следующей статье).
xTaskCreate(&ble_loop, "Heart Rate Simulation", 8 * 1024, (void*)sensorQueue, 6, NULL); MPUparams mpu_params = {mpu6050, sensorQueue}; xTaskCreate(&gesture_predict, "Predict gesture", 8 * 1024, (void*)&mpu_params, 5, NULL);
Для каждой задачи выделяем 8 kB памяти. Приоритет gesture_predict задачи (5) делаем выше, чем ble_loop (6) для того, чтобы первая задача вытесняла вначале вторую. В задачу gesture_predict передается структура MPUparams mpu_params = {mpu6050, sensorQueue};, которая содержит обработчик сенсора mpu6050 и обработчик очереди sensorQueue.
Для того, чтобы использовать C++ классы TFML в модуле gesture.cc функцию gesture_predict объявляем, как extern. Из входных параметров, переданных в задачу получаем обработчики сенсора и очереди.
extern "C" void gesture_predict(void *param) { MPUparams* mpu_params = (MPUparams *)param; mpu6050_handle_t sensor = mpu_params->sensor; QueueHandle_t sensorQueue = mpu_params->sensorQueue; tflite_init(); while (1) { collect_sensor_data(sensor, sensorQueue); } }
В функции ble_loop модуля ble_provider.c первым шагом идет считывание очереди
xQueueReceive(sensorQueue, &data, portMAX_DELAY)
Пока очередь пуста, задача ble_loop блокируется.
После инференса модели в функции collect_sensor_data модуля gesture.cc копируем выходные данные — три float вероятности предсказаний в конец структуры входных данных data.f[SAMPLE_SIZE * 3].
memcpy(&model_input->data.f[SAMPLE_SIZE * 3], output->data.f, 3 * sizeof(float));
После этого передаем указатель входной структуры в очередь
portBASE_TYPE xStatus = xQueueSend(sensorQueue, &model_input->data.f, 0); if( xStatus != pdPASS ) { ESP_LOGE(TAG, "Could not send sensor data to the queue.\r\n"); }
В функции collect_sensor_data модуля gesture.cc принимаем данные из очереди в float *data. Копируем последние три значения очереди в локальный массив, затем сравниваем максимальное значение вероятности жеста. Получив индекс максимальной вероятности выбираем название жеста из константного массива имен и передать его клиенту.
static const char* gestures[] = {"Circle", "Cross", "Pad"}; float prediction[3]; memcpy(prediction, &data[BLE_SAMPLE_SIZE], 3 * sizeof(float)); int predicted_class = 0; float max_confidence = prediction[0]; // Looking for a class with maximum confidence for (int i = 1; i < 3; i++) { if (prediction[i] > max_confidence) { max_confidence = prediction[i]; predicted_class = i; } } snprintf(stored_data, MAX_DATA_LEN, "%s, %.4f", gestures[predicted_class], max_confidence);
После подачи питания на ESP32 с сенсором MPU6050 необходимо открыть консоль монитора последовательно порта устройства, например выполнив Monitor device команду ESP-IDF. А затем выполнить один из определенных жестов: окружность, пересечение или перемещение. Пример вывода в консоль для жеста Circle.
I (13119) BLE-Server: prediction data: I (13119) BLE-Server: [0]: 0.9995 I (13119) BLE-Server: [1]: 0.0000 I (13119) BLE-Server: [2]: 0.0005
На данный момент не реализована логика для неопределенного жеста, поэтому в консоли изначально можно увидеть максимальную вероятность для Cross или некоторые числа для Circle и Pad.
Tensorflow lite на Raspberry Pi
В некоторых случаях для ускорения тестирования модели tflite с помощью Tensorflow lite фреймворка есть смысл запускать выполнение модели на компьютере, например Raspberry Pi. На этой странице написано руководство как сбилдить Tensorflow lite из исходников. Ранее я упоминал про параметр оптимизации конвертирования converter.optimizations = [tf.lite.Optimize.DEFAULT]. Преобразовав tflite с этой опцией я подключил модель к ESP32, и к Android. На микроконтроллере модель не выполняла предсказания, при это никаких ошибок не было. Однако на Android модель работала корректно. Я решил протестировать модель на Raspberry, предварительно собрав Tensorflow их исходников. К моему удивлению, на Raspberry модель также не работала. Но зато процесс компиляции исходного файла и выполнения занимает гораздо меньше времени, и исходные данные можно принимать в любом формате, например CSV-файле. Это позволило мне провести ряд экспериментов и выявить проблему. Исходный файл тестирования модели на Raspberry находится здесь.
Отладка ESP32-S3
В ряде случаев отладка на микроконтроллере является полезным инструментом, позволяющим определить проблемные места в логике работы, и отслеживать значения переменных в точках останова. Поэтому вкратце приведу настройку среды для отладки ESP32-S3, и особенности, с которыми я столкнулся.
ESP32-S3 имеет встроенную схему JTAG и может быть отлажен без дополнительных модулей. Необходим только USB-кабель, подключенный к контактам D+/D-. Или просто подключить USB-кабель.
ESP32-S3 Pin |
USB Signal |
---|---|
GPIO19 |
D- |
GPIO20 |
D+ |
5V |
V_BUS |
GND |
Ground |
Чтобы установить драйвер для отладки ESP32-S3 на Windows необходимо выполнить команду в PowerShell
Invoke-WebRequest 'https://dl.espressif.com/dl/idf-env/idf-env.exe' -OutFile .\idf-env.exe; .\idf-env.exe driver install --espressif
После установки драйвера при попытке запуска отладки можно получить ошибку ESP32-S3 debug issue : libusb_open() failed with LIBUSB_ERROR_NOT_SUPPORTED. Для этого необходимо изменить драйвер с помощью утилиты Zadig, как описано здесь.
Для отладки ESP32 в Visual Studio Code необходимо настроить конфигурацию в файле проекта ./vcscode/launch.json
В моем случае конфигурация выглядит так
{ "version": "0.2.0", "configurations": [ { "type": "cppdbg", "name": "ESP32 OpenOCD", "request": "launch", "cwd": "${workspaceFolder}", "program": "${workspaceFolder}/build/${command:espIdf.getProjectName}.elf", "MIMode": "gdb", "miDebuggerPath": "C:/Users/user/.espressif/tools/xtensa-esp32s3-elf/esp-2021r2-patch5-8.4.0/xtensa-esp32s3-elf/bin/xtensa-esp32s3-elf-gdb.exe", "windows": { "program": "${workspaceFolder}\\build\\${command:espIdf.getProjectName}.elf" }, "setupCommands": [ { "text": "target extended-remote :3333" }, { "text": "set remote hardware-watchpoint-limit 2"}, { "text": "thb app_main" }, { "text": "flushregs" } ] } ] }
Затем необходимо запустить OpenOCD сервер в терминале ESP-IDF с помощью команды
openocd -f board/esp32s3-builtin.cfg
Запуск отладки выполняется на Debug в VSCode

Более подробно про отладку ESP-IDF в VSCode можно почитать в документации.
Заключение
TensorFlow Lite for Microcontrollers открывает возможности машинного обучения для встраиваемых систем, где традиционные модели были невозможны из-за ограничений памяти и вычислительных мощностей. Благодаря продуманной архитектуре, TFLM позволяет запускать нейросетевые модели даже на устройствах с 20 КБ памяти, что делает машинное обучение доступным для IoT, носимых устройств и других встраиваемых решений.
В данной статье был показан пример реализации распознавания жестов руки на миrроконтроллере ESP32 с помощью сенсора MPU6050 и модели машинного обучения. Архитектура TFML micro позволяет достаточно легко интегрировать обученную Tensorflow-модель в устройство с минимальным объемом исходного кода. Особенностью написания кода для TFML micro является необходимость явного указания используемых слоев, поэтому нужно знать структуру tflite-модели. Однако могут быть сложности с некоторыми моделями, например с LSTM слоями, для этого необходимо рассматривать возможность другой аппаратной архитектуры или структуры модели.
Используемые источники
ссылка на оригинал статьи https://habr.com/ru/articles/891314/
Добавить комментарий