Москва

+7 (495) 488-65-70

Корзина пуста
Загрузка списка товаров из файла
SALE!-10% -20% -30% -40% -50% -60%
дефицитные компоненты
Создание простой нейронной сети на микроконтроллере

Создание простой нейронной сети на микроконтроллере

Данная статья является переводом публикации «Simple Neural Network on MCUs», посвященной реализации нейронной сети на микроконтроллерах. В статье описывается процесс установки необходимых программных средств, алгоритм обучения нейронной сети, порядок генерации файлов C++, особенности настройки проектов Mbed.

В настоящее время использование технологии Граничных вычислений (Edge computing) подтверждает правило «золотого молотка», согласно которому человек с молотком в руках во всем видит только гвозди. В предыдущем посте «Why Machine Learning on the Edge» я писал о важности машинного обучения для граничных вычислений. Пит Уорден в статье «Why The Future of Machine Learning is Tiny» также полагает, что будущее машинного обучения в первую очередь будет связано с миниатюрными процессорами и микроконтроллерами. В ближайшее время следует ожидать появления множества интересных технологий, которые обеспечат прорыв в этом направлении. В данной статье мы на конкретном примере рассмотрим, как можно развернуть нейронную сеть на микроконтроллере (МК) с помощью uTensor.

uTensor – это система, которая преобразует ML-модели в файлы C++, используемые в качестве исходников в проектах встраиваемого ПО для микроконтроллеров. Зачем генерировать исходники C ++? Потому что их удобно читать и редактировать. В этом руководстве мы будем использовать uTensor совместно с Mbed и TensorFlow. Процесс разработки условно показан на рис. 1.

Процесс разработки ПО для микроконтроллеров с помощью uTensor

Рис. 1. Процесс разработки ПО для микроконтроллеров с помощью uTensor

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

  • установка необходимых программных средств,
  • методика обучения нейронной сети,
  • порядок генерации файлов C ++,
  • алгоритм настройки проектов Mbed.

Хотя эти инструкции предназначены для Mac OS, они применимы к другим операционным системам.

Необходимые инструменты

Для работы нам понадобятся следующие программные и аппаратные инструменты:

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

Инструменты разработки и отладки позволяют компилировать проекты МК с помощью выбранных IDE/ редакторов в Windows/ Mac/ Linux. Brew удобен для установки пакетов программного обеспечения на ваш Mac. Работаем с терминалом:

