第二章 QSAR建模流程:从数据收集到模型部署的完整Pipeline

很多人刚开始接触QSAR,上来就急着调模型、跑代码。我见过不少同行数据还没洗干净,就把模型跑出来了,结果R²看着挺高,一交叉验证就崩掉。说白了,QSAR建模最关键的,不是你会用哪个算法,而是你有没有一套可靠的流程。

这套流程,我把它拆成6个大环节。每个环节都有坑,我一个个讲清楚。

💡 核心观点:QSAR建模的本质是“数据驱动”,数据质量决定了模型上限。算法只是逼近这个上限的工具。

QSAR经典建模完整Pipeline 数据收集 数据清洗 描述符计算 特征选择 数据集划分 模型训练 模型评估 模型部署 反馈优化 💡 完整Pipeline中包含数据收集→清洗→描述符→特征选择→划分→训练→评估→部署 虚线箭头表示评估不达标时需回溯到数据或特征环节进行优化

2.1 数据收集:源头决定一切

数据收集,看着最简单,其实最容易埋雷。我曾在一个ChEMBL的项目中,发现同一批化合物的活性数据,来自不同实验室的量纲居然不一致——有IC50、Ki、甚至还有% inhibition。如果不标准化,模型根本没法用。

数据源一般有这几个地方:

  • 公共数据库:ChEMBL、PubChem、DrugBank。这些数据量大,但质量参差不齐。
  • 专利/文献:需要手动整理,费时费力。我建议用专门的文本挖掘工具辅助。
  • 内部实验数据:质量最高,但通常数量有限。
  • 商业数据库:像CAS SciFinder,贵但数据可靠。

🔧 小技巧:我习惯在收集阶段就把化合物的标准SMILES写进去,因为后续很多描述符计算工具需要用SMILES作为输入。等你数据量大了再回头加,那感觉太酸爽了。

2.2 数据清洗与标准化

你以为数据来了就能直接用?太天真了。清洗这步,我至少花掉整个项目1/3的时间。

主要做这几件事:

  1. 移除无机物/混合物:有机小分子才是QSAR的菜。
  2. 标准化SMILES:用RDKit做一次标准化,确保同一个分子不会出现两种写法。
  3. 处理缺失值:活性数据缺失的直接删掉。描述符缺失的,要么插补(均值/中位数),要么删掉该特征。
  4. 去除重复化合物:同一个化合物出现多次,活性值还不一样,留下哪个?我一般取均值,或者保留置信度最高的那个。
  5. 处理量纲不一致:全部转成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模型在技术上就不会有太大漏洞了。记住,做项目的时候多回去检查数据,数据才是真正的王道。