Corso
La Proximal Policy Optimization (PPO) è uno degli algoritmi preferiti per risolvere problemi di Reinforcement Learning (RL). È stata sviluppata nel 2017 da John Schuman, cofondatore di OpenAI.
PPO è stata ampiamente utilizzata in OpenAI per addestrare modelli che emulano comportamenti umani. Migliora metodi precedenti come la Trust Region Policy Optimization (TRPO) ed è diventata popolare perché è un algoritmo robusto ed efficiente.
In questo tutorial analizziamo PPO in profondità. Tratteremo la teoria e mostreremo come implementarla con PyTorch.
Capire la Proximal Policy Optimization (PPO)
I classici algoritmi di apprendimento supervisionato aggiornano i parametri lungo la direzione del gradiente più ripido. Se l'aggiornamento risulta eccessivo, viene corretto durante esempi di training successivi, indipendenti tra loro.
Nel reinforcement learning, però, gli esempi di training sono costituiti dalle azioni e dai ritorni dell’agente. Quindi, gli esempi sono correlati tra loro. L’agente esplora l’ambiente per individuare la politica ottimale. Di conseguenza, grandi variazioni del gradiente possono portare la politica a bloccarsi in una regione sfavorevole con ricompense subottimali. Poiché l’agente deve esplorare l’ambiente, grandi cambiamenti di politica rendono instabile il processo di training.
I metodi basati sulle trust region mirano a evitare questo problema garantendo che gli aggiornamenti della politica rimangano all’interno di una regione affidabile. Questa regione affidabile è un’area artificialmente vincolata nello spazio delle politiche entro la quale sono consentiti gli aggiornamenti. La politica aggiornata può trovarsi solo all’interno di una regione affidabile rispetto alla vecchia politica. Assicurare aggiornamenti incrementali della politica previene l’instabilità.
Trust Region Policy Updates (TRPO)
L’algoritmo Trust Region Policy Updates (TRPO) è stato proposto nel 2015 da John Schulman (che ha anche proposto PPO nel 2017). Per misurare la differenza tra la vecchia politica e quella aggiornata, TRPO utilizza la divergenza di Kullback-Leibler (KL), impiegata per misurare la differenza tra due distribuzioni di probabilità. TRPO si è dimostrato efficace nell’implementare le trust region.
Il problema di TRPO è la complessità computazionale associata alla KL divergence. La KL divergence va espansa al secondo ordine con metodi numerici come l’espansione di Taylor, ed è costosa dal punto di vista computazionale. PPO è stata proposta come alternativa più semplice ed efficiente a TRPO. PPO tronca il rapporto tra politiche per approssimare la trust region senza ricorrere a calcoli complessi basati sulla KL divergence.
Per questo motivo PPO è preferita a TRPO nella risoluzione di problemi RL. Grazie a un metodo più efficiente per stimare le trust region, PPO bilancia efficacemente prestazioni e stabilità.
Approssimazione della politica prossimale (PPO)
PPO è spesso considerata una sottoclasse dei metodi actor-critic, che aggiornano i gradienti della politica in base alla funzione di valore. I metodi Advantage Actor-Critic (A2C) usano un parametro chiamato advantage, che misura la differenza tra i ritorni previsti dal critic e i ritorni realizzati applicando la politica.
Per capire PPO, devi conoscere i suoi componenti:
- L’actor esegue la politica. È implementato come una rete neurale. Dato uno stato in input, produce l’azione da intraprendere.
- Il critic è un’altra rete neurale. Riceve lo stato in input e restituisce il valore atteso di quello stato. Quindi, il critic esprime la funzione di valore di stato.
- I metodi basati su policy gradient possono usare obiettivi diversi. In particolare, PPO usa la funzione advantage. L’advantage misura di quanto la ricompensa cumulata (basata sulla politica eseguita dall’actor) supera la ricompensa di base attesa (prevista dal critic). L’obiettivo di PPO è aumentare la probabilità di scegliere azioni con advantage elevato. L’obiettivo di ottimizzazione di PPO usa funzioni di loss basate su questa funzione advantage.
- La funzione obiettivo tronca (clipped) è la principale innovazione di PPO. Impedisce grandi aggiornamenti della politica in una singola iterazione di training. Limita quanto la politica può cambiare in un’unica iterazione. Per misurare aggiornamenti incrementali della politica, i metodi basati su policy usano il rapporto di probabilità tra nuova e vecchia politica.
- La surrogate loss è la funzione obiettivo in PPO e incorpora le innovazioni citate. È calcolata come segue:
- Calcola il rapporto effettivo (come spiegato prima) e moltiplicalo per l’advantage.
- Tronca il rapporto per farlo rientrare in un intervallo desiderato. Moltiplica il rapporto troncato per l’advantage.
- Prendi il valore minimo tra le due quantità sopra.
- In pratica, si aggiunge anche un termine di entropia alla surrogate loss. È chiamato entropy bonus. Si basa sulla distribuzione matematica delle probabilità delle azioni. L’idea è introdurre ulteriore casualità in modo controllato. Questo incoraggia l’ottimizzazione a esplorare lo spazio delle azioni. Un entropy bonus alto favorisce l’esplorazione rispetto allo sfruttamento.
Capire il meccanismo di clipping
Supponiamo che sotto la vecchia politica πold, la probabilità di intraprendere l’azione a nello stato s sia πold(a|s). Con la nuova politica, la probabilità di intraprendere la stessa azione a dallo stesso stato s viene aggiornata a πnew(a|s). Il rapporto tra 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à delle azioni rientrino entro una certa percentuale rispetto alle vecchie probabilità. Ad esempio, r(θ) può essere vincolato tra 0,8 e 1,2. Questo evita salti troppo grandi, garantendo un processo di training stabile.
Nel resto dell’articolo, imparerai a comporre i componenti per una semplice implementazione di PPO con PyTorch.
1. Configurare l’ambiente
Prima di implementare PPO, dobbiamo installare le librerie software necessarie e scegliere un ambiente adatto in cui applicare la politica.
Installare PyTorch e le librerie richieste
Dobbiamo installare il seguente software:
- PyTorch e altre librerie, come
numpy(per funzioni matematiche e statistiche) ematplotlib(per tracciare grafici). - Il pacchetto open-source Gym di OpenAI, una libreria Python che simula diversi ambienti e giochi risolvibili con il Reinforcement Learning. Puoi usare le API di Gym per far interagire il tuo algoritmo con l’ambiente. Poiché la funzionalità di
gymtalvolta cambia con gli aggiornamenti, in questo esempio blocchiamo la versione a0.25.2.
Per installare su un server o una macchina locale, esegui:
$ pip install torch numpy matplotlib gym==0.25.2
Per installare usando un Notebook come Google Colab o DataLab, usa:
!pip install torch numpy matplotlib gym==0.25.2
Crea gli ambienti CartPole
Usa OpenAI Gym per creare due istanze (una per il training e una per il test) dell’ambiente CartPole:
env_train = gym.make('CartPole-v1')
env_test = gym.make('CartPole-v1')
2. Implementare PPO in PyTorch
Ora implementiamo PPO con PyTorch.
Definire la rete di policy
Come spiegato sopra, PPO è implementata come modello actor-critic. L’actor implementa la politica e il critic ne predice il valore stimato. Entrambe le reti neurali, actor e critic, ricevono lo stesso input: lo stato a ogni timestep. Perciò, actor e critic possono condividere una rete neurale comune, detta backbone. Actor e critic possono estendere il backbone con layer aggiuntivi.
Definisci la rete backbone
I passaggi seguenti descrivono la rete backbone:
- Implementa una rete con 3 layer: input, hidden e output.
- Dopo i layer di input e hidden, usiamo una funzione di attivazione. In questo tutorial scegliamo ReLU perché è computazionalmente efficiente.
- Applichiamo anche un dropout dopo i layer di input e hidden per ottenere una rete robusta. Il dropout azzera casualmente alcuni neuroni. Questo riduce la dipendenza da neuroni specifici e previene l’overfitting, rendendo la rete più robusta.
Il codice seguente implementa il backbone:
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
Definisci la rete actor-critic
Ora possiamo usare questa rete per definire la classe actor-critic, ActorCritic. L’actor modella la politica e predice l’azione. Il critic 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
Istanzia le reti di actor e critic
Useremo le reti definite sopra per creare un actor e un critic. Quindi creeremo un agente che includa actor e critic.
Prima di creare l’agente, inizializza i parametri della rete:
- Le dimensioni del layer nascosto, H, che è un parametro configurabile. La dimensione e il numero di layer nascosti dipendono dalla complessità del problema. Useremo un layer nascosto con dimensioni 64 x 64.
- Le feature di input, N, dove N è la dimensione dell’array di stato. Il layer di input ha dimensioni N x H. Nell’ambiente CartPole, lo stato è un array di 4 elementi. Quindi N è 4.
- Le feature di output della rete dell’actor, O, dove O è il numero di azioni nell’ambiente. Il layer di output dell’actor ha dimensioni H x O. L’ambiente CartPole ha 2 azioni.
- Le feature di output della rete del critic. Poiché il critic predice solo il valore atteso (dato uno stato in input), il numero di feature di output è 1.
- Il dropout come frazione.
Il codice seguente mostra come dichiarare le reti di actor e critic basate sul backbone:
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 ritorni
L’ambiente fornisce una ricompensa passando da uno step al successivo, in base all’azione dell’agente. La ricompensa, R, è espressa come:
Il return è definito come il valore accumulato delle ricompense future attese. Le ricompense in timestep più lontani nel futuro valgono meno delle ricompense immediate. Perciò, il return è comunemente calcolato come return scontato, G, definito come:
In questo tutorial (e in molte altre fonti), con return si intende il return scontato.
Per calcolare il return:
- Parti dalle ricompense attese da tutti gli stati futuri.
- Moltiplica ogni ricompensa futura per una potenza del fattore di sconto, . Ad esempio, la ricompensa attesa dopo 2 timestep (dal presente) è moltiplicata per 2.
- Somma tutte le ricompense future scontate per calcolare il return.
- Normalizza il valore del return.
La funzione calculate_returns() 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)
# normalize the return
returns = (returns - returns.mean()) / returns.std()
return returns
Implementare la funzione advantage
L’advantage è calcolato come differenza tra il valore predetto dal critic e il return atteso dalle azioni scelte dall’actor secondo la politica. Per una data azione, l’advantage esprime il beneficio di compiere quella specifica azione rispetto a un’azione arbitraria (media).
Nell’articolo originale su PPO (equazione 10), l’advantage, guardando avanti fino al timestep T, è espresso come:
Nel codice, il vincolo di guardare avanti fino a un numero fissato di timestep è applicato tramite la dimensione del batch. Quindi, l’equazione può essere semplificata come differenza tra il valore e i return attesi. I return attesi sono quantificati nella funzione di valore stato-azione, Q.
La formula semplificata sotto esprime quindi l’advantage di scegliere:
- una particolare azione
- in un dato stato
- sotto una particolare politica
- in un determinato timestep
Ed è espressa come:
OpenAI usa anche questa formula per implementare l’RL. La funzione calculate_advantages() mostrata sotto calcola l’advantage:
def calculate_advantages(returns, values):
advantages = returns - values
# Normalize the advantage
advantages = (advantages - advantages.mean()) / advantages.std()
return advantages
Surrogate loss e meccanismo di clipping
La policy loss, senza tecniche speciali come PPO, sarebbe la classica loss del policy gradient standard. La policy gradient loss standard è calcolata come prodotto di:
- Le probabilità delle azioni della politica
- La funzione advantage, calcolata come differenza tra:
- Il return della politica
- Il valore atteso
La policy gradient loss standard non può correggere cambiamenti bruschi della politica. La surrogate loss modifica la loss standard per limitare l’ampiezza del cambiamento della politica a ogni iterazione. È il minimo tra due quantità:
- Il prodotto di:
- Il rapporto tra politiche. Questo rapporto esprime la differenza tra le vecchie e le nuove probabilità delle azioni.
- La funzione advantage
- Il prodotto di:
- Il valore troncato del rapporto tra politiche. Questo rapporto è tagliato in modo che la politica aggiornata sia entro una certa percentuale rispetto alla vecchia politica.
- La funzione advantage
Nel processo di ottimizzazione, la surrogate loss è usata come proxy della loss effettiva.
Il meccanismo di clipping
Il rapporto di politica, R, è la differenza tra la nuova e la vecchia politica ed è dato dal rapporto tra le probabilità logaritmiche della politica sotto i nuovi e i vecchi parametri:
Il rapporto di politica troncato, R', è vincolato in modo tale che:
Dato l’advantage, At, come mostrato nella sezione precedente, e il rapporto di politica come sopra, la surrogate loss si calcola come:
Il codice qui sotto mostra come implementare il meccanismo di clipping e la surrogate loss.
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. Addestrare l’agente
Ora addestriamo l’agente.
Calcolare le loss di policy e di valore
Siamo pronti a calcolare le loss di policy e di valore:
- La policy loss è la somma della surrogate loss e dell’entropy bonus.
- La loss di valore si basa sulla differenza tra il valore predetto dal critic e i return (ricompensa cumulata) generati dalla politica. Il calcolo della loss di valore usa la funzione Smooth L1 Loss. Questo aiuta a rendere più regolare la funzione di loss e la rende meno sensibile agli outlier.
Entrambe le loss, come calcolate sopra, sono tensori. La discesa del gradiente si basa su valori scalari. Per ottenere un singolo valore scalare che rappresenti la loss, usa la funzione .sum() per sommare gli elementi del tensore. La funzione seguente mostra come fare:
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
Definire il training loop
Prima di iniziare il training, crea un set di buffer come array vuoti. L’algoritmo di training userà questi buffer per salvare informazioni sulle azioni dell’agente, sugli stati dell’ambiente e sulle ricompense a ogni timestep. La funzione seguente 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 training esegue l’agente con i parametri di politica di quell’iterazione. L’agente interagisce con l’ambiente in timestep in un loop finché non raggiunge una condizione terminale.
Dopo ogni timestep, l’azione dell’agente, la ricompensa e il valore vengono aggiunti ai rispettivi buffer. Quando l’episodio termina, la funzione restituisce il set aggiornato di buffer, che riassume i risultati dell’episodio.
Prima di eseguire il training loop:
- Imposta il modello in modalità training con
agent.train(). - Reimposta l’ambiente in uno stato casuale con
env.reset(). Questo è lo stato iniziale per questa iterazione di training.
I passaggi seguenti spiegano cosa succede a ogni timestep nel training loop:
- Passa lo stato all’agente.
- L’agente restituisce:
- L’azione predetta dato lo stato, in base alla politica (actor). Passa questo tensore di azione attraverso la softmax per ottenere l’insieme di probabilità delle azioni.
- Il valore predetto dello stato, in base al critic.
- L’agente seleziona l’azione da intraprendere:
- Usa le probabilità delle azioni per stimare la distribuzione di probabilità.
- Seleziona casualmente un’azione campionando da questa distribuzione. La funzione
dist.sample()esegue questa operazione. - Usa la funzione
env.step()per passare questa azione all’ambiente e simulare la risposta per questo timestep. In base all’azione dell’agente, l’ambiente genera: - Il nuovo stato
- La ricompensa
- Il valore booleano
done(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 training termina quando la funzione env.step() restituisce true per il valore booleano done.
Dopo la terminazione dell’episodio, usa i valori accumulati a ogni timestep per calcolare i return cumulati dell’episodio sommando le ricompense di ciascun timestep. Usiamo la funzione calculate_returns() descritta prima. Gli input di questa funzione sono il fattore di sconto e il buffer con le ricompense di ogni timestep. Usiamo questi return e i valori accumulati a ogni timestep per calcolare gli advantage con la funzione calculate_advantages().
La funzione Python seguente 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
Aggiornare i parametri del modello
Ogni iterazione di training esegue il modello attraverso un episodio completo composto da molti timestep (fino alla condizione terminale). A ogni timestep salviamo i parametri della politica, l’azione dell’agente, i return e gli advantage. Dopo ogni iterazione, aggiorniamo il modello in base alle prestazioni della politica lungo tutti i timestep di quell’iterazione.
Il numero massimo di timestep nell’ambiente CartPole è 500. In ambienti più complessi, i timestep sono molti di più, anche milioni. In tali casi, il dataset dei risultati del training deve essere suddiviso in batch. Il numero di timestep in ciascun batch è chiamato optimization batch size.
Quindi, i passaggi per aggiornare i parametri del modello sono:
- Dividi il dataset dei risultati del training in batch.
- Per ogni batch:
- Ottieni l’azione dell’agente e il valore predetto per ogni stato.
- Usa queste azioni predette per stimare la nuova distribuzione di probabilità delle azioni.
- Usa questa distribuzione per calcolare l’entropia.
- Usa questa distribuzione per ottenere le log-probability delle azioni nel dataset dei risultati di training. Questo è il nuovo set di log-probability delle azioni nel dataset. Il vecchio set di log-probability delle stesse azioni è stato calcolato nel training loop della sezione precedente.
- Calcola la surrogate loss usando le vecchie e le nuove distribuzioni di probabilità delle azioni.
- Calcola la policy loss e la value loss usando la surrogate loss, l’entropia e gli advantage.
- Esegui
.backward()separatamente sulle loss di policy e di valore. Questo aggiorna i gradienti delle funzioni di loss. - Esegui
.step()sull’ottimizzatore per aggiornare i parametri della politica. In questo caso usiamo l’ottimizzatore Adam per bilanciare velocità e robustezza. - Accumula le loss di policy e di valore.
- Ripeti la backward pass (le operazioni sopra) su ogni batch alcune volte, in base al valore del parametro
PPO_STEPS. Ripetere la backward pass su ogni batch è efficiente dal punto di vista computazionale perché aumenta di fatto la dimensione del dataset di training senza dover eseguire ulteriori forward pass. Il numero di step dell’ambiente in ogni alternanza tra campionamento e ottimizzazione è chiamato iteration batch size. - Restituisci la media della policy loss e della value loss.
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):
# get new log prob of actions for all input states
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()
# estimate new log probabilities using old actions
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_steps4. Esecuzione dell'agente PPO
Eseguiamo finalmente l'agente PPO.
Valutare le prestazioni
Per valutare le prestazioni dell'agente, crea un nuovo ambiente e calcola le ricompense cumulative eseguendo l'agente in questo nuovo ambiente. Devi impostare l'agente in modalità valutazione usando la funzione .eval(). I passaggi sono gli stessi del ciclo di training. 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
Visualizzare i risultati del training
Useremo la libreria Matplotlib per visualizzare l'andamento del processo di training. La funzione qui sotto mostra come tracciare le ricompense sia del loop di training sia di quello 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()
Nei grafici di esempio qui sotto mostriamo le ricompense di training e di test, ottenute applicando la policy rispettivamente negli ambienti di training e di test. Nota che la forma di questi grafici apparirà diversa ogni volta che esegui il codice. Questo è dovuto alla casualità intrinseca del processo di training.

