概要
PINN(Physics-Informed Neural Network)について学習したことをまとめます。
すでに様々なところで先人が取り組まれており,以下のサイトを参考にさせていただきました。
https://www.ntt-at.co.jp/product/aicolumnlist/202303.html https://masao-ds.hatenablog.com/entry/2023/07/09/172343 https://github.com/erfanhamdi/pinn-torch
PINNとは
通常のNN(Neural Network)では,大量の教師データを用意して,モデル出力が教師データと一致するように重みを最適化していきます。
ただし,この学習方法は教師データが存在する入力変数の範囲で誤差を最小化しているだけで,データが存在しない範囲ではどれだけモデルが極端な挙動をしたとしても制御することは難しいです。そこで,教師データへの当てはまりだけでなく,データの生成ロジック(支配方程式)に対する当てはまりも評価するように学習方法を改良したものがPINNです。
PINNの学習には以下の2つのデータが必要になります。
- 初期条件・境界条件を定義するデータ
- 支配方程式とモデル出力との一致を評価するデータ
1.は(時間, 空間座標, 物理量)
といった形式になり,記号(t, x, u)
で表します。このデータは通常のNNの仕組みと同様,モデル出力との誤差をとるための教師データとして扱います。
2.がPINNならではのデータで,(時間, 空間座標)
の形式になっており,物理量u
が含まれていません。物理量をあらかじめ教師データとして与えるのではなく,学習途中でモデルが吐き出す物理量u
をもとに支配方程式の残差を計算します。つまり,教師データとの一致を見つつも,支配方程式にも適合するような物理量を出力するように学習が行われます。
コード
パッケージのインポート
必要パッケージをインストールします。モデルの中身まで細かく定義する必要があるので,今回はpytorchを用います。
import random
import numpy as np
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
def torch_fix_seed(seed=42):
""" 乱数シードを固定
"""
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.backends.cudnn.deterministic = True
torch.use_deterministic_algorithms = True
torch_fix_seed()
データセットの作成
境界条件(初期条件,固定壁条件)に沿ったデータをそれぞれ100件ずつ作成します。
時間t
と空間座標x
は乱数で生成します。物理量u
は初期条件の場合はsin関数,固定壁条件の場合は0を埋め込んでいます。
支配方程式の残差の評価には,乱数で生成した5000点を用います。
# 初期条件: x∈[-1,1], t=0のとき,u=-sin(x)
n_initial = 100
t_initial = np.zeros(n_initial)
x_initial = (np.random.random(n_initial) - 0.5) * 2
u_initial = -1 * np.sin(np.pi * x_initial)
# 固定壁条件: x=-1 or x=1, t∈[0,1]のとき,u=0
n_boundary = 100
t_boundary = np.random.random(n_boundary)
x_boundary = np.random.choice([-1, 1], n_boundary)
u_boundary = np.zeros(n_boundary)
# 支配方程式の残差を評価する計算領域内の座標: x∈[-1,1], t∈[0,1]
n_region = 5000
t_region = np.random.random(n_region)
x_region = (np.random.random(n_region)- 0.5) * 2
# 可視化
plt.figure(figsize=(10, 2))
plt.scatter(t_initial, x_initial, c=u_initial)
plt.scatter(t_boundary, x_boundary, c=u_boundary)
plt.colorbar()
plt.scatter(t_region, x_region, c="gray", s=5, marker="x")
plt.show()
plt.figure(figsize=(3, 2))
plt.scatter(x_initial, u_initial)
plt.show()
モデルの作成
モデルを定義します。NN
クラスは一般的なニューラルネットワークです。これをBurgersPINN
クラスのイニシャライザに渡すことで,Physics-Informedな学習ができるようになります。
class NN(nn.Module):
def __init__(self, n_input, n_output, n_hiddens=[32,64,128,128,64,32]):
super(NN, self).__init__()
self.activation = nn.Tanh()
self.input_layer = nn.Linear(n_input, n_hiddens[0])
self.hidden_layers = nn.ModuleList(
[nn.Linear(n_hiddens[i], n_hiddens[i+1]) for i in range(len(n_hiddens)-1)]
)
self.output_layer = nn.Linear(n_hiddens[-1], n_output)
def forward(self, x):
x = self.activation(self.input_layer(x))
for layer in self.hidden_layers:
x = self.activation(layer(x))
x = self.output_layer(x)
return x
class BurgersPINN():
def __init__(self, model):
"""
"""
self.model = model
self.nu = 0.1
def net_u(self, x, t):
""" 物理量を出力
"""
u = self.model(torch.cat([x, t], dim=1))
return u
def net_f(self, x, t):
""" 支配方程式との残差を出力
"""
# モデルが予測する物理量
u = self.net_u(x, t)
# 微分係数を逆伝搬で計算
du_dt = torch.autograd.grad(u, t, grad_outputs=torch.ones_like(u), retain_graph=True, create_graph=True)[0]
du_dx = torch.autograd.grad(u, x, grad_outputs=torch.ones_like(u), retain_graph=True, create_graph=True)[0]
du_dxx = torch.autograd.grad(du_dx, x, grad_outputs=torch.ones_like(du_dx), retain_graph=True, create_graph=True)[0]
# 支配方程式に代入(f=0だと方程式と完全一致)
f = du_dt + u * du_dx - self.nu * du_dxx
return f
def fit(self, X_bc, Y_bc, X_region, max_epochs=300, learning_rate=0.01, pi_weight=5e-4):
""" 学習データでモデルを訓練
"""
# 入力データをスライス
t = X_bc[:, [0]]
x = X_bc[:, [1]]
u = Y_bc
t_region = X_region[:, [0]]
x_region = X_region[:, [1]]
# 入力をtorch.tensorに変換
t = torch.tensor(t, requires_grad=True).float()
x = torch.tensor(x, requires_grad=True).float()
u = torch.tensor(u, requires_grad=True).float()
x_region = torch.tensor(x_region, requires_grad=True).float()
t_region = torch.tensor(t_region, requires_grad=True).float()
# 最適化ロジック
optimizer = torch.optim.Adam(self.model.parameters(), lr=learning_rate)
# モデルを学習モードに変更
self.model.train()
# 学習
history = []
for epoch in range(max_epochs+1):
# 現在のモデルで予測
u_pred = self.net_u(x, t)
f_pred = self.net_f(x_region, t_region)
# 損失を計算
loss_u = torch.mean((u - u_pred)**2)
loss_pi = torch.mean(f_pred**2)
loss_total = loss_u + loss_pi * pi_weight
# 誤差逆伝搬
loss_total.backward()
# 最適化ソルバーで重みを動かす
optimizer.step()
# 最適化ソルバーの勾配情報を初期化
optimizer.zero_grad()
if epoch % 100 == 0:
print(f'epoch:{epoch}, loss:{loss_total.item()}, loss_u:{loss_u.item()}, loss_pi:{loss_pi.item()}')
history.append([epoch, loss_total.item(), loss_u.item(), loss_pi.item()])
return np.array(history)
def predict(self, X_in):
""" モデルの予測
"""
t = X_in[:, [0]]
x = X_in[:, [1]]
# 入力をtorch.tensorに変換
t = torch.tensor(t, requires_grad=True).float()
x = torch.tensor(x, requires_grad=True).float()
self.model.eval() # 評価モードに変更
u = self.net_u(x, t).detach().numpy()
f = self.net_f(x, t).detach().numpy()
return u, f
特に重要なのはBurgersPINN
内のfit()
関数の以下の部分です。通常のNNでは教師データのu
とモデル出力u_pred
から損失関数を計算しますが,ここではloss_pi
という項が追加されています。
これが,モデル出力から計算した支配方程式の残差となります。
# 現在のモデルで予測
u_pred = self.net_u(x, t)
f_pred = self.net_f(x_region, t_region)
# 損失を計算
loss_u = torch.mean((u - u_pred)**2)
loss_pi = torch.mean(f_pred**2)
loss_total = loss_u + loss_pi * pi_weight
支配方程式の残差はnet_f
関数によって計算されます。ここが面白いポイントで,pytorchの自動微分機能(auto_grad
)を使うことで,x
,t
,u
等の変数同士の微分が簡単に計算できます。
通常このような非線形式は厳密解がないものも多く,解くためにかなり緻密な変換を要する(今回挙げているバーガース方程式も「コール・ホップ変換」というものが必要となる)のですが,NN内で物理量を処理することでこのような恩恵が得られます。
# 微分係数を逆伝搬で計算
du_dt = torch.autograd.grad(u, t, grad_outputs=torch.ones_like(u), retain_graph=True, create_graph=True)[0]
du_dx = torch.autograd.grad(u, x, grad_outputs=torch.ones_like(u), retain_graph=True, create_graph=True)[0]
du_dxx = torch.autograd.grad(du_dx, x, grad_outputs=torch.ones_like(du_dx), retain_graph=True, create_graph=True)[0]
# 支配方程式に代入(f=0だと方程式と完全一致)
f = du_dt + u * du_dx - self.nu * du_dxx
学習
1000エポック分の学習を行います。
# 学習用のデータ
X_bc = np.block([[t_initial, t_boundary], [x_initial, x_boundary]]).T
Y_bc = np.block([[u_initial, u_boundary]]).T
X_region = np.block([[t_region], [x_region]]).T
print("shape:", X_bc.shape, Y_bc.shape, X_region.shape)
model = NN(2, 1)
pinn_model = BurgersPINN(model)
# モデルを学習
history = pinn_model.fit(X_bc, Y_bc, X_region, max_epochs=1000)
# 履歴
plt.figure(figsize=(10,2))
plt.plot(history[:, 0], history[:, 1], label='loss_total')
plt.plot(history[:, 0], history[:, 2], label='loss_u')
plt.plot(history[:, 0], history[:, 3], label='loss_pi')
plt.legend()
loss_u
に比べてloss_pi
のオーダーが大きすぎるかもしれません。ここはパラメータの調整次第かなと思います。
(ミニバッチ学習になっていないため,データセットによっては局所解にはまってしまうようです...)
予測
物理量u
と支配方程式の残差f
を可視化します。
# 予測したいポイント
n = 100
t_pred = np.linspace(0, 1, n)
x_pred = np.linspace(-1, 1, n)
t_grid, x_grid = np.meshgrid(t_pred, x_pred)
X_in = np.block([[t_grid.flatten()], [x_grid.flatten()]]).T
# 予測
u_pred, f_pred = pinn_model.predict(X_in)
u_pred = u_pred.reshape(n, n)
f_pred = f_pred.reshape(n, n)
# 物理量を可視化
plt.figure(figsize=(10, 2))
plt.contourf(t_pred, x_pred, u_pred, 32)
plt.colorbar()
plt.show()
# 基礎方程式の残差を可視化
plt.figure(figsize=(10, 2))
plt.contourf(t_pred, x_pred, f_pred, 32)
plt.colorbar()
plt.show()
期待通り,全領域で残差f
が0に近づき,物理量u
の分布も支配方程式に沿ったものになっています。
times = [0, 0.25, 0.5, 0.75]
fig = plt.figure(figsize=(15, 2))
for i, t in enumerate(times):
t_pred = np.ones([100, 1]) * t
X_in = np.column_stack([t_pred, x_pred])
u_pred, f_pred = pinn_model.predict(X_in)
fig.add_subplot(1, 4, i+1)
plt.plot(x_pred, u_pred, label='u_pred')
plt.plot(x_pred, f_pred, label='f_pred')
plt.ylim(-1.5, 1.5)
plt.legend()
plt.show()
時間t
を0, 0.25, 0.5, 0.75に固定して,物理量u
と残差f
をプロットしました。
まとめ
PINNの基本としてバーガース方程式の検討を行いました。
次は,より高度な支配方程式に基づいたモデルにチャレンジしたいと思います。