ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

TAML:从元学习到去任务偏见,让模型在少样本场景更公平

TAML:从元学习到去任务偏见,让模型在少样本场景更公平

1. 背景与问题:Few-shot Learning 和 Meta-Learning 到底在解决什么

做机器学习的时间久了,你会发现一个很“反直觉”的现实:我们拼命喂数据训练出来的模型,拿到一个新任务上往往脆弱得像纸糊的。哪怕只是换了个数据分布,模型可能就需要重新训练,更别提只给几个样本的极端场景了。Few-shot Learning 要解决的,就是让模型在看到极少样本(比如每个类别5张图)的情况下,仍然能快速学会新任务。

但这里有个核心矛盾:常规训练方式学到的参数是基于源任务分布的,换到新任务时,初始参数并不是“空的”,而是带着源任务的偏见。这种偏见在少量样本下会被放大,导致模型很难快速适应。Meta-Learning 的答案是“learn to learn”,通过多个任务来学习一个更好的初始条件,让模型见过足够多的任务类型之后,在新的 few-shot 任务上能更快收敛。MAML(Model-Agnostic Meta-Learning)就是最有名的一类方法,它直接对初始参数做梯度更新,目标是让初始参数在一步或几步梯度之后就能适应新任务。

MAML 的框架很简单:在多个任务上,从当前参数出发,每个任务做几步梯度更新,然后计算这些更新后的参数在对应任务上的 loss,再对初始参数求梯度。公式写出来就是:

θ ← θ - β ∇_θ Σ_T L_T(θ - α ∇_θ L_T(θ))

其中内层 θ' = θ - α ∇_θ L_T(θ) 是任务 T 上的快速适应,外层是对初始参数 θ 的元梯度更新。这个方法在实际中很有效,但它有一个隐含假设:所有任务的梯度方向大致一致,或者至少最终能收敛到一个对多数任务都友好的区域。

问题恰恰出在这里。MAML 优化的只是“在所有任务上平均表现好”,它没有约束每个任务适应之后的性能差异。假设有三个任务,其中一个任务用初始参数适应后 loss 降得非常快,另外两个任务挣扎半天还停在原地,平均值可能仍然好看,但初始参数实际上已经偏向那个“轻松任务”了。这个就是 task bias——模型对任务分布不敏感,被某些任务牵着鼻子走。

TAML 这篇工作就是针对这个问题提出来的。它的思路非常直接:我们不光要优化平均表现,还要让初始参数对所有任务保持“中立”,也就是 task-agnostic。具体做法是在 MAML 的元目标里加一项正则,惩罚不同任务在适应后的性能差异,同时用熵最大化来鼓励初始参数对任务不产生过强偏好。

这一篇博文我会从 TAML 的核心思想、两种实现形式、算法细节、实验结论,以及我实际复现时踩过的坑这几个角度展开。适合已经大概了解 MAML,想进一步理解 task-agnostic 概念,或者准备在项目里尝试 TAML 的读者。

2. TAML 是怎么做到 task-agnostic 的

2.1 从“平均表现”到“性能分布均衡”

要理解 TAML,先要抓住一个转变。MAML 的目标是最小化所有任务适应后 loss 的和(或均值),数学上看就是:

min_θ Σ_{T~p(T)} L_T(θ - α ∇_θ L_T(θ))

TAML 在同样框架下,加了第二个约束:不同任务适应后的 loss 要尽可能接近。也就是说,我们希望初始参数 θ 不仅能让每个任务适应后 loss 都很低,而且这些 loss 的离散程度也要很小。这个离散程度可以用方差、标准差、或者更通用的散度来衡量。

为什么强调这一点?因为 few-shot 场景下,少数样本本身噪声大,如果初始参数已经偏向某些任务,那么遇到一个新任务时,它的适应过程极可能沿着偏见的方向走,导致泛化能力下降。task-agnostic 的初始参数就像一个不带“立场”的中介,不管新任务来自哪个分布,它都能公平地提供起点。

TAML 的做法是用一个正则项 R 来惩罚不同任务之间的性能差异。硬版本(hard TAML)里,R 取任务适应后 loss 的最大熵;软版本(soft TAML)里,R 取任务适应后 loss 的方差或标准差。这样元目标变成:

