L’Ottimizzazione della Politica Prossimale (PPO) è uno degli algoritmi preferiti per risolvere i problemi di Apprendimento per Rinforzo (RL). È stato sviluppato nel 2017 da John Schuman, co-fondatore di OpenAI.
PPO è stato ampiamente utilizzato in OpenAI per addestrare modelli in grado di emulare comportamenti simili a quelli umani. Migliora rispetto a metodi precedenti come l’Ottimizzazione della Politica nella Regione di Fiducia (TRPO) ed è diventato popolare perché è un algoritmo robusto ed efficiente.
In questo tutorial, esaminiamo PPO in profondità. Copriamo la teoria e dimostriamo come implementarlo utilizzando PyTorch.
Comprendere l’Ottimizzazione della Politica Prossimale (PPO)
Gli algoritmi di apprendimento supervisionato convenzionali aggiornano i parametri lungo la direzione del gradiente più ripido. Se questo aggiornamento risulta eccessivo, viene corretto durante gli esempi di addestramento successivi, che sono indipendenti l’uno dall’altro.
Tuttavia, gli esempi di addestramento nell’apprendimento per rinforzo consistono nelle azioni e nei ritorni dell’agente. Pertanto, gli esempi di addestramento sono correlati tra loro. L’agente esplora l’ambiente per determinare la politica ottimale. Pertanto, apportare grandi modifiche al gradiente può portare la politica a bloccarsi in una cattiva regione con ricompense subottimali. Poiché l’agente deve esplorare l’ambiente, grandi cambiamenti nella politica rendono il processo di addestramento instabile.
I metodi basati sulla regione di fiducia mirano a evitare questo problema garantendo che gli aggiornamenti della politica rimangano all’interno di una regione di fiducia. Questa regione di fiducia è una zona artificialmente vincolata all’interno dello spazio delle politiche in cui sono consentiti gli aggiornamenti. La politica aggiornata può trovarsi solo all’interno di una regione di fiducia rispetto alla vecchia politica. Garantire che gli aggiornamenti della politica siano incrementali previene l’instabilità.
Aggiornamenti della politica nella regione di fiducia (TRPO)
L’algoritmo degli Aggiornamenti della Politica nella Regione di Fiducia (TRPO) è stato proposto nel 2015 da John Schulman (che ha anche proposto PPO nel 2017). Per misurare la differenza tra la vecchia politica e la politica aggiornata, TRPO utilizza la divergenza di Kullback-Leibler (KL). La divergenza KL viene utilizzata per misurare la differenza tra due distribuzioni di probabilità. TRPO si è dimostrato efficace nell’implementare regioni di fiducia.
Il problema con TRPO è la complessità computazionale associata alla divergenza KL. L’applicazione della divergenza KL deve essere espansa al secondo ordine utilizzando metodi numerici come l’espansione di Taylor. Questo è computazionalmente costoso. PPO è stato proposto come un’alternativa più semplice ed efficiente a TRPO. PPO limita il rapporto delle politiche per approssimare la regione di fiducia senza ricorrere a calcoli complessi che coinvolgono la divergenza KL.
Questo è il motivo per cui PPO è diventato preferito rispetto a TRPO nella risoluzione di problemi RL. Grazie al metodo più efficiente di stima delle aree di fiducia, PPO bilancia efficacemente prestazioni e stabilità.
L’approssimazione della politica prossimale (PPO)
PPO è spesso considerata una sottocategoria dei metodi attore-critico, che aggiornano i gradienti della politica basandosi sulla funzione di valore. I metodi Advantage Actor-critic (A2C) utilizzano un parametro chiamato vantaggio. Questo misura la differenza tra i ritorni previsti dal critico e i ritorni realizzati implementando la politica.
Per comprendere PPO, devi conoscere i suoi componenti:
- L’attore esegue la politica. È implementato come una rete neurale. Dato uno stato come input, restituisce l’azione da eseguire.
- Il critico è un altro network neurale. Prende lo stato in input e restituisce il valore atteso di tale stato. Così, il critico esprime la funzione di valore dello stato.
- I metodi basati su gradienti di politica possono scegliere di utilizzare diverse funzioni obiettivo. In particolare, PPO utilizza la funzione di vantaggio. La funzione di vantaggio misura l’importo con cui la ricompensa cumulativa (basata sulla politica implementata dall’attore) supera la ricompensa di base attesa (come previsto dal critico). L’obiettivo di PPO è aumentare la probabilità di scegliere azioni con un alto vantaggio. L’obiettivo di ottimizzazione di PPO utilizza funzioni di perdita basate su questa funzione di vantaggio.
- La funzione obiettivo clip è la principale innovazione in PPO.Impedisce grandi aggiornamenti della policy in una singola iterazione di addestramento. Limita di quanto la policy viene aggiornata in un’iterazione singola. Per misurare gli aggiornamenti incrementali della policy, i metodi basati sulla policy utilizzano il rapporto di probabilità della nuova policy rispetto alla vecchia policy.
- La perdita surrogata è la funzione obiettivo in PPO e tiene conto delle innovazioni menzionate in precedenza.Viene calcolata nel seguente modo:
- Calcolare il rapporto effettivo (come spiegato in precedenza) e moltiplicarlo per il vantaggio.
- Tagliare il rapporto in modo che si collochi all’interno di un intervallo desiderato. Moltiplicare il rapporto tagliato per il vantaggio.
- Prendere il valore minimo delle due quantità sopra.
- Nella pratica, viene aggiunto anche un termine di entropia alla perdita surrogata. Questo è chiamato bonus di entropia. Si basa sulla distribuzione matematica delle probabilità di azione. L’idea alla base del bonus di entropia è introdurre un po’ di casualità in modo controllato. Fare ciò incoraggia il processo di ottimizzazione ad esplorare lo spazio delle azioni. Un alto bonus di entropia favorisce l’esplorazione rispetto all’esplorazione.
Comprensione del meccanismo di taglio
Supponiamo che con la vecchia politica πvecchia, la probabilità di prendere l’azione a nello stato s è πvecchia(a|s). Con la nuova politica, la probabilità di prendere la stessa azione a dallo stesso stato s viene aggiornata a πnuova(a|s). Il rapporto di queste probabilità, come funzione dei parametri della politica θ, è r(θ). Quando la nuova politica rende l’azione più probabile (nello stesso stato), il rapporto è maggiore di 1 e viceversa.
Il meccanismo di clipping limita questo rapporto di probabilità in modo che le nuove probabilità di azione debbano trovarsi entro un certo percentuale delle vecchie probabilità di azione. Ad esempio, r(θ) può essere vincolato a trovarsi tra 0.8 e 1.2. Questo impedisce grandi salti, garantendo a sua volta un processo di addestramento stabile.
Nel resto di questo articolo, imparerai come assemblare i componenti per una semplice implementazione di PPO utilizzando PyTorch.
1. Preparazione dell’Ambiente
Prima di implementare PPO, è necessario installare le librerie software pre-requisite e scegliere un ambiente adatto per applicare la policy.
Installazione di PyTorch e delle librerie richieste
Dobbiamo installare il seguente software:
- PyTorch e altre librerie software, come
numpy
(per funzioni matematiche e statistiche) ematplotlib
(per tracciare grafici). - Il pacchetto software Gym open-source di OpenAI, una libreria Python che simula diversi ambienti e giochi, che possono essere risolti utilizzando il Reinforcement learning. Puoi utilizzare l’API di Gym per far interagire il tuo algoritmo con l’ambiente. Dato che la funzionalità di
gym
cambia a volte durante il processo di aggiornamento, in questo esempio, congeliamo la sua versione a0.25.2
.
Per installarlo su un server o una macchina locale, esegui:
$ pip install torch numpy matplotlib gym==0.25.2
Per installare utilizzando un Notebook come Google Colab o DataLab, usare:
!pip install torch numpy matplotlib gym==0.25.2
Creare l’ambiente(i) CartPole
Utilizzare OpenAI Gym per creare due istanze (una per l’addestramento e un’altra per il test) dell’ambiente CartPole:
env_train = gym.make('CartPole-v1') env_test = gym.make('CartPole-v1')
2. Implementazione di PPO in PyTorch
Ora, implementiamo PPO utilizzando PyTorch.
Definire la rete della policy
Come spiegato in precedenza, PPO è implementato come un modello attore-critico. L’attore implementa la policy, e il critico prevede il suo valore stimato. Sia le reti neurali dell’attore che del critico prendono lo stesso input—lo stato ad ogni passo temporale. Pertanto, i modelli dell’attore e del critico possono condividere una rete neurale comune, che è conosciuta come architettura backbone. L’attore e il critico possono estendere l’architettura backbone con strati aggiuntivi.
Definire la rete backbone
I seguenti passaggi descrivono la rete backbone:
- Implementare una rete con 3 strati – uno di input, uno nascosto e uno di output.
- Dopo gli strati di input e nascosto, utilizziamo una funzione di attivazione. In questo tutorial, scegliamo ReLU perché è computazionalmente efficiente.
- Imponiamo anche una funzione di dropout dopo gli strati di input e nascosti per ottenere una rete robusta. La funzione di dropout annulla casualmente alcuni neuroni. Ciò riduce la dipendenza da neuroni specifici e previene l’overfitting, rendendo così la rete più robusta.
Il codice seguente implementa la struttura di base:
class BackboneNetwork(nn.Module): def __init__(self, in_features, hidden_dimensions, out_features, dropout): super().__init__() self.layer1 = nn.Linear(in_features, hidden_dimensions) self.layer2 = nn.Linear(hidden_dimensions, hidden_dimensions) self.layer3 = nn.Linear(hidden_dimensions, out_features) self.dropout = nn.Dropout(dropout) def forward(self, x): x = self.layer1(x) x = f.relu(x) x = self.dropout(x) x = self.layer2(x) x = f.relu(x) x = self.dropout(x) x = self.layer3(x) return x
Definire la rete attore-critico
Ora, possiamo utilizzare questa rete per definire la classe attore-critico, ActorCritic
. L’attore modella la politica e predice l’azione. Il critico modella la funzione di valore e predice il valore. Entrambi prendono lo stato in input.
class ActorCritic(nn.Module): def __init__(self, actor, critic): super().__init__() self.actor = actor self.critic = critic def forward(self, state): action_pred = self.actor(state) value_pred = self.critic(state) return action_pred, value_pred
Istanziare le reti attore e critico
Utilizzeremo le reti definite sopra per creare un attore e un critico. Successivamente, creeremo un agente, includendo l’attore e il critico.
Prima di creare l’agente, inizializzare i parametri della rete:
- Le dimensioni dello strato nascosto, H, che è un parametro configurabile. Le dimensioni e il numero degli strati nascosti dipendono dalla complessità del problema. Utilizzeremo uno strato nascosto con dimensioni 64 x 64.
- Caratteristiche di input, N, dove N è la dimensione dell’array di stato. Lo strato di input ha dimensioni N x H. Nell’ambiente CartPole, lo stato è un array di 4 elementi. Quindi N è 4.
- Caratteristiche di output della rete attore, O, dove O è il numero di azioni nell’ambiente. Lo strato di output dell’attore ha dimensioni H x O. L’ambiente CartPole ha 2 azioni.
- Caratteristiche di output della rete critico. Poiché la rete critico predice solo il valore atteso (dato uno stato in input), il numero di caratteristiche di output è 1.
- Dropout come una frazione.
Il codice seguente mostra come dichiarare le reti attore e critico basate sulla rete di base:
def create_agent(hidden_dimensions, dropout): INPUT_FEATURES = env_train.observation_space.shape[0] HIDDEN_DIMENSIONS = hidden_dimensions ACTOR_OUTPUT_FEATURES = env_train.action_space.n CRITIC_OUTPUT_FEATURES = 1 DROPOUT = dropout actor = BackboneNetwork( INPUT_FEATURES, HIDDEN_DIMENSIONS, ACTOR_OUTPUT_FEATURES, DROPOUT) critic = BackboneNetwork( INPUT_FEATURES, HIDDEN_DIMENSIONS, CRITIC_OUTPUT_FEATURES, DROPOUT) agent = ActorCritic(actor, critic) return agent
Calcolare i rendimenti
L’ambiente fornisce una ricompensa che va da ogni passo al successivo, a seconda dell’azione dell’agente. La ricompensa, R, è espressa come:
Il rendimento è definito come il valore accumulato delle ricompense future attese. Le ricompense dai passaggi temporali che sono più distanti nel futuro sono meno preziose rispetto alle ricompense immediate. Pertanto, il rendimento viene comunemente calcolato come il rendimento scontato, G, definito come:
In questo tutorial (e in molti altri riferimenti), il rendimento si riferisce al rendimento scontato.
Per calcolare il rendimento:
- Partire dalle ricompense attese di tutti gli stati futuri.
- Moltiplicare ciascuna ricompensa futura per un esponente del fattore di sconto, . Ad esempio, la ricompensa attesa dopo 2 passaggi (dal presente) è moltiplicata per 2.
- Somma tutte le ricompense future scontate per calcolare il rendimento.
- Normalizzare il valore del rendimento.
La funzione calcola_rendimenti()
esegue questi calcoli, come mostrato di seguito:
def calculate_returns(rewards, discount_factor): returns = [] cumulative_reward = 0 for r in reversed(rewards): cumulative_reward = r + cumulative_reward * discount_factor returns.insert(0, cumulative_reward) returns = torch.tensor(returns) # normalizza il rendimento returns = (returns - returns.mean()) / returns.std() return returns
Implementazione della funzione vantaggio
Il vantaggio viene calcolato come la differenza tra il valore previsto dal critico e il rendimento atteso dalle azioni scelte dall’attore secondo la politica. Per una data azione, il vantaggio esprime il beneficio nel prendere quell’azione specifica rispetto a un’azione arbitraria (media).
Nel documento originale PPO (equazione 10), l’avanzamento, guardando avanti fino al passo temporale T è espresso come:
Mentre si codifica l’algoritmo, il vincolo di guardare avanti fino a un numero fisso di passaggi temporali è imposto tramite la dimensione del batch. Quindi, l’equazione sopra può essere semplificata come la differenza tra il valore e i rendimenti attesi. I rendimenti attesi sono quantificati nella funzione valore azione, Q.
Quindi, la formula semplificata di seguito esprime l’avanzamento della scelta:
- di una particolare azione
- in uno stato dato
- sotto una particolare politica
- in un particolare passo temporale
Questo è espresso come:
OpenAI utilizza anche questa formula per implementare RL. La calculate_advantages()
funzione mostrata di seguito calcola l’avanzamento:
def calculate_advantages(returns, values): advantages = returns - values # Normalizza l'avanzamento advantages = (advantages - advantages.mean()) / advantages.std() return advantages
Perdita surrogata e meccanismo di troncamento
La perdita di policy sarebbe la perdita standard del gradiente di policy senza tecniche speciali come PPO. La perdita standard del gradiente di policy è calcolata come il prodotto di:
- Le probabilità d’azione della policy
- La funzione di vantaggio, calcolata come la differenza tra:
- Il ritorno della policy
- Il valore atteso
La perdita standard del gradiente di policy non può apportare correzioni per cambi repentini della policy. La perdita surrogata modifica la perdita standard per limitare l’importo con cui la policy può cambiare in ciascuna iterazione. È il minimo di due quantità:
- Il prodotto di:
- Il rapporto della politica. Questo rapporto esprime la differenza tra le vecchie e le nuove probabilità di azione.
- La funzione di vantaggio
- Il prodotto di:
- Il valore bloccato del rapporto della politica. Questo rapporto è limitato in modo che la politica aggiornata sia entro un certo percentuale della vecchia politica.
- La funzione di vantaggio
Per il processo di ottimizzazione, la perdita surrogata viene utilizzata come sostituto della perdita effettiva.
Il meccanismo di clipping
Il rapporto di policy, R, è la differenza tra le nuove e vecchie policy ed è dato come il rapporto delle log probabilità della policy sotto i nuovi e vecchi parametri:
Il rapporto di policy clipato, R’, è vincolato in modo che:
Dato il vantaggio, At, come mostrato nella sezione precedente, e il rapporto di policy, come mostrato sopra, la perdita surrogata viene calcolata come:
Il codice qui sotto mostra come implementare il meccanismo di clipping e la perdita surrogata.
def calculate_surrogate_loss( actions_log_probability_old, actions_log_probability_new, epsilon, advantages): advantages = advantages.detach() policy_ratio = ( actions_log_probability_new - actions_log_probability_old ).exp() surrogate_loss_1 = policy_ratio * advantages surrogate_loss_2 = torch.clamp( policy_ratio, min=1.0-epsilon, max=1.0+epsilon ) * advantages surrogate_loss = torch.min(surrogate_loss_1, surrogate_loss_2) return surrogate_loss
3. Addestramento dell’Agente
Adesso, addestriamo l’agente.
Calcolo della perdita di policy e valore
Siamo ora pronti a calcolare le perdite di policy e valore:
- La perdita di politica è la somma della perdita surrogata e del bonus di entropia.
- La perdita di valore si basa sulla differenza tra il valore previsto dal critico e i ritorni (ricompensa cumulativa) generati dalla politica. Il calcolo della perdita di valore utilizza la funzione di perdita Smooth L1. Questo aiuta a rendere più uniforme la funzione di perdita e a renderla meno sensibile agli outlier.
Entrambe le perdite, come calcolato sopra, sono tensori. La discesa del gradiente si basa su valori scalari. Per ottenere un singolo valore scalare che rappresenta la perdita, utilizzare la funzione .sum()
per sommare gli elementi del tensore. La funzione qui sotto mostra come fare ciò:
def calculate_losses( surrogate_loss, entropy, entropy_coefficient, returns, value_pred): entropy_bonus = entropy_coefficient * entropy policy_loss = -(surrogate_loss + entropy_bonus).sum() value_loss = f.smooth_l1_loss(returns, value_pred).sum() return policy_loss, value_loss
Definizione del ciclo di addestramento
Prima di iniziare il processo di addestramento, crea un insieme di buffer come array vuoti. L’algoritmo di addestramento utilizzerà questi buffer per memorizzare informazioni sulle azioni dell’agente, gli stati dell’ambiente e i premi ad ogni passo temporale. La funzione di seguito inizializza questi buffer:
def init_training(): states = [] actions = [] actions_log_probability = [] values = [] rewards = [] done = False episode_reward = 0 return states, actions, actions_log_probability, values, rewards, done, episode_reward
Ogni iterazione di addestramento esegue l’agente con i parametri di politica per quell’iterazione. L’agente interagisce con l’ambiente in passi temporali in un ciclo fino a quando non raggiunge una condizione terminale.
Dopo ogni passo temporale, l’azione, il premio e il valore dell’agente vengono aggiunti ai rispettivi buffer. Quando l’episodio termina, la funzione restituisce l’insieme aggiornato di buffer, che riassumono i risultati dell’episodio.
Prima di eseguire il ciclo di addestramento:
- Imposta il modello in modalità di addestramento usando
agent.train()
. - Reimposta l’ambiente a uno stato casuale usando
env.reset()
. Questo è lo stato iniziale per questa iterazione di addestramento.
I seguenti passaggi spiegano cosa succede in ogni passaggio nel ciclo di addestramento:
- Passa lo stato all’agente.
- L’agente restituisce:
- L’azione prevista dato lo stato, basata sulla policy (attore). Passa questo tensore di azione previsto attraverso la funzione softmax per ottenere l’insieme di probabilità delle azioni.
- Il valore previsto dello stato, basato sul critico.
- L’agente seleziona l’azione da intraprendere:
- Utilizza le probabilità delle azioni per stimare la distribuzione di probabilità.
- Seleziona casualmente un’azione scegliendo un campione da questa distribuzione. La funzione
dist.sample()
fa questo. - Utilizza la funzione
env.step()
per passare questa azione all’ambiente e simulare la risposta dell’ambiente per questo passaggio temporale. In base all’azione dell’agente, l’ambiente genera: - Il nuovo stato
- La ricompensa
- Il valore booleano di ritorno
fatto
(indica se l’ambiente ha raggiunto uno stato terminale) - Aggiungi ai rispettivi buffer i valori dell’azione dell’agente, delle ricompense, dei valori predetti e del nuovo stato.
L’episodio di addestramento termina quando la funzione env.step()
restituisce true
per il valore booleano di ritorno di fatto
.
Dopo che l’episodio è terminato, utilizzare i valori accumulati da ciascun timestep per calcolare i rendimenti cumulativi di quell’episodio aggiungendo le ricompense di ciascun timestep. Utilizziamo la funzione calculate_returns()
descritta in precedenza per fare ciò. Gli input di questa funzione sono il fattore di sconto e il buffer contenente le ricompense di ciascun timestep. Utilizziamo questi rendimenti e i valori accumulati da ciascun timestep per calcolare gli advantaggi utilizzando la funzione calculate_advantages()
.
La seguente funzione Python mostra come implementare questi passaggi:
def forward_pass(env, agent, optimizer, discount_factor): states, actions, actions_log_probability, values, rewards, done, episode_reward = init_training() state = env.reset() agent.train() while not done: state = torch.FloatTensor(state).unsqueeze(0) states.append(state) action_pred, value_pred = agent(state) action_prob = f.softmax(action_pred, dim=-1) dist = distributions.Categorical(action_prob) action = dist.sample() log_prob_action = dist.log_prob(action) state, reward, done, _ = env.step(action.item()) actions.append(action) actions_log_probability.append(log_prob_action) values.append(value_pred) rewards.append(reward) episode_reward += reward states = torch.cat(states) actions = torch.cat(actions) actions_log_probability = torch.cat(actions_log_probability) values = torch.cat(values).squeeze(-1) returns = calculate_returns(rewards, discount_factor) advantages = calculate_advantages(returns, values) return episode_reward, states, actions, actions_log_probability, advantages, returns
Aggiornamento dei parametri del modello
Ogni iterazione di addestramento esegue il modello attraverso un episodio completo composto da molti timestep (fino a quando non raggiunge una condizione terminale). In ciascun timestep, memorizziamo i parametri della policy, l’azione dell’agente, i rendimenti e gli advantaggi. Dopo ciascuna iterazione, aggiorniamo il modello in base alle prestazioni della policy attraverso tutti i timestep di quell’iterazione.
Il numero massimo di passaggi nell’ambiente CartPole è 500. In ambienti più complessi, ci sono più passaggi, anche milioni. In tali casi, il dataset dei risultati dell’addestramento deve essere diviso in batch. Il numero di passaggi in ciascun batch è chiamato dimensione del batch di ottimizzazione.
Quindi, i passaggi per aggiornare i parametri del modello sono:
- Dividere il dataset dei risultati dell’addestramento in batch.
- Per ogni batch:
- Ottenere l’azione dell’agente e il valore previsto per ciascuno stato.
- Usare queste azioni previste per stimare la nuova distribuzione di probabilità delle azioni.
- Usa questa distribuzione per calcolare l’entropia.
- Usa questa distribuzione per ottenere la probabilità logaritmica delle azioni nel dataset dei risultati dell’addestramento. Questo è il nuovo insieme di probabilità logaritmiche delle azioni nel dataset dei risultati dell’addestramento. Il vecchio insieme di probabilità logaritmiche di queste stesse azioni è stato calcolato nel ciclo di addestramento spiegato nella sezione precedente.
- Calcola la perdita surrogata utilizzando le vecchie e nuove distribuzioni di probabilità delle azioni.
- Calcola la perdita della policy e la perdita del valore utilizzando la perdita surrogata, l’entropia e i vantaggi.
- Esegui
.backward()
separatamente sulle perdite della policy e del valore. Questo aggiorna i gradienti sulle funzioni di perdita. - Esegui
.step()
sull’ottimizzatore per aggiornare i parametri della policy. In questo caso, utilizziamo l’ottimizzatore Adam per bilanciare velocità e robustezza. - Accumula le perdite della policy e del valore.
- Ripeti il passaggio all’indietro (le operazioni sopra) su ciascun batch alcune volte, a seconda del valore del parametro
PPO_STEPS
. Ripetere il passaggio all’indietro su ciascun batch è efficiente dal punto di vista computazionale perché aumenta efficacemente le dimensioni del dataset di addestramento senza dover eseguire passaggi in avanti aggiuntivi. Il numero di passi dell’ambiente in ciascuna alternanza tra campionamento e ottimizzazione è chiamato dimensione del batch di iterazione. - Ritorna la media della perdita della policy e della perdita del valore.
Il codice seguente implementa questi passaggi:
def update_policy( agent, states, actions, actions_log_probability_old, advantages, returns, optimizer, ppo_steps, epsilon, entropy_coefficient): BATCH_SIZE = 128 total_policy_loss = 0 total_value_loss = 0 actions_log_probability_old = actions_log_probability_old.detach() actions = actions.detach() training_results_dataset = TensorDataset( states, actions, actions_log_probability_old, advantages, returns) batch_dataset = DataLoader( training_results_dataset, batch_size=BATCH_SIZE, shuffle=False) for _ in range(ppo_steps): for batch_idx, (states, actions, actions_log_probability_old, advantages, returns) in enumerate(batch_dataset): # ottieni il nuovo log prob delle azioni per tutti gli stati di input action_pred, value_pred = agent(states) value_pred = value_pred.squeeze(-1) action_prob = f.softmax(action_pred, dim=-1) probability_distribution_new = distributions.Categorical( action_prob) entropy = probability_distribution_new.entropy() # stimare nuove probabilità log usando le vecchie azioni actions_log_probability_new = probability_distribution_new.log_prob(actions) surrogate_loss = calculate_surrogate_loss( actions_log_probability_old, actions_log_probability_new, epsilon, advantages) policy_loss, value_loss = calculate_losses( surrogate_loss, entropy, entropy_coefficient, returns, value_pred) optimizer.zero_grad() policy_loss.backward() value_loss.backward() optimizer.step() total_policy_loss += policy_loss.item() total_value_loss += value_loss.item() return total_policy_loss / ppo_steps, total_value_loss / ppo_steps
4. Esecuzione dell’Agente PPO
Infine eseguiamo l’agente PPO.
Valutazione delle prestazioni
Per valutare le prestazioni dell’agente, crea un ambiente nuovo e calcola le ricompense cumulative dall’esecuzione dell’agente in questo nuovo ambiente. È necessario impostare l’agente in modalità di valutazione usando la funzione .eval()
. I passaggi sono gli stessi del ciclo di addestramento. Il frammento di codice qui sotto implementa la funzione di valutazione:
def evaluate(env, agent): agent.eval() rewards = [] done = False episode_reward = 0 state = env.reset() while not done: state = torch.FloatTensor(state).unsqueeze(0) with torch.no_grad(): action_pred, _ = agent(state) action_prob = f.softmax(action_pred, dim=-1) action = torch.argmax(action_prob, dim=-1) state, reward, done, _ = env.step(action.item()) episode_reward += reward return episode_reward
Visualizzazione dei risultati dell’addestramento
Utilizzeremo la libreria Matplotlib per visualizzare il progresso del processo di addestramento. La funzione qui sotto mostra come tracciare le ricompense sia dal ciclo di addestramento che dal ciclo di test:
def plot_train_rewards(train_rewards, reward_threshold): plt.figure(figsize=(12, 8)) plt.plot(train_rewards, label='Training Reward') plt.xlabel('Episode', fontsize=20) plt.ylabel('Training Reward', fontsize=20) plt.hlines(reward_threshold, 0, len(train_rewards), color='y') plt.legend(loc='lower right') plt.grid() plt.show()
def plot_test_rewards(test_rewards, reward_threshold): plt.figure(figsize=(12, 8)) plt.plot(test_rewards, label='Testing Reward') plt.xlabel('Episode', fontsize=20) plt.ylabel('Testing Reward', fontsize=20) plt.hlines(reward_threshold, 0, len(test_rewards), color='y') plt.legend(loc='lower right') plt.grid() plt.show()
Negli esempi di grafici seguenti, mostriamo i premi di allenamento e di test, ottenuti applicando la policy rispettivamente negli ambienti di allenamento e di test. Nota che la forma di questi grafici apparirà diversa ogni volta che esegui il codice. Questo è dovuto alla casualità intrinseca al processo di allenamento.
Premi di allenamento (ottenuti applicando la policy nell’ambiente di allenamento). Immagine dell’Autore.
Premi di test (ottenuti applicando la policy nell’ambiente di test). Immagine dell’Autore.
Nelle grafici di output mostrati sopra, osserva il progresso del processo di allenamento:
- La ricompensa inizia con valori bassi. Man mano che il training avanza, le ricompense aumentano.
- Le ricompense fluttuano casualmente mentre aumentano. Questo è dovuto all’esplorazione dello spazio delle politiche da parte dell’agente.
- Il training termina e le ricompense di testing si sono stabilizzate intorno alla soglia (475) per molte iterazioni.
- Le ricompense sono limitate a 500. Questi sono vincoli imposti dall’ambiente (Gym CartPole v1).
Allo stesso modo, è possibile tracciare i valori delle perdite della politica attraverso le iterazioni:
def plot_losses(policy_losses, value_losses): plt.figure(figsize=(12, 8)) plt.plot(value_losses, label='Value Losses') plt.plot(policy_losses, label='Policy Losses') plt.xlabel('Episode', fontsize=20) plt.ylabel('Loss', fontsize=20) plt.legend(loc='lower right') plt.grid() plt.show()
Nell’esempio di grafico sottostante sono mostrate le perdite monitorate attraverso gli episodi di training:
Perdite di valore e di politica durante il processo di formazione. Immagine dell’Autore
Osserva il grafico e nota:
- Le perdite sembrano essere distribuite casualmente e non seguono alcun modello.
- Questo è tipico dell’addestramento RL, dove l’obiettivo non è minimizzare la perdita ma massimizzare le ricompense.
Esegui l’algoritmo PPO
Ora hai tutti i componenti per addestrare l’agente utilizzando PPO. Per mettere tutto insieme, devi:
- Dichiarare iperparametri come fattore di sconto, dimensione del batch, tasso di apprendimento, ecc.
- Istanzia buffer come array nulli per memorizzare le ricompense e le perdite di ciascuna iterazione.
- Crea un’istanza dell’agente usando la funzione
create_agent()
. - Esegui iterativamente passaggi in avanti e all’indietro usando le funzioni
forward_pass()
eupdate_policy()
. - Testa le prestazioni della policy usando la funzione
evaluate()
. - Aggiungi la policy, le perdite di valore e le ricompense dalle funzioni di addestramento e valutazione ai rispettivi buffer.
- Calcola la media dei premi e delle perdite degli ultimi timestep. L’esempio sotto calcola la media dei premi e delle perdite degli ultimi 40 passaggi temporali.
- Stampa i risultati dell’valutazione ogni pochi passaggi. L’esempio sotto stampa ogni 10 passaggi.
- Interrompi il processo quando la media del premio supera una certa soglia.
Il codice sotto mostra come dichiarare una funzione che fa questo in Python:
def run_ppo(): MAX_EPISODES = 500 DISCOUNT_FACTOR = 0.99 REWARD_THRESHOLD = 475 PRINT_INTERVAL = 10 PPO_STEPS = 8 N_TRIALS = 100 EPSILON = 0.2 ENTROPY_COEFFICIENT = 0.01 HIDDEN_DIMENSIONS = 64 DROPOUT = 0.2 LEARNING_RATE = 0.001 train_rewards = [] test_rewards = [] policy_losses = [] value_losses = [] agent = create_agent(HIDDEN_DIMENSIONS, DROPOUT) optimizer = optim.Adam(agent.parameters(), lr=LEARNING_RATE) for episode in range(1, MAX_EPISODES+1): train_reward, states, actions, actions_log_probability, advantages, returns = forward_pass( env_train, agent, optimizer, DISCOUNT_FACTOR) policy_loss, value_loss = update_policy( agent, states, actions, actions_log_probability, advantages, returns, optimizer, PPO_STEPS, EPSILON, ENTROPY_COEFFICIENT) test_reward = evaluate(env_test, agent) policy_losses.append(policy_loss) value_losses.append(value_loss) train_rewards.append(train_reward) test_rewards.append(test_reward) mean_train_rewards = np.mean(train_rewards[-N_TRIALS:]) mean_test_rewards = np.mean(test_rewards[-N_TRIALS:]) mean_abs_policy_loss = np.mean(np.abs(policy_losses[-N_TRIALS:])) mean_abs_value_loss = np.mean(np.abs(value_losses[-N_TRIALS:])) if episode % PRINT_INTERVAL == 0: print(f'Episode: {episode:3} | \ Mean Train Rewards: {mean_train_rewards:3.1f} \ | Mean Test Rewards: {mean_test_rewards:3.1f} \ | Mean Abs Policy Loss: {mean_abs_policy_loss:2.2f} \ | Mean Abs Value Loss: {mean_abs_value_loss:2.2f}') if mean_test_rewards >= REWARD_THRESHOLD: print(f'Reached reward threshold in {episode} episodes') break plot_train_rewards(train_rewards, REWARD_THRESHOLD) plot_test_rewards(test_rewards, REWARD_THRESHOLD) plot_losses(policy_losses, value_losses)
Esegui il programma:
run_ppo()
L’output dovrebbe assomigliare al campione sotto:
Episode: 10 | Mean Train Rewards: 22.3 | Mean Test Rewards: 30.4 | Mean Abs Policy Loss: 0.37 | Mean Abs Value Loss: 0.39 Episode: 20 | Mean Train Rewards: 38.6 | Mean Test Rewards: 69.8 | Mean Abs Policy Loss: 0.46 | Mean Abs Value Loss: 0.37 . . . Episode: 100 | Mean Train Rewards: 289.5 | Mean Test Rewards: 427.3 | Mean Abs Policy Loss: 1.73 | Mean Abs Value Loss: 0.21 Episode: 110 | Mean Train Rewards: 357.7 | Mean Test Rewards: 461.4 | Mean Abs Policy Loss: 1.86 | Mean Abs Value Loss: 0.22 Reached reward threshold in 116 episodes
È possibile visualizzare ed eseguire il programma funzionante su questo notebook DataLab!
5. Ottimizzazione e Regolazione degli Iperparametri
Nell’apprendimento automatico, gli iperparametri controllano il processo di addestramento. Di seguito, spiego alcuni degli iperparametri importanti utilizzati in PPO:
- Learning rate: Il tasso di apprendimento decide quanto i parametri di politica possono variare in ciascuna iterazione. Nel discesa del gradiente stocastico, l’importo con cui i parametri di politica vengono aggiornati in ciascuna iterazione è deciso dal prodotto del tasso di apprendimento e del gradiente.
- Parametro di clipping: Questo è anche indicato come epsilon, ε. Decide fino a che punto il rapporto delle politiche viene clipato. Il rapporto tra le nuove e le vecchie politiche è consentito di variare nell’intervallo [1-ε, 1+ε]. Quando va oltre questo intervallo, viene artificialmente clipato per rimanere all’interno dell’intervallo.
- Dimensione del batch: Questo si riferisce al numero di passaggi da considerare per ciascun aggiornamento del gradiente. In PPO, la dimensione del batch è il numero di passaggi temporali necessari per applicare la politica e calcolare la perdita surrogata per aggiornare i parametri della politica. In questo articolo, abbiamo utilizzato una dimensione del batch di 64.
- Passaggi di iterazione: Questo è il numero di volte in cui ciascun batch viene riutilizzato per eseguire il passaggio all’indietro. Il codice in questo articolo si riferisce a questo come
PPO_STEPS
. In ambienti complessi, eseguire il passaggio in avanti molte volte è computazionalmente costoso. Una soluzione più efficiente è quella di rieseguire ciascun batch alcune volte. Di solito è consigliabile utilizzare un valore compreso tra 5 e 10. - Fattore di sconto: Questo è anche chiamato gamma, γ. Esprime in che misura i premi immediati sono più preziosi dei premi futuri. Questo è simile al concetto di tassi di interesse nel calcolare il valore nel tempo del denaro. Quando è più vicino a 0, significa che i premi futuri sono meno preziosi e l’agente dovrebbe dare priorità ai premi immediati. Quando è più vicino a 1, significa che i premi futuri sono importanti.
- Coefficiente di entropia: Il coefficiente di entropia decide il bonus di entropia, calcolato come il prodotto del coefficiente di entropia e dell’entropia della distribuzione. Il ruolo del bonus di entropia è introdurre più casualità nella politica. Questo incoraggia l’agente a esplorare lo spazio della politica. Tuttavia, l’addestramento non riesce a convergere verso una politica ottimale quando questa casualità è troppo alta.
- Criteri di successo per la formazione: È necessario stabilire i criteri per decidere quando la formazione è riuscita. Un modo comune per fare ciò è impostare una condizione in base alla quale le ricompense medie degli ultimi N tentativi (episodi) siano al di sopra di una certa soglia. Nell’esempio di codice sopra, questo viene espresso con la variabile
N_TRIALS
. Quando questo valore è impostato su un valore più alto, la formazione richiede più tempo perché la politica deve raggiungere la ricompensa soglia su più episodi. Ciò porta anche a una politica più robusta, pur essendo più costosa dal punto di vista computazionale. Si noti che PPO è una politica stocastica e ci saranno episodi in cui l’agente non supera la soglia. Quindi, se il valore diN_TRIALS
è troppo alto, la formazione potrebbe non terminare.
Strategie per ottimizzare le prestazioni di PPO
Ottimizzare le prestazioni degli algoritmi di addestramento PPO coinvolge tentativi ed errori e sperimentazione con diversi valori di iperparametri. Tuttavia, ci sono alcune linee guida generali:
- Fattore di sconto: Quando le ricompense a lungo termine sono importanti, come nell’ambiente CartPole, dove il palo deve rimanere stabile nel tempo, inizia con un valore gamma moderato, come 0.99.
- Bonus di entropia: In ambienti complessi, l’agente deve esplorare lo spazio delle azioni per trovare la policy ottimale. Il bonus di entropia promuove l’esplorazione. Il bonus di entropia viene aggiunto alla perdita surrogata. Controllare l’entità della perdita surrogata e l’entropia della distribuzione prima di decidere il coefficiente di entropia. In questo articolo, abbiamo utilizzato un coefficiente di entropia di 0.01.
- Parametro di ritaglio: Il parametro di ritaglio decide quanto diversa può essere la politica aggiornata rispetto alla politica attuale. Un valore elevato del parametro di ritaglio incoraggia una migliore esplorazione dell’ambiente, ma rischia di destabilizzare l’addestramento. Si desidera un parametro di ritaglio che permetta un’esplorazione graduale evitando aggiornamenti destabilizzanti. In questo articolo, abbiamo utilizzato un parametro di ritaglio di 0,2.
- Tasso di apprendimento: Quando il tasso di apprendimento è troppo alto, la politica viene aggiornata con passi grandi ad ogni iterazione e il processo di addestramento potrebbe diventare instabile. Quando è troppo basso, l’addestramento richiede troppo tempo. Questo tutorial ha utilizzato un tasso di apprendimento di 0,001, che funziona bene per l’ambiente. In molti casi, è consigliabile utilizzare un tasso di apprendimento di 1e-5.
Sfide e Migliori Pratiche in PPO
Dopo aver spiegato i concetti e i dettagli di implementazione di PPO, discutiamo delle sfide e delle migliori pratiche.
Sfide comuni nel training di PPO
Anche se PPO è ampiamente utilizzato, è necessario essere consapevoli delle sfide potenziali per risolvere con successo problemi del mondo reale utilizzando questa tecnica. Alcune di queste sfide sono:
- Convergenza lenta:In ambienti complessi, PPO può essere inefficiente dal punto di vista del campionamento e ha bisogno di molte interazioni con l’ambiente per convergere sulla policy ottimale. Questo rende l’addestramento lento ed costoso.
- Sensibilità agli iperparametri: PPO si basa sull’esplorazione efficiente dello spazio delle politiche. La stabilità del processo di addestramento e la velocità di convergenza sono sensibili ai valori degli iperparametri. I valori ottimali di questi iperparametri possono spesso essere determinati solo tramite tentativi ed errori.
- Overfitting: Gli ambienti di RL di solito vengono inizializzati con parametri casuali. L’addestramento di PPO si basa sulla ricerca della politica ottimale basata sull’ambiente dell’agente. A volte, il processo di addestramento converge a un insieme di parametri ottimali per un ambiente specifico ma non per un ambiente casualizzato. Questo viene tipicamente affrontato avendo molte iterazioni, ciascuna con un ambiente di addestramento casualizzato in modo diverso.
- Ambienti dinamici: Gli ambienti di apprendimento per rinforzo semplici, come l’ambiente CartPole, sono statici: le regole rimangono le stesse nel tempo. Molti altri ambienti, come un robot che impara a camminare su una superficie instabile in movimento, sono dinamici: le regole dell’ambiente cambiano nel tempo. Per avere buone prestazioni in tali ambienti, spesso è necessario un ulteriore perfezionamento di PPO.
- Esplorazione vs sfruttamento: Il meccanismo di limitazione di PPO garantisce che gli aggiornamenti delle policy rimangano entro una regione affidabile. Tuttavia, impedisce anche all’agente di esplorare lo spazio delle azioni. Ciò può portare alla convergenza verso ottimi locali, soprattutto in ambienti complessi. D’altra parte, permettere all’agente di esplorare troppo può impedirgli di convergere su una politica ottimale.
Linee guida per l’addestramento dei modelli PPO
Per ottenere buoni risultati utilizzando PPO, ti consiglio di seguire alcune best practices, come:
- Normalizzare le caratteristiche di input: La normalizzazione dei valori dei rendimenti e degli vantaggi riduce la variabilità dei dati e porta a aggiornamenti stabili del gradiente. La normalizzazione dei dati porta tutti i valori a un intervallo numerico coerente. Aiuta a ridurre l’effetto degli outlier e dei valori estremi, che altrimenti potrebbero distorcere gli aggiornamenti del gradiente e rallentare la convergenza.
- Usa dimensioni di batch adeguate: I batch piccoli consentono aggiornamenti e addestramenti più veloci ma possono portare alla convergenza verso ottimi locali e all’instabilità nel processo di addestramento. Le dimensioni di batch più grandi permettono all’agente di apprendere politiche robuste, portando a un processo di addestramento stabile. Tuttavia, dimensioni di batch troppo grandi sono anche subottimali. Oltre ad aumentare i costi computazionali, rendono gli aggiornamenti delle politiche meno reattivi alla funzione di valore perché gli aggiornamenti del gradiente si basano su medie stimate su batch grandi. Inoltre, può portare a un adattamento eccessivo agli aggiornamenti di quel batch specifico.
- Passaggi di iterazione: È generalmente consigliabile riutilizzare ciascun batch per 5-10 iterazioni. Questo rende il processo di addestramento più efficiente. Riutilizzare lo stesso batch troppe volte porta all’overfitting. Il codice si riferisce a questo iperparametro come
PPO_STEPS
. - Eseguire valutazioni regolari: Per individuare l’overfitting, è essenziale monitorare periodicamente l’efficacia della policy. Se la policy risulta inefficace in determinati scenari, potrebbe essere necessario un ulteriore addestramento o raffinamento.
- Regolare gli iperparametri: Come spiegato in precedenza, l’addestramento PPO è sensibile ai valori degli iperparametri. Sperimentare con vari valori degli iperparametri per determinare il giusto set di valori per il problema specifico.
- Rete di supporto condivisa:Come illustrato in questo articolo, l’utilizzo di una rete di supporto condivisa impedisce squilibri tra le reti attore e critico. La condivisione di una rete di supporto tra l’attore e il critico aiuta con l’estrazione delle caratteristiche condivise e con una comprensione comune dell’ambiente. Questo rende il processo di apprendimento più efficiente e stabile. Aiuta anche a ridurre lo spazio computazionale e la complessità temporale dell’algoritmo.
- Numero e dimensioni dei livelli nascosti:Aumentare il numero di livelli nascosti e le dimensioni per ambienti più complessi. Problemi più semplici come CartPole possono essere risolti con un solo livello nascosto. Il livello nascosto utilizzato in questo articolo ha 64 dimensioni. Rendere la rete molto più grande del necessario è uno spreco computazionale e può renderla instabile.
- Arresto anticipato: L’arresto dell’allenamento quando le metriche di valutazione sono soddisfatte aiuta a prevenire l’eccessivo addestramento e evita lo spreco di risorse. Una metrica di valutazione comune è quando l’agente supera i premi soglia nei precedenti N eventi.
Conclusione
In questo articolo abbiamo discusso il PPO come modo per risolvere i problemi di RL. Abbiamo poi dettagliato i passaggi per implementare il PPO utilizzando PyTorch. Infine, abbiamo presentato alcuni suggerimenti sulle prestazioni e le migliori pratiche per il PPO.
Il modo migliore per imparare è implementare il codice da soli. È anche possibile modificare il codice per farlo funzionare con altri ambienti di controllo classici in Gym. Per imparare come implementare agenti di RL utilizzando Python e Gymnasium di OpenAI, segui il corso Reinforcement Learning with Gymnasium in Python!
Source:
https://www.datacamp.com/tutorial/proximal-policy-optimization