0
Обложка: Как заставить ИИ-существ развиваться. История маленькой ошибки

Как заставить ИИ-существ развиваться. История маленькой ошибки

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

Даже в последнее время появились сомнения по поводу того что вообще какая-то сходимость у алгоритма достижима. Ну, как бы, сложность задачи понятная, не запредельная, и я ожидал что рано или поздно сетки обязаны обучиться бежать к пище.

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

Это выглядело как надкушенный пирог, как половина победы.

Что я перепробовал, чтобы добиться более предсказуемого поведения:

  1. Крутил настройки мира: вероятность мутации веса, силу мутации, обилие пищи и т.д.
  2. Я увеличил поле для существ, добавил преграды на карте.
  3. Пробовал заменять самописную полносвязную сетку — на аналог на базе PyTorch.
  4. Расширил и усложнил инспектор популяции, чтобы видеть что внутри происходит.
  5. Собрал отдельностоящее приложение, которое загружает дамп и тестирует все существа из дампа в маленьком изолированном эксперименте.
Пока крутил настройки, затраты энергии на перемещение и поворот, получились такие ждуны, которые сидят на месте и ждут еду, было забавно. Это просто настройки мира такие, что двигаться и крутиться — дорого в смысле затрат энергии, вот и сидят на месте.

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

Но при этом, долго не получалось добиться твердой сходимости к целенаправленному движению к пище. Ведь чем стены от пищи отличаются? По сути только знаком. Ладно, какие были возможные причины:

  1. Может быть существа «не видят пищу».
  2. Та часть весов, которые отвечают за пищу, не мутируют.
  3. Какие-то важные веса задираются вверх до больших значений, откуда уже сползти не могут («паралич сети»).
  4. Существа достигают какого-то состояния полу-обученности, которое полностью их устраивает. Другими словами, отбору достаточно, чтобы существа ну с какой-то вероятностью находили что-то покушать, и на этом эволюция останавливалась.

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

Изначально у меня было окно инспектора, но там был только перечень существ и при клике на существо — печатались их веса. Начал добавлять функционал в этот инспектор.

График динамики энергии особи

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


динамика энергии

Динамика энергии

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


динамика энергии, как задумывалась

Динамика энергии, как задумывалась

Я был бы рад увидеть, что энергия ведет себя как-то странно, это означало бы что я нашел ошибку, но увы.

Потом добавил массив с событиями в жизни существа: «поел пищу», «столкновение со стеной», «размножение». Этот массив вывел на том-же графике в виде меток по горизонтальной оси — желтые, зеленые, красные.

Вывел на экран массивы весов

Далее добавил вывод весов, всех слоев, включая тот вес, который для сдвига сигмоиды по вертикали.


вывод весов нейронной сети

Вывод весов нейронной сети

Надо было как-то кодировать вес в виде цвета, сделал так: чем ближе к нуля — тем темнее/чернее. Чем больше значение веса — тем зеленее. Чем меньше отрицательное значение — тем краснее. Веса не нормированные, так что пришлось сделать так: все что выше 1.0 — ярко зеленым, меньше «-1» — ярко красным. Тупенько, но пока для наших задач подходит.

За этими картинками, кстати, было интересно наблюдать. В какой-то момент обнаруживалось, что популяция сузилась до 2-3 сильно отличающихся эм.. скажем, «геномов», которые имеют, по всей видимости одинаковую приспособленность. Особи внутри генома отличаются незначительно: плюс-минус 5-10 весов. А сами геномы отличаются между собой кардинально. Ну… практически разные виды =) Правда потом, все-же остался один геном-победитель с незначительной дивергенцией по популяции.

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

Еще хочу попробовать оценить скорость схождения, если на старте веса всех существ будут одинаковые и равны 0.1

Чистый эксперимент

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

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

Эксперимент был такой: генерировалась пустая карта, размером примерно с область которое существо может видеть. Существо устанавливалось слева, мордой направо: угол=0, скорость устанавливалась в 0.1. Дальше поочередно в каждую клетку карты устанавливалась пища, и запускался один прогон. Прогон считался проваленным «0», если существо достигала любого края карты либо если оно теряло пищу из виду, например — отворачивалось (так как у существ нет памяти, то это имеет смысл), или если изначально пища была установлена вне поля зрения. Пока существо видит пищу — оно имеет право шагнуть еще раз. Если существо в итоге добиралась до установленной пищи, то эта клетка засчитывалась как «1». Итого, прогон завершался либо удачно «1», либо провально «0».