min_θ Σ_T L_T(θ') + λ · R(θ)

其中 θ' = θ - α ∇_θ L_T(θ)。λ 控制公平性约束的强度。

2.2 熵最大化与任务偏好的关系

要说清楚 hard TAML,先得聊一个概念:熵。在信息论里,熵刻画的是分布的不确定性。分布越均匀,熵越高;分布越尖峰,熵越低。对一个分类模型来说,假设输出是类别概率分布,如果这个分布非常尖锐(比如某个类概率接近1),说明模型非常确定自己的判断。

如果在元学习场景下,我们拿初始参数去跑一组任务,把每个任务适应后的 loss 归一化成一个分布(比如所有任务 loss 归一化后总和为1),那么熵就能描述这个 loss 分布的均匀程度。loss 分布越均匀,熵越高,意味着所有任务表现越接近;反之,如果某个任务 loss 特别小、其他任务 loss 特别大,归一化后的分布会非常不均匀,熵就低。

hard TAML 正是把“让 loss 分布熵最大化”作为正则项。当熵达到最大时,所有任务适应后的 loss 趋于相等,初始参数不偏心。这样训练出来的初始参数,不会因为某个任务梯度更强、样本更多或者 loss 曲面更平就“倒向”那一侧。

2.3 soft TAML 与方差正则的直觉

soft TAML 用了另一个度量:任务 loss 的方差。如果说熵是全局分布角度的度量,方差就是数值离散度的度量。方差越小,说明不同任务表现越接近。soft TAML 的正则项可以写成:

R_var = Σ_{i,j} (L_i - L_j)^2 的某种归一化形式,或者直接表示为 loss 向量的经验方差。

相比 hard TAML,soft TAML 计算更简单,梯度也更平稳。因为熵对分布的敏感度受归一化方式影响,如果任务数量少或 loss 数值量级差异大,熵的计算容易不稳定。方差作为二阶矩,在梯度反传时相对直接。

两种形式的本质是相同的:约束初始参数不要过度适配任何单一任务。区别在于“不公平”的度量标准不同。hard 更强调概率均匀性,soft 更强调数值接近性。实际中两者的表现接近,但计算复杂度和稳定性、以及需要调节的超参数(比如 λ)有所不同。

3. TAML 算法核心细节与推导

3.1 任务采样与支持集/查询集的划分

在描公式之前,先锁定训练流程。每次迭代,从任务分布 p(T) 中采样一批任务,比如 batch size 为 4 或 8。每个任务 T_i 都有自己的支持集(support set)和查询集(query set)。支持集用来做内层快速适应,查询集用来计算适应后参数的表现,也就是元目标中的任务 loss。

举个例子:假设做 5-way 1-shot 图像分类,每个任务随机选5个类别,每个类别1张图作为支持集;查询集再从这些类别里取一批图片,比如每类15张。

内层适应时,模型用支持集计算梯度 ∇_θ L_T_i(θ),然后做一步(或几步)梯度下降,得到任务特定参数 θ'_i。这个 θ'_i 是跟任务相关的临时参数,它不会直接更新初始模型,只有外层元梯度更新才会最终改变 θ。

外层目标里,除了每个任务在查询集上的 loss,TAML 还要算这些 loss 之间的差异。于是得到损失:

L_TAML(θ) = Σ_{i=1}^B L_T_i(θ'_i, query) + λ · R({L_T_1, ..., L_T_B})

这个式子看起来很简单,但实际实现时有两个容易忽略的点。第一,内层梯度计算涉及二阶导数,因为 θ'_i 是 θ 的函数,所以外层对 θ 求导时需要通过 θ'_i 对 θ 的依赖关系反传梯度。第二,正则项 R 是多个任务 loss 的函数,它拉平任务间差异的趋势,但如果你对 batch 内所有任务求 R,那么 batch 中任务的选择会影响正则梯度的稳定性。因此通常会适当增大 batch size,让正则估计更可靠。

3.2 hard TAML 的熵正则推导

hard TAML 把任务 loss 转换成一个概率分布。常见做法是:先算每个任务在查询集上的 loss L_i,然后做归一化:

p_i = L_i / Σ_{j=1}^B L_j

注意这里要求 L_i 都是非负的。常见的分类 loss 比如交叉熵本身就是非负的,所以没问题。如果 loss 可能为负,就需要先做平移或使用其他转换,比如 softmax。

然后定义熵:

H(p) = -Σ_{i=1}^B p_i log p_i

正则项就是 -H(p),因为我们要最大化熵,所以最小化负熵。于是整体损失:

L = Σ_i L_i + λ · (-H(p))

梯度怎么算?这里有意思的地方在于,p_i 依赖所有任务的 loss,而每个 loss 又依赖对应的 θ'_i。所以当你反传熵正则项对 θ 的梯度时,需要先把 -H(p) 对每个 L_j 求偏导,然后再接上 L_j 对 θ 的链式梯度。数学形式不复杂,但实现时需要将 batch 内所有任务的计算图保持到外层,不能提前 detach。

有一个细节:熵最大化的力度随着任务数量的变化而变化。当任务数量 B=1 时,归一化后 p=1,熵为0,正则不起作用。B 越大,熵的最大值 ln B 越大,正则的潜力越大。所以如果你在实现中 batch size 很小(比如2),hard TAML 的效果会非常有限,这是要注意的。

另外,如果 loss 数值非常大或非常小,p_i 可能接近0或1,导致 log 出现数值不稳定。实践中需要给 p_i 加一个很小的 epsilon 截断,防止 NaN。

3.3 soft TAML 的方差正则推导

soft TAML 使用方差或标准差作为正则。在 batch 内,定义任务 loss 的均值:

μ_L = (1/B) Σ_i L_i

方差:

σ^2 = (1/B) Σ_i (L_i - μ_L)^2

正则项直接取 σ^2。整体损失:

L = Σ_i L_i + λ · σ^2

为什么有时候用标准差 σ 而不是 σ^2?因为 σ^2 是平方量级,对 loss 的绝对尺度非常敏感。如果每个任务 loss 本身很大,方差也会很大,需要很大的 λ 才能起效果;用标准差可以减少尺度影响,但梯度在 σ=0 附近有奇点,需要做微小偏移。

从梯度的角度看,soft TAML 的正则梯度是让 batch 内各个任务 loss 向均值靠拢。如果某个任务 loss 高于均值,正则会增大它的梯度权重,反过来压低它;如果低于均值,正则会减小它的梯度权重,让它在元更新中不那么突出。这个机制比 hard TAML 更“直观”一点,因为它直接作用于数值差异,而熵是通过概率分布间接作用的。

实现时,soft TAML 不需要归一化,也不用担心 log(0)。它的主要超参数是 λ。λ 太大,模型会过分强调公平性,可能导致所有任务都“平均地差”,即整体表现被拉低;λ 太小,正则项形同虚设。实际中 λ 往往需要通过验证集搜索,常见范围是 0.01 到 1,具体取决于 loss 的量级。

3.4 与 MAML 的统一视角

把 TAML 放到 MAML 的统一框架里看,它只是在 MAML 的 loss 后面加了一个额外的约束项。这个约束项在计算时,不依赖单个任务内部的数据,而是依赖 batch 内多个任务之间的相对表现。

因此,TAML 可以说是一种“即插即用”的元学习正则化方案。如果你的代码里已经实现了 MAML,那么改造为 TAML 的步骤非常简单:

  • 计算 batch 内每个任务的内层适应,得到 θ'_i;
  • 计算每个任务在查询集上的 loss L_i;
  • 根据 L_i 列表计算熵正则项或方差正则项;
  • 将正则项乘以 λ 加到总 loss 上;
  • 对总 loss 求梯度,回传到初始参数。

这也是 TAML 吸引人的地方:它没有对模型结构做任何假设,任何可以用 MAML 的模型都可以直接用 TAML。CNN、RNN、全连接网络,理论上都兼容。

4. 实验设计:如何在基准数据上验证 task-agnostic

4.1 基准数据集与任务构建

TAML 论文中用的实验设置是 few-shot 分类和回归的经典基准。分类任务常用 Omniglot 和 miniImageNet。Omniglot 有 1623 个字符类别,每个类别只有少量样本,非常适合 few-shot。miniImageNet 从 ImageNet 中选了 100 个类别,每类 600 张图,是评估元学习算法的标准数据集之一。

任务构建方式:N-way K-shot 分类。N 表示每次任务中抽几个类别,K 表示每个类别用几张支持集图片。常见配置是 5-way 5-shot、5-way 1-shot、20-way 5-shot。回归任务常用的是正弦波回归:每个任务的振幅和相位随机变化,支持集给几个点,模型需要拟合出曲线。这个任务虽然简单,但能很好反映模型对新函数分布的适应能力。

我个人觉得,评估 TAML 时有个容易被忽略的点:任务批大小(episodes per batch)。TAML 的正则依赖 batch 内的任务多样性。如果你在复现时 batch size 太小(比如1),完全无法计算任务间差异;如果 batch size 是2,正则只能在两个任务之间“较劲”,估计的方差噪声极大。论文使用较大的 batch,比如 4 或 8,才能让正则信号稳定。

4.2 关键对比指标

除了常规的准确率或 loss,TAML 特别值得关注一个指标:不同任务之间性能的方差。比如,在每个训练 epoch 结束后,在验证集上跑一批任务,记录每个任务适应后的准确率,然后计算这些准确率的标准差。这个标准差越小,说明模型越 task-agnostic。

传统 MAML 即使平均准确率不低,任务之间的准确率 std 往往很大——有的任务准确率90%,有的只有60%。TAML 的贡献就是把这个 std 压下来。你在自己实验里,不要只盯着平均 acc,建议把每个任务的 acc 分布画出来。我见过不少复现报告说 TAML 平均表现和 MAML 差不多,但分布明显更集中,这说明它确实达成了设计目标,只是平均指标没有充分体现。

还有一个隐形收益:task-agnostic 的模型在遇到分布外的任务时更稳。因为初始参数不偏向训练任务分布,遇到稍微偏离分布的新任务时,内层适应不容易被“带跑”。如果想验证这个性质,可以在训练完后,构造一些和训练任务分布不同的测试任务(比如训练时 N=5,测试时 N=10;或者训练时用灰度图,测试时加噪声)。这种情况下 TAML 的优势会体现得比 MAML 更明显。

4.3 消融实验的作用

TAML 有两个可调节的部分:正则类型(hard/soft)和正则强度 λ。做消融实验时,可以固定其他条件,单独改变 λ 观察变化。λ=0 时,TAML 退化为 MAML。随着 λ 增大,训练集上的平均 loss 可能会略升,但任务间方差下降。当 λ 过大时,平均 loss 显著上升,因为模型为了公平性牺牲了拟合能力。通过这个曲线,你可以找到最优 λ。

另一个消融点是内层适应步数。MAML 常用一步梯度更新,但 TAML 的正则统计是在适应后计算的,因此内层步数会影响正则信号。步数多,每个任务适应得更充分,任务间差异更接近“收敛后的差异”;步数少,差异更接近“初始参数直接的表现”。论文中一般使用一步或两步。我实测过,在噪声较大的数据上,两步适应的正则效果反而更平滑,因为单步适应容易受支持集噪声干扰。

5. 复现 TAML 的实操过程与踩坑记录

5.1 代码架构建议

如果你想在一个已有 MAML 实现上加入 TAML,改动最少的办法是在“计算每个任务的 queryset loss”之后,先不要立刻对 loss 求和,而是先收集一个列表。具体流程:

# 伪代码: MAML + TAML 风格 meta_loss_list = [] for task in task_batch: # 内层适应 adapted_params = inner_update(model, task.support, task.query) # 得到 theta_i' # 查询集 loss loss_i = compute_loss(model, adapted_params, task.query) meta_loss_list.append(loss_i) # 计算任务间正则 if taml_type == 'hard': reg = entropy_regularizer(meta_loss_list) elif taml_type == 'soft': reg = variance_regularizer(meta_loss_list) meta_loss = sum(meta_loss_list) + lambda * reg meta_grads = torch.autograd.grad(meta_loss, model.parameters())

注意,inner_update中的梯度更新必须保持计算图完整,不能 detach 支持集梯度的依赖。有些人为了省内存会做 detach,那会导致外层元梯度完全错误。如果你内存紧张,可以考虑用检查点(checkpoint)等技术,但不要直接用 detach 代替。

5.2 熵正则和方差正则的具体实现

熵正则实现时,一定要处理数值边界。下面是一份 PyTorch 风格的参考:

def entropy_regularizer(loss_list): # loss_list: list of torch scalars, length = batch_size losses = torch.stack(loss_list) # 每个loss需要梯度 # 归一化到概率分布 # clamp防止log(0) eps = 1e-8 probs = losses / (losses.sum() + eps) # 计算熵的负值 neg_entropy = torch.sum(probs * torch.log(probs + eps)) return neg_entropy # 最小化负熵 = 最大化熵

在这个实现里,我们返回的是负熵,直接加进总 loss 最小化。因此loss_total = sum_loss + lam * neg_entropy。

方差正则实现:

def variance_regularizer(loss_list): losses = torch.stack(loss_list) mean = losses.mean() var = torch.mean((losses - mean) ** 2) return var

如果你担心量纲问题,可以改为返回标准差:

std = torch.sqrt(var + 1e-6) return std

实际测试中,用标准差比用方差更稳定。因为 loss 的水平会随着训练推进下降,方差也会同步下降,正则项可能在后期变得很小;而标准差同样会下降,但下降速度更平缓。最好根据你的 loss 量级对比一下两种形式的曲线。

5.3 超参数调整的经验

λ 是 TAML 最敏感的超参数。我在几个数据集上做过简单搜索,给出参考范围:

  • Omniglot 5-way 5-shot:λ 在 0.05 ~ 0.2 之间比较好。超过 0.5 会导致平均acc明显下降。
  • miniImageNet 5-way 5-shot:loss 量级更大,λ 可能需要 0.1 ~ 0.5。
  • 正弦回归任务:loss 是 MSE,一般小于1,λ 取 0.01 ~ 0.1 即可。

原因其实很简单:正则项必须和任务 loss 在一个量级,才能起到有意义的平衡作用。如果任务 loss 平均是 0.5,而你 λ 取 10,那么正则会完全压过任务 loss,模型只顾着拉平任务间差异,不管拟合质量了。建议先跑一个不加热 sig 的 MAML,记录训练初期和末期的任务 loss 范围,再让 λ × 正则项 大约占任务 loss 的 10% ~ 50%,这个区间通常比较合理。

还有一点,任务 batch size 不够大的时候,方差正则的梯度噪声很大。我在复现时试过 batch=2,loss 曲线剧烈抖动。增大到 batch=8 后,正则的梯度才稳定。如果你显存有限,可以减小内层步数或者使用较浅的网络,但不要牺牲 batch size。

5.4 一个容易忽略的二阶导数问题

TAML 和 MAML 一样,涉及对初始参数 θ 的二阶梯度。原理上内层适应是 θ → θ'_i 的过程,而外层 loss 是 θ'_i 的函数,所以总体是复合函数。PyTorch 的自动微分能处理,但需要确保create_graph=True。

在 PyTorch 中,典型写法:

adapted = model.parameters() - alpha * grads # 这里 grads 是支持集loss对参数的梯度 # 注意 grads 的求导需要 create_graph=True

后面再用 adapted 参与查询集 loss 计算,外层 autograd 就会自动构造二阶图。如果你用torch.autograd.grad求外层梯度,记得设置create_graph=False(默认)就好,因为外层不需要再求二阶了。

我遇到的常见错误是,内层求梯度时忘了加create_graph=True,导致 adapted 是“叶子节点”,和原始参数断了联系,外层梯度变成0。这个问题不报错,但训练完全不更新,特别坑。排查时可以先检查 adapted 的grad_fn是否为 None。如果是 None,说明计算图断了。

6. 常见问题与排查技巧实录

6.1 训练不收敛,loss 变成 NaN

最常见的原因是熵正则的 log(0)。当任务 loss 归一化后,某个任务 loss 占绝对主导地位,其他任务 loss 接近0,probs 中出现0,log(0) 直接 NaN。解决办法:

  • 在 log 里面加 epsilon,比如 1e-8;
  • 对 loss 做 softmax 而不是归一化?实际上 softmax 也需要温度参数,不如直接加 epsilon;
  • 检查任务 loss 是否为负数,比如 MSE 不会负,但某些自定义 loss 可能负,需要对 loss 做 max(0, loss) 或平移。

另一个 NaN 来源是内层更新的学习率 α 过大,导致支持集梯度更新后参数发散。尤其在使用二阶梯度时,发散会被放大。建议在调试时先用小 α(比如 0.01),确认没问题再调大。

6.2 正则项不生效,模型表现和 MAML 一样

如果你发现加了正则和没加结果几乎相同,先检查一下 λ 是不是被设成了0,或者正则项的值是否一直很小。打印正则项数值,如果它在训练初期就一直是0或接近0,说明实现有 bug。

hard TAML 中,如果 batch 内所有任务 loss 都一样,熵正好等于 ln(B)。如果你的模型在训练初期对所有任务都给出相似的 loss——这在小规模数据上有可能——那么正则梯度接近0,更新缓慢。随着训练进行,任务差异才会体现,正则开始起作用。不要因为开头几步没变化就认为实现错了。但如果你训练了几百步后,正则项数值和 ln(B) 差了十万八千里,那大概率就是梯度没传回来。

soft TAML 中,如果正则项一直很小,可能因为你的任务 loss 很接近。这时可以把 λ 调大看看,如果调大后任务 loss 出现反效果,说明正则路径是通的;如果怎么调 loss 曲线都不变,那就是梯度没有接上。

6.3 计算资源不足,怎么缩减显存

TAML 比 MAML 多一个 batch 维度的统计量,如果实现时把每个任务的查询集都保留完整计算图,显存会比较大。我的建议:

  • 使用梯度检查点(gradient checkpointing)重放前向过程;
  • 减少每个任务的查询集样本数量,比如每类只取5张而不是15张;
  • 内层适应只做一步;
  • 如果任务数很多,可以把一个 batch 拆成两个子 batch,分别计算正则,再对两个正则取平均。

不过要注意,子 batch 计算的正则和完整 batch 的正则并不完全等价。如果子 batch 内任务类型刚好都相似,正则会错误地认为没有分歧;所以拆分子 batch 时要确保每个子 batch 包含多样的任务。一个更彻底的方案是使用 Reptile 式的近似,但那就不是 TAML 了,不推荐在正统实现里走这条路。

6.4 评估时应该用多少任务才靠谱

TAML 本身是让任务间性能更均衡,但如果你评估时只跑 10 个任务,随机性太大,标准差算出来不可信。我建议在 miniImageNet 上至少跑 1000 个任务,Omniglot 上 600 个任务,取平均和标准差。每个任务单独进行内层适应,不要复用训练时的支持集,否则会高估性能。

还有一个细节:评估时的支持集数量要和训练一致。比如训练用 5-shot,测试也用 5-shot。如果你测试时只给 1-shot,模型的 task-agnostic 特性可能反而让它不如专门偏向高频任务的模型表现好,因为任务分布变了。TAML 追求泛化,但泛化不等于在任何变化下都优于 MAML,这一点要理性看待。

7. 个人对 TAML 的体会与后续扩展

我在实际踩过 MAML 的坑之后再看 TAML,最大的感悟是:很多元学习算法一味追求平均表现,忘了“平均”能被少数极端任务拉高,而实际部署时我们更关心模型在陌生任务上是否稳定。TAML 的正则思路不算复杂,但它把 task bias 这个问题显式地摆到了损失函数里,这是它在方法论上的价值。

如果你想把 TAML 进一步扩展,可以考虑两个方向。第一,把熵正则从分类任务迁移到强化学习或在线学习场景,那里的任务分布更动态,task-agnostic 可能更有意义。第二,结合可学习的 λ,让模型自动平衡平均表现和公平性,而不是人工固定。现在已经有类似 Meta-Weight-Net 的做法,用一个小网络根据任务loss分布调整每个任务的权重,和 TAML 有异曲同工之处。

最后给准备复现的朋友一个建议:不要一上来就在 miniImageNet 上跑,先用一个简单的正弦回归任务验证逻辑。它的任务构造简单、模型小、训练快,能够快速暴露梯度连接和数据处理的 bug。逻辑走通后,再换到图像数据集上,你会发现排障速度快很多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进