最近把垃圾分类这个老话题搬进了Web项目里,做了一个基于Django的图像识别垃圾分类系统。从训练模型到后端集成,再到上线部署,前前后后踩了不少坑。这篇文章就把整套设计与实现过程拆开讲讲,从模型选型到数据库设计,从识别接口到常见报错,尽量把关键决策背后的原因说清楚。如果你正准备做毕业设计、课程设计,或者刚学完Django想找个练手项目,这套内容应该能帮你少走很多弯路。
1. 项目背景与核心设计思路
1.1 为什么选Django加图像识别这套组合
垃圾分类本身是个偏传统的业务场景,但加上图像识别之后就变成了典型的人工智能应用落地。选技术栈的时候我认真对比过几个方向:Flask轻量但生态不够全,Node.js做后端也可以但Python的AI生态优势浪费了。最终锁定Django,理由很实际:自带ORM、Admin后台、用户认证,开发速度比Flask快一截,而且MVT结构天然适合把模型推理、业务逻辑、前端页面分层管理。
图像识别部分选了TensorFlow/Keras加载预训练的卷积神经网络。之所以不从头训练,是因为垃圾分类数据集规模普遍不大,从头训一个ResNet级别的网络,几万张图片跑一天也不一定收敛,而迁移学习直接用ImageNet上预训练好的权重,只微调最后的全连接层,训练时间能缩短一个量级,精度还很能打。这也是现在做图像分类项目最主流的做法。
这个系统能解决的核心问题很简单:用户上传一张垃圾图片,系统自动识别出它属于哪一类(比如可回收、厨余、有害、其他),同时给出具体的垃圾名称和投放提示。后续还加了历史记录、统计报表和用户反馈功能,保证整个系统不只是“识别一下”那么单薄。
1.2 系统功能模块与整体架构
我设计的系统分四个大模块:
用户模块:注册、登录、个人中心,其实Django自带的auth就能覆盖七八成需求,没必要重复造轮子。
图像识别模块:这是核心。用户通过网页上传图片,后端接收后先做预处理(尺寸统一、归一化),再交给训练好的模型推理,返回识别结果和置信度。
数据记录模块:识别记录存MySQL,包括图片路径、识别类别、置信度、识别时间、用户ID。后面做统计报表都靠这些数据。
反馈模块:用户如果觉得识别得不对,可以手动纠正,这些纠偏数据积累起来以后就是增量训练的数据集。
架构上采用前后端不分离的方式,Django直接渲染模板,识别接口用AJAX异步调用。为什么不拆前后端?因为这个场景页面交互不复杂,模板渲染加少量JavaScript完全够用,拆成Vue/React反而增加部署成本和开发负担。记住一个原则:技术选型不是越新越好,是越适合越好。
2. 图像识别核心:模型选型与训练实战
2.1 模型选型对比:DenseNet121与MobileNetV3
当时在DenseNet121和MobileNetV3之间犹豫了一阵。MobileNetV3主打轻量化,适合部署到嵌入式设备,但精度相比DenseNet稍微逊色。考虑到这个系统运行在云服务器上,不太需要纠结模型体积,最终选了DenseNet121作为骨干网络。
DenseNet的特点在于每一层都跟前面所有层有连接,梯度流更顺畅,参数效率高。用Keras加载预训练权重非常方便:
from tensorflow.keras.applications import DenseNet121 base_model = DenseNet121(weights='imagenet', include_top=False, input_shape=(224, 224, 3)) base_model.trainable = False这里有个关键点:include_top设为False,去掉原来的全连接层。因为ImageNet的1000类跟垃圾分类的类别完全不对应,必须自己加上新的分类头。trainable设为False的意思是冻结基础网络的权重,只训练后面的全连接层,这样计算量大大降低,而且能有效防止在小数据集上过拟合。
2.2 数据集准备与数据增强
数据集是这类项目最花时间的部分。我参考了开源的垃圾分类数据集,又自己去拍摄和爬取了一部分,把类别统一成四分类:可回收垃圾、厨余垃圾、有害垃圾、其他垃圾,每类大约2000张图片。
数据量仍然不够,所以做了数据增强。Keras里用ImageDataGenerator随机旋转、平移、缩放、水平翻转,相当于让一张图片变成好几张不同形态的样本:
from tensorflow.keras.preprocessing.image import ImageDataGenerator train_datagen = ImageDataGenerator( rescale=1./255, rotation_range=40, width_shift_range=0.2, height_shift_range=0.2, shear_range=0.2, zoom_range=0.2, horizontal_flip=True, validation_split=0.2 ) train_generator = train_datagen.flow_from_directory( 'data/train', target_size=(224, 224), batch_size=32, class_mode='categorical', subset='training' )注意rescale=1./255这行,所有像素值归一化到0-1之间,这个操作能大幅加速收敛。不管用的是VGG还是ResNet还是DenseNet,预处理步骤都是必需的。
2.3 训练参数设计与评估
训练过程分为两个阶段。第一阶段只训练新加的全连接层,优化器用Adam,学习率设1e-3;第二阶段解冻基础网络的后几层,学习率降到1e-5,做一遍微调。这种两阶段训练比直接端到端训练效果好很多,因为先让分类头适应任务,再去微调底层特征,不容易把预训练权重破坏掉。
最终模型的验证准确率在93%左右,单张图片推理时间在150毫秒左右。识别结果保存成HDF5格式的model文件,后面要加载到Django项目里用。评估的时候除了准确率,还重点看了每个类别的召回率和精准率,特别是“其他垃圾”这个类,因为它包含各种杂七杂八的东西,最容易混淆。遇到类似的分类问题,一定要多看混淆矩阵,别只盯着Accuracy一个数字。
3. Django后端与识别流程集成
3.1 创建Django项目与应用结构
项目结构是我顺手整理出来的,每个部分职责单一:
garbage_project/ ├── manage.py ├── garbage_project/ │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── classification/ │ ├── views.py │ ├── urls.py │ ├── models.py │ ├── utils/ │ │ └── recognizer.py │ └── tensorflow_models/ ├── user/ │ ├── views.py │ └── models.py └── templates/用django-admin startproject garbage_project创建项目后,分别创建classification和user两个App。可能有人图省事把所有逻辑堆在一个App里,新手阶段我强烈建议别这么干。项目小的时候看不出来,等你要改识别逻辑或者加新功能时,拆开的优势立刻体现出来——只动对应App,不会牵连其他地方。
3.2 模型加载与识别接口实现
模型加载这块有个常见的坑:如果每次请求都去加载模型,服务器会直接被拖垮。因为加载HDF5模型就要好几秒,更别说还有推理计算。正确做法是把模型加载做成单例,模块加载时初始化一次,后续请求直接复用。
我把这部分封装成独立的工具模块recognizer.py:
# classification/utils/recognizer.py import numpy as np from tensorflow.keras.models import load_model from tensorflow.keras.preprocessing.image import img_to_array, load_img MODEL_PATH = 'classification/tensorflow_models/garbage_model.h5' CLASS_LABELS = ['可回收', '厨余', '有害', '其他'] _recognizer = None def get_recognizer(): global _recognizer if _recognizer is None: _recognizer = load_model(MODEL_PATH) print("模型加载完成") return _recognizer def predict_garbage(image): model = get_recognizer() img = load_img(image, target_size=(224, 224)) img_array = img_to_array(img) img_array = np.expand_dims(img_array, axis=0) / 255.0 pred = model.predict(img_array) class_idx = np.argmax(pred[0]) confidence = float(np.max(pred[0])) return CLASS_LABELS[class_idx], confidence注意predict之前那张图片要跟训练时的预处理保持一致——同样的尺寸、同样的归一化方式。最容易犯的错误就是在训练时做了rescale而预测时忘了,或者反过来,导致结果差得离谱。
视图层用函数视图加AJAX处理上传请求。考虑到前端需要无刷新获取结果,我封装了一个简单的识别接口:
# classification/views.py import json from django.http import JsonResponse from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.http import require_POST from .utils.recognizer import predict_garbage from .models import RecognitionRecord @require_POST @ensure_csrf_cookie def recognize_api(request): img = request.FILES.get('image') if not img: return JsonResponse({'code': 1, 'msg': '未上传图片'}) if img.size > 5 * 1024 * 1024: return JsonResponse({'code': 1, 'msg': '图片不能超过5MB'}) try: label, confidence = predict_garbage(img) except Exception as e: return JsonResponse({'code': 2, 'msg': str(e)}) # 保存记录 record = RecognitionRecord.objects.create( user=request.user if request.user.is_authenticated else None, image=img, result_label=label, confidence=confidence ) return JsonResponse({'code': 0, 'data': {'label': label, 'confidence': confidence, 'record_id': record.id}})这个接口同时兼顾了校验、识别、记录落库三个环节。图片上传前限制大小是很有必要的,不然用户传一张几十MB的RAW格式图片,迟迟不响应不说,还浪费服务器资源。
4. 数据库设计与ORM优化技巧
4.1 核心表结构设计
项目里需要三张核心表,他们都继承Django自带Models.Model。设计时我遵循一个原则:尽可能复用Django内置的User模型,而不是重新建一张用户表,这样认证逻辑零成本获得。
用户反馈表比较特殊,它有一个标签字段用于记录用户的纠偏操作,这就像给模型准备“人工标注”,以后要迭代模型的话,这张表的数据就是增量训练数据集的核心来源。在写ORM查询的时候,新人容易踩的一个坑是删除对象。用Model.objects.filter(条件).delete()就能批量删除,但注意它返回的是一个元组,第一个值是删除的总条数。如果要判断某条记录是否存在,推荐用get_object_or_404,而不是自己try/except DoesNotExist,代码会干净很多。
三张表分别是:
图片识别记录表,字段包括id、关联用户、图片文件、识别结果、置信度、识别时间。用户ID允许为空,因为游客也可以试用识别功能。这里用了Django的ForeignKey和CASCADE级联删除,用户注销后历史记录自动清理,避免残留垃圾数据。
用户反馈表的设计更细致一些,包含反馈类型字段用来区分是在纠偏提示还是修改分类错误,还存了用户选的正确标签。这个表跟识别记录表是多对一关系,一条识别记录可以被多次反馈。
4.2 ORM查询优化与倾向缓存
分类统计页面要做按类别分组的计数,如果直接Python层循环统计,数据量小的时候无所谓,但记录过万后就会发现接口越来越慢。用ORM自带的annotate聚合查询是更优雅的方案:
from django.db.models import Count from .models import RecognitionRecord summary = RecognitionRecord.objects.values('result_label').annotate( total=Count('id') ).order_by('-total')这类统计其实很适合加缓存。我当时分了一层简单的缓存:统计5分钟过期,用django.core.cache里的cache.set和cache.get包了一层。平时不用太重的缓存方案,项目规模没到Redis非上不可的程度,本地内存缓存完全够用。
数据库索引也是容易忽视的点。result_label和create_time这两个字段是高频查询条件,我给它们都加了db_index=True。加索引是个动态过程,一开始表里几百条数据感觉不到差别,等数据量大了才发现加不加索引的查询耗时能差好几倍。
5. 前端交互与页面流程实现
5.1 上传页面的交互设计
前端这块我做得比较克制,没有引入任何前端框架,三张主要页面是首页、识别页和历史记录页。识别页的交互是核心场景:用户选择图片后先预览,然后点击识别按钮,页面通过AJAX方式调用后端接口,等待结果返回。
预览功能用JavaScript的FileReader实现,关键在读取图片前判断文件类型,只允许jpg、jpeg和png三种格式。识别按钮在请求发出后要立即禁用,并且显示加载状态,否则用户以为没点上,连续点好几次,结果后端跑了好几个推理任务,服务器CPU占满后谁都识别不了了。这是实战中非常典型的一个体验细节。
AJAX请求要注意带上CSRF Token,Django的CSRF防护默认是开启的,不加Token的话请求会被403拒绝。我用的是一个比较标准的方式,在模板里获取csrf_token,然后在JavaScript里放到请求头:
fetch('/classify', { method: 'POST', headers: { 'X-CSRFToken': getCookie('csrftoken') }, body: formData })5.2 识别结果展示与置信度处理
识别结果返回后,页面展示四个信息:垃圾类别、具体置信度、投放建议、对应垃圾桶图标。置信度这个参数很关键,但展示时必须处理一下,尤其是置信度低的预测。如果模型预测某项概率只有60%,直接硬显示“这属于可回收垃圾”很容易误导用户。
做法是设置一个置信度阈值,低于0.7时提示用户“不太确定,请确认是否准确”,同时给出其他候选类别的名称。这套逻辑虽然简单,但对提升用户信任度帮助很大。模型不一定每张图都能确认,系统应该学会说“不知道”,而不是硬着头皮给答案。这条经验用在任何分类系统里都适用——模型不是每问必答,要允许它表达不确定。
6. 部署上线与性能调优记录
6.1 服务器部署的关键步骤
部署环境是阿里云一台2核4G的Ubuntu服务器,这套配置跑Django加图像识别模型基本够用。部署时没有用django自带runserver,因为runserver是单进程单线程的,性能和稳定性都不足以承载真实访问。我用了经典的uWSGI + Nginx组合方案。
Nginx负责静态文件、媒体文件的直接返回和反向代理动态请求,uWSGI负责跑Django应用。当时针对一个上传文件是图片的POST请求,试过两种方式:一种是把图片先存到服务器再传给模型,另一种是直接内存里处理。第一次实装时我采取的是先落盘再读取的流程,这样好处是留档明确,坏处是多一次磁盘IO。后来测试发现对于5MB以内的图片,落盘读取的延迟几乎感知不到,但要留意存储目录的权限和磁盘空间。
Nginx的client_max_body_size参数一定要设置,默认是1MB,上传稍微大点的图片就直接413错误。这个参数很多人会忘记调整,排查的时候容易一头雾水。
6.2 性能瓶颈分析与模型加载优化
监控下来性能瓶颈主要在两个地方。第一个是模型推理时的CPU占用,因为服务器没有GPU,推理依赖CPU,平均每次150毫秒其实还好但并发上来之后就会有排队。解决方法是把uWSGI的进程数从2调到4,让每个进程都加载一份模型,分别处理请求。
第二个是模型加载时间。每次启动服务进程时,模型加载需要5-8秒,期间如果来了请求就直接报错。我的处理方案是在Django的ready方法里做热加载,App启动后立即触发一次模型加载,宁可慢三秒启动也不让第一个用户撞上未加载的窗口。
7. 常见问题与排查方法速查
调试这套系统的时候,确实遇到好几个经典报错,整理出来给后面做类似项目的同学参考:
| 问题现象 | 触发原因 | 解决方案 |
|---|---|---|
| 上传图片提示403 | 请求头缺少CSRF Token | 在JS请求头带X-CSRFToken |
| 上传大图时413 | Nginx默认限制body大小1MB | 在Nginx配置里调大client_max_body_size |
| 图片识别结果总是同一类 | 修改了模型后没有重启uWSGI | 重启uWSGI让新模型重新加载 |
| 模型首次访问响应极慢 | 模型首次加载耗时长 | 应用启动阶段用ready钩子预加载模型 |
| 上传的图片路径访问404 | Django没配MEDIA_URL和MEDIA_ROOT | 配置媒体文件路由和Nginx的location规则 |
| 数据统计不显示新记录 | ORM查询结果被缓存住 | 确认缓存策略,设置合理的过期时间或主动失效 |
还有一个新手会忽略的坑,就是图片路径中包含中文文件名时,可能是文件名里有中文导致存储后打不开。最好上传时统一改名,比如用UUID重命名存储文件,这样既避免中文路径问题,也让文件名不会泄露用户信息。个人建议在模型使用的图片路径处理中,文件保存到本地后立即把临时文件清理掉,避免大量临时垃圾堆积在/tmp目录。
在模型推理报错排查方面,最常见的报错是维度不匹配。这是因为训练时用的输入尺寸是224x224,而预测时传入的图片尺寸不对。一定要在代码里做强制缩放,并检查通道数是否为3。还有一次遇到预测结果全是同一个类别,排查后发现是模型文件路径错误,加载了一个没训练完的中间模型,所有权重还没收敛就保存了。所以训练时要明确区分中间模型和最终模型,保存文件时加上epoch和val_acc信息,比如model_epoch25_val95.h5这种命名习惯能省去很多混乱。
8. 增量更新与实际项目体会
这个项目做完之后,最大的收获并不是“学会训练一个模型”或者“会写Django接口”,而是真正理解了把深度学习模型嵌进Web项目的完整路径。模型训练只是冰山一角,数据准备、前后端联调、部署运维、性能排查,每一块都需要投入大量精力。
如果你现在刚学完Django基础,想上手做这个项目,我给几个直接的建议:
- 别在一开始就追求识别准确率,先跑通全流程,模型哪怕只有70%的准确率,也比一个只做了页面的漂亮原型有意义。
- 数据集可以从公开的垃圾分类数据集起步,但一定要亲自检查数据质量,有些公开数据集里同一类别图片差异极大,需要手动清洗。
- 模型文件不要提交到Git仓库,HDF5文件动辄几百MB,放进代码库会让仓库体积爆炸,用对象存储或网盘单独管理权重文件。
- 预留用户反馈功能,你这个系统如果真的有真实用户使用,反馈数据是后续模型迭代最宝贵的资源,很多系统的反馈功能就是摆设,实际上接口一定要打通、数据一定要入库。
- 部署阶段多看一眼uWSGI日志,很多隐蔽问题的线索都藏在日志里,别全靠界面上的错误提示瞎猜。
最后再分享一个小的经验:在做图像识别相关Django项目时,把图像预处理函数单独抽出来,训练时用一份代码,预测时用同一份代码,避免两边各写一套逻辑导致预处理方式不一致。这个小习惯救了我不少次,希望你用的时候也能体会到它的好处。