最近我在调一个机械臂抓取实验,结果被稀疏奖励狠狠上了一课:训练了好几个晚上,智能体就是原地发呆,连“伸出去”这个动作都没学会。后来把失败轨迹用 hindsight 的方式重新解释了之后,学习曲线才终于抬头。这让我意识到,hindsight(后见之明)并不只是一句“马后炮”的调侃,它在工程里的价值,是把你已经发生过的失败经历重新变成可学习的经验。
这篇文章想讲的,正是 hindsight 在强化学习中最具有代表性的落地算法——HER(Hindsight Experience Replay,事后经验回放),以及我把它用到机械臂抓取任务时的实操细节。顺便会聊到这种“事后重标注”的思路怎么迁移到技术复盘和数据分析里。适合正在跟稀疏奖励、局部探索难题较劲的 RL 入门者,也适合想建立复盘体系的工程师。我会尽量用场景和类比讲清楚,少上数学,多讲操作。
1. hindsight 到底在说什么
1.1 拆开词根:事后视角与马后炮心理学
hindsight 这个词拆开看很有意思:hind 是“后面的”,sight 是“视线”,合起来就是“回头看”。英文里有一句老话叫 hindsight is 20/20,意思是“事后再看,一切清晰无比”。这不难理解,一件事情尘埃落定以后,你总会觉得结果“很明显”,甚至觉得自己早就预料到了。心理学上专门给这种现象起了个名字:hindsight bias,后见之明偏差。
但这个词在技术圈里的意思已经不完全一样了。过去几年你只要翻过强化学习相关的论文,很难不撞见它。尤其是在机器人控制、多物体抓取这些任务里,hindsight 代表了一种非常具体的训练技巧:当智能体没有完成初始目标时,不要就地把这段轨迹丢掉,而是换一个“事后看起来更合理”的目标,重新解释这段经验,把它存进回放池继续学习。
说白了,hindsight 在工程语境下的潜台词是:如果这件事没能按计划走,那它至少按某种路径走了。那条路径本身也有信息量,你得想办法把它捞回来。
1.2 一个词的三重身份:心理学、强化学习、工程复盘
我理解 hindsight 在当下有三个层面的身份。
第一层是认知科学里的 hindsight bias。它提醒你,记忆会自我修饰,事情发生后你会不自觉地“重写”当时的预判。这一层主要是在提醒人对此要有警觉,别把后知后觉误当先见之明。
第二层是强化学习里的 HER。这是最硬核、最出成果的一层。它把“马后炮”变成算法操作:改目标、改奖励、重新采样,让原本零奖励的失败样本产生学习信号。这一层是本文的重头戏。
第三层是工程复盘方法论。做项目、写代码、设计方案,同样可以借用“目标重标注”的思维:一次上线没有达到核心指标,但它验证了某些约束、暴露了某些系统瓶颈,关键是你用哪个“目标”去解读它。这三层并不割裂,它们都指向同一种能力:在事后,用一套不自我欺骗的规则,从已发生的过程里提取增量信息。
2. 技术核心:HER 如何把失败轨迹重新变成训练数据
2.1 一切从稀疏奖励问题说起
先讲清楚 HER 要解决的问题——稀疏奖励。
假设你训练一个机械臂,任务是让末端到达桌面上的某个红色目标点。最简单的奖励设计是:末端和目标点距离小于阈值,奖励为 0;否则奖励为 -1。每个回合最多走 50 步,智能体需要在一个 25 维左右的观测空间里摸索动作。问题来了:在这个奖励设定下,随机探索碰巧到达目标的概率低到可怜,绝大多数轨迹得到的都是清一色的 -1。
在密集奖励任务里,每走一步你都能获得“靠近一点点”的反馈,策略网络能从这些反馈里学习趋势。可稀疏奖励场景下,奖励信号像沙漠里的水源,智能体在探索阶段基本接触不到任何正样本,梯度更新反复把策略推向平庸,最后训练出的智能体要么一动不动,要么在一个局部区域反复震荡。
这里有个生活化类比。你去练习投飞镖,如果只有正中靶心才有人告诉你“对了”,其它时候完全沉默,你根本不会知道自己偏了 5 度还是 10 度。大脑就算想纠偏,也拿不到足够的信息。
针对稀疏奖励,常见的救法大致有三类:奖励塑形(reward shaping)、课程学习(curriculum learning)、模仿学习。奖励塑形需要非常细致的领域知识,构造不好反而会诱导智能体钻奖励漏洞;课程学习要让任务从易到难,但“怎么自动分课程”本身又是个问题;模仿学习需要示范数据,不是每个场景都有。HER 的路线不一样,它不动状态空间,也不追求让探索更聪明,而是换掉“经验回放”阶段的目标。
2.2 HER 的目标重标注流程
HER 的全称是 Hindsight Experience Replay,核心操作就四个字:目标重标注(goal relabeling)。
假设智能体在一个 episode 里的目标是 g,它从状态 s_t 执行动作 a_t 到了 s_{t+1}。但由于没有到达 g,奖励是 -1。如果这段轨迹到此为止,它对训练就只有一个微弱的负向贡献。HER 的做法是:在这个 episode 结束后,额外把一些 transition 的目标改成别的东西。
改成什么呢?改成这个 agent 在后面某个时刻“真的到达”过的状态。比如在第 t+k 步,它到了状态 s_{t+k},那就把新的目标 g' 设成 s_{t+k}。重新计算奖励:如果 s_{t+1} 接近 g',就给它 0 分,否则 -1。随后把这条新 transition(s_t, a_t, r', s_{t+1}, g')存进回放池,跟原始 transition 一起用于训练。
你不用把目标换成最终状态,常见的做法有三种:final 策略取整个 episode 最后一步的状态当新目标;future 策略从未来某一步随机取状态当新目标;episode 策略从整段轨迹里随机挑一个状态当新目标。从我在实操里的经验看,future 策略最稳,final 策略在一些短任务里也能用,但长 horizon 任务会导致伪目标和当前时间步离得太远,动作和目标之间的关联变得很弱。
有一点需要说清楚:重标定不是修改历史,而是在保留原始样本的同时,额外生成更多不同“任务视角”的样本。同一个动作序列,从“我没能到达 A”这条线看是失败的,从“我其实到达过 B”这条线看,就是一次成功。既然强化学习允许你人为设计训练分布,那为什么不让训练分布更丰富一点?
2.3 为什么这个“后悔药”能成
从理论上看,HER 有效的原因在于,它把“探索中得到的状态转移信息”和“目标达成信号”重新配对了。
原始 MDP 的场景里,一条失败的轨迹只有目标 g 这一个标签。学习系统想知道的是:在目标 g 下,从状态 s_t 执行 a_t 是否有效。结果是无。但在真实物理过程里,动作 a_t 并不是毫无意义的,它确实把状态从 s_t 变成了 s_{t+1}。如果你把目标换成 s_{t+k},那同一个动作就可能变成“有效的”。
用 Q-learning 的话来说,训练时用的是 TD 目标:
y = r' + gamma * max_{a'} Q(s_{t+1}, a', g')
当 r' 因为重标定从 -1 变成 0,这个样本对 Q 网络的监督信号就完全不同。一个原本只会教 Q“这个动作不行”的样本,变成了教 Q“在这个新目标下,这个动作能让状态接近目标”。Q 函数在整个状态-动作-目标空间上的曲面会更快被填满。
这也是为什么 HER 经常和 off-policy 算法(DDPG、TD3、SAC)搭配使用:这些算法都依赖经验回放,回放池里样本目标是什么并不影响算法性质,但要尽量覆盖多样化的“目标视角”。而 on-policy 算法要拿当前策略重新采样,HER 的收益会被摊薄,改造也更麻烦。
我个人理解 HER 并不是让探索本身变聪明,它改变的是对探索结果的解释方式。你多试几次总会碰到某些状态,无论目标达成与否,这些状态背后已经包含了“从哪来、到哪去”的信息,HER 的作用是把这些信息重新命名成可学习的标签。这一点想明白之后,很多参数都不用死记。
3. 实操:用 HER 训练一个机械臂抓取智能体
3.1 环境与问题建模
我用来验证 HER 的环境是 OpenAI Gym 里的 FetchReach-v1。任务不算复杂:一个 Sim 机械臂要把末端执行器移动到随机生成的 3D 目标点。它的 observation 大体包含三块:当前机器人关节和末端状态、desired_goal(期望目标坐标)、achieved_goal(末端当前坐标)。reward 是严格稀疏的,末端和目标的欧氏距离小于 0.05 才给 0 分,否则 -1,每个 episode 最多 50 步。
选这个环境做第一次 HER 实验很划算,因为它目标空间低维(3D),动作空间也小,能快速验证你写的重标定逻辑对不对。跑通了之后再换 FetchPickAndPlace-v1 这种需要抓取和移动物体的任务,思路是一样的,只是目标空间多了物体位置和夹爪状态,维度一下子涨了不少。
建模时有个容易忽略的细节:你必须把 goal 作为神经网络的输入,而不是只把 observation 喂进去。HER 让同一个 transition 在不同的 goal 下被重复学习,如果网络结构根本不接收 goal,那重标定后的样本对网络来说跟原始样本没有区别,等于白做。
3.2 代码结构里的关键实现
我不会贴一整份可运行的工程代码,因为不同的库里抽象方式差很多,但核心逻辑可以写出来。它的结构大概是下面这样:
# HER + off-policy 算法训练主循环的伪代码风格示例 # episode_storage: list of dict,记录每一步的 obs、act、achieved_goal 等 for s, a, r, s_next, achieved_goal, desired_goal in episode_transitions: # 原始 transition 一定要加入回放池 replay_buffer.add(s, a, r, s_next, desired_goal) # 以一定概率对当前 transition 做目标重标注 if random.random() < her_prob: # future 策略:从未来时间步里随机选一个状态作伪目标 future_idx = random.randint(current_t + 1, episode_len - 1) pseudo_goal = episode_transitions[future_idx]["achieved_goal"] pseudo_reward = compute_reward(achieved_goal=achieved_goal, goal=pseudo_goal) replay_buffer.add(s, a, pseudo_reward, s_next, pseudo_goal)上面这段是伪代码,但你真正落地时需要关心的点都在里面了。
第一,不要在 transition 粒度上直接随机选“未来时间步”。如果先打散一条轨迹再采样,你就没有办法保证选的未来状态真的发生在当前 transition 之后。正确做法是按 episode 组织样本,先拿到整条轨迹,再对每条 transition 的“未来区间”做选择。我用过一些开源实现,它们通常会在 buffer 里额外存 episode_id 或者是整段轨迹的列表,目的就是保留时间顺序。
第二,compute_reward 函数要跟原始奖励函数保持一致。比如原始环境用距离阈值 0.05,那重标定时也要用同一个阈值,不要出现“原始任务 0.05,重标定任务 0.2”这种标准漂移,否则目标空间下的真实成功率会和训练分布偏离。
第三,注意原始目标样本和伪目标样本的比例。我见过有人把 her_prob 设成 1.0,结果 buffer 里全是事后重写过的目标,智能体对“最初那个目标”反而没有记忆,训练结束之后按原始 goal 评估成功率非常差。重标定是为了增加信息量,但它不能完全替代原始目标。
3.3 训练配置与结果观察
训练配置上,我沿用了一些公开实现里验证过的默认值,再根据自己的环境微调。策略网络我选的是 DDPG,结构上就是 actor 和 critic 两层 256 宽 MLP,激活函数用 ReLU,critic 输入是拼接后的 state 和 goal。超参数关键几项如下:
| 参数 | 我的配置 | 说明 |
|---|---|---|
| her_prob | 0.8 | 每条 transition 有 0.8 概率被重标定 |
| 伪目标策略 | future | 从未来步骤随机取一个 achieved_goal |
| replay buffer 容量 | 1,000,000 | 足够容纳多个策略阶段样本 |
| batch size | 256 | 混合原始目标与伪目标 |
| gamma | 0.98 | 稀疏任务下用稍小折扣有效 |
| actor/critic 学习率 | 0.001 | Adam 默认即可 |
| episode 步数 | 50 | Gym FetchReach 默认 |
训练开始后,你会发现 loss 曲线几乎不能说明问题。在稀疏奖励和最开始的探索阶段,Q loss 可能会很低,因为它一直在拟合“全是 -1”的样本,网络学到的就是个“躺平”的 Q 曲面。真正需要盯的指标是按原始 goal 评估的成功率,也就是每个 epoch 实际 rollout 多次后,计算机械臂末端有没有到达过 desired_goal。
我实验里比较典型的现象是:单纯 DDPG 跑很久成功率都接近 0,加 HER 之后,大约经过几十个到一百个 epoch,成功率会突然从某个低位开始往上蹿,最后逼近 0.9 以上。这种“突然抬头”不是网络结构带来的,而是重标定样本终于把 Q 函数曲面中“能到达的区域”覆盖出来了。你需要有点耐心,它并不是线性上升。
3.4 调参心得
调参这块是我最容易踩坑的地方。按踩坑频率排序,先说三条最重要的。
第一条,伪目标策略优先用 future 而不是 final。final 策略把整条轨迹终点设成目标,如果轨迹很长,早期动作和最终状态之间的因果关联早就稀释了,Q 网络很难从这种远距离配对里学到可靠信号。future 策略随机从未来若干步里选一个状态,既保持了“这个动作之后真的到达过某处”的因果邻接性,又让目标分布更丰富。
第二条,her_prob 不是越大越好。OpenAI 那时自己在实验里常用 0.8 这类高比例,但实际操作里要想一下你的任务:如果回放池里 80% 都是伪目标,按原始目标评估的成功率不一定差,因为 Q 网络还能从 20% 原始样本里拟合原任务;可如果再把未来策略的区间选的特别远,伪目标里的“有效正样本”占比变低,训练噪音会上升。从 0.5 起步,再根据曲线逐步往上加是比较稳的路径。
第三条,归一化和坐标表示要一致。FetchReach 里 desired_goal 是相对世界坐标,achieved_goal 也必须是同一个坐标系,不能用未经转换的关节角或者局部坐标去当伪目标。我试过一次用末端局部坐标做目标,结果重标定出来的目标分布和真实 reaching 任务分布完全错位,训练成功后 rollout 成功率反而掉了一半。出现这种问题,打印一下 buffer 里 goal 的分布就能看出来。
4. hindsight 思维的技术迁移与偏差陷阱
4.1 技术复盘中的目标重标注
HER 这套“目标重标注”思维,离开算法也能用,而且非常好用。技术复盘最常见的毛病是把原始目标当成唯一裁判:线上服务做了优化,目的是降低 p99 延迟,结果延迟没有明显改善,于是复盘结论就是“失败”。但这个结论对下一次优化没什么帮助。
你可以像 HER 一样,给这次复盘换一个“事后更合理的目标”。延迟没降下去,但这次验证了某一个中间环节根本不是瓶颈,或者发现压测流量模型和线上相差太大。验证“某条路不通”,同样是可以进经验池的知识。我现在的做法是:每次复盘的表格里固定三列——“原始目标”“事后能确认的有效信息”“新目标”,用这套结构逼着自己别掉进二元论里。
4.2 数据标注和产品分析中的后见之明
类似思路在数据标注和产品分析里也有很实际的使用。
做数据标注质量复盘时,一批样本的标注结果不达标,直接结论往往就是“打标员水平不行”。但如果把目标从“所有人必须一次标对”重标定成“找出这些样本里最容易混淆的类别”,这批错误样本立刻变成了分类体系诊断的输入。很多图像分类项目里,最容易暴露问题的地方恰恰集中在长尾类别上,而错误样本其实就是发现长尾的最好线索。
产品分析里也一样。一个注册漏斗的转化率低于预期,如果你只看“有没有完成注册”这个原始目标,每个流失用户都成了失败样本。但把目标重标定为“用户在这个页面到底完成了什么”,原先的流失样本就变成了“用户路径行为”样本。比如用户没有填完表单,但他点击了某个条款帮助页,这个行为本身可以作为下次迭代的信息。
这种迁移的关键在于:你并不是假装失败不存在,而是主动选择更多可解释的目标维度。跟 HER 一样,它改变的不是历史,是叙事方式。
4.3 hindsight bias 会骗你
但这里有个大坑:滥用 hindsight 思维很容易滑向 hindsight bias。算法版的 HER 是严格在环境里重新计算 reward 的,它有明确规则、有物理边界。但人在复盘的时候没有这种约束,事情已经结束,你记住了“我早就知道会这样”的错觉,这种错觉是无数复盘失真案例的共同来源。
我做技术复盘时会刻意加一道护栏。复盘表里除了“事后新目标”,还必须写一栏“当时已知信息”。任何结论都要标出:这条结论基于的是当时就能看到的数据,还是事后才补上的新信息。前者是真正的可迁移经验,后者只能算历史注脚,价值有限。这样能减少对自己的记忆美化和对团队的误导。
说得再直白一点:HER 是给你手里的旧轨迹重新贴标签,贴完标签你还是得回到真实目标里评估成功率。现实里的 hindsight 思维也不该变成“失败总是可以重新解释”的万能自我安慰,而应像算法参数一样,设定好比例和边界。
5. 常见问题与排查技巧
5.1 加了 HER 还是学不动,先从哪查起
如果你加了 HER 之后训练仍然没有起色,我建议按下面的优先级查。
先查网络结构里有没有把 goal 喂进去。这个问题隐蔽,因为代码不会报错。你可以打印一条 sample 的结构,确认在 actor 和 critic 输入里确实有一个维度代表目标。
再查伪目标生成的时间关系。用 future 策略时,伪目标必须来自当前 transition 之后的某个时间步。如果因为轨迹存储方式问题,导致“未来”其实是“任意过去”,整个重标注逻辑就废了。我会在训练早期专门写一个小验证函数,随机抽几条样本,检查 future_idx 是不是严格大于当前 transition 的下标。
然后查回报计算函数。重标注后的目标也许在数值范围上和环境原本的 desired_goal 不一致,尤其是坐标范围没缩放到同一尺度时,critic 会学到错误的目标分布。打开训练日志里的目标分布直方图,一眼就能确认。
如果这些都查了还是不行,问题大概率不只是 HER,而是底层 off-policy 算法不稳定。DDPG 很容易陷入 Q 值过估计,尤其在连续控制任务里。这时候可以把主干换成 TD3 或 SAC,HER 的重标定逻辑基本不用改,成功率往往会好不少。
5.2 伪目标比例到底怎么调
伪目标比例这个参数说简单也简单,说麻烦也麻烦。简单在于它就一个数,麻烦在于它和任务、探索策略、回放池结构都纠缠在一起。
从我实际经验看,her_prob 在 0.3 到 0.8 之间都有成功案例。0.3 时候伪目标带来的信号少,适合环境里本身就比较容易碰到成状态的情况,你不需要太多“事后解释”;0.8 时候适合那些探索阶段几乎无正反馈的任务,否则智能体完全找不到学习支点。
还有一种情况需要特别警惕:当 her_prob 太高,而回放池里原始目标样本过小时,训练过程里的 Q 函数会逐渐被伪目标主导。你观察 rollout 成功率时,可能会看到训练曲线中途猛然回跌,原因是目标分布已经被污染了。如果遇到这种回跌,先把 her_prob 降回 0.6 左右,并在 buffer 里为原始目标样本设置最小保留比例,就像采样器里做类别平衡一样。
你可以把伪目标的任务当成“数据增强”,但数据增强不能把原图全部替换掉。保留相当比例的原始目标样本,是对抗这种污染最简单也最有效的手段。
5.3 回放缓冲区怎么跟 HER 配合
不仅是参数,buffer 的数据结构直接影响 HER 实现正确性。
如果你用一个简单的 deque 把所有 transition 打平存放,做 future 策略重标定就会很别扭。因为你随机采到一个 transition 后,想知道它属于哪个 episode、这个 episode 里后面有哪些状态,成本很高。我的做法是:buffer 里按 episode 存储,每个元素是一整条轨迹,采样时先随机选 episode,再随机选 transition。这样未来时间步的索引关系天然存在,重标定逻辑也简单。
另外一个配合细节是,HER 处理的其实不是单条 transition,而是“经验片段”。当你在同一 episode 里对多条 transition 分别重标定成不同伪目标,理论上没什么问题,但要注意,伪目标是从后续状态里选的,而后续状态本身又受到后续动作影响。如果伪目标选得太近,比如就选 k=1 的下一步状态,那很多伪目标学习到的还是“几乎原地不动”的东西,信息量偏低;选太远又增加噪音。把 future_idx 设计成从 1 到剩余步数之间均匀采样,是我试过多个方案后比较稳的平衡点。
最后建议 buffer 容量不要太小。HER 多生成的目标样本会快速发展,容量太小容易把早期探索的轨迹冲掉。对 FetchReach 这类简单环境,1e6 不是必须,但至少要有足够空间覆盖好几万条 transition,尽量别抠这一点内存。实际项目里,我见过很多“加了 HER 反而变差”,最后查下来就是 buffer 只留了几万条样本,重标定过的有效正样本频繁被后来的负样本挤出。
5.4 问题速查表
| 现象 | 可能原因 | 处理建议 |
|---|---|---|
| 训练曲线始终接近 0 | goal 没有传入网络 | 检查 actor/critic 输入维度,打印样本确认 |
| 伪目标设置看起来对但没用 | 未来时间步关系错误 | 写校验函数,确认重标定状态晚于当前步 |
| 训练后期 rollout 成功率回跌 | 伪目标比例过高 | 降低 her_prob,保留原始目标样本比例 |
| 成功率能上去但不够高 | 目标分布和原始任务不齐 | 检查坐标系统一性,归一化到同一范围 |
| DDPG 下 Q loss 震荡 | Q 过估计 | 换成 TD3 或 SAC |
| 奖励信号长期稀疏 | future_idx 范围不合理 | 从 1 到剩余步数均匀采样,不要只取最终状态 |
这些坑我基本都踩过。尤其“伪目标比例过高导致后期回跌”这个现象,看起来像是模型不收敛,其实问题出在训练分布和目标评估分布错位。你在排查的时候别只盯着 loss,一定要按原始 goal 做周期性的真实 rollout。这就像 HER 本身教我们的:评估一个模型,总得有一个你真正关心的目标,不能全用事后重写的目标来自我安慰。
我在实际使用中对这个项目最深的体会是:hindsight 不是用来取消失败的,它把失败从终点变成了素材。算法上的 HER 之所以有效,是因为它给每条轨迹增加了更多可学习的角度;复盘上的 hindsight 之所以值得用,也是因为单一目标下的失败常常掩盖别的维度的进展。如果你准备把它用到自己的训练项目里,记住一个原则:重标定要大胆,评估要老实。无论你从失败中解释出多少新目标,最终都要回到那个你真正关心的初始目标上,看它有没有真的变好。