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

N3uralV1s10n

Камера, подключенная к LEGO Mindstorms EV3 уже давно не является чем-то необычным. Конечно, в комплекте с набором ее нет, да и стандартное ПО от LEGO лишено возможности ее использования, но с появлением "прошивок" от сторонних разработчиков, таких как ev3dev и leJOS, появилась возможность подключить практически любую современную веб-камеру с USB-интерфейсом.
В нашем сегодняшнем проекте мы будем использовать камеру в качестве элемента системы машинного зрения, запрограммировав на Python простейшую нейронную сеть для распознавания образов.


Python - современный, активно развивающийся язык программирования, для него существует множество готовых модулей для решения задач, связанных с машинным зрением, включая популярный OpenCV. Однако наша цель состоит именно в написании учебного алгоритма на основе  нейронной сети, без использования готовых профессиональных библиотек, с тем чтобы разобраться "как это работает?".

Для получения информации с камеры мы будем использовать легковесный (по сравнению с OpenCV) модуль PyGame. Он не установлен в ev3dev "из коробки", но его можно доустановить используя менеджер модулей pip.

Конструкция у робота незамысловатая, по сути это крепление для камеры и листа бумаги А4, но тем не менее мы традиционно выкладывает инструкцию по ее сборке в формате LEGO Digital Designer, скачать ее можно по ссылке.


Мы используем камеру Logitech C110, это простейшая веб-камера с разрешением 640x480, которая имеет поддержку со стороны Linux. В конструкции используется пара датчиков-кнопок подключенных к 1 и 4 портам - они используются для "поощрения" и "наказания" нейронной сети в процессе обучения, и означают, соответственно, "Да" и "Нет".

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

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

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

В силу довольно скромной производительности блока EV3 мы не будем работать с полным разрешением камеры, мы снизим его в программе до 16x16 пикселов, что вполне достаточно для решения учебной задачи.



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

Первый алгоритм заключается в следующем:
1) В памяти робота перечисляются сущности, которые он сможет отличать друг от друга. Для каждой сущности создается нейрон сети с 16x16=256 входами и 1 выходом. Веса на входах нейронов в начале одинаковы у всех входов и всех нейронов.
2) Роботу показывается сущность из числа тех, которые перечислены в его памяти, он пытается угадать что это такое. Поначалу, конечно, он в большинстве случаев ошибается. Человек нажимает кнопку "Да", если робот угадал (в этом случае мы увеличиваем веса входов соответствующего нейрона, на которых были не белые пиксели), и кнопку "Нет", если не угадал (в этом случае уменьшаем веса входов соответствующего нейрона с закрашенными пикселами).
3) Робот пытается угадать снова и пересчитывает веса на входах нейронов до тех пор, пока не научится стабильно распознавать все сущности из имеющегося у него списка.

Код первой программы на Python выглядит следующим образом::

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

lcd = Screen()
btn = Button()

res = 16

S1 = TouchSensor("in1")
S2 = TouchSensor("in4")

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

class number:
    def __init__(self, n):
        self.name = n
        self.sum = 0
        self.picture = [ [0] * res for i in range(res)]
               
myNumbers = [number(1), number(2), number(3), number(4)]

def camera_update(x):
    for i in range(x):
         
        image = cam.get_image()
        
        image = pygame.transform.scale(image,(res,res))
        image2buf(image)
        for i in range(res):
            for j in range(res):
                if buf[i][j] == 0:
                    lcd.draw.rectangle((i*8+25, j*8, i*8+7+25, j*8+7),fill='white')
                else:
                    lcd.draw.rectangle((i*8+25, j*8, i*8+7+25, j*8+7),fill='black')
        lcd.update()

def write(n):
    f = ImageFont.truetype('FreeMonoBold.ttf', 175)
    lcd.draw.text((30,-15), str(n), 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 > 100:
                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])
cam.start()

f = ImageFont.truetype('FreeMonoBold.ttf', 25)
lcd.draw.text((0,50), "N3uralV1s10n", font=f)
lcd.update()
Sound.speak("nerual vision programm 1").wait()
time.sleep(2)

lcd.clear()

str1 = "please put"
str2 = "first object"
str3 = "and press enter"
lcd.draw.text((0,30), str1, font=f)
lcd.draw.text((0,55), str2, font=f)
f = ImageFont.truetype('FreeMonoBold.ttf', 20)
lcd.draw.text((0,80), str3, font=f)
lcd.update()
Sound.speak("please put first object and press enter").wait()

while(True):
    if(btn.enter): break
lcd.clear()

while(True):
    camera_update(15)
     
    image = cam.get_image()
    
    image = pygame.transform.scale(image,(res,res))
    image2buf(image)
     
    for i in range(res):
        for j in range(res):
            if buf[i][j] == 0:
                lcd.draw.rectangle(((i+25)*8, j*8, (i+25)*8+7, j*8+7),fill='white')
            else:
                lcd.draw.rectangle(((i+25)*8, j*8, (i+25)*8+7, j*8+7),fill='black')

    lcd.update()
    
    for o in myNumbers:
        o.sum = 0
    for o in myNumbers:    
        for i in range(res):
            for j in range(res):
                o.sum += buf[i][j] * o.picture[i][j]

    max_sum = -100000

    for num in myNumbers:
        if num.sum > max_sum:
            max_sum = num.sum
            tmp_obj = num
    
    lcd.clear()
    write(tmp_obj.name)
    
    Sound.speak("It is "+str(tmp_obj.name)).wait()
    while(True):
        if(S1.value()): break
        if(S2.value()): break

    if(S1.value()): 
        Sound.speak("ok yes").wait()
        a = 1
    else: 
        Sound.speak("no no").wait()
        a = -1
        
    for i in range(res):
        for j in range(res):
            if(buf[i][j] == 1):
                tmp_obj.picture[i][j] += a
   
    Sound.speak("put a new object and press enter").wait()
    while(True):
        if(btn.enter): break
        if(btn.backspace):
           Sound.speak("Exit programm").wait() 
           exit()    
    lcd.clear()

cam.stop()   

Второй алгоритм несколько отличается:
1) Изначально память робота пуста.
2) Показываем ему объект и нажимаем кнопку "Запомни эту сущность".
3) Выбираем имя для объекта кнопками на блоке.
4) В памяти робота формируется нейрон с 16x16=256 входами, при этом веса входов, которые видят закрашенные пиксели выше, чем входов с белыми пикселами.
5) показываем роботу следующий объект, он пытается сопоставить его с теми, что уже знает.
6) если робот угадал, поощряем его, нажимая "Да" (выполнится усиление связей с пересчетом весов на входах соответствующего нейрона). Если робот не угадал уже знакомый ему объект, нажимаем "Нет" (ослабляем связи), если объект новый для робота - нажимаем "Запомни эту сущность" и переходим к п. 2

Код второй программы на Python выглядит так::

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

lcd = Screen()
btn = Button()

res = 16

S1 = TouchSensor("in1")
S2 = TouchSensor("in4")

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

class number:
    def __init__(self, n):
        self.name = n
        self.sum = 0
        self.picture = [ [0] * res for i in range(res)]
               

myNumbers = []

def camera_update(x):
    for i in range(x):
         
        image = cam.get_image()
        
        image = pygame.transform.scale(image,(res,res))
        image2buf(image)
        for i in range(res):
            for j in range(res):
                if buf[i][j] == 0:
                    lcd.draw.rectangle(((i*8+25), j*8, (i*8+25)+7, j*8+7),fill='white')
                else:
                    lcd.draw.rectangle(((i*8+25), j*8, (i*8+25)+7, j*8+7),fill='black')
        lcd.update()

def write(n):
    f = ImageFont.truetype('FreeMonoBold.ttf', 175)
    lcd.draw.text((30,-15), str(n), 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 > 100:
                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])
cam.start()

lcd.clear()

f = ImageFont.truetype('FreeMonoBold.ttf', 25)
lcd.draw.text((0,50), "N3uralV1s10n", font=f)
lcd.update()
Sound.speak("neural vision programm 2").wait()
time.sleep(2)
lcd.clear()

str1 = "please put"
str2 = "first object"
str3 = "and press enter"
f = ImageFont.truetype('FreeMonoBold.ttf', 25)
lcd.draw.text((0,30), str1, font=f)
lcd.draw.text((0,55), str2, font=f)
f = ImageFont.truetype('FreeMonoBold.ttf', 20)
lcd.draw.text((0,80), str3, font=f)
lcd.update()
Sound.speak("please put first object and press enter").wait()

while(True):
    if(btn.enter): break
lcd.clear()

