ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

乳腺超声BI-RADS分级深度学习实战:从预处理到模型评估

乳腺超声BI-RADS分级深度学习实战:从预处理到模型评估 简介一份基于深度学习的乳腺肿瘤超声图像BI-RADS分级研究毕业设计项目包含完整Python源码与全部实验数据主要面向计算机相关专业本科生及需要实战练习的开发者可用于毕业设计、课程设计或期末大作业。项目由导师指导并获评审99分代码完整可运行初学者也能按要求复现整个流程。压缩包共1219个文件大小约588.89MB其中1200张png超声图像构成数据集6个py源码文件实现模型训练与分级推理另有pyc编译文件、xml配置、pth模型权重以及README说明文档等目录结构清晰便于按模块学习。目前已有53人学习下载适合希望快速搭建深度学习医疗影像分类项目的学习者借助全部数据与权重可深入理解BI-RADS分级的实现细节。1. 毕业设计做乳腺肿瘤超声BI-RADS分级这个课题值不值得投入超声图像上的BI-RADS分级是放射科医生在报告里写的那串“BI-RADS 3类 / 4A类”结论它直接决定病人是“定期复查”还是“马上穿刺活检”。把这对分级交给深度学习模型来做是典型的小样本医学图像分类任务——数据量通常只有几百到两三千张类别还特别不平衡2类和3类多4C和5类少比单纯分“良性/恶性”难不止一个量级。这个课题适合作为计算机、生物医学工程或医学影像方向的毕业设计既有明确的临床意义又有清晰的技术栈Python、PyTorch或TensorFlow、OpenCV预处理、CNN迁移学习。但它最大的坑在于——很多人把“分级”当成普通多分类题做最后准确率虚高一到验证就翻车。这篇笔记就把数据怎么洗、模型怎么训、坑在哪、答辩怎么讲完整串一遍。2. 超声图像预处理BI-RADS分级的第一步是把噪声挡在数据外2.1 先看清输入长什么样超声图像的固有噪声与冗余区域乳腺超声图像和自然图像完全不同。一是灰度单通道纹理信息集中在乳腺组织的回声分布上二是图像四周通常有大片纯黑区域、探头型号文字、医院信息、深度标尺刻度这些对模型都是噪声甚至会成为模型“抄答案”的捷径——如果某张图恰好把BI-RADS分级文字拍进图里模型直接认字就能“答对”。打开一张图先别急着训练第一步是把这些区域裁掉。常见的做法是用阈值分割找到扇形扫描区域的外接矩形然后沿矩形向内收缩5%到10%把边缘的刻度标尺和文字一并去掉。这里有个前置判断你拿到的数据是原始DICOM还是导出后的PNG/JPG很多公开数据集和毕设数据包给出的是导出后的图片已经是8位灰度图那就直接走OpenCV处理如果是DICOM还得先读窗宽窗位Window Width/Level把灰度范围映射到0-255否则图像会一片黑或者一片白。import cv2 import numpy as np def crop_ultrasound_roi(img_path: str, shrink_ratio: float 0.08): 裁剪乳腺超声图像的扇形扫描区域。 返回裁剪后的灰度图尺寸为正方形。 img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: raise FileNotFoundError(f无法读取图像: {img_path}) # 1. 二值化扇形区域亮度明显高于背景 # 用大津法自动找阈值而不是写死一个固定值 _, mask cv2.threshold(img, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 2. 找所有非零像素点的包围盒 coords cv2.findNonZero(mask) x, y, w, h cv2.boundingRect(coords) # 3. 向内收缩去掉扇形边缘的刻度尺和文字 dx int(w * shrink_ratio) dy int(h * shrink_ratio) x1, y1 x dx, y dy x2, y2 x w - dx, y h - dy roi img[y1:y2, x1:x2] # 4. resize成统一尺寸供后续训练使用 roi_resized cv2.resize(roi, (256, 256), interpolationcv2.INTER_CUBIC) return roi_resized这段代码的思路分四步大津法二值化、找包围盒、收缩边框、统一尺寸。参数shrink_ratio取0.08是我在类似超声数据上的经验值如果你发现裁剪后边缘还残留深度标尺的刻度数字把这个值调到0.12反过来如果发现病灶区域被切掉了就降到0.05。注意二值化这里不推荐用固定阈值因为不同机器的增益设置不同图像的灰度分布差异很大固定阈值在A机器上好使到B机器上就废了。2.2 标签映射怎么做从BI-RADS 0-6到分类目标的三种设计拿到原始标签时你看到的通常是“BI-RADS 2类”“4A类”“5类”这样的字符串。直接拿原始标签做多分类是新手最容易踩的坑——6个类别里有的类只有十几张图模型根本学不动。我在实际项目里一般按下面的规则重新映射二分类方案良性2/3类vs 恶性4A及以上。这是最简单、也是论文里最常见的做法因为临床决策的边界就在这里。三分类方案良性2类、可能良性3类、可疑恶性4A以上。比二分类细但3类内部本身是个模糊区间模型很难学。序数回归方案把标签按0,1,2,3,4,5的序数关系编码当作回归问题训练。这个方案最贴近BI-RADS的临床本意——分级是递进的而不是彼此独立的类别。做分类之前先画一张类别分布图数清楚每个类各有多少张。如果某个类数量是别的类的十分之一就老老实实用二分类或者做类别加权。下面这段代码把原始字符串标签转成三种训练标签方便你后续切换方案对比。import pandas as pd LABEL_MAP_BINARY {2: 0, 3: 0, 4A: 1, 4B: 1, 4C: 1, 5: 1} LABEL_MAP_TRIPLE {2: 0, 3: 1, 4A: 2, 4B: 2, 4C: 2, 5: 2} def map_labels(annotation_df: pd.DataFrame, mode: str binary) - pd.DataFrame: 把原始BI-RADS字符串标签映射为训练用的整数标签。 mode可选: binary / triple / ordinal df annotation_df.copy() if mode binary: df[label_int] df[birads].map(LABEL_MAP_BINARY) elif mode triple: df[label_int] df[birads].map(LABEL_MAP_TRIPLE) elif mode ordinal: # 序数回归直接映射为 0~5 的整数后续配合 rank loss 使用 rank_map {2: 0, 3: 1, 4A: 2, 4B: 3, 4C: 4, 5: 5} df[label_int] df[birads].map(rank_map) else: raise ValueError(f未知mode: {mode}) # 丢弃映射失败的行可能是BI-RADS 0类或6类 df df.dropna(subset[label_int]) return df这里有个容易被忽略的点BI-RADS 0类需结合其他影像检查和6类已穿刺确诊不应该参与常规训练。0类本身就是“无法判断”6类已经是确诊后的回顾性诊断跟筛查场景不符。映射时直接丢弃这两类不是在浪费数据而是在保住模型的评价边界。丢了之后记得打印一下类别分布确认最少的类别至少有30张以上否则后面训练很容易直接过拟合到少数类上。2.3 数据增强医学图像上哪些手法是安全的自然图像上常用的随机裁剪、水平翻转、旋转在超声图像上需要逐一审视。我的判断标准只有一个——这个变换之后医生还能不能认出来是同一张图。水平翻转可以用因为乳腺超声的左右位置与病灶性质无关小角度旋转±15度以内可以用但要配合同心的黑色填充而不是纯黑填充否则会引入伪造的边缘随机亮度和对比度扰动可以用但是幅度要小因为超声图像灰度本身就代表了组织的回声强度乱调会破坏纹理信息。最不推荐的是随机擦除Cutout和随机颜色抖动前者可能正好把病灶盖住后者在灰度图上会制造出原本不存在的回声差异。训练时的增强要去掉验证集和测试集。扩增只做在训练路径上验证集只用中心裁剪和缩放。否则验证集的数字被增强“污染”过评估出来的指标根本不能反映真实泛化能力——这个坑在后面避坑章节还会再提。3. 模型选型与训练为什么ResNet50是医学小数据集的主力3.1 骨架网络怎么选在表达能力、泛化能力和显存之间找平衡乳腺超声图像的数据量通常只有几百到两三千张而深度学习模型动辄上千万参数。这里的核心矛盾是模型表达力越强越容易把训练集的噪声背下来数据越少越需要先验和正则化来压住过拟合。实际项目中我做了三组对比实验——ResNet50、DenseNet121、EfficientNet-B0统一用ImageNet预训练权重做迁移学习输入尺寸224×224batch size 16训练50轮。结果更有代表性的是ResNet50和DenseNet121因为它们在有限数据下收敛更平稳。选型口诀数据少于1000张用ResNet50或DenseNet121数据超过3000张且机器显存够的话12GB以上才考虑EfficientNet或Swin Transformer的小版本。这背后的逻辑是ResNet的残差连接天然缓解了深层网络的梯度问题而预训练权重让模型在训练初期就具备一定的低级视觉特征提取能力只需要在后几层微调“站在巨人肩膀上”学病灶特征。模型参数量在小样本医学图像上的表现训练显存占用224x224, batch16VGG16138M容易过拟合收敛慢约8GBResNet5025.6M稳定是首选基线约6GBDenseNet1218M更不易过拟合但收敛加速慢约5GBEfficientNet-B05.3M需要更多轮数才能追平约5GB3.2 最小可跑通训练脚本迁移学习加微调的完整流程下面这个脚本是我在类似医学图像项目里的标准起点你可以直接改数据路径跑通。核心策略是两阶段先冻结backbone只训练分类头若干个epoch等损失降下来后再解冻backbone后几层做低学习率微调。这种“先学分类、再学特征”的做法在数据少的时候非常关键——如果从一开始就全网络微调预训练权重很快被破坏训练损失和验证损失同时飙升那基本就是学废了。import torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader from torchvision import models, transforms class USGDataset(Dataset): 乳腺超声图像数据集从CSV读取图像路径和标签 def __init__(self, df, transformNone): self.df df self.transform transform def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] img cv2.imread(row[path], cv2.IMREAD_GRAYSCALE) img cv2.cvtColor(img, cv2.COLOR_GRAY2RGB) # 转3通道以适配ImageNet预训练 img cv2.resize(img, (224, 224), interpolationcv2.INTER_LINEAR) if self.transform: img self.transform(img) label torch.tensor(row[label_int], dtypetorch.long) return img, label # 训练阶段的transform验证阶段不传transform或只做ToTensor train_tf transforms.Compose([ transforms.ToPILImage(), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees10, fill0), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) def prepare_model(num_classes: int, pretrained: bool True): 加载ResNet50预训练权重替换最后一层全连接 model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1 if pretrained else None) in_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(0.5), nn.Linear(in_features, num_classes) ) return model # 两阶段训练先冻backbone训分类头再解冻低学习率微调 model prepare_model(num_classes2) for param in model.parameters(): param.requires_grad False for param in model.fc.parameters(): param.requires_grad True optimizer torch.optim.AdamW(model.fc.parameters(), lr1e-3, weight_decay1e-4) criterion nn.CrossEntropyLoss() # 先训5轮分类头观察loss下降到接近0.5左右再解冻说几个关键参数为什么这么设。优化器选AdamW是因为它在权重衰减的处理上比标准Adam更合理在微调场景下不容易把预训练权重冲掉weight_decay给到1e-4太小起不到正则作用太大预训练权重会被拉离原始分布。Dropout加在最后的全连接之前0.5是默认安全值。分类头先训5轮是一个经验值——如果你的训练集特别小少于300张这个阶段缩到3轮因为很快会过拟合如果数据量大可以放到8轮再解冻。解冻微调阶段学习率要降一个数量级经验值是统一使用1e-5到3e-5之间的全局学习率。我习惯用torch.optim.lr_scheduler.CosineAnnealingLR把学习率从3e-5余弦衰减到接近0配合30到50轮的训练周期。# 解冻backbone的最后一阶段layer4和fc层 for name, param in model.named_parameters(): if layer4 in name or fc in name: param.requires_grad True optimizer torch.optim.AdamW( [p for p in model.parameters() if p.requires_grad], lr3e-5, weight_decay1e-4 ) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max40)只解冻layer4而不是全部解冻是在显存和数据量之间做的折中。layer4包含最高阶的语义特征对病灶的纹理和边缘最敏感而layer1到layer3学到的是基础几何特征这些在ImageNet上已经学得足够好强行微调反而可能在超声图像上退化。如果你的数据量有2000张以上可以尝试解冻layer3layer4。3.3 类别不平衡加权损失函数是最简单的止血方案BI-RADS数据集天然不平衡3类和4A类数量往往是4C和5类的5到10倍。最常见的处理方式是给损失函数加权重让少数类的样本在计算梯度时贡献更大。权重取各类样本数的倒数再归一化。下面这个类权重计算和带权重的损失函数可以直接抄。import numpy as np from sklearn.utils.class_weight import compute_class_weight # 计算每个类别的权重 labels df[label_int].values class_weights compute_class_weight( class_weightbalanced, classesnp.unique(labels), ylabels ) class_weights torch.tensor(class_weights, dtypetorch.float32).to(device) criterion nn.CrossEntropyLoss(weightclass_weights)compute_class_weight是scikit-learn自带的方法传balanced会自动按样本数的倒数计算权重。需要强调的是类别加权只能延缓少数类欠拟合不能解决少数类本身信息量不足的问题。如果你的4C类只有20张图加权之后模型虽然能在训练集上把这类“记住”但验证集上遇到没见过的4C病灶大概率还是错。这时候更有效的做法是数据增强的幅度向少数类倾斜同一张图做多种变换来扩充样本量或者干脆在二分类方案里把4C和5合并进恶性牺牲细粒度换稳定性。监控训练状态还有个土办法每个epoch打印训练集和验证集的loss曲线如果训练loss持续下降而验证loss在某一轮后开始回升就是过拟合信号马上保存之前最好的模型权重不要等训练结束。4. 评估与可视化BI-RADS分级模型不能只看Accuracy4.1 五折交叉验证小数据集评估的唯一可靠方式固定划分一次训练集和测试集在小数据集上得出的准确率波动非常大——运气好分到好测试集准确率能上8成运气差分到难样本直接掉到6成。五折交叉验证是评估小样本模型的底线做法。每一折都训练一个独立模型在对应的验证折上做推理最后把五折的结果拼在一起算总体指标。这个方法在你只有几百张图时尤其重要它能让你看到模型在不同数据划分下的稳定性而不是被单次划分的“玄学”结果带着跑。实现上直接借助sklearn.model_selection.StratifiedKFold做分层划分。注意划分的层别是标签label_int要保证每一折里各类别的比例和总体一致否则某折里正好没有4C类那一折的指标就完全失真了。from sklearn.model_selection import StratifiedKFold skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) for fold, (train_idx, val_idx) in enumerate(skf.split(df, df[label_int])): train_df df.iloc[train_idx] val_df df.iloc[val_idx] # 此处在循环内创建独立的模型并训练 # 每折结束后记录最佳权重保存为 model_fold{fold}.pt # 五折全部结束后用5个模型在各自验证折上推理收集所有预测结果划分时一定要用StratifiedKFold而不是简单的KFold因为后者在类别不平衡时很可能把某一类全分到验证折导致训练集里缺类。另外random_state固定下来便于别人复现你的实验答辩时如果评委问“为什么这个数字这么高”你能给出的回答是“我在这个随机种子下做了五折交叉验证”而不是“我调参调出来的”。4.2 混淆矩阵、敏感性、特异性、AUC评估指标怎么选、怎么看医学图像分类的评估指标和通用分类任务有两个重要区别。第一准确率没有参考价值——如果训练集里70%是良性模型全部预测良性准确率就有70%但这个模型在临床上毫无用处。第二敏感性和特异性要一起看敏感性意味着“恶性病人被漏诊的概率”特异性意味着“良性病人被过度活检的概率”这两者需要权衡。对于BI-RADS分级任务敏感性尤其重要把一个恶性病灶错判为良性后果是病人错过治疗窗口反过来把良性判成可疑恶性后果是让病人做一次不必要的穿刺活检。所以我评估模型时优先看敏感性要保证在90%以上再尽量把特异性往70%以上拉。from sklearn.metrics import confusion_matrix, roc_auc_score, recall_score, specificity_score # y_true: 真实标签列表, y_pred: 模型预测标签列表, y_prob: 模型预测为正类的概率 cm confusion_matrix(y_true, y_pred) tn, fp, fn, tp cm.ravel() sensitivity tp / (tp fn) # 召回率恶性病例被正确检出的比例 specificity tn / (tn fp) # 良性病例被正确判为良性的比例 auc roc_auc_score(y_true, y_prob) print(fSensitivity: {sensitivity:.3f} | Specificity: {specificity:.3f} | AUC: {auc:.3f})confusion_matrix返回的矩阵形状是[[TN, FP], [FN, TP]]我经常看到有人把TP和FP的位置弄反导致算出来的指标对不上。动手算之前先打印cm矩阵看一眼确认对角线是预测正确的样本再算派生指标。AUC是另一个重要参考它表示随机抽一个恶性样本和一个良性样本模型把恶性排在前面的概率。AUC越高说明模型的排序能力越好在类别不平衡时比准确率可靠得多。4.3 Grad-CAM可视化让模型的分级决策“说人话”答辩评委大概率会问一句“你凭什么相信这个模型是真的学到了病灶特征而不是学了些表面纹理”此时Grad-CAM热力图是最好用的工具——它把模型在推理时关注的图像区域高亮出来让你一眼看出模型是盯着病灶边缘看还是在看图像角落里不该看的内容。实现Grad-CAM不需要手写反向传播用pytorch_grad_cam这个库直接跑几行代码就出来。核心逻辑是取模型最后一个卷积层的输出特征图乘以该特征图对预测类别的梯度权重得到加权热力图再上采样叠加到原图上。from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget from pytorch_grad_cam.utils.image import show_cam_on_image # 取ResNet50的layer4最后一个残差块作为目标层 target_layers [model.layer4[-1]] cam GradCAM(modelmodel, target_layerstarget_layers) targets [ClassifierOutputTarget(predicted_class)] # input_tensor: 预处理后的图像shape为[1,3,224,224] grayscale_cam cam(input_tensorinput_tensor, targetstargets)[0] # 原图归一化到0-1后与热力图叠加 visualization show_cam_on_image(rgb_img, grayscale_cam, use_rgbTrue)跑完热力图后要做一件事把热力图高亮区域和医生标注的病灶区域做视觉对比。如果高亮区域集中在病灶周边而非病灶中心说明模型更多是在学“边界信号”——这不一定错因为BI-RADS分级本身就看边界、形态和后回声。但如果你发现高亮区域集中在图像角落的标尺文字上那基本可以断定模型在抄捷径得回头检查数据预处理有没有把文字裁干净。5. 避坑BI-RADS分级训练的六个常见问题与排查记录5.1 同一病人的多张图像被分到训练集和验证集现象训练准确率90%以上验证准确率却只有60%出头且五折交叉验证每一折成绩都忽高忽低。 原因同一个病人往往拍了多张超声图像比如左乳和右乳各一张或者同一病灶从不同角度拍了两张。这些图像没有做“按病人分折”的处理导致训练集和验证集里出现同一病人的不同图像。模型实际上在“认病人”而不是“认病灶”。这是医学图像分类中最常见、最严重的数据泄露。 解决在划分数据集时按病人ID分组保证同一个病人的所有图像全部落在同一折里。注意StratifiedKFold是按图像划分的如果你直接用会踩这个坑要先做一层GroupKFold再套分层逻辑。5.2 验证集混入数据增强现象训练loss和验证loss在初期都在下降但验证loss下降速度异常快甚至出现过拟合平缓期比训练集还晚的奇怪曲线。 原因验证集用了和训练集一样的transform包含了随机水平翻转和随机旋转。验证集每一轮被随机增强之后模型每次看到的验证样本都是不同的等于变相“见过更多样本”指标偏乐观。 解决验证集只保留ToTensor和Normalize不做任何随机变换。在代码里把训练和验证的transform分开写命名成train_transform和val_transform避免误用。5.3 类别不平衡导致准确率虚高、AUC却很低现象打印准确率85%看起来很棒但看混淆矩阵恶性类别几乎全部被预测成良性。AUC只有0.55接近随机猜测。 原因负样本良性数量是正样本恶性的5倍以上模型不学习任何特征直接把所有样本预测为良性也能拿到高准确率。准确率被多数类稀释了。 解决改成看敏感性、特异性和AUC而不是准确率。损失函数加类别权重或者改用Focal Loss让模型在训练时更关注难分类的少数类样本。5.4 标签本身不可靠不同医生的BI-RADS标注存在主观差异现象同一张图A医生标3类B医生标4A类模型中A医生的标注为标签训练最后在验证集上对这类边界样本的预测置信度极低接近0.5。 原因BI-RADS分级本身就存在观察者间变异性边界案例3类 vs 4A类尤其明显。这属于标签噪声的一种。 解决如果数据里有多个医生的标注可以取众数或更保守的级别作为最终标签如果没有多医生标注建议把问题重新定义为二分类良性vs恶性而不是细粒度分级。答辩时主动讲出这个局限性反而是加分项。5.5 训练和推理时的图像预处理不一致现象训练时输入是随机裁剪加旋转后的224×224图推理时输入的原始图像没有走训练时的缩放和裁剪流程直接resize成224×224导致推理结果明显变差。 原因训练管线里的pipeline和推理管线里的pipeline不一致——比如训练时先resize到256再随机crop到224而推理时直接resize到224两者看到的图像内容不一样模型对平移和缩放的鲁棒性被打破。 解决把预处理逻辑封装成一个函数训练和测试共用同一个入口。推理时的标准做法是resize到256 → 中心裁剪到224 → 归一化。归一化用的mean和std必须和训练时完全一致也就是ImageNet的[0.485, 0.456, 0.406]否则灰度分布直接被扭曲。5.6 模型把所有样本都预测成占多数的类别现象训练完成后打印测试集预测结果发现预测标签里只有一类另一类完全没出现过。 原因除了类别不平衡之外还有一个隐蔽原因是学习率设置过大。模型在训练初期直接冲进了局部最优把所有样本都分给了多数类后面无论怎么迭代都跳不出来。 解决先确认损失函数是否加了类别权重再把初始学习率从1e-3降到1e-4重新训练。另一个排查点是看训练日志里第一个epoch结束后各类别的预测分布如果从第一轮就是单一预测说明模型压根没在学特征问题出在数据或训练配置而不在训练中期。6. 把课题做出彩序数回归、概率校准与五折模型集成前面的方案是拿ResNet50做普通多分类能顺利毕业但如果你想冲优秀论文或者想让模型真能用在辅助诊断场景里下面三个升级方向值得做。第一个方向是把多分类改成序数回归。BI-RADS分级本质是序数变量从2类到5类恶性风险是单调递增的相邻级别之间的差距比跨级别之间的差距小得多。普通多分类的交叉熵损失把不同类别当完全独立的关系处理丢失了这层顺序信息。实现上不需要换模型只需要改损失函数——把4个类别的分类问题变成3个二分类子问题每个子问题回答“这个样本的级别是否大于k”。训练时模型的输出层有3个节点每个节点独立做sigmoid再按输出向量计算序数损失。这样模型在2类和3类之间的边界判断会明显变好因为这个边界在多个子问题中被反复学习。第二个方向是做概率校准。模型的softmax输出值不等于真实概率——这在类别不平衡和迁移学习场景下尤其明显。用一个温度缩放Temperature Scaling对logits做归一化能在不改变模型预测结果的前提下让输出概率更接近临床上的“置信度”。def temperature_scale(logits, y_true, T_init2.0): 温度缩放用验证集优化温度参数T使输出概率校准 import torch.nn.functional as F logits torch.tensor(logits, dtypetorch.float32) y_true torch.tensor(y_true, dtypetorch.long) # 温度T是一个可学习的标量参数但只用验证集拟合T T torch.nn.Parameter(torch.tensor(T_init)) optimizer torch.optim.LBFGS([T], lr0.01) def closure(): optimizer.zero_grad() loss F.cross_entropy(logits / T, y_true) loss.backward() return loss for _ in range(100): optimizer.step(closure) return T.item()温度缩放的原理很简单把logits除以一个大于1的温度T让softmax输出的概率分布变得更“软”也就是不同类别之间的差异变小。温度越高输出概率越接近均匀分布温度越低输出越接近one-hot。在验证集上优化T能让模型的输出概率校准到实际频率这样当你输出“95%恶性”时它往往真的意味着接近95%的概率是恶性。第三个方向是五折模型集成。把交叉验证阶段训出来的5个模型当作一个模型集合推理时把5个模型的softmax输出取平均得到最终预测。这个做法几乎无成本却能在敏感性和特异性上同时提升2到3个百分点——因为5个模型在不同的数据划分上训练各自的错误模式不同平均之后错误被互相抵消。我做这个课题时单模型的敏感性一般在85%上下五折ensemble之后能稳定在90%以上。这个经验在你答辩时很值钱评委听完会相信你真的把交叉验证用到位了。最后一个实操技巧是把Grad-CAM热力图和原始超声图并排保存成一张汇总图放进毕业论文的“结果与分析”章节。这不是形式上的配图而是让外行评委直观看到模型决策依据的最短路径。我习惯是每张图上同时标注真实BI-RADS类别、模型预测类别、预测置信度、热力图四列排成一张对比网格。你花一个下午做出来的这张图在答辩现场的说明力远超过论文里十页的文字描述。这也是我自己的血泪经验——第一次答辩时只顾着讲准确率被评委一句“模型靠什么判断”问住了后来补了热力图才把逻辑圆上。希望帮到你。本文还有配套的精品资源点击获取
返回列表