我在调机械臂抓取任务时,最崩溃的不是写环境代码,而是给自己挖了一个大坑——把任务奖励设成“抓取成功才有 +1,否则 0”。模型跑了大半天,成功率一直趴在 1% 出头,回放池里堆满了失败的 transition,跟石头一样没有学习信号。后来我把hindsight(Hindsight Experience Replay,HER,后见之明经验回放)加进去,问题开始松动。这一篇就聊聊这个让智能体把“失败”也算作“经验财富”的算法:它到底是什么、为什么有效、落地的时候有哪些坑。
名字本身就很有哲学味。心理学里有个概念叫 hindsight bias,意思是“事后诸葛”——事情发生之后,人会觉得自己早就知道是这个结果。HER 干的事情完全一样:智能体没抓到目标,但事后回头看,它其实“到达”了另一个位置,那这段轨迹就不是废的,可以当成一次成功示范。这套思路在稀疏奖励场景下尤其能救命,适合所有做强化学习落地、做机器人操作、做路径规划的人参考。
1. 稀疏奖励问题:强化学习里的“迷宫出口”
1.1 为什么稀疏奖励能把模型饿死
先说说我为什么一开始会写出那种坑爹奖励。那是个典型的 goal-conditioned 任务:机械臂从任意位置出发,目标是推动某个方块到指定位置。环境给的是最朴素的设计——只有最终没碰到目标就返回 0,碰到才返回 1。
这种奖励函数在数学上很简单,但训练起来是灾难。原因是强化学习本质上靠奖励信号来引导策略,信号太稀疏,智能体绝大多数时间里获得的都是同一个常数 0,它根本分不清哪些动作稍好、哪些动作稍差。你可以想象一个学生在没有任何考试反馈的情况下复习,做完一百套模拟卷也不知道自己到底错在哪,最后只能瞎蒙。
具体到训练曲线上,表现就是:Q 值长时间不变化,actor 的梯度方向几乎纯随机,探索动作集中在原地抖动。我碰到过最夸张的情况是连续 80 万步成功率不到 2%,后来检查了一下,大部分采样到的 transition 的 reward 全是 0,回放池根本没有有效梯度。换句话说,不是模型笨,是它在沙漠里找不到水。
1.2 传统解决思路:Reward Shaping 为什么治标不治本
最早遇到这种情况,大家的第一反应都是“给奖励做一下塑形”(reward shaping)。比如根据机械臂末端和目标点的距离,给一个负的惩罚项,离得越远扣得越多,靠近了就加分。这个做法确实能让训练跑起来,我当时也试了,效果立竿见影,成功率从 2% 涨到 30% 左右。但问题也很明显:
- 距离函数需要人工设计,换一个物体、换一个任务就要重新调。
- 距离函数可能会诱导策略去“钻空子”,例如机械臂先走到一个折中点,让数值上更逼近目标,但物理上根本没在完成任务。
- 稀疏奖励的本质是“目标是否达成”,而塑形奖励改变的是“路径好坏”,两者一旦冲突,最终策略会变得特别拧巴。
还有一个更致命的地方:如果奖励塑形给得太强,模型会过度拟合到奖励表面,而不是真正理解“抓取”这个动作。换一个新的目标位置,成功率立刻掉回解放前。后来我意识到,问题的根源不是奖励函数不够密,而是我们没有合理利用“失败数据”里隐含的目标信息。HER 就是在这一点上动手的。
2. Hindsight 的核心思想:既然够不着目标,那就把目标挪近一点
2.1 事后诸葛:失败的经验也要有价值
先回到机械臂任务最底层的逻辑。一个 episode 的完整轨迹里,智能体从初始状态出发,尝试去接近目标 g,但因为策略很差,最后可能停在离目标很远的地方。常规经验回放算法里,这一整条轨迹都会被标记为“无价值”,因为每一步的 reward 都是 0。
但如果你把自己带入“事后视角”,你会发现这条轨迹实际上完整地展示了另一个过程:智能体从初始状态出发,到达了某个最终状态 s_final。如果我们把这些失败数据的目标从 g 改写成 s_final,那原本全是 0 的奖励序列就会变成一条“成功到达目标”的轨迹。这条轨迹包含真实的动作序列和环境反馈,只是之前被错误地套在了一个不适合的目标上。
这就是 HER 的关键一招:目标重标记(goal relabeling)。它不是像 reward shaping 那样去修改奖励函数本身的形状,而是保留原始的奖励计算逻辑,但在存储经验时,把一部分经验的“目标字段”替换成这条轨迹中实际到达过的状态,然后重新计算奖励。这样,原本稀疏到不行的奖励信号就被人为加密了,模型终于能从失败里学到东西。
2.2 目标重标记的正确姿势
目标重标记不是把所有 transition 盲目改成以最终状态为目标,而是要有选择地补充经验。最朴素的版本,就是每个 episode 结束之后,额外生成一条“虚拟的、以最终状态 g' 为目标”的经验序列,和原始经验一起放回回放池。同一条轨迹,在池子里就有了两种解释:一种是在挑战原始目标 g 时失败的经验,另一种是在达成虚拟目标 g' 时成功的经验。
举个具体例子。假设机械臂任务的目标是“把红色方块推到桌面右上角”,但这次实验里方块最终停在了左下角。原始经验记为:
- (state_t, action_t, reward=0, state_{t+1}, goal=右上角)
重标记之后,我们在回放池里额外放一条:
- (state_t, action_t, reward=1, state_{t+1}, goal=左下角)
关键是第二条里的 reward=1 不是骗出来的,而是“如果目标本来就是左下角,那么这个动作确实成功完成了任务”的真实反馈。模型同时看到大量类似数据后,会慢慢理解:怎么样的动作能够精准地击中一个目标位置,并把这个泛化能力迁移到原始目标上。
这里有一个非常容易被忽略的点:重标记目标的奖励计算,必须使用环境原始的 reward function,而不是重新设计一个。HER 没有引入任何额外的奖励先验,它的全部魔法只是改变了“我们让模型去学什么目标”,而不是“如何评价动作好坏”。这是它和 reward shaping 最本质的区别。
2.3 为什么 HER 能缓解稀疏奖励
用信息论的角度理解,稀疏奖励问题本质是“正样本太少”。HER 通过重标记,把几乎所有失败的 transition 都变成了某个目标下的成功样本。假设一个 episode 有 T 步,原本只有回到目标那一两步有正奖励;重标记后,整条轨迹都可以变成以最终状态为目标的正样本。
这条路一打通,模型就不再需要“瞎猫碰上死耗子”式的探索才能看到正奖励。它对状态-动作空间的覆盖效率会提升好几个量级。我自己的实验里,加了 HER 之后,机械臂推动任务大约 40 万步就能达到 70% 以上的成功率,而之前跑 200 万步还在个位数徘徊。差距就是这么大。
不过也要泼一盆冷水:HER 不是万能药。它解决的问题是“目标达成型任务中的稀疏奖励”,如果你面对的是没有明确目标的纯互动任务,比如对话、游戏通关,HER 并不能直接套用。它要求环境具备可描述的目标空间,并且能够从状态中抽取或构造目标。这也是我在项目选型时最先确认的事。
3. 算法实现细节:从伪代码到可运行
3.1 HER 训练流程全拆解
很多博客讲到 HER 只会讲“目标重标记”这个抽象概念,但落到代码上还是有一堆细节。我先给出一份我在项目里实际使用的伪代码,再一个一个解释关键节点。
# 伪代码:HER + off-policy 强化学习算法(如 DDPG/SAC) # 假设环境是 goal-conditioned def her_train(env, agent, replay_buffer, total_steps, her_strategy='future', future_k=4, her_ratio=1): state, goal = env.reset() # 返回当前状态和采样到的目标 for step in range(total_steps): action = agent.select_action(state, goal) # 策略输入包含状态+目标 next_state, reward, done, info = env.step(action) # 保存原始 transition replay_buffer.add(state, action, reward, next_state, done, goal) if done: # 一条 episode 结束,开始重标记 transitions = collect_episode_transitions() # 从策略中选择一个虚拟目标 g' if her_strategy == 'final': her_goal = transitions[-1].next_state elif her_strategy == 'future': # 从当前 transition 之后 K 个状态里随机选一个 her_goal = random.choice(future_states(transition, k=future_k)) elif her_strategy == 'episode': her_goal = random.choice(all_states_in_episode) for transition in transitions: her_reward = env.compute_reward(transition.next_state, her_goal, info) replay_buffer.add(transition.state, transition.action, her_reward, transition.next_state, done, her_goal) # 可选:每一原始 transition 加一条重标记数据 state, goal = env.reset() else: state = next_state这个流程里有几个容易出问题的地方。第一,goal的编码方式必须和策略输入对齐。我习惯把state和goal拼接成一个一维向量喂给网络,但还有一种是让网络分别编码两部分再融合,没有绝对标准。第二,重标记的done标签怎么处理?如果虚拟目标被“达成”了,理论上环境应该结束,但实际代码里不能直接给done=True,否则会把轨迹截断,导致训练不稳定。我一般直接把原始done复制过来,只在奖励上做文章。
3.2 关键模块与参数选择
HER 本身不是一个独立算法,它是一套经验生成策略,需要配合 off-policy 算法使用。论文里用的是 DDPG,后来的实践里 SAC、TD3 也都能套。选型时我建议优先考虑 SAC 或 TD3,因为 DDPG 对超参太敏感,尤其是探索噪声和网络初始化,调试周期特别长。
在模块层面,真正需要写代码的只有两个地方:目标采样函数和奖励计算函数。目标采样函数决定了重标记目标从哪来,奖励计算函数必须与环境的原始 reward 保持完全一致,否则重标记出的成功样本就是假的,模型会被误导。
参数上最值得关心的是her_ratio,即“每条原始 transition 额外生成多少条重标记 transition”。论文里常见的是 1:1,也就是一条原始经验配一条重标记经验。我在实操中试过 1:2 和 1:4,发现比例太高会让回放池里成功样本过多,策略会偏向虚拟目标而忽略原始目标,反而不利于泛化。1:1 在我的任务里最稳。
3.3 网络结构示意
先直接给一个示例网络结构,这是我在机械臂推方块任务上验证过的:
import torch import torch.nn as nn class Actor(nn.Module): def __init__(self, state_dim, goal_dim, action_dim, hidden=256): super().__init__() input_dim = state_dim + goal_dim self.net = nn.Sequential( nn.Linear(input_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, action_dim), nn.Tanh() # 假设动作归一化到 [-1, 1] ) def forward(self, state, goal): x = torch.cat([state, goal], dim=-1) return self.net(x)如果你把目标空间和状态空间做了特征化预处理,也可以在拼接前分别过一个小 encoder,这个不强制。重要的是在策略和价值网络的输入层把目标信息明显地区分出来,否则网络很难学到“同一个状态下,目标不同,动作不同”的映射关系。
还有个细节:经验回放池最好把state、goal、action、reward、next_state、done都拆开存,用字典形式也行,但千万别只存transition对象。因为 HER 重标记的时候需要反复组合不同目标,拆开存后写起来会省很多事。
4. 实操心得与调参记录
4.1 不同 goal 采样策略的对比:final、future、episode、random
HER 论文里提出了四种目标采样方式,我在实际项目里都跑过,区别非常大。简单给一个横向对比:
| 采样策略 | 做法 | 我的实际体感 | 适用场景 |
|---|---|---|---|
| final | 用 episode 最终状态作为虚拟目标 | 实现最简单,训练速度一般,胜在稳定 | 短 episode、目标近似可达的任务 |
| future | 从当前 transition 之后的 K 个状态里随机选一个 | 效果最好,训练速度和最终成功率都高 | 推荐默认选择 |
| episode | 从整个 episode 的任意状态里随机选一个 | 比 final 好,但不如 future | episode 较长、目标差异大的任务 |
| random | 从经验池里随机选一个状态 | 效果最差,基本等于乱标目标 | 不推荐 |
为什么future比final好?因为final只把最终状态当作虚拟目标,样本空间比较窄,而future把轨迹中后半段的各种中间状态都纳入候选,虚拟目标更多样,奖励信号更密集。但future也有一个坑:如果 K 值选得太大,虚拟目标可能离当前状态太远,导致一条原本“正在接近目标”的轨迹被错误重标成“已经失败”,反而干扰学习。我在 50 步长度的任务里用 K=4~8 比较合适,K 不建议超过 episode 长度的一半。
4.2 经验池怎么设计才不会拖垮训练
HER 会翻倍甚至翻三倍地增加回放池里的数据量,经验池的大小要提前规划。我以前用过 100 万条容量的池子,加上重标记数据后,很快就被填满了,旧的高质量经验被挤掉。后来我把池子扩到 500 万条,训练速度虽然慢了一些,但成功率比之前高了将近 10 个百分点。
经验池的采样策略也值得注意。我习惯给重标记数据打个特殊标记,在采样时按比例混合:原始数据和重标记数据各占一半 batch。如果让 batch 里全是重标记数据,模型会过度关注虚拟目标分布,导致在真实目标上的表现漂移。这个小细节在论文里没有明说,是我自己在对比实验里发现的。
另外,每一条原始 transition 生成重标记数据时,我建议在生成后立刻放入回放池,不要等一个 episode 全部存完再重标。因为 episode 数据可能很大,一次性做矩阵操作虽然快,但内存占用会暴涨。按 transition 流式处理,代码写起来更灵活。
4.3 我在训练机器人臂任务时踩过的坑
第一个大坑是目标归一化。机械臂任务的 state 是关节角度和末端坐标混合单位,数值范围从 -3 到 3 不等,goal 又是另一个量纲。我一开始直接把原始数值拼进网络,导致 loss 在NaN附近横跳,训练根本稳不住。后来把所有 state 和 goal 都做成了归一化,统一放到 [-1, 1] 区间,问题才消失。这个在论文里很少强调,但在工程上是绕不过去的。
第二个坑是虚拟目标的可达性。HER 保证虚拟目标一定来自轨迹中的真实状态,所以“可达”没问题,但要注意虚拟目标对应的 reward 是否合理。如果你的 reward function 不是简单的距离阈值,而是带权重的复合指标,重标记后计算出的虚拟 reward 可能会和虚拟目标不匹配。检查方法很简单:随机抽 100 条重标记数据,打印 reward 和对应目标,人工看看是否符合直觉。
第三个坑是critic 对虚拟目标的过拟合。我观察过一段时间,模型在训练后期会出现一种症状:critic 对虚拟目标的 Q 值普遍偏高,但对真实目标的 Q 值偏低,结果 actor 的策略偏向虚拟目标方向。解决办法是每隔固定步数在“真实目标分布”上做一次验证,或者按 9:1 的比例在 batch 里保留少量“纯原始目标数据”,不给重标记数据。
5. 常见问题与排查技巧实录
5.1 问题速查表
下面这些问题都是我在 HER 落地过程中真实遇到过的,整理成一张速查表,方便大家直接对照。
| 现象 | 可能原因 | 解决思路 |
|---|---|---|
| 训练 loss 不降,成功率长时间为 0 | 重标记比例过低,或 reward 阈值不合适 | 提高 her_ratio,检查 reward 计算 |
| 策略只会在原地绕圈 | 探索噪声太大,或目标采样策略太随机 | 降低动作噪声,改用 future 策略 |
| 训练初期效果很好,后期变差 | 回放池旧数据被冲掉,或过拟合到虚拟目标 | 扩大回放池,加入原始目标数据混合采样 |
| 模型对训练目标表现好,但对新目标泛化差 | 目标编码太原始,缺少特征抽象 | 归一化目标,考虑用辅助特征编码 |
| 训练时 loss 震荡特别剧烈 | DDPG 对超参敏感,或网络层数太深 | 换 SAC/TD3,降低学习率并加 layer normalization |
| 重标记数据过多,训练变慢 | her_ratio 太高 | 回退到 1:1,或者让重标记数据只占 batch 的一半 |
5.2 几个排查技巧
如果你在跑自己的 HER 实验,我建议每一步都留好可复现的验证脚本。最简单的验证方式是:固定一个初始状态,手动指定一个目标,然后看策略是否能够从任意状态逐步逼近。不需要跑完整训练,几十步就能判断出目标编码、奖励阈值、网络结构是否合理。
还有一个非常实用的技巧:从单目标开始调参。我先关掉随机目标采样,让环境每次 reset 都给出同一个固定目标,调通整个训练流程后,再放开多目标。单目标训练时如果成功率都上不去,说明问题不在 HER,而在基础 RL 算法、网络结构或者奖励函数。等单目标跑通了,再加 HER 和多目标,调试效率会高很多。
最后分享一个我自己的独门经验:HER 重标记后得到的“成功轨迹”,不要只用来更新策略,也可以用来做专家示范预训练。具体做法是先收集一批随机策略的轨迹,重标记成“成功经验”,用行为克隆或者类似的监督方式预训练 actor,再进入强化学习阶段。这个 trick 在水下机器人路径规划项目里帮我省了大约 30% 的训练时间,尤其是在二指机械爪这类高自由度任务中,效果比从零开始强化学习明显更快。
PR