콘텐츠로 이동
Data Prep
상세

Neural Operators

메타 정보

항목 내용
분류 Scientific Machine Learning / Operator Learning / PDE Solving
핵심 논문 "Fourier Neural Operator for Parametric PDEs" (Li et al., ICLR 2021, arXiv 2010.08895)
주요 저자 Zongyi Li, Nikola Kovachki, Kamyar Azizzadenesheli, Anima Anandkumar (Caltech)
핵심 개념 함수 공간 간의 매핑을 학습하는 신경망 -- 유한 차원 벡터가 아닌 연속 함수 입출력
관련 시스템 DeepONet, FNO, GNOT, Poseidon, Neural Operator Transformer
관련 분야 PDE Solving, Physics-Informed ML, Surrogate Modeling, Computational Science

정의

Neural Operator는 함수 공간(function space) 사이의 매핑을 학습하는 신경망이다. 일반적인 신경망이 유한 차원 벡터 \(\mathbb{R}^n \to \mathbb{R}^m\)을 학습하는 반면, Neural Operator는 무한 차원 함수 \(\mathcal{A} \to \mathcal{U}\)를 학습한다.

일반 신경망:
  입력: 고정 크기 벡터 x in R^n
  출력: 고정 크기 벡터 y in R^m
  학습: f: R^n -> R^m

Neural Operator:
  입력: 함수 a(x), x in D (도메인)
  출력: 함수 u(x), x in D'
  학습: G: A -> U  (함수 공간 간 매핑)

핵심 차이:
  - 이산화(discretization)에 독립적
  - 해상도가 달라져도 동일 모델 사용 가능
  - 연속 함수 근사의 이론적 보장

배경: 왜 Neural Operator인가

편미분방정식(PDE) 풀이의 병목

전통적 PDE 풀이 (FEM, FDM, Spectral Methods):
  장점: 이론적 보장, 높은 정확도
  단점: 
    - 하나의 초기/경계 조건에 대해 하나의 해 계산
    - 메시 해상도에 따라 O(n^3) 이상의 계산 비용
    - 파라미터 변경 시 처음부터 재계산
    - 실시간 응용 불가

Neural Operator 접근:
  학습 단계: 다양한 입력-해 쌍으로 연산자 학습 (오프라인, 비용 높음)
  추론 단계: 새로운 입력에 대해 즉시 해 예측 (온라인, 비용 매우 낮음)

  결과:
    - 기존 대비 1000~10000배 빠른 추론
    - 파라미터 변경에 즉각 대응
    - 실시간 시뮬레이션 가능

PDE의 형식적 정의

편미분방정식의 일반 형식:
  L[u](x) = f(x),    x in D (도메인)
  B[u](x) = g(x),    x in dD (경계)

여기서:
  L: 미분 연산자
  u: 미지 함수 (해)
  f: 소스 항 (forcing term)
  B: 경계 조건 연산자

Neural Operator가 학습하는 것:
  G_theta: (f, g, 기타 파라미터) -> u
  즉, "입력 조건이 주어지면 해를 출력하는 연산자"

이론적 기반

Universal Approximation Theorem for Operators

Chen & Chen (1995)이 증명한 연산자의 보편 근사 정리:

정리 (Operator Universal Approximation):
  연속 연산자 G: X -> Y에 대해, 충분히 넓은 신경망으로 구성된
  연산자 G_theta가 존재하여:

  sup_{a in K} ||G(a) - G_theta(a)||_Y < epsilon

  여기서 K는 X의 컴팩트 부분집합, epsilon > 0은 임의의 오차

의미:
  - 함수 공간 간의 임의의 연속 매핑을 신경망으로 근사 가능
  - 유한 차원의 보편 근사 정리를 무한 차원으로 확장
  - DeepONet의 이론적 근거

이산화 불변성 (Discretization Invariance)

Neural Operator의 핵심 성질:

