راهنمای گام به گام حل معادله برگر ۱ بعدی با شبکه‌های عصبی فیزیک-آگاه (PINNs): رویکردی با پایتورچ

در این آموزش، رویکردی نوآورانه را بررسی می‌کنیم که یادگیری عمیق را با قوانین فیزیکی ترکیب می‌کند. این کار با بهره‌گیری از شبکه‌های عصبی فیزیک-آگاه (PINNs) برای حل معادله یک‌بعدی برگر انجام می‌شود. با استفاده از PyTorch در Google Colab، نشان می‌دهیم چگونه معادله دیفرانسیل حاکم را مستقیماً در تابع هزینه شبکه عصبی کدگذاری کنیم. این امر به مدل اجازه می‌دهد تا راه‌حل ??(??,??) را یاد بگیرد که ذاتاً به فیزیک زیربنایی پایبند است. این تکنیک وابستگی به مجموعه داده‌های بزرگ برچسب‌دار را کاهش می‌دهد و دیدگاهی تازه برای حل معادلات دیفرانسیل جزئی پیچیده و غیرخطی با استفاده از ابزارهای محاسباتی مدرن ارائه می‌دهد.

!pip install torch matplotlib

ابتدا، کتابخانه‌های PyTorch و matplotlib را با استفاده از pip نصب می‌کنیم تا اطمینان حاصل شود که ابزارهای لازم برای ساخت شبکه‌های عصبی و تجسم نتایج در محیط Google Colab شما در دسترس هستند.

import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt

torch.set_default_dtype(torch.float32)

کتابخانه‌های ضروری را وارد می‌کنیم: PyTorch برای یادگیری عمیق، NumPy برای عملیات عددی و matplotlib برای رسم نمودار. نوع داده پیش‌فرض تانسور را روی float32 تنظیم می‌کنیم تا دقت عددی در طول محاسبات شما سازگار باشد.

x_min, x_max = -1.0, 1.0
t_min, t_max = 0.0, 1.0
nu = 0.01 / np.pi

N_f = 10000  
N_0 = 200    
N_b = 200    

X_f = np.random.rand(N_f, 2)
X_f[:, 0] = X_f[:, 0] * (x_max - x_min) + x_min  # x in [-1, 1]
X_f[:, 1] = X_f[:, 1] * (t_max - t_min) + t_min    # t in [0, 1]

x0 = np.linspace(x_min, x_max, N_0)[:, None]
t0 = np.zeros_like(x0)
u0 = -np.sin(np.pi * x0)

tb = np.linspace(t_min, t_max, N_b)[:, None]
xb_left = np.ones_like(tb) * x_min
xb_right = np.ones_like(tb) * x_max
ub_left = np.zeros_like(tb)
ub_right = np.zeros_like(tb)

X_f = torch.tensor(X_f, dtype=torch.float32, requires_grad=True)
x0 = torch.tensor(x0, dtype=torch.float32)
t0 = torch.tensor(t0, dtype=torch.float32)
u0 = torch.tensor(u0, dtype=torch.float32)
tb = torch.tensor(tb, dtype=torch.float32)
xb_left = torch.tensor(xb_left, dtype=torch.float32)
xb_right = torch.tensor(xb_right, dtype=torch.float32)
ub_left = torch.tensor(ub_left, dtype=torch.float32)
ub_right = torch.tensor(ub_right, dtype=torch.float32)

دامنه شبیه‌سازی را برای معادله برگر با تعریف مرزهای فضایی و زمانی، ویسکوزیته (لزجت) و تعداد نقاط کولوکیشن، اولیه و مرزی تعیین می‌کنیم. سپس، نقاط داده تصادفی و با فاصله یکنواخت را برای این شرایط تولید کرده و آن‌ها را به تانسورهای PyTorch تبدیل می‌کنیم تا محاسبه گرادیان در صورت نیاز امکان‌پذیر باشد.

class PINN(nn.Module):
    def __init__(self, layers):
        super(PINN, self).__init__()
        self.activation = nn.Tanh()
       
        layer_list = []
        for i in range(len(layers) - 1):
            layer_list.append(nn.Linear(layers[i], layers[i+1]))
        self.layers = nn.ModuleList(layer_list)
       
    def forward(self, x):
        for i, layer in enumerate(self.layers[:-1]):
            x = self.activation(layer(x))
        return self.layers[-1](x)

layers = [2, 50, 50, 50, 50, 1]
model = PINN(layers)
print(model)

در اینجا، یک شبکه عصبی فیزیک-آگاه (PINN) سفارشی را با گسترش کلاس `nn.Module` پایتورچ تعریف می‌کنیم. معماری شبکه به صورت پویا با استفاده از لیستی از اندازه‌های لایه ساخته می‌شود، که در آن هر لایه خطی با یک تابع فعال‌سازی Tanh دنبال می‌شود (به جز لایه خروجی نهایی). در این مثال، شبکه یک ورودی ۲ بعدی دریافت می‌کند، آن را از چهار لایه پنهان (هر کدام با ۵۰ نورون) عبور می‌دهد و یک مقدار واحد را خروجی می‌دهد. در نهایت، مدل با معماری مشخص شده نمونه‌سازی می‌شود و ساختار آن چاپ می‌گردد.

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)

در این بخش، بررسی می‌کنیم که آیا یک GPU با قابلیت CUDA در دسترس است یا خیر، دستگاه را بر این اساس تنظیم کرده و مدل را برای محاسبات سریع‌تر در طول آموزش و استنتاج به آن دستگاه منتقل می‌کنیم.

