简介:本资源为一份关于小样本机器学习的发明专利申请PDF,面向从事多模态数据识别分类研究的算法工程师、研究生及科研人员。专利由国防科技大学黄健等人提出,针对样本有限时传统算法易过拟合、泛化不足的问题,给出多模态数据表征、层级池化与关系网络三个核心模块的完整技术方案。压缩包内仅含1个PDF文件,约81KB,内容涵盖权利要求书、说明书及附图,清晰呈现编码器特征向量化、先最大池化再平均池化的降维归纳,以及基于关系网络的小样本分类流程。该方案可应用于语音识别、图像识别、跨媒体信息检索等场景,帮助读者理解如何从有限样例中学习更丰富的特征关系,提升模型准确性与泛化性能。目前已有215人学习,适合需要借鉴专利思路、撰写相关论文或申请材料的读者参考。
1. 多模态小样本分类的工程化拆解:从专利说明书到可复现基线
手里只有几十条带标注的图文对,却要训一个能区分十几类的模型,这种场景我在工业质检和跨媒体检索里都撞见过。传统监督学习直接翻车,过拟合到连训练集都分不开。这份 CN110363239A 的专利文档,核心就是给这个场景提供一套可落地的三模块方案:多模态数据表征、层级池化、关系网络。它不依赖海量标注,而是通过编码器把图像、文本、音频等不同模态映射到统一向量空间,再用先最大后平均的层级池化把变长序列压成定长类别向量,最后用关系网络做小样本分类。适合手里有少量多模态标注数据、想快速搭一个能跑通基线的人。下面我按自己复现时的顺序,把每个模块的参数、代码和踩坑点拆开讲。
2. 多模态数据表征:编码器选型与向量化实操
2.1 为什么不能直接把原始特征拼起来
多模态数据最粗暴的做法是把图像 CNN 特征和文本 TF-IDF 向量直接 concat,然后送进分类器。我在早期项目里这么干过,小样本下准确率直接掉到随机猜。原因有两个:一是不同模态的特征尺度差异大,图像特征值域可能在 [0,1],文本 embedding 可能在 [-10,10],直接拼接会让某一模态主导距离计算;二是模态间的语义对齐没有建立,图像里的“红色圆形”和文本里的“红色圆形”在各自空间里可能离得很远。
专利里明确用编码器分别处理各模态,再映射到统一维度。常见做法是图像走 ResNet 或 ViT 的倒数第二层输出,文本走 BERT 的 [CLS] 向量,音频走 1D-CNN 或 Mel 频谱 + LSTM。每个编码器输出后接一个线性投影层,把维度对齐到同一个 d_model,比如 256 或 512。这一步不做,后面的关系网络根本没法算模态间关系。
2.2 编码器实现与参数设置
下面是我复现时用的 PyTorch 骨架,图像和文本双模态,音频可以按同样模式加分支。代码里关键参数我标了注释。
import torch import torch.nn as nn import torchvision.models as models from transformers import BertModel class MultiModalEncoder(nn.Module): def __init__(self, d_model=256, freeze_backbone=True): super().__init__() # 图像分支:ResNet18 去掉最后的 fc 层 resnet = models.resnet18(pretrained=True) self.img_backbone = nn.Sequential(*list(resnet.children())[:-1]) # 输出 512 维 self.img_proj = nn.Linear(512, d_model) # 文本分支:BERT-base 取 [CLS] 向量 self.text_backbone = BertModel.from_pretrained('bert-base-uncased') self.text_proj = nn.Linear(768, d_model) # 是否冻结骨干网络,小样本下建议冻结,只训投影层 if freeze_backbone: for p in self.img_backbone.parameters(): p.requires_grad = False for p in self.text_backbone.parameters(): p.requires_grad = False self.norm = nn.LayerNorm(d_model) # 对齐后做归一化,稳定训练 def forward(self, img, input_ids, attention_mask): # 图像特征 img_feat = self.img_backbone(img).squeeze(-1).squeeze(-1) # [B, 512] img_feat = self.img_proj(img_feat) # [B, d_model] # 文本特征 text_out = self.text_backbone(input_ids=input_ids, attention_mask=attention_mask) text_feat = text_out.last_hidden_state[:, 0, :] # [CLS] 向量 [B, 768] text_feat = self.text_proj(text_feat) # [B, d_model] # 各自归一化后再返回,避免尺度不一致 return self.norm(img_feat), self.norm(text_feat)逻辑说明:图像分支用 ResNet18 是因为小样本下参数量越小越不容易过拟合,换成 ResNet50 在 5-way 1-shot 下反而掉点。文本分支用 BERT-base 的 [CLS] 向量,如果文本很短(比如商品标题),也可以换成本地训练的 Word2Vec 平均池化,速度更快。freeze_backbone=True是血泪经验,小样本下微调整个 BERT 会直接过拟合到训练集,只训投影层效果更稳。d_model设 256 是专利里没写但工程上常用的折中值,512 也可以,但关系网络的计算量会翻倍。
参数怎么改:如果显存不够,把 BERT 换成 distilbert-base-uncased,维度从 768 降到 768 但层数减半。如果图像分辨率高,ResNet18 的输入从 224 调到 448 会提升细粒度分类,但 batch size 要相应减小。归一化层LayerNorm不要换成BatchNorm,小样本下 batch 统计量不稳定,LayerNorm 更鲁棒。
3. 层级池化:先最大后平均的降维逻辑与代码实现
3.1 为什么是最大池化在前、平均池化在后
专利里写的“先最大池化再平均池化”不是随便定的顺序。我一开始反过来试过,先平均再最大,结果类别特征向量被噪声拉偏,准确率掉了 3 个点。原因在于:最大池化对序列中的显著信号敏感,比如一段音频里只有 0.2 秒的关键词,最大池化能把这个峰值保留下来;但如果先平均,这个峰值会被周围静音段稀释。先最大池化相当于做了一次“显著特征筛选”,再平均池化是在筛选后的特征上做平滑,减少随机波动。
具体操作上,假设编码器输出的是时间/空间连续的向量序列,形状 [B, L, d_model],L 是序列长度。层级池化分两步:第一步在 L 维度上做最大池化,得到 [B, d_model];第二步在多个这样的向量上做平均池化。等等,这里有个容易混淆的点——专利里的“层级”指的是先对每个模态的序列做最大池化,再把多个模态的最大池化结果做平均池化。我一开始理解成对同一个序列做两次池化,代码写错了,后来对照权利要求书才改过来。
3.2 层级池化的 PyTorch 实现
下面代码假设图像和文本各输出一个序列,图像是空间展平后的序列,文本是 token 序列。实际使用时,图像编码器可以输出 [B, H*W, d_model] 而不是全局池化后的向量。
class HierarchicalPooling(nn.Module): def __init__(self): super().__init__() def forward(self, img_seq, text_seq): # img_seq: [B, L_img, d_model], text_seq: [B, L_text, d_model] # 第一步:对每个模态的序列做最大池化,保留显著特征 img_max = torch.max(img_seq, dim=1)[0] # [B, d_model] text_max = torch.max(text_seq, dim=1)[0] # [B, d_model] # 第二步:对多个模态的最大池化结果做平均池化 # 这里两个模态,直接 stack 后平均 stacked = torch.stack([img_max, text_max], dim=1) # [B, 2, d_model] fused = torch.mean(stacked, dim=1) # [B, d_model] return fused逻辑说明:torch.max(img_seq, dim=1)[0]返回每个位置的最大值,忽略序列长度差异。torch.stack把两个模态拼成 [B, 2, d_model],再torch.mean得到融合向量。如果模态数超过 2,比如加了音频,就 stack 三个再平均。注意这里没有用keepdim=True,因为后面不需要广播。
参数怎么改:如果某个模态的序列特别长(比如视频帧序列),最大池化前可以先做一次 1D 卷积降采样,把 L 从 1000 降到 100,减少计算量。平均池化时如果模态重要性不同,可以改成加权平均,权重通过一个小网络学习,但小样本下不建议加这个,容易过拟合。池化后的向量维度保持 d_model 不变,不要在这里做降维,降维留给关系网络。
提示:层级池化的输入序列必须是编码器输出的原始序列,不要先做全局平均池化再送进来,那样最大池化就没意义了。
4. 关系网络:小样本分类的核心机制与训练细节
4.1 关系网络为什么适合小样本
小样本学习的核心矛盾是:分类器参数量不能大,否则过拟合;但表达能力要够,否则欠拟合。关系网络的做法是不学一个固定的分类权重,而是学一个“关系度量函数”,判断查询样本和支撑集样本之间的相似度。具体来说,支撑集有 N 个类,每类 K 个样本,关系网络把查询样本和每个类的原型(或每个样本)拼接后送进一个小的卷积/全连接网络,输出 0 到 1 的关系分数,最后取分数最高的类。
专利里没有展开关系网络的具体结构,但常见做法是:把查询特征和支撑集特征在通道维度拼接,经过两个 Conv1d + ReLU + MaxPool 后接全连接层输出标量。我复现时用的结构如下,在 5-way 1-shot 下比原型网络高 2 个点。
4.2 关系网络模块代码与训练循环
class RelationNetwork(nn.Module): def __init__(self, d_model=256, hidden=128): super().__init__() # 输入是查询和支撑样本拼接后的特征,维度 2*d_model self.layer1 = nn.Sequential( nn.Conv1d(2 * d_model, hidden, kernel_size=1), # 1x1 卷积做通道压缩 nn.ReLU(), nn.MaxPool1d(kernel_size=2) # 这里输入长度是 1,实际不降维,保持结构一致 ) self.layer2 = nn.Sequential( nn.Conv1d(hidden, hidden, kernel_size=1), nn.ReLU(), nn.MaxPool1d(kernel_size=2) ) self.fc = nn.Linear(hidden, 1) # 输出关系分数 def forward(self, query_feat, support_feat): # query_feat: [B, d_model], support_feat: [B, d_model] # 拼接后增加一个维度作为序列长度 combined = torch.cat([query_feat, support_feat], dim=1) # [B, 2*d_model] combined = combined.unsqueeze(-1) # [B, 2*d_model, 1] x = self.layer1(combined) # [B, hidden, 1] x = self.layer2(x) # [B, hidden, 1] x = x.squeeze(-1) # [B, hidden] return torch.sigmoid(self.fc(x)) # [B, 1]逻辑说明:torch.cat把查询和支撑样本的特征拼接,unsqueeze(-1)把特征维度当作序列长度,这样可以用 Conv1d 处理。两个 Conv1d 的 kernel_size 都是 1,相当于全连接,但保留了 Conv1d 的接口方便后续加空间结构。最后sigmoid输出 0 到 1 的关系分数,训练时用 MSE 损失,标签是 1(同类)或 0(不同类)。
训练循环的关键参数:episode 训练,每个 episode 随机采样 N 个类,每类 K 个支撑样本和 M 个查询样本。我一般设 N=5, K=1, M=15,学习率 1e-3,Adam 优化器,训练 2000 个 episode。注意关系网络的输出是分数,不是 logits,所以用 MSE 而不是交叉熵。如果换成交叉熵,需要把输出改成 N 维向量,但专利里的关系网络是二分类度量,MSE 更匹配。
参数怎么改:hidden从 128 调到 64 可以进一步降低过拟合,但太小会欠拟合。d_model如果从 256 改成 512,hidden也要相应增大到 256。训练 episode 数不是越多越好,我试过 5000 个 episode,验证集准确率在 2000 左右就饱和了,再训会过拟合。
5. 避坑与排查:复现时最容易翻车的五个点
5.1 模态缺失导致编码器报错
现象:训练时如果某个样本只有图像没有文本,BertModel的input_ids传空会直接抛异常。原因:多模态数据在实际场景里经常有缺失模态,专利里没提缺失处理,但工程上必须面对。解决:在数据加载阶段做模态对齐,缺失的模态用零向量填充,并在编码器里加一个模态存在性掩码,缺失模态的投影输出置零后再做层级池化。我一般会统计每个模态的缺失率,超过 30% 就考虑换数据集。
5.2 层级池化后特征方差过大
现象:训练 loss 震荡严重,准确率上不去。原因:最大池化取的是序列最大值,如果某个模态的序列里有异常大的激活值,会主导融合向量。解决:在最大池化前对序列做一次 LayerNorm,把每个位置的向量归一化到均值 0 方差 1,再取最大值。这个操作在专利里没写,但实测能稳定训练。
5.3 关系网络过拟合到支撑集
现象:训练 episode 准确率 95%,测试 episode 只有 60%。原因:关系网络把支撑集样本“记住”了,而不是学度量。解决:在关系网络的全连接层前加 Dropout,概率 0.3 到 0.5;同时减少关系网络的参数量,把hidden从 128 降到 64。另外,支撑集采样时每个 episode 重新随机选类,不要固定类集合。
5.4 编码器冻结后投影层学习率过大
现象:投影层权重更新剧烈,loss 不下降。原因:冻结骨干后,只有投影层可训,如果学习率跟骨干微调时一样设 1e-4,投影层更新太慢;如果设 1e-2,又震荡。解决:投影层单独设学习率,我一般用 1e-3,配合余弦退火调度。如果同时训骨干和投影层,骨干用 1e-5,投影层用 1e-3,分组参数传入优化器。
5.5 多模态对齐时维度不匹配
现象:torch.cat报维度错误。原因:图像编码器输出 [B, 512],文本编码器输出 [B, 768],投影层如果只写了一个,另一个没投影。解决:每个模态分支都必须有独立的投影层,投影到同一个d_model。检查方法是打印每个分支的输出形状,确认都是 [B, d_model] 再拼接。我习惯在 forward 里加一行assert img_feat.shape == text_feat.shape,提前暴露问题。
6. 进阶技巧:用原型修正提升 1-shot 下的稳定性
1-shot 场景下,每个类只有一个支撑样本,关系网络容易受这个样本的噪声影响。我在复现时加了一个原型修正步骤:先用支撑样本的特征初始化类原型,然后在查询集上做一次软分配,用高置信度的查询样本更新原型,再重新计算关系分数。这个技巧在专利里没有,但工程上很实用,5-way 1-shot 下能提升 4 到 6 个点。
具体操作:第一轮关系网络输出分数后,取每个类分数最高的前 3 个查询样本,把它们的特征和原始支撑样本特征做平均,得到修正后的类原型。然后用修正原型重新计算关系分数。注意只修正一次,迭代多次会引入确认偏差。代码上就是在关系网络 forward 后加一个原型更新模块,训练时这个模块不参与梯度回传,只在推理时用。
验证方法:在测试集上分别跑修正前和修正后的准确率,如果修正后提升小于 2 个点,说明支撑样本本身质量高,不需要修正;如果提升超过 5 个点,说明支撑样本噪声大,可以考虑在数据预处理阶段做异常检测。我一般会固定随机种子跑 5 次,取平均准确率,单次结果波动太大不可信。
从那以后我每次复现小样本论文,都强制走一遍“冻结骨干 + 投影层单独学习率 + 层级池化前 LayerNorm + 关系网络 Dropout + 原型修正”这套组合,缺一个都可能翻车。希望帮到你。
本文还有配套的精品资源,点击获取