3 Gradient boosting
Les algorithmes de gradient boosting permettent de minimiser des pertes empiriques de la forme \[\frac{1}{n}\sum_{i=1}^n\ell(y_i,f(x_i)).\] où \(\ell:\mathbb R\times\mathbb R\to\mathbb R\) est une fonction de coût convexe en son second argument. Il existe plusieurs type d’algorithmes boosting. Un des plus connus et utilisés a été proposé par Friedman (2001), c’est la version que nous étudions dans cette partie.
Cette approche propose de chercher la meilleure combinaison linéaire d’arbres binaires, c’est-à-dire que l’on recherche \(g(x)=\sum_{m=1}^M\alpha_mh_m(x)\) qui minimise \[\mathcal R_n(g)=\frac{1}{n}\sum_{i=1}^n\ell(y_i,g(x_i)).\] Optimiser sur toutes les combinaisons d’arbres binaires se révélant souvent trop compliqué, Friedman (2001) utilise une descente de gradient pour construire la combinaison d’arbres de façon récursive. L’algorithme est le suivant :
Entrées :
- \(d_n=(x_1,y_1),\dots,(x_n,y_n)\) l’échantillon, \(\lambda\) un paramètre de régularisation tel que \(0\lt\lambda\leq 1\).
- \(M\in\mathbb N\) le nombre d’itérations.
- paramètres de l’arbre (nombre de coupures…)
Itérations :
- Initialisation : \(g_0(.)=\mathop{\mathrm{argmin}}_c \frac{1}{n}\sum_{i=1}^n \ell(y_i,c)\)
- Pour \(m=1\) à \(M\) :
- Calculer l’opposé du gradient \(-\frac{\partial}{\partial g(x_i)}\ell(y_i,g(x_i))\) et l’évaluer aux points \(g_{m-1}(x_i)\) : \[U_i=-\frac{\partial}{\partial g(x_i)}\ell(y_i,g(x_i)) _{\Big |g(x_i)=g_{m-1}(x_i)},\quad i=1,\dots,n.\]
- Ajuster un arbre sur l’échantillon \((x_1,U_1),\dots,(x_n,U_n)\), on le note \(h_m\).
- Mise à jour : \(g_m(x)=g_{m-1}(x)+\lambda h_m(x)\).
Sortie : la suite \((g_m(x))_m\).
Sur R On peut utiliser différents packages pour faire du gradient boosting. Nous utilisons ici le package gbm (Ridgeway 2006).
3.1 Un exemple simple en régression
On considère un jeu de données \((x_i,y_i),i=1,\dots,200\) issu d’un modèle de régression \[y_i=m(x_i)+\varepsilon_i\] où la vraie fonction de régression est la fonction sinus (mais on va faire comme si on ne le savait pas).
<- seq(-2*pi,2*pi,by=0.01)
x <- sin(x)
y set.seed(1234)
<- runif(200,-2*pi,2*pi)
X <- sin(X)+rnorm(200,sd=0.2)
Y <- data.frame(X,Y)
df1 <- data.frame(X=x,Y=y)
df2 <- ggplot(df1)+aes(x=X,y=Y)+geom_point()+
p1 geom_line(data=df2,linewidth=1)+xlab("")+ylab("")
p1
Rappeler ce que siginifie le \(L_2\)-boosting.
Il s’agit de l’algorithme de gradient boosting présenté ci-dessus appliqué à la fonction de perte \[\ell(y,f(x))=\frac{1}{2}(y-f(x))^2.\]
A l’aide de la fonction gbm du package gbm construire un algorithme de \(L_2\)-boosting. On utilisera 500000 itérations et gardera les autres valeurs par défaut de paramètres, à l’exception de
bag.fraction
qu’on prendra égal à 1.library(gbm) <- gbm(Y~.,data=df1,...) L2boost
Visualiser l’estimateur à la première itération. On pourra faire un predict avec l’option
n.trees
ou utiliser directement la fonction plot.gbm avec l’optionn.trees
.help(predict.gbm) <- predict(L2boost,...) prev1 #ou help(plot.gbm) plot(L2boost,...)
On remarque que l’estimateur est un arbre avec une seule coupure. On aurait aussi pu utiliser :
Faire de même pour les itérations 1000 et 500000.
<- predict(L2boost,newdata=df2,...) prev1000 <- predict(L2boost,newdata=df2,...) prev500000 ...
On sur-ajuste lorsque le nombre d’itérations est trop important.
Sélectionner le nombre d’itérations par la procédure de votre choix.
On propose de faire une validation hold out. C’est assez facile avec gbm il suffit de renseigner l’option
train.fraction
de gbm.<- gbm(Y~.,data=df1,...) L2boost.sel gbm.perf(...)
Représenter l’estimateur sélectionné.
<- predict(L2boost.sel,newdata=df2,...) prev_opt ...
3.2 Adaboost et logitboost pour la classification binaire.
On considère le jeu de données spam du package kernlab.
library(kernlab)
data(spam)
set.seed(1234)
<- spam[sample(nrow(spam)),] spam
Exécuter la commande et commenter la sortie.
<- gbm(type~.,data=spam,distribution="adaboost", model_ada1 interaction.depth=2, shrinkage=0.05,n.trees=500)
On obtient le message d’erreur suivant :
Proposer une correction permettant de faire fonctionner l’algorithme.
Il est nécessaire que la variable qualitative à expliquer soit codée 0-1 pour adaboost.
<- spam spam1 $type <- ... spam1 ...
Expliciter le modèle ajusté par la commande précédente.
L’algorithme gbm est une descente de gradient qui minimise la fonction de perte \[\frac{1}{n}\sum_{i=1}^n \ell(y_i,g(x_i)).\] Dans le cas de adaboost on utilise la perte exponentielle : \(\ell(y,g(x))=\exp(-yg(x))\).
Effectuer un summary du modèle ajusté. Expliquer la sortie.
On obtient un indicateur qui permet de mesurer l’importance des variable dans la construction de la méthode.
Utiliser la fonction vip du package vip pour retrouver ce sorties.
Sélectionner le nombre d’itérations pour l’algorithme adaboost en faisant une validation croisée 5 blocs.
Pour l’estimateur sélectionné, calculer la prévision (label et probabilité d’être un spam) de l’individu suivant :
<- spam[1000,-58] xnew
On obtient la probabilité d’être spam avec
On prédira donc
nonspam
.Faire la même procédure en changeant la valeur du paramètre shrinkage (par exemple 0.05 et 0.5). Interpréter.
Le nombre d’itérations optimal augmente lorsque shrinkage diminue. C’est logique car ce dernier paramètre contrôle la vitesse de descente de gradient : plus il est grand, plus on minimise vite et moins on itère. Il faut néanmoins veiller à ne pas le prendre trop petit pour avoir un estimateur stable. Ici, 0.05 semble être une bonne valeur.
Expliquer la différence entre adaboost et logitboost et précisez comment on peut mettre en œuvre ce dernier algorithme.
La seule différence se situe au niveau de la fonction de perte, adaboost utilise \[\exp(-yg(x))\] tandis que logitboost utilise \[\log(1+\exp(-2yg(x)))\] Avec gbm il faudra utiliser l’option
distribution="bernoulli"
pour faire du logitboost, par exemple :
3.3 Comparaison de méthodes
On reprend les données spam
de l’exercice précédent et on les coupe en un échantillon d’apprentissage pour entraîner les algorithmes et un échantillon test pour les comparer :
set.seed(1234)
<- sample(1:nrow(spam))
perm <- spam[perm[1:3000],]
app <- spam[-perm[1:3000],] test
Sur les données d’apprentissage uniquement, entraîner
l’algorithme adaboost en sélectionnant le nombre d’itérations par validation croisée
l’algorithme logitboost en sélectionnant le nombre d’itérations par validation croisée
une forêt aléatoire avec les paramètres par défaut
un arbre CART
<- app |> mutate(type=as.numeric(type)-1) app1 <- gbm(type~.,data=app1,...) model_ada <- gbm.perf(...) nb_ada
<- gbm(type~.,data=app1,...) model_logit <- gbm.perf(...) nb_logit
library(ranger) <- ranger(...,probability=TRUE) foret library(rpart) <- rpart(...) arbre <- ... cp_opt <- prune(...) arbre.opt
Pour les 4 algorithmes, calculer, pour tous les individus de l’échantillon
test
, la probabilité que ce soit un spam. On pourra stocker toutes ces probabilités dans un mêmetibble
.<- tibble(ada=predict(...,type="response"), prob logit=predict(...), foret=predict(...)$prediction[,2], arbre=predict(...)[,2], obs=test$type)
Comparer les 3 algorithmes avec la courbe ROC, l’AUC et l’erreur de classification.
Comment aurait-on pu faire pour obtenir des résultats plus précis ?
Avec une validation croisée plutôt qu’un simple découpage apprentissage/test.
3.4 Xgboost
L’algorithme xgboost Chen et Guestrin (2016) va plusloin que le gradient boosting en minimisant une approximation à l’odre 2 de la fonction de perte et en ajoutant un terme de régularisation dans la fonction objectif. On cherche toujours des combinaisons d’arbres
\[f_b(x)=f_{b-1}(x)+h_b(x)\quad\text{où}\quad h_b(x)=w_{q(x)}\]
est un arbre à \(T\) feuilles : \(w\in\mathbb R^T\) et \(q:\mathbb R^d\to\{1,2,\dots,T\}\). À l’étape \(b\), on cherche l’arbre qui minimise la fonction objectif de la forme
\[ \begin{aligned} \text{obj}^{(b)}= & \sum_{i=1}^n\ell(y_i,f_b(x_i))+\sum_{j=1}^b\Omega(h_j) \\ = & \sum_{i=1}^n\ell(y_i,f_{b-1}(x_i)+h_b(x_i))+\sum_{j=1}^b\Omega(h_j) \end{aligned} \]
où \(\Omega(h_j)\) est un terme de régularisation qui va pénaliser \(h_j\) en fonction de son nombre de feuilles \(T\) et des valeurs prédites \(w\). Un développement limité à l’ordre 2 permet d’approcher cette fonction par
\[\text{obj}^{(b)}=\sum_{i=1}^n[\ell_i^{(1)}h_b(x_i)+\frac{1}{2}\ell_i^{(2)}h_b^2(x_i)]+\Omega(h_b)+\text{constantes},\] avec
\[\ell_i^{(1)}=\frac{\partial \ell(y_i,f(x))}{\partial f(x)}(f_{b-1}(x_i))\quad\text{et}\quad \ell_i^{(2)}=\frac{\partial^2 \ell(y_i,f(x))}{\partial f(x)^2}(f_{b-1}(x_i)).\] Pour les arbres, la fonction de régularisation a la forme suivante :
\[\Omega(h)=\Omega(T,w)=\gamma T+\frac{1}{2}\lambda\sum_{j=1}^Tw_j^2,\] où \(\gamma\) et \(\lambda\) contrôlent le poids que l’on donne aux paramètres de l’arbre. On obtient au final l’algorithme suivant
Initialisation \(f_0=h_0\).
Pour \(b=1,\dots,B\)
Ajuster un arbre \(h_b\) à \(T\) feuilles qui minimise \[\sum_{i=1}^n[\ell_i^{(1)}h_b(x_i)+\frac{1}{2}\ell_i^{(2)}h_b^2(x_i)]+\gamma T+\frac{1}{2}\lambda\sum_{j=1}^Tw_j.\]
Mettre à jour \[f_b(x)=f_{b-1}(x)+h_b(x).\]
Sortie : la suite d’algorithmes \((f_b)_b\).
On pourra trouver plus de précisions ici : https://xgboost.readthedocs.io/en/stable/tutorials/index.html
Exercice 3.1 (Prise en main des principales fonction de xgboost) On commence par charger le package
library(xgboost)
et on reprend les données sur le sinus de la Section 3.1 :
<- seq(-2*pi,2*pi,by=0.01)
x <- sin(x)
y set.seed(1234)
<- runif(200,-2*pi,2*pi)
X <- sin(X)+rnorm(200,sd=0.2)
Y <- data.frame(X,Y)
df1 <- data.frame(X=x,Y=y) df2
La fonction xgboost
requiert que les données possèdent la classe xgb.DMatrix
, on peut l’obtenir avec
<- xgb.DMatrix(as.matrix(df1[,1]),label=df1$Y) X_mat
Expliquer la sortie
<- xgboost(data=X_mat,nrounds = 5, boost1 params=list(objective="reg:squarederror"))
[1] train-rmse:0.633250 [2] train-rmse:0.468674 [3] train-rmse:0.354599 [4] train-rmse:0.275842 [5] train-rmse:0.224803
boost1
##### xgb.Booster raw: 16.5 Kb call: xgb.train(params = params, data = dtrain, nrounds = nrounds, watchlist = watchlist, verbose = verbose, print_every_n = print_every_n, early_stopping_rounds = early_stopping_rounds, maximize = maximize, save_period = save_period, save_name = save_name, xgb_model = xgb_model, callbacks = callbacks) params (as set within xgb.train): objective = "reg:squarederror", validate_parameters = "TRUE" xgb.attributes: niter callbacks: cb.print.evaluation(period = print_every_n) cb.evaluation.log() niter: 5 nfeatures : 1 evaluation_log: iter train_rmse 1 0.6332497 2 0.4686737 3 0.3545990 4 0.2758424 5 0.2248031
On a ici entraîné
xgboost
avec la fonction de perte quadratique et 5 itérations.Faire la même chose avec 100 itération et un
learning rate
de 0.1.On peut obtenir les valeurs prédites entre \(-2\pi\) et \(2\pi\) pour 100 itérations avec
<- as.matrix(df2$X) Xtest <- predict(boost2,newdata=Xtest,iterationrange = c(1,101)) prev100
Tracer les estimateurs xgboost pour 1 itération, 20 itérations et 100 itérations. Commenter.
Comme pour le gradient boosting, l’algorithme sous-apprend si le nombre d’arbres est trop petit et sur-apprend lorsqu’il est trop grand.
Commenter la sortie
set.seed(123) <- xgb.cv(data = X_mat, sel.xgb nrounds = 100, objective = "reg:squarederror", eval_metric = "rmse", nfold=5,eta=0.1, early_stopping_rounds=10, verbose=FALSE) $evaluation_log |> head() sel.xgb
iter train_rmse_mean train_rmse_std test_rmse_mean test_rmse_std 1: 1 0.7894653 0.012973111 0.7906004 0.05513590 2: 2 0.7189852 0.011889426 0.7228767 0.05364182 3: 3 0.6556685 0.010877136 0.6607388 0.05284791 4: 4 0.5985454 0.010104324 0.6055111 0.05240736 5: 5 0.5471973 0.009379501 0.5567722 0.05117564 6: 6 0.5007942 0.008971210 0.5129458 0.04971156
<- sel.xgb$best_iteration) (ite.opt.xgb
[1] 27
$niter sel.xgb
[1] 37
On effectue une validation croisée 5 blocs pour choisir le nombre d’itérations. L’argument
early_stopping_rounds=10
permet de stopper l’algorithme lorsque l’erreur de prévision commence à trop remonter.Tracer les prévisions pour l’algorithme sélectionné.
Exercice 3.2 (Xgboost sur les données spam) On reprend les données spam de la Section 3.2. Entraîner un algorithme xgboost avec la fonction de perte binary:logistic
et en sélectionnant la nombre d’itérations par validation croisée en optimisant l’AUC. Attention cette fonction de perte requiert que la variable à expliquer prenne pour valeurs 0 ou 1 en classe numeric
.
On teste la fonction xgboost
pour voir si les donées sont au bon format.
On peut maintenant faire la valisation croisée :
On récupère les prévisions issues de la validation croisée
pour tracer la courbe ROC et calculer l’AUC :
Exercice 3.3 (Sélection avec tidymodels) Refaire l’exercice précédent avec la syntaxe tidymodels
. On choisira notamment :
la profondeur des arbres dans le vecteur
c(1,3,8,10)
le nombre d’itérations entre 1 et 500 avec un
early_stopping
toujours égal à 10 et un learning rate à 0.05.
On pourra consulter la page https://www.tidymodels.org/find/parsnip/ pour trouver les noms de paramètre du worfklow
et sur le tutoriel https://juliasilge.com/blog/shelter-animals/ pour la stratégie. Elle est ici de fixer le nombre d’itérations à 500 puisqu’on utilise le early stopping en séparant les données en 2. On initialisera donc le workflow avec
library(tidymodels)
<-
tune_spec boost_tree(tree_depth=tune(),trees=500,learn_rate=0.05,
stop_iter=10) |>
set_mode("classification") |>
set_engine("xgboost",validation=0.2)
<- workflow() |>
xgb_wf add_model(tune_spec) |>
add_formula(type ~ .)
On définit la grille de paramètres et le ré-échantillonnage :
On fait la validation croisée :
On visualise les résultats et on choisit la meilleure valeur :
On finit en entraînant l’algorithme sur toute les données pur la valeur choisie :
On peut retrouver le nombre d’itérations sélectionnés par early stopping avec
On visualise enfin l’importance des variables avec