1. 为什么“后见之明”能治好强化学习的稀疏奖励病
干强化学习的朋友,大概率都遇到过这种痛苦:环境给你反馈给得特别吝啬,智能体在状态空间里瞎逛半天,一分钱奖励都拿不到,梯度根本没法更新。稀疏奖励问题(sparse reward problem)是RL落地的头号杀手之一,特别是搞机器人控制、搞复杂决策这类场景,跑几百个episode,reward曲线一直是条水平线,谁看了都头大。
“hindsight”这个名字,从字面看是“后见之明、事后诸葛”,但它并不是教人后悔的,而是一种非常有冲击力的训练思路——用事后视角重新解读失败经验,把失败变成有效学习样本。在强化学习圈子里,这对应的正是Hindsight Experience Replay,简写HER,由OpenAI团队在2018年提出,是我认为近年来最实用的稀疏奖励解决方案之一。
这篇内容我想以HER为主线,从头拆一遍它的原理、实现细节和落地经验。适合的对象是:已经了解DQN、DDPG这类基础RL算法,但被稀疏奖励问题卡住的研究生和工程师,以及想用RL做机器人控制、策略优化的朋友。如果你还没碰过RL,我建议先补点基础再来读,不然细节上会有点吃力。
一句话介绍HER的核心价值:它在不改变环境、不引入额外奖励函数的前提下,通过“把失败的目标改成已经达成的目标”,凭空把一条稀疏reward的轨迹变成多条有学习意义的轨迹。这个思路看起来简单,但效果极其凶猛。
2. 核心思路拆解:把“没做到”变成“已经做到”
2.1 稀疏奖励到底难在哪
先看看我们平时训练的典型困境。假设你让机械臂去抓一个杯子,整个episode的奖励函数是:抓到杯子给+1,否则给0。在机械臂还完全没有策略的时候,它可能几千次尝试都摸不到杯子,每一步收到的奖励都是0。对value-based方法,所有(s, a)的价值估计都在原地踏步;对policy-based方法,梯度一直在零附近徘徊。这种现象叫“奖励荒漠”,智能体得不到任何有区分度的信号,策略只能靠瞎猜。
要解决这个问题,传统思路有几条路:设计密集奖励函数(如让奖励随距离连续变化)、利用课程学习(curriculum learning)从简单任务慢慢过渡到复杂任务、或者大规模使用模仿学习。这些方法都有用,但都有代价。密集奖励的设计耗费大量人力,而且容易被智能体钻空子(reward hacking);课程学习需要仔细设计任务梯度,搞不好就出现灾难性遗忘。
HER的思路则是换个角度:既然环境给的奖励太稀疏,那我不依赖环境奖励了,我自己生成奖励信号。具体来说,就是训练时同时优化“预设目标”和“实际达成目标”,一条segmented trajectory通过重新标注目标,立刻变成多条有奖励的样本。
2.2 HER的核心机制全拆解
先明确HER的基本假设。HER适用的任务有很关键的前提:目标必须是可观察的、可判定的,并且存在某种方式计算当前状态和目标状态的“距离”。比如让机械臂把物体推到指定位置,目标就是坐标点(x, y),你可以算当前物体位置和目标点的距离;比如让蚂蚁走到某个方位,目标就是终态位置。如果任务是“倒一杯水且不能洒”这种没法简单定义距离的,HER就用不了。
在这个前提下,HER的工作流大致是这样的:
- 每收集一条episode轨迹,记下整条轨迹里的状态序列和目标状态g。
- 这条轨迹在原始目标g下大概率是失败的,奖励几乎全是0。
- 重点来了:从这条轨迹中挑出一个“事后目标”g',通常选轨迹末尾达到的真实状态,然后重新计算这条轨迹在g'下的奖励。
- 把重新标注后的新样本(s, a, r, s', g')一并放入replay buffer参与训练。
明明原来是一条只有稀疏奖励甚至零奖励的轨迹,经过事后标注之后,就变成了一条有着密集成功信号的轨迹。因为目标g'本身是从轨迹里采出来的,所以“在未来某个时刻能达成这个目标”这件事是可以保证的。
这个思想我打个比方。就像你从北京出发去广州,结果开到了武汉。在原有的导航目标“广州”下,一路都是“没到目的地”。但你换个视角:把目的地改成“武汉”,这一路就变成了“顺利到达武汉”的成功路线。这个例子未必完全贴合,但核心感觉就是这么回事——重新定义成功,把失败数据盘活。
2.3 事后目标怎么选:四种策略对比
HER论文里给出了4种选择事后目标g'的方法:
- final:取整条轨迹的最后一个状态作为g'。最简单,也是最稳的选择。
- random:从轨迹中随机采样一个未来状态作为g'。能产生更多样的目标,但每个目标的有效学习信号可能弱一些。
- future:在轨迹当前时间步t之后任选一个状态作为目标。这个方法是平衡了样本多样性和有效性的折中方案。
- episode:从整个episode里随机取一个状态,对当前t不要求未来关系。
从我的实测经验看,final最稳定,future的综合效果最好。“random”和“episode”往往会导致目标过多过杂,把训练引入较大的方差。实际项目中我基本只用future,k值(每一条轨迹额外生成几条目标)设成4到8。
这里我想解释一个关键点:为什么future策略比final强。final虽然稳定,但每个episode只有一个事后目标,如果轨迹本身就比较短,能学习的信号还是有限。用future策略,你可以在一条轨迹的多个时间点生成目标,相当于从不同视角审视这段经验,样本丰富度立刻上来了,而且future保证目标是“未来可达的”,并不违反因果性。所以在样本利用率这个维度上,future更优。
2.4 对目标的条件判断要注意什么
在实现HER时,最容易忽略的是判定函数(goal condition check)。你重新标注目标后,要判断这条轨迹在g'下是否真的成功了,需要定义一个距离函数d(state, goal)和一个阈值阈值。比如推物体任务,目标坐标和物体当前坐标的欧氏距离小于0.05米就算成功。
这里的坑在于:训练用的距离函数必须和实际评估环境中的一致。很多人在训练环境里用欧氏距离,但测试时用了绝对坐标相等判定,导致训练一切正常,一到评估阶段成功率就崩。强烈建议把判定函数抽成一个模块,训练和评估共用同一份代码。
另外,如果目标空间维度特别大,比如图像目标,那直接用像素距离作为距离函数效果会非常差,因为像素距离和目标语义相关度太低。这种情况一般要引入目标编码器(如用自编码器把图像压成特征向量),在特征空间算距离。这也是HER落地时比较进阶的玩法。
3. 实操准备:环境、网络与超参的选型心得
3.1 选什么环境做验证
我建议你先别急着上自己的复杂业务,找个基线环境跑通HER的流程,再迁移到自己的任务里。我用的验证环境是OpenAI Gym里的“BitFlip”和“FetchPush”。
- BitFlip:一个n位二进制向量,目标是翻转到指定状态。这是一个标准的离散稀疏奖励环境,非常适合快速验证HER机制是否正确。n=10左右时,随机探索基本不可能碰到成功状态,HER却能稳定学到策略。
- FetchPush:机械臂把一个物体推到目标位置。这是连续控制环境下验证HER效果最经典的环境。
如果你用Python环境,OpenAI Gym老版本中有Fetch系列任务,直接gym.make('FetchPush-v1')就能跑起来。新版gym把这些环境移到gymnasium-robotics里,安装时要留意版本对应关系。
3.2 算法基座怎么选
这里要明确一点:HER并不是一个独立算法,它是一种“经验增强机制”,可以叠加在许多off-policy算法上。最经典的是配合DDPG使用。因为DDPG本身就是off-policy的,回放缓冲区里的样本可以反复利用,正好和HER天生搭配。
用PPO这类on-policy算法时,HER的收益没有DDPG那么大,因为on-policy算法要求用当前策略采集的样本更新,重放旧样本的意义有限。但并不是不能用,只是事倍功半。如果你政策上只能用PPO,我建议考虑把HER用在辅助经验回放里,效果有折扣但聊胜于无。
我个人呢,用的是TD3(Twin Delayed DDPG)作为基座。TD3解决了DDPG常见的过估计问题,稳定性明显好于原生DDPG,尤其是在FetchPush这类连续控制任务上,收敛速度比DDPG+HER快大概20%到30%。你也可以直接用SAC替代,效果也很好,但SAC对超参数更敏感,调起来费劲。
3.3 参数设定经验:一个能跑的配置
我直接给一份实测可复现的配置,基于FetchPush环境+TD3+HER:
| 参数 | 设定值 | 说明 |
|---|---|---|
| 回放缓冲区大小 | 1,000,000 | 经验样本要够多,HER本身就是吃样本的 |
| 每次采样的轨迹数 | 8 | 并行采集多条轨迹 |
| 每轨迹最大步数 | 50 | FetchPush不需要太长rollout |
| HER目标采样策略 | future | 首选 |
| HER后验目标数量k | 4 | 每条轨迹额外生成4条新样本 |
| Actor学习率 | 1e-3 | 用Adam优化器 |
| Critic学习率 | 5e-4 | Critic比Actor略慢,避免不稳定 |
| 折扣因子 | 0.98 | 任务较短,折扣不用太接近1 |
| Batch size | 1024 | 大规模batch能缓解目标噪声 |
| 策略更新延迟 | 2 | TD3的标准设定 |
| 探索噪声 | N(0, 0.2) | 并在训练中逐步衰减 |
| 训练总步数 | 约400,000步 | FetchPush在这个量级内能收敛 |
这个配置不是唯一答案,但比较省心。你可以先原样跑通,再针对自己任务调参。
3.4 网络结构应该怎么做
我用的Actor和Critic都是三层MLP:隐藏层[256, 256, 256],激活函数用ReLU。输入是状态向量和目标向量拼在一起。这个点要特别注意:状态和目标最好分开编码,先分别过一层全连接,再拼接。这样可以避免目标信息被高维状态信息淹没。在FetchPush里,状态是25维,目标是3维,直接拼在一起问题不大,但如果你的目标也是个高维向量,分开编码收益会很明显。
另外需要注意的是,HER要求对未来目标进行采样,所以在网络输入层面就要留好目标的维度。别在写网络时偷懒,把状态和目标固化成一个固定维度的输入,导致后续想换目标类型都费劲。
4. 核心代码逻辑与实现要点
下面给出HER的关键实现片段,基于PyTorch风格。重点不在完整工程,而在于把HER的机制写清楚。
4.1 轨迹存储与目标重标注
def process_episode(episode, k=4, strategy='future'): """ episode: dict with keys 'obs', 'actions', 'rewards', 'goals', 'next_obs' 这个函数把一条原始EPISODE转换成多条带HER标注的训练样本 """ obs = episode['obs'] actions = episode['actions'] goals = episode['goals'] # 原始目标,整条轨迹内不变 next_obs = episode['next_obs'] episode_len = len(actions) transitions = [] for t in range(episode_len): # 原始样本必须保留,即使reward=0,它也有状态转移的信息量 transitions.append({ 'obs': obs[t], 'action': actions[t], 'reward': episode['rewards'][t], 'next_obs': next_obs[t], 'goal': goals[t] }) # 生成HER样本 # future策略:在当前时间步之后随机采样k个时间步作为新目标 if strategy == 'future': future_time_steps = np.random.randint(t + 1, episode_len + 1, size=k) for f_t in future_time_steps: if f_t >= episode_len: continue new_goal = goals[f_t] # 用未来状态作为新目标 new_reward = compute_reward(next_obs[t], new_goal) transitions.append({ 'obs': obs[t], 'action': actions[t], 'reward': new_reward, 'next_obs': next_obs[t], 'goal': new_goal }) elif strategy == 'final': # 只用最后一个状态作为新目标 new_goal = goals[-1] new_reward = compute_reward(next_obs[t], new_goal) transitions.append({ 'obs': obs[t], 'action': actions[t], 'reward': new_reward, 'next_obs': next_obs[t], 'goal': new_goal }) return transitions这里有一个无数人踩过的坑:future_time_steps采样时如果范围是t+1到episode_len+1,那么在t=episode_len-1时,np.random.randint是可以取到episode_len的。如果不加if f_t >= episode_len: continue这一行,就会越界报错。有的实现会把这行漏掉,运行到最后一个时间步直接crash。我加了个防御判断,但更好的做法是采样范围写成np.random.randint(t+1, episode_len, size=k),彻底避免越界。
4.2 reward函数的选择与统一
HER里最关键的是compute_reward函数,它决定每条经验是正样本还是负样本。我在FetchPush里用的是稀疏二值奖励:
def compute_reward(achieved_goal, desired_goal): """ 机械臂推物块环境:以物体实际位置和期望位置的距离判断 """ distance = np.linalg.norm(achieved_goal - desired_goal, axis=-1) return -(distance > 0.05).astype(np.float32)这里是稀疏奖励的形式,奖励只有0或-1。距离小于0.05米视为成功,奖励为0;否则为-1。这种二值奖励的好处是避免了智能体去钻密集奖励的空子,缺点是训练信号依然相对稀疏,但经过HER目标重标注后,每条轨迹都能产出一些0奖励的成功样本,梯度信号已经足够了。
千万别在这里画蛇添足,搞个reward = -distance之类的密集奖励。HER本来就是为了绕开密集奖励设计而存在的,如果你又引入了距离型密集奖励,就失去了HER的意义,不如直接用普通DDPG。
4.3 目标状态和观测状态的处理细节
在OpenAI Gym的Fetch环境中,观测结构是一个字典,包含observation、achieved_goal、desired_goal三个字段。训练时,observation + achieved_goal + desired_goal会一起打包进Replay Buffer。HER重标注时,其实只修改desired_goal字段,不修改observation和achieved_goal。
这个细节看起来平淡无奇,但把数据格式统一好非常关键。我通常的做法是:在buffer里存一个结构体,分别为obs、action、reward、next_obs、goal。在训练网络时,把obs和goal拼接作为输入。如果你用的是老式gym环境,记得old observation其实是拼接后的状态,和goal是分开的,一定要从env.reset()返回的字典里提取,不要图省事直接用扁平化数组。
4.4 训练循环怎么组织
训练循环的基本框架如下:
for epoch in range(total_epochs): # 采集阶段 episode = collect_episode(env, actor, noise_scale) # HER重标注并加入buffer transitions = process_episode(episode, k=4, strategy='future') replay_buffer.add(transitions) # 升级阶段:从buffer采样,更新TD3中的Actor和Critic for _ in range(update_steps): batch = replay_buffer.sample(batch_size=1024) td3_update(batch)采集阶段是on-policy的,用当前actor加探索噪声去跑。更新阶段是off-policy的,从buffer里随机采样大量混合了原始样本和HER样本的batch。更新步数通常可以比采集步数多好几倍,因为HER本质上是牺牲训练时间换样本效率,这个trade-off是值得的。
5. 实操过程复盘:我从0到1跑通FetchPush
5.1 我的环境搭建过程
我用的环境是Ubuntu 20.04 + Python 3.8 + PyTorch 1.13。安装gymnasium-robotics有几个版本坑:
pip install gymnasium pip install gymnasium-robotics如果是老教程里用gym.make('FetchPush-v1'),新版环境里记得先import gymnasium_robotics,并且环境id要带-v2后缀:
import gymnasium as gym import gymnasium_robotics env = gym.make('FetchPush-v2', render_mode=None)FetchPush环境本身使用MuJoCo物理引擎,你需要先安装mujoco-py,并且要有MuJoCo许可证(现在DeepMind已开放免费版本,安装mujoco库即可)。如果你在MacOS上跑,老版本的mujoco-py可能会有编译问题,建议直接用官方新库mujoco加gymnasium-robotics的兼容方案。
5.2 我的训练曲线和踩坑记录
第一次跑FetchPush时,我用的是DDPG + HER,规划400k步。前30k步里成功率一直是0,这是正常的,HER并不是一开始就能看到效果的。关键转折出现在80k步左右,成功率开始爬升;到了200k步,成功率基本能维持在0.8以上。如果你的曲线在100k步还没任何动静,大概率是超参数或者网络结构出了问题,别盲目等。
我在训练中遇到过一次典型的“假性收敛”:成功率在某个阶段冲到0.5之后开始波动,到300k步还在0.4到0.6之间振荡,怎么调学习率都没用。后来发现问题是缓冲区里HER样本和原始样本的比例不对。当时我一条轨迹只生成1个HER样本(k=1),buffer里原始样本占绝对多数,大多数是零奖励样本,训练信号还是太稀。把k调到4之后,情况立刻改善。
这里有一个小建议:当一条轨迹是成功轨迹时,可以不生成HER样本,优先保留原始样本。因为成功的原始样本是“真实目标下成功”的稀有样本,价值极高;而失败的轨迹才需要重标注目标来盘活。这套做法能在有限buffer容量下最大化样本价值。
5.3 评估阶段的判定标准要统一
训练时环境会自动计算reward,但评估阶段我通常是关闭探索噪声、直接跑确定性策略,然后自己统计成功率。统计时用的成功判定要和训练时reward计算里的阈值保持一致,否则会出现“训练刘明明reward都到0了,评估却觉得没成功”的奇怪问题。
具体到FetchPush,环境给出的reward本身已经做了阈值判定,所以直接统计np.mean(episode_rewards >= 0)作为成功率就行。如果你自己定义了新的判定函数,务必做成全局常量,避免训练和评估各写一版本。
6. 常见问题与排查技巧实录
6.1 问题速查表
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
| 训练开始100k步成功率始终为0 | 探索噪声过小 | 把探索噪声从0.2调大,或使用epsilon-greedy策略 |
| 同上 | 网络结构能力太弱 | 隐藏层至少256x256x256,别用128维度凑合 |
| 成功率在中间阶段徘徊不升 | HER的k值过小 | k值从1调到4-8,增加后验目标样本占比 |
| 成功率后期骤降 | 缓冲区中成功样本过多 | 限制成功样本在batch中的比例,防止方差增大 |
| 评估成功率显著低于训练 | 训练时用了随机探索策略,评估时用了确定性策略,动作分布不匹配 | 用确定性策略评估时适当增加动作噪声的退火机制 |
| 训练崩溃,loss出现NaN | 学习率过高或reward过大 | 学习率降一个数量级,确认reward范围在[-1, 0] |
| 有报酬但不增加 | 目标判定函数与replay的reward不一致 | 统一compute_reward和评估成功判定代码 |
| 训练无法复现,换了随机种子结果差异大 | HER本身对随机种子敏感 | 用多个种子取成功率中位数,不要看单次结果 |
6.2 踩坑实录:k值不是越大越好
我在一次任务里把k值调到16,结果训练稳定性反而下降了。原因是HER样本在buffer里占比过高,真实目标下的样本被稀释,智能体对原始任务的理解变差。k值4-8是普适的经验区间,如果任务目标空间特别大,可以考虑用较大的k,但一定要监控replay buffer中HER样本占比,控制在50%到70%之间。
6.3 探索策略的调法心得
HER配合高斯噪声做探索时,噪声标准差需要动态调整。固定的0.2噪声在早期好用,但后期智能体策略逐渐确定时,过大的噪声会把已经学好的策略“震碎”。我常设一个噪声退火表,前100k步用0.3,中间100k步用0.1,后面逐步降到0.03。虽然这种人工退火不如自适应方法优雅,但胜在稳定可控。
另外一个很多人不知道的细节:如果使用TD3,目标策略平滑中的噪声也要设置,这个噪声和探索噪声是两码事。目标策略平滑噪声一般设置0.2,配合裁剪范围[-0.5, 0.5],这个参数对TD3的稳定性影响极大,别随手填。
6.4 调试HER时最有用的可视化
每次训练调试时,我最关心的不是reward曲线,而是“成功距离变化曲线”。也就是画出每个epoch中所有episode里”物体最终位置和目标位置的距离“的中位数。这个距离一旦开始持续下降,说明智能体确实在学会接近目标,即使成功率还没上来。这个指标比reward曲线能提前大约20k步给出信号,对判断训练是否健康非常关键。
实现方式也简单:每个epoch评估时,同时记录最终距离,画个折线图。距离曲线下降明显时,哪怕reward曲线还是平的,也可以放心继续训练;距离曲线完全不动,那就赶紧检查网络或探索参数。
7. 超出基础:HER在更复杂场景中的几个应用思路
基础版的HER虽然强,但它能做的不止于机械臂推东西。在掌握核心机制后,有几个进阶方向你可以拓展。
多目标强化学习:当目标任务本身是一个分布(不只是单一目标点),HER天然支持多目标任务的学习。每个episode里的目标g可以从目标分布里随机采样,重标注后的样本也能回馈到不同目标的学习上。这让一个模型能够学会多个技能,比单独训练多个策略高效得多。
分层强化学习:HER的思路也能用在high-level policy的训练上。高层策略负责设定子目标,底层策略负责执行;高层策略面对稀疏的最终任务奖励时,可以用HER重标注子目标,让高层也获得密集信号。这种结合在长程任务里很有潜力。
模仿学习与强化学习的结合:HER还可以做数据增强。即便你有专家演示,但专家数据覆盖不足,把HER的重标注思想用在专家轨迹上,可以生成更多“伪成功”轨迹,辅助策略学习。
我有一次给一个机器人程序化堆叠任务做过一个很粗暴的尝试:原任务要求把三个方块堆成一列,成功率极低。我把它拆成两个子任务:先把任意一块放到目标区域,再把第二块放上去。每个子任务都用HER训练,最后级联起来,效果比端到端硬训好了很多。这就是把HER从单一目标扩展成“课程式多阶段目标”的典型玩法,难度不大,但思路值得借鉴。
不过也要提醒一下,HER不是银弹。如果你的任务目标是“捉住一只逃跑的猎物”,而猎物的行为完全不可预测,此时“事后目标”的选择会很尴尬,因为目标空间本身在动态变化。这类任务建议直接考虑基于分布匹配或对抗训练的方法,HER在这种场景下帮不上大忙。
8. 写在最后的实操建议
今天和大家分享了HER的全流程拆解,从原理到代码再到调试技巧,都是我一个个坑踩出来的经验。最后浓缩成几条我个人最想强调的建议,希望能帮你少走弯路:
第一,先用BitFlip验证机制。这个环境简单到离谱,如果你用最简单的DQN+HER跑BitFlip都学不会,那肯定是代码逻辑写错了,别急着去跑FetchPush。我曾经花了一个周末调试FetchPush训练失败的bug,后来才发现是目标重标注时索引越界导致部分样本没进buffer。
第二,不要一开始就追求复杂网络。我之前喜欢堆网络结构,把Transformer类的架构拿来处理RL状态,结果参数多了几倍,训练速度和稳定性反而差了。HER的核心在于sample efficiency,不在网络表达力。先把256x256x256的MLP跑到能看的效果,再考虑复杂的表征学习。
第三,训练日志规范一定要做好。记录好每个epoch的reward、成功率、目标距离、buffer中HER样本占比和策略熵。这些日志在排查问题时是你的眼睛。没有日志,你只能对着训练曲线瞎猜,那是折磨。
如果你在做强化学习落地,遇到稀疏奖励卡进度,像推箱子、机械臂抓取、走迷宫这类任务,建议认真试一下HER。它不一定是最新潮的算法,但绝对是最有性价比的提升手段之一。
要了解更前沿的变体,我建议去读几篇关键paper:原始的HER论文“Hindsight Experience Replay”、以及后续的“Intrinsic Motivation and Automatic Curricula via Asymmetric Self-play”中关于自动课程学习的思路、还有“Replacing Rewards with Examples”里关于示例目标设定的方法。这几篇把HER的思想延续和发展得很好,能帮你建立更系统的认知。