1. 先聊个直觉:为什么"事后聪明"能训练AI
先回想一个场景:你在停车场倒车入库,来回折腾了五分钟,最后还是停歪了。下车一看,其实某个瞬间车子的位置离车位线刚刚好,只是当时你没意识到。这种"如果当初把目标定在那个位置上就好了"的感慨,就是 hindsight——事后聪明。
2017年,OpenAI的 Andrychowicz 等人在论文里把这种直觉搬进了强化学习,提出了 Hindsight Experience Replay,简称HER。这篇论文的出发点是:很多现实任务里,智能体根本拿不到奖励,因为"成功"太难碰上了。机器人推箱子,推一万次都推不到目标点,那它学什么?HER的思路非常简单粗暴:既然这次没推到目标点A,那就把这次的"目标"改写成它实际到达的位置B,把一条失败的轨迹重写成一条成功的轨迹,然后丢进经验池里训练。这样一来,失败的尝试不再只是失败,它变成了"从当前状态到B状态"的宝贵成功样本。
这个思路解决的是强化学习里最让人头疼的问题之一:稀疏奖励(sparse reward)。很多落地场景里,奖励不是稠密函数,而是只有"成功"或"失败"两种结果。没有HER这类手段,训练基本靠彩票中奖式的随机探索,数据效率低到没法看。所以HER一出来,就在机器人控制、连续控制、游戏AI等方向被广泛使用,尤其是与DDPG、TD3、SAC这类off-policy算法搭配,效果非常明显。
这篇文章我不会照着论文给你念公式。后面的内容都会围绕我自己在复现和使用HER过程中积累的经验来展开,包括为什么它有效、目标重标记的几种策略怎么选、网络结构怎么写、以及哪些坑我踩过之后流血总结出来的。适合正在做强化学习落地、尤其是被稀疏奖励折磨的工程师和研究者看。
2. 为什么稀疏奖励能把强化学习直接逼进死胡同
2.1 先看奖励信号在RL里的分量
强化学习的标准流程是:智能体与环境交互,根据状态、动作获得奖励,然后用奖励信号更新策略。数学上,目标函数是最大化期望累计回报。听起来很简单,但奖励的具体形式决定了学习的难度和路径。
稠密奖励相当于每个动作都有"打分":你靠近目标给一份奖励,远离目标扣一分,这样策略梯度几乎每一个时刻都有明确的改进方向。而稀疏奖励是"一锤子买卖":只有当动作序列恰好完成目标才给一个+1,其他时刻全是0。试想一下,一个需要连续执行20步正确的动作组合才能拿到奖励的任务,所有中间步骤的奖励全是0。这时策略梯度的方差大得惊人,几乎没有有效梯度信号,学习基本停滞。
2.2 随机探索在稀疏奖励下的真实处境
我们在训练初期通常依赖随机动作去探索环境。假设每步正确的概率是0.5,那么连续20步全对的概率是0.5的20次方,约等于百万分之一。也就是说,一百万个episode里,可能只有一两个episode能"成功"一次。就算你侥幸成功了,这个成功样本在经验池里也只是沧海一粟,策略网络的更新量淹没在大量的零奖励样本中。
更扎心的是,很多连续控制任务的动作空间是连续的,正确动作组合的概率几乎是0。比如机械臂要到达一个精确的三维坐标点,随机动作下别说100万步了,1000万步都未必能碰到一次目标点。于是训练陷入一个恶性循环:因为没有成功样本,策略学不到东西;因为策略一直随机乱撞,更不可能遇到成功样本。
这里还要提一个我常看到的误区:有些人以为用更强的算法、更深的网络能解决稀疏奖励问题。实际上,只要奖励信号本身是稀疏的,算法层面的改进几乎都是杯水车薪。这就像你要教一个小孩射箭,但只告诉他一件事:射中靶心才算及格,射偏了什么都不说。那他很可能永远在乱射。这时候正确的做法,不是换个更聪明的老师,而是想办法让"射偏"这件事本身产生教学价值。
2.3 HER的切入点:让失败成为老师
HER的逻辑就是,既然随机探索撞不到目标点A,那我们干脆换个教法:每一条轨迹结束后,不去管它原本的目标是什么,而是重新挑一个"这轨迹里实际到达过的状态"作为新目标,并假设"如果我的目标就是这个状态,那这次尝试就是成功的"。然后在这个假设下计算奖励,重新构造一条经验存入回放池。
这个技巧给训练带来的信号密度提升是指数级的。原来100万条轨迹里可能只有1条有效,经过HER改造后,几乎每一条轨迹都能产出至少一条"目标达成"的样本。策略开始能从"失败"的数据里持续学到东西,后续再结合一些探索,慢慢就能学会真正推向目标点。
这就是HER的核心魅力——它不改变你熟悉的RL算法流程,只改变经验池里的"标签"。你把一条轨迹的目标标签改写、奖励重算、重新入库,训练信号立刻变密集。接下来我们进入具体机制,看看这个"改写标签"到底是怎么完成的。
3. HER的关键机制:目标重标记的四种姿势与内部原理
3.1 目标重标记到底改写了什么
先定义一下术语。HER设定在goal-conditioned强化学习框架下:策略不仅接收当前状态 s,还接收一个目标 g,输出动作 a,目标是学会"从任意状态出发,达成目标 g"。训练时每条轨迹由初始状态、动作序列、观测序列组成,最终是否成功由"最终状态 s_T 是否等于/接近目标 g"决定。
传统做法下,这条轨迹如果没达到 g,整条轨迹的奖励全是0。HER的做法是:在这条轨迹结束后,重新选一个"伪目标" g',并且 g' 必须是轨迹里某个时刻实际到达过的状态。然后我们假设"这条轨迹是在尝试完成 g'",重新计算每一步的奖励——因为轨迹确实到达过 g',所以从"达成 g'"的角度看,它就是一条成功轨迹。这条重命名后的轨迹会被存入回放池,供后续训练使用。
理论上可以这样理解:原本的轨迹是从 s0 到 sT 的一段记录,它包含的是"从出发点到实际到达点"的成功经验。虽然它没能证明"从s0到g"可行,但它证明了"从s0到sT"可行。而目标条件策略本来就要学会"给定目标,输出到达目标的动作",所以任何一段实际发生过的转移,只要是朝着某种"可达状态"的,都能作为目标条件经验被训练吸收。
用生活类比来说:你约了朋友去商场A,结果走错了路,误打误撞到了商场B。虽然没达成原计划,但这次出行确实证明了"从家出发是可以到商场B的"。下次如果有人问你B怎么走,你至少可以给出一个可行方案。HER就是把这趟走错的行程记录下来,并贴上"去商场B成功"的标签。
3.2 四种重标记策略:future、final、episode、random
论文给出四种选择伪目标的方式,我直接把推荐度放在前面:
| 策略 | 伪目标选择方式 | 我的推荐度 | 适用场景 |
|---|---|---|---|
| final | 只取轨迹最终状态 s_T | 折中、最简单 | 快速验证,大部分任务够用 |
| future | 从轨迹中当前时刻之后的某个状态里随机选 | 强烈推荐 | 连续控制、机器人任务,几乎是最优解 |
| episode | 从整条轨迹的任意状态里随机选 | 一般 | 任务需要中间状态也有参考价值时用 |
| random | 从所有已探索过的状态里随机选一个 | 不推荐 | 极少使用,仅做对比实验时用 |
为什么 future 效果最好?因为它兼顾了"可达性"和"渐进性"。future策略会在轨迹中当前时刻 t 之后采样一个状态作为伪目标,比如在 t=5 时选 t=17 的状态当目标。这样从 state_t 到达 state_17 之间的距离,对比从起点到终点要短得多。而 final 每次选终止状态,轨迹后期的那些步骤到终止状态的距离是0,容易让策略学到"维持现状"的惰性行为。episode 是从全局任意位置取目标,距离分布不可控,方差大。random 更不用说,很多状态下根本不可达,选来等于在教模型学错误映射。
一个常见疑问是:future 到底选未来哪个时刻的状态?论文里的实现是从当前时刻之后随机均匀选一个,不一定要选很远。实践中我建议在一个较大的轨迹长度范围内随机采样,例如在 t 到 T 之间均匀选,这样既有多样性,又保证目标是"当前状态往后几步内可达"的,学习曲线会比较平滑。
3.3 为什么HER必须搭配off-policy算法
这可能是新手最容易忽略的点。HER的本质是"修改轨迹标签"再入库,这完全是在经验回放层面做文章。而经验回放本身就是off-policy学习的标志性组件——你拿旧策略采样出来的数据,更新当前策略。如果你用的是on-policy算法(比如PPO、REINFORCE),数据必须来自当前策略,修改后的经验不满足分布一致性,强行用会引入严重的偏差。
所以HER几乎总是与DDPG、TD3、SAC这类算法搭配。出发点就是DDPG自家的经验池机制天然支持这种目标域的重构。实际使用中我最常搭配的还是TD3和SAC:TD3的延迟更新能抑制Q值过高估计,SAC的熵正则项让探索更稳定,两者结合HER都能在稀疏奖励任务里稳定收敛。
3.4 为什么"目标分布"是把双刃剑
理论上你可以把伪目标改写成任何状态,但一定要考虑一个关键问题:伪目标的分布必须尽量落在"策略真实可能到达的目标分布"附近。如果伪目标选得太远、完全不可达,那么策略收到的指令就是"完成一项不可能的任务",梯度更新方向是混乱的。
这也是为什么 final 策略在某些任务里不够好——它选的目标就是这条轨迹的终点,虽然可达,但往往"太容易"了,尤其是在轨迹后期,目标状态就在眼前,模型学到的更多是"原地摆烂"而非"主动接近目标"。而 future 策略选择"稍微有点挑战但可达"的目标,是一种更合理的课程学习:难度逐步递增,模型每次都学到略超过当前水平的能力。
在实践中,我还会额外限制伪目标选择的时间间隔。比如要求伪目标必须在当前时刻往后的若干步之内,但不能是最后一步之后。这个限制可以避免模型专门去学"最后时刻冲刺",因为稀疏奖励任务的成功条件往往是持续接近目标,而非最后碰撞那一下。
4. 复现HER的实操笔记:网络结构、超参数与细节取舍
4.1 网络结构就是把goal拼进输入,别小看这一步
HER的目标条件设定中,Actor和Critic的输入都要包含 goal。最常见的做法是将当前状态和 goal 向量直接拼接,然后送入MLP。这个操作很简单,但很多人在这里踩坑:忘了拼接、拼接的维度顺序写错、或者goal没有做归一化就硬塞,都会导致训练崩掉。
我习惯的做法是:
import torch.nn as nn import torch class Critic(nn.Module): def __init__(self, state_dim, goal_dim, action_dim, hidden=256): super().__init__() # 输入维度 = state + goal + action self.fc1 = nn.Linear(state_dim + goal_dim + action_dim, hidden) self.fc2 = nn.Linear(hidden, hidden) self.fc3 = nn.Linear(hidden, 1) def forward(self, state, goal, action): x = torch.cat([state, goal, action], dim=-1) x = torch.relu(self.fc1(x)) x = torch.relu(self.fc2(x)) return self.fc3(x) class Actor(nn.Module): def __init__(self, state_dim, goal_dim, action_dim, hidden=256): super().__init__() # 输入维度 = state + goal self.fc1 = nn.Linear(state_dim + goal_dim, hidden) self.fc2 = nn.Linear(hidden, hidden) self.fc3 = nn.Linear(hidden, action_dim) def forward(self, state, goal): x = torch.cat([state, goal], dim=-1) x = torch.relu(self.fc1(x)) x = torch.relu(self.fc2(x)) return torch.tanh(self.fc3(x))有一个细节值得多说一句:如果状态和goal的量纲差异很大,比如状态是关节角度在[-1,1],goal是三轴坐标在[-100,100],你不做归一化直接拼,MLP很容易被goal维度的高方差信号带偏。我一般会在环境包装层做一次min-max归一化,或者用LayerNorm对拼接后的向量做一次标准化。这个操作的收益往往比调学习率还要明显。
4.2 超参数推荐表:直接抄作业
我自己在多个连续控制任务上用过HER,基于DDPG/TD3的稳定配置大致如下:
| 超参数 | 推荐值 | 备注 |
|---|---|---|
| actor学习率 | 1e-3 | Adam优化器 |
| critic学习率 | 1e-3 | 不需要刻意比actor低 |
| 折扣因子γ | 0.98 | 目标条件任务中常用0.95-0.99 |
| 目标网络软更新系数τ | 5e-3 | 我常用0.005,更新过慢容易回波动 |
| 经验池容量 | 1e6 | Hero任务里生成效率高,容量要够大 |
| 批大小 | 256 | 过小会导致目标重构样本的方差大 |
| 每次环境的随机采样步数 | 8-10 | 用于初始化回放池 |
| 伪目标重标记版本数 | 2 | 每条轨迹生成4个重标记样本比较常见 |
| 探索噪声 | 高斯N(0, 0.1) | 连续动作空间加噪声 |
关于重标记版本数(K),论文里用的是每条轨迹额外生成K个伪目标版本,默认K=4效果普遍不错。但注意K太大内存占用成倍增加,而收益在K>4之后几乎不增长。K=2到K=4之间,我更推荐4,特别是在机器人任务里,多样性帮助明显。
4.3 回放池的结构:目标信息要保存完整
HER需要保存完整轨迹,而不只是单步经验。因此在收集阶段,我会用提前分配好的buffer列表记录每个episode的全部transition,包括:状态、动作、下一状态、目标、奖励、done。轨迹结束后再做重标记,把新的transition重新放入统一的经验池。
一个常见的失误是想节省内存,只存状态和动作不存目标。这是不行的,因为重标记时需要原轨迹里每个时刻的目标来计算新的奖励。建议在环境wrapper层面直接返回 goal 字段,并在采集函数里保存完整字典结构,避免事后找不到数据。
奖励函数的写法也注意一下。HER环境里,典型的目标条件任务是二值化奖励:如果当前状态和目标状态的距离小于某个阈值,奖励为1,否则为0。实际编程中,我建议不要用0/1,而用0表示失败、1表示成功的稀疏信号,同时配合一个距离指标做日志监控。这样你可以随时看训练过程中的平均距离曲线,判断模型是否真的在逼近目标,而不是只看reward曲线那种"永远为零"的绝望画面。
4.4 训练循环中的关键细节
训练循环本身和普通DDPG差不多,但有几个细节会让结果产生数量级的差别:
- 训练前一定要收集一批随机探索数据填充回放池,我一般会随机跑5万到10万步再开始更新,否则一开始经验池里空荡荡,采样出来的batch几乎全是重标记后的假样本,策略容易震荡。
- Critic网络的target网络要延迟更新得比Actor更慢。在TD3里是Actor每2步更新一次,Critic每1步更新一次;在DDPG里则是软更新,我会额外把Actor的更新频率调低,抑制Q值高估。
- 每次从环境中采集一批episode后,不要立刻用这批数据做梯度更新。应该先把它们重标记、推入经验池,再从经验池均匀采样。很多简化版实现为了省事,直接用刚收集的数据来更新,这其实破坏了off-policy的优势。
- 观察一下目标网络更新时的Q值均值,如果发现Q值均值持续上涨但实际成功率不涨,说明出现了严重的过估计,立刻检查是否忘了梯度裁剪、或者goal归一化有没有做。
5. 踩坑记录与常见问题排查:我流过的血你直接拿走
5.1 常见问题速查表
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| 训练很久reward始终为0 | 随机探索步数不够、伪目标距离阈值设置不合理 | 打印平均目标距离曲线,确认是否缓慢下降;提高随机探索时长 |
| 成功率突然飙升后又崩回去 | 回放池更新策略有问题、目标网络更新太快 | 降低软更新τ值;检查是否过早使用了包含大量假目标的数据 |
| Q值持续升高但真实成功率不动 | Q过估计 | 用TD3的延迟更新方式;给critic加LayerNorm;检查goal归一化 |
| 学习死水一潭,策略完全不变 | Actor的输入没有拼接goal或拼接错位 | 打印Actor的输入维度,核对状态+目标维度是否匹配 |
| 最终状态无法确定 | 环境没有正确返回轨迹的最终状态 | 检查环境reset时是否返回了终止状态;在done判定时使用额外返回值 |
| 速度很慢但是对的 | 每条轨迹都生成多个重标记样本,批量太大 | 将K设为2,或者减小回放池单批次大小但增加更新频率 |
5.2 三个我真正踩过的深坑
坑一:把奖励写错,导致HER学了等于没学。
我曾经在一个机械臂环境里,把"成功条件"写成了"当前时刻状态等于goal状态",但由于浮点数误差,状态永远不等于goal,于是成功率永远是0。HER本身不会帮你修正这个bug——它重标记时也用同样的成功判定,所以所有重标记样本也全是0奖励。这时候你回看训练日志,会看到平均目标距离明明在下降,但reward曲线一动不动。排查了很久才发现是成功判定阈值写错了,应该用距离小于某个容差,而不是精确相等。这个教训是:在引入HER之前,先用随机策略或简单环境验证你的 reward 函数本身是合理的。
坑二:网络容量不够,目标信息根本没进到Critic。
早期我试过一个比较复杂的多指机械臂任务,状态维度30,目标维度7,action维度9。当时图省事,用了一个单层128维的Critic,结果无论怎么调超参都学不动。后来把网络加深到256x256,并加入LayerNorm,效果立刻改善。原因其实不复杂:goal-conditioned策略需要同时学习"状态表征"和"目标表征",以及两者之间的相对关系,这比普通RL任务需要更大的函数逼近能力。如果你发现HER在你的任务上完全没效果,首先怀疑是不是网络容量不够,别急着怀疑算法本身。
坑三:随机探索太多,回放池里全是"垃圾"。
我有一段时间为了积累经验,让它在训练初期随机跑了50万步。结果重标记后的样本确实很多,但都是"随机运动到达随机位置"的经验,几乎不包含"朝某个方向持续推进"的有用模式。后来我改成"随机探索+少量使用刚训练几步的模型进行采样"混合策略,进度明显加快。正确做法是:先用随机策略积累一批初始数据启动训练,然后尽早切换到"用当前策略带着探索噪声采样"的模式。HER的强项是改造已有轨迹,如果轨迹本身太过杂乱,它能榨取的信息也很有限。
5.3 我对HER适用边界的真实看法
没用过的人会觉得HER是万能神药,用过的老手反而会更谨慎。HER最擅长的场景是"目标条件明确、动作空间连续、状态可观测性好"的任务。例如桌面机器人推箱子、机械臂抓取、简单的导航任务,这类任务里"目标"天然可以用状态向量表示,重标记逻辑顺理成章。
但HER也有明显不擅长的领域。第一个是纯粹的策略游戏——比如围棋、星际争霸这种目标不是"到达一个状态"的任务,HER无法定义状态目标。第二个是离散动作空间很大的组合优化问题,HER重标记后的目标往往对应不到真实状态。第三个是目标状态不可观测的环境,比如部分可观测环境里,智能体根本看不到完整状态,重标记的"目标"指向的是一个不完整的信息,效果会打折扣。
所以我的建议是:想清楚任务是否属于"goal-conditioned且状态可观测"这个框架。如果答案是肯定的,HER几乎没有理由不用;如果不属于,就别硬套,接下来我会简单聊聊HER的后续演进方向。
6. 后续演进与我的扩展尝试方向
HER诞生的这七八年里,后续有不少工作围绕它展开。Nachum等人提出的ECR(Experience Replay with Relabeling)在偏离策略更远的数据分布上做了更多探索;也有人把HER和"课程学习"结合,先让模型在容易的伪目标上学习,再逐步过渡到真实目标。
我自己尝试过的扩展是把HER与自监督目标生成结合在一起。具体做法是:先用一个VAE对探索到的状态分布建模,从中采样合理目标,代替人工指定goal分布。这样可以在完全不设置手工目标的情况下,让模型先在"可到达状态"上学到基础能力。这对一些目标空间定义模糊的任务很有效,但也不是银弹,VAE的训练稳定性本身又会引入新的麻烦。
另一个我推荐实践的方向,是把HER作为"数据增强层"放在SAC框架中,而不是仅在DDPG或TD3里使用。SAC的熵调节机制配合HER的伪目标重标记,能够在较早训练阶段就保持一定探索性,避免HER环境中常见的“权值坍塌到少量策略模式”问题。不过在SAC里用HER时要格外注意:重标记样本的奖励可能是假的,而SAC的熵项会对所有样本统一优化,假奖励样本的比例过大会影响策略熵的调节。我的经验是,在SAC中把HER重标记样本的占比控制在30%以下,其余用真实目标样本。
7. 我个人的实操体会与最值得记住的一件事
如果只让我保留一条关于HER的执行心得,我会选这句话:先确认奖励逻辑正确,再谈算法改进。
HER是一个非常优雅且实现成本极低的trick,它把人类"事后聪明"式的学习方式引入了机器。相比设计复杂奖励塑形、大规模调参,HER往往能用最小的代码改动解决稀疏奖励带来的核心困难。我见过太多人一上来就堆网络、调学习率、换算法,结果忽略了HER本身,或者忽略了实现里的最基本信息流问题——目标有没有正确传给网络、奖励逻辑在重标记后是否正确、归一化有没有做好。
在你动手复现的时候,我建议按照这个顺序来:先在一个最简单的goal-conditioned环境中把HER的完整流程跑通,再去挑战复杂的机械臂任务。从一开始就逼自己把轨迹存储、重标记、奖励重算、经验池采样这四块的代码逻辑理清。踩过几次坑之后你就会发现,HER不是"一个很难实现的算法",而是一种需要你认真对待、但实现了就非常稳定的数据改造方式。
最后分享一个小技巧:在训练日志里永远同时打印"真实目标成功率"和"重标记目标成功率"。这两个指标是诊断一切问题的起点。如果真实成功率在涨,说明你在做正确的事;如果只有重标记成功率在涨而真实成功率不动,那说明策略可能只是学会了在伪目标上做文章,还没真正泛化到真实目标——这时就该检查目标分布的重叠程度,或者提高重标记样本中真实目标样本的比例了。
祝各位在稀疏奖励的世界里,少一点随机碰运气,多一点后见之明。