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

НейроКачели

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


Наверное все мы в детстве любили качаться на качелях. Как вы думаете, если ребенка впервые посадить на качели и не показать ему как на них раскачиваться (и не давать соответственно подсмотреть как это делают другие), получится у него их раскачать, сохранять и контролировать скорость раскачивания? В его мозге нет устойчивых взаимосвязей и образов того как ему это сделать. Он начнет пробовать совершать произвольные действия, если качели начнут раскачиваться в мозг пойдет визуальная и вестибулярная обратная связь и по тем действиям, которые он только что предпринимал, начнут формироваться новые нейронные цепочки, связанные с "раскачать качели". Если какие то действия наоборот начнут тормозить раскачавшиеся качели, например ребенок отклонился не в ту сторону - он "запомнит" что так делать не нужно, соответствующие связи будут ослаблены или созданы новые.
Похожим образом работает и искусственная нейросеть, которую мы создадим в нашем сегодняшнем проекте.
Для начала соберем конструкцию. Качели мы построили на базе LEGO Mindstorms EV3, использовав несколько дополнительных деталей из ресурсных наборов. Инструкцию по сборке наиболее сложной части конструкции -  качающихся человечков - в формате LEGO Digital Designer вы можете скачать по ссылке. Остальную часть качелей каждый без труда сможет достроить из тех деталей, которые будут у него под рукой.


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

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

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

Алгоритм таков:
1) Смотрим куда качаются качели (по данным гироскопа) - влево или вправо и выбираем коробок - первый или второй. Следует заметить что мы не рассматриваем ситуацию когда качели имеют нулевую скорость, например при старте, развивая алгоритм дальше нужно добавить третью коробку с камушками, но пока обойдемся без нее.
2) Вытаскиваем наугад камень из выбранной коробки. Смотрим его цвет и кладем обратно. если камень белый - даем человечкам команду отклониться влево, если черный - вправо.
3) Оцениваем скорость качелей. Если она выросла - кладем в коробку, из которой мы брали камень еще один камень такого же цвета. Если скорость упала - убираем из коробки камень то го цвета, который был вытащен. Следует заметить что последний камень каждого цвета лучше не убирать, пусть остается.
4) Переходим к п.1

В процессе обучения возможна интересная ситуация - неподвижные качели можно раскачать даже неправильными движениями, но только до какого-то предела, если попытки предпринимать и дальше, то они наоборот замедлят раскачивание. В реальной работе робота ситуация действительно проявляется довольно часто, робот раскачивается до какого-то предела (небольшого), но дальше процесс не идет. Дальнейшие попытки раскачать качели приводят к их полной остановке и далее процесс обучение начинается снова, уже в "верном" направлении. И заметьте, это все без вмешательства человека - вполне разумная машина!


Программировать качели мы будем на Python.  Давайте посмотрим код:

# импортируем модуль для работы с железом EV3
from ev3dev.ev3 import *
# импортируем модуль математики
from math import *
# импортируем модуль для работы со временем
from time import *
# импортируем модуль генератора случайных чисел
from random import random
# импортируем модуль многопоточности
import threading

# начальная скорость моторов ("человечков")
speedB = 0
speedC = 0

# текущая средняя скорость движения качелей
moving_average = 0
# предыдущая средняя скорость движения качелей
moving_average_old = 0

# Позиция человечков -100..100
pos = 0

# направление движения качелей
state_gyro = 0

# описание нейрона сети
class brain():
    def __init__(self):
        self.Black = 1
        self.White = 1

# создаем пару нейронов
BrainLeft = brain()
BrainRight = brain()

# объект для отслеживания кнопок на блоке
btn = Button()

# объекты для моторов
B = LargeMotor('outB')
C = LargeMotor('outC')

# объект для работы с гироскопом
gyro = GyroSensor("in4")

# устраняем дрейф гироскопа и ставим его в режим измерения скорости
gyro.mode = "GYRO-RATE"
gyro.mode = "GYRO-ANG"
gyro.mode = "GYRO-RATE"

# человечков - в исходную позицию sleep(1)
B.run_forever(speed_sp=80)
C.run_forever(speed_sp=80)
sleep(3)

B.stop(stop_action="brake")
C.stop(stop_action="brake")
sleep(1)

B.reset()
C.reset()

B.run_to_rel_pos(position_sp=-120, speed_sp=80)
C.run_to_rel_pos(position_sp=-120, speed_sp=80)
while any(C.state): sleep(0.1)

B.stop(stop_action="brake")
C.stop(stop_action="brake")

sleep(1)
B.reset()
C.reset()

# готов к работе!
Sound.speak('Ready').wait()

stop = False

# П-регулятор для управления человечками
def reg():
    while not stop:
        speedB = ((-1*pos)-B.position)*8
        speedC = (pos-C.position)*8

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

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

        sleep(0.01)

# Запускаем регулятор в параллельном потоке
t = threading.Thread(target=reg)
t.daemon = True
t.start()

# Максимальная достигнутая скорость
max_speed = 0

while not stop:

    # вылет по кнопке "назад" на блоке     
    stop = btn.backspace
    
    # скорость по показаниям гироскопа
    state_gyro_speed = gyro.value()
        
    if(state_gyro_speed != 0): 
        # текущее направление движения качелей
        state_gyro = state_gyro_speed/abs(state_gyro_speed)
        if(state_gyro > 0):
            # если влево, и вытащили черный камень
            if(random() <= (BrainLeft.Black/(BrainLeft.Black + BrainLeft.White))): pos = -100
            # если белый 
            else: pos = 100
        else:
            # если вправо, и вытащили черный камень
            if(random() <= (BrainRight.Black/(BrainRight.Black + BrainRight.White))): pos = -100
            # если белый
            else: pos = 100
            
    moving_average_old = moving_average
       
    # ждем перед оценкой
    sleep(0.25)

    moving_average = abs(state_gyro_speed)*0.1 + moving_average*0.9

    if(state_gyro > 0):
        # если скорость не упала при движении влево
        if(moving_average >= moving_average_old): 
            if(pos<0): BrainLeft.Black += 1
            else: BrainLeft.White += 1
        else:
            # если скорость упала при движении влево
            if(pos<0 and BrainLeft.Black > 1): BrainLeft.Black -= 1
            elif(pos>0 and BrainLeft.White > 1): BrainLeft.White -= 1

    elif(state_gyro < 0):
        # если скорость не упала при движении вправо
        if(moving_average >= moving_average_old): 
            if(pos<0): BrainRight.Black += 1
            else: BrainRight.White += 1
        else:
        # если скорость упала при движении вправо
            if(pos<0 and BrainRight.Black > 1): BrainRight.Black -= 1
            elif(pos>0 and BrainRight.White > 1): BrainRight.White -= 1
    # выводим состояние нейронной сети     print("[[ " + str(BrainLeft.Black) + ", " + str(BrainLeft.White) + "][ " + str(BrainRight.Black) + ", " + str(BrainRight.White) + "]]",moving_average)

    # если скорость выросла на 25, фиксируем новый рекорд    
    if moving_average > max_speed + 25:
        print("[[ " + str(BrainLeft.Black) + ", " + str(BrainLeft.White) + "][ " + str(BrainRight.Black) + ", " + str(BrainRight.White) + "]]",moving_average)
        max_speed = moving_average
        # если скорость выросла до 200 - обучение завершено
        if max_speed < 200:
            Sound.speak('New Record')
        else:
            Sound.speak('Learning Complete')

Sound.beep().wait() 
B.stop(stop_action="brake")
C.stop(stop_action="brake")

# завершение работы
Sound.speak('Stop').wait()


print("[[ " + str(BrainLeft.Black) + ", " + str(BrainLeft.White) + "][ " + str(BrainRight.Black) + ", " + str(BrainRight.White) + "]]")

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