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

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *