ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Django与MobileNetV3的智能垃圾分类系统设计与实现

基于Django与MobileNetV3的智能垃圾分类系统设计与实现 1. 系统整体设计与技术选型1.1 为什么拿Django做智能垃圾分类的后端骨架先聊一个很现实的问题为什么是Django而不是Flask、FastAPI或者干脆用Node.js我做这个智能垃圾分类系统的前期想法很朴素——需要一个能快速把页面、接口、数据库和算法串起来的框架。Django恰好把所有东西都给你备齐了自带ORM、自带Admin后台、自带模板引擎、自带用户认证。对于这种需要一个管理后台来维护垃圾类别、需要记录用户识别历史、需要按天统计分类情况的系统来说Django的“全家桶”模式能把开发周期压缩到非常短。我之前用Flask写过类似项目到后期要加用户系统、要加后台数据维护、要做权限划分的时候Flask就得自己一个一个拼挺费劲的。Django不一样你在建的app一开始就能用上它的一套机制models写好之后migrate一跑表就建好了admin后台注册一下后台管理界面直接就出来了。另外一个关键点是Django的同步模型和图像识别这种CPU密集型的推理任务放在一起逻辑上是顺的。图像识别这块一般是拿到图片交给模型推理返回类别和置信度这是一个典型的同步阻塞操作。如果用FastAPI那种异步风格反而要额外处理事件循环阻塞的问题。1.2 图像识别部分的方案取舍图像识别的技术栈常见的有几条路第一自己从头训练一个CNN网络。听着很酷但现实很骨感。垃圾图像分类的数据集虽然有现成的但类别之间的相似度极高——比如一个皱巴巴的塑料袋和一个半透明的塑料瓶都属于可回收物你要让模型区分的是“垃圾类别”而不是“具体物品”这样问题反而简化了。真正麻烦的是要收集足够多、足够真实的场景化垃圾图片。第二用现成的预训练模型做迁移学习。这是我最终采用的方案。拿ImageNet上预训练好的ResNet50或者更轻量的MobileNetV3作为骨干网络把最后的全连接层换成自己的分类头然后在垃圾分类数据集上微调。这种方式有几个很实在的好处训练时间短普通显卡几个小时就能出结果准确率起点高不用从零学特征模型体积可控MobileNetV3的权重只有几十MB部署在普通服务器上毫无压力。第三直接用云服务商的图像识别API比如某某云的垃圾分类接口。但人家是按调用次数收费的而且识别结果返回的是纯文本标签你想把置信度和自己的业务逻辑深度耦合就绕了一圈。自己做模型的好处是可控性高后续要扩展特殊类别、要接入自己的训练数据都很灵活。我在实际项目中选择了MobileNetV3-Large作为骨干网络。逻辑很简单在服务器上跑不需要考虑端侧部署的极限轻量但要兼顾推理速度。MobileNetV3-Large在ImageNet上的准确率比MobileNetV2高了约3个百分点而参数量依然远小于ResNet50。实测下来CPU上单张图片推理大约在200到400毫秒之间体感是能接受的。2. 图像识别模块的核心实现2.1 数据集的准备与预处理坦白讲这个项目里最花时间的不是写Django代码而是整理数据集。垃圾分类的数据集有几份公开的比如Huawei的垃圾分类数据集包含40多类生活垃圾。但我实际用下来发现公开数据集里很多图片是“杂志图”级别的背景干净、光照均匀、物品单一。现实中用户拿手机拍一张可能光线暗、背景杂、多个物体叠在一起效果就会打折扣。我的做法是公开数据集打底自己补充采集一批“脏图”。具体来说我保留了公开数据集里质量较高的图片然后用手机拍了一批在自然场景下的垃圾照片——比如办公桌垃圾桶里的废纸团、小区垃圾桶旁边的饮料瓶、厨房里沾了油渍的塑料袋。这些图放进训练集之后模型的鲁棒性有明显提升。数据预处理这一步有几个细节值得注意统一尺寸我统一resize到224x224这是MobileNet系列的默认输入尺寸。别随便改成别的否则预训练权重的适配会出问题。数据增强用了随机水平翻转、随机旋转20度、随机亮度对比度调整、随机裁剪。这个项目里数据增强非常管用因为垃圾图片往往有很强的方向性——瓶子横着竖着拍都有袋子的褶皱方向也随机。标签编码垃圾分类有两个层级。顶层是可回收物、厨余垃圾、有害垃圾、其他垃圾这四类底层是具体物品类别。我以底层类别作为模型输出然后在业务层映射到顶层类别。这样做的原因很简单如果模型直接输出四类它就得学会把“纸箱”和“易拉罐”都归为可回收物特征差异太大学起来反而乱。先识别具体物品再映射到四大类准确率更高。2.2 训练细节与参数调优训练脚本用PyTorch写的代码量不大核心逻辑大概是这样import torch from torchvision import models, transforms from torch.utils.data import DataLoader from torch import nn # 加载预训练模型并替换分类头 model models.mobilenet_v3_large(weightsmodels.MobileNet_V3_Large_Weights.IMAGENET1K_V1) num_classes len(class_names) model.classifier[3] nn.Linear(model.classifier[3].in_features, num_classes) # 冻结前几层只训练后面的部分 for param in model.features[:12].parameters(): param.requires_grad False # 数据增强 train_transforms transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(20), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.3), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30)几个关键参数的选择逻辑也交代一下学习率设为1e-4而不是默认的1e-3。因为用了预训练权重学习率太大会把之前学到的特征冲掉后面的层学不到东西。这是迁移学习里的铁律——预训练模型的微调阶段学习率一律比训练阶段低一个数量级。只冻结前12层features。MobileNetV3-Large总共大概有16个block前12层学到的都是边缘、纹理、颜色这种低级特征这些特征迁移到垃圾图片上依然有效。后面几层学到的更偏向ImageNet的具体物体形态需要重新训练。损失函数用CrossEntropyLoss这就是多分类的标准配置。有个小技巧是加了label smoothing设为0.1能避免模型过于自信对测试集上那些模糊图片帮助不小。训练集12000多张验证集约3000张大约12个epoch就收敛得差不多了。验证集准确率稳定在91%到93%之间。考虑到四分类的实用场景91%的置信度已经可以用了那种实在认不准的图片就归为“其他垃圾”兜底同时提示用户手动确认。2.3 模型导出与本地化部署训练好的模型不能光留在训练脚本里得导出成可以给Django调用的格式。我用了两种方式第一种是TorchScript。PyTorch的TorchScript可以把模型和参数打包成一个独立文件部署方不用装完整的PyTorch环境。model.eval() example_input torch.randn(1, 3, 224, 224) traced_model torch.jit.trace(model, example_input) traced_model.save(garbage_classifier.pt)第二种是ONNX。ONNX的好处是如果有后续端侧部署需求比如安卓App可以直接通过ONNX Runtime跑起来不依赖Python生态。torch.onnx.export( model, example_input, garbage_classifier.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}} )这里得提醒一个坑TorchScript trace模式对输入tensor的尺寸是“记住”的。你trace的时候用了batch size为1后面推理时最好也用batch size 1如果你想支持动态batch size就得用script模式或者改用ONNX的dynamic_axes。我在实际部署中图省事固定用batch size为1Django里每次处理一张图片就好。3. Django后端集成与接口设计3.1 项目目录规划与App创建Django项目建好之后目录规划是个容易被忽视但非常影响维护体验的环节。我习惯的做法是把“识别”和“业务”分开两个apprecognition负责图像识别的模型加载、预处理、推理对外提供predict(filepath)函数。garbage负责业务逻辑——接收前端上传图片、调用识别模块、存储记录、返回分类结果。创建app的命令很简单django-admin startproject waste_classifier cd waste_classifier python manage.py startapp recognition python manage.py startapp garbage为什么要分两个app因为模型推理逻辑和业务逻辑的变更频率完全不一样。模型升级了你只动recognition里面的代码业务规则变了比如映射关系调整了你只动garbage里面的代码。混在一起的话每次升级模型都要在业务代码里翻来翻去迟早出问题。3.2 图像识别的API接口设计接口我用的DRFDjango REST Framework。如果你不想引这个库也可以用Django自带的JsonResponse手写接口但一旦涉及表单校验、序列化、认证这些需求手写会非常麻烦。DRF在这个项目里帮了大忙。核心接口有三个1. 上传图片并识别POST /api/recognize/ Content-Type: multipart/form-data 参数image文件2. 查询识别历史记录GET /api/history/ 参数page, page_size3. 查询垃圾类别档案GET /api/category/str:category_name/第一个接口是核心后端逻辑大致如下from rest_framework.views import APIView from rest_framework.parsers import MultiPartParser from rest_framework.response import Response from rest_framework import status from recognition.infer import GarbageClassifier from garbage.models import RecognitionRecord class RecognizeView(APIView): parser_classes [MultiPartParser] def post(self, request): # 1. 校验文件 image_file request.FILES.get(image) if not image_file: return Response({error: 缺少图片}, statusstatus.HTTP_400_BAD_REQUEST) if image_file.size 10 * 1024 * 1024: return Response({error: 图片大小不能超过10MB}, statusstatus.HTTP_400_BAD_REQUEST) if image_file.content_type not in [image/jpeg, image/png, image/webp]: return Response({error: 仅支持JPG、PNG、WebP格式}, statusstatus.HTTP_400_BAD_REQUEST) # 2. 保存临时文件 temp_path save_temp_image(image_file) # 3. 调用识别模块 classifier GarbageClassifier.get_instance() result classifier.predict(temp_path) # 4. 写数据库 record RecognitionRecord.objects.create( image_pathsave_uploaded_image(image_file), categoryresult[category], confidenceresult[confidence], top5result[top5], ) # 5. 返回结果 return Response({ category: result[category], category_code: result[category_code], confidence: round(result[confidence], 4), top5: result[top5], record_id: record.id })接口设计里有个细节我把top5也返回给前端了。原因是垃圾识别场景里模型有时候会在“纸杯”和“纸盒”之间摇摆不定前两名置信度很接近。前端拿到top5列表之后可以在界面上展示“我们认为是XX但也有可能是XX请您确认”这样交互体验比硬邦邦地给一个结果好得多。3.3 模型加载的单例模式与并发处理模型加载这个环节是最容易写出性能问题的。如果你在每次请求里都重新加载模型权重文件那单张识别除了推理时间之外还要多花500毫秒左右的加载时间并发一高接口直接被打爆。正确做法是进程启动时加载一次之后常驻内存。我用最简单的方式实现单例import torch from torchvision import transforms class GarbageClassifier: _instance None def __init__(self): self.model torch.load(models/garbage_classifier.pt, map_locationcpu) self.model.eval() self.transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) classmethod def get_instance(cls): if cls._instance is None: cls._instance cls() return cls._instance def predict(self, image_path): # 图像预处理和推理逻辑 ...但这里有一个Python的并发陷阱要提Django自带的开发服务器是多线程的而模型推理是CPU密集型计算。Python的GIL导致多个线程同时推理时并不是真正的并行。实测下来两个并发请求同时打进来每个请求的耗时几乎翻倍。解决办法有两个方向方向一用多进程部署比如Gunicorn启动多worker。这样每个worker有独立的模型实例和解释器并发性能线性增长代价是内存占用翻倍。8GB内存的服务器开4个worker差不多就是上限了。方向二加一个队列把识别请求串行化处理API先返回“任务已接收”前端轮询任务状态。这种方式体验上没那么即时但能稳住服务器负载。我这个项目用的还是Gunicorn多worker的方式。生产部署命令大概是这样gunicorn waste_classifier.wsgi:application --workers 4 --bind 0.0.0.0:8000 --timeout 120--timeout 120必须设因为首次加载模型可能需要几秒钟Gunicorn默认30秒超时不算充裕第一枪请求很容易因为超时被误杀。我第一批请求就被杀过一次后来加上这个参数就好了。4. 数据建模与管理端开发4.1 用Django ORM设计垃圾类别表数据表的设计在Django里就是models.py写完之后migrate建表。这个系统的核心表有两张。垃圾类别表GarbageCategoryclass GarbageCategory(models.Model): 垃圾类别基础档案表 name models.CharField(类别名称, max_length50, uniqueTrue) code models.CharField(类别编码, max_length20, uniqueTrue) parent_type models.CharField(四大分类, max_length20) description models.TextField(详细说明, blankTrue) recycle_method models.TextField(处理方式说明, blankTrue) created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table garbage_category verbose_name 垃圾类别parent_type就是“可回收物”、“厨余垃圾”、“有害垃圾”、“其他垃圾”这四类的映射。这里有个设计上的考虑为什么不直接建一张独立的四大分类表用外键关联我做过对比。如果建外键管理后台里每次新增一个类别都要先去四大分类表里找记录操作成本高。而且四大分类就四行数据属于“死数据”没必要单独建表。直接用一个CharField字段存四大分类的名称查询时按这个字段过滤简单高效。当然如果你后续要给四大分类加更多描述信息比如颜色标识、处理流程图那建独立表是合理的。识别记录表RecognitionRecordclass RecognitionRecord(models.Model): 用户识别历史记录 user models.ForeignKey( settings.AUTH_USER_MODEL, on_deletemodels.CASCADE, nullTrue, blankTrue ) image_path models.CharField(图片存储路径, max_length255) category models.CharField(识别垃圾类别, max_length50) category_code models.CharField(类别编码, max_length20) parent_type models.CharField(所属四大分类, max_length20) confidence models.FloatField(置信度, default0.0) top5 models.JSONField(Top5预测, defaultlist) created_at models.DateTimeField(识别时间, auto_now_addTrue) class Meta: db_table recognition_record verbose_name 识别记录 ordering [-created_at]这里有两个细节想分享。第一top5用JSONField而不是单独建一张子表。因为Top5这串数据只在展示时用不需要按里面某个类别去查询。如果业务上需要“按某个类别统计识别量”Category字段已经有索引了不用去解析JSON。第二图片要不要存数据库。我存的是本地路径而不是BLOB二进制字段。图片文件直接存到服务器上的media目录数据库只存路径。这是最常规的做法——数据库的BLOB字段既浪费存储空间又拖慢查询速度而且对备份很不友好。后面如果要上云可以把路径替换成OSS存储的URL代码改动也小。4.2 Django Admin后台配置带来的维护便利Django自带Admin后台在垃圾类别维护这个场景里提供了特别大的便利。写完模型之后在admin.py里注册一下from django.contrib import admin from .models import GarbageCategory, RecognitionRecord admin.register(GarbageCategory) class GarbageCategoryAdmin(admin.ModelAdmin): list_display [name, code, parent_type, created_at] list_filter [parent_type] admin.register(RecognitionRecord) class RecognitionRecordAdmin(admin.ModelAdmin): list_display [category, category_code, confidence, created_at] list_filter [parent_type, created_at] date_hierarchy created_at管理后台能干什么每天看看识别记录的数量和分布不用写SQL界面里点点就行发现某类垃圾的识别结果经常被用户纠正可以顺便去补充该类别的训练数据。这个后台在实际运维中帮了大忙。没有它的话你得直接在数据库里执行SQL去查询和修改数据操作风险大不说效率也低。5. 实操中的常见问题与排查实录5.1 模型推理慢、接口响应超时的排查思路我遇到过接口响应时间从200毫秒飙到5秒的情况。现象是第一张图识别很快之后越来越慢最后直接超时。排查步骤是这样的先看CPU占用top命令看看有没有别的进程抢占了资源。如果CPU占用率高达100%说明模型推理或者图像处理阻塞了。再看内存占用free -h看看是否内存不足导致频繁swap。模型加载进来占几百MB内存如果服务器只有2GB内存跑着MySQL、Nginx、Django内存很容易见底。看日志里有没有TimeoutError信息。如果Gunicorn的worker超时被杀重开一个worker后又要重新加载模型等于每个请求都在承担模型加载的开销。最终定位到的问题是某一天图片上传量激增Nginx没有限制请求体大小大量超大图片被一次性塞进来Django这边既要处理大文件上传又要跑模型把几个worker全部拖垮了。解决方法是三管齐下Nginx设置client_max_body_size 10m从入口控制请求大小Django接口里也做文件大小和格式校验不让非图片文件进入识别流程。前后端双重限制不单靠一层给Gunicorn配上--max-requests 1000让每个worker处理满1000个请求后自动重启防止内存碎片积累。5.2 CPU推理乱占线程的问题PyTorch默认会把所有CPU核心都用来跑模型。这在批处理场景下没问题但在Web服务场景下很要命——一个识别请求就把服务器所有核占满了其他接口全部卡死。解决办法是给torch设置线程数让模型推理只使用有限的CPU核心torch.set_num_threads(2)这个参数必须在加载模型之前设置。我在GarbageClassifier.__init__的首行就写上了。服务器如果是4核8线程模型推理只占2个线程剩下的资源留给Django处理请求和数据库操作整体系统的吞吐量反而更高了。还有个细节是torch.set_num_threads(2)不能设为0或者负数否则会报错。单张图片推理任务2个线程就已经能发挥性能了再多线程收益递减。5.3 中文标签编码引发的乱码Django返回JSON里包含中文垃圾类别名称前端拿到的是乱码。这个坑很经典。排查的时候发现接口返回的HTTP头里带的是Content-Type: text/html; charsetutf-8没问题。但前端JavaScript解析出来的字符串还是乱码。问题出在哪其实出在前端fetch请求和响应头之间的字符集不一致。具体来说DRF默认的JSONRenderer设置是ensure_asciiFalse如果软件版本较旧返回的中文经过Vary等头处理后被前端按latin-1解码就会乱码。我的解决方案是在DRF的REST_FRAMEWORK配置里显式设置REST_FRAMEWORK { DEFAULT_RENDERER_CLASSES: [ rest_framework.renderers.JSONRenderer, ], }同时在后端所有接口返回数据时用一个统一工具方法包一层强制转成utf-8安全的JSON字符串。另外前端fetch请求要加上headers: {Accept: application/json; charsetutf-8}双保险。5.4 并发上传图片时的临时文件冲突原版代码里我先把上传的图片保存到临时目录再传给识别模块。并发一高就出问题——两个请求同时生成同名的临时文件后到的覆盖了先到的先到的识别请求读文件读到一半文件没了直接报错。解决方案是用UUID生成临时文件名import uuid temp_filename f{uuid.uuid4().hex}.jpg temp_path os.path.join(/tmp/garbage_recognition/, temp_filename)UUID4重复的概率在现实场景下可以忽略不计。另外要记得识别完之后删除临时文件否则服务器上会堆积一堆垃圾图片。我写了一个finally块确保异常也要清理。6. 部署上线与后续优化6.1 一个最省事的部署方案如果你也是个人项目或中小型应用没必要上Kubernetes那一套。我最终的部署方案是Web服务器Nginx负责静态文件服务、反向代理、请求体大小限制应用服务器Gunicorn4个worker数据库PostgreSQLDjango项目数据量小SQLite其实也能扛住但如果识别记录表会不断增长建议还是上PostgreSQL后面查统计数据会舒服很多模型文件直接放在Django项目目录下的models/文件夹。Nginx配置示例server { listen 80; server_name yourdomain.com; client_max_body_size 10m; location /static/ { alias /path/to/project/static/; } location /media/ { alias /path/to/project/media/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_read_timeout 120s; } }proxy_read_timeout 120s也是必须的跟Gunicorn的--timeout 120对应。不然Gunicorn还没超时Nginx先掐断了连接前端拿到的就是502。我第一次上线就吃过这个亏排查了半天最后发现是两个超时时间不匹配。6.2 模型更新与线上迭代的几个方向系统部署起来之后保持识别准确率不下降是一件持续的事情。我目前梳理了几个后续优化方向第一收集线上误判样本持续微调模型。这是最有效也最直接的方法。识别记录表里如果用户手动修改了类别说明模型判断错了。把这些图片捞出来加上用户纠正后的标签作为增量训练数据。每个星期或者每两个星期跑一轮小规模微调模型的准确率会越滚越好。这个在Django里实现起来很简单只需要写一个管理命令把最近一周置信度低于阈值且用户修改过的记录对应的图片重新打包成数据集。第二加入目标检测模块。现在的方案是单物体识别——用户拍一张图片默认图片里只有一个主体垃圾。但现实场景中一个画面里可能有多个垃圾。后续可以引入YOLO这类目标检测模型先框选出每个垃圾区域再逐块跑分类。这样就把“单物体识别”升级为“多物体识别”。第三扩展更多业务场景。比如根据识别结果自动提示这个垃圾的处理方法——塑料瓶应该清洗后压扁再扔废旧电池应该投入有害垃圾收集容器。这些信息可以做成垃圾类别档案表的一部分由管理后台维护前端在识别结果旁边展示。第四把模型推理抽离成独立服务。当前模型在Django进程里跑升级模型要重启Web服务。后续可以用gRPC或者HTTP微服务方式把识别模型部署成独立服务Django通过接口调用。这样模型升级不影响Web服务稳定性而且推理服务可以单独水平扩展。6.3 我在这个项目中最大的体会整个项目做下来我最大的感受是智能垃圾分类系统的难点不在“智能”这两个字而在“稳定可靠地提供智能”。模型准确率做到90%以上其实不难难的是让这个系统在真实环境中一直稳定跑下去。图片上传格式不对怎么办并发请求太猛怎么办模型推理把CPU占满影响其他接口怎么办临时文件越积越多怎么办这些看起来不起眼的问题在实际运行中才是决定用户体验的关键。还有一点是识别结果一定要留给人去确认的空间。有些垃圾本来就不好认比如纸质餐盒表面有食物残渣模型判断成“其他垃圾”还是“可回收物”都有一定道理。多做一层Top5展示让用户自己判断系统反而更可靠。如果你也想做类似的图像识别Web系统我的建议是先把Django的业务骨架搭好用最简单的模型跑通全流程然后再回头优化模型准确率和并发性能。顺序反过来的话模型调得再好部署到线上也是一堆问题。先跑起来再跑好这是所有这类项目都适用的节奏。
返回列表