系列 · 强化学习 · 第 7 篇

强化学习(七):模仿学习与逆强化学习

从专家示范中学习:行为克隆为何在长任务上必败、DAgger 如何把误差从二次降到线性、最大熵 IRL 如何反推奖励、GAIL/AIRL 如何用对抗训练匹配专家占用度。配可运行 PyTorch 代码、方法选择阶梯,以及七张高质量配图。

之前的所有算法都默认能够获取奖励函数,但在实际项目中,设计奖励函数往往是最大的难点。试着用一段话描述“像谨慎的人类一样开车”、“像裁缝那样叠衬衫”,或者“像专业编辑那样总结文档”——你会发现,演示这些行为远比明确指定它们容易得多。

模仿学习正是认真对待了这一直觉:与其优化人工设计的标量奖励,不如直接从专家示范数据 $\mathcal{D} = \{(s_t, a_t)\}$ 中学习策略。本章将介绍四种经典方法——行为克隆(Behavioral Cloning)、DAgger、最大熵逆强化学习(Maximum-Entropy IRL)以及 GAIL/AIRL——并将它们视为一个渐进式阶梯:每上一级,就放松一个关键假设,并通过引入新的结构来弥补由此带来的代价。

强化学习(七):模仿学习与逆强化学习 — 章节概览图


你将学到什么#

  • 行为克隆(BC):将模仿学习视为监督学习,理解它为何在短任务上有效,又因何在长任务上失效。
  • DAgger:如何通过交互式重新标注,将 BC 的二次误差转化为线性误差,并了解其背后的无悔定理。
  • 最大熵 IRL:从示范中恢复一个可解释的奖励函数,使其最优策略能复现专家行为。
  • GAIL 和 AIRL:通过端到端的对抗训练,匹配专家的占用度量(occupancy measure)。
  • 方法选择指南:根据专家是否可用、环境是否可交互,以及是否需要迁移能力,判断该选用哪种方法。

前置知识:策略梯度(第 5 部分 )和 PPO(第 6 部分 )。代码片段假设你已掌握 PyTorch 基础。


问题设定#

$$ \mathcal{D} = \{(s_1, a_1), (s_2, a_2), \ldots, (s_N, a_N)\}, $$

目标是学习一个策略 $\pi_\theta$ ,使其行为尽可能接近某个未知的专家策略 $\pi^*$ 。我们无法直接观测 $\pi^*$ ,只能看到其采样结果,且没有奖励信号。有时我们还能在新状态上查询专家(如 DAgger 所做);但很多时候,这是不可能的。

维度强化学习模仿学习
信号奖励 $r(s, a)$专家数据集 $\mathcal{D}$
是否需交互必须(需探索)有时可选(离线 BC)
优化目标最大化 $\mathbb{E}[\sum \gamma^t r_t]$匹配专家行为分布
典型失败模式奖励操控、探索不足分布偏移、模式坍塌
典型应用游戏、机器人、RLHF自动驾驶、手术、语言风格

我们将要讨论的方法在这些维度上各有权衡。下表概括了这个五级阶梯;本章其余部分将逐级展开。

模仿学习方法阶梯


行为克隆#

$$ \mathcal{L}(\theta) \;=\; \mathbb{E}_{(s,a)\sim \mathcal{D}}\big[ \ell\big(\pi_\theta(s),\, a\big) \big], $$

其中 $\ell$ 在离散动作时为交叉熵,在连续动作时为均方误差(MSE)或负对数似然(NLL)。历史上首个实例是 ALVINN(Pomerleau, 1989),它用全连接网络从摄像头图像中学习方向盘控制。

 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
import torch
import torch.nn as nn
import numpy as np
from torch.utils.data import DataLoader, TensorDataset

class BehavioralCloning:
    """监督式模仿:最小化策略输出与专家动作之间的误差。"""

    def __init__(self, state_dim, action_dim, hidden_dims=(256, 256),
                 lr=1e-3, continuous=False, dropout=0.1):
        self.continuous = continuous
        layers, prev = [], state_dim
        for h in hidden_dims:
            layers += [nn.Linear(prev, h), nn.ReLU(), nn.Dropout(dropout)]
            prev = h
        if continuous:
            layers += [nn.Linear(prev, action_dim), nn.Tanh()]
            self.criterion = nn.MSELoss()
        else:
            layers += [nn.Linear(prev, action_dim)]
            self.criterion = nn.CrossEntropyLoss()
        self.policy = nn.Sequential(*layers)
        self.optim = torch.optim.Adam(self.policy.parameters(), lr=lr)
        self.state_mean = self.state_std = None

    def fit(self, states, actions, epochs=100, batch_size=64, val_frac=0.1):
        # 标准化状态输入——BC 是一个小模型,未归一化的特征会主导损失。
        self.state_mean = states.mean(0)
        self.state_std = states.std(0) + 1e-8
        S = (states - self.state_mean) / self.state_std

        n_val = int(len(S) * val_frac)
        idx = np.random.permutation(len(S))
        S_tr, S_val = S[idx[n_val:]], S[idx[:n_val]]
        A_tr, A_val = actions[idx[n_val:]], actions[idx[:n_val]]

        S_tr = torch.as_tensor(S_tr, dtype=torch.float32)
        S_val = torch.as_tensor(S_val, dtype=torch.float32)
        if self.continuous:
            A_tr = torch.as_tensor(A_tr, dtype=torch.float32)
            A_val = torch.as_tensor(A_val, dtype=torch.float32)
        else:
            A_tr = torch.as_tensor(A_tr, dtype=torch.long)
            A_val = torch.as_tensor(A_val, dtype=torch.long)

        loader = DataLoader(TensorDataset(S_tr, A_tr),
                            batch_size=batch_size, shuffle=True)
        best, best_state = float("inf"), None
        for epoch in range(epochs):
            self.policy.train()
            for s, a in loader:
                loss = self.criterion(self.policy(s), a)
                self.optim.zero_grad(); loss.backward(); self.optim.step()
            self.policy.eval()
            with torch.no_grad():
                v = self.criterion(self.policy(S_val), A_val).item()
            if v < best:
                best, best_state = v, {k: t.clone() for k, t in
                                       self.policy.state_dict().items()}
        self.policy.load_state_dict(best_state)

    def act(self, state):
        s = (state - self.state_mean) / self.state_std
        s = torch.as_tensor(s, dtype=torch.float32).unsqueeze(0)
        self.policy.eval()
        with torch.no_grad():
            out = self.policy(s)
        return out.squeeze().numpy() if self.continuous else out.argmax(-1).item()

有三个实现细节的影响远超表面:

  1. 标准化输入。BC 本质上是一个小型监督模型——未缩放的特征会主导损失曲面,导致在罕见状态下输出过度自信的动作。
  2. 在保留的验证集上早停。长时间训练会过拟合专家的噪声,反而加剧后续问题。
  3. 动作表示方式。在连续控制中,若专家行为具有多模态性,用 NLL 损失预测高斯分布参数的效果通常优于对 Tanh 输出使用 MSE。

为什么 BC 在长任务上失效#

BC 在训练时依赖专家的状态分布 $d_{\pi^*}$ ,但部署时却访问自身策略 $d_{\pi_\theta}$ 生成的状态。由于策略不完美,这两个分布会随每一步逐渐偏离。

$$ J(\pi^*) - J(\pi_\theta) \;\le\; \mathcal{O}\!\left( \varepsilon \, T^2 \right), $$

即误差随任务长度呈二次增长(Ross & Bagnell, 2010)。即使策略单步准确率达 99%,在 200 步任务中也会失败——因为那 1% 的错误概率会不断累积,最终使策略进入专家从未见过的状态,而那里没有任何有效的训练信号。

当你将训练好的策略前向运行时,这种级联效应清晰可见:

BC 与 DAgger 的状态分布对比

在轨迹早期,BC 学习者仍能保持在专家轨迹附近;到中段时,微小误差开始显现,策略进入分布外状态,随后便持续漂向危险区域。下方的单步误差曲线也呈现出同样的累积效应:

专家轨迹与学习者轨迹对比

BC 的误差累积


DAgger:数据集聚合#

DAgger(Ross, Gordon & Bagnell, 2011)通过在学习者实际访问的状态上收集专家标签来打破上述级联。每次迭代都会从 $d_{\pi_\theta}$ 中采样一批 $(s, a^*)$ 对,并将它们加入训练集后重新训练。

算法

  1. 在初始专家示范上训练 $\pi_1$
  2. $i = 1, \ldots, N$
    • 按调度 $\beta_i \to 0$ 混合 $\pi_i$ 与专家策略以生成状态。
    • 在每个访问状态上向专家查询正确动作 $a^*$
    • 聚合数据:$\mathcal{D} \leftarrow \mathcal{D} \cup \{(s, a^*)\}$
    • 在全部 $\mathcal{D}$ 上重训练 $\pi_{i+1}$
$$ J(\pi^*) - J(\pi_{\hat{i}}) \;\le\; \mathcal{O}\!\left( \varepsilon \, T \right) + \mathcal{O}\!\left( \tfrac{T \sqrt{\log N}}{\sqrt{N}} \right), $$

即误差对任务长度 $T$线性的。换句话说,每次额外迭代都能找回 BC 所丢弃的恢复信息。

 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
class DAgger:
    """DAgger = BC + 在学习者访问的状态上反复请专家标注。"""

    def __init__(self, state_dim, action_dim, **bc_kwargs):
        self.bc = BehavioralCloning(state_dim, action_dim, **bc_kwargs)
        self.S, self.A = [], []

    def train(self, env, expert, n_iters=10,
              n_init=50, n_per_iter=20):
        # 第 0 轮:纯专家轨迹做 warm start。
        s0, a0 = self._collect(env, expert, n_init, beta=1.0)
        self.S += s0; self.A += a0
        self.bc.fit(np.array(self.S), np.array(self.A))

        for i in range(1, n_iters + 1):
            beta = max(0.0, 1.0 - i / n_iters)   # 专家比例从 1 衰减到 0
            s, a = self._collect(env, expert, n_per_iter, beta=beta)
            self.S += s; self.A += a
            self.bc.fit(np.array(self.S), np.array(self.A))

    def _collect(self, env, expert, n_episodes, beta):
        states, actions = [], []
        for _ in range(n_episodes):
            s, done = env.reset(), False
            while not done:
                # 行动用混合策略;标签始终来自专家。
                a_play = expert(s) if np.random.rand() < beta else self.bc.act(s)
                a_label = expert(s)
                states.append(s); actions.append(a_label)
                s, _, done, _ = env.step(a_play)
        return states, actions

DAgger 的适用场景。DAgger 要求专家能在运行时被查询。这在以下情况是现实的:

  • 专家是一个算法规划器(如 MPC 控制器、基于搜索的 oracle);
  • 专家是一个更强模型,你希望对其进行蒸馏(如 teacher-student RL、模型蒸馏、语言模型自蒸馏);
  • 人类专家在线并愿意批量标注。

但它不适用于仅有静态日志的情况——例如录制好的驾驶数据集。此时应转向 GAIL。


逆强化学习#

强化学习(七):模仿学习与逆强化学习 — 章节小结图

BC 和 DAgger 都是在模仿动作,而 IRL 则追问一个更深层的问题:为何这个动作是好的? 它假设专家(近似)在优化某个未知奖励 $r^*$ ,从示范中恢复候选奖励 $\hat r$ ,再在 $\hat r$ 下运行标准 RL。

为何要绕道奖励?有两个原因:

  • 可解释性$\hat r$ 能告诉你专家真正在意什么,便于审计和修改。
  • 迁移能力:奖励函数能适应动力学、具身形态或初始状态分布的变化,而行为级模型在这些变化下往往失效。

逆强化学习从行为反推奖励

最大熵 IRL#

$$ p_\theta(\tau) \;\propto\; \exp\!\left( \sum_t r_\theta(s_t, a_t) \right). $$ $$ \nabla_\theta \mathcal{L}(\theta) \;=\; \mathbb{E}_{\tau \sim \pi^*}\!\left[\nabla_\theta r_\theta(\tau)\right] \;-\; \mathbb{E}_{\tau \sim \pi_\theta}\!\left[\nabla_\theta r_\theta(\tau)\right]. $$

这可解读为对比更新:在专家走过的轨迹上提升奖励,在当前策略走过的轨迹上降低奖励。收敛时,两个期望相等,策略便复现了专家的占用分布。

代价在于第二项期望。计算 $\mathbb{E}_{\pi_\theta}$ 需要在每次奖励更新时求解一个 RL 问题(或进行轨迹采样)——这种嵌套循环曾将经典 IRL 限制在小型网格世界中。Guided Cost Learning(Finn, Levine & Abbeel, 2016)用重要性加权的采样轨迹替代内层 RL 求解,将 MaxEnt IRL 扩展到了连续控制领域。

奖励的模糊性#

即使有最大熵正则化,$\hat r$ 仍只能恢复到整形不变性(shaping invariance)的程度:添加势函数 $\Phi(s') - \Phi(s)$ 不改变最优策略,但会改变奖励值。因此,恢复的奖励仅提供状态间的相对排序,而非绝对尺度。对抗式逆强化学习(AIRL,见下文)显式地解耦了整形成分。


对抗式模仿:GAIL 与 AIRL#

IRL 的内层循环代价高昂。GAIL(Ho & Ermon, 2016)指出,模仿其实不需要显式恢复 $r$ ——只需让策略的状态-动作占用度量匹配专家即可。于是 GAIL 将“先恢复奖励再求解 RL”替换为一个对抗博弈。

$$ \min_\theta \max_\phi \;\; \mathbb{E}_{(s,a)\sim \pi^*}\!\left[\log D_\phi(s, a)\right] \,+\, \mathbb{E}_{(s,a)\sim \pi_\theta}\!\left[\log\big(1 - D_\phi(s, a)\big)\right] \,-\, \lambda H(\pi_\theta). $$

熵项 $\lambda H(\pi_\theta)$ 稳定生成器并抑制模式坍塌。在鞍点处,$\pi_\theta$ 的占用度量等于 $\pi^*$ 的——这正是 BC 想做到却因监督与部署分布不匹配而失败的目标。

$$ \hat r(s, a) \;=\; -\log\!\big(1 - D_\phi(s, a)\big) \quad\text{或}\quad \hat r(s, a) \;=\; -\log D_\phi(s, a). $$

两种形式均见于文献:前者是 GAIL 原文所用,后者在训练初期方差更低。

 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
class GAIL:
    """对抗式模仿:判别器输出充当策略的每步奖励。"""

    def __init__(self, state_dim, action_dim, hidden_dim=256, continuous=False):
        self.continuous = continuous
        self.action_dim = action_dim
        in_dim = state_dim + action_dim
        self.discriminator = nn.Sequential(
            nn.Linear(in_dim, hidden_dim), nn.Tanh(),
            nn.Linear(hidden_dim, hidden_dim), nn.Tanh(),
            nn.Linear(hidden_dim, 1),
        )
        self.disc_optim = torch.optim.Adam(self.discriminator.parameters(),
                                           lr=3e-4)
        # 策略 / 价值网络:任何标准 PPO 实现都行(此处省略)。

    def _sa(self, s, a):
        if not self.continuous:
            a = torch.nn.functional.one_hot(a.long(), self.action_dim).float()
        return torch.cat([s, a], dim=-1)

    def reward(self, states, actions):
        """喂给策略学习器的每步模仿奖励。"""
        with torch.no_grad():
            logits = self.discriminator(self._sa(states, actions))
            d = torch.sigmoid(logits)
            r = -torch.log(1.0 - d + 1e-8)   # 原版 GAIL 的奖励形式
        return r.squeeze(-1)

    def update_discriminator(self, expert_sa, policy_sa):
        # 二元交叉熵:专家=0,策略=1。
        e_logits = self.discriminator(expert_sa)
        p_logits = self.discriminator(policy_sa)
        bce = torch.nn.functional.binary_cross_entropy_with_logits
        loss = bce(e_logits, torch.zeros_like(e_logits)) + \
               bce(p_logits, torch.ones_like(p_logits))
        self.disc_optim.zero_grad(); loss.backward(); self.disc_optim.step()
        return loss.item()

调参要点。GAIL 在实践中主要有三种失败模式:

  • 判别器过快获胜:策略侧的奖励梯度消失。缓解方法包括裁剪判别器 logits、使用谱归一化,或施加 Lipschitz 约束(WGAIL)。
  • 奖励信号坍塌:当 $D \to 0$ 时,奖励趋近于零。可通过每批次奖励归一化(滑动均值/标准差)恢复学习信号。
  • 策略模式坍塌:增大熵奖励 $\lambda$ ,或使用最大熵 actor(如 SAC 风格)作为生成器。

“匹配占用度”究竟意味着什么?#

GAIL 并非最小化动作预测损失,而是最小化专家占用度 $\rho_{\pi^*}(s, a)$ 与学习者占用度 $\rho_{\pi_\theta}(s, a)$ 之间的 Jensen-Shannon 散度。这是一个比 BC 强得多的目标——因为它感知了部署分布。代价是:训练时必须与环境交互(以采样 $\rho_{\pi_\theta}$ ),因此原版 GAIL 无法用于纯离线场景。

AIRL:将奖励与整形解耦#

$$ D_\phi(s, a, s') \;=\; \frac{\exp\big(f_\phi(s, a, s')\big)}{\exp\big(f_\phi(s, a, s')\big) + \pi_\theta(a \mid s)}, \qquad f_\phi(s, a, s') \;=\; r_\psi(s) + \gamma \Phi_\xi(s') - \Phi_\xi(s), $$

其中 $r_\psi$仅依赖状态的奖励,而 $\Phi_\xi$ 吸收了整形项。这种参数化训练出的 $\hat r = r_\psi$ 能在动力学变化下保持有效——这也是 AIRL 论文的核心实证结果。

GAIL 判别器-生成器架构


样本效率:模仿学习 vs 强化学习#

模仿学习最强的实际优势在于样本效率。几千条专家示范常可替代数千万次环境交互,尤其在高维控制中更为显著。

样本效率对比:模仿 vs RL

左图的模式在机器人基准测试中高度稳健:BC 表现始终低于专家(无法超越训练数据),DAgger 在见到恢复状态后开始提升,GAIL 最终追平专家,而纯 RL 则需更长时间才能达到同等水平。右图则量化了这一权衡:消耗更多专家数据的方法通常需要更少的环境交互步数,反之亦然。

这也解释了为何“模仿预训练 + RL 微调”成为现代系统的主流范式——AlphaStar、机器人操作和 InstructGPT 均先通过模仿学习初始化,再用 RL 进一步优化。


方法选择#

方法需交互式专家?需环境交互?样本效率可解释奖励?典型场景
BC高(离线)短任务、日志充足
DAgger中高算法/模型专家可用
MaxEnt IRL是(内层循环)小状态空间、需迁移
GAIL高维连续控制
AIRL是(状态奖励)跨任务迁移奖励

简明决策规则:

  • 静态示范日志 + 短任务 → 从 BC 开始。务必加入早停和标准化;若需不确定性估计,可考虑集成方法。
  • 训练时可查询专家 → 选 DAgger。其线性误差保证是你能买到的最划算改进。
  • 需理解专家意图或跨环境迁移 → 小问题用 MaxEnt IRL,连续控制用 AIRL
  • 高维连续控制 + 静态数据 + 可交互环境 → 用 GAIL,但需预留对抗训练不稳定性的调试预算。

常见问题#

模仿学习能超越专家吗?
纯模仿学习不能——最优模仿者最多匹配专家水平。标准解法是以模仿为初始化:从 BC/GAIL 策略出发,再用 RL 微调(基于你能定义的奖励,或 RLHF 的偏好奖励)。几乎所有大规模系统都采用这种两阶段流程。

如何处理含噪声或次优的专家示范?
三重防御:(i) 按质量评分(如回报值、人类偏好)加权样本;(ii) 用鲁棒损失(如 Huber、log-cosh)替代交叉熵,抑制离群点;(iii) 在示范数据上应用离线 RL 方法(如 IQL 或 CQL),它们能优雅处理次优轨迹。

专家行为多模态——同一状态有不同动作,怎么办?
标准 MSE-BC 会平均多模态,产生危险的中间动作(如一半示范左转、一半右转,结果直冲障碍物)。可改用混合密度网络(MDN)、离散量化策略、条件 VAE,或扩散策略(diffusion policy)。Chi 等人(2023)提出的扩散策略目前在多模态操作任务中表现最佳。

多收集数据能否解决 BC 的分布偏移?
有限。更多数据可减小 $\varepsilon$ ,但无法改变 $T^2$ 的指数增长。一旦任务足够长,策略总会离开 $\mathcal{D}$ 的支撑集。此时需 DAgger 式重标注、GAIL 式占用匹配,或保守型离线 RL(如 CQL、IQL)显式惩罚分布外动作。

RLHF 是一种模仿学习吗?
部分是。RLHF 的监督微调阶段本质是 BC(基于人类示范);偏好建模阶段则更接近 IRL 变体——从比较中学习奖励模型,再用 PPO 优化。完整流程将在第 12 部分 展开。


参考文献#

  • Pomerleau, D. (1989). ALVINN: An Autonomous Land Vehicle in a Neural Network. NIPS.
  • Ross, S., Gordon, G., & Bagnell, J. A. (2011). A Reduction of Imitation Learning and Structured Prediction to No-Regret Online Learning. AISTATS. arXiv:1011.0686 .
  • Ziebart, B., Maas, A., Bagnell, J. A., & Dey, A. (2008). Maximum Entropy Inverse Reinforcement Learning. AAAI.
  • Finn, C., Levine, S., & Abbeel, P. (2016). Guided Cost Learning. ICML. arXiv:1603.00448 .
  • Ho, J., & Ermon, S. (2016). Generative Adversarial Imitation Learning. NeurIPS. arXiv:1606.03476 .
  • Fu, J., Luo, K., & Levine, S. (2018). Learning Robust Rewards with Adversarial Inverse Reinforcement Learning. ICLR. arXiv:1710.11248 .
  • Chi, C. et al. (2023). Diffusion Policy: Visuomotor Policy Learning via Action Diffusion. RSS. arXiv:2303.04137 .

系列导航#

本系列

强化学习 12 篇

  1. 01 强化学习(一):基础与核心概念
  2. 02 强化学习(二):Q-Learning 与深度 Q 网络(DQN)
  3. 03 强化学习(三):Policy Gradient 与 Actor-Critic 方法
  4. 04 强化学习(四):探索策略与好奇心驱动学习
  5. 05 强化学习(五):Model-Based 强化学习与世界模型
  6. 06 强化学习(六):PPO 与 TRPO —— 信任域策略优化
  7. 07 强化学习(七):模仿学习与逆强化学习 当前
  8. 08 强化学习(八):AlphaGo 与蒙特卡洛树搜索
  9. 09 强化学习(九):多智能体强化学习
  10. 10 强化学习(十):离线强化学习
  11. 11 强化学习(十一):层次化强化学习与元学习
  12. 12 强化学习(十二):RLHF 与大语言模型应用

读有所得?

GitHub 关注我 → 新文周更

GitHub