Atari游戏中的深度强化学习:从DQN到PPO的算法演进

张开发
2026/4/8 10:52:44 15 分钟阅读

分享文章

Atari游戏中的深度强化学习:从DQN到PPO的算法演进
1. Atari游戏与深度强化学习的完美结合第一次看到AI玩Atari游戏时我完全被震撼到了。屏幕上那个像素化的小球拍在没有任何明确编程规则的情况下竟然能学会打乒乓球而且水平越来越高。这就是深度强化学习的魔力而Atari游戏正是展示这种魔力的最佳舞台。为什么Atari游戏会成为深度强化学习的试金石这要从它的几个独特优势说起。首先Atari游戏的状态空间游戏画面既复杂又规整。复杂在于每个游戏都有独特的视觉元素和动态变化规整在于它们都遵循相同的图像格式210x160像素的RGB图像。这种特性正好可以用来测试神经网络处理视觉信息的能力。我记得刚开始用《Breakout》打砖块做实验时发现智能体在前几百局的表现简直惨不忍睹 - 它连球拍都接不住。但经过几千轮训练后它不仅能稳定接球还发展出了挖隧道的高级策略把球打到砖块墙的一侧让球从上方反复得分。这种从零开始的学习过程完美展示了深度强化学习的核心思想。2. DQN开启深度强化学习新时代2.1 DQN的核心创新2015年DeepMind发表的DQN论文就像一颗炸弹彻底改变了强化学习的格局。我至今记得第一次复现这个算法时的兴奋感。DQN最巧妙的地方在于它解决了两个关键问题第一是经验回放Experience Replay。想象你在教一个小孩下棋如果让他按顺序记住每一步棋他很快就会忘记早期的关键步骤。DQN的做法是把所有经验都存储在一个记忆库里训练时随机抽取片段学习。这就像让棋手复习各种棋局片段而不是死记硬背一整盘棋。第二是目标网络Target Network。这相当于给学生两套教材 - 一套用来学习训练网络另一套相对稳定的用来考试目标网络。这样可以避免今天学的内容明天就全变了的情况让训练过程更加稳定。2.2 实战DQN从代码到游戏高手让我们用PyTorch实现一个完整的DQN。首先定义网络结构class DQN(nn.Module): def __init__(self, input_shape, num_actions): super(DQN, self).__init__() self.conv1 nn.Conv2d(input_shape[0], 32, kernel_size8, stride4) self.conv2 nn.Conv2d(32, 64, kernel_size4, stride2) self.conv3 nn.Conv2d(64, 64, kernel_size3, stride1) self.fc1 nn.Linear(self._get_conv_output(input_shape), 512) self.fc2 nn.Linear(512, num_actions) def _get_conv_output(self, shape): dummy torch.zeros(1, *shape) dummy self.conv3(self.conv2(self.conv1(dummy))) return int(torch.prod(torch.tensor(dummy.size()))) def forward(self, x): x x.float() / 255.0 # 归一化 x F.relu(self.conv1(x)) x F.relu(self.conv2(x)) x F.relu(self.conv3(x)) x x.view(x.size(0), -1) x F.relu(self.fc1(x)) return self.fc2(x)这个网络结构看似简单但包含了几个关键设计输入图像先经过三个卷积层提取特征使用ReLU激活函数引入非线性最后通过全连接层输出每个动作的Q值训练过程中最需要注意的就是探索-利用的平衡。我通常会设置一个逐渐衰减的ε值epsilon_start 1.0 epsilon_final 0.01 epsilon_decay 50000 def get_epsilon(step): return epsilon_final (epsilon_start - epsilon_final) * math.exp(-1. * step / epsilon_decay)这样在前几万步训练中智能体会更多地进行随机探索随着训练进行逐渐依赖学到的策略。3. 超越DQN算法演进之路3.1 Double DQN解决过度估计问题在实际使用DQN时我发现一个奇怪现象有时候Q值会莫名其妙地膨胀。这就是所谓的过度估计问题。Double DQN通过将动作选择和Q值评估分离巧妙地解决了这个问题# 普通DQN的目标Q值计算 next_q_values target_net(next_states).detach().max(1)[0] # Double DQN的目标Q值计算 next_actions policy_net(next_states).detach().max(1)[1].unsqueeze(1) next_q_values target_net(next_states).gather(1, next_actions).squeeze(1)这个改进看似微小但在某些游戏中能将性能提升30%以上。我在《Space Invaders》上的测试显示Double DQN的平均得分比原始DQN高出约40%。3.2 Dueling DQN价值与优势分离Dueling架构是另一个让我眼前一亮的创新。它将Q值分解为状态价值V和动作优势A两部分class DuelingDQN(nn.Module): def __init__(self, input_shape, num_actions): super(DuelingDQN, self).__init__() # 共享的特征提取层 self.conv1 nn.Conv2d(input_shape[0], 32, kernel_size8, stride4) self.conv2 nn.Conv2d(32, 64, kernel_size4, stride2) self.conv3 nn.Conv2d(64, 64, kernel_size3, stride1) # 价值流和优势流 self.value_stream nn.Sequential( nn.Linear(self._get_conv_output(input_shape), 512), nn.ReLU(), nn.Linear(512, 1) ) self.advantage_stream nn.Sequential( nn.Linear(self._get_conv_output(input_shape), 512), nn.ReLU(), nn.Linear(512, num_actions) ) def forward(self, x): x self._conv_forward(x) values self.value_stream(x) advantages self.advantage_stream(x) # 组合价值与优势 qvals values (advantages - advantages.mean()) return qvals这种结构让网络能更清楚地知道哪些状态是好的高V值以及在某个状态下哪些动作比其他动作更好优势A值。在《Seaquest》这类需要长期策略的游戏中Dueling DQN的表现尤为突出。4. PPO策略梯度方法的巅峰之作4.1 从值函数到策略梯度DQN系列虽然强大但它们都属于值函数方法。而PPOProximal Policy Optimization则代表了策略梯度方法的最高水平。我最初接触PPO时最欣赏它的三个特点直接优化策略而不是间接通过值函数使用重要性采样实现样本高效利用通过裁剪机制确保更新步长不会过大PPO的核心更新公式看起来有点吓人但其实很好理解def compute_loss(batch): states, actions, old_log_probs, returns, advantages batch # 计算新策略的概率 dist policy(states) new_log_probs dist.log_prob(actions) # 概率比 ratio (new_log_probs - old_log_probs).exp() # 裁剪的surrogate目标 surr1 ratio * advantages surr2 torch.clamp(ratio, 1.0 - clip_param, 1.0 clip_param) * advantages policy_loss -torch.min(surr1, surr2).mean() # 值函数损失 value_loss (returns - value_fn(states)).pow(2).mean() # 熵奖励 entropy_loss -dist.entropy().mean() return policy_loss 0.5 * value_loss - 0.01 * entropy_loss4.2 PPO实战训练技巧与调参经验在《Montezumas Revenge》这类稀疏奖励游戏中PPO的表现远超DQN。但要让PPO发挥最佳性能需要注意几个关键点GAEGeneralized Advantage Estimation参数λ通常设置在0.9-0.95之间。我发现在探索性强的游戏中λ取较小值如0.9效果更好。裁剪系数一般设为0.2。但在某些游戏中可能需要调整到0.1或0.3。我通常会先跑一个小规模的网格搜索。并行环境使用多个环境并行收集数据可以显著加快训练。我一般设置8-16个并行环境。def collect_trajectories(envs, policy, value_fn, num_steps): states envs.reset() buffer [] for _ in range(num_steps): with torch.no_grad(): dist policy(states) actions dist.sample() log_probs dist.log_prob(actions) values value_fn(states) next_states, rewards, dones, _ envs.step(actions.cpu().numpy()) buffer.append((states, actions, log_probs, rewards, values, dones)) states next_states # 计算回报和优势 returns, advantages compute_gae(buffer) return buffer, returns, advantages在实际项目中我发现PPO对超参数的选择相当鲁棒这也是它广受欢迎的原因之一。但要想达到顶尖性能还是需要针对具体任务进行细致的调参。

更多文章