ARTICLE DETAIL

资讯详情

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

HER算法实战:用事后经验重标注破解强化学习稀疏奖励难题

HER算法实战:用事后经验重标注破解强化学习稀疏奖励难题 做强化学习这一年多我遇到最多的问题不是算法不收敛而是算法在稀疏奖励环境下连“学”的机会都没有。我印象很深的一次是让一个机械臂仿真环境去推一块滑块到指定位置奖励只在最后一步给结果智能体从头到尾都在原地踏步每次探索得到的都是“0分”完全没有梯度可言。后来我接触了一个项目叫“hindsight”它的思路彻底改变了我处理这类问题的方式——与其让智能体在稀疏奖励里大海捞针不如把“失败的轨迹”也变成有效的学习信号。这个想法说起来一句话做起来却有一套完整的机制要打通。这篇我就把它掰开揉碎原理、代码、踩坑全部摊开来讲。1. 项目背景稀疏奖励到底难在哪1.1 为什么“最后给1分”的任务让智能体寸步难行假设我给你一个任务把魔方还原成六个面。你可以在任何一步停下来看一眼当前状态然后决定下一步怎么转。但是评判标准只有一条——完全还原给1分否则给0分。如果你从来没有接触过魔方完全靠随机尝试你有多大概率在几十步内碰巧还原答案几乎为0。强化学习里的稀疏奖励问题就是这样它的奖励信号不是没有而是太稀、太远、太难以触碰。在稀疏奖励环境下智能体每一步都收到相同的“0”意味着它对每个动作的优劣没有任何感知策略梯度算出来全是“因为所有动作回报都一样所以谁都不比谁强”。这会导致两个直接后果一是探索完全失效智能体很快就会趋同于某一个固定动作通常是随机初始化后表现稍好的那个二是就算偶尔撞上了目标这个成功经验也淹没在大量无效样本里学习信号弱到可以忽略不计。我在实际跑实验时感受特别明显用DDPG在稠密奖励的reach任务上几千步就能看到一个像样的抓取动作换成稀疏奖励后跑了几十万步损失函数纹丝不动好像练的是一个“预判谁先眨眼”的玄学任务。传统解决方案无非是奖励塑形reward shaping人为给接近目标的状态铺设一个连续的奖励斜坡。但塑形函数怎么设计离目标近10厘米给0.1分近5厘米给0.5分——这个数字是人拍脑袋想出来的一旦设计得不合理智能体会学会“钻空子”绕着奖励高的区域来回晃反而忽略了真正的任务。1.2 hindsight项目的核心启发从失败里榨取“事后经验”我第一次看到“hindsight”这个项目时第一反应是这名字起得真好。它指的是人类独有的“事后聪明”倾向——你曾经失败过的事情如果事后回看总能发现自己当时“本可以”做得更好。强化学习里的hindsight把这种经验迁移变成了一种算法机制它的核心思想用一句话概括就是轨迹没达成预定的目标没关系我们可以把这条轨迹重新解释成“达成了某个实际达成的状态”的成功轨迹然后让它成为学习信号。举个例子。目标是让机械臂抓到桌上的红球但五次尝试里它只抓到了旁边的蓝球。传统强化学习把这五次轨迹全部抛弃因为它们没拿到“成功”的1分。hindsight的做法是完全不一样的它把这五次轨迹中的前四次重标成“目标是蓝球我抓到了蓝球”的成功轨迹然后让智能体从中学习“怎么抓蓝球”的基本功。你有没有发现抓红球和抓蓝球的底层技能是高度共享的——都要伸手、对准、合拢、抬起。通过把失败轨迹重标为“另一个可得目标”的成功经验智能体相当于在一个“够得着的课程”里不断练习上身肌肉而不是直接扔到“抓红球”的终局考试里干瞪眼。这个思路的价值在于它不动环境、不改奖励函数、不添加任何人工先验只改数据的存储和读取方式就硬生生把稀疏奖励问题转换成了一个相对稠密的学习问题。这在实际工程里是极为难得的“低成本高收益”方法——你不需要重新设计奖励函数不需要费劲做课程学习只需要在经验回放的环节动动手脚。2. Hindsight的核心机制目标重标注到底做了什么2.1 一次重标注的完整流程拆解要实现hindsight先要把一个原本没有目标概念的轨迹“翻译”成带目标的形式。这里我以最简单的位翻转bit flipping环境为例讲清楚整个流程你跟着走一遍就明白了。假设状态空间是一个长度为5的二进制串比如[0, 1, 0, 1, 0]目标是一个同样长度的二进制串例如[1, 1, 0, 1, 1]。每一步智能体可以翻转其中一个比特位只有当当前状态和目标完全一致时奖励才为1其余为0。这个环境麻雀虽小但五脏俱全它具备了稀疏奖励的全部特征。完整的一次经验收集过程如下采样一个目标 g比如[1, 1, 0, 1, 1]初始化状态 s0比如全0串。智能体根据当前策略选择动作每一步动作对应翻转一个位得到状态转移序列 s0 → s1 → s2 → … → sT直到回合结束。如果最终 sT 恰好等于 g这一回合的奖励就是1但绝大多数情况下 sT ≠ g奖励是0。传统方法到这里就把这一整条轨迹丢进回放缓冲区然后等着下一条。hindsight在这里插入了一步关键的“二次解释”它重新看一眼这条轨迹最终到达的状态 sT——比如[0, 1, 1, 0, 0]——然后说“好我现在把这整条轨迹的目标字段从[1, 1, 0, 1, 1]改写成[0, 1, 1, 0, 0]。” 改写完之后这条轨迹就从一个“失败的尝试”变成了一条“成功完成目标[0, 1, 1, 0, 0]的教科书式轨迹”。关键来了这个重写后的轨迹和改写前的轨迹在状态转移、动作序列、每一步的观测上没有任何区别唯一变的是“目标”这个字段以及随之重算的奖励。因为最终状态确实等于新目标所以这条轨迹的最后一个奖励从0变成了1中间步的奖励也要按照“是否达到新目标”重新判断如果中间某一步已经等于新目标那一步的奖励也是1。2.2 为什么“改目标”不等于“造假”很多人第一次听到这里会皱眉头这不是在骗算法吗目标都没达到硬说达到了智能体不是在学一个假任务其实不是。这里面的关键认知是强化学习里的“目标”并不是上帝颁布的终极命令它更像是一个“数据标签”。一条轨迹(s0, a0, r0, s1, a1, r1, …, sT)本身是环境的真实反馈物理过程是真的、状态转移是真的、动作序列是真的。但是“它是在朝哪个目标努力”这件事是训练者贴上去的解释。既然轨迹真实地到达过 sT那么把这条轨迹作为“成功达成 sT 目标的示范”没有任何造假的成分——它确实达成了这个目标只是它本来不想达成而已。这就像你在练投篮十次投三分都没进但其中有一次突破上篮进了。你当然不能说“三分球练成了”但你可以从这次上篮里学到“运球、起步、瞄准篮筐、出手”的整套动作感受。这些基本功迁移到三分投篮上是真实有效的。hindsight做的就是这个事——它不篡改物理结果它只是把同一段经历放到不同的学习课题里去榨取价值。更严谨一点说hindsight在数学上并没有改写环境动力学它只是改变了每个transition里goal这个条件变量的取值然后让策略去学习“在给定各种goal下的通用控制策略”。最终你得到的策略 π(a|s, g)输入的是一个状态和一个目标输出的是一个动作。这个策略学到的是“无论你想让我达成什么目标我都能根据当前状态朝它逼近”——这才是真正通用的目标导向策略。2.3 重标注策略选哪种final、future、episode、random重标注还有一个“给轨迹选择新目标”的策略问题。你在一条轨迹里可以用最终状态 sT 作为新目标也可以用中途的某个状态 s_{tk} 作为新目标还可以用轨迹之外随机采样的状态。我实测下来这几种策略差别很大我挨个说一下。final策略每一条轨迹都用最终状态重标一次新目标永远是这个回合真正到达的最后一个状态。这个策略最稳定、最简单、零参数但它有一个小毛病如果轨迹特别长中途经历了太多状态用最终状态重标会让中间的那几步在“走向最终状态”这个解释下变得没有意义。future策略对于轨迹中的某个时刻 t从它之后的状态里随机挑一个作为新目标。这个策略是目前公认效果最好的原因在于它天然引入了“目标离当前状态越远越难”的时间差和课程学习的思想暗合。我在机械臂抓取任务里用的就是futureK值取4到8之间效果都不错。episode策略把当前回合最开始的那个初始状态作为新目标。这个策略适合那些“起点和终点同样值得学习”的环境比如回到原点类的任务但通用性不如future。random策略从整个采样的状态分布里随机抽一个状态作为新目标。这个策略最容易引入“根本不可能从当前状态达到的目标”学习效率反而下降。下面按照我自己的经验给一个直观对比不代表参数不可调但大方向是没错的策略实现难度样本效率适用场景我的建议final极低中等短轨迹、易达目标拿来跑通流程很好性能不要指望太高future低高大多数稀疏奖励任务首选K值在4~8之间先试episode低中低返回起点类任务需求特殊才用random极低低几乎不推荐别在这上面浪费时间这个选择不是随便拍的。我自己最开始图省事只用final在bit flipping环境还能跑通但换到机械臂推滑块就明显感觉策略后期停滞。后来换成future K4同样的训练步数成功率直接翻了一倍。所以说正确配置重标注策略比调一百次学习率都重要。3. 从零实现在PyTorch里写一个能跑的HER3.1 环境的选定与套路校验在开始写代码之前我建议你先在一个“简单得不能再简单”的环境里验证你对hindsight的理解是否正确。bit flipping环境是最理想的因为状态和目标都是离散二进制串奖励计算非常简单一眼就能看出问题出在哪里。如果连这个都跑不出效果不要急着上机械臂仿真。我自己常用的套路是这样的用OpenAI Gym的接口风格自己写一个BitFlipEnv状态长度n10每回合随机采样目标最多允许操作50步奖励只在完全匹配时给1——不对准确说是在每一步都判断当前状态是否等于目标等于就给1不等于就给0。为什么每一步都判断因为bit flipping环境里如果某一步恰好翻转到了目标状态这个回合其实已经提前成功了后面再翻就走远了。每一步判断能让成功信号更加准确。环境逻辑很简单但它是整套流程的试金石一个实现正确的HER算法在这个环境里的成功率应该在几百个回合内就明显抬头如果跑了几千回合还是一条直线那你的重标注逻辑、奖励计算、采样策略里一定至少有一个是错的。3.2 回放缓冲区的关键改造数据里必须多存一个goal标准的回放缓冲区里存的是(state, action, reward, next_state, done)五元组。但HER要求我们在训练时能“重新解释”一条轨迹的目标所以缓冲区里必须额外存两个字段这个transition原本的目标goal以及这个transition发生时所属轨迹的标识或者更直接一点整条轨迹的全部状态序列。我这里给一个非常实用的设计给缓冲区存(state, action, reward, next_state, done, goal)同时额外维护一个字典把每条轨迹的id映射到它经历过的所有状态列表。这样当我们要对某条轨迹做future重标时可以直接从这个列表里随机抽一个未来的状态作为新目标。具体的存储逻辑如下伪代码级别但已经是能跑的真逻辑def store_transition(self, trajectory_id, state, action, reward, next_state, done, goal): self.buffer.append((state, action, reward, next_state, done, goal)) if trajectory_id not in self.traj_states: self.traj_states[trajectory_id] [] self.traj_states[trajectory_id].append(next_state) def relabel_and_sample(self, batch_size, k4): transitions random.sample(self.buffer, batch_size // 2) her_transitions [] for trans in transitions: new_goals [] traj_id trans[trajectory_id] states_in_traj self.traj_states[traj_id] for _ in range(k): future_state random.choice(states_in_traj) # future策略 new_goals.append(future_state) for new_goal in new_goals: new_trans trans.copy() new_trans[goal] new_goal new_trans[reward] compute_reward(new_trans[next_state], new_goal) # 这里的done标志也要重算如果next_state new_goal则done为True her_transitions.append(new_trans) return transitions her_transitions请注意这里的奖励重算函数compute_reward它不能跟环境里的奖励函数是两份代码必须是同一个函数的两次调用。我见过很多人在环境里写一份奖励、在重标定时又抄一份结果环境改了奖励规则重标定那边忘了同步整个实验白跑。3.3 HER DDPG采样与训练循环怎么衔接HER本身不是一个独立的强化学习算法它只是一个“数据增强器”必须挂载在一个现成的强化学习算法上。我自己的选择是用DDPG做base因为它天然适合处理连续状态和连续动作而且网络结构简单方便调试。如果你更习惯用SAC也是一样的思路HER的接入方式和策略梯度类算法的耦合点都是一样的——改回放缓冲区不改损失函数。完整的训练循环大概是这样的初始化actor网络、critic网络、目标网络以及一个带HER能力的回放缓冲区。每回合开始随机采样一个goal。智能体在当前策略上加上探索噪声与环境交互直到回合结束。把这一整条轨迹送入回放缓冲区注意要完整保存轨迹状态序列。从缓冲区采样时一半样本用原始goal一半样本用重标定后的goal按future策略重算。正常的DDPG更新critic回归Q值actor用确定性策略梯度更新。定期软更新目标网络。这里有一个我一开始经常做错的细节重标定的样本里done标志也必须重算。如果没重算训练时critic会把这步当成非终止步来bootstrapping等于把不真实的未来回报估计值塞进了目标值里。具体来说如果next_state在重标定后等于新目标那么doneTrueQ_target reward否则doneFalseQ_target reward gamma * Q_target_net(next_state, policy_target_net(next_state, new_goal))。这个区别在稀疏奖励下影响极大。3.4 关键代码重标定奖励与done标志我再给一段极度核心的重标定奖励加done标志的实现这是整个HER里最容易被写错的地方def compute_reward_and_done(state, goal): # 对于位翻转环境完全匹配即成功 done bool(np.array_equal(state, goal)) reward 1.0 if done else 0.0 return reward, done # 重标定时 new_reward, new_done compute_reward_and_done(trans[next_state], new_goal) new_trans { state: trans[state], action: trans[action], reward: new_reward, next_state: trans[next_state], done: new_done, goal: new_goal, }为什么我要单独强调这个因为我曾经在某个项目里偷懒重标定时只改了rewarddone直接用原来的False。结果训练出来的策略表现为“在接近目标时犹犹豫豫不敢迈进最后一步”。原因很快定位到了因为done永远是Falsecritic学到的Q值永远包含未来回报的折现哪怕眼前就是目标它也不敢把当前奖励“拿下”反而继续去追逐一个不存在的未来。那把“已经成功”的时刻判断成“还有后续”在稀疏奖励环境里是灾难性的。切忌重标定奖励时一定要同步重算done标志两处必须来自同一个函数。4. 实操中的坑我踩过的五个典型问题4.1 重标定目标与状态空间的“越界”问题如果你的环境里状态空间和目标空间不是同一个空间HER就必须格外小心。举个例子机械臂的任务目标是“把物体推到坐标点 (x, y)”状态里不仅有推块位置还有机械臂各个关节的角度。那么重标定脚本里目标应该来自物体位置子空间而不是整个状态向量。如果你直接把完整状态[joint_angles..., object_pos...]当成新目标写进transition那么“目标”里包含了很多物体位置之外的冗余信息策略会被这些无用维度干扰学习效果大打折扣。正确的做法是把状态空间划分成“受控部分”和“目标相关部分”重标定时只从目标相关部分抽取新目标。我自己在实现时习惯在环境里定义extract_goal_from_state(state)这样一个固定出口保证抽取逻辑在环境和重标定脚本里完全一致避免两边各写一套抽取规则造成偏差。4.2 HER不是万能药连续奖励环境的收益并不明显HER的逻辑建立在“稀疏奖励、目标可判别”的场景里。如果你的环境本来就有稠密奖励——比如每个动作都能得到一个平滑的距离反馈——那么HER带来的提升通常微乎其微甚至因为重标定引入的“伪目标”扰乱了原本奖励信号反而让性能下降。我的建议是先明确你的任务是否真的是“目标稀疏型”。怎么判断跑一个不加HER的baseline如果成功率能在训练中稳步上升说明奖励信号已经够用了加HER的必要性不大如果baseline完全是一根横线才需要HER上场。我在某个避障导航任务里就吃过这个亏任务本身有稠密的“靠近目标”奖励我固执地加了HER结果重标定出来的目标散落在整个状态空间把原本平滑的奖励地形搅得乱七八糟智能体反而学会了“原地不动”这种消极策略。后来去掉HER用普通的DDPG配一点噪声探索反而成绩更好。4.3 不同重标定策略的收益差距可能是数量级的这一点在原理部分提过但实操里真的值得单独再说一次——同一个任务final和future之间的差距在我做的推送任务里甚至超过了“换不换HER”本身的差距。future策略为什么好我理解它的本质是给“轨迹中每个时刻”都设置了一个“在未来某个状态上成功”的近期目标而不是把整条轨迹都押在遥远的最终状态上。这相当于自动生成了一个课程图谱越靠近轨迹末尾的目标学习难度越低越靠前难度越高。我在调参时一般把K每个transition生成的重标定样本数设在4到8之间。K太小时成功经验的数量不够稀疏奖励的影子还在K太大时缓冲区里伪目标的比重过高真实目标和策略的关联被稀释。K4和K8之间的差别一般在小数点后两位但K1和K4之间的差别往往是“不收敛”和“收敛”的区别。4.4 目标分布偏移重标定导致“目标空间被污染”这是HER长期运行最容易忽略的问题重标定所引入的目标全部来源于智能体自己“实际达到过的状态”。在训练的早期智能体只会达到一些简单的、近端的状态这导致缓冲区里伪目标的分布严重偏向“容易达到的状态”。久而久之策略在这些状态上练得很好但真正的任务目标往往是远端状态——它反而没怎么练过。这个问题的表象是训练中后段成功率涨到某个水平后就不再上升loss也不降了像是撞到了天花板。我在四自由度机械臂抓取实验里成功率长期卡在67%左右用的就是这个问题。后面怎么解决的给重标定样本设置一个“目标距离过滤”——只保留那些与transition原目标有一定距离差但又不至于完全不可达的伪目标保证缓冲区里的目标分布更均匀。换句话说不要让过于简单的伪目标霸占训练样本。4.5 评测时必须用真实目标而不是重标定目标这个坑听起来像是常识但我确确实实见过不止一次有人为了图省事把评测环境里的大规模测试也套用了HER的重标定逻辑导致评测结果虚高。你想想HER重标定的本质是把达不到的目标改成达到了的目标如果评测时也这么做那不等于自己给自己放水吗评测必须以“环境给出的真实目标”为准不允许任何改写。我在团队的代码规范里加了一条硬性规定评测代码单独一个文件不继承任何训练时的重标定逻辑评测过程不写缓冲区不采未来状态每一步只看当前策略在真实目标下的表现。这样才能确保你汇报出来的成功率和社区里的可比基线站在同一条标准线上。5. 常见问题速查表与实战调试建议5.1 问题、原因与解决对照表我把这些年在HER调参过程中遇到的典型现象整理成一张速查表碰到类似症状可以直接对号入座现象可能原因排查与解决训练几百回合成功率纹丝不动重标定逻辑没生效奖励/done没有同步重算先打印回放缓冲区里重标定样本的reward确认存在非0奖励明明重标定了成功率还是很低重标定策略用的finalK值过小换成futureK调到4~8训练中后期成功率停滞伪目标分布偏向简单状态对伪目标做距离过滤让目标分布更均匀动作表现忽快忽慢无法稳定达到目标done标志原封不动导致Q值永远在bootstrapping检查重标定时done是否随目标同步重算在稠密奖励任务里用HER反而更差HER引入了伪目标破坏了原有的平滑奖励老实说这个任务不需要HER去掉重标定最终评测结果虚高评测时误用了重标定评测必须只用真实目标独立代码路径5.2 几个能直接上手的好习惯最后分享三个我在实战中形成的小习惯它们会帮你省掉很多查错的时间。第一个习惯是可视化重标定样本。每次训练开始前人为往缓冲区里塞几条手工构造的轨迹然后跑一段采样代码把采出来样本的(state, goal, reward, done)打出来。肉眼检查一遍目标是否来自状态子空间、reward是否为0/1、done是否为True/False逻辑一目了然。这个检查10分钟就能做完能拦住80%的“低级但致命”错误。第二个习惯是单独写一个递归一致性测试。把环境和重标定模块共用的奖励函数提取成纯函数然后在单元测试里同时调用双方确保它们对相同的(state, goal)返回完全一致的(reward, done)。一旦环境改动测试立刻报警不会出现“环境改了三天没人发现重标定还在用老规则”的尴尬局面。第三个习惯是记录重标定命中率——也就是缓冲区里经过重标定后reward为1的样本占比。这个数字在训练早期应该是比较高的因为伪目标都是自己达成的状态随着训练推进真实目标成功率上升这个占比会缓慢下降。如果这个数字一直高得离谱说明你的伪目标选得太容易了策略根本没有在挑战有难度的任务如果一开始就很低说明重标定逻辑有问题伪目标和轨迹实际到达状态不一致。5.3 关于“hindsight”这个命名的思考写到这里我想起实操中经常有人问为什么这个机制会起名叫“后见之明”我个人理解是它揭示了一个很微妙的视角转换——在真实世界里失败的经验当然不能直接等同于成功但过分强调“目标是否达成”的唯一标准会让我们扔掉所有“虽未达成目标但积累了技能”的宝贵数据。hindsight算法做的事就是在“事后”把这些数据的标签换掉让它们在其他目标下发光发热。这跟人类的成长路径是很像的。我刚接触强化学习那阵写的第一个机器人控制实验一塌糊涂但现在回头看失败的那段经历确实让我把REINFORCE、TD、Q-learning之间的关联理解得更透了。事后复盘能够提炼出新的学习材料这正是这个名字背后的隐喻。如果你现在正被某个稀疏奖励任务困住我的建议非常直接先别急着调一整套花哨的奖励塑形也用不着一上来就上课程学习先试着把HER加上去然后用future策略、K4、检查好done标志十有八九你会看到一条漂亮的上升曲线。至于那些更复杂的目标分布偏移和采样策略优化等你把这个基础版本跑通了再慢慢深入也不迟。
返回列表