Environnement et packages¶

In [1]:
# activer l'environnement
using Pkg
Pkg.activate("env_julia_mljflux")
  Activating project at `c:\Users\ricco\Desktop\demo\env_julia_mljflux`
In [2]:
# liste des packages installés
Pkg.status()
Status `C:\Users\ricco\Desktop\demo\env_julia_mljflux\Project.toml`
  [324d7699] CategoricalArrays v1.1.1
  [a93c6f00] DataFrames v1.8.2
  [587475ba] Flux v0.16.10
  [add582a8] MLJ v0.23.2
  [094fc8d1] MLJFlux v0.6.7
  [23777cdb] MLJTransforms v0.1.6
  [3bd65402] Optimisers v0.4.7
  [91a5bcdd] Plots v1.41.6
  [fdbf4ff8] XLSX v0.11.10

Importation et prépration des données¶

Importation, description¶

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

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

# premières lignes
println(DFR.first(df,5))
5×22 DataFrame
 Row │ X01      X02      X03      X04      X05      X06      X07      X08      X09      X10      X11      X12      X13      X14      X15      X16      X17      X18      X19      X20      X21      onde   
     │ Float64  Float64  Float64  Float64  Float64  Float64  Float64  Float64  Float64  Float64  Float64  Float64  Float64  Float64  Float64  Float64  Float64  Float64  Float64  Float64  Float64  String 
─────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1 │    0.8      1.31     1.01     2.91     3.01     5.81     7.09     4.86     5.23     2.89     1.78     2.17     1.74    -0.95     0.35    -1.58     0.72    -1.93     0.83    -1.14     1.12  A
   2 │   -0.11    -0.66     1.06     1.67     2.38     1.9      1.39    -0.61     1.6      2.8      1.9      2.04     0.66     5.29     5.0      5.49     1.59     1.73     2.02     1.22     0.58  A
   3 │   -1.2      0.08     0.94     0.39     0.53     2.95     0.9      1.96     0.58     1.57     3.49     1.67     3.18     3.43     5.35     4.42     3.68     2.84     3.5      0.56     1.39  A
   4 │   -1.03    -0.21     0.71     2.15    -0.89     1.64     3.22     1.87     3.33     4.96     3.93     3.32     1.53     5.04     2.44     4.17     1.4      1.72    -1.0      2.37     0.04  C
   5 │    0.01     0.3      1.28     1.95     2.13     3.08     1.61     0.42    -0.39     2.19     3.83     3.72     4.92     3.02     4.26     3.12     4.15     0.89    -0.13     1.73    -1.03  A
In [4]:
# ou describe
DFR.describe(df)
22×7 DataFrame
Rowvariablemeanminmedianmaxnmissingeltype
SymbolUnion…AnyUnion…AnyInt64DataType
1X010.005144-3.340.013.940Float64
2X020.338746-3.250.343.880Float64
3X030.672438-4.20.664.720Float64
4X040.99161-3.840.945.750Float64
5X051.31089-3.481.126.50Float64
6X061.99731-2.761.867.620Float64
7X072.66181-3.322.58.760Float64
8X082.65923-3.522.727.840Float64
9X092.67209-3.382.817.90Float64
10X102.98867-1.793.07.630Float64
11X113.3366-1.483.179.060Float64
12X123.01361-1.693.07.40Float64
13X132.67891-2.612.837.50Float64
14X142.64863-2.822.77.750Float64
15X152.64767-2.562.498.720Float64
16X162.0005-2.991.827.860Float64
17X171.33503-3.561.26.740Float64
18X181.00062-4.080.946.20Float64
19X190.661482-3.50.625.280Float64
20X200.3573-3.570.354.650Float64
21X21-0.021378-3.88-0.034.010Float64
22ondeAC0String
In [5]:
# vérifier le schéma de la base pour MLJ
import MLJ
MLJ.schema(df)
┌───────┬────────────┬─────────┐
│ names │ scitypes   │ types   │
├───────┼────────────┼─────────┤
│ X01   │ Continuous │ Float64 │
│ X02   │ Continuous │ Float64 │
│ X03   │ Continuous │ Float64 │
│ X04   │ Continuous │ Float64 │
│ X05   │ Continuous │ Float64 │
│ X06   │ Continuous │ Float64 │
│ X07   │ Continuous │ Float64 │
│ X08   │ Continuous │ Float64 │
│ X09   │ Continuous │ Float64 │
│ X10   │ Continuous │ Float64 │
│ X11   │ Continuous │ Float64 │
│ X12   │ Continuous │ Float64 │
│ X13   │ Continuous │ Float64 │
│ X14   │ Continuous │ Float64 │
│ X15   │ Continuous │ Float64 │
│ X16   │ Continuous │ Float64 │
│ X17   │ Continuous │ Float64 │
│ X18   │ Continuous │ Float64 │
│ X19   │ Continuous │ Float64 │
│ X20   │ Continuous │ Float64 │
│ X21   │ Continuous │ Float64 │
│ onde  │ Textual    │ String  │
└───────┴────────────┴─────────┘

