воскресенье, 28 января 2018 г.

Voic3 Command3R

Юбилейный, сотый, проект от "Карандаша и Самоделкина" просто обязан быть чем-то особенным! В прошлых сериях мы успели поработать в области машинного зрения, и наш робот на базе LEGO Mindstorms EV3 смог различать простейшие образы и даже сыграть с нами в "Камень, ножницы, бумага". На этот раз мы решили освоить работу со звуком и решить одну из задач, связанных с "машинным слухом".

Сделать робота, управляемого голосом, не так уж и сложно - достаточно воспользоваться готовым API от того же Google. В этом случае распознаваемый фрагмент отсылается онлайн-сервису, который в ответ сообщает распознанную текстовую строку. Если роботу нужна подобная автономная функциональность, существуют и готовые оффлайновые решения, вроде PocketSphinx, правда работают они в условиях ограниченных ресурсов на роботе крайне задумчиво. Главной же фишкой нашего проекта станет то, что мы не будем использовать никакие вспомогательные инструменты для распознавания речи, а напишем свой собственный "движок" для голосового управления, пусть и простенький.


Перво-наперво нам понадобится устройство, способное дать возможность роботу физически ощущать звуковую волну для возможности преобразования ее в цифровую форму. В комплекте с наборами LEGO NXT первой версии поставлялся датчик звука (NXT Sound Sensor). C его помощью можно измерять звуковое давление в условных единицах. Такого датчика у нас нет, поэтому мы решили использовать USB-микрофон, подключенный в соответствующий порт блока EV3. Что касается датчика звука NXT, то по информации от его владельцев, скорость опроса этого датчика невелика, поэтому на него рассчитывать все равно не стоит.
В качестве USB-микрофона может выступать почти любая USB-звуковая плата, с подключенным к ней аналоговым микрофоном или USB-веб-камера, как правило имеющая встроенный микрофон. EV3-блок тоже имеет в своем составе звуковую плату, к выходу которой подключен встроенный динамик, а вот встроенного микрофона, увы, нет..Максимальная частота дискретизации этого устройства - 22кГц, поэтому качественно воспроизводить звук EV3-блок не может физически.

При подключении дополнительного USB звукового устройства появляется возможность работать с ними одновременно, например получая данные с микрофона внешней звуковой платы и воспроизводя звук по прежнему встроенным динамиком. Возможна и конфигурация, при которой звук будет воспроизводиться внешней USB звуковой платой, при этом появляется возможность подключить к роботу качественную или "громкую" акустику.
В проекте мы снова используем операционную систему ev3dev и будем программировать робота на языке Python. Для работы с звуковыми устройствами существует целый ряд python-модулей, мы остановили свой выбор на pyalsaaudio, обладающим высоким быстродействием в условиях ограниченных ресурсов. Для ускорения обработки данных и воспользуемся модулем NumPy. Хотя можно хранить данные и в традиционных для Python структурах, вроде списков, однако работа с звуковыми потоками, имеющими зачастую немалый объем, при таком подходе происходит крайне неторопливо.


Общий алгоритм работы нашего робота таков:
1) Стартуем процесс записи и анализируем приходящие со звуковой платы данные небольшими порциями, оценивая громкость. Как только она превысит заданный порог - начинаем запись заданной (избыточной для фразы) длительности, например 2 секунлы
2) Анализируем полученный звуковой отрезок (сэмпл) на предмет выделения на нем слова (фразы).


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


3) Воспроизводим сэмпл для мониторинга корректности его записи и выделения слова.
4) Разбиваем звуковой фрагмент на небольшие интервалы, длительностью около 20 мс. Лучший результат получается если разбивать на интервалы, перекрывающиеся н 50%, в этом случае скорость произношения фразы не будет заметно влиять на результат распознавания.


5) Анализируем каждый отрезок, выделяя в нем частоты с максимальной "громкостью". Частотный анализ фрагмента можно выполнить используя встроенную в NumPy функция быстрого преобразования Фурье.


На рисунке выделено 8 таких частот.
6) Получаем двумерную матрицу, содержащую несколько наиболее выраженным частот в каждом отрезке:


7) Сохраняем матрицу в виде именованного образца. Для каждого слова (голосовой команды) нужно записать несколько таких образков, с разной интонацией, можно разными голосами, если это необходимо.
8) Чтобы распознать слово-команду необходимо выполнить наги 1-5 и сравнить полученную матрицу с сохраненными образцами. Здесь есть несколько вариантов:
  • Найти самый похожий образец
  • Найти группу образцов, к которой анализируемый ближе всего
  • Скомбинировать поиск по близости к образцу и по близости к группе образцов. Если, например, анализируемый образец ближе всего к группе с образцами "Влево" и, одновременно, ближе в образцу "Вправо", у робота появляется возможность ответить "Не понял команды, повторите!".
  • Использовать нейронную сеть, обученную на записанных образцах.
В проекте мы используем третий вариант, а четвертый в данный момент изучаем.

суббота, 27 января 2018 г.

MindSnake3D

Пару недель назад в нашей группе VK был запущен опрос, для чего предназначен робот на фото?


Подсказки были таковы:
1) проект носит развлекательный характер
2) робот использует трехмерные массивы
3) разработка проекта ведется на языке С, однако сам проект тесно связан с другим языком программирования, который нам очень нравится
4) Шелдону Куперу он наверняка пришелся бы по душе.


Пришла пора приоткрыть завесу тайны. На каникулах мы тоже любим отдыхать, поэтому наш очередной проект, о котором сегодня пойдет речь, действительно носит развлекательный характер. Наверное многие из вас играли в знаменитую игру Snake ("Змейка"), она портирована на множество различных платформ, а наибольшую популярность получила будучи предустановленной на телефонах Nokia (подсказка №3: Питон - змея).


