共计 13602 个字符,预计需要花费 35 分钟才能阅读完成。
在之前公布的文章中,咱们介绍了飞桨全量反对业内优良科学计算深度学习工具 DeepXDE。本期次要介绍基于飞桨动态图模式对 DeepXDE 中 PINN 办法用例实现、验证及评估的具体流程,同时提供典型环节的代码,旨在帮忙大家更加高效地基于飞桨框架进行科学计算用例建设与调试。
01 用例验证及评估规范
PINN(Physics-Informed Neural Network)是一种交融神经网络和物理机理的深度学习办法,可用于求解各类微分方程(PDE/ODE/IDE/FPDE 等)形容的物理问题。如下图所示,PINN 办法可利用于正问题与逆问题场景。在正问题求解中,可依据自变量和相干参数求解物理方程中的因变量信息,如流场中的速度、压力等信息;在逆问题求解中,依据自变量同时求解方程中的因变量和反演相干参数,如基于局部监督数据可对流场的速度、压力等信息进行预测,同时逆向反演管制方程中的密度、粘度等未知参数。更多对于 PINN 办法的信息,可参考飞桨公众号专题文章“ 飞桨减速 CFD(计算流体力学)原理与实际 ”。
PINN 办法根底原理
相干链接: https://en.wikipedia.org/wiki/Physics-informed_neural_networks
针对 DeepXDE 中提供的 44 个 PINN 办法用例,飞桨目前实现了全量反对,而 PyTorch 目前反对 31 个。基于飞桨对 DeepXDE 中全量用例进行验证及评估的过程中,咱们在 DeepXDE 作者陆路老师的领导下,根据用例中的物理方程是否已知解析解(标签值)制订了两种验证及评估办法,即 “基于训练后果与解析解的误差” 和 “基于网络损失函数值”。具体如下:
Part-1 基于训练后果与解析解的误差
针对已知科学计算问题中物理机理方程解析解的状况,能够间接应用飞桨进行科学计算问题的物理机理方程求解,并计算训练解(网络输入)绝对于解析解(真值)的误差(如 RMSE_Loss、L2_Loss 等),从而对飞桨求解后果进行验证。当 RMSE_Loss 后果小于 \(10^{-2} \) 时,通常可认为飞桨框架对此用例的验证符合要求。此外,在应用飞桨对 DeepXDE 中的用例进行后果评估时,若训练解(网络输入)与解析解的误差后果 RMSE_Loss 不小于 \(10^{-2} \),同时该用例提供了其余框架的公开验证后果,可进行比照评估。若飞桨与其余同类框的 RMSE_Loss 靠近,则阐明以后测试用例的精度次要与其自身算法实现无关,飞桨计算的训练解合乎以后测试用例的自身精度。若要进一步晋升训练解的精度,则须要批改官网用例的求解算法。
Part-2 基于训练网络的损失函数值
针对未知科学计算问题中物理机理方程解析解的状况,应用飞桨进行收敛性测试时,因为短少可参考的解析解或标签值,将训练过程中损失函数 RMSE_Loss 降落的量级作为模型是否收敛的判断规范。个别状况下,咱们须要对初始 loss 值进行归一化,当训练过程中 loss 值降落至 10\(^{-4} \) 量级时认为模型收敛。在实现飞桨框架收敛自测后,若对应的用例曾经在其余框架(如 PyTorch 或 TensorFlow)上进行了公开测试,也可进一步比照飞桨与同类框架的 loss 收敛曲线,评估它们之间的类似度。首先,须要保障不同框架之间 loss 收敛曲线吻合,即图像上无显著不吻合的片段;其次,当 loss 的绝对值大于 1 时,要求飞桨与同类框架 loss 值之间的相对误差达到 \(10^{-6}\) 量级,当 loss 值降落至 1 以下后,若 loss 值的相对误差达到 \(10^{-5} \) 量级,也可认为正当(绝对误差达到 \(10^{-6}\) 量级)。
02 用例验证及评估流程
Part-1 用例实现
采纳 PINN 办法的用例训练过程如下图所示。图中蓝色局部为网络参数更新所须要验证和评估的数据(若须要进行同类框架比照时),黄色局部为用例实现的具体逻辑。
用例训练过程
依照图中步骤,基于 PINN 办法建设用例须要逐次实现训练网络、偏导方程及边界方程、loss 合并计算、反向流传、网络参数更新。上面联合 DeepXDE 中的 PINN 办法用例,具体介绍其实现流程。
训练网络
目前 PINN 办法通常应用全连贯神经网络, 可通过继承 paddle.nn.Layer 来实现各类办法的利用。网络输出数据为物理机理方程的自变量,输入数据为微分方程的因变量。具体地,以基于 Navier-Stokes 方程求解稳态流场问题为例,输出数据包含时空坐标(x, y, z),而输入数据则包含速度和压力信息(u, v, w, p)。
值得注意的是,DeepXDE 提供的局部用例在全连贯网络中减少了 transform 的解决,因而在继承 paddle.nn.Layer 来定义全连贯网络时,须要在网络中定义 transform 接口,详见《附录 -class NN》。在网络结构初始化时,须要给定网络层数、各层参数、激活函数以及参数初始化办法等,如上面代码所示。用户可灵便设置以上参数的传递形式,详见《附录 -class FNN》。用户可实例化以上网络,并调用 forward 函数进行训练。
偏导方程和边界方程
PINN 办法用例的外围是采纳物理机理方程和初边值方程(条件)来调整训练网络的优化方向。这些方程多为各类微分方程,可应用飞桨提供的主动微分接口 grad() 实现。同时为了便于用户调用,DeepXDE 基于 grad() 接口封装了 Jacobian 和 Hessian 接口,别离反对一阶和二阶微分计算,具体可见 DeepXDE 中 dde.grad.hessian()、dde.grad.jacobian() API 的定义。通过 Jacobian 和 Hessian 接口的自由组合,能够实现任意阶微分计算。
利用以上接口,用户可定义偏微分方程以及边界方程,相应代码示意如下:
def Possion(x,y):
dy_xx = dde.grad.hessian(y,x)
return -dy_xx - np.pi ** 2 * bkd.sin(np.pi * x)
在实现方程定义后,可基于训练网络的输出和输入计算相应的 loss 值,相应代码示意如下:
equation_loss = pde(inputs,outputs_)
boundary_condition_loss = boundary(inputs,outputs_)
loss 计算、反向流传及参数更新
- Loss 计算
在进行一次前向计算后,可依照权重对方程、边界、数据等各类 loss 进行加权计算,从而失去单次训练步下的总 loss,相应代码示意如下:
loss = paddle.sum(equation_loss,boundary_condition_loss)
- 反向流传
反向流传过程会对前向计算过程所波及的算子进行更高一阶的微分运算,利用微分算子,能够从输入层开始,逐层计算每个神经元对误差的奉献,并将误差摊派到每个神经元上,相应代码示意如下
loss.backward()
- 参数更新
在应用优化算法对网络进行优化后,会对网络参数进行更新,并用于下一次前向计算,相应代码示意如下:
paddle.optimizer.Adam.step()
paddle.optimizer.Adam.clear_grad()
Part-2 用例验证
在实现用例实现后,即可依照 2.1 与 2.2 大节中形容的用例验证评估规范发展。
独自框架自测验证
若已知物理机理方程的解析解,则可间接基于飞桨对用例进行“独自框架自测”,并确保用例中网络被训练至收敛,同时保留 loss 数据以及测试集对应的预测解。进一步地,可依照 2.1 节中的规范验证是否满足与解析解的精度要求。若短少解析解,则可依照 2.2 节中的规范来评估 loss 收敛是否满足要求。
同样,若短少物理方程解析解,且独自框架自测无奈满足精度要求时,可查看 DeepXDE 中该用例是否在同类框架上进行了公开测试。若有,则可定义飞桨应用与同类框架雷同的训练网络、网络初始参数以及网络的输出数据,并与该用例的训练后果和网络参数进行逐轮比照。
验证留神点
- 初始化参数固定
失常训练过程中,个别随机初始化训练网络的相干参数。为确保屡次训练具备雷同的初始化参数和样本点数据,须要固定全局随机数,相应代码示意如下:
dde.set_random_seed (100)
- 初始化参数对立
针对曾经在同类框架公开后果的用例,在应用飞桨进行用例验证及评估时,应对立飞桨与其余框架生成的初始化参数,而不能应用 NumPy 生成的数据作为初始化参数(可能导致 loss 极大,影响判断)。如基于 PyTorch 实现的局部 PINN 求解用例中,《附录 - 框架参数转换》提供了 PyTorch 参数转换为飞桨参数的函数,反对用户保留同类框架的网络参数。在飞桨读取初始化参数文件时,倡议从参数文件中依照字符串读取的形式对齐初始化参数,同时利用 vimdiff 确认参数是否设置统一。在模型代码中退出读取网络参数的代码,并运行生成初始化参数,能够保留参数数据至 Init_param.log,具体可见《附录 - 模型参数保留》。随后运行命令 python3.7 example.py Init_param.log,即可读入相应的网络参数。
在飞桨的神经网络中实现初始化参数导入以及将网络参数数值存入文件的代码详见《附录 - 初始化参数读入》。其中,__init__() 函数的接口须要传入解决后的 list w_array,b_array。
- 对立验证策略
针对曾经在同类框架公开后果的用例,在应用飞桨进行用例验证及评估时可比照飞桨与同类框的 loss 后果曲线,并将两个框架的相对误差输入至 diff.log 文件中,相干代码可参考《附录 - 比照脚本》。当验证不通过时,优先更换激活函数,测试不同的网络形成是否存在雷同问题;而后,逐渐比照前向过程和反向过程,查找潜在问题。具体参见“5 典型用例验证”。
03 用例问题排查办法
当用例实现存在问题时,可比照同类框架中用例实现过程(若存在公开的测试后果),进行前向和反向的逐渐比照验证。依照 2.1 节中的流程图,打印所有模块的两头输入,定位 diff 模块。为保障精度以及管制相干变量,验证之前需做如下设置:
设置 1(随机数种子)
因为用例中存在随机生成样本的算法,因而须要固定全局的随机种子,保障每次生成的样本数据统一。
设置 2 (数据类型)
因为 float32 的数据只有 6 - 7 位有效数字是有意义的,因而在做单步比拟时,须要将全局数据类型设置为 float64, 从而能够对失去的后果进行逐位对齐(float64 的有效数字位数为 16-17)。在 DeepXDE 中,可通过退出如下代码来设置:
dde.config.set_default_float('float64')
设置 3(初始化参数)
飞桨中参数初始化器 paddle.nn.initializer.Assign 不反对 float64 类型,需替换为 paddle.nn.initializer.Constant,且初值不倡议取 0、1 等非凡值。相干代码示意如下:
weight_attr_ = paddle.ParamAttr(initializer = paddle.nn.initializer.Constant(0.2345))
self.linears.append(paddle.nn.Linear(layer_sizes[i - 1],layer_sizes[i],weight_attr=weight_attr_,bias_attr=False))
同类框架也须要做相应的设置,如 PyTorch 可参考如下代码:
self.linears.append(torch.nn.Linear(layer_sizes[i - 1],layer_sizes[i],bias=False,dtype=torch.float64)
)
torch.nn.init.constant_(self.linears[-1].weight.data,0.2345)
设置 4(管制正当误差)
PINN 办法中训练网络结构绝对简略,应用层数不多的全连贯网络层,但不同框架外部 kernel 的实现形式不同。例如,PyTorch 和飞桨在 matmul 的高阶微分 kernel 实现上形式不同,因而计算的后果会有失常的误差。为防止相加以及程序不同引起的误差,在调试时,能够设置暗藏层大小为 2,这样 matmul 中计算每个后果的加法只有一次,不存在加和程序的影响。
设置 5(升高验证难度)
在验证过程中可通过一系列办法来升高验证难度,如:缩小网络层数、去掉 linear 层的 bias 设置、缩小边界方程、升高偏导方程的阶数、缩小偏导方程的算子等。留神:因为同类框架的高阶算子和飞桨的高阶算子实现形式不同,相应的反向图也不同,从而导致参数的梯度不同,这些误差无奈防止,其量级个别在 \(10^{-6}\)。这类误差只在个别用例中呈现,具体查验须要打印框架 log,画出反向图,剖析参数梯度的奉献算子。
- 前向打印数据
网络输出数据、初始化参数、网络输入数据、equation_loss、bc_loss、loss 等,可参考《附录 - 比照脚本》对各局部的数据进行比照。在上述设置下,前向过程的所有数据都应该严格逐位统一。当某一步呈现 diff 时,能够定位其上一流程呈现问题,依照设置 5 中的形式逐渐控制变量来查验问题。
- 反向打印数据
参数的梯度和更新后的参数同样可参考以上比照脚本对各局部的数据进行比照,若呈现误差,则须要剖析反向图,查看是否是留神点中的正当误差。
04 典型用例验证后果评估
在应用飞桨进行 DeepXDE 中 PINN 用例的验证及评估时,联合用例已有验证状况,如是否已知解析解、是否具备公开的同类框架验证后果等,可能会呈现多种状况。如下所示,并给出了相应的后果评估阐明。
Part-1 满足解析解精度要求
飞桨训练解满足解析解精度,同时 Loss 曲线也满足验证要求,即飞桨训练最初的后果满足精度要求。此局部用例齐全基于飞桨框架独立验证,代表的用例如 Poisson、Burgers 等。
part-2 未满足解析解精度要求
飞桨在对 DeepXDE 中的个别用例进行验证时,训练解不满足解析解精度要求,若此用例曾经在同类框架上进行了公开的测试,可比照飞桨训练的 loss 曲线与同类框架的 loss 曲线吻合状况,有如下可能:
- loss 曲线吻合
咱们可减少不同框架间的解析解和训练解差距的比照图,当图像吻合或差距小于 \(10^{-3} \) 时,也视为满足要求。
- loss 曲线前期呈现稳定
该状况大概率是因为用例中模型具备周期性特点,从而导致拟合函数不够平滑。但不同框架之间的解析解和训练解差距比照图相近时,也可认为满足验证验证及评估要求。在与同类框架比照验证过程中,若 loss 曲线呈现比拟显著的差别,可尝试更换多组初始化参数进行验证,同时也可能是模型自身对初始化参数敏感导致了 loss 的不稳固。
- Loss 曲线两头呈现局部稳定,导致曲线没有严格吻合
飞桨在局部用例验证过程中,前 200 步满足和同类框架的 loss 相对误差在 \(10^{-6} \) 以内,且最终 loss 收敛值小于 \(10^{-4} \),但 loss 曲线在两头有稳定且不同框架稳定的地位不同。对此咱们可认为该用例网络模型拟合的曲线不够平滑,存在震荡点,且不同框架开始震荡的工夫有差别,导致 loss 曲线无奈严格吻合,这种状况不影响最终收敛。
对于同类问题,loss 曲线稳定较大的状况,倡议从稳定点获取参数作为初始化参数从新训练。
05 总结
本期重点介绍了如何基于飞桨对 DeepXDE 中提供的 PINN 用例进行实现、验证及评估,联合用例具备的条件(如解析解、公开测试后果等)进行飞桨独立框架测试以及比照局部同类框架等。在进行用例验证及评估的同时,咱们对飞桨进行了性能优化,在所验证的用例中,75% 的用例性能超过了同类框架 PyTorch,最高晋升高达 25%,具体可见:https://mp.weixin.qq.com/s/yBsuLWozN4AJCFO5grJ8WA
06 附录
- class NN
class NN(paddle.nn.Layer):
"""Base class for all neural network modules."""
def __init__(self):
super().__init__()
self._input_transform = None
self._output_transform = None
def apply_feature_transform(self,transform):
"""Compute the features by appling a transform to the network inputs,i.e.,
features = transform(inputs). Then,outputs = network(features).
"""
self._input_transform = transform
def apply_output_transform(self,transform):
"""Apply a transform to the network outputs,i.e.,
outputs = transform(inputs,outputs).
"""
self._output_transform = transform
-
class FNN
class FNN(NN): """Fully-connected neural network.""" def __init__(self,layer_sizes,activation,kernel_initializer): super().__init__() self.activation = activations.get(activation) self.layer_size = layer_sizes initializer = initializers.get(kernel_initializer) initializer_zero = initializers.get("zeros") self.linears = paddle.nn.LayerList() for i in range(1,len(layer_sizes)): self.linears.append( paddle.nn.Linear(layer_sizes[i - 1], layer_sizes[i], ) ) initializer(self.linears[-1].weight) initializer_zero(self.linears[-1].bias) def forward(self,inputs): x = inputs if self._input_transform is not None: x = self._input_transform(x) for linear in self.linears[:-1]: x = self.activation(linear(x)) x = self.linears[-1](x) if self._output_transform is not None: x = self._output_transform(inputs,x) return x inputs = paddle.to_tensor(inputs, stop_gradient=False) outputs_ = self.net(inputs) self.net.train()
- 框架参数转换
这里以 PyTorch 为例,linear 层的参数与飞桨的参数互为转置,代码如下:
import numpy as np
import torch
import paddle
def torch2paddle():
torch_path = "./data/mobilenet_v3_small-047dcff4.pth"
paddle_path = "./data/mv3_small_paddle.pdparams"
torch_state_dict = torch.load(torch_path)
fc_names = ["classifier"]
paddle_state_dict = {}
for k in torch_state_dict:
if "num_batches_tracked" in k:
continue
v = torch_state_dict[k].detach().cpu().numpy()
flag = [i in k for i in fc_names]
if any(flag) and "weight" in k: # ignore bias
new_shape = [1,0] + list(range(2,v.ndim))
print(f"name: {k},ori shape: {v.shape},new shape: {v.transpose(new_shape).shape}")
v = v.transpose(new_shape)
k = k.replace("running_var","_variance")
k = k.replace("running_mean","_mean")
# if k not in model_state_dict:
if False:
print(k)
else:
paddle_state_dict[k] = v
paddle.save(paddle_state_dict,paddle_path)
if __name__ == "__main__":
torch2paddle()
-
模型参数保留
layer_size = [2] + [num_dense_nodes] * num_dense_layers + [2] # paddle init param w_array = [] // linear 层的所有 weight 数据 b_array = [] // linear 层的所有 bias 数据 input_str = [] file_name1 = sys.argv[1] with open(file_name1,mode='r') as f1: for line in f1: input_str.append(line) j = 0 for i in range(1,len(layer_size)): shape_weight = (layer_size[i-1],layer_size[i]) w_line = input_str[j] w = [] tmp = w_line.split(',') for num in tmp: w.append(np.float(num)) w = np.array(w).reshape(shape_weight) w_array.append(w) print("w . shape :",w.shape) j = j+1 bias_weight = (layer_size[i]) b_line = input_str[j] b = [] tmp = b_line.split(',') for num in tmp: b.append(np.float(num)) b = np.array(b).reshape(bias_weight) b_array.append(b) print("b . shape :",b.shape) j = j+1
-
初始化参数读入
def __init__(self,layer_sizes,activation,kernel_initializer,w_array=[],b_array=[]): super().__init__() self.activation = activations.get(activation) initializer = initializers.get(kernel_initializer) initializer_zero = initializers.get("zeros") self.linears = paddle.nn.LayerList() for i in range(1,len(layer_sizes)): weight_attr_ = paddle.ParamAttr(initializer = paddle.nn.initializer.Assign(w_array[i-1])) bias_attr_ = paddle.ParamAttr(initializer = paddle.nn.initializer.Assign(b_array[i-1])) self.linears.append(paddle.nn.Linear(layer_sizes[i - 1],layer_sizes[i],weight_attr=weight_attr_,bias_attr=bias_attr_)) # 参数输入为文件 if paddle.in_dynamic_mode(): import os f = open('paddle_dygraph_param.log','ab') for linear in self.linears: np.savetxt(f,linear.weight.numpy().reshape(1,-1),delimiter=",") np.savetxt(f,linear.bias.numpy().reshape(1,-1),delimiter=",") f.close()
- 同类框架对齐代码
这里依然以 PyTorch 为例,其与飞桨对齐的代码如下:
def __init__(self,layer_sizes,activation,kernel_initializer,w_array=[],b_array=[]):
super().__init__()
self.activation = activations.get(activation)
initializer = initializers.get(kernel_initializer)
initializer_zero = initializers.get("zeros")
self.linears = torch.nn.ModuleList()
for i in range(1,len(layer_sizes)):
print("init i :",i,"self.linears :",self.linears)
self.linears.append(
torch.nn.Linear(layer_sizes[i - 1],layer_sizes[i],bias=False,dtype=torch.float64
)
)
self.linears[-1].weight = torch.nn.parameter.Parameter(torch.Tensor(w_array[i-1]).transpose(0,1))
self.linears[-1].bias = torch.nn.parameter.Parameter(torch.Tensor(b_array[i-1]))
import os
f = open('pytorch_param.log','ab')
for linear in self.linears:
# general initilizer :
tmp0 = linear.weight.cpu().detach().numpy()
tmp0 = np.transpose(tmp0)
np.savetxt(f,tmp0.reshape(1,-1),delimiter=",")
np.savetxt(f,tmp1.reshape(1,-1),delimiter=",")
f.close()
- 比照脚本
import sys
import numpy as np
file_name1 = sys.argv[1]
file_name2 = sys.argv[2]
comp_file_name = sys.argv[3]
paddle_data=[]
pytorch_data=[]
with open(file_name1,mode='r') as f1:
for line in f1:
#pytorch_data.append(float(line))
tmp = line.split(',')
for num in tmp:
pytorch_data.append(float(num))
with open(file_name2,mode='r') as f2:
for line in f2:
tmp = line.split(',')
for num in tmp:
paddle_data.append(float(num))
compare_data=[]
for i in range(len(pytorch_data)):
if pytorch_data[i] == 0.0:
tmp = np.inf
else:
tmp = (pytorch_data[i] - paddle_data[i]) / pytorch_data[i]
compare_data.append(tmp)
with open(comp_file_name,mode='w') as f3:
compare_data = np.array(compare_data)
np.savetxt(f3,compare_data)
援用
[1] 飞桨全量反对业内 AI 科学计算工具——DeepXDE!
https://mp.weixin.qq.com/s/yBsuLWozN4AJCFO5grJ8WA
[2] DeepXDE 介绍文档
https://deepxde.readthedocs.io/en/latest/
[3] 飞桨减速 CFD(计算流体力学)原理与实际
拓展浏览
[1] 应用飞桨高阶主动微分性能摸索 AI+ 构造畛域科研
[2] 飞桨科学计算实训示例
https://aistudio.baidu.com/aistudio/projectoverview/public?to…
[3] 飞桨黑客松第四期工作—科学计算专题 https://github.com/PaddlePaddle/Paddle/issues/51281#science
相干地址
[1] 飞桨 AI for Science 共创打算
https://www.paddlepaddle.org.cn/science
[2] 飞桨 PPSIG Science 小组
https://www.paddlepaddle.org.cn/specialgroupdetail?id=9