knitr::opts_chunk$set(echo = TRUE)
library(keras)
library(ggplot2)
library(dplyr)
library(corrplot) # pour représenter la matrice des corrélations empiriques
library(factoextra) # pour avoir des représentations graphiques pour l'ACP
library(tensorflow)
#library(reticulate)
# use_miniconda("r-reticulate")
if (tensorflow::tf$executing_eagerly()) {
  tensorflow::tf$compat$v1$disable_eager_execution()
}
K <- keras::backend()

Introduction : l’apprentissage profond en pratique

Il existe deux grandes “bibliothèques logicielles” pour faire de l’apprentissage profond (deep learning).

  1. Pytorch est une “bibliothèque logicielle” écrite en C++, Python et C, basée sur Torch, développée par les équipes d’intelligence artificielle (IA) de Facebook, déposée en open source sur GitHub en 2017. Selon les blogs, Pytorch est réputé être plutôt pour les développeurs orientés recherche, plus convivial, plus facile à adapter et debugger.

  2. TensorFlow est une plateforme Open-Source, proposant un ensemble complet et flexible d’outils, bibliothèques et ressources communautaires pour le “machine learning”. C’est aussi une bibliothèque logicielle pour le calcul numérique utilisant des graphiques de flux de données. Les noeuds représentent des fonctions mathématiques et les arêtes des tableaux multidimensionnels (tenseurs) communiquant entre eux. TensorFlow a été développé par les équipes de Google et publié en 2015. Le “+” revendiqué est le côté écosystème complet, pas besoin de gérer CPU/GPU et facilitation de mise en production d’outils liés à l’IA.

Keras est une interface “high-level” pour construire et entraîner des modèles d’apprentissage profond, développée par F. Chollet et qui sert d’interface pour TensorFlow, mais aussi The MicroSoft Cognitive ToolKit and Theano. Keras ne se charge pas directement des opérations de bas niveau comme les produits ou les convolutions de Tensor, et s’appuie sur un moteur backend (par exemple TensorFlow). Keras inclut aussi des ensembles de données et des modèles bien connus, pré-entraînés (sur ImageNet).

Il semble qu’aujourd’hui les deux ont travaillé sur leurs points faibles.

Et avec R ?

Il existe des bibliothèques R de mêmes noms que celles Python, tensorflow, keras, torch.

  1. torch : construction sur PyTorch C++, pas de dépendance à Python, installation plus facile.

  2. Il existe un projet RStudio autour de TensorFlow https://tensorflow.rstudio.com/ (et donc documentation assez complète) Pour les utilisateurs de R, tensorflow et keras sont basés sur reticulate et revient à utiliser des packages Python depuis R.

L’interface Keras a été portée sous R par JJ. Allaire, et évolue avec la philosophie “make it R-like and natural, where possible and make it easy to port from Python, where necessary”. Vous pouvez lire l’article de blog ci-dessous pour plus d’infos sur les dernières évolutions. Il existe aussi le livre Deep Learning with R écrit par F. Chollet et JJ Allaire dont une deuxième version est attendue sous peu.

“Cheatsheet” pour le package R keras https://raw.githubusercontent.com/rstudio/cheatsheets/master/keras.pdf

Objectif du tuto

Comprendre les commandes permettant de construire et entraîner un autoencodeur (variationnel) avec le package keras, depuis RStudio, avec un objectif de réduction de dimension.

Quelques ateliers passés autour de l’apprentissage profond :

Pré-requis et installation

Pour installer tensorflow et keras et travailler depuis RStudio, suivre les consignes d’installation disponibles ici.

Sous Ubuntu, on est un peu pris par la main. Installer les packages keras et tensorflow.
Si TensorFlow n’est pas installé sur notre machine, il suffira de lancer la fonction tensorflow::install_tensorflow() mais là encore, vous devriez avoir un message explicite. Si besoin on vous proposera d’installer Miniconda.

Pour vérifier son installation tensorflow :

library(tensorflow)
tf$constant("Hellow Tensorflow")

Possiblement on verra apparaître des messages d’erreur qui en fait ne concernent que les calculs sur GPU. Si vous n’avez pas de carte GPU ou ne souhaitez pas faire de calculs sur GPU vous pouvez ignorer les messages.

Si vous souhaitez pouvoir utiliser les fonctionnalités GPU, vous aurez sans doute besoin d’installation supplémentaire.

