☰
树莓派垃圾分类识别:TensorFlow神经网络端到端实战
2026/10/18 12:43:14 网站建设 项目流程

简介:面向树莓派与嵌入式AI实践者的一份完整垃圾分类识别项目,基于Python与TensorFlow/Keras搭建卷积神经网络,覆盖数据预处理、模型训练、评估与部署全流程。资源共131个文件,压缩包约112.45MB,核心包含py源码与ipynb示例、训练好的tflite模型、垃圾分类数据集图片(png/jpg)、前端交互页面(js/vue/html)及配套说明文档(md/json),便于按模块对照学习。已有2062人学习,适合想要从零实现图像分类并落地到树莓派的开发者。项目中不仅给出神经网络各层设计与优化技巧,还包含模型量化剪枝等边缘端部署思路,可借此掌握从数据集标注到实时识别的完整工程路径。

1. 树莓派上跑垃圾分类识别:一块小板子上的神经网络实战

把一袋垃圾照片丢给一块巴掌大的树莓派,一两秒内返回「可回收 / 厨余 / 有害 / 其他」的识别结果——这就是这个树莓派垃圾分类识别项目在做的事。它不是把图片传到云端,而是用 TensorFlow 搭出来的神经网络在嵌入式设备本地完成图像识别和分类,从数据准备、模型训练到模型保存的完整链路都收在压缩包里。对刚开始折腾「树莓派 + Python 垃圾识别」的人来说,这是一份少见的端到端样例;对已经会跑分类模型的开发者,它又是一个能直接换数据集、改网络结构复用的基础工程。

2. 项目架构与技术选型:从摄像头图像到分类结果的完整链路

2.1 为什么是树莓派 + Python + TensorFlow

树莓派本身就是一个 Linux 单板计算机,功耗低、体积小,可以放在垃圾投放点或者社区场景里 7×24 小时跑。相比在 PC 上做演示,树莓派方案的真正价值在于验证「神经网络能不能在有限算力下做出实时推理」,这是从玩具走向产品必须跨过的一步。

Python 是这个场景下的首选,原因很直白:数据预处理、模型训练、模型转换、串口或摄像头驱动全都有现成库,不需要像 C++ 那样自己拼装整个推理管线。TensorFlow 在这里承担两层工作:训练阶段用它的 Keras 高层 API 快速搭建和训练网络;部署阶段再把训练好的模型转成轻量格式跑在树莓派上。项目摘要里专门提到的 Keras 就是运行在 TensorFlow 之上的高级接口,写网络结构时只需要关注「有哪些层、按什么顺序堆叠」,底层的反向传播和张量运算由框架处理。

2.2 目录结构与数据流

这套工程从压缩包解压出来之后,目录里既有 Python 端的 TensorFlow 代码,也保留了交互页面相关的文件。其中.babelrc、index.ejs、icon.icns这类文件看起来像是一个桌面外壳工程(Electron 系的打包模板),而predict.html是预测结果展示页。实际数据流是这样的:图片从文件或者摄像头进来,交给 Python 推理脚本加载模型做预测,最后把分类结果和置信度传给predict.html展示出来。整个链路如下:

图片文件/摄像头帧 --(预处理)--> 模型推理脚本 --(加载 SavedModel)--> 分类结果+置信度 --(HTTP/本地)--> predict.html 展示

我把关键文件整理成了一张表,解压后对号入座即可:

文件角色说明
predict.html预测前端页面,展示识别结果与置信度
index.ejs桌面外壳的页面模板,用于加载预测页
.babelrcJavaScript 转译配置,属于前端工程部分
.gitignore/.eslintignore版本控制与代码检查忽略规则
icon.icnsMac 平台的应用图标文件
数据集与 tf 代码分类图片目录 + 模型构建、训练脚本

2.3 数据集的摆放与标签设计

垃圾分类数据集是这个项目里最容易被低估的部分。模型能不能收敛、准确率能不能上去,一半取决于网络结构,另一半取决于数据怎么组织。这里的常见做法是按类别建目录,每一类垃圾图片放在一个文件夹里,文件夹名就是标签。比如按国内通用的四分法来分:

data/ ├── train/ │ ├── 可回收/ │ ├── 厨余/ │ ├── 有害/ │ └── 其他/ └── test/ ├── 可回收/ ├── 厨余/ ├── 有害/ └── 其他/

目录结构决定了标签的生成方式。用 TensorFlow 的数据加载接口时,它默认扫描子目录名并自动映射成整数标签,不需要手写数据标注文件。这里有一个容易忽略的坑:标签的顺序是按目录名的字典序排序的,也就是说「可回收」可能索引是 1 而不是 0。后面算混淆矩阵的时候,一定要从class_names属性里取真实的类别顺序,不能拍脑袋写死。

3. 核心模块拆解:数据读取、网络搭建与训练配置

3.1 数据读取:一行代码把图片目录变成训练流

在 TensorFlow 2.x 里,最省事的图片数据读取方式是image_dataset_from_directory。它会把目录结构扫描一遍,自动处理标签、乱序、批大小和图像尺寸缩放,不需要自己写循环去cv2.imread。这个项目里一定绕不开这一步,代码如下:

import tensorflow as tf # 从目录读取训练图像,按子目录名自动生成标签 train_ds = tf.keras.preprocessing.image_dataset_from_directory( 'data/train', # 训练集根目录,子目录名为类别名 validation_split=0.2, # 从训练集里切 20% 做验证 subset='training', # 当前取的是训练子集 seed=123, # 随机种子,保证切分可复现 image_size=(150, 150), # 统一缩放到 150x150 batch_size=32, # 每个 batch 32 张图 label_mode='int' # 标签为整数索引,配合 SparseCategoricalCrossentropy ) val_ds = tf.keras.preprocessing.image_dataset_from_directory( 'data/train', validation_split=0.2, subset='validation', # 当前取的是验证子集 seed=123, image_size=(150, 150), batch_size=32, label_mode='int' )

这段代码的关键逻辑是validation_split和subset的配对使用:第一次调用取训练子集,第二次取验证子集,两次的seed必须一致,否则切分结果对不上,验证集会混进训练过的图片,评估结果直接失真。image_size这里我设成 150×150,这是树莓派场景下常见的折中——太小丢特征,太大内存吃紧。如果在纯 PC 上训练,可以放宽到 224×224,精度会好一些。

3.2 网络结构:卷积层、池化层、Dropout 怎么堆

项目摘要里提到的卷积层、池化层、全连接层和激活函数,在这个工程里组合成了一个典型的轻量分类网络。模型主体用 Keras 的Sequential顺序模型来搭,按「卷积提特征 → 池化降维 → 全连接分类」的套路写:

model = tf.keras.Sequential([ # 像素值归一化到 0~1,避免数值过大导致训练不稳定 tf.keras.layers.Rescaling(1./255, input_shape=(150, 150, 3)), # 第一组卷积:32 个 3x3 卷积核,提取边缘、纹理等低级特征 tf.keras.layers.Conv2D(32, (3, 3), activation='relu', padding='same'), tf.keras.layers.MaxPooling2D(2, 2), # 2x2 池化,空间尺寸减半 # 第二组卷积:64 个卷积核,提取组合特征 tf.keras.layers.Conv2D(64, (3, 3), activation='relu', padding='same'), tf.keras.layers.MaxPooling2D(2, 2), # 第三组卷积:128 个卷积核,特征更抽象 tf.keras.layers.Conv2D(128, (3, 3), activation='relu', padding='same'), tf.keras.layers.MaxPooling2D(2, 2), # 把二维特征图拉平成向量,交给全连接层 tf.keras.layers.Flatten(), tf.keras.layers.Dense(128, activation='relu'), # 批量归一化 + Dropout:稳定训练、抑制过拟合 tf.keras.layers.BatchNormalization(), tf.keras.layers.Dropout(0.5), # 输出层:4 个类别,softmax 归一化成概率 tf.keras.layers.Dense(4, activation='softmax') ])

卷积层的参数需要说清楚:Conv2D第一个参数是卷积核数量,数量越多网络容量越大,但也越容易过拟合;(3, 3)是卷积核尺寸,视觉任务里 3×3 是最常用的选择;padding='same'表示边缘补零,让输出尺寸和输入一致。Dropout(0.5)的意思是每次训练随机丢弃 50% 的神经元连接,它只在训练时生效,推理时自动关闭,这是防止过拟合最有效的一招。最后的Dense(4, activation='softmax')输出 4 个类别的概率值,概率最大的就是预测结果。

3.3 训练配置:优化器、损失函数与回调函数

模型编译这一步决定了网络用什么方式学习。项目摘要里提到的 Adam 优化器和交叉熵损失正好都能在 Keras 里直接指定。Adam 是实际项目里最常用的优化器,自带自适应学习率,比 SGD 好调得多;损失函数用SparseCategoricalCrossentropy,因为前面读取数据时label_mode='int',标签是整数而不是 one-hot 向量:

model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4), loss=tf.keras.losses.SparseCategoricalCrossentropy(), metrics=['accuracy'] ) callbacks = [ # 每轮都在验证集上评估,只保存验证准确率最好的权重 tf.keras.callbacks.ModelCheckpoint( 'best_model.h5', monitor='val_accuracy', save_best_only=True ), # 连续 10 轮验证准确率不提升,提前终止训练 tf.keras.callbacks.EarlyStopping(patience=10, restore_best_weights=True), # 验证损失停滞 5 轮,学习率减半 tf.keras.callbacks.ReduceLROnPlateau(patience=5, factor=0.5) ]

这里我把 Adam 的学习率手动降到了1e-4,而不是用框架默认的1e-3。原因是数据集规模不算大,默认学习率容易在训练后期震荡,收敛不稳定。如果你的数据集更大、样本更丰富,可以试着调回默认值看速度。三个回调函数是训练最重要的兜底:ModelCheckpoint保证全程最优权重不丢,EarlyStopping省时间,ReduceLROnPlateau在损失曲面平缓时自动缩小步长,帮模型钻过局部最优点。这组配置在大多数图像分类任务里都能直接复用。

4. 训练与评估:从损失曲线到混淆矩阵

4.1 训练流程:fit 配合回调函数

模型编译完成后,真正的训练就一行model.fit。这里把train_ds和val_ds直接喂进去,整个训练过程中几个回调函数会自动工作:每轮结束评估验证集、保存最优模型、触发早停就终止。50 轮的epochs上限只是个保险丝,实际上一旦验证准确率连续 10 轮不涨,EarlyStopping会自动截断,不会白白跑满:

history = model.fit( train_ds, # 训练数据流 validation_data=val_ds, # 验证数据流 epochs=50, # 训练轮数上限 callbacks=callbacks # 模型保存、早停、学习率衰减 ) # 训练结束后把历史指标画出来,确认没有欠拟合或过拟合 import matplotlib.pyplot as plt plt.plot(history.history['accuracy'], label='train_acc') plt.plot(history.history['val_accuracy'], label='val_acc') plt.legend() plt.savefig('training_curve.png')

检查训练曲线是这一步最关键的验收动作:如果训练准确率一路走高,验证准确率却掉头向下,说明模型过拟合了,需要加大Dropout或者扩增数据;如果两者都很低,说明欠拟合,需要加深网络或者增加训练轮数。树莓派上用 CPU 训练小数据集时,几十轮通常几分钟能跑完,不需要急着上 GPU。训练完best_model.h5就是验证集上表现最好的那个权重文件。

4.2 用混淆矩阵评估:准确率高不等于分类靠谱

单纯看准确率很容易被误导,尤其是垃圾分类这种类别样本数可能不平衡的任务。比如「其他垃圾」图片特别多,模型就算把小类全分错,准确率也可能很高。这时候要看混淆矩阵和每个类别的精确率、召回率。用测试集跑一轮预测,和真实标签做对比:

import numpy as np from sklearn.metrics import confusion_matrix, classification_report # 对测试集做预测,得到每个 batch 的标签 preds = model.predict(test_ds) pred_classes = np.argmax(preds, axis=1) true_classes = np.concatenate([y for _, y in test_ds], axis=0) # 把目录名和标签索引对应起来,便于阅读 class_names = test_ds.class_names print(classification_report( true_classes, pred_classes, target_names=class_names )) print(confusion_matrix(true_classes, pred_classes))

classification_report会输出每个类别的精确率、召回率、F1 值,confusion_matrix则直观展示哪些类别容易被混淆。实战里常见的结果是「厨余」和「其他」互相认错,因为这两类外观本身就接近——厨余垃圾里也常常混着纸巾、塑料袋。如果发现某个类别召回率明显偏低,优先检查这一类图片数量是不是太少、场景是不是太单一,而不是一头扎进调网络结构。

4.3 模型导出:从 h5 到 SavedModel

训练好的.h5文件只适合继续调参用,真正上树莓派部署,建议导出成SavedModel格式。SavedModel 目录结构完整,包含网络结构和权重,TensorFlow 的各类部署工具(包括转成 TFLite)都优先认这个格式:

# 导出成 SavedModel,部署时加载更稳定 model.save('saved_model/garbage_model') # 单独写一个推理脚本,加载模型并预测单张图片 loaded = tf.keras.models.load_model('saved_model/garbage_model') img = tf.keras.utils.load_img('test_photo.jpg', target_size=(150, 150)) img_arr = tf.keras.utils.img_to_array(img) / 255.0 img_arr = np.expand_dims(img_arr, axis=0) # 增加 batch 维度 result = loaded.predict(img_arr) class_index = np.argmax(result[0]) print(f'预测类别: {class_names[class_index]}, 置信度: {result[0][class_index]:.2f}')

这里有个最容易被忽略的细节:推理时的target_size必须是模型训练时用过的image_size值,比如前面的 150×150;预处理也要保持一致,训练用了Rescaling(1./255),推理时就要手动除 255。如果训练用1./255、推理却不归一化,网络输入的分布就变了,精度直接蒸发,而且这类错误很难从报错信息里看出来,只能凭结果反推。

5. 树莓派部署避坑指南:模型跑不起来的常见问题排查

5.1 训练或推理时直接 OOM,进程被杀掉

现象:树莓派上跑到一半,终端提示Killed,或者 Python 进程无故消失。

原因:树莓派动辄只有 2GB~4GB 内存,TensorFlow 构建图和加载模型都要吃内存,再加上图片解码,很容易触顶。内核的内存杀手会直接把进程干掉,不留异常栈。

解决:先降batch_size,从 32 降到 8 或 4 试一轮;再把image_size从 224 降到 150 或 128。训练完在树莓派上只做推理的话,不要加载完整 TensorFlow,改用tflite-runtime,内存占用能压到原来的一半以下。临时开 swap 能续命,但别依赖,SD 卡扛不住长期高频写入。

5.2 import tensorflow 报 Illegal instruction,或者找不到 so 文件

现象:树莓派上明明装好了 TensorFlow,import tensorflow直接崩,报Illegal instruction (core dumped)。

原因:树莓派 CPU 架构特殊,部分预编译 wheel 依赖较新的指令集或较新的 glibc 版本。如果系统镜像版本太老,装上去的包和系统底层库对不上,跑起来就翻车。

解决:优先用官方为 ARM 架构发布的 wheel 包,不要从默认 PyPI 源随便下带armv7l字样的包。如果只是要推理,直接装tflite-runtime,轻量且少踩底层兼容性的坑。装完先跑一行import tensorflow as tf做冒烟测试,过了再继续。

5.3 转成 TFLite 量化模型后,准确率断崖式下跌

现象:原模型测试准确率 85% 以上,转换 int8 量化模型后在树莓派上一测只剩 60%。

原因:量化是把浮点权重压成低精度整数,信息丢失是必然的。如果校准数据集选得太少,或者图片风格和真实场景不一致,量化器找不到合适的动态范围,精度损失会被放大。

解决:做量化时representative_dataset一定要用验证集里随机抽的几百张真实图片,不能只用几张开盲盒。优先试「动态范围量化」而不是全 int8 量化,前者精度损失小,后者才需要做量化感知训练兜底。每次转换完,先在 PC 上用测试集评估一轮,确认精度衰减在可接受范围内再部署。

5.4 摄像头推理画面卡顿,帧率只有 1~2 FPS

现象:树莓派接上摄像头做实时识别,画面像幻灯片,点一次识别要好几秒。

原因:推理脚本每次预测都重新加载模型、初始化会话,或者把所有图像处理都阻塞在主线程里。树莓派 CPU 算力本身就有限,这种写法等于每次都在重启一套管线。

解决:模型全局只加载一次,放进一个常驻进程里接收图片并返回结果;摄像头图像采集和推理放进两个线程,中间用队列缓冲。再把送入模型的帧缩到 150×150,推理耗时能压到一两秒的可用区间。

6. 二次开发:换自己的数据集、加速部署改造

6.1 换数据集:改目录、改参数、改输出层

这套工程最值钱的地方是你完全可以不碰网络结构,只换数据就迁移到新任务。把data/train下的 4 个类别目录换成自己的分类目录,比如水果识别、零件缺陷识别。需要同步改两处:一是最后一层Dense(4, activation='softmax')的 4 改成你的类别数;二是如果新数据集图片尺寸和 150×150 差别很大,重算image_size。训练流程不用动,image_dataset_from_directory会自动扫描新目录并生成标签。

6.2 部署提速:转 TFLite 量化模型

树莓派上真正能提速的是把模型转成 TFLite,再配上tflite-runtime推理。转换时用验证集抽 100 张图做校准,避免精度暴跌:

# 转 TFLite,带代表性数据集做量化校准 def representative_gen(): for img, _ in val_ds.take(100): yield [img] converter = tf.lite.TFLiteConverter.from_saved_model('saved_model/garbage_model') converter.optimizations = [tf.lite.Optimize.DEFAULT] converter.representative_dataset = representative_gen tflite_model = converter.convert() with open('garbage_model.tflite', 'wb') as f: f.write(tflite_model)

转完之后的推理代码要从model.predict换成Interpreter的invoke方式,输入输出都是 NumPy 数组,调用模式完全不同。我自己的习惯是每次换数据集之后,都强制把整套流程从头到尾走一遍:训练、混淆矩阵评估、导出 SavedModel、转 TFLite、树莓派实机跑一轮测试图片。这个习惯帮我避免过太多次「在电脑上一切正常、上设备就翻车」的尴尬——先自己翻一次车,总比到现场翻车便宜。希望这个项目的坑和路径,能帮你少走几段弯路,把垃圾分类识别从「能跑通」推进到「跑得稳」。

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

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

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

立即咨询