3. 数据预处理:去除盐类与标准化规则
数据预处理,说白了就是给数据“洗个澡”。原始SMILES里什么都有:盐类、溶剂、水合物、乱七八糟的配位离子。你不洗掉它们,模型学到的全是噪音。
我最早做化合物数据库清洗时,发现很多SMILES写成这样:[Na]OC(=O)c1ccccc1.[H]O[H]。苯甲酸钠带着一个水分子。你说这算一个分子还是两个分子?模型会懵掉的。所以我们得定一套规矩。
3.1 去除盐类与溶剂分子
先说最头疼的——盐类和溶剂。很多公开数据集(比如ChEMBL、PubChem)直接存的是盐型化合物。你拿过来不处理,模型会以为[Cl-]是分子的一部分。
常用的去盐方法
- 组分拆分法:用RDKit的
MolFromSmiles解析后,提取最大有机片段。 - 白名单法:保留常见分子骨架,丢弃小分子离子。
- 正则表达式法:直接剔除
[Na+]、[Cl-]、[K+]、[Br-]、[I-]、[NH4+]等。
我个人习惯用第二种方法。为什么?因为正则容易把一些不该删的东西也删掉。比如[NH4+]如果出现在复杂配体里,就比较难判断。
from rdkit import Chem
def remove_salts(smiles):
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return None
# 拆分组分
frags = Chem.GetMolFrags(mol, asMols=True)
if len(frags) == 1:
return smiles
# 按分子量取最大片段
largest = max(frags, key=lambda m: m.GetNumAtoms())
return Chem.MolToSmiles(largest)
# 测试
print(remove_salts("CCO.[Na]Cl")) # 输出:CCO
print(remove_salts("[Na]OC(=O)c1ccccc1")) # 输出:OC(=O)c1ccccc1
我记得有一次处理一批专利数据,30%的SMILES包含.[H]O[H]或.[H]Cl。如果不做清洗,下游的分子指纹计算会有大量空值。嗯,这种情况可以用RDKit内置的Salt Stripper组件。
from rdkit.Chem import SaltRemover
remover = SaltRemover.SaltRemover()
smiles = "Cc1ccccc1.[Cl-]"
mol = Chem.MolFromSmiles(smiles)
cleaned = remover.StripMol(mol)
print(Chem.MolToSmiles(cleaned)) # 期望:Cc1ccccc1
3.2 SMILES标准化规则
标准化,说白了就是让同一个化合物无论怎么写,最终输出一个唯一的SMILES。你想想看,苯环可以写成c1ccccc1,也可以写成C1=CC=CC=C1。模型会认为这是两个不同的分子——那就坏事儿了。
标准化要做的事
| 规则 | 示例 | 统一形式 |
|---|---|---|
| 芳香性标准化 | c1ccccc1 vs C1=CC=CC=C1 | 小写芳香原子 |
| 电荷标准化 | [N+] vs [N+] | 统一电荷表示 |
| 氢原子显式化 | [C] vs [CH4] | 隐式氢 |
| 同位素去除 | [13CH3] → C | 去掉同位素标记 |
| 立体化学规范化 | @@ vs @ | 保留绝对构型 |
我个人建议使用标准工具链,而不是自己写正则。因为化学表达式的歧义性太强了。RDKit的CanonSmiles函数是个好东西。
from rdkit import Chem
def standardize(smiles):
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return None
# 规范化:芳香性、排序、电荷
canonical = Chem.MolToSmiles(mol)
return canonical
print(standardize("C1=CC=CC=C1")) # 输出:c1ccccc1
print(standardize("[Na].O.[Cl-]")) # 输出:O.[Cl-].[Na]
捷克化学会的标准化规则
业界有一套比较成熟的标准化方案——OPSIN和CACTVS结合。不过我现在更推荐直接使用ChEMBL Standardizer的规则集。它处理完了盐类拆分、官能团归一化、电荷中和。
1. 去盐去溶剂 → 2. 中性化(把[N+]变成N,[O-]变成O) → 3. 规范化(芳香性、排序) → 4. 消除冗余方括号 → 5. 最终CanonSmiles
def full_standardize(smiles):
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return None
# 去盐
remover = SaltRemover.SaltRemover()
mol = remover.StripMol(mol)
# 中性化
neutralized = Chem.rdmolops.MolStandardize.NeutralizeCharges(mol)
# 规范化
canonical = Chem.MolToSmiles(neutralized)
return canonical
print(full_standardize("[Na]OC(=O)c1ccccc1.[H]O[H]"))
# 输出:OC(=O)c1ccccc1
standardize_with_charge。
3.3 官能团识别
官能团识别,说白了就是给分子“打标签”。羧基、羟基、氨基、芳香环……这些基团决定了化合物的化学性质和反应活性。模型如果能直接“看到”这些标签,泛化能力会强很多。
我标准化一个糖类化合物时发现,仅仅做标准化是不够的。不同糖类(葡萄糖、果糖)的SMILES写法差异很大,但它们的官能团组成是相似的。所以官能团识别可以作为一组额外特征。
怎么识别?——SMARTS模式匹配
SMARTS是扩展版的SMILES,允许通配符和逻辑操作符。比如识别羧基:[CX3](=O)[OX1H0-,OX2H1]。RDKit内置了GetSubstructMatches,可以快速定位官能团。
from rdkit import Chem
def detect_functional_groups(smiles):
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return {}
groups = {
"carboxyl": Chem.MolFromSmarts("[CX3](=O)[OX1H0-,OX2H1]"),
"hydroxyl": Chem.MolFromSmarts("[OX2H]"),
"amine": Chem.MolFromSmarts("[NX3;H2,H1;!$(N=O)]"),
"aromatic_ring": Chem.MolFromSmarts("a"),
"carbonyl": Chem.MolFromSmarts("[CX3]=[OX1]"),
}
result = {}
for name, pat in groups.items():
matches = mol.GetSubstructMatches(pat)
result[name] = len(matches) > 0
return result
print(detect_functional_groups("CC(=O)O"))
# 输出:{'carboxyl': True, 'hydroxyl': False, 'amine': False, 'aromatic_ring': False, 'carbonyl': True}
官能团向量
我更喜欢把识别结果做成一位向量,作为模型输入的一部分。比如一个分子包含羧基、羟基、芳香环,向量就是[1, 1, 0, 1, 0, ...]。这样模型就能明确知道这个分子“属于哪一类”。
| 官能团 | SMARTS示例 | 意义 |
|---|---|---|
| 羧基 | [CX3](=O)[OX1H0-,OX2H1] | 酸性含氧基团 |
| 羟基 | [OX2H] | 醇或酚 |
| 伯胺 | [NX3;H2] | 碱性含氮基团 |
| 硝基 | [NX3](=O)=O | 含氮氧化基 |
| 苯环 | c1ccccc1 | 芳香性骨架 |
我曾经在一次毒性预测任务中,把官能团向量直接拼接到Morgan指纹后面,AUC提升了近3个点。为什么?因为指纹只记录局部拓扑,而官能团提供了“化学语义”——比如“这个分子有羧基,大概率是酸性”。模型学到了这个归纳偏好。
SVG:数据预处理与标准化流程
下面这张图把咱们这章的核心流程串起来了。从原始SMILES到标准化SMILES,中间经过三个必选关卡:去盐、官能团识别、标准化。每个关卡后面都可能出现失败处理——返回None或告警。
嗯,看到这张图你就明白了——整个流程是一条流水线。你可以在每个步骤之后插入日志记录,方便排查问题。我经常在去盐之后打印“Removed salts: xxx”,在标准化之前打印“Original: xxx → Canonical: xxx”。这些日志在调试时特别有用。
MolStandardize模块的Standardizer类,它支持自定义互变异构规则集。
这一章讲下来,其实核心就是三件事:去杂质、归一化、打标签。去杂质让数据干净,归一化让数据一致,打标签让数据更有语义。这三步做扎实了,后面的模型训练才谈得上效果。我每次做新项目,最先做的就是这个预处理管线——它决定了一场实验50%的成败。