简介这份资源面向医学影像处理与深度学习方向的开发者、学生及科研人员提供一套基于肝癌影像的AI诊断完整代码方案帮助读者在Linux x64环境下搭建从数据预处理到模型训练的全流程实验。包内共7个文件以4个Python脚本为核心辅以2个txt说明与1个md文档压缩包约8KB体量轻便便于快速部署与二次开发。脚本涵盖数据预处理、数据集加载、模型定义与训练等关键环节配合Anaconda3与Python3.6环境需安装TensorFlow 1.8.0及MedPy、nibabel、SimpleITK、scikit-image等医学影像与科学计算库GPU机器可选用tensorflow-gpu版本自动配置CUDA与CUDNN依赖。目前已有37人学习适合希望理解医学影像AI诊断流程、复现肝癌识别实验或作为课程设计参考的读者可从中获取可运行的代码框架与依赖配置思路。1. 肝癌影像 AI 诊断落地从一份「大数据医疗」压缩包说起影像科每天产出成千上万张腹部增强 CT肝癌的早筛却仍高度依赖医生逐帧阅片。一个直径 1 到 2 厘米的结节在动脉期、门脉期、延迟期三套序列里可能只闪现几秒漏诊代价极高。大数据医疗方向里「肝癌影像 AI 诊断」要解决的就是这件事把 DICOM 序列批量转成模型能吃的张量训练一个能自动勾画病灶、给出恶性概率的流水线。它适合两类人——手里有院内脱敏影像、想跑通端到端流程的算法工程师以及要做大数据毕业设计、需要一套完整可复现选题的学生。这篇笔记不讲空泛概念只讲这套东西怎么从零搭起来、参数怎么设、哪里最容易翻车。2. 数据管线把 DICOM 变成能训练的体素张量2.1 为什么肝癌影像不能直接喂给模型CT 原始数据是 DICOM 格式每张切片除了像素矩阵还带一堆元信息层厚、像素间距、窗宽窗位、造影期相。直接读像素会踩两个坑。第一不同设备的 HU 值范围差异巨大不统一窗宽窗位模型学到的是设备差异而不是病灶特征。第二肝脏是三维结构单张切片看不到病灶的上下关系必须按层厚重建出体素。常见做法是先把 HU 值截断到肝脏窗窗位 60、窗宽 160 附近再按像素间距做各向同性重采样让每个体素在 x/y/z 三个方向物理尺寸一致通常是 1mm×1mm×1mm。这一步不做后面所有增强、分割都会因为空间尺度混乱而失真。2.2 用 Python 批量转换与重采样下面这段是数据管线的核心负责把一批 DICOM 目录转成统一 spacing 的 npy 体素。import numpy as np import pydicom from pathlib import Path from scipy.ndimage import zoom def load_series(series_dir, target_spacing(1.0, 1.0, 1.0)): 读取一个 DICOM 序列返回 HU 体素和物理间距 slices [pydicom.dcmread(str(p)) for p in Path(series_dir).glob(*.dcm)] # 按 InstanceNumber 排序保证 z 轴顺序正确 slices.sort(keylambda s: int(s.InstanceNumber)) # 像素值转 HU斜率 截距 hu np.stack([s.pixel_array * s.RescaleSlope s.RescaleIntercept for s in slices]).astype(np.float32) # 原始物理间距z 取层厚x/y 取像素间距 z_spacing float(slices[0].SliceThickness) xy_spacing [float(v) for v in slices[0].PixelSpacing] spacing np.array([z_spacing, xy_spacing[0], xy_spacing[1]]) # 各向同性重采样 factors spacing / np.array(target_spacing) hu zoom(hu, factors, order1) return hu, target_spacing def window_liver(hu, level60, width160): 肝脏窗截断并归一化到 [0,1] lo, hi level - width / 2, level width / 2 hu np.clip(hu, lo, hi) return (hu - lo) / (hi - lo)逻辑说明load_series先按InstanceNumber排序这一步不能省DICOM 文件名顺序和实际层序经常不一致排错会导致体素在 z 轴上下颠倒。RescaleSlope和RescaleIntercept是 HU 转换的必需参数漏掉它们像素值没有物理意义。zoom的order1用线性插值比最近邻平滑又比三次插值快适合大批量预处理。参数方面target_spacing设 1mm 是肝脏分割的通行值window_liver的窗位 60、窗宽 160 是腹部软组织常用窗能同时保留肝实质和低密度病灶的对比。2.3 数据质量检查不能跳过预处理完必须做一轮质量抽检否则训练到一半才发现某批数据全是空层或者方向反了返工成本极高。检查项包括体素形状是否接近预期肝脏序列一般 200 到 400 层、HU 分布是否落在 -100 到 200 之间、有没有全零切片。我一般会随机抽 20 例可视化中间层肉眼确认肝脏轮廓完整、没有明显错位。这一步花十分钟能省掉后面几小时的血泪经验。3. 模型选型3D 分割加分类的双头结构怎么搭3.1 分割和分类为什么建议联合训练肝癌 AI 诊断有两个子任务勾出病灶区域分割判断病灶良恶性分类。分开做也能跑但联合训练有两个实际好处。第一分割的监督信号会强迫编码器学到病灶边界特征分类头共享这套特征后对小病灶更敏感。第二推理时分割掩码可以直接作为分类的注意力区域减少背景肝组织的干扰。常见做法是主干用 3D U-Net 编码器分割头输出病灶概率图分类头在编码器瓶颈层接全局池化加全连接。损失函数用 Dice 加交叉熵的加权和分割权重 0.7、分类权重 0.3 是个稳的起点。3.2 一个可跑通的双头网络骨架import torch import torch.nn as nn class DoubleHeadNet(nn.Module): def __init__(self, in_ch1, base16): super().__init__() # 简化的 3D 编码器两层下采样 self.enc1 nn.Sequential( nn.Conv3d(in_ch, base, 3, padding1), nn.BatchNorm3d(base), nn.ReLU(), nn.Conv3d(base, base, 3, padding1), nn.BatchNorm3d(base), nn.ReLU()) self.enc2 nn.Sequential( nn.Conv3d(base, base * 2, 3, stride2, padding1), nn.BatchNorm3d(base * 2), nn.ReLU()) # 分割头上采样回原尺寸 self.seg_head nn.ConvTranspose3d(base * 2, 1, 2, stride2) # 分类头瓶颈层全局池化 self.cls_head nn.Sequential( nn.AdaptiveAvgPool3d(1), nn.Flatten(), nn.Linear(base * 2, 2)) def forward(self, x): f1 self.enc1(x) f2 self.enc2(f1) seg self.seg_head(f2) # 分割 logits cls self.cls_head(f2) # 分类 logits return seg, cls逻辑说明enc2用stride2做下采样把空间尺寸减半、通道翻倍这是 3D U-Net 的标准节奏。seg_head用转置卷积把特征图恢复到输入尺寸输出单通道 logits配合 sigmoid 得到病灶概率。cls_head在瓶颈层做全局平均池化把三维特征压成一维向量再分类这样对输入尺寸不敏感。参数上base16是显存和表达力的折中24G 显存能跑到 batch size 4 左右如果显存紧张把base降到 8或者把输入体素裁到 128×128×64。训练时分割用BCEWithLogitsLoss分类用CrossEntropyLoss两个 loss 加权求和后统一反向传播。3.3 训练参数怎么设才不玄学学习率用 1e-4 配 Adam是 3D 医学分割里比较稳的组合。batch size 受显存限制能开大就开大BN 层在小 batch 下统计量不稳。epoch 数不用设死盯验证集 Dice连续 10 个 epoch 不涨就停。数据增强重点做三样随机旋转 ±15 度、随机缩放 0.9 到 1.1、随机亮度扰动。注意旋转不要超过 20 度肝脏在腹腔里的解剖位置有方向性转太多会造出解剖上不存在的样本。类别不平衡用带权重的采样器处理病灶体素占比通常不到 5%不处理的话模型会倾向于全预测背景。4. 避坑与排查训练跑不起来时先看这几条4.1 损失不下降先查标签对齐现象训练几个 epochloss 在 0.69 附近不动Dice 接近 0。原因分割标签和输入体素没有对齐常见于重采样时只处理了图像没处理掩码或者掩码用了最近邻而图像用了线性插值导致边界错位。解决图像和掩码必须用同一套 spacing 和同一套几何变换掩码重采样固定用order0最近邻保证标签值不被插值污染。对齐后 loss 会立刻开始下降。4.2 验证集 Dice 高但临床看着全是假阳性现象验证 Dice 0.85但把预测掩码叠回原图发现模型把血管断面、胆囊窝都标成了病灶。原因训练集里血管和病灶的纹理特征相似模型学到了捷径。解决在损失里加假阳性惩罚项或者引入难例挖掘把假阳性高的样本加权重训。更根本的办法是扩充训练集里带血管断面的负样本让模型见过足够多的干扰。4.3 显存溢出但 batch size 已经降到 1现象CUDA out of memory降到 batch size 1 还是爆。原因输入体素太大3D 卷积的激活值占用远超预期。解决把输入裁到 128×128×64 或更小用随机裁剪代替整卷输入开启混合精度训练torch.cuda.amp显存能省 30% 到 40%梯度检查点也能换显存代价是训练慢一些。4.4 推理速度慢到没法上临床现象单例推理要 30 秒以上医生等不了。原因整卷体素一次前向计算量随体积立方增长。解决推理时用滑窗把体素切成有重叠的小块逐块预测再拼接重叠区取平均。滑窗尺寸和训练裁剪尺寸保持一致重叠比例设 0.5 能兼顾速度和拼接平滑度。配合 ONNX 导出和 TensorRT单例能压到 3 秒以内。4.5 不同设备的数据表现差异大现象A 医院数据 Dice 0.9B 医院掉到 0.6。原因不同厂商的造影期相触发时间、层厚、重建核不同模型过拟合了 A 院的成像风格。解决训练时做设备维度的域增强比如随机模拟不同层厚和噪声水平或者用少量 B 院数据做微调。跨中心泛化是大数据医疗落地最硬的骨头别指望一个模型通吃所有设备。5. 进阶技巧用弱标注和主动学习把标注成本压下来像素级分割标注极其昂贵一例肝脏病灶勾画要资深医生十几分钟。实际项目里我一般先用弱标注起步只给病灶的粗略边界框或者中心点用框监督或者点监督训练一个粗分割模型再让模型对未标注数据出预测医生只做修正。这套主动学习循环能把标注量压到全监督的三成左右。具体做法是每轮挑模型预测熵最高的 20 例送标标完混入训练集重训迭代三轮通常能逼近全监督效果。验证这套流程是否真的省了成本不能只看 Dice要算「达到目标 Dice 所需的总标注工时」。我自己的习惯是维护一张标注台账记录每例的标注类型全勾画/框/点/修正和耗时每轮迭代后对比。有一次偷懒没记结果发现主动学习挑出来的高熵样本里有一半是伪影医生修正耗时比从头勾还长这就是没有台账的后悔药。后来加了伪影过滤规则只挑肝脏区域内的高熵样本效率才回来。推理部署上滑窗拼接的边界处理有个细节重叠区不要简单平均用高斯权重加权平均中心权重高、边缘权重低拼接缝会平滑很多。这个改动不增加推理时间但能消掉掩码上肉眼可见的网格状接缝。模型导出 ONNX 时注意把动态轴设对batch 和空间维度都标成动态否则换个体素尺寸就报错。最后说个我踩过的坑别在预处理阶段就把数据增强做死。我早期把旋转增强写进了预处理脚本结果同一例数据每次 epoch 读到的都是同一个旋转角度增强等于没做。增强必须放在训练时的数据加载器里每个 epoch 随机生成变换参数。这个习惯保持到现在希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?