“hindsight”这个词,做技术的人应该不陌生——字面意思是“事后视角”“后见之明”。但在强化学习圈子里,这个词已经基本被一个具体方法给占了:Hindsight Experience Replay,也就是HER,译作“事后经验回放”。我第一次在机器人抓取任务里被稀疏奖励折磨的时候,读到OpenAI那篇论文里用“事后经验回放”改写失败轨迹的做法,心里那股感觉是:原来“马后炮”也可以是一门正经手艺。这篇文章不聊论文复现的官话和PPT式总结,直接讲清楚HER从原理到落地的完整链路,以及那些论文里不会写的坑。适合正在做机器人控制、目标达成类任务、稀疏奖励场景,或者对强化学习感兴趣但被“0奖励地狱”逼疯的人。
1. 起底hindsight:这个名字背后的强化学习难题
1.1 稀疏奖励到底有多“坑”
先聊一个很多入门者都会撞上的场景。你想让机械臂把桌上的方块推到目标圆点,环境给的奖励极其吝啬:只有当抓手与目标的距离小于某个阈值,比如5厘米,才返回 +1,其他情况一律 0。这就是典型的稀疏奖励(sparse reward)问题。强化学习算法的核心依赖奖励信号来更新策略,如果试了几万步、几百万步全是0,那智能体完全无法判断哪个动作方向是对的,甚至会觉得“做什么都一样”,梯度根本传不下去。
我当年第一次在这个任务上跑DDPG,曲线就像心电图,loss不降,episode reward永远是个位数。后来逼急了把奖励函数改成连续距离奖励:距离越近reward越高。这个动作立刻让训练变稳定,但代价是人工设计奖励的“气味”太浓——稍微改个任务、换台机器人、换个障碍物位置,奖励函数可能就要从头调。更别提真实环境里很多任务本身就是“要么成功要么失败”,你根本没有中间指标可写。
这正是HER出现的大背景:它想解决的不是“奖励噪声大”的问题,而是“奖励几乎全为0”的问题。它的解决方案不是给你一把更精巧的奖励工程工具,而是换一个视角看待已有的失败数据:一条轨迹没有做到你指定的目标,但如果你把目标改成“它最后实际到达的状态”,这条轨迹就变成成功轨迹了。这个思路听着像耍赖,但它确实在数学上让那些原本无监督的样本重新长出了梯度。
1.2 从“事后视角”找答案的基本思路
把这个思路落到算法层面,核心叫“目标重标注”(goal relabeling)。举个例子。你的机械臂实验目标是“把方块推到坐标(1.0, 0.5)”,智能体试了一整条轨迹,最后停在(0.8, 0.2)。在原本的奖励规则下,这条轨迹从头到尾都是0分,算法学不到任何东西。但HER会问一个问题:如果我们的目标不是(1.0, 0.5),而是(0.8, 0.2),这条轨迹是不是就“成功”了?于是它构造出一组新的经验样本,仍然使用同样的状态序列、动作序列,但把目标字段替换为(0.8, 0.2),并按照新目标重新计算奖励。到了轨迹末尾,距离误差为0,奖励是+1,这组经验就成了正面样本。
用一句话概括:HER让智能体从“我没有完成目标”的失败经历中,提炼出“如果我把目标定成最后走到的地方,那么我确实完成了它”的正确知识。知识本身没有变,但样本的价值从“废数据”变成了“收益数据”。
这种思路背后还有一个哲学层面的合理性:在目标条件化强化学习(Goal-Conditioned RL)里,同一个状态转移其实可以同时服务很多个目标。你从A走到B,这条轨迹对“目标=B”来说极其珍贵;对“目标=C”可能无用,但你不能说它是错的。HER就是把这种“多目标视角”显式地注入到经验回放之中,让一条轨迹的平均信息量显著提升。我们常说的“失败是成功之母”,在这里不是鸡汤,是梯度。
2. 拆解HER的完整实现逻辑
2.1 目标重标注时发生了什么
要落地HER,你得明白重标注过程中具体改变了哪些字段,以及哪些字段不能动。假设我们有一个转移组 $(s_t, a_t, g, r_t, s_{t+1}, done_t)$,原始经验里 $g$ 是期望目标。做完重标注后,我们把 $g$ 换成 $g'$,然后基于 $g'$ 重新计算:
- 新奖励:$r'_t = r(s_t, a_t, g')$
- 新终止标志:$done't = 1$ 当且仅当 $s{t+1}$ 已经达到 $g'$(通常用距离阈值判断)
注意,$(s_t, a_t, s_{t+1})$ 这三个字段完全不用动,因为它们是环境实测得到的事实。动作动作对不对,是相对于目标来说的;轨迹本身是发生了的事情。这就是HER最干净的地方——重标注不改物理事实,只改“这个尝试是在追求什么”这一层语义。
在实践上,大多数复现框架会把单个transition存成字典:dict(obs=..., g=..., action=..., reward=..., next_obs=..., done=...),采样时用一个小概率或固定比例触发重标注。触发后再把新的字典送入RL算法的更新逻辑,比如DDPG、SAC或TD3的critic更新,一模一样,不需要改底层优化器。
2.2 四种目标采样策略的取舍
HER论文里比较了四种采样策略:final、future、episode、random。名字看着抽象,实际上各有各的脾气。
final:直接用整条轨迹的最终状态 $s_T$ 作为新的目标。最简单,在很多机器人任务里已经够用。按我的经验,final策略会让“推到目标点”这类任务快速形成正确的后验知识,因为你总是在教智能体“最后到达的地方就是目标,动作序列是成功的”。
future:从当前时间步 $t$ 之后的某个状态 $s_{t+k}$ 作为新目标。比final更灵活,因为它能把一条轨迹拆出多个“中期成功”的视角,让样本利用率更高。论文和大量复现的结果都表明future通常优于final,一般$k$为一个均匀随机值,范围在$[1, T-t]$之间。
episode:随机从整条轨迹里选一个状态当目标,完全无视时间顺序。好处是目标多样性爆炸,坏处是可能生成一些语义上很别扭的样本,比如“目标是第一步的状态,但你后面做的动作跟这个已经没有关系”,训练噪声相对高。
random:从全局所有状态里随机抽,完全不管轨迹内关系。覆盖面最大,但成功信号最稀薄,一般只作为辅助。
做一个排序表格的话,我的实际体验是:future 优于 final,episode 做补充,random 极少单独用。所以默认配置通常是 future 策略 + 每条轨迹重标注4个新目标($k=4$)。这个 $k=4$ 是论文里的常用参数,实操复现也少见翻车。
2.3 HER在整个训练流程中的插入位置
很多人把HER理解成一个“新的RL算法”,其实不是。HER是一个数据增强模块,插在经验回放和策略更新之间。训练流程大概是:
- 用当前策略跑若干条完整轨迹;
- 每条轨迹存下来,同时在回放池里额外写入若干条“重标注目标”后的转移;
- 每次从回放池中采样时,会同时采到原始经验和重标注经验;
- 用这批量经验去做标准 off-policy 更新(DDPG/SAC/TD3 等)。
为什么必须是 off-policy 算法?因为重标注经验相当于“用另一个目标下的奖励来解释相同动作”,它和数据收集时的行为策略并不完全一致。这种做法没法塞进 on-policy 的 PPO 里直接跑,至少不能用普通方式跑。实际工程里,大家几乎默认把HER和off-policy算法绑定。这不是便利性的问题,是数学上前置条件的问题。我看到过一些强行在PPO上改HER的尝试,曲线极其痛苦,后面在常见问题里细讲。
3. 手把手实现一个HER版本
3.1 环境、观测与目标的表示
动手写代码前,先想清楚目标怎么表示。要使用HER,环境里必须有“目标条件”的概念,也就是智能体的观测里要么包含目标信息,要么可以拼接目标信息。以机械臂推方块任务为例,通常的观测向量是机械臂关节角度、方块位置、目标位置等多个连续数值拼在一起。如果目标只是目标位置,那么设obs_dim = agent_obs_dim + goal_dim就是常见做法。
我建议在实现前先明确三件东西:
- 状态向量
s:物理状态,比如指尖位置、物体位置; - 目标向量
g:期望物体位置,或者期望相对位置差; - 距离函数
d(s, g):判断是否达成目标,常用欧氏距离。
这三点定了,后面的一切都很顺。如果你使用的环境本身不带目标字段(比如很多OpenAI Gym经典控制任务),那就需要先改造成Goal-Conditioned形式,否则HER无从下手。我在实际项目里试过直接把HER套到Pendulum上,结果当然是无效,因为目标根本没进到策略输入里,算法再怎么重标注也是白搭。
3.2 核心代码逻辑与数据流演示
下面给出一段简化版的HER回放池实现,语言用的是Python,框架层面不依赖特定库,方便迁移到PyTorch版本。核心就是在保存轨迹时多做一步重标注。
import numpy as np from collections import deque class HERReplayBuffer: def __init__(self, capacity, relabel_k=4, strategy="future"): self.buffer = deque(maxlen=capacity) self.relabel_k = relabel_k self.strategy = strategy def push_episode(self, episode): # episode: list of dicts with keys: obs, g, act, rew, next_obs, done # 原始经验直接入库 for transition in episode: self.buffer.append(transition) # 对这条轨迹做 target relabel self._relabel_episode(episode) def _relabel_episode(self, episode): length = len(episode) for idx, transition in enumerate(episode): for _ in range(self.relabel_k): if self.strategy == "final": new_goal = episode[-1]["next_obs"][目标维度起点:目标维度终点] elif self.strategy == "future": future_idx = np.random.randint(idx + 1, length + 1) new_goal = episode[future_idx - 1]["next_obs"][目标维度起点:目标维度终点] elif self.strategy == "episode": random_idx = np.random.randint(0, length) new_goal = episode[random_idx]["next_obs"][目标维度起点:目标维度终点] else: raise ValueError("unknown strategy") # 重新计算奖励 new_reward = self._compute_reward(transition["next_obs"], new_goal) new_done = self._check_goal(transition["next_obs"], new_goal) new_transition = { "obs": transition["obs"], "g": new_goal, "act": transition["act"], "rew": new_reward, "next_obs": transition["next_obs"], "done": new_done, } self.buffer.append(new_transition) def sample(self, batch_size): indices = np.random.choice(len(self.buffer), batch_size, replace=False) batch = [self.buffer[i] for i in indices] return map(np.array, zip(*batch))代码里目标维度起点:目标维度终点这一行,就是抽取“目标在拼接观测里的那段切片”,换成你自己的实际维度就好。_compute_reward返回形如“距离小于阈值给1,否则给0”的标量。走到这一步,你已经得到了带HER重标注的回放池,至于用DDPG还是SAC更新,完全照常规来。
另外一个容易被忽略的点:critic更新时用的目标 Q 值,需要用重标注后的new_goal、new_reward计算,不能用原始转移里的旧奖励。很多人复现HER失败,问题就出在这——重标注了奖励,但critic的 target 还拿着旧的done和reward在算,整个loss就乱了。奖励和终止标志必须同步替换。
3.3 超参数选择与训练节奏参考
HER本身并没有新增太多超参数,但“重标注比例”和“目标采样策略”这两个需要盯着。直接把我的常用配置列出来作为起点:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| relabel_k | 4 | 每条轨迹额外生成4条重标注经验 |
| strategy | future | 在四项策略里最稳定 |
| future范围 | 当前步之后均匀采样 | 论文默认 |
| HER经验占比 | 50%左右 | 采样时原始经验与重标注经验各半 |
| 奖励阈值 | 任务具体设置 | 通常取目标半径,如0.05 |
| 算法主体 | SAC / TD3 / DDPG | 任选,不影响HER核心逻辑 |
训练节奏上有个经验:HER不是“灵丹妙药”,它更适合在初期快速建立“基础行为”阶段使用。策略稍微成型后,重标注带来的边际收益会下降,因为这时候原始成功样本已经不少了。你可以按训练进度把重标注比例适当调低,比如从4降到2甚至1,节省计算量的同时避免目标分布过宽导致策略不稳定。这个调法不是论文标准,而是我对照长尾训练曲线观察到的现象,姑且算是一个工程心得。
4. 实战中的坑与排查记录
4.1 什么时候HER收益最大,什么时候基本没用
不是所有任务都适合HER。我踩过的第一条坑就是在连续控制任务里把它当成万能大法。HER的本质是“把目标改到现状”,这个逻辑成立的前提是:任务里有一个清晰可定义的目标向量,且该目标可以直接放进状态/奖励函数。如果你的任务是“保持倒立摆不倒”这类连续维持型任务,没有明确的终态目标,HER就没有可操作的定义;如果你的奖励是“每步自动积累能量”的持续性反馈,重标注目标反而会把环境动力学扭曲成错误信号。
那哪些任务收益最大?我总结出三个特征:
- 任务有明确的终态成功判据,比如“物体到达目标区域”;
- 单条轨迹内部存在时间关联,后期状态可能与某个合理目标高度相关;
- 环境允许你用距离函数计算奖励,而不是依赖一个黑盒开关式奖励。
符合这些条件的,典型就是机械臂抓取、推动、到达,以及各类平面导航。在这类任务里,HER带来的提升经常是“从完全学不会到收敛成功”。它解决的不是优化难度,而是信号稀疏度。
相反,如果任务奖励密度已经很高,比如每一步都有连续奖励,HER会显得多余甚至有害。重标注会让策略学会一种“默认目标偏移”的错觉,训练出来的行为可能在原始目标下并不可用。
4.2 目标域与观测域的数据范围问题
重标注后的目标是从“轨迹中实际出现的状态”里采的,所以有个隐藏风险:如果轨迹状态分布一直在环境允许的局部区域打转,重标注目标也只在同样的局部区域里打转,可能永远覆盖不了你用真实目标希望到达的区域。比如机械臂初始位置在左侧,永远只能推到左侧,重标注目标也全在左侧,那么策略对“推到右侧”这个真实目标一无所知。
解决办法有几个。一是初始时用随机策略或人为扰动多采集一些多样性轨迹,让状态的足迹铺得够开;二是把重标注目标的比例和范围做大,不止用轨迹末端,还要用中间状态;三是可以适当混合random策略采样全局状态库里的目标来增强覆盖。当然这会引入噪声,需要调平衡。我在实际调试时习惯观察“重标注目标的分布散点图”,如果全部挤成一小团,说明探索严重不足,再怎么加HER都不解决根本问题。
另外必须处理的点是目标空间的归一化。目标动辄是三维坐标、七维位姿、甚至更高维,不做归一化的话,距离函数对数值大的维度极度敏感,重标注生成的奖励会偏向某个轴。我一般会维护一个目标的运行均值和方差,在做奖励计算前先做标准化。这个操作时常被忽略,但直接影响训练稳定性。
4.3 常见失败模式速查表
下面这张表是几个我在复现HER和相关变形时遇到的高频问题,附上排查思路,方便直接对号入座。
| 表现 | 可能原因 | 排查方向 |
|---|---|---|
| 训练初期就loss爆炸 | 重标注经验比例过高 | 把relabel_k降到2或1,观察loss曲线 |
| 策略学到“原地飘” | 目标重标注偏向局部状态 | 增强探索,加入随机动作噪声,扩大目标采样范围 |
| 真实目标下永远失败,重标注目标却很成功 | 目标分布与真实分布错位 | 检查目标归一化,增加原始经验占比 |
| 训练后期平台期,成功率不再涨 | HER收益饱和 | 把relabel_k调低,让策略专注真实目标优化 |
| 对真实部署机器人的效果不稳 | 仿真与真实状态分布差异大 | 建议先用更保守的域随机化,HER只是数据端手段 |
还有一个实战体会:HER能缓解稀疏奖励问题,但不能替代探索。如果你的策略在初始阶段连目标区域的边都没摸到,重标注再怎么挖数据,也只是在“原地徘徊”这段轨迹上反复解说。这种情况下要不要先加一点更激进的动作噪声,或者用课程学习把初始状态库从近目标区域开始,往往是决定项目能不能跑通的关键。我做过一个抓取项目,HER加了,效果平平;后来把初始位置分布改成离目标近一点开始训练,成功率立刻上了一个台阶。HER是个好助手,但抓不住探索的主线,它也很难独自扭转局面。
5. 我的一些实操体会
聊到这儿,我脑子里其实浮现出很多次调试画面的碎片:一开始看到重标注样本在tensorboard里混进回放池时,总有一种“自己在刷数据”的心虚感;等训练中期真的看到那条成功率曲线从0开始抬头,才明白这个方法的合理性不仅在理论上,也在工程上经得起检验。它比起精心调奖励函数,更像是在教你如何从已有的失败记录里重新解读价值——而这一点,恰好是很多强化学习项目里最容易被忽略的环节。
最后再分享一个我实验时的个人习惯:在跑HER之前,先跑一个“手工目标版”的基线,比如把每条轨迹的真实目标直接换成轨迹末状态,估算一下环境本身的信息含量。这个做法成本极低,却能帮你判断你的任务到底缺的是探索还是缺的是信号。如果连手工改目标都学不出来,那HER大概率也救不回来。工程的活就是这样,先分清最难的部分在哪,再决定要不要动用什么工具,可能比盲目套用最新论文方法更有效。