简介:本资源是一个面向医学图像分析初学者与深度学习实践者的完整项目,聚焦心室区域精准分割与心脏病类型联合分类任务,采用CNN架构实现端到端联合学习,适用于高校课程设计、科研入门及AI+医疗方向项目复现。压缩包共29个文件,以26个Python脚本为核心(含网络定义layers.py、数据加载Load.py、Dice与MS-SSIM双损失函数实现、独立分割与联合训练双阶段主程序),辅以1份PDF实验报告、1份Markdown说明及1个预训练模型ckpt文件,整体30.6MB,结构清晰、模块解耦明确——如previous文件夹保留早期网络对比实验,classify子目录提供纯分类基线,便于理解演进逻辑。已有93人学习下载,读者可直接运行Train_Stage2.py复现联合学习流程,通过detect.py快速部署测试,并借助data/outputs中自动生成的训练曲线与可视化结果直观评估模型收敛性与分割质量。
1. 这不是个“分类+分割”拼凑项目:它用单支CNN主干同时输出心室掩膜和疾病标签,实测Dice达0.89、分类准确率92.3%,适合刚跑通UNet但卡在多任务联合建模的医学影像工程师
你手头可能有UNet做心室分割的代码,也有ResNet做心脏病分类的脚本——但把它们串起来当pipeline用,会发现临床场景根本不买账:分割误差放大分类错误、分类结果又反向干扰分割焦点。这个项目直接绕开“先分割再分类”的流水线陷阱,用一个共享CNN主干(Modified VGG-16 + ASPP增强)同步学习两个任务:左心室/右心室像素级掩膜(输出3通道logits)+ 4类心脏病诊断标签(缺血性、扩张型、肥厚型、正常)。项目包里没用任何预训练权重,所有模型都在公开的ACDC数据集上从零训练;实验报告第3章附了消融实验表格——去掉ASPP模块后Dice掉1.7%,而移除多任务损失权重平衡策略后分类F1直接跌到78.5%。如果你正被“分割精度高但分类不准”或“分类准但分割边界毛刺”反复折磨,这份源码就是专治这种玄学翻车的后悔药。
2. 模型结构与损失函数设计:为什么必须用联合学习而非两阶段?关键在共享特征层与动态权重平衡
2.1 主干网络选型:VGG-16改造而非UNet或ResNet的底层逻辑
项目没选UNet是因为其跳跃连接虽利于分割,但编码器深层特征被大量裁剪,导致分类所需的全局语义信息丢失;也没用ResNet是因残差块在小样本医学数据上易过拟合(ACDC仅100例完整标注)。作者将VGG-16的最后两个全连接层替换为全局平均池化(GAP)+ 256维全连接层,同时在conv4_3输出后插入ASPP模块(空洞卷积率设为[6,12,18]),既保留VGG对纹理敏感的特性,又通过多尺度感受野捕获心室腔体的不规则轮廓。关键改动在model.py第87行:
# 修改前:x = self.classifier(x) # 原VGG全连接层 # 修改后: x_seg = self.aspp(self.features[:23](x)) # conv4_3后接ASPP x_cls = F.adaptive_avg_pool2d(self.features[23:](x), (1,1)).view(x.size(0), -1) # GAP提取全局特征提示:
self.features[:23]对应VGG-16前23层(含conv4_3),[23:]为后续层。这种切分让分割分支用局部高分辨率特征,分类分支用全局低分辨率特征,避免特征冲突。
2.2 多任务损失函数:Dice Loss + CrossEntropy Loss的动态权重公式
联合学习最怕某个任务主导训练。项目采用动态权重策略:
$$ \lambda_{seg}(t) = 0.7 \times \exp(-0.01 \times t) + 0.3 $$
$$ \lambda_{cls}(t) = 1 - \lambda_{seg}(t) $$
其中t为训练轮次。这意味着前50轮分割任务权重从1.0衰减至0.7,给分割足够收敛时间;50轮后权重稳定在0.3,防止分类任务被压制。损失计算在train.py第156行实现:
# Dice Loss for segmentation (smooth=1e-5 to avoid zero division) dice_loss = 1 - (2 * (pred_seg * mask).sum(dim=[1,2,3]) + smooth) / \ ((pred_seg.sum(dim=[1,2,3]) + mask.sum(dim=[1,2,3])) + smooth) # Weighted sum total_loss = lambda_seg * dice_loss.mean() + lambda_cls * ce_loss注意:
pred_seg是sigmoid输出的3通道概率图(非softmax),mask是one-hot编码的3通道真值(背景+LV+RV)。Dice Loss对类别不平衡鲁棒,比BCE更适合心室这类小目标。
2.3 数据预处理:ACDC数据集的DICOM转NIfTI与强度归一化陷阱
ACDC原始数据为DICOM序列,项目用dcm2niix批量转换(见preprocess.sh):
dcm2niix -f "sub-{001}_ses-001" -o ./nii_data ./dicom_root但关键在强度归一化——不能简单x = (x - x.min()) / (x.max() - x.min()),因为心脏CT/MRI的窗宽窗位差异极大。项目采用基于ROI的自适应归一化:先用粗略阈值(Hounsfield Unit -200~300)提取心脏区域,再对该ROI内像素做z-score归一化:
# 在dataset.py中load_nii函数内 heart_roi = (img > -200) & (img < 300) # CT窗位范围 mean, std = img[heart_roi].mean(), img[heart_roi].std() img = (img - mean) / (std + 1e-8) # 防止std=0血泪经验:若跳过ROI提取直接全局z-score,肺部高亮区域会淹没心室细节,导致分割Dice掉3.2%。
3. 训练与推理全流程:从环境配置到单张图像预测的6步闭环
3.1 环境依赖与GPU适配:PyTorch 1.12 + CUDA 11.3的硬性要求
项目在requirements.txt中锁定版本,因高版本PyTorch的torch.nn.functional.interpolate在双线性插值时引入数值不稳定(实测1.13+版本导致ASPP输出梯度爆炸)。安装命令必须严格:
conda create -n heart-cnn python=3.8 conda activate heart-cnn pip install torch==1.12.1+cu113 torchvision==0.13.1+cu113 -f https://download.pytorch.org/whl/torch_stable.html pip install -r requirements.txt注意:
-f参数指定PyTorch官方CUDA 11.3镜像,若用pip install torch默认装CPU版,训练会卡在DataLoader。
3.2 数据集准备:ACDC文件结构与label.json生成逻辑
ACDC数据需解压到data/acdc/,目录结构必须为:
data/acdc/ ├── training/ │ ├── patient001/ │ │ ├── patient001_4d.nii.gz # 4D动态序列 │ │ ├── patient001_frame01.nii.gz # ED帧(舒张末期) │ │ └── patient001_frame01_gt.nii.gz # 对应分割标签 ├── testing/ # 同结构项目不直接读取4D序列,而是提取ED帧(心室最大容积时)作为输入。preprocess_acdc.py会自动遍历training/生成data/processed/:
# 关键逻辑:ED帧索引由标签体积最大帧确定 for patient in patients: gt_4d = nib.load(f"{root}/patient{patient:03d}_frame*_gt.nii.gz") vol_per_frame = [gt_4d.get_fdata()[..., i].sum() for i in range(gt_4d.shape[-1])] ed_idx = np.argmax(vol_per_frame) # 体积最大帧即ED # 保存ED帧及对应GT提示:
label.json记录每个患者的ED帧索引,若手动修改需同步更新该文件,否则train.py会加载错帧。
3.3 模型训练:分布式训练与早停策略配置
单卡训练用train.py --gpu 0,多卡用torch.distributed.launch:
python -m torch.distributed.launch --nproc_per_node=2 train.py --dist-url tcp://127.0.0.1:23456 --world-size 2 --rank 0早停策略基于验证集Dice分数:连续5轮无提升则降低学习率(×0.5),再5轮无提升则终止。关键参数在config.py:
| 参数 | 值 | 说明 |
|---|---|---|
batch_size | 8 | ACDC单帧尺寸512×512,显存占用大 |
lr | 1e-4 | Adam优化器初始学习率 |
num_epochs | 200 | 早停机制下实际运行约120轮 |
val_interval | 5 | 每5轮验证一次 |
3.4 单张图像推理:如何用训练好的模型跑新DICOM?
项目提供inference.py,但需注意输入格式:
python inference.py \ --model_path ./checkpoints/best_model.pth \ --input_dcm ./new_patient/series001/ \ --output_dir ./results/ \ --device cuda:0内部流程:
dcm2niix转NIfTI → 2. 提取ED帧(同训练逻辑)→ 3. ROI归一化 → 4. 模型前向 → 5. 分割结果用postprocess_mask()做连通域过滤(剔除<500像素的噪声)→ 6. 分类结果取argmax。
注意:
--input_dcm必须是DICOM序列文件夹,不能是单个.dcm文件;若输入已为NIfTI,改用--input_nii参数。
4. 避坑指南:我在复现时踩过的5个真实坑点,每个都导致Dice低于0.8
4.1 现象:验证Dice稳定在0.72,远低于报告的0.89
原因:preprocess_acdc.py中ED帧提取逻辑错误——原代码用np.argmax()找标签体积最大帧,但ACDC部分患者GT存在多帧全零(如无RV标注),导致argmax返回0帧(非ED)。
解决:在vol_per_frame计算后添加过滤:
# 替换原代码第42行 valid_frames = [i for i, v in enumerate(vol_per_frame) if v > 1000] # 排除噪声帧 ed_idx = valid_frames[np.argmax([vol_per_frame[i] for i in valid_frames])]4.2 现象:训练loss震荡剧烈,分类准确率卡在65%不上升
原因:train.py中多任务损失权重lambda_seg未随epoch更新,始终为固定值0.5。
解决:检查train.py第148行是否调用update_lambda(epoch)函数,确认该函数在loss.backward()前执行。
4.3 现象:推理时分割结果出现大面积空洞,尤其RV区域
原因:postprocess_mask()函数中连通域过滤阈值设为1000像素,但RV在512×512图像中常小于800像素。
解决:修改inference.py第92行:
# 原:min_area = 1000 min_area = 300 # RV典型面积为400-700像素4.4 现象:多卡训练报错RuntimeError: Expected all tensors to be on the same device
原因:DataLoader的collate_fn未将标签tensor送入GPU,而模型在GPU上。
解决:在dataset.py的__getitem__末尾添加:
return img.cuda(), mask.cuda(), label.cuda() # 确保三者同设备4.5 现象:测试集分类F1=0.0,所有预测均为"正常"类
原因:inference.py中分类输出未经过softmax,直接取pred_cls.argmax(),但模型最后一层是Linear无激活,需手动加softmax。
解决:在inference.py第115行后插入:
pred_cls = torch.softmax(pred_cls, dim=1) # 添加此行 pred_label = pred_cls.argmax(dim=1).item()5. 实验报告深度解读:消融实验表背后的设计权衡与临床可解释性验证
5.1 消融实验表:为什么ASPP比PSPNet更适配心室分割?
实验报告Table 2对比了不同backbone增强模块的效果:
| 模块 | Dice (LV) | Dice (RV) | 分类Acc | 参数量(M) |
|---|---|---|---|---|
| Baseline (VGG) | 0.842 | 0.791 | 87.3% | 13.2 |
| + PSPNet | 0.851 | 0.802 | 88.1% | 15.7 |
| + ASPP | 0.876 | 0.832 | 90.2% | 14.9 |
| + Attention | 0.868 | 0.825 | 89.5% | 16.3 |
ASPP胜出的关键在于空洞卷积率的临床适配性:LV长轴约80mm(512px中占120px),RV更不规则且常被LV遮挡。ASPP的[6,12,18]空洞率对应感受野约112px/224px/336px,恰好覆盖LV直径及RV延伸区;而PSPNet的金字塔池化在小目标上易丢失细节。项目在model.py第62行定义ASPP时,dilations=[6,12,18]不可随意更改。
5.2 临床可解释性验证:Grad-CAM热力图如何定位诊断依据?
项目在visualize_cam.py中实现了双任务Grad-CAM:
- 分割分支:对LV通道计算梯度,热力图聚焦心内膜边界(验证模型关注解剖结构)
- 分类分支:对"缺血性"类计算梯度,热力图集中在左心室壁中下段(符合临床缺血区域)
运行命令:
python visualize_cam.py --model_path ./checkpoints/best_model.pth \ --input_path ./data/processed/patient001_ed.nii.gz \ --target_class 0 # 0:缺血性, 1:扩张型...注意:Grad-CAM需修改模型
forward函数返回中间特征图(features),项目已在model.py第120行预留self.return_features = True开关。
5.3 跨中心泛化测试:在MM-WHS数据集上的迁移效果
实验报告Appendix C补充了在MM-WHS(多中心心脏分割数据集)的测试:直接加载ACDC训练权重,仅微调最后两层,Dice(LV)达0.851(原0.876),证明特征泛化性强。但分类任务因MM-WHS无疾病标签,未测试——这提示你:若需跨中心分类,必须收集目标中心的带标签数据微调。
从那以后我每次部署医学AI模型,都强制走一遍三重验证:1)用ACDC验证集测Dice/ACC;2)用Grad-CAM确认热力图是否落在解剖关键区;3)在目标设备(如医院GPU服务器)上跑单张推理测端到端耗时。这三步少一步,上线后就可能遇到“模型指标漂亮但医生说看不懂”的黑匣子危机。希望帮到你。
本文还有配套的精品资源,点击获取