Accueil Intelligence artificielle Posit AI Blog : Utiliser les modules de torche

Posit AI Blog : Utiliser les modules de torche

0
Posit AI Blog : Utiliser les modules de torche



Initialementnous avons commencé à en apprendre davantage torch bases en codant un réseau neuronal simple à partir de zéro, en utilisant un seul des torchLes caractéristiques de :
tenseurs.
Alorsnous avons énormément simplifié la tâche en remplaçant la rétropropagation manuelle par
autograd. Aujourd’hui nous modulariser le réseau – à la fois dans le sens habituel et dans un sens très littéral : les opérations matricielles de bas niveau sont remplacées par torch modules.

Modules

À partir d’autres frameworks (Keras, par exemple), vous pouvez être habitué à faire la distinction entre des modèles et couches. Dans torchles deux sont des exemples de
nn_Module(), et ont donc certaines méthodes en commun. Pour ceux qui réfléchissent en termes de « modèles » et de « couches », je divise artificiellement cette section en deux parties. En réalité cependant, il n’y a pas de dichotomie : les nouveaux modules peuvent être composés de modules existants jusqu’à des niveaux de récursivité arbitraires.

Modules de base (« couches »)

Au lieu d’écrire une opération affine à la main – x$mm(w1) + b1, disons –, comme nous l’avons fait jusqu’à présent, nous pouvons créer un module linéaire. L’extrait suivant instancie une couche linéaire qui attend trois entrées de fonctionnalités et renvoie une seule sortie par observation :

Le module comporte deux paramètres, « poids » et « biais ». Les deux sont désormais pré-initialisés :

$weight
torch_tensor 
-0.0385  0.1412 -0.5436
[ CPUFloatType{1,3} ]

$bias
torch_tensor 
-0.1950
[ CPUFloatType{1} ]

Les modules sont appelables ; l’appel d’un module exécute son forward() méthode qui, pour une couche linéaire, multiplie par matrice les entrées et les poids, et ajoute le biais.

Essayons ça:

data  <- torch_randn(10, 3)
out <- l(data)

Sans surprise, out contient maintenant quelques données :

torch_tensor 
 0.2711
-1.8151
-0.0073
 0.1876
-0.0930
 0.7498
-0.2332
-0.0428
 0.3849
-0.2618
[ CPUFloatType{10,1} ]

De plus, ce tenseur sait ce qu’il faudra faire si jamais on lui demande de calculer des gradients :

AddmmBackward

Notez la différence entre les tenseurs renvoyés par les modules et ceux auto-créés. Lorsque nous créons nous-mêmes des tenseurs, nous devons passer
requires_grad = TRUE pour déclencher le calcul du gradient. Avec des modules,
torch suppose à juste titre que nous voudrons effectuer une rétropropagation à un moment donné.

Mais pour l’instant, nous n’avons pas appelé backward() encore. Ainsi, aucun gradient n’a encore été calculé :

l$weight$grad
l$bias$grad
torch_tensor 
[ Tensor (undefined) ]
torch_tensor 
[ Tensor (undefined) ]

Changeons ceci :

Error in (function (self, gradient, keep_graph, create_graph)  : 
  grad can be implicitly created only for scalar outputs (_make_grads at ../torch/csrc/autograd/autograd.cpp:47)

Pourquoi l’erreur ? Autograd s’attend à ce que le tenseur de sortie soit un scalaire, alors que dans notre exemple, nous avons un tenseur de taille (10, 1). Cette erreur ne se produira pas souvent dans la pratique, où nous travaillons avec lots d’intrants (parfois, juste un seul lot). Mais il est quand même intéressant de voir comment résoudre ce problème.

Pour que l’exemple fonctionne, nous introduisons une étape d’agrégation finale – virtuelle – en prenant la moyenne, par exemple. Appelons-le avg. Si une telle moyenne était prise, sa pente par rapport à l$weight serait obtenu via la règle de chaîne :

\[
\begin{equation*}
\frac{\partial \ avg}{\partial w} = \frac{\partial \ avg}{\partial \ out} \ \frac{\partial \ out}{\partial w}
\end{equation*}
\]

Parmi les quantités du côté droit, nous nous intéressons à la seconde. Nous devons fournir le premier, à quoi il ressemblerait si vraiment on prenait la moyenne:

d_avg_d_out <- torch_tensor(10)$`repeat`(10)$unsqueeze(1)$t()
out$backward(gradient = d_avg_d_out)

Maintenant, l$weight$grad et l$bias$grad faire contient des dégradés :

l$weight$grad
l$bias$grad
torch_tensor 
 1.3410  6.4343 -30.7135
[ CPUFloatType{1,3} ]
torch_tensor 
 100
[ CPUFloatType{1} ]

En plus de nn_linear() , torch fournit à peu près toutes les couches communes que vous pourriez espérer. Mais peu de tâches sont résolues par une seule couche. Comment les combiner ? Ou, dans le jargon habituel : comment construisez-vous
des modèles?

Modules conteneurs (« modèles »)

Maintenant, des modèles ne sont que des modules qui contiennent d’autres modules. Par exemple, si toutes les entrées sont censées passer par les mêmes nœuds et le long des mêmes arêtes, alors nn_sequential() peut être utilisé pour construire un graphique simple.

Par exemple:

model <- nn_sequential(
    nn_linear(3, 16),
    nn_relu(),
    nn_linear(16, 1)
)

Nous pouvons utiliser la même technique que ci-dessus pour obtenir un aperçu de tous les paramètres du modèle (deux matrices de poids et deux vecteurs de biais) :

$`0.weight`
torch_tensor 
-0.1968 -0.1127 -0.0504
 0.0083  0.3125  0.0013
 0.4784 -0.2757  0.2535
-0.0898 -0.4706 -0.0733
-0.0654  0.5016  0.0242
 0.4855 -0.3980 -0.3434
-0.3609  0.1859 -0.4039
 0.2851  0.2809 -0.3114
-0.0542 -0.0754 -0.2252
-0.3175  0.2107 -0.2954
-0.3733  0.3931  0.3466
 0.5616 -0.3793 -0.4872
 0.0062  0.4168 -0.5580
 0.3174 -0.4867  0.0904
-0.0981 -0.0084  0.3580
 0.3187 -0.2954 -0.5181
[ CPUFloatType{16,3} ]

$`0.bias`
torch_tensor 
-0.3714
 0.5603
-0.3791
 0.4372
-0.1793
-0.3329
 0.5588
 0.1370
 0.4467
 0.2937
 0.1436
 0.1986
 0.4967
 0.1554
-0.3219
-0.0266
[ CPUFloatType{16} ]

$`2.weight`
torch_tensor 
Columns 1 to 10-0.0908 -0.1786  0.0812 -0.0414 -0.0251 -0.1961  0.2326  0.0943 -0.0246  0.0748

Columns 11 to 16 0.2111 -0.1801 -0.0102 -0.0244  0.1223 -0.1958
[ CPUFloatType{1,16} ]

$`2.bias`
torch_tensor 
 0.2470
[ CPUFloatType{1} ]

Pour inspecter un paramètre individuel, utilisez sa position dans le modèle séquentiel. Par exemple:

torch_tensor 
-0.3714
 0.5603
-0.3791
 0.4372
-0.1793
-0.3329
 0.5588
 0.1370
 0.4467
 0.2937
 0.1436
 0.1986
 0.4967
 0.1554
-0.3219
-0.0266
[ CPUFloatType{16} ]

Et tout comme nn_linear() ci-dessus, ce module peut être appelé directement sur les données :

Sur un module composite comme celui-ci, appelant backward() se propagera à travers toutes les couches :

out$backward(gradient = torch_tensor(10)$`repeat`(10)$unsqueeze(1)$t())

# e.g.
model[[1]]$bias$grad
torch_tensor 
  0.0000
-17.8578
  1.6246
 -3.7258
 -0.2515
 -5.8825
 23.2624
  8.4903
 -2.4604
  6.7286
 14.7760
-14.4064
 -1.0206
 -1.7058
  0.0000
 -9.7897
[ CPUFloatType{16} ]

Et placer le module composite sur le GPU y déplacera tous les tenseurs :

model$cuda()
model[[1]]$bias$grad
torch_tensor 
  0.0000
-17.8578
  1.6246
 -3.7258
 -0.2515
 -5.8825
 23.2624
  8.4903
 -2.4604
  6.7286
 14.7760
-14.4064
 -1.0206
 -1.7058
  0.0000
 -9.7897
[ CUDAFloatType{16} ]

Voyons maintenant comment utiliser nn_sequential() peut simplifier notre exemple de réseau.

Réseau simple utilisant des modules

### 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)


### define the network ---------------------------------------------------------

# dimensionality of hidden layer
d_hidden <- 32

model <- nn_sequential(
  nn_linear(d_in, d_hidden),
  nn_relu(),
  nn_linear(d_hidden, d_out)
)

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

learning_rate <- 1e-4

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

for (t in 1:200) {
  
  ### -------- Forward pass -------- 
  
  y_pred <- model(x)
  
  ### -------- compute loss -------- 
  loss <- (y_pred - y)$pow(2)$sum()
  if (t %% 10 == 0)
    cat("Epoch: ", t, "   Loss: ", loss$item(), "\n")
  
  ### -------- Backpropagation -------- 
  
  # Zero the gradients before running the backward pass.
  model$zero_grad()
  
  # compute gradient of the loss w.r.t. all learnable parameters of the model
  loss$backward()
  
  ### -------- Update weights -------- 
  
  # Wrap in with_no_grad() because this is a part we DON'T want to record
  # for automatic gradient computation
  # Update each parameter by its `grad`
  
  with_no_grad({
    model$parameters %>% purrr::walk(function(param) param$sub_(learning_rate * param$grad))
  })
  
}

La passe avant semble bien meilleure maintenant ; cependant, nous parcourons toujours les paramètres du modèle et mettons chacun à jour manuellement. De plus, vous soupçonnez peut-être déjà que torch fournit des abstractions pour les fonctions de perte courantes. Dans le prochain et dernier volet de cette série, nous aborderons ces deux points, en utilisant torch pertes et optimiseurs. À plus tard!

LAISSER UN COMMENTAIRE

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