☰
迁移学习实战:从零构建高精度花卉识别模型
2026/10/17 7:46:20 网站建设 项目流程

1. 迁移学习识花实践:从零到一构建一个能认花的模型

1.1 为什么选择迁移学习来做识花

做图像分类的人,十有八九第一个真正想做的项目就是“认花”。原因很简单:花的数据集好找,类别多,视觉差异明显,做出来还能直接拿手机拍一张就验证效果,成就感来得快。但真动手之后,第一个拦路虎就出现了——数据量不够。公开的花卉数据集,比如常见的几种,通常每类只有几十到几百张,总共加起来也就几千到上万张。这个量级,如果从零开始训练一个卷积神经网络,结果基本就是过拟合到亲妈都不认识:训练集准确率冲到99%,验证集死活上不去,测试集更是惨不忍睹。

迁移学习的思路就是来解决这个问题的。它的核心逻辑特别像“借鸡生蛋”:我先在一个超大规模的数据集上(比如ImageNet,一千多类、上百万张图)训练一个网络,这个网络已经学会了怎么提取边缘、纹理、形状、局部部件这些通用视觉特征。然后我把这个网络搬到花卉分类任务上,只替换最后的分类层,用花卉数据去微调。这样,网络不需要从零学“什么是花瓣边缘”,只需要学“这种花瓣边缘对应的是玫瑰还是郁金香”。训练时间从几天缩短到几十分钟,数据需求从几万张降到几百张,准确率还能轻松上90%。

我试过在同样的花卉数据上,从零训练一个ResNet18,验证集准确率卡在60%左右就上不去了;换成迁移学习,同样的网络结构,只训练最后几层,二十分钟不到,验证集就到92%了。这个差距,就是迁移学习在中小规模图像任务上的价值。

1.2 这个实践适合谁看,能解决什么问题

这篇内容面向的是有一定Python基础、了解PyTorch或TensorFlow基本用法、想动手做一个完整图像分类项目的读者。你不需要有深度学习调参经验,也不需要显卡集群,一台带普通显卡的笔记本,甚至用Colab的免费GPU,就能完整复现。

具体来说,我会把整个流程拆成几个关键环节:数据准备与增强、模型选型与改造、训练策略设计、结果评估与调优、以及部署推理的轻量化处理。每个环节都会解释“为什么这么做”,而不是只给代码。比如,为什么用预训练模型时要冻结前面的层?为什么学习率要设得比正常训练小?数据增强里哪些操作对花卉分类特别有效?这些细节,才是决定你最后模型能不能用的关键。

另外,我会特别分享几个踩过的坑:比如类别不平衡怎么处理、验证集准确率震荡怎么办、模型在手机拍的照片上表现差怎么排查。这些经验,常规教程里很少讲,但实际做项目时一定会遇到。

2. 数据准备与增强:识花任务的地基

2.1 花卉数据集的获取与整理

做识花,数据是第一步。公开的花卉数据集有几个选择,比如Oxford Flower系列,包含102类,每类40到258张不等,图片是自然场景下的花,背景复杂,光照多样,很接近真实拍照场景。另一个常用的是TensorFlow Datasets里的花卉数据集,类别少一些,但图片质量整齐。我建议用Oxford Flower 102,虽然难一点,但做出来更接近实用。

下载下来之后,目录结构通常是按类别分文件夹,每个文件夹里是图片。但这里有个坑:有些数据集的划分是固定的训练集、验证集、测试集,有些只给了全部图片和标签文件。我习惯自己重新划分,按8:1:1的比例,用随机种子固定,保证每次实验可复现。划分的时候要注意,同一类别的图片要均匀分布到三个集合里,不能出现某个类别只在训练集有、验证集没有的情况。

整理完的数据目录长这样:

flowers/ train/ class_0/ img_001.jpg ... class_1/ ... val/ class_0/ ... test/ class_0/ ...

这个结构可以直接被PyTorch的ImageFolder读取,省去自己写Dataset类的麻烦。如果你用的是TensorFlow,image_dataset_from_directory也支持同样的结构。

2.2 数据增强:让少量数据发挥更大价值

花卉数据每类只有几十张,不做增强,模型很快就能“背下来”。数据增强的本质是人工制造多样性,让模型看到同一朵花在不同角度、不同光照、不同尺度下的样子,从而学到更鲁棒的特征。

