简介:这是一份面向计算机相关专业毕业生的深度学习实战项目,聚焦乳腺肿瘤超声图像的BI-RADS自动分级。项目基于Python实现,包含完整源码、预处理后的超声图像数据集与训练权重,可直接运行用于模型训练、评估与可视化,也可作为毕业设计、课程设计或期末大作业的参考范本。资源共1219个文件,其中1200张png超声图像构成核心数据,6个py源码文件覆盖数据加载、模型构建与训练推理流程,另含2个pth权重文件、配置文件和说明文档,整体压缩包约589MB,目录结构清晰便于按模块复现。目前已有53人学习下载,适合希望快速上手深度学习医学图像分类任务的入门者。项目经导师指导并获得高分,代码完整度较高,下载后按说明即可启动实验,有助于从数据处理到模型评估完整理解BI-RADS分级任务的解决链路,是兼顾实战与论文研究的可靠资料。
1. 深度学习做乳腺超声 BI-RADS 分级:这份源码能帮你把毕设从 60 分冲到 90 分
乳腺超声的 BI-RADS 分级是放射科医生每天都要写的结论,从 0 到 6 级,级别越高恶性风险越大。过去靠人眼判断形态、边界、钙化这些特征,新手医生和资深医生之间的判读一致性并不高。用深度学习 CNN 自动做分级,本质上是一个图像分类任务,输入乳腺超声图,输出 BI-RADS 等级。这个方向作为毕业设计很讨巧——医学背景、算法落地、可视化展示全都有,而且数据本身是公开可获取的,不需要自己跑医院采集。这份源码把整个流程都串起来了:数据预处理、ROI 裁剪、模型训练、评估可视化,完整跑通就是一套可直接答辩的毕设。适合正在做深度学习方向课设或毕设的同学,也适合想快速上手医学图像分类项目的人。
2. 数据与预处理:先搞懂超声图像,再谈模型
2.1 为什么超声图不能直接扔进网络
乳腺超声图像和自然图像最大的区别在于:它是灰度图、噪声多、病灶和正常组织的对比度低,而且一张图里往往只有一小块区域是真正的病灶。直接拿整张图去训练,模型大概率学到的是探头位置、图像亮度这些无关特征,而不是病灶本身的纹理和形态。这就是为什么源码里专门做了 ROI(Region of Interest)裁剪。
从项目文件里的 sample97_pseudo_rf1_roi1.png、sample63_pseudo_rf2_roi2.png 这些命名可以看出,数据是按样本编号、随机森林特征序号、ROI 序号来组织的。也就是说,每个样本对应多张 ROI 图,这些图才是真正喂给网络的输入。我一般拿到超声数据的第一步不是写模型,而是把所有图过一遍,统计尺寸分布和灰度范围,确认病灶区域是不是都集中在图像中部。
import cv2 import numpy as np import glob # 统计所有样本图的尺寸和灰度分布 paths = glob.glob('data/roi/*.png') sizes = [] means = [] for p in paths: img = cv2.imread(p, cv2.IMREAD_GRAYSCALE) sizes.append(img.shape) means.append(img.mean()) print("尺寸分布:", set(sizes)) print("灰度均值范围:", min(means), "-", max(means))这段代码用 OpenCV 批量读取 ROI 图片,统计尺寸和灰度均值。为什么要做这一步?因为后续数据增强和归一化的参数都依赖这些统计量。如果发现有的图是 224x224,有的是 256x256,就需要统一 resize;如果灰度均值差异很大,说明可能存在不同的采集设备或增益设置,归一化时就要特别小心。
2.2 数据增强:不要为了凑数量乱增强
乳腺超声数据量通常不大,一份毕设的样本量可能在几百到一千张左右,这种规模直接训练深度网络几乎必然过拟合。数据增强是必须的,但不是随便翻翻转转就行。
我见过有人对超声图做强烈的色彩抖动增强,结果完全失真——超声图是灰度图,色调抖动这类针对自然图像设计的增强手段在这里会产生大量不真实的样本,让模型学到错误的纹理。超声图合理的增强应该围绕几何变换和轻度噪声扰动来做。
import albumentations as A train_transform = A.Compose([ A.Resize(224, 224), A.HorizontalFlip(p=0.5), A.VerticalFlip(p=0.3), # 超声图上下翻转不改变语义 A.RandomRotate90(p=0.5), A.ShiftScaleRotate(shift_limit=0.05, scale_limit=0.1, rotate_limit=15, p=0.5), A.RandomBrightnessContrast(brightness_limit=0.1, contrast_limit=0.1, p=0.3), A.GaussNoise(var_limit=(10.0, 30.0), p=0.2), # 模拟超声噪声 ]) val_transform = A.Compose([ A.Resize(224, 224), ])这里用 Albumentations 库做增强,它是医学图像领域最常用的增强库。重点看两个参数:GaussNoise 的 var_limit 控制在 10 到 30,因为超声图本身就有斑点噪声,加一点高斯噪声可以让模型更鲁棒,但加太多会掩盖真实纹理。RandomBrightnessContrast 的幅度也调得很小,亮度变化过大在超声图像里意味着探头压力或增益变化,属于物理上不合理的扰动。
2.3 数据划分:最容易被忽略的坑
数据集划分看似简单,但医学影像数据有一个天然陷阱:同一个病人的多张图可能同时出现在训练集和验证集里。这在超声数据里很常见——同一病灶拍了多个切面,或者同一个样本生成了多张 ROI。如果按文件随机划分,模型实际上已经"见过"验证集的病人了,验证分数会虚高,答辩时经不起老师追问。
import pandas as pd from sklearn.model_selection import GroupShuffleSplit # 假设 data.csv 里有 patient_id 和 label 两列 df = pd.read_csv('data.csv') gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, val_idx = next(gss.split(df, groups=df['patient_id'])) train_df = df.iloc[train_idx] val_df = df.iloc[val_idx] print("训练集病人数:", train_df['patient_id'].nunique()) print("验证集病人数:", val_df['patient_id'].nunique())GroupShuffleSplit 是 sklearn 里专门做分组划分的工具。核心在 groups 参数:传 patient_id,它就能保证同一个病人的所有图像只会出现在训练集或验证集的一侧。这一步直接影响模型评估的可信度,很多医学影像比赛和论文里都明确要求按病人划分,毕设里主动做这一点,答辩时是加分项。
3. 模型选型与训练:ResNet50 为什么是稳妥选择
3.1 骨干网络:别在选型上浪费时间
BI-RADS 分级本质上是一个多分类任务,类别一般是 0-6 中的几个有效等级。骨ϗ干网络的选择直接决定训练速度和最终精度。对于毕设场景,我的建议是首选 ResNet50,原因有三点:预训练权重好找且稳定、显存占用在 RTX 3060 级别就能跑、调参资料多遇到问题容易搜到方案。
EfficientNet 在 ImageNet 上精度更高,但 B4 以上的版本输入分辨率大、显存吃紧,而且超声这种纹理简单的图像上,EfficientNet 的增益并不明显。ResNet50 在这个任务上已经够用,训练时间也可控。如果数据量特别少(几百张),可以换成 ResNet18,训练更快且过拟合风险更低。
import torch import torch.nn as nn from torchvision import models class BIRADSClassifier(nn.Module): def __init__(self, num_classes=4, pretrained=True): super().__init__() self.backbone = models.resnet50(pretrained=pretrained) in_features = self.backbone.fc.in_features # 替换最后一层,适配 BI-RADS 分类数 self.backbone.fc = nn.Sequential( nn.Dropout(p=0.3), nn.Linear(in_features, 256), nn.ReLU(inplace=True), nn.Dropout(p=0.2), nn.Linear(256, num_classes) ) def forward(self, x): return self.backbone(x)这个分类头加了两层 Dropout 和一层中间全连接。为什么要这么做?因为超声图像数据集通常不大,直接用一个线性层输出分类结果,模型很容易记住训练集的特征组合。中间的 256 维全连接层相当于让模型先学习一个"压缩特征",再映射到类别,能提升泛化能力。Dropout 0.3 和 0.2 是根据经验设置的,如果训练集很小可以加到 0.5。
3.2 类别不均衡:加权损失函数解决
BI-RADS 分级数据天然不均衡:BI-RADS 3 类(可能良性)和 4A 类(低度可疑)的样本通常很多,而 BI-RADS 5 类(高度提示恶性)和 0 类(评估不完整)样本很少。如果直接用普通的交叉熵损失,模型会倾向于把所有样本都预测成样本量最多的类别,整体准确率看着挺高,但少数类的召回率几乎为 0。
解决办法是计算每个类别的样本权重,在损失函数里给少数类更高的惩罚。
import torch.nn.functional as F from sklearn.utils.class_weight import compute_class_weight import numpy as np # 计算类别权重 labels = train_df['label'].values class_weights = compute_class_weight('balanced', classes=np.unique(labels), y=labels) class_weights = torch.tensor(class_weights, dtype=torch.float32).cuda() criterion = nn.CrossEntropyLoss(weight=class_weights) # 训练循环中使用 for images, targets in train_loader: images, targets = images.cuda(), targets.cuda() outputs = model(images) loss = criterion(outputs, targets) optimizer.zero_grad() loss.backward() optimizer.step()compute_class_weight 的 balanced 模式会自动计算权重:样本数越少的类别权重越大。具体公式是 n_samples / (n_classes * np.bincount(y))。这里要注意的是,权重必须基于训练集的标签计算,不能混入验证集的信息,否则会有轻微的数据泄漏。
3.3 训练流程与学习率策略
训练过程的设置比模型结构更影响最终效果。一个常见的错误是全程用一个固定学习率,结果就是前期收敛慢,后期在最优解附近震荡。
import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR EPOCHS = 50 BATCH_SIZE = 16 LR = 1e-4 model = BIRADSClassifier(num_classes=4).cuda() optimizer = optim.AdamW(model.parameters(), lr=LR, weight_decay=1e-4) scheduler = CosineAnnealingLR(optimizer, T_max=EPOCHS, eta_min=1e-6) for epoch in range(EPOCHS): model.train() train_loss = 0.0 for images, labels in train_loader: images, labels = images.cuda(), labels.cuda() outputs = model(images) loss = criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() train_loss += loss.item() * images.size(0) scheduler.step() avg_loss = train_loss / len(train_loader.dataset) print(f"Epoch {epoch+1}/{EPOCHS}, Loss: {avg_loss:.4f}")用了 AdamW 而不是 Adam,因为 AdamW 的解耦权重衰减在迁移学习场景下能更好地抑制过拟合。weight_decay 设成 1e-4,太小起不到正则化作用,太大会让模型欠拟合。CosineAnnealingLR 让学习率从 1e-4 余弦下降到 1e-6,前期快速收敛后期精细调整。Batch Size 设 16 是因为医学图像通常分辨率高、显存消耗大,如果你的显卡有 12G 以上显存可以试着调到 32。
4. 避坑与常见问题:训练医学图像分类的五个血泪教训
4.1 显存不足:OOM 爆掉
现象:训练到第二个 epoch 时报 CUDA out of memory,进程直接死掉。
原因:输入图像尺寸太大,或者 Batch Size 设置过高。医学图像经常是 512x512 甚至更高分辨率,如果预处理时没有统一缩放到 224x224 或 256x256,显存开销会翻好几倍。
解决:先检查数据加载器里有没有正确的 Resize 变换。如果已经是 224x224 还爆显存,把 Batch Size 从 16 降到 8,或者开启 gradient accumulation——每 2 个 batch 累积一次梯度再更新,相当于不增加显存但模拟了更大的 Batch Size。
# 梯度累积示例 accumulation_steps = 2 for i, (images, labels) in enumerate(train_loader): outputs = model(images) loss = criterion(outputs, labels) loss = loss / accumulation_steps # 归一化累积损失 loss.backward() if (i + 1) % accumulation_steps == 0: optimizer.step() optimizer.zero_grad()4.2 预训练权重加载失败:size mismatch
现象:load_state_dict 报错,提示 fc.weight 尺寸不匹配。
原因:因为替换了最后一层全连接层,预训练权重的 fc.weight shape 和模型定义的 shape 不一样。直接 load 整个状态字典必然报错。
解决:用 strict=False 加载,跳过不匹配的层,然后只把 backbone 部分加载进来。
checkpoint = torch.load('resnet50.pth') model_dict = model.state_dict() pretrained_dict = {k: v for k, v in checkpoint.items() if k in model_dict and 'fc' not in k} model_dict.update(pretrained_dict) model.load_state_dict(model_dict)这里的关键是过滤条件:'fc' not in k,把 backbone 的权重匹配进来,fc 层保持随机初始化。如果不确定哪些 key 匹配,可以先打印出来对比再过滤。
4.3 过拟合:训练集 99 分,验证集 70 分
现象:训练 Loss 持续下降,验证集准确率在某个 epoch 后开始掉头向下。
原因:数据量太少、模型容量太大、正则化不够。很多同学的第一个反应是换更复杂的模型,但方向反了——应该先做数据增强和正则化。
解决:优先检查数据增强是否生效、Dropout 是否加了、weight_decay 是否设置。还可以用早停机制,监控验证集 Loss 连续 5 个 epoch 不下降就停止训练。
best_val_loss = float('inf') patience = 5 counter = 0 for epoch in range(EPOCHS): train_one_epoch() val_loss = validate() if val_loss < best_val_loss: best_val_loss = val_loss counter = 0 torch.save(model.state_dict(), 'best_model.pth') else: counter += 1 if counter >= patience: print(f"Early stopping at epoch {epoch}") break4.4 验证集虚高:数据泄漏的隐蔽形式
现象:验证集准确率 98%,但自己拿一张新图测试效果很差。
原因:同病人在训练集和验证集都有图像。这是医学图像分类里最常见的隐藏错误,普通随机划分根本发现不了。
解决:用前面提到的 GroupShuffleSplit 按病人 ID 划分。如果没有病人 ID,至少按文件名前缀分组。这个坑在高分毕设里特别值得注意,答辩老师一问"你怎么保证没有数据泄漏",这就是一个非常加分的答点。
4.5 预测结果全部集中在一个类别
现象:混淆矩阵显示模型对所有样本都预测成 BI-RADS 3 类,准确率看着有 60%,但其他类别全是 0。
原因:类别不均衡严重,且没有用加权损失。
解决:用 compute_class_weight 计算权重并传入 CrossEntropyLoss。如果加权后还是不均衡,可以试试 Focal Loss,它对难分类样本的关注度更高。
class FocalLoss(nn.Module): def __init__(self, alpha=0.25, gamma=2.0): super().__init__() self.alpha = alpha self.gamma = gamma def forward(self, inputs, targets): ce_loss = F.cross_entropy(inputs, targets, reduction='none') pt = torch.exp(-ce_loss) focal_loss = (1 - pt) ** self.gamma * ce_loss return focal_loss.mean()Focal Loss 的思想是降低易分类样本的损失权重,让模型更关注难分的样本。gamma=2.0 是论文里的推荐值,alpha 通常需要根据数据分布调整,如果正负样本极度失衡可以尝试 alpha=0.5 甚至更大。
5. 评估与可视化:不只用一个 Accuracy 糊弄答辩
5.1 混淆矩阵与分类报告
毕设答辩时,老师最常问的问题是"你这个模型每一类的表现怎么样"。一个整体的 accuracy 完全回答不了这个问题。混淆矩阵能直观展示每一类的预测情况,分类报告能给出 precision、recall、f1-score 三个指标。
from sklearn.metrics import classification_report, confusion_matrix import seaborn as sns import matplotlib.pyplot as plt def evaluate(model, val_loader, class_names): model.eval() all_preds = [] all_labels = [] with torch.no_grad(): for images, labels in val_loader: images = images.cuda() outputs = model(images) _, preds = torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) print(classification_report(all_labels, all_preds, target_names=class_names)) cm = confusion_matrix(all_labels, all_preds) plt.figure(figsize=(8, 6)) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=class_names, yticklabels=class_names) plt.xlabel('Predicted') plt.ylabel('True') plt.savefig('confusion_matrix.png', dpi=150, bbox_inches='tight')classification_report 输出每一类的 precision、recall、f1-score 以及加权平均,这部分可以直接贴到论文的实验章节。需要注意的是,f1-score 在多分类不均衡数据下是最值得看的一个指标,它综合了 precision 和 recall,比 accuracy 靠谱得多。
5.2 ROC 曲线与 AUC:多分类怎么画
很多同学不知道多分类的 ROC 怎么画。标准做法是 one-vs-rest,也就是把每个类别当作正类,其余所有类别当作负类,每个类别画一条 ROC 曲线,计算对应的 AUC。
from sklearn.preprocessing import label_binarize from sklearn.metrics import roc_curve, auc def plot_multiclass_roc(model, val_loader, num_classes, class_names): model.eval() all_probs = [] all_labels = [] with torch.no_grad(): for images, labels in val_loader: images = images.cuda() outputs = torch.softmax(model(images), dim=1) all_probs.extend(outputs.cpu().numpy()) all_labels.extend(labels.numpy()) all_probs = np.array(all_probs) all_labels = np.array(all_labels) # one-vs-rest 二值化 y_bin = label_binarize(all_labels, classes=range(num_classes)) plt.figure(figsize=(8, 6)) for i in range(num_classes): fpr, tpr, _ = roc_curve(y_bin[:, i], all_probs[:, i]) roc_auc = auc(fpr, tpr) plt.plot(fpr, tpr, label=f'{class_names[i]} (AUC={roc_auc:.3f})') plt.plot([0, 1], [0, 1], 'k--', linewidth=0.8) plt.xlabel('False Positive Rate') plt.ylabel('True Positive Rate') plt.legend(loc='lower right') plt.grid(alpha=0.3) plt.savefig('roc_curves.png', dpi=150, bbox_inches='tight')label_binarize 会把多分类标签转成多列的 one-hot 格式,然后用 roc_curve 逐类计算 FPR 和 TPR。注意这里要传入 softmax 之后的概率值而不是 logits,这样画出来的 ROC 曲线才有意义。如果某个类别的样本特别少,它的 ROC 曲线会表现出明显的锯齿形状,这也是一个可以写进论文分析的点。
5.3 Grad-CAM:让模型"看图说话"的可解释性
医学图像分类在毕设答辩中最容易被问的就是"模型凭什么这么分类"。Grad-CAM 能生成热力图,指出来模型重点关注了图像的哪个区域。如果热力图集中在病灶区域而不是背景,就说明模型学到了有意义的特征,这个可视化是论文实验部分非常有力的证据。
import torch import torch.nn.functional as F from torchvision.transforms import functional as TF import cv2 import numpy as np def grad_cam(model, image_tensor, target_class=None): model.eval() # 注册钩子,获取最后一层卷积层的输出和梯度 feature_maps = [] gradients = [] def forward_hook(module, input, output): feature_maps.append(output) def backward_hook(module, grad_input, grad_output): gradients.append(grad_output[0]) # ResNet50 最后一个卷积层是 layer4 target_layer = model.backbone.layer4 forward_handle = target_layer.register_forward_hook(forward_hook) backward_handle = target_layer.register_full_backward_hook(backward_hook) output = model(image_tensor.unsqueeze(0)) if target_class is None: target_class = output.argmax(dim=1).item() model.zero_grad() one_hot = torch.zeros_like(output) one_hot[0, target_class] = 1 output.backward(gradient=one_hot) # 计算权重并生成热力图 weights = gradients[0].mean(dim=(2, 3), keepdim=True) cam = F.relu((weights * feature_maps[0]).sum(dim=1, keepdim=True)) cam = F.interpolate(cam, size=(224, 224), mode='bilinear', align_corners=False) cam = cam.squeeze().cpu().detach().numpy() cam = (cam - cam.min()) / (cam.max() - cam.min() + 1e-8) forward_handle.remove() backward_handle.remove() return cam, target_class这段代码的核心是注册 forward hook 和 backward hook 来捕获中间层的特征图和梯度。Grad-CAM 的原理是:最后一层卷积层的每个通道的梯度平均值,代表了该通道对目标类别的贡献度;用这个权重对特征图加权求和,再经过 ReLU 过滤负值,就得到显著区域。最后的归一化让热力图的值域落在 0 到 1 之间,方便叠加到原图上。
生成热力图后,可以用 OpenCV 的 applyColorMap 和 addWeighted 叠加到原始超声图上,保存成可插入论文的效果图。
6. 进阶用法:迁移学习的正确打开方式与参数调优技巧
6.1 分阶段解冻微调
如果你不想只停留在跑通 Baseline,想让模型精度再上一个台阶,可以试试分阶段微调。做法是:先冻结 backbone 只训练分类头,等分类头收敛后再解冻 backbone 的部分层。
# 第一阶段:冻结 backbone,只训练 fc 层 for param in model.backbone.parameters(): param.requires_grad = False optimizer = optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=5e-4) # 第一个阶段训练 10 个 epoch 后进入第二阶段 # 第二阶段:解冻 layer3 和 layer4 for name, param in model.backbone.named_parameters(): if 'layer3' in name or 'layer4' in name: param.requires_grad = True optimizer = optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-4)这个方案的核心逻辑是:预训练模型在 ImageNet 上学会了通用纹理特征,低层卷积的特征对超声图也有参考价值,但高层的语义特征和乳腺超声差异较大。所以先把高层解冻做自适应,低层保持冻结以免被小数据集带偏。第一阶段的学习率可以调大些,第二阶段必须调小,否则容易破坏已经收敛的权重。
6.2 寻找最佳输入分辨率
224x224 是 ImageNet 预训练的标准分辨率,但并不一定是超声图像的最优分辨率。超声病灶的纹理细节(比如微小钙化点)可能在 224x224 下被降采样抹掉了。如果你的显卡允许,可以试试 256x256 或 288x288,通常在 224 的基础上能提升 2-3 个点的准确率。
# 对比不同分辨率的验证集准确率 resolutions = [224, 256, 288] for res in resolutions: transform = A.Compose([ A.Resize(res, res), A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) val_loader = create_dataloader(val_df, transform, batch_size=16) acc = evaluate_accuracy(model, val_loader) print(f"Resolution {res}: Accuracy {acc:.4f}")注意分辨率变大后,除了显存占用增加,数据加载和预处理时间也会变长。做这个对比实验时,保持其他所有超参数一致,才能确定是分辨率带来的收益。
6.3 推理阶段的 TTA 技巧
Test-Time Augmentation 是推理阶段提升精度的一个白嫖技巧:把同一张测试图做几次不同的增强,分别预测,然后把预测概率取平均。它能提升模型对轻微位移和翻转的鲁棒性,通常能提高 1-2 个点。
def predict_with_tta(model, image, n_aug=4): model.eval() tta_transform = A.Compose([ A.HorizontalFlip(p=0.5), A.VerticalFlip(p=0.5), A.RandomRotate90(p=0.5), ]) probs_list = [] with torch.no_grad(): for _ in range(n_aug): augmented = tta_transform(image=image)['image'] tensor = torch.from_numpy(augmented).permute(2, 0, 1).float().unsqueeze(0).cuda() prob = torch.softmax(model(tensor), dim=1) probs_list.append(prob) avg_prob = torch.mean(torch.cat(probs_list), dim=0) return avg_prob.argmax().item()TTA 的原理很简单:虽然模型在训练时见过翻转和旋转的样本,但推理时只看了原图一个视角。通过多次增强平均,相当于让模型对同一个病灶从多个角度投票。不过要注意,测试时增强的次数并不是越多越好,4 次到 8 次之间的提升最明显,再增加收益就递减了。
6.4 部署与导出
最后一步是把训练好的模型导出成可复用的格式。如果是毕设展示,写一个简单的推理脚本就够了;如果想做成 Web 演示,可以导出成 ONNX 格式,方便接入后端服务。
dummy_input = torch.randn(1, 3, 224, 224).cuda() torch.onnx.export( model, dummy_input, "birads_model.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}}, opset_version=13 ) print("ONNX export done.")dynamic_axes 允许 batch 维度是动态的,这样导出后的模型可以一次预测多张图。ONNX 模型的推理速度通常比 PyTorch 原生的 eager mode 快,而且不依赖 PyTorch 环境,部署时候更省事。
这套流程走下来,从数据预处理到模型训练、评估、可视化和导出,整个闭环我都完整跑过。印象最深的一次是检查数据泄漏问题——一个同组同学的报告里准确率 97%,我帮他按病人 ID 重新划分后直接掉到 78%。从那以后我每次拿到医学影像数据,第一件事就是确认有没有 patient_id 字段,第二件事就是按 GroupShuffleSplit 重新划分。这个习惯让后续所有实验的结论都站得住脚。希望这份拆解能帮你在毕设路上少踩几个坑。
本文还有配套的精品资源,点击获取