
简介这份资源面向医学影像AI方向的开发者与学习者提供一套基于深度学习的肝癌影像诊断完整代码方案适合具备Python基础、希望上手医学图像分割与分类实践的中级读者。压缩包共7个文件以4个Python脚本为核心涵盖数据预处理、数据集加载、模型定义与训练流程另附2个txt说明与1个md文档整体仅8KB轻量便于快速阅读与二次开发。环境基于x64 Linux与Anaconda3、Python3.6搭建需安装TensorFlow 1.8.0及MedPy、nibabel、SimpleITK、scikit-image、opencv-python等医学影像与科学计算库GPU机器可选用tensorflow-gpu版本自动配置CUDA与CUDNN依赖。目前已有37人学习读者可借此理解肝癌影像从读取、预处理到建模训练的完整链路掌握医学图像处理常用工具与排错思路为复现或改进相关诊断模型提供参考。1. 肝癌影像 AI 诊断从 DICOM 到推理结果一条能跑通的工程链路影像科每天产出成千上万张腹部增强 CT肝癌的早筛与随访高度依赖医生逐帧阅片一个病人动辄三四百层漏诊和疲劳是绕不开的现实。大数据医疗与 AI 诊断这两个词被反复提起但真正落到肝癌影像上从业者最关心的不是概念而是一堆 DICOM 文件怎么变成模型能吃的张量肝脏和病灶怎么分出来模型输出怎么回到医生看得懂的层面。这个方向适合有 Python 基础、懂一点深度学习、手上有影像数据或能拿到公开数据集的工程师和影像科研人员。它不需要你从零训练一个基础模型更多是把数据工程、分割、分类、可视化这几段拼成一条稳定链路。下面按我实际做过的顺序把这条链路拆开讲清楚包括参数怎么设、哪里容易翻车。2. 数据准备DICOM 序列怎么变成可训练的体数据2.1 为什么肝癌影像不能直接当普通图片处理腹部增强 CT 是三维体数据一次检查包含平扫、动脉期、门脉期、延迟期多个序列每个序列又是几百层切片。直接拿单张切片训练会丢掉层间连续性而肝癌的强化特征恰恰依赖三期对比动脉期明显强化、门脉期强化减退这是鉴别肝癌和血管瘤、转移瘤的关键。所以工程上第一步不是写模型而是把同一病人的多期序列对齐、重采样到统一体素间距再切成三维块或带上下文的多通道切片。常见做法是用 SimpleITK 或 pydicom 读取按 SeriesInstanceUID 分组用 ImagePositionPatient 的 z 值排序切片。这里有个血泪经验不同设备、不同扫描协议的层厚和像素间距差异很大有的 0.7mm有的 5mm不重采样直接混着训练模型学到的全是扫描参数而不是病灶特征。2.2 用 pydicom 读取并重采样的最小脚本import pydicom import numpy as np import SimpleITK as sitk from pathlib import Path def load_series(series_dir): 读取一个 DICOM 序列按 z 轴位置排序返回体数据和元信息 slices [] for f in Path(series_dir).glob(*.dcm): ds pydicom.dcmread(str(f)) # 跳过定位像和没有像素数据的文件 if not hasattr(ds, PixelData): continue slices.append(ds) # 按 ImagePositionPatient 的 z 值排序保证层序正确 slices.sort(keylambda s: float(s.ImagePositionPatient[2])) # 用 RescaleSlope/Intercept 把像素值转成 HU volume np.stack([ s.pixel_array.astype(np.float32) * float(s.RescaleSlope) float(s.RescaleIntercept) for s in slices ]) spacing ( float(slices[0].SliceThickness), float(slices[0].PixelSpacing[0]), float(slices[0].PixelSpacing[1]), ) return volume, spacing def resample_volume(volume, spacing, target(1.0, 1.0, 1.0)): 把体数据重采样到统一体素间距默认 1mm 各向同性 img sitk.GetImageFromArray(volume) img.SetSpacing(spacing) resampler sitk.ResampleImageFilter() resampler.SetOutputSpacing(target) resampler.SetSize([ int(round(volume.shape[i] * spacing[i] / target[i])) for i in range(3) ]) resampler.SetInterpolator(sitk.sitkLinear) out resampler.Execute(img) return sitk.GetArrayFromImage(out)这段代码的逻辑是先按物理位置排序切片避免层序错乱再用 RescaleSlope 和 RescaleIntercept 把原始像素转成 HU 值这一步不做的话不同机器的灰度范围完全不可比最后统一重采样到 1mm 各向同性。参数上target 设成 (1.0, 1.0, 1.0) 是腹部 CT 的常用选择层厚太薄的可以适当放宽到 1.5mm 省显存。插值方式选线性标签掩膜重采样时要换成最近邻否则会出现不存在的类别值。2.3 窗宽窗位与归一化的取舍CT 的 HU 范围从 -1000 到 3000 以上直接送进网络肝脏和病灶的对比度会被压缩。常见做法是截断到腹部窗比如 [-100, 300] HU再线性归一化到 [0,1]。也有用多窗融合的把肝窗、软组织窗分别归一化后拼成多通道。我一般先用单窗 [-100, 300] 跑基线效果不够再加通道。注意别用整幅图的 min-max 归一化因为不同病人体内气体和骨骼占比不同会导致同一组织在不同病人间灰度漂移这是很多人翻车的地方。3. 分割与分类肝脏、病灶、良恶性三段式怎么搭3.1 先分割后分类还是端到端肝癌 AI 诊断的落地路径基本分两派一派是端到端分类直接把三期 CT 送进 3D CNN 输出良恶性另一派是先分割肝脏和病灶再基于病灶区域做分类。端到端省事但可解释性差医生不信任一个说不清依据的结论。先分割后分类的好处是分割结果本身就是医生能核对的中间产物病灶的形态、强化方式可以量化成特征分类器的输入也更聚焦。我一般推荐三段式肝脏分割 → 病灶分割 → 良恶性分类。肝脏分割用 nnU-Net 这类成熟框架公开数据集上 Dice 能到 0.95 以上病灶分割难度大得多小病灶、边界模糊的病灶 Dice 经常只有 0.6 到 0.7这时候不要死磕分割精度可以把病灶周围一定范围的肝实质一起裁出来做分类让模型自己学上下文。3.2 病灶裁剪与数据增强的参数设置import numpy as np from scipy.ndimage import rotate, zoom def crop_lesion(volume, mask, margin16, size(64, 128, 128)): 以病灶为中心裁剪固定大小的块margin 是病灶外扩的体素 coords np.argwhere(mask 0) if len(coords) 0: return None center coords.mean(axis0).astype(int) # 按 size 裁剪边界不足时用最小值填充 starts [max(0, center[i] - size[i] // 2) for i in range(3)] ends [min(volume.shape[i], starts[i] size[i]) for i in range(3)] patch volume[starts[0]:ends[0], starts[1]:ends[1], starts[2]:ends[2]] # 补齐到固定尺寸 pad [(0, size[i] - patch.shape[i]) for i in range(3)] patch np.pad(patch, pad, modeminimum) return patch def augment_patch(patch): 训练时的轻量增强随机翻转、小角度旋转、强度扰动 if np.random.rand() 0.5: patch patch[::-1, :, :] if np.random.rand() 0.5: patch patch[:, ::-1, :] angle np.random.uniform(-10, 10) patch rotate(patch, angle, axes(1, 2), reshapeFalse, order1) patch patch * np.random.uniform(0.9, 1.1) np.random.uniform(-0.05, 0.05) return patch裁剪的逻辑是以病灶质心为中心取固定大小的块这样每个样本尺寸一致方便组 batch。margin 参数控制外扩范围设 16 到 32 体素比较常见太小会丢掉病灶边缘的强化特征太大则引入过多无关肝实质。增强只做翻转、小角度旋转和强度扰动不要做大幅弹性形变肝脏是刚性器官形变过大会产生不真实的解剖结构。强度扰动模拟不同扫描剂量和重建参数的差异对跨中心泛化有帮助。3.3 分类头的选型与损失函数分类网络可以用 3D ResNet 或 EfficientNet 的 3D 版本输入裁剪后的多期 patch输出良恶性概率。类别不平衡是常态肝癌阳性样本往往只占少数损失函数用 Focal Loss 或带类别权重的交叉熵。我一般先用加权交叉熵权重按训练集类别频率的倒数设置再观察验证集上的敏感性和特异性。如果敏感性明显偏低再换 Focal Lossgamma 设 2 左右。评估指标不要只看 AUC临床上更关心固定特异性下的敏感性比如特异性 90% 时敏感性能不能到 85% 以上。4. 训练与推理显存、batch、学习率这些参数怎么定4.1 3D 网络的显存账要提前算3D 卷积的显存占用是 2D 的数倍一个 64×128×128 的 patch单样本前向就要几百 MB。常见做法是用混合精度训练把显存压下来同时用梯度累积模拟大 batch。batch size 设 2 到 4 是 3D 分割的常态分类任务因为 patch 小一些可以到 8 或 16。学习率用 1e-4 起步配合余弦退火warmup 几百步避免初期震荡。如果显存不够优先降 patch 尺寸而不是降通道数通道数降太多会损失表达能力。4.2 训练循环里的关键检查点import torch from torch.cuda.amp import autocast, GradScaler def train_one_epoch(model, loader, optimizer, criterion, scaler, device): model.train() total_loss 0.0 for step, (images, labels) in enumerate(loader): images images.to(device, non_blockingTrue) labels labels.to(device, non_blockingTrue) optimizer.zero_grad(set_to_noneTrue) with autocast(): logits model(images) loss criterion(logits, labels) # 混合精度反向传播scaler 自动处理梯度缩放 scaler.scale(loss).backward() # 梯度裁剪防止 3D 网络梯度爆炸 scaler.unscale_(optimizer) torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) scaler.step(optimizer) scaler.update() total_loss loss.item() # 每 50 步打印一次观察 loss 是否稳定下降 if step % 50 0: print(fstep {step}, loss {loss.item():.4f}) return total_loss / len(loader)这段训练循环里混合精度和梯度裁剪是两个保命设置。3D 网络参数量大不加梯度裁剪很容易在某个 batch 上梯度爆炸loss 直接变 NaN。max_norm 设 1.0 是常用值太大起不到保护作用太小会拖慢收敛。打印频率设 50 步是为了及早发现 loss 不降或震荡别等到跑完一个 epoch 才看。验证集要每个 epoch 跑一次记录敏感性和特异性保存特异性 90% 时敏感性最高的那个 checkpoint而不是只存 loss 最低的。4.3 推理阶段的滑窗与后处理整卷推理时体数据太大没法一次送进网络要用滑窗。窗口大小和训练 patch 一致步长设窗口的 1/2 到 2/3重叠区域取平均或投票。分割结果出来后去掉小于一定体素的连通域比如小于 50 个体素的当作噪声。分类结果可以结合病灶体积、强化幅度做简单规则后处理比如动脉期强化幅度低于某个阈值的直接判良性减少假阳性。这些阈值要在验证集上定不要拍脑袋。5. 避坑与排查肝癌影像 AI 落地最常见的五个坑5.1 层序错乱导致模型学到伪影现象训练 loss 正常下降但验证集表现远差于训练集可视化发现模型关注的区域和病灶对不上。原因DICOM 切片按文件名排序而文件名顺序和实际层序不一致导致体数据在 z 轴上是乱的。解决一律按 ImagePositionPatient 的 z 值排序排序后打印首尾层的 z 值确认单调。这个坑在跨设备数据上尤其常见我见过文件名从 IM0 到 IM200 但实际层序是反的。5.2 标签泄漏让评估虚高现象验证集 AUC 到 0.98上线后一塌糊涂。原因同一病人的不同病灶被分到了训练集和验证集模型记住了病人特征而不是病灶特征。解决按病人 ID 划分数据集不是按切片或病灶。如果数据来自多个中心还要做中心间的交叉验证留一个中心完全不参与训练看泛化能力。5.3 窗宽窗位设置不当淹没小病灶现象小病灶在预处理后的图像上几乎看不见模型敏感性很低。原因用了全 HU 范围归一化肝脏和病灶的灰度差被压缩到很小。解决截断到腹部窗 [-100, 300]或者针对小病灶单独用更窄的窗。可以在训练时随机在几个窗之间切换让模型适应不同对比度。5.4 类别不平衡导致模型偏向阴性现象准确率很高但敏感性只有 50% 左右模型几乎全判阴性。原因阳性样本占比低交叉熵被阴性样本主导。解决用加权交叉熵或 Focal Loss权重按类别频率倒数设置。同时调整分类阈值不要用默认的 0.5在验证集上找使敏感性达标的最大阈值。5.5 推理滑窗步长过大漏掉病灶现象整卷推理的分割结果里小病灶时有时无。原因滑窗步长太大病灶刚好落在窗口边缘被截断后网络识别不出。解决步长设窗口的 1/2 以下重叠区域做平均。如果显存允许窗口可以适当放大减少边缘效应。推理完还要检查连通域把被截断的病灶合并回来。6. 把模型输出变成医生能用的东西可视化与量化报告模型跑通只是第一步真正让影像科愿意用的是把输出变成可核对、可量化的东西。我一般会做三件事在原始 CT 上叠加病灶分割的彩色掩膜标注良恶性概率和关键量化指标生成病灶的体积、最大径、动脉期和门脉期的强化幅度对比把三期图像和分割结果拼成一张对比图医生一眼能看出模型关注的是不是病灶本身。量化指标里强化幅度比单纯的概率更有说服力。计算方式是病灶区域在动脉期和门脉期的平均 HU 差值肝癌通常表现为动脉期高强化、门脉期强化减退这个差值在良恶性之间有区分度。把概率和这些指标一起呈现医生可以自己判断模型结论是否合理而不是被动接受一个黑匣子。验证方法上除了常规的 AUC、敏感性、特异性我习惯做一层病灶级别的分析把验证集按病灶大小分层看模型在不同大小病灶上的表现差异。小病灶小于 2cm的敏感性往往明显低于大病灶这个差距如果太大说明模型对小病灶的识别能力不足需要针对性补充小病灶样本或调整裁剪策略。另一个习惯是固定随机种子跑三次训练看指标的波动范围波动超过 3 个百分点的说明数据量或模型稳定性不够别急着上线。这套链路我从数据清洗一路踩到可视化最大的教训是别在模型结构上反复折腾数据质量和预处理的一致性才是决定上限的东西。同一批数据把层序、重采样、窗宽这三件事做扎实比换三个网络结构带来的提升都大。希望帮到你。本文还有配套的精品资源点击获取