
简介本资源为基于多模态多阶段CT图像的深度学习模型Recurr-NET研究论文面向医学影像研究人员、肿瘤科医生、生物信息学家及关注肝细胞癌治疗进展的专业人士。该研究利用香港与台湾多中心数据构建并验证了可预测HCC术后1至5年复发的深度学习模型在早期与晚期复发预测、生存期分层及肝源性死亡风险区分上均优于传统MVI与临床评分。资源包内含1个PDF文件大小约1.4MB完整呈现论文的摘要、方法、实验结果与讨论便于读者系统了解多模态CT影像建模流程与预后评估思路。目前已有159人学习下载适合用于术前预后评估、个性化治疗选择及患者随访管理等科研场景也可作为医疗影像分析方向的学习参考。1. 多模态多阶段CT预测肝癌术后复发从三个时间点到一条风险曲线肝癌术后最怕的不是手术本身而是半年或一年后复查时影像上又冒出新的病灶。临床上判断复发风险靠的是术前增强CT的动脉期、门脉期、延迟期加上术后病理和甲胎蛋白这些指标但把这些信息综合起来给出一个量化风险不同医生之间差异很大。这个标题要解决的就是这件事用术前多模态动脉期、门脉期、延迟期加多阶段术前、术后早期、随访的CT图像训练一个深度学习模型输出每个患者术后复发的概率。适合谁做有几百例带随访标签的肝癌CT数据、会PyTorch基础、想发临床预测模型论文或落地辅助决策工具的影像科工程师和研究生。难点不在网络多深而在多模态配准、多阶段时间对齐和复发标签的截尾处理。2. 多模态多阶段CT的数据组织从DICOM到可训练张量2.1 为什么不能把三个期相直接堆成9通道动脉期、门脉期、延迟期是三次不同时间的扫描患者呼吸和体位会导致肝脏在三个期相中的空间位置有偏移。直接按通道拼接相当于假设三个期相像素级对齐模型会学到大量伪影。常见做法是先把三个期相配准到门脉期再做通道堆叠。配准用SimpleITK的刚性配准加B样条或者用ANTs的SyN算法。配准后重采样到统一体素间距比如1.0×1.0×1.0 mm或1.5×1.5×1.5 mm视GPU显存而定。多阶段指的是术前一次CT、术后1个月复查CT、术后3个月复查CT。这三个时间点的图像不能简单当通道因为时间间隔不等且部分患者缺失。我一般把术前多模态作为一个输入分支术后随访CT作为另一个分支用两个编码器分别提取特征再在特征层做融合。如果术后随访缺失该分支置零并加一个缺失指示向量。2.2 肝脏ROI提取与复发标签定义全腹部CT直接输入网络背景占比太大。先做肝脏分割可以用nnU-Net在LiTS或自家数据上训练一个肝脏分割模型推理时取肝脏mask外扩10 mm作为ROI。复发标签定义要明确术后影像随访发现肝内新发病灶或肝外转移且经临床确认。标签时间从手术日算起随访截止到末次影像或死亡。这里有个关键坑无复发生存时间存在截尾不能简单当二分类。常见做法是用离散时间生存模型把时间切成若干区间每个区间预测条件概率。import numpy as np import pandas as pd from sklearn.model_selection import StratifiedKFold # 假设df包含: patient_id, recurrence(0/1), follow_up_months, modality_paths # 离散时间区间: 0-3, 3-6, 6-12, 12-24, 24 月 bins [0, 3, 6, 12, 24, np.inf] labels [0, 1, 2, 3, 4] def make_discrete_time_label(row): 把生存标签转成离散时间区间标签截尾样本只到其随访区间 t row[follow_up_months] for i in range(len(bins)-1): if bins[i] t bins[i1]: return i return len(bins)-2 df[time_bin] df.apply(make_discrete_time_label, axis1) # 截尾样本: recurrence0只参与其所在区间之前的负样本 # 复发样本: recurrence1在其复发区间为正样本之前区间为负样本这段代码把连续随访时间离散化。参数bins根据你的随访密度调整肝癌复发高峰在术后6到12个月所以6-12月区间要单独切。截尾样本的处理逻辑是如果患者随访18个月未复发他在0-3、3-6、6-12、12-24这四个区间都算负样本但24区间不能算负样本因为他没被观察到那么久。这个细节很多论文翻车把截尾当负样本直接二分类AUC虚高。2.3 数据增强与类别不平衡复发率一般在30%到50%不算极端不平衡但离散时间后每个区间的正样本更少。增强用torchio做3D弹性形变、随机旋转±15度、亮度对比度扰动。注意不要做水平翻转肝脏左右叶解剖位置固定翻转会破坏解剖先验。类别不平衡用focal loss或对正样本加权权重设为负样本数除以正样本数。3. 多模态融合网络设计双分支编码器加时间感知模块3.1 术前多模态分支三期相特征提取术前分支输入是配准后的三个期相形状为3×D×H×W。骨干网用3D ResNet-18或3D DenseNet-121输入通道改成3。如果显存不够可以把三个期相分别过2D切片编码器再在切片维度做注意力聚合。我一般用3D ResNet-18的前四层输出特征图后接一个通道注意力模块让网络自己学哪个期相更重要。动脉期对富血供病灶敏感门脉期看门脉癌栓延迟期看包膜这些先验不用手工加权让注意力学。import torch import torch.nn as nn from torchvision.models.video import r3d_18 class PreopEncoder(nn.Module): def __init__(self, in_ch3, feat_dim256): super().__init__() # 使用3D ResNet-18去掉最后的分类头 backbone r3d_18(pretrainedFalse) # 修改第一层卷积输入通道 backbone.stem[0] nn.Conv3d(in_ch, 64, kernel_size(3,7,7), stride(1,2,2), padding(1,3,3), biasFalse) self.encoder nn.Sequential(*list(backbone.children())[:-2]) self.pool nn.AdaptiveAvgPool3d(1) self.fc nn.Linear(512, feat_dim) def forward(self, x): # x: (B, 3, D, H, W) f self.encoder(x) # (B, 512, D, H, W) f self.pool(f).flatten(1) # (B, 512) return self.fc(f) # (B, feat_dim)这里把3D ResNet-18的第一层卷积输入通道从3改成3原本就是3但如果你用单期相预训练权重需要复制通道。注意stride在深度方向设为1因为肝脏CT层厚可能3mm深度方向不要过早下采样。feat_dim设为256两个分支拼接后512维再进分类头。3.2 术后随访分支与缺失处理术后随访分支输入是术后1个月和3个月的CT同样做多期相配准。如果只有平扫就单通道。这个分支用同样的3D ResNet-18但输入通道数根据实际期相数调整。关键在缺失处理如果患者没有术后3个月CT不能直接补零因为零值在卷积后会产生虚假激活。我一般用一个可学习的缺失嵌入向量替代并加一个二值mask指示哪些时间点缺失。class FollowupEncoder(nn.Module): def __init__(self, in_ch3, feat_dim256, max_visits2): super().__init__() self.visit_encoder PreopEncoder(in_ch, feat_dim) # 缺失嵌入每个随访时间点一个 self.missing_emb nn.Parameter(torch.randn(max_visits, feat_dim)) self.visit_attn nn.Linear(feat_dim, 1) def forward(self, visits, visit_mask): # visits: list of (B, C, D, H, W)长度max_visits # visit_mask: (B, max_visits) 1表示有0表示缺失 feats [] for i, v in enumerate(visits): if v is not None: f self.visit_encoder(v) else: f self.missing_emb[i].unsqueeze(0).expand(v.size(0), -1) feats.append(f) feats torch.stack(feats, dim1) # (B, max_visits, feat_dim) # 用mask加权注意力 attn self.visit_attn(feats).squeeze(-1) # (B, max_visits) attn attn.masked_fill(visit_mask 0, -1e9) attn torch.softmax(attn, dim1) return (feats * attn.unsqueeze(-1)).sum(dim1)缺失嵌入是可学习的让网络自己决定缺失时间点应该贡献什么信息。visit_mask在注意力里把缺失位置mask掉避免softmax分配到缺失位置。这个设计比直接补零稳定血泪经验补零会让模型在验证集上波动很大因为零值经过BatchNorm后分布偏移。3.3 融合与离散时间生存头两个分支输出256维特征拼接成512维过两层全连接加Dropout 0.5输出K个区间对应的logits。K根据你的时间分箱数比如5。损失函数用离散时间生存的负对数似然对于复发样本在其复发区间之前的所有区间算负样本复发区间算正样本对于截尾样本在其随访截止区间之前算负样本之后不算。实现时用一个mask矩阵控制每个样本参与哪些区间的损失。class SurvivalHead(nn.Module): def __init__(self, in_dim512, n_bins5): super().__init__() self.net nn.Sequential( nn.Linear(in_dim, 256), nn.ReLU(), nn.Dropout(0.5), nn.Linear(256, n_bins) ) def forward(self, x): return self.net(x) # (B, n_bins) logits def discrete_time_loss(logits, time_bin, event, n_bins): logits: (B, n_bins), time_bin: (B,), event: (B,) 1复发 0截尾 # 构造每个样本在每个区间的标签和mask B logits.size(0) device logits.device loss 0.0 for i in range(B): t time_bin[i] if event[i] 1: # 复发: 0..t-1为负, t为正 for j in range(t): loss nn.functional.binary_cross_entropy_with_logits( logits[i, j], torch.tensor(0.0, devicedevice)) loss nn.functional.binary_cross_entropy_with_logits( logits[i, t], torch.tensor(1.0, devicedevice)) else: # 截尾: 0..t-1为负, t及之后不参与 for j in range(t): loss nn.functional.binary_cross_entropy_with_logits( logits[i, j], torch.tensor(0.0, devicedevice)) return loss / B这个损失实现是逐样本循环实际训练时可以用向量化mask加速。注意截尾样本在t区间不参与损失因为不知道他是否在t区间复发。这个细节决定了模型是否校准。4. 训练策略与验证五折交叉验证加时间依赖AUC4.1 分层五折与时间分箱分层不能随机五折因为复发时间分布不均匀。按event和时间分箱做分层保证每折中复发和截尾比例、各时间区间比例接近。用sklearn的StratifiedKFold分层变量用event和time_bin组合成字符串。from sklearn.model_selection import StratifiedKFold df[stratify_col] df[event].astype(str) _ df[time_bin].astype(str) skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) for fold, (train_idx, val_idx) in enumerate(skf.split(df, df[stratify_col])): train_df df.iloc[train_idx] val_df df.iloc[val_idx] # 检查各时间区间分布 print(fFold {fold} train time_bin分布:\n, train_df[time_bin].value_counts())分层变量组合后如果某些组合样本太少可以合并相邻时间区间。随机种子固定42方便复现。4.2 时间依赖AUC与校准曲线评估不能用普通AUC因为截尾存在。用时间依赖AUC在t6、12、24个月分别算。Python用scikit-survival的cumulative_dynamic_auc。校准曲线把预测的复发概率分箱和实际Kaplan-Meier估计对比。如果校准差检查损失函数是否把截尾样本错误当负样本。from sksurv.metrics import cumulative_dynamic_auc from sksurv.util import Surv # y_train: structured array with event and time y_train Surv.from_arrays(eventtrain_df[event].astype(bool), timetrain_df[follow_up_months]) y_val Surv.from_arrays(eventval_df[event].astype(bool), timeval_df[follow_up_months]) # 模型输出每个时间区间的风险概率转成生存函数 # 这里假设risk_scores是每个样本的累积风险 auc, mean_auc cumulative_dynamic_auc(y_train, y_val, risk_scores, times[6,12,24]) print(f时间依赖AUC: {auc}, 平均: {mean_auc})时间点选6、12、24个月对应临床随访节点。如果6个月AUC低于0.65检查术前多模态配准质量配准失败会导致动脉期和门脉期错位模型学不到有效特征。4.3 训练超参与早停优化器用AdamW学习率1e-4权重衰减1e-5。Batch size根据显存3D网络一般2到4。训练100个epoch早停 patience 15监控验证集时间依赖AUC。学习率调度用CosineAnnealingLR。混合精度用torch.cuda.amp能省30%显存。如果过拟合加Dropout到0.6或者对术前分支做特征层Dropout。5. 避坑与排查配准、标签、显存、过拟合5.1 配准失败导致模型学伪影现象训练loss下降但验证AUC在0.5附近Grad-CAM热图集中在肝脏边缘而非病灶。原因三个期相配准失败肝脏位置偏移模型学到呼吸伪影。解决配准后做质量检查计算配准后肝脏mask的Dice低于0.9的样本重新配准或剔除。用ANTs的SyN配准比SimpleITK刚性配准稳但慢。我一般先用刚性配准Dice低于0.85再上SyN。5.2 截尾标签当负样本导致AUC虚高现象验证AUC 0.85但校准曲线严重高估复发风险。原因把随访未复发但随访时间短的患者当负样本模型学到“随访短不复发”的虚假关联。解决用离散时间生存损失截尾样本只在其随访区间之前算负样本。检查方法把随访时间小于6个月的截尾样本单独拿出来看模型预测风险是否异常低。5.3 3D网络显存爆炸现象batch size1都OOM。原因输入ROI太大比如128×128×1283D ResNet-18中间特征图占用大量显存。解决ROI降到96×96×96或64×64×64或者用梯度检查点。混合精度训练。如果还不够把3D卷积换成2.5D每个期相按切片过2D ResNet再在切片维度做注意力。5.4 多阶段随访缺失处理不当现象有术后随访CT的患者预测准没有的预测全错。原因缺失分支补零BatchNorm统计量偏移。解决用可学习缺失嵌入加mask注意力如3.2节代码。另外训练时随机丢弃部分随访CT让模型学会处理缺失类似Dropout。5.5 数据泄露同一患者多次CT跨折现象交叉验证AUC 0.9但独立测试集掉到0.6。原因同一患者的术前和术后CT被分到不同折模型记住了患者身份。解决按患者ID分组划分折用GroupKFold。检查方法看训练集和验证集是否有重叠patient_id。6. 进阶技巧用注意力热图做可解释性验证与临床落地模型输出概率后临床医生不会直接信。我一般用Grad-CAM在术前门脉期上生成热图看模型关注区域是否和病灶位置一致。如果热图集中在肿瘤边缘或包膜说明模型学到了复发相关影像特征如果集中在肝脏外或血管说明配准或ROI提取有问题。这一步是验证模型是否学到真东西的后悔药。from pytorch_grad_cam import GradCAMPlusPlus from pytorch_grad_cam.utils.image import show_cam_on_image # 取术前分支最后一个卷积层 target_layer model.preop_encoder.encoder[-1][-1] cam GradCAMPlusPlus(modelmodel, target_layers[target_layer]) # 输入一个batch取门脉期通道 grayscale_cam cam(input_tensorpreop_ct, targetsNone) # 叠加到门脉期切片上 visualization show_cam_on_image(portal_slice, grayscale_cam[0], use_rgbTrue)热图验证通过后把模型输出概率分成低危20%、中危20-50%、高危50%和临床医生讨论阈值。阈值不是固定的根据你所在中心的复发率和治疗策略调整。我习惯在验证集上画决策曲线看不同阈值下的净收益选净收益最大的阈值作为临床推荐。最后说个习惯每次训练完把验证集预测概率和实际复发时间画成散点图看有没有系统偏差。如果高危组实际复发时间明显晚于预测说明模型过度保守。这个图比AUC更能暴露问题。希望帮到你。本文还有配套的精品资源点击获取