Создание генетического алгоритма для нейросети и нейроcети для графических игр и видеоигр с помощью Python и NumPy

Привет, Хабр!

Сегодня я расскажу и покажу, как сделать Genetic Algorithm(GA) для нейросети, чтобы с помощью него она смогла проходить разные игры. Я его испробовал на игре Pong и Flappy bird. Он себя показал очень хорошо. Советую прочитать, если вы не читали первую статью: «Создание простого и работоспособного генетического алгоритма для нейросети с Python и NumPy», так как я доработал свой код, который был показан в той статье.

Я разделил код на два скрипта, в одной нейросеть играет в какую-то игру, в другой обучается и принимает решения(сам генетический алгоритм). Код с игрой представляет из себя функцию которая возвращает фитнес функцию (она нужна для сортировки нейросетей, например, сколько времени она продержалась, сколько очков заработала и т.п.). Поэтому код с играми(их две) будет в конце статьи. Генетический алгоритм для нейросети для игры Pong и игры Flappy Bird различаются лишь параметрами.

Используя скрипт, который я написал и описал в предыдущей статье, я создал сильно изменённый код генетического алгоритма для игры Pong, который я и буду описывать больше всего, так как именно на него я опирался, когда я уже создавал GA для Flappy Bird.

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

import numpy as np
import random
import ANNPong as anp
import pygame as pg
import sys
from pygame.locals import *
pg.init()
listNet = {}
NewNet = []
goodNet = []
timeNN = 0
moveRight = False
moveLeft = False
epoch = 0
mainClock = pg.time.Clock()
WINDOWWIDTH = 800
WINDOWHEIGHT = 500
windowSurface = pg.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT), 0, 32)
pg.display.set_caption('ANN Pong')

AnnPong это скрипт с игрой

listNet, NewNet, goodNet - списки нейросетей(потом разберем подробнее)

timeNN - фитнес функция

MoveRight, moveLeft - выбор нейросети куда двигаться

epoch - счетчик эпох

def sigmoid(x):
    return 1/(1 + np.exp(-x))

class Network():
    def __init__(self):
        self.H1 = np.random.randn(6, 12)
        self.H2 = np.random.randn(12, 6)
        self.O1 = np.random.randn(6, 3)
        self.BH1 = np.random.randn(12)
        self.BH2 = np.random.randn(6)
        self.BO1 = np.random.randn(3)
        self.epoch = 0

    def predict(self, x, first, second):
        nas = x @ self.H1 + self.BH1
        nas = sigmoid(nas)
        nas = nas @ self.H2 + self.BH2
        nas = sigmoid(nas)
        nas = nas @ self.O1  + self.BO1
        nas = sigmoid(nas)
        if nas[0] > nas[1] and nas[0] > nas[2]:
            first = True
            second = False
            return first, second
        elif nas[1] > nas[0] and nas[1] > nas[2]:
            first = False
            second = True
            return first, second
        elif nas[2] > nas[0] and nas[2] > nas[1]:
            first = False
            second = False
            return first, second
        else:
            first = False
            second = False
            return first, second
        def epoch(self, a):
            return 0
            

class Network1():
    def __init__(self, H1, H2, O1, BH1, BH2, BO1, ep):
        self.H1 = H1
        self.H2 = H2
        self.O1 = O1
        self.BH1 = BH1
        self.BH2 = BH2
        self.BO1 = BO1
        self.epoch = ep

    def predict(self, x, first, second):
        nas = x @ self.H1 + self.BH1
        nas = sigmoid(nas)
        nas = nas @ self.H2 + self.BH2
        nas = sigmoid(nas)
        nas = nas @ self.O1 + self.BO1
        nas = sigmoid(nas)
        if nas[0] > nas[1] and nas[0] > nas[2]:
            first = True
            second = False
            return first, second
        elif nas[1] > nas[0] and nas[1] > nas[2]:
            first = False
            second = True
            return first, second
        elif nas[2] > nas[0] and nas[2] > nas[1]:
            first = False
            second = False
            return first, second
        else:
            first = False
            second = False
            return first, second

Сигмоида используется как функция активации.

В классе Network мы определяем параметры нейросети, а в функции predict она говорит, куда двигаться в игре. (nas - сокращение от Network answer), функция epoch возвращает эпоху появления этого ИИ для нулевого поколения, так как в классе Network1() для этого задается отдельная переменная.

