# 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

Row | variable | mean | min | median | max | nmissing | eltype
--- | --- | --- | --- | --- | --- | --- | ---
| Symbol | Union… | Any | Union… | Any | Int64 | DataType
1 | X01 | 0.005144 | -3.34 | 0.01 | 3.94 | 0 | Float64
2 | X02 | 0.338746 | -3.25 | 0.34 | 3.88 | 0 | Float64
3 | X03 | 0.672438 | -4.2 | 0.66 | 4.72 | 0 | Float64
4 | X04 | 0.99161 | -3.84 | 0.94 | 5.75 | 0 | Float64
5 | X05 | 1.31089 | -3.48 | 1.12 | 6.5 | 0 | Float64
6 | X06 | 1.99731 | -2.76 | 1.86 | 7.62 | 0 | Float64
7 | X07 | 2.66181 | -3.32 | 2.5 | 8.76 | 0 | Float64
8 | X08 | 2.65923 | -3.52 | 2.72 | 7.84 | 0 | Float64
9 | X09 | 2.67209 | -3.38 | 2.81 | 7.9 | 0 | Float64
10 | X10 | 2.98867 | -1.79 | 3.0 | 7.63 | 0 | Float64
11 | X11 | 3.3366 | -1.48 | 3.17 | 9.06 | 0 | Float64
12 | X12 | 3.01361 | -1.69 | 3.0 | 7.4 | 0 | Float64
13 | X13 | 2.67891 | -2.61 | 2.83 | 7.5 | 0 | Float64
14 | X14 | 2.64863 | -2.82 | 2.7 | 7.75 | 0 | Float64
15 | X15 | 2.64767 | -2.56 | 2.49 | 8.72 | 0 | Float64
16 | X16 | 2.0005 | -2.99 | 1.82 | 7.86 | 0 | Float64
17 | X17 | 1.33503 | -3.56 | 1.2 | 6.74 | 0 | Float64
18 | X18 | 1.00062 | -4.08 | 0.94 | 6.2 | 0 | Float64
19 | X19 | 0.661482 | -3.5 | 0.62 | 5.28 | 0 | Float64
20 | X20 | 0.3573 | -3.57 | 0.35 | 4.65 | 0 | Float64
21 | X21 | -0.021378 | -3.88 | -0.03 | 4.01 | 0 | Float64
22 | onde |  | A |  | C | 0 | String

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

Row | variable | mean | std
--- | --- | --- | ---
| Symbol | Float64 | Float64
1 | X01 | -6.28664e-18 | 1.0
2 | X02 | -3.73035e-17 | 1.0
3 | X03 | -9.61453e-17 | 1.0
4 | X04 | 4.66294e-18 | 1.0
5 | X05 | -8.16847e-17 | 1.0
6 | X06 | -1.35003e-16 | 1.0
7 | X07 | -3.42837e-16 | 1.0
8 | X08 | -1.36335e-16 | 1.0
9 | X09 | 2.30038e-16 | 1.0
10 | X10 | -7.9714e-17 | 1.0
11 | X11 | -1.88072e-16 | 1.0
12 | X12 | -4.75307e-16 | 1.0
13 | X13 | -1.26826e-16 | 1.0
14 | X14 | -1.51934e-16 | 1.0
15 | X15 | 1.35003e-16 | 1.0
16 | X16 | 1.20792e-16 | 1.0
17 | X17 | -1.33227e-16 | 1.0
18 | X18 | 7.52731e-17 | 1.0
19 | X19 | 7.37188e-17 | 1.0
20 | X20 | 5.08482e-17 | 1.0
21 | X21 | -4.21885e-17 | 1.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="")

### 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

Row | variable | mean | std
--- | --- | --- | ---
| Symbol | Float64 | Float64
1 | X01 | 0.0631805 | 1.03486
2 | X02 | 0.0084361 | 0.990996
3 | X03 | -0.0338438 | 0.961525
4 | X04 | -0.0737975 | 0.987057
5 | X05 | 0.00389172 | 0.99079
6 | X06 | -0.0547832 | 0.98203
7 | X07 | 0.00631206 | 0.996754
8 | X08 | -0.0240473 | 1.01188
9 | X09 | 0.0470839 | 0.997109
10 | X10 | -0.0278026 | 0.941908
11 | X11 | 0.0243741 | 0.975445
12 | X12 | 0.0406839 | 0.992951
13 | X13 | -0.00372665 | 0.966481
14 | X14 | 0.0297202 | 0.996019
15 | X15 | 0.0132654 | 0.977217
16 | X16 | 0.0218892 | 0.981901
17 | X17 | -0.0441636 | 0.977805
18 | X18 | -0.0718903 | 1.00514
19 | X19 | 0.00282934 | 1.00999
20 | X20 | -0.0188788 | 0.977144
21 | X21 | 0.022465 | 0.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)