第三章 · 3.1

3.1 DeepChem 的三层抽象:数据、特征化、模型

DeepChem's Three-Layer Abstraction
DeepChem 把分子机器学习流水线切为数据、特征化、模型三层。本节阐明统一抽象如何把多维度实验中"换一个变量"的成本降为一次对象替换:数据层以 Dataset 存放 X、y 与权重掩码 w 并托管划分;特征化层是化学先验的注入点;模型层以统一协议并列传统模型与神经网络。最后走读 Tox21 流水线,并批判默认值与版本现状。

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 画出沿三层的完整数据流。

表 3.1-1三层的职责契约与可替换项(Ramsundar et al., 2019)
核心对象对外契约(承诺什么)典型可替换项
数据层Dataset、Splitter交出 (X, y, w),支持逐批迭代与划分;缺失标签由 w 表达内存后端 / 磁盘后端;随机划分 / 骨架划分
特征化层Featurizer分子到特征的确定性映射,决定 X 的形状ECFP 指纹 / 描述符向量 / GraphConv 图特征
模型层Model、Metricfit / predict / evaluate 统一协议,指标按 w 过滤SklearnModel 包装 / TorchModel 包装;各类指标函数
三层抽象的数据流:原始记录经 Dataset、Featurizer、Model 到 Metric 的流水线,三层的职责区间在下方标出 原始记录 SMILES + 标签表 Dataset X · y · w Featurizer 分子 → 特征 Model fit · predict · evaluate Metric AUC 等口径 Tox21:SMILES+12 列标签 内存 / 磁盘后端同接口 向量 R^d 或图 (V, E) Sklearn / Torch 并列 只统计 w=1 数据层 · Dataset 与 Splitter 特征化层 · Featurizer 模型层 · Model 与 Metric 层间接口:数据层交出 (X, y, w) 三元组;Featurizer 决定 X 的形状;模型与指标只认形状与协议,不问来历。 原始记录(最左)不属于任何一层——它是流水线的输入而非组件;三个区间内的选择各可独立替换,互不牵动。
图 3.1-1 DeepChem 三层抽象下的数据流。原始记录经数据层组织为 (X, y, w),经特征化层定型为向量或图,经模型层训练评估,最后由 Metric 给出指标。下方三段标线是三层的职责区间:区间内的实现可替换,区间之间的箭头是窄接口。训练循环自右向左索要"下一批 (X, y, w)",数据流自左向右供给。

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 的格子,填进什么都不会泄漏进结果。多任务训练由此写成:

L(θ) = (1/|Ω|) ∑(i,t)∈Ω wit · ℓ( yit, ft(xi; θ) ), Ω = {(i, t) : wit = 1}
(3.1-1)i 编号分子,t 编号任务,ℓ 为单样本损失(如交叉熵)。w=0 的项乘出零,等价于从求和中剔除;有效标签总数 |Ω| = ∑ᵢ∑ₜ wit。指标同样按 Ω 过滤。此式与 1.3 节的稀疏标签矩阵一一对应,是数据层对上层最重要的承诺。
注记

权重掩码消解 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 参与训练与指标,各任务的正例率变成多少?偏移方向如何?

表 3.1-2习题 3.1-1 的标签矩阵(∅ 为缺失)
分子T1T2T3
m110
m201
m301
m4110
m50
m610
m7001
m810
参考解答

(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 节)。形式化地写:

xi = Φ(mi) ∈ ℝd(向量特征) 或 xi = (Hi, Ai)(图特征)
(3.1-2)Φ 为特征化器,mi 为第 i 个分子。向量特征把结构折叠进定长实数向量;图特征保留变长结构:H 为节点特征矩阵,A 为邻接关系。Φ 一旦选定,模型 f 的输入空间随之冻结——层与层之间的接口就是 x 的形状。

这层常被"一个字符串参数"的表象掩盖,科学含量却不在模型层之下。选什么特征化,就是在声明化学先验:毒性常与特定子结构模式相关,指纹把子结构词汇表摆到台前;溶解度对整体极性敏感,描述符直接点名拓扑极性表面积;构效关系若取决于连接方式本身,图特征保留全部拓扑,把"哪些信息重要"的判断推迟给模型去学。先验注入得越靠前,模型自由度越小、样本效率越高,偏差风险也越大;注入得越少,上限越高,数据需求随之膨胀。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 节)。
  • 可吃向量,也可经批组装吃图;图神经网络只住这一侧。
  • 随机种子与初始化带来方差,重复实验后报告均值与波动才诚实。
同一条流水线只替换模型层:Dataset 与 Featurizer 共享,随机森林与多层感知机并列,指标口径相同 同一条流水线,只换模型层——数据、特征化、指标、划分全部复用 Dataset Tox21 · 骨架划分 X · y · w(两行共用) 数据层不动 Featurizer ECFP → R^1024 同一份特征 特征化层不动 随机森林 SklearnModel 包装 多层感知机 TorchModel 包装 Metric roc_auc_score → AUC 对比 口径不变 唯一替换的槽位 两行的差值只能归因于模型层——其余变量被冻结在共享的方框里,这正是对照实验的结构。 接口边界:若特征化层改出图特征 (H, A),随机森林的输入形状即不匹配——跨接口的替换天然是两处变更,见 3.1.5 与 3.2。
图 3.1-2 "换模型不换管线"。左起三个纵向方框(Dataset、Featurizer、Metric)为两条流水线共享,中间纵向并列的两个方框是唯一的可替换槽位:随机森林经 SklearnModel 包装、多层感知机经 TorchModel 包装,共用 fit / predict / evaluate 协议与同一份向量特征。对比实验的成本降到一次替换;跨接口的更换(如换图卷积须连特征化一起换)则应如实计为两处变更。

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、标签变换器列表。把走读的每一步落到层上:

  1. 加载与特征化(数据层 × 特征化层)。SMILES 与 12 列标签表读入,Featurizer 把每个分子映成特征;y 的缺失格由 w 记账。特征化在加载时一并完成,此后三层各自拿到的是定型好的对象。
  2. 划分(数据层)。ScaffoldSplitter 按骨架整族切出训练、验证、测试(约 8:1:1),三个仍是同种 Dataset。1.3 节的结论在此生效:这一刀切下去,指标衡量的就是结构外推而非插值。
  3. 训练(模型层)。fit 只认 Dataset:随机森林一次拟合即返回;图卷积按批迭代、按验证损失早停。训练循环不接触分子,它见到的只是形状确定的批。
  4. 评估(模型层协议 × 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 过滤、对有效任务聚合,保证对比口径统一。

参考文献与延伸阅读

  1. 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.
  2. Wu Z, Ramsundar B, Feinberg EN, et al. 2018. MoleculeNet: a benchmark for molecular machine learning. Chemical Science 9:513–530.
  3. Landrum G, et al. 2026. RDKit: Open-Source Cheminformatics. RDKit 官方文档,2026.03 系列.
  4. Pedregosa F, Varoquaux G, Gramfort A, et al. 2011. Scikit-learn: machine learning in Python. Journal of Machine Learning Research 12:2825–2830.
  5. 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.
  6. DeepChem. 2024. DeepChem 2.8 官方文档与教程. deepchem.io(稳定版 2.8.0,2024-04;nightly 持续演进).