Accueil Intelligence artificielle Posit AI Blog : Présentation de la torche automatique

Posit AI Blog : Présentation de la torche automatique

0
Posit AI Blog : Présentation de la torche automatique



La semaine dernière, nous avons vu comment coder un réseau simple à partir de zéroen utilisant rien d’autre que torch tenseurs. Prédictions, perte, gradients, mises à jour de poids – toutes ces choses que nous avons calculées nous-mêmes. Aujourd’hui, nous apportons un changement important : nous nous épargnons du calcul fastidieux des pentes et avons torch faites-le pour nous.

Avant cela, faisons un peu de contexte.

Différenciation automatique avec autograd

torch utilise un module appelé autograd à

  1. enregistrer les opérations effectuées sur les tenseurs, et

  2. stocker ce qu’il faudra faire pour obtenir les gradients correspondants, une fois que nous entrons dans la passe arrière.

Ces actions prospectives sont stockées en interne sous forme de fonctions, et lorsqu’il est temps de calculer les gradients, ces fonctions sont appliquées dans l’ordre : L’application démarre à partir du nœud de sortie et les gradients calculés sont successivement propagé dos à travers le réseau. C’est une forme de différenciation automatique en mode inverse.

Autograd les bases

En tant qu’utilisateurs, nous pouvons voir un peu la mise en œuvre. Comme condition préalable à cet « enregistrement », des tenseurs doivent être créés avec
requires_grad = TRUE. Par exemple:

Pour être clair, x c’est maintenant un tenseur à l’égard duquel les gradients doivent être calculés – normalement, un tenseur représentant un poids ou un biais, pas les données d’entrée. Si nous effectuons ensuite une opération sur ce tenseur, en attribuant le résultat à y,

nous trouvons que y a maintenant un non vide grad_fn ça raconte torch comment calculer le gradient de y en ce qui concerne x:

MeanBackward0

Réel calcul des dégradés est déclenché en appelant backward()
sur le tenseur de sortie.

Après backward() a été appelé, x a un champ non nul appelé
grad qui stocke le dégradé de y en ce qui concerne x:

torch_tensor 
 0.2500  0.2500
 0.2500  0.2500
[ CPUFloatType{2,2} ]

Avec des chaînes de calculs plus longues, nous pouvons jeter un coup d’œil à la façon dont torch
construit un graphique des opérations en arrière. Voici un exemple légèrement plus complexe – n’hésitez pas à le sauter si vous n’êtes pas du genre à simplement
a jeter un œil aux choses pour qu’elles aient un sens.

Creuser plus profond

Nous construisons un graphe simple de tenseurs, avec des entrées x1 et x2 étant connecté à la sortie out par des intermédiaires y et 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$mean()

Pour économiser de la mémoire, les dégradés intermédiaires ne sont normalement pas stockés. Appel retain_grad() sur un tenseur permet de s’écarter de ce défaut. Faisons ceci ici, à des fins de démonstration :

y$retain_grad()

z$retain_grad()

Nous pouvons maintenant revenir en arrière dans le graphique et inspecter torchle plan d’action de backprop, à partir de out$grad_fnainsi:

# how to compute the gradient for mean, the last operation executed
out$grad_fn
MeanBackward0
# how to compute the gradient for the multiplication by 3 in z = y.pow(2) * 3
out$grad_fn$next_functions
[[1]]
MulBackward1
# how to compute the gradient for pow in z = y.pow(2) * 3
out$grad_fn$next_functions[[1]]$next_functions
[[1]]
PowBackward0
# how 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
# how to compute the gradient for the two branches of y = x * (x + 2),
# where the left branch 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
# here we arrive at the other 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

Si nous appelons maintenant out$backward()tous les tenseurs du graphique verront leurs gradients respectifs calculés.

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} ]

Après cette excursion ringard, voyons comment autograd simplifie notre réseau.

Le réseau simple, désormais utilisé autograd

Grâce à autograd, nous disons adieu au processus fastidieux et sujet aux erreurs de codage nous-mêmes de la rétro-propagation. Un seul appel de méthode fait tout : loss$backward().

Avec torch en gardant une trace des opérations selon les besoins, nous n’avons même plus besoin de nommer explicitement les tenseurs intermédiaires. Nous pouvons coder la passe avant, le calcul des pertes et la passe arrière en seulement trois lignes :

y_pred <- x$mm(w1)$add(b1)$clamp(min = 0)$mm(w2)$add(b2)
  
loss <- (y_pred - y)$pow(2)$sum()

loss$backward()

Voici le code complet. Nous sommes à un stade intermédiaire : nous calculons toujours manuellement la passe avant et la perte, et nous mettons toujours à jour manuellement les poids. À cause de ce dernier point, il y a quelque chose que je dois expliquer. Mais je vous laisse d’abord découvrir la nouvelle version :

library(torch)

### generate training data -----------------------------------------------------

# input dimensionality (number of input features)
d_in <- 3
# output dimensionality (number of predicted features)
d_out <- 1
# number of observations in training set
n <- 100


# create random data
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 input 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)

### network parameters ---------------------------------------------------------

learning_rate <- 1e-4

### training loop --------------------------------------------------------------

for (t in 1:200) {
  ### -------- Forward pass --------
  
  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$item(), "\n")
  
  ### -------- Backpropagation --------
  
  # compute gradient of loss w.r.t. all tensors with requires_grad = TRUE
  loss$backward()
  
  ### -------- Update weights -------- 
  
  # Wrap in with_no_grad() because this is a part we DON'T 
  # want to record for automatic 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 every pass, as they'd accumulate otherwise
     w1$grad$zero_()
     w2$grad$zero_()
     b1$grad$zero_()
     b2$grad$zero_()  
   })

}

Comme expliqué ci-dessus, après some_tensor$backward()tous les tenseurs qui le précèdent dans le graphique auront leur grad champs renseignés. Nous utilisons ces champs pour mettre à jour les poids. Mais maintenant que
autograd est « on », chaque fois que nous exécutons une opération, nous ne le faites pas souhaitez être enregistré pour le backprop, nous devons l’exempter explicitement : c’est pourquoi nous englobons les mises à jour de poids dans un appel à with_no_grad().

Bien que ce soit quelque chose que vous puissiez classer sous la rubrique « bon à savoir » – après tout, une fois arrivés au dernier article de la série, cette mise à jour manuelle des poids aura disparu – l’idiome de remise à zéro des gradients est là pour rester : les valeurs stockées dans grad les champs s’accumulent ; chaque fois que nous avons fini de les utiliser, nous devons les mettre à zéro avant de les réutiliser.

Perspectives

Alors, où en sommes-nous ? Nous avons commencé à coder un réseau entièrement à partir de zéro, en n’utilisant rien d’autre que torch tenseurs. Aujourd’hui, nous avons reçu une aide importante de autograd.

Mais nous mettons toujours à jour manuellement les pondérations – et les frameworks d’apprentissage en profondeur ne sont-ils pas connus pour fournir des abstractions (« couches » ou : « modules ») en plus des calculs tensoriels… ?

Nous abordons les deux problèmes dans les versements suivants. Merci d’avoir lu!

LAISSER UN COMMENTAIRE

S'il vous plaît entrez votre commentaire!
S'il vous plaît entrez votre nom ici