做药物活性预测这个项目起因其实挺朴素。计算化学和机器学习结合已经不是新鲜事了但很多刚入坑的朋友拿到的教程要么只讲算法原理要么只给代码跑一遍就结束很难直接迁移到自己的分子数据集上。这次我把ERα拮抗剂活性预测和ADMET性质预测整套流程完整走了一遍用到了Python生态里最经典的那几个模型——神经网络、随机森林、SVM、KNN还有回归家族中间加了PCA做特征压缩把踩过的坑和关键参数调整都记录下来希望对正在做类似QSAR或者药物筛选的朋友有实际帮助。这个项目的价值在于两点一是ERα是乳腺癌药物研发里非常重要的靶点能在化合物合成前用模型快速评估拮抗活性能省下大量湿实验成本二是ADMET性质预测解决的是“活性有了但成药性差”的痛点把吸收、分布、代谢、排泄和毒性风险在早期就暴露出来。整套流程跑通后对一个新化合物从活性到成药性的初步评估几分钟内就能出结果。1. 项目整体思路与方案选型1.1 核心需求拆解ERα拮抗剂预测本质上是一个分子性质回归问题。我们把分子结构转化成计算机能理解的特征向量然后让模型学习“结构特征→拮抗活性”之间的映射关系。常用的活性指标是IC50值但IC50是微摩尔级别数值跨度过大直接回归效果不好所以业界普遍先做负对数转换转换成pIC50这样数值范围基本落在4到10之间更符合回归模型对目标变量分布的要求。ADMET预测则是另一套逻辑。它不是一个单一任务而是多个二分类或回归子任务的集合。比如血脑屏障穿透能力可以做成“能穿透/不能穿透”的二分类水溶性可以做成回归预测LogS值。项目标题里把这些任务放在一起表面上看起来复杂实际上核心挑战是一致的如何用有限的数据训练出泛化能力强的模型以及如何处理高维稀疏的分子特征。1.2 为什么选这五种算法先说说模型选型的思路。如果你去过Kaggle或者看过多篇QSAR论文会发现这几个算法出场率极高不是没有原因的。随机森林是集成学习的代表通过构建多棵决策树并投票或者取平均天然抗过拟合对异常值不敏感而且自带特征重要性评估。在分子描述符这种噪声较多的数据上随机森林往往比单棵决策树稳定得多。SVM在小样本高维场景下表现一直很稳尤其配合RBF核函数能把非线性关系映射到高维空间去拟合。药物筛选数据通常样本量不大几百到几千个化合物SVM正好发挥优势。KNN的思路最简单相似的结构往往有相似的活性这个假设在化学空间里很多时候是成立的。但从实操角度看KNN对特征缩放非常敏感分子描述符量纲差异很大不标准化基本没法用这也正好引出PCA。神经网络我们用的是MLP多层感知机。在小规模表格数据上只要数据量不太小、特征质量高MLP能拟合非常复杂的非线性关系。但它有个明显缺点——可解释性差参数也多调参成本高。回归模型这里主要指线性回归和岭回归作为基线模型非常重要。先用简单的线性模型跑一遍得到一个性能下限再去对比复杂模型有没有真的带来提升。如果随机森林和神经网络比岭回归没高几个点那就要反思特征工程或者数据质量的问题而不是继续往上堆模型复杂度。1.3 数据来源与格式设计数据方面ERα拮抗剂活性数据可以从ChEMBL数据库下载筛选标准是human ERα、IC50类型数据删掉重复项和异常值。ADMET数据可以从Tox21、AMES、以及一些公开的ADMET benchmark数据集获取。格式上我用的是SDF文件加CSV标签文件的结构。SDF存分子结构CSV存pIC50或者ADMET标签两边的ID一一对应。这样设计的好处是换数据集时不用改代码逻辑只要保证ID匹配就行。有一个特别容易忽略的点数据集划分必须按分子骨架或者相似性来分而不是随机分。药物活性数据里经常有大量结构类似的类似物随机划分会导致训练集和测试集高度重叠模型性能虚高。我在实操中用了基于Bemis-Murcko骨架的ScaffoldSplit效果比随机划分要严谨得多。初学者容易忽略这个问题但发论文或者实际项目评估时这个细节很关键。2. 特征工程与PCA降维实战2.1 分子描述符的计算与处理分子描述符是把化学结构转化成数值特征的核心手段。我这里用RDKit库计算了大约200个2D描述符包括分子量、LogP、氢键供体受体数量、拓扑极性表面积、可旋转键数、芳香环数等。为什么不用3D描述符因为3D描述符依赖分子构象计算成本高而且构象生成的不确定性会影响模型复现性。很多QSAR模型只用2D描述符就能达到很好的效果省时省力。拿到原始描述符后第一步是清洗。RDKit计算过程中部分分子可能返回NaN比如某些描述符对特定原子类型不支持。处理策略很简单删除缺失值占比超过5%的特征列剩余缺失值用中位数填充。这里不推荐用均值填充因为描述符分布往往是偏态的均值容易被极端值带偏中位数更稳健。第二步是去低方差特征。那些几乎所有分子取值都一样的描述符对模型区分没有贡献直接删掉。可以用方差阈值法保留方差大于0.01的特征。第三步才是标准化。注意PCA和SVM、KNN这些基于距离的算法都要求特征在同一量纲下否则量级大的特征会主导距离计算。标准化用StandardScaler就行减去均值除以标准差。2.2 PCA降维的合理参数设置主成分分析本身不复杂就是把高维相关特征通过正交变换变成一组线性无关的主成分。但在分子描述符这个场景里PCA有两个实际价值一是消除共线性分子描述符之间相关性普遍很高比如分子量和分子体积基本是正相关的二是压缩维度200多个描述符降到50个主成分能保留95%以上方差同时让SVM和KNN的距离计算更稳定。我需要特别强调一点PCA必须放在特征缩放之后而且只能基于训练集拟合然后用训练集的变换参数去转换测试集。这是数据泄漏问题里最常见的一条见过太多人先对全量数据做PCA再来划分训练测试集结果测试集信息提前进入了训练过程模型评估结果虚高得离谱。正确做法是放进sklearn的Pipeline里统一管理。主成分数量的选择我建议用累计方差贡献率来定。一般保留90%到95%的累计方差即可。比如这套数据里前48个主成分解释了95.2%的方差那我直接用48维特征训练模型。不要盲目保留太多主成分后面那些主成分解释方差极低基本是噪声加进去反而干扰模型。2.3 特征重要性与PCA的对比观察做完PCA之后我还做了一个对照实验一组直接用原始200维描述符训练另一组用PCA降维后训练。结果挺有意思。随机森林对降维不敏感甚至原始特征表现略好一点因为随机森林的特征子集选择机制本身就能处理高维冗余特征。但SVM和KNN在降维后性能明显提升尤其是在KNN上训练和推理时间也大幅下降。这说明PCA不是万能的它的价值跟模型选择强相关。树模型不需要它距离模型很需要它。如果你想节省调参时间直接做一版降维后的数据跑全部模型至少能保证距离类模型不会因为维度灾难而崩掉。3. 五种模型核心原理与关键参数3.1 随机森林在活性预测中的表现随机森林的实现细节值得展开讲。每棵树在训练时用Bootstrap抽样随机取一部分样本每个节点分裂时随机选一部分特征寻找最优分裂。这个“双随机”机制让每棵树都有差异最后取平均时方差大幅降低。实际操作里我重点调的是n_estimators和max_features。n_estimators从100开始增加观察交叉验证分数到300之后基本平台期再增加只是浪费计算时间。max_features默认是sqrt(n_features)在降维后的48维特征上大概取7这个值我试过调大调小效果都略差于默认。样本不均衡问题在ADMET分类任务里很突出比如毒性阳性样本可能只占10%。随机森林的class_weight参数设成balanced能自动调整权重让少数类被更重视。这个参数在ERα回归任务里用不上但ADMET分类必须要加。3.2 SVM的核函数选择与小样本优势SVM在这个项目里的定位是“精度担当”。药物数据样本量不大几百个样本在高维空间里线性不可分很常见RBF核函数能通过高斯映射把样本投影到无穷维空间理论上可以处理任意复杂的决策边界。但RBF核有两个关键超参数C和gamma。C控制误分类惩罚力度太大容易过拟合太小欠拟合。gamma控制径向基函数的宽度gamma越大每个样本的影响范围越小决策边界越复杂。我用了GridSearchCV网格搜索C在0.1到100范围gamma在0.001到1范围用5折交叉验证选组。一个小技巧SVM对数据尺度极敏感Pipeline里先StandardScaler再SVC这一步不做的话RBF核直接失效。另外SVM训练完成后没有天然的概率输出如果要概率值需要设置probabilityTrue但训练时间会变长这个代价在几百样本的规模下完全能接受。3.3 KNN的距离度量与标准化必要性KNN在药物筛选里的作用经常被低估。它虽然简单但在某些局部化学空间里预测效果出奇地好。核心假设是化学相似性原理——结构相似的化合物活性也相似。实际操作中K值的选择比较关键。K太小模型过度依赖最近邻噪声影响大K太大会把距离很远的样本也纳入投票整体趋于平均。我用交叉验证从K3到K15逐个测试发现K7在这个数据集上是折中点。距离度量选的是欧氏距离配合标准化后的特征。为什么不用曼哈顿距离或者余弦相似度因为标准化后欧氏距离能较好地反映高维空间中的真实差异曼哈顿距离在高维下区分度会变差。weights参数我设成了distance即距离近的样本权重更大这样比均匀权重略好。3.4 回归模型家族与基线设定回归部分我选了三种线性回归、岭回归、Lasso回归。线性回归是最朴素的基线岭回归加了L2正则化Lasso加了L1正则化后者还能做特征选择。岭回归的alpha参数控制正则化强度。alpha越大模型系数越被压缩向零方差减小但偏差增大。通过交叉验证这个数据集上alpha1.0周围表现不错。Lasso的alpha适当调大后很多特征的系数直接变成零相当于自动做了特征筛选解释性更强。这里必须强调基线模型的价值。我用岭回归跑出来的R²大概在0.61随机森林能到0.78SVM到0.75这说明复杂的非线性模型确实学到了更多结构活性关系。如果是那种所有复杂模型跟线性回归持平的项目大概率是特征工程有问题或者数据本身线性关系已经很强没必要费劲上深度学习。3.5 神经网络MLP的架构设计与防过拟合MLP的架构我设计得比较克制输入层48维PCA降维后一个隐藏层128个神经元激活函数ReLU输出层1个神经元预测pIC50。为什么只有一个隐藏层因为在小样本数据上网络太深很容易过拟合一层隐藏层加足够的神经元已经能逼近任意连续函数。训练时batch_size设为32优化器Adam学习率0.001loss用MSE。关键步骤是EarlyStopping——监控验证集loss连续20个epoch没有改善就停下并且恢复最佳权重。这套机制比固定epoch数训练好用得多能自动找到合适的训练轮数。MLP在表格数据上还有个隐藏问题特征尺度不一致时收敛很慢。虽然PCA前的标准化已经处理过但最好确认一下PCA后各主成分的值域。另外神经元数量也不是越多越好我试过256个神经元验证集分数反而略降这就是过拟合的信号。4. 实操过程与核心代码实现4.1 数据加载与划分策略整个项目的代码基于Python 3.9和scikit-learn 1.2版本。数据加载、特征计算、模型训练和评估的完整代码我整理好了这里按关键环节拆解说明。数据加载用的是pandas读取CSV分子描述符计算用RDKit。下面是核心代码片段import pandas as pd import numpy as np from rdkit import Chem from rdkit.Chem import Descriptors from rdkit.Chem import Draw df pd.read_csv(er_activity.csv) molecules [Chem.MolFromSmiles(smi) for smi in df[SMILES]] desc_names [desc[0] for desc in Descriptors._descList] desc_funcs [desc[1] for desc in Descriptors._descList] desc_data [] valid_idx [] for i, mol in enumerate(molecules): if mol is None: continue try: desc_values [func(mol) for func in desc_funcs] desc_data.append(desc_values) valid_idx.append(i) except: continue df_desc pd.DataFrame(desc_data, columnsdesc_names) df_desc[pIC50] df.iloc[valid_idx][pIC50].values这里有个坑RDKit的计算有些函数会对特定分子抛异常比如含金属原子的配体。所以必须用try-except包裹把计算失败的分子剔除掉否则整个流程会中断。数据划分用ScaffoldSplit按骨架划分保证训练集和测试集的结构差异性from sklearn.model_selection import train_test_split from rdkit.Chem.Scaffolds import MurckoScaffold scaffolds [] for smi in df_desc[SMILES]: mol Chem.MolFromSmiles(smi) scaffold MurckoScaffold.MurckoScaffoldSmiles(molmol) scaffolds.append(scaffold) from sklearn.model_selection import GroupShuffleSplit gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, test_idx next(gss.split(df_desc, groupsscaffolds)) train_df df_desc.iloc[train_idx] test_df df_desc.iloc[test_idx]ScaffoldSplit做的是按骨架分组后分组切分确保同一骨架的分子不会同时出现在训练集和测试集里。这在评估模型对新化学骨架的泛化能力时比随机划分靠谱得多。4.2 PCA与Pipeline的整合特征缩放和PCA放进Pipeline里管理这一步的核心逻辑是确保交叉验证时每个fold都独立做缩放和降维绝不使用测试集信息。from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA from sklearn.ensemble import RandomForestRegressor from sklearn.svm import SVR from sklearn.neighbors import KNeighborsRegressor from sklearn.linear_model import Ridge from sklearn.neural_network import MLPRegressor from sklearn.metrics import r2_score, mean_absolute_error pipeline_svm Pipeline([ (scaler, StandardScaler()), (pca, PCA(n_components0.95)), (svm, SVR(kernelrbf, C10, gamma0.01, probabilityTrue)) ])n_components0.95这种写法是让PCA自动保留95%的累计方差比写死一个整数更灵活。如果数据集换了一批分子描述符的方差分布会变固定写48反而可能不合适。4.3 模型训练与交叉验证评估训练和评估我统一了流程每个模型跑5折交叉验证用R²和MAE做评估指标。在回归任务里R²衡量模型解释的方差比例MAE则给出预测值跟真实值之间的平均绝对误差两者结合看比较全面。from sklearn.model_selection import cross_validate scoring {r2: r2, neg_mae: neg_mean_absolute_error} results {} models { Ridge: pipeline_ridge, RandomForest: pipeline_rf, SVM: pipeline_svm, KNN: pipeline_knn, MLP: pipeline_mlp } for name, model in models.items(): cv_results cross_validate(model, train_df.drop([SMILES, pIC50], axis1), train_df[pIC50], cv5, scoringscoring) results[name] { R2_mean: cv_results[test_r2].mean(), R2_std: cv_results[test_r2].std(), MAE_mean: -cv_results[test_neg_mae].mean() }交叉验证的5个fold里每个fold的R²可能相差挺大这在小样本数据里太正常了。所以看结果时不能只看均值还得看标准差。如果标准差特别大说明模型对数据划分很敏感需要检查是否存在某些骨架的分子特别难预测。最后在独立测试集上做一次最终评估。测试集不参与任何训练和调参这一步得到的R²才是真正能对外汇报的泛化性能。final_scores {} for name, model in models.items(): model.fit(train_df.drop([SMILES, pIC50], axis1), train_df[pIC50]) pred model.predict(test_df.drop([SMILES, pIC50], axis1)) final_scores[name] { R2: r2_score(test_df[pIC50], pred), MAE: mean_absolute_error(test_df[pIC50], pred) }4.4 ADMET多任务预测的特殊处理ADMET预测跟ERα回归不太一样的地方在于它通常是分类任务。以血脑屏障穿透预测为例标签只有0和1。此时评估指标从R²换成AUC和准确率。分类模型的训练代码跟回归高度相似只是把SVR换成SVCMLPRegressor换成MLPClassifier评估函数换成roc_auc_score。有一个小细节ADMET数据通常正负样本不均衡单纯用准确率容易骗人——比如90%的样本是负类模型全部预测为负类也能拿到90%准确率。所以必须用AUC作为主指标。对于多个ADMET端点我建议分别训练模型而不是合并成一个多标签模型。因为不同端点的最优特征和模型可能不同合并后反而互相干扰。实际操作时写一个循环每个端点单独训练和评估最后汇总成一张性能表。5. 常见问题与排查技巧实录5.1 描述符计算报错与缺失值处理跑RDKit描述符时最容易遇到的是部分分子返回NaN。这可能是分子包含特殊原子类型或者某个描述符定义里出现了除以零的情况。处理逻辑分两步先删列再填行。具体来说如果某列缺失值占比超过5%整列删除因为缺失太多说明这个描述符对很多分子不适用保留反而引入噪声。剩余列的缺失值用中位数填充注意填充必须只基于训练集统计量再应用到测试集否则又是数据泄漏。还有一类问题是SMILES格式本身不合法。RDKit的MolFromSmiles解析不出来会返回None这种情况直接剔掉该分子。如果剔除的比例超过10%那要回头检查SMILES的来源是否可靠。5.2 数据泄漏的典型场景与规避数据泄漏是分子建模里最常见又最难察觉的问题。我总结出三个高发场景。第一个是特征缩放发生在划分之前相当于用全量数据的均值和标准差来缩放训练集测试集的信息其实参与了缩放过程。解决方法是把缩放器放进Pipeline里。第二个是PCA拟合在全量数据上。PCA的本质是寻找方差最大的方向全量数据算出来的主成分方向已经包含了测试集的信息。这个问题的严重性比特征缩放更大因为PCA是线性变换测试集的投影方向被训练过程偷看了。第三个是相同分子出现在训练集和测试集里。很多公开数据集本身可能含有重复结构不洗掉直接用随机划分同一分子的不同条目会同时出现在两边模型等于记住答案。解决方法是先做去重再用骨架划分。5.3 过拟合信号与应对策略小样本建模里过拟合是头号敌人。典型信号是训练集R²接近1而测试集R²掉到0.5以下中间gap越大越危险。应对策略按优先级排序先加正则化参数岭回归的alpha、SVM的C值调低、神经网络的早停和Dropout再做特征筛选或PCA降维减少冗余输入最后考虑数据增强对分子结构做微小的合理扰动比如添加或删除一个甲基。还有一种情况是数据量太少模型本身没有任何办法泛化。这种情况建议降低模型复杂度直接用线性模型或者KNN而不是执着于随机森林和神经网络。5.4 模型性能瓶颈的判断思路当所有模型的性能都上不去时大多数人第一反应是换更复杂的模型但我觉得应该先检查数据和标签。检查标签分布。如果pIC50值集中在很窄的区间里比如6到7之间那么不管什么模型都很难得到高R²。因为目标的方差本身就小模型提高R²的空间有限。这时候与其换模型不如检查生物活性数据的质量或者扩大数据范围涵盖更多活性梯度。检查特征信息量。如果只用2D描述符等于丢弃了立体化学信息。对某些靶点手性对活性影响巨大这时候考虑加入手性描述符或者3D描述符可能更有效。检查相似性假设是否成立。如果同一个靶点的活性数据来自多种不同实验体系比如不同实验室的测定条件差异数据的噪声会非常大单靠模型无法消化。这种情况需要做数据清洗或者加一个实验来源作为辅助分类特征。回头看看这套流程我对“先有数据质量后有模型性能”这句话的体会又深了一层。模型选型固然重要但在药物数据这种小样本高噪声的场景里特征工程的严谨程度和数据划分的合理性往往比模型复杂度的提升带来更多收益。PCA和标准化放在Pipeline里管理骨架划分避免信息泄漏早期用岭回归建立基线这几步看着不起眼但决定了整个项目的可信度。如果有人上手做类似任务我建议先跑通这条经典路径拿到靠谱的基线再去尝试图神经网络或者注意力机制否则很容易被数据里的假象带偏。