☰
事后经验回放HER:把失败轨迹变为强化学习成功样本
2026/9/30 8:27:36 网站建设 项目流程

hindsight 这个词我最早被它真正触动,不是在某本技术书里,而是在一次机械臂抓取的强化学习实验现场。当时模型训练了一整夜,成功率依然是 0%,回放缓冲区里堆满了失败轨迹,我盯着 loss 曲线,觉得这实验基本废了。旁边做机器人的同事随口说了一句:“失败轨迹换个目标看,不就变成成功轨迹了吗?”就这一句话,救回了整个项目。后来我才知道,这背后有一个专门的算法名字:Hindsight Experience Replay,也叫事后经验回放。也就是从那时候开始,我不再把 hindsight 仅仅当成“事后聪明”这种略带贬义的词,而是把它看作一套实打实的工程方法。这篇文章想聊的,就是这个词背后的算法逻辑,以及我把它延伸到日常研发复盘时的真实感受。它适合正在做强化学习实验的工程师,也适合平时做数据分析、产品决策、团队复盘的人——前者用算法,后者用思路。

1. hindsight 这个词本身,就是一面镜子

1.1 从日常语义到技术术语的转译

hindsight 的英文含义是“回顾、事后认识”,中文习惯翻译成“事后聪明”或“事后诸葛亮”。日常语境里,它往往带着点贬义:事故发生了,谁都能说“我早就想到了”。但在工程技术领域,这个词的含义完全不同。hindsight 指的是我们拥有了一段完整轨迹之后,站在终点回看,能比站在起点时获得更多信息。这个信息不是虚构的,而是由一条已经走通的路沉淀下来的、真实发生的状态变化序列。

在机器学习里,这种“事后信息”被用得特别狠。一个智能体在环境里探索,失败了九十九次,第一百次碰巧接近目标,那前九十九次并不是废数据。如果能把失败轨迹重新贴上目标的标签,让模型从“没完成原目标”的轨迹里读出“我说不定能做到另一件事”的信号,训练效率会完全不同。因此 hindsight 在算法里不是形容词,而是一个正式的数据变换流程,它的核心动作叫“目标重标记”。

1.2 为什么值得为这个词单独写一篇

我这些年见过不少刚接触强化学习的人,上手就贴一个 DDPG 或者 PPO 的库,跑通 demo 后换到自己的稀疏奖励环境,立刻发现模型怎么都不收敛。此时十有八九的反馈是“调点奖励工程”,比如给中间状态加分,给距离函数加权重。这个思路本身没错,但属于绕着问题走。真正让我豁然开朗的,是把“奖励怎么设计”这个问题切成两半:一半是环境给我们的客观反馈,另一半是算法从历史轨迹中自己挖掘出来的反馈。HER 解决的就是后半部分,它不需要你精心设计中间奖励曲线,而是让模型自己用“未完成的目标”当作学习样本,从稀疏环境里硬生生挖出梯度来。

所以我觉得,无论你最终是否会用 HER,理解 hindsight 这种重标记的思想都值得。它能教你用另一种视角看待失败数据,也能帮你在实际工程里节省大量调奖励函数的时间。接下来我先讲清楚算法原理,再给一个能跑的最小例子,最后分享我踩过的坑,以及在项目复盘里同样适用的“事后视角”。

2. HER 为什么有效:把失败当成功学

2.1 稀疏奖励到底难在哪

我们先看一个标准问题:让一个智能体在二维平面里从起点走到某个固定目标点,到达目标半径内才给奖励 1,否则每步奖励 0。这是典型的稀疏奖励设置。对随机策略来说,在一个连续平面里恰好落在目标半径内的概率极其低,模型在早期几乎收不到任何正反馈。它就像一个从没吃过糖的小孩,不知道什么行为是好的,只能乱撞。奖励工程就是试图在路上每隔一段撒一点糖,告诉小孩“你方向对了”。这个方法有效,但需要设计者非常了解任务,而且往往换一个环境就要重新调。

HER 改变的是学习的素材来源。它观察到一点:在失败轨迹里,智能体虽然没有到达我们设定的目标,但一定到达了某些真实存在的状态。既然目标是我们自己定义的,那么为什么不能让“真实到达的状态”也成为一个候选目标呢?比如目标在右上角,失败轨迹走到了左下角。轨迹本身没完成任务,但如果我们把这次尝试的目标改成“左下角”,那么这条轨迹从头到尾都是成功的——每一步都精准走向了左下角,最后也确实到达了。这样,原本评分全是 0 的轨迹,转变成了一条评分全为 1 的轨迹,模型从中获得了一个强有力的学习信号:原来这种走法,是可以做成一件事的。

