Environnement et packages¶

In [37]:
# activer l'environnement
using Pkg
Pkg.activate("env_julia_flux")
  Activating project at `c:\Users\ricco\Desktop\demo\env_julia_flux`
In [38]:
# liste des packages installés
Pkg.status()
Status `C:\Users\ricco\Desktop\demo\env_julia_flux\Project.toml`
  [a93c6f00] DataFrames v1.8.2
  [587475ba] Flux v0.16.10
  [91a5bcdd] Plots v1.41.6
  [fdbf4ff8] XLSX v0.11.10

Chargement et préparation des données¶

Importation, inspection¶

In [39]:
# packages
import DataFrames as DFR
import XLSX

# lecture des données
df = DFR.DataFrame(XLSX.readtable("./cars_autoencoder.xlsx"))

# premières lignes
println(DFR.size(df))
(28, 7)
In [40]:
# premières lignes
DFR.first(df,5)
5×7 DataFrame
RowModelepuissancecylindreevitesselongueurhauteurpoids
StringInt64Int64Int64Int64Int64Int64
1CITRONC5 21029462304751481589
2LAGUNA 16519982184581431320
3CITRONC4 13819972074261461381
4CLIO 1001461185382142980
5CITRONC2 611124158367147932
In [41]:
# description
DFR.describe(df)
7×7 DataFrame
Rowvariablemeanminmedianmaxnmissingeltype
SymbolUnion…AnyUnion…AnyInt64DataType
1ModeleALFA 156 YARIS 0String
2puissance148.39354142.53400Int64
3cylindree2101.049981979.556540Int64
4vitesse201.786150200.52500Int64
5longueur429.679344432.55060Int64
6hauteur147.821134146.51690Int64
7poids1325.298401369.018350Int64
In [42]:
# variables actives
X = df[:,2:end]
names(X)
6-element Vector{String}:
 "puissance"
 "cylindree"
 "vitesse"
 "longueur"
 "hauteur"
 "poids"

Standardisation¶

In [43]:
import Statistics

# moyennes
moyenne = Statistics.mean(Matrix(X),dims=1)
println(moyenne)

# écarts-type (avec 1/n)
ecart_type = Statistics.std(Matrix(X),dims=1,corrected = false)
println(ecart_type)
[148.39285714285714 2101.035714285714 201.78571428571428 429.67857142857144 147.82142857142858 1325.2857142857142]
[73.12481466922269 968.799606366517 32.44705263178454 46.952904650319994 6.990062187688683 274.0978523320845]
In [44]:
# centrage-réduction
Z = (X .- moyenne) ./ ecart_type
DFR.describe(Z)
6×7 DataFrame
Rowvariablemeanminmedianmaxnmissingeltype
SymbolFloat64Float64Float64Float64Int64DataType
1puissance7.93016e-17-1.29085-0.08058632.620280Float64
2cylindree4.7581e-17-1.13856-0.125453.667390Float64
3vitesse2.45835e-16-1.59601-0.0396251.485940Float64
4longueur-3.96508e-16-1.824780.06009061.625490Float64
5hauteur-1.7486e-15-1.9773-0.1890443.029810Float64
6poids2.53765e-16-1.770480.1594841.859610Float64
In [45]:
# dix premières lignes
DFR.first(Z,10)
10×6 DataFrame
Rowpuissancecylindreevitesselongueurhauteurpoids
Float64Float64Float64Float64Float64Float64
10.8424930.8721770.8695480.9652530.02554650.962117
20.227107-0.1063540.4997150.603188-0.689755-0.019284
3-0.142125-0.1073860.160701-0.078346-0.2605740.203264
4-0.661784-0.660648-0.517326-1.01546-0.832815-1.25972
5-1.19512-1.0085-1.34945-1.33492-0.117514-1.43484
6-0.484006-0.519236-0.424868-1.058051.59921-0.566534
70.227107-0.1063540.7154510.134633-0.9758750.327308
8-1.20879-0.982696-1.56519-1.82478-0.689755-1.77048
9-0.0463982-0.1053220.4072570.943955-0.6897550.192319
100.0219781-0.1971880.4688960.645784-0.2605740.374736
In [47]:
# préparer pour Flux
# d'abord transposer
ZT = Float32.(Matrix(Z)')
println(size(ZT))
(6, 28)

Autoencodeur avec "Flux"¶

Structure et paramétrage¶

In [48]:
import Flux

# définir la structure du réseau
# encodeur - interieur activation tanh
encoder = Flux.Chain(Flux.Dense(6 => 2, Flux.tanh))

# décodeur - sortie activation linéaire par défaut
decoder = Flux.Chain(Flux.Dense(2 => 6))

# réseau complet
autoencoder = Flux.Chain(encoder,decoder)
Chain(
  Chain(
    Dense(6 => 2, tanh),                # 14 parameters
  ),
  Chain(
    Dense(2 => 6),                      # 18 parameters
  ),
)                   # Total: 4 arrays, 32 parameters, 336 bytes.
In [49]:
# définir l'algo d'optimisation
# une descente de gradient améliorée
opt = Flux.setup(Flux.Adam(0.01), autoencoder)
(layers = ((layers = ((weight = Leaf(Adam(eta=0.01, beta=(0.9, 0.999), epsilon=1.0e-8), (Float32[0.0 0.0 … 0.0 0.0; 0.0 0.0 … 0.0 0.0], Float32[0.0 0.0 … 0.0 0.0; 0.0 0.0 … 0.0 0.0], (0.9, 0.999))), bias = Leaf(Adam(eta=0.01, beta=(0.9, 0.999), epsilon=1.0e-8), (Float32[0.0, 0.0], Float32[0.0, 0.0], (0.9, 0.999))), σ = ()),),), (layers = ((weight = Leaf(Adam(eta=0.01, beta=(0.9, 0.999), epsilon=1.0e-8), (Float32[0.0 0.0; 0.0 0.0; … ; 0.0 0.0; 0.0 0.0], Float32[0.0 0.0; 0.0 0.0; … ; 0.0 0.0; 0.0 0.0], (0.9, 0.999))), bias = Leaf(Adam(eta=0.01, beta=(0.9, 0.999), epsilon=1.0e-8), (Float32[0.0, 0.0, 0.0, 0.0, 0.0, 0.0], Float32[0.0, 0.0, 0.0, 0.0, 0.0, 0.0], (0.9, 0.999))), σ = ()),),)),)

Boucle d'entraînement¶

In [50]:
# vecteur pour stocker la loss à chaque itération
loss_history = Float64[]

# nombre d'itérations
n_epochs = 1000

# boucle d'entraînement avec fonction de coût MSE
for i in 1:n_epochs
    # calcul du gradient
    grads = Flux.gradient(autoencoder) do m
        Flux.mse(m(ZT), ZT)
    end
    # mise à jour des poids synaptiques (coefficients)
    Flux.update!(opt,autoencoder,grads[1])
    # récupération de la loss courante
    cur_loss = Flux.mse(autoencoder(ZT),ZT)
    # stockage
    push!(loss_history,cur_loss)
end

# affichage de la première valeur
println("Loss depart = $(loss_history[1])")

# et de la dernière
println("Loss fin = $(loss_history[end])")
Loss depart = 1.0398298501968384
Loss fin = 0.10130723565816879
In [51]:
# courbe de décroissance de la loss
import Plots
Plots.plot(1:n_epochs,loss_history,title="Decroissance de la loss",label="")
No description has been provided for this image

Inspection de la structure du réseau¶

In [52]:
# structure du réseau
autoencoder.layers
(Chain(Dense(6 => 2, tanh)), Chain(Dense(2 => 6)))
In [53]:
# poids (coefs.) couche entrée -> couche cachée
autoencoder.layers[1].layers[1].weight
2×6 Matrix{Float32}:
  0.121293    0.0845914   0.0830813   0.0941039  -0.252134   0.0779762
 -0.0447269  -0.147656   -0.0116939  -0.0688605  -0.313794  -0.107742
In [54]:
# intercept
autoencoder.layers[1].layers[1].bias
2-element Vector{Float32}:
 0.011199294
 0.11255823
In [55]:
# poids (coefs.) couche cachée -> couche sortie
autoencoder.layers[2].layers[1].weight
6×2 Matrix{Float32}:
  1.69736  -1.00381
  1.268    -1.44341
  1.85964  -0.728047
  1.69045  -0.931925
 -1.72879  -2.02292
  1.34617  -1.5084
In [56]:
# intercept
autoencoder.layers[2].layers[1].bias
6-element Vector{Float32}:
 0.086312234
 0.13462688
 0.05689584
 0.079145245
 0.2199395
 0.14034763

Représentation "factorielle" des individus - Embeddings¶

In [57]:
# représentation des individus dans l'espace réduit
# coordonnées => embeddings
coord = encoder(ZT)
size(coord)
(2, 28)
In [58]:
# projection
# on est bien dans (-1,+1) puisque activation tanh()
Plots.scatter(coord[1,:],coord[2,:],label="",markersize=0,lims=(-1,+1),
            annotations=(coord[1,:],coord[2,:],Plots.text.(df.Modele,7,:black)))
No description has been provided for this image

Reconstitution des données - Détection des valeurs "atypiques"¶

Valeurs estimées par le modèle (qui joue le rôle de filtre)¶

In [59]:
# reconstitution
ZPred = autoencoder(ZT)
size(ZPred)
(6, 28)
In [60]:
# transposer
Pred = Matrix(ZPred)'
size(Pred)
(28, 6)

Ecarts¶

In [61]:
# calculer les écarts entre Z et Pred
ecart = (Z .- Pred)
ecart
28×6 DataFrame
3 rows omitted
Rowpuissancecylindreevitesselongueurhauteurpoids
Float64Float64Float64Float64Float64Float64
1-0.154367-0.107444-0.0966282-0.004625450.00947681-0.0696125
2-0.0718321-0.204640.1052430.2931990.163939-0.127802
3-0.154773-0.05133070.1118-0.09738970.04012340.260661
40.1222280.2796010.151663-0.269339-0.0742716-0.273621
50.1032260.27063-0.0926486-0.072135-0.0819527-0.0873999
60.275214-0.1268780.50071-0.2848720.0584809-0.143791
7-0.108554-0.1808910.253312-0.2169320.1399650.242232
80.1687610.460146-0.277636-0.49332-0.275834-0.25286
9-0.388294-0.257028-0.02284570.5932690.1140310.0278259
10-0.254137-0.3825010.1552160.3687010.112860.177426
11-0.213404-0.4962040.3290060.6446190.166226-0.0496302
120.298629-0.1086470.591674-0.531469-0.01306210.0695836
130.405660.3918360.400526-0.6975450.088413-0.456551
⋮⋮⋮⋮⋮⋮⋮
17-0.2097380.233693-0.3471980.054799-0.1628610.324124
18-0.0490160.213042-0.1893260.0137659-0.1424770.090662
190.1584090.224964-0.0978142-0.1848410.142729-0.376119
20-0.495763-0.163566-0.2385630.673122-0.04378610.216145
210.5013741.52512-0.535653-0.515338-0.272332-0.39522
220.644723-0.8120010.294687-0.2353620.1364910.230471
230.563633-0.315094-0.387762-0.438723-0.02300460.301057
240.2585740.168819-0.0496766-0.03120230.0385955-0.245501
25-0.400499-0.669243-0.2801670.82208-0.06038650.704467
260.0120391-0.1357990.3725360.08535290.111525-0.479783
27-0.488362-0.024410.0353052-0.105125-0.07993560.249218
280.148310.0867757-0.1481210.0126749-0.156793-0.0228629

Visualisation des écarts¶

In [62]:
# ecart max
e_max = maximum(abs.(Matrix(ecart)))
1.5251186311280351
In [63]:
# visaulisation à l'aide d'un heatmap
Plots.heatmap(Matrix(ecart),
                clim=(-e_max,+e_max),
                color=:RdBu,
                yflip=true,
                yticks=(1:DFR.nrow(df),df.Modele),
                xtick=(1:DFR.ncol(X),names(X)))
No description has been provided for this image

Visualisation en introduisant un seuil¶

In [64]:
# ou bien appliquer un seuil (ex. à 0.6 de part et d'autre de 0)
M = Matrix(ecart)
ecart_seuil = ifelse.(abs.(M) .< 0.6, 0, M)
28×6 Matrix{Real}:
 0          0         0  0         0  0
 0          0         0  0         0  0
 0          0         0  0         0  0
 0          0         0  0         0  0
 0          0         0  0         0  0
 0          0         0  0         0  0
 0          0         0  0         0  0
 0          0         0  0         0  0
 0          0         0  0         0  0
 0          0         0  0         0  0
 ⋮                                    ⋮
 0          0         0  0.673122  0  0
 0          1.52512   0  0         0  0
 0.644723  -0.812001  0  0         0  0
 0          0         0  0         0  0
 0          0         0  0         0  0
 0         -0.669243  0  0.82208   0  0.704467
 0          0         0  0         0  0
 0          0         0  0         0  0
 0          0         0  0         0  0
In [65]:
# de nouveau le heatmap, plus lisible cette fois-ci
Plots.heatmap(ecart_seuil,
                clim=(-e_max,+e_max),
                color=:RdBu,
                yflip=true,
                yticks=(1:DFR.nrow(df),df.Modele),
                xtick=(1:DFR.ncol(X),names(X)))
No description has been provided for this image

Variables illustratives (supplémentaires)¶

Chargement des données¶

In [66]:
# lecture des données -- variables supplémentaires
dfVarSupp = DFR.DataFrame(XLSX.readtable("./cars_autoencoder.xlsx","VAR.ILLUS"))
DFR.first(dfVarSupp,5)
5×4 DataFrame
RowModeleco2prixcarburant
StringInt64Int64String
1CITRONC5 23833000Essence
2LAGUNA 19625350Essence
3CITRONC4 14223400Diesel
4CLIO 11317600Diesel
5CITRONC2 14110700Essence
In [67]:
# vérification cohérence des fichiers
# même obs. dans le même ordre puisque 0 incohérence
sum(df.Modele .!= dfVarSupp.Modele)
0

Variable qualitative¶

In [68]:
# liste des modalités de carburant
moda_carb = sort(unique(dfVarSupp.carburant))
moda_carb
2-element Vector{String}:
 "Diesel"
 "Essence"
In [69]:
# graphique dans le plan "factoriel"
# en fonction de carburant
plt = Plots.scatter()

for carb in moda_carb
    id = (dfVarSupp.carburant .== carb)
    Plots.scatter!(plt,coord[1,id],coord[2,id],label=carb)
end

display(plt)
No description has been provided for this image

Variable quantitative¶

In [70]:
Plots.scatter(coord[1,:],coord[2,:],label="",
            marker_z=dfVarSupp.prix,color=:turbo,colorbar=true)
No description has been provided for this image

Individus supplémentaires¶

Chargement et préparation¶

In [71]:
# lecture des données -- individus supplémentaires
dfIndSupp = DFR.DataFrame(XLSX.readtable("./cars_autoencoder.xlsx","IND.ILLUS"))
dfIndSupp
4×7 DataFrame
RowModelepuissancecylindreevitesselongueurhauteurpoids
StringInt64Int64Int64Int64Int64Int64
1P407 13619972124681451415
2P307CC 18019972254351431490
3P1007 7513601653741611181
4P607 20427212304911451723
In [72]:
# standardisation avec les paramètres des individus actifs
XSupp = dfIndSupp[:,2:end]
ZSupp = (XSupp .- moyenne) ./ ecart_type
ZTSupp = Float32.(Matrix(ZSupp)')
6×4 Matrix{Float32}:
 -0.169475   0.432236  -1.00367    0.760441
 -0.107386  -0.107386  -0.764901   0.63993
  0.314799   0.715451  -1.13372    0.869548
  0.816167   0.113335  -1.18584    1.30602
 -0.403634  -0.689755   1.88533   -0.403634
  0.327308   0.600932  -0.526402   1.45099

Projection dans l'espace réduit¶

In [73]:
# coordonnées dans l'espace "factoriel"
# couche cachée de l'autoencodeur
coord_supp = encoder(ZTSupp)
coord_supp
2×4 Matrix{Float32}:
 0.208698  0.332305  -0.715043   0.513613
 0.165956  0.239842  -0.167976  -0.144697
In [74]:
# projection dans le plan
# individus actifs
Plots.scatter(coord[1,:],coord[2,:],label="",markersize=0,lims=(-1,+1),
            annotations=(coord[1,:],coord[2,:],Plots.text.(df.Modele,7,:silver)))

# individus supplémentaires
Plots.scatter!(coord_supp[1,:],coord_supp[2,:],label="",markersize=0,lims=(-1,+1),
            annotations=(coord_supp[1,:],coord_supp[2,:],Plots.text.(dfIndSupp.Modele,7,:blue)))
No description has been provided for this image