3. 分子序列表示与Tokenization

做ADMET预测,首先得把分子“喂”给模型。但Transformer本身不认识化学结构,它只认序列。所以,咱们得先把分子图或者三维构象,翻译成一串字符串。

我最早接触这行时,觉得这不就是给分子起个名字嘛,能有多复杂?结果被狠狠教育了一顿。不同表示法,信息密度、容错率、语法约束,差异巨大。选错了,模型可能学到一堆噪声。

3.1 SMILES:最常用的“化学手语”

SMILES(Simplified Molecular Input Line Entry System)是这行的老前辈了。我个人最常用的表示法就是它。它把分子结构用ASCII字符线性化。

举个例子,乙醇的SMILES写出来就是 CCO。苯环是 c1ccccc1。核心规则是:用元素符号表示原子,用数字标记环的起点和终点,用括号表示分支。

但SMILES有个大坑——它不唯一。

同一个分子,可以写出好几种合法的SMILES。比如丙酮,既可以是 CC(=O)C,也可以是 C(C(=O)C)。我在项目中遇到过,同一个分子在不同数据库里的SMILES不一样,导致模型训练时,同一个结构被当成了两个不同的样本。这非常糟糕。

所以,做训练前,我建议先做标准化,最常用的是用RDKit的CanonSmiles()函数,把分子转成标准SMILES。

from rdkit import Chem

# 原始SMILES可能不唯一
smiles = ["CC(=O)C", "C(C(=O)C)", "CC(C)=O"]

# 转为标准形式
canon_smiles = [Chem.MolToSmiles(Chem.MolFromSmiles(s)) for s in smiles]
print(canon_smiles)
# 输出: ['CC(C)=O', 'CC(C)=O', 'CC(C)=O']

避坑指南:我曾经拿库里的SMILES直接训练,结果模型在验证集上表现极好——后来发现,库里有50%的重复分子,只是因为SMILES写法不同,被当成了不同类别。标准化是第一步,千万别偷懒。

3.2 SELFIES:语法纠错大师

SMILES虽然好用,但对语法错误极其敏感。多一个括号,少一个数字,整个分子就废了。而且,SMILES语法允许很多“非法”组合,比如 CCCC 是丁烷,但 CCCCCCCC... 在字面上可能对应一个不稳定的超长碳链,这在物理上不可能存在。

SELFIES(SELF-Referencing Embedded Strings)就是来解决这个问题的。

它设计了一个约束性更强的语法。每个“词”都包含分支或环的起止信息,所有可能的SELFIES序列都对应一个合法分子(即使化学上不稳定)。说白了,它是100%化学可解码的。

我拿乙醇举个例子:SMILES的CCO,对应SELFIES是 [C][C][O]。看着啰嗦,但机器解析起来容错率极高。

# 用自注意力机制随机生成一个SMILES
# "C1=CC=CC=C1" 是苯,但如果写成 "C1=CC=CC=C"(少一个数字),
# 解析器直接报错。

# 但SELFIES可以避免这个问题。
# 如果token序列是 [C][C][O],那一定对应乙醇。
# 如果序列是 [C][Ring1][C][O],它自动闭合环,不会产生语法错误。

我自己在实际项目中,如果模型需要做分子生成(比如生成新的候选分子),我几乎只用SELFIES。因为它保证了模型输出的每个字符串都能还原成一个真实的分子结构。SMILES生成出来的东西,有一半可能是废物。

我的经验:SELFIES的词表比SMILES大(大约45个基础token vs SMILES的15-20个),但序列长度通常也更长。对于Transformer来说,更长序列意味着更大的计算开销。你需要根据任务做权衡。

3.3 InChI:化学界的标准域名

InChI(International Chemical Identifier)是IUPAC推出的“标准化化学标识符”。它不像SMILES那样为了简洁而生,而是为了唯一性和互认性。

它的结构长这样:

InChI=1S/C2H6O/c1-2-3/h3H,2H2,1H3

这串东西包含了:版本号(1S)、分子式(C2H6O)、连接表(/c1-2-3)、氢原子信息(/h...)等等。层与层之间用斜杠分隔。

