SMILES 与分子编码:从字符串到图

说到分子表示,我脑子里第一个蹦出来的就是 SMILES。这玩意儿当年刚出来的时候,化学信息学领域可算是炸了锅——一个字符串就能代表一个分子,太方便了。

可问题是,咱们搞图神经网络的人,不能直接拿字符串去训练模型。你想想看,字符串是序列数据,分子是图结构,这里面有个「翻译」的过程。

SMILES 的基本语法

SMILES 其实就是一个线性表示的分子。它的规则不复杂,我简单给你捋一下:

  • 原子:用元素符号表示,比如 C、N、O。有机小分子里最常见的就那么几个。
  • 键:单键直接写,双键写 =,三键写 #。
  • 分支:用圆括号 () 包起来。比如 CC(O)C,中间的 (O) 就是个支链。
  • 环:用数字标记环的起点和终点。比如 C1CCCCC1,这个 1 就是环的闭合标记。

举个例子,乙醇的 SMILES 是 CCO。看着简单吧?但你要是处理复杂分子,比如紫杉醇,那字符串能长得让你怀疑人生。

个人小技巧:刚开始接手项目时,我总拿 SMILES 字符串直接去训练图网络,结果死活收敛不了。后来才发现,SMILES 是不唯一的——同一个分子可以有无数种写法。所以,正规化(Canonicalization)是必须的。

SMILES 正规化

正规化说白了就是让同一个分子的 SMILES 只有一种形式。RDKit 里有个函数叫 MolToSmiles,你传一个分子对象进去,出来的 SMILES 就是正规化后的。

from rdkit import Chem

mol = Chem.MolFromSmiles('CCO')
canonical_smiles = Chem.MolToSmiles(mol)
print(canonical_smiles)  # 输出:CCO

我记得有一次,我从三个不同的数据库下载同一个分子的 SMILES:

  • 数据库 A:c1ccccc1
  • 数据库 B:C1=CC=CC=C1
  • 数据库 C:c1ccc(cc1)

乍一看都不一样,但 RDKit 的正规化能帮我统一成 c1ccccc1。没有这一步,你后面的建模全是坑。

从 SMILES 到分子图

好了,现在字符串有了,怎么变成图?

分子图本质上就两个东西:节点和边。节点是原子,边是化学键。

每个原子需要一组特征。我常用的是:

特征 说明 示例
原子种类 C, N, O, S, 卤素等 one-hot 编码
原子度数 连接的键的数量 0, 1, 2, 3, 4
形式电荷 原子上的净电荷 -1, 0, 1
氢原子数 隐含的氢原子数量 0, 1, 2, 3
杂化类型 sp, sp2, sp3 one-hot 编码

每条边呢,特征更简单:

  • 键的类型:单键、双键、三键、芳香键
  • 是否在环里:0 或 1
  • 键的共轭状态:0 或 1

RDKit 里有一个 MolFromSmiles 函数,它返回的就是一个分子对象。从这个对象里,你可以提取出上面说的所有特征。我个人习惯写一个工具函数:

def mol_to_graph_data(mol):
    atoms = mol.GetAtoms()
    bonds = mol.GetBonds()
    
    node_features = []
    for atom in atoms:
        feat = [
            atom.GetAtomicNum(),
            atom.GetDegree(),
            atom.GetFormalCharge(),
            atom.GetTotalNumHs(),
            atom.GetHybridization()
        ]
        node_features.append(feat)
    
    edge_index = []
    edge_features = []
    for bond in bonds:
        i = bond.GetBeginAtomIdx()
        j = bond.GetEndAtomIdx()
        edge_index.append([i, j])
        edge_index.append([j, i])  # 无向图
        edge_features.append(bond.GetBondTypeAsDouble())
        edge_features.append(bond.GetBondTypeAsDouble())
    
    return node_features, edge_index, edge_features

注意:图神经网络一般用无向图。所以每条边你都得加两条——[i, j] 和 [j, i]。我曾经在项目初期没加对称边,结果模型学出来的特征全是错的。

其他分子编码格式

除了 SMILES,还有几种常见的分子编码。我做项目时都接触过,简单给你对比一下:

编码格式 优点 缺点 适用场景
SMILES 轻量,人类可读 不唯一,不保证完整性 日常开发,数据存储
InChI 唯一,层级化结构 字符串很长,人类难读 数据库索引,标准化
SELFIES 完全鲁棒,不会出错 相对较新,工具链不成熟 生成式模型

我个人觉得,做图神经网络,SMILES 加正规化就够用了。但你要是做分子生成,SELFIES 是个更好的选择——因为它的语法不会产生非法分子。

本章知识体系

下面这张图展示了从 SMILES 字符串到分子图结构的完整流程:

SMILES 到分子图:核心流程 SMILES 字符串 RDKit 解析 分子对象 原子特征 种类、度数、电荷、H数 键特征 类型、环内、共轭状态 分子图 正规化是保证一致性的关键步骤

RDKit 常用操作速查

下面这些是我平时用得最多的 RDKit 操作,记下来能省很多事:

from rdkit import Chem
from rdkit.Chem import AllChem, Descriptors

# 读取 SMILES
mol = Chem.MolFromSmiles('CCO')

# 获取 SMILES(正规化)
canon_smiles = Chem.MolToSmiles(mol)

# 获取分子量
mw = Descriptors.ExactMolWt(mol)

# 生成 3D 构象
mol = Chem.AddHs(mol)
AllChem.EmbedMolecule(mol, randomSeed=42)
AllChem.MMFFOptimizeMolecule(mol)

# 写入 SDF 文件
writer = Chem.SDWriter('output.sdf')
writer.write(mol)
writer.close()

核心总结:SMILES 只是起点。你得先正规化,再提取原子和键的特征,最后组装成图结构。这一步做好,后面的 GNN 才能跑得起来。

说实话,我刚开始搞图神经网络的时候,真是被这些 SMILES 的坑整得够呛。但 RDKit 这工具确实好用——你想想看,一个开源库能处理那么多化学格式,本身就是个奇迹。

最后提醒一句:别把 SMILES 直接当特征输入给神经网络。那种做法多半会翻车。老老实实转成图,才是正路。