3.1.1 实验的维度与关注点分离
分子机器学习的研究很少问"这个模型有多准",更常问"换了某个选择之后还准不准"。把实验的自由度列出来——数据集、表示、模型、划分协议、指标——至少五个维度,常见的组合动辄上百。1.3 节已经证明,仅划分协议一项就能让同一模型在同一数据上的 AUC 相差数分;表示的维度(2.4 的描述符、2.5 的指纹)与模型的维度各自都值得单独考察。科学结论住在维度之间的差值里。
耦合实现的代价恰在此处。数据读取、特征构建、训练循环、指标计算挤在一个脚本里时,"换一个变量"意味着改动波及全部代码:换图特征要重写批组装,换骨架划分要重排样本索引,换指标要在好几处同步修改口径。任何一处遗漏都不报错,只是让对照实验悄悄退化成"两个变量同时改"的混合比较——结论在无声处失效。代码量倒在其次,错误传播才是要害。
软件工程对这个问题有现成答案:关注点分离(separation of concerns)。按职责把系统切成层,每层只对相邻层承诺一个窄接口,内部实现随意替换。DeepChem 把这条原则落到分子机器学习上,切成三层(Ramsundar et al., 2019):数据层管样本与标签的存放、划分和逐批供给;特征化层管分子到模型输入的转换;模型层管训练、预测与评估。层与层之间的接口足够窄——数据层交出 (X, y, w) 三元组,特征化层决定其中 X 的形状,模型层只认形状、不问来历——任何一层的更换都坍缩成一处替换,其余两层原样复用。
分层的收益要落在试错速度上才算数。对研究者而言,"层"就是实验变量:控制其余层不动、只换一层的成本越低,单位时间能检验的假设越多。抽象不是工程的奢侈品,是科研的方法论——它把"我可以换什么"从勇气问题变成一行代码问题。表 3.1-1 概括三层的职责契约,图 3.1-1 画出沿三层的完整数据流。
| 层 | 核心对象 | 对外契约(承诺什么) | 典型可替换项 |
|---|---|---|---|
| 数据层 | Dataset、Splitter | 交出 (X, y, w),支持逐批迭代与划分;缺失标签由 w 表达 | 内存后端 / 磁盘后端;随机划分 / 骨架划分 |
| 特征化层 | Featurizer | 分子到特征的确定性映射,决定 X 的形状 | ECFP 指纹 / 描述符向量 / GraphConv 图特征 |
| 模型层 | Model、Metric | fit / predict / evaluate 统一协议,指标按 w 过滤 | SklearnModel 包装 / TorchModel 包装;各类指标函数 |
3.1.2 数据层:Dataset、权重掩码与划分
数据层的核心对象是 Dataset。它对外的承诺很少:持有特征 X、标签 y、样本权重 w,支持按批迭代,支持切分。X 与 y 的形状由数据集本身决定:单任务 y 是长度为 N 的向量;多任务 y 是 N×T 的矩阵——Tox21 有 12 个测定任务,y 便是 N×12。
真正的设计题在"缺失"如何表达。1.3 节的稀疏标签矩阵在此落地:分子 i 在任务 t 上未必有测定记录,y 的该格无值可填。Dataset 不发明新的"缺失类型",而是把 y 全部填成实数,另设一个同为 N×T 的 0/1 矩阵 w 作权重掩码(weight mask):wit=1 表示标签真实存在,wit=0 表示未知。未知的格子在 y 里填 0 即可——任何损失与指标都只统计 w=1 的格子,填进什么都不会泄漏进结果。多任务训练由此写成:
权重掩码消解 y 中 0 的两义。y 矩阵里的 0 有两种身份:真实测得的阴性,或从未测定的未知。单看 y 无法区分,w 是唯一仲裁者——w=1 的 0 是负例,w=0 的格子是空白,填什么都进不了结果。指标同受掩码约束:AUC 只在有效标签上计算。若把缺失一律当 0,正例率被稀释、任务难度被高估,1.3 节已推演过这种口径差错对结论的扭曲;3.7 节处理类别不平衡时,w 还是承载加权方案的同一通道。
存放位置与迭代方式是数据层的另一对设计。内存后端把三个矩阵整块放进内存,随机访问、切片都快;磁盘后端把大表留在盘上,按需逐批读取,特征化也可以推迟到取批时再做。这种"用到才算"的惰性迭代(lazy iteration) 让数据集规模与内存容量解耦——百万级虚拟筛选库因此可训练。关键在:两种后端对上层的脸面完全一致,训练循环拿到的都是"下一批 (X, y, w)",不必也不应关心它来自内存还是硬盘。
数据层还托管划分。划分器(Splitter) 把一个 Dataset 切成训练、验证、测试三个同种对象。语义差异一句话:RandomSplitter 按比例随机切,考察同分布插值;ScaffoldSplitter 按 Bemis–Murcko 骨架归类后整族划归一侧,考察结构外推。1.3 节的三种划分协议(随机、骨架、时间)在工程上就是不同的 Splitter——选哪个是科学决策,但落点只是一个对象名,紧挨着它所影响的那些数字。
切出的三个 Dataset 各司其职:训练集进 fit;验证集只给训练过程看——驱动早停、挑选超参,看多少眼都行;测试集只在一切定型之后进 evaluate 一次。这套纪律完全靠研究者自律维持,数据层不会强制——3.8 节会列举绕开它的种种手法及其代价。
习题 3.1-1
某多任务数据集有 8 个分子(m1–m8)、3 个任务(T1–T3),标签矩阵如表 3.1-2,∅ 表示未测定。(1) 写出权重掩码 w,并计算式 (3.1-1) 中的有效损失项数 |Ω|。(2) 按任务统计有效正例数与有效正例率。(3) 若错误地把 ∅ 一律当作 0 参与训练与指标,各任务的正例率变成多少?偏移方向如何?
| 分子 | T1 | T2 | T3 |
|---|---|---|---|
| m1 | 1 | 0 | ∅ |
| m2 | 0 | 1 | ∅ |
| m3 | ∅ | 0 | 1 |
| m4 | 1 | 1 | 0 |
| m5 | 0 | ∅ | ∅ |
| m6 | ∅ | 1 | 0 |
| m7 | 0 | 0 | 1 |
| m8 | 1 | ∅ | 0 |
(1) w 与 y 同形,非 ∅ 处取 1:T1 列缺 m3、m6,T2 列缺 m5、m8,T3 列缺 m1、m2,各列 6 个有效项,|Ω| = 6×3 = 18(总格 24,缺失 6)。(2) 有效正例:T1 为 m1、m4、m8 共 3 个,T2 为 m2、m4、m6 共 3 个,T3 为 m3、m7 共 2 个;正例率 T1 = 3/6 = 50%,T2 = 50%,T3 = 2/6 ≈ 33%。(3) 缺失当 0 后每列分母变成 8:T1 = 3/8 = 37.5%,T2 = 37.5%,T3 = 2/8 = 25%——正例率一律被低估,类别显得更不平衡,且 6 个"未知"被当作确证阴性参与损失,模型从空白里学到了不存在的规律。
3.1.3 特征化层:化学先验的注入点
特征化器(Featurizer)。实现"分子 → 模型输入"这一确定性映射的对象:入口接受 SMILES 字符串或 Mol 对象(2.1 节的图表示,经 RDKit 解析),出口交出特征数组。它不带可学习参数、不参与训练;其种类决定输出的形状——定长向量或变长图。选择哪一种,即是声明"我认为分子的哪些结构信息与任务相关"。
特征化层只有一个动词:把分子变成模型能吃的输入。入口统一,出口的形状却随种类而变。ECFP 类指纹把分子压成 R^d 的定长位串(2.5 节,d 常取 1024);描述符拼接给出更低维的连续向量(2.4 节,分子量、logP、拓扑极性表面积一类);图特征保留变长结构——节点特征矩阵加邻接关系,把分子图原样递给模型(2.1 节)。形式化地写:
这层常被"一个字符串参数"的表象掩盖,科学含量却不在模型层之下。选什么特征化,就是在声明化学先验:毒性常与特定子结构模式相关,指纹把子结构词汇表摆到台前;溶解度对整体极性敏感,描述符直接点名拓扑极性表面积;构效关系若取决于连接方式本身,图特征保留全部拓扑,把"哪些信息重要"的判断推迟给模型去学。先验注入得越靠前,模型自由度越小、样本效率越高,偏差风险也越大;注入得越少,上限越高,数据需求随之膨胀。1.2 节的三种面貌(字符串、指纹、图)在工程上就是不同 Featurizer 的选择面。
再点破一条分界线:特征化与表示学习的边界划在何处。Featurizer 的"参数"(指纹半径、长度,描述符清单)是人定的,训练前后不变;图特征交出去之后,模型学出的逐节点表示才是数据驱动的那部分。第 3 章后半的演化故事——基线、图卷积、消息传递、注意力——可以一句话预演:把"哪些信息重要"的决定权从特征化层逐步移交给模型层。移交不是取消特征化:哪怕最彻底的图特征,原子特征清单与邻接口径仍出自人的选择(3.2 节展开)。
3.1.4 模型层:统一协议与两种包装
模型层对外只承诺三个动作:fit 吃 Dataset,predict 吐预测,evaluate 吃 Dataset 加 Metric 吐指标。协议之窄,使机理截然不同的两类模型能并列在同一个槽位。一类是 SklearnModel——包装 scikit-learn 的估计器(estimator)(Pedregosa et al., 2011):随机森林、梯度提升树这类传统模型,训练即一次性拟合,预测即查表式推理。另一类是 TorchModel——包装 PyTorch 的模块(Paszke et al., 2019):多层感知机、图卷积网络,训练是梯度下降的迭代循环,还带验证集上的早停(early stopping)(3.8 节)。两者的内部毫无共同之处,对上层的脸面却一致:换模型就是换一个对象,流水线的其余部分不知道、也不需要知道发生了什么。
第三个小件是 Metric 对象。它把 scikit-learn 的指标函数(如 roc_auc_score)封装成"按任务计算、按 w 过滤、对有效任务聚合"的对象。封装解决两件事。其一是口径统一:任何模型、任何流水线算出的 AUC 走同一条代码路径,"各算各的"式对比失效的风险就此消除。其二是缺失标签的自动处理:多任务下某任务在某次划分里可能没有有效正例,AUC 在该任务上无定义,聚合时按有效性取舍,而不是让一个 NaN 毁掉整张报表。指标的概率语义与多任务聚合细节留给 3.3 与 3.7。
图 3.1-2 把"换模型不换管线"画出来:同一份数据与划分、同一份 ECFP 特征,模型槽位上随机森林与多层感知机并列,指标口径不变。两条流水线的差值只能归因于模型层——其余变量被冻结在共享的方框里,这正是对照实验的结构。接口之内自由替换;跨接口的替换(图特征配随机森林)则不被形状允许,天然是两处变更。承认这条边界不是抽象的失败,而是对"实验到底改了几个变量"的诚实核算。
SklearnModel:一次性拟合
- 训练是单次拟合:吃进全部数据,返回即定型,没有"训练了几个轮次"的概念。
- 验证集不参与训练,只用于模型间的最终选择。
- 吃定长向量;指纹、描述符是它的天然搭档。
- 推理即查表,随机种子的影响小,结果基本可复现。
TorchModel:迭代优化
- 训练是梯度下降的循环,损失逐轮下降,轮数与学习率都是决策。
- 验证集嵌入训练过程:监控损失、触发早停(3.8 节)。
- 可吃向量,也可经批组装吃图;图神经网络只住这一侧。
- 随机种子与初始化带来方差,重复实验后报告均值与波动才诚实。
3.1.5 走读:Tox21 的完整数据流
Tox21。"Toxicology in the 21st Century" 计划的高通量筛选数据:约 7 831 个环境化合物在 12 种测定(7 个核受体、5 个应激反应通路)上的活性标签。测定彼此独立,标签矩阵高度稀疏。MoleculeNet 将其收录为基准(Wu et al., 2018),DeepChem 的 molnet 接口以它为默认教学数据集。本节的 w 矩阵、3.7 节的多任务与不平衡,都在这份数据上展开。
molnet 的加载器把"下载、解析、特征化、划分"一次做完,返回三元组:任务名列表、三个已切分的 Dataset、标签变换器列表。把走读的每一步落到层上:
- 加载与特征化(数据层 × 特征化层)。SMILES 与 12 列标签表读入,Featurizer 把每个分子映成特征;y 的缺失格由 w 记账。特征化在加载时一并完成,此后三层各自拿到的是定型好的对象。
- 划分(数据层)。ScaffoldSplitter 按骨架整族切出训练、验证、测试(约 8:1:1),三个仍是同种 Dataset。1.3 节的结论在此生效:这一刀切下去,指标衡量的就是结构外推而非插值。
- 训练(模型层)。fit 只认 Dataset:随机森林一次拟合即返回;图卷积按批迭代、按验证损失早停。训练循环不接触分子,它见到的只是形状确定的批。
- 评估(模型层协议 × Metric)。evaluate 对测试集逐任务算 AUC(只统计 w=1),再对有效任务取平均。同一个 Metric 对象对任何模型一视同仁。
下面两段骨架代码印证走读,细节以注释与省略折叠。第一段是完整流水线:ECFP 特征加随机森林基线;第二段换掉特征化与模型两层,换成图特征与图卷积——注意这是两处变更,接口联动使然;若要严格的单层对照,应在同一份 ECFP 向量上把随机森林换成多层感知机(图 3.1-2 的情形)。
import deepchem as dc
# 数据层 + 特征化层:加载 Tox21,一次完成解析、特征化与骨架划分(tf 为标签变换器,3.7 展开)
tasks, (train, valid, test), tf = dc.molnet.load_tox21(featurizer='ECFP',
splitter='scaffold')
# 模型层:SklearnModel 包装的随机森林;协议统一为 fit / evaluate
model = dc.models.RandomForestClassifier(n_estimators=500)
model.fit(train)
# Metric:封装 roc_auc_score——按任务计算、按 w 过滤、对有效任务取平均
metric = dc.metrics.Metric(dc.metrics.roc_auc_score)
print(model.evaluate(test, [metric])) # 12 项任务的平均 AUC
# 换图卷积:特征形状变了,特征化层与模型层一起换(跨接口的替换天然是两处变更)
tasks, (train, valid, test), tf = dc.molnet.load_tox21(featurizer='GraphConv',
splitter='scaffold')
model = dc.models.GCNModel(n_tasks=len(tasks), graph_batch_size=200,
learning_rate=0.001)
model.fit(train, nb_epoch=30) # 数据层与指标口径:原样复用
print(model.evaluate(test, [metric])) # 同一条流水线,同一个口径
两段代码合起来看,"三层"不是三个函数,而是三处可以独立指认的决策点:第一行的两个字符串(特征化、划分)各定一层,model 一行定一层。骨架划分下 Tox21 的基线成绩显著低于随机划分——1.3 节的机理分析在此得到复现:查表通道被关闭,剩下的才是化学泛化的成绩。GCN 在这份数据上能否胜过指纹基线,正是 3.4 节要拆解的问题。
习题 3.1-2
固定数据集(Tox21)、划分(骨架)、指标(平均 AUC),设计一个"两层不变、只换一层"的 2×2 实验矩阵:特征化取 {ECFP 指纹, 描述符向量},模型取 {随机森林, 多层感知机}。(1) 写出 4 条流水线,指出哪些两两比较是严格的单层差异,各自回答什么问题。(2) 若直接比较"ECFP+随机森林"与"描述符+感知机",混杂了几个变量?(3) 什么情况下双层同时更换不可避免?此时应如何报告结论?
参考解答(1) 流水线共四条:(ECFP, RF)、(ECFP, MLP)、(描述符, RF)、(描述符, MLP)。同行两格只差模型——在给定表示下估计模型效应;同列两格只差特征化——在给定模型下估计表示效应;四格齐备还能估出交互(表示的优劣是否依赖模型)。(2) 对角比较同时换了两层,差值是表示效应、模型效应与交互的混合,无法归因。(3) 接口不匹配时(图特征配随机森林),形状约束使双层联动不可避免;此时应如实报告"整条管线的比较",不得宣称单是模型的优劣。
3.1.6 抽象的代价:默认值、流畅性与版本
分层的第一笔成本是间接层。学习者面对的不再是"一个脚本",而是若干抽象类与包装器;报错要在包装层与底层库之间转译,调试者须同时懂 scikit-learn 与 PyTorch 的习惯。这是学费,尚可付清。更深的代价藏在便捷本身。
默认值的科学风险。加载器一步到位的便捷,把两个本应显式论证的决策压成了两个字符串参数:featurizer 与 splitter。照抄默认值跑通流水线时,"特征化决定了模型能看见什么""划分协议决定了指标衡量什么"这两个决策从未被审视,却各自足以移动数分 AUC(1.3 节)。抽象把决策藏进参数,不等于决策消失。防之道是纪律性的:每层的选择显式写入实验记录,报告结果时逐层交代数据、划分、特征化、模型、指标——3.8 节把这份清单列为诚实评估的必答项。
教程式流水线还有第二重陷阱:跑通不等于理解。"几分钟得到一个 AUC"的流畅体验,容易把注意力全部吸进模型层——那里最有"新意",也最容易刷分;而科学含量最高的选择往往在另外两层。1.3 节的量化已经说明,仅划分协议一项就能解释数分的指标差;本节的论证补上另一半:特征化决定了模型的可见世界。层间的拼接是工程便利,层内的选择才是科学问题。
最后交代版本现状。DeepChem 的稳定版停留在 2.8.0(2024 年 4 月发布),此后 nightly 版持续演进,接口细节时有变动(2026 年 8 月核实);本节的类名与参数以 2.8 系列文档为准。本教材的立场是学抽象而非学版本:三层抽象、w 掩码、统一协议这些设计自库的早期(Ramsundar et al., 2019)延续至今,未随版本漂移;TorchDrug(第 4 章)与更广的生态(第 5 章)各有一套同名不同形的实现,对号入座靠的正是层与接口的理解,而非记忆某个版本的 API。具体类名随时可查文档,分层的眼光才是可迁移的资产。
数据怎么变成向量与张量、原子与键各带什么特征、图如何成批——3.2 节把特征化层单独拿出来,走到管线尽头。
习题 3.1-3
逐层回答:三层抽象中,每一层各有一个最容易被默认值掩盖的科学决策。写出这三个决策,并各用一句话说明它以何种机制影响最终指标。
参考解答数据层:划分协议——随机划分放行同族近重复,指标混入查表成分而虚高;骨架划分关闭该通道,指标衡量的才是结构外推(1.3 节)。特征化层:特征种类——它决定模型的可见世界,先验注入偏则天花板低,注入少则数据需求大,直接改动可达成绩与所需样本量。模型层:容量与训练协议(含早停)——容量不足则系统性欠拟合,容量过大加上训练过久则过拟合验证集之外的分布,早停的监控口径本身也是一个易被忽视的选择(3.8 节)。
关键术语
- 关注点分离 (separation of concerns)
- 按职责切层、以窄接口衔接的软件工程原则;在科研流水线上转化为试错速度。
- Dataset
- 统一持有特征 X、标签 y 与权重 w 并支持逐批迭代的数据容器,内存与磁盘后端同接口。
- 惰性迭代 (lazy iteration)
- 用到才取、用到才算的供给方式,使数据集规模与内存容量解耦。
- 权重掩码 (weight mask)
- 与 y 同形的 0/1 矩阵 w:w=1 的标签进入损失与指标,w=0 表示未知。
- 划分器 (Splitter)
- 把 Dataset 切成训练、验证、测试的对象;不同划分协议考察不同的泛化性质。
- 特征化器 (Featurizer)
- 分子到模型输入的确定性映射对象,决定特征的形状,是化学先验的注入点。
- 特征形状 (feature shape)
- 定长向量或变长图;层间接口的事实标准,决定哪些替换是单层的。
- SklearnModel
- 对 scikit-learn 估计器的包装,使传统模型进入统一训练评估协议。
- TorchModel
- 对 PyTorch 模块的包装,使神经网络与传统模型并列于同一槽位。
- Metric (metric object)
- 指标函数的封装:按任务计算、按 w 过滤、对有效任务聚合,保证对比口径统一。
参考文献与延伸阅读
- Ramsundar B, Eastman P, Walters P, Pande V. 2019. Deep Learning for the Life Sciences: Applying Deep Learning to Genomics, Microscopy, Drug Discovery, and More. O'Reilly Media.
- Wu Z, Ramsundar B, Feinberg EN, et al. 2018. MoleculeNet: a benchmark for molecular machine learning. Chemical Science 9:513–530.
- Landrum G, et al. 2026. RDKit: Open-Source Cheminformatics. RDKit 官方文档,2026.03 系列.
- Pedregosa F, Varoquaux G, Gramfort A, et al. 2011. Scikit-learn: machine learning in Python. Journal of Machine Learning Research 12:2825–2830.
- Paszke A, Gross S, Massa F, et al. 2019. PyTorch: an imperative style, high-performance deep learning library. Advances in Neural Information Processing Systems 32:8024–8035.
- DeepChem. 2024. DeepChem 2.8 官方文档与教程. deepchem.io(稳定版 2.8.0,2024-04;nightly 持续演进).