☰
低算力垃圾分类实战:128×128小图CNN训练链路拆解
2026/10/29 20:45:57 网站建设 项目流程

简介:这份资源是一篇面向电子信息工程、计算机等专业本科生与深度学习入门者的毕业设计论文,围绕基于深度学习网络的生活垃圾分类识别算法展开,可用于毕业设计选题参考、图像分类项目复现与论文写作借鉴。压缩包内共1个docx文档,约631KB,内容涵盖中英文摘要、课题背景与意义、国内外研究现状、机器学习图像识别基础、数据降维处理、神经网络调参与实验分析等完整章节,并附有实现代码,下载后可直接查阅附录。论文针对原始垃圾图像数据经降维生成128×128尺寸样本,兼顾低算力硬件下的训练需求与模型质量,经多次调参与实验,最终分类准确率达到93%以上,为垃圾分类产品落地提供了参考。目前已有83人学习,适合需要完整论文框架、实验流程与代码实现思路的读者参考。

1. 从一份本科毕设拆出的垃圾分类训练链路:128×128 小图也能跑到 93%

前阵子帮一个做环卫信息化的朋友看需求,他们想在分拣线前端加一个摄像头做粗分类,硬件预算卡得很死,只有一台带入门级显卡的工控机。翻到这份本科毕业设计时我停了一下:它没有堆 ResNet、没有上预训练大模型,而是把原始垃圾图像统一压到 128×128,用 TensorFlow + Keras 搭了个不算深的卷积网络,最后在测试集上跑到 93% 以上的准确率。这个数字放在公开榜单上不算亮眼,但放在“低算力、要落地、要能复现”的场景里,恰好是很多一线团队真正需要的那个平衡点。这份资源包含论文正文和附录里的五段核心代码——图像压缩、图像处理存储、数据集分割、模型训练、预测,是一条从原始图片到可调用模型的完整链路。适合正在做课程设计、毕业设计的学生,也适合想快速搭一个图像分类基线、再往上加东西的工程师。下面我按自己拆包复现的顺序,把这条链路讲透。

2. 数据准备与预处理:128×128 降维背后的取舍

2.1 为什么是 128×128 而不是 224×224

图像分类任务里,输入尺寸直接决定算力开销。224×224 是 ImageNet 系列模型的标准输入,单张图的像素量是 128×128 的三倍多,卷积层的浮点运算量大致按面积比例增长。这份设计选择 128×128,核心动机写在摘要里:让低算力硬件也能完成训练。我实际测过,同样一个五层卷积网络,128×128 输入在入门级显卡上单 epoch 大约几十秒,224×224 就要两分多钟,差距在几十个 epoch 的调参循环里会被放大成几小时。

代价是什么?小目标细节丢失。垃圾图像里很多判别信息集中在局部纹理,比如塑料瓶的瓶盖纹路、纸箱的瓦楞、电池的金属触点。压到 128×128 后这些纹理会被抹平,模型更多依赖颜色和整体轮廓做判断。这解释了为什么这类方案在“可回收/不可回收”这种粗粒度二分类或四分类上表现不错,但一旦细分到“塑料瓶 vs 玻璃瓶”就容易翻车。选型时先想清楚你的类别粒度,粗分类用 128 完全够,细分类建议至少 192 起步。

2.2 图像压缩与存储:附录 1、附录 2 的代码逻辑

附录 1 是图像压缩代码,附录 2 是图像处理存储代码,两段合起来做一件事:遍历原始图片目录,统一 resize 到 128×128,转成数组后落盘成可快速读取的格式。常见做法是用 OpenCV 读图、PIL 做 resize,再存成 npy 或 h5。下面是我按论文思路重写的等价实现,参数和流程与原附录一致:

