垃圾分类识别,是我见过上手价值最高的计算机视觉入门项目之一。它看着小众,却把图像识别、卷积神经网络、模型训练、评估调优再到部署验证这一整条链路全走了一遍,而且场景特别真实——你家里随便拍一瓶矿泉水、一个纸箱、一节电池,模型能不能认出来,一测便知。这篇文章就以“基于卷积神经网络实现垃圾分类”为主线,给出完整的项目拆解和可直接运行的Matlab代码思路,适合刚接触深度学习、想在Matlab里跑通一个实际图像分类任务的同学参考。哪怕你之前只用过图像处理的基础函数,跟着这个思路走,也能把一个四分类垃圾分类模型从数据准备一直做到训练完成。
1. 项目方案与技术选型
1.1 为什么是卷积神经网络,而不是老办法
在做垃圾分类识别之前,先想明白一个问题:为什么不用以前那套“手工特征 + 分类器”的方案。传统做法一般是先提取颜色直方图、纹理特征、边缘方向直方图,再丢给支持向量机或随机森林去分类。听起来也能跑,但一放到真实场景就露馅了。
原因在于垃圾分类的视觉差异太复杂。同一个“可回收物”里,既有无色透明的矿泉水瓶,也有印着大logo的彩色饮料瓶,还有压扁的纸箱、揉成团的铝罐。这些物体的核心区别不只是颜色,更是材质、形状、纹理、反光特性这些组合在一起的整体感觉。手工设计一个特征去同时抓住“透明玻璃的反光边缘”和“瓦楞纸的条纹纹理”,工程量巨大,而且换一批拍摄环境就得重新调。卷积神经网络恰好不用你操心特征设计,它自己从数据里学。低层卷积核关注边缘、颜色块、局部纹理,高层卷积核把这些组装成“瓶口螺纹”“纸箱折痕”“塑料袋褶皱”这样有语义的部件。整个过程是端到端的:图像进去,类别标签出来,中间的每一层都在自动优化。
生活里很好理解:传统方法是“你告诉机器应该看什么”,而CNN是“机器自己发现该看什么”。垃圾分类这种类别多、外观差异大的任务,正是CNN最擅长的领域。
提示:如果你手里只有几百张图片,不必被“深度学习需要海量数据”吓住。我在实际测试中,一个小型CNN在每类三四百张图片的规模下,四分类准确率也能跑到90%上下。关键是数据处理和训练策略要靠谱,这个后面展开讲。
1.2 为什么选用Matlab,而不是Python
很多朋友一听到深度学习第一反应是Python,但Matlab在这个场景里其实有它独特的优势,特别适合快速验证和做课程项目。
第一个优势是封装程度高。配合深度学习工具箱,搭建CNN基本不用手动推导反向传播,convolution2dLayer、reluLayer、classificationLayer这些接口拿来就能用,代码量比想象中少很多。第二个优势是数据流组织方便。imageDatastore可以直接按文件夹结构把图片加载成带标签的数据集,augmentedImageDatastore可以在读取图片的同时完成尺寸统一和数据增强,这些操作在Python里往往要借助多个库组合完成,在Matlab里几行代码就搞定。第三个优势是可视化体验。训练过程中打开training-progress绘图,损失曲线和验证准确率实时刷新,对理解训练过程、判断模型是否收敛非常有帮助。
我并不是说Python不好,如果你要做生产级部署,Python生态肯定更完整。但如果你是在校学生、科研人员,或者想快速验证一个识别方案的可行性,Matlab这套工具链能帮你把大量时间从“写代码”省到“想问题”上。而且Matlab训练好的模型可以直接导出到多种部署平台,后期转应用也不难。本文所有代码基于Matlab的深度学习工具箱编写,版本在R2021a及以上都能直接运行。
2. 数据集构建与预处理
2.1 类别设计与数据来源
垃圾分类的类别设计是整个项目的基石。别一上来就照着网上几十类的分类标准做,类别越多数据收集成本和训练难度越大。实际动手建议先做四分类:可回收物、有害垃圾、厨余垃圾、其他垃圾。这个粒度既能覆盖日常主要场景,又不会让初学者被数据量压垮。
如果你想更进一步,可以把可回收物拆成塑料瓶、玻璃瓶、纸类、金属罐四个子类,这样就能做一个七分类或八分类的模型。但从项目复杂度来看,第一次做四分类已经能把整个技术链路趟通,做熟了再扩展类别并不难。
数据来源通常有三类:
- 自己拍。手机就够,把饮料瓶、废纸、果皮、塑料袋放在不同背景、不同光线、不同角度下拍摄,模拟真实场景。
- 公开数据集。网上能找到一些垃圾分类图像集,下载后注意看类别标注是否规范、图片分辨率是否一致。
- 网络爬图。不推荐初学者用,图片质量参差不齐,还要花大量时间清理脏数据。
单类别样本量建议不少于300张,类别之间尽量均衡。如果某一类特别少,后面训练时模型会明显偏向多数类。数据目录结构建议这样组织:
data/ ├── train/ │ ├── Recyclable/ │ ├── Harmful/ │ ├── KitchenWaste/ │ └── Other/ ├── val/ │ └── ...(对应四个子目录) └── test/ └── ...(对应四个子目录)训练集、验证集、测试集的比例用8:1:1比较常见。验证集用来观察训练过程中的表现,测试集只在模型训练完之后评估最终效果,千万不要让测试集参与训练决策,否则结果会虚高。
2.2 图像预处理与数据增强
原始图片不能直接丢给网络,先要做几件标准操作。
第一步是统一尺寸。这里选224×224像素,原因有两个:一是这个尺寸能保留足够多的纹理细节,二是如果以后想换用预训练网络做迁移学习,224×224是很多经典网络的默认输入尺寸,兼容性最好。如果你的机器配置较低,降到128×128也能接受,但识别细节稍差。
第二步是归一化。图像像素值范围是0到255,训练时网络内部计算对数值范围很敏感,Matlab的augmentedImageDatastore会在读取时自动处理,你不需要手动做归一化操作,但心里要明白这一步是存在的。
第三步是数据增强。这是提分性价比最高的手段之一。增强不是造新图片,而是对原图做随机变换,让模型看到更多“同一个人不同姿势”的样子,从而学会抓本质特征而不是死记背景。
Matlab代码里只需要在读取数据时加上增强参数:
imageSize = [224 224 3]; augimdsTrain = augmentedImageDatastore(imageSize, imdsTrain, ... 'DataAugmentation', imageDataAugmenter(... 'RandXTranslation', [-10 10], ... 'RandYTranslation', [-10 10], ... 'RandXScale', [0.9 1.1], ... 'RandXReflection', true, ... 'RandRotation', [-20 20], ... 'RandBrightness', [-0.1 0.1]));这里做了随机平移、随机缩放、水平翻转、随机旋转和亮度抖动。我个人的经验是增强幅度不要过大。比如旋转角度超过30度,瓶子、纸箱这类有明显朝向的物体看起来会很不自然,反而干扰训练。随机水平翻转对瓶瓶罐罐这类东西是安全的,因为翻转后仍然是现实中可能出现的视角;但如果你识别的是有文字的包装袋,翻转会反转文字方向,这时就需要谨慎使用。
验证集和测试集不要用增强,保持原始变换。实际代码里验证集用的是普通的augmentedImageDatastore,只做尺寸统一,不加随机变换。
3. 卷积神经网络结构拆解与Matlab实现
3.1 卷积层、池化层都在做什么
CNN的基础概念有必要简单过一遍,因为在调参时你会反复跟这几个名词打交道。
卷积层可以想象成拿一个放大镜在图片上滑动,每滑动到一个位置,就把放大镜看到的局部区域做一个加权求和。这个放大镜就是卷积核,它的权重是训练中学出来的。不同的卷积核关注不同的模式:有的关注横向边缘,有的关注圆形轮廓,有的关注密集纹理。多个卷积核堆叠在一起,相当于一群各司其职的“特征探测器”同时扫描整张图。
池化层的任务更单纯:把特征图缩小,同时保留最重要信息。常用的是最大池化,取一个小窗口里最大的数值作为输出。这样做的好处有两个,一是大幅减少了后续计算的参数数量,二是让模型对物体位置的微小变化不那么敏感。这就像看一张照片,你不关心瓶子具体在画面的左边还是右边,只要它在那儿,就能认出来。
激活函数这里用ReLU,它的作用是把负值截断为0,给网络引入非线性。如果没有这个非线性,卷积和全连接不管叠多少层,数学上仍然等效于一个线性变换,根本学不了复杂模式。在Matlab代码里,reluLayer直接调用即可。
最后连接的是全连接层和softmax层。全连接层把前面提取到的所有高级特征汇总成一个固定长度的向量,softmax把这个向量转换成每个类别的概率,比如可回收物0.85、其他垃圾0.10、厨余垃圾0.04、有害垃圾0.01,最后取概率最大的类别作为预测结果。
3.2 搭建一个轻量级分类网络
我们不做那种动辄几十层的重型网络,一个三卷积层的小型网络,在几百张每类的数据集上已经足够。网络结构如下:
numClasses = 4; layers = [ imageInputLayer([224 224 3], 'Name', 'input') convolution2dLayer(3, 16, 'Padding', 'same', 'Name', 'conv1') batchNormalizationLayer('Name', 'bn1') reluLayer('Name', 'relu1') maxPooling2dLayer(2, 'Stride', 2, 'Name', 'pool1') convolution2dLayer(3, 32, 'Padding', 'same', 'Name', 'conv2') batchNormalizationLayer('Name', 'bn2') reluLayer('Name', 'relu2') maxPooling2dLayer(2, 'Stride', 2, 'Name', 'pool2') convolution2dLayer(3, 64, 'Padding', 'same', 'Name', 'conv3') batchNormalizationLayer('Name', 'bn3') reluLayer('Name', 'relu3') maxPooling2dLayer(2, 'Stride', 2, 'Name', 'pool3') fullyConnectedLayer(128, 'Name', 'fc1') dropoutLayer(0.5, 'Name', 'dropout') fullyConnectedLayer(numClasses, 'Name', 'fc2') softmaxLayer('Name', 'softmax') classificationLayer('Name', 'classif')];解释一下几个关键设计。卷积核大小选了3×3,这是目前公认性价比最高的选择,两个3×3卷积串联能达到一个5×5卷积的感受野,但参数更少、非线性更强。卷积通道数从16增加到32再到64,逐层扩大特征抽象度,底层学到的特征数量少是正常的,越往高层越需要更多容量去组合复杂语义。
这里特意加了batchNormalizationLayer。它做的事情是在每次训练迭代中对特征做标准化,让每一层的输入分布更稳定。我用过不带BN的版本,训练曲线明显比较挣扎,加了BN之后收敛速度快了不止一倍。dropoutLayer(0.5)是在训练时随机丢弃一半神经元的输出,强制网络不要把赌注全押在某几个神经元上,对防止过拟合很有用。
全连接层中间的维度取了128,不需要太大。如果数据量有限,过大的全连接层反而容易把训练集的特征死记下来,泛化能力变差。
3.3 训练参数怎么定
训练参数设置看起来像玄学,其实背后都是有逻辑的。下面这份配置是我多次跑下来比较稳的起点:
options = trainingOptions('adam', ... 'InitialLearnRate', 1e-3, ... 'MiniBatchSize', 32, ... 'MaxEpochs', 30, ... 'ValidationData', augimdsVal, ... 'ValidationFrequency', 20, ... 'Shuffle', 'every-epoch', ... 'Plots', 'training-progress', ... 'Verbose', false); net = trainNetwork(augimdsTrain, layers, options);adam是目前分类任务默认安全的选择,它能根据每个参数的历史梯度自适应调整学习步长,基本不用手动做学习率衰减。初始学习率1e-3是adam在大多数图像任务里的甜点值。如果loss在震荡,先用1e-4试;如果收敛太慢,谨慎上调到3e-3,不要拍脑袋。
MiniBatchSize取32,这个值对显存和训练稳定性都比较友好。取太大会导致显存溢出,取太小则每个batch的梯度方向噪声太大,loss曲线容易抖动。MaxEpochs取30,每个epoch表示把所有训练数据完整过一遍。对于小数据集,30轮足够看出趋势,还没收敛再加就行。
ValidationFrequency设成20的意思是每20次迭代在验证集上评估一次。Shuffle设成every-epoch,保证每个epoch里batch的组成不同,让训练更稳定。
训练过程不要太久,中等配置的显卡大约几分钟到十几分钟。训练结束后会出现实时曲线,你可以直接看到训练准确率和验证准确率的走向。
4. 训练、评估与调优
4.1 怎样判断训练过程是否健康
训练进度图里两个最重要的东西是训练集的loss曲线和验证集准确率曲线。健康的状态是loss平稳下降然后趋平,验证准确率同步上升。常见的不健康情况有两种。
第一种是loss下降得很慢或者纹丝不动。先检查学习率是不是过小,再看数据归一化是否正常,最后检查标签有没有错乱。第二种是训练准确率很高比如98%,但验证准确率一直停在80%出头,这个就是典型的过拟合,模型把训练集的特征背下来了,换个场景就不认识。对策是增加数据增强强度、加大dropout比例、减少全连接层神经元数,或者干脆增加训练数据量。
还有一条经验值得记住:验证准确率曲线如果剧烈震荡,通常说明batch太小,或者验证集样本太少。验证集每类只有三五十张时,随机抽一个batch的评估结果波动就会很明显。适当增大MiniBatchSize或者补充验证集样本能缓解。
提示:训练过程中不要频繁中断去改参数。先让一组配置完整跑完,看整体趋势再决定怎么调,中途反复修改很难判断到底是哪个改动起作用了。
4.2 测试集评估与混淆矩阵分析
训练完之后,真正考验模型的是没参与训练的测试集。用下面的代码做最终评估:
[labelsPred, scores] = classify(net, augimdsTest); labelsTrue = imdsTest.Labels; accuracy = mean(labelsPred == labelsTrue); fprintf('测试集准确率: %.2f%%\n', accuracy * 100); figure; confusionchart(labelsTrue, labelsPred);混淆矩阵是这个环节最有价值的输出。它是一个四行四列的表格,行代表真实类别,列代表预测类别,对角线上的数字是正确分类的数量,非对角线位置则显示哪些类别之间发生了混淆。
我在实际测试中见过最多的混淆是“玻璃瓶”和“塑料瓶”,这两个类别在透明材质和反光特性上有很强的相似性,如果训练数据里没有足够的瓶身细节差异,模型确实容易看走眼。另一个常见混淆是“其他垃圾”里的脏纸巾和“厨余垃圾”,都偏软、偏皱、偏暗色。看到混淆矩阵之后,针对混淆严重的类别补充样本,比盲目调网络参数有效得多。
如果还想看得更细,可以计算每个类别的精确率、召回率和F1分数。精确率是“模型预测为该类别的样本中有多少真的属于该类别”,召回率是“这一类别的样本有多少被正确找回来”。对于垃圾分类场景,我个人更看重召回率,因为“把有害垃圾漏认成其他垃圾”比“把其他垃圾误判成有害垃圾”风险更大,漏掉有害废弃物是大忌。
4.3 调优的优先级顺序与迁移学习
如果测试集准确率不理想,调优顺序要讲章法。我建议按数据→网络结构→训练参数→迁移学习这个顺序来,不要一上来就换大网络。
第一步永远是回头检查数据。每类样本数量是否均衡、拍得太像的图片是否混入了错误标签、背景干扰是否过大。数据问题不解决,改什么都白搭。第二步才是微调网络结构,比如在卷积层中间再加一层、调整滤波器数量。第三步是重新扫描训练参数。
如果还觉得不够,用迁移学习是提升效果最快的手段。Matlab里内置了多个预训练网络,比如ResNet-18、ResNet-50、GoogLeNet,这些网络在海量图片上预训练过,已经学会了非常通用的视觉特征。我们的做法是保留前面所有卷积层,只把最后几层替换成适合我们类别数的结构,然后在垃圾分类数据上微调。
迁移学习的大致代码结构如下:
net = resnet18; lgraph = layerGraph(net); % 找到原来的全连接层和分类层,替换成新的层 newFc = fullyConnectedLayer(numClasses, 'Name', 'fc_new'); lgraph = replaceLayer(lgraph, 'fc1000', newFc); newClass = classificationLayer('Name', 'classif_new'); lgraph = replaceLayer(lgraph, 'ClassificationLayer_fc1000', newClass); % 训练时可以把学习率调低一些,比如 1e-4 % 因为预训练特征已经足够好,不需要大幅变动用迁移学习之后,小数据集上的准确率往往能再提五到八个百分点,但代价是训练时间和内存占用显著增加。先用小型CNN把整个流程跑通,再考虑要不要上迁移学习,是我比较推荐的做法。
5. 常见问题与排查实录
5.1 训练过程中高频出现的报错
我在复现和帮别人调试这类项目时,遇到最多的几个问题总结如下,基本覆盖了90%的情况。
| 报错现象 | 排查思路 | 解决方案 |
|---|---|---|
| 提示图像尺寸不一致 | 数据集里可能有不同宽高比或通道数的图片 | 统一用augmentedImageDatastore处理;检查灰度图需转成三通道 |
imageDatastore找不到数据 | 文件夹路径或类别子目录名有误 | 确保根目录下每个子目录对应一个类别,确认路径书写正确 |
| 显存不足 | 图片尺寸大、batch大、网络参数量大 | 降低MiniBatchSize到16或8;缩小输入尺寸到128×128 |
| loss从头到尾几乎不下降 | 学习率过低、数据归一化异常、标签错乱 | 检查学习率,查看原始图片和标签是否对应 |
| 训练准确率100%但验证准确率很低 | 过拟合 | 增强数据增强、加大dropout、减少全连接层参数 |
一个很容易踩的坑是:当你自己爬图片或者拍照片建数据集时,有些照片是中转图片,有些是黑白图片,还有可能混入非垃圾类的生活照。这些脏数据会让网络完全学偏。建议在训练前写一个小脚本,把读入图像的尺寸、通道数、类别分布打印出来看一眼,清理干净再用。
5.2 识别效果不佳时的排查清单
模型训练完,放到新图片上测试,发现效果与预期不符。我总结了一个排查清单,按顺序过一遍往往能找到问题所在。
先看是不是测试图片和训练图片的分布差异太大。训练时拍的都是室内白光、平视角度,测试时你在室外阳光下俯拍,模型自然认不出。解决方法是让训练数据尽量多样化,或者在增强里加入更多光照变化和角度变化。
再看数据集规模。每类只有几十张图片的话,再好的网络也救不回来,因为模式根本没有被充分学到。增补数据永远是最笨但最有效的方法。
还要看类别定义是否模糊。比如“其他垃圾”这个类别,它其实是“不属于前三类的都要扔到这里”,包含的东西五花八门,模型很难学出统一特征。如果项目允许,我建议把“其他垃圾”里的常见子类挑出来,比如“脏纸巾类”“一次性餐具类”,单独成类,识别逻辑会清楚很多。
最后一招是检查是否类别不平衡。某类样本是其他类的三倍时,模型倾向把不确定的样本预测成多数类。简单处理办法是给少数类多复制一些样本,让比例尽量均衡,或者利用Matlab的分类损失权重功能给少数类更高权重。
6. 模型保存、加载与部署扩展
6.1 模型保存与单张图片识别
训练完成后的net直接保存即可:
save('garbage_net.mat', 'net');之后使用就是加载模型并对单张图片做同样的预处理流程,然后调用classify。这里有一个经常出问题的细节:测试图片必须经过与训练时相同的前处理,也就是尺寸统一到224×224,并且图片要有三个通道。灰度图需要先转成RGB再识别。
单张图片识别的完整代码:
net = load('garbage_net.mat').net; img = imread('test_plastic_bottle.jpg'); if size(img, 3) == 1 img = repmat(img, [1 1 3]); end img = imresize(img, [224 224]); label = classify(net, img); disp(['识别结果: ', char(label)]);如果你有多张图片要批量识别,把它们放到一个文件夹里,再套一个循环逐张处理即可。如果要做实时摄像头识别,思路也是一样的,把摄像头采集的每一帧先做尺寸统一再调用classify,注意实时识别对速度有要求,小型网络比大网络有明显优势。
6.2 部署与应用方向
模型训练完并不代表项目结束,把它用起来才是真正的完成。Matlab里导出的模型可以部署到多种环境:导出为ONNX格式,方便在其他深度学习框架里加载;用Matlab Coder生成C/C++代码,移植到嵌入式设备;也可以直接在Matlab里做简单的桌面演示程序。
回到垃圾分类这个场景,我觉得最有价值的扩展方向是“从识别到联动”。让模型识别出垃圾类别后,继续控制对应垃圾桶的仓门开关,形成一个完整的智能分类终端。这个方向不需要特别高深的额外技术,本质就是把图像识别模型接到一个控制流程里,但它展示的工程完整性非常打动人。如果你做的是课程设计或者毕设,这个小小的联动功能会让整个项目立刻上一个档次。
另一个扩展方向是边角料利用:把识别错误的图片保存下来,定期整理成“难例集”,加入训练数据重新训练。模型就是这样一点点变强的,有真实的反馈回路比任何调参技巧都管用。
最后说一个我在这个项目里最深的体会:很多人拿到任务第一件事就是改网络结构,但我自己做了几个版本之后发现,数据集整理和增强策略对结果的提升远比盲目增加卷积层数更明显。有一次我只增加了一条“不同光源角度拍摄”的数据增强规则,测试集准确率就提高了两个多百分点,而换成更大的网络,提升几乎可以忽略。另一个小技巧:保存好训练好的模型和测试脚本,每次调整数据后重新训练前,先看一眼混淆矩阵,你会发现错在哪、该补什么数据,答案都在里面。垃圾分类识别这个项目麻雀虽小五脏俱全,把每一环节都做扎实,你会收获一套可以复用到任何图像分类任务的完整方法论。