☰
HER:用事后经验回放破解稀疏奖励难题
2026/10/2 9:53:51 网站建设 项目流程

核心问题在于:智能体在稀疏奖励环境下,几乎学不到任何东西。你设计了一个机器人抓取任务,它伸手、够不到、失败,每次得到reward都是0,Q函数纹丝不动,策略就像撞了墙。你试了更长时间训练、更先进网络、更大的动作噪声,全都没用。这时候真正起作用的思路,反而是那个被低估的日常概念:后见之明。把失败轨迹重新解释为“朝着另一个目标取得进展”,从里面榨出学习信号,这就是我今天想跟你认真聊的Hindsight Experience Replay,也就是HER。

这篇文章适合谁?如果你正在做机器人控制、操作任务、导航等稀疏奖励场景,或者你对强化学习里“奖励设计”这件事感到头疼,HER是绕不开的一个关键方法。我下面会把它从动机、数学原理、代码实现到踩坑经验全部拆开讲清楚。

1. 为什么我们需要“事后诸葛亮”

1.1 稀疏奖励下的探索死局

强化学习的常规设定是智能体通过与环境交互获得奖励,然后优化策略。听起来简单,但凡只在任务成功时才给一个正奖励,其他时候全是0,训练就变成了一个惨淡的寻宝游戏。想一想机器人要学会抓起桌上的杯子:状态空间包含位置、速度、关节角、接触力,动作空间连续六维,杯子只有被抓起那一瞬间才给出reward=1。绝大多数探索都发生在杯子半径之外,智能体得到的奖励恒为0,梯度传回去了也无所谓,因为误差是0,什么都更新不了。

我第一次跑这种环境时,用的是DDPG加随机噪声探索,训练两百万步,成功率还是个位数。原因不复杂:在稀疏reward下,Q函数的Bellman目标基本都是0,目标网络也好、经验回放也好,只是在不断强化“什么都不会发生”这个认知。你换了更大的探索噪声,意味着更多随机动作,但回报依然是0。换句话说,随机运气在维度一高的情况下,几乎永远碰不到成功那个狭窄的集合。

这种局面里,人的学习逻辑根本不是这样。人学投篮的时候,第一次出手没投进,但你至少知道了你这一投偏左了还是偏右了;人学倒车入库,车虽然没停到位,但你知道了当前方向盘角度下,车会往哪儿走。失败轨迹本身包含极高信息量,只是传统RL把它当垃圾扔掉了。

1.2 失败轨迹里藏着金矿

HER的核心思想特别朴素:在一条没有达成预期目标的轨迹里,把轨迹实际到达的状态当作目标,重新解释为一条“成功轨迹”。

比如机器人伸手去抓杯子,原目标是杯子在桌上坐标(g1),但机器人动作一路偏右,最终手指停在了杯子右侧20厘米的地方。常规回放里这就是一条失败样本,要丢弃。HER会额外生成一条虚拟样本:把智能体最终到达的位置设为新目标(g2),在这条虚拟轨迹里,最后一步是成功到达目标的,reward给1。

这么做的直觉依据是:智能体虽然没碰着原目标,但它确实把一个高位状态成功转移到了另一个高位状态——从起点A移动到终点B,这件事本身已经证明它具备一定的控制能力。只是我们原本没把B当目标而已。

想想投篮。你投出的球砸中篮板右角,没进框。对“投篮命中”这个目标是失败;对“让球落在这个位置附近的点”这个新目标,你就是成功的。而这成功信息,比一片空白有用得多。

1.3 从多目标视角看清HER的本质

HER不是改你用的强化学习算法,而是改造经验回放池里样本的生成方式。它把单一目标任务,拆成隐含的多目标任务:在训练期间同时学习“朝原始目标去”和“朝其他任意状态去”的价值。如果模型学会了对任意目标g都估计出较准确的Q(s, a, g),那么它在原始目标g1上的表现也不会差,理由是一种泛化:不同goal对应的最优动作在状态空间里往往是邻近的。

这也解释了为什么HER一般不配Q-learning,通常配DDPG、TD3、SAC这类actor-critic结构,因为critic是带目标输入的多目标价值函数,更方便在goal维度泛化。后面我会细说算法兼容性问题。

提示:HER其实是一种“数据层面的课程学习”。它不改变奖励函数定义本身,只改变了采样时对过去的解释方式。这一点理解到位了,后面看代码和调参都顺。

