Schooling Flappy Bird: un tutorial per l'apprendimento del rinforzo

Pubblicato: 2022-03-11

Nella programmazione classica, le istruzioni del software sono create esplicitamente dai programmatori e non si apprende nulla dai dati. Al contrario, l'apprendimento automatico è un campo dell'informatica che utilizza metodi statistici per consentire ai computer di apprendere ed estrarre conoscenza dai dati senza essere programmati esplicitamente.

In questo tutorial sull'apprendimento per rinforzo, mostrerò come possiamo usare PyTorch per insegnare a una rete neurale di apprendimento per rinforzo come giocare a Flappy Bird. Ma prima, dovremo coprire una serie di elementi costitutivi.

Gli algoritmi di apprendimento automatico possono essere approssimativamente divisi in due parti: algoritmi di apprendimento tradizionali e algoritmi di apprendimento profondo. Gli algoritmi di apprendimento tradizionali di solito hanno molti meno parametri di apprendimento rispetto agli algoritmi di apprendimento profondo e hanno una capacità di apprendimento molto inferiore.

Inoltre, gli algoritmi di apprendimento tradizionali non sono in grado di eseguire l'estrazione di funzionalità : gli specialisti di intelligenza artificiale devono trovare una buona rappresentazione dei dati che viene quindi inviata all'algoritmo di apprendimento. Esempi di tecniche di apprendimento automatico tradizionali includono SVM, foresta casuale, albero decisionale e $k$-means, mentre l'algoritmo centrale nell'apprendimento profondo è la rete neurale profonda .

L'input per una rete neurale profonda può essere costituito da immagini grezze e uno specialista di intelligenza artificiale non ha bisogno di trovare alcuna rappresentazione dei dati: la rete neurale trova la rappresentazione migliore durante il processo di addestramento.

Molte tecniche di deep learning sono note da molto tempo, ma i recenti progressi nell'hardware hanno rapidamente potenziato la ricerca e lo sviluppo del deep learning. Nvidia è responsabile dell'espansione del campo perché le sue GPU hanno consentito esperimenti di deep learning veloci.

Parametri apprendibili e iperparametri

Gli algoritmi di apprendimento automatico sono costituiti da parametri apprendibili che sono sintonizzati nel processo di addestramento e parametri non apprendibili che sono impostati prima del processo di addestramento. I parametri impostati prima dell'apprendimento sono chiamati iperparametri .

La ricerca nella griglia è un metodo comune per trovare gli iperparametri ottimali. È un metodo di forza bruta: significa provare tutte le possibili combinazioni di iperparametri su un intervallo definito e scegliere la combinazione che massimizza una metrica predefinita.

Algoritmi di apprendimento supervisionato, non supervisionato e di rinforzo

