Vérification des versions¶
In [1]:
#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 [2]:
#version de pytorch
import torch
print(torch.__version__)
2.3.0+cpu
Chargement et préparation des données¶
Importation, inspection¶
In [3]:
#modif. du dossier de travail
import os
os.chdir("C:/Users/ricco/Desktop/demo")
#librairie pandas
import pandas
#chargement de la première feuille de données
#individus et variables actives
X = pandas.read_excel("cars_autoencoder.xlsx",header=0,index_col=0,sheet_name=0)
#affichage des 10 premières lignes
print(X.head(10))
puissance cylindree vitesse longueur hauteur poids Modele CITRONC5 210 2946 230 475 148 1589 LAGUNA 165 1998 218 458 143 1320 CITRONC4 138 1997 207 426 146 1381 CLIO 100 1461 185 382 142 980 CITRONC2 61 1124 158 367 147 932 MODUS 113 1598 188 380 159 1170 MEGANECC 165 1998 225 436 141 1415 TWINGO 60 1149 151 344 143 840 MONDEO 145 1999 215 474 143 1378 VECTRA 150 1910 217 460 146 1428
In [4]:
#informations sur les variables
X.info()
<class 'pandas.core.frame.DataFrame'> Index: 28 entries, CITRONC5 to AUDIA8 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 puissance 28 non-null int64 1 cylindree 28 non-null int64 2 vitesse 28 non-null int64 3 longueur 28 non-null int64 4 hauteur 28 non-null int64 5 poids 28 non-null int64 dtypes: int64(6) memory usage: 1.5+ KB
In [5]:
#dimensions
print(X.shape)
#nombre d'observations
n = X.shape[0]
#nombre de variables
p = X.shape[1]
(28, 6)
Standardisation des variables¶
In [6]:
#outil centrage réduction
from sklearn.preprocessing import StandardScaler
#instanciation
std = StandardScaler()
#transformation
Z = std.fit_transform(X)
#affichage des 10 premières lignes
print(Z[:10,:])
[[ 0.84249298 0.87217654 0.86954849 0.96525293 0.02554647 0.9621173 ] [ 0.2271068 -0.106354 0.49971521 0.603188 -0.68975475 -0.01928404] [-0.1421249 -0.10738621 0.16070137 -0.07834598 -0.26057402 0.20326422] [-0.66178434 -0.6606482 -0.51732632 -1.01545521 -0.83281499 -1.25971696] [-1.19511902 -1.00850135 -1.34945121 -1.33492426 -0.11751377 -1.43483691] [-0.48400611 -0.51923608 -0.424868 -1.05805108 1.59920915 -0.56653386] [ 0.2271068 -0.106354 0.71545129 0.13463339 -0.97587523 0.32730751] [-1.20879427 -0.98269622 -1.56518729 -1.82477681 -0.68975475 -1.77048346] [-0.04639816 -0.1053218 0.40725689 0.94395499 -0.68975475 0.19231922] [ 0.02197808 -0.19718806 0.46889577 0.64578387 -0.26057402 0.37473583]]
In [7]:
#tranformer les variables Z en tensor
tensor_Z = torch.FloatTensor(Z)
#qui est d'un type particulier
print(type(tensor_Z))
<class 'torch.Tensor'>
In [8]:
#vérif. dimensions
print(tensor_Z.shape)
torch.Size([28, 6])
Auto-encodeur avec PyTorch¶
Elaboration de la structure¶
In [9]:
#Auto-encodeur
class MyEncoder(torch.nn.Module):
#constructeur
def __init__(self,p):
#appel du constructeur de l'ancêtre
super(MyEncoder,self).__init__()
#couche d'entrée (p variables) vers intermédiaire (2 neurones)
self.layer1 = torch.nn.Linear(p,2)
#fonction de transfert tangente hyperbolique
self.ft1 = torch.nn.Tanh()
#couche vers sortie (p neurones) - linéaire
self.layer2 = torch.nn.Linear(2,p)
#premier forward
#couche entrée -> couche cachée
def forward_1(self,x):
#application de la combinaison linéaire
comb_lin_1 = self.layer1(x)
#appliquer la fonction tanh
return self.ft1(comb_lin_1)
#second forward
#couche cachée -> couche sortie
def forward_2(self,x_prim):
#puis seconde combinaison linéaire
comb_lin_2 = self.layer2(x_prim)
return comb_lin_2
#calcul de la sortie du réseau
#à partir d'une matrice x en entrée
def forward(self,x):
#premier forward
out_1 = self.forward_1(x)
#second forward
out_2 = self.forward_2(out_1)
#return
return out_2
Paramétrage et instanciation¶
In [10]:
#fonction critère à optimiser - MSE
critere_rna = torch.nn.MSELoss()
In [11]:
# instanciation du modele
# p = le nombre de descripteurs => 6
rna = MyEncoder(p)
In [12]:
#algorithme d'optimisation
#on lui passe les paramètres à manipuler
optimiseur_rna = torch.optim.Adam(rna.parameters())
Valeur initiale de la perte¶
In [13]:
#projection avec les poids aléatoires
ZPred = rna.forward(tensor_Z)
#affichage
ZPred[:10,:]
Out[13]:
tensor([[-0.4863, -0.4191, -0.6232, -0.4683, 0.2391, -0.1625], [-0.6650, -0.7077, -0.6246, -0.1280, -0.0663, -0.4709], [-0.6239, -0.6417, -0.6242, -0.2059, 0.0035, -0.4002], [-0.8431, -0.9490, -0.6332, 0.1638, -0.3103, -0.7430], [-0.8625, -0.8983, -0.6462, 0.1170, -0.2364, -0.7139], [-0.7242, -0.5212, -0.6693, -0.3035, 0.2006, -0.3581], [-0.6131, -0.7463, -0.6050, -0.1017, -0.1373, -0.4746], [-0.8947, -1.0055, -0.6378, 0.2347, -0.3634, -0.8115], [-0.6312, -0.6196, -0.6296, -0.2266, 0.0352, -0.3870], [-0.5976, -0.5617, -0.6299, -0.2943, 0.0974, -0.3263]], grad_fn=<SliceBackward0>)
In [14]:
#valeur de la perte : observé vs. reconstitution
print(critere_rna(ZPred,tensor_Z))
tensor(1.1142, grad_fn=<MseLossBackward0>)
Entraînement du réseau¶
In [15]:
import numpy
#fonction pour apprentissage avec les paramètres :
#Z_in, instance de classe torch, critère à optimiser, algo d'optimisation...
#...et n_epochs nombre de passage sur la base
def train_session(Z_in,modele,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
Z_out = modele.forward(Z_in) #ou simplement modele()
#calculer la perte
perte = criterion(Z_out,Z_in)
#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
In [16]:
#lancer l'apprentissage
pertes = train_session(tensor_Z,rna,critere_rna,optimiseur_rna)
In [17]:
#valeur finale de la perte
print(pertes[-1])
0.09925896674394608
In [18]:
#courbe de décroissance de la perte
import matplotlib.pyplot as plt
plt.plot(numpy.arange(0,pertes.shape[0]),pertes)
plt.title("Evolution fnct de perte")
plt.xlabel("Epochs")
plt.ylabel("Loss")
plt.show()
Analyse de la structure du réseau¶
In [19]:
# entrée -> cachée (poids synaptiques)
print(rna.layer1.weight)
Parameter containing: tensor([[-0.0352, -0.0769, -0.0110, -0.0372, -0.1682, -0.0725], [ 0.0812, 0.0447, 0.0722, 0.0655, -0.1665, 0.0432]], requires_grad=True)
In [20]:
# entrée -> cachée (biais)
print(rna.layer1.bias)
Parameter containing: tensor([0.0594, 0.0218], requires_grad=True)
In [21]:
# cachée -> sortie (poids synaptiques)
print(rna.layer2.weight)
Parameter containing: tensor([[-1.6670, 2.2610], [-2.3754, 1.6074], [-1.2288, 2.4920], [-1.5783, 2.2018], [-3.2656, -2.5689], [-2.5200, 1.7039]], requires_grad=True)
In [22]:
# cachée -> sortie (biais)
print(rna.layer2.bias)
Parameter containing: tensor([0.0510, 0.1044, 0.0214, 0.0471, 0.2373, 0.1107], requires_grad=True)
Exploitation des résultats¶
Représentation des individus dans l'espace réduit¶
In [23]:
# coordonnées "factorielles" des individus
ZFact = rna.forward_1(tensor_Z)
#dimensions
print(ZFact.shape)
torch.Size([28, 2])
In [24]:
#positionnement des individus dans le plan
fig, axes = plt.subplots(figsize=(8,8))
axes.set_xlim(-0.8,+0.8)
axes.set_ylim(-0.8,+0.8)
for i in range(ZFact.shape[0]):
plt.annotate(X.index[i],(ZFact[i,0],ZFact[i,1]),fontsize=8)
plt.title('Position des véhicules dans le plan')
plt.xlabel('Coord 1')
plt.ylabel('Coord 2')
plt.show()
Reconstitution des données¶
In [25]:
# dans l'espace des variables standardisées
# attention, le résultat est un tensor, il faut le passer en numpy
Z_out = rna.forward(tensor_Z).detach().squeeze().numpy()
print(Z_out)
[[ 0.9534417 0.93105704 0.92136097 0.91887265 0.01450816 0.98737603] [ 0.30471158 0.10851523 0.39111406 0.300829 -0.8148198 0.11483908] [ 0.02473865 -0.03640842 0.05487291 0.02613201 -0.26215753 -0.0386911 ] [-0.7772301 -0.94523656 -0.65584934 -0.74199843 -0.819185 -1.002655 ] [-1.2910035 -1.2682925 -1.24368 -1.2439239 -0.05256239 -1.3450308 ] [-0.75563234 -0.38884223 -0.908673 -0.7414871 1.5015819 -0.41193336] [ 0.34410098 0.09250599 0.45702994 0.34085667 -1.0503544 0.09779334] [-1.4169495 -1.4958408 -1.3119286 -1.3613386 -0.5077206 -1.5864749 ] [ 0.33878216 0.15308934 0.4182589 0.33323577 -0.76530325 0.16212383] [ 0.28658143 0.19891882 0.31831944 0.27925566 -0.34646752 0.21084663] [ 0.26039487 0.18729807 0.285881 0.2534892 -0.28639087 0.1985403 ] [-0.39073104 0.01188725 -0.5787481 -0.39149716 1.6995754 0.01309332] [ 1.0047998 0.73511696 1.0968156 0.9777032 -1.0514753 0.7792753 ] [-0.42265207 -0.55981004 -0.3332305 -0.4017595 -0.64398515 -0.59386355] [-0.6241071 -0.52436006 -0.6466159 -0.6047181 0.35938734 -0.55597895] [-0.6514348 0.02263516 -0.96634084 -0.65281427 2.8457704 0.02482289] [-0.88373816 -0.94786394 -0.81060815 -0.84849083 -0.38134155 -1.0053146 ] [-1.01393 -1.0868185 -0.9303763 -0.9735149 -0.43455556 -1.1526899 ] [-1.5000597 -1.2736622 -1.5473355 -1.4529414 0.8059256 -1.350477 ] [ 0.03357074 0.04936725 0.02396144 0.0317177 0.07241131 0.05236989] [ 2.1986978 2.2068033 2.0941808 2.1167233 0.29234448 2.3403718 ] [ 0.51227415 0.00435632 0.7485808 0.5125059 -2.1417923 0.00399584] [ 0.4373072 0.6383015 0.3145775 0.41343498 0.92243624 0.6771743 ] [-1.3998269 -1.2958813 -1.38907 -1.3517867 0.286845 -1.3741901 ] [ 0.4342183 0.7724556 0.24145877 0.40525532 1.5169971 0.8196153 ] [ 1.1048027 1.0136132 1.1009915 1.0672218 -0.2660422 1.0748448 ] [ 1.216218 1.1404147 1.1994537 1.1739155 -0.18631265 1.2093394 ] [ 1.6728611 1.556875 1.6557957 1.6151229 -0.30708 1.6509564 ]]
In [26]:
#dans l'espace des variables initiales
#attention, Z_out est un tensor, on le passe en numpy
X_out = std.inverse_transform(Z_out)
In [27]:
#df avec les index et les noms des variables
df_out = pandas.DataFrame(X_out,columns=X.columns,index=X.index)
In [28]:
#affichage sans les décimales
df_out.style.format(precision=0)
Out[28]:
puissance | cylindree | vitesse | longueur | hauteur | poids | |
---|---|---|---|---|---|---|
Modele | ||||||
CITRONC5 | 218 | 3003 | 232 | 473 | 148 | 1596 |
LAGUNA | 171 | 2206 | 214 | 444 | 142 | 1357 |
CITRONC4 | 150 | 2066 | 204 | 431 | 146 | 1315 |
CLIO | 92 | 1185 | 181 | 395 | 142 | 1050 |
CITRONC2 | 54 | 872 | 161 | 371 | 147 | 957 |
MODUS | 93 | 1724 | 172 | 395 | 158 | 1212 |
MEGANECC | 174 | 2191 | 217 | 446 | 140 | 1352 |
TWINGO | 45 | 652 | 159 | 366 | 144 | 890 |
MONDEO | 173 | 2249 | 215 | 445 | 142 | 1370 |
VECTRA | 169 | 2294 | 212 | 443 | 145 | 1383 |
PASSAT | 167 | 2282 | 211 | 442 | 146 | 1380 |
MERC_A | 120 | 2113 | 183 | 411 | 160 | 1329 |
ALFA 156 | 222 | 2813 | 237 | 476 | 140 | 1539 |
AUDIA3 | 117 | 1559 | 191 | 411 | 143 | 1163 |
GOLF | 103 | 1593 | 181 | 401 | 150 | 1173 |
MUSA | 101 | 2123 | 170 | 399 | 168 | 1332 |
FIESTA | 84 | 1183 | 175 | 390 | 145 | 1050 |
CORSA | 74 | 1048 | 172 | 384 | 145 | 1009 |
PANDA | 39 | 867 | 152 | 361 | 153 | 955 |
AVENSIS | 151 | 2149 | 203 | 431 | 148 | 1340 |
CHRYS300 | 309 | 4239 | 270 | 529 | 150 | 1967 |
MAZDARX8 | 186 | 2105 | 226 | 454 | 133 | 1326 |
PTCRUISER | 180 | 2719 | 212 | 449 | 154 | 1511 |
YARIS | 46 | 846 | 157 | 366 | 150 | 949 |
VELSATIS | 180 | 2849 | 210 | 449 | 158 | 1550 |
BMW530 | 229 | 3083 | 238 | 480 | 146 | 1620 |
MERC_E | 237 | 3206 | 241 | 485 | 147 | 1657 |
AUDIA8 | 271 | 3609 | 256 | 506 | 146 | 1778 |
Identification des valeurs "atypiques"¶
In [29]:
#valeur du gap admis
#exprimé en écarts-type
gap = 0.6
In [30]:
#on utilise un formattage conditionnel
#basé sur un écart entre Z et Z_out
def formattage(col):
#numéro de colonne
j = X.columns.get_loc(col.name)
#code de formattage
code = numpy.empty(col.values.shape[0],dtype=str).tolist()
for i in range(col.values.shape[0]):
if (Z[i,j] < (Z_out[i,j]-gap)):
code[i] = 'background-color: green'
elif (Z[i,j] > (Z_out[i,j]+gap)):
code[i] = 'background-color: blue'
#renvoyer le code
return code
In [31]:
#affichage du tableau avec mise en
#évidence des situations atypiques
X.style.apply(func=formattage,axis=0)
Out[31]:
puissance | cylindree | vitesse | longueur | hauteur | poids | |
---|---|---|---|---|---|---|
Modele | ||||||
CITRONC5 | 210 | 2946 | 230 | 475 | 148 | 1589 |
LAGUNA | 165 | 1998 | 218 | 458 | 143 | 1320 |
CITRONC4 | 138 | 1997 | 207 | 426 | 146 | 1381 |
CLIO | 100 | 1461 | 185 | 382 | 142 | 980 |
CITRONC2 | 61 | 1124 | 158 | 367 | 147 | 932 |
MODUS | 113 | 1598 | 188 | 380 | 159 | 1170 |
MEGANECC | 165 | 1998 | 225 | 436 | 141 | 1415 |
TWINGO | 60 | 1149 | 151 | 344 | 143 | 840 |
MONDEO | 145 | 1999 | 215 | 474 | 143 | 1378 |
VECTRA | 150 | 1910 | 217 | 460 | 146 | 1428 |
PASSAT | 150 | 1781 | 221 | 471 | 147 | 1360 |
MERC_A | 140 | 1991 | 201 | 384 | 160 | 1340 |
ALFA 156 | 250 | 3179 | 250 | 443 | 141 | 1410 |
AUDIA3 | 102 | 1595 | 185 | 421 | 143 | 1205 |
GOLF | 75 | 1968 | 163 | 421 | 149 | 1217 |
MUSA | 100 | 1910 | 179 | 399 | 169 | 1275 |
FIESTA | 68 | 1399 | 164 | 392 | 144 | 1138 |
CORSA | 70 | 1248 | 165 | 384 | 144 | 1035 |
PANDA | 54 | 1108 | 150 | 354 | 154 | 860 |
AVENSIS | 115 | 1995 | 195 | 463 | 148 | 1400 |
CHRYS300 | 340 | 5654 | 250 | 502 | 148 | 1835 |
MAZDARX8 | 231 | 1308 | 235 | 443 | 134 | 1390 |
PTCRUISER | 223 | 2429 | 200 | 429 | 154 | 1595 |
YARIS | 65 | 998 | 155 | 364 | 150 | 880 |
VELSATIS | 150 | 2188 | 200 | 486 | 158 | 1735 |
BMW530 | 231 | 2979 | 250 | 485 | 147 | 1495 |
MERC_E | 204 | 3222 | 243 | 482 | 146 | 1735 |
AUDIA8 | 280 | 3697 | 250 | 506 | 145 | 1770 |
Variables illustratives (supplémentaires)¶
In [32]:
#variables illustratives
VarSupp = pandas.read_excel("cars_autoencoder.xlsx",header=0,index_col=0,sheet_name="VAR.ILLUS")
VarSupp.info()
<class 'pandas.core.frame.DataFrame'> Index: 28 entries, CITRONC5 to AUDIA8 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 co2 28 non-null int64 1 prix 28 non-null int64 2 carburant 28 non-null object dtypes: int64(2), object(1) memory usage: 896.0+ bytes
In [33]:
#données - coord. embedding + var. illus
dfSupp = pandas.DataFrame(ZFact.detach().squeeze().numpy(),index=VarSupp.index,columns=["F1","F2"])
dfSupp.info()
<class 'pandas.core.frame.DataFrame'> Index: 28 entries, CITRONC5 to AUDIA8 Data columns (total 2 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 F1 28 non-null float32 1 F2 28 non-null float32 dtypes: float32(2) memory usage: 448.0+ bytes
In [34]:
#concaténation avec les var. supp.
dfSupp = pandas.concat([dfSupp,VarSupp],axis=1)
dfSupp.info()
<class 'pandas.core.frame.DataFrame'> Index: 28 entries, CITRONC5 to AUDIA8 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 F1 28 non-null float32 1 F2 28 non-null float32 2 co2 28 non-null int64 3 prix 28 non-null int64 4 carburant 28 non-null object dtypes: float32(2), int64(2), object(1) memory usage: 1.1+ KB
In [35]:
#appréhension des variables catégorielles
import seaborn as sns
sns.scatterplot(data=dfSupp,x="F1",y="F2",hue="carburant")
Out[35]:
<Axes: xlabel='F1', ylabel='F2'>
In [36]:
#appréhension des variables quantitatives
sns.scatterplot(data=dfSupp,x="F1",y="F2",hue="prix",legend=False)
Out[36]:
<Axes: xlabel='F1', ylabel='F2'>
Individus supplémentaires¶
In [37]:
#chargement + affichage
IndSupp = pandas.read_excel("cars_autoencoder.xlsx",header=0,index_col=0,sheet_name="IND.ILLUS")
IndSupp
Out[37]:
puissance | cylindree | vitesse | longueur | hauteur | poids | |
---|---|---|---|---|---|---|
Modele | ||||||
P407 | 136 | 1997 | 212 | 468 | 145 | 1415 |
P307CC | 180 | 1997 | 225 | 435 | 143 | 1490 |
P1007 | 75 | 1360 | 165 | 374 | 161 | 1181 |
P607 | 204 | 2721 | 230 | 491 | 145 | 1723 |
In [38]:
#transformation 1 - standardisation
#avec les param. (moyenne, écart-type) calculés sur les ind. actifs
ZIndSupp = std.transform(IndSupp)
ZIndSupp
Out[38]:
array([[-0.1694754 , -0.10738621, 0.31479857, 0.81616737, -0.40363426, 0.32730751], [ 0.43223553, -0.10738621, 0.71545129, 0.11333545, -0.68975475, 0.60093242], [-1.00366555, -0.76490092, -1.13371512, -1.1858387 , 1.88532964, -0.52640221], [ 0.76044149, 0.63993037, 0.86954849, 1.30601992, -0.40363426, 1.45099381]])
In [39]:
#calcul des coordonnées "factorielles"
FIndSupp = rna.forward_1(torch.FloatTensor(ZIndSupp))
FIndSupp
Out[39]:
tensor([[ 0.0838, 0.1594], [ 0.1124, 0.2468], [-0.0687, -0.5299], [-0.1115, 0.3716]], grad_fn=<TanhBackward0>)
In [40]:
#positionnement des individus dans le plan
fig, axes = plt.subplots(figsize=(8,8))
axes.set_xlim(-0.8,+0.8)
axes.set_ylim(-0.8,+0.8)
#individus actifs
for i in range(ZFact.shape[0]):
plt.annotate(X.index[i],(ZFact[i,0],ZFact[i,1]),fontsize=8,c="silver")
#individus supplémentaires
for i in range(FIndSupp.shape[0]):
plt.annotate(IndSupp.index[i],(FIndSupp[i,0],FIndSupp[i,1]),fontsize=8,c="blue")
plt.title('Position des véhicules dans le plan')
plt.xlabel('Coord 1')
plt.ylabel('Coord 2')
plt.show()