paint-brush
Classification multiclasse : comprendre les fonctions d'activation et de perte dans les réseaux de neuronespar@owlgrey
1,345 lectures
1,345 lectures

Classification multiclasse : comprendre les fonctions d'activation et de perte dans les réseaux de neurones

par Dmitrii Matveichev 25m2024/01/24
Read on Terminal Reader

Trop long; Pour lire

Pour créer un réseau neuronal de classification multi-classe, vous devez utiliser la fonction d'activation softmax sur sa couche finale ainsi qu'une perte d'entropie croisée. La taille finale de la couche doit être k, où k est le nombre de classes. Les ID de classe doivent être prétraités avec un codage à chaud. Un tel réseau neuronal produira des probabilités p_i que l'entrée appartient à une classe i. Pour trouver l’ID de classe prédit, vous devez trouver l’indice de probabilité maximale.
featured image - Classification multiclasse : comprendre les fonctions d'activation et de perte dans les réseaux de neurones
Dmitrii Matveichev  HackerNoon profile picture


Mon article précédent formule le problème de classification et le divise en 3 types (binaire, multi-classes et multi-étiquettes) et répond à la question « Quelles fonctions d'activation et de perte devez-vous utiliser pour résoudre une tâche de classification binaire ?


Dans cet article, je répondrai à la même question mais pour la tâche de classification multi-classes et vous fournirai un exemple d'implémentation de pytorch dans Google Colab .


Quelles fonctions d'activation et de perte devez-vous utiliser pour résoudre une tâche de classification multi-classes ?


Le code fourni est en grande partie basé sur l'implémentation de la classification binaire puisque vous devez ajouter très peu de modifications à votre code et NN pour passer de la classification binaire à la multi-classe. Les blocs de code modifiés sont marqués par (Modifié) pour une navigation plus facile.


1 Pourquoi est-il important de comprendre la fonction d'activation et la perte utilisées pour la classification multiclasse ?

Comme nous le montrerons plus loin, la fonction d'activation utilisée pour la classification multi-classe est l'activation softmax. Softmax est largement utilisé dans différentes architectures NN en dehors de la classification multiclasse. Par exemple, softmax est au cœur du bloc d'attention multi-têtes utilisé dans les modèles Transformer (voir Attention Is All You Need ) en raison de sa capacité à convertir les valeurs d'entrée en une distribution de probabilité (voir plus à ce sujet plus tard).


Attention aux produits scalaires à l'échelle (le plus souvent dans le module d'attention multi-têtes)



Si vous connaissez la motivation derrière l'application de l'activation softmax et de la perte CE pour résoudre des problèmes de classification multi-classes, vous serez en mesure de comprendre et de mettre en œuvre des architectures NN et des fonctions de perte beaucoup plus compliquées.


2 Formulation du problème de classification multiclasse

Le problème de classification multiclasse peut être représenté comme un ensemble d'échantillons {(x_1, y_1), (x_2, y_2),...,(x_n, y_n)} , où x_i est un vecteur à m dimensions qui contient les caractéristiques de l'échantillon. i et y_i sont la classe à laquelle x_i appartient. Où l'étiquette y_i peut prendre l'une des k valeurs, où k est le nombre de classes supérieur à 2. L'objectif est de construire un modèle qui prédit l'étiquette y_i pour chaque échantillon d'entrée x_i .

Exemples de tâches pouvant être traitées comme des problèmes de classification multi-classes :

  • diagnostic médical - diagnostiquer un patient atteint d'une maladie parmi plusieurs sur la base des données fournies (antécédents médicaux, résultats de tests, symptômes)
  • catégorisation des produits - classification automatique des produits pour les plateformes de commerce électronique
  • prévisions météorologiques - classer le temps futur comme ensoleillé, nuageux, pluvieux, etc.
  • classer les films, la musique et les articles en différents genres
  • classer les avis clients en ligne en catégories telles que les commentaires sur les produits, les commentaires sur le service, les réclamations, etc.


3 Fonctions d'activation et de perte pour la classification multi-classe


Dans le classement multi-classes vous sont donnés :

  • un ensemble d'échantillons {(x_1, y_1), (x_2, y_2),...,(x_n, y_n)}

  • x_i est un vecteur à m dimensions qui contient les caractéristiques de l'échantillon i

  • y_i est la classe à laquelle x_i appartient et peut prendre l'une des k valeurs, où k>2 est le nombre de classes.


Pour construire un réseau neuronal de classification multi-classes en tant que classificateur probabiliste, nous avons besoin de :

  • une couche de sortie entièrement connectée d'une taille de k
  • les valeurs de sortie doivent être comprises dans la plage [0,1]
  • la somme des valeurs de sortie doit être égale à 1. Dans la classification multi-classes, chaque entrée x ne peut appartenir qu'à une seule classe (classes mutuellement exclusives), donc la somme des probabilités de toutes les classes doit être 1 : SUM(p_0,…,p_k )=1 .
  • une fonction de perte qui a la valeur la plus faible lorsque la prédiction et la vérité terrain sont identiques


3.1 La fonction d'activation softmax

La couche linéaire finale d'un réseau neuronal génère un vecteur de « valeurs de sortie brutes ». Dans le cas de la classification, les valeurs de sortie représentent la confiance du modèle selon laquelle l'entrée appartient à l'une des k classes. Comme indiqué précédemment, la couche de sortie doit avoir une taille k et les valeurs de sortie doivent représenter les probabilités p_i pour chacune des k classes et SUM(p_i)=1 .


L'article sur la classification binaire utilise l'activation sigmoïde pour transformer les valeurs de sortie NN en probabilités. Essayons d'appliquer la sigmoïde sur k valeurs de sortie dans la plage [-3, 3] et voyons si la sigmoïde satisfait aux exigences énumérées précédemment :


  • k les valeurs de sortie doivent être comprises dans la plage (0,1), où k est le nombre de classes

  • la somme des k valeurs de sortie doit être égale à 1


    Définition de la fonction sigmoïde


    L'article précédent montre que la fonction sigmoïde mappe les valeurs d'entrée dans une plage (0,1). Voyons si l'activation sigmoïde satisfait à la deuxième exigence. Dans le tableau d'exemple ci-dessous, j'ai traité un vecteur de taille k (k = 7) avec activation sigmoïde et j'ai résumé toutes ces valeurs - la somme de ces 7 valeurs est égale à 3,5. Un moyen simple de résoudre ce problème serait de diviser toutes les valeurs k par leur somme.


Saisir

-3

-2

-1

0

1

2

3

SOMME

sortie sigmoïde

0,04743

0,11920

0,26894

0,50000

0,73106

0,88080

0,95257

3,5000


Une autre façon serait de prendre l'exposant de la valeur d'entrée et de le diviser par la somme des exposants de toutes les valeurs d'entrée :


Définition de la fonction Softmax


La fonction softmax transforme un vecteur de nombres réels en vecteur de probabilités. Chaque probabilité dans le résultat est comprise dans la plage (0,1) et la somme des probabilités est 1.

Saisir

-3

-2

-1

0

1

2

3

SOMME

softmax

0,00157

0,00426

0,01159

0,03150

0,08563

0,23276

0,63270

1

Le tracé de l'exposant dans la plage [-10, 10]


Softmax d'un vecteur de taille 21 avec des valeurs [-10, 10]


Il y a une chose dont vous devez être conscient lorsque vous travaillez avec softmax : la valeur de sortie p_i dépend de toutes les valeurs du tableau d'entrée puisque nous la divisons par la somme des exposants de toutes les valeurs. Le tableau ci-dessous le démontre : deux vecteurs d'entrée ont 3 valeurs communes {1, 3, 4}, mais les valeurs softmax de sortie diffèrent car le deuxième élément est différent (2 et 4).

Entrée 1

1

2

3

4

softmax 1

0,0321

0,0871

0,2369

0,6439

Entrée 2

1

4

3

4

softmax2

0,0206

0,4136

0,1522

0,4136


3.2 Perte d'entropie croisée

La perte d'entropie croisée binaire est définie comme :

Perte d'entropie croisée binaire


Dans la classification binaire, il existe deux probabilités de sortie p_i et (1-p_i) et des valeurs de vérité terrain y_i et (1-y_i).


Le problème de classification multi-classes utilise la généralisation de la perte BCE pour N classes : perte d'entropie croisée.


Perte d'entropie croisée


N est le nombre d'échantillons d'entrée, y_i est la vérité terrain et p_i est la probabilité prédite de la classe i .


4 Exemple de classification NN multi-classes avec PyTorch

Pour implémenter une classification probabiliste multi-classes NN il nous faut :

  • la vérité terrain et les prédictions doivent avoir des dimensions [N,k]N est le nombre d'échantillons d'entrée, k est le nombre de classes - l'identifiant de classe doit être codé dans un vecteur de taille k
  • la taille de la couche linéaire finale doit être k
  • les sorties de la couche finale doivent être traitées avec l'activation softmax pour obtenir les probabilités de sortie
  • La perte CE doit être appliquée aux probabilités de classe prédites et aux valeurs de vérité terrain
  • trouver l'identifiant de classe de sortie à partir du vecteur de sortie de taille k



Le processus de formation d'une classification multi-classes NN


La plupart des parties du code sont basées sur le code de l'article précédent sur la classification binaire.


Les pièces modifiées sont marquées avec (Modifié) :

  • prétraitement et post-traitement des données
  • fonction d'activation
  • fonction de perte
  • métrique de performances
  • matrice de confusion


Codons un réseau de neurones pour la classification multi-classe avec le framework PyTorch.

Tout d'abord, installez torchmétrie - ce package sera utilisé ultérieurement pour calculer la précision de la classification et la matrice de confusion.


 # used for accuracy metric and confusion matrix !pip install torchmetrics


Importer des packages qui seront utilisés plus tard dans le code

 from sklearn.datasets import make_classification import numpy as np import torch import torchmetrics import matplotlib.pyplot as plt import seaborn as sn import pandas as pd from sklearn.decomposition import PCA


4.1 Créer un ensemble de données

Définissez la variable globale avec le nombre de classes (si vous la définissez sur 2 et obtenez une classification binaire NN qui utilise softmax et la perte d'entropie croisée)


 number_of_classes=4


j'utiliserai sklearn.datasets.make_classification pour générer un ensemble de données de classification binaire :

  • n_samples - est le nombre d'échantillons générés

  • n_features - définit le nombre de dimensions des échantillons générés X

  • n_classes - le nombre de classes dans l'ensemble de données généré. Dans le problème de classification multi-classes, il devrait y avoir plus de 2 classes


L'ensemble de données généré aura X avec la forme [n_samples, n_features] et Y avec la forme [n_samples, ] .

 def get_dataset(n_samples=10000, n_features=20, n_classes=2): # https://scikit-learn.org/stable/modules/generated/sklearn.datasets.make_classification.html#sklearn.datasets.make_classification data_X, data_y = make_classification(n_samples=n_samples, n_features=n_features, n_classes=n_classes, n_informative=n_classes, n_redundant=0, n_clusters_per_class=2, random_state=42, class_sep=4) return data_X, data_y


4.2 Visualisation de l'ensemble de données

Définissez des fonctions pour visualiser et imprimer les statistiques des ensembles de données. La fonction show_dataset utilise APC pour réduire la dimensionnalité de X de n'importe quel nombre jusqu'à 2 pour simplifier la visualisation des données d'entrée X dans le tracé 2D.


 def print_dataset(X, y): print(f'X shape: {X.shape}, min: {X.min()}, max: {X.max()}') print(f'y shape: {y.shape}') print(y[:10]) def show_dataset(X, y, title=''): if X.shape[1] > 2: X_pca = PCA(n_components=2).fit_transform(X) else: X_pca = X fig = plt.figure(figsize=(4, 4)) plt.scatter(x=X_pca[:, 0], y=X_pca[:, 1], c=y, alpha=0.5) # generate colors for all classes colors = plt.cm.rainbow(np.linspace(0, 1, number_of_classes)) # iterate over classes and visualize them with the dedicated color for class_id in range(number_of_classes): class_mask = np.argwhere(y == class_id) X_class = X_pca[class_mask[:, 0]] plt.scatter(x=X_class[:, 0], y=X_class[:, 1], c=np.full((X_class[:, 0].shape[0], 4), colors[class_id]), label=class_id, alpha=0.5) plt.title(title) plt.legend(loc="best", title="Classes") plt.xticks() plt.yticks() plt.show()



4.3 Mise à l'échelle de l'ensemble de données

Mettez à l'échelle les caractéristiques de l'ensemble de données X sur la plage [0,1] avec le détartreur min max. Ceci est généralement fait pour un entraînement plus rapide et plus stable.


 def scale(x_in): return (x_in - x_in.min(axis=0))/(x_in.max(axis=0)-x_in.min(axis=0))


Imprimons les statistiques de l'ensemble de données générées et visualisons-les avec les fonctions ci-dessus.

 X, y = get_dataset(n_classes=number_of_classes) print('before scaling') print_dataset(X, y) show_dataset(X, y, 'before') X_scaled = scale(X) print('after scaling') print_dataset(X_scaled, y) show_dataset(X_scaled, y, 'after')


Les résultats que vous devriez obtenir sont ci-dessous.

 before scaling X shape: (10000, 20), min: -9.549551632357336, max: 9.727761741276673 y shape: (10000,) [0 2 1 2 0 2 0 1 1 2] 

L'ensemble de données avant la mise à l'échelle min-max


 after scaling X shape: (10000, 20), min: 0.0, max: 1.0 y shape: (10000,) [0 2 1 2 0 2 0 1 1 2] 

L'ensemble de données après mise à l'échelle min-max


La mise à l'échelle min-max ne déforme pas les caractéristiques de l'ensemble de données, elle les transforme linéairement dans la plage [0,1]. Le chiffre « ensemble de données après mise à l'échelle min-max » semble être déformé par rapport au chiffre précédent car 20 dimensions sont réduites à 2 par l'algorithme PCA et l'algorithme PCA peut être affecté par la mise à l'échelle min-max.


Créez des chargeurs de données PyTorch. sklearn.datasets.make_classification génère l'ensemble de données sous forme de deux tableaux numpy. Pour créer des chargeurs de données PyTorch, nous devons transformer l'ensemble de données numpy en torch.tensor avec torch.utils.data.TensorDataset.


 def get_data_loaders(dataset, batch_size=32, shuffle=True): data_X, data_y = dataset # https://pytorch.org/docs/stable/data.html#torch.utils.data.TensorDataset torch_dataset = torch.utils.data.TensorDataset(torch.tensor(data_X, dtype=torch.float32), torch.tensor(data_y, dtype=torch.float32)) # https://pytorch.org/docs/stable/data.html#torch.utils.data.random_split train_dataset, val_dataset = torch.utils.data.random_split(torch_dataset, [int(len(torch_dataset)*0.8), int(len(torch_dataset)*0.2)], torch.Generator().manual_seed(42)) # https://pytorch.org/docs/stable/data.html#torch.utils.data.DataLoader loader_train = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=shuffle) loader_val = torch.utils.data.DataLoader(val_dataset, batch_size=batch_size, shuffle=shuffle) return loader_train, loader_val


Tester les chargeurs de données PyTorch

 dataloader_train, dataloader_val = get_data_loaders(get_dataset(n_classes=number_of_classes), batch_size=32) train_batch_0 = next(iter(dataloader_train)) print(f'Batches in the train dataloader: {len(dataloader_train)}, X: {train_batch_0[0].shape}, Y: {train_batch_0[1].shape}') val_batch_0 = next(iter(dataloader_val)) print(f'Batches in the validation dataloader: {len(dataloader_val)}, X: {val_batch_0[0].shape}, Y: {val_batch_0[1].shape}')


Le résultat:

 Batches in the train dataloader: 250, X: torch.Size([32, 20]), Y: torch.Size([32]) Batches in the validation dataloader: 63, X: torch.Size([32, 20]), Y: torch.Size([32])


4.4 Prétraitement et post-traitement des ensembles de données (modifié)

Créez des fonctions de pré et post-traitement. Comme vous l'avez peut-être remarqué auparavant, la forme Y actuelle est [N], nous avons besoin qu'elle soit [N, number_of_classes]. Pour ce faire, nous devons encoder à chaud les valeurs dans le vecteur Y.


Le codage à chaud est un processus de conversion d'index de classe en une représentation binaire où chaque classe est représentée par un vecteur binaire unique.


En d'autres termes : créez un vecteur nul de taille [number_of_classes] et définissez l'élément à la position class_id sur 1, où class_ids {0,1,…,number_of_classes-1} :

0 >> [1. 0. 0. 0.]

1 >> [0. 1. 0. 0.]

2 >> [0. 0. 1. 0.]

2 >> [0. 0. 0. 1.]


Les tenseurs Pytorch peuvent être traités avec torch.nn.function.one_hot et l'implémentation de numpy est très simple. Le vecteur de sortie aura la forme [N,number_of_classes].

 def preprocessing(y, n_classes): ''' one-hot encoding for input numpy array or pytorch Tensor input: y - [N,] numpy array or pytorch Tensor output: [N, n_classes] the same type as input ''' assert type(y)==np.ndarray or torch.is_tensor(y), f'input should be numpy array or torch tensor. Received input is: {type(categorical)}' assert len(y.shape)==1, f'input shape should be [N,]. Received input shape is: {y.shape}' if torch.is_tensor(y): return torch.nn.functional.one_hot(y, num_classes=n_classes) else: categorical = np.zeros([y.shape[0], n_classes]) categorical[np.arange(y.shape[0]), y]=1 return categorical


Pour reconvertir le vecteur codé à chaud en identifiant de classe, nous devons trouver l'index de l'élément max dans le vecteur codé à chaud. Cela peut être fait avec torch.argmax ou np.argmax ci-dessous.

 def postprocessing(categorical): ''' one-hot to classes decoding with .argmax() input: categorical - [N,classes] numpy array or pytorch Tensor output: [N,] the same type as input ''' assert type(categorical)==np.ndarray or torch.is_tensor(categorical), f'input should be numpy array or torch tensor. Received input is: {type(categorical)}' assert len(categorical.shape)==2, f'input shape should be [N,classes]. Received input shape is: {categorical.shape}' if torch.is_tensor(categorical): return torch.argmax(categorical,dim=1) else: return np.argmax(categorical, axis=1)


Testez les fonctions de pré et post-traitement définies.

 y = get_dataset(n_classes=number_of_classes)[1] y_logits = preprocessing(y, n_classes=number_of_classes) y_class = postprocessing(y_logits) print(f'y shape: {y.shape}, y preprocessed shape: {y_logits.shape}, y postprocessed shape: {y_class.shape}') print('Preprocessing does one-hot encoding of class ids.') print('Postprocessing does one-hot decoding of class one-hot encoded class ids.') for i in range(10): print(f'{y[i]} >> {y_logits[i]} >> {y_class[i]}')


Le résultat:

 y shape: (10000,), y preprocessed shape: (10000, 4), y postprocessed shape: (10000,) Preprocessing does one-hot encoding of class ids. Postprocessing does one-hot decoding of one-hot encoded class ids. id>>one-hot encoding>>id 0 >> [1. 0. 0. 0.] >> 0 2 >> [0. 0. 1. 0.] >> 2 1 >> [0. 1. 0. 0.] >> 1 2 >> [0. 0. 1. 0.] >> 2 0 >> [1. 0. 0. 0.] >> 0 2 >> [0. 0. 1. 0.] >> 2 0 >> [1. 0. 0. 0.] >> 0 1 >> [0. 1. 0. 0.] >> 1 1 >> [0. 1. 0. 0.] >> 1 2 >> [0. 0. 1. 0.] >> 2


4.5 Création et formation d'un modèle de classification multi-classes

Cette section montre une implémentation de toutes les fonctions requises pour entraîner un modèle de classification binaire.


4.5.1 Activation Softmax (Modifié)

L'implémentation basée sur PyTorch de la formule softmax

Définition de l'activation Softmax


 def softmax(x): assert len(x.shape)==2, f'input shape should be [N,classes]. Received input shape is: {x.shape}' # Subtract the maximum value for numerical stability # you can find explanation here: https://www.deeplearningbook.org/contents/numerical.html x = x - torch.max(x, dim=1, keepdim=True)[0] # Exponentiate the values exp_x = torch.exp(x) # Sum along the specified dimension sum_exp_x = torch.sum(exp_x, dim=1, keepdim=True) # Compute the softmax return exp_x / sum_exp_x


Testons softmax :

  1. générer un tableau numpy test_input dans la plage [-10, 11] avec l'étape 1

  2. remodelez-le en un tenseur avec une forme [7,3]

  3. traiter test_input avec la fonction softmax implémentée et l'implémentation par défaut de PyTorch torch.nn.function.softmax

  4. comparer les résultats (ils doivent être identiques)

  5. afficher les valeurs softmax et la somme des sept tenseurs [1,3]


 test_input = torch.arange(-10, 11, 1, dtype=torch.float32) test_input = test_input.reshape(-1,3) softmax_output = softmax(test_input) print(f'Input data shape: {test_input.shape}') print(f'input data range: [{test_input.min():.3f}, {test_input.max():.3f}]') print(f'softmax output data range: [{softmax_output.min():.3f}, {softmax_output.max():.3f}]') print(f'softmax output data sum along axis 1: [{softmax_output.sum(axis=1).numpy()}]') softmax_output_pytorch = torch.nn.functional.softmax(test_input, dim=1) print(f'softmax output is the same with pytorch implementation: {(softmax_output_pytorch==softmax_output).all().numpy()}') print('Softmax activation changes values in the chosen axis (1) so that they always sum up to 1:') for i in range(softmax_output.shape[0]): print(f'\t{i}. Sum before softmax: {test_input[i].sum().numpy()} | Sum after softmax: {softmax_output[i].sum().numpy()}') print(f'\t values before softmax: {test_input[i].numpy()}, softmax output values: {softmax_output[i].numpy()}')


Le résultat:

 Input data shape: torch.Size([7, 3]) input data range: [-10.000, 10.000] softmax output data range: [0.090, 0.665] softmax output data sum along axis 1: [[1. 1. 1. 1. 1. 1. 1.]] softmax output is the same with pytorch implementation: True Softmax activation changes values in the chosen axis (1) so that they always sum up to 1: 0. Sum before softmax: -27.0 | Sum after softmax: 1.0 values before softmax: [-10. -9. -8.], softmax output values: [0.09003057 0.24472848 0.66524094] 1. Sum before softmax: -18.0 | Sum after softmax: 1.0 values before softmax: [-7. -6. -5.], softmax output values: [0.09003057 0.24472848 0.66524094] 2. Sum before softmax: -9.0 | Sum after softmax: 1.0 values before softmax: [-4. -3. -2.], softmax output values: [0.09003057 0.24472848 0.66524094] 3. Sum before softmax: 0.0 | Sum after softmax: 1.0 values before softmax: [-1. 0. 1.], softmax output values: [0.09003057 0.24472848 0.66524094] 4. Sum before softmax: 9.0 | Sum after softmax: 1.0 values before softmax: [2. 3. 4.], softmax output values: [0.09003057 0.24472848 0.66524094] 5. Sum before softmax: 18.0 | Sum after softmax: 1.0 values before softmax: [5. 6. 7.], softmax output values: [0.09003057 0.24472848 0.66524094] 6. Sum before softmax: 27.0 | Sum after softmax: 1.0 values before softmax: [ 8. 9. 10.], softmax output values: [0.09003057 0.24472848 0.66524094]


4.5.2 Fonction de perte : entropie croisée (Modifié)

L'implémentation basée sur PyTorch de la formule CE

 def cross_entropy_loss(softmax_logits, labels): # Calculate the cross-entropy loss loss = -torch.sum(labels * torch.log(softmax_logits)) / softmax_logits.size(0) return loss


Test de mise en œuvre CE :


  1. générer un tableau test_input avec la forme [10,5] et des valeurs comprises dans la plage [0,1) avec torche.rand

  2. générer un tableau test_target avec la forme [10,] et des valeurs comprises dans la plage [0,4].

  3. tableau test_target d'encodage à chaud

  4. calculer la perte avec la fonction cross_entropy implémentée et l'implémentation de PyTorch torch.nn.fonctionnel.binary_cross_entropy

  5. comparer les résultats (ils doivent être identiques)


 test_input = torch.rand(10, 5, requires_grad=False) test_target = torch.randint(0, 5, (10,), requires_grad=False) test_target = preprocessing(test_target, n_classes=5).float() print(f'test_input shape: {list(test_input.shape)}, test_target shape: {list(test_target.shape)}') # get loss with the cross_entropy_loss implementation loss = cross_entropy_loss(softmax(test_input), test_target) # get loss with the torch.nn.functional.cross_entropy implementation # !!!torch.nn.functional.cross_entropy applies softmax on input logits # !!!pass it test_input without softmax activation loss_pytorch = torch.nn.functional.cross_entropy(test_input, test_target) print(f'Loss outputs are the same: {(loss==loss_pytorch).numpy()}')


Le résultat attendu :

 test_input shape: [10, 5], test_target shape: [10, 5] Loss outputs are the same: True


4.5.3 Métrique de précision (modifiée)

j'utiliserai torchmétrie mise en œuvre pour calculer la précision en fonction des prédictions du modèle et de la vérité terrain.


Pour créer une métrique de précision de classification multiclasse, deux paramètres sont requis :

  • type de tâche "multiclasse"

  • nombre de classes num_classes


 # https://torchmetrics.readthedocs.io/en/stable/classification/accuracy.html#module-interface accuracy_metric=torchmetrics.classification.Accuracy(task="multiclass", num_classes=number_of_classes) def compute_accuracy(y_pred, y): assert len(y_pred.shape)==2 and y_pred.shape[1] == number_of_classes, 'y_pred shape should be [N, C]' assert len(y.shape)==2 and y.shape[1] == number_of_classes, 'y shape should be [N, C]' return accuracy_metric(postprocessing(y_pred), postprocessing(y))


4.5.4 Modèle NN

Le NN utilisé dans cet exemple est un NN profond avec 2 couches cachées. Les couches d'entrée et cachées utilisent l'activation ReLU et la couche finale utilise la fonction d'activation fournie comme entrée de classe (ce sera la fonction d'activation sigmoïde qui a été implémentée auparavant).


 class ClassifierNN(torch.nn.Module): def __init__(self, loss_function, activation_function, input_dims=2, output_dims=1): super().__init__() self.linear1 = torch.nn.Linear(input_dims, input_dims * 4) self.linear2 = torch.nn.Linear(input_dims * 4, input_dims * 8) self.linear3 = torch.nn.Linear(input_dims * 8, input_dims * 4) self.output = torch.nn.Linear(input_dims * 4, output_dims) self.loss_function = loss_function self.activation_function = activation_function def forward(self, x): x = torch.nn.functional.relu(self.linear1(x)) x = torch.nn.functional.relu(self.linear2(x)) x = torch.nn.functional.relu(self.linear3(x)) x = self.activation_function(self.output(x)) return x