while(True):
    camera_update(15)
     
    image = cam.get_image()
    
    image = pygame.transform.scale(image,(res,res))
    image2buf(image)
        
    for i in range(res):
        for j in range(res):
            if buf[i][j] == 0:
                lcd.draw.rectangle(((i+25)*8, j*8, (i+25)*8+7, j*8+7),fill='white')
            else:
                lcd.draw.rectangle(((i+25)*8, j*8, (i+25)*8+7, j*8+7),fill='black')

    lcd.update()
    
    for o in myNumbers:
        o.sum = 0
    for o in myNumbers:    
        for i in range(res):
            for j in range(res):
                o.sum += buf[i][j] * o.picture[i][j]

    max_sum = -100000
    
    for num in myNumbers:
        if num.sum > max_sum:
            max_sum = num.sum
            tmp_obj = num
    
    lcd.clear()
    
    if(len(myNumbers)!=0): 
        write(tmp_obj.name)
    
        Sound.speak("It is "+str(tmp_obj.name)).wait()
    else: Sound.speak("I do not know object").wait()
    while(True):
        if(S1.value() and len(myNumbers)!=0): break
        if(S2.value() and len(myNumbers)!=0): break
        if(btn.enter or len(myNumbers)==0): break
    a=0
    if(S1.value() and len(myNumbers)!=0): 
        Sound.speak("ok yes").wait()
        a = 1
    elif(S2.value() and len(myNumbers)!=0): 
        Sound.speak("no no").wait()
        a = -1
    else: 
        Sound.speak("new object").wait()
        time.sleep(1)
        i = 48
        while(True):
            if(btn.enter): break
            if(btn.right): i+=1
            if(btn.left): i-=1
            if(i>90): i=48
            if(i<48): i=90
            if(i>=58 and i<=64): 
                if(btn.right): i=65
                else: i=57
            lcd.clear()
            
            time.sleep(0.15)
            write(chr(i))
        myNumbers.append(number(chr(i)))
        Sound.speak("new object it is" + chr(i)).wait()
        for i in range(res):
            for j in range(res):
                myNumbers[len(myNumbers)-1].picture[i][j] = buf[i][j]
    if(a!=0):
        for i in range(res):
            for j in range(res):
                if(buf[i][j] == 1):
                    tmp_obj.picture[i][j] += a
   
    Sound.speak("put a new object and press enter").wait()
    while(True):
        if(btn.enter): break
        if(btn.backspace): 
           Sound.speak("Exit programm").wait()
           exit()
    lcd.clear()

cam.stop()   

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

Matrix has you... или малая офисная автоматизация

"Вот две таблетки: красная и синяя...Если ты возьмешь красную таблетку, то окажешься в стране чудес и я покажу тебе, как глубоко вниз уходит кроличья нора.”Выберешь синюю - забудешь. все что увидел здесь..."



Arduino, Raspberry Pi, китайский брат ее Orange и другие прикольные штуковины зачастую используются энтузиастами в оторванных от жизни проектах. Однако иногда жизнь сама подкидывает интересные задачки, в которых их можно с успехом применить.

После переезда в новый офис на работе у руководителя команды "Карандаш и Самоделкин" встал вопрос об использовании старого олдскульного сканера, не имеющего LAN-порта. Никто не жаждал цеплять его по USB к своему компьютеру и день за днем развлекаться в стиле "Чувак, мне тут пару листочков надо отсканировать, поможешь? Кинь скан на почту?".


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

Дело за малым - нужна кнопка, лучше всего USB-шная. Arduino Micro, которая умеет притворяться USB-устройствами? Слишком жирно для такой задачи. Ее младший брат - Digispark - вот один из  самых бюджетных вариантов, которого достаточно для реализации задумки.

Плата имеет 6 портов ввода-вывода, стоит у китайцев всего 50 рублей. На борту 8 КБ памяти для программного кода, частота камня от 1 МГц до 20 МГц, питание от 1,8В до 35В (в зависимости от модификации).

Для проекта также пригодилась пара кнопок, светодиод, несколько сопротивлений, динамик от сломанных наушников и провода. В качестве корпуса - футляр от панельки еще более олдскульной кассетной автомагнитолы Nakamichi. Ребята из "Карандаша и Самоделкина" за выходные спаяли все это в кучу и вышло вот что:



Устройство подключается по USB к компьютеру со сканером. Алгоритм работы девайса следующий:
1. Человек, которому нужно отсканировать документы, нажимает красную кнопку "Начать сканирование/Следующий лист".
2. Устройство издает торжествующе-подтверждающий писк, включает светодиод и, притворившись USB-клавиатурой, посылает на компьютер комбинацию Ctrl-Shift-2. Включенный светодиод означает что сканирование не завершено.
3. На компьютере по данной комбинации запускается в фоне программа на Python (код ниже), которая, используя вызов NAPS2, сканирует лист и сохраняет во временную папку.
4. Человек может повторить п. 1-3 если хочет добавить листы в тот же PDF-документ, иначе нажимает синюю кнопку "Завершить сканирование".
5. Устройство снова пищит, выключает светодиод и посылает на компьютер комбинацию Ctrl-Shift-3.
6. На компьютере по данной комбинации запускается в фоне вторая программа на Python (код также ниже), которая объединяет все листы из временной папки в единый PDF-документ на сетевом диске, добавив в имя файла дату и время.
7. Человек идет на свое рабочее место и забирает файл с сетевого диска. Радуется, что ему не пришлось "просить сканировать".

Экономический эффект потрясающий. Если посчитать грубо, то на ручное сканирование 1 листа человеку. "хозяину-сканера", раньше требовалось затратить около 2-3 минут с учетом предварительного просмотра документа, ручного сохранения на сетевой диск или отправку по почте. В неудачный день он мог бы сканировать для приходящих полчаса, отвлекаясь от своей работы. Даже самая неточная прикидка дает срок окупаемости всего "железа" в несколько часов.

