尧图精选

稀疏奖励下的强化学习:HER原理与DDPG实战详解

🕒 发布时间:2026/10/2 18:08:35 📁 来源:尧图网络
1. 先别把它当英文单词看hindsight在强化学习里指的是什么hindsight直译是“后见之明”放在强化学习语境里它通常指代OpenAI在2017年提出的Hindsight Experience ReplayHER中文一般翻译成“事后经验回放”。我第一次认真调这个算法是在一个机械臂抓取仿真环境里目标点离初始位置很远奖励设计得极其稀疏同一条轨迹里几乎全程拿不到正奖励用DDPG裸训两百万步Q值纹丝不动。后来把回放池里的失败轨迹按HER的逻辑重标注了一遍几十万步就开始出现像样的抓取动作。这个反差给我留下的印象很深也是为什么我一直认为处理稀疏奖励任务HER应该排在第一梯队去尝试。这篇文章不打算泛泛介绍概念而是围绕HER的原理、实现细节和实战踩坑展开给正在做机器人控制、仿真环境、goal-conditioned任务或者被稀疏奖励折磨的读者一份可以照着改的参考。后面涉及DDPG但思路同样适用于TD3和SAC核心逻辑是通用的。1.1 一次抓取失败的轨迹反而成了最有价值的训练数据假设一个抓取任务机械臂需要把桌面上的物体推到指定目标位置目标点是每次reset时随机生成的。环境只给稀疏奖励物体与目标足够接近才返回1其他情况一律给0。从直觉出发这段轨迹绝大多数时间是“无效数据”因为reward一直在“失败”。但换个角度想这条轨迹里机械臂的末端、物体、环境中的各种状态一直在发生变化。虽然物体最终没有到达指定目标但它大概率到达过某个位置——比如轨迹中间某个时刻物体停在了一个离起始点不远、但和指定目标完全不同的地方。如果我们把“目标”临时改写成物体实际到达过的那个位置那么这段轨迹中的一部分transition在改写后的目标下就是“成功轨迹”。这就是HER最关键的思想不要浪费已经发生的事实。每一条看似失败的轨迹其实都暗含着“针对某些目标而言的成功经验”。既然机器人已经用某组动作把物体推到了某个位置那说明这组动作在“以该位置为目标”的任务里是有效的。把这个经验用于训练比硬等一次偶然的全局成功要高效得多。生活里也有类似的场景。你教小朋友投篮要求他投进左侧篮筐他用力过猛把球投到了右侧。批评他“目标都没碰到”当然没错但这个信息对下一次改进帮助不大。更好的教学方式是把右侧篮筐当作一个已经完成的目标告诉他“你刚才用这个力度和角度刚好可以把球送到右侧那么下次稍微调整方向就有可能送到左侧”。强化学习里的HER做的正是这件事——从已发生的轨迹中反推“目标”让失败经验变成可学习的成功样本。1.2 稀疏奖励环境下DDPG为什么“学不动”想理解HER的作用得先搞清楚DDPG这类off-policy算法在稀疏奖励下到底卡在哪里。DDPG的核心是actor-critic架构。Critic网络拟合Q函数即“在某个状态下采取某个动作之后预期能拿到多少累积奖励”Actor网络根据状态输出动作优化的方向是让Q值变大。整个过程依赖一个前提Q值对不同动作要有区分度。如果绝大多数transition的reward都是0Q值对所有动作趋于一致Actor拿到的梯度方向就会近似随机噪声更新了等于没更新。Reward shaping是常见解法比如把奖励改为“物体离目标越近奖励越高”。这能提供梯度但需要人工设计距离度量而且很容易引入局部最优——机械臂可能学到“把手伸到物体附近”就不再进步因为继续往目标推的额外收益不够明显。Curriculum learning也是思路之一但它需要精心安排任务难度递增的顺序稍不注意就会让策略忘记之前学到的技能调度本身又是一层额外负担。HER的切入点完全不同。它不改环境、不设计稠密奖励、不做任务难度调度只改“经验回放池里数据的标注方式”。一条轨迹还是那条轨迹reward函数还是那个稀疏函数但通过给transition换一个它实际达成的目标我们可以在不增加任何人工先验的前提下让回放池里出现大量正样本。这也是我后来越来越喜欢它的原因思路简单侵入性小迁移到新环境的时候几乎不用额外调reward设计。2. HER核心思路把失败经验重写成有效训练样本2.1 目标重标注Goal Re-labeling的完整逻辑HER的整个算法流程可以浓缩为一句话在经验回放阶段除了保留原始目标下的transition还要额外构造一批“伪目标”下的transition并重新计算对应的奖励。具体到一个episode假设初始目标是g环境产生的轨迹是s0, a0, s1, a1, s2, a2, ..., sT其中每个transition是(s_t, a_t, r_t, s_{t1})r_t用原始目标g计算。绝大多数r_t等于0因为轨迹没有碰到目标。HER的操作是从这条轨迹里挑一个后续状态s把它当作新的伪目标g然后重新计算这条transition的奖励rt reward_fn(s{t1}, g)。由于g本身就是轨迹实际到过的状态s_{t1}距离g很可能小于成功阈值于是这个transition就从“失败样本”变成了“成功样本”。接下来把新构造的transition也存入回放池训练时把s_t和g拼成一个输入向量喂给网络。关键点在于动作a_t保持不变。因为动作是策略在原始状态下真实执行过的同一个状态-动作对在不同目标下可以有完全不同的奖励标签这在物理上是合理的——同样的“把物体向左推”的动作在以“左侧位置”为目标的任务里就是好动作在以“右侧位置”为目标的任务里就是坏动作。这种多目标视角下的一致性让同一个经验可以被多个任务复用。这里顺便说明一下很多人第一次看HER时的困惑为什么最终学到的是原始目标上的策略而不是一堆伪目标上的策略因为回放池里只有一部分数据被重标注另一部分保留了原始目标g下的transition。Actor在更新时看到的是混合数据既要满足伪目标下的“成功经验”又要兼顾原始目标下的“失败经验”两股信号共同作用策略就会逐渐学会“先靠近物体所在区域再向指定目标推进”的完整行为链。2.2 四种伪目标采样策略future、final、episode、random具体从哪些状态里抽伪目标直接影响训练效果。OpenAI原论文里提出了四种策略我逐个说下区别和适用场景final把一条episode的最终状态直接作为整条轨迹所有transition的伪目标。最简单实现成本最低但每条轨迹只贡献一个“成功方向”。如果轨迹特别长、最终状态离初始状态很远伪目标分布就偏难。future对第t步的transition从它之后的k步状态里随机挑一个作为伪目标。这是原论文里效果最好、也是最推荐的方式。原因在于从s_t到s_{t1}再到s_{t}(t t)这段过程是物理上真实发生过的由t到t这段“未来信息”补出来的transition不仅合法且难度分布刚刚好——伪目标不会太近也不会太远。episode从同一episode的任意位置随机选状态作为伪目标不限定必须在该transition之后。实现和future只差一步但实际效果略差。因为它可能选出“过去某个状态”作为目标而动作序列与目标方向的一致性没有future那么强。random从全局状态分布里随机采一个状态当伪目标。绝大多数随机状态远离轨迹可达区域构造出来的transition难度过高正样本比例极低所以不太推荐单独使用更多是作为补充混合在future里。实际工程中我建议优先选择future策略k取3到5之间。原论文默认是k3我在多个仿真环境里试下来这个范围都很稳。另外伪目标不一定必须来自“同一个episode”但强烈建议这么做——跨episode取目标会让transition在物理上失去连贯性等于人为制造噪声数据。3. 从零实现HER-DDPG代码拆解与关键参数3.1 回放池必须为“整条轨迹”服务普通的Replay Buffer只需要存单条transition但HER要求在训练采样阶段能够访问“当前transition所属的完整轨迹”否则future策略无从实现。所以回放池的设计要分成两层底层按episode存储上层按transition建立索引。一个很实用的简化做法是每收集完一个完整的episode先把原始目标下的transition批量入池训练采样时从缓冲区里拿到一条transition后再判断是否对它做重标注。为了能够随机抽取“未来状态”还需要记录每条transition属于哪条episode、以及它在episode里的序号。下面的代码是一个可用的缓冲区骨架import numpy as np class HERBuffer: def __init__(self, capacity): self.capacity capacity self.trajectories [] # 按episode存每个元素是一整条轨迹 self.index [] # 展平后的 (episode_id, step_id) 索引 def add_episode(self, episode): if len(self.trajectories) self.capacity: self.trajectories.pop(0) self.index [(i, j) for i, ep in enumerate(self.trajectories) for j in range(len(ep))] self.trajectories.append(episode) self.index [(i, j) for i, ep in enumerate(self.trajectories) for j in range(len(ep))] def sample_transition(self, batch_size): idxs np.random.choice(len(self.index), sizebatch_size, replaceFalse) transitions [] for idx in idxs: ep_id, step_id self.index[idx] transitions.append((self.trajectories[ep_id][step_id], ep_id, step_id)) return transitions这里的episode是列表每个元素格式为(obs, action, reward, next_obs, done)。obs在goal-conditioned设定下可以是带goal的完整向量也可以是不带goal的原始状态具体看后续拼接方式。建议存储时把原始状态和goal分开存重标注时再拼接灵活性更高。3.2 网络结构为什么Actor和Critic都要接收goalHER要学的不是“某一个目标下的策略”而是“任意给定目标时都能达成”的条件策略。所以Actor和Critic的输入都要加上goalActor输入state||goal输出actionCritic输入state||goal||action输出Q值。网络规模不用太大两层256个神经元的MLP足够应对大多数仿真控制任务。我用过512甚至1024的隐藏层效果没有显著提升训练还更慢。真正的关键是输入向量拼接后的一致性state里的物体位置、速度和goal里的目标位置必须处于同样的量纲、同样的坐标系否则网络很难学到“当前状态和目标位置之间的距离关系”。Critic在计算Q值时可以把state和goal拼接后一起过第一层action在第二层拼接也可以三者一起拼在输入层。DDPG原始论文推荐action在第二层或第三层并入理由是这样能减少第一层输入维度、提高稳定性。HER在这一点上没有特殊要求按DDPG惯例来即可。3.3 训练主循环采集、重标注、更新的完整流程下面是一个简化但完整的训练流程以DDPG作为底层算法重点展示HER相关的部分import torch import torch.nn.functional as F # 假设已有 actor, critic, target_actor, target_critic # 以及对应的 optimizer def compute_reward(next_state, goal, threshold0.05): dist torch.norm(next_state - goal, dim-1) return (dist threshold).float() def rollout_one_episode(env, actor): episode [] obs env.reset() for step in range(env.max_steps): obs_tensor torch.tensor(obs, dtypetorch.float32).unsqueeze(0) action actor(obs_tensor).detach().cpu().numpy().squeeze(0) action np.clip(action np.random.normal(0, 0.1, sizeaction.shape), env.action_space.low, env.action_space.high) next_obs, reward, done, info env.step(action) episode.append((obs, action, reward, next_obs, done)) obs next_obs if done: break return episode def her_relabel(transition, episode, step_id, k3, threshold0.05): # 这里默认transition是(obs, action, reward, next_obs, done) obs, action, _, next_obs, done transition # 注意未来状态会在next_obs之后从目标空间取状态 future_start step_id 1 future_end min(step_id k 1, len(episode)) future_idx np.random.randint(future_start, future_end) new_goal episode[future_idx][3][:goal_dim] # 取未来next_obs中的目标相关部分 new_reward compute_reward(next_obs[:goal_dim], new_goal, threshold) return obs, action, new_reward, next_obs, new_goal, done buffer HERBuffer(100000) relabel_ratio 0.8 k 3 for episode_idx in range(5000): episode rollout_one_episode(env, actor) buffer.add_episode(episode) # 收集完一条轨迹后做若干次训练更新 for _ in range(50): transitions buffer.sample_transition(batch_size256) batch_obs, batch_action, batch_reward, batch_next_obs, batch_goal, batch_done [], [], [], [], [], [] for (trans, ep_id, step_id) in transitions: if np.random.rand() relabel_ratio: obs, action, reward, next_obs, goal, done her_relabel(trans, episode, step_id, k) else: obs, action, reward, next_obs, done trans goal obs[goal_start:goal_end] batch_obs.append(obs); batch_action.append(action) batch_reward.append(reward); batch_next_obs.append(next_obs) batch_goal.append(goal); batch_done.append(done) # 将state和goal拼接后送入actor/critic更新 # 细节省略loss计算、梯度更新、target网络软更新几个容易写错的地方future状态必须在next_obs中提取目标而不是从当前obs提取从future状态里提取目标时要确保取的是“目标空间对应的维度”如果state里还包含关节角度、速度等其他信息不能全拼上去。新的reward要基于next_obs和伪目标计算而不是基于未来状态本身——因为在当前transition里真正到达的状态是next_obs。3.4 归一化、回放比例和更新次数这些细节决定成败HER看起来简单但实际跑起来很多“学不动”的问题都出在细节上。第一个是输入归一化。伪目标来自轨迹中的未来状态目标点的分布范围可能横跨整个状态空间如果直接拼原始数值进网络Critic很容易被大的量纲差异带偏。我习惯在buffer里维护每个维度的running mean和std训练前对state和goal分别做标准化。注意不是对整个拼接向量统一标准化而是先分别归一化再拼接因为state和goal在语义上是两个不同来源。第二个是回放比例。relabel_ratio取0.8是原论文和大量复现的常见选择也就是80%的transition会被重标注20%保留原始目标。如果全部重标注策略会完全围绕伪目标学习评估时在原始目标上几乎没有成功率如果保留比例过高又回到了稀疏奖励的老问题。实操中这个参数我建议在0.7到0.9之间调先固定0.8跑通再观察原始目标成功率的变化来决定是否调整。第三个是每条episode后的更新次数。这个数值直接影响样本效率和训练稳定性。FetchReach这种简单推动任务每条轨迹后做30到50次更新就够FetchPush、FetchSlide这类更复杂的任务100次左右更稳。更新次数太多会导致同一个batch的数据被反复学习过拟合到当前轨迹太少又浪费了HER构造出的丰富正样本。我一般从小往大加先50次如果Q值和成功率都涨得稳就不用加如果训练波动大再加到80到100次。4. 实测下来最容易踩的坑以及排查技巧4.1 伪目标不是随便选的语义合法性是底线HER有个隐含假设新目标必须在环境语义上是一个“合理目标”。如果环境里定义的目标是“物体目标位置”伪目标就必须从物体状态中采样不能把机械臂末端位置当成伪目标——即使它能造出一批高奖励transition这些transition在原始任务语义上毫无意义训练出来的策略会充满“假动作”。我踩过一次这样的坑一个机器人操作环境里状态向量包含物体位置、末端执行器位置和关节角度。我把state的所有维度都当作潜在goal空间直接future采样结果训练出来的策略看起来“很忙”评估时成功率几乎为零。后来发现伪目标经常落在末端位置上而reward函数只关心物体位置很多被重标注成“成功”的transition实际离真实目标十万八千里。解决办法是把goal的维度严格限制在reward函数关心的那部分状态上。4.2 当HER“学不动”时的诊断顺序如果训练了一段epoch原始目标下的成功率始终为0不要急着加网络规模或者改奖励先按下面顺序排查检查伪目标分布是否太简单。如果k值取太大伪目标离当前状态过近学到的策略可能只会做小幅修正无法覆盖远距离任务。把k调小或者观察重标注后正样本的比例是否过高。检查Critic的Q值是否在提升。如果Q值持续不涨问题大概率出在归一化或目标维度拼接上和HER没有直接关系。检查原始目标占比是否过低。relabel_ratio如果设到0.95以上原始目标下的数据太少actor的策略会逐渐“忘记”真实任务需要用原始目标下的transition做验证集来监控。检查reward阈值是否标注正确。HER的正样本完全依赖伪目标与next_obs的距离阈值设得太大伪目标随便选都是“成功”训练信号失真阈值设得太小重标注后几乎没有正样本等于没用HER。4.3 常见问题与解决办法速查表很多问题其实是同一类原因的不同表现形式我整理了一张表方便排查时快速对照。现象可能原因解决办法训练很久原始目标成功率一直为0relabel比例过高或未来状态采样导致伪目标过近把relabel_ratio降到0.75~0.8k值降到2~3Q值随着训练不断上涨但策略评估效果很差伪目标空间与真实目标空间不一致检查伪目标是否来自合法goal维度排除非目标状态字段训练前期有效果后期开始波动更新次数过多同一轨迹被反复学习降低每个episode后的训练步数或增大buffer容量在图像输入场景下效果显著变差HER对目标表示的质量非常敏感图像直接拼进网络干扰大先用state-based表示或把图像编码为低维latent goal后再重标注策略只会在目标附近小幅移动无法完成远距离任务k值过大伪目标“太容易”k设为3左右并适当提高原始目标transition在batch中的占比还有一个容易被忽略的点HER和curriculum learning经常被放在一起比较但二者解决的不是同一个问题。Curriculum执行时改变的是“训练任务的难度分布”HER改变的是“经验在回放时的标注方式”。两者可以结合但不能互相替代。如果你手上已经有现成的curriculum调度直接再叠一层HER通常也能work如果你以为HER会自动降低任务难度那大概率会失望。5. 我个人做稀疏奖励任务的一点经验玩了好几年这类稀疏奖励任务我现在的习惯是拿到新环境后第一件事不是设计复杂的dense reward而是先看能不能把任务改写成goal-conditioned形式。只要能明确说出“什么样的状态算完成任务”就能套HER。先按relabel_ratio0.8、future策略、k3、每条episode后训练50次的默认配置跑通然后再根据三个指标判断下一步调参方向回放池中正样本占比、原始目标下的评估成功率、Critic的Q值稳定性。这三个指标能对上说明HER的机制已经生效了接下来再考虑叠加TD3、SAC或者其他探索策略。如果对不上就先回到第4节去排查多半是目标维度、归一化或重标注比例的问题而不是算法本身的缺陷。我自己踩过最深的一个坑就是太早怀疑算法结果每次都是细节写错尤其是伪目标采样维度那一类错误光检查代码很难发现最后都是靠打印重标注后的transition、人工看几个样本才定位出来。所以如果训练表现异常先别急着改算法打几条重标注后的样本出来看看比猜什么都快。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →