hindsight这个词,英文直译是“后见之明”,放在强化学习这个圈子里,它基本就指代一个绕不开的技术名词:Hindsight Experience Replay,中文通常译作“事后经验回放”。我第一次接触这个概念,是在处理机械臂推滑块的任务里——环境只在末端执行器进入目标区域时给一个+1的奖励,其余时刻全是0。训练跑了几十万步,策略纹丝不动,连目标区域的边都摸不到。当时一度怀疑是算法实现写错了,后来把HER加上去,成功率才渐渐从0爬到0.7以上。这篇内容,我想把它从原理到实现、从参数调到坑位全部拆开讲透,希望能帮到正在被稀疏奖励折磨的人。
适合看这篇的,大概有两种:一是刚入门强化学习,跑过几个demo,但一遇到“奖励几乎全是0”就不知道怎么办的新手;二是已经能跑通DQN、PPO之类算法,想提升样本效率、减少手工设计奖励函数的工程人员。前者的收获是理解HER为什么有效、怎么落地;后者可以跳过原理,直接去抄“实操”和“踩坑”部分的配置思路。我不打算堆公式吓人,尽量用大白话把逻辑讲明白,毕竟这些东西我在跑通之前,也是反复绕了很久的弯子。
1. 先从问题说起:稀疏奖励为什么难倒强化学习
1.1 机器人学不会,卡在信号太少
先还原一个典型的场景。假设我们要让一个两轮小机器人走到某个坐标点,这个坐标点每次开局随机生成。奖励设计成:如果机器人当前位置和目标点的距离小于某个阈值,给+1,否则什么都不给。这个环境的问题在于,一个episode里机器人可能随机走几百步,绝大多数时间它都不知道自己离目标还有多远,神经网络看到的状态和目标、动作的组合,和“最后能不能拿到+1”几乎没有任何可学习的相关性。因为成功轨迹出现的概率太低了,低到可以忽略不计。
我在实际项目里测过一个更极端的版本:在一个连续动作空间里,随机策略要走通一个长度为200步的目标抵达任务,概率大概是十万分之一量级。也就是说,指望靠随机探索碰出一次正奖励,然后让价值函数慢慢扩散,那在有限训练步数内基本就是绝望的。就算改用off-policy算法,把历史经验反复利用,经验池里依然连一条像样的“成功示范”都没有,Q网络怎么学都是瞎猜。
这种场景在真实机器人任务里非常普遍:拿盒子、推瓶子、插销钉,这些任务的成功判定都是离散的“成”或“不成”。如果我们非要等成功样本出现才给信号,那绝大多数项目根本跑不起来。很多人的第一反应是给奖励函数加“势场”——比如用欧氏距离的负值做稠密奖励。这方向没有错,但问题也很现实:每个任务都要重新设计距离度量,而且势场设计得不好,agent会学出绕路、震荡甚至故意卡在中间状态刷分的行为。
1.2 “事后诸葛亮”的核心直觉
HER的思路,其实特别像日常生活中的复盘思维。你练投篮的时候,如果只有“空心入框”才算成功,那么一次投歪了、砸到篮筐边缘的球,有没有信息量?当然有。你至少知道了“我把球的弧线压到这个程度时,落点是偏左的”,这就是一条有效经验。问题是,强化学习只认“目标是否达成”,所以这条经验会被当成无效数据丢掉。
HER做的事情,就是把目标换掉。既然这次没推进原定的目标点,但我实际到达了一个新位置,那我干脆把“到达这个新位置”定义为一次成功。原来的那串轨迹,从“失败尝试”重新标记成“成功路径”。这样一来,每条失败的episode里也能挖出大量带正奖励的transition。这个“事后换目标”的动作,对应在代码层面就是改一下transition里的goal字段,再按新目标重新计算奖励,仅此而已。
我当年第一次看到这个方法时,觉得这简直是在玩文字游戏。但仔细想想,你会发现它有个非常隐蔽的前提:我们不是在欺骗算法,而是在还原一个事实——这条轨迹确实“成功完成了某个目标”,只是那个目标不是我们最初指定的那个而已。策略学习的是从状态到动作的映射,它不关心目标是在执行前定的,还是事后补的。只要能学到“当目标在这些位置时,动作该朝哪个方向给”,它对原任务同样产生迁移能力。
1.3 对比几种常规解法,看HER的定位
在HER出现以前,业界处理稀疏奖励常见几类思路。一个是前面提的reward shaping,手工把连续距离、进度之类信息折算成奖励,问题是要为每个新任务单独设计,且容易引入局部最优。一个是课程学习,从简单目标开始逐步加大难度,思路很好,但你需要先设计出“难度递增”的序列,这在很多任务是另一个难题。还有一个是给探索加bonus,比如鼓励agent访问未见过状态,潜力不错,但常常在连续控制问题里参数敏感、效果不稳定。
HER的定位,恰好是另一个维度:它不改变探索方式,也不改造奖励函数,而是从现有失败数据里二次榨取价值。它不需要额外的环境交互,只需要在经验回放里做一次数据变换,计算开销极低,几乎可以说是白捡的样本效率提升。当然它也不是万能药,后面我会重点讲讲它只适用于哪一类环境,以及哪些场景用了反而坏事。
2. HER的原理拆解:怎么把失败重写成经验
2.1 目标条件化:把任务描述塞进状态里
很多初学者理解HER时卡在第一步:反复看到“goal-conditioned”这个词,却不明白和普通RL有什么区别。在传统RL里,状态s就是环境给的全部信息,比如小车的位置、速度;而在目标条件化设定里,观测变成状态加目标的拼接,也就是 o = (s, g)。策略写出来就是 π(a | s, g),意思是“给我一个状态和一个目标,我输出动作”。
这个设定是HER的前提。因为只有目标明确地出现在观测里,你才能事后修改它。如果你的网络输入压根没有goal,那你想把失败轨迹重新标记成“实现了另一个目标”,模型根本不认识这个输入维度,当然无从谈起。所以第一步检查你的环境:目标是否可采样、是否可编码进观测、奖励是否能由(s, g, a)重新计算出来。这三个条件缺一不可。
举一个具体的例子。我想做一个二维点机器人到达任务:状态是(x, y),目标是(gx, gy),动作是(x方向速度, y方向速度)。那输入给网络的向量就是四维: [x, y, gx, gy]。奖励函数定义为:如果 sqrt((x-gx)^2 + (y-gy)^2) < 0.05,给0;否则给-1。注意我用的是负奖励而不是+1,很多实现里用-1/0比用0/1更顺手,因为可以和“未成功”的惩罚对应起来,训练出来的Q值更稳。这个细节后面还会提。
2.2 事后目标重标记:公式化表达
直接看伪代码可能比看长段落更容易建立直觉。先看一个episode里原始transition的标准形式:(s_t || g, a_t, r_t, s_{t+1} || g)。这里的||表示拼接。原始经验里,r_t几乎全是-1,因为agent几乎从不在原目标上成功。HER做的,就是在这个episode结束之后,把其中的一部分transition重新写成另一份:(s_t || g', a_t, r', s_{t+1} || g')。
关键在于g'怎么选。最朴素的取法,是把episode最后一个状态当作新目标,也就是g' = s_T(或者取s_T里和目标相关的维度。有的环境状态里还包含机械臂关节角度,那你得把目标投影到目标空间里)。另一种取法是,在当前transition的时间步t之后再随机抽一个后续状态作为g',也就是从未来状态里采样。最后,重新计算奖励r' = reward(s_{t+1}, g'),计算方式和环境原本的奖励函数完全一样。
为什么说这个新transition是“干净”的?因为g'确实是在这条轨迹后续被到达过的位置,从s_t出发、执行a_t之后,物理上真实地接近了g'。那么把a_t当作“在目标为g'时的好动作”,是完全符合因果的。这不是伪造数据,而是在重新利用真实发生过的因果链条。下面是核心伪代码,我用Python风格写出来,方便你对照实现。
# 假设环境返回:obs为状态,goal是目标,act是动作 for episode in range(total_episodes): goal = env.sample_goal() obs = env.reset(goal=goal) episode_buffer = [] # 临时存放本episode的transition for t in range(max_steps): act = policy.sample_action(obs, goal) next_obs, _, done, info = env.step(act) reward = reward_fn(obs, goal, act, next_obs) # 原始奖励 # 原始transition先存下来 episode_buffer.append((obs, goal, act, reward, next_obs)) obs = next_obs if done: break # 每个transition都重标记K次 for idx, (obs, goal, act, reward, next_obs) in enumerate(episode_buffer): replay_buffer.add(obs, goal, act, reward, next_obs) for _ in range(K): # 从idx之后的时间步中抽一个状态作为新目标 future_idx = random.randint(idx + 1, len(episode_buffer) - 1) new_goal = episode_buffer[future_idx].obs new_goal = project_goal_space(new_goal) # 投影到目标空间 new_reward = reward_fn(obs, new_goal, act, next_obs) replay_buffer.add(obs, new_goal, act, new_reward, next_obs)这段代码核心逻辑非常短,但注意几个容易写错的地方。一是future_idx必须大于当前idx,不能抽一个“已经发生过”的状态当新目标,那就破坏了因果关系。二是new_goal最好是从状态里拆出来的目标分量。三是K次重标记不一定每次都用future策略,也可以一部分用final策略,混合起来效果更稳。
2.3 四种重标记策略与我的实测感受
原论文提出了四种从episode里选新目标的策略:final,也就是直接用最后一个状态当新目标;random,从整个episode的所有状态里随机抽一个;episode,从当前transition后面的状态里随机抽一个;future,每次随机延迟一个固定窗口再从后面抽。很多人看论文时容易把episode和future搞混,我实际写代码时也很纠结,后来干脆直接去看OpenAI开源代码,发现它们基本用的是future类实现,只是具体窗口参数有差异。
四种策略各有短长。final最稳定,但也最保守,因为一个episode的最终状态往往不是“有意义”的目标,尤其当episode很长、最后几步在乱走时,用它重标记可能导致目标分布和真实任务分布差很远。random问题更明显,它可能抽到一个和当前动作毫无因果关系的状态,新目标看似成功,实际上是“天上掉下来的”,这种伪样本会让Q值学习产生偏差。episode和future本质一样,只是future更强调时间顺序上的因果性。
我自己的实测体会是,future是最省心的默认选择。它既保证了因果性——新目标出现在动作之后,又不至于像final那样限制目标多样性。如果你正在搭环境,建议直接用future,K先设4,后续想优化再考虑混合。
2.4 与off-policy算法的结合原理
HER有一个硬性前提:必须配off-policy算法。为什么?因为经验回放里的transition经过重标记后,已经不再是“当前策略在当前目标下采集的数据”了。一个transition被重新打上“目标g'成功”的标签后,它对应的实际行为策略和价值函数都变了。这就是off-policy场景——用别人的经验来更新自己的策略。反过来说,像PPO这类on-policy算法,每一步都要用当前策略采到的数据做重要度采样,不允许拿旧数据直接更新,所以HER在它身上完全不成立。
实际项目里,和HER搭配最常见的是DDPG、TD3、SAC这类连续控制算法,离散动作环境也可以用DQN系列。但有一个点容易翻车:如果你用DQN,动作空间必须离散化,粒度粗了会导致即使重标记出了正样本,策略也很难输出足够精细的动作去逼近目标。我在一个二维点机器人任务里试过把动作离散成9档,结果成功率最高只到30%左右;换成连续动作的DDPG,同样的HER配置直接冲到80%以上。所以我的建议是,能上连续动作算法就上连续动作算法。
3. 实操:搭一个HER训练流程
3.1 环境选型与代码骨架
跑HER最快的方式,是根据你的实际环境类型选工具。如果你只是想快速验证算法有没有写对,强烈推荐先拿一个轻量级环境跑通,而不是直接上真实机械臂。Gymnasium里有现成的Fetch系列环境,比如FetchReach、FetchPush,都自带目标采样和稀疏奖励设置,配合MuJoCo物理引擎,是目前社区里验证HER的准标准环境。我自己早期也是拿FetchReach起步的,环境复杂度适中,训练时间可控,很推荐。
如果连MuJoCo都懒得装,也可以自己写一个几十行的点机器人环境,就是我前面说的那个二维到达任务。别小看这个简化环境,它麻雀虽小五脏俱全:有连续状态、连续动作、稀疏奖励、随机目标,全部具备测试HER的条件。先在它上面把训练曲线跑出来,再迁移到FetchReach,能省掉大量调bug的时间。
我个人的建议顺序是:先跑通简化环境,确认成功率曲线能往上走;再上FetchReach,感受真实高维状态下的训练节奏;最后才考虑真实机器人。因为HER真正能帮你省的是“设计奖励函数”的时间,而不是环境仿真调通的时间,前两步走扎实了,后面才有意义。
3.2 核心代码:HER重标记逻辑逐行讲
上一节已经给了简化伪代码,这里我再补充一点工程细节。实际项目里,我通常会把HER封装成一个独立的回放缓冲区类,和算法本体解耦。它对外暴露两个核心方法:add_episode,接收一整个episode的原始transition列表,负责内部做重标记;sample,从缓冲里随机抽batch供训练。这样写的好处是,算法部分完全不用改动,你只需要在每次episode结束后调用add_episode万事大吉。
有一个细节值得特别强调:重标记的目标并不总是“完整状态”。在FetchReach里,状态包含了机械臂每个关节的角度、角速度、末端执行器的三维坐标等等,但目标只关心末端执行器的三维位置。如果你直接把整个状态向量当新目标塞进去,维度对不上,代码直接崩。所以必须定义一个goal_projection函数,从状态里抽出和目标空间对齐的分量。这个小函数,我在前三个项目里每次都忘记写,然后被维度不匹配的报错狠狠教育一顿。
另外一个工程细节是,K次重标记时,不要全部使用future策略。我的做法是,对每个transition,以50%的概率保留原目标,再生成4个新目标,其中3个来自future窗口,1个来自final。这样重放缓冲区里既有“原任务”的真实经验,也有“事后任务”的伪成功经验,Q值估计会平衡得多。如果K全部拉满且都用future,训练前期缓冲区里几乎全是“伪成功”,Q值容易虚高,后面再校正就慢了。
3.3 关键超参与调参经验
HER本身引入的超参数不多,核心就两个:K和重标记比例。K代表每个原始transition额外生成几个重标记版本,OpenAI论文里用的4,我也建议从4起步。重标记比例在实现里通常体现为“每个episode里随机挑一半transition做重标记”,也就是0.5左右。如果K太大,伪成功样本过多,正负样本比例失衡;如果太小,HER的优势发挥不出来。我分别试过K=1和K=16,K=1几乎看不到提升,K=16早期Q值确实高,但成功率爬升反而更慢,训练曲线像是卡在一个虚高的平台上。K=4到K=8是安全区间。
奖励的形状也要配合算法选。我在前面用了-1/0方案,也就是未成功给-1,成功给0,这样智能体在优化过程中会倾向于累积更大的奖励值,目标函数是“最大化累积回报”。有些教程写成0/1,也就是成功给1,未成功为0,那目标函数就变成“最大化成功次数”。两种写法理论上等价,但在Q网络的数值稳定性和学习率的选择上会有微妙差别。我个人更习惯-1/0,因为初值都是负的,梯度方向更明确,不容易因为Q值全为正导致过估计。
学习率这类算法参数,按原算法的推荐值来就行,一般不需要因为HER单独调整。但有一点要注意:目标网络软更新率τ,HER场景下建议设得比默认略小一点,比如从0.005降到0.001。原因在于重标记后的目标分布是非平稳的,如果Q网络追踪太紧,会把“伪成功”样本的错误信号也牢牢记住,造成后续灾难性遗忘。追得慢一点,反而更稳。
3.4 训练监控与结果判读
跑HER训练时,最关键的一个判读指标是“按原始目标评估的成功率”,而不是缓冲区里的伪成功比例。很多人看到loss下降就以为训练正常,实际上HER的loss下降非常快,因为伪成功样本让TD误差变小了,但真实任务成功率可能纹丝不动。我习惯每5000步就暂停训练,重新用原始目标跑10个episode,统计真实成功率,画一条曲线。只有这条曲线在抬升,才说明HER真的在起作用。
还有一个辅助指标可以看:重放缓冲区里正奖励样本的占比。在纯稀疏奖励环境里,正常回放这个比例几乎是0;加了HER之后,这个比例会快速上升到10%到30%之间(具体取决于K和任务难度)。如果你的缓冲区正样本占比迟迟上不去,多半是重标记逻辑写错了,或者新目标投影函数出了问题。这个信号很有用,能帮你早发现问题。
在我跑FetchReach的经验里,训练初期的真实成功率会在0附近摇摆很长一段时间,看起来像是完全没动静。但如果你观察缓冲区正样本比例,会发现它在稳步上升。这是正常现象:策略必须先学会“去够那些事后目标”,然后才会慢慢迁移到原始目标上。熬过这段平台期,曲线往往会突然向上拐,这种“顿悟”现象在HER里非常典型,别在平台期就以为代码写错了而放弃调参。
4. 踩坑记录:HER容易翻车的五个地方
4.1 目标编码不一致导致训练崩坏
我最早跑HER踩的第一个坑,就是目标维度处理不一致。状态里包含目标的拼接顺序,在训练时是[x, y, gx, gy],但在重标记时我写成了把整个状态向量塞进目标位置,结果目标维度从2维变成了4维,网络输入size都变了,训练直接报错。这类问题还算好发现的。更难查的是,目标空间和状态空间的量纲不一致。比如状态里x的单位是米,取值范围0到1,目标如果是从另一个采样器里来的,取值范围变成了0到10,俩拼接起来等于给网络喂了一个分布极其扭曲的向量,训练起来非常折磨。
解法也很粗暴但有效:所有进网络的向量先做归一化。目标空间和状态空间分开归一化,再拼接。不要相信你的采样器“恰好”和状态空间一致,尤其是从真实环境转到仿真环境时,坐标系原点一变,量纲全变。我后来在任何HER项目里,第一步都是先打印几个样本的目标向量和状态向量,看一眼分布范围再决定要不要加归一化层。
4.2 重标记比例不是越大越好
新手最容易陷入的误区是“反正重标记是白赚的,那我每个transition都重标记10次,样本量不就大了10倍?”这话前半句对,后半句错。重标记确实不增加环境交互成本,但过度重标记会让经验回放缓冲区里的数据出现严重的分布偏移。想象一下,一个缓冲区里90%的样本都是“伪目标下的成功经验”,那Q网络学到的价值函数几乎完全是在拟合“事后目标”分布,真实目标下的价值估计反而被挤占了。训练初期看起来Q值又大又稳,等到中期需要真实目标信息时,网络却已经把真实目标对应的输入模式遗忘得差不多了。
我自己做过一组对照实验,K=1、K=4、K=16三档。K=4最终成功率最高,K=16前期学习速度快,但后期出现明显的成功率回撤,最终比K=4低了一大截。这个现象很好解释:伪成功样本在Q里灌了太多乐观偏差,而真实成功样本太少,无法把偏差掰回来。所以我的默认建议是K=4,如果想冲一冲样本效率,最多到K=8,不要无脑堆。
4.3 非平稳目标分布导致灾难性遗忘
这是HER一个很少被讲的深层问题。因为重标记的goal是跟着episode实际轨迹走的,所以随着策略进化,轨迹状态分布本身在变,导致重放缓冲区里的目标分布也在不断漂移。早训练阶段,agent经常乱逛,重标记目标多集中在某些随机区域;后期策略收缩了,新产生的目标又集中到另一片区域。如果整个回放缓冲区用的是一个固定长FIFO结构,旧目标的经验被逐渐挤掉,网络就会把旧目标对应的能力忘掉。
对付这个问题,我用的一个土办法是:经验回放缓冲区里,天然保留一部分“原始目标transition”,不做任何重标记,这样至少保证真实任务分布一直在训练数据里。这个比例不需要太高,20%到30%就够。另外就是选一个容量比较大的缓冲区,让旧经验的退场慢一些。OpenAI论文里的对数缓冲区大小配到10^6量级,是有道理的,不一定全是追求样本量,可能也隐含着缓解非平稳分布的作用。
4.4 与off-policy算法搭配的版本陷阱
虽然HER理论上能和所有off-policy算法结合,但不同算法对重标记样本的敏感度差别很大。我在实际跑DDPG和TD3时感受很明显:TD3因为有了裁剪式Q学习,对伪成功样本的过估计问题不那么敏感,稳定性比DDPG好很多。如果让我现在选,默认就是TD3或者SAC,DDPG只适合快速验证想法的场景,不适合做长期训练。
还有一个容易被忽略的陷阱是动作bound。HER重标记出来的成功样本,其动作是“真实执行过的”,这个没问题。但Q网络在更新时,输出的是当前策略的目标动作,如果策略网络的输出没有限制在环境动作边界内,就会出现“用一个非法动作去评估Q值”的问题,导致价值估计完全失真。这个锅不完全算HER的,但加了HER后,正样本密度高了,非法动作带来的偏差会被放大。我建议所有连续动作环境中,策略输出层后一定要接tanh缩放或者clip,并且确保Q更新时使用的目标动作也经过同样的缩放处理。
4.5 常见问题速查表
把我在项目里遇到的高频问题整理成一张表,方便你排查。这些问题的现象通常很相似,但原因各不相同,对照着查效率最高。
| 现象 | 可能原因 | 排查与处理 |
|---|---|---|
| 训练loss震荡发散 | 目标维度拼接错误、奖励值域异常 | 打印状态/目标维度与取值范围 |
| 真实成功率曲线长期为0 | 重标记目标投影函数错误 | 单独写单测验证new_goal的维度与范围 |
| 缓冲区正样本占比过低 | K值太小、future窗口实现错误 | 检查future_idx是否严格大于当前idx |
| 成功率前半段上升后半段回撤 | K过大、目标分布漂移 | 降低K、保留部分原始目标transition |
| Q值虚高但成功率低 | 伪成功样本过多导致过估计 | 换TD3/SAC、降低重标记比例 |
| 动作输出越界 | 策略输出未加bound | 输出层加tanh或clip处理 |
5. 这个思路还能往哪里延伸
5.1 HER的边界:什么时候不能用
HER这么好用,但也不是包治百病。我最想提醒的是,如果你的任务不满足“目标可描述”这个前提,HER就无从谈起。比如你要训练一个机器人倒水,让人工定义一个“倒水成功”的目标描述,可能还能做到;但如果是“把桌子收拾整齐”这种开放任务,目标空间难以定义,HER就完全用不上。另一个边界是,那些需要多步因果链的任务。HER能从失败轨迹里学到“最后一步操作接近了某个目标”,但如果任务要求“先拿起螺丝刀,再拧螺丝”,重标记只会学到“拧螺丝这个动作接近了目标”,但“拿螺丝刀”这步没有任何事后目标可以填补。这就得靠分层HER的思路,高层规划子目标,低层用HER执行。
此外,目标奖励函数必须是可计算或可查询的。如果你的奖励逻辑藏在仿真器内部、外部不可见,那就没法重标记。做真实机器人项目时尤其要注意:很多真实环境的奖励是靠人工判断打分的,HER没法实时重打分,也就不适用。这也是为什么目前HER的成熟应用还是集中在仿真环境里。
5.2 后续发展与我的个人建议
HER提出之后,社区在它基础上做了一堆扩展。比如把分层思想加进去的HIGHer,用更高层的策略生成子目标,再让底层策略用HER去实现;又比如Goat,在重标记时额外构造辅助目标,改善表示学习和探索效率。如果从实用角度出发,我不建议一上来就追这些花活。先把基础HER用熟练,理解了目标分布漂移和过估计这两个核心问题,再看这些扩展会事半功倍,因为它们本质上都是在缓解这两类问题。
工程层面的建议只有一条:先小后大,先简后真。任何新环境,都先做一个简化版本验证HER在生产环境里是否生效,再投入算力调大模型。我在真实机器人项目上栽过的跟头,绝大多数不是算法不行,而是环境建模的细节和算法假设不匹配。HER是个便宜又高效的方法,但它的有效性建立在环境符合目标条件化设定之上。把这个前提搞扎实了,它能帮你在稀疏奖励任务里少走好几个月的弯路。
最后再分享一个小技巧
在写这个项目的过程中,我养成一个习惯:训练的任何阶段,都定期保存一份“只含原始目标transition”的少量样本,用它们单独评估Q值。这个评估不参与训练,只看Q值是否随真实成功率同步变化。如果有一天你发现真实成功率在涨,但原始目标上的Q值却在掉,说明网络正在被重标记目标带偏,这时候就该考虑降低K值,或者在经验回放里增加原始目标样本的比例。这个小技巧听着简单,但在我后来几个HER项目里,几乎每次都帮我提前发现了训练跑偏的迹象,比盯着成功率曲线等结果要靠谱得多。也希望你跑HER的时候,不用再经历我当初那种“明明加了方法却完全没效果”的茫然期。