PyTorch Autograd

Entendendo o coração da magia do PyTorch

Vaibhav Kumar Blocked Unblock Seguir Seguindo 7 de janeiro Fonte: http://bumpybrains.com/comics.php?comic=34

Vamos apenas concordar, somos todos ruins no cálculo quando se trata de grandes redes neurais. É impraticável calcular gradientes de funções compostas tão grandes resolvendo explicitamente equações matemáticas, especialmente porque essas curvas existem em um grande número de dimensões e são impossíveis de entender.

Para lidar com hiper-planos em um espaço de 14 dimensões, visualize um espaço 3-D e diga "quatorze" para si mesmo muito alto. Todo mundo faz isso – Geoffrey Hinton

É aí que entra o autograd de PyTorch. Ele abstrai a matemática complicada e nos ajuda a calcular "magicamente" gradientes de curvas de alta dimensional com apenas algumas linhas de código. Este post tenta descrever a magia do autograd.

Noções básicas de PyTorch

Precisamos saber alguns conceitos básicos de PyTorch antes de avançarmos.

Tensores : Em palavras simples, é apenas uma matriz n-dimensional em PyTorch. Tensores suportam alguns aprimoramentos adicionais que os tornam únicos: Além da CPU, eles podem ser carregados ou a GPU para cálculos mais rápidos. Na configuração .requires_grad = True eles começam a formar um gráfico para trás que rastreia todas as operações aplicadas sobre eles para calcular os gradientes usando algo chamado de gráfico de computação dinâmica (DCG) (explicado mais adiante no post).

Em versões anteriores do PyTorch, a classe torch.autograd.Variable era usada para criar tensores que suportam cálculos de gradiente e rastreamento de operação, mas a classe Variável do PyTorch v0.4.0 foi preterida. torch.Tensor e torch.autograd.Variable são agora a mesma classe. Mais precisamente, torch.Tensor é capaz de rastrear a história e se comporta como a antiga Variable

Código para mostrar várias maneiras de criar tensores ativados por gradiente

Nota : Pelo design do PyTorch, os gradientes só podem ser calculados para tensores de ponto flutuante e é por isso que eu criei um array numpy do tipo float antes de torná-lo um tensor de PyTorch ativado por gradiente.

Autograd: Esta classe é um mecanismo para calcular derivativos (produto vetorial jacobiano para ser mais preciso). Ele grava um gráfico de todas as operações realizadas em um tensor ativado por gradiente e cria um gráfico acíclico chamado de gráfico computacional dinâmico. As folhas deste gráfico são tensores de entrada e as raízes são tensores de saída. Os gradientes são calculados traçando o gráfico da raiz até a folha e multiplicando cada gradiente no caminho usando a regra da cadeia.

Redes neurais e retropropagação

Redes neurais nada mais são do que funções matemáticas compostas que são delicadamente ajustadas (treinadas) para produzir o resultado desejado. O ajuste ou o treinamento é feito através de um notável algoritmo chamado backpropagation. A retropropagação é usada para calcular os gradientes da perda em relação aos pesos de entrada para atualizar posteriormente os pesos e, eventualmente, reduzir a perda.

De certa forma, a propagação de volta é apenas o nome fantasia para a regra da cadeia – Jeremy Howard

Criar e treinar uma rede neural envolve os seguintes passos essenciais:

  1. Definir a arquitetura
  2. Encaminhar propagação na arquitetura usando dados de entrada
  3. Calcule a perda
  4. Retropropagação para calcular o gradiente para cada peso
  5. Atualize os pesos usando uma taxa de aprendizado

A alteração na perda de uma pequena alteração em um peso de entrada é chamada de gradiente desse peso e é calculada usando retropropagação. O gradiente é então usado para atualizar o peso usando uma taxa de aprendizado para reduzir globalmente a perda e treinar a rede neural.

Isso é feito de maneira iterativa. Para cada iteração, vários gradientes são calculados e algo chamado de gráfico de computação é construído para armazenar essas funções de gradiente. O PyTorch faz isso criando um Gráfico Computacional Dinâmico (DCG). Esse gráfico é construído do zero em todas as iterações, fornecendo flexibilidade máxima ao cálculo de gradiente. Por exemplo, para uma operação de avanço (função) Mul uma operação inversa (função) chamada MulBackward é dinamicamente integrada no gráfico de retrocesso para calcular o gradiente.

Gráfico Computacional Dinâmico

Os tensores habilitados para gradiente (variáveis), juntamente com as funções (operações), combinam-se para criar o gráfico computacional dinâmico. O fluxo de dados e as operações aplicadas aos dados são definidos em tempo de execução, construindo o gráfico computacional dinamicamente. Este gráfico é feito dinamicamente pela classe de autograd sob o capô. Você não precisa codificar todos os caminhos possíveis antes de iniciar o treinamento – o que você executa é o que você diferencia.

Um simples DCG para multiplicação de dois tensores seria assim:

DCG com requires_grad = False (Diagrama criado usando draw.io)

Cada caixa de contorno pontilhada no gráfico é uma variável e a caixa retangular roxa é uma operação.

Cada objeto variável possui vários membros, alguns dos quais são:

Dados : são os dados que uma variável está segurando. x detém um tensor de 1×1 com o valor igual a 1,0 enquanto y detém 2,0. z detém o produto de dois ou seja, 2.0

requires_grad : Esse membro, se true, começa a rastrear todo o histórico de operações e forma um gráfico de retorno para o cálculo do gradiente. Para um tensor arbitrário um Ele pode ser manipulado no local da seguinte forma: a.requires_grad_(True).

