第二章 QSAR建模流程:从数据收集到模型部署的完整Pipeline
很多人刚开始接触QSAR,上来就急着调模型、跑代码。我见过不少同行数据还没洗干净,就把模型跑出来了,结果R²看着挺高,一交叉验证就崩掉。说白了,QSAR建模最关键的,不是你会用哪个算法,而是你有没有一套可靠的流程。
这套流程,我把它拆成6个大环节。每个环节都有坑,我一个个讲清楚。
💡 核心观点:QSAR建模的本质是“数据驱动”,数据质量决定了模型上限。算法只是逼近这个上限的工具。
2.1 数据收集:源头决定一切
数据收集,看着最简单,其实最容易埋雷。我曾在一个ChEMBL的项目中,发现同一批化合物的活性数据,来自不同实验室的量纲居然不一致——有IC50、Ki、甚至还有% inhibition。如果不标准化,模型根本没法用。
数据源一般有这几个地方:
- 公共数据库:ChEMBL、PubChem、DrugBank。这些数据量大,但质量参差不齐。
- 专利/文献:需要手动整理,费时费力。我建议用专门的文本挖掘工具辅助。
- 内部实验数据:质量最高,但通常数量有限。
- 商业数据库:像CAS SciFinder,贵但数据可靠。
🔧 小技巧:我习惯在收集阶段就把化合物的标准SMILES写进去,因为后续很多描述符计算工具需要用SMILES作为输入。等你数据量大了再回头加,那感觉太酸爽了。
2.2 数据清洗与标准化
你以为数据来了就能直接用?太天真了。清洗这步,我至少花掉整个项目1/3的时间。
主要做这几件事:
- 移除无机物/混合物:有机小分子才是QSAR的菜。
- 标准化SMILES:用RDKit做一次标准化,确保同一个分子不会出现两种写法。
- 处理缺失值:活性数据缺失的直接删掉。描述符缺失的,要么插补(均值/中位数),要么删掉该特征。
- 去除重复化合物:同一个化合物出现多次,活性值还不一样,留下哪个?我一般取均值,或者保留置信度最高的那个。
- 处理量纲不一致:全部转成pIC50或pKi,方便后续建模。
# 数据清洗示例(Python伪代码)
from rdkit import Chem
def clean_smiles(smiles):
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return None
# 标准化
return Chem.MolToSmiles(mol, canonical=True)
def clean_dataset(df):
# 移除无机物
df = df[df['smiles'].apply(lambda x: Chem.MolFromSmiles(x) is not None)]
# 标准化SMILES
df['canonical_smiles'] = df['smiles'].apply(clean_smiles)
# 去除重复
df = df.drop_duplicates(subset='canonical_smiles')
# 丢弃缺失活性
df = df.dropna(subset=['pIC50'])
return df
⚠️ 踩坑提示:我曾经在清洗时漏掉了某批含重金属的化合物,结果训练出来的模型对金属有机配合物的预测效果极差。建议在清洗前先用RDKit的FilterCatalog做一遍过滤。
2.3 分子描述符计算
描述符是模型的“原料”。选错了,模型再怎么调也白搭。
常见的描述符类型:
| 描述符类型 | 常见工具 | 特点 |
|---|---|---|
| 2D描述符(理化性质) | RDKit、Mordred | 计算快,可解释性强,适合经典QSAR |
| 指纹(Fingerprints) | Morgan、MACCS | 高维稀疏,适合分类任务 |
| 3D描述符 | Open3DALIGN | 计算慢,但对构效关系敏感 |
| 连续分子表示 | Autoencoder | 适合深度学习,但可解释性差 |
# 用RDKit计算2D描述符示例
from rdkit import Chem
from rdkit.Chem import Descriptors
mol = Chem.MolFromSmiles('CCO')
mw = Descriptors.MolWt(mol) # 分子量
logp = Descriptors.MolLogP(mol) # LogP
hbd = Descriptors.NumHDonors(mol) # 氢键供体数
hba = Descriptors.NumHAcceptors(mol) # 氢键受体数
print(f"分子量: {mw}, LogP: {logp}, HBD: {hbd}, HBA: {hba}")
我个人经验:新手不要一上来就搞几百个描述符。先用RDKit的200多个2D描述符跑一遍,够用了。如果效果差,再慢慢加指纹。
2.4 特征选择
描述符多了,反而坏事。我遇到过描述符数量比样本数还多的情况,模型直接过拟合到姥姥家。
特征选择我常用的方法:
- 方差过滤:方差接近0的描述符,直接扔掉。留着也提供不了信息。
- 相关性过滤:两个描述符相关系数 >0.9,只留一个。我记得有一次没做这步,模型训练出来系数爆炸了,才发现LogP和某拓扑描述符高度共线。
- 模型特征重要性:用随机森林或XGBoost跑一遍,挑选重要性前k个特征。
- 正向/后向选择:经典但计算量大,数据少的时候倒是好用。
# 使用随机森林进行特征选择
from sklearn.ensemble import RandomForestRegressor
from sklearn.feature_selection import SelectFromModel
model = RandomForestRegressor(n_estimators=100, random_state=42)
selector = SelectFromModel(model, threshold='median')
X_selected = selector.fit_transform(X, y)
2.5 数据集划分
这步看着微不足道,其实特别有讲究。我见过不少人用随机划分,结果模型在外部验证集上表现极差。
常用方法:
| 划分方法 | 适用场景 | 说明 |
|---|---|---|
| 随机划分 | 数据量大且均匀 | 简单,但容易导致训练集和测试集分布不一致 |
| K折交叉验证 | 数据量中等 | 让每个样本都有机会做验证,评估更稳定 |
| 时间轴划分 | 时序数据 | 按时间顺序划分,避免未来信息泄露 |
| 结构相似性划分 | 骨架差异大的数据 | 用Butina聚类,保证训练集和测试集化学空间差异大 |
我建议:至少要用K折交叉验证来调参,再用一个独立的测试集做最终评估。这样你对模型的能力心里有底。
2.6 模型训练与优化
经典QSAR里,我最常用的是:
- 线性回归/PLS:数据量少、描述符少的时候首选。可解释性最好。
- 随机森林/GBDT:非线性关系强的时候好用,默认参数效果就不错。
- SVM:高维小样本场景,核函数选RBF。
- 神经网络:数据量上去了(>1000)才考虑。两三层就够了,别一上来就是深度网络。
🔑 原则:先跑一个简单的基线模型(比如线性回归),看看数据本身信号强不强。信号弱的话,再折腾复杂算法也白费力气。
2.7 模型评估
评估不是只看R²高不高。我曾经有个模型,R²高达0.93,结果外部测试集上一看,q²只有0.25。为什么?因为我当时只用了内部交叉验证,没做外部验证。
评估指标:
| 任务类型 | 回归 | 分类 |
|---|---|---|
| 内部验证(交叉验证) | q²(交叉验证R²)、RMSE | 交叉验证AUC、准确率 |
| 外部验证 | R²ext、RMSEext、MAE | 外部AUC、敏感度、特异度 |
| 其他常用 | CCC(Concordance Correlation Coefficient) | Matthews Correlation Coefficient |
# 回归模型评估示例
from sklearn.metrics import r2_score, mean_squared_error
y_true = [1.2, 2.5, 3.8, 4.1]
y_pred = [1.3, 2.4, 3.7, 4.0]
r2 = r2_score(y_true, y_pred)
rmse = mean_squared_error(y_true, y_pred, squared=False)
print(f"R²: {r2:.3f}, RMSE: {rmse:.3f}")
⚠️ 避坑:不要用训练集上的R²来吹牛。业界认可的标准是:q² > 0.5,且R²ext > 0.6,这个模型才勉强能用。我自己给客户汇报的时候,R²和q²差值超过0.2,我就知道模型有问题了。
2.8 模型部署
模型做出来了,怎么给实验室的人用?你总不能让生物学家去跑Python代码吧。
部署方式:
- 打包成Web服务:用Flask或FastAPI搭个REST API,输入SMILES,输出预测值。 配个前端界面,实验室同事直接点鼠标就能用。
- 嵌入到软件:把模型导出成ONNX或PMML格式,集成到企业内部平台。
- 批处理脚本:数据量大的话,写个脚本批量处理,输出Excel或CSV。
# 用Flask部署QSAR模型示例(简化版)
from flask import Flask, request, jsonify
import joblib
import numpy as np
app = Flask(__name__)
model = joblib.load('qsar_model.pkl')
scaler = joblib.load('scaler.pkl')
@app.route('/predict', methods=['POST'])
def predict():
data = request.json
features = np.array(data['features']).reshape(1, -1)
features_scaled = scaler.transform(features)
pred = model.predict(features_scaled)
return jsonify({'prediction': float(pred[0])})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
🛠️ 实用建议:部署时把数据预处理步骤(比如标准化)也打包进去。别让用户操心怎么归一化,他只需要给一个SMILES就行。这样做出来的工具,实验室的人才会愿意用。
嗯,这套Pipeline走下来,你的QSAR模型在技术上就不会有太大漏洞了。记住,做项目的时候多回去检查数据,数据才是真正的王道。