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
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.
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).
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.
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 :
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 :
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
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 :
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 |
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 |
La perte d'entropie croisée binaire est définie comme :
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.
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 .
Pour implémenter une classification probabiliste multi-classes NN il nous faut :
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é) :
Codons un réseau de neurones pour la classification multi-classe avec le framework PyTorch.
Tout d'abord, installez
# 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
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
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
Définissez des fonctions pour visualiser et imprimer les statistiques des ensembles de données. La fonction show_dataset utilise
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()
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]
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]
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.
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])
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
Cette section montre une implémentation de toutes les fonctions requises pour entraîner un modèle de classification binaire.
L'implémentation basée sur PyTorch de la formule 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 :
générer un tableau numpy test_input dans la plage [-10, 11] avec l'étape 1
remodelez-le en un tenseur avec une forme [7,3]
traiter test_input avec la fonction softmax implémentée et l'implémentation par défaut de PyTorch torch.nn.function.softmax
comparer les résultats (ils doivent être identiques)
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]
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 :
générer un tableau test_input avec la forme [10,5] et des valeurs comprises dans la plage [0,1) avec
générer un tableau test_target avec la forme [10,] et des valeurs comprises dans la plage [0,4].
tableau test_target d'encodage à chaud
calculer la perte avec la fonction cross_entropy implémentée et l'implémentation de PyTorch
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
j'utiliserai
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))
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
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
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%
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()
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
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()
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')
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.