4. 数据预处理:去重、标准化、拆分训练/验证集
GAN 生成化合物,数据预处理是第一个“坑”。
我见过不少同学拿到 ChEMBL 或 ZINC 就直接开训,结果模型输出的分子一半是重复的、一半是格式错误的。原因很简单——原始数据根本没法直接用。
这一章我们就搞定三件事:去重、标准化、拆分。做完这三步,你的训练数据才算“干净”。
核心原则:GAN 的生成质量上限由训练数据决定。脏数据 → 脏生成,没有例外。
4.1 去重:别让模型学“复读”
化合物数据库里经常有重复条目。同一个分子,不同批次上传、不同命名习惯、甚至不同手性表示,都会导致重复。GAN 学这些重复结构会有什么后果?生成时疯狂输出那几个热门分子,多样性直接崩盘。
我个人习惯用 RDKit 的 Canonical SMILES 做去重键。为什么呢?因为正则化后的 SMILES 是唯一的,拿它当 key 最稳。
代码示例:
from rdkit import Chem
from rdkit.Chem import rdchem
def canonical_smiles(smi):
mol = Chem.MolFromSmiles(smi)
if mol is None:
return None
return Chem.MolToSmiles(mol, canonical=True)
def deduplicate(smiles_list):
seen = set()
unique = []
for smi in smiles_list:
can = canonical_smiles(smi)
if can is not None and can not in seen:
seen.add(can)
unique.append(can)
return unique
我曾经碰到过一个坑:有些数据集里的 SMILES 包含立体化学标记(@ 或 @@),但 GAN 输出的分子不需要手性信息。我当时没统一去手性,结果生成出来的分子总带着“@”符号,对接工具直接报错。所以去重之前,最好也把立体化学去掉——用 Chem.RemoveStereochemistry(mol)。
去重后的分子数通常能减少 5%~30%,看你用的数据集。我建议记录一下去重前后的数量,作为数据质量报告的一部分。
4.2 标准化:让数据“对齐口径”
去重只是第一步。下一步是标准化——让所有分子用同一种方式表达。为什么需要标准化?
- SMILES 碎片化:有的数据用了“[Na]”显式写钠离子,有的用“Na+”或省略。
- 盐和溶剂:很多分子是盐酸盐或水合物,但 GAN 只需要骨架。
- 局部/全局电荷:部分字段会给分子加形式电荷,实际上中性的。
我在项目里总结了一套“三刀流”标准化:
- 去盐:把盐和溶剂分子剥离,只保留有机骨架。
- 去手性:移除立体标记(如非必要)。
- 去金属:去掉 K⁺、Na⁺、Cl⁻ 等无关离子。
对应 RDKit 代码:
def standardize_smiles(smi):
mol = Chem.MolFromSmiles(smi)
if mol is None:
return None
# 去盐/溶剂(只保留最大的片段)
frags = Chem.GetMolFrags(mol, asMols=True)
if len(frags) > 1:
mol = max(frags, key=lambda m: m.GetNumAtoms())
# 去手性
Chem.RemoveStereochemistry(mol)
# 去金属(简单正则匹配,常用金属类型)
smi = Chem.MolToSmiles(mol, canonical=True)
# 这里再筛选一次金属原子(跳过)
return smi
一个小技巧:标准化后的 SMILES 可以拿去做分子指纹,然后用 Tanimoto 相似度再筛一遍离群值。这样做能提前发现某些结构非常奇怪的分子,避免它们污染 GAN 的 latent space。
4.3 拆分训练/验证集:别让“亲戚”串门
数据拆分的任务很简单:分出一部分数据不参与训练,用来监控模型是否过拟合。但化合物数据有点特殊——常规的随机拆分容易让相似分子同时出现在训练集和验证集里。
你想想看:如果训练集里有一个“华法林”,验证集里有个结构稍微变了一点的“华法林类似物”,模型可能只是记住了骨架而不是理解了模式。这种“骨架泄露”会让验证指标虚高。
我常用两种拆分策略:
| 拆分策略 | 适用场景 | 优势 | 劣势 |
|---|---|---|---|
| 随机拆分 | 大规模、多样性好的数据集 | 简单、快速、验证集代表性强 | 可能导致骨架泄露 |
| 骨架拆分 | 药物化学先导优化、相似性控制严格 | 避免结构过度重叠 | 计算量稍大,且骨架定义需谨慎 |
两种方法我都会在项目中试一次,然后对比验证集 loss 的分布——如果随机拆分的 loss 明显低于骨架拆分,就说明数据里有很多“亲戚”,需要用骨架拆分才公平。
骨架拆分的 Python 实现可以用 RDKit 的 Murcko 骨架:
from rdkit.Chem.Scaffolds import MurckoScaffold
def get_murcko_scaffold(smi):
mol = Chem.MolFromSmiles(smi)
if mol is None:
return None
scaffold = MurckoScaffold.GetScaffoldForMol(mol)
return Chem.MolToSmiles(scaffold)
# 按骨架分配 train/val
from collections import defaultdict
scaffold_to_indices = defaultdict(list)
for idx, smi in enumerate(all_smiles):
scaf = get_murcko_scaffold(smi)
if scaf:
scaffold_to_indices[scaf].append(idx)
# 将骨架按分子数排序,用贪心算法 split
sorted_scaffolds = sorted(scaffold_to_indices.items(), key=lambda x: len(x[1]), reverse=True)
train_indices, val_indices = [], []
for scaf, indices in sorted_scaffolds:
if len(train_indices) < len(all_smiles) * 0.8:
train_indices.extend(indices)
else:
val_indices.extend(indices)
注意:千万别直接在拆分后用 SMILES 字符串打乱顺序代替随机拆分——那样等于没拆分。一定要用索引或 shuffle 后再分区。否则训练时前几个 batch 全是同一个骨架,梯度会乱跳。
拆分比例我常用 80% / 20% 或 85% / 15%,验证集大小以能稳定估计 loss 为基准,不必太大。
4.4 完整预处理流程图
把上面的步骤串起来,就是一条数据清洗流水线。实际项目里我会先用脚本跑一遍,确认没问题再进入 GAN 训练。
4.5 预处理后的数据验证
数据跑完流水线,不能直接扔给 GAN。我会额外做几个快速检查:
- SMILES 合法性:随便挑 100 条,用 RDKit 转 Mol 并画出来,肉眼看看有没有苯环缺角、原子价不对之类。
- 分子量分布:画个直方图,如果有异常过大的分子(比如分子量 >1000),多半是没去干净。
- LogP 区间:药物分子通常 LogP 在 -2~6 之间,超出太多说明数据源有问题。
我的一贯做法:开一个 Jupyter Notebook,把预处理封装成函数,然后用 pytest 写几个单元测试(比如检查去重后唯一 SMILES 数量、检查标准化后不含‘@’)。这样每次换数据集都能快速复用。
做完这些,你的数据才算达到“生产级”。别忘了把预处理后的数据存成独立文件,比如 smiles_train.txt 和 smiles_val.txt,后面训练的时候直接读就行。