Structures et partition TRAIN/TEST¶

In [6]:
# isoler y et X dans des structures distinctes
y, X = MLJ.unpack(df,==(:onde))

# dimensions
println("Dim. de y = $(DFR.size(y))")
println("Dim. de X = $(DFR.size(X))")
Dim. de y = (5000,)
Dim. de X = (5000, 21)
In [7]:
# convertir y en variable catégorielle pour la classification supervisée
# équivalent du type factor sous R
# utilisation du package CategoricalArrays
import CategoricalArrays as CA
y = CA.categorical(y)

# vérification des modalités
println(eltype(y))
CategoricalArrays.CategoricalValue{String, UInt32}
In [8]:
# indices pour partition en train et test
# stratify est possible parce y est catégorielle maintenant (et non plus string)
# si on met 0.1 on a 501 en TRAIN, d'où le 0.0999 (argh !!!)
idTrain, idTest = MLJ.partition(1:DFR.nrow(X),0.0999,shuffle=true,rng=42,stratify=y)

# structures y et X pour train/test
# par indexation avec les indices
yTrain, yTest = y[idTrain], y[idTest]
XTrain, XTest = X[idTrain,:], X[idTest,:]

# afficher les dimensions pour vérifications
println("Dim. de y = $(DFR.size(yTrain)) et $(DFR.size(yTest))")
println("Dim. de X = $(DFR.size(XTrain)) et $(DFR.size(XTest))")
Dim. de y = (500,) et (4500,)
Dim. de X = (500, 21) et (4500, 21)

Standardisation¶

In [9]:
# classe de calcul pour la standardisation
Standardizer = @MLJ.load Standardizer pkg=MLJTransforms
┌ Info: For silent loading, specify `verbosity=0`. 
└ @ Main C:\Users\ricco\.julia\packages\MLJModels\AWkxi\src\loading.jl:159
import MLJTransforms ✔
MLJTransforms.Standardizer
In [10]:
# standardisation des données d'apprentissage
std = MLJ.machine(Standardizer(),XTrain)

# entraîner i.e. calculer moyennes et écarts-type
MLJ.fit!(std)

# transformer
ZTrain = MLJ.transform(std,XTrain)

# description
DFR.describe(ZTrain,:mean,:std)
┌ Info: Training machine(Standardizer(features = Symbol[], …), …).
└ @ MLJBase C:\Users\ricco\.julia\packages\MLJBase\DCbte\src\machines.jl:499
21×3 DataFrame
Rowvariablemeanstd
SymbolFloat64Float64
1X01-6.28664e-181.0
2X02-3.73035e-171.0
3X03-9.61453e-171.0
4X044.66294e-181.0
5X05-8.16847e-171.0
6X06-1.35003e-161.0
7X07-3.42837e-161.0
8X08-1.36335e-161.0
9X092.30038e-161.0
10X10-7.9714e-171.0
11X11-1.88072e-161.0
12X12-4.75307e-161.0
13X13-1.26826e-161.0
14X14-1.51934e-161.0
15X151.35003e-161.0
16X161.20792e-161.0
17X17-1.33227e-161.0
18X187.52731e-171.0
19X197.37188e-171.0
20X205.08482e-171.0
21X21-4.21885e-171.0

Modélisation¶

In [11]:
# chargement du modèle
# à partir de MLJFlux
import MLJFlux
NeuralNetworkClassifier = MLJ.@load NeuralNetworkClassifier pkg="MLJFlux"
import MLJFlux ✔
┌ Info: For silent loading, specify `verbosity=0`. 
└ @ Main C:\Users\ricco\.julia\packages\MLJModels\AWkxi\src\loading.jl:159
MLJFlux.NeuralNetworkClassifier

Instanciation et paramétrage¶

