Vous êtes sur la page 1sur 300

Machine

learning avec R

Pour une modélisation mathématique rigoureuse


Scott V. Burger


Machine learning avec R

Traduction française publiée et vendue avec l’autorisation de O’Reilly Media,
Inc. de Introduction to Machine Learning with R
ISBN 9781491976449 © 2018 Scott Burger.

© 2018 Éditions First, un département d’Édi8.
12, avenue d’Italie
75013 Paris – France
Tél. : 01 44 16 09 00
Fax : 01 44 16 09 01

Courriel : firstinfo@editionsfirst.fr
Site Internet : lisez.com

ISBN : 978-2-412-04115-4
ISBN numérique : 9782412043387
Dépôt légal : octobre 2018

Traduction de l’anglais : Daniel Rougé
Mise en page : Pierre Brandeis

Cette œuvre est protégée par le droit d’auteur et strictement réservée à l’usage
privé du client. Toute reproduction ou diffusion au profit de tiers, à titre gratuit
ou onéreux, de tout ou partie de cette œuvre est strictement interdite et constitue
une contrefaçon prévue par les articles L 335-2 et suivants du Code de la
propriété intellectuelle. L’éditeur se réserve le droit de poursuivre toute atteinte à
ses droits de propriété intellectuelle devant les juridictions civiles ou pénales.


Ce livre numérique a été converti initialement au format EPUB par Isako
www.isako.com à partir de l'édition papier du même ouvrage.
Introduction
Dans cette courte introduction, j’aborde quelques points clés.
Qui devrait lire ce livre ?
Ce livre est idéal pour les personnes qui ont déjà une connaissance pratique du
langage de programmation R. Si vous n’avez aucune connaissance de R, sachez
que c’est un langage assez facile à assimiler, et que son code en est suffisamment
lisible pour que vous puissiez tirer le meilleur parti des exemples proposés ici.
Portée du livre
Ce livre est une introduction à l’apprentissage automatique avec le langage R.
Nous ne plongerons donc pas profondément dans les fondements mathématiques
de chaque algorithme couvert dans cet ouvrage. Mais vous trouverez ici
suffisamment de détails pour que vous puissiez discerner les différences
essentielles entre un réseau de neurones et, disons, une forêt aléatoire.
Conventions utilisées dans ce livre
Dans ce livre, nous utiliserons les conventions typographiques suivantes :
Italique
Indique un terme nouveau, ou nom de fichier ou encore une extension.

Largeur constante
Cette typographie est utilisée dans les listings de programmes, ainsi que dans tout ce qui peut, dans un
paragraphe, faire référence à un nom de variable ou de fonction, à des bases ou des types de données,
des variables d’environnement, des instructions ou encore des mots-clés. Elle est également utilisée
pour les noms de modules et de packages, ainsi que pour la sortie des commandes.

Cet élément signale une astuce ou une suggestion.

Cet élément signale une note d’ordre général.

Cette icône attire l’attention sur un avertissement ou un problème potentiel.


CHAPITRE 1
Qu’est-ce qu’un modèle ?
Il fut un temps, au cours de mes études de premier cycle en physique, où j’étais
excité à l’idée d’apprendre ce qu’était un modèle. Je me souviens assez bien de
la scène. Nous étions dans une classe « Étoiles et Galaxies », et nous nous
préparions à apprendre des modèles atmosphériques qui pourraient être
appliqués non seulement à la Terre, mais aussi à d’autres planètes du système
solaire. J’avais assez de connaissances sur les modèles climatiques pour savoir
qu’ils étaient compliqués, et donc je m’étais préparé à un assaut de formules de
maths qui me prendraient des semaines à analyser. Quand nous sommes
finalement arrivés au cœur du sujet, je me rappelle que j’avais été un peu déçu :
j’avais déjà eu affaire à des modèles de données dans le passé et je ne m’en étais
même pas rendu compte !
Du fait que les modèles représentent un aspect fondamental de l’apprentissage
automatique, il n’est peut-être pas surprenant que cette histoire reflète la manière
dont j’ai appris à comprendre ce domaine. Au cours de mes études supérieures,
j’étais sur le point de me lancer dans l’industrie de la finance. J’avais entendu
dire que l’apprentissage automatique était largement utilisé dans ce monde et, en
tant qu’étudiant en physique, je sentais que j’aurais besoin d’être plutôt un
ingénieur en informatique pour être compétitif. Je me suis alors rendu compte
que non seulement l’apprentissage automatique n’était pas aussi effrayant que je
le pensais à l’origine, mais que je l’avais en réalité déjà utilisé auparavant. Et
même avant le lycée !
Les modèles sont utiles parce que, contrairement aux tableaux de bord qui
offrent une image statique de ce que les données montrent à un certain instant
(ou pour une période de temps particulière), les modèles peuvent aller plus loin
et vous aider à comprendre l’avenir. Par exemple, une personne qui travaille au
sein d’une équipe de vente peut n’être familière qu’avec des rapports qui
montrent une image statique. Peut-être que son écran est systématiquement à
jour avec les chiffres de ventes quotidiennes. Il y a eu d’innombrables tableaux
de bord que j’ai vus (et même construits), et qui disent simplement « voici le
nombre d’actifs en ce moment ». Ou encore : « voici notre indicateur clé de
performance pour aujourd’hui ». Un tel rapport est une entité statique qui n’offre
aucune intuition quant à son évolution dans le temps.
La Figure 1.1 montre à quoi pourrait ressembler un tel rapport :

op <- par(mar = c(10, 4, 4, 2) + 0.1) #formatage des


marges

barplot(mtcars$mpg, names.arg = row.names(mtcars), las =


2, ylab =
"Rendement Carburant en Miles par Gallon")

Figure 1.1 : Distribution montrant l’efficacité énergétique de véhicules basée sur le jeu de données mtcars
qui est directement intégré à R.

La Figure 1.1 représente un tracé obtenu à partir du jeu de données mtcars qui
est préinstallé avec R. Elle montre un tracé pour un certain nombre de voitures
en fonction de leur consommation de carburant exprimée en miles par gallon
US. Ce rapport n’est pas très utile en soi (d’autant que les données datent
de 1974…). Il ne nous donne en effet aucun pouvoir de prédiction. Il peut bien
sûr être intéressant de voir quelle est l’efficacité des voitures en termes de
consommation, mais comment pouvons-nous établir un lien avec d’autres
éléments présents dans les données et, de plus, faire des prédictions à partir de
ces mêmes données ?
Un modèle est une certaine fonction qui a un pouvoir prédictif.
Comment alors transformer ce rapport ennuyeux en quelque chose de plus utile ?
Comment combler le fossé entre ce reporting et l’apprentissage automatique ?
Souvent, la réponse correcte à cette question est « plus de données » ! Cela peut
se réaliser sous la forme d’un plus grand nombre d’observations portant sur les
mêmes données, ou par la collecte de nouveaux types de données que nous
pourrons ensuite utiliser à des fins de comparaison.
Reprenons le jeu de données mtcars intégré à R et observons-le plus en détail :

head(mtcars)

## mpg cyl disp hp drat wt qsec


vs am gear carb
## Mazda RX4 21.0 6 160 110 3.90 2.620 16.46
0 1 4 4
## Mazda RX4 Wag 21.0 6 160 110 3.90 2.875 17.02
0 1 4 4
## Datsun 710 22.8 4 108 93 3.85 2.320 18.61
1 1 4 1
## Hornet 4 Drive 21.4 6 258 110 3.08 3.215 19.44
1 0 3 1
## Hornet Sportabout 18.7 8 360 175 3.15 3.440 17.02
0 0 3 2
## Valiant 18.1 6 225 105 2.76 3.460 20.22
1 0 3 1

En appelant simplement l’objet mtcars intégré dans R, nous pouvons observer


toutes sortes de colonnes dans les données à partir desquelles il est possible de
faire un choix pour construire un modèle d’apprentissage automatique. Dans le
monde de l’apprentissage automatique, les colonnes de données sont parfois
aussi appelées caractéristiques. Maintenant que nous savons ce dont nous
disposons pour travailler, nous pourrions essayer de voir s’il y a un lien entre le
rendement énergétique de la voiture et l’une de ces caractéristiques. C’est ce
qu’illustre la Figure 1.2 :
pairs(mtcars[1:7], lower.panel = NULL)

Figure 1.2 : Diagrammes de dispersion par paires de caractéristiques du jeu de données mtcars, en se
concentrant sur les sept premières lignes.

Chaque case est un tracé distinct, pour lequel la variable dépendante est la zone
de texte qui se trouve au bas de la colonne, et la variable indépendante est la
zone de texte placée au début de la ligne. Aucun des tracés de la rangée des
cylindres, par exemple, ne semble se prêter facilement à une modélisation par
régression simple.
Dans cet exemple, nous comparons certaines de ces caractéristiques à d’autres.
Les colonnes, ou caractéristiques, de ces données sont définies comme suit :
mpg
Miles par gallon US

cyl
Nombre de cylindres dans le moteur de la voiture

disp
La cylindrée du moteur en pouces cubiques

hp
La puissance du moteur

drat
Le rapport de l’essieu arrière du véhicule

wt
Le poids du véhicule (en milliers de livres)

qsec
Le temps du véhicule pour parcourir un quart de mile

vs
La configuration du moteur du véhicule 5« V » pour un moteur en V, « S » pour un moteur en ligne

am
La transmission du véhicule : 0 pour une boîte automatique, 1 pour une boîte manuelle

gear
Le nombre de vitesses dans la transmission du véhicule

carb
Le nombre de carburateurs utilisés par le moteur du véhicule

Vous pouvez lire le tracé en haut et à droite, par exemple, comme représentant
« mpg en fonction du temps pour le quart de mile ». Ici, nous nous intéressons
essentiellement à quelque chose qui semble avoir une certaine relation
quantifiable. C’est à l’investigateur de choisir les modèles qui semblent
intéressants. Notez que « mpg en fonction de cyl » est très différent de « mpg en
fonction de wt ». Concentrons-nous sur cette dernière relation, comme l’illustre
la Figure 1.3 :

plot(y = mtcars$mpg, x = mtcars$wt, xlab = "Poids de la


Voiture",
ylab = " Rendement Carburant en Miles par Gallon")

Figure 1.3 : Ce graphique est la base pour tracer une ligne de régression à travers les données.

Nous disposons maintenant d’un type de jeu de données plus intéressant. Nous
avons toujours notre efficacité énergétique, mais maintenant elle est comparée au
poids respectif des voitures. À partir de ce type de format, nous pouvons extraire
un meilleur ajustement pour tous les points de données et transformer ce
graphique en équation. Nous y reviendrons plus en détail dans les chapitres
suivants, mais indiquons tout de suite que nous utilisons une fonction dans R
pour modéliser la valeur qui nous intéresse, appelée une réponse, par rapport à
d’autres caractéristiques de notre jeu de données :

mt.model <- lm(formula = mpg ~ wt, data = mtcars)

coef(mt.model)[2]

## wt
## -5.344472

coef(mt.model)[1]
## (Intercept)
## 37.28513

Dans ce morceau de code, nous avons modélisé l’efficacité énergétique du


véhicule (mpg) en fonction du poids de celui-ci (wt), et nous avons extrait des
valeurs de cet objet modèle afin de les utiliser dans une équation que nous
pouvons écrire comme suit :
Efficacité Énergétique = 5.344 × Poids Véhicule + 37.285

Maintenant, si nous voulions savoir quelle était l’efficacité énergétique de


n’importe quelle voiture, et pas seulement celles du jeu de données, nous
n’aurions qu’à entrer le poids de celle-ci, et nous obtiendrions un résultat. C’est
l’avantage apporté par un modèle. Étant donné un certain type d’entrée (par
exemple le poids), nous avons un pouvoir prédictif qui peut nous fournir une
valeur, quel que soit le nombre que nous passons à notre fonction.
Le modèle peut bien sûr avoir ses limites, mais c’est une façon de nous aider à
étendre les données au-delà d’un rapport statique pour en faire quelque chose de
plus souple et de plus perspicace. Le poids d’un véhicule donné peut
évidemment ne pas être prédictif quant à l’efficacité énergétique telle qu’elle est
donnée par l’équation précédente. Il peut aussi y avoir telle ou elle erreur dans
les données ou dans l’observation.
Vous avez peut-être rencontré ce genre de procédure de modélisation avant de
vous lancer dans le monde des données. Si c’est le cas, félicitations : vous avez
fait de l’apprentissage machine sans même le savoir ! Ce type particulier de
modèle d’apprentissage automatique est appelé une régression linéaire. Celle-ci
est beaucoup plus simple que d’autres modèles d’apprentissage automatique,
comme les réseaux de neurones, mais les algorithmes qui font fonctionner un tel
modèle utilisent certainement des principes de l’apprentissage automatique.

Algorithmes contre modèles : Quelle est la


différence ?
L’apprentissage automatique et les algorithmes sont difficilement séparables. Les
algorithmes sont un autre sujet qui peut sembler impénétrable et intimidant au
début, mais ils sont en fait assez simples dans leur fondement, et vous en utilisez
probablement de nombreux sans vous en rendre compte et depuis longtemps.
Un algorithme est une succession d’étapes exécutées dans l’ordre.
Un algorithme, ce n’est rien d’autre que cela. L’algorithme dont vous vous
servez sans le savoir pour mettre vos chaussures pourrait être quelque chose dans
le genre : mettre vos orteils dans la partie ouverte de la chaussure, puis pousser
votre pied vers l’avant et presser votre talon vers le bas. L’ensemble des étapes
nécessaires pour produire un algorithme d’apprentissage automatique sont plus
compliquées que la conception d’un algorithme servant à mettre vos chaussures,
bien entendu. Mais l’un des objectifs de ce livre est d’expliquer le
fonctionnement interne des modèles d’apprentissage automatique les plus
largement utilisés dans R (et non pas dans le domaine de l’habillement) en aidant
à simplifier leurs processus algorithmiques.
L’algorithme le plus simple pour la régression linéaire consiste à placer deux
points sur un graphique, puis à tracer une ligne entre eux. Vous obtenez ainsi les
parties importantes de l’équation (la pente et l’ordonnée à l’origine – soit
respectivement slope et intercept en anglais) en prenant la différence dans les
coordonnées de ces points par rapport à une certaine origine. L’algorithme
devient cependant plus compliqué lorsque vous essayez d’appliquer la même
procédure pour plus de deux points. Ce processus implique davantage
d’équations, ce qui peut être fastidieux à calculer à la main pour un humain, mais
est très facile à traiter en quelques microsecondes pour un processeur
d’ordinateur.
Un modèle d’apprentissage automatique, comme la régression ou bien le
clustering ou encore les réseaux de neurones, repose à la base sur le
fonctionnement d’algorithmes. Les algorithmes sont le moteur qui sous-tend le
code R simple que nous utilisons. Ils réalisent tout le lourd travail de
multiplication des matrices, d’optimisation des résultats et de production d’un
nombre que nous pourrons ensuite utiliser. Il y a dans R de nombreux types de
modèles qui couvrent de manière plus générale tout un écosystème
d’apprentissage automatique. Il existe trois grands types de modèles : les
modèles de régression, les modèles de classification et les modèles mixtes qui
sont une combinaison des deux. Nous avons déjà rencontré un modèle de
régression. Les modèles de classification sont différents dans la mesure où nous
essayons de prendre des données en entrée et de les classer par type, classe,
groupe ou autre sortie discrète. Les modèles mixtes peuvent commencer par une
régression, puis utiliser les résultats ainsi obtenus pour classer d’autres types de
données. L’inverse pourrait être vrai pour d’autres modèles mixtes.
L’appel de fonction pour une régression linéaire simple dans R peut être écrit de
la manière suivante : lm(y ~ x), ce qui peut s’énoncer ainsi « Donnez-moi le
modèle linéaire pour la variable y en fonction de la caractéristique x ». Ce que
vous ne voyez pas, ce sont les algorithmes que le code exécute pour réaliser des
optimisations à partir des données que nous lui donnons.
Dans de nombreux cas, les détails de ces algorithmes dépassent le cadre de ce
livre, mais vous pouvez les consulter facilement. Il est très facile de se perdre
dans les mauvaises herbes des algorithmes et des statistiques lorsque vous
essayez simplement de comprendre quelle est la différence entre un modèle de
régression logistique et un modèle de séparateur à vaste marge (appelé aussi
machine à vecteurs de support, ou SVM).
Bien que la qualité de la documentation dans R puisse varier considérablement
d’une fonction d’apprentissage automatique à une autre, on peut en général
consulter le fonctionnement interne d’un modèle en appelant le fichier d’aide
correspondant, par exemple :

?lm

À partir de ce fichier d’aide, vous pouvez obtenir une mine d’informations sur la
manière dont la fonction elle-même traite les entrées qu’elle reçoit et sur ce
qu’elle produit. De plus, si vous voulez connaître les algorithmes spécifiques
utilisés par cette fonction, vous pouvez trouver des informations à ce sujet dans
le texte lui-même ou sous les citations listées dans les sections « Author(s) » et
« References » (en anglais bien entendu). Certains modèles peuvent toutefois
nécessiter un processus de recherche approfondi pour obtenir la documentation
exacte dont vous avez besoin.

Une note sur la terminologie


Le mot « modèle » est plutôt nébuleux et difficile à séparer de quelque chose
comme une « fonction » ou une « équation ». Au début du chapitre, nous avons
fait un rapport. C’était un objet statique qui n’avait aucun pouvoir prédictif.
Nous avons ensuite fouillé dans les données pour trouver une autre variable que
nous puissions utiliser comme entrée dans une modélisation. Nous avons alors
fait appel à la fonction lm() qui nous a donné à la fin une équation. Nous
pouvons rapidement définir ces termes comme suit :
Rapport
Un objet statique sans puissance prédictive.

Fonction
Un objet qui possède une certaine puissance de traitement, et se trouve probablement à l’intérieur
d’un modèle.

Modèle
Un objet complexe qui reçoit un paramètre en entrée et produit une sortie.

Équation
Une représentation mathématique d’une fonction. Parfois un modèle mathématique.

Algorithme
Ensemble d’étapes qui sont passées à un modèle pour effectuer certains calculs ou traitements.

Il y a des cas où nous utilisons des fonctions qui pourraient ne pas donner des
résultats mathématiques. Par exemple, si nous disposons de beaucoup de
données mais qu’elles ne sont pas sous la bonne forme, nous pourrions
développer un processus permettant de remodeler les données afin d’obtenir
quelque chose de plus utilisable. Si nous devions modéliser ce processus, nous
pourrions avoir à utiliser un organigramme plutôt qu’une équation.
Souvent, ces termes peuvent être employés de façon interchangeable, ce qui peut
prêter à confusion. À certains égards, la terminologie spécifique n’est pas très
importante, mais de savoir que les algorithmes sont intégrés dans un modèle est
important. Le code lm() est en lui-même une fonction, mais c’est aussi un
modèle linéaire. Il appelle une série d’algorithmes pour trouver les meilleures
valeurs qui sont ensuite sorties sous la forme d’une pente et d’une ordonnée à
l’origine. Nous utilisons alors ces pentes et ces ordonnées pour construire une
équation que nous pouvons utiliser pour réaliser des analyses plus approfondies.

Limites de la modélisation
Le statisticien George Box est souvent cité pour sa mise en garde : « Tous les
modèles sont faux, mais certains sont utiles ». Un modèle est une image
simplifiée de la réalité. Et, dans la réalité, les systèmes sont complexes et en
constante évolution. Le cerveau humain est phénoménal de par sa capacité à
découvrir des modèles et donner un sens à l’univers qui nous entoure, mais
même nos sens sont limités. Tous les modèles, qu’ils soient mathématiques,
informatiques ou autres, sont limités par le même cerveau humain qui les
conçoit.
Voici un exemple classique des limites d’un modèle : dans les années 1700, Isaac
Newton avait développé une formulation mathématique décrivant les
mouvements des objets. Elle avait été bien testée et considérée plus ou moins
comme une vérité axiomatique. La loi universelle de gravitation de Newton avait
été utilisée avec beaucoup de succès pour décrire le mouvement des planètes
autour du Soleil. Cependant, il restait une aberration qui n’était pas bien
comprise : l’orbite de Mercure. Lorsque la planète Mercure gravite autour du
Soleil, son périhélie (le point de son orbite qui est le plus proche du Soleil) se
déplace légèrement au fil du temps. Pendant longtemps, les physiciens n’ont pas
pu expliquer cet écart, et ce jusqu’au début du XXe siècle, quand Albert Einstein
a reformulé le modèle avec sa théorie générale de la relativité.
Cependant, même les équations d’Einstein trouvent leurs limites à un certain
niveau. En effet, lorsque de nouveaux paradigmes sont découverts dans le monde
de la science, qu’il s’agisse de la nature nous lançant une balle courbe ou de la
découverte de données anormales dans le monde des affaires, les modèles
doivent être redessinés, réévalués ou réimplémentés pour s’adapter aux
observations.
Pour en revenir à notre exemple mtcars, les limites de notre modèle proviennent
des données, en particulier la période à laquelle elles ont été recueillies, et du
nombre de points de données. Nous ferions des hypothèses plutôt audacieuses au
sujet de l’efficacité énergétique des voitures d’aujourd’hui si nous essayions
d’entrer seulement le poids d’un véhicule moderne dans un modèle qui a été
produit entièrement à partir de voitures fabriquées au début des années 1970. De
même, il y a également très peu de points de données dans ce jeu. Trente-deux
points de données est un échantillon vraiment faible pour pouvoir faire des
prédictions globales quant à l’efficacité énergétique de n’importe quelle voiture.
Les limites de notre modèle mtcars sont donc liées à l’époque et aux
observations. Nous pourrions dire : « Il s’agit d’un modèle portant sur
l’efficacité énergétique de voitures des années 1970 basé sur 32 marques et
modèles différents ». Mais nous ne pourrions pas dire : « C’est un modèle
servant à évaluer l’efficacité énergétique d’une voiture quelconque ».
Différents modèles ont également des limites spécifiques différentes. Nous nous
pencherons plus en détail sur les statistiques plus tard, mais développer un
modèle de régression linéaire simple va s’accompagner d’un certain type
d’erreur. La manière dont nous mesurons cette erreur est très différente de la
façon dont nous mesurons l’erreur pour un modèle tel qu’un partitionnement en
k-moyennes (k-means clustering), par exemple. Il est important de connaître les
erreurs dans un modèle pour commencer à travailler avec lui, mais la façon dont
nous comparons les erreurs entre différents types de modèles est également
importante.
Lorsque George Box dit que tous les modèles sont faux, il veut signifier ainsi
que tous les modèles ont une certaine erreur qui leur est attribuée. Les modèles
de régression ont une méthode spécifique de mesure de l’erreur appelée
coefficient de détermination, souvent appelée aussi valeur « R au carré ». Il
s’agit d’une mesure de la proximité des données avec la droite régression du
modèle pour des valeurs allant de 0 à 1. Un modèle de régression avec une
valeur R2 = 0,99 est un bon ajustement linéaire aux données qu’il modélise, mais
ce n’est pas une corrélation parfaite à 100 %. George Box poursuit en
expliquant1 :
Maintenant, il serait très remarquable qu’un système existant dans le monde réel puisse être
représenté exactement par n’importe quel modèle simple. Cependant, les modèles astucieusement
choisis avec parcimonie fournissent souvent des approximations remarquablement utiles. Par
exemple, la loi PV = RT reliant la pression P, le volume V, et la température T d’un gaz « idéal » via
une constante R n’est pas exactement vraie pour un gaz réel quelconque, mais elle fournit
fréquemment une approximation utile et, en outre, sa structure est informative puisqu’elle est issue
d’une vue physique du comportement des molécules de gaz. Pour un tel modèle, il n’est pas
nécessaire de se poser la question « Le modèle est-il vrai ? ». Si « vrai » doit être la « vérité entière »,
alors la réponse sera « Non ». La seule question intéressante est : « Le modèle est-il éclairant et
utile ? ».

Sans nous enfoncer trop profondément dans les mauvaises herbes de la


philosophie de la science, la modélisation en apprentissage machine sous toutes
ses formes n’est qu’une approximation de l’univers que nous étudions. La
puissance d’un modèle vient de son utilité, qui peut provenir de l’exactitude de
ses prédictions, rien de plus. Un modèle peut être limité par sa vitesse de calcul
ou sa capacité à être expliqué simplement, ou bien à être utilisé dans un cadre
particulier. Il est donc important d’expérimenter et de tester les données avec de
nombreux types de modèles et de choisir ce qui correspond le mieux à nos
objectifs. Implémenter un modèle de régression sur un backend informatique
pourrait être beaucoup plus simple à mettre en œuvre qu’un modèle de forêt
aléatoire de régression, pour lequel il pourrait y avoir un compromis sur
l’exactitude d’une petite quantité.
Statistiques et calcul dans la
modélisation
Quand nous réfléchissons à l’apprentissage automatique, nous pensons d’une
manière naïve presque exclusivement aux ordinateurs. Comme nous le verrons
tout au long de ce livre, l’apprentissage automatique repose sur les
mathématiques et les statistiques. En fait, vous pourriez faire tous les calculs
d’apprentissage automatique à la main en n’utilisant que les mathématiques.
Cependant, un tel processus devient insoutenable après seulement quelques
points de données (selon l’algorithme). Même le modèle de régression linéaire
simple sur lequel nous avons travaillé plus tôt dans ce chapitre devient
exponentiellement plus compliqué lorsque nous passons de deux à trois points de
données. Cependant, les langages de programmation modernes ont tellement de
fonctions et de packages intégrés qu’il est presque impossible de ne pas avoir
une fonction de régression qui demande simplement une ligne de code pour
obtenir le même résultat en une fraction de temps. Vous avez déjà cette situation
avec l’appel de la fonction lm() dans R. La plupart du temps, une solide
compréhension statistique est primordiale pour comprendre le fonctionnement
interne d’un algorithme d’apprentissage automatique. Et c’est deux fois plus vrai
si un collègue vous demande d’expliquer pourquoi vous avez utilisé un tel
algorithme.
Dans ce livre, nous explorons les mathématiques qui se cachent derrière le
fonctionnement de ces algorithmes, mais sans éclipser pour autant l’accent mis
sur le code R et sur les meilleures pratiques. Il peut être tout à fait révélateur de
comprendre comment un modèle de régression trouve les coefficients que nous
utilisons pour construire l’équation, d’autant plus que certains des algorithmes
utilisés pour calculer ces coefficients peuvent apparaître dans d’autres modèles
d’apprentissage automatique et dans des fonctions totalement différentes de la
régression linéaire.
Avec toujours comme exemple la régression linéaire, nous pourrions être
intéressés par les statistiques qui définissent l’exactitude du modèle. Il y en a de
nombreuses à énumérer, et certaines sont trop « statistiques » pour un texte
d’introduction, mais une liste générale comprendrait les éléments suivants :
Coefficient de détermination
Aussi appelé R2, il mesure l’adéquation entre le modèle et les données.

p-valeurs
Il s’agit de mesures de validité statistique, où, si votre p-valeur est inférieure à 0.5, la valeur que vous
examinez est susceptible d’être statistiquement valide.

Intervalles de confiance
Ce sont deux valeurs entre lesquelles on s’attend à ce qu’un paramètre se trouve. Par exemple, un
intervalle de confiance à 95 % entre les chiffres 1 et 3 pourrait décrire la position du chiffre 2.

Nous pouvons utiliser ces notions pour comprendre la différence entre un


modèle qui s’ajuste bien aux données et un modèle qui s’ajuste mal. Nous
pouvons évaluer les caractéristiques qui nous sont utiles dans notre modèle, et
nous pouvons déterminer l’exactitude des réponses produites par celui-ci.
La formulation mathématique de base pour la modélisation de la régression avec
deux points de données est souvent enseignée à un niveau scolaire moyen, mais
la théorie va rarement au-delà de trois points de données ou plus. La raison en
est que, pour calculer les coefficients de cette façon, nous avons besoin
d’employer certaines techniques d’optimisation, comme la descente de gradient.
Ces techniques se situent souvent au-delà de la portée des mathématiques
enseignées à un niveau moyen, mais elles constituent un fondement important
pour de nombreux modèles différents et pour la façon dont ils obtiennent les
chiffres les plus exacts à utiliser.
Il est possible d’exécuter des modèles d’apprentissage automatique sans
connaître les détails complexes des optimisations qui les sous-tendent, mais,
lorsque vous vous lancez dans des réglages de modèles plus avancés, ou lorsque
vous devez rechercher des bogues ou encore évaluer les limites des modèles, il
est essentiel de bien saisir les bases des outils avec lesquels vous travaillez.
Données d’entraînement
Une méthode statistique que nous couvrons en détail est celle des données
d’entraînement. L’apprentissage automatique nous oblige à entraîner d’abord un
modèle de données, mais qu’est-ce que cela signifie exactement ? Disons que
nous avons un modèle pour lequel nous disposons en entrée de données qui
passent alors par un algorithme générant une sortie. Dit autrement, nous avons
des données pour lesquelles nous voulons une prédiction, nous les faisons passer
à travers le modèle et nous obtenons un résultat. Nous évaluons ensuite une série
de résultats et nous voyons si les erreurs correspondantes dans le modèle
diminuent ou non. Si c’est le cas, cela signifie que nous ajustons le modèle dans
la bonne direction. Si, en revanche, les erreurs continuent de s’accumuler, cela
signifie que nous devons peaufiner davantage notre modèle.
Il est très important pour nous de ne pas entraîner nos modèles d’apprentissage
automatique sur les données que nous lui retournerons ensuite afin de tester leur
validité. Si, par exemple, nous entraînons un modèle de type « boîte noire »
sur 50 points de données, et que nous passions ensuite ces mêmes 50 points de
données à travers notre modèle, le résultat que nous obtiendrons sera d’une
exactitude suspecte. Cela vient du fait que notre « boîte noire » a déjà vu les
données, de sorte qu’elle connaît forcément la bonne réponse.
Nous dépendons souvent de la disponibilité des données. Nous ne pouvons pas
transmettre dans un modèle des données que nous ne connaissons pas pour les
tester et voir si ce modèle est exact. Si, toutefois, nous prenons notre jeu de
données de 50 points, et que nous divisions ceux-ci de telle sorte que nous en
utilisons la majorité pour l’entraînement, mais que nous en laissons certains de
côté pour les tests, nous pouvons résoudre notre problème d’une manière plus
valide sur le plan statistique. Le danger avec cette façon de procéder, c’est
d’effectuer une division entre un jeu d’entraînement et un jeu de test alors que
nous ne possédons au départ qu’un petit nombre de points de données. Mais si
nous étions trop limités dans les observations disponibles, l’utilisation de
techniques avancées d’apprentissage automatique pourrait de toute façon ne pas
être la meilleure approche.
Maintenant, si nous avons notre jeu comportant 50 points de données, et que
nous divisions ces points selon un rapport 80/20 (soit 40 points pour le jeu
d’entraînement et 10 points pour le jeu de test), nous pouvons mieux évaluer la
performance du modèle. Le modèle de boîte noire sera entraîné sur des données
qui sont fondamentalement de la même forme que celles du jeu de test (avec un
peu de chance), mais ce modèle n’aura pas encore vu les points de données
exacts du jeu de test. Une fois que le modèle est au point et que nous lui passons
le contenu du jeu de test, il peut faire des prédictions sans risque d’être biaisé par
les données qu’il aura déjà vues auparavant.
Les méthodes de partition des données à des fins d’entraînement et de test sont
connues sous le nom de techniques d’échantillonnage. Celles-ci peuvent se
présenter sous de nombreuses formes, par exemple en prenant les 40 premières
lignes de données comme jeu d’entraînement, en sélectionnant des lignes de
données de manière aléatoire, ou en faisant appel à des techniques plus avancées.
Validation croisée
Les données d’entraînement sont très utiles pour mettre au point les modèles
d’apprentissage automatique. Mettre au point un modèle, c’est quand vous avez
un tas d’entrées dont les valeurs sont susceptibles de changer légèrement sans
modifier les données sous-jacentes. Par exemple, vous pourriez avoir un modèle
avec trois paramètres susceptibles d’être réglés comme suit : A = 1, B = 2, C =
"FALSE". Si votre modèle ne fonctionne pas correctement, vous pourriez
l’ajuster en changeant les valeurs de vos paramètres, disons avec A = 1.5, B
= 2.5, C = "FALSE", et ainsi de suite en effectuant diverses permutations.
De nombreux modèles ont des méthodes intégrées pour « ingérer » des données,
effectuer certaines opérations sur elles, puis enregistrer les opérations après mise
au point vers une structure de données qui sera utilisée sur les données de test.
Dans de nombreux cas, pendant la phase d’entraînement, vous voudrez peut-être
essayer d’autres techniques statistiques comme la validation croisée. C’est un
peu comme une autre « mini-étape » consistant à procéder à une nouvelle
division entre jeux d’entraînement et de test pour faire tourner le modèle, cette
division portant seulement sur les données d’entraînement.
Par exemple, vous partez de votre jeu de données de 50 points, et vous le divisez
dans une proportion de 80 % pour former le jeu d’entraînement, en laissant le
reste pour votre jeu de test. Vous avez donc 40 lignes pour entraîner votre
modèle. Vous pouvez à nouveau diviser ces 40 lignes en un jeu d’entraînement
comportant 32 points de données, et un jeu de test avec 8 points de données. En
procédant ainsi et en passant par une procédure d’entraînement et de test
similaire, vous pouvez obtenir un jeu d’erreurs à partir de votre modèle et utiliser
celles-ci pour vous aider à affiner encore plus les réglages de votre modèle.
Voici quelques exemples de techniques de validation croisée dans R :

• Validation croisée dite bootstrap (avec rééchantillonage)

• Validation croisée bootstrap .632

• Validation croisée k-fold (k-plis)

• Validation croisée répétée


• Validation croisée de type Leave-one-out (LOO)

• Validation croisée de type Leave-group-out

• Validation croisée de type Out-of-bag

• Validation croisée adaptative

• Validation croisée bootstrap adaptatif

• Validation croisée Leave-group-out adaptatif


Nous reviendrons sur ces méthodes plus tard. Leur utilisation dépend fortement
de la structure des données elles-mêmes. L’étalon-or typique des techniques de
validation croisée est la variante k-plis, ou k-fold, dans laquelle vous choisissez k
= 10 plis contre lesquels valider. C’est le meilleur équilibre entre une utilisation
efficace des données et le fait d’éviter des divisions dans ces données qui
pourraient se révéler être de mauvais choix. Le Chapitre 3 examine plus en détail
la validation croisée k-fold.
Pourquoi utiliser R ?
Dans ce livre, nous proposons une introduction en douceur au monde de
l’apprentissage automatique, introduction illustrée avec du code et des exemples
en langage R. R est un langage de programmation libre et open source qui reçoit
son héritage du monde des statistiques, en étant construit initialement à partir de
S et ensuite S+. Ainsi, même si le langage R lui-même n’existe pas depuis très
longtemps, il est bien le descendant historique de ses prédécesseurs, en
conservant une bonne similitude syntaxique avec ce que nous voyons
aujourd’hui. La question est la suivante : pourquoi utiliser préférentiellement R ?
Il y a tellement de langages de programmation parmi lesquels choisir, et donc
comment savoir lequel est le meilleur pour ce que vous voulez accomplir ?
Le bon…
La popularité de R s’est étendue à un rythme explosif. Les outils
complémentaires pour apprendre R se sont également développés, et il n’y a pas
de pénurie d’excellents tutoriels et cours en ligne, bien au contraire ! Un
package, swirl, peut même vous apprendre à utiliser R à partir de la console elle-
même. De nombreux cours en ligne proposent également des formations par
l’intermédiaire de swirl. Certains couvrent des analyses de données simples,
d’autres des sujets plus complexes comme les biostatistiques mathématiques.
R possède également d’excellents outils pour l’accessibilité et la reproduction du
travail. Des visualisations Web, comme le package shiny, rendent possible la
construction d’applications Web interactives qui peuvent être utilisées par des
non-experts pour interagir avec des jeux de données complexes sans avoir besoin
de connaître ou même d’installer R.
Il existe de surcroît un certain nombre d’environnements de développement
intégrés (IDE) dédiés à R, le plus populaire étant R Studio, illustré sur la
Figure 1.4.
En fait, ce livre a été écrit dans R Studio, en utilisant R Markdown. R supporte
en effet une version de Markdown, un langage léger qui vous permet d’effectuer
des conversions vers toutes sortes de formats, aussi bien pour l’affichage sur le
Web que pour un rendu en PDF. C’est un excellent moyen de partager du code
via la publication sur le Web ou pour écrire des documentations professionnelles.
Cela vous donne la possibilité d’écrire de larges pans de texte, mais aussi de
fournir des exemples graphiques tels que ceux montrés plus haut dans le
chapitre.
Une autre caractéristique puissante du langage R est la prise en charge des data
frames. Ces tableaux (ou cadres, ou encore trames) de données sont comme une
base de données SQL en mémoire qui vous permet de remodeler et de manipuler
les données pour les récapituler et de les soumettre à de multiples traitements
intéressants. Contrairement à une matrice traditionnelle, dans laquelle chaque
colonne contient le même type de données, les data frames vous permettent de
mélanger différents types.
Il existe un nombre incalculable de situations dans lesquelles vous aurez à
travailler avec des données dans lesquelles vous disposerez d’un champ
« Nom », contenant des chaînes de caractères, suivi de plusieurs colonnes
numériques comme « ID » ou « Ventes ». La lecture de ces données avec
certains langages peut être une source de problème si vous ne pouvez pas mixer
et faire correspondre les différents types de données des colonnes.

Figure 1.4 : R Studio est un environnement de développement intégré (IDE) pour R qui est gratuit, très
stable et également convivial pour les nouveaux utilisateurs du langage.

Dans l’exemple qui suit, nous avons trois vecteurs de types différents : un
vecteur numérique, un vecteur de facteurs, et un vecteur de valeurs logiques. En
utilisant des data frames, vous pouvez combiner tous ces éléments en un seul jeu
de données. Plus souvent qu’à l’inverse, nous avons, dans le monde de la science
des données, à travailler dans un même tableau avec des données de types variés.
Souvent, cela peut être utile pour obtenir des sous-ensembles du tableau en
fonction de certains critères à des fins d’analyse.

v1 = c(1, 2, 3)
v2 = c("Jerry", "George", "Elaine")
v3 = c(TRUE, FALSE, TRUE)
data_frame = data.frame(v1, v2, v3)

str(data_frame)

## 'data.frame': 3 obs. of 3 variables:


## $ v1: num 1 2 3
## $ v2: Factor w/ 3 levels "Elaine","George",..: 3 2 1
## $ v3: logi TRUE FALSE TRUE

La manipulation des données occupe la majorité du temps de ceux qui sont


impliqués dans l’analyse des données, et R possède plusieurs packages pour
faciliter ce travail. Ainsi, le package dplyr est un outil fantastique pour
remodeler et manipuler les données selon un verbiage possédant un sens intuitif.
Le package lubridate est également un moyen puissant de manipuler des données
ayant un format date/heure compliqué.
Avec le package dplyr vient aussi l’opérateur de tube (pipe), %>%. Cet outil
utile vous permet d’éviter certaines redondances dans le code. Au lieu d’affecter
une variable var1 à une entrée puis d’utiliser cette entrée dans une autre étape
nommée var2, et ainsi de suite, vous pouvez utiliser cet opérateur de pipe
comme une partie « alors faire » de votre code.

R et l’apprentissage automatique
R est associé à un grand nombre de bons packages d’apprentissage automatique.
Vous pouvez en voir certains sur la page d’accueil du CRAN (https://cran.r-
project.org/web/views/MachineLearning.html). Pour autant, la liste des modèles
d’apprentissage automatique existant est beaucoup plus longue. Il existe en effet
plus de 200 types de modèles d’apprentissage automatique qui sont plus ou
moins populaires dans l’écosystème R, et chacun d’entre eux est régi par des
règles assez strictes (vous trouverez cette liste en annexe).
Nous avons choisi R pour ce livre car l’apprentissage automatique trouve son
origine dans les statistiques, et R est bien adapté pour illustrer ces relations.
L’écosystème des packages de modélisation statistique dans R est pour
l’essentiel robuste et convivial. La gestion des données dans R constitue une
grande partie des fonctionnalités dont un data scientist a besoin au quotidien, et
R est très bien adapté pour une telle tâche.
Bien que R soit solide et relativement facile à apprendre du point de vue de la
science des données, la vérité est qu’il n’existe pas de langage de programmation
unique qui serait le meilleur pour couvrir tous vos besoins. Si vous travaillez sur
un projet qui exige que les données se présentent sous une forme spécifique, il
peut y avoir un langage qui possède déjà un package adapté à cette structure
avec un haut degré de précision et de rapidité. Dans d’autres cas, il se peut aussi
que vous ayez besoin de construire votre propre solution à partir de zéro.
R offre un outil fantastique pour aider au processus de modélisation, connu sous
le nom d’opérateur fonction. Cet opérateur symbolique agit comme un signe
égal dans une formule mathématique. Plus haut, nous avons vu l’exemple d’un
modèle linéaire dans lequel nous avions lm(mtcars$mpg ~ mtcars$wt). Dans ce
cas, mtcars$mpg était notre réponse, l’élément que nous voulions modéliser en
tant que sortie, et mtcars$wt était l’entrée. Mathématiquement parlant, y ~ x dans
le code R correspondrait à une notation de la forme y = f(x).
Ce puissant opérateur vous permet d’utiliser très facilement plusieurs entrées.
Nous pourrions par exemple rencontrer une fonction multivariée s’écrivant
comme suit en mathématiques :

y =f(x1, x2, x3, ...)

Avec R, cette formulation est très simple :

y ~ x_1 + x_2 + x_3

Ce que nous faisons ici consiste à dire que la sortie de notre modélisation, y,
n’est pas seulement une fonction de x1, mais aussi de nombreuses autres
variables. Nous verrons le moment venu comment nous pouvons utiliser des
caractéristiques ou entrées multiples dans nos modèles d’apprentissage
automatique.
… et le moins bon
R a aussi quelques inconvénients. Dans son écosystème, de nombreux
algorithmes sont fournis par la communauté des utilisateurs ou d’autres tiers, de
sorte qu’il peut y avoir une certaine incohérence entre eux et d’autres outils.
Chaque package dans R est comme son propre mini-écosystème, ce qui nécessite
une certaine dose de compréhension pour aller jusqu’au bout.
De plus, certains de ces packages ont été développés il y a longtemps, et il n’est
pas évident de savoir quelle est la meilleure application (la « killer app ») pour
un modèle d’apprentissage automatique particulier. Vous pourriez vouloir
construire un modèle de réseau de neurones simple, par exemple, mais vous
voudriez aussi le visualiser. Parfois, vous aurez peut-être besoin de sélectionner
un package que vous connaissez moins bien en par rapport à sa fonctionnalité
spécifique, et laisser de côté votre package préféré.
Parfois aussi, la documentation de certains packages plus ou moins obscurs peut
aussi être incohérente. Comme cela a déjà été mentionné, vous pouvez extraire le
fichier d’aide ou une page de manuel pour une fonction donnée dans R en tapant
quelque chose comme ?lm() ou encore ?rf(). Dans de nombreux cas, vous
trouverez des exemples utiles au bas de la page montrant comment exécuter la
fonction. Cependant, certains cas sont inutilement complexes et peuvent être
simplifiés dans une large mesure. Un des buts de ce livre est d’essayer de
présenter des exemples en partant des cas les plus simples afin de construire une
compréhension de base du modèle, pour ensuite développer la complexité de son
fonctionnement.
Enfin, la manière dont R fonctionne d’un point de vue programmatique peut
pousser certains développeurs professionnels à grimper au mur quand ils voient
la façon dont il gère certaines choses comme les conversions de type pour les
structures de données. Les gens habitués à travailler dans un langage orienté
objet très strict, où il faut allouer des quantités spécifiques de mémoire,
trouveront R plutôt laxiste dans ce genre de traitement. Il est facile alors de
prendre de mauvaises habitudes face à de tels pièges, mais ce livre vise à éviter
ceux-ci afin de se concentrer sur son objectif : expliquer avec clarté et simplicité
le paysage de l’apprentissage automatique.
En résumé
Dans ce chapitre, nous avons défini la vision de notre exploration de
l’apprentissage automatique à l’aide du langage de programmation R.
Nous avons d’abord examiné ce qui constitue un modèle, et en quoi cela diffère
d’un rapport. Vous avez vu qu’un rapport statique ne nous dit pas grand-chose en
termes de prédictions. Vous pouvez transformer un rapport en quelque chose qui
ressemble plus à un modèle en introduisant d’abord une autre caractéristique,
puis en examinant s’il y a une relation quelconque dans les données. Vous
ajustez ensuite un modèle de régression linéaire simple en utilisant la fonction
lm() et vous obtenez comme résultat final une équation. Une caractéristique de R
qui est très puissante pour le développement de modèles est l’opérateur ~. Vous
pouvez utiliser cette fonction pour représenter symboliquement les formules que
vous essayez de modéliser.
Nous avons ensuite exploré la sémantique de ce qui définit un modèle. Un
modèle d’apprentissage automatique tel que la régression linéaire utilise des
algorithmes comme la descente de gradient pour exécuter ses procédures
d’optimisation en arrière-plan. Vous appelez la régression linéaire dans R en
utilisant la fonction lm() puis vous extrayez ensuite les coefficients du modèle,
en utilisant ceux-ci pour construire votre équation.
Une étape importante avec l’apprentissage automatique (et la modélisation en
général) est de comprendre les limites des modèles. Le fait d’avoir un modèle
robuste à partir d’un ensemble complexe de données n’empêche pas le modèle
lui-même d’être limité dans une perspective temporelle, comme nous l’avons vu
avec nos données mtcars. De plus, tous les modèles comportent une certaine
forme d’erreur qui leur est liée. Nous explorons l’évaluation des erreurs sur une
base modèle par modèle, étant donné que nous ne pouvons pas comparer
directement certains types à d’autres.
Beaucoup de modèles d’apprentissage automatique utilisent des algorithmes
statistiques complexes pour calculer ce que nous voulons. Dans ce livre, nous
couvrons les bases de ces algorithmes, mais nous allons nous concentrer
davantage sur l’implémentation et l’interprétation du code. Les techniques
statistiques qui interviennent dans la manière dont nous façonnons les données à
des fins d’entraînement et d’essai sont toutefois discutées en détail. Souvent, il
est très important de savoir comment ajuster spécifiquement le modèle
d’apprentissage automatique choisi, ce qui exige une bonne connaissance de la
façon de gérer les jeux d’entraînement avant de passer les données de test au
modèle entièrement optimisé.
Pour terminer ce chapitre, nous expliquons pourquoi R est un outil approprié
pour l’apprentissage automatique. R a son pedigree et son histoire dans le
domaine de la statistique, ce qui en fait une bonne plateforme sur laquelle
construire des cadres de modélisation utilisant ces statistiques. Bien que
certaines opérations dans R puissent être un peu différentes de ce qui se passe
dans d’autres langages de programmation, R offre dans l’ensemble une interface
relativement simple à utiliser pour nombre de concepts et de fonctions
complexes d’apprentissage automatique.
En tant que langage de programmation open source, R offre un grand nombre de
modèles d’apprentissage automatique et d’algorithmes statistiques de pointe.
Cela peut être une épée à double tranchant en termes de fichiers d’aide ou de
manuels, mais ce livre a aussi pour but d’aider à simplifier certains des exemples
les plus impénétrables rencontrés lors de la recherche d’une aide.
Dans le Chapitre 2, nous allons explorer certains des modèles d’apprentissage
automatique les plus populaires et nous verrons comment les utiliser dans R.
Chaque modèle est présenté d’une manière introductive avec quelques exemples
à l’appui. Nous développerons ces notions dans des chapitres plus approfondis
consacrés à chaque sujet.

1 Box, G. P., l. S. Hunter, et W G. Hunter. Statistics for Experimenters. 2nd ed. John Wiley & Sons, 2005.
CHAPITRE 2
Apprentissage automatique supervisé et
non supervisé
Dans l’univers des algorithmes d’apprentissage automatique, il en existe deux
grands types : supervisé et non supervisé. On parle d’apprentissage supervisé
dans le cas où un modèle d’apprentissage automatique est évalué et ajusté en
fonction d’une certaine quantité connue. La majorité des algorithmes
d’apprentissage automatique sont d’ailleurs supervisés. On parle d’apprentissage
non supervisé dans le cas où le modèle d’apprentissage automatique déduit des
schémas et des informations à partir des données, tout en déterminant le
paramètre d’ajustement de la quantité connue lui-même. Ces modèles sont plus
rares en pratique, mais ils sont utiles en soi et peuvent aider à orienter notre
réflexion quant à la manière d’explorer les données en vue de réaliser une
analyse plus approfondie.
Un exemple d’apprentissage supervisé pourrait être quelque chose comme ceci :
nous avons un modèle que nous avons construit et qui dit que « toute entreprise
qui vend moins de 10 unités est un mauvais performeur, et toute entreprise qui
vend plus de 10 unités est un bon performeur ». Nous avons ensuite un jeu de
données que nous voulons tester en partant de cet énoncé. Supposons que nos
données contiennent les chiffres d’un magasin qui vend huit unités. C’est moins
de 10, et donc, selon la définition de notre modèle, il est classé comme étant un
mauvais performeur. Dans cette situation, nous avons un modèle qui absorbe les
données qui nous intéressent, et qui nous donne un résultat tel que décidé selon
les conditions posées par le modèle.
Par contraste, un modèle d’apprentissage non supervisé pourrait ressembler à
ceci : nous avons un tas de données, et nous voulons savoir comment les scinder
en groupes significatifs. Nous pourrions par exemple avoir une masse de
données provenant d’une enquête sur la taille et le poids des gens. Nous pouvons
alors utiliser certains algorithmes non supervisés pour trouver un moyen de
regrouper ces informations en grappes (ou clusters) significatives pour lesquelles
nous serions à même de définir des tailles de vêtements. Dans ce cas, le modèle
n’a pas de réponse préalable lui disant : « Connaissant la taille et le poids de
cette personne, je devrais la classer comme ayant une petite taille de pantalon ».
Il doit trouver cette réponse par lui-même.
Modèles supervisés
Les modèles supervisés sont plus courants que les modèles non supervisés. Ils
sont disponibles en trois variantes principales :
Régression
Ces modèles sont très courants, et il est probable que vous en avez rencontré au moins un dans vos
cours de mathématiques du secondaire. Ils servent principalement à examiner l’évolution de données
par rapport à une autre variable (par exemple, le temps) et à examiner ce que vous pouvez faire pour
prédire les valeurs à l’avenir.

Classification
Ces modèles sont utilisés pour organiser vos données en schémas correspondant à des catégories. Par
exemple, les magasins qui vendent plus de 10 unités par semaine pourraient être classifiés comme
ayant de bonnes performances, alors que ceux qui en vendent moins seraient classifiés comme ayant
de mauvais résultats.

Mixte
Ces modèles peuvent souvent s’appuyer en partie sur la régression pour déterminer comment
procéder à une classification, ou parfois l’inverse. Un cas possible pourrait être l’examen de
l’évolution de données de ventes au fil du temps, en se posant la question de savoir s’il y a un
changement rapide dans cette évolution au cours d’une certaine période.
Régression
Tel un Monsieur Jourdain moderne, la modélisation de régressions est quelque
chose que vous avez probablement réalisé de nombreuses fois sans vous rendre
compte que vous faisiez de l’apprentissage automatique. Au fond, une droite de
régression est une ligne que nous ajustons en fonction de données possédant un
élément x et un élément y. Nous utilisons ensuite une équation pour prédire
quelle devrait être la sortie correspondante, y, pour n’importe quelle entrée, x.
Cela est toujours réalisé sur des données numériques.
Prenons un exemple de problème de régression :

head(mtcars)

## mpg cyl disp hp drat wt qsec


vs am gear carb

## Mazda RX4 21.0 6 160 110 3.90 2.620 16.46


0 1 4 4
## Mazda RX4 Wag 21.0 6 160 110 3.90 2.875 17.02
0 1 4 4
## Datsun 710 22.8 4 108 93 3.85 2.320 18.61
1 1 4 1
## Hornet 4 Drive 21.4 6 258 110 3.08 3.215 19.44
1 0 3 1
## Hornet Sportabout 18.7 8 360 175 3.15 3.440 17.02
0 0 3 2
## Valiant 18.1 6 225 105 2.76 3.460 20.22
1 0 3 1

mtcars est l’un des nombreux jeux de données intégrés dans R. Il contient des
données sur 32 voitures reprises d’un numéro de 1974 de la revue américaine
Motor Trend. Nous avons 11 caractéristiques allant de l’efficacité énergétique de
la voiture (exprimée en miles par gallon US) à son poids, et même au type de
transmission manuelle ou automatique de chaque véhicule. La Figure 2.1 illustre
le rendement énergétique des voitures (mpg) dans le jeu de données en fonction
de la cylindrée du moteur (disp), en pouces cubiques :

plot(y = mtcars$mpg, x = mtcars$disp, xlab = " Cylindrée


du moteur
(pouces cubiques) ", ylab = " Efficacité énergétique
(Miles par gallon)")

Figure 2.1 : Tracé de l’efficacité énergétique des voitures listées dans le jeu de données mtcars en fonction
de la cylindrée du moteur.

Nous pouvons voir sur le graphique que le rendement énergétique diminue à


mesure que la cylindrée du moteur augmente. Cependant, pour un nouveau
moteur dont vous voudriez connaître l’efficacité, le tracé de la Figure 2.1 ne
vous donnera pas vraiment une réponse exacte. Pour cela, il faut construire un
modèle linéaire :

model <- lm(mtcars$mpg ~ mtcars$disp)


coef(model)

## (Intercept) mtcars$disp
## 29.59985476 -0.04121512

La pierre angulaire de la modélisation d’une régression dans R est la fonction


lm(). Nous utilisons également un autre opérateur puissant de R : l’opérateur de
formule noté ~. Vous vous rappelez certainement que la modélisation de la
régression est de la forme y = mx + b, où la sortie y est déterminée à partir d’une
pente donnée (m), de l’ordonnée à l’origine (b), et des données passées en entrée,
x. Votre modèle linéaire est donné dans ce cas par les coefficients que vous
venez de calculer, et donc le modèle ressemble à ce qui suit :
Efficacité énergétique = -0.041 x Cylindrée moteur + 29.599

Vous disposez maintenant d’un modèle d’apprentissage automatique très


simple ! Vous pouvez utiliser n’importe quelle entrée pour spécifier la cylindrée
du moteur et obtenir en retour une valeur. Examinons par exemple le rendement
énergétique d’une voiture d’une cylindrée de 200 pouces cubiques :

-0.041 * 200 + 25.599

## [1] 17.399

Une autre façon, plus précise, de s’y prendre consiste à appeler directement les
coefficients du modèle :

coef(model)[2] * 200 + coef(model)[1]

## mtcars$disp
## 21.35683

Vous pouvez répéter cette opération avec n’importe quelle entrée numérique qui
vous intéresse. Cependant, vous voudrez peut-être élargir cette analyse pour y
inclure d’autres caractéristiques. Vous souhaiteriez peut-être avoir un modèle qui
calcule le rendement énergétique du véhicule en fonction non seulement de la
cylindrée du moteur, mais aussi du nombre de cylindres, de la puissance, du
nombre de vitesses, etc. Vous voudrez peut-être aussi essayer différentes
fonctions pour les adapter aux données, car si nous essayons de voir ce qui se
passe pour un moteur théorique de 50 000 pouces cubiques, l’efficacité
énergétique devient négative ! Nous explorerons ces types d’approches plus en
profondeur dans le Chapitre 4, exclusivement dédié aux modèles de régression
dans R.
Entraîner et tester des données
Avant de passer à l’autre grand domaine de l’apprentissage supervisé, nous
devons aborder le sujet de l’entraînement et du test des données. Comme nous
l’avons vu jusqu’ici avec la modélisation par régression linéaire simple, nous
disposons d’un modèle que nous pouvons utiliser pour prédire des valeurs
futures. Pourtant, nous ne savons rien pour le moment en ce qui concerne
l’exactitude du modèle. Une façon de déterminer cette exactitude consiste à
examiner la valeur R au carré à partir du modèle :

summary(model)

##
## Call:
## lm(formula = mtcars$mpg ~ mtcars$disp)
##
## Residuals:
## Min 1Q Median 3Q Max
## -4.8922 -2.2022 -0.9631 1.6272 7.2305
##
## Coefficients:
## Estimate Std. Error t value Pr(>¦t¦)
## (Intercept) 29.599855 1.229720 24.070 < 2e-16 ***
## mtcars$disp -0.041215 0.004712 -8.747 9.38e-10 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.'
0.1 ' ' 1
##
## Residual standard error: 3.251 on 30 degrees of
freedom
## Multiple R-squared: 0.7183, Adjusted R-squared:
0.709
## F-statistic: 76.51 on 1 and 30 DF, p-value: 9.38e-10

L’appel à la fonction summary() sur notre objet modèle nous donne beaucoup
d’informations. Le paramètre d’exactitude, qui est le plus important pour nous à
ce stade, est la valeur R-carré ajustée. Cette valeur nous indique le degré de
corrélation linéaire des données : plus elle est proche de l, et plus la sortie du
modèle est probablement gouvernée par des données qui forment presque
exactement une ligne droite possédant une certaine pente. La raison pour
laquelle nous nous concentrons sur la partie ajustée plutôt que sur le multiple est
que, dans des scénarios futurs, nous utiliserons davantage de caractéristiques
dans un modèle. Lorsque le nombre de caractéristiques est faible, les valeurs de
R-carré ajustées et multiples sont fondamentalement la même chose. Pour les
modèles qui possèdent beaucoup de caractéristiques, il est préférable d’utiliser
plusieurs valeurs R-carré, car cela donnera une évaluation plus précise de
l’erreur du modèle.
Mais qu’est-ce que cela nous dit pour ce qui concerne l’estimation de l’erreur
pour le modèle ? Nous obtenons bien des valeurs d’erreur standard à partir de la
sortie, mais il y a un problème avec le modèle, puisqu’il est entraîné sur toutes
les données, puis testé sur ces mêmes données. Ce que nous voulons faire, afin
d’assurer un niveau d’erreur non biaisé, c’est de diviser notre jeu de données de
départ en deux parties : un jeu de données pour l’entraînement, et un second jeu
de données pour le test. C’est une tache courante dans le monde des statistiques.
Vous pouvez jouer avec ces chiffres à votre guise, mais en prenant toujours plus
de données d’entraînement que de données de test. Par exemple :

split_size = 0.8

sample_size = floor(split_size * nrow(mtcars))


set.seed(123)
train_indices <- sample(seq_len(nrow(mtcars)), size =
sample_size)

train <- mtcars[train_indices, ]


test <- mtcars[-train_indices, ]

Cet exemple fixe la taille du fractionnement à 80 % (dans split_size), autrement


dit la taille de l’échantillon pour l’entraînement est fixée à 80 % du nombre total
de lignes dans les données mtcars. Nous définissons ensuite une « graine » pour
la reproductibilité (avec set_seed), et nous obtenons une liste d’indices de lignes
de données que nous allons injecter dans notre jeu d’entraînement. Nous
divisons ensuite les données entre entraînement (train) et test en définissant les
premières comme correspondant aux lignes qui contiennent ces indices, et les
secondes comme contenant tout le reste.
Maintenant, nous voulons construire un modèle de régression en utilisant
uniquement les données d’entraînement. Nous passons ensuite les valeurs des
données de test dans ce modèle pour obtenir les sorties correspondantes.
L’élément clé ici est que nous disposons des données connues à partir desquelles
nous pouvons tester le modèle. Cela nous permet d’obtenir une meilleure
estimation du niveau d’erreur :

model2 <- lm(mpg ~ disp, data = train)

new.data <- data.frame(disp = test$disp)

test$output <- predict(model2, new.data)

sqrt(sum(test$mpg - test$output)^2/nrow(test))

## [1] 4.627365

Passons en revue ces étapes pour calculer l’erreur réelle du modèle. Si vous
regardez l’erreur standard résiduelle affichée plus haut par la fonction
summary(model), vous observerez que la valeur indiquée est de 3.251.
Cependant, cette valeur est douteuse car elle a été calculée en utilisant les mêmes
données que celles qui ont servi à entraîner le modèle. Pour remédier à cela,
nous avons divisé les données originales de mtcars en un jeu d’entraînement que
nous avons utilisé exclusivement pour construire le modèle de régression, et un
jeu de test servant à évaluer ce modèle.
Tout d’abord, nous calculons un nouveau modèle linéaire sur les données
d’entraînement en utilisant la fonction lm(). Ensuite, nous formons un data
frame à partir de la colonne disp de nos données de test. Nous faisons alors des
prédictions sur notre jeu de test et nous les stockons dans une nouvelle colonne
de ce jeu. Enfin, nous calculons un terme d’erreur quadratique moyenne
(RMSE). Pour ce faire, nous prenons la différence entre la sortie de notre modèle
et l’efficacité énergétique connue (mpg) que nous mettons au carré, et ce pour
chaque ligne de données. Ces carrés sont additionnés et divisés par le nombre
total d’entrées dans le jeu de données. Cela nous donne la valeur de l’erreur
standard résiduelle. Cette nouvelle valeur est différente de ce que nous avons vu
auparavant, et c’est un élément important pour comprendre les performances de
notre modèle.
Classification
Contrairement à la modélisation par régression, que vous aviez probablement
réalisée auparavant sans vous en rendre compte, la classification se rencontre
moins fréquemment dans le spectre de l’apprentissage automatique. Dans les
exercices de classification, nous allons prédire des valeurs discrètes au lieu de
valeurs continues, comme des nombres.

Régression logistique
Contrairement à la régression, vous voulez parfois voir si un certain point de
donnée est de nature catégorielle plutôt que numérique. Dans ce que nous avons
étudié plus haut, on nous fournissait une entrée numérique et nous calculions une
sortie également numérique à l’aide d’une simple formule de régression. La
Figure 2.2 présente le même jeu de données mtcars pour expliquer visuellement
la différence :

plot(x = mtcars$mpg, y = mtcars$am, xlab = "Efficacité


énergétique
(Miles per Gallon)", ylab = "Type de Transmission (0
= Automatique,
1 = Manuelle)")
Figure 2.2 : Cette représentation graphique du type de transmission du véhicule en fonction du rendement
énergétique est très différente du tracé de l’efficacité selon la cylindrée du moteur.

Les données sont très différentes de ce que nous avons vu plus tôt. Dans le jeu de
données mtcars, chaque voiture reçoit une étiquette dans la colonne am pour
spécifier son type de transmission. Une valeur de 0 indique une boîte
automatique, et une valeur de 1 une boîte manuelle. Essayer d’ajuster un modèle
de régression linéaire à ces données ne fonctionnerait pas, car nous ne pouvons
évidemment pas avoir la moitié d’une valeur de transmission ! Nous devons
plutôt nous appuyer sur un modèle de régression logistique pour aider à classifier
le fait que de nouvelles données d’efficacité énergétique seraient associées aux
transmissions automatiques ou bien manuelles.
Cette fois, nous devons répondre à une question légèrement différente : comment
l’efficacité énergétique est-elle liée au type de transmission d’une voiture ? Nous
ne pouvons malheureusement pas nous fier ici à la procédure de modélisation de
régression. Essayer d’ajuster une droite de régression aux données donnerait des
résultats très trompeurs. Nous devons plutôt utiliser un algorithme de
classification. Ici, nous ferons appel à un algorithme de régression logistique.
La régression logistique est différente de la régression linéaire en ce sens que
nous obtenons des sorties discrètes au lieu de valeurs continues. Auparavant,
nous pouvions obtenir n’importe quel nombre grâce à notre modèle de
régression, mais, avec notre modèle logistique, nous devrions obtenir un résultat
binaire selon le type de transmission : celle-ci est soit automatique, soit
manuelle. Ici, l’approche est également différente. Tout d’abord, vous devez
charger la bibliothèque caTools :

library(caTools)

Cette bibliothèque contient de nombreuses fonctions, mais celle qui nous


intéresse concerne la régression logistique : LogitBoost. Tout d’abord, vous
devez fournir au modèle l’étiquette à utiliser pour la prédiction ainsi que les
données à sélectionner pour l’entraînement :

Label.train = train[, 9]
Data.train = train[, -9]

Vous pouvez lire la syntaxe train[, -9] comme suit : « Les données que nous
voulons sont prises dans le jeu mtcars que nous avons divisé pour former le jeu
d’entraînement, à l’exception de la colonne 9 ». Il se trouve que c’est la colonne
que nous avons utilisée tout à l’heure. Il s’agit d’une façon plus compacte de
sous-échantillonner les données au lieu d’énumérer chaque colonne
individuellement pour spécifier l’entrée :

model = LogitBoost(Data.train, Label.train)


Data.test = test
Lab = predict(model, Data.test, type = "raw")
data.frame(row.names(test), test$mpg, test$am, Lab)

## row.names.test. test.mpg test.am X0


X1
## 1 Datsun 710 22.8 1 0.9820138
0.0179862100
## 2 Merc 450SE 16.4 0 0.9996646
0.0003353501
## 3 Cadillac Fleetwood 10.4 0 0.9996646
0.0003353501
## 4 Chrysler Imperial 14.7 0 0.9996646
0.0003353501
## 5 Fiat 128 32.4 1 0.8807971
0.1192029220
## 6 Toyota Corolla 33.9 1 0.8807971
0.1192029220
## 7 Toyota Corona 21.5 0 0.9820138
0.0179862100

Dans les étapes précédentes, nous avons d’abord défini l’étiquette et les données
en choisissant les colonnes qui représentaient chacune d’elles. Nous les avons
obtenues à partir des données d’entraînement que nous avons divisées plus tôt.
Nous les avons ensuite passées à la fonction LogitBoost, et nous avons fait une
prédiction similaire à ce que nous avions effectué pour la régression linéaire.
Cependant, la sortie est légèrement différente. Ici, nous avons un rendement du
moteur exprimé en miles par gallon (mpg) et une valeur connue indiquant si la
voiture a une transmission automatique (1) ou non (0).
Nous avons ensuite deux colonnes, X0 et X1, qui sont des probabilités émises
par le modèle selon que la transmission est automatique (X0) ou manuelle (X1).
Les moyens d’ajuster ce modèle pour qu’il soit plus précis pourraient inclure la
collecte de davantage de données dans le jeu d’entraînement, ou le réglage des
options disponibles dans la fonction LogitBoost elle-même.

Méthodes de clustering supervisé


Le clustering (ou partitionnement), c’est lorsque vous disposez d’un jeu de
données et que vous voulez définir des classes en fonction de la manière dont ces
données sont regroupées. Parfois, ces groupements de données ne sont pas
immédiatement évidents, et un algorithme de clustering peut vous aider à trouver
des modèles là où ils pourraient autrement être difficiles à voir explicitement. Le
clustering est un bon exemple d’un écosystème d’algorithmes qui peuvent être
utilisés aussi bien dans un cas supervisé que non supervisé. C’est l’une des
formes de classification les plus populaires, et l’un des modèles de clustering les
plus courants est l’algorithme kmeans (k-moyennes).
Il est possible que la structuration du jeu de données iris varie selon votre installation de
R. Pour vérifier les noms exacts des colonnes, tapez simplement summary(iris) dans la
console.

Examinons le jeu de données iris en examinant le tracé de la largeur des pétales


en fonction de leur longueur (voir la Figure 2.3) :

plot(x = iris$petallength, y = iris$petalwidth, xlab =


"Longueur Pétale",
ylab = "Largeur Pétale")

Figure 2.3 : Un tracé de la largeur des pétales en fonction de leur longueur à partir du jeu de données iris
qui est également préinstallé avec R.

Comment essayer de trouver trois groupes distincts dans lesquels classifier ce


jeu de données ? Le cerveau humain est remarquablement bon lorsqu’il s’agit de
trouver des motifs et des structures, de sorte que le regroupement des données,
tel qu’il est visible dans le coin inférieur gauche de la Figure 2.3, se démarque
comme formant un groupe évident. Mais qu’en est-il du reste ? Comment
procéder pour diviser les données de la partie supérieure droite du graphique en
deux groupes supplémentaires ? L’algorithme de clustering kmeans() va nous
aider à répondre à cette question.
Cet algorithme fonctionne en plaçant d’abord un certain nombre de points de test
aléatoires dans nos données – dans ce cas, deux. Chacun de nos points de
données réels est mesuré comme distance par rapport à ces points de test, puis
ces derniers sont déplacés de manière à minimiser cette distance. C’est ce
qu’illustre la Figure 2.4 :

data = data.frame(iris$petallength, iris$petalwidth)

iris.kmeans <- kmeans(data, 2)


plot(x = iris$petallength, y = iris$petalwidth, pch =
iris.kmeans$cluster,
xlab = "Longueur Pétale", ylab = "Largeur Pétale")
points(iris.kmeans$centers, pch = 8, cex = 2)

Figure 2.4 : Les mêmes données que sur la Figure 2.3, mais après application de l’algorithme de
clustering.

Sur la Figure 2.4, nous pouvons voir comment l’algorithme fonctionne en


divisant les données en deux grands groupes. Dans la partie inférieure gauche,
nous voyons un premier groupe, représenté par les petits triangles, et dans la
partie supérieure droite nous trouvons un second groupe, représenté ici avec des
points de données circulaires. Nous voyons également deux grands astérisques
qui marquent les endroits où les centres des clusters ont finalement arrêté
l’itération.
Tout point que nous ajoutons ensuite aux données est marqué comme se trouvant
dans un certain cluster s’il est plus proche de l’un que de l’autre. Les points en
bas à gauche sont bien distincts des autres, mais il y a par contre un point de
donnée aberrant. Utilisons un cluster supplémentaire pour aider à procurer un
peu plus de sens aux données (voir la Figure 2.5) :
iris.kmeans3 <- kmeans(data, 3)

plot(x = iris$petallength, y = iris$petalwidth, pch =


iris.kmeans3$cluster,
xlab = "Longueur Pétale", ylab = "Largeur Pétale")

points(iris.kmeans3$centers, pch = 8, cex = 2)

Figure 2.5 : En ajoutant un troisième cluster, nous pouvons voir plus de groupes à l’aide desquels
classifier notre jeu de données.

Maintenant, vous pouvez voir que le plus grand groupe de données a été divisé
en deux nouveaux groupes qui semblent de taille à peu près égale. Il y a donc
trois clusters au total, avec trois centres différents pour les données. Vous
pourriez continuer en ajoutant de plus en plus de centres de clusters aux données,
mais vous perdriez ainsi des informations précieuses.
Si chaque point de donnée du jeu était son propre cluster, il serait dénué de sens
pour ce qui est de la classification. C’est là que vous devez faire appel à votre
intuition et/ou à votre instinct pour déterminer le niveau d’ajustement qui
convient en fonction des données. Trop peu de clusters et les données sont
insuffisantes : il n’y a pas de bonne façon de déterminer une structure. Trop de
clusters et vous avez le problème inverse : il y a beaucoup trop de structures
pour trouver facilement un sens aux données.
En poursuivant notre analyse de l’apprentissage supervisé, examinons la
Figure 2.6 et comparons ce résultat à la réponse réelle pour voir si notre
prédiction est vraiment bonne :

par(mfrow = c(1, 2))

plot(x = iris$petallength, y = iris$petalwidth, pch =


iris.kmeans3$cluster,
xlab = "Longueur Pétale", ylab = "Largeur Pétale", main
= "Sortie du Modèle")

plot(x = iris$petallength, y = iris$petalwidth,


pch = as.integer(iris$class),
xlab = " Longueur Pétale", ylab = " Largeur Pétale",
main = "Données
Réelles")

Figure 2.6 : Puisque nous disposons de données sur les espèces, nous pouvons comparer les résultats de
notre modèle, à gauche, avec nos données réelles à droite.

La Figure 2.6 illustre la manière dont l’algorithme kmeans avec trois clusters
fonctionne par rapport aux étiquettes des espèces réelles dans les données. Il
semble que la correspondance soit assez bonne. Nous pouvons voir les mêmes
données représentées sous forme de tableau, ce qu’on appelle une matrice de
confusion :

table(iris.kmeans3$cluster, iris$class)

##
## Iris-setosa Iris-versicolor Iris-virginica
## 1 0 48 6
## 2 50 0 0
## 3 0 2 44

Dans cette matrice de confusion, les lignes correspondent aux clusters de sortie,
les valeurs réelles des données formant les colonnes. Pour le cluster l, il y
a 48 fleurs versicolor et six virginica. Le groupe 2 n’a que des iris setosa, et le
groupe 3 a deux fleurs versicolor et 44 virginica. Si l’algorithme était parfait
à 100 %, on s’attendrait à ce que chaque colonne ait toutes ses données dans
l’une des trois lignes de clusters, mais ce n’est pas un mauvais résultat pour un
exemple aussi superficiel. Il montre qu’il n’y a que six prédictions qui étaient
hors du champ dans le cluster l, et deux dans le cluster 3.
Méthodes mixtes
Jusqu’à présent, nous avons discuté de la régression, qui prend des données
numériques continues et produit ensuite des données numériques également
continues, et de la classification, qui prend des données numériques continues et
produit ensuite des données discrètes, ou vice versa. Il y a beaucoup
d’algorithmes d’apprentissage automatique dans R, et certains sont entièrement
axés sur la régression, alors que d’autres sont entièrement dédiés à la
classification. Mais il existe une troisième classe qui peut utiliser les deux
précédentes. Certaines de ces méthodes sont capables d’utiliser la régression
pour aider à produire un schéma de classification, ou bien les données peuvent
d’abord être prises en tant qu’étiquettes et utilisées pour contraindre des modèles
de régression.

Modèles arborescents
Dans ce qui précède, nous avons vu un exemple de régression linéaire et un
exemple de régression logistique. Une partie de l’univers des modèles
d’apprentissage automatique comprend des méthodes dites arborescentes. En
termes simples, une arborescence est une structure qui a des nœuds et des arêtes.
Pour chaque nœud d’un arbre de décision, nous pouvons avoir une valeur en
fonction de laquelle nous effectuons une division afin d’obtenir des informations
intéressantes sur les données. Il est préférable d’expliquer cela visuellement en
observant la Figure 2.7 :

library(party)
tree <- ctree(mpg ~ ., data = mtcars)
plot(tree)

La Figure 2.7 montre le tracé d’un arbre d’inférence conditionnelle. Nous


traçons l’efficacité énergétique du moteur (mpg), mais nous utilisons toutes les
caractéristiques du jeu de données pour construire le modèle au lieu d’une seule,
d’où l’appel mpg ~. dans la fonction ctree(). Le résultat est une distribution (sous
la forme d’un diagramme en forme de « boîte à moustache ») de l’efficacité
énergétique en fonction des principales caractéristiques qui l’influencent. La
fonction ctree() fait appel à certaines méthodes pour comprendre ces relations.
De cette manière, vous ne vous retrouvez pas avec un tas de branches dans
l’arbre qui ne feraient rien d’autre qu’obstruer la vue. Dans ce cas, les
caractéristiques les plus importantes pour mpg sont disp (la cylindrée du moteur)
et wt (le poids de la voiture). Ce graphique se lit de haut en bas.
Au nœud 1, il y a un partage entre les voitures qui pèsent moins de 2,32 tonnes
et celles qui pèsent plus. Pour les voitures les plus lourdes, nous avons une
nouvelle division à partir de la cylindrée du moteur. Pour des cylindrées
inférieures à 258 pouces cubiques, nous accédons au nœud 4. Pour les autres,
nous passons au nœud 5. Notez que, pour chaque caractéristique, il y a une p-
valeur qui détermine sa pertinence statistique. Plus cette p-valeur est proche
de 0.05 ou plus, et moins elle est utile ou pertinente. Dans ce cas, une p-valeur
de presque exactement 0 est très bonne. De même, vous pouvez voir combien de
points de données composent chaque classe en bas de l’arbre.

Figure 2.7 : Un exemple d’arbre de décision simple appliqué au jeu de données mtcars.

Considérons une voiture qui a un poids de quatre tonnes et un petit moteur


de 100 pouces cubiques. Au nœud 1, nous suivons le chemin de droite jusqu’au
nœud 3 (puisque le poids est supérieur à 2,32 tonnes) puis nous allons à gauche
jusqu’au nœud 4 sur la base des données imaginaires que nous venons de choisir.
Nous devrions donc nous attendre à ce que le rendement énergétique de cette
voiture se situe entre 13 et 25 miles par gallon US.
Essayons maintenant de voir ce qui se passe si vous essayez d’utiliser cette
nouvelle structure de données pour la prédiction. La première chose qui devrait
apparaître, c’est que vous regardez tout le jeu de données au lieu des seules
données d’entraînement. La Figure 2.8 montre la structure arborescente pour les
données d’entraînement :

tree.train <- ctree(mpg ~ ., data = train)


plot(tree.train)

Figure 2.8 : En prenant les mêmes données et en les divisant en un jeu d’entraînement, vous simplifiez
quelque peu l’image. Cependant, la méthodologie reste la même pour les tests.

En regardant uniquement les données d’entraînement, vous obtenez une image


légèrement différente dans la mesure où l’arbre ne dépend que du poids de la
voiture. Dans l’exemple suivant, il n’y a que deux classes au lieu de l’arbre
précédent :

test$mpg.tree <- predict(tree.train, test)


test$class <- predict(tree.train, test, type = "node")
data.frame(row.names(test), test$mpg, test$mpg.tree,
test$class)

## row.names.test. test.mpg mpg test.class


## 1 Datsun 710 22.8 23.46667 2
## 2 Merc 450SE 16.4 16.09231 3
## 3 Cadillac Fleetwood 10.4 16.09231 3
## 4 Chrysler Imperial 14.7 16.09231 3
## 5 Fiat 128 32.4 23.46667 2
## 6 Toyota Corolla 33.9 23.46667 2
## 7 Toyota Corona 21.5 23.46667 2

Ce morceau de code réalise à la fois une régression et un test de classification en


simplement deux lignes. Tout d’abord, il part de la fonction familière predict() et
l’applique au jeu de données de test. Le résultat est alors stocké sous forme de
colonne dans ces données de test. Ensuite, il effectue la même procédure, mais
en ajoutant cette fois l’option type="node" à la fonction predict() pour récupérer
une classe. Il place enfin tout cela dans un seul data frame.
Ce que vous pouvez constater dans le résultat final, c’est qu’il n’y a pas besoin
d’un gros travail pour que certains algorithmes fournissent à la fois une sortie
numérique continue (une régression) et une sortie de classe discrète (une
classification) à partir des mêmes données d’entrée.

Forêts aléatoires
Les forêts aléatoires sont un sujet complexe que nous pouvons aborder au mieux
en utilisant un exemple à propos de films. Supposons que vous et un ami jouez à
un jeu dans lequel votre ami vous pose une série de questions pour déterminer si
vous aimeriez un certain film.
En suivant la logique de l’arbre de décision décrite plus haut, vous pourriez
effectuer une division en partant de critères comme le réalisateur, la durée du
film, l’actrice principale, etc. On pourrait donc commencer par : « Le film est-il
une comédie ? », suivi de « Est-ce que Cate Blanchett y tient la vedette ? », puis
« Est-ce que le film dure plus de deux heures ? ». Comme nous l’avons déjà
montré, c’est la base du fonctionnement des arbres de décision.
Votre ami peut être en mesure de trouver un film que vous aimez en fonction de
ces critères, mais ce n’est qu’une estimation qu’il fait à partir de vos réponses.
Vous voudriez maintenant refaire l’expérience avec un groupe d’amis.
Supposons donc que vous reprenez le jeu de questions avec plusieurs personnes,
et que celles-ci votent selon ce qu’elles pensent de votre intérêt pour un film. En
élargissant ainsi votre panel, vous construisez un classifieur qui est formé d’un
ensemble d’arbres, autrement dit une forêt.
Vous ne voulez pas que vos ami(e)s arrivent à la même réponse en posant les
mêmes questions. Mais vous pouvez obtenir des indications plus larges avec des
questions légèrement différentes à chaque fois. Par exemple, vous dites à
Amanda que vous avez vu The Dark Knight huit fois au cinéma, mais peut-être
qu’il y avait des raisons particulières à cela (vu avec différents amis, horaires,
etc.), et donc ce nombre de vues pourrait être gonflé. Les amis que vous
interrogez devraient peut-être exclure cet exemple.
Vous dites à Amanda que vous avez pleuré pendant le film Armageddon, mais
seulement une fois en coupant un oignon, donc le poids relatif de ce film devrait
sans doute être minimisé. Au lieu de travailler avec le même jeu de données,
vous le faites légèrement varier. Vous ne changez pas les résultats finaux quant
au fait d’aimer ou non un film, mais vous « bricolez » quelque peu les décisions
qui ont mené au résultat. C’est la création d’une version dite bootstrapped de vos
données de départ (le terme bootstrap fait référence à un tirage d’échantillon
avec remise).
Supposons donc que Robert suggère The Rock parce qu’il pense que vous aimez
les films de Jerry Bruckheimer (plus que vous ne l’aimez vraiment en fait),
tandis que Max suggère Kagemusha, alors que Will pense que vous n’aimerez
aucun de ses résultats et ne recommandera donc rien. Ces résultats forment la
« forêt de bootstrap agrégée » des préférences cinématographiques. Vos amis
sont maintenant devenus une forêt aléatoire.
Les forêts aléatoires ne sont pas aussi facilement descriptibles sous la forme de
modèle qu’une simple équation y = mx + b ou qu’un arbre ne comportant que
quelques nœuds. Vous pouvez réaliser l’entraînement et les tests habituels sur
des données continues et discrètes comme vous l’avez vu avec la méthode
ctree(), mais, pour illustrer la différence, exécutez le code qui suit (voir la
Figure 2.9) :

library(randomForest)

mtcars.rf <- randomForest(mpg ~ ., data = mtcars, ntree =


1000,
keep.forest = FALSE, importance = FALSE)

plot(mtcars.rf, log = "y")


Figure 2.9 : Les algorithmes de forêts aléatoires sont beaucoup plus difficiles à illustrer par une
visualisation. Cependant, nous pouvons facilement montrer comment l’erreur évolue avec le nombre
d’arbres que nous introduisons dans le modèle.

La Figure 2.9 montre la contrainte d’erreur dans un algorithme de forêt aléatoire


utilisant 1 000 arbres. C’est comme si vous aviez 1 000 amis jouant aux
devinettes pour obtenir des recommandations. Vous pouvez voir que l’erreur
diminue avec le nombre d’arbres que vous utilisez, et qu’elle est minimale aux
alentours de n=500 arbres.

Réseaux de neurones
Un réseau de neurones, comme son nom l’indique, tire sa forme informatique
selon une analogie avec la manière dont les neurones fonctionnent dans un
système biologique. En substance, pour une liste donnée d’entrées, un réseau de
neurones effectue un certain nombre d’étapes de traitement avant de renvoyer
une sortie. La complexité des réseaux de neurones réside dans le nombre
d’étapes de traitement, et dans la complexité de chacune de ces étapes.
Un exemple très simple de la façon dont un réseau de neurones peut fonctionner
concerne l’utilisation de portes logiques. Nous employons souvent des fonctions
logiques en programmation, mais, juste à titre de rafraîchissement de mémoire,
rappelons qu’une fonction ET (AND) n’est vraie (TRUE) que si les deux entrées
sont vraies. Si l’une est fausse (FALSE), ou les deux, le résultat est également
faux. Ce qui, d’une façon formelle, peut s’écrire ainsi dans R :

TRUE & TRUE

## [1] TRUE

TRUE & FALSE

## [1] FALSE

FALSE & FALSE

## [1] FALSE

Nous pouvons définir un réseau de neurones simple comme étant un réseau qui
prend deux entrées, calcule la fonction logique ET/AND et nous renvoie un
résultat. Cela peut être représenté sous forme graphique avec des couches et des
nœuds. Les couches sont des sections verticales dans le graphe, et les nœuds sont
les points de calcul à l’intérieur de chaque couche. Les mathématiques exigent
ici l’utilisation d’une variable dite de biais, qui est juste une constante que nous
ajoutons à l’équation à des fins de calcul, et qui est représentée comme étant son
propre nœud, typiquement au-dessus de chaque couche dans le réseau de
neurones.
Dans le cas de la fonction ET/AND, nous utiliserons des valeurs numériques
passées dans une fonction de classification afin de donner une valeur de 1 pour
TRUE et de 0 pour FALSE. Nous pouvons pour cela utiliser la fonction
sigmoïde :

Ainsi, pour les valeurs négatives de x qui sont inférieures à -5, la fonction est
fondamentalement nulle (0). Pour les valeurs positives de x supérieures à 5, la
fonction est fondamentalement 1. Si nous avions un ensemble prédéfini de poids
pour chaque nœud du réseau de neurones, nous pourrions avoir une image qui
ressemble à la Figure 2.10.
Figure 2.10 : Exemple de réseau de neurones représenté dans un diagramme lu de gauche à droite.

Nous commençons par les entrées X1, X2 et le nœud de biais qui est juste une
constante additive. Nous calculons tout cela au niveau du cercle vide, qui est
donc un nœud de calcul. Ce résultat est transmis à une fonction d’activation, qui
est presque toujours une fonction sigmoïde. Enfin, la sortie de la fonction
sigmoïde est le résultat de notre réseau de neurones ! Cool, n’est-ce pas ?
Passons lentement en revue cette figure. Pour calculer le résultat final d’une
porte ET/AND (le f(x) sur le côté droit de la Figure 2.10), nous devons partir des
entrées pour X1 et X2. Nous définissons TRUE comme valant 1 et FALSE
comme étant la valeur 0. La dernière entrée dont nous disposons est la variable
de biais, qui vaut 1 dans ce cas simple. Lorsque le réseau est entraîné, nous
trouvons des poids qui sont associés à chaque entrée. Nous construisons ensuite
une équation à l’aide de ces poids, puis nous trouvons le résultat de cette
équation. Nous passons alors ce résultat à travers une fonction sigmoïde (le
cercle vide) et nous obtenons la réponse de l’autre côté.
Cela peut sembler un peu lourd au début, mais nous pouvons l’expliquer
mathématiquement d’une manière plutôt simple. Les poids que nous avons
sont : -20 + 15 * X1 + 17 * X2. Si X1 est TRUE, il vaut l, ou 0 dans le cas
contraire. On résout alors l’équation et on fait passer la valeur finale à travers la
fonction sigmoïde. Nous répétons ce processus pour toutes les combinaisons de
nos variables d’entrée :
x1 = 1, x2 = 1

h(x) = f (−20 + 15 + 17)


h(x) = f (12) ≈ 1

x1 = 1, x2 = 0

h(x) = f (−20 + 15)


h(x) = f (−5) ≈ 0

x1 = 0, x2 = 1

h(x) = f (−20 + 17)


h(x) = f (−3) ≈ 0

x1 = 0, x2 = 0

h(x) = f (−20) ≈ 0

Pour résumer, nous avons commencé avec une seule couche de variables ayant
un certain poids prédéfini qui leur est lié. Nous avons passé cela dans une
couche de traitement, dans ce cas une fonction sigmoïde, et nous avons obtenu
un résultat. Au niveau le plus élémentaire, il s’agit d’un réseau de neurones.
Cependant, ces réseaux peuvent devenir beaucoup plus compliqués s’il y a
plusieurs couches ou étapes de traitement, ou bien encore plus de variables à
calculer. Par exemple, si nous voulions transférer le résultat de notre fonction
ET/AND dans une fonction OU/OR et ensuite dans une fonction XOR, le réseau
de neurones deviendrait plutôt lourd à décrire visuellement.
Il y a un certain nombre d’aspects à connaître dans les réseaux de neurones :
La couche d’entrée
Il s’agit d’une couche qui prend en compte un certain nombre de caractéristiques, y compris un nœud
de biais, celui-ci n’étant souvent qu’un paramètre de décalage.

La couche cachée, ou couche « de calcul »


C’est la couche qui calcule une certaine fonction pour chaque caractéristique. Le nombre de nœuds
dans cette couche cachée dépend du calcul à effectuer. Parfois, il peut n’y avoir qu’un seul nœud.
D’autres fois, le panorama peut être nettement plus complexe avec de multiples couches cachées.

La couche de sortie
Il s’agit d’un nœud de traitement final, qui peut être une unique fonction.

Cet exemple de code utilise à nouveau le jeu de données iris qui est intégré avec
R :
> set.seed(123)
> library(nnet)
> iris.nn <- nnet(class ~ ., data = iris, size = 2)

## # weights: 19
## initial value 209.019347
## iter 10 value 101.040145
## iter 20 value 13.558376
## iter 30 value 6.256923
## iter 40 value 5.976303
## iter 50 value 5.973961
## iter 60 value 5.967246
## iter 70 value 5.964365
## iter 80 value 5.961549
## iter 90 value 5.960291
## iter 100 value 5.958638
## final value 5.958638
## stopped after 100 iterations

Ce code utilise la fonction nnet() avec l’opérateur ~ que nous avons utilisé dans
nos exemples précédents. L’option size=2 nous indique que nous utilisons deux
couches cachées pour le calcul, ce qui doit être explicitement spécifié. Les
résultats que nous observons sont des itérations du réseau.
Une fois que le réseau de neurones a fini par converger, nous pouvons l’utiliser
pour la prédiction :

table(iris$class, predict(iris.nn, iris, type = "class"))

##
## Iris-setosa Iris-versicolor Iris-
virginica
## Iris-setosa 50 0
0
## Iris-versicolor 0 49
1
## Iris-virginica 0 1
49

Les résultats dans la matrice de confusion sont les références aux espèces d’iris
en haut, et les variétés d’iris prédites en lisant les colonnes du haut et vers le bas.
Ainsi, nous voyons que le réseau de neurones a parfaitement réussi à classifier
les données pour l’espèce setosa, mais qu’il a manqué une classification pour les
espèces versicolor et virginica, respectivement. Un modèle parfait
d’apprentissage automatique aurait des zéros pour tous les éléments hors
diagonale, mais ce résultat est plutôt bon pour un exemple proposé à titre
d’illustration.

Séparateur à vaste marge (SVM)


Le sigle SVM signifie en fait Support Vector Machine, soit machine à vecteurs de support,
mais que l’on traduit généralement par séparateur à vaste marge, ce qui permet de
conserver les initiales SVM.

Les séparateurs à vaste marge (les SVM) sont un autre algorithme que vous
pouvez utiliser à la fois pour la régression et la classification. Souvent, il est
introduit en tant que corollaire plus simple ou plus rapide à un réseau de
neurones. Les SVM fonctionnent d’une manière qui ressemble à bien des égards
à la régression logistique. Il y a davantage de complexités statistiques autour des
SVM (nous y reviendrons plus en détail dans le Chapitre 7), mais l’idée est que
nous prenons des données et que nous essayons de trouver un plan ou une droite
séparant ces données en différentes classes, comme l’illustre la Figure 2.11.
Supposons que vous ayez n caractéristiques dans vos données, et m observations,
ou lignes. Si n est beaucoup plus grand que m (par exemple, n = 1000, m = 10),
vous voudriez utiliser une régression logistique. Dans le cas contraire (par
exemple, n = 10, m = 1000), vous préférerez peut-être utiliser à la place un
SVM.
D’un autre côté, vous pouvez faire appel à un réseau de neurones pour l’un ou
l’autre cas, mais il peut être considérablement plus lent à entraîner qu’un de ces
algorithmes spécifiques.
Figure 2.11 : Deux classes de données séparées par une droite (ou un plan), avec des vecteurs qui
décrivent une marge ou une zone tampon entre les points qui sont séparés et la droite ou le plan qui les
scinde.

Vous pouvez opérer une classification SVM d’une manière très similaire à la
classification des réseaux de neurones, telle que nous l’avons vu précédemment :

library(e1071)
iris.svm <- svm(class ~ ., data = iris)
table(iris$class, predict(iris.svm, iris, type =
"class"))

##
## Iris-setosa Iris-versicolor Iris-
virginica
## Iris-setosa 50 0
0
## Iris-versicolor 0 48
2
## Iris-virginica 0 2
48
Les résultats obtenus pour la classification SVM semblent être très similaires à
ceux produits par la fonction nnet(). La seule différence ici est que le nombre
prédit d’espèces versicolor et virginica diffère d’une unité par rapport à notre
classificateur nnet().
Plus haut, nous avons présenté une vue de base d’un type particulier de réseau de
neurones. Bien que l’idée sous-jacente derrière les SVM et les réseaux de
neurones puisse être différente en surface, ces deux algorithmes rentrent en
concurrence l’un avec l’autre relativement fréquemment pour savoir qui
l’emporte. L’une des critiques à propos des réseaux de neurones est qu’ils
peuvent être coûteux à une certaine l’échelle, ou lents selon la complexité du
calcul. Les SVM peuvent être plus rapides dans certains cas. D’un autre côté, les
réseaux de neurones profonds peuvent comporter plus de fonctions
« intelligentes » par rapport à l’architecture SVM qui reste plus simple. Les
réseaux de neurones sont capables de gérer plusieurs entrées, alors que les SVM
ne peuvent traiter qu’une seule entrée à la fois.
Apprentissage non supervisé
Jusqu’à présent, avec les algorithmes d’apprentissage supervisé, nous avons pris
un jeu de données, nous avons décomposé celui-ci en un jeu d’entraînement et
un jeu de test, nous avons entraîné le modèle, puis évalué ses performances à
l’aide des données de test. Les algorithmes d’apprentissage non supervisés
adoptent une approche différente dans la mesure où ils tentent de définir la
structure globale des données. En principe, ils n’utilisent pas de jeu de test pour
évaluer les performances du modèle.
Généralement, la plupart des modèles d’apprentissage automatique que vous
rencontrerez relèveront de l’apprentissage supervisé. Vous construisez un
modèle, vous entraînez et testez les données, puis vous comparez les résultats à
certains paramètres connus. L’apprentissage non supervisé n’a pas de valeur
« réponse » par rapport à laquelle nous pourrions effectuer une comparaison
pour évaluer le modèle. L’évaluation et la notation des modèles se font à cet
égard d’une manière légèrement différente. Un exemple est représenté par ce que
l’on appelle la fouille de texte (text mining). Ainsi, un système d’apprentissage
non supervisé modélisé à partir du texte de tous les écrits d’Abraham Lincoln
pourrait être employé pour essayer de construire une intelligence artificielle (IA)
capable de rédiger des documents comme si Lincoln en avait été l’auteur, en se
basant sur des métriques telles que la fréquence des mots ou encore la proximité
de certains termes. Implicitement, il n’y a pas de « bonne » réponse immédiate
par rapport à laquelle vous seriez à même d’évaluer votre robot Abraham
Lincoln. Au lieu de cela, vous devriez attribuer une note à chaque cas selon le
type de sens contextuel que le modèle générerait.
La forme la plus courante d’apprentissage non supervisé est le clustering. Nous
l’avons déjà vu en action, mais masqué dans un exemple d’apprentissage
supervisé. Nous avons pu le faire parce que nous avions une clé de réponse à
utiliser à des fins de comparaison. Mais que faire si nous ne disposons pas de
certaines données pour lesquelles nous connaissons la réponse par avance ?
Méthodes non supervisées de clustering
Dans cette version non supervisée du clustering, vous allez prendre des données
qui ne possèdent pas d’étiquette catégorielle explicite, et vous allez essayer de
les catégoriser vous-même. Si vous générez des données aléatoires, vous ne
savez pas vraiment comment elles vont pouvoir se regrouper. Comme l’illustre la
Figure 2.12, vous pouvez exécuter ici l’algorithme de classification habituel
kmeans pour voir comment les données doivent être classifiées :

x <- rbind(matrix(rnorm(100, sd = 0.3), ncol = 2),


matrix(rnorm(100,
mean = 1, sd = 0.3), ncol = 2))

colnames(x) <- c("x", "y")

plot(x)

Figure 2.12 : Nous avons une distribution aléatoire des données que nous voulons classifier en deux
groupes distincts. Des cas comme ceux-là sont difficiles à comprendre à l’œil nu, mais des méthodes non
supervisées comme kmeans peuvent aider.
Ce que nous avons fait ici, c’est de générer un jeu aléatoire de données qui est
normalement distribué en deux groupes. Dans ce cas, il pourrait être un peu plus
difficile de voir exactement où se trouvent ces groupes, mais, heureusement,
comme l’illustre la Figure 2.13, l’algorithme kmeans peut aider à montrer quels
points appartiennent à quel groupe :

cl <- kmeans(x, 2)

plot(x, pch = cl$cluster)

Figure 2.13 : Des points de données distribués aléatoirement avec application d’étiquettes de classification
par clustering.

Cependant, du fait que le jeu de données n’a pas d’étiquette explicite déterminée
avant d’appliquer la classification kmeans, le mieux que vous puissiez faire est
d’étiqueter les futurs points de données en fonction des centres des clusters. Vous
pouvez les voir en les affichant à l’aide de la variable cl :

cl[2]

## $'centers'
## x y
## 1 -0.08893983 0.05141142
## 2 0.90465289 1.04480150
La ligne 1 indique les coordonnées x,y du premier cluster, la ligne 2 celles du
second cluster. Tout point que vous ajouterez ensuite au jeu de données sera
étiqueté selon sa proximité avec les centres des clusters.
En résumé
Dans ce chapitre, nous avons exploré une série d’algorithmes d’apprentissage
automatique avec R qui couvrent à la fois les cas supervisés et non supervisés.
Un algorithme d’apprentissage automatique est supervisé lorsqu’il y a un jeu de
test sur lequel vous pouvez évaluer les performances de l’algorithme. Pour ce
faire, vous prenez les données dont vous disposez, vous les divisez en un jeu
d’entraînement qui comprend (généralement) 80 % de l’ensemble des données,
puis vous sauvegardez le reste pour former le jeu de test. Vous entraînez
l’algorithme d’apprentissage automatique sur le premier jeu, puis vous passez le
jeu de test à travers le modèle ainsi entraîné. Vous pouvez ensuite évaluer les
performances du jeu de test par comparaison avec les valeurs connues. De cette
manière, lorsque vous obtenez de nouvelles données à évaluer, vous pouvez
connaître les limites de l’exactitude du modèle d’apprentissage automatique.
Nous avons aussi jeté un coup d’œil rapide sur la différence entre la régression
(données continues en entrée, données continues en sortie) et la classification
(données discrètes en entrée, données discrètes en sortie). Il existe de nombreux
algorithmes d’apprentissage automatique que vous pouvez utiliser pour les deux,
ce que nous explorerons de manière plus détaillée dans le chapitre consacré à
chaque type d’algorithme.
Dans le cas de l’apprentissage supervisé, nous avons couvert les algorithmes les
plus populaires et nous avons vu comment les implémenter à un niveau très
basique :

• Régression linéaire, lm(), pour définir une équation simple via laquelle
vous pouvez décrire une relation entre une sortie et un certain nombre de
caractéristiques qui lui sont attribuées.

• Régression logistique, LogitBoost(), pour déterminer un moyen de séparer


des données numériques en classes.

• Clustering k-means, kmeans(), pour développer des clusters et des données


d’étiquetage en fonction de l’évolution de ces clusters.

• Arbres d’inférence conditionnelle, ctree(), pour définir des divisions dans


les données et effectuer des régressions ou des classifications sur les
données fractionnées.

• Forêts aléatoires, randomForest(), pour une solution plus approfondie et


précise, mais moins intuitive, que les arbres d’inférence conditionnelle.

• Séparateurs à vaste marge, svm(), pour les cas où vous auriez moins de
caractéristiques que d’observations et que vous n’obtiendriez pas de bons
résultats avec une régression logistique.
Dans les chapitres suivants, nous allons nous pencher sur des modèles
spécifiques d’apprentissage automatique dans R et sur la meilleure façon de les
appliquer. Nous couvrirons les algorithmes les plus populaires utilisés pour
chaque modèle et les écueils dont il faut se méfier. Bien que nous aimions la
minutie et la précision quant aux fondements statistiques des algorithmes eux-
mêmes, nous n’aborderons que brièvement ces questions spécifiques dans
chaque chapitre afin de laisser un maximum de place aux exemples de code.
CHAPITRE 3
Statistiques, échantillonnage et
entraînement des modèles dans R
L’échantillonnage et l’apprentissage automatique vont de pair. Dans
l’apprentissage automatique, nous commençons typiquement par un grand jeu de
données que nous voulons utiliser pour prédire quelque chose. Nous divisons
généralement ces données en un jeu d’entraînement dont nous nous servons pour
construire un modèle, puis nous lançons notre modèle entièrement entraîné sur
un certain jeu de test pour voir quel est le résultat final. Dans certains cas, il peut
être très difficile d’exécuter un modèle d’apprentissage automatique sur la
totalité d’un jeu de données, alors que nous pourrions obtenir une aussi bonne
exactitude en l’exécutant sur un petit échantillon et en le testant le cas échéant.
Cette difficulté peut par exemple être due à la taille des données.
Définissons d’abord quelques termes statistiques. Une population est la
collection complète (ou l’univers) des choses que nous voulons étudier. Un
échantillon est une partie de cette population que nous sélectionnons pour
l’analyse. Ainsi, par exemple, nous pourrions commencer par un jeu complet de
données, le diviser afin de former un échantillon, et réaliser notre entraînement
sur celui-ci. Une autre façon de voir les choses, c’est de dire que certaines
données que l’on nous fournit au départ ne sont peut-être qu’un échantillon d’un
jeu beaucoup plus vaste.
Les données des sondages sont des exemples d’échantillonnages. Elles sont
généralement recueillies en posant des questions à des personnes répondant à des
critères démographiques spécifiques. Par nature, les données des sondages ne
peuvent être qu’un sous-ensemble de la population générale d’un pays (ou
disons d’un certain territoire), car ce serait un exploit remarquable que de
demander à tous les habitants d’un pays quelle est leur couleur préférée !
Si nous partons d’un pays comptant 100 millions d’habitants, et que nous
menions un sondage auprès de 30 millions de personnes, nous avons effectué un
type d’échantillonnage. Pour comprendre à fond ce qu’est la couleur préférée de
chacun dans le pays, nous devons faire une certaine extrapolation de notre
échantillon jusqu’à la population tout entière pour disposer d’une vue globale sur
cette question.
Dans le monde de la science statistique, nous avons des valeurs associées à la
population totale (donc au niveau du pays) et à des échantillons plus petits de
cette population, comme le montre la Figure 3.1.

Figure 3.1 : Symboles mathématiques utilisés pour définir des techniques d’échantillonnage statistique.

Lorsque nous parlons de valeurs liées aux termes moyenne, variance et écart
type par rapport à la population totale, nous parlons de paramètres. Lorsque
nous traitons de ces mêmes valeurs, mais spécifiques à un certain sous-ensemble
des données, nous les appelons statistiques. Nous pourrions donc examiner un
sous-ensemble particulier d’un pays et nous intéresser dans ce cas à la statistique
moyenne, en la comparant au paramètre moyen de la population totale. Par
exemple, le nombre de personnes dans un pays dont la couleur préférée est le
bleu serait le paramètre, et le nombre de personnes dans une ville particulière
dont la couleur préférée est aussi le bleu serait la statistique. Ces valeurs peuvent
différer entre population et échantillons, mais elles peuvent aussi varier d’un
échantillon à l’autre.
Biais
Le biais est ce qui se produit lorsque vous échantillonnez des données de telle
sorte que leur distribution dans les échantillons ne corresponde pas aux
distributions dans la population dont vous tirez ces données. Supposons que vous
interrogez les habitants d’un pays et que la couleur préférée de la moitié nord est
le vert et que la couleur préférée de la moitié sud est le jaune. Si vous faisiez un
sondage uniquement auprès des gens de la moitié sud du pays, vous auriez une
distribution de couleur préférée qui serait entièrement jaune, et vice versa. Votre
échantillon serait considérablement biaisé d’une façon ou d’une autre. La
variation de l’échantillon est la mesure dans laquelle une statistique de cet
échantillon (peut-être un aliment préféré par opposition à la couleur) diffère de la
population. Ces deux éléments peuvent être contrôlés en choisissant la bonne
façon d’échantillonner nos données.
Le biais et la variance dans un l’échantillonnage peuvent être représentés de
quatre façons. La Figure 3.2 montre quatre cibles en forme « d’œil-de-bœuf », le
centre de chacune d’entre elles étant la moyenne de la population. Les marques
peuvent représenter le comportement de différents sondages (c’est-à-dire de
différents échantillons) :
Faible biais, faible variance
C’est le meilleur des scénarios. Les échantillons représentent assez bien la population.

Biais élevé, faible variance


Les échantillons sont tous assez cohérents, mais ne reflètent pas vraiment la population.

Faible biais, variance élevée


La consistance des échantillons varie énormément, mais certains d’entre eux peuvent être
représentatifs de la population.

Biais élevé, variance élevée


Les échantillons sont un peu plus uniformes, mais ils ne sont probablement pas représentatifs de la
population.
Figure 3.2 : Lorsque vous prélevez des échantillons de données, vous devez être vigilant quant aux quatre
niveaux différents de biais et de variance que vous pourriez avoir.

Un échantillon aléatoire simple est un moyen de contrôler les biais lors du


prélèvement d’échantillons à partir d’une déclaration de population1. Cela
revient à sélectionner au hasard des valeurs à partir de vos données de sorte que
chaque ligne a une chance égale d’être choisie, comme l’illustre la Figure 3.3.
C’est souvent le meilleur équilibre entre simplicité et représentation de
l’ensemble de la population. L’application d’un échantillon aléatoire simple aux
mêmes données deux fois aura la possibilité de sélectionner les mêmes données,
si le tirage est vraiment aléatoire.
Figure 3.3 : Dans un échantillon aléatoire simple, vous choisissez dans la population les points de données
que vous voulez utiliser en les sélectionnant au hasard.

Une autre forme courante d’échantillonnage est appelée échantillonnage


aléatoire stratifié. C’est lorsque vous séparez les données en groupes
mutuellement exclusifs, appelés strates, puis que vous construisez un échantillon
aléatoire simple sur chaque strate, comme le montre la Figure 3.4. Ce serait
comme réaliser un sondage aléatoire dans chaque état (ou région) du pays. Cette
technique présente deux avantages par rapport à un échantillon aléatoire simple :

• Elle assure une représentation dans chaque strate.

• Elle peut être plus précise qu’un simple échantillon aléatoire s’il y a plus de
variations dans une strate que dans les autres.
Supposons que les échantillons ont été répartis géographiquement ou
spatialement. Vous pouvez alors effectuer un échantillonnage par grappes, par
exemple, lorsque vous disposez de données stratifiées par pays ou par ville.
C’est semblable à l’exécution d’un échantillon aléatoire stratifié, mais en
choisissant des strates entières au hasard au lieu de faire un simple
échantillonnage aléatoire à l’intérieur des strates, comme l’illustre la Figure 3.5.
Figure 3.4 : Un échantillon aléatoire stratifié. C’est quand vous faites un choix au hasard parmi diverses
strates de données (une strate peut être un regroupement ou une division dans les données qui sépare une
partie d’une autre – cela peut aussi être dû à une classification ou à des variables factorielles dans les
données).

Figure 3.5 : L’échantillonnage par grappes (ou clusters) consiste à prendre tous les points d’une certaine
classe ou d’une division dans les données, les classes ou les coupes elles-mêmes étant sélectionnées au
hasard.

On parle d’échantillonnage systématique (voir la Figure 3.6) lorsque vous faites


une sélection au hasard parmi les n premiers points de données, puis que vous
choisissez chaque nième point de donnée par la suite. Ce n’est pas aléatoire en soi
(mis à part pour le tout premier tirage), mais c’est une technique facile à
employer sur des bases de données.

Figure 3.6 : Avec une procédure d’échantillonnage systématique, vous choisissez au hasard un nombre, n,
puis chaque ne point de donnée dans le jeu.

N. d. T. : voir aussi, par exemple, https://bit.ly/2Kd6Bmy)

Jusqu’à présent, nous avons couvert quatre différents types d’échantillonnage :


l’échantillonnage aléatoire simple, l’échantillonnage aléatoire stratifié,
l’échantillonnage par grappes et l’échantillonnage systématique. Dans presque
tous les cas, vous utiliserez un échantillon aléatoire simple pour sa rapidité et sa
facilité de mise en œuvre. Toutefois, dans certains cas, il se peut que vous deviez
stratifier les données avant l’échantillonnage. Ou encore, si les données sont
organisées de manière à être distribuées sur des régions géographiques, vous
préférerez peut-être utiliser une méthode par grappes.
Jusqu’ici, tout cela traite de l’implémentation, mais ne dit rien sur la taille d’un
échantillon que vous devriez prélever. La réponse ici, invariablement, sera « ça
dépend ». Comme vous le verrez dans les prochaines sections, prendre 100 % de
la population comme échantillon n’est pas toujours la meilleure approche.
Toutefois, vous devez trouver un équilibre de manière à ce que votre échantillon
possède suffisamment de points de données pour être statistiquement significatif,
et bien représentatif des statistiques de population que vous examinez.
Échantillonner dans R
Il est assez facile de mettre en œuvre dans R toutes les techniques
d’échantillonnage mentionnées ci-dessus. Si nous commençons par quelques
données d’exemple, comme le jeu de données iris, nous pouvons tester certaines
de ces techniques d’échantillonnage en utilisant un peu de code R :

iris.df <- data.frame(iris)

sample.index <- sample(1:nrow(iris.df), nrow(iris) *


0.75, replace = FALSE)
head(iris[sample.index, ])

## sepallength sepalwidth petallength petalwidth


class
## 109 6.7 2.5 5.8 1.8
Iris-virginica
## 142 6.9 3.1 5.1 2.3
Iris-virginica
## 101 6.3 3.3 6.0 2.5
Iris-virginica
## 20 5.1 3.8 1.5 0.3
Iris-setosa
## 84 6.0 2.7 5.1 1.6
Iris-versicolor
## 112 6.4 2.7 5.3 1.9
Iris-virginica

Ce morceau de code obtient un échantillon aléatoire simple depuis le jeu de


données iris en générant d’abord les indices selon lesquels vous allez former un
sous-ensemble de vos données. Dans ce cas, nous avons sélectionné au hasard
cinq lignes de données sans remplacement. Le remplacement est l’option par
laquelle, si elle est activée, lorsque vous tirez au hasard une ligne dans vos
données, vous avez la possibilité de retomber à nouveau sur cette même ligne.
Par défaut, cette option est désactivée dans la fonction sample() de R, comme
c’est le cas avec la plupart des fonctions d’échantillonnage que vous voyez dans
le monde de la programmation.
Voyons comment procéder à un échantillonnage stratifié dans R. Contrairement à
l’échantillon aléatoire simple, l’échantillonnage stratifié peut être effectué sur
des caractéristiques différentes dans le jeu de données. Développons ce point en
examinant les distributions des données dans le jeu iris :

summary(iris)

## sepallength sepalwidth petallength


petalwidth
## Min. :4.300 Min. :2.000 Min. :1.000 Min.
:0.100
## 1st Qu.:5.100 1st Qu.:2.800 1st Qu.:1.600 1st
Qu.:0.300
## Median :5.800 Median :3.000 Median :4.350
Median :1.300
## Mean :5.843 Mean :3.054 Mean :3.759 Mean
:1.199
## 3rd Qu.:6.400 3rd Qu.:3.300 3rd Qu.:5.100 3rd
Qu.:1.800
## Max. :7.900 Max. :4.400 Max. :6.900 Max.
:2.500
## class
## Iris-setosa :50
## Iris-versicolor:50
## Iris-virginica :50
##

Ici, vous pouvez voir la population des données. Nous voulons obtenir un
échantillon qui a à peu près la même distribution de valeurs pour n’importe
laquelle de ces caractéristiques. Notez que certaines de ces colonnes ont des
variations plus importantes que d’autres. En l’occurrence, petallength a la plus
grande variance, suivie de sepallength. Gardez cela à l’esprit pour l’exercice
d’échantillonnage stratifié. Pour l’instant, opérons un échantillonnage aléatoire
simple sur les valeurs de sepallength :

summary(iris[sample.index, ])

## sepallength sepalwidth petallength


petalwidth
## Min. :4.300 Min. :2.200 Min. :1.000 Min.
:0.100
## 1st Qu.:5.100 1st Qu.:2.800 1st Qu.:1.600 1st
Qu.:0.300
## Median :5.700 Median :3.000 Median :4.300
Median :1.300
## Mean :5.812 Mean :3.043 Mean :3.737 Mean
:1.199
## 3rd Qu.:6.400 3rd Qu.:3.225 3rd Qu.:5.100 3rd
Qu.:1.800
## Max. :7.900 Max. :4.100 Max. :6.900 Max.
:2.500
## class
## Iris-setosa :37
## Iris-versicolor:39
## Iris-virginica :36
##

Cet exemple récupère un échantillon de 75 % des données originales, et vous


pouvez voir que les distributions sont toutes assez proches de ce que sont les
principales valeurs de la population. Essayons maintenant l’échantillonnage
stratifié. Pour cela, vous avez besoin du package fifer et de sa fonction
stratified() :

library(fifer)

## Loading required package: MASS

summary(stratified(iris, "sepallength", 0.7))

## sepallength sepalwidth petallength petalwidth


## Min. :4.300 Min. :2.000 Min. :1.100 Min.
:0.100
## 1st Qu.:5.100 1st Qu.:2.775 1st Qu.:1.500 1st
Qu.:0.300
## Median :5.800 Median :3.000 Median :4.350 Median
:1.300
## Mean :5.867 Mean :3.046 Mean :3.775 Mean
:1.187
## 3rd Qu.:6.425 3rd Qu.:3.325 3rd Qu.:5.100 3rd
Qu.:1.800
## Max. :7.900 Max. :4.400 Max. :6.900 Max.
:2.500
## class
## Iris-setosa :35
## Iris-versicolor:39
## Iris-virginica :34
##

L’échantillon stratifié a à peu près les mêmes valeurs. Nous avons effectué un
échantillonnage stratifié sur les données du jeu iris à l’aide de la fonction
stratified(), en nous concentrant spécifiquement sur les strates de sepallength.
Notez que le code demande un échantillon sur 70 % des données.
Avec l’échantillonnage stratifié, cependant, vous pouvez spécifier les strates
particulières que vous voulez échantillonner. Si vous échantillonnez sur plusieurs
strates, vous voulez généralement commencer par les caractéristiques qui varient
le moins et ensuite avancer vers le haut. Les caractéristiques ayant la plus faible
variance dans le jeu de données iris sont sepalwidth et petalwidth. Commençons
par celles-là :

summary(stratified(iris, c("sepalwidth", "petalwidth"),


0.7))

## sepallength sepalwidth petallength petalwidth


## Min. :4.30 Min. :2.000 Min. :1.100 Min. :0.10
## 1st Qu.:5.10 1st Qu.:2.800 1st Qu.:1.575 1st Qu.:0.30
## Median :5.80 Median :3.000 Median :4.250 Median :1.30
## Mean :5.86 Mean :3.055 Mean :3.791 Mean :1.22
## 3rd Qu.:6.40 3rd Qu.:3.300 3rd Qu.:5.100 3rd Qu.:1.80
## Max. :7.90 Max. :4.400 Max. :6.900 Max. :2.50
## class
## Iris-setosa :37
## Iris-versicolor:39
## Iris-virginica :40
##

Vous pouvez voir sur la sortie que l’échantillonnage stratifié avec des groupes
multiples a toujours une bonne représentation des données de la population avec
laquelle vous avez commencé (c’est-à-dire le jeu de données iris complet). Les
moyennes et les variances semblent toutes assez appropriées dans cet
échantillonnage.
Pour l’échantillonnage systématique, vous pouvez écrire une fonction simple qui
sélectionne chaque nième ligne de façon séquentielle en partant d’un numéro
d’initialisation aléatoire :

sys.sample = function(N, n) {
k = ceiling(N/n)
r = sample(1:k, 1)
sys.samp = seq(r, r + k * (n - 1), k)
}

systematic.index <- sys.sample(nrow(iris), nrow(iris) *


0.75)
summary(iris[systematic.index, ])

## sepallength sepalwidth petallength


petalwidth
## Min. :4.40 Min. :2.000 Min. :1.000 Min.
:0.100
## 1st Qu.:5.15 1st Qu.:2.800 1st Qu.:1.600 1st
Qu.:0.250
## Median :5.80 Median :3.000 Median :4.500 Median
:1.300
## Mean :5.84 Mean :3.064 Mean :3.776 Mean
:1.217
## 3rd Qu.:6.45 3rd Qu.:3.400 3rd Qu.:5.100 3rd
Qu.:1.850
## Max. :7.70 Max. :4.100 Max. :6.900 Max.
:2.500
## NA's :37 NA's :37 NA's :37 NA's
:37
## class
## Iris-setosa :25
## Iris-versicolor:25
## Iris-virginica :25

## NA's :37
##

Ce code définit la fonction d’échantillonnage systématique et l’exécute ensuite


sur les données du jeu iris. Pour cet exemple, nous l’avons exécuté en
fournissant le nombre de lignes, ce qui nous a permis d’obtenir les indices
spécifiés afin de produire le sous-ensemble, mais les résultats sont assez
similaires à ce que vous avez vu jusqu’à présent.
Entraînement et test
Lors de la construction d’un modèle prédictif, vous devez passer par des phases
de validation pour être certain que vous pouvez faire confiance à ses résultats. Si
vous construisez un modèle, vous avez besoin d’un moyen vérifiable de vous
assurer d’abord que vous obtenez quelque chose qui ressemble à la bonne
réponse, avant d’envisager de commencer à le mettre en production. Il vous faut
un procédé vous permettant de voir quelles seront les erreurs générées par le
modèle afin de pouvoir mieux l’ajuster de manière appropriée.
Par exemple, si vous voulez prédire la cotation en Bourse d’une certaine action
pour le lendemain (pour laquelle il n’est pas encore possible d’obtenir les
données du jour), vous pourriez construire un modèle entraîné sur les données
d’il y a quelques jours, et le tester sur les données de la veille. Du fait que vous
avez déjà les réponses pour les cotations d’hier, voir ce que le modèle produit et
comparer avec les valeurs réelles peut fournir un retour d’information précieux
pour juger si le modèle fonctionne.
Vous avez peut-être vu des modèles d’apprentissage automatique utilisant une
méthodologie à base d’entraînement et de test. C’est ce que vous faites lorsque
vous prenez une certaine quantité de données, que vous échantillonnez une
majorité d’entre elles dans un jeu d’entraînement et que vous conservez ce qui
reste pour former un jeu de test. Nous opérons typiquement une division dans un
rapport 70/30 des données pour obtenir nos sous-ensembles entraînement/test,
mais il n’est pas rare de voir aussi des partages 80/20.
Ce que vous réalisez de cette manière, c’est de simuler efficacement le travail du
modèle en l’exécutant d’abord sur des données que vous possédez déjà, avant de
lui faire ingérer des données complètement nouvelles. Cette méthodologie
suppose deux hypothèses majeures :

• Les données constituent une représentation fidèle des processus réels que
vous voulez modéliser (autrement dit, le sous-ensemble reflète fidèlement
la population).

• Les processus que vous voulez modéliser sont relativement stables dans le
temps, et donc un modèle construit avec les données du mois dernier
devrait refléter avec exactitude les données du prochain mois.
Si vos hypothèses sont correctes, un modèle construit sur les données
d’aujourd’hui (ou d’hier) devrait fonctionner pour toutes les données futures que
vous lui transmettez. Vous devriez cependant faire attention à la manière dont
vous partagez vos données d’entraînement et de test.
La première partie de ce chapitre détaille les différentes façons dont vous pouvez
échantillonner et « sous-ensembler » vos données de départ afin de préserver la
distribution globale des caractéristiques de celles-ci. Nous ne voulons
certainement pas nous retrouver avec un sous-ensemble qui contiendrait par
exemple toutes les fleurs d’un même type, ou toutes les réponses d’une même
ville à un sondage sur la couleur préférée des gens.

Rôles des jeux d’entraînement et de test


Lorsque vous divisez les données en jeux d’entraînement et de test, c’est le
premier que vous utilisez pour entraîner le modèle. Presque tous les algorithmes
d’apprentissage non supervisé suivent ce format. Les coefficients spécifiques
que vous obtenez comme résultats des procédures de modélisation sont
entièrement basés sur les données du jeu d’entraînement, et ne dépendent pas du
tout des données de test.
Le rôle du jeu d’entraînement est de fournir une plate-forme sur laquelle le
modèle de votre choix va s’appuyer mathématiquement afin de déterminer les
coefficients voulus, ou effectuer tout ce qu’il pourrait avoir à faire en sous-main.
Le rôle du jeu de données de test est de voir comment ce modèle se compare (en
bien ou en mal) avec les données réelles.

Pourquoi créer un jeu de test ?


Il y a deux manières de réfléchir à l’importance que revêt le fait de créer un jeu
de test à partir de vos données à des fins de modélisation. La première est juste
qu’il s’agit d’un procédé fiable pour valider ces données. Si vous avez un
modèle fonctionnant vraiment bien pour toutes les données sur lesquelles il a été
entraîné, mais qui s’est « planté » lorsque de nouvelles données lui ont été
présentées, alors ce modèle perd toute sa puissance prédictive et ne vaut pas
mieux qu’un rapport statique. Au pire, cela peut être très trompeur quant à la
façon dont vous devriez réfléchir aux valeurs à l’avenir ! Il est donc bon en soi
d’être en mesure de constater à l’avance qu’un certain modèle fonctionne mal.
Cela peut par exemple vous informer qu’il suffit d’ajuster légèrement le
paramètre X pour l’ajuster.
L’autre réflexion à se faire est que certains algorithmes d’apprentissage
automatique dépendent en fait de l’existence préalable d’un jeu de test. Par
exemple, les arbres de classification et de régression (CART) peuvent être si
souples dans leurs capacités de modélisation que, si l’arbre devient assez grand,
il est souvent possible d’obtenir des prédictions trompeuses. Vous pouvez
entraîner un modèle CART et constater que le résultat offre une exactitude
de 100 %. En réalité, ce modèle fonctionnera mal pour toutes les nouvelles
données qu’il verra par la suite. D’un point de vue statistique, tout modèle qui
vous donne une exactitude de 100 % devrait être une source d’inquiétude. Vous
pouvez utiliser le jeu de test pour évaluer la performance prédictive des arbres
afin de trouver celui qui présente l’erreur la plus faible. Ainsi, le jeu de test agit
comme un moyen non seulement de valider les données, mais aussi de
sélectionner la forme du modèle dont vous avez besoin, et ce en fonction de
l’algorithme en jeu.

Jeux d’entraînement et de test : modéliser la


régression
Pour illustrer le besoin de diviser vos données entre entraînement et test, le
mieux est de partir d’exemples de régression simples, comme ceci (voir la
Figure 3.7) :

set.seed(123)

x <- rnorm(100, 2, 1)
y = exp(x) + rnorm(5, 0, 2)

plot(x, y)

linear <- lm(y ~ x)

abline(a = coef(linear[1], b = coef(linear[2], lty = 2)))


Figure 3.7 : Données aléatoires avec un ajustement linéaire. Celui-ci est proche de certains points de
données, mais pas de tous (plus x devient grand, et plus il est probable que votre ajustement linéaire
n’approximera pas très bien les données).

La sortie suivante est le résultat du code précédent :

summary(linear)

##
## Call:
## lm(formula = y ~ x)
##
## Residuals:
## Min 1Q Median 3Q Max
## -5.6481 -3.7122 -1.9390 0.9698 29.8283
##
## Coefficients:
## Estimate Std. Error t value Pr(>¦t¦)
## (Intercept) -13.6323 1.6335 -8.345 4.63e-13 ***
## x 11.9801 0.7167 16.715 < 2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.'
0.1 ' ' 1
##
## Residual standard error: 6.51 on 98 degrees of freedom
## Multiple R-squared: 0.7403, Adjusted R-squared:
0.7377
## F-statistic: 279.4 on 1 and 98 DF, p-value: < 2.2e-16

Ce morceau de code est un territoire familier. Il prend certaines données de


simulation pour x et y, puis trace un modèle linéaire le mieux adapté possible sur
ces données. Cet exemple utilise 100 % des données de simulation comme jeu
d’entraînement et il examine les performances du modèle. Pour cette situation
particulière, une valeur R-carré multiple de 0.74 n’est pas géniale. Essayons
maintenant une version qui divise les données selon notre échantillonnage
aléatoire standard 70/30 et voyons en quoi les résultats diffèrent.
Tout d’abord, partageons les données en jeux d’entraînement et de test en
utilisant un échantillonnage aléatoire simple :

data <- data.frame(x, y)

data.samples <- sample(1:nrow(data), nrow(data) * 0.7,


replace = FALSE)

training.data <- data[data.samples, ]


test.data <- data[-data.samples, ]

Ensuite, appliquons le modèle linéaire sur les données d’entraînement :

train.linear <- lm(y ~ x, training.data)

Maintenant que vous avez un modèle entraîné, comparons ses valeurs en sortie
aux valeurs réelles. Vous pouvez le faire en utilisant dans R la fonction predict(),
qui prend l’objet train. linear et l’applique aux données que vous lui fournissez.
Puisque vos données de test sont disponibles, vous pouvez les utiliser pour
effectuer la comparaison :

train.output <- predict(train.linear, test.data)

Vous avez maintenant utilisé vos données de test, qui ont le même comportement
sous-jacent que les données d’entraînement, en les passant à travers votre
modèle afin d’obtenir des résultats. Les données de test ont une variable
dépendante, x, et une variable indépendante, y. Vous devez utiliser la variable
dépendante spécifiquement pour cette évaluation, car vous voulez comparer ce
que le modèle pense de ce que la réponse devrait être, étant donné x, par rapport
aux valeurs réelles de votre jeu de test, données par y.
Pour la régression, en particulier, vous pouvez vous y prendre de diverses
manières, selon les données et le type d’analyse d’erreur que vous souhaitez
effectuer. Dans ce cas, vous utiliserez une métrique de test appelée « l’erreur
quadratique moyenne », ou RMSE :
Selon les sources, RMSE est définie comme erreur quadratique moyenne, ou comme
erreur type, soit la racine carrée de l’erreur quadratique moyenne. La première version est
conservée ici par mesure de simplicité.

En clair, cela signifie que vous prenez les valeurs de sortie que le modèle a
fournies pour l’entrée des données d’entraînement, vous les soustrayez par les
valeurs y dont vous disposez dans les données de test, vous mettez ces valeurs au
carré, vous les divisez par le nombre total d’observations n, vous sommez toutes
les valeurs et, finalement, vous en prenez la racine carrée. Voici à quoi ressemble
le code :

RMSE.df = data.frame(predicted = train.output, actual =


test.data$y,
SE = ((train.output -
test.data$y)^2/length(train.output)))

head(RMSE.df)

## predicted actual SE
## 2 7.553671 6.383579 0.04563716
## 4 11.183322 7.233768 0.51996594
## 6 31.035159 39.640442 2.46836334
## 8 -4.938659 1.591971 1.42163749
## 9 2.041033 3.022771 0.03212698
## 11 25.108383 23.709676 0.06521277

sqrt(sum(RMSE.df$SE))

## [1] 5.90611
Considérez la valeur RMSE résultante de 5.9 comme étant le score d’erreur de
ce modèle. Pour voir jusqu’à quel point cette valeur est bonne, vous devez la
comparer à une autre valeur RMSE. Vous pouvez procéder en faisant appel à une
fonction de degré supérieur et en observant quel type de RMSE vous obtenez à
l’arrivée :

train.quadratic <- lm(y ~ x^2 + x, training.data)


quadratic.output <- predict(train.quadratic, test.data)

RMSE.quad.df = data.frame(predicted = quadratic.output,


actual = test.data$y,
SE = ((quadratic.output -
test.data$y)^2/length(train.output)))

head(RMSE.quad.df)

## predicted actual SE
## 2 7.553671 6.383579 0.04563716
## 4 11.183322 7.233768 0.51996594
## 6 31.035159 39.640442 2.46836334
## 8 -4.938659 1.591971 1.42163749
## 9 2.041033 3.022771 0.03212698
## 11 25.108383 23.709676 0.06521277

sqrt(sum(RMSE.quad.df$SE))

## [1] 5.90611

Cette sortie montre que le fait d’augmenter l’ajustement du degré polynomial


aide à diminuer l’erreur dans ce que le modèle prédit (à partir de la variable
quadratic.output) par rapport à ce que sont les valeurs réelles. Cela découle
intuitivement du fait que les données réelles que vous tracez semblent de toute
façon être bien ajustées par un polynôme quadratique.
Naturellement, la prochaine étape va consister à augmenter encore plus le degré
polynomial et voir comment cela va affecter la valeur RMSE :

train.polyn <- lm(y ~ poly(x, 4), training.data)

polyn.output <- predict(train.polyn, test.data)


RMSE.polyn.df = data.frame(predicted = polyn.output,
actual = test.data$y,
SE = ((polyn.output -
test.data$y)^2/length(train.output)))

head(RMSE.polyn.df)

## predicted actual SE
## 2 5.0313235 6.383579 0.060953156
## 4 7.0568495 7.233768 0.001043337
## 6 40.6367241 39.640442 0.033085916
## 8 0.7841328 1.591971 0.021753393
## 9 2.7506929 3.022771 0.002467546
## 11 24.2647775 23.709676 0.010271274

sqrt(sum(RMSE.polyn.df$SE))

## [1] 0.9357653

Vous pouvez voir que la valeur RMSE a augmenté par rapport à l’ajustement
quadratique. Cela suit le même schéma que celui d’un polynôme de degré
supérieur provoquant un surajustement des données.
Lorsque nous apprenons à ajuster une droite sur une série de points de données,
nous n’avons pas naturellement tendance à nous pencher sur les subtilités des
techniques d’échantillonnage, ni à diviser nos données en jeux d’entraînement et
de test. Pour une régression, nous commençons par un ajustement en ligne droite
tel que l’illustre la Figure 3.8.
Figure 3.8 : Un ajustement linéaire simple aux données sera le plus souvent « sous-ajusté » (autrement dit,
la droite telle qu’elle résulte des coefficients produits par le modèle ne correspond pas tout à fait aux
données dont vous disposez).

La Figure 3.8 contient quelques données, signalées par les croix, et une ligne
représentant l’ajustement du modèle. Vous y voyez aussi l’équation qui décrit ce
modèle, y = θ + θx, parfois écrite sous la forme y = b + mx. Deux valeurs sont
données par le processus d’ajustement du modèle, θ0 et θ1.
Sur cette image simple, nous avons un modèle qui est plutôt sous-ajusté par
rapport aux données. Cela signifie que la droite qui représente le modèle
d’apprentissage automatique (une régression linéaire simple) n’explique pas la
plupart des données. Ce modèle est trop simple. Les modèles linéaires ne
peuvent que sous-ajuster ou bien représenter les données que vous tracez. Si, par
exemple, votre modèle correspondait graphiquement à une série de points de
données disposés en ligne droite, il s’agirait d’une représentation exacte. Dans le
meilleur des cas, la régression linéaire peut s’adapter exactement aux données,
mais il est difficile pour une procédure simple comme celle-ci de surajuster les
données. De ce fait, nous voyons rarement l’utilisation de jeux d’entraînement et
de test pour évaluer ces modèles linéaires. Si vous ajoutiez un autre point de
données suivant à peu près la même formation que ce que vous voyez ici, le
modèle linéaire continuerait de s’en écarter et ne pourrait pas vraiment produire
une image précise sur le long terme.
Comparons l’ajustement linéaire avec la représentation d’un ajustement
quadratique, illustré sur la Figure 3.9. Dans ce cas, l’ajustement est meilleur que
ce que nous venons de voir. Le modèle tend à suivre la forme de la plupart des
points de données, et il est aussi devenu un peu plus complexe.
Nous avons une équation qui décrit le modèle, avec plus de sorties dérivées de
ce modèle dont il faut se préoccuper. Si vous deviez ajouter d’autres points de
données à l’image en suivant à peu près la même forme, il semble que cette
version quadratique devrait s’adapter plutôt bien pour ce qui est de l’avenir
prévisible.

Figure 3.9 : Un ajustement quadratique correspond à un modèle légèrement plus complexe, tel qu’il est
donné par les coefficients produits par ce modèle. En entraînant un modèle d’apprentissage automatique
avec un échantillon spécifique, puis en examinant la différence entre les résultats obtenus et les données de
test qui ont été sauvegardées, vous pouvez évaluer si le modèle se généralise bien à ces nouvelles données.

Enfin, avec un modèle complexe comme celui qui est illustré sur la Figure 3.10,
pratiquement tous les points de données sont ajustés avec exactitude par le
modèle sans avoir de marge de manœuvre. L’inconvénient réside alors dans le
pouvoir explicatif futur. Si nous suivons la même logique qu’auparavant et que
nous ajoutions sur l’image quelques points de données supplémentaires, le
modèle présenté ne s’ajustera pas bien, ce qui se traduira par une erreur accrue.

Figure 3.10 : Dans un scénario d’ajustement complexe, où le modèle est trop spécifique par rapport aux
données d’entraînement, les données de test qui lui sont appliquées se traduiront probablement par des
erreurs élevées lors de l’évaluation des résultats produits par le modèle versus ces nouvelles données.

Lorsque vous effectuez une validation entraînement/test sur des données


continues comme dans notre exemple de régression, vous pouvez choisir parmi
une foule de mesures statistiques, comme l’erreur quadratique moyenne. En
règle générale, cependant, vous voulez comparer les valeurs de sortie fournies
par le modèle (en vous basant sur un sous-ensemble des données utilisées pour
l’entraînement de ce modèle) avec les données que vous avez conservées à des
fins de test. Vous devriez avoir une liste de nombres pour les estimations du
modèle et une autre liste de nombres pour les valeurs réelles. Il y aura
invariablement une certaine différence entre elles, que vous pouvez ensuite
pousser jusqu’à une certaine valeur d’agrégat et comparer avec d’autres
méthodes.
Pour des données continues comme celles que nous avons testées jusqu’à
présent, il existe quelques tests statistiques différents avec lesquels vous pouvez
comparer les erreurs en sortie de vos résultats :
RMSE

MAE (erreur absolue moyenne)

RRSE (racine de l’erreur carrée relative)

RAE (erreur absolue relative)

Pour RMSE et MAE, nous regardons la « différence moyenne » entre la sortie du


modèle (yprédit) et les valeurs dont nous disposons dans notre jeu de test (yréel).
Elles sont comparées à la même échelle de notre caractéristique. Vous pouvez
vous représenter cela en vous figurant que 1 point d’erreur est une différence
de 1 entre yprédit et yréel.
Dans RRSE et RAE, nous avons une nouvelle variable appelée qui est la
valeur moyenne de la sortie de notre modèle et est simplement un nombre
scalaire. Ces statistiques divisent les valeurs de nos données prévues et réelles
par la variation de notre caractéristique de sorte que le résultat final se trouve sur
une échelle de 0 à l. Nous avons tendance à multiplier ce nombre par 100, ce qui
fait que nous obtenons quelque chose se situant dans une fourchette allant de 0 à
100, ce qui nous permet de le convertir en pourcentage. Les dénominateurs des
deux équations nous disent à quel point la caractéristique s’écarte de sa valeur
moyenne, et c’est pourquoi nous les appelons des erreurs « relatives ».

Jeux d’entraînement et de test : modéliser la


classification
Vous évaluez la performance d’un modèle de classification en partant d’une
« matrice de confusion ». Sous une forme simple, elle peut être représentée sous
la forme d’une matrice 2 x 2, dans laquelle les classes prédites en sortie du
modèle sont comparées aux classes réelles et au nombre de sorties du modèle
dans les cellules de la matrice. Cela vous informe du nombre de vrais positifs, de
vrais négatifs, de faux positifs et de faux négatifs qui en résultent. Comme pour
la régression, les statistiques de classification disposent de nombreux outils
permettant d’évaluer la performance finale du modèle. Jetons un coup d’œil à
certains d’entre eux :

iris.df <- iris

iris.df$class <- as.character(iris.df$class)

iris.df$class[iris.df$class != "Iris-setosa"] <- "autre"

iris.df$class <- as.factor(iris.df$class)


iris.samples <- sample(1:nrow(iris.df), nrow(iris.df) *
0.7,
replace = FALSE)

training.iris <- iris.df[iris.samples, ]

test.iris <- iris.df[-iris.samples, ]

library(randomForest)

iris.rf <- randomForest(class ~ ., data = training.iris)

iris.predictions <- predict(iris.rf, test.iris)

table(iris.predictions, test.iris$class)

##
## iris.predictions autre Iris-setosa
## autre 31 0
## Iris-setosa 0 14
Dans une table de vérité de classe binaire, il y a uniquement deux résultats : soit
la valeur prédite est une classe, soit elle ne l’est pas. Dans ce cas, vous vous
concentrez sur la question de savoir si le modèle a prédit une classe setosa ou
autre chose. Il y a quatre valeurs pour la matrice de confusion :
Vrais positifs
Le modèle a prédit les classes setosa et les a effectivement obtenues.

Vrais négatifs
Le modèle a prédit d’autres classes et les a effectivement obtenues.

Faux positifs
Le modèle a prédit les classes setosa, mais la bonne réponse était autre.

Faux négatifs
Le modèle a prédit les classes autre, mais la bonne réponse était setosa.

Le résultat de cette table de vérité n’est très pas intéressant, parce qu’il est trop
précis. Il n’y a pas ici de classes mal prédites. Par souci d’illustration, cependant,
supposons que nous obtenions en sortie une matrice de confusion légèrement
inexacte :

##
## autre Iris-setosa
## autre 28 3
## Iris-setosa 2 12

Cet exemple impose deux faux positifs et trois faux négatifs. Nous avons donc
maintenant 15 vrais positifs (VP), 26 vrais négatifs (VN), 2 faux positifs (FP)
et 3 faux négatifs (FN). Avec les modèles de classification comme celui-ci, nous
disposons d’un certain nombre de statistiques parmi lesquelles nous pouvons
faire un choix pour tester notre exactitude :
Sensibilité (ou rappel)

Spécificité
Précision (ou valeur prédictive positive)

Exactitude

Score F1

La plupart de ces valeurs sont utilisées comme références pour les modèles de
classification. Avec les modèles de régression, vous disposiez d’une valeur
pratique, RMSE, que vous pouviez comparer à d’autres modèles. Mais alors,
quelle serait la référence correspondante ici pour ce qui concerne l’exactitude ?
Passons en revue les options disponibles :
Sensibilité
Souvent appelée rappel, elle correspond au cas où vous avez fixé un seuil plus bas pour votre modèle
de classification. C’est ce que vous feriez si vous ne vouliez pas manquer des fleurs qui pourraient
être de type setosa.

Spécificité
Logiquement la même chose que la précision, mais pour le cas opposé, lorsque l’on prédit si une fleur
n’est pas de type setosa.

Précision
Le nombre de vrais positifs que vous avez prédit divisé par le nombre total de cas positifs prédits. Si
vous aviez un modèle ayant une sensibilité très élevée, cela reviendrait à fixer un seuil pour ce
modèle afin de dire : « Ne classer une fleur comme étant de type setosa que si nous en sommes
absolument sûrs ».

Exactitude
Nombre de cas vrais divisé par le total des cas vrais et faux.
Score F1
Moyenne pondérée des scores de précision et de rappel.

Vous pourriez être tenté de dire que votre référence en matière d’exactitude ne
devrait être que la mesure statistique de l’exactitude telle qu’elle est décrite ci-
dessus. Si vous avez un nombre presque identique de faux positifs et de faux
négatifs, ce serait une très bonne métrique à utiliser. Cependant, si les faux
positifs ou les faux négatifs sont déséquilibrés en faveur de l’un ou l’autre, vous
avez besoin d’un test statistique plus robuste pour tenir compte d’un tel
comportement.
Bien que le score F1 puisse sembler moins intuitif que l’exactitude, il est
généralement plus utile parce que ce score et l’exactitude sont à peu près
identiques lorsque les taux de faux positifs et de faux négatifs sont faibles.
Vous pouvez mieux juger de l’utilité du score F1 si vous observez quelques
modèles imaginaires avec des valeurs de précision et de rappel différentes. Vous
pourriez être tenté de prendre simplement la moyenne de la précision et du
rappel pour obtenir une métrique de performance, comme vous pouvez le voir
dans le Tableau 3.1.

Tableau 3.1 : Résultats sous forme de tableau des sorties du modèle et mesures
statistiques associées de la performance.

Précision Rappel Moyenne Score F1


0.50 0.40 0.45 0.44
0.70 0.10 0.40 0.18
0.02 1.00 0.51 0.04
0.00 0.01 0.51 0.02
1.00 1.00 1.00 1.00

Cet exemple montre quelques modèles différents illustrant la précision et le


rappel de la classification. Si vous vouliez évaluer la performance du modèle sur
la base de la moyenne de ces deux nombres, cette approche ne fonctionne pas
lorsque vous avez une précision élevée et un faible rappel, ou l’inverse.
Cependant, le score F1 compense ces bizarreries et fournit une métrique plus
fiable pour évaluer la performance de votre modèle de classification.
Validation croisée
Jusqu’à présent, nous avons parlé de la situation dans laquelle le simple fait
d’exécuter un modèle sur 100 % de vos données pourrait donner un résultat ne se
généralisant pas bien à de nouvelles données entrantes. C’est pourquoi nous en
avons conclu qu’il fallait diviser les données de départ en un jeu d’entraînement,
comprenant habituellement 70 % des données, et un jeu de test reprenant le
reste. Vous faites tourner le modèle avec les données du jeu d’entraînement, et
vous utilisez ensuite le jeu de test pour comparer les résultats du modèle avec les
réponses que vous avez en main.
Ce processus d’entraînement et de test est toutefois encore quelque peu limité.
Finalement, lorsque vous testez la sortie du modèle par rapport aux données
mises en réserve, vous voyez uniquement l’erreur pour ce regroupement exact
des données de test. En théorie, le jeu de test devrait être représentatif de la
totalité des données, mais, dans la pratique, il y a des cas où cela pourrait ne pas
être vrai. Vous devriez chercher à entraîner le modèle de telle sorte que vous
puissiez être sûr que l’erreur est effectivement représentative de tout le jeu des
données, et pas seulement de la partie spécifique que vous obtenez via des
sélections opérées au hasard pour former le jeu de test.
La validation croisée est une technique statistique qui vous permet de prendre la
totalité de votre jeu de données, de le partager en un certain nombre de petits
« morceaux » pour l’entraînement et les tests, d’évaluer l’erreur pour chaque
morceau, puis de faire la moyenne de ces erreurs finales. Cette approche se
révèle être un moyen plus précis d’évaluer si votre processus de modélisation est
susceptible de rencontrer des problèmes qui pourraient se dissimuler à l’intérieur
de diverses combinaisons des parties entraînement et test du jeu de données.
En fait, nous avons déjà réalisé une forme de validation croisée ! La séparation
simple 70/30 des données que vous avez faite plus tôt dans ce chapitre est une
technique de validation croisée de type dit « holdout » (ou validation simple). Il
existe cependant de nombreuses autres techniques statistiques de validation
croisée, et, du fait que R prend sa source dans la conception statistique, vous
pouvez modéliser de multiples types différents de validation croisée.

Validation croisée à k-plis


Par contraste avec la validation croisée holdout, une technique beaucoup plus
couramment utilisée est appelée validation croisée k-fold, ou encore à k-plis (voir
la Figure 3.11). Cela implique de prendre votre jeu de données et de le diviser en
k morceaux. Pour chacun de ces morceaux, vous partagez ensuite les données en
jeux d’entraînement et de test plus petits, puis vous évaluez l’erreur
correspondante. Une fois que vous avez toutes les erreurs pour tous les
morceaux, vous en calculez simplement la moyenne. L’avantage de cette
méthode est que vous pouvez alors juger de l’erreur dans tous les aspects de vos
données, au lieu d’en tester simplement un sous-ensemble spécifique.

Figure 3-11. La validation croisée est la pratique statistique consistant à effectuer de multiples procédures
d’entraînement et de test sur nos données. Cet exemple montre une validation croisée à 10 plis.

Dans R, vous pouvez utiliser la fonction cut afin de partager uniformément les
indices d’un certain jeu de données pour effectuer le sous-échantillonnage. Il
vous suffit ensuite d’opérer une boucle sur les plis ainsi formés, en les
fractionnant à chaque fois pour obtenir vos jeux d’entraînement et de test :

set.seed(123)

x <- rnorm(100, 2, 1)
y = exp(x) + rnorm(5, 0, 2)
data <- data.frame(x, y)

data.shuffled <- data[sample(nrow(data)), ]


folds <- cut(seq(1, nrow(data)), breaks = 10, labels =
FALSE)

errors <- c(0)

for (i in 1:10) {
fold.indexes <- which(folds == i, arr.ind = TRUE)

test.data <- data[fold.indexes, ]


training.data <- data[-fold.indexes, ]

train.linear <- lm(y ~ x, training.data)


train.output <- predict(train.linear, test.data)
errors <- c(errors, sqrt(sum(((train.output -
test.data$y)^2/
length(train.output)))))
}

errors[2:11]

## [1] 4.696183 6.392002 4.769101 4.259850 9.634505


5.073442 7.547830
## [8] 7.366703 3.974609 10.539853

mean(errors[2:11])

## [1] 6.425408

Plus tôt dans ce chapitre, nous avons vu comment une régression linéaire ajustée
sur des données d’exemple nous avait donné une estimation de l’erreur autour de
cinq, ou à peu près. Mais l’exemple précédent montre que l’estimation de
l’erreur peut varier dans une large mesure à l’intérieur de vos propres données en
fonction de la façon dont vous les divisez en jeux d’entraînement et de test !
Ici, vous pouvez voir les sorties pour les valeurs RMSE correspondant à dix
partages différents des données. Certaines erreurs peuvent tomber jusqu’à 3.9,
tandis qu’une autre s’élève à 10.5. Ainsi, en utilisant la validation croisée, non
seulement vous constatez le haut degré de variabilité des valeurs RMSE pour ces
données, mais vous pouvez aussi atténuer cela en prenant la moyenne de ces
valeurs pour obtenir un nombre final qui est plus représentatif de l’erreur sur
l’ensemble de toutes les données.
En résumé
Dans ce chapitre, nous avons examiné de nombreux concepts statistiques qui
sont à la base de la manière dont nous concevons nos données pour les utiliser
dans des modèles d’apprentissage automatique. Nous avons d’abord discuté de
diverses techniques d’échantillonnage à l’aide desquelles nous récupérons des
valeurs à partir du jeu de données dont nous disposons au départ. Vous pouvez
procéder de façon aléatoire (ce qui est la méthode la plus courante) grâce à
l’utilisation d’un échantillonnage aléatoire simple. Il existe d’autres techniques,
comme l’échantillonnage stratifié ou l’échantillonnage par grappes (clusters),
mais qui sont beaucoup plus rarement employées.
Lorsque vous évaluez la performance d’un modèle d’apprentissage automatique,
vous avez besoin de certaines valeurs de base pour les comparer avec ce que le
modèle fournit comme prédiction. Pour ce faire, vous prenez les données de
départ et les divisez en un jeu d’entraînement et un jeu de test. Le jeu
d’entraînement représente généralement 70 % du total des données, et donc le
jeu de test les 30 % restants. Il faut toujours que le jeu d’entraînement soit
beaucoup plus grand que le jeu de test afin que le modèle dispose de
suffisamment de points de données pour effectuer ses calculs.
L’évaluation des modèles d’apprentissage automatique se présente sous deux
formes : celle des prédictions basées sur la régression, et celle des prédictions
basées sur la classification. Dans le cas de la régression, vous obtenez
typiquement des vecteurs de nombres par rapport auxquels vous comparez vos
données de test. Vous pouvez le faire en utilisant de nombreuses métriques
statistiques, l’une des formes les plus courantes étant RMSE. Avec RMSE, vous
prenez les valeurs produites en sortie par le modèle, vous soustrayez les valeurs
de test, vous mettez les différences au carré, vous prenez la moyenne, et vous
calculez enfin la racine carrée du résultat final. Cela tend à nous donner une
image souple mais exacte de la sortie du modèle.
Finalement, nous avons discuté des techniques liées à la validation croisée. Il
s’agit d’une technique statistique dans laquelle vous divisez efficacement les
données en de multiples petits jeux d’entraînement et de test. Vous les évaluez
indépendamment les uns des autres, puis vous agrégez leurs erreurs. Cela vous
permet d’avoir davantage de confiance dans les erreurs en sortie du modèle, car
vous savez qu’il a été appliqué à la totalité du jeu de données, et non à une petite
vue sur celui-ci. Cette manière de procéder est un bon moyen de vous assurer de
la validité statistique de votre modèle d’apprentissage automatique.

1 Schaeaffer, R., W. Mendenhall, et al. Elementary Survey Sampling, 3e éd. Boston : PSW-Kent, 1986.
CHAPITRE 4
La régression en bref
Dans le Chapitre 1, nous avons brièvement exploré les domaines de
l’apprentissage automatique en commençant par la régression linéaire, car il
s’agit probablement de quelque chose que vous avez rencontré à un moment
donné de votre formation en mathématiques. Le processus est assez intuitif et
plus facile à expliquer pour débuter que certains autres modèles d’apprentissage
automatique. En outre, de nombreux domaines de l’analyse des données reposent
sur la modélisation de régressions, en allant d’une entreprise qui tente de prévoir
ses futurs profits jusqu’aux frontières de la science s’efforçant de découvrir de
nouvelles lois régissant l’univers. Nous pouvons trouver une régression dans
n’importe quel scénario recherchant une prédiction par rapport au temps. Dans
ce chapitre, nous allons examiner dans une large mesure comment utiliser la
modélisation de la régression dans R, mais nous explorerons également certains
problèmes et certains pièges dont il faut être conscient dans ce processus.
Derrière la régression, la motivation principale est de construire une équation
grâce à laquelle nous pouvons en apprendre davantage sur nos données. Il n’y a
pas de règle absolue quant au type de modèle de régression à ajuster à vos
données. Le choix entre une régression logistique, une régression linéaire ou un
modèle de régression multivariée dépend du problème et des données dont vous
disposez. Vous pourriez ajuster une ligne droite à une certaine série de points de
données, mais est-ce toujours ce qu’il y a de mieux à faire ? Dans l’idéal, nous
recherchons un équilibre entre simplicité et pouvoir explicatif. L’ajustement en
ligne droite sur une série complexe de données peut être simple, mais ne pas
décrire tout le tableau. D’un autre côté, le fait d’avoir un jeu de données très
simple, qui forme fondamentalement une ligne droite, et y ajuster un modèle
avec toutes sortes de courbes farfelues, peut vous donner un très haut degré
d’exactitude, mais cela laisserait très peu de place pour que de nouveaux points
de données y soient adaptés.
Dans le cadre de vos études de mathématiques, vous vous souvenez peut-être
d’avoir eu quelques points de données et d’avoir essayé de tracer une ligne le
mieux ajustée possible par rapport à ces données. Il s’agit de la forme la plus
simple d’apprentissage automatique, et on effectue souvent ce genre de
manipulation sans même se rendre compte qu’il s’agit précisément d’un type
d’apprentissage automatique. Faire passer une droite par deux points de données
est quelque chose de facile à apprendre. Avec trois points de données ou plus,
cela devient une tâche que les ordinateurs peuvent mieux gérer en termes de
calculs. Le simple ajout d’un nouveau point de données (ou, comme nous le
verrons, de plusieurs dizaines d’autres) rend le problème nettement plus difficile
à résoudre.
Mais, grâce à des techniques mathématiques appliquées aux modèles
d’apprentissage automatique classiques, nous pouvons très facilement calculer
ce genre de problèmes. R rend bon nombre de ces étapes assez simples à traiter,
et ce chapitre fournit des bases pour évaluer les questions concernant la position
à laquelle tracer la ligne selon la complexité et l’exactitude du modèle.
Régression linéaire
Dans le Chapitre 1, nous avons effleuré le sujet de la régression linéaire en
partant d’un exemple utilisant le jeu de données mtcars. Dans cet exemple, nous
avions déterminé une relation linéaire concernant l’efficacité énergétique des
véhicules en fonction de leur poids, et nous avions détecté une tendance à la
baisse. Nous avons extrait les coefficients d’une équation mathématique linéaire
et nous avons épousseté (virtuellement) nos mains. Pourtant, il y a beaucoup plus
là-dedans que simplement écrire au tableau noir une équation en partant d’un jeu
de données, et prétendre que le travail est terminé. Reprenons donc notre
exemple mtcars (voir la Figure 4.1) :

model <- lm(mtcars$mpg ~ mtcars$disp)

plot(y = mtcars$mpg, x = mtcars$disp, xlab = "Cylindrée


du moteur (en pouces
cubiques)", ylab = "Rendement énergétique (en miles par
gallon US)", main =
"Rendement énergétique depuis le jeu de données
'mtcars'")

abline(a = coef(model[1]), b = coef(model)[2], lty = 2)

Nous modélisons ici le rendement énergétique (mpg) en fonction de la cylindrée


du moteur (disp). Regardons ensuite les résultats du modèle avec la fonction
summary() :

summary(model)

## Call:
## lm(formula = mtcars$mpg ~ mtcars$disp)
##
## Residuals:
## Min 1Q Median 3Q Max
## -4.8922 -2.2022 -0.9631 1.6272 7.2305
##
## Coefficients:
## Estimate Std. Error t value Pr(>¦t¦)
## (Intercept) 29.599855 1.229720 24.070 < 2e-16 ***
## mtcars$disp -0.041215 0.004712 -8.747 9.38e-10 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.'
0.1 ' ' 1
##
## Residual standard error: 3.251 on 30 degrees of
freedom
## Multiple R-squared: 0.7183, Adjusted R-squared:
0.709
## F-statistic: 76.51 on 1 and 30 DF, p-value: 9.38e-10

Figure 4.1 : Une régression linéaire simple ajustée aux données.

Il y a un grand nombre d’informations extraites lors de l’appel à la fonction


summary() sur cet objet de modèle linéaire. En général, la seule valeur que les
gens regardent pour obtenir une évaluation de l’exactitude de base est le R-carré
multiple. Plus cette valeur est proche de 1, et plus le modèle de régression
linéaire est exact. Il y a cependant beaucoup d’autres termes dans cette sortie.
Passons-les en revue pour comprendre de quoi il s’agit :
Call
Cela affiche l’appel de formule que nous avons utilisé. Dans ce cas, nous avons utilisé une variable
réponse, mpg, en tant que fonction d’une variable dépendante, disp, les deux étant appelées à partir
du data frame mtcars.
Residuals
Il s’agit d’une mesure de la distance verticale entre chaque point de données et la droite ajustée dans
notre modèle. Dans ce cas, nous avons résumé des statistiques pour toutes les distances verticales
pour tous nos points relativement à la ligne ajustée. Plus cette valeur est petite, meilleur est
l’ajustement.

Coefficients
Ce sont les estimations des coefficients de notre équation linéaire. Dans
ce cas, notre équation s’écrirait y = 0.04x + 29.59.

• Std. Error : Ces coefficients sont accompagnés d’estimations des erreurs


telles qu’elles sont données par la partie Std Error du tableau des
coefficients. En réalité, notre équation devrait plutôt s’écrire à peu près
comme ceci : y = (-0.04 ± 0.005) x + (29.59 ± 1.23).

• t-value : Il s’agit de la mesure de la différence relative à la variation de nos


données. Cette valeur est liée à des p-valeurs, mais ces dernières sont
utilisées beaucoup plus fréquemment.

• p-value : Ces p-valeurs sont des évaluations statistiques de la


significativité. Le fonctionnement des p-valeurs est plus compliqué que
cela, mais pour nos besoins, retenons qu’une p-valeur plus petite
que 0.05 signifie que nous pouvons considérer le nombre comme étant
statistiquement significatif. Les étoiles affichées sur la droite sont
expliquées par les codes de significativité qui suivent.
Residual standard error
L’erreur résiduelle standard se rapporte à l’écart type de nos données.

Multiple R-squared
Il s’agit de la valeur R-carré lorsque nous avons de multiples prédicteurs. Cette valeur n’est pas tout à
fait pertinente pour notre exemple linéaire, mais, lorsque nous ajoutons davantage de prédicteurs au
modèle, le R-carré multiple augmentera invariablement. Cela est dû au fait qu’une caractéristique que
nous ajoutons au modèle expliquera une certaine partie de la variance, qu’elle soit vraie ou non.

Adjusted R-squared
Pour contrebalancer les biais introduits par le fait d’avoir une valeur R-carré en constante
augmentation avec plus de prédicteurs, le R-carré ajusté tend à être une meilleure représentation de
l’exactitude d’un modèle lorsqu’il y a de multiples caractéristiques.
F-statistic
Enfin, cette statistique F est le rapport entre la variance expliquée par les paramètres du modèle et la
variance inexpliquée.

Cet exemple linéaire simple a un certain pouvoir explicatif. Nous avons


déterminé une relation entre le rendement énergétique et la cylindrée du moteur.
Souvent, c’est là que les exemples de régression linéaire simple finissent par
perdre leur utilité. Les éléments que nous recherchons le plus dans ce cas précis
sont la pente et l’ordonnée à l’origine. Si cet exemple était par exemple appliqué
à des ventes au fil du temps, le résultat de cet exercice de modélisation serait une
valeur de départ pour l’ordonnée à l’origine et un taux de croissance pour le
coefficient.

Régression multivariée
Supposons que vous vouliez construire un modèle plus robuste concernant le
rendement énergétique avec davantage de variables intégrées. Le rendement
énergétique d’un véhicule peut être un phénomène complexe, avec de nombreux
facteurs intervenant dans ce domaine, autres que la seule cylindrée du moteur.
Vous voulez donc trouver de la manière la plus précise toutes les caractéristiques
qui interviennent dans le comportement de votre modèle. Pour cela, vous pouvez
toujours continuer à utiliser la régression, mais cette fois dans un contexte
multivarié.
Rappelons que notre exemple de régression linéaire simple était fondé sur
l’équation :
y = b + m1x1

où les coefficients sont l’ordonnée à l’origine, b, et la pente, m, liés à l’unique


variable, x, que nous avions dans le modèle. Si vous voulez introduire davantage
de facteurs contribuant au modèle, changez la formule mathématique en :
y = b + m1x1 + m2x2 + m3x3 + ()

où x1, x2, x3, et ainsi de suite, sont des caractéristiques différentes pour le
modèle, comme le poids du véhicule, la cylindrée du moteur, le nombre de
cylindres, etc. Le nouvel objectif consiste alors à trouver des coefficients pour un
modèle de la forme y = f(x1, x2, x3, (....)), et vous devez donc revoir l’appel à la
fonction lm() dans R :
lm.wt <- lm(mpg ~ disp + wt, data = mtcars)
summary(lm.wt)

##
## Call:
## lm(formula = mpg ~ disp + wt, data = mtcars)
##
## Residuals:
## Min 1Q Median 3Q Max
## -3.4087 -2.3243 -0.7683 1. 6.3484
##
## Coefficients:
## Estimate Std. Error t value Pr(>¦t¦)
## (Intercept) 34.96055 2.16454 16.151 4.91e-16 ***
## disp -0.01773 0.00919 -1.929 0.06362 .
## wt -3.35082 1.16413 -2.878 0.00743 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.'
0.1 ' ' 1
##
## Residual standard error: 2.917 on 29 degrees of
freedom
## Multiple R-squared: 0.7809, Adjusted R-squared:
0.7658
## F-statistic: 51.69 on 2 and 29 DF, p-value: 2.744e-10

Ce code élargit la modélisation linéaire que nous avions étudiée auparavant pour
inclure le poids du véhicule dans le montage du modèle. Dans ce cas, vous
constatez que le R-carré ajusté a légèrement augmenté de 0.7809 lorsque vous
ajustez un modèle uniquement sur la cylindrée du moteur, à 0.7658 une fois
inclus le poids du moteur dans cet ajustement.
Cependant, notez que la pertinence statistique de la caractéristique précédente a
considérablement diminué. Avant, la p-valeur de la caractéristique disp était bien
en dessous du seuil de signification statistique 0.05. Maintenant, elle est
d’environ 0.06. Cela pourrait être dû au fait que le rendement énergétique d’une
voiture est plus sensible aux changements de poids du véhicule qu’à la cylindrée
du moteur.
Si vous voulez pousser cette analyse plus loin, vous pouvez ajouter une
caractéristique supplémentaire provenant du jeu de données, et voir comment le
R-carré du modèle et les p-valeurs des coefficients varient en conséquence :
lm.cyl <- lm(mpg ~ disp + wt + cyl, data = mtcars)
summary(lm.cyl)

##
## Call:
## lm(formula = mpg ~ disp + wt + cyl, data = mtcars)
##
## Residuals:
## Min 1Q Median 3Q Max
## -4.4035 -1.4028 -0.4955 1.3387 6.0722
##
## Coefficients:
## Estimate Std. Error t value Pr(>¦t¦)
## (Intercept) 41.107678 2.842426 14.462 1.62e-14 ***
## disp 0.007473 0.011845 0.631 0.53322
## wt -3.635677 1.040138 -3.495 0.00160 **
## cyl -1.784944 0.607110 -2.940 0.00651 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.'
0.1 ' ' 1
##
## Residual standard error: 2.595 on 28 degrees of
freedom
## Multiple R-squared: 0.8326, Adjusted R-squared:
0.8147
## F-statistic: 46.42 on 3 and 28 DF, p-value: 5.399e-11

Ce code adopte la même approche qu’auparavant, mais il ajoute le nombre de


cylindres du moteur au modèle. Notez que la valeur R-carré a encore une fois
augmenté pour passer de 0.7809 à 0.8147. Cependant, la pertinence statistique
de la cylindrée dans les données est fondamentalement défunte, avec une p-
valeur de 10 fois le seuil, à 0.53322 au lieu de 0.05. Cela nous indique que
l’efficacité énergétique est davantage liée aux caractéristiques combinées du
poids du véhicule et du nombre de cylindres qu’à la cylindrée du moteur. Vous
pouvez alors réexécuter cette analyse en prenant seulement les caractéristiques
statistiquement pertinentes :

lm.cyl.wt <- lm(mpg ~ wt + cyl, data = mtcars)


summary(lm.cyl.wt)

##
## Call:
## lm(formula = mpg ~ wt + cyl, data = mtcars)
##
## Residuals:
## Min 1Q Median 3Q Max
## -4.2893 -1.5512 -0.4684 1.5743 6.1004
##
## Coefficients:
## Estimate Std. Error t value Pr(>¦t¦)
## (Intercept) 39.6863 1.7150 23.141 < 2e-16 ***
## wt -3.1910 0.7569 -4.216 0.000222 ***
## cyl -1.5078 0.4147 -3.636 0.001064 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.'
0.1 ' ' 1
##
## Residual standard error: 2.568 on 29 degrees of
freedom
## Multiple R-squared: 0.8302, Adjusted R-squared:
0.8185
## F-statistic: 70.91 on 2 and 29 DF, p-value: 6.809e-12

En supprimant du modèle la caractéristique statistiquement non pertinente pour


ne conserver que les autres, vous avez plus ou moins préservé la valeur R-carré
à 0.8185 contre 0.8147.
Vous devez cependant faire attention lorsque vous ajoutez des caractéristiques
aux données. Dans R, vous pouvez facilement modéliser une réponse pour toutes
les caractéristiques en appelant la fonction lm() sous la forme suivante :

lm.all <- lm(mpg ~ ., data = mtcars)


summary(lm.all)

##
## Call:
## lm(formula = mpg ~ ., data = mtcars)
##
## Residuals:
## Min 1Q Median 3Q Max
## -3.4506 -1.6044 -0.1196 1.2193 4.6271
##
## Coefficients:
## Estimate Std. Error t value Pr(>¦t¦)
## (Intercept) 12.30337 18.71788 0.657 0.5181
## cyl -0.11144 1.04502 -0.107 0.9161
## disp 0.01334 0.01786 0.747 0.4635
## hp -0.02148 0.02177 -0.987 0.3350
## drat 0.78711 1.63537 0.481 0.6353
## wt -3.71530 1.89441 -1.961 0.0633 .
## qsec 0.82104 0.73084 1.123 0.2739
## vs 0.31776 2.10451 0.151 0.8814
## am 2.52023 2.05665 1.225 0.2340
## gear 0.65541 1.49326 0.439 0.6652
## carb -0.19942 0.82875 -0.241 0.8122
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.'
0.1 ' ' 1
##
## Residual standard error: 2.65 on 21 degrees of freedom
## Multiple R-squared: 0.869, Adjusted R-squared:
0.8066
## F-statistic: 13.93 on 10 and 21 DF, p-value: 3.793e-
07

Cette syntaxe crée un modèle linéaire, la variable dépendante mpg étant cette
fois modélisée par rapport à tout ce qui se trouve dans le jeu de données, comme
indiqué par le point (.) dans l’appel de la fonction lm(). Le problème avec cette
approche, cependant, est que vous voyez très peu de valeurs statistiques
pertinentes dans les coefficients du modèle. De même, l’erreur type pour chacun
des coefficients est très élevée, et il est donc très difficile de déterminer une
valeur exacte pour les coefficients. Au lieu de cette approche descendante pour
voir quelles caractéristiques sont les plus importantes dans le jeu de données, il
est préférable de procéder de manière ascendante comme nous l’avons fait
jusqu’à présent.
Bien que le thème de la sélection des caractéristiques soit en lui-même un sujet
très vaste (nous y reviendrons plus en profondeur avec d’autres algorithmes
d’apprentissage automatique), nous pouvons atténuer certains de ces problèmes
grâce à divers procédés :
Sélection minutieuse des caractéristiques
Choisissez les caractéristiques à ajouter au modèle, une à la fois, et éliminez celles qui sont
statistiquement insignifiantes. C’est ce que nous avons fait dans les blocs de code précédents, en
ajoutant un paramètre à la fois et en vérifiant si la p-valeur en sortie du modèle pour ce paramètre est
statistiquement significative.

Régularisation
Il s’agit de conserver toutes les caractéristiques, mais de réduire mathématiquement les coefficients
les moins importants pour minimiser leur impact sur le modèle.

Régularisation
La régularisation est un concept qui peut être délicat mathématiquement parlant,
mais en principe, la notion est assez simple. L’idée est que vous voulez inclure
autant de caractéristiques que possible dans le modèle final. Plus il y a de
caractéristiques, et mieux vous pouvez expliquer toutes les complexités du jeu de
données. Le problème ici est que le degré auquel chaque caractéristique explique
une partie du modèle, une fois la régularisation appliquée, peut être très
différent.
Grâce à l’utilisation de la régularisation, vous pouvez rendre votre modèle plus
succinct et réduire le bruit dans le jeu de données qui pourrait provenir de
caractéristiques ayant peu d’impact sur ce que vous essayez de modéliser.
Voyons à quoi ressemblerait le modèle linéaire pour le jeu de données mtcars si
nous incluions toutes les caractéristiques. Nous aurions une équation comme
celle-ci :
mpg = 12.3 − 0.11cyl + 0.01disp − 0.02hp + 0.79drat − 3.72wt + 0.82qsec + 0.31vs + 2.42am +
0.66gear − 0.20carb

Selon cette équation linéaire, le rendement énergétique est le plus sensible au


poids du véhicule (-3.72wt), étant donné que celui-ci a le coefficient le plus
élevé. Cependant, la plupart de ces coefficients se situent dans un ordre de
grandeur voisin. La régularisation conserverait toutes les caractéristiques, mais
les moins importantes verraient leurs coefficients réduits.
Pour utiliser cette technique de régularisation, vous appelez un type particulier
de modélisation, connu sous le nom de régression lasso, tel qu’illustré ici :

library(lasso2)
lm.lasso <- l1ce(mpg ~ ., data = mtcars)
summary(lm.lasso)$coefficients
## Value Std. Error Z score
Pr(>¦Z¦)
## (Intercept) 36.01809203 18.92587647 1.90311355
0.05702573
## cyl -0.86225790 1.12177221 -0.76865686
0.44209704
## disp 0.00000000 0.01912781 0.00000000
1.00000000
## hp -0.01399880 0.02384398 -0.58709992
0.55713660
## drat 0.05501092 1.78394922 0.03083659
0.97539986
## wt -2.68868427 2.05683876 -1.30719254
0.19114733
## qsec 0.00000000 0.75361628 0.00000000
1.00000000
## vs 0.00000000 2.31605743 0.00000000
1.00000000
## am 0.44530641 2.14959278 0.20715850
0.83588608
## gear 0.00000000 1.62955841 0.00000000
1.00000000
## carb -0.09506985 0.91237207 -0.10420075
0.91701004

Ce code appelle la fonction l1ce() du package lasso2 en lui passant le jeu de


données mtcars. Remarquez que cet appel de fonction est le même que ci-dessus,
c’est-à-dire en prenant la variable du rendement énergétique, mpg, modélisée en
tant que fonction de toutes les autres variables du jeu de données. La technique
de régularisation, qui n’est appliquée que pendant la partie mathématique lourde
de l’algorithme, est intégrée dans la régression lasso. Cette partie régularisation
de la régression rééchelonne les coefficients en fonction de l’impact réel qu’ils
ont sur le modèle d’une manière plus « statistique ». Dans certains cas, cela peut
entraîner la réduction de certaines caractéristiques à une valeur si faible qu’elles
approchent de zéro. Grâce à cette modélisation de régression, vous disposez
maintenant d’une équation différente :
mpg = 36.02 − 0.86cyl + 0disp − 0.014hp + 0.06drat − 2.69wt + 0qsec + 0vs + 0.45am + 0gear −
0.095carb

Ou, plus simplement :


mpg = 36.02 − 0.86cyl − 0.014hp + 0.06drat − 2.69wt + 0.45am + − 0.095carb

La caractéristique la plus importante avant le passage à la régression lasso était


le poids du véhicule, wt, qui est resté inchangé quant à son importance relative.
Même si le coefficient a quelque peu évolué, il reste celui qui a la valeur la plus
élevée. Les caractéristiques les moins utiles (ici, celles qui sont mises à zéro)
sont celles qui ont probablement peu d’impact sur le rendement énergétique dès
le départ. Il s’agit notamment du temps pour atteindre le quart de mile (qsec), la
disposition du moteur en V ou en ligne (vs) et le nombre de vitesses (gear).
Cependant, la variable de la cylindrée a montré une relation claire avec le
rendement énergétique, comme nous l’avons vu plus haut. Le fait qu’elle soit
ramenée à zéro ne signifie pas qu’il n’y a aucune relation entre cette seule
variable et notre réponse. Cela veut simplement dire que son importance devient
négligeable lorsqu’elle est traitée conjointement avec toutes les autres variables
du jeu de données.
Rappelez-vous, dans ce cas, nous sommes intéressés par un modèle de toutes les
caractéristiques, pas nécessairement par l’importance d’une seule caractéristique.
On remarque, dans le nouveau modèle de régression lasso, que certains des
coefficients ont été plus ou moins éliminés mathématiquement. Pour affiner
davantage le modèle et réduire le nombre de caractéristiques qu’il contient, vous
pouvez réexécuter la régression sans ces caractéristiques et voir ce qui change :

lm.lasso2 <- l1ce(mpg ~ cyl + hp + wt + am + carb, data =


mtcars)
summary(lm.lasso2)$coefficients

## Value Std. Error Z score


Pr(>¦Z¦)
## (Intercept) 31.2819166926 4.51160542 6.93365527
4.100942e-12
## cyl -0.7864202230 0.86107128 -0.91330444
3.610824e-01
## hp -0.0009037003 0.02343634 -0.03855979
9.692414e-01
## wt -1.9248597501 1.38749433 -1.38729198
1.653527e-01
## am 0.0000000000 2.22143917 0.00000000
1.000000e+00
## carb 0.0000000000 0.67947216 0.00000000
1.000000e+00

Le jeu de données réduit étant transféré vers une autre régression lasso, vous
pouvez constater que le type de transmission de la voiture, am, et le nombre de
carburateurs, carb, ont tous deux été mis à zéro. En supprimant ces
caractéristiques et en relançant la procédure, vous pouvez voir si d’autres
caractéristiques sont abandonnées :

lm.lasso3 <- l1ce(mpg ~ cyl + hp + wt, data = mtcars)


summary(lm.lasso3)$coefficients

## Value Std. Error Z score Pr(>¦Z¦)


## (Intercept) 30.2106931 1.97117597 15.3262284 0.0000000
## cyl -0.7220771 0.82941877 -0.8705821 0.3839824
## hp 0.0000000 0.01748364 0.0000000 1.0000000
## wt -1.7568469 1.07478525 -1.6346028 0.1021324

Cette fois, c’est la puissance de la voiture, hp, qui est éliminée. Vous pouvez
continuer ainsi tant qu’il vous reste plusieurs caractéristiques à tester :

lm.lasso4 <- l1ce(mpg ~ cyl + wt, data = mtcars)


summary(lm.lasso4)$coefficients

## Value Std. Error Z score Pr(>¦Z¦)


## (Intercept) 29.8694933 1.4029760 21.290096 0.0000000
## cyl -0.6937847 0.5873288 -1.181254 0.2375017
## wt -1.7052064 1.0720172 -1.590652 0.1116879

Le résultat final est un modèle qui n’a plus que deux caractéristiques au lieu des
onze avec lesquelles vous avez commencé :
mpg = 29.87 0.69cyl 1.70wt
Régression polynomiale
La régression polynomiale consiste simplement à ajuster aux données une
fonction de degré supérieur. Nous avons vu plus haut ce type d’ajustement aux
données basé sur la formule suivante :
y = b + m1x1 + m2x2 + m3x3 + ()

La régression polynomiale diffère des cas linéaires simples en possédant


plusieurs degrés pour chaque caractéristique du jeu de données. La formule
pourrait alors être représentée comme suit :
y = b + mx2

L’exemple suivant nous aidera dans notre raisonnement (voir la Figure 4.2) :

pop <- data.frame(uspop)


pop$uspop <- as.numeric(pop$uspop)
pop$year <- seq(from = 1790, to = 1970, by = 10)

plot(y = pop$uspop, x = pop$year, main = "Population des


USA de 1790 à 1970",
xlab = "Année", ylab = "Population")
Figure 4.2 : Tracé de la population des États-Unis, en décennies de 1790 à 1970.

Nous partons d’un jeu de données déjà intégré dans R mais que nous avons
légèrement modifié à des fins de démonstration. Normalement, le jeu uspop est
un objet de type série temporelle qui a ses propres critères de tracé, mais ici nous
l’avons ajusté pour ne prendre que les points de données dans le graphique. Ces
données représentent la population des États-Unis de 1790 à 1970, en allant par
tranches de dix ans. Commençons par ajuster un modèle linéaire aux données :

lm1 <- lm(pop$uspop ~ pop$year)


summary(lm1)

##
## Call:
## lm(formula = pop$uspop ~ pop$year)
##
## Residuals:
## Min 1Q Median 3Q Max
## -19.569 -14.776 -2.933 9.501 36.345
##
## Coefficients:
## Estimate Std. Error t value Pr(>¦t¦)
## (Intercept) -1.958e+03 1.428e+02 -13.71 1.27e-10 ***
## pop$year 1.079e+00 7.592e-02 14.21 7.29e-11 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.'
0.1 ' ' 1
##
## Residual standard error: 18.12 on 17 degrees of
freedom
## Multiple R-squared: 0.9223, Adjusted R-squared:
0.9178
## F-statistic: 201.9 on 1 and 17 DF, p-value: 7.286e-11

Cet ajustement linéaire simple semble fonctionner assez bien. Les p-valeurs des
estimations sont très basses, ce qui indique une bonne signification statistique.
De même, les valeurs R-carré sont toutes les deux très bonnes. Cependant, les
résidus montrent un degré de variabilité plutôt large, allant jusqu’à une
différence de 36, comme le montre la Figure 4.3 :

plot(y = pop$uspop, x = pop$year, main = "Population des


USA de 1790 à 1970",
xlab = "Année", ylab = "Population")

abline(a = coef(lm1[1]), b = coef(lm1)[2], lty = 2, col =


"black")
Figure 4.3 : Données démographiques avec ajustement linéaire du modèle.

La ligne pointillée du modèle linéaire, telle qu’elle apparaît sur la figure, semble
faire l’affaire. Elle s’ajuste mieux sur certaines données que sur d’autres, mais il
est assez clair sur le tracé qu’il ne s’agit pas exactement d’une relation linéaire.
De plus, nous savons par intuition qu’au fil du temps, la population a tendance à
suivre une courbe exponentielle plutôt qu’une forme rectiligne. Ce que vous
voulez faire ensuite est de voir comment un modèle de degré plus élevé va se
comporter par rapport au traitement linéaire, celui-ci étant le polynôme d’ordre
le plus bas que vous puissiez ajuster :

lm2 <- lm(pop$uspop ~ poly(pop$year, 2))


summary(lm2)

##
## Call:
## lm(formula = pop$uspop ~ poly(pop$year, 2))

##
## Residuals:
## Min 1Q Median 3Q Max
## -6.5997 -0.7105 0.2669 1.4065 3.9879
##
## Coefficients:
## Estimate Std. Error t value
Pr(>¦t¦)
## (Intercept) 69.7695 0.6377 109.40 < 2e-
16 ***
## poly(pop$year, 2)1 257.5420 2.7798 92.65 < 2e-
16 ***
## poly(pop$year, 2)2 73.8974 2.7798 26.58 1.14e-
14 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.'
0.1 ' ' 1
##
## Residual standard error: 2.78 on 16 degrees of freedom
## Multiple R-squared: 0.9983, Adjusted R-squared:
0.9981
## F-statistic: 4645 on 2 and 16 DF, p-value: < 2.2e-16
Ce code appelle à nouveau la fonction lm(), mais cette fois avec un paramètre
supplémentaire pour la variable dépendante, la fonction poly(). Celle-ci prend les
données de date et calcule un vecteur orthogonal, qui est ensuite mis à l’échelle
qui convient. Par défaut, la fonction poly() ne change pas les valeurs des
données de date, mais vous pouvez l’utiliser pour voir si elle donne de meilleurs
résultats que l’ajustement d’ordre inférieur que vous avez opéré précédemment.
Rappelons que cet ajustement linéaire est techniquement un polynôme, mais de
degré 1. L’ajustement du modèle résultant se traduit par une équation comme
celle-ci :

Commençons par observer la sortie de la fonction summary(). L’examen de la


sortie résiduelle nous procure un peu de soulagement : il n’y a plus rien de
l’ordre de 30 ! Les plus petits résidus sont toujours meilleurs en termes
d’ajustement du modèle. Le tableau des coefficients comporte maintenant trois
entrées : une pour l’ordonnée à l’origine, une pour le terme de premier degré, et
maintenant une autre pour le terme de second degré. Lorsque vous avez appelé
poly(pop$year, 2), vous avez indiqué à R que vous vouliez un polynôme des
données de date dont le degré le plus élevé soit de 2. En reprenant le tableau des
coefficients, vous pouvez constater que toutes les p-valeurs sont statistiquement
significatives, ce qui est aussi une indication nette du fait qu’il s’agit d’un
modèle s’ajustant bien à vos données (voir la Figure 4.4) :

plot(y = pop$uspop, x = pop$year, main = "Population des


USA de 1790 à 1970",
xlab = "Année", ylab = "Population")

pop$lm2.predict = predict(lm2, newdata = pop)

lines(sort(pop$year), fitted(lm2)[order(pop$year)], col =


"black",
lty = 2)

En regardant la Figure 4.4, on constate que le polynôme de degré supérieur (dans


ce cas, une équation quadratique) s’ajuste manifestement mieux aux données. Il
semble clair que l’utilisation de polynômes de degré supérieur fonctionne mieux
que celle de polynômes de degré inférieur, n’est-ce pas ? Que se passe-t-il alors
si vous passez à un polynôme du troisième degré, ou davantage ?
Figure 4.4 : Modéliser l’évolution dans le temps de la population avec un ajustement quadratique semble «
coller » beaucoup mieux aux données qu’un modèle linéaire. Cependant, si vous voulez le résultat le plus
précis possible, vous devrez peut-être augmenter le degré polynomial avec lequel vous ajustez les données.

Je parie que si vous utilisez un polynôme du sixième degré, vous aurez un


modèle très précis ! Ce qui saute immédiatement aux yeux, c’est que
l’ajustement linéaire simple que vous aviez obtenu auparavant « colle » du
mieux qu’il peut aux données, mais que le polynôme du deuxième degré (une
forme quadratique simple) s’ajuste encore bien mieux. Une bonne manière de
distinguer la différence entre ajustements polynomiaux d’ordre supérieur
consiste à examiner les tracés des valeurs résiduelles de chaque modèle. C’est ce
qu’illustre la Figure 4.5.
Figure 4.5 : Population au fil du temps avec ajustement de plusieurs modèles.

La Figure 4.5 compare l’ajustement linéaire par rapport à ce que l’on obtient en
augmentant les degrés polynomiaux. Les polynômes sont difficiles à séparer en
termes de qualité d’ajustement, mais tous semblent mieux fonctionner que le cas
linéaire simple. Pour comparer des modèles qui sont des approximations
visuellement proches à ce niveau, il faudrait plutôt se plonger dans les tracés de
leurs valeurs résiduelles, comme l’illustre la Figure 4.6 :

par(mfrow = c(2, 3))


plot(resid(lm1), main = "Degré 1", xlab = "Années
(tranches décennales)",
ylab = "Résidus")
plot(resid(lm2), main = "Degré 2", xlab = "Années
(tranches décennales)",
ylab = "Résidus")
plot(resid(lm3), main = "Degré 3", xlab = "Années
(tranches décennales)",
ylab = "Résidus")
plot(resid(lm4), main = "Degré 4", xlab = "Années
(tranches décennales)",
ylab = "Résidus")
plot(resid(lm5), main = "Degré 5", xlab = "Années
(tranches décennales)",
ylab = "Résidus")
plot(resid(lm6), main = "Degré 6", xlab = "Années
(tranches décennales)",
ylab = "Résidus")

Rappelons qu’un résidu est la distance verticale entre un point de donnée et la


ligne ajustée pour le modèle. Un modèle qui s’ajuste exactement aux points de
données devrait donc avoir un tracé des résidus aussi près que possible d’une
droite plate, horizontale. Dans le cas de notre ajustement linéaire, l’échelle du
tracé des résidus est beaucoup plus grande que les autres, et vous pouvez voir
que la distance résiduelle y est assez mauvaise au début, à mi-chemin et à la fin.
Ce modèle n’est pas idéal. D’un autre côté, les polynômes de degré supérieur
semblent plutôt bien fonctionner. L’échelle de leurs distributions résiduelles est
beaucoup plus belle, mais celle qui se démarque vraiment est la dernière, soit
l’ajustement polynomial du sixième degré. Le tracé des résidus y est à peu près
nul au départ, puis il devient un peu plus sujet aux erreurs.

Figure 4.6 : Tracé des résidus de chacun des modèles.

Tout cela est bien beau, mais il pourrait être plus facile de classer les ajustements
du modèle en examinant les valeurs numériques de leurs résidus :
c(sum(abs(resid(lm1))), sum(abs(resid(lm2))),
sum(abs(resid(lm3))),
sum(abs(resid(lm4))), sum(abs(resid(lm5))),
sum(abs(resid(lm6))))

## [1] 272.51432 33.77224 34.54039 36.95125 25.45242


19.59938

Ce code additionne la valeur absolue des résidus. Si vous prenez simplement la


somme brute de ces résidus, vous obtenez une image inexacte parce que
certaines valeurs peuvent être négatives. Ainsi, le résidu total pour l’ajustement
linéaire est quantitativement très mauvais par rapport au reste des modèles,
tandis que le polynôme de degré six est clairement le gagnant en termes
d’ajustement aux points de données.
Mais est-ce que le « meilleur ajustement » donne en fait le meilleur modèle ?
Nous devons tenir compte des notions de surapprentissage et de sous-
apprentissage des données. Ici, le cas du modèle linéaire serait un bon exemple
d’un scénario de sous-apprentissage. Il est clair qu’il existe une certaine structure
dans les données qui n’est pas expliquée par un simple ajustement linéaire.
D’autre part, un modèle peut engendrer un surapprentissage s’il est trop
spécifique aux données qui ont été présentées, et qu’il offre peu de pouvoir
explicatif pour toute nouvelle donnée qui pourrait être injectée dans le système.
C’est le risque que vous courez en augmentant le degré des modèles
polynomiaux.
Qualité de l’ajustement aux données :
les risques d’un ajustement excessif
Nous venons de donner un exemple dans lequel nous essayions d’obtenir un
modèle qui corresponde le mieux à nos données. C’est un bon objectif à se fixer,
mais il faut faire attention à ne pas aller trop loin en cherchant à s’adapter
parfaitement aux données. Nous avons vu plus haut que l’ajustement linéaire à
une courbe de population n’est probablement pas le meilleur modèle pour ce
type de travail. En comparaison, un ajustement polynomial quadratique ou
cubique semble faire beaucoup mieux. Pourtant, est-ce que cela vaut la peine de
continuer à augmenter le degré de cet ajustement polynomial du modèle ? La
minimisation du résidu est-elle le seul objectif à poursuivre en termes de
sélection du meilleur modèle ?

Erreur quadratique moyenne


En statistique, l’erreur quadratique moyenne (RMSE) est un moyen quantifiable
de voir comment les valeurs prédites par notre modèle se comparent aux valeurs
réelles. Mathématiquement, la formule est donnée comme suit :

RMSE = √( (valeur prédite − valeur réelle)2 )

Pour évaluer les ajustements polynomiaux, vous pouvez effectuer une analyse
RMSE sur chacun d’entre eux. Vous comparez ensuite les erreurs résultantes et
vous sélectionnez celle qui fournit le résultat le plus bas. Pour y arriver, vous
avez besoin de nouvelles données qui ne se trouvent pas dans votre modèle. Pour
cela, nous allons utiliser les données du recensement de la population américaine
de 1980 à 2010 :

uspop.2020 <- data.frame(year = c(1980, 1990, 2000,


2010), uspop = c(226.5,
249.6, 282.2, 309.3))
uspop.2020.predict <- uspop.2020

pop2 <- data.frame(uspop)


pop2$uspop <- as.numeric(pop$uspop)
pop2$year <- seq(from = 1790, to = 1970, by = 10)
Ce code réinitialise également les anciennes données de population à des fins de
prédiction afin d’effectuer un « nettoyage » général. À partir de là, vous pouvez
lancer votre routine de prédiction habituelle, puis calculer la valeur RMSE pour
chaque polynôme :

uspop.2020.predict$lm1 <- predict(lm(uspop ~ poly(year,


1), data = pop2),
uspop.2020)
uspop.2020.predict$lm2 <- predict(lm(uspop ~ poly(year,
2), data = pop2),
uspop.2020)

uspop.2020.predict$lm3 <- predict(lm(uspop ~ poly(year,


3), data = pop2),
uspop.2020)

uspop.2020.predict$lm4 <- predict(lm(uspop ~ poly(year,


4), data = pop2),
uspop.2020)

uspop.2020.predict$lm5 <- predict(lm(uspop ~ poly(year,


5), data = pop2),
uspop.2020)

uspop.2020.predict$lm6 <- predict(lm(uspop ~ poly(year,


6), data = pop2),
uspop.2020)

Reste à calculer l’erreur RMSE :

c(sqrt(mean((uspop.2020.predict$uspop -
uspop.2020.predict$lm1)^2)),
sqrt(mean((uspop.2020.predict$uspop -
uspop.2020.predict$lm2)^2)),
sqrt(mean((uspop.2020.predict$uspop -
uspop.2020.predict$lm3)^2)),
sqrt(mean((uspop.2020.predict$uspop -
uspop.2020.predict$lm4)^2)),
sqrt(mean((uspop.2020.predict$uspop -
uspop.2020.predict$lm5)^2)),
sqrt(mean((uspop.2020.predict$uspop -
uspop.2020.predict$lm6)^2)))

## [1] 75.622445 8.192311 5.070814 9.153189


73.632318 124.429798

D’après ces résultats, vous pouvez voir que l’ajustement linéaire simple avait
une valeur RMSE de 75, le polynôme du second degré avait une erreur de 8, et
celui du troisième degré de 5. Au-delà, les erreurs explosent, ce qui est une autre
indication du fait que les modèles ont été trop bien adaptés aux données,
autrement qu’il y a surapprentissage. Dans ce cas, vous sélectionneriez le
modèle qui a la valeur RMSE la plus faible pour les nouvelles données prédites,
autrement dit le polynôme du degré trois.

Simplicité du modèle et qualité de l’ajustement


Si vous vous rappelez les coefficients du modèle, chacun d’eux a une p-valeur de
significativité statistique qui lui est attachée depuis la procédure d’ajustement du
modèle lm(). Si la p-valeur d’un coefficient est inférieure à 0.05, il est sûr de
supposer qu’il est statistiquement important pour votre modèle.
Pour vous aider à choisir le modèle à utiliser, identifiez où se situe le compromis
entre l’exactitude du modèle et la complexité de celui-ci. Plus votre modèle est
complexe (au sens du degré polynomial utilisé), plus il s’ajustera étroitement à
vos données, mais plus vous courez aussi le risque que certains des coefficients
deviennent statistiquement moins valides. Pour éviter cela, examinez d’abord la
valeur R-carré et le nombre de coefficients statistiquement valides pour chacun
de vos modèles :

table((summary(lm1)$coefficients[, 4]) < 0.05)

##
## TRUE
## 2

summary(lm1)$r.squared

## [1] 0.9223434

Cet exemple prend les coefficients de l’ajustement linéaire simple, lm1, puis il
extrait les p-valeurs liées aux coefficients. Il présente ensuite sous forme
tabulaire le nombre d’entre eux qui sont statistiquement valides (donc inférieurs
à 0.05). Pour le cas linéaire simple, il y a deux coefficients : la pente et
l’ordonnée à l’origine, qui sont tous deux statistiquement valides. La valeur R-
carré confirme également que l’ajustement est assez bon. Utilisons cela comme
base de comparaison.
Au lieu d’effectuer ces calculs pour chaque modèle et de regarder les résultats
dans tous les sens, vous pouvez lister toutes ces informations dans un data frame
qui offrira une meilleure lisibilité. Définissons un objet model.order comme
étant le plus haut degré d’ajustement polynomial (il s’agit simplement du
nombre que vous passez à la fonction poly() pendant l’appel de fonction du
modèle linéaire lm()). Vous définissez ensuite coef.true comme représentant le
nombre de coefficients statistiquement valides dans le modèle.
Dans ce cas, vous ne regardez que les coefficients liés aux variables
dépendantes, et non l’ordonnée à l’origine du modèle, qui est statistiquement
valide dans tous les cas, d’où la raison pour laquelle vous soustrayez 1 à la
valeur coef.true. Vous définissez ensuite un terme coef.false pour représenter la
situation inverse : combien de coefficients du modèle sur les variables
dépendantes ne sont pas statistiquement significatifs. Enfin, vous définissez une
valeur model.rsq, qui est l’exactitude R-carré extraite du modèle. Vous
rassemblez ensuite le tout dans un data frame et vous définissez une métrique
finale : goodness. Celle-ci compare le rapport entre les coefficients
statistiquement significatifs et l’ordre du modèle :

model.order <- c(1,2,3,4,5,6)

coef.true <- c(
table((summary(lm1)$coefficients[,4])<0.05) - 1
,table((summary(lm2)$coefficients[,4])<0.05) - 1
,table((summary(lm3)$coefficients[,4])<0.05)[2] - 1
,table((summary(lm4)$coefficients[,4])<0.05)[2] - 1
,table((summary(lm5)$coefficients[,4])<0.05)[2] - 1
,table((summary(lm6)$coefficients[,4])<0.05)[2] - 1
)

coef.false <- c(
0
,0
,table((summary(lm3)$coefficients[,4])<0.05)[1]
,table((summary(lm4)$coefficients[,4])<0.05)[1]
,table((summary(lm5)$coefficients[,4])<0.05)[1]
,table((summary(lm6)$coefficients[,4])<0.05)[1]
)

model.rsq <- c(
summary(lm1)$r.squared
,summary(lm2)$r.squared
,summary(lm3)$r.squared
,summary(lm4)$r.squared
,summary(lm5)$r.squared
,summary(lm6)$r.squared
)

model.comparison <- data.frame(model.order, model.rsq,


coef.true, coef.false)
model.comparison$goodness <- (model.comparison$coef.true
/ model.comparison
$model.order)

model.comparison

## model.order model.rsq coef.true coef.false goodness


## 1 1 0.9223434 1 0 1.0000000
## 2 2 0.9982808 2 0 1.0000000
## 3 3 0.9983235 2 1 0.6666667
## 4 4 0.9984910 2 2 0.5000000
## 5 5 0.9992208 3 2 0.6000000
## 6 6 0.9993027 3 3 0.5000000

Ce résultat démontre que, même si l’exactitude R-carré du modèle augmente à


mesure que l’ajustement devient plus complexe, la qualité de cet ajustement
diminue avec le temps, car le nombre de coefficients statistiquement significatifs
par rapport au nombre total de coefficients tend à diminuer. Une façon de
quantifier statistiquement ce phénomène consiste à classer les éléments associés
que vous souhaitez optimiser :

model.comparison$rank <- sqrt(model.comparison$goodness^2


+
model.comparison$model.rsq^2)
model.comparison

## model.order model.rsq coef.true coef.false goodness


rank
## 1 1 0.9223434 1 0 1.0000000
1.360411
## 2 2 0.9982808 2 0 1.0000000
1.412998
## 3 3 0.9983235 2 1 0.6666667
1.200456
## 4 4 0.9984910 2 2 0.5000000
1.116685
## 5 5 0.9992208 3 2 0.6000000
1.165522
## 6 6 0.9993027 3 3 0.5000000
1.117410

Vous pouvez maintenant voir où se situe le meilleur compromis entre exactitude


du modèle et qualité de l’ajustement. Dans ce cas, il s’agit de l’ajustement
quadratique (ordre 2) dont tous les coefficients sont statistiquement valides. Bien
que l’ajustement du modèle pour un polynôme du troisième degré soit
légèrement meilleur (quoique la différence soit presque non mesurable), la
qualité de l’ajustement n’est pas aussi grande, car nous avons alors un coefficient
qui n’est pas statistiquement significatif.
Ce que nous pouvons dire au sujet de cette procédure, c’est que nous possédons
un modèle optimal à choisir, celui qui a la valeur rank la plus élevée. Un modèle
qui a des valeurs R-carré et rank basses est sous-ajusté aux données. Un modèle
qui a un R-carré plus élevé et une valeur rank plus basse est à l’inverse victime
de surapprentissage.
Régression logistique
Jusqu’à présent, nous avons considéré les modèles de régression en termes
d’adaptation d’une courbe à des séries de données numériques afin de pouvoir
l’utiliser à des fins prédictives. La régression linéaire reçoit en entrée certaines
données numériques, et renvoie en sortie une équation du genre y = mx + b. La
régression linéaire peut aussi avoir plusieurs entrées, et il serait donc possible
d’avoir une équation du genre y = b + m1x1 + m2x2 + (....). De plus, ces types de
modèles de régression numérique peuvent être transformés en modèles non
linéaires tels que y = b + m1x1 + m2x12 + m3x13+ (....). Tous ont leurs propres cas
d’utilisation et dépendent totalement des données avec lesquelles nous
travaillons et de la manière dont nous élaborons des stratégies concernant le type
d’exactitude que nous voulons optimiser.
Jusqu’à présent, tous ces éléments ont ingéré des entrées numériques et nous ont
donné une sortie numérique. Mais si, au lieu de cela, nous voulions un résultat
dans le style « oui » ou « non » à partir de nos données ? Et si nous voulions par
exemple déterminer si nos données d’entrée étaient positives ou négatives ?
Dans ce cas, nous partirions de données numériques continues et nous
obtiendrions une certaine espèce de sortie discrète. C’est la base du versant
classification de notre modèle de régression. La régression logistique est un type
particulier de classification, et de surcroît relativement simple à présenter à
l’aide d’un exemple. La régression logistique, à la différence de la régression
linéaire, trouve le point auquel les données passent d’une classe à une autre, au
lieu d’essayer d’ajuster tous les points de données individuels eux-mêmes.

La motivation pour la classification


Supposons que vous essayez d’effectuer un diagnostic sur des patients pour
déterminer s’ils ont une tumeur maligne. Examinons le code suivant et le tracé
qui en résulte sur la Figure 4.7 :

data <- data.frame(tumor.size <- c(1, 2, 3, 4, 5, 6, 7,


8, 9,
20), malignant <- c(0, 0, 0, 0, 1, 1, 1, 1, 1, 1))

tumor.lm <- lm(malignant ~ tumor.size, data = data)


plot(y = data$malignant, x = data$tumor.size, main =
"Malignité de la tumeur
par taille",
ylab = "Type (0 = bénin, 1 = cancéreux)", xlab =
"Taille de la tumeur")

abline(a = coef(tumor.lm[1]), b = coef(tumor.lm[2]))


coef(tumor.lm)

## (Intercept) tumor.size
## 0.20380952 0.06095238

summary(tumor.lm)$r.squared

## [1] 0.4063492

Figure 4.7 : L’ajustement d’une droite de régression linéaire à des données binaires ne fournit pas un
modèle exact.

Ce code crée un jeu de données de tailles de tumeurs allant de 1 à 20, et il les


classifie de manière binaire : une tumeur bénigne ou non cancéreuse est
étiquetée comme valant 0, et une tumeur maligne ou cancéreuse est étiquetée à la
valeur l. Une tentation naïve pourrait être d’appliquer un modèle de régression à
ces données et de voir quelle est l’équation résultante. Avec cette approche, vous
obtiendriez une équation comme celle-ci :
malignité = 0.061 × taille tumeur + 0.204

Le mauvais ajustement du R-carré à la valeur 0.406 suggère que nous pourrions


obtenir un modèle plus précis. De plus, vous devriez remettre en question
l’évaluation logique de ce que signifie avoir une tumeur maligne de 0.2, alors
que les données sont enregistrées comme étant malignes ou bégnines, sans rien
entre les deux. Cela n’aurait pas non plus de sens avec le jeu de données mtcars
et ses types de transmission ! Que signifierait une transmission 0.2, si 0 signifie
manuelle et 1 automatique ?
Nous devons repenser cette approche. Au lieu d’ajuster une fonction
mathématique normale, nous devons ajuster aux données ce qu’on appelle une
frontière de décision.

La frontière de décision
La frontière de décision est simplement une ligne tracée dans le sable de nos
données, et qui dit que « tout ce qui se trouve de ce côté est classé X et tout ce
qui se trouve de l’autre côté est classé Y ». La Figure 4.8 revisite le graphique
des tailles de tumeurs, et revient sur la question de savoir s’il s’agit de tumeurs
malignes. Vous pouvez clairement voir que toute tumeur de taille supérieure
à 5 semble toujours être maligne :

plot(y = data$malignant, x = data$tumor.size, main =


"Malignité de la tumeur
par taille",
ylab = "Type (0 = bénin, 1 = cancéreux)", xlab = "Taille
de la tumeur")
abline(v = 4.5)
Figure 4.8 : Le tracé d’une frontière de décision au lieu d’une droite de régression classifie les données
inférieures à environ 4.5 comme étant 0 ; les données supérieures à ce seuil sont classifiées comme étant 1.

La régression logistique établit la frontière par rapport à laquelle vous pouvez


classifier les données. La frontière de la Figure 4.8 montre que toute taille de
tumeur supérieure à 4.5 est classifiée comme étant maligne, alors que toute
tumeur de taille inférieure à 4.5 est classifiée comme étant bénigne.

La fonction sigmoïde
La manière dont la régression logistique (ainsi que de nombreux autres types
d’algorithmes de classification) fonctionne est basée sur les fondements
mathématiques de la fonction sigmoïde. La fonction sigmoïde prend la forme
mathématique suivante :

La Figure 4.9 montre à quoi ressemble le tracé de cette fonction :

e <- exp(1)
curve(1/(1 + e^-x), -10, 10, main = "La fonction
Sigmoïde",
xlab = "Entrée", ylab = "Probabilité")

Figure 4.9 : La fonction sigmoïde est la base de la régression logistique.

Cette fonction est utilisée dans la régression logistique pour classifier les
données. En soi, la fonction prend une valeur numérique qui nous intéresse, et
elle la fait correspondre avec une probabilité comprise entre 0 et l. Nous
pourrions être tentés de simplement « brancher » certaines des valeurs de notre
exemple précédent dans la fonction sigmoïde et de voir quelle est la sortie
produite. Si nous le faisions, avec x = l par exemple, nous obtiendrions h(1)
= 0.73, soit environ 73 % de chances qu’une tumeur soit maligne si notre entrée
est l. Pourtant, notre système de classification est basé sur les valeurs 0 pour les
tumeurs bénignes et 1 pour les tumeurs malignes. La taille 1 en entrée donne ici
un résultat de 0.73, ce qui est incorrect.
Au lieu de cela, nous devons transmettre un ensemble de paramètres pondérés au
régresseur logistique. Comme nous n’avons qu’une seule variable dépendante à
l’heure actuelle (en gardant à l’esprit que l’axe des y pour notre sortie de
classification n’est pas une variable d’entrée), nous devrions nous attendre à
passer à notre régresseur logistique une fonction ayant la forme suivante :
g(longueur) = θ0 + θ1longueur

A priori, nous ne savons pas encore quels sont les poids. Ce que nous voulons,
c’est qu’ils soient choisis de telle sorte que notre fonction g(x), lorsqu’elle sera
passée à la fonction sigmoïde, nous donne une classification qui semblera
raisonnable par rapport à ce que nous voyons dans nos données :

lengths <- c(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)


t1 = -4.5
t2 = 1
g = t1 + t2 * lengths
s = 1/(1 + e^-g)
data.frame(lengths, g, s)

## lengths g s
## 1 1 -3.5 0.02931223
## 2 2 -2.5 0.07585818
## 3 3 -1.5 0.18242552
## 4 4 -0.5 0.37754067
## 5 5 0.5 0.62245933
## 6 6 1.5 0.81757448
## 7 7 2.5 0.92414182
## 8 8 3.5 0.97068777
## 9 9 4.5 0.98901306
## 10 10 5.5 0.99592986

Ce morceau de code prend des longueurs de tumeurs en entrée, en allant


de 1 à 10, et choisit deux poids, respectivement θ0 = 4.5 et θ1 = l. Dans la
pratique, il faudrait soit expérimenter en choisissant des valeurs pour les poids et
en regardant ce que donnent les sorties, soit faire appel à un algorithme qui vous
donne la réponse. Le code précédent fournit cette réponse en tant que résultat
final. Ces valeurs sont ensuite utilisées comme poids pour la fonction g(x) qui est
alors transmise au sigmoïde. Le tableau dans le code présente la classification
résultante des données dans la colonne s. Une tumeur de longueur l, lorsqu’elle
est transmise à la fonction d’entrée g(x), donne un résultat de -3.5. Cette valeur,
une fois passée à la fonction sigmoïde, donne un résultat assez proche de zéro.
Cela signifie donc qu’une tumeur de longueur 1 a une très faible probabilité
d’être maligne, comme le montre la Figure 4.10 :

plot(y = s, x = lengths, pch = 1, main = "Entrées de la


fonction sigmoïde
et estimations des arrondis",
xlab = "Longueurs des tumeurs", ylab = "Probabilité
de classification 1")

points(y = round(s), x = lengths, pch = 3)

Figure 4.10 : Pour une longueur donnée en entrée, vous pouvez voir l’estimation de la fonction sigmoïde
(les cercles), et les valeurs des arrondis (les croix).

La Figure 4.10 présente les probabilités que les longueurs de tumeurs soient
classifiées comme étant malignes si cette probabilité est de 1.0 et bénignes si elle
est de 0.0. Le résultat est assez cohérent, mais il y a tout de même quelques
erreurs. Vous obtiendriez une bien meilleure image si vous arrondissiez
simplement les valeurs au nombre entier le plus proche. Le résultat final est une
classification qui ressemble exactement aux données de départ.
Nous sommes partis de la longueur des tumeurs comme données d’entrée. La
sortie du type de tumeur entre bénin, y = 0, et malin, y = 1, nous était déjà
connue. L’objectif était de concevoir un modèle qui calcule la probabilité qu’une
tumeur soit bénigne ou maligne en fonction de sa longueur. Nous avons procédé
en commençant avec l’équation g(x) = θ0 + θ1x, puis en trouvant les poids θ0 et
θ1, ce qui a aidé à obtenir des valeurs qui, lorsqu’elles sont envoyées à une
fonction sigmoïde, fournissent des résultats qui correspondent à ce dont nous
avions besoin. Ce que nous obtenons à la fin est une frontière de décision placée
à la longueur 4.5. Toutes les valeurs supérieures sont classifiées comme l, et
toutes les valeurs inférieures sont classifiées comme 0.
Les mécanismes par lesquels les algorithmes de classification, comme la
régression logistique, fonctionnent pour déterminer les poids de modélisation
sont quelque peu semblables à la façon dont ces poids sont calculés dans une
régression linéaire simple.
La régression logistique et de nombreux autres algorithmes d’apprentissage
automatique travaillent de manière semblable, mais nous ne nous plongerons pas
trop profondément dans le domaine de l’optimisation des algorithmes afin de ne
pas perdre de vue la compréhension et l’application pratique de l’écosystème de
l’apprentissage automatique dans sa globalité.

Classification binaire
Tout ce que nous avons fait jusqu’à présent en termes de classification a porté
sur des données binaires : la tumeur est maligne ou elle est bénigne. La
Figure 4.11 présente un autre exemple dans lequel nous déterminons les classes
en fonction de la distribution des données :

plot(iris$sepallength ~ iris$sepalwidth,
main = "Iris : Longueur de sépale vs largeur de sépale",
xlab = "Largeur de sépale", ylab = "Longueur de
sépale")
Figure 4.11 : Vous pouvez utiliser la régression logistique pour des données en sortie qui ne sont pas
discrètes.

Sur la Figure 4.11, il y a de nombreux points de données, ce qui donne


l’impression de former deux classes différentes de plantes. Il semble aussi y
avoir un regroupement de points de données au bas du tracé qui semblent être
plus éloignés que les autres. Vous pouvez ajuster un modèle de régression
logistique sur ces données et trouver l’équation de la droite qui détermine votre
frontière de décision. Tous les points en dessous de ce seuil seront classifiés
comme formant un type, et tous les points au-dessus de la droite seront classifiés
comme formant un autre type. Cet exercice utilise un modèle linéaire généralisé,
donné par la fonction glm(). Son utilisation est plus souple que celle de la
fonction du modèle linéaire standard, lm(), en ce sens que vous pouvez l’utiliser
à des fins de classification :

iris.binary <- iris


iris.binary$binary <- as.numeric(iris[, 5] == "Iris-
setosa")

iris.logistic <- glm(binary ~ sepalwidth + sepallength,


data = iris.binary,
family = "binomial")
iris.logistic

##
## Call: glm(formula = binary ~ sepalwidth +
sepallength, family = "binomial",
## data = iris.binary)
##
## Coefficients:
## (Intercept) sepalwidth sepallength
## 437.3 137.9 -163.4
##
## Degrees of Freedom: 149 Total (i.e. Null); 147
Residual
## Null Deviance: 191
## Residual Deviance: 2.7e-08 AIC: 6

Le résultat de cette méthode fournit des coefficients et des ordonnées à l’origine


qui ne semblent pas être tout à fait justes. Vous avez besoin d’une étape
supplémentaire pour calculer la pente et l’ordonnée à l’origine de la frontière de
décision de cette manière. Rappelez-vous un instant comment vous avez utilisé
la fonction sigmoïde g(z) = 1/(1 + ez).
z est une fonction ayant la forme suivante :
z = θ0 + θ1x1 + θ2x2

Puisque vous voulez une valeur entre 0 et 1 pour une classification binaire, cette
classification est 1 lorsque votre fonction sigmoïde g(z) est supérieure ou égale
à 0.5. Cela n’est vrai que lorsque la fonction z que vous lui passez est elle-même
supérieure ou égale à 0 :
0 ≤ θ0 + θ1x1 + θ2x2

Vous pouvez réécrire cette équation et la résoudre pour notre valeur x2 :

Cette équation est de la même forme que la droite y = b + mx, où l’on peut
trouver par le calcul la pente et l’ordonnée à l’origine afin de construire la
fonction qui détermine la frontière de décision :
pente = −x1/x2 = −137.9/163.4

interception = −b/x2 = −437.2/163.4

Vous pouvez calculer cela directement à partir de l’objet modèle logistique :

slope.iris <- coef(iris.logistic)[2]/(-


coef(iris.logistic)[3])
int.iris <- coef(iris.logistic)[1]/(-coef(iris.logistic)
[3])

slope.iris

## sepalwidth
## 0.8440837

int.iris

## (Intercept)
## 2.675531

Vous pouvez alors tracer ce graphique à partir de vos données, et voir comment
les classes se répartissent, comme l’illustre la Figure 4.12 :

iris.binary$binary[iris.binary$binary == 0] <- 2

plot(sepallength ~ sepalwidth, data = iris.binary, pch =


(binary),
main = "Iris : Longueur de sépale vs largeur de
sépale",
xlab = "Largeur de sépale", ylab = "Longueur de
sépale")

abline(a = int.iris, b = slope.iris)


Figure 4.12 : Partager une distribution de données en deux classes.

Vous avez maintenant une équation qui vous aide à déterminer comment séparer
nos espèces de fleurs d’iris. Toutes les fleurs du jeu de données qui ont une
valeur tombant sous la droite y = 2.68 + 0.844 x (sepalwidth) seront classifiées
en conséquence.

Classification multiclasse
Si vous voulez trouver les séparations dans vos données qui définissent plusieurs
classes, et pas seulement une classification binaire, vous devez utiliser une
classification multiclasse. Cette approche est légèrement différente dans la
mesure où vous appliquez fondamentalement le même schéma de classification
binaire que précédemment, mais, ici, vous comparez la classe qui vous intéresse
à tout le reste.
La Figure 4.13 illustre à quoi pourrait ressembler un exercice de classification
multiclasse :

multi <- data.frame(x1 = c(0.03, 0.24, 0.21, 0, 0, 0.23,


0.6,
0.64, 0.86, 0.77), x2 = c(0.07, 0.06, 0.19, 1.15,
0.95, 1,
0.81, 0.64, 0.44, 0.74), lab = c(1, 1, 1, 2, 2, 2, 3,
3,
3, 3))

plot(x2 ~ x1, pch = lab, cex = 2, data = multi,


main = "Classification multiclasse",
xlab = "x", ylab = "y")

Figure 4.13 : Les données multiclasses ne peuvent être scindées qu’en utilisant une approche « un contre
tous ».

Ici, il y a trois classes distinctes de données, et vous voulez trouver des sortes de
droites qui les divisent selon leurs propres catégories, un peu comme vous l’avez
fait pour le cas binaire. En fait, cela revient essentiellement au même que pour
notre simple test binaire, si ce n’est que vous changez le groupe avec lequel vous
effectuez la comparaison. C’est ce qu’on appelle un test « un contre tous » ou
encore « un contre plusieurs » (également One vs All ou OvA en abrégé). Dans
notre exemple, vous testez trois cas : triangles contre le reste, cercles contre le
reste et croix contre le reste, comme l’illustre la Figure 4.14 :
par(mfrow = c(1, 3))
multi$lab2 <- c(1, 1, 1, 4, 4, 4, 4, 4, 4, 4)
plot(x2 ~ x1, pch = lab2, cex = 2, data = multi,
main = "Classification multiclasse",
xlab = "x", ylab = "y")

multi$lab3 <- c(4, 4, 4, 2, 2, 2, 4, 4, 4, 4)


plot(x2 ~ x1, pch = lab3, cex = 2, data = multi,
main = "Classification multiclasse",
xlab = "x", ylab = "y")

multi$lab4 <- c(4, 4, 4, 4, 4, 4, 3, 3, 3, 3)


plot(x2 ~ x1, pch = lab4, cex = 2, data = multi,
main = "Classification multiclasse",
xlab = "x", ylab = "y")

Figure 4.14 : Une classification « un contre tous » utilise la même approche que la classification standard,
mais elle reclassifie les autres groupes en un seul aux fins de comparaison.

Dans une approche de classification « un contre tous », vous utilisez une


frontière de décision pour classifier les données d’un type ou d’une classe par
rapport à tous les autres types ou classes. Vous faites ensuite la même chose pour
les autres types ou classes de données, et ce jusqu’à ce que vous disposiez d’un
certain nombre de frontières de décision que vous pouvez utiliser pour
caractériser vos données en conséquence. Ainsi, pour l’exemple de la
Figure 4.14, vous calculez (sur le tracé de gauche) les cercles par rapport au reste
des données, puis vous calculez les triangles par rapport au reste des données
(tracé du milieu), et enfin les croix par rapport au reste des données (tracé de
droite). En divisant un problème à trois classes en trois problèmes à deux
classes, vous pouvez plus facilement trouver une seule frontière de décision pour
chaque tracé et ensuite combiner ces frontières pour construire un modèle final.
Ici, vous allez faire appel à la fonction multinom() de la bibliothèque nnet. Vous
l’utiliserez pour passer un cas multinomial, qui est fondamentalement le même
que vous avez réalisé pour le cas binaire simple, mais avec trois valeurs au lieu
de deux. Cette méthodologie peut également être appliquée à plus de trois
catégories :

library(nnet)
multi.model <- multinom(lab ~ x2 + x1, data = multi,
trace = F)

Notez que vous avez deux droites pour séparer les trois catégories :

multi.model

## Call:
## multinom(formula = lab ~ x2 + x1, data = multi, trace
= F)
##
## Coefficients:
## (Intercept) x2 x1
## 2 -12.47452 28.50805 -17.97523
## 3 -19.82927 12.95949 33.39610
##
## Residual Deviance: 0.0004050319
## AIC: 12.00041

Une nouvelle fois, cependant, vous devez effectuer un calcul spécial pour les
pentes et les ordonnées à l’origine des frontières de décision fondées sur les
résultats de ce modèle. Vous pouvez appliquer les mêmes opérations
mathématiques que précédemment, mais, cette fois, à chacune des équations du
modèle. Vous pouvez ensuite tracer les frontières de décision, comme l’illustre la
Figure 4.15 :

multi.int.1 <- -coef(multi.model)[1]/coef(multi.model)[3]


multi.slope.1 <- -coef(multi.model)[5]/coef(multi.model)
[3]

multi.int.2 <- -coef(multi.model)[2]/coef(multi.model)[4]


multi.slope.2 <- -coef(multi.model)[6]/coef(multi.model)
[4]

plot(x2 ~ x1, pch = lab, cex = 2, data = multi,


main = "Classification multiclasse",
xlab = "x", ylab = "y")
abline(multi.int.1, multi.slope.1)
abline(multi.int.2, multi.slope.2)

Figure 4.15 : Deux lignes séparent trois classes de données.

Régression logistique avec caret


Le package caret permet de traiter des problèmes de régression logistique très
facilement avec des exemples plus complexes que ce que nous avons vu jusqu’à
présent. L’utilisation de caret est assez simple, bien que, pour certaines méthodes
d’apprentissage automatique particulières, certaines optimisations et certains
réglages peuvent être nécessaires pour obtenir les meilleurs résultats possibles.
Voici un exemple de la manière dont vous pouvez effectuer une opération avec
caret :

library(caret)

data("GermanCredit")

Train <- createDataPartition(GermanCredit$Class, p = 0.6,


list = FALSE)
training <- GermanCredit[Train, ]
testing <- GermanCredit[-Train, ]

mod_fit <- train(Class ~ Age + ForeignWorker +


Property.RealEstate +
Housing.Own + CreditHistory.Critical, data =
training, method = "glm",
family = "binomial")
predictions <- predict(mod_fit, testing[, -10])
table(predictions, testing[, 10])

##
## predictions Bad Good
## Bad 3 0
## Good 117 280

Cet exemple simple se sert d’un jeu de données intégré appelé Germancredit, et
il montre comment vous pouvez construire une matrice de confusion à partir
d’un objet d’entraînement caret. Dans ce cas, l’ajustement ne paraît pas être très
bon, car une bonne partie des données semble être classifiée incorrectement.
Bien que caret offre d’excellents outils pour ajuster n’importe quelle méthode
particulière d’apprentissage automatique qui vous intéresse, il est également
assez souple pour passer d’une méthode à une autre. En éditant simplement
l’option method dans le code ci-dessus, vous pouvez spécifier l’un des douze
autres algorithmes de régression logistique qu’il est possible de passer au
modèle, comme l’illustre ce code :
mod_fit <- train(Class ~ Age + ForeignWorker +
Property.RealEstate +
Housing.Own + CreditHistory.Critical, data =
training,
method = "LogitBoost",
family = "binomial")

predictions <- predict(mod_fit, testing[, -10])


table(predictions, testing[, 10])

##
## predictions Bad Good
## Bad 1 2
## Good 119 278
En résumé
Dans ce chapitre, nous avons examiné plusieurs manières différentes de
construire des modèles de base, entre régression linéaire simple et régression
logistique.

Régression linéaire
La régression se présente sous deux formes : la régression linéaire standard, que
vous avez peut-être rencontrée lors de vos cours de mathématiques, et la
régression logistique, qui est très différente. R peut facilement créer un modèle
linéaire en utilisant la fonction lm(). En employant celle-ci en tandem avec
l’opérateur de formule ~, vous pouvez construire une équation de régression
simple y = mx + b en écrivant quelque chose comme lm(y ~ x). Partant de cet
objet de modèle linéaire, vous pouvez extraire une mine d’informations
concernant les coefficients, la validité statistique et l’exactitude. Il suffit pour
cela de faire appel à la fonction summary(), qui peut vous dire à quel point
chaque coefficient de votre modèle est statistiquement valide. Ceux qui ne sont
pas utiles d’un point de vue statistique peuvent être retirés de votre modèle en
toute sécurité.
Les modèles de régression peuvent être plus avancés s’ils possèdent davantage
de caractéristiques. Vous pouvez modéliser des comportements comme le
rendement énergétique en fonction du poids d’un véhicule, mais vous pouvez
aussi incorporer plus de choses dans votre modèle, comme le type de
transmission d’une voiture, le nombre de cylindres du moteur, et ainsi de suite.
La modélisation d’une régression multivariée dans R suit la même démarche que
pour une régression portant sur une unique caractéristique. La seule différence
est qu’il y a davantage de caractéristiques listées dans la vue summary() de votre
modèle.
Cependant, le simple fait d’ajouter plus de caractéristiques à un modèle ne lui
procure pas par défaut davantage d’exactitude. Vous pourriez avoir besoin
d’employer des techniques comme la régularisation, qui prend un jeu de données
possédant beaucoup de caractéristiques et qui réduit l’impact de celles qui ne
sont pas statistiquement importantes. Cela peut vous aider à simplifier
considérablement votre modèle ainsi qu’à améliorer l’évaluation de l’exactitude.
Parfois, il peut y avoir des relations non linéaires entre les données qui
nécessitent des ajustements polynomiaux. Un modèle linéaire régulier est de la
forme y = b + m1x1 + m2x2 + (...), alors qu’un modèle polynomial pourrait avoir
pour forme y = m1x12 + m2x1 + m3x22 + m4x2 + (...). Vous pouvez ajuster le
comportement des polynômes à vos modèles en passant une fonction poly() à la
fonction lm(), comme par exemple dans lm(y~poly(x,2)). Cela crée une relation
quadratique dans les données. Cependant, il est important de ne pas se lancer
dans une course folle avec les degrés polynomiaux, car vous courez alors le
risque d’ajuster vos données si étroitement que toute nouvelle donnée entrante
pourrait se voir pénalisée par des estimations d’erreurs élevées et trompeuses.

Régression logistique
Dans l’apprentissage automatique, il y a les techniques de régression standard,
qui estiment des valeurs continues telles que des nombres, et les techniques de
classification, qui estiment des valeurs discrètes telles que des types de données.
Bien souvent, les données peuvent effectivement présenter des valeurs discrètes
comme dans le cas de variétés de fleurs. Si vous essayez les méthodes de
régression linéaire standard sur ces jeux de données, vous vous retrouverez avec
des relations extrêmement faussées.
La régression logistique est une méthode de classification qui trouve une
frontière séparant les données en classes discrètes. Pour cela, elle passe les
données à travers une fonction sigmoïde qui fait correspondre la valeur réelle de
ces données à une situation binaire, autrement dit 1 ou 0. Ce résultat est ensuite
transmis à une autre équation qui produit des pondérations assignant des
probabilités aux données. Vous pouvez utiliser ces résultats pour déterminer la
probabilité pour qu’un point de donnée appartienne à une certaine classe.
Vous pouvez également vous servir de la régression logistique pour tracer des
frontières de décision dans vos données. Une frontière de décision est une droite
qui ne s’ajuste pas nécessairement aux données dans un tracé (x, y) standard,
mais qui s’ajuste à des ouvertures dans les données pour les scinder en classes
spécifiques. Dans le cas de données pour lesquelles vous avez deux classes, vous
auriez une unique droite qui les divise en une classe 1 et une classe 2.
Dans le cas de classes multiples, vous traitez chaque classe selon une approche
« un contre plusieurs ». Avec trois classes, par exemple, vous vous concentrez
sur l’une, vous regroupez les deux autres, et vous trouvez la frontière de décision
qui les sépare. Vous recommencez ensuite pour la classe suivante. À la fin, vous
devriez obtenir une série de frontières de décision qui scindent les données en
zones distinctes. Toute donnée présente dans une zone particulière est classifiée
de la même manière que tous les autres points de données se trouvant dans cette
zone.
CHAPITRE 5
Les réseaux de neurones en bref
Dans le Chapitre 2, nous avons brièvement abordé le sujet des réseaux de
neurones lors de notre exploration du paysage de l’apprentissage automatique.
Un réseau de neurones est un ensemble d’équations qui nous permettent de
calculer un résultat. Ils ne sont pas si effrayants que cela si on les considère
comme une sorte de cerveau fait de code informatique. Dans certains cas, cette
image est plus proche de la réalité que ce à quoi nous devrions attendre d’un
exemple aussi caricatural. Selon le nombre de caractéristiques que comportent
nos données, le réseau de neurones devient presque une « boîte noire ». En
principe, on peut afficher les équations qui composent un réseau de neurones,
mais, arrivé à un certain niveau, la quantité d’informations mises en jeu devient
trop lourde pour que tout cela puisse rester (même relativement) intuitif.
Les réseaux de neurones sont utilisés sur une grande échelle dans l’industrie en
grande partie du fait de l’exactitude des résultats qu’ils permettent d’obtenir. Il y
a cependant parfois des compromis à trouver entre le fait d’avoir un modèle très
exact, mais en même temps des vitesses de calcul lentes. Par conséquent, il est
préférable d’essayer plusieurs modèles et d’utiliser les réseaux de neurones
uniquement s’ils fonctionnent pour votre jeu de données particulier.
Réseaux de neurones monocouches
Dans le Chapitre 2, nous avons examiné le développement d’une porte AND. Ce
type de porte suit une logique comme celle-ci :

x1 <- c(0, 0, 1, 1)
x2 <- c(0, 1, 0, 1)
logic <- data.frame(x1, x2)
logic$AND <- as.numeric(x1 & x2)
logic

## x1 x2 AND
## 1 0 0 0
## 2 0 1 0
## 3 1 0 0
## 4 1 1 1

Si vous avez deux entrées 1 (toutes les deux TRUE), votre sortie vaut 1 (TRUE).
Cependant, si l’une d’elles, ou les deux, sont 0 (FALSE), votre sortie est
également 0 (FALSE). Ce calcul est quelque peu similaire à notre analyse de la
régression logistique. Au Chapitre 4, nous avons étudié le fonctionnement de la
fonction sigmoïde. Rappelons que cette fonction est donnée par g(z) = 1/(l + e-z),
et que z est une fonction de la forme z = θ0 + θ1x1 + θ2x2.
Pour ce qui concerne la porte logique, tout ce que vous avez à faire est de choisir
des poids θ0, θ1 et θ2 de sorte que lorsque x1 = 1 et x2 = l, le résultat de z lorsque
vous le passez à la fonction sigmoïde g(z) soit également 1. Plus tôt, vous aviez
choisi des poids de valeur θ0 = 20, θ1 = 15, et θ2 = 17 pour satisfaire l’équation.
La façon dont le réseau de neurones calcule ces poids est un processus encore
plus mathématique, mais il s’agit du même type de logique que celle que nous
avons utilisée dans la régression logistique.
Les réseaux de neurones se rencontrent en de nombreuses et différentes
déclinaisons, mais les plus populaires se déclinent en versions monocouche ou
multicouche. Jusqu’à présent, vous avez vu un exemple de réseau avec une
couche unique, dans lequel nous prenons une certaine entrée, (1,0), nous la
traitons via une fonction sigmoïde, et nous obtenons une certaine sortie (0). Vous
pouvez, en fait, chaîner ces étapes de calcul pour former des modèles plus
interconnectés et complexes en prenant la sortie et en la faisant passer dans des
couches de calcul supplémentaires.
La Figure 5.1 présente un exemple de porte AND, mais cette fois avec du code
R :

library(neuralnet)

set.seed(123)
AND <- c(rep(0, 3), 1)
binary.data <- data.frame(expand.grid(c(0, 1), c(0, 1)),
AND)
net <- neuralnet(AND ~ Var1 + Var2, binary.data, hidden =
0,
err.fct = "ce", linear.output = FALSE)
plot(net, rep = "best")
Figure 5.1 : Un réseau de neurones simple.
Construire un réseau de neurones
simple en utilisant R
Avant d’en venir aux aspects mathématiques, commençons par décomposer la
visualisation présentée sur la Figure 5.1, qui est un diagramme d’un réseau de
neurones aussi simple que possible. Il y a une couche d’entrée (les cercles vides
à gauche) et une couche de sortie (le cercle vide à droite). Souvent, on trouve
également une autre rangée verticale de cercles qui signale une couche de calcul.
Ici, la couche de sortie est la couche de calcul. Les nombres sur les lignes
indiquent les meilleures pondérations calculées à utiliser pour le modèle. Le
nombre associé au « 1 » dans le cercle du haut est le poids du nœud de biais. Ce
nœud de biais est juste la constante additive pour la fonction sigmoïde que vous
avez utilisée dans les exemples de régression logistique. En un sens, il s’agit
donc d’une manière différente de représenter une analyse de régression
logistique sous une forme très simple de réseau de neurones. Le résultat final est
un schéma de classification pour les données qui ont pour étiquettes 1 ou 0.
Dans R, il n’y a vraiment qu’une seule bibliothèque spécialisée qui dispose
d’une fonctionnalité intégrée pour tracer des réseaux de neurones. En pratique,
ce tracé est la plupart du temps plus compliqué qu’il n’en vaut en fait la peine,
comme nous le montrerons plus tard. Dans les scénarios de modélisation
complexes, les diagrammes de réseaux de neurones et les mathématiques mises
en œuvre finissent par être si lourds que le modèle lui-même devient plus ou
moins une boîte noire entraînée. Si votre chef de projet vous demande
d’expliquer les mathématiques qui se dissimulent derrière un modèle de réseau
de neurones complexe, vous devrez peut-être bloquer un après-midi entier et
trouver le plus grand tableau blanc disponible dans l’immeuble.
La bibliothèque neuralnet possède cependant une fonctionnalité de traçage
intégrée et, dans l’exemple précédent, vous tracez le réseau de neurones qui a été
déterminé comme ayant ici l’erreur la plus faible. Le nombre d’étapes est la
quantité d’itérations qui se sont déroulées en arrière-plan pour ajuster cette sortie
particulière selon son erreur la plus basse.
Le code correspondant à la Figure 5.1 fait passer un tableau de données similaire
provenant de valeurs binaires à la fonction neuralnet(), qui provient du package
de même nom. Le résultat obtenu serait une équation ayant pour poids
θ0 = 11.86048, θ1 = 7.75382 et θ2 = 7.75519.
Ainsi, si votre chef est vraiment impatient de voir où en est votre procédure de
modélisation du réseau de neurones, vous pourrez faire un grand sourire et
expliquer que vous avez terminé et que le modèle est prêt à l’emploi. Et s’il vous
demande des détails sur le fonctionnement exact de la chose, expliquez-lui
qu’elle prend deux entrées, Var1 et Var2, et qu’elle les injecte dans l’équation :
z = −11.96048 + 7.75382V ar1 + 7.75382V ar2

Vous passez ensuite cette équation à travers une fonction sigmoïde g(z) = 1/(1 +
e-z) et vous obtenez une certaine sortie. En résumé, tout le processus
ressemblerait donc à ceci :

Vous pouvez vérifier la sortie de la fonction neuralnet() en utilisant la fonction


prediction() :

prediction(net)

## Data Error: 0;
## $'rep1'
## Var1 Var2 AND
## 1 0 0 0.000007064115737
## 2 1 0 0.016196147483124
## 3 0 1 0.016217878405446
## 4 1 1 0.974631032550725
##
## $data
## Var1 Var2 AND
## 1 0 0 0
## 2 1 0 0
## 3 0 1 0
## 4 1 1 1

Dans le premier tableau, vous trouvez les variables d’entrée et ce que le réseau
de neurones pense être la réponse correspondante. Comme vous pouvez le voir,
ces réponses sont assez proches de ce qu’elles devraient être, comme le montre
le tableau du listing précédent. Vous venez de construire avec succès un modèle
de réseau de neurones possédant une seule couche. Autrement dit, toutes les
entrées sont passées par un seul point de traitement, comme le montre la
Figure 5.1. Ces points de traitement sont presque toujours des fonctions
sigmoïdes. Dans de rares cas, on peut faire appel à une fonction tangente
hyperbolique, tanh(x), pour obtenir un résultat similaire.

Sorties de calcul multiples


Comme nous l’avons mentionné précédemment, les réseaux de neurones peuvent
recevoir plusieurs entrées et produire plusieurs sorties. Si, par exemple, vous
voulez modéliser deux fonctions via des réseaux de neurones, vous pouvez
utiliser l’opérateur de formule de R ~ et l’opérateur + pour simplement ajouter
une réponse supplémentaire à la gauche de l’équation pendant la modélisation,
comme le montre la Figure 5.2 :

set.seed(123)
AND <- c(rep(0, 7), 1)
OR <- c(0, rep(1, 7))
binary.data <- data.frame(expand.grid(c(0, 1), c(0, 1),
c(0,
1)), AND, OR)
net <- neuralnet(AND + OR ~ Var1 + Var2 + Var3,
binary.data,
hidden = 0, err.fct = "ce", linear.output = FALSE)
plot(net, rep = "best")
Figure 5.2 : Les réseaux de neurones peuvent être plus complexes que la régression logistique en termes de
sorties de calcul multiples.

Nous pouvons modéliser nos fonctions AND et OR à l’aide des deux équations
données par les sorties de la Figure 5.2 :
AND = 19.4 + 7.5V ar1 + 7.6V ar2 + 7.6V ar3

OR = 10.3 + 22.3V ar1 + 21.8V ar2 + 21.9V ar3

Nous pouvons contrôler notre sortie de la même manière qu’auparavant avec la


fonction prediction() :

prediction(net)
## Data Error: 0;
## $'rep1'
## Var1 Var2 Var3 AND OR
## 1 0 0 0 0.00000001045614851 0.00007220621224
## 2 1 0 0 0.00001426236484049 0.99999769205959
## 3 0 1 0 0.00001409371095155 0.99999191105328
## 4 1 1 0 0.01886199255844006 1.00000000000000
## 5 0 0 1 0.00001228339436300 0.99995455791699
## 6 1 0 1 0.01647909336278272 0.99999999999999
## 7 0 1 1 0.01628739761101993 0.99999999999997
## 8 1 1 1 0.95759917455105847 1.00000000000000
##
## $data
## Var1 Var2 Var3 AND OR
## 1 0 0 0 0 0
## 2 1 0 0 0 1
## 3 0 1 0 0 1
## 4 1 1 0 0 1
## 5 0 0 1 0 1
## 6 1 0 1 0 1
## 7 0 1 1 0 1
## 8 1 1 1 1 1

Nos réseaux de neurones semblent bien fonctionner !

Nœuds de calcul cachés


Jusqu’à présent, vous avez construit des réseaux de neurones qui n’ont pas de
couches cachées. En d’autres termes, la couche de calcul était la même que la
couche de sortie. Nous allons maintenant vous montrer comment l’ajout d’une
couche cachée de calcul peut aider à augmenter l’exactitude du modèle.
Les réseaux de neurones utilisent une notation abrégée pour définir leur
architecture, dans laquelle on note le nombre de nœuds d’entrée, suivi de deux-
points, puis le nombre de nœuds de calcul dans la couche cachée, un autre deux-
points, et enfin le nombre de nœuds de sortie. L’architecture du réseau de
neurones que nous avons construit sur la Figure 5.2 était de 3:0:1.
Une façon plus facile d’illustrer cela consiste à schématiser un réseau de
neurones possédant trois entrées, une couche cachée et une couche de sortie, soit
une architecture notée 3:1:1 (voir la Figure 5.3).

set.seed(123)
AND <- c(rep(0, 7), 1)
binary.data <- data.frame(expand.grid(c(0, 1), c(0, 1),
c(0,
1)), AND, OR)
net <- neuralnet(AND ~ Var1 + Var2 + Var3, binary.data,
hidden = 1,
err.fct = "ce", linear.output = FALSE)
plot(net, rep = "best")

Figure 5.3 : Un réseau de neurones avec trois entrées, une couche cachée et une couche de sortie.

Ici, nous avons inséré une étape de calcul avant la sortie. En parcourant le
diagramme de gauche à droite, on voit qu’il y a trois entrées pour une porte
logique. Celles-ci sont regroupées en une fonction de régression logistique dans
la couche cachée, au milieu. L’équation résultante est ensuite envoyée vers la
couche de calcul pour que nous puissions l’utiliser pour notre fonction AND. La
formule mathématique ressemblerait à ceci :
H1 = 8.57 + 3.6V ar13.5V ar2 − 3.6V ar3

Que l’on passerait ensuite via une fonction de régression logistique :

Ensuite, nous prenons cette sortie et nous la transmettons à un autre nœud de


régression logistique en utilisant les poids calculés sur le nœud de sortie :
AND = 5.72 − 13.79 × g(H1)

Un avantage majeur offert par l’utilisation d’une couche cachée avec quelques
nœuds de calcul (également cachés) est que cela améliore l’exactitude du réseau
de neurones. Cependant, plus un modèle de réseau de neurones est complexe,
plus il sera lent et plus il sera difficile de l’expliquer simplement à l’aide
d’équations faciles à comprendre. Plus de couches de calcul cachées signifie
également que vous courez le risque d’un surapprentissage de votre modèle,
comme vous l’avez déjà vu avec les systèmes traditionnels de modélisation par
régression.
Bien que les nombres liés aux poids de chaque nœud de calcul illustrés sur la
Figure 5.4 deviennent assez illisibles, le principal élément à retenir ici est
l’erreur et le nombre d’étapes de calcul. Dans ce cas, l’erreur passe à 0.033 par
rapport au dernier modèle, mais vous avez également réduit le nombre d’étapes
de calcul pour obtenir cette exactitude. La Figure 5.4 montre également un autre
nœud de calcul caché ajouté à l’unique couche cachée, juste avant la couche de
sortie :

set.seed(123)

net2 <- neuralnet(AND ~ Var1 + Var2 + Var3, binary.data,


hidden = 2,
err.fct = "ce", linear.output = FALSE)

plot(net2, rep = "best")


Figure 5.4 : Visualisation d’une architecture de réseau de neurones 3:2:1.

Mathématiquement, cela peut être représenté sous la forme de deux équations de


régression logistique qui sont injectées dans une autre équation de régression
logistique finale pour obtenir notre sortie résultante :
f1 = 13.64 + 13.97 × V ar1 + 14.9 × V ar2 + 14.27 × V ar3

f2 = −7.95 + 3.24 × V ar1 + 3.15 × V ar2 + 3.29 × V ar3

f3 = −5.83 − 1.94 × f1 + 14.09 × f2

Les équations deviennent de plus en plus compliquées à chaque augmentation du


nombre de nœuds de calcul cachés. L’erreur avec deux nœuds a légèrement
augmenté (de 0.029 à 0.033), mais le nombre d’étapes d’itération que le modèle
a pris pour minimiser cette erreur était un peu meilleur (en passant
de 156 à 143). Que se passe-t-il maintenant si vous augmentez le nombre de
nœuds de calcul ? C’est ce qu’illustrent les Figures 5.5 et 5.6.

set.seed(123)

net4 <- neuralnet(AND ~ Var1 + Var2 + Var3, binary.data,


hidden = 4,
err.fct = "ce", linear.output = FALSE)
net8 <- neuralnet(AND ~ Var1 + Var2 + Var3, binary.data,
hidden = 8,
err.fct = "ce", linear.output = FALSE)

plot(net4, rep = "best")

plot(net8, rep = "best")

Figure 5.5 : Un réseau de neurones avec quatre nœuds de calcul dans une seule couche cachée.
Figure 5.6 : Un modèle de réseau de neurones 3:8:1 avec surapprentissage.

Le code des Figures 5.5 et 5.6 utilise le même scénario de modélisation de


réseau de neurones, mais le nombre de nœuds de calcul cachés est d’abord porté
à quatre, puis à huit. Notez que le réseau de neurones à quatre nœuds de calcul
cachés a un (très légèrement) meilleur niveau d’erreur que le réseau avec un seul
nœud caché.
Dans ce cas, l’erreur est passée de 0.029 à 0.028, mais, surtout, le nombre
d’étapes a chuté de 156 à 61. Quelle amélioration ! Cependant, la version avec
huit couches de calcul cachées du réseau de neurones pourrait être entrée en
territoire de surapprentissage. En effet, l’erreur est passée de 0.029 à 0.051,
même si le nombre d’étapes a décru de 156 à 48.
Vous pouvez également appliquer la même méthodologie avec des sorties
multiples, bien que le graphe lui-même commence à devenir illisible à un
moment donné, comme l’illustre la Figure 5.7 :

set.seed(123)
net <- neuralnet(AND + OR ~ Var1 + Var2 + Var3,
binary.data,
hidden = 6, err.fct = "ce", linear.output = FALSE)
plot(net, rep = "best")

Figure 5.7 : Nous pouvons également concevoir des réseaux de neurones avec plusieurs sorties, tout en
ayant encore de multiples nœuds de calcul dans la couche cachée.
Réseaux de neurones multicouches
Tous les réseaux de neurones avec lesquels nous avons joué jusqu’à présent ont
une architecture possédant une couche d’entrée, une ou zéro couche cachée (ou
couche de calcul), et une couche de sortie.
Nous avons déjà utilisé des réseaux de neurones 1:1:1 ou 1:0:1 pour certains
schémas de classification. Dans ces exemples, nous avons essayé de modéliser
des classifications basées sur les fonctions de portes logiques AND et OR :

x1 <- c(0, 0, 1, 1)
x2 <- c(0, 1, 0, 1)
logic <- data.frame(x1, x2)
logic$AND <- as.numeric(x1 & x2)
logic$OR <- as.numeric(x1 ¦ x2)
logic

## x1 x2 AND OR
## 1 0 0 0 0
## 2 0 1 0 1
## 3 1 0 0 1
## 4 1 1 1 1

Comme le montre la Figure 5.8, nous pouvons représenter ce tableau sous la


forme de deux graphiques, chacun montrant visuellement les valeurs d’entrée et
les résultats selon le type de sortie de porte logique que nous utilisons :

logic$AND <- as.numeric(x1 & x2) + 1


logic$OR <- as.numeric(x1 ¦ x2) + 1

par(mfrow = c(2, 1))

plot(x = logic$x1, y = logic$x2, pch = logic$AND, cex =


2,
main = "Classification simple de deux types",
xlab = "x", ylab = "y", xlim = c(-0.5, 1.5), ylim =
c(-0.5,
1.5))
plot(x = logic$x1, y = logic$x2, pch = logic$OR, cex = 2,
main = "Classification simple de deux types",
xlab = "x", ylab = "y", xlim = c(-0.5, 1.5), ylim =
c(-0.5,
1.5))

Ces tracés utilisent des triangles pour les sorties égales à 1 (ou TRUE), et des
cercles pour les sorties nulles (ou FALSE). Dans notre discussion sur la
régression logistique, nous nous préoccupions essentiellement de trouver une
sorte de ligne qui séparerait ces données en points d’une couleur d’un côté, et
d’une autre couleur de l’autre. Rappelons que cette ligne de séparation est
appelée frontière de décision et qu’elle a toujours été une ligne droite.
Cependant, nous ne pouvons pas utiliser une droite pour essayer de classifier des
portes logiques plus compliquées, comme XOR ou XNOR.

Figure 5.8 : Dans ces deux cas de classification, nous pouvons séparer les deux classes en traçant une
frontière de décision en ligne droite.
Présentées sous forme de tableau, comme nous l’avons vu pour les fonctions
AND et OR, les fonctions XOR et XNOR prennent des entrées x1 et x2, et nous
donnent en retour une sortie numérique répondant au même principe, comme
l’illustre la Figure 5.9 :

x1 <- c(0, 0, 1, 1)
x2 <- c(0, 1, 0, 1)
logic <- data.frame(x1, x2)
logic$AND <- as.numeric(x1 & x2)
logic$OR <- as.numeric(x1 ¦ x2)
logic$XOR <- as.numeric(xor(x1, x2))
logic$XNOR <- as.numeric(x1 == x2)
logic

## x1 x2 AND OR XOR XNOR


## 1 0 0 0 0 0 1
## 2 0 1 0 1 1 0
## 3 1 0 0 1 1 0
## 4 1 1 1 1 0 1

logic$XOR <- as.numeric(xor(x1, x2)) + 1


logic$XNOR <- as.numeric(x1 == x2) + 1

par(mfrow = c(2, 1))


plot(x = logic$x1, y = logic$x2, pch = logic$XOR, cex =
2, main =
"Classification non linéaire de deux types",
xlab = "x", ylab = "y", xlim = c(-0.5, 1.5), ylim =
c(-0.5,
1.5))

plot(x = logic$x1, y = logic$x2, pch = logic$XNOR, cex =


2, main =
"Classification non linéaire de deux types",
xlab = "x", ylab = "y", xlim = c(-0.5, 1.5), ylim =
c(-0.5,
1.5))
Figure 5.9 : Ici, aucune ligne droite ne peut séparer les deux classes. Cependant, plusieurs lignes droites
combinées peuvent former une courbe utilisable comme frontière de décision non linéaire pour séparer les
classes de données.

Il n’y a pas une seule ligne droite qui puisse séparer les points ronds et les
triangles sur les tracés de la Figure 5.9. Si vous essayez de tracer un réseau de
neurones très simple, sans couches cachées, pour une classification XOR, les
résultats ne seront pas particulièrement gratifiants, comme l’illustre la
Figure 5.10 :

logic$XOR <- as.numeric(xor(x1, x2))

set.seed(123)
net.xor <- neuralnet(XOR ~ x1 + x2, logic, hidden = 0,
err.fct = "ce",
linear.output = FALSE)
prediction(net.xor)
## Data Error: 0;
## $'rep1'
## x1 x2 XOR
## 1 0 0 0.4870312778
## 2 1 0 0.4970850626
## 3 0 1 0.4980804563
## 4 1 1 0.5081363898
##
## $data
## x1 x2 XOR
## 1 0 0 0
## 2 1 0 1
## 3 0 1 1
## 4 1 1 0

plot(net.xor, rep = "best")

Figure 5.10 : Le calcul d’une sortie non linéaire avec une seule couche cachée (dans ce cas, il s’agit de la
couche de calcul) produit d’énormes erreurs.
Essayer d’utiliser un réseau de neurones sans couches cachées va entraîner une
énorme erreur. En regardant la sortie de la fonction prediction(), vous pouvez
constater que le réseau de neurones pense que pour un scénario donné, comme
xor(0,0), la réponse est 0,5 ± 2.77. Avoir une erreur qui est beaucoup plus élevée
que le niveau de granularité pour lequel vous essayez de trouver la réponse
indique que ce n’est certainement pas la meilleure méthode à utiliser.
Au lieu de l’approche traditionnelle consistant à utiliser une ou zéro couches
cachées, qui fournissent une frontière de décision en ligne droite, vous devez
vous fier ici à des frontières de décision non linéaires, ou des courbes, pour
séparer les classes de données. En ajoutant davantage de couches cachées à vos
réseaux de neurones, vous ajoutez également davantage de frontières de décision
de régression logistique sous forme de lignes droites.
Partant de ces lignes supplémentaires, vous pouvez tracer une frontière de
décision convexe qui active la non-linéarité. Pour cela, vous devez vous appuyer
sur une classe de réseaux de neurones appelée perceptron multicouche, ou MLP
(pour multilayer perceptron en anglais).
Une façon « rapide et expéditive » d’utiliser un perceptron multicouche dans ce
cas consisterait à prendre les entrées x1 et x2 pour obtenir les sorties des
fonctions AND et OR. Vous pouvez ensuite injecter ces sorties en tant qu’entrées
individuelles dans un réseau de neurones avec une couche unique, comme
l’illustre la Figure 5.11 :

set.seed(123)
and.net <- neuralnet(AND ~ x1 + x2, logic, hidden = 2,
err.fct = "ce",
linear.output = FALSE)
and.result <- data.frame(prediction(and.net)$rep1)

## Data Error: 0;

or.net <- neuralnet(OR ~ x1 + x2, logic, hidden = 2,


err.fct = "ce",
linear.output = FALSE)
or.result <- data.frame(prediction(or.net)$rep1)

## Data Error: 0;

as.numeric(xor(round(and.result$AND),
round(or.result$OR)))
## [1] 0 1 1 0

xor.data <- data.frame(and.result$AND, or.result$OR,


as.numeric(xor(round(and.result$AND),
round(or.result$OR))))
names(xor.data) <- c("AND", "OR", "XOR")

xor.net <- neuralnet(XOR ~ AND + OR, data = xor.data,


hidden = 0,
err.fct = "ce", linear.output = FALSE)

prediction(xor.net)

## Data Error: 0;
## $'rep1'
## AND OR XOR
## 1 0.000175498243 0.01115157179 0.013427052868
## 2 0.002185508146 0.99537740097 0.993710672686
## 3 0.008091828479 0.99566427543 0.993306664121
## 4 0.985343384090 0.99806091842 0.003024047907
##
## $data
## AND OR XOR
## 1 0.000175498243 0.01115157179 0
## 2 0.002185508146 0.99537740097 1
## 3 0.008091828479 0.99566427543 1
## 4 0.985343384090 0.99806091842 0

plot(xor.net, rep = "best")


Figure 5.11 : Vous pouvez contourner les limites de l’algorithme en calculant d’abord la couche unique, et
en passant ensuite les résultats dans une autre couche de calcul seule pour émuler un réseau de neurones
multicouches.

Un perceptron multicouche est exactement ce que son nom indique. Un


perceptron est un type particulier de réseau de neurones ayant une façon
spécifique de calculer les poids et les erreurs, technique connue sous le nom de
réseau feed-forward. En partant de là et en ajoutant plusieurs couches cachées,
nous le rendons compatible avec des données non linéaires telles que celles que
nous traitons dans une porte XOR.
Réseaux de neurones pour la régression
Nous avons examiné quelques exemples exhaustifs qui démontrent comment
vous pouvez utiliser les réseaux de neurones pour construire des systèmes
comme des portes AND et OR, dont il est ensuite possible de combiner les
sorties pour former des choses comme des portes XOR. Les réseaux de neurones
sont très bien pour modéliser des fonctions simples, mais lorsque vous les
enchaînez, vous devez parfois vous appuyer sur des phénomènes plus complexes
comme les perceptrons multicouches.
Vous pouvez également utiliser les réseaux de neurones pour des problèmes
d’apprentissage automatique standard, tels que la régression et la classification.
Pour voir l’utilisation de réseaux de neurones pour la régression et acquérir
tranquillement une bonne base de compréhension, observons le cas de la
Figure 5.12, qui représente un exemple simple de régression linéaire. Ici, nous
allons utiliser le jeu de données BostonHousing provenant de la bibliothèque
mlbench :

library(mlbench)
data(BostonHousing)

lm.fit <- lm(medv ~ ., data = BostonHousing)

lm.predict <- predict(lm.fit)

plot(BostonHousing$medv, lm.predict, main = "Prédictions


de la régression
linéaire
vs données réelles", xlab = "Réel", ylab = "Prédiction")

On crée ainsi un modèle linéaire de medv, c’est-à-dire la valeur médiane des


maisons occupées par leur propriétaire, en milliers de dollars. Ensuite, la
fonction predict() réalise une itération sur toutes les entrées du jeu de données en
utilisant le modèle que vous avez créé, et elle stocke les prédictions. Celles-ci
sont alors tracées en les rapportant aux valeurs réelles. Dans le cas idéal d’un
modèle parfait, le tracé résultant serait une relation parfaitement linéaire y = x.
Figure 5.12. Une façon de mesurer les performances d’un modèle consiste à comparer les prédictions qu’il
effectue avec les données réelles.

Comment se compare la régression avec des réseaux de neurones ? C’est ce


qu’illustrent le code qui suit et la Figure 5.13 :

library(nnet)

nnet.fit1 <- nnet(medv ~ ., data = BostonHousing, size =


2)

## # weights: 31
## initial value 283985.903126
## final value 277329.140000
## converged

nnet.predict1 <- predict(nnet.fit1)

plot(BostonHousing$medv, nnet.predict1, main =


"Prédictions du réseau de
neurones vs données réelles", xlab = "Réel", ylab =
"Prédiction")
Figure 5.13. Lorsque l’on change de modèle, il faut d’abord veiller à normaliser les données.

Avec notre ajustement d’un réseau de neurones comportant deux nœuds cachés
dans une couche de calcul cachée, la sortie est en fait plutôt incroyable. Cela
devrait justifier une enquête plus approfondie. Jetons un coup d’œil sur la
réponse :

summary(BostonHousing$medv)

## Min. 1st Qu. Median Mean 3rd Qu. Max.


## 5.00 17.02 21.20 22.53 25.00 50.00

La plage de réponse s’étend de 5 à 50. Les réseaux de neurones ne sont pas très
bons pour utiliser des nombres avec une variation aussi importante. Vous devez
donc utiliser une technique connue sous le nom de recalibrage des
caractéristiques. Cette technique consiste à recalibrer, ou normaliser, vos
données pour qu’elles soient toutes comprises entre 0 et 1 afin que vous puissiez
les intégrer dans certains modèles d’apprentissage automatique afin d’obtenir
des résultats plus exacts. Dans ce cas, vous allez diviser votre réponse par 50 (la
limite supérieure) afin de normaliser les données :

summary(BostonHousing$medv/50)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0.1000 0.3405 0.4240 0.4507 0.5000 1.0000

Maintenant, vous avez une réponse qui a un minimum de 0.1 et un maximum


de 1. La Figure 5.14 montre comment cela affecte la modélisation du réseau de
neurones :

nnet.fit2 <- nnet(medv/50 ~ ., data = BostonHousing, size


= 2,
maxit = 1000, trace = FALSE)

nnet.predict2 <- predict(nnet.fit2) * 50

plot(BostonHousing$medv, nnet.predict2, main =


"Prédictions du réseau de
neurones
vs données réelles avec recalibrage de la réponse",
xlab = "Réel", ylab = "Prédiction")
Figure 5.14. Sortie d’un modèle de réseau de neurones avec des entrées correctement normalisées.

Ce tracé semble un peu meilleur qu’auparavant, mais il est préférable de


quantifier la différence entre nos deux scénarios de modélisation. Vous pouvez le
faire en examinant les erreurs quadratiques moyennes :

mean((lm.predict - BostonHousing$medv)^2)

## [1] 21.89483

mean((nnet.predict2 - BostonHousing$medv)^2)

## [1] 14.55473

L’erreur totale pour le modèle linéaire est d’environ 22, alors que l’erreur totale
pour l’exemple de régression réalisé avec un réseau de neurones s’est améliorée
en descendant en dessous de 15.
Alternativement, vous avez la possibilité d’utiliser le puissant outil caret de R
pour mieux ajuster votre modèle. En invoquant caret, vous pouvez passer
certains paramètres d’ajustement et certaines techniques d’échantillonnage pour
obtenir une meilleure estimation de l’erreur et des résultats plus exacts, comme
le montre ce code :

library(caret)

mygrid <- expand.grid(.decay = c(0.5, 0.1), .size = c(4,


5, 6))
nnetfit <- train(medv/50 ~ ., data = BostonHousing,
method = "nnet",
maxit = 1000, tuneGrid = mygrid, trace = F)
print(nnetfit)

## Neural Network
##
## 506 samples
## 13 predictor
##
## No pre-processing
## Resampling: Bootstrapped (25 reps)
## Summary of sample sizes: 506, 506, 506, 506, 506, 506,
...
## Resampling results across tuning parameters:
##
## decay size RMSE Rsquared MAE
## 0.1 4 0.08266563 0.8085856 0.05816329
## 0.1 5 0.08319615 0.8055233 0.05864329
## 0.1 6 0.08262709 0.8073669 0.05828894
## 0.5 4 0.09225098 0.7729240 0.06523299
## 0.5 5 0.09149737 0.7757494 0.06474592
## 0.5 6 0.09097064 0.7773526 0.06418347
##
## RMSE was used to select the optimal model using the
smallest value.
## The final values used for the model were size = 6 and
decay = 0.1.

La meilleure estimation de l’erreur a ici une taille de 6, ce qui signifie six nœuds
dans la couche cachée du réseau, et un affaiblissement des paramètres de 0.1.
L’erreur quadratique moyenne (RMSE) est la même erreur que celle que vous
avez vue précédemment, mais avec la racine carrée. Pour comparer avec les
résultats déjà rencontrés, la meilleure erreur est donc la suivante :

0.08168503^2

## [1] 0.006672444126

C’est une amélioration remarquable par rapport au résultat précédent.


Réseaux de neurones pour la
classification
Dans un sens, nous avons déjà étudié l’utilisation des réseaux de neurones pour
la classification via les portes AND et OR que nous avons construites au début
de ce chapitre. Ces fonctions reçoivent une certaine entrée binaire, et elles nous
retournent un résultat binaire par le biais de fonctions d’activation de régression
logistique à chaque nœud de calcul du réseau de neurones. Vous pouvez
considérer cela comme une classification portant sur une classe unique. La
plupart du temps, nous sommes évidemment plus intéressés par la classification
multiclasse.
Dans ce cas, vous devez diviser vos données en jeux d’entraînement et de test, ce
qui est assez simple. Entraîner le réseau de neurones sur les données du premier
jeu a également du sens, si nous nous rappelons de nos expériences passées avec
l’approche entraînement/test de l’apprentissage automatique. La différence ici
est que, lorsque vous appelez la fonction predict(), vous le faites avec l’option
type='class'. Cela facilite le traitement des données de classe au lieu des données
numériques que vous utiliseriez avec la régression :

iris.df <- iris


smp_size <- floor(0.75 * nrow(iris.df))

set.seed(123)
train_ind <- sample(seq_len(nrow(iris.df)), size =
smp_size)

train <- iris.df[train_ind, ]


test <- iris.df[-train_ind, ]

iris.nnet <- nnet(class ~ ., data = train, size = 4,


decay = 0.0001,
maxit = 500, trace = FALSE)
predictions <- predict(iris.nnet, test[, 1:4], type =
"class")
table(predictions, test$class)

## predictions Iris-setosa Iris-versicolor Iris-


virginica
## Iris-setosa 11 0
0
## Iris-versicolor 0 13
0
## Iris-virginica 0 0
14

Vous pouvez voir que la matrice de confusion fournit un assez bon résultat pour
la classification en utilisant des réseaux de neurones. Repensez au Chapitre 2 et à
l’exemple qui utilise kmeans pour le clustering multiclasse : nous n’avons ici
aucun cas qui soit mal étiqueté, par comparaison avec les deux cas mal étiquetés
que nous avions vus précédemment.
Réseaux de neurones avec caret
Le package caret offre un ensemble très flexible d’outils utiles pour nos
procédures de traitement en apprentissage automatique. Dans le cas des réseaux
de neurones, il y en a plus de 15 parmi lesquels faire votre choix, chacun ayant
ses propres avantages et ses inconvénients. En nous en tenant à notre exemple
nnet pour le moment, nous pouvons exécuter un modèle dans caret en invoquant
la fonction train() et en lui passant l’option method='nnet'. Nous pouvons alors
passer à nos étapes normales de prédiction. La puissance de caret vient de la
facilité avec laquelle il est possible de sélectionner une méthode différente pour
comparer les résultats.

Régression
Dans le cas d’une régression, le résultat que vous recherchez sera numérique.
Donc, pour comparer les résultats d’un modèle par rapport à un autre, vous
devriez chercher une erreur RMSE et voir laquelle est la plus basse, c’est-à-dire
correspondant à un modèle plus exact. Pour cet exemple, nous allons utiliser le
jeu de données Prestige du package car. Ce jeu de données contient un certain
nombre de caractéristiques liées aux professions et au prestige professionnel
perçu en fonction de certaines caractéristiques comme l’éducation, les revenus et
le pourcentage de féminisation parmi les titulaires de cette profession. Pour cet
exemple de régression, vous essaierez de prédire le revenu en fonction du
prestige et de l’éducation :

library(car)
library(caret)
trainIndex <- createDataPartition(Prestige$income, p =
0.7, list = F)
prestige.train <- Prestige[trainIndex, ]
prestige.test <- Prestige[-trainIndex, ]

my.grid <- expand.grid(.decay = c(0.5, 0.1), .size = c(5,


6, 7))
prestige.fit <- train(income ~ prestige + education, data
= prestige.train,
method = "nnet", maxit = 1000, tuneGrid = my.grid,
trace = F,
linout = 1)

prestige.predict <- predict(prestige.fit, newdata =


prestige.test)

summary(prestige.test$income)

## Min. 1st Qu. Median Mean 3rd Qu. Max.


## 918 4280 6234 7188 8177 25879

sqrt(mean((prestige.predict - prestige.test$income)^2))

## [1] 4146.445

Selon la sortie obtenue, la fourchette de revenus dans le jeu de données va


de 918 dollars canadiens à 25 879. L’erreur de plus de 4 000 dollars canadiens
est élevée, mais vous pouvez comparer avec d’autres types de réseaux de
neurones pour juger du fonctionnement de la méthode nnet :

prestige.fit <- train(income ~ prestige + education, data


= prestige.train,
method = "neuralnet")

prestige.predict <- predict(prestige.fit, newdata =


prestige.test)

sqrt(mean((prestige.predict - prestige.test$income)^2))

Le résultat de cette méthode est de 3 814.09. C’est une amélioration par rapport
à la méthode nnet, mais, en revanche, la vitesse à laquelle ce calcul s’effectue est
beaucoup plus lente. C’est là que vous devez vous fier à la mise au point de vos
objets d’entraînement afin d’extraire les performances optimales pour chaque
méthode que vous choisissez.

Classification
La classification avec caret fonctionne d’une manière similaire selon la méthode
que vous utilisez. Vous pouvez vous servir de la plupart des méthodes de caret
pour la classification ou la régression, mais certaines sont spécifiques à l’une par
rapport à l’autre. La seule méthode qui est explicitement dédiée à la seule
classification est multinom, alors que les méthodes neuralnet, brnn, qrnn et
mlpSGD ne concernent que la régression. Vous pouvez utiliser le reste pour la
classification ou la régression. Voyons l’exemple ci-dessous :

iris.caret <- train(class ~ ., data = train, method =


"nnet",
trace = FALSE)
predictions <- predict(iris.caret, test[, 1:4])
table(predictions, test$class)

## predictions Iris-setosa Iris-versicolor Iris-


virginica
## Iris-setosa 11 0
0
## Iris-versicolor 0 13
0
## Iris-virginica 0 0
14

Le résultat final est le même que précédemment en termes d’exactitude du


modèle, mais la souplesse de caret vous permet à nouveau d’effectuer assez
facilement des comparaisons avec d’autres méthodes :

iris.caret.m <- train(class ~ ., data = train, method =


"multinom",
trace = FALSE)
predictions.m <- predict(iris.caret.m, test[, 1:4])
table(predictions.m, test$class)

## predictions.m Iris-setosa Iris-versicolor Iris-


virginica
## Iris-setosa 11 0
0
## Iris-versicolor 0 13
0
## Iris-virginica 0 0
14

Il est bon de savoir que d’autres méthodes sont tout aussi précises !
En résumé
Les réseaux de neurones peuvent sembler très compliqués à première vue. On les
considère souvent comme une boîte noire ; les données entrent, et la
« perspicacité » surgit en sortie. En réalité, les réseaux de neurones sont assez
faciles à comprendre dans leur forme la plus simple, mais difficiles à expliquer
lorsqu’ils deviennent plus complexes. Fondamentalement, les réseaux de
neurones reçoivent certaines valeurs en entrée, ils les digèrent par le biais d’une
fonction d’activation et renvoient une sortie. La fonction d’activation, plus
souvent que l’inverse, n’est généralement qu’une fonction sigmoïde, de sorte que
vous pouvez considérer les réseaux de neurones comme des modèles de
régression logistique plus sophistiqués. En fait, leur calcul est presque identique
dans le cas d’une architecture de réseau de neurones simple.
Les réseaux neuronaux deviennent plus complexes lorsque vous commencez à
faire évoluer leur architecture. Celle-ci se compose d’une couche d’entrée, d’un
certain nombre de couches cachées et d’une couche de sortie. La couche
d’entrée correspond simplement aux valeurs des caractéristiques que vous
transmettez au modèle. Les couches cachées sont celles qui gèrent les calculs et
les traitements. Les couches de sortie sont celles à partir desquelles vous
récupérez vos résultats. Dans des cas simples, les couches de calcul cachées
peuvent être les mêmes que la couche de sortie, comme dans le cas de la
modélisation de fonctions de portes logiques telles que AND et OR. Un exemple
d’architecture pour un réseau de neurones avec trois entrées, une couche cachée
et un nœud d’activation pourrait s’écrire 3:1:1. Augmenter trop le nombre de
nœuds de calcul, comme dans 3:8:1, va tendre à provoquer un surapprentissage
des données.
Les réseaux de neurones multicouches (par exemple un réseau avec une
architecture de type 3:2:2:1) peuvent également modéliser des comportements
non linéaires. La régression logistique est bonne pour trouver des frontières de
décision qui sont des lignes droites permettant de séparer les données en
plusieurs classes ou types. Par contre, elle échoue dans le cas de comportements
non linéaires. En introduisant dans un système des frontières de décision
multiples via des couches cachées, vous pouvez créer une courbe qui servira
ensuite à séparer les données, ce qui est impossible dans ce cas avec une ligne
droite.
Vous pouvez utiliser les réseaux de neurones pour modéliser aussi bien la
régression que la classification. Cependant, dans le cas de la régression, il
convient d’être prudent et de pratiquer un recalibrage des données. Le plus
souvent, les réseaux de neurones préfèrent que les données se trouvent dans un
intervalle compris entre 0 et 1, et essayer de modéliser des données qui ont des
valeurs plus élevées peut être problématique. Pour les besoins de la
classification, lorsque vous utilisez la fonction predict(), vous devez également
passer l’option type='class' pour que la modélisation se déroule correctement.
Il y a également nombre de méthodes de réseau de neurones que vous pouvez
utiliser dans R à partir des fonctions du package caret. Bien que certaines d’entre
elles se limitent à la régression ou à la classification, une bonne majorité sont
assez souples pour être employées avec l’une ou l’autre. Il est cependant
important d’être prudent lors du choix de la méthode, non seulement pour
sélectionner celle qui est la mieux adaptée au travail qui vous intéresse, mais
aussi parce qu’il peut y avoir des paramètres de réglage ou d’optimisation qui
doivent être transmis au modèle pour l’accélérer ou le rendre plus exact.
CHAPITRE 6
Méthodes arborescentes
Dans le monde de l’apprentissage automatique, les méthodes basées sur les
arbres sont très utiles. Elles sont relativement simples à expliquer, et faciles à
visualiser. Avec certains modèles d’apprentissage automatique (notamment les
réseaux de neurones complexes), le modèle entraîné peut effectivement être une
« boîte noire » dont le fonctionnement interne est trop complexe pour que nous
puissions l’expliquer simplement. Les modèles arborescents, en revanche,
peuvent être beaucoup plus intuitifs pour l’utilisateur moyen.
Dans ce chapitre, nous allons examiner le fonctionnement des modèles
arborescents en nous concentrant d’abord sur les arbres de décision d’un point de
vue général. Nous nous plongerons ensuite dans les mécanismes de base de leur
fonctionnement et nous verrons certains attributs positifs et négatifs qui leur sont
associés. Nous aborderons également différents types de modèles arborescents,
comme l’inférence conditionnelle et les forêts aléatoires. Pour vous donner un
aperçu, les arbres de décision sont aussi simples que des énoncés « if-then »
(« si-sinon ») relatifs aux données.
Les arbres d’inférence conditionnelle fonctionnent de la même manière, mais
avec des bases statistiques légèrement différentes. Les forêts aléatoires peuvent
être compliquées mathématiquement, mais elles se résument généralement à une
collection de différents modèles d’arbres auxquels on demande de voter sur un
résultat. Tous ces types peuvent être utilisés pour modéliser la régression (arbres
de régression) ou la classification (arbres de classification). Bon nombre d’entre
eux peuvent être employés pour les deux, et ils sont alors appelés modèles
d’arbres de classification et de régression (ou CART, pour classification and
regression tree en anglais).
Un modèle d’arbre simple
Commençons par un exemple de jeu de données qui décrit mes courses de vélo
cette année. En fait, nous n’allons pas nous étendre sur mes performances, mais
nous intéresser à des paramètres uniquement liés à la météo. Si j’avais un
échantillonnage suffisamment étendu de ce genre de données, et si j’avais en vue
une course de vélo pour laquelle j’aurais déjà connaissance des prévisions du
temps avec une fiabilité raisonnable, devrais-je m’attendre à faire un bon ou un
mauvais résultat dans cette course ? Le Tableau 6.1 énumère quelques facteurs
météorologiques différents et mon résultat de course correspondant.

Tableau 6.1 : Exemples de conditions météorologiques et de résultats de course.

Semaine Condition du ciel Vitesse du vent Humidité Bon résultat ?


1 nuageux faible élevée oui
2 pluvieux faible normale oui
3 ensoleillé fort normale oui
4 nuageux fort forte oui
5 nuageux faible normale oui
6 pluvieux fort élevée non
7 pluvieux fort normale non
8 nuageux fort normale oui
9 ensoleillé faible élevée non
10 ensoleillé faible normale oui
11 pluvieux faible normale oui
12 ensoleillé faible élevée non
13 ensoleillé fort élevée non

Supposons que la course de la semaine 14 se déroulera avec les conditions météo


suivantes :
• Condition du ciel : pluvieux

• Vitesse du vent : faible

• Humidité : élevée
Ce que nous pourrions faire, si ces données étaient enregistrées dans un tableur,
serait de les filtrer à partir de ces constatations exactes, et de voir à quoi
ressemblent les résultats. Un modèle arborescent fait fondamentalement la même
chose. Il partage les données selon certains critères, et il construit ensuite un
arbre de sorte que, lorsque nous recevons de nouvelles données, il suit les
branches de cet arbre jusqu’à obtenir un résultat. La Figure 6.1 reprend les
données du Tableau 6.1, et les représente sous forme d’arbre, le premier
fractionnement se faisant sur la variable de condition du ciel.
La Figure 6.1 montre un arbre avec trois feuilles, selon que la condition du ciel
est pluvieux, nuageux ou ensoleillé. Pour chacun de ces sous-ensembles, vous
pouvez voir à quoi ressemblent les données. La réponse que vous voulez
modéliser consiste à essayer de prévoir si je vais faire un bon résultat dans la
prochaine course.

Figure 6.1 : Un exemple de la manière dont un arbre de décision scinde les données.

Un arbre de décision étudie ces sous-ensembles, et il examine si la variable de


résultat contient toutes les données d’une classe particulière. Dans le cas des
courses avec une météo nuageuse, j’ai un bon résultat (oui) pour chacune d’entre
elles. Cela indique que le sous-ensemble est « pur », et donc que vous n’avez pas
besoin de le diviser encore. En revanche, les sous-ensembles correspondant aux
conditions ensoleillées et pluvieuses comportent un mélange de oui et de non.
Vous voudrez donc les diviser davantage afin d’obtenir de nouveaux sous-
ensembles ayant une plus grande pureté.
La pureté se définit comme le nombre d’échantillons positifs ou négatifs que
vous essayez de modéliser (dans ce cas, mes résultats) par rapport aux valeurs
totales du tableau. Vous voulez continuer à partager l’arbre jusqu’à ce que vous
ayez autant de feuilles pures que possible.
Comme le montre la Figure 6.2, en divisant davantage les sous-ensembles
« pluvieux » et « ensoleillé » (dans ce cas, selon le vent et l’humidité,
respectivement), il y a maintenant cinq terminaisons à l’arbre. Vous pouvez lire
celui-ci comme s’il s’agissait d’une déclaration « if-then », en partant du haut.
Revoyons d’abord ce que vous voulez prédire. Vous voulez savoir si je vais faire
un bon résultat pour ma course s’il pleut, avec une vitesse de vent faible et un
taux d’humidité élevé. Partez du sommet de l’arbre, et déplacez-vous le long du
chemin de la condition du ciel menant à la pluie. Passez vers la branche qui
correspond à une vitesse de vent faible. Cela vous amène dans un « godet » dans
lequel tous les résultats sont « oui ». Donc, sur la base des conditions à partir
desquelles vous vouliez faire une prédiction, l’arbre de décision vous permet de
me rassurer totalement : j’aurai un bon résultat lors de la prochaine course !

Figure 6.2 : Cet arbre de décision se divise davantage en fonction de la vitesse du vent ou de l’humidité
pour créer des tableaux (ou des feuilles) plus purs à ses extrémités.
Décider comment partager les arbres
Dans l’exemple précédent, vous avez commencé en partant d’un jeu de données
que vous vouliez modéliser. Il y avait de multiples caractéristiques, et une
réponse qui devait être prédite par le modèle (un résultat de course en fonction
de l’état du ciel, de la vitesse du vent et de l’humidité). Vous avez commencé par
examiner le fonctionnement d’un arbre de décision en effectuant une division
basée sur la condition du ciel. Mais pourquoi ne pas choisir une autre variable
pour effectuer le fractionnement ? Un arbre de décision veut maximiser la
« pureté » de ses partages. Jetez un coup d’œil sur la Figure 6.3, qui revisite la
manière dont la première coupe de l’arbre a été effectuée.
Sur la Figure 6.3, la feuille Nuageux est pure à 100 %. Cela signifie que 100 %
des données de ce sous-ensemble sont des « oui », et qu’il n’est donc pas
nécessaire d’effectuer une division supplémentaire. Les feuilles ensoleillées et
pluvieuses nécessitent toutefois d’être repartagées. Par contraste, comparons
avec ce que pourrait donner un arbre basé sur la variable Vent, comme l’illustre
la Figure 6.4.
Sur la Figure 6.4, aucune des feuilles initiales n’est totalement pure. Une est pure
à environ 71 %, et l’autre à 50 %. Les deux nécessitent des divisions
supplémentaires. Vous voulez atteindre autant de pureté qu’il est possible, et le
fait d’avoir un arbre dont une feuille est pure à 100 % dès l’origine indique que
cette variable donne le point de départ à choisir.

Figure 6.3 : Lors de la division des arbres de décision, vous voulez des nœuds de feuilles aussi purs que
possible (dans ce cas, la feuille correspondant aux conditions nuageuses est la plus pure, suivie des
conditions ensoleillées, puis pluvieuses). Nous devons continuer à diviser notre arbre pour les nœuds de
feuilles impurs afin d’obtenir un meilleur modèle.
Figure 6.4 : La division des données en fonction du vent ne donne pas de bons résultats en termes de
pureté.

Virtuellement tous les algorithmes d’arbre de décision intégreront cette


fonctionnalité, et donc vous n’avez pas vraiment besoin de vous soucier de la
détermination d’une division initiale des données. Pour autant, savoir comment
fonctionne la mécanique sous-jacente aux algorithmes des arbres de décision
relève des bonnes pratiques.

Entropie de l’arbre et gain d’information


Une façon plus mathématique de représenter la pureté de chaque sous-ensemble
de l’arbre consiste à mesurer son entropie. Il s’agit d’une façon d’évaluer la
probabilité que vous obteniez un élément positif si vous effectuez un choix au
hasard dans un sous-ensemble particulier. L’équation qui définit l’entropie est la
suivante :

En langage plus simple, cette équation nous dit que l’entropie d’un certain jeu de
données est fonction du nombre de cas positifs p+ dont nous disposons au total,
multiplié par le logarithme (notez la base deux) de la même valeur, puis en
soustrayant de ce résultat le nombre de cas négatifs calculé de la même manière.
Rappelons que, pour ce qui concerne notre exemple, un résultat positif serait un
« oui », et un résultat négatif un « non » (reportez-vous au Tableau 6.1). Pour
voir comment cela fonctionne, regardons comment vous pouvez appliquer cette
formule à l’arbre que vous avez divisé plus haut selon les conditions
atmosphériques :
Les valeurs que vous obtenez pour les conditions de pluie et de soleil sont toutes
les deux l (ou presque). Cela signifie que ces échantillons sont très impurs et
qu’il faudrait les diviser davantage, alors que l’échantillon correspondant aux
conditions nuageuses est totalement pur, ce qu’indique une entropie de 0. Nous
nous en sortons ici avec un tour de passe-passe mathématique, puisque,
techniquement parlant, le logarithme de zéro devrait être de -∞, mais nous
« annulons » cela en le multipliant par zéro.
Bien que l’entropie des feuilles individuelles soit tout à fait intéressante,
l’algorithme détermine les caractéristiques les plus utiles à diviser en premier en
trouvant celles qui ont le gain le plus élevé. Le gain est une mesure de la
pertinence de la caractéristique sur une échelle allant de 0 (le moins utile) à 1 (le
plus utile). Il est défini par la formule :

Ici, V est la valeur possible de la caractéristique, S est le nombre total de points


de données dans la feuille, et SV est le sous-ensemble pour lequel nous avons nos
valeurs possibles de la caractéristique. Exécutons cela sur l’arbre divisé en
fonction du vent :
Un moyen facile de procéder dans R consiste à utiliser la fonction varImpPlot()
du package caret. Bien que cette fonction utilise une méthode de calcul
mathématique légèrement différente de la formule Gain que nous venons de voir,
le résultat est le même. Avec ce tracé, vous pouvez constater que les conditions
météorologiques du ciel représentent le facteur le plus important, suivi de
l’humidité et ensuite de la vitesse du vent. Si vous aviez un jeu de données avec
beaucoup plus de variables, et que vous vouliez voir celles qu’un algorithme
d’arbre particulier penserait être les plus importantes, vous pourriez utiliser la
fonction varImpPlot() pour avoir un aperçu rapide sur la division de l’arbre, en
allant du haut vers le bas :

library(caret)
library(randomForest)

cycling <- read.table("cycling.txt", sep = "\t", header =


T)

fit <- randomForest(factor(Result) ~ ., data =


cycling[2:5])

varImpPlot(fit)
Avantages et inconvénients des arbres
de décision
Jusqu’à présent, nous avons vu le bon côté des arbres de décision : ils sont
faciles à expliquer. En commençant par quelques échantillons, nous avons été en
mesure de représenter ceux-ci sous forme d’arbres, puis de passer simplement en
revue les diverses divisions des données pour arriver à une conclusion. Lorsque
vous entraînez un modèle arborescent, la mécanique sous-jacente utilisera la
même démarche. Il est donc très facile d’expliquer le fonctionnement du modèle
lui-même.
Nous avons également vu que les arbres peuvent traiter automatiquement des
données qui ne sont pas pertinentes (disons lorsque la métrique de gain est
nulle). Cela élimine le besoin d’être trop prudent, puisque l’arborescence
sélectionnera presque toujours les meilleurs attributs à votre place. La sélection
des caractéristiques représente une grande partie de la plupart des procédures de
modélisation, et le fait que les modèles arborescents le fassent pour vous peut
économiser bien des maux de tête.
Un autre avantage des arbres est que les calculs sont plutôt rapides une fois
qu’ils ont été ajustés. En fait, lorsque cet ajustement est réalisé, le modèle
résultant tend à être assez succinct. Cela aide non seulement à expliquer ce qui
se passe, mais aussi à garder le modèle lui-même relativement simple. Il est
assez facile de comprendre ce qui se passe quand un arbre devient trop ajusté,
c’est-à-dire trop spécifique, ou s’il y a de multiples petites branches dans le
modèle. Finalement, les modèles arborescents peuvent également gérer les
données manquantes ou aberrantes, ce qui vous évite d’avoir à effectuer des
procédures de quarantaine fastidieuses susceptibles d’être plus courantes avec
d’autres modèles.

Surapprentissage des arbres


Les inconvénients des arbres sont qu’ils peuvent être très sensibles aux
conditions initiales ou aux variations des données. Du fait que nous divisons les
attributs et évaluons des probabilités dans nos données, si nous modifions
légèrement celles-ci, nous pourrions éliminer des branches entières de notre
modèle. Un autre problème avec les arbres est qu’ils suivent des divisions de
données alignées sur les axes. Si nous avions un arbre pour certains échantillons,
où les sorties se présentent comme ci-dessous, nous aurions un tracé
correspondant des données ressemblant à l’illustration de la Figure 6.5 :

• Si X est inférieur à 2.5, le résultat est égal à 25.

• Si X est supérieur à 2.5 et si Y est inférieur à 15, le résultat est égal à 9.

• Si X est supérieur à 2.5 et si Y est supérieur à 15, le résultat est de 3.14.


Vous pouvez voir immédiatement que les arbres divisent les données en
différentes « boîtes », selon leur conception par rapport aux caractéristiques.
Cela fonctionne mieux pour certaines versions de données qu’avec d’autres. Les
données qui peuvent facilement être divisées en boîtes par rapport aux axes,
comme dans le cas du jeu iris illustré sur la Figure 6.6, se comporteront mieux :

Figure 6.5 : Les arbres de décision fonctionnent différemment des autres algorithmes d’apprentissage
automatique.
Figure 6.6 : Les arbres de décision peuvent classifier certaines données très facilement, comme on peut le
voir avec le jeu de données iris.

Cependant, les données peuvent avoir plusieurs formes et tailles. De plus, dans
le jeu de données iris, il y a une vue des données qui ne peut pas être séparée
seulement par deux ou trois boîtes. Contrairement à d’autres algorithmes
capables de définir une ligne telle que toutes les données d’un côté appartiennent
à la classe A et toutes les données de l’autre côté appartiennent à la classe B, les
algorithmes basés sur les arbres doivent dessiner des boîtes pour partager les
données. Pour approximer une ligne, ou une courbe, vous avez besoin de
beaucoup de boîtes. Ce que cela signifie pour votre arbre, c’est que vous ajoutez
de plus en plus de branches, et donc que vous en augmentez la complexité,
comme l’illustre la Figure 6.7.
Utiliser un plus grand nombre de boîtes pour diviser les données en deux parties
peut probablement se traduire par un phénomène de surapprentissage, aussi bien
pour un modèle de régression que de classification. C’est ce que montre la
Figure 6.8.
Figure 6.7 : Avec le même jeu de données iris, mais en prenant des caractéristiques différentes, vous
rencontrez une situation dans laquelle une ou deux boîtes ne suffisent pas à s’ajuster aux données.

Figure 6.8 : La classification des données à l’aide d’une approche arborescente peut être sujette au
surapprentissage, comme le montrent les nombreuses petites boîtes de ce graphique.
Comme nous l’avons vu précédemment, vous construisez des modèles
arborescents en commençant par une valeur de gain élevée, puis en en effectuant
une division selon les valeurs de gain les plus élevées suivantes. Dans notre
exemple de courses cyclistes, nous disposons de suffisamment d’échantillons
pour construire un arbre nous donnant des feuilles pures à la fin de l’exercice. Si
vous aviez un jeu de données vous obligeant à diviser l’arbre de plus en plus, le
modèle deviendrait beaucoup trop spécifique aux données avec lesquelles vous
l’entraînez.
Si vous prenez des données d’échantillonnage, que vous les divisez en un jeu
d’entraînement et un jeu de test, puis que vous faites « pousser » le modèle de
l’arbre sur le jeu d’entraînement, vous devez trouver un point d’arrêt pour
stopper la croissance de l’arbre. Sinon, le modèle se généralisera mal sur le jeu
de test, car l’arbre sera devenu trop spécifique aux données d’entraînement,
comme l’illustre la Figure 6.9.

Figure 6.9 : Les méthodes arborescentes donnent de bons résultats sur les données d’entraînement, mais
ont des rendements décroissants sur les données de test.

Élaguer les arbres


Pour éviter qu’un modèle arborescent ne sombre dans le surapprentissage, vous
devez élaguer les feuilles les moins importantes de l’arbre. Vous pouvez le faire
en utilisant le package rpart. Tout d’abord, laissons grandir un arbre en utilisant
la fonction rpart(). Il s’agit d’une fonction qui partitionne récursivement les
données pour construire un modèle arborescent. Commençons par jeter un coup
d’œil rapide sur les données que vous allez modéliser. Il s’agit de données
automobiles provenant de l’édition 1990 de Consumer Reports (un magazine
mensuel publié par une association américaine de consommateurs) :

library(rpart)

head(cu.summary)

## Price Country Reliability Mileage


Type
## Acura Integra 4 11950 Japan Much better NA
Small
## Dodge Colt 4 6851 Japan <NA> NA
Small
## Dodge Omni 4 6995 USA Much worse NA
Small
## Eagle Summit 4 8895 USA better 33
Small
## Ford Escort 4 7402 USA worse 33
Small
## Ford Festiva 4 6319 Korea better 37
Small

Chaque véhicule du jeu de données est associé à certaines caractéristiques qui


pourraient intéresser un consommateur afin qu’il puisse effectuer un achat
éclairé. Ces caractéristiques comprennent le prix (le coût de la voiture en dollars
américains), le pays d’origine, une échelle de fiabilité (de « bien pire » à « bien
meilleur » en passant par des appréciations intermédiaires), le kilométrage
parcouru en unités de gallons de carburant consommés par mile, et enfin le type
de voiture (compacte, grande, moyenne, petite, sportive ou encore fourgonnette).
Développons un arbre basé sur ces données en utilisant la fonction rpart() (voir
la Figure 6.10) :

fit <- rpart(


Mileage~Price + Country + Reliability + Type,
method="anova", #method="class" pour la
classification
data=cu.summary
)

plot(fit, uniform=TRUE, margin=0.1)


text(fit, use.n=TRUE, all=TRUE, cex=.8)

L’option method=, spécifique à la fonction rpart(), vous permet de basculer entre


arborescence de régression à arborescence de classification. Ici, vous modélisez
le rendement énergétique d’un véhicule, tel qu’il est donné par la variable
Mileage, qui est une valeur numérique. Vous voulez donc comme résultat un
modèle de régression.
Vous pouvez voir ce résultat sur la Figure 6.10. En la parcourant de haut en bas,
il y a une première division selon le prix (supérieur ou égal à 9 446 $, ou bien
inférieur). Ensuite, vous divisez sur le type. Pour la branche la plus à gauche,
vous effectuez à nouveau un partage sur le type, et, pour la branche la plus à
droite en bas, vous divisez encore sur le prix. La fonction rpart indique quel
pourcentage de chaque branche est partagé, ainsi que le nombre de points de
données obtenu à chaque fois.
Figure 6.10 : Un arbre de décision simple tracé à l’aide de la fonction rpart() de la bibliothèque rpart. Ici,
nous partons de la caractéristique de prix (Price), et nous effectuons la division en conséquence.

La Figure 6.11 présente le taux d’erreur de cet arbre en fonction du nombre de


divisions :

rsq.rpart(fit)[1]

##
## Regression tree:
## rpart(formula = Mileage ~ Price + Country +
Reliability + Type,
## data = cu.summary, method = "anova")
##
## Variables actually used in tree construction:
## [1] Price Type
##
## Root node error: 1354.6/60 = 22.576
##
## n=60 (57 observations deleted due to missingness)
##
## CP nsplit rel error xerror xstd
## 1 0.622885 0 1.00000 1.04244 0.182587
## 2 0.132061 1 0.37711 0.52398 0.102903
## 3 0.025441 2 0.24505 0.37671 0.081047
## 4 0.011604 3 0.21961 0.37999 0.086045
## 5 0.010000 4 0.20801 0.40736 0.088680
Figure 6.11 : Graphiques d’erreur en fonction de la taille de l’arbre de décision en termes de nombre de
fractionnements (ou splits).

L’utilisation de la fonction rsq.rpart() nous donne deux tracés. La


Figure 6.11 montre l’exactitude du modèle arborescent par rapport au nombre de
fractionnements dans l’arbre. La Figure 6.12 affiche l’erreur relative, toujours en
fonction du nombre de divisions dans l’arbre. Il semble assez clair d’après ces
deux graphiques que l’arbre est assez bien ajusté au niveau des
fractionnements 2 et 3, et que l’ajout d’une division supplémentaire (pour
obtenir un total de 4) ne semble pas ajouter beaucoup de valeur au modèle.
Pour « nettoyer » le modèle et s’assurer qu’il n’y a pas de surapprentissage, il
faut élaguer les feuilles les moins utiles de l’arbre. Une façon plus précise de
savoir quelles parties il convient d’élaguer consiste à regarder le paramètre de
complexité de l’arbre, souvent appelé « CP », tel que vous pouvez le voir sur la
Figure 6.13 :

plotcp(fit)
Figure 6.12 : L’erreur relative de la validation croisée en fonction du nombre de fractionnements.
Figure 6.13 : Vous pouvez ajouter un seuil en pointillés qui signifie la meilleure valeur du paramètre de
complexité à choisir (dans ce cas, 0.058 et une taille d’arborescence de 3).

Le paramètre de complexité est le montant par lequel le fractionnement de ce


nœud d’arbre va améliorer l’erreur relative. Sur la Figure 6.13, le premier
fractionnement améliore l’erreur de 0.29, puis de moins en moins pour chaque
fractionnement supplémentaire. La ligne pointillée sur le graphique est générée
par des estimations d’erreurs dans le modèle, et elle signale que vous voulez un
arbre avec un certain nombre de fractionnements en dessous de cette ligne, mais
tout de même sans aller trop loin pour éviter le surapprentissage. L’axe des y
représente l’erreur relative pour un nœud donné. Vous pouvez voir sur le tracé
que l’erreur relative est minimisée jusqu’à une profondeur d’arbre de 4 niveaux
(axe des x supérieur), et que le paramètre de complexité se trouve en dessous du
seuil correspondant à la ligne pointillée. Notez que la taille de l’arbre correspond
au nombre de divisions dans les données, et non à la quantité de feuilles
terminales.
La règle générale à suivre demande de sélectionner le premier paramètre de
complexité qui se trouve sous la ligne pointillée. Dans ce cas, cependant, vous
pouvez voir que passer à une taille d’arbre de 4 (paramètre de complexité égal
à 0.017) n’apporte qu’un gain mineur en ce qui concerne l’évaluation de l’erreur.
Vous pouvez extraire ces valeurs par programme à partir de la colonne cptable
du modèle, comme ceci :

fit$cptable

## CP nsplit rel error xerror xstd


## 1 0.62288527 0 1.0000000 1.0271438 0.17736964
## 2 0.13206061 1 0.3771147 0.5274319 0.10090361
## 3 0.02544094 2 0.2450541 0.3970990 0.08167442
## 4 0.01160389 3 0.2196132 0.3769729 0.08136370
## 5 0.01000000 4 0.2080093 0.3921772 0.07988950

Vous pouvez voir que l’erreur est minimisée pour des tailles d’arbre de 3 et de 4.
Utilisons donc la valeur de xerror pour 4 divisions dans la fonction d’élagage
prune() pour stopper tout fractionnement au-delà de ce niveau de complexité
(voir la Figure 6.14) :

fit.pruned <- prune(fit, cp =


fit$cptable[which.min(fit$cptable[,
"xerror"]), "CP"])

par(mfrow = c(1, 2))

plot(fit, uniform = TRUE, margin = 0.1, main = "Arbre


original")
text(fit, use.n = TRUE, all = TRUE, cex = 0.8)

plot(fit.pruned, uniform = TRUE, margin = 0.1, main =


"Arbre élagué")
text(fit.pruned, use.n = TRUE, all = TRUE, cex = 0.8)

Cet exemple prend le paramètre de complexité, cp, et le passe à la fonction


prune() pour éliminer les divisions qui ne permettent pas au modèle de réduire
son erreur.

Figure 6.14 : Un arbre élagué. Par rapport à la Figure 6.10, nous avons supprimé totalement une branche
et évité quelques erreurs dans le modèle.

Arbres de décision pour la régression


Nous avons couvert de nombreuses notions concernant les arbres de décision et
la manière de les mettre au point pour obtenir les meilleures performances
possibles. Si vous voulez fabriquer un modèle de régression simple en utilisant
la fonction rpart() avec des arbres de décision, vous devez d’abord faire croître
votre arbre puis ensuite l’élaguer, comme le démontre cet exemple de code :

cu.summary.complete <-
cu.summary[complete.cases(cu.summary),]
data.samples <- sample(1:nrow(cu.summary.complete),
nrow(cu.summary.complete) *
0.7, replace = FALSE)
training.data <- cu.summary.complete[data.samples, ]
test.data <- cu.summary.complete[-data.samples, ]

fit <- rpart(


Mileage~Price + Country + Reliability + Type,
method="anova", #method="class" pour la
classification
data=training.data
)

fit.pruned<- prune(fit,
cp=fit$cptable[which.min(fit$cptable[,"xerror"]),"CP"])

prediction <- predict(fit.pruned, test.data)


output <- data.frame(test.data$Mileage, prediction)

RMSE = sqrt(sum((output$test.data.Mileage -
output$prediction)^2) /
nrow(output))
RMSE

## [1] 2.318792

Arbres de décision pour la classification


Répéter l’exercice avec rpart() pour la classification est très facile. Tout ce que
vous avez à faire est de passer l’option de méthode anova à class, ainsi que de
passer la réponse que vous modélisez à une variable class réelle :
cu.summary.complete <-
cu.summary[complete.cases(cu.summary),]

data.samples <- sample(1:nrow(cu.summary.complete),


nrow(cu.summary.complete) *
0.7, replace = FALSE)
training.data <- cu.summary.complete[data.samples, ]
test.data <- cu.summary.complete[-data.samples, ]

fit <- rpart(Type ~ Price + Country + Reliability +


Mileage,
method = "class", data = training.data)

fit.pruned <- prune(fit, cp =


fit$cptable[which.min(fit$cptable[,
"xerror"]), "CP"])

prediction <- predict(fit.pruned, test.data, type =


"class")
table(prediction, test.data$Type)

##
## prediction Compact Large Medium Small Sporty Van
## Compact 4 0 0 1 1 0
## Large 0 0 0 0 0 0
## Medium 1 2 2 0 0 0
## Small 0 0 0 4 0 0
## Sporty 0 0 0 0 0 0
## Van 0 0 0 0 0 0
Arbres d’inférence conditionnelle
Les arbres d’inférence conditionnelle constituent une variété légèrement
différente des arbres de décision. Jusqu’ici, nous avons vu comment développer
et élaguer un arbre de décision construit dans R à l’aide de la fonction rpart().
Cette fonction produit un arbre par sélection des caractéristiques en partant des
valeurs les plus élevées du gain d’information. Dans de nombreux cas, nous
devons élaguer ces types d’arbres pour éviter des problèmes de surapprentissage
et qu’il n’y ait pas trop d’erreurs par division de l’arbre.
Un arbre d’inférence conditionnelle suit une logique très similaire, mais la façon
dont nous effectuons la division est légèrement différente. Un arbre d’inférence
conditionnelle s’appuiera davantage sur des tests statistiques robustes pour une
caractéristique donnée afin de déterminer sa signification, ou son importance
(d’un point de vue statistique).
Nous pouvons voir cela en action en traçant un arbre d’inférence conditionnelle
à partir d’un ajustement de modèle réalisé à l’aide de la fonction ctree()
provenant du package heureusement nommé party (voir la Figure 6.15) :

library(party)

fit2 <- ctree(Mileage ~ Price + Country + Reliability +


Type,
data = na.omit(cu.summary))

plot(fit2)
Figure 6.15 : Un arbre de décision tracé à l’aide de la fonction ctree() de la bibliothèque party.

Comme beaucoup de packages R, certains sont meilleurs que d’autres pour


produire des tracés. Bien qu’il y ait des options pour améliorer le rendu des
arbres dessinés à partir de la fonction rpart(), ctree() offre de meilleurs visuels.
Sur la Figure 6.15, vous pouvez voir un arbre comprenant deux caractéristiques,
le prix (Price) et le type (Type), dont le fractionnement est illustré. Comme
d’habitude, les critères de ce fractionnement sont visibles sur les branches, mais
il y a un nouveau paramètre : la p-valeur. Il s’agit des valeurs à l’intérieur des
bulles qui montrent la caractéristique sur laquelle nous effectuons la division.
Une p-valeur est un outil permettant de mesurer la signification statistique de
quelque chose. La règle suivie par les statisticiens est la suivante : une p-valeur
inférieure à 0.05 est considérée comme statistiquement significative. Aucun
élagage n’est nécessaire pour ce type particulier d’arbre, car il est déjà intégré
aux procédures statistiques qui sélectionnent les caractéristiques à diviser, ce qui
vous permet d’économiser une étape de calcul.
De même, si vous voulez tracer l’arbre pour un schéma de classification, tout ce
que vous avez à faire est de fournir un facteur variable en réponse et de répéter la
même procédure de traçage, comme le montre la Figure 6:16 :

fit3 <- ctree(Type ~ Price + Country + Reliability +


Mileage,
data = na.omit(cu.summary))

plot(fit3)

Figure 6.16 : Un modèle de classification tracé à l’aide de la fonction ctree() peut parfois avoir des sorties
difficiles à visualiser quand il y a beaucoup de classes impliquées.

Si vous avez beaucoup de facteurs variables, le graphique de l’arbre peut devenir


quelque peu encombrant, car le tracé par défaut inclut des valeurs qui sont nulles
pour un certain fractionnement des données.

Régression avec les arbres d’inférence conditionnelle


L’exécution d’un modèle de régression à l’aide d’arbres d’inférence
conditionnelle devrait vous sembler familière. Étant donné que presque tous les
modèles d’apprentissage automatique de R répondent au même schéma de base
« fonction(réponse ~ caractéristiques) », il n’est pas surprenant que l’exécution
d’une régression avec ctree suive la même formule :

set.seed(123)

cu.summary.complete <-
cu.summary[complete.cases(cu.summary),]

data.samples <- sample(1:nrow(cu.summary.complete),


nrow(cu.summary.complete) *
0.7, replace = FALSE)
training.data <- cu.summary.complete[data.samples, ]
test.data <- cu.summary.complete[-data.samples, ]

fit.ctree <- ctree(Mileage ~ Price + Country +


Reliability +
Type, data = training.data)

prediction.ctree <- predict(fit.ctree, test.data)

output <- data.frame(test.data$Mileage, prediction.ctree)

RMSE = sqrt(sum((output$test.data.Mileage -
output$Mileage)^2)/nrow(output))
RMSE

## [1] 3.3747

Classification avec les arbres d’inférence


conditionnelle
La réalisation d’un modèle de classification dans R est tout aussi simple. Du fait
que vous avez déjà exécuté le processus avec un exemple de régression, tout ce
que vous avez à faire pour passer à une classification est de changer la réponse
numérique en réponse catégorielle :

set.seed(456)

data.samples <- sample(1:nrow(cu.summary),


nrow(cu.summary) *
0.7, replace = FALSE)
training.data <- cu.summary[data.samples, ]
test.data <- cu.summary[-data.samples, ]

fit.ctree <- ctree(Type ~ Price + Country + Reliability +


Mileage,
data = training.data)

prediction.ctree <- predict(fit.ctree, test.data)

table(test.data$Type, prediction.ctree)

## prediction.ctree
## Compact Large Medium Small Sporty Van
## Compact 2 0 5 1 0 0
## Large 1 0 1 0 0 1
## Medium 2 0 5 0 0 0
## Small 2 0 0 4 0 0
## Sporty 7 0 0 2 0 1
## Van 1 0 0 0 0 1
Forêts aléatoires
Les méthodes arborescentes dont il a été question jusqu’à présent ont toutes
porté sur un seul arbre. Autrement dit, nous nous sommes basés uniquement sur
une logique « if-then », qui consiste à partir d’une caractéristique, à effectuer une
division en fonction des plages de valeurs de cette caractéristique, puis à
descendre dans l’arbre jusqu’au résultat final. L’une des formes les plus à la
pointe de l’apprentissage automatique est appelée une forêt aléatoire. Au lieu de
cultiver un seul arbre, nous allons maintenant faire pousser N arbres différents.
Nous obtenons ces arbres différents en « randomisant » nos entrées dans
l’algorithme qui les produit pour nous.
Chaque arbre va produire une certaine sortie basée sur les divisions des
caractéristiques dans les données, exactement comme nous l’avons vu dans ce
qui précède. La différence est que nous allons observer les résultats pour chaque
arbre, et retenir la sortie qui a le plus grand nombre de votes. Celle-ci devient la
sortie pour toute la forêt.
Dans le cas d’une classification, nous pourrions avoir un groupe d’arbres qui
ressemblerait à celui qui est illustré sur la Figure 6.17.

Figure 6.17 : Exemple de fonctionnement d’un modèle de forêt aléatoire.

Nous avons trois arbres, chacun avec une sortie appartenant à une classe A ou B.
Notez que les arbres ont des caractéristiques différentes sur lesquelles ils
pourraient être fractionnés. Comme vous construisez les arbres à partir de sous-
ensembles de vos données de départ qui sont formés de manière aléatoire, ces
sous-ensembles peuvent avoir des paramètres de fractionnement différents les
uns des autres.
L’étape suivante consisterait à passer un certain type d’entrée dans la forêt (nos
trois arbres). Supposons qu’après avoir transmis cette entrée à chaque arbre,
vous obtenez une prédiction comme celle-ci :

• Arbre 1 : A

• Arbre 2 : B

• Arbre 3 : A
Vous prenez alors un vote majoritaire à partir des classes qui sont produites par
les arbres afin d’obtenir la réponse finale de la forêt aléatoire. Dans ce cas, cette
réponse serait la classe A.

Forêts aléatoires et régression


La régression avec des forêts aléatoires dans R revient simplement à remplacer
la caractéristique que vous modélisez comme réponse. Tout ce que vous avez à
faire dans ce cas pour passer d’un arbre d’inférence conditionnelle à une forêt
aléatoire consiste à changer l’appel de fonction que vous utilisez sur les données
d’entraînement, comme le montre ce code :

library(randomForest)
set.seed(123)

cu.summary.complete <-
cu.summary[complete.cases(cu.summary),]
data.samples <- sample(1:nrow(cu.summary.complete),
nrow(cu.summary.complete) *
0.7, replace = FALSE)
training.data <- cu.summary.complete[data.samples, ]
test.data <- cu.summary.complete[-data.samples, ]

fit.rf <- randomForest(Mileage ~ Price + Country +


Reliability +
Type, data = training.data)
prediction.rf <- predict(fit.rf, test.data)

output <- data.frame(test.data$Mileage, prediction.rf)

RMSE = sqrt(sum((output$test.data.Mileage -
output$prediction.rf)^2)/
nrow(output))
RMSE

## [1] 3.229279

Forêts aléatoires et classification


Le code pour la classification avec des forêts aléatoires dans R est aussi facile à
mettre en place. Il suffit de régler correctement l’appel à la fonction
randomForest() :

set.seed(456)

cu.summary.complete <-
cu.summary[complete.cases(cu.summary),]

data.samples <- sample(1:nrow(cu.summary.complete),


nrow(cu.summary.complete) *
0.7, replace = FALSE)
training.data <- cu.summary.complete[data.samples, ]
test.data <- cu.summary.complete[-data.samples, ]

fit.rf <- randomForest(Type ~ Price + Country +


Reliability +
Mileage, data = training.data)
prediction.rf <- predict(fit.rf, test.data)

table(test.data$Type, prediction.rf)

## prediction.rf
## Compact Large Medium Small Sporty Van
## Compact 3 0 1 0 0 0
## Large 0 0 1 0 0 0
## Medium 0 0 1 0 0 0
## Small 0 0 0 4 0 0
## Sporty 1 0 0 0 2 0
## Van 0 0 1 0 1 0
En résumé
Dans ce chapitre, nous avons discuté des modèles d’apprentissage automatique
dans R liés aux méthodes arborescentes. Un arbre peut être aussi simple qu’une
représentation pictographique d’énoncés de type « if-then », mais, contrairement
à un arbre dans la nature, notre modèle arborescent est construit de haut en bas,
et l’on se déplace le long des branches de l’arbre en fonction du résultat des
énoncés.
Les modèles arborescents en général sont utiles en raison de leur capacité à
traiter les problèmes de données manquantes et de valeurs aberrantes, et ils sont
raisonnablement compacts. Mais leur force première réside dans leur simplicité.
Il est beaucoup plus facile de dessiner un arbre de décision sur un tableau blanc
que la plupart des autres algorithmes d’apprentissage automatique.
Les algorithmes des arbres de décision fonctionnent en classant les
caractéristiques d’un jeu de données en fonction d’un paramètre spécifique,
parfois selon le gain d’information, d’autres fois à partir de mesures statistiques
comme les p-valeurs. Vous commencez un arbre en partant de la variable la plus
importante, puis vous le fractionnez en fonction des conditions trouvées, et vous
répétez le fractionnement si nécessaire. Dans certains cas, vous devrez peut-être
élaguer vos arbres pour les empêcher d’avoir trop de branches et d’engendrer un
surapprentissage sur les données avec lesquelles vous les entraînez.
Finalement, nous avons abordé l’apprentissage automatique à l’aide de forêts
aléatoires. Une forêt aléatoire est une collection d’arbres de décision différents
générés à partir de données de départ choisies aléatoirement afin de former des
sous-ensembles (tout aussi aléatoires) de votre jeu d’entraînement. Ces arbres
vont alors prendre les données qu’ils reçoivent en entrée et donner une
estimation de ce qu’ils pensent être la bonne réponse. Lorsque tous les arbres de
la forêt ont une réponse, le résultat final est déterminé par un vote majoritaire.
CHAPITRE 7
Autres méthodes avancées
Dans ce chapitre, nous allons présenter divers modèles d’apprentissage
automatique disponibles dans R. Bien que les principaux algorithmes que nous
avons étudiés jusqu’à présent constituent la majorité des modèles effectivement
employés, j’ai voulu inclure ce chapitre pour fournir une vue d’ensemble de
l’écosystème de l’apprentissage automatique dans R.
Nous allons à nouveau traiter de classification, mais cette fois sous l’angle des
statistiques bayésiennes. Il s’agit d’un domaine populaire dans le monde des
statistiques, et qui aide aussi à se diriger vers d’autres algorithmes dépendant
d’une logique similaire. Nous couvrirons également l’analyse en composantes
principales, les séparateurs à vaste marge et l’algorithme des k plus proches
voisins.
Classification naïve bayésienne
Une façon d’effectuer une classification avec des probabilités consiste à utiliser
les statistiques bayésiennes. Bien que l’apprentissage dans ce domaine puisse
être assez difficile, nous allons ici essayer essentiellement de répondre à la
question suivante : « Sur la base des caractéristiques dont nous disposons, quelle
est la probabilité pour que le résultat soit de classe X ? ». Un classifieur bayésien
naïf répond à cette question par une hypothèse plutôt audacieuse : tous les
prédicteurs dont nous disposons sont indépendants les uns des autres. L’avantage
de cette hypothèse est que nous réduisons ainsi considérablement la complexité
des calculs à opérer.
Selon les sources, on rencontre les deux expressions, classification naïve bayésienne et
classification bayésienne naïve. Il s’agit évidemment de la même chose.

Les statistiques bayésiennes en bref


Les statistiques bayésiennes reposent beaucoup sur la multiplication de
probabilités. Faisons une brève introduction à ce sujet. Supposons que je
fasse 100 courses de vélo et que j’en gagne 54 (si seulement !). La probabilité
que je gagne une course est donc simplement le nombre de fois où j’ai gagné
divisé par le nombre total d’occurrences :
P (gagne) = 54/100 = 54 %

Parlons maintenant des probabilités indépendantes et dépendantes. Supposons


que je veuille trouver la probabilité pour que je gagne une course de vélo et la
probabilité pour qu’une tempête de vent se produise sur Mars. Ces deux choses
sont certainement complètement indépendantes l’une de l’autre, étant donné
qu’une tempête de vent sur Mars ne pourrait pas affecter le résultat de ma course
de vélo (du moins selon toute vraisemblance), et vice versa. Supposons ensuite
que la probabilité qu’une tempête de vent se déchaîne sur Mars est de 20 %.
Pour calculer la probabilité que ces deux choses indépendantes se produisent, il
suffit de les multiplier :
P (gagne et Mars) = P (gagne) × P (Mars) = 54 % × 20 % = 10.8 %

Cependant, si deux événements se révélaient être dépendants, nous devrions


utiliser une approche légèrement différente. Pensez à un jeu de cartes. La
probabilité que je choisisse une reine est de 4/52. La probabilité que je tire un as
après avoir choisi une reine, cependant, est dépendante, puisque nous venons de
retirer une carte du jeu. La probabilité pour l’as serait maintenant de 4/51.
L’équation correspondante se présenterait donc ainsi :

Nous pouvons réécrire notre probabilité comme ceci :


P (A et B) = P (A) × P(B | A)

et la réarranger pour lui donner une autre forme :


P (A et B) = P (A) × P(B | A)

En clair, c’est ce qu’expriment ces étapes :


l. Commencer par la probabilité de A et B, qui est la probabilité de A
multipliée par la probabilité de B, étant donné A.
2. Diviser la probabilité de A et B par la probabilité de A.
3. Remplacer la probabilité de A et B du côté gauche par la définition de
départ.
Ce que nous obtenons, c’est la formule naïve de Bayes. Elle nous indique les
probabilités conditionnelles d’un événement, compte tenu d’informations ou de
preuves antérieures sur les caractéristiques que nous examinons. La partie
« naïve » vient de l’affirmation audacieuse que les caractéristiques qui nous
intéressent sont indépendantes.

Application de la formule naïve bayésienne


Appliquons cette formulation mathématique dans R. La bibliothèque e1071 a
une fonction utile, naiveBayes(), servant à construire des modèles de ce type. En
utilisant la formule de Bayes mentionnée ci-dessus, vous pouvez calculer les
probabilités conditionnelles d’une classe catégorielle. À titre d’exemple, nous
allons utiliser des données relatives aux études sur le cancer du sein réalisées par
une structure américaine : University of Wisconsin Hospitals and Clinics.
Il y a neuf caractéristiques dans ce jeu de données, une colonne étant un
identificateur sans intérêt, une autre colonne étant la désignation de la classe
pour le type de cellule, où 2 représente une cellule bénigne et 4 une cellule
maligne. Le reste des caractéristiques se rapporte à la cellule étudiée, avec des
caractéristiques telles que « Uniformité de la taille des cellules » et « Taille des
cellules épithéliales uniques ». Voici le code :
Le jeu de données n’est pas inclus dans la distribution de R. Pour le récupérer, le mieux
est sans doute d’accéder à la page suivante sur Github :
https://github.com/nrkfeller/machinelearningnotes/blob/master/breast-cancer-
wisconsin.data.txt
Cliquez alors sur le bouton Raw, copiez tout le contenu de la fenêtre, puis sauvegardez-le
dans un fichier texte appelé breast_cancer.txt dans le répertoire courant de la console
RGui. Pour retrouver si nécessaire ce répertoire, choisissez l’option Changer le répertoire
courant dans le menu Fichier.

library(e1071)

breast_cancer <-
data.frame(read.table("breast_cancer.txt", header = T,
sep = ","))

names(breast_cancer) <- c("id",


"clump_thickness","unif_cell_size",
"unif_cell_shape",
"marg_adhesion","single_epith_cell_size",
"bare_nuclei", "bland_chrom", "norm_nucleoli",
"mitoses", "class")

breast_cancer <- data.frame(sapply(breast_cancer,


as.factor))

breast_cancer_features <- breast_cancer[, 2:11]

nb.model <- naiveBayes(class ~ ., data =


breast_cancer_features)
print(nb.model)
## Naive Bayes Classifier for Discrete Predictors
##
## Call:
## naiveBayes.default(x = X, y = Y, laplace = laplace)
##
## A-priori probabilities:
## Y
## 2 4
## 0.6552217 0.3447783
##
## Conditional probabilities:
## clump_thickness
## Y 1 10 2 3
4 5
## 2 0.310043668 0.000000000 0.100436681 0.209606987
0.148471616 0.185589520
## 4 0.012448133 0.286307054 0.016597510 0.049792531
0.049792531 0.186721992
## clump_thickness
## Y 6 7 8 9
## 2 0.034934498 0.002183406 0.008733624 0.000000000
## 4 0.074688797 0.091286307 0.174273859 0.058091286
##
## unif_cell_size
## Y 1 10 2 3
4 5
## 2 0.829694323 0.000000000 0.080786026 0.058951965
0.019650655 0.000000000
## 4 0.016597510 0.278008299 0.033195021 0.103734440
0.128630705 0.124481328
## unif_cell_size
## Y 6 7 8 9
## 2 0.004366812 0.002183406 0.002183406 0.002183406
## 4 0.103734440 0.074688797 0.116182573 0.020746888
##
## unif_cell_shape
## Y 1 10 2 3
4 5
## 2 0.766375546 0.000000000 0.113537118 0.072052402
0.028384279 0.006550218
## 4 0.008298755 0.240663900 0.029045643 0.095435685
0.128630705 0.128630705
## unif_cell_shape
## Y 6 7 8 9
## 2 0.006550218 0.004366812 0.002183406 0.000000000
## 4 0.112033195 0.116182573 0.112033195 0.029045643
##

………

Ce que vous voyez comme un résultat du modèle représente quelques propriétés


différentes. Tout d’abord, il y a les « probabilités a priori », qui vous informent
sur la distribution des classes pour la variable dépendante que vous modélisez.
La deuxième propriété concerne les « probabilités conditionnelles ». Il s’agit
d’une liste de tableaux, un pour chaque variable prédictive. Notez que j’ai
tronqué la sortie en la limitant aux trois premières caractéristiques, la suite
n’apportant pas plus d’informations utiles. Remarquez cependant que, pour
chacune des caractéristiques, il existe des probabilités conditionnelles pour les
facteurs de la réponse. Ainsi, par exemple, la caractéristique clump_thickness
contient 10 variables catégorielles différentes. Pour chacune de ces catégories,
on trouve les probabilités conditionnelles de la classe de cellules. Rappelons que
la classe 2 signifie que la cellule est bénigne, alors que la classe 4 indique qu’elle
est maligne. Cette sortie tabulaire vous permet de voir en détail les probabilités
naïves de Bayes pour chacune des caractéristiques.
L’étape logique suivante consiste à utiliser cet algorithme à des fins prédictives.
Vous pouvez le faire en suivant la méthode désormais classique qui consiste à
diviser les données en un jeu d’entraînement et un jeu de test, à modéliser le jeu
d’entraînement, puis à produire une matrice de confusion de la variable
prédictive :

breast_cancer_complete <-

breast_cancer_features[complete.cases(breast_cancer_features),

]
breast_cancer_complete$Class <-
as.factor(breast_cancer_complete$class)
data.samples <- sample(1:nrow(breast_cancer_complete),
nrow(breast_cancer_complete) *
0.7, replace = FALSE)
training.data <- breast_cancer_complete[data.samples, ]
test.data <- breast_cancer_complete[-data.samples, ]

nb.model <- naiveBayes(class ~ ., data = training.data)

prediction.nb <- predict(nb.model, test.data)

table(test.data$Class, prediction.nb)

## prediction.nb
## 2 4
## 2 132 3
## 4 0 75

Ici, nous voyons que le modèle naïf bayésien semble fonctionner plutôt bien, ce
qui nous donne en sortie une prédiction assez exacte.
Analyse en composantes principales
L’analyse en composantes principales (ou PCA) est un type d’apprentissage
automatique dont nous nous servons comme étape de prétraitement des données
pour aider avec quelques approches différentes. Dans de nombreux cas d’analyse
de données, nous pourrions nous trouver avec des caractéristiques fortement
corrélées les unes avec les autres. Si nous devions injecter ces données dans un
modèle d’apprentissage automatique sans aucune procédure de sélection
préalable de caractéristiques, nous pourrions nous retrouver avec une erreur
supplémentaire lors de la modélisation, précisément du fait de la forte corrélation
entre nos caractéristiques.
Supposons par exemple que vous voulez modéliser les ventes d’un produit en
fonction de l’économétrie de divers pays. Vous pourriez avoir des données
possédant un certain nombre de caractéristiques comme le pays, l’année, la
population, le pourcentage d’utilisateurs connectés à Internet en haut débit, le
pourcentage de la population urbaine, le PIB, le PIB par habitant, l’indice de
pauvreté, l’espérance de vie, et ainsi de suite. En théorie, certaines de ces valeurs
sont très dépendantes les unes des autres, par exemple le PIB et la population.
Dans certains cas, elles peuvent être corrélées linéairement. Dans ce cas, une
analyse en composantes principales consisterait à réduire cette corrélation entre
le PIB et la population à une seule caractéristique, qui serait la relation
fonctionnelle entre les deux.
Si vous aviez un certain jeu de données avec 30 ou 40 caractéristiques, la plupart
étant fortement corrélées entre elles, vous pourriez exécuter un algorithme PCA
sur ce jeu et réduire les données à seulement deux caractéristiques. Cela
diminuerait considérablement la complexité du jeu de données en termes de
calcul informatique.
Grâce à un algorithme PCA, vous pourriez réduire vos données en partant de
quelque chose qui ressemble à ceci :

head(mtcars)

## mpg cyl disp hp drat wt qsec


vs am gear carb
## Mazda RX4 21.0 6 160 110 3.90 2.620 16.46
0 1 4 4
## Mazda RX4 Wag 21.0 6 160 110 3.90 2.875 17.02
0 1 4 4
## Datsun 710 22.8 4 108 93 3.85 2.320 18.61
1 1 4 1
## Hornet 4 Drive 21.4 6 258 110 3.08 3.215 19.44
1 0 3 1
## Hornet Sportabout 18.7 8 360 175 3.15 3.440 17.02
0 0 3 2
## Valiant 18.1 6 225 105 2.76 3.460 20.22
1 0 3 1

pour obtenir une forme plus compacte, comme celle-ci :

## x1 x2
## Mazda RX4 1.5560338 2.391719
## Mazda RX4 Wag 1.1481763 2.754611
## Datsun 710 0.2824424 2.622031
## Hornet 4 Drive 3.7019535 2.806743
## Hornet Sportabout 3.2649748 2.483172
## Valiant 4.1630202 2.048424

L’analyse en composantes principales pour la réduction de la dimensionnalité


des données peut être tout à fait utile pour visualiser des motifs complexes. Les
cerveaux humains sont des experts de la visualisation, et nous savons très bien
lire un graphique en deux dimensions. Les visualisations en trois dimensions
peuvent être facilement discernées par le cerveau dans la vie réelle, mais elles
sont un peu plus difficiles à juger sur un écran d’ordinateur, qui est lui-même un
plan bidimensionnel. La Figure 7.1 montre un exemple de données qui
pourraient utiliser une passe PCA :

pairs(mtcars[, 1:7], lower.panel = NULL)


Figure 7.1 : Une sélection de variables du jeu de données mtcars. Vous pouvez utiliser PCA pour trouver
des corrélations entre les données, et réduire la complexité du jeu en vue d’un traitement ultérieur.

Dans le cas du jeu de données mtcars, il y a un bon nombre de caractéristiques,


dont certaines semblent être corrélées entre elles. Une bonne pratique générale,
avant d’appliquer une analyse en composantes principales, consiste à jeter un
coup d’œil à vos données pour voir s’il existe effectivement des valeurs qui
semblent pouvoir être corrélées.
À première vue, il apparaît qu’il existe des valeurs bien corrélées, dont beaucoup
correspondent à la variable poids du véhicule, wt. Voyons comment vous pouvez
réduire certaines des dépendances de ces variables et générer une image plus
simplifiée des données.
Dans R, il y a deux fonctions qui sont assez similaires en termes de syntaxe et
qui peuvent exécuter directement une analyse en composantes principales :
princomp et prcomp. L’un de vos premiers objectifs est de visualiser en quoi un
certain nombre de composantes principales peut expliquer la variance dans vos
données. La fonction princomp dispose d’une fonctionnalité intégrée simple qui
se prête mieux au traçage. C’est donc elle que nous allons choisir dans cet
exemple :
pca <- princomp(mtcars, scores = TRUE, cor = TRUE)

Vous pouvez utiliser l’argument scores pour stocker certaines données utilisées
pour la notation de chaque composante (nous allons y revenir tout de suite).
L’argument cor demande à utiliser une matrice de corrélation pour les calculs au
lieu d’une matrice de covariance. Les différences sont subtiles. Disons qu’elles
dépendent des données ou du type de calcul que vous voulez faire. Mais essayer
d’aller plus loin ici nous ferait sortir du cadre de ce livre. Nous allons donc
simplement nous contenter ici de la matrice de corrélation.
Jetons un coup d’œil à la sortie de l’objet pca :

summary(pca)

## Importance of components:
## Comp.1 Comp.2 Comp.3
Comp.4
## Standard deviation 2.5706809 1.6280258 0.79195787
0.51922773
## Proportion of Variance 0.6007637 0.2409516 0.05701793
0.02450886
## Cumulative Proportion 0.6007637 0.8417153 0.89873322
0.92324208
## Comp.5 Comp.6
Comp.7 Comp.8
## Standard deviation 0.47270615 0.45999578
0.36777981 0.35057301
## Proportion of Variance 0.02031374 0.01923601
0.01229654 0.01117286
## Cumulative Proportion 0.94355581 0.96279183
0.97508837 0.98626123
## Comp.9 Comp.10
Comp.11
## Standard deviation 0.277572792 0.228112781
0.148473587
## Proportion of Variance 0.007004241 0.004730495
0.002004037
## Cumulative Proportion 0.993265468 0.997995963
1.000000000
Ce tableau montre quelle est l’importance de chacune de ces mystérieuses
composantes principales pour l’ensemble du jeu de données. La ligne qui vous
intéresse le plus est la seconde, Proportion of Variance, qui vous indique dans
quelle mesure les données sont expliquées par cette composante principale.
Les composantes sont toujours triées en fonction de leur poids, de sorte que les
plus importantes apparaîtront en premier. Dans la sortie précédente, vous pouvez
voir que la composante 1 explique 60 % des données, la composante 2 suivant
avec 24 %, le reste devenant très rapidement marginal. La Figure 7.2 représente
graphiquement ces données :

plot(pca)

Ce graphique illustre bien le fait que la première composante principale explique


une grande partie des données. Combinée avec la composante 2, on arrive à
expliquer plus de 84 % du jeu de données avec seulement deux caractéristiques
au lieu des onze avec lesquelles nous avions commencé.

Figure 7.2 : Variance de nos diverses composantes. C’est une façon plus visuelle de se représenter dans
quelle mesure nos composantes principales expliquent les données.
Mais ces composantes principales n’en semblent pas moins mystérieuses. Que
signifie ainsi la composante 1 pour nous en tant qu’humains ou décideurs ? Dans
l’objet pca, vous pouvez étudier le contenu de la colonne loadings pour voir
quelle part de chaque variable est contenue dans chaque composante que vous
observez :

pca$loadings[, 1:5]

## Comp.1 Comp.2 Comp.3 Comp.4


Comp.5
## mpg 0.3625305 0.01612440 0.22574419 0.022540255
0.10284468
## cyl -0.3739160 0.04374371 0.17531118 0.002591838
0.05848381
## disp -0.3681852 -0.04932413 0.06148414 -0.256607885
0.39399530
## hp -0.3300569 0.24878402 -0.14001476 0.067676157
0.54004744
## drat 0.2941514 0.27469408 -0.16118879 -0.854828743
0.07732727
## wt -0.3461033 -0.14303825 -0.34181851 -0.245899314
-0.07502912
## qsec 0.2004563 -0.46337482 -0.40316904 -0.068076532
-0.16466591
## vs 0.3065113 -0.23164699 -0.42881517 0.214848616
0.59953955
## am 0.2349429 0.42941765 0.20576657 0.030462908
0.08978128
## gear 0.2069162 0.46234863 -0.28977993 0.264690521
0.04832960
## carb -0.2140177 0.41357106 -0.52854459 0.126789179
-0.36131875

Ces valeurs sont les corrélations entre la composante principale et les


caractéristiques avec lesquelles vous avez commencé. Cet exemple ne montre
que les cinq premières composantes principales pour économiser de l’espace, les
composantes 6 à 9 n’étant de toute façon pas vraiment utiles. Plus la valeur de
corrélation est proche de 1 ou -1 pour chaque combinaison de composantes et de
caractéristiques, et plus cette caractéristique est importante pour cette
composante. Observons par exemple la composante l. Celle-ci présente un
équilibre pour toutes les caractéristiques de départ, mpg étant la valeur positive
dominante, et cyl la valeur négative dominante. La composante 2 est
principalement dominée par les variables qsec, gear et am, dans cet ordre. Il en
va de même pour le reste des composantes.
Si vous deviez par conséquent attribuer une espèce de relation entre les
composantes et les caractéristiques, vous pourriez dire quelque chose comme
ceci :

• La composante 1 est corrélée à mpg et à cyl.

• La composante 2 est corrélée à qsec, gear et am.


Si vous vouliez visualiser ce genre d’information d’une manière plus graphique,
vous pourriez tracer les scores des composantes principales, comme l’illustre la
Figure 7.3 :

scores.df <- data.frame(pca$scores)


scores.df$car <- row.names(scores.df)

plot(x = scores.df$Comp.1, y = scores.df$Comp.2, xlab =


"Comp1 (mpg,cyl)",
ylab = "Comp2 (qsec, gear, am)")

text(scores.df$Comp.1, scores.df$Comp.2, labels =


scores.df$car,
cex = 0.7, pos = 3)

Ce que nous avons fait lors cette dernière étape, c’est de montrer qu’une bonne
partie des données peut être compressée en deux composantes principales : l’une
ayant trait principalement aux variables mpg et cyl, et l’autre étant une
combinaison des variables qsec, gear et am.
Sur la Figure 7.3, vous pouvez voir que certaines voitures se retrouvent dans
certaines extrémités du spectre par rapport à d’autres, mais qu’elles peuvent très
bien être liées les unes aux autres en fonction de nombreux facteurs qui sont
compressés en une ou deux variables.
Notez que les valeurs des axes sont également un peu différentes des valeurs des
variables de départ. Cela vient du fait que certains algorithmes PCA ont des
techniques intégrées de mise à l’échelle des caractéristiques qui permettent de
s’assurer que toutes les variables se situent dans la même plage les unes par
rapport aux autres à des fins de comparaison. Si vous aviez par exemple une
variable (disons le poids du véhicule) susceptible d’être des centaines ou des
milliers de fois plus grande qu’une autre (comme le nombre de cylindres),
l’analyse pourrait être très trompeuse. Avec la fonction princomp, cette mise à
l’échelle est directement intégrée. Par contre, d’autres algorithmes PCA dans R
pourraient vous demander d’activer explicitement ce recalibrage.

Figure 7.3 : Un tracé des données en fonction des deux composantes principales. Les voitures regroupées
sur ce tracé sont très similaires les unes aux autres en fonction des composantes utilisées pour les décrire.

Analyse discriminante linéaire


L’algorithme PCA cherche à trouver une série de vecteurs qui décrivent la
variance dans les données. Par exemple, vous pourriez avoir des données avec
deux caractéristiques, X et Y, que vous pouvez tracer. Vous pouvez trouver une
paire de vecteurs qui expliquent de combien les données varient dans une
direction par rapport à une autre direction qui lui est orthogonale, comme le
montre la Figure 7.4.
Figure 7.4 : Les composantes principales décrivent la variance dans les données. Ici, il y a deux vecteurs
de composantes, la composante principale étant celle qui correspond au plus long des deux axes dans les
données.

Des jeux de données plus complexes peuvent avoir davantage de caractéristiques


et de vecteurs, mais l’idée est la même. En revanche, il existe aussi une autre
façon de procéder à l’analyse des caractéristiques, et qui est appelée analyse
discriminante linéaire (ou LDA). Vous pourriez avoir des données qui sont à
nouveau une fonction de X et de Y, mais, cette fois, vous voulez les classifier
dans différents groupes selon leur distribution (voir la Figure 7.5).
Sur la Figure 7.5, il y a un certain nombre de données qui sont tracées, mais
séparées en deux classes. Les données « + » ont une distribution sur l’axe X, de
même que les autres données. Toutefois, les données sur l’axe Y ne sont pas
distinguées ici par des classes différentes.
Voyons comment ces deux modèles se comparent l’un avec l’autre pour la
classification en prenant en exemple notre jeu de données iris. Utilisons d’abord
PCA sur les données du jeu, puis examinons la variance totale attribuée à
chacune des composantes, en commençant par appeler la fonction prcomp :

iris.pca <- prcomp(iris[, -5], center = T, scale. = T)


iris.pca$sdev^2/sum(iris.pca$sdev^2)

## [1] 0.727704521 0.230305233 0.036838320 0.005151927

Figure 7.5 : LDA décrit la meilleure façon de séparer des données en classes. Ici, il y a un jeu de données
qui est divisé efficacement par les distributions le long de l’axe X et de l’axe Y, respectivement.

Ici, PCA vous informe que vous avez pour l’essentiel deux composantes
principales. La composante 1 décrit la variance de 72 % des données, et la
composante 2, 23 % de la variance des données. Ces deux vecteurs combinés
décrivent un bon 96% des données. Vous pouvez ignorer les autres composantes
à ce stade (afin de garder des visualisations un peu plus simples).
Avant de nous lancer dans LDA, nous devons d’abord établir ce qu’est la
distribution préalable (ou antérieure) des données. Nous avons brièvement
abordé ce sujet en discutant des statistiques bayésiennes.
Pour faire simple, disons que la distribution antérieure est, pour l’essentiel, la
distribution des données que vous modélisez. Il peut arriver que vous ne sachiez
pas forcément avec certitude ce que pourrait être cette distribution. Dans le cas
du jeu de données iris, la question ne se pose pas. Du fait que vous exécutez un
modèle de classification sur les données iris, le seul type de classe de données
dont vous disposez est lié à la variable class, représentant les espèces (et qui peut
aussi s’appeler Species selon la source du jeu de données). Il est possible ici de
voir quelle serait la distribution antérieure en examinant cette variable
spécifique :

table(iris$class)

##
## Iris-setosa Iris-versicolor Iris-virginica
## 50 50 50

Ici, il y a trois classes, toutes également réparties. Dans ce cas, la distribution


antérieure serait de (1/3) pour chaque classe. Vous devez le spécifier sous forme
de vecteur lors de l’entraînement du modèle LDA. La suite fait appel
fondamentalement à la même approche mathématique :

library(MASS)

iris.lda <- lda(class ~ ., data = iris, prior = c(1/3,


1/3,
1/3))
iris.lda$svd^2/sum(iris.lda$svd^2)

## [1] 0.991472476 0.008527524

La sortie ici montre deux valeurs singulières, la première décrivant un


pourcentage énorme de 99 % de la variance dans les données, et l’autre un très
faible 0,8 %. Si vous voulez voir comment les deux discriminants linéaires sont
liés à chacune des caractéristiques des données de la même manière que ce que
vous avez fait avec PCA, il vous suffit d’appeler scaling, comme ceci :

iris.lda$scaling

## LD1 LD2
## sepallength 0.8192685 0.03285975
## sepalwidth 1.5478732 2.15471106
## petallength -2.1849406 -0.93024679
## petalwidth -2.8538500 2.80600460

Ensuite, vous pouvez faire calculer la matrice de confusion habituelle pour voir
comment les réponses du modèle LDA se comparent avec les espèces réelles du
jeu iris :

iris.lda.prediction <- predict(iris.lda, newdata = iris)

table(iris.lda.prediction$class, iris$class)

## Iris-setosa Iris-versicolor Iris-


virginica
## Iris-setosa 50 0
0
## Iris-versicolor 0 48
1
## Iris-virginica 0 2
49

Le modèle LDA semble être très précis. Vous pouvez ensuite essayer de
visualiser la différence entre PCA et LDA. Rappelons les formulations de ces
deux modèles. PCA est un apprenant non supervisé. Nous ne disons pas à PCA
d’essayer de scinder nos données en fonction d’une certaine classe. Avec LDA,
d’un autre côté, nous devons spécifier une classe servant à effectuer la division
des données. Il s’agit donc d’un modèle supervisé.
Les modèles supervisés ont tendance à mieux séparer les données que les
modèles non supervisés. La Figure 7.6 teste cela en comparant les résultats de
PCA par rapport à ceux de LDA :

combined <- data.frame(Espèces = iris[, "class"], pca =


iris.pca$x,
lda = iris.lda.prediction$x)

library(ggplot2)

library(gridExtra)
lda.plot <- ggplot(combined) + geom_point(aes(lda.LD1,
lda.LD2,
shape = Espèces)) + scale_shape_manual(values = c(0,
1, 2))

pca.plot <- ggplot(combined) + geom_point(aes(pca.PC1,


pca.PC2,
shape = Espèces)) + scale_shape_manual(values = c(0,
1, 2))
grid.arrange(pca.plot, lda.plot)

Figure 7.6 : Une comparaison entre PCA et LDA.

La Figure 7.6 montre les résultats pour PCA en haut et pour LDA en bas. Le but
ici est de voir comment chaque modèle sépare vos données. Avec PCA, notez
que les données de l’espèce Setosa sont bien séparées du reste, mais qu’il semble
y avoir un certain chevauchement entre les données Versicolor et Virginica aux
alentours de la zone pca.PC1=l.5. En comparaison, LDA sépare aussi bien les
données Setosa, mais il semble qu’il soit plus efficace pour contenir le
chevauchement entre les espèces Versicolor et Virginica à un minimum.
Séparateurs à vaste marge
Les séparateurs à vaste marge, ou encore machines à vecteurs de support
(support vector machine, ou SVM, en anglais), constituent un modèle
d’apprentissage automatique qui utilise des hyperplans pour séparer les données.
Pour séparer et partitionner nos données, nous déterminons une sorte de plan (ou
dans le cas de données bidimensionnelles, une ligne) qui les divise et nous
utilisons les vecteurs qui maximisent cette séparation, comme l’illustre la
Figure 7.7.

Figure 7.7 : Tracé d’un algorithme SVM simple appliqué sur quelques échantillons de données. Un plan,
ou une ligne, sépare nos données avec deux vecteurs de support donnant la distance maximale de
séparation entre les deux types de données et le plan lui-même.

Les SVM fonctionnent en employant ce qu’on appelle l’astuce du noyau. Il


s’agit d’une méthode par laquelle nous pouvons transformer les données pour
lesquelles nous essayons de tracer une frontière de décision, puis appliquer une
séparation par hyperplan sur ces données transformées.
Par exemple, si nous avions des données dans une sorte de cible, avec un groupe
entouré d’un anneau, il serait impossible de séparer les deux en utilisant une
ligne ou une surface bidimensionnelle. En revanche, si nous transformons nos
données en coordonnées polaires, nous pouvons facilement les séparer à l’aide
d’un hyperplan. Dans la pratique, cette transformation est plus ou moins une
boîte noire du fait que l’espace des caractéristiques peut être assez complexe,
mais l’idée est toujours la même.
Sur la Figure 7.8, vous pouvez voir les vecteurs qui partitionnent les données en
reprenant une fois de plus notre jeu de données préféré, iris :

library("e1071")

s <- sample(150, 100)


col <- c("petallength", "petalwidth", "class")
iris_train <- iris[s, col]
iris_test <- iris[-s, col]

svmfit <- svm(class ~ ., data = iris_train, kernel =


"linear",
cost = 0.1, scale = FALSE)

plot(svmfit, iris_train[, col])


Figure 7.8 : Un tracé des données avec une classification SVM et des frontières superposées.

Ce que nous voyons comme résultat sur la Figure 7.8, ce sont les frontières de la
classification telles qu’elles sont indiquées par le modèle d’entraînement du
SVM. Il est assez clair que les données qui se trouvent dans le coin inférieur
gauche peuvent être facilement séparées du reste et être classées de façon
appropriée. En revanche, il pourrait y avoir quelques ajustements nécessaires à
opérer pour séparer les données des espèces Versicolor et Virginica. Les X sur le
graphique montrent les vecteurs de support, tandis que les bandes délimitent les
régions correspond aux classes prédites.
Vous pouvez utiliser la fonction tune pour vous aider à trouver le meilleur
paramètre de coût pour un réglage optimal avec le SVM :

tuned <- tune(svm, class ~ ., data = iris_train, kernel =


"linear",
ranges = list(cost = c(0.001, 0.01, 0.1, 1, 10,
100)))

summary(tuned)
##
## Parameter tuning of 'svm':
##
## - sampling method: 10-fold cross validation
##
## - best parameters:
## cost
## 0.1
##
## - best performance: 0.03
##
## - Detailed performance results:
## cost error dispersion
## 1 1e-03 0.63 0.13374935
## 2 1e-02 0.30 0.12472191
## 3 1e-01 0.03 0.04830459
## 4 1e+00 0.03 0.04830459
## 5 1e+01 0.06 0.09660918
## 6 1e+02 0.06 0.09660918

Cela révèle que le meilleur paramètre de coût à utiliser est le 1. Vous pouvez
ensuite vous en servir pour réexécuter le modèle, comme le montre la
Figure 7.9 :

svmfit <- svm(class ~ ., data = iris_train, kernel =


"linear",
cost = 1, scale = FALSE)

plot(svmfit, iris_train[, col])

Vous pouvez également utiliser une classification SVM pour des frontières de
décision non linéaires. Dans les exemples précédents, vous avez vu comment
certains algorithmes d’apprentissage automatique scindent les données
uniquement avec des lignes droites. Par exemple, la régression logistique sépare
les données en utilisant des lignes droites. C’est aussi ce que font les arbres de
décision, mais en dessinant des boîtes autour des données.
Figure 7.9 : Un SVM ajusté offre une meilleure adaptation aux données.

Les SVM sont utiles parce qu’ils peuvent utiliser une méthode connue sous le
nom d’astuce du noyau pour transformer nos données, puis effectuer des
opérations sur ces données transformées. Cela nous permet de tracer des courbes
autour de nos données, plutôt que simplement des lignes droites, afin d’obtenir
de meilleurs ajustements. L’inconvénient, cependant, provient de la difficulté à
expliquer le modèle. Comme pour les réseaux de neurones, nous pouvons faire
passer des données à travers un SVM et obtenir des résultats significatifs, mais
décrire le processus par lequel les transformations se produisent peut souvent
être résumé en disant qu’il s’agit d’une opération « boîte noire ».
Jetons un coup d’œil à la Figure 7.10, qui montre comment vous pouvez
employer les SVM pour dessiner des frontières de décision courbes pour la
classification. Pour cet exemple, chargeons le jeu de données cats à partir du
package MASS :

plot(x = cats$Hwt, y = cats$Bwt, pch =


as.numeric(cats$Sex))
Figure 7.10 : Un tracé des données du jeu cats provenant du package MASS.

À première vue, il semble difficile de séparer les chats mâles (triangles) des
chats femelles (cercles). Ce que vous pouvez faire ici est d’exécuter un autre
modèle SVM sur ces données. Cela produira automatiquement une frontière non
linéaire, telle que vous pouvez la voir sur la Figure 7.11 :

library(MASS)
library(e1071)
data(cats)
model <- svm(Sex ~ ., data = cats)

print(model)

##
## Call:
## svm(formula = Sex ~ ., data = cats)
##
##
## Parameters:
## SVM-Type: C-classification
## SVM-Kernel: radial
## cost: 1
## gamma: 0.5
##
## Number of Support Vectors: 84

summary(model)
## Call:
## svm(formula = Sex ~ ., data = cats)
##
##
## Parameters:
## SVM-Type: C-classification
## SVM-Kernel: radial
## cost: 1
## gamma: 0.5
##
## Number of Support Vectors: 84
##
## ( 39 45 )
##
##
## Number of Classes: 2
##
## Levels:
## F M

plot(model, cats)
Figure 7.11 : Les mêmes données de chats avec superposition d’un modèle SVM. Le SVM est capable de
dessiner une frontière de classification non linéaire sur les données, ce qui peut être utile lorsque vous
essayez de créer des frontières de décision pour des données qui se chevauchent.

Finalement, vous pouvez utiliser la classification SVM dans la matrice de


confusion standard pour voir si les données s’alignent bien :

data.samples <- sample(1:nrow(cats), nrow(cats) * 0.7,


replace = FALSE)
training.data <- cats[data.samples, ]
test.data <- cats[-data.samples, ]

svm.cats <- svm(Sex ~ ., data = training.data)


prediction.svm <- predict(svm.cats, test.data[, -1], type
= "class")

table(test.data[, 1], prediction.svm)

## prediction.svm
## F M
## F 9 5
## M 3 27
K plus proches voisins
Les k plus proches voisins (ou k-nearest neighbors, en abrégé kNN en anglais)
est un algorithme d’apprentissage automatique assez simple qui,
fondamentalement, prend tous les cas disponibles dans nos données et prédit une
cible à partir d’un certain type de mesure de similarité – dans ce cas, la distance.
Voyons cela d’un peu plus près à l’aide d’un petit exemple. Supposons que
j’essaie de trouver un nouveau vélo qui me convienne. Les vélos existent dans de
nombreuses configurations, avec des mesures très différentes selon la taille et le
style d’ajustement que je veux.
Il peut y avoir une dizaine ou plus de mesures différentes qui décrivent ce qui
serait parfait pour moi, selon ma morphologie et le type de courses que je
pratique (entre autres critères possibles). Cependant, aller dans un magasin
spécialisé et essayer différents vélos prend du temps, et je préfère utiliser une
approche mathématique pour essayer de trouver le modèle idéal sans quitter ma
maison.
Le Tableau 7.1 recueille cinq mesures pour une série de vélos à partir de
documentations en ligne.

Tableau 7.1 : Cinq mesures pour une série de vélos.

Vélo m1 m2 m3 m4 m5
mon vélo 25 30 11.2 12 7
test1 27 34 7 12 8
test2 22 35 12 15 8
test3 18 39 9 24 8
test4 27 39 8 28 8
test5 29 34 8 24 8
test6 11 38 8 20 7
test7 25 31 10 12 8
test8 25 33 9 21 9
test9 26 34 14 23 7
test10 27 30 12 17 9

Une étape très importante consiste à normaliser d’abord les valeurs du tableau.
Toutes les mesures seront alors placées sur un pied d’égalité. Du coup, si
certaines d’entre elles sont très petites, elles ne seront pas « oubliées » parmi
d’autres mesures bien plus grandes. Pour ce faire, nous divisons simplement
chaque mesure par la somme des valeurs de sa colonne, ce qui nous donne le
Tableau 7.2.

Tableau 7.2 : Mesures normalisées pour les vélos.

Vélo m1 m2 m3 m4 m5 ajust dist


Mon vélo 0.0954 0.1145 0.0427 0.0458 0.0267 0.1639 0.0000
test1 0.1031 0.1298 0.0267 0.0458 0.0305 0.1766 0.0127
test2 0.0840 0.1336 0.0458 0.0573 0.0305 0.1766 0.0127
test3 0.0687 0.1489 0.0344 0.0916 0.0305 0.1933 0.0294
test4 0.1031 0.1489 0.0305 0.1069 0.0305 0.2146 0.0507
test5 0.1107 0.1298 0.0305 0.0916 0.0305 0.1984 0.0345
test6 0.0420 0.1450 0.0305 0.0763 0.0267 0.1740 0.0101
test7 0.0954 0.1183 0.0382 0.0458 0.0305 0.1661 0.0022
test8 0.0954 0.1260 0.0344 0.0802 0.0344 0.1837 0.0198
test9 0.0992 0.1298 0.0534 0.0878 0.0267 0.1948 0.0309
test10 0.1031 0.1145 0.0458 0.0649 0.0344 0.1767 0.0128

Pour chaque vélo, il y a cinq mesures (de m1 à m5), un champ calculé (ajust) et
une simple distance (dist). Avec les kNN, nous utilisons le calcul de la distance
euclidienne donnée par la formule suivante :

Cela définit le champ ajust dans le Tableau 7.2. Après avoir mesuré cette valeur,
nous pouvons voir à quelle distance de cette ligne de base les vélos se situent en
prenant simplement la différence entre le vélo qui nous intéresse et ladite ligne
de base. Cela nous donne la valeur dist dans le tableau. Nous trions ensuite selon
les valeurs de dist, et celle qui se rapproche le plus de la ligne de base est le
voisin le plus proche. Si nous voulions les k vélos les mieux adaptés, disons par
exemple les trois premiers, nous prendrions simplement les trois premiers vélos
de la liste (ligne de base mise évidemment à part).
Pour un exemple de régression, l’algorithme kNN calcule la moyenne de notre
variable de réponse pour les k plus proches voisins. Les fondements
mathématiques sont les mêmes pour la classification, mais légèrement modifiés
du fait qu’il s’agit de valeurs catégorielles au lieu de valeurs numériques.
Reprenons un exemple simple à partir du vénérable jeu de données mtcars :

knn.ex <- head(mtcars[, 1:3])


knn.ex

## mpg cyl disp


## Mazda RX4 21.0 6 160
## Mazda RX4 Wag 21.0 6 160
## Datsun 710 22.8 4 108
## Hornet 4 Drive 21.4 6 258
## Hornet Sportabout 18.7 8 360
## Valiant 18.1 6 225

Si vous vouliez trouver les k plus proches voisins en allant jusqu’à la dernière
rangée pour la voiture Valiant sur la base de la caractéristique mpg, vous devriez
calculer la distance euclidienne entre toutes les autres caractéristiques :

knn.ex$dist <- sqrt((knn.ex$cyl - 6)^2 + (knn.ex$disp -


225)^2)
knn.ex[order(knn.ex[, 4]), ]

## mpg cyl disp dist


## Valiant 18.1 6 225 0.0000
## Hornet 4 Drive 21.4 6 258 33.0000
## Mazda RX4 21.0 6 160 65.0000
## Mazda RX4 Wag 21.0 6 160 65.0000
## Datsun 710 22.8 4 108 117.0171
## Hornet Sportabout 18.7 8 360 135.0148
Cet exemple prend les valeurs de la voiture Valiant, en laissant de côté la
caractéristique que vous essayez de modéliser, et il calcule la distance
euclidienne entre elles. Le listing précédent montre les cinq points de données
pour les plus proches voisins en fonction des caractéristiques sélectionnées.

K plus proches voisins et régression


La Figure 7.12 illustre l’exécution d’un modèle de régression avec l’algorithme
kNN :

library(caret)

data(BloodBrain)
inTrain <- createDataPartition(logBBB, p = 0.8)[[1]]

trainX <- bbbDescr[inTrain, ]


trainY <- logBBB[inTrain]

testX <- bbbDescr[-inTrain, ]


testY <- logBBB[-inTrain]

fit <- knnreg(trainX, trainY, k = 3)

plot(testY, predict(fit, testX))


Figure 7.12 : Un tracé d’estimation des erreurs pour les données dans une régression kNN.

K plus proches voisins et classification


L’utilisation de kNN pour effectuer une classification fonctionne à peu près de la
même manière que pour la régression. Dans l’exemple qui suit, vous allez
utiliser le système de modélisation d’une classification provenant du package
RWeka. Comme cette suite de modélisation repose sur Java, vous devez savoir
quelle version de R vous exécutez. Pour cela, il vous suffit d’appeler la fonction
suivante :

Sys.getenv("R_ARCH")

## [1] "/x64"

Ce résultat signale une architecture 64 bits de R.


Le plus souvent, les problèmes proviennent d’une erreur de type provenant, par exemple,
du fait que vous pourriez avoir installé une version 64 bits de R, mais sur un système avec
une version 32 bits de Java.
Dans tous les cas, l’exécution du système de classification de RWeka est assez
simple et elle produit quelques jolies sorties :

library(RWeka)
iris <- read.arff(system.file("arff", "iris.arff",
package = "RWeka"))

classifier <- IBk(class ~ ., data = iris)


summary(classifier)

##
## === Summary ===
##
## Correctly Classified Instances 150
100 %
## Incorrectly Classified Instances 0
0 %
## Kappa statistic 1
## Mean absolute error 0.0085
## Root mean squared error 0.0091
## Relative absolute error 1.9219 %
## Root relative squared error 1.9335 %
## Total Number of Instances 150
##
## === Confusion Matrix ===
##
## a b c <-- classified as
## 50 0 0 ¦ a = Iris-setosa
## 0 50 0 ¦ b = Iris-versicolor
## 0 0 50 ¦ c = Iris-virginica

Avec le package RWeka, vous pouvez obtenir toutes sortes d’informations


intéressantes sans avoir à les calculer explicitement à la main. Dans ce cas, vous
trouvez de nombreux types d’erreurs ainsi qu’une matrice de confusion pratique,
tout cela en appelant simplement la fonction summary de l’objet.
Vous pouvez également évaluer l’objet RWeka résultant avec une validation
croisée intégrée tout à fait pratique grâce à l’option numFolds :

classifier <- IBk(class ~ ., data = iris, control =


Weka_control(K = 20,
X = TRUE))
evaluate_Weka_classifier(classifier, numFolds = 10)

## === 10 Fold Cross Validation ===


##
## === Summary ===
##
## Correctly Classified Instances 143
95.3333 %
## Incorrectly Classified Instances 7
4.6667 %
## Kappa statistic 0.93
## Mean absolute error 0.0477
## Root mean squared error 0.1616
## Relative absolute error 10.7419 %
## Root relative squared error 34.2831 %
## Total Number of Instances 150
##
## === Confusion Matrix ===
##
## a b c <-- classified as
## 50 0 0 ¦ a = Iris-setosa
## 0 47 3 ¦ b = Iris-versicolor
## 0 4 46 ¦ c = Iris-virginica
En résumé
Dans ce chapitre, nous avons traité brièvement d’autres modèles d’apprentissage
automatique que vous pourriez rencontrer sur le terrain. Les modèles statistiques
naïfs bayésiens sont ceux qui reposent sur une hypothèse audacieuse (d’où
l’expression naïf) selon laquelle toutes les caractéristiques de notre jeu de
données sont indépendantes les unes des autres. Bien que cela puisse être vrai
dans certaines circonstances, il y a dans de nombreux cas des corrélations au sein
des données. Quoi qu’il en soit, les modèles naïfs bayésiens peuvent très bien
fonctionner pour répondre à certains objectifs.
Si les problèmes résultant de corrélations indésirables dans vos données vous
concernent, vous pouvez compter sur l’analyse en composantes principales (ou
PCA). Il s’agit d’une technique qui reformule vos données pour en faire un
amalgame de quelques composantes qui expliquent la plupart des variations du
modèle. En représentant les données de cette façon, vous pouvez simplifier les
entrées des modèles d’apprentissage automatique, mais aussi vous servir de cette
méthode comme technique de compression. Il existe d’autres types d’analyse des
composantes, l’analyse discriminante linéaire (LDA) étant un fort concurrent du
PCA traditionnel.
Souvent, les exercices de classification s’appuient sur des frontières linéaires
pour classifier les données en certains groupes. Dans ce chapitre, nous avons
montré que vous pouvez utiliser des séparateurs à vaste marge (SVM) comme
classifieurs linéaires tout autant que non linéaires. Les SVM fonctionnent en
passant les données via des transformations connues sous le nom d’astuce du
noyau, mais ils peuvent parfois être suffisamment complexes pour être
considérés essentiellement comme des boîtes noires.
Finalement, vous avez examiné les techniques de régression et de classification à
l’aide de l’apprentissage automatique dit des k voisins les plus proches (ou
kNN). Il s’agit d’un algorithme qui s’appuie sur la distance euclidienne par
rapport à d’autres points de données afin de faire des prédictions. Vous prenez
une ligne de base dans les données, vous mesurez la distance entre tous les
points, et vous vous en servez pour effectuer une comparaison avec d’autres
données. Les k points de données les plus proches sont les valeurs que vous
obtenez comme résultat. Cet algorithme pratique est raisonnablement facile à
expliquer et on le trouve dans de nombreux packages de l’écosystème de R, dont
notamment RWeka.
CHAPITRE 8
Apprentissage automatique avec le
package caret
Jusqu’à présent, nous avons fait de l’apprentissage automatique d’une manière
disons… très ponctuelle. Nous avons des données, nous voulons ajuster un
modèle à celles-ci, puis nous mettons au point ce modèle pour obtenir le meilleur
résultat possible en fonction des processus d’échantillonnage que nous avons pu
réaliser et de la façon dont les données elles-mêmes sont organisées. Tout cela
repose en grande partie sur la capacité de reconnaître quand utiliser certains
algorithmes. En visualisant simplement un jeu de données, nous pouvons
habituellement déterminer s’il est possible d’y appliquer une régression linéaire,
si cela fait sens. De même, nous avons vu des exemples dans lesquels la
configuration des données est mieux adaptée pour opérer un regroupement via
un algorithme kmeans ou quelque chose de similaire.
En revanche, nous avons aussi constaté qu’un grand nombre de ces algorithmes
peuvent être très différents les uns des autres. Les options de la fonction lm(),
par exemple, ne sont pas du tout les mêmes que celles de la fonction nnet().
Nous nous disons alors qu’il existe certainement quelque chose qui fournit une
interface commune pour tous ces algorithmes si différents et pourtant
couramment utilisés. Et nous avons de la chance avec R ! En effet, le package
caret offre un ensemble d’outils puissants dont nous pouvons nous servir pour
nous aider à rationaliser notre construction de modèles.
Le nom « caret » est un acronyme qui signifie « Classification and Regression
Training » (soit entraînement pour la classification et la régression), mais le
package lui-même est capable de faire beaucoup plus. Dans l’écosystème de R, il
existe des centaines de packages dédiés à l’apprentissage automatique. Se
familiariser avec les bizarreries et les fonctionnalités spéciales de chacun d’entre
eux peut être une tâche tout à fait intimidante. Heureusement pour nous, caret
fournit une interface commune pour tous ces packages. De surcroît, caret offre
également d’excellentes fonctionnalités pour fractionner nos données. Nous
pouvons facilement diviser une base de données en deux parties réparties selon
le schéma classique de 70 % pour former le jeu d’entraînement et 30 % pour le
jeu de test. En revanche, pour des méthodes plus complexes de division des
données, comme l’échantillonnage aléatoire stratifié, caret fournit une belle
façon d’y arriver. Le package caret est également un système robuste pour la
sélection des caractéristiques. Nous pouvons lui demander de nous aider à
choisir les colonnes ou les caractéristiques qui conviennent le mieux au type de
modèle que nous voulons exécuter. Enfin, caret peut aussi aider en proposant une
façon plus rationnelle de mettre au point nos modèles. Comme mentionné
précédemment, non seulement certains modèles peuvent avoir des options très
différentes, mais ils peuvent parfois aussi être horriblement compliqués. De ce
point de vue, caret fournit des fonctionnalités efficaces pour simplifier les
modèles sans les dégrader.
Le jeu de données Titanic
Dans ce chapitre, nous allons nous concentrer sur la manière dont caret nous aide
à travailler en nous basant sur un jeu de données célèbre : celui consacré au
naufrage du paquebot Titanic, qui a coulé dans l’Atlantique Nord en 1912. Ce
jeu de données est souvent utilisé dans des contextes éducatifs, et ce pour de
nombreuses raisons. Il s’agit d’un événement historique très connu, de sorte que
la plupart des personnes disposent déjà d’un certain nombre de notions sur le
contexte dans lequel les données vont s’inscrire. De plus, le jeu du Titanic est
également un bon substitut à d’autres types de données que l’on rencontre
couramment dans l’industrie, comme des profils de clients.
Le but de ce chapitre est d’utiliser le package caret pour construire un modèle
d’apprentissage automatique dans lequel vous allez essayer de prédire si
quelqu’un a survécu à son voyage sur l’infortuné paquebot. Vous allez construire
un modèle avec la forme de fonction que nous avons vue plus tôt dans ce livre,
écrite comme ceci : train(Survived ~ .). L’objectif est donc de modéliser le
paramètre Survived (indiquant si une personne a ou non survécu au naufrage).
Cependant, nous pourrions avoir besoin de nettoyer et d’organiser les données
un peu mieux que la façon dont elles se présentent à l’origine.
Le jeu de données du Titanic n’est pas installé par défaut avec R. Vous devez donc le
télécharger et l’enregistrer sur votre ordinateur. Pour cela, rendez-vous à l’adresse
suivante :
https://www.kaggle.com/hesh97/titanicdataset-traincsv#train.csv

Dans la section Data Sources de la page, cliquez sur le bouton de téléchargement, à droite
de la ligne train.csv. Acceptez de vous connecter à Kaggle à l’aide d’un compte existant
ou en créant un nouveau compte. Une fois votre connexion à Kaggle validée, vous
pourrez télécharger le fichier train.csv, puis le copier dans le répertoire de travail de la
console RGui (vous le retrouverez en choisissant la commande Changer le répertoire
courant dans le menu Fichier).

Une autre possibilité consiste à accéder au dépôt Github suivant :

https://github.com/datasciencedojo/tutorials/tree/master/Datasets
Choisissez alors Titanic.csv. Cliquez sur Raw, copiez tout le contenu et sauvegardez-le
dans un fichier de votre répertoire courant, comme ci-dessus. N’oubliez pas de renommer
le fichier de destination en train.csv.

Commençons par explorer nos données pour nous familiariser avec elles :
train <- read.csv("train.csv")
str(train)

## 'data.frame': 891 obs. of 12 variables:


## $ PassengerId: int 1 2 3 4 5 6 7 8 9 10 ...
## $ Survived : int 0 1 1 1 0 0 0 0 1 1 ...
## $ Pclass : int 3 1 3 1 3 3 1 3 3 2 ...
## $ Name : Factor w/ 891 levels "Abbing, Mr.
Anthony",..: 109 191358
277 16 559 520 629 417 581 ...
## $ Sex : Factor w/ 2 levels "female","male": 2
1 1 1 2 2 2 2 1 1 ...
## $ Age : num 22 38 26 35 35 NA 54 2 27 14 ...
## $ SibSp : int 1 1 0 1 0 0 0 3 0 1 ...
## $ Parch : int 0 0 0 0 0 0 0 1 2 0 ...
## $ Ticket : Factor w/ 681 levels
"110152","110413",..: 524 597 670 50
473 276 86 396 345 133 ...
## $ Fare : num 7.25 71.28 7.92 53.1 8.05 ...
## $ Cabin : Factor w/ 148 levels
"","A10","A14",..: 1 83 1 57 1 1131
1 1 1 ...
## $ Embarked : Factor w/ 4 levels "","C","Q","S": 4 2
4 4 4 3 4 4 4 2 ...

Certaines de ces données peuvent vous rappeler le genre de choses que vous
voyez dans une analyse de clientèle typique. Vous y trouvez en effet :

• une identification unique pour chaque passager (PassengerID) ;

• une valeur binaire qui indique si le passager ou la passagère a survécu


(Survived) ;

• la classe (première, seconde ou troisième) de sa cabine (Pclass) ;

• des données personnelles comme le nom, le sexe et l’âge (Name, Sex,


Age) ;

• le nombre de personnes (époux, épouse, enfants, ascendants ou


descendants) avec lesquelles la personne voyageait (SibSp, Parch) ;
• le numéro du billet (Ticket) ;

• le prix du billet (Fare) ;

• le numéro de cabine (Cabin) ;

• le lieu d’embarquement (Embarked, avec C pour Cherbourg, Q pour


Queenstown et S pour Southampton).
Certaines de ces données ne sont pas très utiles pour l’analyse que vous allez
effectuer. Des informations telles que les identificateurs uniques ne révèlent rien
en termes de tendances, et ils ne servent qu’à perturber les modèles. Ici, nous
pouvons oublier PassengerID et Name. Les informations contenues dans le billet
pourraient être utiles, par contre leur formatage est un cauchemar. Les valeurs
Cabin pourraient elles aussi être utiles, mais il manque beaucoup d’informations.
Pour cet exercice, nous mettrons donc de côté PassengerID, Name, Ticket et
Cabin.

Manipuler les données


Il y a quelques étapes de nettoyage à effectuer avant d’appliquer vos modèles sur
les données. Remarquez que le port d’embarquement présente quelques valeurs
vides et que l’âge contient nombre de valeurs NA. D’autre part, vous voudrez
peut-être réorganiser les contenus de SibSp et de Parch pour leur donner des
formes plus cohérentes afin que le modèle sache mieux gérer cette information.
Occupons-nous d’abord des données d’embarquement manquantes :

table(train$Embarked)

##
## C Q S
## 2 168 77 644

Nous constatons que 168 personnes ont embarqué à Cherbourg, 77 à


Queenstown et 644 à Southampton. D’autre part, il manque deux valeurs.
Faisons l’hypothèse (connue plus formellement sous le nom d’imputation) que
ces deux blancs peuvent être étiquetés comme appartenant au facteur le plus
important pour cette variable.
Dans ce cas, vous supposez donc que les blancs correspondent à des personnes
ayant embarqué à Southampton, puisque c’est ce qui correspond à la majorité
des points de données. Il s’agit ici probablement d’un pari sûr. Si les données
étaient plus homogènes, la solution pourrait être plus compliquée à formuler,
comme illustré ici :

train$Embarked[train$Embarked == ""] <- "S"

Vous venez d’assigner les deux blancs à la valeur la plus fréquente. Passons
maintenant à la question de l’âge :

table(is.na(train$Age))[2]/table(is.na(train$Age))[1]

## TRUE
## 0.2478992

summary(train$Age)

## Min. 1st Qu. Median Mean 3rd Qu. Max.


NA's
## 0.42 20.12 28.00 29.70 38.00 80.00
177

Ce code nous informe que près de 25 % des données sur l’âge sont manquantes.
Si vous regardez les statistiques affichées par la fonction summary() pour cette
colonne, vous pourriez appliquer le même processus que ci-dessus en remplaçant
simplement tous les âges manquants par la valeur la plus fréquente (dans ce cas,
la médiane).
Vous réaffecteriez alors un âge de 28 ans à toutes les personnes pour lesquelles
cette information est absente. Cependant, intuitivement, il semble bien que cette
hypothèse soit plutôt audacieuse. Un pari plus sûr consisterait à ajouter
simplement une étiquette si l’âge manque pour une certaine personne, et à
remplir ces données plus tard en utilisant la puissance de caret :

train$is_age_missing <- ifelse(is.na(train$Age), 1, 0)

Consolidez maintenant les données pour le nombre de frères/sœurs/conjoints


(SibSp) et de parents/enfants (Parch) avec lesquels la personne voyageait. Cela
facilitera plus tard la sélection du modèle :
train$travelers <- train$SibSp + train$Parch + 1

Ensuite, vous devez factoriser certaines des données :

train$Survived <- as.factor(train$Survived)


train$Pclass <- as.factor(train$Pclass)
train$is_age_missing <- as.factor(train$is_age_missing)

Enfin, vous voulez sous-échantillonner vos données avec les seules


caractéristiques qui vous intéressent :

train2 <- subset(train, select = c(Survived, Pclass, Sex,


Age,
SibSp, Parch, Fare, Embarked, is_age_missing,
travelers))
Libérons caret !
Maintenant que les données du jeu du Titanic sont remises en forme, il est temps
de commencer à utiliser le package caret. R dispose de nombreux modèles
d’apprentissage automatique intégrés ou accessibles en téléchargement.
Cependant, la puissance de caret est qu’il nous permet de faire beaucoup plus
qu’entraîner simplement des modèles d’apprentissage automatique. Nous
pouvons l’utiliser comme outil de prétraitement des données pour aider à
l’imputation, nous pouvons nous en servir pour diviser nos données en jeux
d’entraînement et de test, et nous pouvons faire appel à lui pour des techniques
de validation croisée, en plus de sa grande souplesse en ce qui concerne
l’entraînement des modèles.

Imputation
Dans cette sous-section, nous allons reprendre le problème rencontré plus haut, à
savoir qu’il manque beaucoup d’âges dans les données. Nous avons laissé
entendre tout à l’heure que caret est bon pour traiter ce genre de problème. En
fait, il supporte de nombreuses méthodes différentes d’imputation, comme le
choix de la médiane (ce qui est similaire à la façon dont vous avez choisi les
valeurs pour l’emplacement des embarquements manquants).
Il supporte aussi une méthode d’imputation basée sur les k plus proches voisins
(kNN), qui pourrait être utile dans d’autres situations. Il permet également
l’imputation via des arbres de décision « ensachés » (bagging ou encore
bootstrap aggregating), qui sont proches en théorie des forêts aléatoires. Dans ce
cas, vous optez pour l’algorithme de bagging car c’est la méthode la plus
précise. Malgré la puissance de calcul nécessitée par cette méthode, notre jeu de
données est suffisamment petit pour que vous puissiez l’exécuter sans pénalité
de temps majeure.
La limitation de l’imputation dans caret est que vous devez changer toutes les
variables factorielles en données numériques pour que le processus fonctionne
correctement. Par exemple, les données Embarked sont catégorielles avec trois
valeurs. Il faut donc les transposer en valeurs numériques. Vous pourriez être
tenté de rebaptiser C en 0, Q en 1 et S en 2, mais cela ne marcherait pas bien
avec le modèle. À la place, vous devez prendre ces données et les faire pivoter
de telle sorte que vous ayez une colonne qui vaut 0 ou 1 si Embarked est C,
0 ou 1 si Embarked est Q, et de même pour S. Cela deviendra plus évident si
vous exécutez le code suivant :

library(caret)

dummy <- dummyVars(~., data = train2[, -1])


dummy_train <- predict(dummy, train2[, -1])
head(dummy_train)

## Pclass.1 Pclass.2 Pclass.3 Sex.female Sex.male Age


SibSp Parch Fare
## 1 0 0 1 0 1 22
1 0 7.2500
## 2 1 0 0 1 0 38
1 0 71.2833
## 3 0 0 1 1 0 26
0 0 7.9250
## 4 1 0 0 1 0 35
1 0 53.1000
## 5 0 0 1 0 1 35
0 0 8.0500
## 6 0 0 1 0 1 NA
0 0 8.4583
## Embarked. Embarked.C Embarked.Q Embarked.S
is_age_missing.0
## 1 0 0 0 1
1
## 2 0 1 0 0
1
## 3 0 0 0 1
1
## 4 0 0 0 1
1
## 5 0 0 0 1
1
## 6 0 0 1 0
0
## is_age_missing.1 travelers
## 1 0 2
## 2 0 2
## 3 0 1
## 4 0 2
## 5 0 1
## 6 1 1

Ce code sépare les valeurs catégorielles possibles pour Pclass (1,2,3) en


colonnes distinctes qui forment un indicateur binaire selon qu’elles sont de
première, de seconde ou de troisième classe. De même avec les autres variables
catégorielles. De manière surprenante, caret est assez intelligent pour appliquer
ce traitement uniquement sur les variables factorielles, et pas sur les données qui
sont déjà numériques. Maintenant, toutes nos données se trouvent sous une
forme numérique commode, ce qui leur donne davantage de sens du point de vue
de la modélisation.
L’étape suivante va consister à utiliser la fonction preProcess. Notez que l’aperçu
montre toujours une valeur NA pour un passager. C’est cette étape qui va remplir
cette valeur. La fonction preProcess est très puissante et elle offre plus
de 15 méthodes différentes pour modéliser les valeurs dont vous avez besoin.
Pour l’instant, tenons-nous-en à bagImpute :

pre.process <- preProcess(dummy_train, method =


"bagImpute")
imputed.data <- predict(pre.process, dummy_train)
head(imputed.data)

## Pclass.1 Pclass.2 Pclass.3 Sex.female Sex.male


Age SibSp Parch
## 1 0 0 1 0 1
22.00000 1 0
## 2 1 0 0 1 0
38.00000 1 0
## 3 0 0 1 1 0
26.00000 0 0
## 4 1 0 0 1 0
35.00000 1 0
## 5 0 0 1 0 1
35.00000 0 0
## 6 0 0 1 0 1
28.02132 0 0
## Fare Embarked. Embarked.C Embarked.Q Embarked.S
is_age_missing.0
## 1 7.2500 0 0 0 1
1
## 2 71.2833 0 1 0 0
1
## 3 7.9250 0 0 0 1
1
## 4 53.1000 0 0 0 1
1
## 5 8.0500 0 0 0 1
1
## 6 8.4583 0 0 1 0
0
## is_age_missing.1 travelers
## 1 0 2
## 2 0 2
## 3 0 1
## 4 0 2
## 5 0 1
## 6 1 1

La seule valeur d’âge NA que vous aviez auparavant a maintenant été prédite via
les arbres de décision « ensachés » comme valant 28.96071 ans. Super ! Toutes
les valeurs NA ont maintenant disparu et ont été remplacées par des prédictions
numériques. La dernière étape consiste à prendre ces valeurs prédites pour les
remettre dans le jeu d’entraînement d’origine :

train$Age <- imputed.data[, 6]


head(train$Age, 20)

## [1] 22.00000 38.00000 26.00000 35.00000 35.00000


28.02132 54.00000
## [8] 2.00000 27.00000 14.00000 4.00000 58.00000
20.00000 39.00000
## [15] 14.00000 55.00000 2.00000 33.55770 31.00000
27.28768

À ce stade, vous avez rempli des âges qui étaient auparavant marqués comme
étant NA, ce qu’indiquent les chiffres après le point décimal. Vous avez prédit
des âges de 28.02132, 33.55770 et 27.28768 pour les vingt premières entrées des
données.
Fractionnement des données
Voyons maintenant comment vous pouvez utiliser caret pour diviser les données
en deux jeux d’entraînement et de test. Si le jeu de données comportait à peu
près 50 % de survivants, nous pourrions nous contenter d’un simple échantillon
aléatoire en extrayant simplement la moitié des données sur lesquelles nous
pouvons nous entraîner. Au lieu de cela, vous devez prendre un échantillon
aléatoire stratifié en raison du déséquilibre entre ceux qui ont survécu et ceux qui
n’ont pas survécu. Cette prochaine étape va permettre de conserver les
proportions de la caractéristique Survived dans chacun des partages stratifiés.
Vous indiquez à la fonction createDataPartition que vous voulez que ce
fractionnement ne soit exécuté qu’une seule fois. En théorie, cependant, vous
pourriez l’exécuter plusieurs fois.
Vous prenez 70 % des données d’entraînement, et, finalement, l’option list vous
donne simplement les numéros des lignes de la partition que vous pouvez
retransmettre aux données d’entraînement pour les partager efficacement :

set.seed(123)
partition_indexes <- createDataPartition(train$Survived,
times = 1,
p = 0.7, list = FALSE)
titanic.train <- train[partition_indexes, ]
titanic.test <- train[-partition_indexes, ]

Soulevons le voile de caret…


Avant de vous lancer dans l’entraînement de modèles avec caret, jetons un coup
d’œil rapide sur les choses avec lesquelles vous pouvez jouer en ce qui concerne
la mise au point de vos modèles. Au niveau le plus haut, caret ressemble à
quelque chose comme ceci (mais n’exécutez pas ce code, puisque tout n’y est
pas défini) :

train.model <- train(Survived ~ ., data = titanic.train,


method = "xgbTree",
tuneGrid = tune.grid, trControl = train.control)

Vous retrouvez probablement une forme similaire à celle d’autres scénarios


d’apprentissage automatique pour lesquels il y a une réponse (dans ce cas,
Survived modélisé par rapport à toutes les autres caractéristiques de votre jeu de
données). Voyons de plus près les autres caractéristiques :
data
Cette option est assez explicite : c’est l’objet à partir duquel vous obtenez vos données
d’entraînement.

method
Il s’agit de l’algorithme d’apprentissage automatique spécifique que vous voulez déployer. Celui que
vous utilisez pour le moment, xgbTree, est une forme d’arbres de décision dite eXtreme Gradient
Boosting.

tuneGrid
Il s’agit d’un cadre de données de paramètres que vous pouvez passer à l’entraînement de votre
modèle. Ce modèle est entraîné et évalué pour ces paramètres, puis on passe au jeu suivant de
valeurs. Les options de tuneGrid dépendent du modèle, mais nous allons y revenir un peu plus loin.

trControl
Les options de contrôle vous permettent de spécifier comment vous voulez appliquer des techniques
de validation croisée pour l’entraînement.

Focus sur les méthodes de caret


Plongeons un peu plus profondément dans la question des méthodes de caret.
Ici, nous pouvons spécifier un algorithme d’apprentissage automatique
spécifique à appliquer pour l’entraînement du modèle. La liste des méthodes
disponibles est colossale, et vous la trouverez en annexe de ce livre. Il existe
plus de 200 méthodes dans le style « plug and play » pour changer les modèles
d’apprentissage automatique à la volée. Si nous ne voulions pas nous soucier
de la grille des paramètres de mise au point, nous pourrions simplement
échanger à chaud xgbTree avec rf, ce qui nous ferait passer à un modèle de
forêt aléatoire. Nous pourrions ensuite échanger rf avec nnet pour travailler
avec un réseau de neurones. Il est presque choquant de voir à quel point il est
facile d’essayer différents algorithmes d’apprentissage automatique !
De plus, nous pouvons regarder tous les différents rouages intérieurs de notre
méthode xgbTree en mouvement en appelant la fonction getModelInfo à partir
de caret :
getModelInfo("xgbTree")
Il y a bien entendu une multitude de choses enfouies dans cette vaste
bibliothèque de plus de 200 modèles disponibles dans caret. Certaines servent
simplement de description pour le modèle, d’autres sont des composants en
entrée pour chaque modèle de caret, et certaines autres encore sont
facultatives :
label
Le nom du modèle, comme ici « eXtreme Gradient Boosting ».

library
Les librairies nécessaires à l’exécution de ce modèle. caret vous invite à télécharger les librairies
que vous n’avez pas déjà installées, et il les chargera à la volée si possible.

type
Le modèle est-il capable de gérer la régression, la classification ou les deux ? Dans le cas de
xgbTree, il s’agit des deux.

parameters
Il s’agit d’une suite de paramètres, de classes de paramètres (par exemple, numériques) et
d’étiquettes spécifiques servant à la mise au point du modèle.

grid
Fonction utilisée pour créer une grille de mise au point, sauf indication contraire de l’utilisateur.

loop
Paramètre optionnel qui permet aux utilisateurs de produire de multiples prédictions de sous-
modèles à partir d’un même objet.

fit
C’est ce qui ajuste réellement le modèle.

predict
Fonction pour créer des prédictions de modèles.

prob
Le cas échéant, cette fonction crée des probabilités de classe.

predictors
Fonction optionnelle qui retourne les noms des caractéristiques que nous avons utilisées en tant que
prédicteurs dans notre modèle.

varImp
Fonction facultative qui calcule l’importance variable.

levels
Fonction facultative, typiquement employée pour les modèles de classification qui utilisent une
méthode S4 spécifique.

tags
Des entrées décrivant ce dont le modèle est spécifiquement capable. Ici, nous avons des étiquettes
du genre : « modèle arborescent, boosting, modèle d’ensemble, sélection de caractéristique
implicite ».

sort
Fonction qui trie les paramètres par ordre décroissant de complexité.

En vous penchant davantage sur le champ parameters, vous pouvez voir toutes
les façons différentes de mettre au point ce modèle spécifique :

xgb.params <- getModelInfo("xgbTree")


xgb.params$xgbTree$parameters

## parameter class
label
## 1 nrounds numeric # Boosting
Iterations
## 2 max_depth numeric Max
Tree Depth
## 3 eta numeric
Shrinkage
## 4 gamma numeric Minimum Loss
Reduction
## 5 colsample_bytree numeric Subsample Ratio of
Columns
## 6 min_child_weight numeric Minimum Sum of
Instance Weight
## 7 subsample numeric Subsample
Percentage
Nous pouvons également comparer ces paramètres pour différents modèles :

nnet.params <- getModelInfo("nnet")


nnet.params$nnet$parameters

## parameter class label


## 1 size numeric #Hidden Units
## 2 decay numeric Weight Decay

Cette richesse d’informations nous apprend non seulement ce qu’un algorithme


nouveau et potentiellement inconnu est capable de faire, mais aussi comment il
fonctionne au niveau du code, et comment l’ajuster au mieux pour obtenir des
résultats optimaux.

Entraînement du modèle
Finalement, nous en arrivons au cœur de la construction du modèle. Vous devez
d’abord spécifier quels contrôles il faut transmettre au modèle pour
l’entraînement. Tout simplement, vous dites à caret comment vous voulez que le
modèle soit construit. Le point clé ici est que ce processus d’entraînement est en
fait indépendant du modèle que vous sélectionnez !
Vous indiquez à caret que vous aimeriez faire une validation croisée avec 10 plis,
répétée trois fois, puis faire une recherche par quadrillage. Une recherche par
quadrillage, c’est quand vous parcourez un ensemble de paramètres et que vous
choisissez les meilleurs. Pour l’essentiel, il s’agit de réaliser 30 pseudo-modèles
et de sélectionner les paramètres qui correspondent au meilleur. Voici comment
procéder :

train.control <- trainControl(method = "repeatedcv",


number = 10,
repeats = 3, search = "grid")

Dans le code qui suit, la fonction expand.grid() crée toutes les permutations de
toutes les valeurs qui lui sont passées, ainsi qu’une ligne unique pour chaque
permutation :

tune.grid <- expand.grid(eta = c(0.05, 0.075, 0.1),


nrounds = c(50, 75, 100),
max_depth = 6:8,
min_child_weight = c(2.0, 2.25,
2.5),
colsample_bytree = c(0.3, 0.4,
0.5),
gamma = 0,
subsample = 1
)
head(tune.grid)

## eta nrounds max_depth min_child_weight


colsample_bytree gamma
## 1 0.050 50 6 2
0.3 0
## 2 0.075 50 6 2
0.3 0
## 3 0.100 50 6 2
0.3 0
## 4 0.050 75 6 2
0.3 0
## 5 0.075 75 6 2
0.3 0
## 6 0.100 75 6 2
0.3 0

À l’arrivée, vous vous retrouvez avec 243 combinaisons des valeurs que vous
transmettez à la fonction expand.grid(). Vous demandez à caret d’exécuter une
validation croisée avec 10 plis trois fois sur chacune de ces valeurs que vous
passez dans l’algorithme. Vous entraînez donc en réalité 7 290 modèles
différents ! Voilà qui risque de prendre une éternité à calculer !
Dans une telle situation, il se trouve qu’il y a un joli package pour paralléliser le
code R afin d’accélérer ce genre de choses. Après avoir chargé le package
doSnow, vous pouvez utiliser une fonction qui lancera plusieurs instances de R
en même temps avec votre code. La fonction registerDoSnow() indique à caret
qu’il peut maintenant utiliser les clusters disponibles pour ses besoins en
traitement :

library(doSNOW)
cl <- makeCluster(3, type = "SOCK")
registerDoSNOW(cl)
La fonction makeCluster() provoque un blocage sur certains systèmes ou avec certaines
configurations. Les causes possibles ne sont cependant pas faciles à déterminer. Dans ce
cas, il ne reste plus qu’à annuler l’exécution ci-dessus en appuyant sur la touche
d’échappement, et à passer directement à l’entraînement en exécutant l’instruction qui
suit. Simplement, les temps de calcul vont être beaucoup plus longs…

Passons à l’utilisation de caret pour l’entraînement. Ici, vous avez une structure
similaire à ce que vous avez vu dans les précédents algorithmes. Vous prenez la
variable réponse, Survived, et vous modélisez tous les autres facteurs par rapport
à elle. Vous utilisez en particulier un algorithme xg-boost et vous le faites itérer
sur toutes les permutations différentes des paramètres de mise au point donnés
par le quadrillage que vous avez développé plus haut. Finalement, vous lui
demandez d’utiliser les contrôles d’entraînement de la validation croisée
à 10 plis, et de le faire trois fois. Vous stoppez ensuite (si possible !) le cluster
parallélisé pour économiser les ressources de calcul, puisque vous avez terminé
la procédure d’entraînement exhaustive :

caret.cv <- train(Survived ~ ., data = titanic.train,


method = "xgbTree",
tuneGrid = tune.grid, trControl = train.control)
stopCluster(cl)

Voici les résultats :

caret.cv

eXtreme Gradient Boosting

625 samples
13 predictor
2 classes: '0', '1'

No pre-processing
Resampling: Cross-Validated (10 fold, repeated 3 times)
Summary of sample sizes: 563, 562, 563, 562, 563, 562,
...
Resampling results across tuning parameters:

eta max_depth colsample_bytree min_child_weight


nrounds Accuracy Kappa
0.050 6 0.3 2.00 50
0.8060079 0.5695255
0.050 6 0.3 2.00 75
0.8054702 0.5712391
0.050 6 0.3 2.00 100
0.8028076 0.5667521
0.050 6 0.3 2.25 50
0.8102833 0.5811371
0.050 6 0.3 2.25 75
0.8123997 0.5858105
0.050 6 0.3 2.25 100
0.8060079 0.5728337

Vous obtenez ainsi les résultats pour chacune des combinaisons des sorties de
modélisation. Cependant, il y a tellement de lignes en sortie que seules quelques-
unes sont listées ici. Par contre, la fin contient des informations plus
intéressantes :

0.100 8 0.5 2.25 75


0.8016982 0.5675875
0.100 8 0.5 2.25 100
0.7989845 0.5630275
0.100 8 0.5 2.50 50
0.8044035 0.5725442
0.100 8 0.5 2.50 75
0.8033026 0.5713007
0.100 8 0.5 2.50 100
0.8059396 0.5782341

Tuning parameter 'gamma' was held constant at a value of


0
Tuning parameter 'subsample' was
held constant at a value of 1
Accuracy was used to select the optimal model using the
largest value.
Thefinalvaluesusedforthemodelwerenrounds=50,max_depth=7,eta=0.05,


gamma=0,colsample_bytree=0.4,min_child_weight=2.25andsubsample=1.
Cela nous indique, afin de sélectionner les valeurs optimales pour la
modélisation, quels sont les hyperparamètres xg-boost (à partir de l’exercice
précédent d’expansion du quadrillage) qui fonctionnent le mieux. Maintenant
que votre objet modèle est entraîné, vous pouvez passer vos résultats dans une
fonction de prédiction pour modéliser la probabilité que quelqu’un ait survécu au
désastre du Titanic :

preds <- predict(caret.cv, titanic.test)

Enfin, vous évaluez les capacités du modèle en l’exécutant à travers une matrice
de confusion pour voir comment il fonctionne avec de toutes nouvelles données :

confusionMatrix(preds, titanic.test$Survived)

## Confusion Matrix and Statistics


##
## Reference
## Prediction 0 1
## 0 151 32
## 1 13 70
##
## Accuracy : 0.8308
## 95% CI : (0.7803, 0.8738)
## No Information Rate : 0.6165
## P-Value [Acc > NIR] : 2.211e-14
##
## Kappa : 0.6292
## Mcnemar's Test P-Value : 0.00729
##
## Sensitivity : 0.9207
## Specificity : 0.6863
## Pos Pred Value : 0.8251
## Neg Pred Value : 0.8434
## Prevalence : 0.6165
## Detection Rate : 0.5677
## Detection Prevalence : 0.6880
## Balanced Accuracy : 0.8035
##
## 'Positive' Class : 0
##
La fonction confusionMatrix fournie par caret est très puissante. Elle fournit une
foule d’informations statistiques que vous pouvez utiliser pour déterminer
l’exactitude du modèle, non seulement pour la classification, mais aussi pour les
problèmes de régression. Pas à pas, vous commencez par une véritable matrice
de confusion. Les valeurs correctes prédites par le modèle se trouvent sur la
diagonale. Ici, il a donc prédit correctement 151 valeurs de données pour le cas
Survived=0, avec une prédiction incorrecte pour 32 d’entre elles. De même, avec
la valeur de Survived=1, il a prédit 70 valeurs correctes et 13 valeurs incorrectes.
L’exactitude était d’environ 83%, ce qui n’est pas mauvais.
Cette fonction fournit également dans ses résultats la sensibilité et la spécificité.
La sensibilité, dans ce cas, est simplement la classe « positive » correctement
prédite sur le nombre total, soit 151 / (151 + 13), ce qui donne 0.9207. Ce que
cela signifie en langage courant, c’est que 92 % du temps, vous pouvez prédire
correctement si quelqu’un est mort dans le naufrage du Titanic… De même, la
spécificité est l’autre colonne de données de la matrice de confusion, avec
comme calcul 70 / (70 + 32), soit 0.6863. Il s’agit de prédire avec quel degré
d’exactitude si quelqu’un a survécu ou non. Ce que cela vous indique, c’est que
vous devez faire preuve de plus de diligence pour trouver des caractéristiques
permettant de mieux prédire comment les gens ont survécu au naufrage du
Titanic, étant donné que le modèle est déjà assez exact pour prédire s’ils sont
morts ou non.

Comparer de multiples modèles caret


Jusqu’à présent, vous avez exécuté un certain algorithme d’apprentissage
automatique (xgbTree) sur le jeu de données du Titanic et obtenu un résultat
assez décent. Mais caret rend également très facile le passage à d’autres
algorithmes afin que vous puissiez comparer et évaluer les résultats. Supposons
que vous vouliez comparer une forêt aléatoire et un réseau de neurones en
termes d’exactitude. Il suffit de remplacer « xgboost » par « rf ». Pour l’instant,
ignorez les paramètres spécifiques et exécutez normalement le code :

cl <- makeCluster(3, type="SOCK")


registerDoSNOW(cl)

caret.rf <- train(Survived ~ .,


data = titanic.train,
method = "rf",
#tuneGrid = tune.grid,
trControl = train.control)

stopCluster(cl)

confusionMatrix(predict(caret.rf, titanic.test),
titanic.test$Survived)

## Confusion Matrix and Statistics


##
## Reference
## Prediction 0 1
## 0 151 39
## 1 13 63
##
## Accuracy : 0.8045
## 95% CI : (0.7517, 0.8504)
## No Information Rate : 0.6165
## P-Value [Acc > NIR] : 3.037e-1
##
## Kappa : 0.5656
## Mcnemar's Test P-Value : 0.0005265
##
## Sensitivity : 0.9207
## Specificity : 0.6176
## Pos Pred Value : 0.7947
## Neg Pred Value : 0.8289
## Prevalence : 0.6165
## Detection Rate : 0.5677
## Detection Prevalence : 0.7143
## Balanced Accuracy : 0.7692
##
## 'Positive' Class : 0
##

L’exactitude obtenue ici, sans aucun réglage particulier, est de 80 %. Pas mal
pour avoir simplement changé quelques lettres dans le modèle d’entraînement !
Le modèle xgboost avait donné des résultats avec une exactitude de 83 %, et cela
après un long processus de réglage. Supposons que vous souhaitiez exécuter à la
place un modèle linéaire généralisé. Vous pouvez appliquer la même logique en
spécifiant cette fois glm comme nouvel algorithme :
cl <- makeCluster(3, type="SOCK")
registerDoSNOW(cl)

caret.nnet <- train(Survived ~ .,


data = titanic.train,
method = "glm",
#tuneGrid = tune.grid,
trControl = train.control)
stopCluster(cl)

confusionMatrix(predict(caret.nnet, titanic.test),
titanic.test$Survived)

## Confusion Matrix and Statistics


##
## Reference
## Prediction 0 1
## 0 107 38
## 1 57 64
##
## Accuracy : 0.6429
## 95% CI : (0.5821, 0.7004)
## No Information Rate : 0.6165
## P-Value [Acc > NIR] : 0.20669
##
## Kappa : 0.2704
## Mcnemar's Test P-Value : 0.06478
##
## Sensitivity : 0.6524
## Specificity : 0.6275
## Pos Pred Value : 0.7379
## Neg Pred Value : 0.5289
## Prevalence : 0.6165
## Detection Rate : 0.4023
## Detection Prevalence : 0.5451
## Balanced Accuracy : 0.6399
##
## 'Positive' Class : 0
##

L’exactitude de la sortie n’est pas aussi bonne, mais vous pourriez probablement
résoudre ce problème en affinant vos paramètres tune.grid.
En résumé
Nous avons abordé beaucoup de choses dans ce chapitre concernant le package
d’apprentissage automatique de R appelé caret. Vous avez appris que son nom
signifie « Classification and Regression Training », mais aussi que caret est
capable d’en faire beaucoup plus. En particulier, vous avez vu comment vous
pouvez l’utiliser pour l’imputation des données, le fractionnement et
l’échantillonnage des données et, enfin, pour l’entraînement des modèles.
L’entraînement d’un modèle dans caret est très simple, presque un jeu d’enfant.
Ici, nous avons utilisé la forme suivante :

caret.cv <- train(Survived ~ ., data = titanic.train,


method = "xgbTree",
tuneGrid = tune.grid, trControl = train.control)

Vous avez pu voir les résultats de l’exercice en regardant l’objet produit en sortie
de l’entraînement, en l’occurrence caret.cv. Dans cet objet, vous avez aussi pu
voir les résultats concernant l’exactitude, en fonction des réglages de tous les
paramètres en entrée que vous avez exécutés avec les données. La sortie est
longue, mais l’objet d’entraînement est assez intelligent pour sélectionner le
groupe de paramètres qui procure la meilleure exactitude pour une utilisation
dans les parties prédictives de votre flux de travail.
Finalement, vous avez pris l’objet d’entraînement et vous l’avez passé à la
fonction predict(), puis transmis ce résultat prédictif à la fonction
confusionMatrix() de caret. Utiliser cette fonction spécifique est extrêmement
intéressant, car non seulement vous obtenez la matrice de confusion elle-même,
mais vous découvrez en prime une foule d’autres informations sans avoir besoin
d’effectuer un quelconque traitement manuel. Vous obtenez une exactitude, un
intervalle de confiance à 95 %, une sensibilité, une spécificité et de nombreuses
autres références statistiques vous permettant d’effectuer des comparaisons
quant aux performances du modèle.
La conception de caret permet aussi de changer très facilement l’algorithme que
vous utilisez. En modifiant simplement la valeur xgbTree dans la fonction train()
en quelque chose comme rf pour une forêt aléatoire, ou glm pour un modèle
linéaire généralisé, vous pouvez couvrir toute une gamme de modèles
d’apprentissage automatique en peu de temps, sans avoir à apprendre les
complexités intenses de chaque modèle.
Nous avons vu également que caret offre un nombre vertigineux de modèles
parmi lesquels choisir. Déterminer auquel faire appel en premier dépend
totalement des données et du spécialiste des données qui est aux commandes. Ce
livre contient une annexe qui, en utilisant les informations disponibles dans caret
lui-même, détaille chaque méthode d’apprentissage automatique disponible,
indique si elle est utilisable pour la classification, la régression, ou les deux,
quels packages lui sont nécessaires, et liste quelques mots-clés qui la décrivent.
ANNEXE A
Modèles d’apprentissage automatique
dans caret
Bien que la liste qui suit soit longue, cela ne signifie pas qu’elle soit exhaustive.
Vous trouverez ici des informations sur les algorithmes d’apprentissage
automatique que nous utilisons avec le package caret, étudié plus en détail dans
ce livre. L’une des grandes forces de caret est qu’il vous donne la possibilité de
passer très rapidement de l’utilisation, par exemple, d’un algorithme
d’apprentissage automatique de forêt aléatoire à un réseau de neurones. Avec
caret, tout ce que nous aurions à faire pour cela est de changer de méthode dans
notre modèle pour passer de rf à nnet. Cette annexe se veut une référence rapide
pour rechercher tous les appels d’algorithmes d’apprentissage automatique
disponibles, les bibliothèques dont ils dépendent, une description globale ou une
étiquette, et leur type de modèle (régression, classification, ou les deux).
Pour plus d’explications, voyez le Chapitre 8. Pour des informations plus
détaillées, reportez-vous au dépôt Github du projet caret, à l’adresse
https://github.com/topepo/caret. Cliquez ensuite sur models, puis files.

Tableau A.1 : Algorithmes d’apprentissage automatique dans caret.

Algorithme Dépendances Étiquette dans caret


ada ada, plyr Boosted Classification Trees
AdaBag adabag, plyr Bagged AdaBoost
AdaBoost.M1 adabag, plyr AdaBoost.M1
adaboost fastAdaboost AdaBoost Classification Trees
amdai adaptDA Adaptive-Mixture Discriminant
Analysis
ANFIS frbs
Adaptive-Network-Based Fuzzy
Inference System
avNNet nnet Model-Averaged Neural Network
awnb bnclassify Naive Bayes Classifier with Attribute
Weighting

awtan bnclassify Tree-Augmented Naive Bayes Classifier


with Attribute Weighting
bag caret Bagged Model
bagEarth earth Bagged MARS
bagEarthGCV earth Bagged MARS using gCV Pruning
bagFDA earth, mda Bagged-Flexible Discriminant Analysis
bagFDAGCV earth Bagged FDA using gCV Pruning
bartMachine bartMachine Bayesian Additive Regression Trees
bayesglm arm Bayesian Generalized Linear Model
bdk kohonen Self-Organizing Map
binda binda Binary Discriminant Analysis
blackboost party,mboost, plyr Boosted Tree
blasso monomvn The Bayesian lasso
blassoAveraged monomvn Bayesian Ridge Regression (Model
Averaged)
Boruta Boruta, randomForest Random Forest with
randomForest Additional Feature Selection
bridge monomvn Bayesian Ridge Regression
brnn brnn Bayesian Regularized Neural Networks
BstLm bst, plyr Boosted Linear Model
bstSm bst, plyr Boosted Smoothing Spline
bstTree bst, plyr Boosted Tree
C5.0 C50, plyr C5.0
C5.0Cost C50, plyr Cost-Sensitive C5.0
C5.0Rules C50 Single C5.0 Ruleset
C5.0Tree C50 Single C5.0 Tree
cforest party Conditional Inference Random Forest

chaid CHAID Chi-squared Automated Interaction


Detection
CSimca rrcovHD SIMCA
ctree party Conditional Inference Tree
ctree2 party Conditional Inference Tree
cubist Cubist Cubist
dda sparsediscrim Diagonal Discriminant Analysis
deepboost deepboost DeepBoost
DENFIS frbs Dynamic Evolving Neural-Fuzzy
Inference System
dnn deepnet Stacked AutoEncoder Deep Neural
Network
dwdLinear kerndwd Linear Distance Weighted
Discrimination
dwdPoly kerndwd Distance-Weighted Discrimination with
Polynomial Kernel
dwdRadial kernlab, kerndwd Distance-Weighted Discrimination with
Radial Basis Function Kernel
earth earth Multivariate Adaptive Regression
Spline
elm elmNN Extreme Learning Machine
enet elasticnet Elasticnet
enpls.fs enpls Ensemble Partial Least Squares
Regression with Feature Selection
enpls enpls
Ensemble Partial Least Squares
Regression
evtree evtree Tree Models from Genetic Algorithms
extraTrees extraTrees Random Forest by Randomization
fda earth, mda Flexible Discriminant Analysis
FH.GBML frbs Fuzzy Rules Using Genetic
Cooperative-Competitive Learning and
Pittsburgh
FIR.DM frbs Fuzzy Inference Rules by Descent
Method
foba foba Ridge Regression with Variable
Selection
FRBCS.CHI frbs Fuzzy Rules Using Chi’s Method
FRBCS.W frbs Fuzzy Rules with Weight Factor
FS.HGD frbs Simplified TSKFuzzy Rules
gam mgcv Generalized Additive Model using
Splines
gamboost mboost, plyr Boosted Generalized Additive Model
gamLoess gam Generalized Additive Model using
LOESS
gamSpline gam Generalized Additive Model using
Splines
gaussprLinear kernlab Gaussian Process
gaussprPoly kernlab Gaussian Process with Polynomial
Kernel
gaussprRadial kernlab Gaussian Process with Radial Basis
Function Kernel
gbm gbm, plyr Stochastic Gradient Boosting
gcvEarth earth Multivariate Adaptive Regression
Splines
GFS.FR.MOGUL frbs Fuzzy Rules via MOGUL
GFS.GCCL frbs Fuzzy Rules Using Genetic
Cooperative-Competitive Learning
GFS.LT.RS frbs Genetic Lateral Tuning and Rule
Selection of Linguistic Fuzzy Systems
GFS.THRIFT frbs Fuzzy Rules via Thrift
glm native Generalized Linear Model
glmboost plyr, mboost Boosted Generalized Linear Model
glmnet glmnet glmnet
glmStepAIC MASS Generalized Linear Model with
Stepwise Feature Selection
gpls gpls Generalized Partial Least Squares
hda hda Heteroscedastic Discriminant Analysis
hdda HDclassif High-Dimensional Discriminant
Analysis
hdrda sparsediscrim High-Dimensional Regularized
Discriminant Analysis
HYFIS frbs Hybrid Neural Fuzzy Inference System
icr fastICA Independent Component Regression
J48 RWeka C4.5-like Trees
JRip RWeka Rule-Based Classifier
kernelpls pls Partial Least Squares
kknn kknn k-Nearest Neighbors
knn native k-Nearest Neighbors
krlsPoly KRLS Polynomial Kernel Regularized Least
Squares
krlsRadial KRLS, kernlab Radial-Basis Function Kernel
Regularized Least Squares
lars lars Least Angle Regression
lars2 lars Least Angle Regression
lasso elasticnet The lasso
lda MASS Linear Discriminant Analysis
lda2 MASS Linear Discriminant Analysis
leapBackward leaps Linear Regression with Backwards
Selection
leapForward leaps Linear Regression with Forward
Selection
leapSeq leaps Linear Regression with Stepwise
Selection
Linda rrcov Robust Linear Discriminant Analysis
lm native Linear Regression
lmStepAIC MASS Linear Regression with Stepwise
Selection
LMT RWeka Logistic Model Trees
loclda klaR Localized Linear Discriminant Analysis
logicBag logicFS Bagged Logic Regression
LogitBoost caTools Boosted Logistic Regression
logreg LogicReg Logic Regression
lssvmLinear kernlab Least Squares Support Vector Machine
lssvmPoly kernlab Least Squares Support Vector Machine
with Polynomial Kernel
lssvmRadial kernlab Least Squares Support Vector Machine
with Radial Basis Function Kernel
lvq class Learning Vector Quantization
M5 RWeka Model Tree
M5Rules RWeka Model Rules
manb bnclassify Model Averaged Naive Bayes Classifier
mda mda Mixture Discriminant Analysis
Mlda HiDimDA Maximum Uncertainty Linear
Discriminant Analysis
mlp RSNNS Multilayer Perceptron
mlpML RSNNS Multilayer Perceptron, with multiple
layers
mlpSGD FCNN4R Multilayer Perceptron Network by
Stochastic Gradient Descent
mlpWeightDecay RSNNS Multilayer Perceptron
mlpWeightDecayMLRSNNS RSNNS Multilayer Perceptron, multiple layers
multinom nnet Penalized Multinomial Regression
nb klaR Naive Bayes
nbDiscrete bnclassify Naive Bayes Classifier
nbSearch bnclassify Semi-Naive Structure Learner Wrapper
neuralnet neuralnet Neural Network
nnet nnet Neural Network
nnls nnls Non-Negative Least Squares
nodeHarvest nodeHarvest Tree-Based Ensembles
oblique.tree oblique.tree Oblique Trees
OneR RWeka Single-Rule Classification
ordinalNet ordinalNet, plyr Penalized Ordinal Regression
ORFlog obliqueRF Oblique Random Forest
ORFpls obliqueRF Oblique Random Forest
ORFridge obliqueRF Oblique Random Forest
ORFsvm obliqueRF Oblique Random Forest
ownn snn Optimal-Weighted Nearest Neighbor
Classifier
pam pamr Nearest Shrunken Centroids
parRF e1071, Parallel Random Forest
randomForest,
foreach
PART RWeka Rule-Based Classifier
partDSA partDSA partDSA
pcaNNet nnet Neural Networks with Feature
Extraction
pcr pls Principal Component Analysis
pda mda Penalized Discriminant Analysis
pda2 mda Penalized Discriminant Analysis
penalized penalized Penalized Linear Regression
PenalizedLDA penalizedLDA, Penalized Linear Discriminant Analysis
plyr
plr stepPlr Penalized Logistic Regression
pls pls Partial Least Squares
plsRglm plsRglm Partial Least Squares Generalized
Linear Models
polr MASS Ordered Logistic or Probit Regression
ppr native Projection Pursuit Regression
protoclass proxy, protoclass Greedy Prototype Selection
pythonKnnReg rPython Knn regression via
sklearn.neighbors.KNeighborsRegressor
qda MASS Quadratic Discriminant Analysis
QdaCov rrcov Robust Quadratic Discriminant Analysis
qrf quantregForest Quantile Random Forest
qrnn qrnn Quantile Regression Neural Network
randomGLM randomGLM Ensembles of Generalized Linear
Models
ranger e1071,ranger Random Forest
rbf RSNNS Radial-Basis Function Network
rbfDDA RSNNS Radial-Basis Function Network
Rborist Rborist Random Forest
rda klaR Regularized Discriminant Analysis
relaxo relaxo, plyr Relaxed Lasso
rf randomForest Random Forest
rFerns rFerns Random Ferns
RFlda HiDimDA Factor-Based Linear Discriminant
Analysis

rfRules randomForest, Random Forest Rule-Based Model


inTrees, plyr
ridge elasticnet Ridge Regression
rlda sparsediscrim Regularized Linear Discriminant
Analysis
rlm MASS Robust Linear Model
rmda robustDA Robust Mixture Discriminant Analysis
rocc rocc ROC-Based Classifier
rotationForest rotationForest Rotation Forest
rotationForestCp rpart, plyr, Rotation Forest
rotationForest
rpart rpart CART
rpart1SE rpart CART
rpart2 rpart CART
rpartCost rpart Cost-Sensitive CART
rpartScore rpartScore, plyr CART or Ordinal Responses
rqlasso rqPen Quantile Regression with LASSO
penalty
rqnc rqPen Non-Convex Penalized Quantile
Regression
RRF randomForest,RRF Regularized Random Forest
RRFglobal RRF Regularized Random Forest
rrlda rrlda Robust Regularized Linear Discriminant
Analysis
RSimca rrcovHD Robust SIMCA
rvmLinear kernlab Relevance Vector Machines with Linear
Kernel
rvmPoly kernlab Relevance Vector Machines with
Polynomial Kernel
rvmRadial kernlab Relevance Vector Machines with Radial
Basis Function Kernel
SBC frbs Subtractive Clustering and Fuzzy c-
Means Rules
sda sda Shrinkage Discriminant Analysis
sddaLDA SDDA Stepwise Diagonal Linear Discriminant
Analysis
sddaQDA SDDA Stepwise Diagonal Quadratic
Discriminant Analysis
sdwd sdwd Sparse Distance Weighted
Discrimination
simpls pls Partial Least Squares
SLAVE frbs Fuzzy Rules Using the Structural
Learning Algorithm on Vague
Environment
slda ipred Stabilized Linear Discriminant Analysis
smda sparseLDA Sparse Mixture Discriminant Analysis
snn snn Stabilized Nearest Neighbor Classifier
sparseLDA sparseLDA Sparse Linear Discriminant Analysis
spikeslab spikeslab, plyr Spike and Slab Regression
spls spls Sparse Partial Least Squares
stepLDA klaR, MASS Linear Discriminant Analysis with
Stepwise Feature Selection
stepQDA klaR, MASS Quadratic Discriminant Analysis with
Stepwise Feature Selection
superpc superpc Supervised Principal Component
Analysis
svmBoundrangeString kernlab Support Vector Machines with
Boundrange String Kernel
svmExpoString kernlab Support Vector Machines with
Exponential String Kernel

svmLinear kernlab Support Vector Machines with Linear


Kernel
svmLinear2 e1071 Support Vector Machines with Linear
Kernel
svmLinearWeights e1071 Linear Support Vector Machines with
Class Weights
svmPoly kernlab Support Vector Machines with
Polynomial Kernel
svmRadial kernlab Support Vector Machines with Radial
Basis Function Kernel
svmRadialCost kernlab Support Vector Machines with Radial
Basis Function Kernel
svmRadialSigma kernlab Support Vector Machines with Radial
Basis Function Kernel
svmRadialWeights kernlab Support Vector Machines with Class
Weights
svmSpectrumString kernlab Support Vector Machines with
Spectrum String Kernel
tan bnclassify Tree-Augmented Naive Bayes Classifier
tanSearch bnclassify Tree-Augmented Naive Bayes Classifier
Structure Learner Wrapper
treebag ipred, plyr, e1071 Bagged CART
vbmpRadial vbmp Variational Bayesian Multinomial Probit
Regression
vglmAdjCat VGAM Adjacent Categories Probability Model
for Ordinal Data
vglmContRatio VGAM Continuation Ratio Model for Ordinal
Data
vglmCumulative VGAM Cumulative Probability Model for
Ordinal Data
widekernelpls pls Partial Least Squares
WM frbs Wang and Mendel Fuzzy Rules
wsrf wsrf Weighted Subspace Random Forest
xgbLinear xgboost eXtreme Gradient Boosting
xgbTree xgboost, plyr eXtreme Gradient Boosting
xyf kohonen Self-Organizing Maps
Sommaire

Couverture
Le Machine learning avec R
Copyright
Introduction
Qui devrait lire ce livre ?
Portée du livre
Conventions utilisées dans ce livre

CHAPITRE 1. Qu’est-ce qu’un modèle ?


Statistiques et calcul dans la modélisation
Données d’entraînement
Validation croisée
Pourquoi utiliser R ?
Le bon…
… et le moins bon
En résumé

CHAPITRE 2. Apprentissage automatique supervisé et non


supervisé
Modèles supervisés
Régression
Entraîner et tester des données
Classification
Méthodes mixtes
Apprentissage non supervisé
Méthodes non supervisées de clustering
En résumé

CHAPITRE 3. Statistiques, échantillonnage et entraînement des


modèles dans R
Biais
Échantillonner dans R
Entraînement et test
Validation croisée
En résumé

CHAPITRE 4. La régression en bref


Régression linéaire
Régression polynomiale
Qualité de l’ajustement aux données : les risques d’un ajustement
excessif
Régression logistique
En résumé

CHAPITRE 5. Les réseaux de neurones en bref


Réseaux de neurones monocouches
Construire un réseau de neurones simple en utilisant R
Réseaux de neurones multicouches
Réseaux de neurones pour la régression
Réseaux de neurones pour la classification
Réseaux de neurones avec caret
En résumé

CHAPITRE 6. Méthodes arborescentes


Un modèle d’arbre simple
Décider comment partager les arbres
Avantages et inconvénients des arbres de décision
Arbres d’inférence conditionnelle
Forêts aléatoires
En résumé

CHAPITRE 7. Autres méthodes avancées


Classification naïve bayésienne
Analyse en composantes principales
Séparateurs à vaste marge
K plus proches voisins
En résumé

CHAPITRE 8. Apprentissage automatique avec le package caret


Le jeu de données Titanic
Libérons caret !
En résumé

ANNEXE A. Modèles d’apprentissage automatique dans caret

Vous aimerez peut-être aussi