强化学习(十一):层次化强化学习与元学习

层次化强化学习(Options、MAXQ、Feudal Networks、目标条件策略)与元强化学习(MAML、FOMAML、RL²)的系统讲解:时序抽象、半马尔可夫过程、Manager-Worker 架构、二阶元梯度与循环式元学习器,附带 PyTorch 实现。

普通强化学习把每个问题都当作一串"原子动作"在做:观察状态、选动作、拿奖励,循环往复。这种做法在短回合、稠密奖励的玩具任务上还能凑合,但一旦遇到人类觉得"轻而易举"的真实任务就立刻露怯。“做一顿早餐"显然不是一次决策,而是一棵子任务树——煮咖啡、煎蛋、烤面包、装盘上桌——每个分支本身就是一个小策略。层次化强化学习(HRL) 让智能体把宏动作(macro-action)当成一等公民,从而能在多个时间尺度上同时思考和行动。

普通强化学习的另一道硬伤是:每个新任务都得从头学。会骑自行车的人改骑摩托车只要一个下午,而不是一千万环境步数。元强化学习(Meta-RL) 试图弥补这一点:在一组任务的分布上训练,使得遇到新任务时只需要少量回合(甚至只需要 RNN 走一遍 forward)就能完成适应。

这两条思路其实是同一种洞见在不同维度上的体现:层次化在时间维度上抽象,元学习在任务维度上抽象,二者都在压缩学习问题的有效维数。FuN、HIRO、MAML、RL² 等近年的代表工作,往往同时使用这两种武器。

你将学到什么

  • Options 框架——半马尔可夫过程与 intra-option Q-learning
  • MAXQ——沿任务树做值函数分解
  • Feudal RL(FuN、HIRO)——用连续子目标替代离散选项的 Manager-Worker 架构
  • 目标条件 RL——通用值函数与 HER(事后经验回放)
  • MAML / FOMAML——学一个能"几步就适应"的初始化
  • RL²——把内层 RL 算法折叠进 RNN 的隐藏状态
  • 可运行代码——Four Rooms 上的 intra-option Q-learning 与 2D 导航上的 MAML 策略梯度

前置知识

  • Q-learning、策略梯度与值函数(第 1–6 部分
  • 熟悉 RNN 展开与二阶自动微分
  • PyTorch

一、层次化:Options 框架

为什么需要时序抽象

扁平策略每一环境步做一次决策,因此在长度为 $T$ 的回合上,credit assignment 路径长度是 $T$,探索树叶子数是 $|\mathcal{A}|^T$,二者都呈指数膨胀。引入平均长度为 $\bar k$ 的宏动作之后,决策次数被压缩到 $T/\bar k$,探索树缩到 $|\mathcal{O}|^{T/\bar k}$,其中 $\mathcal{O}$ 是一个通常很小的选项集合。下图给出了两种视角的对比。

Options 框架:时序对比与三元组 (I, π, β)

除了渐近复杂度的论证,层次化还有三个非常实用的好处:

  1. 模块化——在某个任务上学到的选项可以在新任务上直接复用,例如"穿过门口"这个选项可以服务于一切导航问题。
  2. 可解释性——高层策略往往足够小,可以人工检查;决策只发生在语义上有意义的"关键节点"上。
  3. 奖励塑形——子目标天然提供稠密的内在奖励,即使外部奖励极稀疏也学得动。

Option 三元组

按照 Sutton, Precup & Singh(1999)的定义,一个 Option 是一个时序扩展动作

$$ o = \langle \mathcal{I}, \pi_o, \beta \rangle, $$

其中 $\mathcal{I} \subseteq \mathcal{S}$ 是允许启动该选项的状态集合,$\pi_o(a \mid s)$ 是选项的内部策略,$\beta(s) \in [0, 1]$ 是终止概率。引入选项之后,原 MDP 升级为半马尔可夫决策过程(semi-MDP):高层策略 $\mu(o \mid s)$ 选定一个选项,选项内部一直运行直到 $\beta$ 触发,下一次高层决策才会发生。

Intra-Option Q-Learning

最朴素的做法是等到选项终止再 bootstrap 更新 $Q(s, o)$,但这非常浪费——一个选项可能跑几十步,中间所有的转移都被白白扔掉。Intra-option Q-learning(Sutton 等,1999)在每一步都更新 $Q(s, o)$,关键观察是:同一个转移 $(s, a, r, s')$ 对于"在 $s$ 上会选 $a$“的所有选项都是有效的训练样本:

$$ Q(s, o) \leftarrow Q(s, o) + \alpha \big[r + \gamma U(s', o) - Q(s, o)\big], $$

其中**延续值(continuation value)**为

$$ U(s', o) = (1 - \beta(s'))\, Q(s', o) + \beta(s')\, \max_{o'} Q(s', o'). $$

延续值的设计很优雅:如果选项还会继续跑,就沿用它自己的 Q 值;如果它在 $s'$ 处终止,就把控制权交还给高层策略,挑当前最好的下一选项。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import numpy as np
from collections import defaultdict


class IntraOptionQLearning:
    """离散选项集合上的 intra-option Q-learning。

    每一条原始转移都会更新 (1) 当前选项的高层 Q(s, o),以及
    (2) 该选项的内部 Q 表,从而隐式地修正其内部策略。
    """

    def __init__(self, env, options, alpha=0.5, gamma=0.99, epsilon=0.1):
        self.env, self.options = env, options
        self.alpha, self.gamma, self.epsilon = alpha, gamma, epsilon
        self.Q = defaultdict(lambda: np.zeros(len(options)))     # Q(s, o)
        self.Q_prim = defaultdict(lambda: np.zeros(env.n_actions))

    def _select_option(self, state):
        available = [i for i, o in enumerate(self.options)
                     if o.can_initiate(state)]
        if not available:
            return None
        if np.random.rand() < self.epsilon:
            return int(np.random.choice(available))
        q = self.Q[state][available]
        return int(available[np.argmax(q)])

    def train_episode(self, max_steps=1000):
        state = self.env.reset()
        total_reward = 0.0
        for _ in range(max_steps):
            oid = self._select_option(state)
            if oid is None:
                break
            option = self.options[oid]

            while not option.should_terminate(state):
                action = option.act(state, self.epsilon)
                next_state, reward, done = self.env.step(action)
                total_reward += reward

                # 延续值 U(s', o)
                if option.should_terminate(next_state):
                    U = np.max(self.Q[next_state])
                else:
                    U = self.Q[next_state][oid]

                # 高层更新
                self.Q[state][oid] += self.alpha * (
                    reward + self.gamma * U - self.Q[state][oid])

                # 内部策略更新(在原始动作上的扁平 Q)
                td = (reward + self.gamma
                      * np.max(self.Q_prim[next_state])
                      - self.Q_prim[state][action])
                self.Q_prim[state][action] += self.alpha * td
                option.policy[state] = int(np.argmax(self.Q_prim[state]))

                state = next_state
                if done:
                    return total_reward
        return total_reward

在经典的 Four Rooms 实验中,配上四个手工设计的"穿过门口"选项后,intra-option Q-learning 比扁平 Q-learning 快 3–5 倍收敛,而且学到的选项可以直接迁移到新的目标位置。

MAXQ:沿任务树做值函数分解

Options 把层次结构隐藏在选项集合里,而 MAXQ(Dietterich, 2000)把它显式地写出来。智能体被给定一棵子任务的有向无环图,对每个复合任务 $i$ 和它的子任务 $a$,值函数被分解为

$$ Q_i(s, a) = V_a(s) + C_i(s, a), $$

其中 $V_a(s)$ 是完成子任务 $a$ 的价值,$C_i(s, a)$ 是完成函数(completion function)——子任务结束后继续完成父任务 $i$ 的额外价值。由于 $V_a$ 只依赖于 $a$,所有调用 $a$ 的父任务都能共用同一份 $V_a$,这正是样本效率提升的来源。

MAXQ 任务层次:原语在多个父任务间共用

代价是:MAXQ 只能保证递归最优(recursive optimality),而不是全局最优——也就是说,它只在给定的任务分解下最优。如果你的任务图无法表达真正的最优策略,MAXQ 就找不到它。


二、Feudal RL:用连续子目标替代离散选项

离散选项集合在连续控制或像素输入下很难手工列举。Feudal Networks(FuN,Vezhnevets 等,2017)HIRO(Nachum 等,2018) 把离散选项集换成由高层 Manager 输出的连续目标向量

Feudal RL:Manager 每 c 步设一次目标,Worker 每步执行

Manager 看到状态 $s_t$,每 $c$ 步(FuN 取 $c = 10$)输出一个目标 $g_t \in \mathbb{R}^d$。Worker 是一个目标条件策略 $\pi_\phi(a \mid s, g)$,它在向目标移动时获得内在奖励

$$ r^{\text{int}}_t = \cos\!\big(s_{t+c} - s_t,\, g_t\big), $$

而 Manager 直接根据外部环境奖励训练。这种解耦正是 Feudal 架构吸引人的地方:Worker 在稠密、几何的内在奖励上学习运动控制,Manager 则专注于长时序的信用分配,且其有效时序长度只有 $T/c$。

HIRO 的子目标重标注

Feudal 训练有一个鸡生蛋的问题:Worker 还在学习,所以是非平稳的,回放池里旧的目标已经不再对应 Worker 当前能达到的状态了。HIRO 用**子目标重标注(subgoal relabelling)**来解决:从回放池采到 $(s_t, g_t, a_{t:t+c}, s_{t+c})$ 后,把 $g_t$ 替换成最能解释 Worker 实际产生的动作序列的那个目标,

$$ \tilde g_t = \arg\max_{g} \log \pi_\phi(a_{t:t+c} \mid s_{t:t+c},\, g). $$

这一步把 Worker 的训练数据"拉回"到当前参数下的同分布上,从而显著稳定了 Manager 的离线训练。


三、目标条件 RL 与 HER

目标条件策略 $\pi(a \mid s, g)$ 值得单独一节,因为它是层次化与元学习的桥梁——同一个网络可以追求多个目标,目标 $g$ 只是个额外输入。

目标条件策略与 4 条来自同一网络的轨迹

经典的形式化是 Universal Value Function Approximators(UVFA,Schaul 等,2015):学习 $V(s, g)$ 或 $Q(s, a, g)$,而不是普通的 $V(s), Q(s, a)$。如果不加额外技巧,UVFA 在稀疏奖励下会非常痛苦:探索过程中绝大多数目标根本到达不了,奖励信号几乎全是零。

Hindsight Experience Replay(HER,Andrychowicz 等,2017) 是对此的标准补救。当一个回合没有达到目标 $g$ 时,把这条轨迹重新打标成它确实达到的目标(通常是终态),把一次失败变成"对另一个任务的成功演示”:

$$ (s_t, a_t, r, s_{t+1}, g) \;\longrightarrow\; (s_t, a_t, r', s_{t+1},\, g' = s_T). $$

配合离线方法(DDPG、SAC),HER 把"稀疏奖励的目标到达"从几乎不可能做到了"日常操作”。


四、Meta-RL:学习如何学习

元 RL 假设有一个任务分布 $p(\mathcal{T})$,而不是单个 MDP。元训练阶段,智能体见过许多任务 $\mathcal{T}_i \sim p(\mathcal{T})$;元测试阶段,它要面对一个全新任务,并尽可能少花交互就完成适应。

任务分布与在新任务上的适应曲线

主流方法分为两大家族:

  • 基于优化(optimization-based):MAML、Reptile、ANIL——适应 = 在测试任务上跑几步梯度。
  • 基于上下文 / 循环(context-based):RL²、PEARL——适应 = 数据流入时更新 RNN 隐藏状态,测试时无需任何梯度。

MAML——学一个好的初始化

Model-Agnostic Meta-Learning(MAML,Finn 等,2017) 的元目标是寻找参数 $\theta$,使得在任一任务 $\mathcal{T}_i$ 上做一步(或几步)内层 SGD 就能取得高回报:

$$ \theta_i' = \theta - \alpha \nabla_\theta \mathcal{L}_{\mathcal{T}_i}(\theta), \qquad \theta \leftarrow \theta - \beta \nabla_\theta \sum_{i} \mathcal{L}_{\mathcal{T}_i}(\theta_i'). $$

外层梯度穿过内层更新做反传,因此包含一个 Hessian 项 $\nabla^2 \mathcal{L}$。这正是 MAML 昂贵的原因,也催生了 FOMAML——直接忽略二阶项。经验上 FOMAML 比 MAML 大约快 $10\times$,最终回报损失不到 5%。

MAML 在参数空间中的几何意义与两层循环算法

左图给出了最有用的直觉:元训练不是在找一个对任何单一任务都最优的初始化,而是在找一个"甜区"——从这里出发,一步梯度就能落到每个任务的特定最优解附近。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import torch
import torch.nn as nn


class GaussianPolicy(nn.Module):
    """连续控制下的对角高斯策略。"""

    def __init__(self, state_dim=2, action_dim=2, hidden=64):
        super().__init__()
        self.trunk = nn.Sequential(
            nn.Linear(state_dim, hidden), nn.ReLU(),
            nn.Linear(hidden, hidden), nn.ReLU(),
        )
        self.mean = nn.Linear(hidden, action_dim)
        self.log_std = nn.Parameter(torch.zeros(action_dim))

    def sample(self, state):
        h = self.trunk(state)
        mean = torch.tanh(self.mean(h))
        std = self.log_std.exp().clamp(1e-3, 1.0)
        dist = torch.distributions.Normal(mean, std)
        a = dist.rsample()
        return a, dist.log_prob(a).sum(-1)


class MAML:
    """REINFORCE 风格策略梯度的一阶或二阶 MAML。"""

    def __init__(self, policy, inner_lr=0.1, outer_lr=1e-3,
                 first_order=False):
        self.policy = policy
        self.inner_lr = inner_lr
        self.first_order = first_order
        self.opt = torch.optim.Adam(policy.parameters(), lr=outer_lr)

    def _rollout(self, env, max_steps=50):
        state = env.reset()
        rewards, log_probs = [], []
        for _ in range(max_steps):
            s = torch.as_tensor(state, dtype=torch.float32).unsqueeze(0)
            a, lp = self.policy.sample(s)
            state, r, done = env.step(a.detach().numpy()[0])
            rewards.append(r)
            log_probs.append(lp)
            if done:
                break
        return rewards, log_probs

    @staticmethod
    def _returns(rewards, gamma=0.99):
        G, out = 0.0, []
        for r in reversed(rewards):
            G = r + gamma * G
            out.insert(0, G)
        out = torch.as_tensor(out, dtype=torch.float32)
        return (out - out.mean()) / (out.std() + 1e-8)

    def _loss(self, rewards, log_probs):
        returns = self._returns(rewards)
        return -(torch.stack(log_probs) * returns).mean()

    def adapt(self, env):
        """在 env 上做一步内层更新,返回适应后的参数列表。"""
        rewards, log_probs = self._rollout(env)
        loss = self._loss(rewards, log_probs)
        grads = torch.autograd.grad(
            loss, self.policy.parameters(),
            create_graph=not self.first_order,
        )
        return [p - self.inner_lr * g
                for p, g in zip(self.policy.parameters(), grads)]

    def meta_step(self, task_envs):
        meta_loss = 0.0
        for env in task_envs:
            adapted = self.adapt(env)
            # 暂存原参数,把适应后的参数装入网络做评估前向
            backup = [p.detach().clone() for p in self.policy.parameters()]
            for p, a in zip(self.policy.parameters(), adapted):
                p.data = a.data
            rewards, log_probs = self._rollout(env)
            meta_loss = meta_loss + self._loss(rewards, log_probs)
            for p, b in zip(self.policy.parameters(), backup):
                p.data = b
        meta_loss = meta_loss / len(task_envs)

        self.opt.zero_grad()
        meta_loss.backward()
        self.opt.step()
        return meta_loss.item()

上面的实现采用"换权重再前向"的方式来做元评估,而不是真正的 functional forward——这是最易读的版本,对小网络已经足够。研究规模的 MAML 建议改用 highertorch.func.functional_call 这类官方/第三方的函数式 API。

RL²——把算法本身折叠进 RNN

RL²(Duan 等,2016;Wang 等,2016)走了另一条路:测试时智能体的参数完全冻结,所有适应都由 RNN 的隐藏状态来完成。循环策略接收的输入被增广为

$$ x_t = (s_t,\, a_{t-1},\, r_{t-1},\, d_{t-1}), $$

也就是状态加上前一步的动作、奖励、终止标志。在一个元试验内的多个回合里,隐藏状态 $h_t$ 不断累积关于当前任务的信息——本质上在做一种隐式的贝叶斯后验更新。元训练阶段,外层优化器(PPO 或 A2C)调整 RNN 权重,使得这套"内置的 RL 算法"在 $p(\mathcal{T})$ 上具有良好样本效率。

RL² 在跨多个回合的元试验上的展开

RL² 有两个非常吸引人的属性:(i) 测试时零梯度计算,每步适应只是一次 forward;(ii) 适应过程是学出来的,原则上能在训练分布上超过任何手工设计的优化器。代价是元训练阶段的信用分配极其困难——梯度要穿过几十甚至上百步、横跨多个回合的循环展开。


五、常见问题

Options 框架到底为什么能加速? 三个叠加效应:(i) 有效时序长度从 $T$ 缩到 $T/\bar k$;(ii) 高层分支因子 $|\mathcal{O}|$ 通常远小于 $|\mathcal{A}|$;(iii) intra-option 学习意味着每条原始转移都会更新所有“在该状态会选这个动作"的选项的 Q,而不只是当前控制中的那个。

层次化最优 vs 全局最优有什么区别? 层次化最优是给定分解(选项集合或任务树)下的最优策略;全局最优是底层扁平 MDP 上的最优策略。两者只要分解无法表达真正的最优策略就会发生分歧——例如一个最少跑 10 步才能终止的选项,无法实现一个需要每 3 步切换行为的策略。

MAML 为什么需要二阶梯度?FOMAML 损失多少? 元损失是 $\mathcal{L}_{\mathcal{T}_i}(\theta_i')$,其中 $\theta_i' = \theta - \alpha \nabla_\theta \mathcal{L}_{\mathcal{T}_i}(\theta)$。对 $\theta$ 求导得到 $(I - \alpha \nabla^2_\theta \mathcal{L}_{\mathcal{T}_i})\, \nabla_{\theta_i'} \mathcal{L}_{\mathcal{T}_i}(\theta_i')$,里面带 Hessian。FOMAML 直接丢掉 $-\alpha \nabla^2 \mathcal{L}$ 这个因子(即把 $\theta_i'$ 当作 stop-gradient)。原论文报告标准 few-shot 基准上的性能损失小于 5%。

MAML 与 RL² 该怎么选? 能在适应时算梯度、且任务分布广到一个固定策略覆盖不全时,选 MAML;任务族窄、贪婪利用就能赢(例如 arms 会变化的多臂老虎机),或测试时无法做梯度时,选 RL²。PEARL 用一个独立的编码器推断任务嵌入,常常是个不错的折中。

HER 为什么必须配离线方法? HER 修改了某条转移所对应的目标,而 on-policy 方法假定数据是由"当前策略 + 当前目标"产生的——一旦改了目标,这一假定就破了。离线方法(DQN、DDPG、SAC)只需要在更新时知道 $\pi(a \mid s, g')$,而这个量是可计算的,所以重标注是无害的。


系列导航

参考文献

  1. Sutton, Precup & Singh. Between MDPs and semi-MDPs: A framework for temporal abstraction in reinforcement learning. Artificial Intelligence, 1999.
  2. Dietterich. Hierarchical Reinforcement Learning with the MAXQ Value Function Decomposition. JAIR, 2000.
  3. Vezhnevets et al. FeUdal Networks for Hierarchical Reinforcement Learning. ICML 2017. arXiv:1703.01161.
  4. Nachum et al. Data-Efficient Hierarchical Reinforcement Learning (HIRO). NeurIPS 2018. arXiv:1805.08296.
  5. Schaul et al. Universal Value Function Approximators. ICML 2015.
  6. Andrychowicz et al. Hindsight Experience Replay. NeurIPS 2017. arXiv:1707.01495.
  7. Finn, Abbeel & Levine. Model-Agnostic Meta-Learning for Fast Adaptation of Deep Networks. ICML 2017. arXiv:1703.03400.
  8. Duan et al. RL²: Fast Reinforcement Learning via Slow Reinforcement Learning. 2016. arXiv:1611.02779.
  9. Wang et al. Learning to reinforcement learn. 2016. arXiv:1611.05763.
  10. Rakelly et al. Efficient Off-Policy Meta-RL via Probabilistic Context Variables (PEARL). ICML 2019.

Liked this piece?

Follow on GitHub for the next one — usually one a week.

GitHub