def pde_residual(model, X):
    x = X[:, 0:1]
    t = X[:, 1:2]
    u = model(torch.cat([x, t], dim=1))
   
    u_x = torch.autograd.grad(u, x, grad_outputs=torch.ones_like(u), create_graph=True, retain_graph=True)[0]
    u_t = torch.autograd.grad(u, t, grad_outputs=torch.ones_like(u), create_graph=True, retain_graph=True)[0]
    u_xx = torch.autograd.grad(u_x, x, grad_outputs=torch.ones_like(u_x), create_graph=True, retain_graph=True)[0]
   
    f = u_t + u * u_x - nu * u_xx
    return f

def loss_func(model):
    f_pred = pde_residual(model, X_f.to(device))
    loss_f = torch.mean(f_pred**2)
   
    u0_pred = model(torch.cat([x0.to(device), t0.to(device)], dim=1))
    loss_0 = torch.mean((u0_pred - u0.to(device))**2)
   
    u_left_pred = model(torch.cat([xb_left.to(device), tb.to(device)], dim=1))
    u_right_pred = model(torch.cat([xb_right.to(device), tb.to(device)], dim=1))
    loss_b = torch.mean(u_left_pred**2) + torch.mean(u_right_pred**2)
   
    loss = loss_f + loss_0 + loss_b
    return loss

اکنون، باقیمانده (residual) معادله برگر را در نقاط کولوکیشن با محاسبه مشتقات مورد نیاز از طریق مشتق‌گیری خودکار (automatic differentiation) محاسبه می‌کنیم. سپس، یک تابع هزینه تعریف می‌کنیم که مجموع مربعات خطای باقیمانده PDE، خطای شرایط اولیه و خطاهای شرایط مرزی را تجمیع می‌کند. این هزینه ترکیبی، شبکه را به سمت یادگیری راه‌حلی هدایت می‌کند که هم قانون فیزیکی (PDE) و هم شرایط تحمیل شده (اولیه و مرزی) را برآورده سازد.

optimizer = optim.Adam(model.parameters(), lr=1e-3)
num_epochs = 5000

for epoch in range(num_epochs):
    optimizer.zero_grad()
    loss = loss_func(model)
    loss.backward()
    optimizer.step()
   
    if (epoch+1) % 500 == 0:
        print(f'Epoch {epoch+1}/{num_epochs}, Loss: {loss.item():.5e}')
       
print("Training complete!")

در اینجا، حلقه آموزش PINN را با استفاده از بهینه‌ساز Adam با نرخ یادگیری 1×10-3 تنظیم می‌کنیم. در طول ۵۰۰۰ دوره (epoch)، به طور مکرر هزینه (شامل باقیمانده PDE، خطاهای شرایط اولیه و مرزی) محاسبه می‌شود، گرادیان‌ها پس‌پخش (backpropagate) می‌شوند و پارامترهای مدل به‌روزرسانی می‌گردند. هر ۵۰۰ دوره، شماره دوره فعلی و مقدار هزینه برای نظارت بر پیشرفت چاپ می‌شود و در نهایت، اتمام آموزش اعلام می‌شود.

N_x, N_t = 256, 100
x = np.linspace(x_min, x_max, N_x)
t = np.linspace(t_min, t_max, N_t)
X, T = np.meshgrid(x, t)
XT = np.hstack((X.flatten()[:, None], T.flatten()[:, None]))
XT_tensor = torch.tensor(XT, dtype=torch.float32).to(device)

model.eval()
with torch.no_grad():
    u_pred = model(XT_tensor).cpu().numpy().reshape(N_t, N_x)

plt.figure(figsize=(8, 5))
plt.contourf(X, T, u_pred, levels=100, cmap='viridis')
plt.colorbar(label='u(x,t)')
plt.xlabel('x')
plt.ylabel('t')
plt.title("Predicted solution u(x,t) via PINN")
plt.show()

در نهایت، یک شبکه از نقاط را روی دامنه فضایی (x) و زمانی (t) تعریف شده ایجاد می‌کنیم، این نقاط را به مدل آموزش‌دیده می‌دهیم تا راه‌حل ??(??, ??) را پیش‌بینی کند و خروجی را به یک آرایه دو بعدی تغییر شکل می‌دهیم. همچنین، راه‌حل پیش‌بینی‌شده را به صورت یک نمودار کانتور (contour plot) با استفاده از matplotlib تجسم می‌کنیم که شامل نوار رنگی (colorbar)، برچسب محورها و عنوان است. این به شما امکان می‌دهد مشاهده کنید که PINN چگونه دینامیک معادله برگر را تقریب زده است.

در نتیجه، این آموزش نشان داد که چگونه می‌توان PINN ها را به طور مؤثر برای حل معادله برگر ۱ بعدی با گنجاندن فیزیک مسئله در فرآیند آموزش پیاده‌سازی کرد. از طریق ساخت دقیق شبکه عصبی، تولید داده‌های کولوکیشن و مرزی، و مشتق‌گیری خودکار، به مدلی دست یافتیم که راه‌حلی سازگار با PDE و شرایط تعیین‌شده را یاد می‌گیرد. این تلفیق یادگیری ماشین و فیزیک سنتی راه را برای پرداختن به مسائل چالش‌برانگیزتر در علوم محاسباتی و مهندسی هموار می‌کند و کاوش بیشتر در سیستم‌های با ابعاد بالاتر و معماری‌های عصبی پیچیده‌تر را تشویق می‌نماید.