Grokay PyTorch

Hola Habr!

Tenemos en reserva un libro largamente esperado sobre la biblioteca PyTorch .



Como aprenderá todo el material básico necesario sobre PyTorch de este libro, le recordamos los beneficios de un proceso llamado "grokking" o "comprensión profunda" del tema que desea aprender. En la publicación de hoy, te diremos cómo Kai Arulkumaran golpeó a PyTorch (sin foto). Bienvenido a cat.

PyTorch es un marco de aprendizaje profundo flexible que distingue automáticamente entre objetos que usan redes neuronales dinámicas (es decir, redes que usan control de flujo dinámico, como if y bucles while). PyTorch admite aceleración de GPU, entrenamiento distribuido , varios tipos de optimización y muchas otras características interesantes. Aquí expuse algunas ideas sobre cómo, en mi opinión, debería usar PyTorch; Todos los aspectos de la biblioteca y las prácticas recomendadas no están cubiertos aquí, pero espero que este texto le sea útil.

Las redes neuronales son una subclase de gráficos computacionales. Los gráficos de computación reciben datos como entrada, luego estos datos se enrutan (y se pueden convertir) en los nodos donde se procesan. En el aprendizaje profundo, las neuronas (nodos) generalmente transforman los datos al aplicarles parámetros y funciones diferenciables, de modo que los parámetros pueden optimizarse para minimizar las pérdidas mediante el método de descenso de gradiente. En un sentido más amplio, observo que las funciones pueden ser estocásticas y dinámicas de gráficos. Por lo tanto, si bien las redes neuronales se ajustan bien al paradigma de programación de flujo de datos, la API PyTorch se enfoca en el paradigma de programación imperativo , y esta forma de interpretar los programas creados es mucho más familiar. Es por eso que el código PyTorch es más fácil de leer, es más fácil juzgar el diseño de programas complejos, lo que, sin embargo, no requiere un compromiso serio en el rendimiento: de hecho, PyTorch es lo suficientemente rápido y proporciona muchas optimizaciones de las que usted, como usuario final, no puede preocuparse en absoluto. (sin embargo, si está realmente interesado en ellos, puede profundizar un poco más y conocerlos).

El resto de este artículo es un análisis del ejemplo oficial en el conjunto de datos MNIST . Aquí jugamos PyTorch, por lo tanto, recomiendo entender el artículo solo después de conocer los manuales oficiales para principiantes . Por conveniencia, el código se presenta en forma de pequeños fragmentos equipados con comentarios, es decir, no se distribuye en funciones / archivos separados que está acostumbrado a ver en código modular puro.

Importaciones


 import argparse import os import torch from torch import nn, optim from torch.nn import functional as F from torch.utils.data import DataLoader from torchvision import datasets, transforms 

Todas estas son importaciones bastante estándar, con la excepción de los módulos de torchvision , que se utilizan especialmente activamente para resolver tareas relacionadas con la visión por computadora.

Personalización


 parser = argparse.ArgumentParser(description='PyTorch MNIST Example') parser.add_argument('--batch-size', type=int, default=64, metavar='N', help='input batch size for training (default: 64)') parser.add_argument('--epochs', type=int, default=10, metavar='N', help='number of epochs to train (default: 10)') parser.add_argument('--lr', type=float, default=0.01, metavar='LR', help='learning rate (default: 0.01)') parser.add_argument('--momentum', type=float, default=0.5, metavar='M', help='SGD momentum (default: 0.5)') parser.add_argument('--no-cuda', action='store_true', default=False, help='disables CUDA training') parser.add_argument('--seed', type=int, default=1, metavar='S', help='random seed (default: 1)') parser.add_argument('--save-interval', type=int, default=10, metavar='N', help='how many batches to wait before checkpointing') parser.add_argument('--resume', action='store_true', default=False, help='resume training from checkpoint') args = parser.parse_args() use_cuda = torch.cuda.is_available() and not args.no_cuda device = torch.device('cuda' if use_cuda else 'cpu') torch.manual_seed(args.seed) if use_cuda: torch.cuda.manual_seed(args.seed) 

