-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy path05-ClassificationsSupervisees.qmd
1016 lines (787 loc) · 46.4 KB
/
05-ClassificationsSupervisees.qmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
---
jupyter: python3
from: markdown+emoji
execute:
echo: true
eval: true
message: false
warning: false
---
```{python}
#| echo: false
#| output: false
import matplotlib.pyplot as plt
plt.rcParams['axes.titlesize'] = 10
plt.rcParams['axes.labelsize'] = 10
plt.rcParams['xtick.labelsize'] = 10
plt.rcParams['ytick.labelsize'] = 10
plt.rcParams['legend.fontsize'] = 10
plt.rcParams["image.aspect"]= 'equal'
plt.rcParams['figure.dpi'] = 100
import warnings
warnings.filterwarnings('ignore')
```
# Classifications d'images supervisées {#sec-chap05}
## Préambule
Assurez-vous de lire ce préambule avant d'exécuter le reste du notebook.
### Objectifs
Dans ce chapitre, nous ferons une introduction générale à l'apprentissage automatique et abordons quelques techniques fondamentales. La librairie centrale utilisée dans ce chapitre sera [`sickit-learn`](https://scikit-learn.org/). Ce chapitre est aussi disponible sous la forme d'un notebook Python sur Google Colab:
[](https://colab.research.google.com/github/sfoucher/TraitementImagesPythonVol1/blob/main/notebooks/05-ClassificationsSupervisees.ipynb)
::::: bloc_objectif
:::: bloc_objectif-header
::: bloc_objectif-icon
:::
**Objectifs d'apprentissage visés dans ce chapitre**
::::
À la fin de ce chapitre, vous devriez être en mesure de :
- comprendre les principes de l’apprentissage automatique supervisé;
- mettre en place un pipeline d’entraînement;
- savoir comment évaluer les résultats d'un classificateur;
- visualiser les frontières de décision;
- mettre en place des techniques de classifications comme K-NN et les arbres de décision;
:::::
### Librairies
Les librairies utilisées dans ce chapitre sont les suivantes :
- [SciPy](https://scipy.org/)
- [NumPy](https://numpy.org/)
- [opencv-python · PyPI](https://pypi.org/project/opencv-python/)
- [scikit-image](https://scikit-image.org/)
- [Rasterio](https://rasterio.readthedocs.io/en/stable/)
- [xarray](https://docs.xarray.dev/en/stable/)
- [rioxarray](https://corteva.github.io/rioxarray/stable/index.html)
- [geopandas](https://geopandas.org)
- [scikit-learn](https://scikit-learn.org/)
Dans l'environnement Google Colab, seul `rioxarray` et `xrscipy` sont installés:
```{python}
#| eval: false
%%capture
!pip install -qU matplotlib rioxarray xrscipy supertree
```
Vérifiez les importations nécessaires en premier:
```{python}
import numpy as np
import rioxarray as rxr
from scipy import signal
import xarray as xr
import rasterio
import xrscipy
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
import geopandas
from shapely.geometry import Point
import pandas as pd
from numba import jit
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.metrics import confusion_matrix, classification_report, ConfusionMatrixDisplay
from sklearn.preprocessing import StandardScaler
from sklearn.inspection import DecisionBoundaryDisplay
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis, QuadraticDiscriminantAnalysis
from sklearn.datasets import make_blobs, make_classification, make_gaussian_quantiles
```
### Images utilisées
Nous utilisons les images suivantes dans ce chapitre:
```{python}
#| eval: false
%%capture
import gdown
gdown.download('https://drive.google.com/uc?export=download&confirm=pbef&id=1a6Ypg0g1Oy4AJt9XWKWfnR12NW1XhNg_', output= 'RGBNIR_of_S2A.tif')
gdown.download('https://drive.google.com/uc?export=download&confirm=pbef&id=1a4PQ68Ru8zBphbQ22j0sgJ4D2quw-Wo6', output= 'landsat7.tif')
gdown.download('https://drive.google.com/uc?export=download&confirm=pbef&id=1_zwCLN-x7XJcNHJCH6Z8upEdUXtVtvs1', output= 'berkeley.jpg')
gdown.download('https://drive.google.com/uc?export=download&confirm=pbef&id=1dM6IVqjba6GHwTLmI7CpX8GP2z5txUq6', output= 'SAR.tif')
gdown.download('https://drive.google.com/uc?export=download&confirm=pbef&id=1aAq7crc_LoaLC3kG3HkQ6Fv5JfG0mswg', output= 'carte.tif')
```
Vérifiez que vous êtes capable de les lire :
```{python}
#| output: false
with rxr.open_rasterio('berkeley.jpg', mask_and_scale= True) as img_rgb:
print(img_rgb)
with rxr.open_rasterio('RGBNIR_of_S2A.tif', mask_and_scale= True) as img_rgbnir:
print(img_rgbnir)
with rxr.open_rasterio('SAR.tif', mask_and_scale= True) as img_SAR:
print(img_SAR)
with rxr.open_rasterio('carte.tif', mask_and_scale= True) as img_carte:
print(img_carte)
```
## Principes généraux
Une classification supervisée ou dirigée consiste à attribuer une étiquette (une classe) de manière automatique à chaque point d'un jeu de données. Cette classification peut se faire à l'aide d'une cascade de règles pré-établies (arbre de décision) ou à l'aide de techniques d'apprentissage automatique (*machine learning*). L'utilisation de règles pré-établies atteint vite une limite car ces règles doivent être fournies manuellement par un expert. Ainsi, l'avantage de l'apprentissage automatique est que les règles de décision sont dérivées automatiquement du jeu de données via une phase dite d’entraînement. On parle souvent de solutions générées par les données (*Data Driven Solutions*). Cet ensemble de règles est souvent appelé **modèle**. On visualise souvent ces règles sous la forme de *frontières de décisions* dans l'espace des données. Cependant, un des défis majeurs de ce type de technique est d'être capable de produire des règles qui soient généralisables au-delà du jeu d’entraînement.
Les classifications supervisées ou dirigées présupposent donc que nous avons à disposition **un jeu d’entraînement** déjà étiqueté. Celui-ci va nous permettre de construire un modèle. Afin que ce modèle soit représentatif et robuste, il nous faut assez de données d’entraînement. Les algorithmes d'apprentissage automatique sont très nombreux et plus ou moins complexes pouvant produire des frontières de décision très complexes et non linéaires.
### Comportement d'un modèle
Cet exemple tiré de [`sickit-learn`](https://scikit-learn.org/stable/auto_examples/model_selection/plot_underfitting_overfitting.html#sphx-glr-auto-examples-model-selection-plot-underfitting-overfitting-py) illustre les problèmes d'ajustement insuffisant ou **sous-apprentissage** (*underfitting*) et d'ajustement excessif ou **sur-apprentissage** (*overfitting*) et montre comment nous pouvons utiliser la régression linéaire avec un modèle polynomiale pour approximer des fonctions non linéaires. La @fig-overfitting montre la fonction que nous voulons approximer, qui est une partie de la fonction cosinus (couleur orange). En outre, les échantillons de la fonction réelle et les approximations de différents modèles sont affichés en bleu. Les modèles ont des caractéristiques polynomiales de différents degrés. Nous pouvons constater qu'une fonction linéaire (polynôme de degré 1) n'est pas suffisante pour s'adapter aux échantillons d'apprentissage. C'est ce qu'on appelle un sous-ajustement (*underfitting*) qui produit un biais systématique quels que soient les points d’entraînement. Un polynôme de degré 4 se rapproche presque parfaitement de la fonction réelle. Cependant, pour des degrés plus élevés, le modèle s'adaptera trop aux données d'apprentissage, c'est-à-dire qu'il apprendra le bruit des données d'apprentissage. Nous évaluons quantitativement le sur-apprentissage et le sous-apprentissage à l'aide de la validation croisée. Nous calculons l'erreur quadratique moyenne (EQM) sur l'ensemble de validation. Plus elle est élevée, moins le modèle est susceptible de se généraliser correctement à partir des données d'apprentissage.
```{python}
#| echo: false
#| label: fig-overfitting
#| fig-cap: "Exemples de sur et sous-apprentissage."
import matplotlib.pyplot as plt
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import cross_val_score, cross_validate
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
def true_fun(X):
return np.cos(1.5 * np.pi * X+np.pi/2)
np.random.seed(0)
noise_level= 0.1
n_samples = 30
degrees = [1,4, 15]
#degrees= range(1,16)
X = np.sort(np.random.rand(n_samples))
y = true_fun(X) + np.random.randn(n_samples) * noise_level
X_test = np.sort(np.random.rand(10))
y_test = true_fun(X_test) + np.random.randn(int(10)) * noise_level
plt.figure(figsize=(14, 5))
results= []
for i in range(len(degrees)):
ax = plt.subplot(1, len(degrees), i + 1)
plt.setp(ax, xticks=(), yticks=())
polynomial_features = PolynomialFeatures(degree=degrees[i], include_bias=False)
linear_regression = LinearRegression()
pipeline = Pipeline(
[
("polynomial_features", polynomial_features),
("linear_regression", linear_regression),
]
)
pipeline.fit(X[:, np.newaxis], y)
# Evaluate the models using crossvalidation
scores = cross_validate(
pipeline, X[:, np.newaxis], y, scoring="neg_mean_squared_error", cv=10, return_train_score=True
)
X_true = np.linspace(0, 1, 100)
plt.plot(X_true, pipeline.predict(X_true[:, np.newaxis]), label="Modèle")
plt.plot(X_true, true_fun(X_true), label="Vraie fonction")
plt.scatter(X, y, edgecolor="b", s=20, label="Entr.")
plt.scatter(X_test, y_test, edgecolor="g", s=20, label="Test")
plt.xlabel("x")
plt.ylabel("y")
plt.xlim((0, 1))
plt.ylim((-2, 2))
plt.legend(loc="best")
plt.title(
"Degré {}\nErreur = {:.1e}(+/- {:.1e})".format(
degrees[i], -scores['test_score'].mean(), scores['test_score'].std()
), fontsize='small'
)
results.append([degrees[i], -scores['train_score'].mean(), -scores['test_score'].mean(),scores['train_score'].std(),scores['test_score'].std()])
plt.show()
```
On constate aussi que sans les échantillons de validation, nous serions incapables de déterminer la situation de sur-apprentissage, l'erreur sur les points d’entraînement seuls étant excellente pour un degré 15.
### Pipeline
La construction d'un modèle implique généralement toujours les mêmes étapes illustrées sur la [@fig-pipeline]:
1. La préparation des données implique parfois un pré-traitement afin de normaliser les données.
2. Partage des données en trois groupes: entraînement, validation et test.
3. L'apprentissage du modèle sur l'ensemble d'entraînement. Cet apprentissage nécessite de déterminer les valeurs des hyper-paramètres du modèle par l'usager.
4. La validation du modèle sur l'ensemble de validation. Cette étape vise à vérifier que les hyper-paramètres du modèle sont adéquats.
5. Enfin le test du modèle sur un ensemble de données indépendant.
```{mermaid}
%%| echo: false
%%| label: fig-pipeline
%%| fig-cap: "Étapes standards dans un entraînement."
flowchart TD
A[fa:fa-database Données] --> B(fa:fa-gear Prétraitement)
B --> C(fa:fa-folder-tree Partage des données) -.-> D(fa:fa-gears Entraînement)
H[[Hyper-paramètres]] --> D
D --> |Modèle| E>Validation]
E --> |Modèle| G>Test]
C -.-> E
C -.-> G
```
### Construction d'un ensemble d’entraînement {#sec-05.02.02}
Les données d’entraînement permettent de construire un modèle. Elles peuvent prendre des formes très variées mais on peut voir cela sous la forme d'un tableau $N \times D$:
1. La taille $N$ du jeu de données.
2. Chaque entrée définit un échantillon ou un point dans un espace à plusieurs dimensions.
3. Chaque échantillon est décrit par $D$ dimensions ou caractéristiques (*features*).
Une façon simple de construire un ensemble d’entraînement est d'échantillonner un produit existant. Nous utilisons une carte d'occupation des sols qui contient 12 classes différentes.
```{python}
couleurs_classes= {'NoData': 'black', 'Commercial': 'yellow', 'Nuages': 'lightgrey',
'Foret': 'darkgreen', 'Faible_végétation': 'green', 'Sol_nu': 'saddlebrown',
'Roche': 'dimgray', 'Route': 'red', 'Urbain': 'orange', 'Eau': 'blue', 'Tourbe': 'salmon', 'Végétation éparse': 'darkgoldenrod', 'Roche avec végétation': 'darkseagreen'}
nom_classes= [*couleurs_classes.keys()]
couleurs_classes= [*couleurs_classes.values()]
```
On peut visualiser la carte de la façon suivante :
```{python}
import matplotlib.pyplot as plt
import rioxarray as rxr
cmap_classes = ListedColormap(couleurs_classes)
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(8, 6))
img_carte.squeeze().plot.imshow(cmap=cmap_classes, vmin=0, vmax=12)
ax.set_title("Carte d'occupation des sols", fontsize="small")
```
On peut facilement calculer la fréquence d’occurrences des 12 classes dans l'image à l'aide de `numpy`:
```{python}
img_carte= img_carte.squeeze() # nécessaire pour ignorer la dimension du canal
compte_classe = np.unique(img_carte.data, return_counts=True)
print(compte_classe)
```
La fréquence d'apparition de chaque classe varie grandement, on parle alors d'un **ensemble déséquilibré**. Ceci est très commun dans la plupart des ensembles d’entraînement, puisque les classes ont très rarement la même fréquence. Par exemple, à la lecture du graphique en barres verticales, on constate que la classe forêt est très présentes contrairement à plusieurs autres classes (notamment, tourbe, végétation éparse, roche, sol nu et nuages).
```{python}
valeurs, comptes = compte_classe
# Create the histogram
plt.figure(figsize=(5, 3))
plt.bar(valeurs, comptes/comptes.sum()*100)
plt.xlabel("Classes")
plt.ylabel("%")
plt.title("Fréquences des classes", fontsize="small")
plt.xticks(range(len(nom_classes)), nom_classes, rotation=45, ha='right')
plt.show()
```
On peut échantillonner aléatoirement 100 points pour chaque classe:
```{python}
img_carte= img_carte.squeeze()
class_counts = np.unique(img_carte.data, return_counts=True)
# Liste vide des points échantillonnées
sampled_points = []
class_labels= [] # contient les étiquettes des classes
for class_label in range(1,13): # pour chacune des 12 classes
# On cherche tous les pixels pour cette étiquette
class_pixels = np.argwhere(img_carte.data == class_label)
# On se limite à 100 pixels par classe
n_samples = min(100, len(class_pixels))
# On les choisit les positions aléatoirement
np.random.seed(0) # ceci permet de répliquer le tirage aléatoire
sampled_indices = np.random.choice(len(class_pixels), n_samples, replace=False)
# On prends les positions en lignes, colonnes
sampled_pixels = class_pixels[sampled_indices]
# On ajoute les points à la liste
sampled_points.extend(sampled_pixels)
class_labels.extend(np.array([class_label]*n_samples)[:,np.newaxis])
# Conversion en NumPy array
sampled_points = np.array(sampled_points)
class_labels = np.array(class_labels)
# On peut naviguer les points à l'aide de la géoréférence
transformer = rasterio.transform.AffineTransformer(img_carte.rio.transform())
transform_sampled_points= transformer.xy(sampled_points[:,0], sampled_points[:,1])
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(8, 6))
img_carte.squeeze().plot.imshow(cmap=cmap_classes, vmin=0, vmax=12)
ax.scatter(transform_sampled_points[0], transform_sampled_points[1], c='w', s=1) # Plot sampled points
ax.set_title("Carte d'occupation des sols avec les points échantillonnés", fontsize="small")
plt.show()
```
Une fois les points sélectionnés, on ajoute les valeurs des bandes provenant d'une image satellite. Pour cela, on utilise la méthode `sample()` de `rasterio`. Éventuellement, la librairie [`geopandas`](https://geopandas.org) permet de gérer les données d’entraînement sous la forme d'un tableau transportant aussi l'information de géoréférence. Afin de pouvoir classifier ces points, on ajoute les valeurs radiométriques provenant de l'image Sentinel-2 à 4 bandes `RGBNIR_of_S2A.tif`. Ces valeurs seront stockées dans la colonne `value` sous la forme d'un vecteur en format `string` :
```{python}
points = [Point(xy) for xy in zip(transform_sampled_points[0], transform_sampled_points[1])]
gdf = geopandas.GeoDataFrame(range(1,len(points)+1), geometry=points, crs=img_carte.rio.crs)
coord_list = [(x, y) for x, y in zip(gdf["geometry"].x, gdf["geometry"].y)]
with rasterio.open('RGBNIR_of_S2A.tif') as src:
gdf["value"] = [x for x in src.sample(coord_list)]
gdf['class']= class_labels
gdf.to_csv('sampling_points.csv') # sauvegarde sous forme d'un format csv
gdf.head()
```
## Analyse préliminaire des données
Une bonne pratique avant d'appliquer une technique d'apprentissage automatique est de regarder les caractéristiques de vos données:
1. Le nombre de dimensions (*features*).
2. Certaines dimensions sont informatives (discriminantes) et d'autres ne le sont pas.
3. Le nombre classes.
4. Le nombre de modes (*clusters*) par classes.
5. Le nombre d'échantillons par classe.
6. La forme des groupes.
7. La séparabilité des classes ou des groupes.
Une manière d'évaluer la séparabilité de vos classes est d'appliquer des modèles Gaussiens sur chacune des classes. Le modèle Gaussien multivarié suppose que les données sont distribuées comme un nuage de points symétrique et unimodale. La distribution d'un point $x$ appartenant à la classe $i$ est la suivante:
$$
P(x | Classe=i) = \frac{1}{(2\pi)^{D/2} |\Sigma_i|^{1/2}}\exp\left(-\frac{1}{2} (x-m_i)^t \Sigma_k^{-1} (x-m_i)\right)
$$
La méthode [`QuadraticDiscriminantAnalysis`](https://scikit-learn.org/stable/modules/generated/sklearn.discriminant_analysis.QuadraticDiscriminantAnalysis.html) permet de calculer les paramètres des Gaussiennes multivariées pour chacune des classes.
On peut calculer une distance entre deux nuages Gaussiens avec la distance dites de Jeffries-Matusita (JM) basée sur la distance de Bhattacharyya $B$ [@Jensen2016]:
$$
\begin{aligned}
JM_{ij} &= 2(1 - e^{-B_{ij}}) \\
B_{ij} &= \frac{1}{8}(m_i - m_j)^t \left( \frac{\Sigma_i + \Sigma_j}{2} \right)^{-1} (m_i - m_j) + \frac{1}{2} \ln \left( \frac{|(\Sigma_i + \Sigma_j)/2|}{|\Sigma_i|^{1/2} |\Sigma_j|^{1/2}} \right)
\end{aligned}
$$
Cette distance présuppose que chaque classe $i$ est décrite par son centre $m_i$ et de sa dispersion dans l'espace à $D$ dimensions mesurée par la matrice de covariance $\Sigma_i$. On peut en faire facilement une fonction Python à l'aide de `numpy`:
```{python}
def bhattacharyya_distance(m1, s1, m2, s2):
# Calcul de la covariance moyenne
s = (s1 + s2) / 2
# Calcul du premier terme (différence des moyennes)
m_diff = m1 - m2
term1 = np.dot(np.dot(m_diff.T, np.linalg.inv(s)), m_diff) / 8
# Calcul du second terme (différence de covariances)
term2 = 0.5 * np.log(np.linalg.det(s) / np.sqrt(np.linalg.det(s1) * np.linalg.det(s2)))
return term1 + term2
def jeffries_matusita_distance(m1, s1, m2, s2):
B = bhattacharyya_distance(m1, s1, m2, s2)
return 2 * (1 - np.exp(-B))
```
La figure ci-dessous illustre différentes situations avec des données simulées ainsi que les distances JM correspondantes :
```{python}
#| echo: false
#| warning: false
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs, make_classification, make_gaussian_quantiles
cmap_classes = ListedColormap( ['blue','yellow','red'])
np.random.seed(42)
plt.figure(figsize=(8, 8))
plt.subplots_adjust(bottom=0.05, top=0.9, left=0.05, right=0.95)
plt.subplot(321)
X1, Y1 = make_classification(
n_features=2, n_redundant=0, n_informative=1, n_clusters_per_class=1
)
qda = QuadraticDiscriminantAnalysis(store_covariance=True)
qda.fit(X1, Y1)
jm=jeffries_matusita_distance(qda.means_[0], qda.covariance_[0], qda.means_[1], qda.covariance_[1])
plt.title(f"Une dimension informative, un mode par classe JM_12={jm:3.2f}", fontsize="small")
plt.scatter(X1[:, 0], X1[:, 1], marker="o", c=Y1, s=25, edgecolor="k", cmap=cmap_classes)
plt.subplot(322)
X1, Y1 = make_classification(
n_features=2, n_redundant=0, n_informative=2, n_clusters_per_class=1
)
qda = QuadraticDiscriminantAnalysis(store_covariance=True)
qda.fit(X1, Y1)
jm=jeffries_matusita_distance(qda.means_[0], qda.covariance_[0], qda.means_[1], qda.covariance_[1])
plt.title(f"Deux dimensions informatives, un mode par classe JM_12={jm:3.2f}", fontsize="small")
plt.scatter(X1[:, 0], X1[:, 1], marker="o", c=Y1, s=25, edgecolor="k", cmap=cmap_classes)
plt.subplot(323)
X2, Y2 = make_classification(n_features=2, n_redundant=0, n_informative=2)
qda = QuadraticDiscriminantAnalysis(store_covariance=True)
qda.fit(X1, Y1)
jm=jeffries_matusita_distance(qda.means_[0], qda.covariance_[0], qda.means_[1], qda.covariance_[1])
plt.title(f"Deux dimensions informatives, deux modes par classe JM_12={jm:3.2f}", fontsize="small")
plt.scatter(X2[:, 0], X2[:, 1], marker="o", c=Y2, s=25, edgecolor="k", cmap=cmap_classes)
plt.subplot(324)
X1, Y1 = make_classification(
n_features=2, n_redundant=0, n_informative=2, n_clusters_per_class=1, n_classes=3
)
qda = QuadraticDiscriminantAnalysis(store_covariance=True)
qda.fit(X1, Y1)
jm=jeffries_matusita_distance(qda.means_[0], qda.covariance_[0], qda.means_[1], qda.covariance_[1])
plt.title(f"Trois classes, deux dimensions informatives, un mode JM_12={jm:3.2f}", fontsize="small")
plt.scatter(X1[:, 0], X1[:, 1], marker="o", c=Y1, s=25, edgecolor="k", cmap=cmap_classes)
plt.subplot(325)
X1, Y1 = make_blobs(n_features=2, centers=3)
qda = QuadraticDiscriminantAnalysis(store_covariance=True)
qda.fit(X1, Y1)
jm=jeffries_matusita_distance(qda.means_[0], qda.covariance_[0], qda.means_[1], qda.covariance_[1])
plt.title(f"Trois classes JM_12={jm:3.2f}", fontsize="small")
plt.scatter(X1[:, 0], X1[:, 1], marker="o", c=Y1, s=25, edgecolor="k", cmap=cmap_classes)
plt.subplot(326)
X1, Y1 = make_gaussian_quantiles(n_features=2, n_classes=3)
qda = QuadraticDiscriminantAnalysis(store_covariance=True)
qda.fit(X1, Y1)
jm=jeffries_matusita_distance(qda.means_[0], qda.covariance_[0], qda.means_[1], qda.covariance_[1])
plt.title(f"Trois classes, Gaussiennes superposées JM_12={jm:3.2f}", fontsize="small")
plt.scatter(X1[:, 0], X1[:, 1], marker="o", c=Y1, s=25, edgecolor="k", cmap=cmap_classes)
plt.show()
```
On forme notre ensemble d'entrainement à partir du fichier `csv` de la section @sec-05.02.02.
```{python}
df= pd.read_csv('sampling_points.csv')
# Extraire la colonne 'value'.
# 'value' est une chaîne de caractères représentation d'une liste de nombres.
# Nous devons la convertir en données numériques réelles.
X = df['value'].apply(lambda x: np.fromstring(x[1:-1], dtype=float, sep=' ')).to_list()
# on obtient une liste de numpy array qu'il faut convertir en un numpy array 2D
X= np.array([row.tolist() for row in X])
idx= X.sum(axis=-1)>0 # on exclut certains points sans valeurs
X= X[idx,...]
y = df['class'].to_numpy()
y= y[idx]
class_labels = np.unique(y).tolist() # on cherche à savoir combien de classes uniques
n_classes = len(class_labels)
if max(class_labels) > n_classes: # il se peut que certaines classes soit absentes
y_new= []
for i,l in enumerate(class_labels):
y_new.extend([i]*sum(y==l))
y_new = np.array(y_new)
couleurs_classes2= [couleurs_classes[c] for c in np.unique(y).tolist()] # couleurs des classes
nom_classes2= [nom_classes[c] for c in np.unique(y).tolist()]
cmap_classes2 = ListedColormap(couleurs_classes2)
```
On peut faire une analyse de séparabilité sur notre ensemble d'entrainement de 10 classes. On obtient un tableau symmétrique de 10x10 valeurs. On observe des valeurs inférieures à 1, indiquant des séparabilités faibles entre ces classes sous l'hypothèse du modèle Gaussien:
```{python}
qda= QuadraticDiscriminantAnalysis(store_covariance=True)
qda.fit(X, y_new) # calcul des paramètres des distributions Gaussiennes
JM= []
classes= np.unique(y_new).tolist() # étiquettes uniques des classes
for cl1 in classes:
for cl2 in classes:
JM.append(jeffries_matusita_distance(qda.means_[cl1], qda.covariance_[cl1], qda.means_[cl2], qda.covariance_[cl2]))
JM= np.array(JM).reshape(len(classes),len(classes))
JM= pd.DataFrame(JM, index=classes, columns=classes)
JM.head(10)
```
Afin d'évaluer chaque classe, on peut calculer la séparabilité minimale, ce qui nous permet de constater que la classe eau a le maximum de séparabilité avec les autres classes.
```{python}
#| echo: false
#| warning: false
plt.figure(figsize=(5, 3))
plt.bar(range(JM.shape[0]), np.min(JM[JM>0],axis=1))
plt.xlabel("Classes")
plt.ylabel("JM")
plt.title("Séparabilité minimale", fontsize="small")
plt.xticks(range(len(nom_classes2)), nom_classes2, rotation=45, ha='right')
plt.show()
```
## Mesures de performance d'une méthode de classification
Lorsque que l'on cherche à établir la performance d'un modèle, il convient de mesurer la performance du classificateur utilisé. Il existe de nombreuses mesures de performance qui sont toutes dérivées de la matrice de confusion. Cette matrice compare les étiquettes provenant de l'annotation (la vérité terrain) et les étiquettes prédites par un modèle. On peut définir $C(i,j)$ comme étant le nombre de prédictions dont la vérité terrain indique la classe $i$ qui sont prédites dans la classe $j$. La fonction [confusion_matrix](https://scikit-learn.org/stable/modules/generated/sklearn.metrics.confusion_matrix.html) permet de faire ce calcul, voici un exemple très simple:
```{python}
y_true = ["cat", "ant", "cat", "cat", "ant", "bird", "bird"]
y_pred = ["ant", "ant", "cat", "cat", "ant", "cat", "bird"]
confusion_matrix(y_true, y_pred, labels=["ant", "bird", "cat"])
```
La fonction [classification_report](https://scikit-learn.org/stable/modules/generated/sklearn.metrics.classification_report.html#sklearn.metrics.classification_report) permet de générer quelques métriques:
```{python}
y_true = ["cat", "ant", "cat", "cat", "ant", "bird", "bird"]
y_pred = ["ant", "ant", "cat", "cat", "ant", "cat", "bird"]
print(classification_report(y_true, y_pred, target_names=["ant", "bird", "cat"]))
```
Le rappel (*recall*) pour une classe donnée est la proportion de la vérité terrain qui a été correctement identifiée et est sensible aux confusions entre classes (erreurs d'omission). Les valeurs de rappels correspondent à une normalisation de la matrice de confusion par rapport aux lignes.
$$
Recall_i= C_{ii} / \sum_j C_{ij}
$$ Une faible valeur de rappel signifie que le classificateur confond facilement la classe concernée avec d'autres classes.
La précision est la portion des prédictions qui ont été bien classifiées et est sensible aux fausses alarmes (erreurs de commission). Les valeurs de précision correspondent à une normalisation de la matrice de confusion par rapport aux colonnes. $$
Precision_i= C_{ii} / \sum_i C_{ij}
$$ Une faible valeur de précision signifie que le classificateur trouve facilement la classe concernée dans d'autres classes.
Le `f1-score` calcul une moyenne des deux métriques précédentes: $$
\text{f1-score}_i=2\frac{Recall_i \times Precision_i}{Recall_i + Precision_i}
$$
## Méthodes non paramétriques
Les méthodes non paramétriques ne font pas d'hypothèses particulières sur les données. Un des inconvénients de ces modèles est que le nombre de paramètres du modèle augmente avec la taille des données.
### Méthode des parallélépipèdes {#sec-0511}
La méthode du parallélépipède est probablement la plus simple et consiste à délimiter directement le domaine des points d'une classe par une boite (un parallélépipède) à $D$ dimensions. Les limites de ces parallélépipèdes forment alors des frontières de décision manuelles qui permettent d'attribuer une classe d'appartenance à un nouveau point. Un des avantages de cette technique est que si un point n'est dans aucun parallélépipède alors il est non classifié. Par contre, la construction de ces parallélépipèdes se complexifient grandement avec le nombre de bandes. À une dimension, deux paramètres, équivalents à un seuillage d'histogramme, sont suffisants. À deux dimensions, vous devez définir 4 segments par classe. Avec trois bandes, vous devez définir six plans par classes et à D dimensions, D hyperplans à D-1 dimensions par classe. Le modèle ici est donc une suite de valeurs `min` et `max` pour chacune des bandes et des classes:
```{python}
def parrallepiped_train(X_train, y_train):
classes= np.unique(y_train).tolist()
clf= []
for cl in classes:
data_cl= X_train[y_train == cl,...] # on cherche les données pour la classe courante
limits=[]
for b in range(data_cl.shape[1]):
limits.append([data_cl[:,b].min(), data_cl[:,b].max()]) # on calcul le min et max pour chaque bande
clf.append(np.array(limits))
return clf
clf= parrallepiped_train(X, y_new)
```
La prédiction consiste à trouver pour chaque point la première limite qui est satisfaite. Notez qu'il n'y a aucun moyen de décider quelle est la meilleure classe si le point appartient à plusieurs classes.
```{python}
@jit(nopython=True)
def parrallepiped_predict(clf, X_test):
y_pred= []
for data in X_test:
y_pred.append(np.nan)
for cl, limits in enumerate(clf):
inside= True
for b,limit in enumerate(limits):
inside = inside and (data[b] >= limit[0]) & (data[b] <= limit[1])
if ~inside:
break
if inside:
y_pred[-1]=cl
return np.array(y_pred)
```
On peut appliquer ensuite le modèle sur l'image au complet. Les résultats sont assez mauvais, puisque seule la classe eau en bleu semble être bien classifiée.
```{python}
data_image= img_rgbnir.to_numpy().transpose(1,2,0).reshape(img_rgbnir.shape[1]*img_rgbnir.shape[2],4)
y_image= parrallepiped_predict(clf, data_image)
y_image= y_image.reshape(img_rgbnir.shape[1],img_rgbnir.shape[2])
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(8, 6))
plt.imshow(y_image, cmap=cmap_classes2)
ax.set_title("Méthode des parrallélépipèdes", fontsize="small")
plt.show()
```
On peut calculer quelques mesures de performance sur l'ensemble d'entrainement :
```{python}
y_pred= parrallepiped_predict(clf, X)
nom_classes2= [nom_classes[c] for c in np.unique(y).tolist()]
print(classification_report(y_new, y_pred, target_names=nom_classes2, zero_division=np.nan))
```
#### La malédiction de la haute dimension
Augmenter le nombre de dimension ou de caractéristiques des données permet de résoudre des problèmes complexes comme la classification d'image. Cependant, cela amène beaucoup de contraintes sur le volume des données. Supposons que nous avons N points occupant un segment linéaire de taille d. La densité de points est $N/d$. Si nous augmentons le nombre de dimension D, la densité de points va diminuer exponentiellement en $1/d^D$. Par conséquent, pour garder une densité constante et donc une bonne estimation des parallélépipèdes, il nous faudrait augmenter le nombre de points en puissance de D. Ceci porte le nom de la malédiction de la dimensionnalité (*dimensionality curse*). En résumé, l'espace vide augmente plus rapidement que le nombre de données d'entraînement et l'espace des données devient de plus en plus parcimonieux (*sparse*). Pour contrecarrer ce problème, on peut sélectionner les meilleures caractéristiques ou appliquer une réduction de dimension comme une ACP (Analyse en composantes principales).
### Plus proches voisins
La méthode des plus proches voisins (*K-Nearest-Neighbors*) est certainement la plus simple des méthodes pour classifier des données. Elle consiste à comparer une nouvelle donnée avec ses voisins les plus proches en fonction d'une simple distance Euclidienne. Si une majorité de ces $K$ voisins appartiennent à une classe majoritaire alors cette classe est sélectionnée. Afin de permettre un vote majoritaire, on choisira un nombre impair pour la valeur de $K$. Mallgré sa simplicité, cette technique peut devenir assez demandante en temps de calcul pour un nombre important de points et un nombre élevé de dimensions.
Reprenons l'ensemble d’entraînement formé à partir de notre image RGBNIR précédente :
```{python}
df= pd.read_csv('sampling_points.csv')
# Extraire la colonne 'value'.
# 'value' est une chaîne de caractères comme représentation d'une liste de valeurs.
# Nous devons la convertir en données numériques réelles.
X = df['value'].apply(lambda x: np.fromstring(x[1:-1], dtype=float, sep=' ')).to_list()
# on obtient une liste de numpy array qu'il faut convertir en un numpy array 2D
X= np.array([row.tolist() for row in X])
idx= X.sum(axis=-1)>0 # il se peut qu'il y ait des valeurs erronées
X= X[idx,...]
y = df['class'].to_numpy()
y= y[idx]
class_labels = np.unique(y).tolist() # on cherche à savoir combien de classes uniques
n_classes = len(class_labels)
if max(class_labels) > n_classes: # il se peut que certaines classes soit absentes
y_new= []
for i,l in enumerate(class_labels):
y_new.extend([i]*sum(y==l))
y_new = np.array(y_new)
nom_classes2= [nom_classes[c] for c in np.unique(y).tolist()]
```
Il importe de préalablement centrer (moyenne = 0) et de réduire (variance = 1) les données avant d’appliquer la méthode K-NN; avec cette méthode de normalisation, on dit parfois que l’on blanchit les données. Puisque la variance de chaque dimension est égale à 1 (et donc l’inertie totale est égale au nombre de bandes), on s’assure qu’elle ait le même poids ait le même poids dans le calcul des distances entre points. Cette opération porte le nom de `StandardScaler` dans `scikit-learn`. On peut alors former un pipeline de traitement combinant les deux opérations :
```{python}
clf = Pipeline(
steps=[("scaler", StandardScaler()), ("knn", KNeighborsClassifier(n_neighbors=1))]
)
```
Avant d'effectuer un entraînement, on met généralement une portion des données pour valider les performances :
```{python}
X_train, X_test, y_train, y_test = train_test_split(X, y_new, test_size=0.2, random_state=0)
```
On peut visualiser les frontières de décision du K-NN pour différentes valeurs de $K$ lorsque seulement deux bandes sont utilisées (Rouge et proche infra-rouge ici) :
```{python}
#| echo: false
#| warning: false
#| fig-cap: "Frontières de décision pour le classificateur K-NN"
_, axs = plt.subplots(nrows=2,ncols=2, figsize=(9,9))
couleurs_classes2= [couleurs_classes[c] for c in np.unique(y).tolist()]
cmap_classes2 = ListedColormap(couleurs_classes2)
for ax, K in zip(axs.flatten(), [1,3,7,15]):
clf.set_params(knn__weights='distance', knn__n_neighbors = K).fit(X_train[:,2:4], y_train)
y_pred = clf.predict(X_test[:,2:4])
print("Number of mislabeled points out of a total %d points : %d"
% (X_test.shape[0], (y_test != y_pred).sum()))
disp = DecisionBoundaryDisplay.from_estimator(
clf,
X[:,2:4],
response_method="predict",
plot_method="contourf",
shading="auto",
alpha=0.5,
ax=ax,
cmap= cmap_classes2,
)
scatter = disp.ax_.scatter(X_test[:, 2], X_test[:, 3], c=y_test, edgecolors="k", cmap=cmap_classes2)
disp.ax_.set_xlabel('Rouge', fontsize="small")
disp.ax_.set_ylabel('Proche infra-rouge', fontsize="small")
_ = disp.ax_.set_title(
f"K={clf[-1].n_neighbors} erreur={(y_test != y_pred).sum()/X_test.shape[0]*100:3.1f}%", fontsize="small"
)
plt.show()
```
On peut voir comment les différentes frontières de décision se forment dans l'espace des bandes Rouge-NIR. L'augmentation de K rend ces frontières plus complexes et le calcul plus long.
```{python}
clf.set_params(knn__weights='distance', knn__n_neighbors = 7).fit(X_train, y_train)
y_pred = clf.predict(X_test)
print("Nombre de points misclassifiés sur %d points : %d"
% (X_test.shape[0], (y_test != y_pred).sum()))
```
Le rapport de performance est le suivant :
```{python}
nom_classes2= [nom_classes[c] for c in np.unique(y).tolist()]
print(classification_report(y_test, y_pred, target_names=nom_classes2, zero_division=np.nan))
```
La matrice de confusion peut-être affichée de manière graphique :
```{python}
disp= ConfusionMatrixDisplay.from_predictions(y_test, y_pred, display_labels=nom_classes2, xticks_rotation='vertical')
```
L'application du modèle (la prédiction) peut se faire sur toute l'image en transposant l'image sous forme d'une matrice avec Largeur x Hauteur lignes et 4 colonnes :
```{python}
data_image= img_rgbnir.to_numpy().transpose(1,2,0).reshape(img_rgbnir.shape[1]*img_rgbnir.shape[2],4)
y_classe= clf.predict(data_image)
y_classe= y_classe.reshape(img_rgbnir.shape[1],img_rgbnir.shape[2])
```
```{python}
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(8, 6))
plt.imshow(y_classe, cmap=cmap_classes2)
ax.set_title("Carte d'occupation des sols avec K-NN", fontsize="small")
plt.show()
```
### Méthodes par arbre de décision
La méthode par arbre de décision consiste à construire une cascade de règles de décision sur chaque caractéristique du jeu de donnée [@Breiman1984]. On pourra trouver plus de détails dans la documentation de `scikit-learn` ([Decision Trees](https://scikit-learn.org/stable/modules/tree.html)). Les arbres de décision on tendance à surapprendre surtout si le nombre de dimensions est élevé. Il est donc conseillé d'avoir un bon ratio entre le nombre d'échantillons et le nombre de dimensions.
```{python}
X_train, X_test, y_train, y_test = train_test_split(X, y_new, test_size=0.2, random_state=0)
```
```{python}
#| echo: false
#| warning: false
#| fig-cap: "Frontières de décision pour des arbres de décision de différente profondeur"
from sklearn import tree
_, axs = plt.subplots(nrows=2,ncols=2, figsize=(9,9))
couleurs_classes2= [couleurs_classes[c] for c in np.unique(y).tolist()]
cmap_classes2 = ListedColormap(couleurs_classes2)
clf = tree.DecisionTreeClassifier()
for ax, K in zip(axs.flatten(), [1,2,3,4]):
clf.set_params(max_depth = K).fit(X_train[:,2:4], y_train)
y_pred = clf.predict(X_test[:,2:4])
print("Number of mislabeled points out of a total %d points : %d"
% (X_test.shape[0], (y_test != y_pred).sum()))
disp = DecisionBoundaryDisplay.from_estimator(
clf,
X[:,2:4],
response_method="predict",
plot_method="contourf",
shading="auto",
alpha=0.5,
ax=ax,
cmap= cmap_classes2,
)
scatter = disp.ax_.scatter(X_test[:, 2], X_test[:, 3], c=y_test, edgecolors="k", cmap=cmap_classes2)
disp.ax_.set_xlabel('Rouge', fontsize="small")
disp.ax_.set_ylabel('Proche infra-rouge', fontsize="small")
_ = disp.ax_.set_title(
f"max_depth={K} erreur={(y_test != y_pred).sum()/X_test.shape[0]*100:3.1f}%", fontsize="small"
)
plt.show()
```
On peut observer que les frontières de décision sont formées d'un ensemble de plans simple. Chaque plan étant issu d'une règle de décison formé d'un seuil sur chacune des dimensions. On entraine un arbre de décision avec une profondeur maximale de 5:
```{python}
clf = tree.DecisionTreeClassifier(max_depth=5)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
print("Nombre de points misclassifiés sur %d points : %d"
% (X_test.shape[0], (y_test != y_pred).sum()))
```
Le rapport de performance et la matrice de confusion:
```{python}
#| echo: false
#| warning: false
print(classification_report(y_test, y_pred, target_names=nom_classes2, zero_division=np.nan))
```
```{python}
#| echo: false
#| warning: false
disp= ConfusionMatrixDisplay.from_predictions(y_test, y_pred, display_labels=nom_classes2, xticks_rotation='vertical')
```
L'application du modèle (la prédiction) peut se faire sur toute l'image en transposant l'image sous forme d'une matrice avec Largeur x Hauteur lignes et 4 colonnes:
```{python}
#| echo: false
#| output: false
#| warning: false
data_image= img_rgbnir.to_numpy().transpose(1,2,0).reshape(img_rgbnir.shape[1]*img_rgbnir.shape[2],4)
y_classe= clf.predict(data_image)
y_classe= y_classe.reshape(img_rgbnir.shape[1],img_rgbnir.shape[2])
```
```{python}
#| echo: false
#| warning: false
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(8, 6))
plt.imshow(y_classe, cmap=cmap_classes2)
ax.set_title("Carte d'occupation des sols avec un arbre de décision", fontsize="small")
plt.show()
```
Il est possible de visualiser l'arbre avec l'outil [SuperTree](https://github.com/mljar/supertree?tab=readme-ov-file) mais cela contient beaucoup d'information
```{python}
from supertree import SuperTree # <- import supertree :)
super_tree = SuperTree(clf, X_train, y_train, ['Bleu', 'Vert', 'Rouge', 'NIR'], nom_classes2)
super_tree.show_tree()
```
On peut voir que le nœud le plus haut utilise la bande proche-infrarouge pour séparer l'eau de la forêt comme première décision.
## Méthodes paramétriques
Les méthodes paramétriques se basent sur des modélisations statistiques des données pour permettre une classification. Contrairement au méthodes non paramétriques, elles ont un nombre fixe de paramètres qui ne dépend pas de la taille du jeu de données. Par contre, des hypothèses sont faites a priori sur le comportement statistique des données. La classification consiste alors à trouver la classe la plus vraisemblable dont le modèle statistique décrit le mieux les valeurs observées. L'ensemble d’entraînement permettra alors de calculer les paramètres de chaque Gaussienne pour chacune des classes d'intérêt.
### Méthode Bayésienne naïve
La méthode Bayésienne naïve Gaussienne consiste à poser des hypothèses simplificatrices sur les données, en particulier l'indépendance des données et des dimensions. Ceci permet un calcul plus simple.
```{python}
from sklearn.naive_bayes import GaussianNB
gnb = GaussianNB()
y_pred = gnb.fit(X_train, y_train).predict(X_test)
print("Nombre de points erronés sur %d points : %d"
% (X_test.shape[0], (y_test != y_pred).sum()))
```
```{python}
#| echo: false
#| warning: false
#| fig-cap: "Frontières de décision pour un classificateur Bayésien naif"
_, axs = plt.subplots(nrows=1,ncols=1, figsize=(4, 4))
couleurs_classes2= [couleurs_classes[c] for c in np.unique(y).tolist()]
cmap_classes2 = ListedColormap(couleurs_classes2)
#clf.set_params(knn__weights='distance', knn__n_neighbors = K).fit(X_train[:,2:4], y_train)
gnb= gnb.fit(X_train[:,2:4], y_train)
disp = DecisionBoundaryDisplay.from_estimator(
gnb,
X[:,2:4],
response_method="predict",
plot_method="contourf",
shading="auto",
alpha=0.5,
ax=axs,
cmap= cmap_classes2,
)
scatter = disp.ax_.scatter(X_test[:, 2], X_test[:, 3], c=y_test, edgecolors="k", cmap=cmap_classes2)
disp.ax_.set_xlabel('Rouge', fontsize="small")
disp.ax_.set_ylabel('NIR', fontsize="small")
_ = disp.ax_.set_title(
f"Bayésien naif", fontsize="small"
)
plt.show()
```
On observe que les frontières de décision sont beaucoup plus régulières que pour K-NN.
```{python}
gnb.fit(X_train, y_train)
y_pred = gnb.predict(X_test)
print("Nombre de points misclassifiés sur %d points : %d"
% (X_test.shape[0], (y_test != y_pred).sum()))
```
De la même manière, la prédiction peut s'appliquer sur toute l'image:
```{python}
#| echo: false
#| warning: false
data_image= img_rgbnir.to_numpy().transpose(1,2,0).reshape(img_rgbnir.shape[1]*img_rgbnir.shape[2],4)
y_classe= gnb.predict(data_image)
y_classe= y_classe.reshape(img_rgbnir.shape[1],img_rgbnir.shape[2])
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(8, 6))
plt.imshow(y_classe, cmap=cmap_classes2)
ax.set_title("Carte d'occupation des sols avec la méthode Bayésienne naive", fontsize="small")
plt.show()
```
### Analyse discriminante quadratique (ADQ)
L'analyse discriminante quadratique peut-être vue comme une généralisation de l'approche Bayésienne naive qui suppose des modèles Gaussiens indépendants pour chaque dimension et chaque point. Ici, on va considérer un modèle Gaussien multivarié.
```{python}
qda = QuadraticDiscriminantAnalysis(store_covariance=True)
qda.fit(X_train, y_train)
y_pred = qda.predict(X_test)
print("Nombre de points misclassifiés sur %d points : %d"
% (X_test.shape[0], (y_test != y_pred).sum()))
```
Les Gaussiennes multivariées peuvent être visualiser sous forme d'éllipses décrivant le domaine des valeurs de chaque classe:
```{python}
#| echo: false
#| warning: false
import matplotlib as mpl
colors= [couleurs_classes[c] for c in np.unique(y).tolist()]
def make_ellipses(gmm, bands, ax):
for n, color in enumerate(colors):
covariances = gmm.covariance_[n][np.ix_(bands, bands)]
v, w = np.linalg.eigh(covariances)
u = w[0] / np.linalg.norm(w[0])
angle = np.arctan2(u[1], u[0])
angle = 180 * angle / np.pi # convert to degrees
v = 2.0 * np.sqrt(2.0) * np.sqrt(v)
#print(v)
ell = mpl.patches.Ellipse(
gmm.means_[n, :2], v[0], v[1], angle=180 + angle, color=color
)
ell.set_clip_box(ax.bbox)
ell.set_alpha(0.5)
ax.add_artist(ell)
ax.set_aspect("equal", "datalim")
fig, ax = plt.subplots(nrows=1, ncols=3, figsize=(9, 3), sharey=True)
noms_bandes= ['Bleu','Vert','Rouge','NIR']
for index, b in enumerate([0,1,3]):
#h = plt.subplot(1, 3, index+1)
bands= [2,b]
make_ellipses(qda, bands, ax[index])
for n, color in enumerate(colors):
data = X[y_new == n,...]
plt.scatter(
data[:, bands[0]], data[:, bands[1]], s=0.8, color=color
)
ax[index].set_xlabel(noms_bandes[2], fontsize="small")
ax[index].set_ylabel(noms_bandes[b], fontsize="small")
```
De la même manière, la prédiction peut s'appliquer sur toute l'image:
```{python}
#| eval: true
#| echo: false
#| warning: false
data_image= img_rgbnir.to_numpy().transpose(1,2,0).reshape(img_rgbnir.shape[1]*img_rgbnir.shape[2],4)
y_classe= qda.predict(data_image)
y_classe= y_classe.reshape(img_rgbnir.shape[1],img_rgbnir.shape[2])
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(8, 6))
plt.imshow(y_classe, cmap=cmap_classes2)