对花卉分类特别有效的增强操作有这么几个:

  • 随机水平翻转:花在自然场景中左右对称性不强,但翻转不会改变类别,能增加一倍数据量。
  • 随机旋转:±30度以内,模拟拍摄角度变化。注意不要旋转太大,否则花的方向完全颠倒,可能引入噪声。
  • 颜色抖动:调整亮度、对比度、饱和度、色调。花卉的颜色是重要特征,但不同光照下颜色差异很大,颜色抖动能让模型不过度依赖绝对颜色值。
  • 随机裁剪与缩放:先随机裁剪到原图的80%到100%,再缩放到固定尺寸(比如224×224)。这能模拟不同距离拍摄的效果。
  • 随机擦除:随机遮挡图片的一小块区域,强迫模型不要只依赖某个局部特征,比如只看到花蕊就判断类别。

我实测下来,组合使用翻转、旋转、颜色抖动和随机裁剪,验证集准确率能比不做增强提升8到10个百分点。但要注意,不要用垂直翻转,因为花在自然状态下很少上下颠倒,垂直翻转可能让模型学到不存在的模式。

代码上,用torchvision.transforms可以很方便地组合:

from torchvision import transforms train_transform = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.8, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(30), transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.3, hue=0.1), transforms.RandomErasing(p=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])

验证集和测试集只做缩放和归一化,不做随机增强,保证评估结果稳定。

注意:归一化用的均值和标准差是ImageNet的统计值,因为预训练模型是在ImageNet上训练的,必须保持一致。如果你自己从零训练,可以重新计算数据集的均值和标准差,但迁移学习场景下,直接用ImageNet的值就行。

2.3 类别不平衡的处理

花卉数据集里,有些类别图片多,有些少,这是常态。比如Oxford Flower 102里,最多的类有258张,最少的只有40张。如果不处理,模型会偏向样本多的类,少数类准确率很低。

处理方式有几种:一是对少数类做过采样,训练时重复采样,让每个batch里各类别比例均衡;二是用加权损失函数,给少数类更高的权重;三是数据增强时对少数类用更强的增强。我一般用第一种,配合WeightedRandomSampler,简单有效。

from torch.utils.data import WeightedRandomSampler class_counts = [len(os.listdir(os.path.join(train_dir, c))) for c in classes] weights = 1.0 / torch.tensor(class_counts, dtype=torch.float) sample_weights = weights[train_dataset.targets] sampler = WeightedRandomSampler(sample_weights, num_samples=len(sample_weights), replacement=True)

这样每个epoch里,模型看到的各类别样本数大致相等,少数类不会被忽略。

3. 模型选型与改造:借力预训练网络

3.1 选哪个预训练模型

预训练模型的选择,主要看三个因素:准确率、推理速度、模型大小。对于花卉分类这种细粒度任务,我推荐从这几个里选:

模型参数量ImageNet Top-1推理速度适用场景
ResNet1811M69.8%快快速实验,移动端部署
ResNet5025M76.1%中等精度与速度平衡
EfficientNet-B05.3M77.1%快移动端,高精度
ViT-B/1686M81.8%慢追求最高精度,服务器部署

我一般先用ResNet18快速跑通流程,确认数据管道没问题,再换ResNet50或EfficientNet-B0提升精度。ViT虽然精度高,但对数据量要求更大,花卉这种小数据集上,如果不做大量增强,可能还不如ResNet。

选模型时还要注意输入尺寸。ResNet系列默认224×224,EfficientNet-B0也是224,ViT通常是224或384。保持一致就行,不用改。

3.2 怎么改造预训练模型

改造的核心是替换最后的全连接层。以ResNet18为例,原始模型最后是fc层,输出1000类(ImageNet的类别数)。我们要把它换成输出102类(花卉类别数)。

import torchvision.models as models import torch.nn as nn model = models.resnet18(pretrained=True) num_features = model.fc.in_features model.fc = nn.Linear(num_features, 102)

这里有个细节:新换的fc层参数是随机初始化的,而前面的卷积层是预训练好的。如果直接训练,随机初始化的fc层会产生较大的梯度,可能破坏前面预训练好的特征。所以通常的做法是,先冻结前面的层,只训练fc层几个epoch,等fc层稳定了,再解冻部分层做微调。

冻结的代码:

for param in model.parameters(): param.requires_grad = False for param in model.fc.parameters(): param.requires_grad = True

训练几个epoch后,再解冻最后几个卷积块:

