☰
自适应子空间小样本学习:从点到子空间的度量学习与复现实践
2026/10/16 5:26:17 网站建设 项目流程

这篇论文阅读笔记,我其实欠了很久。手头一直在做小样本图像分类的落地项目,各种baseline跑过一轮之后,最后让我愿意停下来细读的就是《Adaptive Subspaces for Few-Shot Learning》。Few-Shot Learning的本质矛盾就一句话:数据那么少,凭什么要求模型有泛化能力?现有的做法要么从元学习角度想办法,让模型学了之后能快速适应新任务;要么从度量学习角度想办法,让模型在新任务上直接比距离。这篇论文把度量学习往前推了一步,它告诉你:不要把一个类别当成一个点,把它当成一个子空间来用。这个改动乍一听抽象,但你要是做过一点PCA或者流形学习,就会意识到它的价值。我会从动机、方法、代码、复现避坑四个层面把这篇论文完整拆开,尽量让不熟悉子空间概念的读者也能顺利读完。

1. 小样本学习里,类到底应该长什么样

1.1 为什么点表示会不够用

小样本学习最常见的做法是原型网络那一套:每个类取支撑样本的均值,得到一个原型点,查询样本来了就分给距离最近的原型。这个思路简洁高效,但它暗含一个很强的假设——每个类在特征空间里是近似各向同性的"团",用中心点就能代表全貌。现实情况往往不是这样。

举一个我自己在业务里遇到的例子。有一组室内场景图像需要分类,同一个"卧室"类别下,有暖色调的卧室、有极简设计的卧室,还有带飘窗的卧室。它们在特征空间里会是好几个簇,均值点会落在这些簇之间的空白地带。查询样本恰好落在这个空白地带附近时,分类就很容易出错。更麻烦的是1-shot场景,整个类只有一个支撑样本,均值定义得再合理,也只是一根独苗,点表示完全丢掉了样本周围可能存在的几何结构信息。

这就是点表示的局限:它把类内方差压缩成了零,把样本间的相关性也随手丢掉了。哪怕数据再少,支撑样本之间也是存在相对位置的,这些相对位置本身是有判别力度的。这个道理说起来简单,但大多数度量学习模型刻意回避了这一点,因为处理点和点之间的距离太方便了,处理集合和集合之间的关系就麻烦不少。

1.2 从点到子空间:允许类内变化

论文的核心观点非常直白:与其用一个点表示类,不如用一个线性子空间表示类。子空间可以理解为特征空间里经过原点(或某个锚点)的一个低维平面,它被这个类自己的支撑样本所张成。你不需要精确复原类内所有样本的分布,只需要捕捉支撑样本所能揭示的主要变化方向。这在几何上相当于:点是一个零维对象,子空间是带有方向的一维或多维对象,能表达的判别信息自然更丰富。

打个比方,点和子空间的差别就像"邮递地址"和"活动范围"。邮递地址告诉你一个精确位置,但活动范围告诉你这个人平时会在哪一带出现。小样本场景下,我们明明能看到几个活动痕迹,却偏要压缩成邮递地址,显然是浪费信息。子空间的好处在于,它能用少数几个支撑样本刻画出这个类允许的变化方式,查询样本哪怕不在支撑样本附近,只要落在子空间覆盖的"走廊"里,就仍然有机会被正确识别。

当然,子空间也不是万能的。线性子空间只能表达直线型的分布结构,真遇到极度非线性的流形分布,它的拟合能力也会到头。但论文的立场是:在小样本条件下,连协方差矩阵都估计不准确,追求复杂的非线性流形模型反而容易过拟合,低维线性子空间反而是统计上更稳的选择。

1.3 "自适应"到底自适应了什么

光有子空间还不够,如果直接使用点到子空间的欧氏距离,本质上还是在用一个固定坐标系衡量一切。论文的标题里藏着第二个关键词:Adaptive。这里的自适应体现在两个层面。第一,子空间的维度和基向量是从每个任务的支持集动态构造的,任务不一样,子空间就不一样,这是表示层面的自适应。第二,也是更关键的一点,论文在距离度量里引入了一个可学习的马氏矩阵,相当于给特征空间的各个通道重新分配权重和相关性,这个矩阵会随着训练任务的不断采样被优化,从而适应不同任务中类别边界的形状。

换句话说,子空间给了你一个"形状",自适应度量给了这个形状一个"标尺"。形状再合理,标尺不合适也量不准。这两个设计合在一起,才构成了论文所讲的 Adaptive Subspaces。后面的方法部分会展开讲这个标尺是怎么学出来的。

2. 方法拆解:自适应子空间到底怎么训练

2.1 整体流程一句话版

标准的few-shot任务包含支持集和查询集。支持集有N个类,每类K个样本,模型要用这NK个样本判断查询集的归属。论文的完整流程可以压缩成四步。第一步,用特征提取网络把支持和查询样本全部映射成特征向量。第二步,对支持集里每个类,用该类样本的特征向量构造一个子空间,并做正交化处理。第三步,定义查询向量到每个类子空间的"自适应距离",距离计算中带一个可学习的度量矩阵。第四步,用一对多的最大间隔损失来训练整个系统,让正确的类距离更近、错误的类距离更远。

这四步里,第一步和普通的度量学习没有区别,第三步和第四步是论文的精华所在。我逐个拆开讲。

2.2 子空间如何从支撑样本中长出来

假设某个类有K个支撑样本,特征维度是D。把这些样本的特征向量按行堆叠,就得到一个K×D的矩阵。这个矩阵的行向量张成一个子空间,维度最多是K,因为行之间如果线性独立,秩最大就是K。但实际中我们通常希望控制子空间的秩,让它的维度比K小,这样可以过滤掉噪声方向。

数值实现上最稳的办法是做QR分解,把支撑样本矩阵分解成一个正交矩阵和一个上三角矩阵。正交矩阵的前r列就是子空间的一组正交基。我不建议直接拿原始支撑样本当作基向量用,因为非正交基会让后续的距离计算产生冗余,还会让训练过程中的梯度传播不稳定。正交基相当于把一个类的表示"拧干"了,只保留方向信息,丢掉样本之间重复的分量。

还有一个细节值得注意:子空间的维度r怎么定。论文里一般允许r最大取到K-1或者K,但实际调参时我发现,r取得太大会导致距离区分度下降,因为子空间维度越高,查询向量距离子空间越近的概率越大,所有类都显得"亲近";r取得太小又丢掉了太多信息。一个比较实用的经验是先取r=K-1,观察验证集表现再往下减。另一点是,如果支撑样本恰好线性相关,QR分解会得到秩亏的正交基,代码里需要加一点正则项或者截断处理,这个我放到复现章节细说。

2.3 自适应距离:马氏距离如何嵌入子空间

点到子空间的欧氏距离,本质上是先求查询向量在子空间上的投影,再算投影残差的二范数。这个距离在特征通道彼此独立、方差相当时是好用的,但实际学习到的特征通道之间往往存在相关性。有些通道组合起来才有判别意义,单独看毫无价值。为了利用这种通道间的相关结构,论文把二范数换成马氏范数,也就是在残差向量上乘一个半正定矩阵M,再计算二次型。

M是全局共享的,但在每个episode训练中都会通过反向传播更新,所以它会随着任务分布自适应地调整。实现上为了避免直接优化半正定约束带来的麻烦,一般会把M参数化成低秩分解M=AA^T的形式。这样既保证了半正定性,也减少了参数数量,还有了"先投影到低维再算距离"的直观解释。A可以是r_m×D的矩阵,训练时只需要学A就行了。

