如果让我用一句话来概括 Hindsight Experience Replay,也就是我们常说的 HER,那么它解决的问题非常朴素:强化学习智能体能不能像人一样,在失败之后停下来说一句“刚才我虽然没做到 A,但我其实做到了 B,那我把 B 当成目标再学一次”?这个思路最早吸引我,是我在跑机械臂推方块实验的时候。目标点是桌面上的某个固定位置,奖励设计得很稀疏,做到就给 1,做不到就是 0。结果智能体跑了一整夜,永远在那几个角落打转,完全不收敛。不是网络太弱,也不是超参数崩了,是它在整个探索空间里几乎拿不到任何正向反馈。后来我把 HER 接上,效果立竿见影。这篇文章就围绕 hindsight 这个关键词,把它的原理、实现方式、目标选择策略、调参经验,以及我实际踩过的坑,一次性讲透。如果你是做机器人控制、稀疏奖励任务或者多步规划决策的,这篇内容可以直接拿去参考。
1. 为什么“事后复盘”比“事前设计奖励”更香
1.1 稀疏奖励是强化学习的隐形天花板
很多人一开始上手强化学习,都是从 CartPole、MountainCar 这类环境入的门。这类环境里奖励几乎是连续可得的,杆子每多立一秒都有反馈,小车每往东挪一点也有累积信号。但真实的控制任务根本不是这个画风。机械臂抓取没有物体在手里的中间奖励,无人车泊车没有接近车位就自动加分的信号,游戏里的隐藏关更是只有通关才知道。奖励越稀疏,智能体能获得的梯度信号就越少,随机探索就像你在一个巨大的仓库里闭着眼找一把钥匙,摸到门把手之前,所有动作都被判定为“无用”。
传统做法是设计密集奖励,也就是人工势场那一套,给智能体铺一条看得见的梯度坡道。比如按照离目标点的欧氏距离给负奖励,离得越近惩罚越小。听起来合理,做起来全是坑。首先是 reward shaping 容易引入局部最优,智能体很可能找到一个“既不靠近目标、但又能稳定获得比乱动更高回报”的姿势,然后卡死在那里。其次是目标稍微变一下,整套奖励函数就得重新调,环境一换,之前的努力全部作废。密集奖励本质上是在用人的先验知识替智能体做规划,这与强化学习自己探索的初衷是冲突的。
hindsight 这类方法解决的是另一条路:不改变环境奖励,而是改变智能体的“记忆方式”。它让智能体学会从已经发生的轨迹里提取有价值的信息,哪怕这条轨迹没有达成原始目标。
1.2 hindsight 到底在“复盘”什么
我先举一个日常生活的例子。你周末想去一家新开的火锅店,结果导航出错,把你带到了旁边一条全是奶茶店的巷子。你走进一家不起眼的奶茶店,发现他家的杨枝甘露意外好喝。虽然这趟行程没有达成“吃火锅”的目标,但你获得了另一个有效认知:这条巷子有家奶茶店不错。下一次你想约朋友喝下午茶,你会直接想到这家店。
HER 做的事情完全一样。在一次强化学习的 episode 中,假设原始目标是 $g$,智能体从状态 $s_0$ 出发,执行了一系列动作,最终却停在状态 $s_T$。按照原始目标去计算奖励,整条轨迹的 reward 全是 0,没有任何学习价值。但 hindsight 的做法是:把“最终实际达到的状态 $s_T$”当作这次 episode 的新目标,重新计算每一步的奖励。你会发现,如果以 $s_T$ 为目标,这条轨迹其实是完美成功的,每一步都能得到正向奖励。
这个操作的本质是通过修改采样数据来制造一条“成功轨迹”。原轨迹不用重跑,同一批状态和动作,只要换一个目标去解读,就凭空多出一批高质量训练样本。这就是“事后复盘”的核心:不是让智能体在环境里多转几圈,而是在已有经验里多挖掘一层信息。
1.3 一个关键前提:目标条件策略
HER 使用的基础模型不是普通的 Q 网络或策略网络,而是目标条件策略,也就是网络输入里除了状态 $s$,还要额外带一个目标 $g$。策略输出动作时,必须同时回答“我现在在哪”和“我想去哪”。这样的设计是为了让同一个策略能够被反复套用到不同目标上,也是后续换目标重算奖励能生效的前提。
举个例子。状态是机械臂关节角度和末端位置,目标可以是桌面上的一个坐标点。网络输入拼接状态和目标,输出动作。当 HER 把目标从“左上角的目标点 A”换成“当前实际到达的右下角位置 B”时,策略会自动把这个新目标 B 当作控制指令来处理。这种网络结构在 robotic manipulation 领域非常常见,DDPG、TD3、SAC 都能很自然地扩展到这种目标条件版本。
2. 拆解 HER 的核心机制:目标重放与四种目标选择策略
2.1 轨迹中目标的“二次标注”过程
我们先看一条原始经验轨迹的组成。一个 transition 通常记作 $(s_t, a_t, s_{t+1}, g, r_t)$,其中 $g$ 是这个 episode 希望达成的目标,$r_t$ 是根据 $g$ 计算出的即时奖励。在多步 episode 中,假设 episode 长度为 $T$,那么轨迹包含 $T$ 个这样的 transition。原始轨迹因为最终没有到达 $g$,所以绝大多数 $r_t$ 都是 0。
hindsight 重放时,我随机给这条轨迹指定一个新目标 $g'$。这个 $g'$ 往往来自轨迹中某个实际访问到的状态。然后整条轨迹的 transition 重写为 $(s_t, a_t, s_{t+1}, g', r_t')$。这个 $r_t'$ 由新目标 $g'$ 和 $s_{t+1}$ 的接近程度决定,如果足够接近,就设为正的奖励,否则设为 0。原来不足 $T$ 步就能到达的位置,现在因为目标换了,原本的时序关系依然有效,相当于轨迹内部的时间顺序没有变,只是“目的地”变了。
这里有一个很容易忽略的细节:轨迹重放时也会重写 episode 结束标志。原始轨迹因为没到达目标,很多情况下被标记为未完成;但以实际到达状态作为新目标后,轨迹末尾就变成完成状态了。如果算法里有单独给 done 标志加惩罚或折扣的逻辑,这个变化会显著影响训练信号分布。我见过不少人只改了 reward 忘了改 done,结果训练过程反复震荡。
2.2 四种目标选择策略:final、future、episode、random
HER 论文里测试了四种从轨迹中选择新目标的方式,尽管后来实践中大家基本都默认用 future,但理解四种策略仍然能帮助你判断自己的任务适合哪一种。
第一种是 final。整条 episode 结束后,只把终点状态 $s_T$ 作为新的目标,所有 transition 全部使用同一个 $g'$。优点是最接近“事后复盘”的直觉,缺点也很直观,如果轨迹特别长、状态变化很大,把最后一个状态当成所有中间步骤的目标,中间很多状态可能与终点状态差异巨大,学习信号会变弱。
第二种是 future。从时刻 $t$ 开始的 transition 中,新目标 $g'$ 从 $t+1$ 到 $T$ 之间的某个状态里随机抽取。这个策略的关键点在于,新目标总是与当前 transition 的下一步状态更接近,所以重新标注后的轨迹每一步都显得“离目标更近一点”,学习信号更强。这也是未来目标策略在实践中效果最好的原因。
第三种是 episode。从同一条轨迹的所有状态里随机选一个作为目标,不考虑时间先后。这种策略实现最简单,但效果和未来目标策略相比有明显的退化,因为它可能把早期状态当作后期 transition 的目标,导致智能体学习到“时间越走越离目标越远”的错误关联。
第四种是 random。整条轨迹使用一个与这条轨迹完全无关的随机目标。这个策略基本只用于基线对比,效果最差,因为它完全不引用轨迹本身的信息。
表格对比会更直观:
| 策略 | 目标来源 | 与轨迹时刻的关系 | 常见效果 |
|---|---|---|---|
| final | 轨迹终点状态 | 全区间同一目标 | 中低效 |
| future | 未来时刻状态 | 按时间向后采样 | 最佳 |
| episode | 本轨迹任意状态 | 无时间约束 | 中效 |
| random | 随机状态 | 无关联 | 较差 |
2.3 目标表示方式直接影响网络学习
不是随便一个状态向量都能当目标用的。你需要在设计状态表示时就想清楚:目标 $g$ 和状态 $s$ 是否处在同一个空间,或者是否通过一个稳定的映射函数关联起来。HER 最舒服的场景是目标就是状态空间的一部分,比如二维坐标、关节角度、像素位置,这样直接拿实际状态当目标即可。
如果目标是离散的,比如“开灯/关灯”“抓取红色物体/蓝色物体”,那么目标选择策略就要针对性修改。离散目标和连续状态之间的相似度度量需要单独定义,不能用欧氏距离。这种情况下,通常做法是引入一个目标编码函数,把离散目标编码成向量,再和状态拼接。还有一种情况是目标不可观测,比如目标是“某个隐藏物体的位置”,智能体永远看不到目标状态,只能通过传感器间接感知。这种环境下 HER 的收益会明显缩水,因为你无法从实际轨迹中提取到可用的目标表示,后面会细讲这个问题。
3. 上手实现:在 DDPG 类算法上接入 HER
3.1 先搭一个目标条件网络
我用 PyTorch 风格的伪代码来展示,实际项目里随时可以套进自己的框架。目标条件策略网络的结构一般是:输入状态和目标,各自通过一个编码层,然后拼接,再通过几层全连接输出动作。Q 网络也类似。
import torch import torch.nn as nn class GoalConditionedActor(nn.Module): def __init__(self, obs_dim, goal_dim, action_dim, hidden=256): super().__init__() self.obs_encoder = nn.Sequential( nn.Linear(obs_dim, hidden), nn.ReLU() ) self.goal_encoder = nn.Sequential( nn.Linear(goal_dim, hidden), nn.ReLU() ) self.fc = nn.Sequential( nn.Linear(hidden * 2, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, action_dim), nn.Tanh() ) def forward(self, obs, goal): h_obs = self.obs_encoder(obs) h_goal = self.goal_encoder(goal) h = torch.cat([h_obs, h_goal], dim=-1) return self.fc(h)这里有个容易被忽略的细节:Actor 网络的输出层使用了 Tanh,把动作限制在 [-1, 1]。如果你的环境动作范围不是这样,要在环境层面做动作缩放,而不要粗鲁地在网络后直接裁剪,否则梯度在饱和区会消失,影响探索效率。
Q 网络的输入则是状态、目标、动作三部分拼接。训练时目标条件 Q 函数需要估计的是 $Q(s, g, a)$,也就是在给定目标的前提下,状态动作对的价值。这一差异决定了 HER 重放的数据必须连带目标一起喂给网络,不能只替换状态动作。
3.2 判定奖励的改写函数
HER 里最核心的一小段代码就是根据新目标重算奖励。我通常封装一个函数,传入原 transition 和新目标,输出重写后的 transition。
def rewrite_transition(state, action, next_state, new_goal, threshold=0.05): # 计算 next_state 与新目标之间的距离 dist = torch.norm(next_state - new_goal, dim=-1) reward = torch.where(dist < threshold, torch.tensor(1.0), torch.tensor(0.0)) done = (dist < threshold).float() return state, action, reward, next_state, new_goal, donethreshold 的选取是这个函数成败的关键。threshold 太大,智能体很容易“碰到”目标,奖励饱和,失去精细调节的动力。threshold 太小,HER 产生的新轨迹中绝大多数 transition 仍然拿不到正奖励,就没有达到创造学习信号的目的。我的经验是先看环境的状态尺度,机械臂末端坐标如果是米为单位,0.05 米比较合理;如果是像素坐标,threshold 可能要放到 5 到 10 个像素。也可以做成随时间衰减的课程式 threshold,前期宽松一点让智能体先见识到成功,后期收紧逼它更精确。
3.3 完整主循环伪代码
下面给一个比较完整的 HER 采样和训练伪代码,配合 DDPG 类算法使用。这个伪代码去掉了冲突处理等外围逻辑,聚焦在“如何把 HER 数据加入到 replay buffer 里”这一块。
# replay buffer 额外保存目标字段 class HindsightBuffer: def __init__(self, capacity): self.capacity = capacity self.buffer = [] def push(self, transition): if len(self.buffer) >= self.capacity: self.buffer.pop(0) self.buffer.append(transition) def sample(self, batch_size): return random.sample(self.buffer, batch_size) def her_sample_a_batch(buffer, batch_size, k=4): normal_size = batch_size // (k + 1) her_size = batch_size - normal_size normal_batch = buffer.sample(normal_size) her_batch = [] for _ in range(her_size): # 随机抽取一条完整 episode episode = random.choice(completed_episodes) t = random.randint(0, len(episode) - 1) transition = episode[t] # 用 future 策略:从 t+1 到末尾随机选一个作为新目标 future_idx = random.randint(t + 1, len(episode)) new_goal = episode[future_idx].next_state rewritten = rewrite_transition( transition.state, transition.action, transition.next_state, new_goal ) her_batch.append(rewritten) return normal_batch + her_batch这段代码中一个关键点是completed_episodes的维护。为了生成 HER 数据,你不能丢失 episode 的结构信息,buffer 里存的不能只是一条一条切碎的 transition,还要保留哪些 transition 属于同一条 episode。所以实际工程中我会用一个字典结构,给每个 episode 一个唯一编号,训练完一个 episode 后把整条轨迹缓存起来,等采 HER 样本时直接从这些完整轨迹里抽取。
3.4 四比一的经验混合比例是怎么来的
原论文里提到,每一份原始经验,搭配大约四份 HER 重放经验,也就是 HER replay ratio 为 4。这个比例不是拍脑袋定的,它是在平衡“从失败中学习”和“不要忘掉真实目标分布”之间的关系。如果你把 HER 数据比例拉得太高,比如 10 比 1,智能体会被大量“容易达成”的替代目标淹没,对原始目标的真实分布反而失去敏感性;比例太低,比如 1 比 1,成功轨迹仍然太少,稀疏奖励问题又回来了。
我在做机器人抓取实验时,明显感受到 4 比 1 是一个甜点值。低于 3 时训练曲线会出现很长的平台期,高于 6 时原始目标上的 episode 平均回报会飘,出现训练后期掉点的现象。如果你的任务目标特别分散,可以试着把 her 比例加大到 5 或 6;如果目标空间很集中,适当降到 3 会更稳。
4. 调参与避坑:我踩过的那些 hindsight 相关的坑
4.1 目标必须满足“事后可观测”这一前提
HER 名字里有 hindsight,但它的实现非常依赖一个条件:新目标必须从轨迹中直接观察到。如果你的环境里目标是一个潜变量,比如“把钥匙插进锁孔”但锁孔位置被遮挡,实际轨迹中并没有可直接采样的锁孔状态,那 HER 就无从下手。
我曾经尝试在一个三指灵巧手的任务里用 HER,目标是“旋转阀门到指定角度”。阀门转动的真实角度可以从环境内部读出,但视觉输入里只能看到手指和阀门外观。我最初用了像素作为目标,结果 HER 效果极差,因为像素向量和关节控制之间的关联太稀疏,目标编码器学不出有效语义。后来我改回用机械臂关节角度加阀门角度作为目标表示,效果立刻上来了。所以如果你的任务视觉信息很丰富,还是要先抽取出结构化状态,再做目标重放。
4.2 没有正确维护 done 标志会导致价值崩塌
我前面提过一次 done 标志的问题,但值得单独拿出来说。HER 重写目标之后,原本很多未达成的轨迹变成了达成轨迹,这时候 transition 的 done 必须从 0 改成 1。否则,Critic 网络会学到“接近目标的状态并不是终止状态,我还需要继续动作”,这个信号是自相矛盾的。
在实际编码中,常见错误是只对最后一个 transition 的 done 做修改,而中间步骤的 done 全部保持 0。这在大多数环境里没有大问题,因为中间步骤本来就没真正到达目标。但如果你使用了 future 策略,新目标可能在第 5 步就已经到达,那么第 5 步之后的 transition 按新目标来看都处于“已经完成”状态。这种情况下,更稳妥的做法是把到达目标之后的所有后续过渡全部标记为 done,或者干脆截断轨迹,不把成功之后的抖动数据喂给网络。
4.3 把 HER 搬到多目标场景时的奖励尺度陷阱
多目标任务的难点在于不同目标的完成阈值可能不同。比如“移动到一个点”的阈值可以是 0.05,但“转动画到一个角度”的阈值可能是 5 度。如果统一用一个 threshold,某些目标会变得极其容易,另一些又极其困难。这会造成 HER 重放后的奖励分布不一致,训练信号忽大忽小。
我的解决思路是对目标做归一化。在 pre-processing 阶段把每个目标映射到一个标准空间,让“距离”的量纲尽量一致。比如点位置除以最大距离,角度除以 180 度,然后再算距离和 threshold。归一化后,整个训练过程稳定很多,也不再需要针对每个子任务单独调阈值。
4.4 常见问题速查表
| 症状 | 可能原因 | 建议解决方式 |
|---|---|---|
| 训练初期完全没有正向奖励 | threshold 过小,或探索噪声太大 | 放宽 threshold,限制探索噪声幅度 |
| 训练中后期原始目标回报掉点 | HER 比例过高 | 把 replay ratio 从 4 降到 2-3,稳定原始目标分布 |
| 智能体一直做小幅抖动的无效动作 | 目标编码维度太少,动作输出 Tanh 饱和 | 增加目标特征维度,检查动作缩放逻辑 |
| 多个目标训练效果差异极大 | 未做目标归一化 | 对目标空间做标准化,统一阈值 |
| 用 future 策略但训练发散 | future 采样区间过大 | 限制 future 采样范围,比如只从 t+1 到 t+10 采样 |
4.5 如何判断你的环境是否真的需要 HER
不是所有稀疏奖励任务都必须上 HER。如果环境本身就是单目标、无随机初始化、且初始位置固定,HER 的价值会打折扣,因为新目标虽然能造出更多成功轨迹,但是这些成功轨迹全都指向同一条路线,泛化意义不大。反过来,如果你的任务是多目标、多初始位置、多障碍布局,HER 相当于天然免费扩充了一倍以上的训练样本,它从“失败”中提取的成功经验能覆盖更多状态空间区域,泛化性会显著提高。
判断标准很直接:你可以先统计当前随机策略产生的 episode 中,有多少比例能到达目标。如果前 1000 个 episode 成功率趋近于 0,而且每个 episode 的轨迹形态差异很大,HER 大概率适合你。如果成功率本来就不是零,也可以用 HER,但收益没那么明显。
5. 我的实战心得与扩展想法
5.1 从单目标 HER 到多任务复用的自然延伸
HER 不只是解决单任务的稀疏奖励问题。我在后来的多任务机器人控制实验里发现,如果把任务描述也当作目标的一部分拼到网络中,HER 天然就能在多任务之间迁移经验。比如今天训练“推到左上角”,明天训练“推到右下角”,这两个任务在环境层面是两套不同的 reward 函数,但 HER 把失败轨迹重写之后,它们在目标空间里就是一体的。最后一次训练出的网络,输入不同目标坐标就能完成不同搬运任务,不需要重训。
这种把目标空间做大做满的思路,后来也启发我理解了离线强化学习中一些数据增强方法。很多 offline RL 算法在处理“次优轨迹”时,本质也是在做某种 hindsight 式的数据重写。只是它们不叫 HER,而是换了一套理论包装。
5.2 一小段值得反复琢磨的代码设计
在实现层面,我最推荐的工程习惯是把 HER 的目标采样逻辑与算法主体解耦。无论你用 DDPG、TD3 还是 SAC,HER 都只是数据层的一个 wrapper,不应该侵入策略更新逻辑。这样你可以在不同算法之间快速切换,对比它们在相同 HER 数据下的表现。很多开源实现把 HER 写死在算法类里,换算法时就得整段重写,非常痛苦。
具体做法是维护一个独立的her_transformer模块,只负责从完整 episodes 里生成重放数据。策略更新时,只需要从 replay buffer 中取普通样本,同时从her_transformer中取 HER 样本,然后合并训练。如果实验不需要 HER,直接把 transformer 关掉即可。
5.3 如果你只带走一件事
我做强化学习这几年,最大的感受是,很多性能瓶颈根本不是模型容量或优化器的问题,而是训练数据里有效信号太少。HER 从一个非常巧妙的角度帮我们绕过奖励设计的诅咒:它不改变环境,不改变算法,也不增加任何仿真成本,只是在读取经验时换了一种“脑补”方式,把失败变成了成功,把稀疏变成了密集。这件事听起来简单,但把它做对、做好,需要理解目标表示、采样策略、阈值设计、比例调整这些细节。希望这篇文章能让你少走一点弯路,下次遇到稀疏奖励任务时,第一时间想到 hindsight,而不是崩溃地堆算力。