news 2026/10/22 4:00:51

基于PyTorch的CNN卷积神经网络鸡蛋破损识别项目实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于PyTorch的CNN卷积神经网络鸡蛋破损识别项目实战

如果你最近正在为毕业设计选题发愁,或者刷到过“基于Python的CNN卷积神经网络图像分类”这类论文标题,那么鸡蛋破损识别这个项目,其实是一个特别典型的样本。它表面看起来就是训练一个二分类模型,输入一张图片,输出“完好”或“破损”,但真正做一遍就会发现,数据怎么采、模型怎么搭、训练怎么调,每一步都藏着不少坑。这篇文章我就用自己完整做过的这个项目,把每一步怎么想、怎么做、为什么这么设计,全部掰开讲清楚,争取让照着做的人少走弯路。

我选的技术栈是PyTorch,没选TensorFlow,原因后面会说。整个项目的核心链条是:任务定义 -> 数据集构建 -> 模型设计 -> 训练调优 -> 评估验证,这五步听起来像教科书目录,实际执行时每一步的决策都会直接影响最终准确率。尤其是数据部分,我见过太多人一上来就敲模型代码,结果训了半天发现准确率上不去,回头一看,问题全出在数据集上。所以这篇文章也会把数据准备放到很重要的位置来讲。

1. 项目整体设计:先想清楚要解决什么问题

1.1 任务本质:不是识别鸡蛋,而是识别“破损”这一状态

很多人拿到这个题目,第一反应是“这不就是图像分类嘛”,然后直接开干。但稍微细想一下,任务描述里有一个容易被忽视的关键词——破损。我们要做的不是“认出这是鸡蛋”,鸡蛋本身的外形大家都很熟悉,真正难的是让模型学会“什么样算破损”。

破损蛋在真实场景里形态差异非常大。常见的有几种:蛋壳出现细长裂纹、一端被磕出凹陷、蛋壳碎成蛛网状但还没掉、甚至蛋液已经流出后蛋壳表面粘着湿润残留物。这些形态之间的视觉特征差别很大,细裂纹可能只有一两个像素宽,凹陷则表现为局部高光和阴影的突变,蛋液渗出会改变蛋壳表面的反光特性。如果一开始就把这些全部塞进同一个“破损”标签,模型要学的是一个多模态的类内分布,难度比单纯区分“红苹果”和“绿苹果”高得多。

所以我在项目第一步做的事情,是先给“破损”定一个可操作的范围。我的方案很朴实:主要识别裂纹和凹陷这两种最常见破损,蛋液渗出的样本如果有就单独收集,但没有强求。因为对毕业设计来说,完整覆盖所有破损形态不是必须的,把问题定义清楚、把一类问题做扎实,比模糊地追求“什么都能识别”更重要。

1.2 为什么传统图像处理方案搞不定这件事

如果时间退回几年前,很多做鸡蛋破损检测的人会先用传统机器视觉方案,思路大概是:对图像做灰度化、二值化,然后用边缘检测算子去找轮廓,再看图像里有没有异常长线。这种方案在固定光照、纯色背景、鸡蛋位置固定的实验室环境里确实能跑通,但放到稍微真实一点的环境里就非常脆弱。

首先,蛋壳本身不是光滑均匀的白色,它有天然的斑点、气孔纹路,甚至有些蛋壳自带细密网状花纹。传统边缘检测算法根本无法区分“蛋壳固有纹理”和“真实裂纹”,因为从像素梯度上看,这两者都可能表现为高强度边缘。其次,光照变化会带来高光、阴影和反光,破损区域在反光下可能被高光完全淹没,也可能因为阴影变得更明显,这种不稳定性是传统算法最头疼的。最后,传统方案要人工设计特征、调阈值,换一批鸡蛋、换一个光源,参数可能全都要重调,通用性很差。

我一开始还真试过用Canny边缘检测加霍夫变换来对比实验,结果在干净背景的测试图上有七八成准确率,一旦把鸡蛋旋转个角度、换个光源方向,准确率直接掉到五成以下,基本等于随机猜。这次对比实验让我彻底放弃了传统路线,也让我在论文里“为什么选择CNN”这一节有了实打实的对比数据可以写。

1.3 为什么最终选择CNN和PyTorch

CNN(卷积神经网络)相比传统方案的核心优势,是它不再依赖人工设计特征,而是让模型从数据中自动学习“什么特征代表破损”。这一点在裂纹检测上非常关键,因为裂纹的特征组合太复杂了:可能是连续的长边缘、可能是局部区域的灰度突变、也可能是细线周围的纹理断裂。手工把这些特征全部列出来几乎不可能,但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_classes=2): super(EggCrackCNN, self).__init__() self.features = nn.Sequential( nn.Conv2d(3, 16, kernel_size=3, padding=1), nn.BatchNorm2d(16), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(16, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), 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大小的卷积核,配padding=1,保证特征图尺寸不缩小,然后用MaxPool2d做一次下采样。图像从128×128经过四次池化,最后变成8×8×128的特征图,再通过自适应平均池化压成1×1×128的向量,最后由全连接层输出2个类别的分数。

为什么要加BatchNorm?因为它的作用是让每一层的输入分布尽量稳定,训练时收敛更快,还能起到轻微的正则化效果,对小网络来说性价比特别高。我试过不加BatchNorm的版本,训练到30轮时仍然明显震荡,加上之后20轮以内就稳定了。

参数量可以简单算一下。以第一个卷积层为例:卷积核大小3×3,输入通道3,输出通道16,那么权重参数是3×3×3×16=432个,再加上16个偏置,共448个参数。第二个卷积层是3×3×16×32+32=4640个。四层加起来,整个模型参数量大约在12万左右。这个量级用CPU训练也完全没问题,一个epoch只要几分钟。

2.3 损失函数与优化器选型

图像分类的标准损失函数是交叉熵损失(CrossEntropyLoss),PyTorch里直接调用就行。它做的事情,是衡量模型预测的概率分布和真实标签之间的差异,训练过程就是不断减小这个差异。

优化器方面,我推荐直接用Adam,初始学习率设为0.001。Adam的优势是自带自适应学习率调整,对初始参数的敏感性低,尤其适合刚入门、不太会手动调学习率的情况。当然,如果你想追求更高精度,后面对比实验里也可以加上SGD+Momentum,配合余弦退火学习率,往往能比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(degrees=20), transforms.RandomHorizontalFlip(p=0.5), transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.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(root='data/egg_dataset', transform=train_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_size=32, shuffle=True, num_workers=4, pin_memory=True) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=4, pin_memory=True)

