Esta no es una guía de estilo oficial para Pytorch. Este documento resume las mejores prácticas de más de un año de experiencia con el aprendizaje profundo utilizando el marco de Pytorch. Tenga en cuenta que los aprendizajes que compartimos provienen principalmente de una perspectiva de investigación y inicio.
Este es un proyecto abierto y otros colaboradores son muy bienvenidos para editar y mejorar el documento.
Encontrará tres partes principales de este documento. Primero, un resumen rápido de las mejores prácticas en Python, seguido de algunos consejos y recomendaciones utilizando Pytorch. Finalmente, compartimos algunas ideas y experiencias utilizando otros marcos que nos ayudaron generalmente a mejorar nuestro flujo de trabajo.
Actualización 20.12.2020
Actualización 30.4.2019
Después de tanta retroalimentación positiva, también agregué un resumen de los bloques de construcción de nuestros proyectos de uso común a la ligera: encontrará bloques de construcción para (autoatición, pérdida perceptiva con VGG, normalización espectral, normalización de instancias adaptativas, ...)
Fragmentos de código para pérdidas, capas y otros bloques de construcción
De nuestra experiencia, recomendamos usar Python 3.6+ debido a las siguientes características que se volvieron muy útiles para un código limpio y simple:
Tratamos de seguir el Google Style Guide para Python. Consulte la guía de estilo bien documentada sobre el código de Python proporcionado por Google.
Proporcionamos aquí un resumen de las reglas más utilizadas:
De 3.16.4
| Tipo | Convención | Ejemplo |
|---|---|---|
| Paquetes y módulos | Lower_with_under | Desde prefetch_generator importar backgroundGenerator |
| Clases | Vaporizaciones | dataloader de clase |
| Constantes | Caps_with_under | Batch_size = 16 |
| Instancias | Lower_with_under | DataSet = DataSet |
| Métodos y funciones | Lower_with_under () | Def Visualize_tensor () |
| Variables | Lower_with_under | background_color = 'Blue' |
En general, recomendamos el uso de un IDE como Visual Studio Code o Pycharm. Mientras que VS Code proporciona resaltamiento de sintaxis y autocompletación en un editor relativamente ligero, Pycharm tiene muchas características avanzadas para trabajar con clústeres remotos. VS Code se ha vuelto muy poderoso con su ecosistema de extensiones de rápido crecimiento.
Asegúrese de tener las siguientes extensiones instaladas:
Si está configurado correctamente, esto le permite hacer lo siguiente:
En general, recomendamos usar cuadernos Jupyter para la exploración/ reproducción inicial con nuevos modelos y código. Los scripts de Python deben usarse tan pronto como desee entrenar el modelo en un conjunto de datos más grande donde la reproducibilidad también es más importante.
Nuestro flujo de trabajo recomendado:
| Cuaderno de jupyter | Guiones de Python |
|---|---|
| + Exploración | + Ejecutar trabajos más largos sin interrupción |
| + Depuración | + Fácil de rastrear los cambios con Git |
| - puede convertirse en un archivo enorme | - La depuración significa principalmente volver a ejecutar todo el guión |
| - se puede interrumpir (no se use para un largo entrenamiento) | |
| - propenso a los errores y convertirse en un desastre |
Bibliotecas de uso común:
| Nombre | Descripción | Utilizado para |
|---|---|---|
| antorcha | Marco base para trabajar con redes neuronales | crear tensores, redes y capacitarlos utilizando backprop |
| vía antorcha | Módulos de visión por computadora de Pytorch | Preprocesamiento de datos de imágenes, aumento, posprocesamiento |
| Almohada (pil) | Biblioteca de imágenes de Python | Cargar imágenes y almacenarlas |
| Numpy | Paquete para computación científica con Python | Preprocesamiento de datos y posprocesamiento |
| prefetch_generator | Biblioteca para el procesamiento de fondo | Cargando el siguiente lote en el fondo durante el cálculo |
| TQDM | Barra de progreso | Progreso durante el entrenamiento de cada época |
| antorchinfo | Imprimir resumen del modelo similar a Keras para Pytorch | Muestra red, sus parámetros y tamaños en cada capa |
| torch.utils.tensorboard | Tensorboard dentro de Pytorch | Experimentos de registro y mostrándolos en TensorBoard |
No ponga todas las capas y modelos en el mismo archivo. Una mejor práctica es separar las redes finales en un archivo separado ( networks.py ) y mantener las capas, las pérdidas y los operaciones en los archivos respectivos ( capas.py , losses.py , ops.py ). El modelo terminado (compuesto por una o múltiples redes) debe ser referencia en un archivo con su nombre (por ejemplo, yolov3.py , dcgan.py )
La rutina principal, respectiva, los scripts de tren y prueba solo deben importar desde el archivo que tiene el nombre del modelo.
Recomendamos romper la red en sus piezas reutilizables más pequeñas. Una red es un nn.module que consta de operaciones u otros modificadores de nn.module como bloques de construcción. Las funciones de pérdida también son nn.module y, por lo tanto, pueden integrarse directamente en la red.
Una clase que hereda de nn.module debe tener un método directo que implementa el pase hacia adelante de la capa u operación respectiva.
Se puede usar un nn.module en datos de entrada utilizando self.net (entrada) . Esto simplemente usa el método de llamada () del objeto para alimentar la entrada a través del módulo.
output = self . net ( input )Use el siguiente patrón para redes simples con una sola entrada y salida única:
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 )Tenga en cuenta lo siguiente:
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 outAquí la conexión de omisión de un bloque RESNET se ha implementado directamente en el pase de avance. Pytorch permite operaciones dinámicas durante el pase hacia adelante.
Para una red que requiere múltiples salidas, como construir una pérdida perceptiva utilizando una red VGG previa a la aparición, utilizamos el siguiente patrón:
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 outTenga en cuenta aquí lo siguiente:
Incluso si Pytorch ya tiene mucha función de pérdida estándar, a veces podría ser necesario crear su propia función de pérdida. Para esto, cree una losses.py de archivo separada.py y extienda la clase nn.Module para crear su función de pérdida personalizada:
class CustomLoss ( nn . Module ):
def __init__ ( self ):
super ( CustomLoss , self ). __init__ ()
def forward ( self , x , y ):
loss = torch . mean (( x - y ) ** 2 )
return loss Se proporciona un ejemplo completo en la carpeta CIFAR10-Ejemplo de este repositorio.
Tenga en cuenta que usamos los siguientes patrones:
# 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
...Hay dos patrones distintos en Pytorch para usar múltiples GPU para el entrenamiento. A partir de nuestra experiencia, ambos patrones son válidos. Sin embargo, el primero resulta en el mejor y menos código. El segundo parece tener una ligera ventaja de rendimiento debido a una menor comunicación entre las GPU. Hice una pregunta en el foro oficial de Pytorch sobre los dos enfoques aquí.
El más común es simplemente dividir los lotes de todas las redes a las GPU individuales.
Un modelo que se ejecuta en 1 GPU con el tamaño de lotes 64, por lo tanto, se ejecutaría en 2 GPU con cada una de un tamaño de lote de 32. Esto se puede hacer automáticamente envolviendo el modelo por Nn.DataparLel (modelo) .
Este patrón se usa menos comúnmente. A NVIDIA se muestra un repositorio que implementa este enfoque en la implementación PIX2PIXHD por NVIDIA
Numpy funciona en la CPU y es más lento que el código de antorcha. Dado que la antorcha se ha desarrollado con ser similar a Numpy en mente, la mayoría de las funciones numpy ya están respaldadas por Pytorch.
La tubería de carga de datos debe ser independiente de su código de entrenamiento principal. Pytorch utiliza trabajadores de fondo para cargar los datos de manera más eficiente y sin alterar el principal proceso de capacitación.
Por lo general, entrenamos nuestros modelos para miles de pasos. Por lo tanto, es suficiente registrar la pérdida y otros resultados cada paso para reducir la sobrecarga. Especialmente, ahorrar resultados intermediarios como imágenes puede ser costoso durante la capacitación.
Es muy útil usar argumentos de línea de comandos para establecer parámetros durante la ejecución del código ( tamaño de lote , tasa de aprendizaje , etc.). Una manera fácil de realizar un seguimiento de los argumentos para un experimento es simplemente imprimir el diccionario recibido 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 realiza un seguimiento de todas las operaciones que involucran tensores para la diferenciación automática. Use .Detach () para evitar la grabación de operaciones innecesarias.
Puede imprimir variables directamente, sin embargo, se recomienda usar variable.detach () o variable.item () . En las versiones anteriores de Pytorch <0.4 debe usar .data para acceder al tensor de una variable.
Las dos formas no son idénticas como se señala en uno de los problemas aquí:
output = self . net . forward ( input )
# they are not equal!
output = self . net ( input )Recomendamos establecer las siguientes semillas al comienzo de su código:
np . random . seed ( 1 )
torch . manual_seed ( 1 )
torch . cuda . manual_seed ( 1 )En las GPU de NVIDIA puede agregar la siguiente línea al comienzo de nuestro código. Esto permitirá que el backend de CUDA optimice su gráfico durante su primera ejecución. Sin embargo, tenga en cuenta que si cambia el tamaño del tensor de entrada/salida de red, el gráfico se optimizará cada vez que ocurra un cambio. Esto puede conducir a un tiempo de ejecución muy lento y errores fuera de memoria. Solo configure este indicador si su entrada y salida siempre tienen la misma forma. Por lo general, esto da como resultado una mejora de aproximadamente el 20%.
torch . backends . cudnn . benchmark = TrueDepende de la máquina utilizada, la tubería de preprocesamiento y el tamaño de la red. Ejecutando en un SSD en una GPU 1080TI, vemos una eficiencia de cálculo de casi 1.0, que es un escenario ideal. Si se usa redes superficiales (pequeñas) o un disco duro lento, el número puede caer a alrededor de 0.1-0.2 dependiendo de su configuración.
En Pytorch podemos implementar muy fácilmente tamaños de lotes virtuales. Simplemente evitamos que el optimizador realice una actualización de los parámetros y resumamos los gradientes para los ciclos 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
...Podemos acceder a la tasa de aprendizaje directamente usando el optimizador instanciado como se muestra aquí:
...
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 desea utilizar un modelo previo al estado previo como VGG para calcular una pérdida pero no entrenarla (por ejemplo, pérdida perceptiva en estilo-transferente/ Gans/ auto-ender), puede usar el siguiente patrón:
...
# 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 )
...Esos métodos se utilizan para establecer capas como BatchNorm2D o Drout2D de entrenamiento a modo de inferencia. Cada módulo que hereda de nn.module tiene un atributo llamado Istraining . .Eval () y .train () simplemente establece este atributo en True/ False. Para obtener más información sobre cómo se implementa este método, eche un vistazo al código del módulo en Pytorch
Asegúrese de que no se calculen y almacenen gradientes durante la ejecución de su código. Simplemente puede usar el siguiente patrón para asegurar que:
with torch . no_grad ():
# run model here
out_tensor = net ( in_tensor )En Pytorch puedes congelar capas. Esto evitará que se actualicen durante un paso de optimización.
# you can freeze whole modules using
for p in pretrained_VGG . parameters (): # reset requires_grad
p . requires_grad = FalseDesde Pytorch 0.4 * variable y tensor se han fusionado. Ya no tenemos que crear explícitamente un objeto variable .
La versión C ++ es aproximadamente un 10% más rápida
Hacer...
Desde nuestra experiencia, puede obtener alrededor del 20% de aceleración. Pero la primera vez que ejecuta su modelo, lleva bastante tiempo construir el gráfico optimizado. En algunos casos (bucles en el pase hacia adelante, no hay forma de entrada fija, si/else en adelante, etc.) Este indicador podría resultar en la memoria u otros errores.
Hacer...
Si libera un tensor de un gráfico de cálculo. Una buena ilustración se muestra aquí
¡Da comentarios sobre cómo podemos mejorar esta guía de estilo! Puede abrir un problema o proponer cambios creando una solicitud de extracción.
Si te gusta este repositorio, no olvides consultar otros marcos de nosotros: