ARTICLE DETAIL

资讯详情

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

HER事后经验回放:如何用失败轨迹破解稀疏奖励难题

HER事后经验回放:如何用失败轨迹破解稀疏奖励难题 做机器人控制或者稀疏奖励RL实验的同学大概率都经历过这种尴尬一个回合跑完环境只给你一个reward等于0的信号算法梯度压根不知道该往哪走。你甚至怀疑是代码bug改了半天发现不是bug——是奖励太稀疏随机探索根本碰不到目标。我自己在复现Hindsight Experience Replay事后经验回放最常见的简称是HER圈子里的口头叫法干脆就叫“hindsight”这个方法之前也在这个问题上耗掉过不少训练机时。这篇文章想说的就是这套方法到底怎么把“失败轨迹”变成“成功教材”的以及我在复现、调参和落地过程中踩过哪些坑。hindsight的中文直译是“事后聪明”民间说法就是“事后诸葛亮”。放到强化学习领域它的思路非常反直觉一条没有完成任务的轨迹按理说是废数据但我们偏偏能通过改目标的方式让它变成有效数据。关键不在于让算法学会“预测未来”而在于让它学会“复盘过去”。这篇文章适合正在做机器人操作任务、处理稀疏奖励问题或者想研究目标条件策略goal-conditioned policy的读者。无论你是刚入门RL的学生还是在工程里被稀疏奖励折磨的算法工程师这套思路都值得你花一个下午彻底搞懂。1. 为什么稀疏奖励是强化学习的“隐形杀手”而hindsight能对症下药先说清楚我们到底在解决什么问题不然你会觉得HER只是个花哨的trick。强化学习的经典设定里奖励函数是明确的走一步给一点奖励完成任务给一个大奖励。但在真实世界里任务往往不是这样。机器人想把一块积木推到一个指定位置积木没到位你就不知道该怎么评价这一步推荐系统想让用户点击用户没点你就不知道哪个环节出了问题。这种环境下奖励就是稀疏的大多数情况下是0偶尔出现一次1或者-1。数学上看问题出在探索的成功率上。假设某个动作维度是连续的目标区域只占状态空间的1%随机策略每回合成功的概率就是0.01而一次episode可能有几百步。想让随机策略碰巧踩到成功样本概率低到接近零。没有正样本价值函数学到的全是“做什么都没用”的平底状态策略自然也就一直摆烂。我最早接触这个问题是在一个机械臂推小球的仿真环境里目标点固定奖励定义为小球是否进入目标区域进了给1没进给0。结果训练了20万步成功率始终是0。我当时以为是环境接口写错了手动跑了一百个随机episode发现成功率本身就只有千分之几。这不是bug而是奖励函数设计导致的学习瓶颈。常规的解决方案有几条路。第一条是奖励塑形把稀疏奖励改造成连续奖励比如每一步根据与目标的距离给一个负的惩罚项。听起来简单但塑形项设计不好策略很容易钻空子机器人可能学会原地转圈刷负值或者找到一个你没想到的漏洞行为。第二条路是课程学习从简单任务开始逐步提高难度。这个思路没问题难的是要手动画任务难度而且还得保证低难度任务学到的技能能迁移到高难度任务。第三条路是模仿学习直接用专家演示数据初始化策略。但它最大的代价是需要标注好的专家数据在机器人场景里这恰恰是昂贵的部分。HER的优势在于它不需要额外的先验知识只需要你明确“目标”和“实际达成状态”的差异。它用来学习的不是成功数据而是大量被传统方法扔掉的失败数据。换个新目标那些失败的episode就变成了成功episode样本利用率瞬间上去了。这是它作为事后经验回放的核心价值。2. 核心机制拆解目标重标记是怎么把失败变成养分的HER的思路听起来像是一句废话如果这次没到达目标A那就把目标改成它实际到达的位置B当它成功。但真正工程化落地时细节比较多。我们逐个拆开说。2.1 从单目标到目标条件把任务形式彻底换掉要实现HER你首先得把问题建模成“目标条件”形式。也就是说策略的输入除了当前状态还要包含一个目标。用符号表达就是策略π接受状态s和目标g输出动作a即a π(s, g)。这里的g可以是机械臂末端的目标位置、棋盘上某个格子的坐标、分子优化的某个分子式总之它可以是一段向量。为什么必须这样因为重标记之后同一个状态会配上不同的目标。如果策略只吃状态不吃目标那改目标就无从谈起。这也是很多人复现HER时第一处疑惑的来源明明自己的环境输入只有状态硬改成目标条件结构后网络收敛反而变慢了。正常因为你不是在修bug是在改任务建模方式。在OpenAI Gym Robotics这类标准环境里观测空间是一个字典包含三个关键字段observation当前状态、achieved_goal当前实际达成的目标、desired_goal期望达成的目标。这三者缺一不可。observation可以理解为机械臂每个关节的角度、速度等运动学信息achieved_goal是机械臂末端当前位置desired_goal是我们要推到的位置。HER的训练过程本质上就是不断改造desired_goal这个字段。2.2 重标记的具体操作流程假设我们采样了一条episode包含T个时间步每个时间步是一个四元组(s_t, a_t, s_{t1}, r_t)。原始奖励是这样计算的如果s_{t1}对应的achieved_goal和desired_goal距离小于阈值奖励为0表示完成否则为-1。用这种稀疏二值奖励绝大多数episode的奖励序列都是-1、-1、-1……没任何信息量。HER的干预方式是取这条episode后续某个时间步tt ≥ t把s_{t}里记录的achieved_goal拿出来作为一个新的目标g。换句话说本次动作虽然没能把物体推到原定目标但在某个未来时刻物体确实到达了某个地方那就当“目标本来就是那里”。然后基于新目标重新计算t时刻的奖励r_t。因为新目标就是实际到达的位置这个奖励大概率不再是-1而是0。放到数学层面原始转移τ (s_t, a_t, r_t, s_{t1}, g)重标记后τ (s_t, a_t, rt, s{t1}, g)其中g achieved_goal 在某个后续时间步u上的值u ≥ trt reward_function(s{t1}, g)当g等于实际到达位置时r_t 0关键在于重标记不是只做一次。原始论文里每个转移通常会额外生成k个重标记目标。如果你设置k4意味着每个原始转移会变成5条训练样本1条原始样本 4条重标记样本。这样做相当于把一个episode的数据量放大了k1倍而且每条样本都携带了明确的奖励信号。2.3 重标记目标不是随便取的未来策略与最终策略重标记时新目标从哪里采样直接影响训练效果。最朴素的想法是从“最终状态”采样就是拿这条episode的最后一个achieved_goal当作新目标。好处是实现简单缺点是如果episode很长中间很多步都对应同一个目标信息缺乏多样性而且可能把“中途路过但最终未能保持”的状态也当成目标。更常用的做法是“未来策略”对于第t个时间步在时间区间[t, T-1]里均匀随机抽取一个未来状态把它的achieved_goal当作新目标。这样做有两个好处。一是每个时间步都能得到不同的目标样本多样性更高二是它尊重时间因果——未来时刻的状态是由当前及之后的动作产生的作为当前动作的结果更合理。反过来如果从过去或全轨迹随机采样新目标会出现“用过去状态评估现在动作”的荒谬情况。过去的物体位置和当前动作没有因果关系网络会学到奇怪的映射关系。所以复现HER时请务必使用未来策略至少把默认值改成未来采样。这是我从实验里得到的第一个原则。2.4 需要一套目标条件的价值函数和策略重标记只是数据层的操作真正的学习要靠目标条件的Q网络或策略网络。在标准实现里我们训练的是Q(s, g, a)输出在状态s和目标g下执行动作a的期望回报。Actor输出a π(s, g)。批评这里有个理解偏差如果网络不把目标作为输入那你改目标就只是改个label网络根本不知道目标变了。所以底层算法通常选择DDPG或TD3这类适应性强的连续控制算法。我自己用的TD3做基底因为它对超参数更鲁棒critic过估计的问题更少。当然原始论文用的是DDPG复现效果也不差但工程上我更推荐TD3尤其是环境噪声大的时候。这是可复现的经验。3. 实操用PyTorch实现一个可跑的HER训练流程理论说再多不如直接看代码。下面这套流程是我复现HER时整理出来的环境以FetchReach这类机器人操作任务为例。你不用全部复制但核心的数据结构和重标记逻辑可以直接参考。3.1 环境接口先保证这三个字段对齐使用gym的robotics系列环境比如FetchReach-v1时环境返回的观测是一个字典obs env.reset() # obs.keys() 包含: # observation - 机械臂运动学状态 # achieved_goal - 当前机械臂末端位置 # desired_goal - 任务目标位置这个字典结构一定要保留因为我们重标记时只能替换desired_goal字段observation和achieved_goal要保持原样。如果你的自定义环境没有区分清晰建议先改造环境把状态划分为运动状态子空间和目标子空间。如果这两个子空间混在一个向量里重标记就是一场灾难。奖励函数建议单独抽出来方便重标记时反复调用。比如Fetch环境是这么算的def compute_reward(achieved_goal, desired_goal): distance np.linalg.norm(achieved_goal - desired_goal, axis-1) return -(distance 0.05).astype(np.float32)注意偏好负奖励-1而不是固定0/1的读者可能会困惑。这里确实是用-1/0表达二值化稀疏奖励只是完成时是0未完成时是-1。在DDPG这类最大化累积奖励的算法里-1/0的设定能提供“活下去并完成”的压力比0/1设定收敛得更稳定。这个细节我从多个实验里确认过原始论文也用的是-1/0。3.2 按episode存储的重放缓冲区HER的重放缓冲区和普通回放稍有不同它不能只存单个转移因为重标记需要一个完整的episode作为上下文。所以缓冲区里存的是episode由一条一条转移拼接而成每个转移带上该时刻的achieved_goal。我参考这种做法缓冲区容量直接以episode为单位再用一个指针实现环形覆盖。采样时从缓冲区里随机取一条episode然后对episode内的转移做批量重标记。import numpy as np from collections import deque class ReplayBuffer: def __init__(self, capacity, k4, future_prob0.5): self.capacity capacity self.k k self.future_prob future_prob self.buffer deque(maxlencapacity) def store_episode(self, episode): # episode 是字典列表: # [{obs:..., act:..., next_obs:..., # achieved_goal:..., desired_goal:..., r:...}, ...] self.buffer.append(episode) def sample_her_batch(self, batch_size): episodes np.random.choice(len(self.buffer), batch_size, replaceTrue) batch [] for idx in episodes: ep self.buffer[idx] # 每个episode里随机取一个转移作为锚点 t np.random.randint(len(ep)) base ep[t] # 原始样本保留 batch.append(base) # 额外生成k个重标记样本 for _ in range(self.k): if np.random.uniform() self.future_prob: # 未来策略: 从t之后的时刻采样新目标 future_idx np.random.randint(t, len(ep)) new_goal ep[future_idx][achieved_goal] else: # 最终状态作为新目标 new_goal ep[-1][achieved_goal] batch.append({ obs: base[obs], act: base[act], next_obs: base[next_obs], desired_goal: new_goal, r: compute_reward(base[next_obs][achieved_goal], new_goal) }) return batch这里提醒两点。第一new_goal的维度必须与desired_goal完全一致如果原始目标是三维坐标那重标记目标也是三维坐标。第二replay buffer的容量要足够大目标是让缓冲区里的数据尽量覆盖不同历史策略的分布。我经验上把容量设为50万条转移以上效果才稳定太小的缓冲会让重标记样本快速陈旧模型在同一个旧分布上反复过拟合。3.3 网络结构Critic和Actor怎么吃目标网络实现上要注意状态和目标不是两个独立的输入而是拼接成一个输入向量。设状态维度为s_dim目标维度为g_dim则演员网络的维度是(s_dim g_dim)。Critic要做的是把拼接向量和动作一起输入。我用的是三隐藏层结构每层256个神经元激活函数ReLU。Actor输出层加tanh限制到[-1, 1]这样可以直接匹配环境动作范围。Critic输出一个标量Q值。如果任务状态维度特别高比如图像输入可以考虑卷积层但一般的机械臂操作任务用MLP就够。import torch import torch.nn as nn import torch.nn.functional as F class Actor(nn.Module): def __init__(self, s_dim, g_dim, a_dim): super().__init__() self.fc1 nn.Linear(s_dim g_dim, 256) self.fc2 nn.Linear(256, 256) self.fc3 nn.Linear(256, a_dim) def forward(self, state, goal): x torch.cat([state, goal], dim-1) x F.relu(self.fc1(x)) x F.relu(self.fc2(x)) return torch.tanh(self.fc3(x)) class Critic(nn.Module): def __init__(self, s_dim, g_dim, a_dim): super().__init__() self.fc1 nn.Linear(s_dim g_dim a_dim, 256) self.fc2 nn.Linear(256, 256) self.fc3 nn.Linear(256, 1) def forward(self, state, goal, action): x torch.cat([state, goal, action], dim-1) x F.relu(self.fc1(x)) x F.relu(self.fc2(x)) return self.fc3(x)这里有个可选优化把输入改成“状态与目标的差值”。比如输入不是[s, g]而是[s - g, s, g]。对于推动物体这类任务差值输入会让平移不变性变得明显网络更容易泛化到目标位置变化的情况。但在需要绝对坐标的任务里差值可能丢失信息。我的经验是先跑一版纯拼接跑不通再试差值版本节省调试时间。3.4 完整训练循环与超参配置训练循环其实不复杂核心是采样、重标记、更新Actor-Critic。我用TD3风格的更新Actor延迟更新、目标网络软更新。训练循环的骨架如下for episode_idx in range(total_episodes): obs env.reset() episode [] done False while not done: state torch.FloatTensor(obs[observation]).unsqueeze(0) goal torch.FloatTensor(obs[desired_goal]).unsqueeze(0) with torch.no_grad(): action actor(state, goal).cpu().numpy()[0] # 添加探索噪声 action np.clip(action np.random.normal(0, 0.2), -1, 1) next_obs, reward, done, info env.step(action) episode.append({ obs: obs, act: action, next_obs: next_obs, achieved_goal: next_obs[achieved_goal], desired_goal: obs[desired_goal], r: reward }) obs next_obs buffer.store_episode(episode) for _ in range(update_steps): batch buffer.sample_her_batch(batch_size256) # 分batch更新Critic和Actor update_td3(batch)我实际的超参数配置贴在下面这些值都是从多个复现实验里压实过的可以直接当初始值用。参数设定值备注每转移重标记目标数 k4数值越大样本越多样但训练更慢未来策略概率0.5剩余概率用最终状态做目标缓冲区容量50万条转移以上每个episode按转移数计入批量大小256太小方差大太大会吃掉显存Critic学习率3e-4用Adam优化器Actor学习率3e-4与Critic保持一致避免不匹配折扣因子 gamma0.98任务长度较短时可以用0.95探索噪声sigma0.2高斯噪声作用在动作上目标网络软更新 tau0.05数据集有限tau可以大一些按照这套配置FetchReach这类简单任务通常在几万步内就能看到成功率快速上升FetchPush这类难度更大的任务大约需要50万到100万步。如果你的环境里随机探索成功率实在太低可以考虑给环境做个基础启动状态比如把物体初始位置和目标位置拉开到一个可控距离方便HER先积累一批可用的失败样本。4. 我在复现时踩过的坑HER不是加了就有效的魔法HER在很多稀疏奖励任务上效果显著但它绝不是无脑装上就完事。这里记录几个我实际遇到的坑基本都能复现希望能帮你避雷。4.1 坑位一把所有样本都重标记原始目标丢失了这是我第一次尝试HER时犯的典型错误。我把每条转移重标记了4次然后完全用重标记样本训练把原始样本丢在一边。结果非常诡异模型在重标记目标上表现良好但在真实目标上的成功率几乎为零。原因很简单重标记样本里从来没有出现过真实的desired_goal模型压根没学会怎么处理真实任务目标。批评它只对“事后补出来的目标”敏感了。解决方法是每个episode的转移必须至少保留一份原始目标样本。我在采样逻辑里就是这样做的base那条样本就是原始目标。重标记样本是增量不是替代品。4.2 坑位二把重标记目标设成了绝对状态而不是achieved_goal有段时间我在自定义机器人环境里做搬运任务状态空间是关节角度 物体位置 目标位置。我一着急直接用下一个状态的完整向量当新目标结果维度完全对不上训练直接崩掉。后面才意识到重标记只动目标部分状态部分必须原封不动。实际工程里建议给环境定义一个明确的函数从完整状态里抽取achieved_goal子向量。比如对于抓取任务achieved_goal就是物体当前位置对于到达任务achieved_goal就是末端位置。目标空间应该是一个低维的、物理上可达的坐标集合而不是整个状态空间。目标空间越大、越抽象重标记越容易产生无意义的目标HER效果就越差。4.3 坑位三没有对观测和目标做归一化机械臂操作任务里关节角度范围通常在[-1,1]但物体坐标可能是[0, 5]甚至更大。如果不做归一化Critic在拼接特征时会毫无悬念地偏向数值大的维度梯度更新也会被不均衡的尺度带着跑偏。我用原生态操作时训练曲线一直震荡后来把观测和目标都做了一次MinMax归一化震荡立刻缓解。推荐做法环境初始化时收集两组数据的真实上下界然后对观测、目标分别做缩放。注意归一是用训练时的统计量测试时要用同一组统计量不能用测试集重新算。这个细节容易在切换环境时被忽略。4.4 坑位四奖励设计成距离惩罚反而拖慢收敛某些任务里人们觉得二值奖励太稀疏干脆用负的欧氏距离当奖励。加上HER后我一度觉得这种连续奖励更好毕竟每一步都有信息。但实验结果是负距离奖励反而让收敛速度变慢。原因是HER重标记时我们会把已经到达的状态设为目标此时距离全是0训练信号是“你已经在最优点”模型学到的就是原地不动策略而真实目标的状态又因为梯度较小不容易推动策略往正确方向改进。二值奖励下重标记样本会明确告诉模型“这一步是成功的”学习信号强得多。所以操作类任务我最终都回到-1/0二值化奖励上HER原本的设计也倾向于这种形式。连续奖励并非不能用但需要更精细的缩放和shaping不适合默认方案。4.5 坑位五忽略“目标是否可达”HER重标记的目标必须来自真实轨迹所以理论上它天然是可达的。但有一种特殊情况如果环境状态里包含了某些不可控变量比如物体在可夹取范围之外、机械臂末端已经触底那么即使重标记到这个状态模型也不一定能复现。换句话说HER会把“可观测但不可控”的状态误当成目标。最典型的案例是我在一个带摩擦和滑动障碍的仿真环境里物体被撞到了墙缝中那个位置在物理上是不可能通过机械臂再次到达的。HER重标记了穿插在轨迹里的这类状态训练完发现策略对这类目标毫无反应。后来我把重标记目标限制在“未来策略采样”的范围内并且排除了那些处于物理极限附近的状态问题基本消失。如果你做的环境有类似的不可逆动力学记得在重标记过滤逻辑里加上约束。5. 从仿真到实际项目hindsight这个思想还能延伸到哪里HER不仅仅是一个具体的算法更是一种思考数据的方式。它解决的核心问题是当任务目标没有达成时这条轨迹是否仍然有价值答案是肯定有但我们不能原样用它而是要换一个能把这作为成功结果的视角。这个思路在实际项目里有几处延伸。比如在多任务强化学习中HER可以天然地与去中心化训练搭配一个模型同时处理多个目标每次失败数据都能通过重标记为其他任务提供样本。又比如一些多智能体场景里一条失败的整体协作轨迹可以被重标记成“完成某个子目标的成功轨迹”用来培养个体技能。我见过有人把它用到对话生成里模型生成了一句不匹配用户意图的回答但这句话在另一个对话目标下可能完全合理于是通过反向构造训练目标来提升样本效率。再比如说真实机器人项目中采集物理数据成本极高HER相当于免费把每次实验的利用率乘以了5到10倍。这一点非常宝贵。机器人系统的动力学复杂、存在摩擦和延迟想要采集大量“成功”样本几乎不可能但“失败”样本随处都是。HER让我们终于能低下头捡起这些看似无用的垃圾数据。工业场景里把状态转移日志完整保存下来配上重标记机制相当于给真实系统增加了一个“后见之明”的放大镜。如果你已经跑通了基础的HER可以继续尝试两个方向。第一个是混合课程先用HER在宽松目标下快速学会控制再逐步收紧目标阈值同时保留重标记样本。第二个是目标生成训练一个小生成器来产生有难度但可达的目标替代当前“从轨迹未来采样”的朴素方式这样能进一步提升训练数据的“教学难度”。6. 最后分享一个我在实际过程中悟到的细节HER最让我感慨的一点是它逼着你重新审视训练数据里的“失败”。很多新手训练RL模型看到奖励长期为负第一反应是调整超参数、换网络结构、加奖励塑形但真正的问题可能不在模型而在数据利用率。我们手里的失败样本里藏着大量关于“什么状态是可达的”“什么动作会导致什么后果”的信息只是它们没有被用一个合适的坐标系去表达。我现在做目标条件任务时会先问自己三个问题状态空间里哪些部分是目标重标记后的目标是否物理可达原始目标样本是否仍然保留在训练集里把这三个问题想清楚HER基本就能发挥出七八成的实力。剩下的两三成靠的是对环境和数据的耐心调试。如果你正在被稀疏奖励折磨我建议你别急着上复杂的课程学习和分层强化学习先动手实现一个HER试试看。把所有未完成的episode都好好存下来别删改一下目标再喂给网络你会发现那些曾经让你绝望的零奖励数据才是这个任务里最值钱的宝藏。
返回列表