拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

稀疏奖励困境下的HER:将失败轨迹改写为成功样本的强化学习技巧

稀疏奖励困境下的HER:将失败轨迹改写为成功样本的强化学习技巧

1. 为什么需要“后见之明”:稀疏奖励是强化学习的第一道坎

做强化学习落地项目的人,十有八九都遇到过同一个问题:智能体在环境里跑了几万步,奖励始终是0。站在旁边看训练曲线的人一脸茫然,只有亲自调过参的人才明白,这不是代码写错了,而是环境给出的反馈太稀疏,稀疏到让智能体根本无从学起。

“hindsight”这个名字,翻译过来叫“后见之明”。我最早接触到这个概念,是在做机器人抓取任务的时候。机械臂要从桌面上抓一个方块放到指定位置,状态是12维的关节角度和末端位置,动作是4维的关节速度,奖励设计长这样:如果末端执行器距离目标点小于5厘米,给1分,否则给0分。听起来很简单对吧?可真跑起来你会发现,随机初始化的策略连“靠近目标”这个事都做不到,一整条episode下来,所有奖励全是0,价值网络拿到的梯度信号等于不存在。训练了2000个episode,成功率还是0。

这其实就是稀疏奖励问题的典型症状。奖励信号太稀,智能体像在黑房间里找一盏灯,只有在碰巧摸到开关的那一瞬间才能看到亮光,可它根本不知道该怎么复现那个动作。解决这类问题,传统思路有三板斧:奖励塑形(reward shaping)、课程学习(curriculum learning)、探索增强(intrinsic motivation)。但每一招都有明显的局限。

奖励塑形需要你针对具体任务手工设计中间奖励,比如“离目标越近分越高”。这个方案在仿真环境里能用,但真实业务里很容易出问题。你让机械臂去抓一个会滚动的小球,手工设计“接近就给分”的奖励,它很快就学会了把球推到角落而不是抓起来,因为推球更容易获得“接近”反馈。这就是典型的奖励黑客(reward hacking),对付这种问题比对付稀疏奖励本身还费劲。

课程学习的问题在于需要人工划分难度阶梯,而且任务一旦切换,前一阶段学到的策略可能完全失效。intrinsic motivation那一类方法,比如好奇心驱动探索,虽然通用性强,但在高维连续动作空间里,好奇心很容易变成“瞎动”,探索效率惨不忍睹。

后来我认真研究了一下HER(Hindsight Experience Replay)的思路,发现它其实跳出了“怎么设计更好的奖励”这个框架,转而问了一个更根本的问题:失败的轨迹里,是不是真的没有学习价值?

答案显然是否定的。所谓失败,只是“没有达成预先指定的那个目标”,但这条轨迹本身,其实已经成功到达了某个状态——如果真的想到达那个状态,这整条轨迹就是一份完美的示范数据。

2. hindsight的核心思路:把失败记录改写成成功样本

我最早是在一篇讲机器人操作的研究论文里看到了完整的HER描述,当时脑子里蹦出来的一个念头是:这不就是把程序员的调试思维搬到了强化学习里吗。调试程序的时候,我们不能只盯着“程序为什么没通过测试”,而要看“程序实际完成了什么”——它可能没通过用例A,但意外完成了用例B,那我们就可以把用例B写进测试集,把它变成一次有效验证。

HER做的正是这件事。它允许智能体在训练过程中,把“没有达成预设目标”的轨迹拿出来,重新选择轨迹中实际到达过的状态作为新的目标,再按新目标重新计算奖励。这样一来,原本全是0的奖励序列,有一部分就变成了正奖励,学习信号出来了。

2.1 从一个具体例子理解重标注

拿我最常用来测试算法的机械臂推方块环境来举例。环境定义是这样的:状态空间是一个二维平面坐标,机械臂末端位置就是状态本身;动作是二维速度指令;目标也是一组二维坐标,表示我们希望把方块推到的位置。每一步动作都有代价,如果末端位置和目标位置的距离小于阈值,回合结束并给正奖励,否则奖励为0。

假设智能体一局跑下来,初始位置是(0.1, 0.2),目标位置是(0.9, 0.8),它这一局实际走出了这样的轨迹:

(0.1, 0.2) → (0.3, 0.4) → (0.5, 0.3) → (0.7, 0.6) → (0.6, 0.9)

最终位置是(0.6, 0.9),离目标(0.9, 0.8)还差一段距离,所以按照原始目标计算,这条轨迹的奖励序列是[0, 0, 0, 0, 0],没有任何学习信号。整个人类裁判看到这段轨迹,会判它失败。

但HER的角度完全不同。它把终态(0.6, 0.9)当作“这次实际想要完成的目标”,然后重新计算整条轨迹的奖励。起点是(0.1, 0.2),终点是(0.6, 0.9)——回头来看,这条轨迹确实完成了从起点到终态位置的移动。按照新目标(0.6, 0.9)重新计算距离,最后一步距离是0,奖励是1,其余各步也都变成了带有激励信号的中间状态。这条原本完全无用的轨迹,被改写成了“如何从(0.1, 0.2)移动到(0.6, 0.9)”的成功示范。

这就是hindsight最核心的一步棋。

2.2 为什么重标注能骗过价值网络

可能有人会问:目标都被改了,智能体学到的还是原来那个任务吗?这个问题我一开始也困惑过。后来想明白了一个关键点:我们并不是真的要让智能体追求那些伪目标,而是用伪目标制造“行为→正向结果”的关联信号,让价值网络逐步逼近真实的最优价值函数。

从数学上看,HER训练的是同一个策略网络和价值网络,只是采样的数据里,有一部分是“带有重新标注目标”的轨迹。价值网络通过这些伪成功样本,学会了一个模式:“如果某段轨迹最终接近某状态,那么这段轨迹的决策序列在该状态下是可行的”。这个模式本身是通用的,不依赖具体目标。等价值网络学到了这类模式,再面对真实目标时,它对“哪些行为能导致目标接近”就有了预判能力,策略网络也有了一条可以爬的梯度路径。

打个比方,这就像学做菜。你本来想学红烧肉,结果第一次做出来一锅偏甜的版本。如果你只盯着“这不是红烧肉”来反思,什么都学不到;但如果你换个角度看,“甜口的红烧肉”其实也成立,那这次做饭就积累了“如何把糖色炒好”“火候怎么控制”的经验。下次做正宗红烧肉时,炒糖色这个关键环节你已经会了,只需要微调其他步骤。HER做的就是这个转换视角的动作。

3. 从零实现HER:环境设计、回放池改造与训练闭环

理论讲得再漂亮,不落地都是空的。我在实际项目中把HER嵌进过一个基于Pytorch的DDPG实现里,也尝试过和SAC、TD3搭配,整体框架是通用的。下面我把完整实现拆成三部分讲,从环境设计到回放池改造再到训练循环,每一步说清楚为什么这么写。

3.1 环境设计:状态与目标必须拆开定义

实现HER的第一步,是修正你对“状态”的理解。很多入门教程里,状态和目标被揉在一起,比如把目标坐标拼进状态向量。这种做法在标准强化学习里没问题,但到了HER场景就会很麻烦,因为重标注目标时,你希望在不改变“状态转换”的前提下替换“目标描述”,两者必须分离。

我在实验里用的是这样一个环境接口:

class HindsightPushEnv: def __init__(self): self.obs_dim = 4 # 机械臂末端位置 + 方块位置 self.goal_dim = 2 # 目标位置坐标 self.action_dim = 2 # 速度指令 def reset(self): # 初始化机械臂末端位置 self.arm_pos = np.array([0.0, 0.0]) # 初始化方块位置 self.block_pos = np.array([0.1, 0.2]) # 设置目标位置 self.goal = np.array([0.9, 0.8]) # 返回观测和目标 obs = np.concatenate([self.arm_pos, self.block_pos]) return {"observation": obs, "achieved_goal": self.block_pos.copy(), "desired_goal": self.goal.copy()} def step(self, action): # 机械臂推动方块,位置更新逻辑略 reward = 0.0 distance = np.linalg.norm(self.block_pos - self.goal) if distance < 0.05: reward = 1.0 done = True else: done = False obs = np.concatenate([self.arm_pos, self.block_pos]) info = {"is_success": done} return {"observation": obs, "achieved_goal": self.block_pos.copy(), "desired_goal": self.goal.copy()}, reward, done, info

观测里必须包含三个字段:observation、achieved_goal、desired_goal。这是HER实现的硬性要求,因为重标注时需要知道“这一时刻实际到达了哪里”(achieved_goal),才能把它替换为新的desired_goal。很多公开框架如mujoco-py的FetchReach环境也是这个结构,直接沿用了这套接口约定。

3.2 回放池改造:原始轨迹与重标注轨迹比例必须控制

HER和普通经验回放最大的区别,就是在把轨迹存入缓冲区时,除了保留原始目标的数据,还要额外生成若干条重标注目标的数据。我在实现里会把每条轨迹保存一个完整的episode,而不是一条一条的transition,这跟传统的DQN回放池不太一样。

代码逻辑大致如下:

def store_episode(self, episode_transitions): # episode_transitions 是一个列表,每项是 (obs, action, reward, next_obs, done, info) original_goal = episode_transitions[0]["desired_goal"] achieved_goals = [trans["achieved_goal"] for trans in episode_transitions] # 保留原始目标版本 for trans in episode_transitions: self.buffer.add(trans) # 生成重标注目标版本 future_goal_count = 4 for _ in range(future_goal_count): # 从轨迹中随机选一个时间步的目标 idx = np.random.randint(len(episode_transitions)) new_goal = achieved_goals[idx] for trans in episode_transitions: new_obs = trans["observation"].copy() new_goal = new_goal.copy() # 替换目标字段 trans["desired_goal"] = new_goal # 重新计算奖励 distance = np.linalg.norm(new_obs[:2] - new_goal) trans["reward"] = 1.0 if distance < 0.05 else 0.0 self.buffer.add(trans)

有几个细节值得单独说。

第一,新目标的采样方式不是随便选的。HER论文里对比过四种策略,分别是final(只用末态)、future(未来k步中的某步)、random(整条轨迹随机一步)、episode(从当前轨迹随机一步)。我实测下来,future策略效果最稳定,特别是在轨迹比较长的时候。因为它选出的伪目标离当前时刻不远不近,既能保证轨迹后半段确实“到达”了目标,又不会让前半段和目标的距离过于遥远导致信号太弱。所以上面的代码示例直接用了future策略的思想,从整个轨迹里随机抽一个时间步的状态作为新目标。

第二,重标注后的奖励必须重新计算,不能直接用原来的transition里的reward。因为目标变了,距离自然变了,原来的奖励序列在新目标下完全没意义。

第三,缓冲区里原始目标和重标注目标的比例要控制好。如果全部放重标注样本,策略就会完全忘记环境中真实的、用户关心的目标是什么,最后学成“不管任务是什么,就往最容易的方向挪”。我的经验是,原始的transition全部保留,另外为每条轨迹额外生成4条重标注轨迹,整体比例大约在1比4,效果比较均衡。如果训练过程中发现原始目标达成率上不去,就把重标注的条数降到2;如果完全学不动,再往8调。

3.3 训练循环:actor-critic更新和目标网络软更新

HER本身是一个数据处理技巧,不关心你底层用什么算法,关键是它必须配合off-policy的算法使用。因为重标注后的轨迹和当前策略产生的数据分布不同,只有off-policy算法能反复从回放池里抽数据更新,on-policy算法比如传统的policy gradient,根本没法利用这些非当前策略的数据。

我用DDPG作为底层的更新逻辑,训练循环简化成这个样子:

policy_net = Actor(obs_dim + goal_dim, action_dim) value_net = Critic(obs_dim + goal_dim + action_dim, 1) target_policy_net = Actor(obs_dim + goal_dim, action_dim) target_value_net = Critic(obs_dim + goal_dim + action_dim, 1) # 目标网络参数初始化与软更新 def soft_update(target, source, tau): for target_param, param in zip(target.parameters(), source.parameters()): target_param.data.copy_(tau * param.data + (1.0 - tau) * target_param.data) for episode in range(total_episodes): transitions = [] obs = env.reset() done = False while not done: state = np.concatenate([obs["observation"], obs["desired_goal"]]) action = policy_net(torch.FloatTensor(state)).detach().numpy() # 在动作上加Ornstein-Uhlenbeck噪声,促进探索 action = np.clip(action + ou_noise.sample(), -1.0, 1.0) next_obs, reward, done, info = env.step(action) transitions.append((obs, action, reward, next_obs, done, info)) obs = next_obs store_episode(transitions) # 从回放池采样batch,更新网络 batch = buffer.sample(batch_size) # 计算target Q值、更新Critic、延迟更新Actor,具体代码略

