{{torch}}
pour la différentiation
automatiqueLe package torch
permet de faire de la
différentiation automatique à condition de réécrire son code avec les
fonctions de torch
. Il est basé sur la librairie C++
libtorch
fournie dans PyTorch. Contrairement à d’anciennes
versions, torch ne fait pas appel à python et ne
nécessite pas reticulate. L’installation en est d’autant plus
simple:
install.packages(torch)
Lors de la première utilisation, torch
vous demandera de
télécharger des fichiers supplémentaires via:
::install_torch() ## si vous avez un GPU compatible avec CUDA
torch## torch::install_torch(type = "cpu") ## sinon
torch
est surtout utilisé pour des applications en ML/IA
mais on peut aussi l’utiliser pour des calculs de gradients, hessiennes
et de l’optimisation dans des modèles plus simples. On va l’illustrer
ici pour de la régression logistique.
library(tidyverse)
## ── Attaching packages ─────────────────────────────────────── tidyverse 1.3.2 ──
## ✔ ggplot2 3.3.6 ✔ purrr 0.3.4
## ✔ tibble 3.1.8 ✔ dplyr 1.0.10
## ✔ tidyr 1.2.0 ✔ stringr 1.4.1
## ✔ readr 2.1.2 ✔ forcats 0.5.2
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag() masks stats::lag()
library(torch)
torch
fonctionne avec ses propres types numériques,
qu’il faut créer avec la fonction torch_tensor()
et ses
propres fonctions torch_*()
. Considérons un exemple très
simple: $ x x^2$
<- torch_tensor(3)
x <- torch_square(x)
y x; y
## torch_tensor
## 3
## [ CPUFloatType{1} ]
## torch_tensor
## 9
## [ CPUFloatType{1} ]
On va pouvoir calculer \(\frac{dy}{dx}\) en définissant
x
avec l’argument require_grad = TRUE
. Cet
argument va spécifier que ‘x’ est entrainable et va démarrer
l’enregistrement par autograd des opérations sur ce tenseur.
Autograd est un module de torch
qui permet de collecter
les gradients. Il le fait en enregistrant des données (tenseurs) et
toutes les opérations exécutées dans un graphe acyclique dirigé dont les
feuilles sont les tenseurs d’entrée et les racines les tenseurs de
sorties. Ces opérations sont stockées comme des fonctions et au moment
du calcul des gradients, sont appliquées depuis le noeud de sortie en
‘backpropagation’ le long du réseau.
<- torch_tensor(2, requires_grad = TRUE)
x x
## torch_tensor
## 2
## [ CPUFloatType{1} ][ requires_grad = TRUE ]
On remarque que x
possède désormais un champ
$grad
(même si ce dernier n’est pas encore défini).
$grad x
## torch_tensor
## [ Tensor (undefined) ]
Lorsqu’on calcule \(y = x^2\), ce
dernier va également hériter d’un nouveau champ
$grad_fn
:
<- torch_log(torch_square(x))
y y
## torch_tensor
## 1.3863
## [ CPUFloatType{1} ][ grad_fn = <LogBackward0> ]
$grad_fn y
## LogBackward0
qui indique comment calculer le gradient en utilisant la dérivée des fonctions composées:
\[ (g\circ f)'(x) = f'(x) \times g'(f(x)) \]
et les fonctions
\[ \frac{dx^2}{dx} = 2x \quad \frac{d \log(x)}{dx} = \frac{1}{x} \]
Le calcul effectif du gradient est déclenché lors de l’appel à la
méthode $backward()
de y
et est stocké dans le
champ $grad
de x
.
$grad ## gradient non défini x
## torch_tensor
## [ Tensor (undefined) ]
$backward()
y$grad ## gradient défini = 1 x
## torch_tensor
## 1
## [ CPUFloatType{1} ]
On a bien:
\[
\frac{dy}{dx} = \underbrace{\frac{dy}{dz}}_{\log}(z) \times
\underbrace{\frac{dz}{dx}}_{\text{power}}(x) = \frac{1}{4} \times 2*2 =
1
\] Intuitivement au moment du calcul de y
,
torch
construit un graphe computationnel qui lui permet
d’évaluer numériquement \(y\) et qui va également
servir pour calculer \(\frac{dy}{dz}\)
au moment de l’appel à la fonction $backward()
issue du
module autograd.
Essayons de reproduire le calcul dans notre exemple. Le calcul forward donne
\[ x = 2 \xrightarrow{x \mapsto x^2} z = 4 \mapsto \xrightarrow{x \mapsto \log(x)} y = \log(4) \]
Pour le calcul backward, il faut donc construire le
graphe formel suivant. La première étape du graphe est accessible via
$grad_fn
$grad_fn y
## LogBackward0
et les fonctions suivantes via $next_functions
$grad_fn$next_functions y
## [[1]]
## PowBackward0
Dans notre exemple, on a donc:
\[ \frac{dy}{dy} = 1 \xrightarrow{x \mapsto \text{logBackward}(x)} \frac{dy}{dz} = \frac{dy}{dy} \times \text{logBackward}(z) \xrightarrow{x \mapsto \text{powerBackward}(x)} \frac{dy}{dx} = \frac{dy}{dz} \times \text{logBackward}(x) \]
Dans cet exemple:
Et la propagation des dérivées donne donc
\[ \frac{dy}{dy} = 1 \to \frac{dy}{dz} = 1 \times \frac{1}{4} = \frac{1}{4} \to \frac{dy}{dx} = \frac{1}{4} \times 4 = 1 \]
Ce graphe est illustré ci-dessous pour la fonction \((x_1, x_2) \mapsto z = sin(x_2) log(x_1 x_2)\)
Pour (beaucoup) plus de détails sur le graphe computationnel, on peut consulter la documentation officielle de PyTorch.
Il faut juste noter que dans torch
, le graphe
computationnel est construit de façon dynamique, au
moment du calcul de y
.
torch
On va adopter un simple modèle de régression logistique:
\[ Y_i \sim \mathcal{B}(\sigma(\theta^T x_i)) \quad \text{avec} \quad \sigma(x) = \frac{1}{1 + e^{-x}} \]
Le but est d’estimer \(\theta\) et éventuellement les erreurs associées. On commence par générer des données.
set.seed(45)
<- 100
n <- 3
p <- matrix(rnorm(n = n*p), ncol = p, nrow = n)
X <- rnorm(3) %>% round(digits = 2)
theta <- (X %*% theta) %>% as.vector()
probs <- rbernoulli(n = n, p = probs) + 0. Y
torch
fonctionne avec ses propres types numériques,
qu’il faut créer avec la fonction torch_tensor()
.
<- torch_tensor(X)
x <- torch_tensor(Y) y
On écrit ensuite la fonction de vraisemblance
\[ \mathcal{L}(\mathbf{X}, \mathbf{y}; \theta) = \sum_{i=1}^n y_i (\theta^Tx_i) - \sum_{i=1}^n log(1 + e^{\theta^T x_i}) \]
<- function(theta, x, y) {
logistic_loss if (!is(theta, "torch_tensor")) {
stop("theta must be a torch tensor")
}<- torch_matmul(x, theta)
odds <- torch_dot(y, odds) - torch_sum(torch_log(1 + torch_exp(odds)))
log_lik return(-log_lik)
}
avant de vérifier qu’elle fonctionne:
logistic_loss(theta = torch_tensor(theta), x = x, y = y)
## torch_tensor
## 50.4573
## [ CPUFloatType{} ]
On veut ensuite définir une fonction objective à maximiser (qui ne
dépend que de theta
):
<- function(theta, verbose = TRUE) {
eval_loss <- logistic_loss(theta, x, y)
loss if (verbose) {
cat(paste(theta |> as.numeric(), collapse=", "), ": ", as.numeric(loss), "\n")
}return(loss)
}
et vérifier qu’elle fonctionne
eval_loss(torch_tensor(theta), verbose = FALSE)
## torch_tensor
## 50.4573
## [ CPUFloatType{} ]
avant de procéder à l’optimisation à proprement parler. Pour cette dernière, on commence par définir notre paramètre sous forme d’un tenseur qui va être mis à jour
<- torch_tensor(rep(0, length(theta)), requires_grad = TRUE) theta_current
et d’un optimiseur:
<- optim_rprop(theta_current) theta_optimizer
On considère ici l’optimiseur Rprop (resilient backpropagation) qui ne prend pas en compte l’amplitude du gradient mais uniquement le signe de ses coordonnées (voir ici pour une introduction pédagogique à Rprop).
Intuitivement, l’optimiseur a juste besoin de la valeur de \(\theta\) et de son gradient pour le mettre à jour. Mais à ce stade on ne connaît pas encore le gradient \(\nabla_\theta \mathcal{L}(\mathbf{X}, \mathbf{y}; \theta)\)
$grad theta_current
## torch_tensor
## [ Tensor (undefined) ]
et il faut donc le calculer:
<- eval_loss(theta_current, verbose = FALSE)
loss $backward() loss
On peut vérifier que le gradient est stocké dans
theta
$grad theta_current
## torch_tensor
## -28.5439
## 14.4407
## 9.6938
## [ CPUFloatType{3} ]
et effectuer la mise à jour avec une étape d’optimisation
$step() theta_optimizer
## NULL
On peut vérifier que le paramètre courant a été mis à jour.
theta_current
## torch_tensor
## 0.001 *
## 10.0000
## -10.0000
## -10.0000
## [ CPUFloatType{3} ][ requires_grad = TRUE ]
Il ne reste plus qu’à recommencer pour un nombre d’itérations donné. Attention, il faut réinitialiser le gradient avant de le mettre à jour, le comportement par défaut de mise à jour étant l’accumulation plutôt que le remplacement.
<- 100
num_iterations <- vector("numeric", length = num_iterations)
loss_vector for (i in 1:num_iterations) {
$zero_grad()
theta_optimizer<- eval_loss(theta_current, verbose = FALSE)
loss $backward()
loss$step()
theta_optimizer<- loss %>% as.numeric()
loss_vector[i] }
On vérifie que la perte diminue au cours du temps.
plot(1:num_iterations, loss_vector)
On constate que notre optimiseur aboutit au même résultat que
glm()
tibble(
torch = theta_current |> as.numeric(),
glm = glm(Y ~ 0 + X, family = "binomial") |> coefficients()
)
## # A tibble: 3 × 2
## torch glm
## <dbl> <dbl>
## 1 1.79 1.79
## 2 -1.04 -1.04
## 3 -0.973 -0.973
Attention la mécanique présentée ci-dessus avec
$step()
ne fonctionne pas pour certaines routines
d’optimisation (BFGS, gradient conjugué) qui nécessite de calculer
plusieurs fois la fonction objective. Dans ce cas, il faut définir une
closure, qui renvoie la fonction objective, et la passer en
argument à $step()
.
## remise à 0 du paramètre courant
<- torch_tensor(rep(0, length(theta)), requires_grad = TRUE)
theta_current <- optim_rprop(theta_current)
theta_optimizer
## définition de la closure
<- function() {
calc_loss $zero_grad()
theta_optimizer<- eval_loss(theta_current, verbose = FALSE)
loss $backward()
loss
loss
}
## Optimisation avec la closure
<- 100
num_iterations <- vector("numeric", length = num_iterations)
loss_vector for (i in 1:num_iterations) {
<- theta_optimizer$step(calc_loss) %>% as.numeric()
loss_vector[i] }
On peut vérifier qu’on obtient des résultats identiques dans les deux cas d’utilisation:
theta_current
## torch_tensor
## 1.7915
## -1.0380
## -0.9729
## [ CPUFloatType{3} ][ requires_grad = TRUE ]
plot(1:num_iterations, loss_vector)
On considère un exemple de régression multiple, réalisé à partir du blog torch for optimization, où l’on cherche à estimer les paramètres de moyenne ainsi que la variance par maximisation de la vraisemblance.
On génère les données
## Generate the data
<- cbind(rep(1,100),matrix(rnorm(1000),100,10))
X <- rnorm(11)
Beta.true <- X%*%Beta.true + rnorm(100)
Y <- nrow(X) n
La fonction de perte à optimiser (ici la log-vraisemblance) va dépendre d’inputs définis comme des “tenseurs torch”:
## Declare the loss function
<- torch_tensor(rep(1,12) %>% as.matrix, requires_grad = TRUE)
Theta <- torch_tensor(X) X.tensor
Quelques remarques :
- le paramètre \(\theta\) à optimiser est ici défini comme un tenseur, i.e. un objet qui va notamment stocker la valeur courante de \(\theta\). Avec l’option “requires_grad=TRUE” la valeur courante du gradient de la dernière fonction appelée dépendant de \(\theta\) va aussi être stockée.
- la matrice \(X\) est aussi définie comme un tenseur, mais l’option “requires_grad=TRUE” n’a pas été spécifiée, le gradient ne sera donc pas stocké pour cet objet. Cette distinction est explicitée lorsque l’on affiche les deux objets:
1:3] Theta[
## torch_tensor
## 1
## 1
## 1
## [ CPUFloatType{3,1} ][ grad_fn = <SliceBackward0> ]
1:3,1:3] X.tensor[
## torch_tensor
## 1.0000 -0.7146 0.4371
## 1.0000 0.1269 1.9857
## 1.0000 -0.5624 0.2708
## [ CPUFloatType{3,3} ]
La fonction de perte est ici la log-vraisemblance, elle-même définie à partir d’opérateurs torch élémentaires :
<- function(){
LogLik torch_mul(n,torch_log(Theta[12])) + torch_square(torch_norm(Y-torch_matmul(X.tensor,Theta[1:11])))/(2*torch_square(Theta[12]))
}
La fonction LogLik peut être appliquée comme une fonction R qui prendra directement en argument les valeurs courantes de X.tensor et \(\theta\), et produira en sortie un tenseur
LogLik()
## torch_tensor
## 1969.8533
## [ CPUFloatType{1} ][ grad_fn = <AddBackward0> ]
Outre la valeur courante de la fonction, ce tenseur contient la “recette” du graphe computationnel utilisé dans calcul backward du gradient de la fonction LogLik par rapport à \(\theta\). On peut ainsi afficher la dernière opération de ce graphe
<- LogLik()
toto $grad_fn toto
## AddBackward0
correspondant à l’addition (AddBackward) des deux termes \[ n\times \log(\theta[12]) \quad \text{et} \quad ||Y-X\theta[1:11]||^2/(2*\theta[12]^2)\] dans le calcul de la perte. On peut afficher les opérations suivantes dans le graphe comme suit:
$grad_fn$next_functions toto
## [[1]]
## MulBackward0
## [[2]]
## DivBackward0
L’étape suivante consiste à choisir la méthode d’optimisation à appliquer. L’intérêt d’utiliser le package `{{torch}}` est d’avoir accès à une large gamme de méthodes d’optimisation, on considère ici la méthode rprop qui réalise une descente de gradient à pas adaptatif et spécifique à chaque coordonnée:
## Specify the optimization parameters
<- 0.01
lr <- optim_rprop(Theta,lr) optimizer
On décrit maintenant un pas de calcul du gradient, contenant les
étapes suivantes : - réinitialisation du gradient de \(\theta\),
- évaluation de la fonction de perte (avec la valeur courante de \(\theta\)),
- calcul backward du gradient. On inclut tout cela dans une
fonction:
## Optimization step description
<- function() {
calc_loss $zero_grad()
optimizer<- LogLik()
value $backward()
value
value }
Commençons par regarder ce que fait concrètement cette fonction. L’état courant du paramètre est le suivant:
Theta
## torch_tensor
## 1
## 1
## 1
## 1
## 1
## 1
## 1
## 1
## 1
## 1
## 1
## 1
## [ CPUFloatType{12,1} ][ requires_grad = TRUE ]
$grad Theta
## torch_tensor
## [ Tensor (undefined) ]
On applique une première fois la fonction, et on obtient la mise à jour suivante :
calc_loss()
## torch_tensor
## 1969.8533
## [ CPUFloatType{1} ][ grad_fn = <AddBackward0> ]
Theta
## torch_tensor
## 1
## 1
## 1
## 1
## 1
## 1
## 1
## 1
## 1
## 1
## 1
## 1
## [ CPUFloatType{12,1} ][ requires_grad = TRUE ]
$grad Theta
## torch_tensor
## 322.0468
## 167.9877
## 279.3360
## 116.2697
## 8.5126
## 241.4523
## 102.8678
## 159.1056
## -123.2218
## 323.6690
## 238.3180
## -3839.7065
## [ CPUFloatType{12,1} ]
Comme on le voit la valeur courante du paramètre n’a pas changée, en revanche Theta$grad contient maintenant le gradient de la fonction de perte calculé en \(\theta\). Dans le cas où la méthode d’optimisation considérée n’a besoin que de la valeur courante du gradient et du paramètre, on peut directement faire la mise à jour de \(\theta\) :
$step() optimizer
## NULL
Theta
## torch_tensor
## 0.9900
## 0.9900
## 0.9900
## 0.9900
## 0.9900
## 0.9900
## 0.9900
## 0.9900
## 1.0100
## 0.9900
## 0.9900
## 1.0100
## [ CPUFloatType{12,1} ][ requires_grad = TRUE ]
$grad Theta
## torch_tensor
## 322.0468
## 167.9877
## 279.3360
## 116.2697
## 8.5126
## 241.4523
## 102.8678
## 159.1056
## -123.2218
## 323.6690
## 238.3180
## -3839.7065
## [ CPUFloatType{12,1} ]
Il n’y a plus qu’à itérer !
## Run the optimization
<- 100
num_iterations <- rep(NA,num_iterations)
LogLik.hist for (i in 1:num_iterations) {
<- calc_loss() %>% as.numeric
LogLik.hist[i] $step()
optimizer }
On vérifie que l’optimisation s’est bien passée (ie que l’on a minimisé la fonction de perte)
## How does the loss function behave ?
plot(LogLik.hist,cex=0.4)
## Are the gradients at 0 ?
$grad Theta
## torch_tensor
## 0.0001 *
## -0.5746
## -0.0858
## 1.5259
## -1.2350
## 0.7248
## 1.3638
## 1.0169
## 0.9131
## 0.2003
## 1.0300
## 1.2445
## -0.9155
## [ CPUFloatType{12,1} ]
et que le résultat est comparable à la solution classique obtenue par OLS :
## Compare the coef estimates with the ones of lm
<- lm(Y ~ X[,-1] )
Res plot(Res$coefficients,as.numeric(Theta)[-12])
abline(a=0,b=1,col=2)
## Compare the variances
summary(Res)$sigma**2
## [1] 0.8109918
12] Theta[
## torch_tensor
## 0.8496
## [ CPUFloatType{1} ][ grad_fn = <SelectBackward0> ]
torch_ones(NB_OF_ROWS, NB_OF_COLS, requires_grad = TRUE)
ou torch_ones(VECTOR_OF_DIMS, requires_grad = TRUE)
: crée
un tenseur rempli de 1.
torch_tensor(OBJECT, requires_grad = TRUE)
:
convertit un objet R (vecteur, matrice ou array) en un tenseur
torch.
$mean()
, $sum()
,
$pow(ORDER)
, $mm(MATRIX)
(matrix
multiplication), cf les fonctions torch_* de la documentation R de
torch.
Les fonctions de torch peuvent s’utiliser de manière analogue aux
pipes. Par exemple,
x$mm(w1)$add(b1)$clamp(min = 0)$mm(w2)$add(b2)
.
output$backward()
: effectue la propagation backward
pour calculer les gradients successifs.
input$grad
: récupère le gradient de
output
par rapport à input
.
midput$retain_grad()
: à lancer avant `\(backward()\) et permet de stocker les
gradients intermédiaires.
Le modèle Poisson lognormal multivarié lie des vecteurs de comptages \(p\)-dimensionnel \(\mathbf{Y}_i\) observés à des vecteurs gaussiens \(p\)-dimensionnel latents \(\mathbf{Z}_i\) comme suit
\[\begin{equation} \begin{array}{rcl} \text{espace latent} & \mathbf{Z}_i \sim \mathcal{N}({\boldsymbol 0},\boldsymbol\Sigma) , \\ \text{espace des observations} & Y_{ij} | Z_{ij} \quad \text{indep.} & \mathbf{Y}_i | \mathbf{Z}_i\sim\mathcal{P}\left(\exp\{{\mathbf{o}_i + \mathbf{x}_i^\top\boldsymbol B} + \mathbf{Z}_i\}\right). \end{array} \end{equation}\]L’effet principal est dû à une combinaison linéaire de \(d\) covariables \(\mathbf{x}_i\) (includant un vecteur de constantes). Le vecteur fixé \(\mathbf{o}_i\) correspond à un vecteur d’offsets, c’est-à-dire un effet connu est fixé sur les \(p\) variables dans chaque échantillon. Les paramètres à estimer sont la matrice des coefficients de régression \(\boldsymbol B\) et la matrice de covariance \(\boldsymbol\Sigma = \mathbf{\Omega}^{-1}\) décrivant la structure de dépendance résiduelle entre les \(p\) variables dans l’espace latent. On note \(\theta = ({\boldsymbol B}, \mathbf{\Omega})\) le vecteur des paramètres du modèles.
Une manière classique d’estimer l’ajustement de ce modèle consiste en l’utilisation d’une approximation variationnelle de la vraisemblance, appelée vraisemblance variationnelle ou ELBO (Evidence Lower Bound) qui prend la forme suivante:
\[\begin{multline} \label{eq:elbo} \mathcal{J}_n(\theta, \mathbf{\psi}) = \mathrm{trace} ( \mathbf{Y}^\top [\mathbf{O} + \mathbf{M} + \mathbf{X}\boldsymbol{B}]) - \mathrm{trace}(\tilde{\mathbf{A}}^\top \mathbf{1}_{n,p}) + K(\mathbf{Y}) + \frac{n}2\log|\mathbf{\Omega}|\\ - \frac12 \mathrm{trace}(\mathbf{M} \mathbf{\Omega} \mathbf{M}^\top) - \frac12 \mathrm{trace}(\bar{\mathbf{S}}^2 \mathbf{\Omega}) + \mathrm{trace}(\log(\mathbf{S})^\top \mathbf{1}_{n,p}) + \frac12 np, \end{multline}\] où \(\mathbf{M} = [\mathbf{m}_1, \dots, \mathbf{m}_n]^\top\), \(\mathbf{S} = [\mathbf{s}_1, \dots, \mathbf{s}_n]^\top\), \(\bar{\mathbf{S}}^2 = \sum_{i=1}^n \mathrm{diag}(\mathbf{s}_i\circ \mathbf{s}_{i})\) sont des paramètres additionnels dits “variationnels” gérant l’approximation de la vraie loi conditionnelle de \(\mathbf{Z} | \mathbf{Y}\) et consécutivement de la log-vraisemblance. On note \(\psi = (\mathbf{M}, \mathbf{S})\) l’ensemble de ces paramètres.
Notre approche consiste à utiliser une forme dite variationnelle de l’algorithme EM alternant l’estimation des paramètres \(\theta\) et \(\psi\):
\[\begin{align} \label{eq:vem} \mathrm{VE-step} & \left( \hat{\psi}^{\text{ve}} \right) = \arg\max_{\psi} \mathcal{J}_n (\psi, \theta)\\ \mathrm{M-step} & \left(\hat{\theta} \right) = \arg\max_{\theta} \mathcal{J}_n(\psi, \theta)\\ \end{align}\]L’optimiseur implémenté dans PLNmodels s’appuie sur les gradients des paramètres \(\theta\) et \(\psi\)
\[\begin{align} \label{eq:derivatives-elbo-model} \begin{split} \nabla_{\mathbf{B}} J_n(\theta) & = \mathbf{X}^\top (\mathbf{Y} - \mathbf{A}), \\ % \nabla_{\mathbf{\Omega}} J_n(\theta) & = \frac{n}{2} \left[ \mathbf{\Omega}^{-1} - \frac{\mathbf{M}^\top\mathbf{M} + \bar{\mathbf{S}}^2}{n} \right] \Leftrightarrow \mathbf{\Omega}^{-1} = \frac{1}{n}\left(\mathbf{M}^\top\mathbf{M} + \bar{\mathbf{S}}^2\right) \\ \nabla_{\mathbf{M}} J_n(\mathbf{\psi}) & = \mathbf{Y} - \tilde{\!\mathbf{A}} - \mathbf{M}\mathbf{\Omega} \\[1.5ex] % \nabla_{\mathbf{S}} J_n(\mathbf{\psi}) & = -\mathbf{S} \circ \tilde{\!\mathbf{A}} + 1/\mathbf{S} - \mathbf{S}\mathbf{D}_{\mathbf{\Omega}}\\[1.5ex] % \end{split} \end{align}\]où
\[\begin{equation*} \mathbf{A} = \exp\{\mathbf{O} + \mathbf{X}\mathbf{B} + \mathbf{M} + \mathbf{S}^2/2\}. \end{equation*}\]On utilise le fait qu’il existe une forme explicite pour \(\Sigma\) dans l’étape M et on fait une descente de gradient sur les autres paramtères \((\mathbf{M}, \mathbf{S}, \mathbf{B})\).
library(PLNmodels)
## This is packages 'PLNmodels' version 0.11.7
## Use future::plan(multicore/multisession) to speed up PLNPCA/PLNmixture/stability_selection.
data(oaks)
<- PLN(Abundance ~ 1 + offset(log(Offset)), data = oaks) myPLN_classical
##
## Initialization...
## Adjusting a PLN model with full covariance model
## Post-treatments...
## DONE!
Avec torch, et pourvu qu’on détermine un bon optimiseur, il suffit de spécifier l’ELBO avec les objets adéquats et faire le calcul de \(\Sigma\) de manière explicite.
On propose ci-dessous une implémentation dans une classe R6 (faite avec Bastien et Mahendra).
library(torch)
library(R6)
<- function(n_){
log_stirling <- n_+ (n_==0)
n_ torch_log(torch_sqrt(2*pi*n_)) + n_*log(n_/exp(1))
}
<-
PLN R6Class("PLN",
public = list(
Y = NULL,
O = NULL,
X = NULL,
n = NULL,
p = NULL,
d = NULL,
M = NULL,
S = NULL,
A = NULL,
B = NULL,
Sigma = NULL,
Omega = NULL,
ELBO_list = NULL,
## Constructor
initialize = function(Y, O, X){
$Y <- torch_tensor(Y)
self$O <- torch_tensor(O)
self$X <- torch_tensor(X)
self$n <- nrow(Y)
self$p <- ncol(Y)
self$d <- ncol(X)
self## Variational parameters
$M <- torch_zeros(self$n, self$p, requires_grad = TRUE)
self$S <- torch_ones(self$n , self$p, requires_grad = TRUE)
self## Model parameters
$B <- torch_zeros(self$d, self$p, requires_grad = TRUE)
self$Sigma <- torch_eye(self$p)
self$Omega <- torch_eye(self$p)
self## Monitoring
$ELBO_list <- c()
self
},
get_Sigma = function(M, S){
1/self$n * (torch_matmul(torch_transpose(M,2,1),M) + torch_diag(torch_sum(torch_multiply(S,S), dim = 1)))
},
get_ELBO = function(B, M, S, Omega){
<- torch_multiply(S, S)
S2 <- torch_matmul(self$X, B)
XB <- torch_exp(self$O + M + XB + S2/2)
A
<- n/2 * torch_logdet(Omega)
elbo <- torch_add(elbo, torch_sum(- A + torch_multiply(self$Y, self$O + M + XB) + .5 * torch_log(S2)))
elbo <- torch_sub(elbo, .5 * torch_trace(torch_matmul(torch_matmul(torch_transpose(M, 2, 1), M) + torch_diag(torch_sum(S2, dim = 1)), Omega)))
elbo <- torch_add(elbo, .5 * self$n * self$p - torch_sum(log_stirling(self$Y)))
elbo
elbo
},
fit = function(N_iter, lr, tol = 1e-8, verbose = FALSE){
$ELBO_list <- double(length = N_iter)
self<- optim_rprop(c(self$B, self$M, self$S), lr = lr)
optimizer <- Inf
objective0 for (i in 1:N_iter){
## reinitialize gradients
$zero_grad()
optimizer
## compute current ELBO
<- - self$get_ELBO(self$B, self$M, self$S, self$Omega)
loss
## backward propagation and optimization
$backward()
loss$step()
optimizer
## update parameters with close form
$Sigma <- self$get_Sigma(self$M, self$S)
self$Omega <- torch_inverse(self$Sigma)
self
<- -loss$item()
objective if(verbose && (i %% 50 == 0)){
pr('i : ', i )
pr('ELBO', objective)
}$ELBO_list[i] <- objective
selfif (abs(objective0 - objective)/abs(objective) < tol) {
$ELBO_list <- self$ELBO_list[1:i]
selfbreak
else {
} <- objective
objective0
}
}
},
plotLogNegElbo = function(from = 10){
plot(log(-self$ELBO_list[from:length(self$ELBO_list) ]), type = "l")
}
) )
Ça torche….
<- oaks$Abundance
Y <- cbind(rep(1, nrow(Y)))
X <- log(oaks$Offset)
O <- PLN$new(Y = Y, O = log(O), X = X)
myPLN $fit(30, lr = 0.1, tol = 1e-5)
myPLN$plotLogNegElbo() myPLN
Des temps de calcul similaires sur ce jeu de données (\(n=116, p=114\))
system.time(
<- PLN(Abundance ~ 1 + offset(log(Offset)), data = oaks,
myPLN_classical control = list(trace = 0)))
## user system elapsed
## 2.652 0.543 1.636
system.time({
<- PLN$new(Y = Y, O = log(O), X = X)
myPLN $fit(30, 0.1, tol = 1e-5)}
myPLN )
## user system elapsed
## 1.193 0.307 0.754
On est proche en terme d’objectif
$loglik myPLN_classical
## [1] -32248.93
last(myPLN$ELBO_list)
## [1] -32278.62
torch
En keras
on différencie entre modèle et couche, en
torch
tout est dans nn_Module()
, classe de
base pour tous les modules de réseaux de neurones. Les modules peuvent
aussi contenir des sous-modules. Il existe un certain nombre de modules
pré-définis.
Exemple d’un module linéaire :
<- nn_linear(3, 1)
mylinearmod mylinearmod
## An `nn_module` containing 4 parameters.
##
## ── Parameters ──────────────────────────────────────────────────────────────────
## • weight: Float [1:1, 1:3]
## • bias: Float [1:1]
Ce module a des paramètres de poids et biais. Le fait d’appeler un
module lance un backward
. Plus besoin de mettre un
requires_grad = TRUE
, c’est compris directement dans les
modules.
<- torch_randn(10, 3)
data <- mylinearmod(data)
out out
## torch_tensor
## -0.4316
## -0.2547
## 0.0521
## -0.2395
## -0.1856
## -0.7849
## 0.1462
## 0.3131
## 0.2181
## 0.2040
## [ CPUFloatType{10,1} ][ grad_fn = <AddmmBackward0> ]
$grad_fn out
## AddmmBackward0
$weight$grad mylinearmod
## torch_tensor
## [ Tensor (undefined) ]
$bias$grad mylinearmod
## torch_tensor
## [ Tensor (undefined) ]
On n’a pas encore fait de parcours backward, les valeurs sont donc
non définies. Si on lance out$backward(),
on obtient une
erreur, due au fait qu’autograd attend en sortie un scalaire et qu’ici
nous avons un tenseur de (10, 1). Une façon de le corriger :
<- torch_tensor(10)$`repeat`(10)$unsqueeze(1)$t()
d_avg_d_out $backward(gradient = d_avg_d_out)
out$weight$grad mylinearmod
## torch_tensor
## -37.7331 2.6029 15.6590
## [ CPUFloatType{1,3} ]
$bias$grad mylinearmod
## torch_tensor
## 100
## [ CPUFloatType{1} ]
C’est un module qui contient des modules. Par exemple
<- nn_sequential(
model nn_linear(3, 16),
nn_relu(),
nn_linear(16, 1)
) model
## An `nn_module` containing 81 parameters.
##
## ── Modules ─────────────────────────────────────────────────────────────────────
## • 0: <nn_linear> #64 parameters
## • 1: <nn_relu> #0 parameters
## • 2: <nn_linear> #17 parameters
# application
<- torch_randn(10, 3)
data <- model(data)
output output
## torch_tensor
## -0.3672
## 0.0463
## 0.1644
## -1.0125
## -0.3617
## -0.0301
## 0.1317
## -0.1463
## -0.0123
## 0.2524
## [ CPUFloatType{10,1} ][ grad_fn = <AddmmBackward0> ]
Comment implémenter une fonction compatible avec autograd ?
Une fonction compatible avec autograd doit être définie comme une classe via l’opérateur
autograd_function
, et posséder deux méthodes,forward
etbackward
, qui déterminent respectivement quelle opération est exécutée par le code et comment calculer le gradient.ctx
correspond aux objets partagés entre la méthodeforward
et la méthodebackward
, et est toujours le premier argument de ces deux méthodes.$save_for_backward()
permet de sauvegarder des valeurs des inputs et/ou outputs à utiliser lors du calcul du gradient dans la méthodebackward
.Exemple de code :
backward
doit être capable de propager le calcul du gradient aux inputs de la fonction implémentée. Par exemple, en considérant que la fonction implémentée correspond à la fonction \(f\) dans le chaînage \[x \xrightarrow[]{f} y \rightarrow z,\] La méthodebackward
de la fonction \(f\) doit être capable de sortir \(\partial z / \partial x = (\partial y / \partial x)(\partial z / \partial y)\), où dans l’exemple précédent \(\partial z / \partial y\) est représenté par l’argumentgrad_output
.