☰
基于PyTorch的花卉图像识别实战:迁移学习与ResNet18完整指南
2026/10/12 8:20:58 网站建设 项目流程

1. 项目概述与毕设定位

花卉图像识别这个题目,几乎每隔一段时间就会被翻出来问一次。原因很简单:它足够典型,有公开数据集、有清晰的任务定义、有一整套成熟的技术路线,同时带有实际应用的味道,不像MNIST那种玩具级任务,做完之后自己都不好意思写进简历。作为一个做过不少CV项目的从业者,我可以负责任地说,基于PyTorch从零跑通一个花卉识别模型,实际上用不了1小时,真正花时间的反而是数据准备、训练调参和踩坑修复。

这篇内容就按我自己的实操顺序来写,从原理、数据集、模型搭建、训练、评估到常见问题,每一步都给出可以直接照抄的代码和配置,同时把关键步骤背后的原因讲清楚。无论你是准备拿这个题目做毕业设计,还是刚接触图像识别想找个练手项目,这篇文章都值得你完整看一遍。我写代码的习惯是"能跑优先、逐步优化",所以你看到的代码不是那种精简到没法用的伪代码,而是真正能放进项目里跑起来的完整实现。

1.1 为什么选花卉识别作为CV入门项目

很多人一开始做图像分类,下意识会选择猫狗识别,因为数据好找、网上教程铺天盖地。但如果把目标定在"完成一个能写进论文或简历的项目",花卉识别其实更合适。第一个原因是类别粒度,猫狗识别本质上是二分类,算法上没什么难度,哪怕模型做得再花哨,展示效果也有限,而花卉识别通常有几十上百个类别,类别之间还存在形状色彩相似的情况,做出来的混淆矩阵、细粒度分析才有东西可写。第二个原因是数据本身,花卉图像的背景复杂度、光照变化、拍摄角度差异都很真实,遇到的是实际场景中的难点,而不是美化过的实验数据。

另外从毕设角度来说,花卉识别这个课题延展性很强。基础任务做完了,你可以向图像分割延伸做花朵区域的分割,可以加入注意力机制做细粒度识别,可以做模型压缩部署到移动端,也可以做数据增强策略对比实验。这些都是日常毕设中非常常见的扩展方向,也就是说这个题目不是死胡同,做完基础版本之后还能继续深挖,这对毕设的"工作量"输出很重要。

1.2 技术栈与方案选型的核心逻辑

这个项目中我们在深度学习框架上使用PyTorch,选它的原因很朴素:生态成熟、资料多、调试方便。PyTorch的默认动态图机制让你可以在训练循环里随意打断、打印中间张量的形状,这在排查模型结构错误时体验极好;而像TensorFlow那种静态图方案,排查起问题来就难受得多。当然,如果你对某个特定框架已经非常熟悉,使用自己的熟悉方案完全可以;但如果是从零开始的新人,我会优先推荐PyTorch。

模型结构上我们采用迁移学习方案,用ImageNet预训练的ResNet18作为骨干网络,只替换最后的全连接分类层。这个选择背后有一个重要认知:从头训练一个深度卷积网络在花卉数据集上很难达到好效果,因为你的数据量通常只有几千到几万张,而ResNet这类模型在ImageNet上已经学到了丰富的底层特征,比如边缘、纹理、颜色分布。这些特征对绝大多数图像识别任务都是通用的,你真正需要做的只是让模型学会在"花卉类别"这个维度上进行区分,也就是训练最后的分类层和微调高层的语义特征。用生活化的话来说,预训练模型相当于一个已经读过上万本书的人,你只需要花很短时间教他认花卉分类的书,而不是让他从识字开始学起。

2. 图像识别原理速通(够用版)

很多教程一上来就贴代码,虽然能跑通,但模型结构一换、数据一变就抓瞎。我建议先把原理中最核心的两块搞明白,你后面调试代码时会省很多力气。这里我不会堆公式,只讲图像识别里真正决定成败的机制。

2.1 卷积神经网络到底在做什么

图像在计算机眼里不是一张"图",而是一个三维数组,高度H、宽度W、通道数C。比如常见的RGB图片就是三个通道,每个像素点对应三个0到255之间的数值。卷积神经网络要做的事情,就是通过一系列可学习的卷积核,从这些原始像素中逐层抽取特征:浅层卷积核提取边缘、颜色块等低级特征,中间层提取纹理、形状等中级特征,深层提取"花瓣""花蕊""叶子轮廓"这类语义特征。这个过程有点像人看画,先看到线条和色块,再辨认出具体物体。

卷积操作的核心参数是卷积核大小、步长和填充。以我们项目中用的ResNet18为例,第一个卷积层用7x7的卷积核、步长为2,把输入图片从224x224缩小到112x112,这一步计算量很大但能快速降低分辨率,后续网络就用3x3卷积逐步提取更深层的特征。这里有个初学者很容易忽略的点:卷积核的数量就是输出通道数,每个卷积核负责检测一种特征,所以通道数越大的层,能表达的特征就越丰富。这也是为什么ResNet越往后的层通道数越多,从64逐渐增加到512。

2.2 从输入图片到分类结果的数据流

完整的前向传播过程可以这样理解。一张224x224的RGB图片,先做归一化,把像素值从[0,255]缩放到约[-1,1]的范围,然后送入卷积网络。经过一系列卷积、池化、残差连接之后,最终得到一个特征向量,这个向量的长度取决于最后一层卷积的输出通道数。在ResNet18里,这个特征向量经过全局平均池化后是512维。

这个512维的特征向量会送入最后一个全连接层,全连接层的输出维度等于类别数。比如数据集有102类花卉,输出就是一个102维的向量。但这个向量还不是概率,它叫logits,数值可以很大也可以为负。要得到概率分布,需要经过Softmax操作,把102个数值转换成加起来等于1的概率值。最终的预测类别就是概率最大的那个索引。训练过程的本质就是让模型输出的概率分布不断逼近真实标签的独热编码,通过反向传播更新所有卷积核的参数。

3. 数据集准备与预处理

数据是图像识别项目里最容易被低估的一环。很多同学代码写得没问题,效果差就是因为数据没处理好。这里我按自己的经验把数据准备的全流程拆开讲。

3.1 数据来源与目录组织

这里使用公开的花卉数据集,常见的有两类:一类是包含102个类别的大型花卉数据集,每个类别有几十到上百张图片,适合做毕设级别的实验;另一类是只有5到10个类别的中小型数据集,比如常见的雏菊、蒲公英、玫瑰、向日葵、郁金香五类数据集,每类几百张图片,适合快速验证和教学演示。我们这篇内容以102类的数据集为例,但你完全可以换成小数据集来跑,代码结构完全不变。

拿到数据之后,第一步不是急着写训练代码,而是把文件按如下结构整理好:

flower_data/ ├── train/ │ ├── class_001/ │ │ ├── image_001.jpg │ │ └── ... │ ├── class_002/ │ │ └── ... └── val/ ├── class_001/ │ └── ... └── class_002/ └── ...

为什么要用这种按类别分文件夹的结构?因为PyTorch的torchvision.datasets.ImageFolder就是为这种结构设计的,它会自动扫描每个子文件夹,把文件夹名当作类别标签,生成class_to_idx的映射关系。如果你自己写脚本读图片路径再手动打标签,不仅麻烦,还容易出错。我见过有人把标签顺序搞错导致整个训练白费,这种低级错误完全可以通过规范的目录结构避免。

3.2 数据增强怎么做才不过度

数据增强是在训练时对原始图片做随机变换,相当于免费扩充数据集,目的是让模型见过更多样的情况,从而提升泛化能力。但对于花卉识别这种任务,数据增强的力度需要精确把握。我们主要使用下面几种增强手段:随机水平翻转、随机旋转10度以内、随机裁剪并缩放到224x224、颜色抖动(轻微改变亮度、对比度、饱和度)。

这里有一个我自己踩过的坑:刚开始我把旋转角度设成了30度,想让模型更鲁棒,结果训练集准确率能到95%,验证集只有80%,明显的过拟合。后来意识到,花卉图片旋转超过一定角度后,会导致花瓣朝向、形状变得不自然,模型学到的是"扭曲的花"而不是正常的特征。所以增强不是越猛越好,增强的目的是模拟真实拍摄中可能出现的合理变化,而不是制造现实中不会出现的极端样本。

验证集和测试集不要去作增强操作,只需要做缩放和归一化,保证评估时使用的是最接近真实分布的图片。

3.3 DataLoader代码与参数选择

数据加载部分的代码看起来简单,但里面的参数值得逐一说清楚。下面是我们项目的标准实现:

from torchvision import datasets, transforms from torch.utils.data import DataLoader # 训练集数据变换 train_transform = transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.RandomRotation(10), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) # 验证集只做resize和归一化 val_transform = 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]) ]) train_dataset = datasets.ImageFolder('./flower_data/train', transform=train_transform) val_dataset = datasets.ImageFolder('./flower_data/val', transform=val_transform) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4, pin_memory=True) val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=4, pin_memory=True) print(f'训练集样本数: {len(train_dataset)}') print(f'验证集样本数: {len(val_dataset)}') print(f'类别数: {len(train_dataset.classes)}')

归一化用的mean和std是ImageNet数据集的统计值,因为我们要用ImageNet预训练权重,输入数据的分布需要与预训练时的输入分布一致,否则预训练特征就发挥不了作用。RandomResizedCrop默认会随机裁剪图片的一部分再缩放到224x224,这相当于模拟了物体在画面中不同大小、不同位置的情况。batch_size设为32是一个比较稳妥的起点,如果你的显存不够,可以降到16甚至8,关于这一点后面排查章节会细讲。num_workers在Windows上建议设为0,否则可能遇到多进程相关的报错。

4. 模型搭建与训练

4.1 迁移学习:站在ResNet的肩膀上

模型搭建的核心逻辑一句话讲完:加载预训练权重,替换分类头,然后分层设置学习率。具体代码如下:

import torch import torch.nn as nn import torchvision.models as models # 加载预训练ResNet18 model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) # 修改最后一层全连接 num_classes = len(train_dataset.classes) in_features = model.fc.in_features model.fc = nn.Linear(in_features, num_classes) # 将模型放到GPU device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device)

这里的models.ResNet18_Weights.IMAGENET1K_V1是新版torchvision推荐的写法,会自动下载预训练权重。如果你网络环境不好,也可以手动下载权重文件后通过load_state_dict加载。model.fc原本的输出维度是1000,因为ImageNet有1000个类别,我们把它替换成输出维度等于花卉类别数的全连接层。注意一定要保存in_features,也就是输入维度,这个值等于最后一层卷积输出的512,直接赋给新的全连接层,否则维度对不上会报错。

关于是否冻结骨干网络的问题,我建议的做法是:前几个epoch冻结骨干网络,只训练分类层;后面打开所有层进行微调。这样做的原因是,一开始就全量微调容易破坏预训练特征在浅层中的稳定性。实现方式也很简单:

# 冻结所有层 for param in model.parameters(): param.requires_grad = False # 只让最后的fc层可训练 for param in model.fc.parameters(): param.requires_grad = True

训练几个epoch之后再解冻所有层,把学习率调低一个量级继续训练,效果通常比全程冻结核骨干好2到3个百分点。这个"先冻结后解冻"的策略,是迁移学习中最实用也最容易见效的小技巧之一。

4.2 训练代码核心逻辑

训练循环本身不复杂,但要把细节写对。我这里给出一个带验证的完整训练框架,你可以直接复制修改:

import torch.optim as optim criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.001) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1) num_epochs = 20 best_acc = 0.0 for epoch in range(num_epochs): # 训练阶段 model.train() running_loss = 0.0 correct = 0 total = 0 for inputs, labels in train_loader: inputs, labels = inputs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() * inputs.size(0) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() epoch_loss = running_loss / total epoch_acc = correct / total # 验证阶段 model.eval() val_correct = 0 val_total = 0 val_loss = 0.0 with torch.no_grad(): for inputs, labels in val_loader: inputs, labels = inputs.to(device), labels.to(device) outputs = model(inputs) loss = criterion(outputs, labels) val_loss += loss.item() * inputs.size(0) _, predicted = torch.max(outputs, 1) val_total += labels.size(0) val_correct += (predicted == labels).sum().item() val_acc = val_correct / val_total val_loss = val_loss / val_total # 保存最佳模型 if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), 'best_model.pth') print(f'Epoch {epoch+1}/{num_epochs} | ' f'Train Loss: {epoch_loss:.4f} | Train Acc: {epoch_acc:.4f} | ' f'Val Acc: {val_acc:.4f}') print(f'训练完成,最佳验证准确率: {best_acc:.4f}')

model.train()和model.eval()这两个调用看起来不起眼,但非常关键。因为BatchNorm层和Dropout层在训练和推理时的行为不同,BatchNorm在训练时使用当前batch的均值和方差,在推理时使用训练阶段累计的全局统计量。如果忘了切换到eval()模式,验证结果会不稳定甚至明显偏低。我自己就曾经因为犯了这个错误,折腾了很久才排查出来。