Код для Digispark у нас получился вот такой:

#include "DigiKeyboard.h"

#define MY_OUT 2 
#define MY_KEY1 0
#define MY_KEY2 1

bool trigger = false;

void setup() { 
  pinMode(MY_OUT, OUTPUT); 
  pinMode(MY_KEY1, INPUT);
  pinMode(MY_KEY2, INPUT);

  tone(MY_OUT,659,500);
}

void loop() {
  DigiKeyboard.update();

  if (digitalRead(MY_KEY1) == true  && digitalRead(MY_KEY2) == false){
    DigiKeyboard.sendKeyStroke(KEY_2, MOD_SHIFT_LEFT | MOD_CONTROL_LEFT);
    tone(MY_OUT,440);         
    DigiKeyboard.delay(500);
    noTone();
    digitalWrite(MY_OUT,true);    
    DigiKeyboard.delay(5000);
    trigger = true;
  }

  if (digitalRead(MY_KEY2) == true && digitalRead(MY_KEY1) == false && trigger){        
    DigiKeyboard.sendKeyStroke(KEY_3, MOD_SHIFT_LEFT | MOD_CONTROL_LEFT);
    tone(MY_OUT,523);
    DigiKeyboard.delay(500);
    noTone();
    digitalWrite(MY_OUT,false);    
    DigiKeyboard.delay(5000);
    trigger = false;
  }
  
  DigiKeyboard.delay(100);
}

Программа на Python, которая запускается по красной кнопке на ПК выглядит так:

# сканирование одного изображения во временную сетевую папку

import os, time
import datetime

# каталог, куда будут сохраняться отсканированные изображения
dir = r'O:\scan\tmp'
# путь к NAPS2
NAPSexe = r'C:\"Program Files (x86)"\NAPS2\NAPS2.Console.exe'
# имя временного файла
tmpPdf = "scan" # .pdf

try:
    files = os.listdir(dir)
except OSError:
    print("Сетевой путь не найден! Скорректируйте переменную dir")    
    time.sleep(60)

now_time = datetime.datetime.now()

print(now_time.strftime("%d.%m.%Y %I:%M:%S")," Идет сканирование... ")

n = 1

# смотрим сколько файлов уже отсканировано
files = os.listdir(dir)
for i in files:
    n += 1

# Сканируем одно изображение в pdf
fullNamePdf = dir + "\\"+tmpPdf+str(n)+".pdf"
os.system(NAPSexe + r' --force -o '+fullNamePdf)

print(now_time.strftime("%d.%m.%Y %I:%M:%S")," Сканирование завершено... ")

"Синяя" кнопка запускает программу на Python следующего содержания:

# сборка отсканированных страниц в многостраничный документ

import os, time
import datetime

# каталог, откуда будут забираться постранично отсканированные изображения
dirIn = r'O:\scan\tmp'
# каталог, куда будет собираться многостраничный документ
dirOut = r'O:\scan\docs'
# путь к NAPS2
NAPSexe = r'C:\"Program Files (x86)"\NAPS2\NAPS2.Console.exe'
# имя временного файла
tmpPdf = "scan" # .pdf

try:
    files = os.listdir(dirIn)
except OSError:
    print("Сетевой путь не найден! Скорректируйте переменную dirIn")    
    time.sleep(60)

try:
    files = os.listdir(dirOut)
except OSError:
    print("Сетевой путь не найден! Скорректируйте переменную dirOut")    
    time.sleep(60)

now_time = datetime.datetime.now()

print(now_time.strftime("%d.%m.%Y %I:%M:%S")," Идет сборка... ")

# Смотрим сколько страниц во временной папке
files = os.listdir(dirIn)
i = 0
cmd = ""
for scan in files:
    i += 1
    cmd += dirIn+"\\"+scan
    if i < len(files):
        cmd += ";"

fullNamePdf = dirOut + "\\scan-"+ now_time.strftime("%Y-%m-%d-%I-%M-%S") + ".pdf"
os.system(NAPSexe + r' -i ' + cmd + r' --force -n 0 -o '+fullNamePdf)

print(now_time.strftime("%d.%m.%Y %I:%M:%S")," Сборка завершена... ")
print(now_time.strftime("%d.%m.%Y %I:%M:%S")," Удяляем временные файлы... ")

for scan in files:
    os.remove(dirIn+"\\"+scan)

 Вот такая вышла малая офисная автоматизация, а цвета кнопок, напоминающий красную и синюю таблетки в всем известном фильме, дали название нашему проекту.

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

НейроБашня

В 94-м проекте мы построим робота, который будет играть с нами в известную игру "11 палочек". В этот раз мы снова коснемся темы нейросетевых технологий. Нейронная сеть робота сможет обучаться как в результате игры с человеком, реагируя на его стратегию, так и в результате "игры в уме", играя в памяти с себе подобной сетью.

