paint-brush
Mehrklassenklassifizierung: Aktivierungs- und Verlustfunktionen in neuronalen Netzen verstehenvon@owlgrey
2,773 Lesungen
2,773 Lesungen

Mehrklassenklassifizierung: Aktivierungs- und Verlustfunktionen in neuronalen Netzen verstehen

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

Zu lang; Lesen

Um ein neuronales Netzwerk mit mehreren Klassenklassifizierungen aufzubauen, müssen Sie die Softmax-Aktivierungsfunktion auf der letzten Ebene zusammen mit dem Kreuzentropieverlust verwenden. Die endgültige Schichtgröße sollte k sein, wobei k die Anzahl der Klassen ist. Die Klassen-IDs sollten mit One-Hot-Codierung vorverarbeitet werden. Ein solches neuronales Netzwerk gibt Wahrscheinlichkeiten p_i aus, dass die Eingabe zu einer Klasse i gehört. Um die vorhergesagte Klassen-ID zu finden, müssen Sie den Index der maximalen Wahrscheinlichkeit ermitteln.
featured image - Mehrklassenklassifizierung: Aktivierungs- und Verlustfunktionen in neuronalen Netzen verstehen
Dmitrii Matveichev  HackerNoon profile picture


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 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 (Geändert) gekennzeichnet.


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 Aufmerksamkeit ist alles, was Sie brauchen ), da es Eingabewerte in eine Wahrscheinlichkeitsverteilung umwandeln kann (mehr dazu später).


Skalierte Punktproduktaufmerksamkeit (am häufigsten im Multi-Head-Aufmerksamkeitsmodul)



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 {(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:

  • 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)}

  • 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:

  • 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 x nur zu einer Klasse gehören (sich gegenseitig ausschließende Klassen), daher sollte die Summe der Wahrscheinlichkeiten aller Klassen 1 sein: 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 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


    Definition der Sigmoidfunktion


    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:


Softmax-Funktionsdefinition


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

Das Diagramm des Exponenten im Bereich [-10, 10].


Softmax eines Vektors der Größe 21 mit Werten [-10, 10]


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


3.2 Kreuzentropieverlust

Der binäre Kreuzentropieverlust ist definiert als:

Binärer Kreuzentropieverlust


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.


Kreuzentropieverlust


N ist die Anzahl der Eingabeproben, y_i ist die Grundwahrheit und p_i ist die vorhergesagte Wahrscheinlichkeit der Klasse 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 [N,k] haben, wobei N die Anzahl der Eingabeproben und k die Anzahl der Klassen ist – die Klassen-ID muss in einen Vektor mit der Größe k codiert werden
  • Die endgültige lineare Schichtgröße sollte k sein
  • Ausgaben aus der letzten Ebene sollten mit Softmax- Aktivierung verarbeitet werden, um Ausgabewahrscheinlichkeiten zu erhalten
  • Der CE- Verlust sollte auf vorhergesagte Klassenwahrscheinlichkeiten und Grundwahrheitswerte angewendet werden
  • Finden Sie die Ausgabeklassen-ID aus dem Ausgabevektor mit der Größe k



Der Prozess des Trainings einer Mehrklassenklassifikation NN


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:

  • 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 Torchmetrics – Dieses Paket wird später zur Berechnung der Klassifizierungsgenauigkeit und der Verwirrungsmatrix verwendet.


 # 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 sklearn.datasets.make_classification So generieren Sie einen binären Klassifizierungsdatensatz:

  • 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


4.2 Datensatzvisualisierung

Definieren Sie Funktionen zum Visualisieren und Ausdrucken von Datensatzstatistiken. show_dataset-Funktion verwendet PCA 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.


 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] 

Der Datensatz vor der Min-Max-Skalierung


 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] 

Der Datensatz nach der Min-Max-Skalierung


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. sklearn.datasets.make_classification 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.


 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

Softmax-Aktivierungsdefinition


 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:

  1. Generieren Sie mit Schritt 1 ein test_input- Numpy-Array im Bereich [-10, 11].

  2. Formen Sie es in einen Tensor mit der Form [7,3] um.

  3. Verarbeiten Sie test_input mit der implementierten Softmax- Funktion und der PyTorch-Standardimplementierung Torch.nn.Functional.Softmax

  4. Vergleichen Sie die Ergebnisse (sie sollten identisch sein)

  5. 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:


  1. Erzeugen Sie ein test_input- Array mit der Form [10,5] und Werten im Bereich [0,1) mit Torch.rand

  2. Generieren Sie ein test_target- Array mit der Form [10,] und Werten im Bereich [0,4].

  3. One-Hot-Codierung des test_target- Arrays

  4. Berechnen Sie den Verlust mit der implementierten Funktion cross_entropy und der PyTorch-Implementierung Torch.nn.Functional.binary_cross_entropy

  5. 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 Torchmetrics Implementierung zur Berechnung der Genauigkeit basierend auf Modellvorhersagen und Ground Truth.


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

Der Prozess des Trainings einer Mehrklassenklassifikation NN


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


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() 

Zug- und Validierungsverlust- und Genauigkeitsverlauf


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() 

Verwirrungsmatrix für den Validierungsdatensatz


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') 


Grundwahrheit des Validierungsdatensatzes

Modellvorhersagen für den Validierungsdatensatz


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.