argparse es la forma estándar de manejar argumentos de línea de comando en Python.

Si necesita escribir código diseñado para trabajar en diferentes dispositivos (usando aceleración de GPU, cuando está disponible, pero si no se torch.device a los cálculos en la CPU), seleccione y guarde el torch.device apropiado, con el que puede determinar dónde debe Los tensores se almacenan. Para obtener más información sobre cómo crear dicho código, consulte la documentación oficial . El enfoque de PyTorch es llevar la selección de dispositivos al control del usuario, lo que puede parecer indeseable en ejemplos simples. Sin embargo, este enfoque simplifica enormemente el trabajo cuando tiene que lidiar con tensores, lo que a) es conveniente para la depuración b) le permite usar dispositivos de manera efectiva de forma manual.

Para la reproducibilidad de los experimentos, debe establecer valores iniciales aleatorios para todos los componentes que usan generación de números aleatorios (incluidos random o numpy , si los usa numpy ). Tenga en cuenta: cuDNN utiliza algoritmos no deterministas y, opcionalmente, se desactiva mediante torch.backends.cudnn.enabled = False .

Datos


 data_path = os.path.join(os.path.expanduser('~'), '.torch', 'datasets', 'mnist') train_data = datasets.MNIST(data_path, train=True, download=True, transform=transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])) test_data = datasets.MNIST(data_path, train=False, transform=transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])) train_loader = DataLoader(train_data, batch_size=args.batch_size, shuffle=True, num_workers=4, pin_memory=True) test_loader = DataLoader(test_data, batch_size=args.batch_size, num_workers=4, pin_memory=True) 


Dado que los modelos de torchvision almacenan en ~/.torch/models/ , prefiero almacenar los conjuntos de torchvision torchvision en ~/.torch/datasets . Este es mi acuerdo de derechos de autor, pero es muy conveniente usarlo en proyectos desarrollados sobre la base de MNIST, CIFAR-10, etc. En general, los conjuntos de datos deben almacenarse por separado del código si tiene la intención de reutilizar múltiples conjuntos de datos.

torchvision.transforms contiene muchas opciones de conversión convenientes para imágenes individuales, como el recorte y la normalización.

Hay muchas opciones en el batch_size , pero además de batch_size y shuffle , también debe tener en cuenta num_workers y pin_memory , ayudan a aumentar la eficiencia. num_workers > 0 usa subprocesos para la carga de datos asíncrona, y no bloquea el proceso principal para esto. Un caso de uso típico es cargar datos (por ejemplo, imágenes) desde un disco y, posiblemente, convertirlos; Todo esto se puede hacer en paralelo, junto con el procesamiento de datos de red. Es posible que sea necesario ajustar el grado de procesamiento para a) minimizar la cantidad de trabajadores y, en consecuencia, la cantidad de CPU y RAM utilizada (cada trabajador carga un lote separado, en lugar de muestras individuales incluidas en el lote) b) minimizar el tiempo que los datos esperan en la red. pin_memory usa memoria fija (en lugar de paginada) para acelerar las operaciones de transferencia de datos desde la RAM a la GPU (y no hace nada con el código específico de la CPU).

Modelo


 class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(1, 10, kernel_size=5) self.conv2 = nn.Conv2d(10, 20, kernel_size=5) self.conv2_drop = nn.Dropout2d() self.fc1 = nn.Linear(320, 50) self.fc2 = nn.Linear(50, 10) def forward(self, x): x = F.relu(F.max_pool2d(self.conv1(x), 2)) x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2)) x = x.view(-1, 320) x = F.relu(self.fc1(x)) x = self.fc2(x) return F.log_softmax(x, dim=1) model = Net().to(device) optimiser = optim.SGD(model.parameters(), lr=args.lr, momentum=args.momentum) if args.resume: model.load_state_dict(torch.load('model.pth')) optimiser.load_state_dict(torch.load('optimiser.pth')) 