$ /usr/bin/ruby -e “$(curl –fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"$ brew update

Устанавливаем рабочую версию кросс-компилятора GCC-arm:

$ brew install https://raw.githubusercontent.com/osx-cross/homebrew-arm/0a6179693c15d8573360c94cee8a60bdf142f7b4/arm-gcc-bin.rb

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

$ brew install python3

Для Mbed-cli понадобится Mercural и Git:

$ brew install mercurial git

Устанавливаем Mbed-CLI. Проверьте актуальность информации:

$ pip3 install mbed-cli$ mbed –version1.8.3

Получение uTensor-cli:

$ pip3 install utensor_cgen

Наконец, скачиваем CoolTerm с веб-сайта Роджера Майера. 

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

Давайте начнем с создания нового проекта Mbed. Это может занять пару минут, поскольку будет загружена вся исходная история Mbed-OS.

$ mbed new my_uTensor$ cd my_uTensor/$ lsmbed-os          mbed-os.lib      mbed_settings.py# Note: You may need to use Mbed-OS 5.11# Please see https://github.com/uTensor/uTensor/issues/155 

Нам понадобится библиотека uTensor runtime library. Она содержит реализации функций, которые будут использованы при компиляции.

$ mbed add https://github.com/uTensor/uTensor$ lsmbed-os          mbed_settings.py uTensor.libmbed-os.lib      uTensor

Наконец, чтобы проверить правильность работы нейронной сети, нам потребуются входные данные. Для демонстрации мы будем использовать сгенерированный заголовочный файл, который содержит изображение рукописной цифры 7 (рис. 2).

Изображение цифры 7

Рис. 2. Изображение цифры 7

Файл входных тестовых данных уже готов. Его нужно скачать и поместить в корень проекта:

$ wget https://gist.github.com/neil-tan/0e032be578181ec0e3d9a47e1e24d011/raw/888d098683318d030b3c4f6f4b375a64e7ad0017/input_data.h 

На этом все. Мы вернемся к этим файлам позже. 

Обучение нейронной сети

Для простоты мы обучим многослойный персептрон (MLP) распознаванию рукописных цифр MNIST. Архитектура сети показана на рис. 3. Система принимает входные данные в виде монохромных изображений рукописных цифр размером 28 x 28 пикселов, и преобразует их в линейные пачки длинной 784 бита. Сеть состоит из следующих слоев:

  • 1 входной слой;
  • 2 скрытых слоя (128 и 64 скрытых узла соответственно) с функциями активации ReLu (rectified linear unit);
  • 1 выходной слой с softmax.

Архитектура нейронной сети mxnet Handwritten Digit Recognition

Рис. 3. Архитектура нейронной сети mxnet Handwritten Digit Recognition

Jupyter-Notebook содержит код, но вы можете использовать старый-добрый файл Python: deep_mlp.py.

In [ ]:

# Данный :скрипт основан на

# https://www.tensorflow.org/get_started/mnist/pros

import sys

import tensorflow as tf

from tensorflow.examples.tutorials.mnist import input_data

from tensorflow.python.framework import graph_util as gu

from tensorflow.python.framework.graph_util import remove_training_nodes

from tensorflow.tools.graph_transforms import TransformGraph

Импортирование данных

In [ ]:

mnist = input_data.read_data_sets("mnist_data/", one_hot=True)

Определение модели Tensorflow

In [ ]:

batch_size = 50

Полное подключение 2-слойной нейронной сети

In [ ]:

def deepnn(x):

  W_fc1 = weight_variable([784, 128], name='W_fc1')

  b_fc1 = bias_variable([128], name='b_fc1')

  a_fc1 = tf.add(tf.matmul(x, W_fc1), b_fc1, name="zscore")

  h_fc1 = tf.nn.relu(a_fc1)

 

  W_fc2 = weight_variable([128, 64], name='W_fc2')

  b_fc2 = bias_variable([64], name='b_fc2')

  a_fc2 = tf.add(tf.matmul(h_fc1, W_fc2), b_fc2, name="zscore")

  h_fc2 = tf.nn.relu(a_fc2)

 

  W_fc3 = weight_variable([64, 10], name='W_fc3')

  b_fc3 = bias_variable([10], name='b_fc3')

  logits = tf.add(tf.matmul(h_fc2, W_fc3), b_fc3, name="logits")

  y_pred = tf.argmax(logits, 1, name='y_pred')

 

  return y_pred, logits

 

def weight_variable(shape, name):

  """weight_variable generates a weight variable of a given shape."""

  initial = tf.truncated_normal(shape, stddev=0.1)

  return tf.Variable(initial, name)

 

def bias_variable(shape, name):

  """bias_variable generates a bias variable of a given shape."""

  initial = tf.constant(0.1, shape=shape)

  return tf.Variable(initial, name)

Задаем входы выходы и весовые функции

In [ ]:

# Reset default graph

tf.reset_default_graph()

 

# Create the model

x = tf.placeholder(tf.float32, [None, 784], name="x")

 

# Define loss and optimizer

y_ = tf.placeholder(tf.float32, [None, 10], name="y")

 

# Build the graph for the deep net

y_pred, logits = deepnn(x)

 

with tf.name_scope("Loss"):

    cross_entropy = tf.nn.softmax_cross_entropy_with_logits_v2(labels=y_,

                                                               logits=logits)

    loss = tf.reduce_mean(cross_entropy, name="cross_entropy_loss")

train_step = tf.train.AdamOptimizer(1e-4).minimize(loss, name="train_step")

 

# Here we specify the output as "Prediction/y_pred", this will be important later

with tf.name_scope("Prediction"):

    correct_prediction = tf.equal(y_pred,

                                  tf.argmax(y_, 1))

    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32), name="accuracy")

 

Обучаем модель

In [ ]:

sess = tf.Session()

# Initialize the variables (i.e. assign their default value)

sess.run(tf.global_variables_initializer())

saver = tf.train.Saver()

 

for i in range(1, 20001):

  batch_images, batch_labels = mnist.train.next_batch(batch_size)

  feed_dict = {x: batch_images, y_: batch_labels}

  if i % 1000 == 0:

    train_accuracy = sess.run(accuracy, feed_dict=feed_dict)

    print('step %d, training accuracy %g' % (i, train_accuracy))

  sess.run(train_step, feed_dict=feed_dict)

Какова конечная точность?

In [ ]:

print('test accuracy %g' % sess.run(accuracy, feed_dict={x: mnist.test.images, y_: mnist.test.labels}))

Зафиксируем граф

In [ ]:

saver.save(sess, "./chkps/mnist_model")

out_nodes = [y_pred.op.name]

print(out_nodes)

Убираем неиспользуемые узлы

In [ ]:

sub_graph_def = remove_training_nodes(sess.graph_def)

Фиксируем константы

In [ ]:

sub_graph_def = gu.convert_variables_to_constants(sess, sub_graph_def, out_nodes)

Сохраняем граф константы

In [ ]:

graph_path = tf.train.write_graph(sub_graph_def,

                                  "./mnist_model",

                                  "deep_mlp.pb",

                                  as_text=False)

 

print('written graph to: %s' % graph_path)