Un modo per classificare gli algoritmi di apprendimento è tracciare una linea tra algoritmi supervisionati e non supervisionati. (Ma non è necessariamente così semplice: l'apprendimento per rinforzo si trova da qualche parte tra questi due tipi.)

Quando parliamo di apprendimento supervisionato, osserviamo le coppie $ (x_i, y_i) $. $ x_i $ è l'input dell'algoritmo e $ y_i $ è l'output. Il nostro compito è trovare una funzione che esegua la mappatura corretta da $ x_i $ a $ y_i $.

Per ottimizzare i parametri di apprendimento in modo che definiscano una funzione che associ $ x_i $ a $ y_i $, è necessario definire una funzione di perdita e un ottimizzatore. Un ottimizzatore riduce al minimo la funzione di perdita. Un esempio di una funzione di perdita è l'errore quadratico medio (MSE):

\[MSE = \sum_{i=1}^{n} (y_i - \widehat{y_i} )^2\]

Qui, $ y_i $ è un'etichetta di verità fondamentale e $ \widehat{y_i} $ è un'etichetta prevista. Un ottimizzatore molto popolare nell'apprendimento profondo è la discesa del gradiente stocastico . Ci sono molte varianti che cercano di migliorare il metodo di discesa del gradiente stocastico: Adam, Adadelta, Adagrad e così via.

Gli algoritmi non supervisionati cercano di trovare la struttura nei dati senza essere forniti esplicitamente di etichette. $k$-means è uno degli esempi di algoritmi non supervisionati che cercano di trovare cluster ottimali nei dati. Di seguito è riportata un'immagine con 300 punti dati. $k$- significa che gli algoritmi hanno trovato la struttura nei dati e assegnato un'etichetta cluster a ciascun punto dati. Ogni grappolo ha il suo colore.

Cluster di punti dati divisi per colore. I cluster sono stati trovati da un algoritmo non supervisionato

L'apprendimento per rinforzo utilizza ricompense: etichette sparse e ritardate. Un agente agisce, che cambia l'ambiente, da cui può ottenere una nuova osservazione e ricompensa. Un'osservazione è lo stimolo che un agente percepisce dall'ambiente. Può essere ciò che l'agente vede, sente, odora e così via.

Una ricompensa viene assegnata all'agente quando compie un'azione. Dice all'agente quanto è buona l'azione. Percependo osservazioni e ricompense, un agente impara come comportarsi in modo ottimale nell'ambiente. Entrerò in questo in modo più dettagliato di seguito.

Apprendimento per rinforzo attivo, passivo e inverso

Esistono diversi approcci a questa tecnica. Prima di tutto, c'è l'apprendimento per rinforzo attivo, che stiamo usando qui. Al contrario, c'è l'apprendimento per rinforzo passivo, in cui le ricompense sono semplicemente un altro tipo di osservazione e le decisioni vengono invece prese secondo una politica fissa.

Infine, l'apprendimento per rinforzo inverso cerca di ricostruire una funzione di ricompensa data la storia delle azioni e delle loro ricompense nei vari stati.

Generalizzazione, Overfitting e Underfitting

Qualsiasi istanza fissa di parametri e iperparametri è chiamata modello. Gli esperimenti di machine learning di solito consistono in due parti: formazione e test.

Durante il processo di addestramento, i parametri apprendibili vengono ottimizzati utilizzando i dati di addestramento. Nel processo di test, i parametri apprendibili vengono congelati e il compito è verificare quanto bene il modello effettua previsioni su dati non visti in precedenza. La generalizzazione è la capacità di una macchina di apprendimento di eseguire in modo accurato un nuovo esempio o attività invisibile dopo aver sperimentato un set di dati di apprendimento.

Se un modello è troppo semplice rispetto ai dati, non sarà in grado di adattarsi ai dati di addestramento e funzionerà male sia sul set di dati di addestramento che su quello di test. In tal caso, diciamo che il modello è underfitting .

Se un modello di machine learning funziona bene su un set di dati di addestramento, ma male su un set di dati di test, diciamo che è overfitting . L'overfitting è la situazione in cui un modello è troppo complesso rispetto ai dati. Può adattarsi perfettamente ai dati di addestramento, ma si adatta così tanto al set di dati di addestramento da funzionare male sui dati di test, ovvero semplicemente non si generalizza.

Di seguito è riportata un'immagine che mostra underfitting e overfitting rispetto a una situazione di equilibrio tra i dati complessivi e la funzione di previsione.

Grafici di underfitting, balance e overfitting. Una funzione bilanciata segue abbastanza bene l'andamento generale dei punti dati, senza attenersi troppo ai singoli punti dati

Scalabilità

I dati sono fondamentali nella creazione di modelli di machine learning. Di solito, gli algoritmi di apprendimento tradizionali non richiedono troppi dati. Ma a causa della loro capacità limitata, anche le prestazioni sono limitate. Di seguito è riportato un grafico che mostra come i metodi di deep learning si adattano bene rispetto ai tradizionali algoritmi di machine learning.

Prestazioni rispetto alla quantità di dati per il deep learning e gli algoritmi tradizionali. Le reti neurali funzionano meglio su larga scala.

Reti neurali

Le reti neurali sono costituite da più livelli. L'immagine seguente mostra una semplice rete neurale con quattro livelli. Il primo livello è il livello di input e l'ultimo livello è il livello di output. I due livelli tra i livelli di input e di output sono livelli nascosti.

Grafico della rete neurale, che mostra ogni nodo di un livello di input mappato su ogni nodo di Hidden Layer 1, a sua volta mappato su ogni nodo di Hidden Layer 2, e infine mappato sul livello di output, che consiste in un singolo nodo

Se una rete neurale ha più di un livello nascosto, la chiamiamo rete neurale profonda. L'input set $ X $ viene fornito alla rete neurale e si ottiene l'output $ y $. L'apprendimento viene effettuato utilizzando un algoritmo di backpropagation che combina una funzione di perdita e un ottimizzatore.

La backpropagation è composta da due parti: un passaggio in avanti e un passaggio all'indietro. Nel passaggio in avanti, i dati di input vengono inseriti nell'input della rete neurale e si ottiene l'output. Viene calcolata la perdita tra la verità fondamentale e la previsione, quindi nel passaggio all'indietro, i parametri delle reti neurali vengono regolati rispetto alla perdita.

Rete neurale convoluzionale

Una variazione della rete neurale è la rete neurale convoluzionale . Viene utilizzato principalmente per attività di visione artificiale.

Lo strato più importante nelle reti neurali convoluzionali è lo strato convoluzionale (da cui il nome). I suoi parametri sono costituiti da filtri conoscibili, chiamati anche kernel. I livelli convoluzionali applicano un'operazione di convoluzione all'input, passando il risultato al livello successivo. L'operazione di convoluzione riduce il numero di parametri apprendibili, funzionando come una sorta di euristica e rendendo la rete neurale più facile da addestrare.

Di seguito è riportato come funziona un kernel convoluzionale in uno strato convoluzionale. Il kernel viene applicato all'immagine e si ottiene una caratteristica convogliata.

L'animazione che evidenzia un kernel e il suo funzionamento come immagine viene elaborata e l'output della funzione convoluta corrispondente

I livelli ReLU vengono utilizzati per introdurre non linearità nella rete neurale. Le non linearità sono importanti perché con esse possiamo modellare tutti i tipi di funzioni, non solo quelle lineari, rendendo la rete neurale un approssimatore di funzioni universale. Questo fa sì che una funzione ReLU sia definita in questo modo:

\[ReLU = \max(0, x)\]

ReLU è uno degli esempi delle cosiddette funzioni di attivazione utilizzate per introdurre non linearità nelle reti neurali. Esempi di altre funzioni di attivazione includono funzioni sigmoidee e ipertangenti. ReLU è la funzione di attivazione più popolare perché è dimostrato che rende la rete neurale addestrata in modo più efficiente rispetto ad altre funzioni di attivazione.

Di seguito è riportato un grafico di una funzione ReLU.

Una funzione ReLU, simile alla diagonale semplice di un grafico y=x, ma con tutti i valori x negativi mappati su zero

Come puoi vedere, questa funzione ReLU cambia semplicemente i valori negativi in ​​zeri. Questo aiuta a prevenire il problema del gradiente di scomparsa. Se un gradiente svanisce, non avrà un grande impatto sull'ottimizzazione del peso della rete neurale.

Una rete neurale convoluzionale è costituita da più livelli: livelli convoluzionali, livelli ReLU e livelli completamente connessi. Gli strati completamente connessi collegano ogni neurone in uno strato a ogni neurone in un altro strato, come si vede con i due strati nascosti nell'immagine all'inizio di questa sezione. L'ultimo livello completamente connesso associa gli output del livello precedente, in questo caso, ai valori number_of_actions .

Applicazioni

Il deep learning ha successo e supera i classici algoritmi di machine learning in diversi sottocampi di machine learning, tra cui la visione artificiale, il riconoscimento vocale e l'apprendimento per rinforzo. Questi campi del deep learning sono applicati in vari domini del mondo reale: finanza, medicina, intrattenimento, ecc.

Insegnamento rafforzativo

L'apprendimento per rinforzo si basa su un agente . Un agente compie azioni in un ambiente e ne ottiene osservazioni e ricompense. Un agente deve essere addestrato per massimizzare la ricompensa cumulativa. Come notato nell'introduzione, con gli algoritmi di apprendimento automatico classici, gli ingegneri di apprendimento automatico devono eseguire l'estrazione di funzionalità, ovvero creare buone funzionalità che rappresentino bene l'ambiente e che siano inserite in un algoritmo di apprendimento automatico.

Utilizzando il deep learning, è possibile creare un sistema end-to-end che riceve input ad alta dimensione, ad esempio video, e da esso apprende la strategia ottimale per un agente per intraprendere buone azioni.

Nel 2013, la startup londinese di intelligenza artificiale DeepMind ha creato una grande svolta nell'apprendimento del controllo degli agenti direttamente da input sensoriali ad alta dimensione. Hanno pubblicato un articolo, Playing Atari with Deep Reinforcement Learning , in cui hanno mostrato come hanno insegnato a una rete neurale artificiale a giocare ai giochi Atari semplicemente guardando lo schermo. Sono stati acquisiti da Google e quindi pubblicato un nuovo articolo su Nature con alcuni miglioramenti: controllo a livello umano attraverso l'apprendimento per rinforzo profondo .

A differenza di altri paradigmi di apprendimento automatico, l'apprendimento per rinforzo non ha un supervisore, ma solo un segnale di ricompensa. Il feedback è ritardato: non è istantaneo come negli algoritmi di apprendimento supervisionato. I dati sono sequenziali e le azioni di un agente influiscono sui dati successivi che riceve.

Ora, un agente è situato nel suo ambiente, che è in un certo stato. Per descrivere questo in modo più dettagliato, utilizziamo un processo decisionale di Markov, che è un modo formale per modellare questo ambiente di apprendimento per rinforzo. È costituito da un insieme di stati, un insieme di possibili azioni e regole (ad es. probabilità) per la transizione da uno stato all'altro.

Un grafico del processo decisionale di Markov: gli stati (contrassegnati con 'S') intraprendono azioni (contrassegnati con 'a') che quindi hanno varie probabilità per lo stato in cui l'agente finisce dopo; alcuni percorsi seguiti indicano anche una ricompensa

L'agente è in grado di eseguire azioni, trasformando l'ambiente. Chiamiamo la ricompensa $ R_t $. È un segnale di feedback scalare che indica quanto bene sta facendo l'agente al passaggio $t$.

Un agente di rete neurale decide quale azione intraprendere in ogni fase in base a osservazioni e ricompense.

Per una buona performance a lungo termine, devono essere presi in considerazione non solo i premi immediati, ma anche quelli futuri. La ricompensa totale per un episodio del passaggio temporale $t$ è $ R_t = r_t + r_{t+1} + r_{t+2} + \ldots + r_n $. Il futuro è incerto e più andiamo avanti nel futuro, più le previsioni future potrebbero divergere. Per questo motivo, viene utilizzato un premio futuro scontato: $ R_t = r_t +\gamma r_{t+1} + \gamma^2r_{t+2} + \ldots + \gamma^{nt}r_n = r_t + \gamma R_{t+1} $. Un agente dovrebbe scegliere l'azione che massimizza la ricompensa futura scontata.

Apprendimento Q profondo

La funzione $ Q(s, a) $ rappresenta la ricompensa futura massima scontata quando l'azione $ a $ viene eseguita nello stato $ s $:

\[Q(s_t, a_t) = \max R_{t+1}\]

La stima di una ricompensa futura è data dall'equazione di Bellman: $ Q(s, a) = r + \gamma \max_{a'}Q(s', a') $ . In altre parole, la massima ricompensa futura data a uno stato $ s $ e un'azione $ a $ è la ricompensa immediata più la massima ricompensa futura per lo stato successivo.

L'approssimazione dei valori Q utilizzando funzioni non lineari (reti neurali) non è molto stabile. Per questo motivo, la riproduzione dell'esperienza viene utilizzata per la stabilità. L'esperienza durante gli episodi di una sessione di allenamento viene archiviata in una memoria di riproduzione. Vengono utilizzati mini-batch casuali dalla memoria di riproduzione invece di utilizzare la transizione più recente. Ciò interrompe la somiglianza dei successivi campioni di addestramento che altrimenti porterebbero la rete neurale al minimo locale.

Ci sono altri due aspetti importanti da menzionare sul Q-learning profondo: esplorazione e sfruttamento. Con lo sfruttamento, viene presa la decisione migliore date le informazioni attuali. L'esplorazione raccoglie più informazioni.

Quando l'algoritmo esegue un'azione proposta dalla rete neurale, sta facendo sfruttamento: sfrutta le conoscenze apprese dalla rete neurale. Al contrario, un algoritmo può intraprendere un'azione casuale, esplorando nuove possibilità e introducendo potenziali nuove conoscenze nella rete neurale.

Di seguito è mostrato l'"algoritmo di Deep Q-learning con Experience Replay" dal documento di DeepMind Playing Atari with Deep Reinforcement Learning.

L'algoritmo Deep Q-learning con Experience Replay in pseudocodice

