\( \newcommand{\R}{\mathbb{R}} \newcommand{\Rp}{\mathbb{R_+}} \newcommand{\Z}{\mathbb{Z}} \newcommand{\Rn}{\mathbb{R}^n} \newcommand{\N}{\mathbb{N}} \renewcommand{\to}{\rightarrow} \newcommand{\from}{\colon} \renewcommand{\Vec}{\mathsf{vec}} \newcommand{\Ker}{\mathrm{Ker}} \renewcommand{\Im}{\mathrm{Im}} \newcommand{\rang}{\mathrm{rang}} \newcommand{\O}{\mathcal{O}} \newcommand{\C}{\mathcal{C}} \newcommand{\U}{\mathcal{U}} \newcommand{\x}{\mathbf{x}} \newcommand{\X}{\mathbf{X}} \newcommand{\z}{\mathbf{z}} \newcommand{\w}{\mathbf{w}} \newcommand{\W}{\mathbf{W}} \newcommand{\a}{\mathbf{a}} \newcommand{\b}{\mathbf{b}} \newcommand{\B}{\mathbf{B}} \newcommand{\dpartial}[2]{{\partial #1 \over \partial #2}} \newcommand{\d}{\mathbf{d}} \)

PyTorch

Programmation pour l’IA

Florent Capelli

Université d’Artois, CRIL

Pytorch: rappels

Tenseurs

import torch

t=torch.tensor([[1.,2.,3.], 
    [4.,5.,6.], 
    [0.,0.,1.]])
u = torch.sum(t*t)
print(u)

>>> tensor(92.)
  • Tableaux multidimensionnels
  • Similaire aux tableaux numpy
  • Principales différences :
    • optimisés pour plusieurs architectures
    • supportent autograd

Autograd

Permet de calculer automatiquement des gradients, symboliquement :

import torch

t=torch.tensor([[1.,2.,3.], [4.,5.,6.], [0.,0.,1.]], requires_grad=True)
u = torch.sum(t*t)
print(u)
u.backward()
print(t.grad)

>>> tensor(92., grad_fn=<SumBackward0>)
>>> tensor([[ 2.,  4.,  6.],
>>>         [ 8., 10., 12.],
>>>         [ 0.,  0.,  2.]])

Une simple régression linéaire

x = torch.empty(100).uniform_(-20,20)
y = 3*x+2+torch.normal(mean=0., std=3., size=(100,))

a = torch.tensor([0.], requires_grad=True)
b = torch.tensor([0.], requires_grad=True)

for e in range(1000):
    loss = torch.mean((a*x+b-y)**2)
    loss.backward()
    with torch.no_grad():
        a -= (1e-3)*a.grad
        b -= (1e-3)*b.grad
    a.grad_zero_()
    b.grad_zero_()
  • On définit a et b comme des tenseurs avec information de gradient
  • Donc loss, en le calculant, maintient une trace du calcul.
  • loss.backward() propage le gradient dans l’arbre de calcul.

Réseaux de neurones

import torch
from torch import nn

m = nn.Sequential(nn.Linear(2,3), nn.ReLU(), 
    nn.Linear(3,1), nn.Sigmoid())
print(m.parameters())
[Parameter containing:
tensor([[ 0.4524, -0.0160],
        [ 0.5088,  0.5218],
        [ 0.3939,  0.5888]], requires_grad=True), Parameter containing:
tensor([-0.1352, -0.0697,  0.5479], requires_grad=True), Parameter containing:
tensor([[ 0.4776, -0.1121,  0.0311]], requires_grad=True), Parameter containing:
tensor([-0.0203], requires_grad=True)]

Optimizers

from torch import optim

Selon la documentation :

torch.optim is a package implementing various optimization algorithms.

Most commonly used methods are already supported, and the interface is general enough, so that more sophisticated ones can also be easily integrated in the future.

Aperçu

Adadelta

Implements Adadelta algorithm.

Adafactor

Implements Adafactor algorithm.

Adagrad

Implements Adagrad algorithm.

Adam

Implements Adam algorithm.

AdamW

Implements AdamW algorithm, where weight decay does not accumulate in the momentum nor variance.

SparseAdam

SparseAdam implements a masked version of the Adam algorithm suitable for sparse gradients.

Adamax

Implements Adamax algorithm (a variant of Adam based on infinity norm).

ASGD

Implements Averaged Stochastic Gradient Descent.

LBFGS

Implements L-BFGS algorithm.

Muon

Implements Muon algorithm.

NAdam

Implements NAdam algorithm.

RAdam

Implements RAdam algorithm.

RMSprop

Implements RMSprop algorithm.

Rprop

Implements the resilient backpropagation algorithm.

SGD

Implements stochastic gradient descent (optionally with momentum).

Exemple avec optim.SGD()

import torch
from torch import optim
from torch import nn

m = nn.Sequential(nn.Linear(2,3), nn.ReLU(), 
    nn.Linear(3,1), nn.Sigmoid())

o = optim.SGD(m.parameters(), lr=0.01)

for input, target in dataset:
    o.zero_grad()
    output = m(input)
    loss = loss_fn(output, target)
    loss.backward()
    o.step()
  • Calcul de l’erreur et de son gradient toujours explicite
  • o.step() met à jour les paramètres en utilisant les spécificités de l’optimizer choisi.

Minibatch et optim.SGD()

import torch
from torch import optim
from torch import nn

m = nn.Sequential(nn.Linear(2,3), nn.ReLU(), 
    nn.Linear(3,1), nn.Sigmoid())

o = optim.SGD(m.parameters(), lr=0.01)

# /!\ la fonction minibatch n'existe pas !
# /!\ C'est juste pour l'exemple

for b in minibatch(dataset, 10): 
    o.zero_grad()
    for input, target in b:
        output = m(input)
        loss = loss_fn(output, target)
        loss.backward()
    o.step()
  • Gradient réinitialisé seulement au début de chaque batch.
  • loss.backward() accumule les gradients pour chaque exemple dans b.
  • On ne met à jour les paramètres (o.step()) qu’après avoir regardé tous les exemples du minibatch.

Adam, un autre optimizer

import torch
from torch import optim
from torch import nn

m = nn.Sequential(nn.Linear(2,3), nn.ReLU(), 
    nn.Linear(3,1), nn.Sigmoid())

o = optim.Adam(m.parameters(), lr=0.01)

for input, target in dataset:
    o.zero_grad()
    output = m(input)
    loss = loss_fn(output, target)
    loss.backward()
    o.step()
  • Adam (Adaptive Moment Estimation) utilise des moments d’ordre 2 pour choisir un pas de gradient adaptatif.
  • Rien d’autre à changer dans le code que la définition de o.

Fonctions d’erreurs

Pour que le calcul de gradient fonctionne sur la fonction loss_fn, il faut qu’elle soit compatible avec la dérivation automatique de Pytorch.

loss = nn.MSELoss()
input = torch.randn(3, 5, requires_grad=True)
target = torch.randn(3, 5)
output = loss(input, target)
output.backward()
loss = nn.MSELoss(reduction="sum")
input = torch.randn(3, 5, requires_grad=True)
target = torch.randn(3, 5)
output = loss(input, target)
output.backward()

Chargements des données

Classe DataSet

Utilisation de la classe abstraite DataSet. Trois méthodes à définir :

  • __init__(self, ...) : constructeur ;
  • __len__(self) : retourne le nombre d’exemples dans le jeu de données.
  • __getitem__(self, idx) : retourne X,y où X est l’exemple d’index idx et y est sa classe.

Exemple éternel iris.csv

import pandas as pd
from torch.utils.data import Dataset


class DatasetIris(Dataset):
    def __init__(self):
        df = pd.read_csv("iris.csv")
        self.data = torch.from_numpy(df.to_numpy()[:, :4].astype(float))
        self.target = torch.from_numpy(
            df["variety"].replace({"Virginica": 0, "Versicolor": 1, "Setosa": 2})
            .to_numpy(dtype=int))
    def __len__(self):
        return len(self.data)
    def __getitem__(self, idx):
        return self.data[idx], self.target[idx]

d = DatasetIris()
print(d[0:4])
print(len(d))

DataLoader

Une fois qu’on a un DataSet, on peut y accéder avec DataLoader :

from torch.utils.data import DataLoader

training_data = DatasetIris() 
train_dataloader = DataLoader(training_data, batch_size=64, shuffle=True)

for X,y in train_dataloader:
    print(X.shape)
    print(y.shape)
    break
>>> torch.Size([64, 4])
>>> torch.Size([64])

Avantages de Dataset/DataLoader

Utilisation du GPU

Détection

if torch.cuda.is_available():
    print(f"GPU: {torch.cuda.get_device_name(0)} is available.")
else:
    print("No GPU available. Training will run on CPU.")

Méthode .to(device)

Envoie la structure de données (et les calculs sous-jacents) dans le GPU :

t = torch.tensor([[1,2,3], [4,5,6]])
t.to("cuda") 

myNN = nn.Sequential(
            nn.Linear(28*28, 512),
            nn.ReLU(),
            nn.Linear(512, 512),
            nn.ReLU(),
            nn.Linear(512, 10),
        )
myNN.to("cuda")

Exemple

Modèle

nn.Sequential( nn.Linear(4,8), nn.ReLU(), nn.Linear(8,3), nn.Softmax(dim=0) )

  • Entrée : un vecteur de taille \(4\) (sepal.width, sepal.length, petal.width, petal.length)
  • Multiplication (à gauche) par une matrice \(8 \times 4\).
  • Sortie: un vecteur de taille \(8\).
  • Entrée : un vecteur de taille \(8\).
  • Une couche ReLU.
  • Sortie : un vecteur de taille \(8\).
  • Entrée : un vecteur de taille \(8\).
  • Multiplication (à gauche) par une matrice \(3 \times 8\).
  • Sortie : un vecteur de taille \(3\).
  • Entrée : un vecteur de taille \(3\).

\[Softmax([x_1,x_2,x_3]) = [{e^{x_1} \over S}, {e^{x_2} \over S}, {e^{x_3} \over S}]\] où \(S = e^{x_1}+e^{x_2}+e^{x_3}\).

Intuition: on représente la probabilité d’être dans la classe \(i \in \{0,1,2\}\) comme \(e^{x_i} \over S\).

  • Sortie : un vecteur de taille \(3\).

Au final :

  • Entrée : un vecteur de taille \(4\)
    • Les dimensions de la fleur
  • Sortie : un vecteur de taille \(3\).
    • Pour chaque classe \(i \in \{0,1,2\}\), la probabilité que le vecteur ait la classe \(i\).

Encodage des classes

\[\begin{align} Pr(classe(X) = 0) = 1 \\ Pr(classe(X) = 1) = 0 \\ Pr(classe(X) = 2) = 0 \end{align} \]

Autres détails

Voir le code