Updates dieser Seite:

  • 08.09.2022: Inhaltliche Überarbeitung
  • 31.08.2022: Aus Kap. 3 herausgelöst

Überblick

Wie gut ist mein Modell? In diesem Kapitel beschäftigen wir uns mit der Evaluation von Modellen und entsprechenden Metriken. Weiterhin geht es noch um die Frage, wie man die Gesamtdaten für das Training in Trainings-, Test- und Validierungsdaten aufteilen muss. Abschließend stellen wir die Methode der Cross-Validierung vor und den "Verlust" durch Testdaten zu minimieren.

Konzepte

Konfusionsmatrix, Recall, Precision, Accuracy, Trainings-/Test- und Validierungsdaten, Cross-Validierung

In [1]:
import warnings
warnings.filterwarnings('ignore')

import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns

1 Evaluation von Modellen

Wie bewerten wir die Güte eines trainierten Modells? Bei der linearen Regression haben wir zum Beispiel den mittleren quadratischen Fehler (MSE) als Maß. Bei Klassifikationsproblemen schauen wir uns an, wie oft eine Klasse richtig errechnet wird.

1.1 Binäre Klassfikation

Bei der binären Klassifikation erstellen wir ein Modell, dass eine Klasse K errechnen können soll. Das heißt, die Ausgabe ist K oder nicht K. Eine solche Klasse kann zum Beispiel "Katzenbild" sein und entsprechend "kein Katzenbild". Ein aktuelles Beispiel wäre Corona, wo wir unterscheiden zwischen "hat Corona" und "hat kein Corona".

Schöner Artikel dazu auf Wikipedia: https://de.wikipedia.org/wiki/Beurteilung_eines_bin%C3%A4ren_Klassifikators

Zufälligerweise hat das ZDF auch Video mit dem Beispiel "Corona-Schnelltest" produziert: Wie zuverlässig sind Corona-Schnelltests?. Hier könnte man mal überlegen, mit welchen der hier vorgestellten Maße die im Video gezeigten Maße übereinstimmen.

Positives und Negatives

Alle Trainingsbeispiele, die von Typ K sind, nennen wir Positives, alle anderen Negatives. Im Corona-Beispiel teilen wir unsere Population in Positives und Negatives ein:

Wenn wir alle Trainingsbeispiele durch unser Modell schicken, erhalten wir jedes Mal eine errechnete Klasse (engl. predicted class) und kennen natürlich auch die echte Klasse (engl. actual class).

Im Corona-Beispiel können wir uns einen Corona-Schnelltest als Modell vorstellen. Die Population besteht jetzt aus allen per Schnelltest getesteten Personen (zum Beispiel für eine Teststation in einem bestimmten Zeitraum mit einem Test pro Person).

Der Schnelltest definiert eine Treffermenge (Kreis). Alle Personen in der Treffermenge sind gemäß des Tests positiv.

