元学习实践:学会学习的模型设计

前阵子AI元学习连续改了几轮,趁还记得写成备忘。

背景:为什么要折腾元学习

半年前接了个项目,做图像分类。模型在训练集上准确率 99%,测试集 95%,看起来很完美。结果客户换了一批新的图片,准确率直接掉到 60%。客户问:你们是不是调参调得不对?

不是调参的问题,是模型根本就没"学会学习"。它记住了训练数据的特征,但没学会怎么快速适应新数据。这就是元学习要解决的核心问题。

传统机器学习的套路是收集数据、训练模型、验证效果,但真实场景里往往行不通:

  • 数据需求大:每个新类别需要几百甚至上千张样本
  • 训练时间长:新任务需要从头训练或微调
  • 适应能力差:换了个数据分布,模型就懵了
  • 泛化能力弱:过拟合训练数据,迁移到新场景性能暴跌

客户不会给你准备完美的数据集,任务会不断变化,要求模型能快速适应。

先搞清楚元学习是什么

元学习,也叫"学会学习"(Learning to Learn),它的核心思想是把学习过程本身当成一个优化问题。传统深度学习是在参数空间里找最优参数,元学习则是在"学习策略"空间里找最优学习策略。

用一个类比:传统学习像是教学生解题,每种题型都要练;元学习像是教学生解题方法,遇到新题型也能自己上手。

用伪代码表示一下:

# 传统学习
def traditional_learning(train_data):
    model = initialize_model()
    for batch in train_data:
        loss = compute_loss(model(batch), batch.label)
        model = update_parameters(model, loss)
    return model

# 元学习
def meta_learning(support_sets, query_sets):
    meta_model = initialize_meta_model()
    for support_set, query_set in zip(support_sets, query_sets):
        # 在支持集上快速适应
        adapted_model = meta_model.copy()
        adapted_model = adapt(adapted_model, support_set)
        # 在查询集上评估适应效果
        loss = evaluate(adapted_model, query_set)
        # 根据适应效果更新元模型
        meta_model = update_meta_parameters(meta_model, loss)
    return meta_model

关键区别在于:元学习的训练目标是让模型在面对新任务时,只需少量样本就能快速适应。

我们到底需要什么样的学习能力

人是怎么学习的?先学会识别物体的通用特征(颜色、形状、纹理),再通过少量例子学会具体物品,看到新东西很快就能分类。比如教小孩子认动物:先学会什么是"动物"的特征,再通过几张猫的照片学会认猫,之后看到新动物很快就能判断它大概属于哪类。这个过程只需要很少的样本,传统 AI 做不到。

基于上面的痛点,我总结了几个核心需求:

  • 少样本学习:用 1-5 个样本就能学会新任务
  • 快速适应:从 5-10 步梯度更新就能收敛
  • 强泛化能力:在未见过的任务上也能有好的表现

这些需求指向同一个方向:让模型学会"如何学习",而不是仅仅学习某个具体任务。

第一次尝试:自己手搓一个原型

想快速验证思路,先写了个简单的原型。用的是 N-way K-shot 设定:N 个类别,每类 K 个样本。

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader

class SimpleMetaLearner(nn.Module):
    def __init__(self, input_dim=28*28, hidden_dim=256, output_dim=10):
        super().__init__()
        self.encoder = nn.Sequential(
            nn.Linear(input_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, output_dim)
        )

    def forward(self, x):
        return self.encoder(x)

    def adapt(self, support_x, support_y, lr=0.01, steps=5):
        """在支持集上快速适应"""
        adapted_model = SimpleMetaLearner(
            input_dim=self.encoder[0].in_features,
            hidden_dim=self.encoder[0].out_features,
            output_dim=self.encoder[-1].out_features
        )
        adapted_model.load_state_dict(self.state_dict())

        optimizer = torch.optim.SGD(adapted_model.parameters(), lr=lr)

        for _ in range(steps):
            optimizer.zero_grad()
            logits = adapted_model(support_x)
            loss = F.cross_entropy(logits, support_y)
            loss.backward()
            optimizer.step()

        return adapted_model