Змейка ползает по полю, поедая пищу, удлинняясь от каждой съеденной порции. Если змейка врезается в ограждение поля или в свой хвост - она погибает и игра начинается сначала. Цель игры - вырастить как можно более длинную змейку.
Мы давно хотели сделать эту игру на платформе LEGO Mindstorms, но интересная реализация никак не вырисовывалась. Создавать очередной клон на экране NXT или EV3 блока нам не хотелось, а реализовать змейку в механике похоже не по зубам даже гуру LEGO-роботостроения. 
Наконец, когда пришла пора изучить трехмерные массивы - все сложилось и идея обрела законченную форму. Далеким прототипом стал наш наш проект 2015 года - EV3 Муха. В этой игре в уме (или "мыслилке", как мы называем такого типа развлечения) мы должны были двигать воображаемую муху по плоскому полю. 
В проекте MinSnake мы добавим пищи для ума в виде еще одного измерения. Наша змейка будет ползать в 3D-кубе, который игрок должен представить в уме, управление головой змеи мы реализовали в конструкции. Все игровые события робот озвучивает голосом, экран блока в геймплее не используется. Змейка как и в классическом варианте не должна выползти за пределы игрового поля (куба) и не должна врезаться сама в себя (в свой хвост). Заморочено, правла? Шелдону Куперу точно бы понравилось!



Исходные данные таковы:
- куб имеет размер 3x3 пиксела
- змейка на старте имеет длину = 1, стартует из центра (ячейка 2,2,2)
- стартовое направление в начале игры - направо
- в зависимости от выбранного уровня сложности на ход дается в среднем 5 секунд, за это время игрок должен выбрать одно из 6 направлений движения (вверх-вниз-влево-вправо-к себе-от себя), в соответствии с которым змейка передвинется по истечении времени хода. На каждый следующий ход дается на несколько миллисекунд меньше чем на предыдущий
- еда появляется в кубе случайным образом, на свободном от змейки месте. Робот называет ячейку где именно. Чтобы не путаться в координатах озвучивается слой сверху вниз (1-2-3) и номер ячейки 1..9 по телефонному принципу (см. рис)


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


Инструкцию по сборке нашего проекта в формате LEGO Digital Designer и программу к роботу вы можете скачать по ссылке.



Исходный код программы на языке NXC:
struct coord{
  int x;
  int y;
  int z;
};

coord head;
coord eda;
coord max;

int snake[3][3][3];

void food(){
  while(true){
    eda.x=Random(2999)/1000.0;
    eda.y=Random(2999)/1000.0;
    eda.z=Random(2999)/1000.0;
    if(snake[eda.x][eda.y][eda.z]==0){
      break;
    }
  }
}

int otkl(int a, int b)
{
  return ((a+36000)%360 + 540 - b)%360 - 180;
}

