ARTICLE DETAIL

资讯详情

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

强化学习稀疏奖励难题:HER算法原理与PyTorch实现详解

强化学习稀疏奖励难题:HER算法原理与PyTorch实现详解 Hindsight后见之明。这个词放到强化学习里指向的是那篇很有名的论文《Hindsight Experience Replay》通常缩写为HER。做机器人控制、游戏AI这类带探索压力的项目时我几乎每次都会被稀疏奖励卡住而HER是最能直接拉高样本利用率的技巧之一。这篇文章就从原理讲到落地用PyTorch搭一个最小可跑的实例把HER到底解决了什么问题、代码怎么写、以及实际训练中最容易踩的坑都过一遍。如果你正在搭强化学习环境或者已经被稀疏奖励搞到头大这篇应该能节省你不少试错时间。1. 为什么需要hindsight稀疏奖励下的学习困境1.1 奖励稀疏到底难在哪里稀疏奖励是强化学习里最经典的头疼事之一。定义很简单agent只有在很少的交互时刻拿到非零奖励其他绝大多数步都是0或者-1。以机械臂抓取为例常见设置是把目标坐标作为输入只有当末端执行器离目标足够近时才给正奖励其他帧一律-1。这意味着如果初始策略基本是随机的一百万步里可能连一次“接近成功”都碰不到。更麻烦的是稀疏奖励会让Q网络输出的值长时间几乎不变actor从critic那里拿到的梯度信号也趋近于零策略更新完全变成无头苍蝇。很多人会用生活直觉理解强化学习像训练宠物做动作做对了给零食做错了不给。这套逻辑成立的前提是你至少踩到过“做对”的瞬间如果奖励信号稀疏到要经历千辛万苦才出现一次那宠物早就懒得配合了。算法也一样随机探索的收益完全看运气而运气在连续控制里从来不怎么可靠。真正落到项目里稀疏奖励还有一层隐形成本每次试错都可能涉及真实机械臂或长时间仿真样本效率就是时间成本。要直观感受这个问题的严重性可以先跑一个最朴素的连续控制环境状态和动作都是二维目标坐标随机给奖励只在欧氏距离小于0.05时给0其他时间给-1。在没有任何技巧的情况下你会看到探索曲线毫无起色loss曲线也没有明显学习迹象。HER就是为这种局面准备的手术刀目标不是让奖励变稠密而是让“失败经验”本身变成可学习的正反馈。1.2 传统补救思路和它们的软肋遇到稀疏奖励大家第一反应往往是奖励塑形reward shaping。也就是在中间过程里加一些人工设计的辅助奖励比如离目标越近分数越高。这确实有效但代价很大你需要对任务有足够深入的理解才能设计出不会被卡bug的奖励函数而且稍微换个环境奖励函数往往要重新调。第二个常见思路是课程学习curriculum learning从简单目标逐步过渡到困难目标。思想没错但难点在于怎么自动排序课程以及怎么判断当前agent适合推进到哪个阶段。真去实现又是一堆工作量。还有一类好奇心驱动的方法用状态新颖度作为内在奖励鼓励agent去探索没见过的状态。它确实能让agent动起来但“动起来”和“靠近目标”是两码事很多时候会走偏。这些方法本质都是在改任务或改奖励而HER换了一个角度不改任务也不改奖励而是改“经验”。它把一条没达到指定目标的轨迹用轨迹里实际到达的状态重新标记成另一个目标的成功轨迹。这样失败就不再是单纯的负样本而是一批实打实的、能提供梯度的经验。这个思路听起来有点取巧但数学上非常干净。2. HER原理拆解重新标记目标这件事2.1 核心思想承认失败但让失败变得有用HER的核心操作可以概括成一句话对一个已经结束的episode把原先的稀疏目标替换成agent实际到达过的状态然后重新计算这条轨迹的奖励。举个例子现在要教一个智能体把箱子推到坐标(1,1)但实际操作中箱子最终停在了(1,2)。按原来的目标这整条轨迹都是失败的每个transition都要记负分。但HER会告诉策略网络假如我们的目标本来就是(1,2)这条轨迹其实挺成功从起点到终点这段经验值得好好学习。这个重标记并不是在撒谎。它并没有凭空捏造成功而是把“真实到达过的地方”写成了目标。一条轨迹里包含的状态可以同时服务于很多不同的目标。遇到的目标是(1,1)时我没推到但我至少学会了怎么把箱子推到(1,2)附近。有了这类局部经验打底后续再学“推更远一点”就容易得多。这也是“replay”的另一层含义经验不只是直接拿去更新还能在回放时重新解读。传统replay buffer里的transition是什么就是什么HER把“同样一条transition”放到不同目标语境下重新计算奖励让一份数据被反复榨取。尤其在样本成本高的真实环境里这个再解读的过程价值极大。2.2 四种目标采样策略怎么选HER论文里讨论了几种从episode中抽取“事后目标”的策略。这里我把它们放在一起对比策略新目标来源数据多样性实际效果final最后一个时间步的achieved goal低一个episode只用一个目标效果稳定但学习偏慢做baseline够用future当前时间步之后的某个achieved goal中等目标来自未来和当前状态有因果关联通常效果最好也是我最推荐的默认选择episode整个episode里随机一个achieved goal较高但可能离当前transition太远少数情况有用整体不稳定random从所有见过的goal里随机选很高但噪声大不推荐容易把学习方向带偏future策略为什么普遍最好因为它保留了一个关键性质新目标是从当前时刻之后的真实状态里取出来的这个状态确实是当前状态经过一段动作后能够到达的。从时序上讲新的目标与当前状态是有因果链的agent学起来不会那么反直觉。final策略只是future在“最末端的一个时刻”的特例胜在简单episode策略随机性太大目标可能与当前状态完全无关徒增难度。在实现时future还要考虑一个额外参数通常叫future_k或者k含义是每条原始transition额外重标记出几个新目标。一般取4到8即可。取1的话重标记给训练带来的增益太小取太大每次batch计算量会明显上升边际收益却递减。2.3 从单任务到多任务为什么重标记能提升样本效率表面上看HER只是换了个目标实际它把单目标学习变成了多目标条件学习。原始目标分布只有一个点agent只能从极稀疏的正信号里学习重标记之后同一条轨迹里出现的每一个achieved goal都能成为一个合法目标相当于把原本一个episode的样本扩展成N份不同目标语境的样本。密度提升是HER最直接的效果。原来一百万个transition里可能只有几次reward非零经过重标记几乎每条轨迹都能挑选出至少一个“事后可以达到的目标”于是训练集里的有效信号密度大幅上升。需要注意这不是无中生有的刷分每次重标记对应的transition确实是真实执行的agent学到的也是“从某个状态到达另一个邻近状态”的能力。还有一点经常被忽略HER和off-policy算法是天然搭档。replay buffer允许我们用旧策略产生的数据去更新当前策略因为有了buffer往里面塞多少“重新解读过”的transition都不会破坏数据流。这也是为什么HER大多和DQN、DDPG、SAC这类算法结合而不是直接在A2C这类on-policy算法上硬套。到了这个层面HER的思路已经清晰了接下来就看代码怎么写才不容易跑偏。3. 实操在PyTorch中实现HER3.1 用自定义二维环境快速验证为了不被MuJoCo的安装和物理参数带节奏我先用一个非常简单的二维环境做验证。状态是平面上一个点的坐标动作是每个step的微小位移目标随机生成。奖励是稀疏的末端位置和目标距离小于0.05时给0否则给-1。每一步返回四件事观测、奖励、是否结束、以及一个dict里带上achieved_goal。为什么强调achieved_goal因为这是HER能不能落地的前提条件。如果你的任务连“实际到达状态”都提取不出来后面所有重标记逻辑都无从谈起。HER要求状态里必须有一个可以和goal对应的、真实可观测的部分。这个点很重要我在后面问题排查里还会展开。import numpy as np import gym from gym import spaces class Point2DEnv(gym.Env): def __init__(self): super().__init__() self.observation_space spaces.Box(-1.0, 1.0, (4,), dtypenp.float32) self.action_space spaces.Box(-0.1, 0.1, (2,), dtypenp.float32) self.max_episode_steps 100 self.reset() def reset(self): self.state np.random.uniform(-0.5, 0.5, (2,)) self.goal np.random.uniform(-1.0, 1.0, (2,)) self.t 0 return self._obs() def _obs(self): return np.concatenate([self.state, self.goal]).astype(np.float32) def step(self, action): action np.clip(action, -0.1, 0.1) self.state np.clip(self.state action, -1.0, 1.0) self.t 1 achieved self.state.copy() reward 0.0 if np.linalg.norm(achieved - self.goal) 0.05 else -1.0 done self.t self.max_episode_steps return self._obs(), reward, done, {achieved_goal: achieved}这里reward的阈值0.05有点苛刻但对Point2D这种小地图来说还好。实际项目里阈值应该根据任务本身精度去设比如机械臂抓取通常用末端速度或目标半径。3.2 重标记逻辑的核心代码HER的buffer和普通replay buffer有个区别普通buffer存储单个transition就够了HER还需要按episode为单位存储因为重标记时要从同一个episode的未来时间步里采样新目标。我的做法是先用一个EpisodeMemory收集一整条轨迹训练时再针对批量采样做重标记。下面的代码是重标记函数的核心未来目标优先采样future策略并保留原始transition。这样处理会让训练目标变得更加丰富。import random from collections import deque import numpy as np class EpisodeMemory: def __init__(self): self.transitions [] def append(self, transition): self.transitions.append(transition) class HERBuffer: def __init__(self, capacity, future_k4, strategyfuture): self.capacity capacity self.future_k future_k self.strategy strategy self.episode_buffer deque(maxlencapacity // 100 10) self.index 0 def add_episode(self, episode): self.episode_buffer.append(episode) def sample_batch(self, batch_size): 采样batch并对每条transition按strategy做重标记 batch [] for _ in range(batch_size): ep random.choice(self.episode_buffer) t random.randint(0, len(ep.transitions) - 1) original ep.transitions[t] new_goal, reward self._relabel(ep, t, original) # 保留原始transition同时加入重标记后的transition batch.append((original, new_goal, reward, False)) for _ in range(self.future_k): new_goal_k, reward_k self._relabel(ep, t, original) batch.append((original, new_goal_k, reward_k, False)) return batch def _relabel(self, episode, t, transition): if self.strategy final: new_goal episode.transitions[-1][achieved_goal] elif self.strategy future: if t len(episode.transitions) - 1: idx np.random.randint(t 1, len(episode.transitions)) new_goal episode.transitions[idx][achieved_goal] else: new_goal transition[achieved_goal] elif self.strategy episode: idx np.random.randint(0, len(episode.transitions)) new_goal episode.transitions[idx][achieved_goal] else: idx np.random.randint(0, len(episode.transitions)) new_goal episode.transitions[idx][achieved_goal] achieved transition[achieved_goal] reward 0.0 if np.linalg.norm(achieved - new_goal) 0.05 else -1.0 return new_goal, reward实现上我做了两个简化第一batch里把原始transition和重标记transition一起放进去比例大致是1比future_k第二重标记后的transition没有再额外保留原始reward所有reward都按新目标重新算。这样做的核心原则是目标变了奖励必须跟着变否则经验就变成矛盾数据这点是HER训练中很多bug的根源。这个函数看似简单但实际用的时候有两个地方容易出错。一是future策略采样时下标一定要严格大于t否则目标变成“过去状态”失去了因果意义。二是如果episode里只有一个transitionfuture就退化成final这种边界情况要么直接跳过重标记要么用原achieved_goal兜底代码里我做了后者。3.3 DDPG训练循环与超参数有了重标记逻辑剩下的就是用一套off-policy算法把梯度跑起来。这里以DDPG为例因为结构清晰、和HER是经典组合。Actor负责输出动作Critic负责估计Q值target网络用软更新。训练循环的骨架大概长这样for epoch in range(epochs): obs env.reset() ep_memory EpisodeMemory() done False while not done: action policy.get_action(obs, noise_std0.1) next_obs, reward, done, info env.step(action) ep_memory.append({ obs: obs, action: action, reward: reward, next_obs: next_obs, done: done, goal: env.goal, achieved_goal: info[achieved_goal] }) obs next_obs her_buffer.add_episode(ep_memory) if len(her_buffer.episode_buffer) 32: batch her_buffer.sample_batch(batch_size256) # 对每个transition更新critic和actor更新target网络 for (original, new_goal, new_reward, _) in batch: critic_loss update_critic(original, new_goal, new_reward) actor_loss update_actor(original, new_goal) soft_update(tau0.05)代码里的update_critic和update_actor可以按普通DDPG的公式实现唯一和标准DDPG不同的是训练时要把原始transition和重标记后的新goal、新reward组合在一起送网络。这意味着网络输入的goal字段不能被历史观测里的goal字段占用需要从batch中单独取出。这里给一份我在二维环境上常用的超参数表超参数取值说明actor学习率1e-3太大容易震荡太小收敛慢critic学习率1e-3对HER来说常用这个量级gamma0.98折扣因子短episode可略低tau0.05target网络软更新系数noise_std0.1动作探索噪声落地好再调小buffer容量约50000条transitionepisode数可适当多存batch_size256无脑256很稳future_k4默认4效果和速度平衡每episode最大步数100环境定义训练总步数20000-40000二维点环境足够看到明显效果表格里目录的buffer容量和样本规模很保守等你在小型环境跑通了再往真实环境中扩比较容易。我最开始直接在一个大任务上开调结果所有参数都是乱的教训就是高维复杂环境下的HER问题往往先要在玩具环境里复现一遍才能分清楚是环境难还是代码有bug。4. 常见问题与排坑实录我从HER训练中踩过的坑4.1 重标记后reward没同步更新最隐蔽的错误这是HER新手最容易踩的坑我自己也栽过一次。你可能只改了transition里的goal字段忘了重新算reward。表面看起来训练循环没有报错但Q值的分布完全错乱因为同一个状态动作对在buffer里一会儿被标成“离目标很近所以reward0”一会儿又被标成“还有很远所以reward-1”critic根本不知道该信哪个信号。排查思路很简单写一个sample_batch的验证函数把batch里每个transition的goal、achieved_goal、reward一起打出来。如果看到goal和achieved_goal距离很远但reward却是0基本可以判死刑。我后来给自己定了一条规矩HER代码里不允许手动修改transition的任意单个字段必须用“重标记目标 重新计算奖励”两个动作同时完成。4.2 Q值高估与训练震荡DDPG本来就容易Q值高估加上HER大量重标记之后batch里会出现很多“同一状态对应不同目标”的样本这会让critic的优化目标更复杂震荡概率上升。我实际遇到的症状是一开始critic loss下降得不错后面突然飙升actor输出动作开始乱跳成功率反而下降。我当时做的调整有几步。先把critic学习率从默认调低比如到1e-3以下然后重标记目标和原始目标的比例不要一边倒原始transition至少要占到batch的一部分最后用较小的动作噪声比如0.05到0.1配合early stopping。还有一个很实用的技巧观察Q值和真实return的平均差距。如果Q值远大于实际累积回报说明高估出现需要降学习率或者多做几个critic step让critic更精确一些。4.3 高维目标空间的退化HER在二维连续目标空间上很好用但目标一旦变成高维、多模态例如直接用图像特征当goal效果会明显退化。原因也很简单重标记时采样的achieved_goal来自策略实际经历的状态在高维目标流形上可能分布很稀疏随机采样一个图像特征大概率离真正要学习的任务目标相去甚远agent学到的也多是没有意义的内部模式。我的建议是如果目标空间高维先做表征学习把图像或复杂输入压缩成低维的、语义明显的特征向量再在这个特征空间上做HER。这不改变HER本身只是让“状态实际到达的部分”更可提取。还有一个更朴素的建议先确认你的目标是否必须高维有些时候把目标空间拆成几个低维子目标比强行HER更高效。4.4 采样策略与k值经验四种策略我都在不同环境里试过最终很愿意推荐future作为默认。final不是不能用只是它每条episode只能重标记一个目标训练过程会慢不少。episode和random策略在我这边很少赢过future。如果你发现future表现不理想先不要怀疑策略本身先检查标号范围是否错误比如采样了t之前的目标。future_k的值也是经验活。k1基本等于轻量HER增益很小k4到8收益明显k16以上虽然数据更多但计算开销和replay buffer存储压力都上升训练时间也会拖长。我通常习惯先设4训练进度不够再提到8很少超过8。重标记比例也要保持在一个合理范围如果整条采样全是重标记样本没有原始目标样本训练初期反而容易迷茫。5. HER的适用边界与扩展方向5.1 什么时候该用HER不是所有项目都需要HER我用它之前一般会先过一遍这三条标准。第一任务的goal必须能从状态里提取出一个可观测、可计算的achieved_goal。比如机械臂末端坐标、车辆位置、方块朝向这些都行如果目标本身是抽象自然语言描述那直接套HER就不太对。第二奖励确实稀疏如果任务本身就带连续奖励HER的收益会被掩盖。第三算法是off-policyDDPG、TD3、SAC、DQN这些都可以如果项目用的是A2C这类on-policy算法硬上HER会破坏重要性采样的一致性。满足这三条时HER通常是提升样本效率的最快路径之一。特别是真实硬件上收集数据成本很高时用HER多榨取一点经验的价值会被成倍放大。反过来如果你的环境已经能在普通reward shaping下稳定训练HER可以不加因为任何额外的重标记逻辑都会增加代码复杂度和排查成本。5.2 从HER出发的几个扩展方向HER之后有不少延续性工作也让训练更稳。比如CHER之类的工作会把目标生成得更巧妙不再从已有achieved_goal里随机挑而是聚类后挑选更有启发性的目标还有基于生成模型的hindsight goal generation通过学习一个目标生成器来产出中等难度目标。另一个方向是把HER和课程学习结合让agent先学会到达自己的“事后成功”状态再逐步迈向更难的目标这在机器人操作里尤其常见。即使最近几年强化学习的内容重心已经偏向离线RL和偏好学习HER那种“从失败叙事里提取可用信号”的思想仍然在奖励重标记、数据增强这些组件里被反复复用。遇到新算法拿不准时我一般会先问一句它的经验回放环节能不能套HER的思路答案往往是可以。6. 写在最后几点个人体会按我的习惯如果训练目标信号稀疏我会先把HER搭起来再谈改奖励还是改网络结构。原因是它用很小的代码量就能让训练曲线出现可学习的迹象方便我判断“环境本身是不是有解”。实际跑下来我最看重的其实不是最终成功率而是每个episode里有没有开始出现“越来越接近目标”的特征agent会先学会靠近再学会停在目标附近这是一个很有代表性的学习轨迹。给准备动手的同学一个落地建议无论如何先在二维玩具环境上把整套buffer和重标记逻辑跑通再切到真实环境。玩具环境可以让你在五分钟内看到策略是否在学习而真实环境下问题往往被物理噪声和模型复杂度盖住了。别一上来就在重任务上死磕那样很难分清是代码问题、参数问题还是环境本身太苛刻。HER这条路的性价比恰好就藏在“把已经发生的经验用好”这个朴素念头里。
返回列表