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

Платформа робота состояла из компонетов: Arduinio, привода с колесами, ультрозвуковой дальномер HC-SR04, фоторезистор как датчик света, датчик звука.
При проектировании топологии сети было принято решение, что нейросеть будет иметь два слоя с 5-ю нейронами на каждом слое. Трехслойная сеть уже не умещалась в технические характеристики нашего микроконтроллера, но двух слоев оказалось достаточно для работы с теми датчиками, которые у меня имелись. Обучение сети должно было проводиться на компьютере с последующей загрузкой базы обученной сети в флешь память.
Из конструкции робота видно, что даже без использования нейронной сети, его возможности ограничены. Максимум что можно «логически» запрограммировать, это обход препятствий с выбором наилучшего направления, включение/выключение робота по звуку или уровню освещенности и. т.д, одним словом в интернете масса подобных поделок. Но вот какая штука – все это быстро надоедает,… Что если захотелось что- то поменять, например реакцию на свет или звук или получить некоторую непредсказуемость в поведении. Конечно, можно предусмотреть все это в программе контроллера, данные грузить с SD карточки, а непредсказуемость получить простым рандомом. Но мы не ищем легких путей…:). Делать будем так: пусть все выше перечисленные задачи решает нейронная сеть. Обучать будем методом обратного распространения ошибки.
Коэффициенты сети будут переноситься в контроллер посредством SD карты.
Как же это работает?
Был заложен следующий принцип работы:
На компьютере:
1) Создается обучающая выборка
2) Сеть обучается до тех пор, пока глобальная ошибка на всех выборках не станет равной нулю.(на самом деле будет немного не так)
3) После обучения создается отчет и файл для контроллера.
4) Файл записывается на карточку.
На Arduino:
1) После включения с SD карточки загружаются параметры сети.
2) После загрузки сеть готова к работе.
3) На основании показаний датчиков формируется «вектор данных».
4) Этот вектор подается на вход сети.
5) После просчета, выходной вектор сети подается на дешифратор.
6) Дешифратор, в зависимости от результата работы сети, определяет тип команды и выполняет ее.
7) Цикл повторяется с пункта 3,
Вектором данных я буду называть одномерный массив определенной размерности, элементы этого массива – числа. Скажем, что в нулевом элементе будет храниться расстояние до препятствия справа, в первом расстояние в прямой видимости и.т.д.
Почему же все-таки вектор? Когда я только начинал разбираться с нейронными сетями я сформировал для себя следующие понятие: «любой набор данных есть точка в N мерном пространстве». В нашем случае пространство будет размерности 5. Да, да такие бывают :).
Вот точное расположение данных в массиве:
input[0] – расстояние с права. input[1] – расстояние в прямой видимости. input[2] – расстояние слева. input[3] – значение фоторезистора. Input[4] – уровень микрофона (1 или 0)

Честно говоря, я не очень сильно хочу уделять сильное внимание этому пункту. Причина этого желания в том, что сам робот в первую очередь не является объектом исследования а его реализация банальна. Единственное о чем я хочу рассказать, это об драйвере двигателей L293D. Эту замечательную микросхему я использовал в силу отсутствия готового шилда. Честно говоря, в электронике я не селен и подключил ее по первой загугленной схеме. В итоге оказалось, что для управления направлением вращения приходилось переводить ножку в состояние INPUT. Это было весьма не привычно, и я до сих пор не знаю чем это может аукнуться…
Насчет колес…в качестве редукторов были использованы серво — машинки, переделанные на постоянное вращение. Все остальные примочки как датчик расстояния и SD card модуля были подключены по стандартным схемам.
Немного фото:



Да, качество сборки на 3 :)
Часть программная:
Даже не знаю с чего начать. С одной стороны тут нет ни каких велосипедов все стандартно:
Начнем с программы для ПК:
Написана она, а точнее они на C#. Потому что это достаточно простой язык и я часто использую его в своей работе, но не об этом. В программах нет классов все сделано в «функциональном» стиле для более простого понимания начинающим. Как Вы успели понять программ две: Первая это простой интерфейс, код которого не заслуживает тут присутствовать ибо та все очень просто. Будет только скриншот:

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

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

