SMILES 编码:从分子结构到一行字符串

做分子表示学习,你绕不开的一个东西,就是 SMILES。
说白了,它就是把化学结构写成一行 ASCII 字符。
看起来简单,但背后有不少门道。

为什么需要 SMILES?

在计算机里表达分子,最早大家用连接表、矩阵之类的东西。
虽然信息完整,但体积大、难处理、不能直接扔进文本搜索。
比如一个复杂分子,它对应的邻接矩阵可能是几百乘几百的大块头。

我刚开始做化学信息学的时候,最头疼的就是怎么高效存这些分子。
存成 MOL 文件吧,一个分子好几 KB;用 InChI 吧,字符串又长得离谱。
后来发现 SMILES,一行搞定,几 KB 变几十字节,舒服多了。

SMILES 的历史

SMILES 是 1980 年代由 David Weininger 在 US Environmental Protection Agency (EPA) 开发的。
初衷很朴素:找个轻量级的方式让化学家能手动输入分子。
后来 Daylight Chemical Information Systems 把它做成了标准。

现在的 SMILES 大致有三个版本:

版本 特点 我的看法
Daylight SMILES 最原始,规则简单 基本够用,但异构体处理弱
OpenSMILES 开源社区规范 我项目里用的就是这套
Canonical SMILES 确定性的唯一字符串 做分子匹配时必用

我的建议:如果你在做分子生成或对比,优先用 Canonical SMILES。否则同一个分子可能出来多种写法,那就会导致很多奇怪的 bug。

基本语法规则

SMILES 的核心思想只有一个:用原子符号表示节点,用标点符号表示键和结构。

原子表示

常规原子直接写元素符号,注意大写字母在前:

  • 碳:C
  • 氧:O
  • 氮:N
  • 氯:Cl(两个字母,C 大写)

遇到同位素或化合价异常的情况,就用方括号括起来:

  • 带电荷的原子:[Na+]
  • 同位素:[18O]
  • 异常价态:[C-](卡宾)

嗯,这里要注意:方括号不只是用来表示离子,只要原子的氢原子数跟默认规则不同,就得显式写出。
我有个学生写苯环时顺手写了个 C1=CC=CC=C1,结果解析出来氢原子数全错了 —— 因为碳默认连 4 个键,但环里默认连两个,剩下的两个氢自动补上。但如果你改成 [CH]1=[CH][CH]=[CH][CH]=[CH]1,意思就不一样了,每个碳显式指定只连一个氢。

键的表示

SMILES 的键表示非常直观:

键型 符号 示例
单键 - 或省略 CC(乙烷)
双键 = C=C(乙烯)
三键 # C#C(乙炔)
芳香键 : c1ccccc1(苯)

很多人刚学时有个困惑:为什么单键可以省略?
根据 SMILES 默认规则,相邻的两个非氢原子之间默认就是单键。你想想看,如果你每个单键都写个 -,那字符串会变得非常啰嗦。CC 比 C-C 短得多,读起来也快。

关键点:SMILES 的默认键是单键。双键、三键、芳香键必须显式写出。

分支与环

SMILES 用圆括号表示分支:

C(C)C           # 丙烷,中间的碳连接一个甲基
CC(C)(C)C       # 2,2-二甲基丙烷,中间的碳上连两个甲基
C(=O)O          # 甲酸,碳双键氧,再单键连羟基

环用数字标记,相同数字表示成环:

C1CCCCC1        # 环己烷
c1ccccc1         # 苯环(小写 c 表示芳香碳)
C1=CC=CC=C1      # 环己二烯(大写 C 表示脂肪族碳)

我曾经对接过一个分子数据库,里面大量分子用了多个环标签,比如 C1CC2CCCC1C2。
当时脚本解析环标签的逻辑写错了,导致双环分子全部无法正确还原。排查了一整天才发现是环编号解析的 bug,教训很深。

简单分子示例

来看几个例子,感受一下 SMILES 的简洁:

分子名称 分子结构 SMILES
乙醇 CH₃CH₂OH CCO
乙酸 CH₃COOH CC(=O)O
苯 C₆H₆ c1ccccc1
甲苯 C₆H₅CH₃ Cc1ccccc1
咖啡因 C₈H₁₀N₄O₂ Cn1c(=O)c2c(ncn2C)n(C)c1=O

乙醇写成 CCO,看着是不是就像在描述“碳-碳-氧”的链条?
乙酸用了括号表示羧基分支。咖啡因那串看起来复杂,但你把它拆开看,就是几个环和甲基的组合。

生成过程中有个细节:写 SMILES 时顺序并不唯一。
比如乙醇你也可以写成 OCC,从氧开始写,语义上也是对的。
但你想想看,如果我们用 OCC 和 CCO 表示同一个分子,做字符串匹配时就对不上。
所以就有了 Canonical SMILES —— 规定一种唯一的写法,方便比较。

避坑指南:我在项目中遇到过一个问题:直接用 SMILES 字符串做分子去重。
同一个分子,不同库提供者给的 SMILES 可能不一样。
比如乙醇可能是 CCO,也可能是 OCC。
解决办法:始终用 RDKit 生成 Canonical SMILES 再比较。

从化学结构到 SMILES 的生成流程

下图是我自己画的一个流程,帮你理解从分子结构到 SMILES 的转换过程:

SMILES 生成流程 分子结构(2D/3D) 选择起始原子与遍历顺序 判断键类型(单/双/三/芳香) 处理分支与环(括号+数字) 规范化处理后输出字符串 遍历策略影响 最终字符串 但结构不变

这个流程你看着也许觉得复杂,
但实际写代码时,RDKit 一个函数就能搞定:

from rdkit import Chem

# 从 SMILES 创建分子对象
mol = Chem.MolFromSmiles("CCO")

# 得到 Canonical SMILES
canonical = Chem.MolToSmiles(mol)
print(canonical)  # 输出:CCO

两行代码就完成了从字符串到分子对象、再回到字符串的全过程。
但背后原理你如果不懂,项目里遇到奇怪的 bug 就不好排查了。
比如芳香环识别错误、同位素符号被忽略、手性中心丢光 —— 这些都是我在实际项目中踩过的坑。

注意:SMILES 不记录氢原子个数。氢原子由化合价自动推断。
如果你需要显式处理氢原子,必须使用方括号标记,比如 [H]C([H])([H])[H](甲烷)。
但默认情况下,你写 C 就代表甲烷,系统自动补足 4 个氢。

好了,这一章就到这。
SMILES 看起来简单,但它是一切分子表示学习的起点。
后面你要处理的图神经网络、分子指纹、反应预测,全都需要先理解这串字符串到底在说什么。