Ricompense di training (ottenute applicando la policy nell'ambiente di training). Immagine dell'autore.

Ricompense di test (ottenute applicando la policy nell'ambiente di test). Immagine dell'autore.
Nei grafici di output mostrati sopra, osserva l'andamento del processo di training:
- La ricompensa parte da valori bassi. Con il procedere del training, le ricompense aumentano.
- Le ricompense fluttuano casualmente mentre crescono. Ciò è dovuto al fatto che l'agente esplora lo spazio delle policy.
- Il training termina e le ricompense di test si sono stabilizzate intorno alla soglia (475) per molte iterazioni.
- Le ricompense sono limitate a 500. Si tratta di vincoli imposti dall'ambiente (Gym CartPole v1).
Allo stesso modo, puoi tracciare le perdite della value function e della policy nel corso delle 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()
Il grafico di esempio qui sotto mostra le perdite tracciate durante gli episodi di training:

Perdite di value e policy durante il processo di training. Immagine dell'autore
Osserva il grafico e nota:
- Le perdite sembrano distribuite casualmente e non seguono alcun pattern.
- Questo è tipico del training RL, dove l'obiettivo non è minimizzare la loss ma massimizzare le ricompense.
Esegui l'algoritmo PPO
Ora hai tutti i componenti per addestrare l'agente usando PPO. Per mettere tutto insieme, devi:
- Dichiarare gli iperparametri come fattore di sconto, batch size, learning rate, ecc.
- Istanziate buffer come array nulli per memorizzare ricompense e perdite di ogni iterazione.
- Creare un'istanza dell'agente usando la funzione
create_agent(). - Eseguire iterativamente i passaggi forward e backward usando le funzioni
forward_pass()eupdate_policy(). - Testare le prestazioni della policy usando la funzione
evaluate(). - Aggiungere ai rispettivi buffer le perdite di policy e value, e le ricompense, dalle funzioni di training e valutazione.
- Calcolare la media di ricompense e perdite sugli ultimi step temporali. L'esempio qui sotto calcola la media su gli ultimi 40 time step.
- Stampare i risultati della valutazione ogni pochi step. L'esempio qui sotto stampa ogni 10 step.
- Terminare il processo quando la ricompensa media supera una certa soglia.
Il codice qui sotto mostra come dichiarare in Python una funzione che fa tutto questo:
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 all'esempio qui 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
Puoi visualizzare ed eseguire il programma funzionante in questo notebook DataLab!
5. Tuning e ottimizzazione degli iperparametri
Nel machine learning, gli iperparametri controllano il processo di training. Di seguito spiego alcuni degli iperparametri importanti usati in PPO:
- Learning rate: il learning rate decide di quanto possono variare i parametri della policy a ogni iterazione. Nella discesa del gradiente stocastica, l'entità dell'aggiornamento dei parametri a ogni iterazione è determinata dal prodotto tra learning rate e gradiente.
- Parametro di clipping: È anche indicato come epsilon, ε. Determina l'entità con cui viene troncato il rapporto tra policy. Il rapporto tra la nuova e la vecchia policy è consentito variare nell'intervallo [1-ε, 1+ε]. Quando è al di fuori di questo intervallo, viene artificialmente troncato per rientrarci.
- Batch size: si riferisce al numero di step da considerare per ogni aggiornamento del gradiente. In PPO, il batch size è il numero di time step necessari per applicare la policy e calcolare la loss surrogata per aggiornare i parametri della policy. In questo articolo abbiamo usato un batch size di 64.
- Iteration steps: è il numero di volte in cui ogni batch viene riutilizzato per eseguire il backward pass. Nel codice di questo articolo è indicato come
PPO_STEPS. In ambienti complessi, eseguire molte volte il forward pass è costoso dal punto di vista computazionale. Un'alternativa più efficiente è ri-eseguire ogni batch alcune volte. In genere si consiglia un valore tra 5 e 10. - Fattore di sconto: È anche indicato come gamma, γ. Esprime quanto le ricompense immediate siano più preziose di quelle future. È simile al concetto di tasso d'interesse nel calcolo del valore temporale del denaro. Quando è più vicino a 0, significa che le ricompense future valgono meno e l'agente dovrebbe privilegiare quelle immediate. Quando è più vicino a 1, significa che le ricompense future sono importanti.
- Coefficiente di entropia: il coefficiente di entropia determina il bonus di entropia, calcolato come prodotto tra coefficiente di entropia ed entropia della distribuzione. Il ruolo del bonus di entropia è introdurre più casualità nella policy. Questo incoraggia l'agente a esplorare lo spazio delle policy. Tuttavia, se la casualità è troppo alta, il training non converge verso una policy ottimale.
- Criteri di successo per il training: devi impostare i criteri per decidere quando il training ha successo. Un modo comune è porre la condizione che la media delle ricompense sugli ultimi N tentativi (episodi) superi una certa soglia. Nell'esempio di codice sopra, questo è espresso con la variabile
N_TRIALS. Quando è impostata su un valore più alto, il training richiede più tempo perché la policy deve raggiungere la soglia di ricompensa su più episodi. Inoltre, porta a una policy più robusta ma più costosa computazionalmente. Nota che PPO è una policy stocastica e ci saranno episodi in cui l'agente non supera la soglia. Quindi, se il valore diN_TRIALSè troppo alto, il tuo training potrebbe non terminare.
Strategie per ottimizzare le prestazioni di PPO
Ottimizzare le prestazioni del training di algoritmi PPO richiede prove ed errori ed esperimenti con diversi valori di iperparametri. Tuttavia, ci sono alcune linee guida generali:
- Fattore di sconto: quando le ricompense di lungo periodo sono importanti, come nell'ambiente CartPole, dove l'asta deve rimanere stabile nel tempo, parti con un valore di gamma moderato, ad esempio 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 è aggiunto alla loss surrogata. Controlla l'ordine di grandezza della loss surrogata e l'entropia della distribuzione prima di decidere il coefficiente di entropia. In questo articolo abbiamo usato un coefficiente di entropia di 0,01.
- Parametro di clipping: il parametro di clipping decide quanto la policy aggiornata possa differire da quella attuale. Un valore grande del parametro incoraggia una migliore esplorazione dell'ambiente, ma rischia di destabilizzare il training. Vuoi un parametro che permetta un'esplorazione graduale prevenendo aggiornamenti destabilizzanti. In questo articolo abbiamo usato un valore di 0,2.
- Learning rate: quando il learning rate è troppo alto, la policy viene aggiornata a grandi passi e a ogni iterazione il processo di training può diventare instabile. Quando è troppo basso, il training richiede troppo tempo. Questo tutorial ha usato un learning rate di 0,001, che funziona bene per questo ambiente. In molti casi si consiglia un learning rate di 1e-5.
Sfide e buone pratiche in PPO
Dopo aver spiegato i concetti e i dettagli implementativi di PPO, vediamo ora le sfide e le buone pratiche.
Sfide comuni nell'addestramento con PPO
Anche se PPO è ampiamente usato, devi conoscere le possibili criticità per risolvere con successo problemi reali con questa tecnica. Alcune di queste sfide sono:
- Convergenza lenta: in ambienti complessi, PPO può essere poco efficiente in termini di campioni e necessita di molte interazioni con l'ambiente per convergere alla policy ottimale. Questo lo rende lento e costoso da addestrare.
- Sensibilità agli iperparametri: PPO si basa su un'esplorazione efficiente dello spazio delle policy. La stabilità del processo di training e la velocità di convergenza sono sensibili ai valori degli iperparametri. I valori ottimali spesso si determinano solo per tentativi.
- Overfitting: Gli ambienti RL sono tipicamente inizializzati con parametri casuali. Il training PPO si basa sulla ricerca della policy ottimale in funzione dell'ambiente dell'agente. A volte il training converge a un insieme di parametri ottimali per uno specifico ambiente ma non per ambienti randomizzati. In genere si affronta con molte iterazioni, ognuna con un ambiente di training randomizzato in modo diverso.
- Ambienti dinamici: ambienti RL semplici, come CartPole, sono statici: le regole restano le stesse nel tempo. Molti altri ambienti, come un robot che impara a camminare su una superficie instabile e in movimento, sono dinamici: le regole dell'ambiente cambiano nel tempo. Per ottenere buone prestazioni in tali ambienti, PPO spesso richiede ulteriore fine-tuning.
- Esplorazione vs sfruttamento: il meccanismo di clipping di PPO assicura che gli aggiornamenti della policy restino in una regione di fiducia. Tuttavia, limita anche l'esplorazione dello spazio delle azioni. Questo può portare a convergenza verso ottimi locali, in particolare in ambienti complessi. D'altra parte, consentire troppa esplorazione può impedire la convergenza verso una qualsiasi policy ottimale.
Best practice per addestrare modelli PPO
Per ottenere buoni risultati con PPO, raccomando alcune best practice, tra cui:
- Normalizzare le feature in input: normalizzare i valori di returns e advantages riduce la variabilità nei dati e porta ad aggiornamenti del gradiente più stabili. La normalizzazione porta tutti i valori su un intervallo numerico coerente. Aiuta a ridurre l'effetto di outlier e valori estremi, che altrimenti potrebbero distorcere gli aggiornamenti del gradiente e rallentare la convergenza.
- Usare batch size adeguatamente grandi: batch piccoli consentono aggiornamenti e training più rapidi ma possono portare a convergenza verso ottimi locali e instabilità nel processo di training. Batch più grandi permettono all'agente di apprendere policy robuste, rendendo il training più stabile. Tuttavia, batch troppo grandi sono subottimali: oltre ad aumentare i costi computazionali, rendono gli aggiornamenti della policy meno reattivi alla funzione di valore perché basati su medie stimate su grandi batch. Inoltre, possono portare a overfitting degli aggiornamenti a quello specifico batch.
- Iteration steps: in generale è consigliabile riutilizzare ogni batch per 5-10 iterazioni. Questo rende il training più efficiente. Riutilizzare lo stesso batch troppe volte porta a overfitting. Il codice si riferisce a questo iperparametro come
PPO_STEPS. - Eseguire valutazioni regolari: per rilevare l'overfitting è essenziale monitorare periodicamente l'efficacia della policy. Se la policy risulta inefficace in certi scenari, potrebbe essere necessario ulteriore training o fine-tuning.
- Sintonizzare gli iperparametri: come spiegato in precedenza, il training PPO è sensibile ai valori degli iperparametri. Sperimenta con vari valori per individuare l'insieme più adatto al tuo problema specifico.
- Backbone condiviso: come illustrato in questo articolo, usare un backbone condiviso previene squilibri tra le reti actor e critic. Condividere un backbone tra actor e critic aiuta con l'estrazione di feature condivise e una comprensione comune dell'ambiente. Questo rende l'apprendimento più efficiente e stabile. Inoltre, aiuta a ridurre la complessità di spazio e tempo dell'algoritmo.
- Numero e dimensione dei layer nascosti: aumenta il numero di layer nascosti e le dimensioni per ambienti più complessi. Problemi più semplici come CartPole possono essere risolti con un solo layer nascosto. Il layer nascosto usato in questo articolo ha 64 dimensioni. Rendere la rete molto più grande del necessario è uno spreco computazionale e può renderla instabile.
- Early stopping: interrompere il training quando vengono soddisfatte le metriche di valutazione aiuta a prevenire l'overtraining ed evita sprechi di risorse. Una metrica comune è quando l'agente supera la soglia di ricompense negli ultimi N eventi.
Conclusione
In questo articolo abbiamo discusso PPO come metodo per risolvere problemi di RL. Abbiamo poi dettagliato i passaggi per implementare PPO con PyTorch. Infine, abbiamo presentato alcuni suggerimenti di performance e best practice per PPO.
Il modo migliore per imparare è implementare tu stesso il codice. Puoi anche modificare il codice per funzionare con altri ambienti di controllo classici in Gym. Per imparare come implementare agenti RL usando Python e Gymnasium di OpenAI, segui il corso Reinforcement Learning with Gymnasium in Python!