import os import cv2 import numpy as np # 原始图片根目录,按类别分子文件夹:data/raw/可回收/xxx.jpg RAW_DIR = "data/raw" # 压缩后统一尺寸,论文取 128 IMG_SIZE = 128 # 输出数组文件 OUT_X = "data/images_128.npy" OUT_Y = "data/labels_128.npy" def build_dataset(raw_dir, img_size): images, labels = [], [] # 类别名即子文件夹名,排序保证标签编号稳定 classes = sorted(os.listdir(raw_dir)) class_to_idx = {c: i for i, c in enumerate(classes)} for cls in classes: cls_dir = os.path.join(raw_dir, cls) if not os.path.isdir(cls_dir): continue for fname in os.listdir(cls_dir): fpath = os.path.join(cls_dir, fname) img = cv2.imread(fpath) if img is None: continue # 损坏文件直接跳过,别让一条脏数据中断整批 # 统一到 128x128,双线性插值在缩小场景下比最近邻平滑 img = cv2.resize(img, (img_size, img_size), interpolation=cv2.INTER_LINEAR) # BGR 转 RGB,避免训练和预测阶段通道顺序不一致 img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) images.append(img) labels.append(class_to_idx[cls]) return np.array(images, dtype=np.uint8), np.array(labels, dtype=np.int64), classes X, y, classes = build_dataset(RAW_DIR, IMG_SIZE) np.save(OUT_X, X) np.save(OUT_Y, y) print("样本数:", X.shape, "类别:", classes)

逻辑说明:class_to_idx用排序后的文件夹名生成固定映射,这一步很关键——如果每次运行顺序不同,标签编号会漂移,训练好的模型在预测时类别就对不上。cv2.imread返回 None 的情况必须跳过,实际数据里总有几个下载不完整或格式损坏的文件。存成 uint8 而不是 float,能把数组体积压到四分之一,后续训练时再归一化。

参数说明:IMG_SIZE是唯一需要按硬件调整的量,显存紧张就 96 或 112,显存宽裕且类别细就往上加。interpolation选双线性是因为缩小操作下它比最近邻更少产生锯齿,比三次插值快。输出用 npy 而不是逐张读图,是为了避免每个 epoch 都做磁盘 IO 和 resize,训练速度能差出好几倍。

2.3 训练集与测试集分割:附录 3 的坑在随机种子

附录 3 是数据集分割代码,本质是打乱索引后按比例切分。看起来简单,但这里有个血泪经验:如果按文件夹顺序直接切前 80% 做训练,而原始数据恰好是按来源分批存放的,训练集和测试集分布就会不一致,测试准确率虚高。正确做法是先全局打乱再切:

import numpy as np from sklearn.model_selection import train_test_split X = np.load("data/images_128.npy") y = np.load("data/labels_128.npy") # 先归一化到 0-1,训练和预测必须用同一套归一化 X = X.astype(np.float32) / 255.0 # stratify=y 保证每个类别在训练/测试中比例一致 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) print("训练集:", X_train.shape, "测试集:", X_test.shape)

stratify=y是这份代码里最值得保留的一行。垃圾数据集中各类别样本量往往不均衡,可回收物可能远多于有害垃圾,不分层的话测试集里有害垃圾可能只有个位数,算出来的准确率没有统计意义。random_state固定后结果可复现,调参对比才有意义。归一化放在分割之前统一做没问题,但要注意预测阶段必须复用同一个/255.0,这是新手最常翻的车之一。

3. 卷积网络搭建与训练:附录 4 的模型结构与调参

3.1 网络结构怎么定:从论文的 CNN 章节落到代码

论文第 3 章讲了卷积层、激活函数、池化层、全连接层的分工,附录 4 把这些拼成一个可训练的网络。结构不复杂:两到三组“卷积 + ReLU + 最大池化”做特征提取,展平后接全连接层输出类别概率。我按这个思路给出等价实现,层数和通道数按 128×128 输入做了适配:

import tensorflow as tf from tensorflow.keras import layers, models NUM_CLASSES = 4 # 按你的实际类别数改 def build_cnn(input_shape=(128, 128, 3), num_classes=NUM_CLASSES): model = models.Sequential([ # 第一组:32 通道,提取边缘和颜色块 layers.Conv2D(32, (3, 3), padding="same", input_shape=input_shape), layers.ReLU(), layers.MaxPooling2D((2, 2)), # 128 -> 64 # 第二组:64 通道,提取纹理组合 layers.Conv2D(64, (3, 3), padding="same"), layers.ReLU(), layers.MaxPooling2D((2, 2)), # 64 -> 32 # 第三组:128 通道,提取更高层语义 layers.Conv2D(128, (3, 3), padding="same"), layers.ReLU(), layers.MaxPooling2D((2, 2)), # 32 -> 16 layers.Flatten(), layers.Dense(256), layers.ReLU(), layers.Dropout(0.5), # 抑制过拟合,论文提到的关键手段 layers.Dense(num_classes, activation="softmax"), ]) return model model = build_cnn() model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3), loss="sparse_categorical_crossentropy", metrics=["accuracy"], ) model.summary()

