第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万就排除深度学习主方案。
- 解释性要不要? 药化同事需要知道为什么预测为阳性,那就别用深度黑盒,留RF特征重要性图给他们。
- 上线环境有GPU吗? 没有GPU就别想着Transformer了,老老实实XGBoost+集成。
下面这张图,是我在内部培训时一直在用的算法选择流程图,拿去做参考:
4.6 一些实战建议
我的最终忠告:别在模型选择上花超过20%的时间。我见过太多团队花两个月比较10种算法,最后发现数据清洗才是瓶颈。先拿一个简单模型(比如RF)跑出基线,再逐步迭代。而且,记得留一个规则/基线模型做对照——有时候最基础的方法就够用了。
嗯,选完算法之后,下一步就是考虑怎么验证了。这部分我们下一个章节再细聊。