(Die grafische Darstellung ist von den Wikipedia-Eintrag inspiriert: https://en.wikipedia.org/wiki/Precision_and_recall)

True Positives und False Positives

Jetzt unterscheiden wir vier Fälle. Zunächst schauen wir uns die Treffermenge an (die, die positiv getestet wurden). Hier unterscheiden wir zwischen Tests, die korrekterweise positiv sind, da die Personen Corona haben (True Positives) und solchen Tests, die fälschlicherweise behaupten, die Person sei positiv, obwohl sie kein Corona hat (False Positives).

  • True Positives (TP): Anzahl der Samples, die korrekterweise K zugeordnet wurden
  • False Positives (FP): Anzahl der Samples, die als K erkannt wurden, obwohl sie nicht zu K gehören

Die False Positives sind also "falscher Alarm" und führen beim Beispiel Corona dazu, dass (unnötigerweise) Quarantäneregelungen greifen.

True Negatives und False Negatives

Jetzt schauen wir uns die Tests an, die außerhalb der Treffermenge sind. Auch hier haben wir Tests, die korrekterweise außerhalb der Treffermenge sind, weil die Personen kein Corona haben (True Negatives), und solche, die fälschlicherweise negativ sind, da die Personen doch Corona haben (False Negatives).

  • True Negatives (TN): Anzahl der Samples, die korrekterweise nicht K zugeordnet wurden
  • False Negatives (FN): Anzahl der Samples, die als nicht K erkannt wurden, obwohl sie zu K gehören

Die False Negatives sind also diejenigen, die wir eigentlich suchen, aber die uns "durch die Lappen" gegangen sind. Im Fall von Corona heißt das: Diese Personen denken, sie seien nicht ansteckend, sind es aber doch!

Gesamtbild

Wir schauen uns nochmal unser Diagramm an, weil wir dort schön alle Konzepte sehen.

Die gesamten Daten spalten sich in zwei Teile:

  • P (Positives): Daten, die zu der gesuchten Klasse gehören, z.B. "ist Corona-inzifiert"
  • N (Negatives): Daten, die nicht zur gesuchten Klasse gehören, z.B. "hat kein Corona"

Die Treffermenge sind solche Daten, die unser Modell für Positives hält. Sie besteht aus:

  • TP (True Positives): Daten, die korrekt klassifiziert wurden, die unser Modell also korrekt "erkannt" hat
  • FP (False Positives): Daten, die gar nicht zur Klasse gehören, aber vom Modell als Treffer zurückgeliefert wurden

Außerhalb der Treffermenge unterscheiden wir ebenfalls zwei Fälle:

  • TN (True Negatives): Daten, die korrekterweise nicht als Treffer zurückgeliefert wurden, denn sie gehören nicht zur Klasse
  • FN (False Negatives): Daten, die eigentlich in die Treffermenge gehören, die unser Modell aber nicht "erkannt" hat

Konfusionsmatrix

In einer Konfusionsmatrix (engl. confusion matrix) zählen wir bei jedem Trainingsbeispiel mit, von welcher Klasse (K oder nicht K) es ist und welche Klasse (K oder nicht K) das Modell errechnet hat. Jede Zelle der Konfusionsmatrix entspricht einem der vier oben genannten Fälle:

Die obere Zeile (d.h. Summe der zwei Zellen) entspricht allen Negatives, die untere Zeile allen Positives.

Auf der Diagonalen stehen alle korrekten Vorhersagen (TP + TN). Die anderen beiden Zellen sind die Fehlklassifikationen (FN + FP). Die Summe aller Zellen ist die Anzahl aller Trainingsbeispiele (TP + TN + FP + FN).

Hier ein Beispiel mit echten Zahlen. Jetzt nehmen wir ein Beispiel, wo wir Mails in SPAM und nicht-SPAM klassifizieren.

nicht SPAM (predicted) SPAM (predicted)
nicht SPAM (actual) 556 12
SPAM (actual) 4 28

Es handelt sich um 600 Trainingsbeispiele (Summe aller Zellen). Davon wurden 584 korrekt klassifiziert (Diagonale). Es gab 32 Samples der Klasse SPAM (untere Zeile). Das Modell hat aber 40 Samples als SPAM klassifiziert (rechte Spalte).

Recall

Die Metrik Recall misst, wie viele von den möglichen Treffern wir "erwischt" haben. Andere Bezeichnungen für Recall sind: Sensitivität, True-Positive-Rate oder Trefferquote/Hit rate.

In anderen Worten: Wie viele Positives wir korrekt vorhergesagt haben (TP), relativ zu allen tatsächlichen Positives (TP + FN).

Rechnerisch heißt das:

$$ \mbox{recall} = \frac{TP}{TP + FN} = \frac{TP}{P} $$

Im Beispiel kommen wir auf einen Recall von 87.5%:

$$ \mbox{recall} = \frac{28}{28 + 4} = 0.875$$

Für das Corona-Beispiel heißt das: Recall misst, wie viele wir von den tatsächlich mit Corona infizierten wir auch gefunden haben.

Precision

Die Metrik Precision misst, wie viele von den Vorhersagen, die wir getroffen haben (TP + FP), wirklich korrekt waren (TP). Ein anderer Begriff dafür ist Genauigkeit.

$$ \mbox{precision} = \frac{TP}{TP + FP} $$

Im Beispiel kommen wir auf eine Precision von 70%:

$$ \mbox{precision} = \frac{28}{28 + 12} = 0.7$$

Für das Corona-Beispiel heißt das: Precision gibt die Wahrscheinlichkeit wider, dass ein positiver Schnelltest auch wirklich stimmt.

Siehe auch: https://developers.google.com/machine-learning/crash-course/classification/precision-and-recall

Bedeutung von Recall und Precision

Sie sollten sich anhand von Beispielszenarien klar machen, dass Recall und Precision sehr unterschiedliche Wichtigkeiten haben.

Hoher Recall: Beim Beispiel Corona möchte man natürlich alle Infizierten auch positiv testen, also einen hohen Recall erzielen. Wie bekommen Sie einen hohen Recall? Ganz einfach: Ihr Schnelltest (Modell) schlägt immer positiv aus. Das ergäbe 100% Recall. Aber natürlich auch extrem viele False Positives. Ein fälschlich positiver Coronatest hat unangenehme Folgen (14 Tage Quarantäne oder ein PCR-Test mit Quarantäne, bis das Ergebnis da ist). Hier ist es sinnvoll, mit der Precision zu messen, wie zuverlässig ein Test ist.

Hohe Precision: Wenn Sie Ihren Schnelltest sehr vorsichtig gestalten und sehr selten positiv ausschlagen, wenn Sie absolut sicher sind, dass der Test positiv ist, dann erzielen Sie vielleicht eine hohe Precision. Aber oft entgehen Ihnen dann sehr viele der tatsächlichen Positives und der Recall ist niedrig.

Überlegen Sie sich für andere Szenarien (defekte Bauteile in der Produktion detektieren, gefährliche Situationen per Überwachungskameras identifizieren etc.), welche Bedeutung Recall und Precision haben.

Accuracy

Die Metrik Accuracy betrachtet die Gesamtmenge der richtigen Vorhersagen (Diagonale der Konfusionsmatrix) relativ zu der Gesamtzahl der Samples (Summe aller Felder der Matrix). Andere Begriffe sind: Korrektklassifikationsrate oder Treffergenauigkeit.

Rechnerisch heißt das:

$$ \mbox{accuracy} = \frac{\mbox{korrekte Vorhersagen}}{\mbox{Anzahl aller Beispiele}} $$

Wenn wir das mit Hilfe unserer Begrifflichkeiten ausdrücken wollen, ist das:

$$ \mbox{accuracy} = \frac{TP + TN}{TP + TN + FP + FN} $$

Im Beispiel kommen wir auf eine Accuracy von 97.3%:

$$ \mbox{accuracy} = \frac{28 + 556}{28 + 556 + 12 + 4} = 0.973$$

Eine Besonderheit von Accuracy ist, dass auch Negatives stark in den Wert eingehen. Umgekehrt ist das nicht immer erwünscht, denn in der Regel gibt es deutlich mehr Negatives als Positives, so dass das Ergebnis von Accuracy weniger aussagekräftig ist. Daher ist bei binärer Klassifikation immer zu überlegen, ob nicht Precision und Recall geeignetere Maße sind.

Ein Vorteil von Accuracy ist, dass sie bei der Mehrklassen-Klassifikation relativ leicht umsetzbar ist (im Gegensatz zu Precision und Recall), wie wir später noch sehen werden.

Siehe auch: https://developers.google.com/machine-learning/crash-course/classification/accuracy

Evaluation des binären MNIST-Klassifizierers

Wir greifen unseren binären Klassifizierer modelLR (5 versus nicht-5) aus dem letzten Kapitel auf.

Dazu laden und präparieren wir die MNIST-Daten.

In [2]:
from sklearn.datasets import fetch_openml

mnist = fetch_openml('mnist_784', version=1)
X_data = mnist["data"] / 255.0
y_data = mnist["target"].astype(int)
In [3]:
X_train, X_test, y_train, y_test = X_data[:60000], X_data[60000:], y_data[:60000], y_data[60000:]
y_train5 = (y_train == 5)
y_test5 = (y_test == 5)

Jetzt erstellen und trainieren wir ein Modell für die binäre Klassifikation.

In [4]:
from sklearn.linear_model import LogisticRegression

model = LogisticRegression(solver='liblinear')
model.fit(X_train, y_train5)
Out[4]:
LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
                   intercept_scaling=1, l1_ratio=None, max_iter=100,
                   multi_class='auto', n_jobs=None, penalty='l2',
                   random_state=None, solver='liblinear', tol=0.0001, verbose=0,
                   warm_start=False)
In [5]:
y_pred5 = model.predict(X_test)
y_pred5
Out[5]:
array([False, False, False, ..., False,  True, False])

Wir hatten auf den Testdaten die Vorhersagen berechnet und lassen uns jetzt die Konfusionsmatrix berechnen. Dazu übergeben wir die echten y-Werte und die errechneten y-Werte.

Siehe: https://scikit-learn.org/stable/modules/generated/sklearn.metrics.confusion_matrix.html

In [6]:
from sklearn import metrics

cm = metrics.confusion_matrix(y_test5, y_pred5)
cm
Out[6]:
array([[9040,   68],
       [ 153,  739]])

Wir visualisieren die Konfusionsmatrix mit Hilfe der Library Seaborn.

Siehe: https://seaborn.pydata.org/generated/seaborn.heatmap.html

In [7]:
sns.heatmap(pd.DataFrame(cm), annot=True, cmap="YlGnBu", fmt='d')
plt.tight_layout()
plt.title('Verwechslungsmatrix', y=1.1)
plt.ylabel('Korrektes Label')
plt.xlabel('Vorhergesagtes Label')
Out[7]:
Text(0.5, 15.0, 'Vorhergesagtes Label')

Precision, Recall und Accuracy

Anhand der Werte in der Matrix setzen wir TN, TP, FP und FN.

