ARTICLE DETAIL

资讯详情

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

稀疏奖励难收敛?HER后见经验回放实战详解与避坑指南

稀疏奖励难收敛?HER后见经验回放实战详解与避坑指南 这阵子我一直在做目标条件强化学习相关的东西最让我觉得“早知道就好了”的反而是hindsight这个名字本身对应的算法——Hindsight Experience Replay后见经验回放。我当时的场景很典型一个机械臂要把物体推到指定位置奖励是稀松的 0/-1训练跑了大半天成功率还是 0。后来我只改了数据回放环节把“失败轨迹”重新标记成“另一种成功”训练信号立刻就有了。这篇文章想把整个思路、实现细节和踩过的坑完整讲清楚适合正在做稀疏奖励任务、听说过 HER 但没真正跑通的人也适合想把手头多目标任务救活的同学。1. 先从名字说起hindsight 是把“事后聪明”变成训练数据1.1 一个“试了很多次都失败”的典型场景假设我们有一个二维点机器人当前位置是(0.2, 0.3)目标是(0.8, 0.9)每一步只能小幅移动奖励只有两种距离小于 0.05 给 0否则给 -1。这种环境叫做“稀疏奖励”sparse reward。随机尝试时点机器人可能跑遍整个地图但绝大多数 episode 的每一步都拿 -1没有任何一个正信号可以用来更新策略。更关键的是这个过程是“无语言”的就算机器人已经经历了“从左边绕到目标附近”如果那一刻刚好差了一点整个轨迹也会被丢进垃圾桶。强化学习靠的是试错后得到的奖励反向传播没有奖励梯度就是 0网络自然一动不动。很多人第一时间会想到“那我手动设计一个距离奖励越近越好”。这确实有效但它是奖励塑形reward shaping要依赖工程师对任务的先验理解而且塑形不当很容易让智能体学会“在原地打转”或者“只接近但不触碰”这类投机行为。HER 的思路比这优雅得多也简单得多。1.2 核心动作重新标记目标HER 的直觉非常朴素一次 episode 没有到达你想要的g但它最终到达了某个别的状态g。那么你可以在回放时这样问自己如果目标本来就是g这条轨迹是不是就成功了机械臂推球没推到左边目标但把球推到了右边某个位置。站在“左边目标”的视角这次尝试完全是失败的但站在“右边那个位置”的视角机械臂的动作链其实非常漂亮最后的到达距离是 0。HER 会把这个轨迹额外存一份并把desired_goal从左边目标改成右边最终位置然后把奖励重新计算成 0放回回放缓冲区。这样一来原本没有任何正样本的缓冲区里出现了大量“我达成了某个目标”的数据。策略网络不再只收到清一色的 -1而是能学到“这类动作组合是有用的”。这比单纯鼓励探索聪明得多因为它不是瞎猜而是从真实轨迹里挖掘“潜在的意图”。1.3 这并不是改奖励函数一定要理解清楚HER 没有修改环境的奖励函数也没有修改 agent 在真实环境中交互时使用的目标。它只改了回放缓冲区里一部分样本的“查询目标”。真实交互时目标还是用户指定的那个回放学习时我们额外造了一些“如果目标不同这次转移就是好样本”的假数据。所以它本质上是一种数据增强而不是任务层面的奖励工程。这里有一个容易混淆的操作。原始的(s, a, r, s)四元组通常不足以做目标重标记因为你不知道s中包含的“目标”是什么。HER 要求环境能返回两样东西当前观测observation和当前“已达成状态”achieved_goal。前者是网络输入后者用于判定目标是否达成和计算新奖励。在 Gym Robotics 系列环境里观测通常是一个字典observation、desired_goal、achieved_goal。自己造环境时也建议照这个结构来后面改代码会省很多事。2. 稀疏奖励为什么难搞在动手前把原理吃透2.1 稀疏奖励的难度不是“慢一点”而是“没信号”很多人误以为稀疏奖励只是训练慢多给点随机动作就能碰运气。在低维动作空间里比如 1D 爬山问题确实可以靠随机探索碰出来。但一旦动作维度超过 5目标又是一个高维连续向量随机命中的概率几乎就是天文数字。你在一百万步里可能一次正奖励都见不到策略梯度算出来的期望奖励始终是负的网络学不到任何有价值的梯度方向。我们可以简单算一下假设动作空间是 20 维每一步动作的精度要求是 0.1那每一步精确落到“好动作区域”的概率大约是0.1^20整个 episode 要连续走对几十步这个概率已经小到没有意义。所以探索噪声增加再多也只是让机器人动作更抖并不会带来有效的正样本。2.2 奖励塑形不是万能的奖励塑形确实能让信号连续化但它需要你定义“距离”和“进度”。在机械臂推球任务里距离目标越近给越大奖励看起来合理但实际训练时会发现策略倾向于“把手伸过去但不敢碰”或者“绕着目标走一圈”因为中间状态的奖励很高最后一下的“事件奖励”反而被淹没。要压住这些副作用你得反复调整塑形系数有时候一个任务要花两周调奖励换一个任务又全部推翻。HER 的价值在于它几乎不需要任务先验。你只需要能回答一个问题给定一个目标和一个已达成状态它们是否足够接近这个判断通常用阈值或者距离就能完成剩下的全部交给算法自己。2.3 relabel 为什么不会破坏 off-policy 训练这是新手最容易想不通的地方回放数据里的动作是在“原目标”下生成的现在把目标换成另一个这数据还能用吗关键要抓住一点在目标条件 MDP 中环境动力学通常不依赖目标。状态转移s - s只由当前状态和动作决定目标g只影响奖励函数。也就是说(s, a, s)这个转移在物理上是真实发生过的无论你把它当成“朝目标 A 走的一步”还是“朝目标 B 走的一步”它都是一条合法的状态转移样本。HER 做的只是把同一个(s, a, s)配上新的目标g然后重新算一个奖励r R(s, a, s, g)把这个五元组(s, a, r, s, g)存进缓冲区。对 off-policy 算法来说只要经验来自任意行为策略且当前策略能从这个转移里学习它就是可用的。DDPG、TD3、DQN 这类算法天生不需要当前策略 rollout 的数据所以 HER 和它们是完美的搭档。这里有个细节值得说relabel 后数据确实不是当前目标导向下的 on-policy 数据但这恰恰是 off-policy 算法存在的意义。你不需要重新收集数据只需要在目标维度上做“虚拟采样”这相当于给每个真实的物理转移多打了几个不同的“标签”。2.4 和 goal-conditioned 策略的关系HER 不是单独的策略算法它必须配合一个能接收“目标”的策略网络。你的网络输入不能只是状态s要把状态和目标拼在一起输入。这样当 relabel 成新目标时网络才能对同一个状态给出不同的动作响应。所以动手实现前请先确认你的策略网络是“目标条件策略”输入(s, g)输出动作a。如果不是那 HER 就无从谈起。3. 最小可复现的 HER 实现一个二维点到达任务理论说多了容易飘不如直接上代码。我用一个纯 NumPy 写的二维点到达环境不用装 MuJoCo也不需要显卡只为了把 HER 的核心逻辑跑明白。3.1 一个简单的 PointGoal 环境环境设计如下状态是二维坐标动作是目标速度每个分量限制在[-0.1, 0.1]每次执行动作后叠加一点高斯噪声奖励函数用阈值 0.05 判断成功成功给 0失败给 -1一个 episode 最多 50 步。import numpy as np class PointGoalEnv: def __init__(self, seed0): self.rng np.random.default_rng(seed) self.state None self.goal None self.threshold 0.05 self.max_steps 50 def reset(self): self.state self.rng.uniform(0.1, 0.9, size2) self.goal self.rng.uniform(0.1, 0.9, size2) self.t 0 return self._obs() def _obs(self): return { observation: self.state.copy(), desired_goal: self.goal.copy(), achieved_goal: self.state.copy(), } def step(self, action): action np.clip(action, -0.1, 0.1) noise self.rng.normal(0, 0.02, size2) self.state np.clip(self.state action noise, 0.0, 1.0) self.t 1 dist np.linalg.norm(self.state - self.goal) reward 0.0 if dist self.threshold else -1.0 done False # 这里不提前结束保证 episode 足够长 if self.t self.max_steps: done True return self._obs(), reward, done, {is_success: dist self.threshold}注意我刻意把done设置成只在超过最大步数时为 True不在到达目标时提前结束。这样做是为了让 relabel 时有足够的“未来状态”可以选。如果你让环境在成功时立刻结束很多轨迹会在到达目标处被截断future 采样会少一块数据。3.2 带 future 目标采样的回放缓冲区HER 最核心的代码就是add_episode这一步。我习惯把完整的 episode 存下来等它结束后统一处理。对每个时间步t除了把原始转移存进缓冲区再额外生成replay_k条 relabel 数据。from collections import deque class HindsightBuffer: def __init__(self, capacity100000, replay_k4, threshold0.05): self.buffer deque(maxlencapacity) self.replay_k replay_k self.threshold threshold staticmethod def _rew(achieved, goal, threshold): return 0.0 if np.linalg.norm(achieved - goal) threshold else -1.0 def arg_append(self, transition): self.buffer.append(transition) def add_episode(self, episode): # episode 里的每个元素是 (s, a, r, s_next, achieved_next) T len(episode) for t, (s, a, r, s_next, achieved_next) in enumerate(episode): # 原始经验 self.arg_append((s, a, r, s_next)) # HER 重标记经验 for _ in range(self.replay_k): if t 1 T: break future_idx np.random.randint(t 1, T) new_goal episode[future_idx][4] # 未来某个时间步的 achieved_goal new_r self._rew(achieved_next, new_goal, self.threshold) # 状态里包含原始目标重标记时要一起替换 s_relabel np.concatenate([s[:2], new_goal]) s_next_relabel np.concatenate([s_next[:2], new_goal]) self.arg_append((s_relabel, a, new_r, s_next_relabel)) def sample(self, batch_size): idxs np.random.choice(len(self.buffer), batch_size, replaceFalse) batch [self.buffer[i] for i in idxs] s np.array([x[0] for x in batch], dtypenp.float32) a np.array([x[1] for x in batch], dtypenp.float32) r np.array([x[2] for x in batch], dtypenp.float32).reshape(-1, 1) s_next np.array([x[3] for x in batch], dtypenp.float32) return s, a, r, s_next这里的episode列表结构需要预先约定每个元素是(s, a, r, s_next, achieved_next)。s和s_next是(4,)的向量前两维是当前位置后两维是当前目标achieved_next是(2,)的向量表示转移后真正到达的位置。原始经验不需要单独保留achieved_next因为对原目标来说s_next里就带着原目标。3.3 DDPG 网络与更新逻辑HER 自己不会训练策略下面用最简 DDPG 来当回放算法。Actor 输入拼接后的(位置, 目标)输出[-0.1, 0.1]范围的动作Critic 输入(拼接状态, 动作)输出 Q 值。import torch import torch.nn as nn import torch.nn.functional as F class Actor(nn.Module): def __init__(self, obs_dim, act_dim): super().__init__() self.net nn.Sequential( nn.Linear(obs_dim, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU(), nn.Linear(256, act_dim), nn.Tanh(), ) def forward(self, s): return self.net(s) * 0.1 # 动作范围 [-0.1, 0.1] class Critic(nn.Module): def __init__(self, obs_dim, act_dim): super().__init__() self.net nn.Sequential( nn.Linear(obs_dim act_dim, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU(), nn.Linear(256, 1), ) def forward(self, s, a): return self.net(torch.cat([s, a], dim-1))训练更新采用标准 DDPG 目标网络加 polyak 软更新gamma 0.98 tau 0.05 actor_opt torch.optim.Adam(actor.parameters(), lr1e-3) critic_opt torch.optim.Adam(critic.parameters(), lr1e-3) def update(s, a, r, s_next): with torch.no_grad(): a_next target_actor(s_next) q_next target_critic(s_next, a_next) y r gamma * q_next q critic(s, a) critic_loss F.mse_loss(q, y) critic_opt.zero_grad() critic_loss.backward() critic_opt.step() actor_loss -critic(s, actor(s)).mean() actor_opt.zero_grad() actor_loss.backward() actor_opt.step() for tp, p in zip(target_actor.parameters(), actor.parameters()): tp.data.copy_(tau * p.data (1 - tau) * tp.data) for tp, p in zip(target_critic.parameters(), critic.parameters()): tp.data.copy_(tau * p.data (1 - tau) * tp.data)完整的训练循环就是“跑一个 episode - 存入 episode 列表 - 结束后 add_episode - 从 buffer 采样并 update”。二维点任务上几百个 episode 之后成功率就能稳定超过一半这个规模完全可以在笔记本上跑完。3.4 采样策略对比final、future、episode、random如果你看过 HER 原论文会知道里面有四种重标记策略。我用一个表把它们的差别整理一下策略新目标从哪来直观效果final当前 episode 最后一步的 achieved_goal实现最简单但所有转移都指向同一个最终点目标多样性差future当前时间步之后随机挑一个 achieved_goal目标随时间变化和状态有天然的因果关系通常效果最好episode当前 episode 任意时间步随机挑一个 achieved_goal比 final 多样性好但可能出现目标在时间上提前逻辑略怪random所有历史 episode 里随机挑一个 achieved_goal数据来源广但目标可能和当前轨迹完全无关方差大我自己的经验是不要过度设计直接用 future replay_k4绝大多数任务都够用了。只有在 episode 很长、future 目标之间的差异太小时才会考虑把replay_k降到 2 并配合 final 一起用。4. 踩坑复盘为什么网上一堆 HER 实现你的成功率还是上不去这部分我想重点讲讲自己调试时踩过的坑。HER 看起来只有二十行代码能顺利跑通的人却不多问题往往出在很隐蔽的地方。4.1 重标记时忘了改状态里的 desired_goal这是最常见的坑。很多人只把r换成新奖励但s和s_next里还留着原始目标。批评网络输入是(s, a)如果s里含有原始目标而奖励是按新目标算的那神经网络学到的就是“同一组特征对应两个不一致的标签”直接导致 Q 值震荡。所以在add_episode里我特意写了s_relabel np.concatenate([s[:2], new_goal])。只要你的状态表示是“物理状态 目标”重标记时必须整体替换目标部分。这一点比奖励重算更关键因为漏了他训练过程连“自洽”都做不到。4.2 future 采样越界或者选到了 t 之前的时刻HER 强调要用“未来”的目标不是随便选一个目标。如果你选到当前时间步之前的状态当作新目标就会出现“物理状态在前面目标在后面”这种时间倒挂的数据等于给网络喂了一个不可能成立的转移序列。我调试时使用np.random.randint(t 1, T)而不是np.random.randint(0, T)就是要保证future_idx严格大于t。另外最后一个时间步已经没有未来t 1 T时直接跳过重标记而不是报错或返回空。4.3 DDPG 本身的超参数太敏感HER 对回放算法没有魔法加成DDPG 不稳定时HER 一样不稳定。我见过很多人把问题归到 HER 头上其实 DDPG 的 Q 值过高估计已经把整个训练搞废了。如果你发现训练早期成功率有一点上升然后突然断崖下跌优先怀疑 DDPG 的 target network 更新速度。我的经验是tau0.05附近比较稳tau0.001会让目标网络长期滞后Q 值容易飘。另外 Actor 和 Critic 的学习率最好都用1e-3不要 Critic 调到1e-2那样批评网络一学就过拟合到当前 batch整个训练变成原地打转。4.4 奖励函数用 dense reward 反而丢了 HER 的优势有些教程会顺手用reward -distance再叠加 HER。这样不是不行但会让 HER 的效果不明显因为 dense 信号本身已经能训练了重标记提供的额外样本只是锦上添花。HER 的优势恰恰在 sparse reward 场景下才体现出来。你用 0/-1 就好不要担心“负奖励太多”或者“0 奖励太少”。奖励缩放对 DDPG 来说很容易适应重要的是转移的时序一致性不是单步奖励的大小。如果你想让信号更平滑可以考虑给成功阈值的边缘加一点点渐变但不要回到纯 distance reward。4.5 评估时用错指标训练过程中reward 会缓慢上升但上升不代表策略好。稀疏奖励下的 reward 和成功率不是一回事一个 episode 里最后 5 步碰巧成功但前面 45 步都在乱晃reward 也可能被拉高。真正要看的指标是“评估时无噪声策略的成功率”也就是每 100 个 episode 里不叠加动作噪声、直接用 actor 输出最后一步距离是否小于阈值。我自己的训练脚本里会单独写一个evaluate()函数跑 50 个固定种子的 episode统计info[is_success]的均值。HER 有没有生效看这个曲线最直观。4.6 一个小型问题排查表现象常见原因解决方向原目标记录的奖励和新奖励混合s里的目标没整体替换重标记时替换s和s_next的目标部分future 采样后训练反而变差选到了当前时间步之前的目标使用randint(t1, T)Q 值爆炸DDPG 目标网络更新太快/太慢调整tau或换 TD3成功率卡在 0 很久环境 reset 的初态和目标范围太极端先缩小初始分布验证通过后再扩大训练曲线过拟合单 batch学习率太高batch 太小降低 lrbatch 保持 128 以上5. 从 hindsight 到更多我学到的三件事5.1 后见之明本质是数据增强HER 最打动我的地方在于它把“失败”这种被大多数算法扔掉的数据变成了一种可控的监督来源。站在原目标视角这次尝试毫无价值站在实际达成状态的视角它是一段完美的成功演示。这种“重新解读数据”的思维方式比任何奖励塑形都更接近人类的学习方式——人复盘时也经常说“虽然没达到 A但我意外把 B 搞定了。”所以后来我做别的目标条件任务时第一反应不是调奖励而是想能不能重标记只要能定义achieved_goal这招就能用。5.2 不是所有任务都能 relabel有两个硬前提必须满足第一目标必须是可验证的能通过观测直接判断达成与否第二转移动力学最好与目标无关。语言生成任务就很尴尬你生成了一句回答但“目标”是语义层面的无法用简单的距离判定更没法随便换一个“已达成目标”。图像生成也是同理像素空间里的目标重标记没有语义意义。如果你想把 HER 用到 NLP 或推荐领域不要直接套要先找到可计算的目标度量和可重标记的目标空间。否则会导致整个训练信号变成随机噪声。5.3 HER 喜欢和多样化探索配合HER 可以制造“成功的幻觉”但这个幻觉质量取决于原始轨迹里是否真的出现过丰富多样的状态。如果 agent 永远只在某个角落打转relabel 出来的目标也全是那个角落附近策略只会过度优化局部区域。所以我很建议在 HER 外面加一层探索策略比如简单的 action noise 衰减或者更先进的 count-based exploration。HER 负责把探索到的轨道“物尽其用”探索本身还是要靠你。我现在的习惯是遇到稀疏奖励的目标条件任务先看一眼能不能做目标重标记能就直接上一套 HER off-policy 算法再加一个固定种子的 evaluate 函数训练前先在小 toy 环境上验证 20 分钟再放到完整任务里。这套流程帮我省下了大量调奖励的时间也希望你能用得上。
返回列表