搞强化学习的人十有八九都经历过这种时刻:精心设计了环境,让智能体从零开始学一个任务,结果跑了大半天,奖励曲线纹丝不动。不是代码写错了,而是奖励太稀疏——智能体在探索阶段连一次“成功”都没碰过,梯度回传根本找不到方向。我自己在做机械臂推物体任务时就在这个坑里卡了快两周,直到把hindsight(后见经验回放,HER)的机制彻底吃透,才真正绕开了这个死胡同。
hindsight 这个词的直觉很简单:人类回忆过去时总爱说“要是当初那样做就好了”。HER 把这套事后总结的思路搬进了强化学习——既然智能体这次没达到预设目标,那就把它实际达到的状态当作一个新目标,重新审视刚才走过的轨迹。这样原本被判定为“失败”的经验也能变成学习信号,梯度更新就有了方向。我最初是在 OpenAI 那篇同名论文里接触到这个概念的,后来又在好几个机器人控制任务里实际跑通了,今天就把从原理到实装、从调参到避坑的完整流程一次性摊开讲清楚。
这篇文章适合两类人:一类是想搞清楚 HER 为什么能解决稀疏奖励问题、正准备把它接进自己 RL 项目里的工程师;另一类是已经在 DDPG、SAC 上加了 HER 却迟迟不出效果、怀疑自己实现有 bug 的同学。这里头的很多细节,论文里不会写,代码注释里也没有,但恰恰是决定成败的关键。
1. 为什么需要“事后聪明”:稀疏奖励下的学习困境
1.1 稀疏奖励到底难在哪
强化学习的经典套路是智能体和环境交互,靠奖励信号调整策略。入门教程里的奖励设计通常很密集,比如“离目标越近分数越高”“每走一步都有反馈”,让智能体像走楼梯一样一步一步往上爬。但真实任务很少这么友好:机器人抓取、物体搬运、零件拼装,这些场景没法在每个时刻都给出有意义的密集反馈,经常只能在一整段操作结束后告诉你“成没成”。这就构成了典型的稀疏奖励环境。
在稀疏奖励环境里,智能体初始策略基本是随机探索。拿机械臂推物体举例,如果目标位置在桌面另一端,随机策略在几十万次交互里都很难碰巧把物体推到目标附近。于是每条轨迹拿到的奖励都是同一个负常数或者零,策略梯度算出来,所有动作的优劣完全无法区分,训练直接陷入死水。这也是为什么很多 RL 项目在真实物理环境中跑不起来——实验室里密集奖励的场景勉强能工作,一换到真实操作就彻底失灵。
1.2 常规补救方案的局限性
遇到稀疏奖励,大多数人第一反应是做奖励塑形(reward shaping),就是人为构造一个密集奖励函数。这确实能加快早期学习,但坑非常多:设计不当会引入局部最优,智能体很快就学会钻空子,比如绕着目标转圈刷分,就是不学任务本身。另一个思路是课程学习(curriculum learning),把任务从简单到复杂排序,让智能体先学容易的再逐步加大难度。这个方法思路没问题,但每换一个任务都要重新编排课程表,人工干预量非常大。第三种方向是增强探索,比如把随机噪声调得更猛,或者引进好奇心机制,但很多稀疏任务就算探索得再充分,依然采不到一次成功状态的样本。
这些方案本质上都还困在“按预设目标给奖励”的大框架里。真正破局的角度是意识到:目标这个变量本身也可以被调整。HER 就是从这个角度切进去的。
1.3 HER 的聪明之处在哪里
HER 的灵感来自一个再普通不过的生活常识:失败不等于全无价值。在“事后”看来,智能体虽然没有到达预期目标,但它一定到达了某个状态。如果把这个实际到达的位置定义为新目标,刚才那条轨迹就变成了一条“成功轨迹”。这种重新解释历史经验的方式,像不像我们复盘项目时说“虽然没完成 A 方案,但顺手验证了 B 方案的可行性”?
从数学视角看,HER 做的事是在经验回放的存储环节里嵌入了一次目标重标注。原来的经验回放存的是 (状态、动作、奖励、下一状态、目标) 五元组,其中目标是预固定的;HER 会额外构造若干个替代目标,并以此重新计算奖励,把一批“失败经验”刷新成“成功经验”再放回缓冲区。这样一来,即使真实总目标一次都没达成,智能体也能从大量虚拟成功中学会“什么动作组合会导致什么状态变化”,策略网络和价值网络的训练信号一下子就充实起来了。
2. HER 核心原理:目标重标注机制深度拆解
2.1 从一条轨迹看重标注怎么做
假设环境是带目标设定的,按照 Gym 里 GoalEnv 的接口,每个状态被拆成三部分:observation 是观测主体;achieved_goal 是实际达到的目标描述;desired_goal 是期望完成的目标描述。以机械臂抓取为例,observation 是关节角度和物体位置,achieved_goal 是当前物体的位置或抓取状态,desired_goal 是物体要送达的目标位置。
智能体执行一条长度为 T 的轨迹,全程都朝着 desired_goal = g 去探索。到轨迹结束时,物体停在位置 g_actual,并没有到达 g,于是这条轨迹里所有 transition 的奖励都是 -1(或 0,取决于指示函数怎么定义)。按传统经验回放处理,这些样本的价值标签全一样,策略更新的梯度被偶然噪声主导,完全学不到东西。
HER 做的事情是:从这条轨迹里挑出一个未来时间步,比如第 k 步,把这一步时物体的位置 g_k 作为新的 desired_goal,然后把整条轨迹重新整理成“以 g_k 为目标”的经验。因为轨迹后来确实到达过 g_k,所以从第 k 步往后的 transition 在新目标下就是成功的,奖励立刻从 -1 变成 0。这些重标注样本进入缓冲区之后,训练算法就能看到大量“目标可达”的正样本。
这里有个关键细节:重标注不是整条轨迹一股脑替换目标,而是逐条 transition 处理的。原始目标版本保留,每个 transition 额外生成若干替代目标版本,所以经验池的存储量大约是原来的 K+1 倍。
2.2 四种重标注策略的取舍
论文和开源实现里通常能见到四种目标重标注策略,我逐个说下实际感受。
- final:取轨迹最后一个状态作为新目标。逻辑最简单,但对长轨迹来说,末尾状态可能离轨迹前段太远,只有后半段的 transition 能获得正信号。
- future:从当前时间步之后的状态里随机挑一个作为新目标。这是论文推荐的主力方案,因为它保留了一个重要的因果属性——新目标是在当前时刻之后真实出现的,所以“当前动作导致后续某个状态”这条因果链是成立的。
- episode:从整条轨迹任意位置随机挑一个状态作为新目标。覆盖范围最大,但可能挑到轨迹开头、与当前动作毫无因果关联的状态,训练时奖励信号容易抖动。
- random:从其他轨迹里随机挑状态作目标。几乎没人用,因为跨轨迹的因果彻底断裂,对学习没有帮助。
我自己的实测经验是:future 策略在绝大多数任务上最稳,final 作为后备方案,episode 偶尔在离散状态任务里有点作用。新上手的话,直接默认 future 就行。
2.3 为什么 future 策略最稳
future 策略的稳定性来自它对待因果关系的克制态度。强化学习学的是条件分布:在某个状态做出某个动作之后,未来可能转移到哪些状态。如果新目标是从当前时刻之后真实发生过的状态里采样出来的,那这个转移关系至少真实发生过,Q 网络在学习时看到的状态-动作-目标三元组,符合数据分布的一致性。
反过来如果乱用 random 策略,新目标与当前状态完全没有关联,Q 网络要去拟合“从这个状态做这个动作就能达到那个完全无关的目标”,这种拟合需要极长的震荡才能收敛,很多时候根本收敛不了。所以 HER 并不是无限制地捏造成功,而是有节制地在真实转移过的状态上做文章,这个“节制”正是它能稳定工作的核心原因。
2.4 和 off-policy 算法的适配逻辑
HER 本身不是一个独立的强化学习算法,它是一个经验增强模块,需要挂在 off-policy 算法上使用,比如 DQN、DDPG、TD3、SAC 这类带经验回放的算法。原因很直接:HER 要求大量重标注样本被反复取用,而 on-policy 算法每次更新完策略就丢弃旧经验,根本没有机会对同一批数据做目标扰动。
HER 对 off-policy 方法的价值,可以从 Q 网络的角度来理解。Q 网络要评估“某个动作在某个状态下能带来多大收益”。重标注等于给同一个 transition 分配了一个新的虚拟目标,这迫使 Q 网络理解“状态转移本身和目标无关,但成功与否取决于目标”。训出来的价值函数自然具备了跨目标泛化的能力。这个性质在机器人操作和多目标任务里特别珍贵,相当于智能体学会的不是某一条特定轨迹,而是“如何把任意状态推向任意目标状态”的抽象技能。
3. 从零实现 HER:代码结构与实操细节
3.1 环境与工具链准备
我实际跑过的组合是 Python 3.8、PyTorch 1.10,强化学习部分用自定义实现,配合 Gym 的 GoalEnv 接口。如果你手头已经有 DDPG 或 SAC 代码,把它改成 HER 版本比从零搭框架省力得多。
基础依赖是 numpy、torch、gym、tqdm,跑机器人仿真还得装 mujoco-py 或 pybullet。强烈建议先拿 Bit Flipping 环境来验证流程。这是个比特翻转游戏:状态是长度为 n 的二进制向量,目标也是同长度的二进制向量,智能体每次翻转一位,当前向量与目标完全一致时奖励为 0,否则奖励为 -1。它把稀疏奖励的所有特征都浓缩在一个极简环境里,又不需要物理引擎,跑起来飞快,调试体验非常好。
3.2 数据结构必须分清楚
HER 要求环境把状态显式拆成 observation、achieved_goal、desired_goal 三个字段。在 Gym 的 GoalEnv 里,reset 函数和 compute_reward 函数天然提供了这套接口。构造 transition 时,除了常规的 (obs, action, reward, next_obs, done),还必须额外记录最初的 desired_goal 和 next_obs 中的 achieved_goal,因为重标注时要用 achieved_goal 替换 desired_goal,并重新算奖励。
我在这个地方踩过一个大坑:把 achieved_goal 和 desired_goal 存成了同一个张量,导致重标注时根本分不清“原目标”和“实际目标”,HER 等于完全失效。建议用字典或命名元组来组织数据,别图省事简化结构。
3.3 重标注与经验存储的核心代码
HER 有两种实现时机:一种是在轨迹结束后整条处理,另一种是采样过程中逐条处理。我强烈推荐前者。整条轨迹摆在那里,future 策略需要的未来状态随取随用,逻辑特别直观。下面是 future 策略处理一条轨迹的核心伪代码:
def relabel_episode(transitions, her_k=4): """ transitions: 长度为 T 的列表,每个元素是 (obs, achieved_goal, desired_goal, action, reward, next_obs, done) 返回加入重标注样本后的批量 transition """ new_transitions = [] T = len(transitions) for t in range(T): obs, ag, dg, action, reward, next_obs, done = transitions[t] # 始终保留原始目标版本 new_transitions.append(transitions[t]) # 额外生成 her_k 个重标注版本 for _ in range(her_k): # future 策略:从 t+1 到 T-1 随机选一个未来时刻 if t == T - 1: future_t = T - 1 else: future_t = np.random.randint(t + 1, T) new_ag = transitions[future_t][1] # 未来时刻的 achieved_goal # 以新目标重新计算奖励和 done new_reward, new_done = compute_reward_after_goal(new_ag) new_transitions.append({ "obs": obs, "achieved_goal": ag, "desired_goal": new_ag, "action": action, "reward": new_reward, "next_obs": next_obs, "done": new_done, }) return new_transitions这段代码里最重要的一行是new_ag = transitions[future_t][1],它把未来某个真实状态拿来做新目标。而compute_reward_after_goal的实现通常非常简洁:如果当前 achieved_goal 与新目标一致,奖励为 0,否则为 -1。别小看这个简单的函数,它出问题的概率远超你的想象,后面会专门展开讲。
3.4 超参数配置与训练流程
HER 最关键的额外超参数是 her_k,也就是每条 transition 额外生成的重标注样本数量。论文默认 4,我实测下来 4 到 8 都合理。设太大,经验池里虚拟成功比例过高,会干扰对真实失败的判断;设太小,增益不明显。建议从 4 起步,训练曲线稳定后可以试着调大看看有没有收益。
完整训练流程大概是这样的:
- 初始化策略网络、Q 网络以及各自的 target 网络,设置一个容量充足的 replay buffer。我建议至少 100 万条,容量不够后面会说有多难受。
- 每轮 episode 开始前随机采样一个 desired_goal,重置环境,记录初始状态。
- 智能体执行动作,采集完整轨迹,实时把原始 transition 临时存到列表。
- 轨迹结束后调用 relabel_episode,把原始样本和重标注样本一起写入 replay buffer。
- 从 buffer 采样一个 batch,更新 Q 网络和策略网络。注意 batch 里混着原始目标和重标注目标,这是一个混合学习的过程。
- 定时软更新 target 网络。
如果一切正常,在 Bit Flipping 上,普通 DDPG 几万步成功率可能还是 0,而 HER+DDPG 几千步就能看到成功率往上走。我在机械臂任务上,同样有限时间预算里只有 HER 版本训练成功,纯 DDPG 几乎完全没动静。这并不代表 DDPG 不行,而是稀疏奖励下传统经验回放根本喂不动 Q 网络。
3.5 观察指标与效果验证
除了成功率,强烈建议额外记录平均到达距离,也就是实际状态和目标状态的差距。成功率指标在早期很长一段时间都是 0,看起来像什么都没发生,而平均到达距离是连续值,能更早地反映策略是否在向目标靠近。我在多个任务里都观察到相同的规律:先看到到达距离开始下降,再过一段训练成功率才真正升起来。
训练结束后,还有一个很好的验证手段:不要急着清空 buffer,拿 buffer 里那些被重标注过的样本做一次离线策略评估。你会看到,即便轨迹原本全是失败的,重标注后的 Q 值分布依然能给出有效的价值信号。这个诊断方法在切换新任务时特别有用,能快速告诉你问题出在环境、HER 还是底层算法。
4. 我踩过的 HER 坑:常见问题与排查技巧
4.1 重标注后的奖励函数不正确
最常见的坑出在 compute_reward 上。很多人直接调用环境的compute_reward(achieved_goal, desired_goal)计算新目标下的奖励,却忘了 done 信号也需要同步更新。如果只是改了 reward 没改 done,智能体可能在同一段轨迹里连续多次触发“成功结束”,训练瞬间崩溃。排查方法是写一个单元测试,专门验证重标注后的 transition 里 reward 和 done 是否逻辑一致。
另外,奖励尺度很关键。HER 内置的稀疏奖励通常是 -1/0,如果给“新目标达成”分配过大的正奖励,Q 网络的价值尺度会漂移;设太小又区分度不够。我建议一开始坚持用 -1/0 的二元指示函数,不要叠加距离惩罚。距离惩罚会让 HER 偏离“纯稀疏奖励解决方案”的定位,还容易和重标注逻辑互相打架。
4.2 目标维度与观测维度不对齐
HER 要求 achieved_goal 和 desired_goal 必须在同一个表征空间里。这个空间可以是一段坐标、一组关节角、一个抓取状态,关键是两者维度必须完全一致,否则 compute_reward 无法判断“是否到达”。我在做一个末端执行器位置任务时,环境的 achieved_goal 是三维坐标,desired_goal 却不知怎么被写成了四维齐次坐标,维度对不上导致所有奖励恒为 -1,HER 形同虚设。
排查这类问题最简单的办法,是在训练前打印一条 transition 各字段的 shape,确认 achieved_goal 和 desired_goal 的维度一致。遇到不一致时优先改环境接口,别到模型里强行 reshape 糊弄过去。
4.3 探索策略太弱导致重标注目标过于集中
HER 是从失败经验里创造成功,但如果随机策略本身太弱,产生的轨迹很短且密集拥挤在起点附近,重标注出来的虚拟目标也都挨着初始位置,网络很容易学会“原地微调就算成功”,后期泛化能力很差。这种情况在真实机械臂上尤其明显,因为前期探索产生的轨迹往往被安全限制约束在很小的空间里。
我尝试过的解决办法是:先不急着上 HER,而是让策略用纯随机探索做一轮 warmup,采集一批覆盖度足够的数据之后再开启正式训练。这样做的稳定性比从零开始边探索边训练好得多,也避免了前期低质量的无效数据反复污染经验池。
4.4 经验池容量不足
HER 的本质代价是倍增存储:每条轨迹要多存 K 倍的样本。如果经验池容量卡在几十万条,前期某一批低质量数据很快占满整个缓冲区,后期真正有价值的高质量样本反而存不进来。我在一台 24G 内存的服务器上把 buffer 容量开到 200 万条毫无压力,所以除非运行环境极度受限,否则一步到位给够容量。
采样比例上,原始样本和重标注样本混在一个 batch 里,不需要刻意设计比例。只要你确认 relabel_episode 严格保留了原始版本,就不会出现“只学虚拟目标、忘了真实目标”的严重失衡。
4.5 训练震荡与冷启动失败
不少人第一次跑 HER 会看到成功率曲线呈锯齿状,刚升上去又掉回来。这种情况十有八九不是 HER 的问题,而是底层 off-policy 算法的 target 网络软更新系数太大,或者 batch size 太小。把软更新系数从 0.05 降到 0.005,batch size 从 64 提成 128 或 256,震荡通常立刻缓解。
如果训练从一开始就完全不动,先确认 replay buffer 里是否存在足够多的正样本,也就是重标注后 reward 为 0 的样本。最直接的验证方式是每隔 100 轮打印一次 buffer 中正负样本的比例。如果正样本占比长期为 0,说明重标注逻辑有 bug,或者探索范围太窄。这个检查点非常实用,建议直接写进日志模块。
5. 实装感受与后续扩展方向
我在实际项目里用 HER 解决过两个棘手的稀疏奖励问题,一个是多指机械臂的物体姿态调整,另一个是欠驱动小车的长距离导航。前者的观测空间很复杂,HER 让 Q 网络学会了在目标空间里做插值;后者因为探索覆盖不足卡了很久,最后靠 warmup 和加长轨迹长度解决了。这两段经历让我深刻体会到:HER 不是万能钥匙,它的价值在于把“失败经验变废为宝”这个思路系统化,真正的上限依然取决于你采到的数据质量和状态空间覆盖度。
顺便整理一个 HER 相对标准 RL 流程的改动对比,方便你评估接入成本。核心思想是:环境接口、transition 结构、buffer 写入口这三处是雷打不动必须改的,剩下的都可以复用已有算法。
| 组件 | 普通经验回放 | HER 版本 |
|---|---|---|
| transition 结构 | 状态、动作、奖励、下一状态 | 额外存放 achieved_goal、desired_goal |
| 经验写入 | 直接存储 | 轨迹结束后先重标注再批量写入 |
| 奖励计算 | 只需原始目标 | 同时保存原始目标和重标注目标两套奖励 |
| 目标空间 | 不需单独定义 | 必须显式定义 achieved_goal 与 desired_goal |
| 底层算法 | 不限 | 仅限 off-policy 算法 |
再分享一个值得尝试的扩展方向:把 HER 和 transformer 类结构结合。在重标注过程中,HER 天然会生成大量“目标-轨迹”对,这些数据非常适合做序列建模。我在一个抓取场景里试过用轻量级 transformer 替代原来的全连接 Q 网络,发现跨目标泛化能力又上了一个台阶。当然这只是实验性的方向,不代表所有任务都适用,但你如果已经在工程里跑通了基础版 HER,不妨把它当成一个有意思的试验田。