In [ ]:

# close session

sess.close()

In [ ]:

Скрипт определяет MLP и параметры обучения. Запустив скрипт, вы должны увидеть нечто вроде такого:

$ python3 deep_mlp.py
...
step 19000, training accuracy 0.92
step 20000, training accuracy 0.94
test accuracy 0.9274
saving checkpoint: chkps/mnist_model
Converted 6 variables to const ops.
written graph to: mnist_model/deep_mlp.pb
the output nodes: ['y_pred']

Буфер протокола, содержащий обученную модель, будет сохранен. Эти данные в виде файла deep_mlp.pb мы предоставим uTensor-cli для генерации кода C++ на следующем шаге.

$ ls mnist_model/
deep_mlp.pb

 Генерация файлов C++

Наконец, самое интересное. Преобразуем граф deep_mlp.pb в файлы C ++:

  • deep_mlp.cpp – содержит реализацию модели;
  • deep_mlp.hpp – интерфейс с программой. В нашем случае функция: get_deep_mlp_ctx (…). Позднее мы рассмотрим, как использовать эту функцию в main.cpp.
  • deep_mlp_weight.hpp– содержит весовые коэффициенты нейронной сети.

Команда для создания файла C ++ имеет вид:

$ utensor-cli convert mnist_model/deep_mlp.pb --output-nodes=y_pred
... Applying sort_by_execution_order
... Transforming graph: mnist_model/deep_mlp.pb
... Applying quantize_weights
... Applying quantize_nodes
... Applying sort_by_execution_order
... Graph transormation done
... Generate weight file: models/deep_mlp_weight.hpp
... Generate header file: models/deep_mlp.hpp
... Generate source file: models/deep_mlp.cpp

Указание выходного узла помогает uTensor-cli анализировать и оптимизировать граф. Имя выходного узла отображается в обучающем сообщении в предыдущем разделе. Это зависит от того, как настроена сеть.

Компиляция программы

На этом этапе у нас должен быть Mbed-проект, содержащий:

  • Mbed OS;
  • библиотеку uTensor;
  • реализацию графа в виде файлов C ++;
  • Заголовочный файл входных данных.

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

Листинг 1 – Файл main.cpp

#include "models/deep_mlp.hpp"  //сгенерированная модель

#include "tensor.hpp"  //полезные классы tensor

#include "mbed.h"

#include <stdio.h>

#include "input_data.h"  //содержит первое изображение, взятое из тестового набора MNIST

 

Serial pc(USBTX, USBRX, 115200);  //скорость := 115200 бит/с

 

int main(void) {

  printf("Simple MNIST end-to-end uTensor cli example (device)\n");

 

  Context ctx;  //создаем экземпляр класса context, на этом уровне происходит распознавание

,

  //сворачиваем входные данные в классе tensor

  Tensor* input_x = new WrappedRamTensor<float>({1, 784}, (float*) input_data);

 

  get_deep_mlp_ctx(ctx, input_x);  // передаем tensor в контекст

  S_TENSOR pred_tensor = ctx.get("y_pred:0");  // получаем данные с выхода

  ctx.eval(); //защелкиваем данные

 

  int pred_label = *(pred_tensor->read<int>(0, 0));  //возвращаем результат

  printf("Predicted label: %d\r\n", pred_label);

 

  return 0;

}

Класс Context – это основной компонент программы, в котором происходит распознавание изображения. Get_deep_mlp () является сгенерированной функцией. Он автоматически заполняет объект Context графом вывода и принимает в качестве входных данных класс Tensor. Работа класса Context, получившего граф вывода, можно оценить по выходному результату (tensor). Имя выходного экземпляра tensor совпадает с именем выходного узла, как указано в учебном скрипте.

В нашем примере статический массив, определенный в input_data.h, используется в качестве входных данных. На практике в качестве входных данных могут выступать изображения с реального датчика или массивы данных хранящихся в памяти МК. Данные располагаются в памяти линейно (так же, как любой массив C). В процессе обработки приложение должно обеспечить неприкосновенность входного блока памяти.

Теперь мы можем скомпилировать весь проект, выполнив:

$ mbed compile -m K66F -t GCC_ARM –profile=uTensor/build_profile/release.json

Для компиляции Mbed-cli должен знать целевую платформу. В нашем случае это K66F. Вы можете изменить этот ключ с учетом используемой вами отладочной платы. Мы также используем собственный профиль сборки, чтобы включить поддержку C ++ 11. Далее на экране должно появиться примерно такое сообщение:

