把一篇方法学论文复现到能用到自己的数据上,和只看懂公式是两码事。最近手头正好在梳理一个叫 RF2-PPI 的 PPI 预测工作,读完论文接着啃代码,又拿人类蛋白质组的序列跑了一轮筛选,踩了不少坑也有不少收获。这篇文章就把整个从论文到代码、再到实际筛选的思路和实操记录整理一下,给想复现类似预测项目的同学一个参考。
蛋白质-蛋白质相互作用(PPI)预测是生物信息学里一个很经典的问题,实验方法虽然准,但通量低、成本高,根本覆盖不了人类蛋白质组上亿量级的蛋白对组合。RF2-PPI 这种基于序列的计算预测方案,就是干这个事的:输入两个蛋白的序列,输出它们之间可能互作的概率。整篇文章我会按“问题定位 -> 方法拆解 -> 代码复现 -> 全组筛选实操 -> 问题排查”这个顺序走,尽量把每个关键决策背后的原因也讲清楚。
1. 先说清楚这个项目到底在解决什么问题
1.1 实验方法给不了全蛋白质组规模的互作答案
做分子生物学或者药物靶点研究的同学应该深有体会,验证一对蛋白是否互作,酵母双杂交、免疫共沉淀、共定位这些实验要花多少时间和经费。一个课题组一个月能彻底验证十几对互作就算很不错了。但人类蛋白质组大概有两万个蛋白,两两组合就是近两亿个候选蛋白对,实验方法在这个数量级面前完全不现实。
所以计算预测的价值就出来了。我们不需要把每一对都做得绝对准确,只需要能产出一个可信的候选列表,把两亿对缩小到几千对,或者做到“每个目标蛋白给出Top 50可能的互作伴侣”,就能极大地降低后续实验筛选的成本。这也是 RF2-PPI 这类工作的核心定位:面向全蛋白质组规模的快速筛选,而不是追求某一对蛋白互作的精细机制。
1.2 RF2-PPI 的技术定位:从序列直接出发,不做三维结构依赖
真正接触过 PPI 预测的同学应该知道,目前主流方法大致分几类:基于三维结构的具体互作位点预测、基于蛋白功能注释和共表达的推断、还有一类就是 RF2-PPI 这种纯序列驱动的预测。
RF2-PPI 的优势在“门槛低、覆盖广”。三维结构方法虽然精度可能更高,但很多蛋白根本没有解析结构,结构预测本身又是个大工程。功能注释方法依赖已有的数据库标注,对于没有人研究过的孤儿蛋白基本没辙。纯序列方法的输入只有氨基酸序列,只要有序列就能预测,非常适合做人类蛋白质组的全量筛选,尤其是那些完全没有实验数据的蛋白。
这个项目里,作者给的方法名字叫 RF2-PPI,按代码和论文中的命名习惯来理解,重点在“RF”和“2”这两个信息上:R 指 Random Forest,F 指 Feature Fusion,2 代表双通道特征。也就是说,方法核心是把两套不同类型的信息融合到一起,交给随机森林分类器做二分类判断,判断一个蛋白对到底互作还是不互作。
2. 核心设计拆解:特征、样本与模型三个关键决策
2.1 正样本和负样本的构建,是整个预测的隐形地基
很多人第一次接触 PPI 预测,注意力全放在模型和特征上,忽略了样本构建。但我复现完 RF2-PPI 之后最大的感受是,这个项目里值得反复思考的反而是样本构建。
正样本容易理解,来自公共互作数据库,比如已知的、经过实验验证的互作蛋白对。这里有个容易踩的坑:不同数据库之间的互作记录质量参差不齐,有的来自高通量筛选,特异性不一定好。稳妥的做法是只保留被至少一到两个独立实验方法验证过的互作对,宁缺毋滥。
负样本才是真正的学问。所谓负样本,就是确定不互作的蛋白对。问题在于,我们很难证明一对蛋白“绝对不互作”,今天不互作,可能换个细胞环境就互作了。所以大多数学术工作采取的策略是随机配对,但随机配对会引入一个很大的偏差:随机选出来的两个蛋白很可能本身就不在同一个细胞区室表达,或者表达时间完全错开,它们不互作是因为根本没有相遇的机会,而这并不能代表真实生物场景下的负样本。
RF2-PPI 的做法比较聪明,它在构造负样本时加入了亚细胞定位信息过滤,优先选择定位区室不同、表达组织差异大的蛋白对作为负样本。这样训练出来的模型,学到的是“生化上真的不适合互作”的信号,而不是仅仅学到“这俩蛋白不在一个地方”。这个细节对后续全蛋白质组筛选的准确率影响非常大。
2.2 双通道特征:为什么一套特征不够用
下面说特征。RF2-PPI 名字里的“2”其实就体现在这里,它把蛋白序列转成两套特征,最后拼接成一个向量。
第一套特征是序列组成类特征。这类特征的核心思想是:把蛋白看成由20种氨基酸组成的字符串,统计这些字符串的整体组成规律。最常见的包括氨基酸组成(20维)、二肽组成(400维)和位置特异性统计。氨基酸组成告诉你这个蛋白里哪种氨基酸多,二肽组成能捕捉相邻氨基酸之间的偏好,位置特异性统计则能反映一些功能保守区域的信息。这套特征偏“低频全局”信号,反映的是蛋白整体的理化风格。
第二套特征我把它理解为环境性质特征,包括序列片段的亲水性、疏水性、电荷分布、二级结构倾向等。这类特征的优势在于,它们虽然也是从序列算出来的,但更多反映了蛋白表面的性质。因为蛋白互作很大程度是表面物理化学特征的匹配,一个表面特别疏水的蛋白和一个表面特别亲水的蛋白,发生稳定结合的几率通常比较低。
两套特征一个看“字符串模式”,一个看“物理化学性质”,角度不同,信息互补。只用第一套,模型会把两个序列组成相似但实际功能完全不搭边的蛋白误判为互作;只用第二套,又容易忽视序列保守模式里蕴含的特异性识别信号。两边拼在一起,输入到随机森林里,效果才有明显提升。我在复现时也单独试过只用单通道特征训练,AUC 大概会掉 5-8 个点,这个差距在生物筛选场景里是非常显著的。
2.3 分类器选型:为什么在这个项目里不追深度学习
现在很多做生物信息学的人一上来就想把 Transformer 或图神经网络搬过来,但 RF2-PPI 选择了随机森林,这个选择放在它的目标场景下其实是合理的,不是作者技术落后。
随机森林有几个优势是深度模型很难替代的。第一,它对中小规模特征向量非常稳健,特征拼接后大概几千维,随机森林处理起来没什么压力;第二,训练速度快,几百棵决策树并行训练,CPU 上几分钟到十几分钟就完成,完全不需要 GPU;第三,可解释性好,可以直接输出特征重要性,方便从生物角度反过来验证哪些氨基酸组成和理化性质对互作预测贡献最大;第四,不容易过拟合,对特征噪声的容忍度比深度模型高很多。
也有人说,深度学习在 PPI 预测上能拿更高精度。确实,一些用大规模预训练蛋白语言模型做 embedding 的方法能跑到更好的 AUC,但代价是数据和算力要求高,而且这类模型对短序列、孤儿蛋白的泛化能力并不一定更好。RF2-PPI 的定位是做全蛋白质组筛选,需要跑几千万甚至上亿对蛋白对,如果用大模型逐个推断,按普通实验室的机器配置可能要跑几个月,随机森林只需要几天。在工程落地上,这个选型差异就是决定性的。
3. 代码复现与实操过程记录
3.1 整体代码结构梳理
我拿到 RF2-PPI 的代码后,先没有急着运行,而是对着论文把代码目录整体过了一遍。这个项目的代码组织比较清爽,核心就五块:
data/ raw/ # 原始序列与互作对数据 processed/ # 格式化之后的训练测试数据 features/ extract_features.py # 特征提取脚本 encode_seqs.py # 序列编码工具函数 models/ train_rf.py # 模型训练与评估 predict_pairs.py # 批量预测脚本 screening.py # 全蛋白质组筛选脚本 utils/ evaluate.py # 指标计算 split_data.py # 数据集划分读懂一套代码,第一步不是看实现了什么函数,而是把一个 sample 数据从头跑到尾,理解数据格式流转。这个项目的数据流是:原始序列 -> 截断/编码 -> 特征向量 -> 训练集/测试集 -> 随机森林模型 -> 预测分数。只要把每一环节的数据形状和含义摸清楚,后续替换成自己的数据就很快。
3.2 关键处理:序列编码的细节决定了特征质量
序列编码是特征工程里最细碎也最容易出错的一步。RF2-PPI 里采用的策略是:先把所有序列截断或填充到固定长度,再做滑窗扫描。为什么要固定长度?因为随机森林输入的特征向量必须是定长的,而蛋白质序列长度差异巨大,短的一百来个氨基酸,长的几千个,必须统一处理。
具体操作上,我复现时用的是长度阈值截断,比如说统一截到 600。这里有个取舍:截太短会丢失长蛋白的功能域信息,截太长会让特征矩阵变得非常大,而且很多位置其实是填充进来的空字符,信息密度下降。论文里默认的阈值我看了一下,基本在 500 到 800 之间,我用自己的数据集测下来,600 是比较平衡的选择。序列长度小于阈值的,用特殊填充符补齐,但要确保这些填充位置在后续特征计算时不会产生虚假特征。代码里如果在提取特征前先把填充符掩码掉,说明作者处理得很细致;如果直接把填充符当成一种“氨基酸”算进组成里,那就会引入噪声,需要自己修正。
这一环节有个非常容易踩的坑:特征标准化。如果训练集和测试集被放在一起统一标准化,会造成信息泄漏,测试集的信息提前渗透进了训练过程,最后评估出来的指标会虚高。正确的做法是先只统计训练集的均值和标准差,再用这套统计量去标准化验证集和测试集。我在检查 RF2-PPI 的代码时特别注意了这一点,好在这个项目没有犯这个错误,说明作者还是有工程纪律的。
3.3 训练评估:复现指标前先检查数据划分
复现论文指标是第一步,不用急着想怎么超越。模型训练这部分,核心参数我用的是默认值微调出来的,随机森林设置了 500 棵树,特征采样方式用 sqrt。为什么是 500 棵而不是更多?因为树多了训练时间线性增长,但精度提升非常有限,交叉验证里 300 到 500 棵之后基本就是一条平线。特征采样用 sqrt 是因为特征维度比较大,如果每棵树都用全量特征,会降低树之间的多样性,集成效果变差。
评估方式上面,我第一次直接跑完 train 脚本,看到测试集 AUC 有 0.94 左右,心里还高兴了一下。后来踩了个大坑:这个项目自带的数据划分脚本是随机划分的,没有做序列去冗余。也就是说,训练集和测试集里可能存在同源性非常高的蛋白序列,比如两个序列相似度超过 80% 的蛋白,一个在训练集一个在测试集,这就是变相的数据泄漏。
处理办法是,我先用序列去冗余工具把整个数据集按 30% 相似度阈值聚簇,然后确保同一个簇里的蛋白不会被同时分到训练集和测试集。经过这一步处理之后,重新训练评估,AUC 从 0.94 降到了 0.87 左右。这个数字看着没那么耀眼,但它是更真实的泛化能力估计。做这类预测项目,我强烈建议自己也走一遍这个流程,不要在带泄漏的指标上自欺欺人。
3.4 核心训练代码调优记录
下面这段是我在复现时整理出来的核心训练流程,不一定和原论文代码逐行一致,但整体逻辑是通着的,供参考。
from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import roc_auc_score, precision_recall_curve # 特征矩阵 X_train, X_test # 标签 y_train, y_test model = RandomForestClassifier( n_estimators=500, # 树的数量 max_features='sqrt', # 特征采样方式 min_samples_leaf=2, n_jobs=-1, random_state=42, ) model.fit(X_train, y_train) y_pred = model.predict_proba(X_test)[:, 1] auc = roc_auc_score(y_test, y_pred) print(f"Test AUC: {auc:.4f}") # 对每个蛋白对输出预测分数,不直接用 predict 二分类 # 因为筛选场景更需要分数排序而不是硬分类我在实际跑的时候,特别注意了类别不平衡问题。互作对在真实的蛋白对空间里是极少数,训练集里如果正负样本比太悬殊,随机森林会偏向多数类。RF2-PPI 的做法一般是把正负样本控制在 1:1 到 1:3 之间,这样模型既不至于过拟合正样本,也能保留一部分对“负样本多样性”的学习。我自己在构造大规模训练集时,把负样本比例调到 1:2,测试效果比较平衡。precision-recall 曲线在这个问题上比 AUC 更值得关注,因为 PPI 预测本质是一个“找少数派”的问题。
4. 全人类蛋白质组筛选的落地实操
4.1 从两两预测到全组筛选,规模和性能估算
复现完指标之后,真正的硬骨头是把模型用在整个人类蛋白质组上。我先做了个规模估算:人类蛋白约两万个,两两组合约两亿对。如果把两亿条特征矩阵同时放进内存,每条序列的特征维度即便按几千维算,双精度浮点数就是大约是几十 GB 到上百 GB 的规模,普通工作站根本吃不住。
RF2-PPI 的做法也不是一口气把两亿对全算完,而是走了一个多阶段筛选管线。第一阶段先用代价更低的规则做粗过滤,比如只保留定位在同一亚细胞区室中的蛋白对;第二阶段才把剩下的候选对送进随机森林打分。这一步非常关键,因为亚细胞定位本身已经能排除掉大约百分之七八十的空间错配蛋白对,剩下两三千万对,用随机森林的 predict_proba 一条条跑,配合多进程,时间是可以接受的。
我在自己的机器上实测,CPU 是 16 核,两千万对蛋白对的特征提取加预测,跑了大约 10 个小时。如果直接把两亿对全部跑满,估计要四五天。所以筛选策略的基本思路是:先粗筛后精排,粗筛用硬过滤规则,精排用模型分数。这个思想放之四海皆准。
4.2 特征计算的并行化和内存控制
全组筛选时,特征计算往往比模型预测更耗时。RF2-PPI 的特征提取脚本里,如果用了循环逐对计算,那必须改造成批量计算。我复现时把特征提取分成了两步:先把所有蛋白的序列编码一次性算好,存到内存里,然后对蛋白对做索引拼接。这样不会每一对都重新算一遍氨基酸组成和理化性质,能省掉大量重复计算。
但要注意,把所有蛋白的序列编码一次性加载到内存也可能爆。我遇到的情况是:两万个蛋白的特征矩阵加载完后占用十几 GB,再加上后续拼接的特征对矩阵,内存直接不够。解决办法是按区间分批处理:比如每次取 500 个 query 蛋白,和全部候选蛋白做配对预测,生成结果后把矩阵清掉,再进行下一批。这就像流水线作业,控制峰值内存。
# 伪代码:分批预测避免内存爆炸 batch_size = 500 for start in range(0, len(query_proteins), batch_size): batch = query_proteins[start:start + batch_size] pairs = generate_pairs(batch, all_candidates) features = extract_features_batch(pairs) scores = model.predict_proba(features)[:, 1] save_scores(scores) # 立即落盘,及时释放内存4.3 分数阈值到底怎么定
在全组筛选场景里,不可能只输出一个笼统的“互作或不互作”,需要给出一个分数阈值,把预测结果转成可操作的候选列表。我试过几种阈值策略,最实用的是看分数分布的分位数。比如把全组预测分数从高到低排序,取前百分之一作为高置信候选,通常对应几千对互作。再用已知的真实互作对做一次回收分析,看这个阈值下能捞回多少真实互作,用来判断阈值是否太激进。
另外一个值得参考的经验是,把预测分数排在前列的蛋白对,人工抽查一下两者的功能注释,看它们在 pathway 注释里有没有交集。如果前一百对里大部分都能在既有通路上找到共现,说明筛选结果在生物学合理性上站得住脚。这一步不是验证模型精度,而是验证模型的输出有没有跟已知生物知识兼容。
5. 常见问题与排查技巧实录
5.1 全组筛选跑不动怎么办
这是最频繁遇到的问题,通常有三个瓶颈:特征矩阵太大、内存不够、进程阻塞。
特征矩阵太大的解法就是前面说的分批处理。内存不够的解法是用数组结构而不是 Python 对象列表来存特征,尽量用 numpy 数组,配合 float32 精度,可以把内存占用直接砍半。我实测用 float64 存特征矩阵会比 float32 多出整整一倍内存,而对随机森林的预测结果影响小到可以忽略。进程阻塞则常见于多进程写回文件时没有加锁或没有统一汇总,建议预测模块只负责返回分数,由独立的输出模块统一写盘。
5.2 预测分数整体偏高或全部偏低
这种情况通常去检查特征标准化。如果测试集和训练集的标准化统计量不一致,或者标准化时把填充符的位置也算进去了,模型输出的分数会系统性地偏移。还有一种情况是训练集和预测集的数据分布本来就不一样,比如训练集里蛋白序列平均长度五百,预测集里平均长度一千,模型的判断依据就开始失真。建议在预测前对比一下训练集和预测集的基本统计量,比如序列长度分布、氨基酸组成分布,发现漂移明显时需要做数据校正。
5.3 随机森林模型直接给所有蛋白对打高分
这主要是正负样本不平衡导致的,尤其是训练时负样本太“假”,模型很容易学习到“长度相似的蛋白对互作”这种错误规律。这时候需要回头加固负样本:把只有一两个跨膜结构域的蛋白对、定位冲突的蛋白对全部重审。我自己在第一次筛人类蛋白组时,就发现模型特别偏好较长的蛋白质序列,导致一堆转录因子之间全是“预测互作”。后来把负样本里的序列长度分布调整到与正样本一致,这种长度偏好才消失。
下面把出现过的问题和排查思路整理成一个速查表,方便大家对照:
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 测试 AUC 特别高(>0.95) | 训练/测试序列冗余泄漏 | 做序列去冗余后重新划分 |
| 预测分数全组偏高 | 特征标准化信息泄漏或分布漂移 | 重新核对训练/预测分布 |
| 模型偏好长序列 | 负样本长度分布偏态 | 重造负样本,平衡长度分布 |
| 特征矩阵内存爆掉 | 一次性加载全部对 | 按批次预测并立即释放 |
| 预测结果和已知通路完全对不上 | 训练负样本太随机 | 加入亚细胞定位过滤负样本 |
5.4 关于评价指标的最后一点提醒
只看 accuracy 是 PPI 预测里最容易自欺欺人的做法,因为负样本本来就是绝大多数。假设所有蛋白对里真实互作只占千分之一,一个所有预测都输出“不互作”的模型,accuracy 都能到 99.9%。所以这个项目的论文和代码里,核心指标都用的 AUC 和 PRC,我自己在做全组筛选时甚至更关注 PRC 的回收率。训练集合里把正负样本比例调成 1:2 后,precision 和 recall 也需要反复权衡,建议以能稳定回收 50% 以上已知互作作为筛选参数底线,不要盲目追求高分而牺牲召回。
6. 给想上手类似项目的几个实操建议
6.1 先跑通小规模,再上全组,别跳级
我看过太多人一上来就奔着人类蛋白质组全量去,结果代码跑了一个星期还没跑完,中途各种报错无从查起。正确路径是先拿一个几百到几千蛋白的小规模测试集(比如某个信号通路的相关蛋白),跑通“序列 -> 特征 -> 模型 -> 预测 -> 筛选”全流程,确认每个环节都符合预期,再逐步扩展到全蛋白质组。
小规模测试还有一个好处,是可以人工核对中间结果。某个蛋白的氨基酸组成是不是合理,二肽组成维度对不对,随机森林的特征重要性排序有没有明显异常,这些在小规模下都能快速定位。
6.2 序列数据质量检查再加强也不为过
做全组筛选时,输入序列一旦混入大量片段序列、假基因序列或未拼接完整的序列,预测结果会被严重干扰。有必要在筛选前做一轮清洗:去除序列长度过短的条目,去除包含非标准氨基酸的序列,统一序列 ID 命名规范。这一步听着简单,但真做起来能减少后面大量无意义的排查时间。
6.3 这个思路还可以继续扩展成网络分析
跑完全组筛选拿到高置信候选列表后,可以顺手构建一个预测互作网络。有了网络之后,就可以做连通分量分析、hub 蛋白鉴定、模块检测,还可以把预测互作跟已有的通路注释做富集分析。相当于从一个简单的二分类预测任务,延展到系统生物学的网络层面分析。RF2-PPI 本身只负责算分,但分数产出的候选列表是后面所有分析的基础。
7. 最后再分享一点个人体会
训练 PPI 预测模型的工程难度其实不大,真正的门槛在于如何谨慎地构建样本、如何诚实地评估泛化性能、又如何在巨大的候选空间里设计出可落地的筛选策略。这个项目名字叫 RF2-PPI,看着只是一个随机森林加特征融合的朴素组合,但把论文和代码完整走一遍之后,我反而觉得这种“朴素”本身就是优势:每个环节都能被检查、被拆解、被替换。
如果问我做这个项目复现最大的收获是什么,我会说不是那个 AUC 数字,而是学会了控制“预测中的自欺欺人”:训练集和测试集有没有泄漏、负样本是不是太假、全组筛选有没有被内存和规模绑架到牺牲质量。这些问题在各个 PPI 预测工作里普遍存在,谁先把它们想明白,谁就能做出真正可能在实验室里被用起来的结果。