PINN的一个小Demo代码释义

以一个最简单的常微分方程为例:

\[\begin{aligned} f'(x) = f(x)\hspace{2.45cm}(1)\\ f(0) = 1 \hspace{2.45cm}(2) \end{aligned}\]

这个微分方程的解其实就是:

\[f(x)=e^{x}\hspace{2.45cm}(3)\]

这里我们假装已经知道结果是 f(x)=e^x 了,所以对于给定的x,我们可以给出理想的输出 y = e^x 了,通过神经网络可以通过输入x可以算出一个 y。神经网络算出的 y 和 理想的 y 差别多大时,才说算得不错了? 这里的评判标准就是Loss函数。 我们把这里的Loss函数分解成两部分

思路就是这么短。

导包:

import torch  
import torch.nn as nn  
import numpy as np  
import matplotlib.pyplot as plt  
# 自动求导模块,用于计算梯度。
from torch import autograd  

神经网络类:

这里Net类继承于 torch.nn.Module类,在构造函数里面可以构造输入层、隐藏层和输出层,每一层都是线性变换,直接使用nn.Linear进行构造。同时实现一个forward方法,含义是前向传播过程,就是从前往后加上一个激活函数,然后从前往后传过去,上一层的输出当作下一层的输入;同时这个forward方法,后面可以直接使用实例名加上括号的方式调用。

注意python里面继承、构造函数的写法。

# 神经网络的构造,继承了 nn.Module类
class Net(nn.Module):  
    # 构造函数  
    def __init__(self, NL, NN): 
        # NL是有多少层隐藏层  
        # NN是每层的神经元数量,是输入数据的维数
        super(Net, self).__init__()  
  
        # 每个隐藏层都通过线性变换和激活函数来处理输入,从而逐步构建了一个复杂的非线性映射。  
        self.input_layer = nn.Linear(1, NN)  
        self.hidden_layer = nn.Linear(NN, int(NN / 2))  ## 原文这里用NN,我这里用的下采样,经过实验验证,“等采样”更优。更多情况有待我实验验证。  
        self.output_layer = nn.Linear(int(NN / 2), 1)  
  
    # 方法,实现网络的前向传播过程,激活函数是tanh函数  
    def forward(self, x):  
        # input_layer首先进行线性变换  
        # 然后结果作为参数传给tanh激活函数做非线性变换,也就映射到一个介于 -1 到 1 之间的值域  
        out = torch.tanh(self.input_layer(x))  
        out = torch.tanh(self.hidden_layer(out))  
        out_final = self.output_layer(out)  
        return out_final

抽出的残差计算方法:

计算残差:f(x) - f’(x) 如果找到了一个函数 f(x),使得残差为零,即 f(x) - f’(x) = 0,那么这个函数就是微分方程的解。

# 残差:f(x) - f'(x)。  
# 如果找到了一个函数 f(x),使得残差为零,即 f(x) - f'(x) = 0,那么这个函数就是微分方程的解。  
def ode_01(x, net):  
    # 根据输入x, 通过神经网络预测y  
    # 实际上也是调用了 forward方法  
    y = net(x)  
    # 使用自动微分autograd.grad计算了 y关于x的梯度y_x (也是导数)  
    # 刚刚 x 设置了 requires_grad=True, 所以现在可以使用这句来  
    # grad_outputs 参数设置为 torch.ones_like(net(x)),即梯度的方向,  
    # create_graph=True 表示创建一个计算图以支持高阶导数计算。  
    y_x = autograd.grad(y, x, grad_outputs=torch.ones_like(net(x)), create_graph=True)[0]  
    return y - y_x

具体流程

接下来的步骤,是面向过程的:

  1. 首先创建神经网络实例、误差函数实例、优化器实例:
    # 创建一个神经网络实例  
    net = Net(4, 20)  # 4层隐藏层 每个隐藏层有20个神经元  
    # 创建一个均方误差损失函数实例  
    # 调用 MSELoss类的构造函数,设置了 reduction参数  
    mse_cost_function = torch.nn.MSELoss(reduction='mean')  # Mean squared error 均方误差  
    # 创建一个Adam优化器示例,用于优化参数,net.parameters()返回模型中需要优化的参数,lr表示学习率  
    optimizer = torch.optim.Adam(net.parameters(), lr=1e-4)  # 优化器
    
  2. 再开始画图,这里使用matplotlib画动态图。首先规定了迭代长度是200000次,这个for循环会执行这么多次。
    plt.ion()  # 动态图  
    iterations = 200000  
    for epoch in range(iterations):  
     # 具体迭代内容
    
  3. 在一个迭代里面(后面所有内容都在此for循环里面): 每次迭代前需要把梯度归0,应为优化器把每个参数的grad梯度属性都更改了,重新迭代前需要归0。
    # 当进行神经网络的反向传播时,梯度值会根据损失函数的变化情况计算并累积在每个参数上。  
    optimizer.zero_grad()  # 优化器的梯度归0, 避免上一轮迭代中的梯度影响当前迭代的参数更新。  
    
  4. 下面分两部分计算损失函数,最后相加得到总的损失函数。
# 边界条件的损失函数, 边界条件f(0) = 1  
# 创建一个包含2000个0的张量x_0作为输入  
x_0 = torch.zeros(2000, 1)  
# 通过网络预测得到y_0  
y_0 = net(x_0)  
# 然后计算 预测值 与 全为1的张量 的MSE损失。  
# 传入模型的预测值和目标值,参数形状需要一样,否则无法计算均方误差  
# 调用了 forward 方法,实际上是一种语法糖,方便调用  
mse_i = mse_cost_function(y_0, torch.ones(2000, 1))  # f(0) - 1 = 0  

# 微分方程的损失函数  
# 随机生成2000个位于0到2之间的输入x_in  
x_in = np.random.uniform(low=0.0, high=2.0, size=(2000, 1))  
# 将其转换为PyTorch张量,并启用梯度追踪。  
# 启用梯度追踪是因为后续的操作需要计算神经网络的 输出值 y 关于输入 x_in 的梯度(导数)  
# 后面就可以使用 autograd.grad 函数来计算这个梯度。  
pt_x_in = autograd.Variable(torch.from_numpy(x_in).float(), requires_grad=True)  # x 随机数  
# 通过ode_01函数计算输入为pt_x_in时, 微分方程算出来的y和 y关于x的梯度y_x 的差值(残差)  
pt_y_collection = ode_01(pt_x_in, net)  
# 并创建一个全为0的张量pt_all_zeros, 代表期望的微分方程差值为零。  
# 理想状态下,每个数据点的残差都应该是零。因为这个张量的值在模型的训练过程中不会发生变化,它也不参与梯度计算和反向传播  
pt_all_zeros = autograd.Variable(torch.from_numpy(np.zeros((2000, 1))).float(), requires_grad=False)  
# 计算微分方程损失,即 预测差值pt_y_collection 与 全为0的差值 之间的MSE损失。  
mse_f = mse_cost_function(pt_y_collection, pt_all_zeros)

# 总损失函数,包括边界条件损失和微分方程损失  
# 衡量损失,一部分是微分方程损失,用于衡量网络在随机选择的输入 x_in 处满足微分方程的程度  
# 另一部分是边界条件损失,  
loss = mse_i + mse_f  
  1. 接着利用Loss函数开始反向扩散,交给优化器。
# 然后执行反向传播计算梯度,并使用优化器更新模型参数。  
# pytorch自动调用反向传播算法来计算参数梯度  
loss.backward()  # 反向传播  
# 优化器: 根据梯度和选定的优化算法,更新模型的参数,从而优化模型在训练数据上的拟合效果。  
optimizer.step()  # 优化下一步。This is equivalent to : theta_new = theta_old - alpha * derivative of J w.r.t theta  

当执行了 loss.backward() 这句话之后,以下几个主要步骤会发生:

  1. 梯度计算: PyTorch 会从损失函数 loss 开始,根据计算图中的连接关系,自动计算损失函数对每个模型参数的梯度。这个过程使用了链式法则,将梯度从损失函数一直传递到模型的每个参数。
  2. 梯度累积: 计算得到的梯度值会被累积在每个参数的 .grad 属性中。这些梯度值表示了在当前参数值下,损失函数会如何变化。
  3. 反向传播路径: 在计算梯度的过程中,PyTorch会根据计算图的反向传播路径,将梯度从输出层向输入层传递。每一层的梯度计算都依赖于上一层的梯度,直到最终传递到输入层。
  4. 参数更新: 一旦梯度计算完成,就可以使用这些梯度来更新模型的参数。通常使用优化器(如 Adam、SGD 等)来根据梯度值更新每个参数的值,使得损失函数逐渐减小。 总之,在执行 loss.backward() 后,PyTorch 会自动完成反向传播计算梯度的过程。这些梯度可以用来更新神经网络的参数,使其朝着减小损失的方向进行调整,从而提升模型的性能。这是神经网络训练的核心步骤之一。

  5. 每1000次迭代,输出当前训练损失,并绘制真实值y和预测值y_train0的散点图,散点图直接输出到独立窗口,下一个1000次迭代进行刷新。
# 每1000次迭代,输出当前训练损失,并绘制真实值y和预测值y_train0的散点图。  
if epoch % 1000 == 0:  
	y = torch.exp(pt_x_in)  # y 真实值  
	y_train0 = net(pt_x_in)  # y 预测值  
	print(epoch, "Training Loss:", loss.data)  
	print(f'times {epoch}  -  loss: {loss.item()} - y_0: {y_0}')  
	# 在每次迭代中清除之前绘制的图像,以便绘制新的图像。  
	# 这样做可以让动态显示训练过程中,每次迭代都只显示当前迭代的图像,避免将之前的图像堆叠在一起。  
	plt.cla()  
	# pt_x_in.detach().numpy() 将输入转换为NumPy数组以便绘图  
	# detach 确保在绘制图像时不会在计算图上保留这些张量的依赖关系,从而提高绘图的效率。 
	# s指定线条粗细 
	plt.scatter(pt_x_in.detach().numpy(), y.detach().numpy(), s=1)  
	plt.scatter(pt_x_in.detach().numpy(), y_train0.detach().numpy(), c='red', s=1)  
	# plt.pause(0.1) 用于在每次迭代后暂停,以便动态查看图形变化。  
	plt.pause(0.1)

#end of for

参考资料:

PINN学习与实验(一)__刘文凯_的博客-CSDN博客

ChatGPT