3. 数据预处理:去除盐类与标准化规则

数据预处理,说白了就是给数据“洗个澡”。原始SMILES里什么都有:盐类、溶剂、水合物、乱七八糟的配位离子。你不洗掉它们,模型学到的全是噪音。

我最早做化合物数据库清洗时,发现很多SMILES写成这样:[Na]OC(=O)c1ccccc1.[H]O[H]。苯甲酸钠带着一个水分子。你说这算一个分子还是两个分子?模型会懵掉的。所以我们得定一套规矩。

核心原则:保留药物分子骨架,剔除无药理活性的辅料、盐离子、溶剂分子。

3.1 去除盐类与溶剂分子

先说最头疼的——盐类和溶剂。很多公开数据集(比如ChEMBL、PubChem)直接存的是盐型化合物。你拿过来不处理,模型会以为[Cl-]是分子的一部分。

常用的去盐方法

  1. 组分拆分法:用RDKit的MolFromSmiles解析后,提取最大有机片段。
  2. 白名单法:保留常见分子骨架,丢弃小分子离子。
  3. 正则表达式法:直接剔除[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
避坑指南:我曾经遇到过“过度清洗”的问题——把母核骨架上的配位金属离子(比如卟啉环上的Fe)也给去掉了。所以去盐前最好先做官能团识别,判断金属离子是否是药效基团的一部分。

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
注意:中性化过程会丢失部分电荷信息。如果你的下游任务是计算pKa或电离状态,建议保留原始电荷。此时可以走另一条分支——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芳香性骨架
实用建议:不要试图识别所有官能团,几十种就好。我一般只选最常见的20~30种,配合药效团特征(氢键供体/受体、疏水中心)一起使用。太多了反而增加噪声。

我曾经在一次毒性预测任务中,把官能团向量直接拼接到Morgan指纹后面,AUC提升了近3个点。为什么?因为指纹只记录局部拓扑,而官能团提供了“化学语义”——比如“这个分子有羧基,大概率是酸性”。模型学到了这个归纳偏好。

SVG:数据预处理与标准化流程

下面这张图把咱们这章的核心流程串起来了。从原始SMILES到标准化SMILES,中间经过三个必选关卡:去盐、官能团识别、标准化。每个关卡后面都可能出现失败处理——返回None或告警。

SMILES数据预处理与标准化流程 SMILES数据预处理与标准化流程 原始SMILES 1. 去盐 + 去溶剂分子 2. 官能团识别 (SMARTS) 3. 标准化 (CanonSMILES) 标准化SMILES 解析失败: 返回 None 说明 1. 去盐优先用最大片段法 2. 官能团识别输出特征向量 3. 标准化保证SMILES唯一性 避坑提示 不要过度去盐: 保留配位金属离子

嗯,看到这张图你就明白了——整个流程是一条流水线。你可以在每个步骤之后插入日志记录,方便排查问题。我经常在去盐之后打印“Removed salts: xxx”,在标准化之前打印“Original: xxx → Canonical: xxx”。这些日志在调试时特别有用。

提醒:标准化不是万能的。对于具有互变异构的化合物(比如酮-烯醇互变),标准化的结果可能取决于RDKit的内部实现。如果遇到这种情况,可以用MolStandardize模块的Standardizer类,它支持自定义互变异构规则集。

这一章讲下来,其实核心就是三件事:去杂质、归一化、打标签。去杂质让数据干净,归一化让数据一致,打标签让数据更有语义。这三步做扎实了,后面的模型训练才谈得上效果。我每次做新项目,最先做的就是这个预处理管线——它决定了一场实验50%的成败。