跳转到正文
社区

用 PyTorch Geometric 构建 QM9 偶极矩 GNN 教学基线

准备 QM9 分子图,用仅基于训练集的标准化训练小规模 CPU GCN 基线,并以 Debye 报告验证集选模后的测试 MAE。

难度: 中等 费用: 免费 隐私: 本地 ~60 分钟
开始安装

你将能够

  • 生成可追溯的计算候选产物,并检查文档规定的验收条件。

你将构建什么

基于官方文档的审核稿。本批未执行此工作流,未验证安装、推理、优化或实验结果。

读者与范围

面向分子图回归学习者。目标 0 是 QM9 偶极矩,单位 Debye(D)。核验稿使用两层 GCN、原子特征与图均值池化;明确不使用键类型边特征或三维坐标,不属于先进模型或几何感知的偶极矩估计器。

数据与许可

Figshare 原始数据条目 1057646 明确声明 CC0,与论文许可及 PyG 代码的 MIT 许可分别核验。保留原始 DOI、条目版本和元数据。固定版本 PyG QM9 在安装 RDKit 时使用 DeepChem gdb9.zip 镜像和 Figshare 3195404 未表征名单,排除名单中的分子并提供 19 个标签。要求 RDKit 和全新数据根目录,避免静默复用无 RDKit 的替代预处理包。记录实际样本数及下载/处理文件的 SHA-256;本批未下载数据,也未确认其与原始 XYZ 包的逐字节等价性。不得静默增加过滤规则。

训练约定与产物

种子 37 从随机排列划分互不重叠的 8000/1000/1000 教学集合;故意保留未使用样本,不属于完整基准。目标均值/标准差仅根据训练标签计算。CPU 运行 10 轮 Adam 与标准化 MSE,通过验证 MAE 选择权重,最后仅一次以 D 评估测试 MAE。上游 NNConv 示例在划分前标准化全部标签,因此不能原样复制。预期产物为划分索引、权重、数据哈希、逐轮训练/验证日志和 results.json;执行之前不声称任何评分。

局限与成本

分布内随机划分不能证明骨架或外部泛化,不能根据测试集调参。后续应比较仅基于训练集的常数基线,并另行开展骨架/OOD 评估。上游要求 Python >=3.10;本稿选择 Python 3.11/Linux/CPU,尚无实测依赖锁。安装、下载、内存和训练消耗资源。下载访问公开外部主机,分子数据与训练保留本地。保留 PyG MIT 声明及原始数据引用。

官方参考

分子图数据集与 GCN 训练

PyTorch Geometric

组合组件

PyTorch Geometric

分子图数据集与 GCN 训练 · source 79d33965a40b7fa83616a9f598a0f8619f25d939; Python 3.11; RDKit raw route

基于官方文档的审核稿。本批未执行此工作流,未验证安装、推理、优化或实验结果。

代码开源;算力/存储及外部服务条件由用户另行承担和确认。

查看资源

兼容环境

客户端操作系统架构版本要求
Python Linux不限>= 3.11

安装与测试

1. 明确偶极矩任务与数据许可

Linux

使用单位为 D 的目标 0。保留原始 CC0 数据元数据和固定版 QM9 源码,检查原始下载及未表征分子过滤规则。

官方来源

预期结果

形成目标、来源与处理策略记录。

2. 准备版本化 CPU 环境

Linux

使用 Python 3.11、兼容 CPU PyTorch、NumPy 和 RDKit。按官方 PyG 安装指南安装核验过的 commit,导出解析后的依赖;不宣称存在实测依赖锁。

git clone https://github.com/pyg-team/pytorch_geometric.git
git -C pytorch_geometric checkout 79d33965a40b7fa83616a9f598a0f8619f25d939
python -m pip install ./pytorch_geometric
官方来源

预期结果

记录环境并确认 QM9/GCNConv/DataLoader 可导入。

3. 审核仅训练集预处理与划分

Linux

将随附编写的代码保存为 qm9_gcn_baseline.py。要求 RDKit 和全新数据目录,记录实际数据哈希,固定种子/划分,均值与标准差仅来自训练集目标 0。它是官方接口的组合,不是上游复现结果。

"""Review-only authored baseline using documented PyG APIs; never run in this batch."""
import argparse
import copy
import hashlib
import json
from pathlib import Path
import random
import numpy as np
import rdkit  # Require the inspected raw-processing route, not the no-RDKit archive.
import torch
from torch import nn
import torch_geometric
from torch_geometric.datasets import QM9
from torch_geometric.loader import DataLoader
from torch_geometric.nn import GCNConv, global_mean_pool

p = argparse.ArgumentParser()
p.add_argument('data_root')
p.add_argument('output')
a = p.parse_args()
out = Path(a.output)
out.mkdir(parents=True, exist_ok=False)
root = Path(a.data_root)
if root.exists() and any(root.iterdir()):
    raise ValueError('Use a fresh data root to prevent an undocumented processed cache')
seed = 37
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.use_deterministic_algorithms(True)
torch.set_num_threads(1)
dataset = QM9(str(root))
# The inspected raw route removes uncharacterized molecules; record actual count.
if len(dataset) < 10000:
    raise ValueError('Unexpectedly small QM9 dataset')
