ARTICLE DETAIL

资讯详情

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

宫颈癌智能诊断系统毕业设计:从数据到部署全链路实战

宫颈癌智能诊断系统毕业设计:从数据到部署全链路实战 简介这份毕业设计资源聚焦宫颈癌智能诊断系统的完整实现面向医学信息化、人工智能方向的本科生与课程设计学习者帮助解决从细胞图像预处理到模型推理落地的全流程问题。压缩包共41个文件约77.55MB以22个Python脚本、5个Jupyter Notebook和2个PyTorch权重文件为核心辅以5张示例图片、5份Markdown说明、1份docx安装指令及1个html页面覆盖数据预处理、模型构建、训练验证、GUI交互与部署配置等模块。已有112人学习下载。读者可参考其中的图像增强与ROI提取脚本、ResNet50训练与微调流程、剪枝与融合推理代码以及基于Qt的图形界面实现快速理解医学图像分类项目的目录组织与工程化思路并借助文档与测试脚本完成环境搭建和结果复现适合作为毕设选题参考或课程设计模板。1. 宫颈癌智能诊断系统一个毕业设计标题背后的真实工程链路宫颈癌智能诊断系统这个题目每年毕业设计季都会被大量计算机、软件工程、生物医学工程专业的学生选中。原因很直接它同时踩中了医学图像处理和深度学习两个热点答辩时听起来有分量简历上也能写一行像样的项目经历。但真正动手做的时候大多数人会卡在同一个地方——不知道这个系统到底该由哪几块拼起来每块用什么技术数据从哪来模型怎么训界面怎么接。我见过太多这类项目最后变成“一个跑不通的 notebook 加一个空壳网页”。问题不在能力在于没人把这条链路完整讲清楚。这篇文章就干这件事从数据获取、预处理、模型选型、训练调参到后端封装和前端展示把宫颈癌智能诊断系统拆成可复现的工程步骤。适合正在做毕业设计、需要一套能跑通且能讲清楚方案的人也适合想了解医学图像分类落地流程的工程师。2. 数据从哪来宫颈癌图像数据集的获取与清洗2.1 公开数据集的选择与对比做宫颈癌智能诊断第一步不是写模型是找数据。没有数据再好的网络结构都是空谈。目前公开可用的宫颈癌图像数据集主要有几个来源我按实际可用性排个序。数据集名称图像类型样本量级获取难度适用任务Herlev宫颈细胞涂片917 张低官网直接下载细胞分类SIPaKMeD宫颈细胞涂片4049 张低Kaggle 可获取细胞分类Mendeley LBC液基细胞约 1000 张中需注册细胞分类自建临床数据阴道镜/病理不定高需伦理审批筛查/分类对于毕业设计我一般推荐 SIPaKMeD 或 Herlev。SIPaKMeD 的样本量够用类别标注清晰分 5 类浅表鳞状细胞、中间鳞状细胞、柱状细胞、异常细胞、 koilocyte挖空细胞。Herlev 分 7 类但样本少类别不均衡严重训练时容易翻车。如果导师要求“必须用深度学习做筛查”那 SIPaKMeD 是最稳的选择。如果要求“贴近临床”那得考虑阴道镜图像但公开数据极少自建数据又涉及伦理审批毕业设计周期内基本来不及。2.2 数据清洗的四个必做步骤拿到数据后不能直接喂给模型。宫颈细胞图像有几个典型问题染色差异大、背景杂质多、细胞重叠、类别不均衡。清洗流程我通常按下面四步走。第一步去除低质量图像。用拉普拉斯方差检测模糊图像阈值一般设在 100 左右。低于这个值的图像直接丢弃否则模型会学到一堆噪声。import cv2 import numpy as np import os def check_blur(image_path, threshold100): img cv2.imread(image_path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) variance cv2.Laplacian(gray, cv2.CV_64F).var() return variance, variance threshold # 遍历数据集标记模糊图像 blur_images [] for root, dirs, files in os.walk(data/raw): for f in files: if f.endswith((.png, .jpg, .bmp)): path os.path.join(root, f) var, is_blur check_blur(path) if is_blur: blur_images.append((path, var)) print(f检测到 {len(blur_images)} 张模糊图像)这段代码的逻辑很直接拉普拉斯算子提取边缘方差越小说明边缘越少图像越模糊。阈值 100 是经验值实际用的时候可以先跑一遍看分布再决定砍在哪。如果数据集整体偏清晰可以提到 150如果本身质量一般降到 80 也行。第二步颜色归一化。不同实验室的染色方案不同同一张切片在不同设备上颜色差异可能很大。用 Reinhard 颜色归一化把图像色调统一到参考图像上能显著提升模型泛化能力。from skimage import color import numpy as np def reinhard_normalize(src, target): # 转换到 LAB 空间 src_lab color.rgb2lab(src) target_lab color.rgb2lab(target) # 计算均值和标准差 src_mean np.mean(src_lab, axis(0, 1)) src_std np.std(src_lab, axis(0, 1)) target_mean np.mean(target_lab, axis(0, 1)) target_std np.std(target_lab, axis(0, 1)) # 归一化 result (src_lab - src_mean) / (src_std 1e-6) * target_std target_mean result color.lab2rgb(result) return np.clip(result, 0, 1)参数说明src是待处理图像target是参考图像。参考图像一般选数据集中颜色最“标准”的一张或者用所有图像的均值。1e-6是防止除零。归一化后图像数值范围在 0 到 1 之间后续可以直接乘 255 保存。第三步类别均衡。SIPaKMeD 的 5 类样本量不完全一致异常细胞偏少。处理方式有两种过采样少数类或者用加权损失函数。我一般优先用加权损失因为过采样容易导致过拟合。from sklearn.utils.class_weight import compute_class_weight import numpy as np # 假设 labels 是训练集的标签列表 classes np.unique(labels) weights compute_class_weight(balanced, classesclasses, ylabels) class_weights dict(zip(classes, weights)) print(class_weights)compute_class_weight的balanced模式会自动按样本量反比计算权重。样本越少权重越高。训练时把这个字典传给损失函数即可。第四步划分训练集、验证集、测试集。比例一般 7:1.5:1.5 或 8:1:1。注意要按类别分层抽样保证每个子集里各类别比例一致。from sklearn.model_selection import train_test_split X_train, X_temp, y_train, y_temp train_test_split( X, y, test_size0.3, stratifyy, random_state42 ) X_val, X_test, y_val, y_test train_test_split( X_temp, y_temp, test_size0.5, stratifyy_temp, random_state42 )stratifyy是关键参数不加的话某个类别可能全被分到测试集里导致验证结果失真。random_state固定后结果可复现写论文时方便截图。3. 模型选型从 CNN 到 Transformer 的取舍3.1 为什么我不建议一上来就上 ViT宫颈癌细胞图像分类本质是细粒度视觉任务。细胞核形态、核质比、染色质分布这些特征在局部纹理上差异明显。卷积神经网络CNN的归纳偏置——局部感受野、权重共享——天然适合这类任务。Vision TransformerViT虽然在大数据集上表现强但需要大量数据预训练小样本下容易过拟合。毕业设计的数据量通常几千张用 ViT 从头训基本没戏。用预训练权重微调可以但显存占用和训练时间对普通显卡不友好。我的建议是ResNet50 或 EfficientNet-B0 起步跑通后再考虑换 backbone 做对比实验。3.2 ResNet50 迁移学习的完整训练脚本下面是一个可直接运行的训练脚本基于 PyTorch。数据加载用ImageFolder模型用预训练 ResNet50损失函数带类别权重。import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from torchvision import datasets, transforms, models from sklearn.utils.class_weight import compute_class_weight import numpy as np # 数据增强与归一化 train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomVerticalFlip(p0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 加载数据集 train_dataset datasets.ImageFolder(data/train, transformtrain_transform) val_dataset datasets.ImageFolder(data/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4) # 计算类别权重 labels [s[1] for s in train_dataset.samples] class_weights compute_class_weight(balanced, classesnp.unique(labels), ylabels) class_weights torch.tensor(class_weights, dtypetorch.float).cuda() # 模型定义 model models.resnet50(pretrainedTrue) num_features model.fc.in_features model.fc nn.Linear(num_features, len(train_dataset.classes)) model model.cuda() # 损失函数与优化器 criterion nn.CrossEntropyLoss(weightclass_weights) optimizer optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) # 训练循环 best_acc 0.0 for epoch in range(30): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.cuda(), labels.cuda() optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() # 验证 model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.cuda(), labels.cuda() outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() acc correct / total scheduler.step() print(fEpoch {epoch1}, Loss: {running_loss/len(train_loader):.4f}, Val Acc: {acc:.4f}) if acc best_acc: best_acc acc torch.save(model.state_dict(), best_model.pth) print(fBest Val Acc: {best_acc:.4f})逐段说明。数据增强部分水平翻转和垂直翻转对细胞图像都合理因为细胞没有固定方向。旋转 15 度是保守值再大可能引入不真实样本。颜色抖动模拟染色差异但幅度不能太大否则细胞核特征会被破坏。类别权重传给CrossEntropyLoss的weight参数让少数类样本的损失被放大。优化器用 AdamW学习率 1e-4权重衰减 1e-4。学习率调度用余弦退火30 个 epoch 内平滑下降。训练循环里每个 epoch 结束后在验证集上算准确率保存最好的模型。best_model.pth就是最终交付的权重文件。3.3 关键参数怎么调学习率是最敏感的。1e-4 是迁移学习的常用起点。如果 loss 震荡厉害降到 5e-5如果 loss 下降太慢提到 3e-4。batch size 受显存限制32 是 8G 显存下的安全值。num_workers 设成 CPU 核心数的一半左右太多反而拖慢数据加载。数据增强的强度需要根据数据集大小调整。样本少于 2000 张时增强可以激进一些样本多的时候增强太强反而拖慢收敛。我一般先跑一版不加增强的 baseline再逐步加增强看验证集准确率变化。4. 系统封装从模型文件到可交互的诊断界面4.1 后端推理服务的三种方案模型训练完只是半成品毕业设计答辩时老师要看的是“系统”不是“脚本”。后端封装常见三种方案Flask 轻量 API、FastAPI 异步服务、Gradio 快速演示。我按场景推荐。如果只是答辩演示Gradio 最快几十行代码就能出一个带上传和结果显示的网页。如果要求“前后端分离”用 FastAPI 写 REST 接口前端用 Vue 或 React。如果导师要求“传统 Java Web”那就 Spring Boot 调 Python 脚本但这条链路坑最多后面避坑章节会讲。4.2 FastAPI 推理接口的完整实现下面是一个生产可用的 FastAPI 推理服务支持图片上传、预处理、模型推理、结果返回。from fastapi import FastAPI, File, UploadFile from fastapi.middleware.cors import CORSMiddleware from PIL import Image import torch import torch.nn as nn from torchvision import transforms, models import io import numpy as np app FastAPI(title宫颈癌智能诊断系统 API) # 允许跨域前端调试用 app.add_middleware( CORSMiddleware, allow_origins[*], allow_methods[*], allow_headers[*], ) # 加载模型 device torch.device(cuda if torch.cuda.is_available() else cpu) model models.resnet50(pretrainedFalse) model.fc nn.Linear(model.fc.in_features, 5) # 5 分类 model.load_state_dict(torch.load(best_model.pth, map_locationdevice)) model.to(device) model.eval() # 类别名称 class_names [浅表鳞状细胞, 中间鳞状细胞, 柱状细胞, 异常细胞, 挖空细胞] # 预处理 transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) app.post(/predict) async def predict(file: UploadFile File(...)): # 读取图像 contents await file.read() image Image.open(io.BytesIO(contents)).convert(RGB) # 预处理 tensor transform(image).unsqueeze(0).to(device) # 推理 with torch.no_grad(): outputs model(tensor) probabilities torch.softmax(outputs, dim1) confidence, predicted torch.max(probabilities, 1) # 返回结果 result { class: class_names[predicted.item()], confidence: round(confidence.item(), 4), probabilities: { class_names[i]: round(probabilities[0][i].item(), 4) for i in range(len(class_names)) } } return result if __name__ __main__: import uvicorn uvicorn.run(app, host0.0.0.0, port8000)逻辑说明/predict接口接收上传文件转成 PIL 图像做和训练时一致的预处理。unsqueeze(0)增加 batch 维度。torch.no_grad()关闭梯度计算节省显存。softmax把 logits 转成概率torch.max取最大概率对应的类别。返回结果包含类别名、置信度和所有类别的概率分布。前端拿到后可以画柱状图答辩时视觉效果更好。参数说明host0.0.0.0允许局域网访问方便手机或另一台电脑测试。port8000是默认端口冲突的话改成 8001 或 8080。map_locationdevice保证模型能在 CPU 和 GPU 之间无缝切换。4.3 前端页面的最小实现前端不需要太复杂一个上传按钮、一个预览区域、一个结果展示区就够了。用原生 HTML JavaScript 即可不依赖框架。!DOCTYPE html html head meta charsetUTF-8 title宫颈癌智能诊断系统/title style body { font-family: Arial; max-width: 600px; margin: 50px auto; } #preview { max-width: 100%; margin: 20px 0; } #result { padding: 15px; background: #f5f5f5; border-radius: 8px; } /style /head body h2宫颈细胞图像智能诊断/h2 input typefile idfileInput acceptimage/* img idpreview src alt div idresult等待上传.../div script document.getElementById(fileInput).addEventListener(change, async (e) { const file e.target.files[0]; if (!file) return; // 预览 const reader new FileReader(); reader.onload (ev) { document.getElementById(preview).src ev.target.result; }; reader.readAsDataURL(file); // 上传推理 const formData new FormData(); formData.append(file, file); document.getElementById(result).innerText 推理中...; try { const response await fetch(http://localhost:8000/predict, { method: POST, body: formData }); const data await response.json(); document.getElementById(result).innerHTML strong诊断结果/strong${data.class}br strong置信度/strong${(data.confidence * 100).toFixed(2)}%br strong概率分布/strongbr ${Object.entries(data.probabilities).map(([k, v]) ${k}: ${(v * 100).toFixed(2)}% ).join(br)} ; } catch (err) { document.getElementById(result).innerText 请求失败 err.message; } }); /script /body /html这段代码的核心是fetch调用后端接口把文件以FormData形式发送。返回的 JSON 直接渲染到页面上。注意http://localhost:8000要换成实际部署的地址如果前后端不在同一台机器上。5. 避坑指南宫颈癌诊断系统开发中的五个血泪教训5.1 现象训练准确率 99%测试准确率 60%原因数据泄露。训练集和测试集里有同一张图像的不同增强版本或者同一患者的细胞图像被分到了不同子集。模型记住了患者特征不是细胞特征。解决按患者 ID 划分数据集而不是按图像。如果数据集没有患者 ID至少保证同一张原图的所有增强版本只出现在一个子集里。另外检查数据加载时有没有重复文件。5.2 现象模型把所有的异常细胞都预测成正常原因类别不均衡 损失函数没加权。异常细胞样本少模型倾向于预测多数类来降低整体损失。解决用加权交叉熵或者 Focal Loss。Focal Loss 对难分类样本加大权重适合极端不均衡场景。另外评估指标不能只看准确率要看召回率和 F1 分数。5.3 现象FastAPI 接口第一次请求特别慢后面正常原因模型在第一次请求时才加载到 GPU或者 CUDA 初始化耗时。解决在应用启动时加载模型不要放在请求处理函数里。用app.on_event(startup)装饰器提前加载。另外torch.cuda.is_available()第一次调用也会慢提前执行一次。5.4 现象前端上传图片后报 CORS 错误原因浏览器跨域限制。前端在 8080 端口后端在 8000 端口浏览器认为这是跨域请求。解决后端加CORSMiddleware允许前端域名。开发阶段可以设allow_origins[*]生产环境要改成具体域名。如果还不行检查请求头里有没有Content-Type: multipart/form-data。5.5 现象模型在验证集上表现很好部署后实际用效果差原因训练数据和实际数据分布不一致。训练用的是公开数据集实际用的是手机拍的屏幕截图或者不同染色方案的数据。解决在推理接口里加一个图像质量检查模糊或过暗的图像直接拒绝。另外收集实际场景的数据做微调哪怕只有几十张也能显著提升效果。6. 进阶技巧用 Grad-CAM 让诊断结果可解释答辩时老师最常问的问题是“模型为什么判断这是异常细胞”如果只给一个类别和置信度很难让人信服。Grad-CAM 能生成热力图标出模型关注的区域让诊断结果有据可查。import torch import torch.nn.functional as F import cv2 import numpy as np from torchvision import transforms from PIL import Image class GradCAM: def __init__(self, model, target_layer): self.model model self.target_layer target_layer self.gradients None self.activations None self._register_hooks() def _register_hooks(self): def forward_hook(module, input, output): self.activations output.detach() def backward_hook(module, grad_input, grad_output): self.gradients grad_output[0].detach() self.target_layer.register_forward_hook(forward_hook) self.target_layer.register_backward_hook(backward_hook) def generate(self, input_tensor, class_idxNone): self.model.eval() output self.model(input_tensor) if class_idx is None: class_idx output.argmax(dim1).item() self.model.zero_grad() output[0, class_idx].backward() # 全局平均池化梯度 weights self.gradients.mean(dim(2, 3), keepdimTrue) cam (weights * self.activations).sum(dim1, keepdimTrue) cam F.relu(cam) cam F.interpolate(cam, sizeinput_tensor.shape[2:], modebilinear, align_cornersFalse) # 归一化 cam cam - cam.min() cam cam / (cam.max() 1e-8) return cam.squeeze().cpu().numpy() # 使用示例 model ... # 加载训练好的模型 grad_cam GradCAM(model, model.layer4[-1]) image Image.open(test.jpg).convert(RGB) tensor transform(image).unsqueeze(0).cuda() cam grad_cam.generate(tensor) heatmap cv2.applyColorMap(np.uint8(255 * cam), cv2.COLORMAP_JET) heatmap cv2.cvtColor(heatmap, cv2.COLOR_BGR2RGB) # 叠加原图 original np.array(image.resize((224, 224))) overlay (0.6 * original 0.4 * heatmap).astype(np.uint8) cv2.imwrite(gradcam_result.jpg, cv2.cvtColor(overlay, cv2.COLOR_RGB2BGR))这段代码的核心逻辑前向传播拿到特征图反向传播拿到梯度梯度做全局平均池化得到权重权重和特征图加权求和ReLU 去掉负值上采样到原图尺寸。最后叠加到原图上红色区域就是模型关注的地方。参数说明target_layer一般选最后一个卷积层ResNet50 是model.layer4[-1]。class_idx指定要看哪个类别的热力图不传的话默认取预测类别。0.6和0.4是叠加比例热力图太浓会盖住细胞形态太淡又看不清。答辩时把原图、热力图、叠加图三张并排展示再解释“模型关注的是细胞核区域和病理医生的判断依据一致”说服力会强很多。做这类毕业设计我的习惯是先把整条链路跑通哪怕用最简模型、最糙界面先让系统能转起来。然后再逐步替换组件、调参、加功能。最怕的是一上来就追求完美架构结果卡在某个环节出不来最后连演示都做不了。希望帮到你。本文还有配套的精品资源点击获取
返回列表