ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

用Django部署CNN模型实现糖尿病视网膜病变识别系统

用Django部署CNN模型实现糖尿病视网膜病变识别系统 简介一套面向毕业设计场景的糖尿病视网膜病变识别系统基于Python与Django框架核心采用CNN卷积神经网络适合计算机相关专业学生、教师或开发者用于毕设、课设及项目演示。资源包共2000个文件包含1416个Python源码文件、271个JavaScript脚本、156个HTML页面、102个CSS样式表及配置说明文档等整体约28.1MB目录结构清晰前端后台与模型代码分层明确便于二次开发。已有152人学习浏览。资料内含通过导师指导、答辩评审分达95分的高分项目源码模型文件与说明文档齐全同时提供全部数据资料能够帮助使用者快速理解视网膜病变识别的完整流程包括数据预处理、CNN模型搭建、训练评估及Django Web端展示也便于在此基础上修改扩展以实现其他功能。1. 用Django把CNN模型变成能用的视网膜识别系统一个二线医院眼科一天能产出两三百张眼底照片能读片的医生却只有一两个而糖尿病视网膜病变DR的早期病灶微动脉瘤只有几个像素大小人工筛查漏诊率不低。这个课题在现实里的位置不是又调出一个98%的CNN模型而是把训练好的分类权重部署到Django服务里让医生上传眼底图像就能拿到分期结果和置信度。本文按毕业设计完整链路讲DR影像特征如何决定CNN骨干选型、训练阶段哪些参数直接左右最终模型能不能部署、Django应用里模型预加载和并发推理的写法最后给一条部署后自检路径。适合要复现课题的学生也适合想把PyTorch模型接到Django做小规模医疗影像辅助系统的后端工程师。2. 糖尿病视网膜病变的常常征象与CNN选型逻辑2.1 眼底图上到底在看什么DR的临床分级本质上是把眼底图里出现的异常征象按严重程度排队。国际临床分级把DR分成0到4级每一级对应的眼底表现和影像特征都可以明确列成一张表。临床分级眼底表现影像上对应的特征0级 无明显DR未见异常视盘、血管、黄斑结构正常1级 轻度NPDR仅见微动脉瘤红色细圆点直径约125微米2级 中度NPDR微动脉瘤出血点硬性渗出暗红色斑片与黄白色颗粒夹杂3级 重度NPDR静脉串珠、棉絮斑、IRMA灰白色棉絮状影、血管迂曲4级 PDR新生血管、玻璃体出血不规则线状新生血管网从这张表能看出两个对模型设计很重要的结论。第一DR判定依赖的是局部纹理、颜色和小目标形态而不是杯子在桌上这类全局空间关系这正是CNN的归纳偏置擅长的地方。第二微动脉瘤只有一百多微米在未经缩放的眼底原图上可能只占十几个像素VGG那种前几个stage连续下采样的结构会把小病灶信息快速稀释。所以选骨干网络时先看第一层卷积的stride和各stage的下采样比例EfficientNet这种用小卷积核加深度可分离卷积的架构对细节保留更友好。2.2 为什么ResNet和EfficientNet这类CNN适合当骨干这里需要回应一个绕不开的问题为什么不用Vision Transformer。ViT在ImageNet-21k这类亿级数据上预训练后再微调确实强但转移到DR任务时公开可用的眼底图像级标注数据集通常只有几千到几万张ViT缺少CNN那种局部性先验微调时对学习率、权重衰减、数据增强强度都非常敏感训练曲线更容易震荡。CNN的数据高效微调特性在小规模医疗图像上更可靠这也是Kaggle的APTOS历届高分方案几乎都以CNN骨干为主的原因。在实际实现里三者是简历和找工作时常见的对比项直接写进论文对比表也很合适。骨干网络参数量部署优势微调注意点ResNet5025.6M各框架复现多故障排查资料全最后分类头换成5分类注意BN层跟着解冻EfficientNet-B05.3M权重文件约20MB推理快对输入分辨率敏感训练与推理尺寸必须一致DenseNet1218.0M特征复用充分适合小数据集中间特征图占用显存略大torchvision里加载EfficientNet-B0并替换分类头的代码很短核心是拿到最后一层全连接的输入维度import torch.nn as nn from torchvision import models model models.efficientnet_b0(weightsmodels.EfficientNet_B0_Weights.IMAGENET1K_V1) # model.classifier 是 Sequential 结构classifier[1] 才是 Linear in_features model.classifier[1].in_features model.classifier[1] nn.Linear(in_features, 5)说明EfficientNet_B0_Weights.IMAGENET1K_V1表示加载ImageNet预训练权重classifier[1].in_features取的是原分类头输入维度替换成输出5类的Linear。如果直接对model.classifier整体赋值需要自己把Dropout层也带进去容易少写p参数。2.3 输入尺寸和预处理为什么不能照搬ImageNet很多复现项目的第一个错误是把任意见到的猫狗分类预处理流水线直接套到眼底图像上。眼底相机拍出来的视野是圆形周围一圈是黑色无信号区域。直接Resize到224×224网络会花大量容量去学习边缘黑色背景这种病态特征换一台相机、灯光亮一点黑边比例变了模型就崩了。常见做法是先裁出眼底圆形区域再做缩放代码里需要关注阈值参数import cv2 import numpy as np def crop_black_border(image, threshold15): # image: BGR格式眼底图 h, w image.shape[:2] gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 黑色背景灰度值接近0先用阈值把它变成255前景 _, thresh cv2.threshold(gray, threshold, 255, cv2.THRESH_BINARY) # 找非零像素的最小外接圆 coords cv2.findNonZero(thresh) (x, y), radius cv2.minEnclosingCircle(coords) x, y, r int(x), int(y), int(radius) # 按外接圆做圆形mask后裁正方形 mask np.zeros((h, w), dtypenp.uint8) cv2.circle(mask, (x, y), r, 255, -1) masked cv2.bitwise_and(image, image, maskmask) top, bot max(0, y - r), min(h, y r) left, right max(0, x - r), min(w, x r) cropped masked[top:bot, left:right] return cv2.resize(cropped, (224, 224))参数说明threshold15是区分黑色背景和眼底组织的经验值取太大会把暗角下的视网膜也裁掉取太小盖不住镜头边缘的渐晕。cv2.minEnclosingCircle返回的是所有非零像素的最小外接圆比单纯取轮廓外接矩形更贴近眼底视野形状。做完圆形裁剪再统一Resize到224训练和推理入口处复用同一个函数避免两边处理不一致。3. 从眼底图像到分类权重训练与评估参数3.1 数据集划分与类别不平衡处理公开可用的眼底图像数据集以EyePACS和APTOS 2019为主APTOS训练集合计约三千多张类别分布很不均匀无明显DR的图像约占一半轻度NPDR和重度NPDR明显偏少。如果直接按原始比例训练模型会把所有图像都推给占比最高的类别来压低损失。处理不平衡有两个稳定手段。第一个是类别加权在CrossEntropyLoss里按每类样本数的倒数设置权重。第二个是训练集分层采样尽量让每个batch里都出现少量类别的样本。下面这段是训练核心配置import torch import torch.nn as nn from torch import optim from torchvision import transforms, models # 类别权重样本少的类别给更高权重 class_counts torch.tensor([1200, 800, 700, 400, 300], dtypetorch.float32) class_weights class_counts.max() / class_counts # 反向比例 model models.efficientnet_b0(weightsmodels.EfficientNet_B0_Weights.IMAGENET1K_V1) model.classifier[1] nn.Linear(model.classifier[1].in_features, 5) criterion nn.CrossEntropyLoss(weightclass_weights) optimizer optim.AdamW([ {params: model.features.parameters(), lr: 1e-4}, {params: model.classifier.parameters(), lr: 1e-3}, ], weight_decay1e-5) # 图像增强直接作用在PIL图像上 train_transform transforms.Compose([ transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees180), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])代码里有两处值得解释。AdamW的两组参数分别设了不同学习率backbone用1e-4新替换的分类头用1e-3原因是预训练特征已经足够好用大学习率反而会破坏已经学到的底层纹理特征。ColorJitter的亮度、对比度扰动是为了模拟不同型号眼底相机、不同光照条件下的色差这是DR任务里最常见的域偏移来源。3.2 图像增强CNN算法里的三个必调参数在训练DR分类模型时三个参数直接决定微调效果。第一个是随机旋转幅度。眼底图像有明确的解剖方向但部分数据集没有统一对齐视盘位置而且旋转90度、180度并不会改变DR的病理特征所以RandomRotation(degrees180)或者更大的旋转范围都可以放心用。对比之下给一般自然图像做增强就不敢转这么大。第二个是冻结和解冻策略。迁移学习的常规做法是先冻结backbone让随机初始化的分类头先收敛几个epoch避免前面几轮的反向传播把预训练权重打乱。一般冻结5到10个epoch后解冻全部参数并用1/10的学习率继续训练。在torchvision里通过model.requires_grad_即可控制但它会对所有参数生效要分别设置的话可以for循环遍历# 冻结backbone只训练分类头 for param in model.features.parameters(): param.requires_grad False # 训练10个epoch后再解冻 for param in model.features.parameters(): param.requires_grad True第三个是Batch Size选择的背面逻辑。医学图像任务常用Batch Size在16到32之间再配合梯度累积或者减少学习率。Batch Size过大会让BatchNorm的统计量在迁移学习中变得不稳定特别是EfficientNet内部有大量Squeeze-and-Excitation模块对batch统计更敏感。3.3 评估模型不能只盯准确率QWK才是DR任务的标准指标DR分级是有序类别把中度误判成重度和中度误判成正常在临床上的代价完全不同。准确率对所有错误一视同仁无法体现这种顺序关系。Kaggle的APTOS竞赛把Quadratic Weighted Kappa作为排名指标就是因为它按类别之间的距离加权。相邻类别误判的惩罚是1隔一个类别是4隔两个类别是9。评估代码很短但要注意数据格式必须是0到4的整数类别标签from sklearn.metrics import cohen_kappa_score y_true [0, 1, 2, 3, 4, 0, 2, 1] y_pred [0, 1, 2, 3, 4, 0, 1, 2] qwk cohen_kappa_score(y_true, y_pred, weightsquadratic) print(fQWK{qwk:.4f})如果模型只会预测最常见类别准确率可能在60%到70%之间但QWK大概率低于0.4优秀模型的QWK通常在0.85以上。所以模型评估阶段要同时打印分类报告、混淆矩阵和QWK三项混淆矩阵能看到具体哪些类别被互相混淆比如轻度NPDR和中度NPDR在特征上确实很难分。顺带说一句QWK对训练集和验证集的分层方式也敏感同一个患者的双眼图像放在不同集合里会泄漏信息划分时应当按患者ID分而不是按图像文件分。4. Django集成把模型封装成Web预测接口4.1 Django项目结构与模型预加载Django集成模型的目标是服务启动时加载一次权重之后每次请求复用内存里的模型而不是每来一张图重新读一遍磁盘。这个目标落地需要项目结构调整和AppConfig配合。推荐的项目结构dr_system/ manage.py dr_system/ # settings、urls predict/ migrations/ services/ __init__.py preprocess.py predictor.py __init__.py apps.py views.py urls.py model_store/ efficientnet_b0_dr.pth staticfiles/ requirements.txt用python manage.py startapp predict创建App后把模型加载代码单独拆到services/predictor.py不要堆在views里这样后续单元测试和脚本调用都能复用。模型预热放在apps.py的ready()方法中Django的runserver和uWSGI启动时都会执行它# predict/apps.py from django.apps import AppConfig class PredictConfig(AppConfig): default_auto_field django.db.models.BigAutoField name predict def ready(self): # 启动时预加载模型避免第一个请求等几秒 from .services.predictor import get_predictor get_predictor()对应的predictor模块用一个模块级缓存变量保存模型句柄# predict/services/predictor.py import torch import torch.nn as nn from torchvision import models _model None def get_predictor(): global _model if _model is None: model models.efficientnet_b0() model.classifier[1] nn.Linear(model.classifier[1].in_features, 5) state torch.load(model_store/efficientnet_b0_dr.pth, map_locationcpu) model.load_state_dict(state[model_state]) model.eval() _model model return _model注意torch.load里的map_locationcpu训练时如果用了GPU权重文件里会有CUDA张量服务器上没有GPU就直接映射到CPU。如果训练环境和推理环境的PyTorch版本相差太大load_state_dict可能因为状态字典键名不一致报错常见原因是torchvision里EfficientNet分类头的key在不同版本间改过名。4.2 上传、预处理与推理接口的完整实现视图部分需要处理三件事接收上传文件、调用预处理管线、返回结构化JSON。预处理函数与训练时的crop_black_border保持同一套代码同时补上PIL的EXIF方向修正和灰度图统一转RGB这两个细节。# predict/views.py import torch from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from .services.predictor import get_predictor from .services.preprocess import load_retina_image csrf_exempt def predict_image(request): if request.method ! POST: return JsonResponse({code: 405, msg: 仅支持POST}, status405) image_file request.FILES.get(image) if not image_file: return JsonResponse({code: 400, msg: 缺少image字段}, status400) # 限制体积防止大文件拖垮服务 if image_file.size 10 * 1024 * 1024: return JsonResponse({code: 413, msg: 图片超过10MB}, status413) try: x load_retina_image(image_file) # shape: [3, 224, 224] Tensor model get_predictor() with torch.no_grad(): logits model(x.unsqueeze(0)) prob torch.softmax(logits, dim1) idx int(prob.argmax(dim1)[0]) confidence float(prob[0][idx]) return JsonResponse({ code: 0, stage: idx, stage_name: [NDR, Mild NPDR, Moderate NPDR, Severe NPDR, PDR][idx], confidence: round(confidence, 4), }) except Exception as exc: # 生产环境应记录日志这里只返回通用错误 return JsonResponse({code: 500, msg: str(exc)}, status500)代码里主要的位置都有注释。x.unsqueeze(0)把单张图片的张量从[3,224,224]扩成[1,3,224,224]凑出一个batch维度。torch.no_grad()在推理时关闭梯度计算能省下不少显存和计算量。置信度返回四位小数前端可以直接拿来判断是否低于阈值需要人工复核。urls.py里做路由映射并记得从HTTP方法上区分# predict/urls.py from django.urls import path from . import views urlpatterns [ path(predict/, views.predict_image, namepredict_image), ]4.3 并发场景下模型推理的边界Django默认的开发服务器是单进程多线程直接跑模型推理在小并发下没问题但生产环境用uWSGI或Gunicorn启动多worker时每个worker都会加载一份模型到内存。单份EfficientNet-B0在CPU上推理一次约100到300毫秒内存占用几百MB如果开4个worker实际占用的内存就是4倍。并发策略的选择取决于实际QPS。医院内部辅助诊断系统通常同时只有几个人在用峰值QPS不超过10同步阻塞式视图完全够用。如果担心多人同时上传导致请求排队可以在视图里加一个简单的信号量限流而不是引入Celery消息队列——为了一个每秒几个请求的系统搭一套Broker明显超出了毕业设计的合理复杂度。各方案对比如下方案是否建议理由同步视图多worker建议实现最简单QPS低时体验良好同步视图信号量限流折中控制并发数超出的返回服务繁忙Celery异步任务不推荐引入RabbitMQ/Redis运维成本高更重要的一点是确认使用的Web服务器能扛住长耗时请求。uWSGI默认socket读超时较短需要把http-timeout调大否则推理超过默认时间会被网关切断前端拿到502。这个问题在部署一章里会专门处理。5. NginxuWSGI部署与推理性能优化5.1 dependencies清单和模型文件管理部署到Linux服务器前先确认依赖与训练环境一致。requirements.txt建议这样写django4.2 torch2.0 torchvision0.15 numpy opencv-python-headless pillow uwsgi注意opencv-python-headless而不是opencv-python前者不依赖GUI库体积小一半适合服务器环境。模型的权重文件不要提交到Git仓库把model_store目录加入.gitignore后直接拷贝到服务器固定路径并在predictor.py中把路径提取到配置项里。创建虚拟环境并收集静态文件的步骤python3 -m venv /home/www/dr_env source /home/www/dr_env/bin/activate pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple python manage.py collectstatic --noinput python manage.py migrate5.2 宝塔部署Django的常用配置宝塔面板部署Django的最常见路径是Nginx负责静态文件和处理外部请求uWSGI跑在本地端口Nginx反向代理到uWSGI。先写uwsgi.ini[uwsgi] chdir /home/www/dr_system module dr_system.wsgi:application master true processes 2 threads 2 socket 127.0.0.1:8001 http-timeout 120 buffer-size 32768 vacuum true daemonize /home/www/logs/uwsgi.logprocesses 2对应内存占用的两倍模型服务器只有4G内存的话进程数不宜超过3。http-timeout设成120秒是因为首次推理可能触发底层库初始化比较慢buffer-size调大避免POST大图像时请求头溢出。Nginx站点配置里加入反向代理和静态文件映射location /static/ { alias /home/www/dr_system/staticfiles/; } location / { include uwsgi_params; uwsgi_pass 127.0.0.1:8001; client_max_body_size 12m; }client_max_body_size必须大于视图里限制的10MB否则Nginx会在到达Django之前拒绝请求。在宝塔面板操作时直接在网站设置里找到反向代理目标URL填http://127.0.0.1:8001静态文件目录指到staticfiles即可。5.3 用PyTorch量化给CPU推理加速模型部署最常见的瓶颈是CPU推理速度。医院服务器的GPU不是标配离线训练在GPU上完成后推理可能落到一台纯CPU机器上。PyTorch动态量化对EfficientNet里的Linear层有效代码改动很小import torch from torchvision import models # 加载训练好的fp32模型 model models.efficientnet_b0() model.classifier[1] torch.nn.Linear(model.classifier[1].in_features, 5) model.load_state_dict(torch.load(model_store/efficientnet_b0_dr.pth, map_locationcpu)[model_state]) model.eval() # 动态量化只量化Linear和Conv2d quantized torch.quantization.quantize_dynamic( model, {torch.nn.Linear, torch.nn.Conv2d}, dtypetorch.qint8 ) torch.save(quantized.state_dict(), model_store/efficientnet_b0_dr_q8.pth)量化之后要用验证集重新测一遍QWK通常轻微掉点0.01到0.03但CPU推理速度能提升1.5到2倍。需要注意动态量化对EfficientNet的MBConv模块里的深度可分离卷积支持并不完美某些算子可能会回退到浮点计算所以实际提速效果以真机测速为准。如果量化掉点不能接受另一个手段是导出ONNX并使用onnxruntime推理dummy torch.zeros(1, 3, 224, 224) torch.onnx.export(quantized, dummy, model_store/dr_efficientnet.onnx, input_names[input], output_names[logits], dynamic_axes{input: {0: batch}})onnxruntime在CPU上的TensorRT执行加速对部分算子有针对性优化实测比纯PyTorch CPU能再快30%左右。但引入ONNX会增加一层依赖和排错复杂度毕业设计阶段如果动态量化够用没必要上。6. 部署后自检用一张脚本验证整套系统可用性服务上线之后第一件事不是打开浏览器点上传按钮而是写一个自检脚本把标注过的测试图片批量发送到接口检查返回的stage是否与预期一致、响应时间是否在合理范围、异常输入是否能被优雅拒绝。import time import requests cases [ {path: test/0_normal.jpg, expect: 0, timeout: 10}, {path: test/2_moderate.jpg, expect: 2, timeout: 10}, {path: test/4_pdr.jpg, expect: 4, timeout: 10}, {path: test/not_retina.jpg, expect: error, timeout: 10}, ] base_url http://127.0.0.1:8000/predict/ for case in cases: start time.time() try: with open(case[path], rb) as fp: resp requests.post(base_url, files{image: fp}, timeoutcase[timeout]) elapsed_ms (time.time() - start) * 1000 data resp.json() print(f{case[path]}: {resp.status_code}, fstage{data.get(stage)}, fconfidence{data.get(confidence)}, felapsed{elapsed_ms:.0f}ms) except Exception as exc: print(f{case[path]}: ERROR - {exc})这个脚本里not_retina.jpg的预期是返回错误背后对应的逻辑是在预处理阶段检查图像是否近似眼底图的圆形视野。一个很轻量的判断指标是Laplacian方差全黑或者模糊到没法看的图像方差极低另外一种是非眼底图检测眼底图通常有一条明显的横向亮斑视盘可以用直方图统计兜底。边界处理是这一章的重头。第一上传的图片可能是手机拍的屏幕照片而不是原始眼底图这类图色彩分布和真实眼底图差很多最稳妥的办法是前端限定文件类型后端再校验尺寸和圆形区域占比两项都不满足了就直接拒绝。第二灰度图会被load_retina_image统一转成三通道RGB但原本训练集里几乎没有灰度图所以转换时要同时做对比度标准化否则推理结果不稳定。第三单眼和双眼标记对系统后续做数据统计很有用接口里预留eye_type字段不影响当前推理逻辑。把上面的自检脚本放进一个healthcheck.py再挂到服务器的crontab里每天早晨跑一次输出写入日志文件。这样任何一次重启、模型替换或者依赖升级导致的回归第二天早上就能在日志里看到而不是等医生使用时才发现接口坏了。最后提醒一个细节量化后的模型文件名字与原始权重分开保存脚本里加一个环境变量MODEL_PATH控制加载哪个文件回滚时就改环境变量重载服务不用改代码。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表