Mein 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?“. vorheriger Beitrag In diesem Beitrag werde ich dieselbe Frage beantworten, jedoch für die Klassifizierungsaufgabe mit mehreren Klassen, und Ihnen eine zur Verfügung stellen . Beispiel einer Pytorch-Implementierung in Google Colab 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 gekennzeichnet. (Geändert) 1 Warum ist es wichtig, die Aktivierungsfunktion und den Verlust zu verstehen, die für die Klassifizierung mehrerer Klassen verwendet werden? 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 ), da es Eingabewerte in eine Wahrscheinlichkeitsverteilung umwandeln kann (mehr dazu später). Aufmerksamkeit ist alles, was Sie brauchen 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. 2 Formulierung des Mehrklassen-Klassifizierungsproblems Das Klassifizierungsproblem mehrerer Klassen kann als Satz von Stichproben dargestellt werden, wobei ein m-dimensionaler Vektor ist, der Merkmale der Stichprobe enthält und ist die Klasse, zu der gehört. Dabei kann die Bezeichnung einen der 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 vorhersagt. {(x_1, y_1), (x_2, y_2),...,(x_n, y_n)} x_i i y_i x_i y_i k- x_i Beispiele für Aufgaben, die als Mehrklassen-Klassifizierungsprobleme behandelt werden können: Medizinische Diagnose – Diagnose eines Patienten mit einer von mehreren Krankheiten auf der Grundlage der bereitgestellten Daten (Krankengeschichte, Testergebnisse, Symptome) Produktkategorisierung – automatische Produktklassifizierung für E-Commerce-Plattformen Wettervorhersage – Klassifizierung des zukünftigen Wetters als sonnig, bewölkt, regnerisch usw Kategorisierung von Filmen, Musik und Artikeln in verschiedene Genres Klassifizierung von Online-Kundenbewertungen in Kategorien wie Produkt-Feedback, Service-Feedback, Beschwerden usw 3 Aktivierungs- und Verlustfunktionen für die Klassifizierung mehrerer Klassen In der Mehrklassenklassifizierung erhalten Sie: eine Menge von Stichproben {(x_1, y_1), (x_2, y_2),...,(x_n, y_n)} ist ein m-dimensionaler Vektor, der Merkmale der Probe enthält x_i i ist die Klasse, zu der gehört und kann einen der Werte annehmen, wobei die Anzahl der Klassen ist. y_i x_i k k>2 Um ein neuronales Multiklassen-Klassifikationsnetzwerk als probabilistischen Klassifikator aufzubauen, benötigen wir: eine vollständig verbundene Ausgangsschicht mit einer Größe von k Ausgabewerte sollten im Bereich [0,1] liegen Die Summe der Ausgabewerte sollte gleich 1 sein. Bei der Mehrklassenklassifizierung kann jede Eingabe nur zu einer Klasse gehören (sich gegenseitig ausschließende Klassen), daher sollte die Summe der Wahrscheinlichkeiten aller Klassen 1 sein: x . SUM(p_0,…,p_k )=1 eine Verlustfunktion, die den niedrigsten Wert hat, wenn die Vorhersage und die Grundwahrheit gleich sind 3.1 Die Softmax-Aktivierungsfunktion 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 Klassen gehört. Wie bereits erwähnt, muss die Ausgabeschicht die Größe haben und die Ausgabewerte sollten Wahrscheinlichkeiten für jede der k Klassen und darstellen. k k p_i SUM(p_i)=1 Der Artikel zur verwendet die Sigmoidaktivierung, um NN-Ausgabewerte in Wahrscheinlichkeiten umzuwandeln. Versuchen wir, Sigmoid auf Ausgabewerte im Bereich [-3, 3] anzuwenden und zu prüfen, ob Sigmoid die zuvor aufgeführten Anforderungen erfüllt: binären Klassifizierung k Ausgabewerte sollten im Bereich (0,1) liegen, wobei die Anzahl der Klassen ist k k Die Summe der Ausgabewerte sollte gleich 1 sein k 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=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 Werte durch ihre Summe. k k- 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 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). p_i 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 3.2 Kreuzentropieverlust Der binäre Kreuzentropieverlust ist definiert als: Bei der binären Klassifizierung gibt es zwei Ausgabewahrscheinlichkeiten und und Grundwahrheitswerte und p_i (1-p_i) y_i (1-y_i). Das Klassifizierungsproblem mehrerer Klassen verwendet die Verallgemeinerung des BCE-Verlusts für N Klassen: Kreuzentropieverlust. N ist die Anzahl der Eingabeproben, ist die Grundwahrheit und ist die vorhergesagte Wahrscheinlichkeit der Klasse . y_i p_i i 4 NN-Beispiel für die Mehrklassenklassifizierung mit PyTorch Um eine probabilistische Mehrklassenklassifizierung NN zu implementieren, benötigen wir: Grundwahrheit und Vorhersagen sollten die Dimensionen haben, wobei die Anzahl der Eingabeproben und die Anzahl der Klassen ist – die Klassen-ID muss in einen Vektor mit der Größe codiert werden [N,k] N k k Die endgültige lineare Schichtgröße sollte sein k Ausgaben aus der letzten Ebene sollten mit Aktivierung verarbeitet werden, um Ausgabewahrscheinlichkeiten zu erhalten Softmax- Der Verlust sollte auf vorhergesagte Klassenwahrscheinlichkeiten und Grundwahrheitswerte angewendet werden CE- Finden Sie die Ausgabeklassen-ID aus dem Ausgabevektor mit der Größe k Die meisten Teile des Codes basieren auf dem Code aus dem vorherigen Artikel zur binären Klassifizierung. Die geänderten Teile sind mit gekennzeichnet: (Geändert) Datenvorverarbeitung und -nachverarbeitung Aktivierungsfunktion verlustfunktion Leistungsmessung Verwirrung Matrix Lassen Sie uns mit dem PyTorch-Framework ein neuronales Netzwerk für die Klassifizierung mehrerer Klassen codieren. Zuerst installieren – Dieses Paket wird später zur Berechnung der Klassifizierungsgenauigkeit und der Verwirrungsmatrix verwendet. Torchmetrics # 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 4.1 Datensatz erstellen 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 So generieren Sie einen binären Klassifizierungsdatensatz: sklearn.datasets.make_classification – ist die Anzahl der generierten Samples n_samples – legt die Anzahl der Dimensionen der generierten Proben X fest n_features – die Anzahl der Klassen im generierten Datensatz. Beim Klassifizierungsproblem mit mehreren Klassen sollte es mehr als zwei Klassen geben n_classes Der generierte Datensatz hat X mit der Form und Y mit der Form . [n_samples, n_features] [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 Datensatzvisualisierung Definieren Sie Funktionen zum Visualisieren und Ausdrucken von Datensatzstatistiken. show_dataset-Funktion verwendet um die Dimensionalität von X von einer beliebigen Zahl auf 2 zu reduzieren, um die Visualisierung der Eingabedaten X im 2D-Diagramm zu vereinfachen. PCA 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 Datensatz-Skalierer 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. generiert den Datensatz als zwei Numpy-Arrays. Um PyTorch-Datenlader zu erstellen, müssen wir den Numpy-Datensatz mit Torch.utils.data.TensorDataset in Torch.tensor umwandeln. sklearn.datasets.make_classification 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]) 4.4 Vor- und Nachbearbeitung von Datensätzen (geändert) 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 4.5 Erstellen und Trainieren eines Klassifizierungsmodells mit mehreren Klassen Dieser Abschnitt zeigt eine Implementierung aller Funktionen, die zum Trainieren eines binären Klassifizierungsmodells erforderlich sind. 4.5.1 Softmax-Aktivierung (geändert) 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 Numpy-Array im Bereich [-10, 11]. test_input- Formen Sie es in einen Tensor mit der Form [7,3] um. Verarbeiten Sie mit der implementierten Funktion und der PyTorch-Standardimplementierung test_input Softmax- 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] 4.5.2 Verlustfunktion: Kreuzentropie (geändert) 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 Array mit der Form [10,5] und Werten im Bereich [0,1) mit test_input- Torch.rand Generieren Sie ein Array mit der Form [10,] und Werten im Bereich [0,4]. test_target- One-Hot-Codierung Arrays des test_target- Berechnen Sie den Verlust mit der implementierten Funktion und der PyTorch-Implementierung cross_entropy Torch.nn.Functional.binary_cross_entropy 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 4.5.3 Genauigkeitsmetrik (geändert) ich werde benützen Implementierung zur Berechnung der Genauigkeit basierend auf Modellvorhersagen und Ground Truth. Torchmetrics 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)) 4.5.4 NN-Modell 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 4.5.5 Training, Bewertung und Vorhersage 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 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. Vorhersagefunktion 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 nur N-mal aufrufen, wobei N die Anzahl der Epochen ist. Die wird aufgerufen, um die aktuelle Modellgenauigkeit im Validierungsdatensatz zu protokollieren. Schließlich wird das beste Modell basierend auf der Validierungsgenauigkeit aktualisiert. Die Funktion gibt die beste Validierungsgenauigkeit und den besten Trainingsverlauf zurück. train_epoch Evaluierungsfunktion model_train 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 Den Datensatz abrufen, das Modell erstellen und trainieren (geändert) 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% 4.5.7 Trainingsverlauf darstellen 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() 4.6 Bewerten Sie das Modell 4.6.1 Berechnen Sie die Zug- und Validierungsgenauigkeit 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 Verwirrungsmatrix drucken (geändert) 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() 4.6.3 Plotvorhersagen und Grundwahrheit 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') Abschluss 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.