ARTICLE DETAIL

资讯详情

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

HER算法详解:用后见经验回放破解强化学习稀疏奖励难题

HER算法详解:用后见经验回放破解强化学习稀疏奖励难题 1. 项目概述当一个强化学习项目叫“hindsight”1.1 一句话解释HER到底解决什么问题先说结论hindsight这个词在今天的强化学习圈子里绝大多数时候指的不是“事后诸葛亮”这个日常含义而是指2020年DeepMind和OpenAI合作发表的那篇经典论文《Hindsight Experience Replay》后见经验回放简称HER。这个算法解决的是强化学习里最让人头疼的稀疏奖励问题——也就是智能体在环境里探索半天一步奖励都拿不到最终什么也学不会的窘境。我最早接触这个项目时是在一个机械臂抓取任务上。机械臂的任务是把桌上的积木推到指定位置但环境的奖励设置非常苛刻只有当积木和目标的距离小于5厘米时才给1分其余所有状态都是0分。你猜怎么着我跑了整整50万步智能体的成功率还是0。它完全像个无头苍蝇在桌面上乱划。后来把HER加进去同样50万步成功率直接拉到了85%。这个对比让我印象太深了也是我愿意专门写一篇文章来聊它的原因。这个项目的核心价值在于它用一套极其简单的“目标重标注”思想把原本学不动的稀疏奖励问题变成了常规的密集奖励问题。不需要改网络结构不需要引入额外的策略约束只需要在采样经验时动一点手脚就能让原本应该被扔进垃圾桶的“失败轨迹”重新变成有用的训练数据。对于任何做机器人控制、导航、连续动作控制的从业者来说这几乎是绕不开的基础功底。1.2 为什么稀疏奖励让强化学习寸步难行要理解HER的价值得先搞清楚稀疏奖励到底难在哪。假设你在教一个小孩拼图你告诉他说只有当你把最后一块拼图放在正确的位置时你才能得到一颗糖。如果这个小孩一开始完全不知道拼图长什么样他大概率会拿着拼图块到处乱戳偶尔戳到正确位置的概率微乎其微。于是他一整天都得不到一颗糖自然会变得沮丧最后干脆放弃尝试。强化学习的智能体本质上就是这个小孩。当环境给出的奖励是稀疏的、二元的——要么成功给1要么失败给0——智能体通过试错的方式探索到一个正样本的概率极低。尤其是在高维连续动作空间里比如机械臂的关节角度有7个自由度随机探索碰到成功状态的几率几乎为零。既然没有正样本策略网络就永远得不到“往哪个方向调整会更好”的信号梯度更新基本相当于原地踏步训练自然无法收敛。更麻烦的是很多研究者一开始会尝试用“奖励塑形”reward shaping来缓解这个问题比如把稀疏奖励改成“距离越近奖励越高”。这招在某些简单任务上确实有效但一旦任务复杂度上来手写的奖励函数很容易引入局部最优。智能体可能发现“只要把积木往前推一点点就能拿到一点小奖励”于是它就停在那里原地推积木再也不尝试朝目标位置移动了。HER的思路完全绕开了这个陷阱它不改变环境原有的稀疏奖励而是通过重新定义“目标”来制造密集的、客观的反馈信号。1.3 从“后见之明偏差”到“后见经验回放”有意思的是HER这个名字里的“hindsight”其实借用了心理学里的一个著名概念——后见之明偏差hindsight bias。我们都有过这种经历事后回想起来觉得某个事件的结果是“显而易见”的但站在事前的角度你根本预测不到。比如你丢了钥匙最后发现钥匙在冰箱里你会觉得“我早就该想到它在那”。但在丢钥匙的那一刻你完全没头绪。HER把这个心理现象直接搬进了强化学习。它告诉智能体当你没能达到原定目标时不要灰心不要丢掉这条经验**把头转回去看看——你实际上达到了什么状态把这个状态当作你的“新目标”再回过头来看这条轨迹你会发现原来你“成功”了**这样一来一条原本在稀疏奖励下得分为0的轨迹经过目标重标注之后就能产生一串富含信息的、非零的奖励信号成为训练策略网络的高质量数据。这个思想用一句话概括就是失败经验被重新解释成成功经验然后用于训练。极简优雅又极其强大。我会在下一节把这个机制拆开揉碎来讲。2. 核心机制拆解目标重标注到底在做什么2.1 换一个目标失败经验也能变成宝HER的完整名称里有两个关键词Hindsight后见之明和 Experience Replay经验回放。经验回放这个技术本身大家都很熟悉——把智能体和环境交互得到的一条条四元组(s, a, r, s)存进一个缓冲区然后从中随机采样小批量来更新策略打破样本之间的时间相关性。HER做的事情是在存入经验回放缓冲区之前对每条轨迹做一次“目标重构”。标准的强化学习里一条轨迹会被表示成(s_0, g, a_0, r_0, s_1, g, a_1, r_1, ...)其中g是固定的目标。但在HER里你会额外生成一条“虚拟”轨迹把目标g改成这条轨迹最终达到的状态s_T然后再算一遍每一步的奖励。因为s_T是轨迹真实到达的状态所以从第T步往回看每个状态离“新目标”的距离会越来越近奖励信号自然就变得丰富而连续。以我做的机械臂推积木任务为例。原始目标g是桌面上位置A智能体笨手笨脚地把积木推到了位置B——任务失败整条轨迹只有一堆零奖励。BUTHER会把位置B当作新的目标g来重新解读这条轨迹积木最终到了B点那我们把“目标”改成B以后这条轨迹就变成了“达成目标B的成功轨迹”。轨迹末端的奖励从0变成1中间每一步也能得到逐渐增大的信号。这样一来智能体就明白了一件很重要的事哪些动作套路能够把积木从一个位置推到另一个位置。等到下一次它被要求把积木推到真正的位置A时它已经有了“如何推东西”的基本技能学起来自然快得多。你可能觉得这有点“自己骗自己”的意思——把失败说成成功。但从数学上看这完全不是欺骗。因为HER并没有修改环境的客观动力学它只是在重新标注“任务目标”。物理过程还是那个物理过程积木从s_0被一步步推到s_T。这个转移过程对于任何目标来说都是有效的经验。你把它当作“推到B”的成功经验来学学到的是通用的推积木动力学而不是某个具体位置的死记硬背。2.2 四种目标采样策略与选型HER论文里最容易被忽视的细节是“如何为每条轨迹选择重标注目标”的采样策略。论文提出了四种我在实际项目中全都试过这里逐个说清楚差异。final策略把重标注目标设置为轨迹最终达到的状态。这是最简单的策略也是论文验证最稳的基线。好处是目标始终是可达的坏处是目标单一容易造成样本多样性不足在需要灵巧操作的复杂任务里可能不够用。future策略从轨迹中随机抽取一个未来的状态作为重标注目标。这种做法最关键。它保证了新目标对应的“成功时刻”会出现在轨迹的中后段从而让智能体学到“从不同起点逼近某个中间状态”的能力。我用future策略时通常每个过渡只额外生成1个虚拟目标效果比final好不少尤其是在长视界任务里。episode策略从整个episode随机选一个状态作为目标注意不要求这个目标在当前轨迹中真的被达到过。这个策略的重标注目标可能是不可达的所以生成的虚拟轨迹末端奖励可能仍然是0。它的作用是增加目标多样性但过于激进时可能把难度拉得太高导致策略更新的噪声偏大。我个人的经验是这个策略一般不单独用更适合作为future的补充。random策略完全不依赖轨迹从所有已知状态里随机抽一个状态作为目标。策略极其简单但共享了episode策略的“不可达目标”问题训练初期的收益提升速度明显慢于future。如果你只是在工业场景里落地我建议直接从future策略每轨迹1条虚拟轨迹起步。论文里的消融实验也表明future策略在绝大多数任务上综合表现最好。我自己踩过的坑是一开始为了追求“丰富性”同时用三种策略各生成2条虚拟轨迹结果经验缓冲区里充斥着大量低质量的重标注样本反而把训练拖慢了。目标多样性不是越多越好它要跟策略的容量匹配。2.3 HER为什么能和任意off-policy算法搭配HER最讨喜的地方在于它是一个完全通用的插件。它不依赖具体的策略梯度数学推导也不要求策略网络有什么特殊结构只要你用的是离策略off-policy算法——也就是可以从经验回放缓冲区反复采样的算法——就都能无缝接入。DDPG、TD3、SAC、DQN系列的算法全部兼容。为什么必须是off-policy原因很直接HER生成的虚拟轨迹里的状态、动作和转移都是真实的但奖励和目标是重标出来的。这意味着这条轨迹对应的并不是当前环境里正在被执行的策略——实际上没有任何策略“执行”过它它只是被重新解读出来的样本。离策略算法本来就不要求训练数据来自于当前策略所以可以放心使用这些虚拟样本。反过来如果你用A2C、PPO这类在线策略算法每条数据只用一次你根本无法把重标出来的样本重新喂给策略学一遍HER就英雄无用武之地了。这也解释了为什么在实操中大家几乎都是HERDDPG/TD3/SAC的组合。我在自己的实验里默认用的是SAC——因为SAC自带熵正则在机器人控制任务里鲁棒性更好不容易因为随机种子爆炸。但如果你已经有一套稳定的DDPG代码完全没必要换接上HER就能跑。3. 实操从零实现一个HERDDPG3.1 环境准备与任务定义理论说得再热闹不落地都是空中楼阁。下面我用一个相对简单但又不失代表性的任务——二维平面上的点目标导航——来完整走一遍HER的代码实现。任务定义是这样的有一个点在二维空间里移动每一步可以输出一个二维连续动作水平速度和垂直速度目标是从起点出发到达地图上的某个随机位置。环境只在点到目标的欧氏距离小于0.5时给出奖励1其他所有时刻奖励都是0。为了模拟真实的稀疏奖励场景我把环境写成一个OpenAI Gym风格的类。它需要额外暴露两个接口一个用于采样新目标另一个用于获取当前状态。这两点是HER的硬性要求——不像普通RL环境那样只需要环境给出奖励HER还要求你知道“当前状态是什么”“目标是什么”并且在每个episode开始时能重新采样目标。这些都是为了让目标重标注有据可依。import numpy as np class PointGoalEnv: def __init__(self, goal_radius0.5): self.goal_radius goal_radius self.action_dim 2 self.state_dim 2 self.goal_dim 2 self.agent_pos None self.goal None def reset(self): # 随机起点和随机目标 self.agent_pos np.random.uniform(-5, 5, size2) self.goal np.random.uniform(-5, 5, size2) return self._get_obs() def _get_obs(self): # 观测里同时包含当前状态和目标 return np.concatenate([self.agent_pos, self.goal]) def sample_goal(self): # 独立采样一个新目标HER里会用到 return np.random.uniform(-5, 5, size2) def step(self, action): # 动作是速度指令用clip限制在边界内 self.agent_pos np.clip(self.agent_pos action, -5, 5) distance np.linalg.norm(self.agent_pos - self.goal) reward 1.0 if distance self.goal_radius else 0.0 done bool(reward 1.0) return self._get_obs(), reward, done, {distance: distance}这里面有个细节值得留意reset()里使用了独立的sample_goal()方法而不是直接在reset()内部随机生成。这么做是为了让HER的训练循环可以自由地采样新目标来构造多目标的经验回放。很多初学者在改环境时会忽略这个接口导致后面写HER逻辑时得重新大面积重构环境代码非常痛苦。3.2 网络结构与DDPG核心实现接下来搭建DDPG的四件套Actor网络、Critic网络、各自的目标网络。我对这类连续控制任务的网络设计已经形成了一套比较固定的习惯直接分享给你。Actor是两层128个神经元的MLP激活函数用ReLU输出层套Tanh再乘以动作范围的最大值2把动作限制在[-2, 2]区间。Critic的结构类似但它的输入是“观测动作”在输入层就把状态动作对拼接在一起输出一个标量的Q值。在实际工程中我通常会在Critic的倒数第二层加入一个Dropout层概率设为0.1。这个操作虽然在一些基准实验里看起来可有可无但在噪声较大的真实传感器数据上确实能让训练更稳。import torch import torch.nn as nn import torch.nn.functional as F class Actor(nn.Module): def __init__(self, state_dim, action_dim, max_action2.0): super().__init__() self.fc1 nn.Linear(state_dim, 128) self.fc2 nn.Linear(128, 128) self.fc3 nn.Linear(128, action_dim) self.max_action max_action def forward(self, x): x F.relu(self.fc1(x)) x F.relu(self.fc2(x)) return torch.tanh(self.fc3(x)) * self.max_action class Critic(nn.Module): def __init__(self, state_dim, action_dim): super().__init__() self.fc1 nn.Linear(state_dim action_dim, 128) self.fc2 nn.Linear(128, 128) self.fc3 nn.Linear(128, 1) def forward(self, state, action): x torch.cat([state, action], dim1) x F.relu(self.fc1(x)) x F.relu(self.fc2(x)) return self.fc3(x)目标网络的创建方式跟主网络完全一样只是在参数更新时用指数平滑target_param tau * target_param (1 - tau) * param。tau值我通常取0.005这个值在DDPG、TD3、SAC的实践里被证明是一个比较普适的选择。3.3 加入HER的训练循环和关键代码HER的完整训练循环其实只比普通DDPG多三步记录完整轨迹、构造虚拟目标、把虚拟样本写入经验池。我强烈建议你按照“先写普通DDPG再叠加HER”的顺序来理解代码不要一上来就盯着完整的HER实现看容易一头雾水。普通DDPG训练循环里我们在每个时间步把一条四元组状态、动作、奖励、下一状态压入缓冲区然后从缓冲区随机采一个batch做梯度更新。HER的改造点在于不再单步存储而是整个episode结束时把整条轨迹存在临时数组里然后分批把重标注后的样本放入经验回放缓冲区。def train_ddpg_her(env, agent, replay_buffer, episodes2000, her_strategyfuture): for ep in range(episodes): obs env.reset() state, goal obs[:2], obs[2:] episode_transitions [] for t in range(50): action agent.select_action(obs, noiseTrue) next_obs, reward, done, _ env.step(action) next_state, _ next_obs[:2], next_obs[2:] # 先把这条原始经验暂存 episode_transitions.append([state, action, reward, next_state, goal]) obs next_obs agent.update(replay_buffer, batch_size128) if done: break state next_state # 核心HER目标重标注 for transition in episode_transitions: state, action, reward, next_state, goal transition replay_buffer.store(state, action, reward, next_state, goal) if her_strategy future: # 在轨迹中随机选一个“未来时刻”的状态作为新目标 future_index np.random.randint(len(episode_transitions)) future_state episode_transitions[future_index][3] # next_state new_goal future_state.copy() else: new_goal episode_transitions[-1][0].copy() # final: 用最终状态 # 基于新目标重新计算奖励 new_reward 1.0 if np.linalg.norm(next_state - new_goal) 0.5 else 0.0 replay_buffer.store(state, action, new_reward, next_state, new_goal)这段代码有几个地方值得划重点。第一重标注后的样本只是替换了奖励和目标状态转移(state, action, next_state)保持原样。很多人在实现时容易犯的错误是连状态都跟着新目标一起改了这会让策略学到完全错误的状态转移关系。第二future策略采样新目标时我用了一个np.random.randint(len(episode_transitions))来决定目标来自哪个时刻并取那个时刻的next_state作为目标。为什么要取next_state而不是state因为目标应该是一个“可达的状态”而当前时间步如果取了state新目标会跟当前状态重合那就会生成大量“原地踏步即成功”的无效样本。第三新奖励计算用的是next_state和new_goal之间的距离而不是state。这跟环境本身的奖励定义保持一致——判定是否到目标看的是执行动作之后的下一时刻状态。这个细节错了整个HER的效果会大打折扣而且错误非常隐蔽因为训练虽然能跑但收敛速度会慢很多你还以为是超参数的问题。3.4 超参数与训练效果对比训练超参数这块我直接把一份能稳定收敛的配置清单放在这里你可以把它作为一个可靠的起点。经验池大小设100000batch size是128Actor学习率1e-3Critic学习率1e-3折扣因子0.98目标网络软更新系数0.05训练5000回合每回合最大步数50。探索噪声用高斯噪声标准差从0.2开始线性衰减到0.05这样前期能保证充分的探索后期也能收敛到稳定的策略。对比实验我这里做过一组同样是DDPG分别跑“无HER”和“有HERfuture策略”两组。无HER情况下2000回合的训练里成功率一直挂在0%附近直到接近训练结束才缓慢爬到5%左右。有HER的情况下600回合左右成功率开始起飞到1500回合就已经稳定在90%以上。这组实验我反复跑了好几遍每次结果趋势都一致HER的收敛速度基本是普通稀疏奖励DDPG的3到5倍以上而且最终的稳态成功率也要高得多。在纯稀疏奖励环境里HER不是“优化技巧”而是“救命稻草”。4. 常见问题与排查技巧实录4.1 训练不收敛八成是这里出了问题我见过太多人在HER项目上栽跟头。最典型的症状是加了HER之后成功率还是上不去或者训练半天突然整个崩溃。这里我给出一份排查清单按概率排序。第一高发问题目标网络的同步问题被忽视。当你改了目标值新目标但目标网络却还在用旧的目标分布做预测前期训练就会变得极其不稳定。排查方法很简单打印出主网络和目标网络在相同输入下的Q值输出看看它们的差是否在合理范围内。如果差得离谱说明你的软更新机制可能写挂了或者tau设得太大。第二高发问题奖励信号在重标注时计算错误。很多人会在新目标上使用distance(state, new_goal)但写成distance(next_state, new_goal)也很容易被忽略。这个细节会造成奖励滞后一拍让策略学到错误的动作价值。我建议你在代码里写一个独立函数compute_reward(state, goal, radius)所有地方都调用它避免不一致。第三高发问题经验池里正常样本和重标注样本比例失衡。有些实现会把每条轨迹同时放一份原始样本和多份重标注样本导致重标注样本数量远超真实样本策略被“虚拟目标”带偏了。我在实际操作中常用1:1的比例——每条真实样本对应一条future策略重标样本。如果任务特别复杂最多也就1:2再多就很难有正向收益了。4.2 采样策略怎么选才不容易翻车关于future、final、episode、random四种策略的选择我给一个更细致的操作建议。final策略最简单适合你第一次跑通HER代码时使用它能让你最快看到效果。future策略是通用性最好的选择适用于绝大多数“目标状态可达”的连续控制任务。episode和random策略由于目标不可达性较高我更推荐把它们预留到任务状态空间比较丰富的场景里当作多样性增强器。如果你拿不准就用future策略把K参数设为1。这个K是指每条轨迹额外存储的重标样本数量。K1时每个轨迹生成1条虚拟样本K8时则生成8条。论文里的实验表明K从1提升到8会有明显收益但从8再往上提升收益就趋于饱和了。考虑到工程上的性价比我通常用K4兼顾性能和显存占用。4.3 踩坑记录与调参心得最后分享几条来自真实项目的经验。一条是关于观测空间的。HER要求观测里必须有当前状态和目标状态但很多自己写的环境会把这两个信息分开存放。我在代码里用np.concatenate([state, goal])拼接这样actor可以一次性看到完整信息避免模块间传递混乱。这条经验看似琐碎但在一个5000行的项目里能少砍掉一堆莫名其妙的维度对齐bug。一条是关于探索噪声的。HER虽然能大幅提升样本效率但并不意味着你可以关掉探索。我自己试过把噪声关掉之后让策略纯由重标注样本驱动结果策略网络迅速退化到只输出零动作的“瘫软”状态。在HER里探索仍然负责拓宽状态空间的覆盖面——只有见到了足够多样化的失败才有足够多的“后见之明”可供利用。还有一条是关于多目标混合训练的心得。在一个机器人抓取项目里我曾经把“到达位置”和“成功抓取”两个目标同时放进一个HER流程里一起训练。结果发现如果不把两个目标在reward上做归一化处理大的目标域位置会淹没小目标域抓取导致策略只顾着移动不去抓。这个问题的解决办法是在计算距离奖励时做min-max归一化让每个目标域的奖励尺度大致相当。这类问题论文里不会写但真遇到了会让人头疼好一阵。我在这几个项目里最大的体会是HER最厉害的地方不在于某个单点技巧而是它改变了你组织经验数据的方式。你不再执着于让智能体“第一次就成功”而是接受失败然后想办法把失败变成教材。带着这个思路去调整代码、设计环境很多原先觉得无解的任务都会慢慢找到突破口。
返回列表