一个我读完论文比较感慨的细节是:这个自适应距离并不是简单替代欧氏距离而已,它改变了整个问题的几何语义。有了M之后,查询向量到子空间的距离不再是一个固定值,而是会随着学习到的度量在特征空间中"伸缩"空间。这相当于在每个任务上都重新校准了一遍坐标轴,让类间距离真正具有可比性。

2.4 最大间隔训练目标

距离有了,还需要一个损失函数让距离变得有判别力。论文选择了一对多的最大间隔损失,思路很像多分类SVM。对于查询样本x,如果它的真实类别是c,那么它到S_c的距离应该比到任何其他类子空间的距离小,而且要小一个间隔margin。损失函数可写成对每个错误类别j计算d_c - d_j + margin,再取和或取最大值,只惩罚那些违反间隔约束的项。

之所以不用更常见的softmax交叉熵,我觉得是出于几何一致性的考虑。softmax鼓励的是相对概率,间隔损失强调的却是绝对的几何间隔,它在优化时会直接拉开不同子空间之间的边界,让分类决策更接近"支持向量机"式的最优分隔。论文的训练方式和MAML这类元学习不同,它没有内层多次梯度更新,而是直接通过episode采样来端到端训练特征提取器和度量矩阵,这大大简化了训练流程。

整个训练过程是标准的episodic training:从训练集里随机抽取任务,每个任务是一个N-way K-shot问题,然后计算损失、反向传播、更新参数。训练收敛后,推理时只需要构造支持集子空间,对每个查询样本比较自适应距离,取距离最小的类作为预测。

3. 复现记录:从公式到PyTorch

3.1 数据组织与骨干网络

复现第一步是把数据组织成episode。我的做法是写一个自定义DataLoader,每次从训练类别里随机抽N个类,再从每个类抽K个支撑样本和Q个查询样本。批量大小不用太大,4到8个episode就够,关键是要有足够的任务多样性。

骨干网络我沿用了论文实验里常见的小型卷积网,4层卷积加ReLU加BatchNorm,最后接一个全局平均池化,输出维度D=64。这个backbone看起来简单,但在小样本benchmark上几乎是标准配置。如果你想把backbone换成ResNet或更大的模型,需要注意特征维度变大后,后续的QR分解和距离计算开销也会明显上升,而且小数据量下大模型更容易过拟合,损失函数会掉得很慢。

3.2 子空间构造的批量实现

子空间构造的PyTorch代码其实非常短,核心就是torch.linalg.qr。假设support_features的形状是[batch, n_way, k_shot, D],我先reshape成两维,然后批量做QR分解。因为PyTorch的qr支持批量维度,只要把支撑样本矩阵放在最后两维即可。

一个关键处理是,QR分解出来的Q矩阵形状是[batch, n_way, k_shot, D],我要小心地只取前r列。如果直接切片,r是一个超参数。我一般取r=min(k_shot-1, D),避免子空间满秩导致数值不稳。另外,为了数值安全,我会在支撑样本矩阵上加上一个很小的单位阵扰动,防止特征完全零向量或者样本线性相关时QR分解炸掉。

3.3 自适应距离的计算

计算自适应距离时,我先把每个类的正交基B(形状是[n_way, r, D])用低秩矩阵A完成度量变换。A的形状是[r_m, D],M=A^T A。查询样本x先减去其在子空间上的投影,得到残差e。然后距离就是残差乘M再乘残差的转置。实现时要注意批量运算的broadcast维度,稍不留神维度就会对不上。

我在实现时为了避免显式构造M,直接分两步:先把残差乘A^T,得到一个低维表示,再对它求平方和。这实际上是先对残差做线性变换,再计算欧氏距离的平方。这样做既节省内存,也等价于原始的马氏距离。对于每个查询样本,我计算它到所有类子空间的距离向量,形状是[n_way],然后用这个向量构造损失。

3.4 训练循环与梯度稳定性