2. HER算法核心机制拆解

2.1 目标重标记:一条样本变成多条

假设一个episode为:

(s_0, a_0, r_0, s_1, a_1, r_1, s_2, ..., s_T, a_T, r_T)

原始目标g不变,奖励r_t = reward(s_t, a_t, g),只有最终进入目标区域为1,其余为0。

HER在把样本放入replay buffer时,从当前轨迹的后续状态集中挑选一个状态s',把它当新目标重新定义这条样本:

g' = s'(或者后续状态序列中的某个状态,取决于变体)

新奖励则是reward(s_t, a_t, g'),这会导致轨迹中至少后半段里,接近g'时奖励由0变成1。

在OpenAI Baselines实现中,每个episode会额外生成k条重标记样本,k常见取4或者8。你可以理解为:同样一条经验,从“失败死路一条”变成“四到八条不同目标下的有效样本”,经验利用率直接翻好几倍。

2.2 未来策略与最终策略:选哪个状态当目标

不同论文里给出了几种选择策略:

  • final:只把每条轨迹的最终状态作为额外目标
  • future:从当前时间步之后的未来状态中随机选一个作为目标
  • episode:从当前episode包含的所有状态中随机选
  • random:从replay buffer里的所有状态中随机选

实际效果差距很大。random基本没用,episode偶尔有效,最稳的是future,其次是final。

为什么future比final好?final有一个问题:在轨迹很长、任务困难时,最终状态可能离起点只有一点点距离,目标几乎没有挑战性,会拖慢学习。future策略在每个时间步都能从后续状态里抽取目标,相当于自动构建了从“当前状态”到“未来某个状态”的虚拟任务,难度和原任务先验地相关,学习的步子更稳。

我建议你第一版直接用future,时间范围h取整个剩余序列。原因有两个:第一,实现简单;第二,baselines里大部分环境验证过的默认配置就是future,从简单环境迁移到你的自定义环境时,改动最少。

2.3 原样本要保留,不能全重标

一个容易犯的错:既然失败样本都被“重新解释”了,干脆每一条样本都重标,活得更轻松。这不行。你至少要保留一定比例的原始样本,否则critic会产生系统性偏差。为什么?

HER生成的新目标在逻辑上总是“这条轨迹可以到达”的。如果所有样本全是能到达的,Q值会习惯性乐观,策略会被推向过于自信的方向。真实世界不是每条轨迹都能到达任意目标。OpenAI实验里一般保留原始样本和HER样本各半,也就是重标概率p=0.5,或每一条原始样本额外生成k条重标样本,但原始样本也还在buffer里。

2.4 为什么on-policy算法不适用

你可能会想,PPO能不能直接用HER?答案是不太能。

on-policy算法的核心是当前策略采样的数据分布必须与更新时的分布一致。HER重标记后,新旧样本对应的“目标”和“奖励”全变了,策略还在更新中,数据分布条件对不上,重要性权重修正会变得非常棘手,强行用会引入巨大偏差。虽然学术界有一些扩展尝试,但工程上不要给自己挖这个坑。

所以实用准则是:优先搭配DDPG、TD3、SAC这类off-policy算法。我自己在抓取任务上用的是TD3加HER,稳定性比DDPG好不少,后面具体讲参数时再说。

3. 实操:把HER绑定进你的强化学习管线

3.1 环境与变量设计的准备工作

先看你的环境需要满足什么条件。HER依赖一个可观测的状态表示,而且这个表示要能够被当成目标。三个要点:

  1. 奖励必须是关于“当前状态与目标之间距离”的形式化函数,比如欧氏距离小于阈值给1,否则给0。你不能用纯游戏得分那种标量奖励,因为没法重标。
  2. 目标空间最好有明确几何意义。比如机械臂末端坐标,属于典型可重标状态。
  3. 目标与状态向着同一空间,至少能互换。这就是所谓的goal-augmented MDP。

如果你的环境满足这几个条件,就可以开始改了。

3.2 在Baselines风格代码里加入HER

下面这个例子基于OpenAI Baselines里的her实现思路,整理成一个可运行的最小结构。环境默认是一个类FetchReach环境,state包含observation和achieved_goal两部分。

import numpy as np class HERReplayBuffer: def __init__(self, buffer_size, k=4, future_horizon=None): self.buffer_size = buffer_size self.k = k self.future_horizon = future_horizon self.data = [] def add_episode(self, obs, actions, rewards, next_obs, original_goals): episode_len = len(obs) for t in range(episode_len): # 原始样本入池 self.data.append({ 'obs': obs[t], 'act': actions[t], 'rew': rewards[t], 'next_obs': next_obs[t], 'goal': original_goals[t], }) # HER重标记样本入池 for _ in range(self.k): # future策略:从[t+1, T]里随机选一个未来状态 if t < episode_len - 1: future_idx = np.random.randint(t + 1, episode_len) new_goal = obs[future_idx]['achieved_goal'] else: new_goal = obs[t]['achieved_goal'] # 根据新目标重新计算奖励 new_reward = compute_reward(obs[t + 1]['achieved_goal'], new_goal, p=1.0) self.data.append({ 'obs': obs[t], 'act': actions[t], 'rew': new_reward, 'next_obs': next_obs[t], 'goal': new_goal, }) # 超出容量就淘汰早期样本 if len(self.data) > self.buffer_size: self.data = self.data[-self.buffer_size:] def sample(self, batch_size): idx = np.random.choice(len(self.data), batch_size, replace=False) batch = [self.data[i] for i in idx] return (np.array([b['obs'] for b in batch]), np.array([b['act'] for b in batch]), np.array([b['rew'] for b in batch]), np.array([b['next_obs'] for b in batch]), np.array([b['goal'] for b in batch]))

你需要额外定义compute_reward函数,在稀疏设置里通常长这样:

def compute_reward(achieved_goal, goal, p=1.0): # 欧氏距离小于阈值则视为成功 dist = np.linalg.norm(achieved_goal - goal, axis=-1) return (dist < 0.05).astype(np.float32)

如果你自定义环境时发现阈值太苛刻,可以用距离的反向指数做密集奖励的替代品,但那样就不叫稀疏设置了,建议第一版还是直接上稀疏阈值。等模型有了基础能力,再考虑细调。

3.3 关键超参数选择

超参数这块我踩了不少坑,逐个说。

k值:每条原始经验额外生成的重标记样本数。OpenAI论文默认4,我自己在7自由度机械臂上试过8,成功率高了一截,但buffer容量和训练时间几乎翻倍。建议先4起步,看成功率曲线不涨再慢慢加。

重标概率:上面代码里我写的k是固定每个样本都生成k条变体,这已经隐式保证了重标样本占多数。经验法则是重标比例别超过采样比例的80%,不然critic对原始目标的Q估计会失真。

目标选择策略:future最通用。但如果你的任务是像“把A物体推到B点”这种,目标其实是位置或者位姿,未来状态直接当目标有时会引入不连贯的新目标,此时final更稳。多跑两组对比,用数据说话。

折扣因子γ:稀疏奖励环境我建议γ设置得偏高,比如0.98或0.99,让Q函数能看得更远。HER会生成很多“到达新目标”的合成样本,这时候高折扣因子会放大合成目标之间转移信号的作用,利于长视界学习。

3.4 把HER叠到DDPG/TD3/SAC上

假设你手头已有TD3算法类,那么改动点集中在三处:

  1. replay buffer换用HER版本
  2. critic输入拼接state和goal
  3. 训练时把goal送入Q网络计算

伪代码段:

# 训练设置示例(基于TD3) obs, act, rew, next_obs, goal = buffer.sample(batch_size) with tf.GradientTape() as tape: q_target = reward + gamma * target_critic(next_obs, target_policy(next_obs, goal), goal) q_current = critic(obs, act, goal) critic_loss = tf.reduce_mean((q_current - tf.stop_gradient(q_target)) ** 2)

我看很多人把goal当普通特征拼进observation一起输入,也行。但更稳妥的做法是保持状态与目标分开的输入结构。原因在于explicit goal结构能让网络更容易学到“目标变化时Q值变化”的规律,而不是在一堆无关位姿数据里指望网络自己拆解。

SAC加HER也完全可用。但SAC本身对温度系数敏感,稀疏环境下自动调温不总是稳定,建议给SAC的熵系数设一个下限,或者直接把α固定小值试一段。TD3的裁剪Q机制天然抑制了Q过估计,用HER时会更稳。

4. 常见问题与排查实录

4.1 为什么我的HER成功率一直不涨

通常第一个查的是缓冲区里有问题的reward。很多人自定义环境时,对“新目标”的compute_reward写错了维度,奖励全是0,重标样本等于没重标。你可以在训练代码里加一段debug:随机打印一条重标样本的目标状态与achieved_goal的距离分布。如果大部分距离小于阈值,说明重标机制正常,问题在其他环节。

另一个隐蔽原因:你重标概率设了0.9以上,原始样本太少,critic开始对任何目标都乐观。我的排查习惯是训练前两千步看Q值均值。如果Q值均值以肉眼可见速度往上蹿,但策略成功率还是0,基本就是重标比例失衡。

4.2 目标空间normlization问题

在机械臂任务里,目标可能是坐标、四元数、关节角度混合。四元数有符号歧义,(x,y,z,w)和(-x,-y,-z,-w)表示同一个姿态,直接当目标计算距离会得到虚假大误差,HER生成重标样本时会把这些混在一起,Q函数预测会崩。

解决办法是:把姿态坐标做规范化变换,只在计算目标距离时用特殊度量公式,而网络输入用标准化的R3向量。别小看这一步,很多论文效果复现不出来,问题就在这种目标表征的坑里。

4.3 HER反应慢,需要更长训练时间

HER提升的是样本效率,不是瞬时爆发。我实测FetchPickAndPlace环境,纯DDPG在200万步几乎学不会,加HER要在50-100万步之间开始看到成功率抬升,这是正常现象。不要拿50万步的成功率断言方法无效,耐心跑满200万步再下结论。

训练中期我会画两条曲线:一条是原始目标下的成功率,另一条是“重标目标命中率”。后者反映智能体在任意目标上的泛化水平,前者才是真正任务指标。如果后者高了前者没跟上,说明泛化有了,但策略对原始目标的偏向还不够,可以通过增大k或调高原始样本权重来修正。

4.4 与优先经验回放PER结合时的坑

很多朋友看到HER就顺手叠个PER,以为双剑合璧。实际效果经常打折扣。PER的优先级基于TD误差,而HER合成样本的TD误差天然比原始失败样本小,合成样本优先级一直低,都被排除在更新外。最终效果:相当于低效版HER。要么先跑纯HER,别叠PER;要么叠时给优先级加一个上限,别让偏差差太大。

4.5 多目标与多任务扩展的注意点

HER天然适合多任务场景,但目标空间维数上来后,计算复杂度随之上升。同样是抓取,从单目标变成多目标,网络参数可能就得多两三倍。我之前试过把多个目标用另一个子网络编码成embedding再送给critic,能显著降低参数量,值得尝试。

另外,goal-conditioned reward设计成稀疏阈值时,阈值越小越难学,阈值太大又丧失精度。0.05米在抓取任务里够用,但如果是精细装配任务,可能得用分段奖励:

if dist < 0.01: reward = 1 elif dist < 0.05: reward = 0.5 elif dist < 0.1: reward = 0.1 else: reward = 0

注意这种分段奖励仍满足重标条件,因为它还是“状态与目标距离的函数”,所以不影响HER使用。

4.6 我自己的超参起手盘

最后分享一套在我多个环境中都能跑的起始配置,方便你直接抄作业:

参数推荐值说明
k4重标样本倍数,前期够用
重标策略future范围取剩余全部时间步
γ0.98稀疏任务偏高
回放容量1e6机械臂任务我用到2e6
探索噪声0.1DDPG类;TD3可以0.1起步
critic学习率1e-3Adam,必要时降到3e-4
actor学习率1e-3Adam,必要时降到3e-4
批量大小256训练更稳
训练频率每个时间步更新1次偏激进,看你算力

这套盘子在FetchReach上,一般在60万步内能到接近100%成功率。到了你自己环境的迁移过程中,如有异常大概率出在目标表示和奖励函数上,而不是算法本身。

我在多个机器人仿真环境里用过HER之后,一个深刻的体会是:很多问题不是模型不够强,而是我们把“成功”定义得太狭隘,导致学习信号为零。重新定义目标后,那些看似废掉的轨迹反而成了最宝贵的训练数据。这一点不仅在强化学习里适用,在设计真实系统的指标、规划项目里程碑时也成立——失败数据不全是噪音,关键是你愿不愿意把“失败”重解释成“朝另一个目标的前进”。HER只是这件事在算法层面的一个极致典例。希望这套思路能帮你把稀疏奖励的硬骨头啃下来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询