4、SMILES 预处理常见问题:盐、溶剂、异构体、片段化处理
做 SMILES 预处理这几年,我踩过不少坑。很多看起来干净的分子数据集,一跑就出各种妖蛾子。其实说白了,大部分问题就出在这四个方面:
- 盐和溶剂混在里面没去掉
- 异构体标记不一致
- 多组分点号把混合物当成了纯化合物
- 或者分子本身就是个杂乱片段
嗯,今天我就把这几类问题的处理思路和实战代码一次性讲透。你想想看,如果模型读到的 SMILES 是 CC(=O)O.[Na+].[OH-],按字符序列它会把醋酸根、钠离子和氢氧根当成一个序列去学 —— 这哪里是学分子,分明是在学一锅乱炖。
4.1 盐与溶剂的去除
我个人习惯,拿到数据集第一件事:先把盐和溶剂筛掉。为什么?因为我们要建模的是活性分子的结构与性质关系,金属离子、水分子这些小碎屑只会引入噪声。
我在处理一个激酶抑制剂数据集时发现,里面大量分子带有 [Na+]、[Cl-]、O(水)、CCO(乙醇)等溶剂残留。直接用 RDKit 的 GetSaltMol 可以搞定分离。
实战代码:
from rdkit import Chem
from rdkit.Chem import SaltRemover
smiles_list = ["CC(=O)O.[Na+]", "c1ccccc1.CCO", "CCOC(=O)c1ccccc1.[Br-]"]
salt_remover = SaltRemover.SaltRemover()
for smi in smiles_list:
mol = Chem.MolFromSmiles(smi)
if mol is None:
continue
core_mol = salt_remover.StripMol(mol, dontRemoveEverything=True)
core_smi = Chem.MolToSmiles(core_mol)
print(f"原始: {smi:30s} -> 核心: {core_smi}")
dontRemoveEverything=True 这个参数很关键。如果没有它,遇到纯盐(比如 [Na+].[Cl-])它会返回空分子,直接把样本丢了。加上这个参数后,若剥离后分子为空,返回 None。
4.2 溶剂过滤:建立一个常见溶剂名单
我常用的做法是建一个常见溶剂 SMARTS 列表,然后用 GetSubstructMatch 判断。举个例子,水、甲醇、乙醇、DMSO、丙酮这些背景溶剂,发生率极高。
solvent_smarts = [
"[OH2]", # 水
"[CH3][OH]", # 甲醇
"CCO", # 乙醇
"CS(=O)C", # DMSO
"CC(=O)C", # 丙酮
]
def is_solvent_only(mol):
if mol is None:
return True
for smart in solvent_smarts:
ss = Chem.MolFromSmarts(smart)
if mol.HasSubstructMatch(ss) and mol.GetNumAtoms() <= ss.GetNumAtoms() + 2:
return True
return False
这里有个坑要提醒你:溶剂过滤不能只看是否匹配子结构,还要看分子总原子数。我以前被这个坑过 —— 一个大分子里恰巧含乙醇的片段,结果被误杀。
4.3 异构体处理:标准化与去除
异构体标记是另一个容易忽略的大问题。同样是亮氨酸,有人写 CC(C)C[C@@H](C(=O)O)N,有人写 CC(C)C[C@H](C(=O)O)N。如果在训练时不做标准化,模型的 SMILES 序列认识两种写法,本质是同一分子,白白浪费数据。
我建议的处理策略:
- 判断是否保留异构体:如果是手性预测任务,保留;如果是 ADMET 性质建模,一般去掉。
- 标准化:统一转化为不带手性标记的形式,但保留双键顺反。
- 彻底去除:用
Chem.RemoveStereochemistry把 @、@@ 全部擦掉。
代码示例:
def remove_stereo(smiles):
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return None
Chem.RemoveStereochemistry(mol)
return Chem.MolToSmiles(mol)
test_set = [
"CC(C)C[C@@H](C(=O)O)N", # L-亮氨酸
"CC(C)C[C@H](C(=O)O)N", # D-亮氨酸
"C/C=C/C(=O)O", # 反式-2-丁烯酸
"C/C=C\C(=O)O" # 顺式-2-丁烯酸
]
for smi in test_set:
print(f"原SMILES: {smi:30s} -> 去手性: {remove_stereo(smi)}")
RemoveStereochemistry 同时移除原子手性和双键顺反标记。如果任务需要保留双键的 E/Z 几何异构,只能用 ClearAtomProp 只清 @ 标记,不能一刀切。
4.4 片段化与多组分拆分
多组分 SMILES 用点号连接,比如 c1ccccc1.CCO。RDKit 解析后会得到一个 RWMol,里面有多个连通组件。如果不拆分,模型会把苯环和乙醇当做一个分子来学。
我常用的拆分方法:
from rdkit.Chem import MolFromSmiles, GetMolFrags
def split_multi_component(smiles):
mol = MolFromSmiles(smiles)
if mol is None:
return []
frag_indices = list(GetMolFrags(mol))
frag_mols = []
for idx_tuple in frag_indices:
atom_map = {old: new for new, old in enumerate(idx_tuple)}
new_mol = MolFromSmiles(Chem.MolToSmiles(mol, atomAtomMap=atom_map))
frag_mols.append(new_mol)
return [Chem.MolToSmiles(m) for m in frag_mols if m is not None]
print(split_multi_component("c1ccccc1.[Na+].[OH-]"))
# 输出: ['c1ccccc1', '[OH-]', '[Na+]']
具体用哪种策略,得看任务场景:
| 场景 | 推荐策略 |
|---|---|
| 物化性质预测 | 保留最大的片段,丢弃盐和溶剂 |
| 毒性预测 | 保留所有有机片段 |
| 生成模型训练 | 仅保留片段,舍弃盐和溶剂 |
| 手性合成预测 | 保留异构体标记,拆分多组分 |
4.5 预处理全流程串联
实际项目中,我通常把这几个步骤串联成一个流水线:
输入原始 SMILES → 解析 → 去除盐和溶剂 → 异构体处理 → 多组分拆分 → 输出标准 SMILES
1. 每个 SMILES 只包含一个有机共价骨架。
2. 不含离子、溶剂、小分子杂质。
3. 异构体标记根据任务需求做标准化。
三者缺一不可。
最后送你一个避坑指南:
- 不要迷信 RDKit 的默认解析:它不会自动帮你做预处理。
- 不要直接套用别人的过滤列表:不同数据集的盐和溶剂种类差异很大,最好先做一轮统计。
- 不要忘记处理空分子和解析失败的情况:有些 SMILES 写错了(比如方括号不配对),你得做好异常捕获。
嗯,预处理这块儿虽然啰嗦,但确实是模型效果的上限锁。处理好了,后面的特征工程和模型训练才能事半功倍。