训练循环和普通小样本学习没有本质区别,先取support和query特征,算距离,算loss,反向传播。但梯度传播经过QR分解时有时候会不稳定。特别是当子空间接近满秩或者矩阵接近奇异时,QR分解的反向传播会产生极大的梯度值。我的处理方式是在支撑样本矩阵里加一个eps参数,并给梯度加一个全局裁剪。

自适应度量矩阵A的学习率也需要单独控制。我把backbone的学习率设为1e-3,A的学习率设为1e-4,这样做的原因是A的参数量虽小,但对几何结构的影响极大,学习率大了会在前期就把子空间顶到奇怪的形态,后期很难收回来。另外,我习惯在每个episode之后对A做一次正交约束的软正则,防止A的秩坍缩到太低。如果你不做这个正则,训练后期距离计算会退化,所有类的距离都趋于相似,准确率会波动。

代码层面还有一个容易忽略的地方:QR分解得到的方向存在符号不确定性,也就是基向量可能随意翻转。这对距离计算没有影响,因为距离用的是投影残差,只要子空间不变,符号不会改变投影。但如果你用Q的行列式做可视化或者正则化,就会踩到符号不一致的坑,建议直接无视Q的符号,只把它当作几何对象来用。

4. 实验结果与避坑实录

4.1 论文报告的性能趋势

论文在常见的两个小样本基准上都报告了正向提升。我复现之后,最大的感受是:5-shot场景的提升幅度比1-shot更明显。原因不难理解,支撑样本越多,能张成的子空间就越能反映类内主要变化方向,子空间相对于点表示的信息优势就越大。1-shot时每个类只有一个样本,子空间退化成一维"线",但它仍然携带了方向信息,所以和点表示相比还是略有优势,只是优势没那么显著。

这种性能趋势其实给了我们一个启发:子空间方法的收益大小,取决于你手头支撑样本的丰富程度。如果你的业务场景普遍只有1到2个样本,子空间带来的增益可能有限,不如老老实实做数据增强或者预训练;但如果你有3到5个样本,子空间方法就非常值得一试。

4.2 复现时最折磨人的几个坑

第一个坑是QR分解梯度爆炸。我遇到的情况是:训练到某个epoch后loss突然变成NaN。定位之后发现是某个类的支撑样本矩阵接近奇异,QR分解方向上的梯度发散到极大。解决方案就是不洁的样本矩阵加单位阵扰动,并开启梯度裁剪。

第二个坑是子空间维度r的敏感性。我一开始图省事,直接令r=K(比如5-shot就取5),结果模型在验证集上的表现反而比原型网络还差。后来把r降到4,效果立刻回暖。我理解是因为r等于样本数时,子空间能完全拟合支撑集噪声,模型的泛化能力反而被削弱。维度降到小于样本数的水平,等于被迫把支撑样本中冗余的方向截断,这反而起到了一种正则化的作用。

第三个坑是M矩阵退化。当你把A设置的秩太大或者训练不够充分时,M会逐渐退化成近似单位阵,导致自适应能力失效。判断标准非常简单:训练过程中看距离数值的均值是否一直在下降。如果所有距离都在缩小,说明M在坍缩。我会在loss里加一个小权重的距离平方惩罚项,或者定期检查A的特征值分布。

第四个坑是1-shot下子空间的方向敏感度。只有一个支撑样本时,子空间方向完全由这一个样本决定,它离真实类中心稍微偏一点,整个距离计算就会跟着偏。我的缓解办法是让特征提取器输出特征做一次l2归一化,再通过一个可学习的缩放因子调整范数权重。这相当于先把样本投影到球面上,再用一个缩放参数模拟未归一化的效果,稳定性好很多。

4.3 关于超参数和经验值的记录

我把复现过程中使用的有效超参数整理成一个速查表,方便后面再跑实验时快速对照。

