Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Аппаратные наборы

Развёртывание машинного обучения на реальном встраиваемом оборудовании. Платы стоят от $25, а если бюджета нет, интерактивные лабораторные работы воспроизводят весь опыт работы с оборудованием в симуляции.

XIAO ESP32S3 Arduino Nicla Vision Grove Vision AI V2 Raspberry Pi

Эти практические лабораторные работы сопровождают учебник Machine Learning Systems и переносят теорию на настоящее оборудование. Вы развернёте модели машинного обучения на встраиваемых устройствах, которые помещаются в ладони: от классификации изображений до распознавания голоса и обнаружения движения. Профессиональные отладочные платы стоимостью $25–100 дают мгновенную ощутимую обратную связь: когда модель работает успешно, светодиоды загораются, моторы вращаются, а зуммеры звучат.

Работа в условиях ограниченных ресурсов встраиваемых устройств (обычно 2 МБ оперативной памяти и 1 МБ флеш-памяти) заставляет сталкиваться с теми же инженерными компромиссами, которые определяют крупномасштабные ML-системы, но в осязаемой среде, где каждое решение по оптимизации сразу даёт наблюдаемый результат.

Разработка лабораторных работ. Эти практические лабораторные работы разработаны совместно проф. Vijay Janapa Reddi и Marcelo Rovai; разработкой руководил Marcelo. Его многолетний опыт в области встраиваемых систем позволил создать доступные практические занятия, которые связывают теорию с реальной реализацией.

Marcelo также написал открытые книги-компаньоны о платах Nicla Vision, XIAO и Raspberry Pi. Прямые ссылки и описания см. в разделе «Companion Books» Хаба для преподавателей.

Аппаратные платформы

Grove Vision AI V2 XIAOML Kit Raspberry Pi Arduino Nicla Vision

  • Grove Vision AI V2, ~$25. Лучший выбор для начинающих: готовое решение для компьютерного зрения «подключи и работай».
  • XIAOML Kit, ~$40. Лучшее соотношение цены и возможностей: зрение, звук и движение.
  • Raspberry Pi, ~$60–80. Продвинутые проекты: LLM, VLM и периферийный ИИ.
  • Nicla Vision, ~$95. Профессиональный уровень и два датчика в компактном корпусе.

Что вы создадите

  • Компьютерное зрение. Классификация изображений и обнаружение объектов на микроконтроллерах. Обучайте модели распознавать объекты, находить лица или классифицировать сцены, а затем запускайте их на устройствах, работающих от батареи.
  • Обработка звука. Распознавание ключевых слов и голосовых команд. Создавайте детекторы слова пробуждения и простые голосовые интерфейсы, которые работают полностью на устройстве, без облака.
  • Классификация движения. Распознавание активности и жестов по данным IMU. Создавайте носимые приложения, которые определяют ходьбу, бег или пользовательские жесты с помощью акселерометра и гироскопа.
  • Большие языковые модели. Запускайте LLM и VLM на периферийных устройствах. Познакомьтесь с передовыми возможностями ИИ на устройстве: модели понимают и генерируют текст.

Начало работы

  1. Выберите оборудование. Подберите платформу по бюджету и учебным целям. Подробное сравнение см. в разделе «Платформы».
  2. Настройте среду. Установите Arduino IDE или инструменты для конкретной платформы. Пошаговые инструкции см. в руководстве по настройке среды разработки.
  3. Соберите и разверните. Выполняйте лабораторные работы для выбранной платформы. Начните с раздела «Начало работы», где описаны все доступные упражнения.

Часть экосистемы MLSysBook

  • Учебник — полная теория по всему стеку систем машинного обучения.
  • Слайды лекций — презентации Beamer и учебные материалы для каждой главы.
  • Лабораторные работы — интерактивные блокноты Marimo, которые проверяют утверждения книги.
  • Аппаратные наборы — практическое развёртывание машинного обучения на реальных встраиваемых устройствах.
  • Хаб для преподавателей — карты курсов, учебные программы и материалы для внедрения курса в преподавание.
  • TinyTorch — постройте собственный фреймворк машинного обучения с нуля, модуль за модулем.
  • MLSys·im — аналитический движок моделирования, на котором основаны количественные рисунки книги.

Начало работы

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

Шаг 1. Выберите платформу

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

Для начинающих и тех, кто ограничен в бюджете:

ПлатформаСтоимостьПочему стоит выбрать
Grove Vision AI V2~$25Интерфейс без кода, самый быстрый путь к запуску моделей
XIAOML Kit~$40Лучшее соотношение цены и возможностей: зрение, звук и движение

Для продвинутых приложений:

ПлатформаСтоимостьПочему стоит выбрать
Raspberry Pi~$60–80Полноценная среда Linux, LLM и VLM
Nicla Vision~$95Профессиональный уровень, сверхнизкое энергопотребление

Подробные характеристики и технические сравнения см. на странице «Платформы».

Шаг 2. Настройте среду разработки

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

Ориентировочное время: 30–60 минут в зависимости от платформы и скорости интернета.

Полные инструкции см. в руководстве по настройке среды разработки. В нём описаны:

  • требования к компьютеру для разработки;
  • установка Arduino IDE для платформ на микроконтроллерах;
  • настройка среды Python для Raspberry Pi;
  • веб-интерфейс SenseCraft AI для Grove Vision AI V2;
  • настройка последовательной связи (Serial) и проверка оборудования.

Шаг 3. Выберите первую лабораторную работу

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

Категория работыGrove VisionXIAOML KitNiclaRaspberry Pi
Классификация изображений✓✓✓✓
Обнаружение объектов✓✓✓✓
Распознавание ключевых слов✓✓
Классификация движения✓✓
Большие языковые модели (LLM)✓
Визуально-языковые модели (VLM)✓

Доступность упражнений по платформам

Шаг 4. Начните первую лабораторную работу

Grove Vision AI V2: начните с раздела «Настройка и приложения без кода». Вы развернёте предобученную модель за несколько минут в визуальном интерфейсе.

XIAOML Kit: начните с раздела «Настройка», затем перейдите к разделу «Классификация изображений», чтобы обучить и развернуть свою первую модель.

Nicla Vision: пройдите «Настройку», чтобы подготовить плату, и попробуйте «Классификацию изображений».

Raspberry Pi: выполните «Настройку», затем выберите путь:

Предварительные требования

Лабораторные работы рассчитаны на следующую подготовку:

  • Программирование: уверенное владение Python. Знание C/C++ полезно для платформ на микроконтроллерах, но не обязательно.
  • Математика: рабочее знание линейной алгебры и основ теории вероятностей на уровне бакалавриата.
  • Оборудование: опыт работы со встраиваемыми системами не нужен. Каждая лабораторная работа содержит полные инструкции по настройке и поиску неисправностей.

Связь с учебником «Machine Learning Systems»

Эти лабораторные работы дополняют отдельные главы учебника «Machine Learning Systems»:

  • Лабораторные работы по классификации изображений закрепляют материал глав о компьютерном зрении и оптимизации моделей;
  • Лабораторные работы по распознаванию ключевых слов связаны с главами об обработке аудио и инференсе в реальном времени;
  • Лабораторные работы по классификации движения демонстрируют слияние данных датчиков и анализ временных рядов;
  • Лабораторные работы по LLM и VLM переносят развёртывание больших моделей в среду с ограниченными ресурсами.

Каждая лабораторная работа указывает соответствующие разделы учебника для более глубокого теоретического изучения.

Настройка среды разработки

Настройка среды разработки — важный первый шаг, который определяет ваш успех на протяжении всей серии лабораторных работ. В отличие от облачной разработки ML, где инфраструктура скрыта, встраиваемые системы требуют понимания всей цепочки инструментов: от компиляции кода до развёртывания на оборудовании.

Настройка среды обычно занимает 30–60 минут в зависимости от выбранной платформы и скорости интернета. Эти процедуры рассчитаны на студентов, у которых нет опыта работы со встраиваемыми системами.

Системные требования

Прежде чем начинать установку, убедитесь, что ваш компьютер для разработки соответствует следующим требованиям:

Компьютер для разработки:

  • Операционная система: Windows 10/11, macOS 10.15+ или Linux (Ubuntu 18.04+)
  • Память: минимум 8 ГБ ОЗУ (для разработки на Raspberry Pi рекомендуется 16 ГБ)
  • Накопитель: 10 ГБ свободного места для инструментов разработки и библиотек
  • Порты USB: хотя бы один порт USB 2.0/3.0 для подключения устройства
  • Интернет: необходим для установки программ и загрузки библиотек

Программные требования:

  • Arduino IDE 2.0+ для платформ на базе Arduino (XIAO, Nicla Vision)
  • Python 3.8+ для разработки на Raspberry Pi
  • Git для управления версиями и доступа к примерам кода
  • Текстовый редактор или IDE, например VS Code или PyCharm

Аксессуары для оборудования:

  • USB-кабели: USB-C или Micro-USB (кабель должен передавать данные, а не только питание)
  • SD-карта: 32 ГБ и больше, класс 10, для Raspberry Pi
  • Блоки питания: подходящие для каждой платформы
  • Модули камеры: входят в состав большинства наборов или продаются отдельно

Установка программного обеспечения для конкретных платформ

Каждая аппаратная платформа требует собственного подхода к разработке, который отражает практику реальной встраиваемой инженерии. Системы на базе Arduino сосредоточены на эффективном использовании ресурсов и ограничениях реального времени, Raspberry Pi демонстрирует полноценные возможности периферийных вычислений, а специализированное ИИ-оборудование показывает методы аппаратного ускорения.

Выберите процедуры установки, подходящие для вашей платформы.

Платформы на базе Arduino (Nicla Vision, XIAOML Kit)

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

Установка Arduino IDE:

  1. Скачайте Arduino IDE 2.0 с сайта arduino.cc/software
  2. Установите программу, следуя мастеру установки для вашей платформы
  3. Запустите Arduino IDE и откройте меню File → Preferences (Файл → Настройки)
  4. Добавьте ссылки на пакеты поддержки плат:
    • для XIAOML Kit: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
    • для Nicla Vision: ссылка указана в Boards Manager (менеджере плат) Arduino IDE

Установка пакетов поддержки плат:

  1. Откройте Tools → Board → Boards Manager (Инструменты → Плата → Менеджер плат)
  2. Найдите свою платформу:
    • XIAOML Kit: введите «ESP32» и установите «esp32 by Espressif Systems»
    • Nicla Vision: введите «Arduino Mbed OS Nicla Boards» и установите пакет
  3. Выберите свою плату в меню Tools → Board (Инструменты → Плата)
  4. Установите нужные библиотеки через Library Manager (менеджер библиотек)

Основные библиотеки:

  • TensorFlow Lite Micro
  • драйверы камер для конкретной платы
  • библиотеки интерфейсов датчиков (I2C, SPI)

Платформа Grove Vision AI V2

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

Настройка SenseCraft AI:

  1. Создайте учётную запись на сайте sensecraft.seeed.cc
  2. Подключите Grove Vision AI V2 через USB
  3. Откройте устройство в веб-интерфейсе SenseCraft AI
  4. Для визуального программирования локальная установка программ не требуется

Настройка Arduino IDE (для собственной разработки):

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

Платформа Raspberry Pi

Среда Raspberry Pi соединяет ограничения встраиваемых систем с полноценными вычислительными возможностями: студенты знакомятся и с оптимизацией ресурсов, и с продвинутыми ML-фреймворками. Такой двойной взгляд показывает, как вычислительные ресурсы влияют на выбор алгоритмов и архитектуру системы.

Установка операционной системы:

  1. Скачайте Raspberry Pi Imager
  2. Запишите Raspberry Pi OS (рекомендуется 64-разрядная версия) на microSD-карту объёмом не меньше 32 ГБ
  3. Во время записи настройте доступ по SSH и учётные данные Wi-Fi
  4. Вставьте записанную SD-карту и включите Raspberry Pi

Настройка программной среды:

Следующие команды создают полноценную среду разработки ML на Python с правильным управлением зависимостями:

# Обновление системных пакетов
sudo apt update && sudo apt upgrade -y

# Установка инструментов разработки Python
# python3-pip: установщик пакетов Python
# python3-venv: создание виртуальных окружений
# python3-dev: заголовочные файлы для разработки на Python
sudo apt install python3-pip \
                 python3-venv \
                 python3-dev -y

# Установка зависимостей ML-фреймворков
# libatlas-base-dev: библиотека линейной алгебры (BLAS/LAPACK)
# libhdf5-dev: библиотека формата данных HDF5
# libhdf5-serial-dev: последовательная версия HDF5
sudo apt install libatlas-base-dev \
                 libhdf5-dev \
                 libhdf5-serial-dev -y

# Установка зависимостей компьютерного зрения
# libcamera-dev: библиотека интерфейса камеры
# python3-libcamera: привязки Python для libcamera
# python3-kms++: библиотека установки режима ядра (kernel mode setting)
sudo apt install libcamera-dev \
                 python3-libcamera \
                 python3-kms++ -y

# Создание виртуального окружения для проектов
python3 -m venv ~/ml_projects
source ~/ml_projects/bin/activate

# Установка основных пакетов ML
# tensorflow: основной фреймворк ML
# tensorflow-lite: оптимизирован для периферийных и мобильных устройств
# opencv-python: библиотека компьютерного зрения
# numpy: основа численных вычислений
pip install tensorflow \
            tensorflow-lite \
            opencv-python \
            numpy

Настройка инструментов разработки

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

Настройка последовательной связи (Serial)

Последовательная связь — основной интерфейс для отладки и сбора данных во встраиваемых системах. Она показывает поведение системы, без чего невозможно понять ограничения производительности и возможности оптимизации.

Windows:

  • установите подходящие драйверы USB-to-serial (CH340, FTDI или драйверы конкретной платы);
  • проверьте в диспетчере устройств, что плата распознаётся.

macOS/Linux:

  • большинство адаптеров USB-to-serial работают без дополнительных драйверов;
  • проверить распознавание устройства можно командой ls /dev/tty* (macOS/Linux);
  • добавьте пользователя в группу dialout: sudo usermod -a -G dialout $USER (Linux).

Настройка IDE

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

Параметры Arduino IDE:

  • выберите нужный COM-порт в меню Tools → Port (Инструменты → Порт);
  • задайте правильную плату и процессор;
  • проверьте скорость загрузки (обычно 115200 бод);
  • включите подробный вывод при компиляции для отладки.

Разработка на Raspberry Pi:

  • настройте SSH-ключи для удалённой разработки;
  • установите VS Code с расширениями Python и Remote SSH;
  • настройте доступ к Jupyter Notebook для интерактивной работы.

Проверка среды

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

Тесты распознавания оборудования

Ниже приведены проверочные процедуры, которые тестируют основные функции, необходимые для лабораторных работ: обмен данными с оборудованием и корректную работу программных библиотек.

Платформы Arduino:

void setup() {
  // Запуск последовательного порта на скорости 115200 бод
  Serial.begin(115200);
  Serial.println("Development environment test");
  Serial.print("Board: ");
  Serial.println(ARDUINO_BOARD);
}

void loop() {
  Serial.println("Environment operational");
  delay(1000);
}

Raspberry Pi:

# Проверка интерфейса камеры
libcamera-hello --timeout 5000

# Проверка среды ML на Python
python3 -c \
  "import tensorflow as tf; print('TensorFlow:', tf.__version__)"
python3 -c \
  "import cv2; print('OpenCV:', cv2.__version__)"

Grove Vision AI V2:

  • проверьте, что устройство видно в веб-интерфейсе SenseCraft AI;
  • протестируйте базовое развёртывание модели через визуальный интерфейс программирования.

Типичные проблемы при настройке и их решения

Трудности при настройке — обычное дело, и они дают ценный опыт в понимании ограничений встраиваемых систем и методов отладки. Ниже описаны решения для наиболее частых проблем.

Проблемы с подключением устройства:

  • убедитесь, что USB-кабель передаёт данные, а не только питание;
  • установите драйверы USB для платформы, если устройство не распознаётся;
  • попробуйте другие порты USB или USB-хаб, если соединение нестабильно.

Ошибки компиляции:

  • проверьте, что в IDE выбраны правильная плата и процессор;
  • убедитесь, что все нужные библиотеки установлены в совместимых версиях;
  • проверьте, достаточно ли места на диске для компиляции.

Ошибки во время работы:

  • убедитесь, что блок питания достаточно мощный (особенно для работы камеры);
  • проверьте совместимость и формат SD-карты (Raspberry Pi);
  • проверьте распределение памяти под ML-модели в рамках ограничений платформы.

Сетевое подключение (платформы с Wi-Fi):

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

Поиск и устранение неисправностей

Типичные аппаратные проблемы:

  • Устройство не распознаётся: убедитесь, что USB-кабель передаёт данные, попробуйте другие порты;
  • Ошибки загрузки: проверьте выбор платы и настройки порта в IDE;
  • Проблемы с питанием: проверьте достаточность питания, особенно для работы камеры;
  • Ошибки памяти: убедитесь, что модель укладывается в ограничения платформы.

Программные проблемы:

  • Конфликты библиотек: используйте совместимые версии, указанные в руководствах по настройке;
  • Ошибки компиляции: проверьте, что все зависимости установлены правильно;
  • Сетевое подключение: проверьте настройки брандмауэра и разрешения сети.

Ресурсы для конкретных платформ:

Поддержка сообщества:

  • GitHub Issues: сообщайте об ошибках и предлагайте функции через репозиторий проекта;
  • Форумы: сообщества конкретных платформ на сайтах Arduino, Raspberry Pi и Seeed Studio;
  • Stack Overflow: задавайте вопросы с тегами соответствующих платформ.

Готовность к лабораторным работам

Когда среда разработки настроена и проверена, у вас есть базовые инструменты для программирования встраиваемых ML-систем. Навыки, полученные при настройке среды (понимание цепочек инструментов, управление зависимостями и проверка работоспособности системы), пригодятся во всех последующих лабораторных работах.

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

Рекомендуемая последовательность:

  1. Начните с простых упражнений на датчики, чтобы проверить работу оборудования.
  2. Перейдите к приложениям ML с одним типом данных (изображения или звук).
  3. Затем переходите к мультимодальным упражнениям и задачам оптимизации.

Каждое лабораторное упражнение включает подробные процедуры реализации, ожидаемые показатели производительности и советы по поиску неисправностей с учётом требований конкретного проекта. Созданная вами среда разработки открывает путь к полному спектру встраиваемых ML-приложений и методов оптимизации.

Платформы

Примечание о переводе. Цены указаны в долларах США, как в оригинале, и ориентировочны: перед покупкой проверьте актуальную стоимость у продавца.

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

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

Платформа в центре внимания

Полный комплект XIAOML Kit со всеми компонентами
Полный комплект XIAOML Kit со всеми компонентами

XIAOML Kit — самая новая из наших учебных аппаратных платформ (выпущена 31 июля 2025 года). Это полноценная среда разработки TinyML для изучения ML-систем: встроенная беспроводная связь, камера, несколько датчиков и обширная документация. Эта компактная плата показывает, как современные встраиваемые системы могут эффективно обеспечивать продвинутые возможности машинного обучения в недорогом исполнении.

Обзор платформ

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

ПлатформаОсновная тема обученияСтоимостьЭнергопрофильЛучше всего для
XIAOML KitIoT и беспроводное ML~$40НизкоеПроекты с ограниченным бюджетом
Arduino NiclaПроектирование со сверхнизким энергопотреблением~$95СверхнизкоеУстройства с питанием от батареи
Grove Vision AIАппаратное ускорение~$25СреднееПромышленные применения
Raspberry PiПолноценные ML-фреймворки$60–145ВысокоеСложные периферийные вычисления

Стратегия выбора платформ

Сравнение платформ

В таблице ниже приведено подробное техническое сравнение всех четырёх платформ.

ХарактеристикаXIAOML KitRaspberry PiArduino NiclaGrove Vision AI V2
Диапазон стоимости (USD)~$40$60–145~$95~$25
ЭнергопотреблениеНизкоеВысокоеСверхнизкоеСреднее
Вычислительная мощностьСредняяОчень высокаяНизкаяВысокая (NPU)
Объём памяти8 МБ1–16 ГБ2 МБ16 МБ
Основной сценарийIoT-сетиПериферийные вычисленияУстройства от батареиПромышленный ИИ
ML-фреймворкTF LiteTensorFlow, PyTorchTensorFlow LiteSenseCraft AI
Среда разработкиArduino/PlatformIOPython/LinuxArduino IDEВизуальный интерфейс / код

Матрица сравнения платформ

Рекомендации по выбору платформы

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

Учебная задача / применениеXIAOML KitRaspberry PiArduino NiclaGrove Vision AI V2
Основы встраиваемых систем✓Ограниченно✓✓
Беспроводная связь✓✓✓
Сверхнизкое энергопотребление✓
Полноценные ML-фреймворки✓
Аппаратное ускорение✓
Компьютерное зрение в реальном времениОграниченно✓✓✓
Интеграция периферийных устройств с облаком✓✓✓
Промышленное развёртывание✓✓✓

Матрица возможностей платформ

Технические характеристики платформ

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

XIAOML Kit (Seeed Studio)

Лучше всего для: IoT и беспроводного ML. XIAOML Kit хорошо справляется с беспроводной связью и проектами с ограниченным бюджетом. Он идеально подходит для изучения сетей IoT-датчиков, систем удалённого мониторинга и беспроводного ML-инференса, когда нужна надёжная связь в компактном и недорогом исполнении.

XIAOML Kit объединяет плату XIAO ESP32S3 Sense и плату расширения, образуя полноценную среду разработки TinyML. Одна только XIAO ESP32S3 Sense обеспечивает камеру и микрофон (зрение и звук), а плата расширения добавляет 6-осевой инерциальный измерительный блок (IMU) для классификации движения. Название «XIAO» (小) в переводе с китайского означает «крошечный» и отражает форм-фактор платы: мм.

XIAOML Kit с платой расширения
XIAOML Kit с платой расширения

Архитектура процессора: Двухъядерный ESP32-S3 с ядрами Xtensa LX7 на частоте 240 МГц

Иерархия памяти: 8 МБ PSRAM и 8 МБ флеш-памяти

Связь: Wi-Fi 802.11 b/g/n и Bluetooth 5.0

Встроенные датчики:

  • XIAO ESP32S3 Sense: камера OV2640, цифровой микрофон
  • Плата расширения: 6-осевой инерциальный измерительный блок (IMU), 0,42-дюймовый OLED-дисплей

Характеристики питания: Питание 3,3 В с несколькими режимами пониженного энергопотребления

Среда разработки: Поддерживаются Arduino IDE и PlatformIO, есть обширная экосистема библиотек. Программирование ведётся на C/C++ с абстракциями в стиле Arduino; опытные пользователи могут работать напрямую с ESP-IDF.

Область применения: Сети IoT-датчиков, системы удалённого мониторинга, беспроводный ML-инференс, проекты с ограниченным бюджетом

Arduino Nicla Vision

Лучше всего для: проектирования со сверхнизким энергопотреблением. Arduino Nicla Vision оптимизирована для устройств с питанием от батареи и систем непрерывного сенсорного наблюдения. Она идеально подходит для изучения проектирования со сверхнизким энергопотреблением, систем классификации изображений и обнаружения объектов, где время работы от батареи измеряется месяцами, а не часами.

Nicla Vision — пример профессиональных встраиваемых систем технического зрения, построенных на микроконтроллере STM32H7. Эта платформа показывает, как специализированная аппаратная архитектура позволяет выполнять сложный ML-инференс в жёстких ограничениях ресурсов.

Arduino Nicla Vision с модулем камеры
Arduino Nicla Vision с модулем камеры

Архитектура процессора: Двухъядерный STM32H747: ARM Cortex-M7 и Cortex-M4 на частоте 480 МГц

Иерархия памяти: 1 МБ встроенной SRAM и 2 МБ флеш-памяти, а также 16 МБ внешней QSPI-флеш-памяти

Встроенные датчики: Камера GC2145, цифровой микрофон MP34DT05, 6-осевой IMU

Характеристики питания: Питание 3,3 В, оптимизированное для работы от батареи

Среда разработки: Поддерживаются Arduino IDE и OpenMV IDE со специализированными библиотеками компьютерного зрения. Для быстрого прототипирования доступен MicroPython, а для промышленного развёртывания — C/C++.

Область применения: Устройства с питанием от батареи, системы классификации изображений, обнаружение объектов, непрерывное сенсорное наблюдение

Grove Vision AI V2

Лучше всего для: аппаратного ускорения. Grove Vision AI V2 содержит специализированный аппаратный блок для нейронных сетей, благодаря которому производительность растёт на порядки. Плата идеально подходит для изучения систем промышленного контроля, аналитики видео в реальном времени и продвинутого обнаружения объектов, где нужен инференс с ускорением на NPU.

Grove Vision AI V2 включает выделенный блок нейронной обработки (NPU), который демонстрирует аппаратно ускоренный ML-инференс. Эта платформа показывает, как специализированные ИИ-процессоры дают прирост производительности на порядки по сравнению с реализациями, работающими только программно.

Grove Vision AI V2 с NPU
Grove Vision AI V2 с NPU

Архитектура процессора: ARM Cortex-M55 со встроенным NPU Ethos-U55

Иерархия памяти: 16 МБ внешней памяти для хранения модели и данных

Нейропроцессорный блок: Выделенный аппаратный ускоритель для ML-инференса

Интерфейс камеры: Стандартный разъём CSI, поддерживающий различные модули камер

Аудиовход: Встроенный цифровой микрофон

Среда разработки: Визуальная платформа программирования SenseCraft AI для разработки без написания кода, а также поддержка Arduino IDE для собственных приложений. Доступны как графическое программирование, так и традиционная разработка на C/C++.

Область применения: Системы промышленного контроля, аналитика видео в реальном времени, продвинутое обнаружение объектов, инференс с ускорением на NPU

Raspberry Pi (модели 4/5 и Zero 2W)

Лучше всего для: полноценных ML-фреймворков. Raspberry Pi соединяет встраиваемые системы и традиционные вычисления, предоставляя полноценную среду Linux для продвинутых ML-приложений. Она идеальна для изучения периферийных ИИ-шлюзов, сложных систем компьютерного зрения, развёртывания языковых моделей и мультимодальных ИИ-приложений, где нужны полные вычислительные возможности.

Семейство Raspberry Pi соединяет встраиваемые системы и традиционные вычисления: оно даёт полноценную среду Linux и при этом остаётся доступным для обучения. Эта платформа показывает, как увеличение вычислительных ресурсов открывает путь к сложным ML-приложениям.

Raspberry Pi 5 и Pi Zero 2W для сравнения
Raspberry Pi 5 и Pi Zero 2W для сравнения

Архитектура процессора: ARM Cortex-A76 (Pi 5) или Cortex-A53 (Zero 2W)

Иерархия памяти: Оперативная память DDR4 объёмом 1–16 ГБ в зависимости от модели

Накопитель: Основное хранилище — microSD-карта, расширение через USB 3.0

Связь: Gigabit Ethernet, Wi-Fi, Bluetooth, несколько портов USB

Интерфейс камеры: Выделенный разъём CSI и поддержка USB-камер

Операционная система: Raspberry Pi OS на основе Debian (полноценный дистрибутив Linux)

Среда разработки: Полноценная среда разработки Linux с нативной поддержкой Python, C/C++ и JavaScript. Менеджеры пакетов (apt, pip) дают доступ к обширному набору ML-библиотек, включая TensorFlow, PyTorch и OpenCV.

Область применения: Периферийные ИИ-шлюзы, системы компьютерного зрения, развёртывание языковых моделей, мультимодальные ИИ-приложения

Начало работы

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

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

Обзор

Эти лабораторные работы дают уникальную возможность получить практический опыт работы с системами машинного обучения (ML). В отличие от работы с большими моделями, которым нужны ресурсы уровня дата-центра, эти упражнения позволяют напрямую работать с оборудованием и программным обеспечением с помощью TinyML. Такой практический подход даёт осязаемое понимание проблем и возможностей развёртывания ИИ, пусть и в крошечном масштабе. При этом принципы в целом те же, что и при работе с более крупными системами.

Arduino Nicla Vision
Nicla Vision. Источник: Arduino.

Где купить

Arduino Nicla Vision можно купить в официальном магазине Arduino:

Предварительные требования

  • Плата Nicla Vision: убедитесь, что плата у вас есть.
  • USB-кабель: для подключения платы к компьютеру.
  • Сеть: доступ в интернет для загрузки необходимого программного обеспечения.

Настройка

Упражнения

МодальностьЗадачаОписаниеСсылка
ЗрениеКлассификация изображенийНаучитесь классифицировать изображенияОткрыть
ЗрениеОбнаружение объектовРеализуйте обнаружение объектовОткрыть
ЗвукРаспознавание ключевых словИзучите системы распознавания голосаОткрыть
IMUКлассификация движения и обнаружение аномалийКлассифицируйте данные движения и находите аномалииОткрыть

Классификация изображений

Мультфильм в стиле 1950-х: компактное устройство с камерой на деревянном столе
Промпт DALL·E 3: мультфильм в стиле 1950-х с компактным электронным устройством и модулем камеры на деревянном столе. На экране слева синие роботы, справа зелёные периквито. Светодиоды на устройстве показывают классы, а персонажи в ретро-одежде с интересом наблюдают.

Обзор

Начиная изучение встраиваемого машинного обучения, или TinyML, невозможно не заметить, как сильно компьютерное зрение (CV) и искусственный интеллект (ИИ) меняют нашу жизнь. Эти две взаимосвязанные дисциплины переопределяют то, что машины способны воспринимать и делать: от автономных автомобилей и робототехники до здравоохранения и систем наблюдения.

Всё чаще мы сталкиваемся с революцией в области искусственного интеллекта, где, как отмечает Gartner, Edge AI имеет очень большой потенциал влияния, и это уже сейчас!

Радар технологий Gartner с Edge AI в центре

В «яблочке» радара находится периферийное компьютерное зрение (Edge Computer Vision). Когда речь заходит о машинном обучении (ML) применительно к зрению, в первую очередь приходит на ум классификация изображений — своего рода «Hello World» для ML!

В этой лабораторной работе мы разберём проект компьютерного зрения, который использует свёрточные нейронные сети (CNN) для классификации изображений в реальном времени. Опираясь на экосистему TensorFlow, мы возьмём предобученную модель MobileNet и адаптируем её для развёртывания на периферийном устройстве. Основное внимание будет уделено оптимизации модели, чтобы она эффективно работала на аппаратном обеспечении с ограниченными ресурсами и при этом не теряла точности.

Мы применим такие приёмы, как квантизация и прунинг (удаление избыточных весов), чтобы снизить вычислительную нагрузку. К концу руководства у вас будет работающий прототип, который классифицирует изображения в реальном времени и работает на маломощной встраиваемой системе на базе платы Arduino Nicla Vision.

Компьютерное зрение

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

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

Классификация изображений и обнаружение объектов в компьютерном зрении

Обе модели можно реализовать на крошечных устройствах вроде Arduino Nicla Vision и использовать в реальных проектах. В этой главе мы разберём классификацию изображений.

Цель проекта классификации изображений

Первый шаг любого проекта ML — определить цель. В данном случае цель — обнаружить и классифицировать два конкретных объекта, присутствующих на одном изображении. Для проекта мы возьмём две небольшие игрушки: робота и маленького бразильского попугая (по имени Periquito). Также мы соберём изображения фона, на которых этих двух объектов нет.

Игрушечный робот и попугай Periquito на фоне

Сбор данных

Как только мы определили цель проекта ML, следующий и самый важный шаг — сбор набора данных. Для съёмки изображений можно использовать:

  • инструмент Web Serial Camera;
  • Edge Impulse Studio;
  • OpenMV IDE;
  • смартфон.

Здесь мы будем использовать OpenMV IDE.

Сбор набора данных в OpenMV IDE

Сначала создадим на компьютере папку, в которую будут сохраняться данные, например «data». Затем в OpenMV IDE откроем Tools > Dataset Editor и выберем New Dataset, чтобы начать сбор набора данных:

Создание нового набора данных в Dataset Editor OpenMV IDE

IDE попросит открыть файл, в который будут сохраняться данные. Выберите созданную папку «data». Обратите внимание: на левой панели появятся новые значки.

Новые значки на левой панели OpenMV IDE

Используя верхний значок (1), введите имя первого класса, например «periquito»:

Ввод имени класса periquito в Dataset Editor

Запустите скрипт dataset_capture_script.py и нажмите значок камеры (2), чтобы начать съёмку изображений:

Съёмка изображений для класса periquito

Повторите ту же процедуру для остальных классов.

Съёмка изображений для остальных классов

Рекомендуем около 50–60 изображений для каждой категории. Старайтесь снимать с разных ракурсов, на разном фоне и при разном освещении.

Сохраняемые изображения имеют размер кадра QVGA и формат цвета RGB565 (формат цветных пикселей).

Когда набор данных собран, закройте инструмент Dataset Editor через Tools > Dataset Editor.

В итоге на компьютере появится набор данных с тремя классами: periquito, robot и background.

Набор данных с тремя классами на компьютере

Теперь вернитесь в Edge Impulse Studio и загрузите набор данных в созданный проект.

Обучение модели в Edge Impulse Studio

Для обучения модели мы воспользуемся Edge Impulse Studio. Войдите в аккаунт и создайте новый проект:

Создание нового проекта в Edge Impulse Studio

Здесь можно клонировать похожий проект: NICLA-Vision_Image_Classification.

Набор данных

Работая в EI Studio (или просто Studio), мы пройдём четыре основных шага, чтобы подготовить модель к использованию на плате Nicla Vision: Dataset (набор данных), Impulse (конвейер), Tests (тесты) и Deploy (развёртывание на периферийном устройстве, в данном случае на NiclaV).

Четыре этапа работы в Edge Impulse Studio

Что касается набора данных, важно отметить: исходный набор, собранный в OpenMV IDE, будет разделён на обучающую, валидационную и тестовую выборки. Тестовая выборка с самого начала откладывается и используется только на этапе тестирования после обучения. Валидационная выборка применяется во время обучения.

EI Studio сам отберёт часть обучающих данных для валидации.

Разделение данных на обучающую и валидационную выборки в Studio

В Studio откройте вкладку Data acquisition, а в разделе UPLOAD DATA загрузите файлы выбранных категорий с компьютера:

Загрузка файлов категорий в раздел UPLOAD DATA

Разделение исходного набора на обучающую и тестовую выборки оставьте Studio и выберите метку для этих данных:

Выбор метки для загружаемых данных

Повторите процедуру для всех трёх классов.

Можно выбрать папку и загрузить все файлы сразу.

В итоге в Studio должны появиться ваши «сырые данные» (raw data):

Сырые данные в Studio после загрузки

Обратите внимание: при начале загрузки может появиться всплывающее окно с вопросом, создаёте ли вы проект обнаружения объектов (Object Detection). Выберите [NO].

Всплывающее окно с вопросом о типе проекта

Тип проекта всегда можно изменить в разделе Dashboard: One label per data item (классификация изображений):

Раздел Dashboard с выбором одной метки на элемент данных

При желании Studio позволяет просмотреть все данные проекта целиком. Можно очищать, проверять или менять метки, щёлкая по отдельным элементам. В нашем случае данные выглядят корректно.

Просмотр всех данных проекта в Studio

Проектирование Impulse

На этом этапе нужно определить, как:

  • предобработать данные: изменить размер отдельных изображений и выбрать color depth (RGB или оттенки серого);
  • задать модель: в данном случае это Transfer Learning (Images) — дообучение предобученной модели классификации изображений MobileNet V2 на наших данных. Этот метод хорошо работает даже с небольшими наборами изображений (в нашем случае около 150 изображений).

Блоки обработки изображений и Transfer Learning в Impulse

Перенос обучения (Transfer Learning) с MobileNet упрощает обучение модели. Это особенно полезно в условиях ограниченных ресурсов и при малом количестве размеченных данных. MobileNet, известная своей лёгкой архитектурой, — предобученная модель, которая уже выучила полезные признаки на большом наборе данных (ImageNet).

Архитектура MobileNet

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

Использование признаков предобученной модели

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

Перейдите на вкладку Impulse Design и создайте impulse, задав размер изображения и сжатие к квадратной форме (без обрезки). Выберите блоки Image и Transfer Learning. Сохраните Impulse.

Создание Impulse с блоками Image и Transfer Learning

Предобработка изображений

Все входные изображения QVGA/RGB565 будут преобразованы в 27 648 признаков ().

Параметры предобработки изображений

Нажмите [Save parameters] и сгенерируйте все признаки:

Генерация признаков в Studio

Проектирование модели

В 2017 году Google представил MobileNetV1 — семейство универсальных нейронных сетей для компьютерного зрения, созданных с расчётом на мобильные устройства и поддерживающих классификацию, обнаружение и другие задачи. MobileNet — компактные модели с малой задержкой и низким энергопотреблением, параметры которых подобраны под ресурсные ограничения разных сценариев. В 2018 году Google выпустил MobileNetV2: Inverted Residuals and Linear Bottlenecks.

MobileNet V1 и MobileNet V2 ориентированы на эффективность в мобильных устройствах и во встраиваемом зрении, но различаются по сложности архитектуры и производительности. Обе используют разделяемые по глубине свёртки (depthwise separable convolutions), чтобы снизить вычислительные затраты, однако MobileNet V2 вводит инвертированные остаточные блоки (Inverted Residual Blocks) и линейные бутылочные горлышки (Linear Bottlenecks), чтобы повысить производительность. Эти новшества позволяют V2 выделять более сложные признаки меньшим количеством параметров, поэтому она вычислительно эффективнее и, как правило, точнее предшественницы. Кроме того, V2 использует нелинейную активацию в промежуточном слое расширения, а для слоя бутылочного горлышка по-прежнему применяет линейную активацию: такое решение, как показали исследования, помогает сохранять важную информацию по всей сети. MobileNet V2 предлагает оптимизированную архитектуру с более высокой точностью и эффективностью, её мы и будем использовать в проекте.

Хотя базовая архитектура MobileNet уже компактна и быстра, в некоторых задачах модель должна быть ещё меньше и быстрее. MobileNet вводит простой параметр (альфа) — множитель ширины (width multiplier), с помощью которого строятся меньшие и менее требовательные к вычислениям модели. Множитель ширины равномерно «утончает» сеть на каждом слое.

Edge Impulse Studio может использовать как MobileNetV1 (изображения ), так и V2 (изображения или ) с несколькими значениями (от 0,05 до 1,0). Например, наибольшая точность достигается у V2 с изображениями и . Однако есть компромисс: чем выше точность, тем больше памяти (около 1,3 МБ ОЗУ и 2,6 МБ ПЗУ) нужно для работы модели и тем выше задержка. Наименьший объём обеспечивает MobileNetV1 с (около 53,2 КБ ОЗУ и 101 КБ ПЗУ).

Варианты моделей MobileNet в Studio и их требования к памяти

Для этого проекта мы используем MobileNetV2 96x96 0.1 (или 0.05) с ожидаемым расходом памяти 265,3 КБ ОЗУ. Этой модели должно хватить для Nicla Vision с 1 МБ SRAM. На вкладке Transfer Learning выберите эту модель:

Выбор модели MobileNetV2 96x96 0.1 на вкладке Transfer Learning

Обучение модели

Ещё одна полезная техника в глубоком обучении — аугментация данных (Data Augmentation). Это метод повышения точности моделей машинного обучения за счёт создания дополнительных искусственных данных. Система аугментации вносит небольшие случайные изменения в обучающие данные в процессе обучения (например, отражает, обрезает или поворачивает изображения).

Заглянем «под капот» и посмотрим, как Edge Impulse реализует политику аугментации данных:

# Реализация политики аугментации данных
def augment_image(image, label):
    # Случайно отражаем изображение по горизонтали
    image = tf.image.random_flip_left_right(image)

    # Увеличиваем размер изображения, затем случайно обрезаем его до
    # исходных размеров
    resize_factor = random.uniform(1, 1.2)
    new_height = math.floor(resize_factor * INPUT_SHAPE[0])
    new_width = math.floor(resize_factor * INPUT_SHAPE[1])
    image = tf.image.resize_with_crop_or_pad(image, new_height,
                                             new_width)
    image = tf.image.random_crop(image, size=INPUT_SHAPE)

    # Меняем яркость изображения
    image = tf.image.random_brightness(image, max_delta=0.2)

    return image, label

Такие вариации во время обучения помогают модели не срезать углы, «запоминая» поверхностные признаки в обучающих данных. Благодаря этому она лучше отражает глубинные закономерности набора данных.

Последний слой модели будет содержать 12 нейронов с dropout 15 % для предотвращения переобучения. Вот результат обучения:

Результат обучения модели в Studio

Результат отличный: оценочная задержка составляет 77 мс, что должно обеспечить около 13 кадров в секунду (FPS) при инференсе.

Тестирование модели

Вкладка тестирования модели в Studio

Теперь возьмём набор данных, отложенный в начале проекта, и подадим его на вход обученной модели:

Запуск тестовой выборки через модель

Результат снова отличный.

Результаты тестирования модели

Развёртывание модели

Теперь обученную модель можно развернуть как прошивку (FW) и запустить через OpenMV IDE на MicroPython, либо развернуть как библиотеку C/C++ или Arduino.

Варианты развёртывания модели в Studio

Библиотека Arduino

Сначала развернём модель как библиотеку Arduino:

Экспорт модели как библиотеки Arduino

Библиотеку нужно установить в Arduino IDE как архив .zip, а затем запустить скетч nicla_vision_camera.ino, который находится в разделе Examples под именем библиотеки.

По умолчанию Arduino Nicla Vision выделяет 512 КБ ОЗУ для ядра M7 и ещё 244 КБ в адресном пространстве M4. В коде это выделение изменено на 288 КБ, чтобы модель гарантированно работала на устройстве (malloc_addblock((void*)0x30000000, 288 * 1024);).

Результат хороший: измеренная задержка составила 86 мс.

Результаты инференса в Arduino IDE

Короткое видео с результатами инференса:

OpenMV

Обученную модель можно использовать с OpenMV двумя способами: как библиотеку и как прошивку (FW). При выборе прошивки Edge Impulse Studio генерирует оптимизированные модели, библиотеки и фреймворки, которые нужны для инференса. Рассмотрим этот вариант.

На вкладке Deploy выберите OpenMV Firmware и нажмите [Build].

Сборка OpenMV Firmware на вкладке Deploy

На компьютере появится ZIP-файл. Откройте его:

ZIP-архив с прошивкой OpenMV

Используйте инструмент Bootloader в OpenMV IDE, чтобы загрузить прошивку на плату (1):

Инструмент Bootloader в OpenMV IDE

Выберите нужный файл (.bin для Nicla-Vision):

Выбор файла .bin для Nicla-Vision

Когда загрузка завершится, нажмите OK:

Подтверждение завершения загрузки прошивки

Если появится сообщение, что прошивка устарела, НЕ ОБНОВЛЯЙТЕ её. Выберите [NO].

Сообщение об устаревшей прошивке

Теперь откройте скрипт ei_image_classification.py, скачанный из Studio, и файл .bin для Nicla.

Открытые скрипт ei_image_classification.py и файл .bin

Запустите его. Направляя камеру на объекты, которые нужно классифицировать, вы увидите результат инференса в Serial Terminal.

Результат инференса в Serial Terminal

Результат классификации появляется в Serial Terminal. Если результат трудно читать, добавьте в код новую строку с задержкой:

import time
while True:
...
	time.sleep_ms(200)  # Задержка 0,2 секунды

Изменение кода для вывода меток

Код от Edge Impulse можно изменить так, чтобы для проверки видеть результат инференса прямо на изображении в OpenMV IDE.

Загрузите код с GitHub или измените его, как показано ниже:

# Marcelo Rovai — NICLA Vision — классификация изображений
# Адаптировано из примера Edge Impulse — OpenMV Image Classification
# @24March25

import sensor
import time
import ml

sensor.reset()  # Сброс и инициализация датчика.
# Формат пикселей RGB565 (или GRAYSCALE)
sensor.set_pixformat(sensor.RGB565)
# Размер кадра QVGA (320x240)
sensor.set_framesize(sensor.QVGA)
sensor.set_windowing((240, 240))  # Окно 240x240.
sensor.skip_frames(time=2000)  # Даём камере настроиться.

model = ml.Model("trained")#mobilenet, load_to_fb=True)

clock = time.clock()

while True:
    clock.tick()
    img = sensor.snapshot()

    fps = clock.fps()
    lat = clock.avg()
    print("**********\nPrediction:")
    # Объединяем метки и уверенность в список кортежей и сортируем его
    # по значениям уверенности.
    sorted_list = sorted(
        zip(model.labels, model.predict([img])[0].flatten().tolist()),
      key=lambda x: x[1], reverse=True
    )

    # Выводим только класс с наибольшей вероятностью
    max_val = sorted_list[0][1]
    max_lbl = sorted_list[0][0]

    if max_val < 0.5:
        max_lbl = 'uncertain'

    print("{} with a prob of {:.2f}".format(max_lbl, max_val))
    print("FPS: {:.2f} fps ==> latency: {:.0f} ms".format(fps, lat))

    # Выводим метку с наибольшей вероятностью на просмотрщике изображения
    img.draw_string(
    10, 10,
    max_lbl + "\n{:.2f}".format(max_val),
    mono_space = False,
    scale=3
    )

    time.sleep_ms(500)  # Пауза 0.5 секунды

Вот результат:

Вывод метки и уверенности поверх изображения в OpenMV IDE

Обратите внимание: задержка (136 мс) почти вдвое больше, чем при работе напрямую через Arduino IDE (86 мс). Это происходит потому, что мы используем IDE как интерфейс, а также из-за времени ожидания готовности камеры. Если запустить таймер непосредственно перед инференсом, задержка должна снизиться примерно до 70 мс.

При подключении к IDE NiclaV работает примерно вдвое медленнее. После отключения FPS должен вырасти.

Пост-обработка со светодиодами

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

Светодиоды платы Nicla Vision

Чтобы это сделать, загрузите код с GitHub или дополните предыдущий код светодиодами:

# Marcelo Rovai — NICLA Vision — классификация изображений со светодиодами
# Адаптировано из примера Edge Impulse — OpenMV Image Classification
# @24Aug23

import sensor, time, ml
from machine import LED

ledRed = LED("LED_RED")
ledGre = LED("LED_GREEN")
ledBlu = LED("LED_BLUE")

sensor.reset()  # Сброс и инициализация датчика.
# Формат пикселей RGB565 (или GRAYSCALE)
sensor.set_pixformat(sensor.RGB565)
# Размер кадра QVGA (320x240)
sensor.set_framesize(sensor.QVGA)
sensor.set_windowing((240, 240))  # Окно 240x240.
sensor.skip_frames(time=2000)  # Даём камере настроиться.

model = ml.Model("trained")#mobilenet, load_to_fb=True)

ledRed.off()
ledGre.off()
ledBlu.off()

clock = time.clock()

def setLEDs(max_lbl):
	if max_lbl == 'uncertain':
		ledRed.on()
		ledGre.off()
		ledBlu.off()

	if max_lbl == 'periquito':
		ledRed.off()
		ledGre.on()
		ledBlu.off()

	if max_lbl == 'robot':
		ledRed.off()
		ledGre.off()
		ledBlu.on()

	if max_lbl == 'background':
		ledRed.off()
		ledGre.off()
		ledBlu.off()

while True:
    img = sensor.snapshot()

    clock.tick()
    fps = clock.fps()
    lat = clock.avg()
    print("**********\nPrediction:")
    sorted_list = sorted(
        zip(model.labels, model.predict([img])[0].flatten().tolist()),
      key=lambda x: x[1], reverse=True
    )

    # Выводим только класс с наибольшей вероятностью
    max_val = sorted_list[0][1]
    max_lbl = sorted_list[0][0]

    if max_val < 0.5:
        max_lbl = 'uncertain'

    print("{} with a prob of {:.2f}".format(max_lbl, max_val))
    print("FPS: {:.2f} fps ==> latency: {:.0f} ms".format(fps, lat))

    # Выводим метку с наибольшей вероятностью на просмотрщике изображения
    img.draw_string(
    10, 10,
    max_lbl + "\n{:.2f}".format(max_val),
    mono_space = False,
    scale=3
    )

    setLEDs(max_lbl)
    time.sleep_ms(200)  # Пауза 0.2 секунды

Теперь каждый раз, когда какой-либо класс получает результат выше 0,8, загорается соответствующий светодиод:

  • Красный светодиод горит: Uncertain (ни один класс не выше 0,8);
  • Зелёный светодиод горит: Periquito > 0,8;
  • Синий светодиод горит: Robot > 0,8;
  • Все светодиоды выключены: Background > 0,8.

Вот результат:

Светодиоды, отражающие результат классификации

Подробнее:

Подробный вид работы светодиодной индикации

Неофициальный сравнительный тест классификации изображений

Для встраиваемого машинного обучения (TinyML) можно использовать несколько отладочных плат. Чаще всего для задач компьютерного зрения с малым энергопотреблением применяют ESP32-CAM, Seeed XIAO ESP32S3 Sense, Arduino Nicla Vision и Arduino Portenta.

Сравнение плат для TinyML: ESP32-CAM, XIAO ESP32S3 Sense, Nicla Vision и Portenta

Воспользовавшись возможностью, мы развернули одну и ту же обученную модель на ESP-CAM, XIAO и Portenta (для последней модель обучили заново на изображениях в оттенках серого, чтобы она подходила к её камере). Вот результат, когда модели развёрнуты как библиотеки Arduino:

Результаты сравнения моделей на разных платах

Итоги

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

  • Автономный транспорт: слияние датчиков, данные лидара и алгоритмы компьютерного зрения для навигации и принятия решений.
  • Здравоохранение: автоматическая диагностика заболеваний по снимкам МРТ, рентгена и КТ.
  • Розничная торговля: автоматические кассовые системы, которые распознают товары при проезде через сканер.
  • Безопасность и наблюдение: распознавание лиц, обнаружение аномалий и отслеживание объектов в видеопотоке в реальном времени.
  • Дополненная реальность: обнаружение и классификация объектов для наложения цифровой информации на реальный мир.
  • Промышленная автоматизация: визуальный контроль продукции, предиктивное обслуживание, наведение роботов и дронов.
  • Сельское хозяйство: мониторинг посевов с дронов и автоматизированная уборка урожая.
  • Обработка естественного языка: создание подписей к изображениям и визуальные вопросно-ответные системы.
  • Распознавание жестов: для игр, перевода жестового языка и взаимодействия человека с машиной.
  • Рекомендательные системы: рекомендации товаров на основе изображений в электронной коммерции.

Ресурсы

Распознавание ключевых слов (KWS)

Мультфильм в стиле 1950-х: две учёные в лаборатории звука у платы с микрофоном и Arduino IDE
Промпт DALL·E 3: мультфильм в стиле 1950-х в старинной лаборатории аудиоисследований. В центре две учёные-женщины. Одна держит увеличительное стекло и внимательно рассматривает старую схему, другая делает записи. На деревянном столе несколько плат с датчиками, в том числе с микрофоном. За платами стоит компьютер с округлой задней панелью, на экране которого открыта Arduino IDE. В IDE видны код назначения выводов светодиодов и инференс модели машинного обучения для распознавания голосовых команд. В окне Serial Monitor выводятся распознанные команды «yes» и «no». Атмосфера комнаты ностальгическая: старинные лампы, классические инструменты анализа звука и графики БПФ и временных кривых.

Обзор

Мы уже исследовали плату Nicla Vision в приложениях классификации изображений и обнаружения объектов. Теперь переходим к голосовым приложениям: проект распознавания ключевых слов (Keyword Spotting, KWS).

Как рассказано в практическом руководстве «Инженерия признаков для KWS», распознавание ключевых слов (KWS) используется во многих системах распознавания речи и позволяет устройствам реагировать на определённые слова или фразы. Хотя эта технология лежит в основе популярных устройств вроде Google Assistant или Amazon Alexa, её можно реализовать и на маленьких маломощных устройствах. Это руководство проведёт вас через реализацию системы KWS с помощью TinyML на отладочной плате Nicla Vision с цифровым микрофоном.

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

Как работает голосовой помощник?

Как уже говорилось, голосовые помощники на рынке, например Google Home или Amazon Echo Dot, реагируют на человека, только когда их «будят» определённые ключевые слова: «Hey Google» в первом случае и «Alexa» во втором.

Ключевые слова «Hey Google» и «Alexa» для пробуждения голосовых помощников

Иначе говоря, распознавание голосовых команд основано на многоступенчатой модели, или каскадном обнаружении (Cascade Detection).

Блок-схема каскадного обнаружения голосовых команд

Этап 1: маленький микропроцессор внутри Echo Dot или Google Home постоянно слушает, ожидая, когда будет распознано ключевое слово, с помощью модели TinyML на периферии (приложение KWS).

Этап 2: данные отправляются в облако и обрабатываются более крупной моделью, только если приложение KWS на этапе 1 сработало.

Видео ниже показывает пример программирования Google Assistant на Raspberry Pi (этап 2) с устройством TinyML Arduino Nano 33 BLE (этап 1).

Чтобы изучить описанный проект Google Assistant, см. руководство: Building an Intelligent Voice Assistant From Scratch.

В этом проекте KWS мы сосредоточимся на этапе 1 (KWS, или Keyword Spotting): здесь мы используем Nicla Vision, у которой есть цифровой микрофон для распознавания ключевого слова.

Практический проект KWS

Схема ниже показывает, как должно работать итоговое приложение KWS (во время инференса):

Схема работы приложения KWS во время инференса

Наше приложение KWS будет распознавать четыре класса звуков:

  • YES (ключевое слово 1);
  • NO (ключевое слово 2);
  • NOISE (слов нет, есть только фоновый шум);
  • UNKNOWN (смесь слов, отличных от YES и NO).

Для реальных проектов всегда полезно включать в набор, помимо ключевых слов, и другие звуки, например «Noise» (фон) и «Unknown» (неизвестные слова).

Рабочий процесс машинного обучения

Главный компонент приложения KWS — его модель. Поэтому нужно обучить модель на наших ключевых словах, шуме и других словах («unknown»):

Схема обучения модели KWS

Набор данных

Важнейший компонент любого рабочего процесса машинного обучения — набор данных. Выбрав конкретные ключевые слова (в нашем случае YES и NO), мы можем воспользоваться набором данных, который разработал Пит Уорден (Pete Warden): «Speech Commands: A Dataset for Limited-Vocabulary Speech Recognition». В этом наборе 35 ключевых слов (по более чем 1000 образцов каждого), например yes, no, stop и go. Для таких слов, как yes и no, можно получить по 1500 образцов.

Небольшую часть набора данных можно скачать из Edge Impulse Studio (готовый набор данных для распознавания ключевых слов). Он включает образцы четырёх классов, которые мы будем использовать в этом проекте: yes, no, noise и background. Для этого выполните следующие шаги:

Загрузка набора данных в Edge Impulse Studio

Создайте новый проект в Edge Impulse Studio (EIS) и выберите инструмент Upload Existing Data в разделе Data Acquisition. Выберите файлы для загрузки:

Выбор файлов для загрузки в Edge Impulse Studio

Задайте метку, выберите Automatically split between train and test и нажмите Upload data, чтобы загрузить данные в EIS. Повторите для всех классов.

Загрузка данных с меткой и автоматическим разделением

Теперь набор данных появится в разделе Data acquisition. Обратите внимание: примерно 6000 образцов (по 1500 на каждый класс) разделены на обучающую (4800) и тестовую (1200) выборки.

Набор данных в разделе Data acquisition

Сбор дополнительных аудиоданных

Хотя у нас много данных из набора Пита, рекомендуется записать и слова, произнесённые нами. При работе с акселерометрами важно, чтобы набор данных был снят датчиком того же типа. Для звука это необязательно, потому что мы классифицируем, по сути, аудиоданные.

Ключевое различие между звуком и аудио — в типе энергии. Звук — механическое возмущение (продольные звуковые волны), которое распространяется в среде и вызывает колебания давления. Аудио — электрический (аналоговый или цифровой) сигнал, представляющий звук.

Когда мы произносим ключевое слово, звуковые волны нужно преобразовать в аудиоданные. Преобразование выполняется дискретизацией сигнала с микрофона с частотой 16 кГц и разрядностью 16 бит на отсчёт.

Поэтому подойдёт любое устройство, которое может создавать аудиоданные с такими базовыми параметрами (16 кГц / 16 бит). В качестве устройства можно использовать NiclaV, компьютер или даже телефон.

Запись аудио с устройства с частотой 16 кГц

Использование NiclaV и Edge Impulse Studio

Как мы узнали в главе «Настройка Nicla Vision», EIS официально поддерживает Nicla Vision, что упрощает сбор данных с её датчиков, в том числе с микрофона. Поэтому создайте новый проект в EIS и подключите к нему Nicla, выполнив следующие шаги:

  • Скачайте последнюю версию прошивки EIS и распакуйте её.

  • Откройте ZIP-архив на компьютере и выберите загрузчик, соответствующий вашей ОС:

Выбор загрузчика прошивки EIS для своей ОС

  • Переведите NiclaV в режим загрузки, дважды нажав кнопку reset.

  • Загрузите двоичный файл arduino-nicla-vision.bin на плату, запустив пакетный файл для своей ОС.

Откройте свой проект в EIS и на вкладке Data Acquisition выберите WebUSB. Откроется окно; выберите вариант, в котором видно, что Nicla is paired, и нажмите [Connect].

В разделе Collect Data на вкладке Data Acquisition можно выбрать, данные какого датчика собирать. Выберите Built-in microphone, задайте label (например, yes), частоту дискретизации Frequency [16000 Гц] и длительность образца Sample length (in milliseconds), например [10 с]. Нажмите Start sampling.

Настройки сбора данных со встроенного микрофона в EIS

Данные набора Пита имеют длительность 1 с, а записанные образцы — по 10 с, и их нужно разбить на отрезки по 1 с. Нажмите три точки после названия образца и выберите Split sample.

Откроется окно инструмента разбиения (Split).

Инструмент Split для разбиения образца на отрезки

В инструменте разбейте данные на записи по 1 секунде (1000 мс). При необходимости добавьте или удалите сегменты. Эту процедуру нужно повторить для всех новых образцов.

Использование смартфона и EI Studio

Аудиоданные можно также записывать на ПК или смартфон с частотой дискретизации 16 кГц и разрядностью 16 бит.

Откройте Devices, отсканируйте QR Code телефоном и перейдите по ссылке. В браузере откроется приложение для сбора данных. Выберите Collecting Audio, затем задайте Label, длительность записи Length и Category.

Сбор аудио с телефона через приложение EI

Повторите ту же процедуру, что и с NiclaV.

Для записи звука можно использовать любое приложение, например Audacity, при условии что образцы записаны с частотой 16 кГц и разрядностью 16 бит.

Создание Impulse (предобработка и определение модели)

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

Проектирование Impulse

Конвейер Impulse для KWS

Сначала мы возьмём точки данных с окном в 1 секунду, применим аугментацию и будем сдвигать окно с интервалом 500 мс. Обратите внимание, что включён параметр zero-pad data (дополнение нулями). Он важен: образцы короче 1 секунды дополняются «нулями» (в некоторых случаях образцы могут быть короче окна 1000 мс в инструменте разбиения — чтобы избежать шумов и выбросов).

Каждый образец аудио длиной 1 секунда предобрабатывается и преобразуется в изображение (например, ). Как обсуждалось в практическом руководстве «Инженерия признаков для KWS», мы будем использовать Audio (MFCC), который извлекает признаки из аудиосигналов с помощью мел-частотных кепстральных коэффициентов, хорошо подходящих для человеческого голоса, то есть для нашей задачи.

Затем выберем блок Classification, чтобы построить модель с нуля на основе свёрточной нейронной сети (CNN).

Вместо этого можно использовать блок Transfer Learning (Keyword Spotting), который дообучает предобученную модель распознавания ключевых слов на ваших данных. Такой подход хорошо работает с относительно небольшими наборами ключевых слов.

Предобработка (MFCC)

Следующий шаг — создать признаки для обучения на следующем этапе.

Можно оставить значения параметров по умолчанию, но мы воспользуемся опцией DSP Autotune parameters.

Опция Autotune parameters для блока MFCC

Мы возьмём Raw features (наши аудиоданные длительностью 1 секунда с частотой 16 кГц) и с помощью блока обработки MFCC вычислим Processed features. На каждые 16 000 сырых признаков (16 000 1 секунда) мы получим 637 обработанных признаков .

Параметры блока MFCC и результаты предобработки

Результат показывает, что предобработка данных потребовала мало памяти (16 КБ) и имеет задержку 34 мс, что отлично. Например, на Arduino Nano (Cortex-M4f @ 64 МГц) та же предобработка займёт около 480 мс. Выбранные параметры, например FFT length [512], существенно влияют на задержку.

Теперь нажмём Save parameters и перейдём на вкладку Generated features, где будут созданы реальные признаки. Используя UMAP, метод снижения размерности, Feature explorer показывает, как признаки распределены на двумерном графике.

Feature explorer: распределение признаков на двумерном графике UMAP

Результат выглядит хорошо: признаки yes (красные) и no (синие) визуально чётко разделены. Признаки unknown расположены ближе к области no, чем к yes. Это говорит о том, что ключевое слово no склонно давать больше ложных срабатываний.

Заглядываем под капот

Чтобы лучше понять, как обрабатывается сырой звук, изучите главу «Инженерия признаков для KWS». Поэкспериментировать с генерацией признаков MFCC можно, скачав блокнот с GitHub или открыв его в Colab.

Проектирование модели и обучение

Мы используем простую свёрточную нейронную сеть (CNN), протестированную с одномерными (1D) и двумерными (2D) свёртками. Базовая архитектура содержит два блока «свёртка + MaxPooling» (с [8] и [16] фильтрами соответственно) и Dropout [0,25] для 1D и [0,5] для 2D. Последний слой после Flatten содержит [4] нейрона — по одному на каждый класс:

Архитектуры моделей 1D и 2D CNN для KWS

Гиперпараметры: Learning Rate — [0,005], модель обучается за [100] эпох. Также включим метод аугментации данных на основе SpecAugment. Модели 1D и 2D обучались с одинаковыми гиперпараметрами. Архитектура 1D дала лучший общий результат (точность 90,5 % против 88 % у 2D), поэтому мы используем 1D.

Результаты обучения модели 1D CNN

Свёртки 1D эффективнее, потому что требуют меньше параметров, чем 2D, и поэтому лучше подходят для сред с ограниченными ресурсами.

Также стоит посмотреть на матрицу ошибок для 1D. F1-мера для yes составляет 95 %, для no — 91 %. Этого и следовало ожидать по результатам Feature Explorer (no и unknown находятся на близком расстоянии). Чтобы улучшить результат, можно внимательно изучить образцы с ошибками.

Матрица ошибок и образцы с ошибками для 1D модели

Послушайте образцы, которые распознаны неверно. Например, для yes большинство ошибок связано с тем, что yes произносилось как «yeh». Вы можете записать дополнительные образцы и переобучить модель.

Заглядываем под капот

Если вы хотите понять, что происходит «под капотом», можно скачать предобработанный набор данных (MFCC training data) на вкладке Dashboard и запустить этот блокнот Jupyter — можно экспериментировать с кодом или открыть его в Colab. Например, можно проанализировать точность по эпохам:

Графики точности и потерь по эпохам обучения

Тестирование

Тестирование модели на данных, отложенных для проверки (тестовых данных), дало точность примерно 76 %.

Результаты тестирования модели на тестовой выборке

Если посмотреть F1-меру, для YES получилось 0,90 — отличный результат, ведь мы ожидаем использовать это слово как основной «триггер» в проекте KWS. Худший результат (0,70) — у UNKNOWN, и это нормально.

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

Классификация в реальном времени

Можно перейти к следующему шагу проекта, но учтите, что возможна классификация Live Classification с помощью NiclaV или смартфона: она позволяет захватывать образцы в реальном времени и тестировать обученную модель до её развёртывания на устройстве.

Развёртывание и инференс

EIS упакует все нужные библиотеки, функции предобработки и обученные модели и загрузит их на ваш компьютер. Перейдите в раздел Deployment, выберите Arduino Library, внизу выберите Quantized (Int8) и нажмите Build.

Выбор Arduino Library и Quantized (Int8) в разделе Deployment

Когда вы нажмёте Build, будет создан ZIP-файл и скачан на компьютер. В Arduino IDE откройте вкладку Sketch, выберите Add .ZIP Library и укажите ZIP-файл, скачанный из EIS:

Добавление ZIP-библиотеки в Arduino IDE

Теперь пора провести настоящий тест. Мы будем делать инференс, полностью отключившись от EIS. Используем пример кода NiclaV, созданный при развёртывании библиотеки Arduino.

В Arduino IDE откройте вкладку File/Examples, найдите свой проект и выберите nicla-vision/nicla-vision_microphone (или nicla-vision_microphone_continuous):

Выбор примера nicla-vision_microphone в Arduino IDE

Дважды нажмите кнопку reset, чтобы перевести NiclaV в режим загрузки, загрузите скетч на плату и протестируйте реальный инференс:

Результаты инференса для yes и no

Постобработка

Теперь, когда мы знаем, что модель работает и распознаёт наши ключевые слова, изменим код, чтобы увидеть результат на NiclaV полностью автономно — без подключения к ПК, от батареи, пауэрбанка или отдельного источника 5 В.

Идея такая: когда распознано ключевое слово YES, загорается зелёный светодиод; при NO — красный; если звучит UNKNOWN — синий; при наличии шума (без ключевого слова) все светодиоды выключены.

Изменим один из примеров кода. Сделаем это на примере nicla-vision_microphone_continuous.

Начнём с инициализации светодиодов:

...
void setup()
{
        // Когда отладка кода завершена, можно закомментировать или удалить
        // часть кода, связанную с Serial
    Serial.begin(115200);
    while (!Serial);
    Serial.println("Inferencing - Nicla Vision KWS with LEDs");

    // Выводы встроенных RGB-светодиодов Arduino NiclaV
    pinMode(LEDR, OUTPUT);
    pinMode(LEDG, OUTPUT);
    pinMode(LEDB, OUTPUT);

    // По умолчанию светодиоды выключены.
    // Примечание: RGB-светодиоды Arduino Nicla Vision
    // горят, когда вывод в состоянии LOW, и гаснут, когда в HIGH.
    digitalWrite(LEDR, HIGH);
    digitalWrite(LEDG, HIGH);
    digitalWrite(LEDB, HIGH);
...
}

Создайте две функции. Первая, turn_off_leds(), выключает все RGB-светодиоды:

/*
 * @brief      Функция turn_off_leds — выключает все RGB-светодиоды
 */
void turn_off_leds(){
    digitalWrite(LEDR, HIGH);
    digitalWrite(LEDG, HIGH);
    digitalWrite(LEDB, HIGH);
}

Вторая функция, turn_on_led(), включает RGB-светодиоды в соответствии с наиболее вероятным результатом классификатора:

/*
 * @brief     Функция turn_on_leds — включает RGB-светодиоды
 * @param[in] pred_index
 *            no:       [0] ==> горит красный
 *            noise:    [1] ==> все выключены
 *            unknown:  [2] ==> горит синий
 *            yes:      [3] ==> горит зелёный
 */
void turn_on_leds(int pred_index) {
  switch (pred_index)
  {
    case 0:
      turn_off_leds();
      digitalWrite(LEDR, LOW);
      break;

    case 1:
      turn_off_leds();
      break;

    case 2:
      turn_off_leds();
      digitalWrite(LEDB, LOW);
      break;

    case 3:
      turn_off_leds();
      digitalWrite(LEDG, LOW);
      break;
  }
}

И измените фрагмент // print the predictions в функции loop():

...

 if (++print_results >= (EI_CLASSIFIER_SLICES_PER_MODEL_WINDOW)) {
     // вывод предсказаний
     ei_printf("Predictions ");
     ei_printf("(DSP: %d ms., Classification: %d ms., "
               "Anomaly: %d ms.)",
         result.timing.dsp, result.timing.classification,
         result.timing.anomaly);
     ei_printf(": \n");
     int pred_index = 0;     // Инициализация pred_index
     float pred_value = 0;   // Инициализация pred_value
     for (size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++) {
         if (result.classification[ix].value > pred_value){
             pred_index = ix;
             pred_value = result.classification[ix].value;
         }
         // ei_printf("    %s: ",
         // result.classification[ix].label);
         // ei_printf_float(result.classification[ix].value);
         // ei_printf("\n");
     }
     ei_printf("  PREDICTION: ==> %s with probability %.2f\n",
               result.classification[pred_index].label,
               pred_value);
     turn_on_leds (pred_index);

#if EI_CLASSIFIER_HAS_ANOMALY == 1
        ei_printf("    anomaly score: ");
        ei_printf_float(result.anomaly);
        ei_printf("\n");
#endif

        print_results = 0;
    }
}

...

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

Загрузите скетч на плату и протестируйте реальный инференс. Идея такая: зелёный светодиод горит при распознавании ключевого слова YES, красный — при NO, а любое другое слово включает синий. Все светодиоды должны быть выключены при тишине или фоновом шуме. Учтите, что тот же приём может «запускать» внешнее устройство для выполнения нужного действия вместо включения светодиода, как мы видели во введении.

Итоги

Блокноты и код, использованные в этой практической работе, вы найдёте в репозитории GitHub.

Прежде чем завершить, учтите: классификация звуков — это больше, чем голос. Например, проекты TinyML со звуком можно развивать в самых разных областях:

  • Безопасность (обнаружение битого стекла, выстрелов);
  • Промышленность (обнаружение аномалий);
  • Медицина (храп, кашель, заболевания лёгких);
  • Природа (контроль ульев, звуки насекомых, борьба с браконьерством).

Ресурсы

Классификация движения и обнаружение аномалий

Мультфильм в стиле 1950-х: лаборатория исследования движения с имитацией контейнера
Промпт DALL·E 3: мультфильм в стиле 1950-х, изображающий лабораторию исследования движения. В центре комнаты стоит имитация контейнера для перевозки грузов на грузовиках, лодках и погрузчиках. Контейнер детализирован заклёпками и маркировкой, характерной для промышленных грузовых ящиков. Вокруг него — старинное оборудование: осциллограф, различные массивы датчиков и большие бумажные рулоны с записанными данными. На стенах висят учебные плакаты о безопасности перевозок и логистике. Общая атмосфера комнаты ностальгическая и научная, с оттенком промышленного стиля.

Обзор

Транспорт — основа мировой торговли. Миллионы контейнеров ежедневно перевозятся кораблями, грузовиками и поездами в города по всему миру. Обеспечить безопасную и эффективную перевозку этих контейнеров — громадная задача, которая требует современных технологий, и TinyML — одна из них.

В этой практической работе мы решим реальные задачи, связанные с транспортом. Мы создадим систему классификации движения и обнаружения аномалий на базе платы Arduino Nicla Vision, Arduino IDE и Edge Impulse Studio. Этот проект поможет понять, как контейнеры испытывают различные силы и движения на разных этапах перевозки: наземная и морская перевозка, вертикальное перемещение погрузчиком и периоды простоя на складе.

Цели обучения

  • Настройка платы Arduino Nicla Vision
  • Сбор и предобработка данных
  • Построение модели классификации движения
  • Реализация обнаружения аномалий
  • Тестирование и анализ в реальных условиях

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

Установка и проверка IMU

В этом проекте мы будем использовать акселерометр. Как обсуждалось в практической работе «Настройка Nicla Vision», на плате Nicla Vision есть встроенный 6-осевой IMU: 3D-гироскоп и 3D-акселерометр LSM6DSOX. Проверьте, установлена ли библиотека IMU LSM6DSOX. Если нет, установите её.

Установка библиотеки IMU LSM6DSOX в Arduino IDE

Затем откройте Examples > Arduino_LSM6DSOX > SimpleAccelerometer и запустите тест акселерометра. Работоспособность можно проверить через Serial Monitor или Plotter в IDE. Значения выражены в g (земное ускорение) с диапазоном по умолчанию ±4 g:

Результаты теста акселерометра в Serial Monitor

Определение частоты дискретизации

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

  1. Характер движения. Разные виды транспорта (наземный, морской и т. д.) могут включать разные диапазоны частот движения. Более быстрые движения могут требовать более высокой частоты дискретизации.

  2. Аппаратные ограничения. Плата Arduino Nicla Vision и связанные с ней датчики могут ограничивать скорость дискретизации данных.

  3. Вычислительные ресурсы. Более высокая частота дискретизации создаёт больше данных, что может быть ресурсоёмко, особенно в среде TinyML.

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

  5. Хранение данных. Более частая дискретизация требует больше места для хранения — ещё одно важное соображение для встраиваемых систем с ограниченной памятью.

Во многих задачах распознавания физической активности человека обычно используют частоты дискретизации около 50–100 Гц. Поскольку мы моделируем сценарии перевозки, которые обычно не являются высокочастотными событиями, диапазон 50–100 Гц может быть разумной отправной точкой.

Определим скетч, который позволит собирать данные с заданной частотой дискретизации (например, 50 Гц):

/*
 * Основано на примере Edge Impulse Data Forwarder (Arduino)
  - https://docs.edgeimpulse.com/docs/cli-data-forwarder
 * Разработано M.Rovai @11May23
 */

/* Подключение библиотек ----------------------------- */
#include <Arduino_LSM6DSOX.h>

/* Константы ----------------------------------------- */
#define CONVERT_G_TO_MS2 9.80665f
#define FREQUENCY_HZ        50
#define INTERVAL_MS         (1000 / (FREQUENCY_HZ + 1))

static unsigned long last_interval_ms = 0;
float x, y, z;

void setup() {
  Serial.begin(9600);
  while (!Serial);

  if (!IMU.begin()) {
    Serial.println("Failed to initialize IMU!");
    while (1);
  }
}

void loop() {
  if (millis() > last_interval_ms + INTERVAL_MS) {
    last_interval_ms = millis();

    if (IMU.accelerationAvailable()) {
      // Считываем сырые измерения ускорения с устройства
      IMU.readAcceleration(x, y, z);

      // переводим в м/с2
      float ax_m_s2 = x * CONVERT_G_TO_MS2;
      float ay_m_s2 = y * CONVERT_G_TO_MS2;
      float az_m_s2 = z * CONVERT_G_TO_MS2;

      Serial.print(ax_m_s2);
      Serial.print("\t");
      Serial.print(ay_m_s2);
      Serial.print("\t");
      Serial.println(az_m_s2);
    }
  }
}

Загрузите скетч и посмотрите Serial Monitor: мы видим, что собираем 50 отсчётов в секунду.

Вывод 50 отсчётов в секунду в Serial Monitor

Обратите внимание: когда плата Nicla лежит на столе (камерой вниз), ось показывает около 9,8 м/с — ожидаемое ускорение свободного падения.

Кейс: имитация перевозки контейнера

Чтобы сделать руководство понятнее и ближе к практике, мы смоделируем перевозку контейнера (точнее, пакета) в разных сценариях. С помощью встроенного акселерометра платы Arduino Nicla Vision мы будем записывать данные о движении, вручную имитируя условия:

  1. Наземная перевозка (автомобилем или поездом);
  2. Морская перевозка;
  3. Вертикальное перемещение погрузчиком (fork-lift);
  4. Стационарный период простоя на складе.

Четыре класса: наземная, морская перевозка, погрузчик и простой на складе

Из приведённых изображений для нашей симуляции следует, что преимущественно горизонтальные движения (по осям или ) относятся к классу «Наземный» (Terrestrial), вертикальные движения (по оси ) — к классу «Погрузчик» (Lift), отсутствие движения — к классу «Простой» (Idle), а движение по всем трём осям — к классу «Морской» (Maritime).

Определение классов движения по осям

Сбор данных

Для сбора данных есть несколько вариантов. В реальной ситуации устройство можно подключить непосредственно к одному контейнеру, а данные записывать в файл (например, .CSV) на SD-карту (через SPI) или в офлайн-хранилище на компьютере. Данные также можно передавать удалённо на ближайшее устройство, например на телефон, по Bluetooth (как сделано в этом проекте: Sensor DataLogger). Когда набор данных собран и сохранён в файле .CSV, его можно загрузить в Studio с помощью CSV Wizard.

В этом видео вы можете узнать о других способах отправки данных в Edge Impulse Studio.

Подключение устройства к Edge Impulse

Мы подключим Nicla непосредственно к Edge Impulse Studio, которая также будет использоваться для предобработки данных, обучения модели, тестирования и развёртывания. Для этого есть два варианта:

  1. Скачать последнюю прошивку и подключить её напрямую в разделе Data Collection.
  2. Использовать CLI Data Forwarder, чтобы собирать данные датчика и отправлять их в Studio.

Первый вариант проще, как мы видели в практической работе «Настройка Nicla Vision», но второй даёт больше гибкости при сборе данных, например при задании частоты дискретизации. Выберем второй вариант.

Создайте новый проект в Edge Impulse Studio (EIS) и подключите к нему Nicla, выполнив следующие шаги:

  1. Установите Edge Impulse CLI и Node.js на компьютер.
  2. Загрузите скетч для сбора данных (тот, что обсуждался выше в этом руководстве).
  3. Используйте CLI Data Forwarder, чтобы собирать данные акселерометра Nicla и отправлять их в Studio, как показано на схеме:

Схема передачи данных акселерометра через CLI Data Forwarder

Запустите CLI Data Forwarder в терминале. Если делаете это впервые, введите команду:

$ edge-impulse-data-forwarder --clean

Затем введите учётные данные EI, выберите проект, переменные (например, accX, accY и accZ) и имя устройства (например, NiclaV):

Настройка CLI Data Forwarder в терминале

Откройте раздел Devices в своём проекте EI и проверьте, что устройство подключено (точка должна быть зелёной):

Раздел Devices: устройство подключено

Вы можете клонировать проект, созданный для этой практической работы: NICLA Vision Movement Classification.

Сбор данных в Studio

В разделе Data Acquisition вы должны увидеть, что ваша плата [NiclaV] подключена. Датчик доступен: [sensor with 3 axes (accX, accY, accZ)], с частотой дискретизации [50 Hz]. Studio предлагает длительность образца [10000] мс (10 с). Осталось задать метку образца. Начнём с [terrestrial]:

Настройки сбора данных в разделе Data Acquisition

Наземная перевозка (паллеты в грузовике или поезде): движение по горизонтали. Нажмите [Start Sample] и перемещайте устройство горизонтально, сохраняя одно направление над столом. Через 10 с данные загрузятся в Studio. Вот как выглядит записанный образец:

Образец наземной перевозки: движение по оси Y

Как и ожидалось, движение зафиксировано в основном по оси (зелёная). Синяя линия — ось , около −10 м/с (камера Nicla смотрит вверх).

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

Морская перевозка (паллеты на лодках в бушующем океане). Движение фиксируется по всем трём осям:

Образец морской перевозки: движение по трём осям

Погрузчик (паллеты, которые вертикально поднимает погрузчик). Движение фиксируется только по оси :

Образец погрузчика: движение только по оси Z

Простой (паллеты на складе). Акселерометр не фиксирует движения:

Образец простоя: движения не зафиксировано

Например, можно записать по 2 минуты (двенадцать образцов по 10 секунд) для каждого из четырёх классов — всего 8 минут данных. Через меню three dots после каждого образца выберите 2 из них и отложите для тестовой выборки. Или можно воспользоваться автоматическим инструментом Train/Test Split в разделе Danger Zone на вкладке Dashboard. Ниже показан итоговый набор данных:

Итоговый набор данных по четырём классам

Когда набор данных собран, его можно детальнее изучить с помощью Data Explorer — визуального инструмента для поиска выбросов и неверно размеченных данных (с его помощью их можно исправить). Data Explorer сначала извлекает осмысленные признаки из данных (применяя обработку сигналов и встраивания нейронной сети), а затем использует алгоритм снижения размерности, например PCA или t-SNE, чтобы отобразить эти признаки в двумерное пространство. Так вы получаете обзор всего набора данных одним взглядом.

Data Explorer: двумерное отображение признаков всех классов

В нашем случае набор данных выглядит корректно (хорошее разделение классов). Однако PCA показывает возможные проблемы между классами «морской» (зелёный) и «погрузчик» (оранжевый). Это ожидаемо: на лодке движение иногда может быть только «вертикальным».

Проектирование Impulse

Следующий шаг — определение нашего Impulse. Он получает сырые данные, с помощью обработки сигналов извлекает признаки и передаёт их как входной тензор в блок обучения для классификации новых данных. Перейдите в Impulse Design и выберите Create Impulse. Studio предложит базовую конструкцию. Добавим также второй блок обучения для Anomaly Detection.

Конструкция Impulse с блоками классификации и обнаружения аномалий

Эта вторая модель использует K-means. Если представить, что известные классы — это кластеры, то любой образец, который не вписывается ни в один из них, может быть выбросом, аномалией: например, контейнер, упавший с корабля в океан или сорвавшийся с погрузчика.

Модель K-means для обнаружения аномалий

Частота дискретизации должна определиться автоматически; если нет — введите её: [50] Гц. Studio предлагает размер окна 2 секунды ([2000] мс) со скользящим окном шагом [20] мс. На этом шаге мы предобрабатываем захваченные данные (временные ряды), создавая табличный набор признаков, который будет входом для нейронной сети-классификатора (DNN) и модели обнаружения аномалий (K-Means), как показано ниже:

Блоки предобработки и моделей в Impulse

Разберём эти шаги и параметры подробнее, чтобы понять, что мы делаем.

Обзор предобработки данных

Предобработка данных — это извлечение признаков из набора данных, снятого акселерометром: обработка и анализ сырых данных. Акселерометры измеряют ускорение объекта по одной или нескольким осям (обычно по трём, обозначаемым , и ). Эти измерения позволяют понять различные аспекты движения объекта: характер перемещений и вибрации.

Сырые данные акселерометра могут быть зашумлёнными и содержать ошибки или лишнюю информацию. Шаги предобработки, например фильтрация и нормализация, очищают и стандартизируют данные, делая их более подходящими для извлечения признаков. В нашем случае данные нужно разбить на небольшие сегменты, или окна. Это помогает сосредоточиться на конкретных событиях или активности в наборе данных, делая извлечение признаков более управляемым и содержательным. Выбор размера окна и перекрытия (шага окна) зависит от приложения и частоты интересующих событий. Как правило, стоит стремиться захватить пару «циклов данных».

При частоте дискретизации (SR) 50 Гц и размере окна 2 секунды мы получим 100 отсчётов на ось, или 300 всего (3 оси 2 секунды 50 отсчётов). Окно будем сдвигать каждые 200 мс, создавая более крупный набор данных, где каждый экземпляр содержит 300 сырых признаков.

Разбиение временного ряда на окна с перекрытием

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

  • Признаки временной области описывают статистические свойства данных внутри каждого сегмента: среднее, медиану, стандартное отклонение, асимметрию (skewness), эксцесс (kurtosis) и частоту переходов через ноль (zero-crossing rate).
  • Признаки частотной области получают, преобразуя данные в частотную область с помощью таких методов, как быстрое преобразование Фурье (FFT). К типичным признакам частотной области относятся спектр мощности, спектральная энергия, доминирующие частоты (амплитуда и частота) и спектральная энтропия.
  • Признаки временно-частотной области объединяют информацию о времени и частоте, например кратковременное преобразование Фурье (STFT) или дискретное вейвлет-преобразование (DWT). Они дают более детальное представление о том, как частотное содержание сигнала меняется во времени.

Во многих случаях количество извлечённых признаков может быть большим, что приводит к переобучению или увеличению вычислительной сложности. Методы отбора признаков, такие как взаимная информация, корреляционные методы или метод главных компонент (PCA), помогают выявить наиболее значимые признаки для конкретного приложения и уменьшить размерность набора данных. Studio может помочь с расчётом важности признаков.

Спектральные признаки в EI Studio

Предобработка данных — сложная область для встраиваемого машинного обучения, но Edge Impulse помогает справиться с ней благодаря шагу цифровой обработки сигналов (DSP) и, в частности, блоку спектральных признаков (Spectral Features Block).

В Studio собранный сырой набор данных становится входом блока спектрального анализа, который отлично подходит для анализа повторяющихся движений, например данных акселерометров. Этот блок выполняет DSP (цифровую обработку сигналов) и извлекает признаки, такие как FFT или вейвлеты.

Для нашего проекта, поскольку временной сигнал непрерывен, стоит использовать FFT с длиной, например, [32].

Статистические признаки временной области для каждой оси (канала):

  • RMS (среднеквадратичное значение): 1 признак;
  • Skewness (асимметрия): 1 признак;
  • Kurtosis (эксцесс): 1 признак.

Спектральные признаки частотной области для каждой оси (канала):

  • Spectral Power (спектральная мощность): 16 признаков (длина FFT / 2);
  • Skewness (асимметрия): 1 признак;
  • Kurtosis (эксцесс): 1 признак.

Таким образом, для длины FFT 32 точки блок спектрального анализа выдаёт 21 признак на ось (всего 63 признака).

Подробнее о том, как вычисляется каждый признак, можно узнать из блокнота Edge Impulse — Spectral Features Block Analysis, из статьи TinyML under the hood: Spectral Analysis или открыв блокнот прямо в Google Colab.

Генерация признаков

Когда мы поняли, что делает предобработка, пора довести дело до конца. Возьмём сырые данные (временные ряды) и преобразуем их в табличные данные. Для этого перейдите в раздел Spectral Features на вкладке Parameters, задайте основные параметры, как обсуждалось в предыдущем разделе ([FFT] с [32] точками), и выберите [Save Parameters]:

Задание параметров спектральных признаков: FFT с 32 точками

В верхнем меню выберите опцию Generate Features и кнопку Generate Features. Каждое окно данных длительностью 2 секунды будет преобразовано в одну точку данных из 63 признаков.

Feature Explorer покажет эти данные в двумерном виде с помощью UMAP. Uniform Manifold Approximation and Projection (UMAP) — метод снижения размерности, который используется для визуализации, как и t-SNE, но применим и для общего нелинейного снижения размерности.

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

Feature Explorer: визуализация признаков по классам

Обучение моделей

Наш классификатор будет полносвязной нейронной сетью (DNN): входной слой из 63 нейронов, два скрытых слоя с 20 и 10 нейронами и выходной слой из четырёх нейронов (по одному на каждый класс), как показано здесь:

Архитектура полносвязной нейронной сети DNN

Гиперпараметры: скорость обучения (Learning Rate) [0.005], размер батча (Batch size) [32], доля данных для валидации [20] % и [30] эпох. После обучения точность составляет 98,5 %. Затраты памяти и задержка при этом незначительны.

Результаты обучения классификатора

Для обнаружения аномалий мы выберем предложенные признаки — те самые наиболее важные признаки, отобранные при извлечении, плюс RMS по оси accZ. Количество кластеров составит [32], как предлагает Studio:

Результаты обучения модели обнаружения аномалий

Тестирование

Проверить поведение модели на неизвестных данных можно, используя 20 % данных, отложенных при сборе. Результат составил почти 95 %, что хорошо. Результаты всегда можно улучшить, например разобравшись, что пошло не так в одном из неверных ответов. Если это уникальная ситуация, её можно добавить в обучающий набор и повторить обучение.

По умолчанию минимальный порог для неопределённого результата — [0.6] для классификации и [0.3] для аномалий. Поскольку классов четыре (их сумма должна быть равна 1,0), можно также задать более низкий порог, при котором класс считается допустимым (например, 0,4). Пороги уверенности можно задать через пункт Set confidence thresholds в меню three dots рядом с кнопкой Classify all.

Тестирование модели и настройка порогов уверенности

Также можно выполнить классификацию в реальном времени с помощью вашего устройства (оно должно оставаться подключённым к Studio).

Учтите, что в этом случае вы будете записывать реальные данные с устройства и загружать их в Studio, где инференс выполняется с помощью обученной модели (но модель НЕ находится на вашем устройстве).

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

Настало время развернуть блок предобработки и обученную модель на Nicla. Studio упакует все нужные библиотеки, функции предобработки и обученные модели и загрузит их на компьютер. Выберите опцию Arduino Library, внизу выберите Quantized (Int8) или Unoptimized (float32) и нажмите [Build]. Будет создан ZIP-файл, который скачается на компьютер.

Развёртывание модели как Arduino Library в Studio

В Arduino IDE откройте вкладку Sketch, выберите Add.ZIP Library и укажите ZIP-файл, скачанный из Studio. В терминале IDE появится сообщение: Library installed (библиотека установлена).

Инференс

Теперь пора провести настоящий тест. Инференс будем выполнять, полностью отключившись от Studio. Изменим один из примеров кода, созданных при развёртывании библиотеки Arduino.

В Arduino IDE откройте вкладку File/Examples, найдите свой проект и в примерах выберите Nicla_vision_fusion:

Выбор примера Nicla_vision_fusion в Arduino IDE

Обратите внимание: код, созданный Edge Impulse, использует подход слияния данных датчиков (sensor fusion), где применяются IMU (акселерометр и гироскоп) и ToF-датчик. В начале кода находятся библиотеки, связанные с нашим проектом: IMU и ToF:

/* Подключение библиотек ---------------------------------- */
#include <NICLA_Vision_Movement_Classification_inferencing.h>
#include <Arduino_LSM6DSOX.h> // IMU
#include "VL53L1X.h" // ToF

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

Вот и всё!

Теперь можно загрузить код на устройство и проводить инференс. Нажмите кнопку [RESET] на Nicla дважды, чтобы перевести её в режим загрузки (если она всё ещё подключена к Studio, отключите её), и загрузите скетч на плату.

Теперь попробуйте разные движения с платой (похожие на те, что вы записывали при сборе данных), наблюдая результат инференса каждого класса в Serial Monitor:

  • Простой и погрузчик:

Результаты инференса для классов простоя и погрузчика

  • Морская и наземная перевозка:

Результаты инференса для классов морской и наземной перевозки

Обратите внимание, что во всех описанных ситуациях значение anomaly score было меньше 0,0. Попробуйте новое движение, которого не было в исходном наборе данных: например, «перекатывание» Nicla, когда камера направлена вниз — как контейнер, упавший с лодки, или даже авария на лодке:

  • Обнаружение аномалии:

Результат обнаружения аномалии при нестандартном движении

В этом случае аномалия гораздо больше — свыше 1,00.

Постобработка

Теперь, когда мы знаем, что модель работает и распознаёт движения, предлагаем изменить код, чтобы увидеть результат на NiclaV полностью автономно — отключённой от ПК и питающейся от батареи, пауэрбанка или отдельного источника питания 5 В.

Идея та же, что и в проекте KWS: при распознавании определённого движения зажигается определённый светодиод. Например, для наземной перевозки загорится зелёный светодиод, для морской — красный, для погрузчика — синий, а если движения нет (простой), все светодиоды выключены. Можно также добавить условие для обнаружения аномалии: например, использовать белый цвет (все светодиоды горят одновременно).

Итоги

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

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

Примеры применения

Промышленность и производство

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

Здравоохранение

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

Бытовая электроника

  • Управление жестами: распознавание определённых движений для управления устройствами, например включение света взмахом руки.
  • Игры: улучшение игрового опыта за счёт ввода, управляемого движениями.

Транспорт и логистика

  • Телематика транспортных средств: мониторинг движения автомобиля для выявления необычного поведения, например резкого торможения, резких поворотов или аварий.
  • Мониторинг груза: обеспечение сохранности товаров при перевозке путём обнаружения необычных движений, которые могут указывать на вмешательство или неправильное обращение.

Умные города и инфраструктура

  • Мониторинг состояния конструкций: обнаружение вибраций или перемещений внутри сооружений, которые могут указывать на потенциальные неисправности или необходимость обслуживания.
  • Управление дорожным движением: анализ потока пешеходов или транспорта для повышения мобильности и безопасности в городе.

Безопасность и наблюдение

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

Сельское хозяйство

  • Мониторинг техники: отслеживание производительности и использования сельскохозяйственной техники.
  • Анализ поведения животных: мониторинг движения скота для выявления признаков проблем со здоровьем или стресса.

Экологический мониторинг

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

Трёхмерный корпус для Nicla

Для реальных приложений, как было сказано выше, к устройству можно добавить корпус. Eoin Jordan из Edge Impulse разработал отличный носимый корпус для контроля здоровья человека и механизмов для семейства плат Nicla. Он работает с магнитом 10 мм, винтами M2 и ремешком 16 мм для сценариев использования, связанных со здоровьем человека и машин. Ссылка: Arduino Nicla Voice and Vision Wearable Case.

Носимый корпус для плат Nicla

Применения классификации движения и обнаружения аномалий очень разнообразны, а Arduino Nicla Vision хорошо подходит для сценариев, где важны низкое энергопотребление и обработка на периферии. Компактный форм-фактор и эффективность обработки делают её идеальным выбором для портативных и удалённых приложений, где важна обработка в реальном времени, а связь может быть ограничена.

Ресурсы

Обнаружение объектов

Мультфильм в стиле 1940–1950-х: конвейер на складе с колёсами и коробками, у которого стоит ретро-робот
Промпт DALL·E 3: мультфильм в стиле 1940–1950-х: просторный промышленный склад. На первом плане конвейер с игрушечными колёсами и коробками. У колёс яркие жёлтые центры и чёрные шины, коробки — белые кубики с чередующимся чёрно-белым узором. В конце движущегося конвейера стоит ретро-робот с инструментами и датчиками, который старательно классифицирует и пересчитывает прибывающие колёса и коробки. Общая эстетика напоминает анимацию середины века с чёткими линиями и классической палитрой.

Обзор

Это продолжение работы с классификацией изображений на Nicla Vision: теперь мы изучаем обнаружение объектов (Object Detection).

Обнаружение объектов на Nicla Vision

Обнаружение объектов и классификация изображений

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

Полосатый кот после ужина: модель распознаёт кота

Но что будет, если кот прыгнет рядом с бокалом вина? Модель по-прежнему распознаёт только преобладающую категорию на изображении — полосатого кота:

Кот рядом с бокалом вина: модель распознаёт только кота

А что, если на изображении нет доминирующей категории?

Изображение без доминирующей категории

Модель совершенно неверно определяет это изображение как «ashcan» (мусорный ящик), возможно из-за цветовых оттенков.

Модель, которая использовалась во всех предыдущих примерах, — MobileNet, обученная на большом наборе данных ImageNet.

Чтобы решить эту проблему, нужен другой тип моделей, которые находят не только несколько категорий (или меток), но и где на изображении расположены объекты.

Такие модели гораздо сложнее и крупнее, например MobileNetV2 SSD FPN-Lite 320x320, обученная на наборе COCO. Эта предобученная модель обнаружения объектов умеет находить до 10 объектов на изображении и выдаёт ограничивающий прямоугольник (bounding box) для каждого обнаруженного объекта. Изображение ниже — результат работы такой модели на Raspberry Pi:

Результат работы MobileNetV2 SSD на Raspberry Pi

Модели для обнаружения объектов (такие как MobileNet SSD или YOLO) обычно занимают несколько мегабайт. Для Raspberry Pi это нормально, но для встраиваемых устройств, где ОЗУ обычно меньше 1 МБ, такие модели не подходят.

Инновационное решение для обнаружения объектов: FOMO

Edge Impulse представила в 2022 году FOMO (Faster Objects, More Objects) — новое решение для обнаружения объектов на встраиваемых устройствах. Оно работает не только на Nicla Vision (Cortex M7), но и на процессорах Cortex M4F (Arduino Nano33 и серии OpenMV M4), а также на устройствах Espressif ESP32 (ESP-CAM и XIAO ESP32S3 Sense).

В этой практической работе мы будем использовать FOMO для обнаружения объектов и не станем вдаваться в подробности самой модели. Чтобы больше узнать о её работе, можно прочитать официальное объявление FOMO от Edge Impulse, где Louis Moreau и Mat Kelcey подробно объясняют принцип работы.

Цель проекта обнаружения объектов

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

Колёса и коробки на конвейере промышленного предприятия

Иначе говоря, нам нужна многоклассовая классификация, где каждое изображение может относиться к трём классам:

  • фон (объектов нет);
  • коробка (Box);
  • колесо (Wheel).

Вот несколько неразмеченных примеров изображений, по которым мы будем обнаруживать объекты (колёса и коробки):

Неразмеченные примеры изображений с колёсами и коробками

Нас интересует, какой объект есть на изображении, его положение (центроид) и сколько таких объектов на нём присутствует. Размер объекта FOMO не определяет — в отличие от MobileNet SSD или YOLO, где ограничивающий прямоугольник является одним из выходных данных модели.

Проект мы будем разрабатывать с Nicla Vision для захвата изображений и инференса, а для машинного обучения использовать Edge Impulse Studio. Но прежде чем начинать проект обнаружения объектов в Studio, создадим сырой набор данных (без разметки) с изображениями, на которых есть объекты для обнаружения.

Сбор данных

Для съёмки изображений можно использовать:

  • инструмент Web Serial Camera;
  • Edge Impulse Studio;
  • OpenMV IDE;
  • смартфон.

Здесь мы будем использовать OpenMV IDE.

Сбор набора данных в OpenMV IDE

Сначала создаём на компьютере папку, в которую будут сохраняться данные, например «data». Затем в OpenMV IDE открываем Tools > Dataset Editor и выбираем New Dataset, чтобы начать сбор набора данных:

Создание папки data и нового набора данных в OpenMV IDE

Edge Impulse рекомендует, чтобы объекты были похожего размера и не перекрывали друг друга, — так модель работает лучше. На промышленном предприятии это нормально: камера закреплена неподвижно и находится на одном расстоянии от объектов. Тем не менее мы попробуем и смешанные размеры и положения, чтобы посмотреть на результат.

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

Подключите Nicla Vision к OpenMV IDE и запустите dataset_capture_script.py. Нажатие кнопки Capture Image начнёт съёмку изображений:

Кнопка Capture Image в OpenMV IDE

Рекомендуем около 50 изображений, с разным сочетанием объектов и разным их количеством в кадре. Старайтесь снимать с разных углов, на разном фоне и при разном освещении.

Сохраняемые изображения имеют размер кадра QVGA и формат RGB565 (формат цветных пикселей).

Когда набор данных собран, закройте инструмент Dataset Editor через Tools > Dataset Editor.

Edge Impulse Studio

Настройка проекта

Откройте Edge Impulse Studio, войдите в систему по кнопке Login (или создайте аккаунт) и начните новый проект.

Здесь можно клонировать проект, созданный для этой практической работы: NICLA_Vision_Object_Detection.

На панели проекта Dashboard откройте Project info и выберите Bounding boxes (object detection), а в правом верхнем углу страницы выберите Target — Arduino Nicla Vision (Cortex-M7).

Настройка проекта: тип Bounding boxes и цель Arduino Nicla Vision

Загрузка неразмеченных данных

В Studio откройте вкладку Data acquisition и в разделе UPLOAD DATA загрузите с компьютера снятые файлы.

Загрузка данных в раздел UPLOAD DATA

Можно поручить Studio автоматически разделить данные на обучающую и тестовую выборки или сделать это вручную.

Разделение данных на обучающую и тестовую выборки

Все неразмеченные изображения (51) загружены, но перед использованием в наборе данных их нужно правильно разметить. Для этого в Studio есть инструмент, который открывается по ссылке Labeling queue (51).

В бесплатной версии Edge Impulse Studio доступны два способа разметки с помощью ИИ:

  • с использованием YOLOv5;
  • отслеживанием объектов между кадрами.

Для клиентов Enterprise Edge Impulse запустила функцию автоматической разметки, которая упрощает разметку в проектах обнаружения объектов.

Обычные объекты можно быстро распознать и разметить с помощью готовой библиотеки предобученных моделей обнаружения YOLOv5 (обученных на наборе COCO). Но в нашем случае объектов нет в наборах COCO, поэтому выберем опцию tracking objects. С ней, как только вы нарисуете рамки и разметите изображения в одном кадре, объекты будут автоматически отслеживаться от кадра к кадру, и новые объекты будут частично размечены (не все — корректно).

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

Разметка набора данных

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

Разметка объектов рамками в Studio

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

Размеченные примеры изображений с колёсами и коробками

Затем просмотрите размеченные примеры на вкладке Data acquisition. Если какая-то метка неверна, её можно отредактировать через меню три точки после названия образца:

Меню «три точки» для редактирования метки образца

Studio подскажет, как заменить неверную метку и исправить набор данных.

Исправление неверной метки в наборе данных

Проектирование Impulse

На этом этапе нужно определить, как:

  • Предобработка: изменить размер отдельных изображений с до , сжав их к квадратной форме (без обрезки). Затем изображения переводятся из RGB в оттенки серого (Grayscale).

  • Проектирование модели: в данном случае «Object Detection» (обнаружение объектов).

Конвейер Impulse: предобработка и обнаружение объектов

Предобработка всего набора данных

В этом разделе выберите Color depth — Grayscale, подходящий для моделей FOMO, и сохраните parameters.

Выбор Grayscale в параметрах предобработки

Studio автоматически переходит к следующему разделу Generate features, где обрабатываются все образцы. В результате получается набор данных из отдельных изображений , или 9 216 признаков.

Генерация признаков в Studio

Просмотр признаков показывает, что после генерации все образцы хорошо разделяются.

Один из образцов (№ 46), судя по всему, находится не в том месте, но щелчок по нему подтверждает, что разметка верна.

Проектирование модели, обучение и тестирование

Мы будем использовать FOMO — модель обнаружения объектов на основе MobileNetV2 (альфа 0,35), которая грубо сегментирует изображение на сетку: фон и интересующие объекты (здесь коробки и колёса).

FOMO — инновационная модель машинного обучения для обнаружения объектов, которая может использовать в 30 раз меньше энергии и памяти, чем традиционные модели вроде Mobilenet SSD и YOLOv5. FOMO способна работать на микроконтроллерах с ОЗУ менее 200 КБ. Главная причина в том, что другие модели вычисляют размер объекта, рисуя вокруг него квадрат (ограничивающий прямоугольник), а FOMO не учитывает размер и выдаёт только информацию о том, где объект находится на изображении, — в виде координат его центроида.

Как работает FOMO

FOMO берёт изображение в оттенках серого и делит его на блоки пикселей с коэффициентом 8. Для входного изображения сетка будет . Затем FOMO запускает классификатор для каждого блока пикселей, чтобы вычислить вероятность того, что в нём есть коробка или колесо, и определяет области с наибольшей вероятностью наличия объекта (если в блоке пикселей объектов нет, он классифицируется как background). По перекрытию итоговой области FOMO выдаёт координаты (относительно размеров изображения) центроида этой области.

Схема работы FOMO: сетка блоков и центроиды объектов

Для обучения нужно выбрать предобученную модель. Используем FOMO (Faster Objects, More Objects) MobileNetV2 0.35. Эта модель занимает около 250 КБ ОЗУ и 80 КБ ПЗУ (флеш-памяти), что хорошо подходит для нашей платы, у которой 1 МБ ОЗУ и ПЗУ.

Выбор модели FOMO MobileNetV2 0.35 в Studio

Гиперпараметры обучения такие:

  • эпох: 60;
  • размер батча (batch size): 32;
  • скорость обучения (learning rate): 0,001.

Для проверки во время обучения будут отложены 20 % набора данных (validation_dataset). Для оставшихся 80 % (train_dataset) применим аугментацию данных: случайно отразим, изменим размер и яркость изображений и обрежем их, искусственно увеличивая количество образцов для обучения.

В итоге модель получает F1-меру около 91 % (валидация) и 93 % (тестовые данные).

Обратите внимание: FOMO автоматически добавил третью метку background к двум заданным ранее (box и wheel).

Результаты обучения модели FOMO в Studio

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

Проверка модели в режиме «Live Classification»

Поскольку Edge Impulse официально поддерживает Nicla Vision, подключим её к Studio. Для этого выполните шаги:

  • Скачайте последнюю прошивку EI и распакуйте её.

  • Откройте ZIP-архив на компьютере и выберите загрузчик для своей ОС.

  • Переведите Nicla Vision в режим загрузки, дважды нажав кнопку reset.

  • Запустите пакетный файл для своей ОС, чтобы загрузить двоичный файл (arduino-nicla-vision.bin) на плату.

Откройте раздел Live classification в EI Studio и с помощью webUSB подключите Nicla Vision:

Раздел Live classification в EI Studio

Когда подключение установлено, Nicla может снимать реальные изображения, которые обученная модель в Edge Impulse Studio будет тестировать.

Тестирование модели на изображениях с Nicla в реальном времени

Учтите, что модель может давать ложные срабатывания и пропуски. Это можно уменьшить, задав подходящий Confidence Threshold (порог уверенности; настройка через меню три точки). Попробуйте значение 0,8 и выше.

Развёртывание модели

На вкладке Deploy выберите OpenMV Firmware и нажмите [Build].

Сборка OpenMV Firmware на вкладке Deploy

При повторном подключении Nicla к OpenMV IDE она попытается обновить свою прошивку. Вместо этого выберите опцию Load a specific firmware. Либо откройте меню Tools > Run Bootloader (Load Firmware).

Выбор загрузки конкретной прошивки в OpenMV IDE

На компьютере появится ZIP-файл из Studio. Откройте его:

ZIP-архив с прошивкой из Studio

Загрузите файл .bin на плату:

Загрузка файла .bin на плату

Когда загрузка завершится, появится всплывающее сообщение. Нажмите Press OK и откройте скрипт ei_object_detection.py, скачанный из Studio.

Примечание: если появится всплывающее окно с сообщением, что прошивка устарела, нажмите [NO], чтобы не обновлять её.

Перед запуском скрипта изменим несколько строк. Обратите внимание, что определение окна можно оставить как , а камера может снимать изображения в формате QVGA/RGB. Снимок будет предобработан прошивкой, развёрнутой из Edge Impulse.

import sensor
import time
import ml
from ml.utils import NMS
import math
import image

sensor.reset()  # Сброс и инициализация датчика.
# Формат пикселей (RGB565 или GRAYSCALE)
sensor.set_pixformat(sensor.RGB565)
# Размер кадра QVGA (320x240)
sensor.set_framesize(sensor.QVGA)
sensor.skip_frames(time=2000)  # Даём камере настроиться.

Задайте минимальную уверенность, например 0,8, чтобы уменьшить количество ложных срабатываний и пропусков.

min_confidence = 0.8

При необходимости измените цвет кругов, которыми отмечаются центроиды обнаруженных объектов, — для лучшего контраста.

threshold_list = [(math.ceil(min_confidence * 255), 255)]

# Загрузка встроенной модели
model = ml.Model("trained")
print(model)

# Вместо этого модели можно загрузить из
# файловой системы.
# model = ml.Model(
#     '<object_detection_modelwork>.tflite',
#     load_to_fb=True)
# labels = [line.rstrip('\n') for line in open("labels.txt")]

colors = [ # Добавьте цвета, если одновременно обнаруживаете более
           # 7 классов.
    (255, 255,   0), # background: жёлтый (не используется)
    (  0, 255,   0), # cube (коробка): зелёный
    (255,   0,   0), # wheel (колесо): красный
    (  0,   0, 255), # не используется
    (255,   0, 255), # не используется
    (  0, 255, 255), # не используется
    (255, 255, 255), # не используется
]

Остальной код оставьте без изменений:

# FOMO выдаёт изображение для каждого класса, где каждый пиксель —
# центроид обученного объекта. Поэтому мы получаем эти выходные
# изображения и запускаем на них find_blobs(), чтобы извлечь
# центроиды. Также запускаем get_stats() для найденных блобов,
# чтобы определить их оценку. Объект Non-Max-Suppression (NMS)
# затем отфильтровывает перекрывающиеся обнаружения и переносит
# их положение из выходного изображения обратно на исходное
# входное изображение. Функция возвращает список для каждого
# класса, в котором содержится список кортежей (rect, score),
# описывающих обнаруженные объекты.


def fomo_post_process(model, inputs, outputs):
    n, oh, ow, oc = model.output_shape[0]
    nms = NMS(ow, oh, inputs[0].roi)
    for i in range(oc):
        img = image.Image(outputs[0][0, :, :, i] * 255)
        blobs = img.find_blobs(
            threshold_list,
            x_stride=1,
            area_threshold=1,
            pixels_threshold=1,
        )
        for b in blobs:
            rect = b.rect()
            x, y, w, h = rect
            score = (
                img.get_statistics(
                    thresholds=threshold_list, roi=rect
                ).l_mean()
                / 255.0
            )
            nms.add_bounding_box(x, y, x + w, y + h, score, i)
    return nms.get_bounding_boxes()


clock = time.clock()
while True:
    clock.tick()

    img = sensor.snapshot()

    for i, detection_list in enumerate(
        model.predict([img], callback=fomo_post_process)
    ):
        if i == 0:
            continue  # класс background
        if len(detection_list) == 0:
            continue  # для этого класса нет обнаружений?

        print("********** %s **********" % model.labels[i])
        for (x, y, w, h), score in detection_list:
            center_x = math.floor(x + (w / 2))
            center_y = math.floor(y + (h / 2))
            print(f"x {center_x}\ty {center_y}\tscore {score}")
            img.draw_circle((center_x, center_y, 12), color=colors[i])

    print(clock.fps(), "fps", end="\n")

Затем нажмите зелёную кнопку Play, чтобы запустить код:

Запуск кода в OpenMV IDE зелёной кнопкой Play

В поле зрения камеры видны объекты, центроиды которых отмечены кругами фиксированного размера 12 пикселей (каждый круг имеет свой цвет в зависимости от класса). В Serial Terminal модель выводит обнаруженные метки и их положение в окне изображения .

Учтите, что начало координат находится в левом верхнем углу.

Вывод обнаруженных объектов с центроидами в окне 240x240

Частота кадров составляет около 8 кадров в секунду (примерно как в проекте классификации изображений). Это объясняется тем, что FOMO искусно построена на основе CNN-модели, а не модели обнаружения вроде SSD MobileNet или YOLO. Например, при запуске модели MobileNetV2 SSD FPN-Lite на Raspberry Pi 4 задержка примерно в 5 раз выше (около 1,5 кадра в секунду).

Короткое видео с результатами инференса:

Итоги

FOMO — важный скачок в области обработки изображений, как сказали Louis Moreau и Mat Kelcey при её запуске в 2022 году:

FOMO — революционный алгоритм, который впервые приносит обнаружение, отслеживание и подсчёт объектов в реальном времени на микроконтроллеры.

Существует множество возможностей для обнаружения объектов (а точнее, их подсчёта) на встраиваемых устройствах. Это может быть очень полезно, например, в проектах по подсчёту пчёл.

Пример подсчёта объектов на встраиваемом устройстве

Ресурсы

Настройка

Иллюстрация с платой Arduino NICLA VISION в центре стола с компьютером
Промпт DALL·E 3: иллюстрация в духе мультфильма 1950-х годов, где плата Arduino NICLA VISION с камерой и другими датчиками стоит в центре старомодного стола. На экране компьютера со скруглёнными углами открыта Arduino IDE. Код связан с настройкой светодиодов и распознаванием голосовых команд. В Serial Monitor явно выводятся слова «yes» и «no».

Обзор

Arduino Nicla Vision (иногда её называют NiclaV) — отладочная плата с двумя процессорами, которые могут выполнять задачи параллельно. Она входит в семейство плат с тем же форм-фактором, но рассчитанных на конкретные задачи, например Nicla Sense ME и Nicla Voice. Платы Nicla хорошо справляются с эффективным выполнением процессов, созданных с помощью TensorFlow Lite. Например, одно ядро NiclaV на лету выполняет алгоритм компьютерного зрения (инференс), а другое решает низкоуровневые задачи: управляет мотором, обменивается данными или работает как пользовательский интерфейс. Встроенный беспроводной модуль позволяет одновременно использовать Wi-Fi и Bluetooth Low Energy (BLE).

Плата Arduino Nicla Vision

Аппаратная часть

Два параллельных ядра

Центральный процессор — двухъядерный STM32H747: ядро Cortex-M7 работает на частоте 480 МГц, ядро Cortex-M4 — на 240 МГц. Два ядра обмениваются данными через механизм удалённого вызова процедур (RPC), который позволяет без затруднений вызывать функции на другом процессоре. Оба процессора используют общую периферию кристалла и могут запускать:

  • скетчами Arduino поверх Arm Mbed OS;
  • нативными приложениями Mbed;
  • MicroPython и JavaScript через интерпретатор;
  • TensorFlow Lite.

Структура процессоров Nicla Vision

Память

Память важна для проектов встраиваемого машинного обучения. На плате NiclaV можно разместить до 16 МБ флеш-памяти QSPI для хранения данных. Но для инференса ML используется именно SRAM микроконтроллера: в STM32H747 её всего 1 МБ, и её делят оба процессора. Кроме того, в микроконтроллер встроено 2 МБ флеш-памяти, в основном для хранения кода.

Датчики

  • Камера: цветная CMOS-камера GC2145 с разрешением 2 Мп.
  • Микрофон: MP34DT05 — сверхкомпактный маломощный всенаправленный цифровой MEMS-микрофон с ёмкостным чувствительным элементом и интерфейсом.
  • 6-осевой IMU: данные 3D-гироскопа и 3D-акселерометра от 6-осевого IMU LSM6DSOX.
  • Датчик времени полёта (ToF): VL53L1CBV0FY добавляет точное измерение расстояния с малым энергопотреблением. Невидимый ближний инфракрасный лазер VCSEL (вместе с аналоговым драйвером) и приёмная оптика собраны в компактный модуль под камерой.

Установка Arduino IDE

Сначала подключите плату (micro USB) к компьютеру:

Подключение платы Nicla Vision по micro USB

Установите ядро Mbed OS для плат Nicla в Arduino IDE. При открытой IDE перейдите в Tools > Board > Board Manager, найдите в поиске Arduino Nicla Vision и установите плату.

Поиск Arduino Nicla Vision в Board Manager

Затем откройте Tools > Board > Arduino Mbed OS Nicla Boards и выберите Arduino Nicla Vision. Когда плата подключена по USB, в списке портов появится Nicla — выберите её.

Откройте скетч Blink в разделе Examples/Basic и запустите его кнопкой Upload в IDE. Должен замигать встроенный светодиод (зелёный RGB). Это значит, что плата Nicla установлена правильно и работает!

Проверка микрофона

В Arduino IDE перейдите в Examples > PDM > PDMSerialPlotter, откройте скетч и запустите его. Откройте Plotter и посмотрите аудиосигнал с микрофона:

График аудиосигнала с микрофона в Serial Plotter

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

Проверка IMU

Перед проверкой IMU нужно установить библиотеку LSM6DSOX. Для этого откройте Library Manager и найдите LSM6DSOX. Установите библиотеку от Arduino:

Установка библиотеки LSM6DSOX в Library Manager

Затем откройте Examples > Arduino_LSM6DSOX > SimpleAccelerometer и запустите тест акселерометра (можно также запустить тест гироскопа и температуры платы):

Вывод теста акселерометра LSM6DSOX

Проверка датчика ToF (времени полёта)

Как и в случае с IMU, нужно установить библиотеку VL53L1X ToF. Для этого откройте Library Manager и найдите VL53L1X. Установите библиотеку от Pololu:

Установка библиотеки VL53L1X в Library Manager

Затем запустите скетч proximity_detection.ino:

Запуск скетча proximity_detection в Arduino IDE

В Serial Monitor вы увидите расстояние от камеры до объекта перед ней (максимум 4 м).

Вывод расстояния в Serial Monitor

Проверка камеры

Камеру можно проверить, например, с помощью кода из раздела Examples > Camera > CameraCaptureRawBytes. Изображение мы напрямую не увидим, но сможем получить необработанные данные, которые формирует камера.

Чтобы увидеть изображение с камеры, можно воспользоваться Web Serial Camera (API). Это веб-приложение передаёт изображение с камеры по Web Serial с плат Arduino, оснащённых камерой.

Пример Web Serial Camera показывает, как передать данные изображения с платы Arduino и как распаковать их в JavaScript для отображения. Кроме того, в исходном коде веб-приложения есть примеры фильтров изображения, которые показывают, как изменять пиксельные данные для получения визуальных эффектов.

Скетч Arduino (CameraCaptureWebSerial) для передачи данных изображения находится здесь. Его также можно открыть в меню «Examples→Camera» Arduino IDE, если выбрана плата Nicla.

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

Изображение с камеры Nicla Vision в Web Serial Camera

Установка OpenMV IDE

OpenMV IDE — ведущая интегрированная среда разработки для камер OpenMV, в том числе похожих на Nicla Vision. В ней есть мощный текстовый редактор, терминал для отладки и просмотрщик кадров с гистограммой. Камеру мы будем программировать на MicroPython.

Откройте страницу загрузки OpenMV IDE, скачайте версию для вашей операционной системы и установите её, следуя инструкциям.

Окно установки OpenMV IDE

IDE должна открыться с кодом helloworld_1.py в области редактирования. Если этого не произошло, откройте его через File > Examples > HelloWorld > helloworld.py.

OpenMV IDE с кодом helloworld_1.py

Все сообщения, которые отправляются по последовательному соединению (через print() или сообщения об ошибках), во время работы отображаются в Serial Terminal. Изображение с камеры выводится в области Camera Viewer (или Frame Buffer), а гистограмма — сразу под ним, в области Histogram.

Обновление загрузчика

Прежде чем подключать Nicla к OpenMV IDE, убедитесь, что установлена последняя версия загрузчика. Откройте Arduino IDE, выберите плату Nicla и откройте скетч Examples > STM_32H747_System STM32H747_manageBootloader. Загрузите код на плату. Serial Monitor подскажет дальнейшие действия.

Установка прошивки

После обновления загрузчика переведите Nicla Vision в режим загрузчика, дважды нажав кнопку reset на плате. Встроенный зелёный светодиод начнёт плавно гаснуть и загораться. Теперь вернитесь в OpenMV IDE и нажмите значок подключения (левая панель инструментов):

Значок подключения на левой панели OpenMV IDE

Появится всплывающее окно, которое сообщит, что обнаружена плата в режиме DFU, и предложит выбрать дальнейшее действие. Сначала выберите Install the latest release firmware (vX.Y.Z). Это установит на Nicla Vision последнюю версию прошивки OpenMV.

Окно выбора установки прошивки в OpenMV IDE

Опцию Erase internal file system можно не выбирать, затем нажмите [OK].

Зелёный светодиод Nicla начнёт мигать, пока прошивка OpenMV загружается на плату. Затем откроется окно терминала с ходом прошивки.

Ход прошивки OpenMV в терминале

Дождитесь, пока зелёный светодиод перестанет мигать и плавно менять яркость. По окончании процесса появится сообщение «DFU firmware update complete!». Нажмите [OK].

Сообщение о завершении обновления DFU

Когда Nicla Vision подключается к панели инструментов, появляется зелёная кнопка «Play».

Зелёная кнопка Play на панели инструментов

Кроме того, на компьютере появится диск с именем «NO NAME».

Диск NO NAME в проводнике

Каждый раз, когда вы нажимаете кнопку [RESET] на плате, автоматически запускается сохранённый на ней скрипт main.py. Вы можете загрузить код main.py в IDE (File > Open File...).

Код main.py в OpenMV IDE

Это код «Blink», он подтверждает, что аппаратная часть исправна.

Проверка камеры

Чтобы проверить камеру, запустим helloworld_1.py. Для этого выберите скрипт в File > Examples > HelloWorld > helloworld.py.

Когда вы нажмёте зелёную кнопку «Play», скрипт MicroPython (helloworld.py) из области редактирования загрузится и запустится на Nicla Vision. В области Camera Viewer начнётся потоковая передача видео. В Serial Monitor будет выводиться FPS (кадров в секунду): примерно 27 кадров/с.

Вывод FPS в Serial Monitor при работе helloworld.py

Вот скрипт helloworld.py:

import sensor, time

sensor.reset()                      # Сброс и инициализация
                                    # датчика.
sensor.set_pixformat(sensor.RGB565) # Формат пикселей RGB565
                                    # (или GRAYSCALE)
sensor.set_framesize(sensor.QVGA)   # Размер кадра
                                    # QVGA (320x240)
sensor.skip_frames(time = 2000)     # Ожидание, пока
                                    # настройки вступят в силу.
clock = time.clock()                # Создание объекта часов
                                    # для отслеживания FPS.

while(True):
    clock.tick()                    # Обновление счётчика FPS.
    img = sensor.snapshot()         # Снимок и возврат
                                    # изображения.
    print(clock.fps())

Скрипты на Python, которые использовались здесь, можно найти на GitHub.

Код можно разделить на две части:

  • Настройка: подключаются и инициализируются библиотеки, определяются и задаются переменные.

  • Цикл: часть кода (цикл while), которая выполняется непрерывно. Снимается изображение (переменная img), то есть один кадр. Каждый кадр можно использовать для инференса в приложениях машинного обучения.

Чтобы остановить программу, нажмите красную кнопку [X].

Примечание: при подключении к IDE OpenMV Cam работает примерно вдвое медленнее. После отключения FPS должен вырасти.

Другие скрипты на Python можно найти в репозитории GitHub. Попробуйте проверить встроенные датчики.

Подключение Nicla Vision к Edge Impulse Studio

Edge Impulse Studio понадобится в других лабораторных работах. Edge Impulse — ведущая платформа для разработки машинного обучения на периферийных устройствах.

Edge Impulse официально поддерживает Nicla Vision. Поэтому сначала создайте новый проект в Studio и подключите Nicla к нему. Для этого выполните шаги:

  • Скачайте Arduino CLI для вашей архитектуры компьютера (ОС).
  • Скачайте самую свежую прошивку EI.
  • Распакуйте оба архива и поместите все файлы в одну папку.
  • Переведите Nicla Vision в режим загрузки, дважды нажав кнопку reset.
  • Запустите загрузчик (прошивку EI), соответствующий вашей ОС:

Запуск загрузчика прошивки EI

  • Пакетный скрипт для вашей ОС загрузит двоичный файл arduino-nicla-vision.bin на плату.

В Chrome для подключения Nicla к EI Studio можно использовать WebUSB. EI CLI не нужен.

Откройте свой проект в Studio, на вкладке Data Acquisition выберите WebUSB (1). Откроется окно; выберите вариант, в котором видно, что Nicla is paired (2), и нажмите [Connect] (3).

Подключение Nicla через WebUSB во вкладке Data Acquisition

Какие данные датчика собирать, можно выбрать в разделе Collect Data на вкладке Data Acquisition.

Выбор данных датчика в разделе Collect Data

Например, IMU data (inertial):

Выбор IMU data (inertial) в Edge Impulse

Или изображение (Camera):

Выбор камеры в Edge Impulse

Также можно проверить внешний датчик, подключённый к ADC (вывод 0 Nicla), и другие встроенные датчики: например, built-in microphone, ToF (Proximity) или комбинацию датчиков (fusion).

Расширение платы Nicla Vision (необязательно)

Иногда при прототипировании нужно поэкспериментировать с внешними датчиками и устройствами. Хорошее расширение для Nicla — Arduino MKR Connector Carrier (Grove compatible).

На плате 14 разъёмов Grove: пять одиночных аналоговых входов (A0–A5), один двойной аналоговый вход (A5/A6), пять одиночных цифровых входов/выходов (D0–D4), один двойной цифровой вход/выход (D5/D6), один I2C (TWI) и один UART (Serial). Все разъёмы совместимы с напряжением 5 В.

Учтите, что все 17 выводов Nicla Vision подключаются к разъёмам Grove расширения, но некоторые соединения Grove остаются неподключёнными.

Плата расширения MKR Connector Carrier

Эта плата расширения совместима с MKR и может использоваться с Nicla Vision и Portenta.

Nicla Vision, подключённая к плате расширения

Например, предположим, что в проекте TinyML нужно отправлять результаты инференса через устройство LoRaWAN и добавлять данные о локальной освещённости. При автономной работе часто рекомендуется локальный маломощный дисплей, например OLED. Такая схема показана ниже:

Схема подключения датчика освещённости, LoRaWAN и OLED к Nicla Vision

Датчик освещённости Grove подключается к одному из одиночных аналоговых выводов (A0/PC4), устройство LoRaWAN — к UART, а OLED-дисплей — к разъёму I2C.

Выводы Nicla 3 (Tx) и 4 (Rx) подключаются к разъёму Serial Shield. Обмен по UART используется для устройства LoRaWAN. Вот простой код для работы с UART:

# Тест UART — By: marcelo_rovai — Sat Sep 23 2023

import time
from pyb import UART
from pyb import LED

redLED = LED(1) # встроенный красный светодиод

# Инициализация объекта UART.
# UART Nicla Vision (выводы TX/RX) находится на «LP1»
uart = UART("LP1", 9600)

while(True):
    uart.write("Hello World!\r\n")
    redLED.toggle()
    time.sleep_ms(1000)

Чтобы проверить работу UART, подключите другое устройство, например Arduino UNO, которое выводит «Hello World» в Serial Monitor. Вот код.

Вывод Hello World на Serial Monitor Arduino UNO

Ниже приведён код «Hello World» для OLED с интерфейсом I2C. Драйвер OLED SSD1306 для MicroPython (ssd1306.py), созданный Adafruit, тоже нужно загрузить на Nicla (скрипт ssd1306.py можно найти на GitHub).

# Nicla_OLED_Hello_World — By: marcelo_rovai — Sat Sep 30 2023

# Сохраните на устройство драйвер OLED SSD1306 для MicroPython,
# интерфейсы I2C и SPI от Adafruit
import ssd1306

from machine import I2C
i2c = I2C(1)

oled_width = 128
oled_height = 64
oled = ssd1306.SSD1306_I2C(oled_width, oled_height, i2c)

oled.text('Hello, World', 10, 10)
oled.show()

Наконец, простой скрипт для чтения значения ADC на выводе «PC4» (вывод A0 Nicla):


# Датчик освещённости (A0) — By: marcelo_rovai — Wed Oct 4 2023

import pyb
from time import sleep

adc = pyb.ADC(pyb.Pin("PC4"))   # создание аналогового объекта
                                # из вывода
val = adc.read()                # чтение аналогового значения

while (True):

    val = adc.read()
    print ("Light={}".format (val))
    sleep (1)

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

Обратите внимание: приведённые выше скрипты (скачаны с GitHub) лишь показывают, как подключать внешние устройства к Nicla Vision с помощью MicroPython.

Итоги

Arduino Nicla Vision — отличное миниатюрное устройство для промышленного и профессионального применения! Оно мощное, надёжное, маломощное и имеет подходящие датчики для самых распространённых задач встраиваемого машинного обучения: зрения, движения, слияния данных датчиков и звука.

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

Ресурсы

Обзор

Эти лабораторные работы дают уникальную возможность получить практический опыт работы с системами машинного обучения (ML). В отличие от работы с большими моделями, которым нужны ресурсы уровня дата-центра, эти упражнения позволяют напрямую работать с оборудованием и программным обеспечением с помощью TinyML. Такой практический подход даёт осязаемое понимание проблем и возможностей развёртывания ИИ, пусть и в небольшом масштабе. При этом принципы в целом те же, что и при работе с более крупными системами.

Комплект XIAOML Kit

Где купить

Комплект XIAOML Kit включает плату XIAO ESP32S3 Sense, плату расширения с IMU, OLED-дисплеем и набором для работы с SD-картой:

Отдельные компоненты также доступны у Seeed Studio.

Предварительные требования

  • Комплект XIAOML Kit:

    • плата XIAO ESP32S3 Sense;
    • плата расширения с 6-осевым IMU и 0,42-дюймовым OLED-дисплеем;
    • набор для работы с SD-картой:
      • SD-карта и адаптер USB для хранения данных;
      • кабель USB-C для подключения платы к компьютеру.
  • Сеть: доступ в интернет для загрузки необходимого программного обеспечения.

Настройка

Упражнения

МодальностьЗадачаОписаниеСсылка
ЗрениеКлассификация изображенийНаучитесь классифицировать изображенияОткрыть
ЗрениеОбнаружение объектовРеализуйте обнаружение объектовОткрыть
ЗвукРаспознавание ключевых словИзучите системы распознавания голосаОткрыть
IMUКлассификация движения и обнаружение аномалийКлассифицируйте данные движения и находите аномалииОткрыть

Классификация изображений

Иллюстрация в стиле мультфильма 1950-х на основе реального изображения Marcelo Rovai
Промпт DALL·E: иллюстрация в стиле мультфильма 1950-х, основанная на реальном изображении Marcelo Rovai

Обзор

Мы всё чаще сталкиваемся с революцией искусственного интеллекта (ИИ), и, как утверждает Gartner, Edge AI и компьютерное зрение обладают очень высоким потенциалом влияния, и это уже сейчас!

Когда мы смотрим на машинное обучение (ML) применительно к зрению, первое понятие, с которым мы сталкиваемся, — классификация изображений, своего рода «Hello World» для ML: простая и вместе с тем глубокая задача!

XIAOML Kit от Seeed Studio — комплексное аппаратное решение на основе XIAO ESP32-S3 Sense со встроенной камерой OV3660 и поддержкой SD-карт. Эти возможности делают XIAO ESP32S3 Sense отличной отправной точкой для знакомства с компьютерным зрением на базе TinyML.

В этой лабораторной работе мы разберём классификацию изображений с помощью инструмента SenseCraft AI, не требующего написания кода, а затем более подробно рассмотрим разработку с Edge Impulse Studio и Arduino IDE.

Цели обучения

  • Развёртывание предобученных моделей с помощью SenseCraft AI Studio для быстрых приложений компьютерного зрения;
  • Сбор и управление наборами изображений для собственных задач классификации с правильной организацией данных;
  • Обучение собственных моделей классификации изображений методом переноса обучения (transfer learning) с архитектурой MobileNet V2;
  • Оптимизация моделей для развёртывания на периферии с помощью квантизации и экономной по памяти предобработки;
  • Реализация конвейеров постобработки, включая управление GPIO и интеграцию инференса в реальном времени;
  • Сравнение подходов к разработке без кода и с помощью продвинутых платформ ML для встраиваемых приложений.

Классификация изображений

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

Классификация изображений важна во многих областях: от организации и поиска по большим базам изображений в цифровых библиотеках и социальных сетях до помощи автономным системам в понимании окружения. Среди архитектур, которые существенно продвинули эту область, — свёрточные нейронные сети (CNN), например AlexNet, VGGNet и ResNet. Эти модели показали впечатляющую точность на сложных наборах данных, таких как ImageNet, выучивая иерархические представления визуальных данных.

Классификация изображений — основа многих систем компьютерного зрения: она открывает путь к более сложным задачам, таким как обнаружение объектов и сегментация изображений, и помогает глубже понимать визуальные данные в разных отраслях. Начнём знакомство с моделью Person Classification («Person — No Person», то есть «человек — не человек»), готовым приложением компьютерного зрения в SenseCraft AI.

Модель Person Classification в SenseCraft AI

Классификация изображений в рабочем пространстве SenseCraft AI

Сначала подключите XIAOML Kit (или только XIAO ESP32S3 Sense, отсоединённую от платы расширения) к компьютеру через USB-C, затем откройте рабочее пространство SenseCraft AI и подключите устройство.

Подключение устройства в рабочем пространстве SenseCraft AI

После подключения выберите опцию [Select Model...] и введите в строке поиска: «Person Classification». Из доступных вариантов выберите модель, обученную на MobileNet V2 (при наведении курсора на модели открывается всплывающее окно с их основными характеристиками).

Выбор модели Person Classification на MobileNet V2

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

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

После успешной загрузки модели в области Preview мы увидим видеопоток с камеры XIAO и результат классификации (Person или Not a Person), а в Device Logger — подробности инференса.

Также можно выбрать интервал кадров инференса — от «Real-Time» (по умолчанию) до 10 секунд, а также режим передачи данных (UART, I2C и др.) — данные передаются устройством (по умолчанию UART через USB).

Результаты инференса модели Person Classification

В Device Logger видно, что задержка модели составляет от 52 до 78 мс на предобработку и около 532 мс на инференс. Итого выходит чуть меньше 600 мс, или примерно 1,7 кадра в секунду (FPS).

Для работы MobileNet V2 0.35 XIAO потребляла пиковый ток 160 мА при напряжении 5,23 В, что даёт энергопотребление 830 мВт.

Постобработка

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

Конвейер: от классификации к действию

В SenseCraft AI это делается в разделе Output -> GPIO. Нажмите значок «Add», чтобы запускать действие при выполнении условий события. Откроется всплывающее окно, где можно задать нужное действие. Например, если человек обнаружен с уверенностью более 60 %, внутренний светодиод LED должен включаться. В реальном сценарии для запуска реле, которое включит свет, использовали бы GPIO, например D0, D1, D2, D11 или D12.

Настройка действия Output GPIO в SenseCraft AI

После подтверждения появится созданное Trigger Action (триггерное действие). Нажмите Send, чтобы отправить команду на XIAO.

Созданное триггерное действие Trigger Action

Теперь, если направить XIAO на человека, внутренний светодиод загорится.

Светодиод загорается при обнаружении человека

Другие триггерные действия и методы постобработки мы рассмотрим дальше в этой лабораторной работе.

Проект классификации изображений

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

Конвейер машинного обучения в SenseCraft AI Studio

В SenseCraft AI Studio откройте вкладку Training:

Вкладка Training в SenseCraft AI Studio

По умолчанию обучается модель Classification с веб-камерой, если она доступна. Выберем вместо этого XIAOESP32S3 Sense. Нажатие зелёной кнопки [Connect] откроет всплывающее окно. Выберите соответствующий порт и нажмите синюю кнопку [Connect].

Выбор XIAOESP32S3 Sense и подключение во вкладке Training

Изображение с камеры отобразится в области предпросмотра.

Цель

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

Промышленная линия распределения колёс и коробок

Смоделируем эту задачу: будем классифицировать игрушечную коробку (box) и игрушечное колесо (wheel). Также добавим третий класс изображений — фон (background), на котором в кадре нет объектов.

Три класса изображений: фон, коробка и колесо

Сбор данных

Создадим классы, например в алфавитном порядке:

  • Класс 1: background (фон);
  • Класс 2: box (коробка);
  • Класс 3: wheel (колесо).

Создание трёх классов в Studio

Выберите один из классов и удерживайте зелёную кнопку (Hold to Record) под областью предпросмотра. Собранные изображения (и их количество) появятся на экране образцов. Аккуратно и медленно перемещайте камеру, чтобы снять объект с разных ракурсов. Чтобы поменять положение объекта или помешать съёмке, отпустите зелёную кнопку, переставьте объект и снова удерживайте её, чтобы продолжить съёмку.

Экран сбора образцов изображений

После съёмки просмотрите изображения и удалите неверные.

Очистка набора данных от неверных изображений

Соберите около 50 изображений из каждого класса и переходите к этапу обучения.

Собранные изображения можно скачать и использовать в другом приложении, например в Edge Impulse Studio.

Обучение

Убедитесь, что выбрано правильное устройство (XIAO ESP32S3 Sense), и нажмите [Start Training].

Настройка обучения и кнопка Start Training

Тест

После обучения можно просмотреть результат инференса.

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

Предварительный просмотр инференса в Studio

Теперь пора по-настоящему развернуть модель на устройстве.

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

В окне Supported Devices выберите обученную модель и XIAO ESP32S3 Sense, затем нажмите [Deploy to device].

Выбор модели и устройства для развёртывания

SenseCraft AI перенаправит нас на вкладку Vision Workplace. Подтвердите развёртывание (Confirm), выберите порт и подключите устройство (Connect).

Подтверждение развёртывания и выбор порта

Модель будет записана в устройство. После автоматической перезагрузки модель начнёт работать на устройстве. В Device Logger видно, что задержка инференса составляет примерно 426 мс, плюс около 110 мс на предобработку, что соответствует частоте кадров 1,8 кадра в секунду (FPS).

Также в Settings можно настроить уверенность модели.

Настройка уверенности модели в Settings

Для работы модели классификации изображений XIAO ESP32S3 потребляла пиковый ток 140 мА при напряжении 5,23 В, что даёт энергопотребление 730 мВт.

Как и раньше, в разделе Output –> GPIO можно включать GPIO или внутренний светодиод в зависимости от обнаруженного класса. Например, светодиод загорится, когда обнаружено колесо.

Светодиод загорается при обнаружении колеса

Сохранение модели

Модель можно сохранить в SenseCraft AI Studio. Studio сохранит все наши модели для последующего развёртывания. Для этого вернитесь на вкладку Training и нажмите кнопку [Save to SenseCraft]:

Кнопка сохранения модели в SenseCraft

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

Сохранённая модель в SenseCraft AI

Обратите внимание: обученную модель (Int8 MobileNet V2 размером 320 КБ) можно скачать для дальнейшего использования или анализа, например с помощью Netron. Модель использует изображения размером в качестве входного тензора. На следующем шаге мы применим другие гиперпараметры в Edge Impulse Studio.

Структура модели в Netron

Модель также можно развернуть на устройстве повторно в любой момент. Рабочее пространство Workspace откроется в SenseCraft AI автоматически.

Проект классификации изображений на основе набора данных

Основная цель нашего проекта — обучить модель и выполнить инференс на XIAO ESP32S3 Sense. Для обучения нужно найти данные (на самом деле огромное количество данных!).

Но, как мы уже знаем, прежде всего нужна цель! Что мы хотим классифицировать?

С TinyML — набором методов, связанных с инференсом моделей машинного обучения на встраиваемых устройствах, — классификацию следует ограничивать тремя-четырьмя категориями из-за ограничений, главным образом по памяти. Например, можно обучить модель на снятых изображениях «коробка против колеса»; их можно скачать из SenseCraft AI Studio.

Также можно использовать совершенно новый набор данных, например с различением яблок, бананов и картофеля или других категорий. По возможности найдите специализированный набор данных с изображениями этих категорий. Хорошим началом будет Kaggle fruit-and-vegetable-image-recognition.

Загрузим набор данных, собранный в предыдущем разделе. Откройте меню (три точки) для каждого снятого класса и выберите Export Data.

Экспорт набора данных по классу

Набор данных будет загружен на компьютер в виде ZIP-файла: по одному файлу на каждый класс. Сохраните их в рабочую папку и распакуйте. У вас должны появиться три папки — по одной на каждый класс.

Три папки классов на компьютере

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

Обучение модели в Edge Impulse Studio

Для обучения модели мы будем использовать Edge Impulse Studio. Edge Impulse — ведущая платформа разработки машинного обучения для периферийных устройств.

Войдите в аккаунт Edge Impulse (или создайте бесплатный) и создайте новый проект.

Сбор данных

Перейдите в раздел Data acquisition и выберите + Add data. Появится всплывающее окно. Выберите UPLOAD DATA.

Добавление данных в Data acquisition

После выбора появится новое всплывающее окно с запросом на обновление данных.

  • В режиме загрузки (Upload mode) выберите select a folder и нажмите [Choose Files].
  • Перейдите в папку, которая содержит один из классов, и нажмите [Upload].

Выбор папки класса для загрузки

  • Вы автоматически вернётесь в окно Upload data.
  • Выберите Automatically split between training and testing.
  • Введите метку изображений, которые находятся в папке.
  • Выберите [Upload data].
  • Файлы начнут загружаться. После этого появится другое всплывающее окно с вопросом, создаёте ли вы проект обнаружения объектов. Выберите [no].

Загрузка данных с меткой и автоматическим разделением

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

Закройте окно Upload Data и вернитесь на страницу Data acquisition. Мы видим, что весь набор загружен. На верхней панели видно 158 элементов, распределённых поровну. Также 19 % изображений оставлены для тестирования.

Загруженный набор данных на странице Data acquisition

Проектирование Impulse

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

Классификация изображений — самое распространённое применение глубокого обучения, но для этой задачи нужен большой объём данных. У нас около 50 изображений на каждую категорию. Достаточно ли этого? Совсем нет! Чтобы «научить» модель различать каждый класс, нужны тысячи изображений. Однако эту проблему можно решить, дообучив уже обученную модель на наших данных. Этот приём называется «переносом обучения» (Transfer Learning, TL). С TL можно дообучить предобученную модель классификации изображений на наших данных, добиваясь хорошей производительности даже на относительно небольших наборах изображений, как в нашем случае.

Схема переноса обучения (Transfer Learning)

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

Итак, начиная с сырых изображений, изменим их размер пикселей и передадим в блок Transfer Learning. Создадим Impulse.

На этом этапе можно также задать целевое устройство, чтобы контролировать «бюджет» (память и задержку). XIAO ESP32S3 официально не поддерживается Edge Impulse, поэтому рассмотрим Espressif ESP-EYE: он похож, но медленнее.

Конструкция Impulse для классификации изображений

Сохраните Impulse, как показано выше, и перейдите в раздел Image.

Предобработка (генерация признаков)

Помимо изменения размера изображений, их можно перевести в оттенки серого или сохранить исходную глубину цвета RGB. Выберем [RGB] в разделе Image. Тогда каждый образец будет иметь размерность 27 648 признаков (). Нажатие [Save Parameters] откроет новую вкладку Generate Features. Нажмите кнопку [Generate Features], чтобы создать признаки.

Проектирование модели, обучение и тестирование

В 2017 году Google представил MobileNetV1. В 2018 году вышла MobileNetV2: Inverted Residuals and Linear Bottlenecks, а в 2019-м — V3. MobileNet — семейство универсальных нейронных сетей для компьютерного зрения, специально созданных для мобильных устройств и поддерживающих классификацию, обнаружение и другие задачи. MobileNet — компактные модели с малой задержкой и низким энергопотреблением, параметры которых подобраны под ограничения ресурсов разных сценариев.

Хотя базовая архитектура MobileNet уже компактна и быстра, конкретному приложению часто нужна ещё более маленькая и быстрая модель. MobileNet вводит простой параметр α (альфа), называемый множителем ширины (width multiplier), для построения меньших и менее требовательных к вычислениям моделей. Роль множителя ширины α — равномерно «утончить» сеть на каждом слое.

В Edge Impulse Studio доступны MobileNet V1 (изображения ) и V2 (изображения и ) с несколькими значениями α (от 0,05 до 1,0). Например, наибольшая точность достигается у V2 с изображениями и α=1,0. Конечно, есть компромисс: чем выше точность, тем больше памяти (около 1,3 МБ ОЗУ и 2,6 МБ ПЗУ) нужно для работы модели и тем выше задержка. Наименьший объём — у MobileNet V1 с α=0,10 (около 53,2 КБ ОЗУ и 101 КБ ПЗУ).

Мы будем использовать MobileNet V2 0.35 как базовую модель (но можно выбрать модель с большим α). Последний слой нашей модели, предшествующий выходному, будет содержать 16 нейронов с 10 %-ным dropout для предотвращения переобучения.

Ещё один обязательный приём в глубоком обучении — аугментация данных (data augmentation). Это метод, который помогает повысить точность моделей машинного обучения за счёт создания дополнительных искусственных данных. Система аугментации вносит небольшие случайные изменения в обучающие данные в процессе обучения (например, отражает, обрезает или поворачивает изображения).

Заглянем «под капот» и посмотрим, как Edge Impulse реализует политику аугментации данных:

# Реализация политики аугментации данных
def augment_image(image, label):
    # Случайно отражаем изображение по горизонтали
    image = tf.image.random_flip_left_right(image)

    # Увеличиваем размер изображения, затем случайно обрезаем его до
    # исходных размеров
    resize_factor = random.uniform(1, 1.2)
    new_height = math.floor(resize_factor * INPUT_SHAPE[0])
    new_width = math.floor(resize_factor * INPUT_SHAPE[1])
    image = tf.image.resize_with_crop_or_pad(image, new_height,
                                             new_width)
    image = tf.image.random_crop(image, size=INPUT_SHAPE)

    # Меняем яркость изображения
    image = tf.image.random_brightness(image, max_delta=0.2)

    return image, label

Теперь определим гиперпараметры:

  • эпох: 20;
  • размер батча (batch size): 32;
  • скорость обучения (learning rate): 0,0005;
  • размер валидационной выборки: 20 %.

Результат обучения:

Результат обучения модели классификации изображений

Профиль модели предсказывает 233 КБ ОЗУ и 546 КБ флеш-памяти, что не создаёт проблем для XIAO ESP32S3, у которой 8 МБ PSRAM. Кроме того, Studio показывает задержку около 1160 мс, что очень много. Однако этого и следовало ожидать: мы используем ESP-EYE, у которого процессор Xtensa LX6, тогда как ESP32S3 использует более новый и мощный Xtensa LX7.

На тестовых данных мы также получили 100 % точности, даже для квантованной модели INT8. В реальных проектах такой результат нехарактерен, но наш проект относительно прост: два объекта очень хорошо отличаются друг от друга.

Развёртывание модели

Обученную модель можно развернуть:

  • как .TFLITE для использования в SenseCraft AI;
  • как Arduino Library в Edge Impulse Studio.

Начнём с SenseCraft, это проще и интуитивнее.

Развёртывание модели в SenseCraft AI

На Dashboard можно скачать обученную модель в нескольких форматах. Скачаем TensorFlow Lite (int8 quantized) размером 623 КБ.

Скачивание модели TensorFlow Lite int8 на Dashboard

В SenseCraft AI Studio откройте вкладку Workspace, выберите XIAO ESP32S3, соответствующий порт и подключите устройство.

Вы увидите последнюю модель, загруженную на устройство. Нажмите зелёную кнопку [Upload Model]. Появится всплывающее окно с запросом названия модели, файла модели и названий классов (объектов). Используем метки в алфавитном порядке: 0: background, 1: box и 2: wheel, затем нажмите [Send].

Загрузка модели TFLite в SenseCraft AI с метками классов

Через несколько секунд модель будет загружена («прошита») на устройство, и изображение с камеры появится в реальном времени в области Preview. Результат классификации будет показан под предпросмотром. В Settings с помощью курсора можно выбрать Confidence Threshold (порог уверенности) инференса.

В Device Logger в Serial Monitor видно задержку: около 81 мс на предобработку и 205 мс на инференс, что соответствует 3,4 кадра в секунду (FPS). Это вдвое больше, чем мы получили при обучении модели в SenseCraft, потому что здесь используются изображения меньшего размера ( против ).

Общая задержка примерно в 4 раза меньше, чем оценка в Edge Impulse Studio для процессора Xtensa LX6; теперь инференс выполняется на процессоре Xtensa LX7.

Результаты инференса TFLite-модели в Device Logger

Постобработка

Результат инференса модели, включая задержку, ID класса и уверенность, можно получить, как показано в Device Logger в SenseCraft AI. Это позволяет использовать XIAO ESP32S3 Sense как ИИ-датчик. Иначе говоря, данные модели можно получать по разным протоколам связи, таким как MQTT, UART, I2C или SPI, в зависимости от требований проекта.

Идея похожа на то, что мы делали в лабораторной работе по классификации изображений для Seeed Grove Vision AI V2 (раздел «Постобработка»).

Ниже пример подключения по шине I2C.

Подключение XIAO по шине I2C как датчика (Wiki Seeed Studio)

Подробнее см. вики Seeed Studio.

Развёртывание как библиотеки Arduino в EI Studio

На вкладке Deploy в Edge Impulse Studio выберите Arduino library, TensorFlow Lite, Quantized(int8) и нажмите [Build]. Обученная модель будет скачана как ZIP-библиотека Arduino:

Сборка Arduino-библиотеки в Edge Impulse Studio

Откройте Arduino IDE, в меню Sketch выберите Include Library и Add .ZIP Library. Затем укажите файл, скачанный из Edge Impulse Studio, и нажмите [Open].

Установка ZIP-библиотеки в Arduino IDE

Перейдите в Examples в Arduino IDE и найдите проект по имени (в данном случае: «Box_versus_Wheel_…Interfering»). Откройте esp32 → esp32_camera. Скетч esp32_camera.ino появится в IDE.

Этот скетч создан для обычной платы ESP32 и не будет работать с XIAO ESP32S3 Sense. Его нужно изменить. Скачайте изменённую версию из репозитория проекта на GitHub: Image_class_XIAOML-Kit.ino.

Разбор кода классификации изображений для XIAO ESP32S3

Код снимает изображения со встроенной камеры, обрабатывает их и классифицирует (в данном случае «Box», «Wheel» или «Background») с помощью модели, обученной в EI Studio. Он работает непрерывно и выполняет инференс в реальном времени на периферийном устройстве.

Коротко:

Камера → изображение JPEG → преобразование в RGB888 → изменение размера до → нейронная сеть → результаты классификации → вывод в последовательный порт

Основные компоненты
  1. Подключение библиотек и зависимостей
#include <Box_versus_Wheel_-_XIAO_ESP32S3_inferencing.h>
#include "edge-impulse-sdk/dsp/image/image.hpp"
#include "esp_camera.h"
  • Библиотека инференса Edge Impulse: содержит нашу обученную модель и движок инференса;
  • Обработка изображений: предоставляет функции для работы с изображениями;
  • ESP Camera: аппаратный интерфейс модуля камеры.
  1. Конфигурации выводов камеры

Плата XIAO ESP32S3 Sense может работать с разными сенсорами камеры (OV2640 или OV3660), у которых могут быть разные конфигурации выводов. Код задаёт три возможные конфигурации:

// Конфигурация 1: наиболее распространённая конфигурация OV2640
#define CONFIG_1_XCLK_GPIO_NUM    10
#define CONFIG_1_SIOD_GPIO_NUM    40
#define CONFIG_1_SIOC_GPIO_NUM    39
// ... остальные выводы

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

  1. Настройки управления памятью
#define EI_CAMERA_RAW_FRAME_BUFFER_COLS   320
#define EI_CAMERA_RAW_FRAME_BUFFER_ROWS   240
#define EI_CLASSIFIER_ALLOCATION_HEAP      1
  • Размер буфера кадра: задаёт размер исходного изображения ( пикселей);
  • Выделение в куче (heap): используется динамическое выделение памяти для гибкости;
  • Поддержка PSRAM: у ESP32S3 есть 8 МБ PSRAM для хранения больших данных, например изображений.
setup() — инициализация
void setup() {
    Serial.begin(115200);
    while (!Serial);

    if (ei_camera_init() == false) {
        ei_printf("Failed to initialize Camera!\r\n");
    } else {
        ei_printf("Camera initialized\r\n");
    }

    ei_sleep(2000);  // Ждём 2 секунды перед запуском
}

Эта функция:

  1. Инициализирует последовательную связь для отладочного вывода;
  2. Инициализирует камеру с автоматическим определением конфигурации;
  3. Ждёт 2 секунды перед началом непрерывного инференса.
loop() — главный цикл обработки

Цикл выполняет следующие шаги непрерывно:

Шаг 1. Выделение памяти

snapshot_buf = (uint8_t*)ps_malloc(EI_CAMERA_RAW_FRAME_BUFFER_COLS *
                                   EI_CAMERA_RAW_FRAME_BUFFER_ROWS *
                                   EI_CAMERA_FRAME_BYTE_SIZE);

Выделяет память под буфер изображения, предпочтительно в PSRAM (более быстрой внешней ОЗУ), а при необходимости — в обычной куче.

Шаг 2. Захват изображения

if (ei_camera_capture((size_t)EI_CLASSIFIER_INPUT_WIDTH,
                     (size_t)EI_CLASSIFIER_INPUT_HEIGHT,
                     snapshot_buf) == false) {
    ei_printf("Failed to capture image\r\n");
    free(snapshot_buf);
    return;
}

Снимает изображение с камеры и сохраняет его в буфер.

Шаг 3. Запуск инференса

ei_impulse_result_t result = { 0 };
EI_IMPULSE_ERROR err = run_classifier(&signal, &result, false);

Запускает модель машинного обучения на снятом изображении.

Шаг 4. Вывод результатов

for (uint16_t i = 0; i < EI_CLASSIFIER_LABEL_COUNT; i++) {
    ei_printf("  %s: %.5f\r\n",
              ei_classifier_inferencing_categories[i],
              result.classification[i].value);
}

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

ei_camera_init() — интеллектуальная инициализация камеры

Эта функция реализует интеллектуальную последовательность инициализации:

bool ei_camera_init(void) {
    // Пробуем конфигурацию 1 (распространённая для OV2640)
    update_camera_config(1);
    esp_err_t err = esp_camera_init(&camera_config);
    if (err == ESP_OK) goto camera_init_success;

    // Пробуем конфигурацию 2 (OV3660)
    esp_camera_deinit();
    update_camera_config(2);
    err = esp_camera_init(&camera_config);
    if (err == ESP_OK) goto camera_init_success;

    // Продолжаем пробовать другие конфигурации...
}

Функция:

  1. Пробует несколько конфигураций выводов;
  2. Проверяет разные тактовые частоты (10 МГц или 16 МГц);
  3. Сначала пытается использовать PSRAM, затем переходит на DRAM;
  4. Применяет настройки сенсора в зависимости от обнаруженного оборудования.
ei_camera_capture() — конвейер обработки изображений
bool ei_camera_capture(uint32_t img_width, uint32_t img_height, uint8_t *out_buf) {
    // 1. Получаем кадр с камеры
    camera_fb_t *fb = esp_camera_fb_get();

    // 2. Преобразуем JPEG в формат RGB888
    bool converted = fmt2rgb888(fb->buf, fb->len, PIXFORMAT_JPEG, snapshot_buf);

    // 3. Возвращаем буфер кадра драйверу камеры
    esp_camera_fb_return(fb);

    // 4. При необходимости меняем размер
    if (do_resize) {
        ei::image::processing::crop_and_interpolate_rgb888(...);
    }
}

Функция:

  1. Снимает JPEG-изображение с камеры;
  2. Преобразует его в формат RGB888 (требуется моделью машинного обучения);
  3. Изменяет размер изображения под входной размер модели ( пикселей).

Инференс

  • Загрузите код на XIAO ESP32S3 Sense.

⚠️ Внимание

  • На XIAO ESP32S3 ОБЯЗАТЕЛЬНО должна быть включена PSRAM. Проверить это можно в верхнем меню Arduino IDE: Tools → PSRAM:OPI PSRAM;
  • Пакет поддержки плат (esp32 by Espressif Systems) должен быть версии 2.0.17. Не обновляйте его.

Настройки Arduino IDE для XIAO ESP32S3 с включённой PSRAM

  • Откройте Serial Monitor.
  • Наведите камеру на объекты и проверьте результат в Serial Monitor.

Результаты инференса в Serial Monitor

Постобработка

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

Маленький 0,42-дюймовый OLED-дисплей XIAOML Kit ( пикселей) становится важным компонентом постобработки: он превращает сырые результаты инференса в понятную человеку обратную связь — выводит названия классов и уровни уверенности прямо на устройстве. Это исключает необходимость во внешних мониторах и позволяет создавать по-настоящему автономные периферийные ИИ-системы в промышленности, сельском хозяйстве или розничной торговле, где мгновенное визуальное подтверждение предсказаний ИИ особенно важно.

Теперь изменим скетч так, чтобы он автоматически адаптировался к модели, обученной в Edge Impulse: он будет читать названия классов и их количество прямо из модели. На дисплее будут показываться сокращённые названия классов (по 3 буквы) крупным шрифтом для лучшей читаемости на маленьком дисплее пикселей. Скачайте код с GitHub: XIAOML-Kit-Img_Class_OLED_Gen.

Запустим код и посмотрим результат:

Результат работы кода на OLED-дисплее XIAOML Kit

Итоги

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

  • SenseCraft AI Studio даёт доступную отправную точку с интерфейсом без кода, позволяя быстро создавать прототипы и развёртывать предобученные модели, например для распознавания людей. С инференсом в реальном времени и встроенными средствами постобработки она показывает, как ИИ можно развернуть без глубоких знаний программирования или ML.

  • Для более сложных приложений Edge Impulse Studio предлагает полноценные инструменты конвейера машинного обучения: управление собственными наборами данных, перенос обучения с несколькими предобученными моделями (например, MobileNet) и оптимизацию модели.

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

Работа демонстрирует фундаментальные принципы TinyML, которые выходят за рамки конкретного оборудования: инференс при ограниченных ресурсах, требования к обработке в реальном времени и полный путь от сбора данных через развёртывание модели до практических применений. Благодаря встроенным средствам постобработки, включая управление GPIO и протоколы связи, XIAO становится не просто движком инференса, а полноценной платформой ИИ-датчика.

Эта основа в классификации изображений подготовит вас к более сложным задачам компьютерного зрения и покажет, как современный периферийный ИИ делает продвинутое компьютерное зрение доступным, экономичным и применимым во встраиваемых системах реального мира — от промышленной автоматизации до систем «умного дома».

Ресурсы

Распознавание ключевых слов (KWS)

Иллюстрация в стиле мультфильма 1950-х на основе реального изображения Marcelo Rovai
Промпт DALL·E: иллюстрация в стиле мультфильма 1950-х на основе реального изображения Marcelo Rovai

Обзор

Распознавание ключевых слов (Keyword Spotting, KWS) — неотъемлемая часть многих систем распознавания речи: оно позволяет устройствам реагировать на определённые слова или фразы. Эта технология лежит в основе популярных устройств вроде Google Assistant или Amazon Alexa, но она в равной мере применима и достижима на небольших маломощных устройствах. Эта лабораторная работа проведёт вас через реализацию системы KWS с помощью TinyML на отладочной плате XIAO ESP32S3.

XIAO ESP32S3 с чипом ESP32-S3 от Espressif — компактный и мощный микроконтроллер с двухъядерным процессором Xtensa LX7, встроенными Wi-Fi и Bluetooth. Сочетание вычислительной мощности, энергоэффективности и гибкой связи делает его отличной платформой для приложений TinyML. Кроме того, благодаря плате расширения Sense у нас будет доступ к «sense»-части устройства, на которой есть камера, слот для SD-карты и цифровой микрофон. Встроенный микрофон и SD-карта будут ключевыми в этом проекте.

Мы будем использовать Edge Impulse Studio — мощную и удобную платформу, которая упрощает создание моделей машинного обучения и их развёртывание на периферийных устройствах. Мы шаг за шагом обучим модель KWS, оптимизируем её и развернём на XIAO ESP32S3 Sense.

Наша модель будет распознавать ключевые слова, которые могут будить устройство или запускать определённые действия (в случае «YES»), и оживлять ваши проекты голосовым управлением.

Опираясь на опыт работы с TensorFlow Lite для микроконтроллеров (движок «под капотом» EI Studio), мы создадим систему KWS, способную выполнять машинное обучение в реальном времени прямо на устройстве.

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

Цели обучения

  • Понять архитектуру голосового помощника, включая каскадные системы обнаружения и роль распознавания ключевых слов на периферии как первого этапа конвейеров обработки голоса;
  • Освоить методы сбора аудиоданных: офлайн (микрофон XIAO ESP32S3 с записью на SD-карту) и онлайн (смартфон и Edge Impulse Studio);
  • Реализовать цифровую обработку сигналов для аудио: основы протокола I2S, дискретизацию аудио 16 кГц/16 бит и преобразование сигналов из временной области в признаки частотной области с помощью MFCC;
  • Обучить свёрточные нейронные сети для классификации аудио методом переноса обучения, с приёмами аугментации данных и оптимизацией модели для классификации по четырём классам (YES, NO, NOISE, UNKNOWN);
  • Развернуть оптимизированные модели на микроконтроллерах с квантизацией (INT8), управлением памятью через PSRAM и оптимизацией инференса в реальном времени для встраиваемых систем;
  • Создать полные конвейеры постобработки, включая пороги уверенности, управление GPIO для внешних устройств, интеграцию OLED-дисплея и создание автономных ИИ-датчиков;
  • Сравнить способы разработки на платформах без кода (Edge Impulse Studio) и в традиционном программировании встраиваемых систем (Arduino IDE) для приложений TinyML.

Проект KWS

Как работает голосовой помощник?

Распознавание ключевых слов (KWS) критически важно для многих голосовых помощников: оно позволяет устройствам реагировать на определённые слова или фразы. Для начала важно понять, что голосовые помощники на рынке, например Google Home или Amazon Echo Dot, реагируют на человека, только когда их «будят» определённые ключевые слова: «Hey Google» в первом случае и «Alexa» во втором.

Ключевые слова для пробуждения голосовых помощников

Иначе говоря, распознавание голосовых команд основано на многоступенчатой модели, или каскадном обнаружении (Cascade Detection).

Схема каскадного обнаружения голосовых команд

Этап 1: маленький микропроцессор внутри Echo Dot или Google Home постоянно слушает звук и ждёт, когда будет распознано ключевое слово. Для такого обнаружения на периферии используется модель TinyML (приложение KWS).

Этап 2: данные отправляются в облако и обрабатываются более крупной моделью, только если приложение KWS на этапе 1 сработало.

В видео ниже показан пример: я эмулирую Google Assistant на Raspberry Pi (этап 2), а в роли устройства TinyML (этап 1) используется Arduino Nano 33 BLE.

Если хотите глубже изучить весь проект, см. моё руководство: Building an Intelligent Voice Assistant From Scratch.

В этой лабораторной работе мы сосредоточимся на этапе 1 (KWS, или Keyword Spotting), где используем XIAO ESP32S3 Sense с цифровым микрофоном для распознавания ключевого слова.

Конвейер инференса

Схема ниже показывает, как должно работать итоговое приложение KWS (во время инференса):

Конвейер инференса приложения KWS

Наше приложение KWS будет распознавать четыре класса звуков:

  • YES (ключевое слово 1);
  • NO (ключевое слово 2);
  • NOISE (ключевых слов нет, есть только фоновый шум);
  • UNKNOWN (смесь слов, отличных от YES и NO).

Для реальных проектов всегда рекомендуется включать в набор не только ключевые слова, но и другие слова и звуки, например «Noise» (шум или фон) и «Unknown» (неизвестные слова).

Рабочий процесс машинного обучения

Главный компонент приложения KWS — его модель. Поэтому нужно обучить модель на наших ключевых словах, шуме и других словах («unknown»):

Рабочий процесс обучения модели KWS

Набор данных

Критически важный компонент рабочего процесса машинного обучения — набор данных. Выбрав конкретные ключевые слова (YES и NO), мы можем воспользоваться набором данных, который разработал Пит Уорден (Pete Warden): «Speech Commands: A Dataset for Limited-Vocabulary Speech Recognition». В этом наборе 35 ключевых слов (по более чем 1000 образцов каждого), например yes, no, stop и go. То есть для yes и no можно получить по 1500 образцов.

Небольшую часть набора данных можно скачать из Edge Impulse Studio (готовый набор данных для распознавания ключевых слов). Он включает образцы четырёх классов, которые мы будем использовать в проекте: yes, no, noise и background. Для этого выполните следующие шаги:

Хотя данных из набора Пита много, рекомендуется записать и слова, которые произнесём сами. При работе с акселерометрами важно было собирать набор данных датчиком того же типа. Для звука ситуация иная, потому что на самом деле мы классифицируем аудио.

Ключевое различие между звуком и аудио — в виде энергии. Звук — это энергия механических волн (продольных звуковых волн), которые распространяются в среде и вызывают колебания давления. Аудио — это электрическая энергия (аналоговые или цифровые сигналы), которая электрически представляет звук.

Когда мы произносим ключевое слово, звуковые волны нужно преобразовать в аудиоданные. Преобразование выполняется дискретизацией сигнала с микрофона с частотой 16 кГц и разрядностью 16 бит.

Поэтому подойдёт любое устройство, которое может создавать аудиоданные с такими базовыми параметрами (16 кГц/16 бит). В качестве устройства можно использовать XIAO ESP32S3 Sense, компьютер или даже смартфон.

Преобразование звука в аудиоданные

Онлайн-запись аудиоданных с Edge Impulse и смартфона

В лабораторной работе «Классификация движения и обнаружение аномалий» мы подключали устройство напрямую к Edge Impulse Studio для сбора данных (с частотой дискретизации 50–100 Гц). Для такой низкой частоты можно было использовать функцию CLI Data Forwarder, однако, по словам Jan Jongboom, CTO Edge Impulse, аудио (16 кГц) передаётся слишком быстро для Data Forwarder. Поэтому, получив цифровые данные с микрофона, мы можем превратить их в файл WAV и отправить в Studio через Data Uploader (так же, как мы поступим с набором Пита).

Если хотите собирать аудио непосредственно в Studio, можно использовать любой смартфон, подключённый к Studio через интернет. Этот вариант мы здесь рассматривать не будем, но вы легко найдёте его описание в документации EI.

Офлайн-запись аудиоданных с XIAO ESP32S3 Sense

Встроенный микрофон — MSM261D3526H1CPM, MEMS-микрофон с PDM-выходом и несколькими режимами работы. Внутри он подключён к ESP32S3 через шину I2S: выводы IO42 (тактовый сигнал) и IO41 (данные).

Подключение микрофона к ESP32S3 через I2S

Что такое I2S?

I2S (Inter-IC Sound) — стандартный протокол для передачи цифрового аудио от одного устройства к другому. Его первоначально разработала Philips Semiconductor (ныне NXP Semiconductors). Он широко используется в аудиоустройствах, например в цифровых сигнальных процессорах, цифровых аудиопроцессорах и, всё чаще, в микроконтроллерах с поддержкой цифрового аудио (как в нашем случае).

Протокол I2S состоит как минимум из трёх линий:

Линии протокола I2S: тактовая, выбора слова и данных

1. Линия битового (или последовательного) тактового сигнала (BCLK или CLK): переключается, чтобы обозначить начало нового бита данных (вывод IO42).

2. Линия выбора слова (WS): переключается, чтобы обозначить начало нового слова (левого или правого канала). Частота сигнала выбора слова (WS) задаёт частоту дискретизации. В нашем случае вывод L/R на микрофоне подключён к земле, поэтому используется только левый канал (моно).

3. Линия данных (SD): по ней передаются аудиоданные (вывод IO41).

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

Начнём с того, как записывать сырые данные с микрофона. Перейдите в проект на GitHub и скачайте скетч XIAOEsp2s3_Mic_Test:

⚠️ Внимание

  • На XIAO ESP32S3 ОБЯЗАТЕЛЬНО должна быть включена PSRAM. Проверить это можно в верхнем меню Arduino IDE: Tools → PSRAM:OPI PSRAM;
  • Пакет поддержки плат (esp32 by Espressif Systems) должен быть версии 2.0.17. Пожалуйста, не обновляйте его.
/*
  Простой тест микрофона XIAO ESP32S3
*/

#include <I2S.h>

void setup() {
  Serial.begin(115200);
  while (!Serial) {
  }

  // Запуск I2S с частотой 16 кГц и 16 битами на отсчёт
  I2S.setAllPins(-1, 42, 41, -1, -1);
  if (!I2S.begin(PDM_MONO_MODE, 16000, 16)) {
    Serial.println("Failed to initialize I2S!");
    while (1); // ничего не делать
  }
}

void loop() {
  // Считываем отсчёт
  int sample = I2S.read();

  if (sample && sample != -1 && sample != 1) {
    Serial.println(sample);
  }
}

Этот код — простой тест микрофона XIAO ESP32S3 с интерфейсом I2S (Inter-IC Sound). Он настраивает I2S на захват аудио с частотой дискретизации 16 кГц и 16 битами на отсчёт, затем непрерывно читает отсчёты с микрофона и выводит их в последовательный порт.

Разберём основные части кода:

  • Подключение библиотеки I2S: она предоставляет функции для настройки и использования интерфейса I2S, который является стандартом для подключения цифровых аудиоустройств.
  • I2S.setAllPins(-1, 42, 41, -1, -1): настраивает выводы I2S. Параметры — (-1, 42, 41, -1, -1): второй параметр (42) — вывод тактового сигнала I2S (CLK), третий (41) — вывод данных I2S (DATA). Остальные параметры равны -1, то есть эти выводы не используются.
  • I2S.begin(PDM_MONO_MODE, 16000, 16): инициализирует I2S в режиме моно PDM (Pulse Density Modulation) с частотой дискретизации 16 кГц и 16 битами на отсчёт. Если инициализация не удалась, выводится сообщение об ошибке и программа останавливается.
  • int sample = I2S.read(): считывает отсчёт аудио из интерфейса I2S.

Если отсчёт корректен, он выводится в Serial Monitor и Plotter.

Ниже показан тест: «шёпот» двумя разными тонами.

График шёпота в Serial Plotter

Сохранение записанных звуковых образцов

Для записи аудиофайлов .wav на встроенный считыватель SD-карт нужно сначала включить PSRAM на XIAO.

В ESP32-S3 на чипе микроконтроллера всего несколько сотен килобайт внутренней оперативной памяти. Этого может не хватить для некоторых задач, поэтому ESP32-S3 может использовать до 16 МБ внешней PSRAM (псевдостатического ОЗУ), подключённой параллельно к флеш-чипу SPI. Внешняя память включена в адресное пространство процессора и при определённых ограничениях может использоваться так же, как внутренняя оперативная память данных.

Для начала вставьте SD-карту в XIAO, как показано на фото ниже (SD-карта должна быть отформатирована в FAT32).

Вставка SD-карты в XIAO

Включите функцию PSRAM микросхемы ESP-32 (Arduino IDE): Tools > PSRAM: "OPI PSRAM" > OPI PSRAM.

Включение OPI PSRAM в меню Tools

  • Скачайте скетч Wav_Record_dataset из репозитория проекта на GitHub.

Этот код записывает аудио через интерфейс I2S платы Seeed XIAO ESP32S3 Sense, сохраняет запись в виде файла .wav на SD-карту и позволяет управлять процессом записи командами, которые отправляются через Serial Monitor. Имя аудиофайла можно задать (оно должно совпадать с метками классов для обучения), и можно делать несколько записей, каждая из которых сохраняется в новый файл. Код также включает функцию увеличения громкости записей.

Разберём самые важные части кода:

#include <I2S.h>
#include "FS.h"
#include "SD.h"
#include "SPI.h"

Это необходимые библиотеки для программы. I2S.h позволяет вводить аудио, FS.h предоставляет возможности работы с файловой системой, SD.h позволяет программе работать с SD-картой, а SPI.h отвечает за связь SPI с SD-картой.

#define RECORD_TIME   10
#define SAMPLE_RATE 16000U
#define SAMPLE_BITS 16
#define WAV_HEADER_SIZE 44
#define VOLUME_GAIN 2

Здесь заданы различные константы программы.

  • RECORD_TIME задаёт длительность записи аудио в секундах.
  • SAMPLE_RATE и SAMPLE_BITS задают качество записи.
  • WAV_HEADER_SIZE задаёт размер заголовка файла .wav.
  • VOLUME_GAIN используется для увеличения громкости записи.
int fileNumber = 1;
String baseFileName;
bool isRecording = false;

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

void setup() {
  Serial.begin(115200);
  while (!Serial);

  I2S.setAllPins(-1, 42, 41, -1, -1);
  if (!I2S.begin(PDM_MONO_MODE, SAMPLE_RATE, SAMPLE_BITS)) {
    Serial.println("Failed to initialize I2S!");
    while (1);
  }

  if(!SD.begin(21)){
    Serial.println("Failed to mount SD Card!");
    while (1);
  }
  Serial.printf("Enter with the label name\n");
}

Функция setup инициализирует последовательный порт, интерфейс I2S для аудиовхода и интерфейс SD-карты. Если I2S не инициализировался или SD-карта не смонтировалась, выводится сообщение об ошибке и выполнение останавливается.

void loop() {
  if (Serial.available() > 0) {
    String command = Serial.readStringUntil('\n');
    command.trim();
    if (command == "rec") {
      isRecording = true;
    } else {
      baseFileName = command;
      fileNumber = 1; // сбрасываем номер файла при каждом
                        // задании нового базового имени
      Serial.printf("Send rec for starting recording label \n");
    }
  }
  if (isRecording && baseFileName != "") {
    String fileName = "/" + baseFileName + "."
                      + String(fileNumber) + ".wav";
    fileNumber++;
    record_wav(fileName);
    delay(1000); // задержка, чтобы не записать несколько файлов
                    // одновременно
    isRecording = false;
  }
}

В основном цикле программа ждёт команду из Serial Monitor. Если команда — rec, программа начинает запись. В противном случае команда считается базовым именем для файлов .wav. Если сейчас идёт запись и задано базовое имя, программа записывает аудио и сохраняет его в файл .wav. Имена файлов формируются добавлением номера файла к базовому имени.

void record_wav(String fileName)
{
  ...

  File file = SD.open(fileName.c_str(), FILE_WRITE);
  ...
  rec_buffer = (uint8_t *)ps_malloc(record_size);
  ...

  esp_i2s::i2s_read(esp_i2s::I2S_NUM_0,
                    rec_buffer,
                    record_size,
                    &sample_size,
                    portMAX_DELAY);
  ...
}

Эта функция записывает аудио и сохраняет его в файл .wav с заданным именем. Сначала инициализируются переменные sample_size и record_size. Значение record_size вычисляется по частоте дискретизации, разрядности и желаемой длительности записи. Рассмотрим важные части:

File file = SD.open(fileName.c_str(), FILE_WRITE);
// Записываем заголовок в файл WAV
uint8_t wav_header[WAV_HEADER_SIZE];
generate_wav_header(wav_header, record_size, SAMPLE_RATE);
file.write(wav_header, WAV_HEADER_SIZE);

Этот фрагмент открывает файл на SD-карте для записи, формирует заголовок .wav с помощью функции generate_wav_header и записывает его в файл.

// Выделение памяти в PSRAM для записи
rec_buffer = (uint8_t *)ps_malloc(record_size);
if (rec_buffer == NULL) {
  Serial.printf("malloc failed!\n");
  while(1) ;
}
Serial.printf("Buffer: %d bytes\n", ESP.getPsramSize()
               - ESP.getFreePsram());

Функция ps_malloc выделяет память в PSRAM для записи. Если выделение не удалось (rec_buffer равен NULL), выводится сообщение об ошибке и выполнение останавливается.

// Запуск записи
esp_i2s::i2s_read(esp_i2s::I2S_NUM_0,
         rec_buffer,
         record_size,
         &sample_size,
         portMAX_DELAY);
if (sample_size == 0) {
  Serial.printf("Record Failed!\n");
} else {
    Serial.printf("Record %d bytes\n", sample_size);
  }

Функция i2s_read считывает аудиоданные с микрофона в rec_buffer. Если данные не прочитаны (sample_size равен 0), выводится сообщение об ошибке.

// Увеличиваем громкость
for (uint32_t i = 0; i < sample_size; i += SAMPLE_BITS/8) {
  (*(uint16_t *)(rec_buffer+i)) <<= VOLUME_GAIN;
}

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

// Записываем данные в файл WAV
Serial.printf("Writing to the file ...\n");
if (file.write(rec_buffer, record_size) != record_size)
  Serial.printf("Write file Failed!\n");

free(rec_buffer);
file.close();
Serial.printf("Recording complete: \n");
Serial.printf("Send rec for a new sample or enter "
              "a new label\n\n");

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

void generate_wav_header(uint8_t *wav_header,
             uint32_t wav_size,
             uint32_t sample_rate)
{
  ...
  memcpy(wav_header, set_wav_header, sizeof(set_wav_header));
}

Функция generate_wav_header формирует заголовок файла .wav на основе параметров (wav_size и sample_rate). Она создаёт массив байтов по формату .wav: поля размера файла, аудиоформата, количества каналов, частоты дискретизации, битовой скорости, выравнивания блоков, разрядности и размера данных. Готовый заголовок копируется в массив wav_header, переданный в функцию.

Теперь загрузите код на XIAO и запишите образцы ключевых слов (yes и no). Также можно записать шум и другие слова.

Serial Monitor предложит получить метку, которую нужно записать:

Serial Monitor: запрос метки для записи

Отправьте метку (например, yes). Программа будет ждать следующую команду: rec.

Serial Monitor: ожидание команды rec

Программа будет записывать новые образцы каждый раз, когда получает команду rec. Файлы сохраняются как yes.1.wav, yes.2.wav, yes.3.wav и так далее, пока не будет отправлена новая метка (например, no). В этом случае для каждого нового образца нужно отправлять команду rec; файлы будут сохраняться как no.1.wav, no.2.wav, no.3.wav и так далее.

Сохранение файлов с номерами для каждой метки

В итоге на SD-карте появятся сохранённые файлы.

Файлы на SD-карте после записи

Файлы готовы к загрузке в Edge Impulse Studio.

Приложения для офлайн-записи аудиоданных

Аудиоданные можно записывать разными способами. Самый простой — использовать смартфон или ПК как подключённое устройство в Edge Impulse Studio.

ПК или смартфон должны записывать аудио с частотой дискретизации 16 кГц и разрядностью 16 бит.

Другой вариант — специализированные приложения. Хорошее приложение для этого — Voice Recorder Pro (iOS). Записи нужно сохранять как файлы .wav и отправлять на компьютер.

Приложение Voice Recorder Pro

Обучение модели в Edge Impulse Studio

Загрузка данных

Когда сырой набор данных определён и собран (набор Пита и записанные ключевые слова), создадим новый проект в Edge Impulse Studio:

Создание проекта в Edge Impulse Studio

После создания проекта выберите инструмент Upload Existing Data в разделе Data Acquisition. Выберите файлы для загрузки:

Выбор файлов для загрузки в Studio

Загрузите их в Studio (можно автоматически разделить данные на обучающую и тестовую выборки). Повторите для всех классов и всех сырых данных.

Загрузка классов в Studio

Образцы появятся в разделе Data acquisition.

Образцы в разделе Data acquisition

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

Нажмите «три точки» после названия образца и выберите Split sample.

Выбор Split sample в меню образца

Внутри инструмента разбейте данные на записи по 1 секунде. При необходимости добавьте или удалите сегменты:

Разбиение образца на сегменты по 1 секунде

Эту процедуру нужно повторить для всех образцов.

Примечание: для более длинных аудиофайлов (минуты) сначала разбейте их на сегменты по 10 секунд, а затем снова используйте инструмент, чтобы получить окончательные отрезки по 1 секунде.

Если при загрузке данные не разделялись автоматически на обучающую и тестовую выборки, это можно сделать вручную (через меню «три точки», перемещая образцы по одному) или с помощью Perform Train / Test Split на вкладке Dashboard в разделе Danger Zone.

При желании все наборы данных можно проверить на вкладке Data Explorer.

Создание Impulse (предобработка и определение модели)

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

Конструкция Impulse для KWS

Сначала мы возьмём точки данных с окном в 1 секунду, применим аугментацию и будем сдвигать окно каждые 500 мс. Обратите внимание, что включён параметр zero-pad data (дополнение нулями). Он важен: образцы короче 1 секунды дополняются нулями (в некоторых случаях я уменьшал окно 1000 мс в инструменте разбиения, чтобы избежать шумов и выбросов).

Каждый образец аудио длиной 1 секунда предобрабатывается и преобразуется в изображение (например, ). Мы будем использовать MFCC — метод, который извлекает признаки из аудиосигналов с помощью мел-частотных кепстральных коэффициентов, хорошо подходящих для человеческого голоса.

Блок MFCC в Impulse

Далее мы выбираем Keras для классификации и строим модель с нуля, используя классификацию изображений с помощью свёрточной нейронной сети (CNN).

Предобработка (MFCC)

Следующий шаг — создать изображения для обучения на следующем этапе:

Можно оставить значения параметров по умолчанию или воспользоваться опцией DSP Autotune parameters, что и сделаем.

Опция Autotune parameters для блока MFCC

Предобработка почти не расходует памяти (всего 16 КБ), однако расчётное время обработки довольно велико: 675 мс для Espressif ESP-EYE (ближайший доступный эталон) при тактовой частоте 240 МГц (как у нашего устройства), но с более слабым процессором (Xtensa LX6 вместо LX7 у ESP32S3). Реальное время инференса должно быть меньше.

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

Пока оставим параметры, заданные инструментом автонастройки. Сохраним параметры и сгенерируем признаки.

Сохранение параметров и генерация признаков

Если хотите подробнее изучить преобразование временных последовательных данных в изображения с помощью FFT, спектрограммы и т. п., можете поэкспериментировать с этим блокнотом Colab: Audio Raw Data Analysis.

Проектирование модели и обучение

Мы будем использовать свёрточную нейронную сеть (CNN). Базовая архитектура состоит из двух блоков Conv1D + MaxPooling (с 8 и 16 нейронами соответственно) и Dropout 0,25. На последнем слое, после Flatten, четыре нейрона — по одному на каждый класс:

Архитектура свёрточной сети Conv1D для KWS

Гиперпараметры: скорость обучения (Learning Rate) 0,005, модель обучается за 100 эпох. Также добавим аугментацию данных, в том числе добавление шума. Результат выглядит хорошо:

Результаты обучения модели KWS

Если хотите понять, что происходит «под капотом», можно скачать набор данных и запустить блокнот Jupyter, поэкспериментировать с кодом. Например, можно проанализировать точность по каждой эпохе:

Точность модели по эпохам

Этот блокнот Colab поможет узнать, как двигаться дальше: KWS Classifier Project — Looking «Under the hood».

Тестирование

Тестирование модели на данных, отложенных до обучения (тестовых данных), дало точность около 87 %.

Результаты тестирования модели KWS

Если посмотреть F1-меру, для YES получилось 0,95 — отличный результат, поскольку именно это слово мы будем использовать как «триггер» для постобработки (включения встроенного светодиода). Даже для NO получилось 0,90. Худший результат — у unknown, и это нормально.

Можно переходить к проекту, но перед развёртыванием на устройстве полезно выполнить классификацию в реальном времени с помощью смартфона. Перейдите в раздел Live Classification и нажмите Connect a Development board:

Подключение платы в Live Classification

Наведите телефон на QR-код и выберите ссылку.

Сканирование QR-кода телефоном

Телефон подключится к Studio. В приложении выберите опцию Classification и, когда она запустится, начните проверять свои ключевые слова, убедившись, что модель работает на реальных данных в реальном времени:

Классификация ключевых слов в приложении на телефоне

Развёртывание и инференс

Studio упакует все нужные библиотеки, функции предобработки и обученные модели и загрузит их на компьютер. Выберите опцию Arduino Library, затем внизу выберите Quantized (Int8) и нажмите Build.

Сборка Arduino Library с квантизацией Int8

Теперь пора провести настоящий тест. Мы будем делать инференс, полностью отключившись от Studio. Изменим один из примеров кода для ESP32, созданных при развёртывании Arduino Library.

В Arduino IDE откройте вкладку File/Examples, найдите свой проект и выберите esp32/esp32_microphone:

Выбор примера esp32_microphone в Arduino IDE

Этот код создан для встроенного микрофона ESP-EYE и его нужно адаптировать под наше устройство.

Начнём с замены библиотек для работы с шиной I2S:

Замена библиотек для работы с I2S

На:

#include <I2S.h>
#define SAMPLE_RATE 16000U
#define SAMPLE_BITS 16

Инициализируйте микрофон I2S в функции setup(), добавив строки:

void setup()
{
...
    I2S.setAllPins(-1, 42, 41, -1, -1);
    if (!I2S.begin(PDM_MONO_MODE, SAMPLE_RATE, SAMPLE_BITS)) {
      Serial.println("Failed to initialize I2S!");
    while (1) ;
...
}

В функции static void capture_samples(void* arg) замените строку 153, которая читает данные с микрофона I2S:

Строка чтения данных с микрофона в capture_samples

На:

/* Чтение данных сразу из i2s */
esp_i2s::i2s_read(esp_i2s::I2S_NUM_0,
                 (void*)sampleBuffer,
                 i2s_bytes_to_read,
                 &bytes_read, 100);

В функции static bool microphone_inference_start(uint32_t n_samples) закомментируйте или удалите строки 198–200, где вызывается функция инициализации микрофона. Она больше не нужна, поскольку микрофон I2S уже инициализирован в setup().

Строки инициализации микрофона в microphone_inference_start

Наконец, в функции static void microphone_inference_end(void) замените строку 243:

Строка освобождения памяти в microphone_inference_end

На:

static void microphone_inference_end(void)
{
    free(sampleBuffer);
    ei_free(inference.buffer);
}

Полный код есть в репозитории проекта на GitHub. Загрузите скетч на плату и проверьте реальный инференс:

⚠️ Внимание

  • На XIAO ESP32S3 ОБЯЗАТЕЛЬНО должна быть включена PSRAM. Проверить это можно в верхнем меню Arduino IDE: Tools → PSRAM:OPI PSRAM;
  • Пакет поддержки плат (esp32 by Espressif Systems) должен быть версии 2.0.17. Пожалуйста, не обновляйте его.

Результаты инференса KWS в Serial Monitor

Постобработка

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

Рассмотрим два подхода к постобработке: использование встроенного светодиода XIAO и OLED-дисплея в XIAOML Kit.

Светодиод

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

Нужно инициализировать светодиод:

#define LED_BUILT_IN 21
...
void setup()
{
...
  pinMode(LED_BUILT_IN, OUTPUT); // Настраиваем вывод на выход
  digitalWrite(LED_BUILT_IN, HIGH); // Выключить
...
}

Затем замените фрагмент // print the predictions в предыдущем коде (в функции loop()):

int pred_index = 0;     // Инициализация pred_index
float pred_value = 0;   // Инициализация pred_value

// вывод предсказаний
ei_printf("Predictions ");
ei_printf("(DSP: %d ms., Classification: %d ms., Anomaly: %d ms.)",
     result.timing.dsp, result.timing.classification,
     result.timing.anomaly);
ei_printf(": \n");
for (size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++) {
      ei_printf("    %s: ", result.classification[ix].label);
      ei_printf_float(result.classification[ix].value);
      ei_printf("\n");

      if (result.classification[ix].value > pred_value){
         pred_index = ix;
         pred_value = result.classification[ix].value;
      }
}

// показываем результат инференса светодиодом
if (pred_index == 3){
    digitalWrite(LED_BUILT_IN, LOW); // Включить
}
else{
   digitalWrite(LED_BUILT_IN, HIGH); // Выключить
}

Полный код есть в репозитории проекта на GitHub. Загрузите скетч на плату и проверьте реальный инференс:

Результаты работы светодиода при распознавании YES

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

OLED-дисплей

Маленький 0,42-дюймовый OLED-дисплей XIAOML Kit ( пикселей) — важный компонент постобработки: он превращает сырые результаты инференса в немедленную, понятную человеку обратную связь, показывая названия классов и уровни уверенности прямо на устройстве. Это исключает необходимость во внешних мониторах и позволяет создавать по-настоящему автономные периферийные ИИ-системы в промышленности, сельском хозяйстве или розничной торговле, где мгновенное визуальное подтверждение предсказаний ИИ особенно важно.

Теперь изменим скетч так, чтобы он автоматически адаптировался к модели, обученной в Edge Impulse: он будет читать названия классов и их количество прямо из модели. Скачайте код с GitHub: xiaoml-kit_kws_oled.

Запустим код и посмотрим результат:

Результат работы KWS на OLED-дисплее XIAOML Kit

Итоги

Эта лабораторная работа продемонстрировала полный цикл разработки системы распознавания ключевых слов на XIAOML Kit и показала, как современные платформы TinyML делают сложный аудио-ИИ доступным на устройствах с ограниченными ресурсами. В ходе практической реализации мы соединили теоретические концепции машинного обучения с практическим развёртыванием встраиваемого ИИ.

Технические достижения. Проект реализует полный конвейер обработки аудио: от захвата сырого звука до инференса в реальном времени. Используя встроенный цифровой микрофон XIAO ESP32S3, мы записали аудио профессионального качества (16 кГц/16 бит) и обработали его с помощью мел-частотных кепстральных коэффициентов (MFCC) для извлечения признаков. Развёрнутая CNN-модель достигла высокой точности в различении целевых ключевых слов («YES», «NO») и фоновых условий («NOISE», «UNKNOWN»), а время инференса подходит для приложений реального времени.

Интеграция с платформой. Edge Impulse Studio оказалась незаменимой комплексной MLOps-платформой для встраиваемых систем: она охватывает всё — от сбора и разметки данных до обучения, оптимизации и развёртывания моделей. Бесшовная интеграция облачного обучения и развёртывания на периферии характерна для современных рабочих процессов TinyML, а Arduino IDE обеспечила гибкость, нужную для собственной постобработки.

Практические применения. Освоенные приёмы выходят далеко за рамки простого распознавания ключевых слов. Системы голосового управления, промышленный мониторинг безопасности с классификацией звуков, медицинские приложения для анализа дыхания и экологический мониторинг по звукам животных или оборудования используют похожие подходы к обработке аудио. Каскадная архитектура обнаружения, показанная здесь — когда KWS на периферии запускает более сложную обработку в облаке, — лежит в основе современных систем голосовых помощников.

Принципы встраиваемого ИИ. Проект подчеркнул важные соображения TinyML: управление энергопотреблением, оптимизацию памяти за счёт использования PSRAM и компромиссы между сложностью модели и скоростью инференса. Успешное развёртывание нейронной сети, выполняющей анализ аудио в реальном времени на микроконтроллере, показывает, как возможности ИИ, раньше требовавшие мощных настольных компьютеров, теперь работают на устройствах с батарейным питанием.

Методология разработки. Мы рассмотрели несколько путей разработки: от стратегий сбора данных (офлайн-запись на SD-карту или онлайн-потоковая передача) до вариантов развёртывания (автоматическая генерация библиотеки в Edge Impulse или собственная реализация на Arduino). Такая гибкость важна для адаптации к разным требованиям и ограничениям проектов.

Дальнейшее развитие. Основа, заложенная здесь, открывает путь к более продвинутым приложениям аудио-ИИ. Распознавание нескольких ключевых слов, идентификация говорящего, определение эмоций по голосу и классификация звуков окружающей среды опираются на те же базовые методы. Возможности интеграции, продемонстрированные с OLED-дисплеем и управлением GPIO, показывают, как KWS может служить интеллектуальным интерфейсом для более широких IoT-систем.

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

  • Безопасность: обнаружение битого стекла, мониторинг вторжений, распознавание выстрелов;
  • Промышленный IoT: мониторинг состояния оборудования, обнаружение аномалий в производственном оборудовании;
  • Здравоохранение: мониторинг нарушений сна, оценка состояния дыхательной системы, системы ухода за пожилыми людьми;
  • Экологический мониторинг: отслеживание дикой природы, анализ городского шума, акустическое управление «умными» зданиями;
  • Интеграция с умным домом: голосовое управление в нескольких комнатах, контроль состояния бытовой техники по звуковым признакам.

Ключевые выводы. XIAOML Kit доказывает, что разработка ИИ профессионального уровня достижима с помощью доступных инструментов и при скромном бюджете. Сочетание мощного оборудования (ESP32S3 с PSRAM и встроенными датчиками), зрелых платформ разработки (Edge Impulse Studio) и полноценных программных библиотек создаёт среду, в которой сложные концепции ИИ превращаются в осязаемые работающие системы.

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

Ресурсы

Классификация движения и обнаружение аномалий

Мультяшная иллюстрация в стиле 1950-х: учёные в винтажной аудиолаборатории изучают графики БПФ и временные кривые на больших досках
Промпт DALL·E: мультяшная иллюстрация в стиле 1950-х в винтажной аудиолаборатории. Учёные в классических белых халатах внимательно анализируют аудиоданные на больших досках. На досках — графики БПФ (быстрого преобразования Фурье) и кривые во временной области. Вокруг разбросано старинное аудиооборудование, но графики данных ясные и подробные, что подчёркивает внимание к анализу звука.

Обзор

Транспорт — основа мировой торговли. Миллионы контейнеров ежедневно перевозятся морем, автомобилями и поездами в разные уголки мира. Безопасная и эффективная перевозка контейнеров — огромная задача, которая требует современных технологий, и TinyML, без сомнения, — одно из ключевых решений.

В этой практической работе мы будем решать реальные задачи транспортной логистики. Мы разработаем систему классификации движения и обнаружения аномалий на основе XIAOML Kit, Arduino IDE и Edge Impulse Studio. Проект поможет понять, какие силы и движения испытывает контейнер на разных этапах перевозки: при наземной и морской транспортировке, при вертикальном перемещении вилочным погрузчиком и при хранении на складе.

Цели обучения

  • настройка XIAOML Kit;
  • сбор и предобработка данных;
  • создание модели классификации движения;
  • реализация обнаружения аномалий;
  • тестирование и анализ в реальных условиях.

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

Установка IMU

В комплект XIAOML Kit на плате расширения уже встроен 6-осевой датчик IMU LSM6DS3TR-C, поэтому внешние датчики подключать не нужно. Такое встроенное решение даёт чистую и надёжную платформу для приложений машинного обучения на основе движения.

LSM6DS3TR-C объединяет в одном корпусе 3-осевой акселерометр и 3-осевой гироскоп. Он подключён по шине I2C к XIAO ESP32S3 по адресу 0x6A и обеспечивает:

  • диапазоны акселерометра: ±2/±4/±8/±16 g (по умолчанию используем ±2 g);
  • диапазоны гироскопа: ±125/±250/±500/±1000/±2000 dps (по умолчанию используем ±250 dps);
  • разрешение: 16-битный АЦП;
  • интерфейс связи: I2C, адрес 0x6A;
  • энергопотребление: сверхнизкое.

Направления осей акселерометра на плате расширения

Система координат. Датчик работает в правосторонней системе координат. Если смотреть на плату расширения снизу (там, где виден датчик IMU с меткой-точкой):

  • ось X направлена вправо;
  • ось Y направлена вперёд (от вас);
  • ось Z направлена вверх (из платы).

Настройка аппаратной части

Поскольку XIAOML Kit поставляется собранным с платой расширения, дополнительных аппаратных соединений не требуется. Датчик LSM6DS3TR-C уже правильно подключён по I2C.

Что уже подключено:

  • IMU LSM6DS3TR-C → I2C (SDA/SCL) → XIAO ESP32S3;
  • адрес I2C: 0x6A;
  • питание: 3,3 В от XIAO ESP32S3.

Необходимая библиотека. Библиотека должна была быть установлена во время настройки. Если это не так, установите библиотеку Seeed Arduino LSM6DS3, выполнив такие шаги:

  1. Откройте Library Manager в Arduino IDE.
  2. Найдите «LSM6DS3».
  3. Установите «Seeed Arduino LSM6DS3» от Seeed Studio.
  4. Важно: НЕ устанавливайте «Arduino_LSM6DS3 by Arduino» — она предназначена для других плат!

Проверка датчика IMU

Начнём с простого теста, чтобы убедиться, что IMU работает правильно. Загрузите этот код для проверки датчика:

#include <LSM6DS3.h>
#include <Wire.h>

// Создаём объект IMU через интерфейс I2C
LSM6DS3 myIMU(I2C_MODE, 0x6A);

float accelX, accelY, accelZ;
float gyroX, gyroY, gyroZ;

void setup() {
  Serial.begin(115200);
  while (!Serial) delay(10);

  Serial.println("Тест IMU XIAOML Kit");
  Serial.println("6-осевой IMU LSM6DS3TR-C");
  Serial.println("====================");

  // Инициализируем IMU
  if (myIMU.begin() != 0) {
      Serial.println("ОШИБКА: не удалось инициализировать IMU!");
      while(1) delay(1000);
  } else {
      Serial.println("✓ IMU успешно инициализирован");
      Serial.println("Формат данных: AccelX,AccelY,AccelZ,"
                    "GyroX,GyroY,GyroZ");
      Serial.println("Единицы: g (ускорение свободного падения), градусы/с");
      Serial.println();
  }
}

void loop() {
  // Читаем данные акселерометра (в g)
  accelX = myIMU.readFloatAccelX();
  accelY = myIMU.readFloatAccelY();
  accelZ = myIMU.readFloatAccelZ();

  // Читаем данные гироскопа (в градусах в секунду)
  gyroX = myIMU.readFloatGyroX();
  gyroY = myIMU.readFloatGyroY();
  gyroZ = myIMU.readFloatGyroZ();

  // Выводим в удобном для чтения формате
  Serial.print("Акселерометр (g): X="); Serial.print(accelX, 3);
  Serial.print(" Y="); Serial.print(accelY, 3);
  Serial.print(" Z="); Serial.print(accelZ, 3);
  Serial.print(" | Гироскоп (°/с): X="); Serial.print(gyroX, 2);
  Serial.print(" Y="); Serial.print(gyroY, 2);
  Serial.print(" Z="); Serial.println(gyroZ, 2);

  delay(100); // частота обновления 10 Гц
}

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

  • ускорение по оси Z около +1,0 g (гравитация);
  • ускорение по осям X и Y близко к 0,0 g;
  • все значения гироскопа близки к 0,0 °/с.

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

Проект классификации движения TinyML

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

Четыре сценария перевозки: морской, наземный, подъём и простой

С помощью акселерометра XIAOML Kit мы будем записывать данные о движении, вручную имитируя условия:

  • морская перевозка (паллеты на лодках) — движение по всем осям с волнообразными колебаниями;
  • наземная перевозка (паллеты на грузовиках или поездах) — преимущественно горизонтальное движение;
  • подъём (паллеты, которые перемещает вилочный погрузчик) — преимущественно вертикальное движение;
  • простой (паллеты на складе) — минимальное движение.

По изображению выше мы определим для нашей симуляции: преимущественно горизонтальные движения (по осям или ) соответствуют классу «Наземная перевозка», вертикальные движения (ось ) — классу «Подъём», отсутствие активности — классу «Простой», а движение по всем трём осям — классу «Морская перевозка».

Определение классов движения по осям

Сбор данных

Для сбора данных есть несколько вариантов. В реальном сценарии устройство можно подключить непосредственно к контейнеру, а собранные данные сохранять в файл (например, CSV) на SD-карте. Данные можно также передавать удалённо на ближайший накопитель, например на телефон, по Wi-Fi или Bluetooth (как в проекте Sensor DataLogger). Когда набор данных собран и сохранён как файл .CSV, его можно загрузить в Studio с помощью инструмента CSV Wizard.

В этом видео вы узнаете о других способах отправки данных в Edge Impulse Studio.

Подготовка кода для сбора данных

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

Для сбора данных сначала подключите комплект к Edge Impulse Studio, которая будет также использоваться для предобработки данных, обучения модели, тестирования и развёртывания.

Следуйте инструкциям, чтобы установить Node.js и Edge Impulse CLI на ваш компьютер.

Поскольку XIAOML Kit не является полностью поддерживаемой отладочной платой Edge Impulse, для захвата данных с датчика и отправки их в Studio используем CLI Data Forwarder, как показано на схеме:

Схема передачи данных через CLI Data Forwarder в Edge Impulse

Изменим тестовый код так, чтобы он выводил данные в формате, подходящем для Edge Impulse:

#include <LSM6DS3.h>
#include <Wire.h>

#define FREQUENCY_HZ        50
#define INTERVAL_MS         (1000 / (FREQUENCY_HZ + 1))

LSM6DS3 myIMU(I2C_MODE, 0x6A);
static unsigned long last_interval_ms = 0;

void setup() {
  Serial.begin(115200);
  while (!Serial) delay(10);

  Serial.println("XIAOML Kit — сбор данных о движении");
  Serial.println("Датчик IMU LSM6DS3TR-C");

  // Инициализируем IMU
  if (myIMU.begin() != 0) {
      Serial.println("ОШИБКА: не удалось инициализировать IMU!");
      while(1) delay(1000);
  }
  delay(2000);
  Serial.println("Начало сбора данных через 3 секунды...");
  delay(3000);
}

void loop() {
  if (millis() > last_interval_ms + INTERVAL_MS) {
      last_interval_ms = millis();

      // Читаем данные акселерометра
      float ax = myIMU.readFloatAccelX();
      float ay = myIMU.readFloatAccelY();
      float az = myIMU.readFloatAccelZ();

      // Переводим в м/с² (умножаем на 9,81)
      float ax_ms2 = ax * 9.81;
      float ay_ms2 = ay * 9.81;
      float az_ms2 = az * 9.81;

      // Выводим в формате Edge Impulse
      Serial.print(ax_ms2);
      Serial.print("\t");
      Serial.print(ay_ms2);
      Serial.print("\t");
      Serial.println(az_ms2);
  }
}

Загрузите код в Arduino IDE. В Serial Monitor должны появиться значения акселерометра (переведённые в м/с²):

Значения акселерометра в Serial Monitor в м/с²

Оставьте код запущенным, но выключите Serial Monitor. Данные с комплекта будут отправлены в Edge Impulse Studio по последовательному соединению.

Подключение к Edge Impulse для сбора данных

Создайте проект Edge Impulse

  • Перейдите в Edge Impulse Studio и создайте новый проект.
  • Выберите понятное название (не длиннее 63 символов для совместимости с библиотеками Arduino).

Создание нового проекта в Edge Impulse Studio

Настройте CLI Data Forwarder

  • Установите Edge Impulse CLI на компьютер.
  • Убедитесь, что XIAOML Kit подключён к компьютеру, код запущен, а Serial Monitor выключен, иначе может возникнуть ошибка.
  • В терминале компьютера выполните: edge-impulse-data-forwarder --clean.
  • Введите учётные данные Edge Impulse.
  • Выберите проект и настройте параметры устройства.

Терминал: запуск edge-impulse-data-forwarder

  • Откройте проект в Edge Impulse Studio. В разделе Device можно проверить, правильно ли подключён комплект (точка должна быть зелёной).

Раздел Device в Edge Impulse Studio: подключённое устройство

Сбор данных в Studio

Как обсуждалось ранее, нужно собрать данные для всех четырёх классов перевозки. Представьте, что у вас есть контейнер со встроенным акселерометром (в нашем случае это XIAOML Kit). Теперь представьте, что контейнер стоит на корабле в бушующем океане:

Контейнер с датчиком на корабле в океане

Или в грузовике, который едет по дороге, или его перемещает вилочный погрузчик и т. д.

Имитация движения

Класс «Морская перевозка»:

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

Класс «Наземная перевозка»:

  • двигайте комплект горизонтально по прямым линиям (слева направо и обратно);
  • имитируйте вибрации грузовика или поезда небольшими горизонтальными встряхиваниями;
  • иногда делайте плавные толчки и повороты.

Класс «Подъём»:

  • двигайте комплект преимущественно по вертикали (вверх и вниз);
  • имитируйте работу погрузчика: подъём, пауза, опускание;
  • добавьте несколько коротких горизонтальных перемещений для позиционирования.

Класс «Простой»:

  • поставьте комплект на устойчивую поверхность;
  • минимальное движение или его отсутствие;
  • фиксируйте окружающие вибрации и шум датчика.

Получение данных

В разделе Data Acquisition вы должны увидеть, что ваша плата [xiaoml-kit] подключена. Датчик доступен: [sensor with 3 axes (accX, accY, accZ)], с частотой дискретизации [50 Hz]. Studio предлагает длину образца [10000] мс (10 с). Осталось задать метку образца. Начнём, например, с [terrestrial].

Нажмите [Start Sample] и двигайте комплект горизонтально (слева направо), сохраняя одно направление. Через 10 секунд данные загрузятся в Studio.

Ниже показан один образец (сырые данные) длиной 10 секунд. Видно, что волнообразное движение происходит преимущественно по оси Y (слева направо). Остальные оси почти неподвижны: ось X колеблется около нуля, а ось Z — около 9,8 м/с² из-за гравитации.

Сырые данные класса terrestrial: волнообразное движение по оси Y

Для каждого из четырёх классов соберите, например, около 2 минут данных (десять–двенадцать образцов по 10 секунд). С помощью значка 3 dots после каждого образца выберите два и перенесите их в Test set (тестовую выборку). Либо используйте автоматический инструмент Train/Test Split в разделе Danger Zone вкладки Dashboard. Ниже видны наборы данных:

Наборы данных для обучения и тестирования в Studio

Предобработка данных

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

Скользящее окно по данным акселерометра

видно 10 секунд данных акселерометра, записанных с частотой дискретизации (SR) 50 Гц. Окно длиной 2 секунды захватывает 300 точек данных (3 оси × 2 секунды × 50 отсчётов). Мы будем сдвигать это окно каждые 200 мс, создавая набор большего размера, где каждый экземпляр содержит 300 сырых признаков.

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

Предобработка данных — непростая задача для встраиваемого машинного обучения. Тем не менее Edge Impulse помогает справиться с ней благодаря шагу цифровой обработки сигналов (DSP), а точнее — спектральным признакам.

В Studio этот набор данных поступит на вход блока спектрального анализа (Spectral Analysis), который отлично подходит для анализа повторяющихся движений, например данных акселерометров. Блок выполняет DSP (цифровую обработку сигналов) и извлекает такие признаки, как «FFT» или «вейвлеты». В самом распространённом случае, при FFT, статистические признаки временной области для каждой оси/канала таковы:

  • RMS;
  • асимметрия (Skewness);
  • эксцесс (Kurtosis).

Статистические признаки временной области в Studio

А спектральные признаки частотной области для каждой оси/канала:

  • спектральная мощность (Spectral Power);
  • асимметрия (Skewness);
  • эксцесс (Kurtosis).

Спектральные признаки частотной области в Studio

Например, при длине FFT 32 точки на выходе блока спектрального анализа будет 21 признак на каждую ось (всего 63 признака).

Эти 63 признака станут входным тензором для нейросетевого классификатора и для модели обнаружения аномалий (K-Means).

Подробнее можно узнать из лабораторной работы Спектральные признаки DSP.

Проектирование модели

Наш классификатор будет полносвязной нейронной сетью (Dense Neural Network, DNN). У неё будет 63 нейрона на входном слое, два скрытых слоя с 20 и 10 нейронами и выходной слой с четырьмя нейронами (по одному на каждый класс), как показано здесь:

Архитектура полносвязной нейронной сети классификатора

Проектирование Impulse

Impulse принимает сырые данные, с помощью обработки сигналов извлекает признаки, а затем использует блок обучения (Dense model), чтобы классифицировать новые данные.

Мы также используем вторую модель — K-means, которая применяется для обнаружения аномалий. Представим, что известные классы — это кластеры. Любой образец, который не попадает ни в один из этих кластеров, может оказаться выбросом, аномалией (например, контейнер, сорвавшийся с корабля в океан, или лежащий вверх дном на полу).

Схема обнаружения аномалий: образцы вне известных кластеров

Представьте, что наш XIAOML Kit катится или движется вверх дном, с движением, которого не было в обучающей выборке.

Две модели в Impulse: классификатор и обнаружение аномалий

Итоговый дизайн Impulse:

Итоговый дизайн Impulse в Edge Impulse

Генерация признаков

На этом этапе проекта мы определили метод предобработки, и модель спроектирована. Теперь пора выполнить работу. Сначала преобразуем сырые данные (временной ряд) в табличные. Перейдите на вкладку Spectral Features и нажмите [Save Parameters]. Либо вместо значений по умолчанию нажмите кнопку [Autotune parameters]. В этом случае Studio сама подберёт гиперпараметры, например конструкцию фильтра и длину FFT, на основе сырых данных.

Настройка параметров спектральных признаков в Studio

В верхнем меню выберите вкладку Generate features, отметьте опции Calculate feature importance и Normalize features и нажмите [Generate features]. Каждое двухсекундное окно данных (300 точек) будет преобразовано в одну табличную точку с 63 признаками.

Feature Explorer отобразит эти данные в 2D с помощью UMAP. Uniform Manifold Approximation and Projection (UMAP) — метод снижения размерности, который используется для визуализации, похоже на t-SNE, но также и для общего нелинейного снижения размерности.

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

Визуализация признаков классов в Feature Explorer

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

Обучение

Наш классификатор — полносвязная нейронная сеть (DNN) со 63 нейронами на входном слое, двумя скрытыми слоями с 20 и 10 нейронами и выходным слоем из четырёх нейронов (по одному на класс), как показано здесь:

Архитектура классификатора для обучения

В качестве гиперпараметров используем скорость обучения (Learning Rate) 0,005 и 20 % данных для валидации, 30 эпох. После обучения точность составляет 100 %.

Результаты обучения классификатора в Studio

Для обнаружения аномалий выберем предложенные признаки, которые как раз и являются наиболее важными в извлечении признаков. Количество кластеров будет 32, как предлагает Studio. После обучения можно выбрать часть данных для проверки, например данные морской перевозки. Полученный показатель аномалий: min: -0.1642, max: 0.0738, avg: -0.0867.

Если менять данные, можно заметить, что малые или отрицательные значения показателя аномалий означают, что данные нормальные.

Результаты обучения модели обнаружения аномалий

Тестирование

С помощью 20 % данных, оставшихся после этапа сбора, можно проверить, как модель поведёт себя на неизвестных данных. Ожидается 100 %, но результат оказался очень хорошим — 8 % ошибок.

Результаты тестирования модели в Studio

Также используйте свой комплект (он всё ещё подключён к Studio) и проведите живую классификацию. Например, проверим движение «наземной перевозки»:

Живая классификация движения в Studio

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

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

Пришло время для волшебства! Studio упакует все нужные библиотеки, функции предобработки и обученные модели и загрузит их на ваш компьютер. Выберите вариант Arduino Library, затем внизу выберите Quantized (Int8) и нажмите [Build]. ZIP-файл будет создан и загружен на компьютер.

Настройки развёртывания библиотеки Arduino в Studio

В Arduino IDE откройте вкладку Sketch, выберите пункт Add .ZIP Library и укажите ZIP-файл, который скачала Studio:

Добавление ZIP-библиотеки в Arduino IDE

Инференс

Теперь пора провести настоящий тест. Мы будем делать инференс полностью независимо от Studio. Изменим один из примеров кода, который создала Studio при развёртывании библиотеки Arduino.

В Arduino IDE откройте вкладку File/Examples, найдите свой проект и в примерах выберите nano_ble_sense_accelerometer:

Конечно, это не ваша плата, но код можно адаптировать, внеся всего несколько изменений.

Например, в начале кода подключается библиотека для IMU Arduino Sense:

/* Подключение библиотек -------------------------------- */
#include <XIAOML_Kit_Motion_Class_-_AD_inferencing.h>
#include <Arduino_LSM9DS1.h>

Замените блок подключений на код для IMU:

#include <XIAOML_Kit_Motion_Class_-_AD_inferencing.h>
#include <LSM6DS3.h>
#include <Wire.h>

Измените константы:

// Настройка IMU
LSM6DS3 myIMU(I2C_MODE, 0x6A);

// Настройки инференса
#define CONVERT_G_TO_MS2    9.81f
#define MAX_ACCEPTED_RANGE  2.0f * CONVERT_G_TO_MS2

В функции setup инициализируйте IMU:

   // Инициализация IMU
    if (myIMU.begin() != 0) {
        Serial.println("ОШИБКА: не удалось инициализировать IMU!");
        return;
    }

В функции loop буферы buffer[ix], buffer[ix + 1] и buffer[ix + 2] получают данные трёхосного акселерометра. В исходном коде есть строка:

IMU.readAcceleration(buffer[ix], buffer[ix + 1], buffer[ix + 2]);

Замените её на такой блок кода:

// Читаем данные IMU
float x = myIMU.readFloatAccelX();
float y = myIMU.readFloatAccelY();
float z = myIMU.readFloatAccelZ();

Два блока кода нужно поменять местами. Сначала выполняется перевод в «метры на секунду в квадрате» (м/с²), затем проверка максимально допустимого диапазона (здесь он задан в м/с², а в примере для Arduino — в g):

  // Переводим в м/с²
  buffer[i + 0] = x * CONVERT_G_TO_MS2;
  buffer[i + 1] = y * CONVERT_G_TO_MS2;
  buffer[i + 2] = z * CONVERT_G_TO_MS2;

  // Ограничиваем диапазон
  for (int j = 0; j < 3; j++) {
      if (fabs(buffer[i + j]) > MAX_ACCEPTED_RANGE) {
          buffer[i + j] = copysign(MAX_ACCEPTED_RANGE, buffer[i + j]);
      }
  }

На этом изменения заканчиваются. Вывод инференса в Serial Monitor тоже можно настроить. Теперь загрузите полный код ниже на плату и переходите к инференсу.

// Классификация движения с датчиком LSM6DS3TR-C
#include <XIAOML_Kit_Motion_Class_-_AD_inferencing.h>
#include <LSM6DS3.h>
#include <Wire.h>

// Настройка IMU
LSM6DS3 myIMU(I2C_MODE, 0x6A);

// Настройки инференса
#define CONVERT_G_TO_MS2    9.81f
#define MAX_ACCEPTED_RANGE  2.0f * CONVERT_G_TO_MS2

static bool debug_nn = false;
static float buffer[EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE] = { 0 };
static float inference_buffer[EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE];

void setup() {
    Serial.begin(115200);
    while (!Serial) delay(10);

    Serial.println("XIAOML Kit — классификация движения");
    Serial.println("Инференс с датчиком LSM6DS3TR-C");

    // Инициализация IMU
    if (myIMU.begin() != 0) {
        Serial.println("ОШИБКА: не удалось инициализировать IMU!");
        return;
    }

    Serial.println("✓ IMU инициализирован");

    if (EI_CLASSIFIER_RAW_SAMPLES_PER_FRAME != 3) {
        Serial.println("ОШИБКА: EI_CLASSIFIER_RAW_SAMPLES_PER_FRAME "
                       "должен быть равен 3");
        return;
    }

    Serial.println("✓ Модель загружена");
    Serial.println("Запуск классификации движения...");
}

void loop() {
    ei_printf("\nЗапуск инференса через 2 секунды...\n");
    delay(2000);

    ei_printf("Сбор данных...\n");

    // Очищаем буфер
    for (size_t i = 0; i < EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE; i++) {
        buffer[i] = 0.0f;
    }

    // Собираем данные акселерометра
    for (int i = 0; i < EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE; i += 3) {
        uint64_t next_tick = micros() +
          (EI_CLASSIFIER_INTERVAL_MS * 1000);

        // Читаем данные IMU
        float x = myIMU.readFloatAccelX();
        float y = myIMU.readFloatAccelY();
        float z = myIMU.readFloatAccelZ();

        // Переводим в м/с²
        buffer[i + 0] = x * CONVERT_G_TO_MS2;
        buffer[i + 1] = y * CONVERT_G_TO_MS2;
        buffer[i + 2] = z * CONVERT_G_TO_MS2;

        // Ограничиваем диапазон
        for (int j = 0; j < 3; j++) {
            if (fabs(buffer[i + j]) > MAX_ACCEPTED_RANGE) {
                buffer[i + j] = copysign(MAX_ACCEPTED_RANGE,
                                         buffer[i + j]);
            }
        }

        delayMicroseconds(next_tick - micros());
    }

    // Копируем во входной буфер инференса
    for (int i = 0; i < EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE; i++) {
        inference_buffer[i] = buffer[i];
    }

    // Создаём сигнал из буфера
    signal_t signal;
    int err = numpy::signal_from_buffer(inference_buffer,
              EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE, &signal);
    if (err != 0) {
        ei_printf("ОШИБКА: не удалось создать сигнал из буфера (%d)\n",
                  err);
        return;
    }

    // Запускаем классификатор
    ei_impulse_result_t result = { 0 };
    err = run_classifier(&signal, &result, debug_nn);
    if (err != EI_IMPULSE_OK) {
        ei_printf("ОШИБКА: не удалось запустить классификатор (%d)\n", err);
        return;
    }

    // Выводим предсказания
    ei_printf("Предсказания (DSP: %d мс, классификация: %d мс, "
              "аномалии: %d мс):\n",
        result.timing.dsp, result.timing.classification, result.timing.anomaly);

    for (size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++) {
        ei_printf("    %s: %.5f\n", result.classification[ix].label,
                  result.classification[ix].value);
    }

    // Выводим показатель аномалии
#if EI_CLASSIFIER_HAS_ANOMALY == 1
    ei_printf("Показатель аномалии: %.3f\n", result.anomaly);
#endif

    // Определяем предсказание
    float max_confidence = 0.0;
    String predicted_class = "unknown";

    for (size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++) {
        if (result.classification[ix].value > max_confidence) {
            max_confidence = result.classification[ix].value;
            predicted_class = String(result.classification[ix].label);
        }
    }

    // Выводим результат с порогом уверенности
    if (max_confidence > 0.6) {
        ei_printf("\n🎯 ПРЕДСКАЗАНИЕ: %s (уверенность %.1f%%)\n",
                 predicted_class.c_str(), max_confidence * 100);
    } else {
        ei_printf("\n❓ НЕОПРЕДЕЛЁННО: наибольшая уверенность у %s (%.1f%%)\n",
                 predicted_class.c_str(), max_confidence * 100);
    }

    // Проверяем аномалию
#if EI_CLASSIFIER_HAS_ANOMALY == 1
    if (result.anomaly > 0.5) {
        ei_printf("⚠️ ОБНАРУЖЕНА АНОМАЛИЯ! Показатель: %.3f\n", result.anomaly);
    }
#endif

    delay(1000);
}

void ei_printf(const char *format, ...) {
    static char print_buf[1024] = { 0 };
    va_list args;
    va_start(args, format);
    int r = vsnprintf(print_buf, sizeof(print_buf), format, args);
    va_end(args);
    if (r > 0) {
        Serial.write(print_buf);
    }
}

Полный код доступен на странице лабораторной работы на GitHub.

Теперь попробуйте свои движения и посмотрите результат инференса для каждого класса на изображениях:

Результат инференса: класс «Простой»

Результат инференса: класс «Наземная перевозка»

Результат инференса: класс «Подъём»

Результат инференса: класс «Морская перевозка»

И, конечно, попробуйте «аномалию», например переверните XIAO вверх дном. Показатель аномалии превысит 0,5:

Результат инференса: обнаружена аномалия при перевёрнутом XIAO

Постобработка

Теперь, когда мы убедились, что модель работает, предлагаем изменить код так, чтобы результат выводился полностью офлайн: без подключения к ПК и с питанием от аккумулятора, пауэрбанка или отдельного источника на 5 В.

Идея в том, что при обнаружении определённого движения на OLED-дисплее появляется соответствующее сообщение.

Вывод результата инференса на OLED-дисплей

Изменённый код инференса с выводом на OLED-дисплей доступен на странице лабораторной работы на GitHub.

Итоги

В этой работе мы показали, как построить полную систему классификации движения на основе встроенного 6-осевого датчика IMU LSM6DS3TR-C из XIAOML Kit. Основные достижения:

Техническая реализация:

  • использован встроенный 6-осевой IMU для регистрации движения;
  • собраны размеченные обучающие данные для четырёх сценариев транспортировки;
  • реализовано извлечение спектральных признаков для анализа временных рядов;
  • развёрнут нейросетевой классификатор, оптимизированный для инференса на микроконтроллере;
  • добавлено обнаружение аномалий для выявления необычных движений.

Конвейер машинного обучения:

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

Практические применения: изученные методы напрямую применяются в реальных сценариях, включая:

  • отслеживание активов и мониторинг логистики;
  • предиктивное обслуживание оборудования;
  • распознавание человеческой активности;
  • мониторинг транспортных средств и оборудования;
  • сети IoT-датчиков для «умных городов».

Ключевые выводы:

  • работа с системами координат IMU и слиянием данных датчиков;
  • баланс между точностью модели и скоростью инференса на периферийных устройствах;
  • построение надёжных конвейеров сбора и предобработки данных;
  • развёртывание моделей машинного обучения во встраиваемых системах;
  • объединение нескольких датчиков (IMU + дисплей) в полноценные решения.

Интеграция классификации движения с XIAOML Kit показывает, как современные встраиваемые системы могут решать сложные задачи ИИ локально и принимать решения в реальном времени без облака. Такой подход лежит в основе будущего периферийного ИИ в промышленном IoT, автономных системах и умных устройствах.

Ресурсы

Обнаружение объектов

Мультфильм в стиле анимации 1950-х: плата с камерой на столе, за ней компьютер с Arduino IDE
Промпт DALL·E: мультфильм в стиле анимации 1950-х с подробной платой, на которой видны датчики, в первую очередь камера, на столе с узорчатой скатертью. За платой стоит компьютер с большой задней панелью, на котором открыта Arduino IDE. Содержимое IDE намекает на назначение выводов светодиодов и инференс модели машинного обучения для распознавания голосовых команд. Serial Monitor в отдельном окне показывает результаты для команд «yes» и «no».

Обзор

В предыдущем разделе о компьютерном зрении (CV) и XIAO ESP32S3, посвящённом классификации изображений, мы научились настраивать эту замечательную отладочную плату и классифицировать изображения. Продолжая знакомство с компьютерным зрением, мы изучим обнаружение объектов на микроконтроллерах.

Обнаружение объектов и классификация изображений

Основная задача моделей классификации изображений — определить наиболее вероятную категорию объекта, присутствующего на изображении, например отличить кота от собаки, то есть доминирующий «объект» на изображении:

Классификация изображения: доминирующий объект на фото

Но что будет, если на изображении нет доминирующей категории?

Изображение без доминирующей категории

Модель классификации изображений совершенно неверно определяет это изображение как «ashcan» (мусорный ящик), возможно из-за цветовых оттенков.

Модель, которая использовалась на предыдущих изображениях, — MobileNet, обученная на большом наборе данных ImageNet, работающая на Raspberry Pi.

Чтобы решить эту проблему, нужен другой тип моделей, которые находят не только несколько категорий (или меток), но и где на изображении расположены объекты.

Такие модели гораздо сложнее и крупнее, например MobileNetV2 SSD FPN-Lite 320x320, обученная на наборе COCO. Эта предобученная модель обнаружения объектов умеет находить до 10 объектов на изображении и выдаёт ограничивающий прямоугольник (bounding box) для каждого обнаруженного объекта. Изображение ниже — результат работы такой модели на Raspberry Pi:

Результат работы MobileNetV2 SSD на Raspberry Pi

Модели для обнаружения объектов (такие как MobileNet SSD или YOLO) обычно занимают несколько мегабайт. Для Raspberry Pi это нормально, но для встраиваемых устройств, где ОЗУ обычно составляет в лучшем случае несколько мегабайт, как у XIAO ESP32S3, такие модели не подходят.

Инновационное решение для обнаружения объектов: FOMO

Edge Impulse представила в 2022 году FOMO (Faster Objects, More Objects) — новое решение для обнаружения объектов на встраиваемых устройствах, таких как Nicla Vision и Portenta (Cortex M7), процессорах Cortex M4F (Arduino Nano33 и серии OpenMV M4), а также устройствах Espressif ESP32 (ESP-CAM, ESP-EYE и XIAO ESP32S3 Sense).

В этом практическом проекте мы будем обнаруживать объекты с помощью FOMO.

Чтобы больше узнать о FOMO, прочитайте официальное объявление FOMO от Edge Impulse, где Louis Moreau и Mat Kelcey подробно объясняют принцип работы.

Цель проекта обнаружения объектов

Любой проект машинного обучения должен начинаться с подробно сформулированной цели. Допустим, мы работаем на промышленном или сельском предприятии и должны сортировать и считать апельсины (фрукты) и определённых лягушек (жуки).

Апельсины и лягушки для проекта обнаружения

Иначе говоря, нам нужна многоклассовая классификация, где каждое изображение может относиться к трём классам:

  • фон (объектов нет);
  • фрукт (Fruit);
  • жук (Bug).

Вот несколько неразмеченных примеров изображений, по которым мы будем обнаруживать объекты (фрукты и жуков):

Неразмеченные примеры изображений с фруктами и жуками

Нас интересует, какой объект есть на изображении, его положение (центроид) и сколько таких объектов на нём присутствует. Размер объекта FOMO не определяет — в отличие от MobileNet SSD или YOLO, где ограничивающий прямоугольник является одним из выходных данных модели.

Проект мы будем разрабатывать с использованием XIAO ESP32S3 для захвата изображений и инференса. Проект машинного обучения будет создан в Edge Impulse Studio. Но прежде чем начинать проект обнаружения объектов в Studio, создадим сырой набор данных (без разметки) с изображениями, на которых есть объекты для обнаружения.

Сбор данных

Изображения можно снимать с помощью XIAO, телефона или других устройств. Здесь мы будем использовать XIAO с кодом из библиотеки ESP32 для Arduino IDE.

Сбор набора данных с XIAO ESP32S3

Откройте Arduino IDE и выберите плату XIAO_ESP32S3 (и порт, к которому она подключена). В меню File > Examples > ESP32 > Camera выберите CameraWebServer.

На панели BOARDS MANAGER (менеджер плат) убедитесь, что установлен последний стабильный (stable) пакет.

⚠️ Внимание

Альфа-версии (например, 3.x-alpha) некорректно работают с XIAO и Edge Impulse. Используйте последнюю стабильную версию (например, 2.0.11).

Также закомментируйте все модели камер, кроме выводов модели XIAO:

#define CAMERA_MODEL_XIAO_ESP32S3 // Есть PSRAM

И включите PSRAM в меню Tools. Введите данные Wi-Fi и загрузите код на устройство:

Настройка CameraWebServer в Arduino IDE для XIAO

Если код выполнился правильно, в Serial Monitor появится адрес:

IP-адрес камеры в Serial Monitor

Скопируйте адрес в браузер и дождитесь загрузки страницы. Выберите разрешение камеры (например, QVGA) и нажмите [START STREAM]. Подождите несколько секунд или минут в зависимости от качества соединения. Кнопкой [Save] можно сохранить изображение в папку загрузок на компьютере.

Сбор изображений через веб-интерфейс CameraWebServer

Edge Impulse рекомендует, чтобы объекты были похожего размера и не перекрывали друг друга, — так модель работает лучше. На промышленном предприятии это нормально: камера закреплена неподвижно и находится на одном расстоянии от объектов. Тем не менее мы попробуем и смешанные размеры и положения, чтобы посмотреть на результат.

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

Рекомендуем около 50 изображений, с разным сочетанием объектов и разным их количеством в кадре. Старайтесь снимать с разных углов, на разном фоне и при разном освещении.

Сохраняемые изображения имеют размер кадра QVGA и формат RGB565 (формат цветных пикселей).

Когда набор данных собран, нажмите [Stop Stream] и переместите изображения в папку.

Edge Impulse Studio

Настройка проекта

Откройте Edge Impulse Studio, войдите в систему по кнопке Login (или создайте аккаунт) и начните новый проект.

Создание нового проекта в Edge Impulse Studio

Здесь можно клонировать проект, созданный для этой практической работы: XIAO-ESP32S3-Sense-Object_Detection.

На панели проекта Dashboard прокрутите вниз до Project info, выберите Bounding boxes (object detection) и Espressif ESP-EYE (наиболее похожее на нашу плату) в качестве целевого устройства:

Выбор типа Bounding boxes и устройства ESP-EYE

Загрузка неразмеченных данных

В Studio откройте вкладку Data acquisition и в разделе UPLOAD DATA загрузите снятые изображения из папки на компьютере.

Загрузка папки с изображениями в Studio

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

Загрузка изображений как обучающих

Все неразмеченные изображения (47) загружены, но перед использованием в наборе данных их нужно правильно разметить. Для этого в Studio есть инструмент, который открывается по ссылке Labeling queue (47).

В бесплатной версии Edge Impulse Studio доступны два способа разметки с помощью ИИ:

  • с использованием YOLOv5;
  • отслеживанием объектов между кадрами.

Для клиентов Enterprise Edge Impulse запустила функцию автоматической разметки, которая упрощает разметку в проектах обнаружения объектов.

Обычные объекты можно быстро распознать и разметить с помощью готовой библиотеки предобученных моделей обнаружения YOLOv5 (обученных на наборе COCO). Но в нашем случае объектов нет в наборах COCO, поэтому выберем опцию tracking objects (отслеживание объектов). С ней, как только вы нарисуете рамки и разметите изображения в одном кадре, объекты будут автоматически отслеживаться от кадра к кадру, и новые объекты будут частично размечены (не все — корректно).

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

Разметка набора данных

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

Разметка объекта рамкой в Studio

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

Размеченные примеры изображений с фруктами и жуками

Затем просмотрите размеченные примеры на вкладке Data acquisition. Если какая-то метка неверна, её можно отредактировать через меню «три точки» после названия образца:

Меню «три точки» для редактирования метки

Studio подскажет, как заменить неверную метку и исправить набор данных.

Исправление неверной метки в наборе данных

Балансировка набора данных и разделение на обучающую и тестовую выборки

После разметки всех данных стало очевидно, что класса «фрукт» гораздо больше, чем «жука». Поэтому собрали 11 дополнительных изображений жуков (всего стало 58). После их разметки нужно отобрать часть изображений в тестовую выборку. Это делается через меню «три точки» после названия изображения. Я отобрал шесть изображений, что составляет 13 % от общего набора данных.

Перемещение изображений в тестовую выборку

Проектирование Impulse

На этом этапе нужно определить, как:

  • Предобработка: изменить размер отдельных изображений с до , сжав их к квадратной форме (без обрезки). Затем изображения переводятся из RGB в оттенки серого (Grayscale).
  • Проектирование модели: в данном случае «Object Detection» (обнаружение объектов).

Конвейер Impulse: предобработка и обнаружение объектов

Предобработка всего набора данных

В этом разделе выберите Color depth — Grayscale, подходящий для моделей FOMO, и сохраните параметры (Save parameters).

Выбор Grayscale в параметрах предобработки

Studio автоматически переходит к разделу Generate features, где обрабатываются все образцы. В результате получается набор данных из отдельных изображений , или 9 216 признаков.

Генерация признаков в Studio

Просмотр признаков показывает, что после генерации все образцы хорошо разделяются.

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

Проектирование модели, обучение и тестирование

Мы будем использовать FOMO — модель обнаружения объектов на основе MobileNetV2 (альфа 0,35), которая грубо сегментирует изображение на сетку: фон и интересующие объекты (здесь фрукты и жуки).

FOMO — инновационная модель машинного обучения для обнаружения объектов, которая может использовать в 30 раз меньше энергии и памяти, чем традиционные модели вроде Mobilenet SSD и YOLOv5. FOMO способна работать на микроконтроллерах с ОЗУ менее 200 КБ. Главная причина в том, что другие модели вычисляют размер объекта, рисуя вокруг него квадрат (ограничивающий прямоугольник), а FOMO не учитывает размер и выдаёт только информацию о том, где объект находится на изображении, — в виде координат его центроида.

Как работает FOMO

FOMO берёт изображение в оттенках серого и делит его на блоки пикселей с коэффициентом 8. Для входного изображения сетка будет . Затем FOMO запускает классификатор для каждого блока пикселей, чтобы вычислить вероятность того, что в нём есть фрукт или жук, и определяет области с наибольшей вероятностью наличия объекта (если в блоке пикселей объектов нет, он классифицируется как background). По перекрытию итоговой области FOMO выдаёт координаты (относительно размеров изображения) центроида этой области.

Схема работы FOMO: сетка блоков и центроиды объектов

Для обучения нужно выбрать предобученную модель. Используем FOMO (Faster Objects, More Objects) MobileNetV2 0.35. Эта модель занимает около 250 КБ ОЗУ и 80 КБ ПЗУ (флеш-памяти), что хорошо подходит для нашей платы.

Выбор модели FOMO MobileNetV2 0.35 в Studio

Гиперпараметры обучения такие:

  • эпох: 60;
  • размер батча (batch size): 32;
  • скорость обучения (learning rate): 0,001.

Для валидации во время обучения будут отложены 20 % набора данных (validation_dataset). Для оставшихся 80 % (train_dataset) применим аугментацию данных: случайно отразим, изменим размер и яркость изображений и обрежем их, искусственно увеличивая количество образцов для обучения.

В итоге модель получает общую F1-меру 85 %, что близко к результату на тестовых данных (83 %).

FOMO автоматически добавил третью метку background к двум заданным ранее (fruit и bug).

Результаты обучения модели FOMO в Studio

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

Проверка модели в режиме «Live Classification»

После обучения модели её можно проверить с помощью инструмента Live Classification. В соответствующем разделе нажмите значок подключения платы разработки (небольшой микроконтроллер) и отсканируйте QR-код телефоном.

Подключение платы через QR-код в Live Classification

После подключения телефон можно использовать для съёмки реальных изображений, которые будет тестировать обученная модель в Edge Impulse Studio.

Тестирование модели на изображениях с телефона

Учтите, что модель может давать ложные срабатывания и пропуски. Это можно уменьшить, задав подходящий Confidence Threshold (порог уверенности; настройка через меню «три точки»). Попробуйте значение 0,8 и выше.

Развёртывание модели (Arduino IDE)

Выберите Arduino Library и модель Quantized (int8), включите компилятор EON на вкладке Deploy и нажмите [Build].

Выбор Arduino Library и Quantized (int8) на вкладке Deploy

Откройте Arduino IDE, в меню Sketch выберите Include Library и Add .ZIP Library. Выберите файл, скачанный из Edge Impulse Studio, — и готово!

Добавление ZIP-библиотеки в Arduino IDE

На вкладке Examples в Arduino IDE под именем вашего проекта вы найдёте скетч (esp32 > esp32_camera).

Скетч esp32_camera в примерах проекта

Нужно изменить строки с 32 по 75, которые задают модель камеры и выводы, используя данные нашей модели. Скопируйте приведённые ниже строки и замените ими строки 32–75:

#define PWDN_GPIO_NUM     -1
#define RESET_GPIO_NUM    -1
#define XCLK_GPIO_NUM     10
#define SIOD_GPIO_NUM     40
#define SIOC_GPIO_NUM     39
#define Y9_GPIO_NUM       48
#define Y8_GPIO_NUM       11
#define Y7_GPIO_NUM       12
#define Y6_GPIO_NUM       14
#define Y5_GPIO_NUM       16
#define Y4_GPIO_NUM       18
#define Y3_GPIO_NUM       17
#define Y2_GPIO_NUM       15
#define VSYNC_GPIO_NUM    38
#define HREF_GPIO_NUM     47
#define PCLK_GPIO_NUM     13

Вот получившийся код:

Итоговый код с настройками выводов камеры

Загрузите код на XIAO ESP32S3 Sense — и можно начинать обнаруживать фрукты и жуков. Результат можно проверить в Serial Monitor.

Фон

Инференс: класс фона

Фрукты

Инференс: фрукты

Жуки

Инференс: жуки

Обратите внимание: задержка модели составляет 143 мс, а частота кадров — около 7 кадров в секунду (примерно как в проекте классификации изображений). Это объясняется тем, что FOMO искусно построена на основе CNN-модели, а не модели обнаружения вроде SSD MobileNet. Например, при запуске модели MobileNetV2 SSD FPN-Lite на Raspberry Pi 4 задержка примерно в пять раз выше (около 1,5 кадра в секунду).

Развёртывание модели (SenseCraft Web Toolkit)

Как обсуждалось в главе о классификации изображений, проверка инференса моделей для изображений в Arduino IDE очень затруднительна, потому что мы не видим, на что направлена камера. Поэтому воспользуемся SenseCraft Web Toolkit.

Чтобы запустить SenseCraft Web Toolkit, выполните следующие шаги:

  1. Откройте сайт SenseCraft Web Toolkit.
  2. Подключите XIAO к компьютеру:
  • При подключённом XIAO выберите его, как показано ниже:

Выбор XIAO в SenseCraft Web Toolkit

  • Выберите устройство и порт, затем нажмите [Connect]:

Выбор порта и подключение в SenseCraft Web Toolkit

Можно попробовать несколько моделей компьютерного зрения, заранее загруженных Seeed Studio. Экспериментируйте!

В нашем случае мы используем синюю кнопку внизу страницы: [Upload Custom AI Model].

Но сначала нужно скачать из Edge Impulse Studio нашу квантованную модель .tflite.

  1. Перейдите в свой проект в Edge Impulse Studio или клонируйте этот:
  1. На вкладке Dashboard скачайте модель («block output»): Object Detection model — TensorFlow Lite (int8 quantized).

Скачивание модели TensorFlow Lite int8 на Dashboard

  1. В SenseCraft Web Toolkit нажмите синюю кнопку внизу страницы: [Upload Custom AI Model]. Откроется окно. Укажите файл модели, скачанный из Edge Impulse Studio, выберите имя модели и введите метки (ID: Object):

Загрузка пользовательской модели и ввод меток

Учтите, что нужно использовать метки, заданные в EI Studio, и вводить их в алфавитном порядке (в нашем случае: background, bug, fruit).

Через несколько секунд (или минут) модель загрузится на устройство, и изображение с камеры появится в реальном времени в области предпросмотра (Preview):

Предпросмотр обнаружения объектов в SenseCraft Web Toolkit

Обнаруженные объекты будут отмечены (центроиды). Можно выбрать уверенность инференса курсором Confidence и IoU — меру, которая оценивает точность предсказанных ограничивающих рамок по сравнению с истинными.

Нажав верхнюю кнопку (Device Log), можно открыть Serial Monitor и следить за инференсом, как мы делали в Arduino IDE.

Device Log с результатами инференса

В Device Log вы получите такие данные:

  • время предобработки (захват и обрезка изображения): 3 мс;
  • время инференса (задержка модели): 115 мс;
  • время постобработки (вывод изображения и отметка объектов): 1 мс;
  • выходной тензор (рамки), например одна из рамок: [[30, 150, 20, 20, 97, 2]]. Здесь 30, 150, 20, 20 — координаты рамки (вокруг центроида); 97 — результат инференса, а 2 — класс (в данном случае 2: fruit, фрукт).

В приведённом примере получилось 5 рамок, поскольку ни один фрукт не получил по 3 центроида. Одно из решений — постобработка, при которой близкие центроиды объединяются в один.

Ещё несколько снимков экрана:

Дополнительные снимки результатов в SenseCraft Web Toolkit

Итоги

FOMO — важный скачок в области обработки изображений, как сказали Louis Moreau и Mat Kelcey при её запуске в 2022 году:

FOMO — революционный алгоритм, который впервые приносит обнаружение, отслеживание и подсчёт объектов в реальном времени на микроконтроллеры.

Для исследования обнаружения объектов (и, точнее, их подсчёта) на встраиваемых устройствах есть множество возможностей.

Ресурсы

Настройка

Иллюстрация платы XIAO ESP32S3 с модулем камеры на лабораторном столе
Промпт DALL·E: рисунок в стиле мультфильма 1950-х с платой XIAO ESP32S3 с характерным модулем камеры, как на предоставленном изображении. Плата стоит на классическом лабораторном столе среди различных датчиков, включая микрофон. За платой — старинный экран компьютера с Arduino IDE в приглушённых цветах; код посвящён настройке выводов светодиодов и инференсу модели машинного обучения для голосовых команд. Serial Monitor в IDE показывает распознанные команды «yes» и «no». Сцена сочетает ретро-шарм лабораторий середины века с современной электроникой.

Обзор

XIAOML Kit предназначен для практического знакомства с приложениями TinyML. В комплект входят мощная отладочная плата XIAO ESP32S3 Sense и плата расширения, которая добавляет датчики, необходимые для проектов машинного обучения.

Состав комплекта XIAOML Kit:

  • XIAO ESP32S3 Sense: основная отладочная плата со встроенной камерой, цифровым микрофоном и поддержкой SD-карт;
  • Плата расширения: содержит 6-осевой IMU (LSM6DS3TR-C) и 0,42-дюймовый OLED-дисплей для регистрации движения и визуализации данных;
  • Набор для работы с SD-картой: включает SD-карту и адаптер USB для хранения данных и развёртывания моделей;
  • Кабель USB-C: для подключения платы к компьютеру;
  • Антенна и радиаторы.

⚠️ Внимание

Не устанавливайте радиаторы на плату XIAO ESP32S3 (или снимите их аккуратно), если хотите использовать плату расширения XIAO ML Kit. Подробнее см. в приложении.

Комплект XIAOML Kit: плата XIAO ESP32S3 Sense и плата расширения

Основные особенности платы XIAO ESP32S3 Sense

XIAO ESP32S3 Sense — сердце комплекта XIAOML Kit. Она объединяет вычислительные возможности для ML с фото- и аудиофункциями и подходит для проектов TinyML в области интеллектуального голосового и визуального ИИ.

Плата XIAO ESP32S3 Sense

Ключевые особенности

  • Мощный микроконтроллер: 32-разрядный двухъядерный процессор ESP32S3 с архитектурой Xtensa, работающий на частоте до 240 МГц, с поддержкой Arduino и MicroPython;
  • Расширенные функции: съёмная камера OV2640 с разрешением , совместимая с камерой OV5640, а также встроенный цифровой микрофон;
  • Продуманное питание: управление зарядом литиевой батареи, четыре режима энергопотребления, режим глубокого сна с потреблением всего 14 мкА;
  • Достаточно памяти: 8 МБ PSRAM и 8 МБ флеш-памяти, слот для SD-карты до 32 ГБ (FAT);
  • Отличные радиохарактеристики: двойная беспроводная связь Wi-Fi 2,4 ГГц и BLE, дальность связи более 100 м при использовании антенны с разъёмом U.FL;
  • Компактный корпус: мм, классический форм-фактор XIAO, удобный для проектов с ограниченным местом.

Расположение выводов XIAO ESP32S3 Sense

Ниже показана общая распиновка платы:

Общая распиновка платы XIAO ESP32S3 Sense

Подробности см. на вики Seeed Studio.

Особенности платы расширения

Плата расширения дополняет XIAOML Kit возможностями для приложений машинного обучения, основанных на движении:

Виды платы расширения XIAOML Kit

Компоненты:

  • 6-осевой IMU (LSM6DS3TR-C):

    • 3-осевой акселерометр и 3-осевой гироскоп для обнаружения и классификации движения;
      • диапазон акселерометра: ±2/±4/±8/±16 g;
      • диапазон гироскопа: ±125/±250/±500/±1000/±2000 dps;
      • интерфейс I2C (адрес: 0x6A).
  • 0,42-дюймовый OLED-дисплей:

    • монохромный дисплей (разрешение ) для визуализации данных в реальном времени;
      • контроллер: SSD1306;
      • интерфейс I2C (адрес: 0x3C).
  • Кнопка перезапуска (EN);

  • Разъём аккумулятора (BAT+, BAT−).

Сборка комплекта

Плата расширения легко подключается к XIAO ESP32S3 Sense, образуя универсальную платформу для мультимодальных экспериментов с машинным обучением: зрение, звук и движение.

Собранный комплект XIAOML Kit

Обратите внимание на ориентацию установки модуля:

Правильная ориентация установки модуля

Обратите внимание:

  • соединение EN, которое находится внизу ESP32S3 Sense, доступно на плате расширения через кнопку RST;
  • соединения BAT+ и BAT- также доступны через белый разъём BAT3.7V.

Применения XIAOML Kit:

  • Зрение: классификация изображений и обнаружение объектов с помощью встроенной камеры;
  • Звук: распознавание ключевых слов и голосовых команд со встроенным микрофоном;
  • Движение: распознавание активности и обнаружение аномалий с помощью датчиков IMU;
  • Мультимодальность: комплексное слияние данных датчиков для сложных приложений ML.

Установка XIAO ESP32S3 Sense в Arduino IDE

  1. Подключите XIAOML Kit к компьютеру через порт USB-C.

Подключение XIAOML Kit к компьютеру через USB-C

  1. Скачайте и установите стабильную версию Arduino IDE для вашей операционной системы.

    [Скачать Arduino IDE]

  2. Откройте Arduino IDE и выберите Boards Manager (менеджер плат), который обозначен значком UNO.

  3. Введите «ESP32» и выберите «esp32 by Espressif Systems». Можно установить или обновить пакеты поддержки плат.

Не выбирайте «Arduino ESP32 Boards by Arduino»: это пакет поддержки Arduino Nano ESP32, а не нашей платы.

Выбор пакета esp32 by Espressif Systems в Boards Manager

⚠️ Внимание

Версии 3.x могут создавать проблемы при использовании XIAO ESP32S3 Sense с кодом развёртывания Edge Impulse. Если это так, используйте последнюю стабильную версию 2.0.x (например, 2.0.17).

  1. Нажмите Select Board, введите xiao или esp32s3, выберите XIAO_ESP32S3 в менеджере плат и соответствующий порт PORT, к которому подключена ESP32S3.

Выбор платы XIAO_ESP32S3 и порта

Готово! Плата должна работать. Проведём несколько тестов.

На плате XIAO ESP32S3 Sense есть встроенный светодиод, подключённый к GPIO21. Поэтому можно запустить скетч Blink, который находится в File/Examples/Basics/Blink. Скетч использует константу Arduino LED_BUILTIN, которая внутри соответствует светодиоду на выводе 21. Или можно изменить скетч Blink вручную:

#define LED_BUILT_IN 21 // Эта строка необязательна

void setup() {
  pinMode(LED_BUILT_IN, OUTPUT); // Настраиваем вывод на выход
}

// Помните, что выводы работают с инверсной логикой:
// LOW — включить, HIGH — выключить
void loop() {
  digitalWrite(LED_BUILT_IN, LOW); // Включить
  delay (1000); // Ждать 1 секунду
  digitalWrite(LED_BUILT_IN, HIGH); // Выключить
  delay (1000); // Ждать 1 секунду
}

Обратите внимание: выводы работают с инверсной логикой — LOW включает светодиод, HIGH выключает.

Мигание встроенным светодиодом XIAO

Проверка микрофона

Начнём с обнаружения звука. Введите код ниже или перейдите в проект на GitHub и скачайте скетч XIAOML_Kit_Mic_Test, затем запустите его в Arduino IDE:

/*
  Простой тест микрофона XIAO ESP32S3
  (для библиотеки ESP32 версии 3.0.x и новее)
*/

#include <ESP_I2S.h>
I2SClass I2S;

void setup() {
  Serial.begin(115200);
  while (!Serial) {
    }

  // Настройка тактового сигнала PDM на выводе 42 и данных PDM на выводе 41
  I2S.setPinsPdmRx(42, 41);

  // Запуск I2S с частотой 16 кГц и 16 битами на отсчёт
  if (!I2S.begin(I2S_MODE_PDM_RX,
                 16000,
                 I2S_DATA_BIT_WIDTH_16BIT,
                 I2S_SLOT_MODE_MONO)) {
    Serial.println("Failed to initialize I2S!");
    while (1); // ничего не делать
  }
}

void loop() {
  // Считываем отсчёт
  int sample = I2S.read();

  if (sample && sample != -1 && sample != 1) {
    Serial.println(sample);
  }
}

Откройте Serial Plotter — вы увидите кривую изменения громкости звука.

Кривая громкости звука в Serial Plotter

Когда вы издаёте звуки, изменения сигнала видны в Serial Plotter.

Сохранение записанного звука (аудиофайлы .wav) на microSD-карту.

Теперь с помощью встроенного считывателя SD-карт можно сохранять аудиофайлы .wav. Для этого сначала нужно включить PSRAM на XIAO.

У ESP32-S3 на чипе микроконтроллера всего несколько сотен килобайт внутренней оперативной памяти. Этого может не хватить для некоторых задач, поэтому к флеш-чипу SPI можно подключить до 16 МБ внешней PSRAM (псевдостатического ОЗУ). На XIAO установлено 8 МБ PSRAM. Внешняя память включена в адресное пространство процессора и при определённых ограничениях может использоваться так же, как внутренняя оперативная память данных.

  • Чтобы её включить, откройте Tools->PSRAM:"OPI PSRAM"->OPI PSRAM.

Включение OPI PSRAM в меню Tools

XIAO ESP32S3 Sense поддерживает microSD-карты объёмом до 32 ГБ. Если вы собираетесь купить microSD-карту для XIAO, выбирайте карту объёмом не более 32 ГБ. Перед использованием отформатируйте microSD-карту в FAT32.

Теперь вставьте отформатированную в FAT32 SD-карту в XIAO, как показано на фото:

Вставка SD-карты в XIAO

/*
 * Диктофон WAV для Seeed XIAO ESP32S3 Sense
 * (для библиотеки ESP32 версии 3.0.x и новее)
*/

#include "ESP_I2S.h"
#include "FS.h"
#include "SD.h"

void setup() {
  // Создаём экземпляр I2SClass
  I2SClass i2s;

  // Переменные для хранения аудиоданных
  uint8_t *wav_buffer;
  size_t wav_size;

  // Инициализация последовательного порта
  Serial.begin(115200);
  while (!Serial) {
    delay(10);
  }

  Serial.println("Initializing I2S bus...");

  // Настройка выводов для аудиовхода
  i2s.setPinsPdmRx(42, 41);

  // Запуск I2S с частотой 16 кГц и 16 битами на отсчёт
  if (!i2s.begin(I2S_MODE_PDM_RX,
                 16000,
                 I2S_DATA_BIT_WIDTH_16BIT,
                 I2S_SLOT_MODE_MONO)) {
    Serial.println("Failed to initialize I2S!");
    while (1); // ничего не делать
  }

  Serial.println("I2S bus initialized.");
  Serial.println("Initializing SD card...");

  // Настройка выводов для доступа к SD-карте
  if(!SD.begin(21)){
    Serial.println("Failed to mount SD Card!");
    while (1) ;
  }
  Serial.println("SD card initialized.");
  Serial.println("Recording 20 seconds of audio data...");

  // Запись 20 секунд аудиоданных
  wav_buffer = i2s.recordWAV(20, &wav_size);

  // Создаём файл на SD-карте
  File file = SD.open("/arduino_rec.wav", FILE_WRITE);
  if (!file) {
    Serial.println("Failed to open file for writing!");
    return;
  }

  Serial.println("Writing audio data to file...");

  // Записываем аудиоданные в файл
  if (file.write(wav_buffer, wav_size) != wav_size) {
    Serial.println("Failed to write audio data to file!");
    return;
  }

  // Закрываем файл
  file.close();

  Serial.println("Application complete.");
}

void loop() {
  delay(1000);
  Serial.printf(".");
}
  • Сохраните код, например, как Wav_Record.ino, и запустите его в Arduino IDE.
  • Эта программа выполняется только один раз после того, как пользователь откроет Serial Monitor (или нажмёт кнопку RESET). Она записывает 20 секунд и сохраняет файл на microSD-карту под именем «arduino_rec.wav».
  • Когда в Serial Monitor каждую секунду появляется «.», выполнение программы завершено, и записанный звуковой файл можно воспроизвести, подключив карту через считыватель.

Вывод Serial Monitor во время записи звука

Качество звука отличное!

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

Подробнее о файловой системе XIAO ESP32S3 Sense — по этой ссылке.

Проверка камеры

Для проверки и использования камеры можно применять несколько способов:

  • SenseCraft AI Studio;
  • приложение CameraWebServer в Arduino IDE (см. следующий раздел);
  • захват изображений и сохранение их на SD-карту (аналогично тому, что мы делали со звуком).

Проверка камеры в SenseCraft AI Studio

Самый простой способ увидеть работу камеры — использовать SenseCraft AI Studio, надёжную платформу, которая предлагает широкий набор моделей ИИ для разных устройств, включая XIAO ESP32S3 Sense и Grove Vision AI V2.

Также можно воспользоваться SenseCraft Web Toolkit — упрощённой версией SenseCraft AI Studio.

Чтобы запустить SenseCraft AI, выполните следующие шаги:

Рабочее пространство SenseCraft AI Vision

  • Подключите XIAOML Kit к ноутбуку и выберите его, как показано ниже:

Выбор XIAOML Kit в SenseCraft AI

Примечание: инструмент WebUSB может работать некорректно в некоторых браузерах, например в Safari. Используйте Chrome. Также убедитесь, что Arduino IDE или любое другое последовательное устройство не подключено к XIAO.

Чтобы увидеть работу камеры, нужно загрузить модель. Можно попробовать несколько моделей компьютерного зрения, заранее загруженных Seeed Studio. Нажмите кнопку [Select Model] и выберите одну из доступных моделей.

Выбор модели в SenseCraft AI

Наведя курсор на модели ИИ, можно увидеть их название, описание, категорию или задачу (классификация изображений, обнаружение объектов или определение позы/ключевых точек), алгоритм (например, YOLO V5 или V8, FOMO, MobileNet V2 и т. д.), а в некоторых случаях и метрики (точность или mAP).

Информация о модели ИИ в SenseCraft AI

Можно выбрать одну из готовых моделей, например «Person Classification», нажав на неё и затем кнопку [Confirm], или загрузить собственную модель.

Подтверждение выбранной модели

В области Preview мы видим видеопоток с камеры.

Предпросмотр видеопотока с камеры в SenseCraft AI

Мы подробнее вернёмся к SenseCraft AI Studio в лабораторных работах по компьютерному зрению.

Проверка Wi-Fi

Установка антенны

Комплект XIAOML Kit приходит полностью собранным. Сначала снимите плату расширения Sense (на ней камера, микрофон и считыватель SD-карт) с XIAO.

В левом нижнем углу передней стороны XIAO ESP32S3 есть отдельный разъём «WiFi/BT Antenna Connector». Чтобы улучшить сигнал Wi-Fi и Bluetooth, достаньте антенну из упаковки и прикрепите её к разъёму.

Есть небольшой приём при установке антенны. Если давить на неё прямо и сильно, она будет поддаваться с трудом, и пальцы заболят! Правильный способ: сначала вставить один край антенного разъёма в гнездо, а затем аккуратно прижать другой край, чтобы антенна надёжно закрепилась.

Снимается антенна так же: не тяните её силой напрямую, а поднимите с одной стороны, и тогда её будет легко снять.

Установка антенны на разъём XIAO ESP32S3

Установить плату расширения обратно очень просто: совместите разъём платы расширения с разъёмом B2B на XIAO ESP32S3, сильно надавите и услышите «щелчок». Установка завершена.

Одна из отличительных черт XIAO ESP32S3 — возможности Wi-Fi. Проверим радиомодуль, сканируя окружающие сети Wi-Fi. Для этого запустите один из примеров кода на плате.

Откройте Arduino IDE, выберите нашу плату и порт. Перейдите в Examples и найдите WiFi > WiFiScan в разделе «Examples for the XIAO ESP32S3». Загрузите скетч на плату.

В Serial Monitor вы должны увидеть сети Wi-Fi (SSID и RSSI) в радиусе действия вашего устройства. Вот что получилось у нас в лаборатории:

Результат сканирования Wi-Fi в Serial Monitor

Простой Wi-Fi-сервер (включение и выключение светодиода)

Проверим способность устройства работать как Wi-Fi-сервер. Мы разместим на устройстве простую страницу, которая отправляет команды для включения и выключения встроенного светодиода XIAO.

Перейдите в Examples и найдите WiFi > SimpleWiFiServer в разделе «Examples for the XIAO ESP32S3».

Перед запуском скетча введите данные своей сети:

const char* ssid     = "Your credentials here";
const char* password = "Your credentials here";

Затем измените вывод 5 на вывод 21, к которому подключён встроенный светодиод. Также измените веб-страницу (строки 85 и 86), чтобы отразить правильный вывод светодиода и то, что он включается сигналом LOW:

client.print("Click <a href=\"/H\">here</a> to turn the LED on pin 21 OFF.<br>");
client.print("Click <a href=\"/L\">here</a> to turn the LED on pin 21 ON.<br>");

Работу сервера можно отслеживать через Serial Monitor.

Работа Wi-Fi-сервера в Serial Monitor

Возьмите IP-адрес из Serial Monitor и введите его в браузере. Откроется страница со ссылками, которые включают и выключают встроенный светодиод XIAO.

Веб-страница для управления светодиодом XIAO

Использование CameraWebServer

В Arduino IDE откройте File > Examples > ESP32 > Camera и выберите CameraWebServer.

На вкладке board_config.h закомментируйте все модели камер, кроме выводов модели XIAO:

#define CAMERA_MODEL_XIAO_ESP32S3 // Есть PSRAM

Не забудьте проверить в меню Tools, включена ли PSRAM.

Настройка board_config.h для модели XIAO

Как и раньше, на вкладке CameraWebServer.ino введите данные Wi-Fi и загрузите код на устройство.

Если код выполнился правильно, в Serial Monitor появится адрес:

WiFi connecting....
WiFi connected
Camera Ready! Use 'http://192.168.5.60' to connect

Скопируйте адрес в браузер и дождитесь загрузки страницы. Выберите разрешение камеры (например, QVGA) и нажмите [START STREAM]. Подождите несколько секунд в зависимости от качества соединения. Кнопкой [Save] можно сохранить изображение в папку загрузок на компьютере.

Захват изображения с камеры через веб-интерфейс

Вот и всё! Изображения можно сохранить прямо на компьютер и использовать в проектах.

Проверка датчика IMU (LSM6DS3TR-C)

Инерциальный измерительный блок (IMU) — датчик, который измеряет движение и ориентацию. LSM6DS3TR-C на вашем XIAOML Kit — это 6-осевой IMU, то есть он объединяет:

  • 3-осевой акселерометр: измеряет линейное ускорение (включая силу тяжести) по осям X, Y и Z;
  • 3-осевой гироскоп: измеряет угловую скорость (скорость вращения) вокруг осей X, Y и Z.

Технические характеристики

  • Связь: интерфейс I2C, адрес 0x6A;
  • Диапазон акселерометра: ±2/±4/±8/±16 g (по умолчанию используем ±2 g);
  • Диапазон гироскопа: ±125/±250/±500/±1000/±2000 dps (по умолчанию используем ±250 dps);
  • Разрешение: 16-битный АЦП;
  • Энергопотребление: сверхнизкое.

Система координат

Датчик использует правостороннюю систему координат. Если смотреть на датчик IMU с видимой меткой (вид платы расширения снизу):

Метка на датчике IMU

  • Ось X: направлена вправо;
  • Ось Y: направлена вперёд (от вас);
  • Ось Z: направлена вверх (из платы).

Направления осей датчика IMU

Необходимые библиотеки

Перед загрузкой кода установите нужную библиотеку:

  1. Откройте Arduino IDE и выберите Manage Libraries (менеджер библиотек), который обозначен значком Books Icon.

  2. Для библиотеки IMU введите «LSM6DS3» и выберите «Seeed Arduino LSM6DS3 by Seeed». Можно INSTALL (установить) или UPDATE (обновить).

Установка библиотеки Seeed Arduino LSM6DS3

⚠️ Важно: НЕ устанавливайте «Arduino_LSM6DS3 by Arduino» — она предназначена для других плат!

Тестовый код

Введите приведённый ниже код в Arduino IDE и загрузите его в комплект:

#include <LSM6DS3.h>
#include <Wire.h>

// Создаём объект IMU через интерфейс I2C
// Датчик LSM6DS3TR-C находится по адресу I2C 0x6A
LSM6DS3 myIMU(I2C_MODE, 0x6A);

// Переменные для хранения показаний датчика
float accelX, accelY, accelZ;  // Значения акселерометра (перегрузка в g)
float gyroX, gyroY, gyroZ;     // Значения гироскопа (градусы в секунду)

void setup() {
  // Инициализация последовательной связи со скоростью 115200 бод
  Serial.begin(115200);

  // Ждём подключения последовательного порта (полезно для отладки)
  while (!Serial) {
    delay(10);
  }

  Serial.println("XIAOML Kit IMU Test");
  Serial.println("LSM6DS3TR-C 6-Axis IMU Sensor");
  Serial.println("=============================");

  // Инициализация датчика IMU
  if (myIMU.begin() != 0) {
    Serial.println("ERROR: IMU initialization failed!");
    Serial.println("Check connections and I2C address");
    while(1) {
      delay(1000); // Останавливаем выполнение, если IMU не инициализировался
    }
  } else {
    Serial.println("✓ IMU initialized successfully");
    Serial.println();

    // Вывод информации о датчике
    Serial.println("Sensor Information:");
    Serial.println("- Accelerometer range: ±2g");
    Serial.println("- Gyroscope range: ±250 dps");
    Serial.println("- Communication: I2C at address 0x6A");
    Serial.println();

    // Вывод описания формата данных
    Serial.println("Data Format:");
    Serial.println("AccelX,AccelY,AccelZ,GyroX,GyroY,GyroZ");
    Serial.println("Units: g-force (m/s²), degrees/second");
    Serial.println();

    delay(2000); // Короткая пауза перед началом измерений
  }
}

void loop() {
  // Считываем данные акселерометра (в единицах g)
  accelX = myIMU.readFloatAccelX();
  accelY = myIMU.readFloatAccelY();
  accelZ = myIMU.readFloatAccelZ();

  // Считываем данные гироскопа (в градусах в секунду)
  gyroX = myIMU.readFloatGyroX();
  gyroY = myIMU.readFloatGyroY();
  gyroZ = myIMU.readFloatGyroZ();

  // Вывод в удобном формате в Serial Monitor
  Serial.print("Accelerometer (g): ");
  Serial.print("X="); Serial.print(accelX, 3);
  Serial.print(" Y="); Serial.print(accelY, 3);
  Serial.print(" Z="); Serial.print(accelZ, 3);

  Serial.print(" | Gyroscope (°/s): ");
  Serial.print("X="); Serial.print(gyroX, 2);
  Serial.print(" Y="); Serial.print(gyroY, 2);
  Serial.print(" Z="); Serial.print(gyroZ, 2);
  Serial.println();

  // Вывод в формате CSV для Serial Plotter
  Serial.println(String(accelX) + "," + String(accelY) + "," +
                 String(accelZ) + "," + String(gyroX) + "," +
                 String(gyroY) + "," + String(gyroZ));

  // Частота обновления: 10 Гц (задержка 100 мс)
  delay(100);
}

Serial Monitor покажет значения, а Plotter — их изменение во времени. Например, если перемещать комплект по оси Y, будет меняться значение 2 (красная линия). Ось Z представлена значением 3 (зелёная линия), которое близко к 1,0 g. Синяя линия (значение 1) относится к оси X.

Графики значений IMU в Serial Plotter

Значения с 4 по 6 можно выбрать, чтобы увидеть поведение гироскопа.

Проверка OLED-дисплея (SSD1306)

OLED-дисплеи (Organic Light-Emitting Diode) — экраны с самосвечением, где каждый пиксель излучает собственный свет. Компактный 0,42-дюймовый монохромный OLED-дисплей XIAO ML kit подходит для вывода данных датчиков, информации о состоянии и простой графики.

Технические характеристики

  • Размер: 0,42 дюйма по диагонали;
  • Разрешение: пикселей;
  • Контроллер: SSD1306;
  • Интерфейс: I2C, адрес 0x3C;
  • Цвет: монохромный (чёрные пиксели на белом фоне или наоборот);
  • Читаемость: высокий контраст, видно даже при ярком освещении;
  • Питание: низкое энергопотребление, подсветка не нужна.

Характеристики дисплея

  • Точность пикселей: каждым из 2 880 пикселей () можно управлять отдельно;
  • Быстрое обновление: подходит для анимации и данных в реальном времени;
  • Без послесвечения: мгновенный отклик пикселей;
  • Широкий угол обзора: изображение читается с разных позиций.

Необходимые библиотеки

Перед загрузкой кода установите нужную библиотеку:

  1. Откройте Arduino IDE и выберите «Manage Libraries» (менеджер библиотек), который обозначен значком Books Icon.

  2. Введите u8g2 и выберите U8g2 by oliver. Можно install (установить) или update (обновить) пакеты.

    ℹ️ Примечание: U8g2 — мощная графическая библиотека, которая поддерживает множество типов дисплеев.

Установка библиотеки U8g2 в Library Manager

Библиотека U8g2 — монохромная графическая библиотека со следующими возможностями:

  • поддержка множества контроллеров дисплеев (включая SSD1306);
  • вывод текста различными шрифтами;
  • графические примитивы (линии, прямоугольники, окружности);
  • экономичный постраничный рендеринг;
  • аппаратная и программная поддержка I2C.

Тестовый код

Введите приведённый ниже код в Arduino IDE и загрузите его в комплект:

#include <U8g2lib.h>
#include <Wire.h>

// Инициализация OLED-дисплея
// Контроллер SSD1306, разрешение 72x40, интерфейс I2C
U8G2_SSD1306_72X40_ER_1_HW_I2C u8g2(U8G2_R2, U8X8_PIN_NONE);

void setup() {
  Serial.begin(115200);

  Serial.println("XIAOML Kit - Hello World");
  Serial.println("==========================");

  // Инициализация дисплея
  u8g2.begin();

  Serial.println("✓ Display initialized");
  Serial.println("Showing Hello World message...");

  // Очистка дисплея
  u8g2.clearDisplay();
}

void loop() {
  // Начало последовательности рисования
  u8g2.firstPage();
  do {
    // Выбор шрифта
    u8g2.setFont(u8g2_font_ncenB08_tr);

    // Вывод «Hello World» по центру
    u8g2.setCursor(8, 15);
    u8g2.print("Hello");

    u8g2.setCursor(12, 30);
    u8g2.print("World!");

    // Простое оформление — рамка вокруг текста
    u8g2.drawFrame(2, 2, 68, 36);

  } while (u8g2.nextPage());

  // Задержка не нужна — дисплей будет показывать изображение постоянно
}

Если всё работает правильно, на дисплее вы увидите надпись «Hello World» внутри прямоугольника.

Надпись Hello World внутри рамки на OLED-дисплее

Размеры и позиционирование текста на OLED

  • Текст позиционируется функцией setCursor(x, y), в данном случае по центру:

    u8g2.setCursor(8, 15);
    
  • В коде используется шрифт среднего размера:

    u8g2.setFont(u8g2_font_ncenB08_tr);
    

    Но доступны и другие размеры шрифта:

    • u8g2_font_4x6_tr: очень маленький шрифт ( пикселей);
    • u8g2_font_6x10_tr: маленький шрифт ( пикселей);
    • u8g2_font_ncenB08_tr: средний полужирный шрифт;
    • u8g2_font_ncenB14_tr: крупный полужирный шрифт.

Фигуры

В коде добавлено простое оформление: рамка вокруг текста.

u8g2.drawFrame(2, 2, 68, 36);

Доступны и другие фигуры:

  • Контур прямоугольника: drawFrame(x, y, width, height);
  • Закрашенный прямоугольник: drawBox(x, y, width, height);
  • Окружность: drawCircle(x, y, radius);
  • Линия: drawLine(x1, y1, x2, y2);
  • Отдельные пиксели: drawPixel(x, y).

Координаты

Дисплей использует систему координат, где:

  • начало координат (0,0): верхний левый угол;
  • ось X: возрастает слева направо (от 0 до 71);
  • ось Y: возрастает сверху вниз (от 0 до 39);
  • позиционирование текста: setCursor(x, y), где y — базовая линия текста.

Поворот дисплея

Параметр поворота можно изменить:

  • U8G2_R0: обычная ориентация;
  • U8G2_R1: поворот на 90° по часовой стрелке;
  • U8G2_R2: 180° (перевёрнуто);
  • U8G2_R3: поворот на 270° по часовой стрелке.

Собственные символы

// Рисуем собственное растровое изображение
static const unsigned char myBitmap[] = {0x00, 0x3c, 0x42, 0x42, 0x3c, 0x00};
u8g2.drawBitmap(x, y, 1, 6, myBitmap);

Измерение текста

int width = u8g2.getStrWidth("Hello");  // Ширина текста
int height = u8g2.getAscent();         // Высота шрифта

OLED-дисплей готов показывать данные датчиков, состояние системы или любую графику, которую вы создадите для своих проектов ML!

Итоги

XIAOML Kit с ESP32S3 Sense — мощная и доступная отправная точка в мир TinyML и встраиваемого машинного обучения. В ходе настройки мы последовательно проверили каждый компонент XIAOML Kit и убедились, что все датчики и периферийные устройства работают правильно. Двухъядерный процессор ESP32S3 и 8 МБ PSRAM обеспечивают достаточную вычислительную мощность для инференса в реальном времени, а камера OV2640, цифровой микрофон, IMU LSM6DS3TR-C и 0,42-дюймовый OLED-дисплей составляют полноценную мультимодальную платформу для сбора данных. Возможности Wi-Fi открывают путь к рабочим процессам ML от периферии к облаку, а среда разработки Arduino IDE теперь настроена со всеми необходимыми библиотеками.

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

Этот процесс настройки демонстрирует ключевые принципы, которые выходят далеко за рамки этого конкретного комплекта. Работа с ограничениями памяти и возможностями обработки ESP32S3 даёт реальный опыт работы с ресурсными ограничениями периферийного ИИ — теми же, что возникают при развёртывании моделей на смартфонах, устройствах IoT или автономных системах. Наличие нескольких модальностей (зрение, звук, движение) на одной платформе позволяет исследовать мультимодальные подходы к машинному обучению, которые всё важнее в реальных приложениях ИИ.

Главное: от сырых данных датчиков до инференса модели и обратной связи через OLED-дисплей комплект воплощает полный цикл развёртывания ML в миниатюре, повторяя трудности, с которыми сталкиваются промышленные системы ИИ.

С этой основой вы готовы решать основные задачи TinyML в следующих главах:

  • Проекты по зрению: использование камеры для классификации изображений и обнаружения объектов;
  • Звуковые проекты: обработка аудиопотоков для распознавания ключевых слов и голосовых команд;
  • Проекты по движению: использование данных IMU для распознавания активности и обнаружения аномалий.

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

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

Ресурсы

Приложение: радиаторы

Соображения о радиаторах

Если вам нужно использовать XIAO ESP32S3 Sense для задач с камерой БЕЗ платы расширения, можно установить радиатор.

Учтите: если радиатор установлен, подключить XIAO ESP32S3 Sense к плате расширения будет невозможно.

Радиатор для XIAO ESP32S3 Sense

Установка радиатора

Чтобы обеспечить оптимальное охлаждение XIAO ESP32S3 Sense, во время работы с камерой следует установить входящий в комплект радиатор. Его конструкция специально рассчитана на охлаждение, особенно при интенсивных операциях, таких как работа с камерой.

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

Установка:

  • Перед началом убедитесь, что устройство выключено и отключено от любого источника питания.
  • В первую очередь закройте радиатором термопрокладку (THERMAL PAD), которая находится прямо над чипом ESP32S3 — главным источником тепла. Правильное выравнивание обеспечивает оптимальный отвод тепла, и важно, чтобы контакты BAT оставались максимально свободными.

Теперь начнём установку:

Шаг 1. Подготовьте радиатор: снимите защитную плёнку с радиатора, чтобы открыть термоклей. Это подготовит радиатор к надёжному креплению на чипе ESP32S3.

Шаг 2. Соберите радиатор:

Сборка радиатора для XIAO ESP32S3 Sense

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

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

⚠️ Внимание

Аккуратно снимите радиаторы перед тем, как снова использовать плату расширения IMU.

Обзор

Эти лабораторные работы дают возможность получить практический опыт работы с системами машинного обучения (ML) на мощном, но компактном встраиваемом устройстве — Seeed Studio Grove Vision AI V2. В отличие от работы с большими моделями, которым нужны ресурсы уровня дата-центра, эти упражнения позволяют напрямую работать с оборудованием и программным обеспечением с помощью TinyML. Такой практический подход даёт осязаемое понимание проблем и возможностей развёртывания ИИ, пусть и в небольшом масштабе. При этом принципы в целом те же, что и при работе с более крупными или, наоборот, ещё более мелкими системами.

Grove Vision AI V2 занимает особое место в мире встраиваемого ИИ: он заполняет разрыв между простыми решениями на микроконтроллерах, такими как Seeed XIAO ESP32S3 Sense или Arduino Nicla Vision, и более мощными одноплатными компьютерами, например Raspberry Pi. В основе платы лежит процессор Himax WiseEye2 HX6538 с двухъядерным Arm Cortex-M55 и встроенным блоком нейронных сетей Arm Ethos-U55.

Arm Ethos-U55 — это специализированный процессор для машинного обучения, класса микро-NPU, который ускоряет инференс ML во встраиваемых и IoT-устройствах с ограниченной площадью кристалла. Сочетание Ethos-U55 с процессором Cortex-M55, рассчитанным на ИИ, даёт прирост производительности ML в 480 раз по сравнению с существующими системами на базе Cortex-M. Работая на частоте 400 МГц с настраиваемой внутренней системной памятью (SRAM) объёмом до 2,4 МБ, Grove Vision AI V2 предлагает возможности компьютерного зрения профессионального уровня и при этом сохраняет энергоэффективность и компактность, необходимые для периферийных приложений.

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

Grove - Vision AI Module V2
Grove - Vision AI Module V2. Источник: SEEED Studio.

Где купить

Grove Vision AI V2 продаётся в Seeed Studio:

Вам также понадобятся совместимый модуль камеры (Raspberry Pi OV5647) и, при желании, управляющая плата, например XIAO ESP32S3.

Предварительные требования

  • Плата Grove Vision AI V2: убедитесь, что плата у вас есть.
  • Модуль камеры Raspberry Pi OV5647: камера должна быть подключена к плате Grove Vision AI V2 для захвата изображений.
  • Управляющий контроллер: может быть Seeed XIAO ESP32S3, XIAO ESP32C6 или другое устройство.
  • Кабель USB-C: для подключения платы к компьютеру.
  • Сеть: доступ в интернет для загрузки необходимого программного обеспечения.
  • Базовая плата расширения XIAO: помогает подключить управляющее устройство к физическому миру (необязательно).

Настройка и приложения без кода

Упражнения

МодальностьЗадачаОписаниеСсылка
ЗрениеКлассификация изображенийНаучитесь классифицировать изображенияОткрыть
ЗрениеОбнаружение объектовРеализуйте обнаружение объектовУточняется

Классификация изображений

С использованием Seeed Studio Grove Vision AI Module V2 (Himax WiseEye2)

Обложка лабораторной работы по классификации изображений

В этой лабораторной работе мы исследуем классификацию изображений с помощью Grove Vision AI Module V2 от Seeed Studio — мощного, но компактного устройства, созданного специально для приложений встраиваемого машинного обучения. Модуль построен на микросхеме Himax WiseEye2 и предназначен для работы ИИ на периферийных устройствах, поэтому он идеально подходит для задач периферийного машинного обучения (Edge ML).

Введение

До сих пор мы исследовали несколько моделей компьютерного зрения, которые Seeed Studio загрузила заранее, либо использовали SenseCraft AI Studio для классификации изображений, не выбирая конкретную модель. Теперь создадим проект классификации изображений с нуля: сами выберем данные и модель.

Ниже показаны основные шаги проекта и то, где мы будем работать на каждом из них:

Блок-схема основных шагов проекта классификации изображений

Цель проекта

Первый шаг любого проекта машинного обучения (ML) — определить цель. В данном случае нужно обнаружить и классифицировать два конкретных объекта, присутствующих на одном изображении. Для проекта мы возьмём две небольшие игрушки: робота и маленького бразильского попугайчика (по имени Periquito). Также соберём изображения фона, на котором этих двух объектов нет.

Игрушки для проекта: робот и попугайчик Periquito

Сбор данных

Когда цель проекта машинного обучения определена, следующий и самый важный шаг — сбор набора данных. Если вашему проекту нужны изображения, которые уже публично доступны в наборах данных, например для проекта Person Detection (обнаружение людей), можно скачать набор Wake Vision и использовать его.

Но в нашем случае изображения не существуют в открытом доступе, поэтому их нужно создать. Фотографии можно сделать телефоном, веб-камерой компьютера или другим устройством — офлайн или с подключением к Edge Impulse Studio.

Если вы хотите использовать Grove Vision AI V2 для съёмки набора данных, можно воспользоваться SenseCraft AI Studio, как в предыдущей лабораторной работе, или скетчем camera_web_server, который мы опишем позже в разделе «Постобработка / Получение видеопотока» этой работы.

Захват видеопотока с Grove Vision AI V2

В этой лабораторной работе для сбора набора данных мы будем использовать SenseCraft AI Studio.

Сбор данных в SenseCraft AI Studio

В SenseCraft AI Studio откройте вкладку Training.

По умолчанию обучается модель Classification с веб-камерой, если она доступна. Выберем вместо неё Grove Vision AI V2. Нажмите зелёную кнопку [Connect] (1) — появится всплывающее окно. Выберите нужный порт (2) и нажмите синюю кнопку [Connect] (3).

Подключение Grove Vision AI V2 к SenseCraft AI Studio

На экране появится изображение, которое передаёт Grove Vision AI V2.

Сбор изображений

Создадим классы, например, в алфавитном порядке:

  • Класс 1: background (фон)
  • Класс 2: periquito (попугайчик)
  • Класс 3: robot (робот)

Создание классов background, periquito и robot

Выберите один из классов (вокруг окна появится зелёная рамка) и удерживайте зелёную кнопку под областью предпросмотра. Собранные изображения появятся на экране выборки (Image Samples).

Экран сбора изображений для выбранного класса

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

Удаление некорректных изображений

Соберите примерно по 50 изображений для каждого класса. Когда все три класса готовы, откройте меню каждого из них и выберите Export Data.

Экспорт данных класса в меню Studio

В папке загрузок компьютера появятся три ZIP-архива — по одному на каждый класс. Каждый архив содержит папку с изображениями.

Загрузка набора данных в Edge Impulse Studio

Для обучения модели мы будем использовать Edge Impulse Studio. Edge Impulse — ведущая платформа для разработки моделей машинного обучения на периферийных устройствах.

  • Войдите в Edge Impulse с вашими учётными данными (или создайте бесплатный аккаунт).
  • Затем создайте новый проект:

Создание нового проекта в Edge Impulse

Набор данных содержит примерно по 50 изображений на метку: 40 для обучения и 10 для тестирования.

Проектирование Impulse и предобработка

Проектирование Impulse

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

Классификация изображений — самое распространённое применение глубокого обучения, но для неё нужен большой объём данных. У нас около 50 изображений на каждую категорию. Достаточно ли этого? Совсем нет! Чтобы «научить» модель различать каждый класс, нужны тысячи изображений. Однако эту проблему можно решить, дообучив уже обученную модель на небольшом наборе данных. Такой приём называется «переносом обучения» (Transfer Learning, TL). С TL можно донастроить предобученную модель классификации изображений на наших данных и добиться хорошего качества даже на относительно небольших наборах, как в нашем случае.

Схема переноса обучения для классификации изображений

Итак, начиная с исходных изображений, уменьшим их до пикселей и передадим в блок переноса обучения (Transfer Learning):

Impulse: изменение размера изображений и блок переноса обучения

Для сравнения оставим размер изображений . Однако учтите: с Grove Vision AI Module V2 и его внутренней SRAM объёмом 2,4 МБ можно использовать и более крупные изображения (например, ).

Также в правом верхнем углу выберите устройство Target (Himax WiseEye2 (M55 400 MHz + U55)).

Предобработка (генерация признаков)

Помимо изменения размера, изображения можно перевести в оттенки серого или сохранить исходную цветовую глубину RGB. Выберем [RGB] в разделе Image. Тогда каждый образец данных будет иметь размерность 27 648 признаков (). Нажатие [Save Parameters] откроет новую вкладку Generate Features. Нажмите кнопку [Generate Features], чтобы сгенерировать признаки.

Проектирование модели, обучение и тестирование

В 2017 году Google представил MobileNetV1. В 2018 году вышла MobileNetV2: Inverted Residuals and Linear Bottlenecks, а в 2019 году — V3. MobileNet — это семейство универсальных нейронных сетей для компьютерного зрения, специально созданных для мобильных устройств и поддерживающих классификацию, обнаружение и другие задачи. MobileNet — компактные модели с малой задержкой и низким энергопотреблением, параметризованные так, чтобы соответствовать ограничениям ресурсов разных сценариев.

Хотя базовая архитектура MobileNet уже компактна и быстра, конкретному сценарию часто нужна модель ещё меньше и быстрее. Для этого в MobileNet есть простой параметр α (альфа), называемый множителем ширины (width multiplier), с помощью которого строятся меньшие и менее требовательные к вычислениям модели. Множитель ширины α равномерно «утончает» сеть на каждом слое.

В Edge Impulse Studio доступны MobileNet V1 (изображения ) и V2 (изображения и ) с несколькими значениями α (от 0,05 до 1,0). Например, наибольшая точность достигается у V2 с изображениями и α=1,0. Конечно, здесь есть компромисс: чем выше точность, тем больше памяти (примерно 1,3 МБ ОЗУ и 2,6 МБ ПЗУ) нужно для запуска модели и тем выше задержка. Самый маленький объём получится на другом полюсе — у MobileNet V1 с α=0,10 (примерно 53,2 КБ ОЗУ и 101 КБ ПЗУ).

Для сравнения мы возьмём MobileNet V2 0.1 в качестве базовой модели (но можно использовать и модель с большим α). Последний слой модели, перед выходным слоем, будет содержать 8 нейронов с dropout-регуляризацией 10 % для предотвращения переобучения.

Ещё один обязательный приём в глубоком обучении — аугментация данных. Это метод, который помогает повысить точность моделей машинного обучения за счёт создания дополнительных искусственных данных. Система аугментации вносит небольшие случайные изменения в обучающие данные в процессе обучения (например, отражает, обрезает или поворачивает изображения).

Задайте гиперпараметры:

  • Эпохи (Epochs): 20;
  • Размер батча (Batch Size): 32;
  • Скорость обучения (Learning Rate): 0,0005;
  • Размер валидационной выборки (Validation size): 20 %.

Результат обучения:

Результат обучения модели в Edge Impulse

Профиль модели предсказывает 146 КБ ОЗУ и 187 КБ флеш-памяти, что не создаёт проблем для Grove Vision AI (V2), у которого почти 2,5 МБ внутренней SRAM. Кроме того, Studio показывает задержку около 4 мс.

Тем не менее, при точности 100 % на валидационной выборке и при тестировании на отложенных данных мы получили точность 81 % для квантованной (Int8) обученной модели. Однако для наших целей этого достаточно.

Развёртывание модели

На вкладке Deployment выберите Seeed Grove Vision AI Module V2 (Himax WiseEye2) и нажмите [Build]. На компьютер загрузится ZIP-файл.

ZIP-архив содержит файл model_vela.tflite — модель TensorFlow Lite (TFLite), оптимизированную для нейронных процессоров (NPU) с помощью компилятора Vela, инструмента от Arm для адаптации моделей TFLite под NPU Ethos-U.

Содержимое ZIP-архива с моделью model_vela.tflite

Модель можно прошить, следуя инструкциям в README.txt, или через SenseCraft AI Studio. Мы воспользуемся вторым способом.

Развёртывание модели в SenseCraft AI Studio

В SenseCraft AI Studio перейдите на вкладку Vision Workspace и подключите устройство:

Подключение устройства на вкладке Vision Workspace

Вы увидите последнюю модель, загруженную на устройство. Нажмите зелёную кнопку [Upload Model]. Во всплывающем окне нужно указать имя модели, файл модели и ввести названия классов (объектов). Используйте метки в алфавитном порядке: 0: background, 1: periquito и 2: robot, затем нажмите [Send].

Ввод имён классов при загрузке модели

Через несколько секунд модель будет загружена («прошита») в устройство, а изображение с камеры появится в реальном времени в секции Preview. Результат классификации отображается под предпросмотром изображения. Также можно выбрать порог уверенности инференса (Confidence Threshold), передвинув ползунок в разделе Settings.

В Device Logger можно посмотреть Serial Monitor, где видна задержка: около 1–2 мс на предобработку и 4–5 мс на инференс, что совпадает с оценками в Edge Impulse Studio.

Журнал устройства с задержками предобработки и инференса

Вот другие снимки экрана:

Результаты инференса в SenseCraft AI Studio

Энергопотребление этой модели составляет около 70 мА, или 0,4 Вт.

Неофициальный бенчмарк классификации изображений

Для встраиваемого машинного обучения (TinyML) можно использовать несколько отладочных плат. Наиболее распространённые на сегодня для задач компьютерного зрения с низким энергопотреблением — ESP32 CAM, Seeed XIAO ESP32S3 Sense и Arduino Nicla Vision.

Мы воспользовались возможностью и развернули аналогичную модель, обученную похожим образом — MobileNetV2 с α=0,1 — также на ESP-CAM, XIAO и Raspberry Pi Zero W2. Вот результат:

Сравнение задержек модели на разных платах

Grove Vision AI V2 с ARM Ethos-U55 работает примерно в 14 раз быстрее устройств с ARM-M7 и более чем в 100 раз быстрее Xtensa LX6 (ESP-CAM). Даже по сравнению с Raspberry Pi, у которой гораздо более мощный процессор, U55 почти вдвое снижает задержку. Кроме того, энергопотребление ниже, чем у других устройств (сравнение энергопотребления см. в полной статье).

Постобработка

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

Учтите, что в качестве управляющего контроллера можно использовать любой микроконтроллер, например XIAO, Arduino или Raspberry Pi.

Получение видеопотока

Обработка изображений и инференс модели выполняются локально в Grove Vision AI (V2), а результат мы хотим передать на XIAO (управляющий контроллер) по шине IIC. Для этого используем библиотеку Arduino SSCMA. Её основная задача — обрабатывать поток данных Grove Vision AI, но не сам инференс модели.

Grove Vision AI (V2) передаёт результаты инференса на XIAO по IIC; адрес устройства на шине IIC — 0x62. Передача изображения осуществляется через последовательный порт USB.

Шаг 1. Скачайте библиотеку Arduino SSCMA в виде ZIP-архива с её страницы на GitHub:

Страница библиотеки Arduino SSCMA на GitHub

Шаг 2. Установите её в Arduino IDE (sketch > Include Library > Add .Zip Library).

Шаг 3. Установите библиотеку ArduinoJSON.

Установка библиотеки ArduinoJSON

Шаг 4. Установите библиотеку Eigen.

Установка библиотеки Eigen

Шаг 5. Теперь подключите XIAO и Grove Vision AI (V2) через разъём (ряд контактов), расположенный на задней стороне устройства.

Подключение XIAO к Grove Vision AI V2 через разъём на задней стороне

ВНИМАНИЕ: соблюдайте направление соединения: Type-C разъём Grove Vision AI должен быть направлен в ту же сторону, что и Type-C разъём XIAO.

Шаг 6. Подключите порт XIAO USB-C к компьютеру.

Подключение порта XIAO USB-C к компьютеру

Шаг 7. В Arduino IDE выберите плату XIAO и соответствующий порт USB.

Когда мы захотим передавать видео на веб-страницу, понадобится XIAO ESP32S3: у неё есть Wi-Fi и достаточно памяти для обработки изображений. Выберите XIAO_ESP32S3 и нужный порт USB:

Выбор платы XIAO_ESP32S3 и порта USB в Arduino IDE

По умолчанию PSRAM отключена. Откройте меню Tools и в пункте PSRAM выберите "OPI PSRAM".

Выбор OPI PSRAM в меню Tools

Шаг 8. Откройте пример в Arduino IDE:

File → Examples → Seeed_Arduino_SSCMA → camera_web_server.

Затем измените ssid и password в скетче camera_web_server.ino, чтобы они совпадали с вашей сетью Wi-Fi.

Шаг 9. Загрузите скетч на плату и откройте Serial Monitor. Когда плата подключится к сети Wi-Fi, в мониторе отобразится её IP-адрес.

IP-адрес платы в Serial Monitor

Откройте этот адрес в веб-браузере. Станет доступно видеоприложение (Video App). Чтобы видеть только видеопоток с Grove Vision AI V2, нажмите [Sample Only] и [Start Stream].

Видеоприложение с кнопками Sample Only и Start Stream

Если хотите создать набор изображений, это приложение можно использовать, сохраняя кадры видео с устройства. Нажмите [Save Frame] — изображение сохранится в папку загрузок на компьютере.

Сохранение кадров видео в веб-приложении

Если открыть приложение без выбора [Sample Only], результат инференса должен появиться на видеоэкране. Для классификации изображений этого не происходит, а вот для обнаружения объектов или оценки позы результат встраивается в видеопоток.

Например, если модель — Person Detection на основе YOLOv8:

Обнаружение людей с помощью YOLOv8 в веб-приложении

Получение результата инференса

  • Перейдите в File → Examples → Seeed_Arduino_SSCMA → inference_class.

  • Загрузите скетч на плату и откройте Serial Monitor.

  • Направьте камеру на один из наших объектов — результат инференса появится в последовательном терминале.

Результат инференса для попугайчика в последовательном терминале

Инференс в Arduino IDE потреблял в среднем 160 мА, или 800 мВт, с пиком 330 мА (1,65 Вт) при передаче изображения в приложение.

Постобработка со светодиодом

Идея постобработки такая: когда распознан определённый объект (например, попугайчик — метка 1), пользовательский светодиод включается. Если обнаружены робот или фон, светодиод выключен.

Скопируйте код ниже и вставьте его в IDE:

#include <Seeed_Arduino_SSCMA.h>
SSCMA AI;

void setup()
{
    AI.begin();

    Serial.begin(115200);
    while (!Serial);
    Serial.println("Инференс — Grove AI V2 / XIAO ESP32S3");

    // Пины встроенного светодиода
    pinMode(LED_BUILTIN, OUTPUT);
    // Убедитесь, что светодиод выключен по умолчанию.
    // Примечание: светодиод горит, когда пин в состоянии LOW, и гаснет при HIGH.
    digitalWrite(LED_BUILTIN, HIGH);
}

void loop()
{
    if (!AI.invoke()){
        Serial.println("\nВызов выполнен успешно");
        Serial.print("Задержка [мс]: предобработка=");
        Serial.print(AI.perf().prepocess);
        Serial.print(", инференс=");
        Serial.print(AI.perf().inference);
        Serial.print(", постобработка=");
        Serial.println(AI.perf().postprocess);
        int pred_index = AI.classes()[0].target;
        Serial.print("Результат= метка: ");
        Serial.print(pred_index);
        Serial.print(", оценка=");
        Serial.println(AI.classes()[0].score);
        turn_on_led(pred_index);
    }
}

/**
* @brief      Функция turn_off_led — выключает пользовательский светодиод
*/
void turn_off_led(){
    digitalWrite(LED_BUILTIN, HIGH);
}

/**
* @brief      Функция turn_on_led — включает пользовательский светодиод
* @param[in]  pred_index
*             метка 0: [0] ==> все выключены
*             метка 1: [1] ==> светодиод включён
*             метка 2: [2] ==> все выключены
*             метка 3: [3] ==> все выключены
*/
void turn_on_led(int pred_index) {
    switch (pred_index)
    {
        case 0:
            turn_off_led();
            break;
        case 1:
            turn_off_led();
            digitalWrite(LED_BUILTIN, LOW);
            break;
        case 2:
            turn_off_led();
            break;
        case 3:
            turn_off_led();
            break;
    }
}

Этот скетч использует библиотеку Seeed_Arduino_SSCMA.h для связи с модулем Grove Vision AI Module V2. Модуль ИИ и светодиод инициализируются в функции setup(), и запускается последовательная связь.

Функция loop() многократно вызывает метод invoke(), чтобы выполнить инференс с помощью встроенных алгоритмов модуля Grove Vision AI Module V2. При успешном инференсе скетч выводит в монитор порта метрики производительности: время предобработки, инференса и постобработки.

Скетч обрабатывает и выводит подробную информацию о результатах инференса:

  • (AI.classes()[0]) определяет класс изображения (.target) и его оценку уверенности (.score);
  • результат инференса (класс) сохраняется в целочисленной переменной pred_index, которая передаётся в функцию turn_on_led(). В результате светодиод включается или выключается в зависимости от результата классификации.

Вот результат:

Если распознан попугайчик (метка 1), светодиод горит:

Светодиод горит при распознавании попугайчика

Если распознан робот (метка 2), светодиод не горит (то же самое для фона, метка 0):

Светодиод не горит при распознавании робота

Поэтому теперь мы можем питать Grove Vision AI V2 + XIAO ESP32S3 от внешней батареи, а результат инференса будет отображаться светодиодом полностью офлайн. Потребление составляет примерно 165 мА, или 825 мВт.

Результат также можно передавать по Wi-Fi, BLE или другим протоколам связи, доступным на используемом управляющем устройстве.

Дополнительно: постобработка на внешних устройствах

Одно из важных преимуществ работы с периферийным ИИ (Edge AI) — устройства могут работать полностью без облака, обеспечивая непосредственное взаимодействие с реальным миром. В предыдущем разделе мы делали это со встроенным светодиодом XIAO. Теперь подключим внешние светодиоды (это может быть любое исполнительное устройство).

Блок-схема: инференс и управление внешними светодиодами

Светодиоды следует подключать к земле XIAO через резистор 220 Ом.

Схема подключения светодиодов к XIAO

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

ЦЕЛЬ: при распознавании изображения попугайчика включается зелёный светодиод; если это робот, включается жёлтый светодиод; если это фон, включается красный светодиод.

Обработка изображений и инференс модели выполняются локально в Grove Vision AI (V2), а результат мы хотим передать на XIAO по IIC. Для этого снова используем библиотеку Arduino SSCMA.

Вот скетч, который будем использовать:

#include <Seeed_Arduino_SSCMA.h>
SSCMA AI;

// Определите пины светодиодов согласно схеме выводов
// Отрицательный вывод светодиодов следует подключить к земле XIAO
// через резистор 220 Ом.
int LEDR = D1; // Пин 1 XIAO ESP32S3
int LEDY = D2; // Пин 2 XIAO ESP32S3
int LEDG = D3; // Пин 3 XIAO ESP32S3

void setup()
{
    AI.begin();

    Serial.begin(115200);
    while (!Serial);
    Serial.println("Инференс — Grove AI V2 / XIAO ESP32S3");

// Инициализация внешних светодиодов
    pinMode(LEDR, OUTPUT);
    pinMode(LEDY, OUTPUT);
    pinMode(LEDG, OUTPUT);
    // Убедитесь, что светодиоды выключены по умолчанию.
    // Примечание: светодиоды горят, когда пин в состоянии HIGH, и гаснут при LOW.
    digitalWrite(LEDR, LOW);
    digitalWrite(LEDY, LOW);
    digitalWrite(LEDG, LOW);
}

void loop()
{
    if (!AI.invoke()){
        Serial.println("\nВызов выполнен успешно");
        Serial.print("Задержка [мс]: предобработка=");
        Serial.print(AI.perf().prepocess);
        Serial.print(", инференс=");
        Serial.print(AI.perf().inference);
        Serial.print(", постобработка=");
        Serial.println(AI.perf().postprocess);
        int pred_index = AI.classes()[0].target;
        Serial.print("Результат= метка: ");
        Serial.print(pred_index);
        Serial.print(", оценка=");
        Serial.println(AI.classes()[0].score);
        turn_on_leds(pred_index);
    }
}

/**
* @brief turn_off_leds — выключает все светодиоды
*/
void turn_off_leds(){
    digitalWrite(LEDR, LOW);
    digitalWrite(LEDY, LOW);
    digitalWrite(LEDG, LOW);
}

/**
* @brief turn_on_leds — включает определённый светодиод
* @param[in]  pred_index
*             метка 0: [0] ==> горит красный
*             метка 1: [1] ==> горит зелёный
*             метка 2: [2] ==> горит жёлтый
*/
void turn_on_leds(int pred_index) {
    switch (pred_index)
    {
        case 0:
            turn_off_leds();
            digitalWrite(LEDR, HIGH);
            break;
        case 1:
            turn_off_leds();
            digitalWrite(LEDG, HIGH);
            break;
        case 2:
            turn_off_leds();
            digitalWrite(LEDY, HIGH);
            break;
        case 3:
            turn_off_leds();
            break;
    }
}

Grove Vision AI V2 нужно подключить к XIAO через разъём I2C Grove. Для XIAO мы используем плату расширения (хотя IIC можно подключить и напрямую к контактам XIAO). Питание плат можно подать через разъём USB-C, но можно использовать и батарею.

Монтаж: Grove Vision AI V2, XIAO и плата расширения с тремя светодиодами

Вот результат:

Результат работы внешних светодиодов при инференсе

Пиковое энергопотребление составило 240 мА (зелёный светодиод), или 1,2 Вт. Питание жёлтого и красного светодиодов потребляет 140 мА, или 0,7 Вт. Вывод информации в терминал через последовательный порт не влияет на энергопотребление.

Итоги

В этой лабораторной работе мы полностью прошли процесс разработки системы классификации изображений на модуле Seeed Studio Grove Vision AI Module V2 с чипом Himax WiseEye2. Мы прошли все этапы рабочего процесса машинного обучения: от определения целей проекта до развёртывания рабочей модели с взаимодействием с реальным миром.

Grove Vision AI V2 показал впечатляющую производительность: время инференса составляет всего 4–5 мс, что намного превосходит другие распространённые платформы TinyML. По результатам нашего бенчмарка он примерно в 14 раз быстрее устройств на ARM-M7 и более чем в 100 раз быстрее Xtensa LX6 (ESP-CAM). Даже по сравнению с Raspberry Pi Zero W2 блок NPU Ethos-U55 обеспечил почти двукратный прирост скорости при меньшем энергопотреблении.

В этом проекте мы увидели, как перенос обучения позволяет получить хорошие результаты классификации на относительно небольшом наборе собственных изображений. Модель MobileNetV2 с α=0,1 обеспечила отличный баланс точности и эффективности для нашей задачи с тремя классами: ей нужно всего 146 КБ ОЗУ и 187 КБ флеш-памяти, что легко укладывается в 2,4 МБ внутренней SRAM модуля Grove Vision AI Module V2.

Мы также рассмотрели несколько вариантов развёртывания: от просмотра результатов инференса в SenseCraft AI Studio до автономной системы с визуальной индикацией на светодиодах. Возможность передавать видео в веб-браузер и обрабатывать результаты инференса локально демонстрирует универсальность систем периферийного ИИ для реальных задач.

Потребление энергии итоговой системы оставалось очень низким — примерно от 70 мА (0,4 Вт) при базовом инференсе до 240 мА (1,2 Вт) при управлении внешними компонентами. Благодаря этой эффективности Grove Vision AI Module V2 — отличный выбор для устройств с батарейным питанием, где энергопотребление критично.

Эта работа показала, что сложные задачи компьютерного зрения теперь можно выполнять полностью на периферии, без облачных сервисов и мощных компьютеров. Благодаря таким инструментам, как Edge Impulse Studio и SenseCraft AI Studio, процесс разработки стал доступен даже тем, у кого нет серьёзного опыта в машинном обучении.

По мере развития технологий периферийного ИИ компактные и энергоэффективные устройства, такие как Grove Vision AI Module V2, будут становиться всё мощнее, открывая новые возможности для умных датчиков, IoT-приложений и встроенного интеллекта в повседневных предметах.

Ресурсы

Обнаружение объектов

Эта лабораторная работа находится в разработке.

Настройка и приложения без кода

Обложка части «Настройка и приложения без кода»

В этой лабораторной работе мы исследуем приложения компьютерного зрения (CV) с помощью модуля Grove Vision AI Module V2 от Seeed Studio — мощного, но компактного устройства, созданного специально для приложений встраиваемого машинного обучения. Модуль построен на микросхеме Himax WiseEye2 и предназначен для работы ИИ на периферийных устройствах, поэтому он идеально подходит для задач периферийного машинного обучения (Edge ML).

Введение

Обзор модуля Grove Vision AI (V2)

Grove Vision AI (V2) — модуль компьютерного зрения на базе микроконтроллера. В нём используется процессор Himax WiseEye2 HX6538 с двухъядерным Arm Cortex-M55 и встроенным блоком нейронных сетей Arm Ethos-U55. Arm Ethos-U55 — это класс процессоров для машинного обучения (ML), созданный специально как микро-NPU для ускорения инференса ML во встраиваемых и IoT-устройствах с ограниченной площадью кристалла. В сочетании с процессором Cortex-M55, рассчитанным на ИИ, Ethos-U55 обеспечивает прирост производительности ML в 480 раз по сравнению с существующими системами на базе Cortex-M. Тактовая частота составляет 400 МГц, а внутренняя системная память (SRAM) конфигурируется и имеет максимальный объём 2,4 МБ.

Чип Himax WiseEye2

Примечание: по данным документации Seeed Studio, помимо внутренней памяти Himax объёмом 2,5 МБ (2,4 МБ SRAM и 64 КБ ПЗУ), Grove Vision AI (V2) оснащён внешней флеш-памятью 16 МБ / 133 МГц.

Модуль Grove Vision AI (V2)

Ниже приведена блок-схема системы Grove Vision AI (V2), включающей камеру и управляющий контроллер.

Блок-схема системы Grove Vision AI (V2) с камерой и управляющим контроллером

Благодаря интерфейсам IIC, UART, SPI и Type-C Grove Vision AI (V2) легко подключается к устройствам вроде XIAO, Raspberry Pi, BeagleBoard и плат на базе ESP для дальнейшей разработки. Например, интеграция Grove Vision AI V2 с одним из устройств семейства XIAO упрощает доступ к результатам инференса через Arduino IDE или MicroPython и подключение к облаку или выделенным серверам, например к Home Assistant.

С помощью разъёма I2C Grove Grove Vision AI V2 легко подключается к любому управляющему устройству.

Монтаж: Grove Vision AI V2 и управляющие устройства

Помимо производительности, стоит отметить энергопотребление. Например, в сравнительном тесте с XIAO ESP32S3 Sense при запуске модели Swift-YOLO Tiny 96×96 Grove Vision AI V2 показал более высокую производительность (30 FPS против 5,5 FPS) и при этом меньшее энергопотребление (0,35 Вт против 0,45 Вт).

Результаты сравнительного теста производительности и энергопотребления

Сравнение (и с другими устройствами) можно найти в статье 2024 MCU AI Vision Boards: Performance Comparison — она подтверждает высокую производительность Grove Vision AI (V2).

Установка камеры

Когда Grove Vision AI (V2) и камера готовы, можно подключить, например, модуль Raspberry Pi OV5647 Camera Module через шлейф CSI.

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

Подключение шлейфа камеры к Grove Vision AI (V2)

SenseCraft AI Studio

SenseCraft AI Studio — надёжная платформа, которая предлагает широкий набор моделей ИИ для разных устройств, включая XIAO ESP32S3 Sense и Grove Vision AI V2. В этой работе мы пройдём процесс использования модели ИИ с Grove Vision AI V2 и посмотрим результат её работы. Также мы разберём ключевые понятия и настройки и научимся оптимизировать производительность модели.

Интерфейс SenseCraft AI Studio

Модели также можно развернуть с помощью SenseCraft Web Toolkit — упрощённой версии SenseCraft AI Studio.

Для простоты можно начать с SenseCraft Web Toolkit, а можно сразу перейти в SenseCraft AI Studio, где больше ресурсов.

SenseCraft Web Toolkit

SenseCraft Web Toolkit — это визуальный инструмент развёртывания моделей, входящий в состав SSCMA (Seeed SenseCraft Model Assistant). Он позволяет просто и быстро развёртывать модели на разных платформах. У инструмента удобный интерфейс, и для работы не нужно писать код.

SenseCraft Web Toolkit основан на Himax AI Web Toolkit, который при желании (необязательно) можно скачать по ссылке. После загрузки и распаковки на локальный компьютер дважды щёлкните index.html, чтобы запустить его локально.

Интерфейс Himax AI Web Toolkit

В нашем случае выполним следующие шаги, чтобы запустить SenseCraft-Web-Toolkit:

  • Откройте сайт SenseCraft-Web-Toolkit в браузере Chrome.
  • Подключите Grove Vision AI (V2) к компьютеру кабелем Type-C.
  • Когда Grove Vision AI (V2) подключён, выберите его, как показано ниже:

Выбор устройства в SenseCraft Web Toolkit

  • Выберите устройство и порт и нажмите [Connect]:

Выбор порта подключения в SenseCraft Web Toolkit

Примечание: инструмент WebUSB может работать некорректно в некоторых браузерах, например в Safari. Используйте Chrome.

Можно попробовать несколько базовых моделей компьютерного зрения, заранее загруженных Seeed Studio. Если навести курсор на модель ИИ, станут видны её название, описание, категория (классификация изображений, обнаружение объектов или детекция позы и ключевых точек), алгоритм (например, YOLO V5 или V8, FOMO, MobileNet V2 и т. д.) и метрики (точность Accuracy или mAP).

Список готовых моделей ИИ в SenseCraft Web Toolkit

Чтобы использовать одну из готовых моделей, нажмите на неё и затем на кнопку [Send], либо загрузите собственную модель.

Для платформы SenseCraft AI следуйте инструкциям по ссылке.

Изучение моделей компьютерного зрения

Обнаружение объектов

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

К архитектурам, которые задали эталон в обнаружении объектов, относятся модели YOLO (You Only Look Once), SSD (Single Shot MultiBox Detector), FOMO (Faster Objects, More Objects) и Faster R-CNN (Region-based Convolutional Neural Networks).

Выберем одну из готовых моделей ИИ, например Person Detection (обнаружение людей), которая обучена с помощью алгоритма Swift-YOLO.

Выбор модели Person Detection в SenseCraft

После успешной загрузки модели в области предпросмотра справа появится видеопоток с камеры Grove Vision AI (V2). Подробности инференса можно вывести в Serial Monitor, нажав кнопку [Device Log] вверху.

Видеопоток и журнал устройства в SenseCraft Web Toolkit

В SenseCraft AI Studio журнал устройства (Device Logger) всегда отображается на экране.

Я направил камеру на себя, поэтому модель нашла только одного человека и выдала одну рамку (box). Если присмотреться, модуль непрерывно передаёт две строки данных:

Результат инференса: строки perf и boxes

perf (производительность) — задержки в миллисекундах:

  • время предобработки (захват изображения и обрезка): 7 мс;
  • время инференса (задержка модели): 76 мс (13 fps);
  • время постобработки (вывод изображения и добавление данных): меньше 1 мс (в журнале — 0 мс).

boxes — объекты, найденные на изображении. В данном случае — один.

  • У рамки координаты x, y, w и h равны (245, 292, 449, 392), а объект (человек, метка 0) распознан с уверенностью 0,89.

Если направить камеру на изображение с несколькими людьми, для каждого человека (объекта) будет своя рамка:

Несколько рамок для нескольких людей на одном изображении

В SenseCraft AI Studio задержка инференса (48 мс) меньше, чем в SenseCraft ToolKit (76 мс), благодаря другой реализации развёртывания.

Результаты инференса в SenseCraft AI Studio

Энергопотребление

Пиковое энергопотребление при работе этой модели Swift-YOLO составило 410 милливатт.

Настройки предпросмотра

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

  • Confidence (уверенность): уровень достоверности, который модель присваивает своим предсказаниям. Этот параметр задаёт минимальный уровень уверенности, при котором детекция считается верной. При более высоком пороге детекций будет меньше, но они будут надёжнее; при более низком — детекций будет больше, однако среди них могут оказаться ложные срабатывания.

  • IoU: метрика, которая оценивает точность предсказанных ограничивающих рамок по сравнению с эталонными (ground truth). IoU измеряет степень перекрытия предсказанной рамки и эталонной и используется для оценки точности обнаружения объектов. Порог IoU задаёт минимальное значение, при котором детекция считается истинно положительной. Изменение этого порога помогает тонко настроить точность (precision) и полноту (recall) модели.

Настройки порогов Confidence и IoU

Поэкспериментируйте со значениями порогов Confidence и IoU, чтобы найти оптимальный баланс между точным обнаружением людей и минимизацией ложных срабатываний. Лучшие настройки могут отличаться в зависимости от конкретного приложения и характеристик изображений или видеопотока.

Детекция позы и ключевых точек

Детекция позы, или ключевых точек, — продвинутая область компьютерного зрения, которая находит на изображении или кадре видео определённые точки интереса, чаще всего относящиеся к телам людей, лицам или другим объектам. Эта технология обнаруживает и отображает ключевые точки объекта, например суставы человеческого тела или черты лица, что позволяет анализировать позы, движения и жесты. Это важно для дополненной реальности, взаимодействия человека с компьютером, спортивной аналитики и мониторинга здоровья, где нужно понимать движения и активность человека.

В отличие от общего обнаружения объектов, которое находит и локализует объекты, детекция позы работает на более тонком уровне и фиксирует положение и ориентацию отдельных частей. Ведущие архитектуры в этой области — OpenPose, AlphaPose и PoseNet, каждая из которых решает задачи оценки позы со своей степенью сложности и точности. Благодаря развитию глубокого обучения и нейронных сетей детекция позы становится всё точнее и эффективнее и даёт данные о динамике объектов на видео в реальном времени.

Давайте исследуем это популярное приложение компьютерного зрения — детекцию позы и ключевых точек.

Выбор модели Pose/Keypoint Detection

Остановите текущий инференс кнопкой [Stop] в области предпросмотра. Выберите модель и нажмите [Send]. После успешной загрузки модели вы увидите видеопоток с камеры Grove Vision AI (V2) в области предпросмотра справа и данные инференса в Serial Monitor (кнопка [Device Log] вверху).

Детекция позы для нескольких людей

Модель YOLOv8 Pose обучена на наборе данных COCO-Pose, который содержит 200 тысяч изображений с разметкой из 17 ключевых точек для задач оценки позы.

Рассмотрим один кадр инференса (для простоты возьмём изображение с одним человеком). Видны две строки: одна — с производительностью инференса в миллисекундах (121 мс), вторая — с ключевыми точками:

  • 1 блок информации, такой же, как в примере с обнаружением объектов: координаты рамки (113, 119, 67, 208), результат инференса (90) и метка (0);
  • 17 групп по 4 числа — это 17 «суставов» тела, где «0» — нос, «1» и «2» — глаза, «15» и «16» — стопы и так далее.

Результат детекции позы для одного человека

Чтобы глубже разобраться в проекте оценки позы, см. руководство Exploring AI at the Edge! — Pose Estimation.

Классификация изображений

Классификация изображений — базовая задача компьютерного зрения: она относит всё изображение к одному из нескольких заранее заданных классов. Для этого анализируется визуальное содержимое изображения и назначается метка из фиксированного набора категорий в зависимости от основного объекта или сцены.

Классификация изображений важна во многих приложениях — от организации и поиска в больших базах изображений в цифровых библиотеках и соцсетях до помощи автономным системам в понимании окружающего мира. Среди архитектур, которые заметно продвинули классификацию изображений, — свёрточные нейронные сети (CNN), например AlexNet, VGGNet и ResNet. Эти модели показали высокую точность на сложных наборах данных, таких как ImageNet, обучаясь иерархическим представлениям визуальных данных.

Классификация изображений лежит в основе многих систем компьютерного зрения: она закладывает фундамент для более сложных задач, например обнаружения объектов и сегментации изображений, и помогает глубже понимать визуальные данные в самых разных отраслях. Давайте исследуем и это приложение компьютерного зрения.

Пример классификации изображения: человек

Этот пример доступен в SenseCraft ToolKit, но не в SenseCraft AI Studio. В последнем можно найти и другие примеры классификации изображений.

После успешной загрузки модели можно посмотреть видеопоток с камеры Grove Vision AI (V2) в области предпросмотра справа и данные инференса в Serial Monitor (кнопка [Device Log] вверху).

Классификация: человек в кадре

В результате мы получаем оценку и класс.

Строка результата классификации

Например, [99, 1] означает класс 1 (человек) с оценкой 0,99. Поскольку эта модель бинарная, класс 0 будет «Нет человека» (или фон). Задержка инференса — 15 мс, то есть около 70 кадров в секунду.

Энергопотребление

Для запуска MobileNet V2 0.35 Grove Vision AI V2 потреблял пиковый ток 80 мА при напряжении 5,24 В, что соответствует энергопотреблению 420 мВт.

При запуске той же модели на XIAO ESP32S3 Sense энергопотребление составило 523 мВт при задержке 291 мс.

Сравнение инференса на XIAO ESP32S3 Sense

Другие модели в SenseCraft AI Studio

Несколько публичных моделей ИИ можно скачать на веб-странице SenseCraft AI. Например, можно запустить модель Swift-YOLO, распознающую светофоры, как показано ниже:

Распознавание светофоров моделью Swift-YOLO

Задержка этой модели составляет около 86 мс, среднее энергопотребление — 420 мВт.

Проект классификации изображений

Создадим полноценный проект классификации изображений в SenseCraft AI Studio.

Блок-схема проекта классификации изображений

В SenseCraft AI Studio откройте вкладку Training:

Вкладка Training в SenseCraft AI Studio

По умолчанию обучается модель Classification с веб-камерой, если она доступна. Выберем вместо неё Grove Vision AI V2. Нажмите зелёную кнопку [Connect] — появится всплывающее окно. Выберите нужный порт и нажмите синюю кнопку [Connect].

Подключение Grove Vision AI V2 к SenseCraft AI Studio

На экране появится изображение, которое передаёт Grove Vision AI V2.

Цель

Первый шаг — всегда определить цель. Классифицируем, например, два простых предмета — игрушечную коробку и игрушечное колесо. Также нужен третий класс изображений — фон, где в кадре нет объектов.

Три класса: background, box и wheel

Сбор данных

Создадим классы, например, в алфавитном порядке:

  • Класс 1: background (фон)
  • Класс 2: box (коробка)
  • Класс 3: wheel (колесо)

Создание классов в интерфейсе Studio

Выберите один из классов и удерживайте зелёную кнопку под областью предпросмотра. Собранные изображения появятся на экране выборки (Image Samples).

Экран выборки изображений

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

Очистка набора данных от некорректных изображений

Соберите примерно по 50 изображений для каждого класса и перейдите к этапу обучения.

Обучение

Убедитесь, что выбрано правильное устройство (Grove Vision AI V2), и нажмите [Start Training].

Запуск обучения модели

Тестирование

После обучения можно посмотреть результат инференса.

Обратите внимание: модель при этом не работает на самом устройстве. Устройство только захватывает изображения, а предпросмотр выполняет обучаемая модель, которая запущена в Studio.

Предпросмотр инференса обучаемой модели в Studio

Теперь пора по-настоящему развернуть модель на устройстве.

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

Выберите обученную модель и нажмите [Deploy to device], затем выберите Grove Vision AI V2:

Выбор устройства для развёртывания модели

Studio перенаправит нас на вкладку Vision Workplace. Подтвердите развёртывание, выберите подходящий порт и подключитесь к нему:

Подтверждение развёртывания и выбор порта

Модель будет записана («прошита») в устройство. После автоматической перезагрузки модель начнёт работать на устройстве. В журнале устройства (Device Logger) видно, что задержка инференса составляет около 8 мс, что соответствует частоте кадров 125 кадров в секунду (FPS).

Также можно настроить уверенность модели (confidence).

Результат инференса на устройстве

Для работы модели классификации изображений Grove Vision AI V2 потреблял пиковый ток 80 мА при 5,24 В, что соответствует энергопотреблению 420 мВт.

Сохранение модели

Модель можно сохранить в SenseCraft AI Studio. Studio хранит все наши модели, и их можно развернуть позже. Для этого вернитесь на вкладку Training и нажмите кнопку [Save to SenseCraft]:

Сохранение модели в SenseCraft

Итоги

В этой лабораторной работе мы исследовали несколько приложений компьютерного зрения (CV) с помощью модуля Seeed Studio Grove Vision AI Module V2. Модуль показал себя как мощное, но компактное устройство, созданное специально для приложений встраиваемого машинного обучения.

Производительность. Grove Vision AI V2 показал отличные результаты в нескольких задачах компьютерного зрения. Благодаря чипу Himax WiseEye2 с двухъядерным Arm Cortex-M55 и встроенным блоком нейронных сетей Arm Ethos-U55 устройство обеспечило:

  • Классификация изображений: время инференса 15 мс (67 FPS);
  • Обнаружение объектов (человек): время инференса от 48 до 76 мс (от 21 до 13 FPS);
  • Детекция позы: 121 мс на обнаружение ключевых точек в реальном времени с отслеживанием 17 суставов (8 FPS).

Лидерство в энергоэффективности. Одно из самых убедительных преимуществ Grove Vision AI V2 — высокая энергоэффективность. Сравнительные тесты показали заметное превосходство над традиционными встраиваемыми платформами:

  • Grove Vision AI V2: пиковое потребление 80 мА (410 мВт) при более чем 60 FPS;
  • XIAO ESP32S3: при похожей задаче классификации изображений — 523 мВт при более чем 3 FPS.

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

Удобная экосистема для разработчиков. SenseCraft AI Studio с возможностями развёртывания без кода и интеграции в пользовательские приложения делает Grove Vision AI V2 доступным как новичкам, так и опытным разработчикам. Обширная библиотека предобученных моделей и поддержка собственных моделей обеспечивают гибкость для разных приложений.

Grove Vision AI V2 — заметный шаг вперёд в аппаратном обеспечении периферийного ИИ: он предлагает профессиональные возможности компьютерного зрения в компактном энергоэффективном корпусе, который делает развёртывание ИИ во встраиваемых приложениях доступным в промышленности, IoT и образовании.

Главные выводы

Эта лабораторная работа показывает, что сложные приложения компьютерного зрения не ограничены облачными решениями или энергоёмким оборудованием, таким как Raspberry Pi или Jetson Nano: сегодня их можно эффективно развернуть на периферии с впечатляющими эффективностью и производительностью.

При желании можно использовать XIAO Vision AI Camera. Это решение объединяет модуль Grove Vision AI V2, контроллер XIAO ESP32-C3 и камеру OV5647 в корпусе, напечатанном на 3D-принтере:

XIAO Vision AI Camera: модуль Grove Vision AI V2, контроллер XIAO ESP32-C3 и камера OV5647

Ресурсы

Обзор

Эти лабораторные работы дают бесценный практический опыт работы с системами машинного обучения на основе Raspberry Pi — гибкой и доступной платформы. В отличие от работы с крупномасштабными моделями, которым нужны обширные облачные ресурсы, эти упражнения позволяют напрямую работать с оборудованием и программным обеспечением в компактной, но мощной среде периферийных вычислений. Вы получите практическое представление о развёртывании ИИ на периферии, используя возможности Raspberry Pi: от экономичной Pi Zero до более мощных моделей Pi 4 и Pi 5. Такой подход даёт осязаемое понимание проблем и возможностей внедрения решений машинного обучения в условиях ограниченных ресурсов. Хотя мы работаем в меньшем масштабе, принципы и методы, которые вы освоите, принципиально похожи на те, что используются в более крупных системах. Способность Raspberry Pi запускать полноценную операционную систему и широкие возможности GPIO создают богатый учебный опыт, который соединяет теоретические знания с реальными приложениями. В ходе этих работ вы разберётесь в тонкостях EdgeML и приобретёте навыки, применимые к широкому спектру сценариев развёртывания ИИ.

Raspberry Pi Zero 2 W и Raspberry Pi 5 с камерой

Где купить

Платы Raspberry Pi доступны у официальных продавцов по всему миру:

  • Продукция Raspberry Pi (официальный сайт с поиском продавцов);
  • Raspberry Pi Zero 2 W: ~$15;
  • Raspberry Pi 4 (4 ГБ): ~$55;
  • Raspberry Pi 5 (4 ГБ): ~$60;
  • Raspberry Pi 5 (8 ГБ): ~$80.

Модули камер, блоки питания и SD-карты доступны у тех же продавцов.

Предварительные требования

  • Raspberry Pi: убедитесь, что у вас есть хотя бы одна из плат: Raspberry Pi Zero 2 W, Raspberry Pi 4 или 5 для лабораторных работ по зрению и Raspberry Pi 5 для лабораторных работ по генеративному ИИ.

  • Блок питания: для питания плат.

    • Raspberry Pi Zero 2 W: 5 В / 2,5 А через адаптер micro-USB;
    • Raspberry Pi 4: 5 В / 3 А через адаптер USB-C;
    • Raspberry Pi 5: 5 В / 5 А через адаптер USB-C.
  • Сеть: доступ в интернет для загрузки необходимого программного обеспечения и удалённого управления платами.

  • SD-карта (не менее 32 ГБ) и переходник для SD-карты: для операционной системы Raspberry Pi OS.

Настройка

Упражнения

МодальностьЗадачаОписаниеСсылка
ЗрениеКлассификация изображенийНаучитесь классифицировать изображенияОткрыть
ЗрениеОбнаружение объектовРеализуйте обнаружение объектовОткрыть
GenAIМалые языковые моделиРазвёртывание SLM на периферииОткрыть
GenAIВизуально-языковые моделиРазвёртывание VLM на периферииОткрыть

Классификация изображений

Лаборатория 1950-х: Raspberry Pi с камерой фотографирует маленького синего робота на верстаке
Промпт DALL·E: обложка для главы «Классификация изображений» руководства по Raspberry Pi, выполненная в том же винтажном стиле лаборатории электроники 1950-х, что и предыдущие обложки. На сцене — Raspberry Pi, подключённая к модулю камеры, которая снимает фотографию маленького синего робота, предоставленного пользователем. Робот стоит на верстаке, вокруг — классические лабораторные инструменты: паяльники, резисторы и провода. На фоне — старинное оборудование: осциллографы и ламповые радиоприёмники, сохраняющие детальное и ностальгическое ощущение эпохи. Логотипов и текста быть не должно.

Обзор

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

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

Применение в реальных сценариях

Классификация изображений нашла применение во многих реальных областях и меняет целые сектора:

  • Здравоохранение: помощь в анализе медицинских изображений, например в выявлении отклонений на рентгеновских снимках или МРТ.
  • Сельское хозяйство: контроль состояния посевов и выявление болезней растений по снимкам с воздуха.
  • Автомобилестроение: системы помощи водителю и беспилотные автомобили распознают дорожные знаки, пешеходов и другие автомобили.
  • Розничная торговля: визуальный поиск товаров и автоматизированное управление складскими запасами.
  • Безопасность и наблюдение: повышение эффективности обнаружения угроз и систем распознавания лиц.
  • Мониторинг окружающей среды: анализ спутниковых снимков для изучения вырубки лесов, городского планирования и исследований изменения климата.

Преимущества классификации на периферийных устройствах, таких как Raspberry Pi

Реализация классификации изображений на периферийных устройствах, таких как Raspberry Pi, даёт несколько весомых преимуществ:

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

  2. Работа без сети. Классификация выполняется без подключения к интернету, что удобно в удалённых местах или там, где связь ненадёжна.

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

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

  5. Масштабируемость. Возможны распределённые архитектуры, в которых несколько устройств работают независимо или в составе сети.

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

  7. Настраиваемость. Развёртывать специализированные или часто обновляемые модели, подогнанные под конкретные задачи, проще.

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

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

Настройка окружения

Обновление Raspberry Pi

Сначала убедитесь, что Raspberry Pi обновлён:

sudo apt update
sudo apt upgrade -y

Установка необходимых библиотек

Установите библиотеки, необходимые для обработки изображений и машинного обучения:

sudo apt install python3-pip
sudo rm /usr/lib/python3.11/EXTERNALLY-MANAGED
pip3 install --upgrade pip

Настройка виртуального окружения (необязательно, но рекомендуется)

Создайте виртуальное окружение для управления зависимостями:

python3 -m venv ~/tflite
source ~/tflite/bin/activate

Установка TensorFlow Lite

Нас интересует инференс — выполнение модели TensorFlow Lite на устройстве для получения предсказаний по входным данным. Чтобы выполнить инференс с моделью TensorFlow Lite, её нужно запустить через интерпретатор. Интерпретатор TensorFlow Lite спроектирован лёгким и быстрым. Он использует статический порядок вычислений графа и собственный (менее динамичный) распределитель памяти, чтобы минимизировать задержки загрузки, инициализации и выполнения.

Мы будем использовать среду выполнения TensorFlow Lite для Raspberry Pi — упрощённую библиотеку для запуска моделей машинного обучения на мобильных и встраиваемых устройствах без установки всех пакетов TensorFlow.

pip install tflite_runtime --no-deps

Установлено колесо (wheel): tflite_runtime-2.14.0-cp311-cp311-manylinux_2_34_aarch64.whl

Установка дополнительных библиотек Python

Установите библиотеки Python, необходимые для классификации изображений:

Если у вас установлена другая версия NumPy, сначала удалите её.

pip3 uninstall numpy

Установите версию 1.23.2, которая совместима с tflite_runtime.

 pip3 install numpy==1.23.2
pip3 install Pillow matplotlib

Создание рабочего каталога

Если вы работаете на Raspi-Zero с минимальной ОС (без рабочего стола), у вас может не быть заранее созданной структуры каталогов пользователя (проверить её можно командой ls). Поэтому создадим её:

mkdir Documents
cd Documents/
mkdir TFLITE
cd TFLITE/
mkdir IMG_CLASS
cd IMG_CLASS
mkdir models
cd models

На Raspi-5 каталог /Documents должен уже существовать.

Получите предобученную модель классификации изображений:

Подходящая предобученная модель — ключ к успешной классификации изображений на устройствах с ограниченными ресурсами, таких как Raspberry Pi. MobileNet создана для мобильного и встраиваемого зрения и хорошо балансирует точность и скорость. Существуют версии MobileNetV1, MobileNetV2 и MobileNetV3. Загрузим версию V2:

# Одна длинная строка, разбитая обратным слешем \
wget https://storage.googleapis.com/download.tensorflow.org/\
models/tflite_11_05_08/mobilenet_v2_1.0_224_quant.tgz

tar xzf mobilenet_v2_1.0_224_quant.tgz

Получите также её метки классов:

wget https://raw.githubusercontent.com/Mjrovai/EdgeML-with-Raspberry-Pi/refs/heads/\
main/IMG_CLASS/models/labels.txt

В итоге в каталоге с моделями должны появиться файлы:

Содержимое каталога models после загрузки модели MobileNet

Нам понадобятся только файл mobilenet_v2_1.0_224_quant.tflite и labels.txt. Остальные файлы можно удалить.

Настройка Jupyter Notebook (необязательно)

Если вы предпочитаете разрабатывать в Jupyter Notebook:

pip3 install jupyter
jupyter notebook --generate-config

Чтобы запустить Jupyter Notebook, выполните команду (замените IP-адрес на свой):

jupyter notebook --ip=192.168.4.210 --no-browser

В терминале появится локальный URL-адрес, по которому можно открыть блокнот:

Вывод терминала с локальным адресом Jupyter Notebook и токеном

К нему можно обратиться с другого устройства, введя в веб-браузере IP-адрес Raspberry Pi и токен из терминала (токен можно скопировать из терминала).

Создание нового блокнота Python 3 в Jupyter

Определите рабочий каталог на Raspi и создайте новый блокнот Python 3.

Проверка установки

Проверьте установку, запустив простой скрипт Python:

import tflite_runtime.interpreter as tflite
import numpy as np
from PIL import Image

print("NumPy:", np.__version__)
print("Pillow:", Image.__version__)

# Пробуем создать интерпретатор TFLite
model_path = "./models/mobilenet_v2_1.0_224_quant.tflite"
interpreter = tflite.Interpreter(model_path=model_path)
interpreter.allocate_tensors()
print("TFLite Interpreter created successfully!")

Скрипт можно создать в nano прямо в терминале и сохранить комбинацией CTRL+O, ENTER, затем CTRL+X:

Редактор nano с тестовым скриптом

И запустить командой:

Результат запуска тестового скрипта в терминале

Или запустите его прямо в блокноте:

Результат проверки установки в блокноте

Инференс с MobileNet V2

В предыдущем разделе мы настроили окружение, в том числе загрузили популярную предобученную модель MobileNet V2. Она обучена на изображениях ImageNet размером (1,2 миллиона) для 1 001 класса (1 000 категорий объектов и 1 фоновый класс). Модель преобразована в компактный формат TensorFlow Lite размером 3,5 МБ, что делает её подходящей для Raspberry Pi с её ограниченными объёмами памяти и накопителя.

Модель MobileNet V2 в формате TensorFlow Lite

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

Импортируйте нужные библиотеки:

import time
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
import tflite_runtime.interpreter as tflite

Загрузите модель TFLite и выделите тензоры:

model_path = "./models/mobilenet_v2_1.0_224_quant.tflite"
interpreter = tflite.Interpreter(model_path=model_path)
interpreter.allocate_tensors()

Получите входные и выходные тензоры.

input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()

Входные данные (input details) сообщают, как модели нужно подавать изображение. Форма (1, 224, 224, 3) означает, что изображение размером подаётся по одному (размерность батча: 1).

Вывод входных данных модели (input details)

Выходные данные (output details) показывают, что инференс даст массив из 1 001 целого значения. Каждое значение — вероятность того, что соответствующая метка относится к изображению.

Вывод выходных данных модели (output details)

Проверим тип данных (dtype) входных данных модели:

input_dtype = input_details[0]["dtype"]
input_dtype
dtype('uint8')

Это означает, что на вход должны подаваться сырые пиксели (0–255).

Получим тестовое изображение. Его можно перенести с вашего компьютера или скачать для проверки. Сначала создадим папку в рабочем каталоге:

mkdir images
cd images
wget https://upload.wikimedia.org/wikipedia/commons/3/3a/Cat03.jpg

Загрузим и выведем изображение:

# Загружаем изображение
img_path = "./images/Cat03.jpg"
img = Image.open(img_path)

# Выводим изображение
plt.figure(figsize=(8, 8))
plt.imshow(img)
plt.title("Original Image")
plt.show()

Исходное тестовое изображение кошки

Размер изображения можно узнать командой:

width, height = img.size

Видно, что это RGB-изображение шириной 1600 и высотой 1600 пикселей. Чтобы использовать модель, его нужно привести к размеру (224, 224, 3) и добавить размерность батча 1, как указано во входных данных: (1, 224, 224, 3). Результатом инференса, как показано в выходных данных, будет массив размером 1001, как видно ниже:

Преобразование размера изображения и формы входных данных

Изменим размер изображения, добавим размерность батча и посмотрим результат:

img = img.resize(
    (input_details[0]["shape"][1], input_details[0]["shape"][2])
)
input_data = np.expand_dims(img, axis=0)
input_data.shape

Форма input_data соответствует ожидаемой: (1, 224, 224, 3).

Проверим тип данных входного массива:

input_data.dtype
dtype('uint8')

Тип данных uint8 совпадает с типом, который ожидает модель.

Используем input_data, запустим интерпретатор и получим предсказания (выход):

interpreter.set_tensor(input_details[0]["index"], input_data)
interpreter.invoke()
predictions = interpreter.get_tensor(output_details[0]["index"])[0]

Предсказание — это массив из 1001 элемента. Получим Top-5 индексов с наибольшими значениями:

top_k_results = 5
top_k_indices = np.argsort(predictions)[::-1][:top_k_results]
top_k_indices

top_k_indices — массив из 5 элементов: array([283, 286, 282, 288, 479])

Итак, 283, 286, 282, 288 и 479 — наиболее вероятные классы изображения. Зная индекс, нужно понять, какому классу он соответствует (например, автомобиль, кошка или собака). Текстовый файл, загруженный вместе с моделью, содержит метку для каждого индекса от 0 до 1000. Используем функцию, чтобы загрузить .txt-файл как список:

def load_labels(filename):
    with open(filename, "r") as f:
        return [line.strip() for line in f.readlines()]

Получим список и выведем метки, соответствующие индексам:

labels_path = "./models/labels.txt"
labels = load_labels(labels_path)

print(labels[286])
print(labels[283])
print(labels[282])
print(labels[288])
print(labels[479])

В результате получаем:

Egyptian cat
tiger cat
tabby
lynx
carton

Как минимум четыре верхних индекса относятся к кошачьим. Предсказание — это вероятность, связанная с каждой из меток. Как мы видели в выходных данных, эти значения квантованы, поэтому их нужно деквантовать и применить softmax:

scale, zero_point = output_details[0]["quantization"]
dequantized_output = (
    predictions.astype(np.float32) - zero_point
) * scale
exp_output = np.exp(dequantized_output - np.max(dequantized_output))
probabilities = exp_output / np.sum(exp_output)

Выведем вероятности для топ-5:

print(probabilities[286])
print(probabilities[283])
print(probabilities[282])
print(probabilities[288])
print(probabilities[479])
0.27741462
0.3732285
0.16919471
0.10319158
0.023410844

Для наглядности создадим функцию, которая сопоставляет метки с вероятностями:

for i in range(top_k_results):
    print(
        "\t{:20}: {}%".format(
            labels[top_k_indices[i]],
            (int(probabilities[top_k_indices[i]] * 100)),
        )
    )
tiger cat           : 37%
Egyptian cat        : 27%
tabby               : 16%
lynx                : 10%
carton              : 2%

Определение общей функции классификации изображений

Создадим общую функцию: на вход подаётся изображение, на выходе получаем Top-5 возможных классов:

def image_classification(
    img_path, model_path, labels, top_k_results=5
):
    # Загружаем изображение
    img = Image.open(img_path)
    plt.figure(figsize=(4, 4))
    plt.imshow(img)
    plt.axis("off")

    # Загружаем модель TFLite
    interpreter = tflite.Interpreter(model_path=model_path)
    interpreter.allocate_tensors()

    # Получаем входные и выходные тензоры
    input_details = interpreter.get_input_details()
    output_details = interpreter.get_output_details()

    # Предобработка
    img = img.resize(
        (input_details[0]["shape"][1], input_details[0]["shape"][2])
    )
    input_data = np.expand_dims(img, axis=0)

    # Инференс на Raspi-Zero
    interpreter.set_tensor(input_details[0]["index"], input_data)
    interpreter.invoke()

    # Получаем результаты и сопоставляем их с классами
    predictions = interpreter.get_tensor(output_details[0]["index"])[
        0
    ]

    # Получаем индексы top-k результатов
    top_k_indices = np.argsort(predictions)[::-1][:top_k_results]

    # Получаем параметры квантования
    scale, zero_point = output_details[0]["quantization"]

    # Деквантуем выход и применяем softmax
    dequantized_output = (
        predictions.astype(np.float32) - zero_point
    ) * scale
    exp_output = np.exp(
        dequantized_output - np.max(dequantized_output)
    )
    probabilities = exp_output / np.sum(exp_output)

    print("\n\t[PREDICTION]        [Prob]\n")
    for i in range(top_k_results):
        print(
            "\t{:20}: {}%".format(
                labels[top_k_indices[i]],
                (int(probabilities[top_k_indices[i]] * 100)),
            )
        )

Загрузив несколько изображений для проверки, получим:

Результаты работы общей функции классификации на нескольких тестовых изображениях

Тестирование модели, обученной с нуля

Получим модель TFLite, обученную с нуля. Для этого следуйте блокноту:

CNN для классификации набора данных Cifar-10

В блокноте мы обучили модель на наборе данных CIFAR-10, который содержит 60 000 изображений 10 классов CIFAR (самолёт, автомобиль, птица, кошка, олень, собака, лягушка, лошадь, корабль и грузовик). Изображения CIFAR — цветные, размером (3 цветовых канала). Объекты на них не центрированы и могут находиться на фоне: например, самолёты могут быть на фоне облачного неба! Короче говоря, это маленькие, но настоящие изображения.

Обученная CNN-модель (cifar10_model.keras) занимала 2,0 МБ. С помощью TFLite Converter модель cifar10.tflite уменьшилась до 674 КБ (около трети исходного размера).

Размеры модели CIFAR-10 в формате Keras и TensorFlow Lite

В блокноте Cifar 10 — классификация изображений на Raspi с TFLite, который можно запустить на Raspi, мы повторим те же шаги, что и с mobilenet_v2_1.0_224_quant.tflite. Ниже приведены примеры классификации изображений с помощью общей функции классификации на Raspi-Zero, как в предыдущем разделе.

Результаты классификации CIFAR-10 на Raspi-Zero

Установка Picamera2

Picamera2 — библиотека Python для работы с камерой Raspberry Pi. Она основана на стеке камер libcamera и поддерживается фондом Raspberry Pi. Picamera2 работает на всех моделях Raspberry Pi — от Pi Zero до RPi 5. На Raspi она уже установлена в системе, но её нужно сделать доступной внутри виртуального окружения.

  1. Сначала активируйте виртуальное окружение, если оно ещё не активировано:

    source ~/tflite/bin/activate
    
  2. Теперь создайте файл .pth в виртуальном окружении, чтобы добавить путь к системным site-packages:

    echo "/usr/lib/python3/dist-packages" > \
      $VIRTUAL_ENV/lib/python3.11/site-packages/system_site_packages.pth
    

    Примечание: если у вас другая версия Python, замените python3.11 на нужную версию.

  3. После создания файла попробуйте импортировать picamera2 в Python:

    python3
    >>> import picamera2
    >>> print(picamera2.__file__)
    

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

/home/username/tflite/lib/python3.11/site-packages/\
picamera2/__init__.py

Также можно вывести список доступных в системе камер:

>>> print(Picamera2.global_camera_info())

В моём случае, с подключённой USB-камерой, получилось:

Список камер, найденных Picamera2

Теперь, когда мы убедились, что picamera2 работает в окружении с index 0, попробуем простой скрипт Python, который захватывает изображение с USB-камеры:

from picamera2 import Picamera2
import time

# Инициализируем камеру
picam2 = Picamera2()  # по умолчанию индекс 0

# Настраиваем камеру
config = picam2.create_still_configuration(main={"size": (640, 480)})
picam2.configure(config)

# Запускаем камеру
picam2.start()

# Даём камере прогреться
time.sleep(2)

# Делаем снимок
picam2.capture_file("usb_camera_image.jpg")
print("Image captured and saved as 'usb_camera_image.jpg'")

# Останавливаем камеру
picam2.stop()

Используйте редактор Nano, Jupyter Notebook или любой другой редактор. Сохраните код как Python-скрипт (например, capture_image.py) и запустите его. Скрипт захватит изображение с камеры и сохранит его как «usb_camera_image.jpg» в том же каталоге, где находится скрипт.

Запуск скрипта захвата изображения в терминале

Если Jupyter открыт, захваченное изображение можно увидеть прямо на своём компьютере. Иначе перенесите файл с Raspi на компьютер.

Захваченное изображение с USB-камеры

Если вы работаете с Raspi-5 и у вас полноценный рабочий стол, можно открыть файл прямо на устройстве.

Проект классификации изображений

Теперь разработаем полноценный проект классификации изображений с использованием Edge Impulse Studio. Как и в случае с MobileNet V2, для инференса будет использоваться обученная и сконвертированная модель TFLite.

Цель

Первый шаг в любом проекте машинного обучения — определить цель. В данном случае нужно обнаружить и классифицировать два конкретных объекта на одном изображении. Для проекта мы возьмём две небольшие игрушки: робота и маленького бразильского попугая (по имени Periquito). Также соберём изображения фона, на котором этих двух объектов нет.

Робот и игрушечный попугай Periquito, которые будут классифицироваться

Сбор данных

Определив цель проекта машинного обучения, мы переходим к следующему, самому важному шагу — сбору набора данных. Снимки можно делать с телефона, но здесь мы будем использовать Raspi. Настроим простой веб-сервер на Raspberry Pi, чтобы просматривать в браузере снимки в формате QVGA (320 x 240).

  1. Сначала установите Flask, лёгкий веб-фреймворк для Python:

    pip3 install flask
    
  2. Создадим новый скрипт Python, который объединяет захват изображений и веб-сервер. Назовём его get_img_data.py:

from flask import (Flask, Response, render_template_string,
                   request, redirect, url_for)
from picamera2 import Picamera2
import io
import threading
import time
import os
import signal

app = Flask(__name__)

# Глобальные переменные
base_dir = "dataset"
picam2 = None
frame = None
frame_lock = threading.Lock()
capture_counts = {}
current_label = None
shutdown_event = threading.Event()

def initialize_camera():
    global picam2
    picam2 = Picamera2()
    config = picam2.create_preview_configuration(
             main={"size": (320, 240)}
    )
    picam2.configure(config)
    picam2.start()
    time.sleep(2)  # Ждём, пока камера прогреется

def get_frame():
    global frame
    while not shutdown_event.is_set():
        stream = io.BytesIO()
        picam2.capture_file(stream, format='jpeg')
        with frame_lock:
            frame = stream.getvalue()
        time.sleep(0.1)  # При необходимости измените для плавного предпросмотра

def generate_frames():
    while not shutdown_event.is_set():
        with frame_lock:
            if frame is not None:
                yield (b'--frame\r\n'
                       b'Content-Type: image/jpeg\r\n\r\n' +
                                       frame + b'\r\n')
        time.sleep(0.1)  # При необходимости измените для плавной передачи видео

def shutdown_server():
    shutdown_event.set()
    if picam2:
        picam2.stop()
    # Даём время остальным потокам завершиться
    time.sleep(2)
    # Отправляем SIGINT основному процессу
    os.kill(os.getpid(), signal.SIGINT)

@app.route('/', methods=['GET', 'POST'])
def index():
    global current_label
    if request.method == 'POST':
        current_label = request.form['label']
        if current_label not in capture_counts:
            capture_counts[current_label] = 0
        os.makedirs(os.path.join(base_dir, current_label),
                                 exist_ok=True)
        return redirect(url_for('capture_page'))
    return render_template_string('''
        <!DOCTYPE html>
        <html>
        <head>
            <title>Dataset Capture - Label Entry</title>
        </head>
        <body>
            <h1>Enter Label for Dataset</h1>
            <form method="post">
                <input type="text" name="label" required>
                <input type="submit" value="Start Capture">
            </form>
        </body>
        </html>
    ''')

@app.route('/capture')
def capture_page():
    return render_template_string('''
        <!DOCTYPE html>
        <html>
        <head>
            <title>Dataset Capture</title>
            <script>
                var shutdownInitiated = false;
                function checkShutdown() {
                    if (!shutdownInitiated) {
                        fetch('/check_shutdown')
                            .then(response => response.json())
                            .then(data => {
                                if (data.shutdown) {
                                    shutdownInitiated = true;
                                    document.getElementById(
                                          'video-feed').src = '';
                                    document.getElementById(
                                          'shutdown-message')
                                    .style.display = 'block';
                                }
                            });
                    }
                }
                setInterval(checkShutdown, 1000); // Проверка каждую секунду
            </script>
        </head>
        <body>
            <h1>Dataset Capture</h1>
            <p>Current Label: {{ label }}</p>
            <p>Images captured for this label: {{ capture_count
                                                  }}</p>
            <img id="video-feed" src="{{ url_for('video_feed')
                                         }}" width="640"
            height="480" />
            <div id="shutdown-message" style="display: none;
                                              color: red;">
                Capture process has been stopped.
                You can close this window.
            </div>
            <form action="/capture_image" method="post">
                <input type="submit" value="Capture Image">
            </form>
            <form action="/stop" method="post">
                <input type="submit" value="Stop Capture"
                style="background-color: #ff6666;">
            </form>
            <form action="/" method="get">
                <input type="submit" value="Change Label"
                style="background-color: #ffff66;">
            </form>
        </body>
        </html>
    ''', label=current_label, capture_count=capture_counts.get(
                                            current_label, 0))

@app.route('/video_feed')
def video_feed():
    return Response(generate_frames(),
                    mimetype='multipart/x-mixed-replace; boundary=frame')

@app.route('/capture_image', methods=['POST'])
def capture_image():
    global capture_counts
    if current_label and not shutdown_event.is_set():
        capture_counts[current_label] += 1
        timestamp = time.strftime("%Y%m%d-%H%M%S")
        filename = f"image_{timestamp}.jpg"
        full_path = os.path.join(base_dir, current_label,
                                 filename)

        picam2.capture_file(full_path)

    return redirect(url_for('capture_page'))

@app.route('/stop', methods=['POST'])
def stop():
    summary = render_template_string('''
        <!DOCTYPE html>
        <html>
        <head>
            <title>Dataset Capture - Stopped</title>
        </head>
        <body>
            <h1>Dataset Capture Stopped</h1>
            <p>The capture process has been stopped.
               You can close this window.</p>
            <p>Summary of captures:</p>
            <ul>
            {% for label, count in capture_counts.items() %}
                <li>{{ label }}: {{ count }} images</li>
            {% endfor %}
            </ul>
        </body>
        </html>
    ''', capture_counts=capture_counts)

    # Запускаем новый поток для остановки сервера
    threading.Thread(target=shutdown_server).start()

    return summary

@app.route('/check_shutdown')
def check_shutdown():
    return {'shutdown': shutdown_event.is_set()}

if __name__ == '__main__':
    initialize_camera()
    threading.Thread(target=get_frame, daemon=True).start()
    app.run(host='0.0.0.0', port=5000, threaded=True)
  1. Запустите скрипт:
python3 get_img_data.py
  1. Откройте веб-интерфейс:

    • На самой Raspberry Pi (если есть графический интерфейс): откройте веб-браузер и перейдите по адресу http://localhost:5000.
    • С другого устройства в той же сети: откройте веб-браузер и перейдите по адресу http://<raspberry_pi_ip>:5000 (замените <raspberry_pi_ip> на IP-адрес вашей Raspberry Pi). Например: http://192.168.4.210:5000/

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

Основные возможности

  1. Веб-интерфейс: доступ с любого устройства в той же сети, что и Raspberry Pi.
  2. Живой предпросмотр с камеры: показывает видео с камеры в реальном времени.
  3. Система меток: позволяет вводить метки для разных категорий изображений.
  4. Организованное хранение: автоматически сохраняет изображения в подкаталогах по меткам.
  5. Счётчики по меткам: ведут учёт количества снимков для каждой метки.
  6. Итоговая статистика: выводит сводку по снимкам при остановке захвата.

Основные компоненты

  1. Веб-приложение Flask: обрабатывает маршруты и отдаёт веб-интерфейс.
  2. Интеграция с Picamera2: управляет камерой Raspberry Pi.
  3. Потоковый захват кадров: обеспечивает плавный предпросмотр.
  4. Управление файлами: раскладывает снимки по каталогам с метками.

Ключевые функции

  • initialize_camera(): настраивает экземпляр Picamera2.
  • get_frame(): непрерывно захватывает кадры для предпросмотра.
  • generate_frames(): отдаёт кадры для живой видеотрансляции.
  • shutdown_server(): устанавливает событие остановки, останавливает камеру и выключает сервер Flask.
  • index(): обрабатывает страницу ввода метки.
  • capture_page(): выводит основной интерфейс захвата.
  • video_feed(): показывает живой предпросмотр для наведения камеры.
  • capture_image(): сохраняет изображение с текущей меткой.
  • stop(): останавливает процесс захвата и выводит сводку.

Порядок работы

  1. Запустите скрипт на Raspberry Pi.
  2. Откройте веб-интерфейс в браузере.
  3. Введите метку для изображений, которые хотите снять, и нажмите Start Capture.

Экран ввода метки в веб-интерфейсе захвата

  1. Используйте живой предпросмотр, чтобы навести камеру.
  2. Нажмите Capture Image, чтобы сохранить изображения под текущей меткой.

Кнопка Capture Image в веб-интерфейсе

  1. При необходимости меняйте метки для разных категорий, выбирая Change Label.
  2. Нажмите Stop Capture, когда закончите, чтобы увидеть сводку.

Сводка захвата после нажатия Stop Capture

Технические заметки

  • Скрипт использует потоки (threading), чтобы одновременно захватывать кадры и обслуживать веб-страницы.
  • Имена файлов содержат временную метку, чтобы каждое имя было уникальным.
  • Веб-интерфейс адаптивен и доступен с мобильных устройств.

Возможности настройки

  • Измените разрешение изображения в функции initialize_camera(). Здесь мы использовали QVGA .
  • Измените HTML-шаблоны, чтобы поменять внешний вид.
  • Добавьте дополнительные шаги обработки или анализа изображений в функции capture_image().

Количество образцов в наборе данных

Снимите около 60 изображений для каждой категории (periquito, robot и background). Старайтесь снимать с разных ракурсов, на разных фонах и при разном освещении. На Raspi в итоге появится папка dataset, которая содержит 3 подпапки: periquito, robot и background — по одной для каждого класса изображений.

Для переноса созданного набора данных на основной компьютер можно использовать Filezilla.

Обучение модели в Edge Impulse Studio

Для обучения модели мы будем использовать Edge Impulse Studio. Перейдите на страницу Edge Impulse, войдите в свой аккаунт и создайте новый проект:

Создание нового проекта в Edge Impulse Studio

Здесь можно клонировать похожий проект: Raspi - Img Class.

Набор данных

В Studio (EI Studio) мы пройдём четыре основных шага: Dataset (набор данных), Impulse, Tests (тесты) и Deploy (развёртывание на периферийном устройстве, в нашем случае на Raspi). Эти шаги помогают подготовить модель к работе на Raspi.

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

В Studio загрузите снятые данные, выполнив шаги:

  1. Откройте вкладку Data acquisition и в разделе UPLOAD DATA загрузите файлы с компьютера в выбранные категории.
  2. Оставьте разбиение исходного набора данных на обучающую и тестовую выборки за Studio и выберите метку (label) для каждой категории.
  3. Повторите процедуру для всех трёх классов. В итоге в Studio должны появиться ваши «сырые данные»:

Сырые данные трёх классов в разделе Data acquisition

Studio позволяет изучать данные, показывая полную картину всех данных проекта. Можно очищать, проверять и менять метки, нажимая на отдельные элементы данных. В нашем простом проекте данные выглядят корректными.

Обозреватель данных Data Explorer в Edge Impulse Studio

Проектирование Impulse

На этом этапе нужно определить, как:

  • предобработать данные: изменить размер отдельных изображений и выбрать глубину цвета (RGB или оттенки серого);
  • задать модель. В нашем случае это Transfer Learning (Images): дообучение предобученной модели классификации изображений MobileNet V2 на наших данных. Этот метод хорошо работает даже с относительно небольшими наборами изображений (около 180 изображений в нашем случае).

Перенос обучения (transfer learning) с MobileNet упрощает обучение модели, что особенно полезно в условиях ограниченных ресурсов и для проектов с небольшим числом размеченных данных. MobileNet, известная своей лёгкой архитектурой, — это предобученная модель, которая уже выучила полезные признаки на большом наборе данных (ImageNet).

Схема переноса обучения на основе MobileNet (часть 1)

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

Схема переноса обучения на основе MobileNet (часть 2)

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

Перейдите на вкладку Impulse Design и создайте impulse, задав размер изображения и сжатие (квадратная форма без обрезки). Выберите блоки Image и Transfer Learning. Сохраните Impulse.

Схема Impulse: блоки Image и Transfer Learning

Предобработка изображений

Все входные изображения QVGA/RGB565 будут преобразованы в 76 800 признаков .

Параметры предобработки изображений в Edge Impulse

Нажмите Save parameters и на следующей вкладке выберите Generate features.

Модель

MobileNet — семейство эффективных свёрточных нейронных сетей, созданных для мобильного и встраиваемого зрения. Ключевые особенности MobileNet:

  1. Лёгкость: оптимизирована для мобильных устройств и встраиваемых систем с ограниченными вычислительными ресурсами.
  2. Скорость: быстрый инференс, подходящий для приложений реального времени.
  3. Точность: сохраняет хорошую точность, несмотря на компактный размер.

MobileNetV2, представленная в 2018 году, улучшает исходную архитектуру MobileNet. Её ключевые особенности:

  1. Инвертированные остаточные блоки (Inverted Residuals): используются инвертированные остаточные структуры, в которых соединения-шорткаты связывают тонкие «бутылочные» слои (bottleneck).
  2. Линейные бутылочные слои (Linear Bottlenecks): в узких слоях удаляются нелинейности, чтобы не разрушать информацию.
  3. Разделяемые по глубине свёртки (depthwise separable convolutions): сохраняется эта эффективная операция из MobileNetV1.

В нашем проекте мы выполним Transfer Learning с моделью MobileNetV2 160x160 1.0. Это значит, что изображения для обучения (и для будущего инференса) должны иметь входной размер пикселей и множитель ширины (Width Multiplier) 1,0 (полная ширина, без уменьшения). Эта конфигурация сбалансирована по размеру модели, скорости и точности.

Обучение модели

Ещё одна полезная методика глубокого обучения — аугментация данных (Data Augmentation). Она повышает точность моделей машинного обучения за счёт создания дополнительных искусственных данных: система аугментации вносит небольшие случайные изменения в обучающие данные во время обучения (например, отражает, обрезает или поворачивает изображения).

Заглянем «под капот» и посмотрим, как Edge Impulse реализует политику аугментации данных:

# Реализует политику аугментации данных
def augment_image(image, label):
    # Случайно отражаем изображение
    image = tf.image.random_flip_left_right(image)

    # Увеличиваем изображение, затем случайно обрезаем его до
    # исходных размеров
    resize_factor = random.uniform(1, 1.2)
    new_height = math.floor(resize_factor * INPUT_SHAPE[0])
    new_width = math.floor(resize_factor * INPUT_SHAPE[1])
    image = tf.image.resize_with_crop_or_pad(
        image, new_height, new_width
    )
    image = tf.image.random_crop(image, size=INPUT_SHAPE)

    # Меняем яркость изображения
    image = tf.image.random_brightness(image, max_delta=0.2)

    return image, label

Такие вариации во время обучения помогают модели не срезать углы, «запоминая» поверхностные признаки в обучающих данных. Благодаря этому модель лучше отражает глубинные закономерности набора данных.

Последний полносвязный слой нашей модели будет содержать 0 нейронов с dropout 10 % для предотвращения переобучения. Вот результат обучения:

Результаты обучения модели в Edge Impulse Studio

Результат отличный: задержка около 35 мс (для Raspi-4), что должно дать около 30 кадров в секунду (fps) при инференсе. Raspi-Zero будет медленнее, а Raspi-5 — быстрее.

Компромисс между точностью и скоростью

Если нужен более быстрый инференс, модель следует обучать с меньшими значениями альфа (0,35; 0,5 и 0,75) или даже уменьшить размер входного изображения, пожертвовав частью точности. Однако уменьшение размера входного изображения и снижение альфа (множителя ширины) могут ускорить инференс MobileNet V2, и у них разные компромиссы. Сравним:

  1. Уменьшение размера входного изображения:

Плюсы:

  • Существенно снижает вычислительные затраты во всех слоях.
  • Уменьшает использование памяти.
  • Часто даёт заметный прирост скорости.

Минусы:

  • Может ухудшить способность модели обнаруживать мелкие признаки и детали.
  • Может заметно повлиять на точность, особенно в задачах, требующих тонкого распознавания.
  1. Уменьшение альфа (множителя ширины):

Плюсы:

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

Минусы:

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

Сравнение:

  1. Влияние на скорость:

    • Уменьшение входного размера часто даёт более заметный прирост скорости, поскольку снижает вычисления квадратично (уменьшение ширины и высоты вдвое сокращает вычисления примерно на 75 %).
    • Уменьшение альфа даёт более линейное снижение вычислений.
  2. Влияние на точность:

    • Уменьшение входного размера может сильно повлиять на точность, особенно при обнаружении мелких объектов или деталей.
    • Уменьшение альфа обычно влияет на точность более плавно.
  3. Архитектура модели:

    • Изменение входного размера не меняет архитектуру модели.
    • Изменение альфа меняет структуру модели, уменьшая количество каналов в каждом слое.

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

  1. Если приложению не нужно обнаруживать мелкие детали и оно может мириться с небольшой потерей точности, уменьшение входного размера — часто самый эффективный способ ускорить инференс.
  2. Уменьшение альфа предпочтительнее, если важно сохранить способность обнаруживать мелкие детали или нужен более сбалансированный компромисс между скоростью и точностью.
  3. Для лучшего результата поэкспериментируйте с обоими способами:
    • Попробуйте MobileNet V2 с входными размерами, например или ;
    • Поэкспериментируйте со значениями альфа 1,0; 0,75; 0,5 или 0,35.
  4. Всегда проверяйте разные конфигурации на своём оборудовании и на своём наборе данных, чтобы найти оптимальный баланс для вашей задачи.

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

Тестирование модели

Теперь возьмите набор данных, отложенный в начале проекта, и запустите на нём обученную модель. Результат снова отличный: 92,22 %.

Развёртывание модели

Как и в предыдущем разделе, модель можно развернуть в формате .tflite и запустить на Raspi с помощью Python.

На вкладке Dashboard перейдите к модели Transfer learning (int8 quantized) и нажмите значок загрузки:

Загрузка квантованной int8 модели с вкладки Dashboard

Для сравнения скачайте также версию float32.

Перенесите модель с компьютера на Raspi (в ./models), например с помощью FileZilla. Также сделайте несколько снимков для инференса (в ./images).

Импортируйте нужные библиотеки:

import time
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
import tflite_runtime.interpreter as tflite

Укажите пути и метки:

img_path = "./images/robot.jpg"
model_path = "./models/ei-raspi-img-class-int8-quantized-model.tflite"
labels = ["background", "periquito", "robot"]

Обратите внимание: модели, обученные в Edge Impulse Studio, выдают значения с индексами 0, 1, 2 и т. д., а метки следуют в алфавитном порядке.

Загрузите модель, выделите тензоры и получите информацию о входных и выходных тензорах:

# Загружаем модель TFLite
interpreter = tflite.Interpreter(model_path=model_path)
interpreter.allocate_tensors()

# Получаем входные и выходные тензоры
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()

Важное отличие: dtype входных данных модели теперь int8, то есть входные значения лежат в диапазоне от –128 до +127, тогда как каждый пиксель нашего изображения — от 0 до 255. Поэтому изображение нужно предобработать. Проверим:

input_dtype = input_details[0]["dtype"]
input_dtype
numpy.int8

Откроем изображение и выведем его:

img = Image.open(img_path)
plt.figure(figsize=(4, 4))
plt.imshow(img)
plt.axis("off")
plt.show()

Результат инференса для изображения робота

Выполним предобработку:

scale, zero_point = input_details[0]["quantization"]
img = img.resize(
    (input_details[0]["shape"][1], input_details[0]["shape"][2])
)
img_array = np.array(img, dtype=np.float32) / 255.0
img_array = (
    (img_array / scale + zero_point).clip(-128, 127).astype(np.int8)
)
input_data = np.expand_dims(img_array, axis=0)

Проверим входные данные, чтобы убедиться, что входной тензор соответствует ожиданиям модели:

input_data.shape, input_data.dtype
((1, 160, 160, 3), dtype('int8'))

Теперь выполним инференс. Также вычислим задержку модели:

# Инференс на Raspi-Zero
start_time = time.time()
interpreter.set_tensor(input_details[0]["index"], input_data)
interpreter.invoke()
end_time = time.time()
inference_time = (end_time - start_time) * 1000  # Переводим в миллисекунды
print("Inference time: {:.1f}ms".format(inference_time))

На инференс модель затратит около 125 мс на Raspi-Zero, то есть в 3–4 раза дольше, чем на Raspi-5.

Теперь получим метки и вероятности на выходе. Важно отметить, что модель, обученная в Edge Impulse Studio, имеет softmax на выходе (в отличие от исходной MobileNet V2), поэтому в качестве «вероятностей» нужно использовать сырой выход модели.

# Получаем результаты и сопоставляем их с классами
predictions = interpreter.get_tensor(output_details[0]["index"])[0]

# Получаем индексы top-k результатов
top_k_results = 3
top_k_indices = np.argsort(predictions)[::-1][:top_k_results]

# Получаем параметры квантования
scale, zero_point = output_details[0]["quantization"]

# Деквантуем выход
dequantized_output = (
    predictions.astype(np.float32) - zero_point
) * scale
probabilities = dequantized_output

print("\n\t[PREDICTION]        [Prob]\n")
for i in range(top_k_results):
    print(
        "\t{:20}: {:.2f}%".format(
            labels[top_k_indices[i]],
            probabilities[top_k_indices[i]] * 100,
        )
    )

Результат классификации: три наиболее вероятных класса

Изменим ранее созданную функцию так, чтобы она работала с моделями разных типов:

def image_classification(
    img_path, model_path, labels, top_k_results=3, apply_softmax=False
):
    # Загружаем изображение
    img = Image.open(img_path)
    plt.figure(figsize=(4, 4))
    plt.imshow(img)
    plt.axis("off")

    # Загружаем модель TFLite
    interpreter = tflite.Interpreter(model_path=model_path)
    interpreter.allocate_tensors()

    # Получаем входные и выходные тензоры
    input_details = interpreter.get_input_details()
    output_details = interpreter.get_output_details()

    # Предобработка
    img = img.resize(
        (input_details[0]["shape"][1], input_details[0]["shape"][2])
    )

    input_dtype = input_details[0]["dtype"]

    if input_dtype == np.uint8:
        input_data = np.expand_dims(np.array(img), axis=0)
    elif input_dtype == np.int8:
        scale, zero_point = input_details[0]["quantization"]
        img_array = np.array(img, dtype=np.float32) / 255.0
        img_array = (
            (img_array / scale + zero_point)
            .clip(-128, 127)
            .astype(np.int8)
        )
        input_data = np.expand_dims(img_array, axis=0)
    else:  # float32
        input_data = (
            np.expand_dims(np.array(img, dtype=np.float32), axis=0)
            / 255.0
        )

    # Инференс на Raspi-Zero
    start_time = time.time()
    interpreter.set_tensor(input_details[0]["index"], input_data)
    interpreter.invoke()
    end_time = time.time()
    inference_time = (end_time - start_time) * 1000  # Переводим в миллисекунды

    # Получаем результаты
    predictions = interpreter.get_tensor(output_details[0]["index"])[0]

    # Получаем индексы top-k результатов
    top_k_indices = np.argsort(predictions)[::-1][:top_k_results]

    # Обрабатываем выход в зависимости от его типа
    output_dtype = output_details[0]["dtype"]
    if output_dtype in [np.int8, np.uint8]:
        # Деквантуем выход
        scale, zero_point = output_details[0]["quantization"]
        predictions = (
            predictions.astype(np.float32) - zero_point
        ) * scale

    if apply_softmax:
        # Применяем softmax
        exp_preds = np.exp(predictions - np.max(predictions))
        probabilities = exp_preds / np.sum(exp_preds)
    else:
        probabilities = predictions

    print("\n\t[PREDICTION]        [Prob]\n")
    for i in range(top_k_results):
        print(
            "\t{:20}: {:.1f}%".format(
                labels[top_k_indices[i]],
                probabilities[top_k_indices[i]] * 100,
            )
        )
    print("\n\tInference time: {:.1f}ms".format(inference_time))

Протестируем её на разных изображениях с int8-квантованной моделью (160x160, alpha = 1,0):

Классификация изображений моделью int8 160×160, alpha = 1.0

Для проверки скачаем модель меньшего размера, например обученную для Nicla Vision Lab (int8-квантованная модель, , alpha = 0,1). Можно использовать ту же функцию:

Классификация изображений моделью int8 96×96, alpha = 0.1

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

Классификация изображений в реальном времени

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

Через nano в терминале сохраните приведённый ниже код, например как img_class_live_infer.py.

from flask import (Flask, Response, render_template_string,
                   request, jsonify)
from picamera2 import Picamera2
import io
import threading
import time
import numpy as np
from PIL import Image
import tflite_runtime.interpreter as tflite
from queue import Queue, Empty

app = Flask(__name__)

# Глобальные переменные
picam2 = None
frame = None
frame_lock = threading.Lock()
is_classifying = False
confidence_threshold = 0.8
model_path = "./models/ei-raspi-img-class-int8-quantized-model.tflite"
labels = ['background', 'periquito', 'robot']
interpreter = None
classification_queue = Queue(maxsize=1)

def initialize_camera():
    global picam2
    picam2 = Picamera2()
    config = picam2.create_preview_configuration(
        main={"size": (320, 240)}
    )
    picam2.configure(config)
    picam2.start()
    time.sleep(2)  # Ждём, пока камера прогреется

def get_frame():
    global frame
    while True:
        stream = io.BytesIO()
        picam2.capture_file(stream, format='jpeg')
        with frame_lock:
            frame = stream.getvalue()
        time.sleep(0.1)  # Захватываем кадры чаще

def generate_frames():
    while True:
        with frame_lock:
            if frame is not None:
                yield (
                   b'--frame\r\n'
                   b'Content-Type: image/jpeg\r\n\r\n'
                   + frame + b'\r\n'
                )
        time.sleep(0.1)

def load_model():
    global interpreter
    if interpreter is None:
        interpreter = tflite.Interpreter(model_path=model_path)
        interpreter.allocate_tensors()
    return interpreter

def classify_image(img, interpreter):
    input_details = interpreter.get_input_details()
    output_details = interpreter.get_output_details()

    img = img.resize((input_details[0]['shape'][1],
                      input_details[0]['shape'][2]))
    input_data = np.expand_dims(np.array(img), axis=0).astype(input_details[0]['dtype'])

    interpreter.set_tensor(input_details[0]['index'], input_data)
    interpreter.invoke()

    predictions = interpreter.get_tensor(output_details[0]
                                         ['index'])[0]
    # Обрабатываем выход в зависимости от его типа
    output_dtype = output_details[0]['dtype']
    if output_dtype in [np.int8, np.uint8]:
        # Деквантуем выход
        scale, zero_point = output_details[0]['quantization']
        predictions = (predictions.astype(np.float32) -
                       zero_point) * scale
    return predictions

def classification_worker():
    interpreter = load_model()
    while True:
        if is_classifying:
            with frame_lock:
                if frame is not None:
                    img = Image.open(io.BytesIO(frame))
            predictions = classify_image(img, interpreter)
            max_prob = np.max(predictions)
            if max_prob >= confidence_threshold:
                label = labels[np.argmax(predictions)]
            else:
                label = 'Uncertain'
            classification_queue.put({
                 'label': label,
                 'probability': float(max_prob)
            })
        time.sleep(0.1)  # При необходимости настройте

@app.route('/')
def index():
   return render_template_string('''
      <!DOCTYPE html>
      <html>
      <head>
          <title>Image Classification</title>
          <script
          	src="https://code.jquery.com/jquery-3.6.0.min.js">
          </script>
          <script>
              function startClassification() {
                  $.post('/start');
                  $('#startBtn').prop('disabled', true);
                  $('#stopBtn').prop('disabled', false);
              }
              function stopClassification() {
                  $.post('/stop');
                  $('#startBtn').prop('disabled', false);
                  $('#stopBtn').prop('disabled', true);
              }
              function updateConfidence() {
                  var confidence = $('#confidence').val();
                  $.post('/update_confidence',
                         {confidence: confidence}
                        );
              }
              function updateClassification() {
                  $.get('/get_classification', function(data) {
                    $('#classification').text(data.label + ': '
                    + data.probability.toFixed(2));
                  });
              }
              $(document).ready(function() {
                  setInterval(updateClassification, 100);
                  // Обновляем каждые 100 мс
              });
          </script>
      </head>
      <body>
          <h1>Image Classification</h1>
          <img src="{{ url_for('video_feed') }}"
               width="640"
               height="480" />

          <br>
          <button id="startBtn"
                  onclick="startClassification()">
            Start Classification
          </button>

          <button id="stopBtn"
                  onclick="stopClassification()"
                  disabled>
            Stop Classification
          </button>

          <br>
          <label for="confidence">Confidence Threshold:</label>
          <input type="number"
                 id="confidence"
                 name="confidence"
                 min="0" max="1"
                 step="0.1"
                 value="0.8"
                 onchange="updateConfidence()" />

          <br>
          <div id="classification">
             Waiting for classification...
          </div>

      </body>
      </html>
   ''')

@app.route('/video_feed')
def video_feed():
    return Response(
       generate_frames(),
       mimetype='multipart/x-mixed-replace; boundary=frame'
    )

@app.route('/start', methods=['POST'])
def start_classification():
    global is_classifying
    is_classifying = True
    return '', 204

@app.route('/stop', methods=['POST'])
def stop_classification():
    global is_classifying
    is_classifying = False
    return '', 204

@app.route('/update_confidence', methods=['POST'])
def update_confidence():
    global confidence_threshold
    confidence_threshold = float(request.form['confidence'])
    return '', 204

@app.route('/get_classification')
def get_classification():
    if not is_classifying:
        return jsonify({'label': 'Not classifying',
                       'probability': 0})
    try:
        result = classification_queue.get_nowait()
    except Empty:
        result = {'label': 'Processing', 'probability': 0}
    return jsonify(result)

if __name__ == '__main__':
    initialize_camera()
    threading.Thread(target=get_frame, daemon=True).start()
    threading.Thread(target=classification_worker,
                     daemon=True).start()
    app.run(host='0.0.0.0', port=5000, threaded=True)

В терминале выполните:

python3 img_class_live_infer.py

И откройте веб-интерфейс:

  • На самой Raspberry Pi (если есть графический интерфейс): откройте веб-браузер и перейдите по адресу http://localhost:5000.
  • С другого устройства в той же сети: откройте веб-браузер и перейдите по адресу http://<raspberry_pi_ip>:5000 (замените <raspberry_pi_ip> на IP-адрес вашей Raspberry Pi). Например: http://192.168.4.210:5000/

Вот несколько скриншотов приложения, запущенного на внешнем компьютере:

Веб-интерфейс классификации изображений в реальном времени

Видео работы приложения можно посмотреть на YouTube: ссылка на видео.

Код создаёт веб-приложение для классификации изображений в реальном времени на Raspberry Pi с модулем камеры и моделью TensorFlow Lite. Приложение использует Flask для веб-интерфейса, где можно видеть изображение с камеры и результаты классификации в реальном времени.

Ключевые компоненты

  1. Веб-приложение Flask: отдаёт пользовательский интерфейс и обрабатывает запросы.
  2. PiCamera2: захватывает изображения с модуля камеры Raspberry Pi.
  3. TensorFlow Lite: запускает модель классификации изображений.
  4. Потоки (Threading): управляют параллельными операциями для плавной работы.

Основные возможности

  • Вывод изображения с камеры в реальном времени
  • Классификация изображений в реальном времени
  • Настраиваемый порог уверенности
  • Запуск и остановка классификации по требованию

Структура кода

  1. Импорты и настройка:

    • Flask для веб-приложения
    • PiCamera2 для управления камерой
    • TensorFlow Lite для инференса
    • Threading и Queue для параллельных операций
  2. Глобальные переменные:

    • Управление камерой и кадрами
    • Управление классификацией
    • Информация о модели и метках
  3. Функции камеры:

    • initialize_camera(): настраивает PiCamera2
    • get_frame(): непрерывно захватывает кадры
    • generate_frames(): отдаёт кадры для веб-трансляции
  4. Функции модели:

    • load_model(): загружает модель TFLite
    • classify_image(): выполняет инференс для одного изображения
  5. Рабочий поток классификации:

    • Работает в отдельном потоке
    • Непрерывно классифицирует кадры, пока включена классификация
    • Обновляет очередь последними результатами
  6. Маршруты Flask:

    • /: отдаёт главную HTML-страницу
    • /video_feed: передаёт видеопоток с камеры
    • /start и /stop: управляют классификацией
    • /update_confidence: меняет порог уверенности
    • /get_classification: возвращает последний результат классификации
  7. HTML-шаблон:

    • Показывает изображение с камеры и результаты классификации
    • Содержит элементы управления запуском, остановкой и настройками
  8. Главный блок выполнения:

    • Инициализирует камеру и запускает нужные потоки
    • Запускает приложение Flask

Ключевые понятия

  1. Параллельные операции: потоки обрабатывают захват с камеры и классификацию отдельно от веб-сервера.
  2. Обновления в реальном времени: результаты классификации обновляются часто, без перезагрузки страницы.
  3. Повторное использование модели: модель TFLite загружается один раз и используется повторно, что эффективно.
  4. Гибкая настройка: пользователь может менять порог уверенности на лету.

Использование

  1. Убедитесь, что все зависимости установлены.
  2. Запустите скрипт на Raspberry Pi с модулем камеры.
  3. Откройте веб-интерфейс в браузере, используя IP-адрес Raspberry Pi.
  4. Запустите классификацию и при необходимости настройте параметры.

Итоги

Классификация изображений стала мощным и универсальным применением машинного обучения с важными последствиями для самых разных областей — от здравоохранения до мониторинга окружающей среды. В этой главе показано, как реализовать надёжную систему классификации изображений на периферийных устройствах, таких как Raspi-Zero и Raspi-5. Это демонстрирует потенциал интеллекта в реальном времени прямо на устройстве.

Мы прошли весь конвейер проекта классификации изображений: от сбора данных и обучения модели в Edge Impulse Studio до развёртывания и запуска инференса на Raspi. В процессе выделилось несколько ключевых моментов:

  1. Важность правильного сбора данных и предобработки для обучения эффективных моделей.
  2. Сила переноса обучения: можно использовать предобученные модели, такие как MobileNet V2, для эффективного обучения на ограниченных данных.
  3. Компромиссы между точностью модели и скоростью инференса, особенно важные для периферийных устройств.
  4. Реализация классификации в реальном времени через веб-интерфейс, что демонстрирует практическое применение.

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

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

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

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

Ресурсы

Малые языковые модели (SLM)

Мультфильм в стиле 1950-х: Raspberry Pi запускает малую языковую модель на периферийном устройстве
Промпт DALL·E: иллюстрация в стиле мультфильма 1950-х, на которой Raspberry Pi запускает малую языковую модель на периферийном устройстве. Raspberry Pi оформлена в ретрофутуристическом стиле, с закруглёнными краями и хромированными деталями, и подключена к игривым мультяшным датчикам и устройствам. Вокруг плывут речевые пузыри, символизирующие обработку языка, а фон представляет собой причудливый пейзаж из связанных между собой устройств, проводов и маленьких гаджетов, нарисованных в винтажном мультяшном стиле. Цветовая палитра использует мягкие пастельные тона и жирные контуры, типичные для мультфильмов 1950-х, что придаёт сцене весёлую и ностальгическую атмосферу.

Обзор

В быстро развивающейся области искусственного интеллекта периферийные вычисления открывают возможность перенести функции, которые традиционно доступны только мощным централизованным серверам, ближе к источнику данных. Эта лабораторная работа посвящена практической интеграции небольших версий традиционных больших языковых моделей (LLM) в Raspberry Pi 5. Так это периферийное устройство превращается в ИИ-узел, способный обрабатывать данные на месте в реальном времени.

По мере того как большие языковые модели растут в размере и сложности, малые языковые модели (SLM) становятся привлекательной альтернативой для периферийных устройств: они обеспечивают баланс между производительностью и эффективным использованием ресурсов. Запуская такие модели непосредственно на Raspberry Pi, мы можем создавать отзывчивые приложения с защитой приватности, которые работают даже там, где интернет ограничен или отсутствует.

Эта лабораторная работа проведёт вас через установку, оптимизацию и использование SLM на Raspberry Pi. Мы изучим установку и применение Ollama — открытой платформы, которая позволяет запускать LLM локально на наших машинах: настольных компьютерах или периферийных устройствах, таких как Raspberry Pi или NVIDIA Jetson. Ollama спроектирована эффективной, масштабируемой и простой в использовании, поэтому подходит для развёртывания моделей ИИ, например Microsoft Phi, Google Gemma, Meta Llama и LLaVA (мультимодальная модель). Мы интегрируем некоторые из этих моделей в проекты с помощью экосистемы Python и изучим их потенциал в реальных сценариях (или хотя бы наметим такое направление).

Пример работы малой языковой модели

Настройка

В предыдущих лабораторных работах можно было использовать любую модель Raspi, но здесь выбор — Raspberry Pi 5 (Raspi-5). Это надёжная платформа, которая заметно превосходит предыдущую версию 4. Она оснащена процессором Broadcom BCM2712 — 64-битным четырёхъядерным Arm Cortex-A76 с частотой 2,4 ГГц, с криптографическим расширением и улучшенным кэшированием. В ней есть GPU VideoCore VII, два выхода HDMI® с разрешением 4Kp60 с поддержкой HDR и декодер HEVC 4Kp60. Доступны варианты памяти 4 ГБ и 8 ГБ высокоскоростной LPDDR4X SDRAM; для запуска SLM мы выбираем 8 ГБ. Также есть расширяемое хранилище через слот microSD и интерфейс PCIe 2.0 для быстрой периферии, например твердотельных накопителей M.2 SSD.

Для реальных приложений SLM SSD — лучший выбор, чем SD-карты.

Кстати, как рассказал Alasdair Allan, инференс непосредственно на CPU Raspberry Pi 5 — без ускорения на GPU — сейчас сопоставим по производительности с Coral TPU.

Результаты бенчмарка TensorFlow и TensorFlow Lite на Raspberry Pi 5

Подробнее см. полную статью: Benchmarking TensorFlow and TensorFlow Lite on Raspberry Pi 5.

Активный кулер Raspberry Pi

Для этой лабораторной работы мы рекомендуем установить Active Cooler — навесное охлаждающее решение для Raspberry Pi 5 (Raspi-5). Оно сочетает алюминиевый радиатор с вентилятором, управляемым температурой, чтобы Raspi-5 комфортно работал под высокой нагрузкой, например при запуске SLM.

Active Cooler на Raspberry Pi 5

Active Cooler имеет предустановленные термопрокладки для теплопередачи и крепится непосредственно к плате Raspberry Pi 5 пружинными фиксаторами. Прошивка Raspberry Pi управляет им автоматически: при 60 °C включается вентилятор, при 67,5 °C его скорость увеличивается, а при 75 °C он работает на полной скорости. Когда температура опускается ниже этих порогов, вентилятор автоматически замедляется.

Зависимость температуры и скорости вентилятора Active Cooler

Чтобы предотвратить перегрев, все платы Raspberry Pi начинают снижать частоту процессора (троттлинг) при температуре 80 °C, а при достижении максимальной температуры 85 °C троттлинг усиливается (подробнее здесь).

Генеративный ИИ (GenAI)

Генеративный ИИ — это система искусственного интеллекта, способная создавать новый, оригинальный контент в разных форматах: текст, изображения, аудио и видео. Такие системы изучают закономерности в существующих данных и используют их, чтобы генерировать новые результаты, которых раньше не было. Большие языковые модели (LLM), малые языковые модели (SLM) и мультимодальные модели можно считать видами GenAI, если они используются для генеративных задач.

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

Большие языковые модели (LLM)

Большие языковые модели (LLM) — это продвинутые системы искусственного интеллекта, которые понимают, обрабатывают и генерируют человекоподобный текст. Такие модели характеризуются огромным масштабом: объёмом данных, на которых они обучаются, и количеством параметров. Ключевые аспекты LLM:

  1. Размер: LLM обычно содержат миллиарды параметров. Например, у GPT-3 175 миллиардов параметров, а некоторые новые модели превышают триллион.

  2. Обучающие данные: их обучают на огромных объёмах текста, часто включающих книги, веб-сайты и другие разнообразные источники; это сотни гигабайт или даже терабайты текста.

  3. Архитектура: большинство LLM используют архитектуры на основе трансформеров, которые позволяют обрабатывать и генерировать текст, одновременно «обращая внимание» на разные части входных данных.

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

    • генерацию текста;
    • перевод;
    • суммаризацию;
    • ответы на вопросы;
    • генерацию кода;
    • логические рассуждения.
  5. Обучение на нескольких примерах (few-shot learning): они часто могут понять и выполнить новую задачу по нескольким примерам или инструкциям.

  6. Ресурсоёмкость: из-за своего размера LLM обычно требуют значительных вычислительных ресурсов и часто нуждаются в мощных GPU или TPU.

  7. Непрерывное развитие: область LLM быстро развивается, постоянно появляются новые модели и методы.

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

  9. Применения: LLM используются во многих областях, включая создание контента, обслуживание клиентов, помощь в исследованиях и разработку программного обеспечения.

  10. Ограничения: несмотря на свою мощь, LLM могут выдавать неверную или предвзятую информацию и не обладают подлинным пониманием или способностью к рассуждению.

Стоит отметить, что крупные модели мы используем не только для текста: их называют мультимодальными моделями. Такие модели одновременно интегрируют и обрабатывают информацию из нескольких типов входных данных. Они спроектированы для понимания и генерации контента в разных формах: текст, изображения, аудио и видео.

Закрытые и открытые модели

Закрытые модели, также называемые проприетарными, — это модели ИИ, внутренняя работа, код и обучающие данные которых не раскрываются публично. Примеры: GPT-4 (OpenAI), Claude (Anthropic), Gemini (Google).

Открытые модели, также известные как модели с открытым исходным кодом, — это модели ИИ, базовый код, архитектура и часто обучающие данные которых публично доступны. Примеры: Gemma (Google), LLaMA (Meta) и Phi (Microsoft).

Открытые модели особенно важны для запуска на периферийных устройствах, таких как Raspberry Pi, поскольку их легче адаптировать, оптимизировать и развернуть в условиях ограниченных ресурсов. Тем не менее важно проверять их лицензии: открытые модели распространяются по разным лицензиям с открытым исходным кодом, которые могут влиять на использование в коммерческих приложениях. У закрытых моделей условия использования чёткие, хотя и ограничительные.

Сравнение больших и малых языковых моделей
Адаптировано по [arXiv 2304.13712](https://arxiv.org/abs/2304.13712)

Малые языковые модели (SLM)

В контексте периферийных вычислений на таких устройствах, как Raspberry Pi, полномасштабные LLM обычно слишком велики и ресурсоёмки, чтобы работать на них напрямую. Это ограничение стимулировало разработку более компактных и эффективных моделей — малых языковых моделей (SLM).

SLM — это компактные версии LLM, созданные для эффективной работы на устройствах с ограниченными ресурсами: смартфонах, IoT-устройствах и одноплатных компьютерах вроде Raspberry Pi. Эти модели значительно меньше по размеру и требованиям к вычислениям, чем их крупные аналоги, и при этом сохраняют впечатляющие возможности понимания и генерации языка.

Ключевые характеристики SLM:

  1. Меньшее число параметров: обычно от нескольких сотен миллионов до нескольких миллиардов параметров, против двузначного числа миллиардов у крупных моделей.

  2. Меньший объём памяти: требуется не больше нескольких гигабайт памяти, а не десятки или сотни гигабайт.

  3. Более быстрый инференс: могут генерировать ответы за миллисекунды или секунды на периферийных устройствах.

  4. Энергоэффективность: потребляют меньше энергии, что делает их подходящими для устройств с батарейным питанием.

  5. Сохранение приватности: обработка выполняется на устройстве, без отправки данных на облачные серверы.

  6. Работа без подключения: могут работать без подключения к интернету.

SLM достигают компактности благодаря разным методам: дистилляции знаний (knowledge distillation), обрезке (pruning) и квантованию. Хотя SLM могут не сравниться с крупными моделями по широте возможностей, они превосходно справляются с конкретными задачами и предметными областями, что делает их идеальными для целевых приложений на периферийных устройствах.

Далее мы будем называть SLM языковые модели с числом параметров менее 5 миллиардов, квантованные до 4 бит.

Примеры SLM включают сжатые версии моделей вроде Meta Llama, Microsoft PHI и Google Gemma. Эти модели позволяют решать широкий круг задач обработки естественного языка прямо на периферийных устройствах: от классификации текста и анализа тональности до ответов на вопросы и ограниченной генерации текста.

Подробнее о SLM: статья LLM Pruning and Distillation in Practice: The Minitron Approach описывает подход, который применяет обрезку и дистилляцию, чтобы получить SLM из LLM. А работа SMALL LANGUAGE MODELS: SURVEY, MEASUREMENTS, AND INSIGHTS представляет всесторонний обзор и анализ малых языковых моделей (SLM) — языковых моделей со 100 миллионами — 5 миллиардами параметров, предназначенных для устройств с ограниченными ресурсами.

Ollama

Логотип Ollama

Ollama — это открытая платформа, которая позволяет запускать языковые модели (LM), большие или малые, локально на наших машинах. Вот ключевые моменты об Ollama:

  1. Локальный запуск моделей: Ollama позволяет запускать LM на персональных компьютерах или периферийных устройствах, таких как Raspi-5, без облачных API-вызовов.

  2. Простота использования: предоставляет простой интерфейс командной строки для загрузки, запуска и управления разными языковыми моделями.

  3. Разнообразие моделей: Ollama поддерживает различные LLM, включая Phi, Gemma, Llama, Mistral и другие открытые модели.

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

  5. Лёгкость: спроектирована эффективно и работает на доступном аппаратном обеспечении.

  6. Интеграция через API: предлагает API для интеграции с другими приложениями и сервисами.

  7. Фокус на приватности: запуск моделей локально решает проблемы конфиденциальности, связанные с отправкой данных на внешние серверы.

  8. Кроссплатформенность: доступна для macOS, Windows и Linux (наш случай).

  9. Активная разработка: регулярно обновляется новыми функциями и поддержкой моделей.

  10. Развитие сообществом: выигрывает от вкладов сообщества и обмена моделями.

Чтобы узнать больше о том, что такое Ollama и как она работает, посмотрите короткое видео от Matt Williams, одного из основателей Ollama: смотреть на YouTube.

Matt также ведёт полностью бесплатный курс об Ollama, который мы рекомендуем: смотреть на YouTube.

Установка Ollama

Создадим и активируем виртуальное окружение для работы с Ollama:

python3 -m venv ~/ollama
source ~/ollama/bin/activate

Теперь выполним команду установки Ollama:

curl -fsSL https://ollama.com/install.sh | sh

В результате API будет работать в фоновом режиме по адресу 127.0.0.1:11434. Теперь мы можем запускать Ollama из терминала. Для начала проверим версию Ollama — это также подтвердит, что установка прошла успешно:

ollama -v

Проверка версии Ollama на Raspberry Pi 5

На странице библиотеки Ollama можно найти модели, которые поддерживает Ollama. Например, если отфильтровать по Most popular (самые популярные), увидим Meta Llama, Google Gemma, Microsoft Phi, LLaVa и другие модели.

Meta Llama 3.2 1B/3B

Малые и мультимодальные модели в библиотеке Ollama

Установим и запустим нашу первую малую языковую модель, Llama 3.2 1B (и 3B). Серия Meta Llama 3.2 — это набор многоязычных генеративных языковых моделей размером 1 и 3 миллиарда параметров. Эти модели предназначены для обработки текстового ввода и генерации текстового вывода. Инструктивные варианты этой серии специально оптимизированы для многоязычных диалоговых приложений, включая задачи поиска информации и суммаризации с агентным подходом. По сравнению со многими существующими чат-моделями с открытым исходным кодом и проприетарными моделями инструктивные версии Llama 3.2 демонстрируют превосходные результаты в широко используемых отраслевых тестах.

Модели 1B и 3B были обрезаны (pruned) из Llama 8B, а затем в качестве целевых значений на уровне токенов использовались логиты моделей 8B и 70B (дистилляция на уровне токенов). Для восстановления качества применялась дистилляция знаний: обучение проводилось на 9 триллионах токенов. Модель 1B содержит 1,24 млрд параметров с квантованием в целые числа (Q8_0), а модель 3B — 3,12 млрд параметров с квантованием Q4_0. Их размеры составляют 1,3 ГБ и 2 ГБ соответственно. Окно контекста — 131 072 токена.

Модель Llama 3.2

Установка и запуск модели

ollama run llama3.2:1b

После запуска модели этой командой появится приглашение Ollama, в котором можно ввести вопрос и начать диалог с LLM, например:

>>> What is the capital of France?

Почти мгновенно мы получаем правильный ответ:

The capital of France is Paris.

Если при вызове модели использовать параметр --verbose, будет выведена статистика о её работе (модель загружается только при первом запуске команды).

Статистика производительности Llama 3.2 1B

Каждый показатель даёт представление о том, как модель обрабатывает входные данные и генерирует ответы. Вот расшифровка метрик:

  • Total Duration (2,620170326 с): полное время от начала выполнения команды до завершения ответа. Оно включает загрузку модели, обработку входного запроса и генерацию ответа.
  • Load Duration (39,947908 мс): время загрузки модели или необходимых компонентов в память. Если значение мало, возможно, модель была предзагружена или требовалась лишь минимальная настройка.
  • Prompt Eval Count (32 токена): количество токенов во входном запросе. В NLP токены обычно являются словами или их частями, поэтому здесь учитываются все токены, которые модель обработала, чтобы понять запрос и ответить на него.
  • Prompt Eval Duration (1,644773 с): время, которое модель тратит на обработку входного запроса. Оно составляет основную часть общего времени, то есть понимание запроса и подготовка ответа — самая трудоёмкая часть процесса.
  • Prompt Eval Rate (19,46 токенов/с): скорость обработки токенов входного запроса. Она отражает быстродействие модели при понимании естественного языка.
  • Eval Count (8 токенов): количество токенов в ответе модели; в данном случае это фраза «The capital of France is Paris.»
  • Eval Duration (889,941 мс): время генерации ответа на основе обработанного входа. Оно заметно короче времени обработки запроса, а значит, генерация ответа менее сложна и вычислительно менее затратна, чем понимание запроса.
  • Eval Rate (8,99 токена/с): аналогично скорости обработки запроса, показывает скорость генерации выходных токенов. Это важный показатель эффективности модели при генерации ответа.

Такой подробный разбор помогает понять вычислительные требования и характеристики производительности SLM, например Llama, на периферийных устройствах вроде Raspberry Pi 5. Видно, что обработка запроса занимает больше времени, тогда как сама генерация ответа проходит относительно быстрее. Этот анализ важен для оптимизации производительности и поиска узких мест в приложениях реального времени.

Загрузим и запустим модель 3B и сравним производительность на том же запросе:

Статистика производительности Llama 3.2 3B

Скорость генерации ниже: 5,3 токена/с против 9 токенов/с у меньшей модели.

На вопрос

>>> What is the distance between Paris and Santiago, Chile?

модель 1B ответила 9,841 kilometers (6,093 miles) — это неточно, а модель 3B ответила 7,300 miles (11,700 km) — это близко к правильному значению (11 642 км).

Спросим координаты Парижа:

>>> what is the latitude and longitude of Paris?

The latitude and longitude of Paris are 48.8567° N (48°55'
42" N) and 2.3510° E (2°22' 8" E), respectively.

Ответ модели о координатах Парижа

Обе модели — 1B и 3B — дали правильные ответы.

Google Gemma 2 2B

Установим Gemma 2 — высокопроизводительную и эффективную модель, доступную в трёх размерах: 2B, 9B и 27B. Мы установим Gemma 2 2B — лёгкую модель, обученную на 2 триллионах токенов, которая даёт выдающиеся результаты, перенимая знания у более крупных моделей через дистилляцию. В модели 2,6 млрд параметров и квантование Q4_0, её размер — 1,6 ГБ. Окно контекста — 8192 токена.

Модель Gemma 2 2B

Установка и запуск модели

ollama run gemma2:2b --verbose

После запуска команды появится приглашение Ollama, и можно задать вопрос, например:

>>> What is the capital of France?

Почти сразу получаем правильный ответ:

The capital of France is **Paris**. 🗼

И её статистика:

Статистика производительности Gemma 2 2B

Видим, что Gemma 2:2B работает примерно так же быстро, как Llama 3.2:3B, но при меньшем числе параметров.

Другие примеры:

>>> What is the distance between Paris and Santiago, Chile?

The distance between Paris, France and Santiago, Chile is
approximately **7,000 miles (11,267 kilometers)**.

Keep in mind that this is a straight-line distance, and actual
travel distance can vary depending on the chosen routes and any
stops along the way. ✈️

Ответ тоже хороший, но менее точный, чем у Llama3.2:3B.

>>> what is the latitude and longitude of Paris?

You got it! Here are the latitudes and longitudes of Paris,
France:

* **Latitude**: 48.8566° N (north)
* **Longitude**: 2.3522° E (east)

Let me know if you'd like to explore more about Paris or its
location! 🗼🇫🇷

Хороший и точный ответ (немного многословнее, чем ответы Llama).

Microsoft Phi3.5 3.8B

Загрузим модель побольше, но всё ещё компактную: PHI3.5 — лёгкую современную открытую модель с 3,8 млрд параметров от Microsoft. Модель относится к семейству Phi-3, поддерживает контекст 128K токенов и языки: арабский, китайский, чешский, датский, нидерландский, английский, финский, французский, немецкий, иврит, венгерский, итальянский, японский, корейский, норвежский, польский, португальский, русский, испанский, шведский, тайский, турецкий и украинский.

Размер модели в байтах зависит от выбранного формата квантования. Он может варьироваться от квантования 2 бита (q2_k) размером 1,4 ГБ (выше производительность, ниже качество) до 16-битного (fp-16) размером 7,6 ГБ (ниже производительность, выше качество).

Запустим 4-битное квантование (Q4_0), которому нужно 2,2 ГБ оперативной памяти. Это компромисс между качеством ответов и производительностью.

ollama run phi3.5:3.8b --verbose

Загрузить модель можно и командой run, и командой pull. Ollama запоминает загруженные модели: если Phi3 ещё нет на устройстве, Ollama скачает её перед запуском.

Введём тот же запрос, что и раньше:

>>> What is the capital of France?

The capital of France is Paris. It' extradites significant
historical, cultural, and political importance to the country as
well as being a major European city known for its art, fashion,
gastronomy, and culture. Its influence extends beyond national
borders, with millions of tourists visiting each year from around
the globe. The Seine River flows through Paris before it reaches
the broader English Channel at Le Havre. Moreover, France is one
of Europe's leading economies with its capital playing a key role

...

Ответ получился очень «многословным», поэтому зададим более удачный запрос:

Ответ модели Phi 3.5 на уточнённый запрос

В этом случае ответ всё равно был длиннее, чем мы ожидали, со скоростью генерации 2,25 токена/с, то есть более чем вдвое медленнее, чем у Gemma и Llama.

Выбор наиболее подходящего запроса (prompt) — один из самых важных навыков при работе с LLM, независимо от их размера.

Когда мы задали те же вопросы о расстоянии и координатах, ответ о расстоянии 13,507 kilometers (8,429 miles) оказался неверным, а координаты — приемлемыми. Ответы снова могли бы быть менее многословными (более 200 токенов на каждый ответ).

Любую модель можно использовать как ассистента, поскольку её скорость вполне приемлема. Однако по состоянию на 24 сентября 2023 года для такой задачи лучше подходит Llama2:3B. Стоит попробовать и другие модели в зависимости от ваших потребностей. 🤗 Open LLM Leaderboard поможет оценить лучшие модели по размеру, результатам тестов, лицензии и т. д.

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

Мультимодальные модели

Мультимодальные модели — это системы искусственного интеллекта (ИИ), которые могут обрабатывать и понимать информацию из нескольких источников: изображений, текста, аудио и видео. В нашем контексте мультимодальные LLM могут принимать в качестве запросов разные входные данные, включая текст, изображения и аудио, и преобразовывать их в выходные данные разных типов, а не только того же типа.

Мы будем работать с LLaVA-Phi-3 — доработанной моделью LLaVA на основе Phi 3 Mini 4k. По результатам тестов она сопоставима с исходной моделью LLaVA (Large Language and Vision Assistant).

LLaVA-Phi-3 — это крупная мультимодальная модель, обученная сквозным образом, которая понимает визуальные данные (изображения) и текстовые инструкции и генерирует контент на их основе. Она сочетает возможности визуального энкодера и языковой модели, чтобы обрабатывать мультимодальные входные данные и отвечать на них.

Установим модель:

ollama run llava-phi3:3.8b --verbose

Начнём с текстового ввода:

>>> You are a helpful AI assistant.
    What is the capital of France?

As an AI language model, I can tell you that the capital of
France is Paris. It's not only the largest city in the country
but also serves as its political and administrative center.
Paris is known for its iconic landmarks such as the Eiffel
Tower, Notre-Dame Cathedral, and the Louvre Museum. The city
has a rich history, beautiful architecture, and is widely
considered to be one of the most romantic cities in the world.

Ответ занял около 30 секунд, скорость генерации — 3,93 токена/с. Неплохо!

Теперь передадим модели изображение. Для этого создадим рабочую папку:

cd Documents/
mkdir OLLAMA
cd OLLAMA

Скачаем изображение размером , например из Википедии: Paris, France:

Эйфелева башня, вид с башни Сен-Жак в Париже

С помощью FileZilla, например, загрузим изображение в папку OLLAMA на Raspi-5 и назовём его image_test_1.jpg. Нам понадобится полный путь к изображению (его можно узнать командой pwd):

/home/username/Documents/OLLAMA/image_test_1.jpg

Если вы используете рабочий стол, путь к изображению можно скопировать, щёлкнув по изображению правой кнопкой мыши.

Копирование пути к изображению через контекстное меню

Введём такой запрос:

>>> Describe the image /home/username/Documents/OLLAMA/\
                        image_test_1.jpg

Результат получился хорошим, но общая задержка оказалась значительной: почти 4 минуты на инференс.

Результат описания изображения моделью LLaVA-Phi-3

Проверка локальных ресурсов

С помощью htop можно отслеживать ресурсы, используемые устройством:

htop

Пока модель работает, можно проверять ресурсы:

Загрузка CPU и памяти в htop во время инференса

Все четыре ядра CPU работают почти на 100 % своей мощности, а объём памяти с загруженной моделью составляет 3.24 GB. После выхода из Ollama память снижается примерно до 377 MB (без рабочего стола).

Также важно следить за температурой. При работе Raspberry с рабочим столом температуру можно видеть на панели задач:

Индикатор температуры на панели задач Raspberry Pi

Если Raspi работает без монитора (headless), температуру можно проверить командой:

vcgencmd measure_temp

Если устройство простаивает, температура составляет около 50 °C при загрузке CPU 1 %. Во время инференса, когда CPU загружены на 100 %, температура может подняться почти до 70 °C. Это нормально: значит, активный кулер работает и удерживает температуру ниже предела 80/85 °C.

Библиотека Ollama для Python

До сих пор мы изучали возможности диалога с SLM через командную строку в терминале. Однако мы хотим интегрировать эти модели в наши проекты, поэтому логичнее использовать Python. Хорошая новость: у Ollama есть такая библиотека.

Библиотека Ollama для Python упрощает взаимодействие с продвинутыми LLM, позволяя получать более сложные ответы и реализовывать дополнительные возможности. Она также даёт самый простой способ интегрировать проекты на Python 3.8+ с Ollama.

Чтобы лучше понять, как создавать приложения с Ollama на Python, можно посмотреть видео Matt Williams, например это: смотреть на YouTube.

Установка:

В терминале выполните команду:

pip install ollama

Для создания Python-скрипта понадобится текстовый редактор или IDE. Если вы работаете с Raspberry Pi OS на рабочем столе, по умолчанию уже установлено несколько вариантов, например Thonny и Geany (открываются через [Menu][Programming]). Другие IDE, например Visual Studio Code, можно загрузить через [Menu][Recommended Software]. Когда откроется окно, перейдите в [Programming], выберите нужный вариант и нажмите [Apply].

Меню Raspberry Pi OS с инструментами программирования

Если вы предпочитаете разрабатывать в Jupyter Notebook:

pip install jupyter
jupyter notebook --generate-config

Чтобы запустить Jupyter Notebook, выполните команду (замените IP-адрес на свой):

jupyter notebook --ip=192.168.4.209 --no-browser

В терминале появится локальный URL-адрес, по которому можно открыть блокнот:

Вывод терминала с адресом Jupyter Notebook и токеном

Получить доступ к блокноту можно с другого компьютера: введите в веб-браузере IP-адрес Raspberry Pi и токен из терминала (скопируйте его оттуда).

В рабочем каталоге Raspi создадим новый блокнот Python 3.

Запустим очень простой скрипт, чтобы проверить установленные модели:

import ollama

ollama.list()

Все модели будут выведены в виде словаря, например:

  {'name': 'gemma2:2b',
   'model': 'gemma2:2b',
   'modified_at': '2024-09-24T19:30:40.053898094+01:00',
   'size': 1629518495,
   'digest': (
     '8ccf136fdd5298f3ffe2d69862750ea7fb56555fa4d5b18c0'
     '4e3fa4d82ee09d7'
    ),

   'details': {'parent_model': '',
    'format': 'gguf',
    'family': 'gemma2',
    'families': ['gemma2'],
    'parameter_size': '2.6B',
    'quantization_level': 'Q4_0'}}]}

Повторим один из вопросов, которые мы задавали раньше, но теперь с помощью ollama.generate() из библиотеки Ollama для Python. Этот API генерирует ответ на заданный запрос с помощью указанной модели. Это потоковая конечная точка, поэтому ответ приходит серией фрагментов. Итоговый объект ответа содержит статистику и дополнительные данные о запросе.

MODEL = "gemma2:2b"
PROMPT = "What is the capital of France?"

res = ollama.generate(model=MODEL, prompt=PROMPT)
print(res)

Если запускаете код как Python-скрипт, сохраните его, например, как test_ollama.py. Запустить его можно в IDE или прямо в терминале. Помните, что при запуске отдельного скрипта модель всегда нужно указывать явно.

python test_ollama.py

В результате мы получим ответ модели в формате JSON:

{
  'model': 'gemma2:2b',
  'created_at': '2024-09-25T14:43:31.869633807Z',
  'response': 'The capital of France is **Paris**.\n',
  'done': True,
  'done_reason': 'stop',
  'context': [
      106, 1645, 108, 1841, 603, 573, 6037, 576, 6081, 235336,
      107, 108, 106, 2516, 108, 651, 6037, 576, 6081, 603, 5231,
      29437, 168428, 235248, 244304, 241035, 235248, 108
  ],
  'total_duration': 24259469458,
  'load_duration': 19830013859,
  'prompt_eval_count': 16,
  'prompt_eval_duration': 1908757000,
  'eval_count': 14,
  'eval_duration': 2475410000
}

Как видно, генерируется несколько видов информации, например:

  • response: основной текст, который модель сгенерировала в ответ на наш запрос:

    • The capital of France is **Paris**. 🇫🇷
  • context: идентификаторы токенов, представляющие входные данные и контекст, которые использовала модель. Токены — это числовые представления текста, которые языковая модель использует для обработки:

    [106, 1645, 108, 1841, 603, 573, 6037, 576, 6081, 235336,
     107, 108, 106, 2516, 108, 651, 6037, 576, 6081, 603,
     5231, 29437, 168428, 235248, 244304, 241035, 235248, 108]
    

Показатели производительности:

  • total_duration: общее время выполнения операции в наносекундах. В данном случае примерно 24,26 секунды.
  • load_duration: время загрузки модели или её компонентов в наносекундах. Около 19,83 секунды.
  • prompt_eval_duration: время обработки запроса в наносекундах. Около 1,9 секунды.
  • eval_count: количество токенов, обработанных при генерации. Здесь 14 токенов.
  • eval_duration: время, за которое модель сгенерировала ответ, в наносекундах. Примерно 2,5 секунды.

Но нам нужен только текст ответа (response) и, возможно, для анализа общее время инференса. Поэтому изменим код так, чтобы извлекать их из словаря:

print(f"\n{res['response']}")
print(
    f"\n [INFO] Total Duration: "
    f"{res['total_duration']/1e9:.2f} seconds"
)

Теперь получаем:

The capital of France is **Paris**. 🇫🇷

 [INFO] Total Duration: 24.26 seconds

Использование Ollama.chat()

Другой способ получить ответ — ollama.chat(), который генерирует следующее сообщение в диалоге с указанной моделью. Это тоже потоковая конечная точка, поэтому ответ приходит серией фрагментов. Потоковую передачу можно отключить, указав "stream": false. Итоговый объект ответа также содержит статистику и дополнительные данные о запросе.

PROMPT_1 = "What is the capital of France?"

response = ollama.chat(
    model=MODEL,
    messages=[
        {
            "role": "user",
            "content": PROMPT_1,
        },
    ],
)
resp_1 = response["message"]["content"]
print(f"\n{resp_1}")
print(
    f"\n [INFO] Total Duration: "
    f"{(response['total_duration']/1e9):.2f} seconds"
)

Ответ такой же, как и раньше.

Важный момент: при использовании ollama.generate() ответ «забывается» моделью после завершения инференса (запрос используется только один раз). Если же мы хотим сохранить диалог, нужно использовать ollama.chat(). Посмотрим, как это работает:

PROMPT_1 = "What is the capital of France?"
response = ollama.chat(
    model=MODEL,
    messages=[
        {
            "role": "user",
            "content": PROMPT_1,
        },
    ],
)
resp_1 = response["message"]["content"]
print(f"\n{resp_1}")
print(
    f"\n [INFO] Total Duration: "
    f"{(response['total_duration']/1e9):.2f} seconds"
)

PROMPT_2 = "and of Italy?"
response = ollama.chat(
    model=MODEL,
    messages=[
        {
            "role": "user",
            "content": PROMPT_1,
        },
        {
            "role": "assistant",
            "content": resp_1,
        },
        {
            "role": "user",
            "content": PROMPT_2,
        },
    ],
)
resp_2 = response["message"]["content"]
print(f"\n{resp_2}")
print(
    f"\n [INFO] Total Duration: "
    f"{(response['total_duration']/1e9):.2f} seconds"
)

В приведённом коде мы выполняем два запроса, и второй учитывает результат первого.

Вот как ответила модель:

The capital of France is **Paris**. 🇫🇷

 [INFO] Total Duration: 2.82 seconds

The capital of Italy is **Rome**. 🇮🇹

 [INFO] Total Duration: 4.46 seconds

Получение описания изображения:

Так же, как мы использовали модель LlaVa-PHI-3 в командной строке для анализа изображения, это можно сделать и в Python. Используем то же изображение Парижа, но теперь с ollama.generate():

MODEL = "llava-phi3:3.8b"
PROMPT = "Describe this picture"

with open("image_test_1.jpg", "rb") as image_file:
    img = image_file.read()

response = ollama.generate(model=MODEL, prompt=PROMPT, images=[img])
print(f"\n{response['response']}")
print(
    f"\n [INFO] Total Duration: "
    f"{(response['total_duration']/1e9):.2f} seconds"
)

Вот результат:

This image captures the iconic cityscape of Paris, France. The
vantage point is high, providing a panoramic view of the Seine
River that meanders through the heart of the city. Several
bridges arch gracefully over the river, connecting different
parts of the city. The Eiffel Tower, an iron lattice structure
with a pointed top and two antennas on its summit, stands
tall in the background, piercing the sky. It is painted in a
light gray color, contrasting against the blue sky speckled
with white clouds.

The buildings that line the river are predominantly white or
beige, their uniform color palette broken occasionally by red
roofs peeking through. The Seine River itself appears calm
and wide, reflecting the city's architectural beauty in its
surface. On either side of the river, trees add a touch of
green to the urban landscape.

The image is taken from an elevated perspective, looking down
on the city. This viewpoint allows for a comprehensive view of
Paris's beautiful architecture and layout. The relative
positions of the buildings, bridges, and other structures
create a harmonious composition that showcases the city's charm.

In summary, this image presents a serene day in Paris, with its
architectural marvels - from the Eiffel Tower to the river-side
buildings - all bathed in soft colors under a clear sky.

 [INFO] Total Duration: 256.45 seconds

Модели потребовалось около 4 минут (256,45 с), чтобы вернуть подробное описание изображения.

В блокноте 10-Ollama_Python_Library можно найти эксперименты с библиотекой Ollama для Python.

Вызов функций

До сих пор мы видели, что, помещая ответ модели в переменную, его можно эффективно использовать в реальных проектах. Однако возникает серьёзная проблема: модель может давать разные ответы на один и тот же вход. Например, предположим, что в предыдущих примерах нам нужны только название столицы страны и её координаты, без дополнительной информации, даже при использовании многословных моделей вроде Microsoft Phi. Чтобы обеспечить стабильные ответы, можно использовать «вызов функций Ollama» (Ollama function call), который полностью совместим с API OpenAI.

Что такое «вызов функций»?

В современном искусственном интеллекте вызов функций с большими языковыми моделями (LLM) позволяет этим моделям выполнять действия, выходящие за рамки генерации текста. Интегрируясь с внешними функциями или API, LLM могут получать данные в реальном времени, автоматизировать задачи и взаимодействовать с различными системами.

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

Подробнее о вызове функций см. видео, сделанное Mervin Praison: смотреть на YouTube.

Создадим проект

Мы хотим создать приложение, в котором пользователь вводит название страны и получает на выходе расстояние в километрах от столицы этой страны до места расположения приложения (для простоты местоположение приложения — Сантьяго, Чили).

Блок-схема проекта с вызовом функций

Когда пользователь вводит название страны, модель возвращает название её столицы (строка), а также широту и долготу этого города (числа с плавающей точкой). Используя эти координаты, мы можем с помощью простой библиотеки Python (haversine) вычислить расстояние между двумя точками.

Идея проекта — продемонстрировать сочетание взаимодействия с языковой моделью, обработки структурированных данных с помощью Pydantic и геопространственных вычислений по формуле гаверсинусов (традиционные вычисления).

Сначала установим несколько библиотек. Помимо Haversine, главная из них — библиотека OpenAI для Python, которая обеспечивает удобный доступ к REST API OpenAI из любого приложения на Python 3.7+. Вторая — Pydantic (и instructor), надёжная библиотека проверки данных и управления настройками, созданная для повышения устойчивости и надёжности кода. Коротко говоря, Pydantic поможет гарантировать, что ответ модели всегда будет согласованным.

pip install haversine
pip install openai
pip install pydantic
pip install instructor

Теперь создадим Python-скрипт, который взаимодействует с нашей моделью (LLM), чтобы определить координаты столицы страны и вычислить расстояние от Сантьяго-де-Чили до этой столицы.

Разберём код по шагам.

1. Импорт библиотек

import sys
from haversine import haversine
from openai import OpenAI
from pydantic import BaseModel, Field
import instructor
  • sys: предоставляет доступ к системным параметрам и функциям. Используется для получения аргументов командной строки.
  • haversine: функция из библиотеки haversine, которая вычисляет расстояние между двумя географическими точками по формуле гаверсинусов.
  • openAI: модуль для взаимодействия с API OpenAI (здесь он используется вместе с локальной установкой Ollama). Всё работает офлайн.
  • pydantic: обеспечивает проверку данных и управление настройками с помощью аннотаций типов Python. Используется для описания структуры ожидаемых данных ответа.
  • instructor: модуль, который «патчит» клиента OpenAI, чтобы он работал в определённом режиме (вероятно, связанном с обработкой структурированных данных).

2. Определение входных данных и модели

country = sys.argv[1]  # Получаем страну из
# аргументов командной строки
MODEL = "phi3.5:3.8b"  # Название модели для использования
mylat = -33.33  # Широта Сантьяго-де-Чили
mylon = -70.51  # Долгота Сантьяго-де-Чили
  • country: в Python-скрипте название страны можно получить из аргументов командной строки. В Jupyter Notebook его можно задать вручную, например:
    • country = "France"
  • MODEL: указывает используемую модель, в данном примере phi3.5.
  • mylat и mylon: координаты Сантьяго-де-Чили, которые служат отправной точкой для расчёта расстояния.

3. Определение структуры данных ответа

class CityCoord(BaseModel):
    city: str = Field(..., description="Name of the city")
    lat: float = Field(
        ..., description="Decimal Latitude of the city"
    )
    lon: float = Field(
        ..., description="Decimal Longitude of the city"
    )
  • CityCoord: модель Pydantic, которая определяет ожидаемую структуру ответа LLM. Она ожидает три поля: city (название города), lat (широта) и lon (долгота).

4. Настройка клиента OpenAI

client = instructor.patch(
    OpenAI(
        base_url="http://localhost:11434/v1",  # Базовый URL локального API
        # (Ollama)
        api_key="ollama",  # Ключ API
        # (не используется)
    ),
    mode=instructor.Mode.JSON,  # Режим для
    # структурированного вывода JSON
)
  • OpenAI: эта настройка создаёт клиента OpenAI с локальным базовым URL и ключом API (ollama). Используется локальный сервер.
  • instructor.patch: «патчит» клиента OpenAI, переводя его в режим JSON, что позволяет получать структурированный вывод, соответствующий модели Pydantic.

5. Генерация ответа

resp = client.chat.completions.create(
    model=MODEL,
    messages=[
        {
            "role": "user",
            "content": f"return the decimal latitude and \
            decimal longitude of the capital of the {country}.",
        }
    ],
    response_model=CityCoord,
    max_retries=10,
)
  • client.chat.completions.create: вызывает LLM для генерации ответа.
  • model: указывает модель, которую нужно использовать (в нашем коде это phi3.5).
  • messages: содержит запрос к LLM с просьбой вернуть широту и долготу столицы заданной страны.
  • response_model: указывает, что ответ должен соответствовать модели CityCoord.
  • max_retries: максимальное число повторных попыток, если запрос не удался.

6. Вычисление расстояния

distance = haversine((mylat, mylon), (resp.lat, resp.lon), unit="km")

print(
    f"Santiago de Chile is about {int(round(distance, -1))} "
    f"kilometers away from {resp.city}."
)
  • haversine: вычисляет расстояние между Сантьяго-де-Чили и столицей, которую вернула LLM, по их координатам.
  • (mylat, mylon): координаты Сантьяго-де-Чили.
  • resp.city: название столицы страны.
  • (resp.lat, resp.lon): координаты столицы, которые предоставил ответ LLM.
  • unit = ‘km’: расстояние вычисляется в километрах.
  • print: выводит расстояние, округлённое до ближайших 10 километров, с разделителями тысяч для удобства чтения.

Запуск кода

Если указать разные страны, например Францию, Колумбию и США, мы видим, что каждый раз получаем одну и ту же структурированную информацию:

Santiago de Chile is about 8,060 kilometers away from
    Washington, D.C..
Santiago de Chile is about 4,250 kilometers away from Bogotá.
Santiago de Chile is about 11,630 kilometers away from Paris.

Если запустить код как скрипт, результат будет выведен в терминале:

Вывод скрипта с вызовом функций в терминале

И расчёты довольно точны!

Сверка расчёта расстояния с реальными значениями

В блокноте 20-Ollama_Function_Calling можно найти эксперименты со всеми установленными моделями.

Добавление изображений

Пора собрать всё вместе! Изменим скрипт так, чтобы вместо названия страны (текстом) пользователь вводил изображение, а приложение на основе SLM возвращало город, изображённый на нём, и его географическое положение. Имея эти данные, мы можем вычислить расстояние, как и раньше.

Блок-схема приложения, которое принимает изображение

Для простоты реализуем новый код в два шага. Сначала LLM проанализирует изображение и создаст его описание (текст). Этот текст передаётся другому экземпляру модели, которая извлечёт из него нужную информацию.

Начнём с импорта библиотек:

import sys
import time
from haversine import haversine
import ollama
from openai import OpenAI
from pydantic import BaseModel, Field
import instructor

Чтобы видеть изображение при запуске кода в Jupyter Notebook, также импортируем:

import matplotlib.pyplot as plt
from PIL import Image

Эти библиотеки не нужны, если код запускается как скрипт.

Теперь определим модель и локальные координаты:

MODEL = "llava-phi3:3.8b"
mylat = -33.33
mylon = -70.51

Можно скачать новое изображение, например Мачу-Пикчу из Википедии, и посмотреть его в блокноте:

# Загружаем изображение
img_path = "image_test_3.jpg"
img = Image.open(img_path)

# Выводим изображение
plt.figure(figsize=(8, 8))
plt.imshow(img)
plt.axis("off")
# plt.title("Image")
plt.show()

Изображение Мачу-Пикчу для проверки модели

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

def image_description(img_path):
    with open(img_path, "rb") as file:
        response = ollama.chat(
            model=MODEL,
            messages=[
                {
                    "role": "user",
                    "content": """return the decimal latitude and \
                 decimal longitude of the city in the image, \
                 its name, and what country it is located""",
                    "images": [file.read()],
                },
            ],
            options={
                "temperature": 0,
            },
        )
    # print(response['message']['content'])
    return response["message"]["content"]

Для отладки можно вывести весь ответ целиком.

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

start_time = time.perf_counter()  # Запускаем таймер


class CityCoord(BaseModel):
    city: str = Field(
        ..., description="Name of the city in the image"
    )
    country: str = Field(
        ...,
        description=(
            "Name of the country where "
            "the city in the image is located"
        ),
    )
    lat: float = Field(
        ...,
        description=("Decimal latitude of the city in " "the image"),
    )
    lon: float = Field(
        ...,
        description=("Decimal longitude of the city in " "the image"),
    )


# Включает `response_model` в вызове create
client = instructor.patch(
    OpenAI(base_url="http://localhost:11434/v1", api_key="ollama"),
    mode=instructor.Mode.JSON,
)

image_description = image_description(img_path)
# Отправляем это описание модели
resp = client.chat.completions.create(
    model=MODEL,
    messages=[
        {
            "role": "user",
            "content": image_description,
        }
    ],
    response_model=CityCoord,
    max_retries=10,
    temperature=0,
)

Если вывести описание изображения, получим:

The image shows the ancient city of Machu Picchu, located in
Peru. The city is perched on a steep hillside and consists of
various structures made of stone. It is surrounded by lush
greenery and towering mountains. The sky above is blue with
scattered clouds.

Machu Picchu's latitude is approximately 13.5086° S, and its
longitude is around 72.5494° W.

А вторым ответом модели (resp) будет:

CityCoord(city='Machu Picchu', country='Peru', lat=-13.5086,
                lon=-72.5494)

Теперь можно выполнить «постобработку»: вычислить расстояние и подготовить итоговый ответ:

distance = haversine((mylat, mylon), (resp.lat, resp.lon), unit="km")

print(
    (
        f"\nThe image shows {resp.city}, with lat: "
        f"{round(resp.lat, 2)} and long: "
        f"{round(resp.lon, 2)}, located in "
        f"{resp.country} and about "
        f"{int(round(distance, -1)):,} kilometers "
        f"away from Santiago, Chile.\n"
    )
)

end_time = time.perf_counter()  # Останавливаем таймер
elapsed_time = end_time - start_time  # Вычисляем затраченное время
print(
    f"[INFO] ==> The code (running {MODEL}), "
    f"took {elapsed_time:.1f} seconds to execute.\n"
)

И получим:

 The image shows Machu Picchu, with lat:-13.16 and long:
 -72.54, located in Peru  and about 2,250 kilometers away
 from Santiago, Chile.

В блокноте 30-Function_Calling_with_images можно найти эксперименты с несколькими изображениями.

Теперь скачайте скрипт calc_distance_image.py из репозитория EdgeML-with-Raspberry-Pi и запустите его в терминале командой:

python calc_distance_image.py \
  /home/username/Documents/OLLAMA/image_test_3.jpg

Укажите полный путь к изображению Мачу-Пикчу в качестве аргумента. Мы получим тот же результат, что и раньше.

Результат работы приложения для изображения Мачу-Пикчу

А как насчёт Парижа?

Результат работы приложения для изображения Парижа

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

Методы оптимизации SLM

Большие языковые модели (LLM) произвели революцию в обработке естественного языка, но их развёртывание и оптимизация связаны с особыми сложностями. Одна из существенных проблем — склонность LLM (и тем более SLM) генерировать правдоподобно звучащую, но фактически неверную информацию. Это явление известно как галлюцинации: модель выдаёт содержимое, которое выглядит связным, но не опирается на истину или реальные факты.

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

Основные методы повышения производительности и эффективности LLM (и SLM): дообучение (fine-tuning), инженерия запросов (prompt engineering) и генерация с дополнением поиском (Retrieval-Augmented Generation, RAG).

  • Дообучение (fine-tuning) требует больше ресурсов, но позволяет специализировать LLM для конкретных областей или задач. Процесс заключается в дополнительном обучении модели на тщательно отобранных наборах данных, что позволяет адаптировать её обширные общие знания к конкретным приложениям. Дообучение может существенно улучшить производительность, особенно в узких областях или для особых сценариев.

  • Инженерия запросов (prompt engineering) стоит на переднем крае оптимизации LLM. Тщательно составляя входные запросы, мы можем направить модель на более точные и релевантные ответы. Этот метод предполагает формулировку запросов, которые используют предобученные знания и возможности модели, часто включая примеры или конкретные инструкции, задающие нужный ответ.

  • Генерация с дополнением поиском (Retrieval-Augmented Generation, RAG) — ещё один мощный подход к повышению производительности LLM. Метод сочетает обширные знания, заложенные в предобученной модели, со способностью получать и использовать внешнюю, актуальную информацию. Извлекая релевантные данные для поддержки процесса принятия решений моделью, RAG может существенно повысить точность и снизить вероятность генерации устаревших или ложных сведений.

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

Реализация RAG

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

Схема RAG: вопрос пользователя, поиск в базе знаний и ответ модели

Простой RAG-проект

Вот шаги для реализации базовой генерации с дополнением поиском (Retrieval Augmented Generation, RAG):

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

  • Разбейте текст на фрагменты (chunks): нельзя хранить текст как один длинный поток из-за ограничений размера контекста и риска путаницы. Разбиение (чанкинг) означает деление текста на более мелкие части. Текст можно разбивать разными способами: по количеству символов, токенов, слов, абзацев или разделов. Фрагменты также можно перекрывать.

  • Создайте эмбеддинги: эмбеддинги — это числовые представления текста, которые отражают его смысл. Чтобы создать эмбеддинг, каждый фрагмент текста пропускается через определённую модель эмбеддингов. Модель выдаёт вектор, длина которого зависит от используемой модели. Для этой задачи нужно загрузить из Ollama одну (или несколько) моделей эмбеддингов. Ниже приведены примеры моделей эмбеддингов, доступных в Ollama:

МодельРазмер параметровРазмер эмбеддинга
mxbai-embed-large334M1024
nomic-embed-text137M768
all-minilm23M384
> Как правило, большие размеры эмбеддингов лучше передают тонкие нюансы входных данных. Однако их обработка требует больше вычислительных ресурсов, а большее число параметров должно увеличивать задержку (но и качество ответа).
  • Сохраните фрагменты и эмбеддинги в векторной базе данных: нам нужен способ эффективно находить наиболее релевантные фрагменты текста для данного запроса, и здесь на помощь приходит векторная база данных. Мы будем использовать Chromadb — открытую векторную базу данных для задач ИИ, которая упрощает создание RAG, подключая знания, факты и навыки к LLM. Для каждого фрагмента хранятся и эмбеддинг, и исходный текст.

  • Соберите запрос (prompt): когда появляется вопрос, мы создаём для него эмбеддинг и запрашиваем у векторной базы данных наиболее похожие фрагменты. Затем выбираем несколько лучших результатов и включаем их текст в запрос.

Цель RAG — предоставить модели наиболее релевантную информацию из наших документов, чтобы она могла генерировать более точные и содержательные ответы. Поэтому реализуем простой пример SLM, который опирается на набор фактов о пчёлах («Bee Facts»).

В окружении ollama выполните в терминале команду установки Chromadb:

pip install ollama chromadb

Загрузим модель эмбеддингов среднего размера nomic-embed-text:

ollama pull nomic-embed-text

Создадим рабочий каталог:

cd Documents/OLLAMA/
mkdir RAG-simple-bee
cd RAG-simple-bee/

Создадим новый блокнот Jupyter 40-RAG-simple-bee для экспериментов.

Импортируем нужные библиотеки:

import ollama
import chromadb
import time

Определим модели:

EMB_MODEL = "nomic-embed-text"
MODEL = "llama3.2:3B"

Сначала нужно создать базу знаний о пчёлах. Для этого собираем релевантные документы и преобразуем их в векторные эмбеддинги. Эти эмбеддинги сохраняются в векторной базе данных, что позволяет быстро выполнять поиск по сходству в дальнейшем. Введём «документы» — базу «фактов о пчёлах» в виде списка:

documents = [
    "Пчеловодство, также известное как апикультура, подразумевает \
    уход за пчелиными семьями, обычно в ульях, которым занимается человек.",
    "Самый распространённый вид пчёл, которых содержат люди, — \
    европейская медоносная пчела (Apis mellifera).",

    ...

    "В мире существует ещё около 20 000 видов пчёл.",
    "Только в Бразилии более 300 видов пчёл, и подавляющее большинство \
    из них, в отличие от западных медоносных пчёл, не жалит.",
    "В отчётах, написанных в 1577 году Гансом Стаденом, упоминаются три \
    местных вида пчёл, которых использовали коренные жители Бразилии.",
    "Коренные жители Бразилии использовали пчёл в медицинских и пищевых целях.",
    "Из отчёта Ганса Стадена: вероятные виды: mandaçaia (Melipona quadrifasciata), \
    mandaguari (Scaptotrigona postica) и jataí-amarela (Tetragonisca angustula)."
]

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

Теперь создадим векторную базу эмбеддингов bee_facts и сохраним в ней документы:

client = chromadb.Client()
collection = client.create_collection(name="bee_facts")

# Сохраняем каждый документ в векторной базе эмбеддингов
for i, d in enumerate(documents):
    response = ollama.embeddings(model=EMB_MODEL, prompt=d)
    embedding = response["embedding"]
    collection.add(
        ids=[str(i)], embeddings=[embedding], documents=[d]
    )

Теперь, когда «база знаний» создана, можно начинать запросы и извлекать из неё данные:

Схема извлечения данных из базы знаний для RAG

Запрос пользователя: процесс начинается, когда пользователь задаёт вопрос, например: «Сколько пчёл в семье? Кто откладывает яйца и в каком количестве? А как насчёт распространённых вредителей и болезней?»

prompt = "Сколько пчёл в семье? Кто откладывает яйца и в каком количестве? \
          А как насчёт распространённых вредителей и болезней?"

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

response = ollama.embeddings(prompt=prompt, model=EMB_MODEL)

Поиск релевантных документов: система ищет в базе знаний по эмбеддингу запроса наиболее релевантные документы (в данном случае 5 наиболее вероятных). Это делается поиском по сходству, который сравнивает эмбеддинг запроса с эмбеддингами документов в базе.

results = collection.query(
    query_embeddings=[response["embedding"]], n_results=5
)
data = results["documents"]

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

prompt = (
    f"Using this data: {data}. " f"Respond to this prompt: {prompt}"
)

Генерация ответа: дополненный запрос передаётся языковой модели, в данном случае llama3.2:3b. Модель использует этот обогащённый контекст, чтобы сформировать развёрнутый ответ. Параметры temperature, top_k и top_p задают степень случайности и качество генерируемого ответа.

output = ollama.generate(
  model=MODEL,
  prompt = (
      f"Using this data: {data}. "
      f"Respond to this prompt: {prompt}"
  ),

  options={
    "temperature": 0.0,
    "top_k":10,
    "top_p":0.5
  }
)

Передача ответа: наконец, система возвращает пользователю сгенерированный ответ.

print(output["response"])
Based on the provided data, here are the answers to your \
questions:

1. How many bees are in a colony?
A typical bee colony can contain between 20,000 and 80,000 bees.

2. Who lays eggs and how much?
The queen bee lays up to 2,000 eggs per day during peak seasons.

3. What about common pests and diseases?
Common pests and diseases that affect bees include varroa \
mites, hive beetles, and foulbrood.

Создадим функцию, которая поможет отвечать на новые вопросы:

def rag_bees(prompt, n_results=5, temp=0.0, top_k=10, top_p=0.5):
    start_time = time.perf_counter()  # Запускаем таймер

    # Создаём эмбеддинг запроса и получаем релевантные данные
    response = ollama.embeddings(
      prompt=prompt,
      model=EMB_MODEL
    )

    results = collection.query(
      query_embeddings=[response["embedding"]],
      n_results=n_results
    )
    data = results['documents']

    # Генерируем ответ, объединяя запрос и полученные данные
    output = ollama.generate(
      model=MODEL,
      prompt = (
         f"Using this data: {data}. "
         f"Respond to this prompt: {prompt}"
      ),

      options={
        "temperature": temp,
        "top_k": top_k,
        "top_p": top_p
      }
    )

    print(output['response'])

    end_time = time.perf_counter()  # Останавливаем таймер
    elapsed_time = round(
       (end_time - start_time), 1
    )  # Вычисляем затраченное время

    print(
       f"\n[INFO] ==> The code for model: {MODEL}, "
       f"took {elapsed_time}s to generate the answer.\n"
    )

Теперь можно формулировать запросы и вызывать функцию:

prompt = "Есть ли в Бразилии пчёлы?"
rag_bees(prompt)
Yes, bees are found in Brazil. According to the data, Brazil \
has more than 300 different bee species, and indigenous people \
in Brazil used bees for medicine and food purposes. \
Additionally, reports from 1577 mention three native bees \
used by indigenous people in Brazil.

 [INFO] ==> The code for model: llama3.2:3b, took 22.7s to \
 generate the answer.

Кстати, если модель поддерживает несколько языков, можно задавать вопросы и на другом языке (например, на португальском), даже если набор данных составлен на русском:

prompt = "Existem abelhas no Brazil?"
rag_bees(prompt)
Sim, existem abelhas no Brasil! De acordo com o relato de Hans \
Staden, há três espécies de abelhas nativas do Brasil que \
foram mencionadas: mandaçaia (Melipona quadrifasciata), \
mandaguari (Scaptotrigona postica) e jataí-amarela \
(Tetragonisca angustula). Além disso, o Brasil é conhecido \
por ter mais de 300 espécies diferentes de abelhas, a \
maioria das quais não é agressiva e não põe veneno.

 [INFO] ==> The code for model: llama3.2:3b, took 54.6s to \
            generate the answer.

Дальнейшие шаги

Протестированные небольшие языковые модели хорошо работают на периферии и с текстом, и с изображениями, однако при работе с изображениями задержка оказалась высокой. Сочетание специализированных моделей может дать лучшие результаты. Например, в реальных задачах модель обнаружения объектов (такая как YOLO) может получить общее описание и количество объектов на изображении. Затем это описание передаётся LLM, которая помогает извлечь ключевые выводы и определить действия.

Avi Baum, технический директор (CTO) Hailo, говорит:

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

Логотип Hailo

Итоги

Эта лабораторная работа показала, как Raspberry Pi 5 можно превратить в мощный ИИ-узел, способный запускать большие языковые модели (LLM) для анализа данных на месте и в реальном времени с помощью Ollama и Python. Универсальность и мощность Raspberry Pi в сочетании с возможностями лёгких LLM, таких как Llama 3.2 и LLaVa-Phi-3-mini, делают её отличной платформой для периферийных вычислений.

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

1. Умный дом:

  • Интегрируйте SLM, чтобы интерпретировать голосовые команды или анализировать данные датчиков для интеллектуальной автоматизации дома. Это может включать мониторинг и управление бытовыми устройствами в реальном времени, системами безопасности и управлением энергопотреблением, всё с локальной обработкой без облачных сервисов.

2. Сбор и анализ данных в полевых условиях:

  • Развёртывайте SLM на Raspberry Pi в удалённых или мобильных установках для сбора и анализа данных в реальном времени. Это может применяться в сельском хозяйстве для мониторинга состояния посевов, в экологических исследованиях для отслеживания диких животных или при ликвидации последствий катастроф для оценки обстановки и управления ресурсами.

3. Образовательные инструменты:

  • Создавайте интерактивные обучающие инструменты, которые используют SLM для мгновенной обратной связи, перевода и репетиторства. Это особенно полезно в регионах с ограниченным доступом к современным технологиям и интернету.

4. Медицинские приложения:

  • Используйте SLM для медицинской диагностики и мониторинга пациентов. Они могут анализировать симптомы в реальном времени и предлагать возможные варианты лечения. Это можно интегрировать в платформы телемедицины или портативные медицинские устройства.

5. Локальная бизнес-аналитика:

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

6. Промышленный интернет вещей (IIoT):

  • Интегрируйте SLM в промышленные системы IoT для предиктивного обслуживания, контроля качества и оптимизации процессов. Raspberry Pi может служить локальным блоком обработки данных, снижая задержки и повышая надёжность автоматизированных систем.

7. Автономные транспортные средства:

  • Используйте SLM для обработки сенсорных данных автономных транспортных средств, обеспечивая принятие решений и навигацию в реальном времени. Это применимо к дронам, роботам и беспилотным автомобилям для повышения автономности и безопасности.

8. Культурное наследие и туризм:

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

9. Художественные и творческие проекты:

  • Используйте SLM для анализа и генерации творческого контента: музыки, искусства и литературы. Это может стимулировать инновационные проекты в креативных индустриях и создавать уникальный интерактивный опыт на выставках и представлениях.

10. Персонализированные вспомогательные технологии:

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

Ресурсы

Обнаружение объектов

Лаборатория 1950-х: Raspberry Pi снимает колёса и кубики на верстаке
Промпт DALL·E: обложка для главы «Обнаружение объектов» руководства по Raspberry Pi, выполненная в том же винтажном стиле лаборатории электроники 1950-х, что и предыдущие обложки. На переднем плане на верстаке — колёса и кубики, похожие на предоставленные пользователем. Raspberry Pi с подключённым модулем камеры снимает эти объекты. Сцену окружают классические лабораторные инструменты: паяльники, резисторы и провода. На фоне лаборатории — старинное оборудование: осциллографы и ламповые радиоприёмники, сохраняющие детальное и ностальгическое ощущение эпохи. Логотипов и текста быть не должно.

Обзор

Продолжая разговор о классификации изображений, перейдём к более сложной задаче компьютерного зрения — обнаружению объектов. Классификация изображений присваивает одну метку всему изображению, а обнаружение объектов идёт дальше: оно находит и определяет положение нескольких объектов на одном изображении. Эта возможность открывает много новых применений и задач, особенно в периферийных вычислениях и IoT-устройствах вроде Raspberry Pi.

Обнаружение объектов объединяет задачи классификации и локализации. Оно не только определяет, какие объекты присутствуют на изображении, но и указывает их положение, например рисуя вокруг них ограничивающие рамки (bounding boxes). Эта дополнительная сложность делает обнаружение объектов более мощным инструментом для понимания визуальных сцен, но требует более сложных моделей и методов обучения.

В периферийном ИИ (edge AI), где мы работаем с ограниченными вычислительными ресурсами, эффективные модели обнаружения объектов становятся критически важными. Проблемы, с которыми мы столкнулись при классификации изображений — баланс между размером модели, скоростью инференса и точностью, — при обнаружении объектов усиливаются. Однако и отдача выше, поскольку обнаружение объектов позволяет гораздо более тонко и подробно анализировать визуальные данные.

Некоторые применения обнаружения объектов на периферийных устройствах:

  1. Системы наблюдения и безопасности
  2. Автономные автомобили и дроны
  3. Контроль качества в промышленности
  4. Мониторинг дикой природы
  5. Приложения дополненной реальности

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

  • одностадийные детекторы (Single Stage Detectors), такие как MobileNet и EfficientDet,
  • FOMO (Faster Objects, More Objects) и
  • YOLO (You Only Look Once — «ты смотришь только один раз»).

Чтобы узнать больше о моделях обнаружения объектов, пройдите учебник «A Gentle Introduction to Object Recognition With Deep Learning» (на английском языке): machinelearningmastery.com.

Мы будем исследовать эти модели обнаружения объектов с помощью:

  • TensorFlow Lite Runtime (теперь переименован в LiteRT),
  • Edge Impulse Linux Python SDK и
  • Ultralytics

Блок-схема конвейера обнаружения объектов на Raspi

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

Основы обнаружения объектов

Обнаружение объектов опирается на основы классификации изображений, но значительно расширяет её возможности. Чтобы понять обнаружение объектов, сначала важно осознать ключевые различия с классификацией изображений:

Классификация изображений и обнаружение объектов

Классификация изображений:

  • Присваивает одну метку всему изображению
  • Отвечает на вопрос: «Какой основной объект или сцена на этом изображении?»
  • Выдаёт одно предсказание класса для всего изображения

Обнаружение объектов:

  • Находит и определяет положение нескольких объектов на изображении
  • Отвечает на вопросы: «Какие объекты есть на этом изображении и где они находятся?»
  • Выдаёт несколько предсказаний, каждое из которых состоит из метки класса и ограничивающей рамки

Чтобы наглядно показать эту разницу, рассмотрим пример:

Сравнение классификации изображений и обнаружения объектов

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

Ключевые компоненты обнаружения объектов

Системы обнаружения объектов обычно состоят из двух основных компонентов:

  1. Локализация объектов: определяет, где на изображении находятся объекты. Обычно выдаёт ограничивающие рамки — прямоугольные области, охватывающие каждый найденный объект.

  2. Классификация объектов: определяет класс или категорию каждого найденного объекта. Это похоже на классификацию изображений, но применяется к каждой локализованной области.

Проблемы обнаружения объектов

Обнаружение объектов сталкивается с рядом проблем, помимо тех, что есть у классификации изображений:

  • Несколько объектов: на изображении может быть много объектов разных классов, размеров и положений.
  • Разные масштабы: объекты могут выглядеть по-разному в зависимости от размера на изображении.
  • Окклюзия: объекты могут быть частично скрыты или перекрывать друг друга.
  • Загромождённый фон: отличить объекты от сложного фона может быть трудно.
  • Работа в реальном времени: многие приложения требуют быстрого инференса, особенно на периферийных устройствах.

Подходы к обнаружению объектов

Есть два основных подхода к обнаружению объектов:

  1. Двухстадийные детекторы: сначала предлагают интересующие области, а затем классифицируют каждую из них. Примеры: R-CNN и его варианты (Fast R-CNN, Faster R-CNN).

  2. Одностадийные детекторы: предсказывают ограничивающие рамки (или центроиды) и вероятности классов за один проход сети. Примеры: YOLO (You Only Look Once), EfficientDet, SSD (Single Shot Detector) и FOMO (Faster Objects, More Objects). Они часто быстрее и лучше подходят для периферийных устройств вроде Raspberry Pi.

Метрики оценки

Обнаружение объектов использует метрики, отличные от метрик классификации изображений:

  • Intersection over Union (IoU): измеряет перекрытие предсказанной и истинной ограничивающих рамок.
  • Mean Average Precision (mAP): объединяет точность (precision) и полноту (recall) по всем классам и порогам IoU.
  • Кадры в секунду (FPS): измеряют скорость обнаружения, что важно для приложений реального времени на периферийных устройствах.

Обзор предобученных моделей обнаружения объектов

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

Некоторые распространённые модели можно протестировать онлайн на странице Object Detection — MediaPipe Studio.

На Kaggle можно найти наиболее распространённые предобученные модели TFLite для Raspi: ssd_mobilenet_v1 и EfficientDet. Эти модели обучены на наборе данных COCO (Common Objects in Context), содержащем более 200 000 размеченных изображений в 91 категории. Скачайте модели и загрузите их в каталог ./models на Raspi.

Модели и метки COCO также можно найти на GitHub.

В первой части лабораторной работы мы сосредоточимся на предобученной модели SSD-Mobilenet V1 размером и сравним её с EfficientDet-lite0 размером , также обученной на наборе COCO 2017. Обе модели переведены в формат TensorFlow Lite (4,2 МБ для SSD Mobilenet и 4,6 МБ для EfficientDet).

SSD-Mobilenet V2 или V3 рекомендуется для проектов с переносом обучения, но поскольку модель V1 в формате TFLite общедоступна, для этого обзора мы используем её.

Развёртывание предобученных моделей обнаружения на Raspi

Настройка окружения TFLite

Повторим шаги, выполненные в предыдущей практической работе «Классификация изображений»:

  • Обновление Raspberry Pi

  • Установка необходимых библиотек

  • Настройка виртуального окружения (необязательно, но рекомендуется)

source ~/tflite/bin/activate
  • Установка среды выполнения TensorFlow Lite

  • Установка дополнительных библиотек Python (внутри окружения)

Создание рабочего каталога

Поскольку в прошлой лабораторной работе мы создали папку Documents/TFLITE, теперь создадим папки для этой лабораторной работы по обнаружению объектов:

cd Documents/TFLITE/
mkdir OBJ_DETECT
cd OBJ_DETECT
mkdir images
mkdir models
cd models

Инференс и постобработка

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

Импортируйте нужные библиотеки:

import time
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
import tflite_runtime.interpreter as tflite

Загрузите модель TFLite и выделите тензоры:

model_path = "./models/ssd-mobilenet-v1-tflite-default-v1.tflite"
interpreter = tflite.Interpreter(model_path=model_path)
interpreter.allocate_tensors()

Получите входные и выходные тензоры.

input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()

Входные данные (input details) сообщают, как модели нужно подавать изображение. Форма (1, 300, 300, 3) с dtype uint8 означает, что ненормализованное изображение (значения пикселей от 0 до 255) размером подаётся по одному (размерность батча: 1).

Выходные данные (output details) включают не только метки («classes») и вероятности («scores»), но и относительное положение ограничивающих рамок («boxes») — где на изображении находится объект, — а также количество найденных объектов («num_detections»). Выходные данные также показывают, что модель может обнаружить не более 10 объектов на изображении.

Результат инференса SSD-Mobilenet: классы, вероятности и рамки

Итак, для приведённого примера — того же изображения кота, что и в лабораторной по классификации изображений, — мы получаем вероятность 76 % обнаружения объекта с ID класса 16 в области, ограниченной ограничивающей рамкой [0.028011084, 0.020121813, 0.9886069, 0.802299]. Четыре числа соответствуют ymin, xmin, ymax и xmax — координатам рамки.

Учитывая, что y идёт сверху вниз (от ymin до ymax), а x — слева направо (от xmin до xmax), мы фактически получаем координаты левого верхнего и правого нижнего углов. Зная эти два угла и размер изображения, можно нарисовать прямоугольник вокруг объекта, как показано на рисунке ниже:

Ограничивающая рамка вокруг кота на изображении

Далее нужно определить, что означает ID класса 16. Открыв файл coco_labels.txt как список, видим, что каждый элемент имеет свой индекс; для индекса 16 получаем, как и ожидалось, cat. Вероятность — это значение, возвращаемое из score.

Теперь загрузим несколько изображений с несколькими объектами для проверки.

img_path = "./images/cat_dog.jpeg"
orig_img = Image.open(img_path)

# Выводим изображение
plt.figure(figsize=(8, 8))
plt.imshow(orig_img)
plt.title("Original Image")
plt.show()

Исходное изображение с кошкой и собакой

На основе входных данных предобработаем изображение, изменив его форму и добавив размерность:

img = orig_img.resize(
    (input_details[0]["shape"][1], input_details[0]["shape"][2])
)
input_data = np.expand_dims(img, axis=0)
input_data.shape, input_data.dtype

Новая форма input_data — (1, 300, 300, 3) с dtype uint8, что соответствует ожиданиям модели.

Используя input_data, запустим интерпретатор, измерим задержку и получим выход:

start_time = time.time()
interpreter.set_tensor(input_details[0]["index"], input_data)
interpreter.invoke()
end_time = time.time()
inference_time = (
    end_time - start_time
) * 1000  # Переводим в миллисекунды
print("Inference time: {:.1f}ms".format(inference_time))

Задержка составляет около 800 мс, и мы получаем 4 отдельных выхода:

boxes = interpreter.get_tensor(output_details[0]["index"])[0]
classes = interpreter.get_tensor(output_details[1]["index"])[0]
scores = interpreter.get_tensor(output_details[2]["index"])[0]
num_detections = int(
    interpreter.get_tensor(output_details[3]["index"])[0]
)

При беглом осмотре видно, что модель обнаружила 2 объекта с оценкой выше 0,5:

for i in range(num_detections):
    if scores[i] > 0.5:  # Порог уверенности
        print(f"Object {i}:")
        print(f"  Bounding Box: {boxes[i]}")
        print(f"  Confidence: {scores[i]}")
        print(f"  Class: {classes[i]}")

Вывод в терминале: два обнаруженных объекта с рамками, классами и оценками

И можно также визуализировать результаты:

plt.figure(figsize=(12, 8))
plt.imshow(orig_img)
for i in range(num_detections):
    if scores[i] > 0.5:  # При необходимости настройте порог
        ymin, xmin, ymax, xmax = boxes[i]
        (left, right, top, bottom) = (
            xmin * orig_img.width,
            xmax * orig_img.width,
            ymin * orig_img.height,
            ymax * orig_img.height,
        )
        rect = plt.Rectangle(
            (left, top),
            right - left,
            bottom - top,
            fill=False,
            color="red",
            linewidth=2,
        )
        plt.gca().add_patch(rect)
        class_id = int(classes[i])
        class_name = labels[class_id]
        plt.text(
            left,
            top - 10,
            f"{class_name}: {scores[i]:.2f}",
            color="red",
            fontsize=12,
            backgroundcolor="white",
        )

Обнаруженные объекты с красными ограничивающими рамками и подписями

EfficientDet

EfficientDet технически не является моделью SSD (Single Shot Detector), однако имеет общие черты и опирается на те же идеи, что SSD и другие архитектуры обнаружения объектов:

  1. EfficientDet:

    • Разработана исследователями Google в 2019 году
    • В качестве магистральной сети (backbone) использует EfficientNet
    • Применяет новую двунаправленную пирамиду признаков (BiFPN)
    • Использует составное масштабирование (compound scaling), чтобы эффективно масштабировать магистральную сеть и компоненты обнаружения
  2. Сходство с SSD:

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

    • Магистраль: SSD обычно использует VGG или MobileNet, а EfficientDet — EfficientNet
    • Слияние признаков: SSD использует простую пирамиду признаков, а EfficientDet — более продвинутую BiFPN
    • Метод масштабирования: EfficientDet вводит составное масштабирование для всех компонентов сети
  4. Преимущества EfficientDet:

    • Как правило, обеспечивает лучший компромисс между точностью и эффективностью, чем SSD и многие другие модели обнаружения объектов
    • Более гибкое масштабирование позволяет получить семейство моделей с разными компромиссами между размером и производительностью

Хотя EfficientDet не является моделью SSD, её можно рассматривать как эволюцию одностадийных архитектур обнаружения: она использует более продвинутые методы для повышения эффективности и точности. При работе с EfficientDet можно ожидать выходную структуру, похожую на SSD (например, ограничивающие рамки и оценки классов).

На GitHub можно найти ещё один блокнот, в котором исследуется модель EfficientDet — по аналогии с SSD MobileNet.

Проект обнаружения объектов

Теперь разработаем полноценный проект обнаружения объектов — от сбора данных до обучения и развёртывания. Как и в проекте классификации изображений, обученная и сконвертированная модель будет использоваться для инференса.

Мы будем использовать один и тот же набор данных для обучения трёх моделей: SSD-MobileNet V2, FOMO и YOLO.

Цель

Любой проект машинного обучения начинается с цели. Представим, что мы работаем на промышленном предприятии и должны сортировать и подсчитывать колёса и специальные коробки.

Колёса и коробки, которые нужно сортировать и подсчитывать

Иными словами, нам нужна многометочная классификация (multi-label classification), где на каждом изображении может присутствовать три класса:

  • Фон (нет объектов)
  • Коробка
  • Колесо

Сбор исходных данных

Определив цель проекта машинного обучения, переходим к следующему, самому важному шагу — сбору набора данных. Исходный набор без меток можно создать с помощью телефона, Raspi или их комбинации. Воспользуемся простым веб-приложением на нашей Raspberry Pi, чтобы просматривать в браузере снимки в формате QVGA (320 x 240).

Из репозитория на GitHub возьмите Python-скрипт get_img_data.py и запустите его в терминале:

python3 get_img_data.py

Откройте веб-интерфейс:

  • На самой Raspberry Pi (если есть графический интерфейс): откройте веб-браузер и перейдите по адресу http://localhost:5000
  • С другого устройства в той же сети: откройте веб-браузер и перейдите по адресу http://<raspberry_pi_ip>:5000 (замените <raspberry_pi_ip> на IP-адрес вашей Raspberry Pi). Например: http://192.168.4.210:5000/

Веб-интерфейс для захвата и организации набора изображений

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

Откройте веб-интерфейс в браузере, введите общую метку для изображений, которые хотите снять, и нажмите Start Capture.

Ввод метки и запуск захвата в веб-интерфейсе

Обратите внимание: снятые изображения будут иметь несколько меток, которые определяются позже.

Используйте живой предпросмотр, чтобы навести камеру, и нажмите Capture Image, чтобы сохранить изображения под текущей меткой (в данном случае box-wheel).

Снимки с меткой box-wheel в веб-интерфейсе

Когда изображений станет достаточно, нажмите Stop Capture. Снятые изображения сохраняются в папку dataset/box-wheel:

Структура папок набора данных dataset/box-wheel

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

Разметка данных

Следующий шаг проекта обнаружения объектов — создание размеченного набора данных. Нужно разметить исходные изображения, обведя рамками объекты на каждом снимке (коробку и колесо). Для разметки можно использовать инструменты вроде LabelImg, CVAT, Roboflow или даже Edge Impulse Studio. Инструмент Edge Impulse мы уже изучили в других лабораторных, а здесь воспользуемся Roboflow.

Мы используем бесплатную версию Roboflow по двум основным причинам. Во-первых, там есть автоматическая разметка (auto-labeler). Во-вторых, размеченный набор доступен во многих форматах и может использоваться как в Edge Impulse Studio (его мы применим для обучения MobileNet V2 и FOMO), так и в Colab (например, для обучения YOLOv8). Размеченный набор в Edge Impulse (с бесплатным аккаунтом) нельзя использовать для обучения на других платформах.

Загрузите исходный набор данных в Roboflow. Создайте там бесплатный аккаунт и новый проект, например «box-versus-wheel».

Создание нового проекта в Roboflow

Подробно процесс работы в Roboflow описывать не будем, так как для него существует много обучающих материалов.

Разметка изображений

Когда проект создан и набор данных загружен, выполните разметку с помощью инструмента «Auto-Label». Можно также загружать изображения только с фоном: их нужно сохранить без разметки.

Разметка изображения: рамки вокруг коробки и колеса

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

Разделение набора данных на обучающую, валидационную и тестовую выборки в Roboflow

Предобработка данных

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

Для аугментации мы будем поворачивать изображения (на ±15°), обрезать их, а также менять яркость и экспозицию.

Настройки предобработки и аугментации в Roboflow

В конце процесса у нас будет 153 изображения.

Итоговый набор данных: 153 изображения

Теперь экспортируем размеченный набор данных в формате, который понимают Edge Impulse, Ultralytics и другие фреймворки и инструменты, например YOLOv8. Скачаем архивную версию набора данных на рабочий стол.

Экспорт набора данных в формате YOLOv8

Здесь можно посмотреть, как устроен набор данных:

Структура папок экспортированного набора данных

Есть 3 отдельные папки — по одной на каждую выборку (train, test, valid). В каждой из них есть по 2 подпапки: images и labels. Изображения хранятся как image_id.jpg, а файлы разметки — как image_id.txt, где «image_id» уникален для каждого снимка.

Формат файла разметки: class_id и координаты ограничивающей рамки, где в нашем случае class_id равен 0 для box и 1 для wheel. Числовые идентификаторы (0, 1, 2…) следуют алфавитному порядку названий классов.

Файл data.yaml содержит информацию о наборе данных, в том числе названия классов (names: ['box', 'wheel']) в формате YOLO.

Вот и всё! Теперь можно начинать обучение в Edge Impulse Studio (как мы сделаем на следующем шаге), в Ultralytics (как будем делать, когда будем рассматривать YOLO) или даже обучить модель с нуля в Colab (как мы делали с набором Cifar-10 в лабораторной по классификации изображений).

Предобработанный набор данных можно найти на сайте Roboflow.

Обучение модели SSD MobileNet в Edge Impulse Studio

Перейдите на сайт Edge Impulse Studio, войдите в систему (Login) или создайте аккаунт и начните новый проект.

Можно клонировать проект, разработанный для этой лабораторной работы: Raspi - Object Detection.

На вкладке проекта Dashboard прокрутите вниз до блока Project info и в поле Labeling method выберите Bounding boxes (object detection).

Загрузка размеченных данных

В Studio откройте вкладку Data acquisition и в разделе UPLOAD DATA загрузите исходный набор данных с компьютера.

Можно использовать опцию Select a folder, выбрав, например, папку train на компьютере, которая содержит две подпапки: images и labels. Выберите формат меток «YOLO TXT», загрузите данные в категорию Training и нажмите Upload data.

Загрузка данных в Edge Impulse Studio

Повторите процедуру для тестовых данных (загрузите обе папки: test и validation). По окончании загрузки у вас будет размеченный набор из 153 изображений, разделённый на обучающую и тестовую выборки (84 % и 16 %).

Обратите внимание: метки будут храниться в файлах меток как 0 и 1, которые соответствуют box и wheel.

Размеченный набор данных в Edge Impulse Studio

Проектирование Impulse

Первое, что нужно задать на шаге Create impulse, — целевое устройство для развёртывания. Появится всплывающее окно. Мы выберем Raspberry Pi 4 — промежуточное устройство между Raspi-Zero и Raspi-5.

Этот выбор не влияет на обучение: он лишь даёт представление о задержке модели на этом конкретном устройстве.

Выбор целевого устройства Raspberry Pi 4 в Edge Impulse

На этом этапе нужно определить:

  • Предобработку: изменение размера отдельных изображений. В нашем случае изображения уже были предобработаны в Roboflow до , поэтому оставим как есть. Изменение размера здесь не важно, поскольку изображения квадратные. Если вы загрузите прямоугольное изображение, его нужно сжать до квадрата (без обрезки). Затем можно определить, преобразовывать ли изображения из RGB в оттенки серого.
  • Проектирование модели: в нашем случае «Object Detection» (обнаружение объектов).

Схема Impulse для обнаружения объектов

Предобработка всего набора данных

В разделе Image выберите Color depth как RGB и нажмите Save parameters.

Настройки глубины цвета в разделе Image

Studio автоматически переходит к следующему разделу Generate features, где обрабатываются все образцы. В итоге получаем 480 объектов: 207 коробок и 273 колеса.

Генерация признаков: 480 объектов для обучения

Исследователь признаков (feature explorer) показывает, что после генерации признаков все образцы хорошо разделяются.

Проектирование модели, обучение и тестирование

Для обучения нужно выбрать предобученную модель. Используем MobileNetV2 SSD FPN-Lite (только 320x320). Это предобученная модель обнаружения объектов, которая находит до 10 объектов на изображении и выдаёт ограничивающую рамку для каждого из них. Размер модели — около 3,7 МБ. Она принимает RGB-вход размером пикселей.

Гиперпараметры обучения:

  • Эпохи: 25
  • Размер батча: 32
  • Скорость обучения (Learning Rate): 0,15

Для валидации во время обучения будет отложено 20 % набора данных (validation_dataset).

Результаты обучения модели SSD MobileNet в Edge Impulse

В итоге модель получает общую точность (precision) по метрике COCO mAP 88,8 %, что выше результата на тестовых данных (83,3 %).

Развёртывание модели

Есть два способа развернуть модель:

  • Модель TFLite: позволяет развернуть обученную модель в формате .tflite, чтобы Raspi запускал её с помощью Python.
  • Linux (AARCH64): бинарный файл для Linux (AARCH64) реализует протокол Edge Impulse Linux, который позволяет запускать модели на любой плате разработки на базе Linux, с SDK, например для Python. Подробнее см. в документации и инструкциях по установке.

Развернём модель TFLite. На вкладке Dashboard перейдите к модели Transfer learning (int8 quantized) и нажмите значок загрузки:

Загрузка int8-квантованной модели с вкладки Dashboard

Перенесите модель с компьютера в папку ./models на Raspi, а также сделайте снимки или получите изображения для инференса и сохраните их в папку ./images.

Инференс и постобработка

Инференс можно выполнить так, как описано в разделе «Обзор предобученных моделей обнаружения объектов». Начнём новый блокнот, чтобы пройти все шаги по обнаружению кубиков и колёс на изображении.

Импортируйте нужные библиотеки:

import time
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from PIL import Image
import tflite_runtime.interpreter as tflite

Укажите путь к модели и метки:

model_path = "./models/ei-raspi-object-detection-SSD-MobileNetv2-320x0320-int8.lite"
labels = ["box", "wheel"]

Помните, что модель будет выводить ID классов как значения (0 и 1), следуя алфавитному порядку названий классов.

Загрузите модель, выделите тензоры и получите информацию о входных и выходных тензорах:

# Загружаем модель TFLite
interpreter = tflite.Interpreter(model_path=model_path)
interpreter.allocate_tensors()

# Получаем входные и выходные тензоры
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()

Важное отличие: dtype входных данных модели теперь int8, то есть входные значения лежат в диапазоне от –128 до +127, тогда как каждый пиксель нашего исходного изображения — от 0 до 255. Поэтому изображение нужно предобработать. Проверим:

input_dtype = input_details[0]["dtype"]
input_dtype
numpy.int8

Теперь откроем изображение и выведем его:

# Загружаем изображение
img_path = "./images/box_2_wheel_2.jpg"
orig_img = Image.open(img_path)

# Выводим изображение
plt.figure(figsize=(6, 6))
plt.imshow(orig_img)
plt.title("Original Image")
plt.show()

Исходное изображение с коробками и колёсами

И выполним предобработку:

scale, zero_point = input_details[0]["quantization"]
img = orig_img.resize(
    (input_details[0]["shape"][1], input_details[0]["shape"][2])
)
img_array = np.array(img, dtype=np.float32) / 255.0
img_array = (
    (img_array / scale + zero_point).clip(-128, 127).astype(np.int8)
)
input_data = np.expand_dims(img_array, axis=0)

Проверим входные данные, чтобы убедиться, что входной тензор соответствует ожиданиям модели:

input_data.shape, input_data.dtype
((1, 320, 320, 3), dtype('int8'))

Теперь выполним инференс. Также вычислим задержку модели:

# Инференс на Raspi-Zero
start_time = time.time()
interpreter.set_tensor(input_details[0]["index"], input_data)
interpreter.invoke()
end_time = time.time()
inference_time = (
    end_time - start_time
) * 1000  # Переводим в миллисекунды
print("Inference time: {:.1f}ms".format(inference_time))

На инференс модель затратит около 600 мс на Raspi-Zero, то есть примерно в 5 раз дольше, чем на Raspi-5.

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

boxes = interpreter.get_tensor(output_details[1]["index"])[0]
classes = interpreter.get_tensor(output_details[3]["index"])[0]
scores = interpreter.get_tensor(output_details[0]["index"])[0]
num_detections = int(
    interpreter.get_tensor(output_details[2]["index"])[0]
)
for i in range(num_detections):
    if scores[i] > 0.5:  # Порог уверенности
        print(f"Object {i}:")
        print(f"  Bounding Box: {boxes[i]}")
        print(f"  Confidence: {scores[i]}")
        print(f"  Class: {classes[i]}")

Текстовый вывод результатов обнаружения: рамки, уверенность и классы

По результатам видно, что обнаружено 4 объекта: два с ID класса 0 (box) и два с ID класса 1 (wheel). Это верно!

Визуализируем результат при пороге (threshold) 0,5:

threshold = 0.5
plt.figure(figsize=(6, 6))
plt.imshow(orig_img)
for i in range(num_detections):
    if scores[i] > threshold:
        ymin, xmin, ymax, xmax = boxes[i]
        (left, right, top, bottom) = (
            xmin * orig_img.width,
            xmax * orig_img.width,
            ymin * orig_img.height,
            ymax * orig_img.height,
        )
        rect = plt.Rectangle(
            (left, top),
            right - left,
            bottom - top,
            fill=False,
            color="red",
            linewidth=2,
        )
        plt.gca().add_patch(rect)
        class_id = int(classes[i])
        class_name = labels[class_id]
        plt.text(
            left,
            top - 10,
            f"{class_name}: {scores[i]:.2f}",
            color="red",
            fontsize=12,
            backgroundcolor="white",
        )

Визуализация обнаружения при пороге 0,5: коробки и колёса с рамками

Но что будет, если уменьшить порог, например до 0,3?

Множественные обнаружения при пороге 0,3

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

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

Чтобы улучшить результаты обнаружения, нужно применить подавление не-максимумов (Non-Maximum Suppression, NMS). Оно убирает перекрывающиеся ограничивающие рамки и оставляет только самое уверенное обнаружение.

Для этого создадим общую функцию non_max_suppression(), которая уточняет результаты обнаружения, отбрасывая избыточные и перекрывающиеся рамки. Она итеративно выбирает обнаружение с наибольшей оценкой уверенности и удаляет другие существенно перекрывающиеся обнаружения по порогу Intersection over Union (IoU).

def non_max_suppression(boxes, scores, threshold):
    # Переводим в координаты углов
    x1 = boxes[:, 0]
    y1 = boxes[:, 1]
    x2 = boxes[:, 2]
    y2 = boxes[:, 3]

    areas = (x2 - x1 + 1) * (y2 - y1 + 1)
    order = scores.argsort()[::-1]

    keep = []
    while order.size > 0:
        i = order[0]
        keep.append(i)
        xx1 = np.maximum(x1[i], x1[order[1:]])
        yy1 = np.maximum(y1[i], y1[order[1:]])
        xx2 = np.minimum(x2[i], x2[order[1:]])
        yy2 = np.minimum(y2[i], y2[order[1:]])

        w = np.maximum(0.0, xx2 - xx1 + 1)
        h = np.maximum(0.0, yy2 - yy1 + 1)
        inter = w * h
        ovr = inter / (areas[i] + areas[order[1:]] - inter)

        inds = np.where(ovr <= threshold)[0]
        order = order[inds + 1]

    return keep

Как это работает:

  1. Сортировка: сначала все обнаружения сортируются по оценке уверенности от высокой к низкой.
  2. Выбор: выбирается рамка с наибольшей оценкой и добавляется в итоговый список обнаружений.
  3. Сравнение: выбранная рамка сравнивается со всеми оставшимися рамками с меньшими оценками.
  4. Удаление: любая рамка, которая существенно перекрывается с выбранной (выше порога IoU), удаляется.
  5. Итерация: процесс повторяется со следующей по оценке рамкой, пока не будут обработаны все рамки.

Теперь определим более точную функцию визуализации, которая учитывает порог IoU и выводит только объекты, отобранные функцией non_max_suppression:

def visualize_detections(
    image, boxes, classes, scores, labels, threshold, iou_threshold
):
    if isinstance(image, Image.Image):
        image_np = np.array(image)
    else:
        image_np = image
    height, width = image_np.shape[:2]
    # Переводим нормализованные координаты в координаты пикселей
    boxes_pixel = boxes * np.array([height, width, height, width])
    # Применяем NMS
    keep = non_max_suppression(boxes_pixel, scores, iou_threshold)
    # Задаём размер фигуры 12x8 дюймов
    fig, ax = plt.subplots(1, figsize=(12, 8))
    ax.imshow(image_np)
    for i in keep:
        if scores[i] > threshold:
            ymin, xmin, ymax, xmax = boxes[i]
            rect = patches.Rectangle(
                (xmin * width, ymin * height),
                (xmax - xmin) * width,
                (ymax - ymin) * height,
                linewidth=2,
                edgecolor="r",
                facecolor="none",
            )

            ax.add_patch(rect)
            class_name = labels[int(classes[i])]
            ax.text(
                xmin * width,
                ymin * height - 10,
                f"{class_name}: {scores[i]:.2f}",
                color="red",
                fontsize=12,
                backgroundcolor="white",
            )
    plt.show()

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

def detect_objects(img_path, conf=0.5, iou=0.5):
    orig_img = Image.open(img_path)
    scale, zero_point = input_details[0]["quantization"]
    img = orig_img.resize(
        (input_details[0]["shape"][1], input_details[0]["shape"][2])
    )
    img_array = np.array(img, dtype=np.float32) / 255.0
    img_array = (
        (img_array / scale + zero_point)
        .clip(-128, 127)
        .astype(np.int8)
    )
    input_data = np.expand_dims(img_array, axis=0)

    # Инференс на Raspi-Zero
    start_time = time.time()
    interpreter.set_tensor(input_details[0]["index"], input_data)
    interpreter.invoke()
    end_time = time.time()
    inference_time = (
        end_time - start_time
    ) * 1000  # Переводим в миллисекунды

    print("Inference time: {:.1f}ms".format(inference_time))

    # Извлекаем выходные данные
    boxes = interpreter.get_tensor(output_details[1]["index"])[0]
    classes = interpreter.get_tensor(output_details[3]["index"])[0]
    scores = interpreter.get_tensor(output_details[0]["index"])[0]
    num_detections = int(
        interpreter.get_tensor(output_details[2]["index"])[0]
    )

    visualize_detections(
        orig_img,
        boxes,
        classes,
        scores,
        labels,
        threshold=conf,
        iou_threshold=iou,
    )

Теперь запустим код с тем же изображением, с порогом уверенности 0,3, но с небольшим IoU:

img_path = "./images/box_2_wheel_2.jpg"
detect_objects(img_path, conf=0.3, iou=0.05)

Результат обнаружения с порогом уверенности 0,3 и малым порогом IoU

Обучение модели FOMO в Edge Impulse Studio

Инференс с моделью SSD MobileNet работает хорошо, но задержка оказалась существенной. Инференс занимал от 0,5 до 1,3 секунды на Raspi-Zero, то есть около 1 кадра в секунду (FPS) или меньше. Один из способов ускорить процесс — использовать FOMO (Faster Objects, More Objects).

Этот алгоритм машинного обучения позволяет подсчитывать несколько объектов и находить их положение на изображении в реальном времени, используя в 30 раз меньше вычислительной мощности и памяти, чем MobileNet SSD или YOLO. Главная причина в том, что другие модели вычисляют размер объекта, рисуя вокруг него квадрат (ограничивающую рамку), тогда как FOMO не учитывает размер и сообщает только, где находится объект, — через координаты его центроида.

Как работает FOMO

В типичном конвейере обнаружения объектов первый этап — извлечение признаков из входного изображения. FOMO использует MobileNetV2 для выполнения этой задачи. MobileNetV2 обрабатывает входное изображение и создаёт карту признаков, которая вычислительно эффективно отражает ключевые характеристики: текстуры, формы и границы объектов.

Извлечение признаков в FOMO с помощью MobileNetV2

Когда признаки извлечены, более простая архитектура FOMO, сфокусированная на обнаружении центральных точек, интерпретирует карту признаков, чтобы определить, где на изображении находятся объекты. На выходе получается сетка ячеек, где каждая ячейка показывает, обнаружен ли центр объекта. Для каждой ячейки модель выдаёт одну или несколько оценок уверенности, указывающих вероятность присутствия объекта.

Посмотрим, как это работает на изображении.

FOMO делит изображение на блоки пикселей с коэффициентом 8. Для входа сетка будет , для — и так далее. Затем FOMO пропускает через каждый блок пикселей классификатор, чтобы вычислить вероятность того, что в нём есть коробка или колесо, и определить области с наибольшей вероятностью содержать объект (если блок пикселей не содержит объектов, он классифицируется как background, фон). По итоговой области FOMO определяет координаты центроида этой области (относительно размеров изображения).

Сетка блоков FOMO и найденные центроиды объектов

Компромисс между скоростью и точностью:

  • Разрешение сетки: FOMO использует сетку фиксированного разрешения, поэтому каждая ячейка определяет, есть ли объект в своей части изображения. Точность локализации при этом невысока, зато модель быстрая и вычислительно лёгкая, что критично для периферийных устройств.
  • Обнаружение нескольких объектов: поскольку каждая ячейка независима, FOMO может одновременно обнаруживать несколько объектов, находя несколько центров.

Новое проектирование Impulse, обучение и тестирование

Вернитесь в Edge Impulse Studio, на вкладке Experiments создайте новый impulse. Теперь входные изображения должны быть (это ожидаемый входной размер для MobileNetV2).

Новый impulse для FOMO: вход 160×160

На вкладке Image сгенерируйте признаки и перейдите на вкладку Object detection.

Для обучения нужно выбрать предобученную модель. Используем FOMO (Faster Objects, More Objects) MobileNetV2 0.35.

Выбор модели FOMO MobileNetV2 0.35 в Edge Impulse

Гиперпараметры обучения:

  • Эпохи: 30
  • Размер батча: 32
  • Скорость обучения (Learning Rate): 0,001

Для валидации во время обучения будет отложено 20 % набора данных (validation_dataset). Аугментацию для оставшихся 80 % (train_dataset) мы не применяем, поскольку набор уже был аугментирован на этапе разметки в Roboflow.

В итоге модель получает общую F1-меру 93,3 % при впечатляющей задержке 8 мс (Raspi-4), что примерно в 60 раз меньше, чем у SSD MobileNetV2.

Результаты обучения FOMO: F1-мера и задержка

Обратите внимание: FOMO автоматически добавил третью метку background к двум ранее заданным классам box (0) и wheel (1).

На вкладке Model testing точность составила 94 %. Вот один из результатов на тестовом образце:

Результат на тестовом образце во вкладке Model testing

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

Развёртывание модели

Как и в предыдущем разделе, обученную модель можно развернуть как TFLite или как Linux (AARCH64). Сейчас развернём её как Linux (AARCH64) — бинарный файл, реализующий протокол Edge Impulse Linux.

Модели Edge Impulse for Linux поставляются в формате .eim. Этот исполняемый файл содержит наш «полный impulse», созданный в Edge Impulse Studio. Impulse состоит из блоков обработки сигнала и блоков обучения и аномалий, которые мы добавили и обучили. Он скомпилирован с оптимизациями под наш процессор или GPU (например, инструкции NEON для ядер ARM) и содержит простой слой межпроцессного взаимодействия (IPC) поверх Unix-сокета.

На вкладке Deploy выберите опцию Linux (AARCH64), модель int8 и нажмите Build.

Сборка модели для Linux (AARCH64) в Edge Impulse

Модель будет автоматически загружена на ваш компьютер.

На Raspi создайте новое рабочее место:

cd ~
cd Documents
mkdir EI_Linux
cd EI_Linux
mkdir models
mkdir images

Переименуйте модель для удобства, например в raspi-object-detection-linux-aarch64-FOMO-int8.eim, и перенесите её в новую папку ./models на Raspi. Также сделайте снимки или получите изображения для инференса и сохраните их в папку ./images.

Инференс и постобработка

Инференс будет выполняться с помощью Linux Python SDK. Эта библиотека позволяет запускать модели машинного обучения и собирать данные с датчиков на Linux-машинах с помощью Python. SDK имеет открытый исходный код и размещён на GitHub: edgeimpulse/linux-sdk-python.

Создадим виртуальное окружение для работы с Linux Python SDK:

python3 -m venv ~/eilinux
source ~/eilinux/bin/activate

Установим все необходимые библиотеки:

sudo apt-get update
sudo apt-get install libatlas-base-dev \
                     libportaudio0 libportaudio2
sudo apt-get install libportaudiocpp0 portaudio19-dev

pip3 install edge_impulse_linux -i https://pypi.python.org/simple
pip3 install Pillow matplotlib pyaudio opencv-contrib-python

sudo apt-get install portaudio19-dev
pip3 install pyaudio
pip3 install opencv-contrib-python

Сделайте модель исполняемой:

chmod +x raspi-object-detection-linux-aarch64-FOMO-int8.eim

Установите Jupyter Notebook в новом окружении:

pip3 install jupyter

Запустите блокнот локально (на Raspi-4 или 5 с рабочим столом):

jupyter notebook

или в браузере на вашем компьютере:

jupyter notebook --ip=192.168.4.210 --no-browser

Начнём новый блокнот, выполнив все шаги по обнаружению кубиков и колёс на изображении с помощью модели FOMO и Edge Impulse Linux Python SDK.

Импортируйте нужные библиотеки:

import sys, time
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from PIL import Image
import cv2
from edge_impulse_linux.image import ImageImpulseRunner

Укажите путь к модели и метки:

model_file = "raspi-object-detection-linux-aarch64-FOMO-int8.eim"
model_path = "models/" + model_file  # Обученная ML-модель из Edge Impulse
labels = ["box", "wheel"]

Помните, что модель будет выдавать ID классов как значения (0 и 1), следуя алфавитному порядку названий классов.

Загрузите и инициализируйте модель:

# Загружаем файл модели
runner = ImageImpulseRunner(model_path)

# Инициализируем модель
model_info = runner.init()

Переменная model_info содержит важную информацию о модели. Однако в отличие от интерпретатора TFLite библиотека EI Linux Python SDK сама подготовит модель к инференсу.

Теперь откроем изображение и выведем его. Для совместимости используем OpenCV — библиотеку компьютерного зрения, которую EI применяет внутри. OpenCV читает изображения в формате BGR, поэтому его нужно преобразовать в RGB:

# Загружаем изображение
img_path = "./images/1_box_1_wheel.jpg"
orig_img = cv2.imread(img_path)
img_rgb = cv2.cvtColor(orig_img, cv2.COLOR_BGR2RGB)

# Выводим изображение
plt.imshow(img_rgb)
plt.title("Original Image")
plt.show()

Исходное изображение с коробкой и колесом

Теперь получим признаки и предобработанное изображение (cropped) с помощью runner:

features, cropped = (
    runner.get_features_from_image_auto_studio_settings(img_rgb)
)

Выполним инференс. Также вычислим задержку модели:

res = runner.classify(features)

Получим выходные классы обнаруженных объектов, центроиды их ограничивающих рамок и вероятности:

print(
    "Found %d bounding boxes (%d ms.)"
    % (
        len(res["result"]["bounding_boxes"]),
        res["timing"]["dsp"] + res["timing"]["classification"],
    )
)
for bb in res["result"]["bounding_boxes"]:
    print(
        "\t%s (%.2f): x=%d y=%d w=%d h=%d"
        % (
            bb["label"],
            bb["value"],
            bb["x"],
            bb["y"],
            bb["width"],
            bb["height"],
        )
    )
Found 2 bounding boxes (29 ms.)
	1 (0.91): x=112 y=40 w=16 h=16
	0 (0.75): x=48 y=56 w=8 h=8

Результаты показывают, что обнаружено два объекта: один с ID класса 0 (box) и один с ID класса 1 (wheel). Это верно!

Визуализируем результат (threshold равен 0,5 — это значение по умолчанию, заданное при тестировании модели в Edge Impulse Studio).

print(
    "\tFound %d bounding boxes (latency: %d ms)"
    % (
        len(res["result"]["bounding_boxes"]),
        res["timing"]["dsp"] + res["timing"]["classification"],
    )
)
plt.figure(figsize=(5, 5))
plt.imshow(cropped)

# Перебираем каждую из возвращённых ограничивающих рамок
bboxes = res["result"]["bounding_boxes"]
for bbox in bboxes:

    # Получаем углы ограничивающей рамки
    left = bbox["x"]
    top = bbox["y"]
    width = bbox["width"]
    height = bbox["height"]

    # Рисуем кружок в центре обнаружения
    circ = plt.Circle(
        (left + width // 2, top + height // 2),
        5,
        fill=False,
        color="red",
        linewidth=3,
    )
    plt.gca().add_patch(circ)
    class_id = int(bbox["label"])
    class_name = labels[class_id]
    plt.text(
        left,
        top - 10,
        f'{class_name}: {bbox["value"]:.2f}',
        color="red",
        fontsize=12,
        backgroundcolor="white",
    )
plt.show()

Результат FOMO: обнаруженные объекты отмечены кружками с подписями

Исследуем модель YOLO с помощью Ultralytics

В этой лабораторной работе мы исследуем YOLOv8. Ultralytics YOLOv8 — версия известной модели для обнаружения объектов и сегментации изображений в реальном времени, YOLO. YOLOv8 построена на передовых достижениях глубокого обучения и компьютерного зрения и обеспечивает выдающуюся скорость и точность. Её упрощённая архитектура подходит для разных приложений и легко адаптируется к разным аппаратным платформам — от периферийных устройств до облачных API.

Введение в модель YOLO

Модель YOLO (You Only Look Once) — высокоэффективный и широко используемый алгоритм обнаружения объектов, известный возможностями обработки в реальном времени. В отличие от традиционных систем обнаружения, которые адаптируют классификаторы или локализаторы для выполнения обнаружения, YOLO формулирует задачу обнаружения как единую задачу регрессии. Такой подход позволяет YOLO одновременно предсказывать несколько ограничивающих рамок и вероятности их классов по всему изображению за один проход, что заметно повышает скорость.

Ключевые особенности

  1. Единая архитектура сети:

    • YOLO использует одну нейронную сеть для обработки всего изображения. Сеть делит изображение на сетку и для каждой ячейки сетки напрямую предсказывает ограничивающие рамки и связанные с ними вероятности классов. Такое сквозное обучение повышает скорость и упрощает архитектуру модели.
  2. Обработка в реальном времени:

    • Одна из сильных сторон YOLO — возможность обнаруживать объекты в реальном времени. В зависимости от версии и оборудования YOLO может обрабатывать изображения с высокой частотой кадров (FPS). Это делает её идеальной для приложений, которым нужно быстрое и точное обнаружение объектов, например видеонаблюдения, автономного вождения и анализа спортивных трансляций.
  3. Эволюция версий:

    • За годы YOLO значительно улучшилась: от YOLOv1 до новейшей YOLOv10. Каждая итерация приносила улучшения точности, скорости и эффективности. YOLOv8, например, включает улучшения архитектуры сети, усовершенствованные методики обучения и лучшую поддержку различного оборудования, что обеспечивает более надёжную работу.
    • Хотя YOLOv10 — новейший член семейства с многообещающими результатами, согласно её статье, она была выпущена только в мае 2024 года и пока не полностью интегрирована в библиотеку Ultralytics. При этом анализ кривых «точность — полнота» показывает, что YOLOv8 в целом превосходит YOLOv9: она захватывает большую долю истинно положительных результатов и эффективнее минимизирует ложноположительные (подробнее см. в этой статье). Поэтому эта лабораторная работа основана на YOLOv8n.

    Сравнение версий семейства YOLO

  4. Точность и эффективность:

    • Ранние версии YOLO жертвовали точностью ради скорости, но новые версии значительно продвинулись в балансе между ними. Новые модели быстрее и точнее: они обнаруживают мелкие объекты (например, пчёл) и хорошо справляются со сложными наборами данных.
  5. Широкий спектр применений:

    • Универсальность YOLO привела к её использованию во многих областях. Её применяют в системах мониторинга дорожного движения для обнаружения и подсчёта автомобилей, в системах безопасности для выявления потенциальных угроз и в агротехнологиях для мониторинга посевов и скота. Она подходит для любой области, где нужно эффективное и точное обнаружение объектов.
  6. Сообщество и развитие:

    • YOLO продолжает развиваться и поддерживается сильным сообществом разработчиков и исследователей (особенно YOLOv8). Открытые реализации и обширная документация сделали её доступной для настройки и интеграции в разные проекты. Популярные фреймворки глубокого обучения, включая Darknet, TensorFlow и PyTorch, поддерживают YOLO, что ещё больше расширяет её применимость.
    • Ultralytics YOLOv8 умеет не только обнаруживать объекты (наш случай), но и сегментировать и определять позы с моделями, предобученными на наборе COCO, а также выполнять классификацию с моделями YOLOv8, предобученными на наборе ImageNet. Режим отслеживания доступен для всех моделей обнаружения, сегментации и определения позы.

    Задачи, поддерживаемые Ultralytics YOLO

Установка

На Raspi деактивируем текущее окружение, чтобы создать новое рабочее место:

deactivate
cd ~
cd Documents/
mkdir YOLO
cd YOLO
mkdir models
mkdir images

Создадим виртуальное окружение для работы с Ultralytics YOLOv8:

python3 -m venv ~/yolo
source ~/yolo/bin/activate

И установим пакеты Ultralytics для локального инференса на Raspi:

  1. Обновите список пакетов, установите pip и обновите его до последней версии:
sudo apt update
sudo apt install python3-pip -y
pip install -U pip
  1. Установите pip-пакет ultralytics с дополнительными зависимостями:
pip install ultralytics[export]
  1. Перезагрузите устройство:
sudo reboot

Проверка YOLO

После загрузки Raspi-Zero активируйте окружение yolo, перейдите в рабочий каталог

source ~/yolo/bin/activate
cd ~/Documents/YOLO

и запустите инференс на изображении, которое будет загружено с сайта Ultralytics, с моделью YOLOv8n (самой маленькой в семействе) в терминале (CLI):

yolo predict model='yolov8n' \
     source='https://ultralytics.com/images/bus.jpg'

Семейство моделей YOLO предобучено на наборе данных COCO.

Результат инференса появится в терминале. На изображении (bus.jpg) обнаружены 4 persons (человека), 1 bus (автобус) и 1 stop signal (знак «стоп»).

Результат инференса YOLOv8n в терминале для bus.jpg

Также мы получили сообщение Results saved to runs/detect/predict. В этой папке появилось новое сохранённое изображение (bus.jpg). Скачаем его с Raspi-Zero на рабочий стол для просмотра:

Сохранённое изображение bus.jpg с обнаруженными объектами

Итак, Ultralytics YOLO корректно установлен на Raspi. Однако на Raspi-Zero проблема — высокая задержка инференса, около 18 секунд, даже для самой маленькой модели семейства (YOLOv8n).

Экспорт модели в формат NCNN

Развёртывание моделей компьютерного зрения на периферийных устройствах с ограниченной вычислительной мощностью, таких как Raspi-Zero, может приводить к проблемам с задержкой. Один из вариантов — использовать формат, оптимизированный для производительности. Это гарантирует, что даже устройства с ограниченной вычислительной мощностью смогут справляться с продвинутыми задачами компьютерного зрения.

Среди всех форматов экспорта, поддерживаемых Ultralytics, NCNN — высокопроизводительный фреймворк для вычислений нейронных сетей, оптимизированный для мобильных платформ. С самого начала NCNN проектировался с учётом развёртывания на мобильных телефонах и не имеет сторонних зависимостей. Он кроссплатформенный и работает быстрее, чем известные открытые фреймворки (например, TFLite).

NCNN обеспечивает лучшую производительность инференса на устройствах Raspberry Pi. Он хорошо оптимизирован для мобильных встраиваемых платформ (например, на архитектуре ARM).

Итак, конвертируем нашу модель и повторим инференс:

  1. Экспортируйте модель YOLOv8n PyTorch в формат NCNN. В результате будет создана папка /yolov8n_ncnn_model:
yolo export model=yolov8n.pt format=ncnn
  1. Запустите инференс с экспортированной моделью (источником может быть изображение bus.jpg, которое было загружено с сайта в текущий каталог при последнем инференсе):
yolo predict model='./yolov8n_ncnn_model' source='bus.jpg'

При первом инференсе, когда модель загружается, задержка обычно высокая (около 17 с), но начиная со второго инференса она снижается примерно до 2 с.

Исследование YOLO с помощью Python

Для начала запустим интерпретатор Python, чтобы построчно разобрать, как работает модель YOLO:

python3

Теперь подключим библиотеку YOLO из Ultralytics и загрузим модель:

from ultralytics import YOLO

model = YOLO("yolov8n_ncnn_model")

Далее выполним инференс на изображении (снова используем bus.jpg):

img = "bus.jpg"
result = model.predict(img, save=True, imgsz=640, conf=0.5, iou=0.3)

Результат инференса через Python для bus.jpg

Результат почти идентичен тому, что мы получили при инференсе в терминале (CLI). Исключение: знак остановки не был обнаружен уменьшенной моделью NCNN. Заметим, что задержка снизилась.

Разберём содержимое переменной result.

Например, result[0].boxes.data показывает основной результат инференса — тензор формы (4, 6). Каждая строка соответствует одному обнаруженному объекту: первые 4 столбца — координаты ограничивающей рамки, 5-й — уверенность, 6-й — класс (в данном случае 0: person и 5: bus):

Тензор результатов инференса для bus.jpg

Можно получить отдельные результаты инференса, например время инференса, и вывести его в удобном формате:

inference_time = int(result[0].speed["inference"])
print(f"Inference Time: {inference_time} ms")

Или общее количество обнаруженных объектов:

print(f"Number of objects: {len(result[0].boxes.cls)}")

Вывод времени инференса и количества объектов для bus.jpg

С помощью Python можно создать подробный вывод, отвечающий нашим потребностям (подробнее см. Model Prediction with Ultralytics YOLO). Запустим не строку за строкой в интерпретаторе, а Python-скрипт. Для редактирования используем nano. Сначала создадим пустой Python-скрипт, например yolov8_tests.py:

nano yolov8_tests.py

Введите строки кода:

from ultralytics import YOLO

# Загружаем модель YOLOv8
model = YOLO("yolov8n_ncnn_model")

# Выполняем инференс
img = "bus.jpg"
result = model.predict(img, save=False, imgsz=640, conf=0.5, iou=0.3)

# Выводим результаты
inference_time = int(result[0].speed["inference"])
print(f"Inference Time: {inference_time} ms")
print(f"Number of objects: {len(result[0].boxes.cls)}")

Код тестового скрипта yolov8_tests.py в редакторе nano

Чтобы сохранить Python-скрипт, нажмите [CTRL+O], затем [ENTER], после чего [CTRL+X].

Запустите скрипт:

python yolov8_tests.py

Результат будет таким же, как при инференсе в терминале (CLI) и во встроенном интерпретаторе Python.

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

Обучение YOLOv8 на собственном наборе данных

Вернёмся к нашему набору данных «Box versus Wheel», размеченному на Roboflow. На вкладке Download Dataset, вместо варианта Download a zip to computer, который мы использовали для обучения в Edge Impulse Studio, выберем Show download code. Этот вариант откроет всплывающее окно с фрагментом кода, который нужно вставить в наш блокнот для обучения.

Окно Show download code в Roboflow

Для обучения адаптируем один из публичных примеров Ultralytics и запустим его в Google Colab. Ниже — мой блокнот, который можно адаптировать для собственного проекта:

  • Обучение YOLOv8 на наборе данных Box versus Wheel [Open In Colab]

Ключевые моменты блокнота

  1. Запускайте его с GPU (NVIDIA T4 доступна бесплатно).

  2. Установите Ultralytics через PIP.

    Установка Ultralytics в блокноте Colab

  3. Теперь можно импортировать YOLO и загрузить набор данных в Colab, вставив код загрузки, полученный из Roboflow. Обратите внимание: наш набор будет смонтирован в /content/datasets/:

Загрузка набора данных в Colab

  1. Обязательно проверьте и исправьте файл data.yaml, указав правильные пути к изображениям (скопируйте путь к каждой папке images).
names:

- box
- wheel
nc: 2
roboflow:
  license: CC BY 4.0
  project: box-versus-wheel-auto-dataset
  url: https://universe.roboflow.com/marcelo-rovai-riila/ \
     box-versus-wheel-auto-dataset/dataset/5
  version: 5
  workspace: marcelo-rovai-riila
test: /content/datasets/Box-versus-Wheel-auto-dataset-5/ \
      test/images
train: /content/datasets/Box-versus-Wheel-auto-dataset-5/ \
       train/images
val: /content/datasets/Box-versus-Wheel-auto-dataset-5/ \
     valid/images
  1. Задайте основные гиперпараметры, которые хотите изменить по сравнению со значениями по умолчанию, например:

    MODEL = 'yolov8n.pt'
    IMG_SIZE = 640
    EPOCHS = 25 # Для итогового проекта стоит рассмотреть
                # не менее 100 эпох
    
  2. Запустите обучение (через CLI):

    !yolo task=detect mode=train model={MODEL} \
      data={dataset.location}/data.yaml \
      epochs={EPOCHS} \
      imgsz={IMG_SIZE} plots=True
    

Результаты обучения YOLOv8: графики потерь и метрик

Обучение заняло несколько минут и дало отличный результат (mAP50 = 0,995). По окончании все результаты сохраняются в указанной папке, например /runs/detect/train/. Там можно найти, например, матрицу ошибок (confusion matrix).

Матрица ошибок обучения YOLOv8

  1. Обученная модель (best.pt) сохраняется в папке /runs/detect/train/weights/. Теперь проверьте обученную модель на изображениях из valid/images:
!yolo task=detect mode=val model={HOME}/runs/detect/train/\
       weights/best.pt data={dataset.location}/data.yaml

Результаты оказались похожими на результаты обучения.

  1. Теперь выполните инференс на изображениях, отложенных для тестирования:
!yolo task=detect mode=predict model={HOME}/runs/detect/train/\
     weights/best.pt conf=0.25 source={dataset.location}/test/\
     images save=True

Результаты инференса сохраняются в папке runs/detect/predict. Посмотрим некоторые из них:

Результаты инференса YOLOv8 на тестовых изображениях

  1. Рекомендуется экспортировать результаты обучения, валидации и тестирования на Google Drive. Для этого нужно подключить диск:

    from google.colab import drive
    drive.mount('/content/gdrive')
    

    и скопировать содержимое папки /runs в папку на вашем Drive, например:

    !scp -r /content/runs '/content/gdrive/MyDrive/\
         10_UNIFEI/Box_vs_Wheel_Project'
    

Инференс обученной моделью на Raspi

Скачайте обученную модель /runs/detect/train/weights/best.pt на свой компьютер. С помощью FTP-клиента FileZilla перенесите best.pt в папку моделей на Raspi (перед переносом модель можно переименовать, например в box_wheel_320_yolo.pt).

С помощью FileZilla перенесите несколько изображений из тестового набора в папку ./YOLO/images:

Вернитесь в папку YOLO и запустите интерпретатор Python:

cd ..
python

Как и раньше, импортируем библиотеку YOLO и подключим нашу преобразованную модель для обнаружения коробок и колёс:

from ultralytics import YOLO

model = YOLO("./models/box_wheel_320_yolo.pt")

Теперь определим изображение и вызовем инференс (на этот раз сохраним результат в виде изображения для внешней проверки):

img = "./images/1_box_1_wheel.jpg"
result = model.predict(img, save=True, imgsz=320, conf=0.5, iou=0.3)

Повторите для нескольких изображений. Результат инференса сохраняется в переменной result, а обработанное изображение — в папке runs/detect/predict8.

Обработанное изображение с обнаруженными коробками и колёсами

С помощью FileZilla можно отправить результаты инференса на рабочий стол для проверки:

Результаты инференса, перенесённые с Raspi на компьютер

Результат инференса отличный! Модель обучена на базе меньшей модели семейства YOLOv8 (YOLOv8n). Проблема — задержка: около 1 секунды (или 1 FPS на Raspi-Zero). Конечно, эту задержку можно уменьшить, если конвертировать модель в TFLite или NCNN.

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

Все модели, рассмотренные в этой лабораторной работе, могут обнаруживать объекты в реальном времени с камеры. Захваченное изображение должно подаваться на вход обученной и сконвертированной модели. Для Raspi-4 или 5 с рабочим столом OpenCV может захватывать кадры и показывать результат инференса.

Однако создать видеопоток с веб-камеры для обнаружения объектов в реальном времени тоже возможно. Например, возьмём скрипт, разработанный для приложения классификации изображений, и адаптируем его под веб-приложение обнаружения объектов в реальном времени на основе TensorFlow Lite и Flask.

Эта версия приложения работает со всеми моделями TFLite. Убедитесь, что модель лежит в правильной папке, например:

model_path = "./models/ssd-mobilenet-v1-tflite-default-v1.tflite"

Скачайте Python-скрипт object_detection_app.py с GitHub.

В терминале выполните:

python3 object_detection_app.py

И откройте веб-интерфейс:

  • На самой Raspberry Pi (если есть графический интерфейс): откройте веб-браузер и перейдите по адресу http://localhost:5000.
  • С другого устройства в той же сети: откройте веб-браузер и перейдите по адресу http://<raspberry_pi_ip>:5000 (замените <raspberry_pi_ip> на IP-адрес вашей Raspberry Pi). Например: http://192.168.4.210:5000/

Вот несколько скриншотов приложения, запущенного на внешнем компьютере:

Веб-приложение обнаружения объектов, запущенное на внешнем компьютере

Рассмотрим техническое описание ключевых модулей, используемых в приложении обнаружения объектов:

  1. TensorFlow Lite (tflite_runtime):

    • Назначение: эффективный инференс моделей машинного обучения на периферийных устройствах.
    • Почему: TFLite даёт меньший размер модели и оптимизированную производительность по сравнению с полным TensorFlow, что критично для устройств с ограниченными ресурсами, таких как Raspberry Pi. Он поддерживает аппаратное ускорение и квантование, которые дополнительно повышают эффективность.
    • Ключевые функции: Interpreter для загрузки и запуска модели, get_input_details() и get_output_details() для взаимодействия с моделью.
  2. Flask:

    • Назначение: лёгкий веб-фреймворк для создания серверной части.
    • Почему: простота и гибкость Flask делают его идеальным для быстрой разработки и развёртывания веб-приложений. Он менее требователен к ресурсам, чем крупные фреймворки, что важно для периферийных устройств.
    • Ключевые компоненты: декораторы маршрутов для определения конечных точек API, объекты Response для потоковой передачи видео, render_template_string для отдачи динамического HTML.
  3. Picamera2:

    • Назначение: интерфейс к модулю камеры Raspberry Pi.
    • Почему: Picamera2 — современная библиотека для управления камерами Raspberry Pi, которая даёт лучшую производительность и возможности по сравнению с исходной библиотекой Picamera.
    • Ключевые функции: create_preview_configuration() для настройки камеры, capture_file() для захвата кадров.
  4. PIL (Python Imaging Library):

    • Назначение: обработка и редактирование изображений.
    • Почему: PIL даёт широкие возможности обработки изображений. Здесь он используется для изменения размера изображений, рисования ограничивающих рамок и преобразования между форматами.
    • Ключевые классы: Image для загрузки и обработки изображений, ImageDraw для рисования фигур и текста на изображениях.
  5. NumPy:

    • Назначение: эффективные операции с массивами и численные вычисления.
    • Почему: операции с массивами NumPy намного быстрее обычных списков Python, что критично для обработки данных изображений и входов и выходов модели.
    • Ключевые функции: array() для создания массивов, expand_dims() для добавления размерностей.
  6. Threading (потоки):

    • Назначение: параллельное выполнение задач.
    • Почему: потоки позволяют одновременно захватывать кадры, выполнять обнаружение объектов и работать веб-серверу, что критично для работы в реальном времени.
    • Ключевые компоненты: класс Thread создаёт отдельные потоки выполнения, а Lock используется для синхронизации потоков.
  7. io.BytesIO:

    • Назначение: двоичные потоки в памяти.
    • Почему: позволяет эффективно работать с данными изображений в памяти без временных файлов, что ускоряет работу и снижает число операций ввода-вывода.
  8. time:

    • Назначение: функции, связанные со временем.
    • Почему: используются для задержек (time.sleep()), чтобы управлять частотой кадров, и для измерения производительности.
  9. jQuery (на стороне клиента):

    • Назначение: упрощённая работа с DOM и AJAX-запросы.
    • Почему: позволяет динамически обновлять веб-интерфейс и обмениваться данными с сервером без перезагрузки страницы.
    • Ключевые функции: .get() и .post() для AJAX-запросов, методы работы с DOM для обновления интерфейса.

Архитектура системы приложения:

  1. Главный поток: запускает сервер Flask, обрабатывает HTTP-запросы и отдаёт веб-интерфейс.
  2. Поток камеры: непрерывно захватывает кадры с камеры.
  3. Поток обнаружения: пропускает кадры через модель TFLite для обнаружения объектов.
  4. Буфер кадров: общая область памяти (защищённая блокировками), в которой хранятся последний кадр и результаты обнаружения.

Поток данных приложения кратко:

  1. Камера захватывает кадр → буфер кадров.
  2. Поток обнаружения читает кадр из буфера → обрабатывает его моделью TFLite → обновляет результаты обнаружения в буфере кадров.
  3. Маршруты Flask обращаются к буферу кадров, чтобы отдать последний кадр и результаты обнаружения.
  4. Веб-клиент получает обновления через AJAX и обновляет интерфейс.

Такая архитектура обеспечивает эффективное обнаружение объектов в реальном времени и сохраняет отзывчивый веб-интерфейс на периферийном устройстве с ограниченными ресурсами, таком как Raspberry Pi. Потоки и эффективные библиотеки вроде TFLite и PIL позволяют обрабатывать видеокадры в реальном времени, а Flask и jQuery дают удобный способ взаимодействия с ними.

Вы можете протестировать приложение с другой предобработанной моделью, например EfficientDet, изменив строку в приложении:

model_path = "./models/lite-model_efficientdet_lite0_detection_metadata_1.tflite"

Если нужно использовать приложение с моделью SSD-MobileNetV2, обученной в Edge Impulse Studio на наборе «Box versus Wheel», код также нужно адаптировать под входные данные модели, как мы разобрали в её блокноте.

Итоги

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

  1. Сравнение моделей: мы изучили разные модели обнаружения объектов, включая SSD-MobileNet, EfficientDet, FOMO и YOLO, и сравнили их производительность и компромиссы на периферийных устройствах.
  2. Обучение и развёртывание: на собственном наборе данных с коробками и колёсами (размеченном в Roboflow) мы прошли процесс обучения моделей в Edge Impulse Studio и Ultralytics и развернули их на Raspberry Pi.
  3. Методы оптимизации: для повышения скорости инференса на периферийных устройствах мы рассмотрели такие методы, как квантование модели (TFLite int8) и конвертация формата (например, в NCNN).
  4. Приложения реального времени: лабораторная работа показала веб-приложение для обнаружения объектов в реальном времени, демонстрируя, как такие модели можно интегрировать в практические интерактивные системы.
  5. Соображения о производительности: на протяжении работы мы обсуждали баланс между точностью модели и скоростью инференса — критически важный аспект для приложений периферийного ИИ.

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

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

  • реализация конвейеров из нескольких моделей для более сложных задач;
  • изучение возможностей аппаратного ускорения для Raspberry Pi;
  • интеграция обнаружения объектов с другими датчиками для более полных систем периферийного ИИ;
  • разработка решений «периферия — облако», которые используют и локальную обработку, и облачные ресурсы.

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

Ресурсы

Настройка

Лаборатория электроники в стиле 1950-х с Raspberry Pi 5 на верстаке
Промпт DALL·E: лаборатория электроники в мультяшном стиле 1950-х. Винтажное оборудование, большие осциллографы, старые ламповые радиоприёмники и громоздкие компьютеры. Плата Raspberry Pi 5 крупным планом, в реальном размере (примерно как банковская карта), лежит на верстаке. Вокруг — классические инструменты: паяльник, резисторы и провода. Сцена яркая, с утрированными цветами и игривыми деталями, характерными для мультфильма. Логотипов и текста нет.

В этой главе описана настройка моделей Raspberry Pi Zero 2 W (Raspi-Zero) и Raspberry Pi 5 (Raspi-5). Мы рассмотрим настройку оборудования, установку операционной системы, первичную конфигурацию и проверки.

Общие инструкции для Raspi-5 применимы и к более старым версиям Raspberry Pi, например Raspi-3 и Raspi-4.

Обзор

Raspberry Pi — мощный и универсальный одноплатный компьютер, который стал незаменимым инструментом инженеров самых разных специальностей. Эти компактные устройства, разработанные Raspberry Pi Foundation, сочетают доступность, вычислительную мощность и широкие возможности GPIO (General Purpose Input/Output), что делает их идеальными для прототипирования, разработки встраиваемых систем и сложных инженерных проектов.

Ключевые особенности

  1. Вычислительная мощность. Несмотря на небольшой размер, Raspberry Pi обладает значительными возможностями: в новейших моделях — многоядерные процессоры ARM и до 8 ГБ оперативной памяти.

  2. Интерфейс GPIO. 40-контактный разъём GPIO позволяет напрямую взаимодействовать с датчиками, исполнительными устройствами и другими электронными компонентами, что облегчает проекты интеграции аппаратной и программной частей.

  3. Широкие возможности подключения. Встроенные Wi-Fi, Bluetooth, Ethernet и несколько портов USB позволяют реализовать разные коммуникационные и сетевые проекты.

  4. Низкоуровневый доступ к аппаратуре. Raspberry Pi предоставляет доступ к интерфейсам I2C, SPI и UART, что позволяет детально управлять внешними устройствами и обмениваться с ними данными.

  5. Возможности работы в реальном времени. При правильной настройке Raspberry Pi можно использовать для приложений мягкого реального времени, что делает его пригодным для систем управления и задач обработки сигналов.

  6. Энергоэффективность. Низкое энергопотребление позволяет создавать устройства с батарейным питанием и энергоэффективные решения, особенно на моделях Pi Zero.

Модели Raspberry Pi (рассматриваемые в этой книге)

  1. Raspberry Pi Zero 2 W (Raspi-Zero):

    • подходит для: компактных встраиваемых систем;
    • ключевые характеристики: четырёхъядерный процессор ARM Cortex-A53 с частотой 1 ГГц, 512 МБ ОЗУ, минимальное энергопотребление.
  2. Raspberry Pi 5 (Raspi-5):

    • подходит для: более требовательных задач, таких как периферийные вычисления, компьютерное зрение и периферийный ИИ, включая LLM;
    • ключевые характеристики: четырёхъядерный процессор ARM Cortex-A76 с частотой 2,4 ГГц, до 8 ГБ ОЗУ, интерфейс PCIe для расширений.

Инженерные применения

  1. Проектирование встраиваемых систем: разработка и прототипирование встраиваемых систем для реальных задач.

  2. IoT и сетевые устройства: создание взаимосвязанных устройств и изучение протоколов MQTT, CoAP и HTTP/HTTPS.

  3. Системы управления: реализация контуров обратной связи, ПИД-регуляторов и взаимодействие с исполнительными устройствами.

  4. Компьютерное зрение и ИИ: использование библиотек OpenCV и TensorFlow Lite для обработки изображений и машинного обучения на периферии.

  5. Сбор и анализ данных: сбор данных с датчиков, анализ в реальном времени и создание систем журналирования данных.

  6. Робототехника: создание контроллеров роботов, реализация алгоритмов планирования движения и взаимодействие с драйверами моторов.

  7. Обработка сигналов: анализ сигналов в реальном времени, фильтрация и приложения DSP.

  8. Сетевая безопасность: настройка VPN, межсетевых экранов и изучение тестирования на проникновение в сеть.

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

Обзор оборудования

Raspberry Pi Zero 2W

Плата Raspberry Pi Zero 2 W

  • Процессор: 64-битный четырёхъядерный Arm Cortex-A53 с частотой 1 ГГц;
  • ОЗУ: 512 МБ SDRAM;
  • Беспроводная связь: 2,4 ГГц 802.11 b/g/n (Wi-Fi), Bluetooth 4.2, BLE;
  • Порты: mini HDMI, micro USB OTG, разъём камеры CSI-2;
  • Питание: 5 В через порт micro USB.

Raspberry Pi 5

Плата Raspberry Pi 5

  • Процессор:
    • Pi 5: 64-битный четырёхъядерный Arm Cortex-A76 с частотой 2,4 ГГц;
    • Pi 4: 64-битная SoC с четырьмя ядрами Cortex-A72 (ARM v8) с частотой 1,5 ГГц;
  • ОЗУ: варианты 2, 4 или 8 ГБ (для задач ИИ рекомендуется 8 ГБ);
  • Беспроводная связь: двухдиапазонный 802.11ac, Bluetooth 5.0;
  • Порты: 2 × micro HDMI, 2 × USB 3.0, 2 × USB 2.0, разъём камеры CSI, разъём дисплея DSI;
  • Питание: 5 В постоянного тока через разъём USB-C (3 А).

В лабораторных работах мы будем использовать разные имена для Raspberry Pi: Raspi, Raspi-5, Raspi-Zero и т. д. Обычно Raspi используется, когда инструкция или комментарий относится ко всем моделям.

Установка операционной системы

Операционная система (ОС)

Операционная система (ОС) — это базовое программное обеспечение, которое управляет аппаратными и программными ресурсами компьютера и предоставляет стандартные сервисы для программ. Это основной программный слой, который работает на компьютере и выступает посредником между аппаратурой и прикладным программным обеспечением. ОС управляет памятью компьютера, процессами, драйверами устройств, файлами и протоколами безопасности.

  1. Основные функции:
    • управление процессами: выделение процессорного времени разным программам;
    • управление памятью: выделение и освобождение памяти по мере необходимости;
    • управление файловой системой: организация файлов и каталогов и учёт их расположения;
    • управление устройствами: взаимодействие с подключёнными аппаратными устройствами;
    • пользовательский интерфейс: предоставление способа взаимодействия пользователя с компьютером.
  2. Компоненты:
    • ядро (kernel): центральная часть ОС, которая управляет аппаратными ресурсами;
    • оболочка (shell): пользовательский интерфейс для работы с ОС;
    • файловая система: организует и управляет хранением данных;
    • драйверы устройств: программное обеспечение, которое позволяет ОС взаимодействовать с аппаратурой.

Raspberry Pi работает под специальной версией Linux, созданной для встраиваемых систем. Эта операционная система, обычно представляющая собой вариант Debian под названием Raspberry Pi OS (раньше — Raspbian), оптимизирована для архитектуры ARM и ограниченных ресурсов Pi.

Последняя версия Raspberry Pi OS основана на Debian Bookworm.

Ключевые особенности:

  1. Легковесность: адаптирована для эффективной работы на аппаратуре Pi.
  2. Универсальность: поддерживает широкий набор приложений и языков программирования.
  3. Открытый исходный код: позволяет настраивать систему и получать улучшения от сообщества.
  4. Поддержка GPIO: обеспечивает взаимодействие с датчиками и другой аппаратурой через контакты Pi.
  5. Регулярные обновления: система постоянно совершенствуется в части производительности и безопасности.

Встраиваемый Linux на Raspberry Pi предоставляет полноценную операционную систему в компактном корпусе, что делает его идеальным для проектов — от простых IoT-устройств до более сложных приложений периферийного машинного обучения. Совместимость со стандартными инструментами и библиотеками Linux делает его мощной платформой для разработки и экспериментов.

Установка

Чтобы использовать Raspberry Pi, нам понадобится операционная система. По умолчанию Raspberry Pi ищет операционную систему на любой SD-карте, вставленной в слот, поэтому установить систему мы будем с помощью Raspberry Pi Imager.

Raspberry Pi Imager — инструмент для загрузки и записи образов в macOS, Windows и Linux. В нём есть множество популярных образов операционных систем для Raspberry Pi. Imager мы также используем для предварительной настройки учётных данных и параметров удалённого доступа.

Чтобы установить ОС на Raspi, выполните следующие шаги:

  1. Скачайте и установите Raspberry Pi Imager на свой компьютер.

  2. Вставьте microSD-карту в компьютер (рекомендуется SD-карта объёмом 32 ГБ).

  3. Откройте Raspberry Pi Imager и выберите модель вашей Raspberry Pi.

  4. Выберите подходящую операционную систему:

    • Для Raspi-Zero: например, можно выбрать Raspberry Pi OS Lite (64-bit).

    Выбор образа Raspberry Pi OS Lite (64-bit) в Imager для Raspi-Zero

    Из-за небольшого объёма SDRAM (512 МБ) для Raspi-Zero рекомендуется 32-битная версия системы. Однако для запуска некоторых моделей машинного обучения, например YOLOv8 от Ultralytics, нужна 64-битная версия. Хотя на Raspi-Zero можно запустить рабочий стол, мы выберем версию LITE (без рабочего стола), чтобы уменьшить расход ОЗУ при обычной работе.

    • Для Raspi-5: можно выбрать полную 64-битную версию с рабочим столом: Raspberry Pi OS (64-bit).

Выбор образа Raspberry Pi OS (64-bit) в Imager для Raspi-5

  1. Выберите microSD-карту в качестве устройства хранения.
  2. Нажмите Next, затем значок «шестерёнки», чтобы открыть расширенные настройки.
  3. Задайте имя хоста (hostname), имя пользователя и пароль Raspi, настройте Wi-Fi и включите SSH (очень важно!).

Расширенные настройки Imager: имя хоста, пользователь, Wi-Fi и SSH

  1. Запишите образ на microSD-карту.

В примерах здесь мы используем разные имена хостов в зависимости от устройства: raspi, raspi-5, raspi-zero и т. д. Замените их на те, которые используете вы.

Первичная настройка

  1. Вставьте microSD-карту в Raspberry Pi.
  2. Подключите питание, чтобы загрузить Raspberry Pi.
  3. Дождитесь завершения первоначальной загрузки (это может занять несколько минут).

Самые распространённые команды Linux для работы с Raspi можно найти здесь или здесь.

Удалённый доступ

Доступ по SSH

Проще всего работать с Raspi-Zero через SSH («headless», то есть без монитора и клавиатуры). Для этого можно использовать Terminal (macOS/Linux), PuTTY (Windows) или любой другой клиент.

  1. Узнайте IP-адрес вашей Raspberry Pi (например, в настройках роутера).

  2. На компьютере откройте терминал и подключитесь по SSH:

    ssh username@<raspberry_pi_ip_address>
    

    Если IP-адрес неизвестен, можно попробовать так:

    ssh username@hostname.local
    

    например, ssh username@rpi-5.local или ssh username@raspi.local.

    Подключение к Raspberry Pi по SSH из терминала

    Когда вы увидите приглашение:

    username@rpi-5:~ $
    

    значит, вы работаете с Raspi удалённо. Полезно регулярно обновлять систему. Для этого выполните:

    sudo apt-get update
    sudo apt upgrade
    

    Также проверьте IP-адрес Raspi. В терминале это можно сделать так:

    hostname -I
    

Вывод терминала с IP-адресом Raspi

Выключение Raspi через терминал

Когда нужно выключить Raspberry Pi, лучше не просто выдёргивать шнур питания. Raspi может ещё записывать данные на SD-карту, и тогда простое отключение питания может привести к потере данных или, что хуже, к повреждению карты.

Для безопасного выключения используйте командную строку:

sudo shutdown -h now

Чтобы избежать потери данных и повреждения SD-карты, перед отключением питания подождите несколько секунд после выключения, пока светодиод Raspberry Pi перестанет мигать и погаснет. Когда светодиод погаснет, питание можно безопасно отключить.

Передача файлов между Raspi и компьютером

Передавать файлы между Raspi и основным компьютером можно с помощью флешки, напрямую в терминале (через scp) или по сети с помощью FTP-клиента.

Использование Secure Copy Protocol (scp)

Копирование файлов на Raspberry Pi

Создадим на компьютере текстовый файл, например test.txt.

Создание текстового файла test.txt на компьютере

Можно использовать любой текстовый редактор. В том же терминале удобен nano.

Чтобы скопировать файл test.txt с личного компьютера в домашний каталог пользователя на Raspberry Pi, выполните следующую команду из каталога, где лежит test.txt, заменив <username> на имя пользователя, под которым вы входите в Raspberry Pi, и <pi_ip_address> на IP-адрес вашей Raspberry Pi:

$ scp test.txt <username>@<pi_ip_address>:~/

Обратите внимание: ~/ означает, что файл будет перемещён в корневой домашний каталог Raspi. Можно выбрать любую папку на Raspi, но её нужно создать до запуска scp, так как scp не создаёт папки автоматически.

Например, перенесём файл test.txt в домашний каталог моей Raspi-Zero с IP-адресом 192.168.4.210:

scp test.txt username@192.168.4.210:~/

Команда scp для передачи test.txt на Raspi

Для различения терминалов я использую другой профиль. Описанное выше действие выполняется на вашем компьютере. Теперь подключимся к Raspi по SSH и проверим, что файл на месте:

Список файлов в домашнем каталоге Raspi после передачи

Копирование файлов с Raspberry Pi

Чтобы скопировать файл myfile.txt из домашнего каталога пользователя на Raspberry Pi в текущий каталог другого компьютера, выполните следующую команду на вашем компьютере-хосте:

$ scp <username>@<pi_ip_address>:myfile.txt .

Например:

На Raspi создадим копию файла с другим именем:

cp test.txt test_2.txt

А на компьютере-хосте (в моём примере — Mac):

scp username@192.168.4.210:test_2.txt .

Копирование test_2.txt с Raspi на Mac

Передача файлов по FTP

Передавать файлы можно и по FTP, например с помощью клиента FileZilla. Установите программу для операционной системы вашего компьютера и укажите IP-адрес Raspi в поле Host. Например:

sftp://192.168.4.210

Введите имя пользователя и пароль Raspi. Нажатие Quickconnect откроет два окна: одно для рабочего стола вашего компьютера (справа), другое для Raspi (слева).

Окно FileZilla с локальным компьютером справа и Raspi слева

Увеличение памяти SWAP

С помощью htop — интерактивного монитора процессов, работающего на разных платформах, — можно удобно отслеживать ресурсы Raspi: список процессов, загрузку процессоров и используемую память в реальном времени. Чтобы запустить htop, введите в терминале:

htop

Интерфейс htop с загрузкой процессора и памяти

Что касается памяти, среди устройств семейства Raspberry Pi у Raspi-Zero меньше всего ОЗУ (500 МБ), тогда как у Raspi 4 или 5 — от 2 до 8 ГБ. Для любой Raspi можно увеличить объём памяти, доступной системе, с помощью «swap». Swap-память, или область подкачки, — это приём, при котором операционная система временно сохраняет данные из ОЗУ (оперативной памяти) на SD-карту, когда физическая память полностью занята. Это позволяет ОС продолжать работу, когда ОЗУ заполнено, и предотвращает зависания и замедление системы.

Swap полезен устройствам с ограниченным объёмом ОЗУ, таким как Raspi-Zero. Увеличение swap помогает запускать более требовательные приложения и процессы, но нужно учитывать возможное снижение производительности из-за частого обращения к диску.

По умолчанию swap-память Raspi-Zero составляет всего 100 МБ, что очень мало для более сложных и требовательных приложений машинного обучения (например, YOLO). Увеличим её до 2 ГБ.

Сначала отключите файл подкачки:

sudo dphys-swapfile swapoff

Затем откройте и измените файл /etc/dphys-swapfile. Для этого используем nano:

sudo nano /etc/dphys-swapfile

Найдите переменную CONF_SWAPSIZE (по умолчанию 200) и измените её значение на 2000:

CONF_SWAPSIZE=2000

Сохраните файл.

Далее снова включите файл подкачки и перезагрузите Raspi-Zero:

sudo dphys-swapfile setup
sudo dphys-swapfile swapon
sudo reboot

После перезагрузки (снова подключитесь по SSH) вы увидите, что максимальный объём swap в верхней части htop теперь составляет около 2 ГБ (в моём случае — 1,95 ГБ).

Чтобы htop продолжал работать, откройте второе окно терминала и продолжайте взаимодействовать с Raspi в нём.

Установка камеры

Raspi — отличное устройство для приложений компьютерного зрения, и для этого понадобится камера. Можно подключить стандартную USB-веб-камеру к micro-USB-порту через OTG-адаптер (Raspi-Zero и Raspi-5) или модуль камеры, подключённый к порту CSI (Camera Serial Interface) на Raspi.

USB-веб-камеры обычно уступают по качеству модулям камер, которые подключаются к порту CSI. Кроме того, ими нельзя управлять командами raspistill и raspivid в терминале или пакетом записи picamera для Python. Тем не менее есть причины подключить USB-камеру к Raspberry Pi: например, так проще подключить несколько камер к одной плате, можно использовать длинные кабели или просто под рукой уже есть такая камера.

Установка USB-веб-камеры

  1. Выключите Raspi:
sudo shutdown -h now
  1. Подключите USB-веб-камеру (USB Camera Module, 30 кадров/с, ) к Raspi. В этом примере используется Raspi-Zero, но инструкции подходят для всех моделей Raspi.

Подключение USB-веб-камеры к Raspi-Zero

  1. Включите питание снова и подключитесь по SSH.
  2. Чтобы проверить, распознана ли USB-камера, выполните:
lsusb

Камера должна появиться в выводе команды.

Вывод lsusb с USB-камерой

  1. Чтобы сделать тестовый снимок с USB-камеры, используйте:
fswebcam test_image.jpg

Эта команда сохранит изображение с именем «test_image.jpg» в текущем каталоге.

Тестовый снимок, сохранённый командой fswebcam

  1. Поскольку мы подключаемся к Raspi по SSH, снимок нужно перенести на основной компьютер, чтобы его просмотреть. Для этого можно использовать FileZilla или SCP:

Откройте терминал на основном компьютере и выполните:

scp username@raspi-zero.local:~/test_image.jpg .

Замените «username» на своё имя пользователя, а «raspi-zero» — на имя хоста Raspi.

Тестовый снимок, перенесённый на основной компьютер

  1. Если качество изображения не устраивает, можно изменить настройки. Например, задайте разрешение, подходящее для YOLO ():
fswebcam -r 640x640 --no-banner test_image_yolo.jpg

Эта команда создаёт снимок с более высоким разрешением без стандартной подписи (баннера).

Снимок с разрешением 640×640

Обычную USB-веб-камеру тоже можно использовать:

Обычная USB-веб-камера

И проверить её командой lsusb:

Проверка USB-камеры командой lsusb

Потоковое видео

Для потокового видео (это требует больше ресурсов) можно установить и использовать mjpg-streamer.

Сначала установите Git:

sudo apt install git

Теперь установите зависимости для mjpg-streamer, клонируйте репозиторий и выполните установку:

sudo apt install cmake libjpeg62-turbo-dev
git clone https://github.com/jacksonliam/mjpg-streamer.git
cd mjpg-streamer/mjpg-streamer-experimental
make
sudo make install

Затем запустите потоковую передачу:

mjpg_streamer -i "input_uvc.so" -o "output_http.so -w ./www"

Открыть поток можно в веб-браузере по адресу http://<IP-адрес_вашего_Pi>:8080. В моём случае это http://192.168.4.210:8080

Вы увидите страницу с вариантами просмотра. Нажмите на ссылку «Stream» или откройте адрес:

http://<raspberry_pi_ip_address>:8080/?action=stream

Страница mjpg-streamer с видеопотоком

Установка модуля камеры в порт CSI

Существует несколько модулей камер Raspberry Pi. Первая камера, 5-мегапиксельная, была выпущена в 2013 году, затем в 2016 году появилась 8-мегапиксельная Camera Module 2. Самая новая модель — 12-мегапиксельная Camera Module 3, выпущенная в 2023 году.

Оригинальная 5-мегапиксельная камера (Arducam OV5647) больше не продаётся Raspberry Pi, но её можно найти у других поставщиков. Ниже показан пример такой камеры на Raspi-Zero.

Камера Arducam OV5647, подключённая к Raspi-Zero

Вот ещё один пример модуля камеры второй версии, в котором установлен 8-мегапиксельный сенсор Sony IMX219:

Модуль камеры Sony IMX219, подключённый к Raspi-5

Любой модуль камеры будет работать на Raspberry Pi, но для этого нужно обновить файл config.txt:

sudo nano /boot/firmware/config.txt

В конец файла, например для камеры 5 МП Arducam OV5647, добавьте строку:

dtoverlay=ov5647,cam0

Для модуля второй версии с 8-мегапиксельной камерой Sony IMX219 добавьте:

dtoverlay=imx219,cam0

Сохраните файл (CTRL+O, [ENTER], CTRL+X) и перезагрузите Raspi:

sudo reboot

После загрузки можно проверить, видна ли камера в системе:

libcamera-hello --list-cameras

Список камер в выводе libcamera на Raspi-Zero

Список камер в выводе libcamera на Raspi-5

libcamera — это библиотека с открытым исходным кодом, которая напрямую поддерживает камерные системы в Linux на процессорах Arm. Она сводит к минимуму проприетарный код, работающий на GPU Broadcom.

Сделаем тестовый JPEG-снимок с разрешением и сохраним его в файл test_cli_camera.jpg:

rpicam-jpeg --output test_cli_camera.jpg --width 640 --height 480

Чтобы убедиться, что файл сохранён, используйте ls -l: команда выводит содержимое текущего каталога в длинном формате. Как и раньше, для просмотра изображения можно использовать scp:

Тестовый снимок с камеры Raspi-5

Удалённый запуск рабочего стола Raspi

Мы в основном работали с Raspberry Pi через терминал по SSH, но можно получить удалённый доступ ко всей графической среде рабочего стола, если установлена полная версия Raspberry Pi OS (например, Raspberry Pi OS (64-bit)). Это особенно полезно для задач, где нужен визуальный интерфейс. Чтобы включить такую возможность, нужно настроить сервер VNC (Virtual Network Computing) на Raspberry Pi. Вот как это сделать:

  1. Включите сервер VNC:

    • Подключитесь к Raspberry Pi по SSH.
    • Запустите инструмент настройки Raspberry Pi, введя команду:
      sudo raspi-config
      
    • Перейдите к пункту Interface Options, используя стрелки.

    Пункт Interface Options в raspi-config

    • Выберите VNC и Yes, чтобы включить сервер VNC.

    Включение сервера VNC в raspi-config

    • Выйдите из инструмента настройки, сохранив изменения при запросе.

    Выход из raspi-config с сохранением настроек

  2. Установите VNC Viewer на свой компьютер:

    • Скачайте и установите приложение VNC-клиента на основной компьютер. Популярные варианты: RealVNC Viewer, TightVNC или VNC Viewer от RealVNC. Мы будем использовать VNC Viewer от RealVNC.
  3. Убедившись, что приложение установлено, уточните IP-адрес Raspi. Например, в терминале можно выполнить:

    hostname -I
    

    Вывод hostname -I с IP-адресом Raspberry Pi

  4. Подключитесь к Raspberry Pi:

    • Откройте приложение VNC Viewer.

    Окно VNC Viewer перед подключением

    • Введите IP-адрес и имя хоста Raspberry Pi.
    • По запросу введите имя пользователя и пароль Raspberry Pi.

    Запрос имени пользователя и пароля в VNC Viewer

  5. Рабочий стол Raspberry Pi 5 должен появиться на мониторе вашего компьютера.

    Рабочий стол Raspberry Pi 5 в VNC Viewer

  6. При необходимости настройте экран:

    • После подключения настройте разрешение экрана для комфортной работы. Это можно сделать в настройках рабочего стола Raspberry Pi или изменив файл config.txt.
    • Сделаем это через настройки рабочего стола. Откройте меню (значок Raspberry в левом верхнем углу) и выберите наилучшее разрешение для вашего монитора:

    Выбор разрешения экрана в настройках рабочего стола

Обновление системы и установка программ

  1. Обновите систему:

    sudo apt update && sudo apt upgrade -y
    
  2. Установите необходимые программы:

    sudo apt install python3-pip -y
    
  3. Включите pip для Python-проектов:

    sudo rm /usr/lib/python3.11/EXTERNALLY-MANAGED
    

Особенности разных моделей

Raspberry Pi Zero (Raspi-Zero)

  • Ограниченная вычислительная мощность, лучше подходит для лёгких проектов.
  • Лучше работать без монитора, через SSH, чтобы экономить ресурсы.
  • Для задач с большим расходом памяти имеет смысл увеличить объём swap-памяти.
  • Подходит для лабораторных работ по классификации изображений и обнаружению объектов, но не для LLM (SLM).

Raspberry Pi 4 или 5 (Raspi-4 или Raspi-5)

  • Подходят для более требовательных проектов, включая ИИ и машинное обучение.
  • Могут плавно работать со всей графической средой рабочего стола.
  • Raspi-4 подходит для лабораторных работ по классификации изображений и обнаружению объектов, но плохо работает с LLM (SLM).
  • Для Raspi-5 при интенсивной нагрузке, например в лабораторной работе по LLM (SLM), стоит использовать активный кулер для контроля температуры.

Подбирайте требования проекта под конкретную модель Raspberry Pi. Raspi-Zero хорошо подходит для проектов с низким энергопотреблением и ограниченным местом, а Raspi-4 или 5 лучше подходят для более ресурсоёмких задач.

Визуально-языковые модели (VLM)

Установка Raspberry Pi с камерой для задач компьютерного зрения
Промпт DALL·E: установка Raspberry Pi для задач компьютерного зрения. На изображении Raspberry Pi подключена к камере, а вокруг неё визуально представлены различные задачи: обнаружение объектов, описание изображений, сегментация и визуальная привязка (visual grounding). Raspberry Pi стоит на столе, на дисплее видны ограничивающие рамки и аннотации, связанные с этими задачами. Фон — домашняя мастерская с инструментами и устройствами, которыми обычно пользуются разработчики и энтузиасты.

Введение

В этой практической работе мы будем последовательно исследовать прикладной ИИ на периферии: от базовой настройки Florence-2, передовой базовой модели компьютерного зрения от Microsoft, до продвинутых реализаций на таких устройствах, как Raspberry Pi. Мы научимся использовать визуально-языковые модели (Vision-Language Models, VLM) для таких задач, как описание изображений (captioning), обнаружение объектов, визуальная привязка (grounding), сегментация и OCR на Raspberry Pi.

Почему Florence-2 на периферии?

Florence-2 — визуально-языковая модель, которую Microsoft выпустила с открытым исходным кодом по лицензии MIT. Она заметно продвигает такие модели, сочетая лёгкую архитектуру с надёжными возможностями. Благодаря обучению на огромном наборе данных FLD-5B, который содержит 126 миллионов изображений и 5,4 миллиарда визуальных аннотаций, модель достигает производительности, сопоставимой с более крупными моделями. Это делает Florence-2 идеальной для развёртывания на периферии, где питание и вычислительные ресурсы ограничены.

В этом руководстве мы разберём, как использовать Florence-2 для приложений компьютерного зрения в реальном времени, таких как:

  • описание изображений (Image captioning);
  • обнаружение объектов;
  • сегментация;
  • визуальная привязка (Visual grounding).

Визуальная привязка (visual grounding) связывает текстовые описания с конкретными областями изображения. Это позволяет модели понять, где на изображении находятся объекты или сущности, описанные в запросе. Например, если запрос — «красная машина», модель найдёт и выделит область, где на изображении находится красная машина. Визуальная привязка полезна там, где нужно точное соответствие между текстом и визуальным содержимым, например во взаимодействии человека с компьютером, в аннотировании изображений и интерактивных системах ИИ.

В руководстве мы пройдём следующие этапы:

  • настройка Florence-2 на Raspberry Pi;
  • запуск задач инференса, таких как обнаружение объектов и описание изображений;
  • оптимизация модели для максимальной производительности на периферийном устройстве;
  • изучение практических приложений в реальных условиях с дообучением (fine-tuning).

Архитектура модели Florence-2

Florence-2 использует унифицированное представление на основе запросов для решения различных визуально-языковых задач. Архитектура модели состоит из двух основных компонентов: кодировщика изображений и мультимодального кодировщика-декодера на основе трансформера.

Архитектура Florence-2: вход (изображение и текстовый запрос), кодировщик изображений DaViT, токенизация текста, мультимодальный кодировщик и декодер, выход (текст или координаты)

  • Кодировщик изображений: основан на архитектуре DaViT (Dual Attention Vision Transformers). Он преобразует входные изображения в последовательность эмбеддингов визуальных токенов. Эти эмбеддинги служат базовым представлением визуального содержимого и отражают как пространственную, так и контекстную информацию об изображении.

  • Мультимодальный кодировщик-декодер на основе трансформера: ядро Florence-2 объединяет эмбеддинги визуальных токенов из кодировщика изображений с текстовыми эмбеддингами, которые создаёт модель, похожая на BERT. Такое объединение позволяет модели одновременно обрабатывать визуальные и текстовые входные данные, обеспечивая единый подход к таким задачам, как описание изображений, обнаружение объектов и сегментация.

Обучение на обширном наборе данных FLD-5B гарантирует, что модель эффективно справляется с разнообразными задачами зрения без модификаций под каждую задачу. Florence-2 использует текстовые запросы, чтобы активировать конкретные задачи, что делает её очень гибкой и способной к обобщению без дополнительного обучения (zero-shot). Для таких задач, как обнаружение объектов или визуальная привязка, модель использует дополнительные токены местоположения, чтобы обозначать области изображения, что обеспечивает точное понимание пространственных отношений.

Компактная архитектура Florence-2 и инновационный подход к обучению позволяют ей точно выполнять задачи компьютерного зрения даже на устройствах с ограниченными ресурсами, таких как Raspberry Pi.

Обзор технологии

Florence-2 содержит несколько инновационных особенностей, которые выделяют её среди других моделей:

Архитектура

Схема архитектуры Florence-2

  • Лёгкий дизайн: доступны два варианта:
    • Florence-2-Base: 232 миллиона параметров;
    • Florence-2-Large: 771 миллион параметров.
  • Унифицированное представление: решает несколько задач компьютерного зрения одной архитектурой.
  • Визуальный кодировщик DaViT: преобразует изображения в эмбеддинги визуальных токенов.
  • Мультимодальный кодировщик-декодер на основе трансформера: обрабатывает объединённые визуальные и текстовые эмбеддинги.

Обучающий набор данных (FLD-5B)

Состав обучающего набора данных FLD-5B

  • 126 миллионов уникальных изображений;
  • 5,4 миллиарда комплексных аннотаций, включая:
    • 500 млн текстовых аннотаций;
    • 1,3 млрд аннотаций «регион — текст»;
    • 3,6 млрд аннотаций «текстовая фраза — регион»;
  • автоматический конвейер аннотирования с использованием специализированных моделей;
  • итеративный процесс уточнения для получения качественных меток.

Ключевые возможности

Florence-2 показывает отличные результаты во множестве задач компьютерного зрения:

Работа без дообучения (zero-shot)

  • Описание изображений: показатель CIDEr 135,6 на COCO;
  • Визуальная привязка: 84,4 % recall@1 на Flickr30k;
  • Обнаружение объектов: 37,5 mAP на COCO val2017;
  • Выражения-ссылки (referring expression): 67,0 % точности на RefCOCO.

Работа после дообучения (fine-tuned)

  • Конкурентоспособна со специализированными моделями, несмотря на меньший размер;
  • Превосходит более крупные модели в отдельных тестах;
  • Эффективно адаптируется к новым задачам.

Практические применения

Florence-2 можно применять во многих областях:

  1. Работа с контентом
    • Автоматическое описание изображений для доступности;
    • Модерация визуального контента;
    • Управление медиаактивами.
  2. Электронная коммерция
    • Анализ изображений товаров;
    • Визуальный поиск;
    • Автоматическая разметка товаров тегами.
  3. Здравоохранение
    • Анализ медицинских изображений;
    • Помощь в диагностике;
    • Обработка данных исследований.
  4. Безопасность и наблюдение
    • Обнаружение объектов и отслеживание;
    • Обнаружение аномалий;
    • Понимание сцены.

Сравнение Florence-2 с другими VLM

Florence-2 выделяется среди других визуально-языковых моделей впечатляющими возможностями работы без дообучения (zero-shot). В отличие от моделей вроде Google PaliGemma, которые полагаются на обширное дообучение, чтобы адаптироваться к разным задачам, Florence-2 работает сразу после установки, как мы увидим в этой лабораторной работе. Она может конкурировать и с более крупными моделями, такими как GPT-4V и Flamingo, у которых гораздо больше параметров, но которые лишь иногда показывают результаты лучше, чем Florence-2. Например, Florence-2 достигает лучших результатов без дообучения, чем Kosmos-2, хотя у той более чем вдвое больше параметров.

В тестах Florence-2 показала выдающиеся результаты в таких задачах, как описание изображений COCO и понимание выражений-ссылок (referring expression comprehension). Она превзошла модели PolyFormer и UNINEXT в задачах обнаружения объектов и сегментации на наборе данных COCO. Это высококонкурентный выбор для реальных приложений, где важны и производительность, и эффективность использования ресурсов.

Настройка и установка

Наш выбор периферийного устройства — Raspberry Pi 5 (Raspi-5). Эта надёжная платформа оснащена процессором Broadcom BCM2712 — 64-битным четырёхъядерным Arm Cortex-A76 с частотой 2,4 ГГц, с криптографическим расширением и улучшенным кэшированием. В ней есть GPU VideoCore VII, два выхода HDMI® 4Kp60 с HDR и декодер HEVC 4Kp60. Варианты памяти — 4 ГБ и 8 ГБ высокоскоростной LPDDR4X SDRAM; для запуска Florence-2 мы выбираем 8 ГБ. Также есть расширяемое хранилище через слот microSD и интерфейс PCIe 2.0 для быстрой периферии, например твердотельных накопителей M.2 SSD.

Для реальных приложений SSD — лучший выбор, чем SD-карты.

Для этой лабораторной работы мы рекомендуем установить Active Cooler — специальное навесное охлаждение для Raspberry Pi 5 (Raspi-5). Оно сочетает алюминиевый радиатор с вентилятором, управляемым температурой, чтобы Raspi-5 комфортно работал под высокой нагрузкой, например при запуске Florence-2.

Active Cooler, установленный на Raspberry Pi 5

Настройка окружения

Чтобы запустить Microsoft Florence-2 на Raspberry Pi 5, понадобится несколько библиотек:

  1. Transformers:

    • Florence-2 использует библиотеку transformers из Hugging Face для загрузки модели и инференса. Она предоставляет архитектуру для работы с предобученными визуально-языковыми моделями, что упрощает такие задачи, как описание изображений, обнаружение объектов и другие. По сути, transformers помогает взаимодействовать с моделью, обрабатывать входные запросы и получать результаты.
  2. PyTorch:

    • PyTorch — фреймворк глубокого обучения, который предоставляет инфраструктуру для работы Florence-2: тензорные операции, ускорение на GPU (если он доступен), функции обучения и инференса моделей. Florence-2 обучена в PyTorch, и нам нужны её функции, слои и возможности вычислений, чтобы выполнять инференс на Raspberry Pi.
  3. Timm (модели компьютерного зрения для PyTorch):

    • Florence-2 использует timm для доступа к эффективным реализациям моделей зрения и предобученным весам. В частности, библиотека timm применяется для кодировщика изображений Florence-2, в первую очередь для работы с архитектурой DaViT. Она предоставляет определения моделей и оптимизированный код для распространённых задач зрения и позволяет легко подключать различные лёгкие магистрали (backbone), подходящие для периферийных устройств.
  4. Einops:

    • Einops — библиотека для гибких и мощных тензорных операций. Она упрощает изменение формы тензоров и работу с их размерностями, что особенно важно для мультимодальной обработки в Florence-2. Визуально-языковые модели, такие как Florence-2, часто должны переупорядочивать данные изображений, текстовые и визуальные эмбеддинги, чтобы корректно выровнять их для блоков трансформера, и einops упрощает эти сложные операции, делая код более читаемым и лаконичным.

Коротко, эти библиотеки обеспечивают различные ключевые компоненты Florence-2:

  • Transformers и PyTorch нужны для загрузки модели и запуска инференса.
  • Timm используется для доступа к кодировщику изображений и его эффективной реализации.
  • Einops помогает изменять форму данных, упрощая объединение визуальных и текстовых признаков.

Все эти компоненты работают вместе, чтобы Florence-2 работала на нашем Raspberry Pi без сбоев и выполняла сложные визуально-языковые задачи достаточно быстро.

Поскольку на Raspberry Pi уже установлена ОС, подключимся к ней с другого компьютера по SSH:

ssh username@raspi-5.local

И проверим назначенный ей IP-адрес:

hostname -I

192.168.4.209

Подключение к Raspberry Pi 5 по SSH

Обновление Raspberry Pi

Сначала убедитесь, что Raspberry Pi обновлён:

sudo apt update
sudo apt upgrade -y

Первоначальная настройка для работы с PIP:

sudo apt install python3-pip
sudo rm /usr/lib/python3.11/EXTERNALLY-MANAGED
pip3 install --upgrade pip

Установка зависимостей

sudo apt-get install libjpeg-dev libopenblas-dev libopenmpi-dev \
    libomp-dev

Настроим и активируем виртуальное окружение для работы с Florence-2:

python3 -m venv ~/florence
source ~/florence/bin/activate

Установка PyTorch

pip3 install setuptools numpy Cython
pip3 install requests
pip3 install torch torchvision \
    --index-url https://download.pytorch.org/whl/cpu
pip3 install torchaudio \
    --index-url https://download.pytorch.org/whl/cpu

Проверим, что PyTorch установлен корректно:

Проверка установки PyTorch

Установка Transformers, Timm и Einops:

pip3 install transformers
pip3 install timm einops

Установка модели:

pip3 install autodistill-florence-2

Jupyter Notebook и библиотеки Python

Для запуска и тестирования наших Python-скриптов можно установить Jupyter Notebook.

pip3 install jupyter
pip3 install numpy Pillow matplotlib
jupyter notebook --generate-config

Тестирование установки

Запустим Jupyter Notebook на удалённом компьютере:

jupyter notebook --ip=192.168.4.209 --no-browser

Выполнив эту команду в терминале SSH, мы увидим локальный URL-адрес, по которому можно открыть блокнот:

Вывод терминала с адресом Jupyter Notebook и токеном

Блокнот с кодом для этого первого теста можно найти в репозитории лабораторных работ на GitHub:

Получить доступ к блокноту с удалённого компьютера можно, введя в веб-браузере IP-адрес Raspberry Pi и токен из терминала (скопируйте весь URL из терминала).

На домашней странице создайте новый блокнот Python 3 (ipykernel) и скопируйте и вставьте пример кода с Hugging Face Hub.

Код предназначен для запуска Florence-2 на заданном изображении для обнаружения объектов. Он загружает модель, обрабатывает изображение и запрос, а затем генерирует ответ, который находит и описывает объекты на изображении.

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

Этот процесс использует универсальность Florence-2 для решения визуально-языковых задач и эффективно реализован с помощью PyTorch, Transformers и связанных инструментов обработки изображений.

import requests
from PIL import Image
import torch
from transformers import AutoProcessor, AutoModelForCausalLM

device = "cuda:0" if torch.cuda.is_available() else "cpu"
torch_dtype = (
    torch.float16 if torch.cuda.is_available() else torch.float32
)

model = AutoModelForCausalLM.from_pretrained(
    "microsoft/Florence-2-base",
    torch_dtype=torch_dtype,
    trust_remote_code=True,
).to(device)
processor = AutoProcessor.from_pretrained(
    "microsoft/Florence-2-base", trust_remote_code=True
)

prompt = "<OD>"
url = (
    "https://huggingface.co/datasets/huggingface/"
    "documentation-images/resolve/main/transformers/"
    "tasks/car.jpg?download=true"
)
image = Image.open(requests.get(url, stream=True).raw)

inputs = processor(text=prompt, images=image, return_tensors="pt").to(
    device, torch_dtype
)

generated_ids = model.generate(
    input_ids=inputs["input_ids"],
    pixel_values=inputs["pixel_values"],
    max_new_tokens=1024,
    do_sample=False,
    num_beams=3,
)
generated_text = processor.batch_decode(
    generated_ids, skip_special_tokens=False
)[0]

parsed_answer = processor.post_process_generation(
    generated_text,
    task="<OD>",
    image_size=(image.width, image.height),
)

print(parsed_answer)

Разберём приведённый код по шагам:

Импорт необходимых библиотек

import requests
from PIL import Image
import torch
from transformers import AutoProcessor, AutoModelForCausalLM
  • requests: используется для HTTP-запросов. В данном случае библиотека загружает изображение по URL.
  • PIL (Pillow): предоставляет инструменты для работы с изображениями. Здесь она используется для открытия загруженного изображения.
  • torch: PyTorch импортируется для работы с тензорами и определения доступности оборудования (CPU или GPU).
  • transformers: модуль даёт удобный доступ к Florence-2 через AutoProcessor и AutoModelForCausalLM, которые загружают предобученные модели и обрабатывают входные данные.

Определение устройства и типа данных

device = "cuda:0" if torch.cuda.is_available() else "cpu"

torch_dtype = (
    torch.float16 if torch.cuda.is_available() else torch.float32
)
  • Настройка устройства: код проверяет, доступен ли GPU с поддержкой CUDA (torch.cuda.is_available()). Если GPU есть, устройство задаётся как «cuda:0», иначе используется "cpu" (наш случай).
  • Тип данных: если GPU доступен, выбирается torch.float16 — половинная точность, которая ускоряет обработку и снижает расход памяти. На CPU по умолчанию используется torch.float32 для совместимости.

Загрузка модели и процессора

model = AutoModelForCausalLM.from_pretrained(
    "microsoft/Florence-2-base",
    torch_dtype=torch_dtype,
    trust_remote_code=True,
).to(device)

processor = AutoProcessor.from_pretrained(
    "microsoft/Florence-2-base", trust_remote_code=True
)
  • Инициализация модели:

    • AutoModelForCausalLM.from_pretrained() загружает предобученную модель Florence-2 из репозитория Microsoft на Hugging Face. torch_dtype задаётся в соответствии с доступным оборудованием (GPU или CPU), а trust_remote_code=True разрешает использовать пользовательский код, который может поставляться вместе с моделью.
    • .to(device) переносит модель на нужное устройство (CPU или GPU). В нашем случае будет выбран CPU.
  • Инициализация процессора:

    • AutoProcessor.from_pretrained() загружает процессор Florence-2. Процессор преобразует текстовые и графические входные данные в формат, с которым может работать модель (например, кодирует текст и нормализует изображения).

Определение запроса

prompt = "<OD>"
  • Запрос: строка "<OD>" используется как запрос. Это обозначение задачи «Object Detection» (обнаружение объектов), которое указывает модели найти объекты на изображении.

Загрузка изображения

url = (
    "https://huggingface.co/datasets/huggingface/"
    "documentation-images/resolve/main/transformers/"
    "tasks/car.jpg?download=true"
)
image = Image.open(requests.get(url, stream=True).raw)
  • Загрузка изображения: функция requests.get() получает изображение по заданному URL. Параметр stream=True означает, что изображение загружается потоком, а не целиком сразу.
  • Открытие изображения: Image.open() открывает изображение, чтобы модель могла его обработать.

Обработка входных данных

inputs = processor(text=prompt, images=image, return_tensors="pt").to(
    device, torch_dtype
)
  • Обработка входных данных: функция processor() обрабатывает текст (prompt) и изображение (image). Аргумент return_tensors="pt" преобразует результат в тензоры PyTorch, которые необходимы для подачи данных в модель.
  • Перенос входных данных на устройство: .to(device, torch_dtype) переносит входные данные на нужное устройство (CPU или GPU) и задаёт соответствующий тип данных.

Генерация результата

generated_ids = model.generate(
    input_ids=inputs["input_ids"],
    pixel_values=inputs["pixel_values"],
    max_new_tokens=1024,
    do_sample=False,
    num_beams=3,
)
  • Генерация моделью: model.generate() создаёт результат на основе входных данных.
    • input_ids: токенизированная форма запроса.
    • pixel_values: обработанные данные изображения.
    • max_new_tokens=1024: задаёт максимальное число новых токенов в ответе. Это ограничивает длину ответа.
    • do_sample=False: отключает семплирование; вместо него используются детерминированные методы (поиск лучом, beam search).
    • num_beams=3: включает поиск лучом с тремя лучами, что улучшает качество результата, рассматривая несколько вариантов во время генерации.

Декодирование сгенерированного текста

generated_text = processor.batch_decode(
    generated_ids, skip_special_tokens=False
)[0]
  • Пакетное декодирование: processor.batch_decode() декодирует сгенерированные ID (токены) в читаемый текст. Параметр skip_special_tokens=False означает, что результат будет включать специальные токены, если они являются частью ответа.

Постобработка результата

parsed_answer = processor.post_process_generation(
    generated_text,
    task="<OD>",
    image_size=(image.width, image.height),
)
  • Постобработка: processor.post_process_generation() дополнительно обрабатывает сгенерированный текст, интерпретируя его в соответствии с задачей ("<OD>" для обнаружения объектов) и размером изображения.
  • Функция извлекает из текста конкретную информацию, например ограничивающие рамки обнаруженных объектов, что делает результат полезнее для задач зрения.

Вывод результата

print(parsed_answer)
  • Наконец, print(parsed_answer) выводит результат, который может включать данные обнаружения объектов: координаты ограничивающих рамок и метки найденных на изображении объектов.

Результат

Запустив код, получаем в качестве Parsed Answer:

[{'<OD>': {
   'bboxes': [
     [34.23999786376953, 160.0800018310547, 597.4400024414062],
     [371.7599792480469, 272.32000732421875, 241.67999267578125],
     [303.67999267578125, 247.4399871826172, 454.0799865722656],
     [276.7200012207031, 553.9199829101562, 370.79998779296875],
     [96.31999969482422, 280.55999755859375, 198.0800018310547],
     [371.2799987792969]
    ],
    'labels': ['car', 'door handle', 'wheel', 'wheel']
}}]

Сначала посмотрим на изображение:

import matplotlib.pyplot as plt

plt.figure(figsize=(8, 8))
plt.imshow(image)
plt.axis("off")
plt.show()

Исходное изображение автомобиля для обнаружения объектов

По результату обнаружения объектов видим, что:

'labels': ['car', 'door handle', 'wheel', 'wheel']

Похоже, что обнаружено как минимум несколько объектов. Мы также можем написать код, который рисует ограничивающие рамки найденных объектов:

def plot_bbox(image, data):
    # Создаём фигуру и оси
    fig, ax = plt.subplots()

    # Отображаем изображение
    ax.imshow(image)

    # Рисуем каждую ограничивающую рамку
    for bbox, label in zip(data["bboxes"], data["labels"]):
        # Распаковываем координаты рамки
        x1, y1, x2, y2 = bbox
        # Создаём прямоугольник
        rect = patches.Rectangle(
            (x1, y1),
            x2 - x1,
            y2 - y1,
            linewidth=1,
            edgecolor="r",
            facecolor="none",
        )
        # Добавляем прямоугольник на оси
        ax.add_patch(rect)
        # Подписываем метку
        plt.text(
            x1,
            y1,
            label,
            color="white",
            fontsize=8,
            bbox=dict(facecolor="red", alpha=0.5),
        )

    # Убираем деления и подписи осей
    ax.axis("off")

    # Показываем график
    plt.show()

Box (x0, y0, x1, y1): токены местоположения соответствуют левому верхнему и правому нижнему углам рамки.

Запустим:

plot_bbox(image, parsed_answer['<OD>'])

Получаем:

Изображение с ограничивающими рамками и метками найденных объектов

Задачи Florence-2

Florence-2 создана для выполнения множества задач компьютерного зрения и визуально-языковых задач с помощью запросов (prompts). Эти задачи активируются заданием конкретного текстового запроса, как мы видели в случае <OD> (обнаружение объектов).

Универсальность Florence-2 основана на комбинировании таких запросов: меняя запрос, можно направлять поведение модели на выполнение конкретной задачи зрения. Смена запроса позволяет адаптировать Florence-2 к разным задачам без модификаций архитектуры. Эта возможность напрямую следует из унифицированной архитектуры модели и масштабного мультизадачного обучения на наборе данных FLD-5B.

Вот некоторые ключевые задачи, которые может решать Florence-2, с примерами запросов:

Обнаружение объектов (OD)

  • Запрос: "<OD>"
  • Описание: находит объекты на изображении и выдаёт ограничивающую рамку для каждого из них. Эта задача полезна для визуального контроля, систем наблюдения и общего распознавания объектов.

Описание изображения (Image Captioning)

  • Запрос: "<CAPTION>"
  • Описание: создаёт текстовое описание входного изображения. Эта задача помогает модели описать происходящее на изображении, давая понятную человеку подпись для понимания содержимого.

Подробное описание (Detailed Captioning)

  • Запрос: "<DETAILED_CAPTION>"
  • Описание: создаёт более подробную подпись с более тонкой информацией о сцене, например о присутствующих объектах и их взаимосвязях.

Визуальная привязка (Visual Grounding)

  • Запрос: "<CAPTION_TO_PHRASE_GROUNDING>"
  • Описание: связывает текстовое описание с конкретными областями изображения. Например, по запросу «a green car» (зелёная машина) модель выделяет, где на изображении находится зелёная машина. Это полезно для взаимодействия человека с компьютером, когда нужно найти конкретные объекты по тексту.

Сегментация (Segmentation)

  • Запрос: "<REFERRING_EXPRESSION_SEGMENTATION>"
  • Описание: выполняет сегментацию по выражению-ссылке, например «the blue cup» (синяя чашка). Модель находит и выделяет область, содержащую объект, упомянутый в запросе (все относящиеся к нему пиксели).

Плотное описание областей (Dense Region Captioning)

  • Запрос: "<DENSE_REGION_CAPTION>"
  • Описание: создаёт подписи для нескольких областей изображения и даёт подробный разбор всех видимых участков, включая разные объекты и их взаимосвязи.

OCR с областями (OCR with Region)

  • Запрос: "<OCR_WITH_REGION>"
  • Описание: выполняет оптическое распознавание символов (OCR) на изображении и выдаёт ограничивающие рамки для найденного текста. Это полезно для извлечения текстовой информации и определения её положения на изображении, например для чтения надписей, этикеток или других текстовых элементов.

Привязка фраз для конкретных выражений (Phrase Grounding for Specific Expressions)

  • Запрос: "<CAPTION_TO_PHRASE_GROUNDING>" вместе с конкретным выражением, например "a wine glass" (бокал вина).
  • Описание: находит на изображении область, соответствующую конкретной текстовой фразе. Эта задача позволяет выделять определённые объекты или элементы по слову или ключевому слову в запросе.

Обнаружение объектов с открытым словарём (Open Vocabulary Object Detection)

  • Запрос: "<OPEN_VOCABULARY_OD>"
  • Описание: модель может обнаруживать объекты без ограничения заранее заданным списком классов, поэтому она помогает распознавать более широкий круг предметов на основе общего визуального понимания.

Исследование задач компьютерного зрения и визуально-языковых задач

Для исследования все примеры кода можно найти на GitHub:

Загрузим несколько изображений, созданных в DALL·E, на Raspi-5 (для этого можно использовать FileZilla). Изображения сохраним в подкаталог images:

dogs_cats = Image.open("./images/dogs-cats.jpg")
table = Image.open("./images/table.jpg")

Тестовые изображения: собаки и кошки, а также стол с фруктами и вином

Создадим функцию, которая облегчит исследование и позволит отслеживать задержку модели для разных задач:

def run_example(task_prompt, text_input=None, image=None):
    start_time = time.perf_counter()  # Запускаем таймер
    if text_input is None:
        prompt = task_prompt
    else:
        prompt = task_prompt + text_input
    inputs = processor(
        text=prompt, images=image, return_tensors="pt"
    ).to(device)
    generated_ids = model.generate(
        input_ids=inputs["input_ids"],
        pixel_values=inputs["pixel_values"],
        max_new_tokens=1024,
        early_stopping=False,
        do_sample=False,
        num_beams=3,
    )
    generated_text = processor.batch_decode(
        generated_ids, skip_special_tokens=False
    )[0]
    parsed_answer = processor.post_process_generation(
        generated_text,
        task=task_prompt,
        image_size=(image.width, image.height),
    )

    end_time = time.perf_counter()  # Останавливаем таймер
    elapsed_time = end_time - start_time  # Вычисляем затраченное время
    print(
        f" \n[INFO] ==> Florence-2-base ({task_prompt}), \
          took {elapsed_time:.1f} seconds to execute.\n"
    )

    return parsed_answer

Подпись (Caption)

1. Собаки и кошки

run_example(task_prompt="<CAPTION>", image=dogs_cats)
[INFO] ==> Florence-2-base (<CAPTION>), \
took 16.1 seconds to execute.

{'<CAPTION>': 'A group of dogs and cats sitting in a garden.'}

2. Стол

run_example(task_prompt="<CAPTION>", image=table)
[INFO] ==> Florence-2-base (<CAPTION>), \
took 16.5 seconds to execute.

{'<CAPTION>': 'A wooden table topped with a plate of fruit \
and a glass of wine.'}

Подробная подпись (Detailed Caption)

1. Собаки и кошки

run_example(task_prompt="<DETAILED_CAPTION>", image=dogs_cats)
[INFO] ==> Florence-2-base (<DETAILED_CAPTION>), \
took 25.5 seconds to execute.

{'<DETAILED_CAPTION>': 'The image shows a group of cats and \
dogs sitting on top of a lush green field, surrounded by plants \
with flowers, trees, and a house in the background. The sky is \
visible above them, creating a peaceful atmosphere.'}

2. Стол

run_example(task_prompt="<DETAILED_CAPTION>", image=table)
[INFO] ==> Florence-2-base (<DETAILED_CAPTION>), \
took 26.8 seconds to execute.

{'<DETAILED_CAPTION>': 'The image shows a wooden table with \
a bottle of wine and a glass of wine on it, surrounded by \
a variety of fruits such as apples, oranges, and grapes. \
In the background, there are chairs, plants, trees, and \
a house, all slightly blurred.'}

Более подробная подпись (More Detailed Caption)

1. Собаки и кошки

run_example(task_prompt="<MORE_DETAILED_CAPTION>", image=dogs_cats)
[INFO] ==> Florence-2-base (<MORE_DETAILED_CAPTION>), \
took 49.8 seconds to execute.

{'<MORE_DETAILED_CAPTION>': 'The image shows a group of four \
cats and a dog in a garden. The garden is filled with colorful \
flowers and plants, and there is a pathway leading up to \
a house in the background. The main focus of the image is \
a large German Shepherd dog standing on the left side of \
the garden, with its tongue hanging out and its mouth open, \
as if it is panting. On the right side, there are \
two smaller cats, one orange and one gray, sitting on the \
grass. In the background, there is another golden retriever \
dog sitting and looking at the camera. The sky is blue and \
the sun is shining, creating a warm and inviting atmosphere.'}

2. Стол

run_example(task_prompt="<MORE_DETAILED_CAPTION>", image=table)
[INFO] ==> Florence-2-base (<MORE_DETAILED_CAPTION>), \
took 32.4 seconds to execute.

{'<MORE_DETAILED_CAPTION>': 'The image shows a wooden table \
with a wooden tray on it. On the tray, there are various \
fruits such as grapes, oranges, apples, and grapes. There \
is also a bottle of red wine on the table. The background \
shows a garden with trees and a house. The overall mood \
of the image is peaceful and serene.'}

Заметим, что чем подробнее задача описания, тем выше задержка и вероятность ошибок (например, в ответе «The image shows a group of four cats and a dog in a garden» модель называет четырёх кошек и одну собаку, хотя на изображении было иначе).

Обнаружение объектов (Object Detection)

Ту же функцию можно использовать для обнаружения объектов с запросом <OD>.

task_prompt = "<OD>"
results = run_example(task_prompt, image=dogs_cats)
print(results)

Посмотрим результат:

[INFO] ==> Florence-2-base (<OD>), took 20.9 seconds to execute.

{'<OD>': {'bboxes': [
  [737.79, 571.90, 1022.46, 980.48],
  [0.51, 593.40, 211.45, 991.74],
  [445.95, 721.40, 680.44, 850.43],
  [39.42, 91.64, 491.00, 933.37],
  [570.88, 184.83, 974.33, 782.84]
  ],
  'labels': ['cat', 'cat', 'cat', 'dog', 'dog']
}}

По меткам ['cat', 'cat', 'cat', 'dog', 'dog'] видно, что основные объекты на изображении распознаны. Применим функцию, которую мы использовали ранее, чтобы нарисовать ограничивающие рамки:

plot_bbox(dogs_cats, results["<OD>"])

Ограничивающие рамки кошек и собак на изображении

Сделаем то же самое для изображения стола:

task_prompt = "<OD>"
results = run_example(task_prompt, image=table)
plot_bbox(table, results["<OD>"])
[INFO] ==> Florence-2-base (<OD>), took 40.8 seconds to execute.

Ограничивающие рамки объектов на изображении стола

Плотное описание областей (Dense Region Caption)

Классическое обнаружение объектов можно сочетать с подписями для конкретных подобластей изображения:

task_prompt = "<DENSE_REGION_CAPTION>"

results = run_example(task_prompt, image=dogs_cats)
plot_bbox(dogs_cats, results["<DENSE_REGION_CAPTION>"])

results = run_example(task_prompt, image=table)
plot_bbox(table, results["<DENSE_REGION_CAPTION>"])

Плотные подписи для областей на обоих изображениях

Привязка подписи к фразе (Caption to Phrase Grounding)

В этой задаче можно передать подпись, например «a wine glass» (бокал вина), «a wine bottle» (бутылка вина) или «a half orange» (половина апельсина), и Florence-2 найдёт объект на изображении:

task_prompt = "<CAPTION_TO_PHRASE_GROUNDING>"

results = run_example(
    task_prompt, text_input="a wine bottle", image=table
)
plot_bbox(table, results["<CAPTION_TO_PHRASE_GROUNDING>"])

results = run_example(
    task_prompt, text_input="a wine glass", image=table
)
plot_bbox(table, results["<CAPTION_TO_PHRASE_GROUNDING>"])

results = run_example(
    task_prompt, text_input="a half orange", image=table
)
plot_bbox(table, results["<CAPTION_TO_PHRASE_GROUNDING>"])

Привязка фраз «wine bottle», «wine glass» и «half orange» к изображению стола

[INFO] ==> Florence-2-base (<CAPTION_TO_PHRASE_GROUNDING>), \
took 15.7 seconds to execute
each task.

Каскадные задачи (Cascade Tasks)

Также можно передать подпись изображения как входной текст, чтобы заставить Florence-2 найти больше объектов:

task_prompt = "<CAPTION>"
results = run_example(task_prompt, image=dogs_cats)
text_input = results[task_prompt]
task_prompt = "<CAPTION_TO_PHRASE_GROUNDING>"
results = run_example(task_prompt, text_input, image=dogs_cats)
plot_bbox(dogs_cats, results["<CAPTION_TO_PHRASE_GROUNDING>"])

Меняя задачу (task_prompt) между <CAPTION>, <DETAILED_CAPTION> и <MORE_DETAILED_CAPTION>, мы получим больше объектов на изображении.

Каскадные задачи: найденные дополнительные объекты на изображении

Обнаружение с открытым словарём (Open Vocabulary Detection)

<OPEN_VOCABULARY_DETECTION> позволяет Florence-2 находить распознаваемые объекты на изображении без опоры на заранее заданный список категорий. Это универсальный инструмент для поиска предметов, которые могли не быть явно размечены во время обучения. В отличие от <CAPTION_TO_PHRASE_GROUNDING>, которому нужна конкретная текстовая фраза, чтобы найти и выделить определённый объект, <OPEN_VOCABULARY_DETECTION> выполняет широкий просмотр и находит и классифицирует все присутствующие объекты.

Поэтому <OPEN_VOCABULARY_DETECTION> особенно полезен там, где нужен общий обзор всего, что есть на изображении, без предварительных знаний о том, что там может быть. Передайте текст с описанием конкретных объектов, которые ещё не были обнаружены, и модель их найдёт. Например:

task_prompt = "<OPEN_VOCABULARY_DETECTION>"

# Описания оставлены на английском: модель обучена на английских запросах
text = [
    "a house",
    "a tree",
    "a standing cat at the left",
    "a sleeping cat on the ground",
    "a standing cat at the right",
    "a yellow cat",
]

for txt in text:
    results = run_example(
        task_prompt, text_input=txt, image=dogs_cats
    )

    bbox_results = convert_to_od_format(
        results["<OPEN_VOCABULARY_DETECTION>"]
    )

    plot_bbox(dogs_cats, bbox_results)

Результаты обнаружения с открытым словарём для описаний домов, деревьев и кошек

[INFO] ==> Florence-2-base (<OPEN_VOCABULARY_DETECTION>), \
took 15.1 seconds to execute
each task.

Примечание: попытка использовать Florence-2 для поиска объектов, которых на изображении нет, может приводить к ошибкам (примеры см. в блокноте).

Сегментация по выражению-ссылке (Referring expression segmentation)

Также можно сегментировать конкретный объект на изображении и задать его описание (подпись), например «a wine bottle» (бутылка вина) на изображении со столом или «a German Shepherd» (немецкая овчарка) на изображении собак и кошек.

Результат сегментации по выражению-ссылке имеет формат: {'<REFERRING_EXPRESSION_SEGMENTATION>': {'Polygons': [[[polygon]], ...], 'labels': ['', '', ...]}}. Один объект представлен списком полигонов, каждый полигон — [x1, y1, x2, y2, ..., xn, yn].

Полигон (x1, y1, …, xn, yn): токены местоположения представляют вершины полигона по часовой стрелке.

Сначала создадим функцию для отображения сегментации:

from PIL import Image, ImageDraw, ImageFont
import copy
import random
import numpy as np

colormap = [
    "blue",
    "orange",
    "green",
    "purple",
    "brown",
    "pink",
    "gray",
    "olive",
    "cyan",
    "red",
    "lime",
    "indigo",
    "violet",
    "aqua",
    "magenta",
    "coral",
    "gold",
    "tan",
    "skyblue",
]


def draw_polygons(image, prediction, fill_mask=False):
    """
        Рисует маски сегментации с полигонами на изображении.

        Параметры:
          - image_path: путь к файлу изображения.
          - prediction: словарь с ключами 'polygons' и 'labels'.
                  'polygons' — список списков, каждый из которых
                  содержит вершины полигона. 'labels' — список меток,
                  соответствующих каждому полигону.
    - fill_mask: логическое значение, которое указывает, заливать ли
                 полигоны цветом.
    """
    # Загружаем изображение

    draw = ImageDraw.Draw(image)

    # Задаём коэффициент масштабирования при необходимости (1, если без масштабирования)
    scale = 1

    # Перебираем полигоны и метки
    for polygons, label in zip(
        prediction["polygons"], prediction["labels"]
    ):
        color = random.choice(colormap)
        fill_color = random.choice(colormap) if fill_mask else None

        for _polygon in polygons:
            _polygon = np.array(_polygon).reshape(-1, 2)
            if len(_polygon) < 3:
                print("Invalid polygon:", _polygon)
                continue

            _polygon = (_polygon * scale).reshape(-1).tolist()

            # Рисуем полигон
            if fill_mask:
                draw.polygon(_polygon, outline=color, fill=fill_color)
            else:
                draw.polygon(_polygon, outline=color)

            # Рисуем текст метки
            draw.text(
                (_polygon[0] + 8, _polygon[1] + 2), label, fill=color
            )

    # Сохраняем или выводим изображение
    # image.show()  # Показать изображение
    display(image)

Теперь можно запустить функции:

task_prompt = "<REFERRING_EXPRESSION_SEGMENTATION>"

results = run_example(
    task_prompt, text_input="a wine bottle", image=table
)
output_image = copy.deepcopy(table)
draw_polygons(
    output_image,
    results["<REFERRING_EXPRESSION_SEGMENTATION>"],
    fill_mask=True,
)

results = run_example(
    task_prompt, text_input="a german shepherd", image=dogs_cats
)
output_image = copy.deepcopy(dogs_cats)
draw_polygons(
    output_image,
    results["<REFERRING_EXPRESSION_SEGMENTATION>"],
    fill_mask=True,
)

Сегментация бутылки вина на столе и немецкой овчарки

[INFO] ==> Florence-2-base
(<REFERRING_EXPRESSION_SEGMENTATION>), took 207.0 seconds
to execute each task.

Сегментация по области (Region to Segmentation)

С помощью этой задачи также можно задать координаты объекта на изображении, чтобы его сегментировать. Формат входных данных: '<loc_x1><loc_y1><loc_x2><loc_y2>', [x1, y1, x2, y2] — квантованные координаты в диапазоне [0, 999].

Например, при запуске кода:

task_prompt = "<CAPTION_TO_PHRASE_GROUNDING>"
results = run_example(
    task_prompt, text_input="a half orange", image=table
)
results

Результат был таким:

{'<CAPTION_TO_PHRASE_GROUNDING>': {'bboxes': [[343.552001953125,
    689.6640625,
    530.9440307617188,
    873.9840698242188]],
  'labels': ['a half']}}

Используем округлённые координаты из bboxes:

task_prompt = "<REGION_TO_SEGMENTATION>"
results = run_example(
    task_prompt,
    text_input=("<loc_343><loc_690>" "<loc_531><loc_874>"),
    image=table,
)
output_image = copy.deepcopy(table)
draw_polygons(
    output_image, results["<REGION_TO_SEGMENTATION>"], fill_mask=True
)

Мы получили сегментацию объекта по этим координатам (задержка: 83 секунды):

Сегментация половины апельсина по заданным координатам

Регион в текст (Region to Texts)

Также можно передать регион (координаты) и запросить для него подпись:

task_prompt = "<REGION_TO_CATEGORY>"
results = run_example(
    task_prompt,
    text_input=("<loc_343><loc_690>" "<loc_531><loc_874>"),
    image=table,
)
results
[INFO] ==> Florence-2-base (<REGION_TO_CATEGORY>), \
took 14.3 seconds to execute.

{
  '<REGION_TO_CATEGORY>':
    'orange<loc_343><loc_690>'
    '<loc_531><loc_874>'
}

Модель распознала в этом регионе апельсин. Запросим описание:

task_prompt = "<REGION_TO_DESCRIPTION>"
results = run_example(
    task_prompt,
    text_input=("<loc_343><loc_690>" "<loc_531><loc_874>"),
    image=table,
)
results
[INFO] ==> Florence-2-base (<REGION_TO_DESCRIPTION>), \
took 14.6 seconds to execute.

{
  '<REGION_TO_DESCRIPTION>':
    'orange<loc_343><loc_690>'
    '<loc_531><loc_874>'
}

В данном случае описание не дало больше подробностей, но в другой ситуации могло бы. Попробуйте другой пример.

OCR

С помощью Florence-2 можно выполнить оптическое распознавание символов (OCR) на изображении и получить написанный на нём текст (task_prompt = '<OCR>'), а также ограничивающие рамки (положение) распознанного текста (task_prompt = '<OCR_WITH_REGION>'). Эти задачи помогают извлекать текстовую информацию и находить её на изображениях, например читать вывески, этикетки или другие текстовые элементы.

Загрузим на Raspi афишу доклада из Бразилии. Так мы проверим работу модели на другом языке, здесь на португальском:

flayer = Image.open("./images/embarcados.jpg")
# Выводим изображение
plt.figure(figsize=(8, 8))
plt.imshow(flayer)
plt.axis("off")
# plt.title("Image")
plt.show()

Афиша мероприятия Machine Learning Embarcados на португальском языке

Посмотрим, что модель видит на изображении, с задачей '<MORE_DETAILED_CAPTION>':

[INFO] ==> Florence-2-base (<MORE_DETAILED_CAPTION>), \
took 85.2 seconds to execute.

{'<MORE_DETAILED_CAPTION>': 'The image is a promotional poster \
for an event called "Machine Learning Embarcados" hosted by \
Marcelo Roval. The poster has a black background with white \
text. On the left side of the poster, there is a logo of a \
coffee cup with the text "Café Com Embarcados" above it. \
Below the logo, it says "25 de Setembro as 17th" which \
translates to "25th of September as 17" in English. \n\nOn \
the right side, there are two smaller text boxes with the names \
of the participants and their names. The first text box reads \
"Democratizando a Inteligência Artificial para Paises em \
Desenvolvimento" and the second text box says "Toda \
quarta-feira" which is Portuguese for "Transmissão via in \
Portuguese".\n\nIn the center of the image, there has a photo \
of Marcelo, a man with a beard and glasses, smiling at the \
camera. He is wearing a white hard hat and a white shirt. \
The text boxes are in orange and yellow colors.'}

Описание очень точное. Перейдём к более важным словам с помощью задачи OCR:

task_prompt = "<OCR>"
run_example(task_prompt, image=flayer)
[INFO] ==> Florence-2-base (<OCR>), took 37.7 seconds to execute.

{'<OCR>':
 'Machine Learning Café com Embarcado Embarcados '
 'Democratizando a Inteligência Artificial para Paises em '
 '25 de Setembro às 17h Desenvolvimento Toda quarta-feira '
 'Marcelo Roval Professor na UNIFIEI e Transmissão via in '
 'Co-Director do TinyML4D'}

Найдём слова на афише:

task_prompt = "<OCR_WITH_REGION>"
results = run_example(task_prompt, image=flayer)

Создадим также функцию, которая рисует ограничивающие рамки вокруг найденных слов:

def draw_ocr_bboxes(image, prediction):
    scale = 1
    draw = ImageDraw.Draw(image)
    bboxes = prediction["quad_boxes"]
    labels = prediction["labels"]
    for box, label in zip(bboxes, labels):
        color = random.choice(colormap)
        new_box = (np.array(box) * scale).tolist()
        draw.polygon(new_box, width=3, outline=color)
        draw.text(
            (new_box[0] + 8, new_box[1] + 2),
            "{}".format(label),
            align="right",
            fill=color,
        )
    display(image)
output_image = copy.deepcopy(flayer)
draw_ocr_bboxes(output_image, results["<OCR_WITH_REGION>"])

Найденные слова на афише с ограничивающими рамками

Посмотрим найденные слова:

results["<OCR_WITH_REGION>"]["labels"]
'</s>Machine Learning',
 'Café',
 'com',
 'Embarcado',
 'Embarcados',
 'Democratizando a Inteligência',
 'Artificial para Paises em',
 '25 de Setembro ás 17h',
 'Desenvolvimento',
 'Toda quarta-feira',
 'Marcelo Roval',
 'Professor na UNIFIEI e',
 'Transmissão via',
 'in',
 'Co-Director do TinyML4D']

Сводка по задержкам

Задержка для разных задач Florence-2 на Raspberry Pi (Raspi-5) зависит от сложности задачи:

  • Описание изображения (Image Captioning): примерно 16–17 секунд на создание подписи.
  • Подробное описание (Detailed Captioning): задержка выросла до 25–27 секунд, так как модель генерирует более тонкие описания сцены.
  • Более подробное описание (More Detailed Captioning): около 32–50 секунд; задержка растёт по мере усложнения описания.
  • Обнаружение объектов (Object Detection): примерно 20–41 секунда в зависимости от сложности изображения и количества найденных объектов.
  • Визуальная привязка (Visual Grounding): около 15–16 секунд на определение местоположения конкретных объектов по текстовым запросам.
  • OCR (оптическое распознавание символов): извлечение текста с изображения заняло около 37–38 секунд.
  • Сегментация и сегментация по области (Region to Segmentation): эти задачи заметно дольше, около 83–207 секунд в зависимости от сложности и количества областей.

Эти задержки подчёркивают ограничения ресурсов периферийных устройств, таких как Raspberry Pi, и показывают, что модель и окружение нужно оптимизировать для работы в реальном времени.

Загрузка ресурсов Raspi-5 во время задачи Florence OD в htop

Сложные задачи могут использовать всю память Raspi-5 объёмом 8 ГБ. Например, на скриншоте выше, снятом во время задачи Florence OD, видно, что четыре ядра CPU работают на полной мощности, а используется более 5 ГБ памяти. Имеет смысл увеличить объём SWAP-памяти до 2 ГБ.

Проверка температуры процессора командой vcgencmd measure_temp показала, что температура может подниматься до +80 °C.

Дообучение (Fine-Tuning)

Как мы видели в этой лабораторной работе, Florence поддерживает множество задач «из коробки», включая описание изображений, обнаружение объектов, OCR и другие. Однако, как и другие предобученные базовые модели, Florence-2 может потребовать знаний конкретной предметной области. Например, ей может понадобиться улучшение на медицинских или спутниковых снимках. В таких случаях необходимо дообучение (fine-tuning) на собственном наборе данных. Руководство Roboflow How to Fine-tune Florence-2 for Object Detection Tasks показывает, как дообучить Florence-2 на наборах данных для обнаружения объектов, чтобы повысить качество модели для вашей задачи.

Следуя этому руководству, можно дообучить модель Florence-2 обнаруживать коробки и колёса, которые использовались в предыдущих лабораторных работах:

Результат дообучения Florence-2 на наборе данных box и wheel

Важно отметить, что после дообучения модель всё ещё может обнаруживать классы, которых нет в нашем наборе данных, например кошек, собак, винограда и т. д., как мы видели раньше.

Полный проект дообучения с набором данных, размеченным в Roboflow и выполненным в Colab, можно найти в блокноте:

В другом примере, в статье Fine-tuning Florence-2 - Microsoft’s Cutting-edge Vision Language Models, авторы показывают пример дообучения Florence на DocVQA. Авторы сообщают, что Florence-2 может отвечать на вопросы по изображениям (VQA), но опубликованные модели не включают эту возможность.

Итоги

Florence-2 предлагает универсальный и мощный подход к визуально-языковым задачам на периферии: по производительности она сопоставима с более крупными моделями, заточенными под конкретные задачи, такими как YOLO для обнаружения объектов, BERT/RoBERTa для анализа текста и специализированные модели OCR.

Благодаря мультимодальной архитектуре трансформера Florence-2 гибче YOLO с точки зрения решаемых задач. Сюда входят обнаружение объектов, описание изображений и визуальная привязка.

В отличие от BERT, который сосредоточен только на языке, Florence-2 объединяет зрение и язык, что позволяет ей хорошо справляться с приложениями, которым нужны обе модальности, например описание изображений и визуальная привязка.

Кроме того, традиционные модели OCR, такие как Tesseract и EasyOCR, предназначены только для распознавания и извлечения текста из изображений, тогда как возможности OCR у Florence-2 входят в более широкую систему, которая включает понимание контекста и согласование визуального и текстового содержимого. Это делает её особенно полезной в сценариях, где нужно и читать текст, и понимать его контекст на изображении.

В целом Florence-2 выделяется тем, что объединяет различные визуально-языковые задачи в единой модели, которая достаточно эффективна, чтобы работать на периферийных устройствах вроде Raspberry Pi. Это делает её привлекательным выбором для разработчиков и исследователей, которые изучают приложения ИИ на периферии.

Ключевые преимущества Florence-2

  1. Унифицированная архитектура

    • Одна модель решает несколько задач зрения вместо специализированных моделей (YOLO, BERT, Tesseract);
    • Исключает необходимость развёртывания и интеграции нескольких моделей;
    • Единый API и интерфейс для всех задач.
  2. Сравнение производительности

    • Обнаружение объектов: сопоставимо с YOLOv8 (~37,5 mAP на COCO против ~39,7 mAP у YOLOv8), несмотря на универсальность модели;
    • Распознавание текста: эффективно работает с несколькими языками, как специализированные модели OCR (Tesseract, EasyOCR);
    • Понимание языка: включает возможности, похожие на BERT, для обработки текста, добавляя визуальный контекст.
  3. Эффективность использования ресурсов

    • Базовая модель (232 млн параметров) показывает хорошие результаты, несмотря на небольшой размер;
    • Эффективно работает на периферийных устройствах (Raspberry Pi);
    • Развёртывание одной модели вместо нескольких специализированных.

Компромиссы

  1. Производительность и специализированные модели

    • Серия YOLO может обеспечивать более быстрый инференс для чистого обнаружения объектов;
    • Специализированные модели OCR могут лучше справляться со сложной структурой документов;
    • BERT/RoBERTa обеспечивают более глубокое понимание языка для задач только с текстом.
  2. Требования к ресурсам

    • Более высокая задержка на периферийных устройствах (15–200 с в зависимости от задачи);
    • Требуется аккуратное управление памятью на Raspberry Pi;
    • Может потребоваться оптимизация для приложений реального времени.
  3. Соображения по развёртыванию

    • Первоначальная настройка сложнее, чем у моделей одного назначения;
    • Нужно понимать множество типов задач и запросов;
    • Освоение оптимальной инженерии запросов требует времени.

Лучшие сценарии применения

  1. Среды с ограниченными ресурсами

    • Периферийные устройства, которым нужны несколько возможностей зрения;
    • Системы с ограниченным объёмом хранения и развёртывания;
    • Приложения, которым нужна гибкая обработка изображений.
  2. Мультимодальные приложения

    • Системы модерации контента;
    • Инструменты доступности;
    • Рабочие процессы анализа документов.
  3. Быстрое прототипирование

    • Быстрое развёртывание возможностей зрения;
    • Проверка множества задач зрения без отдельных моделей;
    • Разработка концепции (proof of concept).

Перспективы

Florence-2 знаменует переход к унифицированным моделям зрения, которые в перспективе могут заменить архитектуры, заточенные под конкретные задачи, во многих приложениях. Специализированные модели сохраняют преимущества в отдельных сценариях, но удобство и эффективность унифицированных моделей вроде Florence-2 делают их всё более привлекательными для реальных развёртываний.

Лабораторная работа демонстрирует, что Florence-2 пригодна для периферийных устройств, что открывает перспективы для IoT, мобильных вычислений и встраиваемых систем, где развёртывание нескольких специализированных моделей было бы непрактичным.

Ресурсы

Обзор

Лабораторные работы этого раздела посвящены темам и методам, которые применимы на разных аппаратных платформах. Эти работы не привязаны к конкретной плате, что позволяет сосредоточиться на фундаментальных концепциях и алгоритмах, используемых в приложениях TinyML (сверхмалого машинного обучения).

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

УпражнениеNicla VisionXIAO ESP32S3
Инженерия признаков для KWS✔ Ссылка✔ Ссылка
Блок спектральных признаков DSP✔ Ссылка✔ Ссылка

Спектральные признаки DSP

Мультяшная иллюстрация в стиле 1950-х: учёные в лаборатории вибраций, осциллограф и графики БПФ и вейвлетов
Промпт DALL·E 3: мультяшная иллюстрация в стиле 1950-х: учёные (мужчина и женщина) в лаборатории исследования вибраций. Мужчина изучает старую электронику с помощью линейки. Женщина работает за компьютером со сложными графиками вибрации. На деревянном столе лежат платы с датчиками, среди них — акселерометр. На классическом компьютере с округлым корпусом открыта Arduino IDE с кодом назначения пинов светодиодов и алгоритмами машинного обучения для обнаружения движения. Монитор порта показывает БПФ, классификацию, вейвлеты и DSP. Сцену дополняют винтажные лампы, инструменты и графики БПФ и вейвлетов.

Обзор

Проекты TinyML, связанные с движением (или вибрацией), работают с данными IMU — обычно акселерометров и гироскопов. Такие временные ряды нужно предварительно обработать перед обучением модели машинного обучения, и для встраиваемого машинного обучения это непростая задача. Тем не менее Edge Impulse упрощает её с помощью шага цифровой обработки сигналов (DSP) и, точнее, блока спектральных признаков для инерциальных датчиков.

Но как это устроено внутри? Давайте разберёмся.

Обзор извлечения признаков

Извлечение признаков из набора данных, записанных инерциальными датчиками, такими как акселерометры, включает обработку и анализ сырых данных. Акселерометры измеряют ускорение объекта по одной или нескольким осям (обычно трём: X, Y и Z). Эти измерения помогают понять различные аспекты движения объекта, например характер перемещений и вибрации. Ниже — общая схема процесса:

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

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

Studio не выполняет нормализацию и стандартизацию, поэтому при работе со слиянием данных датчиков (Sensor Fusion) этот шаг иногда нужно выполнить до загрузки данных в Studio. Это особенно важно в проектах со слиянием датчиков, как в этом руководстве: Sensor Data Fusion with Spresense and CommonSense.

Сегментация. В зависимости от природы данных и задачи может понадобиться разделить данные на более короткие сегменты, или окна. Это помогает сосредоточиться на конкретных событиях или активности в наборе данных и делает извлечение признаков более управляемым и содержательным. Выбор размера окна и перекрытия (шага окна, window span) зависит от задачи и частоты интересующих событий. Как эмпирическое правило, стоит стараться захватить несколько «циклов данных».

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

  • Признаки временной области описывают статистические свойства данных внутри каждого сегмента: среднее, медиану, стандартное отклонение, асимметрию (skewness), эксцесс (kurtosis) и частоту пересечения нуля.
  • Признаки частотной области получают, преобразуя данные в частотную область с помощью таких методов, как быстрое преобразование Фурье (FFT). Типичные признаки этой группы — спектр мощности, спектральная энергия, доминантные частоты (амплитуда и частота) и спектральная энтропия.
  • Признаки временно-частотной области объединяют информацию о времени и частоте, например кратковременное преобразование Фурье (STFT) или дискретное вейвлет-преобразование (DWT). Они дают более подробное представление о том, как частотный состав сигнала меняется во времени.

Во многих случаях число извлечённых признаков может быть большим, что ведёт к переобучению или росту вычислительной сложности. Методы отбора признаков, такие как взаимная информация, корреляционные методы или метод главных компонент (PCA), помогают выявить наиболее значимые признаки для конкретного приложения и снизить размерность набора данных. Studio умеет выполнять такие вычисления для признаков.

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

Проект классификации движения TinyML

Блок спектральных признаков в проекте классификации движения

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

  • Морская перевозка (паллеты на кораблях);
  • Наземная перевозка (паллеты в грузовике или поезде);
  • Подъём (паллеты, которые перемещает погрузчик);
  • Простой (паллеты на складе).

Акселерометры передавали данные о паллете (или контейнере).

Схема кейса: паллет с акселерометром при транспортировке

Ниже показан один образец (сырые данные) длиной 10 секунд, записанный с частотой дискретизации 50 Гц:

Образец сырых данных акселерометра длиной 10 секунд

Результат похож, если тот же анализ провести для другого набора данных с тем же принципом, но с другой частотой дискретизации — 62,5 Гц вместо 50 Гц.

Предобработка данных

Сырые данные, полученные акселерометром (данные «временного ряда»), нужно преобразовать в «табличные данные» одним из типичных методов извлечения признаков, описанных в предыдущем разделе.

Для извлечения признаков данные нужно разбить на сегменты с помощью скользящего окна по образцам. В проекте акселерометр записывал данные каждые 10 секунд с частотой дискретизации 62,5 Гц. Окно длиной 2 секунды захватывает 375 точек данных (3 оси × 2 секунды × 62,5 отсчёта). Окно сдвигается каждые 80 мс, что создаёт набор данных большего размера, где каждый экземпляр содержит 375 «сырых признаков».

Схема окна скользящей сегментации данных

В Studio предыдущая версия (V1) блока спектрального анализа извлекала из временной области только RMS, а из частотной области — пики и частоты (с помощью FFT) и мощностные характеристики (PSD) сигнала во времени. В результате получался фиксированный табличный набор из 33 признаков (по 11 на каждую ось).

Список признаков, которые извлекала первая версия блока спектрального анализа

Эти 33 признака были входным тензором нейросетевого классификатора.

В 2022 году Edge Impulse выпустила вторую версию блока спектрального анализа, которую мы и рассмотрим.

Блок спектрального анализа Edge Impulse версии 2 изнутри

Во второй версии статистические признаки временной области для каждой оси/канала таковы:

  • RMS;
  • асимметрия (Skewness);
  • эксцесс (Kurtosis).

А спектральные признаки частотной области для каждой оси/канала:

  • спектральная мощность (Spectral Power);
  • асимметрия (Skewness; в следующей версии);
  • эксцесс (Kurtosis; в следующей версии).

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

Склонируйте публичный проект. Также можно следовать объяснению и поэкспериментировать с кодом в моём блокноте Google Colab: Edge Impulse Spectral Analysis Block Notebook.

Начнём с импорта библиотек:

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import math
from scipy.stats import skew, kurtosis
from scipy import signal
from scipy.signal import welch
from scipy.stats import entropy
from sklearn import preprocessing
import pywt

plt.rcParams['figure.figsize'] = (12, 6)
plt.rcParams['lines.linewidth'] = 3

Из изучаемого проекта выберем образец данных акселерометра:

  • Размер окна 2 секунды: [2,000] мс;
  • Частота дискретизации: [62.5] Гц;
  • Фильтр [None] (для простоты) и
  • Длина FFT: [16].
f = 62.5 # Гц
wind_sec = 2 # секунды
FFT_Length = 16
axis = ['accX', 'accY', 'accZ']
n_sensors = len(axis)

Impulse проекта: блоки обработки и обучения

Выбрав Raw Features на вкладке Spectral Analysis в Studio, можно скопировать в буфер обмена все 375 точек данных для конкретного двухсекундного окна.

Вкладка Spectral Analysis с выбранными Raw Features

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

data = [
    -5.6330,  0.2376,  9.8701,
    -5.9442,  0.4830,  9.8701,
    -5.4217, ...
]
No_raw_features = len(data)
N = int(No_raw_features/n_sensors)

Всего сырых признаков 375, но мы будем работать с каждой осью отдельно, где (число отсчётов на ось).

Наша цель — понять, как Edge Impulse получает обработанные признаки.

Обработанные признаки в Studio

Также вставьте обработанные признаки в переменную, чтобы сравнить рассчитанные в Python признаки с теми, что выдаёт Studio:

features = [
    2.7322, -0.0978, -0.3813,
    2.3980, 3.8924, 24.6841,
    9.6303, ...
]
N_feat = len(features)
N_feat_axis = int(N_feat/n_sensors)

Всего обработанных признаков 39, то есть 13 признаков на ось.

Если внимательно посмотреть на эти 13 признаков, в них 3 признака временной области (RMS, Skewness и Kurtosis):

  • [rms] [skew] [kurtosis]

и 10 признаков частотной области (вернёмся к ним позже):

  • [spectral skew][spectral kurtosis][Spectral Power 1] ... [Spectral Power 8]

Разделение сырых данных по датчикам

Данные содержат отсчёты всех осей; разделим их и построим графики отдельно:

def plot_data(sensors, axis, title):
    [plt.plot(x, label=y) for x,y in zip(sensors, axis)]
    plt.legend(loc='lower right')
    plt.title(title)
    plt.xlabel('Отсчёт')
    plt.ylabel('Значение')
    plt.box(False)
    plt.grid()
    plt.show()

accX = data[0::3]
accY = data[1::3]
accZ = data[2::3]
sensors = [accX, accY, accZ]
plot_data(sensors, axis, 'Сырые признаки')

График сырых признаков по трём осям акселерометра

Вычитание среднего

Далее вычтем среднее из data. Вычитание среднего — распространённый шаг предобработки в статистике и машинном обучении. Его цель — центрировать данные около нуля. Это важно, потому что центрирование может выявить закономерности и связи, которые скрыты, если данные не центрированы.

Вот несколько конкретных причин, почему вычитание среднего полезно:

  • Упрощает анализ: после центрирования среднее становится нулём, что упрощает некоторые вычисления и их интерпретацию.
  • Убирает смещение: если данные смещены, вычитание среднего устраняет смещение и позволяет провести более точный анализ.
  • Помогает выявить закономерности: центрирование может помочь обнаружить закономерности, скрытые в нецентрированных данных. Например, при анализе временного ряда оно помогает увидеть тренды во времени.
  • Может улучшить качество: в некоторых алгоритмах машинного обучения центрирование повышает качество, так как снижает влияние выбросов и делает данные лучше сопоставимыми. В целом вычитание среднего — простой, но мощный приём для улучшения анализа и интерпретации данных.
dtmean = [
    (sum(x) / len(x))
    for x in sensors
]

[
    print('mean_' + x + ' =', round(y, 4))
    for x, y in zip(axis, dtmean)
][0]

accX = [(x - dtmean[0]) for x in accX]
accY = [(x - dtmean[1]) for x in accY]
accZ = [(x - dtmean[2]) for x in accZ]
sensors = [accX, accY, accZ]

plot_data(sensors, axis, 'Сырые признаки — вычитание среднего')

График сигналов после вычитания среднего

Статистические признаки временной области

Вычисление RMS

Среднеквадратичное значение (RMS) набора значений (или непрерывного сигнала) — это квадратный корень из среднего арифметического квадратов этих значений или квадрата функции, которая задаёт непрерывную форму сигнала. В физике RMS значения электрического тока определяется как «значение постоянного тока, который рассеивает ту же мощность в резисторе».

Для набора из значений RMS вычисляется так:

Обратите внимание: значение RMS различается для исходных сырых данных и для данных после вычитания среднего.

# Используем numpy и стандартизованные данные (после вычитания среднего)
rms = [np.sqrt(np.mean(np.square(x))) for x in sensors]

Сравним рассчитанные значения RMS с теми, что показывает Edge Impulse:

[print('rms_'+x+'= ', round(y, 4)) for x,y in zip(axis, rms)][0]
print("\nСравнение с признаками результата Edge Impulse")
print(features[0:N_feat:N_feat_axis])

rms_accX= 2.7322

rms_accY= 0.7833

rms_accZ= 0.1383

Сравнение с признаками из Edge Impulse:

[2.7322, 0.7833, 0.1383]

Вычисление асимметрии и эксцесса

В статистике асимметрия и эксцесс — два способа описать форму распределения.

Посмотрим на распределение значений датчиков:

fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(13, 4))
sns.kdeplot(accX, fill=True, ax=axes[0])
sns.kdeplot(accY, fill=True, ax=axes[1])
sns.kdeplot(accZ, fill=True, ax=axes[2])
axes[0].set_title('accX')
axes[1].set_title('accY')
axes[2].set_title('accZ')
plt.suptitle('Распределение значений IMU-датчиков', fontsize=16, y=1.02)
plt.show()

Оценка плотности распределения значений датчиков по осям

Асимметрия (skewness) — мера асимметрии распределения. Её значение может быть положительным или отрицательным.

Виды асимметрии распределения

  • Отрицательная асимметрия означает, что «хвост» распределения находится слева и тянется в сторону более отрицательных значений.
  • Положительная асимметрия означает, что «хвост» находится справа и тянется в сторону более положительных значений.
  • Нулевое значение означает отсутствие асимметрии: распределение полностью симметрично.
skew = [skew(x, bias=False) for x in sensors]
[print('skew_'+x+'= ', round(y, 4))
  for x,y in zip(axis, skew)][0]
print("\nСравнение с признаками результата Edge Impulse")
features[1:N_feat:N_feat_axis]

skew_accX= -0.099

skew_accY= 0.1756

skew_accZ= 6.9463

Сравнение с признаками из Edge Impulse:

[-0.0978, 0.1735, 6.8629]

Эксцесс (kurtosis) — мера того, является ли распределение «тяжёлохвостым» или «легкохвостым» по сравнению с нормальным распределением.

Виды эксцесса распределения относительно нормального

  • Эксцесс нормального распределения равен нулю.
  • Если эксцесс распределения отрицательный, его называют платикуртическим: такое распределение даёт меньше и менее экстремальных выбросов, чем нормальное.
  • Если эксцесс положительный, распределение называют лептокуртическим: оно даёт больше выбросов, чем нормальное.
kurt = [kurtosis(x, bias=False) for x in sensors]
[print('kurt_'+x+'= ', round(y, 4))
  for x,y in zip(axis, kurt)][0]
print("\nСравнение с признаками результата Edge Impulse")
features[2:N_feat:N_feat_axis]

kurt_accX= -0.3475

kurt_accY= 1.2673

kurt_accZ= 68.1123

Сравнение с признаками из Edge Impulse:

[-0.3813, 1.1696, 65.3726]

Спектральные признаки

Отфильтрованный сигнал передаётся в раздел спектральной мощности (Spectral power), который вычисляет FFT для получения спектральных признаков.

Поскольку размер окна обычно больше размера FFT, окно разбивается на кадры («подокна»), и FFT вычисляется для каждого кадра.

Длина FFT — размер FFT. Он определяет количество бинов FFT и разрешение, с которым можно разделить пики частот. Малое значение означает, что в одном бине FFT усредняется больше сигналов, но при этом уменьшается количество признаков и размер модели. Большое значение разделяет сигналы по большему числу бинов и даёт более крупную модель.

  • Общее количество признаков спектральной мощности зависит от настроек фильтра и параметров FFT. Без фильтрации количество признаков равно половине длины FFT.

Спектральная мощность — метод Уэлча

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

# Функция, которую использует Edge Impulse вместо scipy.signal.welch().
def welch_max_hold(fx, sampling_freq, nfft, n_overlap):
    n_overlap = int(n_overlap)
    spec_powers = [0 for _ in range(nfft//2+1)]
    ix = 0
    while ix <= len(fx):
        # Срез обрезается, если end_idx больше длины,
        # а rfft автоматически дополняет нулями
        fft_out = np.abs(np.fft.rfft(fx[ix:ix+nfft], nfft))
        spec_powers = np.maximum(spec_powers, fft_out**2/nfft)
        ix = ix + (nfft-n_overlap)
    return np.fft.rfftfreq(nfft, 1/sampling_freq), spec_powers

Применим эту функцию к трём сигналам:

fax,Pax = welch_max_hold(accX, fs, FFT_Length, 0)
fay,Pay = welch_max_hold(accY, fs, FFT_Length, 0)
faz,Paz = welch_max_hold(accZ, fs, FFT_Length, 0)
specs = [Pax, Pay, Paz ]

Построим график спектра мощности P(f):

plt.plot(fax,Pax, label='accX')
plt.plot(fay,Pay, label='accY')
plt.plot(faz,Paz, label='accZ')
plt.legend(loc='upper right')
plt.xlabel('Частота (Гц)')
#plt.ylabel('PSD [V**2/Hz]')
plt.ylabel('Мощность')
plt.title('Спектр мощности P(f) по методу Уэлча')
plt.grid()
plt.box(False)
plt.show()

Спектр мощности P(f) по методу Уэлча для трёх осей

Помимо спектра мощности, в частотной области можно также учесть асимметрию и эксцесс признаков (в новой версии, которая будет доступна позже):

spec_skew = [skew(x, bias=False) for x in specs]
spec_kurtosis = [kurtosis(x, bias=False) for x in specs]

Теперь выведем все спектральные признаки для каждой оси и сравним их с Edge Impulse:

print("Обработанные спектральные признаки EI (accX): ")
print(features[3:N_feat_axis][0:])
print("\nРассчитанные признаки:")
print (round(spec_skew[0],4))
print (round(spec_kurtosis[0],4))
[print(round(x, 4)) for x in Pax[1:]][0]

Обработанные спектральные признаки EI (accX):

2.398, 3.8924, 24.6841, 9.6303, 8.4867, 7.7793, 2.9963, 5.6242, 3.4198, 4.2735

Рассчитанные признаки:

2.9069 8.5569 24.6844 9.6304 8.4865 7.7794 2.9964 5.6242 3.4198 4.2736

print("Обработанные спектральные признаки EI (accY): ")
print(features[16:26][0:]) # 13: 3+N_feat_axis;
                           # 26 = 2x N_feat_axis
print("\nРассчитанные признаки:")
print (round(spec_skew[1],4))
print (round(spec_kurtosis[1],4))
[print(round(x, 4)) for x in Pay[1:]][0]

Обработанные спектральные признаки EI (accY):

0.9426, -0.8039, 5.429, 0.999, 1.0315, 0.9459, 1.8117, 0.9088, 1.3302, 3.112

Рассчитанные признаки:

1.1426 -0.3886 5.4289 0.999 1.0315 0.9458 1.8116 0.9088 1.3301 3.1121

print("Обработанные спектральные признаки EI (accZ): ")
print(features[29:][0:]) #29: 3+(2*N_feat_axis);
print("\nРассчитанные признаки:")
print (round(spec_skew[2],4))
print (round(spec_kurtosis[2],4))
[print(round(x, 4)) for x in Paz[1:]][0]

Обработанные спектральные признаки EI (accZ):

0.3117, -1.3812, 0.0606, 0.057, 0.0567, 0.0976, 0.194, 0.2574, 0.2083, 0.166

Рассчитанные признаки:

0.3781 -1.4874 0.0606 0.057 0.0567 0.0976 0.194 0.2574 0.2083 0.166

Временно-частотная область

Вейвлеты

Вейвлет — мощный инструмент для анализа сигналов с кратковременными особенностями или резкими изменениями, такими как всплески или фронты, которые трудно интерпретировать традиционными методами на основе преобразования Фурье.

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

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

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

Выберем вейвлет на блоке спектральных признаков в том же проекте:

  • Тип: Wavelet;
  • Уровень декомпозиции вейвлетом: 1;
  • Вейвлет: bior1.3.

Результат настройки блока спектральных признаков с вейвлетом

Вейвлет-функция

wavelet_name='bior1.3'
num_layer = 1

wavelet = pywt.Wavelet(wavelet_name)
[phi_d,psi_d,phi_r,psi_r,x] = wavelet.wavefun(level=5)
plt.plot(x, psi_d, color='red')
plt.title('Вейвлет-функция')
plt.ylabel('Значение')
plt.xlabel('Время')
plt.grid()
plt.box(False)
plt.show()

График вейвлет-функции bior1.3

Как и раньше, скопируем и вставим обработанные признаки:

Обработанные вейвлет-признаки в Studio

features = [
    3.6251, 0.0615, 0.0615,
    -7.3517, -2.7641, 2.8462,
    5.0924, ...
]
N_feat = len(features)
N_feat_axis = int(N_feat/n_sensors)

Edge Impulse вычисляет дискретное вейвлет-преобразование (DWT) для каждого выбранного уровня декомпозиции вейвлетом. После этого извлекаются признаки.

Для вейвлетов извлекаются базовые статистические значения, значения пересечений и энтропия. Всего на каждый уровень приходится 14 признаков:

  • [11] статистических признаков: n5, n25, n75, n95, mean, median, стандартное отклонение (std), дисперсия (var), среднеквадратичное значение (rms), kurtosis (эксцесс) и асимметрия (skew);
  • [2] признака пересечений: частота пересечения нуля (zcross) и частота пересечения среднего уровня (mcross) — то есть количество раз, когда сигнал пересекает базовую линию и средний уровень (y = u), за единицу времени;
  • [1] признак сложности: энтропия (Entropy) — характеристическая мера сложности сигнала.

Все 14 значений рассчитываются для каждого уровня (включая L0 — исходный сигнал).

  • Общее количество признаков зависит от настроек фильтра и числа уровней. Например, при фильтрации [None] и уровне [1] количество признаков на ось составит (L0 и L1) = 28. Для трёх осей получится 84 признака.

Анализ вейвлетами

Вейвлет-анализ раскладывает сигнал (accX, accY и accZ) на различные частотные компоненты с помощью набора фильтров. Эти фильтры разделяют компоненты на низкочастотные (медленно меняющиеся части сигнала, содержащие долгосрочные закономерности), такие как accX_l1, accY_l1, accZ_l1, и высокочастотные (быстро меняющиеся части, содержащие краткосрочные закономерности), такие как accX_d1, accY_d1, accZ_d1. Это позволяет извлекать признаки для дальнейшего анализа или классификации.

Используются только низкочастотные компоненты (коэффициенты аппроксимации, или cA). В этом примере предполагается один уровень (одноуровневое дискретное вейвлет-преобразование), когда функция возвращает кортеж. При многоуровневой декомпозиции («Multilevel 1D Discrete Wavelet Transform») результатом будет список (подробности см. в описании дискретного вейвлет-преобразования (DWT)).

(accX_l1, accX_d1) = pywt.dwt(accX, wavelet_name)
(accY_l1, accY_d1) = pywt.dwt(accY, wavelet_name)
(accZ_l1, accZ_d1) = pywt.dwt(accZ, wavelet_name)
sensors_l1 = [accX_l1, accY_l1, accZ_l1]

# Строим график мощности спектра в зависимости от частоты
plt.plot(accX_l1, label='accX')
plt.plot(accY_l1, label='accY')
plt.plot(accZ_l1, label='accZ')
plt.legend(loc='lower right')
plt.xlabel('Время')
plt.ylabel('Значение')
plt.title('Аппроксимация вейвлетом')
plt.grid()
plt.box(False)
plt.show()

Аппроксимация вейвлетом для трёх осей акселерометра

Извлечение признаков

Начнём с базовых статистических признаков. Обратите внимание: функцию применяем и к исходным сигналам, и к коэффициентам аппроксимации cA, полученным из DWT:

def calculate_statistics(signal):
    n5 = np.percentile(signal, 5)
    n25 = np.percentile(signal, 25)
    n75 = np.percentile(signal, 75)
    n95 = np.percentile(signal, 95)
    median = np.percentile(signal, 50)
    mean = np.mean(signal)
    std = np.std(signal)
    var = np.var(signal)
    rms = np.sqrt(np.mean(np.square(signal)))
    return [n5, n25, n75, n95, median, mean, std, var, rms]

stat_feat_l0 = [calculate_statistics(x) for x in sensors]
stat_feat_l1 = [calculate_statistics(x) for x in sensors_l1]

Асимметрия и эксцесс:

skew_l0 = [skew(x, bias=False) for x in sensors]
skew_l1 = [skew(x, bias=False) for x in sensors_l1]
kurtosis_l0 = [kurtosis(x, bias=False) for x in sensors]
kurtosis_l1 = [kurtosis(x, bias=False) for x in sensors_l1]

Пересечение нуля (zcross) — это количество раз, когда коэффициент вейвлета пересекает нулевую ось. Оно помогает оценить частотное содержание сигнала, так как высокочастотные сигналы обычно имеют больше пересечений нуля, чем низкочастотные.

Пересечение среднего (mcross), в свою очередь, — это количество раз, когда коэффициент вейвлета пересекает среднее значение сигнала. Оно помогает оценить амплитуду, так как сигналы с большой амплитудой обычно имеют больше пересечений среднего, чем сигналы с малой амплитудой.

def getZeroCrossingRate(arr):
    my_array = np.array(arr)
    zcross = float(
        "{:.2f}".format(
          (((my_array[:-1] * my_array[1:]) < 0).sum()) / len(arr)
        )
    )
    return zcross

def getMeanCrossingRate(arr):
    mcross = getZeroCrossingRate(np.array(arr) - np.mean(arr))
    return mcross

def calculate_crossings(list):
    zcross=[]
    mcross=[]
    for i in range(len(list)):
        zcross_i = getZeroCrossingRate(list[i])
        zcross.append(zcross_i)
        mcross_i = getMeanCrossingRate(list[i])
        mcross.append(mcross_i)
    return zcross, mcross

cross_l0 = calculate_crossings(sensors)
cross_l1 = calculate_crossings(sensors_l1)

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

def calculate_entropy(signal, base=None):
    value, counts = np.unique(signal, return_counts=True)
    return entropy(counts, base=base)

entropy_l0 = [calculate_entropy(x) for x in sensors]
entropy_l1 = [calculate_entropy(x) for x in sensors_l1]

Теперь перечислим все вейвлет-признаки и сгруппируем их по уровням.

L1_features_names = [
    "L1-n5", "L1-n25", "L1-n75", "L1-n95", "L1-median",
    "L1-mean", "L1-std", "L1-var", "L1-rms", "L1-skew",
    "L1-Kurtosis", "L1-zcross", "L1-mcross", "L1-entropy"
]

L0_features_names = [
    "L0-n5", "L0-n25", "L0-n75", "L0-n95", "L0-median",
    "L0-mean", "L0-std", "L0-var", "L0-rms", "L0-skew",
    "L0-Kurtosis", "L0-zcross", "L0-mcross", "L0-entropy"
]

all_feat_l0 = []
for i in range(len(axis)):
    feat_l0 = (
        stat_feat_l0[i]
        + [skew_l0[i]]
        + [kurtosis_l0[i]]
        + [cross_l0[0][i]]
        + [cross_l0[1][i]]
        + [entropy_l0[i]]
    )
    [print(axis[i] + ' +x+= ', round(y, 4))
       for x, y in zip(L0_features_names, feat_l0)][0]
    all_feat_l0.append(feat_l0)

all_feat_l0 = [
    item
    for sublist in all_feat_l0
    for item in sublist
]
print(f"\nВсего признаков L0 = {len(all_feat_l0)}")

all_feat_l1 = []
for i in range(len(axis)):
    feat_l1 = (
        stat_feat_l1[i]
        + [skew_l1[i]]
        + [kurtosis_l1[i]]
        + [cross_l1[0][i]]
        + [cross_l1[1][i]]
        + [entropy_l1[i]]
    )
    [print(axis[i]+' '+x+'= ', round(y, 4))
       for x,y in zip(L1_features_names, feat_l1)][0]
    all_feat_l1.append(feat_l1)

all_feat_l1 = [
    item
    for sublist in all_feat_l1
    for item in sublist
]
print(f"\nВсего признаков L1 = {len(all_feat_l1)}")

Результат расчёта вейвлет-признаков для всех осей

Итоги

Edge Impulse Studio — мощная онлайн-платформа, которая может взять на себя задачи предобработки. Тем не менее с инженерной точки зрения важно понимать, что происходит «под капотом». Эти знания помогут подобрать лучшие опции и гиперпараметры для настройки проектов.

Daniel Situnayake написал в своём блоге: «Сырые данные датчиков многомерны и зашумлены. Алгоритмы цифровой обработки сигналов помогают отделить сигнал от шума. DSP — важная часть встраиваемой инженерии, и многие периферийные процессоры имеют аппаратное ускорение DSP. Как инженер по ML, освоив основы DSP, вы получаете суперсилу для работы с высокочастотными временными рядами в своих моделях». Рекомендую прочитать его замечательный пост целиком: nn to cpp: What you need to know about porting deep learning models to the edge.

Инженерия признаков для KWS

Мультяшная сцена в стиле 1950-х: два учёных изучают графики БПФ и временных сигналов в аудиолаборатории
Промпт DALL·E 3: мультяшная сцена в стиле 1950-х в аудиолаборатории. Два учёных, один с лупой, другой с записями, изучают большие графики, прикреплённые к стене. На графиках — спектры БПФ и временные кривые аудиоданных. Комната в ретро-стиле: деревянные столы, винтажные лампы и классические инструменты анализа звука.

Обзор

В этом практическом руководстве основное внимание уделяется той критической роли, которую инженерия признаков играет в повышении качества моделей машинного обучения для задач классификации звука, например для распознавания речи. Важно понимать, что качество любой модели машинного обучения во многом зависит от качества используемых признаков. Мы разберём «внутреннюю механику» извлечения признаков, в первую очередь кепстральные коэффициенты на мел-шкале (Mel-frequency Cepstral Coefficients, MFCC) — основу обработки аудиосигналов.

Модели машинного обучения, особенно классические алгоритмы, не понимают звуковых волн. Они работают с числами, организованными определённым образом, то есть с признаками. Признаки описывают характеристики аудиосигнала и помогают модели отличать одни звуки от других.

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

KWS

Самое распространённое применение TinyML — распознавание ключевых слов (Keyword Spotting, KWS), которое является частью более широкой области распознавания речи. Общее распознавание речи переводит всю произнесённую речь в текст, а KWS сосредоточено на обнаружении определённых «ключевых слов» или «слов пробуждения» в непрерывном аудиопотоке. Система обучена распознавать эти ключевые слова как заранее заданные фразы или слова, например yes или no. Короче говоря, KWS — это специализированная форма распознавания речи со своими задачами и требованиями.

Типичный процесс KWS с преобразователем признаков MFCC выглядит так:

Схема типичного процесса KWS с преобразованием MFCC

Применения KWS

  • Голосовые помощники: в устройствах вроде Amazon Alexa или Google Home KWS используется для обнаружения слова пробуждения («Alexa» или «Hey Google») и активации устройства.
  • Голосовое управление: в автомобилях или на промышленных объектах KWS может запускать конкретные команды, например «Запустить двигатель» или «Выключить свет».
  • Системы безопасности: системы безопасности с голосовым доступом могут использовать KWS для аутентификации пользователей по произнесённой парольной фразе.
  • Телекоммуникационные услуги: линии службы поддержки могут использовать KWS для маршрутизации звонков по произнесённым ключевым словам.

Отличия от общего распознавания речи

  • Вычислительная эффективность: KWS обычно требует меньше вычислений, чем полное распознавание речи, поскольку нужно распознавать лишь небольшой набор фраз.
  • Обработка в реальном времени: KWS часто работает в реальном времени и оптимизировано для обнаружения ключевых слов с малой задержкой.
  • Ограничения ресурсов: модели KWS часто делают лёгкими, чтобы они могли работать на устройствах с ограниченными вычислительными ресурсами, например на микроконтроллерах или мобильных телефонах.
  • Узкая задача: модели общего распознавания речи обучаются работать с широким словарём и разными акцентами, а модели KWS настраиваются на распознавание конкретных ключевых слов, часто точно и в шумной обстановке.

Обзор аудиосигналов

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

  • Частота и амплитуда: частота — это количество колебаний волны за единицу времени, измеряется в герцах (Гц). Для аудиосигналов разные частоты соответствуют разной высоте тона. Амплитуда, в свою очередь, измеряет величину колебаний и связана с громкостью звука. И частота, и амплитуда — важные признаки, которые отражают тональные и ритмические свойства звука.

  • Частота дискретизации: частота дискретизации, которая обычно обозначается в герцах (Гц), определяет количество отсчётов, взятых за секунду при оцифровке аналогового сигнала. Чем выше частота дискретизации, тем точнее цифровое представление сигнала, но тем больше вычислительных ресурсов требуется для обработки. Типичные частоты дискретизации — 44,1 кГц для аудио в качестве CD и 16 кГц или 8 кГц для задач распознавания речи. Понимание компромиссов при выборе подходящей частоты дискретизации важно для баланса точности и вычислительной эффективности. В целом в проектах TinyML мы работаем с частотой 16 кГц. Хотя тоны музыки слышны на частотах до 20 кГц, голос ограничен 8 кГц. Традиционные телефонные системы используют частоту дискретизации 8 кГц.

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

  • Временная и частотная области: аудиосигналы можно анализировать во временной и в частотной областях. Во временной области сигнал представлен как волна, где амплитуда отложена по времени. Такое представление помогает увидеть временные признаки, например начало и длительность звука, но тональные характеристики в нём выражены слабо. Напротив, представление в частотной области показывает составляющие сигнала и их амплитуды, обычно получаемые с помощью преобразования Фурье. Это незаменимо для задач, где нужно понимать спектральное содержание сигнала, например для определения музыкальных нот или речевых фонем (как в нашем случае).

На изображении ниже показаны слова YES и NO с типичными представлениями во временной (исходное аудио) и частотной областях:

Слова YES и NO во временной (исходный звук) и частотной областях

Почему не сырое аудио?

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

Применение сырого аудио для KWS, например на устройствах TinyML, сопряжено с трудностями: высокая размерность (при частоте дискретизации 16 кГц), вычислительная сложность при выделении временных признаков, чувствительность к шуму и отсутствие семантически осмысленных признаков. Поэтому для приложений с ограниченными ресурсами более практичным выбором становятся методы извлечения признаков, такие как MFCC.

Вот некоторые дополнительные подробности о ключевых проблемах, связанных с сырым аудио:

  • Высокая размерность: аудиосигналы, особенно при высокой частоте дискретизации, дают большие объёмы данных. Например, односекундный фрагмент, записанный с частотой 16 кГц, содержит 16 000 отдельных точек данных. Высокая размерность увеличивает вычислительную сложность, время обучения и затраты на вычисления, что делает подход непрактичным в условиях ограниченных ресурсов. Кроме того, широкий динамический диапазон аудиосигналов требует много бит на отсчёт, при этом полезной информации передаётся мало.

  • Временные зависимости: у сырых аудиосигналов есть временная структура, которую простым моделям машинного обучения бывает трудно уловить. Рекуррентные нейронные сети, например LSTM, способны моделировать такие зависимости, но они требовательны к вычислениям и трудны в обучении на крошечных устройствах.

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

  • Отсутствие семантического смысла: сырое аудио по своей природе не содержит семантически осмысленных признаков для задач классификации. Такие признаки, как высота тона, темп и спектральные характеристики, которые важны для распознавания речи, нельзя напрямую получить из формы сигнала.

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

Поэтому для преобразования сырых аудиоданных в более удобный и информативный формат обычно используют методы извлечения признаков: кепстральные коэффициенты на мел-шкале (MFCC), мел-энергии (Mel-Frequency Energies, MFE) и простые спектрограммы. Эти признаки сохраняют главные характеристики аудиосигнала, уменьшая размерность и шум, и делают машинное обучение эффективнее.

Обзор MFCC

Что такое MFCC?

Кепстральные коэффициенты на мел-шкале (MFCC) — набор признаков, которые вычисляются по спектральному содержанию аудиосигнала. Они основаны на особенностях человеческого слуха и часто используются для описания фонетических характеристик звука. MFCC вычисляются в несколько этапов: предыскажение (pre-emphasis), разбиение на кадры (framing), оконная функция (windowing), быстрое преобразование Фурье (FFT) для перехода в частотную область и, наконец, дискретное косинусное преобразование (DCT). В результате получается компактное представление спектральных характеристик исходного аудиосигнала.

На изображении ниже слова YES и NO показаны в виде MFCC:

Слова YES и NO в представлении MFCC

В этом видео объясняется, что такое кепстральные коэффициенты на мел-шкале (MFCC), и показано, как их вычислять.

Зачем нужны MFCC?

MFCC важны по нескольким причинам, особенно в контексте распознавания ключевых слов (KWS) и TinyML:

  • Снижение размерности: MFCC сохраняют важные спектральные характеристики сигнала и заметно уменьшают размерность данных, что делает их идеальными для приложений TinyML с ограниченными ресурсами.
  • Устойчивость: MFCC меньше подвержены шуму и вариациям высоты тона и амплитуды, предоставляя более стабильный и надёжный набор признаков для задач классификации звука.
  • Моделирование слуха человека: мел-шкала в MFCC приближает реакцию человеческого уха на разные частоты, что делает их практичными для распознавания речи, где желательно восприятие, близкое к человеческому.
  • Вычислительная эффективность: расчёт MFCC эффективен с точки зрения вычислений, что делает их хорошо подходящими для работы в реальном времени на аппаратуре с ограниченными ресурсами.

Итак, MFCC сочетают информативность и вычислительную эффективность, поэтому они популярны в задачах классификации звука, особенно в условиях ограничений, как в TinyML.

Вычисление MFCC

Вычисление кепстральных коэффициентов на мел-шкале (MFCC) включает несколько ключевых шагов. Разберём их, так как они особенно важны для задач распознавания ключевых слов (KWS) на устройствах TinyML.

  • Предыскажение (pre-emphasis): первый шаг — предыскажение, которое усиливает высокочастотные составляющие аудиосигнала и выравнивает частотный спектр. Для этого применяется фильтр, усиливающий разницу между соседними отсчётами. Формула предыскажения: , где — коэффициент предыскажения, обычно около 0,97.

  • Разбиение на кадры (framing): аудиосигнал делится на короткие кадры (длина кадра), обычно 20–40 миллисекунд. Это основано на допущении, что частоты в сигнале стационарны на коротком промежутке времени. Разбиение на кадры позволяет анализировать сигнал в таких небольших временных интервалах. Шаг кадра (frame stride) определяет смещение от одного кадра к соседнему. Кадры могут идти последовательно или перекрываться.

  • Оконная функция (windowing): каждый кадр затем умножается на оконную функцию, чтобы уменьшить разрывы на границах кадра. Часто используется окно Хэмминга. Оконная обработка готовит сигнал к преобразованию Фурье, уменьшая краевые эффекты. На изображении ниже показаны три кадра (10, 20 и 30) и отсчёты времени после оконной обработки (длина кадра и шаг кадра составляют 20 мс):

Три кадра аудиосигнала после оконной обработки

  • Быстрое преобразование Фурье (FFT): к каждому кадру после оконной обработки применяется FFT, которое переводит его из временной области в частотную. FFT даёт комплексное представление, содержащее и амплитуду, и фазу. Однако для MFCC используется только амплитуда, чтобы рассчитать спектр мощности (power spectrum). Спектр мощности — это квадрат амплитудного спектра, он показывает энергию, присутствующую в каждой частотной составляющей.

Спектр мощности сигнала определяется как , где — преобразование Фурье . Возведение модуля преобразования Фурье в квадрат усиливает сильные частоты по сравнению со слабыми и тем самым лучше передаёт значимые спектральные характеристики аудиосигнала. Это важно в таких задачах, как классификация звука, распознавание речи и KWS, где нужно выделять различимые частотные паттерны, которые отличают классы звуков или фонемы речи.

Переход от кадра к спектру FFT

  • Банк мел-фильтров (Mel Filter Banks): частотная область затем отображается на мел-шкалу, которая приближает реакцию человеческого уха на разные частоты. Идея в том, чтобы извлекать больше признаков (больше фильтров) на низких частотах и меньше на высоких. Поэтому метод хорошо работает со звуками, которые различает человеческое ухо. Обычно для вычисления мел-энергий используют от 20 до 40 треугольных фильтров. Затем эти энергии логарифмируются, чтобы превратить мультипликативные факторы в аддитивные, что удобнее для дальнейшей обработки.

Банк мел-фильтров

  • Дискретное косинусное преобразование (DCT): последний шаг — применить дискретное косинусное преобразование (DCT) к логарифмам мел-энергий. DCT помогает декоррелировать энергии, эффективно сжимая данные и оставляя только наиболее информативные признаки. Обычно сохраняют первые 12–13 коэффициентов DCT, которые образуют итоговый вектор признаков MFCC.

Итоговые коэффициенты MFCC

Практика на Python

Применим рассмотренное на реальном аудиообразце. Откройте блокнот в Google Colab и извлеките признаки MFCC из ваших аудиофайлов: Открыть в Colab

Итоги

Какой метод извлечения признаков использовать?

Кепстральные коэффициенты на мел-шкале (MFCC), мел-энергии (MFE) и спектрограммы — способы представления аудиоданных, которые часто полезны в разных контекстах.

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

MFCC особенно хороши для

  1. Распознавания речи: MFCC отлично подходят для выявления фонетического содержания речевых сигналов.
  2. Идентификации диктора: с их помощью можно отличать разных говорящих по особенностям голоса.
  3. Распознавания эмоций: MFCC способны передавать тонкие изменения речи, которые указывают на эмоциональное состояние.
  4. Распознавания ключевых слов: особенно в TinyML, где важны низкая вычислительная сложность и небольшой размер признаков.

Спектрограммы или MFE чаще подходят для

  1. Анализа музыки: спектрограммы передают гармоническую и тембровую структуру музыки, что важно для классификации жанров, распознавания инструментов или транскрипции музыки.
  2. Классификации окружающих звуков: при распознавании неречевых звуков (например, дождя, ветра, транспорта) полная спектрограмма даёт более информативные признаки.
  3. Идентификации птичьего пения: тонкие детали птичьих голосов часто лучше передаются спектрограммами.
  4. Обработки биоакустических сигналов: для анализа звуков дельфинов или летучих мышей тонкая частотная информация спектрограммы может быть критичной.
  5. Контроля качества аудио: спектрограммы часто используются в профессиональном анализе звука, чтобы выявлять нежелательные шумы, щелчки и другие артефакты.

Ресурсы

Уведомление об авторстве

Аппаратные наборы — русский перевод раздела «Hardware Kits» сайта учебника «Machine Learning Systems» (Harvard University).

  • Оригинальный текст: Vijay Janapa Reddi, Marcelo Rovai и соавторы, Harvard University, mlsysbook.ai/kits. Исходный репозиторий: harvard-edge/cs249r_book, папка kits/.
  • Лицензия оригинала: CC BY-NC-SA 4.0. Эта книга распространяется на тех же условиях: с указанием авторства, только в некоммерческих целях и на условиях той же лицензии. Поэтому она не публикуется по общей лицензии CC BY 4.0 репозитория, см. meta.toml.
  • Перевод и редакция: Сергей Монин.

Переведён весь раздел Hardware Kits: главная страница, «Начало работы», «Платформы», настройка среды разработки, лабораторные работы по Arduino Nicla Vision, Seeed XIAOML Kit, Grove Vision AI V2, Raspberry Pi и общие лабораторные работы. Переведены заголовки, текст, таблицы, подписи к рисункам и таблицам, врезки, тексты ссылок, подписи в схемах и комментарии в примерах кода. Формулы, числа, цены и названия продуктов сохранены как в оригинале. Код, команды и выводы программ остаются на английском языке. Что изменено при переносе в mdBook, какие схемы перерисованы и что осталось на языке оригинала, описано в файле PROVENANCE.md (в этой же папке).

Происхождение материалов

Источник

Материал основан на разделе Hardware Kits (папка kits/) репозитория harvard-edge/cs249r_book, сайт mlsysbook.ai/kits. Лицензия оригинала — CC BY-NC-SA 4.0 (файл kits/LICENSE).

Перевод выполнил Сергей Монин.

Что переведено

  • Главная страница (index.qmd), включая блок «Часть экосистемы MLSysBook» (shared/config/_ecosystem-cards.qmd). Блок переведён списком; интерактивная отметка «Вы здесь» не переносится, так как в mdBook нет скриптов сайта.
  • «Начало работы» (getting-started.qmd), «Платформы» (platforms.qmd), «Настройка среды разработки» (ide-setup.qmd).
  • Arduino Nicla Vision (arduino/nicla_vision/): обзор, настройка, классификация изображений, распознавание ключевых слов, классификация движения и обнаружение аномалий, обнаружение объектов.
  • Seeed XIAOML Kit, XIAO ESP32S3 (seeed/xiao_esp32s3/): обзор, настройка, классификация изображений, распознавание ключевых слов, классификация движения, обнаружение объектов.
  • Grove Vision AI V2 (seeed/grove_vision_ai_v2/): обзор, настройка и приложения без кода, классификация изображений, обнаружение объектов (в оригинале страница помечена «в разработке»).
  • Raspberry Pi (raspi/): обзор, настройка (установка ОС, SSH, SWAP, камера, удалённый рабочий стол VNC, обновление системы, особенности моделей Zero и 4/5), классификация изображений, обнаружение объектов, малые языковые модели (SLM, Ollama), визуально-языковые модели (VLM, Florence-2).
  • Общие лабораторные работы (shared/): обзор, инженерия признаков для распознавания ключевых слов, блок спектральных признаков DSP.
  • Текст, заголовки, таблицы, подписи к таблицам и рисункам, врезки, примечания, тексты ссылок и alt-тексты изображений.
  • Комментарии в примерах кода переведены там, где это пояснения на естественном языке. Имена функций, переменных и библиотек и команды оставлены как в оригинале. Строки вывода (Serial.println, ei_printf, print, подписи графиков matplotlib) в части лабораторных работ (XIAOML Kit — классификация движения, Grove Vision AI V2 — классификация изображений, общая лабораторная по спектральным признакам DSP) переведены на русский; в остальных они оставлены на английском, как в оригинале и на скриншотах.

Схемы

Схемы переведены двумя способами.

  • Схема архитектуры Florence-2 (в оригинале TikZ, который mdBook не отображает) перерисована как SVG с русскими подписями: 05-raspberry-pi/vlm/images/svg/florence2-architecture.svg.
  • Самостоятельные схемы из курса перерисованы как SVG с русскими подписями, без логотипов и без скриншотов:
    • 05-raspberry-pi/llm/images/svg/: llm-block-fc-proj.svg, llm-block-image.svg, llm-rag-scheme-1.svg, llm-rag-scheme-2.svg;
    • 05-raspberry-pi/object_detection/images/svg/: od-pipeline.svg, od-model-deploy.svg;
    • 04-grove-vision/setup_and_no_code_apps/images/svg/grove-person-detection.svg;
    • 04-grove-vision/image_classification/images/svg/: grove-ml-pipeline.svg, grove-inference-leds.svg;
    • 03-seeed-xiao/image_classification/images/svg/xiao-pipeline.svg.

Исходные растровые файлы этих схем остаются в папках images/png/ и images/jpeg/ рядом с главами, но в тексте они больше не используются.

Что изменено при переносе в mdBook

  • Таблицы Quarto (сеточные таблицы) переписаны в формат Markdown-таблиц. Содержимое ячеек не менялось.
  • Атрибуты Quarto удалены: {#sec-…} у заголовков, {.unnumbered}, {width=…}, {fig-align=…}, {.striped .hover}. Подпись таблицы вынесена в курсивную строку под таблицей.
  • Команды LaTeX и служебные маркеры Quarto удалены: \noindent, \clearpage, \part{…}, маркеры викторин ::: {.quiz-end}.
  • Врезки Quarto (::: callout-…) переписаны как цитаты с жирным заголовком.
  • Рисунки Quarto заменены HTML-блоками <img> с альтернативным текстом и подписью. Версии, предназначенные только для PDF (content-visible when-format="pdf"), удалены; для HTML сохранена версия для веб-страниц.
  • Видео Quarto ({{< video … >}}) заменены ссылками на YouTube. Подключение общего блока ({{< include … >}}) заменено его текстом.
  • Перекрёстные ссылки Quarto (@sec-…, @fig-…, @tbl-…) заменены текстом или ссылками на переведённые страницы книги. Все внутренние ссылки проверены скриптом scripts/check_links.py.
  • Таблица оглавления SUMMARY.md создана скриптом scripts/make_summary.py. Для названий глав книги в скрипте добавлены подписи (CHAPTER_TITLES).
  • Изображения скопированы в папку images/ каждой главы. Подписи и альтернативный текст переведены.

Исправления в примерах кода и опечатки оригинала

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

  • Импорт from flask import … без скобок переносит строку с ошибкой синтаксиса: добавлены скобки.
  • Строка mimetype=…, разорванная переносом внутри строкового литерала: собрана в одну строку.
  • Присваивание url = "…" "…" без скобок: добавлены скобки, иначе вторая часть строки терялась.
  • Пути к моделям, разорванные обратным слэшем внутри строки (в них попадали пробелы): собраны в одну строку.
  • except Queue.Empty заменено на except Empty с импортом из queue.
  • В примере LLM вместо response_2 и res использованы правильные переменные.
  • В вызове ollama.generate добавлена пропущенная запятая перед options=.
  • В функции rag_bees удалён дублирующий print, стоявший вне функции.
  • Команда sudo apt-get installlibportaudiocpp0 исправлена на sudo apt-get install libportaudiocpp0 portaudio19-dev.
  • В команде обучения YOLO пропущен обратный слэш в строке epochs={EPOCHS}; добавлен.
  • cd /Documents/YOLO заменено на cd ~/Documents/YOLO.
  • В примере Florence-2 < MORE_DETAILED_CAPTION> и <CAPTION,> приведены к виду <MORE_DETAILED_CAPTION> и <CAPTION>.
  • В JavaScript-примере однострочный комментарий, разорванный переносом, собран в одну строку.
  • Время обработки запроса в примере Ollama: в тексте оригинала указано «около 16 наносекунд», в самом выводе — 1 908 757 000 нс (около 1,9 с). Указано значение из вывода.
  • В примере вызова функций модель указана как phi3.5:3.8b (как в коде), а не llava-phi3, как было в тексте оригинала.
  • Пустая ссылка на скрипт calc_distance_image.py заменена ссылкой на репозиторий EdgeML-with-Raspberry-Pi.
  • Опечатки исправлены: «Florense-2», «Movilinet», «Jupiter Notebook», «CRTL+X», «witch», «to detect bees» (в примере о коробках и колёсах) и «from 0 to 256» (исправлено на 0–255).
  • В главе «Обнаружение объектов» заголовок проекта «Image Classification project» исправлен на «проект обнаружения объектов».

Дополнительные исправления после вычитки

Код:

  • While True: → while True:; в скетче со светодиодами (Nicla Vision, классификация изображений) типографские апострофы 'uncertain’ заменены на обычные, а лишний отступ перед while True: убран.
  • В коде C++ для Grove Vision AI V2 комментарии # заменены на //.
  • Строковые константы, разорванные переносом внутри ei_printf(...) и Serial.printf(...) (Nicla Vision KWS, XIAO KWS), собраны из соседних литералов.
  • sudo shutdown -h no → sudo shutdown -h now; ls -f → ls -l (длинный формат); в sudo apt-get install libatlas-base-dev\ добавлен пробел перед обратным слэшем.
  • Имя модели в примере FOMO согласовано с командой переименования (...-FOMO-int8.eim); /arduinor_rec.wav → /arduino_rec.wav; plt.title('...Welch's...') → строка в двойных кавычках; {{ в выводе Florence-2 → {; ключ <REGION_TO_DESCRIPTION> в выводе задачи «регион в описание».
  • Пути и команды с именем автора (mjrovai) заменены на username.
  • Опечатки в именах: helloword.py → helloworld.py, HelloWord → HelloWorld, WiFI/WiFIScan/SimpleWiFIServer → WiFi/WiFiScan/SimpleWiFiServer, «Hello Word» → «Hello World», LoRaWan → LoRaWAN, SSMA → SSCMA, YOLOV8 → YOLOv8, «German Sheppard» → «German Shepherd», «Marvin Prison» → «Mervin Praison».

Числа и факты:

  • Год выпуска MobileNetV1: в оригинале указан 2007, в переводе — 2017 (во всех главах про классификацию изображений).
  • Число признаков для изображения : 27 640 → 27 648.
  • Пиковый ток 14 мА при 5,23 В → 140 мА (при 730 мВт, XIAO) и 14 мА → 140 мА (0,7 Вт, Grove Vision AI V2, внешние светодиоды).
  • Выводы I2S-микрофона XIAO: «IO41 (тактовый сигнал) и IO41 (данные)» → «IO42 (тактовый сигнал) и IO41 (данные)».
  • Вывод top_k_indices: пример с тремя индексами заменён пятью (283, 286, 282, 288, 479); размер входа 92×92 → 96×96.
  • В итогах лабораторной по классификации изображений на Grove Vision AI V2: «архитектура Edge TPU» → «блок NPU Ethos-U55» (в модуле нет Edge TPU).
  • Копипаст в оригинале: в XIAO «Обнаружение объектов» классы «коробка/колесо» заменены на «фрукт/жук»; в Grove «Когда XIAO подключён» заменено на Grove Vision AI (V2); в Grove повторявшийся «Шаг 3» перенумерован.

Ссылки и вёрстка:

  • Ссылка на лабораторную Grove Vision AI V2 заменена относительной; в URL Colab-блокнота добавлено %20.
  • Закомментированный рисунок iic.png в XIAO «Классификация изображений» возвращён в текст; удалены два служебных HTML-комментария, оставшихся после переноса из Quarto.
  • Заключённые в <em> подписи к DALL·E больше не показывают литеральные звёздочки; цитаты после рисунков отделены пустой строкой, чтобы отображаться как цитаты.

Терминология (единообразно по книге и по глоссариям репозитория): VLM — «визуально-языковая модель»; wake word — «слово пробуждения»; fine-tuning — «дообучение»; no-code — «без кода»; SD card — «SD-карта»; Boards/Library Manager — «менеджер плат/библиотек»; power spectrum — «спектр мощности»; Impulse (Edge Impulse) не переводится. Десятичные дроби в тексте записаны через запятую; точка оставлена в версиях, идентификаторах моделей и значениях из интерфейсов и кода.

Что оставлено на языке оригинала

  • Названия продуктов, компаний, библиотек и моделей: Grove Vision AI V2, XIAOML Kit, Nicla Vision, Raspberry Pi, Arduino IDE, SenseCraft AI, Edge Impulse, Roboflow, Ultralytics, Ollama, Hugging Face, Llama, Gemma, Phi, LLaVA, Florence-2, MobileNet, EfficientDet, YOLOv8, FOMO, TFLite, OpenCV, PyTorch, TensorFlow.
  • Обозначения компонентов: ESP32-S3, XIAO ESP32S3 Sense, OV2640, MP34DT05, GC2145, STM32H747, Ethos-U55, PSRAM, QSPI, microSD, LSM6DS3TR-C, LSM6DSOX.
  • Метрики и обозначения: mAP, IoU, NMS, FPS, CIDEr, recall@1, F1, int8, float32, Q4_0.
  • Код, команды, имена файлов и переменных и подписи в веб-интерфейсах (например, Flask-страницы). Строки вывода программ оставлены на английском, кроме исключений, перечисленных выше.
  • Промпты для моделей и ответы моделей (например, What is the capital of France?). Модели обучены на английских запросах, поэтому они не переводились.
  • Цены в долларах (ориентировочные, как в оригинале).
  • Символы ✓ в таблицах доступности и возможностей.
  • Китайский иероглиф «小» в объяснении названия «XIAO»: рядом приведён перевод («крошечный»).
  • Скриншоты интерфейсов сторонних программ (Edge Impulse Studio, Arduino IDE, SenseCraft AI, Roboflow, Raspberry Pi Imager, Ollama, htop и другие): текст на них остаётся на английском. Каждый рисунок имеет русскую подпись и альтернативный текст.
  • Фотографии плат, устройств и изображения-иллюстрации: оставлены без изменений.
  • Рисунки из научных статей и внешних публикаций (архитектура Florence-2 и распределение набора FLD-5B, сравнение LLM и SLM, графики бенчмарков). Их не перерисовывали: это чужие иллюстрации. У них русская подпись, исходный текст на рисунке остаётся английским.
  • Составные схемы с фотографиями и логотипами: kws_diagram.jpg (распознавание ключевых слов), objxclas.jpg (классификация и обнаружение объектов, на схеме фотографии яблока, апельсина и банана). Текст на них оставлен на английском; русские подписи и пояснения даны в тексте главы.

Что не перерисовано

  • Растровые схемы, которые не перерисованы в SVG, в основном являются скриншотами программ и составными иллюстрациями, описанными выше. Часть растровых схем (например, spectral_block.jpeg, pa_block.jpg, impulse-block.jpg, model_1.jpg, model_2.jpg) при переносе не просматривалась по отдельности. Их текст нужно проверить и при необходимости перерисовать.

Что требует внимания

  • Цены и версии программного обеспечения (Raspberry Pi OS, Python 3.11, версии библиотек и моделей) ориентировочные. Перед работой проверьте их актуальность.
  • Ссылки на блокноты и скрипты в репозитории авторов (GitHub), на страницы Edge Impulse, Hugging Face и YouTube указаны по оригиналу. Их доступность не проверялась.
  • Правописание в этой книге с буквой «ё» («всё», «ещё», «развёртывание», «объём»).
  • Расхождение, унаследованное от оригинала: в лабораторной по классификации изображений на Nicla Vision в коде светодиодов порог «неопределённого» класса равен 0,5, а в тексте говорится о 0,8.
  • В примере RAG (Raspberry Pi, малые языковые модели) документы и запросы переведены на русский, а показанные результаты получены на английском наборе. Модель эмбеддингов nomic-embed-text ориентирована на английский язык, поэтому на русском корпусе качество поиска может отличаться.