동일한 학습된 모델 G_theta에 대해:

  저해상도 입력 (32 x 32):  G_theta(a_32) -> u_32
  고해상도 입력 (256 x 256): G_theta(a_256) -> u_256

  두 출력이 일관된 결과를 제공

왜 중요한가:
  - 저해상도로 학습하고 고해상도로 추론 가능 (제로샷 초해상도)
  - 메시가 불규칙해도 동작
  - 학습 비용 절감 (저해상도에서 학습)

주요 아키텍처

1. DeepONet (Nature Machine Intelligence, 2021)

Lu et al.이 제안한 최초의 실용적 Neural Operator.

구조:
  Branch Network (입력 함수 인코딩):
    a(x_1), a(x_2), ..., a(x_m)  ->  [b_1, b_2, ..., b_p]

  Trunk Network (출력 좌표 인코딩):
    y  ->  [t_1, t_2, ..., t_p]

  출력:
    G_theta(a)(y) = sum_{k=1}^{p} b_k * t_k + b_0

  = Branch 출력과 Trunk 출력의 내적

직관:
  Branch: "입력 함수의 특성을 p개의 계수로 요약"
  Trunk:  "출력 공간의 기저 함수를 학습"
  결합:   "학습된 기저 함수의 선형 결합으로 출력 함수 구성"
구성 요소 입력 출력 역할
Branch Net 입력 함수의 센서값 [a(x_1),...,a(x_m)] 계수 벡터 [b_1,...,b_p] 입력 함수 인코딩
Trunk Net 출력 좌표 y 기저 벡터 [t_1,...,t_p] 출력 공간 기저 학습

변형:

변형 핵심 개선
POD-DeepONet Trunk을 Proper Orthogonal Decomposition 기저로 초기화
Physics-Informed DeepONet PDE 잔차를 손실에 추가 (데이터 없이도 학습 가능)
MIONet 다중 입력 함수 처리 (Multiple-Input Operator Net)
L-DeepONet 잠재 공간에서 연산자 학습 (차원 축소)

2. Fourier Neural Operator (FNO) (ICLR 2021)

Li et al.이 제안한 스펙트럴 도메인 기반 Neural Operator. 핵심 아이디어는 적분 연산자의 커널을 주파수 영역에서 학습하는 것이다.

FNO 레이어 (Fourier Layer):

  v_{t+1}(x) = sigma( W * v_t(x) + K(v_t)(x) )

  여기서:
    W: 선형 변환 (포인트별, 잔차 연결 역할)
    K: 적분 커널 연산자
    sigma: 활성화 함수 (GeLU)

  핵심: K의 효율적 계산

  K(v)(x) = F^{-1}( R_phi * F(v) )(x)

  단계별:
    1. FFT:     F(v)     -- 공간 -> 주파수 변환
    2. Filter:  R_phi * F(v) -- 주파수 영역에서 학습된 가중치 적용
    3. IFFT:    F^{-1}(.) -- 주파수 -> 공간 역변환

  R_phi: 학습 가능한 주파수 필터 (저주파 k_max개만 유지)

계산 복잡도:
  직접 적분: O(n^2)  (모든 점 쌍에 대해)
  FFT 활용:  O(n log n)  (FFT의 효율성)

아키텍처 전체 구조:

입력 a(x) 
  -> Lifting (FC layer): R^d_a -> R^d_v  (채널 차원 확장)
  -> Fourier Layer 1
  -> Fourier Layer 2
  -> ...
  -> Fourier Layer L
  -> Projection (FC layer): R^d_v -> R^d_u  (출력 차원)
  -> 출력 u(x)
하이퍼파라미터 역할 일반적 값
k_max (모드 수) 유지할 주파수 모드 수 12~32
d_v (너비) 은닉 채널 차원 20~64
L (깊이) Fourier Layer 수 4~8
활성화 비선형 함수 GeLU

FNO 변형:

변형 개선 점 차원
FNO-1d 1차원 시계열 PDE 1D
FNO-2d 정상 상태 2D PDE 2D
FNO-3d 시공간 PDE (2D 공간 + 시간) 3D
U-FNO U-Net 구조 결합 (다중 스케일) 2D/3D
Geo-FNO 불규칙 도메인 지원 (좌표 변환) 임의
F-FNO 잔차 연결 + 정규화 개선 임의
SFNO 구면(Spherical) 도메인 (기상 예보) S^2

3. Neural Operator Transformer (GNOT)

Attention 메커니즘을 사용한 Neural Operator.

GNOT (General Neural Operator Transformer):

  입력: (좌표, 함수값) 쌍의 집합 {(x_i, a(x_i))}

  1. 토큰화: 각 점을 토큰으로 변환
  2. Cross-Attention: 입력 함수 -> 쿼리 포인트
  3. Self-Attention: 쿼리 포인트 간 상호작용
  4. 디코딩: 출력 함수값 예측

장점:
  - 불규칙 도메인에 자연스럽게 적용
  - 다중 해상도/다중 스케일 처리
  - 긴 범위 의존성 포착

아키텍처 비교

특성 DeepONet FNO GNOT
이론적 근거 Universal Approx. 커널 적분 + FFT Attention
입력 형태 고정 센서 위치 균일 격자 임의 점 집합
도메인 제약 없음 균일 격자 (원본) 없음
해상도 전이 제한적 자연스러움 자연스러움
계산 복잡도 O(mp) O(n log n) O(n^2)
장기 의존성 제한적 전역 (주파수) 전역 (Attention)
구현 난이도 낮음 중간 높음

학습 방법

데이터 기반 학습

학습 데이터: {(a_i, u_i)}_{i=1}^{N}
  a_i: i번째 입력 함수 (이산화된 값)
  u_i: i번째 출력 함수 (해, 이산화된 값)

손실 함수:
  L(theta) = (1/N) * sum_{i=1}^{N} ||G_theta(a_i) - u_i||^2

  또는 상대 오차:
  L(theta) = (1/N) * sum_{i=1}^{N} ||G_theta(a_i) - u_i|| / ||u_i||

데이터 생성:
  - 전통적 PDE 솔버 (FEM, Spectral Method)로 (입력, 해) 쌍 생성
  - 학습 시 비용이 크지만 추론 시 상각 (amortization)

Physics-Informed 학습

PDE 잔차를 손실에 포함 (데이터 부족 시 유용):

L(theta) = L_data + lambda_pde * L_pde + lambda_bc * L_bc

L_data = ||G_theta(a) - u||^2                (데이터 손실)
L_pde  = ||L[G_theta(a)] - f||^2             (PDE 잔차)
L_bc   = ||B[G_theta(a)] - g||^2             (경계 조건)

장점:
  - 데이터가 부족해도 물리 법칙으로 보완
  - 물리적으로 일관된 해 보장

단점:
  - 미분 연산 계산 필요 (자동 미분)
  - 학습 불안정성 증가
  - lambda 하이퍼파라미터 조정 필요

벤치마크 및 성능

표준 벤치마크

벤치마크 PDE 차원 난이도
Darcy Flow 타원형 PDE (정상 상태) 2D 낮음
Navier-Stokes 비압축성 유체 2D+t 높음
Burgers 비선형 쌍곡선 PDE 1D+t 중간
Shallow Water 천수 방정식 2D+t 높음
Advection 이류 방정식 1D+t 낮음
Helmholtz 파동 산란 2D 중간

성능 비교 (Navier-Stokes, 상대 오차 %)

모델 점성도 1e-3 점성도 1e-4 점성도 1e-5 추론 시간
FEM (기준) - - - ~30분
ResNet 6.44 16.8 - 0.01초
U-Net 2.33 8.21 - 0.01초
FNO-2d 1.56 7.31 15.6 0.005초
FNO-3d 0.83 4.83 8.59 0.03초
U-FNO 0.67 4.12 - 0.03초
GNOT 0.58 3.87 - 0.1초

Python 구현

Fourier Neural Operator (2D)

import torch
import torch.nn as nn
import torch.nn.functional as F
from typing import Optional, Tuple


class SpectralConv2d(nn.Module):
    """2D Fourier Layer의 스펙트럴 합성곱"""

    def __init__(
        self,
        in_channels: int,
        out_channels: int,
        modes1: int,
        modes2: int
    ):
        """
        Args:
            in_channels: 입력 채널 수
            out_channels: 출력 채널 수
            modes1: 첫 번째 차원의 Fourier 모드 수
            modes2: 두 번째 차원의 Fourier 모드 수
        """
        super().__init__()
        self.in_channels = in_channels
        self.out_channels = out_channels
        self.modes1 = modes1
        self.modes2 = modes2

        self.scale = 1 / (in_channels * out_channels)

        # 복소수 가중치 (실수부 + 허수부)
        # 4개 영역: (+,+), (+,-) 주파수 조합
        self.weights1 = nn.Parameter(
            self.scale * torch.rand(
                in_channels, out_channels, modes1, modes2, dtype=torch.cfloat
            )
        )
        self.weights2 = nn.Parameter(
            self.scale * torch.rand(
                in_channels, out_channels, modes1, modes2, dtype=torch.cfloat
            )
        )

    def compl_mul2d(
        self,
        input: torch.Tensor,
        weights: torch.Tensor
    ) -> torch.Tensor:
        """복소수 행렬 곱셈 (배치 텐서)"""
        # input:   (batch, in_ch, x, y)
        # weights: (in_ch, out_ch, x, y)
        # output:  (batch, out_ch, x, y)
        return torch.einsum("bixy,ioxy->boxy", input, weights)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        Args:
            x: (batch, channels, size_x, size_y)
        Returns:
            (batch, channels, size_x, size_y)
        """
        batchsize = x.shape[0]

        # 1. FFT (공간 -> 주파수)
        x_ft = torch.fft.rfft2(x)

        # 2. 저주파 모드에 학습된 가중치 적용
        out_ft = torch.zeros(
            batchsize, self.out_channels,
            x.size(-2), x.size(-1) // 2 + 1,
            dtype=torch.cfloat, device=x.device
        )

        # 상단 모드
        out_ft[:, :, :self.modes1, :self.modes2] = self.compl_mul2d(
            x_ft[:, :, :self.modes1, :self.modes2], self.weights1
        )
        # 하단 모드
        out_ft[:, :, -self.modes1:, :self.modes2] = self.compl_mul2d(
            x_ft[:, :, -self.modes1:, :self.modes2], self.weights2
        )

        # 3. IFFT (주파수 -> 공간)
        x = torch.fft.irfft2(out_ft, s=(x.size(-2), x.size(-1)))

        return x


class FNO2d(nn.Module):
    """2D Fourier Neural Operator"""

    def __init__(
        self,
        modes1: int = 12,
        modes2: int = 12,
        width: int = 32,
        n_layers: int = 4,
        in_channels: int = 3,    # 입력 채널 (함수값 + 좌표)
        out_channels: int = 1     # 출력 채널
    ):
        super().__init__()
        self.modes1 = modes1
        self.modes2 = modes2
        self.width = width
        self.n_layers = n_layers

        # Lifting: 입력 -> 은닉 차원
        self.fc0 = nn.Linear(in_channels, self.width)

        # Fourier Layers
        self.spectral_convs = nn.ModuleList()
        self.linear_convs = nn.ModuleList()
        self.norms = nn.ModuleList()

        for _ in range(n_layers):
            self.spectral_convs.append(
                SpectralConv2d(width, width, modes1, modes2)
            )
            self.linear_convs.append(
                nn.Conv2d(width, width, 1)  # 1x1 합성곱 (잔차)
            )
            self.norms.append(nn.InstanceNorm2d(width))

        # Projection: 은닉 -> 출력
        self.fc1 = nn.Linear(self.width, 128)
        self.fc2 = nn.Linear(128, out_channels)

    def forward(
        self,
        x: torch.Tensor,
        grid: Optional[torch.Tensor] = None
    ) -> torch.Tensor:
        """
        Args:
            x: (batch, size_x, size_y, in_channels) 입력 함수
            grid: 격자 좌표 (선택, 없으면 자동 생성)
        Returns:
            (batch, size_x, size_y, out_channels) 출력 함수
        """
        # 격자 좌표 추가
        if grid is not None:
            x = torch.cat([x, grid], dim=-1)

        # Lifting
        x = self.fc0(x)                          # (B, S, S, width)
        x = x.permute(0, 3, 1, 2)                # (B, width, S, S)

        # Fourier Layers
        for i in range(self.n_layers):
            x1 = self.spectral_convs[i](x)       # 스펙트럴 경로
            x2 = self.linear_convs[i](x)          # 잔차 경로
            x = self.norms[i](x1 + x2)            # 합산 + 정규화
            if i < self.n_layers - 1:
                x = F.gelu(x)                     # 마지막 레이어 제외 활성화

        # Projection
        x = x.permute(0, 2, 3, 1)                # (B, S, S, width)
        x = F.gelu(self.fc1(x))
        x = self.fc2(x)                           # (B, S, S, out_channels)

        return x

    @staticmethod
    def get_grid(shape: Tuple[int, int], device: torch.device) -> torch.Tensor:
        """균일 격자 좌표 생성"""
        size_x, size_y = shape
        gridx = torch.linspace(0, 1, size_x, device=device)
        gridy = torch.linspace(0, 1, size_y, device=device)
        gridx, gridy = torch.meshgrid(gridx, gridy, indexing='ij')
        grid = torch.stack([gridx, gridy], dim=-1)  # (S, S, 2)
        return grid.unsqueeze(0)  # (1, S, S, 2)

DeepONet 구현

import torch
import torch.nn as nn
from typing import List


class DeepONet(nn.Module):
    """Deep Operator Network"""

    def __init__(
        self,
        branch_input_dim: int,     # 센서 개수 m
        trunk_input_dim: int,      # 좌표 차원 (1D=1, 2D=2, ...)
        hidden_dim: int = 128,
        n_basis: int = 64,         # 기저 함수 개수 p
        n_hidden_layers: int = 4,
        activation: str = "relu"
    ):
        super().__init__()
        self.n_basis = n_basis

        act_fn = {"relu": nn.ReLU, "tanh": nn.Tanh, "gelu": nn.GELU}[activation]

        # Branch Network (입력 함수 인코딩)
        branch_layers = [nn.Linear(branch_input_dim, hidden_dim), act_fn()]
        for _ in range(n_hidden_layers - 1):
            branch_layers.extend([nn.Linear(hidden_dim, hidden_dim), act_fn()])
        branch_layers.append(nn.Linear(hidden_dim, n_basis))
        self.branch = nn.Sequential(*branch_layers)

        # Trunk Network (출력 좌표 인코딩)
        trunk_layers = [nn.Linear(trunk_input_dim, hidden_dim), act_fn()]
        for _ in range(n_hidden_layers - 1):
            trunk_layers.extend([nn.Linear(hidden_dim, hidden_dim), act_fn()])
        trunk_layers.append(nn.Linear(hidden_dim, n_basis))
        self.trunk = nn.Sequential(*trunk_layers)

        # 바이어스
        self.bias = nn.Parameter(torch.zeros(1))

    def forward(
        self,
        branch_input: torch.Tensor,   # (batch, m) 센서 값
        trunk_input: torch.Tensor      # (batch, n_points, dim) 좌표
    ) -> torch.Tensor:
        """
        Args:
            branch_input: (batch, m) 입력 함수의 센서 값
            trunk_input: (batch, n_points, dim) 출력 좌표
        Returns:
            (batch, n_points) 출력 함수값
        """
        # Branch: (batch, m) -> (batch, p)
        b = self.branch(branch_input)

        # Trunk: (batch, n_points, dim) -> (batch, n_points, p)
        t = self.trunk(trunk_input)

        # 내적: sum_k b_k * t_k + bias
        # b: (batch, p) -> (batch, 1, p)
        output = torch.sum(b.unsqueeze(1) * t, dim=-1) + self.bias

        return output


class PhysicsInformedDeepONet(DeepONet):
    """Physics-Informed DeepONet (PDE 잔차 학습)"""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def compute_pde_residual(
        self,
        branch_input: torch.Tensor,
        trunk_input: torch.Tensor,
        pde_fn  # PDE 잔차 계산 함수
    ) -> torch.Tensor:
        """PDE 잔차 계산 (자동 미분 활용)"""
        trunk_input.requires_grad_(True)

        output = self.forward(branch_input, trunk_input)

        # 좌표에 대한 미분
        grad_outputs = torch.ones_like(output)
        grads = torch.autograd.grad(
            output, trunk_input,
            grad_outputs=grad_outputs,
            create_graph=True
        )[0]

        # PDE 잔차
        residual = pde_fn(output, grads, trunk_input)

        return residual

    def loss_fn(
        self,
        branch_input: torch.Tensor,
        trunk_input: torch.Tensor,
        targets: torch.Tensor,
        pde_fn,
        lambda_pde: float = 1.0,
        lambda_bc: float = 10.0,
        bc_mask: torch.Tensor = None
    ) -> dict:
        """통합 손실 함수"""
        output = self.forward(branch_input, trunk_input)

        # 데이터 손실
        loss_data = F.mse_loss(output, targets)

        # PDE 잔차 손실
        residual = self.compute_pde_residual(
            branch_input, trunk_input, pde_fn
        )
        loss_pde = torch.mean(residual ** 2)

        # 경계 조건 손실
        loss_bc = torch.tensor(0.0)
        if bc_mask is not None:
            loss_bc = F.mse_loss(
                output[bc_mask], targets[bc_mask]
            )

        total = loss_data + lambda_pde * loss_pde + lambda_bc * loss_bc

        return {
            "total": total,
            "data": loss_data.item(),
            "pde": loss_pde.item(),
            "bc": loss_bc.item()
        }

학습 파이프라인

import torch
from torch.utils.data import DataLoader, TensorDataset


def train_fno(
    model: FNO2d,
    train_data: dict,       # {"input": (N, S, S, c_in), "output": (N, S, S, c_out)}
    val_data: dict,
    epochs: int = 500,
    lr: float = 1e-3,
    batch_size: int = 16,
    scheduler_step: int = 100,
    scheduler_gamma: float = 0.5,
    device: str = "cuda"
):
    """FNO 학습 루프"""
    model = model.to(device)
    optimizer = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=1e-4)
    scheduler = torch.optim.lr_scheduler.StepLR(
        optimizer, step_size=scheduler_step, gamma=scheduler_gamma
    )

    # 데이터 준비
    x_train = train_data["input"].to(device)
    y_train = train_data["output"].to(device)

    # 격자 좌표 생성
    S = x_train.shape[1]
    grid = FNO2d.get_grid((S, S), device).repeat(x_train.shape[0], 1, 1, 1)

    dataset = TensorDataset(x_train, y_train)
    dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)

    for epoch in range(epochs):
        model.train()
        total_loss = 0.0

        for batch_x, batch_y in dataloader:
            optimizer.zero_grad()

            # 격자 좌표 추가
            batch_grid = FNO2d.get_grid(
                (S, S), device
            ).repeat(batch_x.shape[0], 1, 1, 1)

            # Forward
            pred = model(batch_x, batch_grid)

            # 상대 L2 오차
            loss = relative_l2_loss(pred, batch_y)

            loss.backward()
            optimizer.step()
            total_loss += loss.item()

        scheduler.step()

        # 검증
        if (epoch + 1) % 10 == 0:
            model.eval()
            with torch.no_grad():
                val_grid = FNO2d.get_grid(
                    (S, S), device
                ).repeat(val_data["input"].shape[0], 1, 1, 1)
                val_pred = model(
                    val_data["input"].to(device), val_grid
                )
                val_loss = relative_l2_loss(
                    val_pred, val_data["output"].to(device)
                )

            print(f"Epoch {epoch+1}: "
                  f"Train Loss = {total_loss/len(dataloader):.6f}, "
                  f"Val Loss = {val_loss.item():.6f}")


def relative_l2_loss(pred: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
    """상대 L2 손실"""
    diff_norm = torch.norm(
        pred.reshape(pred.shape[0], -1) - target.reshape(target.shape[0], -1),
        dim=1
    )
    target_norm = torch.norm(
        target.reshape(target.shape[0], -1), dim=1
    )
    return torch.mean(diff_norm / target_norm)

해상도 전이 실험

def resolution_transfer_experiment(
    model: FNO2d,
    test_data_low: dict,    # 64 x 64
    test_data_high: dict,   # 256 x 256
    device: str = "cuda"
):
    """
    저해상도(64x64)로 학습된 모델을 고해상도(256x256)에서 평가
    FNO의 이산화 불변성 검증
    """
    model.eval()

    with torch.no_grad():
        # 저해상도 평가
        grid_low = FNO2d.get_grid((64, 64), device).repeat(
            test_data_low["input"].shape[0], 1, 1, 1
        )
        pred_low = model(test_data_low["input"].to(device), grid_low)
        error_low = relative_l2_loss(
            pred_low, test_data_low["output"].to(device)
        )

        # 고해상도 평가 (동일 모델, 다른 해상도)
        grid_high = FNO2d.get_grid((256, 256), device).repeat(
            test_data_high["input"].shape[0], 1, 1, 1
        )
        pred_high = model(test_data_high["input"].to(device), grid_high)
        error_high = relative_l2_loss(
            pred_high, test_data_high["output"].to(device)
        )

    print(f"저해상도 (64x64) 상대 오차:  {error_low.item():.4f}")
    print(f"고해상도 (256x256) 상대 오차: {error_high.item():.4f}")

    return error_low.item(), error_high.item()

실세계 응용

기상 예보 (Weather Forecasting)

FourCastNet (Pathak et al., NVIDIA, 2022):
  - SFNO (Spherical FNO) 기반
  - ERA5 재분석 데이터로 학습
  - 전통적 NWP 대비 45000배 빠른 추론
  - 6시간 단위 예보, 7일까지

Poseidon (Herde et al., 2024):
  - Foundation Model for PDE Solving
  - 15개 PDE 벤치마크에서 사전학습
  - 새로운 PDE에 소량 파인튜닝으로 적응

분자 동역학 (Molecular Dynamics)

DPMD (Deep Potential Molecular Dynamics):
  - 원자 간 포텐셜 에너지를 Neural Operator로 학습
  - DFT(밀도 범함수 이론) 정확도 + MD 속도
  - 10^6 원자 규모 시뮬레이션 가능

EquiNO (Equivariant Neural Operator):
  - E(3) 등변성을 Neural Operator에 통합
  - 분자의 회전/이동에 자연스럽게 불변

공학 설계 최적화

응용 입력 함수 출력 함수 속도 향상
공기역학 날개 형상 압력/양력 분포 약 1000배
구조 해석 하중 분포 응력/변형 약 500배
열 전달 온도 경계 조건 온도장 약 2000배
전자기 안테나 형상 전자기장 약 800배

한계 및 주의사항

1. 학습 데이터 생성 비용
   - 전통적 솔버로 충분한 (입력, 해) 쌍 필요
   - 고차원/고정밀 시뮬레이션은 데이터 생성 자체가 병목
   - 해결: Physics-Informed 학습, 전이 학습

2. 외삽(Extrapolation) 한계
   - 학습 분포 밖의 파라미터에서 성능 저하
   - 극단적 조건 (높은 Re수 등)에서 불안정
   - 해결: 분포 외 탐지, 물리 제약 추가

3. 정확도 vs 기존 솔버
   - 고정밀이 필요한 경우 기존 솔버가 여전히 우수
   - Neural Operator는 "빠른 근사"에 적합
   - 해결: 하이브리드 (Neural Operator 초기값 + 솔버 정제)

4. FNO의 균일 격자 제약
   - 원본 FNO는 균일 격자만 지원
   - Geo-FNO, GNOT 등으로 해결 가능
   - 실제 공학 문제는 대부분 불규칙 메시

5. 해석 가능성
   - 블랙박스 특성으로 물리적 해석 어려움
   - 기존 솔버는 수렴성 등 이론적 보장 제공
   - Neural Operator의 오차 바운드 연구 진행 중

관련 연구 흐름

Universal Approximation for Operators (Chen & Chen, 1995)
    |
    +-- DeepONet (Lu et al., Nature MI, 2021)
    |       |
    |       +-- Physics-Informed DeepONet (Wang et al., 2021)
    |       +-- MIONet (Jin et al., 2022)
    |       +-- L-DeepONet (Sharma & Shankar, 2024)
    |
    +-- Neural Operator Theory (Kovachki et al., JMLR 2023)
    |       |
    |       +-- 보편 근사 정리 통합 프레임워크
    |
    +-- Fourier Neural Operator (Li et al., ICLR 2021)
    |       |
    |       +-- U-FNO (Wen et al., 2022)
    |       +-- Geo-FNO (Li et al., 2023)
    |       +-- SFNO (Bonev et al., 2023)
    |       +-- F-FNO (Tran et al., 2023)
    |
    +-- Graph Neural Operator (Li et al., NeurIPS 2020)
    |
    +-- GNOT (Hao et al., ICML 2023)
    |
    +-- FourCastNet (Pathak et al., 2022)
    |       |
    |       +-- 기상 예보 분야 돌파
    |
    +-- Poseidon (Herde et al., NeurIPS 2024)
    |       |
    |       +-- PDE Foundation Model
    |
    +-- Neural Operator + Symmetry (2024~)
            |
            +-- 등변 Neural Operator
            +-- 구조 보존 Neural Operator

참고 자료

핵심 논문

  1. Li, Z. et al. (2021). Fourier Neural Operator for Parametric Partial Differential Equations. ICLR 2021. arXiv:2010.08895.
  2. Lu, L. et al. (2021). Learning Nonlinear Operators via DeepONet Based on the Universal Approximation Theorem of Operators. Nature Machine Intelligence, 3, 218-229.
  3. Kovachki, N. et al. (2023). Neural Operator: Learning Maps Between Function Spaces with Applications to PDEs. JMLR, 24(89), 1-97.
  4. Hao, Z. et al. (2023). GNOT: A General Neural Operator Transformer for Operator Learning. ICML 2023.

확장 논문

  1. Wang, S., Wang, H., & Perdikaris, P. (2021). Learning the Solution Operator of Parametric PDEs with Physics-Informed DeepONets. Science Advances.
  2. Pathak, J. et al. (2022). FourCastNet: A Global Data-driven High-resolution Weather Forecasting Model. arXiv:2202.11214.
  3. Herde, M. et al. (2024). Poseidon: Efficient Foundation Models for PDEs. NeurIPS 2024.
  4. Li, Z. et al. (2023). Geometry-Informed Neural Operator for Large-Scale 3D PDEs. NeurIPS 2023.

관련 개념