子空间维度r取K-1,1-shot时退化为1;低秩矩阵A的秩取16到32;支撑样本矩阵的eps扰动取1e-4;margin松弛取0.5到1.0;backbone学习率1e-3,A学习率1e-4;优化器用Adam,weight decay取1e-4;训练epoch视数据集规模而定,一般300到500个episode为一个阶段。

这些数值不一定每个场景都最优,但作为起点很够用。特别是margin的选择,我试过从0.1到2.0的几组值,发现margin太小会让类间约束太弱,训练过程不收敛;margin太大则会让梯度期望值过大,训练震荡。0.5到1.0范围算是比较稳的甜点区间。

5. 延伸思考:这个思路还能怎么用

5.1 子空间思想给我的三点启发

第一,表示方式比距离函数更本质。做度量学习的人经常会花大量时间设计距离函数,却没有想过"比较的对象"到底应该是什么。这篇论文告诉你,从点到子空间这一步,比你在距离函数里加多少花活都更有效。

第二,低秩结构在小样本场景下是一个被低估的正则化器。很多人在小样本任务上一味增加模型容量,结果越学越差。子空间本质上强制了一个低秩假设,这在高噪声低样本场景下反而带来稳定性。我后来在其他项目里也用这个思路做特征筛选,效果不错。

第三,自适应度量可以嵌入很多现成框架。论文的马氏距离不是只能配子空间,你完全可以把它移到原型网络上,就变成自适应原型网络;移动到图神经网络分类器上,也可以替换掉里面的邻居聚合权重。这是一个即插即用的模块,拆出A矩阵单独研究应该会很有意思。

5.2 局限性与可以改进的地方

论文最大的局限是子空间的线性假设。真实特征分布往往是非线性流形,线性子空间只能捕捉主要方向,无法表达弯曲。一个自然的改进方向是先学习一个非线性映射,把特征投影到更适合做线性子空间的空间,再套用整篇论文的流程。甚至可以端到端地学习一个李群变换,让每个类的子空间都对齐到共同坐标系。

另一个局限是子空间构造只考虑了类内关系,没有显式利用类间冲突。如果两个类在子空间上接近,它们的基向量会很相似,但损失函数并没有一个显式项去推动基向量正交化。实际上可以加一个跨类的基向量正交正则项,直接让不同类子空间尽量垂直。我在一个二分类数据集上试过,加了这项目后收敛速度明显加快,说明这个方向有潜力。

计算复杂度方面,每类都要做QR分解,分类数一多开销就会变大,不适合在线推理场景。后续如果有人能提出不依赖QR分解的参数化子空间,比如直接让网络预测一个基向量组,就可以绕开分解这一步,把计算复杂度压下来。

5.3 真实业务场景里的联想

我在处理工业质检小样本缺陷分类时,经常遇到同类缺陷形态多变的情况。比如划痕有短的、长的、弯的,用点表示很容易把不同形态判成不同类。子空间天然允许这种"形态变化",只要变化方向没有超出该类基向量张成的范围,就能保持同一个类别。这个特性对很多真实场景都很贴合,因为真实世界里的同类物体本来就存在正常范围内的形变。

在医疗影像或者遥感情景里,支撑样本往往来自不同拍摄条件、不同角度,子空间可以比均值点更好地容纳这些条件差异。不过需要注意,在这类高维小样本场景里,特征维度可能远大于样本数,QR分解的代价会很高。建议先对特征做一次降维,把D压到64或128,再构造子空间,否则训练速度会非常感人。

我个人的体会是,读这篇论文真正有价值的部分不是那几个公式,而是它逼你去重新思考类表示这件事。点、子空间、流形、概率分布,本质上是同一件事在不同信息量下的表达方式。数据充分时用分布,数据中等用流形,数据紧缺时,子空间是在信息量和鲁棒性之间最划算的一个折中。后面如果再让我设计小样本分类器,我会先把这个问题想清楚:我的类表示承载了多少信息,又丢弃了多少不该丢的东西。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询