1 Objectif
Dans cet exemple, nous allons essayer d’expliciter le rôle des neurones de la couche cachée du
perceptron. Nous utiliserons des données générées artificiellement. Nous nous en tenons à 2
variables explicatives afin de pouvoir projeter
les données dans
le plan et ainsi décrire
concrètement le comportement de l’approche. Nous proposerons une petite routine de détection
automatique du nombre optimal de neurones avec le logiciel R (package nnet).
2 Données
Le fichier « artificial.xls » contient deux jeux de données de n = 2000 observations décrites par p = 2
variables X1 (définie dans [-0.5 ; 0.5]) et X2 ([0 ; 1]). La variable cible Y est binaire {pos, neg}.
Le premier ensemble « data2 » correspond à une liaison fonctionnelle relativement simple, elle est
définie de la manière suivante :
Si (0.1 * X2 > X1²) Alors Y = pos Sinon Y = neg
Le concept sous-jacent à « data4 » est un peu plus complexe
Si (X1 < 0)
Alors Si (0.5 * X2 > 0.5 - |X1|) Alors Y = pos Sinon Y = neg
Sinon Si (X2 > X1 et 1 – X2 > X1) Alors Y = pos Sinon Y = neg
Bien évidemment, nous ne disposons pas de ces informations dans les études réelles. Nous
comptons justement sur le système d’apprentissage pour tenter de les reconstituer ou, tout du
moins, de les approximer. L’idée de cet exemple pratique est de situer le comportement du
perceptron sur des données dont on connaît les tenants et aboutissants.
3 Perceptron avec R – Package « nnet »
Notre étude est rendue facile par le très faible nombre de descripteurs (p = 2) qui permet de projeter
les observations dans le plan et, ainsi, d’identifier le bon paramétrage. En pratique, cette démarche
n’est pas tenable, surtout lorsque le nombre de descripteurs augmente (p > 2). Il faut définir une
stratégie générique qui permet d’identifier automatiquement le bon nombre de neurones sur un
jeu de données. R est un outil privilégié pour cela. Tout simplement parce qu’il est très facile de
programmer des actions complexes avec quelques lignes de codes bien senties.
Dans cette section, dans un premier temps, nous reproduisons l’étude ci-dessus (sur « data2 ») pour
décrire la mise en œuvre du perceptron multicouche avec le package nnet. Puis, dans un second
temps, nous mettons en place un dispositif pour spécifier le nombre adéquat de neurones de la
couche cachée pour le fichier « data4 » où le concept à apprendre est plus complexe c.-à-d. il faut
plus de droites séparatrices, mais combien ?
3.1
La procédure nnet du package éponyme
3.1.1
Importation et définition des échantillons
Nous avons transformé la feuille Excel « data2 » en un fichier texte avec séparateur tabulation. Nous
l’importons dans R avec la commande read.table(). Nous affichons les caractéristiques des variables
avec la commande summary().
#loading the data file "data2"
data2 <- read.table(file="artificial2d_data2.txt",sep="\t",dec=".",header=T)
#number of instances
n <- nrow(data2)
print(n)
#summary
print(summary(data2))
Nous obtenons :
Nous partitionnons aléatoirement les données en deux échantillons de taille égale : la première, dite
d’apprentissage, servira à la construction du réseau ; la seconde, dite de test, sera utilisée pour
évaluer les performances en généralisation.
#splitting in train and test samples
set.seed(5)
index <- sample(n,1000)
data2.train <- data2[index,]
data2.test <- data2[-index,]
print(summary(data2.train))
print(summary(data2.test))
Advertisement
La commande set.seed() permet de fixer la valeur d’initialisation du générateur de nombres
aléatoires. Ainsi, lecteur devrait obtenir des résultats identiques à ceux décrits dans ce tutoriel.
Nous constatons que les caractéristiques des deux échantillons sont très proches. Ceci veut dire que
la subdivision ayant été réalisée de manière aléatoire. Il faudrait s’inquiéter dans le cas contraire.
3.1.2 Construction du modèle
Nous utilisons le package « nnet » dans ce tutoriel. Il présente l’avantage d’être particulièrement
simple à utiliser. Ses sorties sont en revanche très sobres, voire laconiques. Nous pouvons quand
même obtenir les poids synaptiques du perceptron.
#loading the nnet package
library(nnet)
#multilayer perceptron, 2 neurons into the hidden layer
set.seed(10)
model2 <- nnet(Y ~ .,skip=FALSE,size=2,data=data2.train,maxit=300,trace=F)
print(model2)
print(summary(model2))
La commande nnet() construit le réseau à partir de l’échantillon d’apprentissage. Nous fixons le
nombre d’itérations maximum à 300 (maxit = 300) pour nous assurer de la stabilité des résultats. La
valeur par défaut (maxit = 100) n’est apparemment pas suffisante pour nos données. L’option « skip
= FALSE » indique la présence d’une couche cachée, et « size = 2 » définit le nombre de neurones.
La variable cible étant binaire, nous avions constaté que les deux sorties étaient strictement
complémentaires. De ce fait, R n’en décrit qu’une seule. Nous obtenons le réseau suivant :
bias =
const
-0.83
const
178.04
-0.29
h1
-226.91
output
13.06
-2.04
-9.28
-1.90
X1 =
input 1
X2 =
input 2
-255.67
h2
3.1.3 Evaluation
Nous élaborons une fonction pour l’évaluation. Elle prend en entrée les données à utiliser et le
modèle. Ce dernier se charge de produire la prédiction à l’aide de la commande predict(). Nous
construisons par la suite la matrice de confusion à partir de la confrontation entre la cible observée
et la prédiction. Nous en déduisons le taux d’erreur (taux de mauvais classement).
#evaluating the model
err.rate <- function(test.data,model){
#prediction on the test set
pred <- predict(model,newdata=test.data,type="class")
#confusion matrix
mc <- table(test.data$Y,pred)
#error rate
error <- 1-sum(diag(mc))/sum(mc)
#output
return(error)
}
Appliquée sur l’échantillon test (1000 observations) et le réseau construit dans la section précédente,
#printing the error rate on the test set
print(err.rate(data2.test,model2))
Advertisement
Nous obtenons un taux d’erreur de 0.004.
5.2 Algorithme pour le paramétrage automatique du réseau
Notre objectif dans cette section est de mettre en place une procédure très simple de détection du
nombre « optimal » de neurones de la couche cachée. Elle doit être générique c.-à-d. s’appliquer
quel que soit le nombre de variables prédictives. Nous l’utiliserons alors pour apprendre la fonction –
le concept - associant (X1, X2) à Y pour les données « data4 ».
5.2.1 Algorithme de paramétrage automatique
Les données sont subdivisées en 2 échantillons : apprentissage et validation. Nous construisons
différentes hypothèses sur l’échantillon d’apprentissage, dans notre cas il s’agit du nombre de
neurones différents dans la couche cachée (1, ce qui revient à un perceptron simple ; puis 2, puis 3,
etc.), que nous évaluons sur l’échantillon test. La solution correspond à la configuration la plus
simple, c.-à-d. avec le plus petit nombre de neurones, permettant de minimiser le taux d’erreur en
généralisation.
Le programme tient en une boucle passant en revue différentes valeurs de « k », nombre de
neurones dans la couche cachée. Nous avons fixé ex ante le nombre maximal de neurones à tester (K
= 20). C’était le plus simple à implémenter. Mais nous pouvons également imaginer des stratagèmes
plus élaborés (ex. s’arrêter dès que le taux d’erreur ne décroît plus).
#detecting the right number of neurons in the hidden layer
K <- 20
res <- numeric(K)
for (k in 1:K){
set.seed(10)
model <- nnet(Y ~ .,skip=FALSE,size=k,data=data4.train,maxit=300,trace=F)
error <- err.rate(data4.test,model)
res[i] <- error
}
Le nom de la variable cible doit être Y dans ce petit programme. En revanche, il n’y a aucune
limitation concernant le nombre et le nom des prédicteurs.
5.2.2 Application sur les données « data4 »
Pour vérifier l’efficacité du programme, nous l’appliquons sur les données « datat4 » dont nous ne
sommes pas censés connaître le nombre adéquat de neurones (voir Section 2). Nous montrons ici le
processus complet : l’importation des données, leur subdivision en apprentissage-validation, la mise
en œuvre de la détection, l’affichage des résultats (nombre de neurones vs. taux d’erreur en
validation) dans un graphique.
#loading the data file "data4"
data4 <- read.table(file="artificial2d_data4.txt",sep="\t",dec=".",header=T)
#splitting in train and test samples
data4.train <- data4[index,]
data4.test <- data4[-index,]
#detecting the right number of neurons in the hidden layer
K <- 20
res <- numeric(K)
for (k in 1:K){
set.seed(10)
model <- nnet(Y ~ .,skip=FALSE,size=k,data=data4.train,maxit=300,trace=F)
#print(model)
error <- err.rate(data4.test,model)
res[k] <- error
}
plot(1:K,res,type="b",xlab="Neurons into the hidden layer",ylab="Test error rate")
Nous obtenons une courbe particulièrement édifiante. A partir de « k = 4 » neurones dans la couche
intermédiaire, nous reproduisons parfaitement le concept.
e
t
a
r
r
o
r
Advertisement
r
e
t
s
e
T
5
3
.
0
0
3
.
0
5
2
.
0
0
2
.
0
5
1
.
0
0
1
.
0
5
0
.
0
0
0
.
0
5
10
15
Neurons into the hidden layer
2
0
On serait tenté de parler de sur-apprentissage pour k = 14. Il faut surtout y voir un artefact lié aux
fluctuations d’échantillonnage. Il serait plus approprié de passer par la validation croisée pour
obtenir des résultats plus stables, mieux lissées. Mais, nous ne souhaitons pas alourdir l’exposé au
risque de semer le lecteur en cours de route dans le contexte de cet exemple pratique.
5.2.3 Vérification et représentation graphique
Est-ce que vraiment k = 4 neurones est la bonne solution ici ? Pour le savoir, et puisque que nous
n’avons que 2 descripteurs, nous projetons les observations étiquetées dans le plan.
#graphical representation of the instances
plot(data4$X1,data4$X2, pch = 21, bg = c("blue","red")[unclass(data4$Y)], main="Data4 problem")
Oui, il faut clairement une combinaison de 4 droites séparatrices pour isoler les « pos » des « neg ».
Data4 problem
2
X
$
4
Advertisement
a
t
a
d
0
.
1
8
.
0
6
.
0
4
.
0
2
.
0
0
.
0
(4)
(1)
-0.4
-0.2 (2)
0.0
(3)
data4$X1
0.2
0.4
5.3 Sélection de variables et paramétrage du réseau
Nous nous sommes placés dans une configuration très favorable dans cet exemple. Nous avions
un nombre réduit de variables prédictives et nous savions à l’avance qu’elles étaient toutes
pertinentes. Dans les études réelles, la situation est un peu plus corsée. Il faut à la fois détecter
les variables adéquates et identifier la bonne architecture du réseau.
A priori, la généralisation de notre approche à ce type de situation ne pose pas de problème. Il suffit
de mettre en place une double boucle imbriquée permettant de combiner la recherche des bonnes
variables prédictives et du nombre de neurones de la couche intermédiaire. Attention cependant au
sur apprentissage. En multipliant les hypothèses à tester, nous augmentons le risque de trouver des
solutions faussement optimales. De plus, l’échantillon de test tel que nous l’avons définie dans notre
démarche devient partie prenante de l’apprentissage puisqu’il sert à discerner la meilleure solution
parmi les très nombreuses combinaisons « variables – architecture » qu’on lui a soumises.
De fait, il faudrait plutôt prévoir 3 échantillons dans ce cas : le premier, apprentissage, sert à
l’apprentissage des modèles ; le second, validation (ou « sélection » même serait-on tenté de dire),
sert à sélectionner le meilleur modèle ; le troisième, test, ne sert qu’à mesurer les performances en
prédiction du modèle sélectionné. Ce dernier, n’intervenant qu’en dernier recours et n’ayant jamais
participé au processus d’apprentissage/sélection, permet ainsi d’obtenir une estimation non biaisée
des performances en prédiction du modèle sélectionné.
6 Conclusion
Le perceptron bénéficie d’une capacité très favorable en apprentissage supervisé, à juste titre au
regard de ses performances. Même si ces dernières années, si nous réfèrons aux publications dans
les revues et les conférences spécialisées, les méthodes ensemblistes et les SVM (support vector
machine, de manière plus générique les méthodes à noyaux « kernel machines ») semblent
bénéficier d’une plus grande popularité au sein de la communauté du data mining (oui, les effets de
mode sont partout, y compris dans la recherche). Il semble que ce soit moins vrai en ce qui concerne
leur utilisation dans les études réelles.