Schulung Session 5 | Modellentwicklung & -training

In dieser Session wollen wir uns mit der Modellentwicklung und dem Trainieren eines Modells befassen. Dafür schauen wir uns ein einfaches Klassifikationsbeispiel an.

Inhalte

  1. Datensätze
  2. Modellauswahl
  3. Training
  4. Weitere Trainingstechniken

Datensätze

Vor dem Training eines ML-Modells müssen alle Daten in Trainings- und Testdaten aufgespaltet werden.

Warum müssen wir unsere Daten aufteilen?

Ziel: Ein möglichst generalisierendes Modell auf Basis der vorhandenen Daten

Wie erreichen wir das?

Damit das Modell später auch die richtigen Entscheidungen für ungesehene Daten trifft, nehmen wir einen Teil der Daten beiseite. Diese Daten verwenden wir dann zur Evaluierung des Modells. Dadurch können wir überprüfen, ob das Modell generalisiert.

Zunächst müssen wir dafür einen passenden Datensatz herunter laden. Zu Übungszwecken nehmen wir hierfür den bekannten Iris-Datensatz. Der Iris-flower-Datensatz besteht aus jeweils 50 Beobachtungen dreier Arten von Schwertlilien (Iris) (Iris Setosa, Iris Virginica und Iris Versicolor), an denen jeweils vier Attribute der Blüten erhoben wurden: Die Länge und die Breite des Sepalum (Kelchblatt) und des Petalum (Kronblatt). (Quelle: wikipedia.org)

Zunächst laden wir das Datenset mit dem uns bekannten Framework pandas.

In [ ]:
import pandas as pd
import numpy as np
In [ ]:
file_path = "iris.csv"
df = pd.read_csv(file_path)
df.sample(5).T

Weitere Möglichkeiten um Daten zu erhalten

Andere beliebte Datensätze

  • MNIST:
    • 28x28 Pixel Graustufenbilder
    • Handgeschriebene Ziffern
    • Ziel: Bildklassifikation
  • Fashion MNIST:
    • Wie MNIST
    • Kleidungsstücke statt Ziffern.
    • Von Zalando
    • Ziel: Bildklassifikation
  • Boston Housing:
    • Informationen über Häuser in Boston
    • Vorhersage des Hauspreises
    • Ziel: Regression
  • ImageNet:
    • 14+ Mio Bilder aus 20.000+ Kategorien
    • Jährlicher Wettbewerb ImageNet Large Scale Visual Recognition Challenge (ILSVRC)
    • Ziel: Bildklassifikation
  • $\dots$

Bekannte Datensätzen werden oft von den ML Frameworks wie z.B. sklearn, tensorflow bereitgestellt. Dadurch kann der eigene Prototyp schnell getestet werden.

In [ ]:
from sklearn.datasets import load_iris
In [ ]:
# return_X_y:
# - True: return only data and targets (labels)
# - False: return dictionary with data, targets, target_names etc.

# as_frame: 
# - True: return as pandas dataframe
# - False: return as NumPy arrays
X,y = load_iris(return_X_y=True, as_frame=True)
X.sample(5)

Fashion MNIST

In [ ]:
from tensorflow.keras.datasets import fashion_mnist
import matplotlib.pyplot as plt
In [ ]:
class_names = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat',
               'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot']
In [ ]:
(train_images, train_labels), (test_images,
                               test_labels) = fashion_mnist.load_data()
plt.figure(figsize=(10,10))
for i in range(25):
    plt.subplot(5,5,i+1)
    plt.axis("off")
    plt.grid(False)
    plt.imshow(train_images[i], cmap=plt.cm.binary)
    label = class_names[train_labels[i]]
    plt.title(label)
plt.show()

Wie splitten wir unsere Daten?

TODO 1: Was schätzt ihr? In welchem Verhätlnis werden Trainings- und Testdaten normalerweise gesplittet?

Wir unterteilen die Daten klassischerweise folgendermaßen:

  • _% für Training
  • _% Validierungsdaten.

Die Daten werden dabei zufällig aus dem vorhandenen Datensatz gezogen. Vereinzelte Datensätze für Benchmarks oder Wettbewerbe geben einen Train-Test-Split bereits vor. Dies wird in der Realität aber so nicht vorkommen.

Was passiert wenn wir unsere Daten nicht splitten?

