Académique Documents
Professionnel Documents
Culture Documents
learning avec R
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.
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)
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 :
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 :
coef(mt.model)[2]
## wt
## -5.344472
coef(mt.model)[1]
## (Intercept)
## 37.28513
?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.
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 ? ».
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.
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)
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 :
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)
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 :
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.
## (Intercept) mtcars$disp
## 29.59985476 -0.04121512
## [1] 17.399
Une autre façon, plus précise, de s’y prendre consiste à appeler directement les
coefficients du modèle :
## 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
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 :
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)
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 :
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.
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.
Figure 2.4 : Les mêmes données que sur la Figure 2.3, mais après application de l’algorithme de
clustering.
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 :
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)
Figure 2.7 : Un exemple d’arbre de décision simple appliqué au jeu de données mtcars.
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.
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)
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 :
## [1] TRUE
## [1] 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
x1 = 1, x2 = 0
x1 = 0, x2 = 1
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 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 :
##
## 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.
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 :
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)
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.
• 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.
• 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.
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.
summary(iris)
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, ])
library(fifer)
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à :
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)
}
## NA's :37
##
• 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.
set.seed(123)
x <- rnorm(100, 2, 1)
y = exp(x) + rnorm(5, 0, 2)
plot(x, y)
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
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 :
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 :
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 :
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
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.
library(randomForest)
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.
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)
for (i in 1:10) {
fold.indexes <- which(folds == i, arr.ind = TRUE)
errors[2:11]
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) :
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
Coefficients
Ce sont les estimations des coefficients de notre équation linéaire. Dans
ce cas, notre équation s’écrirait y = 0.04x + 29.59.
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.
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ù 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
##
## 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
##
## 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
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
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 :
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 :
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 + ()
L’exemple suivant nous aidera dans notre raisonnement (voir la Figure 4.2) :
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 :
##
## 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 :
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 :
##
## 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 :
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 :
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))))
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 :
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)))
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.
##
## 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 :
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
## (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.
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 :
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 :
e <- exp(1)
curve(1/(1 + e^-x), -10, 10, main = "La fonction
Sigmoïde",
xlab = "Entrée", ylab = "Probabilité")
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 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
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.
##
## 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
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
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
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
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 :
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")
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.
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 :
library(caret)
data("GermanCredit")
##
## 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 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 :
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.
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
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
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
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)
set.seed(123)
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.
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
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
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 :
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
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;
## Data Error: 0;
as.numeric(xor(round(and.result$AND),
round(or.result$OR)))
## [1] 0 1 1 0
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
library(mlbench)
data(BostonHousing)
library(nnet)
## # weights: 31
## initial value 283985.903126
## final value 277329.140000
## converged
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)
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
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)
## 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
set.seed(123)
train_ind <- sample(seq_len(nrow(iris.df)), size =
smp_size)
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, ]
summary(prestige.test$income)
sqrt(mean((prestige.predict - prestige.test$income)^2))
## [1] 4146.445
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 :
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.
• 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.
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é.
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 :
library(caret)
library(randomForest)
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.
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.
library(rpart)
head(cu.summary)
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).
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).
fit$cptable
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) :
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.
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.pruned<- prune(fit,
cp=fit$cptable[which.min(fit$cptable[,"xerror"]),"CP"])
RMSE = sqrt(sum((output$test.data.Mileage -
output$prediction)^2) /
nrow(output))
RMSE
## [1] 2.318792
##
## 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)
plot(fit2)
Figure 6.15 : Un arbre de décision tracé à l’aide de la fonction ctree() de la bibliothèque party.
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.
set.seed(123)
cu.summary.complete <-
cu.summary[complete.cases(cu.summary),]
RMSE = sqrt(sum((output$test.data.Mileage -
output$Mileage)^2)/nrow(output))
RMSE
## [1] 3.3747
set.seed(456)
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.
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.
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, ]
RMSE = sqrt(sum((output$test.data.Mileage -
output$prediction.rf)^2)/
nrow(output))
RMSE
## [1] 3.229279
set.seed(456)
cu.summary.complete <-
cu.summary[complete.cases(cu.summary),]
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.
library(e1071)
breast_cancer <-
data.frame(read.table("breast_cancer.txt", header = T,
sep = ","))
………
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, ]
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)
## 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
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)
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]
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.
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
library(MASS)
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 :
table(iris.lda.prediction$class, iris$class)
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 :
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))
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.
library("e1071")
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 :
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 :
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 :
À 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.
## 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.
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.
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 :
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 :
library(caret)
data(BloodBrain)
inTrain <- createDataPartition(logBBB, p = 0.8)[[1]]
Sys.getenv("R_ARCH")
## [1] "/x64"
library(RWeka)
iris <- read.arff(system.file("arff", "iris.arff",
package = "RWeka"))
##
## === 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
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).
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)
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 :
table(train$Embarked)
##
## C Q S
## 2 168 77 644
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)
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 :
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)
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 :
À 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, ]
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.
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 :
## 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 :
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 :
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 :
À 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
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:
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 :
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 :
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)
stopCluster(cl)
confusionMatrix(predict(caret.rf, titanic.test),
titanic.test$Survived)
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)
confusionMatrix(predict(caret.nnet, titanic.test),
titanic.test$Survived)
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 :
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.
Couverture
Le Machine learning avec R
Copyright
Introduction
Qui devrait lire ce livre ?
Portée du livre
Conventions utilisées dans ce livre