for param in model.layer4.parameters(): param.requires_grad = True

这样逐步解冻,能让模型平滑地适应新任务,避免灾难性遗忘。

3.3 学习率设置:迁移学习的关键参数

迁移学习的学习率设置,和从零训练完全不同。从零训练时,学习率通常设0.1或0.01;迁移学习时,预训练部分的学习率要小得多,因为那些参数已经很好,只需要微调。我一般这样设:

  • 新加的fc层:学习率0.001到0.01
  • 解冻的预训练层:学习率0.0001到0.001

用PyTorch的param_groups可以实现不同层不同学习率:

optimizer = torch.optim.SGD([ {'params': model.fc.parameters(), 'lr': 0.01}, {'params': model.layer4.parameters(), 'lr': 0.001} ], momentum=0.9, weight_decay=1e-4)

如果嫌麻烦,也可以用Adam,它对学习率不那么敏感,但最终精度可能略低于精心调参的SGD。我一般先用Adam快速实验,确定模型结构没问题,再用SGD精调。

实操心得:学习率设大了,预训练特征会被破坏,验证集准确率会先升后降,降得很厉害;设小了,收敛慢,但最终精度更高。如果不确定,就从0.001开始试,观察前几个epoch的loss变化,如果loss震荡,就调小。

4. 训练策略与实操流程

4.1 完整训练流程拆解

整个训练流程我分成三个阶段:预热阶段、微调阶段、精调阶段。

预热阶段:只训练新加的fc层,预训练层全部冻结。学习率设0.01,训练3到5个epoch。这个阶段模型快速学会把预训练特征映射到花卉类别上,loss下降很快。

微调阶段:解冻最后一个卷积块(比如ResNet的layer4),学习率降到0.001,训练10到15个epoch。这个阶段模型开始调整高层语义特征,适应花卉的细粒度差异。

精调阶段:解冻所有层,学习率降到0.0001,训练5到10个epoch。这个阶段模型在整个网络上做细微调整,通常能再提升1到2个百分点。但如果数据量特别小(每类少于50张),这个阶段容易过拟合,可以跳过。

每个阶段都监控验证集准确率,如果连续3个epoch不提升,就降低学习率或提前停止。

4.2 训练循环与关键代码

训练循环本身不复杂,但有几个细节要注意:用model.train()和model.eval()切换模式,验证时用torch.no_grad()关闭梯度计算,保存验证集准确率最高的模型。

def train_one_epoch(model, loader, optimizer, criterion, device): model.train() running_loss = 0.0 correct = 0 total = 0 for images, labels in loader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() * images.size(0) _, predicted = outputs.max(1) total += labels.size(0) correct += predicted.eq(labels).sum().item() return running_loss / total, correct / total def validate(model, loader, criterion, device): model.eval() running_loss = 0.0 correct = 0 total = 0 with torch.no_grad(): for images, labels in loader: images, labels = images.to(device), labels.to(device) outputs = model(images) loss = criterion(outputs, labels) running_loss += loss.item() * images.size(0) _, predicted = outputs.max(1) total += labels.size(0) correct += predicted.eq(labels).sum().item() return running_loss / total, correct / total

训练时用ReduceLROnPlateau调度器,验证集loss不降就自动降学习率:

scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=3)

4.3 训练过程监控与早停

训练过程中,我习惯每个epoch打印训练loss、训练准确率、验证loss、验证准确率。如果验证loss开始上升,而训练loss还在下降,就是过拟合的信号,该早停了。

早停的实现很简单:记录最佳验证准确率,如果连续N个epoch没超过最佳值,就停止训练,并加载最佳模型权重。

best_acc = 0.0 patience = 7 counter = 0 for epoch in range(num_epochs): train_loss, train_acc = train_one_epoch(...) val_loss, val_acc = validate(...) if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), 'best_model.pth') counter = 0 else: counter += 1 if counter >= patience: print(f'Early stopping at epoch {epoch}') break

我实测下来,ResNet18在Oxford Flower 102上,通常15到20个epoch就能达到最佳验证准确率,再训练就是浪费时间。

5. 常见问题与排查技巧实录

5.1 验证集准确率震荡怎么办

验证集准确率震荡是常见问题,原因可能有几个:学习率太大、batch size太小、数据增强太强、验证集太小。