DeepMind si riferisce a reti convoluzionali addestrate con il loro approccio come Deep Q-networks (DQN).

Esempio di deep Q-learning con Flappy Bird

Flappy Bird era un popolare gioco per cellulare originariamente sviluppato dall'artista e programmatore di videogiochi vietnamita Dong Nguyen. In esso, il giocatore controlla un uccello e cerca di volare tra i tubi verdi senza colpirlo.

Di seguito è riportato uno screenshot di un clone di Flappy Bird codificato utilizzando PyGame:

Uno screenshot di FlapPyBird, un clone di Flappy Bird codificato utilizzando PyGame

Da allora il clone è stato biforcato e modificato: lo sfondo, i suoni e i diversi stili di uccelli e pipe sono stati rimossi e il codice è stato modificato in modo che possa essere facilmente utilizzato con semplici framework di apprendimento per rinforzo. Il motore di gioco modificato è tratto da questo progetto TensorFlow:

Uno screenshot di DeepLearningFlappyBird, un fork del clone con grafica semplificata

Ma invece di usare TensorFlow, ho creato un framework di apprendimento per rinforzo profondo usando PyTorch. PyTorch è un framework di deep learning per una sperimentazione veloce e flessibile. Fornisce tensori e reti neurali dinamiche in Python con una forte accelerazione GPU.

