第4章 模型算法选择:机器学习、深度学习、集成方法

选模型算法这事,我当年刚入行时也纠结过。你想想看,ADMET预测任务千差万别,有的数据量就几百个分子,有的却几十万。用同一个算法包打天下?不现实。

这一章,我把实际项目里的选型经验摊开来讲。咱们不聊虚的,直接看什么场景该用机器学习,什么时候上深度学习,什么时候用集成方法能救命。

4.1 三种算法家族的核心差异

先给个总览表。我习惯在项目启动前,拿这张表跟团队对齐一下——省得后面争论「为啥不用BERT」。

维度 传统机器学习 深度学习 集成方法
数据量需求 几百~几千条 万级以上 千级即可
特征工程依赖 高,需要手工指纹/描述符 低,自动提取 中,需要基础特征
训练时间 分钟级 小时~天级 分钟级
可解释性 好(特征重要性) 差(黑盒) 中等(可以看基学习器权重)
过拟合风险 低(正则化成熟) 高(需dropout/早停) 低(多模型投票可降低方差)
典型算法 RF, GBM, XGBoost, SVM GNN, Transformer, CNN Stacking, Bagging, Blending

我的经验:刚开始做hERG预测时,只有800个阳性数据,硬上GNN,结果验证集AUC只有0.56。换成XGBoost+ECFP4指纹,AUC直接到0.82。数据量少的时候,别跟风深度学习。

4.2 传统机器学习:小数据场景的定海神针

传统机器学习在ADMET领域仍然是主力。为什么?因为大部分ADMET实验数据都很贵、很少。你想想看,一个CYP抑制实验,测几十个化合物就要好几万经费,哪来的十万级数据。

我建议:数据量<3000时,优先试随机森林或XGBoost。这两个模型对异常值不敏感,而且有内置的特征重要性,方便你回溯哪些分子片段在驱动预测。

避坑指南:我曾经把SVM加到ADMET管道里,没调C和gamma,结果预测全变成同一类。后来用网格搜索+5折交叉验证才老实。记住:SVM对缩放非常敏感,一定要先标准化。

代码片段:我常用的XGBoost基线配置,直接用eta=0.1, max_depth=6起步,然后调subsample防过拟合:

import xgboost as xgb

model = xgb.XGBClassifier(
    n_estimators=500,
    max_depth=6,
    learning_rate=0.1,
    subsample=0.8,
    colsample_bytree=0.8,
    reg_lambda=1.0,
    eval_metric='auc',
    early_stopping_rounds=50
)
model.fit(X_train, y_train, eval_set=[(X_val, y_val)], verbose=False)

注意:别一股脑把全部特征扔进去。我曾经把2048位MACCS指纹全塞进去,结果模型在验证集上表现不错,但内部测试时发现大量特征与任务无关,导致过拟合。用SelectFromModel做一轮特征筛选,通常能把维度砍掉60%。

4.3 深度学习:数据充足时的天花板突破

当你有1万以上标注数据,或者想利用分子图结构时,深度学习才能发挥价值。我个人的一个判断标准:如果数据量能让模型收敛后验证集还在持续下降,就可以上深层网络。

常见做法:

  • 分子图神经网络(GNN):把原子和键作为图节点和边,自动学习拓扑特征。适合CYP、hERG等结构敏感任务。
  • Transformer:把SMILES当成序列处理。我在一个Caco-2渗透性项目里用过,效果比指纹方法好5%,但训练时间多了20倍。
  • CNN on 2D分子图像:把分子画成图片,用卷积网络。这个方法可解释性好,但图像分辨率很影响结果。

避坑指南:我曾经在GPU上跑GNN,batch size设了512,结果显存爆炸。更坑的是,改了batch size后发现BN层参数不收敛。后来我改用LayerNorm+gradient accumulation才稳住。新手建议batch size从32开始。

简易的PyTorch Geometric网络示例:

import torch.nn as nn
from torch_geometric.nn import GCNConv, global_mean_pool

class GNNModel(nn.Module):
    def __init__(self, node_dim, hidden_dim, num_classes):
        super().__init__()
        self.conv1 = GCNConv(node_dim, hidden_dim)
        self.conv2 = GCNConv(hidden_dim, hidden_dim)
        self.fc = nn.Linear(hidden_dim, num_classes)
        self.dropout = nn.Dropout(0.3)

    def forward(self, x, edge_index, batch):
        x = self.conv1(x, edge_index).relu()
        x = self.dropout(x)
        x = self.conv2(x, edge_index).relu()
        x = global_mean_pool(x, batch)
        return self.fc(x)

关键点:深度的代价是调参更繁琐。学习率从1e-3开始,用余弦退火调度。遇到过拟合就加dropout和weight decay。如果验证集loss震荡,试试梯度裁剪:clip_grad_norm_(model.parameters(), 1.0)。

4.4 集成方法:取众家之长

集成方法是ADMET项目中的「作弊器」。原理很简单——多个不同模型投票,总比单个模型稳定。我个人习惯把集成方法放在调参的最后一环,作为精度的保险栓。

三种常见策略:

  • Bagging:随机森林是典型代表。对噪声数据很鲁棒,适合公共数据集标注不一致的场景。
  • Boosting:XGBoost/LightGBM/CatBoost。序列化训练,不断修正前一个模型的错误。我用CatBoost处理过含缺失值的ADMET数据集——它原生支持缺省值,不用额外填充。
  • Stacking:用多个基础模型(比如RF+SVM+GNN)的输出作为新特征,再训练一个元模型。上限高,但容易过拟合。我建议用5折交叉验证来生成元特征,而不是直接用训练集预测。

我曾经踩过的坑:在Stacking中用了太强的元模型(比如XGBoost的深度设为10),结果元模型完美记住了基础模型的输出,在测试集上直接崩溃。正确做法:元模型用简单逻辑回归或浅层决策树,尽量减小方差。

一个简单的Stacking实现思路:

from sklearn.ensemble import StackingClassifier
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.svm import SVC

base_models = [
    ('rf', RandomForestClassifier(n_estimators=200)),
    ('gbm', GradientBoostingClassifier(n_estimators=200)),
    ('svm', SVC(probability=True, kernel='rbf'))
]
meta_model = LogisticRegression()
stack = StackingClassifier(estimators=base_models, final_estimator=meta_model, cv=5)
stack.fit(X_train, y_train)

4.5 如何做最终决策?

每次项目结尾,我都会让团队回答三个问题:

  1. 数据量够不够? 少于1万就排除深度学习主方案。
  2. 解释性要不要? 药化同事需要知道为什么预测为阳性,那就别用深度黑盒,留RF特征重要性图给他们。
  3. 上线环境有GPU吗? 没有GPU就别想着Transformer了,老老实实XGBoost+集成。

下面这张图,是我在内部培训时一直在用的算法选择流程图,拿去做参考:

ADMET模型算法选择流程 开始 数据量 ≥ 10000 ? 否 是 传统ML 指纹/描述符 RF/GBM/SVM 深度学习 GNN/Transformer/CNN BD/BatchNorm/Dropout 集成方法提升? 选定最终模型 提示:优先试Bagging/Boosting Stacking需慎防过拟合

4.6 一些实战建议

我的最终忠告:别在模型选择上花超过20%的时间。我见过太多团队花两个月比较10种算法,最后发现数据清洗才是瓶颈。先拿一个简单模型(比如RF)跑出基线,再逐步迭代。而且,记得留一个规则/基线模型做对照——有时候最基础的方法就够用了。

嗯,选完算法之后,下一步就是考虑怎么验证了。这部分我们下一个章节再细聊。