11. Physics-Informed Neural Network (PINN)#
Marc BUFFAT , dpt mécanique, Université Lyon 1

(C) Zhang, P., Zhang, H., Zhou, J. et al., Sci Rep. 16, 12760, 2026 Nature
Certains contenus ont été coconstruits avec l’appui d’un outil d’IA générative puis relus et validés par l’enseignant.
from validation.libIA_GPU import Init_torchGPU
try: cuda_dev
except NameError: cuda_dev = Init_torchGPU(4, 0)
if cuda_dev is None: cuda_dev = "cpu"
print(cuda_dev)
Max threads : 4 / used threads 4
Attention: no GPU available! using CPU
cpu
/home/buffat/venvs/jupyter/lib/python3.10/site-packages/torch/cuda/__init__.py:107: UserWarning: CUDA initialization: The NVIDIA driver on your system is too old (found version 9010). Please update your GPU driver by downloading and installing a new version from the URL: http://www.nvidia.com/Download/index.aspx Alternatively, go to: https://pytorch.org to install a PyTorch version that has been compiled with your version of the CUDA driver. (Triggered internally at ../c10/cuda/CUDAFunctions.cpp:109.)
return torch._C._cuda_getDeviceCount() > 0
Les Physics-Informed Neural Networks (PINN) forment une classe de méthodes qui consiste à entraîner un réseau de neurones non seulement sur des données, mais aussi sur les lois physiques décrites par des équations différentielles. L’idée a été popularisée par Maziar Raissi et ses collaborateurs en 2019 Raissi, Perdikaris, and Karniadakis [RPK19].
références
import numpy as np
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
print(torch.cuda.is_available())
device = torch.device(cuda_dev)
print("CUDA device: ",cuda_dev)
print("Torch CUDA device: ",device," threads:",torch.get_num_threads(),torch.get_num_interop_threads())
False
CUDA device: cpu
Torch CUDA device: cpu threads: 4 4
11.1. Approximation de fonction#
L’approximation de fonctions est l’une des applications fondamentales des réseaux de neurones. Avec PyTorch, il est simple de construire un réseau capable d’apprendre une fonction à partir de données.
Prenons un exemple où l’on souhaite approximer la fonction : \(f(x)=sin(x)\)
On définit les données xdata et ydata
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
# Données d'entraînement: attention sans gradient
Nd = 100
xdata = torch.linspace(-2 * torch.pi, 2 * torch.pi, Nd,device=device).reshape(-1, 1)
ydata = torch.sin(xdata)
# réseau de neuronnes
class NeuralNet(nn.Module):
def __init__(self):
super().__init__()
self.model = nn.Sequential(
nn.Linear(1, 32),
nn.Tanh(),
nn.Linear(32, 32),
nn.Tanh(),
nn.Linear(32, 1)
)
def forward(self, x):
return self.model(x)
model = NeuralNet().to(device)
# fonction cout
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
# entrainement du réseau
num_trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Model Trainable parameters: {num_trainable}")
epochs = 3000
for epoch in range(epochs):
optimizer.zero_grad()
ypred = model(xdata)
loss = torch.mean((ypred-ydata)**2)
loss.backward()
optimizer.step()
if epoch % 200 == 0:
print(f"Epoch {epoch} : Loss = {loss.item():.6f}")
print(f"Erreur finale = {loss.item():.6f}")
Model Trainable parameters: 1153
Epoch 0 : Loss = 0.467291
Epoch 200 : Loss = 0.008134
Epoch 400 : Loss = 0.001052
Epoch 600 : Loss = 0.000379
Epoch 800 : Loss = 0.000280
Epoch 1000 : Loss = 0.000062
Epoch 1200 : Loss = 0.000081
Epoch 1400 : Loss = 0.000024
Epoch 1600 : Loss = 0.000062
Epoch 1800 : Loss = 0.000017
Epoch 2000 : Loss = 0.000028
Epoch 2200 : Loss = 0.001427
Epoch 2400 : Loss = 0.000015
Epoch 2600 : Loss = 0.003150
Epoch 2800 : Loss = 0.001329
Erreur finale = 0.000010
11.1.1. Validation#
Pour valider, on définit des points de collocation x (remarque: xdata et x ne sont pas forcément identiques).
Si on veut calculer les dérivées de la fonction \(f(x)\), on définit les points de collocation x avec l’option x.requires_grad=True.
# points de collocation où on définit la fonction
N = 2*Nd
x = torch.linspace(-2 * torch.pi, 2 * torch.pi, N,device=device).reshape(-1, 1)
# si on veut calculer les dérivées
x.requires_grad=True
# tester le modele
with torch.no_grad():
y_pred = model(x)
xn = xdata.cpu()
yn = ydata.cpu()
xp = x.detach().cpu()
yp = y_pred.cpu()
plt.figure(figsize=(8,8))
plt.subplot(2,1,1)
plt.plot(xn, yn, label="sin(x)")
plt.plot(xp, yp, '--', label="Approximation NN")
plt.legend()
plt.grid()
plt.subplot(2,1,2)
yp = model(xdata).detach().cpu()
plt.plot(xn, yn - yp)
plt.title("Erreur")
plt.show()
11.1.2. Calcul de la dérivée#
Si on a calculé l’approximation avec x.requires_grad=True, on peut calculer la dérivée avec la fonction torch.autograd.grad, qui utilise la différentiation automatique dans PyTorch.
ypred = model(x)
dypred = torch.autograd.grad(
ypred,
x,
grad_outputs=torch.ones_like(ypred),
create_graph=True
)[0]
dyp = dypred.detach().cpu()
dye = np.cos(xp)
# tracer
plt.figure(figsize=(8,8))
plt.subplot(2,1,1)
plt.title("Dérivée de la fonction $f(x)=sin(x)$")
plt.plot(xp,dyp,label='dérivée PINN')
plt.plot(xp,dye,label='exacte')
plt.legend()
plt.grid()
plt.subplot(2,1,2)
plt.plot(xp, dye - dyp)
plt.title("Erreur")
Text(0.5, 1.0, 'Erreur')
11.1.3. théorème d’approximation universelle#
Le théorème d’approximation universelle Wikipedia indique qu’un réseau de neurones à une couche cachée suffisamment large (avec une fonction d’activation non linéaire comme Tanh, ReLU ou Sigmoid) peut approximer arbitrairement bien toute fonction continue sur un domaine compact.
Principe des combinaisons non linéaires
Chaque neurone décale, étire ou contracte l’entrée d’une manière légèrement différente, puis la fait passer par une fonction d’activation non-linéaire pour produire une réponse partielle.
Ces réponses partielles sont fortement actives dans certaines régions et presque inactives dans d’autres, comme de petits fragments de fonction.
La couche de sortie combine ensuite ces fragments par somme pondérée pour former la fonction globale.
Au final, le réseau de neurones ne décrit pas une règle générale d’un seul bloc : il construit une fonction complexe en empilant de nombreuses petites réponses.
Signification de l’approximation
Le théorème d’approximation universelle ne signifie pas que le réseau reproduit toujours exactement la fonction d’origine.
Son sens précis est le suivant : si l’on fixe à l’avance une tolérance d’erreur, il est possible de réduire l’écart entre les fonctions en dessous de cette tolérance.
L’idée centrale n’est donc pas exactement identique, mais aussi proche que souhaité.
Cette distinction permet d’éviter de confondre capacité de représentation et précision réelle.
avec $\( \max_{x\in K} |f(x)-\hat{f}(x)| < \epsilon\)$
importance et limite
Le théorème d’approximation universelle fournit une base mathématique solide montrant qu’un réseau de neurones n’est pas seulement un modèle linéaire simple, mais qu’il peut aussi représenter des fonctions continues extrêmement complexes. Il est donc souvent cité comme point de départ théorique pour expliquer pourquoi le Deep Learning peut traiter une grande variété de problèmes.
Toutefois, ce théorème garantit uniquement que la représentation est possible : il ne signifie pas que cette représentation sera efficace avec peu de neurones, ni que l’apprentissage sera simple et rapide. Dans les problèmes réels, le nombre de neurones requis peut devenir très important, et des difficultés liées à l’optimisation, à la quantité de données et à la généralisation apparaissent séparément. Autrement dit, ce théorème est essentiel pour comprendre le potentiel des réseaux de neurones, mais il ne garantit pas automatiquement leurs performances en pratique.
Extrait du programme ZeroMathAI.
11.1.4. Comparaison de l’approximation#
On peut comparer l’approximation par réseau de neurones avec une approximation par spline cubique.
Dans le cas de la fonction sinus, une approximation avec 20 splines donne un meilleur résultat qu’avec le réseau de neurones avec 1153 paramètres !
Il ne faut donc pas systématiquement utiliser cette approximation réseau de neurones.
import numpy as np
from scipy.interpolate import CubicSpline
N = 20
x = np.linspace(-2 * np.pi, 2 * np.pi, N)
y = np.sin(x)
cs = CubicSpline(x, y)
xs = torch.linspace(-2 * np.pi, 2 * np.pi,200)
ys = np.sin(xs)
ypred = cs(xs)
plt.figure(figsize=(8,8))
plt.subplot(2,1,1)
plt.title("Approximation classique")
plt.plot(xs,ys, label="sin(x)")
plt.plot(xs,ypred, '--', label="Splines")
plt.legend()
plt.grid()
plt.subplot(2,1,2)
plt.plot(xs, ys - ypred)
plt.title("Erreur")
plt.show()
Conseils pratiques
Normalisez les données si les entrées ou sorties ont des échelles très différentes.
Réservez un jeu de validation pour surveiller le sur apprentissage.
Choisissez la capacité du réseau (nombre de couches et de neurones) en fonction de la complexité de la fonction à approximer.
Utilisez torch.utils.data.DataLoader pour les grands ensembles de données.
Surveillez le niveau de perte (loss) sur les jeux d’entraînement et de validation afin d’ajuster les hyper paramètres (taux d’apprentissage, taille du réseau, nombre d’époques).
Ce schéma constitue la base de nombreuses applications en régression avec PyTorch, qu’il s’agisse d’approximer une fonction analytique, un modèle physique ou une relation empirique issue de données expérimentales.
11.2. Principe général d’un PINN pour résoudre une EDO#
Supposons que l’on souhaite résoudre une équation différentielle
avec des conditions aux limites ou des conditions initiales.
Au lieu de discrétiser le domaine (comme avec les méthodes des différences finies ou des éléments finis), on approxime directement la solution par un réseau de neurones :
où \(\theta\) représente les poids du réseau.
L’objectif est alors de trouver les paramètres \(\theta\) qui rendent cette fonction compatible avec les lois physiques.
11.2.1. Utilisation de la différentiation automatique#
L’un des points clés des PINN est que les dérivées sont calculées grâce à la différentiation automatique de PyTorch ou TensorFlow.
Par exemple, si \(u_\theta = u_\theta(x),\) alors \(\frac{\partial u_\theta}{\partial x}\) est obtenue automatiquement par
du_dx = torch.autograd.grad(
outputs=u,
inputs=x,
grad_outputs=torch.ones_like(u),
create_graph=True
)[0]
Ce calcul n’est pas basée sur une approximation numérique des dérivées, mais sur une différentiation automatique numérique de l’approximation par réseau de neurones.
11.2.2. Calcul du résidu physique#
Considérons l’ODE
Le réseau de neurones prédit \(u_\theta(t)\). On calcule ensuite la dérivée:
puis le résidu de l’équation
Si \(u_\theta(t)\) est solution de l’EDO, alors \(r(t)=0 \;\forall t\)
11.2.3. Calcul Fonction de coût#
L’entraînement consiste à minimiser plusieurs termes :
1. Perte physique
Elle impose à la fonction \(u_\theta(t)\) de vérifier l’équation différentielle en certains points.
Les \(x_i\) sont appelés points de collocation.
2. Conditions initiales ou aux limites
Par exemple pour imposer la condition initiale \(u(0)=1.\), on ajoute
3. Données expérimentales (optionnel)
Si l’on possède quelques mesures \((x_i,u_i),\) on ajoute
Ainsi, les PINN peuvent fonctionner :
sans données (la physique seule guide l’apprentissage) ;
avec quelques données (cas hybride) ;
avec beaucoup de données (en renforçant la cohérence physique).
11.2.4. Principe de l’algorithme#
Choix des points x
│
▼
Réseau de neurones
uθ(x)
│
┌─────────┴──────────┐
▼ ▼
Conditions Dérivées automatiques
initiales (autograd)
│ │
└─────────┬──────────┘
▼
Résidu de l'équation
│
▼
Fonction de coût totale
│
▼
Descente de gradient (Adam/L-BFGS)
│
▼
Mise à jour des paramètres θ
11.2.5. Pourquoi cela fonctionne ?#
Le théorème d’approximation universelle garantit qu’un réseau de neurones suffisamment grand peut approcher une très large classe de fonctions continues. Les PINN exploitent cette propriété pour chercher la fonction qui satisfait au mieux les équations physiques et les conditions imposées.
11.2.6. Avantages#
Pas de maillage complexe à construire.
Les dérivées sont obtenues avec une grande précision grâce à la différentiation automatique.
Faciles à étendre aux problèmes multidimensionnels.
Intègrent naturellement des connaissances physiques.
Peuvent résoudre des problèmes inverses, où des paramètres inconnus sont estimés en même temps que la solution.
11.2.7. Limites#
L’entraînement peut être lent, surtout pour des problèmes de grande dimension.
Les équations très raides, fortement non linéaires ou comportant des discontinuités sont difficiles à traiter.
Le choix des poids entre les différentes composantes de la fonction de perte est délicat.
Pour de nombreuses PDE industrielles, les méthodes numériques classiques (éléments finis, volumes finis, différences finies) restent souvent plus rapides et plus précises.
En résumé, un PINN remplace le solveur numérique traditionnel par un réseau de neurones entraîné à satisfaire directement les équations de la physique. La résolution d’une ODE ou d’une PDE devient alors un problème d’optimisation où l’on ajuste les paramètres du réseau pour minimiser le résidu de l’équation et respecter les conditions initiales ou aux limites.
11.3. Exemple 1: ODE d’ordre 1#
Voici un exemple complet d’un Physics-Informed Neural Network (PINN) en PyTorch pour résoudre une équation différentielle ordinaire (ODE).
Prenons l’ODE suivante :
La solution analytique est :
Le principe d’un PINN est de minimiser simultanément :
le résidu de l’équation différentielle,
les conditions initiales (ou aux limites).
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
# Choix du périphérique
print("using ",device)
# -------------------------
# Réseau de neurones
# -------------------------
class PINN(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(1, 32),
nn.Tanh(),
nn.Linear(32, 32),
nn.Tanh(),
nn.Linear(32, 32),
nn.Tanh(),
nn.Linear(32, 1)
)
def forward(self, x):
return self.net(x)
model = PINN().to(device)
# -------------------------
# Points de collocation
# -------------------------
N = 100
t = torch.linspace(0, 5, N).view(-1,1).to(device)
t.requires_grad = True
# Condition initiale
t0 = torch.tensor([[0.0]], device=device)
y0 = torch.tensor([[1.0]], device=device)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
# -------------------------
# Entraînement
# -------------------------
epochs = 5000
total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f'Total number of parameters: {total_params}')
for epoch in range(epochs):
optimizer.zero_grad()
# Prédiction
y = model(t)
# Dérivée dy/dt
dy_dt = torch.autograd.grad(
outputs=y,
inputs=t,
grad_outputs=torch.ones_like(y),
create_graph=True
)[0]
# Résidu de l'ODE
residual = dy_dt + y
loss_ode = torch.mean(residual**2)
# Condition initiale
pred0 = model(t0)
loss_ic = (pred0 - y0).pow(2).mean()
# Fonction coût
loss = loss_ode + loss_ic
# descente
loss.backward()
optimizer.step()
# affichage résidu
if epoch % 500 == 0:
print(f"Epoch {epoch:5d} Loss={loss.item():.3e}")
# -------------------------
# Évaluation et tracé de la solution
# -------------------------
with torch.no_grad():
t_test = torch.linspace(0,5,200).view(-1,1).to(device)
y_pred = model(t_test).cpu()
t_exact = t_test.cpu()
y_exact = torch.exp(-t_exact)
plt.figure(figsize=(8,5))
plt.plot(t_exact, y_exact, label="Solution exacte")
plt.plot(t_exact, y_pred, "--", label="PINN")
plt.xlabel("t")
plt.ylabel("y")
plt.legend()
plt.grid()
plt.show()
using cpu
Total number of parameters: 2209
Epoch 0 Loss=1.092e+00
Epoch 500 Loss=1.437e-04
Epoch 1000 Loss=3.391e-05
Epoch 1500 Loss=8.474e-06
Epoch 2000 Loss=3.132e-06
Epoch 2500 Loss=1.571e-06
Epoch 3000 Loss=7.364e-07
Epoch 3500 Loss=3.363e-07
Epoch 4000 Loss=2.209e-07
Epoch 4500 Loss=1.771e-07
11.3.1. Explication#
Le PINN apprend une fonction \(y_\theta(t)\) paramétrée par un réseau de neurones.
À chaque itération :
Le réseau prédit (y(t)).
PyTorch calcule automatiquement la dérivée \(\frac{dy_\theta}{dt}\) grâce à
torch.autograd.grad.On construit le résidu de l’ODE : \(r(t)=\frac{dy_\theta}{dt}+y_\theta.\)
La fonction de perte est $\( \mathcal{L} = \underbrace{\frac1N\sum_i r(t_i)^2}*{\text{équation}} + \underbrace{\left(y*\theta(0)-1\right)^2}_{\text{condition initiale}}. \)$
Le réseau est ainsi entraîné à satisfaire simultanément l’équation différentielle et la condition initiale.
11.3.2. Généralisation#
Pour une ODE quelconque
il suffit de remplacer
residual = dy_dt + y
par
residual = dy_dt - f(t, y)
où f(t, y) est une fonction Python définissant le membre de droite.
11.4. Exemple 2: l’oscillateur harmonique#
avec les conditions initiales
La solution exacte est
L’idée est de faire apprendre cette solution par un PINN.
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
# Choix du périphérique
print("using ",device)
# =====================================================
# Réseau de neurones
# =====================================================
class PINN(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(1,64),
nn.Tanh(),
nn.Linear(64,64),
nn.Tanh(),
nn.Linear(64,64),
nn.Tanh(),
nn.Linear(64,1)
)
def forward(self,x):
return self.net(x)
model = PINN().to(device)
# =====================================================
# Points de collocation
# =====================================================
N = 200
t = torch.linspace(0,10,N).reshape(-1,1).to(device)
t.requires_grad = True
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
# =====================================================
# Entraînement
# =====================================================
epochs = 5000
total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f'Total number of parameters: {total_params}')
for epoch in range(epochs):
optimizer.zero_grad()
# prédiction
y = model(t)
# première dérivée
dy = torch.autograd.grad(
y,
t,
grad_outputs=torch.ones_like(y),
create_graph=True
)[0]
# seconde dérivée
d2y = torch.autograd.grad(
dy,
t,
grad_outputs=torch.ones_like(dy),
create_graph=True
)[0]
# Résidu de l'ODE
residual = d2y + y
loss_phys = torch.mean(residual**2)
# =====================================================
# Conditions initiales
# =====================================================
t0 = torch.tensor([[0.0]], requires_grad=True,device=device)
y0 = model(t0)
dy0 = torch.autograd.grad(
y0,
t0,
grad_outputs=torch.ones_like(y0),
create_graph=True
)[0]
loss_ic = (y0-1)**2 + dy0**2
# =====================================================
loss = loss_phys + loss_ic
loss.backward()
optimizer.step()
if epoch % 500 == 0:
print(epoch, loss.item())
# =====================================================
# verification
# =====================================================
with torch.no_grad():
t_test = torch.linspace(0,10,300,device=device).view(-1,1)
y_pred = model(t_test).cpu()
t_exact = t_test.cpu()
y_exact = np.cos(t_exact)
plt.figure(figsize=(8,5))
plt.plot(t_exact,y_exact,label="Exact")
plt.plot(t_exact,y_pred,"--",label="PINN")
plt.legend()
plt.grid()
plt.show()
using cpu
Total number of parameters: 8513
0 1.487247109413147
500 0.029029496014118195
1000 0.019310669973492622
1500 0.01752581261098385
2000 0.014037854969501495
2500 0.021743537858128548
3000 0.005634583532810211
3500 0.0030259592458605766
4000 5.6583892728667706e-05
4500 2.9837970942026004e-05
11.4.1. Explication#
Pour une ODE du second ordre, le réseau approxime directement la fonction :
Les dérivées sont calculées par différentiation automatique :
En PyTorch, cela se fait avec deux appels successifs à torch.autograd.grad :
dy = torch.autograd.grad(
y, t,
grad_outputs=torch.ones_like(y),
create_graph=True
)[0]
d2y = torch.autograd.grad(
dy, t,
grad_outputs=torch.ones_like(dy),
create_graph=True
)[0]
Le résidu de l’équation est alors
et la perte physique est
Les conditions initiales sont ajoutées sous forme de pénalités :
La fonction de coût totale est
11.4.2. Généralisation#
Pour une ODE du second ordre de la forme
il suffit de remplacer le résidu par :
residual = d2y - f(t, y, dy)
où f est une fonction Python. Cette structure est valable pour la plupart des ODE du second ordre, qu’elles soient linéaires ou non linéaires. Elle peut ensuite être étendue aux systèmes d’ODE en faisant sortir plusieurs composantes du réseau et en construisant un résidu pour chaque équation.
11.5. Exemple 3: problème inverse#
Les problèmes inverses sont l’une des applications les plus intéressantes des PINN. Contrairement au problème direct (où les paramètres de l’équation sont connus), le but est ici d”estimer des paramètres physiques inconnus à partir de quelques mesures.
Considérons l’ODE suivante :
avec \(y(0)=1,\) où \(\lambda\) est inconnu.
Supposons que l’on dispose de quelques mesures bruitées de \(y(t)\) et que l’on souhaite retrouver \(\lambda\).
11.5.1. Principe#
Le réseau approxime la solution \(y_\theta(t).\)
Mais cette fois, le paramètre physique est lui aussi optimisé :
lambda_param = nn.Parameter(torch.tensor([0.5]))
Il est ajouté à la liste des paramètres entraînables.
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
# Choix du périphérique
print("using ",device)
# =====================================================
# Génération de données
# =====================================================
lambda_true = 2.0
t_data = torch.linspace(0, 2, 20,device=device).reshape(-1,1)
y_data = torch.exp(-lambda_true*t_data)
# bruit
y_data += 0.01*torch.randn_like(y_data)
# =====================================================
# PINN
# =====================================================
class PINN(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(1,32),
nn.Tanh(),
nn.Linear(32,32),
nn.Tanh(),
nn.Linear(32,1)
)
def forward(self,x):
return self.net(x)
model = PINN().to(device)
# =====================================================
# Paramètre inconnu
# =====================================================
lambda_param = nn.Parameter(torch.tensor([0.5],device=device))
# =====================================================
# Optimiseur
# =====================================================
optimizer = torch.optim.Adam(
list(model.parameters())+[lambda_param],
lr=1e-3
)
# =====================================================
# Points de collocation
# =====================================================
t_phys = torch.linspace(0,2,100,device=device).reshape(-1,1)
t_phys.requires_grad=True
# =====================================================
# Entraînement
# =====================================================
total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f'Total number of parameters: {total_params}')
for epoch in range(5000):
optimizer.zero_grad()
# --------------------------
# Physique
# --------------------------
y = model(t_phys)
dy = torch.autograd.grad(
y,
t_phys,
grad_outputs=torch.ones_like(y),
create_graph=True
)[0]
residual = dy + lambda_param*y
loss_phys = torch.mean(residual**2)
# --------------------------
# Données
# --------------------------
y_pred = model(t_data)
loss_data = torch.mean((y_pred-y_data)**2)
# --------------------------
# Condition initiale
# --------------------------
t0 = torch.tensor([[0.0]],device=device)
loss_ic = (model(t0)-1.)**2
# --------------------------
loss = loss_phys + loss_data + loss_ic
loss.backward()
optimizer.step()
if epoch%500==0:
print(epoch,
loss.item(),
lambda_param.item())
print("\nValeur estimée :", lambda_param.item(), "exacte:",lambda_true)
# =====================================================
# Comparaison
# =====================================================
with torch.no_grad():
t_test = torch.linspace(0,2,200,device=device).reshape(-1,1)
y_pred = model(t_test).cpu()
t = t_test.cpu()
plt.plot(t,np.exp(-lambda_true*t),label="Exacte")
plt.plot(t,y_pred,'--',label="PINN")
plt.scatter(t_data.cpu(),y_data.cpu(),color='red')
plt.legend()
plt.grid()
plt.show()
using cpu
Total number of parameters: 1153
0 1.08479905128479 0.49900001287460327
500 0.026822654530405998 0.9531194567680359
1000 0.010493461973965168 1.2658004760742188
1500 0.004385540261864662 1.4881391525268555
2000 0.0018292642198503017 1.6531840562820435
2500 0.0007415474974550307 1.7756088972091675
3000 0.00030112499371171 1.8634330034255981
3500 0.0001416778686689213 1.9224516153335571
4000 9.33576884563081e-05 1.9581180810928345
4500 0.00011533707584021613 1.9766340255737305
Valeur estimée : 1.9846880435943604 exacte: 2.0
11.5.2. Explication#
Le réseau apprend simultanément :
la fonction \(y(t)\),
le paramètre \(\lambda\).
La fonction de perte s’écrit
avec
Perte Physique
Perte sur les Données
Perte sur la Condition initiale
11.5.3. Pourquoi cela fonctionne ?#
Le gradient de la perte dépend de deux types de paramètres :
les poids du réseau de neurones \(\theta\) ;
le paramètre physique \(\lambda\).
Comme \(\lambda\) est déclaré comme un nn.Parameter, PyTorch calcule automatiquement
et l’optimiseur met à jour \(\lambda\) exactement comme les poids du réseau.
11.6. Applications réelles des PINNs#
Cette approche est utilisée pour identifier des paramètres physiques à partir de données limitées, par exemple :
coefficient de diffusion dans une équation de chaleur ;
viscosité d’un fluide dans les équations de Navier–Stokes equations ;
module d’Young ou conductivité thermique d’un matériau ;
constantes de réaction en cinétique chimique ;
paramètres d’un modèle épidémiologique (par exemple le taux de transmission dans un modèle SIR model) ;
paramètres d’amortissement et de raideur dans les systèmes mécaniques.
C’est précisément dans ce contexte où l’on dispose de peu de mesures mais d’un modèle physique fiable que les PINN offrent souvent leur plus grande valeur.
11.6.1. Extensions possibles#
Cette structure s’étend facilement à des problèmes plus complexes :
Systèmes d’ODE (Lotka-Volterra, pendule, Lorenz, etc.) en faisant sortir plusieurs composantes du réseau.
Équations différentielles d’ordre supérieur, en calculant des dérivées successives avec
torch.autograd.grad.Équations aux dérivées partielles (PDE) comme l’équation de la chaleur, de Burgers ou de Navier-Stokes, en utilisant plusieurs variables d’entrée (
x,t, etc.) et les dérivées partielles correspondantes.PINN inverses, où certains paramètres physiques de l’ODE sont également appris par le réseau en les déclarant comme paramètres optimisables (
torch.nn.Parameter).