O que é o método actor-critic
O Actor-Critic é um método de aprendizado por reforço híbrido. Ele combina duas abordagens: baseada em valor e baseada em política. Primeiramente, o “ator” (actor) decide qual ação deve ser tomada. Em segundo lugar, o “crítico” (critic) avalia se a ação foi boa. O ator é uma rede que aprende a política diretamente. O crítico é uma rede que aprende o valor do estado. Esta arquitetura resolve problemas de alta variância dos gradientes. Por exemplo, um robô pode aprender a andar mais rápido. O ator escolhe os movimentos, e o crítico dá notas. Assim, o aprendizado torna-se mais estável e eficiente.
Uma grande vantagem é o aprendizado a cada passo (online). Diferentemente do REINFORCE, não é necessário esperar o episódio terminar. Primeiramente, o erro temporal (TD Error) é calculado instantaneamente. Este erro mede a diferença entre a recompensa esperada e a real. Depois, o erro é usado para atualizar tanto o ator quanto o crítico. Consequentemente, o algoritmo aprende muito mais rápido. O TD Error é uma das ideias mais importantes do reforço. Ele reduz a variância e melhora a estabilidade. Este método é amplamente usado em problemas complexos. Por exemplo, em jogos, robótica e finanças.
Arquitetura básica do actor-critic
A arquitetura possui duas redes neurais principais. O ator (policy network) mapeia estados para ações. A saída do ator é uma distribuição de probabilidade (softmax). O crítico (value network) mapeia estados para valores. A saída do crítico é um único número (o valor do estado). Primeiramente, o ator escolhe uma ação baseada no estado atual. Depois, o ambiente retorna a recompensa e o próximo estado. O crítico avalia o valor do estado anterior e do novo estado. Com essas informações, o TD Error é calculado. Finalmente, ambas as redes são atualizadas simultaneamente.
O ator é atualizado na direção que maximiza a vantagem. A vantagem é calculada usando o TD Error. O crítico é atualizado para minimizar o erro de predição. Duas redes separadas são usadas para estabilidade. Contudo, elas podem compartilhar camadas iniciais. Isso reduz o número total de parâmetros. O algoritmo aprende a cada transição (estado, ação, recompensa). Não é necessário armazenar episódios completos. Esta é a principal diferença para o REINFORCE. O Actor-Critic é considerado o estado da arte atual.
Hiperparâmetros e fórmulas matemáticas
A taxa de aprendizado do ator (alpha) é um hiperparâmetro crucial. Valores típicos variam entre 0.0001 e 0.001. A taxa de aprendizado do crítico (beta) pode ser diferente. Geralmente, beta é igual ou ligeiramente maior que alpha. O fator de desconto (gamma) prioriza recompensas futuras. Gamma é tipicamente 0.99 para problemas contínuos. O número de neurônios nas redes afeta a capacidade. Valores entre 64 e 256 são comuns na prática. A função de ativação ReLU é usada nas camadas escondidas. A inicialização dos pesos deve ser pequena e aleatória.
O TD Error é calculado pela seguinte fórmula: \(\delta_t = r_{t+1} + \gamma V(s_{t+1}) – V(s_t)\). O gradiente do ator é ponderado pelo TD Error: \(\nabla_\theta J(\theta) = \mathbb{E}[\nabla_\theta \log \pi_\theta(a_t|s_t) \delta_t]\). A atualização do ator segue a regra: \(\theta \leftarrow \theta + \alpha \nabla_\theta \log \pi_\theta(a_t|s_t) \delta_t\). O crítico é atualizado para minimizar o erro quadrático: \(L_{critic} = \delta_t^2 = (r_{t+1} + \gamma V(s_{t+1}) – V(s_t))^2\). O gradiente do crítico é: \(\nabla_\omega L = -2\delta_t \nabla_\omega V(s_t)\). Estas fórmulas são a base do algoritmo Actor-Critic. O TD Error substitui o retorno bruto usado no REINFORCE. Isso reduz drasticamente a variância do gradiente. O aprendizado torna-se mais rápido e estável.
Enunciado do exemplo clássico: cartpole
Você deve equilibrar um poste sobre um carrinho móvel. O ambiente se chama CartPole-v1 na biblioteca Gymnasium. O carrinho pode mover-se para esquerda ou direita. O objetivo é manter o poste na posição vertical. Cada passo sem queda gera uma recompensa de +1. O episódio termina quando o poste cai mais de 15 graus. Seu agente Actor-Critic deve aprender a política ótima. Uma rede neural ator escolherá as ações. Uma rede neural crítico avaliará cada estado. O TD Error guiará o aprendizado de ambas as redes.
Primeiramente, implemente o ator com saída softmax. A entrada do ator são as 4 observações do ambiente. A saída são 2 ações (esquerda ou direita). Em segundo lugar, implemente o crítico com saída linear. A entrada do crítico também são as 4 observações. A saída é um único valor (o valor esperado do estado). Depois, execute um loop de treinamento por 600 episódios. A cada passo, calcule o TD Error e atualize as redes. Ao final, exiba gráficos das recompensas e do erro. O código abaixo resolve completamente este enunciado.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 |
# -*- coding: utf-8 -*- """Actor_Critic_CartPole_Corrected.ipynb""" import numpy as np import matplotlib.pyplot as plt import gymnasium as gym # Instalar PyTorch !pip install torch -q import torch import torch.nn as nn import torch.optim as optim import torch.nn.functional as F print("✅ Bibliotecas carregadas!") print(f"PyTorch versão: {torch.__version__}") # Dispositivo device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print(f"Dispositivo: {device}") # Ambiente env = gym.make('CartPole-v1') # Hiperparâmetros GAMMA = 0.99 LR_ACTOR = 0.0005 # Taxa aprendizado do ator LR_CRITIC = 0.001 # Taxa aprendizado do crítico EPISODES = 600 MAX_STEPS = 500 print(f"\n📊 Ambiente CartPole-v1:") print(f" - Estado: {env.observation_space.shape[0]} dimensões") print(f" - Ações: {env.action_space.n} (0=esquerda, 1=direita)") print(f" - Gamma: {GAMMA}") print(f" - LR Ator: {LR_ACTOR}, LR Crítico: {LR_CRITIC}") # Rede do Ator (Policy) class ActorNetwork(nn.Module): def __init__(self, state_size, action_size): super(ActorNetwork, self).__init__() self.fc1 = nn.Linear(state_size, 128) self.fc2 = nn.Linear(128, 128) self.fc3 = nn.Linear(128, action_size) def forward(self, x): x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) x = F.softmax(self.fc3(x), dim=-1) return x # Rede do Crítico (Value) class CriticNetwork(nn.Module): def __init__(self, state_size): super(CriticNetwork, self).__init__() self.fc1 = nn.Linear(state_size, 128) self.fc2 = nn.Linear(128, 128) self.fc3 = nn.Linear(128, 1) # Saída: valor do estado def forward(self, x): x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) x = self.fc3(x) return x # Parâmetros state_size = env.observation_space.shape[0] action_size = env.action_space.n # Inicializar redes actor = ActorNetwork(state_size, action_size).to(device) critic = CriticNetwork(state_size).to(device) # Otimizadores actor_optimizer = optim.Adam(actor.parameters(), lr=LR_ACTOR) critic_optimizer = optim.Adam(critic.parameters(), lr=LR_CRITIC) print("\n🔧 Redes criadas:") print(f" - Ator: {state_size} -> 128 -> 128 -> {action_size} (softmax)") print(f" - Crítico: {state_size} -> 128 -> 128 -> 1 (valor)") # Função para escolher ação def select_action(state): state = torch.FloatTensor(state).unsqueeze(0).to(device) probs = actor(state) m = torch.distributions.Categorical(probs) action = m.sample() log_prob = m.log_prob(action) return action.item(), log_prob # Métricas episode_rewards = [] episode_lengths = [] td_errors_history = [] print("\n" + "="*60) print("🎯 TREINANDO ACTOR-CRITIC COM TD ERROR") print("="*60 + "\n") # Treinamento for episode in range(EPISODES): state, _ = env.reset() total_reward = 0 episode_td_errors = [] for step in range(MAX_STEPS): # Ator escolhe ação action, log_prob = select_action(state) # Executa ação no ambiente next_state, reward, terminated, truncated, _ = env.step(action) done = terminated or truncated # Crítico avalia valores state_tensor = torch.FloatTensor(state).unsqueeze(0).to(device) next_state_tensor = torch.FloatTensor(next_state).unsqueeze(0).to(device) value = critic(state_tensor) next_value = critic(next_state_tensor) if not done else torch.tensor([[0.0]]).to(device) # Cálculo do TD Error (fórmula central) with torch.no_grad(): td_target = reward + GAMMA * next_value td_error = td_target - value # δ = r + γV(s') - V(s) episode_td_errors.append(td_error.item()) # CORREÇÃO: Atualizar Ator (usando TD Error como vantagem) # O td_error aqui já está com gradiente do crítico, então precisamos detach actor_loss = -log_prob * td_error.detach() actor_optimizer.zero_grad() actor_loss.backward() actor_optimizer.step() # CORREÇÃO: Atualizar Crítico (minimizar erro quadrático) # Recriar o TD error para o crítico para evitar conflitos value_for_critic = critic(state_tensor) next_value_for_critic = critic(next_state_tensor) if not done else torch.tensor([[0.0]]).to(device) td_target_for_critic = reward + GAMMA * next_value_for_critic td_error_for_critic = td_target_for_critic - value_for_critic critic_loss = td_error_for_critic.pow(2) critic_optimizer.zero_grad() critic_loss.backward() critic_optimizer.step() state = next_state total_reward += reward if done: break # Armazenar métricas episode_rewards.append(total_reward) episode_lengths.append(step + 1) td_errors_history.append(np.mean(episode_td_errors) if episode_td_errors else 0) # Progresso if (episode + 1) % 50 == 0: avg_reward = np.mean(episode_rewards[-50:]) avg_td = np.abs(np.mean(td_errors_history[-50:])) print(f"📈 Episódio {episode + 1:4d}/{EPISODES} | " f"Recompensa média: {avg_reward:6.1f} | " f"|TD Error| médio: {avg_td:.4f} | " f"Passos: {step + 1:3d}") # Avaliação print("\n" + "="*60) print("🏆 AVALIANDO O AGENTE TREINADO") print("="*60) test_episodes = 10 test_rewards = [] test_lengths = [] actor.eval() for episode in range(test_episodes): state, _ = env.reset() total_reward = 0 steps = 0 for step in range(MAX_STEPS): with torch.no_grad(): state_tensor = torch.FloatTensor(state).unsqueeze(0).to(device) probs = actor(state_tensor) action = torch.argmax(probs).item() next_state, reward, terminated, truncated, _ = env.step(action) done = terminated or truncated state = next_state total_reward += reward steps += 1 if done: break test_rewards.append(total_reward) test_lengths.append(steps) status = "✅" if total_reward >= 475 else "⚠️" print(f"{status} Teste {episode + 1:2d}: {int(total_reward):3d} passos") # Resultados print("\n" + "="*60) print("📊 RESULTADOS FINAIS") print("="*60) print(f"🎯 Melhor recompensa no treino: {int(max(episode_rewards))} passos") print(f"📈 Média nos testes: {np.mean(test_rewards):.1f} ± {np.std(test_rewards):.1f} passos") print(f"⭐ Maior pontuação nos testes: {int(max(test_rewards))} passos") # Gráficos fig, axes = plt.subplots(2, 2, figsize=(14, 10)) # Gráfico 1: Evolução das recompensas axes[0, 0].plot(episode_rewards, alpha=0.5, color='blue', linewidth=0.5, label='Por episódio') if len(episode_rewards) >= 50: moving_avg = np.convolve(episode_rewards, np.ones(50)/50, mode='valid') axes[0, 0].plot(range(49, len(episode_rewards)), moving_avg, 'r-', linewidth=2, label='Média móvel (50)') axes[0, 0].set_xlabel('Episódio', fontsize=12) axes[0, 0].set_ylabel('Recompensa (passos)', fontsize=12) axes[0, 0].set_title('🎯 Evolução do Actor-Critic', fontsize=14) axes[0, 0].legend() axes[0, 0].grid(True, alpha=0.3) axes[0, 0].axhline(y=500, color='green', linestyle='--', alpha=0.5, label='Meta (500)') axes[0, 0].set_ylim([0, 520]) # Gráfico 2: Evolução do TD Error axes[0, 1].plot(np.abs(td_errors_history), 'g-', linewidth=1.5, label='|TD Error| médio') axes[0, 1].set_xlabel('Episódio', fontsize=12) axes[0, 1].set_ylabel('|TD Error|', fontsize=12) axes[0, 1].set_title('📉 Erro Temporal (TD Error)', fontsize=14) axes[0, 1].legend() axes[0, 1].grid(True, alpha=0.3) axes[0, 1].set_yscale('log') axes[0, 1].set_ylim([0.001, 10]) # Gráfico 3: Desempenho nos testes colors = ['green' if r >= 475 else 'orange' for r in test_rewards] bars = axes[1, 0].bar(range(1, test_episodes + 1), test_rewards, color=colors, edgecolor='black') axes[1, 0].axhline(y=np.mean(test_rewards), color='red', linestyle='--', linewidth=2, label=f'Média: {np.mean(test_rewards):.1f}') axes[1, 0].axhline(y=475, color='blue', linestyle=':', linewidth=1.5, label='Sucesso (475)') axes[1, 0].set_xlabel('Episódio de teste', fontsize=12) axes[1, 0].set_ylabel('Passos', fontsize=12) axes[1, 0].set_title('🏅 Desempenho Final do Agente', fontsize=14) axes[1, 0].legend() axes[1, 0].grid(True, alpha=0.3) axes[1, 0].set_ylim([0, 520]) for bar, valor in zip(bars, test_rewards): axes[1, 0].text(bar.get_x() + bar.get_width()/2, bar.get_height() + 5, f'{int(valor)}', ha='center', va='bottom', fontsize=9) # Gráfico 4: Comprimento dos episódios axes[1, 1].plot(episode_lengths, alpha=0.5, color='purple', linewidth=0.5) axes[1, 1].set_xlabel('Episódio', fontsize=12) axes[1, 1].set_ylabel('Passos por episódio', fontsize=12) axes[1, 1].set_title('⏱️ Sobrevivência do Poste', fontsize=14) axes[1, 1].grid(True, alpha=0.3) axes[1, 1].axhline(y=500, color='green', linestyle='--', alpha=0.5, label='Meta (500)') axes[1, 1].legend() plt.tight_layout() plt.show() # Análise do aprendizado print("\n" + "="*60) print("📈 ANÁLISE DO APRENDIZADO") print("="*60) initial_avg = np.mean(episode_rewards[:100]) final_avg = np.mean(episode_rewards[-100:]) improvement = final_avg - initial_avg final_td = np.mean(np.abs(td_errors_history[-100:])) print(f"📊 Recompensa inicial (100 episódios): {initial_avg:.1f} passos") print(f"📊 Recompensa final (100 episódios): {final_avg:.1f} passos") print(f"📈 Melhoria: {improvement:.1f} passos") print(f"📉 |TD Error| final: {final_td:.4f}") if improvement > 200: print("✅ Aprendizado excelente! O TD Error foi eficaz!") elif improvement > 100: print("✅ Bom aprendizado! O Actor-Critic funcionou bem.") else: print("⚠️ Aumente os episódios para 1000 para melhores resultados") # Explicação do TD Error print("\n" + "="*60) print("💡 O QUE É O TD ERROR?") print("="*60) print("✅ Mede a diferença entre recompensa esperada e real") print("✅ Fórmula: δₜ = r + γV(sₜ₊₁) - V(sₜ)") print("✅ Positivo: ação melhor que o esperado") print("✅ Negativo: ação pior que o esperado") print("✅ Guia o aprendizado do ator e do crítico") # Explicação final print("\n" + "="*60) print("📚 RESUMO DO ACTOR-CRITIC") print("="*60) print("✅ Ator: aprende a política (quais ações tomar)") print("✅ Crítico: aprende o valor dos estados") print("✅ TD Error: conecta ator e crítico") print("✅ Aprendizado online (a cada passo)") print("✅ Menor variância que REINFORCE") print("\n🚀 O agente aprendeu a equilibrar o poste!") print(" O TD Error permitiu aprendizado estável e rápido.") env.close() print("\n✨ Fim do treinamento Actor-Critic!") |
O código implementa o Actor-Critic com TD Error completo. O ator escolhe ações, e o crítico avalia os estados. Primeiramente, o TD Error é calculado a cada passo. Depois, o ator é atualizado usando este erro como vantagem. O crítico é atualizado para minimizar o erro quadrático. Este processo acontece online, sem esperar o episódio terminar. Consequentemente, o aprendizado é muito mais rápido. O agente aprende a equilibrar o poste em poucos episódios. O gráfico do TD Error mostra a convergência do aprendizado. Parabéns! Você implementou um algoritmo Actor-Critic completo.