资讯动态

基于Pong的深度强化学习算法对比:DQN与PPO实现解析

发布时间:2026/10/18 15:39:24 来源:尧图企业网站定制
简介雅达利游戏Pong是深度强化学习算法验证的经典环境具有离散动作空间和即时反馈的特点很适合进行算法研究。该资源聚焦于此系统实现了深度Q网络、近端策略优化、优势演员评论家、深度确定性策略梯度、异步优势演员评论家等多种经典与前沿算法面向希望掌握DRL原理并对比算法表现的开发者与研究者。压缩包内共5个文件包含两个Python源码、一个h5模型权重、一张结果对比图和一段gif演示动画文件类型覆盖代码、模型、图表与动画整体仅2.13MB轻量简洁。运行源码可观察智能体在Pong中的学习曲线动手调整网络与超参数深入理解经验回放、目标网络、优势函数等核心机制。目前已有506人学习下载可作为从理论到实践的优质参考。1. 为什么是 Pong多种深度强化学习算法从原理到实现的试金石Pong 这个游戏规则简单到一句话就能说清两个球拍一个球谁先到 21 分谁赢。但在深度强化学习领域它近乎扮演着“试金石”的角色——界面干净、状态输入不需要复杂建模、奖励虽然稀疏但信号稳定每一个算法在这里都会快速暴露它的真实性格值函数方法收敛扎实但样本要求高策略梯度方法上手快但对超参数敏感。这个标题所指向的工作本质上是把 DQN、Double DQN、Dueling DQN、PPO 等主流深度强化学习算法放进完全相同的环境和状态管线里跑通、调优、并对比结果。适合正在入门 DR尤其深度强化学习核心算法的开发者也适合拿课程设计或毕业设计题目做强化学习实现的同学。下面不推公式给的是能一次跑通的环境搭建、预处理流程、训练循环、参数说明和避坑经验。2. 搭好训练台环境选择、状态预处理与最小可运行骨架无论后面跑哪种算法Pong 的训练入口都是一致的一个 Atari 环境、一份统一的观测预处理、一个能反复产出样本的循环。这一章先把这个公共底座搭牢后续换算法时只替换“学习更新”的部分。2.1 环境选择为什么固定用 PongNoFrameskip-v4常见做法是直接用 Gymnasium或早期 OpenAI Gym提供的PongNoFrameskip-v4这个环境 ID。它有两个关键特点第一NoFrameskip表示环境不会自动做随机的帧跳帧每一步返回的都是真实游戏帧这给训练带来了可控的时序第二Atari 环境的观测是Box(210, 160, 3)的 RGB 图像动作空间是Discrete(6)六个动作分别是 NOOP、FIRE、UP、DOWN、RIGHT、LEFT但对 Pong 来说真正影响球拍的只有 UP 和 DOWN其余动作不会产生有效位移。import gymnasium as gym env gym.make(PongNoFrameskip-v4, render_modergb_array) print(观测空间:, env.observation_space) # Box(210, 160, 3) print(动作空间:, env.action_space) # Discrete(6) obs, info env.reset() print(单帧观测形状:, obs.shape)逻辑说明render_mode设为rgb_array可以在训练时不弹窗只在必要时用env.render()抓帧保存为视频。env.reset()在 Gymnasium 中返回(obs, info)二元组和旧版 Gym 只返回obs不同初学很容易在这里踩一脚。观测空间是 210x160 的 RGB 图像但网络吃的一般不是它原始的样子需要预处理。参数说明如果本地环境较新PongNoFrameskip-v4是默认注册的标准 ID如果换用Pong-v0环境内部会做随机帧跳导致相邻观测之间的时序不稳定对依赖精确位置感知的 DQN 类算法非常不友好。所以做算法对比时固定用NoFrameskip版本帧跳逻辑由我们自己在外部统一实现保证所有算法面对同样的环境动态。2.2 状态预处理管线灰度、裁剪、缩放与帧堆叠网络吃的不是单帧图像而是连续四帧的堆叠。原因很直观Pong 的球速很快单帧只能看到位置看不到方向和速度四帧拼在一起卷积网络才能从像素位移中提取运动信息。这套预处理最早在 DQN 论文中被确定为标准做法灰度化、裁剪黑边和计分板、缩放到 84x84、按通道堆叠连续帧并归一化。import cv2 import numpy as np from collections import deque class AtariPreprocessor: def __init__(self, height84, width84, stack_size4): self.height height self.width width self.stack_size stack_size self.frames deque(maxlenstack_size) def reset(self, raw_frame): processed self._process(raw_frame) for _ in range(self.stack_size): self.frames.append(processed) return self._stack() def step(self, raw_frame): processed self._process(raw_frame) self.frames.append(processed) return self._stack() def _process(self, raw): # Pong 的计分板在画面上方上下有黑边裁掉更有利于网络聚焦球拍和球 gray cv2.cvtColor(raw, cv2.COLOR_RGB2GRAY) crop gray[34:194, :] resized cv2.resize(crop, (self.width, self.height), interpolationcv2.INTER_AREA) return resized.astype(np.float32) / 255.0 def _stack(self): # 返回形状 (4, 84, 84)通道维度放在最前 return np.stack(self.frames, axis0)逻辑说明deque(maxlen4)保证每次 append 后永远只保留最近 4 帧reset时用同一帧填充四次避免回合起点处的观测缺失。裁剪范围[34:194, :]是常见经验值Pong 的原始画面上下各有黑边上方计分板大约占前 34 行裁掉后既减少计算量也防止网络被计分数字干扰。参数说明归一化到[0, 1]是浮点输入的标准做法如果为了节省显存也可以归一化到[0, 255]的 uint8 存储但送进网络时一般仍会转成浮点。缩放插值用INTER_AREA比INTER_LINEAR更适合缩小图像能保留更多球和球拍的轮廓信息。2.3 动作执行与最小训练骨架frame skip 的细节Atari 环境里一个常见的做法是执行一次动作后连续重复 4 帧再观测这被称为 frame skip。它让智能体不能每一帧都改变方向反而迫使策略更有连续性同时大幅缩短训练步数。更细致的处理是在这 4 帧中取像素变化最大的一帧作为观测可以避开某些游戏精灵闪烁带来的视觉抖动。def skip_frame_step(env, action, skip4): obs, total_reward, terminated, truncated None, 0.0, False, False for _ in range(skip): obs, reward, terminated, truncated, info env.step(action) total_reward reward if terminated or truncated: break return obs, total_reward, terminated, truncated, info逻辑说明累计奖励是因为一帧内可能同时发生得分与失分以外的奖励变化Pong 每次只给 ±1 的回合奖励但统一累加是 Atari 训练的标准习惯。提前跳出循环可以避免在回合结束时继续调env.step导致的异常。参数说明skip4是 DQN 论文以来的默认值增大 skip 会让训练步数更少但球在画面中运动的颗粒度变粗网络更难学到精细的接球时机减小到 2 则训练成本变高。实际对比中发现Pong 在 skip4 下能稳定工作不建议为了追求分辨率去调小它。最小训练骨架就是把预处理、skip、回放、训练循环串起来replay ReplayBuffer(capacity100000) state preprocessor.reset(env.reset()[0]) for step in range(total_steps): action select_action(state, epsilon) # 探索/利用策略 raw_obs, reward, terminated, truncated, _ skip_frame_step(env, action) next_state preprocessor.step(raw_obs) replay.push(state, action, reward, next_state, terminated) if len(replay) warmup_steps: # 填满后才开始学习 batch replay.sample(batch_size) train_one_step(batch) state next_state if terminated or truncated: state preprocessor.reset(env.reset()[0])逻辑说明warmup_steps是为了让回放缓冲区里先积累足够多样的样本一般取 10000 步左右过早开始学习会让网络反复拟合少量重复经验严重拖慢收敛。epsilon在 DQN 类算法里负责探索这一步先占位具体衰减逻辑在下一章展开。参数说明回放缓冲区容量 100000 是 Pong 这类短回合游戏的一个合理起点。容量太小会让采样集中在最近经验上破坏回放机制打破相关性的初衷容量过大比如 1M在 Pong 上收益不大反而增加采样内存。如果显存吃紧可以先从 50000 起步看曲线。3. 值函数路线DQN、Double DQN 与 Dueling DQN 的实现与改动值函数方法的核心是把“状态-动作”映射成一个数值网络学习的是这个数值本身。在 Pong 上DQN 是第一个能稳定玩出正分的主流算法但原始 DQN 存在过估计和动作解耦问题后面两个变体分别从不同角度补了短板。这一章先把公共网络写好再逐一说明变体的改动量。3.1 从 Q-Learning 到 DQN三个关键机制先立住DQN 相对传统 Q-Learning 有三个关键贡献经验回放、目标网络、以及卷积网络直接吃图像。经验回放通过随机采样打破样本之间的时间相关性让梯度更新更接近独立同分布假设目标网络提供一份滞后更新的 Q 值作为回归目标避免网络同时追赶自己和自己的目标带来的震荡卷积网络则让状态输入从手工特征变成原始像素。没有这三个机制DQN 在 Pong 上几乎是训不出来的。需要说明选型理由在 Pong 上值函数方法比策略梯度方法更吃样本量但优势是训练过程相对稳定梯度更新的方差小。因此如果你的机器只有 CPU 或者显存很小优先跑通 DQN 系列是更现实的选择。import torch import torch.nn as nn import torch.nn.functional as F class BaseCNN(nn.Module): def __init__(self, in_channels4): super().__init__() self.features nn.Sequential( nn.Conv2d(in_channels, 32, kernel_size8, stride4), nn.ReLU(), nn.Conv2d(32, 64, kernel_size4, stride2), nn.ReLU(), nn.Conv2d(64, 64, kernel_size3, stride1), nn.ReLU(), nn.Flatten(), ) # 用假输入推断卷积输出维度避免手工计算 dummy torch.zeros(1, in_channels, 84, 84) self.fc_in self.features(dummy).size(1) def forward(self, x): return self.features(x) class DQN(BaseCNN): def __init__(self, in_channels4, num_actions6, duelingFalse): super().__init__(in_channels) self.dueling dueling if dueling: self.value_head nn.Linear(self.fc_in, 1) self.advantage_head nn.Linear(self.fc_in, num_actions) else: self.q_head nn.Sequential( nn.Linear(self.fc_in, 512), nn.ReLU(), nn.Linear(512, num_actions), ) def forward(self, x): feat self.features(x) if not self.dueling: return self.q_head(feat) value self.value_head(feat) advantage self.advantage_head(feat) # Dueling 的聚合方式减去优势均值保证可辨识性 return value advantage - advantage.mean(dim1, keepdimTrue)逻辑说明dummy前向计算卷积输出维度的做法比硬编码 7x7 更稳避免改输入尺寸时手算翻车。duelingTrue时网络把状态价值和优势分离value_head输出一个标量描述当下状态整体好坏advantage_head输出每个动作相对该状态的好坏。聚合时减去优势均值可以解决值函数不可辨识的问题这是 Dueling DQN 的标准实现。参数说明卷积核从 8 到 4 再到 3通道数从 32 到 64是 Atari 环境最经典的配置直接照搬即可。第一层卷积 stride4 会把 84x84 直接压到 20x20大幅降低计算量但在 Pong 这种小球场景中球可能只有一两个像素过大的 stride 会让小目标在特征图中消失。若发现网络总学不会追球可以尝试把第一层 stride 改成 2代价是训练变慢。3.2 DQN 训练循环经验回放、目标网络与 epsilon 衰减DQN 的训练循环由三块拼装采样动作时按 epsilon 概率随机探索否则取网络输出的 argmax每一步把刚产生的转移存入回放缓冲区定期从回放采样 batch 更新网络。epsilon 的衰减策略直接影响探索质量这一步的失败率很高。def select_action(state, q_net, epsilon): if np.random.random() epsilon: return env.action_space.sample() state_t torch.tensor(state, dtypetorch.float32).unsqueeze(0) with torch.no_grad(): q_values q_net(state_t) return q_values.argmax(dim1).item() for step in range(1, total_steps 1): # epsilon 线性衰减前 100k 步从 1.0 降到 0.01之后保持 if step 100000: epsilon 1.0 - step / 100000 * 0.99 else: epsilon 0.01 action select_action(state, q_net, epsilon) raw_obs, reward, terminated, truncated, _ skip_frame_step(env, action) next_state preprocessor.step(raw_obs) replay.push(state, action, reward, next_state, terminated) if len(replay) 10000 and step % 4 0: batch_obs, batch_act, batch_rew, batch_next, batch_done replay.sample(32) loss compute_dqn_loss(batch_obs, batch_act, batch_rew, batch_next, batch_done, q_net, target_net) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(q_net.parameters(), 10.0) optimizer.step() if step % 10000 0: target_net.load_state_dict(q_net.state_dict()) if terminated or truncated: state preprocessor.reset(env.reset()[0]) else: state next_state逻辑说明step % 4 0表示每 4 个环境步更新一次网络这是 DQN 论文中常用的“训练频率稀疏化”技巧减少相邻样本带来的相关性。clip_grad_norm_把梯度范数限制在 10.0防止偶尔出现的离群样本把参数推出稳定区。目标网络每 10000 步硬同步一次同步频率过大会让目标跟踪太紧失去稳定性过小会让学习变慢。参数说明epsilon 衰减周期 100k 步是 Pong 的合理范围但如果你发现智能体到后期仍然频繁乱动可以把周期延长到 200k 步反过来如果前期探索足够网络已经开始接球可以缩短到 50k 步。这里有个血泪经验epsilon 从 1.0 线性降到 0.01 比指数衰减更稳指数衰减前期降得太快会导致探索不足表现为 reward 长时间停留在 -21一次都不赢。3.3 Double DQN 与 Dueling DQN各自的改动到底改在哪Double DQN 解决的是过估计问题。原始 DQN 在计算下一状态目标时直接用目标网络的 max 值这会让 Q 估计系统性偏高。Double DQN 的改动只有一行逻辑用在线网络选择动作用目标网络评估该动作的值。def compute_dqn_loss(obs, act, rew, next_obs, done, q_net, target_net, gamma0.99): q_values q_net(obs).gather(1, act.unsqueeze(1)).squeeze(1) with torch.no_grad(): if double_dqn: # 在线网络选动作目标网络给值 next_action q_net(next_obs).argmax(dim1, keepdimTrue) next_q target_net(next_obs).gather(1, next_action).squeeze(1) else: # 原始 DQN直接取目标网络的 max next_q target_net(next_obs).max(dim1)[0] target rew gamma * (1 - done.float()) * next_q return F.smooth_l1_loss(q_values, target)逻辑说明原始 DQN 的max操作同时完成了“选动作”和“评估值”两件事任何评估噪声都会被 max 放大。Double DQN 把动作选择交给实时更新的在线网络目标网络只负责打分两边的错误不会叠加。在 Pong 这类奖励对称的游戏中过估计会让智能体高估某个动作的长期收益表现为明明球已飞远还在原地上下抖动。参数说明smooth_l1_loss也就是 Huber loss对离群目标值更鲁棒比 MSE 更适合强化学习场景。如果换回 MSE遇到奖励异常大的样本时 loss 会突然飙升虽然 Pong 的奖励始终是 ±1但累积回报的 bootstrap 值仍然可能较大。Dueling DQN 的改动则集中在网络结构上训练算法完全不变。它把 Q 值拆成状态价值和动作优势两部分意味着当球的位置不值得采取任何不同动作时优势分支可以趋近于零而状态价值分支仍然能正常学习。这在 Pong 中体现在等待球弹回的场景球在对方半场时你做什么动作收益都差不多Dueling 结构不会因为无意义动作的微小误差扰乱整体 Q 值。常见实现中把 DQN 的duelingTrue即可完成切换其余超参数不需要动。4. 策略梯度路线REINFORCE 与 PPO 在 Pong 上的实现差异值函数方法需要先学出一个 Q 值再导出策略而策略梯度方法直接优化策略本身的参数输入状态输出动作概率分布并用实际回报调节概率高低。它最大的优点是天然支持随机策略——Pong 里偶尔带一点随机性会让接球更稳而不是傻傻地只在两个固定动作间切换。4.1 为什么在对比实验中要加上 PPO只做 DQN 系列对比会带来一个盲区值函数方法的收敛曲线相似度高可能让你误以为所有强化学习算法的性格都一样。Pong 是少数几种策略梯度方法比 DQN 更快见正分的游戏之一原因是它的奖励信号虽然稀疏但方差相对可控策略梯度对每回合的回报直接做 credit assignment不需要等 Q 值慢慢逼近真实值。把你的实验项目加上 PPO对比结果才完整。4.2 PPO 的实现要点Actor-Critic、GAE 与 clipped surrogatePPO 的核心是一个 Actor-Critic 网络Actor 输出动作概率分布Critic 输出状态价值两者共享卷积特征提取层。更新时用 clip 机制限制单次更新的步长防止策略在一步更新中变化过大导致训练崩掉。class ActorCritic(BaseCNN): def __init__(self, in_channels4, num_actions6): super().__init__(in_channels) self.policy_head nn.Linear(self.fc_in, num_actions) self.value_head nn.Linear(self.fc_in, 1) def forward(self, x): feat self.features(x) logits self.policy_head(feat) value self.value_head(feat).squeeze(-1) return logits, value def act(self, x, sampleTrue): logits, value self.forward(x) dist torch.distributions.Categorical(logitslogits) if sample: action dist.sample() else: action torch.argmax(logits, dim1) log_prob dist.log_prob(action) return action, log_prob, value逻辑说明Categorical分布直接把 logits 转成动作概率采样时按概率抽取评估时取 argmax。act方法同时返回动作、对数概率和状态价值三者都是后续更新所需的。共享特征层的好处是在 Pong 这种小图像任务中减少参数总量坏处是特征需要同时服务策略和价值两个目标训练初期可能出现两边互相拉扯不过 Pong 的观测简单共享层不会成为瓶颈。参数说明如果追求更强的分离性可以分成两个独立的 CNN 分支但 Pong 上没有必要会白白增加显存占用。如果跑更复杂的游戏比如需要长期规划的再考虑分开。GAE广义优势估计是 PPO 计算 advantage 的默认工具。它用 Critic 估算的价值构造一个比单步 TD 误差更平滑的优势信号def compute_gae(rewards, values, dones, gamma0.99, lam0.95): advantages torch.zeros_like(rewards) gae 0.0 for t in reversed(range(len(rewards))): if t len(rewards) - 1: next_value 0.0 else: next_value values[t 1] delta rewards[t] gamma * next_value * (1 - dones[t]) - values[t] gae delta gamma * lam * (1 - dones[t]) * gae advantages[t] gae returns advantages values return advantages, returns逻辑说明lam是 GAE 的衰减参数取值 0.95 意味着优势信号在时间尺度上偏向“中程”不完全依赖单步奖励也不完全依赖整回合回报。倒序遍历是 GAE 的经典实现从回合末尾向开头累积这样每个时间步的优势都包含了后续所有步的信息。注意当某一步doneTrue时next_value视为 0因为回合终止后不再有未来回报。参数说明gamma0.99是大多数 Atari 任务的默认折现因子lam偏大0.99会让优势更接近整回合蒙特卡洛回报方差更大但偏差更小偏小0.9则更接近单步 TD方差小但可能出现奖励传播不及时的问题。Pong 上从 0.95 起步不要一上来就动这个参数。PPO 更新阶段的 clipped surrogate 是它稳定的关键def ppo_update(batch): obs, act, old_log_prob, adv, returns batch logits, values ac_net(obs) dist torch.distributions.Categorical(logitslogits) new_log_prob dist.log_prob(act) ratio (new_log_prob - old_log_prob).exp() adv_normalized (adv - adv.mean()) / (adv.std() 1e-8) surr1 ratio * adv_normalized surr2 torch.clamp(ratio, 1 - clip_eps, 1 clip_eps) * adv_normalized policy_loss -torch.min(surr1, surr2).mean() value_loss F.mse_loss(values, returns) entropy_bonus dist.entropy().mean() total_loss policy_loss 0.5 * value_loss - 0.01 * entropy_bonus optimizer.zero_grad() total_loss.backward() optimizer.step()逻辑说明ratio是新旧策略下同一动作概率的比值大于 1 说明新策略更愿意选这个动作小于 1 则反之。clip 操作把 ratio 限制在 0.9~1.1 之间目的是防止策略在单次更新中步子迈得太大。取min(surr1, surr2)保证策略只会在对 advantage 有利的方向被推大不会因为一次偶然的高回报就激进地改变策略。entropy bonus 鼓励策略保持一定随机性避免过早去过拟合单一动作模式。参数说明clip_eps0.1是 PPO 论文的默认值调大到 0.2 可以加快学习但风险增加。Pong 的任务比较稳0.1 够用如果你发现策略更新太慢可以先调到 0.2 试一轮观察 divergence 情况再决定。0.5 * value_loss和0.01 * entropy_bonus是常见的权重系数不要随意调大 entropy 权重否则策略会一直乱动学不会接球。4.3 两条路线在 Pong 上的对比维度与选型建议值函数路线和策略梯度路线在 Pong 上差异明显。从训练曲线看DQN 通常要几十万步后才能稳定为正分但一旦学会策略动作非常果断PPO 更常出现“先用大量随机探索积累回放然后在几万步内突然学会”的跳跃式上升且最终策略会保留一定随机性让接球看起来更稳。下表给出你可以在实验报告中直接使用的对比维度对比维度DQN 系列PPO样本效率较低需要百万帧级相对更高单环境也能收敛训练稳定性依赖目标网络与回放缓冲依赖 clip 系数与 GAE 系数探索机制epsilon 线性衰减概率分布的固有熵回合随机性实现复杂度中等需维护双网络较高需管理 rollout 与 GAE适合场景环境交互成本低、离线学习有并行环境或多个采样器时更划算这条线的实现里面最大的坑是存储和计算 rolloout 数据。PPO 需要保留当轮所有观测、动作、对数概率、价值、奖励、终止标志打包成 batch 后再更新。如果你沿用 DQN 的逐样本训练逻辑而不做 mini-batch 周期更新PPO 的 clip 机制就形同虚设。常见做法是写一个简单的 RolloutBuffer容量为horizon比如 128 步满了一轮后统一更新再清空继续采样。5. 训练 Pong 避坑指南5 个高频翻车现场与排查路径只要你前后跑过 DQN 和 PPO大概率会撞上下面这些现象。我把它们按“现象→原因→解决”梳理出来遇到时直接对照排查能省下大量调参时间。5.1 现象训练了很久回合平均奖励一直是 -21-21 意味着智能体每回合都输 21 个球一场没赢。最常见原因是 epsilon 衰减太快智能体在学会基本接球之前就进入纯利用模式。它只会重复几个固定动作而固定动作又没能碰到球。排查方式打印每一步的 epsilon 值看它在哪个时间点降到了 0.01 以下回溯到该时间点前的训练步数确认是否真的积累过足够多探索样本。解决把 epsilon 衰减周期从 100k 步拉长到 200k或者改成分段线性衰减——前 20% 步从 1.0 降到 0.1后 80% 从 0.1 慢慢降到 0.01。5.2 现象loss 不降反升或出现 NaNloss 上升通常不是“没学对”而是优化过程不稳定。先看梯度范数是否异常可以在 backward 后用grad_norm sum(p.grad.norm() ** 2 for p in net.parameters()) ** 0.5打印出来。如果某几个 step 后梯度范数飙升到几千以上说明是梯度爆炸。NaN 一般是学习率过高导致的数值溢出。解决给所有网络参数统一做clip_grad_norm_(params, 10.0)并把学习率从 1e-4 降到 5e-5 重试。Pong 不是复杂的控制问题学习率太高只会让 Q 值和策略分布震荡。5.3 现象智能体学会了“不动”——球过来时不迎球这是典型的过拟合局部策略在 Pong 里不动有可能恰好在球弹回时接到偶尔的成功会给网络一个正的 TD 误差让它误以为不动是个好策略。解决检查奖励是否出现了正反馈被放大的情况。很多人会把奖励按自己的业务逻辑乘以 10这在 Pong 里会让 TD 误差变得很大。保持环境原始 ±1 奖励不要在 reward 上做手脚。另一个解决方向是降低 epsilon 的最小值到 0.001给策略保留极少量随机尝试打破“不动”的僵局。5.4 现象episode 永远不结束一个回合跑了很久Pong 的回合会在某方先到 21 分时结束但实际训练中你经常看到智能体在场地里来回击球连续几万步都不结束。这不是模型出了问题而是智能体已经学会了“接球并回击”但还没学会“得分”。处理训练循环需要给单个 episode 设置最大步数上限比如 20000 步达到后强制truncatedTrue并重置。注意区分terminated和truncated两者在 Gymnasium 里都会返回True但truncated意味着步数超限而非游戏结束计算目标值时如果误把truncated当作终止会把本应继续的未来回报人为清空导致价值估计偏低。5.5 现象训练曲线抖动剧烈偶发跑出正分但下个回合又跌回 -21PPO 上尤其常见偶尔一次高回合回报被记进策略更新策略瞬间发生变化之后连续输球。这是策略梯度方法在单环境采样下的天然高方差特性。解决对回合均值奖励做指数滑动平均后再画曲线别直接画原始回合值更新时对 advantage 做标准化减去均值除以标准差这能让 clip 范围内的梯度更稳定如果还是抖得厉害把clip_eps从 0.1 降到 0.05相当于给策略更新加更紧的限速。6. 让训练结果可见评估协议、奖励曲线与三分钟验证法训练结束不代表实验完成还差一个能让别人相信你结论的评估协议。我的习惯是固定三件事随机种子、评估频率、以及评估时是否关闭探索。def evaluate(env, preprocessor, net, episodes10, renderFalse): total_rewards [] for _ in range(episodes): state preprocessor.reset(env.reset()[0]) ep_reward 0.0 while True: logits, _ net(torch.tensor(state, dtypetorch.float32).unsqueeze(0)) action logits.argmax(dim1).item() # DQN 或确定性评估 obs, reward, terminated, truncated, _ skip_frame_step(env, action) state preprocessor.step(obs) ep_reward reward if terminated or truncated: break total_rewards.append(ep_reward) return np.mean(total_rewards), np.std(total_rewards)逻辑说明评估时关掉 epsilon 概率、关掉策略采样统一用 argmax 确定性动作这样跑出来的分数才是纯策略水平不掺杂随机性。每训练 10000 步跑一次 10 局评估记录均值和标准差。判断标准Pong 的回合均分从 -21 起步。均值到 -10 左右说明学会了基本接球到 -5 说明能稳定回击到 0 以上说明有了得分能力到 5 以上基本已经是能主动调度的水平。如果连续 5 次评估都在 0 以上且标准差低于 2我认为可以停训如果你的实验目的是对比算法那应该在固定预算下对比比如统一训 500k 帧而不是“训到收敛”再比否则每个算法的训练时长不对等结果会有偏差。小技巧把训练过程中的前 20 个回合录成 GIF比奖励曲线直观得多。你经常能从录像里看到 reward 曲线没暴露的问题——球在智能体面前飞过它却傻站着多半是帧堆叠顺序错了球被打出界后智能体还对着空气挥拍说明它把“挥拍”和“得分”的因果关系学反了。这个“三分钟验证法”是我排查训练效果时最先用的手段。我最后留一个自己的教训有一次对比实验里DQN 和 PPO 的预处理脚本、环境 ID、种子全部一样唯一的区别是网络结构但结果 PPO 遥遥领先。我一度以为 PPO 在 Pong 上天生占优后来发现用户在跑 PPO 时无意间多加了环境并行相当于多吃了 8 倍样本。所以——在强化学习里先统一口径再比较算法。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