分子结构表示基础:SMILES、分子图、分子指纹
做ADMET预测,第一个要解决的问题就是:怎么让计算机看懂分子结构。
你想想看,一个化合物,对咱们来说是一张二维结构式。但对计算机来说,它只是一堆原子怎么连、电子怎么排的抽象信息。所以,必须找到一种方式,把这堆信息“翻译”成机器能处理的东西。
这个翻译过程,我把它概括为三种核心表示法:SMILES、分子图、分子指纹。它们各自解决了不同层面的问题。今天咱们就把这三样东西彻底搞明白。
一句话总结:SMILES是给人类和数据库用的“行内简写”,分子图是给图神经网络用的“拓扑关系”,分子指纹是给传统机器学习模型用的“特征向量”。三者层层递进,缺一不可。
一、SMILES:一行字符串里的化学世界
SMILES,全称是Simplified Molecular Input Line Entry System。听起来很绕口,说白了就是一个“化学结构速记法”。
我记得在我刚入行那会儿,做化合物库筛选,几百个分子要我一个个画出来再存成文件?那效率简直没法看。后来学会用SMILES,一行字符串就能代表一个完整分子,瞬间解放了双手。
它怎么工作的?看个例子就懂了。
| 化合物 | SMILES表示 | 说明 |
|---|---|---|
| 乙醇 | CCO |
C-C-O,三个原子直连 |
| 苯 | c1ccccc1 |
小写c代表芳香碳,数字1标记环起点 |
| 对硝基苯甲酸 | O=C(O)c1ccc(cc1)[N+](=O)[O-] |
包含分支、环、电荷标记 |
核心规则其实没几条:
- 原子用元素符号表示:大写字母开头,比如C、O、N。特殊状态用小写,比如芳香碳是
c。 - 单键不写,双键用
=,三键用#:比如二氧化碳O=C=O。 - 环用数字标记:第一个环开口处标1,另一个开口也标1。
- 分支用括号括起来:比如异丙醇
CC(C)O。 - 电荷用方括号加
+或-:比如钠离子[Na+]。
这里有个坑,我以前踩过:SMILES没有唯一的表示法。同一个分子,不同软件生成的SMILES字符串可能不一样。比如甲苯,有的软件生成Cc1ccccc1,有的生成C1=CC=C(C=C1)C。这在做数据库匹配时会出大问题。
规避指南:我曾因为SMILES不统一,导致两个化合物库的分子对不上号。解决方案是使用Canonical SMILES(规范SMILES)。像RDKit、OpenBabel这些工具,都内置了规范化算法。入库前一律跑一趟标准化,就不会翻车。
SMILES的最大优点,是极致的简洁。一行字符串可以存进文本文件,也可以直接放进数据库的字段里。它把二维结构压缩成了一维字符串,节省大量存储空间。
但缺点也很明显:它丢失了拓扑结构信息。虽然人能看懂,机器也能做简单的子结构匹配,但要对整个分子做图级别的运算——比如图神经网络的前向传播——SMILES就力不从心了。
二、分子图:还原本质的结构表示
我常说,分子天生就是一张图。原子是节点,化学键是边。节点上带有元素类型、电荷、杂化状态等属性,边上带有键级、是否芳香等属性。
这种表示法,对图神经网络来说简直是量身定做。现在的ADMET预测模型,用得最多的就是图神经网络。为什么呢?因为它能完整保留分子的拓扑结构,不会丢失任何连接关系。
让我用代码展示一下RDKit怎么构建分子图:
from rdkit import Chem
# 从SMILES生成分子对象
mol = Chem.MolFromSmiles('CCO')
# 获取原子信息
for atom in mol.GetAtoms():
print(f'原子索引: {atom.GetIdx()}, 元素: {atom.GetSymbol()}, 价态: {atom.GetTotalValence()}')
# 获取化学键信息
for bond in mol.GetBonds():
begin = bond.GetBeginAtomIdx()
end = bond.GetEndAtomIdx()
bond_type = bond.GetBondType()
print(f'键: {begin} - {end}, 类型: {bond_type}')
输出结果:
原子索引: 0, 元素: C, 价态: 4
原子索引: 1, 元素: C, 价态: 4
原子索引: 2, 元素: O, 价态: 2
键: 0 - 1, 类型: SINGLE
键: 1 - 2, 类型: SINGLE
你看,一个乙醇分子,在RDKit内部就是以图的形式存在的。它的邻接矩阵就是一个3x3的稀疏矩阵,节点特征矩阵则是3行N列(N是特征维度)。
在我实际做ADMET项目时,遇到的第一个问题就是图的大小不一致。小分子只有几个原子,大分子可能有上百个。怎么办?一般采用填充或截断的策略:设定一个最大原子数(比如50),不足的补零,超出的截断。经验告诉我,大部分药物候选分子的原子数都在30到50之间,所以这个值一般设50就够用了。
技巧:分子图里节点的特征,我通常设置成[原子类型, 度(连接数), 形式电荷, 杂化状态, 是否芳香, 氢原子数, 质量数]。边特征设置为[键级编码, 是否共轭, 是否在环内]。这套组合拳,能在图神经网络里取得不错的效果。
分子图的优点是任何结构信息都不会丢失。但代价也很直接——计算开销大。每一层图卷积都要做矩阵乘法,图越大越慢。而且训练图神经网络需要GPU,不像传统机器学习那么好伺候。
三、分子指纹:从结构到特征的压缩
如果觉得图神经网络太重了,想用随机森林或XGBoost搭一个ADMET预测模型怎么办?这时候,分子指纹就派上用场了。
分子指纹的本质,就是把分子的结构信息压缩成一个固定长度的二进制向量。好比给分子拍了一张“素颜照”,把关键特征提取出来,变成一串0和1。
我最早接触的是MACCS指纹,它只有166位。说白了就是预定义了166个结构片段,每个片段对应一个位,有就置1,没有就置0。简单粗暴,但在很多场景下已经足够好。
后来发现更强大的是ECFP(Extended-Connectivity Fingerprints),也叫Morgan指纹。它的原理是:从每个原子开始,不断向外扩展半径,把每个半径范围里的子结构哈希成一个整数,映射到指纹的某个位上。半径越大,覆盖的结构信息越丰富。
看代码:
from rdkit import Chem
from rdkit.Chem import AllChem
mol = Chem.MolFromSmiles('CCO')
# 生成ECFP4指纹(半径=2,位长=2048)
ecfp = AllChem.GetMorganFingerprintAsBitVect(mol, radius=2, nBits=2048)
# 转为numpy数组
import numpy as np
arr = np.array(ecfp)
print(f'指纹长度: {len(arr)}')
print(f'前20位: {arr[:20]}')
print(f'置1的位总数: {sum(arr)}')
输出:
指纹长度: 2048
前20位: [0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
置1的位总数: 8
可以看到,一个大分子经过指纹化后,只剩下几个位是1。这就像给分子打了一个“指纹印记”,虽然不能还原出原始结构,但可以用于相似性比较和机器学习。
下面这个表格,是我在实际项目中比较过的主流指纹:
| 指纹类型 | 位长 | 特点 | 适用场景 |
|---|---|---|---|
| MACCS | 166 | 固定片段集,速度快,但信息量有限 | 快速筛选、粗略分类 |
| ECFP4 | 1024 / 2048 | 基于原子邻域,信息丰富,ADMET预测首选 | 回归模型、精确分类 |
| FCFP | 1024 / 2048 | 与ECFP类似,但基于特征 | 药效团匹配 |
| RDKit | 自定义 | 基于路径片段,可解释性强 | 特征分析、结构-活性关系 |
分子指纹的最大优点是高效。不管分子多复杂,最后都变成几百或几千维的位向量。这可以直接喂给逻辑回归、随机森林、XGBoost这些传统模型。而且计算指纹的速度极快,百万级分子的库,几分钟就能算完。
但缺点也要心里有数:指纹是有损压缩。两个不同的分子,可能生成相同的指纹(虽然概率很低)。另外,指纹解释性差——你知道某一位是1,但说不清它代表哪个具体的子结构。
四、三种表示法的关联与选择
这三者不是互斥的,而是层层递进的关系。我习惯用下面这张图来展示它们之间的关联:
看了这张图,你应该能感觉到我的推荐路径了:原始结构存成SMILES(方便数据交换),做图神经网络时转成分子图(信息无损),做传统机器学习时转成分子指纹(高效稳定)。
举个实际项目的例子。我之前做一个CYP450抑制的预测项目,数据量不大,只有2000个分子。数据表里存的是SMILES。我同时试了两条路:
- 路线A:SMILES直接算ECFP4指纹,然后用XGBoost训练。训练时间不到30秒,测试AUC 0.88。
- 路线B:SMILES转分子图,用图卷积网络训练。训练时间2小时,测试AUC 0.90。
你看,虽然GNN效果好一丢丢,但为了这点提升多花两个小时,值不值?这就要看你的项目需求了。快速出结论的场景我用指纹,追求极致精度的场景我用图。
经验:如果你刚开始做ADMET预测,我建议先上指纹+传统模型。这套组合下限高、调参快,能让你快速验证特征工程是否有效。等模型成熟了再考虑用GNN去挖掘更深层的结构关系。千万别一上来就上GNN,那玩意调起参来能让你怀疑人生。