ANN/ORE #5 – Forward-propagation manuelle

On a vu dans un billet précédent que la construction du réseau de neurones avec le package neuralnet était notablement plus rapide qu’avec nnet. En revanche, le modèle ainsi produit ne semble pas réutilisable une fois sauvegardé dans le datastore ORE car il est beaucoup trop volumineux… (~24GB). 🙁

Je ne sais pas exactement ce qui est stocké via ore.save mais, à mon sens les éléments les plus importants du modèle (outre sa topologie fixée à priori) sont les poids synaptiques. En tout état de cause, cela ne devrait pas constituer une telle masse d’information…

En effet, avec la topologie utilisée dans les précédents billets – à savoir :

Input Layer (784 valeurs d’entrées)

Premier Hidden Layer (300 neurones)

Second Hidden Layer (100 neurones)

Output Layer (10 neurones)

Cela représente 266610 poids synaptiques [(784 + 1 ) * 300 + (300 + 1) * 100 + (100 + 1) * 10].

Mon idée est donc de récupérer directement ces poids pour les stocker dans des tables classiques. La mise en œuvre de la forward-propagation sera ensuite réalisée « manuellement » à l’aide de ces données.

> library(ORE)
Loading required package: OREbase
Loading required package: OREcommon

Attaching package: ‘OREbase’

The following objects are masked from ‘package:base’:

    cbind, data.frame, eval, interaction, order, paste, pmax, pmin, rbind, table

Loading required package: OREembed
Loading required package: OREstats
Loading required package: MASS
Loading required package: OREgraphics
Loading required package: OREeda
Loading required package: OREmodels
Loading required package: OREdm
Loading required package: lattice
Loading required package: OREpredict
Loading required package: ORExml
> 
> ore.connect(user="c##rafa", password="Password1#", conn_string="//clorai2-scan:1521/pdb_hodba08")
> 
> library(tictoc)
>
> tic()
> 
> ore.doEval(function() {
+   library(ORE)
+   library(neuralnet)
+   set.seed(3456)
+   ore.sync(table = "MNIST_TRAINING_SET")
+   mnist_training <- ore.pull(ore.get("MNIST_TRAINING_SET"))
+   
+   # -- One Hot Encoding du champ IMG_LBL
+   mnist_training_ohe <- as.data.frame(model.matrix(~.-1,mnist_training))
+   
+   # -- Construction de la formule en spécifiant les champs
+   f <- as.formula(paste(paste(paste("IMG_LBL",seq(0,9),sep=""),collapse="+"), " ~", paste(paste("P",seq(1,784),sep=""),collapse="+")))
+   
+   # -- Construction du modèle
+   nn_neuralnet <- neuralnet(f, data=mnist_training_ohe,
+                             hidden=c(300, 100),
+                             linear.output=FALSE)
+ 
+   # -- Récupération des poids du premier Layer, ajout d'un champ de tri et sauvegarde en base
+   L1 <- as.data.frame(nn_neuralnet$weights[[1]][[1]])
+   L1$"R1$$" <- seq_len(nrow(L1))                      
+   ore.drop(table = "L1")
+   ore.create(L1, table = "L1")
+   ore.exec("alter table L1 add constraint L1_PK primary key (\"R1$$\")")
+   
+   # -- Récupération des poids du second Layer, ajout d'un champ de tri et sauvegarde en base
+   L2 <- as.data.frame(nn_neuralnet$weights[[1]][[2]])
+   L2$"R1$$" <- seq_len(nrow(L2))                      
+   ore.drop(table = "L2")  
+   ore.create(L2, table = "L2")
+   ore.exec("alter table L2 add constraint L2_PK primary key (\"R1$$\")")
+   
+   # -- Récupération des poids du Layer de sortie, ajout d'un champ de tri et sauvegarde en base
+   L3 <- as.data.frame(nn_neuralnet$weights[[1]][[3]])
+   L3$"R1$$" <- seq_len(nrow(L3))                      
+   ore.drop(table = "L3")  
+   ore.create(L3, table = "L3")
+   ore.exec("alter table L3 add constraint L3_PK primary key (\"R1$$\")")
+   
+   }, ore.connect = TRUE)
NULL
> 
> toc()
1973.29 sec elapsed
> 

