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.
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
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
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]
Il programma si rivela quasi sempre in grado di trovare una rete neurale in grado di sopravvivere al percorso.