“hindsight”这个词,字面上是“后见之明”,放到不同领域意思完全不一样。心理学里说的是“事后诸葛”那种偏差,工程圈里Mozilla还给日志分析工具起过这名,但在强化学习这个圈子里,一提到hindsight,大家条件反射想到的就是那个赫赫有名的算法——Hindsight Experience Replay,也就是HER。
这个项目标题就一个词,信息量却非常大。HER解决的是强化学习里最磨人的稀疏奖励问题:智能体在环境里狂奔上千步,奖励始终是0,它到底该怎么学?传统Q-learning的做法基本是“看运气”,运气不好几百万步都转不动。HER换了个思路:目标A没达成,那我就把这整段轨迹伪装成朝着目标B努力的成果,而目标B恰好是轨迹里实际到达过的某个状态。这样一来,原本没有任何学习价值的失败轨迹,就变成了能提供有效梯度的成功经验。
如果你正在做机械臂抓取、导航规划、机器人控制这类项目,或者你手里的任务奖励函数根本设计不出来,那这篇文章就是写给你的。下文会从原理讲到代码实现,再附上我实际过程中踩过的坑和排查思路,尽量让你少走弯路。
1. 内容整体设计与思路拆解
1.1 稀疏奖励问题:RL最经典的拦路虎
先把标准强化学习的逻辑捋一遍。智能体每一步执行一个动作,环境返回一个奖励信号,智能体根据历史累计回报去更新策略。拿CartPole这种经典任务举例,每个时间步只要杆子不倒就加1分,奖励是稠密的,智能体每一步都能快速判断动作好坏,学起来自然很快。
但真实世界的任务远没有这么温柔。机械臂抓取一个螺丝,只有机械爪完全闭合、物体被抓起来的那一瞬间,奖励才变成1,之前所有动作反馈全是0。想象一下,智能体随机地把机械臂挥来挥去,从这个状态空间里找到“抓取成功”这个区域,概率可能低到十万分之一甚至更低。轨迹里每一步都是0奖励,没有梯度,没有信号,相当于一个学生在毫无提示的情况下做一道完全没头绪的题,做对了也不知道为什么对,做错了也不知道为什么错。
从概率角度看这个问题更直观。假设环境状态空间的维度是d,目标达成区域占整体状态空间的体积比例是p,那么纯随机策略单次尝试的成功率大概就是p这个数量级。在机械臂任务里p通常小到可以忽略,这意味着平均要跑几十万次尝试才可能碰到一次成功。更尴尬的是,就算运气好撞上一次成功了,靠孤零零一条轨迹也学不出什么泛化能力,下次换个初始位置照样抓不起来。
稀疏奖励问题本质上就是样本效率和探索效率的双输。不是算法不聪明,是信息源本身就没有信息。
1.2 HER的核心思想:换个角度解读失败
HER的想法其实带着一点“事后诸葛亮”的味道:既然我最终没有到达目标A,但我这一路走过来确实经过了若干状态,那我为什么不把这条路重新解读成“克服重重困难抵达目标B”的成功示范呢?
拿机器人开抽屉举例。设定的目标是“把手移动到抽屉把手的位置并拉开”,智能体连续尝试了100步,最后手的位置和目标差了10厘米,这条轨迹对原目标来说一败涂地。但在采集轨迹的时候,HER会记录下每个时间步实际到达的状态,然后把轨迹最后的位置(或者中途某个状态)当作新的“既定目标”,重新计算奖励:手真的到了这个位置,那么对于“到达这个位置”的目标而言,这条轨迹就是一次满分的成功示范。
这么做直接的效果是:原本毫无价值的失败轨迹,被重新变成了带着“完整状态-动作-目标”三元组的成功经验。智能体从中学到的是一个更本质的规律——当目标状态恰好等于我这条轨迹的终点时,我刚刚执行的那一串动作确实能达成目标。这比硬生生等待一个稀有的成功信号要高效得多。
从课程学习(curriculum learning)的角度看,HER也很有意思。它相当于自动给学习过程编排了一个从易到难的热身序列:先让智能体学会如何到达那些容易到达的状态,再逐步扩展目标覆盖范围到更远的区域。随着重标记的目标越来越接近真实分布,智能体对“如何达成一个任意给定目标”的理解会越来越完整,最终泛化到原本那个棘手的真实目标上。
1.3 为什么必须配合off-policy算法使用
很多人问过我一个问题:HER是不是一个独立的算法,能不能直接替换掉PPO?答案是不能。HER本质上不是一个完整的强化学习算法,它更像一个经验池改造模块,必须寄生在off-policy算法(比如DQN、DDPG、TD3、SAC)之上才能发挥作用。
原因在于HER重标记出来的经验与策略参数是解耦的。想象一下,你采集了一条轨迹,把终点状态重标成一个新目标,那这条经验里记录的动作、奖励、下一个状态和目标状态全部是可以复用的。它不依赖于“当前策略是什么”,只依赖于“这条轨迹确实发生过了”。所以它可以被反复从回放缓冲区里采样出来用于训练。而像PPO这类on-policy算法,经验用完就丢,每一轮都用最新的策略去采样,重标记带来的信息增益根本没有机会被充分消化。
关键差异在于,HER改善的是经验池的内容质量,而不是直接修改策略梯度的计算公式。它通过提高单位样本的信息密度,让off-policy算法在奖励极其稀疏的情况下也能学到梯度方向,但前提是这个算法本身要有回放缓冲区。理解了这一点,你就明白为什么OpenAI那篇论文里所有实验都是DDPG配HER,而不是PPO配HER。
2. 核心细节解析与实操要点
2.1 四种目标重标记策略的取舍
OpenAI那篇提出HER的文章里,明确规定了四种从轨迹中选取新目标的方式,分别是final、future、episode和random。我用过很长一段时间,大概讲讲每种的实际表现。
final最简单粗暴,直接把轨迹最后一步的状态当作新目标。要求再低一点的任务用final也能看到效果,比如目标是到达某个区域,轨迹末尾离目标区不远,那重标记出来的目标就和真实目标接近,学习曲线还算好看。但它有个问题:轨迹中途经历了多次转折,末尾只是最后停的地方,很可能是个随机位置,信息量偏弱。
future策略是我最推荐的,也是在绝大多数任务中效果最好的。它的做法是从当前时间步之后的某个时刻,随机挑一个后续状态当作当前时间步的新目标。这样保留了因果顺序:智能体确实在接下来的轨迹中到达了这个状态,并且这个状态之后没有发生破坏性的变化。实测下来,future策略在Fetch系列任务中比final的收敛速度快了不少,成功率也能稳定提升一个档次。
episode策略是从整条轨迹里任意挑选一个状态,实现上是允许从历史状态中选,这样带来的问题是可能选到早已路过的状态,目标“时效性”被稀释。random策略几乎是随机从经验池里挑一个状态当目标,基本等于没用,我试过几次都没什么提升。
我的最终建议是:默认选择future,k值取4,如果任务状态空间维度特别高或者轨迹特别长,可以适当调大k,但别超过8,否则经验池会被大量的“远目标”稀释。
2.2 关键超参数:k值、重放比例与目标维度
以下是我在实际调参中总结出的几个最影响HER效果的因素,挨个说一下。
k值指的是每条原始经验额外生成的重标记经验条数。k值越大,重标记经验在经验池中的占比越高,稀疏奖励轨迹被“纠正”的密度就越大,训练初期更容易找到梯度。但k值过大也有副作用:额外生成的目标往往是轨迹中途或末尾的状态,距离初始状态非常远,对智能体来说属于“极端困难目标”,如果数量占比过高,反而会让训练变得不稳定。我见过有人把k调到16,结果训练中期出现明显的震荡。
replay比例指的是原始经验与重标记经验被采样的占比。原文建议是1比1,也就是采样时原始经验和重标记经验各占一半。实际调参时这个比例对性能影响非常显著。如果重标记经验占比过高,模型会对“如何达成随机目标”过度拟合,真实目标反而学不好。我通常的做法是先按1比1起步,然后观察成功率曲线:如果真实目标成功率增长缓慢,尝试把重标记占比降到0.5比1、0.3比1;如果反而下降,就提高重标记占比。
目标维度的设计和HER的成败完全绑定。HER要求目标必须是状态空间中的一个可观测子集,比如机械臂末端的三维坐标,而不是一个离散的任务ID。如果你把一个包含位置、速度、姿态的完整高维状态当作目标,那重标记出来的经验几乎没法学习,因为即使再聪明的重标记算法,也无法从一条轨迹中推断出所有维度上都严格匹配的目标。尽量把目标限制在任务真正关心的维度上,比如只关心位置不关心速度,这样重标记出来的经验天然就具有更多的有效样本。
2.3 奖励函数设计的隐藏细节
HER能用,前提是你要有一个度量目标达成程度的奖励函数。绝大多数情况下这个函数是距离函数:新目标g'是轨迹经过的某个状态,奖励给的是当前实际状态与g'之间的距离差,或者距离小于某个阈值的二值奖励。
这里有个细节必须注意:阈值膨胀会毁掉整个任务。假设你设置了一个比较宽松的阈值,比如机械臂末端距离目标5厘米就算成功,那智能体很快就学会“差不多就行了”,它不会去追求精确到达目标状态。而在HER的框架下,目标状态是轨迹中的真实状态,如果阈值太宽,重标记出来的经验里“目标达成”根本没达成,错误信号就会被一遍遍放大。
我给出一份经验值参考:二值奖励的阈值最好是目标空间正常尺度的大约5%到10%。比如目标是末端位置在1立方米的范围内,那阈值设在5到10厘米比较合适。如果你用距离作为连续奖励,就不存在阈值问题,但需要额外设计一个奖励缩放系数,避免Q值范围过大导致训练不稳定。
另外还建议设计一个辅助的“成功判定函数”,它只在真实目标上生效,用于记录成功率指标,不参与奖励计算。这个判定函数的作用是让你在训练日志中看清模型对真实目标的真实掌握程度,而不是被重标记经验带偏。
3. 实操过程与核心环节实现
3.1 环境选型与基线对比:为什么选Fetch系列
HER的经典验证环境是OpenAI Gym里的Fetch系列,常见几个任务包括FetchReach、FetchPush、FetchSlide和FetchPickAndPlace。我建议不要一开始就跑这些背后那个巨大的仿真库,可以先在一个轻量级环境里把代码流程跑通,再上机器人仿真。
Fetch系列的核心交互结构是:目标是一个三维位置坐标,奖励稀疏,只有在目标位置与物体位置的距离小于阈值时才给0,否则给-1。状态空间里包含物体当前位置、目标位置、手指位置、相对距离等字段。这套设计天然就是为HER准备的,因为目标状态可观测、低维度、有明确的距离度量。
实操中我的对比基线建议这样定:先跑一个纯DDPG拿来当基准,然后跑DDPG加HER,最后跑DDPG加HER再加一些优化技巧(比如优先经验回放、目标网络更新比率调整)。对比的意义在于你能明确看到HER带来的提升幅度。我跑下来的经验是:纯DDPG在FetchReach这种简单任务上勉强能学会,但FetchPush这种任务基本学不动;加了HER之后,FetchPush能达到90%以上成功率,差距非常明显。
3.2 核心代码实现:在DDPG中集成HER
下面给出一段我常用的HER经验池集成代码,使用PyTorch和Gym接口。注意这里省略了完整DDPG实现,重点展示HER重标记和采样逻辑。
import numpy as np import torch from collections import deque class HERBuffer: def __init__(self, capacity, k=4, strategy='future'): self.capacity = capacity self.k = k self.strategy = strategy self.buffer = deque(maxlen=capacity) def add_episode(self, episode, achieved_goals): # episode: list of dicts, each contains {state, action, reward, next_state, goal} # achieved_goals: list of achieved goal vectors for each time step for i, transition in enumerate(episode): # 原始目标经验直接入池 self.buffer.append(( transition['state'], transition['action'], transition['reward'], transition['next_state'], transition['goal'] )) # 对每条经验做目标重标记 for i, transition in enumerate(episode): for _ in range(self.k): if self.strategy == 'future': # 从当前步之后的步中随机选一个时间步的achieved_goal future_idx = np.random.randint(i, len(episode)) new_goal = achieved_goals[future_idx] elif self.strategy == 'final': new_goal = achieved_goals[-1] elif self.strategy == 'episode': new_goal = achieved_goals[np.random.randint(0, len(episode))] else: # random: no-op于此略 continue new_reward = compute_reward(new_goal, achieved_goals[i], threshold=0.05) self.buffer.append(( transition['state'], transition['action'], new_reward, transition['next_state'], new_goal )) def sample(self, batch_size, replay_ratio=1.0): # replay_ratio表示原始经验占比,这里简化处理为原始:重标记 = 1:1 batch = np.random.choice(len(self.buffer), size=batch_size, replace=False) states, actions, rewards, next_states, goals = [], [], [], [], [] for idx in batch: s, a, r, ns, g = self.buffer[idx] states.append(s) actions.append(a) rewards.append(r) next_states.append(ns) goals.append(g) return (torch.tensor(np.array(states), dtype=torch.float32), torch.tensor(np.array(actions), dtype=torch.float32), torch.tensor(np.array(rewards), dtype=torch.float32), torch.tensor(np.array(next_states), dtype=torch.float32), torch.tensor(np.array(goals), dtype=torch.float32)) def compute_reward(goal, achieved_goal, threshold): # 二值稀疏奖励:距离小于阈值算成功 dist = np.linalg.norm(goal - achieved_goal) return 0.0 if dist < threshold else -1.0这段代码里有几个细节值得重点说。第一,不要对整个episode一次性集中重标记,那会给经验池带来严重的样本相关性,正确做法是以“回合结束”为粒度将整段经验及其重标记版本一起入池,但采样时仍然用均匀随机采样,不对同一回合的经验做特殊分组,因为经验池足够大之后,相关性会被稀释。第二,compute_reward里的threshold是真正决定学习难度的参数,不能太大也不能太小,我通常先试0.05,再根据任务尺度调整。第三,observation里要包含goal信息,重标记只修改goal字段,不要改动state、action这些实际交互数据。
数据流上有个容易踩的坑:你必须在每个时间步都记录“当前实际到达的目标位置”(achieved goal),而不能只记录最终结果。原因很简单,重标记的目标是从轨迹中任意一个时间步的状态里挑出来的,这意味着每一步的achieved goal都得提前保存好。很多人的代码里只存了最后的achieved goal,结果future策略没法实现,只能用final,效果直接掉一截。
3.3 训练日志怎么读:成功率曲线的三个阶段
很多同学训练完只看finally的成功率,忽略中间过程,这会导致你无法判断算法是否真的在工作。我看了大量训练曲线后发现,HER加持下的训练过程一般会走三个阶段。
第一个阶段叫“漫无目的期”。训练刚开始的几万步,成功率曲线几乎是一条水平直线,在0附近微弱波动。这个阶段很正常,因为经验池里还没有足够多的成功轨迹,即使重标记了,智能体也在疯狂试探。不要着急,持续训练。
第二个阶段叫“快速上升期”。在某一个拐点之后,成功率突然开始飙升,增长率可能一天比一天高,这表示智能体已经意识到“达成某些目标”是有规律可循的,策略网络开始输出有意义的动作。这个阶段通常能持续到70%左右,是训练最让人兴奋的时期。
第三个阶段叫“平台期”。成功率升到某个平台后就不再明显增长,偶尔还会掉几个百分点。平台期的出现通常意味着剩余的目标难度过高或者策略已经收敛到局部最优。此时可以尝试调整replay比例、降低探索噪声、或者延长训练步数,但不要指望会有爆发式提升。
读曲线时有一条重要的经验:如果训练跑到中期,成功率还纹丝不动,先别急着调超参,先确认经验池里是否有足够多的重标记成功经验。你可以打印出经验池里“原始经验中reward不为0”的比例,通常这个比例非常低(小于1%),这才是正常状态。如果这个比例过高,说明环境本身其实没那么稀疏,那你根本不需要HER,直接用标准DDPG就能好。
4. 常见问题与排查技巧实录
4.1 目标状态分布失衡:HER失效的常见原因
HER最怕的一种情况是目标状态分布严重失衡。举个例子,某个任务中智能体的轨迹全部集中在一个很小的区域里,那么重标记出来的新目标也全是从这个小区域里选的,经验池里的成功经验高度同质化,模型泛化不出来。
我在机械臂抓取中遇到过这问题。目标位置被限制在一个非常窄的工作空间里,智能体很快就能让末端到达那个区域,但一旦初始位置变远,成功率立刻暴跌。排查思路很简单,打印经验池中重标记目标的三维坐标分布,如果发现它们挤成一个小团,说明探索严重受限。这时需要加大探索噪声、提高随机初始状态的多样性,或者干脆调整状态空间的边界。
4.2 特殊状态转移的陷阱:终止、越界与不合法状态
HER的重标记逻辑默认环境会持续跑完整个回合,然后从轨迹里挑一个状态当目标。但很多真实环境里存在终止条件和越界状态:机器人掉下悬崖、机械臂撞到限位、物体掉出桌面,这些情况下轨迹的“终点”往往是一个非法或极端状态。
把这样的状态重标记成目标,后果非常麻烦。举个例子,物体已经滑落桌面,轨迹末端的目标位置在桌面之外,你把它标记成“成功目标”,智能体学到的是“手伸到桌子外是成功”,完全违背任务意图。实际项目中,不能盲信重标记,必须额外写一个合法性过滤器:新的目标状态必须满足环境约束(在限位内、在有效区域内)才允许入池。
我习惯的做法是在add_episode里加一个判断,比如新目标位置的坐标是否超出工作空间的合法范围,超出就跳过不重标记。这个过滤器的代码不超过十行,却是避免“花两个星期学出来一个有毒策略”的关键。
4.3 调参实战经验:三个我踩过的坑与结论
第一个坑:k值贪多。一开始觉得重标记目标越多越好,直接把k拉到16,结果训练速度反而变慢,最后还出现严重震荡。原因前面说过,重标记目标太多、目标距离太远,模型根本拟合不过来。k值建议在4到8之间,先用4,看平台期表现再增加。
第二个坑:奖励阈值设得太大。当时为了任务能快速看到成功,把阈值放得很宽,结果智能体学出了“走个大概方向就算成功”的坏毛病,后面把阈值收窄,成功率反而大幅下降,因为策略已经无法精确控制末端。正确做法是直接设一个较小的阈值,让模型从训练第一天就适应高精度要求,虽然前期成功率涨得慢,但后期更稳。
第三个坑:忽略achieved goal的维度对齐。在对接自定义环境时,我一度把achieved goal设置成3维位置,但状态空间里的目标字段是4维(多了一个姿态角),导致compute_reward里np.linalg.norm报错或者算出来的距离完全不对。排查了半天才发现问题。这个很容易在接新环境时犯,一定要从第一行数据就开始验证数组维度是否对齐。
4.4 一个被低估的细节:探索噪声的初始值
前面提到的都是经验池和奖励设计层面的问题,最后补充一个容易被低估的细节——探索噪声的初始值。DDPG这类算法在训练开始阶段会用OUNoise或高斯噪声来增加探索力度。HER的作用是让稀疏奖励下的失败轨迹变得可用,但探索本身依然是产生多样性轨迹的前提。如果噪声太小,轨迹全是一条直线,重标记出来的目标也全在一条线上,效果自然不好。
实际做法是把探索噪声的初始幅度设置得比平时大一点,让智能体在早期能“乱跑”起来,等成功率进入快速上升期之后再把噪声幅度逐步降下来。我个人的经验是噪声初始幅度比默认值大0.5到1倍,收敛速度会有肉眼可见的提升。
写在最后
说了这么多,最后还是想强调一遍我自己的体会:HER并不神秘,它更像是给RL工程师提供了一副“失败滤镜”,让我们可以用另一种姿态审视那些没有奖励回报的轨迹。我以前遇到稀疏奖励任务的第一反应是拼命调奖励函数,后来发现重标记的思路往往能带来更根本的改善。
如果你决定在项目里尝试HER,我的建议是先用开源环境把代码跑通,再迁移到自己的任务上。迁移的时候多花点时间核对目标空间维度、合法状态边界、奖励阈值这三个地方,这三个细节比我前面提到的所有超参都更容易导致项目翻车。
对了,最后再分享一个小技巧:训练中途记得定期把经验池里的重标记目标分布可视化出来,画成散点图。你能直观看到智能体探索了哪些区域、哪些目标从没被真正学过,这在调试时比看十行训练日志都有用。