一、图神经网络与QSAR概述:背景、意义与课程目标
1.1 我们为什么需要这个课程?
先聊点实际的。各位搞药物发现的朋友,或者做计算化学的同行,应该都有这种体会:
- 数据越来越多:高通量筛选、公共数据库每天产出海量分子活性数据。
- 传统方法有点顶不住了:指纹+随机森林虽然经典,但分子的空间结构、原子间长程依赖关系,这些信息基本被丢掉了。
- 图神经网络(GNN)恰好能补这个短板:分子天生就是图结构,原子是节点,化学键是边。GNN 直接在图上做消息传递,说白了就是让每个原子“看”到邻居甚至更远的原子,然后学出整个分子的表征。
我在早期的 QSAR 项目里,试过用 ECFP 加 XGBoost,模型 AUC 做到 0.85 左右,但一到骨架跃迁的测试集就直接掉到 0.72。后来换了个简陋的 GCN,同样的数据集,AUC 涨到 0.91。那一刻我意识到,这条路值得深耕。
核心矛盾:传统分子描述符是“人工设计的固定视角”,GNN 是“数据驱动的动态视角”。我们需要把这两种思路融合起来。
1.2 这门课到底讲什么?
课程内容可以分为三大块,我画了个结构图,你大概看一眼就明白了:
1.3 课程目标:你能带走什么?
跟着我把这30章走完,我希望你至少能做到三件事:
- 能自己搭一套 GNN QSAR 流水线:从分子データ读入、图构建、模型训练到结果分析,每一步都讲透。
- 能避开那些常见的坑:比如图构建时氢原子怎么处理?节点特征用什么?我曾在一个项目中没考虑氢键供体,结果模型在激酶数据集上直接翻车,那个教训挺深刻的。
- 能理解模型在“想”什么:不只是调包跑个 AUC,而是能用注意力权重、梯度解释去分析模型学到了哪些化学知识。
打个比方:传统的 QSAR 像用固定焦距的相机拍照,角度有限。GNN 像无人机环绕拍摄,还能自动补光。本课就是教你怎么操作这架无人机,以及怎么把拍出来的数据变成可用的信息。
1.4 背景:QSAR 的困境与 GNN 的破局
先快速回顾一下。QSAR(定量构效关系)核心目标是:用分子结构预测活性。
传统做法长这样:
- 计算分子描述符(MolLogP、TPSA、分子量...)
- 生成指纹(ECFP、MACCS...)
- 用 RF、SVM、XGBoost 做回归或分类
问题在哪?我总结了三个:
| 困境 | 表现 | 后果 |
|---|---|---|
| 信息丢失 | 指纹是哈希得到的位向量,丢失了原子连接的精确拓扑 | 对类似但不等价的分子区分度差 |
| 长程依赖弱 | 传统描述符很难捕捉相隔较远的官能团协同作用 | 对弱相互作用、协同效应预测不准 |
| 可迁移性差 | 在训练集分布外的分子上,模型基本失效 | 仅适用已知骨架,通用性不足 |
GNN 怎么破的? 说白了,它把分子当成一张图,每个原子通过消息传递不断聚合邻居信息。经过若干层后,每个节点的表征里包含了局部甚至全局的结构信息。你想想看,这比固定指纹灵活多了。
我习惯把这种能力叫作“端到端的结构编码”。不需要手工设计特征,图结构本身就是输入。这也是为什么 GNN 在分子性质预测上表现突出的原因。
一句话总结:GNN 让 QSAR 从“离散特征”时代跨入了“连续结构学习”时代。
1.5 课程面向谁?需要什么基础?
我不要求你已经是图神经网络专家,但希望你有这些基础:
- Python 熟练:能写类、会调包、懂 numpy/pandas
- 机器学习基础:知道什么是过拟合、交叉验证、超参数
- 基本化学知识:看得懂 SMILES,知道原子、键、官能团
如果上面三条有短板,建议先补齐再上车。尤其是 SMILES 解析,我在代码里会经常用到 RDKit,如果你没接触过,最好提前跑几个例子熟悉一下。
1.6 课程资源与工具链
本课程所有核心代码都基于以下工具:
- PyTorch + PyTorch Geometric:GNN 主力框架
- RDKit:分子处理与图构建
- DGL:备选图框架,部分章节我会对比两种框架的差异
- Weights & Biases / MLflow:实验跟踪
一个典型的分子图构建流程,大概是这样:
from rdkit import Chem
from rdkit.Chem import AllChem, Descriptors
import torch
from torch_geometric.data import Data
def mol_to_graph(smiles):
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return None
# 节点特征:原子序数 + 手性 + 度 + 形式电荷 + 氢键供体/受体
atom_features = []
for atom in mol.GetAtoms():
feat = [
atom.GetAtomicNum(),
float(atom.GetChiralTag()),
float(atom.GetDegree()),
float(atom.GetFormalCharge()),
float(atom.GetNumExplicitHs()),
float(atom.GetIsAromatic()),
]
atom_features.append(feat)
# 边索引与边特征
edge_index = []
edge_attr = []
for bond in mol.GetBonds():
i = bond.GetBeginAtomIdx()
j = bond.GetEndAtomIdx()
edge_index.append([i, j])
edge_index.append([j, i]) # 无向图
bond_type = bond.GetBondTypeAsDouble()
edge_attr.append([bond_type, float(bond.GetIsConjugated())])
edge_attr.append([bond_type, float(bond.GetIsConjugated())])
x = torch.tensor(atom_features, dtype=torch.float)
edge_index = torch.tensor(edge_index, dtype=torch.long).t().contiguous()
edge_attr = torch.tensor(edge_attr, dtype=torch.float)
return Data(x=x, edge_index=edge_index, edge_attr=edge_attr)
# 示例
data = mol_to_graph("CC(=O)Oc1ccccc1C(=O)O") # 阿司匹林
print(data)
这段代码是我早期项目里的简化版。实际生产环境中,节点特征通常要包含更多信息,比如 Gasteiger 电荷、部分电荷、是否在环中等。后面课程我会详细展开特征工程的最佳实践。
1.7 学习建议:怎么用好这份指南?
嗯,这里说点我个人习惯的学习方法:
- 别只看,要动手跑:每一章的代码我都提供了完整的 notebook,务必执行一遍。
- 改参数、换数据:把里面的分子数据集换成你自己的,看看效果差异。这才是真学透。
- 记笔记:尤其是踩过的坑,比如“这个分子 SMILES 解析出来是 None”、“这个层的传播维度对不上”。记录下来,以后就是你的避坑手册。
警惕“复制粘贴依赖症”:我看到太多人把代码复制过去跑通了就觉得自己会了。模型不收敛的时候,需要你能看懂梯度为什么消失,而不是换个学习率碰运气。这也是我设计大量可视化与解释性内容的原因。
1.8 本章小结
这一章是热身。你知道了:
- 为什么传统 QSAR 到了瓶颈,GNN 能带来新突破
- 这门课的三块知识体系:理论基础、模型实战、部署优化
- 你需要哪些前置技能、用哪些工具
- 一个最简的分子图构建示例