Ce n'est pas un guide de style officiel pour Pytorch. Ce document résume les meilleures pratiques de plus d'une année d'expérience avec l'apprentissage en profondeur à l'aide du cadre Pytorch. Notez que les apprentissages que nous partageons proviennent principalement d'un point de vue de la recherche et des startups.
Il s'agit d'un projet ouvert et d'autres collaborateurs sont très bien accueillis pour modifier et améliorer le document.
Vous trouverez trois parties principales de ce doc. Tout d'abord, un récapitulatif rapide des meilleures pratiques dans Python, suivi de quelques conseils et recommandations utilisant Pytorch. Enfin, nous partageons quelques idées et expériences en utilisant d'autres cadres qui nous ont aidés à améliorer généralement notre flux de travail.
Mise à jour 20.12.2020
Mise à jour 30.4.2019
Après tant de commentaires positifs, j'ai également ajouté à la légère un résumé des éléments constitutifs couramment utilisés de nos projets: vous trouverez des blocs de construction (auto-agencement, perte perceptuelle en utilisant VGG, normalisation spectrale, normalisation des instances adaptatives, ...)
Extraits de code pour les pertes, les couches et autres blocs de construction
D'après notre expérience, nous vous recommandons d'utiliser Python 3.6+ en raison des fonctionnalités suivantes qui sont devenues très pratiques pour un code propre et simple:
Nous essayons de suivre le Google StyleGuide pour Python. Veuillez vous référer au guide de style bien documenté sur le code Python fourni par Google.
Nous fournissons ici un résumé des règles les plus couramment utilisées:
À partir de 3.16.4
| Taper | Convention | Exemple |
|---|---|---|
| Packages et modules | inférieur_with_under | de Prefetch_Generator Import BackgroundGenerator |
| Classes | Capuchon | coader de la classe |
| Constantes | Caps_with_under | Batch_size = 16 |
| Instances | inférieur_with_under | ensemble de données = ensemble de données |
| Méthodes et fonctions | Lower_With_under () | def visualize_tensor () |
| Variables | inférieur_with_under | background_color = 'bleu' |
En général, nous recommandons l'utilisation d'un IDE tel que Visual Studio Code ou PyCharm. Tandis que le code VS fournit la mise en évidence de la syntaxe et la complétion automatique dans un éditeur relativement léger, PyCharm a de nombreuses fonctionnalités avancées pour travailler avec des clusters distants. VS Code est devenu très puissant avec son écosystème d'extensions à croissance rapide.
Assurez-vous que les extensions suivantes sont installées:
Si vous configurez correctement, cela vous permet de faire ce qui suit:
En général, nous vous recommandons d'utiliser des cahiers Jupyter pour l'exploration / jouer initial avec de nouveaux modèles et code. Les scripts Python doivent être utilisés dès que vous souhaitez former le modèle sur un ensemble de données plus important où la reproductibilité est également plus importante.
Notre flux de travail recommandé:
| Cahier de jupyter | Scripts python |
|---|---|
| + Exploration | + Exécuter des travaux plus longs sans interruption |
| + Débogage | + Modifications faciles à suivre avec Git |
| - peut devenir un énorme fichier | - Le débogage signifie principalement relancer l'ensemble du script |
| - peut être interrompu (ne pas utiliser pour une formation longue) | |
| - sujet aux erreurs et devenir un gâchis |
Bibliothèques couramment utilisées:
| Nom | Description | Utilisé pour |
|---|---|---|
| torche | Cadre de base pour travailler avec les réseaux de neurones | Créer des tenseurs, les réseaux et les former à l'aide d'un rétroprop |
| torchion | Modules de vision informatique pytorch | Prétraitement des données d'image, augmentation, post-traitement |
| Oreiller (PIL) | Bibliothèque d'imagerie Python | Chargement des images et les stocker |
| Nombant | Package pour l'informatique scientifique avec Python | Prétraitement des données et post-traitement |
| prefetch_generator | Bibliothèque pour le traitement des antécédents | Chargement du prochain lot en arrière-plan pendant le calcul |
| tqdm | Barre de progression | Progrès pendant la formation de chaque époque |
| torchinfo | Résumé du modèle imprimé de type Keras pour Pytorch | Affiche le réseau, ses paramètres et tailles à chaque couche |
| torch.utils.tensorboard | Tensorboard dans Pytorch | Exploitation des expériences et les montrer dans Tensorboard |
Ne mettez pas toutes les couches et modèles dans le même fichier. Une meilleure pratique consiste à séparer les réseaux finaux en un fichier séparé ( Networks.py ) et à conserver les couches, les pertes et les opérations dans les fichiers respectifs ( couches.py , pertes.py , ops.py ). Le modèle fini (composé d'un ou de plusieurs réseaux) doit être référence dans un fichier avec son nom (par exemple yolov3.py , dcgan.py )
La routine principale, respective les scripts de train et de test ne doit importer que le fichier ayant le nom du modèle.
Nous vous recommandons de briser le réseau en ses plus petites pièces réutilisables. Un réseau est un nn.module composé d'opérations ou d'autres nn.modules comme blocs de construction. Les fonctions de perte sont également nn.module et peuvent donc être directement intégrées dans le réseau.
Une classe héritée de NN.Module doit avoir une méthode avant implémentant la passe directe de la couche ou de l'opération respective.
Un module NN. peut être utilisé sur les données d'entrée à l'aide de self.net (entrée) . Cela utilise simplement la méthode Call () de l'objet pour nourrir l'entrée via le module.
output = self . net ( input )Utilisez le modèle suivant pour des réseaux simples avec une seule entrée et une seule sortie:
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 )Notez ce qui suit:
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 outIci, la connexion SKIP d'un bloc Resnet a été implémentée directement dans la passe avant. Pytorch permet des opérations dynamiques pendant la passe avant.
Pour un réseau nécessitant plusieurs sorties, comme la construction d'une perte perceptuelle à l'aide d'un réseau VGG pré-entraîné, nous utilisons le modèle suivant:
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 outRemarque ici ce qui suit:
Même si Pytorch a déjà une grande partie de la fonction de perte standard, il pourrait parfois être nécessaire de créer votre propre fonction de perte. Pour cela, créez un fichier séparé losses.py et étendez la classe nn.Module pour créer votre fonction de perte personnalisée:
class CustomLoss ( nn . Module ):
def __init__ ( self ):
super ( CustomLoss , self ). __init__ ()
def forward ( self , x , y ):
loss = torch . mean (( x - y ) ** 2 )
return loss Un exemple complet est fourni dans le dossier CIFAR10-Exemple de ce référentiel.
Notez que nous avons utilisé les modèles suivants:
# 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
...Il existe deux modèles distincts dans Pytorch pour utiliser plusieurs GPU pour la formation. D'après notre expérience, les deux modèles sont valables. Le premier en résulte cependant dans un code plus agréable et moins. Le second semble avoir un léger avantage de performance en raison de moins de communication entre les GPU. J'ai posé une question dans le forum officiel de Pytorch sur les deux approches ici
Le plus courant est de simplement diviser les lots de tous les réseaux aux GPU individuels.
Un modèle fonctionnant sur 1 GPU avec une taille de lot 64 fonctionnerait donc sur 2 GPU avec chacun une taille de lot de 32 .
Ce modèle est moins couramment utilisé. Un référentiel implémentant cette approche est présenté ici dans l'implémentation PIX2PIXHD par NVIDIA
Numpy fonctionne sur le CPU et est plus lent que le code de torche. Depuis que Torch a été développé en étant similaire à Numpy à l'esprit que la plupart des fonctions Numpy sont déjà soutenues par Pytorch.
Le pipeline de chargement des données doit être indépendant de votre code de formation principal. Pytorch utilise des travailleurs d'arrière-plan pour charger les données plus efficacement et sans perturber le processus de formation principal.
En règle générale, nous formons nos modèles pour des milliers de pas. Par conséquent, il suffit de journaliser la perte et d'autres résultats à chaque étape pour réduire les frais généraux. Surtout, économiser les résultats intermédiaires en tant qu'images peut être coûteux pendant la formation.
Il est très pratique d'utiliser des arguments de ligne de commande pour définir des paramètres pendant l'exécution du code ( taille du lot , taux d'apprentissage , etc.). Un moyen facile de garder une trace des arguments pour une expérience est de simplement imprimer le dictionnaire reçu de Parse_args :
...
# saves arguments to config.txt file
opt = parser . parse_args ()
with open ( "config.txt" , "w" ) as f :
f . write ( opt . __str__ ())
...Pytorch garde une trace de toutes les opérations impliquant des tenseurs pour la différenciation automatique. Utilisez .detach () pour empêcher l'enregistrement des opérations inutiles.
Vous pouvez imprimer directement les variables, mais il est recommandé d'utiliser variable.detach () ou variable.item () . Dans les versions Pytorch antérieures <0,4, vous devez utiliser .data pour accéder au tenseur d'une variable.
Les deux manières ne sont pas identiques comme indiquées dans l'un des problèmes ici:
output = self . net . forward ( input )
# they are not equal!
output = self . net ( input )Nous vous recommandons de définir les graines suivantes au début de votre code:
np . random . seed ( 1 )
torch . manual_seed ( 1 )
torch . cuda . manual_seed ( 1 )Sur les GPU nvidia, vous pouvez ajouter la ligne suivante au début de notre code. Cela permettra au backend CUDA d'optimiser votre graphique lors de sa première exécution. Cependant, sachez que si vous modifiez la taille du tenseur d'entrée / sortie réseau, le graphique sera optimisé chaque fois qu'un changement se produit. Cela peut conduire à des erreurs d'exécution très lentes et hors de la mémoire. Définissez ce drapeau si votre entrée et votre sortie ont toujours la même forme. Habituellement, cela entraîne une amélioration d'environ 20%.
torch . backends . cudnn . benchmark = TrueCela dépend de la machine utilisée, du pipeline de prétraitement et de la taille du réseau. En fonctionnant sur un SSD sur un GPU 1080ti, nous voyons une efficacité de calcul de près de 1,0, ce qui est un scénario idéal. Si des réseaux peu profonds (petits) ou un disque dur lent sont utilisés, le nombre peut chuter à environ 0,1 à 0,2 selon votre configuration.
Dans Pytorch, nous pouvons implémenter des tailles de lots très facilement virtuelles. Nous empêchons simplement l'optimiseur de faire une mise à jour des paramètres et de résumer les gradients des cycles Batch_Size .
...
# 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
...Nous pouvons accéder au taux d'apprentissage directement en utilisant l'optimiseur instancié comme indiqué ici:
...
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 ))
...Si vous souhaitez utiliser un modèle pré-entraîné tel que VGG pour calculer une perte mais pas le former (par exemple, une perte perceptuelle dans le transfert de style / gans / auto-encodeur), vous pouvez utiliser le modèle suivant:
...
# 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 )
...Ces méthodes sont utilisées pour définir des couches telles que BatchNorm2D ou Dropout2D de la formation au mode d'inférence. Chaque module qui hérite de Nn.module a un attribut appelé istraining . .Eval () et .train () définit simplement cet attribut à True / False. Pour plus d'informations sur la façon dont cette méthode est mise en œuvre, veuillez consulter le code du module dans Pytorch
Assurez-vous qu'aucun dégradé n'est calculé et stocké lors de l'exécution de votre code. Vous pouvez simplement utiliser le modèle suivant pour vous assurer:
with torch . no_grad ():
# run model here
out_tensor = net ( in_tensor )À Pytorch, vous pouvez geler les couches. Cela les empêchera d'être mis à jour lors d'une étape d'optimisation.
# you can freeze whole modules using
for p in pretrained_VGG . parameters (): # reset requires_grad
p . requires_grad = FalseDepuis la variable Pytorch 0.4 * et le tenseur ont été fusionnés. Nous n'avons plus à créer explicitement un objet variable .
La version C ++ est environ 10% plus rapide
Faire...
D'après notre expérience, vous pouvez accélérer environ 20%. Mais la première fois que vous exécutez votre modèle, il faut un certain temps pour construire le graphique optimisé. Dans certains cas (boucles en passant avant, pas de forme d'entrée fixe, si / else en avant, etc.) Cet drapeau peut entraîner la mémoire ou d'autres erreurs.
Faire...
Si libère un tenseur d'un graphique de calcul. Une belle illustration est montrée ici
Veuillez donner des commentaires sur la façon dont nous pouvons améliorer ce guide de style! Vous pouvez ouvrir un problème ou proposer des modifications en créant une demande de traction.
Si vous aimez ce dépôt, n'oubliez pas de consulter d'autres frameworks de nous: