2017年的某一天我在一个房间导航类任务里做分层强化学习数据结构、奖励函数、环境接口都搭好了卡了整整两周问题却不在代码而在一个听起来很基础的概念Option。理论上只要给智能体设计好“开门”“走到走廊尽头”“按房间搜索”几个技能它就能把长时程任务分解成一段段短动作序列学起来又快又稳。但实际操作时我发现每个技能的开端条件、内部策略、终止条件全都得靠手工设计尤其是那个终止条件——定得太松一个技能会一直执行到荒谬的位置定得太紧技能退化成单步动作分层就没了意义。所以当我在一篇2017年的论文里看到《The Option-Critic Architecture》时思路一下子被打开了原来Option集合本身可以不用预先定义智能体在训练过程中自己就能决定“什么时候该启动一个技能、什么时候该结束它、当前技能内部要怎么走”。这篇工作把“设计技能”从超参调优里拿了出来变成端到端梯度训练的一部分。对当时被手写Option折磨到怀疑人生的我来说这几乎等于救命稻草。这篇文章我会从动机、架构原理、公式直觉、落地复现的真实坑位四个角度把这套架构讲透。适合正在做分层强化学习、或者刚开始读架构类论文但不知道如何转化成代码的读者。1. 为什么需要Option-Critic手动分层与自动分层的分水岭1.1 Option框架的“老问题”预定义选项集合要说清楚Option-Critic解决的是什么得先回忆一下传统的Option框架是怎么工作的。Option可以理解为一个“宏动作”一个带内部策略的暂时性行为。比如在一个4x4仓库环境里“去左上角取货”就是一个Option它内部可能包含“向北走两步”“再向西走一步”这样的底层动作。传统定义下一个Option由三部分构成初始状态集合、内部策略、终止函数。听起来很干净但工程实现时问题全出在“谁来决定这三部分”。初始集合和内部策略还好说环境结构清楚基本能写出个大概。但终止函数是最难受的它是定义在状态上的一个概率描述“当前这个Option在状态s下有多少概率结束”。一个看起来合理的终止函数放进整个训练循环里经常出现两种极端——要么Option永不停止整个智能体仍然只在执行一个长策略分层结构名存实亡要么Option频繁切换高层选择器被架空每个Option实际上变成了单步动作的包装壳。我当时的第一反应是多调几个阈值不就好了结果发现真正的问题在于“最优终止条件”无法和值函数同时优化。举个很直接的例子智能体在迷宫里找到一条新路能更快到达目标这时旧Option的终止条件就可能需要调整但旧终止条件是在训练之前手工写死的。环境稍微粗糙一点比如增加了随机障碍手工终止条件几乎必然和学到的策略脱节。1.2 Option-Critic的解题思路让终止条件变成可学习参数Option-Critic这篇工作最核心的转变是把Option的“存在性”和“边界”都变成参数化结构。它放弃了“先设计技能、再学底层策略”的两阶段流程改为在训练的一开始就设定好一个固定数量的候选Option集合例如6个或8个然后让每个候选Option拥有自己的参数化内部策略和终止函数这两个部分全部参与梯度更新不依赖任何手工终止阈值。这个转变听起来不大但意义在于高层的“选哪个Option”不再是一个手工宏命令而是值函数的自然结果低层的“这个Option内部该做什么”不再是一段写死的行为序列而是和其他强化学习策略一样用策略梯度训练终止函数也不再是二值开关而是一个经过sigmoid映射的概率输出。用一句不严谨但容易理解的话来说智能体从“按人类写好的技能库执行”变成了“自己发明一套技能库并且知道每个技能什么时候该拿出来用、什么时候该收起来”。我在复现时最喜欢拿四房间一类的导航迷宫做实验不是为了效果好而是因为这类环境能直观看出Option是否被“发明”出来几个候选Option会慢慢分化出“向下走”“向右走”“穿过门”之类有语义的行为而不是所有Option退化成同一个随机动作。当然要让这种分化稳定出现并不容易后半篇文章会详细讲我在类四房间环境上踩到的坑。至少在当时把终止概率从超参变成神经网络输出已经让我长舒一口气。2. 架构拆解策略、终止函数与option-value的联动2.1 三个核心组件及各自角色Option-Critic架构里一个完整的时刻包含三层决策我把它们拆开看会更好理解。第一层是高层选择器它在当前Option结束后根据当前状态s对所有候选Option的价值进行比较选出下一个Option。这个选择可以直接取最大值也可以做成softmax采样论文里倾向于用 ( \max_{o} U(s, o) ) 的形式也就是选当前状态下价值最高的那个候选Option。第二层是Option的内部策略 ( \pi_\theta(a|s, o) )。它告诉智能体给定当前状态s在已经选定Option o的条件下底层动作a应该取哪个。这个内部策略用策略梯度优化和普通Actor-Critic里的策略梯度本质上没有区别只是条件里多了个o。第三层是终止函数 ( \beta_\omega(s, o) )。它输出一个介于0和1之间的概率表示“当前状态s下Option o要不要终止”。终止后智能体回到第一步重新选择不终止则继续执行当前Option的内部策略。传统Option框架中终止函数是预先给定的而这里 ( \beta_\omega ) 有一个神经网络参数 ( \omega )它的更新方式是整篇论文的核心。这三层可以对齐到一句人话上高层决定“接下来练哪个技能”内部策略决定“这个技能具体怎么动”终止函数决定“这个技能什么时候算练完了”。三者各有各的参数却全部在一个共同的目标函数下被联合训练。2.2 状态价值与option价值的关系要让终止函数有学习信号必须先定义清楚“一个Option在一个状态下有多好”。论文用 ( U(s, o) ) 表示状态-选项价值读作“从状态s出发先执行Option o的策略直到其终止再按规则选择后续Option这样拿到的期望回报”。注意它不是简单的动作价值而是把“当前Option可能终止、终止后可能换Option”全部纳入考量之后的价值。与之相对状态价值 ( V(s) ) 则是所有候选Option中的最优价值可近似写为 ( V(s) \max_{o} U(s, o) )。这个最大值很重要因为终止函数的直觉判断来自一个对比如果当前Option o在状态s的价值明显低于其他候选Option的价值上限那么继续硬撑下去就没有意义终止并切换是更优的选择反过来如果当前Option的价值就是最优的那终止就是给自己添乱。听起来很直观但真正落实到参数更新上要绕一个弯。终止函数是一个概率我们不能直接把“该不该终止”的二值标签拿去做监督学习因为从环境里根本采不到这种标签。Option-Critic的关键就在于它为终止函数设计了一条可微的梯度路径让“该不该终止”完全由价值比较产生而不是由外部先验给出。2.3 为什么终止函数能用策略梯度学我用一个特别粗糙的类比来解释终止决策本质上也是一个“动作”只不过它不是一个环境动作而是作用在Option层面的二值决策。普通强化学习里策略网络输出一个动作的概率分布然后策略梯度能告诉我们“增加哪些动作的概率能提高期望回报”。终止函数也一样它输出一个终止概率我们可以用同样的思路去增大或减小终止概率只是目标从“动作的Q值”换成了“Option层面的价值差”。当下一个状态s出现时如果当前Option继续执行所能获得的期望收益低于直接终止当前Option后改选最优Option所能获得的期望收益即 ( U(s, o) \max_{o} U(s, o) )那么终止概率应该上升。这个差值就是终止函数的学习信号而 ( \log \beta_\omega(s, o) ) 提供了对终止概率参数的梯度路径。整个过程完全可微不需要手工为每个Option标注终止标签。我把这一步想明白之后才意识到它的妙处传统做法里“技能边界”是外挂的信息模型只能去适应它而在Option-Critic里“技能边界”变成了与环境交互中涌现出来的结果。从形式上看是三个模块各司其职本质上却是同一个价值函数在不同尺度上的投影。3. 终止梯度定理推导逻辑与直觉验证3.1 直觉建立回想、稀疏奖励与出口在钻进公式之前我建议先建立一个动态直觉。想象你在一座自己不熟悉的商场里找一个地下车库出口手里有一个“跟着商场导航牌走”的Option。一开始这个Option很好用一路都能看到指示牌。但走了十分钟你发现自己进了一个没有指示牌的通道这时如果继续沿用“跟导航牌”这个Option价值就很低更好的做法是终止它回到高层的“重新规划路线”Option。终止梯度定理做的就是把上面这种人类都会做的决策形式化为一个可计算的方向。当前Option在状态s上的价值如果低于其他选项终止概率参数的梯度就会指向“增大终止概率”。相反如果当前Option仍然有价值梯度就会抑制终止。我在复现中习惯把这个直觉画成一个二维价值地形图状态空间里某些区域当前Option的价值远高于其他选项终止概率应该低某些区域当前Option价值出现一个凹陷终止概率就应该形成一个小山包。观察 ( \beta_\omega ) 在状态空间里的分布能很直观地看到技能边界到底被学到了没有。3.2 关键公式拆解论文推导过程中最核心的是终止梯度定理完整证明会涉及不动点条件和多步期望展开。这里我用工程上可直接使用、并且和原论文更新方向一致的简化形式来写去掉常系数和期望下标只保留梯度走向[ \Delta \omega \propto \mathbb{E}\left[; \beta_\omega(s,o) \cdot \nabla_\omega \log \beta_\omega(s,o) \cdot \left(V(s) - U(s,o)\right);\right] ]这里的 ( V(s) ) 就是 ( \max_{o} U(s,o) )。当 ( V(s) - U(s,o) ) 为正说明当前Option不如别的选择梯度方向会推动终止概率增加当差值为负说明当前Option更好梯度会把终止概率往下压。前面的 ( \beta_\omega ) 是重要性修正保证只有在终止确实可能发生的地方梯度才有意义。内部策略的更新则比较常规形式上接近带优势函数 ( A(s,o,a) Q_U(s,o,a) - U(s,o) ) 的策略梯度。不同之处在于这里 ( Q_U ) 的计算里嵌入了终止函数因此Critic学到的不只是动作价值而是“考虑Option终止后的价值”这也是名称里Option-Critic的来源。原论文的终止梯度定理推导比这复杂得多它需要处理终止函数与价值函数之间的不动点关系以及 ( \nabla_\omega U(s,o) ) 的展开。但落到算法实现上上面这个简化式就是真正写进损失函数的那个项。我自己做复现时会把论文中符号的期望展开和折扣系数与代码逐行对应方便以后查错但最终留下的核心就是两个差值和一个对数概率。3.3 为什么不能写成普通Q-learning的“终止分支”有人可能会问既然已经有了 ( U(s,o) ) 和 ( V(s) )为什么不干脆把终止决策当作一个Q-learning分支在每个状态学习一个“终止动作的Q值”这个问题我在读论文时也纠结过因为从值函数角度看两者都依赖类似的价值对比。区别在于普通Q-learning假设终止决策是独立于策略的参数而Option-Critic把终止函数和Option内部策略放到同一个随机策略框架里优化。终止函数的梯度不是直接朝着“最大化当前状态价值”的方向而是要考虑到“当前Option内部策略会在终止前继续行动”这个事实。换句话说终止决策的改变会连带改变内部策略的有效作用范围。这种耦合如果只用Q-learning式的独立分支去做要么收敛慢要么技能边界和内部策略完全脱节。终止梯度定理的价值恰恰是把这种耦合显式建模出来。我实际测试中还有一个更微妙的感受单独用Q-learning分支学终止会把终止函数打得很散因为Q-learning天然倾向于在每个状态找“当前最优行为”导致Option频繁结束而Option-Critic里的策略梯度天然带有平滑性终止概率会更像一个区域性的软边界而不是一个点状开关。4. 算法落地与训练流程从离散到连续4.1 参数化选择离散动作与连续动作在代码层面Option-Critic并不挑环境但参数化方式需要区分离散和连续两种情况。离散动作环境里内部策略 ( \pi_\theta(a|s,o) ) 通常直接用softmax输出各个动作的概率输入是状态特征拼接当前Option的one-hot编码。终止函数 ( \beta_\omega(s,o) ) 则是一个sigmoid输出输入同样拼接Option编码。连续动作环境则把内部策略改成高斯策略输出均值和对数方差终止函数保持不变。Option编码有两种常见做法一是给每个Option一个可学习的嵌入向量二是直接用one-hot。我试下来两者效果在中小型环境差别不大但可学习嵌入在连续控制任务里稍微稳定一点因为它的维度可以压缩梯度更新也更平滑。终止函数网络可以做得比内部策略网络小很多它只需要判断“当前Option还适不适合继续”不需要掌握环境动力学细节。4.2 训练流程串讲与损失函数Option-Critic可以和A3C、PPO这类策略梯度算法结合。整体训练循环可以概括成先按当前Option的内部策略采样若干步然后利用Critic估计 ( Q_U ) 和 ( U )再分别更新Critic、内部策略、终止函数。下面是一份我在模拟项目X里验证过的简化伪代码主要是帮助理解梯度走向实际工程里会再加入并行采样和优势归一化for epoch in range(num_epochs): s env.reset() o high_level_select(s) # 初始Option按U的大小选 for t in range(max_steps): a pi(s, o).sample() s_next, r, done env.step(a) # 用当前网络估计 U(s_next, o) 和 V(s_next) u_next critic_U(s_next, o) v_next max_over_options(critic_U(s_next, :)) # 终止梯度方向V - U advantage_terminate (v_next - u_next).detach() term_prob beta(s_next, o) loss_terminate - (term_prob * log(term_prob) * advantage_terminate).mean() # 内部策略梯度使用带终止修正的优势 q_u r gamma * ((1 - term_prob) * u_next term_prob * v_next) advantage_action (q_u - u_next).detach() loss_policy - log_pi(a | s, o) * advantage_action # Critic回归 loss_critic MSE(q_target, critic_U(s, o)) update_all(loss_policy, loss_terminate, loss_critic) o decide_option(term_prob, s_next, o) s s_next代码里有一个很容易搞错的地方loss_terminate前面的符号是负号优化器方向是梯度下降而我们希望朝着 ( V-U ) 的正方向增大终止概率所以实际更新的是 ( -\log \beta \cdot (V-U) )。判断方向时可以直接检查当 ( V-U0 ) 时loss减小对应 ( \log \beta ) 增大终止概率上升逻辑闭环。4.3 超参数调法我跑通离散导航任务用的一组参考超参数如下。注意环境不同差异会很大但有几个起点值得记下终止概率网络初始偏置设为-2左右保证sigmoid输出初值大概在0.1到0.2附近给训练前期一点“稳定执行当前Option”的余地终止梯度的学习率通常设为内部策略学习率的0.1到0.5倍因为终止决策的变化会影响整个高层选择步子太大会震荡。超参数推荐起点说明初始终止偏置-2.0让 ( \beta(s,o) ) 初始接近0.1-0.2终止梯度系数0.1-0.5相对于策略梯度系数的比例熵正则系数0.01防止策略和终止函数过快固化折扣因子0.99长时程任务常用候选Option数4-8根据任务复杂度设定高维连控时Critic隐藏层256-512容量不足会影响终止信号质量候选Option数量不是越多越好。Option多了训练初期每个Option都有机会被选中但价值函数要在多个Option之间做比较方差会明显上升Option少了又可能出现连一个技能边界都表达不出来的情况。我通常先设4个跑通后再增加到6个对比稳定性。5. 复现过程中的真实坑位与排错记录5.1 选项消散问题与终止正则第一次完整跑通Option-Critic时我兴奋地打印了终止概率的均值曲线然后看到一条让我很不安的曲线它从初始的0.2附近迅速爬升最后贴着1.0走。视觉上就是所有终止概率都在疯狂变大每个Option都变成了“只执行一步就终止”的短命鬼。这种现象我后来统一称之为“选项消散”。背后的原因其实不复杂如果环境奖励稀疏( V(s) ) 在很多状态上会比实际执行一个长Option的 ( U(s,o) ) 稍微高一点哪怕这个差距只是噪声终止梯度也会持续微调终止概率不断累积后Option就散了。解决方向有三个。第一给终止函数加一个正比于平均终止概率的惩罚项例如 ( \lambda \cdot \beta(s,o) ) 进loss抑制过度终止第二降低终止梯度系数的同时对终止概率做EMA平滑第三增加内部策略的熵正则避免策略坍缩成单点动作让Option在终止前确实具有“行为一致性”。第三个方向是我印象最深的。如果不加任何正则Option-Critic很容易把每个Option都训成同一套单步动作因为对所有Option来说策略梯度目标基本指向类似的方向。加上熵正则后不同Option的内部策略才慢慢分化终止概率也回调到合理的0.3-0.5之间。5.2 共享网络参数时的梯度拉锯另一个让我排查了很久的问题是训练loss反复横跳数值上看起来总是不收敛。后来我把内部策略网络和终止函数网络从“共享同一套底层特征提取层”改成“两套独立的浅层网络”局面立刻改善。原因很直观内部策略希望学到的是“在当前Option内如何把动作做细”而终止函数希望学到的是“当前状态是不是技能边界”。这两个目标在共享层上的梯度方向经常打架尤其是在迷宫这类具有强空间结构的状态上。共享参数时终止梯度会污染底层特征表达让内部策略无法稳定学习拆开之后两者各自维护一套特征收敛速度反而更快。如果你为了让参数量可控而坚持共享可以试试给终止函数分支加一个梯度停靠层stop-gradient让它只更新顶层少量参数底层特征主要由策略梯度和Critic来驱动。这个方法我在一个连续控制模拟环境里也测试过能把震荡幅度明显压下来。5.3 探索与多Option平衡还有一个非常容易忽略的问题Option-Critic的高层选择一旦收敛某些Option会长期占据主导剩下的Option几乎没机会被选中于是它们的内部策略和终止函数根本得不到有效训练形成“死技能”。这种死技能对最终性能伤害很大因为你不确定它们是没用还是在某个尚未访问到的状态下会突然发挥作用。我的处理方式是在高层的Option选择上加入探索噪声例如用 ( \epsilon )-greedy 在Option层面做探索而不是只在底层动作上做探索另一个办法是维护每个Option的访问计数在价值比较时对访问次数少的Option加一个小bonus。这类方法让候选Option的使用率分布更均匀最终学到的技能库也更有多样性。在复现过程中我还发现终止函数和内部策略的更新频率最好保持一致不要单独降低某一个的更新频率否则练出来的Option会出现“策略会走但不会停或者会停但内部动作混乱”的割裂状态。把这些小坑一个个填平之后Option-Critic跑起来才算是稳定可用。6. 后续影响与个人实践建议6.1 哪些场景适合Option-Critic哪些不适合Option-Critic不是万能药它适合的是“任务内部存在可复用技能结构”的场景。比如导航任务里“越过门”、机器人操作里的“抓取”“对齐”、游戏里的“走向补给点”等这些子行为在多个任务片段中反复出现才有必要用Option去抽象。如果任务本身步骤短、目标单一比如倒立摆这种一两步就能稳定控制的环境强行上Option-Critic只会白白增加训练方差效果反而不如普通Actor-Critic。判断一个环境合不合适有一个很实用的测试方法先在环境上跑一个普通策略梯度基线观察它的单步动作序列里是否存在明显的“状态访问模式分组”。如果动作序列中经常出现“一段时间集中执行一类动作然后切换成另一类动作”的规律说明任务天然具有时间层结构值得用Option-Critic做显式建模如果动作序列看起来像随机游走那即使把Option数量调到10个也学不出稳定的技能边界。6.2 两个值得一试的改法我后来在项目里做了两个小改动都获得了明显提升。第一个是把终止函数的输出从sigmoid单值概率改成温度参数可调的softmax二分类温度高时终止决策更平滑有利于训练初期探索温度低时终止决策更决断有利于训练后期形成清晰边界。效果上相当于给终止函数增加了一个可学习的随机性控制和策略熵正则配合得很好。第二个改法是给内部策略加上一个与终止条件相关的辅助预测头让它不只输出动作还同时预测“当前状态下自己有多大可能即将终止”。这相当于给内部策略一个提前量让它在技能快要结束时更有意识地完成当前子目标。实现上只需要在策略网络末梢加一个分支训练时用终止概率作为弱标签做回归对最终收敛速度有正向帮助。我个人的体会是Option-Critic这类方法最需要耐心。它不像普通强化学习那样每几千步就能看到明显的奖励曲线上升而是先有一段“技能分化期”这个阶段奖励曲线往往很平甚至会有回落。熬过那个阶段终止概率开始出现空间聚集性不同Option的内部策略开始走不同路线后面的训练才会真正体现出分层结构的优势。现在回看2017年那次入坑最值得感谢的反而是这些不算顺利的复现经历它们让我把论文里那些优雅的公式一点点捏成了自己脑海里真正立体的直觉。