☰
Hindsight Experience Replay:用事后重标注破解稀疏奖励难题
2026/11/2 5:20:51 网站建设 项目流程

如果你做过机械臂抓取这类强化学习任务,大概率遇到过这种让人挠头的场景:环境跑了几十万步,奖励函数看着也没问题,智能体就是死活学不会,一直在原地打转。问题往往不在算法本身,而在于任务太稀疏——试了几千次才碰到一次成功,智能体根本拿不到正向反馈来调整策略。这时候很多人的第一反应是改奖励函数、加课程学习,或者直接上模仿学习。不过有一个思路非常巧妙且非常值得一试,那就是标题里的这个词,hindsight,也就是强化学习领域里大名鼎鼎的 Hindsight Experience Replay,简称 HER。

这个算法非常适合两类学习者:一是正在做机器人操作、导航类连续控制任务的工程师,二是被稀疏奖励问题折磨到最后只能靠手搓 reward shaping 的 RL 炼丹师。它能把你的失败经验重新变成有效训练数据,让智能体在近乎没有正反馈的情况下也能慢慢学会目标。这篇文章我会从算法本身的语义说起,把它的核心机制、代码实现、调参经验完整走一遍,尤其是那些论文里不会直接写清楚的坑,我都会摊开来讲。

1. 项目概述:hindsight 到底在解决什么问题

1.1 稀疏奖励环境下的“学不会”困局

先还原一个最典型的问题场景。假设你有一个七自由度机械臂,任务是抓住桌面上一个随机位置的小方块。整个 episode 结束后的奖励只看一件事:方块是否被抓起来了。抓起来了给正奖励,没抓到就一直是负分或者零分从头到尾。这在代码里实现起来非常干净,效果却非常残酷——在完全没有先验知识的随机策略下,机械臂连“靠近方块”都很难做到,更别说成功抓取了。一千次 rollout 里可能连一次正奖励都看不到,也就是说神经网络压根儿没收到过一条“做对了”的梯度信号。

这种就是典型的稀疏奖励问题。它跟奖励误导不一样,后者是智能体找到了作弊路径拿高分,前者是智能体连往哪个方向走都不知道。为了应对这种局面,业界的常规操作无非就几类:密集奖励就是把“距离”之类的中间量加进 reward 里,但人工设计痕迹太重,稍微复杂点的任务就很容易被钻空子或者导致策略局部最优;课程学习是在简单环境里先训练再逐步过渡到困难环境,但课程难度的自动编排本身就是个不简单的优化问题;模仿学习倒是能绕过奖励问题,但它需要演示数据,很多场景拿不到高质量的专家轨迹。

HER 提供了一种完全不同的视角:不当场修改奖励函数,也不引入外部示范,只改变利用经验数据的方式。

1.2 核心思路:失败经验如何变成成功数据

hindsight 这个词,日常语境里就是“事后看”,心理学上对应“后见之明偏差”。想想看,一个人事后复盘时总是容易觉得自己当初早该看出端倪,可当时就是看不出来。HER 这个算法做的事,恰好就是用这种“事后诸葛亮的视角”来重新审视失败轨迹。

往具体了说,当智能体没能实现原本设定的目标时,对这条失败轨迹不直接扔掉,而是重新给它安一个“事后目标”——也就是它实际上达到的那个状态。这样一来,轨迹的 reward 就不再是“失败”,而是“成功达成另一个目标”。比如机械臂没有抓到方块,但把方块推到了某个角落,那这条轨迹就可以被标记为“成功地把方块推到了那个角落”,然后放进经验池里当作一条有效经验来训练。这个过程叫作 goal relabeling,中文通常翻译成目标重标注。

这套操作在生活里也很好类比。一个小孩本来想投篮得分,结果球没进篮筐,但碰巧把球拨到了一个很刁钻的位置。你如果想让他学会投篮得分,这事确实不算成功;但如果你先把目标切换成“把球推到这个位置”,他刚才那一瞬间的动作就刚好是标准示范。多积累几次这种“歪打正着”,孩子至少先学会了拨球,再慢慢练瞄准,最终才可能升级到投篮。HER 就是这么一层一层给智能体搭台阶的。

1.3 为什么只有这类任务能套用 HER

不是所有强化学习任务都能用 HER。它有一个硬性前提,任务必须可以被描述成“达成某个目标”的形式,而且这个目标要显式地出现在状态里。具体说就是,状态量需要拆成两部分:一部分是环境本身的观测,比如机械臂的关节角、末端位置;另一部分是期望目标,比如要抓的方块的目标位置。此外环境里还必须有“当前已达到的目标”能够被观测出来,也就是 achieved goal,比如当前方块的真实位置。没有这个可得的目标状态,HER 就没法做重标注,因为你根本没有“事后目标”可以拿来替换原来的 goal。