2.2 重标记的算法直觉

用更正式一点的话说,HER 会把一条 episode 记作若干(观测, 动作, 目标, 奖励)的元组。原本的目标g没有达成,那么我们从这条轨迹里挑出一个真实到达过的状态g',把它当作替代目标,再按照环境的奖励函数重新计算奖励。因为g'是轨迹中真实到达过的状态,所以重标记后的轨迹天然是“成功”的。把重标记后的样本丢进经验回放,模型就能在这些原本失败的数据上学到“如何达成一个目标”的通用能力。

这里有一个很关键的细节:替代目标不是随便挑一个状态就行,通常会从轨迹中均匀采样。如果取轨迹终止状态作为目标,本质上是在教模型“走到最后这点也行”,但样本多样性会差一些。采样中间状态则能给模型提供不同难度层次的样本。另外,也不是所有样本都要重标记,常见做法是保留一定比例的原目标样本,混合使用。这种混合让模型既学到“如何完成具体任务”,又能借助重标记样本提高目标达成率。

2.3 伪代码流程拆解

HER 并不是一个独立的算法,而是一种和 DDPG、TD3、SAC 等 off-policy 算法搭配使用的经验回放策略。它围绕“目标-conditioned 强化学习”设计,也就是策略不仅要输入状态,还要输入目标。流程大致如下:

for 每个训练回合: 初始化环境,采样一个原始目标 g 执行策略,记录整条轨迹 tau = (s_0, a_0, r_0, s_1, ..., s_T) 把原始的 (s_t, a_t, g, r_t) 存入回放缓冲区 从轨迹中按概率采样若干替代目标 g' 对每个 g',用奖励函数重新计算 r_t' = reward(s_t, a_t, g') 把重标记后的样本 (s_t, a_t, g', r_t') 也存入缓冲区 从缓冲区采样一个 batch,更新策略网络和价值网络

这样做的好处是显著增加了回放缓冲区里的有效样本量。原始轨迹里真正成功的样本可能一个都没有,经过重标记后,每一条轨迹都至少能贡献几组“接近成功”的样本。后续我在自己的实验里明显感觉到,同样一个 DDPG,套上 HER 之后,在二维导航、机械臂推积木这类任务里收敛速度可以快一个数量级。

3. 用最小代码把 HER 跑起来

3.1 我选了一个 2D 导航环境

为了验证 HER 到底有没有用,我最早动手写了一个极简的二维导航环境。智能体是一个点,状态是它的(x, y)坐标,动作是它在 x 和 y 方向上的位移增量,目标也是一个(x, y)坐标。每一幕从随机起点出发,最大步数设成 50。奖励函数只判断一件事:当前坐标是否进入目标半径 0.5 以内,是则奖励 1,否则奖励 0。这个环境简单到不需要 GPU,但它完整保留了稀疏奖励的难点。

构造这个环境时要注意一个容易忽略的点:目标本身必须编码到观测里。很多第一次写 HER 的人会把状态和目的分开,策略网络只接收状态,不接收目标,那重标记以后的目标信息根本无处安放。正确的做法是先把状态和目标拼接成一个向量,比如[x, y, gx, gy],再喂给网络。这一点看似基础,但直接决定了算法能否收敛。

3.2 核心代码逻辑

下面这段代码是我当时写的一个最小 HER 缓冲逻辑,重点不在神经网络,而在重标记过程本身。具体网络结构直接用了简单的两层 MLP,你换成 DDPG 或 TD3 也能跑:

import numpy as np # 奖励函数:智能体是否进入了目标半径 def compute_reward(state, goal, radius=0.5): dist = np.linalg.norm(state - goal) return 1.0 if dist < radius else 0.0 # 核心:事后重标记 def relabel_episode(episode_transitions, achieved_states, original_goal, replay_buffer, future_prob=0.8): """ episode_transitions: 原始经验列表,每个元素是 (obs, action) achieved_states: 轨迹中每一时刻的真实状态 original_goal: 原本设定的目标 replay_buffer: 全局经验回放 """ T = len(episode_transitions) # 先保留原始目标样本 for t, (obs, action) in enumerate(episode_transitions): reward = compute_reward(achieved_states[t], original_goal) replay_buffer.append((obs, action, original_goal, reward)) # 对每条轨迹额外生成若干个替代目标样本 for t in range(T): # 以 future_prob 概率从 t 之后的轨迹中采样目标 if np.random.rand() < future_prob: future_idx = np.random.randint(t, T) fake_goal = achieved_states[future_idx] else: # 否则从整条轨迹里随机选一个状态当目标 random_idx = np.random.randint(0, T) fake_goal = achieved_states[random_idx] obs, action = episode_transitions[t] reward = compute_reward(achieved_states[t], fake_goal) replay_buffer.append((obs, action, fake_goal, reward))