Remarque : pour avoir des infos sur sa carte graphique sous Ubuntu, taper lspci | egrep "3D|VGA" dans une console.

Recette keras

Pour définir le modèle puis l’ajuster, on utilisera toujours les étapes suivantes.

1. Construction du modèle (configuration des couches)

Dans keras on assemble des couches pour construire un modèle (mode de construction le plus courant est le mode séquentiel).

layer_input(shape = NULL)

layer_dense(object, units, activation = NULL, use_bias = TRUE, input_shape = NULL)

Outre la fonction d’activation à spécifier, les schémas d’initialisation des poids (kernel_initializer) ou de régularisation des poids (kernel_regularizer) peuvent être importants.

2. Processus d’apprentissage

a. Configuration (methode compile)

compile(loss='mean_squared_error', optimizer='sgd')

  • optimizer : optimiseur, souvent "rmsprop" par défaut.

  • loss: fonction de perte à minimiser.

  • metrics: métriques à évaluer pendant l’entrainement et le test du modèle.

Ex : pour de la classification

optimizer = 'adam', loss = 'categorical_crossentropy', metrics = list('accuracy')

b. Préparer les donnees d’apprentissage

c. Apprentissage (fit)

fit(object, x = NULL, y = NULL, bath_size = NULL, epochs = 10)

  • epochs: l’apprentissage est structuré en ‘epochs’, ie itération sur le jeu entier de données (ce qui fait en lots (‘batches’) plus petits).

  • batch_size: le modèle découpe les données en lots plus petits et itèrent sur chaque lote pendant l’entrainement. Attention si le nombre total d’échantillons n’est pas divisible par la taille du lot, le dernier batch doit être le plus petit.

3. Evaluer et prédire (predict)

Réduction de dimension

Données

Nous considérerons le jeu de données “vin rouge” de la base de données de vins portugais “Vinho Verde”. Nous disposons de 12 variables physico-chimiques et sensorielles, dont une variable de quality de vin correspondant à un score entre 0 et 10. Une courte description de chaque variable est disponible ici. Les données sont utilisées à titre purement illustratif.

wine <- read.csv("http://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv", sep = ";")
#quality <- wine$quality<6
#wine2 <- read.table("http://archive.ics.uci.edu/ml/machine-learning-databases/wine/wine.data", sep = ",")
# Taille du jeux de données
dim(wine)
## [1] 1599   12
# Aperçu des premières lignes
head(wine[,-12])
##   fixed.acidity volatile.acidity citric.acid residual.sugar chlorides
## 1           7.4             0.70        0.00            1.9     0.076
## 2           7.8             0.88        0.00            2.6     0.098
## 3           7.8             0.76        0.04            2.3     0.092
## 4          11.2             0.28        0.56            1.9     0.075
## 5           7.4             0.70        0.00            1.9     0.076
## 6           7.4             0.66        0.00            1.8     0.075
##   free.sulfur.dioxide total.sulfur.dioxide density   pH sulphates alcohol
## 1                  11                   34  0.9978 3.51      0.56     9.4
## 2                  25                   67  0.9968 3.20      0.68     9.8
## 3                  15                   54  0.9970 3.26      0.65     9.8
## 4                  17                   60  0.9980 3.16      0.58     9.8
## 5                  11                   34  0.9978 3.51      0.56     9.4
## 6                  13                   40  0.9978 3.51      0.56     9.4
# Résumé
summary(wine)
##  fixed.acidity   volatile.acidity  citric.acid    residual.sugar  
##  Min.   : 4.60   Min.   :0.1200   Min.   :0.000   Min.   : 0.900  
##  1st Qu.: 7.10   1st Qu.:0.3900   1st Qu.:0.090   1st Qu.: 1.900  
##  Median : 7.90   Median :0.5200   Median :0.260   Median : 2.200  
##  Mean   : 8.32   Mean   :0.5278   Mean   :0.271   Mean   : 2.539  
##  3rd Qu.: 9.20   3rd Qu.:0.6400   3rd Qu.:0.420   3rd Qu.: 2.600  
##  Max.   :15.90   Max.   :1.5800   Max.   :1.000   Max.   :15.500  
##    chlorides       free.sulfur.dioxide total.sulfur.dioxide    density      
##  Min.   :0.01200   Min.   : 1.00       Min.   :  6.00       Min.   :0.9901  
##  1st Qu.:0.07000   1st Qu.: 7.00       1st Qu.: 22.00       1st Qu.:0.9956  
##  Median :0.07900   Median :14.00       Median : 38.00       Median :0.9968  
##  Mean   :0.08747   Mean   :15.87       Mean   : 46.47       Mean   :0.9967  
##  3rd Qu.:0.09000   3rd Qu.:21.00       3rd Qu.: 62.00       3rd Qu.:0.9978  
##  Max.   :0.61100   Max.   :72.00       Max.   :289.00       Max.   :1.0037  
##        pH          sulphates         alcohol         quality     
##  Min.   :2.740   Min.   :0.3300   Min.   : 8.40   Min.   :3.000  
##  1st Qu.:3.210   1st Qu.:0.5500   1st Qu.: 9.50   1st Qu.:5.000  
##  Median :3.310   Median :0.6200   Median :10.20   Median :6.000  
##  Mean   :3.311   Mean   :0.6581   Mean   :10.42   Mean   :5.636  
##  3rd Qu.:3.400   3rd Qu.:0.7300   3rd Qu.:11.10   3rd Qu.:6.000  
##  Max.   :4.010   Max.   :2.0000   Max.   :14.90   Max.   :8.000
wine %>%
  select_if(is.numeric) %>%
  cor() %>% # Calcul de la matrice de corrélation empirique
  corrplot::corrplot() # représentation graphique de la matrice

scaled_data <- wine[, -12] %>%
  select_if(is.numeric) %>%
  mutate_all(.funs = scale) # On applique à toutes les colonnes la fonction scale
# La fonction scale centre et réduit un vecteur

Création de jeux de tests et d’entrainements

Ici, nous ne nous servirons pas de jeux de tests et d’entrainement. Exemple de code ci-dessous si on avait voulu en créer un pour prédire la qualité du vin.

set.seed(seed = 123)
dataset_size <- nrow(wine)
train_size <- as.integer(dataset_size * 0.85)
# Training dataset
train_dataset <- wine %>%
  group_by(quality) %>%
  sample_frac(0.85)
# Creation of the test dataset
test_dataset <- anti_join(as_tibble(wine), train_dataset)
# Scale the data
scaled_train <- train_dataset %>%
  select_if(is.numeric) %>%
  mutate_all(.funs = scale)
# Attention : il faudrait aussi mettre à l'echelle le jeu de test, peut-etre plutôt avec les valeurs du jeu d'entrainement
scaled_test <- test_dataset %>%
  select_if(is.numeric) %>%
  mutate_all(.funs = scale)

Analyse en composantes principales

On fait une ACP sur toutes les données et en utilisant les variables hors qualité du vin.

res.pca <- prcomp(scaled_data)
head(res.pca$x)
##            PC1        PC2        PC3         PC4         PC5        PC6
## [1,] -1.619023  0.4508091  1.7738992  0.04372663 -0.06699352  0.9136349
## [2,] -0.798920  1.8559724  0.9114050  0.54789457  0.01838581 -0.9294232
## [3,] -0.748245  0.8817630  1.1710279  0.41089213  0.04351739 -0.4013476
## [4,]  2.356935 -0.2698916 -0.2434125 -0.92815961  1.49868022  0.1309762
## [5,] -1.619023  0.4508091  1.7738992  0.04372663 -0.06699352  0.9136349
## [6,] -1.583212  0.5690169  1.5378050  0.02374246  0.11004154  0.9933156
##             PC7        PC8          PC9       PC10        PC11
## [1,]  0.1609928 -0.2821700 -0.005096442  0.2676757  0.04861491
## [2,]  1.0095128  0.7623485  0.520543822 -0.0628132 -0.13809868
## [3,]  0.5393847  0.5977591  0.086829762  0.1873837 -0.11819169
## [4,] -0.3441828 -0.4552326 -0.091547938  0.1303517  0.31661464
## [5,]  0.1609928 -0.2821700 -0.005096442  0.2676757  0.04861491
## [6,]  0.1096249 -0.3139360  0.034336772  0.3714947  0.03660059
## plot of the eigenvalues
factoextra::fviz_eig(res.pca)

# Plot -2D
ggplot(as.data.frame(res.pca$x), aes(x = PC1, y = PC2, col =wine$quality)) + geom_point()

