简介:这是一份面向Python初学者与计算机视觉入门者的图像识别实践资源,聚焦垃圾分类这一典型多分类应用场景,提供基于深度残差网络(ResNet)的端到端解决方案。资源包含7个文件,涵盖3个Jupyter Notebook(分别完成数据处理、模型训练与测试)、1个XMind流程图(清晰呈现系统整体架构与模块依赖)、1个核心模型定义Python脚本、1个使用说明文本及1个编译缓存文件,总大小仅415KB,轻量易部署。已有21046人学习下载,印证其在教学与实训中的广泛适用性。读者可直接复现完整流程:从原始图像预处理、ResNet模型构建与迁移学习训练,到实际样本预测与结果可视化;配套流程图与分步Notebook设计显著降低理解门槛,特别适合课程设计、毕业设计或Kaggle式入门项目快速上手。
1. 项目概述:图像识别垃圾分类到底在做什么
1.1 从一个很实在的痛点说起
去年家里换了一批新垃圾桶,分四个颜色摆了一圈,孩子每次扔垃圾都站在桶前面发呆,拿个塑料瓶盯半天,最后问我"爸爸这个算可回收还是其他垃圾"。那一刻我就琢磨,与其让他背分类口诀,不如直接做一个能拍照识别的小工具。
这就是这个项目的原初动机:图像识别垃圾分类系统。用摄像头拍一张垃圾照片,程序告诉你它属于什么类别,同时给出投放建议。听起来是一个标准的图像分类任务,但真正做下来发现,里面涉及的东西远比"训练一个模型"要多:数据怎么整理、模型怎么选、识别结果怎么展示、甚至不同光线下的拍摄效果怎么处理,全是坑。
这套系统我完整地实现了一遍,代码全部开源在个人仓库里,下面把整个设计和实现思路一步步拆开来讲。无论你是刚入门深度学习想找个练手项目,还是打算做一个能落地的垃圾分类辅助工具,这篇文章的思路和代码应该都能直接用。
1.2 技术选型:为什么用图像识别这条路
垃圾分类在技术上有几种实现路径:条形码/二维码扫描、语音问答、传感器识别、图像识别。我选择图像识别作为主方案,理由很直接。
条形码扫描的优点是准确率高,但现实里垃圾不是都有条码,比如剩菜剩饭、破碎陶瓷、揉成一团的塑料袋,这些占了日常垃圾的大头,条码方案完全覆盖不了。语音问答本质是知识库查询,用户需要自己描述垃圾特征,体验很割裂。图像识别是最接近人类判断方式的手段,拍一张照片,机器"看"一眼就给结论,交互最自然。
当然图像识别不是万能的,它依赖拍摄质量,依赖训练数据覆盖的类别范围。所以我在设计系统时做了一个取舍:把识别范围锁定在家居日常最常见的六大类——可回收物、厨余垃圾、有害垃圾、其他垃圾,以及细分场景下的塑料瓶、纸箱、易拉罐等高频单品。宁可在范围内做深、做准,也不追大而全的泛泛识别。
1.3 系统整体架构一览
这整套系统的架构其实很简单,拆开来看就是三个模块:
- 数据层:图像采集、清洗、预处理、数据增强
- 模型层:基于迁移学习的图像分类模型
- 应用层:本地推理脚本、Web界面、安卓端适配
从技术栈上说,模型部分用Python + TensorFlow,图像处理用OpenCV,Web端用Flask搭了一个轻量服务,安卓端后期做了一个简化版调用入口。整条链路跑通之后,用户的操作路径就是"拍照/上传图片——服务端推理——返回分类结果和投放建议"。
这样的架构最大的好处是每个环节都可以独立替换。数据不好就换数据,模型精度不够就换更深的骨干网络,前端不喜欢可以完全重写。框架的耦合度控制在最低,方便后续维护和扩展。
2. 数据准备:垃圾分类模型的核心地基
2.1 数据集选型:公开数据集怎么选、怎么下载
做图像分类,第一步永远不是写模型,而是搞定数据。垃圾分类领域比较知名的公开数据集有 TrashNet、华为云垃圾分类数据集、垃圾分类图片数据集(GitHub上有多个版本)。我最终选了一个约5000张图片、覆盖40多个常见类目的数据集作为初始版本,再补充了少量自己拍的单品图片做边界修正。
选择数据集时有一个关键点容易被忽略:类目体系。有的数据集是"可回收/厨余/有害/其他"四分类,有的是50多类细分类目,还有的是按材质分(塑料/玻璃/纸类)。不同体系之间其实不能直接混用,因为模型的输出是固定维度的,训练时是四分类,推理时就只能输出四分类结果。我的做法是选一个和自己需求匹配的粗分类数据集作为基础,再对其中模糊样本做手工重标注。
下载数据集时还踩了一个小坑:某些公开数据集虽然标注了40多类,但很多类别的图片数量极不均衡,最多的几百张,最少的只有几十张。这种数据直接喂给模型,少数类基本学不出来。所以拿到数据后第一步不是训练,而是统计每个类别的样本量,决定是筛类别还是做数据增强补足。
2.2 图像预处理:统一尺寸、归一化和标注检查
模型训练前,图像预处理是限流的关卡。无论原始图片来自手机拍摄还是网络素材,第一件事就是统一尺寸。我用的MobileNetV2默认输入尺寸是224x224,所以把所有训练图片都resize到224x224。这里有一个容易纠结的点:直接resize会变形怎么办?其实在图像分类任务里,短边缩放加中心裁剪比直接拉伸更主流。
import cv2 import os def preprocess_image(img_path, target_size=(224, 224)): img = cv2.imread(img_path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) h, w = img.shape[:2] # 短边缩放,再中心裁剪,避免直接拉伸导致的形变 if h < w: new_h = target_size[0] new_w = int(w * (target_size[0] / h)) else: new_w = target_size[1] new_h = int(h * (target_size[1] / w)) img = cv2.resize(img, (new_w, new_h)) start_x = (new_w - target_size[1]) // 2 start_y = (new_h - target_size[0]) // 2 img = img[start_y:start_y + target_size[0], start_x:start_x + target_size[1]] # 归一化到 [0, 1] img = img.astype('float32') / 255.0 return img这段代码里用到了OpenCV读取图片后转RGB(OpenCV默认是BGR,不转的话模型看到的就是颜色错乱的图),然后短边缩放加中心裁剪,最后归一化到0到1之间。
除了尺寸问题,我在整理数据时还发现一个常见问题:数据集中存在少量标注错误,比如矿泉水瓶被标成"玻璃瓶"。这类噪声样本在几百张数据里看起来不多,但确实会影响模型收敛。我的处理办法是训练前做了两轮人工抽检,每类随机抽30张,肉眼确认标注是否靠谱。这个工作很枯燥,但对最终效果有很大帮助。
2.3 数据增强:小数据集也能练出稳模型
垃圾分类这个场景下,训练数据的数量普遍不够。这个时候如果直接硬训,效果大概率不理想,所以数据增强是必须做的。我用的增强手段包括随机旋转、水平翻转、亮度对比度扰动、随机裁剪、添加高斯噪声,组合出5倍以上的训练样本。
代码用的是TensorFlow/Keras内置的ImageDataGenerator,好处是训练时在线增强,不占额外磁盘空间。
from tensorflow.keras.preprocessing.image import ImageDataGenerator datagen = ImageDataGenerator( rotation_range=20, width_shift_range=0.15, height_shift_range=0.15, brightness_range=[0.7, 1.3], zoom_range=0.2, horizontal_flip=True, fill_mode='nearest', validation_split=0.2 )这里有一个我反复调过的细节:brightness_range的范围不要设太大。一开始我图省事设了[0.5, 1.5],结果模型在正常光线下识别率反而下降了。原因很简单,增强后图片过度偏离真实分布,模型学到了"暗光模式",到了正常拍摄场景就不适应了。后来把范围收敛到[0.7, 1.3],效果马上好起来。
还有一个tips:厨余垃圾这一类的识别难度比可回收物高不少,因为剩菜叶子在增强变换下形状变化大,颜色又是绿黄交错。针对这类难样本,我在增强时额外增加了色彩抖动,把hue通道做小范围随机偏移,这个方法对厨余类的识别提升明显。
3. 模型训练:迁移学习是最高效的路径
3.1 骨干网络选型:MobileNetV2还是ResNet50
垃圾分类的识别目标本质上还是图片分类,目前主流做法是用预训练模型做迁移学习。我在项目里对比了两条路线:MobileNetV2和ResNet50。
先看计算量。MobileNetV2的参数量约3.4M,计算量约300M FLOPs,ResNet50参数量约25.6M,计算量约4.1G FLOPs。两者在ImageNet上的top-1准确率相差不到2个百分点,但在资源占用上MobileNetV2优势巨大。如果后续要部署到手机端或树莓派上,MobileNetV2几乎是当然选择。
这里要说明一下,选MobileNetV2并不是因为它最强,而是因为垃圾分类这个任务本身不需要特别强的特征提取能力。垃圾图片和ImageNet里的自然图像相比,类别间差异相对明显(矿泉水瓶和香蕉皮无论在颜色还是纹理上都差很多),所以一个轻量级骨干网络就够了。你要是非得上ResNet50或者EfficientNet,识别率可能会高一点点,但训练和推理成本翻几倍,性价比不高。
最终我选定的方案是:MobileNetV2作为骨干网络,去掉原始分类层,保留全局平均池化,接两个全连接层做微调。第一层做降维,第二层输出分类概率。
3.2 训练参数配置与损失函数选择
整个模型结构定义参考了TensorFlow官方的迁移学习教程,但在几个关键参数上做了个人化的调整:
- 优化器:Adam,初始学习率1e-4
- 损失函数:CategoricalCrossentropy
- Batch Size:32
- Epochs:第一阶段冻结骨干网络训练20轮,第二阶段解冻部分层再训练20轮
- 早停策略:监控验证集loss,连续5轮不下降就停止
分段训练是我认为这个项目里最值得强调的经验。第一阶段冻结骨干网络,因为预训练权重在ImageNet上已经学到了通用的边缘、纹理等低级特征,不需要重新学;第二阶段解冻最后几层,让模型针对垃圾分类数据微调高层语义特征。这样的训练方式比从头训练收敛快得多,而且不容易过拟合。
代码中用了两段式训练:
from tensorflow.keras.applications import MobileNetV2 from tensorflow.keras.layers import GlobalAveragePooling2D, Dense, Dropout from tensorflow.keras.models import Model base_model = MobileNetV2(weights='imagenet', include_top=False, input_shape=(224, 224, 3)) # 第一阶段:冻结所有骨干层 base_model.trainable = False x = base_model.output x = GlobalAveragePooling2D()(x) x = Dense(128, activation='relu')(x) x = Dropout(0.5)(x) predictions = Dense(num_classes, activation='softmax')(x) model = Model(inputs=base_model.input, outputs=predictions) model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])值得提醒的是Dropout层级在这里的作用。早期版本我没有加Dropout,训练时准确率能到95%以上,但一到验证集就掉到80%出头,典型的过拟合信号。加了0.5的Dropout之后,验证集准确率稳在了88%左右,虽然训练集准确率下降了,但模型的泛化能力反而上来了。
另外,类别不平衡问题我用class_weight做了加权处理。样本量少的类别(比如有害垃圾中的电池)在损失函数中自动获得更高的权重,防止模型"偷懒"把所有样本都预测为大类。
3.3 训练过程监控:准确率不代表一切
训练过程中我不仅看准确率,更看重验证集loss曲线。准确率是一个离散指标,当模型在某个batch里只错了一两张图,准确率可能就从90%蹦到95%,曲线抖动很大;loss是连续指标,能更平滑地反映模型状态。
实际训练中监控到的一个典型现象是:第一阶段训练结束后,验证集loss降到0.5左右,然后进入平台期;第二阶段解冻骨干网络后,loss快速下降再缓慢收敛。这个过程中最容易犯的错误是在第一阶段训练过头,导致骨干网络的底层特征被大幅度破坏。所以第二阶段解冻时,我把骨干网络的学习率降到1e-5,远远小于新加分类层的1e-4。这样做是为了防止反向传播时梯度过大,把预训练学到的良好特征抹掉。
训练完的最终模型在验证集上top-1准确率为89.2%,top-5准确率超过96%。对常见的塑料瓶、纸箱、易拉罐识别效果尤其好,基本能到95%以上。
4. 系统实现:从模型到用户可用的应用
4.1 推理模块封装:把模型和预处理打包成管线
模型训练好之后,最重要的就是写一个干净易用的推理接口。我在项目中封装了一个GarbageClassifier类,把模型加载、图片预处理、类别映射、置信度输出全包在里面,对外只需要一行调用。
import numpy as np from tensorflow.keras.models import load_model class GarbageClassifier: def __init__(self, model_path, class_names): self.model = load_model(model_path) self.class_names = class_names def predict(self, image_array): # image_array 是 (H, W, 3) 的RGB数组 img = cv2.resize(image_array, (224, 224)) img = img.astype('float32') / 255.0 img = np.expand_dims(img, axis=0) probs = self.model.predict(img, verbose=0)[0] top_idx = np.argsort(probs)[::-1] results = [] for idx in top_idx[:3]: results.append({ 'class': self.class_names[idx], 'probability': float(probs[idx]) }) return results返回前三个最可能的类别和概率而不是只返回最高概率,这个设计是我在实际使用后加的。因为单个类别预测经常有误判,给用户展示前三个候选项,用户自己一眼就能判断哪个是对的,准确率体验瞬间提升不少。
另外,置信度阈值值得多说一句。模型对某些图片的预测概率普遍偏低(比如所有类别的概率都在30%以下),这个时候系统应该明确告诉用户"置信度不足,请重新拍一张",而不是强行输出一个最可能的答案。我在系统里把阈值设为0.45,低于这个值的直接提示重拍。实测下来,这比"永远给一个答案"的体验要靠谱得多,因为用户更讨厌错误答案,而不是"我不确定"。
4.2 Web界面:Flask五分钟搭建图片上传服务
为了让系统能实际用起来,我写了一个Flask网页服务,用户打开浏览器就能上传图片并获取识别结果。页面没有过度设计,一个文件上传框、一个预览区域、一个结果卡片,重点是把功能和交互做顺。
from flask import Flask, request, jsonify, render_template import base64 app = Flask(__name__) classifier = GarbageClassifier('models/garbage_mobilenetv2.h5', class_names) @app.route('/') def index(): return render_template('index.html') @app.route('/predict', methods=['POST']) def predict(): if 'file' not in request.files: return jsonify({'error': 'no file uploaded'}), 400 file = request.files['file'] file_bytes = np.frombuffer(file.read(), np.uint8) img = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) results = classifier.predict(img) return jsonify({'results': results, 'top_class': results[0]['class']})前后端分离是另一个选择,但在这个项目里我直接用Flask模板渲染,因为逻辑简单,没必要拆成两个服务。
有一个细节:前端上传的图片直接是二进制流,我用了cv2.imdecode而不是cv2.imread,原因在于前者能直接从内存中解码来自网络的图片,不需要先保存到本地临时文件。如果你用imread,Flask上传的文件还需要先存入磁盘再读取,多了一步IO,处理高并发时效率会差。
4.3 安卓端的窗口图像识别调用
做完整套Python服务后,我又想把识别能力扩展到手机上。这里选择了一条最轻量的路线:安卓端不内置模型,而是调用后端HTTP接口完成识别。
安卓端核心代码其实就几行。用OkHttp把图片Base64编码后POST到服务器,拿到JSON结果再解析。界面上用Android的Intent调起系统相机,拍照后把图片送到识别接口。
// 安卓端关键代码:调起相机拍照并上传识别 private void takephotoAndRecognize() { Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) { Bundle extras = data.getExtras(); Bitmap imageBitmap = (Bitmap) extras.get("data"); uploadToServer(imageBitmap); } }如果你做的是安卓端的窗口图像识别(也就是在手机屏幕上实时框选识别),流程是相似的,只是把"拍照"换成"截取当前屏幕窗口",两者在识别环节没有本质差异。
不过这里遇到一个典型的工程问题:手机摄像头拍出的照片分辨率远高于模型输入尺寸。手机拍一张是4000x3000像素,模型只接收224x224,直接缩大会丢失大量细节。我做了两步优化:先把图片等比缩放到短边不超过800像素,再做中心裁剪到224x224,这样既保留主体信息,又不会因为原图过大导致数据传输缓慢。
5. 常见问题与排查技巧实录
5.1 识别准确率上不去?先别急着换模型
这是被问到最多的问题。我总结了一套系统的排查步骤:
第一,验证数据。随机抽一部分训练集图片单独跑一次预测,如果训练集准确率都不高(比如低于90%),说明模型容量不足或数据标注有问题,换更大的模型才有意义;如果训练集准确率很高但验证集很低,才说明是过拟合,应该做数据增强或加正则化。
第二,检查类别分布。统计一下每个类别的样本量,如果某几类样本极少,准确率低是正常的。解决办法除了采集更多数据,也可以用class_weight,或者在评估时只看每类单独的recall而不是总accuracy。
第三,分析错误样本。把预测错的图片全部打印出来,按类别分组观察。我这边遇到过一个有趣的情况:模型总是把"陶瓷碗"识别成"塑料碗"。看了几十张错误图,发现训练数据里的陶瓷碗大多带花纹,而塑料碗大多是纯色的。模型其实是学到了"有没有花纹"这个特征而不是材质。后来补充了一批纯色陶瓷碗的图片,问题才解决。
这个案例说明:模型学到的特征很可能和我们以为的特征不是一回事。遇到这种问题不要盲目调参,先把错误样本看清楚。
5.2 训练速度慢、显存不足怎么处理
如果是个人电脑训练,GPU显存一般比较紧张。我的做法是:
- 把Batch Size从64降到16,显存占用大幅下降,收敛速度虽然慢了但不会OOM
- 用mixed_precision混合精度训练,速度提升约30%,显存占用减半
- 图片读入用TFRecord或者直接用tf.data的缓存机制,避免重复IO
如果你没有GPU,纯CPU训练也不是不能跑,但要把骨干网络换成更轻的MobileNetV3-Small,并且减少epoch数量。我自己早期用CPU训练一个epoch大约需要8分钟,完整训练要5个多小时,后来换了GPU后压缩到20分钟。做图像分类项目,有个支持CUDA的显卡还是能省不少时间。
5.3 模型在本地环境好使,部署到别的机器就崩
这个问题的常见原因有两个。第一,模型训练时的图片预处理和推理时的预处理不一致。训练时我用datagen做了归一化到0到1,推理脚本里忘了做同样处理,输入直接变成了0到255范围的值,模型输出自然一塌糊涂。这个问题排查了很久才发现,因为代码单独看都没毛病,但训练和推理的预处理逻辑没有对齐。
第二,依赖版本不一致。TensorFlow 2.x版本之间模型文件兼容性整体没问题,但不同小版本的算子实现有差异,建议部署环境使用和训练环境完全相同的TensorFlow版本,并且在跑模型前先跑一次简单的推理验证。
我把整个部署环境用requirements.txt固定了版本,并且在Docker镜像里打包,这样无论部署到哪个机器,环境都是一致的。
5.4 推理耗时长,如何优化到实时可用
这个项目目前的推理时间在CPU上大约是150ms一张图,GPU上大约20ms,要进一步提高可以考虑量化。TensorFlow Lite的量化可以把模型压缩到原来的四分之一大小,推理速度提升2-3倍,精度损失在1%以内。
# 将H5模型转换为TFLite并进行INT8量化 tflite_convert \ --saved_model_dir=./saved_model \ --output_file=./garbage_model.tflite \ --optimizations=default量化的模型在手机上可以直接用TensorFlow Lite Interpreter运行,不需要网络请求,延迟降低到50ms以内,对实时识别场景很友好。
6. 项目复盘:做这套垃圾分类系统踩过的关键坑
第一,数据质量永远重于模型结构。我在项目前期花了大量时间在数据清洗上,当时感觉很浪费时间,回头看不亏。模型结构可以抄现成的,但数据质量只能自己的把关,它是决定模型上限的关键因素。
第二,加功能前先想清楚用户体验。我一开始想做一个"实时视频流识别"的功能,做完发现,用户拿着手机对着垃圾扫来扫去,画面不停跳变,识别结果来回翻,反而不好用。后来改成"拍一张照识别一次",体验明显好很多。
第三,不要太迷信公开数据集。公开数据集的图片大多是在统一场景下拍的,背景干净、光线均匀。真实使用中,用户可能在昏暗的楼道里拍,可能在强光下逆光拍,这些情况公开数据集覆盖不到。想做好真实场景的识别,必须自行采集一批贴近使用环境的数据来微调模型。
第四,这个项目还有不少扩展空间。目前做的是单张图片分类,后续可以加入目标检测能力,在一张图里同时识别出多个垃圾物体;也可以接入语音播报功能,识别结果直接读出来;还可以把识别记录保存下来,统计分析家庭垃圾的组成和变化趋势,这个对环保教育有很实际的意义。
最后再说一个我个人的体会:图像识别项目做得好不好,很多时候不在模型那一环,而在于你有没有把整个链条的细节处理好。任何一个环节出了偏差,最后的表现都会打折。希望这篇拆解能帮你少踩一些我已经踩过的坑,直接走通"拍摄—识别—推荐投放"这条完整链路。
本文还有配套的精品资源,点击获取