int direct(){
  if(abs(otkl(MotorRotationCount(OUT_B),0))<=45){
    return 4;
  }
  if(abs(otkl(MotorRotationCount(OUT_B),180))<=45){
    return 5;
  }

  if(abs(otkl(MotorRotationCount(OUT_A),0))<=45 && abs(otkl(MotorRotationCount(OUT_B),90))<=45){
    return 3;
  }

  if(abs(otkl(MotorRotationCount(OUT_A),0))<=45 && abs(otkl(MotorRotationCount(OUT_B),270))<=45){
    return 2;
  }

  if(abs(otkl(MotorRotationCount(OUT_A),90))<=45 && abs(otkl(MotorRotationCount(OUT_B),90))<=45){
    return 1;
  }

  if(abs(otkl(MotorRotationCount(OUT_A),90))<=45 && abs(otkl(MotorRotationCount(OUT_B),270))<=45){
    return 0;
  }

  if(abs(otkl(MotorRotationCount(OUT_A),180))<=45 && abs(otkl(MotorRotationCount(OUT_B),90))<=45){
    return 3;
  }

  if(abs(otkl(MotorRotationCount(OUT_A),180))<=45 && abs(otkl(MotorRotationCount(OUT_B),270))<=45){
    return 2;
  }

  if(abs(otkl(MotorRotationCount(OUT_A),270))<=45 && abs(otkl(MotorRotationCount(OUT_B),90))<=45){
    return 1;
  }

  if(abs(otkl(MotorRotationCount(OUT_A),270))<=45 && abs(otkl(MotorRotationCount(OUT_B),270))<=45){
    return 0;
  }

  return -1;
}
int time_hod=5000;
int direction=4;//0-туда1-обратно2-вверх3-вниз4-вправо5-влево
int eda_farther=0;
int maxa=0;
int direction_old=4;
long ti;
int len=1;
int level=0;
int len_up;
task main()
{
  ClearScreen();
  //TextOut(0,16,"Easy");
  //TextOut(0,8,"Normal");
  //TextOut(0,0,"Hard");
  Wait(1000);
  while(ButtonPressed(BTNCENTER,0)
  && ButtonPressed(BTNLEFT,0)
  && ButtonPressed(BTNRIGHT,0)){

  }
  if(ButtonPressed(BTNCENTER,1)){
    level=2;
    len_up=9;
    time_hod=5000;
    ClearScreen();
  //  TextOut(0,0,"Normal");
  }
  if(ButtonPressed(BTNLEFT,1)){
    level=1;
    len_up=3;
    time_hod=5000;
    ClearScreen();
  //  TextOut(0,0,"Easy");
  }
  if(ButtonPressed(BTNRIGHT,1)){
    level=3;
    len_up=9;
    time_hod=3000;
    ClearScreen();
  //  TextOut(0,0,"Hard");
  }
  Wait(3000);

  eda.x=10;
  eda.y=10;
  eda.z=10;
  head.x=1;
  head.y=1;
  head.z=1;
  PlayFile("start.rso");
  PlayFile("center.rso");
  ResetRotationCount(OUT_AB);
  direction=direct();
  for(int i=0;i<=2;i++){
    for(int z=0;z<=2;z++){
      for(int q=0;q<=2;q++){
        snake[i][z][q]=0;
      }
    }
  }
  snake[1][1][1]=1;
  long time=CurrentTick();
  long time_eda=CurrentTick();
  while(true){
    if(len==len_up && level==3){
      break;
      PlayFile("levelup.rso");
    }
    if(len==len_up && level==2){
      level=3;
      len_up=9;
      time_hod=3000;
      PlayFile("levelup.rso");
      Wait(1000);
    }
    if(len==len_up && level==1){
      level=2;
      len_up=6;
      PlayFile("levelup.rso");
      Wait(1000);
    }

    direction_old=direction;
    direction=direct();

    if(direction!=direction_old){
      switch(direction)
      {
      case 0:
        PlayFile("farther.rso");
        Wait(1000);
        break;
       case 1:
         PlayFile("closer.rso");
         Wait(1000);
         break;
       case 2:
         PlayFile("up.rso");
         Wait(1000);
         break;
       case 3:
         PlayFile("down.rso");
         Wait(1000);
         break;
       case 4:
         PlayFile("right.rso");
         Wait(1000);
         break;
       case 5:
         PlayFile("left.rso");
         Wait(1000);
         break;
       default:
         break;
       }
    }
    if(CurrentTick()-time_eda>=20000){
      time_eda=CurrentTick();
      food();
      PlayFile("food.rso");
      Wait(1000);
      if(eda.y==0){
        PlayFile("3.rso");
        Wait(1000);
      }
      if(eda.y==1){
        PlayFile("2.rso");
        Wait(1000);
      }
      if(eda.y==2){
        PlayFile("1.rso");
        Wait(1000);
      }
      if(eda.z==2){
        if(eda.x==0){
          PlayFile("1.rso");
          Wait(1000);
        }
        if(eda.x==1){
          PlayFile("2.rso");
          Wait(1000);
        }
        if(eda.x==2){
          PlayFile("3.rso");
          Wait(1000);
        }
      }
      if(eda.z==1){
        if(eda.x==0){
          PlayFile("4.rso");
          Wait(1000);
        }
        if(eda.x==1){
          PlayFile("5.rso");
          Wait(1000);
        }
        if(eda.x==2)
        {
          PlayFile("6.rso");
          Wait(1000);
        }
      }
      if(eda.z==0){
        if(eda.x==0){
          PlayFile("7.rso");
          Wait(1000);
        }
        if(eda.x==1){
          PlayFile("8.rso");
          Wait(1000);
        }
        if(eda.x==2){
          PlayFile("9.rso");
          Wait(1000);
        }
      }

    }
    if(CurrentTick() - ti>=1000){
      ti=CurrentTick();
 //     ClearScreen();
     // NumOut(10,0,eda.z);
    // / NumOut(10,8,eda.y);
    //  NumOut(10,16,eda.x);
    //  NumOut(0,0,head.z);
   ///   NumOut(0,8,head.y);
    //  NumOut(0,16,head.x);
    //  NumOut(0,24,direction);
     // NumOut(5,0,len);
    }
    if(CurrentTick()-time>=time_hod){
      PlayTone(440,100);
      time_hod=time_hod-10;
     
      time=CurrentTick()
      if(head.x==2 && direction==4
      || head.x==0 && direction==5
      || head.y==2 && direction==2
      || head.y==0 && direction==3
      || head.z==2 && direction==0
      || head.z==0 && direction==1){
        break;
      }
   
      eda_farther=0;
   
      if(direction==0 && head.x==eda.x && head.y==eda.y && head.z+1==eda.z){
        eda_farther=1;
      }
      if(direction==1 && head.x==eda.x && head.y==eda.y && head.z-1==eda.z){
        eda_farther=1;
      }
      if(direction==2 && head.x==eda.x && head.y+1==eda.y && head.z==eda.z){
        eda_farther=1;
      }
      if(direction==3 && head.x==eda.x && head.y-1==eda.y && head.z==eda.z){
        eda_farther=1;
      }
      if(direction==4 && head.x+1==eda.x && head.y==eda.y && head.z==eda.z){
        eda_farther=1;
      }
      if(direction==5 && head.x-1==eda.x && head.y==eda.y && head.z==eda.z){
        eda_farther=1;
      }
      for(int i=0;i<=2;i++){
        for(int z=0;z<=2;z++){
          for(int q=0;q<=2;q++){
            if(snake[i][z][q]!=0){
              snake[i][z][q]+=1;
            }
          }
        }
      }
      max=0;
      for(int i=0;i<=2;i++){
        for(int z=0;z<=2;z++){
          for(int q=0;q<=2;q++){
            if(snake[i][z][q]>maxa){
              maxa=snake[i][z][q];
              max.x=i;
              max.y=z;
              max.z=q;
            }
          }
        }
      }

      if(eda_farther==0){
        snake[max.x][max.y][max.z]=0;
      }
      else{
        len++;
        time_eda=CurrentTick();
        eda.x=10;
        eda.y=10;
        eda.z=10;
        PlayFile("len.rso");
        Wait(1000);
      }
      maxa=0;
      max.x=0;
      max.y=0;
      max.z=0;
      if(direction==0){
        if(snake[head.x][head.y][head.z+1]==0){
          snake[head.x][head.y][head.z+1]=1;
          head.z++;
        }
        else{
          break;
        }
      }
      if(direction==1){
        if(snake[head.x][head.y][head.z-1]==0){
          snake[head.x][head.y][head.z-1]=1;
          head.z--;
        }
        else{
          break;
        }
      }
      if(direction==2){
        if(snake[head.x][head.y+1][head.z]==0){
          snake[head.x][head.y+1][head.z]=1;
          head.y++;
        }
        else{
          break;
        }
      }
      if(direction==3){
        if(snake[head.x][head.y-1][head.z]==0){
          snake[head.x][head.y-1][head.z]=1;
          head.y--;
        }
        else{
          break;
        }
      }
      if(direction==4){
        if(snake[head.x+1][head.y][head.z]==0){
          snake[head.x+1][head.y][head.z]=1;
          head.x++;
        }
        else{
          break;
        }
      }
      if(direction==5){
        if(snake[head.x-1][head.y][head.z]==0){
          snake[head.x-1][head.y][head.z]=1;
          head.x--;
        }
        else{
          break;
        }
      }
      direction=direct();
    }
  }
  PlayFile("end.rso");
  Wait(1000);
}


