DQN算法PyTorch 2.0实战:从零实现CartPole-v1智能控制
1. 环境准备与核心概念
在开始构建DQN模型之前,我们需要先理解几个关键概念。CartPole-v1是OpenAI Gym中的一个经典控制问题:一个小车需要在水平轨道上移动,以保持其顶部的杆子竖直不倒。这个环境的状态空间包含4个连续变量:小车位置、小车速度、杆子角度和杆子顶端速度。
安装必要库:
pip install gym torch numpy matplotlib核心组件对比表:
| 组件 | 传统Q-learning | DQN |
|---|---|---|
| Q值表示 | 查表法 | 神经网络近似 |
| 状态处理 | 离散状态 | 连续状态空间 |
| 内存效率 | 低(存储所有状态) | 高(参数共享) |
| 泛化能力 | 无 | 通过神经网络泛化 |
提示:PyTorch 2.0的torch.compile()可以显著提升训练速度,建议在支持CUDA的设备上启用
2. 网络架构设计与实现
我们将使用PyTorch 2.0构建一个三层的全连接网络作为Q函数近似器。网络输入是4维状态向量,输出是2个动作(向左/向右)的Q值。
import torch import torch.nn as nn import torch.optim as optim class DQN(nn.Module): def __init__(self, state_dim, action_dim): super().__init__() self.net = nn.Sequential( nn.Linear(state_dim, 128), nn.ReLU(), nn.Linear(128, 128), nn.ReLU(), nn.Linear(128, action_dim) ) # 使用PyTorch 2.0的编译优化 self.net = torch.compile(self.net) def forward(self, x): return self.net(x)关键参数说明:
state_dim: CartPole-v1的状态维度(4)action_dim: 动作空间大小(2)- 隐藏层使用ReLU激活函数避免梯度消失
- 输出层直接输出Q值,不适用激活函数
3. 经验回放机制实现
经验回放是DQN稳定训练的关键技术,它通过存储和随机采样过去的经验来打破数据相关性。
from collections import deque import random class ReplayBuffer: def __init__(self, capacity): self.buffer = deque(maxlen=capacity) def push(self, state, action, reward, next_state, done): self.buffer.append((state, action, reward, next_state, done)) def sample(self, batch_size): batch = random.sample(self.buffer, batch_size) states, actions, rewards, next_states, dones = zip(*batch) return ( torch.FloatTensor(states), torch.LongTensor(actions), torch.FloatTensor(rewards), torch.FloatTensor(next_states), torch.FloatTensor(dones) ) def __len__(self): return len(self.buffer)经验回放的优势:
- 提高数据利用率
- 打破样本间的时间相关性
- 使训练数据分布更平稳
4. 完整训练流程实现
下面我们将所有组件整合,实现完整的DQN训练流程。这里采用ε-greedy策略平衡探索与利用,并使用目标网络稳定训练。
import gym import numpy as np import matplotlib.pyplot as plt # 超参数配置 config = { "gamma": 0.99, "lr": 1e-3, "batch_size": 64, "memory_size": 10000, "target_update": 100, "epsilon_start": 1.0, "epsilon_end": 0.01, "epsilon_decay": 500, "episodes": 500 } def train_dqn(): env = gym.make("CartPole-v1") state_dim = env.observation_space.shape[0] action_dim = env.action_space.n policy_net = DQN(state_dim, action_dim) target_net = DQN(state_dim, action_dim) target_net.load_state_dict(policy_net.state_dict()) target_net.eval() optimizer = optim.Adam(policy_net.parameters(), lr=config["lr"]) memory = ReplayBuffer(config["memory_size"]) rewards_history = [] steps_done = 0 for episode in range(config["episodes"]): state = env.reset() episode_reward = 0 while True: # ε-greedy动作选择 epsilon = config["epsilon_end"] + (config["epsilon_start"] - config["epsilon_end"]) * \ np.exp(-1. * steps_done / config["epsilon_decay"]) steps_done += 1 if np.random.rand() < epsilon: action = env.action_space.sample() else: with torch.no_grad(): action = policy_net(torch.FloatTensor(state)).argmax().item() # 执行动作 next_state, reward, done, _ = env.step(action) episode_reward += reward # 存储经验 memory.push(state, action, reward, next_state, done) state = next_state # 训练步骤 if len(memory) > config["batch_size"]: states, actions, rewards, next_states, dones = memory.sample(config["batch_size"]) # 计算当前Q值 current_q = policy_net(states).gather(1, actions.unsqueeze(1)) # 计算目标Q值 with torch.no_grad(): next_q = target_net(next_states).max(1)[0] target_q = rewards + config["gamma"] * next_q * (1 - dones) # 计算损失并更新 loss = nn.MSELoss()(current_q.squeeze(), target_q) optimizer.zero_grad() loss.backward() optimizer.step() # 更新目标网络 if steps_done % config["target_update"] == 0: target_net.load_state_dict(policy_net.state_dict()) if done: break rewards_history.append(episode_reward) print(f"Episode {episode}, Reward: {episode_reward}, Epsilon: {epsilon:.2f}") # 绘制训练曲线 plt.plot(rewards_history) plt.xlabel("Episode") plt.ylabel("Reward") plt.title("DQN Training Progress") plt.show() return policy_net5. 高级调优技巧与可视化
当基础DQN实现能够稳定达到200分后,我们可以进一步优化模型性能。以下是几个实用的调优方向:
超参数优化建议:
- 学习率:尝试1e-4到1e-3之间的值
- 批量大小:32到128之间调整
- γ值:0.9到0.99之间
- 网络结构:增加/减少隐藏层神经元数量
奖励曲线分析:
- 初期奖励波动大:正常现象,表示智能体在探索
- 中期奖励快速上升:学习阶段
- 后期稳定在200分:成功掌握策略
可视化Q值变化:
def visualize_q_values(model, state): with torch.no_grad(): q_values = model(torch.FloatTensor(state)) plt.bar(["Left", "Right"], q_values.numpy()) plt.ylabel("Q Value") plt.title("Action Q Values") plt.show() # 示例使用 trained_model = train_dqn() visualize_q_values(trained_model, [0, 0, 0, 0]) # 中立状态6. 常见问题与解决方案
在实际实现过程中,可能会遇到以下典型问题:
训练不稳定:
- 检查目标网络更新频率
- 减小学习率
- 增大经验回放缓冲区大小
奖励无法提升:
- 调整ε衰减速度
- 检查网络结构是否足够复杂
- 验证奖励函数设计
收敛速度慢:
- 尝试优先经验回放(PER)
- 使用Double DQN减少过估计
- 增加批量大小
7. 扩展应用与进阶方向
当CartPole-v1被攻克后,可以考虑以下进阶挑战:
更复杂环境:
- MountainCar
- LunarLander
- Atari游戏
算法改进:
# Double DQN目标值计算示例 with torch.no_grad(): next_actions = policy_net(next_states).argmax(1) next_q = target_net(next_states).gather(1, next_actions.unsqueeze(1)) target_q = rewards + config["gamma"] * next_q.squeeze() * (1 - dones)工程优化:
- 使用PyTorch Lightning简化训练循环
- 添加TensorBoard日志记录
- 实现分布式训练
在实际项目中,我发现将ε初始值设为1.0并缓慢衰减到0.01的效果最好,同时目标网络每100步更新一次能很好平衡训练稳定性与收敛速度。对于更复杂的控制问题,建议先从小型网络开始,逐步增加复杂度以避免过拟合。