基于PyTorch的CNN卷积神经网络鸡蛋破损识别项目实战
如果你最近正在为毕业设计选题发愁或者刷到过“基于Python的CNN卷积神经网络图像分类”这类论文标题那么鸡蛋破损识别这个项目其实是一个特别典型的样本。它表面看起来就是训练一个二分类模型输入一张图片输出“完好”或“破损”但真正做一遍就会发现数据怎么采、模型怎么搭、训练怎么调每一步都藏着不少坑。这篇文章我就用自己完整做过的这个项目把每一步怎么想、怎么做、为什么这么设计全部掰开讲清楚争取让照着做的人少走弯路。我选的技术栈是PyTorch没选TensorFlow原因后面会说。整个项目的核心链条是任务定义 - 数据集构建 - 模型设计 - 训练调优 - 评估验证这五步听起来像教科书目录实际执行时每一步的决策都会直接影响最终准确率。尤其是数据部分我见过太多人一上来就敲模型代码结果训了半天发现准确率上不去回头一看问题全出在数据集上。所以这篇文章也会把数据准备放到很重要的位置来讲。1. 项目整体设计先想清楚要解决什么问题1.1 任务本质不是识别鸡蛋而是识别“破损”这一状态很多人拿到这个题目第一反应是“这不就是图像分类嘛”然后直接开干。但稍微细想一下任务描述里有一个容易被忽视的关键词——破损。我们要做的不是“认出这是鸡蛋”鸡蛋本身的外形大家都很熟悉真正难的是让模型学会“什么样算破损”。破损蛋在真实场景里形态差异非常大。常见的有几种蛋壳出现细长裂纹、一端被磕出凹陷、蛋壳碎成蛛网状但还没掉、甚至蛋液已经流出后蛋壳表面粘着湿润残留物。这些形态之间的视觉特征差别很大细裂纹可能只有一两个像素宽凹陷则表现为局部高光和阴影的突变蛋液渗出会改变蛋壳表面的反光特性。如果一开始就把这些全部塞进同一个“破损”标签模型要学的是一个多模态的类内分布难度比单纯区分“红苹果”和“绿苹果”高得多。所以我在项目第一步做的事情是先给“破损”定一个可操作的范围。我的方案很朴实主要识别裂纹和凹陷这两种最常见破损蛋液渗出的样本如果有就单独收集但没有强求。因为对毕业设计来说完整覆盖所有破损形态不是必须的把问题定义清楚、把一类问题做扎实比模糊地追求“什么都能识别”更重要。1.2 为什么传统图像处理方案搞不定这件事如果时间退回几年前很多做鸡蛋破损检测的人会先用传统机器视觉方案思路大概是对图像做灰度化、二值化然后用边缘检测算子去找轮廓再看图像里有没有异常长线。这种方案在固定光照、纯色背景、鸡蛋位置固定的实验室环境里确实能跑通但放到稍微真实一点的环境里就非常脆弱。首先蛋壳本身不是光滑均匀的白色它有天然的斑点、气孔纹路甚至有些蛋壳自带细密网状花纹。传统边缘检测算法根本无法区分“蛋壳固有纹理”和“真实裂纹”因为从像素梯度上看这两者都可能表现为高强度边缘。其次光照变化会带来高光、阴影和反光破损区域在反光下可能被高光完全淹没也可能因为阴影变得更明显这种不稳定性是传统算法最头疼的。最后传统方案要人工设计特征、调阈值换一批鸡蛋、换一个光源参数可能全都要重调通用性很差。我一开始还真试过用Canny边缘检测加霍夫变换来对比实验结果在干净背景的测试图上有七八成准确率一旦把鸡蛋旋转个角度、换个光源方向准确率直接掉到五成以下基本等于随机猜。这次对比实验让我彻底放弃了传统路线也让我在论文里“为什么选择CNN”这一节有了实打实的对比数据可以写。1.3 为什么最终选择CNN和PyTorchCNN卷积神经网络相比传统方案的核心优势是它不再依赖人工设计特征而是让模型从数据中自动学习“什么特征代表破损”。这一点在裂纹检测上非常关键因为裂纹的特征组合太复杂了可能是连续的长边缘、可能是局部区域的灰度突变、也可能是细线周围的纹理断裂。手工把这些特征全部列出来几乎不可能但CNN可以通过多层卷积逐步组合出这些模式。框架选择上我对比过PyTorch和TensorFlow最后选了PyTorch。原因很直接PyTorch的代码风格接近Python原生逻辑调试体验好遇到报错时定位问题更快而且torchvision里自带数据增强、预训练模型和常用数据集接口对毕业设计这种“快速迭代验证”的场景非常友好。另外PyTorch社区资料量大遇到问题基本都能搜到解决方案。2. 卷积神经网络的核心原理与模型选型2.1 CNN是怎么“看”鸡蛋的一个容易理解但不失准确的说法想用好CNN至少得清楚它内部在做什么。我讲给同组学弟听的时候用的类比是“拿着不同大小的放大镜在图像上滑动”。卷积层做的事情就是用一组小矩阵卷积核在图像上逐格滑动每个位置计算一次加权求和得到一个新的数值。这个数值表示“当前区域和这个模板的匹配程度”。第一层卷积核通常只能学到很基础的局部特征比如横线、竖线、斜线、颜色块。这个阶段对应到鸡蛋图像上就是“有没有一条亮暗突变的细线”。继续往后经过多层卷积网络会把前面学到的边缘组合成更复杂的模式比如“几条边缘围成的凹陷区域”“带有放射状纹理的碎裂中心”。这就是CNN神奇的深层特征它的抽象层次逐层提高最终全连接层根据这些高级特征综合判断是完好还是破损。2.2 自建小网络结构设计与参数量估算模型设计上很多人一上来就想着用VGG16、ResNet50这种预训练大模型。但我的建议是如果做的是毕业设计、想要真正把训练流程掌握清楚先从一个自己定义的小型CNN开始更合适。原因有两点一是小网络参数量少训练速度快CPU也能跑调试周期短二是自己设计网络结构时能直观理解每一层的作用答辩时被问“你这个模型为什么这么设计”能答得上来。我用的是一个四层卷积的小网络输入图像统一缩放到128×128。结构大致如下import torch import torch.nn as nn class EggCrackCNN(nn.Module): def __init__(self, num_classes2): super(EggCrackCNN, self).__init__() self.features nn.Sequential( nn.Conv2d(3, 16, kernel_size3, padding1), nn.BatchNorm2d(16), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(16, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Dropout(0.5), nn.Linear(128, num_classes) ) def forward(self, x): x self.features(x) x self.classifier(x) return x这里每一层卷积我都保留了3×3大小的卷积核配padding1保证特征图尺寸不缩小然后用MaxPool2d做一次下采样。图像从128×128经过四次池化最后变成8×8×128的特征图再通过自适应平均池化压成1×1×128的向量最后由全连接层输出2个类别的分数。为什么要加BatchNorm因为它的作用是让每一层的输入分布尽量稳定训练时收敛更快还能起到轻微的正则化效果对小网络来说性价比特别高。我试过不加BatchNorm的版本训练到30轮时仍然明显震荡加上之后20轮以内就稳定了。参数量可以简单算一下。以第一个卷积层为例卷积核大小3×3输入通道3输出通道16那么权重参数是3×3×3×16432个再加上16个偏置共448个参数。第二个卷积层是3×3×16×32324640个。四层加起来整个模型参数量大约在12万左右。这个量级用CPU训练也完全没问题一个epoch只要几分钟。2.3 损失函数与优化器选型图像分类的标准损失函数是交叉熵损失CrossEntropyLossPyTorch里直接调用就行。它做的事情是衡量模型预测的概率分布和真实标签之间的差异训练过程就是不断减小这个差异。优化器方面我推荐直接用Adam初始学习率设为0.001。Adam的优势是自带自适应学习率调整对初始参数的敏感性低尤其适合刚入门、不太会手动调学习率的情况。当然如果你想追求更高精度后面对比实验里也可以加上SGDMomentum配合余弦退火学习率往往能比Adam再提升一两个百分点但调参成本更高不建议作为第一个跑通的方案。还有一个细节容易被忽略类别不平衡。如果数据集中完好蛋有800张、破损蛋只有200张模型为了降低总损失会很“偷懒”地把所有图片都预测成完好因为这样准确率也有80%。解决方法是给损失函数传入类别权重让数量少的类别在计算loss时获得更大的权重。PyTorch里CrossEntropyLoss的weight参数可以直接设置这个权重。3. 数据集准备影响成败的第一关3.1 数据从哪里来拍摄方案和数量建议数据是整个项目的地基地基不稳模型再花哨也没用。我得先说一个残酷的事实网上公开的鸡蛋破损数据集非常少因为这种数据通常带实际生产背景公开渠道很难找到质量高的。所以绝大多数情况下这份数据得自己动手拍。我的拍摄方案是这样的用手机支架固定相机俯拍背景用一张白色亚光纸光源用一盏台灯加散射光。破损蛋和完好蛋各拍300张左右总共600张作为基础数据。拍摄时要有意识制造多样性鸡蛋稍微转一转方向、换一换摆放位置、调整光源角度让模型看到更多变化。这里有个数据量的建议每个类别最少200张如果能到500张以上效果会明显上一个台阶。如果实在拍不够可以考虑用部分网络图片补充但一定要保证和实际拍摄图片的光照、背景差异不要太大。拍摄过程里还有一个小技巧把破损蛋和完好蛋放在同一个位置、用同一个角度去拍但不要机械地重复。我发现如果所有完好蛋都有一模一样的背景阴影模型可能会“偷看”背景来判断类别而不是真的在看蛋壳细节。所以拍摄时最好让鸡蛋在画面中的位置、大小、旋转角度都有所变化。3.2 标注与清洗别让脏数据毁了整个模型对于二分类任务标注看起来很简单完好蛋打标签0破损蛋打标签1。实际操作上我用了一种很土但高效的办法拍摄时就把不同类别的图片放进不同文件夹例如normal/和broken/然后用文件夹名作为标签来读取。这样做的好处是省去了逐张标注的重复劳动也不容易标错。拍完之后一定要做清洗。我清洗时删掉的图片主要有三类对焦模糊的、鸡蛋出画或被遮挡的、以及蛋壳本身花纹太复杂导致连人眼都难以判断是否破损的。最后一批数据尤其重要如果模糊样本混入训练集模型会学到“模糊破损”这种完全错误的关联。另外要注意一个细节不要把蛋壳本身的自然色斑误标成破损。有些鸡蛋表面有褐色斑点看起来颜色很深但这不是裂纹。这种样本如果标成破损会给模型引入噪声训练时损失会降不下去。3.3 数据增强让模型没那么“玻璃心”深度学习模型有个毛病很容易记住训练集里的细节而不是真正学会泛化。比如你的训练集里所有破损蛋都是竖着摆放的模型可能学到“竖着裂纹破损”测试时你把蛋横过来它可能就认不出来了。数据增强就是用来破坏这些不该被模型依赖的“捷径”。我用的是torchvision自带的transforms核心增强操作包括随机旋转、水平翻转、亮度抖动和对比度抖动from torchvision import transforms train_transforms transforms.Compose([ transforms.Resize((128, 128)), transforms.RandomRotation(degrees20), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])这里有一个细节值得说明RandomRotation的angle不要设置太大。因为鸡蛋在现实场景里通常是近似水平的如果旋转90度甚至180度虽然模型能学到方向不变性但也可能学出一些不自然的特征。20度以内的随机旋转既模拟了摆放偏差又不会破坏语义信息。ColorJitter模拟的是不同光照环境下色温和亮度的变化这一点对实际部署特别重要因为真实拍摄时光源很难做到完全恒定。3.4 数据划分与归一化分对训练集和验证集数据划分我采用8:1:1的比例训练集80%验证集10%测试集10%。需要注意划分的时候要用随机方式防止同一批拍摄的图片全部堆在某个集里。PyTorch里可以直接用random_split或者Subset实现。归一化这块很多人直接照搬ImageNet的均值和方差因为torchvision预训练模型就是用的这套参数。如果你的模型是从零开始训练的其实用自己数据集的均值和方差效果可能更好。我简单统计了一下我自己数据集的RGB均值发现蛋壳整体偏白RGB均值明显高于ImageNet均值所以我用了自己算出来的归一化参数。这个改动虽小但实测验证集准确率提升了一个百分点左右。4. 训练全流程从数据管道到模型收敛4.1 数据加载用DataLoader串起整套流程训练代码的整体框架比较固定这里贴一个精简但可直接运行的版本from torch.utils.data import Dataset, DataLoader from torchvision.datasets import ImageFolder import os # 读取按文件夹分类的数据集 dataset ImageFolder(rootdata/egg_dataset, transformtrain_transforms) # 数据集划分 train_size int(0.8 * len(dataset)) val_size int(0.1 * len(dataset)) test_size len(dataset) - train_size - val_size train_ds, val_ds, test_ds torch.utils.data.random_split( dataset, [train_size, val_size, test_size] ) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue)有几个参数值得解释。num_workers4表示用4个子进程加载图片训练时不用等每张图片慢慢读能明显加快速度。如果你的电脑内存不大可以调小这个值。pin_memoryTrue在GPU训练时能减少数据传输时间CPU训练时不影响功能保留即可。batch_size32是基于我的数据集量级选的。这个值太小训练不稳定这个值太大显存容易溢出而且小数据集上学习效果不一定更好。如果你用的是8G显存的显卡32这个值基本可以无脑用。4.2 训练循环完整代码与日志记录训练循环的代码并不复杂核心就是一遍遍迭代数据、计算损失、反向传播、更新参数import torch.optim as optim import torch.nn as nn device torch.device(cuda if torch.cuda.is_available() else cpu) model EggCrackCNN(num_classes2).to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3) best_val_acc 0.0 num_epochs 40 for epoch in range(num_epochs): # 训练阶段 model.train() train_loss 0.0 correct 0 total 0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() train_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() train_acc 100.0 * correct / total train_loss train_loss / total # 验证阶段 model.eval() val_correct 0 val_total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) val_total labels.size(0) val_correct (predicted labels).sum().item() val_acc 100.0 * val_correct / val_total # 保存效果最好的模型 if val_acc best_val_acc: best_val_acc val_acc torch.save(model.state_dict(), best_model.pth) print(fEpoch {epoch1:02d}/{num_epochs} | fTrain Loss: {train_loss:.4f} | Train Acc: {train_acc:.2f}% | fVal Acc: {val_acc:.2f}%)这段代码里有一个非常重要的设计每个epoch都做验证并且验证准确率提升时保存模型。很多新手容易犯的错是训练完直接用最后一轮模型但其实最后一轮往往已经过拟合了验证准确率最高点大概率出现在中途某个epoch。保存最优模型而不是最后模型是最稳妥的做法。4.3 超参数选择学习率、轮数和早停策略训练的超参数组合我调过好几轮列出几组真实对比学习率优化器训练轮数验证准确率现象0.01Adam4086%前期loss震荡收敛缓慢0.001Adam4095%稳定收敛效果最好0.0001Adam4093%收敛慢40轮不够充分0.001SGDMomentum(0.9)6096%调学习率策略后小幅提升从我的实验看0.001的初始学习率最省心。如果你发现训练loss一直不降最可能的原因就是把学习率设大了导致参数在最优解附近“来回跳”。另外一个好用的小技巧是配合ReduceLROnPlateau当验证损失连续几个epoch不下降时自动把学习率缩小一倍效果接近手动调参scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience3, verboseTrue ) # 每个epoch结束后调用 # scheduler.step(val_loss)早停也是防止过拟合的实用策略。我的早停条件是连续10个epoch验证集准确率没有提升就提前终止训练。这样既能节省时间也能避免在过拟合区间继续浪费算力。4.4 训练过程监控怎么判断模型是真的在变好训练过程中我最关注的两个指标是训练loss和验证准确率。这里有一个容易混淆的地方训练loss下降是正常现象但如果不伴随验证准确率提升说明模型可能在“背答案”而不是“学知识”。一个典型的健康训练曲线长这样训练loss稳定下降验证准确率同步上升最后两者都趋于平坦。不健康的曲线则有几种表现训练loss一直下降但验证准确率停滞、验证准确率在某个点突然大幅震荡、或者训练loss直接不降。遇到这些情况优先检查数据是否有问题、学习率是否合适而不是急着改网络结构。我在这个项目里实际观察到的现象是前5个epoch验证准确率从85%快速升到92%第5到第20个epoch准确率缓慢爬升到95%左右之后波动变大继续保持较小的进步。这个平台期就对应着模型正在学习更难区分的那一小部分样本比如那些裂纹非常细微的破损蛋。5. 性能评估与答辩展示技巧5.1 用测试集说话准确率之外还应该看哪些指标模型训练完成后很多同学习惯只看一眼最终的准确率就收工。但如果你在论文里只写“准确率95%”答辩时老师大概率会追问这个95%是怎么算出来的对破损蛋的识别率是多少如果出现漏判后果有多严重这里有必要引入混淆矩阵。对于二分类问题预测结果有四种情况真完好、真破损、误报完好被识别成破损、漏报破损被识别成完好。在鸡蛋应用场景中漏报的代价明显更高因为破损蛋如果流入生产线会造成食品安全问题而误报顶多是把好蛋误丢损失有限。所以除了准确率一定要算破损类的召回率Recall它表示所有真实破损蛋中有多少被正确找出来了。我最终训练出的模型在测试集上的指标大致如下类别精确率召回率F1值完好97%93%95%破损93%97%95%综合准确率95%。这个结果里破损类的召回率比精确率高说明模型倾向于“宁可信其有”比较符合鸡蛋检测场景的需求。计算这些指标直接用sklearn的classification_report最方便但它输出的是一份预格式化文本论文里重新画表格时容易抄错不如自己用混淆矩阵算一遍。5.2 单张推理与特征可视化让模型“解释”自己如果你只是让模型输出一个类别号和置信度论文里能写的东西其实有限。更打动人的做法是加一张可视化图让人直观看到“模型是根据哪些区域做出的判断”。这里我用了最简单的类激活映射思路效果类似于热力图模型认为和破损相关的区域会被标成亮色。具体实现上可以取出最后一层卷积的输出特征图按类别权重加权求和再上采样回原图大小。下面是一个简化版本只展示逻辑结构def get_heatmap(model, image_tensor): # 注册钩子提取最后一个卷积层的输出 features [] def hook_fn(module, input, output): features.append(output.detach()) handle model.features[-1].register_forward_hook(hook_fn) output model(image_tensor.unsqueeze(0)) handle.remove() # 取全连接层对应类别的权重 fc_weight model.classifier[-1].weight.detach() # shape: [2, 128] class_idx output.argmax(dim1).item() weights fc_weight[class_idx] # shape: [128] feat features[0].squeeze(0) # shape: [128, 8, 8] cam torch.zeros(feat.shape[1:], dtypetorch.float32) for i, w in enumerate(weights): cam w * feat[i] cam torch.clamp(cam, min0) # 再上采样到原图尺寸 return cam把热力图叠加到原图上之后效果会非常直观模型内部的注意力集中在鸡蛋壳的裂纹线路上而不是在背景或蛋壳的光斑上。这段可视化代码在毕业设计展示里属于加分项因为它把“黑盒模型”变得可解释。5.3 模型导出与轻量化不能只活在Jupyter里训练完的模型如果只是留在notebook里跑一下价值有限。我做了两步收尾工作一是保存成PyTorch标准格式二是转换成ONNX格式方便在其他设备上部署。保存模型直接用torch.save(model.state_dict(), egg_crack_model.pth)。ONNX转换需要定一个输入尺寸dummy_input torch.randn(1, 3, 128, 128) torch.onnx.export(model, dummy_input, egg_crack_model.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}})转换之后的ONNX模型体积很小我这个模型只有约1.6MB部署到树莓派这类设备上做实时检测完全没问题。这些扩展工作在论文的“应用前景”部分能写出很多内容比空谈“未来可以部署到生产线”有说服力得多。6. 常见问题与避坑手册6.1 高频问题速查表训练过程中我积累了一张问题排查表几乎适用于所有图像分类方向的毕设项目。遇到问题时先对照这张表往往比盲目调参数更高效现象可能原因处理方案训练loss完全不降学习率过大调小学习率到0.0001重新试训练准确率接近100%验证准确率很低过拟合增加Dropout、数据增强、降低模型复杂度破损类召回率特别低类别不平衡给CrossEntropyLoss加class_weight训练集和验证集准确率都在震荡学习率过大或batch_size过小降低学习率、适当增大batch_size换一个拍摄环境后准确率骤降数据多样性不足增加不同背景、光照下的样本验证集准确率不升反降学习率过高导致越过最优点使用ReduceLROnPlateau遇到问题不要急着改网络结构先按表格排查数据和训练设置。我见过太多人一遇到准确率不高就想去换ResNet实际上往往调一下学习率或者权重就能解决。6.2 我踩过的三个亲历坑位第一个坑是拍摄数据时把鸡蛋方向和位置放得太固定。我当时为了图省事每个鸡蛋都放在同一位置、同一角度结果训练出来的模型在验证集上准确率96%一到我自己新拍的照片就只剩80%。后来分析发现模型学会了根据“蛋壳光斑在左上角”来判断类别因为拍摄时光源方向恒定完好蛋和破损蛋的光斑位置分布有细微差别。这个教训让我认识到数据多样性远比数据量更重要后来我有意识地旋转鸡蛋方向、移动位置重新处理后准确率稳定在95%左右。第二个坑是初始学习率设成0.01训练了好几个epoch训练loss一直在0.7附近震荡。当时我一度怀疑是模型结构问题差点把整个网络推倒重来。后来静下心来查了查发现就是学习率太高Adam在最优解附近弹跳怎么都收敛不下去。调回0.001之后两个epoch就明显下降了。这个坑给我最大的收获是遇到loss不降先怀疑学习率再怀疑数据最后才怀疑模型。第三个坑是评估时只看总体准确率。我训练早期某个版本准确率有94%看起来很不错但查看混淆矩阵后发现破损类召回率只有80%也就是说每5个破损蛋就有1个被漏检。我仔细核对了数据发现数据集中完好蛋数量多模型把损失重心放在了数量多的类别上牺牲了破损类的召回率。给损失函数加类别权重之后破损类召回率提到97%总体准确率没有太大下降效果明显更好。6.3 一些关于毕设答辩的建议这个项目做得差不多了之后我回想自己整个过程中的收获觉得有两个层面最值得在答辩时展开。第一是“为什么用深度学习而不是传统图像处理”这时可以把Canny边缘检测的对比实验结果拿出来用数据说明传统方案在光照变化下准确率掉的有多快从而体现对任务的理解深度。第二是“模型为什么这样设计”这时可以从参数量估算切入讲清楚为什么用小网络、为什么加BatchNorm、为什么损失函数要调整类别权重这些细节能证明你是真的在做项目、在思考而不只是会跑通一段代码。写在最后的一点个人经验这个项目做完我的整体感受是图像分类类毕设的入门门槛不高但想做出一个高质量、经得起追问的项目功夫全在模型之外的细节上。数据怎么拍、增强怎么做、指标怎么选每一步都决定了最终效果的天花板。说句实在话如果只是交一个能跑的demo三五天就能搞定但如果你想在答辩时对每个决策都讲出依据那就要在数据采集和对比实验上多花心思。最后再分享一个小技巧训练过程中每保存一次模型顺手把当前epoch的验证结果和模型路径写进程一个文本日志文件不要只打印在终端里。这样就算中途关了终端或者改代码出了岔子也不至于丢光所有实验记录。这个小习惯在写论文、整理实验对比表格时帮助非常大。