…Compile [ 99.9%]: uTensor_util.cppCompile [100.0%]: quantization_utils.cppLink: my_uTensorElf2Bin: my_uTensor| Module             |           .text |       .data |          .bss
|--------------------|-----------------|-------------|--------------
| CMSIS_5/CMSIS      |         68(+68) |       0(+0) |         0(+0)
| [fill]             |       505(+505) |     11(+11) |       22(+22)
| [lib]/c.a          |   69431(+69431) | 2548(+2548) |     127(+127)
| [lib]/gcc.a        |     7456(+7456) |       0(+0) |         0(+0)
| [lib]/m.a          |       788(+788) |       0(+0) |         0(+0)
| [lib]/misc         |       248(+248) |       8(+8) |       28(+28)
| [lib]/nosys.a      |         32(+32) |       0(+0) |         0(+0)
| [lib]/stdc++.a     | 173167(+173167) |   141(+141) |   5676(+5676)
| main.o             |     4457(+4457) |       0(+0) |     105(+105)
| mbed-os/components |         16(+16) |       0(+0) |         0(+0)
| mbed-os/drivers    |       844(+844) |       0(+0) |         0(+0)
| mbed-os/hal        |     1472(+1472) |       4(+4) |       68(+68)
| mbed-os/platform   |     3494(+3494) |   260(+260) |     221(+221)
| mbed-os/rtos       |     8313(+8313) |   168(+168) |   6057(+6057)
| mbed-os/targets    |     7035(+7035) |     12(+12) |     301(+301)
| models/deep_mlp1.o | 148762(+148762) |       0(+0) |         1(+1)
| uTensor/uTensor    |     7995(+7995) |       0(+0) |       10(+10)
| Subtotals          | 434083(+434083) | 3152(+3152) | 12616(+12616)
Total Static RAM memory (data + bss): 15768(+15768) bytes
Total Flash memory (text + data): 437235(+437235) bytes Image: ./BUILD/K66F/GCC_ARM/my_uTensor.bin 

 

Программируем микроконтроллер

Обычно плата Mbed поставляется с USB-интерфейсом DAPLink. Этот инструмент существенно упрощает программирование, которое сводится к перетаскиванию файлов между окнами. После подключения платы Mbed вы должны увидеть на экране изображение, аналогичное рис. 4.

Программирование в Mbed сводится к перетаскиванию файлов между окнами

Рис. 4. Программирование в Mbed сводится к перетаскиванию файлов между окнами

Далее следует выполнить следующую последовательность действий:

  • Подключите вашу плату;
  • Найдите двоичный файл в ./BUILD/YOUR_TARGET_NAME/GCC_ARM / my_uTensor.bin;
  • Перетащите его в точку монтирования Mbed DAPLink (показано на рис. 4);
  • Дождитесь завершения передачи. 

Получение результата

По умолчанию функции вывода в Mbed, printf () ориентированы на терминал последовательного порта. Интерфейс DAP-Link позволяет нам просматривать данные, передаваемые по USB. Для этого мы будем использовать CoolTerm (рис. 5).

Настройка параметров терминала в CoolTerm

Рис. 5. Настройка параметров терминала в CoolTerm

Далее следует выполнить следующую последовательность действий:

  • Запустите CoolTerm;
  • Перейдите к параметрам (значок Options);
  • Нажмите на Re-Scan Serial Ports;
  • Выберите порт usbmodem1234 (название может меняться при каждом новом подключении платы);
  • Скорость передачи данных должна соответствовать скорости, определенной в файле main.cpp. В нашем случае 115200 бит/с;
  • Нажмите ОК;
  • Нажмите Connect.

Нажмите кнопку сброса на вашей отладочной плате. Вы должны увидеть следующее сообщение:

Simple MNIST end-to-end uTensor cli example (device)Predicted label: 7

Поздравляем! Вы успешно создали простую нейронную сеть на микроконтроллере! 

Заключение

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

  • Полностью связанные слои (MatMul & Add)
  • Сверточные слои
  • Пулинг;
  • ReLu
  • Softmax
  • Argmax
  • Dropout

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

 
Автор: Нил Тан Перевод: Вячеслав Гавриков, г. Смоленск

Опубликовано: 26.06.2019

  • Москва
  • Санкт-Петербург
  • Мурманск
  • Ульяновск
  • Новосибирск
  • Екатеринбург
  • Краснодар
  • Нижний Новгород
  • Воронеж
  • Уфа
  • Челябинск
  • Самара
  • Красноярск
  • Казань
  • Ростов-на-Дону
  • Саратов
  • Пермь
  • Томск
  • Иркутск
  • Омск
  • Тюмень

Актуальность предложений на товары в корзине истекла, данные были удалены 14.09.2025 в 00:00:00 (Мск.) Список позиций из корзины сохранен в Списке товаров
Актуальность предложений на товары в корзине истекла, данные были удалены 14.09.2025 в 00:00:00 (Мск.) Зарегистрируйтесь или авторизуйтесь на сайте, если регистрировались ранее, чтобы сохранять список товаров из корзины

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