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ć 🙂