ids = torch.randperm(len(dataset), generator=torch.Generator().manual_seed(seed))
# A deliberately small teaching run, not a literature benchmark split.
train_ids, val_ids, test_ids = ids[:8000], ids[8000:9000], ids[9000:10000]
assert not (set(train_ids.tolist()) & set(val_ids.tolist()))
assert not (set(train_ids.tolist()) & set(test_ids.tolist()))
assert not (set(val_ids.tolist()) & set(test_ids.tolist()))
targets = torch.cat([dataset[int(i)].y[:, 0] for i in train_ids])
mu, sigma = targets.mean(), targets.std(unbiased=False)
if not torch.isfinite(targets).all() or not torch.isfinite(sigma) or sigma <= 0:
    raise ValueError('Invalid training targets')
loaders = [DataLoader(dataset[ix], batch_size=64, shuffle=(j == 0),
    num_workers=0, generator=torch.Generator().manual_seed(seed))
    for j, ix in enumerate([train_ids, val_ids, test_ids])]

class Baseline(nn.Module):
    def __init__(self):
        super().__init__()
        self.c1 = GCNConv(dataset.num_node_features, 64)
        self.c2 = GCNConv(64, 64)
        self.head = nn.Linear(64, 1)

    def forward(self, batch):
        x = self.c1(batch.x.float(), batch.edge_index).relu()
        x = self.c2(x, batch.edge_index).relu()
        return self.head(global_mean_pool(x, batch.batch)).flatten()

model = Baseline()  # CPU only; no geometry or edge_attr features in this baseline.
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

@torch.no_grad()
def mae(loader):
    model.eval()
    total, count = 0.0, 0
    for batch in loader:
        predicted = model(batch) * sigma + mu
        errors = (predicted - batch.y[:, 0]).abs()
        if not torch.isfinite(errors).all():
            raise ValueError('Nonfinite evaluation')
        total += errors.sum().item()
        count += batch.num_graphs
    return total / count  # Debye; no normalization statistics fitted to this split.

history, best, weights = [], float('inf'), None
for epoch in range(1, 11):
    model.train()
    loss_sum, n = 0.0, 0
    for batch in loaders[0]:
        optimizer.zero_grad()
        loss = nn.functional.mse_loss(model(batch), (batch.y[:, 0] - mu) / sigma)
        if not torch.isfinite(loss):
            raise ValueError('Nonfinite loss')
        loss.backward()
        optimizer.step()
        loss_sum += loss.item() * batch.num_graphs
        n += batch.num_graphs
    val = mae(loaders[1])
    history.append({'epoch': epoch, 'train_normalized_mse': loss_sum / n, 'val_mae_D': val})
    if val < best:
        best, weights = val, copy.deepcopy(model.state_dict())
model.load_state_dict(weights)
test_mae = mae(loaders[2])  # Test once after validation-only selection.
torch.save(weights, out / 'best_state_dict.pt')
np.savez(out / 'splits.npz', train=train_ids.numpy(), validation=val_ids.numpy(), test=test_ids.numpy())
files = {str(f.relative_to(root)): hashlib.sha256(f.read_bytes()).hexdigest()
    for f in root.rglob('*') if f.is_file()}
report = {'target': 0, 'property': 'dipole moment', 'unit': 'D', 'seed': seed,
    'dataset_count': len(dataset), 'split_sizes': [8000, 1000, 1000],
    'train_mean_D': mu.item(), 'train_std_D': sigma.item(), 'epochs': 10,
    'validation_mae_D': best, 'test_mae_D': test_mae, 'history': history,
    'versions': {'torch': torch.__version__, 'pyg': torch_geometric.__version__,
        'rdkit': rdkit.__version__, 'numpy': np.__version__},
    'device': 'cpu', 'dataset_file_sha256': files,
    'scope': 'Teaching baseline; small random split, no bond types or coordinates; no transfer guarantee.'}
(out / 'results.json').write_text(json.dumps(report, indent=2) + '\n')
官方来源

预期结果

明确互不重叠的教学划分与仅训练集标准化。

4. 训练并评估,不用测试集选模

Linux

在已记录环境中使用全新数据和输出目录。检查图形状、有限特征/标签及数据清单;仅用验证集选模,并以 D 报告最后一次测试 MAE。

python qm9_gcn_baseline.py qm9_fresh qm9_review
官方来源

预期结果

预期生成 splits.npz、best_state_dict.pt 和 results.json,含逐轮日志、带单位指标及数据哈希;本批没有实际指标。

故障排除

  • RDKit 缺失或旧缓存:配置所需环境并使用全新目录。
  • 标签/损失非有限:停止并检查数据和单位,不静默替换。
  • 数量异常:检查排除名单及文件哈希。
  • MAE 较差:比较训练均值基线并检查划分,不根据测试集调参。
仍无法运行

替代方案

官方 NNConv 属于另一个使用键/几何信息的方向;改编前需修正全数据标准化。此 GCN 基线不宣称质量等价。