воскресенье, 17 декабря 2017 г.

SwarmLINEr

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


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


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

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


Итак, представьте себе стаю птиц. Каждая птица - это агент нашего алгоритма. Условимся, что наши птицы - из тех, которые не имеют в стае четко выраженного вожака. Несмотря на то, что каждая птица не обладает достаточным интеллектом чтобы управлять стаей, однако стая в целом проявляет вполне себе разумное поведение:
  • птицы в стае почти никогда не сталкиваются в воздухе
  • стая двигается плавно и скоординировано, словно ей кто-то управляет
  • кружа в небе, каждая из птиц следит за своими сородичами и корректирует свое движение согласно их положению
  • найдя источник пищи, она оповестит об этом остальных.
Именно оповещение других особей стаи о найденной пище и является основой рассматриваемого алгоритма. Такое странное поведение птиц было исследовано многими учеными, но они пока не сошлись во мнении, почему так происходит. Источники пищи располагаются обычно довольно случайным образом и отдельно взятая птица может летать довольно долго, но так и не найдя пищу, погибнет. С одной стороны, получив сигнал о расположении пищи, птица скорректирует свое направление и полетит к ней, что повышает ее шансы на выживание. С другой стороны, птицам приходится конкурировать с другими особями за найденную пищу, так как количество ее ограничено.


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

В общем виде алгоритм выглядит так:


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

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

Попробуем реализовать алгоритм на практике. Конструкция робота полностью повторяет GeneLINErа,  инструкцию по сборке которого можно скачать по ссылке. Программировать LEGO Mindstorms NXT мы снова будем на полюбившемся нам NXC. Код нашей программы можно скачать здесь.

Первым делом давайте опишем каждую птицу (агента)

struct agent
{
  // скорость агента
  float speed;
  // текущий П-коэффициент ПИД-регулятора у агента
  float Kp;
  // текущий И-коэффициент у агента
  float Ki;
  // текущий Д-коэффициент 
  float Kd;
  // динамика изменения скорости у агента
  float change_speed;
  // динамика изменения П-коэффициента у агента
  float change_Kp;
  // динамика изменения И-коэффициента
  float change_Ki;
  // динамика изменения Д-коэффициента
  float change_Kd;
  // лучшее значение П-коэффициента, найденное агентом
  float best_Kp;
  // лучшее значение И-коэффициента
  float best_Ki;
  // лучшее значение Д-коэффициента
  float best_Kd;
  // скорость, соответствующая лучшим значением коэффициентов
  float best_speed;
  // максимальная длина пути с лучшими из найденных параметров
  long best_path;
  // текущий путь, пройденный за время последнего испытания
  long path;
};

При создании каждого агента случайно сгенерируем его начальные параметры в некотором диапазоне с помощью функции born(). 

В нашем случае мы полагаем что оптимальные параметры лежат в диапазоне

0 < Kp < 2
0 < Ki < 0.1
0 < Kd < 4
0 < speed < 100

а динамику их изменения установим в +/- 2..5%

-0.1 < change_Kp < 0.1
-0.005 < change_Ki < 0.005
-0.2 < change_Kd < 0.2
-2 < change_speed < 2

agent born(){
  agent tmp;
  tmp.speed = Random(70)+30;
  tmp.Kp = Random(2000)/1000.0;
  tmp.Ki= Random(10000)/100000.0;
  tmp.Kd= Random(4000)/1000.0;
  tmp.change_speed=(Random(4000)-2000)/1000.0;
  tmp.change_Kp=(Random(2000)-1000)/10000.0;
  tmp.change_Ki=(Random(1000)-500)/100000.0;
  tmp.change_Kd=(Random(4000)-2000)/10000.0;
  tmp.path = 0;
  tmp.best_path=0;
  return tmp;
}

Создадим 10 птиц (агентов):

agents = 10
agent robot[agents];
for(int i=0;i<agents;i++){
  robot[i]=born();
}

Еще одного агента создадим в качестве хранилища для лучшего найденного решения:

agent best;

Теперь испытаем каждую птицу, "узнаем сколько пищи он нашла" в тех координатах, в которых она сейчас находится. Функция pid() для испытания особей в целом не претерпела изменений по сравнению с GeneLINEr, за исключением последнего параметра - это длительность испытания в миллисекундах.

for (int i=0;i<agents;i++){
      robot[i].path = pid(robot[i].Kp,robot[i].Ki
                      ,robot[i].Kd,robot[i].speed,2000);

  // сохраняем лучшее общее решение, найденное стаей
  if(robot[i].path>best.path){
    best.path=robot[i].path;
    best.speed=robot[i].speed;
    best.Kp=robot[i].Kp;
    best.Ki=robot[i].Ki;
    best.Kd=robot[i].Kd;        
  }
  // сохраняем лучшее решение, найденное каждой птицей
  if(robot[i].path>robot[i].best_path){
    robot[i].best_path=robot[i].path;
    robot[i].best_speed=robot[i].speed;
    robot[i].best_Kp=robot[i].Kp;
    robot[i].best_Ki=robot[i].Ki;
    robot[i].best_Kd=robot[i].Kd;
  }
}

Теперь самое сложное в алгоритме - коррекция скорости, направления полета и положения каждой птицы.

Сначала введем два параметра:

float nostalgia =0.25;
float confidence=0.75;

Nostalgia - это стремление птицы быть поближе к тому месту, где она нашла пищи больше всего. Confidence - стремление птицы быть поближе месту, где пиши больше всего по общему мнению стаи.

Формула для корректировки скорости изменения каждого параметра имеет вид:

СКОРОСТЬ_ИЗМЕНЕНИЯ_ПАРАМЕТРА = СКОРОСТЬ_ИЗМЕНЕНИЯ_ПАРАМЕТРА
              + Nostalgia * СЛУЧАЙНОЕ_ЧИСЛО(0..1)  
              * (ЛУЧШЕЕ_ЗНАЧЕНИЕ_ПАРАМЕТРА_ОСОБИ - ТЕКУЩЕЕ_ЗНАЧЕНИЕ_ПАРАМЕТРА)  
              + Confidence * СЛУЧАЙНОЕ_ЧИСЛО(0..1)  
              * (ЛУЧШЕЕ_ЗНАЧЕНИЕ_ПАРАМЕТРА_СТАИ - ТЕКУЩЕЕ_ЗНАЧЕНИЕ_ПАРАМЕТРА)

Давайте ее проанализируем на примере высоты полета птицы:

  • скорость изменения высоты полета основывается на скорости изменения высоты полета в предыдущую единицу времени (то есть у птицы есть инерция)
  • чем больше параметр Nostalgia, тем сильнее птица будет стремиться лететь на высоте, на которой она увидела больше всего пищи
  • чем выше птица поднимается от той точки, из которой она видела больше всего пищи, тем сильнее замедляется скорость набора высоты (тоже самое со снижением высоты полета)
  • чем больше параметр  Confidence, тем сильнее птица будет стремиться лететь на высоте, на которой по общему мнению стаи видно больше всего пищи
  • чем выше птица поднимается от той точки, из которой по общему мнению стаи больше всего пищи, тем сильнее замедляется скорость набора высоты (тоже самое со снижением высоты полета)
  • СЛУЧАЙНОЕ_ЧИСЛО(0..1) превносит в действия птицы элемент случайности, на каждом шаге птица может более или менее может хотеть действовать самостоятельно или подчиняться мнению стаи, это некие перепады ее настроения.

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

НОВОЕ_ЗНАЧЕНИЕ_ПАРАМЕТРА =
  СТАРОЕ_ИЗМЕНЕНИЯ_ПАРАМЕТРА + СКОРОСТЬ_ИЗМЕНЕНИЯ_ПАРАМЕТРА

В программе это выглядит следующим образом:

for (int i=0;i<agents;i++){

  robot[i].change_Kp = robot[i].change_Kp+nostalgia 

                       * (Random(1000)/1000.0)

                       * (robot[i].best_Kp-robot[i].Kp)
                       + confidence*(Random(1000)/1000.0)
                       * (best.Kp-robot[i].Kp);
  robot[i].Kp=robot[i].Kp+robot[i].change_Kp;

  robot[i].change_Ki = robot[i].change_Ki+nostalgia 
                       * (Random(1000)/1000.0)
                       * (robot[i].best_Ki-robot[i].Ki)
                       + confidence*(Random(1000)/1000.0)
                       * (best.Ki-robot[i].Ki);
  robot[i].Ki=robot[i].Ki+robot[i].change_Ki;

  robot[i].change_Kd = robot[i].change_Kd+nostalgia 
                       * (Random(1000)/1000.0)
                       * (robot[i].best_Kd-robot[i].Kd)
                       + confidence*(Random(1000)/1000.0)
                       * (best.Kd-robot[i].Kd);
  robot[i].Kd=robot[i].Kd+robot[i].change_Kd;


  robot[i].change_speed = robot[i].change_speed+nostalgia 
                          * (Random(1000)/1000.0)
                          * (robot[i].best_speed-robot[i].speed)
                          + confidence*(Random(1000)/1000.0)
                          * (best.speed-robot[i].speed);
  robot[i].speed=robot[i].speed+robot[i].change_speed;
}

Если птица вылетела за пределы территории поиска, она пересоздается:

