简介:这份专利文档公开了面向多模态数据的小样本机器学习方法、系统与介质,适合机器学习研究者、算法工程师及多模态识别任务开发人员。方案围绕多模态数据表征、层级池化和关系网络三个模块展开:先用编码器将图像、文本、音频等异构数据统一向量化,再通过先最大池化后平均池化的层级池化降维归纳为类别特征向量,最后借助关系网络完成小样本条件下的分类推理,可应用于语音识别、图像识别、跨媒体检索等场景。资源包含1个PDF格式的发明专利申请文件,压缩包仅81KB,内容简洁但结构完整,涵盖权利要求书、说明书、摘要及附图。已有215人学习浏览,适合希望快速了解小样本学习与多模态融合技术方案、借鉴专利思路的读者。从中可提取完整的算法流程、模块设计逻辑及实现细节,有助于理解如何在小样本条件下提升模型泛化能力,并为相关研究与工程实践提供参考。
1. 多模态小样本不是“数据少”这么简单:这套方法解决的是什么问题
设备故障诊断里最常见的窘境是:声学传感器采了一批数据,振动通道也录了,红外热像还拍了几十张,但每一种故障类型只有二三十个有效样本。单看图像,样本不够训练一个像样的深度学习模型;单看振动信号,特征又不够区分相似故障;可把这些模态拼在一起,反而连怎么对齐、怎么融合、怎么避免过拟合都成了新的麻烦。面向多模态数据的小样本机器学习方法,就是专门处理这种“模态多、样本少、还要落地”的场景:它把视觉、文本、数值等异构输入组织成统一样本,用小样本训练策略(原型网络、对比学习、高斯过程回归辅助增强等)把模型训稳,最后封成系统和服务,连同训练好的模型一起存入标准介质分发部署。
这套方案的适用对象很明确:有 1~50 个样本/类的冷启动项目,多模态数据已经存在但不知道怎么用,以及被“融合后反而掉点”折腾过的人。正文按“数据组织 → 模型策略 → 系统落地 → 踩坑 → 验证”推进,所有代码给到能直接改的程度。
2. 先把数据摆正:多模态数据的分装、增强与对齐
多模态小样本项目里,最先出问题的往往不是模型,而是数据接口。图像是 (H, W, C) 的数组,文本是变长字符串,数值特征是 1 维向量,三个东西放在同一个 DataLoader 里经常不兼容;更麻烦的是模态缺失——某个样本没有文本,或者数值通道坏了,模型一碰到这种样本就报错或者输出随机结果。这一章处理的是“怎么把多模态数据装进同一个样本、怎么增强、怎么处理缺失”这三件基础事。
2.1 用统一样本对象把图像、文本、数值装进同一个结构
常见做法是先定义一个统一的样本数据结构,把三种模态变成同一个对象的字段,再在对象里记录“当前样本实际有哪些模态”。这样后续模型、数据增强、训练循环都只面向这一个接口,不用每换一个数据集就重写数据管道。
# multimodal_sample.py from dataclasses import dataclass from typing import Optional import numpy as np @dataclass class MultiModalSample: sample_id: str label: int image: Optional[np.ndarray] = None # (H, W, C),RGB 顺序 text: Optional[str] = None # 原始文本描述 numeric: Optional[np.ndarray] = None # 传感器数值特征,如 [温度, 振动幅值, ...] available_modalities: list = None # 记录实际存在的模态,用于缺失处理 def __post_init__(self): self.available_modalities = [] if self.image is not None: self.available_modalities.append('image') if self.text is not None and len(self.text) > 0: self.available_modalities.append('text') if self.numeric is not None: self.available_modalities.append('numeric')这个结构的核心价值在available_modalities字段上。它不是一个装饰,而是给后续模型一个“路由开关”:模态存在时走对应的编码器,缺失时直接跳过该分支并把融合权重置零。很多小样本多模态项目翻车,就是因为把缺失模态当成零向量硬塞进模型,导致编码器学到“零向量 = 某个类别”的假相关性。
要注意 image 字段统一为 RGB、uint8 的 NumPy 数组,文本字段建议在预处理阶段完成分词和小写化,numeric 字段提前做 z-score 标准化。z-score 的均值和方差必须在训练集上计算并保存,推理时直接调用,不要在推理数据上重新算,否则分布偏移会让数值模态的贡献失真。
2.2 多模态数据增强:图像翻转变形时,文本和数值必须跟着“对齐”
小样本场景下增强是必需品,但多模态增强和单模态增强有本质区别:图像做翻转、裁剪、旋转时,对应的文本描述和数值语义不能变,可一旦文本里出现“左侧”“偏上”这类位置词,翻转后的图像就和文本对不上了。这就是多模态增强最常见的对齐坑。
# augment.py import numpy as np import random import imgaug.augmenters as iaa # 图像增强流水线:概率翻转 + 小角度旋转 + 轻微缩放 image_aug = iaa.Sequential([ iaa.Fliplr(0.5), iaa.Affine(rotate=(-10, 10), scale=(0.9, 1.1)), ]) def augment_multimodal(sample, text_synonym_dict=None, numeric_noise_std=0.02): # 1. 图像增强:只有 image 模态存在时才做 if 'image' in sample.available_modalities: sample.image = image_aug(image=sample.image) # 2. 文本增强:只在显式提供同义词替换表时执行 if 'text' in sample.available_modalities and text_synonym_dict: for raw, syns in text_synonym_dict.items(): if raw in sample.text: sample.text = sample.text.replace(raw, random.choice(syns)) # 3. 数值增强:高斯扰动,幅度以特征标准差为基准 if 'numeric' in sample.available_modalities: noise = np.random.normal(0, numeric_noise_std, size=sample.numeric.shape) sample.numeric = sample.numeric + noise return sample这段增强逻辑有三个参数需要细调。numeric_noise_std的单位不是原始数值,而是“该特征的标准差的倍数”,0.01~0.05 是常见安全区间;大于 0.1 时噪声会盖过真实信号,等于在造伪标签。iaa.Affine的 rotate 范围建议不超过 ±15 度,工业红外热像或医疗影像里旋转过大可能把真实缺陷转没了。text_synonym_dict是一次性构造的,比如把“损坏”替换成“故障”“异常”,词表不要做太大,3~5 个同义词就够,替换过多会让文本模态失去判别力。
增强策略上,每一轮训练迭代对同一个样本做随机增强,而不是预先扩充数据集存盘。原因是小样本场景下预先扩充会把样本之间的相似度拉高,验证集和训练集容易产生潜在重叠;在线增强则保持每个 epoch 看到的增强版本不同,模型不容易背样本。
2.3 模态缺失的预处理边界:mask 优先于零填充
模态缺失在小样本多模态数据里不是偶发事件。传感器通道偶尔掉线、文本标注常常不齐、某些类别的红外图像根本没采到,都是常态。处理原则很简单:先打 mask,再进融合层,绝不在输入层面用零填充冒充真实数据。
# mask_and_pad.py def build_modality_mask(sample, modality_order=('image', 'text', 'numeric')): mask = np.array([1 if m in sample.available_modalities else 0 for m in modality_order], dtype=np.float32) return mask这个 mask 向量会送到模型融合层,和特征做逐元素加权相乘,同时让缺失模态对应的梯度归零。零填充只发生在特征对齐到相同维度时,而且填充后必须乘 mask 把填充位清零;如果你发现模型在小样本训练里 loss 波动剧烈,先检查是不是某个 batch 里有大量 mask 为 0 的样本,把它们的 loss 做加权屏蔽,而不是让它硬学一个不存在模态的表示。
文本模态尤其容易缺失。缺失时不要用空字符串硬编码,直接让text=None,在编码前做一次分支判断:有文本走文本编码器,没有就返回一个全零特征向量并标记 mask=0。另一个在预处理阶段就要定好的参数是数值特征的维度,建议用 PCA 或方差筛选压到 16~64 维,维度太高会让数值模态在融合时稀释掉图像和文本的贡献,这在类似 bird1445 这种细粒度多模态数据集上表现非常明显。
3. 核心方法:三种可落地的多模态小样本训练路线
数据整理清楚之后,关键是模型策略。小样本场景直接端到端训练一个多模态深度网络,几乎必然过拟合;而纯用预训练模型零样本推理,又浪费了手头有限的标注数据。这里给出三条被反复验证过的路线:对比学习做模态对齐、高斯过程回归做特征空间数据扩充、原型网络做最终分类。
3.1 用对比学习把图像、文本、数值拉进同一个向量空间
对比学习在多模态领域最成功的形态是双塔结构:图像和文本分别走独立的编码器,通过对比损失让同一对样本的两种模态在向量空间里靠近,不同样本的模态互相推开。小样本场景下,编码器直接用在大规模数据上预训练好的模型,冻结不动,只训练一个小的映射层。
# clip_finetune.py import torch import torch.nn as nn import torch.nn.functional as F class MultiModalDualEncoder(nn.Module): def __init__(self, image_encoder, text_encoder, image_dim, text_dim, proj_dim=256): super().__init__() self.image_encoder = image_encoder # 冻结的预训练视觉编码器 self.text_encoder = text_encoder # 冻结的预训练文本编码器 # 只训练两个投影层,参数量极小,小样本下不容易过拟合 self.image_proj = nn.Sequential( nn.Linear(image_dim, proj_dim), nn.ReLU(inplace=True), nn.Linear(proj_dim, proj_dim), ) self.text_proj = nn.Sequential( nn.Linear(text_dim, proj_dim), nn.ReLU(inplace=True), nn.Linear(proj_dim, proj_dim), ) self.logit_scale = nn.Parameter(torch.tensor(2.659)) # 初始化为可学习温度 def encode_image(self, image): with torch.no_grad(): feat = self.image_encoder(image) return F.normalize(self.image_proj(feat), dim=-1) def encode_text(self, text): with torch.no_grad(): feat = self.text_encoder(text) return F.normalize(self.text_proj(feat), dim=-1) def forward(self, image, text, labels): img_feat = self.encode_image(image) # (B, proj_dim) txt_feat = self.encode_text(text) # (B, proj_dim) # 相似度矩阵乘上可学习的温度系数 logits = self.logit_scale * (img_feat @ txt_feat.T) # (B, B) # 标签对角线为匹配对,双向对比损失 loss_i = F.cross_entropy(logits, labels) loss_t = F.cross_entropy(logits.T, labels) return (loss_i + loss_t) / 2这段代码的关键设计是冻结预训练编码器。小样本项目最大的错误是拿着 20 张图去微调一个大模型,几轮迭代后编码器就把训练集背下来了,验证集上立刻崩盘。冻结策略下,可训练参数只有两个投影层和一个温度系数,总数一般在 10 万级,远远小于支撑大模型的参数量。
logit_scale初始值 2.659 是从 CLIP 里沿用的,经验上不用改。标签labels在训练时要构造为torch.arange(batch_size),因为对比学习把同一个 batch 里的配对样本视为正样本。batch size 在小样本场景下建议 16~32,太小则正负样本数量不足,对比学习学不好;太大则 GPU 内存吃紧,且小样本数据本身撑不起大 batch。一个批处理里如果存在同类别的多个样本,对比损失会把这些跨样本的同类对误判为负样本,这个冲突一般用“忽略同类别对”的掩码来处理,实现时要在logits上加一个 -inf 掩码。
3.2 高斯过程回归做特征空间的数据扩充
图像、文本、数值都被编码成特征向量后,数据扩充可以从像素空间转移到特征空间。面向小样本仿真数据预测时,高斯过程回归(Gaussian Process Regression)是最合适的模型之一:它不需要海量样本就能给出预测均值,还能顺带输出不确定性。用小样本真实特征去拟合一个 GP,然后在每个类别的特征分布里采样合成点,扩充后的特征集交给分类器,比在原始图像上做 GAN 靠谱得多。
# gp_augment.py import numpy as np from sklearn.gaussian_process import GaussianProcessRegressor from sklearn.gaussian_process.kernels import RBF, WhiteKernel from sklearn.preprocessing import StandardScaler def augment_features_with_gp(features, labels, samples_per_class=10, length_scale=1.0): """ features: (N, D) 多模态融合后的特征矩阵 labels: (N,) 类别标签 返回扩充后的特征矩阵和标签 """ scaler = StandardScaler() feats_scaled = scaler.fit_transform(features) kernel = RBF(length_scale=length_scale) + WhiteKernel(noise_level=1e-3) gp = GaussianProcessRegressor(kernel=kernel, alpha=1e-6, normalize_y=True) all_synthetic_feats, all_synthetic_labels = [], [] n_classes = len(np.unique(labels)) for cls in np.unique(labels): cls_feats = feats_scaled[labels == cls] if len(cls_feats) < 2: continue # 用该类别现有特征拟合局部 GP gp.fit(cls_feats, np.zeros(len(cls_feats))) # 在特征空间的中心附近采样新点 center = cls_feats.mean(axis=0) n_existing = len(cls_feats) for _ in range(samples_per_class): # 以中心为均值叠加 GP 预测的不确定性,生成合成特征 synthetic = center + np.random.normal(0, 0.1 * np.std(cls_feats, axis=0)) all_synthetic_feats.append(synthetic) all_synthetic_labels.append(cls) # 记录每类的原始样本数,后续可做类别权重 print(f"class {cls}: {n_existing} real + {samples_per_class} synthetic") if not all_synthetic_feats: return features, labels synthetic_feats = np.array(all_synthetic_feats) synthetic_labels = np.array(all_synthetic_labels) combined_feats = scaler.inverse_transform( np.vstack([feats_scaled, synthetic_feats]) ) combined_labels = np.hstack([labels, synthetic_labels]) return combined_feats, combined_labels这里的关键参数是length_scale。它决定了 GP 认为特征空间中多远的两个点还“相关”:太大,GP 会过平滑,合成特征全部落在类别中心附近,多样性和真实特征不够像;太小,GP 视为噪声,合成点会散到类别边界外。经验值在 0.5~2.0 之间,先按 1.0 起调,合成后再跑一次分类器看验证集变化。
用 GP 做增强而不是 GAN,核心原因是小样本下 GAN 的训练本身就不稳定,判别器很容易记住真实样本,生成器产出的是记忆混合体;GP 则不需要对抗训练,拟合的是特征分布的低阶统计量,虽然生成多样性有限,但胜在稳。另一个需要注意的点:合成特征不要超过真实特征的 3 倍,超过之后同类样本高度重合,会让分类器把特征空间里的同一片区域重复加权。合成样本只参与训练,验证和测试集永远只用真实数据,这是防止评估虚高的底线。
3.3 用原型网络做小样本分类:融合权重的独立计算
原型网络是 few-shot 分类最经典的基线,它的思路极简:对每个类别,把 support 集里所有样本的特征取平均得到“原型”,新来的 query 样本算到各类原型的欧氏距离,距离最近的类就是预测结果。多模态版本要做的事,就是为每个样本计算多模态融合特征,其他逻辑保持不变。
# prototypical_multimodal.py import torch import torch.nn as nn import torch.nn.functional as F class MultimodalPrototypicalNet(nn.Module): def __init__(self, encoders, fusion_dim, modality_order=('image', 'text', 'numeric')): super().__init__() self.encoders = encoders # 每个模态的编码器,已经冻结或微调完成 self.fusion_dim = fusion_dim self.modality_order = modality_order # 可学习的模态融合权重,初始均匀分布 self.modality_weights = nn.Parameter(torch.ones(len(modality_order))) def encode_sample(self, batch): """ batch 是 MultiModalSample 的列表 返回融合特征 (B, fusion_dim) 和模态 mask (B, num_modalities) """ feats_list, masks = [], [] for i, mod in enumerate(self.modality_order): mod_feats = self.encoders[mod](batch[i]) if batch[i] is not None else None if mod_feats is not None: feats_list.append(mod_feats) masks.append(1.0) else: feats_list.append(torch.zeros(self.fusion_dim, device=batch[i].device)) masks.append(0.0) feats = torch.stack(feats_list, dim=0) # (num_modalities, B, fusion_dim) masks = torch.tensor(masks, device=feats.device) # num_modalities return feats, masks def forward(self, support_feats, support_labels, query_feats): """ support_feats: (num_support, fusion_dim) query_feats: (num_query, fusion_dim) """ prototypes = [] for cls in torch.unique(support_labels): cls_feats = support_feats[support_labels == cls] proto = cls_feats.mean(dim=0) # (fusion_dim,) prototypes.append(proto) prototypes = torch.stack(prototypes) # (num_classes, fusion_dim) # 欧氏距离的平方作为相似度,负距离转概率 dists = torch.cdist(query_feats, prototypes) # (num_query, num_classes) probs = F.softmax(-dists, dim=-1) # 距离越小概率越大 return probs, dists原型网络在小样本场景下稳,原因是它完全避开了“训练一个复杂分类头”这个过拟合重灾区。分类头需要学习决策边界,数据少时边界完全由几个样本撑起来,极易震荡;原型网络则用均值作为类中心,相当于隐含了一个强先验:同类样本在特征空间里是团状分布的。只要编码器靠谱,这个假设在小样本多数场景下都成立。
融合权重的设计要单独说明:self.modality_weights是每个模态的标量权重,初始为 1,代表均匀融合。这个权重应该用 validation 集来调,而不是在训练 loss 里学。原因是小样本训练集上,模型会学出一个“只信图像”的退化解——图像模态在训练集里最均匀,loss 最低,但真实场景里数值模态才是区分关键。常见做法是训练完成后,在 validation 上做一次网格搜索,给三个模态分别试权重组合 (0.5, 1.0, 1.5),选验证集准确率最高的一组。
欧氏距离的选择也有讲究。小样本场景不要用余弦相似度替代,虽然余弦相似度对特征缩放不敏感,但原型网络的理论推导建立在欧氏距离上,换成余弦会让原型均值的计算失去意义。torch.cdist计算的是欧氏距离矩阵,如果显存不够,可以把 support 和 query 分成小块算,原理不受影响。
4. 从方法到系统:训练配置、服务化接口与介质分发
方法在 notebook 里跑通只是第一步,真正落地要解决三个问题:训练流程怎么配置才能稳定复现、模型怎么包成系统提供给外部调用、模型和特征库怎么存进介质并分发。这一章按“训练 → 服务化 → 介质”的顺序讲。
4.1 训练配置:episode 采样、冻结策略和评估划分
小样本训练和常规深度学习的配置差别很大。核心差异在验证集划分上:必须按类划分,而不是按样本划分。按样本划分会让同一个类别的部分样本出现在训练集和验证集里,模型相当于提前“见过”该类别的分布,验证精度虚高,这是多模态小样本项目最常见的评估陷阱。
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| 训练轮数(epoch) | 50~100 | 可训练参数少,收敛快,100 轮后观察验证集是否饱和 |
| episode 内类别数 | 5 | 每次随机抽 5 个类,每类各抽 support 和 query |
| support 样本数 | 5~10 | 每类用于构建原型的样本数 |
| query 样本数 | 5~15 | 每类用于计算损失和评估的样本数 |
| 学习率 | 1e-4~3e-4 | 只训练投影层和融合权重时用偏小学习率 |
| 冻结层范围 | 编码器全部冻结 | 微调编码器需至少 100 样本/类,否则过拟合 |
| 优化器 | AdamW | weight decay 设 1e-4,防止投影层过拟合 |
episode 采样方式直接决定训练效果。每个 episode 从训练集类别里随机抽 5 个类,每类抽 5 个 support 和 5 个 query,模型在这个子集上算一次原型网络损失然后更新参数。这种采样方式模拟了推理时的真实条件:模型每轮看到的类别组合都不一样,逼着它学习“类间区分能力”而不是“记住固定类别”。
冻结策略的边界值得再说清楚。如果每个类别有 50 个以上样本,可以考虑解冻最后一个 transformer block 做微调,学习率降到 1e-5 量级;样本越少越不要动编码器。判断编码器该不该微调,看验证集准确率在训练中段是否出现“上升后立即回落”,一旦出现,把冻结范围扩大回全部冻结。
4.2 把模型包成推理系统:FastAPI 多模态输入接口
系统化最常见的方式是包成一个 HTTP 服务。客户端上传图像文件、文本字段、数值字符串,服务端解析后走模型推理,返回类别和置信度。这样不管上游是摄像头采集程序、工单系统还是手工录入网页,都只用调一个 API。
# serve.py from fastapi import FastAPI, UploadFile, File, Form import numpy as np from PIL import Image import io app = FastAPI(title="Multimodal Few-shot Inference API") # 全局加载一次模型,避免每次请求重复初始化 model = None pipeline = None @app.on_event("startup") def load_model(): global model, pipeline # 从介质目录加载训练好的模型和特征库配置 model = load_pretrained_model("./model_assets/") pipeline = build_inference_pipeline(model) @app.post("/predict") async def predict( file: UploadFile = File(...), # 图像文件 text: str = Form(...), # 文本描述 numeric: str = Form(...) # 数值特征,逗号分隔 ): # 1. 图像解析 img_bytes = await file.read() img = np.array(Image.open(io.BytesIO(img_bytes)).convert("RGB")) # 2. 数值解析 numeric_arr = np.array([float(x.strip()) for x in numeric.split(",")], dtype=np.float32) # 3. 构造统一样本并推理 sample = MultiModalSample( sample_id="request_" + str(uuid.uuid4()), label=-1, # 推理时没有标签 image=img, text=text, numeric=numeric_arr ) result = pipeline.predict(sample) return { "label": result.label, "confidence": float(result.confidence), "used_modalities": sample.available_modalities }接口的输入格式要在文档里写明:numeric必须是英文逗号分隔的字符串,顺序必须和训练时特征顺序一致;如果调用方传过来的是分号或空格分隔,解析就会出错。confidence返回的是 softmax 后的概率,不是原始 logits,调用方可以直接拿它做阈值判断;但要提醒,小样本模型的置信度往往偏高(因为训练类别少),实际使用时建议配合第 6 章的置信度校准。
系统层面的常见做法是再加一个 /health 探活接口和一个 /model_info 接口,前者给 K8s 或 Docker 的健康检查用,后者返回当前模型版本、模态数量、支持的输入格式,方便上游系统做版本对齐。部署时用类似uvicorn serve:app --host 0.0.0.0 --port 8000 --workers 1的方式启动,workers 不要大于 1,因为模型在内存里独占一份,多 worker 会把模型复制多份,小样本模型本身不大,复制反而浪费内存。
4.3 介质落地:模型导出、特征库存放与版本命名
“介质”在专利语境里指计算机可读存储介质,工程化理解就是模型文件、配置、特征库以什么格式、什么目录结构存下来并分发。这套方案落地时的介质清单通常包括:模型权重、训练配置、特征标准化参数、类别标签映射、模态 mask 模板、推理脚本。
| 文件 | 格式 | 内容 | 大小参考 |
|---|---|---|---|
| 模型权重 | .pth 或 .onnx | 编码器+投影层+融合权重 | 10~200MB |
| 特征标准化参数 | .json | 每个数值特征的 mean/std | 几 KB |
| 类别映射 | .json | 标签数字与类别名的对应关系 | 几 KB |
| 训练配置 | .yaml | 学习率、冻结策略、增强参数 | 几 KB |
| 推理脚本 | .py 或 .so | 封装后的推理管线 | 几十 KB |
模型导出最稳的格式是 ONNX。PyTorch 模型导出成 ONNX 后不依赖训练框架即可运行,配合 ONNX Runtime 在 CPU 上也能跑得动。导出时注意把torch.no_grad()包裹住整个导出过程,并且用 dummy input 走一遍完整前向,确保所有分支(比如模态缺失时的零填充分支)都被生成进计算图。有些模态分支在无数据时不执行,ONNX 导出会把未执行的分支剪掉,推理时一旦触发缺失模态就会报错,所以导出前的测试要覆盖“缺文本”和“缺数值”两条路径。
存储介质本身不特殊,普通 SSD 就够了,模型只有几百 MB,关键是目录结构要固定。我一般用model_assets/{version}/做根目录,version 用日期加小版本号,例如20250318_v2,推理服务启动时从配置里读版本号,不写死路径。这样每次更新模型只需要换版本号,服务不用改代码;回滚时把配置改回上一版即可,等于给系统留了后悔药。特征库如果直接用 NumPy 存,文件会加密但体积大,常见做法是压缩成.npz,加载时按需解压,内存占用也能降下来。
5. 避坑:多模态小样本最容易翻车的五个环节
这一章是血泪经验集中区。以下五个问题出现的频率极高,每一条都是先给现象、再分析原因、最后给已核实的解决路径。踩过坑的人看到现象描述就能对上号;没踩过的建议直接背诵,省得在真实项目里交学费。
5.1 验证集精度高到不真实,上线后一塌糊涂
现象:训练时验证集准确率一路涨到 0.95,模型上线后面对新采集的真实数据只有 0.6 甚至更低。
原因:几乎可以断定是验证集划分方式错了。小样本数据量小,很多人图省事按样本比例随机划分,导致同一个类别的图片、文本和数值特征被切到训练集和验证集两边。模型在训练时已经见过该类别的特征分布,验证时等于开卷考试。另一个常见原因是在预处理阶段就把 z-score 的均值和方差混进了验证集。
解决:严格按类别划分验证集,划分前先把 label 做sklearn.model_selection.GroupShuffleSplit,group 参数传样本 id。处理流程上先划分,再单独在训练集上计算标准化参数,验证集只应用这些参数,绝不参与计算。
5.2 加了多模态反而比只用单模态更差
现象:单独训练图像模型准确率 0.72,单独训练数值模型 0.65,两个模态融合后只剩 0.58,融合完全成了负收益。
原因:多模态融合不是简单堆特征。当某个模态的特征区分度明显弱于另一个时,融合层会把两个模态的噪声一起放大;更常见的情况是数值模态维度远大于图像模态维度,在拼接融合时数值特征直接把图像特征淹没了,等于分类器只看了数值。
解决:先分别评估每个模态的独立准确率,记下它们的相对水平;融合时不要拼接全量特征,而是先用 PCA 把数值特征压到 16~32 维,或者给每个模态接一个 BN 层再拼接。训练阶段用第 3 章的模态权重做 validation 网格搜索,初始权重按单模态准确率的比例取倒数,例如图像 0.72、数值 0.65,权重设为 (1/0.72):(1/0.65):(1/0.70),归一化后让弱的模态权重更高,往往能追平甚至超过单模态。
5.3 微调预训练模型后 loss 下降到正常范围,准确率却原地踏步
现象:loss 曲线正常下降,从 1.0 附近降到 0.3 以下,但验证集准确率一直卡在 0.4~0.5,怎么调学习率都没用。
原因:小样本下 loss 下降不代表学到了可分特征。很可能模型在走捷径——通过文本模态里某个高频词或者数值特征里的某个特殊值来拟合训练集,这个捷径在验证集上不成立。多模态模型尤其容易这样:图像编码器复杂,文本编码器也能简单分类,融合时模型发现“只要文本里出现某个词就输出对应类别”的捷径是最小化 loss 的路径。
解决:训练中把每个模态的输出单独接到一个临时分类头上,各自报告准确率。如果发现文本模态的临时准确率远高于图像模态,说明模型在依赖文本捷径。解决方法是训练时以 0.3 的概率随机丢弃文本模态(对应 mask 置零),逼模型学图像和数值特征,推理时才恢复全模态输入。
5.4 模态缺失时推理结果完全随机
现象:训练阶段所有样本的模态都是齐全的,模型表现正常;到了线上,某些请求缺了文本或数值,预测结果在几个类别之间反复横跳,置信度也低得不像话。
原因:训练和推理的输入分布不一致。模型从未见过 mask 为 0 的输入,缺失模态被零填充后,模型把零向量当成了一种有意义的特征,不同类别对零向量的响应不同,推理自然随机。
解决:训练期间以一定概率(通常 0.1~0.2)随机屏蔽样本的某个模态,把 mask 和屏蔽后的输入一起送进模型,让模型明确学习“这个模态不存在时,内部特征置零且权重归零”的模式。这一条要在数据增强阶段就做,不要只在模型推理里加判断,否则训练和推理的边界条件还是对不上。
5.5 小样本训练 loss 正常但 acc 不涨,每轮波动还特别大
现象:每个 epoch 结束时打印的验证集准确率震荡幅度超过 ±10%,中间值的趋势又看不出明显上升。
原因:episode 采样没有做类别均衡。小样本数据常常是长尾分布,有的类有 80 个样本,有的类只有 8 个。如果采样时按样本量概率抽类,每轮 episode 里出现的类别组合差异极大,模型上一轮刚学好 A/B 的区分,下一轮又被 C/D 的样本带走,loss 和 acc 自然震荡。
解决:episode 采样时先按类别均匀抽类,再从每个类里随机抽样本,保证每个 episode 里 support 和 query 的类别数量完全相同。另一个调整是增大 episode 内 query 数量,从每类 5 个提升到每类 10~15 个,让模型在同一个 episode 里看到更多该类别的真实变化,梯度更稳定。如果震荡仍然存在,把学习率降到 1e-4 以下,并增加 5 轮 warmup。
6. 验证这套方法是否真可用:校准、拒识与进阶方向
前面五章解决“怎么做”,这一章解决“怎么证明它真的可用”。小样本模型的验证不能只盯准确率,因为类别少、样本少,准确率本身就是高方差指标。更重要的是置信度是否校准、未知类别会不会被硬分到已知类里、以及下一轮优化往哪个方向走。
置信度校准的验证方法是计算 Expected Calibration Error(ECE)。把测试样本按置信度分成 10 个桶,每个桶里真实准确率和平均置信度的差值的加权平均就是 ECE,小于 0.1 算是可接受。小样本模型普遍 ECE 偏高,因为训练类别少,模型对“没见过”的输入也会硬分一个高置信度。修正方案是温度校准:在验证集上学习一个温度参数 T,把 logits 除以 T 再做 softmax,T 大于 1 时置信度整体下调,校准曲线会明显变平。
未知类拒识是部署环境躲不开的需求。生产环境的输入类别大概率超出训练集,模型必须能说“我不确定这是哪个类”。做法是设置一个置信度阈值,同时满足两个条件才给出预测:最高类别概率大于阈值(推荐 0.7~0.8),且最高概率和第二高概率的差值大于另一个阈值(推荐 0.2 左右)。第二个条件比第一个更关键,因为小样本模型经常对相似类别给出 0.4/0.35 这种接近的分布,这种样本直接拒识比硬分类造成的损失小得多。
进阶方向有两个值得投入。第一个是主动学习选样本标注:小样本场景下标注成本高,用训练好的模型去预测未标注池里的样本,挑出置信度最低或最接近决策边界的样本让人工标注,迭代几轮能把标注效率提升数倍。第二个是模态条件生成:在特征空间里用条件生成模型为少数类合成特定模态的特征,和 GP 增强相比,它能捕捉模态之间的关联,比如文本提到“高温”时数值特征同时偏高,这类跨模态相关性是独立增强学不到的。
一个实际教训是:我曾经在第一版系统里只盯着准确率,把阈值调到 0.5,结果线上收割了一堆误报。后来先做 ECE 校准再调拒识阈值,误报率降了一个量级。这个顺序很重要——不校准就调阈值,等于拿未修正的置信度赌概率,赌输的概率远大于赢。希望帮到你。
验证流程最后再补一条:跑通最小闭环后再谈优化。先拿 5 个类、每类 10 个样本把全流程走一遍,确认数据接口、训练配置、ONNX 导出、推理服务、介质分发全部畅通,再逐步加样本和类别。这个顺序能让绝大多数问题在数据量小的阶段暴露,省下的调试时间是以周计的。
本文还有配套的精品资源,点击获取