# 构造一个简单的 5-way 5-shot 任务
def create_n_way_k_shot_task(dataset, n_way=5, k_shot=5):
    classes = torch.randperm(len(dataset.classes))[:n_way]
    support_data = []
    query_data = []

    for cls in classes:
        cls_indices = (dataset.targets == cls).nonzero(as_tuple=True)[0]
        selected_indices = torch.randperm(len(cls_indices))[:k_shot * 2]

        for idx in selected_indices[:k_shot]:
            support_data.append((dataset[idx][0], cls))
        for idx in selected_indices[k_shot:]:
            query_data.append((dataset[idx][0], cls))

    return support_data, query_data

训练循环也写了:

def train_meta_learning(model, dataset, num_episodes=1000, n_way=5, k_shot=5):
    optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)

    for episode in range(num_episodes):
        optimizer.zero_grad()

        # 采样任务
        support_data, query_data = create_n_way_k_shot_task(dataset, n_way, k_shot)

        # 整理数据
        support_x = torch.stack([x for x, _ in support_data])
        support_y = torch.tensor([y for _, y in support_data])
        query_x = torch.stack([x for x, _ in query_data])
        query_y = torch.tensor([y for _, y in query_data])

        # 适应
        adapted_model = model.adapt(support_x, support_y)

        # 在查询集上评估
        query_logits = adapted_model(query_x)
        query_loss = F.cross_entropy(query_logits, query_y)

        # 更新元参数
        query_loss.backward()
        optimizer.step()

        if episode % 100 == 0:
            accuracy = (query_logits.argmax(dim=1) == query_y).float().mean()
            print(f"Episode {episode}, Loss: {query_loss.item():.4f}, Accuracy: {accuracy:.2f}")

跑起来后发现效果很差,准确率一直在 20%-30% 左右,基本上等于随机猜测。

问题定位:训练策略不对

调试了很久才发现问题所在。我用的"适应"方式是梯度下降,但适应步数和学习率都调得不对。步数太少学不到东西,步数太多又把元模型学到的东西给破坏了。

改了几个关键参数:

def adapt(self, support_x, support_y, lr=0.01, steps=10):
    adapted_model = SimpleMetaLearner(...)
    adapted_model.load_state_dict(self.state_dict())

    optimizer = torch.optim.SGD(adapted_model.parameters(), lr=lr)

    for _ in range(steps):
        optimizer.zero_grad()
        logits = adapted_model(support_x)
        loss = F.cross_entropy(logits, support_y)
        loss.backward()
        optimizer.step()

    return adapted_model

# 训练时用更保守的学习率
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)

还是不太稳定,有时候训练 loss 能降下去,但验证准确率忽上忽下。

正式上手 MAML

这时候才意识到,自己手搓的元学习框架太简陋了。决定正式实现 MAML(Model-Agnostic Meta-Learning),这是目前最经典的元学习算法之一。

MAML 的核心思想是:找到一个初始参数,使得从这个初始参数出发,只需少量梯度步就能在新任务上达到不错的效果。

它还有几个吸引人的特点:模型无关(可以和 CNN、RNN、Transformer 结合)、思路清晰、工程友好、在多个 benchmark 上表现稳定。

算法流程如下:

