迁移学习实战:小数据集花卉识别从数据准备到模型部署
迁移学习这四个字刚入门的时候我也觉得挺玄乎听起来像是要把一个训练好的大模型整个搬过来用。后来真正动手做识花项目才发现它的核心思路其实特别朴素你不需要从零开始教一个模型认识花而是找一个已经见过上千万张图的模型告诉它你之前学的那些边缘、纹理、形状的本事别丢现在只需要把最后那层判断改一改专门用来分辨花的种类就行。我拿到的需求很典型手头只有几千张花卉图片分属几十个类别每类样本量参差不齐有的类别只有几十张。如果直接从头训练一个卷积网络准确率大概率卡在及格线附近上不去而且训练时间长到让人怀疑人生。迁移学习就是为这种数据不够、算力有限、但又想快速出效果的场景准备的。这篇文章我会把整个识花项目的完整链路拆开讲——从数据集怎么整理、预训练模型怎么选、特征提取和微调两种策略分别适合什么情况到训练过程中那些文档里不会写的坑以及最终怎么评估和部署。不管你是刚接触深度学习的学生还是想快速验证一个图像分类想法的开发者这套流程都能直接拿去改改用。1. 先搞清楚识花任务到底难在哪1.1 花卉分类和普通物体分类的区别很多人觉得图像分类都差不多无非是输入图片输出类别。但花卉识别有几个很特殊的地方直接决定了你不能随便套一个现成的分类流程。首先是类间差异极小、类内差异极大。同一个属的不同品种花瓣形状、颜色可能只差一点点比如某些蔷薇科的花肉眼都得凑近了看才能分辨。反过来同一品种因为拍摄角度、光照、开花阶段不同看起来又像是完全不同的东西。这就导致模型必须学到非常细粒度的特征而不是粗线条的有没有花瓣。其次是背景干扰严重。花卉图片大多来自自然拍摄背景里有叶子、枝干、泥土、其他植物甚至还有昆虫。模型很容易偷懒把背景里的某些纹理当成判别依据结果换一个拍摄场景就完全失效。这个问题在数据量少的时候尤其致命。第三是类别体系本身就不统一。不同数据集的分类粒度不一样有的按属分有的按种分有的干脆按颜色分。你在做迁移学习之前必须先确定自己的类别体系否则后面标签混乱训练出来的模型根本没法用。1.2 为什么小数据集上从头训练几乎必败我做过一个对比实验用同样的几千张花卉图片一个从随机初始化开始训练一个用预训练模型做迁移。前者训练了五十个epoch验证集准确率在百分之四十多就上不去了而且波动很大后者只训练了十几个epoch准确率就稳定在百分之八十五以上。原因其实不复杂。卷积网络的前几层学的是边缘、角点、颜色斑块这些低级特征中间层学的是纹理、局部形状后面几层才逐渐组合成语义级别的部件。低级和中级特征在不同视觉任务之间是高度通用的——识别猫狗用到的边缘检测器识别花鸟同样用得上。从头训练意味着这些通用特征也要靠你那几千张图重新学一遍数据量根本不够支撑模型只能记住训练集的噪声。迁移学习的价值就在于它把一个在大规模数据上已经学好的特征提取器直接拿来用你只需要针对自己的任务调整高层语义部分。这相当于站在别人的肩膀上而不是从地基开始盖楼。1.3 迁移学习识花的两种基本姿势具体到操作层面迁移学习做识花主要有两条路选择哪条取决于你的数据量和任务难度。第一种是特征提取。把预训练模型当成一个固定的特征转换器把每张花图变成一串特征向量然后在这个特征向量上训练一个简单的分类器比如逻辑回归或者小型全连接网络。预训练模型的权重完全冻结不参与更新。这种方式训练极快几分钟就能跑完适合数据量很小每类几十张或者只是想快速验证想法的情况。第二种是微调。不仅替换最后的分类层还解冻预训练模型的一部分甚至全部层用较小的学习率继续训练。这样模型的高层特征会逐渐适应花卉领域的特殊性。微调效果通常更好但需要更多数据和时间也更容易过拟合。实际项目中我一般会先用特征提取跑一个baseline看看数据本身有没有问题、类别是否可分然后再上微调去冲更高的准确率。这个顺序很重要因为如果特征提取阶段效果就很差说明问题可能出在数据质量或标签上这时候盲目微调只是浪费算力。2. 数据集准备比模型选择更关键的环节2.1 花卉数据的采集与清洗我拿到的原始数据是从几个公开渠道汇总来的加起来大概六千多张覆盖四十多个类别。但直接拿来用是不行的必须先过一遍清洗流程。第一步是去重。同一张图可能在不同渠道里重复出现或者有轻微的裁剪、缩放版本。我用的是感知哈希pHash做近似去重把每张图转成一个六十四位的指纹计算汉明距离距离小于某个阈值的就认为是重复图只保留一张。这一步删掉了将近八百张冗余图片如果不做训练集和验证集之间就可能出现泄漏导致验证准确率虚高。第二步是剔除低质量样本。分辨率太低、严重模糊、主体被遮挡超过一半的图片留着只会干扰训练。我写了一个简单的过滤脚本检查图片短边是否小于二百像素以及用拉普拉斯算子算清晰度低于阈值的直接丢弃。第三步是检查标签一致性。这是最费精力但最不能省的一步。花卉类别名称在不同来源里写法不统一有的用拉丁学名有的用中文俗名还有的用英文通用名。我建了一张映射表把所有标签统一到一套命名体系上。另外还要人工抽查看看有没有明显标错的图片比如把月季标成了玫瑰。标错的样本对模型伤害很大尤其是小类别里错几张模型就彻底学偏了。2.2 类别不平衡的处理策略清洗完之后类别分布很不均匀。最多的类别有三百多张最少的只有三十几张差了十倍。这种不平衡如果不处理模型会倾向于预测样本多的类别小类别基本被忽略。我试过几种方案最后组合使用。第一种是数据增强对小类别做更强的增强包括随机旋转、水平翻转、颜色抖动、随机裁剪。注意花卉识别里垂直翻转要慎用因为花一般不会倒着长垂直翻转产生的样本不符合真实分布。第二种是重采样在构造每个batch的时候让每个类别被抽到的概率大致相等这样小类别在每个epoch里也能被充分训练。第三种是损失函数加权给样本少的类别更高的损失权重。这里有个经验不要一上来就用很复杂的方案。我最初直接上了focal loss加过采样加激进增强结果模型在小类别上过拟合验证集上反而更差。后来退回到适度增强加类别平衡采样效果就稳了。处理不平衡的核心是让模型见到足够多的小类别样本而不是把损失函数搞得过于复杂。2.3 划分训练集、验证集和测试集划分看似简单其实很容易出错。我的做法是按类别分层抽样保证每个类别在训练集、验证集、测试集里的比例一致。比例大概是七比一点五比一点五。这里有个坑要特别提醒如果同一个植株或同一组拍摄的图片被分到了训练集和验证集两边验证准确率会严重虚高。因为同一植株的不同照片非常相似模型在训练时见过了验证时自然容易认出来。正确的做法是按拍摄批次或图片来源分组同一组的图片要么全在训练集要么全在验证集。我在项目中期才发现这个问题重新划分之后验证准确率掉了将近十个百分点但那个数字才是真实的。另外测试集要留到最后再用训练过程中绝对不要拿测试集来调参。我习惯把测试集单独放在一个目录里训练脚本根本不加载它避免手滑。3. 预训练模型的选择与改造3.1 主流骨干网络的对比与取舍选预训练模型的时候我主要看三个维度在ImageNet上的表现、参数量、推理速度。识花这个任务不算特别难不需要上最大的模型但也不能太小的否则特征表达能力不够。我实际对比了几个常见的骨干网络。ResNet50是个稳妥的选择结构经典预训练权重容易获取参数量两千五百万左右在大多数花卉数据集上微调后能到百分之八十八以上的准确率。EfficientNet系列在同等准确率下参数量更小B0版本只有五百多万参数推理速度快适合后面要部署到移动端的场景。Vision Transformer这两年很火在大数据集上微调效果确实好但对小数据集不太友好需要更仔细地调学习率和正则化否则容易过拟合。骨干网络参数量相对推理速度小数据集微调难度我的推荐场景ResNet50约25M中等低通用首选稳定EfficientNet-B0约5M快中移动端部署ViT-Base约86M慢高数据量充足时MobileNetV3约5M很快低极致轻量化最后我选了ResNet50作为主力模型同时用EfficientNet-B0做了一个轻量版本备用。选型的原则不是追新追大而是匹配你的数据规模和部署需求。数据只有几千张的时候大模型带来的收益很有限反而增加过拟合风险。3.2 替换分类头的正确做法预训练模型原本是在ImageNet上做一千类分类的最后的全连接层输出维度是一千。我们要改成自己的类别数比如四十五类。标准做法是把最后的全连接层换掉前面加一个Dropout层防止过拟合。但这里有个细节很多人忽略新加的分类层初始化方式会影响训练稳定性。如果随机初始化得不好一开始梯度会很大可能把前面预训练好的特征也带偏。我一般用较小的标准差做初始化或者干脆先冻结骨干网络只训练分类头几个epoch等分类头稳定了再解冻微调。这个预热步骤能让训练曲线平滑很多。另外有些骨干网络在分类头之前有一个全局池化层替换的时候要注意保持维度匹配。我见过有人直接把全连接层删了接自己的网络结果忘了池化特征维度对不上报错报了半天。3.3 冻结策略冻多少层才合适微调的时候冻结多少层是个需要实验的问题。我的经验是分阶段来。第一阶段冻结整个骨干网络只训练新加的分类头。学习率可以设大一点比如千分之一。这个阶段通常跑三到五个epoch让分类头先适应新的类别空间。第二阶段解冻骨干网络的后半部分比如ResNet50的layer3和layer4用较小的学习率比如万分之一一起训练。前面的层继续保持冻结因为它们学的是通用低级特征没必要动。第三阶段可选如果数据量还算充足可以解冻全部层做精细微调学习率再降一个数量级。但这一步要小心很容易过拟合需要配合早停和更强的正则化。我实测下来只做到第二阶段就能拿到很不错的效果第三阶段提升有限但风险不小。对于花卉这种类间差异细碎的任务让高层特征充分适应比动底层特征更重要。4. 训练过程中的实操细节与调参4.1 数据增强的分寸感数据增强是防止过拟合的利器但用过头会适得其反。花卉识别里我常用的增强手段和参数范围是这样的。随机水平翻转基本可以放心用概率设零点五。随机旋转控制在正负三十度以内因为花在自然状态下不会歪太多。颜色抖动要温和亮度、对比度、饱和度的变化幅度控制在零点二左右太剧烈会让花色失真而颜色恰恰是识花的重要线索。随机裁剪缩放的比例设在零点七到一点零之间保证主体不会被裁掉太多。我踩过的一个坑是用了CutMix和MixUp这类强增强。它们在通用分类任务上效果好但花卉识别里把两朵花混在一起会让模型学到错误的特征组合尤其是细粒度分类时花瓣的局部特征被破坏准确率反而下降。强增强适合类别差异大的任务细粒度识别要克制。4.2 学习率调度与优化器选择优化器我用的是AdamW权重衰减设零点零一。相比SGDAdamW在微调场景下收敛更快对学习率不那么敏感适合快速迭代。如果追求极致准确率SGD加动量最终可能略好但需要更仔细地调学习率。学习率调度用的是余弦退火加预热。预热阶段大概占总步数的百分之五学习率从接近零线性升到初始值避免一开始就大步更新破坏预训练权重。然后余弦退火慢慢降到接近零。初始学习率在微调阶段设万分之一到万分之五之间分类头单独训练时可以设千分之一。有个细节分类头和解冻的骨干层可以用不同的学习率这叫判别式学习率。骨干层用小的分类头用大的因为分类头是随机初始化的需要更快地学习。在PyTorch里可以通过参数组来实现。import torch from torch import optim # 分类头参数用较大学习率骨干网络用较小学习率 head_params list(model.fc.parameters()) backbone_params [p for n, p in model.named_parameters() if not n.startswith(fc) and p.requires_grad] optimizer optim.AdamW([ {params: backbone_params, lr: 1e-5}, {params: head_params, lr: 1e-3} ], weight_decay0.01)4.3 早停与模型保存策略训练过程中我监控验证集的准确率和损失。早停的耐心值设成七个epoch如果验证准确率连续七个epoch没有提升就停止训练。模型保存只保留验证准确率最高的那个checkpoint不要每个epoch都存否则磁盘很快就满了。这里有个容易忽略的点验证损失和验证准确率不一定同步。有时候损失还在降但准确率已经不动了有时候反过来。我一般以准确率为主但如果损失突然飙升说明训练不稳定即使准确率没降也要警惕可能需要降低学习率。另外训练日志要记录每个epoch的训练损失、验证损失、验证准确率、学习率方便后面画曲线分析。我习惯用TensorBoard或者简单的CSV记录出问题的时候回看曲线能快速定位。5. 效果评估与常见问题排查5.1 混淆矩阵告诉你模型到底错在哪光看总体准确率是不够的。我训练完之后一定会画混淆矩阵看看哪些类别之间容易混淆。在花卉项目里我发现模型经常把同一属的不同品种搞混比如几种蔷薇科的花互相错分这符合直觉因为它们的形态确实接近。混淆矩阵还能暴露数据问题。如果某个类别几乎全被预测成另一个类别很可能是这两个类别的标签在数据里本身就混乱或者其中一个类别的样本质量太差。这时候回去检查数据比调模型更有效。除了混淆矩阵我还会看每个类别的精确率和召回率。有些小类别虽然总体准确率被拉高了但召回率很低说明模型基本没学会识别它们只是被大类别的正确预测掩盖了。5.2 过拟合和欠拟合的判断与应对训练过程中最常见的两个问题就是过拟合和欠拟合判断方法很直接看训练损失和验证损失的差距。过拟合的表现是训练损失持续下降验证损失先降后升两者差距越来越大。应对手段包括增强数据增强、增加Dropout、加大权重衰减、减少微调的层数、早停。我遇到过一次严重过拟合训练准确率到了百分之九十九验证只有百分之七十多后来发现是验证集划分时和训练集有重叠图片重新划分后问题就缓解了。欠拟合的表现是训练损失和验证损失都居高不下两者差距不大。这时候说明模型容量不够或者训练不充分。可以尝试解冻更多层、增大模型、提高学习率、训练更久。但要注意如果数据本身噪声很大或者标签质量差欠拟合可能是数据的问题换模型也救不了。5.3 那些文档里不会写的坑说几个我在项目里实际踩过的坑都是文档里不太会提但很影响结果的。第一个是图片通道问题。有些花卉图片是带透明通道的PNG直接读进来是四通道而预训练模型期望三通道不处理会报错或者产生奇怪的结果。我统一在数据加载时转成RGB避免这个问题。第二个是归一化参数不匹配。预训练模型训练时用的均值和标准差是ImageNet的统计值如果你用自己的数据统计值做归一化特征分布就对不上效果会打折。我一开始图省事用了自己算的均值方差准确率比用ImageNet标准参数低了三个百分点。第三个是验证阶段的模型状态。PyTorch里模型有训练和评估两种模式Dropout和BatchNorm的行为不一样。我见过有人验证时忘了调model.eval()导致验证结果波动很大还以为是模型不稳定。训练循环里一定要记得切换状态并且用torch.no_grad()包住验证过程省显存也更快。第四个是随机种子的设置。深度学习训练有随机性不同随机种子跑出来的结果可能差一两个百分点。做对比实验的时候一定要固定种子否则你以为是模型改动的效果其实只是随机波动。我一般会跑三次不同种子取平均结果更可靠。6. 从训练完成到实际可用6.1 模型导出与推理优化训练完的模型要真正用起来还得做导出和优化。如果只是本地测试直接保存state_dict加载就行。但如果要部署到服务端或者移动端就需要转成更高效的格式。服务端部署我一般导出成ONNX格式然后用ONNX Runtime推理比原生PyTorch快不少而且不依赖PyTorch环境。导出的时候要注意指定动态batch维度否则推理时只能处理固定batch大小。移动端可以用TensorFlow Lite或者NCNN把模型量化成int8体积能缩小到原来的四分之一速度也快很多准确率损失通常在一个百分点以内。推理优化还有一个实用技巧是把预处理也集成进模型。正常情况下推理前要对图片做缩放、归一化这些操作如果放在Python里做会拖慢速度。可以把它们写成模型的一部分导出时一起带走推理时直接输入原始图片就行。6.2 一个简单的推理接口示例下面是我常用的推理脚本骨架把加载模型、预处理、预测封装成一个类方便复用。import torch import torch.nn as nn from torchvision import transforms, models from PIL import Image class FlowerClassifier: def __init__(self, ckpt_path, num_classes, class_names): self.device torch.device(cuda if torch.cuda.is_available() else cpu) self.class_names class_names # 构建和训练时一致的网络结构 self.model models.resnet50(weightsNone) self.model.fc nn.Linear(self.model.fc.in_features, num_classes) self.model.load_state_dict(torch.load(ckpt_path, map_locationself.device)) self.model.to(self.device).eval() # 预处理必须和训练时保持一致 self.tf transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) torch.no_grad() def predict(self, image_path, topk3): img Image.open(image_path).convert(RGB) x self.tf(img).unsqueeze(0).to(self.device) logits self.model(x) probs torch.softmax(logits, dim1)[0] top_probs, top_idx probs.topk(topk) return [(self.class_names[i], float(p)) for p, i in zip(top_probs, top_idx)]这个类用起来很直接传入图片路径就能返回最可能的几个类别和置信度。注意预处理里的Resize和CenterCrop参数必须和验证阶段完全一致否则输入分布对不上预测会不准。6.3 持续迭代的思路模型上线不是终点。实际使用中会遇到训练集里没有的花卉品种或者拍摄条件差异很大的图片模型可能给出低置信度的错误答案。我的做法是记录低置信度的预测结果定期人工审核把确认正确的新样本加入训练集重新微调模型。这样模型能随着使用不断进化覆盖更多品种和场景。另外如果发现某类花总是识别不准不要急着调模型先看看这类花的训练样本是不是太少或者质量太差。补数据往往比调参更有效。我在项目后期给几个小类别各补充了几十张高质量图片重新微调后这些类别的召回率提升了十几个百分点比任何调参手段都管用。整个识花项目做下来我最大的体会是迁移学习本身并不复杂真正花时间的是数据准备和问题排查。模型选型、冻结策略、学习率这些都有比较成熟的套路但数据里的坑千奇百怪只能一个个踩过去。如果你也在做类似的项目建议把至少一半的精力放在数据上剩下的交给迁移学习结果通常不会让你失望。