In [8]:
TN = 9040.0
TP = 741.0
FP = 68.0
FN = 151.0

Mit diesen Werten können wir Precision, Recall und Accuracy anhand der Formeln berechnen.

In [9]:
print("Precision: ", TP / (TP + FP))
Precision:  0.9159456118665018
In [10]:
print("Recall: ", TP / (TP + FN))
Recall:  0.8307174887892377
In [11]:
print("Accuracy: ", (TP+TN)/(TP+TN+FP+FN))
Accuracy:  0.9781
In [12]:
print("Precision:", metrics.precision_score(y_test5, y_pred5))
print("Recall:", metrics.recall_score(y_test5, y_pred5))
print("Accuracy:", metrics.accuracy_score(y_test5, y_pred5))
Precision: 0.9157372986369269
Recall: 0.82847533632287
Accuracy: 0.9779

1.2 Mehrklassen-Klassifikation

Bei mehreren Klassen können wir auch eine Konfusionsmatrix aufstellen.

Hier ein Beispiel mit drei Klassen A, B, C:

Auch hier finden wir auf der Diagonalen alle korrekten Vorhersagen. In den anderen Zellen können wir genau ablesen, welche Klasse mit welcher verwechselt wurde.

Accuracy

Bei der Mehrklassen-Klassifikation kann man nicht mehr von True Positives, False Positives etc. sprechen, denn es gibt nicht mehr die binäre Unterteilung in zwei Klassen (Positives, Negatives). Für die Berechnung der Accuracy ist das aber nicht weiter schlimm. Mit Hilfe der Konfusionsmatrix kann Accuracy direkt berechnet werden:

$$ \mbox{accuracy} = \frac{\mbox{Summe aller Diagonalzellen}}{\mbox{Summe aller Zellen}} $$

Denn das entspricht der Definition

$$ \mbox{accuracy} = \frac{\mbox{korrekte Vorhersagen}}{\mbox{Anzahl aller Beispiele}} $$

Natürlich benötigt man nicht unbedingt die Konfusionsmatrix zur Berechnung. Man kann einfach den entsprechenden Datensatz durchlaufen, die korrekten Vorhersagen zählen und dies am Schluss durch die Größe des Datensatzes teilen.

Da es im Machine Learning oft um Mehrklassen-Klassifikation geht, trifft man dort Accuracy am häufigsten als Maß an, da es relativ leicht zu berechnen ist.

Recall und Precision

Die Metriken Precision und Recall lassen sich nicht ohne weiteres von binärer Klassifikation auf mehrere Klassen übertragen. Stattdessen berechnet man für jede Klasse das entsprechende Maß, als wäre es ein binäres Problem (Klasse vs. Rest) und bildet anschließend den Mittelwert über alle Klassen.

Zum Beispiel kann man für Kategorie A die Precision $P_A$ für Klasse A versus nicht-A berechnen. Das kann man sich anhand der Konfusionsmatrix verdeutlichen, wo wir die Zellen für B und C zu "nicht A" zusammenfassen. Dann haben wir wieder die Werte für TP, FP, TN und FN und können Precision aus der Sicht von Klasse A berechnen.

Dasgleiche können wir für B und C tun, um $P_B$ und $P_C$ zu berechnen. Die gesamte Precision ist dann der Mittelwert:

$$P = \frac{P_A + P_B + P_C}{3}$$

Jetzt kann man auf die Idee kommen, dass eine Klasse, die häufiger vertreten ist, auch mit mehr Gewicht in das Gesamtmaß eingebracht werden soll. Nehmen wir an die Klassen A, B, C sind mit je 50%, 30%, 20% in den Samples vertreten.

Dann wäre ein gewichtetes Mittel wie folgt:

$$P_{weighted} = 0.5 \,P_A + 0.3\,P_B + 0.2\,P_C$$

Evaluation des MNIST-Mehrklassen-Klassifizierers

Wir greifen jetzt unser Mehrklassen-Modell aus dem letzten Kapitel auf.

In [13]:
model = LogisticRegression(solver='liblinear')
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
y_pred
Out[13]:
array([7, 2, 1, ..., 4, 5, 6])

Zunächst berechnen wir die Konfusionsmatrix:

In [14]:
cm = metrics.confusion_matrix(y_test, y_pred)
cm
Out[14]:
array([[ 960,    0,    1,    2,    0,    5,    6,    3,    1,    2],
       [   0, 1112,    3,    1,    0,    1,    5,    1,   12,    0],
       [   8,    8,  920,   20,    9,    5,   10,   11,   37,    4],
       [   4,    0,   17,  919,    2,   22,    4,   12,   21,    9],
       [   1,    2,    5,    3,  914,    0,   10,    2,    7,   38],
       [  10,    2,    0,   42,   10,  769,   17,    7,   28,    7],
       [   9,    3,    7,    2,    6,   20,  907,    1,    3,    0],
       [   2,    7,   22,    5,    8,    1,    1,  950,    5,   27],
       [  10,   14,    5,   21,   14,   27,    7,   11,  853,   12],
       [   8,    8,    2,   13,   31,   14,    0,   24,   12,  897]])

Wir visualisieren die Matrix mit Seaborn.

In [15]:
sns.heatmap(pd.DataFrame(cm), annot=True, cmap="YlGnBu", fmt='d')
plt.tight_layout()
plt.title('Verwechslungsmatrix', y=1.1)
plt.ylabel('Korrektes Label')
plt.xlabel('Vorhergesagtes Label')
Out[15]:
Text(0.5, 15.0, 'Vorhergesagtes Label')

Jetzt berechnen wir die Accuracy über alle Klassen. In der einfachsten Variante wird der Mittelwert über alle Accuracywerte der Klassen gebildet.

Siehe: https://scikit-learn.org/stable/modules/generated/sklearn.metrics.accuracy_score.html#sklearn.metrics.accuracy_score

In [16]:
acc = metrics.accuracy_score(y_test, y_pred)
print(f"Accuracy: {acc:.3f}")
Accuracy: 0.920

Precision und Recall werden hier für jede Kategorie einzeln berechnet (z.B. 4 vs nicht-4). Anschließend werden die 10 Werte (in unserem Beispiel) gemittelt.

In [17]:
precision = metrics.precision_score(y_test, y_pred, average='macro')
recall = metrics.recall_score(y_test, y_pred, average='macro')
print(f"Precision: {precision:.4f}")
print(f"Recall: {recall:.4f}")
Precision: 0.9190
Recall: 0.9189

Mit der Option average='weighted' wird der Mittelwert gewichtet, je nachdem wie hoch der Anteil der jeweiligen Kategorie ist (s.o.):

In [18]:
precision = metrics.precision_score(y_test, y_pred, average='weighted')
recall = metrics.recall_score(y_test, y_pred, average='weighted')
print(f"Precision: {precision:.4f}")
print(f"Recall: {recall:.4f}")
Precision: 0.9200
Recall: 0.9201

Bei den MNIST-Daten ist jede Klasse gleich oft vertreten, daher sind Mittelwert und gewichteter Mittelwert gleich (Rundungsabweichungen können auftreten). Wenn man eine sehr ungleiche Verteilung hat, sollte man aber das gewichtete Mittel in Betracht ziehen.

2 ROC und AUC

Im Machine Learning findet man neben Recall, Precision und Accuracy zwei weitere Maße, um Modelle zu evaluieren:

  • ROC: receiver operating characteristic
  • AUC: area under the (ROC) curve

Hierbei ist AUC direkt von ROC abgeleitet.

Betrachten wir binäre Klassifikation als Szenario. Alle Verfahren geben als Output einen numerischen Wert, z.B. zwischen 0 und 1 aus. Mit Hilfe eines Schwellwerts wird dann entschieden, ob das Modell sich für Klasse A oder Nicht-A entscheidet. Oft ist dieser Schwellwert 0,5 - aber offensichtlich kann man den Wert auch anders wählen. Mit Schwellwert 0,8 werden weniger Fälle als A klassifiziert. Mit Schwellwert 0,1 werden fast alle Fälle als A klassifiziert.

Nun ist es so, dass man mit einem niedrigen Schwellwert einen hohen Recall erzielt. Im Extremfall - mit Schwellwert 0 - bekommt man garantiert 100% Recall. Umgekehrt leidet natürlich die Precision bei einem niedrigen Schwellwert. Man könnte also sagen: Wir suchen den optimalen Schwellwert, so dass Recall und Precision beide gut abschneiden. Die ROC-Kurve versucht, das abzubilden. Wir benutzen allerdings nicht direkt den Precision-Wert, sondern die False Positive Rate.

TPR und FPR

