3. SMILES表示法:SMILES语法、分子编码、数据预处理

好,咱们今天聊聊SMILES。这东西说白了就是分子的身份证号,而且是那种方便电脑读的条形码版本。我刚开始做分子生成时,第一反应是:哎?把分子画成图不就行了?后来才明白,想让RNN、LSTM这些序列模型去理解分子,就得把图结构掰成一串字符——SMILES就是这个「掰直」的过程。

3.1 SMILES语法:从原子到字符串

SMILES(简化分子线性输入规范)就是把分子结构转成一行ASCII字符。你可以把它想象成用文字描述一个化学结构:哪个原子连哪个原子,环怎么闭合,分支怎么分叉,一气呵成。

3.1.1 基本元素表示

  • 原子:用元素符号表示,如C、N、O、S。有机元素默认小写?不对,字母要大写。默认大写后面跟的小写字母才是元素全称的第二位,比如Cl、Br、Fe。
  • 氢原子:默认隐去。比方说甲烷就写C,而不是C4——写出来反而没人认。
  • 键接方式:单键用-,双键用=,三键用#。不过大多数情况下单键可以不写,直接连着写就行。
  • 分支:用圆括号括起来。括号内的结构表示连在前一个原子上。
  • 环:用数字表示环的闭合位置,同一个数字出现两次就代表闭合。

举个例子,乙醇可以写成CCO,简单粗暴。因为C-C-O连起来就是乙醇的主链。丙酮呢?CC(=O)C,这个等号和括号告诉模型:中间那个碳上还连着一个双键氧。

避坑指南:我在项目中遇到过一批SMILES数据,有些分子里边有同位素或手性标记,比如[13CH3]、[C@@H]。这些方括号不能省!你要是图省事直接去掉了,那分子结构就100%错了,模型学出来的全是假结构。

3.1.2 常见SMILES示例

# 简单分子
乙醇: CCO
乙酸: CC(=O)O
苯: c1ccccc1
甲苯: Cc1ccccc1
阿司匹林: CC(=O)Oc1ccccc1C(=O)O

# 带环的复杂分子 (咖啡因)
CN1C=NC2=C1C(=O)N(C(=O)N2C)C

看到这个咖啡因的例子,你可能会说:这跟平时画的分子式差太远了!确实,但这就是SMILES的厉害之处——它可以表达任意复杂的分子拓扑结构。

3.2 分子编码:从字符串到向量

SMILES是字符串,可咱们的LSTM吃的是向量。怎么把字符串变成向量?我个人的习惯是先做分词,再映射成整数,最后转成one-hot或者embedding向量。

3.2.1 分词策略

分词就是把SMILES字符串拆成一个个基本单元。听起来简单,但坑不少。比如Cl,如果你按字符拆,那就被拆成了C和l,原子就被破坏了。

我常用的分词方法是:

  • 先用正则把所有双字符元素(如Cl、Br、Si)找出来,单独标记
  • 再把单字符元素和大写字母开头的官能团做一次扫描
  • 特殊符号如括号、等号、数字、@、#,各自视为独立Token
我推荐的方式:用RDKit的SMILES解析器做分词,因为RDKit有完整的原子级分词逻辑。很多人自己写正则,很容易漏掉[Te]、[Se]这种冷门元素。

3.2.2 Token到整数映射

假设你的训练集有100万条SMILES,首先你得统计出总共有多少种Token。然后建一个字典:每个Token对应一个整数。

# Token计数示例(基于RDKit分词)
vocab = {
    'C': 0,
    'c': 1,
    'O': 2,
    'N': 3,
    '(': 4,
    ')': 5,
    '=': 6,
    '#': 7,
    '1': 8,
    '2': 9,
    'Cl': 10,
    'Br': 11,
    ...
}

嗯,这里要注意:有些SMILES里出现了Na(钠),如果你把Na当成了N和a,那就会出大问题。所以分词前一定要做元素周期表全量检查。

3.2.3 向量化方式对比

编码方式 空间复杂度 信息密度 适用场景
one-hot 高(每个Token都对应一个维度) 低(非零维度只有一个1) 小规模词表(<200)
Label Encoding 低(整数占用1维) 低(但可能引入次序偏差) 配合embedding层使用
Embedding 中(可自定义维度,常用128-512) 高(学到语义关联) LSTM/Transformer等序列模型

说实话,做LSTM生成化合物时,我最常用的策略是Label Encoding+Embedding层。为什么?因为one-hot词表大了后内存直接爆炸。我记得有次建了一个2000+Token的词表,用one-hot映射,100万条数据跑下来,内存直接飚到128G。后来换成Embedding,32G机器跑得飞起。

3.3 数据预处理:清洗与标准化

数据预处理这事,我做过无数次,也踩过无数坑。说白了,ML里「垃圾进,垃圾出」,SMILES处理更是如此。化学数据跟自然语言不一样——自然语言里写错一两个字还能猜,SMILES写错一个括号或者漏一个氢,那分子就完全不一样了。

3.3.1 预处理流程

  1. 格式标准化:所有SMILES统一用正则清洗多余空格、换行、制表符。我见过某个数据集的SMILES开头带隐形字符,直接导致模型无法预测。
  2. 去重与排序:同一个分子可能对应多个SMILES表示,去重后保留规范的SMILES(用RDKit的Canonicalize处理)。
  3. 长度过滤:设置最大长度阈值。比如我只保留20-120个字符的SMILES。太短可能是碎片或盐,太长可能是高分子聚合物——LSTM处理不了超长序列。
  4. 原子限制:只保留常见原子(C、N、O、S、P、F、Cl、Br、I等)。出现其他原子的直接过滤。
  5. 盐分离:像SMILES里含有[Na]或者[Cl-]这种离子的,通常要分离掉,只保留有机分子本体。

高能预警:我曾经做过一个项目,数据里混入了不带电荷的SMILES和带电荷的SMILES。模型竟然学会了「产生带电分子」这种不稳定的输出。后来我才发现,是因为某些预处理时没有统一用RDKit做电荷中性化处理。

3.3.2 一个简约但有效的预处理代码模板

from rdkit import Chem
from rdkit.Chem import Descriptors, SaltRemover

def preprocess_smiles(smiles, max_len=120):
    # 步骤1:解析
    mol = Chem.MolFromSmiles(smiles)
    if mol is None:
        return None  # 不合法的直接扔掉
    
    # 步骤2:去盐
    remover = SaltRemover.SaltRemover()
    mol = remover.StripMol(mol)
    
    # 步骤3:中性化
    mol = Chem.rdmolops.SanitizeMol(mol)
    
    # 步骤4:标准化SMILES
    canonical = Chem.MolToSmiles(mol, isomericSmiles=False)
    if len(canonical) > max_len:
        return None  # 太长,扔掉
    
    # 步骤5:过滤特殊原子(只留C,N,O,S,P,F,Cl,Br,I)
    allowed_atoms = {6,7,8,16,15,9,17,35,53}
    for atom in mol.GetAtoms():
        if atom.GetAtomicNum() not in allowed_atoms:
            return None
    return canonical

这个模板我一直在用,稍微调一下就可以做到80%的预处理任务。但是你要是遇到一些奇奇怪怪的SMILES(比如带有同位素、手性标记、价键异常),那就得再补充自定义过滤逻辑了。

3.4 SMILES到分子图的映射关系

我觉得很有必要给大家画一张图,帮你们梳理一下SMILES在整个LSTM生成流程里的定位。

SMILES在LSTM生成流程中的角色 原始分子(3D/2D结构) SMILES字符串 Token序列 → 向量 LSTM模型训练/生成 生成新SMILES字符串 新分子(3D/2D结构) 关键点: 1. SMILES是分子结构与LSTM之间的「桥接层」 2. 预处理的好坏直接影响生成质量 3. 规范SMILES是训练的前提

这张图清晰地展示了:我们输入的是分子3D结构,但真正进LSTM的是经过分词的SMILES向量序列。生成时走反向:LSTM输出Token序列,再拼接成SMILES字符串,最后还原成新的分子结构。预处理就夹在中间,如果这个环节出了纰漏,后面一步错步步错。

3.5 关于SMILES表示法的几条实战建议

讲到这里我归纳几点,都是我自己踩过的坑,大家拿小本本记一下:

  • 不要相信公开数据集里的所有SMILES。不管是从ChEMBL、PubChem还是ZINC下载的,至少跑一遍RDKit的MolFromSmiles检查,不过滤直接丢掉。
  • 建立词表时一定要加入、、、四个特殊Token。我第一版模型就没加和,结果模型永远不知道在哪停止生成。
  • 做embedding时维度的选择:我一般选128维。太低可能学不到化学语义,太高计算量大但收益边际递减。
  • SMILES增广(augmentation):同一个分子可以通过不同的原子开始位置生成不同的SMILES字符串。比如乙醇可以写成CCO,也可以写成OCC。在训练时用这些不同的表示,能让模型学到更好的鲁棒性。
小技巧:在预处理阶段,把SMILES按照长度分组(如40-60、60-80、80-100),然后每个批次里只取同一长度组的SMILES。这样LSTM就不必为每个样本都做不同长度的padding,训练效率有明显提升。