上一部分我们把层级强化学习里最常见的 options 框架铺开来聊了聊,从定义到直觉都有了。这一部分我们换个节奏,直接拆理论、算法和工程调度:为什么要在动作和时间两个维度同时抽象,SMDP 到底改了哪些公式,Intra-Option 学习为什么能省下大量样本,还有 Option-Critic、技能自动发现、多任务迁移这些进阶玩法。如果你搞过选项强化学习却总觉得训练不稳定,这篇可以当一张排查地图用。
我默认你对基础强化学习(值函数、策略梯度、贝尔曼方程)已经有个大致概念。如果没有也不慌,每个地方我都尽量用“选线路 vs 踩油门”这类类比帮你垫底。写的时候我会把我自己复现这些方法时踩过的坑直接甩出来,很多细节从论文里根本读不到。
1. 时间抽象与选项:为什么高层要做“宏决策”
1.1 单步决策在长程任务里的天然劣势
先回到一个最朴素的问题:为什么不能让一个扁平策略直接解决所有任务?答案是,当任务跨度和奖励稀疏同时出现时,扁平强化学习几乎寸步难行。从门口走到城市中心,如果每一步都决策“先迈左脚还是右脚”,或者更细一点“膝关节弯曲多少度”,你就会陷入大量重复且高度相关的动作序列,探索信号被稀释得看不见。
强化学习里有个经典现象叫“延迟奖励”:智能体走了两万步,只有最后一步才拿到正奖励,中间没有任何 shaped 信号。在这个设定下,单步策略的信用分配相当于要在一串极长的因果链里找到真正关键的那几步,这对普通值函数和策略梯度都是一场灾难。层次化解决这个问题的方式不是“造一个更聪明的网络”,而是主动把决策尺度拉长:高层不再选择动作,而是选择一段行为模式,这段模式本身有一套独立的内部策略来执行。
这里的核心思想是时间抽象。时间抽象不是把几个动作绑定在一起那么浅层,它同时改变了状态空间上长期依赖的传递方式。高层策略只需要关心“当前处在哪个走廊”“下一步去哪个房间”这种粗粒度状态,低层策略则负责“怎么在走廊里移动”“怎么开门”这种细粒度控制。二者共享同一个环境,却在不同时间尺度上更新,这本质上把一个大 MDP 拆成了两个关联的小问题。
1.2 选项的三个组件再梳理
在选项框架里,一个选项 o 由三元组定义:初始集合 I、内部策略 π、终止函数 β。初始集合描述“这个选项从哪些状态可以开始”,内部策略描述“选项被选中后要做什么动作”,终止函数描述“什么时候把控制权交还给高层”。我这里特别强调一下终止函数 β(s) 是概率,不是硬性的“到达某个格子就退出”。它给选项的持续时间引入随机性,这样在同一个状态多次执行同一个选项,退出时间可以不同,能满足更灵活的时序需求。
为了和单层强化学习对齐,假设上层还有一个策略 π_Ω,作用于选项集合而不是动作集合。高层的每一步,都是从选项集合里挑一个 o,然后下层的 π_o 在环境中执行动作;每经过一个状态,以概率 β(s) 决定继续还是退出。一旦退出,高层再次做选择。这样整个系统就形成了一个“选项层的马尔可夫链”,而选项内部的低层策略无需理会高层为什么选了自己,只需尽力把内部行为做到最优。
选项和宏动作的区别值得拎出来说。宏动作通常是一段固定长度的动作序列,或者直到到达某个指定状态就停止;选项则是“带随机退出条件的行为策略”,终止概率可以由学习得到,也可以手写。正因为 β 是概率,选项才能表达“再走一步看看要不要转弯”这种细粒度策略,也才能被 Bellman 方程干净地吸收进值函数更新。理解了这三个组件,后面看 SMDP 和 Intra-Option 就不会绕。
2. SMDP:选项决策的底层数学框架
2.1 从 MDP 到 SMDP:决策时刻变成了随机变量
普通 MDP 的时间步是均匀的,每个时刻都做一次决策,转移概率只关心相邻两步。但选项执行时间是不固定的,可能 5 步退出,也可能 23 步退出,决策时刻变成了随机变量。这种“决策间隔随机”的过程就是半马尔可夫决策过程的核心特征之一。
半马尔可夫并不意味着“半个马尔可夫”,它只是说在决策时刻之间,系统内部经历一个更细致的基础随机过程(底层动作序列)。真正做决策的时点是选项的开始和结束。为了描述这种过程,SMDP 引入一个半马尔可夫转移核 P(s', τ | s, o),它表示从状态 s 选择选项 o 开始,经过 τ 步之后到达状态 s',并且选项在 s' 终止的概率。这个概率已经包含了选项内部策略 π_o 和终止函数 β_o 的全部影响,是一个“宏观一步”的转移。
读者可以这样理解:普通 MDP 的转移核是“动作到动作”,SMDP 的转移核是“行为段到行为段”。正因为存在时长 τ,奖励也不能只按单步看,而是要从选项开始累计到结束。这个看似简单的变化,直接改写了值函数更新公式。
2.2 SMDP 的值函数与贝尔曼方程
在 SMDP 下,选项值函数写成:
Q(s, o) = E[ r_1 + γ r_2 + ... + γ^(τ-1) r_τ + γ^τ * max_{o'} Q(s', o') ]
其中 τ 是随机执行步数,r_1 到 r_τ 是选项执行期间每一步的即时奖励,s' 是选项终止时到达的状态。这里最容易被初学忽略的一点是:折扣 γ 的指数必须与选项执行时长 τ 绑定。如果你在第 3 步退出,那么后续选项值的折扣系数就是 γ^3,而不是 γ^1。否则整个公式的时间一致性就崩了。
从贝尔曼角度来看,SMDP 的方程和普通 MDP 长得几乎一样,只是把“单步期望”替换成了“随机时长期望”。这种形式的优雅之处在于,只要有办法估计 τ 和累积回报,那么所有单层强化学习的算法架构都能平移过来。所以很多选项强化学习的实现,本质上仍然是 Q-learning 或策略梯度,只是把 update unit 从“一步”换成了“一个选项周期”。
2.3 SMDP Q-learning 与实现要点
SMDP Q-learning 的更新公式是:
Q(s, o) ← Q(s, o) + α [ G_t + γ^τ * max_{o'} Q(s', o') - Q(s, o) ]
其中 G_t 是选项运行期间累计折扣回报,s' 和 τ 来自一次完整选项执行。实现时最常翻车的不是公式本身,而是累计回报的边界处理。如果一个选项在 episode 中途退出,那没问题;如果选项还没退出但 episode 已经结束,那后面的 max 项必须清零,同时 τ 只累计到 episode 结束为止,不能继续向未来无中生有地延展。
一个稳妥的伪代码结构是:
# 伪代码:SMDP Q-learning def run_smdp_q_learning(env, options, Q, alpha=0.1, gamma=0.99): s = env.reset() o = select_option(s) # 高层 epsilon-greedy r_sum = 0.0 t = 0 while True: a = options[o].policy(s) # 选项内部策略选动作 s_next, r, done, _ = env.step(a) r_sum += (gamma ** t) * r t += 1 # 选项是否终止:终止概率参与采样 opt_ends = (np.random.rand() < options[o].beta(s_next)) or done if opt_ends: if done: target = r_sum else: target = r_sum + (gamma ** t) * max(Q[s_next].values()) Q[s][o] += alpha * (target - Q[s][o]) s = s_next o = select_option(s) r_sum = 0.0 t = 0 else: s = s_next if done: break注意这里 Q 如果是 tabular 形式,需要给每个状态保存所有可选选项的值;如果是函数近似,则要用同一个网络输出对应选项分支。工程上我会建议先在 tabular 或小规模连续状态上验证这段逻辑,确认 τ 和 r_sum 都正确,再放大到深度网络版本。否则很多参数都混在一起,出了 bug 根本定位不到是折扣指数写错,还是终止采样写错。
3. Intra-Option 学习:把样本效率翻倍的更新方式
3.1 核心思想:选项不需要“实际被选中”也能被学习
SMDP Q-learning 一个明显的浪费在于:只有当某个选项被高层选中、并且真正执行到退出时,才会产生一条学习样本。高层每轮只选一个选项,那没被选中的选项就一直没有更新。更麻烦的是,如果 β 学得不好,有些选项很少被选中,它们的值估计会长期偏置在初始状态。
Intra-Option(选项内部)学习是为了解决这个数据效率问题。它的出发点很朴素:一条低层的单步转移 (s, a, s', r) 不只是“当前被采样选项”的经验,只要某个选项的内部策略在状态 s 可能选择动作 a,那么这条转移对这个选项同样有效。
举个例子。假设有两个选项 x 和 y,它们的内部策略在某个状态 s 都会执行同一个动作 a。现在系统实际运行的是选项 x,因此观测到 (s, a, s', r)。这条数据能不能用来更新选项 y 的价值?当然能,因为 y 的内部策略在同一个状态下也会产生相同的 a,所以对于 y 来说,这就是一个“如果选中 y,会遇到的后续转移”的有效样本。关键在于如何给这个样本封上正确的价值目标。
3.2 更新公式推导与 β 的角色
Intra-Option 的单步更新公式写成:
Q(s, o) ← Q(s, o) + α [ r + γ * ( (1 - β_o(s')) * Q(s', o) + β_o(s') * max_{o'} Q(s', o') ) - Q(s, o) ]
读法是这样的:执行动作 a 到达 s' 之后,选项 o 有两类可能的走向。以概率 1 - β_o(s'),选项继续运行,则未来价值是当前选项 o 自己的 Q(s', o);以概率 β_o(s'),选项终止,则未来价值回到高层选择环节,用 max_{o'} Q(s', o') 接管。注意这里 β 用的是终止概率本身,而不是一次终止采样的结果。
这就是 Intra-Option 学习和朴素“把 flag 当 β”的根本区别。如果你拿实际执行中“是否终止”的 1/0 来替代 β,那就退化成了单次蒙特卡洛估计。因为终止事件在一条轨迹里可能非常少见,max 分支长期不被观察到,Q(s', o) 会被系统性地高估。用概率 β 做加权期望,才能保证更新目标覆盖两种分支的期望值。
从偏差角度看,这个公式其实做了三层简化:第一,假设选项内部策略 π_o 是马尔可夫的;第二,用商层 max 作为终止后的价值,忽略了高层实际选择策略的分布;第三,没有显式建模选项初始集合。绝大多数场景这些简化可接受,但在设计实验时要心里有数。
3.3 伪代码和数据流
实现的时候,核心不是“更新哪个选项”,而是“哪些选项值得更新”。对于状态 s 下观测到动作 a,我们需要遍历所有满足 π_o(a|s) > 0 的选项 o,把这些选项逐个更新。如果选项内部策略是确定性且动作只依赖状态,那满足条件的选项往往就是少数几个,遍历成本很低。
# 伪代码:one-step intra-option update for (s, a, r, s_next, done) in replay_buffer: for o in candidate_options(s, a): # 内部策略在 s 会选择 a 的选项 if done: target = r else: beta_next = options[o].beta(s_next) target = r + gamma * ( (1 - beta_next) * Q[s_next][o] + beta_next * max(Q[s_next].values()) ) Q[s][o] += alpha * (target - Q[s][o])trajectory 生成策略可以是任意探索策略,甚至可以是另一个完全无关的随机策略。因为 Intra-Option 更新属于 off-policy 更新,它利用的是转移样本中“动作与选项内部策略的一致性”,不要求样本来自当前选项本身。这也是它能与经验回放自然结合的原因。
SMDP Q-learning 和 Intra-Option 学习是互补关系:SMDP 更新给了一个选项真正从头到尾执行的“宏观回报”,减少长期估计方差;Intra-Option 更新则让每个单步转移都能反哺所有可行选项,放大样本利用率。实际项目中我会两层都保留,底层用 Intra-Option 做高频更新,高层用 SMDP 模式做低频校准。
4. 选项的自动发现与端到端学习
4.1 手工指定选项的局限
最早的选项实验基本靠手写。对于小迷宫,你可以把每个走廊出口定义成一个选项;对于机械臂,可以把“移动到抓取点”“抓取”“抬升”三个阶段拆开。手写选项的好处是语义清晰、调试方便,但坏处也很明显:需要领域知识、定义边界难、碰到没见过的新状态就失效。尤其当状态空间是高维视觉或者连续向量时,手写终止函数几乎没有可操作性。
于是研究者逐步把“选项从哪里来”这个问题变成学习问题。理论上形成的流派很丰富,我觉得可以分成两类:一类是间接发现,通过覆盖度和多样性目标先造出一个技能库,再在任务上复用;另一类是端到端学习,把选项的内部策略和终止函数全部放进神经网络,直接由任务奖励驱动训练。两类方法各有各的脾气。
4.2 覆盖度与多样性驱动的技能发现
间接发现里比较有代表性的思路是最大化技能之间的区分度和状态覆盖度。以 DIAYN 这一流派(多样性驱动的技能发现)为例,假设有一个隐变量 z 表示技能索引或者技能向量,目标是让技能 z 既能充分影响状态分布,又不要过度影响动作选择。典型目标函数可以拆成几项:
max I(S; Z) - I(A; Z | S) + H(A | S)
第一项鼓励不同技能访问不同状态,第二项限制“技能对动作的控制不能喧宾夺主”,第三项维持一定的探索随机性。实际实现里,互信息 I(S; Z) 通过一个判别器 q(z|s) 做变分下界逼近,于是技能学习的奖励就变成 log q(z|s) 再加上一些熵正则。这个框架跑起来很有意思,你会看到不同的技能自动分化出不同的行为模式:一个喜欢走左路,一个喜欢沿墙壁走,另一个站在原地转圈。
这类方法和经典选项的接口并不完全一致。它产出的更像一个连续技能库,没有显式终止函数,也没有初始集合限制。要用在运动规划里,通常需要额外定义一个“技能结束时刻”,比如固定时长或者基于状态距离的阈值。我自己的经验是:技能发现适合先构建底层行为库,但上游任务如果对延迟很敏感,纯多样性方法容易训练出一些“花哨但不实用”的技能,需要加任务相关的注意力机制过滤。
4.3 Option-Critic:让终止函数也接受梯度
端到端学习里最值得细看的是 Option-Critic 这路方法。它的设定是每个选项都有参数化的内部策略 π_θ 和终止函数 β_ϑ,高层选择选项的策略可以用任意方式训练,论文常用的是一套基于值函数的方法。整个系统不再需要手写终止边界,全部由任务奖励驱动。这样一来,选项的“持续时间”“内部行为”和“高层调度”三者同时被优化。
Option-Critic 的终止函数更新格外有直觉。关键在于引入一个差量信号:V_O(s') - Q_U(s', o)。如果当前选项 o 在状态 s' 的价值低于全局所有选项的价值,说明继续留在这个选项不划算,应增大 β;反过来,如果当前选项明显优于全局均值,应减小 β,让选项继续执行。所以终止函数的收敛行为可以理解成“当前技能表现差就赶紧退出,表现好就赖着不走”。这个机制在行为上会产生非常清晰的技能分割。
一个整理过的实现简化版长这样:
# 伪代码:Option-Critic 风格更新(含终止函数梯度) for each transition (s, o, a, r, s_next, done): beta_next = termination_net(o)(s_next) keep = 1.0 - beta_next # 目标值 q_o_next = option_value(s_next, o) if keep > 0 else global_value(s_next) q_target = r + gamma * (keep * option_value(s_next, o) + beta_next * global_value(s_next)) # 1) 内部策略更新:最大化 log π_o(a|s) * (q_target - value_baseline) advantage = (q_target - option_value(s, o)).detach() policy_loss += -log_pi(o, a, s) * advantage # 2) 终止函数更新:差值为正则增大 β,为负则减小 β stop_signal = (global_value(s_next) - option_value(s_next, o)).detach() term_loss += -log_beta(o, s_next) * stop_signal这个版本把终止项做了概率二分类处理,数值上更稳定。但要注意,Option-Critic 的原始推导在终止函数梯度上用了若干近似,直接硬训容易发生震荡,尤其在连续控制任务。我复现时踩到过一次:终止网络在头几千步内快速把 β 推到接近 0.5,导致所有选项都变成“走两步就停”,高层策略完全失去了长时间规划能力。后来我把终止学习率压低到内部策略的十分之一,同时给 β 的输出加一个 0.1 的熵惩罚,才把这种行为压下去。工程上请务必给终止网络单独设学习率,不要和策略网络共用一个 Adam。
5. 泛化、迁移与技能库的组合问题
5.1 选项作为可复用单元的价值
选项框架最有吸引力的承诺并不是单任务性能,而是迁移。一个训练好的低层技能库可以看作一组“原子行为”,比如“移动到门口”“打开抽屉”“拿起水杯”。当新任务进来时,高层策略只需要学会重新排列这些原子行为,而不必重新学习每个动作级映射。这在多任务场景下非常关键,因为高层搜索空间通常很小,可以快速收敛。
从这个角度看,选项库类似于代码里的函数库:低层函数经过充分单元测试,高层业务逻辑只需要调用。这也是为什么技能发现领域那么重视“行为多样性”:只有技能库覆盖了环境里各种状态转移方式,迁移到陌生任务时才有足够的函数供新高层策略调用。
5.2 迁移时什么容易被破坏
转移过程中最脆弱的环节往往不是内部策略,而是终止函数。原因在于终止函数是在旧任务的状态分布上学习的。新任务可能改变了奖励分布、目标位置、甚至物理参数,导致旧终止函数在完全不同的状态下触发。比如旧任务里“移动到门口再停”在门的状态下 β 升高,但新任务里门的位置变化了,终止函数可能在空旷地带过早退出,选项behavior就变得碎片化。
一个应对方案是分层冻结策略:迁移时固定低层内部策略,只重新学习高层调度和部分终止函数的参数。另一种方案是给终止函数附加一个环境状态编码,让它感知域偏移,但这样会破坏“技能任务无关”的初衷。在我的个人实践里,如果域偏移不大,固定内部策略 + 让终止函数用极小学习率微调,通常能保留大部分旧技能;一旦偏移超出阈值,更合理的做法是重新做一遍技能发现,而不是硬修旧库。
5.3 组合万能 vs 选项爆炸
选项数量有一个经典的权衡:N 太小,抽象粒度太粗,高层只能做出“去客厅”这种决策,没法细分“去客厅的茶几”这种需求;N 太大,高层动作空间迅速膨胀,选择正确选项的难度呈指数上升。更微妙的是,选项库内部还会出现冗余:多个技能在新任务上产生几乎相同的效果,让高层策略对选项索引的敏感性变得很弱,梯度信号被稀释。
实际操作中我没有见过谁靠一个巨型选项库直接解决所有问题,更多的是“小库 + 快速组合”的组合拳。比如先用几个简单选项学会任务 A,然后新增一两个专用选项,同时保持旧选项不变。这种增量式扩展比一次性生成一个大技能库稳定得多。如果真的需要大规模迁移,请务必给选项加上可解释的名称或语义标签,否则你会看着二十个技能向量完全不知道它们在干什么。
6. 工程实操:调试、超参和常见坑
6.1 终止概率失控:选项碎片化的典型表现
我复现选项相关算法时碰到最多的一个问题是“选项平均执行时长过短”。高层策略每步都在换选项,低层技能根本没机会产生长时序行为,训练曲线像条死线。检查下来,十有八九是终止函数被初始化得太大,或者训练过程中被奖励噪声带偏。建议在训练日志里直接加入一个统计量:每个 episode 内选项的平均执行步数。
如果平均步数低于你任务语义预期的 30%,先不要调高层探索率,回去看终止函数。一个简单修复是初始化 β 非常小(比如 0.01~0.05),让高层先形成稳定选择,再让终止网络逐渐放开自由度。也可以用课程方式:前 10 万步冻结终止网络,只学内部策略,等基本行为成形后再打开终止网络。
6.2 多步回报与折扣指数:两个经典翻车现场
SMDP 更新里 γ^τ 的指数要是写错了,后果非常隐蔽:可能整个值函数偏小或者偏大,但训练曲线看起来仍然在缓慢上升。我有一个检查脚本,专门在固定环境里用已知最优选项验证 Q 收敛值,凡是发现收敛值差 20% 以上,基本都在折扣指数或累计回报汇总处查问题。
另一个翻车现场是奖励被“重复计算”。当你的底层策略用单步回报训练,同时高层又用选项整体回报训练,如果底层 TD 误差里包含了未来选项切换的收益,而高层更新里本来就包含这部分收益,整个系统的价值信号会叠加两次,导致高层严重高估。我的做法是用两套完全独立的收益计算路径:低层用“当前选项内部的动作级优势”,高层用“切换到下一个选项前的选项级累计回报”。两条路径分开做梯度回传,互不污染。
6.3 超参数速查表
下面这张表是我在几个仿真任务上试出来的经验值,不是金科玉律,但能帮你省掉最痛苦的第一轮搜索。
| 参数 | 建议起始值 | 说明 |
|---|---|---|
| 终止函数 β 初始值 | 0.0 ~ 0.1 | 初始太大容易选项碎片化 |
| 终止网络学习率 | 策略网络学习率的 1/10 | 终止梯度震荡是常态,压低学习率最省心 |
| 高层探索率 | 0.2 ~ 0.3 | 高层动作空间通常比底层小,可以保持较高探索 |
| 内部策略熵权重 | 0.01 ~ 0.05 | 防止低层过早坍缩成确定性噪声 |
| 折扣 γ | 0.99 起步 | 长程稀疏任务可以尝试 0.995 |
| 选项数量 | 4 ~ 8 起步 | 先小后大,验证抽象粒度是否合理 |
| Intra-Option 更新频率 | 每个低层转移都更新 | 这是它高效的原因,不要降低频率 |
如果训练一段时间发现选项内部策略学到了“自杀式”行为,也就是主动冲进终止区域来换取终止奖励,请检查你的终止网络是否给了“终止本身”额外奖励。正常的 Option-Critic 里终止只是一个状态切换事件,不应该携带额外激励,否则技能自然退化。
6.4 常见问题速查表
| 现象 | 常见原因 | 排查与解决 |
|---|---|---|
| 选项平均时长过短,行为碎片化 | β 初始值过大或终止网络过度敏感 | 降低 β 初值,调整终止网络学习率,观察选项时长曲线 |
| 高层策略长期不更新 | 高层梯度被底层收益路径截断 | 检查两套收益是否互相覆盖,确认 Q 值有没有把选项值误当成动作值 |
| 样本利用率低,训练太慢 | 没有用 Intra-Option 更新 | 启用 intra-option 单步更新,确保候选选项集合覆盖所有 π(o |
| 值函数爆炸 | 折扣指数写错、多步回报累加边界没处理 | 检查 γ^τ 和 episode 边界清零逻辑,用已知最优策略做校验 |
| 端到端训练一开始就崩 | 终止网络和策略网络共用一个学习率 | 给终止网络单独设低学习率,并添加熵惩罚 |
6.5 从单层强化学习迁移到选项强化学习的路径
最后一个实操建议,很多人第一次上手选项学习就直奔复杂连续控制,结果全军覆没。我更推荐按下面顺序走:先实现固定选项的 SMDP Q-learning 在小型离散任务上验证值函数收敛;接着替换成 Intra-Option 更新,比较两种更新的收敛曲线差异,确认数据效率提升;然后在小规模连续控制环境里加入参数化终止函数,每次只动一个组件,保留上一次的稳定版本作为对照;最后才上端到端事务或技能自动发现。这个顺序的本质是逐步增加不确定性,每一步都有清晰的可观测指标,不会所有 bug 一起爆发。
到最后收个尾,分享一个我长期坚持的小习惯:每次训练选项级 agent 之前,先跑 10 个短 episode,专门记录选项平均时长、终止时状态分布和高层选项选择频率。如果选项总是在目标状态附近终止、平均时长符合语义,我就继续深入调参;否则先改 β 或奖励形状,绝不急着堆模型容量。这个习惯帮我省了大量无效训练时间,希望你也能用类似方式验证自己的选项框架。