Привет, Хабр!
Сегодня я расскажу и покажу, как сделать 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.
