Se você quer mergulhar no mundo do Reinforcement Learning (RL) de forma prática, o problema do CartPole é um excelente ponto de partida. Neste post, vamos destrinchar o código completo, explicando cada parte detalhadamente para que você possa entender como funciona um agente inteligente capaz de equilibrar esse desafio. Prepare-se para uma jornada didática e divertida e aprenda como resolver o Cart Pole com DQN!
O que é o CartPole e por que começar por aqui?
Imagine um carrinho que se move sobre um trilho, com um poste conectado a ele por uma articulação. O objetivo é manter o poste em pé, controlando o movimento do carrinho para a esquerda ou para a direita. Parece um brinquedo simples, mas é um problema clássico de RL que oferece uma ótima introdução aos conceitos fundamentais.
O CartPole é popular porque:
- É fácil de entender e visualizar.
- Apresenta um desafio interessante que requer estratégia.
- Permite treinar agentes de forma relativamente rápida.
Ingredientes da receita: As ferramentas que vamos utilizar
Para construir nosso agente inteligente, vamos usar as seguintes ferramentas:
- Python: Nossa linguagem de programação.
- Gymnasium: Biblioteca para criar e interagir com o ambiente CartPole.
- TensorFlow/Keras: Frameworks para construir e treinar nossa rede neural.
- NumPy: Biblioteca para manipulação de arrays e cálculos numéricos.
Mão na massa: Código explicado em detalhes
Agora, vamos dissecar o código em partes menores para facilitar a compreensão.
1. Importando as bibliotecas necessárias
Comece instalando as dependências e importando as bibliotecas necessárias:
pip install numpy tensorflow "gymnasium[all]"
Começamos importando as bibliotecas que vamos utilizar ao longo do código:
import random
from collections import deque
import gymnasium as gym
import numpy as np
from tensorflow.keras.layers import Dense
from tensorflow.keras.models import Sequential
from tensorflow.keras.optimizers import Adam
- random: Para gerar números aleatórios (usado na exploração).
- collections.deque: Para criar uma fila (usada no replay buffer).
- gymnasium as gym: Para interagir com o ambiente CartPole.
- numpy as np: Para realizar operações numéricas eficientes.
- tensorflow.keras.layers.Dense: Para criar camadas densas em nossa rede neural.
- tensorflow.keras.models.Sequential: Para construir nossa rede neural.
- tensorflow.keras.optimizers.Adam: Para otimizar os pesos da rede neural.
2. Definindo os hiperparâmetros
Os hiperparâmetros são os “temperos” da nossa receita de Reinforcement Learning. Eles controlam como o agente aprende e afetam o desempenho do treinamento.
# Define hiperparâmetros
learning_rate = 0.001
discount_factor = 0.99
epsilon_start = 1.0
epsilon_end = 0.01
epsilon_decay_steps = 1000
replay_buffer_size = 10000
batch_size = 64
sync_target_interval = 100
- learning_rate: A taxa de aprendizado controla o tamanho dos ajustes nos pesos da rede neural a cada atualização.
- discount_factor (Gamma): Determina a importância das recompensas futuras. Um valor próximo de 1 dá mais peso às recompensas a longo prazo.
- epsilon_start e epsilon_end: Controlam a exploração. No início (epsilon_start), o agente explora mais. Ao longo do tempo, epsilon diminui até epsilon_end, favorecendo a explotação (uso do conhecimento aprendido).
- epsilon_decay_steps: Define quantos passos leva para epsilon decair de epsilon_start para epsilon_end.
- replay_buffer_size: Define o tamanho do buffer de replay (a memória do agente).
- batch_size: Quantidade de experiências usadas para atualizar a rede neural em cada passo de treinamento.
- sync_target_interval: A frequência com que os pesos da rede alvo são atualizados com os pesos da rede principal. Isso ajuda a estabilizar o treinamento.
3. Criando o ambiente CartPole
# Cria o ambiente CartPole-v1
env = gym.make('CartPole-v1')
state_space_size = env.observation_space.shape[-1]
action_space_size = env.action_space.n
weights_file = "cartpole_dqn_weights.weights.h5"
load_existing_weights = True
- env = gym.make(‘CartPole-v1’): Cria uma instância do ambiente CartPole.
- state_space_size = env.observation_space.shape[-1]: Obtém o tamanho do espaço de estados (as informações que o agente recebe do ambiente). No CartPole, o estado é um vetor de 4 dimensões (posição do carrinho, velocidade do carrinho, ângulo do poste e velocidade angular do poste).
- action_space_size = env.action_space.n: Obtém o tamanho do espaço de ações (as ações que o agente pode tomar). No CartPole, o agente pode se mover para a esquerda (0) ou para a direita (1).
- weights_file: Nome do arquivo para salvar/carregar os pesos da rede neural treinada.
- load_existing_weights: Flag para indicar se devemos carregar pesos pré-treinados (útil para continuar o treinamento).
4. Definindo a arquitetura da rede neural (Q-Network)
# Define a arquitetura da rede neural (Q-Network)
def create_q_network(state_space_size, action_space_size):
model = Sequential([
Dense(64, activation='relu', input_shape=(state_space_size,)),
Dense(64, activation='relu'),
Dense(action_space_size, activation='linear') # Saída para valores Q
])
model.compile(optimizer=Adam(learning_rate=learning_rate), loss='mse')
return model
Aqui definimos a estrutura da rede neural, que é o “cérebro” do nosso agente. Esta rede é chamada de Q-Network.
- Sequential: Define um modelo sequencial, onde as camadas são empilhadas uma após a outra.
- Dense(64, activation=’relu’, input_shape=(state_space_size,)): Cria uma camada densa (totalmente conectada) com 64 neurônios e função de ativação ReLU (Rectified Linear Unit). Esta é a camada de entrada da rede, que recebe o estado do ambiente como entrada.
- Dense(64, activation=’relu’): Cria outra camada densa com 64 neurônios e ativação ReLU.
- Dense(action_space_size, activation=’linear’): Cria a camada de saída, com um neurônio para cada ação possível. A ativação é linear porque queremos que a saída represente os valores Q (estimativas de recompensa futura) para cada ação.
- model.compile(optimizer=Adam(learning_rate=learning_rate), loss=’mse’): Configura o modelo para o treinamento, definindo o otimizador (Adam) e a função de perda (MSE – Mean Squared Error).
5. Criando as redes Q (Online e Target)
# Cria as redes Q (online e target)
q_network = create_q_network(state_space_size, action_space_size)
target_network = create_q_network(state_space_size, action_space_size)
target_network.set_weights(q_network.get_weights())
Usamos duas redes neurais para melhorar a estabilidade do treinamento: a Q-Network (online) e a Target-Network.
- q_network: É a rede que é treinada e atualizada a cada passo. Ela é usada para escolher as ações.
- target_network: É uma cópia da Q-Network que é atualizada periodicamente (a cada sync_target_interval passos). Ela é usada para calcular os “alvos” para o treinamento da Q-Network. Isso ajuda a evitar instabilidade no treinamento, pois os alvos não estão em constante mudança.
- target_network.set_weights(q_network.get_weights()): Inicializa os pesos da Target-Network com os mesmos valores da Q-Network.
6. Implementando o agente DQN
# Implementa o agente DQN
class DQNAgent:
def __init__(self, state_space_size, action_space_size, learning_rate, discount_factor, epsilon_start, epsilon_end, epsilon_decay_steps, replay_buffer_size, batch_size, sync_target_interval):
self.state_space_size = state_space_size
self.action_space_size = action_space_size
self.learning_rate = learning_rate
self.discount_factor = discount_factor
self.epsilon = epsilon_start
self.epsilon_start = epsilon_start
self.epsilon_end = epsilon_end
self.epsilon_decay_steps = epsilon_decay_steps
self.replay_buffer = deque(maxlen=replay_buffer_size)
self.batch_size = batch_size
self.train_step = 0
self.sync_target_interval = sync_target_interval
def choose_action(self, state):
if random.random() < self.epsilon:
return env.action_space.sample()
else:
q_values = q_network.predict(np.expand_dims(state, axis=0), verbose=None)[0]
return np.argmax(q_values)
def store_experience(self, state, action, reward, next_state, done):
self.replay_buffer.append((state, action, reward, next_state, done))
def sample_experience(self):
if len(self.replay_buffer) < self.batch_size:
return None
batch = random.sample(self.replay_buffer, self.batch_size)
states, actions, rewards, next_states, dones = zip(*batch)
return np.array(states), np.array(actions), np.array(rewards).reshape(-1, 1), np.array(next_states), np.array(dones).reshape(-1, 1)
def update_q_network(self):
if len(self.replay_buffer) < self.batch_size:
return
states, actions, rewards, next_states, dones = self.sample_experience()
# Calcular os valores Q alvo usando a rede target
target_q_values = target_network.predict(next_states, verbose=None)
max_next_q_values = np.max(target_q_values, axis=1, keepdims=True)
targets = rewards + self.discount_factor * max_next_q_values * (1 - dones)
# Obter os valores Q previstos para os estados atuais
q_values = q_network.predict(states, verbose=None)
# Atualizar os valores Q para as ações tomadas
for i in range(self.batch_size):
q_values_target = q_values.copy()
q_values_target[[i], [actions[[i]]]] = targets[[i]]
q_network.train_on_batch(states, q_values_target)
# Atualizar o epsilon
self.epsilon = self.epsilon_end + (self.epsilon_start - self.epsilon_end) * np.exp(
-self.train_step / self.epsilon_decay_steps
)
self.train_step += 1
# Sincronizar as redes Q e target
if self.train_step % self.sync_target_interval == 0:
target_network.set_weights(q_network.get_weights())
Esta classe define o agente DQN (Deep Q-Network), que é o componente principal do nosso sistema.
- __init__: O construtor da classe inicializa os atributos do agente, como o tamanho do espaço de estados e ações, a taxa de aprendizado, o fator de desconto, o valor de epsilon (para a exploração), o buffer de replay e o tamanho do lote.
- choose_action(self, state): Este método escolhe uma ação com base no estado atual. Ele usa uma estratégia epsilon-greedy: com probabilidade epsilon, ele escolhe uma ação aleatória (exploração); caso contrário, ele usa a rede Q para escolher a ação com o maior valor Q previsto (explotação).
- store_experience(self, state, action, reward, next_state, done): Este método armazena a experiência do agente (o estado, a ação, a recompensa, o próximo estado e se o episódio terminou) no replay buffer.
- sample_experience(self): Este método amostra um lote aleatório de experiências do replay buffer. Estas experiências serão usadas para treinar a rede Q.
- update_q_network(self): Este método atualiza os pesos da rede Q usando um lote de experiências amostradas do replay buffer. Ele calcula os valores Q “alvo” usando a rede alvo e usa estes valores para treinar a rede Q usando a função train_on_batch.
- Cálculo dos valores Q alvo: É a parte crucial do aprendizado. Usamos a Target Network para estimar a melhor recompensa futura possível (max_next_q_values). A recompensa real (rewards) é combinada com essa estimativa descontada pelo discount_factor. Se o episódio termina (done), o alvo é apenas a recompensa recebida.
- Atualização dos valores Q para as ações tomadas: Ajustamos os valores Q previstos pela Q-Network para se aproximarem dos valores alvo calculados.
- Atualização do epsilon: Reduzimos gradualmente o valor de epsilon ao longo do tempo para favorecer a explotação.
- Sincronização das redes Q e target: Copiamos os pesos da Q-Network para a Target-Network a cada sync_target_interval passos.
7. Inicializando o agente e carregando pesos (Opcional)
if load_existing_weights:
epsilon_start = epsilon_end
# Inicializa o agente
agent = DQNAgent(state_space_size, action_space_size, learning_rate, discount_factor, epsilon_start, epsilon_end, epsilon_decay_steps, replay_buffer_size, batch_size, sync_target_interval)
if load_existing_weights:
try:
q_network.load_weights(weights_file)
target_network.load_weights(weights_file)
print(f"Pesos carregados do arquivo: {weights_file}")
except FileNotFoundError:
print("Arquivo de treinamento não encontrado. Iniciando o treinamento do zero.")
Esta parte inicializa o agente DQN e carrega pesos pré-treinados (se load_existing_weights for True).
- Se load_existing_weights for True, definimos epsilon_start igual a epsilon_end para reduzir a exploração, pois o agente já deve ter aprendido alguma coisa.
- Criamos uma instância da classe DQNAgent.
- Se load_existing_weights for True, tentamos carregar os pesos das redes Q e alvo a partir do arquivo weights_file. Se o arquivo não for encontrado, iniciamos o treinamento do zero.
8. O loop de treinamento
# Loop de treinamento
num_episodes = 500
target_score = 195 # Score médio para considerar o problema resolvido
consecutive_successes = 0
for episode in range(num_episodes):
state, info = env.reset()
total_reward = 0
done = False
while not done:
action = agent.choose_action(state)
next_state, reward, done, truncated, info = env.step(action)
agent.store_experience(state, action, reward, next_state, done)
agent.update_q_network()
state = next_state
total_reward += reward
print(f"Episódio: {episode + 1}, Recompensa Total: {total_reward:.2f}, Épsilon: {agent.epsilon:.2f}")
# Verificar se o agente está aprendendo (opcional)
if total_reward >= target_score:
consecutive_successes += 1
if consecutive_successes >= 10: # Considerar resolvido após 10 episódios com alta recompensa
print(f"\nProblema resolvido em {episode + 1} episódios!")
break
else:
consecutive_successes = 0
# Salvar os pesos após o treinamento
q_network.save_weights(weights_file)
print(f"Pesos da rede Q salvos em: {weights_file}")
env.close()
Este é o coração do nosso código. Ele itera por um número definido de episódios (num_episodes) e treina o agente em cada episódio.
- num_episodes: O número total de episódios de treinamento.
- target_score: A pontuação média que consideramos como “resolvido” o problema do CartPole.
- consecutive_successes: Contador de episódios consecutivos onde o agente obteve uma pontuação alta.
- Loop for episode in range(num_episodes):
- Reseta o ambiente (env.reset()) para começar um novo episódio.
- Inicializa total_reward e done para controlar a recompensa total e se o episódio terminou.
- Loop while not done:
- Escolhe uma ação usando o método choose_action do agente.
- Executa a ação no ambiente (env.step(action)) e recebe o próximo estado, a recompensa, se o episódio terminou e outras informações.
- Armazena a experiência no replay buffer usando o método store_experience do agente.
- Atualiza a rede Q usando o método update_q_network do agente.
- Atualiza o estado para o próximo estado.
- Adiciona a recompensa à recompensa total.
- Imprime a recompensa total do episódio e o valor atual de epsilon.
- Verifica se o agente está aprendendo (se a recompensa total é maior que target_score). Se sim, incrementa o contador consecutive_successes. Se o agente obtiver consecutive_successes sucessos consecutivos, considera que o problema foi resolvido e interrompe o treinamento.
- Salva os pesos da rede Q no arquivo weights_file após o treinamento.
- Fecha o ambiente (env.close()).
Execute o código e acompanhe o aprendizado do algoritmo em tempo real. Pode levar algumas épocas para que as redes atingam o resultado esperado, mas é muito gratificante ver o resultado.

Considerações finais
Parabéns! Você chegou ao final deste guia completo sobre como resolver o problema do CartPole usando Reinforcement Learning e Python. Cobrimos todos os aspectos do código, desde a importação das bibliotecas até o loop de treinamento.
Este é apenas o começo da sua jornada no mundo do Reinforcement Learning. Existem muitos outros algoritmos, ambientes e desafios para explorar. Encorajo você a experimentar, modificar o código, e aprofundar seus conhecimentos.
O código completo pode ser obtido no repositório do Github, juntamente com o arquivo com os pesos da rede pré treinada.
Agora é a sua vez!
O aprendizado não termina aqui. Para consolidar seu conhecimento e contribuir com a comunidade, convido você a:
- Comentar: Compartilhe suas dúvidas, sugestões ou insights nos comentários abaixo.
- Experimentar: Modifique os hiperparâmetros, a arquitetura da rede ou o algoritmo para ver como o desempenho do agente é afetado.
- Compartilhar: Se este guia foi útil, compartilhe-o com seus amigos e colegas interessados em IA.
Obrigado por ler e boa jornada no mundo do Reinforcement Learning!