第2章 分子表示方法:SMILES、SELFIES与分子图

做分子生成,第一道坎就是——怎么让机器"看懂"分子结构。

你想想看,化学家画个苯环一目了然,但深度学习模型只认识数字和向量。所以我们必须把分子结构转成机器能处理的格式。说白了,这就是分子表示要解决的问题。

目前主流的有三种:SMILES、SELFIES 和 分子图。每种都有各自的脾气,选不对,后面训练全白搭。我在这三种表示上踩过不少坑,今天一并讲清楚。

2.1 SMILES:最流行的分子语言

SMILES 全称是 Simplified Molecular-Input Line-Entry System,简单说就是用一串 ASCII 字符来描述分子结构。

举个最简单的例子,乙醇的 SMILES 就是 CCO。苯环是 c1ccccc1。你看,原子符号、键、分支、环,全都压在一行字符串里。

CCO          # 乙醇
c1ccccc1     # 苯
CC(=O)Oc1ccccc1C(=O)O  # 阿司匹林

SMILES 的优点很明显:简洁、人类可读、生态完善。RDKit、OpenBabel 等工具全都支持,数据集也大多是 SMILES 格式。

但问题也很要命——同一个分子可以有多个有效的 SMILES。比如乙醇,你可以写成 CCO,也可以写成 OCC。这直接导致生成模型学到的映射关系不唯一。

我曾经踩过的坑:刚开始用 SMILES 训练一个分子生成模型,看起来 loss 降得挺好,结果生成出来的分子一大半在化学上根本不可能存在。后来排查发现,模型学会了"凑语法"但没学会"凑化学",说白了就是 SMILES 的歧义性把模型带偏了。

所以,如果你要用 SMILES 做生成,一定要做规范化(canonicalization),把每个分子转成唯一的标准形式。

from rdkit import Chem

smiles_list = ["CCO", "OCC", "C(O)C"]
canonical = set()
for s in smiles_list:
    mol = Chem.MolFromSmiles(s)
    canonical.add(Chem.MolToSmiles(mol))

print(canonical)  # 输出: {'CCO'}

2.2 SELFIES:解决SMILES的痛点

SELFIES(SELF-referencIng Embedded Strings)是专门为解决 SMILES 歧义问题设计的。它的核心思路是:每个符号都是自描述的,语法完全完备。

乙醇的 SELFIES 是 [C][C][O],苯环是 [C][=C][C][=C][C][=C][Ring1][Branch1]。

[C][C][O]                    # 乙醇
[C][=C][C][=C][C][=C][Ring1][Branch1]  # 苯

你看,每个符号都明确表达了它是什么、怎么连接,不存在"多种写法"的问题。

我个人习惯在分子生成任务中优先用 SELFIES。为什么?因为 100% 语法有效。你随便生成一串 SELFIES,只要符合语法规则,它就对应一个真实分子。这对生成任务来说太重要了。

小技巧:SMILES 和 SELFIES 之间可以互相转换,用 selfies 库就行。
import selfies as sf

smiles = "CCO"
selfies = sf.encoder(smiles)
print(selfies)  # 输出: [C][C][O]

# 转回来
smiles_back = sf.decoder(selfies)
print(smiles_back)  # 输出: CCO

SELFIES 的缺点是字符串更长,人类阅读不直观。另外,一些老工具不支持 SELFIES,需要额外转换。

2.3 分子图:保留拓扑结构

序列表示(SMILES、SELFIES)本质上是把分子压成了一维,丢失了空间拓扑信息。而分子图直接保留了原子之间的连接关系。

在分子图中:

  • 节点 = 原子(带有原子类型、电荷、手性等特征)
  • 边 = 化学键(带有键型、共轭状态等特征)

这种方式最贴近化学家对分子的理解,也最适合做属性预测和结构分析。

import torch
from torch_geometric.data import Data
from rdkit import Chem

smiles = "CCO"
mol = Chem.MolFromSmiles(smiles)

# 提取原子特征(简化版)
atom_features = []
for atom in mol.GetAtoms():
    atom_features.append([atom.GetAtomicNum()])
x = torch.tensor(atom_features, dtype=torch.float)

# 提取边索引
edge_indices = []
for bond in mol.GetBonds():
    i, j = bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()
    edge_indices.append([i, j])
    edge_indices.append([j, i])
edge_index = torch.tensor(edge_indices, dtype=torch.long).t().contiguous()

# 构建 PyG 图数据
data = Data(x=x, edge_index=edge_index)
print(data)

我记得有次做分子属性预测,用 SMILES 做输入,R² 死活到不了 0.85。换成分子图 + GNN,直接飙到 0.93。为什么?因为图结构把原子间的连接关系完整保留了下来,模型能学到更本质的化学特征。

当然,分子图也有代价:计算开销更大,而且不同分子的图结构不同,需要做批处理(batch)时得用一些技巧(比如虚拟节点)。

2.4 三种表示方法对比

维度 SMILES SELFIES 分子图
表示类型 线性字符串 线性字符串 图结构
语法完备性 有歧义 完全完备 天然完备
人类可读性 高 低 中
生成任务适配度 中(需规范化) 高 高
属性预测适配度 低 低 高
计算效率 高 中 低
工具生态 最成熟 发展中 成熟
分子表示方法知识体系 真实分子结构 SMILES SELFIES 分子图 线性序列,存在歧义 线性序列,语法完备无歧义 图结构,保留完整拓扑 根据任务需求选择表示方法

2.5 实践建议

三种表示方法各有主场,选哪个取决于你的任务目标。我分享一下自己的选型逻辑:

  • 做分子生成(特别是从头生成) → 优先 SELFIES。语法完备性太香了,省去大量后处理过滤。
  • 做属性预测/回归任务 → 优先分子图 + GNN。拓扑信息对预测精度提升明显。
  • 做数据预处理或大规模筛选 → 用 SMILES。工具生态最成熟,处理速度快。
  • 做多模态或混合任务 → 可以同时用分子图 + 序列表示,做特征融合。
我的习惯做法:数据存储用 SMILES(通用性好),训练生成模型时转成 SELFIES,做属性预测时转成分子图。说白了,就是"存储用最通用的,训练用最合适的"。
关键结论:没有"最好"的表示方法,只有"最合适"的表示方法。理解每种表示的本质,才能为你的任务做出正确选择。