In [12]:
# instanciation avec paramétrage
# cf. https://fluxml.ai/MLJFlux.jl/dev/
# 2 neurones dans une seule couche cachée
# attention, spécifié comme un tuple "(2,)" --> "," obligatoire
import Flux
modele = NeuralNetworkClassifier(
    builder = MLJFlux.MLP(; hidden=(2,), σ = Flux.tanh),
    finaliser = Flux.softmax,
    optimiser = Flux.Adam(0.01),
    loss = Flux.crossentropy,
    batch_size = 20,
    epochs = 1000
    )
NeuralNetworkClassifier(
  builder = MLP(
        hidden = (2,), 
        σ = tanh), 
  finaliser = NNlib.softmax, 
  optimiser = Optimisers.Adam(eta=0.01, beta=(0.9, 0.999), epsilon=1.0e-8), 
  loss = Flux.Losses.crossentropy, 
  epochs = 1000, 
  batch_size = 20, 
  lambda = 0.0, 
  alpha = 0.0, 
  rng = Random.TaskLocalRNG(), 
  optimiser_changes_trigger_retraining = false, 
  acceleration = ComputationalResources.CPU1{Nothing}(nothing), 
  embedding_dims = Dict{Symbol, Real}())

Préparation à l'entraînement¶

In [13]:
# préparer au lancement de l'entraînement
mach = MLJ.machine(modele,ZTrain,yTrain)
untrained Machine; caches model-specific representations of data
  model: NeuralNetworkClassifier(builder = MLP(hidden = (2,), …), …)
  args: 
    1:	Source @485 ⏎ ScientificTypesBase.Table{AbstractVector{ScientificTypesBase.Continuous}}
    2:	Source @297 ⏎ AbstractVector{ScientificTypesBase.Multiclass{3}}

Entraînement sur les données TRAIN¶

In [14]:
# lancer l'entraînement du modèle
MLJ.fit!(mach)
┌ Info: Training machine(NeuralNetworkClassifier(builder = MLP(hidden = (2,), …), …), …).
└ @ MLJBase C:\Users\ricco\.julia\packages\MLJBase\DCbte\src\machines.jl:499
┌ Info: MLJFlux: converting input data to Float32
└ @ MLJFlux C:\Users\ricco\.julia\packages\MLJFlux\9HWUB\src\core.jl:294
Optimising neural net: 100%[=========================] Time: 0:00:30
trained Machine; caches model-specific representations of data
  model: NeuralNetworkClassifier(builder = MLP(hidden = (2,), …), …)
  args: 
    1:	Source @485 ⏎ ScientificTypesBase.Table{AbstractVector{ScientificTypesBase.Continuous}}
    2:	Source @297 ⏎ AbstractVector{ScientificTypesBase.Multiclass{3}}

Décroissance de la "loss" (report)¶

In [15]:
# rapport
rapport = MLJ.report(mach)
keys(rapport)
(:training_losses,)
In [16]:
# courbe de décroissance de la loss
import Plots
Plots.plot(1:1000,rapport.training_losses[1:1000],title="Decroissance de la loss",label="")
No description has been provided for this image

Structure du réseau et poids synaptiques (fitted_params)¶

In [17]:
# paramètres appris
fp = MLJ.fitted_params(mach)
keys(fp)
(:chain,)
In [18]:
# structure du réseau
fp.chain
Chain(
  Chain(
    Dense(21 => 2, tanh),               # 44 parameters
    Dense(2 => 3),                      # 9 parameters
  ),
  NNlib.softmax,
)                   # Total: 4 arrays, 53 parameters, 420 bytes.
In [19]:
# les couches
fp.chain.layers
(Chain(Dense(21 => 2, tanh), Dense(2 => 3)), NNlib.softmax)
In [20]:
# poids de la première couche
# entrée -> cachée
fp.chain.layers[1][1].weight
2×21 Matrix{Float32}:
  0.0903711  -0.0219708  -0.0287282  …   0.0947618   0.0362732   0.0726964
 -0.230598    0.0474596   0.208017      -0.126253   -0.212919   -0.117373
In [21]:
# biais (intercept)
fp.chain.layers[1][1].bias
2-element Vector{Float32}:
 -0.2711948
  1.6111445
In [22]:
# poids de la seconde couche
# cachée -> sortie
fp.chain.layers[1][2].weight
3×2 Matrix{Float32}:
  4.53067      2.41938
  0.0372478    4.04926
 -6.916      -10.2303
In [23]:
# biais (intercept)
fp.chain.layers[1][2].bias
3-element Vector{Float32}:
  1.137664
 -1.062453
  0.07133843

Evaluation en test¶

Application de la standardisation en test¶

In [24]:
# préparation des descripteurs de l'éch. test
ZTest = MLJ.transform(std,XTest)

