ARTICLE DETAIL

资讯详情

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

基于深度学习的乳腺超声图像BI-RADS分级Python源码实战解析

基于深度学习的乳腺超声图像BI-RADS分级Python源码实战解析 简介这是一份毕业设计级别的完整项目围绕乳腺肿瘤超声图像开展BI-RADS分级研究基于深度学习完成数据划分、模型训练与自动分级预测主要面向计算机、医学影像等专业的毕设学生以及需要完整实战项目的学习者。资源共1219个文件压缩包约588.89MB其中包含1200张png超声图像数据、6个Python源码脚本、2个pth模型权重文件另有xml配置文件与pyc缓存等代码和数据可直接配合运行。项目经过导师认可评审99分核心模型与脚本完整结果可复现特别适合作为毕业设计、课程设计或期末大作业。目前已有53人学习下载。使用这份资源既可快速跑通完整流程也能结合源码理解超声图像预处理、BI-RADS分级标签构建及模型评估等环节便于后续改进和论文撰写。1. 答辩前夜我才想明白这份源码的价值不在模型在完整链路毕业设计答辩前三天我盯着训练曲线心里发虚训练集准确率已经到 96%验证集却像心电图一样上下跳。导师只问了一句——你的 BI-RADS 分级结果凭什么可信后来我把整个项目按数据预处理、模型训练、评估可视化拆开重跑才发现问题根本不在网络结构而在数据划分和标签分布。这份基于深度学习的乳腺肿瘤超声图像 BI-RADS 分级 Python 源码加全部数据帮你把医学图像分级项目从 PNG 超声图一路做到 BI-RADS 级别标签包含数据清洗、ROI 图预处理、CNN 训练、评估与可视化完整流程。适合正在做毕业设计、课程设计或期末大作业的学生也适合想拿真实医学图像练手深度学习的入门者。它不是给你一个黑匣子而是把你该踩的坑提前标了出来。2. 数据准备与预处理从 PNG 文件名反推作者的预处理管线2.1 先盘目录文件名里藏着预处理顺序这份资源里最显眼的不是 Python 脚本而是那一堆成对出现的 PNG 文件。sample97_pseudo_rf1_roi1.png这种命名方式几乎是医学图像预处理的标准习惯sample是患者编号pseudo大概率是伪彩图或中间特征图的意思rf1/rf2通常代表不同超声扫描帧或不同特征通道roi1/roi2则说明已经做过 ROI 裁剪。也就是说原作者在把图喂给模型之前已经完成了从原始超声序列图到带标注 ROI 图块的转换。拿到手第一步不是直接跑训练而是先把目录结构摸清楚。我一般会用下面这段脚本做盘点from pathlib import Path from collections import Counter data_dir Path(乳腺肿瘤超声) png_files sorted(data_dir.rglob(*.png)) print(PNG 文件总数:, len(png_files)) # 看文件后缀分布确认有没有混入 jpg / bmp / dcm suffix_counter Counter(p.suffix.lower() for p in png_files) print(格式分布:, suffix_counter) # 解析文件名前 10 个样本的命名结构 name_parts [p.stem.split(_) for p in png_files[:10]] for parts in name_parts: print(parts)这段代码用rglob(*.png)递归收集所有 PNG 文件顺便统计格式分布。如果你的数据里混了 DICOM 格式的.dcm文件预处理脚本就得重写因为 DICOM 不能直接用 OpenCV 读。解析文件名这一段能帮你确认sample、rf、roi这三段信息在每张图里是否齐全——如果有的文件名缺了roi段说明那张图可能没做裁剪喂进模型前要单独处理。超声数据的 ROI 是否统一直接决定了模型输入的空间尺寸和特征范围。2.2 读图与格式确认灰度图与三通道的坑超声图像通常以灰度存储但有些采集软件导出的 PNG 是 RGB 三通道的“灰图”像素三个通道数值完全一样。这个细节不影响人眼看图但对模型改造影响很大。读第一张图确认模式from PIL import Image sample_path str(png_files[0]) img Image.open(sample_path) print(图像模式:, img.mode) # L 表示单通道灰度RGB 表示三通道 print(图像尺寸:, img.size) # 原始 ROI 图通常是正方形或接近正方形这一步很关键。如果img.mode是L你有两条路把单通道灰度图复制成三通道喂给 ImageNet 预训练模型或者把模型第一层卷积改成输入通道为 1。如果已经是RGB也别高兴太早要确认三个通道数值是否完全一致——有些导出工具会在存储时做轻微压缩导致三个通道出现极小差异。我习惯的做法是统一转成 RGB 三通道理由是能直接加载 ImageNet 预训练权重省掉一部分训练时间。医学图像虽然和自然图像分布差异很大但预训练权重提供的底层边缘、纹理特征仍然是有效的。2.3 预处理管线中值滤波加 CLAHE 的参数组合超声图像最大的问题是斑点噪声和回声强度分布不均。直接拿原始灰度图训练模型会把噪声当成特征学进去。常见的做法是两步走先做中值滤波降噪再做 CLAHE 增强局部对比度。我用 OpenCV 实现如下import cv2 import numpy as np def preprocess_ultrasound(img_path, target_size(224, 224)): # 读成单通道灰度图 img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: raise FileNotFoundError(f无法读取图像: {img_path}) # 第一步中值滤波去斑点噪声ksize5 是超声图的常用起点 img cv2.medianBlur(img, 5) # 第二步CLAHE 限制对比度自适应直方图均衡 clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) img clahe.apply(img) # 统一尺寸保持宽高比不变 img cv2.resize(img, target_size, interpolationcv2.INTER_CUBIC) # 归一化到 [0,1] img img.astype(np.float32) / 255.0 return img参数说明medianBlur的ksize5是比较保守的核大小既能压住斑点噪声又不会把边界磨掉。如果图像噪点很密集可以试ksize7但边界模糊的风险会加大超声图像的肿瘤边界本身就不清晰不建议用更大的核。CLAHE的两个参数里clipLimit2.0控制对比度放大的上限值越大局部对比越强但放太大会把背景噪声也放大tileGridSize(8, 8)把图片分成 8x8 的块做直方图均衡适合 224x224 的输入尺寸。整套预处理的目标是让肿瘤区域的内部回声和边界形态更可辨这是 BI-RADS 分级时医生主要看的两个特征维度。2.4 Dataset 封装与分层划分固定随机种子是第一纪律医学图像数据集的标签分布天然不均衡。BI-RADS 分级里低级别样本通常远多于高级别样本如果随机划分训练集和验证集很可能验证集里某个类别只有一两张图训练结果完全没参考价值。我固定用stratify做分层划分from sklearn.model_selection import train_test_split # labels 是与 png_files 一一对应的标签列表内容为 0/1/2/3/4 等类别索引 train_paths, val_paths, train_labels, val_labels train_test_split( png_files, labels, test_size0.2, stratifylabels, # 按标签比例分层抽样 random_state42 # 固定随机种子保证每次划分一致 )stratifylabels会让训练集和验证集中的类别比例与全量数据保持一致避免“验证集里某个类别只有一张图”的尴尬。random_state42必须固定否则每次运行划分结果都不一样调参时对比的基线就乱了。这一步做完再把预处理封装进 Datasetfrom torch.utils.data import Dataset from PIL import Image class USBiRADSDataset(Dataset): def __init__(self, img_paths, labels, transformNone): self.img_paths img_paths self.labels labels self.transform transform def __len__(self): return len(self.img_paths) def __getitem__(self, idx): img preprocess_ultrasound(str(self.img_paths[idx])) # 单通道复制成三通道喂给 ImageNet 预训练模型 img np.stack([img] * 3, axis2) img (img * 255).astype(np.uint8) img Image.fromarray(img) if self.transform: img self.transform(img) return img, self.labels[idx]Dataset 里把预处理产生的[0,1]浮点数组先转成 PIL 图这样才能接 torchvision 的增强变换。训练集和验证集用不同的 transform——训练集做随机旋转、水平翻转、轻微缩放验证集只做标准化。增强的强度要保守超声图像翻转后解剖结构仍然合理但过度的随机裁剪可能会把 ROI 区域切掉一部分。预处理步骤推荐参数说明中值滤波ksize5压斑点噪声保留边界CLAHEclipLimit2.0, tileGridSize(8,8)提升局部对比度尺寸统一224x224匹配 ResNet 输入归一化/255.0 转 [0,1]稳定训练3. 模型搭建与训练ResNet 主干改造、训练配置与参数调优3.1 骨干选型为什么是 ResNet 而不是 Transformer拿超声图像做分类我第一推荐还是 ResNet不是因为它先进而是因为它在小数据集上不容易翻车。医学图像数据集通常只有几百到几千张Vision Transformer 这类结构需要大量数据才能发挥全局注意力优势数据量不够时反而容易过拟合。超声图像的 BI-RADS 分级核心判别信息在局部——肿瘤边界是否光滑、内部回声是否均匀、后方回声是否衰减——这些特征用卷积的局部感受野天然契合。具体选 ResNet18 还是 ResNet50取决于你的数据量。我一般先用 ResNet18 跑通全流程因为它参数量小训练快过拟合风险低如果验证集准确率明显有瓶颈再换 ResNet50 验证。答辩时这个选型逻辑也站得住先轻量基线再逐步加容量每一步都有数据支撑。3.2 改造 ResNet 的输入与分类头torchvision 里加载 ResNet改第一层卷积和最后的全连接层import torch import torch.nn as nn from torchvision import models num_classes 5 # BI-RADS 分级任务的具体类别数按你的标注粒度定 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) # 方案一把预训练的第一层卷积权重从 3 通道均值化后复制成 3 通道 # 适用于输入是灰度图复制成三通道的情况 old_conv model.conv1.weight.data # shape: (64, 3, 7, 7) model.conv1.weight.data old_conv.mean(dim1, keepdimTrue).repeat(1, 3, 1, 1) # 替换全连接层输出维度 model.fc nn.Linear(model.fc.in_features, num_classes)如果 torchvision 版本较老weightsmodels.ResNet18_Weights.IMAGENET1K_V1的写法会报错换成pretrainedTrue即可。参数说明old_conv.mean(dim1, keepdimTrue)把三个通道的卷积核权重取平均压缩成单通道再通过repeat(1, 3, 1, 1)复制回三通道。这样做的好处是保留了预训练权重的底层特征而不是重新随机初始化第一层。全连接层直接替换成与类别数匹配的线性层这一层随机初始化不影响整体效果因为它本来就需要在新任务上重新学习。3.3 训练脚本优化器、学习率计划与保存策略训练配置是这套代码里最值得抄的部分。我用 AdamW 配合余弦退火学习率这个组合在医学图像小数据集上收敛稳定import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max30, eta_min1e-6) best_val_acc 0.0 num_epochs 30 for epoch in range(num_epochs): model.train() train_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() train_loss loss.item() # 验证 model.eval() val_correct 0 val_total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) val_correct (predicted labels).sum().item() val_total labels.size(0) val_acc val_correct / val_total scheduler.step() if val_acc best_val_acc: best_val_acc val_acc torch.save(model.state_dict(), best_model.pth) print(fEpoch {epoch1}: 保存最佳模型验证集准确率 {val_acc:.4f})lr1e-4是微调预训练权重的安全起点如果从零训练可以放宽到 1e-3但医学图像小数据集不建议从零训练。weight_decay1e-4做 L2 正则抑制过拟合。CosineAnnealingLR的T_max30要和num_epochs对齐意思是在 30 个 epoch 内学习率从 1e-4 余弦下降到 1e-6。保存策略只保留验证集准确率最高的那一次权重防止最后几个 epoch 过拟合后把好权重覆盖掉。3.4 处理类别不均衡先统计再加权Categories 不均衡在 BI-RADS 任务里几乎是必然的。比如 2 级、3 级样本可能有 300 张4C 级可能只有 40 张。模型天然偏向多数类最后的准确率虚高但没有实际意义。处理方式有两种先统计再决定from collections import Counter label_counter Counter(labels) print(label_counter) total len(labels) class_weights [] for i in range(num_classes): class_weights.append(total / (num_classes * label_counter[i])) criterion nn.CrossEntropyLoss( weighttorch.tensor(class_weights, dtypetorch.float32).to(device) )total / (num_classes * label_counter[i])是经典的逆频率加权少数类的 loss 权重自动变大。要注意的是如果某个类别完全没有样本label_counter[i]会是 0直接除会报错所以先用Counter输出确认每个类别的数量。另一种做法是WeightedRandomSampler在采样阶段就调整类别比例两种方案效果接近。我一般先加 loss 权重因为它改动最小不需要重写 DataLoader。4. 避坑清单从数据划分到训练过程的五个翻车现场4.1 训练集 Accuracy 虚高验证集抖成心电图现象训练集准确率稳定攀升到 95% 以上验证集却忽高忽低有的 epoch 70%有的 epoch 85%。原因最常见的两个原因一是标签分布不均模型学会了输出多数类二是验证集样本太少某个类别只有几张图预测错一张准确率就掉几个点。解决先画标签分布直方图确认每个类的样本量。然后用stratify做分层划分确保验证集类别比例和全量一致。如果某个类别样本实在少考虑把类别合并比如把 4A、4B、4C 合并成“可疑恶性”一类这在 BI-RADS 临床评估里也是合理的归并方式。4.2 预训练权重加载失败或 Loss 不降现象torch.load报错说 weight 尺寸不匹配或者模型能跑但 loss 一直不降。原因预训练权重是为 1000 类 ImageNet 训练的全连接层设计的你把model.fc换了之后旧权重里fc.weight和fc.bias的形状对不上更隐蔽的是如果你改了conv1的输入通道数第一层权重也会不匹配。解决替换fc和conv1是必须的所以加载权重时用strictFalse让 PyTorch 忽略不匹配的层state_dict torch.load(resnet18.pth, map_locationcpu) model.load_state_dict(state_dict, strictFalse)strictFalse会跳过形状不一致的 key只加载匹配的层。注意打印一下加载日志确认fc.weight确实是被跳过的而不是被错误加载。如果连卷积层权重都不匹配检查你改conv1时是否用了均值化复制的方法。4.3 路径一换就崩中文路径和目录层级现象本地跑得好好的换台电脑运行提示FileNotFoundError或者读图全部失败。原因这份资源是 Windows 下打包的目录名带有中文。Linux 服务器上如果编码不一致rglob可能扫不出文件更常见的坑是有人在代码里硬编码了E:/乳腺肿瘤超声/这种绝对路径。解决把所有路径改成pathlib.Path相对路径数据集目录和项目代码放在同级目录下。跑任何项目前先执行一次盘点脚本确认len(png_files)不为 0。中文目录名在 Windows 上通常没问题但如果你部署到 Docker 容器里记得把挂载路径也统一成 UTF-8 编码。4.4 Loss 在 0.7 左右躺平模型学成了“平均人”现象训练 loss 降到 0.7 附近后完全不动准确率稳定在 50% 或 60% 不变像在猜硬币。原因深度学习的一个玄学时刻——模型把所有样本都预测成了多数类。0.7 这个数值很典型等于-ln(1/2)附近说明模型输出的概率分布趋近于均匀或只剩一个类。造成这个问题的原因可能是标签和图片没有对齐也可能是数据预处理时图像全成了黑色或全白。解决先做“小批量过拟合”测试拿 5 张图训练 10 个 epoch如果 loss 能降到接近 0说明模型和数据管线没问题如果 loss 不动检查预处理输出把preprocess_ultrasound的结果保存一张 PNG 肉眼看看。我遇到过一次增强参数把图像随机裁剪成了纯黑块肉眼检查一秒钟就发现了比调半天参数快得多。4.5 同一个患者的多张图同时进了训练集和验证集现象验证集准确率 90%但答辩时导师随便抽一张新病人的超声图模型预测结果明显不对。原因超声数据通常一个患者有多帧图像如果你按文件粒度随机划分同一个患者的图可能同时出现在训练集和验证集。模型等于提前见过这个人的纹理特征验证集分数是虚的。解决按患者 ID 分组划分保证同一个sample的所有图只进训练集或只进验证集patient_ids [p.stem.split(_)[0] for p in png_files] train_patients, val_patients train_test_split( sorted(set(patient_ids)), test_size0.2, random_state42 ) train_paths [p for p in png_files if p.stem.split(_)[0] in train_patients] val_paths [p for p in png_files if p.stem.split(_)[0] in val_patients]这一步是医学图像项目里最容易漏的也是答辩时最能体现你有没有真做过医学项目的地方。5. 评估与进阶用加权 Kappa、混淆矩阵和 Grad-CAM 把结果讲清楚5.1 用加权 Kappa 替代 Accuracy 做评估BI-RADS 分级是有序的——2 级到 5 级是恶性程度递增的关系把 2 级错分成 3 级的严重程度远低于把 2 级错分成 5 级。Accuracy 把所有错误一视同仁体现不了这个差异。医学图像领域常用的指标是加权 Kappa 系数直接算一下from sklearn.metrics import cohen_kappa_score, confusion_matrix cohen_kappa_score(val_labels, val_preds, weightsquadratic) confusion_matrix(val_labels, val_preds)weightsquadratic意味着错分等级越远惩罚越重。这个指标在答辩时比 Accuracy 有说服力得多。5.2 单张推理脚本从超声图到 BI-RADS 级别的完整链路训练完不是结束还要能对单张图做推理。把预处理和模型串成一条完整的链路def predict_single_image(img_path, model, label_map, device): img preprocess_ultrasound(img_path) img np.stack([img] * 3, axis2) img (img * 255).astype(np.uint8) img Image.fromarray(img).convert(RGB) img transform(img).unsqueeze(0).to(device) model.eval() with torch.no_grad(): logits model(img) probs torch.softmax(logits, dim1) conf, pred torch.max(probs, dim1) return label_map[pred.item()], conf.item()label_map是你自己定义的类别映射比如{0: BI-RADS 2, 1: BI-RADS 3, 2: BI-RADS 4A, 3: BI-RADS 4B, 4: BI-RADS 4C/5}。conf是置信度如果模型输出 0.4 这种低置信度预测说明这个样本本身处于类别边界需要重点排查。5.3 两个能让答辩加分的小改动第一个改动是把多分类任务往有序回归方向靠。BI-RADS 级别天然有序一种常见做法是保持 softmax 分类的同时额外加一个回归头预测连续的分级值用 MSE 做辅助损失。这样模型不仅知道“这图是 4A”还隐含了“4A 离 3 比离 5 更近”的顺序信息。第二个改动是 Grad-CAM 可视化用pytorch_grad_cam库from pytorch_grad_cam import GradCAM cam GradCAM(modelmodel, target_layers[model.layer4[-1]], use_cudaTrue) grayscale_cam cam(input_tensor)[0]把热力图叠加到原图上你能直观看到模型是根据肿瘤的哪个区域做出的分级判断。答辩时拿出这张图配合 Kappa 系数比单讲准确率有说服力得多。从那以后我每次拿到医学图像数据集第一件事就是画标签分布直方图第二件事就是固定随机种子做患者级数据划分。这两个习惯帮我省下了无数个“深度学习玄学”的夜晚。希望帮到你。本文还有配套的精品资源点击获取
返回列表