这里future_prob是一个值得玩味的超参数。取 1.0,意味着替代目标永远来自当前时刻之后的轨迹状态,样本更偏向“从近到远”的目标;取 0.0,则完全随机采样。我在二维导航里试下来,future_prob在 0.8 左右效果比较稳,太低会导致重标记目标和轨迹起点太接近,学到的全是“原地不动”这种短程技能。

3.3 训练循环里的重要细节

有了重标记函数之后,训练循环的逻辑就清晰了。每一幕结束,我把轨迹拆成状态序列、动作序列和目标序列,然后调一次relabel_episode。策略网络的更新频率可以设置成每一幕更新几次,也可以设置成固定步数更新。这里我把训练动机写下:这个环境里状态是二维坐标,用 soft actor-critic 的效果很好,但为了演示 HER 的作用,用简单的 DQN 变体也够用。

实操中有个经验,重标记的样本和原始样本最好在同一个 batch 里混合采样。如果分两个 batch 分开更新,模型容易产生目标偏移——它可能过度关注替代目标而忘记原始任务。我看到不少开源代码把这两类样本混在同一缓冲区,效果通常很不错。

我把整条训练流程跑完后,最直观的对比是:不用 HER 的模型在 1000 幕训练后成功率还是 0%,而加了 HER 的模型大约在 300 幕左右开始出现稳定成功的轨迹。这个对比足够说明问题——重标记不是玄学,是用数据变换提升了样本效率。

4. 参数选择与调试中的那些坑

4.1 目标采样策略怎么选

HER 论文里对替代目标采样提过几种方案,包括final(只用最后状态)、future(从当前时刻之后的轨迹状态里采样)和random(从整条轨迹里随机采样)。我自己的试验排序是:future好于random,random好于final。原因在于final只使用终点状态,样本多样性低,模型学到的其实只是“刹车的技能”;random提供各种距离的目标,但太短程的目标容易让模型偷懒;future既保证目标离当前状态有足够距离,又不会像final那样过于单一。

在多目标任务里,我建议把每一个替代目标的轨迹长度都限制在合理范围内。比如最大步数 200 的任务,替代目标如果是从第 10 步的状态采样出来的,那看起来就像是一个“短跑任务”,模型可能只顾着向前冲,忽略了转向等复杂行为。这时候可以给替代目标采样加一个最小时距过滤,比如要求future_idx - t > 5,避免过于简单的样本泛滥。

4.2 我在实验里踩过的常见坑

问题现象可能原因我的排查方法
训练 2000 幕成功率还是 0%目标没有拼接到观测里打印网络输入维度,确认目标维度和状态维度同时存在
模型学会了奇怪行为,总是停在原地替代目标太靠近起点调大future_prob,或设置采样最小时距
训练loss很低,但评估成功率极低评估时仍然用重标记目标评估阶段固定原始目标,禁止任何重标记
替代目标导致训练不稳定重标记样本占比过高保留 20% 原始目标样本,再混合重标记样本
模型在训练环境好,换环境就崩过拟合于特定目标分布训练中随机化目标和起点分布

这里最隐蔽的坑是第四项。我一开始把整条轨迹全部重标记,把原始目标样本全丢掉了,模型确实学得飞快,但学出来的策略只对“已经到达过的地方”有效,一但评估目标换了位置,成功率立刻跳水。后来我改成每个原始样本保留一份,重标记样本最多三份,模型的泛化能力明显好很多。所以重标记不是越多越好,它是在扩大样本多样性,不是替换真实任务信号。

4.3 一套能落地的调试工作流

我在新环境里用 HER 时,会按下面的顺序调试。第一步,先用最小环境确认代码链路没有 bug,目标拼接、奖励函数、重标记函数都写对了。第二步,跑一个没有 HER 的版本,作为基线,这也帮我判断环境本身难度是否合理。第三步,加入 HER,横向对比收敛曲线。如果加入后反而变慢,我首先怀疑是目标维度设计不合理,或是重标记的目标和真实环境动态不一致。