Wenn wir unsere Daten nicht splitten haben wir keine Möglichkeit zu überprüfen, ob und wie unser Modell auf ungesehenen Daten funktioniert. Zum Beispiel können wir nicht feststellen, ob unser Modell "overfittet". Overfitting ist, wenn sich unser Modell zu sehr auf die Trainingsdaten spezialisiert und nicht generalisiert. Das heißt es funktioniert sehr gut auf Daten, die es bereits kennt, aber sehr schlecht auf Daten, die es noch nie gesehen hat.

Source: [What is underfitting and overfitting in machine learning and how to deal with it.](https://medium.com/greyatom/what-is-underfitting-and-overfitting-in-machine-learning-and-how-to-deal-with-it-6803a989c76)

Da wir dies verhindern wollen, sollten wir jetzt unsere Daten in Test- und Trainingsset aufteilen.

TODO 2: Implementiere eine Funktion train_test_split, die aus einem DataFrame einen Train und einen Test Dataframe macht. Das Prozentsatz an Trainingsdaten wird als Parameter split angegeben, z.B. split=0.8 bedeutet, dass 80% des Datensatz zum Training verwendet werden.

Tipp:

  • Eine mögliche Vorgehensweise ist mit df.sample zufällig Elemente aus dem ursprünglichen DataFrame zu nehmen und als Trainings-DataFrame zu verwenden. Hier kann für das Argument frac der Wert von split übergeben werden.
  • Um nun die verbleibenden Elemente des DataFrames als Testdaten zu nehmen, entfernen wir alle Elemente aus dem ursprünglichen DataFrame, die schon im TrainingsFrame sind.
    • Die Indices diese Elemente erhalten wir mit df.index
    • Das entfernen der Elemente klappt mit df.drop(index). Hier wird also eine Liste von Indices übergeben
In [ ]:
# Complete this function
def train_test_split(df, split):
    
    df_train = pd.DataFrame()
    df_test = pd.DataFrame()
    
    return df_train, df_test
In [ ]:
train_split = 0.8
df_train, df_test = train_test_split(df, train_split)
print(f"Train samples: {df_train.size}\nValidation samples: {df_test.size}")
len(df_train), len(df_test), df_train.size/df.size

Visualisierung der Trainingsdaten

Für ein schnelle Visualisierung des Train-Test-Splits verwenden wir die Bibliothek matplotib.

In [ ]:
import matplotlib.pyplot as plt # common import shortening
In [ ]:
train_counts = df_train["species"].value_counts(normalize=True)
test_counts = df_test["species"].value_counts(normalize=True)

labels = train_counts.keys()
x = np.arange(len(labels))  # the label locations
width = 0.35  # the width of the bars

fig, [ax1, ax2] = plt.subplots(1, 2, figsize=(12, 4))
rects1 = ax1.bar(x - width/2, train_counts.values, width, label='Train')
rects2 = ax1.bar(x + width/2, test_counts.values, width, label='Test')

# Add some text for labels, title and custom x-axis tick labels, etc.
ax1.set_ylabel('Anteil')
ax1.set_title('Anteil der Klassen im Datensatz')
ax1.set_xticks(x)
ax1.set_xticklabels(labels)
ax1.legend()

# scatter plot of sepal length and with
label1 = "sepal_length"
label2 = "sepal_width"

ax2.scatter(df_train[label1], df_train[label2], label="Train")
ax2.scatter(df_test[label1], df_test[label2], label="Test")
ax2.set_title('Sample Punkte in Trainings- und Testdatensatz')
ax2.set_ylabel(label2)
ax2.set_xlabel(label1)
ax2.legend()
plt.show()
  • In der linken Grafik sehen wir die Verteilung nach Trainings- und Testdaten pro Klasse.
    • Im Optimalfall sollten die Balken innerhalb jeder Klasse (orange und blau) gleich hoch sein. Dies bedeutet, dass der Anteil an Samples pro Klasse in beiden Datensätzen (Train und Test) gleich ist.
  • In der rechten Grafik sehen wir die einzelnen Datenpunkte für zwei Features (sepal_length, sepal_width) aufgezeichnet.
    • Beide Datensätze liegen in etwa im gleichen Bereich. Dies zeigt, dass die Daten in Train- und Testset ungefähr die gleichen Werte haben.

Nachdem wir unsere Daten nun geladen haben, wird es Zeit das Modell zu bauen.

Modell

Um ein passendes Modell zu wählen, müssen wir uns überlegen, was wir in das Modell hineingeben und was am Ende genau vorhergesagt werden soll. Dementsprechend passen wir unseren In- und Output des Modells an.

Wie wählen wir unser Modell?

Als Modell nehmen wir ein neuronales Netzwerk mit fully-connected Layers. Dies ist typisch für strukturelle/tabellarische Daten, wie wir sie hier vor uns haben.

Was ist der Input in unser Modell? Was ist der Output?

Der Input in unser Modell sind die folgenden vier Features:

In [ ]:
# drop species since it is the label here
label_col = "species"
features = [*df_train.drop(label_col, axis=1).columns]
num_features = len(features)
print(f"Features:{features}")
print(f"Anzahl der Features: {num_features}")
  1. Länge des Sepalum (Kelchblatt)
  2. Breite des Sepalum (Kelchblatt)
  3. Länge des Petalum (Kronblatt)
  4. Breite des Petalum (Kronblatt)

In anderen Datensätzen haben wir mehr Inputfeatures. Diese erfordern dann auch komplexere Modelle.

Das Ziel des Modells ist es vorherzusagen, zu welcher Spezies die Blume gehört. Es gibt die folgenden Spezies (Labels):

In [ ]:
labels = df_train["species"].unique()
num_labels = len(labels)
print(f"Labels: {labels}")
print(f"Anzahl der Labels: {num_labels}")

Unser neuronales Netz muss also den folgenden Aufbau haben:

  1. Vier Inputnodes für vier Features
  2. Drei Outputnodes für die drei verschiedenen Labels.

Dazwischen können wir beliebig viele versteckte (hidden) Layer einbauen.

TODO 3: Warum können wir nicht einfach einen Outputknoten haben, der direkt die Klasse vorher sagt?

Wie wird unser Modell nun gebaut?

Nachdem wir nun gesehen haben, was der In- und Output unseres Modells ist, können wir es nun bauen.

Dazu importieren wir zunächst die nötigen Libraries, die uns dabei unterstützen. Wir möchten schließlich nicht die darunterliegende Mathematik von neuem implementieren.

In [ ]:
import tensorflow as tf
from tensorflow.keras import Sequential, Input
from tensorflow.keras.layers import Dense, InputLayer
from tensorflow.keras.losses import CategoricalCrossentropy
from tensorflow.keras.optimizers import Adam, SGD
from tensorflow.keras.callbacks import TensorBoard
from tensorflow.keras.metrics import Precision, Recall, CategoricalAccuracy

import os

Lasst uns kurz überprüfen, ob die richtige Tensorflow-Version installiert ist.

In [ ]:
assert tf.__version__.startswith("2"), "Die falsche Version ist installiert"
"Ok!"

Nun definieren wir unser ML Modell mit Hilfe von Tensorflow's Keras.

  • Sequentielles Modell von Keras keras.Sequential.
  • Übergabe der Layer als Liste
  • Verschiedene Layer-Arten aus keras.layers
    • Wir verwenden das sog. Dense (oder Fully Connected ) Layer.

Ein weiterer wichtiger Bestandteil von neuronalen Netzen sind die Aktivierungsfunktionen. Am populärsten sind ReLU (rectified linear unit) und die softmax-Funktion.

Diese Aktivierungsfunktionen werden gebraucht, um sog. Nicht-Linearitäten zu erzeugen, d.h. eine Entscheidungsgrenze zwischen den Klassen zu schaffen, die nicht nur eine gerade Linie ist. Ohne diese entspräche das Modell einer einfachen linearen Klassifizierung der Form $y = Wx+t$.

Aktivierungsfunktion Anwendung Formel
Softmax Klassifizierung $$\sigma(\mathbf{z})_{i}=\frac{e^{z_{i}}}{\sum_{j=1}^{K} e^{z_{j}}}$$
ReLU Hidden layer $$\sigma(\mathbf{z})_{i}=\max\{0,z_i\}$$
Linear/ keine Regression $$\sigma(\mathbf{z})_{i}=z_i$$
In [ ]:
model = Sequential([
    InputLayer(input_shape=(num_features,), name="input_features"),
    # hier können wir später noch mehr Layers hinzufügen
    Dense(num_labels, activation="softmax", name="prediction"),
])

Woher weiß unser Modell eigentlich was gut und was schlecht ist?

Dafür legen wir die Loss-Funktion fest: Für eine Klassifizierung mit mehreren Klassen nehmen wir den CategoricalCrossEntropy-Loss.

Alternativen:

  • BinaryCrossEntropy (Binäre Klassifizierung)
  • MeanSquaredError (Regression): $\text{MSE}(y, \hat{y}) = \frac{1}{N}\sum_i||y_i-\hat{y_i}||^2 $

Wie bringen wir unser Modell dazu, diesen Loss zu minimieren?

Dazu benutzen wir einen sog. Optimizer.

(Vorsicht: Es folgt ein bisschen Mathe!)

  • Der Optimizer ist ein Algorithmus, um die Parameter unseres Modells anzupassen, sodass der Loss minimiert wird.
  • Der einfachste Optimierungsalgorithmus ist Gradient Descent.
    • Dazu schauen wir uns die Ableitung (Gradient) der Loss-Funktion an. Diese gibt uns die Steigung der Loss-Funktion für die aktuellen Gewichte.
    • Die Gewichte (Parameter) unseres Modells werden in die negative Richtung des Gradienten upgedatet. Wir steigen also die Funktion in die Richtung mit dem größten Gefälle hinab. (Descent)
    • $W_{t+1} = W_{t} - \eta*\nabla{\mathcal{L}}$
      • $W$: Gewicht im Netzwerk
      • $\eta$: Learning Rate
      • $\mathcal{L}$: Loss-Funktion, z.B. Mean Squared Error

https://ml-cheatsheet.readthedocs.io/en/latest/gradient_descent.html

In [ ]:
from datetime import datetime

model.compile(optimizer=SGD(learning_rate=0.01),
              loss=CategoricalCrossentropy(),
              metrics=[CategoricalAccuracy(name="acc")])

time_str = datetime.utcnow().strftime("%y%m%d-%H%M%S")

model.summary()

Training

Wie lernt unser Modell?

Zum Training selbst können wir jetzt noch einige Parameter festlegen, die das Training selbst betreffen.

  1. Wie lange soll trainiert werden?
  2. Wie viele Datensätze sollen auf einmal verarbeitet werden?
  1. Epochen: Epochen geben an, wie lange/wie oft trainiert werden soll. Eine Epoche entspricht einem Durchgang durch den gesamten Datensatz
  2. Batch Size: Die Größe eines Batches gibt an, wie viele Samples auf einmal in das Modell geschickt werden soll. Auf Basis eines Batches wird der Loss berechnet und das Update unseres Modells durchgeführt.
In [ ]:
epochs = 50
batch_size = 8

TODO 4 : Anhand der Epochen, der Menge der Trainingsdatensätze und der Batch Size können wir die Gesamtanzahl der Update-Steps und die Anzahl der Update-Steps pro Epoche berechnen.

In [ ]:
# Reminder: Number of Training Samples
len(df_train)
In [ ]:
num_train_samples = len(df_train)

# Berechne diese beiden Variablen
update_steps_per_epoch = 0
update_steps_total = 0

update_steps_per_epoch, update_steps_total

(15, 750) sollte hier das Ergebnis sein.

Wie bekommen wir nun aus den eingelesenen DataFrames die Werte mit denen wir das Modell trainieren können?

  1. Aus den Input-Daten müssen wir das Label entfernen, da wir dieses vorhersagen wollen und in der Realität nicht kennen.
  2. Außerdem kann unser Modell nur Zahlen verarbeiten keine Strings. Unsere Labels liegen aber als Strings vor.
In [ ]:
X_train = df_train.drop(label_col, axis=1).values
y_train = pd.get_dummies(df_train[label_col]).values

X_val = df_test.drop(label_col, axis=1).values
y_val = pd.get_dummies(df_test[label_col]).values # convert pd.Series into numpy array

TODO 5: Finde heraus was die Methode pd.get_dummies(df) macht.

In [ ]:
# Platz zum Ausprobieren von pd.get_dummies()
In [ ]:
log_dir = os.path.join(".", "logs", "iris", time_str)

tensorboard_callback = TensorBoard(log_dir=log_dir,
                                   histogram_freq=1,
                                   write_images=True
                                   )

history = model.fit(X_train, y_train,
                    batch_size=batch_size,
                    epochs=epochs,
                    validation_data=(X_val, y_val),
                    callbacks=[tensorboard_callback])

Speichern des Modells nach dem Training

In [ ]:
model.save("models\iris_model")
In [ ]:
# tf.keras.models.load_model("models\iris_model")
In [ ]:
os.listdir("models\iris_model")

Modularer Modellbau

TODO 6: Verpacke den Modellbau in eine Funktion.

  • Vervollständige die Funktion build_classifier. Die Funktion erhält als Parameter
    • eine Liste mit der Anzahl der Neuronen im jeweiligen Hidden Layer
    • die Anzahl der Labels (Output Neuronen)
    • die Anzahl der Features (Input Neuronen)
    • den Namen des Optimizers. Diesen Parameter ignorieren wir vorerst.
  • Benutze alle Bausteine von Keras, die wir bisher kennen gelernt haben.
  • Diese Liste hilft dir dabei
    • Bausteine: InputLayer, Sequential
    • Layers: Dense
    • Optimizer: SGD
    • Activation: ReLU, Softmax
    • Loss: CategoricalCrossentropy
  • Am Ende nicht vergessen das Model zu bauen(.compile)
  • Teste anschließend, ob alles passt, indem du die Funktion train_and_evaluate aufrufst. Diese ist darunter definiert.
In [ ]:
def build_classifier(hidden_layers, num_labels, num_features, optim="sgd"):

    # hidden_layers: List of integers, indicating the neurons per layer
    # num_labels: number of output neurons
    # num_features: number of input values

    if len(hidden_layers) == 0:
        # Logistic Regression, no hidden layers
        model = Sequential([
            Dense(num_labels, input_shape=(
                num_features,),
                activation="softmax"),
        ])
    else:
        # Vervollständige diesen Block
        layers = []
        # Definiere die notwendigen layers (input layer, hidden layer, output layer)
        
        model = Sequential(layers)

    # Make the model ready for training.
    learning_rate = 0.01
    
    
    # Definiere den Optimizer
    
    # Kompiliere das Modell
    
    print(model.summary())
    return model
In [ ]:
model_sgd = build_classifier(hidden_layers=[8], num_labels=num_labels,
                         num_features=num_features)

Oft bietet es sich an, den Trainings- und Evaluierungsprozess in einer Funktion zusammenzufassen, um z.B.

  • zusätzliche Parameter zu übergeben (Änderung der Learning Rate etc.)
  • das Training aus einem anderen Modul heraus zu starten
In [ ]:
def train_and_evaluate(df_train, df_test, model):
    # prepare data
    X_train = df_train.drop("species", axis=1).values
    y_train = pd.get_dummies(df_train["species"]).values

    # convert pd.Series into numpy array
    X_val = df_test.drop("species", axis=1).values
    y_val = pd.get_dummies(df_test["species"]).values

    time_str = datetime.utcnow().strftime("%y%m%d-%H%M%S")
    log_dir = os.path.join(".", "logs", "iris", time_str)
    
    # prepare training
    batch_size = 8
    epochs = 50
    tensorboard_callback = TensorBoard(log_dir=log_dir,
                                       histogram_freq=1,
                                       write_images=True
                                       )
    # train the model
    train_history = model.fit(X_train, y_train,
                              batch_size=batch_size,
                              epochs=epochs,
                              validation_data=(X_val, y_val),
                              callbacks=[tensorboard_callback])

    # evaluate the model
    eval_dict = model.evaluate(X_val, y_val, batch_size=batch_size)
    return train_history, eval_dict
In [ ]:
history_sgd, eval_sgd = train_and_evaluate(df_train, df_test, model_sgd)
In [ ]:
dict(zip(["loss", "acc", "prec", "recall"], eval_sgd))

Die Ergebnisse sollten in etwa folgendermaßen aussehen: $$\text{Loss} <= 0.5$$ $$\text{Accuracy} >= 0.8$$ $$\text{Precision} >= 0.8$$ $$\text{Recall} >= 0.0$$

Optimizer Vergleich

  • In unserem Training oben benuten wir den normalen (Stochastic) Gradient Descent
  • Im Laufe der Zeit haben sich jedoch noch einige zusätzliche Optimizer entwickelt, die schnellere Konvergenz erreichen
  • Der populärste Optimizer ist Adam.
  • Oben haben wir gesehen, wie der Standard Optimizer abschneidet.
    • Jetzt wollen wir den Adam-Optimizer verwenden
In [ ]:
model = build_classifier([8], num_labels=num_labels,
                         num_features=num_features,
                         optim="adam")
history, eval = train_and_evaluate(df_train, df_test, model)
In [ ]:
dict(zip(["loss", "acc", "prec", "recall"], eval))

Veranschaulichung des Verhaltens einiger Optimizer

http://www.benfrederickson.com/numerical-optimization/

Trainingsverlauf

Nachdem das Training abgeschlossen ist, wollen wir uns jetzt ansehen wie das Training verlaufen ist. Dafür benutzen wir das sog. Tensorboard. Dieses kann im Terminal über tensorboard --logdir "./logs" gestartet werden. Anschließend findet sich das Tensorboard im Browser unter http://localhost:6006.

Alternativ kann der Befehl auch hier im Notebook ausgeführt werden.

In [ ]:
%load_ext tensorboard
In [ ]:
#%reload_ext tensorboard # in case it doesnt load, uncomment this line
%tensorboard --logdir ".\logs\iris" --port 6007

Wenn wir nicht das Tensorboard benutzen wollen, bietet sich mit matplotlib auch ein sehr mächtiges Tool um unsere Ergebnisse zu visualisieren.

In [ ]:
import matplotlib.pyplot as plt

Unsere Loss-Kurve lässt sich zum Beispiel mit den folgenden einfachen Zeilen darstellen:

In [ ]:
plt.plot(np.arange(epochs),history.history["loss"])
plt.show()

So sieht das nur noch ein bisschen schwach aus. Was heißt jetzt was in dieser Grafik? Lasst uns doch die Achsen beschriften.

In [ ]:
plt.plot(np.arange(epochs), history.history["loss"])
plt.xlabel("Epoche")
plt.ylabel("Loss")
plt.show()

TODO 7: Erstelle einen Plot für die Accuracy-Kurve.

  • Die Kurve sollte rot sein. (Setze hier den color-parameter der Funktion plt.plot())
  • Füge Gitterlinien zur Grafik hinzu. Benutze die Funktion plt.grid().
  • Füge eine Legende hinzu (plt.legend())
  • Beschrifte die Achsen ähnlich wie oben mit "Accuracy" und "Epoche".
In [ ]:
# Platz für den Code

Wenn wir alle relevanten Kurven in einer Grafik darstellen wollen, könnten wir das z.B. so machen.

In [ ]:
fig, ax1 = plt.subplots()
ax1.set_xlabel('Epochen')
color = 'tomato'
ax1.plot(np.arange(epochs),history.history["val_acc"], color=color, label="Val")
color = 'tab:red'
ax1.set_ylabel('Accuracy', color=color)
ax1.plot(np.arange(epochs),history.history["acc"], color=color, label="Train")
ax1.tick_params(axis='y', labelcolor=color)
ax1.legend()

ax2 = ax1.twinx()  # instantiate a second axes that shares the same x-axis

color = 'lightblue'
ax2.plot(np.arange(epochs),history.history["val_loss"], color=color, label="Val")
color = 'tab:blue'
ax2.set_ylabel('Loss', color=color)  # we already handled the x-label with ax1
ax2.plot(np.arange(epochs),history.history["loss"], color=color, label="Train")
ax2.tick_params(axis='y', labelcolor=color)

ax2.legend()
plt.show()

Ob diese etwas unübersichtliche Darstellungsweise hilft das Training zu analysieren, sei an dieser Stelle dem Leser überlassen.

TODO 8: Warum fällt die Accuracy manchmal wieder, obwohl der Loss kontinuierlich fällt?

Confusion Matrix

Für die Analyse der Performance unseres Modells benutzen wir die sog. Confusion Matrix.

Zunächst brauchen wir die Vorhersagen von unserem Modell.

In [ ]:
y_pred = model.predict(X_val)
y_pred[:5]

Das sind jetzt allerdings nur die Wahrscheinlichkeiten. Die Klassenvorhersage bekommen wir folgendermaßen:

In [ ]:
y_pred.argmax(axis=1)

Die annotierten Labels müssen wir auch in diese Repräsentation umwandeln.

In [ ]:
y_val.argmax(axis=1)
In [ ]:
df_pred = pd.DataFrame()
df_pred["pred"]= y_pred.argmax(axis=1)
df_pred["gt"] = y_val.argmax(axis=1)
df_pred.sample(5)

Nun müssen wir anhand der Vorhersagen die einzelnen Elemente der Confusion Matrix berechnen:

  • True Positives
  • False Positives
  • True Negatives
  • False Negatives

TODO 9: True Positive ist auch bei mehreren Klassen klar definiert. Aber was ist ein False Negative bei mehreren Klassen?

In [ ]:
def true_positives(df_pred, label):
    label, = np.where(labels==label)[0]
    return ((df_pred["gt"]==label) & (df_pred["pred"] == label)).sum()
In [ ]:
TPs={l:true_positives(df_pred,l) for l in labels}
TPs
In [ ]:
def false_negatives(df_pred, label):
    label, = np.where(labels==label)[0]
    return ((df_pred["gt"]==label) & (df_pred["pred"] != label)).sum()
In [ ]:
FNs={l:false_negatives(df_pred,l) for l in labels}
FNs

TODO 10: Schreibe die verblieben Funktionen false_positives und true_negatives, die dir die Anzahl der False Positives/True Negatives im Multi-Klassen-Szenario zurückgeben. Du kannst dich dabei an den Implementierungen von oben orientieren.

Vervollständige dazu die Vergleichsoperatoren in den beiden Funktionen. (Ersetze ... mit !=, ==, >=, >, <, <=).

(Zur Erinnerung: False Positives sind alle die, die als eine Klasse vorausgesagt wurden, aber zu einer anderen gehören, z.B. alle Samples die als "setosa" vorhergesagt wurden, aber zu "virginica" oder "versicolor" gehören.

In [ ]:
def false_positives(df_pred, label):
    # fill in the signs 
    
    # convert string label to number
    label, = np.where(labels==label)[0]
    print(label)
    
    # mark false positives
    FP_list = ((df_pred["gt"] ... label) & (df_pred["pred"] ... label))
    print(FP_list)
    
    # sum of all false positives
    FP = FP_list.sum()
    return FP
In [ ]:
FPs={l:false_positives(df_pred,l) for l in labels}
FPs
In [ ]:
def true_negatives(df_pred, label):
    # fill in the signs for the '...'
    
    label, = np.where(labels==label)[0]
    
    
    TN = ((df_pred["gt"] ... label) & (df_pred["pred"] ... label)).sum()
    
    return TN
In [ ]:
TNs = {l: true_negatives(df_pred, l) for l in labels}
TNs
In [ ]:
def confusion_matrix3(df_pred):
    TPs = [true_positives(df_pred, l) for l in labels]
    FPs = [false_positives(df_pred, l) for l in labels]
    FNs = [false_negatives(df_pred, l) for l in labels]

    cm = np.array([[TPs[0], FNs[0], FNs[0]],
                   [FPs[0], TPs[1], FNs[1]],
                   [FPs[0], FPs[1], TPs[2]]
                   ])
    return cm


confusion_matrix3(df_pred)

Wir können uns es auch viel einfacher machen und sklearn benutzen.

In [ ]:
from sklearn.metrics import confusion_matrix
In [ ]:
confusion_matrix(df_pred["gt"],df_pred["pred"])

Was macht unsere Neural Network?

Das NN liefert für jeden beliebigen, numerischen Input drei numerische Werte zwischen 0 und 1, die die Wahrscheinlichkeit für die Klasse angeben. In den meisten Fällen ist der Input des NNs hochdimensional (mehrere 100 Inputfeatures) und eine Visualisierung der Decision Boundaries ergibt wenig Sinn oder ist nicht so einfach möglich. Da wir hier mit einem relativ einfachem Datensatz arbeiten, können wir uns jedoch die Decision Boundaries mit einem kleinen Trick ansehen.

Der Singular Value Decomposition (SVD) Algorithmus erlaubt es uns die Daten auf eine 2-dimensionale Ebene zu projezieren. Hier verlieren wir zwar Informationen, aber im Fall dieses Datensets ist der Verlust minimal. Diese 2-dimensionale Projektion können wir graphisch darstellen.

Die unten dargestellte Grafik zeigt unsere Trainingsdaten (Punkte) und die Wahrscheinlichkeit für jede Klasse (Hintergrundfarbe). Die schwarzen Linien stellen die Decision Boundaries dar, also die Stellen, an dem sich die Klasse mit der höchsten Wahrscheinlichkei ändert.

Wichtig: Wir sehen hier nur einen vereinfachte Projektion der Entscheidungsgrenzen (Decision Boundaries), da diese eigentlich 4-dimensional sind

In [ ]:
from sklearn.decomposition import TruncatedSVD

svd = TruncatedSVD()
data = df_train.drop("species", axis=1).values
svd.fit(data)  # ermitteln der optimalen Projektionsebene
# Projection der Daten auf eine 2-dimensionale Ebene
transformed = svd.transform(data)

res = 200

# Auswertung des Modells für jeden sichtbaren Punkt
grid = np.stack(np.meshgrid(np.linspace(transformed[:, 0].min(), transformed[:, 0].max(), res),
                            np.linspace(transformed[:, 1].min(), transformed[:, 1].max(), res)), axis=2)
# projektion der sichtbaren 2d Punkte ins 4-dimensionale
t = svd.inverse_transform(grid.reshape(-1, 2))

# Ermitteln der Grenzen des Datensatzes
min_sepal_length = df_train["petal_length"].min()
max_sepal_length = df_train["petal_length"].max()
min_sepal_width = df_train["petal_width"].min()
max_sepal_width = df_train["petal_width"].max()
mean_petal_length = df_train["sepal_length"].mean()
mean_petal_width = df_train["sepal_width"].mean()


class_map = model.predict(t).reshape((*grid.shape[:2], 3))
class_map_borders = class_map.argmax(axis=2)

fig, ax = plt.subplots(figsize=(8, 8))

ax.imshow(class_map, extent=[transformed[:, 0].min(), transformed[:, 0].max(
), transformed[:, 1].min(), transformed[:, 1].max()], alpha=.5, aspect='auto', origin="lower")
ax.contour(-class_map_borders, extent=[transformed[:, 0].min(), transformed[:, 0].max(
), transformed[:, 1].min(), transformed[:, 1].max()], colors='k', alpha=.3)

plt.scatter(transformed[:, 0], transformed[:, 1], c=y_train.argmax(axis=1))
ax.set_title("Decision Boundary")
plt.show()

Weitere Trainingstechniken

Early Stopping

  • Manchmal wissen wir nicht genau, wie lange unser Modell trainieren muss bis der Loss konvergiert, d.h. unser Modell nicht mehr besser wird.
  • Andererseits kann es auch passieren, dass das Modell auf die Trainingsdaten overfittet, wenn es zu lange trainiert.
  • Lösung: Early Stopping.
    • Early Stopping ist ein Mechanismus, der das Training unterbricht, sobald das Modell nicht mehr besser/ wird.

Ein Beispiel:

In [ ]:
from tensorflow.keras.callbacks import EarlyStopping

Wir setzen die Anzahl der Epochen sehr hoch an, um zu sehen, dass das Training schon vorher unterbrochen wird.

In [ ]:
# important parameters
# the prefix "val_" ensures monitoring the validation metric, instead of the
monitor = "val_"+"loss"
mode = "min"  # whether the goal is to min- or maximize the metric stated in monitor

patience = 10  # wait epochs until the stopping kicks in

early_stopping_callback = EarlyStopping(
    monitor=monitor,
    verbose=2,
    patience=patience,
    mode=mode,  # change to "min" for loss
    restore_best_weights=True
)
epochs = 1000

# build new model
model_early_stopping = build_classifier([8], num_labels=num_labels,
                                        num_features=num_features)

# train the model
history = model_early_stopping.fit(X_train, y_train,
                                   batch_size=batch_size,
                                   epochs=epochs,
                                   validation_data=(X_val, y_val),
                                   callbacks=[early_stopping_callback])
In [ ]:
 eval_es = model_early_stopping.evaluate(X_val,y_val)
In [ ]:
dict(zip(["loss", "acc", "prec", "recall"],eval_es))

Checkpointing

  • Manchmal kann das Training eines ML Modells sehr lange dauern (mehrere Stunden bis Tage)
  • Problem: Wenn das Training währenddessen abgebrochen wird, müssen wir von vorne beginnen
  • Lösung: Speichern des aktuellen Stands während dem Training. (sog. Checkpointing)
In [ ]:
from tensorflow.keras.callbacks import ModelCheckpoint
In [ ]:
# important parameters

ckpt_path = os.path.join("ckpts", "iris")
os.makedirs(ckpt_path, exist_ok=True)

# the prefix "val_" ensures monitoring the validation metric, instead of the
monitor = "val_"+"acc"
mode = "max"  # whether the goal is to min- or maximize the metric stated in monitor

# how often is the model saved
period = 2

# Saves storage
save_best_only = True  # False

ckpt_callback = ModelCheckpoint(
    ckpt_path,
    monitor=monitor,
    verbose=2,
    save_best_only=save_best_only,
    mode=mode,
    save_freq="epoch",
    period=period,
)
epochs = 20

# build new model
model = build_classifier([8], num_labels=num_labels,
                         num_features=num_features)

print(batch_size)
# train the model
history = model.fit(X_train, y_train,
                    batch_size=batch_size,
                    epochs=epochs,
                    validation_data=(X_val, y_val),
                    callbacks=[ckpt_callback])

Fragen?

Ich wollte schon immer mal wissen, ob/wie/was...?

Wie geht es jetzt weiter?

Session 6: Hyperparameter-Tuning