
每次听见hindsight这个词我脑子里都会同时蹦出两个画面。一个是项目复盘会上那句当时要是早看一步就好了另一个是NIPS 2017那篇《Hindsight Experience Replay》。前者是会议室里的叹息后者是让AI从失败里反推目标的强化学习算法——它们共享同一个内核当我们没达到原定目标时别急着把这段经历丢进垃圾桶先看看实际上走到了哪里。这篇博文想聊的是后者也就是HER算法但我会把事后视角这条线串到底。适合读的人大概有三类刚接触强化学习、正在跟稀疏奖励死磕的新手做机器人控制或离线策略优化、被探索效率折磨的工程师以及那些想从复盘里挖出更多复利价值的项目和团队——你会发现EREM的思路放在真实决策里同样好使。1. 从事后诸葛亮到目标重标记这个算法到底解决了什么问题1.1 失败经验为什么常常被浪费先抛一个反直觉的事实在强化学习里失败样本的数据价值通常比成功样本低得多但绝大多数算法并没有主动去盘活它们。以机械臂抓取为例随机策略抓一百次可能只成功一两次剩下98条episode的末尾奖励全是-1梯度反向传播时几乎传不回任何有效信息。换成人类学习者你第一次拧螺丝拧歪了至少知道往左偏了这个事实但标准的RL训练流程里拧歪了会被压缩成一个负数几毫秒后就被采样器淹没。HER的思路简单到有点不讲理既然agent没抓到目标物A那它实际抓到的位置B能不能临时改写成一个新的目标如果策略在目标B下恰好实现了末端到达B那么这段经验就不是失败而是一条成功数据。换句话说每当一个机器人没考上清华我们就把他实际考上的学校临时改称他的目标那么他这次备考过程瞬间变成了成功经验。1.2 目标条件强化学习是个什么设定要让这套目标替换成立任务必须是目标条件goal-conditioned的。普通RL里的目标是隐含在奖励函数里的agent只学一个从状态到动作的映射。目标条件RL则把目标也作为输入策略写成π(a|s, g)奖励写成r(s, a, g)。训练结束后你给不同的g它就追不同的结果。实现时环境通常会把状态拆成两部分desired_goal当前任务想要达成的目标和achieved_goal当前状态实际达成的情况。比如机械臂环境里desired_goal是目标物体的三维坐标achieved_goal是机械臂末端手爪的三维坐标。奖励函数只看两者差多远距离小于阈值给正奖励否则给负奖励。这种结构天然适合HER因为实际到达的位置本身就在状态里随时可以拎出来当伪目标。1.3 稀疏奖励是RL最硬的墙之一你可能会问为什么非要搞这套花活把奖励做稠密不就行了吗理论上是实际操作往往不行。很多真实任务根本给不出稠密奖励你无法为走到门边的每个半步都定义合理得分更别说像写出一段好代码这种抽象目标。即便能定义人工设计的稠密奖励还经常诱导agent钻漏洞——研究人员叫它reward hacking。另一个角度是探索效率。稀疏奖励下随机策略撞到成功状态的概率极低等于在沙漠里找一口井。HER提供的不是更大的沙漠地图而是把任何一口你路过的水坑都标记成井的能力。目标条件策略一旦被训练好agent在无数个伪目标上都有了往那个方向推进的能力这些能力叠加起来它找到真正目标的概率就高多了。2. HER核心机制重写一段transiton的全过程2.1 先看一整个episode如何变成多条有效经验假设一次rollout跑出了这条序列s_0机械臂在初始位置a_0向左移动s_1抓到了半空中...s_T末端位置停留在(0.3, 0.8, 0.4)目标A在(0.1, 0.2, 0.5)判定失败如果用原始数据整条episode的奖励全是-1对策略几乎没有训练价值。HER的介入方式是从这条episode里随机挑几个状态或挑最后一个状态把它们的位置坐标当作新目标B、C、D然后对这条轨迹里的transition逐条重新计算奖励。原来从(0.3,0.8,0.4)再走一步这个transition在新目标到达(0.3,0.8,0.4)的设定下就变成了一个成功收尾的组合。这里有个关键操作不是只重标最后一步而是把该transition之后的某个状态作为新目标并且要用这个新目标替换掉transition里的desired_goal。这样做的好处是大量原本无梯度的transition重新获得了学习信号。2.2 单条transition的重标定细节拆解把问题缩小到一条transition上。原始数据长这样s_t, a_t, reward-1, s_{t1}desired_goalG_oldHER的第一步先决定新目标G_new从哪来。论文里常用的三种策略后面会细表最简单的future策略是从t1时刻到episode结束之间随机挑一个状态u令G_new achieved_goal(s_u)。第二步把这条transition重写成s_t, a_t, reward_new, s_{t1}desired_goalG_newreward_new怎么算用环境原本的奖励函数但目标是G_new。如果s_{t1}和G_new足够接近reward_new就是正奖励同时done标记也应当改成true否则还是负奖励done保持false。很多人会在这里翻车——光换了目标忘了同步调done标志导致环境终止信号和奖励自相矛盾训练直接崩。第三步把重写后的transition和原始transition一起存进经验池。也就是说每条原始transition最终会变成1K条数据。2.3 HER优化了什么又没什么都优化HER本质上是在拓宽目标空间里的正反馈覆盖范围。它不改变策略的架构不增加探索噪声也不直接解决全局最优问题它只是让每个已经发生过的事实状态都能被当成潜在目标来学习。这样策略看到的目标分布从只有少数几个任务主目标变成了接近整个可达状态空间。明白这一点很重要。HER最大的优势场景是目标是可达到状态可达状态都在状态空间里且奖励稀疏。如果奖励本身已经非常稠密额外做的重标定并不会带来多少收益还会白白增加计算量和内存占用。它也不是万能的如果目标根本不属于状态空间比如考到100分而状态里只有答对的题数那这套伪目标替换就没法直接套用。3. 从理论到代码一个可运行落地的最小HER训练闭环3.1 数据流和环境设计实操之前先确认你的环境是否具备HER条件obs是否包含achieved_goal和desired_goal奖励能否根据这两个量独立计算如果是自定义环境最好把奖励函数写成独立的compute_reward(achieved_goal, desired_goal)而不是藏在step里——这样重标定阶段才能方便地复用。以OpenAI Gym的FetchReach为例观测空间分成三块observation机械臂本体状态、achieved_goal末端三维坐标、desired_goal目标三维坐标。你在采集transition时要把三块一起存下来。3.2 核心代码一个带HER的经验池封装我把经验池封装成下面这个样子。它只做两件事接收一条完整episode把原始transition存入内存再按策略生成K条HER transition存入内存。import random import numpy as np class HERBuffer: def __init__(self, capacity, strategyfuture, K4, success_threshold0.05): self.capacity capacity self.strategy strategy self.K K self.threshold success_threshold self.buffer [] self.pos 0 def compute_reward(self, achieved, desired): # 稀疏奖励欧氏距离小于阈值视为成功 dist np.linalg.norm(achieved - desired) return 1.0 if dist self.threshold else 0.0 def sample_goal(self, episode, t): # episode是list(obs, action, obs_next, done)已按时间排序 if self.strategy final: return episode[-1][2][achieved_goal] # 最后一个obs_next里的achieved_goal if self.strategy episode: idx random.randint(0, len(episode) - 2) return episode[idx][2][achieved_goal] if self.strategy future: idx random.randint(t 1, len(episode) - 2) return episode[idx][2][achieved_goal] raise ValueError(unknown strategy) def add_transition(self, obs, action, reward, obs_next, done): if len(self.buffer) self.capacity: self.buffer.append((obs, action, reward, obs_next, done)) else: self.buffer[self.pos] (obs, action, reward, obs_next, done) self.pos (self.pos 1) % self.capacity def add_episode(self, episode): T len(episode) for t in range(T - 1): obs, action, _, obs_next, _ episode[t] # 用原始目标算一次奖励这里假设episode里已算好 origin_reward self.compute_reward(obs_next[achieved_goal], obs_next[desired_goal]) origin_done origin_reward 1.0 self.add_transition(obs, action, origin_reward, obs_next, origin_done) # 生成K条HER经验 for _ in range(self.K): new_goal self.sample_goal(episode, t) her_reward self.compute_reward(obs_next[achieved_goal], new_goal) her_done her_reward 1.0 new_obs dict(obs) new_obs[desired_goal] new_goal new_obs_next dict(obs_next) new_obs_next[desired_goal] new_goal self.add_transition(new_obs, action, her_reward, new_obs_next, her_done)这个封装有个细节值得留意原始transition的reward也要用compute_reward重新算一遍而不是直接采用环境返回的值。因为你把new_goal塞进obs_next后done标志必须与reward保持一致否则部分算法尤其是DQN系会学到自相矛盾的Q值。3.3 训练主循环与参数速查主循环的骨架不复杂难点都集中在数据组织上buffer HERBuffer(capacity1_000_000, strategyfuture, K4) for epoch in range(MAX_EPOCHS): episode roll_one_episode(env, policy, herTrue) buffer.add_episode(episode) for _ in range(UPDATE_STEPS): batch buffer.sample(batch_size256) update_policy(policy, batch)关键参数我整理成一张表按我自己的调参习惯给推荐值参数推荐值说明K4每条原始transition额外生成的重标定数量strategyfuture伪目标采样策略后面会详细对比buffer容量1e6目标条件任务经验池要尽量大batch_size256过小会导致伪目标分布不稳定成功阈值0.05欧氏距离取决于环境本身过大过小都会让重标定失效原始数据占比100%保留不要为了省空间丢弃原始transition训练后端的RL算法建议先用DDPG或SAC这类适合连续控制的演员-评论家方法把HER当作一个数据增强模块挂上去。如果你用的是PPO需要额外处理on-policy的样本复用问题HER的收益会被削弱不少这点后面会再提。4. 训练中真正会踩的坑伪目标采样、K值与调平衡4.1 final / episode / future 三种采样策略怎么选论文里对比了三种主要的伪目标采样策略我实践中总结出的差异看这张表策略取法优点缺点final取episode最后一个状态计算量最小实现最简单伪目标种类单一策略容易过拟合到终点即目标episode从整条episode里均匀随机取一点作为伪目标目标分布更均匀覆盖更多中间状态会出现伪目标在transition之前的时间错位逻辑上有点别扭future从当前transition之后的剩余轨迹里取伪目标符合因果时序目标与当前状态有时间关联计算开销略高但几乎总是最稳我个人的结论是默认直接用future别犹豫。future保证了这个目标是在动作执行之后才选定的满足事后视角的直觉而且经验池里的伪目标分布更贴近真实训练分布。episode策略做出来的伪目标与transition之间没有先后约束某些时刻会让策略学到从没发生过的因果路径。4.2 K值不是越大越好K值代表每条原始transition额外派生出多少条HER数据。K4是论文和大多数开源项目的默认值。调大K确实能更快丰富目标覆盖但代价有三经验池里真实目标的比例被稀释、每次训练更新的数据量剧增、收益呈现递减。如果遇到成功率卡住不动不妨先检查是不是K太大导致agent被伪成功惯坏了——这类任务的目标空间极大重标定样本占绝对多数时策略会变得擅长追任何状态却对用户指定的真实目标敏感度下降。我习惯把K控制在2到6之间并始终保留一条规则每条原始transition的原始版本必须留在buffer里且容量要设计成原始数据不会被重标定数据挤出去。4.3 三个容易让HER静默失效的细节第一个是伪目标与done标志不同步。前面代码里已经踩过这个坑改了desired_goal就必须重算reward和done。不少首次尝试HER的人只替换了goal字段结果环境的终止条件还是基于原始目标判断训练时Q值一会说这一步是终点一会又说不是终点梯度就来回来去打架。第二个是伪目标必须来自同一episode。有些人为了省内存把目标池单独抽出来随机塞一个其他episode的状态当伪目标。表面看都是可达状态实际会让算法学到毫无来由的目标切换训练曲线一开始很好看后期必然发散。第三个是对成功episode过度重标定。如果本来的episode已经成功完成了原始目标就别再给它配一堆伪目标了。这类样本的价值就在它完成了真实目标重标定后反而稀释了真实成功信号。建议只对失败episode执行HER或者对成功episode使用很小的K值。4.4 调参时盯着什么指标调参不能只看最终成功率我通常同时盯四样东西训练中每100个episode的滑动成功率、平均Q值分布、奖励分布、以及重标定样本与原始样本的比例。平均Q值如果持续升高但成功率没起来大概率是reward和done逻辑出了偏差先去查伪目标替换如果Q值稳定而成功率缓慢爬升那是正常的学习节奏耐心等即可。另外on-policy算法如PPO与HER的配合天生别扭。HER改写了历史数据的目标相当于破坏了当前策略的采样分布再用重要性采样去纠正会引入不小的偏差。真想用PPO建议把HER数据当作额外的辅助监督信号而不是混进主采样流。5. 把事后视角带到项目复盘HER思维不止属于强化学习5.1 重新定义复盘里的目标我做了几年项目后发现技术圈里的复盘经常变成责任划定现场。失败的项目里每个人都在辩解我没达成目标但很少有人重新问一句如果把这次实际完成的东西重新定义成目标我们在这条路上有哪些动作其实是高效且正确的HER给了我一个更柔和的复盘结构先承认原始目标没达成然后换一套标准把实际到达的状态看成伪目标去复盘这个伪目标下哪些环节是成功的。比如新产品上线后活跃度不达标但你们意外打通了某个用户分层工具那就可以把打通分层工具当作一个新目标去挖掘那条路径上沉淀了什么。这种做法不是为了自我安慰而是为了给下一次决策留下可复用的行为策略。5.2 为未达成的目标主动留数据太多团队只保存成功案例、只归档最佳实践失败项目结案后就清理现场。用HER的眼光看这等于把大量伪目标样本直接丢掉了。我现在的做法是每个项目不管成不成都要留下三类记录做了哪些决策、每个决策对应的反馈是什么、如果有机会重来一次哪一步会不同。记录这些的时候不许修改当时的判断只加事后视角的批注保证伪目标和原始样本都完整。这种积累方式的效果很慢但复利极高。存档满两个季度后你会发现很多新项目和旧存档之间都能形成伪目标-经验的匹配新项目里卡住的某一步早在半年前某个失败项目的最后阶段就做成过一次。5.3 用事后视角倒推事前检查清单这是我最受用的迁移HER是事后重标定但它训练出来的策略在预测时依然要面对真实目标。对应到做事上就是每次启动前用事后视角预演一遍。我会问自己一个问题假设这个项目三个月后注定失败最可能的原因是什么然后针对这三个原因在起点就布好检查点。这一招的心理学原理和后见之明正好相反——它是把后见之明的能力前置。人类天生擅长事后解释那就把这种解释能力挪到事前假装自己已经从未来穿越回来然后往下拆解。我试过几次每次拆出来的风险项比团队头脑风暴挤出来的靠谱得多因为顺着已然失败这个前提倒推人们敢于说出平时碍于面子不敢提的隐患。6. 我实际使用HER的习惯与几个进阶方向先交代一下我的默认配置只要目标是可达状态且奖励是稀疏的我一定开HER策略后端优先SACK取4strategy取future成功阈值按环境的物理尺度定。经验池直接复用上面的HERBuffer只在奖励函数接缝处做适配。进阶方向上最值得做的是把HER和课程学习curriculum learning结合。HER虽然能极大缓解稀疏奖励问题但还是需要agent至少撞到过一定的状态覆盖。简单做法是先让agent在距离目标较近的初始分布上训练等成功率超过某个阈值再把初始分布拉远。另一个方向是学一个goal embedding把高维状态压成低维表示后再做伪目标匹配这能处理目标不是原始坐标而是复杂图像的任务代价是实现复杂度会跳一个台阶。有个小技巧分享给做机器人控制的朋友HER成功后不要急着关掉探索噪声。HER擅长建立从任意状态到任意可达状态的能力但面对真实任务指标策略还需要在动作空间里主动搜索。实践里我通常把噪声方差维持在中等水平让agent既依赖HER提供的泛化能力也不丢掉对高奖励区域的微调能力。至于失败的案例我也经历过不少。最常见的是把HER一股脑塞进连续任务却没意识到连续任务的episode特别长future策略在高维状态下采到的伪目标和当前transition关联太弱导致Q值被大量低质量数据搅乱。后来我加了一条过滤伪目标与当前achieved_goal之间的距离不能超过某个上限太离谱的目标直接丢弃。这算是我给HER加的第一个自定义补丁效果立竿见影。最后再聊一个和算法无关的体会。hindsight这个词本身挺有意思它天生带着遗憾的意味但HER教会我把它看成一种数据增强的手段——遗憾不是用来懊恼的是用来重新标注经验的。下次项目没做成先别急着写总结把整个过程的原始数据留好再问自己如果这次的经验是一条HER transition那它的伪目标应该是什么这个问题的答案往往比检讨本身值钱得多。