Суть игры такова: в кучке 11 палочек, два игрока ходят по очереди. За каждый ход можно взять 1 или 2 палочки. Выигрывает тот, кто взял последнюю палочку.

Вместо палочек наш робот будет работать с лего-кубиками. 11 блоков из кубиков, сложенных друг на друга, образуют высокую башню, что и дало название нашему проекту, "НейроБашня".



В этот раз мы построили конструкцию из старого доброго LEGO Mindstorms NXT. Это связано с тем, что параллельно с Python мы изучаем C/C++, а для платформы NXT есть замечательная свободно распространяемая среда разработки BricxCC, Си-компилятором в которой мы и воспользуемся для программирования робота.

Инструкцию по сборке нашего робота в формате LEGO Digital Designer можно скачать по ссылке. В конструкции мы используем средний EV3-мотор в силу его компактности, однако вам ничего не помешает использовать NXT-мотор, изменив способ его крепления, изменений в коде это не потребует.


Робот может работать в двух режимах:

  • Игра на двоих. В этом режиме играют два человека. В башню заряжено 11 лего-кубиков. Игроки по очереди нажимают 1 или 2 раза на кнопку со своей стороны, забирая себе 1 или 2 кубика. Выигрывает игрок, который забрал последний кубик.
  • Игра человека против нейронной сети робота. В этом режиме игрок использует правую кнопку, за левого игрока думает робот.
Для объяснения алгоритма обучения нейронной сети робота давайте снова воспользуемся терминологией коробков и камушков, как доступной для пониманию робототехниками любого возраста.

Так как у нашего робота 11 состояний (по количеству оставшихся кубиков в башне), представим 11 коробков, в каждом из которых в начале игры по 1 камню белого и черного цветов. Когда наступает ход нейронной сети, она "смотрит" сколько кубиков осталось в башне и выбирает соответствующий коробок, затем наугад вытаскивает из него камень. Если он белый - она берет 2 кубика, если черный, то 1 кубик, при этом запоминая сколько она взяла в каждый ход. После того как камень вытащен и его цвет определен, он помещается назад в коробок.
По окончанию игры наступает пора поощрений и наказаний:

  • Если нейронная сеть выиграла, она добавляет в каждый задействованный ею коробок камень того цвета, который она вытащила, делая соответствующий ход. 
  • Если нейронная сеть проиграла, то она убирает из последнего задействованного ею коробка камень того цвета, который она вытащила. При этом, если камень этого цвета последний, то он остается в коробке.
Этот способ был опубликован в серии книг «Берсеркер» (издавались в рамках «Боевая фантастика») Фреда Саберхагена в 1995 году. По сюжету, главный герой должен был замаскировать своё отсутствие на космокорабле. Он обучил методу «спичечных коробков» свою мартышку, которая и имитировала игру с жутким космическим монстром. 
Ранее, в 70-x годах задача «Самообучающаяся машина из спичечных коробков» была опубликована известным математиком Мартином Гарднером в книге "Математические досуги".

В самом начале игры наша нейронная сеть не обучена, ее ходы случайны, как как в каждом "коробке" у нее 50% белых и 50% черных камней. Каждый такой коробок - это нейрон сети.
По мере игры с человеком нейронная сеть набирается опыта, подстраивается под стратегии игрока, не давая реализовывать ему выигрышные комбинации. Такой обучение эффективно в плане выстраивания качественных взаимосвязей между нейронами сети, так как сеть играет с думающим соперником, однако длится оно может очень долго. Сеть начнет играть на уровне игрока только через несколько часов игры. Мы реализовали такой метод обучения нейронной сети самым первым, но использование только его нас не устроило в силу трудоемкости обучения робота.
Второй способ обучения, который мы попробовали, стало обучение игре с виртуальным игроком, делающим случайные ходы. Параллельно игре с человеком в памяти робота проводятся непрерывная последовательность игр нейронной сети, которую нужно обучить, с генератором чисел 1 и 2. Этот способ малоэффективен в силу того, что генератор "не думает", однако из-за того что таких игр проводится много (более сотни каждую секунду), сумма различных игровых ситуаций и исходов игр в итоге приводят к образованию связей, необходимых для выигрыша нейронной сети. Такая сеть начинает играть сильнее человека через полчаса, но нас результат снова не устроил и мы двинулись дальше.
Третьим способом, который мы применили, явилось создание еще одной нейронной сети и проведение непрерывной последовательности фоновых игр между сетями. Обе сети обучаются одновременно, по каждый ведется статистика побед.. Этот способ оказался самым эффективным, правда удвоенная вычислительная нагрузка снизила количество фоновых игр (теперь их 70-80 в секунду), однако качество обучения заметно выросло.

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

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



Исходный код программы приведен ниже. Скачать программу можно по ссылке. Мотор подключается в порт А, датчики кнопки - в порты 1 и 2. Чтобы озвучить игры мы записали набор звуков в формате RSO, которые нужно поместить в память робота, их вы тоже найдете по ссылке.

//время ожидания второго нажатия
long timeToPress=0;

//переменные с количеством выигрышей нейросетей
long winNN1=0;
long winNN2=0;

//количество игр NN против NN (здесь и далее NN - нейросеть)
long game=0;

//сколько осталось кубиков в игре с человеком
int box1 = 0;
//сколько осталось кубиков в игре NN с NN
int box2=0;

//win это переменная обозначает чей-то выйгрыш
//win2 это тоже самое, только в подпроцессе  learn
int win1=0;
int win2=0;

//7 строк обозначают 7 строк экрана, мы в них храним то, что надо вывести
string line1,line2,line3,line4,line5,line6,line7;

//buttonPress это количество нажатий на кнопку
int buttonPress=0;

//ошибка и старая ошибка PD-регулятора
int ERR = 0;
int ERRo=0;

//управляющее воздействие
int u = 10;

//коФф-ты PD регулятора
float P = 0.5;
float D = 1.5;

//точка, которой мы хотим достичь при повороте мотора
int target=0;

//время для поворота влево или вправо
long timeToMove=0;

//два массива пуговиц NN1
int white1[] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
int black1[] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};

//два массива пуговиц NN2
int white2[] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
int black2[] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};

//три временных массива 2 для learn и 1 для main
int tmp1[11];
int tmp2[11];
int tmp[11];

//блок для поворота по регулятору налево
sub Left (int raz)
{
  //устонавливаем точку, которой хотим достичь
  target=MotorRotationCount(OUT_A)+360*raz;

  //засекаем время
  timeToMove=CurrentTick();

  //цикл, работоющий пока мы не достигнем точки или времени
  while(abs(MotorRotationCount(OUT_A)-target)>5 && CurrentTick()-timeToMove < 1000){
    //с помощью регулятора подаем нагрузку на мотор
    ERR=target-MotorRotationCount(OUT_A);
    u=P*ERR+D*(ERR-ERRo);
    if(u>100)u=100;
    if(u<-100)u=-100;
    OnFwd(OUT_A,u);
    ERRo=ERR;
  }

  //выключаем мотор
  Off(OUT_A);
}

//блок для поворота по регулятору направо
sub Right(int raz)
{
  //устанавливаем точку, которой хотим достичь
  timeToMove=CurrentTick();
  
  //засекаем время
  target=MotorRotationCount(OUT_A)-360*raz;

  //цикл, работающий пока мы не достигнем точки или времени
  while(abs(MotorRotationCount(OUT_A)-target)>5 && CurrentTick()-timeToMove < 1000){
    //с помощью регулятора подаем нагрузку на мотор
    ERR=target-MotorRotationCount(OUT_A);
    u=P*ERR+D*(ERR-ERRo);
    if(u>100)u=100;
    if(u<-100)u=-100;
    OnFwd(OUT_A,u);
    ERRo=ERR;
  }

  //выключаем мотор
  Off(OUT_A);
}

//функция для подсчета нажатий на кнопки
int touch(int sensor) {
  //переменная, в которой мы храним количевство нажатий
  int countPress=0;
  //условие, проверяющее на каком датчике ждать нажатия
  if(sensor==1){
    //ожидание первого нажатия
    while(Sensor(S1)==0){
      Wait(1);
    }
    //защина от шума
    Wait(100);
    //ожидаем отжатия
    while(Sensor(S1)!=0);
    long timeToPress=CurrentTick();
    //цикл, ожидающий второго нажатия заданое время
    while(Sensor(S1)==0 && CurrentTick()-timeToPress < 1000);
    //если вылетели не по времени, а по нажатию
    if(CurrentTick()-timeToPress < 1000) {
      //зашита от шума
      Wait(100);
      //устанавливаем количество нажатий в 2(0 - одно нажитие, 1 - два нажатие)
      countPress=1;
      //ожидаем отжатия
      while(Sensor(S1)!=0);
    }
    //возвращаем значение
    return countPress+1;
  }
  //если проверяется второй датчик
  if(sensor==2){
    //ожидаем нажатия
    while(Sensor(S2)==0){
      Wait(1);
    }
    //защита от шума
    Wait(100);
    //ожидание отжатия
    while(Sensor(S2)!=0);
    long timeToPress=CurrentTick();
    //цикл, ожидающий второго нажатия заданое время
    while(Sensor(S2)==0 && CurrentTick()-timeToPress < 1000);
    //если вылетели не по времени, а по нажатию
    if(CurrentTick()-timeToPress < 1000) {
      //защита от шума
      Wait(100);
      //устанавливаем количество нажатий в 2
      countPress=1;
      //ожидаем отжатия
      while(Sensor(S2)!=0);
    }
    //возвращаем значение
    return countPress+1;
  }

// параллельный процесс для обучения нейронных сетей
}
task learn(){
  Wait(2000);
  //говорим "начало обучения нейронной сети"
  PlayFile("start.rso");
  Wait(2000);
  //запоминаем время
  long timeToLearn = CurrentTick();
  //главный цикл
  while(true)
  {
    //обнуляем tmp1 и tmp2
    for(int i=0;i<=10;i++){
      tmp1[i]=0;
      tmp2[i]=0;
    }
    //если прошла секунда? (каждую секунду)
    if(CurrentTick()-timeToLearn>=1000){
      //вывод на экран  количевства игр и выигрышей
      line1=NumToStr(game)+" "+NumToStr(winNN1)+" "+NumToStr(winNN2);
      //вывод на экран выигрывающей нейросети
      if(winNN1>=winNN2){
        line2="HC1";
      }
      else{
        line2="NC2";
      }
      //вывод строк
      ClearScreen();
      TextOut(0,LCD_LINE1,line1);
      TextOut(0,LCD_LINE2,line2);
      TextOut(0,LCD_LINE3,line3);
      TextOut(0,LCD_LINE4,line4);
      TextOut(0,LCD_LINE5,line5);
      TextOut(0,LCD_LINE6,line6);
      TextOut(0,LCD_LINE7,line7);
      //очистка строк
      line1 = "";
      line2 = "";
      line4 = "";
      line6 = "";
      line7 = "";
      //перезаписываем время
      timeToLearn = CurrentTick();
    }
    //увеличиваем число игр
    game++;
    //переменная кто выиграл
    win2=0;
    //сколько кубиков осталось
    box2=11;

    //цикл игры
    while(true){

      //ход НС2
      //вытягиваем камень
      if(Random(100) < white2[box2-1]*1.0/(white2[box2-1]+black2[box2-1])*100.0){
        //запоминаем какой использовали коробок
        tmp2[box2-1]=2;
        //уменьшаем количество оставшихся кубиков в кучке
        box2-=2;
      }
      else{
        //запоминаем какой использовали коробок
        tmp2[box2-1]=1;
        //уменьшаем количество оставшихся кубиков в кучке
        box2-=1;
      }
      //проверка на выигрыш
      if(box2<=2){
        win2=1;
        break;
      }

      //ход НС1
      //вытягиваем камень
      if(Random(100) < white1[box2-1]*1.0/(white1[box2-1]+black1[box2-1])*100.0){
        //запоминаем какой использовали коробок
        tmp1[box2-1]=2;
        //уменьшаем количество оставшихся кубиков в кучке
        box2-=2;
      }
      else{
        //запоминаем какой использовали коробок
        tmp1[box2-1]=1;
        //уменьшаем количество оставшихся кубиков в кучке
        box2-=1;
      }
      //проверка на выигрыш
      if(box2<=2){
        win2=2;
        break;
      }

    }
    //поощрение-наказание
    //NN1 выиграла
    if(win2==1){
      winNN1++;
      for(int i = 0;i<=10;i++){
        if(tmp1[i]==1) black1[i]++;
        if(tmp1[i]==2) white1[i]++;
      }
    }
    else{
      for(int i = 0;i<=10;i++){
        if (tmp1[i] > 0)
        {
          if(tmp1[i]==1 && black1[i]>1) black1[i]--;
          if(tmp1[i]==2 && white1[i]>1) white1[i]--;
          break;
        }
      }
    }
    //NN2
    if(win2==2){
      winNN2++;
      for(int i = 0;i<=10;i++){
        if(tmp2[i]==1) black2[i]++;
        if(tmp2[i]==2) white2[i]++;
      }
    }
    else{
      for(int i = 0;i<=10;i++){
        if (tmp2[i] > 0)
        {
          if(tmp2[i]==1 && black2[i]>1) black2[i]--;
          if(tmp2[i]==2 && white2[i]>1) white2[i]--;
          break;
        }
      }
    }
  }
}

