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.