
Создание простой нейронной сети на микроконтроллере
Данная статья является переводом публикации «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.
Рис. 1. Процесс разработки ПО для микроконтроллеров с помощью uTensor
В данном руководстве рассматриваются следующие вопросы:
- установка необходимых программных средств,
- методика обучения нейронной сети,
- порядок генерации файлов C ++,
- алгоритм настройки проектов Mbed.
Хотя эти инструкции предназначены для Mac OS, они применимы к другим операционным системам.
Необходимые инструменты
Для работы нам понадобятся следующие программные и аппаратные инструменты:
- Mbed Command Line Tool (Mbed-cli);
- uTensor-cli (компилятор, преобразующий граф в исходники С++);
- TensorFlow (поставляется с uTensor-cli);
- Mbed board (оценочные платы в каталоге или Mbed Simulator);
- CoolTerm (для работы с последовательным портом);
- VS Code (опционально).
Установка программных средств разработки и отладки
Инструменты разработки и отладки позволяют компилировать проекты МК с помощью выбранных 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).
Рис. 2. Изображение цифры 7
Файл входных тестовых данных уже готов. Его нужно скачать и поместить в корень проекта:
На этом все. Мы вернемся к этим файлам позже.
Обучение нейронной сети
Для простоты мы обучим многослойный персептрон (MLP) распознаванию рукописных цифр MNIST. Архитектура сети показана на рис. 3. Система принимает входные данные в виде монохромных изображений рукописных цифр размером 28 x 28 пикселов, и преобразует их в линейные пачки длинной 784 бита. Сеть состоит из следующих слоев:
- 1 входной слой;
- 2 скрытых слоя (128 и 64 скрытых узла соответственно) с функциями активации ReLu (rectified linear unit);
- 1 выходной слой с softmax.
Рис. 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.
Рис. 4. Программирование в Mbed сводится к перетаскиванию файлов между окнами
Далее следует выполнить следующую последовательность действий:
- Подключите вашу плату;
- Найдите двоичный файл в ./BUILD/YOUR_TARGET_NAME/GCC_ARM / my_uTensor.bin;
- Перетащите его в точку монтирования Mbed DAPLink (показано на рис. 4);
- Дождитесь завершения передачи.
Получение результата
По умолчанию функции вывода в Mbed, printf () ориентированы на терминал последовательного порта. Интерфейс DAP-Link позволяет нам просматривать данные, передаваемые по USB. Для этого мы будем использовать CoolTerm (рис. 5).
Рис. 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