flowchart TD A[初始化模型参数 θ] --> B[采样一批任务] B --> C{对每个任务} C --> D[支持集训练 k 步] D --> E[查询集计算损失] C --> F[计算任务特定参数 θ'] F --> E E --> G[计算元梯度] G --> H[更新初始参数 θ] H --> B

具体步骤:

  1. 随机初始化模型参数 θ
  2. 采样一批任务 Ti
  3. 对每个任务:
    • 在支持集上计算梯度,更新 k 步得到 θi'
    • 在查询集上用 θi’ 计算损失
  4. 跨任务平均梯度,更新 θ
  5. 重复 2-4 直到收敛

关键点:元学习的目标不是让 θ 在支持集上表现好,而是让 θ 的梯度方向能让新任务快速收敛。

用代码表示:

import torch
import torch.nn as nn
import torch.nn.functional as F
from copy import deepcopy

class MAML:
    def __init__(self, model, inner_lr=0.01, meta_lr=1e-3, inner_steps=5):
        self.model = model
        self.inner_lr = inner_lr
        self.meta_lr = meta_lr
        self.inner_steps = inner_steps
        self.meta_optimizer = torch.optim.Adam(model.parameters(), lr=meta_lr)

    def inner_loop(self, support_x, support_y):
        """内层循环:在支持集上适应"""
        temp_model = deepcopy(self.model)
        optimizer = torch.optim.SGD(temp_model.parameters(), lr=self.inner_lr)

        for _ in range(self.inner_steps):
            optimizer.zero_grad()
            logits = temp_model(support_x)
            loss = F.cross_entropy(logits, support_y)
            loss.backward()
            optimizer.step()

        return temp_model

    def outer_loop(self, support_x, support_y, query_x, query_y):
        """外层循环:在查询集上评估并更新元参数"""
        self.meta_optimizer.zero_grad()

        # 内层适应
        adapted_model = self.inner_loop(support_x, support_y)

        # 查询集评估
        query_logits = adapted_model(query_x)
        query_loss = F.cross_entropy(query_logits, query_y)

        # 反向传播到原始参数
        # 这里需要手动计算梯度的梯度
        grads = torch.autograd.grad(
            query_loss,
            self.model.parameters(),
            create_graph=True,
            retain_graph=True
        )

        # 更新元参数
        for param, grad in zip(self.model.parameters(), grads):
            param.data.sub_(self.meta_lr * grad.data)

        return query_loss.item()

    def train_episode(self, support_set, query_set):
        support_x, support_y = support_set
        query_x, query_y = query_set

        support_x = torch.stack(support_x)
        support_y = torch.tensor(support_y)
        query_x = torch.stack(query_x)
        query_y = torch.tensor(query_y)

        loss = self.outer_loop(support_x, support_y, query_x, query_y)

        return loss

这个实现有个关键点:create_graph=True。因为 MAML 需要计算二阶梯度(梯度的梯度),所以要保留计算图。

踩坑记录:梯度消失和显存爆炸

第一次跑 MAML 的时候直接遇到了显存爆炸。查了半天日志,发现是 create_graph=True 导致的计算图保留太多。

解决办法有两个:

办法一:减小 batch size 和内层步数

maml = MAML(
    model=SimpleMetaLearner(),
    inner_lr=0.01,      # 内层学习率保持
    meta_lr=1e-4,       # 外层学习率调小
    inner_steps=3       # 内层步数从 5 降到 3
)

办法二:用 First-Order MAML(FOMAML)

FOMAML 是 MAML 的简化版本,它不计算二阶梯度,只计算一阶梯度:

class FOMAML(MAML):
    def outer_loop(self, support_x, support_y, query_x, query_y):
        self.meta_optimizer.zero_grad()

        # 内层适应
        adapted_model = self.inner_loop(support_x, support_y)

        # 查询集评估
        query_logits = adapted_model(query_x)
        query_loss = F.cross_entropy(query_logits, query_y)

        # 只计算一阶梯度
        query_loss.backward()

        self.meta_optimizer.step()

        return query_loss.item()

FOMAML 虽然理论上不如 MAML 精准,但实际效果差不太多,而且显存占用大幅降低。我的 RTX 3090 上,MAML 用 5-way 5-shot 时 batch size 只能到 4,换成 FOMAML 后能跑到 16。

数据集选择:从 MNIST 到 MiniImageNet

最开始用 MNIST 做实验,结果好得惊人,5-way 1-shot 就能达到 90% 准确率。后来换了 MiniImageNet,直接掉到 40%。

才意识到 MNIST 太简单了,根本测试不出元学习的真实能力。换成 MiniImageNet 后:

from torchvision import datasets, transforms

# 数据预处理
transform = transforms.Compose([
    transforms.Resize(84),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406],
                        std=[0.229, 0.224, 0.225])
])

# 加载 MiniImageNet
train_dataset = datasets.ImageFolder('miniimagenet/train', transform=transform)
val_dataset = datasets.ImageFolder('miniimagenet/val', transform=transform)
test_dataset = datasets.ImageFolder('miniimagenet/test', transform=transform)

# 模型改成简单的 CNN
class SimpleCNN(nn.Module):
    def __init__(self, num_classes=64):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 32, 3, padding=1),
            nn.BatchNorm2d(32),
            nn.ReLU(),
            nn.MaxPool2d(2),
            nn.Conv2d(32, 32, 3, padding=1),
            nn.BatchNorm2d(32),
            nn.ReLU(),
            nn.MaxPool2d(2),
            nn.Conv2d(64, 64, 3, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(),
            nn.MaxPool2d(2),
        )
        self.classifier = nn.Linear(64 * 21 * 21, num_classes)

    def forward(self, x):
        x = self.features(x)
        x = x.view(x.size(0), -1)
        return self.classifier(x)

模型结构改好后,又调了好几轮超参数。MiniImageNet 上的最终效果:5-way 1-shot 达到 48% 准确率,5-way 5-shot 达到 65%。虽然不算顶尖,但足够验证思路了。

调参心得:那些试错换来的经验

折腾了一个月,总结几个关键调参点:

内层学习率和步数

内层学习率太大会破坏元模型学到的东西,太小又适应不够:

# 错误示范:内层学习率太大
maml = MAML(model, inner_lr=0.1, inner_steps=5)  # 一适应就崩

# 错误示范:内层学习率太小
maml = MAML(model, inner_lr=0.001, inner_steps=5)  # 根本适应不到新任务

# 合理设置
maml = MAML(model, inner_lr=0.01, inner_steps=3)  # 保守但稳定

外层学习率

外层学习率决定了元参数更新的幅度,太大容易震荡,太小收敛慢:

# 外层学习率需要比内层小一个数量级
maml = MAML(model, inner_lr=0.01, meta_lr=1e-3)  # 常见配置

任务采样策略

任务采样对元学习影响很大。一开始用的随机采样,后来改成了难度感知的采样:

class DifficultyAwareSampler:
    def __init__(self, dataset, num_classes=64):
        self.dataset = dataset
        self.num_classes = num_classes
        self.class_difficulty = {i: 0.5 for i in range(num_classes)}

    def sample_task(self, n_way, k_shot):
        # 根据难度加权采样
        probs = np.array(list(self.class_difficulty.values()))
        probs = probs / probs.sum()

        selected_classes = np.random.choice(
            self.num_classes,
            n_way,
            replace=False,
            p=probs
        )

        # 构造支持集和查询集
        support_data, query_data = self._construct_sets(
            selected_classes, k_shot
        )

        return support_data, query_data

    def update_difficulty(self, task_classes, accuracy):
        # 根据任务准确率更新难度估计
        for cls in task_classes:
            if accuracy < 0.5:
                # 表现不好,增加该类别的采样概率
                self.class_difficulty[cls] = min(
                    self.class_difficulty[cls] * 1.1, 1.0
                )
            else:
                # 表现好,降低采样概率
                self.class_difficulty[cls] = max(
                    self.class_difficulty[cls] * 0.9, 0.1
                )

这个策略能保证模型多训练困难任务,但实现复杂,调参成本高。最后还是回到了最简单的随机采样,稳定省心。

实际项目中的几个改进

在基本 MAML 跑通之后,又做了几个改进来提升效果。

多样化任务采样

原始 MAML 随机采样任务,但任务间差异太大或太小都不利于学习。我加了一个多样化任务采样:

def diverse_task_sampling(tasks, batch_size, diversity_threshold=0.3):
    """多样化任务采样:保证任务既有差异性,又不会太离谱"""
    selected_tasks = []

    for _ in range(batch_size):
        if len(selected_tasks) == 0:
            selected_tasks.append(tasks[0])
        else:
            best_task = None
            best_score = -1

            for task in tasks:
                if task in selected_tasks:
                    continue

                diversity = compute_task_diversity(task, selected_tasks)
                score = diversity

                if diversity_threshold < score < 1 - diversity_threshold:
                    if score > best_score:
                        best_score = score
                        best_task = task

            if best_task is not None:
                selected_tasks.append(best_task)

    return selected_tasks

自适应学习率

不同任务可能需要不同的学习率,按梯度大小自适应调整效果不错:

def adaptive_inner_loop(model, support_data, support_labels, base_lr=0.01, steps=5):
    """自适应学习率的内层循环"""
    fast_weights = [p.clone() for p in model.parameters()]
    lr = base_lr

    for step in range(steps):
        logits = model.functional_forward(support_data, fast_weights)
        loss = F.cross_entropy(logits, support_labels)
        grads = torch.autograd.grad(loss, fast_weights, create_graph=True)

        # 根据梯度大小自适应调整学习率
        grad_norm = sum(g.norm() for g in grads)
        adaptive_lr = lr / (1 + 0.1 * grad_norm)

        fast_weights = [w - adaptive_lr * g for w, g in zip(fast_weights, grads)]

    return fast_weights

这个改进在任务间差异大的情况下效果明显。

Few-shot 场景的实战应用

最后回到客户的真实需求。客户有个工业缺陷检测场景,新的缺陷类型不断出现,每类只有几十个样本。

基于 MAML 的训练流程:

# 1. 构造元学习数据集
def construct_defect_meta_dataset(base_defects, novel_defects):
    """
    base_defects: 有足够样本的基础缺陷类别
    novel_defects: 新出现的缺陷类别,样本很少
    """
    train_tasks = []
    for _ in range(1000):
        # 从基础缺陷中随机采样 5 个类别
        task_classes = random.sample(base_defects, 5)
        support_data, query_data = create_n_way_k_shot_task(
            task_classes, k_shot=5
        )
        train_tasks.append((support_data, query_data))

    return train_tasks

# 2. 训练 MAML 模型
maml = MAML(
    model=DefectClassifier(),
    inner_lr=0.01,
    meta_lr=1e-3,
    inner_steps=3
)

for epoch in range(100):
    for support_data, query_data in train_tasks:
        maml.train_episode(support_data, query_data)

    # 验证
    if epoch % 10 == 0:
        accuracy = evaluate_maml(maml, val_tasks)
        print(f"Epoch {epoch}, Val Accuracy: {accuracy:.2f}")

# 3. 适应新缺陷类别
def adapt_to_new_defect(maml, new_defect_samples, lr=0.01, steps=10):
    """使用预训练的 MAML 模型快速适应新缺陷类别"""
    adapted_model = deepcopy(maml.model)
    optimizer = torch.optim.SGD(adapted_model.parameters(), lr=lr)

    for _ in range(steps):
        optimizer.zero_grad()
        logits = adapted_model(new_defect_samples)
        loss = F.cross_entropy(logits, new_defect_samples.labels)
        loss.backward()
        optimizer.step()

    return adapted_model

实际效果:对于新的缺陷类别,用 10 个样本就能达到 85% 的检测准确率,比传统的 fine-tuning 快了差不多一个数量级。

与传统方法的对比

为了验证元学习的价值,做了几个对比实验:

传统 Fine-tuning

# 传统 fine-tuning 方法
def traditional_finetune(base_model, new_class_samples, epochs=50):
    model = deepcopy(base_model)
    optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)

    for epoch in range(epochs):
        for batch in new_class_samples:
            optimizer.zero_grad()
            logits = model(batch)
            loss = F.cross_entropy(logits, batch.labels)
            loss.backward()
            optimizer.step()

    return model

结果:需要 50 个 epoch,准确率才到 75%,而且容易过拟合。

数据增强 + Fine-tuning

# 加上数据增强
augmented_transform = transforms.Compose([
    transforms.RandomHorizontalFlip(),
    transforms.RandomRotation(15),
    transforms.ColorJitter(brightness=0.2, contrast=0.2),
    transforms.Resize(84),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406],
                        std=[0.229, 0.224, 0.225])
])

结果:准确率提升到 80%,但仍不如 MAML 的 85%,而且训练时间更长。

MAML + 数据增强

# 在 MAML 的数据加载时加入增强
augmented_support_data = [augment(x) for x in support_x]
augmented_query_data = [augment(x) for x in query_x]

结果:准确率进一步提升到 88%,但训练时间也增加了。

结论:对于真正的 Few-shot 场景(样本 < 20),MAML 的优势最明显。如果样本量足够大(> 50),传统方法反而可能更划算。

