简介:本资源是一套完整的煤矸石智能识别分类系统实现方案,面向计算机、人工智能、自动化等专业的本科生及研究生,适用于毕业设计、课程设计与工业场景初步验证。系统基于ResNet卷积神经网络构建,集成图像预处理、特征提取、模型训练与GUI交互界面,解决煤矿井下煤与矸石的自动区分难题。压缩包共45个文件,含11个核心Python源码(如main.py、CNN/vgg16相关模块)、4个Jupyter Notebook实验脚本、7张典型样本图像、10个CSV格式中间数据及模型文件,整体大小仅4.37MB,结构清晰、模块解耦,便于理解深度学习流程与工程落地细节。已有181人下载学习,项目源自高分毕设(答辩95分),代码经实测可直接运行,配套详细操作教程与数据集,支持零基础入门与二次开发拓展。 先交代一下背景。我最近在做一个煤矸石识别分类的课设级项目,核心是用深度学习里的ResNet卷积神经网络做图像分类,最终交付的是一个带GUI界面的Python程序,里面把训练好的模型、数据集、源码和操作文档全打包好了。这篇文章就把这个项目从选型到落地过程中,那些真正值得记下来的东西完整梳理一遍。不管你是正在找课设方向的在校生,还是想了解AI视觉在矿业场景如何落地的工程师,这篇文章应该都能给你一些能直接拿去用的参考。
先说结论:这个项目选ResNet作为主干网络,配上PyTorch框架,用迁移学习的方式在自建的煤矸石图像数据集上做微调,最后用PyQt5把推理流程封装成可视化的GUI程序。整个过程并不复杂,但里面有不少坑藏在细节里。后面我会把这些细节一个个展开讲。
1. 为什么是ResNet:从煤矸石识别场景反推网络选型
1.1 煤矸石识别到底是个什么任务
煤矸石是采煤和洗煤过程中排出的含碳量较低的岩石,成分以砂岩、页岩、石灰岩为主。在选煤厂,煤和矸石的分离直接关系到精煤质量和后续利用价值。传统的分选方式有跳汰、重介、浮选等物理方法,人工拣矸则是劳动强度最大、环境最差、效率最不稳定的环节。
随着智慧矿山推进,基于机器视觉的煤矸石识别逐渐成为热点。它的本质是一个图像二分类问题:给一张图,判断里面是煤还是矸石。但难点在于:
- 煤和矸石在可见光下颜色接近,尤其是矸石表面常附着煤粉,外观差异不明显
- 现场光照复杂,皮带上的物料有堆叠、遮挡、运动模糊
- 不同矿区的煤质和矸石成分差异大,模型泛化能力要够强
- 实时性要求高,识别速度要跟上皮带运行速度
这些约束决定了不能用太浅的网络,也不能用对图像细节不敏感的网络。ResNet在ImageNet上验证过的深层特征提取能力,恰好能覆盖这些需求。
1.2 ResNet解决了什么核心问题
在ResNet出现之前,CNN网络层数加深反而会出现退化问题——训练集准确率饱和后下降,这不是过拟合,而是深层网络难以优化。ResNet通过残差连接(skip connection)改变了这一局面。
残差块的核心思想可以用一句话概括:与其让网络直接学习一个从输入到输出的映射 H(x),不如让网络学习残差 F(x) = H(x) - x,最终的输出为 F(x) + x。这样即使深层模块的参数没有学到有效特征,输入信息也能通过捷径连接直接传递到后面层,梯度在反向传播时也多了一条高速公路。也就是说,ResNet在结构层面保证了深层网络的训练下限不会低于浅层网络。
这一设计的直观效果就是网络可以做到非常深,而不必担心梯度消失或网络退化。在煤矸石识别这类细粒度分类任务中,深层网络意味着更大的感受野和更抽象的特征表达能力,能够捕捉煤和矸石表面纹理、光泽、结构上的细微差异。
1.3 ResNet的几个常见变体怎么选
ResNet家族有18层、34层、50层、101层、152层等常见版本,层数差异主要体现在残差块的数量和结构上。
| 模型 | 层数 | 残差块结构 | 参数量 | 适合场景 |
|---|---|---|---|---|
| ResNet18 | 18 | BasicBlock(2层卷积) | 约11M | 快速验证、算力有限 |
| ResNet34 | 34 | BasicBlock(2层卷积) | 约21M | 精度要求中等 |
| ResNet50 | 50 | Bottleneck(3层卷积) | 约25M | 精度与速度均衡 |
| ResNet101 | 101 | Bottleneck(3层卷积) | 约44M | 高精度、算力充足 |
这个项目我用的是ResNet50。原因很实际:ResNet50的Bottleneck结构在保持参数量不夸张的前提下,特征提取能力比ResNet18/34有明显提升;相比ResNet101,训练速度更快,对显存的要求也更低。如果你是在普通笔记本电脑上跑,显存只有4-6G,ResNet18是个更稳妥的选择,但精度上要做好心理准备。
实践中还有一个组合方案:先用ResNet18快速验证数据质量和标注是否正确,确认baseline能跑通后,再切到ResNet50做最终训练。这样既不会在数据有问题时浪费时间训练大模型,也不会因为模型太小而误判数据集的难度。
2. 数据集的构建比模型本身更决定上限
2.1 数据从哪来,怎么整理
煤矸石数据集的获取渠道主要有三条:一是去现场用工业相机或手机拍摄,二是网上公开数据集,三是通过爬虫抓取矿业相关图片。这个项目用的是综合方案:以公开数据为基础,补充了部分现场拍摄图像。
数据整理最核心的原则是类别目录结构清晰。PyTorch的ImageFolder机制要求数据按类别分文件夹存放,我的目录结构是这样的:
data/ ├── train/ │ ├── coal/ │ │ ├── coal_001.jpg │ │ ├── coal_002.jpg │ │ └── ... │ └── gangue/ │ ├── gangue_001.jpg │ ├── gangue_002.jpg │ └── ... ├── val/ │ ├── coal/ │ └── gangue/ └── test/ ├── coal/ └── gangue/这个结构看起来简单,但有个容易被忽略的细节:类别名称建议直接用英文,不要用中文文件夹名。虽然ImageFolder支持中文路径,但后续在Windows上做模型加载、打包发布时,中文路径很容易在编码上出问题。我一开始用了"煤"和"矸石"两个中文文件夹名,训练一切正常,结果在GUI里加载模型时路径解析总是报错,排查了半天才发现是编码问题。
2.2 数据量不够怎么办:数据增强的完整策略
煤矸石分类数据集通常不会太大,公开数据集一般也就几千张到一两万张。如果直接用原始数据训练ResNet50这种深层网络,过拟合几乎是必然的。解决思路主要有两条:预训练权重做迁移学习、数据增强扩大样本分布覆盖。
数据增强这部分,我用的组合包括:
- 随机旋转(±30度)
- 随机水平翻转
- 随机亮度、对比度、饱和度调整
- 随机缩放裁剪(ResizedCrop)
- 随机高斯噪声
这些增强操作不是随便加的。旋转和翻转对应皮带运输过程中煤矸石姿态的不确定性,亮度对比度调整对应现场光照变化,缩放裁剪模拟不同拍摄距离下目标大小变化。每一个增强策略背后都要对应真实场景中可能出现的变化,这比盲目堆叠增强手段有意义得多。
需要提醒的是,验证集和测试集不应该做数据增强,只需要做Resize和归一化。否则验证集和训练集存在分布重叠,会高估模型真实泛化能力。
2.3 数据标注的质量控制
如果你是自建数据集,标注质量直接决定模型上限。煤矸石图像标注虽然只有两个类别,但有几个容易出错的地方:
- 煤炭可能带有矸石夹层,矸石表面可能粘附煤粉,这种"跨界"样本需要人工仔细甄别
- 图像中若包含多个目标,需要考虑是整体标注还是裁剪后单目标标注
- 模糊、过曝、欠曝严重的图像,要么删除要么单独归入困难样本集
我处理"跨界样本"的方式是单独抽出来做人工复核,请了几个矿业背景的朋友帮忙确认,最终形成共识后再决定归属。这里投入的时间不会白费,模型上线后碰到的边界case,往往就是这些样本。
3. 核心实现:从Model到Train到GUI的完整链路
3.1 迁移学习:用预训练权重比从零训练强多少
在ImageNet上预训练的ResNet50,学到的是通用的图像底层特征——边缘、纹理、颜色块、形状组合。煤矸石图像的底层特征和自然图像有共通之处,因此直接微调比从零训练高效得多。
我的模型定义代码是这样写的:
import torch import torch.nn as nn import torchvision.models as models class ResNetClassifier(nn.Module): def __init__(self, num_classes=2, model_name='resnet50', pretrained=True): super().__init__() if model_name == 'resnet50': self.backbone = models.resnet50(pretrained=pretrained) elif model_name == 'resnet18': self.backbone = models.resnet18(pretrained=pretrained) else: raise ValueError(f"Unsupported model: {model_name}") in_features = self.backbone.fc.in_features self.backbone.fc = nn.Sequential( nn.Dropout(0.5), nn.Linear(in_features, 256), nn.ReLU(inplace=True), nn.Dropout(0.3), nn.Linear(256, num_classes) ) def forward(self, x): return self.backbone(x)这里有一个细节值得说明:替换全连接层时,我加了两层Dropout。原因很简单,煤矸石数据集中煤和矸石的图像在高层语义上差异确实不大,模型很容易在训练集上死记硬背,Dropout是抑制过拟合性价比最高的手段之一。
微调策略上,我没有对所有层都用同一个学习率。更大的学习率会破坏预训练特征,因此我采用了参数分组——backbone部分用较小的学习率(一般是1e-4),新加的fc层用较大的学习率(1e-3)。
optimizer = torch.optim.AdamW([ {'params': model.backbone.parameters(), 'lr': 1e-4}, {'params': model.backbone.fc.parameters(), 'lr': 1e-3} ], weight_decay=1e-4)这种差异化学习率的做法在迁移学习中非常实用。它本质上是在尊重预训练知识的同时,让新任务的分类头能够更快地适应自己的数据分布。
3.2 训练流程的关键细节
训练流程本身不复杂,但有几个细节决定了最终效果。
第一是损失函数。二分类问题,直接用nn.CrossEntropyLoss()即可。很多人会犹豫要不要用Focal Loss处理类别不平衡,如果训练集中煤和矸石的数量基本一致,就没必要。如果两类数量差距超过3倍,再用Focal Loss或者对损失做类别加权也不迟。
第二是学习率调度。我用的是一步到位的方式:步长衰减。每10个epoch把学习率乘以0.1,配合早期停止,能在不频繁干预的情况下获得稳定的收敛效果。也可以用CosineAnnealingLR,但实践中步长衰减对课设项目更直观。
第三是保存最优模型。不要简单保存最后一个epoch的模型,而是监控验证集准确率,只在验证准确率刷新时保存权重。这个"best模型"策略是老生常谈,但确实有效。
best_acc = 0.0 for epoch in range(epochs): train_loss = train_one_epoch(model, train_loader, optimizer, criterion) val_acc = evaluate(model, val_loader) if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), 'best_model.pth') print(f"Epoch {epoch}: save best model, acc = {val_acc:.4f}")3.3 GUI界面:PyQt5的封装思路
GUI部分用的是PyQt5。设计思路不复杂:程序启动时加载训练好的模型权重,用户通过文件选择框选择煤矸石图片,点击预测按钮后,程序对图片做预处理、推理、后处理,把预测结果显示在界面上。
界面布局分三个区域:
- 左侧是图片展示区,QLabel加载待预测的图片
- 右上角是预测结果区,显示类别标签和置信度
- 右下角是控制按钮区,包含选择图片、开始预测、退出程序按钮
核心推理代码如下:
from PyQt5.QtWidgets import QMainWindow, QFileDialog, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QWidget from PyQt5.QtGui import QPixmap from PyQt5.QtCore import Qt import torch from PIL import Image from torchvision import transforms class PredictWindow(QMainWindow): def __init__(self, model, device, class_names): super().__init__() self.model = model self.device = device self.class_names = class_names self.setWindowTitle("煤矸石识别分类系统") self.setGeometry(100, 100, 800, 600) self.init_ui() def init_ui(self): self.image_label = QLabel(self) self.image_label.setAlignment(Qt.AlignCenter) self.image_label.setStyleSheet("border: 1px solid #cccccc; background-color: #f5f5f5;") self.image_label.setMinimumSize(400, 400) self.result_label = QLabel("请选择图片进行识别", self) self.result_label.setAlignment(Qt.AlignCenter) self.result_label.setStyleSheet("font-size: 16px; font-weight: bold;") self.select_btn = QPushButton("选择图片", self) self.predict_btn = QPushButton("开始预测", self) self.quit_btn = QPushButton("退出", self) # 此处省略布局管理代码 self.select_btn.clicked.connect(self.select_image) self.predict_btn.clicked.connect(self.predict_image) def select_image(self): file_path, _ = QFileDialog.getOpenFileName(self, "选择图片", "", "图片文件 (*.jpg *.jpeg *.png *.bmp)") if file_path: self.image_path = file_path pixmap = QPixmap(file_path) scaled_pixmap = pixmap.scaled(self.image_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation) self.image_label.setPixmap(scaled_pixmap) def predict_image(self): if not hasattr(self, 'image_path'): return image = Image.open(self.image_path).convert('RGB') tensor = self.preprocess(image).unsqueeze(0).to(self.device) with torch.no_grad(): outputs = self.model(tensor) probs = torch.softmax(outputs, dim=1) confidence, predicted = torch.max(probs, 1) label = self.class_names[predicted.item()] self.result_label.setText(f"识别结果:{label} 置信度:{confidence.item():.4f}") def preprocess(self, image): 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]) ]) return transform(image)3.4 预处理的一致性有多重要
我在开发过程中踩过一个非常典型的坑:训练时用的预处理是Resize到224x224,而GUI里预测时用的预处理是CenterCrop到224x224。表面上看都是输出224x224,但图像内容完全不同,导致预测结果大幅波动。
这个问题在深度学习项目中很常见。必须保证训练、验证、测试、部署四个阶段的预处理完全一致。最佳实践是把预处理封装成一个函数或类,在训练脚本和部署脚本中共享,而不是各写一份。
这个项目的预处理管线相对简单:Resize(224,224),ToTensor,用ImageNet的mean和std做归一化。因为训练时用的是ImageNet预训练权重,归一化的统计量必须与预训练时保持一致,否则特征分布会发生偏移。
4. 训练效果评估与模型调优过程记录
4.1 实验记录:从基线到最终模型
我记录了三组关键对比实验,这几组数据对理解煤矸石识别问题很有参考价值。
| 实验配置 | 训练集准确率 | 验证集准确率 | 说明 |
|---|---|---|---|
| ResNet18 + 从零训练 | 96.2% | 82.5% | 严重过拟合,泛化差 |
| ResNet18 + 预训练微调 | 98.1% | 91.3% | 迁移学习效果显著 |
| ResNet50 + 预训练微调 | 99.0% | 94.8% | 精度进一步提升 |
| ResNet50 + 预训练微调 + 数据增强 | 96.7% | 96.1% | 训练集和验证集差距缩小 |
从这个结果可以明显看到两个核心结论:
第一,迁移学习远比网络深度更重要。ResNet18从零训练到82.5%,ResNet18预训练微调直接到91.3%,这近9个百分点的提升不是来自网络结构的改变,而是预训练权重带来的先验知识。
第二,数据增强让训练集和验证集准确率差距从4.2个百分点缩小到0.6个百分点。这说明之前的高验证集准确率确实有一定程度的过拟合。如果你是做课设需要展示模型鲁棒性,数据增强是必备环节。
4.2 训练中的loss曲线怎么看
训练过程中我习惯同时观察训练loss和验证准确率,两者结合起来能判断模型的训练状态。
- 训练loss下降但验证准确率停滞:可能还在过拟合的路上,需要提前停止或加强数据增强
- 训练loss和验证loss都在下降但很慢:学习率太小,可以适当调大
- 训练loss下降很慢且振荡大:学习率太大,或者batch size太小,需要考虑梯度累积
我最终训练ResNet50时用的是batch size 32,初始学习率1e-4,AdamW优化器,总共训练了大概25个epoch。大约在第15个epoch左右,验证准确率基本稳定在95%以上,之后每轮最多提升0.1-0.2个百分点,考虑性价比就提前停止了。
4.3 混淆矩阵揭示的信息
准确率之外,我习惯额外看一眼混淆矩阵。这个项目里,混淆矩阵给出了一个有意思的发现:模型把矸石误判为煤的概率,要高于把煤误判为矸石的概率。
原因是矸石表面常常覆盖煤粉,在视觉上确实接近煤;而煤的表面如果干净且有光泽,和矸石的差别反而更大。所以模型学到的并不是简单的颜色差异,更多的是纹理和光泽特征。这提醒我们:如果你想让模型在判断矸石时更保守(宁可误报也不要漏报,从选煤角度看漏掉矸石比误丢煤损失更大),可以在后处理时对置信度阈值做类别差异化设置。
5. 环境配置与打包发布:最容易翻车的环节
5.1 环境版本搭配
这个项目在Python 3.8环境下开发,PyTorch用的是1.12.1,torchvision对应0.13.1,GUI部分用PyQt5。这套组合在Windows 10/11 64位上都能稳定运行,CPU和GPU均支持。
安装核心依赖,建议用pip直接安装:
pip install torch==1.12.1 torchvision==0.13.1 --index-url https://download.pytorch.org/whl/cu113 pip install PyQt5 pip install numpy opencv-python pillow matplotlib如果是纯CPU环境,把下载地址换成cpu版本即可。注意Python版本不要高于3.10,不然一些旧版本的torch可能装不上。
5.2 PyInstaller打包GUI程序
整个项目完成后需要交付给非技术用户,我用PyInstaller做了打包。打包配置里最关键的坑是把模型权重文件一并打包进去。
PyInstaller默认不会把非代码文件打包到exe中,即使你用了--add-data。我的做法是写一个spec文件:
# coal_recognition.spec # -*- mode: python ; coding: utf-8 -*- a = Analysis( ['main.py'], pathex=[], binaries=[], datas=[ ('best_model.pth', 'models'), ('class_names.txt', 'config') ], hiddenimports=['torch', 'torchvision'], hookspath=[], runtime_hooks=[], excludes=[], noarchive=False, ) pyz = PYZ(a.pure) exe = EXE( pyz, a.scripts, a.binaries, a.datas, [], name='煤矸石识别系统', debug=False, strip=False, upx=True, console=False, icon='icon.ico' )这里有一个需要特别说明的问题:hiddenimports里面必须包含torch和torchvision,否则PyInstaller在静态分析时可能漏掉一些动态导入的模块,打包后的exe一运行就报ImportError。还有一个隐性问题是torch本身非常大,打包后exe体积可能达到1GB以上,这是正常的,不用惊慌。
为了减小打包体积,可以在excludes里排除不需要的torch模块,比如torch.distributed、torch.testing等,能把体积压缩20%-30%。
5.3 模型加载路径的正确写法
打包后模型文件的路径和开发环境完全不同。开发时用相对路径'best_model.pth'没问题,但打包后工作目录不一定在exe所在目录,直接加载会报文件找不到。
正确的做法是通过sys._MEIPASS获取资源路径:
import sys import os def resource_path(relative_path): if hasattr(sys, '_MEIPASS'): base_path = sys._MEIPASS else: base_path = os.path.abspath(".") return os.path.join(base_path, relative_path) model_path = resource_path(os.path.join('models', 'best_model.pth'))这个resource_path函数在PyInstaller打包场景中是标准解法,注意在开发调试时也应该调用这个函数,而不是直接写相对路径,这样才能保证两套环境下的行为一致。
6. 推理速度测量与实时性评估
6.1 单张图片推理耗时
煤矸石识别如果用于在线皮带分选,实时性是很重要的指标。我做了推理速度测试,在GPU为GTX 1660的机器上,ResNet50单张图片推理耗时约18ms;在纯CPU环境,单张图片推理耗时约80ms。
注意这里的推理耗时只统计了model(tensor)的部分,也就是纯前向传播时间。完整的识别流程还包括图像读取、解码、预处理、后处理,这些额外操作大约还要消耗10-20ms。
6.2 如何进一步压缩推理延迟
如果实际部署需要更高的实时性,可以考虑以下降级路径:
- 模型蒸馏:用ResNet50当教师网络,蒸馏一个ResNet18或者更小的学生网络,推理速度能快2到3倍,精度损失通常在1到2个百分点以内
- TensorRT加速:NVIDIA显卡上将模型转换为TensorRT引擎,推理速度可以再提升2到3倍
- 输入分辨率降为160x160或128x128:损失部分精度但速度大幅提升
对课设项目来说,CPU实测80ms单张完全够用,不需要额外折腾。但如果要在Demo现场走实时摄像头视频流,建议至少用GPU跑,同时将预处理统一改为CV2或PIL的快速路径。
6.3 批量推理与单张推理的取舍
GUI场景一般单张预测,不需要批处理。但如果场景是做离线图片批量分类,比如对一批皮带抓拍图做统计,应该把多张图片组成一个batch,batch size设置为GPU显存允许的最大值,这样能显著提升吞吐量。PyTorch对batch维度有各种底层优化,4张图一起推理的时间大概是单张推理的1.5-2倍左右,而不是4倍。
7. 常见报错清单与排查方案汇总
这个项目从开发到打包交付,各种报错碰到了不少。我把这些问题按出现频率整理成一个清单,方便你排查。
7.1 训练阶段的典型报错
| 报错信息 | 原因 | 解决方案 |
|---|---|---|
| OSError: image file is truncated | 图像文件损坏或不完整 | 用cv2.imdecode代替PIL读取,或跳过损坏文件 |
| RuntimeError: CUDA out of memory | 显存不足 | 减小batch size、降低输入分辨率,或改用CPU |
| UserWarning: The given NumPy array is not writable | torch与numpy版本不兼容 | 升级或统一numpy版本,无需过度关注 |
| FileNotFoundError: No such file or directory | 数据路径设置有误 | 检查数据集目录结构,注意相对路径和绝对路径的关系 |
7.2 GUI阶段的典型报错
| 报错信息 | 原因 | 解决方案 |
|---|---|---|
| ModuleNotFoundError: No module named 'PyQt5.sip' | PyQt5安装不完整 | 重装PyQt5或升级PyQt5-sip |
| AttributeError: 'NoneType' object has no attribute 'text' | 界面控件未正确初始化 | 检查UI文件是否加载,信号槽是否连接 |
| UnidentifiedImageError | 选择的图片格式不支持 | PIL只支持部分格式,建议统一用jpg/png |
| CUDA error: no kernel image available | PyTorch版本和显卡驱动不匹配 | 增加兼容的CUDA计算能力即可,或改用CPU |
7.3 打包阶段的典型报错
PyInstaller打包torch项目最常见的报错是ModuleNotFoundError: No module named 'torch._C'。这是PyInstaller的静态分析漏掉了torch的C扩展模块导致的,解决方法是安装pyinstaller-hooks-contrib:
pip install pyinstaller-hooks-contrib另一个常见问题是打包后exe运行就闪退,没有任何提示。这种情况先在命令行模式下运行exe,即打包时console=True,让报错信息打印在终端里,定位后再改回console=False。
8. 项目交付结构一览与最终代码组织
整个项目完成后,最终的目录结构如下:
coal_gangue_recognition/ ├── main.py # 程序入口,负责启动GUI ├── model.py # ResNet模型定义 ├── train.py # 训练脚本 ├── predict.py # 单张图片预测脚本(命令行版) ├── gui_window.py # PyQt5主窗口实现 ├── utils.py # 预处理、数据加载等公共函数 ├── models/ │ ├── best_model.pth # 训练好的最佳模型权重 │ └── resnet50_initial.pth # 初始预训练权重(可选) ├── data/ │ ├── train/ │ ├── val/ │ └── test/ ├── requirements.txt # 依赖清单 ├── 操作教程.md # 详细的图文操作说明 └── 煤矸石识别系统.spec # PyInstaller打包配置8.1 requirements.txt的完整内容
torch==1.12.1 torchvision==0.13.1 numpy==1.24.3 opencv-python==4.8.1.78 Pillow==10.0.0 PyQt5==5.15.9 matplotlib==3.7.2 scikit-learn==1.3.0这里有个个人经验:scikit-learn虽然只在计算混淆矩阵和分类报告时用到,但建议一并放进依赖里,因为做实验评估时它几乎是必需品。如果打包最终应用,可以在spec文件里把sklearn排除掉,减小体积。
8.2 模型文件的管理
训练好的best_model.pth就是使用torch.save(model.state_dict(), 'best_model.pth')整存的状态字典。加载时需要先实例化同样结构的模型,再load_state_dict。整个过程容易出错的地方是类别顺序。
class_names.txt里固定保存类别的顺序,例如:
coal gangue模型的输出维度0对应coal,维度1对应gangue。这个顺序必须和训练时保持一致,否则预测结果就是错乱的。建议在保存模型的同时保存class_names文件,并写一个自动校验逻辑——加载模型后先做一次小批量推理,检查输出的类别分布是否符合预期。
8.3 总结一个端到端的使用流程
从拿到项目到成功运行,标准流程如下:
- 根据requirements.txt安装依赖
- 把数据集放到data目录
- 运行train.py完成训练,得到best_model.pth
- 运行main.py启动GUI,选择图片测试预测
- 如需交付,用PyInstaller执行spec文件打包
这个流程就是打包在zip文件里的"操作教程"的核心内容,教程里补充了截图和常见问题排查,这里不再展开。
9. 几点额外的经验教训
9.1 不要迷信模型复杂度
煤矸石识别本质上是二分类,图像特征差异比想象中要小。不要一开始就上ResNet101、EfficientNet甚至Vision Transformer,先跑通一个小的baseline,把数据和预处理链路验证好,再逐步升级模型。Baseline的意义不在于最终精度,而在于能把整个训练流程的bug暴露出来。
9.2 做一个"让人能看懂"的GUI
如果你的项目最终需要展示给老师或者观众,GUI好不好用直接影响印象分。这里有几个实用建议:
- 显示预测结果时,同时展示置信度数值,让抽象的分类结果变得可解释
- 界面提供历史记录列表,展示最近10次识别的图像路径和结果
- 支持批量选择图片,连续识别多张并统计识别率
- 界面上用不同颜色区分结果——煤用绿色,矸石用红色,直观明了
这些改动看起来琐碎,但对体验的提升是质的飞跃。
9.3 训练时做好实验记录
训练过程中的每一次改动,包括数据切分方式、增强策略、学习率、batch size、最终精度,都建议用表格记录下来。这不但让你的项目报告更有说服力,更重要的是,当结果不理想时,你能快速定位是哪个环节出了问题。
9.4 关于置信度阈值的一个进阶建议
如果你的煤矸石识别系统最终要接入实际的分选控制流程,建议不要简单取argmax作为最终输出,而是设置置信度阈值。比如当置信度低于0.6时,判定为"不确定",扔掉或者送到人工复核环节。在实际工况中,"不确定"比"错误分类"造成的损失要小得多。这个思路适用于任何分类系统的工程化部署。
9.5 CPU环境下如何跑得更快
没有显卡的环境也能跑,但要注意几个优化:
- 使用
torch.set_num_threads(4)限制CPU线程数,避免线程过多导致调度开销 - 使用
torch.backends.mkldnn.enabled = True开启MKL-DNN加速 - 输入尺寸从224降到160或128,单张推理时间能缩短近一半
实测下来,CPU环境ResNet18配合这些优化,单张图片推理时间可以压缩到30ms以内,已经具备基本的实时处理能力。
9.6 最后分享一个排查模型的思路
如果模型训练完,验证准确率很高,但实际测试时表现很差,优先检查预处理是否一致、类别顺序是否紊乱、模型是否保存了最优权重而不是最后一个epoch的权重。这三个问题占了"模型上线后翻车"原因的八成以上,剩下的两成才是数据质量问题。
如果你是要拿这个项目做课程设计,我的建议是把从零到一的完整过程做扎实——从数据分析、模型选型、训练评估到GUI封装和打包发布,每一步都能讲清楚为什么这么做,远比堆叠花哨技术有用。实际开发过程中踩过的那些坑,往往才是答辩时最能展示你理解深度的素材。
本文还有配套的精品资源,点击获取