玩强化学习的人,迟早会撞上稀疏奖励这堵墙;而 hindsight 这个项目,盯的正是这个问题。hindsight 是 Hindsight Experience Replay(HER)的复现项目,名字直译过来就是“事后诸葛亮”。它在 2018 年那篇同名论文里被提出,核心思路一句话:别让智能体只从成功里学,让它把每次失败都改写成一次成功,再拿这些数据来训练。下面我会按问题、原理、实现、调参、踩坑的顺序,把 HER 从头到尾聊透。适合已经能跑通 DQN、DDPG 基础代码、正被稀疏奖励困扰的读者,也适合想搞清楚目标重标注原理的强化学习新手。
1. hindsight 到底在解决什么问题
1.1 稀疏奖励为什么这么难搞
想象一个机械臂要推方块到目标位置。奖励函数最简单写法就是:方块位置与目标距离小于阈值就给 1,否则给 0。在这个设置下,智能体随机探索碰到目标的概率极低,尤其在目标只是三维空间里一个小区域时。于是训练过程变成:策略网络输出的梯度几乎处处是 0,参数迟迟不更新,看起来就像在原地空转。
更麻烦的是,即使你把奖励换成每一步 -1 的负常数,在没有正反馈的情况下,智能体也学不出什么好行为。它只能学会“少扣分”的投机策略:要么原地停住不动,要么往边界外面跑,因为它找不到任何能减少惩罚的方向。信号完全消失时,再好的网络结构、再大的 batch 都白搭。这是目标导向任务里最让人崩溃的处境,也是 HER 出场的理由。
1.2 “事后诸葛亮”的学习直觉
hindsight 的英文原意是“事后回看”,HER 把它用到强化学习里,思路其实特别生活化:考试没答对,老师把正确答案发下来,让你把错题当成新题重新练。对应到训练里,就是每次 rollout 失败后,别把这次经历扔进回收站,而是把“当时实际达到的状态”重新定义成一个新目标,再把这组数据放进经验池里训练。
举个例子,机械臂没把方块推到红色目标点,但途中碰巧推到过蓝色区域。按照 HER 的逻辑,这条数据不用浪费,我们直接把它当作“学习如何推到蓝色区域”的成功经验。等‘推到蓝色区域’这个子策略学会了,再学推红色目标就轻松得多——因为动作空间里已经有了成功方向。这个思想的精妙之处在于,它不改变环境、不手工设计奖励,只改数据本身。失败的轨迹摇身一变,成了有价值的训练样本。
1.3 什么样的任务适合用 hindsight
不是所有任务都能上 HER,它有一个硬性前提:任务必须能用 goal 和 achieved_goal 来表述,并且训练时要能判断当前是否达成目标。典型场景有机械臂抓取与推方块、导航到达、拼图还原、分子生成等。这类任务天然存在“目标状态”和“实际达到状态”的对应关系,HER 才有东西可以重标。
反过来,如果任务没有明确目标,比如对抗性游戏、或者只追求“总分最大化”的连续控制,就很难套用 HER,因为你根本没法定义“智能体实际达到了什么目标”。另外还有一点要注意,HER 要求目标维度和观测维度能分离,至少要能把 achieved_goal 单独拎出来存储。这个细节看起来不起眼,却是许多复现项目改到一半卡住的根源,后面实现部分我会专门强调。
1.4 为什么不直接做奖励塑形
很多人第一反应是:“奖励函数写密一点不就行了,用当前距离当惩罚不就有梯度了吗?”确实有效,但奖励塑形有两个深坑。第一,设计成本高,距离阈值、惩罚系数稍微没调好,智能体就会钻漏洞,比如绕过障碍物、原地抖动骗距离下降;第二,塑形后的奖励和真实目标之间容易产生偏差,策略在塑形奖励下表现很好,换回真实目标却直接失效。
HER 相当于绕开了手工奖励工程,从数据侧解决信号稀疏问题。而且它和奖励塑形并不冲突,实际项目里两者经常叠加使用:HER 保证有梯度,塑形给梯度提供一个更好的方向。不过新手最好先只加 HER,把机制吃透了再考虑塑形,这样出问题时容易定位。
2. hindsight 的核心原理拆解
2.1 目标重标注的数学表达
先看一条普通 transition 的存储形式:(s_t, a_t, s_{t+1}, g, r),其中 g 是任务目标,s 里会带一个 achieved_goal 分量 ag,r 是当前奖励。对于稀疏奖励,可以写成:
- r = 1,当 || ag_{t+1} - g || ≤ ε
- r = 0,否则
HER 做的就是一件事:把这条 transition 里的 g 换成另一个目标 g'。g' 从哪里来?最简单的取法就是 g' = ag_{t+1},也就是智能体下一时刻实际达到的状态。代入奖励函数之后你会发现,新奖励 r' = 1。原本一条失败轨迹中的 transition,重标后变成了一条“成功”transition。
这就是 HER 所有魔力的来源:网络在回放时看到的不是一片全零奖励,而是大量“目标达成”的信号,价值函数和策略梯度都有了明确的方向。注意重标时不仅要改 goal,还要根据新 goal 重新算是否终止、是否成功,否则训练信号里会混入不一致的 done 标记,导致价值函数估计混乱。
2.2 为什么必须搭配离策略算法
这是新手最容易忽略的前提。HER 重标后的 transition,并不符合当前策略的真实行为分布:这条数据是智能体在追求原目标 g 时采出来的,你却把它当作追求新目标 g' 的数据来训练。换句话说,数据来源和策略分布之间出现了错位。
这种错位对离策略算法(DQN、DDPG、TD3、SAC)不是问题,因为这些算法本来就从历史经验池里反复采样,不要求数据严格来自当前策略;只要重新赋予的目标、奖励和 done 是自洽的,价值函数就能正常学习。但对 PPO 这类同策略算法,它要求训练数据必须来自当前策略,HER 重标后的数据会严重违背这一前提,实战里很少见到 PPO + HER 的成熟组合。所以搭 HER 项目时,优先选择 DDPG 或 SAC 作为底层算法,不要在主循环里硬塞一个 PPO 然后怪 HER 没用。
2.3 新目标从哪里来:final、random 与 future
重标目标并不是随便抽个状态就行,论文里系统比较过三种策略:
- final:只拿 episode 终点时的 achieved_goal 作为新目标。实现最简单,但一条轨迹只能生成一个重标样本,坏处是浪费了中途接近目标的信息。
- random:从同一条 episode 里随机抽一个状态作为新目标。多样性最好,但目标可能太随机,和轨迹内容没什么因果联系,学到的东西比较碎。
- future:从当前 transition 之后的轨迹里抽 k 个状态作为候选目标。既贴近实际情况,又有足够的多样性,是论文验证下来最稳的策略,k 通常取 4。
| 策略 | 采样来源 | 优点 | 要注意的地方 |
|---|---|---|---|
| final | 整条轨迹终点 | 代码最简单,目标明确 | 信息量有限,一条轨迹只出一个重标样本 |
| random | 同一轨迹任意点 | 目标分布多样 | 可能抽到太远或无意义的目标 |
| future | 当前时刻之后的轨迹 | 目标近且真实,多样性好 | 需要存整条轨迹和时间戳,代码稍复杂 |
我用最顺的配置一直是 future,k=4。如果你的环境轨迹很长,可以适当把 k 调小,减少重标样本量对 buffer 的压力。另外,采样新目标时记得加一点随机性,固定取“最近的下一个状态”会让目标分布过于集中,反而不利于探索。
3. 完整实现流程与关键参数
3.1 先用玩具环境验证想法
直接上手机械臂环境调试 HER,是我见过最多人踩的坑。稀疏奖励 + 高维状态 + 仿真渲染,三个因素叠加起来,一个隐藏 bug 能让你白跑两三天。我的习惯是先在 bit-flip 环境上验证:环境里有若干个二进制位,智能体每次可以翻转其中一位,目标是让整串状态变成目标向量。
这个环境收敛快、失败模式可预期,而且非常适合检查重标逻辑是否正确:如果 HER 实现错了,真目标成功率要么不动、要么疯狂震荡,你能在十分钟内发现问题。在 toy environment 跑通之后,再迁移到 FetchReach、FetchPush 这类机器人环境。千万不要跳过这一步,HER 的错误大多出在数据存储和目标重标上,越简单的环境越容易定位。
3.2 重放缓冲区要做的改造
HER 对经验池提出的第一个要求是:每条 transition 必须额外保存 achieved_goal,并且要知道自己属于哪条 episode、处于哪个时间步。普通优先经验回放那种散装 transition 池在这里不够用,因为 future 策略需要“回到同一条轨迹的未来状态里采样”。所以实际项目里我用的是 episode 级存储:按整条轨迹存,采样时再从轨迹里随机截取 transition。
重放逻辑的核心其实就是一句话:从 buffer 里采一小批数据,随机选一部分做重标,用 new_goal 重新计算 reward 和 done。下面是我整理过的最小伪代码,可以照着实现:
# HER 的 future 策略重标核心逻辑 def relabel_future(episode, t, k=4): """ episode: 存储整条轨迹的列表,每项包含 state/action/achieved_goal t: 当前 transition 在轨迹中的位置 """ relabeled = [] # 只从 t 之后的时刻里抽候选目标 candidates = np.arange(t + 1, len(episode) - 1) if len(candidates) == 0: return relabeled chosen_times = np.random.choice(candidates, size=min(k, len(candidates)), replace=False) for j in chosen_times: new_goal = episode[j].achieved_goal.copy() relabeled.append({ "state": episode[t].state, "action": episode[t].action, "next_state": episode[t + 1].state, "goal": new_goal, # 注意:reward 必须用新目标重新算 "reward": compute_reward(episode[t + 1].achieved_goal, new_goal), "done": is_success(episode[t + 1].achieved_goal, new_goal), }) return relabeled注意看 reward 这一行,它用的是 episode[t+1].achieved_goal 和 new_goal 计算,而不是原来保存的 reward。忘记重算 reward 是 HER 复现里最常见的错误,没有之一。另外,如果你的 batch 里有一部分 transition 保持原目标、一部分走重标,那你需要在采 batch 时控制比例,这个比例就是下面要说的 replay_ratio。
3.3 关键超参数怎么设
结合我的调参经验,HER 最值得关注的超参数就四个:replay_ratio、k、goal 阈值 ε、buffer 大小。
replay_ratio 控制的是 batch 里有多大比例的 transition 会被重标。我常用的起点是 0.8,也就是 80% 的样本被重标,剩下 20% 保留原始目标。如果训练后期发现真目标成功率上不去,怀疑智能体“忘本”,就把这个值降到 0.5 左右。千万别设成 1.0,全重标会让策略彻底忽略真实目标,训练出来的东西只在重标目标上有效,真任务基本报废。
k 前面说过,future 策略采候选目标数量,默认 4。环境轨迹特别长时可以降到 2,对经验池和算力都友好。
goal 阈值 ε 直接决定信号密度。阈值太严,重标后的奖励也大部分是 0,HER 的优势就体现不出来;阈值太松,success 判定失真,策略学到的动作很粗。我的做法是分段收紧:先用一个较大的 ε 把训练拉起来,中期再逐步缩小,相当于给任务做了一个隐式的课程学习。
buffer 大小主要影响稳定性,我在玩具环境用 10 万条 transition,机器人任务一般开到 100 万条量级。注意 HER 的重标副本会占用额外空间,buffer 尺寸要预留一部分给重标数据。
3.4 训练时看什么指标
HER 项目里最容易出现的监控误区是:只盯着重标后的 reward 看。重标数据是“自己给自己打分”,曲线再漂亮也不能说明真实任务学会了。真正的金标准是 original goal success rate,也就是不重标、直接用原始目标跑评估时,智能体成功完成的概率。
我建议训练脚本里同时打三行指标:原始目标成功率、重标目标平均奖励、动作噪声当前大小。如果第一行不动、第二行暴涨,说明 HER 在工作,但策略被重标目标带偏了;如果第一行和第二行一起涨,说明路子对了。另外要留意环境并行度,我用 8 个并行环境收集轨迹,比单环境数据多样性好很多,重标目标也不容易集中在一小块区域。
4. 实操中的常见问题与排查实录
4.1 目标分布偏移导致训练“学偏”
我在 FetchPush 上遇到过最典型的怪现象:重标奖励一直很漂亮,但真目标成功率涨到 40% 后就开始原地踏步,甚至掉头往下。排查下来,根因是目标分布偏移。训练前期,智能体大多失败在起点附近,重标目标也集中在起点周边;随着策略进步,它能到达的状态范围变了,但经验池里还堆着大量前期“失败样本”,这些旧分布的重标数据不断把策略往回拉。
应对办法有几个,我实测有效的是:控制 buffer 里旧 episode 的比例,训练中后期定期清理最老的一批轨迹;同时把 replay_ratio 从 0.8 降到 0.5,让原始目标信号重新占据主导。还有一种思路是给重标数据加一点优先级采样,把“距离成功最近”的轨迹放得更重,这样方向感更强,但实现复杂度也更高。
4.2 重标比例失控的连锁反应
有段时间我把 replay_ratio 调到 0.95,结果真目标成功率一路接近零。复盘后发现原因很直接:batch 里几乎全是重标样本,价值函数学到的全是如何逼近各种“实际达到状态”,原始目标对应的状态在训练中变成了少数派,策略自然就把真目标忘干净了。
这就是为什么我一直强调留足原始目标样本。更稳的做法是动态调整:训练早期重标比例大一点,快速建立动作-目标之间的映射关系;训练中后期逐步降低重标比例,让策略把注意力拉回真实目标。这个退火过程不需要很精细,两三个阶段就够,但效果比固定比例稳定得多。
4.3 奖励阈值和奖励密度怎么搭配
另一个容易翻车的地方是 ε 和奖励函数的设计。如果 ε 定得太小,即使经过重标,目标在实际达到状态附近时奖励仍然大概率是 0,网络还是学不到有效梯度;如果 ε 定得太大,success 判定形同虚设,智能体稍微靠近一点就算成功,训练出来的策略精度很差。
我的经验是:先单独看一条失败轨迹重标后的奖励分布。如果你发现重标样本里 reward=1 的比例很低,说明 ε 太严或者 future 采到的目标太远,把 ε 调大一点,或者把 k 调小让候选目标更近一些。有些项目会在 HER 之上叠加距离奖励,比如用 -distance 替代纯 0/1,这对收敛速度有帮助,但要小心奖励尺度变化导致价值函数训练不稳,通常需要同时把学习率调小。
4.4 常见问题速查表
| 症状 | 可能原因 | 快速检查方法 | 对策 |
|---|---|---|---|
| 重标奖励也很低 | ε 太严 / k 太大 | 打印重标 reward 分布 | 调大 ε,或减小 k |
| 真目标成功率上不去 | replay_ratio 过高 | 看 batch 里重标占比 | 降到 0.5~0.6 |
| 成功率涨到一半开始掉 | buffer 旧数据过多 | 统计 buffer 里新老 episode 比例 | 清理旧轨迹或降 replay_ratio |
| 智能体原地不动 | 探索不足 | 看动作噪声大小 / 目标分布集中度 | 加高斯噪声或参数噪声 |
| loss 剧烈震荡 | 奖励尺度太大 / 学习率高 | 看奖励绝对值量级 | 归一化奖励或降低学习率 |
| done 标记混乱 | 重标后没重算 done | 检查重标样本的 done 是否一致 | 重标时同步重算 done 和 reward |
提示:HER 的所有重标逻辑都必须确保“目标-奖励-done”三者自洽。只换 goal 不换 reward 和 done,等于给训练数据里掺噪声,问题的隐蔽性还特别高。
5. 周边扩展和个人实操体会
5.1 从 HER 到后续改进思路
如果把 HER 吃透了,后续有不少方向可以顺势展开。比如 Hindsight Goal Generation(HGG),用生成模型来提出中间目标,解决 HER 在长程任务里“目标只能从现实轨迹里找”的局限;再比如把 HER 嵌进分层强化学习框架,高层规划出子目标、底层用 HER 快速学会到达子目标,这对复杂机器人操作特别有价值。
这些扩展本质上都是把 HER“自己给自己造目标”的思想放到更大的框架里。所以我不建议一上来就追新变体,先把基础版本在 toy environment 上调通,理解目标重标和环境交互数据的关系,后面看任何改进论文都能一眼看出它动了哪一块。
5.2 我自己在实操中沉淀的几条心得
最后分享几个被坑出来的经验。第一,数据格式上把 state、achieved_goal、goal 分开存,千万别拼成一个向量。HER 要反复对目标做替换和切片,拼在一起会让每个重标步骤都缠上维度处理的 bug,排查起来非常痛苦。第二,把玩具环境先跑通这件事无论如何都别省,HER 的隐藏 bug 大多藏在重标逻辑里,越简单的环境越容易暴露。第三,HER 不代替探索,如果 achieved_goal 的分布一直集中在起点附近,再好的重标机制也榨不出有用信号,优先去改善探索策略,比如加 RND 或参数噪声,再回头看 HER 的效果。
我刚接触 HER 时,总觉得“把失败改成成功”过于投机,试过之后才明白,关键不在于数据是不是真的成功,而在于它是否给价值函数提供了正确的学习方向。hindsight 这个项目让我最受益的一点,就是逼我把重放缓冲区的每个字段都想明白——reward、done、goal 三者一致性,才是 HER 能不能收敛的命门。