元学习概念
传统机器学习得到的模型擅长的是已经学习过的任务,现在希望训练出一个模型 ,在遇到新任务时,只需要很少的样本、梯度更新,就能获得不错的表现。
元学习目标是得到一个能利用少量样本快速进行新任务的模型。
MAML:最经典的基于优化的元学习方法
论文地址:[1703.03400] Model-Agnostic Meta-Learning for Fast Adaptation of Deep Networks
MAML 的核心思想:
寻找一个好的模型初始化,使模型能够通过少量梯度更新,快速适应新任务。
主要改变了模型初始化参数的优化目标。
MAML有两个阶段
Inner Loop:学习当前任务
Outer Loop:学习如何更快地学习任务
Inner Loop:针对一个任务进行适应
内循环就是用初始化模型做一次梯度下降,如果有三个任务,得到三组不同的参数
Outer Loop:评价模型适应之后的表现
假设三个任务都完成了内循环,得到了三组不同的参数
再跑一次,评价得到的三组参数
元学习内层循环相当于是把初始模型经过小幅度计算得到更新后的模型,外层循环的目标只是希望找到让内层循环效果最好的初始模型参数。
MAML论文精读
学习过程可以被理解为:最大化新任务损失函数对模型参数的敏感性,当这种敏感性较高时,模型参数在局部发生较小变化就能使任务损失得到显著改善。
MAML 可以通过学习一个便于适应的初始化来解决不同任务,而不一定需要学习到某种共同的高级语义特征。
MAML的通用算法:任务采样 -> 任务内梯度更新 -> 计算适应后损失 ->元参数更新
MAML的两个核心公式:
无论做图像分类、回归任务还是强化学习,只要能获得适当的梯度估计,就可以使用这套框架。区别主要体现在不同任务使用不同的损失函数。
监督任务(回归、分类)
在监督学习任务中,目标是利用从相似任务中获得的先验知识再进行元学习,使模型能仅根据某个新任务的少量数据学习一个新的函数。
监督分类和回归中常用的两种损失函数是交叉熵损失和均方误差MSE,下面以其为例。
PyTorch MAML 简单实现
对应 Finn 等人在《Model-Agnostic Meta-Learning for Fast Adaptation of Deep Networks》第 5.1 节中的正弦函数回归实验。
代码包含:
- 随机生成不同的正弦函数任务。
- 定义神经网络。
- 实现 MAML 的 Inner Loop。
- 实现 MAML 的 Outer Loop。
- 完成元训练。
- 在从未见过的新任务上进行快速适应。
- 绘图对比适应前后的预测效果。
- 支持切换一阶 MAML(FOMAML)和完整二阶 MAML。
import copy import random import numpy as np import matplotlib.pyplot as plt import torch import torch.nn as nn import torch.nn.functional as F from collections import OrderedDict from torch.func import functional_call # ============================================================ # 1. 配置 # ============================================================ SEED = 42 META_ITERATIONS = 2000 # 元训练迭代次数 META_BATCH_SIZE = 4 # 每次元训练采样多少个任务 SUPPORT_SIZE = 10 # 每个任务用于 Inner Loop 的样本数 QUERY_SIZE = 20 # 每个任务用于 Outer Loop 的样本数 INNER_STEPS = 1 # 每个任务执行几次梯度下降 INNER_LR = 0.01 # Inner Loop 学习率 META_LR = 0.001 # Outer Loop 学习率 FIRST_ORDER = False # False: 完整 MAML; True: FOMAML DEVICE = torch.device( "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu" ) def set_seed(seed): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) set_seed(SEED) print("Device:", DEVICE) # ============================================================ # 2. 生成任务 # # 每个 Task 是一个正弦函数: # # y = A * sin(x + phase) # # A ~ Uniform(0.1, 5.0) # phase ~ Uniform(0, pi) # # ============================================================ class SineTask: def __init__(self): self.amplitude = random.uniform(0.1, 5.0) self.phase = random.uniform(0, np.pi) def sample(self, K): # K 表示从当前任务中随机采样多少个数据点(样本) # torch.rand(K, 1)生成一个形状为 (K, 1) 的张量,其中有 K 个随机数,范围是 [0, 1)。 x = torch.rand(K, 1, device=DEVICE) * 10 - 5 # 将随机数映射到 [-5, 5)。 y = self.amplitude * torch.sin(x + self.phase) return x, y # ============================================================ # 3. 定义模型 # # 输入:x # 输出:预测的 y # # 网络结构: # # Linear(1, 40) # ReLU # Linear(40, 40) # ReLU # Linear(40, 1) # # ============================================================ class SineModel(nn.Module): def __init__(self): super().__init__() self.net = nn.Sequential(# Sequential 的意思是:按照定义的顺序,依次执行里面的网络层。 nn.Linear(1, 40), # 表示一个全连接层:输入 1 个特征,输出 40 个特征。 # y = Wx + b,其中 W(40*1)是权重矩阵,b 是偏置向量。 x(1*1)是输入特征,y(40*1)是输出特征。 nn.ReLU(), # 表达更加复杂的非线性关系,从而近似正弦函数。 nn.Linear(40, 40), nn.ReLU(), nn.Linear(40, 1) ) # 输入 x # │ # ▼ # Linear(1, 40) # │ # ▼ # ReLU # │ # ▼ # Linear(40, 40) # │ # ▼ # ReLU # │ # ▼ # Linear(40, 1) # │ # ▼ # 输出预测值 y def forward(self, x): return self.net(x) # ============================================================ # 4. Inner Loop # # 输入: # model 当前元模型 # support_x 当前任务的训练数据 # support_y 当前任务的标签 # # 输出: # adapted_params # # 核心: # # theta' = theta - alpha * grad(L_support) # # ============================================================ def inner_loop( model, support_x, support_y, inner_steps=INNER_STEPS, inner_lr=INNER_LR, first_order=FIRST_ORDER ): # 取出当前模型参数 params = OrderedDict(model.named_parameters()) # params = { # "net.0.weight": tensor(...), # "net.0.bias": tensor(...), # "net.2.weight": tensor(...), # "net.2.bias": tensor(...), # "net.4.weight": tensor(...), # "net.4.bias": tensor(...) # } # 每个任务都有自己的一份适应后参数 for step in range(inner_steps): # 使用当前参数进行前向传播 predictions = functional_call( model, params, (support_x,) ) # Support Loss support_loss = F.mse_loss( predictions, support_y ) # 计算梯度 # MAMl 后续会根据W‘的梯度来更新W,也就是二阶导数。所以这里手动计算梯度 grads = torch.autograd.grad( support_loss, tuple(params.values()), create_graph=not first_order ) # 梯度下降 adapted_params = OrderedDict() for (name, param), grad in zip( params.items(), grads ): adapted_params[name] = ( param - inner_lr * grad ) # 下一次 Inner Step 使用更新后的参数 params = adapted_params return params # ============================================================ # 5. Outer Loop # # 每次: # # 1. 采样多个任务 # 2. 每个任务执行 Inner Loop # 3. 在 Query Set 上计算适应后损失 # 4. 汇总 Meta Loss # 5. 更新共同初始化 theta # # ============================================================ def meta_train(model): optimizer = torch.optim.Adam( model.parameters(), lr=META_LR ) history = [] for iteration in range(1, META_ITERATIONS + 1): optimizer.zero_grad() meta_loss = 0.0 # 采样一批任务 for task_index in range(META_BATCH_SIZE): # 随机生成一个新任务 task = SineTask() # 同一任务中的 Support / Query support_x, support_y = task.sample(SUPPORT_SIZE) query_x, query_y = task.sample(QUERY_SIZE) # ------------------------------- # Inner Loop # ------------------------------- adapted_params = inner_loop( model, support_x, support_y ) # ------------------------------- # Query Forward # ------------------------------- query_predictions = functional_call( model, adapted_params, (query_x,) ) # ------------------------------- # Query Loss # ------------------------------- query_loss = F.mse_loss( query_predictions, query_y ) meta_loss = meta_loss + query_loss # 对任务数量取平均 meta_loss = meta_loss / META_BATCH_SIZE # ------------------------------- # Outer Loop # ------------------------------- # 元梯度 meta_loss.backward() # 更新初始化 theta optimizer.step() history.append(meta_loss.item()) if iteration % 100 == 0: print( f"Iteration {iteration:4d} | " f"Meta Loss: {meta_loss.item():.6f}" ) return history # ============================================================ # 6. Meta-Test # # 在一个从未见过的新任务上: # # 1. 从元学习得到的初始化出发 # 2. 使用少量 Support 样本进行适应 # 3. 评价适应前后的预测效果 # # ============================================================ def meta_test(model): print("\n========== META TEST ==========") # 新任务 task = SineTask() print( f"New Task: " f"y = {task.amplitude:.3f} * " f"sin(x + {task.phase:.3f})" ) # 新任务的少量训练样本 support_x, support_y = task.sample(SUPPORT_SIZE) # 测试曲线 x_test = torch.linspace( -5, 5, 200, device=DEVICE ).reshape(-1, 1) y_true = ( task.amplitude * torch.sin(x_test + task.phase) ) # ------------------------------- # 适应前 # ------------------------------- with torch.no_grad(): y_before = model(x_test) # ------------------------------- # Inner Loop:适应新任务 # ------------------------------- adapted_params = inner_loop( model, support_x, support_y, inner_steps=5, inner_lr=INNER_LR, first_order=True ) # ------------------------------- # 适应后 # ------------------------------- with torch.no_grad(): y_after = functional_call( model, adapted_params, (x_test,) ) # ------------------------------- # 计算误差 # ------------------------------- mse_before = F.mse_loss( y_before, y_true ).item() mse_after = F.mse_loss( y_after, y_true ).item() print(f"MSE Before Adaptation: {mse_before:.6f}") print(f"MSE After Adaptation: {mse_after:.6f}") # ------------------------------- # 绘图 # ------------------------------- plt.figure(figsize=(10, 6)) plt.plot( x_test.cpu().numpy(), y_true.cpu().numpy(), label="True Function", linewidth=2 ) plt.plot( x_test.cpu().numpy(), y_before.cpu().numpy(), label="Before Adaptation", linestyle="--" ) plt.plot( x_test.cpu().numpy(), y_after.cpu().numpy(), label="After Adaptation", linewidth=2 ) plt.scatter( support_x.cpu().numpy(), support_y.cpu().numpy(), label="Support Samples", marker="o", s=60 ) plt.title("MAML: Fast Adaptation to a New Sine Task") plt.xlabel("x") plt.ylabel("y") plt.legend() plt.grid(alpha=0.3) plt.savefig( "maml_result.png", dpi=200, bbox_inches="tight" ) plt.show() # ============================================================ # 7. 主函数 # ============================================================ def main(): print("\n========== MAML TRAINING ==========") model = SineModel().to(DEVICE) print( "Model Parameters:", sum(p.numel() for p in model.parameters()) ) # ------------------------------- # Meta Training # ------------------------------- history = meta_train(model) # 保存模型 torch.save( model.state_dict(), "maml_sine.pth" ) print("\nModel saved to maml_sine.pth") # ------------------------------- # 绘制训练曲线 # ------------------------------- plt.figure(figsize=(10, 5)) plt.plot(history) plt.xlabel("Meta Iteration") plt.ylabel("Meta Loss") plt.title("MAML Training Loss") plt.grid(alpha=0.3) plt.savefig( "maml_training.png", dpi=200, bbox_inches="tight" ) plt.show() # ------------------------------- # Meta Testing # ------------------------------- meta_test(model) if __name__ == "__main__": main()关于一阶 MAML(FOMAML)和完整二阶 MAML
二阶导数的由来:
一阶 MAML(FOMAML):First-Order Model-Agnostic Meta-Learning,一阶模型无关元学习
核心思想:计算二阶导数太昂贵就忽略
FOMAML 直接使用适应后模型的 Query Loss 梯度,近似作为初始模型的元梯度。
实验结果分析
========== MAML TRAINING ==========
Model Parameters: 1761
Iteration 100 | Meta Loss: 3.788455
Iteration 200 | Meta Loss: 1.681703
Iteration 300 | Meta Loss: 2.237962
Iteration 400 | Meta Loss: 2.531215
Iteration 500 | Meta Loss: 2.465373
Iteration 600 | Meta Loss: 4.893228
Iteration 700 | Meta Loss: 3.005637
Iteration 800 | Meta Loss: 3.053581
Iteration 900 | Meta Loss: 0.725297
Iteration 1000 | Meta Loss: 1.229570
Iteration 1100 | Meta Loss: 2.742692
Iteration 1200 | Meta Loss: 1.627140
Iteration 1300 | Meta Loss: 0.964882
Iteration 1400 | Meta Loss: 1.556150
Iteration 1500 | Meta Loss: 1.551608
Iteration 1600 | Meta Loss: 1.505540
Iteration 1700 | Meta Loss: 0.492393
Iteration 1800 | Meta Loss: 3.379267
Iteration 1900 | Meta Loss: 2.190859
Iteration 2000 | Meta Loss: 1.860071
模型确实完成了 2000 次元训练迭代,Meta Loss 在部分迭代中明显降低。metaloss是outer loop的优化目标,这里其实应该放outer loop最低的模型拿去训练和基础模型训练准确率的对比,但这里只是学习需要就没做那么严谨😝。
这张图就是meta loss数据的可视化,没有引入新的评价指标。