L'architettura della rete neurale è la stessa di DeepMind utilizzata nel documento Controllo a livello umano attraverso l'apprendimento per rinforzo profondo .

Strato Ingresso Dimensione del filtro Passo Numero di filtri Attivazione Produzione
conv1 84x84x4 8x8 4 32 ReLU 20x20x32
conv2 20x20x32 4x4 2 64 ReLU 9x9x64
conv3 9x9x64 3x3 1 64 ReLU 7x7x64
fc4 7x7x64 512 ReLU 512
fc5 512 2 Lineare 2

Ci sono tre strati convoluzionali e due strati completamente collegati. Ogni livello utilizza l'attivazione ReLU, tranne l'ultimo, che utilizza l'attivazione lineare. La rete neurale emette due valori che rappresentano le uniche azioni possibili del giocatore: "Vola su" e "non fare nulla".

L'input è costituito da quattro immagini in bianco e nero 84x84 consecutive. Di seguito è riportato un esempio di quattro immagini che vengono inviate alla rete neurale.

Noterai che le immagini sono ruotate. Questo perché l'output del motore di gioco del clone viene ruotato. Ma se la rete neurale viene insegnata e quindi testata utilizzando tali immagini, non influirà sulle sue prestazioni.

Quattro fotogrammi in bianco e nero consecutivi del clone di Flappy Bird, inviati direttamente a una rete neurale

Potresti anche notare che l'immagine è ritagliata in modo da omettere il pavimento, perché è irrilevante per questo compito. Tutti i pixel che rappresentano i tubi e l'uccello sono bianchi e tutti i pixel che rappresentano lo sfondo sono neri.

Questo fa parte del codice che definisce la rete neurale. I pesi delle reti neurali vengono inizializzati per seguire la distribuzione uniforme $\mathcal{U}(-0.01, 0.01)$. La parte di bias dei parametri delle reti neurali è impostata su 0,01. Sono state provate diverse inizializzazioni (Xavier uniforme, Xavier normale, Kaiming uniforme, Kaiming normale, uniforme e normale), ma l'inizializzazione di cui sopra ha fatto convergere la rete neurale e l'allenamento è stato il più veloce. La dimensione della rete neurale è di 6,8 MB.

 class NeuralNetwork(nn.Module): def __init__(self): super(NeuralNetwork, self).__init__() self.number_of_actions = 2 self.gamma = 0.99 self.final_epsilon = 0.0001 self.initial_epsilon = 0.1 self.number_of_iterations = 2000000 self.replay_memory_size = 10000 self.minibatch_size = 32 self.conv1 = nn.Conv2d(4, 32, 8, 4) self.relu1 = nn.ReLU(inplace=True) self.conv2 = nn.Conv2d(32, 64, 4, 2) self.relu2 = nn.ReLU(inplace=True) self.conv3 = nn.Conv2d(64, 64, 3, 1) self.relu3 = nn.ReLU(inplace=True) self.fc4 = nn.Linear(3136, 512) self.relu4 = nn.ReLU(inplace=True) self.fc5 = nn.Linear(512, self.number_of_actions) def forward(self, x): out = self.conv1(x) out = self.relu1(out) out = self.conv2(out) out = self.relu2(out) out = self.conv3(out) out = self.relu3(out) out = out.view(out.size()[0], -1) out = self.fc4(out) out = self.relu4(out) out = self.fc5(out) return out

Nel costruttore, noterai che ci sono degli iperparametri definiti. L'ottimizzazione degli iperparametri non viene eseguita ai fini di questo post del blog. Invece, gli iperparametri sono usati principalmente dai documenti di DeepMind. Qui, alcuni degli iperparametri sono ridimensionati per essere inferiori rispetto all'articolo di DeepMind, perché Flappy Bird è meno complesso dei giochi Atari che hanno usato per l'ottimizzazione.

Inoltre, epsilon è stato modificato per essere molto più ragionevole per questo gioco. DeepMind usa un epsilon di uno, ma qui usiamo 0.1. Questo perché epsilon più alti costringono l'uccello a sbattere molto, il che spinge l'uccello verso il bordo superiore dello schermo, provocando sempre lo schianto dell'uccello contro un tubo.

Il codice di apprendimento per rinforzo ha due modalità: Train e test. Durante la fase di test, possiamo vedere quanto bene l'algoritmo di apprendimento per rinforzo ha imparato a giocare. Ma prima, la rete neurale deve essere addestrata. Dobbiamo definire la funzione di perdita da minimizzare e gli ottimizzatori che ridurranno al minimo la funzione di perdita. Useremo il metodo di ottimizzazione di Adam e l'errore quadratico medio per la funzione di perdita:

 optimizer = optim.Adam(model.parameters(), lr=1e-6) criterion = nn.MSELoss()

Il gioco dovrebbe essere istanziato:

 game_state = GameState()

La memoria di riproduzione è definita come un elenco Python:

 replay_memory = []

Ora dobbiamo inizializzare il primo stato. Un'azione è un tensore bidimensionale:

  • [1, 0] rappresenta "non fare nulla"
  • [0, 1] rappresenta "volare in alto"

Il metodo frame_step ci fornisce la schermata successiva, la ricompensa e le informazioni sul fatto che lo stato successivo sia terminale. La ricompensa è 0.1 per ogni mossa dell'uccello senza morire quando non sta attraversando un tubo, 1 se l'uccello passa con successo attraverso un tubo e -1 se l'uccello si schianta.

