分子化学基础:从SMILES到立体化学
要说图神经网络预测ADMET,我们得先聊聊分子长什么样。说白了,模型再厉害,它也得先把分子“看”懂了才能干活。我这些年带项目,发现不少人在图模型上使劲调参,但忽略了分子表示这个地基。地基不稳,楼盖得再高也得塌。
这一章,我们就把分子表示的几个核心概念掰开揉碎讲清楚。你掌握了这些,后面上手GNN才会顺手。
2.1 SMILES与InChI:分子的两种“身份证”
分子怎么用文字描述?这是个古老的问题。目前最通用的两种格式就是SMILES和InChI。我项目中经常拿它们做数据预处理,你想想看,不同数据库出来的分子格式五花八门,没有统一表示就没法训练。
SMILES表示法
SMILES全称是简化分子线性输入规范。它用ASCII字符串来表示分子的结构。举个例子,乙醇的SMILES是CCO。简单吧?
这里有几个基本规则:
- 原子:用元素符号表示。碳是C,氧是O,氮是N。有机分子里氢通常省略。
- 键:单键默认不写,双键写
=,三键写#。 - 分支:用圆括号
()表示支链。比如异丙醇是CC(C)O。 - 环:用数字标记环的连接点。苯环可以写成
c1ccccc1。
看一下实际例子:
# 几个常见分子的SMILES
阿司匹林: CC(=O)Oc1ccccc1C(=O)O
咖啡因: Cn1c(=O)c2c(ncn2C)n(C)c1=O
布洛芬: CC(C)Cc1ccc(cc1)C(C)C(=O)O
对乙酰氨基酚: CC(=O)Nc1ccc(O)cc1
我刚开始用SMILES时犯过一个错:没注意芳香性表示。苯环里碳用小写c不是大写C。大写C表示sp3杂化的饱和碳。你写C1=CC=CC=C1也行,但那不是标准芳香表示,在ADMET预测里会丢关键信息。
InChI表示法
InChI是国际化合物标识的缩写。它比SMILES更规范,更擅长处理立体化学和互变异构体。我建议做数据合并时用InChI当主键,因为它能避免很多歧义。
典型的InChI长这样:
# 乙醇的InChI
InChI=1S/C2H6O/c1-2-3/h3H,2H2,1H3
# 阿司匹林的InChI
InChI=1S/C9H8O4/c1-6(10)13-8-5-3-2-4-7(8)9(11)12/h2-5H,1H3,(H,11,12)
它分几层:分子式层1S/C2H6O、连接层/c1-2-3、氢原子层/h3H,2H2,1H3。每一层解决一个化学表示问题。
我个人经验是:SMILES适合做模型输入,InChI适合做数据去重。模型看SMILES字符串里的原子序列,人去重用InChI的唯一性。两者互补。
用Python的RDKit做转换很简单:
from rdkit import Chem
# SMILES转Mol对象
mol = Chem.MolFromSmiles("CCO")
# Mol转InChI
from rdkit.Chem import rdmolops
inchi = Chem.MolToInchi(mol)
print(inchi) # 输出: InChI=1S/C2H6O/c1-2-3/h3H,2H2,1H3
# InChI转SMILES
mol2 = Chem.MolFromInchi(inchi)
smiles = Chem.MolToSmiles(mol2)
print(smiles) # 输出: CCO
2.2 官能团与分子性质:预测ADMET的核心抓手
为什么我们要讲官能团?因为ADMET预测的本质就是“结构决定性质”。我做过一个项目,用GNN预测药物水溶性,发现模型对羟基(-OH)和羧基(-COOH)特别敏感。为什么?因为这些极性基团直接影响氢键能力。
常见的官能团及其对ADMET的影响:
| 官能团 | 化学式 | 对ADMET的影响 |
|---|---|---|
| 羟基 | -OH | 增加水溶性,促进代谢(II相葡萄糖醛酸化) |
| 羧基 | -COOH | 强酸性,增加水溶性,可能引起胃肠道刺激 |
| 氨基 | -NH₂ | 碱性,影响口服生物利用度,可能产生毒性 |
| 羰基 | C=O | 影响氢键受体能力,与CYP450酶结合 |
| 芳香环 | 苯环等 | 增加脂溶性,提高LogP,但可能引起代谢活化 |
| 卤素 | -F, -Cl, -Br | 调节脂溶性和代谢稳定性,氟原子常用于优化 |
我在做GNN特征工程时,除了原子类型和键类型,还会显式加入“是否是氢键供体/受体”这个特征。说白了,官能团信息可以编码成原子级别的属性,喂给图神经网络。
举个例子:
# 用RDKit识别官能团
from rdkit import Chem
from rdkit.Chem import Descriptors, MolFromSmiles, Mol
mol = MolFromSmiles("CC(=O)Oc1ccccc1C(=O)O") # 阿司匹林
# 计算分子描述符
logP = Descriptors.MolLogP(mol)
h_donors = Descriptors.NumHDonors(mol)
h_acceptors = Descriptors.NumHAcceptors(mol)
tpsa = Descriptors.TPSA(mol)
print(f"LogP: {logP:.2f}")
print(f"氢键供体: {h_donors}")
print(f"氢键受体: {h_acceptors}")
print(f"拓扑极性表面积: {tpsa:.2f} Ų")
2.3 手性与立体化学:容易被忽视的关键维度
啊,立体化学。这是ADMET预测里最容易被忽视但影响极大的因素。我记得有一次,模型对某种药物的代谢半衰期预测偏差巨大。排查了两天,发现是手性中心搞反了——R构型和S构型的药代动力学完全不同。
什么是手性? 简单说就是一个碳原子连着四个不同的基团,它的镜像不能与自身重合。就像左手和右手,看起来一样但无法重叠。
在SMILES中,手性用@或@@标记:
# L-丙氨酸 (S构型)
SMILES: C[C@@H](N)C(=O)O
# D-丙氨酸 (R构型)
SMILES: C[C@H](N)C(=O)O
这里的@表示方向。具体规则我不展开,你记住@和@@表示两种不同构型就够了。
在InChI中,立体化学信息放在/t和/m层:
# L-丙氨酸的InChI
InChI=1S/C3H7NO2/c1-2(4)3(5)6/h2H,4H2,1H3,(H,5,6)/t2-/m0/s1
最后那个/t2-/m0/s1就是立体信息。但说实话,我很少直接解析InChI的立体层。我通常用RDKit提取手性中心信息,然后编码到图特征里。
实战中如何处理手性?这里给个RDKit示例:
from rdkit import Chem
from rdkit.Chem import rdmolops
mol = Chem.MolFromSmiles("C[C@@H](N)C(=O)O") # L-丙氨酸
# 获取手性中心
chiral_centers = Chem.FindMolChiralCenters(mol, includeUnassigned=True, includeCIP=True)
print(chiral_centers)
# 输出: [(2, 'S')],表示2号碳是S构型
# 获取所有原子手性信息
for atom in mol.GetAtoms():
if atom.GetChiralTag() != Chem.ChiralType.CHI_UNSPECIFIED:
print(f"原子{atom.GetIdx()}: 手性类型={atom.GetChiralTag()}")
C.C(N)C(=O)O),RDKit会默认生成CHI_UNSPECIFIED类型。这种未指定手性的分子,在预测手性敏感的ADMET性质(如CYP450代谢)时会产生巨大误差。一定要确保你的训练数据包含立体化学信息。如果不包含,可以考虑使用InChI补全。
对于GNN来说,手性信息可以编码成边特征或者全局特征。我个人习惯把每个手性中心的方向编码成一个整数特征(R=1, S=2, 未指定=0),然后作为全局图属性传给读出头。这样模型能感知到手性差异。
from rdkit.Chem import AllChem
mol3d = Chem.AddHs(mol)
AllChem.EmbedMolecule(mol3d, AllChem.ETKDG())
print(AllChem.MMFFOptimizeMolecule(mol3d))
好,现在我们回头看分子表示的全貌。下方这张图展示了从原始分子到图输入的完整链条:
从图上你能看到,分子表示从SMILES字符串出发,经过RDKit解析成Mol对象,再通过特征工程提取官能团和手性信息,最终构造成图神经网络的输入。每一步都环环相扣。
- SMILES和InChI各有用途。我建议在进模型前统一用一种规范化的SMILES(比如用RDKit的MolToSmiles输出芳香模式)
- 官能团是ADMET预测的隐形冠军。不要只依赖图结构自动学习,该加的特征要手动加
- 立体化学不是锦上添花,是雪中送炭。做ADMET预测,尤其涉及代谢和转运蛋白时,手性信息必须保留
这一章的内容比较基础,但属于必须打牢的地基。你理解了分子表示、官能团和立体化学,下一阶段我们才能谈怎么把这些信息喂给图神经网络。嗯,先消化这些,到第三章我们开始动手搭第一个GNN模型。