开头部分我来处理:
折腾了大半年强化学习,手里一堆机器人控制项目,最让我头疼的不是网络结构,不是超参调优,而是那个绕不开的稀疏奖励问题——你想训练机械臂把积木推到指定位置,可它试了几万次,一次都没成功过,每次回传的奖励都是0,梯度全靠盲人摸象。后来同事塞给我一篇论文,标题里就一个词:hindsight。这个名字我一直记到现在,因为它背后的思路实在巧妙——你不是只能从成功里学到东西,你完全可以学着从失败里偷师。说白了,hindsight代表的就是强化学习里鼎鼎大名的Hindsight Experience Replay(事后经验回放,业界都简称HER),也是我做稀疏奖励任务时用过的、最立竿见影的一招。
这篇文章不打算给你复述教科书,我会按照自己做项目的思路,把HER的原理、参数选择、实现细节和踩坑心得整个过一遍。适合正在搞机器人控制、RL算法落地、或者被奖励函数折磨得想换方向的研究生和工程师阅读。看完你至少能回答三个问题:HER到底改了什么东西?它为什么有效?以及你自己搭一个HER训练管线时,哪些地方最容易翻车。
1. 内容整体设计与思路拆解
1.1 hindsight是什么:一个AI概念的前世今生
先把这个词从根上扯清楚。hindsight翻译过来是“事后聪明”或“后见之明”,在人类决策里通常带点贬义——事情都发生了你才说应该怎么选。但在强化学习领域,这个词被玩出了新花样,它变成了一个极其高效的学习信号。
2017年NIPS上,当时还在OpenAI的Andrychowicz等人把后见之明放进经验回放里,提出了Hindsight Experience Replay。这可不是换了个术语包装,而是从机制上重新定义了“经验”的价值。传统经验回放里,一条transition长这样:(状态、动作、奖励、下一状态),它被存进缓冲区后,训练的时候就按原样采样回放。它的隐含前提是,这些经验对应的目标都是固定死的,这个轨迹到底“好”还是“坏”,奖励已经盖棺定论了。HER不改这个结构,但它在存储和回放之间加了一道工序:为原来的失败轨迹换一个能达到的目标去解释。
我当年第一次看到这个思路时,脑子里蹦出来一个特别生活化的类比。一个人练投篮,手感和运气都不在线,十个球全砸在篮筐右侧偏上的位置。传统RL会觉得这就是十次失败,不值得记录。但HER的思路是:如果我现在不是要“投进篮筐”,而是要“投到某个点”,那么我这次投出的球实际落在哪里,就可以重新设定为这次投篮的目标。这样一来,十次全部“命中”了。它把目标从“固定端点”变成了“现实落点”,用重新定义目标的方式,从失败里榨出训练信号。机器人有没有达成你一开始想要的目标?没有,但那不重要,重要的是机器人确实把末端执行器移到过一个特定位置,这个“做到过的状态”就是很好的训练素材。
1.2 为什么要做HER:稀疏奖励环境下的学习困境
你可能要问了,道理是不错,但传统RL真的没法处理稀疏奖励吗?答案是理论上有办法,但实际上收效甚微。稀疏奖励任务里,最常见的问题就是探索不足,代理在没有任何反馈的环境中像无头苍蝇一样乱撞,它不知道自己做得到底好不好,很多时候策略网络的参数压根得不到有效更新。
拿我最常用来验证算法的栅格寻路来举例。一个10乘10的网格,起点在左下角,目标在右上角,每一步的奖励都是0,只有到达终点才给1。用传统DQN去跑,一开始没有一条轨迹能到达终点,Q值的更新全靠想象,探索的随机性让轨迹散得满天飞。而稀疏奖励意味着这些轨迹之间没有区分度,好的动作和坏的动作拿到的回报都是0,你无法告诉代理哪一步走对了、哪一步是撞墙的。
这时候你可以尝试奖励塑形(Reward Shaping),比如把每一步的奖励设计成距离目标点的远近变化。但奖励塑形这个事,说多了都是泪——你辛辛苦苦设计的奖励函数,代理大概率会利用你的设计漏洞去刷分,而且不同任务之间没法复用。HER则是对机制层面的改进:不需要你额外设计中间奖励,而是通过“如果目标是这个,那这条轨迹就是成功的”这种逻辑改写来凭空制造正样本。这相当于是把“事后诸葛亮”变成了一种高密度reward的生成器。
比较一下几种常见思路就清楚了:
| 方案 | 解决思路 | 优点 | 陷阱 |
|---|---|---|---|
| 奖励塑形 | 设计稠密中间奖励 | 训练快、信号密 | 易被钻空子、泛化差 |
| 课程学习 | 从易到难逐步训练 | 样本效率高 | 难度曲线难设计 |
| 好奇心驱动 | 以外界新奇度作激励 | 无需任务先验 | 容易沉迷无意义状态 |
| HER | 用事后目标改写轨迹 | 无需额外工程 | 依赖多目标结构 |
说实话,这些方法并不是互斥的,你在实际项目中完全可以把HER和课程学习叠着用。但如果你只有精力做一个改动,我强烈建议从HER开始,因为它的侵入性最低——不需要重建环境,不用重新设计奖励,只需要改经验回放那一个模块。
1.3 方案取舍:HER适合解决什么问题,不适合解决什么问题
HER能火的另一个原因,是其适用范围足够清晰。它针对的是这种任务:环境里存在一个可以独立定义的目标,这个目标可以表示成状态空间里的一个点或一个区域,并且代理每次拿到的目标都不相同。机器人操作任务(推箱子、到达某个位置、开门)、导航任务(走到指定坐标)、体力活儿式的游戏(移动一个物体到指定位置)都满足这个条件。
反过来,如果你的任务没有明确可参数化的目标,比如“学会走路不要摔倒”,或者目标是固定的、状态空间里没有对应的目标向量可供改写,那HER的效果就很有限。前一种情况你更适合去搞奖励塑形或者模仿学习,后一种情况可以考虑改用其他探索增强手段,比如NoisyNet或者参数空间噪声,因为它们不依赖目标结构也能发挥作用。
我在团队内部做技术预研时总结过一个很粗略的经验:如果一个RL任务你写了两个版本——稀疏奖励版本和一个精心塑形过的稠密奖励版本,并且稠密版本训练得快但容易过拟合到奖励函数的形态上,那这个任务就非常值得用HER来替代奖励塑形。HER的另一个隐性优势是鲁棒性比较强,因为它是自监督性质的,不依赖任务设计者对“怎么走是好的”做主观假设,所以模型迁移到新环境时不容易出现奖励语义失效的情况。
2. 核心细节解析与实操要点
2.1 HER的核心机制:目标改写到底改了什么
HER最核心的一个操作叫作目标改写(goal replay)。为了让你理解得更准确,我先把相关概念捋一遍。
在多目标强化学习(Multi-Goal RL)的设定里,一个transition不仅仅包含状态、动作、奖励这些常规元素,还包含这个回合的目标g。比如机械臂任务中,g是目标位置的坐标;导航任务中,g是目标经纬度或网格编号。代理每回合会拿到一个随机采样的目标g,它的策略网络输入是观测o加上目标g的拼接,输出是动作概率分布。每做完一步,环境会根据当前状态s和初始目标g返回一个奖励值。理论上如果你用稀疏奖励,那么只有当s满足目标检测条件时奖励才为1,其余情况一律是0。
HER的改写操作分三步:第一步,正常跑完一个回合,把这个回合里的所有transition存成一条完整轨迹;第二步,拿到这条轨迹的最终状态s_T;第三步,以一定的概率,把轨迹中的原始目标g替换成s_T(或者s_T附近的状态),然后重新计算每个时刻的奖励和Q值目标。改写后的轨迹中,最后一步的状态正好等于新目标,因此这条轨迹结束时奖励是1,中间的某些步骤甚至也可能在接近目标时提前触发奖励信号。
这个“重算奖励”的步骤特别容易被新手忽略。你只是把目标向量换掉了,但如果奖励函数没用新目标重新算一遍,回放出来的每个transition还是旧的稀疏奖励,那HER等于白做。我见过不少同仁问我“为什么加了HER还是没收敛”,检查一下代码,好家伙,写的是把原来的reward直接存进memory,目标改了奖励没改,所有成功信号依然湮没在零奖励的海洋里。这是最基础但也是最容易翻车的点。
2.2 关键设计选择:目标怎么选,奖励怎么定
HER也不是随便改写都能加速的,有两个设计维度对你的性能影响最大:一是“把哪些状态改写为目标”,二是“改写后的奖励怎么确定”。
先看目标选择。最常见的策略是从同一条轨迹的future状态里选一个作为替换目标,这就是大名鼎鼎的future采样策略。为什么要选future而不是past呢?直观原因是,轨迹的后半段更接近最终成功状态,用它做目标,最后几步的奖励更容易是正数,能更直接地制造正向经验。如果非要用过去的某个状态作为目标,你这回合还没走到那个状态呢,往后每一步能不能到达就是个未知数,奖励信号可能还是稀疏的。
还有一个细节:实际工程中不建议把最终状态s_T直接作为新目标,因为它太“极端”了,通常建议加一点噪声或者缩放,目标取s_T或其附近的一个邻域,让代理学到的不只是到达具体那个点,而是到达那个点“附近”区域的能力。这一步看着无关紧要,但在连续控制任务里很关键。我去复现别人的代码时就看到过,有人用s_T做目标时效果不差,但换成“s_T加上一个均值为零的小高斯噪声”后,成功率曲线的震荡明显变小了。原因在于连续空间中严格到达一个点几乎不可能,目标稍微放宽一点,策略就学得稳得多。
再来说奖励计算。建议你在实现HER的时候把奖励函数单独封装成一个函数,输入是“当前轨迹的实际状态”和“要考虑的目标g”,输出是标量奖励。你在环境采样阶段会用原始目标调用一次,在存储回放经验时用改写后的目标再调用一次。你最不想看到的就是两处奖励函数逻辑不一致,那会直接让训练过程陷入自我矛盾的循环。好的工程实践是写一个独立的函数get_reward(achieved_goal, desired_goal),两边都调它,谁也不许偷偷改。
2.3 HER和网络结构的适配要点
很多资料忽略了网络结构这个维度,但我的经验是:HER和“可分解的输入结构”配合起来效果最好。什么意思呢?多目标任务里,观测o里通常包含一段“当前已实现的目标”(achieved goal),比如机械臂当前末端位置。在HER的框架里,你想让代理明白“目标也是输入的一部分”,而不仅仅是监督信号。
这时网络设计上推荐用两个独立的分支:一个分支编码当前观测,另一个分支编码目标,然后在中层特征处拼接起来。这样做的原因是:观测和目标承载的信息语义不同,过早地把它们拼成一个向量容易让网络把二者混淆。我在做机器人推箱子任务时,把状态编码和目标编码分开后,训练速度大概提升了百分之二三十,最关键的是收敛后的波动更小。这是从经验里尝到甜头才回头找理论解释的——你希望网络学到的是当前状态和目标之间的相对关系,而分离编码给了它足够的自由度去表达这种关系。
如果你是拿现成的RL库(比如Stable-Baselines3)来改,它会默认把observation和desired_goal拼起来作为模型的输入,这也能跑,但你得知道它在架构上是吃了亏的。想榨干HER的潜力,稍微改一改网络前向逻辑是值得的。
3. 实操过程与核心环节实现
3.1 HER训练管线全流程拆解
先上一段核心伪代码,这是我自己实现HER时的骨架,简化掉了很多工程细节但保留了关键逻辑:
def her_train(env, agent, replay_buffer, her_ratio=0.8, total_timesteps=1_000_000): obs = env.reset() episode_transitions = [] for step in range(total_timesteps): action = agent.act(obs) next_obs, reward, done, info = env.step(action) transition = { "obs": obs, "action": action, "reward": reward, "next_obs": next_obs, "goal": env.goal, # 记录这个回合的原始目标 "achieved_goal": info["achieved_goal"] } episode_transitions.append(transition) obs = next_obs if done: final_state = info["achieved_goal"] # 原始经验正常存入 replay_buffer.store(episode_transitions) # 额外把改写后的经验存入,her_ratio控制改写数量 for _ in range(int(her_ratio * len(episode_transitions))): fake_goal = sample_future_goal(episode_transitions) # 从后续状态里采 her_transitions = replay_target(episode_transitions, fake_goal) replay_buffer.store(her_transitions) # 从回放池里采样更新策略 batch = replay_buffer.sample(batch_size) agent.update(batch) obs = env.reset() episode_transitions = []这个流程里有三个环节最值得说道说道。
第一个环节是env.goal的存储。很多人在环境里把goal设成内部状态,却没把goal放进transition里,这导致后面改写目标时找不到原始目标去替换。你需要确保每条transition都同时带有当时回合的原始目标、当前已实现目标(achieved goal),这对HER来说是输入数据的灵魂,缺了哪个都没法干活。
第二个环节是sample_future_goal的实现。最常见实现是:从当前时刻之后的所有已经达到过的状态里随机挑一个。如果你故意偷懒选了final状态也没事,但如前面所讲,效果可能会稍微差一点。我通常的做法是:从这条轨迹的[t + 1, T]区间内均匀随机采样一个状态,这样既保证了目标在逻辑上“尚未达到”,又给了代理不同的难度梯度。
第三个环节是replay_target,它要做的事情包括:把transition里的goal字段替换成新目标;再用新目标重新调用一次奖励函数;同时更新Q值目标里的next_goal。如果你用TD3或者SAC这类算法,critic的输入里包含“目标”这个分量,每个地方的goal都得一并替换,漏一个你的训练信号就乱套了。
3.2 目标重标注的两种物美价廉实现
目标重标注听起来玄乎,但真正实现起来无非两种套路:整条轨迹翻译型、逐步替换型。我建议你在脑子里把这两种都过一遍,它们对应代码组织的不同风格。
整条轨迹翻译型就是把整个回合的所有transition集中处理:拿出原来的(goal, achieved_goal)对,把goal换成新的,重新计算每个transition的reward,然后作为一个整体推入缓冲区。这种做法的优点是实现简洁,你只需要一次遍历就能处理一整条轨迹。缺点是内存占用比较大,因为改写后的经验也算新经验,原来那份失败经验还需要保留吗?实际上不必,你可以选择只存储改写版本,也可以原始+改写都存,我在工程上常见的是两者都存,但如果你内存紧张,只存改写版也能跑。
逐步替换型的思路是:在轨迹运行过程中实时地维护一个“候选目标池”,每个transition存进去的时候,顺便检查池子里有没有可以替换的目标,如果有,就直接同步生成改写后的transition并一起存入缓冲区。这种写法和你跑回合的顺序耦合更紧密,代码风格上更像流式处理。它适合那种内存规划比较严格、不想等到回合结束再处理一遍的场景。
就我个人的偏好来说,我更喜欢整条轨迹翻译型,因为它的逻辑隔离得最清晰。你采样是一层,修改目标是单独一层,排查bug的时候直接看目标改写函数就好了。另外从流程上看,整条处理方式也很自然地把“HER改写”和“经验入库”解耦了,万一以后想换别的目标改写策略(比如改成给定概率采样),改动面很小。
3.3 完整参数配置实例:机械臂到达任务
为了让实操参考更具体,我拿一个经典任务——FetchReach(机械臂末端到达指定位置)来给你一份可以直接抄作业的配置。
环境状态维度不用想太多,关键是四元组:observation由机器人关节角度、末端位置、目标位置这些字段拼接而成。我在实践中一般用:
观测向量: - 机械臂关节角 (3维) - 机械臂末端位置 (3维) - 目标位置 (3维) 动作空间: 3维连续控制,机械臂末端速度增量 奖励函数: 稀疏奖励,末端位置与目标距离 < 0.05 时返回 0,否则返回 -1注意这个奖励设计:非成功每一步给-1,成功给0,这种写法在RL里很常见,因为它把“越久没成功惩罚越大”的直觉编码进去了,其实还是等价于稀疏奖励,但比全部给0多了一点时间压力。
在这种配置下,如果你用DDPG+HER作为基线,参数可以这样初始化:
| 参数 | 推荐值 | 解释与调参建议 |
|---|---|---|
| 回放池大小 | 1_000_000 | 足够大才能容纳HER改写后的额外经验 |
| 批大小 | 256 | 批大小不能太小,因为HER经验里有很多是“假成功”样本,需要足够多样本去稳定Q估计 |
| HER改写比例k | 4 | 每条原始轨迹额外生成4条改写轨迹,这是论文常见值 |
| 未来采样策略 | future | 从当前时刻到回合结束之间均匀随机采样目标 |
| 优化器 | Adam(1e-3) | 先用默认学习率,不稳定再降到3e-4 |
| 策略噪声 | 0.2 | 探索噪声,任务简单时可以略高 |
| 目标成功阈值 | 0.05 | 调大则更容易看到成功信号,但策略精度会下降 |
这段配置里最值得留意的是HER改写比例k。k不是越大越好。k大确实会带来更多正样本,但也会让经验池里的“假成功”比例过高,策略容易沦为“只会到达平均状态”的平庸策略。k=4是论文里的常用值,但具体任务里你大可以试试k=1和k=8,画出训练曲线对比一下再定。我自己跑任务时发现,过于复杂的目标空间里k=4偏向保守,k=8反而会掉点,简单任务又反过来,所以别死守一个值。
3.4 训练中的性能观察指标
训练HER任务时,不要只盯着最终成功率一个指标看。我建议额外记录三个东西:改写后经验占回放池的比例、每个回合的“隐含成功率”(即最终状态距离目标小于阈值的比例)、以及Q值的均值变化。
改写后经验占比这个指标很有意思。如果你发现占比太高(比如超过一半还多),说明你的原始轨迹成功率实在太低,几乎全靠HER在生成正样本。这未必是坏事,但也提示你可能需要更强探索。反过来,如果占比很低,说明轨迹已经经常成功了,这时候可以减小k值,把训练重心真正放到“优化现有成功轨迹的边上”。
Q值均值的变化则是用来判断训练是否“自欺欺人”的。HER比较容易出现一种情况:代理学会了改写目标的捷径,但真实目标下的成功率还没起来。你观察Q值时,如果真实目标下的Q值一直很低,但改写目标下的Q值已经冲到很高,那模型其实已经“理解”了任务结构,只是欠一点真实目标下的探索机会,这时往往稍微调整目标采样分布就能救回来。这点心得在调参过程中帮助我很大。
4. 常见问题与排查技巧实录
4.1 训练不收敛,loss不降反升怎么办
这大概是所有人遇到的第一座山。单看loss数值上升其实不用太慌,因为强化学习的loss又不是监督学习那种单调递减的玩意儿,特别是多了HER这种改写机制后,Q值目标本身就在变,loss波动大是常态。真正需要警惕的是“成功率曲线一条直线,完全没有抬头的迹象”。
如果出现这种状况,我建议按下面顺序排查。第一步,看你的奖励函数重置了没有。具体来说,HER改写目标后有没有重新计算reward,这是最常见的低级错误,一行日志就能查出来。第二步,检查目标与观测的拼接维度是否对齐——一旦目标维度没有传进网络,代理就完全不知道自己在追什么目标,训练自然废掉。第三步,看看经验池里真正的成功样本密度。你可以打印一下每个batch里“success信号”的占比,如果连0.1%都不到,那说明HER没有真正工作,或者你future采样的实现有bug。
之前帮一个读者看代码,他跑了三万步,成功率纹丝不动。我让他打印回放池里“来自改写轨迹”的样本数量,结果显示为零。为啥呢?他的实现里只存储了原来的transition到缓冲区,根本没有任何改写后的transition被写进去。这行代码补上,两千步之后曲线就开始抬头了。很多问题是逻辑对了但工程漏了一环。
4.2 不同future采样策略的“玄学”差异
前面提到了future、final、episode、random四种采样策略,很多刚接触HER的人都想知道到底该用哪一版。我直接把我做实验的感受写出来。
| 策略 | 做法 | 我的实测体会 |
|---|---|---|
| final | 只用最终状态做目标 | 简单粗暴,收敛速度最快,但学到的策略偏向于“冲终点”,对中途状态理解不足 |
| future | 从未来某时刻状态采样 | 综合表现最好,兼顾了探索多样性和成功信号密度 |
| episode | 从整个回合里任意时刻采样 | 相当于future的弱化版,因为可能采到过去的点,信号密度低一些 |
| random | 从整个状态空间随机采样 | 几乎没有正向信号,不推荐单独用,只能当数据增强用 |
未来采样时还有一个细节:说“均匀随机”其实也不是完全均匀的,我倾向于给更靠后的时刻稍高的采样权重,因为离成功近的状态改写成目标后,更有可能出现“未来几步真的逼近目标”的正向transition。你可以理解成这是给代理创造了更多“差一点点就成功”的练习机会,而恰恰是这些次数积累起来最能推动技能完善。
4.3 经验池污染问题与目标表示的坑
HER用久了,你会开始关注经验池污染。改写轨迹确实制造了大量正样本,但也引入了噪声。假如你的目标表征和状态表征混在一起,比如机械臂任务里把末端的vel也当成了goal的一部分,那你去改写目标时就是把“只能控制当前速度”这种瞬时信息当成了最终状态来用,改写出来的目标相当于在给代理出无解题。这种任务不出错才怪。
所以我的自查习惯是:每换一个任务,先打印几条改写后的transition,肉眼看一看里面的目标值是否合理。如果目标是位置,它的单位应该和真实坐标一致;如果目标是状态向量,它能和原始目标对齐,而不是混入了一些无关维度。这个过程很土,但非常有效,强化学习调试就是这样,很多时候靠的不全是理论,而是你到底愿不愿意把脏活做细。
另外一个坑是目标表示的归一化。连续控制里目标向量各个维度的量纲可能差很多,比如位置数值在0到1之间,但角度数值可能在-3.14到3.14之间。你不做归一化就把它们拼一起,网络会不自觉地去拟合数值大的维度,这不是HER特有的坑,但在HER高频改写目标的情况下会被放大。解决办法很简单:目标向量做一次平移缩放,把每个维度规范到单位区间左右,训练稳定性明显不一样。
4.4 调试清单:检查代码时逐项核对
每次训练异常的时候,我习惯把那几个点挨个过一遍,就像飞行员起飞前照单检查一样。分享给你,你可以直接复制成README放在项目文件夹里。
- [ ] 经验池里是否有改写后的transition,数量是否大于0
- [ ] 改写的目标向量维度与原始目标一致吗,有没有多出或漏掉维度
- [ ] 每条改写后的transition,其reward是否用新目标重新计算过
- [ ] 动作网络输入层中,目标分量是否被正确传入,而不是只在critic里当监督信号
- [ ] 当回合结束状态s_T与某个历史状态重复时,是否有去重或加噪声处理
- [ ] 采样batch时,原始经验与改写经验的比例是否失衡,尤其注意改写经验太多的情况
- [ ] 目标向量的每个维度都做过归一化或缩放到合理数值范围了吗
- [ ] 测试阶段有没有用真实目标做评估,而不是沿用训练时的改写目标
这张清单救过我太多次。说实话,90%的HER“不生效”问题都出在这几个工程细节上,真不是什么高深的数学原理出错。先把这些低级错误排除干净,再往网络结构、超参数这些更抽象的方向研究,才是正经的调试顺序。
唠到这儿基本把HER从原理到工程整个捋了一遍。我个人的体会是,HER是那种“第一次听说觉得平平无奇,亲手实现后才拍大腿”的技术。它的厉害之处不在于增加多少复杂的网络结构,而在于对“失败经验”这一资源的重新审视。做强化学习项目这些年,很多让我头疼的稀疏奖励问题并不是真的无解,而是我一直在等那些“成功样本”出现;而HER提醒了我,真正的解法往往藏在你已经走过的路里——哪怕那条路当时并未通往你预设的终点。如果你正在被稀疏奖励折磨,不妨花一个下午把这条hindsight的路线在你的代码里补上,等训练曲线开始抬头的时候,你会回来感谢那次失败。