比较值得说的是第二步。有些环境确实不适合 HER,比如那些目标本身极其复杂、需要多步逻辑推理的任务,重标记后的轨迹物理上可能不存在。比如一个机器人里,如果你把抓取目标换成轨迹末端的位置,但这条轨迹末端物体已经掉落,那么重标记的“成功”在物理世界并不成立。这种情况下模型学到的只是纸面上的奖励,换个物理随机种子就失效了。判断环境适不适合,我的简单标准是:重标记后的目标,有没有可能从当前状态出发真正达到。如果答案是“否”,那这套方法大概率帮不上忙。

5. 把 hindsight 从算法延伸到工程复盘

5.1 复盘不是批斗会,是数据提取会

HER 给我留下最深的震动,不是它让模型变强,而是它揭示了一个通用的道理:失败轨迹里的信息密度,往往被我们严重低估。做工程也一样。我团队里现在每个迭代结束会开一个轻量复盘会,但规则很明确:不追责,不找“谁的问题”,只回答三个问题。当时的目标是什么?当时基于哪些信息做了决策?现在回看,有哪些信息在决策时是被我们忽略的?

把复盘从追责改成数据提取后,大家愿意讲真话多了。因为你会发现,几乎所有的“低级失误”背后都有一个共同的模式:决策时缺少某块关键数据。比如某个功能上线后崩溃,回看日志才发现有个异常分支从来没被测试覆盖过。用 hindsight 的视角看,我们要做的不是骂测试不仔细,而是把“异常分支”纳入测试清单。这就像 HER 把失败轨迹重新标记后加入缓冲区一样,不是在掩盖失败,而是在提取有效学习信号。

5.2 一套简单可执行的复盘模板

我建议复盘时按这个模板记录,不需要复杂的工具,代价低才能坚持。

  • 预期目标:用一句话写清楚当初想达到什么状态。
  • 实际结果:用客观数据描述发生了什么。
  • 信息差距:在决策时点,我们缺了哪些信息?哪些信息其实是可以提前拿到的?
  • 系统改进:把差距转化成一条可执行的动作,而不是“下次注意”。
  • 复用的经验:写一条如果重新来过,我会保持不动的策略。

这个模板看起来简单,但执念是难在“系统改进”这一行。如果这条动作没有被排进日程,复盘就等于白做。我见过很多团队开完复盘会,文档写了几千字,后面两周大家还是按老路子做事。那不是复盘,是一种仪式感透支。所以我在团队里定了一条很硬的规则:每条系统改进必须绑定一个负责人和截止日期。做不到的,下个迭代继续跟。

5.3 从“当初的我”到“现在的我”

hindsight 这个词最迷人的地方在于:它承认我们无法预知未来,但允许我们从结果中重新理解当时的情境。当我们处在决策节点的时候,信息不完整、时间压力大、选择很多,这些都是真实约束。事后站在终点回看,信息完整了,因此“当初我为什么没想到”这句自责,本质上是拿终点的信息苛责起点的人。更合理的做法是:把这次的不完整信息记录成一条系统改进项,让下一次决策点不再缺同一个信息。

这与算法中的思路高度一致:策略网站在训练时不断接收重标记后的样本,是为了让它在未来遇到类似状态时,能更好地理解当前状态和目标的关系。它不会因为一次失败而被重置,而是从失败中提取转移信息。人也一样。我们的项目经验不是一个个成功案例的堆砌,而是一次次从结果反推过程的积累。每次复盘都是一次对过往轨迹的重标记,把“未达成的目标”变成“我真实走过的路”。

结尾的小建议

如果你问我,技术里学到的 hindsight 最后变成了什么,我会说它变成了一个简单的工作习惯:每次实验或迭代结束后,我在日志最后强制写下两行。一行写“如果重来,我会改变什么”,另一行写“如果重来,我会保持什么”。写这两行的过程,就是一次自己的 HER 重标记。模型需要这种重标记才能从稀疏奖励里学会复杂任务,人同样需要从稀疏的成功率里,把失败过程变成下一轮的导航信号。

最后再分享一个具体技巧:如果你做强化学习相关项目,建议第一次接触 HER 时,不要直接上机械臂等高难度环境,先在一个二维网格或连续二维平面里亲手实现一遍重标记函数。把重标记后的样本从回放缓冲区里打出来,肉眼看一下奖励变化,你会对这套机制产生非常直观的理解。之后再去调复杂的机器人环境,心里对每个参数会特别有数。这个顺序,我试过两遍,第一遍偷懒跳过了,后来在复杂环境里多花了整整一周调参。

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

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

立即咨询