调研了PINN的简单使用

PDE的求解

PDE有多种求解方式,包括有限差分法、有限元法在内的网格法以及Kansa法、移动最小二乘法在内的无网格法。

网格法主要使用网格对偏微分方程进行离散化,在网格上使用数值微分求解。

无网格法主要是使用近似函数对原函数进行近似,然后使用PDE的条件对原函数进行拟合的方法。

在传统方法中,无论是网格法还是无网格法,均使用数值微分对PDE进行求解,但是数值微分本身存在误差,而且在无网格法中,由于我们会预设基函数来对原函数进行拟合,这也存在误差,因此在传统方法中,网格法相较于无网格法更容易实现的同时还具有更低的误差,因此目前常见的PDE求解器使用的方法均为网格法。

但是如果去除了数值微分的误差影响,在近似函数取得较为复杂的情况下,无网格法理论上较于网格法会具有更高的精确度,而PINN就是采用精确微分求解PDE的一种无网格法

自动微分

自动微分基于链式法则,将复杂函数分解为基本运算的组合,然后逐步计算每个基本运算的导数并累积。它通过跟踪计算图中的数值流动来实现导数计算。

图源:Automatic differentiation in machine learning: a survey

由此可见,自动微分显然是一种精确微分。自动微分需要一种函数,这种函数最好是由大量的$\omega x + b$复合而成,这样我们就可以通过链式法则对复合函数进行求导,从而得到精确的导数。

显然,自动微分天生契合神经网络的结构,因此自动微分广泛运用在神经网络的反向传播之中。

PINN

内嵌物理信息神经网络显然具有两部分,一部分是PI,也就是物理信息,另一部分则是NN,也就是神经网络。

考虑到无网格法的核心是找到近似函数,在PINN中,我们使用神经网络作为近似函数(因而考虑到网络的复杂性,这个近似函数往往只有网络的形式),这就是NN,使用边界条件、初始条件作为损失函数,这就是PI,训练后的网络,即我们的PDE的解。

我们以一个简单的例子来说明PINN的原理。