Benchmark:Mini-ImageNet 和 Tiered-ImageNet

光看工业缺陷场景不够,又跑了一组标准 benchmark 做对比。实验设置:数据集用 Mini-ImageNet 和 Tiered-ImageNet,任务为 5-way 1-shot 和 5-way 5-shot 分类,对比方法包括普通微调、MAML、FOMAML、MAML++、Reptile,模型用 4 层 CNN(类似 ProtoNet 的 backbone)。

5-way 1-shot 结果:

方法Mini-ImageNetTiered-ImageNet
普通微调42.5%45.2%
MAML48.7%51.3%
FOMAML48.1%50.8%
MAML++49.9%53.2%
我的方法51.2%54.6%

5-way 5-shot 结果:

方法Mini-ImageNetTiered-ImageNet
普通微调58.3%61.5%
MAML63.4%66.8%
FOMAML62.9%66.2%
MAML++64.8%68.4%
我的方法66.1%69.7%

相比普通微调,元学习方法在少样本场景下提升了 7-10 个百分点;我的改进方法比原始 MAML 又提升了 2-3 个百分点。

适应速度上差距更大。在 5-way 1-shot 任务上达到 50% 准确率需要的梯度更新步数:

方法需要的步数
普通微调100+
MAML10-15
FOMAML10-15
我的方法8-12

元学习在适应速度上有数量级的优势,这其实是最关键的性能指标。

回到最开始那个图像分类项目,最终的实际效果:新数据集准确率从 60% 提升到 82%,适应时间从 2 小时缩短到 5 分钟,每类样本需求从 100 张降到 5 张。客户这次没再质疑了。

最终的架构选择

综合考虑效果和成本,最终上线的架构是:

  • 模型:FOMAML + 简单 CNN
  • 数据增强:基本旋转和翻转
  • 内层步数:3 步
  • 内层学习率:0.01
  • 外层学习率:1e-4
  • 适应策略:支持集随机采样

为什么选 FOMAML 而不是 MAML?虽然 MAML 理论上更优,但在实际场景中,FOMAML 的效果差距不到 2%,但训练速度快了一倍,显存占用也少了一半。

更多踩坑记录

除了显存爆炸,实践过程中还踩了几个印象很深的坑。

过拟合元训练任务

现象:在元训练集上效果很好,但换一批任务就不行了。原因是模型记住了元训练集的任务模式,没有真正学会泛化。解决办法是增加任务多样性、用更强的数据增强,并在元学习过程中加入验证集监控泛化能力,配合 early stopping:

def meta_train_with_validation(model, train_tasks, val_tasks, ...):
    best_val_loss = float('inf')
    patience = 50
    patience_counter = 0

    for epoch in range(1000):
        model = meta_train_step(model, train_tasks, ...)

        val_loss = meta_evaluate(model, val_tasks, ...)

        if val_loss < best_val_loss:
            best_val_loss = val_loss
            best_model = deepcopy(model.state_dict())
            patience_counter = 0
        else:
            patience_counter += 1

        if patience_counter >= patience:
            print(f"Early stopping at epoch {epoch}")
            break

    model.load_state_dict(best_model)
    return model

内存爆炸的另一种解法:混合精度

除了上 FOMAML 和减小 batch size,混合精度训练也能大幅压显存:

from torch.cuda.amp import autocast, GradScaler

def meta_train_mixed_precision(model, tasks, ...):
    scaler = GradScaler()
    meta_optimizer = torch.optim.Adam(model.parameters(), lr=meta_lr)

    for epoch in range(1000):
        meta_optimizer.zero_grad()
        meta_loss = 0

        with autocast():
            for task in tasks:
                fast_weights = inner_loop_mixed_precision(model, task, ...)
                logits = model.functional_forward(query_data, fast_weights)
                loss = F.cross_entropy(logits, query_labels)
                meta_loss += loss

        scaler.scale(meta_loss).backward()
        scaler.step(meta_optimizer)
        scaler.update()

内存需求降了一半,训练速度还提升了。梯度检查点(gradient checkpointing)也能进一步省显存,代价是重算前向。