La inicialización de la red generalmente se extiende a las variables miembro, capas que contienen parámetros de aprendizaje y, posiblemente, parámetros de aprendizaje individuales y memorias intermedias no capacitadas. Luego, con un pase directo, se usan en combinación con funciones de F que son puramente funcionales y no contienen parámetros. A algunas personas les gusta trabajar con redes puramente funcionales (por ejemplo, mantener parámetros y usar F.conv2d lugar de nn.Conv2d ) o redes que consisten completamente en capas (por ejemplo, nn.ReLU lugar de F.relu ).

.to(device) es una forma conveniente de enviar parámetros (y memorias intermedias) del device a la GPU si el device configurado en GPU, porque de lo contrario (si el dispositivo está configurado en CPU) no se hará nada. Es importante transferir los parámetros del dispositivo al dispositivo apropiado antes de pasarlos al optimizador; de lo contrario, el optimizador no podrá rastrear los parámetros correctamente.

Tanto las redes neuronales ( nn.Module ) como los optimizadores ( optim.Optimizer ) pueden guardar y cargar su estado interno, y se recomienda hacer esto con .load_state_dict(state_dict) : es necesario volver a cargar el estado de ambos para reanudar la capacitación basada en diccionarios previamente guardados estados. Guardar todo el objeto puede estar lleno de errores . Si guardó los tensores en la GPU y desea cargarlos en la CPU u otra GPU, entonces la forma más fácil es cargarlos directamente en la CPU utilizando la opción map_location , por ejemplo, torch.load('model.pth' , map_location='cpu' ).

