
各位读者朋友大家好。前面十讲我们一直在围绕物理信息神经网络Physics-Informed Neural NetworksPINN的核心思想和数学原理展开——从微分方程怎么描述物理规律到如何把损失函数拆分为方程残差项和边界条件项。理论铺垫到这里接下来不得不面对一个非常现实的问题在实际落地时我们到底该选 PyTorch 还是 TensorFlow两种框架在 PINN 里的写法差异有多大环境怎么配自动微分写在哪个位置这一讲我们就来完整解决这些问题。本文会从框架选型对比、环境搭建、PyTorch 实现 1D Poisson 方程 PINN、TensorFlow 实现相同案例、常见报错排查、工程建议六个部分展开。代码都能直接复制运行适合已经接触过基础神经网络、准备系统上手 PINN 的读者。1. 为什么 PINN 落地必须关注深度学习框架1.1 PINN 的核心技术依赖PINN 之所以能成为近年来科学计算和深度学习交叉领域的热门方向核心就在于它不需要网格化离散求解微分方程而是直接把物理方程作为约束嵌入神经网络的损失函数。要让这个思路成立框架必须具备两个能力自动微分网络输出对输入坐标求偏导这是构造方程残差的关键。动态计算图或灵活的张量运算在损失函数里自由组合高阶导数、偏导数和非线性项。在 PyTorch 和 TensorFlow 之前我们想实现 PINN 往往需要手动推导梯度表达式甚至用有限差分近似导数效率和精度都很难兼顾。而这两个主流框架把自动微分变成了开箱即用的能力让物理约束的嵌入变成了一种“编程模式”而不是“数学苦力”。举个具体例子。一维热传导方程[ \frac{\partial u}{\partial t} \alpha \frac{\partial^2 u}{\partial x^2} ]在 PINN 中我们需要让神经网络输出 ( u(x,t) )然后自动计算 ( u_t ) 和 ( u_{xx} )再把它们组合成残差项 ( u_t - \alpha u_{xx} ) 作为损失。如果没有自动微分这一步要写非常复杂的链式法则。而在 PyTorch 或 TensorFlow 里只需要调用torch.autograd.grad或者tf.GradientTape就能拿到指定阶数的导数。1.2 框架选择对 PINN 开发的影响选择框架并不只是一个“喜好问题”它会直接影响影响维度具体表现代码表达习惯PyTorch 倾向于“定义网络 前向传播 手动写训练循环”TensorFlow 更常见的是“构建层 compile fit”或自定义训练循环。自动微分 APIPyTorch 使用torch.autograd.gradTensorFlow 使用tf.GradientTape两者语法差异较大。部署生态如果后续要部署到移动端或服务端TensorFlow 的 SavedModel/TFLite 生态更成熟如果做研究原型和快速迭代PyTorch 社区优势更明显。社区资源近年来 PINN 开源库如 DeepXDE、PyDAP 等对两种框架的支持情况不同选型会影响你能复用的代码量。坦白说对于 PINN 学习阶段两个框架都能完成 90% 以上的常见任务。真正影响效率的是你对某一个框架的熟练度而不是框架本身的绝对优劣。这也是本讲同时覆盖两个框架的原因——理解它们的核心差异你就能根据自己的项目场景做判断。2. 环境准备与版本说明2.1 本讲的实验环境清单这一讲的代码开发环境如下操作系统Windows 11 / Ubuntu 22.04两种系统均可 编程语言Python 3.10 深度学习框架PyTorch 2.1.0 / TensorFlow 2.15.0 数值计算NumPy 1.26.0 可视化Matplotlib 3.8.0 IDEVS Code 或 PyCharm版本需要根据你的项目实际情况调整。如果机器没有 GPU完全可以用 CPU 运行本讲的案例因为 1D Poisson 方程的 PINN 参数量很小几分钟就能训练完。有 NVIDIA GPU 并且安装了 CUDA 环境可以使用 GPU 加速。本文以 CPU 环境为主要演示目标方便绝大多数读者直接复现。2.2 PyTorch 安装过程推荐通过 Anaconda 创建独立虚拟环境避免不同项目之间的依赖冲突。打开终端或 Anaconda Prompt执行以下命令# 创建并激活虚拟环境 conda create -n pinn python3.10 conda activate pinn # 安装 CPU 版 PyTorch pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu如果读者使用的是 GPU 环境需要先确认自己的 CUDA 版本然后从 PyTorch 官网选择对应的安装命令。例如 CUDA 11.8 环境可以使用pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118安装完成后在 Python 中验证import torch print(torch.__version__) # 输出框架版本 print(torch.cuda.is_available()) # GPU 是否可用CPU 环境为 False2.3 TensorFlow 安装过程在同一台机器上不太建议把 PyTorch 和 TensorFlow 装在同一个环境避免底层依赖例如numpy、protobuf、absl-py版本冲突。建议再新建一个虚拟环境conda create -n pinn-tf python3.10 conda activate pinn-tf # 安装 CPU 版 TensorFlow pip install tensorflow-cpu2.15.0如果希望使用 GPU需要安装tensorflow而非tensorflow-cpu并确认本机满足 CUDA、cuDNN 的版本要求。TensorFlow 对 CUDA 版本的匹配比 PyTorch 更严格如果不想折腾学习阶段推荐 CPU 版。验证安装import tensorflow as tf print(tf.__version__) print(len(tf.config.list_physical_devices(GPU))) # CPU 环境通常输出 02.4 自动微分基础对比在进入完整案例之前我们先看两种框架自动微分的核心语法差异。这是 PINN 里最常写的一段代码。2.5 案例选择一维 Poisson 方程为了让对比更直观本讲选择同一个物理问题分别用 PyTorch 和 TensorFlow 实现。这个案例足够简单适合讲清楚框架写法又足够典型能覆盖 PINN 的通用结构。考虑一维 Poisson 方程[ \frac{d^2 u}{dx^2} -\pi^2 \sin(\pi x) ]定义域为 ( x \in [0, 1] )边界条件为[ u(0) 0, \quad u(1) 0 ]这个方程的精确解为 ( u(x) \sin(\pi x) )。选择它有两点好处一是解析解已知方便我们验证 PINN 训练结果二是方程右端项足够简单让初学者能专注于框架代码结构。3. PyTorch 实现 PINN 解决 1D Poisson 方程3.1 项目结构设计本讲的 PyTorch 代码放在以下结构里pinn-pytorch/ ├── pinn_poisson.py └── requirements.txtpinn_poisson.py是完整的训练脚本包含网络定义、损失函数、训练循环和可视化。下面直接给出完整代码。3.2 完整代码# 文件路径pinn-pytorch/pinn_poisson.py import torch import torch.nn as nn import numpy as np import matplotlib.pyplot as plt # 固定随机种子保证结果可复现 torch.manual_seed(42) np.random.seed(42) # 定义物理参数 PI np.pi # 定义神经网络输入 x输出 u(x) class PINN(nn.Module): def __init__(self): super(PINN, self).__init__() self.net nn.Sequential( nn.Linear(1, 20), nn.Tanh(), nn.Linear(20, 20), nn.Tanh(), nn.Linear(20, 20), nn.Tanh(), nn.Linear(20, 1) ) def forward(self, x): return self.net(x) # 实例化网络 net PINN() # 自动微分函数计算 u 对 x 的二阶导数 def compute_second_derivative(x): x x.clone().requires_grad_(True) u net(x) u_x torch.autograd.grad(u, x, grad_outputstorch.ones_like(u), create_graphTrue, retain_graphTrue)[0] u_xx torch.autograd.grad(u_x, x, grad_outputstorch.ones_like(u_x), create_graphTrue, retain_graphTrue)[0] return u, u_xx # 损失函数方程残差 边界条件 def loss_function(x_collocation, x_boundary): # 方程残差损失 u, u_xx compute_second_derivative(x_collocation) f -PI**2 * torch.sin(PI * x_collocation) residual_loss torch.mean((u_xx - f) ** 2) # 边界条件损失 u_boundary net(x_boundary) boundary_loss torch.mean(u_boundary ** 2) return residual_loss boundary_loss, residual_loss, boundary_loss # 生成配置点定义域内部随机采样 x_collocation torch.linspace(0, 1, 100).reshape(-1, 1) x_collocation x_collocation 0.01 * torch.randn_like(x_collocation) x_collocation torch.clamp(x_collocation, 0, 1) # 边界点 x_boundary torch.tensor([[0.0], [1.0]]) # 优化器 optimizer torch.optim.Adam(net.parameters(), lr1e-3) # 训练循环 epochs 3000 for epoch in range(epochs): optimizer.zero_grad() total_loss, res_loss, bc_loss loss_function(x_collocation, x_boundary) total_loss.backward() optimizer.step() if (epoch 1) % 500 0 or epoch 0: print(fEpoch {epoch1:4d} | Total Loss: {total_loss.item():.6e} f| Residual: {res_loss.item():.6e} f| Boundary: {bc_loss.item():.6e}) # 测试对比精确解 x_test torch.linspace(0, 1, 200).reshape(-1, 1) u_pred net(x_test).detach().numpy() u_exact np.sin(PI * x_test.numpy()) # 打印相对 L2 误差 error np.linalg.norm(u_pred - u_exact) / np.linalg.norm(u_exact) print(f相对 L2 误差: {error:.6f}) # 可视化 plt.figure(figsize(8, 5)) plt.plot(x_test.numpy(), u_exact, b-, labelExact Solution) plt.plot(x_test.numpy(), u_pred, r--, labelPINN Prediction) plt.xlabel(x) plt.ylabel(u(x)) plt.legend() plt.grid(True) plt.savefig(poisson_pinn_pytorch.png, dpi150) plt.show()3.3 代码关键点解读compute_second_derivative是 PINN 的核心。我们先让输入x开启梯度追踪requires_gradTrue把x传入网络得到u再用torch.autograd.grad求一阶导数u_x最后在u_x基础上再求一次得到二阶导数u_xx。注意这里有两个参数很重要create_graphTrue让求导过程保留计算图这样第二次求导能基于第一次导数的图继续展开。retain_graphTrue默认每次backward()后计算图会释放这里保留是为了后面继续用。损失函数分两部分方程残差损失把u_xx与右端项-π² sin(πx)的差平方后取平均。边界条件损失网络在边界点x0和x1的预测值应接近 0。训练结果每 500 个 epoch 输出一次。训练完成后用相对 L2 误差评估模型的预测精度。对于这个简单问题3000 轮训练后误差通常能降到 0.01 以下。4. TensorFlow 实现 PINN 解决相同的 1D Poisson 方程4.1 项目结构设计TensorFlow 版本的代码结构如下pinn-tensorflow/ └── pinn_poisson_tf.py这个文件会包含同样的网络结构、损失函数和训练流程但 API 风格差异明显。4.2 完整代码# 文件路径pinn-tensorflow/pinn_poisson_tf.py import tensorflow as tf import numpy as np import matplotlib.pyplot as plt # 固定随机种子 tf.random.set_seed(42) np.random.seed(42) PI np.pi # 使用 Keras 构建网络 class PINN(tf.keras.Model): def __init__(self): super(PINN, self).__init__() self.hidden1 tf.keras.layers.Dense(20, activationtanh) self.hidden2 tf.keras.layers.Dense(20, activationtanh) self.hidden3 tf.keras.layers.Dense(20, activationtanh) self.output_layer tf.keras.layers.Dense(1) def call(self, x): x self.hidden1(x) x self.hidden2(x) x self.hidden3(x) return self.output_layer(x) net PINN() # 自动微分通过 GradientTape 求二阶导数 def compute_second_derivative(x): with tf.GradientTape(persistentTrue) as tape2: tape2.watch(x) with tf.GradientTape() as tape1: tape1.watch(x) u net(x) u_x tape1.gradient(u, x) u_xx tape2.gradient(u_x, x) return u, u_xx # 损失函数 def loss_function(x_collocation, x_boundary): # 方程残差损失 u, u_xx compute_second_derivative(x_collocation) f -PI**2 * tf.sin(PI * x_collocation) residual_loss tf.reduce_mean(tf.square(u_xx - f)) # 边界条件损失 u_boundary net(x_boundary) boundary_loss tf.reduce_mean(tf.square(u_boundary)) return residual_loss boundary_loss, residual_loss, boundary_loss # 生成配置点 x_collocation tf.linspace(0.0, 1.0, 100) x_collocation tf.reshape(x_collocation, (-1, 1)) x_collocation x_collocation 0.01 * tf.random.normal(x_collocation.shape) x_collocation tf.clip_by_value(x_collocation, 0.0, 1.0) # 边界点 x_boundary tf.constant([[0.0], [1.0]]) # 优化器 optimizer tf.keras.optimizers.Adam(learning_rate1e-3) # 自定义训练步骤 tf.function def train_step(): with tf.GradientTape() as tape: total_loss, res_loss, bc_loss loss_function(x_collocation, x_boundary) grads tape.gradient(total_loss, net.trainable_variables) optimizer.apply_gradients(zip(grads, net.trainable_variables)) return total_loss, res_loss, bc_loss # 训练循环 epochs 3000 for epoch in range(epochs): total_loss, res_loss, bc_loss train_step() if (epoch 1) % 500 0 or epoch 0: print(fEpoch {epoch1:4d} | Total Loss: {total_loss.numpy():.6e} f| Residual: {res_loss.numpy():.6e} f| Boundary: {bc_loss.numpy():.6e}) # 测试 x_test tf.linspace(0.0, 1.0, 200) x_test tf.reshape(x_test, (-1, 1)) u_pred net(x_test).numpy() u_exact np.sin(PI * x_test.numpy()) error np.linalg.norm(u_pred - u_exact) / np.linalg.norm(u_exact) print(f相对 L2 误差: {error:.6f}) # 可视化 plt.figure(figsize(8, 5)) plt.plot(x_test.numpy(), u_exact, b-, labelExact Solution) plt.plot(x_test.numpy(), u_pred, r--, labelPINN Prediction) plt.xlabel(x) plt.ylabel(u(x)) plt.legend() plt.grid(True) plt.savefig(poisson_pinn_tensorflow.png, dpi150) plt.show()4.3 代码关键点解读TensorFlow 版本的自动微分方式与 PyTorch 完全不同用的是tf.GradientTape上下文管理器。两层 GradientTape 求二阶导内层tape1记录对x求一阶导数的计算过程。外层tape2使用persistentTrue在内层求出u_x后再对u_x求一次导数。persistentTrue是因为默认 GradientTape 调用一次gradient()后资源就会被释放设置成持久化才能在后续继续求导。tf.function装饰器它会把 Python 函数编译成 TensorFlow 计算图提高训练速度。对于 PINN 这种涉及多次自动微分和梯度累积的训练循环这个优化效果很明显。不过初学阶段即使不加这行装饰器代码也能正常运行。训练循环手动实现TensorFlow 中如果不使用 Keras 的compilefit高层 API就需要像这里一样手动用tf.GradientTape记录损失调用tape.gradient计算梯度再用apply_gradients更新参数。这是实现 PINN 最灵活的方式因为自定义损失函数中包含高阶导数很难直接塞进compile的标准流程。5. PyTorch 与 TensorFlow 实现 PINN 的全面对比5.1 代码结构对比从上面的案例可以清楚看到两种框架的差异对比维度PyTorchTensorFlow网络定义风格继承nn.Module以forward方法组织前向传播继承tf.keras.Model或使用tf.keras.Sequential以call方法组织自动微分torch.autograd.grad每次求导显式传入grad_outputstf.GradientTape上下文管理器watch 后自动记录二阶导数多次调用torch.autograd.grad注意create_graph参数嵌套两层tf.GradientTape注意persistent参数损失函数普通 Python 函数返回张量直接参与反向传播需要手动构造返回张量后在train_step里求梯度训练循环optimizer.zero_grad()→loss.backward()→optimizer.step()tape.gradient(loss, vars)→optimizer.apply_gradients()整体来看PyTorch 的训练循环“更像面向对象编程”——把优化器、网络、损失都作为对象来操作TensorFlow 自定义训练循环“更像函数式编程”——用装饰器和上下文管理器把计算过程包裹起来。5.2 高阶导数的处理差异这是 PINN 实现中最重要的框架差异值得单独展开。在PyTorch中求高阶导数的核心是create_graphTrue。每一步autograd.grad都会在前一次导数结果上继续构建求导图因此能支持任意阶导数。不过要注意随着导数阶数增加内存开销也会成倍增长。在TensorFlow中求高阶导数的核心是嵌套GradientTape和persistentTrue。一层 tape 对应一阶导数两层 tape 对应二阶导数。如果需要三阶导数就需要三层嵌套代码会变得比较深。5.3 PINN 场景下的框架选型建议两者都能完成 PINN 的基础实现。具体选择可以参考以下场景科研探索和快速原型优先 PyTorch。调试体验好报错信息相对直观社区里 PINN 论文的开源代码多数是 PyTorch 写的。工业部署和服务化如果团队已经有 TensorFlow Serving 或 TFLite 的部署链路选 TensorFlow 会更顺畅。复现开源项目先看你要复现的项目用什么框架。DeepXDE 底层从 TensorFlow 1.x 演进到同时支持两种框架但很多早期项目还是 TensorFlow 风格。刚入门深度学习如果你完全零基础建议从 PyTorch 开始学习曲线更平滑。掌握 PyTorch 之后再切换到 TensorFlow其实只需要花几天适应 API 差异。6. 常见问题与排查思路6.1 两种框架常见报错对照表问题现象可能原因解决思路PyTorch 报RuntimeError: element 0 of tensors does not require grad输入x未设置requires_gradTrue在求导前调用x.requires_grad_(True)或创建变量时指定PyTorch 报RuntimeError: Trying to backward through the graph a second time多次调用backward()但未保留计算图在损失函数里设置retain_graphTrue或把多次求导合并成一次backward()TensorFlow 报RuntimeError: GradientTape.gradient can only be called once on non-persistent tapes默认 tape 使用一次后就释放创建 tape 时设置persistentTrueTensorFlow 报ValueError: No gradients provided for any variable网络变量与损失之间没有建立计算图连接检查是否在GradientTape作用域内调用网络检查输入是否watch训练 Loss 下降很慢学习率过小尝试增大learning_rate到 1e-2观察后再微调Loss 前期不降反升学习率过大或网络初始化不好降低学习率到 1e-4或换用tanh激活函数二阶导数全为 0使用了ReLU激活函数ReLU 的二阶导数几乎处处为 0建议使用Tanh或SiLU训练结果与解析解差距很大配置点数量不足或网络容量小增加配置点数量或增加网络隐藏层宽度6.2 一个容易被忽略的坑激活函数选择很多初学者照搬图像分类的网络结构使用 ReLU 作为隐藏层激活函数结果 PINN 训练总是失败。原因是 ReLU 在非零区域的一阶导数为常数 1二阶导数为 0这导致方程残差里二阶导数项完全失去信息。PINN 常用的激活函数有Tanh光滑、二阶导数非零本讲案例使用它。SiLU也就是 Swish同样光滑在某些问题里收敛更快。Sin有人发现使用正弦激活函数在某些波动问题上表现更好。实际使用中Tanh 是最稳妥的默认选择。6.3 配置点的采样策略本讲的代码里用了简单的均匀采样加微小噪声。实际项目中配置点采样方式会影响训练稳定性和精度简单均匀采样适合低维问题实现最容易。随机采样每个 epoch 重新采样相当于训练过程中不断更换配置点能让模型看到更多位置的信息。自适应采样在残差较大的区域加密采样精度更高但实现复杂。Latin Hypercube 采样在多个维度上均匀分布适合高维输入。初学者先用均匀采样就好后面遇到精度瓶颈再考虑自适应方案。7. 最佳实践与工程建议7.1 代码组织规范PINN 项目往往要反复修改方程、边界条件和网络结构建议从一开始就按下面方式组织代码pinn-project/ ├── models/ │ ├── __init__.py │ └── pinn_net.py # 网络定义 ├── physics/ │ ├── __init__.py │ └── equations.py # 方程残差、边界条件 ├── train.py # 训练脚本 ├── evaluate.py # 测试、误差分析 └── config.yaml # 超参数配置把网络定义、物理方程、训练循环分离换方程时只需要修改equations.py换网络结构时只需要修改pinn_net.py能显著降低实验成本。7.2 训练过程的可视化与监控强烈建议在训练过程中定期保存 loss 曲线和预测结果。即使是最简单的 1D 问题看到 loss 从 1e-1 降到 1e-4 的过程也能帮助你判断模型是否真正收敛。# 示例在训练循环中记录 loss 并保存到列表 history {total: [], residual: [], boundary: []} for epoch in range(epochs): optimizer.zero_grad() total_loss, res_loss, bc_loss loss_function(x_collocation, x_boundary) total_loss.backward() optimizer.step() history[total].append(total_loss.item()) history[residual].append(res_loss.item()) history[boundary].append(bc_loss.item()) # 训练后绘制 loss 曲线 plt.figure(figsize(8, 5)) plt.semilogy(history[total], labelTotal) plt.semilogy(history[residual], labelResidual) plt.semilogy(history[boundary], labelBoundary) plt.xlabel(Epoch) plt.ylabel(Loss) plt.legend() plt.grid(True) plt.show()7.3 超参数调试顺序PINN 训练不收敛时不要一次改多个参数。建议按下面顺序排查激活函数先确认用了tanh或sin而不是 ReLU。学习率在 1e-4 到 1e-2 之间尝试计算量允许的话做几次小规模扫描。网络宽度和深度从 3 层 × 20 个神经元开始不要一上来就堆大网络。配置点数量从 100 个开始逐步增加到 500、1000观察精度提升是否明显。边界条件的权重如果边界损失比残差损失大很多可以给边界项乘以大于 1 的权重系数。7.4 性能与内存优化建议PINN 的高阶导数计算比较消耗内存尤其是三维、四维问题时训练可能非常吃力。以下几点很实用使用小批量训练mini-batch而不是一次把全部配置点送入网络。适当降低网络宽度PINN 的精度不一定和网络容量成正比。优先使用 Adam 优化器收敛稳定对初始学习率不敏感。如果物理场变化剧烈考虑对输入做归一化让坐标范围落在 0 到 1 之间。训练后期可以切换 L-BFGS 优化器进行微调在很多 PINN 案例中能进一步提升精度。# PyTorch 中可以在 Adam 之后继续使用 L-BFGS 微调 optimizer_bfgs torch.optim.LBFGS(net.parameters(), lr1.0, max_iter500) def closure(): optimizer_bfgs.zero_grad() total_loss, _, _ loss_function(x_collocation, x_boundary) total_loss.backward() return total_loss optimizer_bfgs.step(closure)7.5 生产环境与安全边界如果 PINN 要用于实际工程场景比如结构分析、流场预测务必注意保留验证集不能只看训练集上的 loss要用独立的精确解或实验数据验证模型精度。控制模型输出边界神经网络可能在某些区域给出物理上不合理的值生产使用时应加后处理校验。保存模型版本训练好的模型、配置参数、训练数据都应记录版本方便复现和回滚。合规使用开源代码如果参考了别人的 PINN 开源实现注意检查开源协议。8. 总结与下一步学习方向这一讲我们完成了两件关键事情。第一理解了 PINN 对深度学习框架的核心需求——自动微分、灵活的计算图、自定义损失函数。第二用 PyTorch 和 TensorFlow 分别实现了一个完整的 1D Poisson 方程 PINN 案例跑通了从网络定义到高阶导数计算、训练、验证的全流程。两种框架的代码已经放在上面的小节里。建议读者先复制运行 PyTorch 版本把训练循环中的每一行都读懂再对照 TensorFlow 版本找出对应的代码片段。这种“同一问题、两种框架”的对比学法能让你在最短时间内建立起框架迁移能力。关于下一步学习推荐按照以下路径推进把方程从 1D Poisson 扩展到 2D Laplace 方程体会多个输入维度下梯度计算的变化。给 PINN 加入时间维度实现一维热传导方程理解初值条件和边界条件的处理方式。阅读 DeepXDE 源码中关于网络初始化和边界条件施加的部分学习工程化的 PINN 写法。尝试用 PINN 求解带源项的非线性方程比如 Burgers 方程体会损失函数中非线性残差项的写法。PINN 这个方向的价值在于它打通了“物理规律”和“数据驱动”两套语言。本讲完成了框架层的准备接下来就能把更多精力放到方程本身的设计和物理约束的构造上。如果本文对你有帮助可以收藏备用下一篇我们继续深入 PINN 的损失函数设计细节。