hindsight,英文直译就是“后见之明”。事情发生之后回头复盘,谁都觉得自己早就知道结果;这种人类认知里再普通不过的现象,到强化学习里反而演变成了一个非常经典的算法——Hindsight Experience Replay(后见经验回放,简称HER)。我第一次真正吃透这个项目,是在做一个机器臂抓取仿真任务的时候:任务奖励给得极其稀疏,Agent在巨大的状态空间里到处乱撞,成功率长期是零。当时我满脑子只有一个念头:如果连“没抓到”的轨迹都能拿来学习就好了。HER解决的就是这个问题。这篇博文适合正在被稀疏奖励问题折磨的强化学习初学者,也适合想往DDPG、SAC这类框架里快速集成HER的工程实践者。我会从问题本身讲起,拆开算法原理,给出可复现的实现细节、调参记录,以及几个我在复现过程里真踩过的坑。
1. 稀疏奖励问题:为什么“没有反馈”比“错误反馈”更致命
1.1 什么样的任务会遇到稀疏奖励
先明确“稀疏奖励”是什么。假设我们做一个机器人抓取任务:目标是把方块从A点抓到B点,成功加1分,没成功扣1分,中间没有任何进度奖励。在这个任务里,Agent每一步能拿到的反馈几乎都是同一个“失败值”,真正能区分行为好坏的信号只在极少数成功的episode里才出现。生活里很多真实目标都有这个特征:能不能把帐篷撑起来、能不能把螺丝拧进孔、能不能在迷宫里走出去——都是“成/败”二元结果。你没法说“我多撑开了一厘米所以加0.01分”,环境根本没提供这种信息。
这种任务放在学术基准里就是经典的goal-conditioned稀疏奖励问题,OpenAI Gym的Fetch系列就是最典型的测试床。我刚接触FetchPush的时候,第一反应是“这不就是推个方块吗,有那么难吗”,结果跑起来才发现,难的不是动作控制,而是“信号不存在”。Agent推了十万次方块,没有一次到达目标位置,那么所有transitions的reward都一样,无论怎么更新参数,梯度方向都几乎是零。这才是稀疏奖励真正要命的地方。
1.2 为什么“没有梯度信号”会让强化学习整体失效
强化学习的各种算法,本质都在利用“差异”来更新:TD误差、优势函数、策略梯度,全都建立在“我这个行为比预期好还是差”之上。如果一条轨迹里所有reward都是同一个常数,那么无论Agent采取什么动作,更新量都趋近于零,甚至直接塌缩到随机策略。
可以做一个非常粗略的估算:假设一个16状态的小型网格世界,终点只有1个,随机游走到达终点的概率大概在5%以下;放到连续控制场景里,目标位置要求落在半径0.05的球内,连续空间维度稍微一高,随机命中的概率就趋近于零。也就是说,Agent不是“学得慢”,而是它压根拿不到可学的信号。很多新手会把这类问题误判为“模型容量不够”或者“学习率没调好”,折腾半天之后才发现,问题是环境根本不给你梯度。
我见过有人在FetchPush上不加任何额外机制硬跑DDPG,两百万步之后成功率仍然是0.1%级别,几乎就是随机策略的水平。这不是DDPG烂,而是目标函数本身出了问题。此时你需要的不是更强的优化器,而是一种能从失败里“硬造”出成功样本的机制,这也是HER出现的根本原因。
1.3 常见的“打补丁”方案为什么治标不治本
遇到稀疏奖励,很多人的第一反应是设计中间奖励,也就是reward shaping。比如把reward改成“到目标点的负距离”,让Agent每靠近一点都有反馈。这个方法在不少简单任务里确实有效,但它有几个很实际的坑:第一,距离函数不一定能准确反映任务的结构,你设计了一个“靠近中心”的奖励,结果Agent学会了停在中心附近而不是真正完成任务;第二,每个新任务都要重新设计一套势函数,工程量很大;第三,shaping term引入后,最优策略可能会发生变化,甚至导致Agent钻空子。我记得论文里专门讨论过potential-based shaping,虽然理论上可以保持最优策略不变,但在实际工程里,你很难保证自己设计的势函数满足那个严苛条件。
另一个常见思路是课程学习(curriculum learning),先让Agent从简单难度开始学,再逐步增加难度。这个方法有用,但课程怎么设计、难度怎么递增、每个阶段训练多久,全是新的超参数。而且阶段切换处理不好,Agent容易灾难性遗忘,前面学的好好的,后面一提高难度,策略直接崩掉。还有一个路线是给Agent加内在奖励,比如好奇心驱动的ICM,让Agent优先探索“预测误差大”的区域。这个思路我也在几个环境里试过,效果不稳定,而且额外引入了一个预测网络,调参成本并不低。
这些方案本质上都在“修改环境反馈”,而HER选择了一个完全不同的角度:修改学习素材本身——同一个已经经历过的事实,换一个目标来看,它就是成功经验。这个视角的转变,是理解HER的关键。
2. HER核心原理:把“失败轨迹”改写成“成功轨迹”
2.1 后见之明的数学化表达
HER的核心观察非常朴素:一条没有达到指定目标的轨迹,对于另一个“恰好落在轨迹实际终点附近”的目标而言,就是一条成功轨迹。
我们用数学化一点的记号来表达。假设一条episode产生了一个状态-动作序列:
τ = (s₀, a₀, r₀, s₁, a₁, r₁, …, s_T)
在goal-conditioned设置下,环境会额外给一个目标g,每一步的奖励是:
r_t = reward(s_{t+1}, g)
当奖励函数是稀疏的时候,r_t在绝大多数时间步都是同一个失败值,比如-1。这意味着,这条轨迹对原始目标g来说毫无学习价值。但如果我们把目标从g换成一个虚拟目标g',其中g'就取轨迹最终达到的状态s_T,那么新奖励:
r't = reward(s{t+1}, g')
在轨迹的后期会变成成功值,因为s_{t+1}已经非常接近s_T了,或者说,它就是在向s_T逼近的过程中产生的状态。于是一条“整体失败”的轨迹,经过目标改写之后,变成了大量“成功”样本的集合。这就是后见之明在算法里的体现:既然失败了,那就重新定义目标,再教育自己一次。
2.2 Goal Relabeling:一次性把“废样本”变成“黄金样本”
实际操作里,目标重标记(Goal Relabeling)并不复杂,核心就三步:拿到一条episode的全部transition,为每一条transition选一个合适的虚拟目标g',然后按照原始稀疏奖励函数重新计算这条transition的reward,得到一个改写后的新样本。
以OpenAI Gym的Fetch环境为例,具体是这样的:原始goal是方块应该被推到的目标位置,achieved goal是方块当前的实际位置,真实reward在“未成功”时是-1,“成功”时是0。做relabel时,我们把transition里的目标从g替换成一个新的g',然后重新计算:
r' = 0.0 如果 ‖achieved_goal − g'‖ ≤ threshold,否则 r' = −1.0
这里threshold就是“成功判定”的距离阈值,Fetch环境里一般是0.05。由于virtual goal本身就是从这条轨迹的未来状态里选的,所以s_{t+1}和g'之间的距离往往非常小,r'几乎都是0。于是一条原本全是-1的轨迹,经过relabel之后,在buffer里贡献了几十条带“成功”标签的样本。
这里需要特别强调一个细节:relabel不改变环境的状态转移,也不改变动作本身。因为我们只是在经验回放的时候改写了“目标”这个条件变量,并没有篡改物理过程。对一个目标条件策略π(a|s, g)来说,它学到的是“当目标恰好是g'时,这个状态-动作对能带来成功”。这种知识是可以迁移到测试阶段的——如果测试时目标真的落在g'附近,这个策略自然能用上。
2.3 为什么改写目标不会把策略学歪
很多人第一次听到HER时的反应是:“这不是让Agent自欺欺人吗?把失败说成成功,策略不会学歪吗?”
我的理解是这样的:HER并没有让Agent以为“我做到了原始目标”,它只是额外提供了一批“另一个目标下成功”的样本。策略是goal-conditioned的,也就是说,它必须同时看到“当前状态”和“目标”才能输出动作。对这批relabeled样本,网络学到的是“当目标为g'时,执行a可以走向成功”。这批样本的价值在于教给Q函数一个非平凡的梯度:这个状态-动作组合是“有价值”的,只是它的价值是相对于g'而言的。
从梯度的角度来看,原始目标下的失败样本对Q函数的更新贡献几乎为零,因为TD误差接近于零。而relabeled样本的奖励r'是成功值,TD误差不再为零,Q函数就能真正开始学“状态-动作-目标”之间的价值映射。这就像考试失利的学生复盘错题:试卷是同一张,但目标从“这次考试要上清华”改成“把这套卷子的每个知识点彻底搞懂”,每一道错题都变成了宝贵的学习资料。你并没有欺骗自己说“我考上了清华”,你只是换了一个更务实的学习目标。
2.4 RELABEL的频率与样本比例
理论讲完了,工程上第一个要问的问题是:relabel多少条合适?总不可能每一条原始样本都配一条relabel样本,那样buffer里全是虚拟目标,真实目标反而被稀释了。
从论文和主流复现来看,通常每个transition会额外生成k条relabeled样本,k的取值在2到8之间,最常见的是4。比如一条原始transition进入buffer后,它会连带产生4条不同的虚拟目标版本一起进入buffer。这样在采样时,batch里原始目标和虚拟目标的比例大约是1:4。这个比例不是严格固定的,我在实验里测过,k=4和k=8的成功率差距不大,但k=8会明显增加buffer存储压力和采样的重复率,所以k=4是我认为性价比最高的选择。
3. 算法实现:目标重标记的工程细节与代码
3.1 完整训练流程与伪代码
HER本身不是一个独立的强化学习算法,它是一个“数据增广策略”,必须挂在某个off-policy算法外面。最经典的组合是DDPG+HER,后来的SAC+HER、TD3+HER也都是这个思路。
整条训练流程是这样的:先让环境随机采样一批目标任务,Agent跑完整episode,把原始transition暂存起来;然后遍历每条transition,用future策略选出虚拟目标,重新计算奖励,生成额外的relabeled transition;之后把原始transition和relabeled transition一起push进replay buffer;最后每次训练采样batch,正常更新critic和actor。
下面是一个简化但足够说明逻辑的伪代码:
def collect_and_relabel(env, actor, buffer, her_k=4, mode="future"): transitions = [] goal = env.sample_goal() obs = env.reset(goal=goal) for t in range(max_steps): action = actor.select_action(obs, goal) # goal-conditioned策略 next_obs, reward, done, info = env.step(action) transitions.append((obs, action, reward, next_obs, goal.copy())) obs = next_obs if done: break # 1. 原始transition直接入buffer for s, a, r, s_next, g in transitions: buffer.push(s, a, r, s_next, g) # 2. 构造“未来状态”列表,用于relabel future_states = [tr[3] for tr in transitions] # 3. 为每条transition生成relabeled样本 for t, (s, a, r, s_next, g) in enumerate(transitions): for _ in range(her_k): # future模式:从t时刻之后的未来状态里随机选一个 candidates = future_states[t:] virtual_goal = np.random.choice(candidates) new_reward = 0.0 if is_success(s_next, virtual_goal) else -1.0 buffer.push(s, a, new_reward, s_next, virtual_goal)这段伪代码里最关键的是future_states[t:]这个切片。它保证了virtual goal只会从当前时间步之后的未来状态里选,不会拿“过去的状态”来当目标,否则会破坏因果关系。
3.2 虚拟目标选择:四种策略到底怎么选
论文里对比了四种虚拟目标的选择策略。我直接给结论:future策略效果最好,k=4最常用,自己做实验建议无脑用这个组合。
| 策略 | 虚拟目标来源 | 优点 | 缺点 |
|---|---|---|---|
| final | 只用轨迹最后一个状态s_T | 实现最简单 | 虚拟目标种类单一,多样性差 |
| random | 从episode任意状态随机选 | 多样性高 | 容易选到不现实的、离当前太远的目标 |
| future | 从当前t之后的未来状态里随机选k个候选,再选一个 | 兼顾多样性和可达性 | 必须等整个episode结束才能做relabel |
| episode | 从完整episode的所有状态里随机选 | 与random类似 | 多样性高但因果性差 |
future之所以最好,是因为它选出的虚拟目标都是“当前状态之后经过真实动作序列能达到的状态”,这对Q函数非常友好。它告诉Q函数:“你目前走的方向是对的,因为后续确实有人到达过这里。”而random和episode策略可能会选出轨迹开头的状态作为虚拟目标,导致Q函数学到一些相互矛盾的信息。
3.3 为什么只有Off-Policy算法能配合HER
这个问题我在很多技术群里被反复问过:能不能把HER塞进PPO里面?答案很明确:不能。
原因要从on-policy的前提讲起。PPO这类算法要求所有训练样本都来自当前策略π_old,它的目标函数里隐含了重要性采样权重,样本的行为分布假设和当前策略一致。而HER做relabel之后,样本的目标g'被改写了,这些样本的隐含行为策略已经不再是当前策略π_old了,而是“一个试图完成g'的未知策略”留下的轨迹。硬塞进PPO,更新方向会很快失真,方差爆炸,训练直接崩。
DDPG、SAC、TD3这类off-policy算法则完全没这个问题,因为它们从replay buffer里随机采样更新,本来就不要求样本来自当前策略。你在buffer里放什么样本,它都能利用。HER只是额外多放了一批“以虚拟目标为条件”的样本而已,对于off-policy算法来说,这跟换了一批新数据没区别。这也是为什么你看到的HER论文和复现,十有八九都挂的是DDPG,偶尔有SAC,但几乎见不到PPO。
3.4 以DDPG为骨架的核心更新逻辑
下面给出一段以DDPG为骨架的“最小核心实现”。这里不贴完整工程代码,只展示和HER最相关的更新逻辑,方便理解样本怎么流入网络:
# 伪代码:DDPG + HER 的单次更新 batch = buffer.sample(batch_size) states, actions, rewards, next_states, goals = unpack(batch) # critic loss:用TD目标计算 with torch.no_grad(): next_actions = target_actor(next_states, goals) target_q = rewards + gamma * target_critic(next_states, next_actions, goals) current_q = critic(states, actions, goals) critic_loss = mse_loss(current_q, target_q) # actor loss:最大化当前Q值 actor_loss = -critic(states, actor(states, goals), goals).mean() # 更新参数,软更新目标网络注意actor和critic的输入都包含了goals。如果没有这个goal-conditioned结构,HER根本无从谈起。另一个实现细节是:采样batch时建议控制原始样本和relabeled样本的比例,比如80%原始+20%relabeled。如果你让batch里大部分都是relabeled样本,策略会过度偏向“虚拟目标区域”,导致真实目标上的表现反而变差。
4. 实验记录:从FetchReach到FetchPush的参数摸索
4.1 测试环境与基准结果
我在复现HER时主要用了OpenAI Gym的MuJoCo Fetch系列,这是HER论文里最标准的测试环境。FetchReach是机械臂末端到达目标点,任务最简单,连随机策略都有一定成功率;FetchPush是机械臂把方块推到目标位置,是稀疏奖励的典型案例;FetchSlide是推完后靠滑行到达目标,难度更高,对物理参数更敏感。
我的实验对比结果如下表。数值不一定是标准答案,但趋势非常明确:
| 环境 | 算法 | 达到80%成功率所需训练步数 |
|---|---|---|
| FetchReach | DDPG(无HER) | 约20k~40k |
| FetchPush | DDPG(无HER) | 长期无法收敛,成功率<1% |
| FetchPush | DDPG + HER | 约250k~400k |
| FetchSlide | DDPG + HER | 约500k以上,且波动很大 |
无HER的DDPG在FetchPush上跑了两百万步,成功率几乎一直贴着零轴;加了HER之后,大约二十万步开始出现明显的成功率爬升,稳定在80%以上还需要更长时间。这个差距足够说明HER的价值。
4.2 关键超参数经验值
超参数直接决定HER能不能收敛。下面这些数值是我在自己实验里反复试过之后觉得最稳的:
- her_k = 4,虚拟目标数量为原始transition的4倍。
- 虚拟目标选择方式:future策略。
- replay buffer容量:建议1e6起步,HER对buffer大小非常敏感,因为一条episode的原始轨迹必须整体保存,buffer太小的话relabeled样本还没被采到就被冲掉了。
- 成功判定阈值:Fetch环境默认0.05,不要随意调大,调大等于降低任务难度,会让测试结果虚高。
- critic网络:两层MLP,隐藏层256x256,ReLU激活。
- 折扣因子γ:0.98左右。
- 目标网络软更新系数:0.05。
我试过把her_k从4调到8,成功率并没有显著提升,反而让buffer里同一时间步的样本密度过高,采样多样性变差。所以我最终维持在4。
4.3 训练中的典型曲线:看懂“成功率的爬坡”
加了HER之后,成功率曲线并不是“突然从0跳到90%”,而是平稳爬坡。我在FetchPush上观察到的典型曲线是:前5万步几乎平线,10万步开始微升,20万步以后斜率明显变大,30万步以后在80%附近震荡。如果完全没有HER,同一曲线会贴着零轴,偶尔蹦出几个点又掉回去。
还有一个经验:训练时一定要把“成功率”作为独立指标记录下来,不要只看total reward。因为HER改写后的样本里有很多虚拟目标,total reward会被这些relabeled样本拉高,看起来“训练好像不错”,但实际上真实目标上的成功率可能并不高。我后来习惯每训练一定步数就做一次纯evaluation,测试时绝对不做任何relabel,只看原始目标下的真实成功率。
4.4 目标采样与分布匹配
训练时给环境采样的目标分布,必须和测试分布保持一致。很多复现失败的案例就出在这里:训练时随机采样的目标集中在某个区域,测试时却把目标放在另一个区域,Agent自然跑不动。
我在FetchSlide上吃过一次亏:训练时用均匀随机采样目标,结果测试目标集中在远端区域,成功率从70%掉到20%。后来我改成“训练和测试使用同一套目标分布”,问题立刻缓解。这个细节虽然简单,但在HER里尤其重要,因为relabeled样本的目标分布是“环境实际达到的状态分布”,如果你训练时的真实目标分布太窄,虚拟目标的分布也会跟着偏,最终学到的策略只对“容易到达的区域”有效。
5. 常见问题与排查技巧实录
5.1 目标分布偏移:虚拟目标分布不等于真实目标分布
这是HER最隐蔽的坑。relabel时用的虚拟目标大量来自“Agent实际到达过的状态”,这些状态往往集中在起始区域、容易到达的区域,分布很窄。而测试时的真实目标可能分布在整个目标空间。两者不一致,会导致Q函数在虚拟目标分布上过拟合。
我采用的缓解手段有三个:第一,训练时保证真实目标的采样范围尽量覆盖完整目标空间;第二,每隔一段时间向buffer里重新注入一些“纯原始目标样本”,不做任何relabel,用真实奖励计算;第三,evaluation时绝对不加HER,只看真实目标上的表现。这三种手段叠在一起,能把分布偏移对最终效果的影响压到最低。
5.2 “我把HER加进PPO,效果直接崩了”
如果你也试过这个组合,停手吧,这不是代码bug,是原理不兼容。PPO等on-policy算法的目标函数隐含了行为策略的分布假设,而relabeled样本的目标被改写后,行为策略分布已经变了。强行让PPO优化这些样本,等同于喂给它一批“来路不明”的数据,更新方向自然会乱。
如果项目里必须用on-policy算法,我建议重新审视任务是否真的需要PPO。大部分稀疏奖励的goal-conditioned控制任务,DDPG+SAC这类off-policy算法配上HER已经足够能打。如果你执着于on-policy,可以考虑先离线用HER+SAC生成一批高质量轨迹,再拿去做PPO的初始化,但这样工程复杂度会高很多,我一般不建议。
5.3 高维目标下HER失灵
当目标不是低维坐标,而是完整图像时,HER的效果会大幅下降。原因很直接:成功判定从“欧氏距离小于阈值”变成“两张图相似”,高维空间的采样密度极低,relabel后依然很难产生“成功”样本;而且图像目标之间相似度矩阵过于稠密,Q值估计的方差会变得很大。
我的处理思路是:先训练一个自编码器把图像目标压缩成低维潜变量,在潜变量空间里做HER和距离判定。这相当于把HER从“原始观测空间”搬到“语义空间”,能显著缓解维度灾难。另一个变体是Hindsight Goal Generation,通过额外生成虚拟目标来提升样本效率,但工程复杂度更高,初学阶段不推荐一上来就碰。
5.4 Buffer里重复样本太多,采样效率下降
每一条原始transition生成k条relabeled样本后,同一时间步的样本会以很高的密度挤在buffer里。如果不加控制,采样时可能连续抽到同一个时间步的不同虚拟目标版本,多样性很受影响。我的做法是:在采样的batch里手动设置一个比例,比如80%原始样本+20%relabeled样本;同时避免k值过大,4就够。如果你发现训练过程中Q值偶尔出现尖刺,大概率就是batch里重复样本太多造成的。
5.5 最容易忽略的预处理:achieved goal的索引与归一化
在Fetch系列环境里,observation向量里同时包含机械臂末端位置、夹爪位置、方块位置等多个信息。提取achieved goal时必须确认你拿的是“任务相关对象”的坐标。比如FetchPush里的achieved goal是方块位置,不是机械臂末端位置。我见过有人把机械臂末端位置当作achieved goal做relabel,结果就是Agent学了十几万步,测试时方块纹丝不动,因为他一直在教Agent“把机械臂末端移动到虚拟目标”,而不是“把方块推过去”。
另外一个容易踩的坑是尺度问题。如果achieved goal和goal在数值范围上差异很大,距离计算会被某个维度主导,成功判定失真。我建议在做距离计算前,把achieved goal和goal在相关维度上做归一化,并保存一份单独的achieved_goal用于relabel,不要直接用原始observation做切片。这个预处理看起来不起眼,但它决定了你训练数据里“成功”到底意味着什么。
我在实际复现HER的过程中,最深的体会是:这个算法不复杂,也不玄学,核心核心就是那几行relabel逻辑。但它从根上改变了我对“失败样本”的态度。以前做强化学习,遇到不收敛第一反应永远是调reward、调shaping,费了很大劲却常常越调越歪;有了HER之后,很多任务根本不需要额外设计中间奖励,只要把稀疏奖励做对、relabel写好,Agent就能自己从失败轨迹里挖出经验。
最后分享一个小技巧:如果你想复现HER,先别急着上FetchPush这种难环境,先在FetchReach上把代码跑通。FetchReach就算没有HER,DDPG也能勉强收敛,你可以快速验证自己的goal-conditioned策略和relabel逻辑是否正确。一旦逻辑没问题,再换到FetchPush,你会发现剩下的坑——目标采样、buffer比例、achieved goal索引——几乎都是工程细节问题,而不是算法本身的问题。祝你们都能从失败轨迹里挖出金子。