这个流程有一个点值得细看:状态输入是把observation和desired_goal拼接起来,而不是只输入observation。这是因为策略必须感知“当前想去哪”,目标不同,最优动作不同。HER重标注后,同一条轨迹可能被标注了不同目标,策略网络被迫学会“根据目标调整行为”,而不是机械记忆某条固定路径。这其实是HER的另一个隐藏好处:它天然提升了策略目标的泛化能力。

4. 参数调优与训练稳定性的实战经验

HER不是加了就一定能收敛,它只是把“完全学不动”的问题变成了“能学但需要调参”的问题。我在几轮实验里总结出一些关键超参数的选法和现象,这里直接分享。

4.1 关键超参数怎么选:目标采样策略、回放比例、噪声强度

先列一张参数速查表,是我调过的几次实验里比较靠谱的初始值,新环境可以直接照搬再微调。

参数名建议值说明
目标采样策略future(k=4)从轨迹未来4步内随机选状态作伪目标
每条轨迹重标注条数4原始轨迹和重标注轨迹比例约为1:4
回放池容量1e6越大越稳定,但内存占用要高
batch_size256大batch能缓解重标注样本的方差
actor学习率1e-3和critic学习率分开调,actor低了容易不动
critic学习率1e-3高了容易震荡,建议先固定1e-3再调
目标网络软更新tau0.05HER下目标网络更新要快一点,避免旧目标信号残存
探索噪声OU噪声,sigma=0.2连续动作空间,比高斯噪声更平滑

具体聊几个容易踩坑的。

目标采样策略选future而不是final。我最早用的final策略,只用轨迹终态做伪目标,短轨迹效果好,但一旦trajectory超过50步,如果终态离起点特别远,前面40步的所有状态到终态目标的距离都很远,重标定的奖励只能在最后几步变成正信号,中间依然全是0,学习效率纤维。future策略相当于把整条轨迹拆成多个“子成功片段”,每一段附近都有正奖励,信号密度高得多。

重标注条数不是越多越好。有一轮我把每条轨迹重标注数量从4加到16,结果奖励噪声急剧增大,value network的loss曲线像锯齿一样抖。原因也好理解,重标注目标越多样,同一条轨迹的“目标含义”就越模糊,价值网络拟合出一个平均的东西,既不对应原目标也不对应伪目标,泛化能力反而变差。实际跑下来,4到6条是甜点区间。

探索噪声的scale也很关键。标准DDPG里我喜欢用0.1的OU噪声,但HER场景下噪声要调大一些,我习惯设到0.2甚至0.3。原因是在稀疏奖励环境里,只有探索到足够多样化的状态,重标注才能发挥“变废为宝”的价值。噪声太小,轨迹全挤在起点附近,重标注出来的伪目标也都在起点附近,没意义。但也不能无限大,否则智能体满地图乱跑,轨迹和目标完全没有关联性,学习同样失效。

4.2 从训练曲线看HER是否生效

HER是否起了作用,不需要等到最终成功率统计,看训练过程中三个指标就能判断。

第一个是回放池中“伪成功样本”的比例。我在回放池里加了一个简单的统计接口,每次存入样本时记录一下新目标的reward是否为1。训练初期这个比例应该在20%到40%之间,如果太低,说明轨迹多样性不够,调整探索噪声;如果太高,重标注目标太过容易达成,学的都是简单模式,这时候要把重标注条数降低或增大任务难度。

第二个是critic的Q值估计和实际回报的偏差。HER训练中常见的一个病态现象是:Q值曲线升得很快,但真实成功率没跟上。这说明价值网络被伪成功样本带偏了,对“成功”的标准过于乐观。解决手段比较粗暴但有效:临时把重标注条数减半,让原始目标样本占比变大,把价值网络的期望拉回现实。

第三个指标是从回放池采到的样本,其“目标达成率”应该平稳上升。这里的达成率不是指真实任务的成功率,而是指采样出来的样本里,reward=1的比例。这个指标升到50%以上后,真实成功率才会有质的飞跃。

我记得有一次跑搬运实验,前5000个episode真实成功率一直是0,但采样达成率已经从0爬到了30%。当时旁边同事问这是不是没救了,我说还没,这就是HER在积累基础能力的过程,等到采样达成率突破60%,真实成功率大概率会跟着陡增。结果在8000个episode附近,真实成功率一次性跳到了70%。所以训练中途看到成功率长时间为0不用慌,关键看采样达成率有没有在涨。

5. 常见问题与排查技巧实录

HER实现起来逻辑不复杂,但真正落地时,五花八门的问题还是让我花了不少时间。这里把我在三个不同项目里遇到过的典型问题和排查思路整理出来,每个都附上了具体的观察特征和解决方案。

问题现象可能原因排查思路与解决方式
训练很久,采样达成率一直低于10%探索不够,轨迹状态多样性差调大探索噪声到0.3,检查动作空间是否被clip得过紧
真实成功率上升后突然回跌价值网络被伪成功样本过度带偏降低重标注条数,或增大原始目标样本占比
训练初期Q值就异常大奖励范围没有归一化强制把奖励除以最大距离,让Q值始终在可控范围
多目标环境(比如需要抓取+放置)失效伪目标采样太随机,没有考虑任务结构改用future策略并限制定向窗口,让伪目标从“实际路径上的状态”选取
同一套代码在A任务上有效,B任务完全无效状态和目标没有做scale归一化检查obs和goal的量纲是否一致,比如位置是0到1,速度是-1到1,需要做归一化后再拼接

再展开说几个我印象最深的排查案例。

有一次在机械臂“推方块到指定位置”的任务中,HER训练后真实成功率只能到30%,怎么调都上不去。后来我把采样的轨迹打印出来看,发现智能体学会了一个钻空子的行为:把方块推到场地边缘卡住,然后停在旁边。因为HER会用“终态附近的方块位置”作为伪目标,这种行为恰好能制造大量伪成功样本,价值网络误以为“把方块推到边缘然后停下”是个高回报动作,原目标(0.9, 0.8)反而被忽视了。这个案例让我意识到,HER不能盲目替换目标,当真实目标达成率开始明显停滞时,要立刻怀疑是不是伪样本在“教坏”策略。

另一次踩坑是在多目标任务上直接套用单目标HER,结果完全学不动。后来我看了论文里的细节,发现multigoal场景有一个关键设置:伪目标的选取需要从“可达的状态集合”中选,而不是从全部状态空间中随机选。如果某个目标区域是物理上不可达的(比如机械臂根本伸不过去),把它当成伪目标会引入大量无效学习。我的做法是额外训练一个简单的状态可达性判别器,或者在采样时只从历史轨迹里抽目标,不引入外部随机状态,这样能天然保证伪目标都是可达的。

还有一个非常基础的坑,但不能不提:observation和goal拼接之前,一定要分别做归一化。我有一次在机器人领域复现时,observation里有一维是关节力矩值,取值范围是0到100,goal是归一化的坐标0到1,拼接之后,神经网络的输入大部分维度都只在0到1之间,唯独力矩那一维数值很大,结果loss曲线震荡得厉害。把每个维度单独做归一化之后,训练稳定了很多。这类数据预处理问题在普通强化学习里也会埋雷,但HER里因为有“重标注”这个额外环节,任何维度分布异常都会被放大,所以自查顺序永远是先看输入数据分布,再看算法逻辑。

6. 一点个人体会:HER的边界与后续扩展

做一个“hindsight”项目,说实话,价值不在于这个算法本身有多惊艳,而在于它提供了一种思考问题的角度:当你面对一个看起来毫无希望的训练过程时,不妨问问自己,这条轨迹里有没有哪怕一个状态是值得当作目标的。

从实用角度来看,我后来在几个业务项目中验证过HER的迁移价值。比如一个物品分拣场景,真正的任务是“把不同形状的零件放到对应分拣槽”,直接训练完全学不动,我改造了一下HER,把“零件当前位置”当作附加目标,让智能体先学会“把零件推到任意位置”,再在这个基础上加类别条件。这本质上是用HER做课程学习的替代品,效果比手工设计中间目标稳定得多。

现在很多开源框架里已经内置了Hindsight Experience Replay的实现,比如stable-baselines3的her接口、mujoco的Fetch环境,在你动手写完整代码前,建议先去跑通这些现成例程,再回去改自己的项目。因为HER对环境的接口约定比较严格,自己从裸环境开始调,很容易在数据格式上浪费一两周时间。

最后分享一个我后期一直在用的训练小技巧:在HER的回放池之外,额外保留一个“真实目标专用小池子”,每次更新时从中额外采10%的样本,只放原始目标版本的数据。这个小改动能让智能体始终不忘记真正的任务目标,又保住了HER带来的平滑学习路径。我在多个任务上测试过,这个混合采样的收敛速度比单一HER还要快一些,推荐试一下。

返回列表