Pour installer Pytorch, suivez les instructions sur le site officiel:
pip install torch torchvision
Nous visons à élargir progressivement cette série en ajoutant de nouveaux articles et en maintenant le contenu à jour avec les dernières versions d'API Pytorch. Si vous avez des suggestions sur la façon d'améliorer cette série ou de trouver les explications ambiguës, n'hésitez pas à créer un problème, à envoyer des correctifs ou à contacter par e-mail.
Pytorch est l'une des bibliothèques les plus populaires pour le calcul numérique et est actuellement parmi les bibliothèques les plus utilisées pour effectuer la recherche sur l'apprentissage automatique. À bien des égards, Pytorch est similaire à Numpy, avec l'avantage supplémentaire que Pytorch vous permet d'effectuer vos calculs sur les CPU, les GPU et les TPU sans aucun changement de matériel à votre code. Pytorch facilite également la distribution de votre calcul sur plusieurs appareils ou machines. L'une des caractéristiques les plus importantes de Pytorch est la différenciation automatique. Il permet de calculer les gradients de vos fonctions analytiquement de manière efficace qui est cruciale pour la formation de modèles d'apprentissage automatique en utilisant la méthode de descente de gradient. Notre objectif ici est de fournir une introduction douce à Pytorch et de discuter des meilleures pratiques pour l'utilisation de Pytorch.
La première chose à apprendre sur Pytorch est le concept de tenseurs. Les tenseurs sont simplement des tableaux multidimensionnels. Un tenseur de pytorche est très similaire à un tableau numpy avec certains magique fonctionnalité supplémentaire.
Un tenseur peut stocker une valeur scalaire:
import torch
a = torch . tensor ( 3 )
print ( a ) # tensor(3)ou un tableau:
b = torch . tensor ([ 1 , 2 ])
print ( b ) # tensor([1, 2])Une matrice:
c = torch . zeros ([ 2 , 2 ])
print ( c ) # tensor([[0., 0.], [0., 0.]])ou tout tenseur dimensionnel arbitraire:
d = torch . rand ([ 2 , 2 , 2 ])Les tenseurs peuvent être utilisés pour effectuer efficacement les opérations algébriques. L'une des opérations les plus couramment utilisées dans les applications d'apprentissage automatique est la multiplication matricielle. Dites que vous souhaitez multiplier deux matrices aléatoires de taille 3x5 et 5x4, cela peut être fait avec l'opération de multiplication de la matrice (@):
import torch
x = torch . randn ([ 3 , 5 ])
y = torch . randn ([ 5 , 4 ])
z = x @ y
print ( z )De même, pour ajouter deux vecteurs, vous pouvez faire:
z = x + yPour convertir un tenseur en un tableau Numpy, vous pouvez appeler la méthode Numpy () de Tensor:
print ( z . numpy ())Et vous pouvez toujours convertir un tableau Numpy en tenseur par:
x = torch . tensor ( np . random . normal ([ 3 , 5 ]))L'avantage le plus important de Pytorch sur Numpy est sa fonctionnalité de différenciation automatique qui est très utile dans les applications d'optimisation telles que l'optimisation des paramètres d'un réseau neuronal. Essayons de le comprendre avec un exemple.
Dites que vous avez une fonction composite qui est une chaîne de deux fonctions: g(u(x)) . Pour calculer la dérivée de g par rapport à x nous pouvons utiliser la règle de chaîne qui indique que: dg/dx = dg/du * du/dx . Pytorch peut calculer analytiquement les dérivés pour nous.
Pour calculer les dérivés dans Pytorch en premier, nous créons un tenseur et définissons son requires_grad sur true. Nous pouvons utiliser les opérations du tenseur pour définir nos fonctions. Nous supposons que u est une fonction quadratique et g est une fonction linéaire simple:
x = torch . tensor ( 1.0 , requires_grad = True )
def u ( x ):
return x * x
def g ( u ):
return - u Dans ce cas, notre fonction composite est g(u(x)) = -x*x . Donc sa dérivée par rapport à x est -2x . Au point x=1 , c'est égal à -2 .
Vérifions cela. Cela peut être fait en utilisant la fonction Grad dans Pytorch:
dgdx = torch . autograd . grad ( g ( u ( x )), x )[ 0 ]
print ( dgdx ) # tensor(-2.) Pour comprendre à quel point la différenciation automatique peut être puissante, jetons un coup d'œil à un autre exemple. Supposons que nous avons des échantillons d'une courbe (disons f(x) = 5x^2 + 3 ) et nous voulons estimer f(x) sur la base de ces échantillons. Nous définissons une fonction paramétrique g(x, w) = w0 x^2 + w1 x + w2 , qui est une fonction de l'entrée x et des paramètres latents w , notre objectif est alors de trouver les paramètres latents tels que g(x, w) ≈ f(x) . Cela peut être fait en minimisant la fonction de perte suivante: L(w) = Σ (f(x) - g(x, w))^2 . Bien qu'il existe une solution de formulaire fermé pour ce problème simple, nous choisissons d'utiliser une approche plus générale qui peut être appliquée à n'importe quelle fonction de différenciation arbitraire, et qui utilise une descente de gradient stochastique. Nous calculons simplement le gradient moyen de L(w) par rapport à w sur un ensemble de points d'échantillonnage et nous déplacez dans la direction opposée.
Voici comment cela peut être fait à Pytorch:
import numpy as np
import torch
# Assuming we know that the desired function is a polynomial of 2nd degree, we
# allocate a vector of size 3 to hold the coefficients and initialize it with
# random noise.
w = torch . tensor ( torch . randn ([ 3 , 1 ]), requires_grad = True )
# We use the Adam optimizer with learning rate set to 0.1 to minimize the loss.
opt = torch . optim . Adam ([ w ], 0.1 )
def model ( x ):
# We define yhat to be our estimate of y.
f = torch . stack ([ x * x , x , torch . ones_like ( x )], 1 )
yhat = torch . squeeze ( f @ w , 1 )
return yhat
def compute_loss ( y , yhat ):
# The loss is defined to be the mean squared error distance between our
# estimate of y and its true value.
loss = torch . nn . functional . mse_loss ( yhat , y )
return loss
def generate_data ():
# Generate some training data based on the true function
x = torch . rand ( 100 ) * 20 - 10
y = 5 * x * x + 3
return x , y
def train_step ():
x , y = generate_data ()
yhat = model ( x )
loss = compute_loss ( y , yhat )
opt . zero_grad ()
loss . backward ()
opt . step ()
for _ in range ( 1000 ):
train_step ()
print ( w . detach (). numpy ())En exécutant ce morceau de code, vous devriez voir un résultat à proximité de ceci:
[ 4.9924135 , 0.00040895029 , 3.4504161 ]Qui est une approximation relativement proche de nos paramètres.
Ce n'est que la pointe de l'iceberg pour ce que Pytorch peut faire. De nombreux problèmes tels que l'optimisation de grands réseaux de neurones avec des millions de paramètres peuvent être mis en œuvre efficacement dans Pytorch en quelques lignes de code. Pytorch s'occupe de l'échelle sur plusieurs appareils et threads, et prend en charge une variété de plates-formes.
Dans l'exemple précédent, nous avons utilisé des tenseurs osseux nus et des opérations de tenseur pour construire notre modèle. Pour rendre votre code légèrement plus organisé, il est recommandé d'utiliser les modules de Pytorch. Un module est simplement un conteneur pour vos paramètres et résume les opérations du modèle. Par exemple, dites que vous souhaitez représenter un modèle linéaire y = ax + b . Ce modèle peut être représenté avec le code suivant:
import torch
class Net ( torch . nn . Module ):
def __init__ ( self ):
super (). __init__ ()
self . a = torch . nn . Parameter ( torch . rand ( 1 ))
self . b = torch . nn . Parameter ( torch . rand ( 1 ))
def forward ( self , x ):
yhat = self . a * x + self . b
return yhatPour utiliser ce modèle dans la pratique, vous instanciez le module et appelez-le simplement comme une fonction:
x = torch . arange ( 100 , dtype = torch . float32 )
net = Net ()
y = net ( x ) Les paramètres sont essentiellement des tenseurs avec requires_grad set sur true. Il est pratique d'utiliser des paramètres car vous pouvez simplement les récupérer tous avec la méthode parameters() du module:
for p in net . parameters ():
print ( p ) Maintenant, dites que vous avez une fonction inconnue y = 5x + 3 + some noise , et vous souhaitez optimiser les paramètres de votre modèle pour s'adapter à cette fonction. Vous pouvez commencer par échantillonner certains points de votre fonction:
x = torch . arange ( 100 , dtype = torch . float32 ) / 100
y = 5 * x + 3 + torch . rand ( 100 ) * 0.3Semblable à l'exemple précédent, vous pouvez définir une fonction de perte et optimiser les paramètres de votre modèle comme suit:
criterion = torch . nn . MSELoss ()
optimizer = torch . optim . SGD ( net . parameters (), lr = 0.01 )
for i in range ( 10000 ):
net . zero_grad ()
yhat = net ( x )
loss = criterion ( yhat , y )
loss . backward ()
optimizer . step ()
print ( net . a , net . b ) # Should be close to 5 and 3 Pytorch est livré avec un certain nombre de modules prédéfinis. Un tel module est torch.nn.Linear qui est une forme plus générale d'une fonction linéaire que ce que nous avons défini ci-dessus. Nous pouvons réécrire notre module ci-dessus en utilisant torch.nn.Linear comme ceci:
class Net ( torch . nn . Module ):
def __init__ ( self ):
super (). __init__ ()
self . linear = torch . nn . Linear ( 1 , 1 )
def forward ( self , x ):
yhat = self . linear ( x . unsqueeze ( 1 )). squeeze ( 1 )
return yhat Notez que nous avons utilisé la compression et la non-éventualité puisque torch.nn.Linear fonctionne sur un lot de vecteurs par opposition aux scalaires.
Par défaut, les paramètres d'appel () sur un module renverront les paramètres de tous ses sous-modules:
net = Net ()
for p in net . parameters ():
print ( p ) Il existe des modules prédéfinis qui agissent comme un conteneur pour d'autres modules. Le module de conteneur le plus couramment utilisé est torch.nn.Sequential . Comme son nom l'indique, il est utilisé pour empiler plusieurs modules (ou couches) les uns sur les autres. Par exemple, pour empiler deux couches linéaires avec une non-linéarité ReLU entre les deux, vous pouvez faire:
model = torch . nn . Sequential (
torch . nn . Linear ( 64 , 32 ),
torch . nn . ReLU (),
torch . nn . Linear ( 32 , 10 ),
) Pytorch prend en charge la diffusion des opérations élémentaires. Normalement, lorsque vous souhaitez effectuer des opérations comme l'addition et la multiplication, vous devez vous assurer que les formes des opérandes correspondent, par exemple, vous ne pouvez pas ajouter un tenseur de forme [3, 2] à un tenseur de forme [3, 4] . Mais il y a un cas spécial et c'est à ce moment que vous avez une dimension singulière. Pytorch tuile implicitement le tenseur à travers ses dimensions singulières pour correspondre à la forme de l'autre opérande. Il est donc valable d'ajouter un tenseur de forme [3, 2] à un tenseur de forme [3, 1] .
import torch
a = torch . tensor ([[ 1. , 2. ], [ 3. , 4. ]])
b = torch . tensor ([[ 1. ], [ 2. ]])
# c = a + b.repeat([1, 2])
c = a + b
print ( c )La radiodiffusion nous permet d'effectuer un carrelage implicite, ce qui rend le code plus court et plus efficace par la mémoire, car nous n'avons pas besoin de stocker le résultat de l'opération de carrelage. Un endroit intéressant que cela peut être utilisé est lors de la combinaison des caractéristiques de longueur variable. Afin de concaténer les caractéristiques de la longueur variable, nous moussons généralement les tenseurs d'entrée, concatenons le résultat et appliquons une certaine non-linéarité. Il s'agit d'un modèle commun dans une variété d'architectures de réseau neuronal:
a = torch . rand ([ 5 , 3 , 5 ])
b = torch . rand ([ 5 , 1 , 6 ])
linear = torch . nn . Linear ( 11 , 10 )
# concat a and b and apply nonlinearity
tiled_b = b . repeat ([ 1 , 3 , 1 ])
c = torch . cat ([ a , tiled_b ], 2 )
d = torch . nn . functional . relu ( linear ( c ))
print ( d . shape ) # torch.Size([5, 3, 10]) Mais cela peut être fait plus efficacement avec la diffusion. Nous utilisons le fait que f(m(x + y)) est égal à f(mx + my) . Nous pouvons donc faire les opérations linéaires séparément et utiliser la diffusion pour faire une concaténation implicite:
a = torch . rand ([ 5 , 3 , 5 ])
b = torch . rand ([ 5 , 1 , 6 ])
linear1 = torch . nn . Linear ( 5 , 10 )
linear2 = torch . nn . Linear ( 6 , 10 )
pa = linear1 ( a )
pb = linear2 ( b )
d = torch . nn . functional . relu ( pa + pb )
print ( d . shape ) # torch.Size([5, 3, 10])En fait, ce morceau de code est assez général et peut être appliqué aux tenseurs de forme arbitraire tant que la diffusion entre les tenseurs est possible:
class Merge ( torch . nn . Module ):
def __init__ ( self , in_features1 , in_features2 , out_features , activation = None ):
super (). __init__ ()
self . linear1 = torch . nn . Linear ( in_features1 , out_features )
self . linear2 = torch . nn . Linear ( in_features2 , out_features )
self . activation = activation
def forward ( self , a , b ):
pa = self . linear1 ( a )
pb = self . linear2 ( b )
c = pa + pb
if self . activation is not None :
c = self . activation ( c )
return cJusqu'à présent, nous avons discuté de la bonne partie de la diffusion. Mais quelle est la partie laide que vous pourriez demander? Les hypothèses implicites rendent presque toujours le débogage plus difficile à faire. Considérez l'exemple suivant:
a = torch . tensor ([[ 1. ], [ 2. ]])
b = torch . tensor ([ 1. , 2. ])
c = torch . sum ( a + b )
print ( c ) Selon vous, quelle serait la valeur de c après l'évaluation? Si vous avez deviné 6, c'est mal. Cela va être 12. En effet, lorsque le rang de deux tenseurs ne correspond pas, Pytorch étend automatiquement la première dimension du tenseur avec un rang inférieur avant le fonctionnement élémentaire, de sorte que le résultat de l'ajout serait [[2, 3], [3, 4]] , et la réduction de tous les paramètres nous donnerait 12.
La façon d'éviter ce problème est d'être aussi explicite que possible. Si nous avions spécifié la dimension que nous voudrions réduire, la capture de ce bug aurait été beaucoup plus facile:
a = torch . tensor ([[ 1. ], [ 2. ]])
b = torch . tensor ([ 1. , 2. ])
c = torch . sum ( a + b , 0 )
print ( c ) Ici, la valeur de c serait [5, 7] , et nous devions immédiatement en fonction de la forme du résultat qu'il y a quelque chose qui ne va pas. Une règle générale consiste à toujours spécifier les dimensions des opérations de réduction et lors de l'utilisation torch.squeeze .
Tout comme Numpy, Pytorch surcharge un certain nombre d'opérateurs Python pour rendre le code Pytorch plus court et plus lisible.
L'OP de tranchage est l'un des opérateurs surchargés qui peuvent rendre les tenseurs d'indexation très faciles:
z = x [ begin : end ] # z = torch.narrow(0, begin, end-begin)Soyez très prudent lorsque vous utilisez cet OP. L'OP de tranchage, comme tout autre OP, a des frais généraux. Parce que c'est un OP commun et innocent, cela peut être trop utilisé, ce qui peut conduire à des inefficacités. Pour comprendre à quel point ce PO peut être inefficace, regardons un exemple. Nous voulons effectuer manuellement la réduction entre les rangées d'une matrice:
import torch
import time
x = torch . rand ([ 500 , 10 ])
z = torch . zeros ([ 10 ])
start = time . time ()
for i in range ( 500 ):
z += x [ i ]
print ( "Took %f seconds." % ( time . time () - start )) Cela fonctionne assez lentement et la raison en est que nous appelons la tranche OP 500 fois, ce qui ajoute beaucoup de frais généraux. Un meilleur choix aurait été d'utiliser torch.unbind OP pour trancher la matrice dans une liste de vecteurs à la fois:
z = torch . zeros ([ 10 ])
for x_i in torch . unbind ( x ):
z += x_iC'est significativement (~ 30% sur ma machine) plus rapidement.
Bien sûr, la bonne façon de faire cette simple réduction est d'utiliser torch.sum .
z = torch . sum ( x , dim = 0 )ce qui est extrêmement rapide (~ 100x plus rapide sur ma machine).
Pytorch surcharge également une gamme d'opérateurs arithmétiques et logiques:
z = - x # z = torch.neg(x)
z = x + y # z = torch.add(x, y)
z = x - y
z = x * y # z = torch.mul(x, y)
z = x / y # z = torch.div(x, y)
z = x // y
z = x % y
z = x ** y # z = torch.pow(x, y)
z = x @ y # z = torch.matmul(x, y)
z = x > y
z = x >= y
z = x < y
z = x <= y
z = abs ( x ) # z = torch.abs(x)
z = x & y
z = x | y
z = x ^ y # z = torch.logical_xor(x, y)
z = ~ x # z = torch.logical_not(x)
z = x == y # z = torch.eq(x, y)
z = x != y # z = torch.ne(x, y) Vous pouvez également utiliser la version augmentée de ces opérations. Par exemple, x += y et x **= 2 sont également valides.
Notez que Python n'autorise pas la surcharge and , or , et not les mots clés.
Pytorch est optimisé pour effectuer des opérations sur de grands tenseurs. Faire de nombreuses opérations sur les petits tenseurs est assez inefficace dans le pytorch. Ainsi, dans la mesure du possible, vous devez réécrire vos calculs sous forme par lots pour réduire les frais généraux et améliorer les performances. S'il n'y a aucun moyen que vous puissiez parler manuellement vos opérations, l'utilisation de TorchScript peut améliorer les performances de votre code. TorchScript est simplement un sous-ensemble de fonctions Python qui sont reconnues par Pytorch. Pytorch peut optimiser automatiquement votre code TORCHScript à l'aide de son compilateur Just In Time (JIT) et réduire certains frais généraux.
Regardons un exemple. Une opération très courante dans les applications ML est "Rassemble de lots". Cette opération peut simplement écrire en output[i] = input[i, index[i]] . Cela peut être simplement mis en œuvre dans Pytorch comme suit:
import torch
def batch_gather ( tensor , indices ):
output = []
for i in range ( tensor . size ( 0 )):
output += [ tensor [ i ][ indices [ i ]]]
return torch . stack ( output ) Pour implémenter la même fonction à l'aide de TorchScript, utilisez simplement le décorateur torch.jit.script :
@ torch . jit . script
def batch_gather_jit ( tensor , indices ):
output = []
for i in range ( tensor . size ( 0 )):
output += [ tensor [ i ][ indices [ i ]]]
return torch . stack ( output )Lors de mes tests, cela représente environ 10% plus rapidement.
Mais rien ne vaut manuellement vos opérations. Une implémentation vectorisée dans mes tests est 100 fois plus rapide:
def batch_gather_vec ( tensor , indices ):
shape = list ( tensor . shape )
flat_first = torch . reshape (
tensor , [ shape [ 0 ] * shape [ 1 ]] + shape [ 2 :])
offset = torch . reshape (
torch . arange ( shape [ 0 ]). cuda () * shape [ 1 ],
[ shape [ 0 ]] + [ 1 ] * ( len ( indices . shape ) - 1 ))
output = flat_first [ indices + offset ]
return output Dans la dernière leçon, nous avons parlé de l'écriture de code pytorch efficace. Mais pour faire fonctionner votre code avec une efficacité maximale, vous devez également charger vos données efficacement dans la mémoire de votre appareil. Heureusement, Pytorch offre un outil pour faciliter le chargement des données. Cela s'appelle un DataLoader . Un DataLoader utilise plusieurs travailleurs pour charger simultanément des données à partir d'un Dataset et utilise éventuellement un Sampler pour exempter les entrées de données et former un lot.
Si vous pouvez accéder au hasard à vos données, l'utilisation d'un DataLoader est très facile: vous devez simplement implémenter une classe de Dataset qui implémente __getitem__ (pour lire chaque élément de données) et __len__ (pour retourner le nombre d'éléments dans l'ensemble de données). Par exemple, voici comment charger des images à partir d'un répertoire donné:
import glob
import os
import random
import cv2
import torch
class ImageDirectoryDataset ( torch . utils . data . Dataset ):
def __init__ ( path , pattern ):
self . paths = list ( glob . glob ( os . path . join ( path , pattern )))
def __len__ ( self ):
return len ( self . paths )
def __item__ ( self ):
path = random . choice ( paths )
return cv2 . imread ( path , 1 )Pour charger toutes les images JPEG à partir d'un répertoire donné, vous pouvez ensuite effectuer ce qui suit:
dataloader = torch . utils . data . DataLoader ( ImageDirectoryDataset ( "/data/imagenet/*.jpg" ), num_workers = 8 )
for data in dataloader :
# do something with dataIci, nous utilisons 8 travailleurs pour lire simultanément nos données du disque. Vous pouvez régler le nombre de travailleurs sur votre machine pour des résultats optimaux.
L'utilisation d'un DataLoader pour lire les données avec un accès aléatoire peut être OK si vous avez un stockage rapide ou si vos éléments de données sont grands. Mais imaginez avoir un système de fichiers réseau avec une connexion lente. Demander des fichiers individuels de cette façon peut être extrêmement lent et finirait probablement par devenir le goulot d'étranglement de votre pipeline de formation.
Une meilleure approche consiste à stocker vos données dans un format de fichier contigu qui peut être lu séquentiellement. Par exemple, si vous avez une grande collection d'images, vous pouvez utiliser TAR pour créer une seule archive et extraire des fichiers de l'archive séquentiellement dans Python. Pour ce faire, vous pouvez utiliser IterableDataset de Pytorch. Pour créer une classe IterableDataset , vous n'avez qu'à implémenter une méthode __iter__ qui lit et fournit séquentiellement les éléments de données à partir de l'ensemble de données.
Une implémentation naïve aimerait ceci:
import tarfile
import torch
def tar_image_iterator ( path ):
tar = tarfile . open ( self . path , "r" )
for tar_info in tar :
file = tar . extractfile ( tar_info )
content = file . read ()
yield cv2 . imdecode ( content , 1 )
file . close ()
tar . members = []
tar . close ()
class TarImageDataset ( torch . utils . data . IterableDataset ):
def __init__ ( self , path ):
super (). __init__ ()
self . path = path
def __iter__ ( self ):
yield from tar_image_iterator ( self . path )Mais il y a un problème majeur avec cette implémentation. Si vous essayez d'utiliser DatalOader pour lire à partir de cet ensemble de données avec plus d'un travailleur, vous observez beaucoup d'images dupliquées:
dataloader = torch . utils . data . DataLoader ( TarImageDataset ( "/data/imagenet.tar" ), num_workers = 8 )
for data in dataloader :
# data contains duplicated items Le problème est que chaque travailleur crée une instance distincte de l'ensemble de données et que chacun commencerait depuis le début de l'ensemble de données. Une façon d'éviter cela consiste à avoir un fichier de goudron, à diviser vos données en fichiers TAR num_workers et à les charger avec un travailleur séparé:
class TarImageDataset ( torch . utils . data . IterableDataset ):
def __init__ ( self , paths ):
super (). __init__ ()
self . paths = paths
def __iter__ ( self ):
worker_info = torch . utils . data . get_worker_info ()
# For simplicity we assume num_workers is equal to number of tar files
if worker_info is None or worker_info . num_workers != len ( self . paths ):
raise ValueError ( "Number of workers doesn't match number of files." )
yield from tar_image_iterator ( self . paths [ worker_info . worker_id ])C'est ainsi que notre classe de données peut être utilisée:
dataloader = torch . utils . data . DataLoader (
TarImageDataset ([ "/data/imagenet_part1.tar" , "/data/imagenet_part2.tar" ]), num_workers = 2 )
for data in dataloader :
# do something with dataNous avons discuté d'une stratégie simple pour éviter le problème des entrées en dupliquée. Le package tfrecord utilise des stratégies légèrement plus sophistiquées pour retirer vos données à la volée.
Lorsque vous utilisez une bibliothèque de calcul numérique telle que Numpy ou Pytorch, il est important de noter que l'écriture de code mathématiquement correct ne conduit pas nécessairement à des résultats corrects. Vous devez également vous assurer que les calculs sont stables.
Commençons par un exemple simple. Mathématiquement, il est facile de voir que x * y / y = x pour toute valeur non nulle de x . Mais voyons si c'est toujours vrai dans la pratique:
import numpy as np
x = np . float32 ( 1 )
y = np . float32 ( 1e-50 ) # y would be stored as zero
z = x * y / y
print ( z ) # prints nan La raison du résultat incorrect est que y est tout simplement trop petit pour le type float32. Un problème similaire se produit lorsque y est trop grand:
y = np . float32 ( 1e39 ) # y would be stored as inf
z = x * y / y
print ( z ) # prints nanLa plus petite valeur positive que le type float32 peut représenter est de 1,4013e-45 et tout ce qui serait en dessous serait stocké comme zéro. De plus, n'importe quel nombre au-delà de 3.40282E + 38 serait stocké sous forme d'inf.
print ( np . nextafter ( np . float32 ( 0 ), np . float32 ( 1 ))) # prints 1.4013e-45
print ( np . finfo ( np . float32 ). max ) # print 3.40282e+38Pour vous assurer que vos calculs sont stables, vous souhaitez éviter les valeurs avec une valeur absolue petite ou très grande. Cela peut sembler très évident, mais ce genre de problèmes peut devenir extrêmement difficile à déboguer, en particulier lors de la descente de dégradé à Pytorch. En effet, vous devez non seulement vous assurer que toutes les valeurs de la passe avant se trouvent dans la plage valide de vos types de données, mais que vous devez également vous assurer de la même chose pour la passe arrière (pendant le calcul du gradient).
Regardons un vrai exemple. Nous voulons calculer le softmax sur un vecteur de logits. Une implémentation naïve ressemblerait à ceci:
import torch
def unstable_softmax ( logits ):
exp = torch . exp ( logits )
return exp / torch . sum ( exp )
print ( unstable_softmax ( torch . tensor ([ 1000. , 0. ])). numpy ()) # prints [ nan, 0.] Notez que le calcul de l'exponentiel des logits pour des résultats de nombres relativement petits aux résultats gigantesques qui sont hors de la plage float32. La plus grande logit valide pour notre implémentation naïve Softmax est ln(3.40282e+38) = 88.7 , tout ce qui est au-delà qui mène à un résultat de nan.
Mais comment pouvons-nous rendre cela plus stable? La solution est assez simple. Il est facile de voir que exp(x - c) Σ exp(x - c) = exp(x) / Σ exp(x) . Par conséquent, nous pouvons soustraire toute constante des logits et le résultat resterait le même. Nous choisissons cette constante pour être le maximum de logits. De cette façon, le domaine de la fonction exponentielle serait limité à [-inf, 0] , et par conséquent sa portée serait [0.0, 1.0] ce qui est souhaitable:
import torch
def softmax ( logits ):
exp = torch . exp ( logits - torch . reduce_max ( logits ))
return exp / torch . sum ( exp )
print ( softmax ( torch . tensor ([ 1000. , 0. ])). numpy ()) # prints [ 1., 0.] Regardons un cas plus compliqué. Considérez que nous avons un problème de classification. Nous utilisons la fonction Softmax pour produire des probabilités à partir de nos logits. Nous définissons ensuite notre fonction de perte pour être l'entropie croisée entre nos prédictions et les étiquettes. Rappelons que l'entropie croisée pour une distribution catégorique peut être simplement définie comme xe(p, q) = -Σ p_i log(q_i) . Ainsi, une implémentation naïve de l'entropie croisée ressemblerait à ceci:
def unstable_softmax_cross_entropy ( labels , logits ):
logits = torch . log ( softmax ( logits ))
return - torch . sum ( labels * logits )
labels = torch . tensor ([ 0.5 , 0.5 ])
logits = torch . tensor ([ 1000. , 0. ])
xe = unstable_softmax_cross_entropy ( labels , logits )
print ( xe . numpy ()) # prints infNotez que dans cette implémentation à mesure que la sortie SoftMax s'approche de zéro, la sortie du journal approche de l'infini qui provoque l'instabilité dans notre calcul. Nous pouvons réécrire cela en élargissant le softmax et en faisant des simplifications:
def softmax_cross_entropy ( labels , logits , dim = - 1 ):
scaled_logits = logits - torch . max ( logits )
normalized_logits = scaled_logits - torch . logsumexp ( scaled_logits , dim )
return - torch . sum ( labels * normalized_logits )
labels = torch . tensor ([ 0.5 , 0.5 ])
logits = torch . tensor ([ 1000. , 0. ])
xe = softmax_cross_entropy ( labels , logits )
print ( xe . numpy ()) # prints 500.0Nous pouvons également vérifier que les gradients sont également calculés correctement:
logits . requires_grad_ ( True )
xe = softmax_cross_entropy ( labels , logits )
g = torch . autograd . grad ( xe , logits )[ 0 ]
print ( g . numpy ()) # prints [0.5, -0.5]Permettez-moi de rappeler à nouveau que des soins supplémentaires doivent être pris en cas de descente de gradient pour vous assurer que la plage de vos fonctions ainsi que les gradients de chaque couche se trouvent dans une plage valide. Les fonctions exponentielles et logarithmiques lorsqu'elles sont utilisées naïvement sont particulièrement problématiques car elles peuvent cartographier de petits nombres à des nombres énormes et l'inverse.
Par défaut, les tenseurs et les paramètres du modèle dans Pytorch sont stockés dans une précision de point flottante 32 bits. La formation de réseaux de neurones utilisant des flotteurs 32 bits est généralement stable et ne provoque pas de problèmes numériques majeurs, mais les réseaux de neurones se sont révélés très bien performants dans des précisions 16 bits et encore plus faibles. Le calcul dans des précisions plus faibles peut être considérablement plus rapide sur les GPU modernes. Il a également l'avantage supplémentaire d'utiliser moins de mémoire permettant à la formation de modèles plus grands et / ou avec des tailles de lots plus grandes qui peuvent encore augmenter les performances. Le problème est cependant que la formation en 16 bits devient souvent très instable car la précision n'est généralement pas suffisante pour effectuer certaines opérations comme les accumulations.
Pour aider à ce problème, Pytorch soutient la formation en précision mixte. En un mot, une formation de précision mixte se fait en effectuant des opérations coûteuses (comme des convolutions et des multiplications matricielles) en 16 bits en jetant les entrées tout en effectuant d'autres opérations numériquement sensibles comme des accumulations en 32 bits. De cette façon, nous obtenons tous les avantages du calcul 16 bits sans ses inconvénients. Ensuite, nous parlons d'utiliser Autocast et GradScaleur pour effectuer une formation automatique à la précision mixte.
autocast aide à améliorer les performances d'exécution en jetant automatiquement des données à 16 bits pour certains calculs. Pour comprendre comment cela fonctionne, regardons un exemple:
import torch
x = torch . rand ([ 32 , 32 ]). cuda ()
y = torch . rand ([ 32 , 32 ]). cuda ()
with torch . cuda . amp . autocast ():
a = x + y
b = x @ y
print ( a . dtype ) # prints torch.float32
print ( b . dtype ) # prints torch.float16 Remarque Les x et y sont des tenseurs 32 bits, mais autocast effectue une multiplication matricielle en 16 bits tout en gardant un fonctionnement d'addition en 32 bits. Et si l'un des opérandes est en 16 bits?
import torch
x = torch . rand ([ 32 , 32 ]). cuda ()
y = torch . rand ([ 32 , 32 ]). cuda (). half ()
with torch . cuda . amp . autocast ():
a = x + y
b = x @ y
print ( a . dtype ) # prints torch.float32
print ( b . dtype ) # prints torch.float16 Encore une fois, autocast et jette l'opérande 32 bits à 16 bits pour effectuer une multiplication matricielle, mais il ne modifie pas l'opération d'addition. Par défaut, l'ajout de deux tenseurs dans Pytorch entraîne un plâtre à une précision plus élevée.
En pratique, vous pouvez faire confiance autocast pour faire le bon casting pour améliorer l'efficacité du temps d'exécution. L'important est de conserver tous vos calculs de passes avancées dans le contexte autocast :
model = ...
loss_fn = ...
with torch . cuda . amp . autocast ():
outputs = model ( inputs )
loss = loss_fn ( outputs , targets )C'est peut-être tout ce dont vous avez besoin si vous avez un problème d'optimisation relativement stable et si vous utilisez un taux d'apprentissage relativement faible. L'ajout de cette ligne de code supplémentaire peut réduire votre formation jusqu'à la moitié du matériel moderne.
Comme nous l'avons mentionné au début de cette section, la précision 16 bits n'est peut-être pas toujours suffisante pour certains calculs. Un cas particulier d'intérêt est de représenter les valeurs de gradient, dont une grande partie sont généralement de petites valeurs. Les représenter avec des flotteurs 16 bits conduisent souvent à des sous-flux de tampon (c'est-à-dire qu'ils seraient représentés comme des zéros). Cela rend la formation de réseaux de neurones très instables. GradScalar est conçu pour résoudre ce problème. Il prend en entrée votre valeur de perte et le multiplie par un grand scalaire, gonflant les valeurs de gradient, et les rend donc représentées en précision 16 bits. Il les fait ensuite évoluer pendant la mise à jour du gradient pour s'assurer que les paramètres sont mis à jour correctement. C'est généralement ce que fait GradScalar . Mais sous le capot, GradScalar est un peu plus intelligent que cela. Le gonflement des gradients peut en fait entraîner des débordements, ce qui est tout aussi mauvais. GradScalar surveille donc les valeurs du gradient et si elle détecte le débordement, il saute les mises à jour, réduisant le facteur scalaire en fonction d'un calendrier configurable. (Le calendrier par défaut fonctionne généralement, mais vous devrez peut-être ajuster cela pour votre cas d'utilisation.)
L'utilisation GradScalar est très facile en pratique:
scaler = torch . cuda . amp . GradScaler ()
loss = ...
optimizer = ... # an instance torch.optim.Optimizer
scaler . scale ( loss ). backward ()
scaler . step ( optimizer )
scaler . update () Notez que nous créons d'abord une instance de GradScalar . Dans la boucle de formation, nous appelons GradScalar.scale pour évoluer la perte avant de appeler en arrière pour produire des gradients gonflés, nous utilisons ensuite GradScalar.step qui (peut) mettre à jour les paramètres du modèle. Nous appelons ensuite GradScalar.update qui effectue la mise à jour scalaire si nécessaire. C'est tout!
Ce qui suit est un exemple de code qui montre des cas de formation de précision mixte sur un problème synthétique d'apprendre à générer un damier à partir des coordonnées d'image. Vous pouvez le coller sur un Google Colab, définir le backend sur GPU et comparer les performances uniques et mixtes. Notez qu'il s'agit d'un petit exemple de jouet, en pratique avec des réseaux plus grands, vous pouvez voir des augmentations plus importantes des performances en utilisant une précision mixte.
import torch
import matplotlib . pyplot as plt
import time
def grid ( width , height ):
hrange = torch . arange ( width ). unsqueeze ( 0 ). repeat ([ height , 1 ]). div ( width )
vrange = torch . arange ( height ). unsqueeze ( 1 ). repeat ([ 1 , width ]). div ( height )
output = torch . stack ([ hrange , vrange ], 0 )
return output
def checker ( width , height , freq ):
hrange = torch . arange ( width ). reshape ([ 1 , width ]). mul ( freq / width / 2.0 ). fmod ( 1.0 ). gt ( 0.5 )
vrange = torch . arange ( height ). reshape ([ height , 1 ]). mul ( freq / height / 2.0 ). fmod ( 1.0 ). gt ( 0.5 )
output = hrange . logical_xor ( vrange ). float ()
return output
# Note the inputs are grid coordinates and the target is a checkerboard
inputs = grid ( 512 , 512 ). unsqueeze ( 0 ). cuda ()
targets = checker ( 512 , 512 , 8 ). unsqueeze ( 0 ). unsqueeze ( 1 ). cuda () class Net ( torch . jit . ScriptModule ):
def __init__ ( self ):
super (). __init__ ()
self . net = torch . nn . Sequential (
torch . nn . Conv2d ( 2 , 256 , 1 ),
torch . nn . BatchNorm2d ( 256 ),
torch . nn . ReLU (),
torch . nn . Conv2d ( 256 , 256 , 1 ),
torch . nn . BatchNorm2d ( 256 ),
torch . nn . ReLU (),
torch . nn . Conv2d ( 256 , 256 , 1 ),
torch . nn . BatchNorm2d ( 256 ),
torch . nn . ReLU (),
torch . nn . Conv2d ( 256 , 1 , 1 ))
@ torch . jit . script_method
def forward ( self , x ):
return self . net ( x ) net = Net (). cuda ()
loss_fn = torch . nn . MSELoss ()
opt = torch . optim . Adam ( net . parameters (), 0.001 )
start_time = time . time ()
for i in range ( 500 ):
opt . zero_grad ()
outputs = net ( inputs )
loss = loss_fn ( outputs , targets )
loss . backward ()
opt . step ()
print ( loss )
print ( time . time () - start_time )
plt . subplot ( 1 , 2 , 1 ); plt . imshow ( outputs . squeeze (). detach (). cpu ());
plt . subplot ( 1 , 2 , 2 ); plt . imshow ( targets . squeeze (). cpu ()); plt . show () net = Net (). cuda ()
loss_fn = torch . nn . MSELoss ()
opt = torch . optim . Adam ( net . parameters (), 0.001 )
scaler = torch . cuda . amp . GradScaler ()
start_time = time . time ()
for i in range ( 500 ):
opt . zero_grad ()
with torch . cuda . amp . autocast ():
outputs = net ( inputs )
loss = loss_fn ( outputs , targets )
scaler . scale ( loss ). backward ()
scaler . step ( opt )
scaler . update ()
print ( loss )
print ( time . time () - start_time )
plt . subplot ( 1 , 2 , 1 ); plt . imshow ( outputs . squeeze (). detach (). cpu (). float ());
plt . subplot ( 1 , 2 , 2 ); plt . imshow ( targets . squeeze (). cpu (). float ()); plt . show ()