前阵子集中干了件事:把三篇顶刊上关于膀胱癌与机器学习的论文端到端复现了一遍。说是“复现”,其实不光是跑通代码,而是从公开数据源把原始数据拉下来,按论文的方法描述重新做特征提取、建模、评估,最后跟论文报告的数字逐项对比。整个过程花了将近三周,期间有几天卡到凌晨两三点,踩的坑比预想的多得多,但跑完之后对这类研究有了完全不一样的理解。
这篇手记我打算把最有价值的经验留下来:三篇论文分别走的技术路线、数据准备的实操细节、核心算法的参数设置、评估指标怎么一步一步实现,以及我在复现过程中遇到的十个典型问题和排查方法。无论你是刚接触肿瘤数据挖掘,还是已经有一定经验想做论文复现,这篇都值得花二十分钟看完。里面不会只给你听概念,而是尽量给出可以直接参考的流程和代码思路。
1. 三篇论文是什么:膀胱癌与机器学习的三条技术路线
先交代一下背景。膀胱癌是泌尿系统里很常见的恶性肿瘤,临床上最关心的问题无非是几个:肿瘤到底有没有侵犯肌层、术后会不会复发、分子分型是哪一类、该选择什么治疗方案。机器学习的介入方式也基本围着这几个问题转。三篇论文恰好覆盖了三种完全不同的技术路线,我觉得放在一起看是很有意思的。
1.1 影像组学路线:从MRI到肌层浸润预测
第一篇论文做的是术前预测膀胱癌是否已经侵犯肌层,典型的影像组学(Radiomics)流程。它用的是术前的多参数MRI,包括T2WI和DWI序列,由有经验的影像科医生在图像上手工勾画肿瘤区域作为感兴趣区(ROI),然后从中提取大量人工设计的影像特征——包括一阶统计量、形状特征、纹理特征等。提取上来的特征先做可重复性筛选,再用LASSO压到十几个关键特征,最终以逻辑回归输出一个预测概率。
这条技术路线背后有一个很核心的临床需求:肌层浸润性膀胱癌和非肌层浸润性膀胱癌的治疗策略完全不一样,前者通常要做根治性膀胱切除,后者更多是经尿道切除后保留膀胱。如果能靠术前MRI准确预测浸润状态,就能帮医生更精准地做治疗决策,也避免一部分不必要的创伤性手术。
1.2 多组学整合路线:从转录组到风险分层
第二篇论文走的是多组学整合路线,数据来自公开的膀胱尿路上皮癌转录组队列。它同时拉了信使RNA表达谱、微小RNA表达谱和对应的临床病理信息,先用差异分析和单因素Cox回归筛掉大量无关基因,再用LASSO-Cox构建预后相关风险评分,最后根据风险评分中位数把病人划分为高风险组和低风险组。
这篇文章的亮点其实不在某个具体算法,而在“整合”和“分层”这两个动作。单一组学数据往往只能从某一个侧面反映肿瘤生物学特征,把多个维度的转录组信息联合起来,理论上能更全面地刻画每个肿瘤的异质性。实操中用的模型并不复杂,但整个特征筛选链条特别讲究顺序,任何一步放错位置都可能让结果虚高。
1.3 病理全切片路线:用迁移学习逼近分子分型
第三篇论文的技术路线最前沿:完全跳过人工勾画和手工特征,直接对数字病理全切片图像(WSI)做深度学习。做法是先加载在ImageNet上预训练好的卷积神经网络,例如ResNet50,把一行切片裁成若干张224×224的小图,逐张经过骨干网络转成特征向量,再用多实例学习框架把整张切片的全部特征压缩成一个预测结果,最终输出膀胱癌的分子分型,比如管腔型和基底型。
这条思路的“反直觉”之处在于:分子分型本来是靠基因测序确定的,而它试图从组织形态学图像里推测出分子层面的信息。实际操作中我最大的感受是,病理切片的预处理工作量远远大于模型本身,一个全切片文件可能就有几百MB甚至超过1GB,组织区域检测、Patch抽取、特征缓存这些工程步骤才是真正耗时的地方。
三条路线可以这样做一个快速对比:
| 对比维度 | 影像组学论文 | 多组学论文 | WSI深度学习论文 |
|---|---|---|---|
| 数据形态 | MRI影像+人工勾画ROI | 转录组表达谱+临床数据 | 数字病理全切片+分子分型标签 |
| 核心特征 | 手工影像特征 | 基因表达特征 | 深度神经网络自动特征 |
| 特征选择 | ICC+LASSO | 单因素Cox+LASSO-Cox | 无需显式特征选择 |
| 建模方式 | 逻辑回归 | Cox比例风险模型 | 多实例学习 |
| 主要评估指标 | AUC、校准曲线、DCA | C-index、时间相关ROC、K-M曲线 | AUC、混淆矩阵、注意力热图 |
| 复现难度 | 中,依赖人工标注 | 中低,依赖数据清洗 | 高,依赖大规模算力和预处理 |
2. 数据准备:复现顶刊代码最容易败走的一步
如果让我给复现难度排序,数据准备绝对排在模型建模前面。很多人拿到论文兴冲冲开始写代码,结果卡在第一步:压根不知道上哪下载数据,下载完又发现文件格式对不上。我把三篇论文的数据处理过程拆开说。
2.1 表达谱与临床数据:下载、匹配、清洗
第二篇论文的数据基本来自TCGA的膀胱尿路上皮癌队列。下载表达数据时,GDC门户下载得到的通常是counts、TPM或FPKM其中一种形式,不会自动帮你整理成漂亮的样本×基因矩阵。最容易被忽略的是样本条形码的匹配规则:TCGA的barcode长得很像TCGA-XX-XXXX-XX,前12位是样本标识,后面还有分析物类型等字段,比如01A代表原发肿瘤的RNA分析物,11A代表正常组织。做生存分析时要把表达矩阵、临床XML文件里的生存时间、生存状态、病理分期全部对齐,字符串截取很快,但一旦遇到编号长度不统一的样本就会出错。
我的习惯是下载数据时记录好版本信息,包括下载日期和manifest文件的快照。GDC隔一段时间会更新数据版本,同一批样本在不同版本下的表达值会有细微差别,如果别人复现的版本和你不一样,结果对不上是很正常的。把版本号存进项目目录,后续排查差异时会省下大量时间。
昨天有朋友问我,TCGA下载下来的是counts,要不要自己转TPM?这个问题没有标准答案。我复现时是跟论文保持一致:论文写了“log2(counts+1)”,我就按counts处理;论文写“TPM”,我就用TPM。转录组数据不同标准化方式对最终模型的影响非常大,复现时必须严格贴着论文的描述走,而不是自己觉得哪种好就换哪种。
2.2 影像数据:从DICOM到可计算的特征矩阵
第一篇论文的影像数据来自公开的影像数据库,下载下来的原始文件是DICOM格式,不能直接喂给特征提取工具。需要先转成常见的医学图像格式(比如NIfTI),再用图像分割掩膜(mask)把肿瘤区域圈出来。PyRadiomics是这套流程里最常用的工具,但它要求输入的是重采样到同一坐标网格后的图像和掩膜。
实际操作中有两个坎。第一,MRI的体素间距在不同序列、不同病人之间不一致,有的0.5毫米,有的1.5毫米,不统一重采样的话,纹理特征就是一堆不可比较的数。第二,ROI掩膜的来源。如果发表论文时没有同时发布分割掩膜文件,你就只能用论文里附带的示例图去近似,或者干脆自己重新分割,这样做出来的特征值和原文差出一个量级都不奇怪。
我在复现第一篇论文时,参考了论文方法部分的描述,也参考了许多影像组学研究公认的默认参数:体素重采样到1×1×1毫米,灰度离散化采用固定bin宽度25,特征提取类别覆盖一阶统计量、形状特征、灰度共生矩阵(GLCM)、灰度游程矩阵(GLRLM)、灰度大小区域矩阵(GLSZM)等。这些参数只要有一个跟原文不一样,后面LASSO选出来的特征清单就会变,最终AUC肯定也对不上。
2.3 WSI病理数据:组织检测与Patch抽取
第三篇论文的数据获取难度比前两篇明显高一个级别。WSI的常见格式是SVS,单个文件体积很大。读取时基本用OpenSlide库按坐标动态截取patch,不能一次性把整个文件读进内存。预处理的第一步通常是在低倍率下生成一张缩略图,用OTSU阈值或饱和度阈值把白色背景筛掉,再把组织区域的坐标映射到目标倍率,最后按224×224的窗口切成小块。
这一步听起来简单,做起来全是细节。不同扫描仪的染色条件不同,切片的折叠区域、气泡区域、墨迹标注区域都会混进patch里。论文如果只写一句“对每个patch做标准化”,你根本不知道它用的是ImageNet全局统计量还是自己数据集算出来的统计量。我最终选了ImageNet的均值和标准差,同时每个patch独立做归一化,这样跟当前深度学习的主流做法保持一致,复现结果也和原文比较接近。
提一句硬件层面的经验:一张切片切出的patch数量可能上千甚至上万,如果每个epoch都重新跑一遍骨干网络提取特征,时间成本完全不可接受。我通常先把所有patch过一次骨干网络,把embedding结果缓存成文件,之后训练分类头时直接读缓存。这个“两阶段”思路能省掉至少80%的重复计算时间,尤其是在显存不充裕的机器上,非常值得养成习惯。
2.4 外部验证队列:怎么选才不会泄漏
外部验证队列的选择看起来是纯数据分析问题,实际很容易踩坑。第二篇论文用GEO里的芯片数据集做外部验证,而训练集是TCGA的RNA测序数据,属于跨平台验证。跨平台验证本身没问题,但必须做足批次效应处理,否则模型表现在外部队列上可能非常差,这并不代表模型本身有问题。
我在挑选外部验证集时主要看三点:平台注释是不是完整、临床终点定义是否跟训练集一致(比如总生存期vs无复发生存期)、样本量是否足够支持亚组分析。如果外部队列的随访时间普遍偏短,用它验证三年生存预测就会缺失大量远期信息,结果自然没有说服力。一个合格的外部验证集不是“能下载就行”,而是“终点定义、平台类型、样本组成都尽量贴近训练集”。
3. 核心算法与参数实现:这些细节决定成败
数据拿到手之后,建模本身反而不复杂,但每一个算法细节都直接影响结果。这一章我把三篇论文里通用的几步拆开讲,重点说参数选择和理解背后的原理。
3.1 特征筛选:LASSO的lambda选取与ICC门槛
影像组学和转录组学研究都离不开特征筛选,LASSO是出现频率最高的方法。但LASSO里的lambda到底怎么选,论文里如果写了“通过十折交叉验证选择lambda.min”,那是一个标准做法,但实操中lambda.min对应最小交叉验证误差的那个值,lambda.1se则是最简单且误差在最小值一个标准误范围内的模型。如果论文不写清楚用哪个,复现的人只能各猜各的,结果差0.03都很正常。我在复现第二篇论文时固定了随机种子,确保交叉验证的分折方式可复现,然后按论文的指标选择为“lambda.min”来做。
ICC(组内相关系数)是影像组学里用来评估特征可重复性的指标。论文通常写“由两名影像科医师独立勾画ROI,保留ICC>0.75的特征”。真正实现的时候,需要同一个病人同时存在两套ROI掩膜,分别提取特征后计算一致性。如果公开数据里没有提供第二套掩膜,这一步就只能跳过,但跳过之后特征筛选的起点就不一样了。试过一次之后我的体会是:ICC是影像组学论文里最容易被复现者忽略、但影响相当大的前置步骤。
3.2 模型构建:从传统机器学习到深度迁移学习
第一篇论文的最终模型通常用带罚项的逻辑回归或SVM,因为样本量不大、特征已经筛过一轮,逻辑回归足够稳定,还可以直接画列线图。SVM适合高维小样本,但是解释性差。我在复现时选了L2惩罚逻辑回归,惩罚强度C用训练集内部的交叉验证选出,C越小正则化越强。如果是生存数据,就改为LASSO-Cox或随机生存森林。
深度学习路径的核心是迁移学习。加载预训练好的ResNet50后,通常的做法是冻结前几层的权重,只微调后面几层,或者干脆完全冻结骨干网络,只训练注意力池化层和最后的分类层。我把初始学习率设为1e-4,优化器用带动量的SGD。这里想提醒一句:把ImageNet预训练模型直接用到病理图像上,虽然域差距很大,但作为特征提取器仍然是有效的,这一点在多个研究中都被验证过。
3.3 多组学模型的特征融合技巧
多组学整合不是把两个表达矩阵直接拼起来就行。第二篇论文的做法是先分别对mRNA和miRNA做差异分析和单因素Cox初筛,再把筛选出的特征合并成统一特征空间,最后做LASSO-Cox。这个顺序决定了哪些特征能留下来:如果先合并再筛选,共线性强的特征会互相挤压,弱信号容易被丢掉;如果初筛太严格,又可能过早砍掉有交互作用的位点。
复现时最安全的策略是跟着原文的顺序走。不要自己随手改特征筛选顺序,也不要为了方便把标准化放到训练集和测试集合并后统一做。正确的做法永远是对训练集做fit,再用同一组参数对测试集做transform,这个原则在特征筛选、缺失值填补、标准化各个环节都适用。
4. 评估体系复现:从AUC到DCA一整套流程
模型建完之后,真正拉开文献档次的是评估体系。很多论文的亮点不在于方法多惊艳,而在于评估全面、贴近临床决策。这一部分我按二分类模型和生存模型两种场景分别讲。
4.1 ROC与时间相关AUC
二分类模型直接画ROC曲线,用AUC衡量区分度,这个谁都懂。但生存模型的AUC并不是一个静态数字,它在不同时间点取值不同。比如你关心两年生存预测,那时间点就设为730天。R语言里survivalROC包的survivalROC()函数可以计算时间相关AUC,方法参数有NNE和KM两种,样本量不同时两种方法的表现差异不小。Python里sksurv库也提供了类似实现。
C-index是生存模型绕不开的另一个指标,它衡量的是模型对不同个体的风险排序能力。可以简单理解为:随机抽取两个可比较的个体,模型给风险较高那个个体打分更高的概率。C-index不是直接拿预测概率套二分类AUC公式,比较逻辑是排序而不是分类,千万别搞混。
4.2 校准曲线实现
校准曲线用于评估“预测概率与实际结果是否一致”,它跟区分度是两回事。AUC再高,如果预测概率整体偏高或偏低,说明模型校准有问题。二分类的校准曲线实现起来很直接:把预测概率按从低到高分成十组,计算每组的平均预测概率和实际阳性率,然后画成曲线对比对角线。
生存数据要做校准曲线就麻烦一些,因为存在删失。我通常用R语言的riskRegression包或rms包的校准函数,在指定时间点(比如三年)做校准。复现时最容易犯的错误是直接用全部随访时间做校准,忽略了删失的影响,画出来自然跟原文对不上。这个小细节值得多花一点时间理解。
4.3 决策曲线分析(DCA)的细节
DCA是很多临床预测模型论文里必放的图,但它的解释往往被低估。DCA计算的是在不同阈值概率下,把高风险病人筛出来进行临床干预所获得的净获益。所谓净获益,真阳性带来的收益减去假阳性带来的危害,跟阈值有关。两条基线分别是“所有病人都干预”和“所有病人都不干预”。
复现时我常看到有人把DCA画出来就完事,从不解释阈值范围怎么选。如果论文没给范围,画0.01到0.99是常规操作,但临床真正有意义的阈值通常在0.1到0.5之间。如果模型只在极低阈值下优于全干预策略,那说明这个模型距离临床可用还很远。理解DCA背后的临床决策含义,比画图本身重要得多。
4.4 列线图的构建与解读
列线图(Nomogram)是把回归方程可视化的经典工具,把所有预测因子放在同一张图上,每个变量对应一个分值刻度,把各项分值累加得到总评分,再映射到预测概率。我用R语言的rms包构建列线图,核心是lrm或cph对象加nomogram()函数。
画列线图最容易翻车的地方在于变量取值范围的设定。训练集里年龄是40到80岁,画图时间却默认了20到100岁,所有患者的预测结果都会偏差。复现时必须把nomogram()函数中每个变量的limits参数严格设置成训练集的取值范围。这一步做完,前面所有建模只是为了数学,列线图才是真正面向医生的交付物。
5. 复现过程中的真实Bug与排查记录
复现三篇论文期间,我遇到了大量问题。挑几个有代表性的写下来,附带我的排查思路和最终解决方案,希望你能避开同样的坑。
5.1 数据划分时的信息泄漏
第一个重大教训来自第二篇论文。一开始我用全数据集做LASSO特征选择,然后才划分训练集和测试集,测试集AUC直接飙到0.95,比原文还漂亮。但仔细一想,这个结果是假的,因为特征选择过程看到了测试集的标签信息。正确做法是:先切分训练集和测试集,然后在训练集内部完成全部特征筛选和数据预处理,测试集从头到尾只能碰一次。
这个问题几乎可以称为“复现类工作第一杀手”。现在我看任何一篇预测模型论文,第一反应就是检查它有没有在方法部分明确写出“所有特征选择和模型拟合只在训练集上进行”。这条规定没写清楚的论文,结果可信度都要打个问号。
5.2 标准化和缺失值填补的顺序问题
跟数据泄漏类似但更隐蔽的是标准化和缺失值填补的顺序问题。正确的做法是:在训练集上计算出标准化所用的均值、标准差、缺失值填补用的中位数等统计量,然后把这些统计量应用到测试集。我最初图省事,把全数据标准化后再拆分,结果测试集和训练集的分布重叠,再次产生泄漏,AUC虚高约0.04。
推荐做法是把这些操作封装进一个Pipeline对象。训练集上用Pipeline的fit方法,测试集上只调用transform,这样每一步的统计量都限定在训练集范围内,逻辑清晰,也不容易出错。
5.3 种子不固定导致AUC波动
交叉验证的折分、随机森林的抽样、深度学习的权重初始化、bootstrap重采样步骤全都依赖随机状态。不同的随机种子可能让最终指标波动0.01到0.03。顶刊论文的差距往往就在这零点零几,所以复现时务必要固定所有能固定的随机种子。
我在项目中约定:每次运行前把全局种子设为固定值,同时记录运行时间。如果后续稍微改动了一个预处理步骤,导致指标发生变化,可以快速排查是改动造成的还是随机波动造成的。这个小习惯帮我排掉了大量疑神疑鬼的时间。
5.4 PyRadiomics版本差异导致特征不一致
PyRadiomics在2.2.0和3.x版本之间,部分特征的计算实现有细微调整,同一张图像提取出来的特征值会有差别。如果复现时用的版本跟原文不一致,后面无论怎么调参,特征值都对不上,LASSO选的特征清单也会变化。
解决办法是用虚拟环境固定库版本,并在项目文档里记录环境信息。我在复现第一天就把所有依赖库的版本号导出来存档,后面排查问题时翻出来一看,立刻定位到版本差异造成的偏差。
5.5 深度学习显存与推理速度
第三篇论文在训练阶段最常碰到显存不足。一张切片切出几千个patch,如果一次性全部过模型,显存直接爆掉。我的做法是用较小批尺寸逐批提取并缓存特征,之后训练分类头时不再需要耗费大量显存。
另外一个容易被忽略的瓶颈是磁盘IO。读几百MB的SVS文件时,磁盘速度决定了patch抽取时间。按固定坐标批量读取并缓存成npy文件,比每次训练都重新切patch快得多。实测下来这个“预处理一次、训练多次”的策略,让整个复现周期缩短了一个数量级。
6. 复现之后的冷思考
跑完三篇论文之后,最大的感受是:顶刊论文的可复现性差距极大。有的论文代码开源、依赖环境写得清清楚楚,照着跑一两个小时就能出结果;有的论文只给了方法描述,数据版本、超参数、随机种子、预处理细节全都缺失,复现者只能靠猜。这种差距也让我更加理解“可复现性”在学术研究里为什么会越来越被强调。
另一个感受是:复现结果跟原文数字不完全一致,不等同于复现失败。数据版本更新、样本排除标准差异、预处理细节差异都可能造成小幅偏差。我给自己定的判断标准是:AUC偏差在0.03以内、C-index偏差在0.02以内、生存曲线的分层趋势一致,就认为核心结论可复现。学术研究追求的是结论稳健性,而不是数字齐平。
最后给想要自己复现或迁移到自有数据的朋友一点建议:先想清楚自己的数据形态再决定路线。有标注规范的影像数据,首选影像组学流程;有转录组表达谱数据,多组学模型是稳扎稳打的方向;有数字病理切片平台,可以考虑WSI深度学习。不要看到一篇顶刊的AUC很高就原样照搬,你的数据能不能支撑这条路线,才是决定成败的前提。
我个人在实际操作中最大的体会是,复现本身就属于深度学习性质的科研训练。只有把论文里每一句“简单”的方法描述翻译成代码,你才会真正理解作者为什么用这个阈值、为什么选这种划分方式、为什么在那一步做特征筛选。等你从头到尾跑完三篇,再去看任何新的预测模型论文,关注点会完全不一样:不再是“它的AUC真高”,而是“它有没有说明数据版本,特征筛选放在训练集里了吗,外部验证平台跟训练集差多远”。这个关注点转变的瞬间,就是复现带给你最大的回报。