for s in range (1000):
    s = Network()
    timeNN = anp.NNPong(s)
    listNet.update({
        s : timeNN
    })
    
listNet = dict(sorted(listNet.items(), key=lambda item: item[1]))
NewNet = listNet.keys()
goodNet = list(NewNet)
NewNet = goodNet[:10]
listNet = {}
goodNet = NewNet
anp.NPong(NewNet[0])
print(str(epoch) + " epoch")
print(NewNet[0].epoch)
print('next')
anp.NPong(NewNet[1])
print(NewNet[1].epoch)
print('next')
anp.NPong(NewNet[2])
print(NewNet[2].epoch)
print('next')
anp.NPong(NewNet[3])
print(NewNet[3].epoch)
print('next')
anp.NPong(NewNet[4])
print(NewNet[4].epoch)
print('next')
anp.NPong(NewNet[5])
print(NewNet[5].epoch)
print('next')
anp.NPong(NewNet[6])
print(NewNet[6].epoch)
print('next')
anp.NPong(NewNet[7])
print(NewNet[7].epoch)
print('next')
anp.NPong(NewNet[8])
print(NewNet[8].epoch)
print('next')
anp.NPong(NewNet[9])
print(NewNet[9].epoch)
print('that is all')

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

Подробнее:

В timeNN записывается возвращенная из кода с игрой фитнес функция, затем мы добавляем в listNet ИИ и его значение timeNN. После цикла мы сортируем список, записываем в NewNet нейросети из listNet, дальше мы формируем список и оставляем только десять.

for g in range(990):
    parent1 = random.choice(NewNet)
    parent2 = random.choice(NewNet)
    ch1H = np.vstack((parent1.H1[:3], parent2.H1[3:])) * random.uniform(-2, 2)
    ch2H = np.vstack((parent1.H2[:6], parent2.H2[6:])) * random.uniform(-2, 2)
    ch1O = np.vstack((parent1. O1[:3], parent2. O1[3:])) * random.uniform(-2, 2)
    chB1 = parent1.BH1 * random.uniform(-2, 2)
    chB2 = parent2.BH2 * random.uniform(-2, 2)
    chB3 = parent2.BO1 * random.uniform(-2, 2)
    g = Network1(ch1H, ch2H, ch1O, chB1, chB2, chB3, 1)
    goodNet.append(g)
NewNet = []

Здесь происходит скрещивание и мутация.(Такие моменты более подробно были описаны в первой статье)

while True:
    epoch += 1
    print(str(epoch) + " epoch")
    for s in goodNet:
        timeNN = anp.NNPong(s)
        listNet.update({
            s : timeNN
        })
    goodNet =[]
    listNet = dict(sorted(listNet.items(), key=lambda item: item[1], reverse=True))
    goodNet = list(listNet.keys())
    NewNet.append(goodNet[0])
    goodNet = list(listNet.values())
    for i in listNet:
        a = goodNet[0]
        if listNet.get(i) == a:
            NewNet.append(i)
    goodNet = list(NewNet)
    listNet = {}
    try:
        print(NewNet[0].epoch)
        anp.NPong(NewNet[0])
        print('next')
        print(NewNet[1].epoch)
        anp.NPong(NewNet[1])
        print('next')
        print(NewNet[2].epoch)
        anp.NPong(NewNet[2])
        print('next')
        print(NewNet[3].epoch)
        anp.NPong(NewNet[3])
        print('next')
        print(NewNet[4].epoch)
        anp.NPong(NewNet[4])
        print('next')
            
        print(NewNet[5].epoch)
        anp.NPong(NewNet[5])
        print('next')
        print(NewNet[6].epoch)
        anp.NPong(NewNet[6])
        print('next')
        print(NewNet[7].epoch)
        anp.NPong(NewNet[7])
        print('next')
    except IndexError:
        print('that is all')
        
    for g in range(1000 - len(NewNet)):
        parent1 = random.choice(NewNet)
        parent2 = random.choice(NewNet)
        ch1H = np.vstack((parent1.H1[:3], parent2.H1[3:])) * random.uniform(-2, 2)
        ch2H = np.vstack((parent1.H2[:6], parent2.H2[6:])) * random.uniform(-2, 2)
        ch1O = np.vstack((parent1. O1[:3], parent2. O1[3:])) * random.uniform(-2, 2)
        chB1 = parent1.BH1 * random.uniform(-2, 2)
        chB2 = parent2.BH2 * random.uniform(-2, 2)
        chB3 = parent2.BO1 * random.uniform(-2, 2)
        g = Network1(ch1H, ch2H, ch1O, chB1, chB2, chB3, epoch)
        goodNet.append(g)
    print(len(NewNet))
    print(len(goodNet))
    NewNet = []

Здесь уже пошло повторение, поэтому объясню только то, что не было сказано до этого:

Здесь мы берём первого в списке, то есть одного из лучших в эпохе и сверяем его результаты с остальными, так как очень часто есть несколько ИИ, которые добились таких же успехов. И эти равноправные лидеры будут учавствовать в мутациях, мы используем метод try, так как лучших в этой эпохе может быть меньше 10. А ещё мы закидываем эти нейросети в следующую эпоху без изменений, так как потомки могут оказаться хуже их предков, то есть чтобы они не деградировали.

Это всё по первому коду!

Перейдем к коду игры. Тут я объясню только то, что касается обучения ИИ(весь размещу ссылкой на диск).

В игре Pong нейросеть играла дважды: в первый раз мячик отскакивает влево, второй раз - вправо

*whGo - это переменная в коде(сокращение от "where to go")

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

После месяцев работы и доработок, у меня получилось создать алгоритм обучения для игры Pong, однако для уверенности я решил проверить ИИ не на своей игре, а созданную другим человеком(проверка на всеядность)))), я выбрал игру Flappy Bird на pygame с этого видео: https://youtu.be/7IqrZb0Sotw?feature=shared

Немного изменив игру для нейросети, например, добавил переменные расстояния от птицы до трубы. Их 3 по 3, так как нам нужно знать высоту каждой трубы(y) и расстояние по х, а на экране не было больше трех пар труб, поэтому и три по три(всего девять). Также после столкновения функция перезапускалась и третьим параметром, который назван rep функции передавалось какой это перезапуск, если он был равен трем, то игра возвращала фитнес функцию в Genetic Algorithm, а если нулю, то мы присуждаем переменной time значение 0. Также я не писал две очень похожие друг на друга функции, а просто проверял, если переменная checkNN равна True, то нужно обновить экран.

Я также доработал код обучения

while True:
    for event in pg.event.get():
        if event.type == KEYDOWN:
            if event.key == K_1:
                showNN = True
    epoch += 1
    print(str(epoch) + " epoch")
    if epoch < 10:
        for s in goodNet:
            timeNN = anp.NPong(s, False, 0, 0)
            listNet.update({
                s : timeNN
            })
    if epoch >= 10:
        for s in goodNet:
            timeNN = anp.NPong(s, False, 0, 1)
            listNet.update({
                s : timeNN
            })

После десятой эпохи из-за последнего параметра, который мы меняем на единицу(в коде игры я назвал этот параметр varRe от слов variant of return), игра возвращает не время, а кол-во труб до столкновения(так нейросеть учиться лучше)

 howALot = 1000 - len(NewNet)
    if howALot < 40:
        howALot = 40

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

На этом всё, если есть вопросы, пишите в комментариях, пока!

Эта же моя статья, но на DTF или dtf: https://dtf.ru/u/1361840-kirill-lanskoi/2888076-sozdanie-geneticheskogo-algoritma-dlya-neiroseti-i-neiroceti-dlya-graficheskih-igr-s-pomoshyu-python-i-numpy.

Коды: https://github.com/LanskoyKirill/GenNumPy.git

@LanskoyGames
10.03.2024 13:17 UTC
Первоисточник

Комментарии

@unffuunnyy
11.03.2024 05:36 UTC
0

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

@LanskoyGames
11.03.2024 07:59 UTC
+1

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

@ivef
12.03.2024 18:56 UTC
-2

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

@eugenk
18.03.2024 15:35 UTC
0

Спасибо, утащил в закладки ! Скажите, а как с более сложными играми ??? Например вот такая игра https://unixpapa.com/floodit/?sz=14&nc=6 . О ней уже писали на Хабре, правда как о головоломке. Но её можно рассмотреть и как игру двух или более лиц. Правила следующие. Каждому игроку выделяется один или несколько цветов. Игроки по очереди делают ходы. Выигрывает тот, кто зальёт всё поле своим цветом. Тут тоже неизвестно что сеть должна выдавать на выходе ибо результат хода очень неопределен (в начале игры во всяком случае). Можно с этим как-то бороться ???