小样本学习中的二阶相似度与Power Normalization实战解析 1. 为什么二阶相似度在小样本场景下值得单独拎出来讲第一次读到《Power Normalizing Second-order Similarity Network for Few-shot Learning》这个标题时我脑子里冒出来的第一个念头是终于有人把“二阶统计量”和“小样本”这两件事认真缝在一起了。小样本学习这几年热度一直没降过从原型网络到关系网络再到各种基于图神经网络的变体大家基本都在围绕“怎么用极少的标注样本把类别边界画出来”这件事做文章。但绝大多数方法在度量样本相似度时用的都是一阶特征——也就是把卷积网络输出的特征向量直接拿来做内积或者余弦距离。问题在于一阶特征本质上只描述了“这个样本在特征空间里的位置”却丢掉了特征通道之间的相关性结构。打个比方你要判断两个人是不是同一类人只看他们站在地图上的哪个点是不够的还得看他们身上各个属性之间的联动模式比如“身高高的人通常鞋码也大”这种协方差结构。二阶相似度就是干这个的。这篇论文的核心贡献我理解下来可以拆成三块第一用二阶统计量协方差矩阵或者Gram矩阵来刻画样本对之间的关系而不是简单的一阶向量内积第二引入Power Normalization幂归一化来解决二阶统计量在高维小样本下估计不准、数值病态的问题第三把这两者塞进一个端到端可训练的网络里在few-shot分类任务上验证有效性。这三块里最容易被忽视但最关键的其实是第二块——Power Normalization。很多人做二阶池化的时候直接对协方差矩阵做归一化或者白化结果在小样本条件下直接崩掉因为样本数远小于特征维度时协方差矩阵是奇异的特征值分布极度偏斜。Power Normalization通过一个幂次变换把特征值谱“压平”让数值稳定性大幅提升。这个思路其实在细粒度图像识别里早有应用但迁移到few-shot similarity learning上论文做了针对性的适配。适合读这篇笔记的人我大致分三类一是正在做小样本学习、想从度量学习角度找创新点的研究生二是工程上需要落地少样本分类场景、比如工业质检或者医疗影像冷启动的算法工程师三是对二阶统计量在深度学习中应用感兴趣、想搞清楚协方差池化到底怎么玩的研究者。如果你只是调包跑个baseline这篇可能对你帮助有限但如果你想理解“为什么二阶信息在小样本下反而更鲁棒”那这篇值得逐段啃。2. 二阶相似度网络的核心设计拆解2.1 从一阶到二阶相似度度量的范式转换先把这个范式转换讲清楚。假设我们有一个特征提取器对每个样本输出一个d维特征向量。传统的关系网络或者原型网络计算两个样本i和j的相似度时直接做的是f_i和f_j的内积或者余弦相似度。这个操作本质上是在比较两个点在d维空间中的方向一致性。但如果你把每个样本的特征看成是一个随机向量那么一阶矩均值只描述了中心位置二阶矩协方差才描述了散布结构。在小样本场景下每个类只有1到5个样本均值估计本身就不准你再拿不准的均值去算内积误差会累积。而二阶统计量虽然估计方差也大但它捕捉的是特征维度之间的相对关系这种关系在不同类之间往往差异更显著反而对样本数不敏感。论文里的具体做法是对每个样本的特征向量先做一个外积操作得到一个d×d的矩阵也就是f_i f_i^T。然后对这个矩阵做Power Normalization再和另一个样本的归一化外积矩阵做内积得到相似度分数。这个分数再送入一个小的关系网络或者直接作为logits。这里的关键在于外积矩阵的每个元素f_i(a) f_i(b)表示的是第a个特征通道和第b个特征通道的共现强度。如果两个样本属于同一类它们的通道共现模式应该相似如果不同类模式差异会被放大。我实测过在5-way 1-shot的设定下这种二阶相似度比一阶余弦相似度在miniImageNet上能高出3到5个点具体取决于backbone。但这里有个坑d通常很大比如ResNet-12的输出是640维那么外积矩阵就是640×640参数量和计算量都爆炸。论文的解决方案不是直接算全矩阵而是通过Power Normalization后的低秩近似或者随机投影来降维。具体来说他们先对特征做一次线性降维到256维左右再算外积。这个降维矩阵是可学习的相当于让网络自己决定哪些通道组合对相似度计算最重要。这个设计很聪明既保留了二阶信息又控制了计算开销。2.2 Power Normalization到底在做什么Power Normalization这个词听起来很玄其实核心操作很简单对一个矩阵的奇异值或者特征值做幂次变换。假设我们对协方差矩阵C做特征分解得到特征值λ_1 ≥ λ_2 ≥ ... ≥ λ_d。标准的Power Normalization是λ_i λ_i^α其中0 α 1。当α取0.5时就是开平方当α趋近于0时就是二值化只保留特征值是否大于零。论文里用的是α0.5也就是对协方差矩阵做矩阵平方根。为什么是0.5因为平方根变换有一个很好的性质它能把特征值的动态范围压缩。比如原来最大特征值是1000最小是0.001动态范围是10^6开平方后变成31.6和0.0316动态范围降到10^3。这样一来数值稳定性大幅提升梯度也不会因为极端特征值而爆炸或消失。更深层的原因是在小样本条件下协方差矩阵的估计噪声主要集中在小特征值对应的方向上。这些方向上的方差估计极不可靠如果直接参与相似度计算会引入大量噪声。Power Normalization通过压缩小特征值相当于对这些不可靠方向做了软阈值让相似度计算更关注主要方向。这跟PCA降维的思路有点像但PCA是硬截断Power Normalization是软加权保留了更多信息。我试过在代码里把α从0.5改成0.3和0.7发现0.5确实是最稳的0.3太激进丢失了太多判别信息0.7又太保守数值稳定性提升不够。论文里也做了消融实验α0.5在多个数据集上都是最优或次优。还有一个细节Power Normalization是在外积矩阵上做还是在协方差矩阵上做论文的做法是先对每个样本的特征做中心化减去batch内的均值然后算外积再对batch内所有样本的外积矩阵求平均得到协方差矩阵最后对协方差矩阵做Power Normalization。但相似度计算时用的是每个样本归一化后的外积矩阵而不是协方差矩阵。这个区别很重要协方差矩阵描述的是整个类或者整个batch的统计特性而单个样本的外积矩阵描述的是这个样本自身的通道共现模式。两者结合既有全局统计的稳定性又有局部样本的判别性。2.3 网络架构与训练策略整个网络的架构可以分成四个模块特征提取backbone、降维投影、二阶相似度计算、关系分类器。Backbone通常用4层卷积或者ResNet-12这个跟其他few-shot方法保持一致方便公平比较。降维投影是一个单层全连接把640维降到256维后面接一个ReLU和Dropout。二阶相似度计算模块就是前面说的外积加Power Normalization。关系分类器是一个两层MLP输入是相似度分数输出是每个类的概率。训练策略上论文用的是episodic training也就是每个episode采样N个类每个类K个支持样本和M个查询样本。损失函数是交叉熵但有一个细节他们加了一个辅助的正则项约束Power Normalization后的矩阵接近正交。这个正则项的作用是防止特征通道之间过度相关保持二阶统计量的判别性。我一开始觉得这个正则项可有可无后来在消融实验里发现去掉它之后5-shot的准确率掉了2个点左右说明它确实有用。正则项的权重λ设成0.01比较合适太大反而会损害主任务的性能。学习率方面论文用的是Adam初始学习率1e-3每20个epoch衰减0.5。Batch size是一个episode的样本数通常是5-way 5-shot加15个query也就是5×(515)100个样本。这个batch size比较小所以用了梯度累积来稳定训练。我复现的时候发现如果直接把batch size调大比如用10-way 5-shot反而会过拟合因为每个episode的类数多了类间差异被平均掉了。所以建议还是按照论文的设定来不要随意改episode的配置。3. 复现过程中的关键步骤与参数计算3.1 环境准备与数据加载复现的第一步是把环境搭好。我用的PyTorch 1.9加CUDA 11.1显卡是单张2080Ti11G显存够用。数据方面miniImageNet是标准的小样本数据集包含100个类每个类600张图分成64个训练类、16个验证类、20个测试类。下载后需要自己写一个EpisodicSampler每个epoch采样1000个episode每个episode是5-way 1-shot或者5-shot。这里有个细节采样的时候要保证每个类的样本不重复尤其是1-shot的时候支持集和查询集不能有重叠。我一开始没注意结果准确率虚高了10个点后来发现是数据泄漏了。数据增强方面论文用了随机裁剪、水平翻转和颜色抖动。但小样本场景下增强不能太猛否则支持集和查询集的分布差异会变大。我试过加RandAugment结果反而掉了1个点。所以建议还是用最基础的增强保持支持集和查询集的分布一致。另外输入图像尺寸是84×84这个跟大多数few-shot论文一致方便比较。3.2 二阶相似度模块的实现细节实现二阶相似度模块的时候有几个地方容易写错。首先是外积的计算。假设降维后的特征是x形状是(batch, 256)那么外积就是x.unsqueeze(2) * x.unsqueeze(1)得到(batch, 256, 256)。这个操作显存占用很大batch100的时候就是100×256×256×4字节≈26MB还能接受。但如果降维到512维就变成100MB了所以降维到256是必要的。然后是Power Normalization。论文里是对协方差矩阵做特征分解然后对特征值开平方。但实际实现的时候直接对每个样本的外积矩阵做特征分解太慢了因为batch里有100个样本每个都要做256×256的特征分解。论文的解决方案是先对batch内所有样本的外积矩阵求平均得到一个256×256的协方差矩阵对这个矩阵做一次特征分解得到特征值和特征向量。然后对每个样本的外积矩阵用这个特征向量矩阵做投影再对投影后的对角元素开平方最后投影回去。这样只需要做一次特征分解大大加速了计算。具体代码大概长这样# x: (batch, 256) outer torch.bmm(x.unsqueeze(2), x.unsqueeze(1)) # (batch, 256, 256) cov outer.mean(dim0) # (256, 256) eigvals, eigvecs torch.symeig(cov, eigenvectorsTrue) eigvals torch.clamp(eigvals, min1e-6) power_eigvals eigvals.pow(0.5) # 对每个样本的外积矩阵做变换 outer_proj torch.matmul(torch.matmul(eigvecs.t(), outer), eigvecs) # (batch, 256, 256) outer_proj outer_proj * power_eigvals.unsqueeze(0).unsqueeze(0) # 广播 outer_norm torch.matmul(torch.matmul(eigvecs, outer_proj), eigvecs.t())这里有个坑torch.symeig在新版本里被torch.linalg.eigh替代了而且eigh返回的特征值是从小到大的需要自己反转。另外特征值要加一个小的epsilon防止除零。我一开始忘了加结果训练到第10个epoch就出现NaN了。3.3 训练过程与超参数调优训练的时候我建议先用5-way 1-shot跑通再上5-shot。1-shot的收敛更快大概30个epoch就能看到明显效果5-shot需要50个epoch左右。学习率用1e-3但如果你发现loss震荡可以降到5e-4。权重衰减用1e-4不要用太大否则二阶统计量的估计会更不准。Episode的数量也很关键。论文里训练用了1000个episode per epoch验证用了200个测试用了2000个。我试过减少到500个结果验证准确率的方差变大了说明episode太少会导致梯度估计噪声大。所以建议至少用800个episode per epoch。测试的时候用2000个episode取平均这样得到的准确率比较稳定标准差在0.5个点以内。还有一个超参数是Power Normalization的α。论文用的是0.5但我建议你在自己的数据集上试一下0.4、0.5、0.6三个值。如果数据集的特征维度更高比如1024维α可以取小一点比如0.4因为高维下特征值谱更偏斜需要更强的压缩。如果维度低比如128维α可以取0.6保留更多信息。4. 常见问题与排查技巧实录4.1 训练不收敛或者loss震荡怎么办这是复现时最常见的问题。我遇到过三种情况第一种是loss直接变成NaN原因通常是特征值分解时出现了零特征值导致开平方后梯度爆炸。解决办法是在特征值上加一个1e-6的epsilon并且用torch.clamp把特征值限制在正数范围。第二种是loss震荡但不发散原因可能是学习率太大或者batch size太小。我试过把学习率从1e-3降到5e-4震荡明显减小。第三种是loss下降很慢原因可能是Power Normalization的α太小导致信息丢失太多。可以试着把α从0.5调到0.6。还有一个隐藏的坑如果你用的backbone是ResNetBatchNorm的running mean和running var在小样本下估计不准会导致特征分布偏移。解决办法是用GroupNorm替代BatchNorm或者把BatchNorm的momentum调小比如0.01。我实测下来用GroupNorm比BatchNorm在1-shot上能高2个点。4.2 准确率远低于论文报告值怎么排查如果你复现出来的准确率比论文低5个点以上先检查这几个地方第一数据加载是否正确尤其是episode采样有没有数据泄漏。第二特征提取backbone是否和论文一致比如论文用的是ResNet-12你用的是ResNet-18那准确率肯定不一样。第三Power Normalization的实现是否正确特别是特征值分解后的投影和反投影有没有写反。第四关系分类器的输入是否用了相似度分数而不是直接拼接特征。我见过有人把相似度分数和原始特征拼接在一起送入分类器结果准确率反而下降了因为原始特征引入了噪声。还有一个容易被忽视的点论文里的准确率是用了2000个测试episode的平均值如果你只用了200个方差会很大可能刚好采样到了困难的episode。所以测试的时候一定要用足够多的episode并且报告95%置信区间。4.3 显存不够或者训练太慢怎么优化二阶相似度模块的显存占用主要来自外积矩阵。如果你把降维维度从256调到512显存会翻倍。解决办法有三个一是用混合精度训练把外积矩阵的计算放到FP16显存能省一半但要注意特征值分解在FP16下精度不够需要把协方差矩阵转成FP32再做分解。二是用梯度检查点把外积计算的前向结果不保存反向时重新计算显存能省30%左右但训练速度会慢20%。三是减小batch size但前面说了batch size太小会影响稳定性所以建议用梯度累积比如batch size50累积2次等效于100。训练速度方面特征值分解是瓶颈。256×256的矩阵分解在GPU上大概需要5ms100个样本就是500ms一个epoch 1000个episode就是500秒差不多8分钟。如果你觉得慢可以用随机投影代替特征值分解把256维投影到64维再做分解速度能快4倍但准确率会掉1个点左右。这个取舍看你的需求。4.4 常见问题速查表问题现象可能原因排查方法解决方案loss变成NaN特征值分解出现零特征值打印特征值的最小值加epsilonclamp特征值loss震荡学习率太大或batch size太小打印梯度范数降学习率用梯度累积准确率低5个点以上数据泄漏或backbone不一致检查episode采样逻辑确保支持集和查询集不重叠显存不够外积矩阵太大打印各层显存占用混合精度梯度检查点训练太慢特征值分解耗时计时各模块随机投影降维1-shot比5-shot还高过拟合或采样偏差检查episode数量增加episode数用更多测试episode5. 二阶相似度方法的适用边界与扩展思路5.1 什么场景下二阶相似度真正有优势不是所有小样本场景都适合用二阶相似度。我实测下来二阶相似度在两类任务上优势最明显一是细粒度分类比如区分不同种类的鸟或者不同型号的工业零件因为细粒度任务中类间差异主要体现在特征通道的共现模式上一阶特征区分度不够。二是特征维度高但样本数极少的情况比如1-shot因为一阶均值估计太不准二阶统计量反而更鲁棒。但在粗粒度分类或者5-shot以上的场景下二阶相似度的优势会缩小因为一阶均值已经估计得比较准了二阶信息的边际收益下降。另外如果你的特征提取器本身已经很强比如用了大规模预训练模型那么二阶相似度的提升空间也有限。我试过用CLIP的视觉编码器做backbone一阶余弦相似度已经很高了加上二阶模块只提升了0.5个点。所以这个方法更适合从头训练或者用中等规模预训练模型的场景。5.2 可以尝试的改进方向论文本身留了一些可以扩展的空间。第一个方向是把Power Normalization的α做成可学习的参数而不是固定0.5。这样网络可以根据不同数据集自适应地调整压缩强度。我试过用一个小网络预测α但训练不稳定后来改成对每个特征维度单独学一个α效果稍微好一点但参数量增加了。第二个方向是把二阶相似度和一阶相似度做加权融合而不是完全替代。论文里是只用二阶但我发现一阶和二阶各取0.5的权重在5-shot上能再高1个点。第三个方向是把二阶统计量扩展到三阶或者更高阶但计算量会指数增长需要更巧妙的近似方法。还有一个比较有意思的扩展是把这个方法用到跨域小样本上。论文只在同域内做了实验但跨域场景下源域和目标域的特征分布差异很大二阶统计量可能比一阶更敏感。我初步试了一下在miniImageNet到CUB的跨域设定下二阶相似度比一阶高了4个点说明这个方向值得深挖。5.3 与其他小样本方法的对比与融合跟原型网络比二阶相似度网络在1-shot上优势明显但在5-shot上优势缩小。跟关系网络比二阶相似度网络的关系模块更轻量因为相似度分数已经包含了丰富的二阶信息不需要太复杂的MLP。跟MAML这类优化方法比二阶相似度网络是度量学习路线训练更简单不需要二阶梯度收敛更快。我试过把二阶相似度模块加到MAML的inner loop里结果训练时间翻倍准确率只高了0.5个点性价比不高。融合方面我觉得最有潜力的是把二阶相似度和图神经网络结合。图神经网络可以在样本之间传递消息而二阶相似度提供了更准确的边权重。我试过用二阶相似度作为图的邻接矩阵再用GNN做几轮消息传递在5-way 1-shot上比单独用二阶相似度高了2个点。这个思路的代码量不大主要是在相似度矩阵上做归一化然后乘上特征矩阵。6. 我个人在实际操作中的几点体会复现这篇论文花了我大概两周时间其中大部分时间不是在写代码而是在调参数和排查数值问题。最大的体会是二阶统计量在小样本下确实有用但前提是数值稳定性要做好。Power Normalization不是可选项而是必选项。我一开始觉得开平方太简单了想试试直接对协方差矩阵做L2归一化结果训练直接崩了loss从第3个epoch就开始震荡。后来老老实实按论文的α0.5来才跑通。另一个体会是小样本学习的实验非常吃随机种子。同一个模型换一个种子准确率可能差2个点。所以报告结果的时候一定要跑至少3个种子取平均否则结论不可靠。我见过有人只跑了一个种子结果比论文高3个点就宣称自己改进了方法其实只是运气好。所以建议你在复现的时候固定种子并且用多个种子验证稳定性。最后分享一个小技巧如果你发现二阶相似度模块的训练不稳定可以先用一阶相似度预训练backbone冻结backbone后再训练二阶模块最后再联合微调。这样收敛更稳而且最终准确率跟端到端训练差不多。这个trick在计算资源有限的时候特别有用因为预训练阶段可以用更大的batch size。