第四讲:分子表示方法

好,到这一章,我们要聊一个绕不过去的基础问题:怎么把分子喂给计算机?

你想想看,计算机又不认识苯环。它只看得到 0 和 1。如果我们想建 ADMET 模型,第一步就是得把分子结构转成它能理解的语言。这部分我叫它“分子表示方法”。我用过很多种表示法,每种都有自己的脾气。

我个人习惯把这四种记牢:SMILES、InChI、分子指纹、分子描述符。接下来一个个说。

分子表示方法知识体系 分子表示方法 SMILES InChI 分子指纹 ECFP / MACCS 分子描述符 RDKit / Mordred 位向量 / 计数指纹 理化性质 / 拓扑指数 外部验证中的关键: 训练集与验证集必须使用完全相同的表示方法

SMILES —— 最常用的分子字符串

SMILES 说白了就是一行字符串。你写一个CCO,它就知道是乙醇。有人觉得这玩意儿很简单,其实它门道挺多。

我记得刚入行那会儿,我在一个项目里遇到过大坑:SMILES 不规范。同一个分子,两个人写的 SMILES 不一样,导致后续建出来的模型完全走偏。比如苯环,有人写c1ccccc1,有人写C1=CC=CC=C1。程序会认为它们不一样,除非你做标准化。

SMILES 几个核心规则

  • 原子用元素符号表示,C 是碳,O 是氧,N 是氮;小写 c 表示芳香族碳。
  • 单键隐含,双键用=,三键用#。
  • 分支用括号包裹,比如CC(=O)O 是乙酸。
  • 环结构用数字标记开环和闭环位置。

我在项目中常用 RDKit 来标准化 SMILES。别嫌麻烦,这一步能省后面很多 debug 时间。

from rdkit import Chem

smiles_raw = "C1=CC=CC=C1"
mol = Chem.MolFromSmiles(smiles_raw)
smiles_canonical = Chem.MolToSmiles(mol)
print(smiles_canonical)  # 输出:c1ccccc1

小技巧:外部验证前,记得把训练集和测试集的 SMILES 都过一遍 canonicalization。不然模型可能学到的只是写法差异,不是化学本质。

InChI —— 给分子发身份证

如果你觉得 SMILES 有点“随性”,那 InChI 就是规范化的代表。它由 IUPAC 制定,目的是让每个唯一结构只有一个 InChI 字符串。

它的特点是分层的。前面告诉你连接性,后面告诉你立体化学,再后面告诉你同位素。我给你看一个例子你就明白了。

InChI=1S/C2H6O/c1-2-3/h3H,2H2,1H3
# 乙醇的 InChI
# 第一部分:分子式 C2H6O
# 第二部分:连接性 c1-2-3 → C-C-O
# 第三部分:氢原子位置

但是要注意,InChI 虽然标准化,但长度通常比 SMILES 长得多。我一般只在做数据库去重或结构匹配时用它。ADMET 项目里,如果你需要跨库比对化合物,InChI 非常可靠。

避坑:我曾经遇到过 InChI 在极端大的分子上生成失败的情况。另外,InChIKey(哈希版)虽然短,但可能有碰撞风险,别拿来做唯一标识符。

分子指纹 —— 把结构变成 0/1 向量

好,现在我们到了建模最喜欢的部分:分子指纹。为什么机器学习模型喜欢它?因为它是一长串 0 和 1,直接能当特征向量。

我常用的指纹有两类:ECFP 和 MACCS。

指纹名称原理长度适用场景
ECFP (Extended-Connectivity Fingerprint)以每个原子为中心,扩展周围半径的拓扑片段进行哈希通常 1024 或 2048 位活性预测、相似性搜索
MACCS (Molecular ACCess System)166 个预定义的结构碎片166 位快速过滤、分类规则检查

ECFP 是“数据驱动”的——你不需要预定义任何片段,程序自动从分子中学习。我在一个先导化合物优化项目里就用它做相似度打分,效果很不错。MACCS 则更偏向“专家经验”,166 个片段的设定有点老,但胜在稳健。

代码生成例子:

from rdkit import Chem
from rdkit.Chem import AllChem

mol = Chem.MolFromSmiles("CCO")
# ECFP4, 半径2, 1024位
ecfp = AllChem.GetMorganFingerprintAsBitVect(mol, radius=2, nBits=1024)
print(ecfp.ToBitString()[:64])  # 只看前64位

# MACCS指纹
from rdkit.Chem import MACCSkeys
maccs = MACCSkeys.GenMACCSKeys(mol)
print(len(maccs))  # 166位

我个人习惯:在训练集上确定好指纹参数后,在外部验证集上千万不要改。半径、长度、哈希种子这些,一旦变了一点,整个特征空间就变了。

分子描述符 —— 用数字说话

如果说指纹是“细粒度”的 0/1 特征,那么描述符就是“宏观属性”。分子量、LogP、氢键供体数量、可旋转键数量……这些都是描述符。

RDKit 内置了几百个描述符,基本上覆盖了大部分常用的理化性质。Mordred 更疯,有超过 1800 个描述符。但别急着全用上——特征多了不一定好。

避坑:我曾经在一个项目里贪心用了全部 1800 个 Mordred 描述符,结果外部验证 R² 比训练集低了 0.3。过拟合了。后来老老实实只选了 50 个与药代性质强相关的描述符,效果反而好了。

from rdkit import Chem
from rdkit.Chem import Descriptors

mol = Chem.MolFromSmiles("CCO")
mw = Descriptors.MolWt(mol)
logp = Descriptors.MolLogP(mol)
hbd = Descriptors.NumHDonors(mol)

print(f"MW: {mw:.1f}, LogP: {logp:.2f}, HBD: {hbd}")

# 使用Mordred(需要安装)
from mordred import Calculator, descriptors
calc = Calculator(descriptors, ignore_3D=True)
vals = calc(mol)
print(vals['SLogP'])

总结一下我个人的选择逻辑:

  • 想做快速相似度搜索?用 ECFP 位向量。
  • 想做结构过滤或规则检查?用 MACCS 或 SMARTS。
  • 想建 QSAR 模型?用 RDKit 选一批物化描述符,再配合少量指纹。
  • 想做交叉库去重或结构唯一性检查?用 InChI。

不管选哪种表示法,记住一条铁律:外部验证集和训练集的分子表示流程必须完全一致。我曾经见过有人训练集用 Mordred 2.0,验证集用 Mordred 1.0,结果算出来的描述符值都不一样——这模型根本没法验证。嗯,说到这,你应该明白为什么我会把这章放在外部验证前面了吧?