元学习简单入门

元学习概念

传统机器学习得到的模型擅长的是已经学习过的任务,现在希望训练出一个模型 ,在遇到新任务时,只需要很少的样本、梯度更新,就能获得不错的表现。

元学习目标是得到一个能利用少量样本快速进行新任务的模型。

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 节中的正弦函数回归实验。

代码包含:

  1. 随机生成不同的正弦函数任务。
  2. 定义神经网络。
  3. 实现 MAML 的 Inner Loop。
  4. 实现 MAML 的 Outer Loop。
  5. 完成元训练。
  6. 在从未见过的新任务上进行快速适应。
  7. 绘图对比适应前后的预测效果。
  8. 支持切换一阶 MAML(FOMAML)和完整二阶 MAML。
python 复制代码
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数据的可视化,没有引入新的评价指标。

相关推荐
198******126341 小时前
2026企业AI办公工具选型指南:企业部署AI前需要考虑哪些问题
大数据·人工智能
Zucker n1 小时前
一款适合研究生科研工作安排的日历
linux·数据库·python
Raas1001 小时前
MAI Gateway(魔芋企业级AI网关)选购攻略:AI网关哪个好?避坑指南与对比
大数据·人工智能·ai·gateway·ai网关·企业级ai网关
广东帝工智能安防1 小时前
航道数智化升级实战:基于“云-网-数-智“架构的信息化基础设施建设方案
大数据·运维·人工智能
小鹿软件办公1 小时前
微软公布更多 AI 功能加持的 Windows 11 全新“照片”应用
人工智能·microsoft
匠测AI说1 小时前
AI for Testing 提效实战 · 执行自动化(四):脚本坏了让 AI 自愈——它说修好了,你凭什么信
人工智能·测试
YHL1 小时前
造一个"读过《天龙八部》"的 AI 助手 —— advance-rag
人工智能
光影少年1 小时前
AI Agent、自动化与未来工作
人工智能·算法·架构
CALL158119098451 小时前
从GB/T 14644到ASTM D6413,纺织品燃烧性能标准有哪些
大数据·人工智能