Um Pytorch zu installieren, folgen Sie den Anweisungen auf der offiziellen Website:
pip install torch torchvision
Wir wollen diese Serie schrittweise erweitern, indem wir neue Artikel hinzufügen und den Inhalt mit den neuesten Veröffentlichungen der Pytorch -API auf dem neuesten Stand halten. Wenn Sie Vorschläge zur Verbesserung dieser Serie haben oder die Erklärungen mehrdeutig finden, können Sie ein Problem erstellen, Patches senden oder per E -Mail angreifen.
Pytorch ist eine der beliebtesten Bibliotheken für die numerische Berechnung und gehört derzeit zu den am häufigsten verwendeten Bibliotheken für die Durchführung von Forschungsergebnissen für maschinelles Lernen. In vielerlei Hinsicht ähnelt Pytorch Numpy, mit dem zusätzlichen Nutzen, den Sie mit Pytorch Ihre Berechnungen zu CPUs, GPUs und TPUs ohne wesentliche Änderung Ihres Codes durchführen können. Pytorch erleichtert auch einfach, Ihre Berechnung auf mehrere Geräte oder Maschinen zu verteilen. Eines der wichtigsten Merkmale von Pytorch ist die automatische Differenzierung. Es ermöglicht die Berechnung der Gradienten Ihrer Funktionen effizient analytisch, was für die Schulung maschineller Lernmodelle mithilfe der Gradientenabstichtmethode von entscheidender Bedeutung ist. Unser Ziel hier ist es, eine sanfte Einführung in Pytorch zu bieten und Best Practices für die Verwendung von Pytorch zu diskutieren.
Das erste, was man über Pytorch lernen kann, ist das Konzept der Tensoren. Tensoren sind einfach mehrdimensionale Arrays. Ein Pytorch -Tensor ist einem Numpy -Array mit einigen sehr ähnlich magisch Zusätzliche Funktionalität.
Ein Tensor kann einen skalaren Wert speichern:
import torch
a = torch . tensor ( 3 )
print ( a ) # tensor(3)oder ein Array:
b = torch . tensor ([ 1 , 2 ])
print ( b ) # tensor([1, 2])Eine Matrix:
c = torch . zeros ([ 2 , 2 ])
print ( c ) # tensor([[0., 0.], [0., 0.]])oder beliebiger dimensionaler Tensor:
d = torch . rand ([ 2 , 2 , 2 ])Tensoren können verwendet werden, um algebraische Operationen effizient durchzuführen. Eine der am häufigsten verwendeten Operationen in Anwendungen für maschinelles Lernen ist die Matrixmultiplikation. Angenommen, Sie möchten zwei zufällige Matrizen der Größe 3x5 und 5x4 multiplizieren. Dies kann mit der Matrix -Multiplikation (@) -Operation erfolgen:
import torch
x = torch . randn ([ 3 , 5 ])
y = torch . randn ([ 5 , 4 ])
z = x @ y
print ( z )Um zwei Vektoren hinzuzufügen, können Sie dies tun:
z = x + yUm einen Tensor in ein Numpy -Array umzuwandeln, können Sie Tensors Numpy () -Methode nennen:
print ( z . numpy ())Und Sie können immer ein Numpy -Array in einen Tensor umwandeln, um:
x = torch . tensor ( np . random . normal ([ 3 , 5 ]))Der wichtigste Vorteil von Pytorch gegenüber Numpy ist die automatische Differenzierungsfunktionalität, die bei Optimierungsanwendungen sehr nützlich ist, z. B. die Optimierung der Parameter eines neuronalen Netzwerks. Versuchen wir es mit einem Beispiel zu verstehen.
Angenommen, Sie haben eine zusammengesetzte Funktion, die eine Kette von zwei Funktionen ist: g(u(x)) . Um die Ableitung von g in Bezug auf x zu berechnen, können wir die Kettenregel verwenden, die besagt, dass: dg/dx = dg/du * du/dx . Pytorch kann die Derivate für uns analytisch berechnen.
Um die Derivate in Pytorch zuerst zu berechnen, erstellen wir zuerst einen Tensor und setzen ihren requires_grad auf true. Wir können Tensoroperationen verwenden, um unsere Funktionen zu definieren. Wir gehen davon aus, dass u eine quadratische Funktion ist und g eine einfache lineare Funktion ist:
x = torch . tensor ( 1.0 , requires_grad = True )
def u ( x ):
return x * x
def g ( u ):
return - u In diesem Fall lautet unsere zusammengesetzte Funktion g(u(x)) = -x*x . Es ist also der Ableitungen in Bezug auf x -2x . An Punkt x=1 ist dies gleich -2 .
Überprüfen wir dies. Dies kann mit der Grad -Funktion in Pytorch erfolgen:
dgdx = torch . autograd . grad ( g ( u ( x )), x )[ 0 ]
print ( dgdx ) # tensor(-2.) Um zu verstehen, wie stark die automatische Differenzierung sein kann, schauen wir uns ein anderes Beispiel an. Angenommen, wir haben Stichproben aus einer Kurve (sagen wir f(x) = 5x^2 + 3 ) und wir möchten f(x) basierend auf diesen Proben schätzen. Wir definieren eine parametrische Funktion g(x, w) = w0 x^2 + w1 x + w2 , was eine Funktion der Eingabe x und latente Parameter w ist. Unser Ziel ist es dann, die latenten Parameter so zu finden, dass g(x, w) ≈ f(x) . Dies kann durch Minimieren der folgenden Verlustfunktion erfolgen: L(w) = Σ (f(x) - g(x, w))^2 . Obwohl es eine geschlossene Formlösung für dieses einfache Problem gibt, entscheiden wir uns für einen allgemeineren Ansatz, der auf eine beliebige differenzierbare Funktion angewendet werden kann und die stochastische Gradientenabstiegungen verwendet. Wir berechnen einfach den durchschnittlichen Gradienten von L(w) in Bezug auf w über einen Satz von Stichprobenpunkten und bewegen uns in die entgegengesetzte Richtung.
So kann es in Pytorch gemacht werden:
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 ())Wenn Sie diesen Code ausführen, sollten Sie ein Ergebnis in der Nähe davon sehen:
[ 4.9924135 , 0.00040895029 , 3.4504161 ]Dies ist eine relativ enge Annäherung an unsere Parameter.
Dies ist nur die Spitze des Eisbergs für das, was Pytorch tun kann. Viele Probleme wie die Optimierung großer neuronaler Netzwerke mit Millionen von Parametern können in nur wenigen Code -Zeilen effizient in Pytorch implementiert werden. Pytorch kümmert sich um die Skalierung über mehrere Geräte und Threads und unterstützt eine Vielzahl von Plattformen.
Im vorherigen Beispiel haben wir nackte Knochen -Tensoren und Tensoroperationen verwendet, um unser Modell aufzubauen. Um Ihren Code etwas organisierter zu gestalten, wird empfohlen, die Module von Pytorch zu verwenden. Ein Modul ist einfach ein Container für Ihre Parameter und verkauft Modellvorgänge. Angenommen, Sie möchten ein lineares Modell y = ax + b darstellen. Dieses Modell kann mit dem folgenden Code dargestellt werden:
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 yhatUm dieses Modell in der Praxis zu verwenden, instanziieren Sie das Modul und nennen Sie es einfach wie eine Funktion:
x = torch . arange ( 100 , dtype = torch . float32 )
net = Net ()
y = net ( x ) Parameter sind im Wesentlichen Tensoren mit requires_grad auf true. Es ist bequem, Parameter zu verwenden, da Sie sie einfach alle mit der Methode des Moduls parameters() abrufen können:
for p in net . parameters ():
print ( p ) Nehmen Sie nun an, Sie haben eine unbekannte Funktion y = 5x + 3 + some noise und möchten die Parameter Ihres Modells so optimieren, dass diese Funktion entspricht. Sie können zunächst einige Punkte aus Ihrer Funktion starten:
x = torch . arange ( 100 , dtype = torch . float32 ) / 100
y = 5 * x + 3 + torch . rand ( 100 ) * 0.3Ähnlich wie beim vorherigen Beispiel können Sie eine Verlustfunktion definieren und die Parameter Ihres Modells wie folgt optimieren:
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 verfügt über eine Reihe vordefinierter Module. Ein solches Modul ist torch.nn.Linear Wir können unser Modul oben mit torch.nn.Linear wie folgt umschreiben:
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 Beachten Sie, dass wir Squeeze und Unquice seit torch.nn.Linear verwendet haben.
Standardmäßig werden die Parameter () auf einem Modul die Parameter aller Submodules zurückgegeben:
net = Net ()
for p in net . parameters ():
print ( p ) Es gibt einige vordefinierte Module, die als Behälter für andere Module wirken. Das am häufigsten verwendete Containermodul ist torch.nn.Sequential . Wie der Name impliziert, wird es verwendet, um mehrere Module (oder Schichten) übereinander zu stapeln. Zum Beispiel, um zwei lineare Schichten mit einer ReLU -Nichtlinearität zwischen Ihnen zu stapeln:
model = torch . nn . Sequential (
torch . nn . Linear ( 64 , 32 ),
torch . nn . ReLU (),
torch . nn . Linear ( 32 , 10 ),
) Pytorch unterstützt die Operationen von Rundfunkelemente. [3, 4] Sie Operationen wie Addition und Multiplikation ausführen möchten, müssen Sie normalerweise sicherstellen [3, 2] dass die Formen der Operanden übereinstimmen. Aber es gibt einen besonderen Fall und dann haben Sie eine einzigartige Dimension. Pytorch fliesen den Tensor implizit über seine einzigartigen Dimensionen, um der Form des anderen Operanden zu entsprechen. Daher ist es gültig, einem Formsor der Form [3, 2] einen Tensor der Form [3, 1] hinzuzufügen.
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 )Mit dem Rundfunk ermöglicht es uns, implizite Fliesen durchzuführen, was den Code kürzer und speicher effizienter macht, da wir das Ergebnis des Fliesenoperos nicht speichern müssen. Ein ordentlicher Ort, den dies verwendet werden kann, ist die Kombination von Merkmalen unterschiedlicher Länge. Um Merkmale unterschiedlicher Länge zu verkettet, fliesen wir üblicherweise die Eingangstensoren, verkettet das Ergebnis und wenden eine gewisse Nichtlinearität an. Dies ist ein gemeinsames Muster für eine Vielzahl von neuronalen Netzwerkarchitekturen:
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]) Dies kann jedoch mit dem Rundfunk effizienter erfolgen. Wir verwenden die Tatsache, dass f(m(x + y)) gleich f(mx + my) ist. So können wir die linearen Operationen separat ausführen und über das Rundfunk verwenden, um implizite Verkettung durchzuführen:
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])Tatsächlich ist dieses Stück Code ziemlich allgemein und kann auf Tensoren in willkürlicher Form angewendet werden, solange das Rundfunk zwischen Tensoren möglich ist:
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 cBisher haben wir über den guten Teil des Rundfunks gesprochen. Aber was ist der hässliche Teil, den Sie fragen können? Implizite Annahmen erschweren fast immer das Debuggen. Betrachten Sie das folgende Beispiel:
a = torch . tensor ([[ 1. ], [ 2. ]])
b = torch . tensor ([ 1. , 2. ])
c = torch . sum ( a + b )
print ( c ) Was ist Ihrer Meinung nach der Wert von c nach der Bewertung? Wenn Sie 6 erraten haben, ist das falsch. Es wird 12 sein. Dies liegt daran, dass Pytorch, wenn der Rang von zwei Tensoren nicht übereinstimmt, die erste Dimension des Tensors automatisch mit niedrigerem Rang vor dem elementiösen Betrieb erweitert, sodass das Ergebnis der Zugabe [[2, 3], [3, 4]] und die Reduktion aller Parameter 12 geben würde 12.
Der Weg, dieses Problem zu vermeiden, besteht darin, so explizit wie möglich zu sein. Hätten wir angegeben, in welcher Dimension wir reduzieren möchten, wäre es viel einfacher gewesen, diesen Fehler zu fangen:
a = torch . tensor ([[ 1. ], [ 2. ]])
b = torch . tensor ([ 1. , 2. ])
c = torch . sum ( a + b , 0 )
print ( c ) Hier wäre der Wert von c [5, 7] , und wir würden sofort anhand der Form des Ergebniss erraten, dass etwas nicht stimmt. Eine allgemeine Faustregel gilt immer, die Abmessungen in Reduktionsoperationen und bei der Verwendung torch.squeeze immer anzugeben.
Genau wie Numpy überlastet Pytorch eine Reihe von Python -Operatoren, um den Pytorch -Code kürzer und lesbarer zu machen.
Das Slicing OP ist einer der überlasteten Operatoren, die die Indizierung Tensoren sehr einfach machen können:
z = x [ begin : end ] # z = torch.narrow(0, begin, end-begin)Seien Sie jedoch sehr vorsichtig, wenn Sie dieses OP verwenden. Das Slicing OP hat wie jedes andere OP etwas Overhead. Weil es ein allgemeines OP und unschuldig ist, kann es viel übertragen werden, was zu Ineffizienzen führen kann. Um zu verstehen, wie ineffizient dieses OP sein kann, schauen wir uns ein Beispiel an. Wir möchten die Reduzierung einer Matrix manuell über die Reihen einer Matrix durchführen:
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 )) Dies läuft ziemlich langsam und der Grund ist, dass wir das Slice Op 500 -mal nennen, was viel Overhead hinzufügt. Eine bessere Wahl wäre es gewesen, torch.unbind zu verwenden.
z = torch . zeros ([ 10 ])
for x_i in torch . unbind ( x ):
z += x_iDies ist erheblich schneller (~ 30% auf meiner Maschine).
Natürlich ist der richtige Weg, um diese einfache Reduktion durchzuführen torch.sum
z = torch . sum ( x , dim = 0 )Das ist extrem schnell (~ 100x schneller auf meiner Maschine).
Pytorch überlädt auch eine Reihe von arithmetischen und logischen Operatoren:
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) Sie können auch die Augmented -Version dieser OPS verwenden. Zum Beispiel sind auch x += y und x **= 2 gültig.
Beachten Sie, dass Python Überladung nicht zulässt and or not Schlüsselwörter.
Pytorch ist optimiert, um Operationen bei großen Tensoren durchzuführen. In Pytorch ist viele Operationen bei kleinen Tensoren durchzuführen. Wenn möglich, sollten Sie Ihre Berechnungen in Stapelform umschreiben, um den Overhead zu reduzieren und die Leistung zu verbessern. Wenn Sie Ihre Vorgänge auf keinen Fall manuell stapeln können, kann die Verwendung von Torchscript die Leistung Ihres Codes verbessern. Torchscript ist einfach eine Untergruppe von Python -Funktionen, die von Pytorch erkannt werden. Pytorch kann Ihren Torchscript -Code automatisch mit seinem Just Palte Time (JIT) -Kompiler optimieren und einige Gemeinkosten reduzieren.
Schauen wir uns ein Beispiel an. Ein sehr häufiger Betrieb in ML -Anwendungen ist "Batch -Getree". Diese Operation kann einfach als output[i] = input[i, index[i]] geschrieben werden. Dies kann einfach in Pytorch wie folgt implementiert werden:
import torch
def batch_gather ( tensor , indices ):
output = []
for i in range ( tensor . size ( 0 )):
output += [ tensor [ i ][ indices [ i ]]]
return torch . stack ( output ) Um dieselbe Funktion mit Torchscript zu implementieren, verwenden Sie einfach den Dekorator 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 )Bei meinen Tests ist dies etwa 10% schneller.
Aber nichts ist es, Ihre Operationen manuell zu besiegen. Eine vektorisierte Implementierung in meinen Tests ist 100 -mal schneller:
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 In der letzten Lektion haben wir darüber gesprochen, einen effizienten Pytorch -Code zu schreiben. Damit Ihr Code jedoch mit maximaler Effizienz ausgeführt wird, müssen Sie Ihre Daten auch effizient in den Speicher Ihres Geräts laden. Glücklicherweise bietet Pytorch ein Tool, um das Laden von Daten zu vereinfachen. Es heißt DataLoader . Ein DataLoader verwendet mehrere Mitarbeiter, um Daten gleichzeitig aus einem Dataset zu laden und optional einen Sampler für Dateneinträge zu verwenden und eine Stapel zu bilden.
Wenn Sie zufällig auf Ihre Daten zugreifen können, ist die Verwendung eines DataLoader sehr einfach: Sie müssen lediglich eine Dataset implementieren, die __getitem__ (um jedes Datenelement lesen) und __len__ (um die Anzahl der Elemente in den Datensatz zurückzugeben) implementiert. So laden Sie beispielsweise Bilder aus einem bestimmten Verzeichnis:
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 )Um alle JPEG -Bilder aus einem bestimmten Verzeichnis zu laden, können Sie Folgendes machen:
dataloader = torch . utils . data . DataLoader ( ImageDirectoryDataset ( "/data/imagenet/*.jpg" ), num_workers = 8 )
for data in dataloader :
# do something with dataHier verwenden wir 8 Arbeiter, um unsere Daten gleichzeitig von der Festplatte zu lesen. Sie können die Anzahl der Arbeiter auf Ihrer Maschine für optimale Ergebnisse einstellen.
Die Verwendung eines DataLoader zum Lesen von Daten mit Zufallszugriff kann in Ordnung sein, wenn Sie einen schnellen Speicher haben oder wenn Ihre Datenelemente groß sind. Stellen Sie sich jedoch vor, ein Netzwerkdateisystem mit langsamer Verbindung zu haben. Das Anfragen einzelner Dateien auf diese Weise kann extrem langsam sein und würde wahrscheinlich zum Engpass Ihrer Trainingspipeline werden.
Ein besserer Ansatz besteht darin, Ihre Daten in einem zusammenhängenden Dateiformat zu speichern, das nacheinander gelesen werden kann. Wenn Sie beispielsweise eine große Sammlung von Bildern haben, können Sie TAR verwenden, um ein einzelnes Archiv zu erstellen und Dateien aus dem Archiv nacheinander in Python zu extrahieren. Zu diesem Zweck können Sie Pytorchs IterableDataset verwenden. So erstellen Sie eine IterableDataset -Klasse, müssen Sie nur eine __iter__ -Methode implementieren, die nacheinander Datenelemente aus dem Datensatz liest und ergibt.
Eine naive Implementierung würde dies mögen:
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 )Bei dieser Implementierung gibt es jedoch ein großes Problem. Wenn Sie versuchen, Dataloader mit mehr als einem Arbeiter aus diesem Datensatz zu lesen, würden Sie viele doppelte Bilder beobachten:
dataloader = torch . utils . data . DataLoader ( TarImageDataset ( "/data/imagenet.tar" ), num_workers = 8 )
for data in dataloader :
# data contains duplicated items Das Problem ist, dass jeder Arbeiter eine separate Instanz des Datensatzes erstellt und jeder vom Beginn des Datensatzes beginnt. Eine Möglichkeit, dies zu vermeiden, besteht darin, anstatt eine TAR -Datei zu haben, Ihre Daten in num_workers getrennte TAR -Dateien aufzuteilen und jeweils mit einem separaten Arbeiter zu laden:
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 ])So kann unsere Datensatzklasse verwendet werden:
dataloader = torch . utils . data . DataLoader (
TarImageDataset ([ "/data/imagenet_part1.tar" , "/data/imagenet_part2.tar" ]), num_workers = 2 )
for data in dataloader :
# do something with dataWir haben eine einfache Strategie besprochen, um ein Problem mit doppelten Einträgen zu vermeiden. Das TFRECORD -Paket verwendet etwas anspruchsvollere Strategien, um Ihre Daten im laufenden Fliegen zu schärfen.
Bei Verwendung einer numerischen Berechnungsbibliothek wie Numpy oder Pytorch ist es wichtig zu beachten, dass das Schreiben mathematisch korrekter Code nicht unbedingt zu korrekten Ergebnissen führt. Sie müssen auch sicherstellen, dass die Berechnungen stabil sind.
Beginnen wir mit einem einfachen Beispiel. Mathematisch ist leicht zu erkennen, dass x * y / y = x für jeden Nicht -Null -Wert von x . Aber mal sehen, ob das in der Praxis immer wahr ist:
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 Der Grund für das falsche Ergebnis ist, dass y für Float32 einfach zu klein ist. Ein ähnliches Problem tritt auf, wenn y zu groß ist:
y = np . float32 ( 1e39 ) # y would be stored as inf
z = x * y / y
print ( z ) # prints nanDer kleinste positive Wert, den Float32-Typ darstellen kann, ist 1,4013E-45 und alles, was darunter als Null gespeichert wird. Außerdem würde eine beliebige Zahl über 3,40282E+38 als INF gespeichert.
print ( np . nextafter ( np . float32 ( 0 ), np . float32 ( 1 ))) # prints 1.4013e-45
print ( np . finfo ( np . float32 ). max ) # print 3.40282e+38Um sicherzustellen, dass Ihre Berechnungen stabil sind, möchten Sie Werte mit kleinem oder sehr großem absolutem Wert vermeiden. Dies mag sehr offensichtlich klingen, aber diese Art von Problemen können extrem schwer zu debuggen werden, insbesondere wenn sie Gradientenabstieg in Pytorch durchführen. Dies liegt daran, dass Sie nicht nur sicherstellen müssen, dass alle Werte im Vorwärtspass innerhalb des gültigen Bereichs Ihrer Datentypen liegen, sondern auch für den Rückwärtspass (während der Gradientenberechnung) sicherstellen müssen.
Schauen wir uns ein echtes Beispiel an. Wir möchten den Softmax über einen Vektor von Logits berechnen. Eine naive Implementierung würde ungefähr so aussehen:
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.] Beachten Sie, dass die Berechnung des Exponentials von Logits für relativ kleine Zahlen zu gigantischen Ergebnissen führt, die aus dem Bereich Float32 liegen. Der größte gültige Logit für unsere naive Softmax -Implementierung ist ln(3.40282e+38) = 88.7 , alles, was über ein NAN -Ergebnis hinausgeht.
Aber wie können wir das stabiler machen? Die Lösung ist ziemlich einfach. Es ist leicht zu erkennen, dass exp(x - c) Σ exp(x - c) = exp(x) / Σ exp(x) . Daher können wir jede Konstante von den Logits subtrahieren und das Ergebnis würde gleich bleiben. Wir wählen diese Konstante, um das Maximum an Logits zu sein. Auf diese Weise wäre die Domäne der exponentiellen Funktion auf [-inf, 0] beschränkt, und folglich wäre ihr Bereich [0.0, 1.0] was wünschenswert ist:
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.] Schauen wir uns einen komplizierteren Fall an. Bedenken Sie, dass wir ein Klassifizierungsproblem haben. Wir verwenden die Softmax -Funktion, um Wahrscheinlichkeiten aus unseren Logits zu erzeugen. Wir definieren dann unsere Verlustfunktion als Kreuzentropie zwischen unseren Vorhersagen und den Etiketten. Erinnern Sie sich daran, dass die Kreuzentropie für eine kategoriale Verteilung einfach als xe(p, q) = -Σ p_i log(q_i) definiert werden kann. Eine naive Implementierung der Kreuzentropie würde also so aussehen:
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 infBeachten Sie, dass in dieser Implementierung als Softmax -Ausgangsausgang Null nähert, der Ausgangsausgang des Protokolls nähert sich unendlich, was zu einer Instabilität in unserer Berechnung führt. Wir können dies umschreiben, indem wir den Softmax erweitern und einige Vereinfachungen durchführen:
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.0Wir können auch überprüfen, ob die Gradienten auch korrekt berechnet werden:
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]Lassen Sie mich noch einmal daran erinnern, dass beim Gradientenabstieg zusätzliche Sorgfalt aufgenommen werden muss, um sicherzustellen, dass der Bereich Ihrer Funktionen sowie die Gradienten für jede Schicht in einem gültigen Bereich liegen. Exponentielle und logarithmische Funktionen sind besonders problematisch, wenn sie naiv verwendet werden, da sie kleine Zahlen den enormen und umgekehrten Weg zuordnen können.
Standardmäßig werden Tensoren und Modellparameter in Pytorch in 32-Bit-schwimmender Punktpräzision gespeichert. Das Training neuronaler Netzwerke mit 32-Bit-Floats ist normalerweise stabil und verursacht keine wichtigen numerischen Probleme. Es wurde jedoch gezeigt, dass neuronale Netze in 16-Bit- und sogar niedrigeren Präzisionen recht gut abschneiden. Die Berechnung in niedrigeren Präzisionen kann bei modernen GPUs erheblich schneller sein. Es hat auch den zusätzlichen Vorteil, weniger Speicher zu verwenden, das das Training größere Modelle und/oder mit größeren Chargengrößen ermöglicht, was die Leistung weiter steigern kann. Das Problem ist jedoch, dass das Training in 16 Bit oft sehr instabil wird, da die Präzision normalerweise nicht ausreicht, um einige Operationen wie Ansammlungen auszuführen.
Um dieses Problem zu unterstützen, unterstützt Pytorch das Training in gemischter Präzision. Kurz gesagt, ein Mischprezisions-Training erfolgt durch die Durchführung einiger teurer Operationen (wie Konvolutionen und Matrix-Multplikationen) in 16-Bit, indem die Eingaben abgelegt werden, während andere numerisch empfindliche Operationen wie Akkumulationen in 32-Bit durchgeführt werden. Auf diese Weise erhalten wir alle Vorteile einer 16-Bit-Berechnung ohne ihre Nachteile. Als nächstes sprechen wir über die Verwendung von Autocast und Gradscaler, um automatische Schulungen mit gemischtem Präzision durchzuführen.
autocast hilft, die Laufzeitleistung zu verbessern, indem Daten automatisch Daten für einige Berechnungen auf 16-Bit gestrichen werden. Um zu verstehen, wie es funktioniert, schauen wir uns ein Beispiel an:
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 Beachten Sie sowohl x als auch y sind 32-Bit-Tensoren, aber autocast führt die Matrixmultiplikation in 16-Bit durch, wobei der Zusatzbetrieb in 32-Bit den Betrieb beibehält. Was ist, wenn einer der Operanden in 16-Bit ist?
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 Auch erneut autocast und lässt den 32-Bit-Operanden auf 16-Bit abgelegt, um die Matrix-Multiplikation durchzuführen, ändert jedoch nicht den Additionsvorgang. Standardmäßig führt die Zugabe von zwei Tensoren in Pytorch zu einem Guss zu einer höheren Präzision.
In der Praxis können Sie autocast vertrauen, dass Sie das richtige Casting durchführen, um die Laufzeit -Effizienz zu verbessern. Wichtig ist, alle Ihre Vorwärts -Pass -Berechnungen im autocast -Kontext zu halten:
model = ...
loss_fn = ...
with torch . cuda . amp . autocast ():
outputs = model ( inputs )
loss = loss_fn ( outputs , targets )Dies ist möglicherweise alles, was Sie brauchen, wenn Sie ein relativ stabiles Optimierungsproblem haben und wenn Sie eine relativ niedrige Lernrate verwenden. Durch das Hinzufügen dieser einen zusätzlichen Code können Sie Ihr Training auf moderne Hardware auf die Hälfte reduzieren.
Wie wir zu Beginn dieses Abschnitts erwähnt haben, reicht 16-Bit-Präzision möglicherweise nicht immer für einige Berechnungen aus. Ein besonderer Fall von Interesse ist die Darstellung von Gradientenwerten, von denen ein großer Teil normalerweise kleine Werte sind. Die Repräsentation mit 16-Bit-Schwimmer führt oft zu Pufferunterläufen (dh sie würden als Nullen dargestellt). Dies macht die Schulung neuronaler Netzwerke sehr instabil. GradScalar wurde entwickelt, um dieses Problem zu lösen. Es dauert als Eingabe Ihres Verlustwerts und multipliziert ihn mit einem großen Skalar, aufblasen Gradientenwerte und somit sie daher in 16-Bit-Präzision repräsentativ. Es skaliert sie dann während des Gradientenupdates, um sicherzustellen, dass die Parameter korrekt aktualisiert werden. Dies ist im Allgemeinen, was GradScalar tut. Aber unter der Haube ist GradScalar etwas schlauer. Das Aufblasen der Gradienten kann tatsächlich zu Überläufen führen, die gleichermaßen schlecht sind. Daher überwacht GradScalar tatsächlich die Gradientenwerte und wenn es überläuft, überläuft sie Aktualisierungen und skaliert den Skalarfaktor gemäß einem konfigurierbaren Zeitplan. (Der Standardplan funktioniert normalerweise, aber Sie müssen dies möglicherweise für Ihren Anwendungsfall anpassen.)
Die Verwendung GradScalar ist in der Praxis sehr einfach:
scaler = torch . cuda . amp . GradScaler ()
loss = ...
optimizer = ... # an instance torch.optim.Optimizer
scaler . scale ( loss ). backward ()
scaler . step ( optimizer )
scaler . update () Beachten Sie, dass wir zuerst eine Instanz von GradScalar erstellen. In der Trainingsschleife nennen wir GradScalar.scale , um den Verlust zu skalieren, bevor wir rückwärts aufgerufen werden, um aufgeblasene Gradienten zu erzeugen, und dann verwenden wir GradScalar.step , die (Mai) die Modellparameter aktualisieren. Wir nennen dann GradScalar.update , das bei Bedarf das skalare Update durchführt. Das ist alles!
Das Folgende ist ein Beispielcode, der Fälle gemischtes Präzisionstraining für ein synthetisches Problem des Lernens zeigt, um ein Schachbrett aus Bildkoordinaten zu generieren. Sie können es in ein Google Colab einfügen, das Backend auf die GPU einstellen und die Leistung der einzelnen und gemischten Präzision vergleichen. Beachten Sie, dass dies ein kleines Spielzeugbeispiel ist. In der Praxis mit größeren Netzwerken werden Sie möglicherweise größere Leistungssteigerungen unter Verwendung gemischter Präzision feststellen.
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 ()