加载中…

📍 本章小节进度
0/0 子节完成
✅ 全部子节完成 — 可进入章节测验

7.1 问题定义与物理建模

我们将用一个一维弹簧-质点链作为入门案例。 虽然简单,但它包含了物理仿真GNN的所有核心要素: 节点状态、边相互作用、动力学演化、数据生成和模型训练。

物理模型

考虑N个质量为m的质点,通过N-1个弹簧串联成一条链。 每个弹簧的刚度为k,原长为L₀。 系统的运动由牛顿第二定律和胡克定律控制:

m · d²xi/dt² = Fi,i-1 + Fi,i+1

Fij = k · (|xj - xi| - L₀) · (xj - xi) / |xj - xi|

其中:

数值积分

为了生成训练数据,我们需要一个"ground truth"求解器。 这里使用简单的速度Verlet积分(velocity Verlet):

x(t+Δt) = x(t) + v(t)·Δt + ½·a(t)·Δt²

v(t+Δt) = v(t) + ½·[a(t) + a(t+Δt)]·Δt

Verlet积分是动力学仿真中最常用的积分方法之一, 具有二阶精度、辛结构(能量近似守恒)、实现简单等优点。

为什么选弹簧-质点系统?
1. 物理简单:方程简单易懂,你可以清楚地知道"正确答案"是什么
2. 天然图结构:质点是节点,弹簧是边,直接对应GNN输入
3. 全流程覆盖:从数据生成到训练验证,所有环节都能走通
4. 可扩展性:掌握后很容易扩展到二维/三维、更复杂的物理系统

📝 小节检测

1. 在弹簧-质点系统的GNN表示中,节点和边分别对应什么?
A. 节点对应力,边对应位移
B. 节点对应弹簧,边对应质点
C. 节点对应质点(位置、速度等状态),边对应弹簧(连接关系和属性)
D. 节点对应时间步,边对应状态变化
正确答案:C
在物理系统的图表示中,节点通常对应物理实体(质点)及其状态,边对应它们之间的相互作用(弹簧)。
2. 速度Verlet积分方法的主要优点是什么?
A. 二阶精度、能量近似守恒、实现简单
B. 不需要计算加速度
C. 步长可以无限大
D. 只适用于弹簧系统
正确答案:A
Verlet积分是辛积分器,具有二阶精度、能量长期守恒好、实现简单等优点,是动力学仿真的标准方法。
3. 弹簧-质点系统太简单了,用它学习GNN仿真没有什么实际价值。
A. 正确
B. 错误
正确答案:B(错误)
弹簧-质点系统包含了物理仿真GNN的所有核心要素:图表示、消息传递、动力学演化、训练流程。掌握后可快速扩展到复杂系统。

7.2 数据生成与图构建

首先,我们需要用传统数值方法生成"ground truth"数据, 然后将数据转换为GNN可用的图格式。

# ======================================== # 第一步:生成弹簧-质点系统的训练数据 # ======================================== import numpy as np import torch from torch_geometric.data import Data, Dataset import os # --- 物理参数 --- N_MASS = 10 # 质点数量 MASS = 1.0 # 每个质点的质量 K_SPRING = 100.0 # 弹簧刚度 L0 = 1.0 # 弹簧原长 DT = 0.01 # 时间步长 N_STEPS = 200 # 每个序列的时间步数 def compute_acceleration(positions): """计算每个质点受到的加速度""" n = len(positions) acc = np.zeros(n) for i in range(n): # 左边弹簧的力 if i > 0: dx_left = positions[i] - positions[i-1] force_left = -K_SPRING * (dx_left - L0) acc[i] += force_left / MASS # 右边弹簧的力 if i < n - 1: dx_right = positions[i+1] - positions[i] force_right = -K_SPRING * (dx_right - L0) acc[i] += force_right / MASS return acc def verlet_step(pos, vel, acc, dt): """一步速度Verlet积分""" pos_new = pos + vel * dt + 0.5 * acc * dt**2 acc_new = compute_acceleration(pos_new) vel_new = vel + 0.5 * (acc + acc_new) * dt return pos_new, vel_new, acc_new def generate_trajectory(seed=0): """生成一条仿真轨迹""" np.random.seed(seed) # 初始位置:平衡位置 + 随机扰动 pos0 = np.arange(N_MASS, dtype=np.float64) * L0 pos0 += np.random.normal(0, 0.1, N_MASS) pos0[0] = 0.0 # 左端固定 # 初始速度:随机 vel0 = np.random.normal(0, 0.5, N_MASS) vel0[0] = 0.0 # 左端固定 acc0 = compute_acceleration(pos0) # 积分 positions = [pos0.copy()] velocities = [vel0.copy()] pos, vel, acc = pos0, vel0, acc0 for _ in range(N_STEPS - 1): pos, vel, acc = verlet_step(pos, vel, acc, DT) pos[0] = 0.0 # 保持左端固定 vel[0] = 0.0 positions.append(pos.copy()) velocities.append(vel.copy()) return np.array(positions), np.array(velocities)

图构建

接下来,我们将弹簧-质点系统转换为GNN需要的图格式。 每个时间步的状态构成一张图:

def build_graph(positions, velocities, step): """将某一时刻的状态构建为PyG的Data对象""" pos = positions[step] vel = velocities[step] n = len(pos) # 节点特征:位置、速度、是否为固定端 x = np.zeros((n, 3), dtype=np.float32) x[:, 0] = pos x[:, 1] = vel x[0, 2] = 1.0 # 第0个节点是固定端 # 边索引:相邻质点相连(无向边) src = [] dst = [] for i in range(n - 1): src.extend([i, i+1]) dst.extend([i+1, i]) edge_index = torch.tensor([src, dst], dtype=torch.long) # 边特征:相对位移、距离、原长、刚度 edge_attr = np.zeros((len(src), 4), dtype=np.float32) for e in range(len(src)): i, j = src[e], dst[e] dx = pos[j] - pos[i] edge_attr[e, 0] = dx # 相对位移 edge_attr[e, 1] = abs(dx) # 距离 edge_attr[e, 2] = L0 # 原长 edge_attr[e, 3] = K_SPRING # 刚度 # 目标:下一步的加速度(即速度变化率) # 用数值方法计算的ground truth acc = compute_acceleration(pos) y = torch.tensor(acc, dtype=torch.float32).unsqueeze(1) data = Data( x=torch.tensor(x), edge_index=edge_index, edge_attr=torch.tensor(edge_attr), y=y ) return data

关键设计选择:预测加速度 vs 预测速度变化
在这个简化例子中,我们直接预测加速度,因为它是物理上的基本量(F=ma)。
更复杂的系统中,预测速度变化Δv更常见,因为:
1. 数值上更稳定(输出更小的量)
2. 更容易与积分器结合
3. 模型天然学习到"增量"而非"全量"

📝 小节检测

1. 在物理仿真GNN中,边特征通常包含什么信息?
A. 节点的位置和速度
B. 相对位移、距离、材料属性等与相互作用相关的量
C. 全局系统的总能量
D. 时间步编号
正确答案:B
边特征描述两个节点之间的相互作用属性,如相对位移、距离、弹簧刚度、材料参数等。
2. 为什么GNN仿真模型通常预测加速度或速度变化量,而不是直接预测下一时刻的位置?
A. 数值更稳定,符合残差预测思想,且物理意义更直接
B. 这样参数量更少
C. 这样不需要解码器
D. 预测位置无法训练
正确答案:A
预测加速度/速度变化量更稳定(小量),物理意义直接(F=ma),是残差预测的一种形式。
3. 无向图在PyG中需要存储双向边(i→j和j→i都要有)。
A. 正确
B. 错误
正确答案:A(正确)
PyG的edge_index是有向的(COO格式),表示无向图需要每条边存两个方向。

7.3 GNN模型实现(PyG)

现在,我们使用PyG的MessagePassing基类来实现一个自定义的GNN仿真模型。 模型采用Encoder-Processor-Decoder架构。

import torch import torch.nn as nn import torch.nn.functional as F from torch_geometric.nn import MessagePassing class SpringMassGNN(nn.Module): """ 弹簧-质点系统的GNN仿真模型 Encoder-Processor-Decoder架构 """ def __init__( self, node_in_dim=3, # 节点输入特征维度 edge_in_dim=4, # 边输入特征维度 hidden_dim=64, # 隐藏层维度 num_layers=5, # 消息传递层数 out_dim=1, # 输出维度(加速度) ): super().__init__() # --- Encoder: 将输入特征映射到潜空间 --- self.node_encoder = nn.Sequential( nn.Linear(node_in_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), ) self.edge_encoder = nn.Sequential( nn.Linear(edge_in_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), ) # --- Processor: 多层消息传递 --- self.processor = nn.ModuleList() for _ in range(num_layers): self.processor.append(MPBlock(hidden_dim)) # --- Decoder: 将潜特征映射到物理输出 --- self.decoder = nn.Sequential( nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, out_dim), ) def forward(self, data): x, edge_index, edge_attr = data.x, data.edge_index, data.edge_attr # 编码 h = self.node_encoder(x) e = self.edge_encoder(edge_attr) # 消息传递 for layer in self.processor: h, e = layer(h, edge_index, e) # 解码:预测加速度 acc_pred = self.decoder(h) return acc_pred class MPBlock(MessagePassing): """消息传递块(Graph Network Block风格)""" def __init__(self, hidden_dim): super().__init__(aggr='add') # 聚合方式:求和 # 边更新MLP self.edge_mlp = nn.Sequential( nn.Linear(hidden_dim * 3, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), ) # 节点更新MLP self.node_mlp = nn.Sequential( nn.Linear(hidden_dim * 2, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), ) # LayerNorm 稳定训练 self.node_norm = nn.LayerNorm(hidden_dim) self.edge_norm = nn.LayerNorm(hidden_dim) def forward(self, x, edge_index, edge_attr): """前向传播:先更新边,再聚合更新节点""" # 保存输入,用于残差连接 x_in = x e_in = edge_attr # 1. 更新边特征 row, col = edge_index edge_input = torch.cat([x[row], x[col], edge_attr], dim=-1) edge_attr = self.edge_mlp(edge_input) # 2. 消息传递 & 节点更新 # propagate会自动调用message和aggregate x = self.propagate(edge_index, x=x, edge_attr=edge_attr) # 3. 残差连接 + LayerNorm x = self.node_norm(x + x_in) edge_attr = self.edge_norm(edge_attr + e_in) return x, edge_attr def message(self, x_i, x_j, edge_attr): """ 构造消息:这里我们已经在forward中更新了edge_attr, 直接用更新后的边特征作为消息。 x_i: 目标节点特征, x_j: 源节点特征 """ return edge_attr def update(self, aggr_out, x): """ 节点更新:将聚合后的消息与自身特征拼接,过MLP """ node_input = torch.cat([x, aggr_out], dim=-1) return self.node_mlp(node_input)

模型要点解析:
1. MessagePassing基类:继承它可以方便地实现自定义消息传递
2. 残差连接:每层都有残差,支持深层网络训练
3. LayerNorm:层归一化稳定训练过程
4. 边更新+节点更新:标准的Graph Network块结构
5. 求和聚合:力是矢量叠加的,求和符合物理直觉
6. 预测加速度:Decoder输出加速度,再用积分器推进状态

📝 小节检测

1. MPBlock中加入残差连接的主要目的是什么?
A. 减少参数量
B. 让代码更短
C. 缓解梯度消失,支持训练更深的网络
D. 让每层学习不同的东西
正确答案:C
残差连接可以有效缓解梯度消失问题,使深层GNN能够被有效训练,同时也能减轻过度平滑。
2. 为什么消息聚合选择"求和"(sum)而不是"平均"(mean)?
A. 物理上力是矢量叠加的,求和符合物理直觉
B. 求和计算更快
C. 平均会导致信息丢失
D. 两者效果完全一样,随便选
正确答案:A
在物理仿真中,每个邻居对目标节点的作用力是矢量叠加的,sum聚合方式与这个物理直觉一致。
3. Decoder直接预测下一时刻的位置,而不是加速度或速度变化。
A. 正确
B. 错误
正确答案:B(错误)
在动力学仿真中,通常预测加速度或速度变化量(更稳定、物理意义更直接),然后用积分器推进状态。

7.4 训练、验证与可视化

有了数据和模型,就可以开始训练了。我们将训练模型预测一步加速度, 然后进行多步推演(rollout)来验证长时性能。

# ======================================== # 训练 & 验证 # ======================================== import torch.optim as optim from torch.utils.data import DataLoader import matplotlib.pyplot as plt # --- 生成数据集 --- N_TRAIN = 100 # 训练轨迹数 N_VAL = 20 # 验证轨迹数 train_graphs = [] val_graphs = [] for i in range(N_TRAIN): pos, vel = generate_trajectory(seed=i) # 每隔几步取一个样本(避免过拟合相邻步) for step in range(0, N_STEPS - 1, 5): graph = build_graph(pos, vel, step) train_graphs.append(graph) for i in range(N_VAL): pos, vel = generate_trajectory(seed=1000 + i) # 验证用整条轨迹,用于rollout评估 val_graphs.append((pos, vel)) print(f"训练样本数: {len(train_graphs)}") print(f"验证轨迹数: {len(val_graphs)}") # --- 训练循环 --- device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = SpringMassGNN(hidden_dim=64, num_layers=5).to(device) optimizer = optim.Adam(model.parameters(), lr=1e-3) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=50, gamma=0.5) train_losses = [] val_rollout_errors = [] for epoch in range(100): # --- 训练 --- model.train() epoch_loss = 0.0 for graph in train_graphs: graph = graph.to(device) optimizer.zero_grad() acc_pred = model(graph) loss = F.mse_loss(acc_pred, graph.y) loss.backward() optimizer.step() epoch_loss += loss.item() avg_loss = epoch_loss / len(train_graphs) train_losses.append(avg_loss) scheduler.step() # --- 验证:长时rollout误差 --- if (epoch + 1) % 10 == 0: model.eval() rollout_errors = [] with torch.no_grad(): for pos_true, vel_true in val_graphs: # 从初始状态开始推演 pos = pos_true[0].copy() vel = vel_true[0].copy() errors = [] for step in range(N_STEPS - 1): graph = build_graph( pos[np.newaxis, :], vel[np.newaxis, :], 0 ).to(device) acc_pred = model(graph).cpu().numpy().flatten() acc_pred[0] = 0.0 # 固定端 # 用Verlet积分推进 acc = compute_acceleration(pos) # 真实加速度用于Verlet的半步 pos, vel, _ = verlet_step_with_acc( pos, vel, acc_pred, DT ) pos[0] = 0.0 vel[0] = 0.0 # 记录位置误差 err = np.mean((pos - pos_true[step+1])**2) errors.append(err) rollout_errors.append(np.mean(errors)) avg_rollout_err = np.mean(rollout_errors) val_rollout_errors.append(avg_rollout_err) print(f"Epoch {epoch+1}, Train Loss: {avg_loss:.6f}, " f"Val Rollout MSE: {avg_rollout_err:.6f}")

可视化与分析

训练完成后,需要从多个角度评估模型:

进阶练习:
1. 尝试加入噪声扰动(noise corruption),观察rollout稳定性变化
2. 尝试二维弹簧网络,增加更多物理现象
3. 加入能量守恒损失,将物理约束引入训练
4. 改变节点数量,测试模型的泛化能力
5. 尝试用不同的GNN层(GAT、GIN)对比效果

写在最后: 这个简单的弹簧-质点系统只是入门。真正的碰撞仿真要复杂得多—— 三维、非线性材料、接触碰撞、大变形、多种材料组合…… 但核心的方法论是通用的: 将物理系统建模为图,用消息传递学习局部相互作用, 用注意力捕捉长程依赖,用物理约束保证一致性。 掌握了基础,你就可以继续探索更复杂的系统了!

📝 小节检测

1. 评估GNN仿真模型时,最重要的指标是什么?
A. 训练损失(单步MSE)
B. 长时推演(rollout)误差和稳定性
C. 参数量
D. 训练速度
正确答案:B
单步误差低不代表长时推演好(可能误差累积很快)。实际应用中rollout的精度和稳定性更重要。
2. 以下哪种方法可以提高自回归推演的长期稳定性?
A. 增加模型层数
B. 增大batch size
C. 减小学习率
D. 训练时加入噪声扰动(noise corruption)
正确答案:D
噪声扰动让模型学会处理"带误差"的状态,从而减轻自回归推演中的误差累积问题。
3. 能量守恒等物理量的守恒性是评估物理仿真模型的重要指标。
A. 正确
B. 错误
正确答案:A(正确)
物理量(能量、动量、质量等)的守恒性是衡量仿真物理一致性的重要指标,仅看MSE不够。
4. 完成本课程后,建议的进阶路径不包括?
A. 扩展到二维/三维系统
B. 加入物理约束损失
C. 放弃GNN,改用纯Transformer
D. 尝试更复杂的物理系统(如接触、非线性材料)
正确答案:C
纯Transformer缺乏物理归纳偏置,在物理仿真中通常不是最优选择。更推荐GNN+Transformer的混合架构。
🎴 知识卡片

← 第 6 章 · 工具框架与数据集 →
📝 本章测验