Na semana passada, vimos como codificar uma rede simples do zerousando nada além de torch
tensores. Previsões, perdas, gradientes, atualizações de peso – todas essas coisas que temos computado nós mesmos. Hoje, fazemos uma mudança significativa: ou seja, nos poupamos do complicado cálculo de gradientes e temos torch
faça isso por nós.
Antes disso, porém, vamos obter algumas informações básicas.
Diferenciação automática com autograduação
torch
usa um módulo chamado autograduação para
registrar operações realizadas em tensores, e
armazene o que deverá ser feito para obter os gradientes correspondentes, uma vez que entramos na passagem para trás.
Essas ações prospectivas são armazenadas internamente como funções e, quando chega a hora de calcular os gradientes, essas funções são aplicadas em ordem: A aplicação começa no nó de saída e os gradientes calculados são sucessivamente propagado voltar através da rede. Esta é uma forma de diferenciação automática de modo reverso.
Autogradação noções básicas
Como usuários, podemos ver um pouco da implementação. Como pré-requisito para que esta “gravação” aconteça, os tensores devem ser criados com
requires_grad = TRUE
. Por exemplo:
Para ser claro, x
agora é um tensor em relação ao qual os gradientes devem ser calculados – normalmente, um tensor representando um peso ou uma tendência, não os dados de entrada. Se posteriormente realizarmos alguma operação nesse tensor, atribuindo o resultado a y
,
encontramos isso y
agora tem um não vazio grad_fn
isso diz torch
como calcular o gradiente de y
em relação a x
:
MeanBackward0
Actual computação de gradientes é acionado chamando backward()
no tensor de saída.
Depois backward()
foi chamado, x
tem um campo não nulo denominado
grad
que armazena o gradiente de y
em relação a x
:
torch_tensor
0.2500 0.2500
0.2500 0.2500
( CPUFloatType{2,2} )
Com cadeias mais longas de cálculos, podemos dar uma olhada em como torch
constrói um gráfico de operações reversas. Aqui está um exemplo um pouco mais complexo – sinta-se à vontade para pular se você não for do tipo que simplesmente
tem espiar as coisas para que elas façam sentido.
Indo mais fundo
Construímos um gráfico simples de tensores, com entradas x1
e x2
sendo conectado à saída out
por intermediários y
e z
.
x1 <- torch_ones(2, 2, requires_grad = TRUE)
x2 <- torch_tensor(1.1, requires_grad = TRUE)
y <- x1 * (x2 + 2)
z <- y$pow(2) * 3
out <- z$imply()
Para economizar memória, os gradientes intermediários normalmente não são armazenados. Chamando retain_grad()
em um tensor permite desviar-se desse padrão. Vamos fazer isso aqui, para fins de demonstração:
y$retain_grad()
z$retain_grad()
Agora podemos voltar no gráfico e inspecionar torch
plano de ação do backprop, a partir de out$grad_fn
assim:
# methods to compute the gradient for imply, the final operation executed
out$grad_fn
MeanBackward0
# methods to compute the gradient for the multiplication by 3 in z = y.pow(2) * 3
out$grad_fn$next_functions
((1))
MulBackward1
# methods to compute the gradient for pow in z = y.pow(2) * 3
out$grad_fn$next_functions((1))$next_functions
((1))
PowBackward0
# methods to compute the gradient for the multiplication in y = x * (x + 2)
out$grad_fn$next_functions((1))$next_functions((1))$next_functions
((1))
MulBackward0
# methods to compute the gradient for the 2 branches of y = x * (x + 2),
# the place the left department is a leaf node (AccumulateGrad for x1)
out$grad_fn$next_functions((1))$next_functions((1))$next_functions((1))$next_functions
((1))
torch::autograd::AccumulateGrad
((2))
AddBackward1
# right here we arrive on the different leaf node (AccumulateGrad for x2)
out$grad_fn$next_functions((1))$next_functions((1))$next_functions((1))$next_functions((2))$next_functions
((1))
torch::autograd::AccumulateGrad
Se ligarmos agora out$backward()
todos os tensores do gráfico terão seus respectivos gradientes calculados.
out$backward()
z$grad
y$grad
x2$grad
x1$grad
torch_tensor
0.2500 0.2500
0.2500 0.2500
( CPUFloatType{2,2} )
torch_tensor
4.6500 4.6500
4.6500 4.6500
( CPUFloatType{2,2} )
torch_tensor
18.6000
( CPUFloatType{1} )
torch_tensor
14.4150 14.4150
14.4150 14.4150
( CPUFloatType{2,2} )
Depois dessa excursão nerd, vamos ver como autograduação torna nossa rede mais simples.
A rede simples, agora usando autograduação
Graças a autograduaçãodizemos adeus ao processo tedioso e sujeito a erros de codificar a retropropagação nós mesmos. Uma única chamada de método faz tudo: loss$backward()
.
Com torch
acompanhando as operações conforme necessário, nem precisamos mais nomear explicitamente os tensores intermediários. Podemos codificar passagem para frente, cálculo de perda e passagem para trás em apenas três linhas:
y_pred <- x$mm(w1)$add(b1)$clamp(min = 0)$mm(w2)$add(b2)
loss <- (y_pred - y)$pow(2)$sum()
loss$backward()
Aqui está o código completo. Estamos em um estágio intermediário: ainda calculamos manualmente o avanço e a perda, e ainda atualizamos manualmente os pesos. Devido a este último, há algo que preciso explicar. Mas vou deixar você conferir a nova versão primeiro:
library(torch)
### generate coaching knowledge -----------------------------------------------------
# enter dimensionality (variety of enter options)
d_in <- 3
# output dimensionality (variety of predicted options)
d_out <- 1
# variety of observations in coaching set
n <- 100
# create random knowledge
x <- torch_randn(n, d_in)
y <- x(, 1, NULL) * 0.2 - x(, 2, NULL) * 1.3 - x(, 3, NULL) * 0.5 + torch_randn(n, 1)
### initialize weights ---------------------------------------------------------
# dimensionality of hidden layer
d_hidden <- 32
# weights connecting enter to hidden layer
w1 <- torch_randn(d_in, d_hidden, requires_grad = TRUE)
# weights connecting hidden to output layer
w2 <- torch_randn(d_hidden, d_out, requires_grad = TRUE)
# hidden layer bias
b1 <- torch_zeros(1, d_hidden, requires_grad = TRUE)
# output layer bias
b2 <- torch_zeros(1, d_out, requires_grad = TRUE)
### community parameters ---------------------------------------------------------
learning_rate <- 1e-4
### coaching loop --------------------------------------------------------------
for (t in 1:200) {
### -------- Ahead move --------
y_pred <- x$mm(w1)$add(b1)$clamp(min = 0)$mm(w2)$add(b2)
### -------- compute loss --------
loss <- (y_pred - y)$pow(2)$sum()
if (t %% 10 == 0)
cat("Epoch: ", t, " Loss: ", loss$merchandise(), "n")
### -------- Backpropagation --------
# compute gradient of loss w.r.t. all tensors with requires_grad = TRUE
loss$backward()
### -------- Replace weights --------
# Wrap in with_no_grad() as a result of it is a half we DON'T
# wish to document for computerized gradient computation
with_no_grad({
w1 <- w1$sub_(learning_rate * w1$grad)
w2 <- w2$sub_(learning_rate * w2$grad)
b1 <- b1$sub_(learning_rate * b1$grad)
b2 <- b2$sub_(learning_rate * b2$grad)
# Zero gradients after each move, as they'd accumulate in any other case
w1$grad$zero_()
w2$grad$zero_()
b1$grad$zero_()
b2$grad$zero_()
})
}
Como explicado acima, depois some_tensor$backward()
todos os tensores que o precedem no gráfico terão seus grad
campos preenchidos. Utilizamos esses campos para atualizar os pesos. Mas agora isso
autograduação está “ligado”, sempre que executamos uma operação, não queremos registrado para backprop, precisamos isentá-lo explicitamente: é por isso que agrupamos as atualizações de peso em uma chamada para with_no_grad()
.
Embora isso seja algo que você pode arquivar como “bom saber” – afinal, quando chegarmos ao último submit da série, essa atualização handbook de pesos desaparecerá – o idioma de zerando gradientes veio para ficar: valores armazenados em grad
os campos se acumulam; sempre que terminamos de usá-los, precisamos zerá-los antes de reutilizá-los.
Panorama
Então, onde estamos? Começamos codificando uma rede completamente do zero, usando apenas torch
tensores. Hoje, recebemos uma ajuda significativa de autograduação.
Mas ainda estamos atualizando manualmente os pesos – e não são conhecidas estruturas de aprendizado profundo por fornecer abstrações (“camadas” ou: “módulos”) sobre cálculos de tensores…?
Abordamos ambos os problemas nas parcelas seguintes. Obrigado por ler!