Камень, ножницы, бумага — популярная игра на руках, известная во многих странах мира. Часто используется как методика жеребьёвки для выбора персоны для какой-либо цели (наряду с бросанием монеты, вытягиванием соломинок и т. п.).
Игроки считают вместе вслух «Камень… Ножницы… Бумага… Раз… Два… Три», одновременно качая кулаками. На счёт «Три» они одновременно показывают при помощи руки один из трёх знаков: камень, ножницы или бумагу. Знаки изображены на картинке.
Победитель определяется по следующим правилам:
Камень побеждает ножницы («камень слишком крепок для ножниц»)
Если игроки показали одинаковый знак, то засчитывается ничья и игра переигрывается.
В предыдущем нашем проекте с использованием 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
Последнее время вокруг все больше разговоров об искусственном интеллекте, то там то тут звучат модные термины "нейросети" и "генетические алгоритмы". В прошлых проектах (НейроКачели, НейроБашня и N3uralV1s10n) мы уже создавали простейшие нейронные сети, разобрались с тем что это такое в первом приближении и как они работают. Похоже пришло время сделать тоже самое с генетическими алгоритмами.
Генетический алгоритм - это прежде всего алгоритм эволюционный. Его основная фишка взята из живой природы. При поиске оптимального решения задачи мы порождаем варианты, отбираем из них лучшие, "скрещиваем" между собой, получая решения с общими для "родителей" удачными свойствами.
Для того чтобы пощупать всю эту магию в действии мы применим ее к решению классической задачи робототехники - движению робота по черной линии, а точнее - к подбору параметров ПИД-регулятора для того, чтобы робот смог двигаться по линии быстрее и точнее.
Замечание: данный проект не несет в себе ни оттенка соревновательной составляющей. Наша основная цель не в том, чтобы "вывести" быстрого гонца по линии, мы хотим получить опыт использования алгоритмов генетического типа с целью дальнейшего их использования в близкой нам по духу хоббийной робототехнике.
Начнем с конструкции робота. Это традиционная двухмоторная тележка на базе LEGO Mindstorms NXT, в передней части установлено 4 датчика освещенности, два из которых (внутренних, подключенных к портам 2 и 3) используются ПИД-регулятором робота для движения по линии, а два внешних(подключенных соответственно к портам 1 и 4) - для контроля срыва с линии в процессе обучения. Инструкцию в формате LEGO Digital Designer можно скачать по ссылке.
Для реализации поиска лучших для данной трассы коэффициентов ПИД-регулятора с применением генетического алгоритма нам потребуется создавать в памяти экземпляры ПИД-регулятора и автоматизированно тестировать их на реальном роботе, оценивая результат на соответствие заданному условию - более длинный пройденный путь в единицу времени, соответственно выше скорость движения по линии, при этом срывы с трассы недопустимы.
Алгоритм в общем виде выглядит следующим образом:
Для создания первой популяции виртуальных роботов давайте, для начала, опишем пользовательскую структуру данных, которая будет использоваться в качестве шаблона для создания особей в популяции.
struct person { // у каждой особи должно быть имя, хотя бы codename string name; // номер поколения, в котором родилась данная особь int generation; // энергичность (быстрота) особи int speed; // скорость реакции float reaction; // мудрость (память) особи float memory; // интуиция (проницательность) float intuition; // степень доминантности особи float dominance; // пройденный особью путь за отведенный на тестирование промежуток времени int path; };
Теперь создадим первую популяцию на основе этого шаблона, в ней у нас будет 6 особей:
person robot[6];
Генерируем случайным образом свойства особей первой популяции:
for (int i=0;i<6;i++){ robot[i].name = "GeneLINEr_"+NumToStr(Random(1000)); robot[i].generation=1; // энергичность (быстрота) особи 0..100 robot[i].speed = Random(70)+30; // скорость реакции 0..3 robot[i].reaction = Random(3000)/1000; // мудрость (память) особи 0..0,1 robot[i].memory = Random(100)/1000; // интуиция (проницательность) 0..3 robot[i].intuition = Random(3000)/1000; // степень доминантности особи не понятна до тестирования robot[i].dominance = 0; // пройденный особью жизненный путь = 0 robot[i].path = 0; }
Далее начинается самое интересное. Начинаем условно бесконечный цикл смены поколений. В каждом поколении нам нужно испытать особей данного поколения и выявить из них самых быстрых и точных, не слетающих с трассы.
В роботе реализована функция ПИД-регулятора движения по линии, принимающий на вход параметры Kp, Ki, Kd, скорость робота, выполняющая 5 секундное движение по линии и возвращающая длину пройденного пути, сглаженного до криволинейной траектории.
long pid(float Pk,float Ik,float Dk,int speed){ long B=0; long C=0; long path=0; long MC=MotorRotationCount(OUT_C); long MB=MotorRotationCount(OUT_B); long e = 0; int porog=28; float ERRo=0; float ERR=0; float u=0; float z1=0; float z2=0; long tmp=CurrentTick(); int p; int i; int d; while(CurrentTick()-tmp<=5000){ MC=MotorRotationCount(OUT_C); MB=MotorRotationCount(OUT_B); ERR=Sensor(IN_3)-Sensor(IN_2); p=Pk*ERR; d=Dk*(ERR-ERRo); i=Ik*e; if(i>10)i=10; if(i<-10)i= -10; u=p+i+d; z1=speed-u; z2=speed+u; if(speed-u>100)z1=100; if(speed-u<-100)z1=-100; if(speed+u>100)z2=100; if(speed+u<-100)z2=-100; OnFwd(OUT_B,z1); OnFwd(OUT_C,z2); if(Sensor(IN_1)<=porog){ PlayTone(TONE_C5, MS_500); RotateMotorEx(OUT_BC, 35, 100, 100, true, true); go_to_line(); break; } if(Sensor(IN_4)<=porog){ PlayTone(TONE_C5, MS_500); RotateMotorEx(OUT_BC, 35, 100, -100, true, true); go_to_line(); break; } ERRo=ERR; e+=ERR; B=MotorRotationCount(OUT_B)-MB; C=MotorRotationCount(OUT_C)-MC; if(B>0 && C>0){ if(B<C){ path+=B; } else{ path+=C; } } } Off(OUT_BC); return path; } void go_to_line(){ float P=1.0; float D=1.0; float ERRo=0; float ERR=0; float u=0; while(abs(Sensor(IN_2)-Sensor(IN_3))>5){ int ERR=Sensor(IN_3)-Sensor(IN_2); int u=P*ERR+D*(ERR-ERRo); int z1=-u; int z2=+u; if(z1>100) z1=100; if(z1<-100) z1=-100; if(z2>100) z2=100; if(z2<-100) z2=-100; OnFwd(OUT_B,z1); OnFwd(OUT_C,z2); }
}
В случае, если в процессе 5-секундного испытания особи один из внешних датчиков видит линию, считается что робот сошел с трассы. При этом функция возвращает его на линию и особь выбывает из тестирования с результатом, который успела набрать до срыва, как правило рейтинг данной особи будет низким.
for (int i=5;i>=0;i--){ robot[i].path = pid(robot[i].reaction, robot[i].memory,robot[i].intuition,robot[i].speed); PlayTone(TONE_A4, MS_500); }
После испытания всех особей текущего поколения ранжируем их в порядке убывания пройденного за время тестирования пути. Так как время на тест для каждой особи фиксировано - 5 секунд, соответственно у особей убывает и скорость. Будем использовать пузырьковую сортировку и "временную особь", для перестановки пар при ранжировании.
person robot_tmp;
for (int i=0;i<5;i++){ for (int j=0;j<(5-i);j++){ if(robot[j].path < robot[j+1].path){ robot_tmp = robot[j]; robot[j]=robot[j+1]; robot[j+1]= robot_tmp; } } }
Чем выше у особи рейтинг, тем выше и доминантность данной особи, соответственно тем большую часть свойст данной особи унаследуют ее потомки (выше = ближе к 1):
for (int i=0;i<6;i++){ robot[i].dominance=i+1; }
Две особи, самые слабые в популяции (5 и 6 в ранжированном списке) умирают, остальные дают потомство, порождая 6 особей новой популяции, наследующих черты родительских особей. В скрещивании участвуют доминантные признаки особей, давая соотношение унаследованных признаков. Унаследованный признак новорожденного кроме этого подвергается колебанию в 20%, для ускорения эволюции.
Теперь в дело вступает природа и у одной, случайной особи происходит мутация - ее один, случайный, признак изменяется. Мутация чаще всего приводит к появлению неконкурентоспособных или вообще нежизнеспособных особей, однако очень полезна в ситуации, когда вектор развития эволюции пошел изначально не в том направлении. В результате мутации может появиться особь со свойством, выигрышно выделяющим ее на фоне остальных. Такая особь сразу же окажется на вершине рейтинга и даст начало новой династии.
// Мутация void mutants(){ int property=Random(3); int mutant=Random(5);; if(mutant<4){ switch(property){ case 0: robot_next_generation[mutant].speed = max_speed+5; break; case 1: robot_next_generation[mutant].reaction =robot_next_generation[mutant].reaction *((Random(40)+80)/100.0); break; case 2: robot_next_generation[mutant].memory =robot_next_generation[mutant].memory *((Random(40)+80)/100.0); break; case 3: robot_next_generation[mutant].intuition = robot_next_generation[mutant].intuition *((Random(40)+80)/100.0); break; default: break; } } else{ switch(property) { case 0: robot_next_generation[mutant].speed = max_speed+5; break; case 1: robot_next_generation[mutant].reaction = Random(3000)/1000; break; case 2: robot_next_generation[mutant].memory = Random(100)/1000; break; case 3: robot_next_generation[mutant].intuition = Random(3000)/1000; break; default: break; } } }
Теперь производим смену популяций. Дети сменяют родителей:
Теперь снова пришла пора испытать новое поколение, более приспособленное к решению поставленной задачи, оценить возросшую скорость,, выявить сильнейших и так до тех пор, пока результат не будет нас устраивать.
На этапе ранжирования можно сохранять информацию о свойствах особей в каждой популяции в файл.
По данным, накопленным в файле в процессе работы программы, можно представить ход эволюции в виде наглядного графика (ось X - номер поколения, ось Y - средняя скорость особи):