
简介这份毕业设计资源聚焦宫颈癌智能诊断系统的完整实现面向计算机、人工智能及医学信息方向的毕业生与课程设计学习者帮助解决医学图像分类项目从数据到部署的全流程落地问题。压缩包共41个文件约77.55MB以22个Python脚本、5个Jupyter Notebook和2个PyTorch权重文件为核心辅以5张jpg示例图、5份md说明、1份docx安装指令及1个html页面覆盖预处理、模型训练、剪枝与界面交互等环节。资源包含图像去噪、ROI提取、数据增强等预处理脚本ResNet50训练与微调笔记模型剪枝及pth权重文件以及基于Qt的图形界面医生可上传细胞图像获取预测结果。目录按预处理、神经网络训练、GUI等模块划分便于按需查阅与二次开发。目前已有112人学习适合需要完整项目参考、排错思路与模型部署示例的读者。1. 宫颈癌智能诊断系统从毕业设计到能跑通的完整方案宫颈癌智能诊断系统这个题目每年毕业设计季都会被大量计算机、软件工程、生物医学工程专业的学生选中。原因很直接它同时踩中了医学图像处理和深度学习两个热点论文好写答辩时老师也容易认可。但真正动手做的时候大部分人会在三个地方卡住——数据集怎么选、模型怎么搭、系统怎么串起来。我见过太多人把 GitHub 上的代码 clone 下来改个标题就交差结果答辩时被问“你的模型输入尺寸是多少”“为什么用这个骨干网络”就答不上来。这篇笔记就是把我自己做过的一套方案拆开讲清楚从数据预处理到模型训练再到系统集成每一步都给出可复现的命令和参数。适合正在做毕业设计、需要一套能跑通且能讲清楚原理的宫颈癌智能诊断系统的同学也适合想了解医学图像分类落地流程的工程师。2. 数据准备宫颈癌图像数据集的选择与预处理2.1 公开数据集对比与选型理由做宫颈癌智能诊断第一步不是写模型而是找数据。没有数据再好的网络也是空壳。目前公开的宫颈癌图像数据集主要有几个来源我按实际使用体验列个对比数据集名称图像类型样本量类别获取难度适用场景Herlev细胞学涂片917张7类低官网直接下载细胞分类入门SIPaKMeD细胞学涂片4049张5类低Kaggle可获取细胞分类进阶Mendeley LBC液基细胞学963张2类低Mendeley Data二分类快速验证自建医院数据阴道镜/病理不定不定高需伦理审批真实场景研究对于毕业设计我一般推荐 SIPaKMeD 或 Herlev。SIPaKMeD 样本量适中类别清晰而且图像质量比 Herlev 好不少。Herlev 只有 917 张做深度学习容易过拟合但胜在经典论文引用多。如果你时间紧直接用 SIPaKMeD 做五分类准确率做到 92% 以上不算难。选好数据集后目录结构要规范。我习惯这样组织dataset/ ├── train/ │ ├── superficial/ │ ├── intermediate/ │ ├── columnar/ │ ├── parabasal/ │ └── metaplastic/ ├── val/ │ └── (同上五个类别) └── test/ └── (同上五个类别)划分比例按 7:1.5:1.5 走即训练集 70%验证集 15%测试集 15%。注意验证集和测试集要严格分开不能混用否则最后报的指标是虚的。2.2 图像预处理与数据增强的代码实现拿到原始图像后不能直接喂给网络。宫颈细胞涂片图像通常有染色不均、光照差异、细胞重叠等问题。我一般做三步预处理尺寸统一、颜色归一化、数据增强。尺寸统一到 224×224这是大多数 ImageNet 预训练模型的输入尺寸。颜色归一化用 Reinhard 方法把图像的颜色分布对齐到目标模板减少染色差异。数据增强用 torchvision 的 transforms 做在线增强包括随机旋转、水平翻转、颜色抖动。import torch from torchvision import transforms, datasets from torch.utils.data import DataLoader, WeightedRandomSampler import numpy as np # 训练集增强旋转、翻转、颜色抖动、归一化 train_transform transforms.Compose([ transforms.Resize((224, 224)), # 统一尺寸 transforms.RandomRotation(degrees15), # 随机旋转±15度 transforms.RandomHorizontalFlip(p0.5), # 水平翻转 transforms.RandomVerticalFlip(p0.3), # 垂直翻转细胞图像方向不敏感 transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.1, hue0.05), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], # ImageNet统计值 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(dataset/train, transformtrain_transform) val_dataset datasets.ImageFolder(dataset/val, transformval_transform) test_dataset datasets.ImageFolder(dataset/test, transformval_transform) # 处理类别不平衡用WeightedRandomSampler给少数类更高采样权重 targets [s[1] for s in train_dataset.samples] class_counts np.bincount(targets) class_weights 1.0 / class_counts sample_weights [class_weights[t] for t in targets] sampler WeightedRandomSampler(sample_weights, num_sampleslen(sample_weights), replacementTrue) train_loader DataLoader(train_dataset, batch_size32, samplersampler, num_workers4) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4) test_loader DataLoader(test_dataset, batch_size32, shuffleFalse, num_workers4)这段代码里几个关键点RandomRotation的 15 度是经验值细胞图像旋转太大会引入不真实的形态ColorJitter的 hue 只给 0.05因为染色颜色变化不能太剧烈否则模型学到的颜色特征就乱了WeightedRandomSampler解决类别不平衡SIPaKMeD 里各类别样本数差异不小不加这个采样器模型会偏向多数类。注意num_workers在 Windows 上设成 0 或 2设 4 以上容易报错。Linux 上可以设 4 到 8根据 CPU 核心数调整。预处理做完后建议先可视化几张增强后的图像确认没有出现图像被裁切、颜色失真等问题。这一步花五分钟能省后面几小时的排查时间。3. 模型构建从骨干网络选型到迁移学习策略3.1 骨干网络对比与选择依据宫颈癌智能诊断系统的核心是分类模型。选骨干网络时毕业设计常见的选项有 ResNet、VGG、DenseNet、EfficientNet 和 Vision Transformer。我按实际训练效果和资源消耗列个对比骨干网络参数量输入尺寸训练速度小数据集表现推荐场景ResNet5025.6M224快好首选平衡性好VGG16138M224慢一般不推荐参数太多DenseNet1218.0M224中等很好数据量少时优选EfficientNet-B05.3M224快好资源受限时用ViT-B/1686M224慢差需大数据不推荐毕业设计用我的建议很明确首选 ResNet50备选 DenseNet121。ResNet50 的残差结构成熟稳定ImageNet 预训练权重容易获取在 SIPaKMeD 上微调后准确率能到 93% 左右。DenseNet121 参数更少特征复用强在样本量少于 3000 时表现往往比 ResNet50 好半个百分点。VGG16 参数太多毕业设计的算力通常不够训练一轮要等很久不推荐。ViT 需要大量数据预训练小数据集上微调效果很差除非你有十万级以上的数据否则别碰。3.2 迁移学习与微调策略的代码实现选好骨干网络后不要从零训练。用 ImageNet 预训练权重做迁移学习这是小数据集上的标准做法。具体策略分两步先冻结骨干网络只训练分类头再解冻最后几个层做小学习率微调。import torch.nn as nn import torchvision.models as models from torch.optim import Adam from torch.optim.lr_scheduler import CosineAnnealingLR def build_model(num_classes5, backboneresnet50, freeze_backboneTrue): 构建迁移学习模型 if backbone resnet50: model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2) # 冻结骨干网络参数 if freeze_backbone: for param in model.parameters(): param.requires_grad False # 替换最后的全连接层 in_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(0.5), # 防止过拟合 nn.Linear(in_features, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.3), nn.Linear(256, num_classes) ) elif backbone densenet121: model models.densenet121(weightsmodels.DenseNet121_Weights.IMAGENET1K_V1) if freeze_backbone: for param in model.parameters(): param.requires_grad False in_features model.classifier.in_features model.classifier nn.Sequential( nn.Dropout(0.5), nn.Linear(in_features, num_classes) ) return model # 第一阶段只训练分类头 model build_model(num_classes5, backboneresnet50, freeze_backboneTrue) device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) # 只优化分类头参数 optimizer Adam(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3) criterion nn.CrossEntropyLoss() scheduler CosineAnnealingLR(optimizer, T_max10) # 第二阶段解冻最后两个残差块小学习率微调 def unfreeze_layers(model, unfreeze_from6): 解冻ResNet最后几个layer layers [model.layer1, model.layer2, model.layer3, model.layer4] for layer in layers[unfreeze_from - 4:]: for param in layer.parameters(): param.requires_grad True return model # 微调时用更小的学习率 # optimizer_finetune Adam(filter(lambda p: p.requires_grad, model.parameters()), lr1e-4)这段代码的核心逻辑第一阶段冻结骨干只训练新加的分类头学习率设 1e-3跑 10 个 epoch 左右让分类头先收敛。第二阶段解冻 layer3 和 layer4学习率降到 1e-4再跑 20 到 30 个 epoch。为什么要分两阶段因为如果一开始就解冻全部参数预训练权重会被随机初始化的分类头产生的大梯度破坏这就是常说的“灾难性遗忘”。分阶段微调能保住预训练学到的通用特征同时让模型适应宫颈细胞图像的特殊模式。提示weightsmodels.ResNet50_Weights.IMAGENET1K_V2是 torchvision 新版的写法旧版用pretrainedTrue。如果你装的 torchvision 版本较老改成pretrainedTrue即可。训练时的 batch size 设 32如果显存不够降到 16 或 8。学习率调度用余弦退火比 StepLR 更平滑。损失函数用交叉熵如果类别不平衡严重可以换成带权重的交叉熵或 Focal Loss。4. 训练与评估让模型真正学到宫颈细胞特征4.1 训练循环与关键监控指标训练循环本身不复杂但有几个细节决定成败。我一般监控四个指标训练损失、验证损失、验证准确率、验证集上的混淆矩阵。训练损失下降但验证损失上升说明过拟合两者都下降但准确率不涨说明学习率可能太小准确率震荡大说明 batch size 太小或学习率太大。import torch from sklearn.metrics import confusion_matrix, classification_report import matplotlib.pyplot as plt def train_one_epoch(model, loader, optimizer, criterion, device): model.train() running_loss 0.0 correct 0 total 0 for images, labels in loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) _, predicted outputs.max(1) total labels.size(0) correct predicted.eq(labels).sum().item() return running_loss / total, correct / total def evaluate(model, loader, criterion, device): model.eval() running_loss 0.0 correct 0 total 0 all_preds [] all_labels [] with torch.no_grad(): for images, labels in loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) running_loss loss.item() * images.size(0) _, predicted outputs.max(1) total labels.size(0) correct predicted.eq(labels).sum().item() all_preds.extend(predicted.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) return running_loss / total, correct / total, all_preds, all_labels # 完整训练流程 num_epochs_stage1 10 num_epochs_stage2 30 best_acc 0.0 for epoch in range(num_epochs_stage1): train_loss, train_acc train_one_epoch(model, train_loader, optimizer, criterion, device) val_loss, val_acc, _, _ evaluate(model, val_loader, criterion, device) scheduler.step() print(fStage1 Epoch {epoch1}: train_loss{train_loss:.4f}, train_acc{train_acc:.4f}, val_loss{val_loss:.4f}, val_acc{val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model_stage1.pth) # 解冻微调 model unfreeze_layers(model, unfreeze_from6) optimizer_finetune Adam(filter(lambda p: p.requires_grad, model.parameters()), lr1e-4) scheduler_finetune CosineAnnealingLR(optimizer_finetune, T_maxnum_epochs_stage2) for epoch in range(num_epochs_stage2): train_loss, train_acc train_one_epoch(model, train_loader, optimizer_finetune, criterion, device) val_loss, val_acc, _, _ evaluate(model, val_loader, criterion, device) scheduler_finetune.step() print(fStage2 Epoch {epoch1}: train_loss{train_loss:.4f}, train_acc{train_acc:.4f}, val_loss{val_loss:.4f}, val_acc{val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth) # 测试集评估 model.load_state_dict(torch.load(best_model.pth)) test_loss, test_acc, preds, labels evaluate(model, test_loader, criterion, device) print(fTest Accuracy: {test_acc:.4f}) print(classification_report(labels, preds, target_namestrain_dataset.classes)) print(confusion_matrix(labels, preds))训练过程中我习惯每 5 个 epoch 存一次 checkpoint防止训练中断后从头再来。best_model.pth只保存验证集准确率最高的那一轮。测试集只在最后用一次不要边训练边在测试集上调参那样测试集就变成了验证集报出来的指标没有意义。4.2 评估指标解读与结果分析宫颈癌智能诊断系统不能只看准确率。医学图像分类里召回率比准确率更重要——漏诊一个异常细胞比误诊一个正常细胞后果严重得多。所以评估时要重点看每个类别的召回率和 F1 分数。假设测试集上得到这样的结果类别精确率召回率F1分数支持样本数superficial0.950.930.94120intermediate0.910.890.90115columnar0.880.920.9098parabasal0.930.900.91105metaplastic0.860.880.8782整体准确率 0.91macro F1 0.90。metaplastic 类别的精确率和召回率偏低说明这个类别和 columnar 容易混淆。这时候要回到混淆矩阵看具体是哪些样本分错了然后针对性做数据增强或调整类别权重。如果某个类别召回率低于 0.85我一般会做三件事一是检查这个类别的训练样本是否太少二是看增强后的图像是否失真三是考虑用 Focal Loss 给难分类样本更高权重。这些调整做完再跑一轮通常能提升 2 到 3 个百分点。5. 系统集成与避坑从模型文件到可演示的桌面应用5.1 用 PyQt5 搭建诊断界面模型训练完只是完成了 60% 的工作毕业设计还需要一个能演示的系统界面。我一般用 PyQt5 做桌面端轻量、跨平台、上手快。界面包含三个区域图像选择区、诊断结果区、置信度展示区。import sys import torch from PyQt5.QtWidgets import (QApplication, QMainWindow, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QWidget, QFileDialog) from PyQt5.QtGui import QPixmap from torchvision import transforms from PIL import Image class DiagnosisWindow(QMainWindow): def __init__(self, model, class_names, device): super().__init__() self.model model self.class_names class_names self.device device self.init_ui() def init_ui(self): self.setWindowTitle(宫颈癌智能诊断系统) self.setGeometry(100, 100, 800, 600) # 图像显示标签 self.image_label QLabel(请选择细胞图像) self.image_label.setFixedSize(400, 400) self.image_label.setStyleSheet(border: 1px solid gray;) # 结果标签 self.result_label QLabel(诊断结果等待输入) self.confidence_label QLabel(置信度--) # 按钮 self.select_btn QPushButton(选择图像) self.select_btn.clicked.connect(self.load_image) self.diagnose_btn QPushButton(开始诊断) self.diagnose_btn.clicked.connect(self.diagnose) # 布局 left_layout QVBoxLayout() left_layout.addWidget(self.image_label) left_layout.addWidget(self.select_btn) right_layout QVBoxLayout() right_layout.addWidget(self.result_label) right_layout.addWidget(self.confidence_label) right_layout.addWidget(self.diagnose_btn) right_layout.addStretch() main_layout QHBoxLayout() main_layout.addLayout(left_layout) main_layout.addLayout(right_layout) container QWidget() container.setLayout(main_layout) self.setCentralWidget(container) def load_image(self): file_path, _ QFileDialog.getOpenFileName(self, 选择图像, , Images (*.png *.jpg *.bmp)) if file_path: self.current_image_path file_path pixmap QPixmap(file_path).scaled(400, 400) self.image_label.setPixmap(pixmap) def diagnose(self): if not hasattr(self, current_image_path): return # 预处理 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]) ]) image Image.open(self.current_image_path).convert(RGB) tensor transform(image).unsqueeze(0).to(self.device) # 推理 self.model.eval() with torch.no_grad(): outputs self.model(tensor) probs torch.softmax(outputs, dim1) confidence, predicted probs.max(1) class_name self.class_names[predicted.item()] self.result_label.setText(f诊断结果{class_name}) self.confidence_label.setText(f置信度{confidence.item():.2%}) # 启动应用 # app QApplication(sys.argv) # model build_model(num_classes5) # model.load_state_dict(torch.load(best_model.pth, map_locationcpu)) # window DiagnosisWindow(model, train_dataset.classes, torch.device(cpu)) # window.show() # sys.exit(app.exec_())界面逻辑很直接选图、预处理、推理、显示结果。注意推理时要加torch.no_grad()否则会构建计算图显存占用大且速度慢。map_locationcpu保证在没有 GPU 的机器上也能加载模型。5.2 避坑记录宫颈癌智能诊断系统最常见的五个翻车点现象一训练准确率 99%测试准确率只有 60%。原因训练集和测试集划分时没有按患者 ID 划分同一个患者的细胞图像同时出现在训练集和测试集里模型记住了患者特征而不是细胞特征。 解决如果有患者 ID 信息按患者划分数据集如果没有至少保证同一张原始图像增强后的样本不跨集。现象二模型训练 loss 不下降一直停在 1.6 左右。原因学习率设太大或者数据归一化参数用错了。有人把 ImageNet 的 mean/std 用到灰度图上导致输入分布完全不对。 解决检查输入图像的通道数和归一化参数。灰度图要么转成三通道要么把 mean/std 改成单通道的值。学习率从 1e-3 开始试不降就降到 1e-4。现象三PyQt5 界面点诊断按钮后卡死。原因推理在主线程里跑模型加载和推理耗时阻塞了 UI 事件循环。 解决把推理放到 QThread 里或者至少加一个等待提示。毕业设计演示时提前加载好模型不要每次点按钮都重新加载。现象四换一台电脑跑系统报错 “No module named ‘torch’”。原因依赖没有打包。毕业设计答辩时经常要换电脑演示环境不一致直接翻车。 解决用 PyInstaller 打包成 exe或者提前在答辩电脑上装好依赖。PyInstaller 打包 torch 体积很大至少 1.5G但能保证跑起来。现象五混淆矩阵里 columnar 和 metaplastic 大量互错。原因这两个类别在形态学上确实相似模型学到的特征区分度不够。 解决针对这两个类别做针对性增强比如对 columnar 类别加更多的旋转和缩放或者用类别权重让模型更关注难分类样本还可以考虑用双分支网络一个分支学全局特征一个分支学局部纹理。6. 进阶技巧用 Grad-CAM 让诊断结果可解释毕业设计答辩时老师经常会问“你的模型为什么判断这张图是异常”如果只回答“因为 softmax 输出概率高”说服力不够。用 Grad-CAM 生成热力图标出模型关注的区域既能提升论文质量也能让答辩更从容。Grad-CAM 的原理不复杂对目标类别的输出分数求梯度梯度大的特征图区域就是模型关注的地方。代码实现如下import cv2 import numpy as np import torch import torch.nn.functional as F class GradCAM: def __init__(self, model, target_layer): self.model model self.target_layer target_layer self.gradients None self.activations None # 注册钩子 target_layer.register_forward_hook(self.save_activation) target_layer.register_full_backward_hook(self.save_gradient) def save_activation(self, module, input, output): self.activations output.detach() def save_gradient(self, module, grad_input, grad_output): self.gradients grad_output[0].detach() def generate(self, input_tensor, target_classNone): self.model.eval() output self.model(input_tensor) if target_class is None: target_class output.argmax(dim1).item() # 反向传播 self.model.zero_grad() one_hot torch.zeros_like(output) one_hot[0][target_class] 1 output.backward(gradientone_hot, retain_graphTrue) # 计算权重 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.squeeze().cpu().numpy() cam (cam - cam.min()) / (cam.max() - cam.min() 1e-8) return cam, target_class # 使用示例对ResNet50的layer4生成热力图 # grad_cam GradCAM(model, model.layer4[-1]) # cam, pred_class grad_cam.generate(input_tensor) # 叠加到原图 # heatmap cv2.applyColorMap(np.uint8(255 * cam), cv2.COLORMAP_JET) # overlay cv2.addWeighted(original_img, 0.6, heatmap, 0.4, 0)这段代码里target_layer选model.layer4[-1]这是 ResNet50 最后一个残差块特征图分辨率 7×7既能保留空间信息又有足够的语义信息。生成的 cam 归一化到 0 到 1再用 OpenCV 的applyColorMap转成热力图叠加到原图上。我一般会在论文里放三组对比图正确分类且热力图集中在细胞核区域的、正确分类但热力图分散的、错误分类且热力图跑到背景区域的。第三组最能说明问题——模型被背景干扰了这时候就需要检查数据增强是否引入了背景偏差或者考虑加一个分割网络先把细胞区域抠出来。做这个毕业设计我最大的教训是不要一上来就追求 SOTA 模型。ResNet50 加好的预处理和增强比随便套一个 ViT 但数据没处理好效果要好得多。先把基线跑通再逐步调优每一步都有记录答辩时才有东西讲。希望帮到你。本文还有配套的精品资源点击获取