task main()
{
  //подключаем датчики
  SetSensorTouch(IN_1);
  SetSensorTouch(IN_2);
  ClearScreen();
  line1 = "";
  line2 = "";
  line3 = "";
  line4 = "";
  line5 = "";
  line6 = "";
  line7 = "";

  //делаем случайные числа действительно случайными
  for(int i=0; i<=CurrentTick()%1000; i++) { int tmp_r = Random(); }

  //сбрасываем tmp
  for(int i=0;i<=10;i++){
    tmp1[i]=0;
    tmp2[i]=0;
    tmp[i]= 0;
  }
  
  //запуск параллельной задачи learn для обучения нейросетей
  start learn;


  //Ожидание нажатия на кнопки
  line5="left 2 right 1";
  while (!ButtonPressed(BTNRIGHT, true) && !ButtonPressed(BTNLEFT, true));
  line5 = "";
  // если нажат левый датчик, игра человек-человек
  if(ButtonPressed(BTNLEFT, true))
  {
    //бесконечный цикл игры
    while(true)
    {
      //сброс tmp
      for(int i=0;i<=10;i++){
        tmp[i]= 0;
      }
      //Кубиков 11
      box1=11;
      while(true)
      {
        //ход игрока1
        buttonPress=touch(2);
        //выкидываем столько кубиков, сколько нажатий
        Left(buttonPress);
        //запоминаем сколько выкинули
        box1-=buttonPress;

        line4=NumToStr(box1);
        //проверка на выигрыш
        if(box1<=2){
          win1=1;
          line3="Player1 Win";
          break;
        }
        //ход игрока2
        buttonPress=touch(1);
        //выкидываем столько кубиков, сколько нажатий
        Right(buttonPress);
        //запоминаем сколько выкинули
        box1-=buttonPress;

        line4=NumToStr(box1);
        if(box1<=2){
          //проверка на выигрыш
          win1=2;
          line3="Player Win";
          break;
        }
      }
      //ожидания нажатия для новой игры(загрузка кубиков)
      while (!ButtonPressed(BTNCENTER, true));
      line3 = "";
    }
  }

  //если нажат правый датчик, игра человек-NN
  if(ButtonPressed(BTNRIGHT, true))
  {
    while(true)
    {
      //переменная кто выйграл
      win1=0;
      //сколько кубиков осталось
      box1=11;
      //рандомно определям первый ход
      if (Random(100)>50)
      {
        //первый ходит человек
        while(true){
          //ход игрока
          PlayFile("playerh.rso");
          Wait(1000);
          buttonPress=touch(2);
          if(buttonPress==1) {
            PlayFile("player1.rso");
            Wait(2000);
          }
          else{
            PlayFile("player2.rso");
            Wait(2000);
          }
          //выкидываем кубики по количеству нажатий
          Left(buttonPress);
          box1-=buttonPress;
          line4=NumToStr(box1);

          //проверка на выигрыш
          if(box1<=2){
            PlayFile("networkw.rso");
            Wait(2000);
            win1=1;
            line3="Robot Win";
            break;
          }
          PlayFile("networkh.rso");
          Wait(2000);
          PlayFile("networkd.rso");
          Wait(Random(10000)+2000);
          
          //ход НС
          if(Random(100) < white1[box1-1]*1.0/(white1[box1-1]+black1[box1-1])*100.0){
            PlayFile("network2.rso");
            Wait(2000);
            //берёт 2 кубика
            Right(2);
            box1-=2;
          }
          else{
            PlayFile("network1.rso");
            Wait(2000);
            //берёт 1 кубик
            Right(1);
            box1-=1;
          }

          line4=NumToStr(box1);
          
          //проверка на выигрыш
          if(box1<=2){
            PlayFile("playerw.rso");
            Wait(2000);
            win1=2;
            line3="Player Win";
            break;
          }
        }
      }
      // если первой ходит NN
      else
      {
        while(true){
          //ход НС
          if(Random(100) < white2[box1-1]*1.0/(white2[box1-1]+black2[box1-1])*100.0){
            PlayFile("network2.rso");
            Wait(2000);
            //берёт 2 кубика
            Right(2);
            box1-=2;
          }
          else{
            PlayFile("network1.rso");
            Wait(2000);
            // берёт 1 кубик
            Right(1);
            box1-=1;
          }

          line4=NumToStr(box1);
          // проверка на выигрыш
          if(box1<=2){
            PlayFile("playerw.rso");
            Wait(2000);
            win1=2;
            line3="Player Win";
            break;
          }

          //ход игрока
          PlayFile("playerh.rso");
          Wait(1000);
          buttonPress=touch(2);
          if(buttonPress==1) {
            PlayFile("player1.rso");
            Wait(2000);
          }
          else{
            PlayFile("player2.rso");
            Wait(2000);
          }
          // выкидываем по количеству нажатий
          Left(buttonPress);
          box1-=buttonPress;
          line4=NumToStr(box1);

          //проверка на выигрыш
          if(box1<=2){
            PlayFile("networkw.rso");
            Wait(2000);
            win1=1;
            line3="Robot Win";
            break;
          }
          PlayFile("networkh.rso");
          Wait(2000);
          PlayFile("networkd.rso");
          Wait(Random(10000)+2000);
        }
      }

      // одидания нажатия для продолжения игры
      PlayFile("networkl.rso");
      Wait(3000);
      PlayFile("next.rso");
      Wait(2000);
      while (!ButtonPressed(BTNCENTER, true));
        line3 = "";
      }
    }
  }
}



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