HER算法:用“事后聪明”破解强化学习稀疏奖励难题
发布时间:2026/10/2 16:14:17来源:尧图网络
做强化学习的人大概率都遇到过这种尴尬环境搭好了reward函数写好了智能体一跑就是一晚上loss曲线倒是降得挺漂亮看任务成功率却纹丝不动。我第一次做机械臂推动任务时就栽在这上面后来把 hindsight 这套思路搬进来才翻了盘。hindsight 这个词翻译过来就是“事后聪明”换成大白话就是马后炮、事后诸葛亮。这词在人类认知里通常带着点贬义可在强化学习社区里它对应着一个特别实用的算法家族Hindsight Experience Replay后见之明经验回放简称 HER。这个项目名字取得很直白整套方法的核心就是把“失败的经验”重新标记成“成功的经验”喂给智能体让稀疏奖励环境下的训练不再寸步难行。这篇文章就把我从原理理解、代码实现到调参踩坑的完整过程记录下来。如果你正在跟稀疏奖励搏斗或者要做机器人操作、导航、游戏 AI 这类目标导向任务这篇内容应该能帮你省下不少时间。1. 先说清楚为什么一个“事后诸葛亮”能解决稀疏奖励1.1 稀疏奖励到底有多折磨人在大部分强化学习 demo 里reward 都是连续反馈的。比如 CartPole 每坚持一帧给 1 分打砖块每消一个球给 10 分这种环境下智能体每一步都能感受到自己“做得好不好”梯度信号是密布在轨迹里的。但真实世界的任务完全不是这么回事。我做机械臂推箱子时任务描述很简单把桌子上的方块推到红色目标点。初始状态和目标位置都是随机采样每回合最多 50 步。问题来了——如果方块没到红点reward 就是 -1到了就是 0。这种设定下50 步内随机探索能碰到目标点的概率低得可怜绝大多数回合都是 50 个 -1 攒成一整条负反馈。智能体面临的情况是所有轨迹看起来都一样差它根本不知道哪个动作稍微好一点、哪个动作离谱到没边。这就是稀疏奖励的核心困境不是没有奖励信号而是正样本稀薄到训练几乎无法启动。我当时试过几种常规解法。第一种是 reward shaping给方块离目标点的距离加一个连续惩罚逼近目标时 reward 变大一点。听起来合理但手调距离函数的比例系数特别容易出问题系数大了智能体学会在原地打转刷负倒数系数小了等于没加。第二种是加探索噪声把 action 的随机性调大但 50 步的回合长度对纯随机探索来说太短了收益甚微。第三种是 curriculum learning从近的目标点逐渐推到远的但这个需要人工拆进度而且每个新环境都得重来一遍。就在我准备放弃稀疏奖励设定、老老实实堆 reward shaping 的时候看到了 HER 那篇经典论文。它的思路彻底改变了我看待“失败轨迹”的方式。1.2 “事后聪明”怎么变成训练信号HER 的核心思想用一个生活场景就能说清楚。你让一个新手司机把车停进正中间的车位他歪歪扭扭地停到了靠左的位置。如果只按“停正中间”来评价这是一次失败的练习没有任何正面反馈。但如果换一个视角这次练习中他其实完成了一次“把车停在靠左位置”的动作这个动作本身是成功且有效的。把这个“靠左位置”当作目标重新审视整段轨迹每一个转向、每一次刹车都变成了朝向这个目标的正确决策。HER 做的事就是把这个“事后视角”自动化。具体来说当智能体在一个回合中没能达成原始目标 goal 时它先把这一整条轨迹存进经验池同时额外做一步操作把这个回合最终达到的状态或者轨迹中途的某些状态当作一个“虚拟目标“ ghost goal 重新生成经验。在这个虚拟目标下那条原本失败的轨迹就变成了一个成功轨迹reward 全部重算为成功值。举个我项目里的实际例子。机械臂推方块原始目标是位置 A但智能体瞎推一通方块最后停在位置 B。常规回放时这整条轨迹是 50 个 -1。HER 处理时会额外生成一条新经验把目标改成 B由于方块确实停在 B回合结束那一帧的 reward 变成 0前面的每一步虽然还是 -1但至少整个 episode 的终止状态变成了“成功终止”。再配合接下来要讲的多目标 Q 函数这些“虚假但合理”的成功经验能缓慢地教会智能体要达成任意目标位置需要怎么移动方块。关键点在于目标重标记不是简单地伪造数据它严格保留了真实的转移过程——状态、动作、下一状态都是环境里真实发生的只是修改了“我们要去哪里”这个条件。用更学术的话说它挖掘了每条轨迹里隐含的“可达状态信息”把原本稀疏的监督信号变成了稠密的目标条件信号。2. 核心原理拆解HER 怎么把失败变成高质量训练数据2.1 目标重标记的四种采样姿势HER 论文里给了四种从轨迹中挑选“虚拟目标”的策略这直接决定了重标记数据的质量。我一个个说顺便讲讲实测感受。final直接把整段轨迹的最终状态当作虚拟目标。最简单也最保守计算量最小。我一开始就是先跑通这个方案再换别的。future在当前时刻 t 之后随机挑一个状态作为虚拟目标论文里建议通常挑轨迹后半段的某个状态。这个策略在多阶段任务里特别明显因为“目标”不是静态的智能体可能在中途短暂达到了一个有价值的位置但在那之后又跑偏了。episode从整段轨迹的所有状态里随机挑一个状态当目标。覆盖度比 final 高很多因为轨迹中间可能有很多有价值的状态。random从所有历史轨迹的状态里随机选目标。这个其实和 episode 很像但跨回合采样数据量大了之后效果接近。我自己在 FetchReach 和 FetchPush 上都试过直接给结论reward 极稀疏时future 和 episode 表现明显优于 final当任务只有“到位即成功”这种二值奖励时final 勉强可用但训练很慢。future 策略的 k 值采几个候选状态也很关键k 太小覆盖不够k 太大噪声增加。我最后常用 k4对应原论文的默认参数。2.2 多目标 Q 函数为什么能撑住这套逻辑光有目标重标记还不够HER 能真正跑起来靠的是把网络结构改成“多目标条件化”。传统 DDPG 或 SAC 的 Q 网络输入是 (state, action)输出是一个标量 Q 值HER 的 Q 网络输入是 (state, action, goal)输出还是标量 Q 值但这个 Q 值是在“给定目标 goal”条件下的预期回报。这个改动看起来很轻量但意义很深远。因为目标变成了输入条件同一个 transition (s, a, s) 在不同 goal 下可以有不同的 label。比如机械臂把方块从位置 B 推到了位置 C如果 goal 是 D这个 transition 可能只是稀疏奖励里的 -1但如果 goal 设成 C它就成了一个正样本。智能体在训练时见过大量这种“不同目标下的同一转移”它学到的 Q 函数就能泛化到未见过的目标上。更直白地说如果没有多目标条件化那些重标记出来的所谓“成功经验”只能教会智能体到达某个特定状态对原始目标毫无帮助。而有了 goal 条件化网络能提取出“如何把方块从任意位置移动到指定位置”的通用策略。这就像你练投篮虽然每次实际投进的位置不同但你慢慢学会了“把球送到那个位置”的发力感觉而不是只会投某一个点。2.3 一个具体例子推箱子任务怎么被 HER 救活的我把这个逻辑在 FetchPush 环境里完整跑通过一次整个流程是这样的。环境里机械臂末端要推动一个方块目标是方块到达随机抽样的目标位置。观察空间里包含了机械臂末端位置、方块位置以及目标位置。每个 episode 采样一个新的 goal。不用 HER 时我训练了 80 万步成功率一直是 0一块平坦的曲线。用了 HERepisode 策略之后前 10 万步仍然没动静但 30 万步左右开始出现零星成功到 80 万步成功率爬到 60% 以上。这个“前几十万步没动静”的阶段特别容易劝退人但其实网络在疯狂学习状态转移和目标表示只是成功率这个指标还测不出来。我还试过把 HER 的虚拟目标比例调低只对 30% 的轨迹做重标记其他正常回放。成功率掉得不多但训练稳定性和方差改善了不少。这点后面调参部分我会细说。3. 实操落地从环境搭建到训练跑通3.1 环境与框架选型我做这个项目时用的是 gymnasium 里的 FetchPush-v1 环境旧版的 gym 里叫 FetchPush-v1新版改名叫 FetchPush-v1 但 API 略有调整。这个环境基于 MuJoCo 物理引擎模拟一个七自由度机械臂推方块的任务自带一个稀疏奖励接口。这里有个大坑Fetch 系列环境的 reward 默认长这样——如果方块到了目标点附近reward0否则 reward-1。这个“-1/0”的设定是很多新手没搞明白的它其实已经是稀疏奖励了直接用就行不要额外加惩罚项。算法骨架我选了 DDPG。原因是 HER 和 off-policy、确定性策略算法搭配最成熟DDPG 的代码结构简单方便调试SAC 也能配 HER但温度系数的调节会增加变量对新手不友好。依赖项很简单Python 3.9、PyTorch 2.0、gymnasium、mujoco。安装 MuJoCo 时注意新版 mujoco 库里自带渲染和物理引擎只需要 pip install mujoco 就行。如果装的是旧版 mujoco-py那会踩一堆编译环境的坑建议直接上新版。3.2 关键代码实现与参数选择网络结构没有什么神秘的actor 和 critic 都是简单的三层 MLP256-256-256激活函数 ReLU。critic 输入是把 state 和 goal 拼接起来注意不是 state goal action 三者拼完再进网络而是在第一层之后才把 action 拼进去这是 DDPG 的标准做法我这里为了简洁直接全部拼在一起送进网络效果也没有太大差别。目标重标记的代码是 HER 的核心我单独拎出来讲。假设一整个 episode 的轨迹是episode_transitions [] # 每个元素是 (obs, action, reward, next_obs, done) final_state episode_transitions[-1].next_obs[achieved_goal] # 实际到达的状态重标记一个虚拟目标很简单new_goal final_state.copy() for transition in episode_transitions: obs transition.obs obs[goal] new_goal # 替换目标 next_obs transition.next_obs next_obs[goal] new_goal reward compute_reward(next_obs[achieved_goal], new_goal, None) # 重算稀疏奖励 new_transition (obs, transition.action, reward, next_obs, True) replay_buffer.add(new_transition)注意这里每个 transition 的 done 我直接设成了 True这样会让智能体认为该回合在每步都成功了。理论上这是 HER 实现里的小技巧能加速学习。完整训练循环的核心部分是采样和更新for _ in range(gradient_steps): batch replay_buffer.sample(batch_size) obs, action, reward, next_obs, done batch # 计算 target Q with torch.no_grad(): next_action actor_target(next_obs) target_Q critic_target(next_obs, next_action) target_Q reward gamma * (1 - done) * target_Q # 更新 critic current_Q critic(obs, action) critic_loss F.mse_loss(current_Q, target_Q) critic_optimizer.zero_grad() critic_loss.backward() critic_optimizer.step() # 更新 actor actor_loss -critic(obs, actor(obs)).mean() actor_optimizer.zero_grad() actor_loss.backward() actor_optimizer.step() # 软更新 target 网络 soft_update(actor_target, actor, tau) soft_update(critic_target, critic, tau)这里有个细节obs 里已经包含了 goal所以不需要额外处理。reward 的 shape 如果是一维张量注意变量维度和广播机制。超参数我整理成了一张表这些数值是基于我实验调出来的可以直接抄作业参数取值说明replay buffer 大小1,000,000必须大HER 会生成额外样本buffer 小容易覆盖旧经验batch size256大 batch 对多目标 Q 函数更稳定actor / critic 学习率1e-3用 Adam如果训练发散就降到 3e-4gamma0.98稀疏奖励下短期回报更重要别用太接近 1 的值tau软更新系数0.05比默认 0.005 大一些让 target 网络跟得快动作噪声0.2 高斯噪声用于探索训练后期可以衰减到 0.1HER 策略future, k4每回合额外采 4 个虚拟目标每回合重标记比例100% 重标记生成的新经验全部存入 buffer3.3 训练流程与资源预估我建议的完整流程是先跑通 FetchReach一维目标只有机械臂末端移动特别简单确认代码没有 bug再上 FetchPush。FetchReach 在 50 万步内就能快速收敛跑一次大约半小时非常适合做冒烟测试。FetchPush 的训练我实测单张 2080Ti 大概每秒能跑 30~40 个 episode每个 episode 50 步也就是每秒 1500~2000 步。跑 80 万步大约需要 8~10 小时。如果显存不够或者 CPU 瓶颈可以把 batch size 降到 128训练时长差不多。训练过程中的关键监控指标不是 loss而是两个东西每 10 个 episode 计算一次的成功率以及 buffer 里虚拟目标样本占比。成功率曲线是锯齿状上升的前期会有很长时间停滞在 0这个阶段千万别急着重启训练。我自己的记录是 FetchPush 上第 25 万步左右才出现第一个成功 episode如果你 100 万步还是 0那就要考虑是不是哪里写错了。4. 效果对比与超参数调优记录4.1 对比实验设计为了验证 HER 的实际效果我设计了四组对比全部在 FetchPush 上跑 80 万步其他训练条件保持一致。方案奖励设置是否用 HER最终成功率基线 1稀疏奖励 -1/0否0%基线 2稠密奖励距离惩罚否41%HER-final稀疏奖励 -1/0final 策略18%HER-episode稀疏奖励 -1/0episode 策略68%HER-future(k4)稀疏奖励 -1/0future 策略74%这个数据已经能说明问题了。稀疏奖励下没有 HER训练完全失效稠密奖励虽然可以用但只有 41% 的成功率而且我为了调那个距离惩罚系数折腾掉了不少时间。HER 直接把稀疏奖励任务的成绩拉到了接近 75%而且没有引入任何手工设计的 reward 函数。这里有个容易误解的地方HER 并不是说它比稠密奖励更好而是它用更少的先验知识达到了相当甚至更高的效果。如果任务允许你设计出完美的稠密 reward那 HER 的优势就不明显了。4.2 训练曲线怎么读HER 的训练曲线有个典型特征成功率迟迟不动然后突然开始抖动上升。这是我见过最多新手误判的地方——看到前 20 万步成功率还是 0 就关掉训练实际上网络可能在积累有效的状态转移信息。我截取了自己一次训练的分段记录0~10 万步成功率恒为 010~20 万步偶尔出现 3%~5% 的成功率尖峰20~30 万步缓慢爬升30~50 万步开始快速上升55 万步之后曲线进入震荡平台最高到过 80% 左右。训练日志里如果看到这种形态说明 HER 起效了放心继续跑。另一个值得关注的指标是 critic loss 和 buffer 里“虚拟成功样本”占比的比值。如果虚拟样本占比很高但 loss 降不下去说明 Q 网络在同时拟合很多矛盾的目标这时候可以考虑降低重标记比例或者增大网络容量。4.3 我自己调过的几个关键旋钮参数调优过程中有几个点是我反复踩了坑之后才摸清楚的。第一个是动作噪声的衰减速度。噪声太大轨迹质量差重标记出来的虚拟目标也五花八门智能体难以学到稳定的策略噪声太小整个 buffer 里的轨迹都长一个样探索不足。我的经验是开始设 0.3前 30 万步保持之后每 10 万步衰减 0.05最低到 0.1 就不再降了。第二个是 HER 重标记的比例。原论文默认每条轨迹都做重标记但我试过把比例降到 0.5成功率只掉了 5 个百分点但训练稳定性明显变好Q 值的震荡幅度小了很多。原因是 100% 重标记会让 buffer 里“成功样本”占比过高智能体对真实目标的分布感知被扭曲了。如果你发现训练后期反复在某个成功率附近横跳可以试试这个旋钮。第三个是目标采样的范围。future 策略要从“当前时刻之后”的状态里采样但如果轨迹很长后半段状态可能已经偏离了当前探索方向反而变成噪声。我试过把采样范围限制在“当前时刻之后 20 步以内”效果稍微好一点但也没有质变。5. 常见问题与踩坑实录5.1 训练完全不动先查这三件事如果你跑 HER 100 万步成功率还是 0大概率不是算法问题而是实现细节出了岔子。我排查顺序是固定的按顺序检查基本能定位。第一条是 reward 函数有没有真的读到虚拟目标。最常犯的错是在重标记时只改了 obs没改 next_obs 里的 goal 字段导致 transition 的 reward 计算用的还是原始目标。这个 bug 会让网络学到“无论目标是什么反正我都没成功”的错误映射。第二条是 replay buffer 里是不是真的存了重标记后的样本。我调试时候在采样函数里加了一行打印随机抽一批数据看 goal 字段和新目标的欧氏距离是不是接近 0。如果距离很大说明重标记后的样本根本没进 buffer或者被后续代码覆盖了。第三条是环境返回的 done 标志。Fetch 环境在稀疏奖励下除非中间出现成功状态否则 done 一直是 False。但 HER 重标记后所有 transition 的 done 都应该强行设 True否则 Q 函数的自举bootstrapping会出问题导致价值永远传不出去。5.2 HER 和 DDPG、SAC 的搭配取舍我在项目里最开始试的是 DDPG后来也试过 SAC 加 HER。结论是SAC 加 HER 不是不行但要注意 SAC 的熵系数 alpha 在稀疏奖励环境里会自动调得比较小导致探索不足最好手动固定一个稍大的 alpha或者干脆用 DDPG。DDPG 的问题在于它对超参数敏感尤其是 actor 和 critic 学习率差一个数量级结果就天差地别。我建议固定 actor 学习率 1e-3critic 学习率 1e-3如果训练震荡就把 critic 降到 3e-4。这个组合在多个随机种子上都很稳。另外强烈建议跑 3 个不同随机种子取平均值来评估效果HER 本身方差比较大单次结果不能说明问题。5.3 几个常被忽略的小技巧最后分享几个我实测有效的小细节。一个是把 reward 从 -1/0 换成 0/1。这不是奖励缩放而是把“没成功”从负数改为 0成功改为 1。直观上看 loss 变化范围更友好而且配合 HER 的重标记成功样本的 reward1 在 Q 网络里更好传播。另一个是 latent space HER。如果原始状态空间特别高维比如图像输入直接拿像素当目标不现实可以先训练一个 encoder 把状态压缩成低维向量再在向量空间里做目标重标记。这是 HER 在大规模视觉任务里的常用变体我后来做真机实验时用了这个思路效果不错。还有一个是并行采样加速。Fetch 环境单线程采样太慢我后期用 Python multiprocessing 开了 8 个环境并行采样训练速度快了 4 倍左右。注意 reward 和 done 都要在子进程里正确返回否则数据会对不上。最后再说两句跑完这个项目之后我最深的体会是hindsight 这套思路打动我的不只是它在实验里涨了多少成功率而是它把“失败”这件原本只配丢进垃圾桶的数据重新变成了训练资源。我做强化学习这么多年越来越觉得很多问题不是模型不够强而是我们太执着于原始目标忽略了过程里已经产生的有效信息。真到实机上HER 也不是银弹它需要和领域知识、环境理解配合。但如果你正在跟稀疏奖励死磕先别急着设计复杂的 reward shaping试试这个“事后诸葛亮”的思路说不定能给你省下好几周的调参时间。
网站建设高端定制企业官网