Para instalar o Pytorch, siga as instruções no site oficial:
pip install torch torchvision
Nosso objetivo é expandir gradualmente esta série adicionando novos artigos e mantendo o conteúdo atualizado com os lançamentos mais recentes da API da Pytorch. Se você tiver sugestões sobre como melhorar esta série ou encontrar as explicações ambíguas, sinta -se à vontade para criar um problema, enviar patches ou alcançar por e -mail.
A Pytorch é uma das bibliotecas mais populares para computação numérica e atualmente está entre as bibliotecas mais usadas para realizar pesquisas de aprendizado de máquina. De muitas maneiras, a Pytorch é semelhante a Numpy, com o benefício adicional que a Pytorch permite executar seus cálculos nas CPUs, GPUs e TPUs sem qualquer alteração material no seu código. O Pytorch também facilita a distribuição de seu cálculo em vários dispositivos ou máquinas. Uma das características mais importantes do Pytorch é a diferenciação automática. Ele permite calcular os gradientes de suas funções analiticamente de maneira eficiente, o que é crucial para o treinamento de modelos de aprendizado de máquina usando o método de descida de gradiente. Nosso objetivo aqui é fornecer uma introdução suave à Pytorch e discutir as melhores práticas para o uso de Pytorch.
A primeira coisa a aprender sobre Pytorch é o conceito de tensores. Os tensores são simplesmente matrizes multidimensionais. Um tensor de pytorch é muito semelhante a uma matriz numpy com alguns mágico funcionalidade adicional.
Um tensor pode armazenar um valor escalar:
import torch
a = torch . tensor ( 3 )
print ( a ) # tensor(3)ou uma matriz:
b = torch . tensor ([ 1 , 2 ])
print ( b ) # tensor([1, 2])uma matriz:
c = torch . zeros ([ 2 , 2 ])
print ( c ) # tensor([[0., 0.], [0., 0.]])ou qualquer tensor dimensional arbitrário:
d = torch . rand ([ 2 , 2 , 2 ])Os tensores podem ser usados para executar operações algébricas com eficiência. Uma das operações mais usadas em aplicativos de aprendizado de máquina é a multiplicação da matriz. Digamos que você deseja multiplicar duas matrizes aleatórias de tamanho 3x5 e 5x4, isso pode ser feito com a operação de multiplicação da matriz (@):
import torch
x = torch . randn ([ 3 , 5 ])
y = torch . randn ([ 5 , 4 ])
z = x @ y
print ( z )Da mesma forma, para adicionar dois vetores, você pode fazer:
z = x + yPara converter um tensor em uma matriz Numpy, você pode chamar o método Numpy () de Tensor:
print ( z . numpy ())E você sempre pode converter uma matriz Numpy em um tensor por:
x = torch . tensor ( np . random . normal ([ 3 , 5 ]))A vantagem mais importante do Pytorch sobre o Numpy é sua funcionalidade de diferenciação automática, que é muito útil em aplicações de otimização, como otimizar parâmetros de uma rede neural. Vamos tentar entendê -lo com um exemplo.
Digamos que você tenha uma função composta que é uma cadeia de duas funções: g(u(x)) . Para calcular o derivado de g em relação a x podemos usar a regra da cadeia que afirma que: dg/dx = dg/du * du/dx . A Pytorch pode calcular analiticamente os derivados para nós.
Para calcular os derivados no Pytorch primeiro, criamos um tensor e definimos seus requires_grad como true. Podemos usar operações tensoras para definir nossas funções. Assumimos que u é uma função quadrática e g é uma função linear simples:
x = torch . tensor ( 1.0 , requires_grad = True )
def u ( x ):
return x * x
def g ( u ):
return - u Nesse caso, nossa função composta é g(u(x)) = -x*x . Portanto, seu derivado em relação a x é -2x . No ponto x=1 , isso é igual a -2 .
Vamos verificar isso. Isso pode ser feito usando a função de graduação em Pytorch:
dgdx = torch . autograd . grad ( g ( u ( x )), x )[ 0 ]
print ( dgdx ) # tensor(-2.) Para entender como a diferenciação automática poderosa pode ser, vamos dar uma olhada em outro exemplo. Suponha que tenhamos amostras de uma curva (digamos f(x) = 5x^2 + 3 ) e queremos estimar f(x) com base nessas amostras. Definimos uma função paramétrica g(x, w) = w0 x^2 + w1 x + w2 , que é uma função dos parâmetros de entrada x e latente w , nosso objetivo é encontrar os parâmetros latentes de modo que g(x, w) ≈ f(x) . Isso pode ser feito minimizando a seguinte função de perda: L(w) = Σ (f(x) - g(x, w))^2 . Embora exista uma solução de forma fechada para esse problema simples, optamos por usar uma abordagem mais geral que pode ser aplicada a qualquer função diferenciável arbitrária e que esteja usando ascendência de gradiente estocástica. Simplesmente calculamos o gradiente médio de L(w) em relação a w sobre um conjunto de pontos de amostra e nos movemos na direção oposta.
Veja como isso pode ser feito em Pytorch:
import numpy as np
import torch
# Assuming we know that the desired function is a polynomial of 2nd degree, we
# allocate a vector of size 3 to hold the coefficients and initialize it with
# random noise.
w = torch . tensor ( torch . randn ([ 3 , 1 ]), requires_grad = True )
# We use the Adam optimizer with learning rate set to 0.1 to minimize the loss.
opt = torch . optim . Adam ([ w ], 0.1 )
def model ( x ):
# We define yhat to be our estimate of y.
f = torch . stack ([ x * x , x , torch . ones_like ( x )], 1 )
yhat = torch . squeeze ( f @ w , 1 )
return yhat
def compute_loss ( y , yhat ):
# The loss is defined to be the mean squared error distance between our
# estimate of y and its true value.
loss = torch . nn . functional . mse_loss ( yhat , y )
return loss
def generate_data ():
# Generate some training data based on the true function
x = torch . rand ( 100 ) * 20 - 10
y = 5 * x * x + 3
return x , y
def train_step ():
x , y = generate_data ()
yhat = model ( x )
loss = compute_loss ( y , yhat )
opt . zero_grad ()
loss . backward ()
opt . step ()
for _ in range ( 1000 ):
train_step ()
print ( w . detach (). numpy ())Ao executar este código, você verá um resultado próximo a isso:
[ 4.9924135 , 0.00040895029 , 3.4504161 ]Que é uma aproximação relativamente próxima aos nossos parâmetros.
Isso é apenas uma ponta do iceberg para o que Pytorch pode fazer. Muitos problemas, como otimizar grandes redes neurais com milhões de parâmetros, podem ser implementados com eficiência em Pytorch em apenas algumas linhas de código. A Pytorch cuida da dimensionamento em vários dispositivos e tópicos e suporta uma variedade de plataformas.
No exemplo anterior, usamos tensores de ossos nus e operações de tensor para construir nosso modelo. Para tornar seu código um pouco mais organizado, é recomendável usar os módulos de Pytorch. Um módulo é simplesmente um contêiner para seus parâmetros e encapsula operações do modelo. Por exemplo, digamos que você deseja representar um modelo linear y = ax + b . Este modelo pode ser representado com o seguinte código:
import torch
class Net ( torch . nn . Module ):
def __init__ ( self ):
super (). __init__ ()
self . a = torch . nn . Parameter ( torch . rand ( 1 ))
self . b = torch . nn . Parameter ( torch . rand ( 1 ))
def forward ( self , x ):
yhat = self . a * x + self . b
return yhatPara usar este modelo na prática, você instanciam o módulo e simplesmente chamam de função:
x = torch . arange ( 100 , dtype = torch . float32 )
net = Net ()
y = net ( x ) Os parâmetros são essencialmente tensores com requires_grad definido como true. É conveniente usar parâmetros porque você pode simplesmente recuperá -los todos com o método parameters() do módulo:
for p in net . parameters ():
print ( p ) Agora, digamos que você tenha uma função desconhecida y = 5x + 3 + some noise e deseja otimizar os parâmetros do seu modelo para ajustar essa função. Você pode começar amostrando alguns pontos da sua função:
x = torch . arange ( 100 , dtype = torch . float32 ) / 100
y = 5 * x + 3 + torch . rand ( 100 ) * 0.3Semelhante ao exemplo anterior, você pode definir uma função de perda e otimizar os parâmetros do seu modelo da seguinte forma:
criterion = torch . nn . MSELoss ()
optimizer = torch . optim . SGD ( net . parameters (), lr = 0.01 )
for i in range ( 10000 ):
net . zero_grad ()
yhat = net ( x )
loss = criterion ( yhat , y )
loss . backward ()
optimizer . step ()
print ( net . a , net . b ) # Should be close to 5 and 3 O Pytorch vem com vários módulos predefinidos. Um desses módulos é torch.nn.Linear , que é uma forma mais geral de uma função linear do que o que definimos acima. Podemos reescrever nosso módulo acima usando torch.nn.Linear como este:
class Net ( torch . nn . Module ):
def __init__ ( self ):
super (). __init__ ()
self . linear = torch . nn . Linear ( 1 , 1 )
def forward ( self , x ):
yhat = self . linear ( x . unsqueeze ( 1 )). squeeze ( 1 )
return yhat Observe que usamos Squeeze e Deatcheeze, já que torch.nn.Linear opera em lote de vetores em oposição aos escalares.
Por padrão, os parâmetros de chamada () em um módulo retornarão os parâmetros de todos os seus submódulos:
net = Net ()
for p in net . parameters ():
print ( p ) Existem alguns módulos predefinidos que atuam como um contêiner para outros módulos. O módulo de contêiner mais comumente usado é torch.nn.Sequential . Como o próprio nome implica, é usado para empilhar vários módulos (ou camadas) um sobre o outro. Por exemplo, para empilhar duas camadas lineares com uma não linearidade ReLU entre você pode fazer:
model = torch . nn . Sequential (
torch . nn . Linear ( 64 , 32 ),
torch . nn . ReLU (),
torch . nn . Linear ( 32 , 10 ),
) O Pytorch suporta operações de transmissão do ElementWise. Normalmente, quando você deseja executar operações como adição e multiplicação, é necessário garantir que as formas dos operandos correspondam, por exemplo, você não pode adicionar um tensor de forma [3, 2] a um tensor de forma [3, 4] . Mas há um caso especial e é aí que você tem uma dimensão singular. Pytorch implicitamente tende o tensor em suas dimensões singulares para corresponder à forma do outro operando. Portanto, é válido adicionar um tensor de forma [3, 2] a um tensor de forma [3, 1] .
import torch
a = torch . tensor ([[ 1. , 2. ], [ 3. , 4. ]])
b = torch . tensor ([[ 1. ], [ 2. ]])
# c = a + b.repeat([1, 2])
c = a + b
print ( c )A transmissão nos permite executar o ladrilho implícito, o que torna o código mais curto e mais eficiente em memória, pois não precisamos armazenar o resultado da operação de ladrilhos. Um local interessante que isso pode ser usado é ao combinar recursos de comprimento variável. Para concatenar as características de comprimento variável, geralmente ladrilhamos os tensores de entrada, concatenamos o resultado e apliquem alguma não linearidade. Este é um padrão comum em uma variedade de arquiteturas de rede neural:
a = torch . rand ([ 5 , 3 , 5 ])
b = torch . rand ([ 5 , 1 , 6 ])
linear = torch . nn . Linear ( 11 , 10 )
# concat a and b and apply nonlinearity
tiled_b = b . repeat ([ 1 , 3 , 1 ])
c = torch . cat ([ a , tiled_b ], 2 )
d = torch . nn . functional . relu ( linear ( c ))
print ( d . shape ) # torch.Size([5, 3, 10]) Mas isso pode ser feito com mais eficiência com a transmissão. Usamos o fato de que f(m(x + y)) é igual a f(mx + my) . Assim, podemos fazer as operações lineares separadamente e usar a transmissão para fazer concatenação implícita:
a = torch . rand ([ 5 , 3 , 5 ])
b = torch . rand ([ 5 , 1 , 6 ])
linear1 = torch . nn . Linear ( 5 , 10 )
linear2 = torch . nn . Linear ( 6 , 10 )
pa = linear1 ( a )
pb = linear2 ( b )
d = torch . nn . functional . relu ( pa + pb )
print ( d . shape ) # torch.Size([5, 3, 10])De fato, este pedaço de código é bastante geral e pode ser aplicado a tensores de forma arbitrária, desde que seja possível transmitir entre tensores:
class Merge ( torch . nn . Module ):
def __init__ ( self , in_features1 , in_features2 , out_features , activation = None ):
super (). __init__ ()
self . linear1 = torch . nn . Linear ( in_features1 , out_features )
self . linear2 = torch . nn . Linear ( in_features2 , out_features )
self . activation = activation
def forward ( self , a , b ):
pa = self . linear1 ( a )
pb = self . linear2 ( b )
c = pa + pb
if self . activation is not None :
c = self . activation ( c )
return cAté agora, discutimos a boa parte da transmissão. Mas qual é a parte feia que você pode perguntar? As suposições implícitas quase sempre dificultam a depuração. Considere o seguinte exemplo:
a = torch . tensor ([[ 1. ], [ 2. ]])
b = torch . tensor ([ 1. , 2. ])
c = torch . sum ( a + b )
print ( c ) O que você acha que o valor de c seria após a avaliação? Se você adivinhou 6, isso está errado. Vai ser 12. Isso ocorre porque, quando a classificação de dois tensores não corresponde, Pytorch expande automaticamente a primeira dimensão do tensor com classificação mais baixa antes da operação do elemento, de modo que o resultado da adição seria [[2, 3], [3, 4]] , e a redução de todos os parâmetros nos daria 12.
A maneira de evitar esse problema é ser o mais explícito possível. Se tivéssemos especificado qual dimensão gostaríamos de reduzir, pegar esse bug teria sido muito mais fácil:
a = torch . tensor ([[ 1. ], [ 2. ]])
b = torch . tensor ([ 1. , 2. ])
c = torch . sum ( a + b , 0 )
print ( c ) Aqui, o valor de c seria [5, 7] , e imediatamente adivinharíamos com base na forma do resultado de que há algo errado. Uma regra geral é sempre especificar as dimensões nas operações de redução e ao usar torch.squeeze .
Assim como a Numpy, a Pytorch sobrecarrega vários operadores de Python para tornar o código Pytorch mais curto e mais legível.
O Slicing Op é um dos operadores sobrecarregados que pode facilitar muito os tensores de indexação:
z = x [ begin : end ] # z = torch.narrow(0, begin, end-begin)Tenha muito cuidado ao usar este OP. A OP de fatiamento, como qualquer outra OP, tem algumas despesas gerais. Porque é um OP comum e uma aparência inocente, pode ser muito usada, o que pode levar a ineficiências. Para entender o quão ineficiente esse OP pode ser, vamos ver um exemplo. Queremos realizar manualmente a redução nas fileiras de uma matriz:
import torch
import time
x = torch . rand ([ 500 , 10 ])
z = torch . zeros ([ 10 ])
start = time . time ()
for i in range ( 500 ):
z += x [ i ]
print ( "Took %f seconds." % ( time . time () - start )) Isso é bastante lento e o motivo é que estamos chamando o Slice OP 500 vezes, o que adiciona muita sobrecarga. Uma escolha melhor teria sido usar a torch.unbind .
z = torch . zeros ([ 10 ])
for x_i in torch . unbind ( x ):
z += x_iIsso é significativamente (~ 30% na minha máquina) mais rápido.
Obviamente, a maneira certa de fazer essa redução simples é usar torch.sum .
z = torch . sum ( x , dim = 0 )que é extremamente rápido (~ 100x mais rápido na minha máquina).
Pytorch também sobrecarrega uma variedade de operadores aritméticos e lógicos:
z = - x # z = torch.neg(x)
z = x + y # z = torch.add(x, y)
z = x - y
z = x * y # z = torch.mul(x, y)
z = x / y # z = torch.div(x, y)
z = x // y
z = x % y
z = x ** y # z = torch.pow(x, y)
z = x @ y # z = torch.matmul(x, y)
z = x > y
z = x >= y
z = x < y
z = x <= y
z = abs ( x ) # z = torch.abs(x)
z = x & y
z = x | y
z = x ^ y # z = torch.logical_xor(x, y)
z = ~ x # z = torch.logical_not(x)
z = x == y # z = torch.eq(x, y)
z = x != y # z = torch.ne(x, y) Você também pode usar a versão aumentada dessas operações. Por exemplo, x += y e x **= 2 também são válidos.
Observe que o Python não permite a sobrecarga and , or e not as palavras -chave.
O Pytorch é otimizado para executar operações em grandes tensores. Fazer muitas operações em pequenos tensores é bastante ineficiente em Pytorch. Portanto, sempre que possível, você deve reescrever seus cálculos em forma de lote para reduzir a sobrecarga e melhorar o desempenho. Se não houver como você poder em lote manualmente suas operações, o uso do TorchScript pode melhorar o desempenho do seu código. O TorchScript é simplesmente um subconjunto de funções Python reconhecidas por Pytorch. O Pytorch pode otimizar automaticamente seu código de tochcript usando o compilador Just In Time (JIT) e reduzir algumas despesas gerais.
Vejamos um exemplo. Uma operação muito comum nos aplicativos ML é "Lotch Gather". Esta operação pode simplesmente escrever como output[i] = input[i, index[i]] . Isso pode ser simplesmente implementado em Pytorch da seguinte maneira:
import torch
def batch_gather ( tensor , indices ):
output = []
for i in range ( tensor . size ( 0 )):
output += [ tensor [ i ][ indices [ i ]]]
return torch . stack ( output ) Para implementar a mesma função usando o TorchScript, basta usar o torch.jit.script Decorator:
@ torch . jit . script
def batch_gather_jit ( tensor , indices ):
output = []
for i in range ( tensor . size ( 0 )):
output += [ tensor [ i ][ indices [ i ]]]
return torch . stack ( output )Nos meus testes, isso é cerca de 10% mais rápido.
Mas nada supera manualmente em lote suas operações. Uma implementação vetorizada em meus testes é 100 vezes mais rápida:
def batch_gather_vec ( tensor , indices ):
shape = list ( tensor . shape )
flat_first = torch . reshape (
tensor , [ shape [ 0 ] * shape [ 1 ]] + shape [ 2 :])
offset = torch . reshape (
torch . arange ( shape [ 0 ]). cuda () * shape [ 1 ],
[ shape [ 0 ]] + [ 1 ] * ( len ( indices . shape ) - 1 ))
output = flat_first [ indices + offset ]
return output Na última lição, conversamos sobre escrever um código Pytorch eficiente. Mas para fazer com que seu código seja executado com a máxima eficiência, você também precisa carregar seus dados com eficiência na memória do seu dispositivo. Felizmente, a Pytorch oferece uma ferramenta para facilitar o carregamento de dados. É chamado de DataLoader . Um DataLoader usa vários trabalhadores para carregar dados simultaneamente de um Dataset e opcionalmente usa um Sampler para amostrar entradas de dados e formar um lote.
Se você pode acessar aleatoriamente seus dados, o uso de um DataLoader é muito fácil: basta implementar uma classe Dataset que implementa __getitem__ (para ler cada item de dados) e __len__ (para retornar o número de itens no conjunto de dados). Por exemplo, aqui está como carregar imagens de um determinado diretório:
import glob
import os
import random
import cv2
import torch
class ImageDirectoryDataset ( torch . utils . data . Dataset ):
def __init__ ( path , pattern ):
self . paths = list ( glob . glob ( os . path . join ( path , pattern )))
def __len__ ( self ):
return len ( self . paths )
def __item__ ( self ):
path = random . choice ( paths )
return cv2 . imread ( path , 1 )Para carregar todas as imagens JPEG de um determinado diretório, você pode fazer o seguinte:
dataloader = torch . utils . data . DataLoader ( ImageDirectoryDataset ( "/data/imagenet/*.jpg" ), num_workers = 8 )
for data in dataloader :
# do something with dataAqui estamos usando 8 trabalhadores para ler simultaneamente nossos dados do disco. Você pode ajustar o número de trabalhadores em sua máquina para obter melhores resultados.
O uso de um DataLoader para ler dados com acesso aleatório pode ser bom se você tiver armazenamento rápido ou se os itens de dados forem grandes. Mas imagine ter um sistema de arquivos de rede com conexão lenta. Solicitar arquivos individuais dessa maneira pode ser extremamente lento e provavelmente acabaria se tornando o gargalo do seu pipeline de treinamento.
Uma abordagem melhor é armazenar seus dados em um formato de arquivo contíguo que pode ser lido sequencialmente. Por exemplo, se você tiver uma grande coleção de imagens, poderá usar alcatrão para criar um único arquivo e extrair arquivos do arquivo sequencialmente no Python. Para fazer isso, você pode usar IterableDataset de Pytorch. Para criar uma classe IterableDataset , você só precisa implementar um método __iter__ que lê e produz sequencialmente itens de dados do conjunto de dados.
Uma implementação ingênua gostaria disso:
import tarfile
import torch
def tar_image_iterator ( path ):
tar = tarfile . open ( self . path , "r" )
for tar_info in tar :
file = tar . extractfile ( tar_info )
content = file . read ()
yield cv2 . imdecode ( content , 1 )
file . close ()
tar . members = []
tar . close ()
class TarImageDataset ( torch . utils . data . IterableDataset ):
def __init__ ( self , path ):
super (). __init__ ()
self . path = path
def __iter__ ( self ):
yield from tar_image_iterator ( self . path )Mas há um grande problema com essa implementação. Se você tentar usar o Dataloader para ler neste conjunto de dados com mais de um trabalhador, observaria muitas imagens duplicadas:
dataloader = torch . utils . data . DataLoader ( TarImageDataset ( "/data/imagenet.tar" ), num_workers = 8 )
for data in dataloader :
# data contains duplicated items O problema é que cada trabalhador cria uma instância separada do conjunto de dados e cada um começará a partir do início do conjunto de dados. Uma maneira de evitar isso é, em vez de ter um arquivo TAR, dividir seus dados em num_workers separar arquivos TAR e carregar cada um com um trabalhador separado:
class TarImageDataset ( torch . utils . data . IterableDataset ):
def __init__ ( self , paths ):
super (). __init__ ()
self . paths = paths
def __iter__ ( self ):
worker_info = torch . utils . data . get_worker_info ()
# For simplicity we assume num_workers is equal to number of tar files
if worker_info is None or worker_info . num_workers != len ( self . paths ):
raise ValueError ( "Number of workers doesn't match number of files." )
yield from tar_image_iterator ( self . paths [ worker_info . worker_id ])É assim que nossa classe de conjunto de dados pode ser usada:
dataloader = torch . utils . data . DataLoader (
TarImageDataset ([ "/data/imagenet_part1.tar" , "/data/imagenet_part2.tar" ]), num_workers = 2 )
for data in dataloader :
# do something with dataDiscutimos uma estratégia simples para evitar o problema de entradas duplicadas. O pacote Tfrecord usa estratégias um pouco mais sofisticadas para destruir seus dados em tempo real.
Ao usar qualquer biblioteca de computação numérica, como Numpy ou Pytorch, é importante observar que a gravação de código matematicamente correto não leva necessariamente aos resultados corretos. Você também precisa garantir que os cálculos sejam estáveis.
Vamos começar com um exemplo simples. Matematicamente, é fácil ver que x * y / y = x para qualquer valor não zero de x . Mas vamos ver se isso é sempre verdade na prática:
import numpy as np
x = np . float32 ( 1 )
y = np . float32 ( 1e-50 ) # y would be stored as zero
z = x * y / y
print ( z ) # prints nan O motivo do resultado incorreto é que y é simplesmente pequeno demais para o tipo float32. Um problema semelhante ocorre quando y é muito grande:
y = np . float32 ( 1e39 ) # y would be stored as inf
z = x * y / y
print ( z ) # prints nanO menor valor positivo que o tipo float32 pode representar é de 1.4013e-45 e qualquer coisa abaixo que seria armazenada como zero. Além disso, qualquer número além de 3.40282e+38 seria armazenado como inf.
print ( np . nextafter ( np . float32 ( 0 ), np . float32 ( 1 ))) # prints 1.4013e-45
print ( np . finfo ( np . float32 ). max ) # print 3.40282e+38Para garantir que seus cálculos sejam estáveis, você deseja evitar valores com valor absoluto pequeno ou muito grande. Isso pode parecer muito óbvio, mas esse tipo de problema pode se tornar extremamente difícil de depurar, especialmente ao fazer descendência de gradiente em Pytorch. Isso ocorre porque você não apenas precisa garantir que todos os valores no passe para a frente estejam dentro do intervalo válido de seus tipos de dados, mas também precisará garantir o mesmo para o passe para trás (durante o gradiente de computação).
Vejamos um exemplo real. Queremos calcular o softmax em um vetor de logits. Uma implementação ingênua ficaria assim:
import torch
def unstable_softmax ( logits ):
exp = torch . exp ( logits )
return exp / torch . sum ( exp )
print ( unstable_softmax ( torch . tensor ([ 1000. , 0. ])). numpy ()) # prints [ nan, 0.] Observe que a computação da exponencial de logits para números relativamente pequenos resulta em resultados gigantescos que estão fora da faixa de float32. O maior logit válido para a nossa implementação ingênua de softmax é ln(3.40282e+38) = 88.7 , qualquer coisa além disso levar a um resultado de NAN.
Mas como podemos tornar isso mais estável? A solução é bastante simples. É fácil ver que exp(x - c) Σ exp(x - c) = exp(x) / Σ exp(x) . Portanto, podemos subtrair qualquer constante dos logits e o resultado permaneceria o mesmo. Escolhemos essa constante para ser o máximo de logits. Dessa forma, o domínio da função exponencial seria limitado a [-inf, 0] e, consequentemente, seu intervalo seria [0.0, 1.0] o que é desejável:
import torch
def softmax ( logits ):
exp = torch . exp ( logits - torch . reduce_max ( logits ))
return exp / torch . sum ( exp )
print ( softmax ( torch . tensor ([ 1000. , 0. ])). numpy ()) # prints [ 1., 0.] Vejamos um caso mais complicado. Considere que temos um problema de classificação. Utilizamos a função Softmax para produzir probabilidades a partir de nossos logits. Em seguida, definimos nossa função de perda como a entropia cruzada entre nossas previsões e os rótulos. Lembre -se de que a entropia cruzada para uma distribuição categórica pode ser simplesmente definida como xe(p, q) = -Σ p_i log(q_i) . Portanto, uma implementação ingênua da entropia cruzada seria a seguinte:
def unstable_softmax_cross_entropy ( labels , logits ):
logits = torch . log ( softmax ( logits ))
return - torch . sum ( labels * logits )
labels = torch . tensor ([ 0.5 , 0.5 ])
logits = torch . tensor ([ 1000. , 0. ])
xe = unstable_softmax_cross_entropy ( labels , logits )
print ( xe . numpy ()) # prints infObserve que, nesta implementação, à medida que a saída do softmax se aproxima de zero, a saída do log se aproxima do infinito, o que causa instabilidade em nossa computação. Podemos reescrever isso expandindo o softmax e fazendo algumas simplificações:
def softmax_cross_entropy ( labels , logits , dim = - 1 ):
scaled_logits = logits - torch . max ( logits )
normalized_logits = scaled_logits - torch . logsumexp ( scaled_logits , dim )
return - torch . sum ( labels * normalized_logits )
labels = torch . tensor ([ 0.5 , 0.5 ])
logits = torch . tensor ([ 1000. , 0. ])
xe = softmax_cross_entropy ( labels , logits )
print ( xe . numpy ()) # prints 500.0Também podemos verificar se os gradientes também são calculados corretamente:
logits . requires_grad_ ( True )
xe = softmax_cross_entropy ( labels , logits )
g = torch . autograd . grad ( xe , logits )[ 0 ]
print ( g . numpy ()) # prints [0.5, -0.5]Deixe -me lembrar novamente que o cuidado extra deve ser tomado ao fazer uma descida de gradiente para garantir que o alcance de suas funções e os gradientes para cada camada esteja dentro de um intervalo válido. As funções exponenciais e logarítmicas quando usadas ingenuamente são especialmente problemáticas porque podem mapear pequenos números para enormes e o contrário.
Por padrão, os tensores e os parâmetros do modelo em Pytorch são armazenados em precisão de ponto flutuante de 32 bits. O treinamento de redes neurais usando carros alegóricas de 32 bits é geralmente estável e não causa grandes problemas numéricos, no entanto, as redes neurais demonstraram ter um bom desempenho em precisões de 16 bits e ainda mais baixas. A computação em precisões mais baixas pode ser significativamente mais rápida nas GPUs modernas. Ele também tem o benefício extra de usar menos memória, permitindo o treinamento de modelos maiores e/ou com tamanhos maiores de lote, o que pode aumentar ainda mais o desempenho. O problema é que o treinamento em 16 bits geralmente se torna muito instável porque a precisão geralmente não é suficiente para realizar algumas operações, como acumulações.
Para ajudar com esse problema, a Pytorch suporta o treinamento em precisão mista. Em poucas palavras, o treinamento de precisão mista é realizada executando algumas operações caras (como convoluções e multplicações matriciais) em 16 bits, lançando as entradas enquanto executava outras operações numericamente sensíveis, como acumulações em 32 bits. Dessa forma, obtemos todos os benefícios da computação de 16 bits sem suas desvantagens. Em seguida, conversamos sobre o uso do Autocast e Gradscaler para fazer treinamento automático de precisão mista.
autocast ajuda a melhorar o desempenho do tempo de execução, colocando os dados automaticamente para 16 bits para alguns cálculos. Para entender como funciona, vamos ver um exemplo:
import torch
x = torch . rand ([ 32 , 32 ]). cuda ()
y = torch . rand ([ 32 , 32 ]). cuda ()
with torch . cuda . amp . autocast ():
a = x + y
b = x @ y
print ( a . dtype ) # prints torch.float32
print ( b . dtype ) # prints torch.float16 Observe que os x e y são tensores de 32 bits, mas autocast realiza multiplicação de matrizes em 16 bits, mantendo a operação de adição em 32 bits. E se um dos operandos estiver em 16 bits?
import torch
x = torch . rand ([ 32 , 32 ]). cuda ()
y = torch . rand ([ 32 , 32 ]). cuda (). half ()
with torch . cuda . amp . autocast ():
a = x + y
b = x @ y
print ( a . dtype ) # prints torch.float32
print ( b . dtype ) # prints torch.float16 Novamente, autocast e derruba o operando de 32 bits para 16 bits para realizar a multiplicação da matriz, mas não altera a operação de adição. Por padrão, a adição de dois tensores em Pytorch resulta em um elenco para maior precisão.
Na prática, você pode confiar autocast para fazer o elenco certo para melhorar a eficiência do tempo de execução. O importante é manter todos os seus cálculos de passagem para a frente em contexto autocast :
model = ...
loss_fn = ...
with torch . cuda . amp . autocast ():
outputs = model ( inputs )
loss = loss_fn ( outputs , targets )Talvez tudo o que você precisa se tiver um problema de otimização relativamente estável e se usar uma taxa de aprendizado relativamente baixa. Adicionar esta linha de código extra pode reduzir seu treinamento até metade do hardware moderno.
Como mencionamos no início desta seção, a precisão de 16 bits nem sempre é suficiente para alguns cálculos. Um caso particular de interesse está representando valores de gradiente, uma ótima parte dos quais geralmente são pequenos valores. Representá-los com carros alegóricos de 16 bits geralmente leva a tampões de fluxos (ou seja, eles seriam representados como zeros). Isso torna as redes neurais de treinamento muito instáveis. GradScalar foi projetado para resolver esse problema. É necessário como inserir seu valor de perda e multiplica-o por um grande escalar, inflando valores de gradiente e, portanto, tornando-os representáveis em precisão de 16 bits. Em seguida, ele os escala durante a atualização do gradiente para garantir que os parâmetros sejam atualizados corretamente. É geralmente o que GradScalar faz. Mas, sob o cofre, GradScalar é um pouco mais inteligente do que isso. Inflar os gradientes pode realmente resultar em transbordamentos, o que é igualmente ruim. Portanto, GradScalar realmente monitora os valores do gradiente e, se detectar transbordamentos, ele ignora as atualizações, diminuindo o fator escalar de acordo com um cronograma configurável. (O cronograma padrão geralmente funciona, mas pode ser necessário ajustá -lo para o seu caso de uso.)
Usar GradScalar é muito fácil na prática:
scaler = torch . cuda . amp . GradScaler ()
loss = ...
optimizer = ... # an instance torch.optim.Optimizer
scaler . scale ( loss ). backward ()
scaler . step ( optimizer )
scaler . update () Observe que primeiro criamos uma instância do GradScalar . No loop de treinamento, chamamos de GradScalar.scale para escalar a perda antes de ligar para trás para produzir gradientes inflados, usamos GradScalar.step que (pode) atualizar os parâmetros do modelo. Em seguida, chamamos GradScalar.update , que executa a atualização escalar, se necessário. Isso é tudo!
A seguir, é apresentado um código de amostra que mostra os casos de treinamento de precisão misturada em um problema sintético de aprender para gerar um quadro de xadrez a partir de coordenadas de imagem. Você pode colá-lo em um Google Colab, definir o back-end na GPU e comparar o desempenho único e de precisão mista. Observe que este é um pequeno exemplo de brinquedo, na prática com redes maiores, você pode ver maiores impulsos no desempenho usando precisão mista.
import torch
import matplotlib . pyplot as plt
import time
def grid ( width , height ):
hrange = torch . arange ( width ). unsqueeze ( 0 ). repeat ([ height , 1 ]). div ( width )
vrange = torch . arange ( height ). unsqueeze ( 1 ). repeat ([ 1 , width ]). div ( height )
output = torch . stack ([ hrange , vrange ], 0 )
return output
def checker ( width , height , freq ):
hrange = torch . arange ( width ). reshape ([ 1 , width ]). mul ( freq / width / 2.0 ). fmod ( 1.0 ). gt ( 0.5 )
vrange = torch . arange ( height ). reshape ([ height , 1 ]). mul ( freq / height / 2.0 ). fmod ( 1.0 ). gt ( 0.5 )
output = hrange . logical_xor ( vrange ). float ()
return output
# Note the inputs are grid coordinates and the target is a checkerboard
inputs = grid ( 512 , 512 ). unsqueeze ( 0 ). cuda ()
targets = checker ( 512 , 512 , 8 ). unsqueeze ( 0 ). unsqueeze ( 1 ). cuda () class Net ( torch . jit . ScriptModule ):
def __init__ ( self ):
super (). __init__ ()
self . net = torch . nn . Sequential (
torch . nn . Conv2d ( 2 , 256 , 1 ),
torch . nn . BatchNorm2d ( 256 ),
torch . nn . ReLU (),
torch . nn . Conv2d ( 256 , 256 , 1 ),
torch . nn . BatchNorm2d ( 256 ),
torch . nn . ReLU (),
torch . nn . Conv2d ( 256 , 256 , 1 ),
torch . nn . BatchNorm2d ( 256 ),
torch . nn . ReLU (),
torch . nn . Conv2d ( 256 , 1 , 1 ))
@ torch . jit . script_method
def forward ( self , x ):
return self . net ( x ) net = Net (). cuda ()
loss_fn = torch . nn . MSELoss ()
opt = torch . optim . Adam ( net . parameters (), 0.001 )
start_time = time . time ()
for i in range ( 500 ):
opt . zero_grad ()
outputs = net ( inputs )
loss = loss_fn ( outputs , targets )
loss . backward ()
opt . step ()
print ( loss )
print ( time . time () - start_time )
plt . subplot ( 1 , 2 , 1 ); plt . imshow ( outputs . squeeze (). detach (). cpu ());
plt . subplot ( 1 , 2 , 2 ); plt . imshow ( targets . squeeze (). cpu ()); plt . show () net = Net (). cuda ()
loss_fn = torch . nn . MSELoss ()
opt = torch . optim . Adam ( net . parameters (), 0.001 )
scaler = torch . cuda . amp . GradScaler ()
start_time = time . time ()
for i in range ( 500 ):
opt . zero_grad ()
with torch . cuda . amp . autocast ():
outputs = net ( inputs )
loss = loss_fn ( outputs , targets )
scaler . scale ( loss ). backward ()
scaler . step ( opt )
scaler . update ()
print ( loss )
print ( time . time () - start_time )
plt . subplot ( 1 , 2 , 1 ); plt . imshow ( outputs . squeeze (). detach (). cpu (). float ());
plt . subplot ( 1 , 2 , 2 ); plt . imshow ( targets . squeeze (). cpu (). float ()); plt . show ()