分子表示基础:SMILES、InChI、分子图、3D坐标的表示方法与互转

做分子生成,第一个门槛就是“怎么描述一个分子”。

你想想看,计算机不认识结构式,它只认字符串和数字。所以我们需要把化学结构翻译成它听得懂的语言。SMILES、InChI、分子图、3D 坐标——这四种表示法,基本覆盖了所有场景。我个人习惯是:初期建模用 SMILES,数据库存 InChI,图神经网络用分子图,对接或构象分析用 3D 坐标。但互转的时候经常踩坑,今天我们就逐个拆开讲清楚。

SMILES——最常用的线性表示

SMILES 全称 Simplified Molecular Input Line Entry System。说白了就是用一行 ASCII 字符描述分子连接关系。比如水的 SMILES:O,乙醇是 CCO,苯是 c1ccccc1。

规不规范,差别很大。我在项目中遇到过一版训练数据,同一个分子有的用 CCO,有的用 OCC,模型根本学不对。后来我强制用 RDKit 的 MolToSmiles 做 canonical 化,才把问题解决了。

SMILES 的语法很简单:原子用元素符号,单键省略,双键写 =,三键写 #,分支用括号,环用数字标记。比如环己烷:C1CCCCC1。

但注意两点:

  • SMILES 不包含立体化学信息(除非用 @ 符号表示手性)
  • 同一个分子可以有无数种 SMILES 字符串,必须规范化

实用经验:用 rdkit.Chem.MolToSmiles(mol, canonical=True) 确保输出统一。数据清洗时一定要做这步。

InChI——国际化标识符

InChI(International Chemical Identifier)是 IUPAC 推的标准。跟 SMILES 比,它更严谨,层次化更强。怎么看 InChI 的格式?比如水的 InChI:

InChI=1S/H2O/h1H2

前面是版本和层数,后面是分子式和氢原子信息。InChI 还带一个哈希版本叫 InChIKey,固定 27 个字符,适合做数据库索引。

我个人习惯:对外发布数据时用 InChI,避免乱序歧义。但注意,InChI 转回结构时,RDKit 的速度比 SMILES 慢几倍,做大规模生成时尽量少用。

避坑:我曾经在爬取 PubChem 数据时直接用 InChI 做 Key,结果发现同分异构体(如对映体)InChI 几乎一样,只有最后一段有区别。如果你需要区分立体构型,请用 InChI 完整版本,不要只截取前几层。

分子图——GNN的“母语”

图神经网络(GNN)处理分子,必须把分子转成图结构。节点是原子,边是化学键。每个节点可以包含原子类型、电荷、手性等特征;边可以包含键级、环信息等。

RDKit 里用 Chem.rdmolops.GetAdjacencyMatrix 拿到邻接矩阵,然后自己拼特征。但更推荐用 Chem.MolFromSmiles 之后直接遍历原子和键:

from rdkit import Chem
mol = Chem.MolFromSmiles('CCO')
for atom in mol.GetAtoms():
    print(atom.GetAtomicNum(), atom.GetSymbol())
for bond in mol.GetBonds():
    print(bond.GetBeginAtomIdx(), bond.GetEndAtomIdx(), bond.GetBondType())

分子图的好处是能直接喂给 GNN,不需要额外编码。但我做项目时发现,如果直接用 SMILES 的原子顺序建图,图的拓扑映射对不上标准的 InChI 编号,导致验证集准确率偏低。后来我改用 Chem.CanonicalRankAtoms 排序,问题才解决。

3D 坐标——真实空间位置

SMILES 和分子图没有三维信息。做分子对接、构象搜索、力场优化必须用 3D 坐标。常见格式:XYZ 文件和 SDF 文件。

XYZ 文件长这样:

3
water
O    0.0000   0.0000   0.1170
H    0.7570   0.0000  -0.4690
H   -0.7570   0.0000  -0.4690

SDF 更丰富,可以存性质、键类型、多分子。RDKit 从 SMILES 生成 3D 坐标的典型做法:

mol = Chem.MolFromSmiles('CCO')
mol = Chem.AddHs(mol)  # 加氢
AllChem.EmbedMolecule(mol, randomSeed=42)
AllChem.MMFFOptimizeMolecule(mol)  # 力场优化
print(mol.GetConformer().GetPositions())

这里有个坑:不加氢直接嵌入,坐标很离谱。我第一版 pipeline 就忘了这步,生成的分子大部分键长都异常。后来我加了一行 Chem.AddHs,效果立刻正常了。

互转:核心工具链与实战对比

实际工作中,这四种表示经常需要来回倒。核心工具还是 RDKit 和 Open Babel。下表总结常见互转场景和推荐方法:

转换方向 推荐方法 注意
SMILES → InChI Chem.MolToInchi(mol) 确保 mol 是 Sanitize 后的
InChI → SMILES Chem.MolFromInchi(inchi) → Chem.MolToSmiles(mol) 可能丢失立体信息
SMILES → 分子图 RDKit 原子/键遍历 建议用 canonical 原子顺序
SMILES → 3D 坐标 AllChem.EmbedMolecule + 力场优化 必须加氢
3D 坐标 → SMILES 先用 MolFromXYZ 或 SDF 读入,再 MolToSmiles 有损转换,注意环的感知
分子图 → SMILES 从邻接矩阵重建 mol 对象 需要原子类型、键级等完整特征

另外,我常用 Open Babel 做批量转换,尤其处理 SDF 和 XYZ 互转。命令行一行搞定:

obabel input.sdf -O output.xyz
obabel input.smi -O output.inchi --geninchi

但 Open Babel 的 SMILES canonical 跟 RDKit 不总是严格一致。我的建议是:pipeline 中从头到尾只用同一个工具,避免不一致。

分子表示及互转核心流程 SMILES InChI 分子图 3D 坐标 核心工具:RDKit (MolFromSmiles / MolToInchi / EmbedMolecule) + Open Babel

我的建议:如果你刚开始搭建分子生成 pipeline,先拿 SMILES 作为主干,所有输入都转成 RDKit 的 Mol 对象。需要什么表示,就从 Mol 对象生成。这样能避免多工具不一致的问题。我自己的项目一直这么干,后面加图解码、3D 构象都挺顺畅的。

代码整合示例:完整互转流程

下面这个函数展示从 SMILES 出发,生成 InChI、分子图特征、3D 坐标,并检查一致性:

def molecular_representation_pipeline(smiles):
    from rdkit import Chem
    from rdkit.Chem import AllChem, Descriptors
    
    mol = Chem.MolFromSmiles(smiles)
    if mol is None:
        raise ValueError("Invalid SMILES")
    Chem.SanitizeMol(mol)
    
    # 1. SMILES canonical
    canonical_smiles = Chem.MolToSmiles(mol)
    
    # 2. InChI
    inchi = Chem.MolToInchi(mol)
    
    # 3. 分子图(节点特征:原子序数,边特征:键级)
    atoms = [atom.GetAtomicNum() for atom in mol.GetAtoms()]
    bonds = [(b.GetBeginAtomIdx(), b.GetEndAtomIdx(), int(b.GetBondTypeAsDouble()))
             for b in mol.GetBonds()]
    
    # 4. 3D 坐标(需要加氢)
    mol_h = Chem.AddHs(mol)
    AllChem.EmbedMolecule(mol_h, randomSeed=42)
    AllChem.MMFFOptimizeMolecule(mol_h)
    conf = mol_h.GetConformer()
    coords = conf.GetPositions()
    
    return {
        'canonical_smiles': canonical_smiles,
        'inchi': inchi,
        'num_atoms': len(atoms),
        'num_bonds': len(bonds),
        'coords_3d': coords
    }

# 测试
result = molecular_representation_pipeline('CCO')
print(result['canonical_smiles'])  # CCO
print(result['inchi'])             # InChI=1S/C2H6O/c1-2-3/h3H,2H2,1H3

一句话总结:所有表示都可以从 Mol 对象派生,千万别自己在字符串级别解析。RDKit 的 Mol 就是你分子生成的“中转枢纽”。