做课程设计选了这个题目,第一反应是挺简单的——把玻璃图片扔进卷积神经网络里看一眼,是完整还是破碎,这不就是个图像二分类任务吗?真上手之后才发现,里面的坑一个接一个。单说数据准备,就折腾了将近一半的时间。这里不打算贴那种注释满天飞但跑不起来的长篇代码,而是把整个项目从思路、数据、训练、评估到答辩交付,按我实际走通的顺序完整讲一遍。任何做图像分类课程设计或毕业设计的同学,看完之后应该都对从零搭一个识别“玻璃是否破碎”的CNN模型有比较完整的认知,也能直接复用这套流程。
1. 项目要解决的到底是什么问题——思路拆解与应用边界
1.1 任务本质:这是一个图片二分类问题
先说清楚任务边界。你要识别的是“玻璃是否破碎”,落到模型层面,就是给模型一张图片,让它输出两个类别中的一个:完整玻璃(0)或者破碎玻璃(1)。这是典型的二分类任务,而且是监督学习下的图像分类子问题。
为什么必须强调这个边界?因为很多同学第一次看到“机器学习”四个字,就想着要堆什么复杂模型。实际上玻璃破碎识别的核心不是模型有多深,而是特征是否可分。破碎玻璃的特征集中在裂纹网络、碎玻璃边缘的高光、不规则的几何碎片、反光模式断裂这些视觉模式上;完整玻璃则表现为连续光滑的表面、规律的反射、没有断裂纹理。两条特征在像素空间里天然有区分度,所以用CNN去自动提取这些特征,本质上比人工设计边缘检测算子要省事得多。
这里还有个容易被忽略的前提:输入图片的来源。课程设计里常用的是实验室拍的样品图,或者网上搜集的玻璃照片,甚至可以是手机随手拍。不同来源意味着不同的分辨率、光照条件和拍摄角度,模型能不能在这些变化下保持稳定,才是项目真正的挑战。换句话说,识别本身不难,难的是在真实环境里还认得出。
1.2 为什么选CNN而不是传统图像处理方案
这个理由我建议提前想清楚,因为答辩时大概率会被问到。传统方案比如直接用OpenCV做边缘检测、统计灰度梯度、计算纹理特征,理论上确实能区分部分玻璃裂纹。但问题在于,传统特征工程需要针对具体场景手工调参,光照一变、玻璃厚度一变、裂纹形态一变,阈值就失效了。裂纹的形态根本不能用一两组固定的滤波器参数概括。
CNN的天然优势是端到端学习。它自己从数据里学边缘、纹理、局部模式,从浅层的边缘检测到深层的语义特征,逐层抽象。对于玻璃这种材质,表面纹理和裂纹特征恰好是有强局部结构的目标,卷积操作通过局部感受野天然适配这种模式。
另外还有一点,CNN不需要你手工设计任何特征。玻璃的光照反射非常复杂,单纯靠灰度、梯度的规则很难穷举。模型只要大量看图,就能把“什么样的裂纹纹理组合更大概率是破碎玻璃”这种隐性规则沉淀进权重里。从课程设计的角度,选CNN也更容易展示出完整的技术流程,从数据到训练再到评估,所有环节都能体现。
1.3 这类项目适合什么场景,你自己能拿到什么用途
放在课程设计和毕业设计的语境里,这个题目属于图像分类方向里的“小而完整”型项目。它不像目标检测那么复杂,不需要标框也不需要懂NMS,但对数据、训练、评估、调参的完整链路要求很高,非常适合用来体现你对深度学习基础流程的掌握程度。做出来之后,模型是一个能单张预测、也能批量预测的工具,还能顺带导出成网页演示Demo,展示效果很直观。
往实际应用方向延伸,玻璃破碎识别在工业生产质检、建筑安全巡检、汽车玻璃检测等场景里都有相似需求。课程设计虽然不用真去生产线采集数据,但学到的方法是完全通用的。我当时做完之后,又把框架迁移到了“瓷砖表面裂纹”和“木板节疤”两个模拟数据集上,调整网络输入通道和分类数,基本没费什么劲。这一点可以作为答辩中的亮点——你做的不是一个孤立模型,而是一套可复用的图像分类方法。
2. 数据准备与预处理,训练集的构建质量直接决定模型上限
2.1 数据来源:自拍、公开数据集和程序生成
数据是这类项目里最容易被低估的环节。我见过太多人上来就套用某个公开数据集,跑个准确率就交差,但问到数据怎么来的、分布怎么样,完全答不上来。这种答辩基本撑不过三分钟。
玻璃是否破碎这个方向没有特别标准的数据集,常规的做法是三种来源混合。
第一种是自建数据。有条件的话,用手机或相机在自然光、白炽灯、逆光、侧光、平铺、竖放等不同条件下拍玻璃样品。完整玻璃和破碎玻璃至少各拍150到300张,图片数量不需要太多,但场景覆盖一定要丰富。我当时就是拿几个玻璃杯和一块玻璃板,特意敲碎之后铺在不同颜色背景上拍的,整个过程花了两个下午。
第二种是爬取网络图片。注意筛选质量,优先要那些画面主体是玻璃、背景干扰少的图片。玻璃反光严重,网络图容易混入大量背景信息,后期清洗成本很高。
第三种是数据合成。写个简单的脚本生成带裂纹纹理的图片,或者从已有的裂纹图像上做随机裁剪、旋转、拼接。合成数据的好处是能快速扩大样本多样性,坏处是合成图和真实图之间可能存在域差异,使用时要保证测试集里包含足够多的真实样本。
2.2 数据标注规范化与类别边界定义
数据不是拍完就完事,还得仔细标注。建议建一个CSV文件,两列:图片路径和标签。0代表完整,1代表破碎。文件夹结构可以这样组织:
glass_data/ ├── train/ │ ├── intact/ # 完整玻璃 │ └── broken/ # 破碎玻璃 ├── val/ │ ├── intact/ │ └── broken/ └── test/ ├── intact/ └── broken/按文件夹划分是最直观的方式,用PyTorch的ImageFolder就能直接读取,省去写复杂CSV解析的麻烦。
标注时容易犯的一个错误是忽略“中间状态”。比如有裂纹但还没碎开的玻璃,或者只碎了一个小角的情况,这算完整还是破碎?我的建议是做项目时干脆把边界定义为:存在贯穿性或大面积裂纹、明显破碎形状的就算破碎;表面完好或者只有不影响整体结构的轻微痕迹的算完整。如果训练集里这类边界样本不多,模型就会在模糊区域乱猜,所以宁可花时间做几张边界样例,也别让模型在测试时栽在语义模糊上。
2.3 预处理流程:尺寸统一、归一化与数据增强
模型要求输入尺寸固定。我的网络设计成输入224x224的RGB图片,所以所有原始图片都要resize到这个尺寸。直接resize会把长宽比压扁,玻璃几何形状会发生畸变,但好在这个任务对长宽比不太敏感,模型主要学的是纹理特征,所以简单resize问题不大。如果任务换成对长宽比敏感的零件尺寸检测,就需要用letterbox方式填充,不能直接拉伸。
归一化也非常关键。常见做法是把每个像素的RGB值从0到255归一化到0到1,更规范一点的做法是计算数据集的均值和标准差,再做标准化。标准化能让网络收敛更稳定。直接除以255在多数场景下也能跑,但归一化后训练曲线的震荡会小很多。
数据增强是提升泛化能力的重要手段。对于玻璃图片来说,真正有效的方法包括随机水平翻转、随机小角度旋转(±15度以内)、亮度对比度微调,以及随机裁剪。我个人踩过的坑是:不要用太过激进的增强,比如大范围随机旋转、重缩放、颜色反转反而会破坏玻璃的反射结构,导致模型学到错误语义。增强的目的是模拟真实拍摄条件的差异,不是把图片变得人眼都认不出来。
2.4 数据集划分:训练集、验证集、测试集各有各的用处
很多同学把数据划成训练集和测试集就完事了,这是不对的。测试集只能碰一次,最终评估用的;训练过程中你需要一个验证集来判断模型是否过拟合、什么时候该停。正确做法是划分成三份:训练集占70%到80%,验证集占10%到15%,测试集占10%到15%。
划分时注意保证类别比例一致,让完整和破碎在每份数据里都大致是1:1。如果原始数据本身就不均衡,比如完整玻璃200张、破碎玻璃只有60张,就需要做扩充:将破碎类做更多增强,或对完整类做降采样,避免模型把所有图片都预测成占比高的那一类。
另外,划分前一定要把同一场景下连拍的照片分配到同一集合。我遇到过这样一个情况:一组灯光下的完整玻璃连拍了五张,拆分时两张进了训练集、两张进了验证集、一张进了测试集。因为图片几乎一样,验证集和测试集里出现了训练集图片的“复刻版”,结果准确率看着虚高,实际换新场景就崩。数据划分要按场景分组,而不是逐张随机分配。
3. 模型搭建与训练,从零实现一个可用的CNN识别模型
3.1 网络结构设计:先用简单结构跑通,再考虑加深
很多教程一上来就是几层卷积堆叠,动辄几百层残差网络,这对一个数千张图片的课程设计来说,不是最优策略。网络太深,数据量小,很快过拟合,训练时间也长。更合理的方案是先用一个轻量网络把项目跑通,确认数据流程没有问题,再根据验证结果决定是否加深或换用预训练模型。
我参考LeNet和VGG的思路,设计了一个简单结构:
| 层 | 类型 | 输出尺寸 | 核心参数 |
|---|---|---|---|
| 输入 | RGB图片 | 224x224x3 | — |
| Block1 | 卷积+ReLU+最大池化 | 112x112x16 | 3x3卷积核,16通道,padding=1 |
| Block2 | 卷积+ReLU+最大池化 | 56x56x32 | 3x3卷积核,32通道,padding=1 |
| Block3 | 卷积+ReLU+最大池化 | 28x28x64 | 3x3卷积核,64通道,padding=1 |
| 输出前 | 展平+全连接+Dropout | 512 | dropout=0.5 |
| 输出层 | 全连接 | 2 | softmax或sigmoid |
为什么卷积核用3x3而不是更大的5x5或7x7?因为多个小卷积核串联,有效感受野和大卷积核相同,但参数量更少,非线性更强,这在实践里是经典做法。之所以从16通道开始,是因为玻璃破碎识别属于纹理分类,浅层特征已经占主导,通道数不需要一开始就很大,逐层翻倍的策略能显式控制模型容量。
3.2 损失函数、优化器和基础训练配置
二分类的最后一层有两种写法:输出2个神经元接softmax,或者输出1个神经元接sigmoid。两种写法在数学上等价,但PyTorch里对应的损失函数不同。输出2个神经元时,用CrossEntropyLoss,它会自动做softmax;输出1个神经元时,用BCEWithLogitsLoss,它把sigmoid和二元交叉熵合并在一起,数值稳定性更好。我选的是前者,在课程设计里解释起来更直观,混淆矩阵也好画。
优化器方面,Adam是默认选择,初始学习率设为0.001。学习率太大,loss会震荡甚至发散;太小,收敛慢,训练半天还停在原地。对这类小型CNN,0.001是一个很稳的起点。训练时还可以配合StepLR或CosineAnnealing做学习率衰减,我当时观察到最后50个epoch里loss降不动了,就把学习率改为0.0001继续跑了20轮,又降了一截。
批次大小batch size,在显存允许的前提下选32或者64。batch太小(比如4)会导致梯度噪声大,收敛不稳定;太大(比如256)在小数据集上容易很快收敛到平坦区域,泛化不一定好。我实测下来32时精度和稳定性的平衡最好。
3.3 训练流程实现与代码注释
下面这段代码是我实际跑通的训练主循环骨架,去掉了平台相关部分,只保留核心逻辑。完整工程里还应该包括断点续训、训练日志记录和配置解析,但理解分类任务的核心就看这一段。
import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms from torchvision.models import resnet18 # 仅用于对比,实际训练用自建结构 class GlassCNN(nn.Module): def __init__(self, num_classes=2): super(GlassCNN, self).__init__() self.features = nn.Sequential( nn.Conv2d(3, 16, kernel_size=3, stride=1, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(64 * 28 * 28, 512), nn.ReLU(inplace=True), nn.Dropout(p=0.5), nn.Linear(512, num_classes) ) def forward(self, x): return self.classifier(self.features(x)) train_transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(degrees=15), transforms.ColorJitter(brightness=0.2, contrast=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) val_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]) ]) train_dataset = datasets.ImageFolder(root='glass_data/train', transform=train_transform) val_dataset = datasets.ImageFolder(root='glass_data/val', transform=val_transform) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=4) model = GlassCNN(num_classes=2) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.001) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.9) best_acc = 0.0 for epoch in range(30): model.train() train_loss = 0.0 for inputs, labels in train_loader: optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() train_loss += loss.item() * inputs.size(0) model.eval() val_correct = 0 val_total = 0 with torch.no_grad(): for inputs, labels in val_loader: outputs = model(inputs) _, predicted = torch.max(outputs, 1) val_correct += (predicted == labels).sum().item() val_total += labels.size(0) val_acc = val_correct / val_total print(f'Epoch {epoch+1:02d} | Loss: {train_loss/len(train_dataset):.4f} | Val Acc: {val_acc:.4f}') if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), 'glass_cnn_best.pth') scheduler.step()代码里有个细节:训练过程中每个epoch结束都保存验证集准确率最高的权重,而不是最后一个epoch结束才保存。因为后期可能发生过拟合,验证集准确率反而下降,保存最优权重可以保证交付的模型是验证集上的最优状态。训练循环里的scheduler放在每个epoch结束后更新,不是每个batch结束。
3.4 训练过程观察什么,曲线怎么读
训练时不要只盯着一个最终准确率。我建议记录四个量:训练集loss、验证集loss、训练集准确率、验证集准确率。每次epoch结束打一次日志,最后用可视化工具画成两条曲线。
判断模型状态的经典规律是:训练loss下降但验证loss上升,说明模型过拟合,开始在记忆训练样本而不是总结规律。这时候可以增加dropout比例、增加数据增强强度,或者降低模型复杂度。反过来说,如果训练和验证的loss都居高不下,说明模型欠拟合,需要增加网络宽度或深度、降低正则化、调大学习率。训练loss下降但验证loss也缓慢下降,准确率跟着涨,这是最健康的信号,保持现有配置继续训练就好。
我跑这个模型的时候,前期大概8个epoch训练准确率就到了80%,但验证准确率只有70%,这就是典型的过拟合倾向。后来的对策是把dropout从0.3提到0.5,同时给训练集加了亮度扰动,问题立刻缓解。
4. 评估测试与模型交付,准确率不是唯一标准
4.1 评估指标:准确率之外还要看精确率和召回率
多数人汇报模型性能时只写准确率。这不能说是错的,但对一个二分类任务来说,准确率会掩盖很多问题。玻璃破碎识别场景里,把完整玻璃误判成破碎(假正例)和把破碎玻璃漏判成完整(假负例)的代价完全不同——在质检场景里漏判的后果更严重。所以要把更细致的指标算出来。
需要重点理解这几个定义:
- 准确率(Accuracy):预测正确的样本占总样本的比例。
- 精确率(Precision):预测为破碎的样本中,真正破碎的比例。
- 召回率(Recall):实际破碎的样本中,被正确识别为破碎的比例。
- F1分数(F1 Score):综合精确率与召回率的调和平均,避免只偏重某一个指标。
如果验证集上你的模型准确率是92%,但查一下混淆矩阵发现,10张破碎玻璃里有3张被漏判成完整,那召回率只有70%,这个92%就很有迷惑性。我的建议是训练完成后,用sklearn的classification_report输出完整报告,作为课程设计报告里的核心数据。
4.2 混淆矩阵与误判案例整理
混淆矩阵是答辩时非常有说服力的工具。它直观展示模型在四个区域的表现:完整预测为完整(TN)、完整预测为破碎(FP)、破碎预测为完整(FN)、破碎预测为破碎(TP)。我最后的模型在测试集上的混淆矩阵大致如下:
| 真实类别 | 预测为完整 | 预测为破碎 |
|---|---|---|
| 完整玻璃 | 45 | 3 |
| 破碎玻璃 | 2 | 50 |
也就是说,完整类被误报成破碎的有3张,破碎类被漏报成完整的有2张。整体准确率95%,精确率和召回率都在92%以上。
误判集中在哪几类图片?我把错误的测试样本单独归档后,发现两类情况占比最高。一类是磨砂玻璃或花纹玻璃的完整状态,表面本身带有复杂纹理,模型容易把纹理误当裂纹;另一类是碎玻璃上带有大面积高光反射,镜面反射干扰了模型判断。整理出这些案例,一方面能让答辩汇报更有深度,另一方面也说明模型边界在哪里。真实应用里如果遇到类似图片,就需要在训练集里补充这类样本。
4.3 模型导出和最简单的预测脚本
模型训练完成后,课程设计交付至少要有一个能独立运行的预测脚本,不能每次预测都重新跑一遍训练代码。PyTorch的保存方式常用的有两种:直接保存整个模型,或者只保存state_dict。我习惯用state_dict,因为加载灵活,结构代码改动后只需要重新构造模型再load就行。
一个最简单的单张预测脚本核心思路是:加载权重、图像预处理、forward推理、解析输出概率。我建议不只用argmax取类别,而是把softmax后的概率值也打印出来。如果模型输出完整玻璃的概率是0.51,破碎是0.49,虽然也能给出类别,但置信度很低,这种结果需要人工复核。概率值输出对分析模型的不确定性很有帮助。
如果想让项目更亮眼,可以再做两步:把模型导出成ONNX格式,写一个非常简单的Flask网页Demo,支持上传图片并返回识别结果。这个成本不高,但演示效果和答辩加分效果却很直接。我当时就是在本地起了个简单的Web界面,毕业论文答辩时直接现场演示,不用开训练终端,也不用加载一堆开发环境。
5. 实操中的常见问题与避坑技巧实录
5.1 数据阶段常见问题
数据阶段最容易踩的坑就是类别不平衡。一张数据集中,如果完整玻璃有500张、破碎玻璃只有50张,模型学到的决策边界会偏向完整类。后果是测试时破碎玻璃漏判率极高。解决的办法不只是复制破碎图片,而是配合数据增强和加权损失。
第二个常见问题是图片尺寸不统一时直接送进模型就报错。PyTorch的DataLoader不太可能在运行时自动帮你resize,除非在transform里显式写了Resize。很多报错信息“Expected input batch_size to match target size”就是这类问题。
第三个问题,也是最隐蔽的:标签写反了。文件夹名是broken但里面放的是完整玻璃的图片。这类错误不会让训练崩掉,但会让准确率永远上不去,而且很难排查。我后来习惯在预处理阶段写一个检查脚本,随机抽几张图输出标签和原始文件名核对一遍。
5.2 训练阶段常见问题
训练不收敛是高频问题。第一步检查的不是模型结构,而是数据预处理和标签。如果标签没问题,再确认是否有对数据做了normalize,输入范围是否在0到1附近。第二步看损失函数下降趋势,如果loss停在某个值不动,可以尝试调低学习率或换优化器。第三步才考虑网络结构是不是太浅。
loss出现NaN也是常见现象。原因往往不是模型不行,而是学习率过大或者数据里出现了极端值。解决方法是调小学习率,并检查图片像素是否正常。如果训练数据里有损坏图片(比如完全黑色的空文件),也要先清洗掉。
还有一个细节要提醒:PyTorch默认的卷积层权重初始化对浅层网络是有效的,但如果自己定义了特殊的初始化逻辑,可能会破坏训练稳定性。对于课程设计级别的项目,使用默认初始化完全足够。
5.3 课程设计答辩与文档撰写的实用建议
答辩时老师最喜欢问的问题是“为什么选这个结构”“你的模型和其他方法相比有什么优势”“换一组数据还能用吗”。回答这些问题的核心不是背代码,而是理解设计决策。提前准备好几个问题的答案:为什么用3x3卷积堆叠、为什么在池化后加dropout、为什么选Adam、数据增强为什么选这几个组合。
文档撰写方面,我建议按这个逻辑组织:背景与问题定义、数据采集与预处理、模型结构设计、训练过程与结果、评估分析、总结与展望。重点是评估分析部分,把你踩过的坑、调参过程中的对比实验都写进去。答辩评审想看到的是一个会思考的人,而不是只会跑代码的工具人。
课程设计类项目还有一个常被忽略的问题:运行环境复现。尽量在文档开头写清楚Python版本、PyTorch版本、CUDA版本,最好再附一个requirements.txt。我见过不少同学答辩现场环境崩掉,演示不了的场面,提前准备一个无GPU也能运行的CPU推理逻辑,关键时刻很管用。
最后说点个人体会。这类图像分类课程设计,代码层面的难度其实不高,训练流程也基本固定。真正决定项目最终效果的是数据质量和对评估的理解。你要是能在答辩时清晰说出“我的模型误判集中在磨砂玻璃上,因为表面纹理和裂纹特征有重叠”,比回答“我用了一个优化器”要强得多。另外,别觉得CNN是个黑盒,做几个误判样本的可视化分析,把特征分布的差异讲清楚,整个项目的层次立刻就不一样了。