这也是为什么 HER 天然的栖息地是机器人操作、目标导航这类任务,而不是像 Atari 游戏那种纯粹为了拿高分的场景。想套用 HER,先得确认环境状态里有没有目标信息,以及目标信息能不能半路改掉。

2. 核心机制拆解:目标重标注与经验回放

2.1 从“四元组”开始理解 HER 的数据流

大部分 off-policy 强化学习算法,比如 DQN、DDPG、SAC,训练时都是从 replay buffer 里取出一个四元组样本:当前状态 s、动作 a、奖励 r、下一时刻状态 s'。HER 在这个基础上改造了样本的构成。为了叙述清楚,这里把 s 拆成两个部分:观测 o 和目标 g。智能体的策略输入是 o 和 g 的拼接,也就是 π(a | o, g),所以每条 transition 实际要记为 (o_t, g_t, a_t, r_t, o_{t+1}, g_{t+1})。

HER 真正动刀的地方就在 g_t 和 reward r_t 这一尊一簇上。它允许你从“未来某个时刻实际达到的目标状态”里挑选一个新的目标 g' 来替换当前的 g_t。替换完成后,reward 也必须重新计算,不能用原来的 r_t,而是要根据 g' 来判断这一步是否成功。替换后的新 transition 会变成长这样:(o_t, g', a_t, r'(g'), o_{t+1}, g')。

请注意,g_{t+1} 也要被替换成 g',不能保留原来的目标不变。因为策略在下一时刻输入的目标也必须跟着变,否则网络输入和输出目标就错位了,智能体学到的会是一团浆糊。我第一次写这块代码时就没太注意 g_{t+1},结果训练曲线的诡异程度让我排查了整整两天。

2.2 目标重标注的完整形式化过程

为了让表述更精确,可以给 HER 的“事后目标”一个数学化的定义。假设原始任务有一个目标集合 G,每个 episode 开始时从 G 里采样一个真实目标 g。一个 episode 结束后,我们可以拿到一组真实的状态轨迹。重标注时的关键选择是:在轨迹的某个时刻 t 之后,选一个时刻 t',把 t' 时刻达到的状态(或状态的目标相关部分)记为新的目标 g'。

接下来看单步 transition。原始数据是 (o_t, g, a_t, r_t, o_{t+1}, g),其中 r_t 表示智能体在目标 g 下获得的奖励。重标注后变成 (o_t, g', a_t, r', o_{t+1}, g')。这里的 r' 用的是稀疏的、重新判断过的奖励,通常是一个指示函数,比如 1[dist(o_{t+1}, g') < δ],只有下一步真的接近了 g' 才给正奖励,否则就是负分或零分。

从形式上看,HER 并没有修改交互时的行为策略,智能体在当前 episode 中仍然朝着原始目标在探索。只是从经验池中采样学习时,历史数据被有意识地替换成了更可达的目标。这种“目标换一换、奖励重算一算”的做法,在数学上等于隐式地构造了一个多目标策略学习的课程,让价值函数先学会应对众多较简单的目标,再逐步泛化到真正困难的目标。

还有一层值得点透:HER 这类 relabeling 必须搭配 off-policy 算法,因为只有 off-policy 算法允许你用历史策略产生的数据来反复更新当前策略,而且这些数据经过篡改之后仍然符合“某个策略下交互得到”的约束——毕竟在正确的多目标形式下,数据 (o, g', a, r', o') 确实可以看作策略在目标 g' 下的行为记录。像 PPO 那种强 on-policy 的算法,训练集基本只能用当前策略新采的数据,HER 的重标注价值就会大打折扣,甚至引入严重方差。

2.3 四种采样策略对比与选择建议

重标注时,一个新的目标 g' 从哪个集合里抽取,直接决定算法的效果。原论文提出了四种方案,我直接把它们的逻辑和特点拉成一张表。

采样策略目标来源特点适用场景
final只取 episode 最后一个状态的目标部分实现最简单,轨迹信息用得最少,偏向于塑造“往终点方向走”的意图起点到终点结构相对单一、轨迹不太曲折的任务
random从整个 replay buffer 里随机取一个状态目标多样性最高,但可能挑出一个和当前状态毫无关系的目标,训练难度大只在环境本身非常容易时才用,我基本不推荐
episode从当前 episode 内随机取一个未来状态目标来自同一条轨迹,状态分布更连贯,比 random 可控性高中等复杂度任务可以逐步尝试
future严格从当前 transition 时间步之后的未来状态里采样在未来状态与当前过渡之间有真实因果联系,学习信号最干净大部分机器人任务的第一选择

在实践里,future 基本是默认策略。原因也不难理解:t 时刻之后的状态是从当前状态开始演化产生的,把它当目标至少不会出现“目标完全够不着”的情况。由于未来状态与当前动作之间存在真实的时序依赖,目标 g' 与 o_t 之间的关联性强,critic 拟合起来相对容易。

关于重标注比例,论文和后续社区的实践通常给出一个经验值:对每条原始 transition,以大约 80% 的概率做 future 重标注,剩下 20% 保留原始目标。这个比例背后的逻辑是,完全不做重标注的话,经验池几乎没有正反馈信号;如果全部重标注,智能体可能只学会各种随手可达的目标,却忽略了真实目标本身的价值。80% 是我试过很多次之后觉得泛化最稳的数值。

3. 实操落地:把 HER 接进 DDPG 的完整流程

3.1 实验环境选型与改造思路

动手写代码前,先说环境。最容易跑通的是 OpenAI Gym 的 Fetch 系列环境,尤其是 FetchReach,它是最简单的入门级任务,HER 在它上面的表现几乎是秒收敛,适合验证你的代码链路有没有写错。如果你想自己造一个更可控的验证环境,我自己常用的做法是做一个 2D 点目标导航任务。

这个环境的规格很简单:智能体是二维平面上一个点,动作是它在一个维上的位移,范围 [-1, 1]。观测 o_t 是智能体当前位置坐标和当前速度,初始位置随机,目标 g 是一个随机出现的二维坐标。稀疏奖励规则是 dd = ||当前点位置 - 目标||,小于 0.05 就给 0,否则给 -1。说实话这个环境和抓取任务相比已经算很温和的,但如果没有 HER 的话,纯随机的点要想精准落进半径只有 0.05 的小圆盘里,概率非常低,训练起来就会观察到奖励始终卡在 -1 附近不动。

如果你想快速复现,直接用 gym.fetch 系列会省掉不少环境细节的坑。需要注意 Fetch 环境返回的 observation 字典里已经帮你拆好了 observation、achieved_goal、desired_goal 三个字段,HER 可以直接基于它写。自己造环境时,最忌讳的是把 achieved_goal 和 observation 混在一起喂网络,那样后续做重标注会非常痛苦。

3.2 网络结构与 reward 函数设计

DDPG 是 HER 最常见的基座算法,它由 actor 网络和 critic 网络组成。actor 输入是 observation 和 desired_goal 的拼接,输出一个连续动作。critic 输入是同样的拼接结果加上动作,输出一个 Q 值。为了训练稳定,两个网络都各有一个 target 网络,用软更新的方式同步参数。

一个容易忽略的点是输入特征的尺度。机械臂关节角、末端位置、目标位置往往量纲差异很大,如果不做归一化直接拼接,critic 的拟合难度会上升。我的做法是在环境层面就把所有观测和目标都标准化到 [0, 1] 或者 [-1, 1] 区间,然后再拼接。很多人在 Fetch 上复现 HER 效果不佳,第一步就该检查这里。

reward 函数和重标注逻辑是绑定的,不要单独把 reward 写成一个环境里不可变的值。计算逻辑可以抽象成独立函数,输入是 achieved_goal 和 desired_goal,输出是稀疏奖励。这样原始目标算一份奖励,重标注后的目标再算一份奖励,逻辑统一,不容易出 bug。具体代码片段如下:

def compute_reward(achieved_goal, desired_goal, threshold=0.05): dist = np.linalg.norm(achieved_goal - desired_goal, axis=-1) return -(dist > threshold).astype(np.float32)

3.3 replay buffer 的 HER 采样核心代码

直接调库的话,OpenAI Baselines 的 her 模块是最经典的参考实现。不过为了讲清楚原理,我更喜欢把它的核心逻辑压缩成一个足够清晰的版本。首先,replay buffer 里存的不再是普通四元组,而是一个 episode 的完整轨迹,包含每一步的 observation、achieved_goal、action、reward、observation_next、achieved_goal_next、done。HER 的采样是在拿到了整条 episode 之后才做的,所以训练前得按 episode 为单位往里塞数据。

训练采样时,每次从 buffer 里抽出一个 batch 的 episode,然后从每个 episode 里再抽若干条 transition。对每条 transition,根据 2.3 节的比例决定走原始目标还是重标注目标。重标注逻辑如下:

def sample_her_transition(episode, t, strategy="future"): # episode: dict of lists, 长度为 T # t: 当前 transition 的时刻索引 if strategy == "future": # 从时刻 t+1 到 episode 末尾随机选一个未来时间点 future_t = np.random.randint(t + 1, len(episode["achieved_goal"])) new_goal = episode["achieved_goal"][future_t].copy() elif strategy == "final": new_goal = episode["achieved_goal"][-1].copy() elif strategy == "episode": any_t = np.random.randint(0, len(episode["achieved_goal"])) new_goal = episode["achieved_goal"][any_t].copy() else: raise NotImplementedError obs_t = episode["observation"][t] act_t = episode["action"][t] obs_next = episode["observation"][t + 1] reward = compute_reward(episode["achieved_goal"][t + 1], new_goal) new_obs_t = np.concatenate([obs_t, new_goal]) new_obs_next = np.concatenate([obs_next, new_goal]) return new_obs_t, act_t, reward, new_obs_next

要注意两个小细节。一是 obs 和 goal 的拼接一定要在采样函数里统一完成,不要在环境里拼一次又到采样里拼一次,维度很容易错位。二是重标注后的 done 信号也要重新考虑,在稀疏任务里我一般就把 done 直接设成 0,避免过早结束 episode 导致正常探索被截断。

3.4 超参数配置与训练技巧

为了让整套流程跑起来,我给一套在 Fetch 系列和点导航上都验证过的参数,直接拿去用在大多数任务里能当起点。

超参数推荐值备注
replay buffer 容量1,000,000HER 对 buffer 大小非常敏感,绝对不能吝啬
batch size256太小会让重标注后的目标信号过于稀疏
critic 学习率1e-3过大会导致 Q 值震荡,建议 Adam 优化器
actor 学习率1e-3与 critic 保持一致即可
gamma0.98Fetch 类任务我常用 0.98,导航任务 0.95 也行
tau0.05target 网络软更新系数,比默认的 0.005 稍快
HER 重标注比例0.8保留 0.2 的原始目标数据
采样策略future复杂任务只用 future,不要换
探索噪声OU 噪声或高斯噪声,幅度 0.2训练后期可逐步衰减噪声

训练循环的大致节奏是,每个 episode 结束后立刻把整条轨迹塞进 buffer,每步或者每隔固定步数做一次 DDPG 更新。DDPG 更新时,从 buffer 里采样一批完整轨迹,通过上面这个函数得到新的 transition,再走标准的 critic 和 actor 梯度计算。为了保证 actor 崩溃得慢一点,我会给 actor 的梯度更新稍微加一点梯度裁剪,max norm 设为 0.5,稳定效果立竿见影。

4. 调参记录与避坑指南

4.1 训练不收敛:先怀疑这三处

我见过很多人兴冲冲地复现 HER,结果跑了几十万步智能体还在乱动,然后开始怀疑算法。其实大部分情况下问题都非常机械,按这三处排查基本能解决一大半。

第一处是 achieved_goal 的定义是否正确。点导航里,achieved_goal 就是当前位置坐标,简单直接,不容易错。但拓扑结构复杂一点的任务里,achieved_goal 往往来自某个物体的显式状态。比如推方块任务里,方块位置才是目标,机械臂末端位置不是。如果你错误地把末端位置当成了 achieved_goal,重标注出来的目标就会完全错位,智能体想学都不知道在学啥。

第二处是 reward 重算逻辑是否和新的目标同步。有些人的代码是从环境的 transition 里原样取 reward,忘了重标注后要重新调用 compute_reward,导致一条 transition 上标注的目标是新目标、奖励却是旧目标的。这种数据对 critic 来说完全自相矛盾,表现就是 Q 值一会儿上天一会儿入地,批评家直接精神分裂。

第三处是拼接顺序。我在 3.3 节里专门强调 obs 和 goal 要统一拼接,很多人用的是环境给出的 dict,却各自在网络的输入层里分别处理,然后目标在某个模块里被替换了,图省事没同步,最后网络输入维度的不同位置对应不同的语义信息。这种 bug 很难从训练曲线里看出来,只能靠细致地打印每一步的样本形状和目标数值来排查。

4.2 HER 特有的几个坑

排查完上面三处,如果还不收敛,那大概率碰到了 HER 特有的几个坑。

第一个坑是 replay buffer 太小。HER 实际上在利用海量历史失败状态来丰富目标分布,如果 buffer 只有十万容量,很多早期探索到的状态还没来得及被重新利用就被挤掉了,目标多样性受限,训练很难往前推进。我自己在点导航任务里对比过,buffer 从十万提到一百万,收敛步数几乎下降了一个数量级。这个差异非常直观,所以宁可把内存多分一点给 buffer,也别省。

第二个坑是重标注比例过高。如果你试图把每条 transition 都重标注成未来目标,actor 很容易退化成“只会追逐近在咫尺的目标”的短视策略,最终表现是真实目标任务上一直无法突破。我建议固定住 0.8 这个比例别来回调,要么先不动,要动的话也在 0.6 到 0.9 之间小幅试。

第三个坑是只在 episode 结束后才做重标注。HER 的重标注是在采样阶段发生在整条轨迹上的,而不是交互中进行。有些初学者贪图方便把重标注逻辑塞进了环境 step 里,一 episode 走一步就换一次目标,等于人为把原始任务改成了不断漂移目标的追赶游戏,策略输出会非常毛糙。

4.3 一个小型对比实验的经验记录

为了更直观地展示 HER 的威力,我拿自定义的 2D 点导航任务做了一组对比。环境是 100 个随机起点和随机目标,没有 HER 的 DDPG 跑 50 万步,成功率大概是 12%,基本靠运气。同样条件下加上 HER,future 策略,重标注比例 0.8,大约 12 万步左右成功率就到了 95% 以上。

我又单独对比了 final 和 future 两种采样策略。final 在点导航这种单峰任务里表现也不差,大概 20 万步能到 90%。但在一个更曲折的迷宫式导航任务里,final 明显力不从心,因为 episode 终点通常跟中途状态差距很大,用它做目标会导致探索信号不连续。future 几乎是把最终收敛步数砍半。这组对比说明了一个事实:future 的“时序因果”特性是它稳定好用的根本原因,不是玄学。

5. 边界与扩展:HER 适合什么、不适合什么

5.1 不适合 HER 的场景

HER 不是万灵丹,有些场景硬套反而会误导设计。首先是完全没有目标概念的任务。比如让机械臂学一个灵巧操作动作序列,整个过程没有显式的目标状态,只有“完成整套动作”这种高层判定,HER 无法找到可替换的目标向量,自然无法应用。

其次是目标状态极难达到且没有中间状态可用的任务。例如让智能体生成一段特定频率的信号,失败时状态和目标差异是全方位的,未来状态跟真实目标之间的插值意义很弱,重标注出来的目标依然几乎不可达,HER 的课程作用就基本消失了。

还有一种情况是奖励本身并非稀疏,已经做了很精细的 shaping,加 HER 意味着你的 reward 函数会被反复重算,反而跟 shaping 逻辑纠缠不清。这个时候应该做减法而不是加法。我自己遇到过类似项目,后来选择关掉 shaping,纯跑 HER 加稀疏奖励,效果反而更干净。

5.2 可以继续扩展的方向

HER 的后续发展空间其实很大,它在多目标强化学习里更像是一种基础数据增强手段。常做的扩展方向有几个:一是把 HER 和 SAC 结合,SAC 比 DDPG 更稳,搭配 future 采样在不少机器人任务上是目前的主流组合;二是把它和 meta learning 叠加,让智能体在多个任务上快速适应新目标,HER 生成的大量目标数据天然适合做元训练的支撑;三是把 HER 的逻辑延伸到 skill 发现上,先用重标注找到各种可达状态,再聚类成技能,这件事已经有人在做了。

这些方向本质上都在利用同一个洞察:失败经验里不只有失败,换一个角度总能挖出价值。个人在实际操作中的体会是,复现 HER 这件事花半天就能跑通,但真正把它的思想用透、用对,需要你对自己任务里的状态结构和目标结构有足够清晰的认识。这也是为什么我一直建议新手先在小环境里把 HER 的每个环节都打印出来看一遍,再上真实机械臂平台,这样后面踩的坑会少很多。

最后分享一个小技巧:如果你在一开始就计划用 HER,别等到训练不收敛时才想起来加,应该从环境设计阶段就把 observation、achieved_goal、desired_goal 三个字段拆好,哪怕当前任务还在用普通 DDPG 也能把数据格式先对齐。后期切 HER 时只要改几行采样逻辑,不需要重构整个训练链路。这种提前留接口的习惯,能让你省下大量的返工时间。

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

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

立即咨询