1. 项目概述当图像分类遇上“样本荒”在计算机视觉的日常研发中图像分类是个绕不开的基础活。无论是安防监控里的人脸识别还是电商平台的商品自动归类背后都依赖一个强大的分类模型。传统的深度学习方法比如我们熟知的ResNet、VGG确实厉害但它们有个“富贵病”得“吃”下海量的标注数据才能训练出好效果。标注一万张猫狗图片可能还行。但如果是医疗影像中罕见的病理切片或是工业质检里新出现的缺陷类型收集成百上千的标注样本成本高、周期长甚至可能根本找不到那么多正例。这就是我们常说的“少样本学习”要啃的硬骨头。少样本学习的目标很明确让模型学会“举一反三”。比如只给模型看3到5张“斑马”的照片它就得能学会识别所有斑马而不是只会认训练时见过的那几只。这听起来有点像人类的学习方式但对机器来说挑战巨大。主流的解决思路之一是度量学习。简单说就是教模型一个“距离感”把图像映射到一个特征空间嵌入空间让同类图片的特征彼此靠近异类图片的特征相互远离。分类时新图片查询集的特征离哪个类别的“中心点”原型最近就归为哪一类。原型网络就是这个思路下的经典方法它计算每个类别所有支持样本特征的平均值作为该类别的原型然后用欧氏距离进行分类。然而在实际工程中原型网络有几个痛点。第一特征提取不够“细”。它把所有支持样本一视同仁地求平均忽略了样本间的细微关联和差异对于纹理复杂、类内差异大的图像比如不同品种的鸟平均后的原型可能丢失关键细节。第二分类边界太“僵”。它在新任务新类别上直接使用训练好的度量空间但这个空间的决策边界可能并不适合新数据集的分布导致分类器“水土不服”。针对这些问题我们团队提出了DCPNet。这个项目的核心思路是双管齐下一方面我们设计了一个并行分层特征提取模块结合对比学习让模型提取的特征更鲁棒、更具判别性另一方面我们引入了一个改进的分布校准方法利用基础类别Base Class的先验知识动态调整分类边界使其更好地贴合新数据集。下面我将结合我们团队在Mini-Imagenet、Omniglot和CUB数据集上的实验拆解DCPNet的设计细节、实现步骤以及我们踩过的坑和总结的经验。2. 核心思路拆解为什么是“并行特征”与“分布校准”要理解DCPNet得先看清原型网络的两个核心瓶颈以及我们对应的解决逻辑。2.1 瓶颈一特征表达的“粗糙”与改进之道传统的原型网络通常使用一个预训练好的CNN如ResNet作为编码器将所有图像映射为一个固定维度的特征向量。这里存在一个矛盾一个编码器要同时捕捉通用特征如边缘、颜色和特定特征如鸟类的喙形、汽车的logo。在少样本场景下支持样本极少模型很容易对这几个样本的偶然性特征比如背景、光照过拟合而忽略了真正具有判别性的、深层的语义信息。我们的解决思路是分而治之再融合。我们设计了一个双分支的编码器结构两个分支都基于ResNet-18。为什么选ResNet-18因为它在轻量化和性能之间取得了很好的平衡适合快速迭代和部署。分支一可训练分支这个分支参与模型训练梯度可以反向传播更新权重。它的目标是学习当前少样本任务下的深度语义特征。例如在识别鸟类时它会更关注喙、翅膀纹理等高级语义。分支二冻结分支这个分支的权重在训练过程中被冻结不更新。它通常加载在大型数据集如ImageNet上预训练好的权重。它的作用是提供稳定、通用的浅层语义特征如基本的形状、轮廓和纹理。冻结它是为了防止在少量数据上训练时这些宝贵的通用特征被“带偏”或遗忘。两个分支的特征在最后通过拼接Concatenation操作融合再经过一个全连接层调整到合适的维度如640维。这样最终的特征向量就同时包含了“稳”的通用知识和“活”的特定知识。这比单一编码器更能应对少样本数据中的噪声和多样性。2.2 瓶颈二分类边界的“僵化”与校准策略即使我们获得了更好的特征直接用原型网络的距离度量进行分类仍然假设了特征空间是全局均匀的。但现实是基础类别训练阶段见过的类别和新类别测试阶段遇到的类别的数据分布可能存在差异。直接用基础类别上学习到的距离度量来划分新类别就像用一把根据成年人身高制作的尺子去量儿童的身高虽然能用但不够精确。我们的策略是引入先验动态校准。我们假设每个类别的特征向量服从高斯分布。在基础类别上我们可以预先计算出每个类别的特征均值和协方差作为这个类别的“分布指纹”。当面对新类别的少数支持样本时我们不是简单地用这几个样本计算原型而是借用基础类别的知识来“丰富”和“校准”这个新类别的分布估计。具体操作分三步寻找近邻对于一个新类别的支持样本特征计算它与所有基础类别特征均值之间的欧氏距离找出最接近的k个基础类别。分布融合将这k个基础类别的均值、协方差与新样本的特征进行加权融合生成一个校准后的、更可靠的均值和协方差估计。这相当于用基础类别的分布信息对新类别的稀疏样本分布进行了平滑和补充。特征生成与分类基于校准后的高斯分布采样生成一些“虚拟”的特征向量。将这些虚拟特征与原始查询集特征混合共同训练一个简单的分类器如逻辑回归。这样分类器学习到的决策边界就不仅基于寥寥几个真实样本还融合了来自基础类别的分布先验从而更鲁棒、更适应新数据。注意这里的分布校准不是直接修改特征提取器的权重而是在特征空间的后处理阶段对分类器的输入数据进行“增强”和“纠偏”。这是一种轻量且高效的适配策略。3. 模型架构与实现细节理解了核心思想我们来看DCPNet的具体实现。整个流程分为两个阶段表征学习阶段和测试查询阶段。3.1 并行分层特征提取模块的实现我们使用PyTorch框架实现。编码器部分两个ResNet-18分支共享相同的结构但初始化方式不同。import torch import torch.nn as nn import torchvision.models as models class ParallelHierarchicalEncoder(nn.Module): def __init__(self, feature_dim640): super().__init__() # 分支一可训练分支从随机初始化或预训练权重开始参与训练 self.branch_trainable models.resnet18(pretrainedTrue) # 加载预训练权重作为起点 # 移除原始ResNet-18的最后一层全连接层 self.branch_trainable.fc nn.Identity() # 分支二冻结分支加载预训练权重并冻结 self.branch_frozen models.resnet18(pretrainedTrue) self.branch_frozen.fc nn.Identity() # 冻结该分支所有参数 for param in self.branch_frozen.parameters(): param.requires_grad False # 特征融合层 # ResNet-18最后一个池化层输出是512维两个分支拼接后是1024维 self.fusion_fc nn.Linear(512 * 2, feature_dim) def forward(self, x): # 前向传播 feat_trainable self.branch_trainable(x) # 形状: [batch_size, 512] feat_frozen self.branch_frozen(x) # 形状: [batch_size, 512] # 拼接特征 combined_feat torch.cat((feat_trainable, feat_frozen), dim1) # 形状: [batch_size, 1024] # 降维融合 final_feat self.fusion_fc(combined_feat) # 形状: [batch_size, feature_dim] return final_feat实操心得在初始化冻结分支时一定要确保requires_gradFalse并且在前向传播中不要将其包含在需要计算梯度的张量图中。否则不仅会浪费计算资源还可能因为意外的梯度更新而破坏预训练的特征。3.2 少样本区分损失函数的设计原型网络的损失通常是查询集样本特征到其对应类别原型距离的交叉熵损失。我们在此基础上引入了对比学习的思想增加了三元组损失。三元组损失的核心是构造三元组锚点样本、正样本、负样本拉近锚点与正样本的距离推远锚点与负样本的距离。在少样本任务中我们直接从支持集中构造三元组。锚点 (Anchor): 随机选取的一个支持样本。正样本 (Positive): 与锚点属于同一类别的另一个支持样本。负样本 (Negative): 与锚点属于不同类别的某个支持样本。损失函数如下TotalLoss λ * TripletLoss (1 - λ) * CrossEntropyLoss其中λ是一个超参数用于平衡两种损失的贡献。三元组损失迫使模型学习更具判别性的特征让同类样本在特征空间内更紧凑不同类样本更分离。交叉熵损失则确保模型能正确地进行分类。import torch.nn.functional as F def triplet_loss(anchor, positive, negative, margin0.4): 计算三元组损失 pos_dist F.pairwise_distance(anchor, positive, p2) # 欧氏距离 neg_dist F.pairwise_distance(anchor, negative, p2) loss torch.relu(pos_dist - neg_dist margin) # hinge loss return loss.mean() def compute_prototypes(support_features, support_labels): 计算每个类别的原型特征均值 unique_labels torch.unique(support_labels) prototypes [] for label in unique_labels: # 选出该类所有样本的特征 class_features support_features[support_labels label] prototype class_features.mean(dim0) # 计算均值作为原型 prototypes.append(prototype) return torch.stack(prototypes), unique_labels def dcpnet_loss(support_features, support_labels, query_features, query_labels, lambda_param0.3, margin0.4): 计算DCPNet的总损失。 为简化这里假设support_features已包含用于构造三元组的样本。 实际中需从support set中专门采样构造三元组。 # 1. 计算原型和交叉熵损失标准原型网络损失 prototypes, proto_labels compute_prototypes(support_features, support_labels) # 计算查询集每个样本到所有原型的距离 dists torch.cdist(query_features, prototypes, p2) # 欧氏距离矩阵 # 负距离作为logits距离越小相似度越高 logits -dists ce_loss F.cross_entropy(logits, query_labels) # 2. 计算三元组损失需从support set中采样三元组 # 这里简化演示实际需更复杂的三元组采样逻辑 triplet_loss_val 0.0 # ... (三元组采样和损失计算代码) ... # 3. 加权总损失 total_loss lambda_param * triplet_loss_val (1 - lambda_param) * ce_loss return total_loss注意事项三元组采样的质量对训练效果影响很大。要避免采到“太简单”的三元组负样本离锚点本来就很远这样损失为0不产生梯度。我们实践中采用了“半难”采样策略即选择那些使三元组损失为正但又非最难距离最近的负样本以提供稳定的梯度信号。3.3 改进的分布校准流程分布校准发生在测试阶段模型权重已固定。其输入是基础类别的统计信息和新类别的少量支持样本。import numpy as np from sklearn.linear_model import LogisticRegression class ImprovedDistributionCalibration: def __init__(self, base_class_stats, k_neighbors2, alpha1e-5): base_class_stats: 字典键为类别id值为(mean, cov)为基础类别的统计信息。 k_neighbors: 选择最近邻的基础类别数量。 alpha: 添加到协方差矩阵对角线的小常数确保数值稳定性。 self.base_means {cid: stats[0] for cid, stats in base_class_stats.items()} self.base_covs {cid: stats[1] for cid, stats in base_class_stats.items()} self.k k_neighbors self.alpha alpha def calibrate_and_classify(self, support_features, support_labels, query_features): 支持集和查询集特征都是经过编码器提取的。 返回查询集的预测标签。 calibrated_features [] calibrated_labels [] unique_labels torch.unique(support_labels) for label in unique_labels: # 获取当前新类别的所有支持样本特征 class_feats support_features[support_labels label].cpu().numpy() # 计算当前类别的初始原型均值 class_proto np.mean(class_feats, axis0) # 1. 寻找最近邻的基础类别 distances [] for base_cid, base_mean in self.base_means.items(): dist np.linalg.norm(class_proto - base_mean) distances.append((dist, base_cid)) distances.sort(keylambda x: x[0]) nearest_cids [cid for _, cid in distances[:self.k]] # 2. 融合均值和协方差 # 融合均值 nearest_means [self.base_means[cid] for cid in nearest_cids] calibrated_mean (np.sum(nearest_means, axis0) class_proto) / (self.k 1) # 融合协方差 (简化处理取平均) nearest_covs [self.base_covs[cid] for cid in nearest_cids] calibrated_cov np.mean(nearest_covs, axis0) self.alpha * np.eye(class_proto.shape[0]) # 3. 从校准后的分布中采样生成虚拟特征 # 假设每个新类别生成N个虚拟样本 N_virtual 20 try: # 生成多元高斯分布样本 virtual_feats np.random.multivariate_normal(calibrated_mean, calibrated_cov, N_virtual) except np.linalg.LinAlgError: # 如果协方差矩阵奇异使用对角矩阵近似 calibrated_cov np.diag(np.diag(calibrated_cov)) self.alpha * np.eye(class_proto.shape[0]) virtual_feats np.random.multivariate_normal(calibrated_mean, calibrated_cov, N_virtual) # 收集真实虚拟特征和标签 calibrated_features.append(class_feats) # 真实样本 calibrated_features.append(virtual_feats) # 虚拟样本 calibrated_labels.extend([label] * (len(class_feats) N_virtual)) # 合并所有校准后的特征和标签 X_calibrated np.vstack(calibrated_features) y_calibrated np.array(calibrated_labels) # 4. 训练逻辑回归分类器 # 注意这里用校准后的支持集含虚拟样本训练 clf LogisticRegression(max_iter1000, solverlbfgs, multi_classmultinomial) clf.fit(X_calibrated, y_calibrated) # 5. 对查询集进行分类 query_preds clf.predict(query_features.cpu().numpy()) return torch.from_numpy(query_preds).to(query_features.device)核心要点分布校准的本质是一种数据增强。它利用基础类别的分布先验为新类别“想象”出更多合理的特征样本从而让后续的分类器逻辑回归在更丰富、更符合真实数据分布的特征上进行训练其决策边界自然更准确。4. 实验配置与结果分析理论和方法再好也得靠实验说话。我们在三个公认的少样本学习基准数据集上进行了验证。4.1 数据集与实验设置Mini-ImageNet包含100个类别每个类别600张84x84的彩色图像。我们按通用划分用80个类训练20个类测试。这是最常用的基准类别通用难度适中。Omniglot包含50个字母表中的1623个手写字符每个字符由20个人书写。图像为105x105的灰度图。这个数据集类别多、样本少且类间差异有时很细微挑战性在于细粒度识别。CUB-200-2011包含200种鸟类的11788张图像。这是一个细粒度分类数据集不同鸟类间差异非常小比如不同种类的麻雀。这非常考验模型提取判别性特征的能力。我们采用标准的N-way K-shot评估协议。最常见的是5-way 1-shot和5-way 5-shot。即每个任务中随机从新类别中选取5个类别每个类别提供K个1或5个带标签的样本作为支持集另外提供一批未标注的样本作为查询集。模型需要正确分类查询集样本。最终报告的是在大量随机采样的任务上的平均分类准确率。训练细节编码器双分支ResNet-18。图像统一缩放到84x84并进行归一化。优化器Adam。损失权重λ0.3 margin0.4。这个组合在我们调参中发现效果最稳定。分布校准k2选择2个最近邻基础类别。评估在测试阶段从测试集随机采样100,000个任务计算平均准确率及95%置信区间。4.2 对比实验结果我们将DCPNet与多个经典少样本学习方法进行了对比包括基于度量学习的原型网络Prototypical Net、匹配网络Matching Net基于优化的MAML以及一些更新的方法如关系网络Relation Net和CovaMNet。方法Mini-ImageNet (5-way 1-shot)Mini-ImageNet (5-way 5-shot)Omniglot (5-way 1-shot)Omniglot (5-way 5-shot)CUB (5-way 1-shot)CUB (5-way 5-shot)Matching Net43.56%55.31%98.10%98.90%61.16%72.86%Prototypical Net49.42%68.20%97.50%99.20%62.27%79.17%Relation Net50.44%65.32%99.60%99.80%66.20%82.30%MAML48.70%63.11%98.70%99.30%64.55%80.60%CovaMNet51.19%67.65%98.10%98.90%65.88%78.50%DCPNet (Ours)53.87%70.12%99.10%99.50%67.45%83.21%表DCPNet与基线方法在三个数据集上的分类准确率对比示例数据基于论文结果概括结果分析全面领先在Mini-ImageNet和CUB这两个更具现实挑战性的数据集上DCPNet在1-shot和5-shot设置下均取得了最佳或接近最佳的性能。这证明了我们方法在通用物体和细粒度物体分类上的有效性。Omniglot的启示在Omniglot上DCPNet略逊于关系网络。我们分析原因有二首先Omniglot是手写字符结构信息强关系网络通过深度非线性比较如全连接层计算样本间关系可能更适合这种高度结构化的相似性度量。其次我们依赖的WideResNet预训练特征主要来自自然图像ImageNet对手写字符的迁移效果可能不是最优。这提示我们特征提取器的预训练领域与目标领域越接近方法效果越好。5-shot提升显著相比于1-shot5-shot时DCPNet的优势更为明显。这是因为有了更多支持样本分布校准能更准确地估计新类别的分布生成的虚拟特征也更可靠分类器性能提升空间更大。4.3 消融实验为了验证各个组件的贡献我们在Mini-Imageet上进行了消融实验。模型变体5-way 1-shot5-way 5-shot说明原型网络 (基线)49.42%68.20%原始方法DCPNet (无分布校准)52.10%69.55%仅使用并行特征提取和三元组损失DCPNet (完整模型)53.87%70.12%包含所有组件表在Mini-ImageNet上的消融实验结果示例数据结论并行特征提取与三元组损失单独使用这部分DCPNet w/o DC相比基线在1-shot和5-shot上分别提升了约2.7%和1.4%。这证实了融合深浅层语义特征以及对比学习对于学习更好原型表示的有效性。改进的分布校准加上分布校准后性能得到进一步巩固和提升尤其在1-shot上提升更明显约1.8%。这说明在样本极度稀缺时利用先验知识校准分布对于稳定和提升分类边界至关重要。组件协同两个改进点从不同角度特征学习和分类器适配共同作用产生了“112”的效果。5. 实操中的挑战与调优经验纸上得来终觉浅在复现和调优DCPNet的过程中我们遇到了不少坑也积累了一些实战经验。5.1 特征融合的维度与过拟合最初我们将两个512维的特征直接拼接成1024维后就输入后续计算。但在小规模数据集如Mini-ImageNet的子集上训练时模型很快过拟合。我们发现1024维的特征在少量数据下显得过于高维包含了大量冗余和噪声。解决方案在拼接后立即加入一个全连接层进行降维如降至640维或512维。这个全连接层起到了“特征筛选器”的作用迫使网络学习两个分支特征中最关键、最互补的信息有效缓解了过拟合并略微提升了性能。5.2 三元组采样的效率与策略在训练循环中实时在线采样三元组尤其是对于每个任务的支持集计算开销很大会严重拖慢训练速度。优化策略预计算与缓存在每个训练周期Epoch开始前对基础训练集的所有类别预先计算并缓存每个类别的所有样本特征。在构造每个少样本任务时直接从缓存中抽取特征来构造三元组避免重复前向传播。难例挖掘简单的随机采样效率低下。我们实现了在线难例挖掘在一个训练批次Batch内对于每个锚点选择距离它最远的正样本难正例和距离它最近的负样本难负例来构造三元组。这样产生的损失梯度信号更强收敛更快。但要注意设置边界margin防止因样本过于“难”而导致训练不稳定。5.3 分布校准的数值稳定性在校准过程中需要计算和采样多元高斯分布。当基础类别的协方差矩阵接近奇异即特征间高度相关时np.random.multivariate_normal会报错。稳健性处理def sample_from_calibrated_distribution(mean, cov, num_samples, alpha1e-5): 从校准后的分布中稳健地采样。 n_features mean.shape[0] # 尝试进行Cholesky分解检查协方差矩阵的正定性 try: L np.linalg.cholesky(cov alpha * np.eye(n_features)) samples mean np.dot(np.random.randn(num_samples, n_features), L.T) except np.linalg.LinAlgError: # 如果失败退化为对角协方差矩阵假设特征独立 print(fWarning: Covariance matrix is not positive definite. Using diagonal approximation.) var np.diag(cov) alpha samples np.random.randn(num_samples, n_features) * np.sqrt(var) mean return samples我们添加了一个小的正则化项alpha * I到协方差矩阵上确保其正定性。如果仍然失败则退化为使用对角协方差矩阵即假设各特征维度独立进行采样。虽然这是一种近似但在实践中能保证程序稳定运行且对最终分类性能影响很小。5.4 超参数 λ 和 margin 的调优λ 控制着三元组损失和交叉熵损失的权重margin 控制着三元组损失中正负样本对的距离差。这两个超参数对模型性能敏感。调优经验λ损失权重我们通过网格搜索发现λ在0.2到0.4之间效果较好。λ太小0.1三元组损失作用微弱模型退化为普通原型网络λ太大0.5会削弱分类的直接监督信号导致训练初期不稳定。0.3是一个稳健的起点。marginmargin的设置与特征空间的尺度有关。如果特征经过L2归一化距离范围在[0,2]之间margin通常设置在0.2到0.6之间。我们最终选择0.4它在拉近同类和推开异类之间取得了较好的平衡。一个实用的技巧是可以在训练初期用一个较小的margin如0.2随着训练进行逐步增大以逐步提高特征空间的判别力。6. 总结与展望DCPNet从特征学习和分类器适配两个层面对原型网络进行了切实有效的改进。并行分层特征提取确保了特征的丰富性和判别性而改进的分布校准则巧妙地将基础类别的知识迁移到新任务上缓解了少样本条件下的分布偏移问题。代码实现上模块清晰易于集成到现有的原型网络框架中。当然方法仍有改进空间。例如双分支结构增加了参数量和计算量尽管推理时冻结分支不计算梯度。未来的一个方向是探索更轻量化的特征融合方式或者使用知识蒸馏技术将双分支的知识压缩到单分支网络中。此外当前分布校准假设特征服从高斯分布对于更复杂的真实数据分布可能是一个简化。探索更灵活的非参数分布估计方法或者结合生成模型如归一化流来建模特征分布是值得尝试的方向。对于我们工程师而言DCPNet提供了一个很好的范例在改进模型时不仅要关注网络结构的创新更要深入思考问题背后的根本矛盾如少样本下的特征粗糙和分布不匹配并设计出针对性强的、可解释的解决方案。将对比学习、度量学习、迁移学习的思想有机融合往往能产生出人意料的好效果。希望这次对DCPNet的深度拆解能为你解决自己的少样本视觉问题带来一些启发。