O que é o problema baseado em valor?
Em aprendizado por reforço, métodos baseados em valor estimam a “bondade” de cada estado ou ação. Frequentemente, isso é feito por uma função chamada Q-valor. Então, o agente escolhe a ação com maior valor estimado. Contudo, estados reais são complexos e contínuos. Por isso, usamos aproximação de funções com redes neurais. A arquitetura Dueling DQN é uma evolução desse raciocínio.
Primeiramente, uma rede neural comum mapeia estado-ação diretamente. Em contraste, a Dueling DQN separa essa estimativa em duas partes. Isso permite aprendizado mais estável e eficiente. Ademais, ela foca no valor do estado e na vantagem de cada ação. Essa divisão é especialmente útil quando ações são irrelevantes em alguns estados. Consequentemente, o agente aprende mais rápido.
Características e arquitetura do modelo
A arquitetura Dueling DQN possui duas streams (caminhos) após camadas convolucionais ou densas. Um caminho estima o valor do estado \( V(s) \). O outro caminho estima a vantagem \( A(s,a) \) para cada ação. Finalmente, esses valores são combinados para gerar \( Q(s,a) \). A combinação é feita pela seguinte fórmula:
\[ Q(s,a) = V(s) + \left( A(s,a) – \frac{1}{|\mathcal{A}|} \sum_{a’} A(s,a’) \right) \]O termo de subtração da média das vantagens garante identificabilidade. Sem isso, \( V \) e \( A \) poderiam variar arbitrariamente. A vantagem média zero foca o aprendizado no valor real do estado. Isso é uma diferença crucial em relação ao DQN padrão. Hiperparâmetros comuns incluem taxa de aprendizado (ex: 0.0001), fator de desconto (\( \gamma \approx 0.99 \)), e tamanho do buffer replay (ex: 50000). A rede é treinada minimizando o erro quadrático médio entre Q atual e Q alvo.
A equação de atualização usa a seguinte perda:
\[ \mathcal{L} = \mathbb{E}\left[ \left( r + \gamma \max_{a’} Q_{\text{target}}(s’,a’) – Q(s,a) \right)^2 \right] \]Aqui, \( r \) é a recompensa imediata, e \( s’ \) o próximo estado. Um segundo conjunto de pesos (target network) é atualizado suavemente. Assim, o treinamento se torna mais estável. É importante notar que a Dueling DQN compartilha a memória replay do DQN. Todavia, sua arquitetura dupla reduz a superestimação do Q-valor.
Hiperparâmetros e fórmulas matemáticas
Os principais hiperparâmetros são: learning rate (\( \alpha \)), batch size (32–128), e epsilon-greedy decay. O epsilon inicial geralmente é 1.0 e final 0.01. A cada passo, o agente escolhe ação aleatória com probabilidade \( \epsilon \). Do contrário, escolhe \( a = \arg\max_a Q(s,a) \). O fator de desconto \( \gamma \) pondera recompensas futuras. Para exploração eficiente, usa-se também soft target updates (\( \tau \approx 0.001 \)).
Vamos formalizar a vantagem média centralizada novamente:
\[ A_{\text{centralizada}}(s,a) = A(s,a) – \frac{1}{|\mathcal{A}|}\sum_{a’}A(s,a’) \]Então a saída final da Dueling DQN é:
\[ Q(s,a) = V(s) + A_{\text{centralizada}}(s,a) \]Essa forma garante que o valor médio das vantagens seja zero. A função de perda é minimizada via gradiente descendente estocástico. Em muitos ambientes, a Dueling DQN alcança pontuações mais altas que a DQN padrão. Exemplos clássicos incluem o jogo Pong e o ambiente CartPole.
Exemplo clássico: CartPole com Dueling DQN
Enunciado: Um carrinho (agente) deve equilibrar um poste na vertical por 500 passos. A cada passo, ele pode empurrar o carrinho para esquerda ou direita. A recompensa é +1 por cada passo que o poste permanece em pé. O episódio termina se o poste cair (ângulo > ±12°) ou o carrinho sair dos limites. Você deve implementar um agente Dueling DQN que aprenda a equilibrar o poste. Use a biblioteca gymnasium (antigo gym). Treine por 500 episódios e mostre a evolução da recompensa média. Ao final, gere um gráfico da recompensa por episódio e da perda durante o treino.
|
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 |
import gymnasium as gym import numpy as np import torch import torch.nn as nn import torch.optim as optim import random from collections import deque import matplotlib.pyplot as plt # 1. Ambiente env = gym.make('CartPole-v1') # 2. Rede Dueling DQN class DuelingDQN(nn.Module): def __init__(self, state_dim, action_dim, hidden=128): super().__init__() self.feature = nn.Sequential( nn.Linear(state_dim, hidden), nn.ReLU() ) # Stream de valor V(s) self.value_stream = nn.Sequential( nn.Linear(hidden, hidden//2), nn.ReLU(), nn.Linear(hidden//2, 1) ) # Stream de vantagem A(s,a) self.advantage_stream = nn.Sequential( nn.Linear(hidden, hidden//2), nn.ReLU(), nn.Linear(hidden//2, action_dim) ) def forward(self, x): features = self.feature(x) V = self.value_stream(features) # shape (batch, 1) A = self.advantage_stream(features) # shape (batch, action_dim) # Combinação centralizada Q = V + A - A.mean(dim=1, keepdim=True) return Q # 3. Agente class DuelingDQNAgent: def __init__(self, state_dim, action_dim, lr=0.0005, gamma=0.99, epsilon=1.0, epsilon_min=0.01, epsilon_decay=0.995, buffer_size=10000, batch_size=64, tau=0.005): self.action_dim = action_dim self.gamma = gamma self.epsilon = epsilon self.epsilon_min = epsilon_min self.epsilon_decay = epsilon_decay self.batch_size = batch_size self.tau = tau self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") self.q_network = DuelingDQN(state_dim, action_dim).to(self.device) self.target_network = DuelingDQN(state_dim, action_dim).to(self.device) self.target_network.load_state_dict(self.q_network.state_dict()) self.optimizer = optim.Adam(self.q_network.parameters(), lr=lr) self.memory = deque(maxlen=buffer_size) def act(self, state): if np.random.rand() <= self.epsilon: return np.random.randint(self.action_dim) state_t = torch.FloatTensor(state).unsqueeze(0).to(self.device) with torch.no_grad(): q_values = self.q_network(state_t) return q_values.argmax().item() def remember(self, state, action, reward, next_state, done): self.memory.append((state, action, reward, next_state, done)) def learn(self): if len(self.memory) < self.batch_size: return 0.0 batch = random.sample(self.memory, self.batch_size) states, actions, rewards, next_states, dones = zip(*batch) states = torch.FloatTensor(np.array(states)).to(self.device) actions = torch.LongTensor(actions).to(self.device) rewards = torch.FloatTensor(rewards).to(self.device) next_states = torch.FloatTensor(np.array(next_states)).to(self.device) dones = torch.FloatTensor(dones).to(self.device) current_q = self.q_network(states).gather(1, actions.unsqueeze(1)).squeeze() with torch.no_grad(): next_q = self.target_network(next_states).max(1)[0] target_q = rewards + (1 - dones) * self.gamma * next_q loss = nn.MSELoss()(current_q, target_q) self.optimizer.zero_grad() loss.backward() self.optimizer.step() # Soft update target network for target_param, param in zip(self.target_network.parameters(), self.q_network.parameters()): target_param.data.copy_(self.tau * param.data + (1.0 - self.tau) * target_param.data) # Decay epsilon self.epsilon = max(self.epsilon_min, self.epsilon * self.epsilon_decay) return loss.item() # 4. Treinamento state_dim = env.observation_space.shape[0] action_dim = env.action_space.n agent = DuelingDQNAgent(state_dim, action_dim) episodes = 500 rewards_history = [] losses_history = [] for ep in range(episodes): state, _ = env.reset() total_reward = 0 ep_loss = 0 steps = 0 while True: action = agent.act(state) next_state, reward, terminated, truncated, _ = env.step(action) done = terminated or truncated agent.remember(state, action, reward, next_state, done) loss = agent.learn() if loss > 0: ep_loss += loss total_reward += reward state = next_state steps += 1 if done: break rewards_history.append(total_reward) losses_history.append(ep_loss / max(1, steps)) if (ep+1) % 50 == 0: print(f"Ep {ep+1}, Recompensa: {total_reward:.1f}, Epsilon: {agent.epsilon:.3f}") env.close() # 5. Gráficos plt.figure(figsize=(12,5)) plt.subplot(1,2,1) plt.plot(rewards_history) plt.title('Recompensa por Episódio') plt.xlabel('Episódio') plt.ylabel('Passos equilibrados') plt.grid(True) plt.subplot(1,2,2) plt.plot(losses_history) plt.title('Perda Média por Episódio') plt.xlabel('Episódio') plt.ylabel('MSE Loss') plt.grid(True) plt.tight_layout() plt.show() |
O código acima treina o agente usando Dueling DQN. Primeiramente, a rede é definida com duas streams separadas. Depois, o agente interage com o ambiente CartPole. A memória replay guarda as últimas transições. Finalmente, dois gráficos mostram o progresso do aprendizado. Este exemplo pode ser executado diretamente no Google Colab. Aproximação de funções é utilizada para lidar com estados contínuos. Resultados melhores são obtidos com ajuste fino dos hiperparâmetros. Portanto, a arquitetura dupla é uma escolha poderosa para muitos problemas.