La funzione resize_and_bgr2gray il pavimento, ridimensiona lo schermo a un'immagine 84x84 e cambia lo spazio colore da BGR a bianco e nero. La funzione image_to_tensor converte l'immagine in un tensore PyTorch e la inserisce nella memoria della GPU se CUDA è disponibile. Infine, le ultime quattro schermate sequenziali vengono concatenate insieme e sono pronte per essere inviate alla rete neurale.

 action = torch.zeros([model.number_of_actions], dtype=torch.float32) action[0] = 1 image_data, reward, terminal = game_state.frame_step(action) image_data = resize_and_bgr2gray(image_data) image_data = image_to_tensor(image_data) state = torch.cat((image_data, image_data, image_data, image_data)).unsqueeze(0)

L'epsilon iniziale viene impostato utilizzando questa riga di codice:

 epsilon = model.initial_epsilon

Segue il ciclo infinito principale. I commenti sono scritti nel codice e puoi confrontare il codice con l'algoritmo Deep Q-learning con Experience Replay scritto sopra.

L'algoritmo campiona i mini-batch dalla memoria di riproduzione e aggiorna i parametri della rete neurale. Le azioni vengono eseguite utilizzando l'esplorazione avida di epsilon . Epsilon viene ricotto nel tempo. La funzione di perdita ridotta al minimo è $ L = \frac{1}{2}\left[\max_{a'}Q(s', a') - Q(s, a)\right]^2 $ . $ Q(s, a) $ è il valore di verità fondamentale calcolato utilizzando l'equazione di Bellman e $ \max_{a'}Q(s', a') $ è ottenuto dalla rete neurale. La rete neurale fornisce due valori Q per le due possibili azioni e l'algoritmo esegue l'azione con il valore Q più alto.

 while iteration < model.number_of_iterations: # get output from the neural network output = model(state)[0] # initialize action action = torch.zeros([model.number_of_actions], dtype=torch.float32) if torch.cuda.is_available(): # put on GPU if CUDA is available action = action.cuda() # epsilon greedy exploration random_action = random.random() <= epsilon if random_action: print("Performed random action!") action_index = [torch.randint(model.number_of_actions, torch.Size([]), dtype=torch.int) if random_action else torch.argmax(output)][0] if torch.cuda.is_available(): # put on GPU if CUDA is available action_index = action_index.cuda() action[action_index] = 1 # get next state and reward image_data_1, reward, terminal = game_state.frame_step(action) image_data_1 = resize_and_bgr2gray(image_data_1) image_data_1 = image_to_tensor(image_data_1) state_1 = torch.cat((state.squeeze(0)[1:, :, :], image_data_1)).unsqueeze(0) action = action.unsqueeze(0) reward = torch.from_numpy(np.array([reward], dtype=np.float32)).unsqueeze(0) # save transition to replay memory replay_memory.append((state, action, reward, state_1, terminal)) # if replay memory is full, remove the oldest transition if len(replay_memory) > model.replay_memory_size: replay_memory.pop(0) # epsilon annealing epsilon = epsilon_decrements[iteration] # sample random minibatch minibatch = random.sample(replay_memory, min(len(replay_memory), model.minibatch_size)) # unpack minibatch state_batch = torch.cat(tuple(d[0] for d in minibatch)) action_batch = torch.cat(tuple(d[1] for d in minibatch)) reward_batch = torch.cat(tuple(d[2] for d in minibatch)) state_1_batch = torch.cat(tuple(d[3] for d in minibatch)) if torch.cuda.is_available(): # put on GPU if CUDA is available state_batch = state_batch.cuda() action_batch = action_batch.cuda() reward_batch = reward_batch.cuda() state_1_batch = state_1_batch.cuda() # get output for the next state output_1_batch = model(state_1_batch) # set y_j to r_j for terminal state, otherwise to r_j + gamma*max(Q) y_batch = torch.cat(tuple(reward_batch[i] if minibatch[i][4] else reward_batch[i] + model.gamma * torch.max(output_1_batch[i]) for i in range(len(minibatch)))) # extract Q-value q_value = torch.sum(model(state_batch) * action_batch, dim=1) # PyTorch accumulates gradients by default, so they need to be reset in each pass optimizer.zero_grad() # returns a new Tensor, detached from the current graph, the result will never require gradient y_batch = y_batch.detach() # calculate loss loss = criterion(q_value, y_batch) # do backward pass loss.backward() optimizer.step() # set state to be state_1 state = state_1

Ora che tutti i pezzi sono a posto, ecco una panoramica di alto livello del flusso di dati utilizzando la nostra rete neurale:

Panoramica finale di alto livello del flusso di dati utilizzando la nostra rete neurale: lo stato di input è costituito da quattro schermate consecutive. Da ciò, la rete neurale fornisce due valori Q per le due possibili azioni ("non fare nulla" e "volare su")

Ecco una breve sequenza con una rete neurale addestrata.

Un'animazione del clone Flappy Bird riprodotto dalla rete neurale addestrata risultante

La rete neurale mostrata sopra è stata addestrata utilizzando una GPU Nvidia GTX 1080 di fascia alta per alcune ore; utilizzando invece una soluzione basata su CPU, questa particolare attività richiederebbe diversi giorni. Gli FPS del motore di gioco sono stati impostati su un numero molto elevato durante l'allenamento: 999...999, in altre parole, quanti più fotogrammi al secondo possibile. In fase di test, l'FPS è stato impostato a 30.

Di seguito è riportato un grafico che mostra come il valore Q massimo è cambiato durante le iterazioni. Viene mostrata ogni 10.000 iterazione. Un picco al ribasso significa che per un particolare frame (un'iterazione è un frame) la rete neurale prevede che l'uccello riceverà una ricompensa molto bassa in futuro, ovvero che andrà in crash molto presto.

Un grafico che mostra come il valore Q massimo è cambiato durante le iterazioni. Partendo da zero e con diversi picchi al ribasso, mostra una tendenza generale verso un valore Q massimo intorno a 12 o 13 dopo circa un milione di iterazioni.

Il codice completo e il modello preaddestrato sono disponibili qui.

Apprendimento per rinforzo profondo: 2D, 3D e persino la vita reale

In questo tutorial sull'apprendimento per rinforzo di PyTorch, ho mostrato come un computer può imparare a giocare a Flappy Bird senza alcuna conoscenza precedente del gioco, utilizzando solo un approccio per tentativi ed errori come farebbe un essere umano quando incontra il gioco per la prima volta.

È interessante notare che l'algoritmo può essere implementato in poche righe di codice utilizzando il framework PyTorch. Il documento su cui si basa il metodo in questo blog è relativamente vecchio e sono disponibili molti documenti più recenti con varie modifiche che consentono una convergenza più rapida. Da allora, l'apprendimento per rinforzo profondo è stato utilizzato per i giochi 3D e nei sistemi robotici del mondo reale.

Aziende come DeepMind, Maluuba e Vicarious stanno lavorando intensamente sull'apprendimento per rinforzo profondo. Tale tecnologia è stata utilizzata in AlphaGo, che ha battuto Lee Sedol, uno dei migliori giocatori del mondo a Go. A quel tempo, si riteneva che ci sarebbero voluti almeno dieci anni prima che le macchine potessero sconfiggere i migliori giocatori di Go.

L'ondata di interesse e investimenti nell'apprendimento per rinforzo profondo (e nell'intelligenza artificiale in generale) potrebbe persino portare a una potenziale intelligenza artificiale generale (AGI), intelligenza a livello umano (o anche oltre) che può essere espressa sotto forma di algoritmo e simulato al computer. Ma l'AGI dovrà essere oggetto di un altro articolo.


Riferimenti:

  • Demistificare l'apprendimento per rinforzo profondo
  • Apprendimento profondo per rinforzo per Flappy Bird
  • "Rete neurale convoluzionale" su Wikipedia
  • "Apprendimento per rinforzo" su Wikipedia
  • "Processo decisionale di Markov" su Wikipedia
  • Corso dell'University College London su RL
Correlati: un tutorial di deep learning: dai perceptron alle reti profonde