Wir betrachten also zwei Metriken für viele mögliche Schwellwerte ansehen: die True Positive Rate (TPR), welche identisch mit Recall ist, und die False Positive Rate (FPR), die zumindest mit der Idee der Precision verwandt ist.

TPR (= Recall) setzt - wie wir schon wissen - die korrekte Treffermenge ins Verhältnis zu allen Positives:

$$ \mbox{TPR} = \frac{TP}{TP + FN} = \frac{TP}{P} $$

FPR setzt die falsch klassifizierten Treffer (False Positives), die also eigentlich Negatives sind, ins Verhältnis zu allen Negatives:

$$ \mbox{FPR} = \frac{FP}{FP + TN} = \frac{FP}{N} $$

Für unser Corona-Beispiel bedeutet das: Eine hohe FPR heißt, dass vielen Getesteten fälschlicherweise gesagt wurde, sie hätten Corona. Der Unterschied zu Precision ist, dass hier ein hoher Wert quasi "schlecht" ist. Außerdem steht hier im Nenner die Gesamtzahl aller Negatives (beim Coronabeispiel ein sehr großer Wert), bei Precision steht im Nenner die Größe der Treffermenge (ein eher überschaubarer Wert).

ROC

Wenn wir unser Modell mit vielen unterschiedlichen Schwellwerten testen und für jeden Test FPR auf der x-Achse abtragen und TPR auf der y-Achse, erhalten wir die ROC-Kurve (receiver operating characteristic).

Schauen wir uns ein Beispiel an, wo wir ein Modell mit Hilfe von logistischer Regression trainiert haben.

Wenn wir einen Schwellwert von 1 anlegen, haben wir immer eine leere Treffermenge. Also bekommen wir einen Recall von 0%, also TPR = 0, aber wir haben auch keine False Positives, also FPR = 0.

Bei einem Schwellwert von 0,7 haben wir bei unserem Modell einen Recall von 50%, also TPR = 0,5. Wir bekommen aber auch ein paar False Positives, sagen wir FPR = 0,1.

Bei Schwellwert 0,5 liegt der Recall bei 80% (TPR = 0,8) und die FPR steigt auf 0,4.

Bei einem Schwellwert von 0 enthält die Treffermenge alle Datenpunkte, daher ist der Recall 100% (TPR = 1), aber wir haben auch so viele False Positives, wie theoretisch möglich sind, also FPR = 1.

Bei dem obigen Szenario, wo die Werte erfunden, aber plausibel sind, ergibt sich folgende "Kurve" mit den vier Datenpunkten. Diese Kurse ist die ROC-Kurve.

In [19]:
tpr = [0, 0.5, 0.8, 0.9, 1]
fpr = [0, 0.1, 0.2, 0.4, 1]

plt.plot(fpr, tpr, '-o')
plt.ylabel("True Positive Rate (TPR)")
plt.xlabel("False Positive Rate (FPR)")
plt.show()

AUC

Je "bauchiger" die Kurve, umso besser die Performance des Modells, wohingegen eine Diagonale auf einen Zufallsprozess hindeutet. Dies wiederum messen wir mit dem Flächeninhalt AUC (area under the ROC curve).

Beispiel

Wir können unser Modell von oben nehmen, dann lassen sich ROC und AUC mit Hilfe von Scikit-learn berechnen.

In [20]:
y_pred5_prob = model.predict_proba(X_test)[::,1]
fpr, tpr, _ = metrics.roc_curve(y_test5,  y_pred5_prob)
auc = metrics.roc_auc_score(y_test5, y_pred5_prob)

plt.plot(fpr, tpr, label=f"auc = {auc:.3f}")
plt.ylabel("True Positive Rate (TPR)")
plt.xlabel("False Positive Rate (FPR)")
plt.legend(loc=4)
plt.show()

Der Artikel Understanding the ROC curve in three visual steps von Valeria Cortez erläutert die Konzepte sehr schön.

Auch auf Wikipedia finden Sie unter Receiver operating characteristi diese Konzepte relativ ausführlich erläutert.

3 Trainings-, Validierungs- und Testdaten

Beim überwachten Lernen hat man Trainingsdaten $(x_k, y_k)$ mit der jeweils korrekten Ausgabe. Hat man ein Modell trainiert, darf man die Performance des Modells offensichtlich nicht auf den gleichen Daten testen, mit denen man das Modell trainiert hat. Daher teilt man die Daten vor dem Training in zwei getrennte Datensätze:

  • Trainingsdaten (z.B. 80% der Daten)
  • Testdaten (z.B. 20% der Daten)

Diese Datensätze dürfen sich nicht überlappen, d.h. sie sind disjunkt. Normalerweise durchmischt man die Daten vor der Aufteilung, denn der Testdatensatz soll ja im Idealfall repräsentativ für die Gesamtdaten sein.

Die Testdaten nennt man auch Holdout-Daten, da man sie dem Modell im Training vorenthält.

Die Aufteilung ist z.B. 80:20, wie oben genannt. Natürlich möchte man soviele Trainingsdaten wie möglich für ein möglichst optimales Modell verwenden. Umgekehrt muss man ausreichend Daten für einen aussagekräftigen Test haben. Bei sehr großen Datensätzen mit Millionen von Datenpunkten, kann auch eine Aufteilung 90:10 oder 95:5 sinnvoll sein.

Die Testdaten sind immer die Daten, mit denen man letztendlich die Performanz des Modells misst und publiziert.

In Wettbewerben - wie z.B. auf der Plattform Kaggle - werden oft die Trainingsdaten komplett bereitgestellt. Bei den Testdaten fehlen aber die Zielwerte (also das $y$). Als Teilnehmer:in eines Wettbewerbs kann man dann die Vorhersagen des eigenen Modells in Form einer Datei einreichen und die Plattform errechnet die Performance und zeigt das Ergebnis in einer großen Tabelle, dem Leaderboard, an. Beachten Sie, dass Sie beim Erstellen eines solchen Modells natürlich nochmal eigene Testdaten zurückhalten, um ihr Modell einzuordnen.

Validierungsdaten

In vielen Kontexten wird oft ein dritter Datensatz hinzugenommen, die Validierungsdaten. Diese werden benutzt, wenn man Hyperparameter wie die Lernrate oder die Trainingsdauer optimieren möchte. Warum das sinnvoll ist, wird hoffentlich gleich klar.

Statt die Daten in Trainings- und Testdaten zu teilen, nehmen wir eine Dreiteilung vor:

  • Trainingsdaten (70%)
  • Validierungsdaten (10%)
  • Testdaten (20%)

Die Prozentangaben sind natürlich nur Beispiele bzw. Anhaltspunkte.

Jetzt nehmen wir an, wir möchten die Trainingsdauer festlegen. Wir wissen, dass ein zu langes Trainings (d.h. zu viele Epochen) zu Overfitting führen kann. Wie finden wir also die optimale Trainingsdauer? Beim Training, wo wir nur die Trainingsdaten für die Updates der Parameter (Gewichte) verwenden, messen wir nach jeder Epoche die Accuracy auf den Validierungsdaten (nicht auf den Testdaten). Das könnte so aussehen:

Sobald die Accuracy auf den Validierungsdaten stagniert oder sinkt, stoppen wir das Training, da wir annehmen, dass zu diesem Zeitpunkt die Generalisierungsfähigkeit des Netzes abnimmt. Man nennt diese Technik auch Early Stopping. In der Abbildung ist das Zeitpunkt $t_{stop}$. Es kann durchaus sein, dass bei einer anderen Wahl von Validierungsdaten dieser Zeitpunkt etwas anders ausfallen würde. Und genau das ist der Grund, warum wir $t_{stop}$ nicht anhand der "echten" Testdaten ermitteln dürfen. Denn letztlich ist auch $t_{stop}$ ein Parameter - genauer gesagt, ein Hyperparameter - und Daten, die man zum Tunen von Parametern und Hyperparametern verwendet, gelten als vom Modell "gesehen".

Ist das Modell also derart trainiert mit der vermeindlich optimalen Trainingsdauer $t_{stop}$, so kann man die Performance auf den tatsächlich "ungesehenen" Testdaten durchführen.

Der Unterschied zwischen Validierungs- und Testdaten ist also, dass die Validierungsdaten bereits während des Trainings (i.d.R. in jeder Epoche) zum Einsatz kommen, um die Performanz des Modell auf "ungesehenen" Daten zu testen, wohingegen die Testdaten erst nach Abschluss des Trainings für einen finalen Test des trainierten und ausgewählten Modells verwendet werden.

4 Cross-Valididierung

Holdout-Daten (egal ob Validierungsdaten oder Testdaten) sind natürlich für das Training "verloren". Man könnte überlegen, dass man genau die "falschen" Daten für das Testen zurückhält, oder dass man zu viele oder zu wenige nimmt. Schließlich möchte man für das Training möglichst viele und möglichst "gute" Daten verwenden.

Ein Verfahren, um die obigen Bedenken abzuschwächen, ist Cross-Validation (im Deutschen auch "Kreuzvalidierung", aber der englische Begriffe ist üblicher). Die Grundidee ist, dass man in mehreren Runden immer andere Validierungsdaten benutzt, und am Ende die jeweils gemessene Performanz mittelt. Wir nehmen hier als Performanzmaß die Accuracy, es könnte aber auch der Fehler (Loss), Precision oder Recall sein.