Aquí hay algunos otros puntos que no se muestran aquí, pero que vale la pena mencionar, que puede usar el flujo de control con un pase directo (por ejemplo, la ejecución de la if puede depender de la variable miembro o de los datos en sí. Además, es perfectamente aceptable emitir en medio del proceso ( print ) tensores, lo que simplifica enormemente la depuración. Finalmente, con un pase directo, se pueden usar muchos argumentos. Ilustraré este punto con una lista breve que no está vinculada a ninguna idea en particular:

 def forward(self, x, hx, drop=False): hx2 = self.rnn(x, hx) print(hx.mean().item(), hx.var().item()) if hx.max.item() > 10 or self.can_drop and drop: return hx else: return hx2 

Entrenamiento


 model.train() train_losses = [] for i, (data, target) in enumerate(train_loader): data = data.to(device=device, non_blocking=True) target = target.to(device=device, non_blocking=True) optimiser.zero_grad() output = model(data) loss = F.nll_loss(output, target) loss.backward() train_losses.append(loss.item()) optimiser.step() if i % 10 == 0: print(i, loss.item()) torch.save(model.state_dict(), 'model.pth') torch.save(optimiser.state_dict(), 'optimiser.pth') torch.save(train_losses, 'train_losses.pth') 

Los módulos de red se ponen en modo de entrenamiento de forma predeterminada, lo que en cierta medida afecta el funcionamiento de los módulos, sobre todo, la reducción y la normalización de lotes. De una forma u otra, es mejor configurar estas cosas manualmente usando .train() , que filtra la bandera de "entrenamiento" para todos los módulos secundarios.

Aquí, el método .to() no solo acepta el dispositivo, sino que también establece non_blocking=True , asegurando así la copia asíncrona de datos a la GPU desde la memoria comprometida, permitiendo que la CPU permanezca operativa durante la transferencia de datos; de lo contrario, non_blocking=True simplemente no es una opción.

Antes de crear un nuevo conjunto de gradientes con loss.backward() y optimiser.step() con optimiser.step() , debe restablecer manualmente los gradientes de los parámetros para optimizar con optimiser.zero_grad() . Por defecto, PyTorch acumula gradientes, lo cual es muy conveniente si no tiene suficientes recursos para calcular todos los gradientes que necesita en una sola pasada.

PyTorch utiliza un sistema de "cinta" de gradientes automáticos: recopila información sobre qué operaciones y en qué orden se realizaron en los tensores, y luego los reproduce en la dirección opuesta para realizar la diferenciación en orden inverso (diferenciación en modo inverso). Es por eso que es tan súper flexible y permite gráficos computacionales arbitrarios. Si ninguno de estos tensores requiere gradientes (debe establecer requires_grad=True , creando un tensor para este propósito), ¡entonces no se guarda ningún gráfico! Sin embargo, las redes generalmente tienen parámetros que requieren gradientes, por lo que cualquier cálculo realizado sobre la base de la salida de la red se almacenará en el gráfico. Entonces, si desea guardar los datos resultantes de este paso, deberá deshabilitar manualmente los gradientes o (un enfoque más común), guardar esta información como un número de Python (usando .item() en el escalar PyTorch) o una matriz numpy . Lea más sobre autograd en la documentación oficial .

Una forma de acortar el gráfico computacional es usar .detach() cuando se pasa el estado oculto al aprender RNN con una versión truncada de backpropagation-through-time. También es conveniente cuando se diferencian las pérdidas, cuando uno de los componentes es la salida de otra red, pero esta otra red no debe optimizarse con respecto a las pérdidas. Como ejemplo, enseñaré la parte discriminatoria en el material de salida que se genera cuando se trabaja con la GAN, o la capacitación de políticas en el algoritmo de actor crítico utilizando la función objetivo como la función base (por ejemplo, A2C). Otra técnica que impide el cálculo de gradientes es efectiva en la enseñanza de GAN (capacitación de la parte generadora en material discriminante) y lo típico en el ajuste fino es recorrer los parámetros de red para los cuales se establece param.requires_grad = False .

Es importante no solo registrar los resultados en el archivo de consola / registro, sino también establecer puntos de control en los parámetros del modelo (y el estado del optimizador) por si acaso. También puede usar torch.save() para guardar objetos regulares de Python, o usar otra solución estándar: el pickle incorporado.

Prueba


 model.eval() test_loss, correct = 0, 0 with torch.no_grad(): for data, target in test_loader: data = data.to(device=device, non_blocking=True) target = target.to(device=device, non_blocking=True) output = model(data) test_loss += F.nll_loss(output, target, reduction='sum').item() pred = output.argmax(1, keepdim=True) correct += pred.eq(target.view_as(pred)).sum().item() test_loss /= len(test_data) acc = correct / len(test_data) print(acc, test_loss) 

En respuesta a .train() redes deben ponerse explícitamente en modo de evaluación usando .eval() .

Como se mencionó anteriormente, cuando se usa una red, generalmente se compila un gráfico computacional. Para evitar esto, use el no_grad contexto no_grad con with torch.no_grad() .

Algo mas


Esta es una sección adicional, en la que hice algunas digresiones más útiles.
Aquí está la documentación oficial que explica trabajar con memoria.

CUDA errores? Es difícil solucionarlos, y generalmente están conectados con inconsistencias lógicas, según las cuales se muestran mensajes de error más sensibles en la CPU que en la GPU. Lo mejor de todo, si planea trabajar con la GPU, puede cambiar rápidamente entre la CPU y la GPU. Un consejo de desarrollo más general es organizar el código para que pueda verificarse rápidamente antes de comenzar una tarea completa. Por ejemplo, prepare un conjunto de datos pequeño o sintético, ejecute una era train + test, etc. Si el problema es un error de CUDA, o no puede cambiar a la CPU, configure CUDA_LAUNCH_BLOCKING = 1. Esto hará que el kernel CUDA se inicie sincrónicamente y recibirá mensajes de error más precisos.

Una nota sobre torch.multiprocessing o simplemente ejecutando múltiples scripts PyTorch al mismo tiempo. Debido a que PyTorch utiliza bibliotecas BLAS de subprocesos múltiples para acelerar los cálculos de álgebra lineal en la CPU, generalmente están involucrados varios núcleos. Si desea hacer varias cosas al mismo tiempo, utilizando un procesamiento multiproceso o varios scripts, puede ser aconsejable reducir manualmente su número configurando la variable de entorno OMP_NUM_THREADS a 1 u otro valor bajo. Por lo tanto, la probabilidad de deslizar el procesador se reduce. La documentación oficial tiene otros comentarios sobre el procesamiento multiproceso.

Source: https://habr.com/ru/post/471228/


All Articles