2. 分子生成基础:分子表示与生成任务

好,咱们进入正题。做分子生成,第一件事就是搞清楚:计算机到底怎么“看懂”一个分子?说白了,就是怎么把化学结构翻译成它能处理的数据。我个人习惯把这一步叫做“分子翻译官”——翻译得好不好,直接决定了后续模型能不能学好。

2.1 分子表示:从化学结构到张量

分子表示的方式有很多种,但用 GPU 加速做生成训练,我们最关心的就三种:

  • SMILES 字符串:一种用 ASCII 字符描述分子结构的线性表示法。简单,人类可读,但有很多“语法陷阱”。
  • 分子图 (Molecular Graph):原子是节点,化学键是边。这是图神经网络的最爱,天然适合 GPU 并行。
  • 3D 坐标 + 特征:包含原子的空间位置和物理化学属性。对生成构象或做药效团匹配很关键。

我最开始在项目里用 SMILES,觉得它轻便。结果后来发现,很多看似合法的 SMILES 字符串,生成的分子三维结构根本不合理——比如空间位阻巨大。嗯,这里要注意,SMILES 的“语法合法”和“化学合理”是两码事。

2.1.1 常见分子表示对比

表示方法 数据形式 GPU 友好度 典型应用场景 我踩过的坑
SMILES 字符串 低(需 tokenize) 生成式模型(如 RNN, Transformer) 语法合法但化学不合理
分子图 邻接矩阵 + 节点特征 高(批量稀疏操作) 图神经网络生成 批次内分子尺寸差异大
3D 坐标 + 特征 浮点数张量 非常高(矩阵运算) 构象生成、等变模型 坐标对齐和旋转不变性
我的个人习惯:如果项目初期数据量小,先用 SMILES 快速验证模型架构。等要追求生成质量和多样性时,果断切换到分子图或 3D 表示。GPU 的并行计算能力在图和 3D 表示上才能发挥到极致。

2.1.2 为什么分子图在 GPU 上这么香?

你想想看,分子图天然就是稀疏的。一个大分子可能几十个原子,但每个原子只跟周围 3-4 个原子成键。用传统的密集矩阵去表示,GPU 的显存和算力都浪费在了那些不存在的“键”上。所以,我们得用稀疏张量存储。

在 PyTorch 里,我一般这么搞:

# 一个简单的分子图稀疏表示示例
# 节点特征: [num_atoms, feat_dim]
# 边索引: [2, num_edges]  (COO 格式)
# 边特征: [num_edges, edge_feat_dim]

import torch

# 假设分子有 5 个原子,每个原子有 10 个特征
node_features = torch.randn(5, 10)

# 定义化学键:原子0-原子1, 原子1-原子2, 原子2-原子3, 原子3-原子4
edge_index = torch.tensor([[0, 1, 1, 2, 2, 3, 3, 4],
                            [1, 0, 2, 1, 3, 2, 4, 3]], dtype=torch.long)

# 每条边有 4 个特征(如:键类型、键长、是否芳香...)
edge_attr = torch.randn(8, 4)

print(f"节点矩阵占用: {node_features.numel() * 4 / 1024:.2f} KB")
print(f"边索引占用: {edge_index.numel() * 4 / 1024:.2f} KB")

你看,这才是 GPU 友好的方式。我在一个早期项目里,没注意分子尺寸差异,导致批次内需要大量 padding。结果显存白白浪费了 40% 多。避坑指南:尽可能按分子尺寸动态分组再送入 GPU。

2.2 分子生成任务:我们到底在做什么?

搞清楚表示后,咱们得聊聊任务。分子生成不是单一问题,它有很多变种。我把它分成三大类:

2.2.1 任务分类与定义

  1. 从头生成 (De Novo Generation):给定一些目标性质(如 LogP, 合成可及性),从无到有生成全新分子。这是最“炫酷”但也最难的。
  2. 条件生成 (Conditional Generation):给定一个分子骨架或片段,让你生成合理的延伸或替换。例如,给定一个苯环,生成针对特定靶点的侧链。
  3. 优化/插值 (Optimization / Interpolation):输入一个分子,输出一个性质更好的类似物;或者让你在分子空间中平滑地从一个分子“走”到另一个。