Wir demonstrieren das hier mit 5-fold cross validation (5-fold CV). Man teilt die Trainingsdaten zufällig in fünf Teile (auch "folds" genannt) $D_1, \ldots, D_5$ mit je 20% der Daten. Natürlich sind die fünf Teilmengen nicht-überlappend (man sagt auch disjunkt).

Die Grundidee ist jetzt, dass man in fünf Durchläufen immer einen andere Teilmenge als Testdaten nimmt. Zunächst wählt man $D_5$ als Testdatensatz und trainiert auf $D_1, \ldots, D_4$. Man misst die Accuracy entsprechend auf $D_5$. In der nächsten Runde setzt man das Netz komplett zurück und training neu.

Wir schauen uns alle fünf Durchläufe mit der entsprechenden Aufteilung und Messung der Accuracy an:

  1. Training auf $D_1, D_2, D_3, D_4$, Test auf $D_5$ => Accuracy $Acc_1$
  2. Training auf $D_1, D_2, D_3, D_5$, Test auf $D_4$ => Accuracy $Acc_2$
  3. Training auf $D_1, D_2, D_4, D_5$, Test auf $D_3$ => Accuracy $Acc_3$
  4. Training auf $D_1, D_3, D_4, D_5$, Test auf $D_2$ => Accuracy $Acc_4$
  5. Training auf $D_2, D_3, D_4, D_5$, Test auf $D_1$ => Accuracy $Acc_5$

Die Gesamt-Accuracy ist dann der Durchschnitt $\frac{1}{5} \sum_i Acc_i$.

Ein Extremfall der Cross-Validierung ist die Leave-One-Out-Methode. Hier wird in jeder Runde nur ein Validierungsbeispiel aus dem Trainingsdatensatz herausgehalten (leave one out). Bei 100 Trainingsbeispielen wird also 1 Beispiel herausgehalten und auf den restlichen 99 Beispielen trainiert. Anschließend wird auf dem einen Validierungsbeispiel gemessen. In den nächsten 99 Runden wird das gleiche mit jeweils anderen Beispielen wiederholt und am Ende werden die 100 Werte gemittelt. Bei $N$ Trainingsbeispielen entspricht Leave-One-Out also einer N-fold cross validation.

4.1 Relevanz für Deep Learning

Aktuell wird im Bereich des Deep Learning allerdings eher selten Cross-Validation angewendet, weil das Verfahren mit einem Mehraufwand verbunden ist: Bei K-fold cross validation vervielfacht sich die Trainingsdauer um Faktor K. Umgekehrt ist das Problem, dass Validierungsdaten "verloren" gehen, gerade bei großen Datenmengen eher marginal.

Umgekehrt ist Cross-Validierung im Bereich Machine Learning, auch z.B. auf Plattformen wie Kaggle, sehr üblich, denn es gibt in den entsprechenden Bibliotheken (z.B. scikit-learn) vorgefertigte Funktionen und Mechanismen, um Cross-Validierung relativ leicht umzusetzen. Es könnte eine Rolle spielen, dass die populären Methoden im allgemeinem ML-Bereich (z.B. Random Forests oder Gradient Boosting) nicht ganz so rechenintensiv sind wie die meisten Deep-Learning-Verfahren.

4.2 Cross-Validierung in Scikit-learn

Hier führen wir 5-fold CV mit dem obigen Modell durch und verwenden Accuracy als Vergleichsmetrik.

Siehe auch https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.cross_val_score.html

In [21]:
from sklearn.model_selection import cross_val_score

scores = cross_val_score(model, X_train, y_train, 
                         scoring='accuracy', cv=5)
scores
Out[21]:
array([0.91683333, 0.91141667, 0.91116667, 0.9095    , 0.92091667])

Als Ergebnis sehen wir die fünf Werte für die fünf Durchläufe. Jetzt berechnet man üblicherweise den Durchschnitt.

In [22]:
print(f'Accuracy = {scores.mean():.3f}')
Accuracy = 0.914

Die Standardabweichung (engl. standard deviation) kann man hinzuziehen, um ein Gefühl dafür zu bekommen, wie stark die fünf Ergebnisse voneinander abweichen. Ist die Abweichung hoch, sollte man z.B. 10-fold CV in Betracht ziehen.

In [23]:
print(f'Standardabweichung = {scores.std():.5f}')
Standardabweichung = 0.00426