排查顺序:先把学习率降一半,看震荡是否减小;如果还震荡,把batch size从32加到64或128;如果还不行,检查验证集是不是只有几十张图,样本太少本身就会导致准确率波动大。这时候可以增大验证集比例,或者用交叉验证。

我遇到过一次,验证集准确率在85%到92%之间来回跳,最后发现是RandomErasing的概率设太高(0.5),模型有时候看到的是被遮挡严重的图,预测不稳定。把概率降到0.2就好了。

5.2 模型在手机拍的照片上表现差

训练时用的是数据集里的图,背景、光照、角度都有一定规律。手机拍的照片,背景杂乱、光照不均、角度随意,分布和训练集差异大,模型表现下降很正常。

解决办法:一是在数据增强里加入更接近真实拍照的变换,比如更大的随机裁剪、更强的颜色抖动、随机模糊;二是收集一些手机拍的花卉图,人工标注后加入训练集;三是用测试时增强,对同一张测试图做多次增强,取平均预测结果。

我试过测试时增强,对手机拍的照片,准确率能提升5到8个百分点。具体做法是对每张测试图做5次不同的裁剪和翻转,分别预测,然后对softmax输出取平均。

5.3 常见问题速查表

问题可能原因解决方法
训练loss不下降学习率太小、模型没解冻、数据标签错调大学习率、检查requires_grad、抽查标签
验证loss上升过拟合增强数据、加dropout、早停、减小模型
验证准确率远低于训练过拟合或数据分布不一致检查验证集划分、增强、正则化
某些类别准确率极低类别样本太少或类间相似度高过采样、加权损失、细粒度特征
训练速度慢batch size小、数据加载瓶颈增大batch、用num_workers、预加载到内存
显存不足模型太大、batch太大换小模型、减小batch、用梯度累积

独家避坑:数据加载是最大的性能瓶颈。用DataLoader时,把num_workers设成CPU核心数,pin_memory=True,能显著加快训练。如果数据集不大,直接全部加载到内存,比从磁盘读快得多。

6. 模型部署与轻量化

6.1 导出ONNX模型

训练完的PyTorch模型,要部署到不同平台,最好导出成ONNX格式。ONNX是跨框架的模型表示,支持多种推理引擎。

dummy_input = torch.randn(1, 3, 224, 224).to(device) torch.onnx.export(model, dummy_input, 'flower_model.onnx', input_names=['input'], output_names=['output'], dynamic_axes={'input': {0: 'batch_size'}, 'output': {0: 'batch_size'}})

导出后可以用onnxruntime推理,速度比原生PyTorch快不少,尤其是在CPU上。

6.2 模型量化与剪枝

如果要在移动端部署,模型大小和推理速度很关键。量化是把浮点参数转成整数,模型大小缩小4倍,推理速度提升2到3倍,精度损失通常不到1%。

import torch.quantization model.eval() model.qconfig = torch.quantization.get_default_qconfig('fbgemm') model_prepared = torch.quantization.prepare(model) # 用校准数据跑一遍 for images, _ in calib_loader: model_prepared(images) model_quantized = torch.quantization.convert(model_prepared)

剪枝是去掉不重要的连接,进一步压缩模型。但剪枝后需要微调恢复精度,流程复杂一些。如果只是做Demo,量化就够了。

6.3 推理接口封装

最后把模型封装成一个简单的推理函数,输入图片路径,输出类别和置信度:

def predict(image_path, model, transform, classes, device): image = Image.open(image_path).convert('RGB') image = transform(image).unsqueeze(0).to(device) model.eval() with torch.no_grad(): outputs = model(image) probabilities = torch.softmax(outputs, dim=1) confidence, predicted = probabilities.max(1) return classes[predicted.item()], confidence.item()

这个函数可以直接集成到Web服务或移动端App里。如果要做成手机App,可以用ONNX Runtime Mobile或TensorFlow Lite,把模型转过去就行。

我个人在实际操作中的体会是,迁移学习识花这个项目,最难的不是模型结构,而是数据管道的搭建和训练策略的设计。把数据增强做扎实,学习率调对,模型选型不要太激进,基本都能拿到不错的结果。另外,别一上来就追求SOTA,先用ResNet18跑通全流程,再逐步换模型、加技巧,这样每一步都有反馈,出了问题也好定位。最后再分享一个小技巧:训练时把验证集准确率最高的模型保存下来,而不是最后一个epoch的模型,因为最后一个epoch往往已经过拟合了。这个细节,能让你的最终精度提升2到3个百分点。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询