Mein vorheriger Beitrag formuliert das Klassifizierungsproblem und unterteilt es in drei Typen (binär, Multi-Klasse und Multi-Label) und beantwortet die Frage „Welche Aktivierungs- und Verlustfunktionen müssen Sie verwenden, um eine binäre Klassifizierungsaufgabe zu lösen?“.
In diesem Beitrag werde ich dieselbe Frage beantworten, jedoch für die Klassifizierungsaufgabe mit mehreren Klassen, und Ihnen eine zur Verfügung stellen
Welche Aktivierungs- und Verlustfunktionen müssen Sie verwenden, um eine Klassifizierungsaufgabe mit mehreren Klassen zu lösen?
Der bereitgestellte Code basiert weitgehend auf der Implementierung der Binärklassifizierung, da Sie nur sehr wenige Änderungen an Ihrem Code und NN vornehmen müssen, um von der Binärklassifizierung zur Mehrklassenklassifizierung zu wechseln. Die geänderten Codeblöcke sind zur einfacheren Navigation mit (Geändert) gekennzeichnet.
Wie später gezeigt wird, ist die Aktivierungsfunktion für die Klassifizierung mehrerer Klassen die Softmax-Aktivierung. Softmax wird häufig in verschiedenen NN-Architekturen außerhalb der Mehrklassenklassifizierung verwendet. Softmax ist beispielsweise das Herzstück des Multi-Head-Aufmerksamkeitsblocks, der in Transformer-Modellen verwendet wird (siehe Aufmerksamkeit ist alles, was Sie brauchen ), da es Eingabewerte in eine Wahrscheinlichkeitsverteilung umwandeln kann (mehr dazu später).
Wenn Sie die Motivation hinter der Anwendung von Softmax-Aktivierung und CE-Verlust zur Lösung von Klassifizierungsproblemen mit mehreren Klassen kennen, werden Sie in der Lage sein, viel kompliziertere NN-Architekturen und Verlustfunktionen zu verstehen und zu implementieren.
Das Klassifizierungsproblem mehrerer Klassen kann als Satz von Stichproben {(x_1, y_1), (x_2, y_2),...,(x_n, y_n)} dargestellt werden, wobei x_i ein m-dimensionaler Vektor ist, der Merkmale der Stichprobe enthält i und y_i ist die Klasse, zu der x_i gehört. Dabei kann die Bezeichnung y_i einen der k- Werte annehmen, wobei k die Anzahl der Klassen größer als 2 ist. Das Ziel besteht darin, ein Modell zu erstellen, das die Bezeichnung y_i für jede Eingabestichprobe x_i vorhersagt.
Beispiele für Aufgaben, die als Mehrklassen-Klassifizierungsprobleme behandelt werden können:
In der Mehrklassenklassifizierung erhalten Sie:
eine Menge von Stichproben {(x_1, y_1), (x_2, y_2),...,(x_n, y_n)}
x_i ist ein m-dimensionaler Vektor, der Merkmale der Probe i enthält
y_i ist die Klasse, zu der x_i gehört und kann einen der k Werte annehmen, wobei k>2 die Anzahl der Klassen ist.
Um ein neuronales Multiklassen-Klassifikationsnetzwerk als probabilistischen Klassifikator aufzubauen, benötigen wir:
Die letzte lineare Schicht eines neuronalen Netzwerks gibt einen Vektor von „Rohausgabewerten“ aus. Bei der Klassifizierung stellen die Ausgabewerte die Sicherheit des Modells dar, dass die Eingabe zu einer der k Klassen gehört. Wie bereits erwähnt, muss die Ausgabeschicht die Größe k haben und die Ausgabewerte sollten Wahrscheinlichkeiten p_i für jede der k Klassen und SUM(p_i)=1 darstellen.
Der Artikel zur binären Klassifizierung verwendet die Sigmoidaktivierung, um NN-Ausgabewerte in Wahrscheinlichkeiten umzuwandeln. Versuchen wir, Sigmoid auf k Ausgabewerte im Bereich [-3, 3] anzuwenden und zu prüfen, ob Sigmoid die zuvor aufgeführten Anforderungen erfüllt:
k Ausgabewerte sollten im Bereich (0,1) liegen, wobei k die Anzahl der Klassen ist
Die Summe der k Ausgabewerte sollte gleich 1 sein
Der vorherige Artikel zeigt, dass die Sigmoidfunktion Eingabewerte einem Bereich (0,1) zuordnet. Mal sehen, ob die Sigmoidaktivierung die zweite Anforderung erfüllt. In der Beispieltabelle unten habe ich einen Vektor der Größe k (k=7) mit Sigmoidaktivierung verarbeitet und alle diese Werte summiert – die Summe dieser 7 Werte beträgt 3,5. Eine einfache Möglichkeit, dies zu beheben, wäre die Division aller k- Werte durch ihre Summe.
Eingang | -3 | -2 | -1 | 0 | 1 | 2 | 3 | SUMME |
---|---|---|---|---|---|---|---|---|
Sigmoid-Ausgabe | 0,04743 | 0,11920 | 0,26894 | 0,50000 | 0,73106 | 0,88080 | 0,95257 | 3,5000 |
Eine andere Möglichkeit wäre, den Exponenten des Eingabewerts zu nehmen und ihn durch die Summe der Exponenten aller Eingabewerte zu dividieren:
Die Softmax-Funktion wandelt einen Vektor reeller Zahlen in einen Wahrscheinlichkeitsvektor um. Jede Wahrscheinlichkeit im Ergebnis liegt im Bereich (0,1) und die Summe der Wahrscheinlichkeiten ist 1.
Eingang | -3 | -2 | -1 | 0 | 1 | 2 | 3 | SUMME |
---|---|---|---|---|---|---|---|---|
Softmax | 0,00157 | 0,00426 | 0,01159 | 0,03150 | 0,08563 | 0,23276 | 0,63270 | 1 |
Bei der Arbeit mit Softmax müssen Sie eines beachten: Der Ausgabewert p_i hängt von allen Werten im Eingabearray ab, da wir ihn durch die Summe der Exponenten aller Werte dividieren. Die folgende Tabelle zeigt dies: Zwei Eingabevektoren haben drei gemeinsame Werte {1, 3, 4}, aber die Ausgabe-Softmax-Werte unterscheiden sich, weil das zweite Element unterschiedlich ist (2 und 4).
Eingabe 1 | 1 | 2 | 3 | 4 |
---|---|---|---|---|
Softmax 1 | 0,0321 | 0,0871 | 0,2369 | 0,6439 |
Eingabe 2 | 1 | 4 | 3 | 4 |
Softmax 2 | 0,0206 | 0,4136 | 0,1522 | 0,4136 |
Der binäre Kreuzentropieverlust ist definiert als:
Bei der binären Klassifizierung gibt es zwei Ausgabewahrscheinlichkeiten p_i und (1-p_i) und Grundwahrheitswerte y_i und (1-y_i).
Das Klassifizierungsproblem mehrerer Klassen verwendet die Verallgemeinerung des BCE-Verlusts für N Klassen: Kreuzentropieverlust.
N ist die Anzahl der Eingabeproben, y_i ist die Grundwahrheit und p_i ist die vorhergesagte Wahrscheinlichkeit der Klasse i .
Um eine probabilistische Mehrklassenklassifizierung NN zu implementieren, benötigen wir:
Die meisten Teile des Codes basieren auf dem Code aus dem vorherigen Artikel zur binären Klassifizierung.
Die geänderten Teile sind mit (Geändert) gekennzeichnet:
Lassen Sie uns mit dem PyTorch-Framework ein neuronales Netzwerk für die Klassifizierung mehrerer Klassen codieren.
Zuerst installieren
# used for accuracy metric and confusion matrix !pip install torchmetrics
Importieren Sie Pakete, die später im Code verwendet werden
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
Legen Sie die globale Variable mit der Anzahl der Klassen fest (wenn Sie sie auf 2 setzen und eine binäre Klassifizierungs-NN erhalten, die Softmax und Cross-Entropy-Verlust verwendet).
number_of_classes=4
ich werde benützen
n_samples – ist die Anzahl der generierten Samples
n_features – legt die Anzahl der Dimensionen der generierten Proben X fest
n_classes – die Anzahl der Klassen im generierten Datensatz. Beim Klassifizierungsproblem mit mehreren Klassen sollte es mehr als zwei Klassen geben
Der generierte Datensatz hat X mit der Form [n_samples, n_features] und Y mit der Form [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
Definieren Sie Funktionen zum Visualisieren und Ausdrucken von Datensatzstatistiken. show_dataset-Funktion verwendet
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()
Skalieren Sie die Datensatzmerkmale X auf den Bereich [0,1] mit dem Min-Max-Skalierer. Dies geschieht normalerweise für ein schnelleres und stabileres Training.
def scale(x_in): return (x_in - x_in.min(axis=0))/(x_in.max(axis=0)-x_in.min(axis=0))
Lassen Sie uns die generierten Datensatzstatistiken ausdrucken und mit den oben genannten Funktionen visualisieren.
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')
Die Ausgaben, die Sie erhalten sollten, sind unten aufgeführt.
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]
Die Min-Max-Skalierung verzerrt die Merkmale des Datensatzes nicht, sondern transformiert sie linear in den Bereich [0,1]. Die Abbildung „Datensatz nach Min-Max-Skalierung“ scheint im Vergleich zur vorherigen Abbildung verzerrt zu sein, da 20 Dimensionen durch den PCA-Algorithmus auf 2 reduziert werden und der PCA-Algorithmus durch die Min-Max-Skalierung beeinflusst werden kann.
Erstellen Sie PyTorch-Datenlader.
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
Testen Sie PyTorch-Datenlader
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}')
Die Ausgabe:
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])
Erstellen Sie Vor- und Nachbearbeitungsfunktionen. Wie Sie vielleicht bereits bemerkt haben, ist die aktuelle Y-Form [N], wir brauchen sie also [N,Anzahl_der_Klassen]. Dazu müssen wir die Werte im Y-Vektor einmalig kodieren.
Bei der One-Hot-Codierung handelt es sich um einen Prozess der Konvertierung von Klassenindizes in eine binäre Darstellung, bei der jede Klasse durch einen eindeutigen binären Vektor dargestellt wird.
Mit anderen Worten: Erstellen Sie einen Nullvektor mit der Größe [number_of_classes] und setzen Sie das Element an der Position class_id auf 1, wobei class_ids {0,1,…,number_of_classes-1}:
0 >> [1. 0. 0. 0.]
1 >> [0. 100.]
2 >> [0. 0. 1. 0.]
2 >> [0. 0. 0. 1.]
Pytorch-Tensoren können mit Torch.nn.Functional.one_hot verarbeitet werden und die Numpy-Implementierung ist sehr einfach. Der Ausgabevektor hat die Form [N,Anzahl_der_Klassen].
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
Um den One-Hot-codierten Vektor zurück in die Klassen-ID umzuwandeln, müssen wir den Index des Max-Elements im One-Hot-codierten Vektor finden. Dies kann mit Torch.argmax oder np.argmax unten erfolgen.
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)
Testen Sie die definierten Vor- und Nachverarbeitungsfunktionen.
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]}')
Die Ausgabe:
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
Dieser Abschnitt zeigt eine Implementierung aller Funktionen, die zum Trainieren eines binären Klassifizierungsmodells erforderlich sind.
Die PyTorch-basierte Implementierung der Softmax-Formel
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
Lassen Sie uns Softmax testen:
Generieren Sie mit Schritt 1 ein test_input- Numpy-Array im Bereich [-10, 11].
Formen Sie es in einen Tensor mit der Form [7,3] um.
Verarbeiten Sie test_input mit der implementierten Softmax- Funktion und der PyTorch-Standardimplementierung Torch.nn.Functional.Softmax
Vergleichen Sie die Ergebnisse (sie sollten identisch sein)
Gibt Softmax-Werte und Summe für alle sieben [1,3]-Tensoren aus
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()}')
Die Ausgabe:
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]
Die PyTorch-basierte Implementierung der CE-Formel
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
Testen Sie die CE-Implementierung:
Erzeugen Sie ein test_input- Array mit der Form [10,5] und Werten im Bereich [0,1) mit
Generieren Sie ein test_target- Array mit der Form [10,] und Werten im Bereich [0,4].
One-Hot-Codierung des test_target- Arrays
Berechnen Sie den Verlust mit der implementierten Funktion cross_entropy und der PyTorch-Implementierung
Vergleichen Sie die Ergebnisse (sie sollten identisch sein)
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()}')
Die erwartete Ausgabe:
test_input shape: [10, 5], test_target shape: [10, 5] Loss outputs are the same: True
ich werde benützen
Um eine Metrik für die Klassifizierungsgenauigkeit mehrerer Klassen zu erstellen, sind zwei Parameter erforderlich:
Aufgabentyp „Multiclass“
Anzahl der Klassen 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))
Das in diesem Beispiel verwendete NN ist ein tiefes NN mit zwei verborgenen Schichten. Eingabe- und verborgene Ebenen verwenden die ReLU-Aktivierung und die letzte Ebene verwendet die als Klasseneingabe bereitgestellte Aktivierungsfunktion (es handelt sich um die Sigmoid-Aktivierungsfunktion, die zuvor implementiert wurde).
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
Die obige Abbildung zeigt die Trainingslogik für einen einzelnen Batch. Später wird die Funktion train_epoch mehrmals aufgerufen (gewählte Anzahl von Epochen).
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
Die Bewertungsfunktion iteriert über den bereitgestellten PyTorch-Datenlader, berechnet die aktuelle Modellgenauigkeit und gibt durchschnittlichen Verlust und durchschnittliche Genauigkeit zurück.
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
Die Vorhersagefunktion iteriert über den bereitgestellten Datenlader, sammelt nachverarbeitete (One-Hot-dekodierte) Modellvorhersagen und Grundwahrheitswerte in [N,1] PyTorch-Arrays und gibt beide Arrays zurück. Später wird diese Funktion verwendet, um die Verwirrungsmatrix zu berechnen und Vorhersagen zu visualisieren.
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
Um das Modell zu trainieren, müssen wir die Funktion train_epoch nur N-mal aufrufen, wobei N die Anzahl der Epochen ist. Die Evaluierungsfunktion wird aufgerufen, um die aktuelle Modellgenauigkeit im Validierungsdatensatz zu protokollieren. Schließlich wird das beste Modell basierend auf der Validierungsgenauigkeit aktualisiert. Die Funktion model_train gibt die beste Validierungsgenauigkeit und den besten Trainingsverlauf zurück.
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
Lassen Sie uns alles zusammenfügen und das Klassifizierungsmodell für mehrere Klassen trainieren.
######################################### # 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))
Die erwartete Ausgabe sollte der unten angegebenen ähneln.
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')
Für die Klassifizierung mehrerer Klassen müssen Sie Softmax-Aktivierung und Kreuzentropieverlust verwenden. Für den Wechsel von der Binärklassifizierung zur Mehrklassenklassifizierung sind einige Codeänderungen erforderlich: Datenvor- und -nachverarbeitung, Aktivierung und Verlustfunktionen. Darüber hinaus können Sie das Problem der binären Klassifizierung lösen, indem Sie die Anzahl der Klassen mit One-Hot-Codierung, Softmax und Kreuzentropieverlust auf 2 setzen.