4.3 训练超参数怎么定

超参数设置是初学者最容易困惑的地方。我这里给出一个经过验证的参考方案,并解释每个参数设置的逻辑。优化器我们用Adam,初始学习率0.001。为什么用0.001而不是0.1?因为我们是基于预训练模型微调,网络参数已经有比较好的初始位置了,太大的学习率会把参数冲得太远,破坏了已经学好的特征;0.001是一个在微调场景下非常稳妥的默认值。如果你从头训练一个模型,反而可以尝试0.01到0.1的较大学习率,但那个场景不适合我们。

batch size设为32。这个值的选择既要看显存,也要考虑BatchNorm的效果。BatchNorm在小batch上统计量不稳定,太小了影响收敛,所以如果显存允许,尽量保持32以上。但如果你的显卡只有4GB显存,32也够用,ResNet18本身不算大模型。

学习率调度我们用StepLR,每5个epoch把学习率乘以0.1,也就是从0.001降到0.0001,再降到0.00001。其实还有更现代一点的方案,比如CosineAnnealingLR,它在训练后期平滑地降低学习率,在很多情况下效果更好。但StepLR更直观,也更容易理解,作为毕设项目完全够用。数据量少的时候,20到30个epoch就能完全收敛,没必要追求过长的训练周期。

5. 训练过程监控与调优

5.1 损失曲线与准确率曲线怎么看

训练过程中的可视化监控,往往决定了你能不能及时发现问题。最直接的方式是把每个epoch的损失和准确率记录下来,画成曲线图。我用的是一个非常简单的办法,在训练循环里把数据存到列表中,训练结束后用matplotlib绘制:

import matplotlib.pyplot as plt plt.plot(train_losses, label='Train Loss') plt.plot(val_losses, label='Val Loss') plt.xlabel('Epoch') plt.ylabel('Loss') plt.legend() plt.title('Loss Curve') plt.savefig('loss_curve.png') plt.show()

读曲线的方式是有讲究的。如果训练损失持续下降但验证损失在某个epoch之后开始上升,说明模型开始过拟合了,这时应该停止训练或者增强正则化手段。如果训练损失和验证损失都降不下去,那可能是学习率不合适或者模型容量不够。如果曲线呈锯齿状剧烈波动,可能是batch size太小导致梯度估计不稳定。我见过很多同学训练完之后丢一张图在论文里就完事了,实际上那几条曲线的形态,恰恰是你写实验分析时最有价值的素材。

5.2 过拟合的几个实用对策

花卉数据集普遍不大,过拟合是大概率会遇到的问题。除了前面说的数据增强,还有三个很实用的手段。第一个是Dropout,在分类层前面加一层Dropout,以0.5的概率随机丢弃神经元,强制网络不要过度依赖某一个特征。第二个是权重衰减,也就是Adam优化器里的weight_decay参数,设为1e-4或1e-5,它对大权重施加惩罚,防止模型把某些参数学得过于极端。第三个就是前面提到的早停,当验证准确率连续多个epoch不提升甚至下降时,停止训练并恢复最佳模型参数。

其实还有一个容易被忽略的点:类别不均衡。如果某个花卉类别的图片数量明显少于其他类别,模型会倾向于把该类别的图片预测成数量多的类别。解决办法是给损失函数加类别权重,让少数类别的错误得到更大的惩罚。计算方式很简单,统计每个类别的样本数,取倒数后归一化,然后传给CrossEntropyLoss的weight参数。不过从模型效果来看,更推荐优先解决数据层面的问题,比如做简单的过采样,把样本数少的类别重复复制几份。这个手段在实际项目中经常比损失函数加权更有效。

6. 评估与推理部署

6.1 用混淆矩阵看模型的真实水平

准确率是一个很粗的指标。在102类的花卉数据集上,如果准确率到了85%,看起来好像不错,但你并不知道模型具体在哪些类别上翻车了。混淆矩阵能直观展示每一类的预测情况,帮助我们定位问题。绘制混淆矩阵的代码如下:

from sklearn.metrics import confusion_matrix import numpy as np model.eval() all_preds = [] all_labels = [] with torch.no_grad(): for inputs, labels in val_loader: inputs = inputs.to(device) outputs = model(inputs) _, preds = torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) cm = confusion_matrix(all_labels, all_preds) print(cm) # 按每个类别的准确率排序,找出最差的类别 class_acc = cm.diagonal() / cm.sum(axis=1) worst_classes = np.argsort(class_acc)[:10] for idx in worst_classes: print(f'类别 {idx} 准确率: {class_acc[idx]:.4f}')

拿到混淆矩阵后,重点看那些经常被混淆的类别对。在花卉数据集中,长相相似的品种非常多,比如不同种类的雏菊之间、玫瑰和月季之间,模型确实很难区分,这本身就是在论文里可以展开讨论的点。你可以把这个现象与细粒度识别的难点联系起来,说明这类任务为什么比普通分类更难,然后提出一个改进方向,比如引入注意力机制或者做局部特征放大。这样一做,毕设的工作量和深度就都有了。

6.2 单张图片预测与结果可视化

模型训练完之后,最直观的演示效果是输入一张没见过的花卉图片,输出预测类别和置信度。这部分代码写起来很简单:

from PIL import Image device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 加载最佳模型 model.load_state_dict(torch.load('best_model.pth', map_location=device)) model.to(device) model.eval() def predict_image(image_path): img = Image.open(image_path).convert('RGB') img_tensor = val_transform(img).unsqueeze(0).to(device) with torch.no_grad(): outputs = model(img_tensor) probs = torch.softmax(outputs, dim=1) top_probs, top_indices = torch.topk(probs, k=3) top_probs = top_probs[0].cpu().numpy() top_indices = top_indices[0].cpu().numpy() idx_to_class = {v: k for k, v in train_dataset.class_to_idx.items()} print('预测结果 Top-3:') for i in range(3): class_name = idx_to_class[top_indices[i]] print(f'{i+1}. {class_name} 置信度: {top_probs[i]:.4f}') predict_image('./test_images/sunflower_test.jpg')

这里的细节是unsqueeze(0)把单张图片变成batch维度为1的张量,因为模型期望的输入是四维的。还有idx_to_class的反向映射,因为训练时目录文件夹名被映射到了0到101的索引,预测得到索引之后需要还原成可读的类别名。Top-3输出也是一个实用的设计,因为只输出Top-1的话,遇到相似花卉类别时看起来很不友好,Top-3能展示出"模型其实认出了几个相似的候选"这个信息,演示效果更好。

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

图像识别项目跑起来之后,问题往往集中在几个固定的位置上。我这里收集了最常被问到的几类问题,每一类都给出我的排查思路和最终的解决方案。

7.1 训练不起来:损失不下降、准确率恒定

这类问题我遇到过的数量最多,表现形式有两种:一种是损失从头到尾基本不变,另一种是准确率一直保持在某个值附近波动。先说第一种,如果训练第一个epoch损失就和随机猜测差不多,然后一直不下降,首先检查数据预处理,用matplotlib或torchvision.utils.save_image把DataLoader里读出来的图片可视化一下,确认图片内容正确、归一化设置合理。有一个经典错误是忘了transforms.ToTensor(),导致图片以0到255的整数张量直接送进模型,模型根本学不到有效梯度。

第二种情况,准确率恒定在你预期的一个固定值附近,通常是类别不均衡或者模型输出全部偏向某一类。打印一下预测结果的分布,看模型是不是把所有图片都预测成了样本量最大的类别。这类问题一般靠类别权重或者重新划分数据集来解决。还有一个小概率但是很气人的情况:数据集的标签顺序和模型输出类别顺序对不上,训练时标签做了shuffle,验证时却没做,或者某个分类文件夹的排序在不同环境不一致,导致模型学的类别和验证用的类别完全错位。遇到诡异问题时,先把数据加载流程打出来核对一遍,往往能快速定位。

7.2 显存不足怎么办

显存不足是入门阶段最容易碰到的硬性问题。如果你的GPU显存只有4GB甚至更低,跑ResNet18加batch size 32确实会报CUDA out of memory。我给出几条按优先级排序的解决路径。第一条是把batch size降到16或者8,代价是训练稳定性会稍微下降,但基本能解决问题。第二条是换用更轻量的模型,比如MobileNetV3或SqueezeNet,这类模型在保持不错准确率的同时,参数量和显存占用明显低于ResNet,训练起来压力小得多。第三条是开启梯度累积,每攒够几个batch的梯度再更新一次参数,这样可以模拟大batch的效果又不会爆显存。

