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.