☰
Option-Critic架构详解:从手动技能到自动分层强化学习
2026/10/22 4:06:41 网站建设 项目流程

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-U>0 ) 时,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年那次入坑,最值得感谢的反而是这些不算顺利的复现经历,它们让我把论文里那些优雅的公式,一点点捏成了自己脑海里真正立体的直觉。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询