Environnement et packages¶

In [40]:
# activer l'environnement
using Pkg
Pkg.activate("env_julia_flux")
  Activating project at `c:\Users\ricco\Desktop\demo\env_julia_flux`
In [41]:
# 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
  [da1fdf0e] FreqTables v1.0.0
  [91a5bcdd] Plots v1.41.6
  [fdbf4ff8] XLSX v0.11.10

Importation et préparation des données TRAIN¶

Importation¶

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

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

# premières lignes
DFR.describe(dfTrain)
7×7 DataFrame
Rowvariablemeanminmedianmaxnmissingeltype
SymbolUnion…AnyUnion…AnyInt64DataType
1ucellsize3.040111.0100Int64
2ucellshape3.1854612.0100Int64
3mgadhesion2.6741911.0100Int64
4sepics3.1152912.0100Int64
5normnucl2.799511.0100Int64
6mitoses1.4987511.0100Int64
7classebegninmalignant0String

Standardisation des descripteurs¶

In [43]:
# matrice des descripteurs
XTrain = Float32.(dfTrain[:,1:(end-1)])
DFR.describe(XTrain)
6×7 DataFrame
Rowvariablemeanminmedianmaxnmissingeltype
SymbolFloat32Float32Float64Float32Int64DataType
1ucellsize3.04011.01.010.00Float32
2ucellshape3.185461.02.010.00Float32
3mgadhesion2.674191.01.010.00Float32
4sepics3.115291.02.010.00Float32
5normnucl2.79951.01.010.00Float32
6mitoses1.498751.01.010.00Float32
In [44]:
import Statistics

# moyennes
moy = Statistics.mean(Matrix(XTrain),dims=1)
moy
1×6 Matrix{Float32}:
 3.0401  3.18546  2.67419  3.11529  2.7995  1.49875
In [45]:
# écarts-type
sigma = Statistics.std(Matrix(XTrain),dims=1)
sigma
1×6 Matrix{Float32}:
 3.00392  2.96643  2.80149  2.17264  3.05558  1.58808
In [46]:
# centrage et réduction
ZTrain = (XTrain .- moy) ./ sigma
DFR.describe(ZTrain,:mean,:std,:min,:max)
6×5 DataFrame
Rowvariablemeanstdminmax
SymbolFloat32Float32Float32Float32
1ucellsize-1.51626e-81.0-0.6791472.31694
2ucellshape-2.00176e-81.0-0.7367332.29722
3mgadhesion-4.57118e-81.0-0.5976052.61497
4sepics-1.58348e-81.0-0.9736053.16883
5normnucl3.88028e-81.0-0.5889222.35651
6mitoses-5.82602e-91.0-0.3140575.35318
In [47]:
# mettre sous la forme d'une matrice
# et il faut transposer !!!
ZTrain = Matrix(ZTrain)'

# vérifier
size(ZTrain)
(6, 399)

Codage de la variable cible¶

In [48]:
# vérification -- fréquence absolue des classes
# utilisation du package FreqTables
import FreqTables as FT
FT.freqtable(dfTrain.classe)
2-element Named Vector{Int64}
Dim1      │ 
──────────┼────
begnin    │ 267
malignant │ 132
In [49]:
# variable cible -- recodage en 0/1
yTrain = Float32.(dfTrain.classe .== "malignant")

# et somme sur la variable recodée en 0/1
println("\nSomme")
println(Statistics.sum(yTrain))
Somme
132.0
In [50]:
# mettre yTrain sous la forme d'une matrice
# avec la bonne conformation
yTrain = reshape(yTrain,1,:)

# dimensions
size(yTrain)
(1, 399)

Perceptron simple avec Flux¶

Structure du réseau et paramétrage¶

In [51]:
# définir un perceptron simple
import Flux

# Chain n'est pas nécessaire ici
# mais pour être cohérent avec la suite (cf. PMC)
model_ps = Flux.Chain(
    Flux.Dense(6 => 1, Flux.sigmoid)
)

# définir la fonction de perte
loss_ps(X,y) = Flux.mse(model_ps(X),y)