Хороший он потому, что строки Input Res и NET OUT совпадают. Это говорит о том, что при подачи на вход Input data мы хотели бы получить Input Res, а получаем NET OUT. Так что о качестве работы сети мы будем судить из этого отчета.
Теория гласит, что при обучении сети образцы стоит предъявлять в случайном порядке. К чему я это – в моей практике это, на первый взгляд, незначительное утверждение в действительности влияет на скорость сходимости алгоритма. Так же на скорость влияет и первоначально выбранные веса. Бывало что с первой попытки алгоритм не сходился, приходилось по несколько раз запускать обучение для того что бы добиться результата.
Я не знаю стоит ли приводить полный код программы вместе с парсером файлов и созданием выходного файла для контроллера. Так что приведу на свой взгляд только самые важны моменты в одном листинге:
static int InputN = 5, //Входные нейронны OutN = 5; //Выходные нейроны static int InputW = 5, //Веса на входе OutW = 5; //Веса на выходе static Random run = new Random(); //Гениратор случайностей static float[,] INPUT_W = new float[InputN, InputW]; //Входной слой static float[,] OUTPUT_W = new float[OutN,OutW]; //Выходной слой static float[] INPUT_RESULT = new float[InputN]; //Выход первого слоя static float[] OUTPUT_RESULT = new float[OutN]; //Выход последнего слоя static float[] OUTPUT_ERROR = new float[OutN]; //Ошибка на выходе static float[] INPUT_ERROR = new float[InputN]; //Ошибка на входе static float Sig = 0.5f; //Значение порога static float etta = 0.001f; //Значение параметра скорости static List<float[]> Input_patern = new List<float[]>(); //Входные данные static List<float[]> Out_patern = new List<float[]>(); //Ответы /*Инициализация весов*/ static void init_net() { for (int i = 0; i < InputN; i++){ for (int j = 0; j < InputW; j++){ INPUT_W[i, j] = (float)run.NextDouble() - 0.5f; } } for (int i = 0; i < OutN; i++){ for (int j = 0; j < OutW; j++){ OUTPUT_W[i, j] = (float)run.NextDouble() - 0.5f; } } } /*Прямой проход*/ static void DirectMotion(float[] vector) { if (vector.Length > InputW) { return; } /*Первый слой*/ for (int i = 0; i < InputN; i++){ INPUT_RESULT[i] = 0.0f; for (int j = 0; j < InputW; j++){ INPUT_RESULT[i] += INPUT_W[i, j] * vector[j]; } INPUT_RESULT[i] = Sigma(INPUT_RESULT[i]); } /*Выходной слой*/ for (int i = 0; i < OutN; i++){ OUTPUT_RESULT[i] = 0.0f; for (int j = 0; j < OutW ; j++){ OUTPUT_RESULT[i] += OUTPUT_W[i, j] * INPUT_RESULT[j]; } OUTPUT_RESULT[i] = Sigma(OUTPUT_RESULT[i]); } } static float BackPropagation(float[] vector, float[] target) { /*--------------------------------Прямой проход-------------------------------------------*/ DirectMotion(vector); /*----------------------------------------------------------------------------------------*/ /*--------------------------------Расчет ошибки-------------------------------------------*/ /*Ошибка на выходе*/ for (int i = 0; i < OUTPUT_ERROR.Length; i++) { OUTPUT_ERROR[i] = 0.0f; OUTPUT_ERROR[i] = target[i] - OUTPUT_RESULT[i]; } /*Ошибка на входе*/ for (int neurons = 0; neurons < INPUT_ERROR.Length; neurons++){ INPUT_ERROR[neurons] = 0.0f; for (int out_n = 0; out_n < OutN; out_n++) { INPUT_ERROR[neurons] += OUTPUT_ERROR[out_n] * OUTPUT_W[out_n, neurons]; } } /*-----------------------------------------------------------------------------------------*/ /*---------------------------------Коррекция ошибки----------------------------------------*/ /* Входной слой */ for (int n = 0; n < InputN; n++){ for (int w = 0; w < InputW; w++){ INPUT_W[n, w] += (etta * INPUT_ERROR[n] * vector[w]); } } /*Выходной слой*/ for (int n = 0; n < OutN; n++){ for (int w = 0; w < OutW; w++){ OUTPUT_W[n, w] += (etta * OUTPUT_ERROR[n] * INPUT_RESULT[w]); } } /*------------------------------------------------------------------------------------------*/ /*возвращаем ошибку на текущем наборе...*/ return calculate_global_error(); } static float calculate_global_error() { float error = 0.0f; for (int i = 0; i < OUTPUT_ERROR.Length; i++) { error += Math.Abs(OUTPUT_ERROR[i] * OUTPUT_ERROR[i]); } return error * 0.5f; } static void Learn() { int Iteration = 0; float Error = 0.0f; while (Iteration < 250000) { Error = 0.0f; for (int paterns = 0; paterns < Input_patern.Count; paterns++) { Error += BackPropagation(Input_patern[paterns], Out_patern[paterns]); } Console.WriteLine(Error); Iteration++; } } /*Пороговая функция активации*/ static float Sigma(float value) { if (value >= Sig) { return 1.0f; } return 0.0f; } Собственно ни чего сложного. Как мы видим из кода в качестве функции активации нейронов выбрана пороговая функция с параметром срабатывания 0,5. Это позволяет избавиться от производных при обучении, а на контроллере не тратить время на вычисление значения логистической функции. Хотя при этом мы жертвуем временем сходимости алгоритма (это не научная теория, а лишь мои наблюдения на практике). Да, возможно кто то заметил, что в процессе обучения стоит константное число итераций. Это связанно с тем, что иногда сеть проваливается в локальные минимумы, при этом ошибка равно нулю, а вот качество обучения порой ужасное. Так что пришлось сделать так… Возможно я что то делаю неверно… если кто то заметит, скажите пожалуйста. Собственно код Arduino: #include <Servo.h> #include <SD.h> #include <Ultrasonic.h> #define R_F 5 #define L_F 8 #define R_R 6 #define L_R 9 #define TRIG 4 #define ECHO 7 #define INPUT_W_COUNT 5 // #define INPUT_N_COUNT 5 // #define OUTPUT_W_COUNT 5 // #define OUTPUT_N_COUNT 5 // #define INPUT_VECTOR_SIZE 5 // #define TMP_VECTOR_SIZE 50 // #define START_ANGLE 130 #define RIGHT_ANGLE 55 #define LEFT_ANGLE 180 #define SERVO_PIN 3 #define MIC_PIN 2 #define REZISTOR_PIN 0 /*Для чтения данных с флешки*/ File myFile; /*Датчик растояния*/ Ultrasonic dist(TRIG, ECHO); /*голова :)*/ Servo servo; float input[INPUT_VECTOR_SIZE]; float Wtmp[TMP_VECTOR_SIZE]; /*Веса первого слоя*/ float INPUT_W[INPUT_N_COUNT][INPUT_W_COUNT]; /*Веса выходного слоя*/ float OUTPUT_W[OUTPUT_N_COUNT][OUTPUT_W_COUNT]; float INPUT_OUT[INPUT_N_COUNT]; //Выход первого слоя float NET_OUT[OUTPUT_N_COUNT]; //Выход сети int FileCount = 0, CountW = 0; void setup(){ servo.attach(SERVO_PIN); servo.write(START_ANGLE); delay(1000); ReadDataFromSD(); delay(1000); } void loop(){ /*Получаем данные с датчиков*/ CreateVector(); /*Расчитываем выход сети*/ NeuralNetUpdate(); } /*Пересчет выхода сети*/ void NeuralNetUpdate(){ /*Входной слой*/ for(int N = 0; N < INPUT_N_COUNT; N++){ INPUT_OUT[N] = 0.0; for(int W = 0; W < INPUT_W_COUNT; W++){ INPUT_OUT[N] += input[W] * INPUT_W[N][W]; } INPUT_OUT[N] = Sigmoid(INPUT_OUT[N]); } /*Выходной слой*/ for(int N = 0; N < OUTPUT_N_COUNT; N++){ NET_OUT[N] = 0.0; for(int W = 0; W < OUTPUT_W_COUNT; W++){ NET_OUT[N] += INPUT_OUT[W] * OUTPUT_W[N][W]; } NET_OUT[N] = Sigmoid(NET_OUT[N]); if(NET_OUT[N] == 1.0){ command(N); } } } float Sigmoid(float value){ if(value > 0.50f){return 1.0;} return 0.0f; } void CreateVector(){ input[1] = (float)dist.Ranging(CM); input[3] = analogRead(0); input[4] = digitalRead(2); } void LoadInputW(){ for(int N = 0; N < INPUT_N_COUNT; N++){ for(int W = 0; W < INPUT_W_COUNT; W++){ INPUT_W[N][W] = Wtmp[CountW]; CountW++; } } } void LoadOutputW(){ for(int N = 0; N < OUTPUT_N_COUNT; N++){ for(int W = 0; W < OUTPUT_W_COUNT; W++){ OUTPUT_W[N][W] = Wtmp[CountW]; CountW++; } } } /*Заполняет временный массив коэфиуиентами с SD карты*/ void ReadDataFromSD(){ pinMode(10, OUTPUT); if (!SD.begin(10)) {return; } myFile = SD.open("test.txt"); if (myFile) { int i = 0; char tmp[32]; while (myFile.available()) { tmp[i] = myFile.read(); if(tmp[i] == ';'){ tmp[i] = ' '; Wtmp[FileCount] = atof(tmp); FileCount++; i = 0; } else { i++; } } myFile.close(); } /*Загружаем первый слой*/ LoadInputW(); /*Загружаем второй слой*/ LoadOutputW(); } void command(int value){ if(value == 0){ Run(); delay(2000); Stop(); return;} if(value == 1){ Stop(); delay(2000); return;} if(value == 2){ Revers(); delay(2000); Left(); delay(2000); Stop(); return; } if(value == 3){ Revers(); delay(2000); Right(); delay(2000); Stop(); return;} if(value == 4){ View(); return;} } void Stop(){ pinMode(R_R, OUTPUT); pinMode(L_R, OUTPUT); pinMode(R_F, OUTPUT); pinMode(L_F, OUTPUT); } void Run(){ input[2] = 0.0f; input[0] = 0.0f; pinMode(R_R, OUTPUT); pinMode(L_R, OUTPUT); pinMode(R_F, INPUT); pinMode(L_F, INPUT); } void Revers(){ pinMode(R_R, INPUT); pinMode(L_R, INPUT); pinMode(R_F, OUTPUT); pinMode(L_F, OUTPUT); } void Left(){ input[2] = 0.0f; input[0] = 0.0f; pinMode(R_R, OUTPUT); pinMode(L_F, OUTPUT); pinMode(L_R, INPUT); pinMode(R_F, INPUT); } void Right(){ input[2] = 0.0f; input[0] = 0.0f; pinMode(R_R, INPUT); pinMode(L_F, INPUT); pinMode(L_R, OUTPUT); pinMode(R_F, OUTPUT); } void View(){ input[1] = 0.0f; servo.write(RIGHT_ANGLE); delay(500); input[0] = (float)dist.Ranging(CM); delay(800); servo.write(LEFT_ANGLE); delay(500); input[2] = (float)dist.Ranging(CM); delay(800); servo.write(START_ANGLE); delay(500); }
Запустить сеть на Arduino возможно и она даже будет работать. Временами конечно же начинаются глюки. Я думаю они связаны с обучающей выборкой. Я не тратил особо много времени на ее подборку и по этому не могу утверждать точно. Возможно в скором времени будут внесены некоторые коррективы. Ну а сейчас пока вот так. Спасибо за внимание.