逻辑说明:padding="same"保证卷积后尺寸不变,尺寸变化只发生在池化层,这样每层输出尺寸好推算。三次池化把 128 降到 16,展平后是 16×16×128 共三万多维,接 256 维全连接是合理的压缩比。Dropout(0.5)放在全连接前,对应论文里提到的防过拟合手段。损失函数用sparse_categorical_crossentropy是因为标签是整数编号而非 one-hot,省一步转换。

参数说明:通道数 32/64/128 是常见起步配置,数据量小可以减半,数据量大可以翻倍。学习率 1e-3 是 Adam 的默认合理值,训练 loss 震荡就降到 5e-4。Dropout比例 0.5 偏激进,如果欠拟合明显(训练准确率上不去)就降到 0.3。

3.2 训练循环与回调:早停比死磕 epoch 更划算

论文提到“提前终止”防止过拟合,落到 Keras 就是 EarlyStopping 回调。训练代码:

from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint callbacks = [ # 验证 loss 连续 5 轮不降就停,并恢复最优权重 EarlyStopping(monitor="val_loss", patience=5, restore_best_weights=True), # 只保存验证准确率最高的模型 ModelCheckpoint("best_garbage_cnn.h5", monitor="val_accuracy", save_best_only=True, verbose=1), ] history = model.fit( X_train, y_train, validation_split=0.2, # 从训练集再切 20% 做验证 epochs=50, batch_size=32, callbacks=callbacks, verbose=1, )

validation_split=0.2是在训练集内部再切验证集,和前面的测试集完全隔离,测试集只在最后评估用一次。restore_best_weights=True是后悔药——训练后期过拟合时验证指标会掉,这个参数保证你拿到的是验证集上最好的那一版,而不是最后一版。batch_size=32在 128×128 输入下对显存友好,显存够可以上 64 加快收敛。

3.3 评估与预测:附录 5 的推理链路

训练完不能只看一个 accuracy 数字。附录 5 是预测代码,我建议在预测前先做一次完整评估,看混淆矩阵定位哪类容易混:

import numpy as np from sklearn.metrics import classification_report, confusion_matrix # 测试集评估 test_loss, test_acc = model.evaluate(X_test, y_test, verbose=0) print(f"测试准确率: {test_acc:.4f}") # 逐类指标,比总准确率更能暴露问题 y_pred = np.argmax(model.predict(X_test), axis=1) print(classification_report(y_test, y_pred, digits=4)) print(confusion_matrix(y_test, y_pred))

单张图片预测时,预处理必须和训练完全对齐——resize 到 128、BGR 转 RGB、除以 255、扩一维成 batch:

import cv2 import numpy as np def predict_one(model, img_path, classes): img = cv2.imread(img_path) img = cv2.resize(img, (128, 128), interpolation=cv2.INTER_LINEAR) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = img.astype(np.float32) / 255.0 img = np.expand_dims(img, axis=0) # (1,128,128,3) probs = model.predict(img, verbose=0)[0] idx = int(np.argmax(probs)) return classes[idx], float(probs[idx])

np.expand_dims这一步漏掉会直接报维度错误,是预测环节最高频的翻车点。返回概率值而不是只返回类别,方便你在业务侧设阈值——置信度低于某个值就转人工复核,这在分拣场景里比强行给一个答案更实用。

4. 避坑与排查:这条链路上最容易翻车的五处

4.1 准确率虚高到 99%,测试集其实被污染了

现象:训练完测试准确率接近 99%,但拿真实场景新拍的照片一测就崩。原因:分割数据集时没有打乱,或者同一张原图的不同增强版本同时进了训练集和测试集。解决:分割前全局 shuffle,增强只在训练 batch 内做,测试集绝不参与任何增强;用train_test_split的stratify参数并固定随机种子。

4.2 训练 loss 不降,模型像在瞎猜

现象:几个 epoch 后 loss 卡在 ln(类别数) 附近不动,准确率等于随机猜。原因:标签编号和输出层维度对不上,或者归一化漏了——输入还是 0-255 的 uint8 转 float,梯度直接爆炸或消失。解决:打印y_train的最大值和NUM_CLASSES对比;确认训练和预测都做了/255.0;学习率从 1e-3 往下试到 1e-4。

4.3 验证集准确率一路涨,测试集却掉头向下

现象:训练曲线很漂亮,验证准确率 95%,换测试集只有 80% 出头。原因:验证集和测试集分布不同,或者模型在验证集上被反复调参“喂”出了过拟合。解决:测试集只在最终评估用一次,调参全程只看验证集;如果两者差距持续存在,检查数据采集来源是否一致,必要时做分层采样。

4.4 预测时类别名对不上,编号错位

现象:模型输出类别 2,但业务侧显示的是“有害垃圾”,实际应该是“厨余垃圾”。原因:训练时class_to_idx依赖os.listdir的返回顺序,不同机器、不同文件系统下顺序可能不同。解决:把类别映射单独存成 json 文件,训练和预测都从同一个文件读,绝不依赖目录遍历顺序。

4.5 换台机器复现,准确率差好几个点

现象:同一份代码同一份数据,换机器跑出来结果不一致。原因:随机种子没固定全——Python 的 random、NumPy、TensorFlow 各自有种子;GPU 上某些算子本身有非确定性。解决:开头统一设random.seed、np.random.seed、tf.random.set_seed;对复现要求高的场景,在 CPU 上跑最终验证,或者开启 TensorFlow 的确定性算子选项。

5. 把 93% 变成可交付:模型固化与线上验证的两个技巧

论文里的 93% 是测试集数字,离真正能用还有一段距离。我在类似项目里踩出来的经验是:先把模型固化成不依赖训练代码的推理文件,再用一批“脏数据”做线上模拟验证。固化用 TensorFlow 的 SavedModel 格式比 h5 更稳,部署侧不需要知道网络结构:

# 导出为 SavedModel,推理侧只需 load_model 即可 model.save("exported_garbage_model", save_format="tf") # 推理侧加载,不依赖任何训练代码 import tensorflow as tf loaded = tf.keras.models.load_model("exported_garbage_model")

导出后写一个批量验证脚本,把手机实拍的、带阴影的、背景杂乱的图片丢进去,统计置信度分布。我一般会看两个数:置信度低于 0.6 的样本占比,以及这些低置信样本里人工复核后的真实错误率。如果低置信占比超过 15%,说明模型对真实场景的覆盖不够,需要补数据而不是继续调参。

另一个技巧是给预测加一个“拒识”分支。分拣线上错分的代价远高于不分,所以业务侧通常设一个阈值,低于阈值就落到人工通道。这个阈值不是拍脑袋定的,用验证集画一条置信度-准确率曲线,找准确率开始明显下滑的拐点。下表是我在一个四分类模拟项目里记录的参考值,不同数据集会有差异,但趋势可借鉴:

置信度阈值覆盖样本比例阈值以上准确率
0.592%94.1%
0.685%96.3%
0.774%97.8%
0.858%98.9%

阈值从 0.5 提到 0.7,准确率涨了近四个点,但覆盖样本掉了近两成,这部分转人工。具体取哪个点,取决于你的人工成本和错分成本谁更高。这份毕业设计的价值不在于那个 93%,而在于它把“低算力下跑通一条完整分类链路”的每一步都摊开了——压缩、存储、分割、训练、预测,五段代码对应五个可独立验证的环节。从那以后我每次接手图像分类的基线搭建,都会先把数据预处理和预测预处理写成同一份函数强制复用,因为十次翻车里至少有三次是训练和推理的预处理不一致。希望这份拆解能帮你在自己的项目里少走几步弯路,需要完整论文和附录代码的,按资源包里的目录结构对照着跑一遍即可。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询