「小样本」是一个问题:只给几个例子,怎么学会一个新任务?今天的四个概念是四种逐级递进的答案——从「复用旧权重」(迁移学习),到「学一个好的起点」(MAML),到「干脆不更新权重、用距离查表」(度量学习),最后到 LLM 最神奇的「连前向传播都不出、在 prompt 里就学会了」(In-context Learning)。核心张力始终是一句话:数据少到不够梯度下降时,「学习」该发生在哪一层?
迁移学习就是不从零写服务,而是 fork 一个跑顺了的基础镜像(base image),只改最上面那层业务逻辑。预训练模型 = 已经预热好连接池、装好通用依赖的母镜像;你的小数据集 = 那点业务定制。底层的通用能力(识别边缘、理解语法)早就有了,你只补「最后一公里」。
痛点:从零训一个深度网络要海量标注数据 + 巨量算力。但很多任务的底层特征是共享的——识别猫和识别狗都要先会识别边缘、纹理、形状。重新学一遍这些是浪费。
机制:拿一个在大数据集上预训练好的网络,冻结(freeze)前面大部分层(它们已经学会通用特征),只重新训练最后一两层来适配你的新任务。神经网络天然分层:浅层学通用低级特征(边缘、词法),深层学任务相关的高级特征。所以「底层冻结、顶层微调」既省数据又省算力。这是后面三个概念的共同地基——没有好的预训练表示,小样本学习根本无从谈起。
import torch, torch.nn as nn from torchvision import models # 1) 载入在 ImageNet 上预训练好的 ResNet(母镜像) net = models.resnet50(weights="IMAGENET1K_V2") # 2) 冻结所有层——通用特征不再更新 for p in net.parameters(): p.requires_grad = False # 3) 把最后的分类头换成你的任务(比如 5 类)——只有这层会训练 net.fc = nn.Linear(net.fc.in_features, 5) # 4) 优化器只收集 requires_grad=True 的参数(即新分类头) opt = torch.optim.Adam( (p for p in net.parameters() if p.requires_grad), lr=1e-3) # → 几百张图就能训出一个像样的分类器,而非几百万张
普通训练是「为某个具体任务找最优解」;MAML 是「找一个最好的『起点』,让它对任意新任务只需几步就能适应」。类比:你不是为每个新项目手搭环境,而是精心配一个黄金母镜像(golden base image)——任何新项目从它 clone 后,改三五行配置就能跑。MAML 优化的不是终点,是起点。
痛点:迁移学习复用的是「特征」,但新任务还是要训。能不能让模型学会「如何快速学新任务」本身?这就是元学习(meta-learning,「学会学习」)。MAML(Model-Agnostic Meta-Learning,Finn et al. 2017)给了一个优雅答案。
机制是双层优化(bi-level optimization),两个嵌套的循环:
用一行直觉化的公式说清外循环更新(每个符号都解释):
θ ← θ − β ∇θ Σ任务 i Li( θ − α ∇θ Li(θ) )
内层括号 θ − α∇Li(θ) 就是「在任务 i 上走一步」得到的 θ'(α 是内循环学习率);外层对这个适应后的损失再求一次梯度,去更新起点 θ(β 是外循环学习率)。关键反直觉点:这里出现了「梯度的梯度」——你在对「一步梯度下降之后会有多好」求导。MAML 不是在找「现在好」的参数,而是在找「一推就到位」的参数。
# MAML 一步外循环的骨架(伪 PyTorch,省去数据加载) def maml_step(theta, tasks, alpha=0.01, beta=0.001): meta_loss = 0 for task in tasks: sup, qry = task.support(), task.query() # 支持集/查询集 # 内循环:在 support 上走一步,得到适应后的 θ' loss = forward_loss(theta, sup) grad = torch.autograd.grad(loss, theta, create_graph=True) theta_prime = [w - alpha*g for w, g in zip(theta, grad)] # 用 θ' 在 query 上的损失累加到 meta_loss(注意是 query!) meta_loss += forward_loss(theta_prime, qry) # 外循环:对起点 θ 求二阶梯度并更新 meta_grad = torch.autograd.grad(meta_loss, theta) return [w - beta*g for w, g in zip(theta, meta_grad)] # create_graph=True 让我们能对「一步梯度后的损失」再求导(梯度的梯度)
前两个概念都还在「训练/更新权重」。原型网络换了思路:新任务来了根本不训,把每个类算成一个『质心向量』存起来,新样本来了算它离哪个质心最近——这就是答案。本质就是你熟悉的向量检索 + 最近邻(KNN),只不过嵌入空间是学出来的。「分类」退化成了「查表」。
痛点:MAML 每个新任务还要跑内循环梯度下降,在线推理时不够轻。能不能让「适应新类」完全不涉及梯度?原型网络(Prototypical Networks,Snell et al. 2017)的答案:把分类变成几何问题。
机制(这是 N-way K-shot 的标准设定——N 个新类、每类 K 个样本):
用 softmax 把距离变成概率(每个符号都解释):
p(y=k | x) = softmax( −d( f(x), ck ) )
d 是距离(通常欧氏距离),距离越近 → −d 越大 → 概率越高。整个「适应」过程就是「算几个平均向量」——零次梯度更新。模型真正学的是那个编码器 f:让同类样本在嵌入空间里抱团、异类拉开。一旦空间学好,分类新类就只是查询最近质心。这和 RAG 里「查最相关文档」是同一套向量几何。
import torch, torch.nn.functional as F # support: [N, K, D] 已编码向量;query: [Q, D] def proto_classify(support, query): # 1) 每个类的原型 = K 个支持向量的均值 → [N, D] prototypes = support.mean(dim=1) # 2) 每个 query 到每个原型的欧氏距离 → [Q, N] dist = torch.cdist(query, prototypes) # 3) 负距离过 softmax = 类别概率(越近概率越高) return F.softmax(-dist, dim=1) # 推理时:来一个全新的 5-way 任务,直接算原型即可分类 # 不需要任何反向传播——"适应"就是几次 mean() probs = proto_classify(support_emb, query_emb) pred = probs.argmax(dim=1)
前三个概念好歹都「算了点东西」(训权重、求原型)。ICL 最神奇:你在 prompt 里塞几个示例,模型权重一个字节都不变,它就「学会」了新任务。类比:数据库的 prepared statement——同一个引擎不重新编译,根据你传入的几个参数即时生成一个执行计划。「学习」发生在一次前向传播内部,推理结束即蒸发。
现象:GPT-3(Brown et al. 2020,「Language Models are Few-Shot Learners」)发现一件怪事——不微调、只在 prompt 里给几个「输入→输出」示例,大模型就能做新任务。这就是 in-context learning(上下文学习),是 prompt engineering 中 few-shot 的底层原理。但「为什么不更新权重也能学」一直是谜,目前有两条主流解释:
一个反直觉的实证(Min et al. 2022):把 few-shot 示例的标签随机打乱、故意标错,性能几乎不掉。说明示例的核心作用不是提供「正确答案」,而是提供任务的「格式、标签空间、输入分布」——即帮模型「定位」该激活哪种能力,呼应了贝叶斯解释。
| 范式 | 更新权重? | 「适应」发生在 |
|---|---|---|
| 迁移学习 | 是(顶层) | 微调阶段 |
| MAML | 是(内循环几步) | 显式梯度下降 |
| 原型网络 | 否 | 算几个均值向量 |
| ICL | 否(永不变) | 一次前向传播内 |
from anthropic import Anthropic client = Anthropic() # 读 ANTHROPIC_API_KEY 环境变量 # few-shot ICL:在 prompt 里给 3 个示例,权重完全不动 shots = ( "评论: 物流太慢了 → 负面\n" "评论: 质量超出预期 → 正面\n" "评论: 包装一般般 → 中性\n") resp = client.messages.create( model="claude-opus-4-8", max_tokens=16, messages=[{"role": "user", "content": shots + "评论: 客服回复很耐心 → "}]) print(resp.content[0].text) # → 正面 # 模型从 3 个示例里"读懂"了任务格式与标签空间, # 无需任何训练就完成分类——这就是 in-context learning