Nauka gry w Flappy Bird wykorzystując ANN i algorytm genetyczny
Postanowiłem spróbować stworzyć samo-uczący się algorytm radzący sobie z prostym problemem. Wiem, że moje rozwiązanie jest mocno na wyrost, ale i tak chciałem poeksperymentować z możliwościami połączenia GA i ANN.

Plan działania algorytmu jest prosty:
1. Zapisz ekran gry korzystając z OpenCV.
2. Znajdź X i Y ptaka i kolumn. Wskaż algorytmowi kiedy gra, a kiedy jest w menu.
3. Podaj ANN dane wsadowe i czekaj na „decyzję”.
4. Oceń ANN na podstawie czasu gry.
5. Stwórz nową populację ANN korzystając z GA na podstawie najlepszych rodziców.
6. Powtórz
Krokiem zerowym było znalezienie odpowiedniej wersji gry do użycia. Wybrałem tę wersję głównie ze względu na to, że można było używać spacji do skakania i mogłem ją spowolnić edytują skrypt JS.
Zapisz ekran gry korzystając z OpenCV.
Zapisywanie ekranu używając biblioteki OpenCV było prostym zadaniem. Umieściłem przeglądarkę internetową po lewej stronie ekranu w taki sposób, żeby okno gry miało zawsze te same współrzędne (612,162,1090,781) i użyłem kodu:
import numpy as np
from PIL import ImageGrab
import cv2
#To get the right colors
def process_img(original_image):
processed_img = cv2.cvtColor(original_image, cv2.COLOR_BGR2RGB)
return processed_img
screen1 = np.array(ImageGrab.grab(bbox=(612,162,1090, 781)))
new_screen = process_img(screen1)
cv2.imshow('Flappy bird tracking',new_screen)
if cv2.waitKey(25) & 0xFF == ord('q'):
cv2.destroyAllWindows()
break
To dało mi dostęp do ekrany w formacie numpy array. Dzięki temu mogłem dodać warstwę logiki, która pozwoliła mi znaleźć ptaka, kolumny i sprawdzić czy algorytm jest w grze lub menu.
Znajdź X i Y ptaka i kolumn. Wskaż algorytmowi kiedy gra, a kiedy jest w menu.
Znalezienie kolumny zostało wykonane poprzez użycie metody matchTemplate. Załadowałem rysunek kolumny i odczytałem jej wysokość i szerokość.
template = cv2.imread('column.jpg',0)
w, h = template.shape[::-1]
res = cv2.matchTemplate(cv2.cvtColor(new_screen,cv2.COLOR_BGR2GRAY),template,cv2.TM_CCOEFF_NORMED)
threshold = 0.8
loc_column = np.where( res >= threshold)
for pt in zip(*loc_column[::-1]):
cv2.rectangle(new_screen, pt , (pt[0] + w+55, pt[1] + h), (0,255,255), 2)
if loc_column[::-1][0].any():
bird_distance = -bird_dist+loc_column[::-1][0][0]
column_height = loc_column[::-1][1][0]
else:
bird_distance = -bird_dist+300
column_height = -bird_height+280
Znalezienie animowanego i poruszającego ptaka nie było możliwe przy użyciu matchTemplate. Postanowiłem znaleźć ptaka poprzez zlokalizowanie kolorów, które występują tylko na ptaku. Było to sposób, który wymagał kilku prób, ale ostatecznie miał 100% skuteczności.
lower_red = np.array([30,180,200])
upper_red = np.array([50,200,220])
mask = cv2.inRange(new_screen, lower_red, upper_red)
threshhold = 1
loc_bird = np.where(mask >= threshhold)
for pt in zip(*loc_bird[::-1]):
cv2.rectangle(new_screen, (loc_bird[::-1][0][0] -20, loc_bird[::-1][1][0] - 5), (loc_bird[::-1][0][0] +30, loc_bird[::-1][1][0] +30), (0,255,255), 2)
if loc_bird[::-1][0].any():
#print('Ptak na wysokosci' + str(560-loc_bird[::-1][1][0]))
bird_height = loc_bird[::-1][1][0]
bird_dist = loc_bird[::-1][0][0]
#print(bird_dist)
else:
bird_height = 0
bird_dist = 72
W celu sprawdzenia czy algorytm jest w menu, czy nie, postanowiłem sprawdzać obecność punktacji nad ptakiem. W momencie ich zniknięcia algorytm wie, że gra się kończy. Cała logika jest dostępna poniżej:
#If there are no white points in the screen upper part:
while(i<10):
if(not([0,0,0] in screen1[100:120,197:267])):
time.sleep(0.5)
#Press space to start playing:
if(state != 'Play'):
state_update = 'Menu'
state=state_update
pyautogui.press('space')
#The game just ended, score everything:
else:
state_update = 'End'
if(state_update != state):
state=state_update
czas_gry = time.time()-last_time
print(str(bird_height_old) + ' ' + str(czas_gry-0.5))
if((bird_height_old != 600) and (czas_gry-0.5 > 10)):
ranking_sieci.append([lista_sieci[i],czas_gry-0.5])
i=i+1
if(i == 10):
if(len(ranking_sieci)>1):
mutation_rate = 0.2
sieci_ranking = sorted(ranking_sieci,key=lambda x: (x[1]),reverse=True)
ranking_sieci=[]
print(sieci_ranking)
lista_sieci = []
sieci_ranking[0][0].save(str(sieci_ranking[0][1]).replace(".",""))
print('tworze nowa populacje')
evolution.create_new_pop(sieci_ranking[0][0],sieci_ranking[1][0],lista_sieci,mutation_rate)
i=0
else:
print(ranking_sieci)
print('musze zaorac')
mutation_rate = 0.2
test = sorted(ranking_sieci,key=lambda x: (x[1]),reverse=True)
lista_sieci = [neural_net.NN(2,4,1) for i in range(10) ]
i=0
ranking_sieci=[]
if (len(test)>0):
test[0][0].save(str(test[0][1]).replace(".",""))
lista_sieci[0] = evolution.mutate(test[0][0],0.05)
lista_sieci[1] = evolution.mutate(test[0][0],0.1)
lista_sieci[2] = evolution.mutate(test[0][0],0.2)
time.sleep(1)
pyautogui.press('space')
else:
state_update = 'Play'
if(state == 'Menu'):
last_time= time.time()
state=state_update
pyautogui.press('space')
else:
decision = lista_sieci[i].runNN([column_height,bird_height,])
if(column_height == 600):
time.sleep(1.4)
if(decision[0]>0.5):
pyautogui.press('space')
bird_height_old = bird_height
Podaj ANN dane wsadowe i czekaj na „decyzję”.
Ok, na tym etapie mam już X i Y obiektów, a algorytm wie kiedy gra. Następnym krokiem jest policzenie odległości w obu osiach między ptakiem, a nadchodzącą kolumną. To będą dane wsadowe do ANN. Tworzę losowe ANN za pomocą tego i tego skryptu.
Ta linijka tworzy 10 sieci, które będą pierwszą generacją. Wybór neuronów w warstwie ukrytej jest losowy, bo nie chciałem spędzać nad tym zbyt dużo czasu.
lista_sieci = [neural_net.NN(2,4,1) for i in range(10) ]
Następnie gram w pętli, gdzie i oznacza iterację. W celu uzyskania decyzji o skoku podaję dane wsadowe do ANN i na podstawie otrzymanej wartości podejmuję decyzję:
decision = lista_sieci[i].runNN([column_height,bird_height,])
if(decision[0]>0.5):
pyautogui.press('space')
Oceń ANN na podstawie czasu gry
Aby ocenić ANN obliczam czas między startem gry, a jej końcem. Zegar zaczyna odliczać czas w momencie pierwszego kliknięcia spacji w menu. Zatrzymuję się kiedy ptak ginie. Ranking_sieci jest listą zawierającą ANN i ich czasy. Kiedy zostanie posortowana, na pierwszym miejscu znajdą się rodzice wykorzystani do stworzenia kolejnej generacji.
czas_gry = time.time()-last_time print(str(bird_height_old) + ' ' + str(czas_gry-0.5)) ranking_sieci.append([lista_sieci[i],czas_gry-0.5])
Stwórz nową populację ANN korzystając z GA na podstawie najlepszych rodziców.
Na podstawie wyników sieci, wybieram najlepsze i stosuję metody crossover i mutate poprzez wywołanie create_new_pop z evolution.py (dostępnego wyżej):
evolution.create_new_pop(sieci_ranking[0][0],sieci_ranking[1][0],lista_sieci,mutation_rate)
Jeśli mniej niż 2 sieci przejdą przez minimalne wymagania, tworzę nową losową populację i umieszam w niej mutacje najlepszego sieci z poprzedniej generacji. Tym sposobem nie tracę poprzednich wyników całkowicie.
Powtórz
Po stworzeniu nowej populacji, ustawiam i na 0 i pozwalam pętli kontynuować grę.
Wyniki okazały się zaskakująco dobre. Po kilku generacjach algorytm stal się „wystarczająco dobry”, żeby grać tak długo jak chciało mi się go pilnować 🙂