InChI的优势是它从生成算法上就是唯一的。同一个分子在世界任何地方用InChI软件算出来,结果都一样。我在做分子数据库合并时,经常用InChI作为去重的黄金标准。

不过它也有缺点:太长、可读性差、包含冗余信息。而且,它包含的立体化学信息有时候过于详细,对于ADMET预测来说,可能是一些噪声。

表示法 核心特点 优势 缺点
SMILES 线性ASCII字符串 简洁、可读性好、生态工具丰富 不唯一、语法脆弱、存在“伪造”分子
SELFIES 约束语法字符串 100%可解码、生成任务首选、语法鲁棒 序列较长、词表稍大、解析速度略慢
InChI IUPAC标准层状结构 绝对唯一、适合数据库去重、包含立体化学 可读性差、长度长、信息冗余

简单总结:

  • 做分类/回归预测(ADMET),SMILES标准化后足矣。
  • 做分子生成,首选SELFIES。
  • 做数据库整合,InChI是硬道理。

3.4 Tokenization:把字符串变成模型能啃的数字

表示法选好了,下一步就是把字符串变成Token ID序列。Transformer的输入是整数序列,你不能直接把“CCO”扔进去。

Tokenization的关键是什么? 词表的设计和切分粒度。

我常用的策略分三种:

  1. 原子级(Atom-level):把每个原子符号(如C、N、O、[、])看作一个token。SMILES中就是单字符或双字符(如Cl、Br)。词表很小(20-30个),但序列长度非常长。比如一个60个原子的分子,SMILES可能有80-100个token。
  2. 子结构级(Substructure-level):用BPE(Byte Pair Encoding)或WordPiece,从常见SMILES子串中学习个词表。比如“C=C”作为一个token,“C=O”作为一个token。序列长度可以缩短30%-50%,但词表变大(1000-5000个)。
  3. 片段级(Fragment-level):直接用预定义的化学片段(如官能团、环系统)作为token。这需要外部知识,但非常适合做可解释性分析。

我个人习惯在ADMET任务中,先用Atom-level的SMILES,简单暴力。如果模型收敛太慢或内存不够,再换BPE。

# 一个简单的基于字符的Tokenization示例
import re

smiles = "CCO"
# 先处理双字符元素(Cl、Br、[nH]、[O-]等)
# 再用正则匹配单字符
pattern = r'\[.*?\]|Br?|Cl?|Si?|Se?|Na?|Mg?|Al?|Si?|P[H]?|S[^e]?|F|I|B|C|N|O|H'

tokens = re.findall(pattern, smiles)
print(tokens)
# 输出: ['C', 'C', 'O']

# 用pre-tokenized序列构建词表
vocab = {token: i for i, token in enumerate(set(tokens))}
token_ids = [vocab[t] for t in tokens]
print(token_ids)
# 输出: [0, 0, 1]

一个我曾经踩过的坑:用BPE对SMILES做Tokenization时,不要用自然语言的分词器直接套。比如“Cl”在SMILES中是氯元素,但分词器可能把它拆成“C”和“l”。一定要用化学专用的正则表达式先做pre-tokenization。否则你会把“CCCl”误解为“CC”+“Cl”,而不是“C”+“C”+“Cl”。

下面这张图总结了三种分子表示法和Tokenization的关系,以及它们在Transformer流水线中的位置。

分子序列表示与Tokenization流程图 输入分子结构 SMILES CC(=O)C SELFIES [C][C][O] InChI InChI=1S/... Tokenization 词表构建 → BPE / Atomic / Fragment Transformer Embedding Layer Token ID序列 [2, 5, 5, 3] 长度、词表大小 直接影响模型开销

一个实用技巧:做Tokenization时,词表里最好加一个<MASK> token(用于MLM预训练),以及一个<PAD> token(用于batch padding)。这两个token在ADMET任务中几乎是必备的。我建议把PAD token的ID设为0,这样对大多数框架(PyTorch的embedding)最友好。