# Plot - 3D
#library(plotly)
#pca_plotly <- plot_ly(as.data.frame(res.pca$x), x = ~PC1, y = ~PC2, z = ~PC3, color = ~wine$quality) %>% add_markers()
#pca_plotly

On peut effectuer une classification supervisée en utilisant la variable qualité pour vérifier que la qualité de prédiction ne diminue que très peu quand on passe des données complètes aux six premières composantes de l’ACP.

library(class)
y <- wine$quality
neigh <- knn(scaled_data, scaled_data, cl = y, k = 3)
# confusion matrix
tab <- table(neigh, y)
# accuracy
sum(diag(tab)) / sum(rowSums(tab)) * 100
## Using the 6 first PC
neigh_reduced <- knn(res.pca$x[, 1:6], res.pca$x[, 1:6], cl = y, k = 3)
tab <- table(neigh_reduced, y)
sum(diag(tab)) / sum(rowSums(tab)) * 100
# si on voulait prédire des individus supplementaires :
# Centre-reduire les individus supplementaires
# ind.scaled  <- scale(ind.sup, center = res.pca$center, scale = res.pca$scale)
# ind.sup.coord <- predict(res.pca, newdata = ind.sup)

Autoencodeur

1. Définition du modèle

Représentation schématique d’un autoencodeur

# Ecriture condensée
input_size <- 11
m <- 6 # nb de composantes
ae_1 <- keras_model_sequential()
ae_1 %>%
  layer_dense(units = m, input_shape = input_size, use_bias = TRUE, activation = "linear", name = "bottleneck") %>%
  layer_dense(units = input_size, use_bias = TRUE, activation = "linear") %>%
  summary(ae_1)
## Model: "sequential"
## ________________________________________________________________________________
##  Layer (type)                       Output Shape                    Param #     
## ================================================================================
##  bottleneck (Dense)                 (None, 6)                       72          
##                                                                                 
##  dense (Dense)                      (None, 11)                      77          
##                                                                                 
## ================================================================================
## Total params: 149
## Trainable params: 149
## Non-trainable params: 0
## ________________________________________________________________________________

On peut écrire le modèle en séparant l’encodeur et le décodeur.

# Ecriture en separant encodeur et decodeur
# Encodeur
enc_input <- layer_input(shape = input_size)
enc_output <- enc_input %>%
  layer_dense(units = m, activation = "linear")
encoder <- keras_model(enc_input, enc_output)
# Decodeur
dec_input <- layer_input(shape = m)
dec_output <- dec_input %>%
  layer_dense(units = input_size, activation = "linear")
decoder <- keras_model(dec_input, dec_output)
# Auto-encodeur
ae_2_input <- layer_input(shape = input_size)
ae_2_output <- ae_2_input %>%
  encoder() %>%
  decoder()
ae_2 <- keras_model(ae_2_input, ae_2_output)
summary(ae_2)
## Model: "model_2"
## ________________________________________________________________________________
##  Layer (type)                       Output Shape                    Param #     
## ================================================================================
##  input_3 (InputLayer)               [(None, 11)]                    0           
##                                                                                 
##  model (Functional)                 (None, 6)                       72          
##                                                                                 
##  model_1 (Functional)               (None, 11)                      77          
##                                                                                 
## ================================================================================
## Total params: 149
## Trainable params: 149
## Non-trainable params: 0
## ________________________________________________________________________________

On peut récupérer la configuration du modèle à l’aide de la fonction get_config.

# Couche encodeur : m*n poids + m terme de biais
# Couche décodeur : m*n + n termes de biais
get_config(ae_1)
## {'name': 'sequential', 'layers': [{'class_name': 'InputLayer', 'config': {'batch_input_shape': (None, 11), 'dtype': 'float32', 'sparse': False, 'ragged': False, 'name': 'bottleneck_input'}}, {'class_name': 'Dense', 'config': {'name': 'bottleneck', 'trainable': True, 'batch_input_shape': (None, 11), 'dtype': 'float32', 'units': 6, 'activation': 'linear', 'use_bias': True, 'kernel_initializer': {'class_name': 'GlorotUniform', 'config': {'seed': None}}, 'bias_initializer': {'class_name': 'Zeros', 'config': {}}, 'kernel_regularizer': None, 'bias_regularizer': None, 'activity_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}}, {'class_name': 'Dense', 'config': {'name': 'dense', 'trainable': True, 'dtype': 'float32', 'units': 11, 'activation': 'linear', 'use_bias': True, 'kernel_initializer': {'class_name': 'GlorotUniform', 'config': {'seed': None}}, 'bias_initializer': {'class_name': 'Zeros', 'config': {}}, 'kernel_regularizer': None, 'bias_regularizer': None, 'activity_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}}]}

2.a Processus d’apprentissage : compilation

On compile le modèle en ajoutant fonction de perte et optimiseur

Attention des modifications de valeurs de paramètres peuvent avoir des impacts importants. Exemple du taux d’apprentissage. A ce propos, cet argument a été renommé de lr en learning_rate (pas encore à jour sur le site de RStudio)). Le Learning rate est noté \(\rho\) dans nos slides de cours.

ae_1 %>% compile(
  loss = "mean_squared_error",
  optimizer = optimizer_sgd(learning_rate = 0.1) # stochastic gradient descent optimizer
)

2.b Ajustement du modèle aux données préparées

Attention : penser à standardiser les données au préalable et à les mettre sous forme de matrice et non de dataframe. Les messages d’erreur si on oublie de passer en matrice sont parfois un peu obscures.

epochs_nb <- 50L # L spécifie que c'est un entier
batch_size <- 10L
scaled_train <- as.matrix(scaled_data)
ae_1 %>% fit(x = scaled_train, y = scaled_train, epochs = epochs_nb, batch_size = batch_size)
# evaluate the performance of the model
mse.ae <- evaluate(ae_1, scaled_train, scaled_train)
mse.ae
##      loss 
## 0.1455614

Si on ne précise pas la taille ds “batchs”, elle est fixée à 32. epochs est à 10 par défaut. On peut utiliser l’argument verbose = 0 pour ne plus avoir l’addiche des résultats par epoch.

On peut s’amuser à changer le nombre d’epoch, la taille des batchs et regarder ce que ça change.

On peut remarques qu’il n’y a aucune contrainte sur les poids “décodeurs” et les poids “encodeurs”.

poids <- get_weights(ae_1)
# Poids encodeurs/ decodeurs par defaut
w_encodeur <- poids[[1]] %>% print()
##              [,1]        [,2]        [,3]        [,4]        [,5]          [,6]
##  [1,]  0.23468414  0.06929202  0.17294656  0.15061887  0.23833768  0.0056886827
##  [2,]  0.33239827 -0.15120839  0.15678896  0.02139481 -0.45791674 -0.0717435107
##  [3,]  0.03911459  0.10943296 -0.03852385  0.22915500  0.27809152  0.1401681453
##  [4,]  0.12761573  0.32623178 -0.31349796  0.16859703  0.06419385 -0.6000732183
##  [5,]  0.36893070 -0.18268254 -0.24759518 -0.01182475 -0.26909330  0.3475288749
##  [6,] -0.15080374 -0.35679284 -0.07660953  0.25706884  0.04749735  0.0451730974
##  [7,] -0.02730569 -0.45066795  0.03646427  0.32723713 -0.01812807  0.1201260686
##  [8,]  0.32202190 -0.07197118 -0.02763597 -0.25654280  0.29900402 -0.3886266649
##  [9,] -0.30189106  0.16621883 -0.29382595 -0.39581701  0.02554698 -0.3968795240
## [10,] -0.03883547  0.02805572 -0.53302705 -0.28257433  0.18249787  0.1098793373
## [11,] -0.28967756  0.54928517 -0.22669598  0.35186616 -0.07382300 -0.0000277053
w_decodeur <- poids[[3]] %>% print()
##           [,1]        [,2]        [,3]        [,4]        [,5]        [,6]
## [1,] 0.4803997  0.39648020  0.23494892  0.37783548  0.68008435 -0.38851345
## [2,] 0.2322183 -0.06041185  0.23661095  0.05997032  0.01200666 -0.75674850
## [3,] 0.2767902  0.08524980 -0.09072675 -0.52330399 -0.69819802 -0.36067218
## [4,] 0.1840855  0.04511364  0.31249666  0.59227073  0.01198283  0.49945936
## [5,] 0.5273888 -0.84126109  0.59490931 -0.08548960 -0.37692022  0.09208596
## [6,] 0.1343837 -0.29665691  0.27479261 -0.77625358  0.44917628 -0.26834562
##             [,7]          [,8]       [,9]      [,10]      [,11]
## [1,] -0.21268399  0.5159686208 -0.5366725  0.1098122 -0.1972939
## [2,] -0.76504529 -0.1957928091 -0.1395066 -0.1072347  0.6005887
## [3,] -0.27012762 -0.0005750165 -0.2192065 -0.8509939 -0.2561342
## [4,]  0.57269549 -0.1940685809 -0.4480691 -0.2922272  0.4941948
## [5,]  0.05043474  0.5146766305 -0.2168749  0.3036602 -0.2714503
## [6,] -0.22787918 -0.3780225217 -0.4108859  0.3887524  0.1526451
# ACP : unit Norm: the weights on a layer have unit norm.
sum(w_decodeur^2) / m
## [1] 1.775577
ae_1predict <- ae_1 %>% predict(scaled_train)
# Repasser en non standardisé pour comparer...
varcol <- apply(wine[, -12], 2, var)
meancol <- colMeans(wine[, -12])
ae_1predict_or <- sapply(c(1:11), FUN = function(x) ae_1predict[, x] * sqrt(varcol[x]) + meancol[x])
# extract the bottleneck layer
intermediate_layer_model <- keras_model(inputs = ae_1$input, outputs = get_layer(ae_1, "bottleneck")$output)
intermediate_output <- predict(intermediate_layer_model, scaled_train)
ggplot(data.frame(PC1 = intermediate_output[,1], PC2 = intermediate_output[,2]), aes(x = PC1, y = PC2, col = wine$quality)) + geom_point()

Comparaison des performances de l’ACP et de l’autoencodeur

# PCA reconstruction
pca.recon <- function(pca, x, k){
  mu <- matrix(rep(res.pca$center, nrow(pca$x)), nrow = nrow(res.pca$x), byrow = T)
  recon <- res.pca$x[,1:k] %*% t(res.pca$rotation[,1:k]) + mu
  mse <- mean((recon - x)^2)
  return(list(x = recon, mse = mse))
}
xhat <- rep(NA, 10)
for(k in 1:10){
  xhat[k] <- pca.recon(res.pca, scaled_train, k)$mse
}
ae.mse <- rep(NA, 5)
input_size <- 11
#m <- 6 # nb de composantes
for(k in 1:10){
  modelk <- keras_model_sequential()
  modelk %>%
  layer_dense(units = k, input_shape = input_size, use_bias = TRUE, activation = "linear", name = "bottleneck") %>%
  layer_dense(units = input_size, use_bias = TRUE, activation = "linear")
  modelk %>% compile(
    loss = "mean_squared_error", 
    optimizer = optimizer_sgd(learning_rate = 0.1)
  )
  modelk %>% fit(
    x = scaled_train, 
    y = scaled_train, 
    epochs = 50,
    verbose = 0,
  )
  ae.mse[k] <- unname(evaluate(modelk, scaled_train, scaled_train))
}
df <- data.frame(k = c(1:10, 1:10), mse = c(xhat, ae.mse), method = c(rep("acp", 10), rep("autoencodeur", 10)))
ggplot(df, aes(x = k, y = mse, col = method)) + geom_line()

Autoencodeur variationnel

Représentation schématique d’un autoencodeur variationnel Pour construire un auto-encodeur variationnel, on effectuera deux modifications principales : une portant sur la couche intermédiaire de dimension réduite, et une sur la fonction de perte à minimiser. Vous pouvez modifier les poids \(k_1\) et \(k_2\) de deux composantes de la fonction de perte.

# Attention ecriture keras k_rendom_normal, k_shape etc.
sampling <- function(arg) {
  z_mean <- arg[, 1:(latent_dim)]
  z_log_var <- arg[, (latent_dim + 1):(2 * latent_dim)]

  epsilon <- k_random_normal(
    shape = c(k_shape(z_mean)[[1]]),
    mean = 0.,
    stddev = epsilon_std
  )

  z_mean + k_exp(z_log_var / 2) * epsilon
}
# Fonction de perte
vae_loss <- function(x, x_decoded, k1 = 1, k2 = 0.01) {
  mse_loss <- k_sum(loss_mean_squared_error(x, x_decoded))
  kl_loss <- -0.5 * k_sum(1 + z_log_var - k_square(z_mean) - k_exp(z_log_var))
  k1*mse_loss + k2*kl_loss
}
set.seed(123)
# Parameters --------------------------------------------------------------
batch_size <- 32
latent_dim <- 6L # L spécifie que c'est un entier
epochs_nb <- 50L
epsilon_std <- 1.0

# Définition du modèle --------------------------------------------------------
x <- layer_input(shape = c(input_size))
z_mean <- layer_dense(x, latent_dim)
z_log_var <- layer_dense(x, latent_dim)

# note that "output_shape" isn't necessary with the TensorFlow backend
z <- layer_concatenate(list(z_mean, z_log_var)) %>%
  layer_lambda(sampling)

# On instancie les couches séparément pour pouvoir les réutiliser plus tard
decoder <- layer_dense(units = input_size, activation = "sigmoid")
x_decoded <- decoder(z)

# end-to-end autoencoder
vae <- keras_model(x, x_decoded)

# encoder, from inputs to latent space
encoder <- keras_model(x, z_mean)

# generator, from latent space to reconstructed inputs
decoder_input <- layer_input(shape = latent_dim)
x_decoded_2 <- decoder(decoder_input)
generator <- keras_model(decoder_input, x_decoded_2)

vae %>% compile(optimizer = "sgd", loss = vae_loss) # rmsprop

vae %>% fit(x = scaled_train, y = scaled_train, epochs = epochs_nb)

x_train_encoded <- predict(encoder, scaled_train, batch_size = batch_size)
## Representation dans espace latent
x_train_encoded %>%
  as_tibble()  %>%
  ggplot(aes(x = V1, y = V2, colour = wine$quality)) +
  geom_point()
## Warning: The `x` argument of `as_tibble.matrix()` must have unique column names if `.name_repair` is omitted as of tibble 2.0.0.
## Using compatibility `.name_repair`.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was generated.

Bonus : pour obtenir des résultats plus cohérents entre ACP et autoencodeur linéaire monocouche

Architectural similarity between PCA and Autoencoder from towards data science website

L’auteur du blog nous dit que souvent les autoencodeurs sont sous-optimaux et qu’ils pourraient être optimisés si on les forçait à avoir les mêmes propriétés que l’ACP à savoir :

Tout n’est pas forcément facile à faire avec keras. Par exemple, pour créer des poids liés, il est nécessaire de créer une sous-classe.

reticulate::py_config()
## python:         /home/juaubert/.local/share/r-miniconda/envs/r-reticulate/bin/python
## libpython:      /home/juaubert/.local/share/r-miniconda/envs/r-reticulate/lib/libpython3.7m.so
## pythonhome:     /home/juaubert/.local/share/r-miniconda/envs/r-reticulate:/home/juaubert/.local/share/r-miniconda/envs/r-reticulate
## version:        3.7.11 (default, Jul 27 2021, 14:32:16)  [GCC 7.5.0]
## numpy:          /home/juaubert/.local/share/r-miniconda/envs/r-reticulate/lib/python3.7/site-packages/numpy
## numpy_version:  1.21.4
## tensorflow:     /home/juaubert/.local/share/r-miniconda/envs/r-reticulate/lib/python3.7/site-packages/tensorflow
## 
## NOTE: Python version was forced by RETICULATE_PYTHON
tensorflow::tf_config()
## TensorFlow v2.7.0 (~/.local/share/r-miniconda/envs/r-reticulate/lib/python3.7/site-packages/tensorflow)
## Python v3.7 (~/.local/share/r-miniconda/envs/r-reticulate/bin/python)

Pour aller plus loin

References

Ressources autour de tensorflow, keras et rstudio : - https://tensorflow.rstudio.com/learn/resources/ - https://raw.githubusercontent.com/rstudio/cheatsheets/master/keras.pdf - https://keras.rstudio.com/articles/examples/variational_autoencoder.html

Quelques concepts-clefs : - https://www.analyticsvidhya.com/blog/2017/05/25-must-know-terms-concepts-for-beginners-in-deep-learning/

Référence du jeu de données

P. Cortez, A. Cerdeira, F. Almeida, T. Matos and J. Reis. Modeling wine preferences by data mining from physicochemical properties. In Decision Support Systems, Elsevier, 47(4):547-553, 2009.