# définir l'algo d'optimisation
# une descente de gradient améliorée
opt_ps = Flux.setup(Flux.Adam(0.01), model_ps)
(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], Float32[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], Float32[0.0], (0.9, 0.999))), σ = ()),),)
In [52]:
# vérification, structure du réseau
model_ps.layers
(Dense(6 => 1, σ),)
In [53]:
# coefficients initiaux (couche entrée -> couche de sortie)
model_ps[1].weight
1×6 Matrix{Float32}:
 0.490448  0.801712  0.122542  0.448223  -0.501278  -0.572844
In [54]:
# intercept initial (bias)
model_ps[1].bias
1-element Vector{Float32}:
 0.0

Entraînement du modèle¶

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

# nombre d'itérations
n_epochs = 1000

# boucle d'entraînement
for i in 1:n_epochs
    # calcul du gradient
    grads = Flux.gradient(model_ps) do m
        Flux.mse(m(ZTrain), yTrain)
    end
    # mise à jour des poids synaptiques (coefficients)
    Flux.update!(opt_ps,model_ps,grads[1])
    # récupération de la loss courante
    cur_loss = loss_ps(ZTrain,yTrain)
    # 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 = 0.13366098701953888
Loss fin = 0.03610197827219963

Inspection des résultats¶

In [56]:
# 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
In [57]:
# affichage des poids après entraînement
println("Coefficients :")
println(model_ps[1].weight)

#et du biais
println("\nIntercept :")
println(model_ps[1].bias)
Coefficients :
Float32[1.6772531 2.439507 0.18046932 1.5186521 0.89737046 0.668222]

Intercept :
Float32[0.13474798]
In [58]:
# bien sûr on peut associer les coefficients aux variables
# les valeurs absolues => intensité de la relation avec la cible
# signes des coefs => sens de la relation
DFR.DataFrame(var=names(XTrain),coef=vec(model_ps[1].weight))
6×2 DataFrame
Rowvarcoef
StringFloat32
1ucellsize1.67725
2ucellshape2.43951
3mgadhesion0.180469
4sepics1.51865
5normnucl0.89737
6mitoses0.668222

Chargement et préparation des données TEST¶

In [59]:
# lecture de l'échantillon test
dfTest = DFR.DataFrame(XLSX.readtable("./breast_test.xlsx"))

# premières lignes
DFR.describe(dfTest)
7×7 DataFrame
Rowvariablemeanminmedianmaxnmissingeltype
SymbolUnion…AnyUnion…AnyInt64DataType
1ucellsize3.2611.0100Int64
2ucellshape3.2366711.0100Int64
3mgadhesion2.9833311.0100Int64
4sepics3.3512.0100Int64
5normnucl2.9566711.0100Int64
6mitoses1.7111.0100Int64
7classebegninmalignant0String
In [60]:
# XTest
XTest = Float32.(dfTest[:,1:(end-1)])
names(XTest)
6-element Vector{String}:
 "ucellsize"
 "ucellshape"
 "mgadhesion"
 "sepics"
 "normnucl"
 "mitoses"
In [61]:
# standardisation avec les moyennes et écarts-type de train
ZTest = (XTest .- moy) ./ sigma

#puis sous la forme de matrice transposée
ZTest = Matrix(ZTest)'

# dimesions
size(ZTest)
(6, 300)

Prédiction sur l'échantillon test¶

In [62]:
# prédiction en test
pred_ps = model_ps(ZTest)
first(pred_ps,5)
5-element Vector{Float32}:
 0.029839434
 0.9988803
 0.011804679
 0.045362037
 0.011804679
In [63]:
# traduire en prédiction
y_ps = ifelse.(vec(pred_ps) .>= 0.5, "malignant", "begnin")
first(y_ps,5)
5-element Vector{String}:
 "begnin"
 "malignant"
 "begnin"
 "begnin"
 "begnin"

Performances prédictives¶

In [64]:
# accuracy
Statistics.mean(dfTest.classe .== y_ps)
0.9666666666666667
In [65]:
# ou encore la matrice de confusion avec FreqTables
FT.freqtable(dfTest.classe,y_ps)
2×2 Named Matrix{Int64}
Dim1 ╲ Dim2 │    begnin  malignant
────────────┼─────────────────────
begnin      │       184          7
malignant   │         3        106

Preceptron multicouche¶

Structure du réseau¶

In [66]:
# Chain est nécessaire ici
# pour enchaîner les deux couches
model_pmc = Flux.Chain(
    Flux.Dense(6 => 2, Flux.sigmoid),
    Flux.Dense(2 => 1, Flux.sigmoid)
)

# définir la fonction de perte
loss_pmc(X,y) = Flux.mse(model_pmc(X),y)

# définir l'algo d'optimisation
# une descente de gradient améliorée
opt_pmc = Flux.setup(Flux.Adam(0.01), model_pmc)
(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))), σ = ()), (weight = 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))), bias = Leaf(Adam(eta=0.01, beta=(0.9, 0.999), epsilon=1.0e-8), (Float32[0.0], Float32[0.0], (0.9, 0.999))), σ = ())),)
In [67]:
# inspection de la structure
model_pmc.layers
(Dense(6 => 2, σ), Dense(2 => 1, σ))
In [68]:
# poids (coef) : couche d'entrée -> couche cachée
model_pmc[1].weight
2×6 Matrix{Float32}:
 0.0172516  -0.779909   0.576837  0.396249  -0.0472927  -0.746339
 0.505588   -0.19096   -0.734207  0.550847  -0.119603   -0.538707
In [69]:
# poids : couche cachée -> couche de sortie
model_pmc[2].weight
1×2 Matrix{Float32}:
 0.109559  1.40793

Entraînement¶

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

# nombre d'itérations
n_epochs = 1000

# boucle d'entraînement
for i in 1:n_epochs
    # calcul du gradient
    grads = Flux.gradient(model_pmc) do m
        Flux.mse(m(ZTrain), yTrain)
    end
    # mise à jour des poids synaptiques (coefficients)
    Flux.update!(opt_pmc,model_pmc,grads[1])
    # récupération de la loss courante
    cur_loss = loss_pmc(ZTrain,yTrain)
    # stockage
    push!(loss_history_pmc,cur_loss)
end

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

# et de la dernière
println("Loss fin = $(loss_history_pmc[end])")
Loss depart = 0.3525426983833313
Loss fin = 0.034496910870075226

Evaluation¶

In [71]:
# prédiction en test
# probas
pred_pmc = model_pmc(ZTest)

# converties en classes prédites
y_pmc = ifelse.(vec(pred_pmc) .>= 0.5, "malignant", "begnin")

# fréquences des classes prédites
FT.freqtable(y_pmc)
2-element Named Vector{Int64}
Dim1      │ 
──────────┼────
begnin    │ 187
malignant │ 113
In [72]:
# accuracy
Statistics.mean(dfTest.classe .== y_pmc)
0.9666666666666667
In [73]:
# matrice de confusion
cm_pmc = FT.freqtable(dfTest.classe, y_pmc)
cm_pmc
2×2 Named Matrix{Int64}
Dim1 ╲ Dim2 │    begnin  malignant
────────────┼─────────────────────
begnin      │       184          7
malignant   │         3        106

Inspection de la couche intermédiaire¶

In [74]:
# récupération de la structure intermédiaire (couche cachée)
repr_cc = Flux.Chain(model_pmc[1])

# vérification
repr_cc.layers
(Dense(6 => 2, σ),)
In [75]:
# coordonnées des individus TEST
coord_cc = repr_cc(ZTest)
coord_cc
2×300 Matrix{Float32}:
 0.945858   0.000248021  0.99344    …  0.000963716  0.890286   0.99344
 0.0386392  0.9986       0.0270332     0.991846     0.0722556  0.0270332
In [76]:
# sous forme transposée
# il s'agit d'une sorte de représentation "factorielle"
fact = Matrix(coord_cc)'
size(fact)
(300, 2)
In [77]:
# représentation des individus dans le plan
# avec les classes d'appartenance
les_m = (dfTest.classe .== "malignant")
les_b = (dfTest.classe .== "begnin")

# graphique
Plots.scatter(fact[les_m,1],fact[les_m,2],label="Malignant",markersize=4)
Plots.scatter!(fact[les_b,1],fact[les_b,2],label="Begnin",color=:green,markersize=4)
No description has been provided for this image
In [78]:
# rappel de la matrice de confusion
cm_pmc
2×2 Named Matrix{Int64}
Dim1 ╲ Dim2 │    begnin  malignant
────────────┼─────────────────────
begnin      │       184          7
malignant   │         3        106