4.5.5 Formation, évaluation et prédiction

Le processus de formation d'une classification multi-classes NN


La figure ci-dessus représente la logique de formation pour un seul lot. Plus tard, la fonction train_epoch sera appelée plusieurs fois (nombre d'époques choisi).


 def train_epoch(model, optimizer, dataloader_train): # set the model to the training mode # https://pytorch.org/docs/stable/generated/torch.nn.Module.html#torch.nn.Module.train model.train() losses = [] accuracies = [] for step, (X_batch, y_batch) in enumerate(dataloader_train): ### forward propagation # get model output and use loss function y_pred = model(X_batch) # get class probabilities with shape [N,1] # apply loss function on predicted probabilities and ground truth loss = model.loss_function(y_pred, y_batch) ### backward propagation # set gradients to zero before backpropagation # https://pytorch.org/docs/stable/generated/torch.optim.Optimizer.zero_grad.html optimizer.zero_grad() # compute gradients # https://pytorch.org/docs/stable/generated/torch.Tensor.backward.html loss.backward() # update weights # https://pytorch.org/docs/stable/optim.html#taking-an-optimization-step optimizer.step() # update model weights # calculate batch accuracy acc = compute_accuracy(y_pred, y_batch) # append batch loss and accuracy to corresponding lists for later use accuracies.append(acc) losses.append(float(loss.detach().numpy())) # compute average epoch accuracy train_acc = np.array(accuracies).mean() # compute average epoch loss loss_epoch = np.array(losses).mean() return train_acc, loss_epoch


La fonction d'évaluation parcourt le chargeur de données PyTorch fourni, calcule la précision du modèle actuel et renvoie la perte moyenne et la précision moyenne.


 def evaluate(model, dataloader_in): # set the model to the evaluation mode # https://pytorch.org/docs/stable/generated/torch.nn.Module.html#torch.nn.Module.eval model.eval() val_acc_epoch = 0 losses = [] accuracies = [] # disable gradient calculation for evaluation # https://pytorch.org/docs/stable/generated/torch.no_grad.html with torch.no_grad(): for step, (X_batch, y_batch) in enumerate(dataloader_in): # get predictions y_pred = model(X_batch) # calculate loss loss = model.loss_function(y_pred, y_batch) # calculate batch accuracy acc = compute_accuracy(y_pred, y_batch) accuracies.append(acc) losses.append(float(loss.detach().numpy())) # compute average accuracy val_acc = np.array(accuracies).mean() # compute average loss loss_epoch = np.array(losses).mean() return val_acc, loss_epoch 


La fonction de prédiction parcourt le chargeur de données fourni, collecte les prédictions de modèle post-traitées (décodées à chaud) et les valeurs de vérité terrain dans [N,1] tableaux PyTorch et renvoie les deux tableaux. Plus tard, cette fonction sera utilisée pour calculer la matrice de confusion et visualiser les prédictions.


 def predict(model, dataloader): # set the model to the evaluation mode # https://pytorch.org/docs/stable/generated/torch.nn.Module.html#torch.nn.Module.eval model.eval() xs, ys = next(iter(dataloader)) y_pred = torch.empty([0, ys.shape[1]]) x = torch.empty([0, xs.shape[1]]) y = torch.empty([0, ys.shape[1]]) # disable gradient calculation for evaluation # https://pytorch.org/docs/stable/generated/torch.no_grad.html with torch.no_grad(): for step, (X_batch, y_batch) in enumerate(dataloader): # get predictions y_batch_pred = model(X_batch) y_pred = torch.cat([y_pred, y_batch_pred]) y = torch.cat([y, y_batch]) x = torch.cat([x, X_batch]) # print(y_pred.shape, y.shape) y_pred = postprocessing(y_pred) y = postprocessing(y) return y_pred, y, x


Pour entraîner le modèle, il suffit d'appeler la fonction train_epoch N fois, où N est le nombre d'époques. La fonction d'évaluation est appelée pour enregistrer la précision actuelle du modèle sur l'ensemble de données de validation. Enfin, le meilleur modèle est mis à jour en fonction de la précision de la validation. La fonction model_train renvoie la meilleure précision de validation et l'historique d'entraînement.


 def model_train(model, optimizer, dataloader_train, dataloader_val, n_epochs=50): best_acc = 0 best_weights = None history = {'loss': {'train': [], 'validation': []}, 'accuracy': {'train': [], 'validation': []}} for epoch in range(n_epochs): # train on dataloader_train acc_train, loss_train = train_epoch(model, optimizer, dataloader_train) # evaluate on dataloader_val acc_val, loss_val = evaluate(model, dataloader_val) print(f'Epoch: {epoch} | Accuracy: {acc_train:.3f} / {acc_val:.3f} | ' + f'loss: {loss_train:.5f} / {loss_val:.5f}') # save epoch losses and accuracies in history dictionary history['loss']['train'].append(loss_train) history['loss']['validation'].append(loss_val) history['accuracy']['train'].append(acc_train) history['accuracy']['validation'].append(acc_val) # Save the best validation accuracy model if acc_val >= best_acc: print(f'\tBest weights updated. Old accuracy: {best_acc:.4f}. New accuracy: {acc_val:.4f}') best_acc = acc_val torch.save(model.state_dict(), 'best_weights.pt') # restore model and return best accuracy model.load_state_dict(torch.load('best_weights.pt')) return best_acc, history


4.5.6 Récupérer l'ensemble de données, créer le modèle et l'entraîner (modifié)

Rassemblons tout et formons le modèle de classification multi-classes.

 ######################################### # Get the dataset X, y = get_dataset(n_classes=number_of_classes) print(f'Generated dataset shape. X:{X.shape}, y:{y.shape}') # change y numpy array shape from [N,] to [N, C] for multi-class classification y = preprocessing(y, n_classes=number_of_classes) print(f'Dataset shape prepared for multi-class classification with softmax activation and CE loss.') print(f'X:{X.shape}, y:{y.shape}') # Get train and validation datal loaders dataloader_train, dataloader_val = get_data_loaders(dataset=(scale(X), y), batch_size=32) # get a batch from dataloader and output intput and output shape X_0, y_0 = next(iter(dataloader_train)) print(f'Model input data shape: {X_0.shape}, output (ground truth) data shape: {y_0.shape}') ######################################### # Create ClassifierNN for multi-class classification problem # input dims: [N, features] # output dims: [N, C] where C is number of classes # activation - softmax to output [,C] probabilities so that their sum(p_1,p_2,...,p_c)=1 # loss - cross-entropy model = ClassifierNN(loss_function=cross_entropy_loss, activation_function=softmax, input_dims=X.shape[1], output_dims=y.shape[1]) ######################################### # create optimizer and train the model on the dataset optimizer = torch.optim.Adam(model.parameters(), lr=0.001) print(f'Model size: {sum([x.reshape(-1).shape[0] for x in model.parameters()])} parameters') print('#'*10) print('Start training') acc, history = model_train(model, optimizer, dataloader_train, dataloader_val, n_epochs=20) print('Finished training') print('#'*10) print("Model accuracy: %.2f%%" % (acc*100))


Le résultat attendu doit être similaire à celui fourni ci-dessous.

 Generated dataset shape. X:(10000, 20), y:(10000,) Dataset shape prepared for multi-class classification with softmax activation and CE loss. X:(10000, 20), y:(10000, 4) Model input data shape: torch.Size([32, 20]), output (ground truth) data shape: torch.Size([32, 4]) Model size: 27844 parameters ########## Start training Epoch: 0 | Accuracy: 0.682 / 0.943 | loss: 0.78574 / 0.37459 Best weights updated. Old accuracy: 0.0000. New accuracy: 0.9435 Epoch: 1 | Accuracy: 0.960 / 0.967 | loss: 0.20272 / 0.17840 Best weights updated. Old accuracy: 0.9435. New accuracy: 0.9668 Epoch: 2 | Accuracy: 0.978 / 0.962 | loss: 0.12004 / 0.17931 Epoch: 3 | Accuracy: 0.984 / 0.979 | loss: 0.10028 / 0.13246 Best weights updated. Old accuracy: 0.9668. New accuracy: 0.9787 Epoch: 4 | Accuracy: 0.985 / 0.981 | loss: 0.08838 / 0.12720 Best weights updated. Old accuracy: 0.9787. New accuracy: 0.9807 Epoch: 5 | Accuracy: 0.986 / 0.981 | loss: 0.08096 / 0.12174 Best weights updated. Old accuracy: 0.9807. New accuracy: 0.9812 Epoch: 6 | Accuracy: 0.986 / 0.981 | loss: 0.07944 / 0.12036 Epoch: 7 | Accuracy: 0.988 / 0.982 | loss: 0.07605 / 0.11773 Best weights updated. Old accuracy: 0.9812. New accuracy: 0.9821 Epoch: 8 | Accuracy: 0.989 / 0.982 | loss: 0.07168 / 0.11514 Best weights updated. Old accuracy: 0.9821. New accuracy: 0.9821 Epoch: 9 | Accuracy: 0.989 / 0.983 | loss: 0.06890 / 0.11409 Best weights updated. Old accuracy: 0.9821. New accuracy: 0.9831 Epoch: 10 | Accuracy: 0.989 / 0.984 | loss: 0.06750 / 0.11128 Best weights updated. Old accuracy: 0.9831. New accuracy: 0.9841 Epoch: 11 | Accuracy: 0.990 / 0.982 | loss: 0.06505 / 0.11265 Epoch: 12 | Accuracy: 0.990 / 0.983 | loss: 0.06507 / 0.11272 Epoch: 13 | Accuracy: 0.991 / 0.985 | loss: 0.06209 / 0.11240 Best weights updated. Old accuracy: 0.9841. New accuracy: 0.9851 Epoch: 14 | Accuracy: 0.990 / 0.984 | loss: 0.06273 / 0.11157 Epoch: 15 | Accuracy: 0.991 / 0.984 | loss: 0.05998 / 0.11029 Epoch: 16 | Accuracy: 0.990 / 0.985 | loss: 0.06056 / 0.11164 Epoch: 17 | Accuracy: 0.991 / 0.984 | loss: 0.05981 / 0.11096 Epoch: 18 | Accuracy: 0.991 / 0.985 | loss: 0.05642 / 0.10975 Best weights updated. Old accuracy: 0.9851. New accuracy: 0.9851 Epoch: 19 | Accuracy: 0.990 / 0.986 | loss: 0.05929 / 0.10821 Best weights updated. Old accuracy: 0.9851. New accuracy: 0.9856 Finished training ########## Model accuracy: 98.56%


4.5.7 Tracer l'historique de formation

 def plot_history(history): fig = plt.figure(figsize=(8, 4), facecolor=(0.0, 1.0, 0.0)) ax = fig.add_subplot(1, 2, 1) ax.plot(np.arange(0, len(history['loss']['train'])), history['loss']['train'], color='red', label='train') ax.plot(np.arange(0, len(history['loss']['validation'])), history['loss']['validation'], color='blue', label='validation') ax.set_title('Loss history') ax.set_facecolor((0.0, 1.0, 0.0)) ax.legend() ax = fig.add_subplot(1, 2, 2) ax.plot(np.arange(0, len(history['accuracy']['train'])), history['accuracy']['train'], color='red', label='train') ax.plot(np.arange(0, len(history['accuracy']['validation'])), history['accuracy']['validation'], color='blue', label='validation') ax.set_title('Accuracy history') ax.legend() fig.tight_layout() ax.set_facecolor((0.0, 1.0, 0.0)) fig.show() 

Historique des pertes et de la précision du train et de la validation


4.6 Évaluer le modèle


4.6.1 Calculer la précision du train et de la validation

 acc_train, _ = evaluate(model, dataloader_train) acc_validation, _ = evaluate(model, dataloader_val) print(f'Accuracy - Train: {acc_train:.4f} | Validation: {acc_validation:.4f}')
 Accuracy - Train: 0.9901 | Validation: 0.9851


4.6.2 Imprimer la matrice de confusion (Modifié)

 val_preds, val_y, _ = predict(model, dataloader_val) print(val_preds.shape, val_y.shape) multiclass_confusion_matrix = torchmetrics.classification.ConfusionMatrix('multiclass', num_classes=number_of_classes) cm = multiclass_confusion_matrix(val_preds, val_y) print(cm) df_cm = pd.DataFrame(cm) plt.figure(figsize = (6,5), facecolor=(0.0,1.0,0.0)) sn.heatmap(df_cm, annot=True, fmt='d') plt.show() 

Matrice de confusion sur l'ensemble de données de validation


4.6.3 Prédictions de tracé et vérité terrain

 val_preds, val_y, val_x = predict(model, dataloader_val) val_preds, val_y, val_x = val_preds.numpy(), val_y.numpy(), val_x.numpy() show_dataset(val_x, val_y,'Ground Truth') show_dataset(val_x, val_preds, 'Predictions') 


Vérité terrain de l'ensemble de données de validation

Prédictions du modèle sur l’ensemble de données de validation


Conclusion

Pour la classification multi-classes, vous devez utiliser l'activation softmax et la perte d'entropie croisée. Quelques modifications de code sont nécessaires pour passer de la classification binaire à la classification multi-classe : fonctions de prétraitement et de post-traitement des données, d'activation et de perte. De plus, vous pouvez résoudre le problème de classification binaire en définissant le nombre de classes sur 2 avec un codage à chaud, softmax et une perte d'entropie croisée.