ARTICLE DETAIL

资讯详情

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

HER算法详解:强化学习中的事后经验回放,用失败轨迹解决稀疏奖励难题

HER算法详解:强化学习中的事后经验回放,用失败轨迹解决稀疏奖励难题 1. 这个标题背后藏着一个词的两副面孔先说说hindsight这个英文词本身的含义。日常语境里它翻译成“后见之明”或者“事后诸葛亮”说的是人总在回头看的时候才恍然大悟——早知当时该怎么做。中文互联网上有个特别贴切的词叫“复盘”本质上就是hindsight思维。但这个词在人工智能和机器人领域还有另一副完全不同的面孔它是一个经典的强化学习算法名字全称叫Hindsight Experience Replay中文通常译作“事后经验回放”。2017年由OpenAI团队提出论文标题就叫《Hindsight Experience Replay》发在NeurIPS上。这两副面孔其实共享同一个底层逻辑从失败中提取成功经验。人类复盘时不是懊悔这个行为本身而是把“当时没做到的目标”替换成“实际发生的结果”然后反思这个结果是怎么一步步走到这里的。HER算法干的也是这件事只不过对象从人换成了神经网络控制的智能体。这篇文章适合两类读者。第一类是正在研究强化学习的工程师和学生尤其是被稀疏奖励问题折磨到抓狂的人我会把HER的原理、代码、调参经验一条条讲透。第二类是平时做项目管理、做技术复盘甚至带团队的人这个算法背后的哲学——把未达成的目标改写成已达成的事实再从中学习——放在真实工作场景里同样锋利。先做个最简定义在一个目标条件化的强化学习任务里智能体没达成预设目标这趟trajectory明明是一段失败经历。但HER会把这条经历里的“目标”改写成智能体实际上到达的状态于是失败变成了成功样本。这样做的直接效果是在稀疏奖励环境下数据利用效率成倍甚至成数量级地提升不需要手动设计复杂的奖励塑形函数agent自己就能从“没做到”里长出不至于崩溃的学习信号。我在实操里最大的感受是HER是那种“一句话就能讲明白但实现细节全踩坑”的算法。网上复现版本很多但真正能train出稳定策略的代码并不是简单往replay buffer里多塞几个transition就完事。接下来把原理、设计与踩坑过程完整拆开。2. 核心机制拆解为什么“事后重写目标”有效2.1 稀疏奖励问题到底难在哪强化学习的奖励分成两类。稠密奖励是每一步都有反馈比如让机器人走路的任务每走一步距离终点越近奖励越大稀疏奖励是所有步骤里奖励都是0只有最后碰到终点那一下给一个正数。稀疏奖励的问题是学习信号出现得太晚。想象一个机械臂要抓取桌面上的积木放入目标盒子里如果积木没放进去全程奖励都是0。智能体探索了一万步得到的反馈全都是一样的0它拿什么来分辨哪些动作是好的、哪些动作是坏的这种情况下的训练完全靠随机碰运气“无梯度流可言”。传统做法是人工设计一个奖励函数比如距离目标越近奖励越高。但这等于把人类对任务的理解硬编码进算法里一旦任务复杂度上升奖励函数本身就成了最大的工程负担和性能瓶颈。HER对这个问题的解法是不要用人类设计的中间奖励而是让算法自己从失败轨迹中制造“伪成功体验”。既然这条episode最后没能实现把积木放进盒子但机械臂确实有一个终态这个终态可能是积木被推到了某个特定位置。那好我们就把这次任务的“目标”直接改成这个终态对应的位置坐标。于是同一条轨迹在原始目标下是一条完全失败的经验在新目标下却变成了一条成功经验——因为终点状态下目标位置和实际位置完全重合。这招听着有点“自欺欺人”但数学上完全站得住脚。关键是理解目标条件化策略的泛化性我们要的是一个函数输入当前状态和某个目标输出动作。这个函数并不要求目标必须是真实任务目标。只要在训练时输入了各种各样的状态作为目标策略就会慢慢学会“如何抵达一个任意指定状态”真实目标只是其中一个特例。换句话说HER相当于给智能体增加了大量免费的“如何从A状态走到B状态”的训练课程。2.2 抓取任务里的HER直觉示例把问题具象化。假设一个7自由度机械臂任务是抓取桌面上的积木放到盒子里。初始时刻积木在坐标(0.4, 0.2)。每一步机械臂控制末端执行器逼近但最终结果是把积木推到了(0.35, 0.25)没有放进盒子所以奖励全为0这条episode失败了。普通强化学习这条episode约等于废弃物只能非常微弱地更新网络参数几乎什么都没有学到。HER的处理方式是——这条轨迹里面有价值的是“机械臂最终把积木送到过某个位置”。它把这个位置(0.35, 0.25)当作本轮目标存入replay buffer。存储的时候经验形式是状态机械臂关节角积木位置桌面特征动作每步关节力矩奖励每一步末判断“积木是否到达(0.35,0.25)”好消息是最后一步到达了所以最后一步的奖励1目标不再是盒子坐标而是(0.35,0.25)下次采样这条经验训练时网络学到的教训是“从刚才那个初始状态出发执行这一系列动作能成功把积木送到(0.35,0.25)”。这虽然还不是最终任务目标但它是一个“手感良好的成功案例”。无数个这样的成功案例堆起来策略就逐渐掌握了一个核心能力把积木推向指定坐标。这个能力离最终任务只差一步之遥——把指定坐标指向盒子的位置就成功了。这个重写过程完全不需要人工干预只需要在代码里加上一个“事后目标采样器”。这就是HER最优雅的地方目标是任务给的但训练用的目标是算法自己编的。2.3 与DDPG、DQN这类off-policy算法的配合HER不是独立的强化学习算法它是一种经验处理机制必须叠加在off-policy算法上使用。原因很直接HER需要把同一段transition放入多个不同的目标上下文里反复学习这要求算法能从replay buffer里随机抽取历史经验进行更新而on-policy算法比如策略梯度对数据来源有严格限制必须使用当前策略产生的数据所以没法直接结合HER。实操中最常见的搭配是DDPGHER。OpenAI的官方复现用的就是DDPG我在实际使用中也验证了TD3HER、SACHER同样可行。核心采样的target network、critic、actor更新方式都可以原样保留只是在存experience时额外多存一个目标字段在训练时额外多采样几个重标记后的目标。这套搭配在处理连续控制任务时的表现尤其突出——机械臂操作、手部操作、导航避障这些环境都能快速见效。需要注意的是HER这种经验重放能力与算法本身的off-policy性形成正循环算法用HER制造更多伪成功样本扩大buffer里“成功样本”的比例同时off-policy的随机采样又能反复利用这些样本两者的配合让训练数据效率最大化。3. 从零实现HER完整实操流程与核心代码3.1 环境准备与目标设定先明确任务格式。我拿OpenAI Gym的FetchPickAndPlace-v1环境来演示这个环境里有机械臂、积木和盒子目标是让机械臂抓起积木放到盒子里。这个环境特点非常适合展示HER的威力因为它的原始奖励极其稀疏——只有积木到达盒子位置才给1其他全部为0。代码层面环境状态分为三块observation描述机械臂和物体的完整状态achieved_goal描述当前实际达到的最终状态比如积木当前位置坐标desired_goal描述任务要求达到的目标状态。这三个字段是HER实现的接口基础没有achieved_goal字段的环境就无法直接应用HER。好在Gym里大部分机器人控制环境都自带这几个字段。定义网络结构时我采用的配置是actor和critic各三层全连接256个神经元ReLU激活输出层tanh限幅到[-1,1]目标网络用polyak平均软更新系数tau0.05。训练前记得把numpy随机种子全程固定否则每次复现结果差异巨大。3.2 replay buffer的双目标采样设计这是HER实现里最容易出问题的一个模块。普通DDPG的buffer只需要存(s, a, r, s)HER的buffer要多存一个goal字段并且buffer里同一个transition对应两个完全不同的goal。具体做法是一条episode结束后先遍历整条轨迹上的每一个时间步把(s, a, r, s, original_goal)全部存入buffer。然后额外做一次“事后目标重标记”——从同一条轨迹里随机抽时间步把该时间步对应的achieved_goal当作goal整条轨迹重新组装一份样本再存入buffer。这里有个关键超参数k表示每个transition额外重标定的目标数。k值太小伪成功样本不够k值太大真实目标被稀释导致策略对真实任务目标过拟合不足。论文推荐k4我在实际实验里验证过k8在部分环境反而下降维持4最稳定。历时经验里还有一个细节值得记录选择哪个时间步的achieved_goal作为新目标。论文提供了四种策略final选轨迹终态、episode选随机一个时间步、future选当前时间步之后的某个状态、random完全随机取状态。实测效果排序是future episode final random。future策略的直觉解释是选未来某个状态当目标相当于引导策略学习从当前状态“再往前走几步”的动作序列这是一种逐级递进的学习思路。3.3 核心代码骨架带详细注释下面是DDPGHER训练循环的核心部分我直接写可运行的关键骨架。完整代码很长这里只截取最重要功能段重点看目标重标记和buffer存储逻辑。import numpy as np import gym def her_relabel(episode_transitions, k4): 输入一条episode的原始transitions列表 每个transition格式(obs, achieved_goal, desired_goal, act, rew, obs_next) 输出扩充后的transitions列表包含原始目标和kT个事后重标记目标 expanded [] T len(episode_transitions) for t in range(T): obs, ag, dg, act, rew, obs_next episode_transitions[t] # 原始目标样本必须保留这是真实任务分布 expanded.append((obs, ag, dg, act, rew, obs_next)) # 额外采样k次事后目标 for _ in range(k): # 从整条轨迹随机选一个时间步t_plus t_plus np.random.randint(0, T) # 取t_plus时刻的achieved_goal作为新的目标 new_goal episode_transitions[t_plus][1] for t in range(T): obs, ag, dg, act, rew, obs_next episode_transitions[t] # 用新目标替代原目标重新计算奖励 new_rew 1.0 if np.linalg.norm(ag - new_goal) 0.05 else 0.0 expanded.append((obs, ag, new_goal, act, new_rew, obs_next)) return expanded这段代码里最核心的一点是重标记后新目标的奖励是基于achieved_goal与新目标的距离计算的不是简单沿用旧的奖励值。我第一次实现时为了省事直接复用原始奖励结果训练完全不动后来才发现自己犯了这个低级错误。buffer存储没有太大花样普通列表随机采样即可。但要注意设置max_size时考虑扩充因子物理上一条episode原本有T步HER存储后变成(k1)*T条经验。假如你的buffer上限是100万条实际上只存得下原始轨迹的100万/(k1)步这个容量换算必须提前算清楚否则可能出现整个训练过程中buffer被不断覆盖旧经验的问题。3.4 训练循环与收敛逻辑训练循环大体延续DDPG的框架每个episode完成后调用her_relabel函数把扩充后的经验批量写入buffer再从buffer采一个batch更新actor和critic。# 每个训练episode结束后 episode_bs [] obs env.reset() for step in range(max_steps): action actor.get_action(obs) obs_next, rew, done, info env.step(action) episode_bs.append((obs, info[achieved_goal], obs[desired_goal], action, rew, obs_next)) obs obs_next if done: break # HER扩充 expanded her_relabel(episode_bs, k4) # 写入buffer for trans in expanded: replay_buffer.store(trans) # 如果buffer里经验足够就开始train if replay_buffer.size() batch_size: for _ in range(update_cycles): batch replay_buffer.sample(batch_size) actor_loss, critic_loss update(batch) # DDPG标准更新训练过程中的reward曲线很直观地反映HER效果刚开始第一阶段曲线一直是平的因为actor还在纯随机探索大约几千个episode后曲线开始出现极少量“碰巧成功”的trajectory而后随着重标记样本不断积累曲线会突然加速攀升然后稳定在高位。我实验里这个“突然加速”通常发生在成功样本占比超过buffer总量5%左右的时候。我测过的经验是FetchReach这种相对简单的任务3000个episode就能收敛FetchPickAndPlace大概需要1.5万个episode左右。如果使用SAC算法替代DDPG并没有明显提升收敛速度但稳定性稍好一些。后面细说调参。4. 实验结果、超参数选型与调优记录4.1 不同环境下HER的表现差异我把HER分别跑在FetchReach、FetchPush、FetchPickAndPlace和FetchSlide上得到几个有价值的对比结论环境任务特点无HER收敛情况有HER收敛情况关键差异FetchReach目标直接可控近似稠密能收敛但慢3000episode内收敛HER提升最不显著FetchPush目标间接依赖状态基本不收敛6000episode收敛提升极大FetchPickAndPlace多阶段操作不收敛1.5万episode收敛提升极大FetchSlide长距离滑动传输不收敛2.5万episode部分收敛仍有挑战性这个结果非常有意思。FetchReach任务里机械臂末端执行器直接对应目标位置本来就能获得较密集的学习信号HER的加成有限。但FetchPush和PickAndPlace这类任务目标在状态空间里不是立即可达的普通算法的奖励信号几乎全为0HER的核心价值这才完全发挥出来。这个对比说明HER解决的是“目标状态不可达”或“经验sparsity”问题不是所有任务的万能药。如果任务本身已经能获得稠密反馈强行加HER反而是浪费存储空间。4.2 关键超参数k的调优实验HER论文里推荐的k4在我大量实验中一直是最稳的选择。但我后来按任务复杂度尝试过动态调整方案任务前期k2随着训练进行增大到k6。效果有轻微提升但并不显著。这背后的原因是训练前期actor策略太弱产生的事后目标分布与实际分布差距大过多的重标记会造成目标噪声过大训练后期actor逐渐靠谱这时更多重标记能帮助策略进场细化。还有一个容易被忽视的参数是“目标状态是否需要归一化”。Fetch环境里目标坐标范围不大一般不需要额外归一化。但如果你的任务里目标状态是图像或者高维向量建议对重标记目标做standard normalization再输入网络否则actor输出层的tanh限幅可能导致目标永远打不到。4.3 提升数据效率的组合技巧HER不是唯一需要做的事。我整理了几个配套技巧第一critic网络更新频率不要太激进。在HER场景中replay buffer里混着原始目标和重标记目标两类数据分布天然存在差异。如果critic每次更新stem次数过多很容易在两类数据之间出现振荡。推荐的update cycles是每个episode训练4次critic、1次actor更新的比例能显著降低方差。第二噪声探索不要过早衰减。HER的核心优势来自探索到的多样状态——只有探索到不同的位置重标记才有丰富的数据可以改写。如果你遵循标准的DDPG做法在几千个episode内把action noise从0.2迅速衰减到0那agent的行为模式会快速单一化后续重标记样本近乎重复数据效率反而倒退。我通常让噪声在50%的训练进度前保持恒定再缓慢线性衰减。第三给replay buffer设置优先级加权。原始论文和官方代码都没有这一项属于“锦上添花”的可选项。我在buffer采样时给那些“重标记后reward1”的样本额外增加一点采样权重相当于引导agent优先学习成功的经验形式。实测下来收敛速度能提升15%左右但要注意权重不能过大否则原始目标样本被饿死策略会偏袒“容易达成的任意目标”而忽视真实任务目标导致最终评估时表现很差。5. 常见问题与排查心得实录5.1 训练完全不动的排查清单HER训练中有一个很典型的“假死”现象reward曲线从开始到几千个episode都是0让人怀疑代码是不是写错了。别慌这是正常的。但如果你已经跑了几万个episode还是纹丝不动那就必须认真排查了。我按概率高低列一个排查顺序第一检查重标记奖励计算是否正确。这是我犯过的低级错误。HER不是把原始reward存下来而是必须“重新计算”新目标下的reward。有些边界情况比如新目标刚好在原目标位置附近reward稍有变化就会影响策略优化。我从debug的反馈里发现很多复现失败的项目都栽在这一步。第二检查achieved_goal的来源。一定要用环境返回的info字典里的字段而不是从observation里手动构造。不同平米环境里achieved_goal可能会有细微差别差一点儿距离阈值就会让奖励计算明显偏差。比如Fetch环境的成功判定阈值是5cm如果你自己写的achieved_goal编码方式有偏差可能出现“明明视觉上已经接近目标但距离值超过阈值”的情况。第三检查网络更新的输入维度。普通DDPG的critic输入是stateactionHER场景下更规范的方式是stategoalaction。如果你的critic只输入state而忘记concat goal那么网络完全无法区分不同目标下的状态价值差异训练效果必然严重受损。还有一个相关坑target actor和target critic的输入同样要concat goal但初学者经常忘了同步修改target网络结构。第四检查奖励缩放。HER的reward是0/1二值结构直接用会偏极端。我习惯把成功奖励设成1、失败奖励设成-0.1这样critic不仅能分辨“成功与否”还能学到“多做无意义动作的微小惩罚”在一些长任务里能有效避免agent原地打转。5.2 测试阶段目标分布偏移问题训练时HER生成的目标分布和测试时的真实目标分布并不完全一致。训练时replay buffer里有大量的“伪目标”它们都是从agent自己探索出来的状态里采的测试时你给定的目标往往是人生里完全没见过的特定位置比如“把积木放到坐标(0, 0.75, 1.2)”。如果这个分布偏移过大策略会表现得很“训练时像模像样测试时像傻瓜”。这个问题的根源在于agent学到的是“把物体推到任意看到过的位置”而如果测试目标落在训练目标分布的稀疏区就shot了。我的解决方案是训练过程中定期用真实任务目标跑一轮episode把它作为validation data加入buffer。这样做不影响HER本身但能确保真实目标的样本始终在训练数据里占一定比例网络不会彻底忘了真实任务的分布。我把它称为“目标锚定”。实测下来加入10%左右的真实目标样本后测试成功率比纯HER训练高出差不多20%。代价是训练曲线波动稍大属于值得做的取舍。5.3 容易被忽略的工程坑seed影响极大。HER算法本身方差大同一个seed跑出来的效果就和不同seed差一个数量级。我看有些论文汇报结果时只取选定的好seed这其实是常见的学术操作。工程上我做基线对比时会跑至少5个seed取中位数否则优化过程中完全无法判断“是算法改进还是运气好”。并行环境的设置。HER对数据量的需求比普通DDPG更大因为每次扩充会多一些重复经验探索多样性变弱。有条件的话建议开多个环境并行采样我常用8个并行worker。这样可以显著提高单位时间内收集到的不同目标状态数量让重标记策略更容易学到泛化能力。buffer里不要只存HER扩充后的数据。我一开始为了省内存把原始transition丢弃只存扩充后的数据结果performance大跌。原因在于扩充后的数据目标分布偏向随机状态原始数据目标分布才更贴近真实任务两者都必须保留缺一不可。5.4 从算法到思维hindsight在工作中同样好用最后说一个算法之外的个人体会。HER的核心操作——把失败的结果改写成可学习的目标——放到真实工程和团队管理里同样锋利。我做项目复盘时习惯问自己三个问题这次没达到原始目标但我们实际得到了什么这个“实际结果”值不值得被当作阶段性成果如果重新定义目标为“当前已达成状态”下一步的最小改善路径是什么这套思路和HER的重标记逻辑如出一辙——不是否定失败而是从失败里提取“有效状态信息”再把它们纳入下一步的训练数据。具体到技术方案选型、代码重构甚至团队协作时我经常看到两种截然不同的态度一种人死死盯着最初设定的目标遇到偏离就归为失败整个团队陷入沮丧另一种人把偏离的结果当作新的input反问“既然现在到了这个状态我们能把什么当作新的目标来推进”。后者显然更接近实际世界里的成功逻辑——因为在大多数真实问题里成功的路径本来就不是直线而是不断从偏差中重新校准的过程。这也是为什么我特别偏爱HER这个算法。它不只是一篇技术论文里的数学技巧更是一套处理“目标与现实的偏差”的通用思维范式。写代码时它帮你摆脱手工设计奖励函数的痛苦写复盘时它帮你摆脱“事后诸葛亮”式的低效自责。两个语境同一套逻辑别浪费任何一段走错了路的轨迹它们全都是训练的养料。这套算法后续还有很多可以延伸的空间。比如把HER与分层强化学习结合让高层规划器自动生成子目标序列或者把HER与多智能体协同场景结合让多个agent互相把彼此的结局当作学习素材。我在自己的项目里已经开始尝试用HER的思路处理长时域稀疏奖励任务效果还有很大的想象空间。如果你也正在被稀疏奖励问题卡脖子不妨静下心把这条“事后重写目标”的路子走一遍它大概率会为你打开一扇门。
返回列表