做强化学习这几年,我越来越觉得英文里那句“hindsight is 20/20”简直是对稀疏奖励任务最精准的注解。事后回看一切都很清晰,可问题在于,智能体当下根本不知道自己做对了什么、做错了什么。如果你训过机器人,大概率见过这个让人血压升高的场景:机械臂在桌子上面疯狂乱甩,抓了几百个回合,目标物体纹丝不动,训练曲线像条直线一样贴着零,怎么调学习率、换网络结构、加探索噪声,都看不到一点起色。这时候大概率不是代码写错了,而是你撞上了强化学习里最经典也最头疼的稀疏奖励问题。而解决这个问题,最值得先试的方案,就是标题里这个词——hindsight,具体来说就是 Hindsight Experience Replay(HER)。
HER 这个名字直译过来就是“后见之明经验回放”,它干的事也确实很“事后诸葛亮”:把一条没有达成原始目标的失败轨迹,重新标记成“达成了另一个目标”的成功轨迹,再塞回经验池里训练。听起来像作弊,但它在实验里的效果是实打实的——OpenAI 那篇论文用它在 19 个模拟机器人操作任务上都跑出了接近甚至超过人工设计密集奖励的效果。这篇博文我就围绕 hindsight 这个核心词,把 HER 从原理到工程实现、再到我实测踩过的坑,完整拆一遍。适合正在做机器人控制、导航规划、游戏 AI,或者对强化学习里的目标条件策略感兴趣的人参考。
1. 先搞清楚:HER到底解决的是强化学习里的什么病
1.1 一个会让算法当场崩溃的经典场景
想象这样一个任务:控制一只七自由度机械臂,把桌面上一个方块抓起来,放到托盘正中央。状态下有机械臂关节角、末端位置、物体位置,动作是各关节力矩或位置增量,环境只在“方块最终到达托盘中心、距离小于某个阈值”的时候返回奖励 1,其余每一步奖励都是 0。
这就是教科书级的稀疏奖励任务。整个 episode 可能有 50 步,动作空间是连续高维的。假设每步随机探索能“靠近目标”的概率只有百分之一,那一个回合内能碰到一次正奖励的概率也不算特别低,但注意,真正的问题不是“碰不到一次正样本”,而是“这一个正样本根本不足以让神经网络学会整条行为链”。拿抓取来举例:你要让机械臂完成“靠近——对准——抓取——抬起——移动——放下”这一整套动作,中间任何一环断了都拿不到奖励。随机动作下,这一整套链式行为能完整走通一次的几率,低到可以忽略。
结果就是,智能体绝大多数时间在收集奖励全为零的轨迹。梯度算出来要么全是零,要么被少数噪声样本主导,策略更新了相当于没更新,甚至是乱更新。说白了,强化学习的本质是靠奖励牵引的,没有奖励就没有方向。这就像让一个从没考过试的学生直接做一张满是超纲题的卷子,他连自己哪里错了都不知道,自然无从改进。
1.2 为什么“随机探索”在稀疏奖励下几乎必败
再往深处说一点。所有无模型强化学习算法,核心都是“试错”。试错没有问题,问题在于试错的成本。密集奖励任务里,你每走一步都能获得一个“偏了还是对了”的反馈,像导航时每走一段路都能看到路标;稀疏奖励任务里,路标只在终点出现,中途全是荒漠。
如果目标位置是一个精度极高的点,比如要求误差小于 1 毫米,那么随机策略在整个状态空间中“命中”这个点的概率基本为零。就算把成功率当成 0.01,一个 50 步的 episode 里至少命中一次的概率算出来大概是 1 - (0.99)^50 ≈ 39%,听起来不低。但你要注意,强化学习要学的不是“某一个瞬间碰巧正确”,而是“从任意状态出发都能稳定地走向目标”。一次偶然命中留下的轨迹,不足以让价值函数泛化到整个状态空间。实际工程里,很多任务的正样本率低到 1e-4 以下,这时候任何勉强能用的探索策略都是杯水车薪。
这跟监督学习有本质区别。监督学习每一条样本都自带标签,你不知道答案,但至少知道“应该往哪个方向修正”。稀疏奖励 RL 的大部分样本标签全是“0”,这不是“答错了”,而是“根本没有作答”。所以,怎么让那些零奖励的轨迹也变成学习信号,才是一切的突破口。
1.3 传统解法的困境:Reward Shaping真的够用吗
很多人第一反应是:不给密集奖励,我手写一个不就行了?比如“离目标越近奖励越大”。这就是奖励塑形(Reward Shaping)。它在很多任务里确实有效,但有几个绕不开的坑。
第一,人工设计密集奖励本质上是在注入先验知识。你怎么定义“接近”?用欧氏距离?那物体被挡住、绕路的时候,欧氏距离反而会误导策略。第二,奖励函数设计得不好,智能体一定会钻空子。我见过一个机械臂任务,为了让智能体靠近目标,把“距离减少”作为奖励,结果策略学到的是原地抖动手臂——因为小幅抖动会让距离传感器产生微小波动,智能体发现这样能稳定“刷”奖励,就不再前进了。第三,不同任务的奖励尺度差异极大,换个环境就得重新调。
另一个常见思路是课程学习:先让智能体学简单版本的任务,再逐步提高难度。但课程本身怎么设计、怎么判断“学会了”、怎么防止学新任务时忘掉旧任务,每一环都是额外工作量。HER 的聪明之处在于:它不引入人工先验,而是利用轨迹本身的结构,把“失败经验”重新定义成“成功经验”,在稀疏奖励条件下凭空造出学习信号。
2. Hindsight的核心思想:把失败重新标记成成功
2.1 从“事后聪明”到算法:一句话理解HER
HER 的核心操作,用一个句子就能说完:假设一个 episode 的原始目标 g 没有达成,但智能体在过程中实际到达了某个状态 g'(这个状态叫 achieved goal),那么把这条经历的“目标”改写为 g',并按照这个新目标重新计算奖励。
因为原始目标没达成,原本的奖励是 0;可一旦把目标临时换成智能体实际到达的那个状态,这个目标显然已经达成了,奖励就是 1。于是一条本来毫无反馈价值的失败轨迹,被改写成了一条成功轨迹,进入经验池参与训练。
拿射箭举例特别直观。你瞄准靶心射了一箭,没中,落在旁边一个点。教练走过来,不说你射偏了,而是说:“这次咱们的目标就是旁边那个点,你成功了。”下次再遇到类似局面,你的身体就知道用这套动作可以稳稳把箭送到那个位置。靶心距离很远,但“旁边那个点”总是能射中的,练多了,你对“如何把箭送到指定位置”的理解就越来越准,最终靶心也能射中。
2.2 一条轨迹的重标注全流程
具体到一条轨迹上,HER 是这样工作的。假设一个 episode 存储了 T 步经验,每一步形如 (s, a, r, s', g),其中 g 是原始期望目标,r 是通过奖励函数计算出来的。为了做 HER,我们额外记录每一步状态的 achieved_goal,通常它就是状态里的一部分,比如机械臂末端的位置坐标。
原本这条轨迹里,绝大多数 r 都是 0。现在我们遍历每一个时间步 t,以第 t 步的转移 (s_t, a_t, s_{t+1}) 为基础,从这条轨迹里挑一个“事后目标” g_new。这个 g_new 可以选轨迹终点的 achieved_goal,也可以选 t 之后某一步的 achieved_goal。然后重新计算奖励:
r_new = reward_func(s_{t+1} 的 achieved_goal, g_new)
由于 g_new 本来就是轨迹中实际到达过的状态,这个奖励几乎总是 1。我们把新生成的经验 (s_t, a_t, r_new, s_{t+1}, g_new) 存进经验池,和原始的 (s_t, a_t, r, s_{t+1}, g) 一起参与训练。
这里有一个新手最容易忽略、也是理解 HER 合法性的关键点:为什么“改写目标”不算是伪造数据?因为在一个目标条件马尔可夫决策过程(Goal-Conditioned MDP)里,状态转移概率 P(s'|s, a, g) = P(s'|s, a),目标是独立于物理动力学的。“从某个状态出发、执行某个动作、到达下一个状态”这件事,跟你“定的目标是啥”没有任何关系。所以给一条真实发生的轨迹换一个目标标签,并没有篡改物理过程,只是修改了一个条件变量。这就是“后见之明”真正合法的地方。
2.3 目标重采样的几种策略,为什么future最好
既然要选“事后目标”,那到底选哪个状态来做新目标?论文里对比过四种采样策略:
- final:直接用 episode 最后一个状态作为新目标。
- random:从整个轨迹的所有状态里随机抽一个。
- episode:从当前这个 episode 的所有状态里随机抽一个。
- future:只从当前时间步之后的状态里随机抽一个。
实验结果非常明确:future 效果最好。原因也容易理解:以第 t 步的转移为例,用“未来某时刻的状态”作为目标,意味着这条经验描述的是“从当前状态出发,向着未来的某个状态靠近”的过程,时间因果是顺的。如果拿过去的状态当目标,比如机械臂早就离开那个位置了,你现在让策略往“过去的位置”走,物理上说不通。future 策略实际上是借用了“序列的后续结果”来给行为提供正向标注,这跟课程学习里从易到难的思路一脉相承。
按照论文默认配置,每个原始 transition 会额外生成 k 条 future 重标注经验,k 通常取 4。也就是经验池里,原始经验与 HER 经验的比例为 1:4。不过这个比例不是一成不变的,后面调参部分我会细讲。
3. 工程落地:从核心公式到可复现的实现细节
3.1 算法选型:HER只能搭配Off-Policy算法
这一步很多人会栽跟头。Hindsight 的思想听起来很通用,但它在算法层面有一个硬约束:重标注出来的经验是“离线”的,也就是说这些经验并不是在当前策略下采集到的。所以只能搭配 off-policy 算法来用。
DQN、DDPG、TD3、SAC 都是 off-policy,可以用任意历史经验来更新当前网络,所以能直接叠 HER。REINFORCE、A2C、PPO 这类 on-policy 算法,更新时必须使用当前策略刚采集的轨迹,一旦把重标注经验混进去,策略分布和回报就错位了,训练直接崩。
所以实操推荐组合就三组:DDPG + HER(经典组合,论文默认)、SAC + HER(更稳定,探索更充分)、TD3 + HER(缓解过估计,适合连续控制)。我做实验的体感是:如果环境不复杂,DDPG 配 HER 已经足够;如果任务精度要求高、状态维度高,SAC 配 HER 更省心。
3.2 关键代码细节:经验池、目标重标注、奖励重算
下面这段代码是 HER 最核心的重标注逻辑,我把它单独拎出来讲,因为它覆盖了百分之八十的实现要点。
import numpy as np def her_relabel(episode_buffer, k=4, strategy='future', reward_func=None): """ episode_buffer: 一个episode的transition列表 每个元素为 dict,包含 obs, action, reward, next_obs 其中 obs 和 next_obs 是 dict,至少包含: observation: 原始状态特征 achieved_goal: 实际达到的目标状态 desired_goal: 期望目标状态 """ her_transitions = [] T = len(episode_buffer) for t in range(T): transition = episode_buffer[t] obs = transition['obs'] action = transition['action'] next_obs = transition['next_obs'] # 原始experience保留 original = ( obs['observation'], action, transition['reward'], next_obs['observation'], obs['desired_goal'], ) her_transitions.append(original) for _ in range(k): if strategy == 'future': # 只从 t 之后的状态里采样新目标 if t >= T - 1: break # 最后一个transition没有未来状态可用 future_idx = np.random.randint(t + 1, T) new_goal = episode_buffer[future_idx]['next_obs']['achieved_goal'] elif strategy == 'final': new_goal = episode_buffer[-1]['next_obs']['achieved_goal'] else: # episode / random random_idx = np.random.randint(0, T) new_goal = episode_buffer[random_idx]['next_obs']['achieved_goal'] # 用新goal重算reward,这是HER的命根子 new_reward = reward_func(next_obs['achieved_goal'], new_goal) new_transition = ( obs['observation'], action, new_reward, next_obs['observation'], new_goal, ) her_transitions.append(new_transition) return her_transitions有三个细节必须强调。
第一,重算奖励时用的是next_obs['achieved_goal']和new_goal之间的距离。这里如果奖励函数写错了,HER 就完全失去意义。很多人在最初实现时只改了目标、忘了重算奖励,结果存进去的还是 0 奖励,练了等于白练。第二,future 采样时注意边界条件,最后一个 transition 没有未来状态,只能放弃或者改用 final。第三,奖励函数最好写成批量形式,不要 for 循环逐条算,否则 HER 生成四倍数据后会严重拖慢训练。
奖励函数的一个参考实现:
def compute_reward(achieved_goal, desired_goal, threshold=0.05): # 批量计算目标距离 dist = np.linalg.norm(achieved_goal - desired_goal, axis=-1) return (dist < threshold).astype(np.float32)3.3 网络输入设计:哪些拼进状态、哪些只在奖励里出现
HER 是目标条件算法,网络输入比普通 RL 多一个“目标向量”。以 gymnasium 的 Fetch 类环境为例,观测空间是一个 dict,包含三个字段:observation(原始传感状态)、achieved_goal(实际到达位置)、desired_goal(期望位置)。在构造网络输入时,我强烈建议只拼接observation和desired_goal,把achieved_goal留给奖励重算用,不要拼进状态。
为什么?因为achieved_goal本身就是observation的一个子集或者与它高度相关,拼进去只会徒增维度,让网络更难以收敛。我曾经在一个任务里图省事,直接把三个字段全 concatenate 进网络,结果原本 50 维的状态变成了 65 维,训练速度下降了近三分之一,成功率反而更低。目标向量和状态向量拼接后,通常不需要额外归一化,但要确保两者的量纲一致。如果一个是位置(单位米,数值在零点几),一个是关节角度(单位弧度,数值在负三到正三),最好各自做标准化再拼。
3.4 参数选择与调参实战
HER 的调参核心就两个:k 值和重标注策略,剩下都是常规 RL 参数。
k 值代表每条原始经验额外生成多少条 HER 经验。任务目标单一(比如永远是把物体推到固定位置)时,原始经验的目标没有多样性,可以加大 HER 经验比重;多目标任务(目标从多个起点中随机生成)时,k=4 是个稳妥起点。k 值太大有个副作用:重标注经验把原始经验冲得太淡,智能体反而忘了“真正任务”的目标分布。我用 k=8 跑过 FetchPush,前 50 万步还行,后面越训越差,回调到 k=4 后立刻稳定。
重标注策略直接选 future,别再犹豫。只有一种情况可以试试 final:任务非常短,比如 2~3 步就能完成,整个轨迹长度太短,future 采样空间太小,这时候 final 反而稳定。其他常规参数,我给出一个可以直接抄作业的参考表:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 基础算法 | SAC 或 DDPG | DDPG 简单,SAC 稳 |
| k | 4(多目标)、0~1(单目标) | 额外重标注经验比例 |
| gamma | 0.95~0.98 | 目标导向任务不推荐太高 |
| batch_size | 128~256 | 显存充裕可加 |
| replay buffer | 1e6 | HER 需要大量离线样本 |
| 网络结构 | 两层 MLP,隐藏层 256 | 图像输入换 CNN |
| actor 学习率 | 1e-3(DDPG)/ 3e-4(SAC) | DDPG 的 actor 学习率再低更稳 |
| critic 学习率 | 1e-3 | 与 actor 解耦 |
| soft update tau | 0.05 | DDPG 推荐 |
| 探索噪声 | 高斯噪声 std=0.1~0.3 | 前期大噪声,后期衰减 |
3.5 一组可参考的基线实验结果
我把自己在模拟环境里的几组实验结果整理成表,供参考。环境都来自 MuJoCo/Gymnasium 的 Fetch 系列,算法为 DDPG + HER,每个配置跑 200 万步,成功率按最后 100 个 episode 统计。
| 环境 | 任务描述 | k 值 | 最终成功率 | 备注 |
|---|---|---|---|---|
| FetchReach | 末端点到达目标点 | 0 | 98% | 任务简单,仅重标注即可 |
| FetchPush | 把物体推至目标点 | 4 | 92% | future 策略优势明显 |
| FetchPickAndPlace | 抓起物体放至目标点 | 4 | 76% | 需要更多步数,建议 SAC |
| FetchSlide | 把物体滑到目标点 | 8 | 68% | 有摩擦,奖励阈值要放宽一点 |
这些数据不算多漂亮,但能说明一个趋势:任务越复杂,HER 需要的 k 值和步数都越大,而且单纯加 k 不解决问题,换更稳定的算法更有效。
4. 实操中我踩过的坑与排查记录
4.1 加了HER反而训练不动?先查这三个地方
HER 不是加了就一定能跑起来的魔法,我见过不少“加了反而更差”的案例,排查顺序通常固定:
第一,先确认奖励有没有真的重算。这是新手最常犯的错误。改目标只是换了个标签,如果new_reward还是拿原始目标算出来的,那 HER 经验里的“成功”就是假的,训练曲线永远起不来。第二,确认achieved_goal的编码是否一致。在 Fetch 类环境里,物体的位置是全局坐标;如果你把状态做了平移或变换,achieved_goal却忘了跟着变换,距离算出来完全错误,HER 会往错误的方向强化。第三,确认 future 采样是否限制在同一 episode 内。如果从全局经验池里随便抽未来的状态,就等于把不同轨迹拼接成了假轨迹,时间结构彻底破坏。我见过有人图省事直接从 replay buffer 里抽“未来”状态,结果是奖励偶尔出现,但成功率怎么也不涨,最后定位到就是这个原因。
4.2 训练曲线长期平地:问题往往不在算法,在状态表示
有个导航任务,状态维度做到了 100 维,真正对任务有影响的其实只有 4 维位置信息,其余全是传感器噪声和无关变量。HER 跑了几百万步,成功率一直趴在地上。我当时一度怀疑是 k 值不够,后来把状态用 t-SNE 降维可视化才发现,网络全在学无关特征,目标信息完全被淹没了。解决方式是先做特征选择,或者给状态加权,把关键维度单独抽出来拼成新的observation,曲线马上就起来了。
除了状态维度,奖励阈值也是隐形杀手。阈值设得太松,比如要求误差小于 5 厘米,任务确实容易学,但学到的策略精度很差,真机上根本没法定点抓取;阈值设得太严,比如小于 0.1 毫米,HER 重标注后正样本依然极度稀疏,相当于没用。我的经验是,阈值跟真实传感器精度对齐,比传感器精度略松一点,这样学出来的策略才有迁移价值。
4.3 HER的边界:什么时候应该果断放弃HER
HER 不是万能的,有几类任务我试下来效果很差,建议直接换方案。
第一,目标是抽象语义(比如“把钥匙插进锁孔并旋转”)。这种任务的 achieved_goal 没法直接从状态里提取,你不能说“只要钥匙在锁孔附近就是达成目标”,因为“插进去转一圈”是一个过程性动作,不是目标状态。这种情况可以考虑额外训练一个目标编码网络,但复杂度很高。第二,奖励不是二元稀疏的,而是需要长期累积才能判断(比如“保持平衡 100 步”)。HER 的核心假设是“达到某个状态立即获得正奖励”,对于需要持续维持的任务,重标注出来的正样本语义不成立。第三,目标状态几乎不可达。如果任务要求智能体到达一个物理上极难触达的状态,而轨迹中实际到达的状态也都很差,那重标注出来的目标同样是垃圾,没有学习价值。
4.4 常见问题速查表
把我在多个项目里遇到的典型问题整理成一张表,建议直接收藏。
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 训练曲线完全不起 | 重标后没有重算奖励 | 检查 r_new 是否基于新目标计算 |
| 训练曲线不起,但奖励偶有波动 | achieved_goal 编码或归一化错误 | 打印 distance 分布,看是否在合理范围 |
| 训练震荡严重、越训越差 | k 值太大,原始经验被淹没 | 降低 k,或提高原始经验采样权重 |
| 上一步还行、下一步崩了 | future 采样跨 episode | 限定同一 episode 内采样 |
| 状态维度高但成功率极低 | 无关特征干扰 | 可视化状态,做特征筛选 |
| 加了 HER 后 PPO 直接爆 | 算法不匹配 | HER 只能用 off-policy 算法 |
| 目标阈值太宽导致精度差 | 奖励阈值设置不合理 | 对齐传感器精度,轻微放宽 |
最后再分享几点我个人的实测体会
HER 真正厉害的地方,不在于“让算法作弊”,而在于它彻底改变了我们对失败数据的态度。回放 buffer 里那些奖励全零的轨迹,原本是要被丢弃的废料,现在却能被重新标记成成功样本,变成训练的燃料。我在实际跑实验时慢慢地有了一个习惯:遇到任何目标导向型任务,第一版先不上手工奖励塑形,直接把稀疏奖励加上 HER 扔上去跑,看曲线能不能起来,再决定要不要加辅助信号。大多数情况下,这一套配置已经比手写奖励函数既省力又可靠。
另外一个从踩坑里换来的心得是,HER 在工程上最好的搭档不是 DDPG,而是 SAC。DDPG 的确定性策略对探索和超参数都太敏感,稍不留神 critic 就过估计了;SAC 自带的熵正则能自动平衡探索,配合 HER 的重标注,整体训练稳定性好很多。如果非要用 DDPG,记得把 actor 的学习率降到 1e-4 以下,并在 critic 输入层对目标向量和状态向量分别做归一化。
最后分享一个小技巧:HER 生成的重标注经验不需要一视同仁地进经验池,可以进一步配合优先级采样——把“新目标与原始目标距离较远”的样本赋予更高优先级,因为这种样本往往携带更丰富的泛化信息。实测在 FetchPush 上能再提升 5% 左右成功率。这个方向再延伸下去,就是和离线强化学习、自动课程学习、分层强化学习的结合了,我们改天再单独开一篇聊。