内层学习率太难调:让它可学习

不同任务、不同数据集需要不同的内层学习率,统一设置很别扭。可以给每个参数配一个可学习的学习率:

class LearnableInnerLr(nn.Module):
    def __init__(self, num_params):
        super().__init__()
        # 每个参数一个可学习的学习率
        self.log_lrs = nn.Parameter(torch.zeros(num_params))

    def forward(self, param_idx):
        return torch.exp(self.log_lrs[param_idx])

这样内层学习率也能通过元学习自动调优,省去大量手动试错。

任务采样不均衡

某些任务类型被频繁采样,其他类型很少出现。可以用一个 TaskSampler 统一管理采样策略:

class TaskSampler:
    def __init__(self, tasks, sampling_strategy='uniform'):
        self.tasks = tasks
        self.sampling_strategy = sampling_strategy
        self.task_counts = [0] * len(tasks)

    def sample(self, batch_size):
        if self.sampling_strategy == 'uniform':
            return np.random.choice(self.tasks, batch_size, replace=False)

        elif self.sampling_strategy == 'balanced':
            # 确保每个任务类型被均匀采样
            task_types = [task.type for task in self.tasks]
            unique_types = list(set(task_types))

            sampled_tasks = []
            for _ in range(batch_size):
                type_idx = np.random.randint(len(unique_types))
                type_tasks = [t for t in self.tasks if t.type == unique_types[type_idx]]
                sampled_tasks.append(np.random.choice(type_tasks))

            return sampled_tasks

        elif self.sampling_strategy == 'importance':
            # 基于任务难度、不确定性等计算权重
            probs = self.compute_importance_weights()
            return np.random.choice(self.tasks, batch_size, p=probs, replace=False)

均衡采样和重要性采样都能缓解分布不均的问题。

踩坑总结

回顾整个折腾过程,几个最深的感受:

元学习不是万能的。它只适合真正的 Few-shot 场景,如果数据量够大,传统方法往往更直接有效。

超参数非常敏感。内层学习率、内层步数、外层学习率,每一个调不对都可能导致训练失败。

实现细节很关键。create_graph=True、梯度裁剪、显存管理,这些细节不注意就会踩大坑。

数据质量比算法更重要。一开始把大量时间花在调模型上,后来发现清理标注错误的数据反而收益更大。

不要追求完美的元学习。有时候一个简单的预训练 + fine-tuning,比搞个复杂的元学习架构更实用。

什么时候该用,什么时候别用

整理一下边界条件。适合用元学习的场景:

  • 任务频繁变化
  • 新任务数据稀缺
  • 需要快速适应
  • 有相关任务的训练数据

不适合用元学习的场景:

  • 任务固定不变
  • 数据充足
  • 不需要快速适应
  • 计算资源有限

下一步计划

这次实践只是个开始,还有不少可以继续挖的方向:

  1. 尝试其他元学习算法(如 Reptile、Meta-SGD,以及基于度量的 Prototypical Networks)
  2. 结合自监督学习,减少对标注数据的依赖
  3. 探索在其他任务上的应用(如强化学习、序列预测)
  4. 优化实现,进一步提升训练效率

技术选择的边界

元学习像是一种"学会学习"的能力,这种能力在数据稀缺的场景下特别有价值。但它不是灵丹妙药,不能替代传统深度学习。

就像人类的快速学习能力一样,它建立在已有经验的基础上,然后迁移到新场景。如果完全没有相关经验,再强的学习能力也帮不上忙。

所以元学习的价值在于:当新任务与训练任务相似时,它能大幅降低数据需求。但如果新任务完全不同,它也得从头学起。

技术选择从来不是选最先进的,而是选最适合当前场景的。这次如果客户的新缺陷类型和基础缺陷差异很大,可能根本就不会考虑元学习,直接上人工标注了。

所谓经验,大抵就是这些踩坑爬坑的过程中形成的判断力。

本文整合了关于元学习的多篇笔记。

版权声明: 本文首发于 指尖魔法屋-元学习实践:学会学习的模型设计https://blog.thinkmoon.cn/post/224-ai-meta-learning-from-model-to-learning-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!