2017年前后是深度强化学习最热闹的几年,但也是大家开始产生怀疑的几年。深度Q网络证明了像素输入可以直接学出策略,各种“模型玩游戏”的工作层出不穷,可这些模型到底是在“解决任务”还是在“背下任务”,谁也说不清楚。一个很多人认同的判断是:机器缺少人处理长任务时那种“拆成子目标、分时间尺度决策”的能力。Option-Critic就是在那个时间点出现的。
它做的事情一句话能讲完:把原本需要手工设计的“选项/技能”(option),变成一个能完全端到端训练、自动从回报里涌现出来的架构。它同时学三样东西——每个选项内部的低级策略、选项之间的选择策略、以及选项何时终止——而且三者用同一个价值函数串联,全部靠策略梯度联合优化。
如果你在研究层次强化学习,或者想在项目里引入“时间抽象”的能力,这篇论文值得仔细拆。下面我把这套架构的来龙去脉、关键公式怎么来的、以及复现时那些文档里不会写的坑,全部摊开讲一遍。
1. 项目概述与核心思路拆解
1.1 先搞清楚:Option到底是个什么东西
Option一般翻译成“选项”,但我更愿意叫它的本名“时序扩展动作”(temporally extended action)。普通强化学习的动作是单步的:这一帧移动一格、这一次往前走、这一拍发一个指令。而Option是一段被命名好的行为序列,比如“从当前房间走到门口”“走到目标房间”“绕开障碍物”。人的行为组织方式本来就是分层的:我“去厨房”是一个高层动作,里面包含了站起来、迈步、转弯一大堆底层动作,但我规划的时候只思考“去厨房”这一条,不会去抠每一步怎么迈。
在这个语境里,一个option有三个组成部分:它允许在哪些状态下启动(初始集I)、被选中后内部如何执行(内部策略π)、以及每一步结束后以多大概率把这个option交还给高层(终止函数β)。经典框架里这三个部分要么手工设计,要么靠很别扭的方式单独学,而Option-Critic做的是把π和β的梯度都算出来,让它们和价值函数一起联合训练。
换句话说,你不再需要告诉模型“什么是好的option”,只需要告诉它“我给你N个option的名额”,至于每个option具体长成什么样、在什么状态下该激活、什么情况下该结束,全部交给回报信号自己去长。
1.2 为什么要动“时间抽象”这个念头
最直接的理由是稀疏回报。拿论文里反复出现的四房间网格环境举例:一个11x11的格子世界被墙壁分成四个房间,每面墙上有门通向隔壁,目标固定在某个格点,到达才给奖励。对一个随机游走的平坦策略来说,想在上百步的无头探索里正好踩中那个目标格点,概率非常低。更糟糕的是,就算侥幸到过一次,它也记不住“先到门口、再穿门、再去目标”这种规律,因为除了最后一格,每步的即时奖励都是零。
层次化思路是在两个尺度上做决策:高层只在房间之间选择“我去哪个门口”,低层负责把“走向那个门口”执行成几十步的动作。这样一来,规划深度从上百步压缩到几步,探索也从瞎走变成了有方向性的探索。Option-Critic真正贡献的不是这个思想——这个思想九十年代末就有了——而是把“选择哪个门、脚步怎么迈、什么时候算到达门口”这三层结构全部变成可微参数,直接用一个统一的目标端到端训练。
1.3 与经典选项框架的根本区别
经典选项框架的核心操作是“只学习高层策略”:给定一组设计好的option,比如“向上走到顶”“向左走到边”,然后用Sarsa或Q-learning去学在什么状态下应该选哪个option。这个路线的问题在于,option本身是任务相关的知识,换个环境就得重新设计,而设计一个能用的option往往比学一个策略还难。
Option-Critic的变革在于,它把option内部策略、终止函数、option选择策略看成同一个计算图的三条分支,价值函数(critic)作为中间的监督信号,用两个梯度定理把信号分别传导给三条分支。为了更直观,我把三种方案的差别列成一张表:
| 方案 | 动作粒度 | 需要人工设计什么 | 端到端学到什么 |
|---|---|---|---|
| 平坦策略 | 单步 | 奖励函数 | 状态到动作的映射 |
| 经典选项框架 | 多步 | option内部策略、终止函数 | 状态到option选择的映射 |
| Option-Critic | 多步 | 只指定option数量 | 内部策略、终止函数、选择策略 |
表格里最后一行就是这篇论文的核心买卖:把人工设计的部分砍到只剩一个“数量”超参数,剩下的全部交给优化。
2. 原理拆解:关键公式与推导直觉
2.1 从MDP到Option:半马尔可夫视角
先把标准设定摆出来。一个马尔可夫决策过程由状态集、动作集、转移概率、奖励函数和折扣因子组成,策略输出每个状态下选动作的概率,目标是最小化长期折扣累计回报的负期望。普通策略梯度定理说的是:回报的期望对策略参数的梯度,等于在轨迹上对“选这个动作的log概率”乘以“这个动作后续的回报”求期望。
Option-Critic把“一个动作”替换成“一个时序扩展动作”——也就是option。这样一来,整个过程的视角拉长了:高层看到的不是一个“状态→动作”的马尔可夫过程,而是一个“状态→option”的半马尔可夫过程,因为一个option的决策效果要跨越多个时间步才能显现。但真正棘手的问题在于,option本身不是一个黑盒,它的内部策略、终止函数都是有参数的,梯度需要穿过option内部。
2.2 选项的三元组定义与符号约定
一个option由三部分构成。初始集I规定了option在哪些状态下可以被选择;内部策略π_o(a|s)规定了这个option被选中后,在每个状态下以多大概率执行哪个原子动作;终止函数β_o(s)是一个0到1之间的概率,表示到达状态s后这个option是否结束、把控制权交还给高层。
执行节奏需要格外注意:在状态s处,高层按π_Ω(o|s)选一个option,然后agent执行内部策略产生的动作a,转移到达s',拿到奖励r。到s'之后才裁定:以概率β_o(s')决定option终止并重新选择,或者以概率1−β_o(s')继续执行同一个option。“终止发生在转移之后”这个时序,后面会反复用到,也是很多错误实现的根源。
一个重要特例是:如果β_o(s)恒等于1,这个option退化成了一步即终止的单步动作。也就是说,选项集合是原子动作集合的自然扩展,而不是替代。这保证了Option-Critic的表达能力不会比普通策略更弱,至少从结构上讲。
2.3 三个价值函数怎么互相嵌套
为了给三条分支提供监督信号,需要三个价值函数。第一个是option价值Q_U(s,o),表示从状态s开始执行option o,直到它终止为止的期望折扣回报。第二个是Q_U(s,o,a),表示在option o被选中的情况下,先执行动作a、之后继续遵循option内部策略的期望回报。第三个是高层状态价值V_U(s),表示从状态s开始、按照当前高层策略π_Ω选择option的期望回报。三者关系如下:
Q_U(s,o) = Σ_a π_o(a|s) · Q_U(s,o,a)
Q_U(s,o,a) = r(s,a) + γ Σ_{s'} P(s'|s,a) · [ (1−β_o(s')) · Q_U(s',o) + β_o(s') · V_U(s') ]
V_U(s) = Σ_o π_Ω(o|s) · Q_U(s,o)
中间那个方括号是关键:如果option在s'处不终止,那么未来继续由同一个option兜着,价值是Q_U(s',o);如果终止了,控制权回到高层,价值变成V_U(s')。这其实就是把“是否终止”这一个随机决策,嵌进了标准Bellman方程里。实际写TD更新时,目标就是:
target = r + γ · [ (1−β(s')) · Q_U(s',o) + β(s') · V_U(s') ]
这个target既用来更新价值函数本身,也用来构造后面各种advantage。
2.4 两个核心梯度定理:内部策略梯度和终止梯度
论文最实质的贡献是两个梯度定理。第一个是内部策略梯度定理,近似写成:
∇_θ Q_U(s,o) = E[ Σ_t γ^t · Q_U(s_t,o_t,a_t) · ∇_θ log π_θ(a_t|s_t,o_t) ]
用大白话说:option内部策略更新时,把“在这个option下执行这个动作之后的价值”当作评价,走的是标准策略梯度。所以option内部并不是在凭空学技能,它是在学习如何最好地实现顶层的选择。实际实现里会再扣一个baseline降方差,于是评价量变成advantage而不是原始Q值。
第二个是终止函数梯度定理,形式上更反直觉:
∇_θ Q_U(s,o) = −E[ Σ_t γ^t · (Q_U(s_t,o_t) − V_U(s_t)) · ∇_θ log β_θ(s_t) ]
这里的负号特别重要。令A(s,o)=Q_U(s,o)−V_U(s),它表示“当前这个option比高层平均水平好多少”。如果优势为正,说明这个option正在做正确的事,应该让它继续跑下去,也就是终止概率要下降——负号保证了这一点。反过来,如果优势为负,说明这个option拖后腿,应该提高终止概率,把它换掉。
落到最小化loss的实现层面,终止函数对应的loss是:
L_β = A(s,o) · log β(s)
A为正时最小化它会把log β往负方向压,也就是β变小、option不容易终止;A为负时则推着β变大、加速终止。终止函数的学习信号不是奖励本身,而是“当前option比平均好多少”的优势,这是整个架构最核心的直觉。
2.5 别被公式吓住:它本质上是一条链式法则
从直觉上理解,option级过程构成一个半马尔可夫决策过程,动作是“option执行一整段”,时间步是“从选option到终止”。目标对β参数和内部策略参数的梯度,本质上是在问两件事:改变终止概率会怎样改变option的持续时间,进而怎样改变回报分布;改变内部动作选择会怎样改变option在执行过程中的即时回报分布。这两个问题和普通策略梯度问的是一模一样的事,只不过这里的“决策”分成了两个层次——每一步是否终止、每个时刻选哪个动作。
需要说明的是,论文给出的定理是在比较规范的条件下证明的,但一旦上了深度网络和函数近似,实际训练效果主要由标准的actor-critic技巧控制,比如value是否正确detach、advantage要不要归一化、学习率怎么配。定理保证了更新方向的合理性,却不保证网络不会在某个局部行为里抽搐。所以看公式时记住“终止看优势、内部看价值”这两个方向就够了,真正的功夫在实现细节里。
3. 实操过程与核心实现要点
3.1 网络结构:一个共享主干,四个输出头
Option-Critic的实现不复杂,常见的网络结构是:一个共享特征层负责把状态转成向量,然后分出四个输出头。高层选择策略π_Ω在option集合上做softmax;critic头输出每个option的Q_U值;内部策略头为每个option分别输出动作概率,也就是N个option就对应N个softmax动作头;终止函数头为每个option输出一个sigmoid概率。
一个能跑通四房间的最小配置是:两层全连接、每层128个隐藏单元、激活函数用ReLU或者tanh。如果状态是像素输入,就把主干换成卷积层,后面再接同样结构的头。option数量N是先验超参数:四房间这种有四个门的环境,取4;完全没头绪的任务,先取4到8,不要贪多,后面我会解释为什么option太多反而容易坏事。
一个小建议:不同option的内部策略头不要共享初始化。给每个option的最后一层加不同的偏置,能让它们在训练初期就各自偏向不同的动作方向,避免“长得一模一样”的可悲开局。
3.2 训练流程:A2C风格的单步更新
下面是我常用的单步更新伪代码,每个环境step都执行一遍:
# 输入:状态特征 phi_s q_values = critic(phi_s) # 每个option的Q_U(s,o),形状[N] v_s = (pi_options(phi_s) * q_values).sum() # 高层价值 V_U(s) option = sample(policy_over_options(phi_s)) # 高层选option action = sample(intra_policy(phi_s)[option]) # 内部策略选动作 phi_s2, reward, done = env.step(action) # 关键:到了s'之后才算终止概率 beta_s2 = termination(phi_s2)[option] dial = Bernoulli(beta_s2).sample() option2 = sample(policy_over_options(phi_s2)) if dial or done else option # 1) critic更新:用期望beta构造TD目标 q_next = (1 - beta_s2) * critic(phi_s2)[option] + beta_s2 * (pi_options(phi_s2) * critic(phi_s2)).sum() target = reward + gamma * (1 - done) * q_next critic_loss = (target.detach() - q_values[option]).pow(2) # 2) 高层option选择策略更新 adv = target.detach() - v_s.detach() option_loss = -log_pi(phi_s, option) * adv # 3) 内部策略更新:评价当前option内的动作 intra_loss = -log_pi(phi_s, option)[action] * adv # 4) 终止函数更新:最小化 A * log beta A = (q_values[option].detach() - v_s.detach()) termination_loss = A * torch.log(beta_s2 + 1e-8) total_loss = critic_loss + option_loss + intra_loss + termination_loss total_loss.backward()这里有三件事必须解释清楚。第一,构造TD target时用的是期望beta,而不是伯努利采样出来的那个0/1开关,因为Bellman方程里算的就是期望;真正的采样开关只用来决定下一个时刻到底执行哪个option。第二,所有作为监督信号传给别的头的估计值都要detach,否则价值函数和策略会互相咬尾巴,训练很容易发疯。第三,严格来说,内部策略梯度的理论上界应该用Q_U(s,o,a)减去baseline,但工程实现里直接用单步TD advantage效果也稳定,而且方差更小。先按这个简单版本跑通,再往上加GAE也不迟。
3.3 超参数与调参经验
把我在四房间和各种小控制任务上试过的经验参数整理成表格:
| 超参数 | 建议范围 | 说明 |
|---|---|---|
| option数量N | 4~8 | 与任务天然子结构数量同量级 |
| 学习率 | 1e-4~3e-4 | Adam为主,终止函数头单独用小学习率 |
| β初始化logit | −0.5~0 | 对应β约0.38~0.5,防止崩向0或1 |
| 内部策略熵系数 | 0.01~0.05 | 保证option内部有探索 |
| 终止函数熵系数 | 0.01~0.1 | 防止过早锁死在“永远不终止” |
| 折扣γ | 0.99 | 长回报路径任务 |
| 步数n | 5步累计回报 | 单步TD容易方差大,5步通常够 |
值得注意的是,终止函数的更新对学习率非常敏感。我给终止函数头单独配一个优化器,学习率通常是主网络的十分之一,效果明显比全家桶一起更新稳定。另外,每跑几百个episode就把各option的β均值打出来看一眼,如果所有option都稳定在0.5附近没动,说明优势信号没进来,多半是critic出了问题,而不是策略问题。
3.4 复现基准:四房间环境里该看到什么
四房间是最好的调试场,因为它结果直观、算力开销极小。训练结束后,应该看到两类现象。第一,终止函数的热力图明显集中在门口附近——这意味着option“走到门口”正是它们认为该结束的地方。第二,option内部策略呈现空间分化,比如某个option的轨迹集中在上方门附近,另一个集中在下门,高层策略则学会按目标位置编排这些option的先后顺序。
如果训练完看不到门口附近的终止集中,先别怀疑算法,先问三件事:option数量是不是太多,导致每个option都分摊不到明确的子任务;β是不是已经崩到0或1;终止函数的优势是不是被错误地detach掉了。弹球环境也是论文里用过的连续域示例,里面有若干挡板,option-critic能学出“贴近挡板、蓄力、朝目标弹出”这类的技能结构,终止点恰好落在挡板附近。四房间跑通了,再上弹球这类连续环境,你会对“时间抽象”这四个字有真正的体感。
4. 常见问题与排查技巧实录
4.1 选项退化:所有option变成单步动作
最常遇到的问题是β快速逼近1,每个option选择之后执行一个动作就终止,层次结构彻底消失,训练曲线和普通平坦强化学习一模一样。原因是多方面的:β初始化太靠近1、终止函数学习率过大、优势没有做归一化导致偶尔的大梯度把β推过头。
解决的思路也很直接。β的logit初始值设成−0.5甚至更低,让训练从“倾向于持续执行”开始。给β的logit加一点熵正则,防止概率锁死在边界。更稳的办法是把终止函数的优化器和主网络分开,把它的学习率压到主网络的十分之一。最后,一定记得对优势做标准化,至少也要做一个裁剪,不然终止信号忽大忽小,很容易把β拍向极值。
4.2 另一个极端:选项永不终止
和上面相反,β也可能一路掉到接近0,第一个选中的option从此陪跑到回合结束,高层决策形同虚设。这种情况在稀疏回报任务里尤为常见。原因通常是所有option的价值估计差不多,优势长期处于零附近,终止梯度根本没有有效的推动力。
一个实在的救法是把β的初始化往上抬一点,比如logit设为0.2,同时在终止logit上保留较大的熵正则,逼它不要静止不动。另一个办法是加一个很小的、恒定的“终止倾向”项,让每个option在每个状态都有最低限度的终止概率,相当于给层次结构一条呼吸的缝。如果某个option长期不终止,还有一个歪招:单独把这个option的终止熵系数调大,让它在犹豫中分化出更多的终止行为。
4.3 所有option长得一模一样
option数量设得过大,或者初始化完全对称,会出现所有option的内部策略收敛到同一个分布,高层选谁都一样,整个架构退化成一个带冗余头的平坦策略。我自己踩过这个坑,N=16时四个房间的实验里,16个头全学会了向右走,看着既好笑又心疼。
破法从易到难排:先减少N,四房间用4就够,别贪心;再给每个option的内部策略头和终止函数头加不同的偏置初始化,破坏对称性;然后给内部策略加熵正则,鼓励每个头自主探索出不同的行为。如果这些都试过还是同质化,就需要上一点显式的多样性约束了,比如让option内部策略的分布彼此拉开距离,但这类方法比较复杂,我建议先把它当成最后的手段。
4.4 复现结果不稳定、跑不出论文的热力图
如果代码逻辑看起来都对,但训练结果忽好忽坏,大概率是几个隐蔽的细节问题。我列一张排查表:
| 现象 | 可能原因 | 建议 |
|---|---|---|
| 全是噪声,热力图无结构 | TD target里用了采样的0/1开关而不是期望β | 确认target用的是β(s')本身 |
| 训练开始就发散 | 监督信号没detach | 所有advantage和target作为监督时都要detach |
| β很快锁死在0或1 | 学习率太大或熵正则缺失 | 终止头单独低学习率,加熵正则 |
| 结果依赖随机种子 | 环境奖励尺度太大 | 归一化奖励,或做advantage标准化 |
| 不稳定 | 单步TD方差大 | 改用5步累计回报或GAE |
另外提醒一句:论文报告的结果往往是多次随机实验里的代表性曲线,不是每次都能复现一模一样的热力图。多跑几个随机种子,看中位数趋势,比盯着某一次实验的曲线更靠谱。
4.5 一个反复出现的时序理解错误
最后这点值得单独拿出来说。β(s')的s'是转移之后到达的新状态,不是转移之前的老状态s。执行顺序是:在s处选option,执行动作a,到s'拿奖励r,然后才在s'处决定“这个option还继不继续”。TD target里那个β,参数应该来自s'。我见过不少实现把β(s)放在转移前的位置,看起来公式差不多,实际价值计算全错,而且很难查出来,因为loss照样会下降,只是训练出的行为完全不符合理论。
判断你的实现有没有踩这个坑,只需要检查TD target一行的状态符号:如果β取的是下一个状态的特征,那就对了;如果取的是当前状态的特征,请立刻改掉。
5. 延伸思考与个人体会
5.1 从Option-Critic长出来的研究方向
这篇论文之后,层次强化学习的研究方向分出去好几条线。一条是把“终止”这个动作赋予成本,让终止函数在“继续执行”和“终止重选”之间学会一种经济的平衡,高层决策不再免费,从而更接近真实系统里规划成本高昂的设定。另一条是把优势函数换成兴趣函数,只学习那些“值得专门为之启动一个option”的状态,避免在无关紧要的状态上白费option维度。还有一条影响更大的线是彻底改变训练方式:先离线预训练一组技能(skill),冻结之后,高层只学技能之间的组合调度,不再端到端回传梯度。
这些后续工作你都会看到Option-Critic的影子,尤其是“终止函数由优势驱动”这个设定,几乎成了所有skill-based方法的标准组件。换句话说,理解了这篇论文,后面很多分层RL论文你都能一眼看出它们在哪里改了终止规则、在哪里换了训练信号。
5.2 我的亲测体验:Option-Critic什么时候真能用
说点扎心的大实话。我先后在几个环境里复现过这个架构,结论是它不是一个开箱即用的提分机器。在连续控制的标准benchmark上,一个调好的平坦策略常常更快更强,因为这类任务没有明显的瓶颈状态,层次结构反而成了束缚。但在四房间这种天然带子结构、奖励又稀疏的任务里,Option-Critic能稳定地长出比手工设计还干净的空间技能,这是平坦策略做不到的。
所以我给自己定了三条判断标准,三条里中了两条才考虑上层次化:第一,任务是否存在自然的子结构或瓶颈状态,比如走廊、门口、挡板;第二,低层执行是否比高层决策更昂贵,比如机械臂需要平滑轨迹而规划只需决定目标点;第三,是否需要跨任务迁移共享技能池。没有这些前提,说实话层次化是给自己找麻烦。
5.3 复现时最值得记住的三条实操建议
最后收个尾,给真想动手复现的你三条建议。第一,先在四房间这类小网格环境把整套流程跑通、把热力图看顺眼,再考虑上大环境,因为大环境下你根本分不清是算法问题还是显示问题。第二,β单独用一个低学习率优化器,并且坚持加熵正则,终止区间两端是重灾区,几乎所有训练事故最后都发生在β上。第三,永远检查detach的位置,监督信号是“评价量”而不是“被优化的量”,这个界限一旦模糊,理论再漂亮也救不了训练曲线。
我不敢说Option-Critic是那种能让你在测评榜上直接提分的算法,在不少标准任务上,它甚至不如一个调好的平坦策略。但我认为它是层次强化学习里最重要的转折点之一——它第一次把“如何终止、如何执行、如何选择”放进了同一个可微框架里,让时间抽象成了一个可以靠梯度涌现出来的性质。我后来看任何分层方法,都会下意识地去找这三个信号分别由谁在什么地方提供。这个思维习惯,比跑通一个demo值钱得多。