# vérif. ?? pas du tout
# et c'est tout à fait normal
DFR.describe(ZTest,:mean,:std)
21×3 DataFrame
Rowvariablemeanstd
SymbolFloat64Float64
1X010.06318051.03486
2X020.00843610.990996
3X03-0.03384380.961525
4X04-0.07379750.987057
5X050.003891720.99079
6X06-0.05478320.98203
7X070.006312060.996754
8X08-0.02404731.01188
9X090.04708390.997109
10X10-0.02780260.941908
11X110.02437410.975445
12X120.04068390.992951
13X13-0.003726650.966481
14X140.02972020.996019
15X150.01326540.977217
16X160.02188920.981901
17X17-0.04416360.977805
18X18-0.07189031.00514
19X190.002829341.00999
20X20-0.01887880.977144
21X210.0224650.966729

Probas d'appartenance¶

In [25]:
# prédiction en test - probabilités (puisque softmax)
probs = MLJ.predict(mach,ZTest)

# vérif.
DFR.first(probs,5)
5-element CategoricalDistributions.UnivariateFiniteVector{ScientificTypesBase.Multiclass{3}, String, UInt32, Float32}:
 UnivariateFinite{ScientificTypesBase.Multiclass{3}}(A=>0.0199, B=>0.978, C=>0.00192)
 UnivariateFinite{ScientificTypesBase.Multiclass{3}}(A=>0.0954, B=>0.904, C=>0.000155)
 UnivariateFinite{ScientificTypesBase.Multiclass{3}}(A=>0.023, B=>0.975, C=>0.0016)
 UnivariateFinite{ScientificTypesBase.Multiclass{3}}(A=>0.584, B=>0.416, C=>1.16e-6)
 UnivariateFinite{ScientificTypesBase.Multiclass{3}}(A=>0.286, B=>0.714, C=>1.39e-5)

Classes prédites¶

In [26]:
# prédiction en test - classe d'appartenance
pred = MLJ.predict_mode(mach,ZTest)
DFR.first(pred,5)
5-element CategoricalArrays.CategoricalArray{String,1,UInt32}:
 "B"
 "B"
 "B"
 "A"
 "B"

Matrice de confusion et accuracy¶

In [27]:
# matrice de confusion
# attention transposée sur MLJ
MLJ.confusion_matrix(pred,yTest)
          ┌──────────────┐
          │ Ground Truth │
┌─────────┼────┬────┬────┤
│Predicted│ A  │ B  │ C  │
├─────────┼────┼────┼────┤
│    A    │1114│145 │ 74 │
├─────────┼────┼────┼────┤
│    B    │174 │1245│ 94 │
├─────────┼────┼────┼────┤
│    C    │203 │ 92 │1359│
└─────────┴────┴────┴────┘
In [28]:
# accuracy
MLJ.accuracy(pred,yTest)
0.8262222222222222

Représentation intermédiaire - Couche cachée¶

Calcul des coordonnées des individus¶

In [29]:
# sorties intermédiaires du réseau
# attention, transformer les données en Matrix
# et transposer parce que Flux le souhaite ainsi
coord = fp.chain[1].layers[1](Matrix(ZTrain)')

# dimension -> 2 couches (cachée et sortie)
size(coord)
┌ Warning: Layer with Float32 parameters got Float64 input.
│   The input will be converted, but any earlier layers may be very slow.
│   layer = Dense(21 => 2, tanh)
│   summary(x) = 21×500 adjoint(::Matrix{Float64}) with eltype Float64
└ @ Flux C:\Users\ricco\.julia\packages\Flux\hrg9M\src\layers\stateless.jl:60
(2, 500)
In [30]:
# qqs valeurs
# forcément entre -1 et +1 puisque
# activation = tanh
coord[:,1:10]
2×10 Matrix{Float32}:
 -0.7966     0.755963  -0.104458   0.938893  …  0.920955  -0.169109  0.204676
  0.999782  -0.962109  -0.682212  -0.988619     0.128225  -0.77803   0.247974

Représentation dans le plan (2 neurones dans la couche cachée)¶

In [31]:
# liste des modalités de la variable cible
modalites = sort(unique(yTrain))
modalites
3-element CategoricalArrays.CategoricalArray{String,1,UInt32}:
 "A"
 "B"
 "C"
In [32]:
# graphique vide
plt = Plots.scatter()

# enchaîner les modalités
for m in modalites
    id = (yTrain .== m)
    Plots.scatter!(plt,coord[1,id],coord[2,id],label = m,markersize=4)
end

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