17. 视觉模型:用预训练骨干识别报销单
前三篇处理的是声音。资料助手上线一段时间后,员工开始直接上传图片提问:“这张票能报吗?”“帮我看看这张打车票的金额”。在回答之前,系统得先判断这是什么票据,因为发票、打车票、餐饮小票走的是三套不同的报销规则,而有人上传的根本不是票据。这一篇以这个四分类任务为例,讲视觉模型怎样训练和调优:为什么不从零训练、预训练骨干该冻结多少层、数据增强什么时候会把标签改错、ViT 和自监督预训练改变了什么,以及怎样评估才能发现“总体准确率很高,某一类几乎全错”。读完应能为一个小型图像分类需求选出起点,并说出它可能在哪里失败。
两千张图,两千多万个参数
团队从报销系统里导出并人工标注了 2000 张图片(教学示例,下同):发票 900 张、打车票 500 张、餐饮小票 500 张、其他 100 张。“其他”包括会议纪要、合同截图、聊天截图等误传内容。
如果拿一个常见的卷积网络 ResNet-50 从随机初始化开始训练,要学的参数约 2556 万个(torchvision 官方权重记录为 25,557,032 个)。两千张图对两千多万个参数,模型很快就能把训练集全部背下来:它可能记住“蓝色背景的就是发票”“拍得歪的就是小票”,在验证集上表现很差。这正是第 05 篇讲的过拟合,训练 loss 一路下降,验证 loss 早早回头。
更好的起点是迁移学习:拿一个已经在大规模图片上训练过的网络,保留它学到的“看图能力”,只针对报销单任务调整一部分。被借用的那部分网络叫骨干(backbone),负责把图片变成一串特征数字;后面接的小网络叫分类头(head),负责把特征映射到四个类别。
预训练骨干里已经有什么
最常用的预训练来源是 ImageNet-1K,即 ILSVRC-2012 竞赛使用的数据集:训练集 1,281,167 张图片,覆盖 1000 个类别,每类 732 到 1300 张。一个在上面训练好的 ResNet-50,官方权重在验证集上的 top-1 准确率约 76%(V1 权重)到 81%(V2 权重)。
ImageNet 里并没有“增值税发票”这一类,为什么它的骨干仍然有用?网络靠近输入的几层学到的是边缘、角点、颜色块、纹理这类通用局部模式;中间几层组合出线条排列、格子、文字块这样的结构;最后几层才逐渐对应“狗”“汽车”这样的具体类别。对报销单来说,前面几层几乎可以原样复用,最后几层则偏向 ImageNet 的物体类别,需要不同程度的调整。
使用预训练骨干有一个容易被忽视的约定:输入必须按它训练时的方式预处理。torchvision 的 ResNet-50 V2 权重要求先把短边缩放到 232 像素、中心裁出 224×224,再按 ImageNet 的均值 [0.485, 0.456, 0.406] 和标准差 [0.229, 0.224, 0.225] 归一化。训练时这样做、上线时忘了归一化,模型会给出看似正常、实际随机的结果。视觉项目里把它叫作预处理合同。
冻结多少层
“迁移”不是一个开关,而是一条连续的旋钮:从只训练分类头,到所有层一起训练,中间可以只解冻最后一段。下图把同一个 ResNet-50 的三种做法并排放在一起。
三种做法的可训练参数可以手算。ResNet-50 最后输出 2048 维特征,四分类头是一个全连接层:
text
线性探针:2048 × 4(权重)+ 4(偏置)= 8,196 个参数
微调最后一段:layer4 由 3 个瓶颈块组成
第 1 块:1×1(1024→512) + 3×3(512→512) + 1×1(512→2048) + 下采样 1×1(1024→2048) + BN
= 524,288 + 2,359,296 + 1,048,576 + 2,097,152 + 10,240 = 6,039,552
第 2、3 块:1×1(2048→512) + 3×3 + 1×1(512→2048) + BN = 4,462,592,各一个
layer4 合计 6,039,552 + 2 × 4,462,592 = 14,964,736,加分类头约 1497 万
全量微调:25,557,032 − 原 1000 类头 2,049,000 + 新 4 类头 8,196 = 23,516,228,约 2352 万线性探针(linear probe)把骨干完全冻结,只训练最后一层。因为骨干不变,可以先把 2000 张图各跑一次前向,把 2048 维特征存下来,之后的训练只是在 2000 × 2048 的表格上做一次逻辑回归,CPU 上几分钟就完成。它的上限取决于预训练特征本身:如果区分发票和小票所需的信息不在这 2048 个数里,再怎么调分类头也学不出来。
微调最后一段让 layer4 也参与训练。底层的纹理检测器保持不动,高层可以重新组合,适配票据的版式。这时每一步都要跑完整前向,并对 layer4 做反向传播,算力和显存比线性探针多得多,但对一张消费级显卡仍然不算负担。常见做法是给骨干部分设置比分类头小一个量级的学习率,避免一开始随机初始化的分类头产生的大梯度把预训练权重冲乱。
全量微调更新所有层,在数据量大、或者图片和 ImageNet 差别很大(医学影像、遥感、密集文字)时收益最明显。数据只有两千张时,它也最容易过拟合,要配合更强的数据增强、更小的学习率和早停。
实际操作可以按“先便宜、后昂贵”的顺序:先做线性探针,得到一个基线和每类错误样本;如果验证集准确率明显低于需要,再解冻 layer4;仍不够,再考虑全量微调或换骨干。每一步都看验证集,而不是看训练 loss。
不适用的情况:报销单的关键差异常常在小字上,比如抬头印着“增值税电子普通发票”还是“出租汽车专用发票”。ImageNet 的 224×224 中心裁剪会把一张 A4 发票缩到文字几乎看不清,ImageNet 特征也更擅长物体而不是文字。如果线性探针在“发票 vs 打车票”上卡在七八成,解冻更多层未必能补回被缩放丢掉的信息,这时应考虑提高输入分辨率、先做 OCR 再按文字分类,或者改用下一篇的图文模型。
数据增强:只做不改变标签的变化
两千张图不够多,可以在训练时对每张图做随机变换,让模型每个 epoch 看到略有不同的版本,这叫数据增强。它的前提是变换后的图片仍然属于原来的类别,而且在真实上传中可能出现。
对报销单合理的增强包括:小角度旋转(几度以内,模拟手机拍歪)、透视变换(斜着拍桌上的票)、亮度和对比度变化(不同光线)、轻微模糊和 JPEG 压缩(聊天软件转发后的画质)、保留主体的随机裁剪。
有些增强在 ImageNet 上习以为常,放到票据上就会出问题。水平翻转是最典型的例子:一只翻转的猫仍然是猫,翻转的发票却是镜像文字,真实上传里不会出现,模型花力气去适应一类永远不会见到的输入;如果后续还要读文字,镜像文字更是直接把信息毁掉。大幅随机裁剪也可能只留下发票的下半截,把“增值税发票”字样和发票专用章都裁掉,剩下的部分和一张餐饮小票没有区别,标签却仍然写着“发票”,这等于在训练集里制造错标。检查方法很直接:把增强后的图片随机抽一批出来看,问自己“只看这张图,我能判断它是什么吗”。
增强只用于训练集。验证集和测试集使用确定的预处理,否则每次评估的结果都会随机波动。
“其他”只有 100 张
四类里“其他”只占 5%。模型即使把所有“其他”都判成某类票据,总体准确率也只损失 5 个百分点,训练时的损失里这一类的权重同样很小,它自然学得最差。这叫类别不平衡。
常见的处理有三层。第一层是在损失里给少数类更大的权重,或者训练时按类别重新采样,让每个 batch 里各类出现得更均匀。第二层是补数据:从真实上传日志里专门收集误传的图片,这比在损失函数上做文章更有效。第三层是承认“其他”本身没有边界:员工可能上传任何东西,靠 100 张样本不可能覆盖所有“不是票据”的图。分类器之外还需要拒识机制,下文评估部分会讲。
ViT:把图块当成 token
上面一直以卷积网络为例。另一类常见骨干是视觉 Transformer(ViT,Vision Transformer,2020 年提出),它把大语言模型里处理 token 的 Transformer 直接用在图片上。做法是把图片切成固定大小的方块(patch),每个方块展平后经一个线性层变成一个向量,当作一个 token:
text
输入 224 × 224 × 3,patch 大小 16 × 16
patch 数 = (224 / 16) × (224 / 16) = 14 × 14 = 196
每个 patch 展平后 = 16 × 16 × 3 = 768 个数,经线性层映射为模型宽度的向量
序列 = 196 个 patch token + 1 个分类用的 [CLS] token卷积网络天生假设“相邻像素关系更紧密”“同一个图案出现在哪里都一样”,这类内置假设叫归纳偏置,它让卷积网络在数据不多时也学得不错。ViT 几乎没有这些假设,每个 patch 从第一层起就能和所有 patch 做注意力,位置关系要从数据中学出来。ViT 论文的实验正好说明了代价:只在 ImageNet-1K 这种中等规模数据上训练、不加强正则时,ViT 比同规模的 ResNet 低几个百分点;换到 1400 万张的 ImageNet-21k 或约 3 亿张的内部数据集 JFT-300M 上预训练后,情况反过来,ViT 超过了卷积网络。论文的说法是大规模训练胜过归纳偏置。
对资料助手的含义很明确:不要从零训练 ViT,两千张图远远不够;要用已经在大数据上预训练好的 ViT 权重,再按上面的旋钮做迁移。ViT 的另一个实际影响是计算量随 patch 数增长,把输入分辨率从 224 提到 448,patch 数从 196 变成 784,注意力的成对计算量涨到约 16 倍,这在处理文字密集的票据时要算进成本。
自监督预训练:没有标签也能学特征
ImageNet 需要人工给 128 万张图打标签。能不能不要标签,直接从图片本身学到好特征?这类方法叫自监督预训练,它从数据里自己构造练习题。
MAE(Masked Autoencoder,2021)的练习题是“补全被遮住的图块”:随机遮掉 75% 的 patch,只把剩下 25% 送进编码器,再用一个轻量解码器重建被遮住部分的像素。遮掉的比例这么高,是因为图片冗余很大,遮得少时模型靠周围像素插值就能补上,学不到整体结构。编码器只处理四分之一的 patch,训练也因此快了 3 倍以上。MAE 的 ViT-Huge 只用 ImageNet-1K 的图片(不用其标签)预训练,再微调后达到 87.8% 的准确率。
DINO(2021)走另一条路:同一张图做两种不同的裁剪和颜色变换,让“学生”网络的输出去匹配“教师”网络的输出,教师是学生权重的滑动平均,整个过程没有标签,所以叫无标签自蒸馏。DINOv2(2023)把这套方法放到一个自动筛选出的约 1.42 亿张图片的数据集上,训练了 10 亿参数的 ViT 再蒸馏成小模型,特征好到很多任务冻结骨干、只训一个线性头就够用。
这对资料助手有两个用处。一是骨干的选择:DINOv2 这类自监督骨干的冻结特征往往比监督训练的 ImageNet 骨干更通用,线性探针的起点更高。二是公司内部有大量未标注的历史报销图片时,可以先在这些图片上做一轮自监督继续预训练,让骨干适应票据的视觉风格,再用少量标注数据微调。后者成本更高,只有在线性探针和微调都卡住、且未标注图片足够多(数万张以上)时才值得尝试。
检测与分割:换一个任务头
如果需求从“这是什么票”变成“金额栏在哪里”,任务就变成了目标检测(输出类别和矩形框);如果要精确抠出印章区域,就是分割(每个像素一个类别)。它们的训练思路与分类相同:沿用预训练骨干,在上面接检测头或分割头,用标注了框或掩码的数据微调。差别在数据和评估:一张图要标多个框,标注成本高得多;增强时框要跟着图片一起变换;评估用 IoU、mAP 等指标。坐标缩放和还原的细节见计算机视觉。
评估:按类别看,看没见过的图
分类器在 500 张留出的测试图上跑完,总体准确率 88.4%。这个数字看上去可以上线,但把结果按“真实类别 × 预测类别”排成表,即混淆矩阵,问题就出来了:
每一行是一类真实图片被分到了哪里。召回率是这一类里被认出来的比例,“其他”只有 12/30 = 40%;精确率是预测为这一类的图里真正属于它的比例。“其他”只占测试集的 6%,即使全错,总体准确率也只降 6 个点,所以总体准确率掩盖了它。发票和餐饮小票之间互相混淆 16 张,回到图片去看,多是带公司抬头、版式接近发票的餐饮小票,这类样本应该补进训练集,并检查标注规范是否写清了边界。
测试集的切分方式也会骗人。同一张发票常被员工拍了两次再上传,如果一张进训练集、一张进测试集,测试成绩就虚高。应按上传人或按时间切分,让测试集里的图片是模型真的没见过的,做法见把问题变成样本。
最后是分布外(out-of-distribution)图片:截图、自拍、风景照,这些既不属于三类票据,也和训练集里的“其他”样本长得不一样。分类器的 softmax 输出总要在四类里分出概率,面对一张猫的照片,它可能以 0.9 的“置信度”判成餐饮小票。只看最高概率做拒识并不可靠,但可以作为第一道闸:在验证集上选一个阈值,最高概率低于它时不自动分流,而是让员工确认类别。再准备一组专门的分布外测试图,统计有多少被错误地自动处理。上线后把员工手动纠正的样本收集起来,按泛化与模型评估里的分片方法定期复评。
分类器只负责分流,不负责判断“能不能报”。票据真伪、金额是否超标、是否重复报销,仍由报销系统的规则和人工审核决定。
这类任务是否一定要训练,也值得先问一句。如果四类票据靠文字就能区分,OCR 加关键词规则可能已经够用;如果类别经常变化,下一篇的图文模型可以不训练、直接用文字描述类别做零样本分类,先拿它作为基线再决定。
视觉模型的训练很少从零开始:拿一个在 ImageNet 或自监督大数据上预训练的骨干,按数据量决定冻结多少层,用不改变标签的增强扩充样本,再按类别、按混淆矩阵、按分布外图片评估。ViT 和 MAE、DINO 把“大数据预训练、小数据迁移”推得更远,也把输入分辨率和计算量的取舍带进来。下一篇把图片和文字连在一起,看 CLIP 和 LLaVA 这类模型怎样训练,以及怎样让资料助手读懂表单截图:18. 图文模型与多模态大模型。
