Arduino нейросеть для управления роботом

Публикация 03.04.2015

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

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)
Двухслойная нейросеть с пятью нейронами на слой для ArduinoЖелезо:

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

Немного фото:

Arduino нейросеть для управления роботомArduino нейросеть для управления роботомArduino нейросеть для управления роботом

Да, качество сборки на 3 :)

Часть программная:

Даже не знаю с чего начать. С одной стороны тут нет ни каких велосипедов все стандартно:

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

Arduino нейросеть для управления роботом

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

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

Arduino нейросеть для управления роботом

Слева двоеточия что подаем на вход, справа то, что хотим получить.

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

Вот такой отчет хороший:

Arduino нейросеть для управления роботом

Хороший он потому, что строки 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 возможно и она даже будет работать. Временами конечно же начинаются глюки. Я думаю они связаны с обучающей выборкой. Я не тратил особо много времени на ее подборку и по этому не могу утверждать точно. Возможно в скором времени будут внесены некоторые коррективы. Ну а сейчас пока вот так. Спасибо за внимание.

* комментарии публикуются после модерации
Нет комментариев