分子图数据表示:一场从化学结构到机器学习的翻译

做分子图深度学习,说白了就是让计算机看懂化学结构。但计算机不懂“苯环”“羰基”这些概念,它只认得数字。所以我们需要一套严格的翻译规则——把原子、化学键乃至整个分子的信息,编码成计算机能处理的特征向量。

我记得自己第一次做分子属性预测时,以为直接把SMILES扔进模型就行了——嗯,结果当然是一塌糊涂。后来才明白,特征表示才是图神经网络的灵魂。搞不定特征,再好的模型架构也是白搭。

这一节我们就来拆解分子图的三层特征:原子特征(节点)、键特征(边)和全局特征(图级别)。这是构建分子图的基石,也是后续GNN模型最底层的输入。

原子特征:给每个节点贴上标签

原子是分子图的节点。每个节点需要携带足够的信息,来描述“这个原子是什么、处于什么状态”。

我个人习惯把原子特征分成三类:

  • 身份特征:原子类型、周期表中的位置
  • 电子特征:杂化方式、形式电荷、孤对电子数
  • 拓扑特征:连接原子数(度)、是否在环上

实际项目中,最常用的原子特征组合是这样的:

特征名称 描述 常见编码方式 维度(示例)
原子类型 C, N, O, S, F, Cl, Br, I, P... 前9-10种常见元素 One-hot 9
度(Degree) 该原子连接的化学键数量(氢键除外) One-hot(0~5或更多) 6
形式电荷 原子在分子中的电荷状态,常见 -1, 0, 1 One-hot 3
杂化类型 sp, sp², sp³ One-hot 3
氢原子数目 连接该原子的氢原子个数 One-hot(0~4或直接数值) 5
芳香性 是否在芳香环中 布尔值(0或1) 1
在环中 是否在任意环结构中 布尔值 1

来看一个具体的编码例子。假设我们有一个碳原子(C),sp²杂化,形式电荷为0,连接3个原子(度=3),带1个氢,不在芳香环中。它的特征向量大概长这样:

原子类型:    [1, 0, 0, 0, 0, 0, 0, 0, 0]  # C是第一个
度:          [0, 0, 0, 1, 0, 0]          # 度=3 -> 第4位为1
形式电荷:    [0, 1, 0]                    # 0 -> 中间
杂化:        [0, 1, 0]                    # sp² -> 中间
氢原子数:    [0, 1, 0, 0, 0]             # 1个H -> 第2位
芳香性:      [0]                          # false
在环中:      [0]                          # false

完整拼接:22维浮点向量

避坑指南:我曾经在项目中把所有原子特征都用了One-hot编码,结果模型训练特别慢。后来发现像“氢原子数目”这样的特征,直接用归一化的数值输入效果反而更好。实验前建议先做个小测试——One-hot vs 数值编码,哪个对下游任务更友好。

键特征:边的信息同样重要

光有节点还不够。原子之间怎么连接的?单键还是双键?顺反异构怎么区分?这些信息要编码在边上。

键特征往往被初学者忽略,但实战中它经常是模型性能的关键。我做过一个手性分子识别项目,一开始只用键序(单、双、三),模型死活分不清对映体。加上立体化学特征后,准确率直接从70%跳到了92%。

最常见的键特征:

  • 键序:单键=1,双键=2,三键=3,芳香键=1.5
  • 共轭性:是否处于共轭体系
  • 环内键:是否在环中
  • 立体化学:顺式(Z)、反式(E)、无指定
  • 键长(可选):通常归一化后使用

用JSON来表示分子图数据,是最常见的存储格式:

{
  "atoms": [
    {"element": "C", "hybridization": "sp2", "charge": 0, "degree": 3, "h_count": 1, "in_ring": false},
    {"element": "O", "hybridization": "sp2", "charge": 0, "degree": 1, "h_count": 0, "in_ring": false}
  ],
  "bonds": [
    {"src": 0, "dst": 1, "order": 2, "conjugated": false, "in_ring": false, "stereo": "none"},
    {"src": 0, "dst": 2, "order": 1, "conjugated": false, "in_ring": false, "stereo": "none"}
  ]
}

注意:立体化学的编码比较微妙。有些任务用3位One-hot:[Z, E, 无指定];有些则直接用一个取值范围[-1, 0, 1]的数值。我个人倾向于前者,因为布尔型的特征在模型里学习起来更直接。不过遇到手性分子多的数据集,我会额外加一个“是否存在手性中心”的全局特征。

曾经踩过的坑:在编码芳香键时,我用了默认键序1.5。结果模型在预测某些环状分子的性质时误差特别大。原因是RDKit默认的芳香键是1.5,但有些模型库(比如DGL的SchNet)预期输入是整数键序。最后我不得不加了一层预处理:遇到芳香键时拆分成单键+双键交替。这种细节差异,换框架就得查一遍,不是某一家的问题。

全局特征:整个分子的肖像

有些性质,只看原子和键是看不出来的。比如分子量、辛醇-水分配系数(logP)、拓扑极性表面积(TPSA)。这些是全局特征,附加到整个图上,不是某个节点或边上。

常见的全局特征:

特征 意义 来源
分子量 所有原子质量之和 计算
LogP 脂溶性,药物化学经典指标 RDKit计算或实验值
TPSA 极性表面积,与吸收相关 RDKit
可旋转键数 分子柔性指标 RDKit
氢键供体/受体数 影响溶解性和靶点结合 RDKit
芳香环数 芳香体系复杂度 计算

全局特征怎么用?常见做法有两种:

  • 嵌入到图表示中:作为额外的输入向量,和图神经网络输出的节点表示拼接
  • 作为辅助监督:多任务学习中,让模型同时预测分子属性和全局特征,提升表征质量

我个人更推荐第二种。不过要注意:全局特征和目标任务不能太相关,否则模型会偷懒,只靠全局特征猜测,忽略图结构信息。比如预测溶解度时,把LogP作为额外输入,模型可能就不学原子间的交互模式了——反而导致泛化能力下降。

分子图数据结构全景 分子图示例 (乙醇) C1 C2 O 单键 单键 原子特征: 类型, 杂化, 电荷 度, 氢数, 环信息 原子特征: 类型, 杂化, 电荷 度, 氢数, 环信息 原子特征: 类型, 杂化, 电荷 度, 氢数, 环信息 键特征 (每条边) 键序: 单=1, 双=2, 三=3 共轭性, 环内标志 立体化学: Z/E/无指定 手性标志 (可选) 键长 (归一化后) 可选, 实验需对齐 全局特征 (整个图) 分子量 所有原子质量 LogP 脂溶性指标 TPSA 极性表面积 可旋转键 分子柔性

特征组合策略:不是越多越好

你可能会想:那就把所有能算的特征都怼进去呗。可别这么干。特征维度爆炸会带来几个问题:

  • 模型参数量激增,小数据集上容易过拟合
  • 冗余特征混淆学习信号,降低泛化能力
  • 训练速度下降,每个batch传递的数据量变大

那我怎么选?先说一个我自己总结的经验法则:

  1. 根据任务类型判断:分类任务优先原子类型+键序+全局LogP;回归任务注意特征归一化
  2. 先跑一个基线模型(只用最基本特征),再看加特征后性能是否提升
  3. 如果特征加了一堆但性能没涨,就是白加——果断砍掉

另外,不同框架对特征顺序有默认要求。比如PyTorch Geometric(PyG)的GINConv,希望节点特征在第一维;DGL的GATConv则默认特征在最后一维。写代码前一定要读文档,不然维度错位排查起来极其痛苦——我就在这上边浪费过整整两天。