从入门到放弃再到真香:我用 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 存的是一条条完整的 transition,HER 要求你存储完整的 episode,因为重标注必须在整条轨迹的层面做。我为此设计了两个容器:
class HERBuffer: def __init__(self, capacity=100000, reward_fn=None): self.capacity = capacity self.episodes = deque(maxlen=capacity) 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_ratio=0.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 网络结构与对比学习式的目标 Embedding
DDPG 包含 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, hidden=256): 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, hidden=256): 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 论文里用的是 UVFA(Universal 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_episodes=2000, her_ratio=0.8, k=50): 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_scale=0.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_size=256, her_ratio=her_ratio, k=k) agent.update(batch) if ep_idx % 100 == 0: success_rate = evaluate(env, agent) print(f"Episode {ep_idx}, success rate: {success_rate:.2f}")实际项目里 HER 集成最常遇到的坑是——用同一个 buffer 同时存原始 transition 和整条 episode,导致采样混乱。我强烈建议分开:原始 transition 照常存,HER 重标注样本单独从这个 episode 容器里现做现采。原因是重标注需要知道整条轨迹,而普通 transition 采样只需要单个样本,两者生命周期不同。
还有一点很多人忽略:done flag 在重标注之后也要重新计算。如果新目标就是未来某步的状态,那这条样本的 done 应该为 True(因为下一时刻确实到达了目标附近)。如果你机械地沿用原始样本的 done=False,HER 样本在时序上就会产生矛盾——Critic 学到了“到了目标位置还能继续走”,这会严重破坏值函数的收敛性。下面这段是我在 agent.update 里对 done 的处理:
def compute_done(state, goal, threshold=0.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 步。
实测结果很有意思——k=50 附近的成功率峰值约 76%,k=200 掉到了 58%,k=1 更是惨不忍睹,只有 21%。这背后的原因:k=1 相当于只把“下一步的状态”作为目标,所有 HER 增强样本都是“复现下一个状态”,跟做轨迹跟踪没区别,学不到目标达成的顶层规划能力;k=200 时,采样到的目标常常在几百步之后才出现,中间智能体已经做过多次转向、减速、碰撞,早期的“状态 + 动作”对最终目标的贡献极弱,因果噪声太大。
一个在我项目里验证有效的经验法则是:先统计 episode 的平均长度,然后让 k 约等于平均长度的 1/6 到 1/3。不要迷信论文里的 c 值,不同环境的时间尺度差别太大了。c=50 只是 OpenAII 的 Fetch 环境的经验值,搬到你的自定义环境前先算一下自己的时间尺度。
4.2 her_ratio 和批大小的协同
her_ratio 控制一个 batch 里有多少比例是重标注样本。我试过 0.3、0.5、0.8、1.0。这个参数有意思的是,它不是越大越好。her_ratio=1.0 时,所有训练样本都来自 HER 重标注,策略确实学会了“从当前状态走向任意可达目标”,但代价是它失去了对“原始指定目标”的专注性,评估时固定目标的成功率反而下降,大概只有 55%。0.8 那组效果最好,原始样本占比 20%,保留了对指定目标的执着,HER 样本负责拓宽对状态空间的覆盖。
批大小方面,原来 DDPG 我一般用 128,加上 HER 后我建议至少 256。原因很朴素——HER 本质上是在做数据增强,增强后的样本多样性大幅提高,需要更大的批来稳定梯度估计。我用 128 的批时,Q 值的震荡明显加剧,换成 256 后曲线平滑很多。
一个容易踩的坑是,在 buffer 容量不变的情况下提高 her_ratio,会导致原始样本快速被挤出 buffer。如果你的 replay buffer 容量只有 1000 条 episode,而 her_ratio=0.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,等于给强化学习填上了一块最容易被忽视的拼图。