Addestrare una IA (Reinforcement Learning) a giocare a Pong
Questo articolo non ha la pretesa di insegnare a creare un Reinforcement Learning Agent, ma di condividere la mia esperienza di modo che possa essere utile, stimolante o quanto meno interessante per qualcuno.
di Federico Sello
<div><span style="letter-spacing: normal;">Come prima cosa, due parole semplificate sul Reinforcement Learning per chi non conoscesse questa tecnica di machine learning: si tratta di una addestramento "rinforzato" ovvero guidato da un segnale ricompensa (<i>reward</i>) che viene applicato all'agente (la "funzione" che decide quali azioni compiere) dopo che ha preso una decisione e compiuto la relativa azione. In questo modo gli viene garantito un feedback sul quale basarsi per capire se si sta comportando correttamente o meno, e nel caso aggiustare la sua valutazione per le azioni future. Le potenzialità del Reinforcement Learning sono subito chiare quando si considera che non servono centinaia di dati catalogati per insegnare ad una rete neurale come comportarsi, ma con solo un set di regole predefinito e una reward può impararlo da sé. Chiaramente non tutti i problemi sono risolvibili con questo approccio.</span></div><div><br></div><div>Ora entriamo nel dettaglio del nostro caso: il gioco di Pong.</div><div><br></div><div>Per iniziare abbiamo bisogno di definire alcuni punti essenziali sui quali si basa tutta la struttura del Reinforcement Learning:</div><div>-<b>L'ambiente</b>: ovvero lo spazio entro il quale il nostro agente si muove. L'ambiente è dinamico e si evolve in base a regole che stabiliamo noi a seconda del problema. Ad ogni istante L'ambiente si troverà in un particolare stato che lo descrive completamente. Nel nostro caso si tratta dell'area di gioco con tutte le sue regole (la palla che rimbalza, la posizione delle porte, la velocità di spostamento delle barre, i limiti dei bordi, ecc...)</div><div>-<b>L'osservazione</b>: gli "input sensoriali" che diamo al nostro agente e sui quali si deve basare per compiere una azione. Nel nostro caso ho scelto di considerare la posizione della palla, data da due coordinate<b> Xp</b>, <b>Yp</b>, visto che ci muoviamo in un piano, il vettore velocità della palla che ci fornisce anche informazioni sulla direzione della palla anche qui espresso in due coordinate <b>Vxp</b>, <b>Vyp</b>, infine la coordinata y del centro della barra che dobbiamo muovere: <b>Yb</b>. Questa informazione è sufficiente all'agente per capire dove si trova nell'area di gioco dal momento che la barra può muoversi solo nella direzione y. Tenere al minimo indispensabile il numero di input al problema è buona cosa e facilita l'apprendimento all'agente, detto questo era possibile anche avere un set diverso di input purché descrivessero in modo adeguato lo stato del problema.</div><div>-<b>L'insieme delle azioni</b>: tutte le possibili azioni che l'agente può compiere in un dato istante, definite sempre da noi, fanno parte delle "regole del gioco" se vogliamo. Nel nostro caso la barra ha tre possibili azioni a disposizione: andare su, giù o stare ferma. Per semplicità le codificheremo con un vettore <b>(1,-1,0)</b> dato che poi basterà moltiplicare il valore scelto per la velocità della barra.</div><div><br></div><div>Bene, ora possiamo iniziare a scrivere il codice, sempre per semplicità ci appoggeremo a Matlab, nota piattaforma di calcolo scientifico, poiché incorpora al suo interno strumenti già pronti e utili per addestrare reti neurali e lavorare con programmazione e matrici. In particolare sfrutteremo proprio il toolbox dedicato al Reinforcement Learning: <div> https://it.mathworks.com/products/reinforcement-learning.html?s_tid="srchtitle </div>Nulla vieta però di scrivere tutto il codice a mano o sfruttando un altro software dedicato.</div><div><br></div><div>La prima cosa da fare è costruire l'ambiente. Questo non ha molto a che vedere con il machine learning in sé, ma più che altro con le vostre abilità da programmatore, quindi ridurrò al minimo questa parte. Il risultato finale è quello di creare un ambiente snello a livello di codice e di intensità di calcolo, con una interfaccia grafica per poter valutare cosa sta succedendo ed interagire col gioco.</div><div>Matlab mette a disposizione la possibilità di creare un Custom Template Enviroment che facilita la fusione del machine learning con l'ambiente di gioco. Per velocizzare la stesura del codice ho riadattato l'ottima grafica che potete trovare a questo indirizzo:<div> https://it.mathworks.com/matlabcentral/fileexchange/31177-dave-s-matlab-pong </div><div> <br>Seguendo le istruzioni sul '<i>Reinforcement Learning Toolbox™ User's Guide</i> abbiamo realizzato un ambiente perfettamente funzionante che ci permette di giocare a Pong controllando la barra di destra con input da tastiera. Il nostro avversario per ora è immobile in quanto non abbiamo creato l'agente che controllerà l'altra barra. Prima di passare a questa fase una nota sulla sequenza di gioco: la funzione '<i>step</i>' ovvero quella che fa progredire il gioco all'istante successivo (o se preferite allo stato successivo) si compone di questi passi:</div><div>-riceve l'azione scelta dall'agente (ovviamente l'osservazione precede la scelta della azione, in questo caso l'osservazione iniziale è stata definita precedentemente)</div><div>-aggiorna il sistema (chiamando apposite funzioni): muove la palla e le barre</div><div>-ottiene la nuova osservazione del sistema</div><div>-controlla le condizioni di fine partita (verifica se è stato fatto un goal)</div><div>-restituisce all'agente la ricompensa per l'azione scelta</div><div><br></div><div><div>A questo punto ci rimangono da definire due elementi essenziali: l'agente e la reward.</div><div><br></div><div>Partiamo dalla seconda in modo da inserirla come funzione nel nostro Custom Template Enviroment e completarlo.</div></div><div>La <b>reward </b>guida l'apprendimento dell'agente ed è essenziale per il risultato finale. Un buon segnale ricompensa è in grado di discriminare le azioni sbagliate e promuovere quelle corrette senza essere ambiguo o eccessivamente complesso.</div><div>Un primo approccio nel nostro caso potrebbe essere quello di dare una reward negativa piuttosto consistente quando l'agente subisce goal e ricompensarlo invece con una piccola reward positiva per ogni istante/frame di gioco dove la palla non entra in goal. Una ricompensa del genere stimola l'agente a tenere la palla in campo il più a lungo possibile, quindi cercherà di non subire goal. Questa reward ha il vantaggio di essere molto semplice da implementare e piuttosto "universale", nel senso che non limita le azioni dell'agente. Se scrivessi infatti una reward troppo specifica basandomi magari su come giocherei io, l'agente imparerebbe a giocare al massimo delle mie capacità, noi invece vogliamo che gli unici limiti della nostra IA siano quelli imposti dalle regole del gioco.</div><div>La reward sopra definita è già un ottimo punto di partenza, ma per velocizzare l'apprendimento e guidare meglio l'agente verso lo scopo ultimo del gioco (ho preferito concentrarmi sul 'non prendere goal' piuttosto che 'fare goal', credo che come strategia paghi di più) ho aggiunto un valore alla reward che ho chiamato InterceptReward che si applica ogni volta che la palla è sulla traiettoria della porta e l'agente si frappone/non si frappone per impedire il goal. Ovviamente nel primo caso riceverà una reward positiva (sempre relativamente piccola) e nel secondo una negativa. Questo dovrebbe aiutarlo meglio a capire cosa fare visto che se consideriamo che una partita dura in media 4oo steps, ad ogni step gli arrivano 5 input in base ai quali deve scegliere una di 3 azioni diverse, potrebbe essere faticoso capire come comportarsi, almeno all'inizio.</div><div><span style="letter-spacing: normal;"><br></span></div><div><span style="letter-spacing: normal;">Per finire creiamo il nostro agente, il protagonista del nostro esperimento. Per farlo useremo sempre la libreria di Matlab, in particolare scegliamo <b>DQN Agent</b>. Ci sono diversi agenti di Reinforcement Learning, ognuno con le sue peculiarità. Nulla vieta di provarli tutti e vedere quale si comporta meglio. Io ho scelto il DQN perché, innanzitutto, supporta un spazio input continuo e uno spazio azioni discreto, quello che serve a noi, poi leggendo i modello matematico e le informazione fornite sulla documentazione mi sembrava un ottimo candidato.</span></div><div>Per definire formalmente l'agente abbiamo bisogno di una <b>rete neurale</b> e un set di opzioni che specifichino nel dettaglio come ottimizzare i parametri matematici dell'algoritmo di apprendimento del DQN.</div><div>Per la rete neurale avremo due percorsi, uno con un layer di input a 5 neuroni (che corrisponde al numero di osservazioni) e uno a 3 neuroni (che corrisponde al numero di azioni), entrambi questi percorsi proseguono con strati di <i>FullyConnectedLayer</i> a 30 neuroni collegati da funzioni <i>Relu</i>, infine i due rami si fondono in un percorso comune che restituisce il risultato. Di seguito riporto lo schema per chiarezza.<br></div><div style=""> https://www.wearestudents.it/post/php/getUploadedMedia.php?tkn=urq0hxdXMb5d3dedbb288d3&amp;mime=image%2Fpng <br></div><div><span style="letter-spacing: normal;"><i style=""><br></i></span></div><div><i style=""><div style=""><br></div><div style=""><b>'UseDoubleDQN',true, ...</b>migliora la stima dell'azione</div><div style=""><br></div><div style=""><br></div><div style=""><b>'TargetUpdateMethod',"periodic", ... </b>aggiorna ad ogni partita i valori della rete</div><div style=""><br></div><div style=""><b><br></b></div><div style=""><b>'TargetUpdateFrequency',1, ... </b>vedi riga sopra</div><div style=""><br></div><div style=""><b><br></b></div><div style=""><b>'NumStepsToLookAhead',3, ... </b>la documentazione è ambigua su questo parametro ma suppongo sia quello che suggerisce il nome. D<span style="letter-spacing: normal;">i default era 1 ma poiché in un frame la palla si muove di poco e compiere una azione piuttosto che un altra risulta quasi irrilevante ho pensato che 3 frame fosse più sensato</span></div><div style=""><br></div><div style=""><b><br></b></div><div style=""><b>'ExperienceBufferLength',100000, ... </b>set di esperienze(partite giocate) con le quali aggiorna la rete</div><div style=""><br></div><div style=""><b><br></b></div><div style=""><b>'MiniBatchSize',256</b><b>, ...</b><span style="letter-spacing: normal;">quante esperienze usa per aggiornare la rete ogni volta</span></div><div style=""><br></div><div style=""><b>agentOpts.EpsilonGreedyExploration.Epsilon = 1; </b>fondamentale per evitare la caduta in un minimo locale e un valore subottimale</div><div style=""><b>agentOpts.EpsilonGreedyExploration.EpsilonDecay =0.0001; </b>insieme con il valore di sopra promuovono un'ampia esplorazione dello spazio delle azioni</div><div style=""><br></div></i></div><div style=""><span style="letter-spacing: normal;">Ora è tutto pronto, non ci resta che lanciare il training e vedere come si comporta.</span></div><div style=""><span style="letter-spacing: normal;"><br></span></div><div style="">Sfortunatamente i risultati sono alquanto deludenti e l'agente sembra smettere di imparare dopo appena qualche episodio. Per settimane ho cambiato parametri e valori ma il risultato era sempre lo stesso: la barra controllata dall'agente si piazzava in un angolo, in genere quello basso, e ci restava per tutto il resto della partita. Non riuscendo a spiegare questo comportamento in nessun modo (sembrava che la rete convergesse a quel valore dal suo punto di vista "ottimale" e non provasse nemmeno a cambiare azione, nonostante mettersi in un angolo è probabilmente la scelta peggiore da fare in tutto il gioco di Pong) ho chiesto consiglio sul forum di Matlab e ne ho ricavato che un buon punto di partenza era cambiare la <i>Epsilon</i><i>GreedyExploration </i>(i valori riportati sopra sono già quelli aggiornati). In effetti dalle successive sessioni di allenamento ho iniziato a vedere dei risultati incoraggianti. Dopo un training di 215 partite durato un paio d'ore il nostro agente finalmente sembra aver imparato a giocare, nonostante alcune volte subisca ancora certi goal clamorosi. In teoria un DQN agent ha bisogno di molti più episodi per imparare, almeno qualche migliaio, ma per motivi ancora da indagare più alleno questo agente più torna ad esibire il fatidico comportamento del posizionarsi nell'angolo... forse è il suo modo di dirmi che non gli piace il gioco, o magari sente che la sua intelligenza è sprecata a rincorrere una pallina verde.</div><div style=""><br></div><div style="">Ad ogni modo se volete vedere l'IA in azione ho pubblicato il video su: https://twitter.com/FedericoSello/status/1155456721298608129 <br></div>Come avrete sicuramente notato ha una predilezione particolare per gli angoli.<br></div><div style=""><u><br></u></div><div style="">Naturalmente c'è ancora un ampio margine di miglioramento, ed in futuro proverò ad ottimizzarlo ulteriormente.</div>