10. Introduction à PyTorch#
Marc BUFFAT , dpt mécanique, Université Lyon 1

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
10.1. Introduction#
PyTorch est une bibliothèque logicielle Python Open Source d’apprentissage automatique https://pytorch.org qui s’appuie sur Torch développée par Meta (Facebook).
Tout comme Numpy, PyTorch est une bibliothèque python qui permet de faire du calcul numérique optimisé avec des tableaux multidimensionnels (tensor sous Pytporch, ndarray sous Numpy).
NumPy est la base du calcul numérique en Python, tandis que PyTorch s’appuie sur des concepts similaires (les tenseurs) mais ajoute des fonctionnalités essentielles pour l’intelligence artificielle, comme l’accélération GPU et le calcul automatique des gradients.
La syntaxe de PyTorch est similaire à celle de Numpy.
ATTENTION par defaut pytorch utilise des réels en simple précision (float32) pour des questions de mémoire, alors que numpy utilise des réels en double pression (float64).
Il faut donc faire attention lors des conversions numpyvers pytorch
10.2. Vérification de l’installation#
La principale bibliothèque python s’appelle torch (et pour des applications en imagerie torchvision). Pour l’utilisation sur GPU, elle nécessite l’installation de la bibliothèque cuda (utilisation de GPU nvidia pour faire du calcul scientifique HPC).
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
10.3. Objets de base: les tenseurs#
Les tenseurs sont les objets fondamentaux de PyTorch.
Création d’un tenseur (tensor):
Vecteur
import torch
x = torch.tensor([1.,2.,3.])
print(x)
Matrice :
A = torch.tensor([[1.,2.],
[3.,4.]])
Vecteur aléatoire :
x = torch.randn(5)
Matrice aléatoire :
A = torch.randn(3,4)
import torch
x = torch.tensor([1.,2.,3.])
print(x)
A = torch.tensor([[1.,2.],
[3.,4.]])
print(A)
tensor([1., 2., 3.])
tensor([[1., 2.],
[3., 4.]])
10.4. Opérations matricielles#
Utilisation de notations vectorielles similaires à Numpy
x = torch.tensor([1.,2.,3.])
y = 2*x + 1
print(y)
Produit tensoriel (matriciel) @ :
pytorch.dot
pytorch.tensordot
A = torch.randn(3,3)
B = torch.randn(3,3)
C = A @ B
x = torch.tensor([1.,2.,3.])
y = 2*x + 1
print(y)
C = x@y
print(C)
tensor([3., 5., 7.])
tensor(34.)
10.5. Calcul automatique des dérivées#
C’est la fonctionnalité indispensable pour les Réseaux de Neurones. Elle permet de calculer numériquement la valeur de la dérivée grâce à une analyse de graphe avec retour arrière (backtracking)
On active le calcul des gradients par rapport à une variable lors de la création du tenseur associé.
x = torch.tensor([2.0], requires_grad=True)
y = x**2
y.backward()
print(x.grad)
x = torch.tensor([2.0], requires_grad=True)
y = x**2
y.backward()
print(x.grad)
tensor([4.])
10.6. Exemple plus complexe#
x = torch.tensor([3.0], requires_grad=True)
f = x**3 + 2*x
f.backward()
print(x.grad)
PyTorch calcule la valeur numérique du gradient pour \(x=3\)
x = torch.tensor([3.0], requires_grad=True)
f = x**3 + 2*x
f.backward()
print(x.grad)
tensor([29.])
10.7. Les réseaux de neurones#
Un réseau de neurones est une classe héritée de nn.Module. On doit définir deux méthodes:
__init__(self): pour définir la structure du réseau, par exemple un réseau multi-couche linéaire avec des fonctions d’activation en tangente hyperbolique (” tanh`).forward(self,x): pour définir la façon de calculer la valeur du réseau de neurones pour une entréex. Dans le cas d’un réseau séquentiel, on appelle successivement chaque couche, ce qui si se simplifie si on utilise la fonctionSequentialet la méthode associée dansforward
Un réseau de neurones dépend d’un certain nombre de paramètres, que l’on va déterminer en minimisant une fonction coût par une méthode d’optimisation par gradient.
10.7.1. Création#
définition du réseau:
import torch.nn as nn
class NeuralNet(nn.Module):
def __init__(self):
super().__init__()
self.layers = nn.Sequential(
nn.Linear(1,20),
nn.Tanh(),
nn.Linear(20,20),
nn.Tanh(),
nn.Linear(20,1)
)
def forward(self,x):
return self.layers(x)
qui équivaut à
class NeuralNet(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(1,20),
self.ac1 = nn.Tanh(),
self.fc2 = nn.Linear(20,20),
self.ac2 = nn.Tanh(),
self.fc3 = nn.Linear(20,1)
def forward(self,x):
x = self.fc1(x)
x = self.ac1(x)
x = self.fc2(x)
x = self.ac2(x)
x = self.fc3(x)
return x
création/utilisation :
model = NeuralNet()
import torch.nn as nn
class NeuralNet(nn.Module):
def __init__(self):
super().__init__()
self.layers = nn.Sequential(
nn.Linear(1,20),
nn.Tanh(),
nn.Linear(20,20),
nn.Tanh(),
nn.Linear(20,1)
)
def forward(self,x):
return self.layers(x)
# création
model = NeuralNet()
10.7.2. Prédiction#
Pour calculer la valeur du réseau de Neurones, on utilise simplement la valeur de l’instance de la classe en fournissant la valeur du tenseur d’entrée
x = torch.tensor([[0.5]])
y = model(x)
print(y)
x = torch.tensor([[0.5]])
y = model(x)
print(y)
tensor([[0.0543]], grad_fn=<AddmmBackward0>)
10.7.3. Fonction coût (ou fonction perte)#
C’est l’écart entre la valeur attendue et la valeur du réseau de neurones: i.e. entre les données et la prédiction. On calcule en générale un écart quadratique fournit par la fonction MSELoss()
prediction = model(x)
target = torch.tensor([[1.5]])
loss = ((prediction-target)**2).mean()
ou
criterion = nn.MSELoss()
loss = criterion(prediction,target)
prediction = model(x)
target = torch.tensor([[1.5]])
criterion = nn.MSELoss()
loss = criterion(prediction,target)
print(loss)
tensor(2.0899, grad_fn=<MseLossBackward0>)
10.7.4. Méthode d’optimisation#
Choix de la méthode d’optimisation. Un algorithme très utilisé est l’algorithme d’Adam. Adam est un algorithme d’optimisation du premier ordre fondé sur le gradient, destiné à l’optimisation de fonctions objectif stochastiques. Il repose sur des estimations adaptatives des moments d’ordre 1 et 2 à partir d’une évaluation stochastique du gradient. C’est l’itération élémentaire d’un algorithme de descente par gradient stochastique (SGD).
Création de l’optimiseur :
optimizer = torch.optim.Adam(
model.parameters(),
lr=1e-3
)
optimizer = torch.optim.Adam(
model.parameters(),
lr=1e-3
)
10.7.5. Boucle d’entraînement#
C’est la boucle itérative de l’algorithme de descente par gradient stochastique. La variable d’itération est communément appelée epoch pour indiquer que le calcul du gradient se fait en utilisant l’ensemble des données.
Pour certains très gros problèmes, on peut inclure à l’intérieur de cette boucle, une boucle interne de sous-itérations pour ne calculer les gradients que sur un sous-ensemble des données (mini-batch).
for epoch in range(1000):
optimizer.zero_grad()
prediction = model(x)
loss = criterion(prediction,target)
loss.backward()
optimizer.step()
Les trois étapes importantes sont :
calcul de la fonction perte (
loss) ;calcul des gradients (
backward()) ;mise à jour des paramètres ou poids (
step()).
for epoch in range(1000):
optimizer.zero_grad()
prediction = model(x)
loss = criterion(prediction,target)
loss.backward()
optimizer.step()
10.8. Calcul du gradient#
10.8.1. Dérivée première par rapport aux entrées#
Pour les PINN, on ne dérive pas par rapport aux poids mais par rapport aux entrées: i.e. temps ou à l’espace.
Le calcul du gradient :
t = torch.linspace(0,1,100).reshape(-1,1)
t.requires_grad = True
y = model(t)
dy = torch.autograd.grad(
y,
t,
grad_outputs=torch.ones_like(y),
create_graph=True
)[0]
Cette instruction calcule la valeur numérique \(\frac{dy}{dt}.\) pour les valeurs des entrées t.
10.8.2. Dérivée seconde#
d2y = torch.autograd.grad(
dy,
t,
grad_outputs=torch.ones_like(dy),
create_graph=True
)[0]
On obtient \(\frac{d^2y}{dt^2}.\)
C’est cette capacité à calculer des dérivées d’ordre élevé qui rend PyTorch particulièrement adapté aux PINN.
10.9. Sauvegarder/ charger un modèle#
Python permet de sauvegarder le modèle RNN dans un fichier d’extension .pt du type zip-file.
torch.save(model.state_dict(),"model.pt")
Chargement :
model = NeuralNet()
model.load_state_dict(torch.load("model.pt"))
model.eval()
10.10. Utilisation du GPU#
L’un des points forts de PyTorch est la possibilité d’utiliser un GPU nvidia pour faire les calculs tensoriels de façon optimisé. Pour cela, on sélectionne le périphérique (device) à utiliser our faire les calculs (“cuda” pour un GPU nvidia ou “CPU” pour un calcul sans GPU).
Attention si les tenseurs sont créés sur le GPU, il faut explicitement les transférer dans la mémoire du CPU pour utiliser Numpy et les bibliothèques d’analyse et de tracé. Attention le transfert de données entre GPU et CPU n’est pas instantané et peut devenir un point d’étranglement des algorithmes de machine learning si on ne les minimise pas.
device = torch.device(
"cuda" if torch.cuda.is_available()
else "cpu"
)
model = model.to(device)
x = x.to(device)
Sinon, on peut dans la fonction Pytorch de création d’un tenseur ajouter le paramètre device pour spécifier le device (cpu ou gpu)
x = torch.zeros([5,10],device=torch.device("cuda:0")
Pour transférer des données du GPU au CPU on utilise la méthode cpu()
xc = x.cpu()
10.11. Schéma général d’un projet PyTorch pour du machine learning/PINN#
Création des données
│
▼
Création du réseau
│
▼
Choix de la fonction de coût
│
▼
Choix de l'optimiseur
│
▼
Boucle d'entraînement
├── calcul de la valeur: forward()
├── calcul de la fonction coût/perte: loss
├── calcul du gradient: backward()
└── itération suivante
│
▼
Évaluation de l'approximation RNN ou PINN
10.11.1. Approche recommandée#
Pour apprendre à utiliser efficacement PyTorch en machine learning / PINN, vous pouvez suivre cette démarche :
Apprendre à manipuler les tenseurs et les opérations de base.
Comprendre le calcul automatique des gradients (
autograd).Construire des réseaux de neurones simples avec
nn.Module.Maîtriser la boucle d’entraînement (perte, rétropropagation, optimiseur).
Calculer des dérivées par rapport aux entrées avec
torch.autograd.grad.Implémenter un premier PINN pour une ODE du premier ordre, puis étendre à des ODE du second ordre et enfin à des PDE.
Une fois ces bases acquises, la transition vers les PINN est naturelle, car ils utilisent les mêmes briques de PyTorch : réseau de neurones, différentiation automatique et optimisation. La principale différence réside dans la fonction de perte, qui intègre le résidu des équations différentielles et les conditions initiales ou aux limites.