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 字符串到分子图结构的完整流程:
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 直接当特征输入给神经网络。那种做法多半会翻车。老老实实转成图,才是正路。