有几个参数值得解释。num_workers=4表示用4个子进程加载图片,训练时不用等每张图片慢慢读,能明显加快速度。如果你的电脑内存不大,可以调小这个值。pin_memory=True在GPU训练时能减少数据传输时间,CPU训练时不影响功能,保留即可。

batch_size=32是基于我的数据集量级选的。这个值太小,训练不稳定;这个值太大,显存容易溢出,而且小数据集上学习效果不一定更好。如果你用的是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_classes=2).to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=1e-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(f'Epoch {epoch+1:02d}/{num_epochs} | ' f'Train Loss: {train_loss:.4f} | Train Acc: {train_acc:.2f}% | ' f'Val Acc: {val_acc:.2f}%')

这段代码里有一个非常重要的设计:每个epoch都做验证,并且验证准确率提升时保存模型。很多新手容易犯的错是训练完直接用最后一轮模型,但其实最后一轮往往已经过拟合了,验证准确率最高点大概率出现在中途某个epoch。保存最优模型而不是最后模型,是最稳妥的做法。

4.3 超参数选择:学习率、轮数和早停策略

训练的超参数组合我调过好几轮,列出几组真实对比:

学习率优化器训练轮数验证准确率现象
0.01Adam4086%前期loss震荡,收敛缓慢
0.001Adam4095%稳定收敛,效果最好
0.0001Adam4093%收敛慢,40轮不够充分
0.001SGD+Momentum(0.9)6096%调学习率策略后小幅提升

从我的实验看,0.001的初始学习率最省心。如果你发现训练loss一直不降,最可能的原因就是把学习率设大了,导致参数在最优解附近“来回跳”。另外一个好用的小技巧是配合ReduceLROnPlateau,当验证损失连续几个epoch不下降时自动把学习率缩小一倍,效果接近手动调参:

scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode='min', factor=0.5, patience=3, verbose=True ) # 每个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(dim=1).item() weights = fc_weight[class_idx] # shape: [128] feat = features[0].squeeze(0) # shape: [128, 8, 8] cam = torch.zeros(feat.shape[1:], dtype=torch.float32) for i, w in enumerate(weights): cam += w * feat[i] cam = torch.clamp(cam, min=0) # 再上采样到原图尺寸 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的验证结果和模型路径写进程一个文本日志文件,不要只打印在终端里。这样就算中途关了终端或者改代码出了岔子,也不至于丢光所有实验记录。这个小习惯在写论文、整理实验对比表格时帮助非常大。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 4:00:48

Windows Android SDK命令行安装与模拟器配置实战

简介:面向 Windows 平台开发者的 Android 命令行工具集最新版压缩包,专为无需安装完整 Android Studio 的用户设计。内含 sdkmanager、avdmanager 等核心可执行组件,可通过命令行下载 SDK 包、管理虚拟设备、执行构建与调试任务,适…

作者头像 李华
网站建设 2026/10/22 3:59:48

OpenClaw:面向开发者的自动记忆增强工程方法论

1. “OpenClaw”不是开源项目,而是一套可落地的记忆增强工程方法论“OpenClaw”这个名字乍听像某个GitHub上的新晋开源库——带“Open”前缀,后缀“Claw”(爪),容易让人联想到抓取、捕获、钩住信息的意象。但实际在多个…

作者头像 李华
网站建设 2026/10/22 3:58:29

Git多仓库合并实战:保留完整提交历史的三种方案

先给大家还原一个经常遇到的场景:手头攒了好几个 Git 仓库,逻辑上同属一条产品线,或者就是当年把项目拆得太碎,现在想收拢。某天一句"把它们合并成一个仓库,历史别丢",任务就落到了你头上。一开始…

作者头像 李华
网站建设 2026/10/22 3:57:21

感知机训练全拆解:从手算推导到Python实现与常见陷阱

很多人都以为感知机简单到不值得单独写一篇。公式就一行,代码几十行,跑起来也快。但我这些年给人讲机器学习,发现恰恰是这种“觉得简单”的心态,会让人在最基础的地方翻车:有人把标签写成0和1,训练完发现负…

作者头像 李华
网站建设 2026/10/22 3:57:06

10美元MCU上的端侧AI Agent:水产养殖智能闭环实践

1. 项目概述:当“养虾”不再只是水产养殖,而是一场端侧智能的硬核突围你可能第一眼看到“养虾神器「PicoClaw」”会笑出声——这名字太跳脱了,像极了某位极客在深夜调试完第7版固件后,顺手敲进README里的彩蛋。但别急着划走。它背…

作者头像 李华
网站建设 2026/10/22 3:55:52

Agent Harness实战:让AI从“只说不做”到真正干活的工程框架

我这段时间翻技术社区的帖子,翻到最多的一个词就是Agent Harness。说实话,我第一次看到这个词的时候也愣了半天——"Harness"是马具、挽具的意思,翻译成"智能体套具"多少有点拗口,但它想表达的东西其实特别直…

作者头像 李华