Percorso a ostacoli

Last modified: 2019-12-04 22:45:25

Il progetto

Ho realizzato un programma in Python in grado di imparare autonomamente ad affrontare un percorso a ostacoli utilizzando una forma semplice di neuroevoluzione

Per progettare il gioco mi sono ispirato al gioco del dinosauro di Google che compare su Chrome quando non funziona la connessione.

L'idea è che il quadrato rosso si trova ad affrontare un percorso dove si susseguono ostacoli posizionati a altezze diverse, e man mano la velocità aumenta. Il programma deve imparare a saltare al momento giusto e sopravvivere il più a lungo possibile basandosi su alcune informazioni.

Il gioco

Per creare il gioco ho usato il modulo tkinter, con cui è possibile gestire agilmente gli elementi grafici.

Gli ostacoli vengono generati ad altezze variabili sul bordo destro della finestra e si avvicinano con una determinata velocità al quadrato, che deve imparare a saltarli.

Il gioco è completamente gestito dalla classe Game, che contiene diverse funzioni, tra cui:

La funzione jump:

                def jump(self):
                    #SE STO SALTANDO PROSEGUO MOTO FINCHE' NON TORNO A GROUND
                    if self.cang_y <= 330 and self.jumping == True:
                        self.new_y = 330 - 10.5 * self.time_count + 0.2 * self.time_count * self.time_count
                        self.movement_y = self.new_y - self.cang_y
                        self.w.move(self.canguro, 0, self.movement_y)
                        #print ("-->jump " + str(self.cang_y) + " > " + str(self.new_y))
                        self.cang_y = self.new_y
                    elif self.cang_y > 330:
                        #print ("-->FIX jump")
                        self.new_y = 330
                        self.movement_y = self.new_y - self.cang_y
                        self.w.move(self.canguro, 0, self.movement_y)
                        self.cang_y = self.new_y
                        self.jumping = False
                    else:
                        self.new_y = 330
                        self.movement_y = self.new_y - self.cang_y
                        self.w.move(self.canguro, 0, self.movement_y)
                        self.cang_y = self.new_y
                        self.jumping = False
                        self.jump_triggered = False
            

Imparare a giocare

Una volta pronto il gioco, il programma deve imparare a giocare. Avrà a disposizione un solo comando: salta; e dovrà basarsi su tre informazioni: la distanza dell'ostacolo, la velocità con cui si avvicina e la sua altezza. Queste informazioni verranno interpretate da delle reti neurali strutturate in questo modo:

L'algoritmo che ho scritto è una sorta di algoritmo genetico dove le popolazioni sono gruppi di reti neurali ciascuna con diversi pesi. Per ogni popolazione, ogni singola rete neurale viene usata per giocare e riceve un punteggio basato sulla distanza totale percorsa, ovvero su quanto è sopravvissuta.

Ogni rete neurale è una classe NeuralNetwork che, grazie alle classi Layer e Neuron, gestisce i calcoli, i pesi, la funzione attivazione e gli output.

La funzione play() chiede continuamente istruzioni alla rete neurale considerata fornendogli le informazioni a disposizione e in base all'output avvia o meno il salto:

                self.population[i].set_input([self.dist_x, self.obs_speed*100, self.obs_y])
                received = self.population[i].calc_output()

                if received[0] > received[1] and not self.jump_triggered:
                    self.time_count=0
                    self.jumping=True
                    self.jump_triggered = True
                else:
                    pass
            

Mutazione e selezione

La rete neurale che è stata in grado di sopravvivere più a lungo verrà usata per generare la nuova popolazione. La prima nuova rete sarà esattamente identica alla migliore trovata fino a quel momento, per le altre invece ogni peso avrà il 15% di probabilità di mutare, ovvero di essere sostituito da un peso random.

La funzione evolve():

                def evolve(self):
                    self.generation += 1
                    self.best_score = 0
                    self.best_nn = -1
                    for i in range(0, self.n_entities):
                        if self.scores[i] > self.best_score and self.scores[i] > self.last_best:
                            self.best_score = self.scores[i]
                            self.best_nn = i
                    
                    if self.best_nn >=0:
                        self.population[0] = self.population[self.best_nn]


                    if self.best_score > self.last_best:
                        self.last_best = self.best_score

                    pacchetto_pesi_rand_layer_1 = self.rand.rand(self.n_entities, 5, 3) *2 - 1
                    pacchetto_pesi_rand_layer_2 = self.rand.rand(self.n_entities, 5, 5) *2 - 1
                    pacchetto_pesi_rand_layer_3 = self.rand.rand(self.n_entities, 2, 5) *2 - 1

                    rands = self.rand.random(size=(100000))
                    c=0
                    

                    for y in range(1, self.n_entities):
                        self.population[y] = self.population[self.best_nn]

                        mutation_rate = 0.15
                        for l in range(1, self.population[y].n_layers):
                            for n in range(0, self.population[y].layers_sizes[l]):
                                for w in range(0, self.population[y].layers_sizes[l-1]):
                                    r = rands[c]
                                    c += 1
                                    if r < mutation_rate:
                                        if l==1:
                                            self.population[y].layer[l].weights[n][w] = pacchetto_pesi_rand_layer_1[y][n][w]
                                        elif l==2:
                                            self.population[y].layer[l].weights[n][w] = pacchetto_pesi_rand_layer_2[y][n][w]
                                        elif l==3:
                                            self.population[y].layer[l].weights[n][w] = pacchetto_pesi_rand_layer_3[y][n][w]
            

Esito

Il programma si rivela quasi sempre in grado di trovare una rete neurale in grado di sopravvivere al percorso.




🌙 Modalità notte