grad: grad mantém o valor do gradiente. Se requires_grad for False, ele terá um valor None. Mesmo se requires_grad é True, que irá realizar um valor Nenhum a menos .backward() função é chamada de algum outro nó. Por exemplo, se você chamar out.backward() para alguma variável que envolveu x em seus cálculos, em seguida, x.grad vai realizar ?out / ?x.

grad_fn: Esta é a função de retrocesso usada para calcular o gradiente.

is_leaf : Um nó é folha se:

  1. Foi inicializado explicitamente por alguma função como x = torch.tensor(1.0) ou x = torch.randn(1, 1) (basicamente todos os métodos de inicialização do tensor discutidos no início deste post).
  2. Ele é criado após operações em tensores, todos com requires_grad = False.
  3. É criado chamando o método .detach() em algum tensor.

Na ligando backward() , gradientes são calculados apenas para os nós que têm tanto requires_grad e is_leaf True.

Ao girar o require_grad requires_grad = True PyTorch irá iniciar o rastreamento da operação e armazenar as funções do gradiente em cada etapa da seguinte forma:

DCG with requires_grad = True (Diagrama criado usando draw.io)

O código que geraria o gráfico acima sob o capô do PyTorch é:

Para impedir que o PyTorch rastreie o histórico e forme o gráfico de retrocesso, o código pode ser empacotado internamente with torch.no_grad(): Ele fará o código rodar mais rápido sempre que o rastreamento de gradiente não for necessário.

Função Backward ()

Backward é a função que realmente calcula o gradiente passando seu argumento (1×1 tensor de unidade por padrão) através do gráfico de retrocesso até todo nó de folha rastreável a partir do tensor de raiz de chamada. Os gradientes calculados são então armazenados em .grad de cada nó da folha. Lembre-se, o gráfico para trás já é feito dinamicamente durante o passe para frente. A função de retrocesso calcula apenas o gradiente usando o gráfico já feito e armazena-os em nós de folha.

Vamos analisar o seguinte código

Uma coisa importante a notar é que quando z.backward() é chamado, um tensor é passado automaticamente como z.backward(torch.tensor(1.0)) . O torch.tensor(1.0) é o gradiente externo fornecido para finalizar as multiplicações de gradiente de regra de cadeia. Esse gradiente externo é passado como a entrada para a função MulBackward para calcular ainda mais o gradiente de x . A dimensão do tensor passada para .backward() deve ser a mesma que a dimensão do tensor cujo gradiente está sendo calculado. Por exemplo, se o gradiente ativado tensor x e y forem os seguintes:

x = torch.tensor([0.0, 2.0, 8.0], requires_grad = True)

y = torch.tensor([5.0 , 1.0 , 7.0], requires_grad = True)

z = x * y

então, para calcular gradientes de z (um tensor de 1×3) em relação a x ou y , um gradiente externo precisa ser passado para a função z.backward() como segue: z.backward(torch.FloatTensor([1.0, 1.0, 1.0])

z.backward() daria um RuntimeError: grad can be implicitly created only for scalar outputs

O tensor passado para a função de retorno funciona como pesos para uma saída ponderada de gradiente. Matematicamente, este é o vector multiplicado pela matriz Jacobiana da tensores não escalares (discutidas mais adiante nesta pós), portanto, deve estar quase sempre um tensor unidade de dimensão igual ao tensor backward é chamado, a não ser que as saídas ponderadas precisa ser calculado .

tldr: O gráfico para trás é criado automaticamente e dinamicamente pela classe autograd durante o forward forward. Backward() simplesmente calcula os gradientes passando seu argumento para o grafo retroativo já feito.

Matemática – Jacobians e vetores

Matematicamente, a classe de autograd é apenas um mecanismo de computação de produto vetorial jacobiano. Uma matriz jacobiana em palavras muito simples é uma matriz representando todas as possíveis derivadas parciais de dois vetores. É o gradiente de um vetor em relação a outro vetor.

Se um vetor X = [x1, x2,… .xn] for usado para calcular algum outro vetor f (X) = [f1, f2,…. fn] através de uma função f, então a matriz jacobiana ( J ) contém simplesmente todas as combinações de derivadas parciais como segue:

Matriz Jacobiana (Fonte: Wikipedia)

Matriz acima representa o gradiente de f (X) em relação a X

Suponha que um gradiente PyTorch habilitasse os tensores X como:

X = [x1, x2,… .. xn] (Que este seja o peso de algum modelo de aprendizado de máquina)

X sofre algumas operações para formar um vetor Y

Y = f (X) = [y1, y2,…. yn]

Y é então usado para calcular uma perda escalar l. Suponha que um vetor v seja o gradiente da perda escalar l em relação ao vetor Y da seguinte forma:

O vetor v é chamado de grad_tensor e passado para a função backward() como um argumento

Para obter o gradiente da perda l em relação aos pesos X, a matriz jacobiana J é multiplicada por vetores com o vetor v

Esse método de calcular a matriz jacobiana e multiplicá-la por um vetor v permite a possibilidade de o PyTorch alimentar gradientes externos com facilidade até mesmo para as saídas não-escalares.

Leitura adicional

Retropropagação: revisão rápida

PyTorch: Pacote de diferenciação automática – torch.autograd

Código-fonte Autograd

Vídeo: PyTorch Autograd Explained – Tutorial detalhado por Elliot Waite

Obrigado pela leitura! Sinta-se à vontade para expressar qualquer dúvida nas respostas.