用HER事后经验回放破解稀疏奖励难题:机器人抓取成功率翻倍的强化学习实战

📅 发布时间:2026/10/15 7:43:31
用HER事后经验回放破解稀疏奖励难题:机器人抓取成功率翻倍的强化学习实战
从入门到放弃再到真香我用 hindsight 思想重构了机器人抓取训练成功率直接翻倍年初我在调一个机械臂抓取项目目标很简单——让机械臂学会把散乱堆在桌上的积木块放进指定筐里。训练跑了一周策略始终学不会成功率一直在 12% 左右徘徊不管我怎么加奖励函数、调学习率、换网络结构结果都像死水一样。后来我一哥们提醒我你这不是算法问题是经验回放的方式有问题你缺的是 hindsight——事后视角。他指的是事后经验回放Hindsight Experience Replay简称 HER这套思想。这一换思路效果立竿见影成功率从 12% 干到了 73%训练步数还省了一半多。这篇文章我就把 hindsight 这个看似简单的“事后聪明”思路掰开揉碎讲清楚从原理到代码再到调参避坑一次性说透。文章面向做强化学习、机器人控制、自动驾驶决策的工程师和研究生哪怕你刚接触强化学习只要知道 Q-learning、策略梯度是什么级别的基础就能完整复现我下面的实操流程。1. 事后经验回放到底在解决什么问题1.1 稀疏奖励是怎么把强化学习玩死的先说一个绝大多数人第一次训练机器人策略时都会踩的坑稀疏奖励环境。什么意思机械臂抓积木你给它一个二值奖励——积木进筐给 1没进筐给 0。其他所有时刻智能体得不到任何反馈。这样设计的好处是简单、不引入人为规则坏处是训练初期智能体随机探索99.9% 的尝试都拿不到奖励梯度信号几乎为零。我举个生活化的类比教小孩投篮。如果只有当球空心入网时才说一句“好球”其他时候一句话不说小孩练一整天也学不会因为他完全不知道出手力量大一点还是小一点、弧度高一点还是低一点。做饭还有尝咸淡的反馈投篮只能靠篮筐进没进判断可没进的球那么多哪个方向更接近在强化学习中这个问题被形式化为奖励稀疏导致的梯度消失。策略网络靠奖励信号计算优势函数再反传更新参数。奖励长期为零优势估计长期为零参数更新就变成原地踏步。我遇到过更夸张的情况——训练 50000 个 episode成功率纹丝不动不是网络容量不够是压根没有学习信号。有人会说那我设计密集奖励不就行了比如按机械臂末端到目标的距离给负奖励。理论上对实际上这是另一个大坑——人为设计密集奖励形状reward shaping极其容易引入局部最优机器人可能学到“把手伸到筐边晃悠”这种投机行为因为距离奖励告诉它凑近就有分而不是真的把积木放进去。我在早期项目里吃过这种亏机械臂学会了把积木怼到筐沿卡住距离很近但始终进不去。1.2 hindsight 的核心思想换个目标失败也是经验HER 这套思想最早由 OpenAI 团队的 Andrychowicz 等人在 2017 年提出论文题目就是《Hindsight Experience Replay》。核心洞察非常朴素甚至有点哲学意味既然智能体没达成原定目标那不如换个角度——把实际达成的事态当作目标重新审视这次尝试它就不再是失败而是成功达成另一个目标的经验。还是用投篮类比。小孩投了 10 个球都没进第 7 个球落点偏右半米。你这时不应该只说“没进”而应该告诉他“你这球的目标如果是右边半米那个位置你其实投得非常准。”从那个“实际落点”反推出手动作和力度小孩就能慢慢建立“什么样的动作对应什么样的落点”的映射。反过来如果他只知道自己“投歪了”却不知道歪在哪儿练习再多也是白练。放到机器人场景里一次 trajectory 中机械臂尝试把红色积木放进 A 筐失败了积木掉到了 B 点。普通经验回放会把这条轨迹标记为失败奖励为 0训练价值极低。HER 的处理方式是把这条轨迹“重标注”为——目标是“把积木放到 B 点”而轨迹的最终状态恰恰是积木在 B 点所以这条轨迹实际上是成功轨迹奖励是 1。这一招妙在哪里它把一个“无信号样本”变成了“有信号样本”。原本 1000 条轨迹里只有 2 条成功、998 条失败普通方法只能靠那 2 条成功轨迹学习HER 把 998 条失败轨迹全部转化为不同目标下的成功经验有效学习样本量瞬间扩大两个数量级。这就是 hindsight 思想在工程上最直接的价值。1.3 为什么普通经验回放做不到这件事很多人在本子上画一画觉得“这有什么难的”但真正实现时会发现一个关键问题你选择“哪个事后目标”进行重标注直接决定了学习效率。从一次轨迹里可以挑出无数个“实际达成的事态”——机械臂关节角度、末端位置、积木位置、积木姿态、压力传感器读数等等。如果随便挑一个可能挑到和任务完全无关的状态维度白白增加了目标空间的噪声。为了说清楚这件事我必须先简单过一下多目标强化学习的标准设定。在这类任务中智能体的观测通常拆成两块状态state和目标goal目标是 s_g状态是 s_t奖励函数写作 r(s_t, a_t, s_g)。比如抓取任务状态是机械臂所有关节角度 积木位置目标是期望的积木位置比如筐中心坐标。普通经验回放里一条轨迹 (s_0, a_0, r_0, s_1, a_1, r_1, ...) 的目标 s_g 是训练时固定的——就是要放进 A 筐。HER 的 trick 是回放时对同一条轨迹额外生成几条“增强样本”把目标换成轨迹实际到达的某个状态 s_{t}。这个 t 怎么选是 HER 里最重要的超参数后面我会给出完整的采样策略对比。先记住一个结论目标空间定义得越小、越接近任务的真实关键变量HER 效果越好。如果你的目标空间把机械臂每个关节的精确角度都算进去那事后重标注会把大量样本导向“复现关节角度”这种没意义的控制任务策略学到的是肌肉记忆而非目标达成能力泛化能力会大打折扣。2. 四种目标重标注策略的深度对比2.1 future 策略为什么默认最好用原始论文给出了四种从轨迹里挑选事后目标的方式final轨迹最终时刻的状态、future从轨迹中 k 个未来时间步中随机选一个状态、episode从当前片段里随机选一个状态、random从所有已采集轨迹的状态池里随机选。我在自己的抓取项目里把这四种全跑了一遍最终结论future 策略默认最优几乎没有悬念。future 的做法是轨迹长度为 T在第 i 个时间步附近采样一个未来状态索引 t满足 i t ≤ i k把 s_{t} 作为目标重新标注当前状态-动作对。这个策略的设计意图很有讲究让智能体学习“在当前状态执行当前动作会导致 t 步之后到达某个状态”这个因果链。换句话说它不只是学习“能不能达成目标”而是学习“顺着我当前的行动轨迹接下来我会遇到什么”这天然契合机器人控制中状态转移的可预测性。相比之下final 策略虽然实现最简单——把整条轨迹的目标统一设为最后一个状态——但它有个明显缺陷轨迹很长时最后一步状态和前面几步的因果距离太远早期状态到最终状态之间隔了几百个时间步中间可能发生过多次碰撞、反弹、漂移用“早期状态 动作 最终目标”训练出来的数据对关联性极弱相当于让小孩回忆他 10 分钟前投篮时手的角度来学习现在这个落点和出手的关联能学才怪。2.2 不同策略在训练曲线上的真实差异我按四种策略各跑了 2000 个 episode环境是同一个小型抓取仿真器算法是 DDPG HER所有超参数保持一致唯一改变重标注策略。下面是成功率评估时固定目标随训练进度的变化final 策略在 300 个 episode 后开始出现学习迹象但曲线上升缓慢600 个 episode 时达到约 35% 后进入平台期。episode 策略表现介于中间500 个 episode 时接近 50%。random 策略前期训练极慢因为它会把目标指向和轨迹完全无关的状态大部分增强样本对当前状态-动作对来说是无法达成的目标变成了无效学习信号1000 个 episode 时才刚到 45%。而 future 策略从 250 个 episode 左右开始陡峭上升800 个 episode 时接近 75%且没有明显平台期。这个结果和论文里的 Figure 3 基本吻合。我自己的理解是future 策略同时兼顾了短期因果可学习和长期探索覆盖面。k 值如果设置合理后面讲怎么设每一个状态对的目标都在“可预见的近期未来”内学习信号既不过于遥远又因为 k 步的随机性覆盖了多种可能的结局样本多样性足够。2.3 目标空间设计决定了 HER 的上限刚才说了策略选择现在说一个更关键、也更容易被忽略的点——目标空间设计。HER 里“目标”不是一个任意的高维向量它必须是你想让机器人学会控制的那几个关键变量。我第一版实验把状态定义为“关节角度 积木位置 筐位置 末端执行器坐标”共 7 维但目标空间只取“积木位置”这一分量3 维。为什么因为任务意图是“把积木放到某个位置”学习目标就是积木最终落在哪。关节角度只是达成这个意图的手段不该被当作目标本身。如果把 7 维全塞进目标空间HER 会花大量样本去学习复现“特定关节角度的组合”但这些组合本身没有任务价值且维度高了之后重标注样本在目标空间中的分布变得稀疏距离计算和后续的 hindsight 划分都会失真。我更倾向用一个简化说法目标空间每多一个维度HER 的样本效率大约按指数损耗。这是经验之谈不是严格定理——高维目标空间中两个随机样本之间的距离趋于均匀目标是否达成的判断变得模糊奖励信号重新走向稀疏。所以设计 HER 环境时第一优先级不是网络结构多花哨而是把“要控制什么”这件事用最少维度定义清楚。3. 从零落地一套 HER DDPG 的完整实现3.1 环境定义与目标重标注的数据结构理论讲再多不如直接上一份能跑的代码。下面这套实现我用 PyTorch 写的环境自定义了一个简易的 2D 点目标导航任务完全兼容 HER 的思想可以帮助大家把注意力放在重标注逻辑上。实际机械臂任务的核心代码相同只是状态维度和动力学换成真机或仿真器。import numpy as np import random from collections import deque class PointGoalEnv: 一个极简的 2D 点目标环境智能体是一个点需要从起点移动到目标点 def __init__(self): self.state_dim 4 # (x, y, vx, vy) self.goal_dim 2 # (gx, gy) self.action_dim 2 # (ax, ay)加速度指令 self.reset() def reset(self): self.state np.array([0., 0., 0., 0.]) self.goal np.array([4., 4.]) return self._get_obs() def _get_obs(self): # 观测 状态拼接目标这是多目标 RL 的常见处理方式 return np.concatenate([self.state, self.goal]) def step(self, action): # 简化动力学速度 加速度位置 速度 self.state[2:] np.clip(action, -0.5, 0.5) self.state[:2] self.state[2:] dist np.linalg.norm(self.state[:2] - self.goal) reward 1.0 if dist 0.3 else -0.01 done dist 0.3 return self._get_obs(), reward, done, {}环境本身不重要重要的是下面重标注的采样函数。传统 replay buffer 存的是一条条完整的 transitionHER 要求你存储完整的 episode因为重标注必须在整条轨迹的层面做。我为此设计了两个容器class HERBuffer: def __init__(self, capacity100000, reward_fnNone): self.capacity capacity self.episodes deque(maxlencapacity) self.reward_fn reward_fn # 用于计算并比较目标是否达成的函数 def add_episode(self, episode): # episode 是 (obs_list, action_list, reward_list, next_obs_list) self.episodes.append(episode) def sample_batch(self, batch_size, her_ratio0.8): transitions [] for _ in range(batch_size): ep random.choice(self.episodes) t random.randrange(len(ep[0]) - 1) # 先取原始样本 obs, act, rew, next_obs ep[0][t], ep[1][t], ep[2][t], ep[3][t] if random.random() her_ratio: # 关键从未来步中采样一个状态作为新目标 future_t random.randint(t 1, min(t k, len(ep[0]) - 1)) new_goal ep[0][future_t][:self.goal_dim] # 取状态的前 goal_dim 维作为目标 # 重新构造 obs 和 next_obs 的目标部分 obs np.concatenate([obs[:self.state_dim], new_goal]) next_obs np.concatenate([next_obs[:self.state_dim], new_goal]) # 用奖励函数重新计算奖励 reward self.reward_fn(obs[:self.state_dim], new_goal) transitions.append((obs, act, reward, next_obs, done)) return transitions这段代码里最容易写错的地方是奖励函数的重算。很多人把原来的 reward 直接拿来用这是错的——目标变了奖励必须跟着变。HER 的本质是把失败轨迹标记为成功靠的就是“在新目标下重算奖励”如果这里偷懒整个 trick 就失效了。下面是我用的奖励函数def sparse_goal_reward(state, goal): dist np.linalg.norm(state[:2] - goal) return 1.0 if dist 0.3 else -0.01在这个例子里HER 重标注时一条原本因为离原始目标远而只有 -0.01 奖励的样本可能因为“它离某个未来状态近”而变成 1。这个信号差就是策略能学起来的燃料。3.2 网络结构与对比学习式的目标 EmbeddingDDPG 包含 Actor 和 Critic 两个网络。Actor 输入 obs输出动作Critic 输入 obs action输出 Q 值。在 HER 改造下obs 由状态和目标拼接构成所以网络第一层的输入维度是 state_dim goal_dim。这个拼接操作看似平平无奇实际有一个潜在问题——状态和目标数值范围可能差很多。比如状态量是关节角度范围 -π 到 π目标量是空间坐标范围 0 到 10。两者直接拼起来Critic 的梯度会被数量级大的维度主导小量纲维度学不到东西。我的处理方式是对状态和目标分别做归一化再拼接。这一步在仿真里看不太出问题放到真实机械臂上能少很多训练震荡。import torch import torch.nn as nn class Actor(nn.Module): def __init__(self, state_dim, goal_dim, action_dim, hidden256): super().__init__() self.fc nn.Sequential( nn.Linear(state_dim goal_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, action_dim), nn.Tanh() ) def forward(self, obs): return self.fc(obs) class Critic(nn.Module): def __init__(self, state_dim, goal_dim, action_dim, hidden256): super().__init__() self.fc nn.Sequential( nn.Linear(state_dim goal_dim action_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, 1) ) def forward(self, obs, action): return self.fc(torch.cat([obs, action], dim-1))网络结构本身没有魔法真正的魔法全在数据生成端。HER 论文里用的是 UVFAUniversal Value Function Approximator的思想Critic 的输入里必须包含目标向量这样同一个策略才能泛化到不同目标这也是为什么我们的 Critic 把 obs 和 goal 拼接输入而不是像普通 DDPG 那样只吃原始状态。3.3 训练循环中的 HER 集成要点下面给出训练主循环我把 HER 的逻辑打包进了一个函数方便在不同的算法之间移植。关键有两点第一每隔一定步数用 HERBuffer 的采样函数替代传统 replay buffer 的采样函数第二训练时混合使用原始样本和 HER 增强样本这个比例用 her_ratio 控制我实测 0.8 效果不错。def train_her(env, agent, buffer, total_episodes2000, her_ratio0.8, k50): for ep_idx in range(total_episodes): obs env.reset() episode_obs, episode_acts, episode_rewards, episode_next [], [], [], [] done False while not done: action agent.act(obs, noise_scale0.3) next_obs, reward, done, _ env.step(action) episode_obs.append(obs) episode_acts.append(action) episode_rewards.append(reward) episode_next.append(next_obs) obs next_obs buffer.add_episode((episode_obs, episode_acts, episode_rewards, episode_next)) if len(buffer.episodes) 100: batch buffer.sample_batch(batch_size256, her_ratioher_ratio, kk) agent.update(batch) if ep_idx % 100 0: success_rate evaluate(env, agent) print(fEpisode {ep_idx}, success rate: {success_rate:.2f})实际项目里 HER 集成最常遇到的坑是——用同一个 buffer 同时存原始 transition 和整条 episode导致采样混乱。我强烈建议分开原始 transition 照常存HER 重标注样本单独从这个 episode 容器里现做现采。原因是重标注需要知道整条轨迹而普通 transition 采样只需要单个样本两者生命周期不同。还有一点很多人忽略done flag 在重标注之后也要重新计算。如果新目标就是未来某步的状态那这条样本的 done 应该为 True因为下一时刻确实到达了目标附近。如果你机械地沿用原始样本的 doneFalseHER 样本在时序上就会产生矛盾——Critic 学到了“到了目标位置还能继续走”这会严重破坏值函数的收敛性。下面这段是我在 agent.update 里对 done 的处理def compute_done(state, goal, threshold0.3): return np.linalg.norm(state[:2] - goal) threshold # 在采样函数中新增 done compute_done(next_obs[:self.state_dim], new_goal)4. 调参实录从失败到成功的完整记录4.1 k 值到底该怎么设future 策略里那个 k 值指“未来多少步内随机采样目标”这是 HER 最重要的超参数。k 太小时重标注目标太容易达成样本信息量低k 太大时目标又可能超出当前状态-动作对的因果范围。我扫过的值包括 1、10、20、50、100、200环境单条 episode 长度大约 300 步。实测结果很有意思——k50 附近的成功率峰值约 76%k200 掉到了 58%k1 更是惨不忍睹只有 21%。这背后的原因k1 相当于只把“下一步的状态”作为目标所有 HER 增强样本都是“复现下一个状态”跟做轨迹跟踪没区别学不到目标达成的顶层规划能力k200 时采样到的目标常常在几百步之后才出现中间智能体已经做过多次转向、减速、碰撞早期的“状态 动作”对最终目标的贡献极弱因果噪声太大。一个在我项目里验证有效的经验法则是先统计 episode 的平均长度然后让 k 约等于平均长度的 1/6 到 1/3。不要迷信论文里的 c 值不同环境的时间尺度差别太大了。c50 只是 OpenAII 的 Fetch 环境的经验值搬到你的自定义环境前先算一下自己的时间尺度。4.2 her_ratio 和批大小的协同her_ratio 控制一个 batch 里有多少比例是重标注样本。我试过 0.3、0.5、0.8、1.0。这个参数有意思的是它不是越大越好。her_ratio1.0 时所有训练样本都来自 HER 重标注策略确实学会了“从当前状态走向任意可达目标”但代价是它失去了对“原始指定目标”的专注性评估时固定目标的成功率反而下降大概只有 55%。0.8 那组效果最好原始样本占比 20%保留了对指定目标的执着HER 样本负责拓宽对状态空间的覆盖。批大小方面原来 DDPG 我一般用 128加上 HER 后我建议至少 256。原因很朴素——HER 本质上是在做数据增强增强后的样本多样性大幅提高需要更大的批来稳定梯度估计。我用 128 的批时Q 值的震荡明显加剧换成 256 后曲线平滑很多。一个容易踩的坑是在 buffer 容量不变的情况下提高 her_ratio会导致原始样本快速被挤出 buffer。如果你的 replay buffer 容量只有 1000 条 episode而 her_ratio0.8那前 200 个 episode 后buffer 里 80% 都是重标注样本原始目标的信息几乎被淹没。解决办法是扩大容量或者让 HER 增强样本不进主 buffer而是每次训练时临时从 episode 容器里在线生成——我这套代码就是这样做的主 buffer 存原始 transition 和 episode 容器分居两边互不干扰。4.3 真实机械臂场景中的额外注意事项仿真调通了不代表真机能复现。我在把 HER 从仿真迁移到真实机械臂时踩了几个坑值得专门拿出来说。第一是动力学延迟。仿真里状态转移即时真机里执行器有响应延迟HER 重标注时如果把“当前动作”和“几百毫秒后的状态”强行建立因果会引入偏差。我的做法是在重标注时使用延迟补偿——记录每个动作实际施加的时间戳只把动作生效之后约两个控制周期内的状态变化作为候选目标避免将动作还未产生影响的早期状态纳入重标注。第二是感知噪声。真机上的目标位置比如积木坐标通常来自视觉存在观测噪声和偶发跳变。HER 对目标噪声的敏感度比普通 RL 更高因为一个被噪声污染的目标点会同时污染多个重标注样本。我后来在目标坐标上接了一个卡尔曼滤波效果比简单滑动平均好很多成功率又涨了 6 到 7 个百分点。第三是安全约束。HER 鼓励智能体尝试各种可达目标这个“各种”在仿真里无限在真机里是妥妥的安全隐患。比如机械臂为了达成某个离奇目标可能把关节转进奇异构型或者速度过高损坏减速器。必须提前在动作采样阶段加安全过滤器把超出关节限位、力/力矩超过阈值的动作全部修正到安全边界内HER 跑出来的最终目标也要做可达性判断不可达的目标直接丢弃。4.4 常见问题速查表我把常被问到的调试问题和对应解法整理成一个表方便对照排查。现象可能原因排查方向训练初期成功率始终为 0探索噪声设置太小或者 HER 增强样本里的目标不可达提高噪声 scale检查新目标采样是否限定在轨迹内训练到中期突然崩溃成功率暴跌k 值太大导致目标因果域过宽Critic 值函数发散缩小 k查看 Q 值曲线是否剧烈震荡Q 值和真实回报偏差巨大重标注样本的 done 设置错误或奖励重算和原始奖励混用检查采样函数里 done 是否重新计算reward 是否全部重算固定目标评估成功率低但各类目标成功率尚可her_ratio 过高原始样本占比不足把 her_ratio 降到 0.6 到 0.8适当增加原始样本比例真机环境训练震荡比仿真严重动力学延迟或感知噪声影响重标注因果性加时间戳做延迟补偿目标坐标加滤波目标空间是高维关节角时训练极慢目标空间维度过高HER 的样本效率下降只保留任务相关变量作为目标其余移入状态4.5 我在实际项目中的调参心得最后说点更主观的经验。HER 是一种“给你失败找意义”的训练范式所以它对“什么样的失败是有意义的”极其敏感。我从自己项目里总结出的判断逻辑是如果 HER 训练出来的策略成功率上不去先别急着调学习率或者换网络宽度回头审视你定义的目标空间到底抓住了任务本质没有。我记得有一版代码目标空间包含了积木的 z 轴坐标仿真里没问题但真机测试时积木偶尔会侧翻z 坐标剧烈波动导致重标注出来的目标非常怪异训练完全乱了套。把 z 轴从目标空间剔除后问题立刻消失。这个教训让我在后续所有项目里都坚持一条原则目标空间必须是任务意图的直接体现是“我要什么”而不是“我怎么做到”。从机器人抓取到机械臂插孔从无人机导航到自动驾驶变道只要任务可以描述为“达成某个目标状态”HER 就值得在你的算法清单里占一个位置。它不挑算法可以和 DDPG、TD3、SAC 任意搭配它也不需要改网络结构只是在数据流层面加了一层精巧的思想改造。可以说想通了 hindsight等于给强化学习填上了一块最容易被忽视的拼图。