这三种任务对分子表示和模型架构的要求完全不同。举个例子,从头生成通常用自回归模型(像 Transformer),把 SMILES 当句子生成;而条件生成更倾向于用基于图的模型,因为你必须保持输入骨架的拓扑结构。

关键认知:别指望一个模型能搞定所有生成任务。我刚开始做的时候,觉得只要模型够大,什么都行。结果发现,在生成任务上,“一专多能”比“样样稀松”靠谱得多。针对具体任务选对你的“分子表示 + 模型架构”组合。

2.2.2 任务形式化定义(你要懂的核心)

不管哪种任务,在 GPU 训练的视角下,都可以抽象成一个条件概率模型:

# 伪代码:一个通用的分子生成损失函数思想
# 最大化生成分子的概率,给定条件

def loss_function(generated_molecule, target_property, condition):
    # 生成一个分子,希望它:
    # 1. 化学上有效(所有原子价态正确)
    # 2. 性质上满足目标(如 LogP 在 1-3 之间)
    # 3. 结构上符合约束(包含给定的核心片段)
    validity_loss = compute_validity(generated_molecule)
    property_loss = compute_property_diff(generated_molecule, target_property)
    condition_loss = compute_condition_satisfaction(generated_molecule, condition)
    
    return validity_loss + lmbda * property_loss + gamma * condition_loss

这里面最重要的就是那个lmbda和gamma——超参数。调过的人都知道,那是一个痛苦的过程。我曾经遇到过,模型完美学会了生成合法分子,但性质毫无变化。后来发现是lmbda设得太小,性质损失根本没发挥作用。

2.3 本章知识体系:一张图理清

下面这张图是我自己梳理的。它把分子表示和生成任务的关系串起来了。你可以把它当成一个“检查清单”——项目开始的,对着它选好你的表示方法和任务目标,能少走很多弯路。

分子生成基础核心逻辑 分子表示 (Molecular Representation) SMILES 字符串 分子图 3D 坐标特征 关键: GPU 并行度:图 ≈ 3D > SMILES 生成任务 (Generation Task) 从头生成 条件生成 优化/插值 核心公式: P(分子 | 条件, 性质) 输入 选型与调优核心要点 1. 表示与任务匹配: 从头生成 → SMILES/Transformer;条件生成 → 分子图/GNN 2. GPU 效率: 图数据用稀疏操作,按尺寸分批,避免大量 padding 3. 损失函数: 有效性 + 性质损失,注意 λ 和 γ 的超参数平衡 别忘了验证生成分子的化学合理性(如原子价态、环张力) —— 这是很多项目翻车的根本原因 第 2 章 · 分子生成基础
不要被 SMILES 的“简洁”迷惑。 我曾经在一个项目里,用 SMILES 训练了 3 天,生成的结果一看,80% 是化学上荒谬的结构(比如同一个碳上连了 5 个键)。后来切换到分子图表示,加上化合价约束,这个问题才解决。记住,分子表示不仅仅是数据格式,它还隐含着模型的归纳偏置。

2.4 如何验证你的分子生成器是“好”的?

任务和表示都定了,怎么知道模型学得好不好?我一般看这四个指标:

  • 有效性 (Validity):生成分子中,化学上合理的比例。低于 90% 说明表示或约束没做好。
  • 新颖性 (Novelty):生成的分子不在训练集中。别让模型只会“背诵”。
  • 多样性 (Diversity):生成分子之间的结构差异。分子图空间很大,别生成一堆相似的。
  • 性质达标率 (Property Hit Rate):生成分子中,达到目标性质的占比。这是终极目标。

嗯,这里要注意,有效性和新颖性往往是矛盾的。模型越敢尝试新的,就越容易产生无效结构。你需要找到一个平衡点。我个人在调优时,一般先确保有效性稳定在 95% 以上,再去追求新颖性和多样性。