简介:面向机器学习课程大作业与毕业设计场景,这份3D医学影像分类资源提供了从数据预处理到模型训练评估的完整实现方案。代码采用参数化编程,关键参数可灵活调整,并配有清晰注释与已运行成功的结果文件,方便计算机、电子信息、数学等专业学生直接复现或二次开发。压缩包共596个文件,以582个npz数据文件为主,配合8个Python源码、3个CSV标注/提交文件及1份Markdown说明文档,整体大小约446MB,结构上覆盖训练、验证与测试环节,便于按流程查阅。目前已有360人学习下载。除核心分类代码外,还包含面向Kaggle风格赛题的候选数据组织思路与提交样例,可作为3D医学影像特征提取、分类器调参及结果分析的参照模板;若运行中遇到环境或路径问题,也可借助作者提供的技术支持渠道快速排查,适合需要高效完成课程设计或期末大作业的在校生使用。
1. 3D医学影像分类实战:npz数据怎么读、怎么训、怎么提交流程拆解
拿到一份3D医学影像分类的大作业资源包,第一个感受是:跟平时玩2D图像分类完全不一样——数据不是一堆jpg和png,而是十几个npz压缩包和一张CSV标注表。训练时不是说“读图片->归一化->塞进CNN”就完事,而是先要把三维数组从npz里抠出来,处理掉尺寸不一致、标签错位、显存爆炸这些隐性问题,模型本身反而成了最简单的部分。这份资料恰好把一套完整的3D医学影像分类流程串起来了:标注文件、候选样本npy、划分用的CSV、提交样例、README说明,外加参数化程度很高的Python源码。适合做课程设计、期末大作业的学生,也适合想快速上手3D医学影像分类的开发者:照着跑一遍就知道三维卷积的工程套路,改改配置就能迁到自己的数据上。
2. 项目结构与数据形态:拿到压缩包先看什么
2.1 文件清单与各自用途
先不看代码,把包内文件按“数据/标注/文档/样例”归类,项目的整体脉络就出来了。我当时拆开压缩包,第一件事是建了一个表,把每个文件的角色标清楚:
| 文件 | 类型 | 作用 |
|---|---|---|
| train_val.csv | CSV表格 | 训练与验证样本的划分表,记录每个样本的ID、文件路径和标签 |
| 0617.csv | CSV表格 | 另一批候选样本标注,用于扩展验证或追加训练集,0617通常是生成日期 |
| sampleSubmission.csv | CSV表格 | 提交格式样例,保持它的行顺序和列名,预测结果直接套用 |
| README.md | Markdown文档 | 环境说明、参数含义、运行顺序,先读它 |
| candidate*.npz | NumPy压缩数组 | 每个文件是一个三维影像候选块,shape一般是(深度, 高, 宽) |
candidate开头的npz是核心数据。资源里有candidate409、candidate222、candidate307、candidate61、candidate452、candidate364这六个文件,数量不多但足够跑通全流程。npz是NumPy的压缩存储格式,里面可以放多个数组,也可以只有一个数组。常见做法是np.savez_compressed保存,读出来直接用np.load即可。
train_val.csv和0617.csv的区别值得注意。train_val.csv承担的是“划分+标签”双重任务:训练时用它分出多少比例做验证集,同时从里面读标签;0617.csv更像是后来补充的标注批次,可能是同一个数据源不同日期生成的候选样本。实际使用中可以把两者拼接,也可以只用train_val.csv做训练,把0617.csv当额外验证集,看模型泛化能力。
2.2 技术栈与一次完整的数据流
从文件组织形式推断,这个项目的技术栈是典型的Python机器学习组合:NumPy负责数组读写,PyTorch或TensorFlow负责三维卷积模型,pandas处理CSV标注,scikit-learn提供AUC等评估指标。参数化编程是这套代码比较突出的设计:训练轮数、学习率、batch size、输入尺寸、是否用GPU这些超参,都集中在开头或配置区,改参数不需要去翻模型层代码。
整个数据流可以分成五段,跑通后整条链路就清晰了:
npz文件与CSV按样本ID关联 -> Dataset加载时读入三维数组 -> 归一化与尺寸统一 -> 三维卷积模型输出二分类logits -> 训练验证循环得到AUC指标和提交文件
这里有一个关键点:CSV和npz不是靠文件名后缀对应,而是靠ID字段关联。比如train_val.csv里某一行写candidate409,那么数据加载代码就把candidate409.npz读进来,再从同一行取标签。这种设计决定了数据对齐必须严格——文件名写错、CSV里多了空格、顺序乱了,都会造成模型训练时标签和图像错位,这是3D影像项目最常见的翻车点,后面第五部分会专门说。
3. 核心实现拆解:数据加载、三维模型、训练循环
3.1 数据读取:把npz变成模型能吃的东西
第一步是把npz和CSV接起来。常见的实现是写一个继承Dataset的类,初始化时读取CSV拿到样本列表,__getitem__里通过索引找到文件名再加载npz。代码大致长这样:
import os import numpy as np import pandas as pd import torch from torch.utils.data import Dataset class CandidateDataset(Dataset): def __init__(self, csv_path, data_dir, target_size=(32, 32, 32)): self.df = pd.read_csv(csv_path) self.data_dir = data_dir self.target_size = target_size # 统一处理标签列,假设列名为 label self.labels = self.df['label'].values.astype(np.float32) def __len__(self): return len(self.df) def __getitem__(self, idx): # 从CSV行拿文件名,拼成路径 sample_id = self.df.iloc[idx]['id'] npz_path = os.path.join(self.data_dir, f"{sample_id}.npz") # npz可能有多个键,按实际存储键名取,常见为'data'或'arr_0' data = np.load(npz_path, allow_pickle=True) arr = data['data'] if 'data' in data.files else data['arr_0'] # 归一化到 [0,1] 区间,避免数值过大 arr = (arr - arr.min()) / (arr.max() - arr.min() + 1e-8) # 统一尺寸:小于目标尺寸就pad,大于就随机裁剪或中心裁剪 arr = self._resize(arr, self.target_size) # 增加通道维度,变成 (1, D, H, W) 适配Conv3d输入 tensor = torch.from_numpy(arr).float().unsqueeze(0) label = torch.tensor(self.labels[idx]).float() return tensor, label这段代码里有几个细节需要说明。np.load读出来的是一个NpzFile对象,不能直接当数组用;如果存储时用了data这个键就用data访问,否则就退回到arr_0。归一化用min-max而不是z-score,主要考虑到CT值范围不固定且可能有极端值,先压到0到1更稳。_resize方法在代码里通常是先用中心裁剪保证空间对齐,再用插值处理剩余的不一致,目标尺寸默认设为32的立方体,这跟candidate样本本身的分辨率有关。
参数方面,csv_path指train_val.csv或0617.csv,data_dir指存放npz的目录,target_size是三维输入边长,改成64就意味着模型输入从32x32x32变成64x64x64,显存占用会直接翻倍。想排查加载是否正确,可以单独实例化Dataset后打印一条数据shape和label,不要一上来就整个训练循环。
3.2 模型结构:一个小而可改的3D CNN
这个项目的模型层不算复杂,但结构完整,正适合做作业演示。常见设计是三层Conv3d加BN加ReLU,下采样用MaxPool3d,最后接全局池化和全连接层输出二分类分数:
import torch.nn as nn class Simple3DCNN(nn.Module): def __init__(self, in_channels=1, num_classes=2): super().__init__() # 第一层:8个卷积核,捕捉低阶纹理,保证参数量不至于爆炸 self.conv1 = nn.Conv3d(in_channels, 8, kernel_size=3, padding=1) self.bn1 = nn.BatchNorm3d(8) self.pool1 = nn.MaxPool3d(2) # 32 -> 16 self.conv2 = nn.Conv3d(8, 16, kernel_size=3, padding=1) self.bn2 = nn.BatchNorm3d(16) self.pool2 = nn.MaxPool3d(2) # 16 -> 8 self.conv3 = nn.Conv3d(16, 32, kernel_size=3, padding=1) self.bn3 = nn.BatchNorm3d(32) self.pool3 = nn.MaxPool3d(2) # 8 -> 4 # 最后直接用全局平均池化,把每个通道压成一个值 self.gap = nn.AdaptiveAvgPool3d(1) self.fc = nn.Linear(32, num_classes) def forward(self, x): x = self.pool1(torch.relu(self.bn1(self.conv1(x)))) x = self.pool2(torch.relu(self.bn2(self.conv2(x)))) x = self.pool3(torch.relu(self.bn3(self.conv3(x)))) x = self.gap(x) x = x.view(x.size(0), -1) return self.fc(x)模型采用“卷积核翻倍、空间尺寸减半”的经典设计:通道数从8到16再到32,每经过一个池化层空间边长减半。输入32的立方体,三次池化后变成4x4x4,全局平均池化直接把这4x4x4压成一个值,省掉了全连接层的巨量参数。这也是3D模型能跑得动的重要原因——三维卷积的参数量和显存本来就是二维的好几倍,如果全用全连接层,光参数就能把内存吃穿。
输出层用了num_classes=2,配合交叉熵就是二分类;如果只想输出一个概率,也可以把num_classes改成1,配合BCEWithLogitsLoss用。训练时一般不会单独对概率做sigmoid,而是让模型输出logits交给损失函数处理,数值稳定性更好——这一点在改损失函数时容易踩坑,很多人先手动sigmoid再用BCELoss,反而出现梯度不稳定。
3.3 训练循环:参数化配置和验证逻辑
训练部分几乎每个大作业都长得很像,但这个项目的优势在于把参数集中在一起,跑的时候不用改代码。训练脚本开头通常会有一大段超参定义:
# 超参配置区,跑实验直接改这里 BATCH_SIZE = 8 EPOCHS = 50 LR = 3e-4 DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu' TRAIN_RATIO = 0.8 SEED = 42训练循环保留最标准的写法:按TRAIN_RATIO对数据集做随机划分,每个epoch用DataLoader跑batch,记录loss和准确率,验证集用AUC做指标。AUC对正负样本不均衡不那么敏感,比准确率更适合医学影像二分类场景。训练过程大致是:
from sklearn.metrics import roc_auc_score # train_loader和val_loader由Dataset按比例划分后构建 for epoch in range(EPOCHS): model.train() running_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.long() if num_classes == 2 else labels) loss.backward() optimizer.step() running_loss += loss.item() # 验证阶段 model.eval() val_probs, val_true = [], [] with torch.no_grad(): for images, labels in val_loader: images = images.to(DEVICE) outputs = model(images) probs = torch.softmax(outputs, dim=1)[:, 1] val_probs.extend(probs.cpu().numpy()) val_true.extend(labels.numpy()) auc = roc_auc_score(val_true, val_probs) print(f"Epoch {epoch+1}/{EPOCHS} | Loss: {running_loss:.4f} | AUC: {auc:.4f}")这段代码建议保留一个关键习惯:验证阶段必须写model.eval()并且包在torch.no_grad()里。前者关闭Dropout和BatchNorm的批次统计更新,后者停止计算梯度,两个不写都会让验证AUC忽高忽低。训练时没加Dropout和权重衰减也能跑,但在只有六个npz样本规模前提下不影响作业演示;如果想提高泛化性,可以给卷积后面加Dropout3d,或者给优化器加weight_decay。
4. 训练、评估与提交:从本地验证到生成结果文件
4.1 划分策略:到底用哪个CSV
许多同学拿到这份资源会困惑:train_val.csv和0617.csv都是标注文件,训练时用哪个?我实践后的组合方式是:用train_val.csv做主要训练数据,按8比2切出验证集,训练结束后用0617.csv的预测结果做一次盲测验证。前提是两份CSV的列名和ID命名规则一致,可以直接共用同一个Dataset类。
注意sampleSubmission.csv的角色:它是Kaggle风格的任务中规定的提交格式,列名通常像id,probability。生成提交结果时必须按这个文件里的ID顺序输出预测概率,千万不能把训练时的随机顺序直接写进去。常见做法是读取sampleSubmission.csv后,按它的id逐行去预测,再把概率填回probability列:
import pandas as pd submission = pd.read_csv('sampleSubmission.csv') submission['probability'] = submission['id'].apply(lambda sid: predict_single(sid)) submission.to_csv('my_submission.csv', index=False)4.2 结果怎么看:loss下降和AUC指标的关系
跑完50个epoch后,观察到的典型输出有三类情况。第一类是loss稳步下降、验证AUC同步到0.8以上,说明数据加载和训练流程全对,可以直接进入提交环节。第二类是loss下降但val AUC卡在0.5附近,这通常是标签错位或者数据增强过度,常见于CSV和npz的名称对不上。第三类是loss几乎不变,从头到尾像条平线,那就是输入数据出了问题,优先检查Dataset里arr的数值范围是不是全零。
我跑这份资源时还有一个实用做法:第一次只训10个epoch,打印每批数据的shape、标签分布和初始loss,确认没毛病后重启全量训练。这个习惯帮我避开了不少隐性错误,比如某个npz的shape和别的不一样导致DataLoader在collate时报错,等训到一半才暴露就晚了。
4.3 推理时的内存控制
3D医学影像推理时最容易忽略的问题是显存突然爆掉。单个候选块尺寸不大,32x32x32的输入在GPU上毫无压力,但如果把整个npz一次性读入内存再转tensor,内存可能持续走高。推理阶段应该沿用训练时的Dataset机制,保持batch size为1或2,按样本逐个预测。比如predict_single函数内部只负责处理一个文件,避免把所有数据堆进一个tensor。
5. 常见问题与避坑记录:3D数据加载和模型训练的五个典型翻车点
5.1 训练loss为NaN,验证集指标直接崩
现象:刚开始训练loss就是NaN,或者训了若干轮后突然变NaN,模型输出全是nan。
原因:最常见是输入数组里有NaN或Inf,npz里的原始数据没做清洗;其次是归一化时除数为0,比如某个候选块所有体素值都一样,arr.max() - arr.min()等于0,除以0自然产生非数值。
解决:在Dataset里加一层防御,归一化公式改为(arr - arr.min()) / (arr.max() - arr.min() + 1e-8),并对arr先做np.nan_to_num处理。我一般会在归一化前后各打印一次数组极值,确认输入稳定后再进模型。另外,若学习率设置过高也可能导致loss发散到NaN,把LR从3e-3降到3e-4会稳很多。
5.2 多个npz尺寸不一致导致batch训练时报错
现象:训练到某个step时DataLoader抛错,提示“stack expects each tensor to be equal size”,但前几十个batch都正常。
原因:candidate样本来自不同的扫描切片,天然存在尺寸差异。如果有的样本是32x32x32,有的是24x40x32,默认的default_collate会把它们堆不成一个batch tensor。
解决:统一在Dataset里做resize,手段是先中心裁剪到目标尺寸,再用插值补到统一大小。想偷懒也有个不算太稳的办法:把batch size设成1,这样不需要stack,但训练效率太低。我的做法是强制所有样本在__getitem__里最终输出同一个shape,并在初始化数据集时遍历一遍所有npz,记录每个人的原始尺寸,发现不一致提前预警。
5.3 标签错位:训练集AUC高,提交成绩反而垫底
现象:本地验证集AUC接近0.9,模型看起来非常好;但按sampleSubmission.csv提交后,线上成绩接近随机。
原因:本地验证集是用随机种子在train_val.csv内部划分的,如果原始数据本身就存在同一候选块的近邻样本被同时分到训练集和验证集(即泄漏),本地的AUC就被“数据重复”抬高了。另一个更常见的原因是生成提交结果时没有按sampleSubmission.csv的id顺序输出,打乱了样本和预测值的对应关系。
解决:先检查训练集和验证集里是否有重复的candidate编号,有则按id去重后再划分。生成提交结果时,严格以sampleSubmission.csv的id列表为基准,逐行预测,不要用训练DataLoader的顺序。更稳妥的验证方式是把0617.csv作为独立测试集,它和train_val.csv是不同批次的候选块,能更好地模拟真实场景。
5.4 显存OOM:三维卷积比二维吃内存得多
现象:输入尺寸是64x64x64、batch size设为16,GPU显存直接溢出;batch改成2虽然不报错,但训练速度慢得让人崩溃。
原因:Conv3d的参数量和中间特征图体积随边长三次方增长。64的立方体在第一个卷积层就会生成batch x 8 x 64 x 64 x 64的张量,单个张量就占几百兆,多层特征图叠加很容易把显卡打满。
解决:最直接的办法是降低分辨率,从64x64x64降到48或32,精度损失在作业场景下完全可以接受。其次,调整卷积通道数,第一层从8改到4。还有一个被很多人忽视的点:把batch size降到2配合梯度累积等效大batch,训练照样能收敛。如果用的是PyTorch,还可以开启torch.backends.cudnn.benchmark和AMP混合精度,效果立竿见影。
5.5 验证集AUC始终在0.5附近,模型学了等于没学
现象:训练了50轮,训练集loss下降正常,看训练集准确率有90%,但验证集AUC始终在0.52左右波动。
原因:这个场景大概率是数据划分时按文件顺序直接切分,导致验证集和训练集来自不同扫描批次,分布差异大;小概率是正负样本极度不均衡,验证集里某一个类别样本极少,AUC计算不稳定。
解决:先看train_val.csv的label字段分布。如果正负比超过5比1,就改用分层采样保证验证集里两类比例相近;DataLoader里也可以给正样本加权。如果分布正常,再看两个集合里candidate编号是否有共同后缀,有就说明批次关系没处理好,需要重做划分。
6. 二次开发:把固定训练流程改成可扩展的实验Pipeline
这套资源的代码能一次跑通,但它最大的价值不在“跑通”,而在怎么被改成自己的实验框架。我拿到后的第一次改造,是把散落在脚本里的超参数全部抽到一个配置类里,用字典管理,然后确保每次实验都能记录下完整的超参组合和对应AUC。这个习惯在多次调参时非常重要,原代码的“参数可方便更改”指的只是改文件头部,但真正做对比实验时需要能回溯每次训练用了什么参数。
第二步是模型层的替换。原项目的Simple3DCNN结构从零开始可读性好,但如果你想在作业里加一点亮点,可以把它替换成更高阶的3D ResNet变体,只保留forward的输入输出接口不变。在PyTorch里甚至可以先用torchvision里现成的2D ResNet改造成3D版本,把每层卷积的kernel从2D换成3D,再加载预训练权重的前几层做迁移学习,能让小数据集训练更稳。但要注意,迁移学习时input通道必须是1,得先把单通道三维数据重复成三通道再输入,或者在网络入口加一个1x1x1卷积把通道升到3。
第三步是加数据增强。3D医学影像的增强和2D不太一样,可以用torchvision.transforms包一层自定义函数,做随机翻转、随机旋转90度、加高斯噪声。我个人常用的组合是:以0.5概率随机翻转深度方向、随机旋转不超过10度、对体素值施加掺了噪声的弹性扰动。增强幅度不能太大,否则原本就少的训练样本会被破坏掉边界信息,AUC反而下降。
最后一步,把二分类扩展成多分类。假设要按病灶类型区分三到四类,只需把num_classes改成类别数,损失函数保持nn.CrossEntropyLoss不变,DataLoader的label类型从float改成long,最后评估指标从AUC换成macro F1或每类单独计算AUC。这套改完,整个项目就从“作业Demo”变成了一个可复用的小型3D分类实验平台。
我在第一次跑这类医学影像课程作业时也吃过数据对不齐的亏,当时训练好的模型验证集AUC高达0.93,提交时却几乎随机,后来发现是提交文件的行序跟训练DataLoader的索引对不上。从那以后,我每次跑与CSV、npz关联的数据集,都会强制先打印一条样本和CSV对应关系再训练,这个习惯一直保留到现在。希望帮到你。
本文还有配套的精品资源,点击获取