for (int i=0;i<agents;i++){
  if(robot[i].Kp<=0 || robot[i].Kp>=2 ||
     robot[i].speed<=0 || robot[i].speed>100 ||
     robot[i].Ki<=0 || robot[i].Ki>0.1 ||
     robot[i].Kd<=0 || robot[i].Kd>4){
     robot[i]=born();
}

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

final = pid(best.Kp,best.Ki,best.Kd,best.speed,100000);

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

logstr =NumToStr(pok)+ ";"+NumToStr(best.Kp)+";"
        +NumToStr(best.Ki)+";" +NumToStr(best.Kd)+";"
        +NumToStr(best.speed)+";"+NumToStr(best.path);
msg_len = StrLen(logstr);
WriteLnString(fh, logstr, msg_len);




воскресенье, 26 ноября 2017 г.

EV3 Робот, играющий в "Камень, ножницы, бумага"

Камень, ножницы, бумага — популярная игра на руках, известная во многих странах мира. Часто используется как методика жеребьёвки для выбора персоны для какой-либо цели (наряду с бросанием монеты, вытягиванием соломинок и т. п.).



Игроки считают вместе вслух «Камень… Ножницы… Бумага… Раз… Два… Три», одновременно качая кулаками. На счёт «Три» они одновременно показывают при помощи руки один из трёх знаков: камень, ножницы или бумагу. Знаки изображены на картинке.
Победитель определяется по следующим правилам:
  • Камень побеждает ножницы («камень слишком крепок для ножниц»)
  • Бумага побеждает камень («бумага накрывает камень»)
  • Ножницы побеждают бумагу («ножницы разрезают бумагу»)
Если игроки показали одинаковый знак, то засчитывается ничья и игра переигрывается.

В предыдущем нашем проекте с использованием LEGO Mindstorms EV3, камеры и нейронной сети, N3uralV1s10n, мы уже научились распознавать простейшие образы. Использование машинного зрения открывает интересные возможности по решению давно задуманных задач. Одна из них - робот, способный играть с человеком в "Камень, ножницы, бумагу".
Начнем с конструкции робота. "Модуль распознавания образов" в виде подставки для камеры и рамка-держатель для фона перекочевали без особых изменений из "распознавателя цифр". Белый фон за рукой позволяет распознавать образы более стабильно, хотя возможна работа и без него. В конструкции предусмотрен датчик-кнопка, она может использоваться особо мнительными игроками в розыгрыше раунда. В "контактном" режиме игры до нажатия кнопки можно быть уверенным, что робот не подсмотрел, какую фигуру начал ставить или поставил человек. В основном режиме игры, "бесконтактном", никакие органы управления не используются, робот полностью опирается на данные с камеры.

Руку робота мы сконструировали с использованием 4 моторов, три из которых управляют пальцами, а четвертый мотор позволяет "махать кулаком". 
Инструкцию по сборке конструкции можно скачать по ссылке. 
Программное обеспечение для робота написано на языке Python. В составе комплекта предусмотрено 2 программы:
  • Программа обучения нейронной сети робота
  • Программа для игры в "Камень, ножницы, бумага", использующая данные обученной нейросети
Первая программа предлагает человеку последовательно показывать предлагаемые роботом фигуры, чтобы робот запомнил как они выглядят и сформировал нейронные связи для их распознавания. После того как первая программа устойчиво начинает распознавать "камень", "ножницы" и "бумагу", нейронные связи сохраняются в файл для дальнейшего использования их второй, игровой программой.

Код программы для обучения нейронной сети:

from ev3dev.ev3 import *
import pygame
import time
import pygame.camera
from random import random
from PIL import Image, ImageDraw, ImageFont
import datetime

lcd = Screen()
btn = Button()

Sound.play('sound/load.wav').wait()

form = 1
game = 1
ok = 0
ok_all = True

v = 32
g = 24

S1 = TouchSensor("in2")

buf = [ [0] * g for i in range(v)]

class object:
    def __init__(self, n):
        self.name = n
        self.sum = 0
        self.picture = [ [0] * g for i in range(v)]
               
myObject = [object(1), object(2), object(3)]

def image2buf(surf):
    width, height = surf.get_size() 
    for y in range(height): 
        for x in range(width): 
            red, green, blue, alpha = surf.get_at((x, y)) 
            L = 0.3 * red + 0.59 * green + 0.11 * blue
            if L > 70:
                buf[x][y] = 0
            else:
                buf[x][y] = 1
      
pygame.init()
pygame.camera.init()
cameras = pygame.camera.list_cameras()
cam = pygame.camera.Camera(cameras[0])

Sound.play('sound/learnbegin.wav').wait()

while(True):
    lcd.clear()    
         
    if(form == 1): Sound.play('sound/move2stone.wav').wait()
    if(form == 2): Sound.play('sound/move2scissors.wav').wait()
    if(form == 3): Sound.play('sound/move2paper.wav').wait()
    
    while(True): 
        if(S1.value()): break   

    time.sleep(2)
    
    cam.start()
    image = cam.get_image()
    cam.stop()    

    Sound.beep().wait()    


    image = pygame.transform.scale(image, (v, g))
    image2buf(image)
      
    for i in range(v):
        for j in range(g):
            if buf[i][j] == 0:
                lcd.draw.rectangle((i*5+9, j*5+4, i*5+4+9, j*5+4+4),fill='white')
            else:
                lcd.draw.rectangle((i*5+9, j*5+4, i*5+4+9, j*5+4+4),fill='black')

    lcd.update()
       
    while(True):
        for o in myObject:
            o.sum = 0
        for o in myObject:    
            for i in range(v):
                for j in range(g):
                    o.sum += buf[i][j] * o.picture[i][j]

        max_sum = -100000

        for num in myObject:
            if num.sum > max_sum:
                max_sum = num.sum
                tmp_obj = num

        a = 0
    
        if(form == 1): 
            if(tmp_obj.name == 1): a = 1
            else: a = -1
        if(form == 2): 
            if(tmp_obj.name == 2): a = 1
            else: a = -1
        if(form == 3): 
            if(tmp_obj.name == 3): a = 1
            else: a = -1
                
        if(a == 1): ok+=1
        if(a == -1): 
            ok=0
            ok_all = False
        for i in range(v):
            for j in range(g):
                if(buf[i][j] == 1):
                    tmp_obj.picture[i][j] += a
   
        print(tmp_obj.name, ok)
        if(ok == 3): 
            form+=1
            if(form == 4): form = 1
            ok = 0
            break
            
    game+=1
    if(game > 9): 
        if(ok_all): break
        else: game-=3
    if(game % 3 == 0): ok_all = True
        
Sound.play('sound/saveneural.wav').wait()
    
f = open("kmn_file.txt", "w")
for i in myObject:
    f.write("\n" + str(i.name) + "\n\n")
    for x in range(v):
        for y in range(g):
            f.write(str(i.picture[x][y]) + " ")
        f.write("\n") 
f.close()
Sound.play('sound/learncomplete.wav').wait()

Вторая программа при запуске считывает файл с данными обученной с помощью первой программы нейронной сети, озвучивает правила игры и робот, начинает, тряся кулаком, отсчет первого раунда. Как только считалочка  роботом произнесена, человек может установить задуманную им фигуру напротив белого экрана, нажав запястьем на кнопку,, тем самым дав понять роботу что игра началась.
В момент нажатия кнопки робот, делает кадр с камеры для распознавания нейронной сетью и используя ряд заложенных в его программу стратегий поведения, устанавливает свою фигуру. Управления рукой робота производится с помощью четырех независимых ПИД-регуляторов. 
Для независимого управления рукой робота используется параллельный процесс, организованный с применением модуля Threading.
После того, как фигура роботом установлена, начинает работу нейронная сеть, ее цель - распознать фигуру, которую установил человек, по данным кадра, сохраненного в момент нажатия кнопки.
Нейронная сеть в данном проекте однослойная, с прямым распространением ошибки. Такой сети вполне достаточно, чтобы отличить три фигуры друг от друга, она достаточно быстро работает на такой малопроизводительной по современным меркам платформе как EV3.
В зависимости от исхода раунда робот озвучивает результат и переходит к розыгрышу следующего. В случае одинаковых фигур раунд переигрывается. В каждой игре всего 3 раунда. по ее итогам робот сообщает результат.

Код основной игровой программы:


from ev3dev.ev3 import *
from PIL import Image, ImageDraw, ImageFont
import pygame.camera
import threading
import datetime
import pygame
import random
import time

lcd = Screen()
btn = Button()

Sound.play("sound/load.wav").wait()

lcd.clear()

game = 0
bot_win = 0
man_win = 0
game_itog = 0
roboform = 0
old_man_form = 0
first_game = True
stop = False

v = 32
g = 24

PBC = 5
PA = 5
PD = 10

uA = 0
uB = 0
uC = 0
uD = 0

eA = 0
eB = 0
eC = 0
eD = 0

speedA = 0
speedB = 0
speedC = 0
speedD = 0

speed = 300

S1 = TouchSensor("in2")

A = LargeMotor('outA')
B = LargeMotor('outB')
C = LargeMotor('outC')
D = MediumMotor('outD')

A.reset()
B.reset()
C.reset()
D.reset()

buf = [ [0] * g for i in range(v)]

class object:
    def __init__(self, n):
        self.name = n
        self.sum = 0
        self.picture = [ [0] * g for i in range(v)]
               
myObject = [object(1), object(2), object(3)]

def write(n, m):
    f = ImageFont.truetype('FreeMonoBold.ttf', 155)
    lcd.draw.text((40,-25), str(chr(58)), font=f)

    f = ImageFont.truetype('FreeMonoBold.ttf', 155)
    lcd.draw.text((-10,-10), str(n), font=f)

    f = ImageFont.truetype('FreeMonoBold.ttf', 155)
    lcd.draw.text((95,-10), str(m), font=f)

    lcd.update()

def image2buf(surf):
    width, height = surf.get_size() 
    for y in range(height): 
        for x in range(width): 
            red, green, blue, alpha = surf.get_at((x, y)) 
            L = 0.3 * red + 0.59 * green + 0.11 * blue
            if L > 80:
                buf[x][y] = 0
            else:
                buf[x][y] = 1

def upload_file():
    Sound.play("sound/loadneural.wav").wait()    
    f = open("kmn_file.txt", "r")
    tmp = []
    for i in f:
        tmp.append(i)
    j = 0
    x = 0
    tmpls = ""
    for i in range(len(myObject)):
        j+=3
        for x in range(v):
            tmpls = tmp[j].strip()
            tmpline = tmpls.split()
            j+=1
            for y in range(len(tmpline)-1):
                myObject[i].picture[x][y] = int(tmpline[y])
    f.close()

    for i in myObject:
        for x in range(v):
            for y in range(g):
                print(i.picture[x][y], end=" ")
            print()
        print("\n\n")

def if_form(itog, man_form):
    if(man_form == 1 and itog == 1): return 1
    if(man_form == 2 and itog == 1): return 2
    if(man_form == 3 and itog == 1): return 3

    if(man_form == 1 and itog == 0): return 3
    if(man_form == 2 and itog == 0): return 1
    if(man_form == 3 and itog == 0): return 2

def brake_motor():
    A.stop(stop_action="brake")
    B.stop(stop_action="brake")
    C.stop(stop_action="brake")
    D.stop(stop_action="brake")

def move_hand():
    pos = 30
    while(True):
        if(pos == 0 and not stop): pos = 30
        uA = (pos - A.position)

        speedA = uA*PA
        if(stop):
            pos = 0
            if(abs(uA) < 5): speedA = 0
        if(abs(uA)<5):
            pos *= -1   

        if(speedA > 900): speedA = 900

        if(speedA < -900): speedA = -900

        A.run_forever(speed_sp=speedA)

        time.sleep(0.1)

    brake_motor()
  
def put_form(pB, pC, pD):
    time_start = time.time()
    while(True):
        uB = (pB - B.position)
        uC = (pC - C.position)
        uD = (pD - D.position)
        
        if(abs(uB)<10 and abs(uC)<10 and abs(uD)<15): break 
        
        speedB = uB*PBC
        speedC = uC*PBC
        speedD = uD*PD

        if(time.time() - time_start > 3): break 

        if(speedB > 900): speedB = 900
        if(speedC > 900): speedC = 900
        if(speedD > 900): speedD = 900
         
        if(speedB < -900): speedB = -900
        if(speedC < -900): speedC = -900
        if(speedD < -900): speedD = -900

        B.run_forever(speed_sp=speedB)
        C.run_forever(speed_sp=speedC)
        D.run_forever(speed_sp=speedD)

def put_stone():
    put_form(0, 0, 0)
    brake_motor()
def put_scissors():
    put_form(-160, 0, 0)
    brake_motor()
def put_paper():
    put_form(-160, 160, 0)
    brake_motor()
def put_ok():
    put_form(0, 0, -65)
    brake_motor()



pygame.init()
pygame.camera.init()
cameras = pygame.camera.list_cameras()
cam = pygame.camera.Camera(cameras[0])

upload_file()

stop = True 

move = threading.Thread(target=move_hand)
move.daemon = True
move.start()

Sound.play("sound/rules.wav").wait()

Sound.play("sound/begingame.wav").wait()

while(True):
    time1 = datetime.datetime.now()

    stop = False 

    Sound.play("sound/knb123.wav").wait()

    while(True): 
        cam.start()
        image = cam.get_image()
        cam.stop()

        sum_image = 0

        image = pygame.transform.scale(image, (v, g))
        
        width, height = image.get_size()
        for y in range(height):
            for x in range(width):
                red, green, blue, alpha = image.get_at((x, y))
                L = 0.3 * red + 0.59 * green + 0.11 * blue
                sum_image += L
        print(sum_image/(width*height))
        if(sum_image/(width*height) < 200): 
            break
            Sound.beep()
        if(btn.backspace): 
            brake_motor()
            exit()

    stop = True 
    
    if(not first_game): old_man_form = tmp_obj.name
    
    if(first_game): roboform = int(random.randint(100,399)/100)
    else: roboform = if_form(game_itog, old_man_form)

    game_itog = 0

    if(roboform == 3):
        put_paper()
        Sound.play("sound/paper.wav").wait()
    if(roboform == 1):
        put_stone()
        Sound.play("sound/stone.wav").wait()
    if(roboform == 2):
        put_scissors()
        Sound.play("sound/scissors.wav").wait()

    cam.start()   
    image = cam.get_image()
    cam.stop()   

    Sound.beep().wait()    
    
    image = pygame.transform.scale(image, (v, g))
    image2buf(image)    
    
    for o in myObject:
        o.sum = 0
    for o in myObject:    
        for i in range(v):
            for j in range(g):
                o.sum += buf[i][j] * o.picture[i][j]

    max_sum = -100000

    for num in myObject:
        if num.sum > max_sum:
            max_sum = num.sum
            tmp_obj = num
    
    print(bot_win, man_win)

    Sound.play("sound/" + str(tmp_obj.name) + "-" + str(roboform) + ".wav").wait()

    if((tmp_obj.name == 1 and roboform == 2) or (tmp_obj.name == 2 and roboform == 1)): 
        Sound.play("sound/rules12.wav").wait()
    if((tmp_obj.name == 1 and roboform == 3) or (tmp_obj.name == 3 and roboform == 1)):
        Sound.play("sound/rules13.wav").wait()
    if((tmp_obj.name == 2 and roboform == 3) or (tmp_obj.name == 3 and roboform == 2)):
        Sound.play("sound/rules23.wav").wait()

    if(tmp_obj.name == 1):
        if(roboform == 3): 
            Sound.play("sound/ok.wav").wait()
            game_itog = 1
            put_ok()
            game+=1
            bot_win+=1
        elif(roboform == 2): 
            Sound.play("sound/robotlostround1.wav").wait()
            Sound.play("sound/robotlostround2.wav").wait()
            man_win += 1
            game_itog = 0
            game+=1
        else: Sound.play("sound/paritet.wav").wait()
            
    if(tmp_obj.name == 2):
        if(roboform == 1):
            Sound.play("sound/ok.wav").wait()
            game_itog = 1
            put_ok()
            game+=1
            bot_win+=1
        elif(roboform == 3): 
            Sound.play("sound/robotlostround1.wav").wait()
            Sound.play("sound/robotlostround2.wav").wait()
            man_win += 1
            game_itog = 0
            game+=1
        else: Sound.play("sound/paritet.wav").wait()
    if(tmp_obj.name == 3):
        if(roboform == 2):
            Sound.play("sound/ok.wav").wait()
            game_itog = 1
            put_ok()
            game+=1
            bot_win+=1
        elif(roboform == 1): 
            Sound.play("sound/robotlostround1.wav").wait()
            Sound.play("sound/robotlostround2.wav").wait()
            man_win += 1        
            game_itog = 0
            game+=1
        else: Sound.play("sound/paritet.wav").wait()

    lcd.clear()
    write(bot_win, man_win)

    if(game >= 3):
        game = 0
        if(bot_win > man_win): Sound.play("sound/robotwin.wav").wait()
        else: Sound.play("sound/robotlose.wav").wait()
        bot_win = 0
        man_win = 0
    
        lcd.clear()
        write(bot_win, man_win)
    
        print("\n\n\n NEW GAME \n\n\n")
        Sound.play("sound/newgame.wav").wait()

    else: Sound.play("sound/newround.wav").wait()

    put_stone()

    time2 = datetime.datetime.now()
    delta = time2-time1
    print(delta.seconds)

    first_game = False



Самое популярное