还有两个容易被忽视的技巧:一个是在不再需要梯度的时候用torch.no_grad()包住验证代码,避免验证阶段也占用了大量显存;另一个是不要在一个脚本里同时加载训练集和验证集的多个副本,用完及时释放引用。我见过有人把所有数据集都放在内存里再创建DataLoader,导致内存和显存双重吃紧,其实ImageFolder配合num_workers流式读取已经足够高效,没必要做这种极端的做法。

7.3 验证准确率上不去

如果训练准确率很高但验证准确率明显偏低,优先从过拟合角度排查,数据增强力度是否够、权重衰减是否设置、早停是否触发。如果训练和验证准确率都很低,则要检查模型和数据是否匹配,包括图片分辨率是不是太小、类别数是否远大于数据集的可区分度。

我这里还想特别强调一个重要观点:在基线模型中,如果验证准确率能达到70%到80%,对于102类的花卉数据集来说已经是合理水平,不用盲目追求99%。网上很多教程截图显示98%、99%的准确率,仔细看往往会发现类别数只有5类或者数据划分有问题。你在毕设里写论文时,真诚地展示你的方法比基准高了几个百分点,并分析错误案例,远比虚高的准确率更有说服力。这也是我反复告诉自己、也告诉身边做项目的人的一句话:准确率只是结果,分析过程和找到问题才是能力。

7.4 训练速度太慢怎么加速

如果你只有CPU没有GPU,训练ResNet18在102类花卉数据集上,20个epoch可能要跑数小时。这时可以按顺序尝试这几个方案:把图片尺寸从224降到160或128,训练会快很多但准确率会小幅下降;换用MobileNetV3 Small这类轻量模型,训练速度比ResNet18快数倍;减少训练轮数配合早停;在Colab这类免费的云端GPU环境上运行。这里面我觉得云端GPU是性价比最高的选择,很多云平台都提供免费的GPU额度,足够跑完这个小项目。

另外需要注意一个反直觉的事情:num_workers设得太大不一定会加速,在小数据集上反而可能因为进程调度开销让速度变慢。数据加载通常不会成为GPU训练瓶颈时,把num_workers设为2到4就够了。如果数据读取确实很慢,可以先用torch.utils.data.DataLoader的prefetch_factor参数做预取,或者把图片预先用torchvision.datasets.ImageFolder的缓存机制处理,但大多数情况不需要这么复杂。

8. 实操心得:一些文档里不会写的小经验

最后分享几个完全来自个人实操的经验,算不上什么高深理论,但确实能在关键时刻帮你节省大量时间。

第一点,训练脚本一定要在最开始设置随机种子。PyTorch和NumPy的随机种子分别设置,确保每次运行结果可复现。毕设论文里最好写上你用了什么随机种子,不然别人复现你的实验时会发现结果差异很大。虽然复现性在深度学习里本来就是相对的,但至少同一个环境下跑两次要得到相同的结果。

import random import numpy as np def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) set_seed(42)

第二点,日志记录比打印更重要。训练时不要只print到终端,建议用tensorboard或者直接把日志写入文件,包含时间戳、epoch、学习率、损失、准确率等信息。到了写论文阶段,你会发现这些记录就是实验对比表的原始素材,如果当时只打印在终端里,终端一关数据就再也找不回来了。我自己吃过这个亏,重训了好几次模型才补齐了论文里需要的全部实验记录。

第三点,关于部署的小建议是:不要一开始就想着做Web服务或App,先把模型跑通、评估做好,再考虑用FastAPI封装一个简单的HTTP接口。其实封装本身也很简单,就是一个接收图片、返回类别和置信度的函数。但作为毕设来说,功能展示的意义大于工程实现,简洁清晰的演示界面,配合解释技术原理,远胜于复杂的系统架构。

如果你做完基础版本之后还有余力,我建议把注意力放在细粒度分类这个方向上。比如在模型中加入一个注意力模块,让网络更关注花卉的花蕊和花瓣边缘区域,这类改进在论文里很好写,效果也容易通过热力图可视化来展示。或者你可以把训练好的模型导出成ONNX格式,跑一次推理速度对比实验,这也是毕设中很受欢迎的加分内容。核心思路是别停留在"能跑"阶段,多做一个维度的对比分析,你的项目深度会立刻不一样。

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

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

立即咨询