1. 这个项目到底解决了什么问题:稀疏奖励下的“从失败中学习”
1.1 为什么机器人任务经常卡在“没奖励”这一步
接触过强化学习(RL)控制类任务的朋友一定对这样的场景不陌生:你给机器人一个目标,比如“把红色积木推到桌面上标记的位置 A”,然后让它自由探索。跑了十几个 episode,没有任何一次碰到目标,奖励一直是最低的负值,策略网络梯度更新了也像没更新一样,训练曲线平得像一条直线。最后你只能反复怀疑:是环境写错了?是网络坏了?还是 reward shaping 没做好?
这个问题在学术界有个专门的名字:稀疏奖励问题(sparse reward problem)。它的本质是,在大多数目标条件任务里,只有一个最终状态才是“成功”的,只有到达成功状态才给正奖励,其他所有时间步都是惩罚或零奖励。于是,随机初始化的策略几乎不可能碰巧完成目标,整个训练过程收不到任何“有用信号”。从期望回报的视角看,每条轨迹的回报都差不多低,价值函数梯度被一堆无效样本淹没,策略自然学不到任何东西。
更扎心的是,人类并不是这样学习的。一个小孩想够到高处的玩具,够不到会换个姿势凳再够,如果最终碰到了玩具,哪怕不是按最初设想的姿势,他也知道“这样做能碰到”。他会从偏离目标的尝试里学到经验,而不是把整次尝试定义为纯粹的失败。但传统的 RL 算法会怎么做?它会无情地把这条轨迹标注为“负样本”,直接扔进垃圾堆。这种“一刀切”的评价方式,恰恰是稀疏奖励任务难以训练的根源。
我记得自己第一次在 FetchReach 环境里跑 DDPG,看它最后学会伸手够目标点的过程,一个念头老是挥之不去:明明每一次尝试都在接近目标,目标点的位置信息其实已经隐藏在轨迹里了,只是算法不知道“把没够到的位置当成一个新目标”去学。如果能把这种“事后聪明”注入训练过程,是不是就能让机器人从失败里吸取营养了?这正是 hindsight 这个项目的核心。
1.2 hindsight的核心思想:把失败结果改写为“新的目标”
Hindsight(严格说是 Hindsight Experience Replay,事后经验回放,简称 HER)的核心思想,一句话概括就是:如果没达到预期目标,就用实际达到的状态作为新目标,把当前轨迹重新定义成一次成功经验。
举个例子。目标是把积木推到坐标 (1, 1),但机器人实际推到了 (1.3, 0.9)。传统做法:这条轨迹的目标是 (1, 1),没达成,奖励全负,丢弃或低权重使用。HER 的做法:保留这条轨迹的“状态-动作”转移序列,把目标改成 (1.3, 0.9),重新计算每个时间步的奖励。现在回头看,每一步动作都是“为了推到 (1.3, 0.9)”而设的,最终也确实到达了 (1.3, 0.9),所以这条轨迹立刻摇身一变,成为一条完美达成任务的成功样本。
这个“改标签”的操作乍看有点自欺欺人,但在数学上非常自洽:我们训练的本来就是一个以“目标条件”为输入的策略,即 π(a | s, g) 和 Q(s, a, g)。它并不关心目标 g 是用户指定的还是事后构造的,只要 (s, a, g') 存在合理的因果关系,这条样本就能参与训练。把实际达成状态当作目标来学,相当于系统在说:我虽然没做你让我做的事,但我做了另一件事,而且我做得很好,这也可以作为经验沉淀下来。
生活里也有完全对应的例子。你照着糖醋排骨的菜谱做菜,结果糖放多了,变成“红烧排骨风味”。如果只按“糖醋排骨”的标准打分,你这道菜是零分;但换个目标“做一道合格的排骨菜”,你其实学会了腌、炸、收汁的全流程。HER 做的就是这种目标重写,让每一次“失败”都变成阶梯,而不是墓碑。
从这个角度看,HER 的底层哲学特别像那句老话——“失败是成功之母”。只是以前这句话是鸡汤,HER 把它变成了一个可计算的算法。
2. 核心原理拆解:HER是怎么“骗”自己进步的
2.1 从公式到直觉:目标条件策略与事后目标
要把 HER 的原理讲透,先得说清楚目标条件 RL 的设定。在普通 RL 里,状态就是 s,动作就是 a,环境给一个标量奖励 r。而在目标条件 RL 中,每一轮还要额外输入一个目标 g,环境是否给正奖励取决于当前状态是否“达成”目标 g。典型做法是引入一个“已达成目标状态” ag,比如机械臂末端当前的位置、或者积木当前的位置坐标,然后环境视 ag 与 g 的距离是否小于阈值来给奖励。
于是,策略是 π(s, g),Q 函数是 Q(s, a, g),它们都是把目标作为额外输入的网络。HER 在这个框架下做的事情非常简洁优雅:在一段 episode 执行完之后,针对每一个时间步 t,额外采样若干“事后目标” g',将原来的样本 (s_t, a_t, r_t, s_{t+1}, g) 重写成 (s_t, a_t, r't, s{t+1}, g'),其中 g' 取自同一个 episode 中后续某个时刻真正达到的状态 ag_{t'},奖励 r'_t 则按照新的目标 g' 重新计算。这样一来,Q 网络的更新目标就变成了:
- 原样本:Q(s_t, a_t, g) ≈ r_t + γ max_a Q(s_{t+1}, a, g)
- HER 重写样本:Q(s_t, a_t, g') ≈ r't + γ max_a Q(s{t+1}, a, g')
注意,重写后的 g' 不仅仅出现在当前 (s_t, a_t) 里,也出现在后继状态 s_{t+1} 的 Q 函数中,因为同一段轨迹是朝着同一个“事后目标”走的,因果链不能断。
如果只看最终效果,HER 等价于在原始经验回放的基础上,给每条轨迹额外“复制”了几份修改过目标标签的样本。这些样本理论上都是真实可执行的轨迹,只是目标定义不同。策略网络在这些样本上学习时,会逐步发现:原来“实际能到达的状态”本身就是一种可以完成的目标。这种目标空间的自我延展,正是它能从稀疏奖励中爬出来的关键。
为了理解这一点,不妨再想一下为什么标准 RL 学不动。在目标条件设定下,如果目标 g 永远来自外部指定,而在前几百万步里没有一个外部指定的目标被达成,那么所有样本的 TD 误差里 r 项全是负的或零,价值函数处处一碗水端平,没有任何“高价值区域”可以引导策略收敛。HER 相当于人为制造了一片“高地”——目标 g' 对应的就是实际轨迹终点,那里奖励为成功值。这样价值函数就有了凹坑和山峰,梯度有了方向,策略也就能顺势爬坡了。
2.2 目标采样策略的三种方式:final、future、episode
HER 虽好,但“事后目标”具体怎么采,大有讲究。OpenAI 那篇经典论文里总结了三种采样策略,我挨个在实验里试过,它们的差异比想象中大得多。
final(最终状态目标):把一条 episode 最后真正到达的状态 ag_T 当作事后目标。实现最简单,也最符合直觉。“你做成了什么,就把什么当目标”。问题在于,它只提供一个目标,且往往离初始状态比较远,早期策略根本学不会那么难的目标,样本利用率还是偏低。
future(未来状态目标):对时间步 t,从它之后的片段 [t+1, t+K] 里随机选一个状态作为事后目标,K 通常设为一个比例,比如轨迹长度的 10%~50%。这是我最推荐的方式:每个时间步都能产生目标,并且目标天然存在于真实轨迹上、和当前状态有着合理的“因果距离”。训练早期,K 范围内的大多是中间状态,离当前位置不太远,学起来难度适中;训练后期,随着轨迹变长,目标的分布自然跟随策略能力演化。
episode(整段轨迹随机目标):从整条轨迹里随机选一个状态当目标。理论上可行,但实际训练里目标跨度忽远忽近,不稳定,我个人只在做简单 Grid World 验证时用过。
从信息论角度看,future 比 final 好的原因是它保留了轨迹上的“中间里程碑”。你想让一个刚出生的智能体直接学会“把积木推到最远的角落”,不现实;但从当前手边开始,一步步学“推到离手近的位置”,很快就学会。Future 采样天然把困难目标分解成多个相邻目标,无形中形成了一条课程学习的路径。
另外还要注意一个细节:HER 的经验里,理论上可实现性和因果一致性必须保证。也就是说,事后目标 g' 必须是在同一段轨迹上确实可达的状态,而不是从别的轨迹搬过来的状态。否则你等于在教智能体“从 s 出发要到达一个根本去不了的 g'”,那是在制造幻觉,模型学不到真实动力学。
2.3 为什么效果能收敛:等价于“自动课程学习”
很多人第一次接触 HER 时会有个疑问:用一堆“自己重标的目标”训练出来的策略,真的能完成“用户指定的目标”吗?这不会跑偏吗?
答案是不会,原因归结为两点。第一,经验回放里始终保留了一部分用原始目标 g 的样本(后面会讲到比例问题),这部分样本保证策略不会放弃追踪用户目标。第二,HER 产生的数据并不是凭空捏造,它有现实的动力学支撑,学习出来的策略是在“能做到的事”的分布上逐步扩展的。
细品一下你会发现,HER 在学习过程中自动形成了一个课程表:初期目标是轨迹上距离近的状态,中期目标逐步变远,后期目标覆盖整个可达空间。这比人工设计课程学习巨大的优势在于——它是完全由数据驱动的,不需要你手工定义任务的难度阶梯,也不需要任何先验知识。每个“事后目标”都是从真实经验中长出来的,所以始终可达、始终符合动力学。
这种“自动课程”效果,我在 FetchReach 里感受非常明显。一开始机械臂只会做微小的关节运动,事后目标基本都是“原地挪几毫米”;跑了三五个 epoch 之后,轨迹变长了,事后目标变成“伸手够到离起点 20 厘米的位置”;再往后,机械臂的动作越来越流畅,目标点也铺满整个工作空间。整个训练像有人在旁边偷偷布置作业,从易到难,循序渐进。
这里还要顺带澄清一个误区:HER 是通用技巧,不是只适用于 DDPG。理论上它可以在任何 off-policy 算法上叠加,只要你的经验回放里存了状态、动作、奖励、后继状态和目标。我后来还在 SAC 和 TD3 上接 HER 跑过,效果都不错,只是收敛曲线形态会有差异。on-policy 算法也能改造成 HER,但实现起来麻烦一些,因为样本往往用一次就扔,重标效率不高。
3. 实操过程:在小环境里跑通HER
3.1 环境选型与依赖准备
纸上谈兵没意思,下面讲怎么自己动手跑起来。第一个建议是:不要上来就选高维连续控制的大环境,比如全自由度机械臂抓取。先在简单环境里把 HER 的流程跑顺,再上复杂度。
我推荐的环境组合是 Gymnasium Robotics(原名 Gym Robotics)里的 FetchReach。这个任务让一只七自由度机械臂把末端移动到随机目标点,动作空间是 4 维连续控制(位置增量+夹爪开合,虽然抓取场景里夹爪没太大用处),观测空间包含机械臂关节角度、线速度、末端坐标、相对目标位置等。它是最简单的 HER 入门环境:维度适中、目标空间只有 3 维、奖励天然稀疏,非常适合观察 HER 的效果。
依赖方面就需要这么几样:Python 3.8+、PyTorch、gymnasium、gymnasium-robotics。安装的版本坑比较多,尤其是 mujoco 那套依赖,我建议用 conda 新建环境,按官方推荐顺序装,避免在少依赖的情况下编译 mujoco-py 报一堆错。
如果你想完全掌控流程,也可以自写一个二维网格世界:状态是 (x, y),目标是另一个坐标,动作是上下左右移动,走到目标点给 1 分,否则给 0 分。虽然简单,但能让你看清 HER 重标样本的每一个细节。我第一版 HER 就是在这种微型环境上调通的,效率比直接上 Fetch 高得多。
3.2 网络结构设计与目标向量编码
HER 的网络设计和普通 DDPG 相比,核心差异在于“目标怎么进网络”。最自然的做法是在特征层直接把目标向量拼进观测。假设观测 obs 是 25 维,目标 g 是 3 维坐标,那 actor 和 critic 的输入就是 28 维的拼接向量(有些实现里 critic 会把动作也单独拼一层,但输入维度只是结构细节)。
我常用的 MLP 结构是三层 256 的 ReLU 网络,Q 网络和策略网络各一套,同时各配一个 target 网络做软更新。大一点的任务可以上 512,但 FetchReach 这种难度 256 足够。目标向量一般不需要额外归一化,因为环境返回的坐标通常已经在一个合理范围(比如 0~1 或 -1~1),但如果你的自定义环境没有归一化,一定要手动 scale 到 [-1, 1],否则 MLP 很难学。
目标条件输入越简单越直白越好。不要一开始就搞复杂的注意力机制或者目标编码器。HER 能否成功,关键是重标目标和样本分布,而不是目标编码方式。这一点我踩过坑:早期为了让网络“更聪明地理解目标”,加了额外的 LayerNorm 和投影层,训练反而更慢,去掉之后一切正常。
下面是核心网络搭建的大概样子,我用 PyTorch 写个框架让大家参考:
import torch import torch.nn as nn def make_net(input_dim, output_dim, hidden=256): return nn.Sequential( nn.Linear(input_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, output_dim) ) class Actor(nn.Module): def __init__(self, obs_dim, goal_dim, action_dim): super().__init__() self.net = make_net(obs_dim + goal_dim, action_dim) def forward(self, obs, goal): x = torch.cat([obs, goal], dim=-1) return torch.tanh(self.net(x)) # 动作范围通常在 [-1, 1] class Critic(nn.Module): def __init__(self, obs_dim, goal_dim, action_dim): super().__init__() self.net = make_net(obs_dim + goal_dim + action_dim, 1) def forward(self, obs, goal, action): x = torch.cat([obs, goal, action], dim=-1) return self.net(x)注意,动作最后要套一个 tanh 来限制在动作空间边界内。目标空间的“已达成状态”可以直接从环境返回的 info 字典里取,通常字段名是 achieved_goal,目标字段是 desired_goal。这一步是 HER 数据流的关键。
3.3 完整的HER训练循环:代码与关键步骤
下面给出 HER + DDPG 训练循环的骨架,这段代码我在模拟机器人环境里实际调过,注释标注了哪些地方是 HER 的精华:
import numpy as np from collections import deque import random class HERReplayBuffer: def __init__(self, capacity, future_k=4, her_ratio=0.8): self.buffer = deque(maxlen=capacity) self.future_k = future_k # 每个 transition 额外生成的目标数 self.her_ratio = her_ratio # HER 样本占比 def add_episode(self, episode): # episode 是列表,每个元素是 (obs, action, reward, next_obs, achieved_goal, desired_goal) for t, (obs, action, rew, next_obs, ag, g) in enumerate(episode): self.add(obs, action, rew, next_obs, ag, g) # 原目标样本 if np.random.uniform() < self.her_ratio: for _ in range(self.future_k): future_idx = random.randint(t, len(episode) - 1) new_goal = episode[future_idx][4] # 取 future achieved goal new_rew = compute_reward(ag, new_goal) # 按新目标算奖励 self.add(obs, action, new_rew, next_obs, ag, new_goal) def add(self, obs, action, rew, next_obs, ag, g): self.buffer.append((obs, action, rew, next_obs, ag, g)) def sample(self, batch_size): batch = random.sample(self.buffer, batch_size) obs, act, rew, next_obs, ag, g = map(np.stack, zip(*batch)) return obs, act, rew, next_obs, ag, g # 训练主循环(简化) env = gym.make('FetchReach-v2') replay = HERReplayBuffer(capacity=int(1e6), future_k=4, her_ratio=0.8) actor, critic = build_networks() target_actor = copy.deepcopy(actor) target_critic = copy.deepcopy(critic) for epoch in range(200): # cycles = 50, episodes_per_cycle = 16 是 OpenAI 论文里的常用配置 for _ in range(50): episode = roll_out(env, actor, max_steps=50) # 采样一条轨迹 replay.add_episode(episode) for _ in range(40): # 每 cycle 更新 40 步 obs, act, rew, next_obs, ag, g = replay.sample(batch_size=256) # critic loss 用目标 Q 计算 # actor loss 用 -Q(s,a,g) 计算 update_networks(actor, critic, target_actor, target_critic)这段代码里最容易写错的地方,就是重标目标后的 next_obs 到底怎么处理。尤其要注意:HER 重写的是目标 g,不是状态。next_obs 里如果包含了“该目标的相对位置”之类的录制字段,那也得重新计算。实际操作中,我建议干脆不要在 obs 里拼 relative goal,而是让网络直接吃“绝对坐标目标”,避免重标时牵一发动全身。
训练步数方面,FetchReach 在我的 GPU 上大约 2 万步更新就能看到成功率爬升,4 万步左右基本稳定。这个速度在 RL 任务里算非常快了,要知道未加 HER 的 DDPG 在这个环境里很多教程跑到几十万步成功率还是零。
3.4 关键超参数怎么调:k、比例、网络更新频率
HER 的超参数并不玄学,但几个核心值确实需要知道为什么这么设。
第一个是future_k,也就是每个 transition 额外生成几个事后目标。OpenAI 论文里默认是 4,我实际调到 4~8 都有提升,过大会让回放缓冲区里 HER 样本占比过高,导致策略对“实际达到目标空间”过于敏感,对用户原始目标跟踪能力下降;过小则目标多样性不够,加速不明显。我建议先从 4 起步,看成功率曲线的爬升速度再微调。
第二个是HER 样本占比,也就是 her_ratio。经验上,80% 的样本走 HER 重标,20% 保留原始目标,这个比例不是我拍脑袋定的,而是有讲究的:如果 100% 都重标,策略会完全忽略用户给定的目标 G,退化成一个“我只会做我做过的事”的模仿机器;如果重标比例太低,稀疏奖励问题没解决干净。我在 FetchPush 里试过 her_ratio=1.0 和 0.5,前者偶尔会看到策略明明能完成目标任务但找到目标后不会“停”,后者成功率爬升明显变慢,0.8 是折中里最稳的。
第三个是cycle 结构。OpenAI 的配置是每个 epoch 跑 50 个 cycle,每个 cycle 采 16 条 trajectory,然后做 40 步梯度更新。这种“批量采样-批量更新”的结构有它的好处:一阶段收集的经验被多次使用,放大样本效率。如果你用普通的每步在线更新,HER 的效果也会打折,因为重标样本没有充分的“被学习机会”。我建议不要省掉 cycles 的概念,哪怕是简单任务,也保持“采一批、学一批”的节奏。
学习率方面,actor 和 critic 都建议 1e-3 起步,如果发现 Q 值震荡明显,降到 3e-4。折扣因子 γ 常用 0.98,因为机器人任务往往步数在 50 左右,算不上长 horizon,0.98 已经足够。软更新系数 τ 用 0.05,这是 DDPG 家族的常见配置,比 0.005 激进一些,在 HER 这个场景反而能更快传递目标信息。
4. 常见问题与排查技巧实录
4.1 问题:训练损失在下降,但成功率长期为零
这是 HER 新手最容易撞见的怪现象:Q loss 降得挺好看,cos 距离也在缩小,可环境评估的成功率死活不上涨。我一度以为网络坏了,后来排查才发现问题出在“成功标准”的判定上。
在 FetchReach 里,环境会告诉你一个阈值距离,比如末端位置与目标的距离小于 0.05 才算成功。如果你的 HER 重标样本只按“是否完全一致”来给奖励,而不是按阈值距离,那么大量努力一半的轨迹重标后还是负奖励,导致自我诱导的成功信号不足。正确的做法是:重标后同样用阈值判定,即 |ag - g'| < thresh 给正奖励,否则给负奖励。这听起来只是小细节,实操里能直接影响收敛速度。
另一个可能原因是探索噪声设置过大或过小。DDPG 训练早期通常往动作上加 OU 噪声或 Gaussian 噪声来探索。噪声太大,机械臂到处乱飞,轨迹质量差,即使重标也是乱标;噪声太小,动作太保守,轨迹永远停在起点附近,事后目标全是“起点邻域”,学不到新东西。我的经验是噪声标准差从 0.2 起步,随着训练衰减到 0.05 左右,你能看到轨迹长度和末端覆盖范围同步增长。
另外提醒一句:训练损失下降是个很弱的诊断信号,因为它只代表 TD 误差减小,不代表策略真的发现了好动作;对稀疏奖励环境,价值网络的“自洽”常常是负向自洽——全部 Q 值收敛到同一个低值上。所以看损失不如看一个更直白的指标:每条 episode 里 achieved_goal 与 desired_goal 的平均距离变化曲线。距离曲线只要持续下行,说明策略在动,HER 在生效;距离曲线不动,再调 Q 函数结构也白搭。
4.2 问题:future 采样窗口不合理,目标多样性不足或过度
future 采样里有一个容易被低估的参数:future 窗口的长度(也就是未来多少步内可以选目标)。如果窗口限制在 t+1 到 t+10,而 episode 总长 50,那么大多数事后目标都集中在轨迹的前半段,未命中后段的长程目标;如果窗口取到整条轨迹的尾部,生成的 g' 又过于贴近最终状态,每个 transition 的目标基本重合成同一个点,多样性也会降低。
我自己比较稳的窗口设置是t + 1 到 t + horizon,其中horizon = 0.4 * (T - t),也就是随 t 动态缩小的窗口。这样做的好处是:越靠近轨迹末尾,可选目标越少,但那些目标与当前状态的距离越近,始终维持某种“不远不近”的难度。你也可以从经验里摸规律:打印一下每次重标目标的平均距离,如果距离波动范围太小,说明窗口可能卡在一个很窄的区间,调大窗口试试;如果重标目标经常“可望不可即”,可能是窗口跨度太大,需要收窄。
再补充一个有些反直觉的经验:future 采样时不要完全随机,加一点“目标-结果匹配”约束效果更好。也就是说,在选择 g' 时,优先选择那些与当前 achieved_goal 距离适中的状态,而不是随机任意选择。实现上很简单,随机采 20 个候选未来状态,挑一个距离最接近预设值(比如 0.3 倍轨迹长度的平均距离)的当目标。这个小 trick 我在一个机械臂推箱子任务里让成功率提升了将近 10 个点。
4.3 问题:回放缓冲区的目标分布偏移导致策略震荡
HER 样本大量来自“当前策略自己跑的轨迹”。策略在进步,轨迹分布就在变,重标目标的分布也跟着变。这个特性既是 HER 的优势(顺势课程学习),也会带来隐患:早期重标目标集中在起点附近,后期突然铺满全空间,如果回放缓冲区太小,旧样本被挤出,新样本占主导,策略会在“学近目标”和“学远目标”之间来回摆动。
排查方法依然是看数据:每隔一段时间统计一下缓冲区内事后目标 g' 的三维坐标分布。如果发现某个方向的标准差突然跳变,回放比例又不大,策略大概率是在震荡。对应的调整手段有几个:一是把缓冲区容量开大一点,1e6 是一个在 Fetch 类任务上比较保底的值;二是降低每 episode 的重标比例,比如从 0.8 降到 0.6,给原始目标更多话语权;三是在重标时按距离分桶采样,保证不同难度的目标都能被学一轮。
这一类问题本质上是你和“分布漂移”的拉锯战。不必指望一劳永逸,重要的是建立监控习惯。我在训练脚本里总会同时记录三个量:原目标样本数、重标样本平均目标距离、最近 1000 条轨迹的平均回报。三者一对比,很容易判断策略是在稳稳爬坡还是在大步后撤。
4.4 排查速查表
下面这张表是我踩坑后整理的经验集合,适合直接贴在训练屏旁边当参考。使用方法很简单:遇到问题,先对症状,再看可能原因,最后按建议操作。
| 症状 | 可能原因 | 解决手段 |
|---|---|---|
| 损失下降但成功率零 | 奖励阈值计算错误 / 探索噪声过大或过小 | 重写奖励判定,按阈值统一逻辑;调整噪声幅度并观察覆盖率 |
| 目标距离曲线走平 | HER 样本被原始样本淹没 | 提高 her_ratio 到 0.8,增大 future_k 到 6 |
| 策略震荡、旧技能遗忘 | 缓冲区太小 / 目标分布漂移剧烈 | 扩大 buffer 到 1e6,降低重标比例,或分桶采样 |
| Q 值整体畸形(全正或全负) | 奖励函数尺度失衡 | 统一成功奖励为 1,失败为 0 或 -1,避免混用 |
| 早期学习过慢 | future 窗口太长 / 目标太难 | 缩小 future 窗口,优先选近期可达状态 |
| 最终行为“忽远忽近”不收敛 | critic 学习率过高 / target 更新太快 | 学习率降到 3e-4,τ 降到 0.005 |
顺带说一个测试重标逻辑是否正确的好方法:写一个单元测试,人为构造一段轨迹,手动算出按 HER 重标后的奖励,再和compute_reward函数跑出来的结果对一对。我自己至少有三次在重标奖励上栽了跟头,都是这种 5 分钟的小测试救回来的。很多看不出原因的 “训练失败”,最后查到底都是奖励重算和状态拼接的细节出了偏差。
5. 从机器人到业务场景:hindsight还能用在哪
5.1 推荐系统里的“事后目标”
机器人控制是 HER 出生的地方,但它的思想完全能迁移到其他领域。我最早意识到这一点是在做推荐系统项目的时候。推荐场景本质上也是一个目标条件决策问题:你给用户推荐了一件商品 c,用户没点它,但点了另一件商品 c'。如果用传统监督学习,这只是一次失败的“按 c 推荐”,损失正常反传;但如果我们借用 HER 的思路,把这次曝光重定义为“按 c' 推荐”,那么用户点击 c' 这件事就成了一个成功样本。
具体来说,推荐系统的“状态”是用户的历史行为序列,“动作”是推荐内容,“目标”是预期的点击物品,“实际达成目标”是用户点击的物品。对一次推荐失败,我们可以重标为目标“推荐了用户最终点击的那一项”,然后强化学习策略就会学到“看到这样的用户,推荐这一类内容更容易被点击”。这和机器人“虽然没推到指定点,但推到了另一个点”的逻辑一模一样。
不过这里有一个风险:推荐场景的反馈延迟和噪声大,目标重标容易引入虚假相关性,所以不能机械照搬 HER。我的做法是只对“用户有明确点击行为”的样本做重标,并且限制重标比例在 30% 以内,保证原始目标样本占主体,防止策略变成纯热门物品推送器。
5.2 对话系统与文本生成里的“重新定义成功”
在对话生成模型里也可以找到 HER 的影子。生成式对话的评估标准大多是模糊的,没有一个明确的“目标状态”。但如果你把对话任务形式化为目标条件 RL:目标是“生成一个符合指定意图的回复”,实际生成结果可能没有实现指定意图,却实现了一个相关意图。这时,把这次生成结果对应的“实际意图”作为重标目标,此次回复就变成了一个有效的成功样本。
这种做法在模仿学习和离线强化学习里越来越常见。核心方法是先给每条数据标注一个 low-level 的“实际达成意图”,再做目标重写,让模型学会的是“把意图映射到回复”的能力,而不是死死绑定某个外部打分器。我在做客服自动应答时试过类似手段,对冷启动意图识别有肉眼可见的帮助。
当然,文本生成的目标空间是离散的、非欧几里得的,不像机器人坐标那样可以算欧氏距离。所以 HER 在文本上的应用需要额外设计目标表征,比如用句向量空间里的距离来判断“实际达成目标”和“重标目标”的匹配度。这块还处在半研究半工程的状态,但方向是清晰的。
5.3 自动化运维与故障恢复中的“从成功结果反推目标”
再往远了说,运维领域有一类任务特别适合 HER:自动故障恢复。假设系统设置了目标“5 秒内恢复服务”,但实际故障恢复用了 12 秒。从运维流程采集的数据看,这次尝试可以被重标为“目标是 12 秒内恢复服务”,并作为成功样本沉淀到恢复策略里。随着样本增多,恢复策略能学会在不同故障模式下如何高效行动,逐步把“平均恢复时间”从 12 秒压到 10 秒、8 秒、5 秒。
这种应用的动人之处在于,很多自动化系统在演练时都会面临“目标定得太高,一次都没成功过”的尴尬,而 HER 恰恰能把每一次部分成功的演练都变成正样本。只要动作序列和结果状态之间存在着大致可靠的因果链,这套重标逻辑就能用。
我自己的体会是,HER 这种“把失败当成改目标后的成功”的思维方式,在工程上最大的价值并不仅仅是提几个成功率点数。它改变了你看待负样本的态度:很多领域里“负样本”里藏着与成功样本等价的因果信息,只是奖励函数的定义把它们一票否决了。重新思考“目标从哪来”,有时候比重调网络结构更接近问题的本质。
最后分享一个实操上的小建议:如果你想真正理解 HER,不要急着上复杂环境,先在一个迷你网格世界里,把重标逻辑、卡方采样、奖励计算全部手写一遍,再跑通 FetchReach。等你亲手看到那条从零开始爬升的成功率曲线,你就明白了“后见之明”这四个字在算法里到底有多值钱。这是我看再多论文都比不上的一次经历。