Olá Habr!
Temos uma encomenda antecipada de um livro há muito aguardado sobre
a biblioteca PyTorch .

Como você aprenderá todo o material básico necessário sobre o PyTorch neste livro, lembramos os
benefícios de um processo chamado "grokking" ou "compreensão profunda" do tópico que você deseja aprender. Na postagem de hoje, contaremos como Kai Arulkumaran bateu o PyTorch (sem foto). Bem-vindo ao gato.
O PyTorch é uma estrutura flexível de aprendizado profundo que distingue automaticamente objetos usando redes neurais dinâmicas (ou seja, redes usando controle dinâmico de fluxo, como
if
e loops
while
). O PyTorch suporta aceleração de GPU,
treinamento distribuído , vários tipos de
otimização e muitos outros recursos interessantes. Aqui, expus algumas reflexões sobre como, na minha opinião, deve usar o PyTorch; todos os aspectos da biblioteca e práticas recomendadas não são abordados aqui, mas espero que este texto seja útil para você.
Redes neurais são uma subclasse de gráficos computacionais. Os gráficos de computação recebem dados como entrada e, em seguida, esses dados são roteados (e podem ser convertidos) nos nós em que são processados. No aprendizado profundo, os neurônios (nós) geralmente transformam dados aplicando parâmetros e funções diferenciáveis a eles, para que os parâmetros possam ser otimizados para minimizar as perdas pelo método da descida do gradiente. Em um sentido mais amplo, observo que as funções podem ser estocásticas e dinâmicas de gráfico. Assim, enquanto as redes neurais se encaixam bem no paradigma de programação de fluxo de dados, a API do PyTorch se concentra no paradigma de
programação imperativa , e essa maneira de interpretar os programas criados é muito mais familiar. É por isso que o código PyTorch é mais fácil de ler, é mais fácil julgar o design de programas complexos, o que, no entanto, não exige comprometimento sério no desempenho: na verdade, o PyTorch é rápido o suficiente e oferece muitas otimizações que você, como usuário final, não pode se preocupar com nada. (no entanto, se você estiver realmente interessado neles, poderá se aprofundar um pouco mais e conhecê-los).
O restante deste artigo é uma análise do
exemplo oficial no conjunto de dados MNIST . Aqui
jogamos PyTorch, portanto, recomendo entender o artigo somente após o conhecimento
dos manuais oficiais para iniciantes . Por conveniência, o código é apresentado na forma de pequenos fragmentos equipados com comentários, ou seja, não é distribuído em funções / arquivos separados que você costuma ver em código modular puro.
Importações
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
Tudo isso é uma importação bastante padrão, com exceção dos módulos de
torchvision
da
torchvision
, que são usados ativamente para resolver tarefas relacionadas à visão computacional.
Personalização
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
é a maneira padrão de lidar com argumentos de linha de comando no Python.
Se você precisar escrever um código projetado para funcionar em dispositivos diferentes (usando a aceleração da GPU, quando disponível, mas se não for revertido para os cálculos na CPU), selecione e salve o
torch.device
apropriado, com o qual você pode determinar onde deve tensores são armazenados. Para mais informações sobre como criar esse código, consulte a
documentação oficial . A abordagem do PyTorch é levar a seleção de dispositivos ao controle do usuário, o que pode parecer indesejável em exemplos simples. No entanto, essa abordagem simplifica bastante o trabalho quando você precisa lidar com tensores, o que: a) é conveniente para a depuração; b) permite que você use efetivamente os dispositivos manualmente.
Para a reprodutibilidade das experiências, é necessário definir valores iniciais aleatórios para todos os componentes que usam geração aleatória de números (incluindo
random
ou
numpy
, se você
numpy
os usar). Observe: cuDNN usa algoritmos não determinísticos e, opcionalmente, é desativado usando
torch.backends.cudnn.enabled = False
.
Dados
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)
Como os modelos de
torchvision
armazenados em
~/.torch/models/
, eu prefiro armazenar os
torchvision
visão da tocha em
~/.torch/datasets
. Este é meu contrato de direitos autorais, mas é muito conveniente usar em projetos desenvolvidos com base no MNIST, CIFAR-10, etc. Em geral, os conjuntos de dados devem ser armazenados separadamente do código se você pretende reutilizar vários conjuntos de dados.
torchvision.transforms
contém muitas opções de conversão convenientes para imagens individuais, como corte e normalização.
Existem muitas opções no
batch_size
, mas, além de
batch_size
e
shuffle
, você também deve ter em mente
num_workers
e
pin_memory
, eles ajudam a aumentar a eficiência.
num_workers > 0
usa subprocessos para carregamento assíncrono de dados e não bloqueia o processo principal para isso. Um caso de uso típico é carregar dados (por exemplo, imagens) de um disco e, possivelmente, convertê-los; tudo isso pode ser feito em paralelo, juntamente com o processamento de dados da rede. O grau de processamento pode precisar ser ajustado para: a) minimizar o número de trabalhadores e, consequentemente, a quantidade de CPU e RAM usada (cada trabalhador carrega um lote separado, em vez de amostras individuais incluídas no lote); b) minimizar o tempo que os dados aguardam na rede.
pin_memory
usa
memória fixada (em oposição a paginada) para acelerar qualquer operação de transferência de dados da RAM para a GPU (e não faz nada com o código específico da 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'))
A inicialização da rede geralmente se estende a variáveis-membro, camadas que contêm parâmetros de aprendizado e, possivelmente, parâmetros de aprendizado individuais e buffers não treinados. Então, com um passe direto, eles são usados em combinação com funções de
F
que são puramente funcionais e não contêm parâmetros. Algumas pessoas gostam de trabalhar com redes puramente funcionais (por exemplo, mantêm parâmetros e usam
F.conv2d
vez de
nn.Conv2d
) ou redes inteiramente constituídas por camadas (por exemplo,
nn.ReLU
vez de
F.relu
).
.to(device)
é uma maneira conveniente de enviar parâmetros (e buffers) para a GPU se o
device
definido como GPU, porque, caso contrário (se o dispositivo estiver definido como CPU), nada será feito. É importante transferir os parâmetros do dispositivo para o dispositivo apropriado antes de passá-los para o otimizador; caso contrário, o otimizador não poderá rastrear os parâmetros corretamente!
As redes neurais (
nn.Module
) e os otimizadores (
optim.Optimizer
) podem salvar e carregar seu estado interno, e é recomendável fazer isso com
.load_state_dict(state_dict)
- é necessário recarregar o estado de ambos para retomar o treinamento com base em dicionários salvos anteriormente estados. Salvar o objeto inteiro pode estar
cheio de erros . Se você salvou os tensores na GPU e deseja carregá-los na CPU ou em outra GPU, a maneira mais fácil é carregá-los diretamente na CPU usando a
opção map_location
, por exemplo,
torch.load('model.pth'
,
map_location='cpu'
).
Aqui estão alguns outros pontos que não são mostrados aqui, mas dignos de menção, de que você pode usar o fluxo de controle com uma passagem direta (por exemplo, a execução da
if
pode depender da variável do membro ou dos próprios dados. Além disso, é perfeitamente aceitável exibir no meio do processo (
print
) tensors, o que simplifica bastante a depuração.Finalmente, com uma passagem direta, muitos argumentos podem ser usados.Ilustrarei esse ponto com uma pequena lista que não está vinculada a nenhuma idéia específica:
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
Treinamento
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')
Os módulos de rede são colocados no modo de treinamento por padrão - o que afeta, em certa medida, a operação dos módulos, principalmente - a diluição e a normalização de lotes. De uma forma ou de outra, é melhor definir essas coisas manualmente usando
.train()
, que filtra o sinalizador "training" para todos os módulos filhos.
Aqui, o método
.to()
não apenas aceita o dispositivo, mas também define
non_blocking=True
, garantindo assim a cópia assíncrona de dados para a GPU da memória confirmada, permitindo que a CPU permaneça operacional durante a transferência de dados; caso contrário,
non_blocking=True
simplesmente não é uma opção.
Antes de criar um novo conjunto de gradientes usando
loss.backward()
e
optimiser.step()
usando
optimiser.step()
, você deve redefinir manualmente os gradientes dos parâmetros a serem otimizados usando optimiser.zero_grad
optimiser.zero_grad()
. Por padrão, o PyTorch acumula gradientes, o que é muito conveniente se você não tiver recursos suficientes para calcular todos os gradientes necessários em uma única passagem.
O PyTorch usa um sistema de "fita" de gradientes automáticos - coleta informações sobre quais operações e em que ordem foram executadas nos tensores e as reproduz na direção oposta para realizar a diferenciação na ordem inversa (diferenciação no modo reverso). É por isso que é tão super flexível e permite gráficos computacionais arbitrários. Se nenhum desses tensores exigir gradientes (você precisa definir
requires_grad=True
, criando um tensor para essa finalidade), nenhum gráfico será salvo! No entanto, as redes geralmente têm parâmetros que exigem gradientes; portanto, todos os cálculos realizados com base na saída da rede serão armazenados no gráfico. Portanto, se você quiser salvar os dados resultantes desta etapa, será necessário desativar manualmente os gradientes ou (uma abordagem mais comum), salve essas informações como um número Python (usando
.item()
no escalar do PyTorch) ou uma matriz
numpy
. Leia mais sobre o
autograd
na
documentação oficial .
Uma maneira de reduzir o gráfico computacional é usar
.detach()
quando o estado oculto é passado ao aprender RNN com uma versão truncada da retropropagação ao longo do tempo. Também é conveniente ao diferenciar perdas, quando um dos componentes é a saída de outra rede, mas essa outra rede não deve ser otimizada com relação às perdas. Como exemplo, ensinarei a parte discriminatória do material de saída gerado ao trabalhar com a GAN ou o treinamento de políticas no algoritmo de ator-crítico, usando a função objetivo como a função base (por exemplo, A2C). Outra técnica que impede o cálculo de gradientes é eficaz no treinamento GAN (treinamento da parte geradora em material discriminante) e típica no ajuste fino é a enumeração cíclica de parâmetros de rede para os quais
param.requires_grad = False
.
É importante não apenas registrar os resultados no arquivo de console / log, mas também definir pontos de controle nos parâmetros do modelo (e no estado do otimizador) apenas por precaução. Você também pode usar
torch.save()
para salvar objetos Python regulares ou usar outra solução padrão - o
pickle
.
Teste
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)
Em resposta a
.train()
redes precisam ser explicitamente colocadas no modo de avaliação usando
.eval()
.
Como mencionado acima, ao usar uma rede, geralmente é compilado um gráfico computacional. Para evitar isso, use o
no_grad
contexto
with torch.no_grad()
.
Um pouco mais
Esta é uma seção adicional, na qual fiz algumas digressões mais úteis.
Aqui está a
documentação oficial que explica como trabalhar com memória.
Erros CUDA? É difícil corrigi-los e, geralmente, eles estão conectados com inconsistências lógicas, de acordo com as quais mensagens de erro mais sensíveis são exibidas na CPU do que na GPU. O melhor de tudo é que, se você planeja trabalhar com a GPU, pode alternar rapidamente entre a CPU e a GPU. Uma dica de desenvolvimento mais geral é organizar o código para que ele possa ser verificado rapidamente antes de iniciar uma tarefa completa. Por exemplo, prepare um conjunto de dados pequeno ou sintético, execute um teste de trem da era +, etc. Se o problema for um erro CUDA ou você não puder alternar para a CPU, defina CUDA_LAUNCH_BLOCKING = 1. Isso fará com que o kernel CUDA inicie de forma síncrona e você receberá mensagens de erro mais precisas.
Uma observação sobre
torch.multiprocessing
ou apenas executando vários scripts PyTorch ao mesmo tempo. Como o PyTorch usa bibliotecas BLAS com vários threads para acelerar os cálculos de álgebra linear na CPU, geralmente vários núcleos estão envolvidos. Se você deseja fazer várias coisas ao mesmo tempo, usando processamento multithread ou vários scripts, pode ser aconselhável reduzir manualmente seu número, configurando a variável de ambiente
OMP_NUM_THREADS
para 1 ou outro valor baixo. Assim, a probabilidade de escorregar no processador é reduzida. A documentação oficial possui outros comentários sobre o processamento multithread.