1. 元学习到底在解决什么问题
1.1 从“一个任务一个模型”说起
如果你跟着CS285的课程一路学下来,到Lec18这里应该已经积累了不少强化学习的工具:策略梯度、Actor-Critic、Q-Learning、模型基方法等等。这些方法在单一任务上都能跑出不错的效果,但有一个共同的隐含前提——训练和测试是在同一个任务分布下进行的。换句话说,你训练一个机械臂抓取特定形状的物体,训练完之后它只会抓那个形状,换一个形状就得重新训练。
这个痛点在实际场景里非常致命。机器人要适应不同的地面摩擦力、不同的负载重量、不同的传感器噪声;推荐系统要适应不同用户的偏好分布;自动驾驶要适应不同城市的路况风格。如果每个新任务都从头训练一遍,样本效率和时间成本根本扛不住。
元学习(Meta-Learning)要解决的就是这个问题:让模型学会“如何快速学习”。不是学某一个具体任务,而是学一个“学习算法”或者“初始化参数”,使得面对新任务时,只需要少量样本和少量梯度步就能达到不错的性能。这个思路在监督学习里叫Few-Shot Learning,在强化学习里就是Meta-RL。
1.2 Meta-RL和普通RL的本质区别
普通RL的目标是最大化单个任务的期望回报:
$$\theta^* = \arg\max_\theta J(\theta)$$
Meta-RL的目标则是在一个任务分布 $p(\mathcal{T})$ 上,学到一个适应算法 $f$,使得:
$$\min_\theta \mathbb{E}{\mathcal{T} \sim p(\mathcal{T})} \left[ \mathcal{L}(\mathcal{T}, f\theta(\mathcal{T})) \right]$$
注意这里的 $f_\theta(\mathcal{T})$ 不是直接输出策略,而是输出一个针对该任务适应后的策略。这个适应过程本身可能只用了任务 $\mathcal{T}$ 的少量轨迹。
我个人的理解是:普通RL学的是“解题”,Meta-RL学的是“解题方法”。前者给你一道题的答案,后者给你一本参考书,让你遇到新题时能快速翻到对应章节。
1.3 为什么RL里的元学习比监督学习更难
监督学习里的元学习(比如MAML)已经够复杂了,但Meta-RL还要额外面对几个挑战:
- 信用分配跨任务:在监督学习里,损失函数直接告诉你预测对不对。在RL里,你只有稀疏的奖励信号,而且奖励还延迟。要搞清楚“哪个适应步骤导致了后来的好结果”非常困难。
- 非平稳性:策略更新会改变数据分布,元学习的外循环和内循环互相影响,训练极不稳定。
- 任务分布的设计:监督学习里任务就是不同的分类问题,定义清晰。RL里“任务”可能意味着不同的奖励函数、不同的动力学、不同的初始状态分布,怎么设计任务分布本身就是一门学问。
- 样本效率的双重压力:内循环要少样本,外循环也要少样本,两头都要省,难度指数级上升。
Lec18主要讲了两大类方法:基于优化的方法(以MAML为代表)和基于上下文的方法(以RL²为代表)。下面我逐个拆解。
2. 基于优化的Meta-RL:MAML及其变体
2.1 MAML的核心思想:学一个好的初始化
MAML(Model-Agnostic Meta-Learning)的思路非常优雅:不学一个能直接解决所有任务的模型,而是学一个“起点”,从这个起点出发,任何任务只需要几步梯度下降就能收敛到好解。
用数学表达就是:
$$\theta^* = \arg\min_\theta \sum_{\mathcal{T}i \sim p(\mathcal{T})} \mathcal{L}{\mathcal{T}i}(\theta - \alpha \nabla\theta \mathcal{L}_{\mathcal{T}_i}(\theta))$$
这里 $\theta$ 是元参数(初始化),$\alpha$ 是内循环学习率,$\mathcal{L}_{\mathcal{T}_i}$ 是任务 $\mathcal{T}_i$ 上的损失。内循环用任务数据做一步或多步梯度更新得到 $\theta_i'$,外循环优化初始参数 $\theta$ 使得所有任务适应后的损失之和最小。
这个式子里最妙的地方在于:外循环的梯度要穿过内循环的更新步骤。也就是说,你要对“梯度更新后的参数”再求梯度。这需要二阶导数,计算成本很高。
2.2 一阶近似:FOMAML和Reptile
为了解决二阶导的计算问题,有两个经典的近似方案:
FOMAML(First-Order MAML)直接忽略二阶项,把内循环更新后的梯度当作常数。具体来说,外循环梯度近似为:
$$\nabla_\theta \mathcal{L}{\mathcal{T}i}(\theta_i') \approx \nabla{\theta_i'} \mathcal{L}{\mathcal{T}_i}(\theta_i')$$
也就是说,只取适应后参数处的梯度,不回传通过内循环更新。这样做计算量大幅降低,效果在多数任务上损失不大。
Reptile更简单粗暴:它不做任何梯度展开,直接把初始参数往适应后的参数方向拉:
$$\theta \leftarrow \theta + \beta (\theta_i' - \theta)$$
你可以理解为:对每个任务跑几步SGD,然后让初始参数朝着这些适应后参数的平均方向移动。Reptile的收敛性分析表明它实际上在优化一个与MAML类似的目标,但实现起来只需要一阶信息。
我实测下来的感受是:如果任务之间差异不大,Reptile和MAML效果差不多;如果任务差异很大,MAML的二阶信息确实有帮助,但训练时间可能翻倍。所以选哪个取决于你的算力预算和任务分布。
2.3 MAML在RL里的具体实现
把MAML用到RL上,需要把“损失”替换成策略梯度目标。具体流程如下:
- 从任务分布中采样一批任务 ${\mathcal{T}_i}$
- 对每个任务,用当前策略 $\pi_\theta$ 采集少量轨迹(比如K条)
- 计算策略梯度 $\nabla_\theta J_{\mathcal{T}_i}(\theta)$
- 用梯度更新得到任务特定参数 $\theta_i' = \theta + \alpha \nabla_\theta J_{\mathcal{T}_i}(\theta)$
- 用 $\theta_i'$ 再采集少量轨迹,计算适应后的性能
- 外循环更新:$\theta \leftarrow \theta - \beta \nabla_\theta \sum_i J_{\mathcal{T}_i}(\theta_i')$
这里有个关键细节:内循环和外循环用的数据必须分开。内循环用support set(少量轨迹)做适应,外循环用query set(另一批轨迹)评估适应效果。如果混用,元学习会过拟合到support set上,新任务上表现很差。
另一个坑是策略梯度的方差。RL的梯度估计本身方差就大,元学习里还要在任务间求平均,方差会进一步放大。常见的做法是:
- 用PPO或TRPO这类相对稳定的算法作为内循环更新
- 增加每个任务的轨迹数量
- 使用基线(baseline)或优势函数(advantage)降低方差
- 对外循环梯度做裁剪(gradient clipping)
2.4 实操中的参数选择
以下是我在复现MAML-RL时总结的参数经验:
| 参数 | 典型值 | 说明 |
|---|---|---|
| 内循环步数 | 1-5 | 步数越多,适应能力越强,但二阶导计算越贵 |
| 内循环学习率 α | 0.01-0.1 | 太大会发散,太小适应不够 |
| 外循环学习率 β | 0.001-0.01 | 通常比α小一个量级 |
| 每任务轨迹数(内) | 5-20 | 取决于任务复杂度 |
| 每任务轨迹数(外) | 10-40 | 要比内循环多,保证评估可靠 |
| 任务batch size | 4-16 | 太小梯度噪声大,太大显存吃紧 |
注意:内循环学习率α不能设得太大,否则适应后的参数会跑到策略空间的很远的地方,外循环梯度会爆炸。我一般从0.01开始试,观察训练曲线再调整。
3. 基于上下文的Meta-RL:RL²与变分方法
3.1 RL²:把适应过程塞进RNN
MAML的思路是“学一个好的起点”,RL²(RL平方)的思路完全不同:把整个学习过程建模成一个序列决策问题,用RNN来记住任务信息。
具体来说,RL²把智能体的历史(状态、动作、奖励)作为RNN的输入,RNN的隐藏状态编码了“当前任务是什么”的信息。策略输出不仅依赖当前状态,还依赖RNN的隐藏状态:
$$a_t \sim \pi(a_t | s_t, h_t), \quad h_t = \text{RNN}(h_{t-1}, s_{t-1}, a_{t-1}, r_{t-1})$$
训练时,每个episode对应一个任务,RNN在episode内部逐步积累任务信息。经过大量任务的训练后,RNN学会了“从少量交互中推断任务”的能力。
这个方法的优雅之处在于:不需要二阶导数,不需要显式的内循环。整个适应过程隐式地发生在RNN的前向传播中。但代价是:RNN的隐藏状态容量有限,任务信息必须压缩在这个固定维度的向量里。
3.2 RL²的训练细节与坑
RL²的训练有几个容易踩坑的地方:
第一,episode长度和任务切换。每个episode必须对应一个固定的任务,episode结束时重置RNN隐藏状态。如果任务在episode中间切换,RNN会学到错误的关联。
第二,奖励的尺度。RNN对奖励的绝对值很敏感。如果不同任务的奖励尺度差异很大,RNN很难区分“任务本身奖励高”和“策略表现好”。常见的做法是对奖励做归一化,或者用奖励的排名而不是绝对值。
第三,探索不足。RL²的策略在早期训练时随机性很强,可能采集不到足够的信息来推断任务。可以在训练初期加入额外的探索噪声,或者用熵正则化鼓励探索。
第四,梯度爆炸。RNN在长序列上训练容易梯度爆炸。用LSTM或GRU可以缓解,但episode太长时仍然有问题。实践中通常把episode长度限制在100-200步。
3.3 变分Meta-RL:PEARL
PEARL(Probabilistic Embeddings for Actor-critic RL)是近年来比较有代表性的变分方法。它的核心思想是:把任务编码成一个隐变量 $z$,用变分推断来学习 $z$ 的后验分布。
具体来说,PEARL有一个编码器 $q_\phi(z | \mathcal{D})$,输入是任务的一些轨迹 $\mathcal{D}$,输出是隐变量 $z$ 的分布。策略 $\pi_\theta(a | s, z)$ 以 $z$ 为条件。训练时优化ELBO(证据下界):
$$\mathcal{L} = \mathbb{E}{z \sim q\phi} \left[ \sum_t \log \pi_\theta(a_t | s_t, z) + \beta \log p(z) - \beta \log q_\phi(z | \mathcal{D}) \right]$$
PEARL的优势在于:离线适应。你可以在不与环境交互的情况下,用少量轨迹推断出 $z$,然后直接部署策略。这在真实机器人场景里非常实用,因为交互成本很高。
3.4 方法对比:MAML vs RL² vs PEARL
| 维度 | MAML | RL² | PEARL |
|---|---|---|---|
| 适应方式 | 梯度更新 | RNN前向 | 隐变量推断 |
| 是否需要二阶导 | 是(或近似) | 否 | 否 |
| 适应样本量 | 少(1-5条轨迹) | 中(需要足够交互) | 少(可离线) |
| 任务表示 | 参数空间 | 隐藏状态 | 隐变量分布 |
| 训练稳定性 | 中 | 低 | 中高 |
| 实现难度 | 中 | 低 | 高 |
| 适合场景 | 任务差异中等 | 任务差异大、序列决策 | 交互成本高 |
我个人的选择逻辑是:如果环境交互便宜、任务差异大,优先试RL²;如果交互贵、需要离线适应,选PEARL;如果任务差异中等、算力充足,MAML是稳妥选择。
4. 任务分布设计与评估方法
4.1 任务分布怎么设计才合理
Meta-RL的效果高度依赖任务分布的设计。如果任务之间差异太小,元学习学不到有用的适应能力;差异太大,又很难找到一个通用的初始化或编码。
常见的任务分布设计维度包括:
- 奖励函数变化:比如目标位置不同、奖励权重不同
- 动力学变化:比如摩擦系数、质量、延迟不同
- 观测变化:比如传感器噪声、观测维度不同
- 初始状态分布变化:比如起始位置、初始速度不同
我的经验是:从单一维度开始,逐步增加变化维度。比如先只改变目标位置,确认元学习有效后再加入动力学变化。一次性加入太多变化维度,训练会非常困难。
另外,任务分布要有平滑性。如果任务之间的差异是突变的(比如从“走到A点”突然变成“走到B点且地面摩擦翻倍”),元学习很难泛化。理想情况下,任务分布应该是一个连续变化的流形。
4.2 评估Meta-RL的正确姿势
评估Meta-RL模型时,有几个常见的错误:
错误一:在训练任务上评估。这相当于在训练集上测准确率,没有意义。必须留出一批未见过的任务做测试。
错误二:适应样本量不一致。训练时用5条轨迹适应,测试时也用5条,不能多也不能少。否则比较不公平。
错误三:忽略随机性。Meta-RL的评估方差很大,至少跑3-5个随机种子,报告均值和标准差。
错误四:只看最终性能。还要看适应曲线——随着适应样本增加,性能如何变化。好的元学习模型应该在前几条轨迹就快速提升。
我通常用以下指标:
- 适应后性能:用K条轨迹适应后的平均回报
- 适应效率:达到某个性能阈值所需的轨迹数
- 零样本性能:不适应直接部署的性能(衡量初始化的质量)
- 任务间方差:不同任务上性能的标准差(衡量泛化能力)
4.3 训练不稳定的排查思路
Meta-RL训练不稳定是常态,不是例外。我总结了一套排查流程:
- 先检查内循环:单独跑内循环,看单任务适应是否正常。如果内循环都跑不好,外循环肯定崩。
- 检查梯度范数:外循环梯度如果爆炸或消失,说明内循环学习率或步数有问题。
- 检查任务batch:任务太少会导致外循环梯度噪声大,尝试增大batch size。
- 检查数据划分:确认support set和query set没有重叠。
- 降低内循环步数:从1步开始,逐步增加,观察训练稳定性。
- 换一阶近似:如果二阶导导致不稳定,先换FOMAML或Reptile验证思路。
我踩过最大的坑是:内循环用了太多步(10步),外循环梯度穿过这些步之后变得极其不稳定,训练loss来回震荡。后来降到3步,立刻稳定了。所以内循环步数不是越多越好。
5. 代码实现关键片段与调试技巧
5.1 MAML内循环的实现
以下是一个简化的MAML内循环实现(PyTorch风格):
def inner_loop(policy, task, inner_lr, inner_steps): # 复制一份参数,不影响原始参数 adapted_params = {k: v.clone() for k, v in policy.named_parameters()} for step in range(inner_steps): # 用当前参数采集轨迹 trajectories = collect_trajectories(policy, task, adapted_params) # 计算策略梯度损失 loss = compute_policy_loss(trajectories, adapted_params) # 计算梯度 grads = torch.autograd.grad(loss, adapted_params.values(), create_graph=True) # 手动更新参数 adapted_params = { k: v - inner_lr * g for (k, v), g in zip(adapted_params.items(), grads) } return adapted_params关键点在于create_graph=True,这样外循环才能对更新后的参数求导。如果不需要二阶导,设为False即可。
5.2 外循环的梯度处理
外循环更新时,需要对所有任务的适应后损失求和再求导:
meta_loss = 0 for task in task_batch: adapted_params = inner_loop(policy, task, inner_lr, inner_steps) # 用适应后参数在query set上评估 query_loss = compute_policy_loss(query_data[task], adapted_params) meta_loss += query_loss meta_loss /= len(task_batch) meta_optimizer.zero_grad() meta_loss.backward() torch.nn.utils.clip_grad_norm_(policy.parameters(), max_norm=1.0) meta_optimizer.step()梯度裁剪是必须的,否则外循环梯度很容易爆炸。
5.3 RL²的RNN实现要点
RL²的RNN部分需要注意:
class RL2Policy(nn.Module): def __init__(self, obs_dim, act_dim, hidden_dim): super().__init__() self.rnn = nn.LSTM(obs_dim + act_dim + 1, hidden_dim, batch_first=True) self.actor = nn.Linear(hidden_dim, act_dim) self.critic = nn.Linear(hidden_dim, 1) def forward(self, obs, act, rew, hidden=None): # 拼接观测、动作、奖励作为RNN输入 x = torch.cat([obs, act, rew], dim=-1) out, hidden = self.rnn(x, hidden) return self.actor(out), self.critic(out), hidden训练时,每个episode开始时重置hidden状态。如果batch里有多个episode,需要分别处理。
5.4 调试Meta-RL的实用技巧
- 先用小任务验证:比如2D导航任务,任务就是目标位置不同。确认元学习有效后再上复杂环境。
- 可视化适应曲线:画出“适应步数 vs 性能”的曲线,直观判断适应效率。
- 检查任务编码:如果是PEARL或RL²,可视化隐变量或隐藏状态,看是否真的编码了任务信息。
- 对比随机初始化:元学习后的初始化应该显著优于随机初始化,否则元学习没学到东西。
- 监控内循环梯度:内循环梯度如果太小,适应没效果;太大,外循环不稳定。
6. 常见问题与排查速查表
| 问题 | 可能原因 | 解决方法 |
|---|---|---|
| 外循环loss震荡 | 内循环步数太多、学习率太大 | 减少步数、降低α |
| 新任务上性能差 | 过拟合support set | 增加query set、正则化 |
| 训练初期就发散 | 梯度爆炸 | 梯度裁剪、降低β |
| 适应后性能没提升 | 内循环学习率太小 | 增大α、增加内循环步数 |
| RNN记不住任务 | 隐藏状态太小、episode太短 | 增大hidden dim、延长episode |
| 任务间方差大 | 任务分布太宽 | 缩小任务变化范围 |
| 计算太慢 | 二阶导开销大 | 换FOMAML或Reptile |
| 显存不够 | 任务batch太大 | 减小batch、用梯度累积 |
最后分享一个我常用的调试策略:先固定任务分布,只跑一个任务,确认内循环能正常适应。然后逐步增加任务数量,观察外循环是否稳定。最后再扩大任务分布的变化范围。这个渐进式调试方法能帮你快速定位问题出在哪一层。
7. 我个人在实际操作中的体会
Meta-RL这个方向,论文看起来都很漂亮,但真正复现的时候坑非常多。我最大的体会是:不要一上来就追求SOTA,先把最简单的MAML在2D导航上跑通,理解每一步在做什么,再逐步加复杂度。
另外,任务分布的设计比算法选择更重要。我见过太多人花大量时间调算法,结果任务分布设计得不合理,怎么调都上不去。花时间分析你的任务空间,确保任务之间有共享结构,同时又有足够的多样性,这比换算法收益大得多。
还有一个反直觉的经验:内循环步数少一点往往更好。MAML原论文用1-5步,我试过10步以上,外循环梯度变得极其不稳定,而且适应后的策略容易过拟合到support set。1-3步通常就够了,剩下的适应能力交给外循环学到的初始化。
最后,Meta-RL的训练时间通常是普通RL的5-10倍,做好心理准备。如果算力有限,优先考虑Reptile或RL²这类一阶方法,效果差距没有想象中那么大。