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>