
简介这份资源面向计算机、电子信息工程、数学等专业的大学生用于课程设计、期末大作业或毕业设计中的3D医学影像分类任务提供一套可直接运行的机器学习完整实现方案。压缩包共596个文件约446.14MB以582个npz数据文件为核心配合8个Python脚本、3个csv标签与提交文件、1个md说明文档及少量pyc缓存覆盖数据读取、模型训练、结果输出等环节。代码采用参数化编程关键参数可方便调整注释清晰并附有运行结果便于理解整体编程思路。目前已有359人学习下载。读者可据此获得从数据组织到分类实现的完整参考借助csv与npz文件快速复现实验流程结合说明文档与源码梳理建模逻辑适合作为医学影像分类方向的入门实践与二次开发基础。1. 3D医学影像分类大作业从数据到模型的完整落地路径3D医学影像分类是机器学习课程大作业里比较硬核的一类题目。它和常见的二维图像分类有本质区别CT、MRI 这类数据是三维体数据层与层之间存在空间连续性直接套用 2D 卷积网络会丢掉切片间的上下文信息。很多同学拿到题目后第一反应是找现成代码改改但真正跑起来才发现数据读取、维度对齐、显存占用、类别不均衡这几个问题会接连翻车。这篇笔记围绕「3D医学影像分类实现 源代码 文档说明」这个方向把从环境搭建、数据预处理、模型选型到训练调参的完整链路拆开讲清楚。适合正在做机器学习大作业的本科生、研究生也适合想从 2D 迁移到 3D 任务的算法初学者。读完你至少能拿到一套可复现的代码框架知道每一步为什么这么做以及哪里最容易踩坑。2. 3D医学影像分类的任务定义与数据准备2.1 3D医学影像和2D图像分类的本质差异2D 图像分类处理的是单张切片输入维度是(H, W, C)而 3D 医学影像的输入是(D, H, W)其中 D 是切片数量也就是深度维度。这个差异带来的连锁反应比想象中大。第一卷积核从二维变成三维参数量和计算量呈立方级增长。一个 3×3 的 2D 卷积核有 9 个参数换成 3×3×3 就变成 27 个。如果通道数再翻倍显存占用会迅速吃满。第二3D 数据的标注成本极高。医学影像需要专业医生逐层勾画公开数据集规模通常远小于 ImageNet。常见做法是用数据增强、迁移学习、或者把 3D 数据切成 2.5D 的切片序列来降低门槛。第三不同设备的扫描参数不一致体素间距spacing差异很大。同一个器官在不同机器上可能占据完全不同的体素数量不做重采样直接训练模型学到的可能是设备特征而不是病理特征。理解这三点之后后面的预处理和模型设计才有依据。我一般会先确认手头数据的维度、spacing、标签分布再决定用纯 3D 网络还是混合方案。2.2 数据读取与预处理的最小可运行代码假设你拿到的是 NIfTI 格式的 3D 影像医学影像里最常见的格式之一用 Python 读取和预处理的核心流程如下import numpy as np import nibabel as nib from scipy.ndimage import zoom def load_and_preprocess(nii_path, target_shape(64, 64, 64)): 读取NIfTI文件并做重采样归一化 nii_path: .nii或.nii.gz文件路径 target_shape: 目标体素尺寸根据显存调整 img nib.load(nii_path) data img.get_fdata() # 得到float64的3D数组 # 重采样到统一尺寸 zoom_factors [t / s for t, s in zip(target_shape, data.shape)] data zoom(data, zoom_factors, order1) # order1双线性插值 # CT值截断到软组织窗口避免极端值干扰 data np.clip(data, -1000, 400) # 归一化到[0,1] data (data - data.min()) / (data.max() - data.min() 1e-8) return data.astype(np.float32)这段代码做了四件事读取 NIfTI、重采样到固定尺寸、CT 值截断、归一化。target_shape是最关键的参数64×64×64 在 8GB 显存上比较安全如果数据分辨率高且显存充足可以调到 128×128×128。order1表示双线性插值比最近邻更平滑但如果你做的是分割任务且标签也是 3D 的标签要用order0保持整数。CT 值截断的范围[-1000, 400]是腹部软组织的常用窗口不同部位要调整。比如肺部用[-1000, 400]也常见脑部 MRI 则不需要截断直接做 z-score 标准化更合适。2.3 标签处理与数据集划分的注意事项分类任务的标签通常是每个病例一个类别比如正常/良性/恶性。这里有两个容易忽略的点。一是类别不均衡。医学数据里正常样本往往远多于异常样本直接训练会导致模型偏向多数类。常见做法是加权交叉熵或者过采样少数类。加权交叉熵的实现很简单import torch import torch.nn as nn # 假设三类样本数分别为[800, 150, 50] class_counts [800, 150, 50] weights 1.0 / torch.tensor(class_counts, dtypetorch.float) weights weights / weights.sum() # 归一化 criterion nn.CrossEntropyLoss(weightweights)二是数据泄露。同一个病人的多次扫描不能同时出现在训练集和验证集里否则验证指标会虚高。我一般按病人 ID 划分而不是按样本随机划分。这一点在写文档说明时也要写清楚不然复现的人容易搞错。3. 3D分类网络的选型与实现3.1 从2D迁移到3D三种主流方案对比做 3D 医学影像分类模型选型上有三条路可走各有适用场景。方案核心思路优点缺点适用场景纯3D网络直接用3D卷积充分利用空间信息参数量大需要大数据数据量充足、显存够2.5D切片每层切片单独过2D网络再聚合可复用预训练权重丢失层间关系数据量小、快速出结果混合方案2D提取特征3D融合平衡效果和成本实现复杂中等数据量如果大作业时间紧、数据量不大2.5D 方案是最稳的。具体做法是把每个 3D 样本的每层切片送进 ResNet 提取特征得到(D, feature_dim)的序列再用一个 GRU 或简单的注意力池化做聚合。这样可以直接用 ImageNet 预训练权重收敛快很多。如果追求更好的效果且显存允许纯 3D 网络是正路。下面给一个轻量 3D CNN 的实现。3.2 一个可复现的3D CNN分类网络实现import torch import torch.nn as nn class ConvBlock3D(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.block nn.Sequential( nn.Conv3d(in_ch, out_ch, kernel_size3, padding1), nn.BatchNorm3d(out_ch), nn.ReLU(inplaceTrue), nn.MaxPool3d(kernel_size2, stride2) ) def forward(self, x): return self.block(x) class Simple3DCNN(nn.Module): def __init__(self, num_classes3): super().__init__() self.features nn.Sequential( ConvBlock3D(1, 16), # 64-32 ConvBlock3D(16, 32), # 32-16 ConvBlock3D(32, 64), # 16-8 ConvBlock3D(64, 128), # 8-4 ) self.gap nn.AdaptiveAvgPool3d(1) # 全局平均池化 self.classifier nn.Sequential( nn.Dropout(0.5), nn.Linear(128, num_classes) ) def forward(self, x): x self.features(x) x self.gap(x) x x.view(x.size(0), -1) return self.classifier(x)这个网络有四层卷积块每层后接最大池化输入 64×64×64 经过四次下采样变成 4×4×4再全局平均池化到 1×1×1。AdaptiveAvgPool3d(1)的好处是不管输入尺寸多少输出都是固定维度这样你可以灵活调整target_shape而不用改网络结构。Dropout(0.5)放在分类头前面医学数据量小的时候正则化很重要。如果过拟合严重可以加到 0.6如果欠拟合就降到 0.3。3.3 训练循环与关键参数设置训练部分的核心代码import torch.optim as optim from torch.utils.data import DataLoader device torch.device(cuda if torch.cuda.is_available() else cpu) model Simple3DCNN(num_classes3).to(device) optimizer optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max50) for epoch in range(50): model.train() for batch_x, batch_y in train_loader: batch_x, batch_y batch_x.to(device), batch_y.to(device) optimizer.zero_grad() logits model(batch_x) loss criterion(logits, batch_y) loss.backward() optimizer.step() scheduler.step() # 每个epoch后在验证集上评估 model.eval() with torch.no_grad(): # 验证逻辑省略注意记录AUC和F1 pass学习率1e-4配合 AdamW 是 3D 网络的常用起点。如果 loss 震荡明显降到5e-5。weight_decay1e-4控制权重衰减医学数据小正则化不能太弱。CosineAnnealingLR 让学习率按余弦曲线下降比阶梯下降更平滑T_max设成总 epoch 数。批次大小方面64×64×64 的输入在 8GB 显存上 batch_size 一般能到 8 或 16。如果显存不够优先降 batch_size 而不是降输入尺寸因为尺寸太小会丢失解剖结构信息。4. 训练过程中的避坑与排查4.1 损失不下降从数据到梯度的排查顺序现象训练几个 epoch 后 loss 几乎不变准确率停在多数类比例附近。原因通常有三层数据层面、模型层面、优化层面。排查顺序建议从数据开始。先检查输入数据的均值和方差是否正常如果全是 0 或者全是 1说明归一化写错了。再检查标签是否和输入对应曾经遇到过一个坑是 DataLoader 的 shuffle 和标签数组没有同步打乱导致模型学的是随机映射。如果数据没问题看模型输出。在 softmax 之前打印 logits 的均值和方差如果方差极小说明网络没有学到东西可能是初始化有问题或者学习率太小。最后检查梯度用torch.nn.utils.clip_grad_norm_之前先打印梯度范数如果一直是 0说明反向传播断了。解决数据问题修数据模型问题换初始化或加 BatchNorm优化问题调学习率。我一般会先用一个极小数据集比如 10 个样本过拟合如果连 10 个样本都过拟合不了那肯定是代码有 bug。4.2 显存溢出3D网络的常见爆显存场景现象训练开始几秒后报CUDA out of memory。原因3D 卷积的中间激活值占用远超 2D。一个 64×64×64 的输入经过第一层 16 通道卷积后特征图是 16×64×64×64float32 下就是 16MB四层下来加上梯度存储很容易超过 8GB。解决第一用混合精度训练torch.cuda.amp可以把显存占用降低约 40%。第二减小 batch_size从 16 降到 8 甚至 4。第三如果还不行把输入尺寸从 64 降到 48 或 32但要评估是否影响分类效果。第四检查是否有不必要的张量保留在计算图中比如在验证时忘了torch.no_grad()。4.3 验证集指标虚高数据泄露的隐蔽形式现象验证集准确率 95%但测试集只有 60%。原因最常见的是同一个病人的数据同时出现在训练和验证集。另一个隐蔽形式是做了全局归一化也就是用整个数据集的均值和方差做标准化而不是只用训练集的统计量。这会让验证集的信息泄露到训练过程中。解决按病人 ID 划分数据集归一化统计量只在训练集上计算然后应用到验证集和测试集。如果做交叉验证每一折都要重新计算训练集的统计量。4.4 类别不均衡导致的假高准确率现象三分类任务准确率 80%但少数类召回率接近 0。原因多数类样本占比过高模型学会了全部预测多数类就能拿到不错的准确率。解决除了加权交叉熵还可以用 Focal Loss 让模型关注难分类样本。评估指标不要只看准确率要看每类的 F1 和 AUC。如果少数类样本极少考虑数据增强或者用 SMOTE 类的过采样方法但 3D 数据的过采样实现起来比 2D 复杂常见做法是旋转、翻转、弹性形变。4.5 训练和推理结果不一致现象训练时验证集表现正常部署推理时结果完全不对。原因最常见的是预处理不一致。训练时用了归一化和重采样推理时忘了做同样的处理。另一个原因是模型没有切换到 eval 模式Dropout 和 BatchNorm 在训练和推理时的行为不同。解决把预处理封装成一个函数训练和推理都调用同一个。推理前务必model.eval()并且用torch.no_grad()包住。如果用了混合精度训练推理时也要保持一致或者把权重转成 float32 再推理。5. 文档说明的写法与代码组织建议5.1 一份能让人复现的README应该包含什么大作业的文档说明不是凑字数而是让别人能按步骤跑通。我一般按这个结构写第一段写任务描述和数据集来源说明输入输出是什么。第二段写环境依赖包括 Python 版本、PyTorch 版本、CUDA 版本以及安装命令。第三段写数据准备说明数据放在哪个目录、格式是什么、需不需要预处理脚本。第四段写训练命令和关键参数。第五段写评估结果和已知问题。代码组织上建议分成data/、models/、utils/、train.py、eval.py五个部分。data/放 Dataset 类和预处理函数models/放网络定义utils/放指标计算和可视化工具。这样别人拿到代码能快速定位。5.2 用配置文件管理超参数不要把学习率、batch_size 这些硬编码在 train.py 里。用一个 YAML 或 JSON 配置文件# config.yaml data: target_shape: [64, 64, 64] batch_size: 8 num_workers: 4 model: num_classes: 3 dropout: 0.5 train: lr: 0.0001 weight_decay: 0.0001 epochs: 50然后在 train.py 里用argparse或yaml.load读取。这样做的好处是换数据集或调参时不用改代码也方便记录实验配置。5.3 结果可视化的最小实现分类任务至少要有混淆矩阵和 ROC 曲线。混淆矩阵用 sklearn 的confusion_matrix加 seaborn 热力图就能画。ROC 曲线需要每个类的概率输出用sklearn.metrics.roc_curve和auc计算。如果做 3D 数据的可视化可以用matplotlib画几个中间切片确认预处理没有把图像搞坏。import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix import seaborn as sns def plot_confusion(y_true, y_pred, classes): cm confusion_matrix(y_true, y_pred) sns.heatmap(cm, annotTrue, fmtd, xticklabelsclasses, yticklabelsclasses) plt.xlabel(Predicted) plt.ylabel(True) plt.savefig(confusion_matrix.png, dpi150)这段代码保存的图片可以直接放进文档说明里比文字描述直观得多。6. 进阶技巧用2.5D方案快速拿到基线结果如果你时间紧或者显存不够跑纯 3D 网络2.5D 方案是最实用的退路。具体做法是把每个 3D 样本的 D 层切片分别送进一个 2D ResNet-18用 ImageNet 预训练权重每层得到一个 512 维特征向量然后对这些向量做聚合。聚合方式有三种平均池化、最大池化、或者用一个单向 GRU。平均池化最简单效果也不差。GRU 能捕捉层间顺序信息但训练慢一些。我一般先用平均池化跑一个基线如果效果不够再加 GRU。import torchvision.models as models import torch.nn as nn class TwoPointFiveD(nn.Module): def __init__(self, num_classes3): super().__init__() resnet models.resnet18(pretrainedTrue) # 去掉最后的全连接层保留特征提取部分 self.backbone nn.Sequential(*list(resnet.children())[:-1]) self.classifier nn.Sequential( nn.Linear(512, 128), nn.ReLU(), nn.Dropout(0.5), nn.Linear(128, num_classes) ) def forward(self, x): # x: (B, D, H, W) - (B*D, 1, H, W) B, D, H, W x.shape x x.view(B * D, 1, H, W) # 单通道复制成三通道以适配预训练模型 x x.repeat(1, 3, 1, 1) feats self.backbone(x) # (B*D, 512, 1, 1) feats feats.view(B, D, 512) feats feats.mean(dim1) # 沿深度方向平均 return self.classifier(feats)这个方案的优势是收敛快通常 10 个 epoch 就能看到不错的结果。缺点是丢失了层间的空间对应关系对于需要精确空间定位的任务比如小结节分类效果会打折扣。但在大作业场景下先拿到一个能跑的基线再决定要不要投入时间优化纯 3D 方案是更稳妥的策略。最后说一个我自己的习惯每次跑实验前先用 5 个样本过一遍完整流程确认数据加载、前向传播、loss 计算、反向传播、验证评估都能跑通再上全量数据。这个习惯帮我省过很多次通宵排查的时间。希望帮到你。本文还有配套的精品资源点击获取