hindsight,英文词典里的意思是“后见之明”,通俗点说就是“事后诸葛亮”。做强化学习的人听到这个词,脑子里多半会冒出另一件事——2018年OpenAI那篇《Hindsight Experience Replay》,中文通常译作“事后经验回放”。这篇文章做的事情很朴素:当机器人没能完成任务目标时,别把这段轨迹丢掉,而是把“没完成的目标”替换成一个“它实际到达的状态”,假装刚才的任务就是去那个地方,于是失败的尝试也被当成成功样本来学习了。就这么一手“重新判定目标”的操作,把长期卡住强化学习的稀疏奖励问题撬开了一个口子。
这篇文章不打算复述论文推导,只想把这套思路掰开揉碎讲清楚,再附上一些复现和调参过程中的经验总结。如果你是刚接触目标条件强化学习,或者已经在跑DDPG、SAC这类算法但发现奖励曲线一直趴着不动,那这篇文章大概率能帮到你。读完你会知道HER到底改了什么、为什么它能起作用、以及真正动手时最容易被忽略的细节在哪里。
1. 为什么强化学习会卡在“稀疏奖励”上
1.1 稀疏奖励到底难在哪里
稀疏奖励问题一直是强化学习里的大坑。很多真实任务天然就是稀疏的:机器人把方块推到目标位置、机械臂抓起物体放到托盘里、小车成功停进车位,这些任务只有在动作序列足够接近正确答案时环境才会给出奖励。比如一个二值化的奖励函数:目标没达成给0,达成了给1。在训练刚开始,策略完全随机,机器人几乎不可能碰巧完成任务,于是整条轨迹的奖励全是0。没有梯度、没有信号、没有反馈,模型只能原地乱撞。
这种场景下常遇到的现象是:训练跑了几十万步,reward曲线纹丝不动,loss倒是很漂亮,因为actor网络的loss一直在0附近徘徊,critic也觉得“没什么好学的”。问题在于,agent根本不知道“差一点”和“差很远”哪个更好,所有失败样本给它带来的信息量都等于零。有人做过粗略统计,在Fetch系列的机械臂任务上,不加任何技巧只用DDPG去训,很多时候跑了上百万步成功率仍然是0。
稀疏奖励难的本质有两点。第一是探索问题:状态空间那么大,动作又是高维连续的,纯靠随机探索撞到目标状态的概率低到可以忽略。第二是信用分配问题:就算偶尔接近了目标,也很难判断到底是哪一步动作起了作用。这两个问题叠加在一起,让常规的off-policy算法直接失去学习信号。
1.2 “事后诸葛亮”为什么会有效
既然任务目标常常做不到,那就换一个说法——把你实际到达的状态当作目标,重新定义任务。这不是自欺欺人,而是给学习过程提供了大量“软反馈”:虽然没把方块推到指定位置,但你把它推到了另一个位置,如果你把“推到那个位置”当目标,那这次操作的每一步就全都是标准示范了。
我拿投篮打比方。一个新手练投篮,目标是空心入筐,投了100个全都不进,每个球都是一次完全失败。但如果把“打中篮板左上角这个点”当作目标,那么他可能已经成功了很多次。把这些“成功打到别的点”的经验收集起来,训练一个模型“如何把球投到任意指定位置”,最后再让它追求空心入筐,学习和积累速度就完全不一样了。HER做的正是这件事:每个任务回合结束,不只保留原始目标对应的经验,还把轨迹拆成若干条“替代目标的成功经验”,一起进学习池。
这个方法之所以成立,是因为机器人的控制策略是“目标条件化”的,也就是策略以(s, g)作为输入,输出动作,你的任务是学出一个能完成任意给定目标的控制规律。既然目标本来就是一个输入参数,那么拿实际到达状态去替换目标参数,逻辑上完全自洽。换句话说,失败轨迹里不是没有成功信息,只是我们要把成功从“撞大运”重新定义成“按实际结果倒推”。
2. HER核心原理拆解:把失败轨迹变成成功教材
2.1 目标重标记到底改了什么
先说清楚标准的目标条件MDP。每个回合开始,环境会给你一个原始目标g,每一步你基于状态s和g选择动作a,环境返回下一步状态s'和奖励r,常见稀疏奖励写成:
r_t(s_t, a_t, g) = -[ ||s_{t+1} - g|| > δ ]
距离小于阈值δ时奖励为0(或者1),大于阈值时奖励为负(或者0)。这样所有轨迹要么全0要么有极少数非零,训练效率很低。
HER的关键改动发生在数据采集后、存入回放缓冲区前。假设这一个回合你从状态s0开始,目标g没达成,但agent实际走出来的整条状态序列是s0, s1, ..., s_T,最终停在s_T。HER会这样处理:把这个回合当作多条经验来存。第一条按原始目标g存,奖励照旧,大概率全是0;第二条开始,把目标换成轨迹中某个实际到达的状态g',比如用末态s_T,然后重新计算整条轨迹的奖励。由于“目标”就是真实到达过的位置,这条轨迹的最后一步一定满足距离阈值,奖励序列里会出现有价值的非零项。这种操作在论文里叫“goal relabeling”,中文常译作“目标重标记”。
重标记之后的经验长什么样?原来是一条无助的无反馈轨迹,现在变成了一条“展示如何到达另一个目标”的成功示范。对critic来说,它学到了“某个动作序列加上某个目标状态能导致什么结果”;对actor来说,它多了大量“从什么状态出发能接近什么目标”的监督信号。HER本身完全不改动策略梯度公式、不改变值函数结构,只是在数据存进缓冲区之前动了手脚,这也是它能无缝嵌入各种off-policy算法的主要原因。
2.2 四种重标记策略怎么选
不过“拿哪个状态当作替代目标”是有讲究的。论文里给出了四种策略:final、future、episode、random。具体含义我用表格整理了一下。
| 策略 | 新目标来源 | 特点 | 适合场景 |
|---|---|---|---|
| final | 轨迹最终状态s_T | 简单直接,一条轨迹只额外生成一条经验 | 任务结果集中在终点、轨迹较短 |
| future | 轨迹中当前时刻之后的某个状态 | 最常用,能产生多条有因果一致性的经验 | 一般操作类任务,推荐优先尝试 |
| episode | 当前回合轨迹中的任意状态 | 目标多样性更强 | 目标空间覆盖广、需要丰富探索 |
| random | 全局缓冲区中任意状态 | 与当前轨迹基本无关,偶尔能增加覆盖度 | 特殊实验需求,日常训练不建议 |
实际工程里最常用的还是future策略。原因在于它保持了时间上的因果一致性:假设你在第t步发生了一段动作,那么用它之后某时刻的状态作为目标,至少这段动作确实把系统从s_t带到了更接近s_t'的位置,奖励信号是有物理意义的。final策略虽然简单,但一条轨迹就产生一条额外数据,数据利用率偏低;episode策略会引入不少“目标在状态序列起点”的样本,早期这些样本几乎和新目标没啥关联,容易干扰学习。论文里的实验也显示,future和episode效果通常优于final,random则很不稳定。
实现future策略时还有个参数K,代表一个回合额外生成几条重标轨迹。随机选未来状态时,常用均匀分布从[t, T]中采样,K默认取4。后文我会专门谈K这个参数多容易翻车,这里先记住:K不是一个越大越好的量。
2.3 一段伪代码看清HER全流程
理论说了半天都不如一段伪代码直观。假设我们已经拿到了某个回合的完整轨迹,状态序列obs、动作序列acts、实际到达状态序列achieved、原始目标goal,接下来要做的事就三步:按原目标存一次、按重标目标存K次、把新经验丢进回放缓冲区。
def her_store(obs, acts, achieved, goal, buffer, k=4): # 先存原始目标对应的轨迹 for t in range(len(acts)): reward = compute_reward(obs[t + 1], goal) buffer.add(obs[t], acts[t], reward, obs[t + 1], goal, done=(t == len(acts) - 1)) # 重标记目标:这里是future策略 for _ in range(k): alternate_goal = achieved[-1] # 也可以从[t, T]中随机采样 for t in range(len(acts)): reward = compute_reward(obs[t + 1], alternate_goal) buffer.add(obs[t], acts[t], reward, obs[t + 1], alternate_goal, done=(t == len(acts) - 1))上面这个写法里,每一条experience都包含五元组(s, a, r, s', g)。注意重标后的目标在整个轨迹里是同一个,只是和原始目标不同。compute_reward函数则是根据s'和目标g之间的距离判断奖励。所有轨迹存进同一个回放缓冲区,训练时DDPG或者SAC从缓冲区里均匀采样,学习过程自然就能利用到这些“假目标”产生的真实奖励。
如果你看过OpenAI的官方实现,会发现他们还做了一些细节处理,比如:重标目标一般在整条轨迹内保持一致而不逐时间步变化;额外生成的轨迹数量受K控制;训练时batch里原始目标和重标目标的比例也影响巨大。这些都是在工程上踩过坑之后才意识到的重要细节,放到后面“调参”章节细说。
3. 实操落地:从Fetch任务开始复现HER
3.1 环境准备与安装
要快速验证HER效果,首选还是机械臂目标达成类环境。新项目优先用gymnasium生态,安装命令很简单:
pip install gymnasium-roboticsFetch系列环境需要MuJoCo引擎,现在MuJoCo已经开源并且可以直接通过pip安装,比早期折腾mujoco-py要省心多了。装好之后创建环境试试:
import gymnasium as gym import gymnasium_robotics gym.register_envs(gymnasium_robotics) env = gym.make("FetchPush-v2", render_mode="human") obs, info = env.reset() print(obs.keys()) # observation, achieved_goal, desired_goal值得留意的点是,这类环境的观测量是一个字典,包含三块:机器人自身关节信息、实际到达的目标状态、期望目标状态。HER之所以方便,正是因为环境直接给了achieved_goal,你不用自己从高位像素里猜测机器人到底碰到了什么。这也是为什么很多论文实验喜欢拿Fetch系列做验证,它把最难的“目标提取”问题给简化了。
如果你的任务是自己的仿真器,没有现成的achieved_goal,那需要在环境接口里单独实现一个提取函数,把“当前已经做到了什么”显式算出来。我见过不少项目在这一点上栽跟头,环境里没有目标提取模块,硬靠网络端到端预测,最终HER效果大打折扣。
3.2 训练超参数与经验值
复现HER时,算法主体用DDPG或SAC都能跑。很多开源项目默认使用DDPG,原因不是它最好,而是原版论文用的就是它,超参相对成熟。我用下来的推荐配置是这样的:
| 参数 | 参考值 | 说明 |
|---|---|---|
| actor/critic隐藏层 | [256, 256] | 两层MLP,ReLU激活 |
| 学习率 | 1e-3 | Adam优化器 |
| γ | 0.98 | 任务周期短,折扣可以稍小 |
| K | 4 | HER额外生成轨迹条数 |
| 回放缓冲区大小 | 1,000,000 | 用numpy数组存储,效率更高 |
| batch size | 128 | 训练batch |
| 探索噪声 | 高斯噪声σ=0.2 | 连续控制常用高斯噪声 |
| 训练步数 | 至少1,000,000 | Fetch类任务百万步是常态 |
训练流程并不复杂:每个回合在环境中采集50步左右的数据,回合结束后用HER生成额外经验存入缓冲区,然后每隔一定步数执行一次策略更新。在你自己的实现里,最关键是要确保HER生成的“成功经验”确实进到了采样池,而不是只存在于日志里。怎么确认呢?最简单的方法是训练时打印缓冲区中非零奖励样本的比例,HER加入后这个比例应该明显上升。
我自己在调的时候还习惯固定随机种子,并且在训练早期打印“原始目标成功率”和“重标目标成功率”两个指标。只统计前者是为了避免被HER制造的“假成功”迷惑。如果原始目标成功率始终为0,但buffer里正样本比例很高,说明策略在“替代目标”上确实学到了东西,这是正常的过渡阶段;但如果几百万步后原始目标成功率还是0,那就要怀疑K值或者目标采样策略是不是有问题。
3.3 训练曲线到底该怎么看
不加HER的DDPG在Fetch这类任务上,成功率曲线基本贴着0走,偶尔蹦到个位数然后掉回去。加上HER之后,曲线并不会像线性增长那么漂亮,常见形态是先平淡一段时间,然后突然翘头,再进入平台期,接着再爬一截。我最早看到这种阶梯式曲线还以为是代码有bug,后来明白这其实是目标分布变化导致的现象。
HER的目标不是固定的,它会随着agent能力变化不断重标新目标。早期agent只会做简单动作,它能“成功”的目标都很容易;随着策略改进,它能到达的reachable状态范围扩大,HER开始自动提供稍难一点的目标,训练难度也随之上升。这种动态难度调节很像自动课程学习,反馈在曲线上就是一段平台、一段猛涨交替出现。
如果你在调试时看到训练reward一直稳定在某个负值不动,千万别急着加随机噪声或者调网络结构。先做一件事:挑几条轨迹,把obs里的achieved_goal和desired_goal拿出来,算一下它们之间的距离。很多时候问题不在算法,而在奖励函数和目标表示上,比如目标空间没归一化、距离阈值设置不合理,这些都是HER救不回来的。
4. 调参与避坑指南
4.1 relabel比例:最容易被忽略的生死线
HER默认K=4,也就是每条真实轨迹额外生成4条重标轨迹,缓冲区里重标数据和原始数据的比例会被抬到4比1。很多初学者觉得“重标目标越多越好”,直接把K调到16甚至更高,结果训练曲线反而崩了。这里面的原因值得细讲一下。
HER数据本质上是对“别的目标”的成功示范。如果缓冲区里这类数据比例太高,critic会被训练得过于乐观,认为随便什么目标都容易达成,actor在真实原始目标上的表现反而变差。打个比方,你让一个学生反复做“今天就复习这一页书就能考好”的简单测试,他当然次次满分,但真正的期末考试题目分布完全变了,他自然就抓瞎了。评估时我们只关心原始目标成功率,不关心重标目标成功率,所以训练数据分布必须保留足够的“真实任务样本”。
实际操作中,K取2到4通常比较稳。OpenAI论文用4,很多复现项目也沿用4,效果不错。但如果你发现训练后期评估成功率明显低于训练奖励所暗示的水平,第一反应应该是降低K,而不是去调网络结构。某些任务上K=1反而更好,因为任务本身的目标空间比较窄,重标样本边际收益递减。
4.2 别把HER用在不该用的地方
HER不是魔法,它有明确的应用前提:任务必须是目标条件化的,并且目标能用状态显式表达。如果你的任务压根不是目标条件MDP,比如只有一个固定的reward函数,没有goal输入,那HER就没有用武之地,硬搬只会白增开销。
常见误用主要集中在两类。一是目标不是状态,而是“完成一个无形动作”,比如“把杯子拿稳别洒水”,这类任务的目标很难从状态里提取,强行用末态替代目标会丢失语义信息。二是奖励依赖于历史序列,而不是当前状态和目标之间距离,比如“必须按顺序按亮三个按钮才算成功”,这时重标目标也无法挽回序列依赖的复杂度。遇到这些情况,正确做法是回到奖励塑形、课程学习或者设计更好的状态表示,而不是死磕HER。
另外要注意HER对on-policy算法基本不适用。理由很简单:重标样本来自旧策略的历史数据,on-policy算法本身不允许用旧策略采样数据做更新,强行用就变成了某种形式的off-policy使用,很容易引入偏差。实践中HER几乎总是和DDPG、TD3、SAC这类off-policy算法搭配,原因就在这。
4.3 目标没归一化会引起连锁问题
这一点值得单独说。Fetch环境的obs空间里关节角度、物体位置、目标位置往往量纲不同,有的数值范围是几弧度,有的是几十厘米。如果你直接用原始数值去算欧氏距离作为奖励,距离的绝对值跨度很大,而网络输入又没做归一化,训练会非常不稳定。
我见过不少项目的坑是:重标逻辑写对了,奖励阈值也用对了,但没对目标向量做标准化,导致critic在不同量纲目标之间反复横跳。常规做法是先对obs和目标做min-max归一化或者标准化,再传入网络和奖励函数。尤其在使用HER时,目标会被频繁替换成各种位置,如果这些位置的尺度不统一,等于每时每刻给网络换了个度量衡。
5. 常见问题与排查实录
5.1 现象、原因、对策速查表
这部分内容是我把实际训练里最典型的几个故障汇总出来的,按照“先看现象、再查原因、最后动手”的顺序整理成表。
| 现象 | 可能原因 | 排查与对策 |
|---|---|---|
| 奖励曲线全程不动,成功率一直为0 | 奖励函数和目标表示不一致;HER数据没进buffer | 打印obs里的achieved_goal与desired_goal,人工算距离;确认her_store确实被调用 |
| 训练指标好看,评估原始目标成功率却很低 | K过大,真实目标样本占比太少 | 降低K到2或1,提升原始目标样本比例 |
| 加入HER后训练反而比原来更不稳 | 重标目标选择太激进(比如用random策略) | 改用future策略,并确认采样范围在[t, T]内 |
| 某个时刻开始奖励突然崩掉 | 探索噪声过大或学习率过高,策略陷入不良区域 | 降低高斯噪声标准差,或把学习率降到5e-4附近,加载之前checkpoint继续调 |
| buffer占用暴涨,训练速度明显变慢 | K设太大导致额外经验过多 | 按实际需要调回K≤4,或用更大buffer容量 |
| 同一个任务的多次运行结果波动极大 | 随机种子不同、目标重标采样随机性大 | 固定环境seed、policy网络seed和numpy seed,增加训练步数取平均曲线 |
这个表是“起点”而不是“终点”,很多问题需要组合排查。比如训练曲线不涨,先看奖励定义,再看buffer数据分布,最后才怀疑网络结构。顺序很重要,因为HER带来的问题九成出现在数据流上,而不是网络结构上。
5.2 几个值得记住的细节坑
关于done标志,这是个隐藏的雷。保存HER生成的替代目标轨迹时,done标志很容易写错。如果你的dqn或ddpg在done之后进行bootstrapping截断,而重标目标轨迹的done和原始轨迹不一致,会导致critic的target计算错乱。最稳妥的做法是:当且仅当新目标在最后一步达成时,done设为True,否则一律False。因为重标目标不一定等于末态,如果中途状态被选为目标,那轨迹还没结束,显然不该设为done。
还有一个训练策略上的经验:HER和随机探索噪声配合时,避免采用过大的动作噪声。有些人为了让agent多探索,把高斯噪声标准差调到0.5,结果策略被噪声淹没,重标目标乱跳,critic学到的映射关系全是抖动的。常用做法是把标准差初始设在0.2附近,训练后期线性衰减到0.1以下。
最后想说一说日志记录。HER唯一的坏处是mask了真实目标信息,让你以为策略学会了,实际上可能只是在重标目标上自嗨。所以一定要在训练循环中额外记录“原始目标成功率”这一个指标。这个指标只用原始目标去跑评估rollout,不掺任何重标数据,是判断训练是否真正有效的唯一金标准。
6. 延伸:从HER到更聪明的“事后复盘”
HER刚出来的时候,大家更关注它“免费”把失败样本变成训练信号的巧劲。后来有不少工作在它基础上继续推进,思路都挺值得了解的。比如Hindsight Goal Generation(HGG),这个想法的核心是用一个生成模型来自动选出更有训练价值的重标目标,而不是从轨迹里均匀采样。它的动机也很直白:未来策略生成的目标虽然与当前状态有关,但不一定具备递进难度,如果能让重标目标集中在agent“踮踮脚就能碰到”的区域,学习效率会更高。
另一条线是HER与表征学习的结合。原始HER用了很干净的状态表示,碰到图像输入、高维观测时,目标重标就得建立在良好的表征上。后来一些方法尝试把图像映射到语义空间里再执行重标,相当于把“实际到达状态”翻译成一个agent能理解的目标向量。这类方向虽然名词多,但底子还是HER那套“以实际结果为锚点重新定义目标”的思想。
回到工程应用上,HER在机械臂操作、移动机器人导航、游戏AI等领域都有落地。OpenAI后来在机器人灵巧手任务和更多仿真环境中把HER当作基础组件来使用,很多商业项目里解决稀疏奖励问题的第一反应也是“先上HER试试”。相比其他复杂的课程学习或者层次化强化学习方案,HER胜在简单、免费、不改算法数学,像一个插线即用的数据增强插件。
不过也要认清它的边界。HER解决的是“奖励太稀疏导致学不到信号”的问题,它没法凭空创造任务结构里不存在的因果信息。如果任务目标是长期序列约束,如果目标状态本身难以定义,如果奖励函数中含着复杂的逻辑判断,HER能做的很有限。在这些场景里,它只能作为整个系统里的一层预处理,还得靠奖励塑形、模仿学习等方法一起兜底。
最后分享一点个人实操中的心得。我第一次在FetchPush上复现HER时,最大的教训不是原理没看懂,而是没意识到“数据分布被HER改变”这件事的分量。一条真实轨迹在重标后会产生4条额外数据,长期训练下来,更新网络用的样本绝大多数来自替代目标。这既带来了学习效率的提升,也悄悄改变了critic对真实任务难度的认知。后来我养成了两个习惯:始终监控原始目标成功率、始终保留一部分不掺HER的真实轨迹样本。这两个小习惯救了我好多次,也让HER从一个“看起来加的代码”变成了真正可控的训练利器。如果你也准备在自己的任务里用HER,不妨从一开始就把这两个习惯带上。