Такой прогон повторялся для всех клеток на карте, итого, получался двумерный массив из «0» и «1».

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




До обучения, после обучения

А что получилось в итоге?




Примерно такая картина была на 180-м поколении, это практически 2 дня симуляции

То есть эксперимент намекал, что существа не обучаются. Плюс-минус похожая картина была у каждого существа в популяции.

Вывел на печать что видит существо в процессе чистого эксперимента

В воскресенье сидел чуть оформлял код, у меня было всего минут 30, решил просто посмотреть что видит существо на каждом шаге эксперимента, добавил одну строчку и увидел вот что:




Вот этих троек в эксперименте не должно было быть

Тройки тут означают «существо», двойки — «пищу», единицы, если бы они были — «стены». Видно, что откуда-то существо видит тройки, хотя в эксперименте на постановочной карте не было никаких других существ, кроме него самого. Так что получалось, оно видит себя, свое тело. Координаты существа — вещественные числа, шаг для raytrace алгоритма я установил 0.9, и получается, что в какой-то момент существо действительно может видеть только свое тело.




Raytrace с шагом 0.9 иногда может ступать в ту-же самую клетку, где существо и находится

Так что я raytrace чуть сдвинул вперед, стартовал не с нуля, а с 0.2, и всё сошлось, причем мгновенно, я даже не обучал сетки заново.

    def look(self , mappointer):
        step = 0.9 # шаг перемещения взгляда (для raycast - дистанция на котороую двигаем вперед указатель)
        vision = []
        for a in range(self.resolution):
            adelta = self.angleofview/2 - a*self.anglestep
            d = 0.2 # <----- тут с прошлого года скрывалась ошибка, было 0, надо 0.2
            cur_vision = 0
            while d < self.viewdistance:
                d += step
                x = self.x + d*math.cos(self.angle+adelta)
                y = self.y + d*math.sin(self.angle+adelta)
                #pygame.draw.rect(screen, WHITE, pygame.Rect(int(x)*CELLSIZE-1, int(y)*CELLSIZE-1, 2, 2))
                try:
                    dot = mappointer[int(y)][int(x)]
                except IndexError:
                    cur_vision = 0
                    break
                
                if dot > 0:
                    cur_vision = dot
                    break
            vision.append(cur_vision)

        # Разложим зрение на цветовые каналы
        visionRed = []
        visionGreen = []
        visionBlue = []
        dotColor = []

Результаты


Поколение 1

Поколение 1

Поколение 2

Поколение 2

Поколение 7

Поколение 7&nbsp;

Поколение 15

Поколение 15

Поколение 260

Поколение 260

Так это выглядело пошагово:

Вот что записал в дневнике тот момент:

 

Омг, я проверил. Просто поправил 0.0 на 0.2 в функции look() и
загрузил дамп 150-го поколения. Ни одного существа, которое бы
действовало странно не увидел, долго наблюдал, пытался найти
странности. кое где, крайне редко, пару раз заметил, но я списываю их на
то что возможно это были молодые особи с вредной мутацией. Наблюдал
забавные ситуации, когда несколько существ бегут к одному кусочку еды
наперегонки. Или когда одно существо рядом с другим и постоянно на пару
шагов позади, а так как одно видит тоже что и первое существо почти, то
оно продолжает гоняться за ускользающей едой, а первое существо все
поедает, опреежая второе на несколько шагов.

В общем, да, это прорыв, потому что приложение снова ведет себя понятно. Быстро обучается в пределах 5-7 поколений. Избегает столкновений со стенами, если видит пищу — корректирует курс и скорость, чтобы добраться до пищи. Бинго! Отсюда можно смело ставить новые задачи.

В эксперименте все же есть изредка дыры. И пока не уверен почему, откуда эти дыры. Надо разбираться.

Есть положительный момент в этой ситуации. Она наглядно показывает устойчивость системы к шуму. Шум в виде искажения зрения — не препятствует целенаправленному обучению. Так как этот шум — имеет рандомный безсистемный характер. Щас переформулирую, популяция обученных существ, после 260-го поколения, жило и обучалась с неприятными помехами (регулярно существа видели свое тело, оно какбы загораживало взгляд). И несмотря на это, существа обучились перемещаться в направлении пищи. Это стало очевидно, когда я сместил raycast чуть вперед, и экспериментальные прогоны сразу показали ожидаемые результаты, без какого-то либо отбора, без обучения, на те-же весах. Это дает надежду на то, что обученные сетки и в дальнейшем будут усточивы к всякого рода рандомному шуму.

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