
每年都有人把深度学习毕设做成“调包炖蘑菇”把公开数据集往预训练模型里一塞训练几轮就交差题目和结果两张皮。今天我把“基于CNN识别花卉是否绽放”这个毕设从立项到答辩的完整思路捋一遍重点不在跑通代码而在每一步怎么自圆其说怎么把“是什么”“为什么”“怎么调”讲明白。这套玩法换个课题也能直接移植。1. 项目本质拆解你以为在分类其实在拟合“绽放程度”先把题目解剖清楚。“识别花卉是否绽放”是个图像二分类问题输出是“绽放”或“未绽放”。但如果你把它当成普通的猫狗分类去做大概率会挂在数据上——因为“是否绽放”不是物种级的视觉差异而是同一种花在不同生长阶段的表型差异。花苞和半开、盛开的区别往往就是花瓣张开角度、花蕊可见性、颜色饱和度这些细微变化。这一点直接决定了后续所有选择不能用现成的花卉品种分类数据集直接训练因为那些数据标的是“这是什么花”不是“这朵花开没开”。模型需要更关注局部纹理和形状比例所以卷积核的感受野设计、注意力机制的引入都有讨论空间。评价指标不能只看准确率因为如果数据里“未绽放”占多数模型全猜“未绽放”也能有好看的数字必须引入精确率、召回率、F1甚至画混淆矩阵。我当时和朋友聊这个题他第一反应是“这有什么难的分类嘛”。我反问了他三个问题花苞算不算未绽放半开的算不算凋谢的算不算如果这三个问题定义不清楚标注的人会疯掉模型也会学到错误边界。所以项目的第一步不是敲代码而是写标注规范。建议把所有图片分成三类而不是两类未绽放花苞、闭合状态、绽放中花萼已裂、花瓣可见但未全开、完全绽放花瓣充分展开、花蕊暴露。训练时把后两类合并成“绽放”但标注阶段保留三分类标签这样既能处理边界情况又能为后续做细粒度分类留余地。2. 数据集的构建与预处理2.1 公开数据集的可用性筛查很多同学会直接去下载“花卉数据集”但我筛查了一圈常见公开数据集的标注都是品种级别包括某高校开源的花卉数据集、国际上知名的牛津花数据集里面没有“绽放状态”标签。所以只能自己造数据。造数据有两条路基于品种数据集二次标注下载一批花卉图片自己人工筛选绽放状态写个小脚本重新生成标签文件。优点是图片质量高、物种多样缺点是费眼睛且原始数据集里的花苞图往往很少正负样本会不平衡。爬虫采集按“含苞待放”“花苞特写”“盛开的花”等关键词爬公开图片。优点是能控制样本分布缺点是要花很多时间清洗错误样本。我推荐混合方案主体用公开花卉数据集做二次标注再用爬虫补充花苞和半开状态的图片最后统一用脚本清洗大小、压缩格式和亮度。2.2 清洗与标注的实操细节清洗环节有个容易忽视的坑图片文件名和标签对不上。建议一律改成统一命名规则比如“Tulip_12.jpg”配上同一个CSV的id字段不要用中文文件名也不要让标签存在文件名里除非你想在读取数据时被编码折磨。标注规范要写成文档至少包含三条标准花苞状态花瓣未张开外层萼片包裹明显。绽放中花瓣开始展开但整体轮廓仍呈杯状或半球形花蕊不可见或部分可见。完全绽放花瓣展平或外翻花蕊充分暴露雄蕊可见。标注的时候最好两个人背靠背各标一遍再取交集遇到不一致的单独处理。这个“背靠背标注”的流程在开题报告和论文里都能写出方法论价值导师看了会加分。2.3 数据增强要“适度”花卉识别比较容易过拟合因为背景、光照、拍摄角度的影响经常比花本身的状态还大。数据增强我常用的组合是随机水平翻转、随机旋转10到15度、随机亮度饱和度抖动、随机裁剪后缩放回原尺寸。有一点要提醒不要用太激进的增强比如大角度旋转和上下翻转。玫瑰花倒过来就不像花了模型会把上下文背景当成特征反而降低泛化能力。随机裁剪裁剪范围也不要超过原图的15%到20%否则会切掉整个花朵。另外建议在训练集之外单独留出验证集保证验证集里同一朵花的不同角度图片不要同时出现在训练集和验证集。要按“花”去分数据不要按“图片”去分。实际写代码的时候你得先按文件夹或文件名前缀聚合出唯一的“花”再划分train/val/test否则同一朵花的不同照片会泄漏进验证集指标虚高答辩时一问就露馅。3. CNN模型设计从零训练还是迁移学习3.1 基础CNN的结构逻辑先看一眼基础版结构我给出PyTorch风格的代码但没装PyTorch环境也看得懂核心是理解每一层的动机import torch.nn as nn class FlowerBlossomNet(nn.Module): def __init__(self): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 16, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.BatchNorm2d(16), nn.MaxPool2d(2), nn.Conv2d(16, 32, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.BatchNorm2d(32), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.BatchNorm2d(64), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.BatchNorm2d(128), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(), nn.Linear(128, 64), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(64, 2) ) def forward(self, x): x self.features(x) x self.classifier(x) return x几个关键决策点输入统一为224×224的RGB图。这个尺寸不是拍脑袋定的而是综合了计算开销和细节保留能力。输入越小训练越快但花瓣纹理容易丢失输入越大显存压力大训练时间成倍增长。224是多种预训练模型的标准输入尺寸如果你将来打算迁移学习可以直接复用这套预处理流程。BatchNorm放在每个卷积后的ReLU之前能显著减少“梯度消失”和“内部协变量偏移”的影响。理解上你可以把BatchNorm理解为每一层输出的数据分布被拉回标准范围下一层不用费力去适应上一层输出范围的变化。全连接层之前用AdaptiveAvgPool2d把特征图压成1×1无论输入尺寸怎么变全连接层的输入维度都固定。这比Flatten后接尺寸固定的全连接层要灵活很多也降低了过拟合风险。3.2 为什么要跟迁移学习做对比毕业论文如果想有“工作量”最好不要只用自己写的小网络。我的建议是至少跑三组对照自定义浅层CNN上述结构。微调预训练的ResNet18或MobileNetV2替换最后的全连接层冻结前若干层只微调后面几个stage。线性探测只替换分类头冻结全部卷积层作为下限参照。为什么因为从零训练的小网络在少量数据上往往表现不稳而迁移学习大概率会涨点。如果你的自定义CNN效果好于迁移学习这是一个很有说服力的结论如果略低于迁移学习也很正常你能在论文里解释清楚“数据量不足时预训练特征的优势”引用现有研究支撑即可。迁移学习代码片段PyTorch写法示意import torchvision.models as models def build_transfer_model(num_classes2, freeze_ratio0.5): model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) # 冻结前面的层 modules list(model.children()) freeze_num int(len(modules) * freeze_ratio) for layer in modules[:freeze_num]: for param in layer.parameters(): param.requires_grad False # 替换分类头 model.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(model.fc.in_features, num_classes) ) return model冻结比例值得聊一下。如果数据量大你可以少冻结一些如果数据量只有几千张建议冻结一半以上避免底层卷积被少量数据带偏。这里有个经验值数据量小于5000张时Bottom layers全冻结、只微调最后两个Block和全连接层成功率最高。3.3 类别不均衡与Loss函数设计假设你最后的花苞图只有盛开图的五分之一直接上交叉熵模型会严重偏向多数类。常规做法是给loss加上class weightimport torch # class_weight [0.5, 1.0] 表示“未绽放”权重小“绽放”权重大 loss_fn nn.CrossEntropyLoss(weighttorch.tensor([0.5, 1.0]).cuda())权重的设定逻辑很简单少数类样本少导致它对梯度的贡献小给它一个更大的权重让模型每次更新时更“在意”少数类的分类错误。具体值根据“多数类样本数/少数类样本数”折算比如比例是3:1那权重可以取1和3。注意如果权重给太大模型会严重误杀多数类建议在1到3倍之间尝试。4. 训练过程的完整实操4.1 超参数怎么定我的习惯是先固定一组基线再单独去调最敏感的参数。基线设置如下优化器Adam初始学习率1e-3beta10.9beta20.999。批量大小16在224×224下16能让大多数消费级显卡跑得动也保留了梯度更新的随机性。训练轮数50轮配合早停机制连续10轮验证集F1不上升就停止。学习率衰减每20轮乘以0.1。为什么不用SGD对于毕设这种规模Adam收敛快且对学习率不那么敏感省去大量调参时间。但Adam有个特性它会把历史梯度的二阶矩累积得很大后期的更新步长偏小所以当你发现验证集指标“卡住”不再上升时往往不是模型不行而是学习率需要降下来了。4.2 训练循环里的三个关键细节第一个细节是训练与验证模式切换。模型里如果用了Dropout和BatchNorm在验证阶段必须切到eval模式否则BatchNorm依然按训练模式统计批内均值和方差验证结果会乱掉带来莫名其妙的波动。model.train() # 训练阶段 ... model.eval() # 验证/测试阶段 with torch.no_grad(): outputs model(images)第二个细节是保存模型时一定要连 optimizer 的 state_dict 一起存。不然恢复训练时优化器的动量信息全部丢失模型的收敛轨迹会断掉。最简单的做法是只存model.state_dict()用于推理但如果你想中途断训续跑建议存完整checkpointcheckpoint { epoch: epoch, model_state: model.state_dict(), optimizer_state: optimizer.state_dict(), best_f1: best_f1, } torch.save(checkpoint, checkpoint_v1.pt)第三个细节是早停的判断标准。我建议监控验证集的F1指标而不是loss因为交叉熵的浮动可能迷惑人F1能更好地反映正类绽放的识别率。把patience设成10也就是连续10个epoch验证F1没有创新高就停止训练然后用best_f1对应的模型权重做测试。4.3 可视化与日志训练过程要记录四个东西train_loss、val_loss、train_f1、val_f1。用TensorBoard或每个epoch打印一行都行但必须保留完整记录。答辩时老师问“你怎么判断模型是否过拟合”你直接甩出两张曲线图训练损失持续下降、验证损失在某个epoch后反弹这就叫过拟合。如果没有日志你只能在台上支支吾吾。我习惯在验证阶段额外生成一组预测正确与错误的可视化图片一张错误样本里同时显示真实标签、预测标签、置信度。这能帮助快速定位模型是“把花苞认成绽放”还是“把半开认成未绽放”。如果是前者大概率是标注规范问题如果是后者可能是训练数据里半开样本太少。5. 实验结果的呈现与调优技巧5.1 混淆矩阵是答辩利器不要只报告“准确率94%”这一点。多分类任务的答辩核心是“谁和谁混淆了”。画一个2×2的混淆矩阵你会看清模型真正的短板。比如未绽放样本的召回率低意味着很多花苞被漏判成绽放这对农业生产里的花苞采摘提醒场景是不可接受的而绽放样本的精确率低意味着用户拿一张未开花图片问系统系统容易误报。答辩话术可以这么组织准确率反映总体水平但两个类别的recall差距较大因此后续将采集更多花苞阶段的样本并考虑在Loss上加权。这是非常自然的改进方向。5.2 调优顺序很多人的调参顺序是乱的一会儿改学习率一会儿改网络层数结果模型崩了也分不清是哪步造成的。我的建议是先跑通基线确认数据管道没问题。这一步卡很久往往是数据集路径或标签错位问题代码报错不可怕怕的是不报错但loss不下降。再检查过拟合程度。如果训练F1接近1、验证F1明显低就用数据增强、Dropout和L2正则如果两个都低那是模型容量或数据量的问题优先增强数据或增大网络。等模型“靠谱”了再动学习率和batch size。这两个参数决定你最终能收敛到什么水平。最后才尝试调整输入分辨率和预处理策略。有一点容易被忽略每次只改一个变量。这个变量改完了跑一轮记下结果再改下一个。如果你同时改了网络结构和学习率后面分析实验结果时根本说不清是谁带来的提升审稿人和答辩老师都不会满意。5.3 关于“准确率天花板”花卉绽放识别这个任务准确率到一定程度后会卡住卡住的水平大约在90%到95%之间。为什么因为花朵的绽放状态本身存在灰度地带有些花半开的状态和全开从视觉上差异极小连人眼都判断不稳机器当然也很难。你的毕设不需要做到100%但你需要可视化那些“模型判断错误但人也模棱两可”的样例证明任务本身的挑战性。这一手在答辩时非常有用能够把“错误”转化为“任务边界分析”。6. 常见问题与排查技巧实录6.1 loss不下降遇到loss几乎不变化先别怀疑模型结构查以下顺序标签是否错位。打印几个batch的图片和对应标签人眼看一遍。是否真的在更新参数。观察模型中某一层的权重值在一轮训练后是否发生变化。输入图片是否归一化正确。如果像素值都是0到255而模型期望0到1初始梯度就可能是异常大或异常小导致训练不稳定。初始化是否正常。如果用了不合适的初始化深层激活值会直接饱和梯度消失。我遇到过最诡异的情况是模型在CPU上正常上GPU之后loss是nan。排查半天发现是某张损坏的图片含有inf像素值卷积一算就爆了。解决办法是在数据加载后加一个数值剪切把像素值限幅到合法区间。6.2 GPU显存不足224×224的输入batch size从16起手显存不够就逐次减半到8、4。如果4都跑不了就把输入尺寸缩到160×160。注意改输入尺寸后原模型如果有预训练它的统计信息是基于224的可能在特征图上产生偏差迁移学习场景要慎重。更好的做法是开启混合精度训练。PyTorch里可以用自动混合精度包装训练步骤显存立刻省下百分之三四十速度还能提升。毕设阶段不需要你推导混合精度的完整原理只需在论文的“实验环境”一节说明采用了半精度浮点加速训练即可。6.3 训练时准确率高验证时很低这是典型的过拟合信号要么数据量太小要么增强不够。我的排查清单是先看训练集图片数量。单类少于500张很容易过拟合。增加DataLoader里的NumWorkers会不会有影响不会它只影响吞吐不影响模型泛化。Dropout比例从0.2加到0.5看验证F1是否回升。加上LabelSmoothing把硬标签松弛一下。这个技巧对二分类很有效能让模型对“边界样本”更温和。LabelSmoothing的代码不该是突然冒出来的它背后对应一个洞察花卉绽放边界模糊硬标签0和1过于绝对模型被迫用无限大的logit输出去拟合这个绝对性会导致泛化下降。改用0.1的平滑后模型不再追求输出完美的0或1概率泛化更好。这个理由写进论文里是加分的。6.4 光源和背景干扰花卉识别和一般物体识别最大的不同是背景经常比主体还醒目。不少模型实际上是“通过背景判断是否绽放”如果背景是花园大概率绽放如果背景是深色盆栽大概率未绽放。这就很糟。我的对策是引入随机擦除Random Erase增强随机遮掉图像的一小块区域训练强迫模型利用花朵本身的结构信息另外把图片做中心裁剪让花朵尽量占满画面削弱背景信号。如果你有时间和算力也可以跑一个热力图可视化把模型的关注点画出来看看模型在看花瓣还是看背景。这一步如果效果好就是论文里的亮点图。7. 从毕设到可用的推理方案7.1 菜鸡也能跑起来的推理脚本训练完模型最终要交付一个能对单张图片做预测的脚本。别把推理逻辑写在训练代码里单独拆出来import torch from PIL import Image from torchvision import transforms def predict_single(model, image_path, devicecuda): transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) image Image.open(image_path).convert(RGB) tensor transform(image).unsqueeze(0).to(device) model.eval() with torch.no_grad(): logits model(tensor) prob torch.softmax(logits, dim1) return prob.cpu().numpy()[0] # [P(未绽放), P(绽放)]归一化参数是ImageNet数据集的统计值迁移学习场景直接沿用如果是纯从零训练你可以改成自己数据集的均值和标准差差异不会特别大。7.2 TinyWeb姿态的部署思路如果毕设有拓展要求想做成一个网页Demo推荐用Flask把上述预测脚本包成一个REST接口。前端传一张图后端返回两个概率值。界面不需要花哨一个文件上传按钮加一个结果展示区域就够了。这里有个部署的坑模型文件要加载到CPU内存里做推理不要依赖显存。部署机器的GPU不一定和训练机一样如果服务器没有CUDA环境就map_locationcpu保证预测能在纯CPU上跑。虽然单张图多几十毫秒延迟但换来的是最大兼容性。8. 最后的经验小结与答辩切入视角做这类图像分类毕设最后一个月能不能从容收尾取决于前面数据标注、日志记录这些阶段有没有偷懒。我的体感是模型训练只占总工作量的四成数据准备占四成结果分析和可视化占两成。很多同学前半个月就训练出不错的结果后半个月却不知道每天该干什么就是因为没有留出充足时间做实验对比和可视化。答辩时最容易被追问的三个问题你提前准备好答案为什么不用更大的网络回答应当是数据量只有数万张大网络会严重过拟合训练成本也高小网络已经能捕捉花瓣纹理级特征。数据哪里来的回答应当是有标注规范、有清洗流程、有人工复核而不是“从网上下载的”。怎么证明模型真的学会了“绽放”的概念回答应当是用热力图展示模型聚焦在花蕊和花瓣边缘以及展示错误样本来说明任务的灰度边界。最后分享一个我屡试不爽的小技巧训练完成后随机找20张测试图片自己先肉眼预测一遍再跟模型预测对照。找到模型错但你也没把握的图片把它们单独放进“边界样本”文件夹。答辩展示里这部分内容最能体现你对任务理解的深度比单纯报一个准确率数字有说服力得多。这个习惯毕业之后做任何视觉项目也都能沿用。