Vérification des versions¶

In [ ]:
#version de python
import sys
print(sys.version)
3.11.9 | packaged by Anaconda, Inc. | (main, Apr 19 2024, 16:40:41) [MSC v.1916 64 bit (AMD64)]
In [ ]:
#version de pytorch
import torch
print(torch.__version__)
2.2.0

Importation et préparation des données¶

In [ ]:
#modifier le dossier de travail
import os
os.chdir("C:/Users/ricco/Desktop/demo")

#importer les données
import pandas
D = pandas.read_excel("iris.xlsx",header=0)
print(D.info())
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 150 entries, 0 to 149
Data columns (total 5 columns):
 #   Column     Non-Null Count  Dtype  
---  ------     --------------  -----  
 0   SepLength  150 non-null    float64
 1   SepWidth   150 non-null    float64
 2   PetLength  150 non-null    float64
 3   PetWidth   150 non-null    float64
 4   Species    150 non-null    object 
dtypes: float64(4), object(1)
memory usage: 6.0+ KB
None

Partition apprentissage-test

In [ ]:
# Partion Train/Test
from sklearn.model_selection import train_test_split
DTrain, DTest = train_test_split(D,test_size=50,stratify=D.Species,random_state=0)

#dimensions
print(DTrain.shape)
print(DTest.shape)
(100, 5)
(50, 5)

Préparation des descripteurs

In [ ]:
# X - toutes les colonnes sauf la dernière
XTrain = DTrain.iloc[:,:-1]

#attention, doit être un dataframe avec 1 colonne
#sinon, onehotencoder de sklearn ne fonctionnera pas
#DTrain.iloc[:,-1] -> Series et non Dataframe, provoquera une erreur
yTrain = DTrain.iloc[:,-1:]

#dimensions
print(XTrain.shape)
print(yTrain.shape)
(100, 4)
(100, 1)
In [ ]:
#centrage-réduction des descripteurs
#avec StandardScaler
from sklearn.preprocessing import StandardScaler
sts = StandardScaler()

#données centrées et réduites
ZTrain = sts.fit_transform(XTrain)

#dimensions
print(ZTrain.shape)

#10 premières lignes
print(ZTrain[:10,:])
(100, 4)
[[-0.08929295  2.06443616 -1.4465214  -1.32561253]
 [ 0.38066995  0.75090308  0.9028173   1.44984741]
 [ 0.1456885  -1.87616309  0.1197044  -0.26829446]
 [-1.02921875  0.75090308 -1.22277486 -1.06128301]
 [-0.7942373   0.75090308 -1.33464813 -1.32561253]
 [-1.02921875  0.96982526 -1.39058477 -1.19344777]
 [-0.44176513 -1.43831872  0.00783113 -0.1361297 ]
 [ 0.6156514  -0.78155218  0.84688067  0.92118837]
 [ 0.26317922 -0.12478564  0.62313412  0.78902361]
 [ 0.49816068 -0.56263     0.73500739  0.39252933]]
In [ ]:
#tranformer les variables Z en tensor
tensor_ZTrain = torch.FloatTensor(ZTrain)

#qui est d'un type particulier
print(type(tensor_ZTrain))
<class 'torch.Tensor'>

Encodage de la variable cible sous la forme d'une matrice 0/1.

In [ ]:
#codage 0/1 de la variable cible
from sklearn.preprocessing import OneHotEncoder
ohe = OneHotEncoder(sparse_output=False)

#variable encodée -> matrice
MYTrain = ohe.fit_transform(yTrain)

#dimensions
print(MYTrain.shape)
(100, 3)
In [ ]:
#premières lignes
print(MYTrain[:10,:])
[[1. 0. 0.]
 [0. 0. 1.]
 [0. 1. 0.]
 [1. 0. 0.]
 [1. 0. 0.]
 [1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]
 [0. 0. 1.]
 [0. 0. 1.]]
In [ ]:
#vérification pour ces premières lignes
print(DTrain.Species[:10])
14         setosa
148     virginica
62     versicolor
26         setosa
28         setosa
40         setosa
80     versicolor
111     virginica
127     virginica
133     virginica
Name: Species, dtype: object
In [ ]:
#là aussi passer en tensor
tensor_MYTrain = torch.FloatTensor(MYTrain)
print(tensor_MYTrain.shape)
torch.Size([100, 3])

Perceptron simple multiclasse¶

Classe - Architecture et agencement du réseau

In [ ]:
#classe de calcul - Perceptron simple
#héritier de torch.nn.Module
class MyPS(torch.nn.Module):
    #constructeur - liste des éléments
    #qui composent le réseau
    def __init__(self,p,K):
        #appel du constructeur de l'ancêtre
        super(MyPS,self).__init__()
        #couche d'entrée - p variables explicatives
        #vers sortie - autant de neurones que modalités de Y
        self.layer1 = torch.nn.Linear(p,K)
        #fonction de transfert sotfmax à la sortie
        #dim = 1 indique que la normalisation se fait ligne par ligne
        #pour la matrice des probabilités d'affectation
        self.ft1 = torch.nn.Softmax(dim=1)
        
    #structuration des éléments
    #calcul de la sortie du réseau
    #à partir d'une matrice x en entrée
    def forward(self,x):
        #application de la combinaison linéaire
        comb_lin = self.layer1(x)
        #transformation avec la fonction de transfert
        proba = self.ft1(comb_lin)
        return proba
In [ ]:
#instanciation du modele
#ZTrain.shape[1] pour le nombre de descripteurs => 4
#MYTrain.shape[1] pour le nombre de modalités de Y => 3
ps = MyPS(tensor_ZTrain.shape[1],tensor_MYTrain.shape[1])

Fonction de perte.

In [ ]:
#fonction critère à optimiser
critere_ps = torch.nn.MSELoss()

Algorithme d'optimisation

In [ ]:
#algorithme d'optimisation
#on lui passe les paramètres à manipuler
optimiseur_ps = torch.optim.Adam(ps.parameters())

Vérification 1 - Coefficients du réseau

In [ ]:
#poids synaptiques
#initialisés aléatoirement
print(ps.layer1.weight)
Parameter containing:
tensor([[-0.3821,  0.1018,  0.2921, -0.2374],
        [-0.3275, -0.1199,  0.3713, -0.4724],
        [-0.3262, -0.3296, -0.2911, -0.2648]], requires_grad=True)
In [ ]:
#coefs. (poids) mieux organisés
pandas.DataFrame(ps.layer1.weight.detach().numpy().T,
                 index = D.columns[:-1],
                 columns = list(D.Species.unique()))
Out[ ]:
setosa versicolor virginica
SepLength -0.382063 -0.327539 -0.326248
SepWidth 0.101787 -0.119865 -0.329602
PetLength 0.292132 0.371290 -0.291080
PetWidth -0.237383 -0.472387 -0.264799
In [ ]:
#et les intercept
print(ps.layer1.bias)
Parameter containing:
tensor([ 0.1827, -0.2088,  0.2739], requires_grad=True)

Vérification 2 - Normalisation (softmax) des sorties du réseau

In [ ]:
#probas d'affectation avec les coefs. initiaux
probaFirst = ps.forward(tensor_ZTrain)

#pour les 10 premiers individus
tempProba = probaFirst.detach().numpy()
print(tempProba[:10,:])
[[0.38506058 0.19962992 0.41530952]
 [0.5246918  0.23424596 0.24106216]
 [0.22515489 0.2500379  0.52480716]
 [0.31231174 0.1968728  0.49081543]
 [0.29538146 0.19891919 0.5056994 ]
 [0.31309077 0.19138753 0.4955216 ]
 [0.25484282 0.23900309 0.50615406]
 [0.37578815 0.26904526 0.3551665 ]
 [0.4143197  0.254945   0.3307354 ]
 [0.37232742 0.2831788  0.34449378]]
In [ ]:
#et la somme ligne par ligne est bien égale à 1
import numpy
numpy.sum(tempProba,axis=1)
Out[ ]:
array([1.        , 0.99999994, 0.99999994, 1.        , 1.        ,
       0.99999994, 1.        , 0.99999994, 1.        , 1.        ,
       1.        , 1.        , 0.99999994, 1.        , 1.        ,
       1.        , 1.        , 1.        , 1.        , 1.        ,
       1.        , 1.        , 1.        , 1.        , 1.        ,
       1.        , 0.9999999 , 1.        , 1.        , 1.        ,
       0.99999994, 1.        , 1.        , 1.        , 1.        ,
       1.        , 1.        , 1.        , 1.        , 1.        ,
       0.99999994, 1.        , 1.        , 1.        , 1.        ,
       1.        , 1.        , 1.        , 1.0000001 , 0.9999999 ,
       0.99999994, 1.        , 1.        , 1.        , 1.        ,
       1.        , 1.        , 0.99999994, 1.        , 1.        ,
       0.99999994, 1.        , 0.9999999 , 1.        , 1.        ,
       0.9999999 , 1.        , 1.        , 1.        , 1.        ,
       1.        , 1.        , 1.        , 1.        , 1.        ,
       1.        , 1.        , 1.        , 1.        , 1.        ,
       1.0000001 , 1.0000001 , 1.        , 1.        , 1.        ,
       1.        , 0.99999994, 0.99999994, 1.        , 1.0000001 ,
       1.        , 1.        , 1.        , 1.        , 1.        ,
       1.        , 1.        , 1.        , 0.99999994, 1.        ],
      dtype=float32)

Fonction "fit" avec les paramètres idoines

In [ ]:
#fonction pour apprentissage avec les paramètres :
#X, y, instance de classe torch, critère à optimiser, algo d'optimisation...
#...et n_epochs nombre de passage sur la base
def train_session(X,y,classifier,criterion,optimizer,n_epochs=10000):
    #vecteur pour collecter le loss au fil des itérations
    losses = numpy.zeros(n_epochs)
    #itérer (boucle) pour optimiser - n_epochs fois sur la base
    for iter in range(n_epochs):
        #réinitialiser (ràz) le gradient
        #nécessaire à chaque passage sinon PyTorch accumule
        optimizer.zero_grad()
        #calculer la sortie du réseau
        yPred = classifier.forward(X) #ou simplement classifier(X)
        #calculer la perte
        perte = criterion(yPred,y)
        #collecter la perte calculée dans le vecteur losses
        losses[iter] = perte.item()
        #calcul du gradient et retropropagation
        perte.backward()
        #màj des poids synaptiques
        optimizer.step()
    #sortie de la boucle
    #renvoie le vecteur avec les valeurs de perte à chaque epoch
    return losses

Entraînement du modèle sur les données d'apprentissage

In [ ]:
#lancer l'apprentissage
#revenir sur les paramètres passés à la fonction
pertes = train_session(tensor_ZTrain,tensor_MYTrain,ps,critere_ps,optimiseur_ps)

Evolution de la perte (au départ, à l'arrivée)

In [ ]:
#différentes valeurs de la perte au cours
#de l'entraînement sur les données TRAIN
print(f"Perte initiale : {pertes[0]}")
print(f"Perte finale : {pertes[-1]}")
Perte initiale : 0.2622387707233429
Perte finale : 0.011758451350033283

Coefficients obtenus après entraînement

In [ ]:
#affichage des poids après entraînement
#c.-à-d. après optimisation de la fonction de coût
pandas.DataFrame(ps.layer1.weight.detach().numpy().T,
                 index = D.columns[:-1],
                 columns = list(D.Species.unique()))
Out[ ]:
setosa versicolor virginica
SepLength -2.747997 1.720873 1.784792
SepWidth 2.478131 -1.549065 -3.461830
PetLength -3.119729 0.018623 3.360341
PetWidth -3.780033 -5.065096 3.310145
In [ ]:
#et les intercept
print(ps.layer1.bias)
Parameter containing:
tensor([ 0.0534,  3.9643, -3.9658], requires_grad=True)

Evaluation en test¶

Préparation des descripteurs

In [ ]:
#préparation des descripteurs l'échantillon test
tensor_ZTest = torch.FloatTensor(sts.transform(DTest.iloc[:,:-1]))
tensor_ZTest.shape
Out[ ]:
torch.Size([50, 4])

Application du modèle : probabilités d'appartenance aux classes

In [ ]:
#prédiction des probas. d'appartenance
proba_test = ps.forward(tensor_ZTest).detach().numpy()

#affichage des 10 premières lignes
print(proba_test[:10,:])
[[9.7680941e-06 2.3420505e-02 9.7656971e-01]
 [9.7211480e-01 2.7885241e-02 6.7695036e-13]
 [3.1735426e-06 2.1444676e-02 9.7855216e-01]
 [9.9591810e-01 4.0819724e-03 6.9231662e-14]
 [9.4342586e-06 6.7108381e-04 9.9931955e-01]
 [9.9916887e-01 8.3111547e-04 4.0741853e-15]
 [2.0845798e-03 9.7747451e-01 2.0440888e-02]
 [9.9979287e-01 2.0718497e-04 9.1551463e-15]
 [1.8217407e-04 9.9098301e-01 8.8348379e-03]
 [9.9033582e-01 9.6641444e-03 7.6458147e-14]]

Transformation des probabilités en codes 0/1

In [ ]:
#transformation des probas en affectation 0/1
#en identifiant le max par ligne
idx_pred = numpy.apply_along_axis(lambda x: (x==numpy.max(x)).astype(int),axis=1,arr=proba_test)

#affichage des 10 premiers
print(idx_pred[:10,:])
[[0 0 1]
 [1 0 0]
 [0 0 1]
 [1 0 0]
 [0 0 1]
 [1 0 0]
 [0 1 0]
 [1 0 0]
 [0 1 0]
 [1 0 0]]

Transformation des codes 0/1 en classe prédite

In [ ]:
#transformer en prédiction {setosa, versicolor, virginica}
#avec la transformation inverse du OneHotEncoder
pred_test = ohe.inverse_transform(idx_pred)

#10 premières prédictions
print(pred_test[:10])
[['virginica']
 ['setosa']
 ['virginica']
 ['setosa']
 ['virginica']
 ['setosa']
 ['versicolor']
 ['setosa']
 ['versicolor']
 ['setosa']]

Matrice de confusion

In [ ]:
#matrice de confusion
from sklearn import metrics
metrics.ConfusionMatrixDisplay(
    metrics.confusion_matrix(DTest.Species,pred_test),
    display_labels=ohe.categories_[0]).plot()
Out[ ]:
<sklearn.metrics._plot.confusion_matrix.ConfusionMatrixDisplay at 0x1fa65570f90>
No description has been provided for this image