1. DQN算法与CartPole问题简介
强化学习中的DQN(Deep Q-Network)算法是Q-learning与深度神经网络的结合体。想象一下教一个机器人骑自行车——它一开始会东倒西歪,但通过不断尝试和反馈(比如保持平衡得1分,摔倒扣5分),最终学会稳定骑行。CartPole-v1环境就是这样一个经典测试场景:控制小车底部的移动来保持顶端杆子的平衡。
传统Q-learning需要维护庞大的Q表格记录每个状态的动作价值,就像用Excel表格记录每个路口该左转还是右转。但当面对连续状态(如杆子的精确倾斜角度)时,表格就力不从心了。DQN用神经网络替代表格,输入当前状态(如杆子角度、小车位置),直接输出各个动作的预期收益,就像给机器人装上了会自主判断的大脑。
PyTorch作为深度学习框架,其动态计算图和自动求导特性让神经网络训练变得简单。比如我们可以用几行代码构建价值评估网络:
import torch.nn as nn class QNetwork(nn.Module): def __init__(self, state_size=4, action_size=2): super().__init__() self.fc = nn.Sequential( nn.Linear(state_size, 64), nn.ReLU(), nn.Linear(64, action_size) ) def forward(self, x): return self.fc(x)这个网络接收4维状态(小车位置、速度、杆角度、角速度),通过全连接层输出左右移动的动作价值。相比传统表格,它能泛化到没见过的状态组合,就像人类看到新路况也能合理应对。
2. 搭建DQN的核心组件
2.1 经验回放机制
想象学习骑自行车时,大脑会记住最近几次摔倒的经历反复琢磨。经验回放(Experience Replay)就是这个原理——将状态转移(state, action, reward, next_state)存入记忆库,训练时随机抽取批次数据打破时间关联性。以下是实现代码:
from collections import deque import random class ReplayBuffer: def __init__(self, capacity=10000): self.buffer = deque(maxlen=capacity) # 固定大小的循环队列 def push(self, transition): self.buffer.append(transition) def sample(self, batch_size): return random.sample(self.buffer, batch_size) def __len__(self): return len(self.buffer)实测发现,当记忆库积累到2000条以上数据后再开始训练效果更好。就像学骑车需要先积累一定经验,而不是刚上车就研究怎么改进姿势。
2.2 目标网络与预测网络
直接用一个网络同时预测和更新目标值,就像学生自己出题自己考——容易陷入自我重复的误区。DQN采用双网络结构:
# 初始化时 self.policy_net = QNetwork().to(device) self.target_net = QNetwork().to(device) self.target_net.load_state_dict(self.policy_net.state_dict()) # 参数同步 # 每隔C步更新目标网络 if step_count % TARGET_UPDATE == 0: self.target_net.load_state_dict(self.policy_net.state_dict())目标网络提供稳定的学习目标,就像考试大纲不会天天变。我测试过不同更新频率,发现每100步更新一次在CartPole中表现最佳。
3. 完整训练流程实现
3.1 环境交互与数据收集
首先初始化Gym环境并封装交互逻辑:
import gym env = gym.make('CartPole-v1') state = env.reset() for episode in range(1000): action = agent.select_action(state) # ε-greedy策略 next_state, reward, done, _ = env.step(action) agent.memory.push((state, action, reward, next_state, done)) state = next_state if done: state = env.reset()这里有个实用技巧:将原始reward乘以0.1能加速初期训练,因为CartPole每步固定+1的奖励会导致初期Q值膨胀。
3.2 网络训练关键步骤
从记忆库采样后,计算时序差分目标:
# 计算当前Q值 state_batch = torch.FloatTensor(np.array(states)) q_values = policy_net(state_batch).gather(1, action_batch) # 计算目标Q值 with torch.no_grad(): next_q = target_net(next_state_batch).max(1)[0] target = reward_batch + GAMMA * next_q * (1 - done_batch) # Huber损失计算 loss = F.smooth_l1_loss(q_values, target.unsqueeze(1))我对比过MSE和Huber损失,后者在训练初期更稳定。当预测值与目标差异大时,Huber损失线性增长避免梯度爆炸。
3.3 超参数调优心得
通过网格搜索验证的最佳参数组合:
BATCH_SIZE = 128 # 每次训练采样数 GAMMA = 0.99 # 折扣因子 EPS_START = 0.9 # 初始探索率 EPS_END = 0.05 # 最小探索率 EPS_DECAY = 1000 # 探索率衰减速度 TARGET_UPDATE = 100 # 目标网络更新步数 LR = 1e-3 # 学习率特别提醒:EPS_DECAY需要与训练总步数匹配。我曾设50000导致还没收敛探索率就降到底,智能体陷入局部最优。
4. 效果评估与可视化
4.1 训练曲线分析
引入滑动平均更清晰观察趋势:
def plot_rewards(rewards, window=100): plt.figure(figsize=(10,5)) plt.plot(rewards, alpha=0.3, label='Raw') plt.plot(pd.Series(rewards).rolling(window).mean(), label=f'{window}-episode avg') plt.xlabel('Episode') plt.ylabel('Reward') plt.legend()典型成功训练会出现三个阶段:
- 初期(<100episode):随机探索,奖励在20-50间波动
- 中期(100-300episode):快速上升期,平均奖励突破100
- 后期(>300episode):稳定在195以上(接近满分200)
4.2 实际运行演示
用训练好的模型实时演示:
state = env.reset() while True: env.render() action = agent.predict(state) # 去掉探索直接取最优动作 state, _, done, _ = env.step(action) if done: break env.close()如果看到杆子能持续平衡超过190步,说明模型已掌握控制策略。有个常见误区:测试时忘记禁用epsilon探索,会导致表现不稳定。
5. 常见问题与解决方案
问题1:奖励始终在30左右徘徊
- 检查记忆库是否足够大(建议1万以上)
- 适当提高探索率EPS_START到0.95
- 增加网络隐藏层神经元数量(如128→256)
问题2:训练后期表现突然崩溃
- 降低学习率(如1e-3→5e-4)
- 增加目标网络更新间隔(如100→200步)
- 尝试梯度裁剪:
torch.nn.utils.clip_grad_norm_(net.parameters(), 10)
问题3:GPU内存不足
- 减小BATCH_SIZE(如256→128)
- 使用
del及时释放中间变量 - 在采样后调用
.cpu()将数据转移出GPU
曾经遇到一个棘手问题:模型在训练时表现良好,但测试时完全失效。最后发现是预处理函数在训练和测试时不一致——训练时对状态做了归一化而测试时忘记处理。这提醒我们保持数据处理的一致性至关重要。