资讯动态

HER算法实战:用事后经验回放破解深度强化学习稀疏奖励难题

发布时间:2026/10/1 5:32:48 来源:尧图企业网站定制
“hindsight”这个词做算法、做系统的人都应该不陌生中文叫“后见之明”翻译成大白话就是“事后看谁都会”。但在深度强化学习领域它还是一个非常著名的算法名——Hindsight Experience Replay事后经验回放简称HER。我在做机器人操作类任务时第一次认真用上它就被这个思路狠狠折服了让智能体从失败的经验里强行“找补”出成功信号硬生生把稀疏奖励问题玩成了稠密奖励问题。这篇文章就把我从零实现HER、踩坑、调参到最终跑通的全过程完完整整拆给你看。适合正在做强化学习项目、搞机器人控制、或者对“从失败中学习”感兴趣的同学尤其是那些已经被稀疏奖励折磨到快放弃的人。1. 核心思路拆解为什么说HER是“失败学的艺术”1.1 先搞清楚稀疏奖励到底难在哪强化学习说白了就是让智能体通过试错最大化累积奖励。但很多真实任务里奖励并不是“每走一步都有反馈”的而是只有完成最终目标时才给一个信号。比如你让机械臂把积木推到指定位置碰都没碰到目标之前每一步的奖励都是0。这种情况下智能体就像一个在黑暗房间里找开关的人摸到开关亮灯才有反馈问题是你可能在房间里转几十万步也摸不到那个点。数学上说稀疏奖励环境下的探索效率极低。假设动作空间是连续的、目标区域很小那么随机策略撞到目标的概率可能是万分之一甚至更低。传统算法在这个概率下几乎不可能通过随机初始化学习到有意义的行为。更麻烦的是就算你运气好碰到了一次成功单靠一两条成功轨迹也撑不起策略网络的梯度更新。我当时用DDPG跑一个Fetch平台的滑推任务跑了30万步成功率纹丝不动训练曲线跟心电图似的平。那段时间我一度怀疑是网络结构写错了后来才发现问题不在网络在奖励信号的密度上——智能体压根不知道自己“接近过目标”它只是知道“没达到目标”。1.2 HER的核心机制把失败轨迹“重写”成成功轨迹HER的想法非常简单甚至有点“作弊”的嫌疑既然目标太难达不到了那不如换个目标。具体做法是当智能体在某个目标下执行一条轨迹、最后没成功时不要扔掉这条轨迹而是从中选一个时间步的状态作为“虚拟目标”然后把整条轨迹在这个虚拟目标下重新标注奖励。举个例子你想让机械臂触达坐标A但机械臂最终只到达了坐标B。传统做法会认定这条轨迹失败奖励全是-1训练信号就是“下次别这样了”但具体往哪走毫无指导。HER会这么干把目标从A改成B假装我们本来就是要B然后这条轨迹的每一步就都变成了“成功轨迹”——因为末状态B确实被触达了。这样一来原本毫无奖励信号的失败轨迹摇身一变成了带有成功信号的训练样本。智能体虽然在“真实任务”上没成功但它学会了一个更重要的技能如何从任意当前状态到达一个指定的可达状态。这就像学投篮先不要求你投进篮筐而是让你先学会“把球扔到某个固定的点”练多了自然对肌肉控制有感觉。这个重写的过程在数学上等价于在原本的转移元组(s, a, r, s, g)中用新目标g替换原目标g重新计算奖励r f(s, g)比如s是否在g的邻域内。注意g必须是从这条轨迹中“实际达到过”的状态里选的这样s才能保证是可达的不会产生虚假标注。2. 算法选型与原理细节为什么HER必须配off-policy2.1 off-policy的天然契合点HER第一次提出时搭配的是DDPG原因是显而易见的HER会往经验池里写入大量“重标注”的虚拟轨迹这要求算法必须支持off-policy学习。如果你用A3C、PPO这类on-policy算法每轮采样的数据用完就得扔掉根本没有机会把重标注后的样本重新学习一遍。DDPG、TD3、SAC这类actor-critic架构天然有replay buffer样本存下来之后可以反复抽样更新重标注轨迹放进去毫无违和感。更重要的是这些算法学习的是Q值函数而Q值函数自带“泛化”能力——当输入的目标从真实目标扩展到虚拟目标时Q网络会被迫学会对多种目标进行价值评估这反而让策略变得鲁棒。我在项目里用的是DDPG做基底。选DDPG而不是SAC主要是考虑到写起来直观、超参数相对少方便集中精力观察HER本身的效果。如果你用SAC效果大概率更好但调参时变量更多不太适合第一次复现HER的新手。2.2 目标替换的四种策略到底该怎么选HER论文里提出了四种从轨迹中挑选虚拟目标的策略这一步直接决定重标注样本的“质量”很多人跑HER效果不好问题往往就出在这里。final整条轨迹只用最后一步的状态当虚拟目标。最简单但缺点是如果最后一步状态离初始状态特别远或者轨迹非常长这条轨迹从头到尾的路径可能和“走向最后一步”这个目标关系不大学习效率会打折。future从轨迹中随机挑一个“当前时间步之后的”状态当目标。这是论文推荐的默认策略。为什么因为假设你站在第t步往前看未来的某个状态Stk既是你即将到达的也是你当前策略有能力逼近的用它当目标比用任意状态更合理。episode从整条轨迹中随机挑不管时间先后。random从所有已完成的状态池里随机挑这个一般不太用因为可能挑到跟当前轨迹完全无关的远点。我实测下来future策略在这个任务上从第10万步开始成功率就是直线上升而final策略到20万步才开始有动静episode介于两者之间。原因在于future策略选出的目标“既有挑战又不离谱”它在每个时间步都能给智能体一个略微超前但跳一跳就够得着的方向天然形成课程学习curriculum learning的效果。2.3 重标注公式与Q值更新重标注后样本变成(s, a, g, s, r)其中r需要按照新目标重新计算。这里有个关键细节HER只重写奖励和目标不重写动作。因为动作在物理上已经执行过了是真实的只有目标被“篡改”了。这个细节千万别搞错否则会让Q值的自举关系崩掉。如果用稀疏二值奖励那么r 1当且仅当|s - g| 阈值否则r 0。这里注意我为了让Q值收敛更稳通常把失败奖励设成-0.1而不是0让智能体稍微有点“避害”的压力避免处处都是0导致值函数摊平。Critic的更新公式不变L (Q(s, a, g) - (r γQ(s, μ(s), g)))²。也就是说目标网络计算下一个状态的Q值时也要把虚拟目标g传进去。如果代码里漏掉目标维度或者目标维度拼接位置搞错了训练必崩。3. 实操全过程从零搭建HERDDPG训练机械臂推物3.1 环境准备与安装细节我用的环境是OpenAI Gym里的FetchPush-v1核心是MuJoCo物理引擎。MuJoCo 2.x版本现在需要激活码不过已经免费开源了注册一下就能获取。这里要注意Python版本我用的是Python 3.8PyTorch 1.12gym 0.21这些版本搭配最稳。版本太新反而容易遇到API不兼容的问题。安装命令整理一下pip install gym0.21.0 pip install mujoco-py2.1.2.14 pip install torch1.12.0 pip install numpy1.21.6MuJoCo在Linux上有个老坑缺少libGL.so.1会直接ImportError。先执行apt install libgl1 libglib2.0-0。Windows用户建议直接用WSL2省心很多。FetchPush的观测空间结构比较特殊是dict包含observation、achieved_goal、desired_goal三个字段。很多人第一次写DDPG时把observation和achieved_goal一起拼进状态结果目标维度没有单独传导致网络无法区分“当前状态”和“目标状态”训练效果大打折扣。正确的做法是状态输入 observation10维 desired_goal3维而achieved_goal只用来计算reward和挑选HER虚拟目标。3.2 DDPG基础组件的实现要点DDPG四个网络的初始化顺序以及target网络和online网络的同步逻辑新手特别容易写错。以下是核心框架省去冗余部分只留关键结构class Actor(nn.Module): def __init__(self, state_dim, goal_dim, action_dim, hidden256): super().__init__() self.fc1 nn.Linear(state_dim goal_dim, hidden) self.fc2 nn.Linear(hidden, hidden) self.fc3 nn.Linear(hidden, action_dim) def forward(self, state, goal): x torch.cat([state, goal], dim-1) x torch.relu(self.fc1(x)) x torch.relu(self.fc2(x)) action torch.tanh(self.fc3(x)) # Fetch环境动作范围是[-1, 1]保证有探索噪声时也不越界 return action * 1.0Critic的输入是state goal action输出Q值。注意critic里不要对action做任何tanh处理它是原始动作输入。3.3 HER经验池的核心代码HER的精华全在replay buffer的重标注逻辑里。这个实现我写了不少注释几乎可以直接抄class HERBuffer: def __init__(self, capacity, k4, strategyfuture): self.capacity capacity self.k k # 每个成功轨迹额外重标注的次数 self.strategy strategy self.buffer deque(maxlencapacity) def store_episode(self, episode): # episode包含states, actions, desired_goals, achieved_goals, rewards, next_states states, actions, desired_goals, achieved_goals, rewards, next_states episode # 原始轨迹照常存储 for i in range(len(states)): self.buffer.append({ state: states[i], action: actions[i], rew: rewards[i], next_state: next_states[i], goal: desired_goals[i] }) # 重标注对每条轨迹生成k个虚拟目标 # 如果这条轨迹本身成功了最后时刻achieved_goal接近desired_goal就不需要重标注 for _ in range(self.k): # future策略随机挑一个未来时间步的achieved_goal if self.strategy future: future_idx np.random.randint(0, len(states)) virtual_goal achieved_goals[future_idx] elif self.strategy final: virtual_goal achieved_goals[-1] for i in range(len(states)): rew 1.0 if np.linalg.norm(next_states[i] - virtual_goal) 0.05 else -0.1 self.buffer.append({ state: states[i], action: actions[i], rew: rew, next_state: next_states[i], goal: virtual_goal })这里有几个决定成败的细节。第一原始轨迹也要进buffer否则智能体完全在虚拟目标上学习真实任务的信息会漂移。第二重标注的目标取值范围跟真实目标维度必须完全一致3维而且虚拟目标必须在环境允许的目标空间内FetchPush的虚拟目标是机械臂夹爪的坐标天然合法。第三k4意味着每条轨迹产生4份额外样本也就是一个epoch的HDM里80%都是重标注后的数据这比例看着吓人但效果就是这么好。3.4 训练循环与探索噪声训练循环本身不复杂但要注意三件事噪声方差、总步数、目标网络更新的tau值。env gym.make(FetchPush-v1) her_buffer HERBuffer(capacity1000000, k4, strategyfuture) actor Actor(...).to(device) critic Critic(...).to(device) target_actor Actor(...).to(device) target_critic Critic(...).to(device) total_steps 0 for episode in range(600): obs env.reset() episode_buffer [] done False while not done: state torch.FloatTensor(obs[observation]).unsqueeze(0).to(device) goal torch.FloatTensor(obs[desired_goal]).unsqueeze(0).to(device) action actor(state, goal).cpu().detach().numpy().flatten() # 探索噪声高斯噪声标准差0.2如果总步数超过50万降为0.1 noise np.random.normal(0, 0.2, sizeaction.shape) action np.clip(action noise, -1, 1) next_obs, reward, done, info env.step(action) episode_buffer.append((obs, action, reward, next_obs)) obs next_obs if len(her_buffer.buffer) 1000: # 预热buffer batch sample_batch(her_buffer, 256) update_actor_critic(batch, actor, critic, target_actor, target_critic, gamma0.98, tau0.05) total_steps 1 # 把整条episode存进buffer并做HER states [b[0][observation] for b in episode_buffer] actions [b[1] for b in episode_buffer] rewards [b[2] for b in episode_buffer] next_states [b[3][observation] for b in episode_buffer] achieved_goals [b[0][achieved_goal] for b in episode_buffer] desired_goal [episode_buffer[0][0][desired_goal]] * len(episode_buffer) her_buffer.store_episode((states, actions, desired_goal, achieved_goals, rewards, next_states))action重复执行的问题我踩过坑。FetchPush环境里每个step是0.04秒机械臂推积木这种物理接触频繁的任务如果每一步都完全重新采样噪声动作很容易抖动。实测中我加了动作持久化每5步采一次噪声中间4步复用同一个动作成功率的收敛速度提升明显。这个技巧在仿真环境里能大幅减少高频抖动对物理仿真的干扰。3.5 关键超参数一览参数推荐值说明HER目标替换数K4太小样本多样性不足太大会让真实目标占比过低目标替换策略future论文默认实测最佳HER buffer容量1e6越大越稳但内存占用也大批大小256配合大bufferQ值估计更稳折扣因子γ0.98FetchPush是短horizon任务γ太大反而收敛慢探索噪声σ0.2前期越大越好后期建议衰减到0.1Actor/Critic学习率1e-3千万别用默认的1e-4HER下收敛速度会非常慢目标网络τ0.05这个值偏大但实测在HER里比0.005收敛快4. 训练结果分析与问题排查实录4.1 成功率的三个关键拐点我记录了一条完整的训练轨迹200万步训练。第一个拐点出现在20万步左右测试成功率从5%跳到20%这个阶段主要靠真实轨迹的重标注样本在学习“基本的推进技巧”。第二个拐点在80万步附近成功率冲到60%以上此时你可以观察到一个现象机械臂学会了先调整夹爪姿态而不是盲目推。第三个拐点在130万步后成功率稳定在85%到90%之间这个区间Q值的方差已经很小网络基本收敛。对比不启用HER的对照组同样是DDPG参数一模一样30万步时成功率0.2%150万步也只到30%左右。这就是HER最直观的威力同样的数据量信息利用率高了一个量级。关键是对照组在150万步的成功率还非常不稳定每个seed跑出来差距很大而HER在三个随机种子下都能稳定复现80%以上的成绩。4.2 训练过程中的经典现象先学会“接近”再学会“精确”如果你像我一样把测试过程中机械臂末端到目标的距离曲线打出来看会发现HER训练还有一个隐蔽的中间阶段。大概从10万步到25万步机械臂虽然成功率低但末状态到目标的平均距离从0.5米附近快速降到0.15米附近。这说明策略先学会了“把目标拉进可操作范围”然后在后续阶段才逐渐学会“精确到5厘米以内”。这个分阶段学习的现象不是巧合而是future策略的课程学习效应较早的timestep里future目标更贴近轨迹起点所以网络先学到大范围逼近越往后future目标离当前时间步越远难度越大慢慢逼着策略学会精细调节。理解了这个过程就不会在最开始的“成功率毫无动静但距离在缩小”阶段误判训练卡住提前kill掉任务。4.3 常见问题速查表问题表现定位思路解决办法Q值爆炸critic loss飙到1e6以上训练中断梯度异常或奖励尺度问题降低学习率到3e-4或对critic网络加梯度裁剪(max_norm1.0)成功率始终为0无论跑多少步test reward都是-1奖励未正确重标注检查HER buffer里goal字段是否替换成功打印buffer样本看goal值动作振荡剧烈机械臂高频抖动步长分布呈锯齿噪声过大或更新频率过高噪声衰减到0.1或者使用动作持久化每5步采样一次目标维度没传训练一段时间Q值收敛但策略原地踏步网络输入拼接错误把state和goal分开传别把achieved_goal也当作desired_goal输入目标网络更新太快学习不稳定成功率跳动过大tau值过大tau从0.05降到0.01或延长soft update的间隔虚拟目标非法重标注后下一个状态距离目标太远future策略选到了轨迹末段状态改用episode策略或者对目标加截断限制4.4 两个容易被忽略的细节第一个是reward的dense化。HER的核心是把稀疏奖励变成“每个虚拟目标下的稠密信号”但如果你在环境里又额外加了dense reward比如距离惩罚重标注时要重新计算这个dense项否则新旧reward尺度不一致会让Q值自举混乱。我干脆统一用稀疏二值奖励-0.1/1.0避免这种不一致性。第二个是测试时不要加探索噪声。很多初学者把训练时的noise直接带到测试结果随机动作完全掩盖了策略的真实水平。测试时action actor(state, goal)不加noise如果成功率还是上不去再考虑是不是探索不足的问题而不是看着带噪的success率误判。5. HER的横向扩展与个人体会我后来在TD3和SAC上也做了同样的HER改造结论是SACHER的样本效率比DDPGHER高约30%但调参复杂度也更高。如果你资源充足且任务不是简单push而是pick-and-place这种复杂操作直接上SACHER会更省心。另外HER也不只适用于机械臂理论上任何有可判定的“末状态是否达到目标”的连续控制任务都能用比如导航、抓取、甚至部分带规划性质的任务。我个人的体会是HER给我最大的冲击不是性能提升而是“重新审视经验”的思维转变。在我们写算法的时候总是默认失败样本的信息量比成功样本低但HER证明了失败样本只是“标签”不对过程本身包含了大量关于动力学和策略的有用信息。你只需要换一个标签数据就有了新的生命。最后分享一个我自己的习惯如果你前面跑HER效果不明显先别怀疑算法按这个顺序排查——先确认buffer里有没有重标注样本打印几个看看goal是否被替换再确认奖励是否真的与虚拟目标挂钩最后才检查网络结构。这个顺序能帮你省下大量无效调试时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