2. 分子生成基础:分子表示与生成任务
好,咱们进入正题。做分子生成,第一件事就是搞清楚:计算机到底怎么“看懂”一个分子?说白了,就是怎么把化学结构翻译成它能处理的数据。我个人习惯把这一步叫做“分子翻译官”——翻译得好不好,直接决定了后续模型能不能学好。
2.1 分子表示:从化学结构到张量
分子表示的方式有很多种,但用 GPU 加速做生成训练,我们最关心的就三种:
- SMILES 字符串:一种用 ASCII 字符描述分子结构的线性表示法。简单,人类可读,但有很多“语法陷阱”。
- 分子图 (Molecular Graph):原子是节点,化学键是边。这是图神经网络的最爱,天然适合 GPU 并行。
- 3D 坐标 + 特征:包含原子的空间位置和物理化学属性。对生成构象或做药效团匹配很关键。
我最开始在项目里用 SMILES,觉得它轻便。结果后来发现,很多看似合法的 SMILES 字符串,生成的分子三维结构根本不合理——比如空间位阻巨大。嗯,这里要注意,SMILES 的“语法合法”和“化学合理”是两码事。
2.1.1 常见分子表示对比
| 表示方法 | 数据形式 | GPU 友好度 | 典型应用场景 | 我踩过的坑 |
|---|---|---|---|---|
| SMILES | 字符串 | 低(需 tokenize) | 生成式模型(如 RNN, Transformer) | 语法合法但化学不合理 |
| 分子图 | 邻接矩阵 + 节点特征 | 高(批量稀疏操作) | 图神经网络生成 | 批次内分子尺寸差异大 |
| 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 任务分类与定义
- 从头生成 (De Novo Generation):给定一些目标性质(如 LogP, 合成可及性),从无到有生成全新分子。这是最“炫酷”但也最难的。
- 条件生成 (Conditional Generation):给定一个分子骨架或片段,让你生成合理的延伸或替换。例如,给定一个苯环,生成针对特定靶点的侧链。
- 优化/插值 (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 本章知识体系:一张图理清
下面这张图是我自己梳理的。它把分子表示和生成任务的关系串起来了。你可以把它当成一个“检查清单”——项目开始的,对着它选好你的表示方法和任务目标,能少走很多弯路。
2.4 如何验证你的分子生成器是“好”的?
任务和表示都定了,怎么知道模型学得好不好?我一般看这四个指标:
- 有效性 (Validity):生成分子中,化学上合理的比例。低于 90% 说明表示或约束没做好。
- 新颖性 (Novelty):生成的分子不在训练集中。别让模型只会“背诵”。
- 多样性 (Diversity):生成分子之间的结构差异。分子图空间很大,别生成一堆相似的。
- 性质达标率 (Property Hit Rate):生成分子中,达到目标性质的占比。这是终极目标。
嗯,这里要注意,有效性和新颖性往往是矛盾的。模型越敢尝试新的,就越容易产生无效结构。你需要找到一个平衡点。我个人在调优时,一般先确保有效性稳定在 95% 以上,再去追求新颖性和多样性。