Ici, on a donc construit le modèle nn_neuralnet mais au lieu de le sauvegarder dans le datastore ORE, on se contente de récupérer les valeurs des poids synaptiques calculés (nn_neuralnet$weights). Ces derniers sont ensuite stockés dans des tables (L1, L2 et L3) dans la base de données. On y ajoute un champ (R1$$) qui permet d’ordonner les valeurs (et de conserver cet ordre une fois en base via l’utilisation d’une clé primaire).

On peut alors procéder au scoring du modèle. Pour cela, on charge localement (dans une session R interactive) le dataframe MNIST_TEST à partir de la table MNIST_TEST_SET. On charge aussi les dataframes P1, P2 et P3 à partir respectivement de L1, L2 et L3.

On créé aussi une fonction sigmoid qui sera chargée du calcul la fonction logistique:

> ore.sync(table = "MNIST_TEST_SET")
> mnist_test <- ore.pull(ore.get("MNIST_TEST_SET"))
> 
> ore.sync(table = "L1")
> P1 <- ore.pull(ore.get("L1"))
> 
> ore.sync(table = "L2")
> P2 <- ore.pull(ore.get("L2"))
> 
> ore.sync(table = "L3")
> P3 <- ore.pull(ore.get("L3"))
> 
> 
> sigmoid = function(x) {
+   1 / (1 + exp(-x))
+ }
> 

La propagation est alors relativement aisée à mettre en oeuvre. On peut utiliser l’opérateur de produit matriciel de R pour multiplier les éléments de chaque couche avec les poids synaptiques associés. L’ajout de biais est quant à lui réalisé via l’opérateur cbind.

> 
> mnist_test_biais <- cbind(rep(1,10000), mnist_test[,c(-1,-2)])
> 
> HL1 <- as.matrix(mnist_test_biais) %*% as.matrix(P1[,-ncol(P1)])
> HL1_sigmoid <- sigmoid(HL1)
> HL1_sigmoid <- cbind(rep(1,10000), HL1_sigmoid)
> 
> HL2 <- HL1_sigmoid %*% as.matrix(P2[,-ncol(P2)])
> HL2_sigmoid <- sigmoid(HL2)
> HL2_sigmoid <- cbind(rep(1,10000), HL2_sigmoid)
> 
> OL <- HL2_sigmoid %*% as.matrix(P3[,-ncol(P3)])
> OL_sigmoid <- sigmoid(OL)
> 
> colnames(OL_sigmoid) <- c("0","1","2","3","4","5","6","7","8","9")
> 

Le dataframe OL_sigmoid contient la couche de sortie. C’est une matrice de 10000 x 10. Pour chaque ligne, la prédiction va correspondre à la colonne ayant le résultat de la fonction d’activation le plus important:

> head(OL_sigmoid, n=2)
aaaaa           0            1            2            3            4            5            6            7            8            9
[1,] 5.034350e-21 2.567005e-20 5.541205e-21 1.909965e-23 4.049345e-09 2.895403e-22 4.900381e-17 1.000000e+00 1.094959e-27 8.415124e-12
[2,] 2.304927e-16 1.343347e-14 1.000000e+00 1.133973e-17 5.348067e-18 1.887870e-14 7.471780e-11 1.250825e-17 3.126613e-17 5.096724e-24

Pour construire une table de contingence, on peuple le vecteur « pred » avec le nom de la colonne correspondant à la valeur la plus importante de chaque ligne. On peut alors comparer cette valeur à celle du label connu (mnist_test$IMG_LBL):

> pred <- colnames(OL_sigmoid)[max.col(OL_sigmoid,ties.method="first")]
> lbl <- mnist_test$IMG_LBL
>
> table(pred, lbl)
    lbl
pred    0    1    2    3    4    5    6    7    8    9
   0  966    0    8    2    1    3    8    3    9    9
   1    0 1121    2    3    2    1    3    3    1    4
   2    3    2  977    8    6    3    3   12    9    0
   3    1    2   11  952    2   13    1    6   17   11
   4    1    1    4    1  934    4    5    4    5   15
   5    3    0    1    7    1  847   12    1   12    4
   6    2    3    5    1    7    7  924    0    5    0
   7    2    2   10   11    5    2    0  987    4   12
   8    1    4   13   18    2    8    2    2  909    5
   9    1    0    1    7   22    4    0   10    3  949
>

 

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

6 + 2 =