\[\left.\left\{\begin{array}{l}u_t+u\times u_x-w\times u_{xx}=0\\u(0,x)=-sin(\pi x)\\u(t,\quad1)=0\\u(t,-1)=0\\w=\frac{0.01}\pi,x\in(-1,1),t\in(0,1)\end{array}\right.\right.\]

在PINN中,我们往往使用MLP作为神经网络,我们使用torch构造一个网络结构

class MLP(nn.Module):
    def __init__(self, NN): 
        super(MLP, self).__init__()
        self.input_layer = nn.Linear(2, NN)
        self.hidden_layer1 = nn.Linear(NN,int(NN/2))
        self.hidden_layer2 = nn.Linear(int(NN/2), int(NN/2))  
        self.output_layer = nn.Linear(int(NN/2), 1)

    def forward(self, x): 
        out = torch.tanh(self.input_layer(x))
        out = torch.tanh(self.hidden_layer1(out))
        out = torch.tanh(self.hidden_layer2(out))
        out_final = self.output_layer(out)
        return out_final

然后我们写一下方程,作为约束条件:

def pde(x, net):
    u = net(x)
    u_tx = torch.autograd.grad(u, x, grad_outputs=torch.ones_like(net(x)),
                               create_graph=True, allow_unused=True)[0]  
    d_t = u_tx[:, 0].unsqueeze(-1)
    d_x = u_tx[:, 1].unsqueeze(-1)
    u_xx = torch.autograd.grad(d_x, x, grad_outputs=torch.ones_like(d_x),
                               create_graph=True, allow_unused=True)[0][:,1].unsqueeze(-1) 
    w = torch.tensor(0.01 / np.pi)
    return d_t + u * d_x - w * u_xx  

定义一下我们的损失函数形式和优化器:

mse_cost_function = torch.nn.MSELoss(reduction='mean')  # Mean squared error
optimizer = Lion(net.parameters(), lr=1e-4)

特别的,我们使用Lion作为优化器,其具有非常好的优化效果,能显著降低迭代步数。

然后写一下我们的训练函数:

t_bc_zeros = np.zeros((2000, 1))
x_in_pos_one = np.ones((2000, 1))
x_in_neg_one = -np.ones((2000, 1))
u_in_zeros = np.zeros((2000, 1))

iterations = 3000
for epoch in range(iterations):
    optimizer.zero_grad()  

    t_in_var = np.random.uniform(low=0, high=1.0, size=(2000, 1))
    x_bc_var = np.random.uniform(low=-1.0, high=1.0, size=(2000, 1))
    u_bc_sin = -np.sin(np.pi * x_bc_var)

    pt_x_bc_var = Variable(torch.from_numpy(x_bc_var).float(), requires_grad=False)
    pt_t_bc_zeros = Variable(torch.from_numpy(t_bc_zeros).float(), requires_grad=False)
    pt_u_bc_sin = Variable(torch.from_numpy(u_bc_sin).float(), requires_grad=False)
    pt_x_in_pos_one = Variable(torch.from_numpy(x_in_pos_one).float(), requires_grad=False)
    pt_x_in_neg_one = Variable(torch.from_numpy(x_in_neg_one).float(), requires_grad=False)
    pt_t_in_var = Variable(torch.from_numpy(t_in_var).float(), requires_grad=False)
    pt_u_in_zeros = Variable(torch.from_numpy(u_in_zeros).float(), requires_grad=False)

    # 求边界条件的损失
    net_bc_out = net(torch.cat([pt_t_bc_zeros, pt_x_bc_var], 1))  # u(x,t)的输出
    mse_u_2 = mse_cost_function(net_bc_out, pt_u_bc_sin)  # e = u(x,t)-(-sin(pi*x))

    net_bc_inr = net(torch.cat([pt_t_in_var, pt_x_in_pos_one], 1))  # 0=u(t,1) 
    net_bc_inl = net(torch.cat([pt_t_in_var, pt_x_in_neg_one], 1))  # 0=u(t,-1) 

    mse_u_3 = mse_cost_function(net_bc_inr, pt_u_in_zeros)  # e = 0-u(t,1)
    mse_u_4 = mse_cost_function(net_bc_inl, pt_u_in_zeros)  # e = 0-u(t,-1)

    x_collocation = np.random.uniform(low=-1.0, high=1.0, size=(2000, 1))
    t_collocation = np.random.uniform(low=0.0, high=1.0, size=(2000, 1))
    all_zeros = np.zeros((2000, 1))
    pt_x_collocation = Variable(torch.from_numpy(x_collocation).float(), requires_grad=True)
    pt_t_collocation = Variable(torch.from_numpy(t_collocation).float(), requires_grad=True)
    pt_all_zeros = Variable(torch.from_numpy(all_zeros).float(), requires_grad=False)

    f_out = pde(torch.cat([pt_t_collocation, pt_x_collocation], 1), net)  # f(x,t)的输出
    mse_f_1 = mse_cost_function(f_out, pt_all_zeros)

    loss = mse_f_1 + mse_u_2 + mse_u_3 + mse_u_4

    loss.backward()  
    optimizer.step() 

    with torch.autograd.no_grad():
        if epoch % 100 == 0:
            print(epoch, "Traning Loss:", loss.data)

求解过程如下:

0 Traning Loss: tensor(0.5126)
100 Traning Loss: tensor(0.4608)
200 Traning Loss: tensor(0.4518)
300 Traning Loss: tensor(0.4403)
400 Traning Loss: tensor(0.4373)
500 Traning Loss: tensor(0.4268)
600 Traning Loss: tensor(0.4031)
700 Traning Loss: tensor(0.3871)
800 Traning Loss: tensor(0.3683)
900 Traning Loss: tensor(0.3462)
1000 Traning Loss: tensor(0.3033)
1100 Traning Loss: tensor(0.2588)
1200 Traning Loss: tensor(0.2162)
1300 Traning Loss: tensor(0.1814)
1400 Traning Loss: tensor(0.1661)
1500 Traning Loss: tensor(0.1583)
1600 Traning Loss: tensor(0.1538)
1700 Traning Loss: tensor(0.1535)
1800 Traning Loss: tensor(0.1468)
1900 Traning Loss: tensor(0.1399)
2000 Traning Loss: tensor(0.1330)
2100 Traning Loss: tensor(0.1306)
2200 Traning Loss: tensor(0.1255)
2300 Traning Loss: tensor(0.1195)
2400 Traning Loss: tensor(0.1111)
2500 Traning Loss: tensor(0.1106)
2600 Traning Loss: tensor(0.1091)
2700 Traning Loss: tensor(0.1085)
2800 Traning Loss: tensor(0.1059)
2900 Traning Loss: tensor(0.1042)

最后我们画一下结果: