Dies ist kein offizieller Leitfaden für Pytorch. Dieses Dokument fasst Best Practices aus mehr als einem Jahr Erfahrung mit Deep Learning unter Verwendung des Pytorch -Frameworks zusammen. Beachten Sie, dass die Erkenntnisse, die wir teilen, hauptsächlich aus Sicht der Forschung und Startup stammen.
Dies ist ein offenes Projekt, und andere Mitarbeiter sind sehr begrüßt, um das Dokument zu bearbeiten und zu verbessern.
Sie finden drei Hauptteile dieses Dokuments. Erstens eine kurze Zusammenfassung der Best Practices in Python, gefolgt von einigen Tipps und Empfehlungen mit Pytorch. Schließlich teilen wir einige Erkenntnisse und Erfahrungen mit anderen Frameworks, die uns dabei geholfen haben, unseren Workflow im Allgemeinen zu verbessern.
Aktualisieren Sie 20.12.2020
Update 30.4.2019
Nach so viel positivem Feedback fügte ich auch eine Zusammenfassung der häufig verwendeten Bausteine aus unseren Projekten hinzu: Sie finden Bausteine für (Selbstbekämpfung, Wahrnehmungsverlust unter Verwendung von VGG, spektrale Normalisierung, adaptive Instanznormalisierung, ...)
Code -Snippets für Verluste, Schichten und andere Bausteine
Aus unserer Erfahrung empfehlen wir die Verwendung von Python 3.6+ aufgrund der folgenden Funktionen, die für sauberen und einfachen Code sehr praktisch wurden:
Wir versuchen, dem Google StyleGuide für Python zu folgen. Weitere Informationen zu Python-Code von Google finden Sie im gut dokumentierten Stilhandbuch.
Wir bieten hier eine Zusammenfassung der am häufigsten verwendeten Regeln:
Von 3.16.4
| Typ | Konvention | Beispiel |
|---|---|---|
| Pakete und Module | Lower_with_under | vom Prefetch_Generator importieren Sie Hintergrundgenerator |
| Klassen | Kopfwörter | Klasse Dataloader |
| Konstanten | Caps_with_under | Batch_size = 16 |
| Instanzen | Lower_with_under | DataSet = Dataset |
| Methoden und Funktionen | Lower_with_under () | Def visualisierung_tensor () |
| Variablen | Lower_with_under | BINTERGY_COLOR = 'Blue' ' |
Im Allgemeinen empfehlen wir die Verwendung einer IDE wie Visual Studio Code oder Pycharm. Während der VS -Code Syntax -Hervorhebung und Autoperation in einem relativ leichten Editor bietet Pycharm bietet viele erweiterte Funktionen für die Arbeit mit Remote -Clustern. VS Code ist mit seinem schnell wachsenden Ökosystem der Erweiterungen sehr mächtig geworden.
Stellen Sie sicher, dass die folgenden Erweiterungen installiert sind:
Wenn Sie ordnungsgemäß eingerichtet sind, können Sie Folgendes tun:
Im Allgemeinen empfehlen wir, Jupyter -Notizbücher für die erste Exploration/ das Spielen mit neuen Modellen und Code zu verwenden. Python -Skripte sollten verwendet werden, sobald Sie das Modell in einem größeren Datensatz trainieren möchten, in dem auch die Reproduzierbarkeit wichtiger ist.
Unser empfohlener Workflow:
| Jupyter Notebook | Python -Skripte |
|---|---|
| + Erkundung | + Längere Jobs ohne Unterbrechung ausführen |
| + Debugging | + Einfach zu verfolgen Änderungen mit Git |
| - kann eine riesige Datei werden | - Das Debuggen bedeutet hauptsächlich, das gesamte Drehbuch erneut zu übertragen |
| - kann unterbrochen werden (verwenden Sie nicht für ein langes Training) | |
| - Anfällig für Fehler und ein Chaos werden |
Häufig verwendete Bibliotheken:
| Name | Beschreibung | Verwendet für |
|---|---|---|
| Fackel | Grundrahmen für die Arbeit mit neuronalen Netzwerken | Erstellen von Tensoren, Netzwerken und Schulungen mit Backprops |
| Torchvision | Pytorch Computer Vision Module | Vorverarbeitung, Augmentation, Nachverarbeitung |
| Kissen (Pil) | Python Imaging Library | Laden Sie Bilder und speichern Sie sie |
| Numpy | Paket für wissenschaftliches Computing mit Python | Datenvorverarbeitung und Nachbearbeitung |
| Prefetch_Generator | Bibliothek für die Hintergrundverarbeitung | Laden Sie die nächste Stapel im Hintergrund während der Berechnung |
| tqdm | Fortschrittsbalken | Fortschritte beim Training jeder Epoche |
| Torchinfo | Drucken kerasähnlicher Modellzusammenfassung für Pytorch | Zeigt das Netzwerk an, es sind Parameter und Größen in jeder Ebene |
| Torch.utils.tensorboard | Tensorboard in Pytorch | Protokollierungsexperimente und zeigen sie in Tensorboard |
Legen Sie nicht alle Ebenen und Modelle in dieselbe Datei. Eine bewährte Praxis besteht darin, die endgültigen Netzwerke in eine separate Datei ( networks.py ) zu trennen und die Ebenen, Verluste und OPs in den jeweiligen Dateien ( Layers.Py , Verluste.Py , Ops.py ) aufzubewahren. Das fertige Modell (bestehend aus einem oder mehreren Netzwerken) sollte in einer Datei mit seinem Namen (z. B. yolov3.py , dcgan.py ) Referenz sein.
Die Hauptroutine, die die Zug- und Testskripte entsprechen, sollte nur aus der Datei mit dem Namen des Modells importieren.
Wir empfehlen, das Netzwerk in seine kleineren wiederverwendbaren Stücke aufzuteilen. Ein Netzwerk ist ein nn.modul , das aus Operationen oder anderen Nn.Moduls als Bausteine besteht. Verlustfunktionen sind auch nn.modul und können daher direkt in das Netzwerk integriert werden.
Eine Klasse, die von Nn.Module erbt, muss eine Vorwärtsmethode haben, die den Vorwärtspass der jeweiligen Schicht oder Operation implementiert.
Ein nn.module kann mit self.net (Eingabe) für Eingabedaten verwendet werden. Dadurch wird einfach die CALL () -Methode des Objekts verwendet, um die Eingabe über das Modul zu versorgen.
output = self . net ( input )Verwenden Sie das folgende Muster für einfache Netzwerke mit einem einzelnen Eingang und einer einzelnen Ausgabe:
class ConvBlock ( nn . Module ):
def __init__ ( self ):
super ( ConvBlock , self ). __init__ ()
self . block = nn . Sequential (
nn . Conv2d (...),
nn . ReLU (),
nn . BatchNorm2d (...)
)
def forward ( self , x ):
return self . block ( x )
class SimpleNetwork ( nn . Module ):
def __init__ ( self , num_resnet_blocks = 6 ):
super ( SimpleNetwork , self ). __init__ ()
# here we add the individual layers
layers = [ ConvBlock (...)]
for i in range ( num_resnet_blocks ):
layers += [ ResBlock (...)]
self . net = nn . Sequential ( * layers )
def forward ( self , x ):
return self . net ( x )Beachten Sie Folgendes:
class ResnetBlock ( nn . Module ):
def __init__ ( self , dim , padding_type , norm_layer , use_dropout , use_bias ):
super ( ResnetBlock , self ). __init__ ()
self . conv_block = self . build_conv_block (...)
def build_conv_block ( self , ...):
conv_block = []
conv_block += [ nn . Conv2d (...),
norm_layer (...),
nn . ReLU ()]
if use_dropout :
conv_block += [ nn . Dropout (...)]
conv_block += [ nn . Conv2d (...),
norm_layer (...)]
return nn . Sequential ( * conv_block )
def forward ( self , x ):
out = x + self . conv_block ( x )
return outHier wurde die Übertragungsverbindung eines Resnet -Blocks direkt im Vorwärtspass implementiert. Pytorch ermöglicht dynamische Operationen während des Vorwärtspasses.
Für ein Netzwerk, das mehrere Outputs erfordert, z. B. einen Wahrnehmungsverlust mithilfe eines vorbereiteten VGG -Netzwerks, verwenden wir das folgende Muster:
class Vgg19 ( nn . Module ):
def __init__ ( self , requires_grad = False ):
super ( Vgg19 , self ). __init__ ()
vgg_pretrained_features = models . vgg19 ( pretrained = True ). features
self . slice1 = torch . nn . Sequential ()
self . slice2 = torch . nn . Sequential ()
self . slice3 = torch . nn . Sequential ()
for x in range ( 7 ):
self . slice1 . add_module ( str ( x ), vgg_pretrained_features [ x ])
for x in range ( 7 , 21 ):
self . slice2 . add_module ( str ( x ), vgg_pretrained_features [ x ])
for x in range ( 21 , 30 ):
self . slice3 . add_module ( str ( x ), vgg_pretrained_features [ x ])
if not requires_grad :
for param in self . parameters ():
param . requires_grad = False
def forward ( self , x ):
h_relu1 = self . slice1 ( x )
h_relu2 = self . slice2 ( h_relu1 )
h_relu3 = self . slice3 ( h_relu2 )
out = [ h_relu1 , h_relu2 , h_relu3 ]
return outBeachten Sie hier Folgendes:
Auch wenn Pytorch bereits eine Menge Standardverlustfunktion hat, kann es manchmal erforderlich sein, Ihre eigene Verlustfunktion zu erstellen. Erstellen Sie hierfür eine separate losses.py nn.Module
class CustomLoss ( nn . Module ):
def __init__ ( self ):
super ( CustomLoss , self ). __init__ ()
def forward ( self , x , y ):
loss = torch . mean (( x - y ) ** 2 )
return loss Ein vollständiges Beispiel finden Sie im Ordner CIFAR10-Example dieses Repositorys.
Beachten Sie, dass wir die folgenden Muster verwendet haben:
# import statements
import torch
import torch . nn as nn
from torch . utils import data
...
# set flags / seeds
torch . backends . cudnn . benchmark = True
np . random . seed ( 1 )
torch . manual_seed ( 1 )
torch . cuda . manual_seed ( 1 )
...
# Start with main code
if __name__ == '__main__' :
# argparse for additional flags for experiment
parser = argparse . ArgumentParser ( description = "Train a network for ..." )
...
opt = parser . parse_args ()
# add code for datasets (we always use train and validation/ test set)
data_transforms = transforms . Compose ([
transforms . Resize (( opt . img_size , opt . img_size )),
transforms . RandomHorizontalFlip (),
transforms . ToTensor (),
transforms . Normalize (( 0.5 , 0.5 , 0.5 ), ( 0.5 , 0.5 , 0.5 ))
])
train_dataset = datasets . ImageFolder (
root = os . path . join ( opt . path_to_data , "train" ),
transform = data_transforms )
train_data_loader = data . DataLoader ( train_dataset , ...)
test_dataset = datasets . ImageFolder (
root = os . path . join ( opt . path_to_data , "test" ),
transform = data_transforms )
test_data_loader = data . DataLoader ( test_dataset ...)
...
# instantiate network (which has been imported from *networks.py*)
net = MyNetwork (...)
...
# create losses (criterion in pytorch)
criterion_L1 = torch . nn . L1Loss ()
...
# if running on GPU and we want to use cuda move model there
use_cuda = torch . cuda . is_available ()
if use_cuda :
net = net . cuda ()
...
# create optimizers
optim = torch . optim . Adam ( net . parameters (), lr = opt . lr )
...
# load checkpoint if needed/ wanted
start_n_iter = 0
start_epoch = 0
if opt . resume :
ckpt = load_checkpoint ( opt . path_to_checkpoint ) # custom method for loading last checkpoint
net . load_state_dict ( ckpt [ 'net' ])
start_epoch = ckpt [ 'epoch' ]
start_n_iter = ckpt [ 'n_iter' ]
optim . load_state_dict ( ckpt [ 'optim' ])
print ( "last checkpoint restored" )
...
# if we want to run experiment on multiple GPUs we move the models there
net = torch . nn . DataParallel ( net )
...
# typically we use tensorboardX to keep track of experiments
writer = SummaryWriter (...)
# now we start the main loop
n_iter = start_n_iter
for epoch in range ( start_epoch , opt . epochs ):
# set models to train mode
net . train ()
...
# use prefetch_generator and tqdm for iterating through data
pbar = tqdm ( enumerate ( BackgroundGenerator ( train_data_loader , ...)),
total = len ( train_data_loader ))
start_time = time . time ()
# for loop going through dataset
for i , data in pbar :
# data preparation
img , label = data
if use_cuda :
img = img . cuda ()
label = label . cuda ()
...
# It's very good practice to keep track of preparation time and computation time using tqdm to find any issues in your dataloader
prepare_time = start_time - time . time ()
# forward and backward pass
optim . zero_grad ()
...
loss . backward ()
optim . step ()
...
# udpate tensorboardX
writer . add_scalar (..., n_iter )
...
# compute computation time and *compute_efficiency*
process_time = start_time - time . time () - prepare_time
pbar . set_description ( "Compute efficiency: {:.2f}, epoch: {}/{}:" . format (
process_time / ( process_time + prepare_time ), epoch , opt . epochs ))
start_time = time . time ()
# maybe do a test pass every x epochs
if epoch % x == x - 1 :
# bring models to evaluation mode
net . eval ()
...
#do some tests
pbar = tqdm ( enumerate ( BackgroundGenerator ( test_data_loader , ...)),
total = len ( test_data_loader ))
for i , data in pbar :
...
# save checkpoint if needed
...Es gibt zwei unterschiedliche Muster in Pytorch, um mehrere GPUs für das Training zu verwenden. Aus unserer Erfahrung sind beide Muster gültig. Der erste ergibt sich jedoch in schöner und weniger Code. Der zweite scheint aufgrund der weniger Kommunikation zwischen dem GPUs einen leichten Leistungsvorteil zu haben. Ich stellte eine Frage im offiziellen Pytorch -Forum zu den beiden Ansätzen hier
Am häufigsten ist es, die Chargen aller Netzwerke einfach in den einzelnen GPUs aufzuteilen.
Ein Modell, das auf 1 GPU mit Stapelgröße 64 ausgeführt wird, würde daher mit jeweils 2 GPUs mit einer Stapelgröße von 32 ausgeführt. Dies kann automatisch durchgeführt werden, indem das Modell von nn.dataparallel (Modell) einwickelt wird.
Dieses Muster wird weniger häufig verwendet. Ein Repository, der diesen Ansatz implementiert
Numpy läuft auf der CPU und ist langsamer als Fackelcode. Seit T Torch entwickelt wurde, wobei Numpy im Kopf ähnlich ist, werden die meisten Numpy -Funktionen bereits von Pytorch unterstützt.
Die Datenladepipeline sollte unabhängig von Ihrem Haupttrainingscode sein. Pytorch verwendet Hintergrundarbeiter, um die Daten effizienter und ohne Störung des Haupttrainingsprozesses zu beladen.
Normalerweise trainieren wir unsere Modelle für Tausende von Schritten. Daher reicht es aus, den Verlust und andere Ergebnisse in jedem N'th -Schritt zu protokollieren, um den Overhead zu verringern. Insbesondere das Speichern von Vermittlungsgebern als Bilder kann während des Trainings kostspielig sein.
Es ist sehr praktisch, Befehlszeilenargumente zu verwenden, um Parameter während der Codeausführung ( Stapelgröße , Lernrate usw.) festzulegen. Eine einfache Möglichkeit, die Argumente für ein Experiment im Auge zu behalten, besteht darin, das von Parse_ARGS erhaltene Wörterbuch nur zu drucken:
...
# saves arguments to config.txt file
opt = parser . parse_args ()
with open ( "config.txt" , "w" ) as f :
f . write ( opt . __str__ ())
...Pytorch verfolgt aller Vorgänge mit Tensoren zur automatischen Differenzierung. Verwenden Sie .Detach (), um die Aufzeichnung unnötiger Vorgänge zu verhindern.
Sie können Variablen direkt drucken, es wird jedoch empfohlen, Variable zu verwenden. Detach () oder Variable.Item () . In früheren Pytorch -Versionen <0,4 müssen Sie .Data verwenden, um auf den Tensor einer Variablen zuzugreifen.
Die beiden Möglichkeiten sind nicht identisch, wie in einem der Probleme hier ausgewiesen wurde:
output = self . net . forward ( input )
# they are not equal!
output = self . net ( input )Wir empfehlen, die folgenden Samen zu Beginn Ihres Codes festzulegen:
np . random . seed ( 1 )
torch . manual_seed ( 1 )
torch . cuda . manual_seed ( 1 )Bei NVIDIA GPUs können Sie zu Beginn unseres Code die folgende Zeile hinzufügen. Dadurch kann das CUDA -Backend Ihr Diagramm während seiner ersten Ausführung optimieren. Beachten Sie jedoch, dass das Diagramm bei der Änderung der Network -Eingangs-/Ausgabe -Tensorgröße jedes Mal optimiert wird, wenn eine Änderung auftritt. Dies kann zu einer sehr langsamen Laufzeit und zu den Speicherfehlern führen. Stellen Sie dieses Flag nur ein, wenn Ihre Eingabe und Ausgabe immer die gleiche Form haben. Normalerweise führt dies zu einer Verbesserung von etwa 20%.
torch . backends . cudnn . benchmark = TrueEs hängt von der verwendeten Maschine, der Vorverarbeitungspipeline und der Netzwerkgröße ab. Auf einer SSD auf einer 1080ti -GPU sehen wir eine Recheneffizienz von fast 1,0, was ein ideales Szenario ist. Wenn flache (kleine) Netzwerke oder ein langsamer Hartharnstoff verwendet werden, kann die Zahl je nach Ihrem Setup auf etwa 0,1-0,2 fallen.
In Pytorch können wir sehr leicht virtuelle Stapelgrößen implementieren. Wir verhindern nur, dass der Optimierer eine Aktualisierung der Parameter vornimmt und die Gradienten für batch_size -Zyklen zusammenfasst.
...
# in the main loop
out = net ( input )
loss = criterion ( out , label )
# we just call backward to sum up gradients but don't perform step here
loss . backward ()
total_loss += loss . item () / batch_size
if n_iter % batch_size == batch_size - 1 :
# here we perform out optimization step using a virtual batch size
optim . step ()
optim . zero_grad ()
print ( 'Total loss: ' , total_loss )
total_loss = 0.0
...Wir können auf die Lernrate direkt mit dem hier gezeigten sofortigen Optimierer zugreifen: hier:
...
for param_group in optim . param_groups :
old_lr = param_group [ 'lr' ]
new_lr = old_lr * 0.1
param_group [ 'lr' ] = new_lr
print ( 'Updated lr from {} to {}' . format ( old_lr , new_lr ))
...Wenn Sie ein vorgezogenes Modell wie VGG verwenden möchten, um einen Verlust zu berechnen, aber nicht trainieren (z. B. Wahrnehmungsverlust bei Style-Transfer/ Gans/ Auto-Coder), können Sie das folgende Muster verwenden:
...
# instantiate the model
pretrained_VGG = VGG19 (...)
# disable gradients (prevent training)
for p in pretrained_VGG . parameters (): # reset requires_grad
p . requires_grad = False
...
# you don't have to use the no_grad() namespace but can just run the model
# no gradients will be computed for the VGG model
out_real = pretrained_VGG ( input_a )
out_fake = pretrained_VGG ( input_b )
loss = any_criterion ( out_real , out_fake )
...Diese Methoden werden verwendet, um Ebenen wie BatchNORM2D oder Dropout2D vom Training in den Inferenzmodus festzulegen. Jedes Modul, das von Nn.Module erbt, hat ein Attribut, das als Istraining bezeichnet wird. .eval () und .Train () legt dieses Attribut einfach auf wahr/ false fest. Weitere Informationen darüber, wie diese Methode implementiert wird
Stellen Sie sicher, dass während Ihrer Codeausführung keine Gradienten berechnet und gespeichert werden. Sie können einfach das folgende Muster verwenden, um das sicherzustellen:
with torch . no_grad ():
# run model here
out_tensor = net ( in_tensor )In Pytorch können Sie Schichten einfrieren. Dies verhindert, dass sie während eines Optimierungsschritts aktualisiert werden.
# you can freeze whole modules using
for p in pretrained_VGG . parameters (): # reset requires_grad
p . requires_grad = FalseSeit Pytorch 0,4 * Variable und Tensor wurden zusammengeführt. Wir müssen nicht mehr explizit ein variables Objekt erstellen.
Die C ++ - Version ist ungefähr 10% schneller
Todo ...
Aus unserer Erfahrung können Sie etwa 20% beschleunigen. Wenn Sie Ihr Modell zum ersten Mal ausführen, dauert es einige Zeit, um das optimierte Diagramm zu erstellen. In einigen Fällen (Schleifen im Vorwärtspass, keine feste Eingangsform, wenn/sonst in usw.) kann dieses Flag zu einem Speicher oder anderen Fehlern führen.
Todo ...
Wenn ein Tensor aus einem Berechnungsdiagramm befreit wird. Eine schöne Illustration wird hier gezeigt
Bitte geben Sie Feedback darüber, wie wir diesen Stilführer verbessern können! Sie können ein Problem öffnen oder Änderungen vorschlagen, indem Sie eine Pull -Anfrage erstellen.
Wenn Sie dieses Repo mögen, vergessen Sie nicht, andere Frameworks von uns zu überprüfen: