2. 分子表示方法:SMILES、InChI、分子图、指纹图谱的对比与选择

做ADMET模型,第一步就是把分子“喂”给机器。但分子是三维的,机器只认数字和符号。怎么把分子转化成机器能理解的语言?这里面的门道可不少。

我刚入行那会儿,以为随便拿个SMILES串就能训练,结果模型效果惨不忍睹。后来才明白,选错了表示方法,后面的模型再怎么调参也是白搭。今天我把常用的几种方法掰开揉碎了讲清楚。

2.1 几种主流分子表示法

目前最常用的有四种:SMILES、InChI、分子图、指纹图谱。每种都有自己的脾气和适用场景。

2.1.1 SMILES

SMILES说白了就是用一串ASCII字符来描述分子结构。比如乙醇是CCO,苯是c1ccccc1。简洁直观,是行业标配。

但我必须提醒你:SMILES不是唯一的。同一个分子可以有多种合法SMILES写法。这就麻烦了——如果你用SMILES做模型输入,不同写法可能导致模型认为这是不同分子。

实操技巧:用RDKit的MolToSmiles函数对SMILES做标准化(canonicalization)。我习惯在数据预处理阶段统一转成规范形式,省去后续很多麻烦。
from rdkit import Chem

smiles_list = ["CCO", "OCC", "C(O)C"]
canonical = []
for s in smiles_list:
    mol = Chem.MolFromSmiles(s)
    if mol:
        canonical.append(Chem.MolToSmiles(mol))
# 三条都会变成 "CCO"
print(set(canonical))

2.1.2 InChI

InChI是IUPAC推出的标准,比SMILES更严谨。它能精确表达立体化学、同位素信息。你想想看,对于一个手性药物,SMILES可能搞混,InChI不会。

但代价是什么?字符串巨长。一个复杂天然产物的InChI能写满一行屏幕。我一般在数据归档和跨库比对时用InChI,模型训练时很少直接用——太长,还得给模型专门配个编码器。

2.1.3 分子图

把原子看成节点,化学键看成边,分子就变成了一张图。这是目前图神经网络(GNN)的标配输入。GCN、GAT、MPNN这些模型,吃的都是这碗饭。

分子图的优势在于保留了完整的拓扑结构。你可以用它做消息传递,学到原子级别的表征。但问题也很明显——不同分子的图大小不一样,训练时要做padding或者分批处理。

遇到过坑:图构建时,有些官能团被当作多个原子来处理,丢失了基团语义。比如苯环如果只按6个碳原子和6个单键处理,模型学不到“这是一个芳香环”的全局信息。我后来强行在特征里加入了芳香性标志。

2.1.4 指纹图谱

这是老前辈了。把分子结构“哈希”成一个固定长度的位向量。Morgan指纹(ECFP)和MACCS指纹是最常用的两员大将。

Morgan指纹:基于Morgan算法,以每个原子为中心,向外扩展不同半径,记录环境信息。设半径r=2,就能捕获直径5个键内的子结构。我很喜欢用它做相似性搜索和初步筛选。

MACCS指纹:只有166个预定义的结构片段。简单粗暴,但胜在快速和可解释。我在做高通量虚拟筛选时,经常先拿MACCS跑一轮,把明显不合适的化合物过滤掉。

from rdkit import Chem
from rdkit.Chem import AllChem, MACCSkeys

mol = Chem.MolFromSmiles("CCO")

# Morgan指纹,半径2,长度2048
morgan_fp = AllChem.GetMorganFingerprintAsBitVect(mol, 2, nBits=2048)
print("Morgan bits:", list(morgan_fp.GetOnBits())[:10])

# MACCS指纹
maccs_fp = MACCSkeys.GenMACCSKeys(mol)
print("MACCS bits:", list(maccs_fp.GetOnBits())[:10])

2.2 对比与选择策略

你可能会问:那我到底该用哪个?别急,我整理了一张表,你一看就明白。

表示法 维度/长度 信息保真度 模型兼容性 我最常用的场景
SMILES 可变 中(歧义需标准化) RNN/Transformer 序列模型、迁移学习预训练
InChI 可变(通常较长) 高(立体化学精确) RNN/Transformer 数据归档、跨库比对
分子图 可变 高(完整拓扑) GNN ADMET属性预测主力方案
Morgan指纹 固定(如2048位) 中高(半径可调) MLP/随机森林/XGBoost 相似性搜索、快速建模
MACCS指纹 固定(166位) 中(预定义片段) MLP/随机森林/XGBoost 高通量筛选、初步过滤
我的选择原则:
  • 想做SOTA模型?直接上分子图+GNN,配合特征增强。
  • 数据集小,又想快速出结果?用Morgan指纹+XGBoost,调参时间短,效果不差。
  • 需要可解释性?MACCS指纹配合SHAP,一目了然。
  • 做大规模预训练?SMILES序列模型,结合自回归任务。

2.3 避坑指南

这些年我踩过的坑,今天一次性告诉你。

  • SMILES标准化不全:我曾经花了三天时间排查模型为什么对同一个分子的不同写法预测不一致。最后发现问题出在标准化的函数没有处理所有情况。现在我已经加上了tautomer(互变异构体)标准化。
  • 分子图过于复杂:图越大,训练越慢。我在一个大项目里用GAT,每个分子平均100多个原子,训练一个Batch要半分钟。后来做子结构采样,速度提升了10倍。
  • 指纹长度不匹配:Morgan指纹的nBits不能太小,否则碰撞严重。我建议至少1024位,2048位更安全。
  • 忘记做盐基去除:很多化合物数据库里有盐酸盐、钠盐。不剥掉盐基,模型学到的其实是“分子+盐”的混合信息。我一般用RDKit的RemoveSalts函数先处理。
from rdkit import Chem
from rdkit.Chem import Descriptors

# 标准化SMILES + 去除盐基
def clean_molecule(smiles):
    mol = Chem.MolFromSmiles(smiles)
    if mol is None:
        return None
    mol = Chem.RemoveSalts(mol)
    smiles_clean = Chem.MolToSmiles(mol, canonical=True)
    return smiles_clean

# 批量处理
raw_smiles = ["CCO.Na", "c1ccccc1.Cl", "O=C(O)C"]
cleaned = [clean_molecule(s) for s in raw_smiles]
print(cleaned)
# 输出: ['CCO', 'c1ccccc1', 'CC(=O)O']

2.4 知识体系总览

下面这张图概括了整个分子表示方法的选择逻辑。自己琢磨琢磨,就能明白什么时候该用什么。

分子表示方法选择流程图 输入分子结构 序列字符串 分子拓扑图 固定长度位向量 SMILES InChI SELFIES GCN GAT MPNN Morgan MACCS ECFP/FCFP RNN/LSTM Transformer 通常不用 GNN GNN GNN MLP/RF MLP/RF MLP/RF ADMET 属性预测:吸收、分布、代谢、排泄、毒性 选择原则:序列模型适合SMILES/InChI做预训练,GNN适合复杂拓扑学习,指纹+MLP适合快速迭代小规模任务。
一个小建议:刚开始做ADMET模型时,别贪多。把一种表示方法吃透,再拓展。我个人推荐从Morgan指纹+XGBoost入手,一天就能跑通,成就感满满。