Machine Learning Performance Kennzahlen

Metriken zur Performancebestimmung bei Klassifizierungsproblemen

Wir stellen in diesem Artikel eine Reihe von Metriken vor, mit denen unterschiedliche Verfahren und trainierte Modelle für Klassifizierungsprobleme verglichen und bewertet werden können. In unserem Artikel Machine Learning Klassifizierung in Python – Teil 1: Data-Profiling und Vorverarbeitung stellen wir unterschiedliche Klassifizierungsverfahren vor, deren Performance mit den hier beschriebenen Metriken verglichen werden können.

Die Fälle im Detail

Diese Fälle beziehen sich alle auf das vorhergesagte Target eines zu klassifizierenden Objekts. Nachfolgend listen wir dabei alle Fälle zusammen mit ihrer Bedeutung auf.

  • True Negative: Der Fall eines richtig nicht als Treffer eines klassifizierten Targets eines Objekts.
  • False Negative: Der Fall eines falscherweise nicht als Treffer klassifizierten Targets eines Objekts.
  • True Positive: Der Fall eines richtig als Treffer klassifizierten Targets eines Objekts.
  • False Positive: Der Fall eines falsch als Treffer klassifizierten Targets eines Objekts.
Darstellung Klassifizierungsfälle
Darstellung der Klassifizierungsfälle

Beispiel: Fluggastdatenweitergabe an das BKA

Um die Begriffe noch einmal zu verdeutlichen, schauen wir uns einen Artikel zur Weitergabe von Fluggastdaten an das Bundeskriminalamt (BKA) der SZ vom 24. April 2019 an. Seit dem 29. August 2018 wurden Daten von Fluggästen automatisch an das BKA zur Auswertung übermittelt. Mit diesen Daten sollen gesuchte Straftäter und Verdächtige identifiziert werden. Die Daten bestehen aus Namen, Anschrift, Zahlungsdaten und Flugdaten, wie Reisedatum, Uhrzeit, Start- und dem Zielflughafen des Reisenden und werden im Passenger Name Record gespeichert.

Diese Daten sollen zukünftig nicht nur für die Identifizierung von Straftätern dienen, sondern ebenfalls im “Predictive Policing” zum Einsatz kommen. Beim “Predictive Policing” handelt es sich um die Analyse von geschehenen Straftaten zur Berechnung der Wahrscheinlichkeiten zukünftiger Straftaten. Ziel ist es, dass Muster, wie z.B. die Einbruchsrate in einem Wohngebiet, erkannt werden sollen, um so gezielter Polizeikräfte einsetzen zu können.

False Positives bei großen Datenmengen

Seit Beginn der Erfassung bis zum 31. März 2019 wurden insgesamt 1,2 Mio. Fluggastdaten übergeben. Von diesen hat ein Algorithmus 94.098 Flugreisende als potenziell verdächtig klassifiziert. Alle Treffer werden anschließend von Beamten auf ihre Richtigkeit geprüft. Dabei stellte sich heraus, dass von diesen Treffern lediglich 277 echte Treffer (True Positive) waren. Bei den übrigen 93.821 klassifizierten Verdächtigen handelte es sich um falsche Treffer (False Positives). Die Reisenden aus der verbleibenden Menge die falscher Weise nicht als Verdächtige klassifiziert wurden, lassen sich in der Regel nicht genau ermitteln (False Negative). Der Rest der Daten, die richtigerweise nicht als Treffer klassifiziert wurden, ergeben die True Negatives.

Der Artikel kritisiert dabei die hohe Quote an False Positives. Denn dadurch würden häufig Menschen zu unrecht ins Visier von Behörden geraten, in diesem Fall nämlich 99,71% aller Treffer. Dabei ist jedoch zu erwähnen, dass durch eben diese hohe Fehlerrate die Wahrscheinlichkeit für False Negative Fälle sinkt. Das heißt, dass auf Kosten vieler Fehltreffer, nur wenige Zielpersonen nicht getroffen werden.

Häufig verwendete Performancemetriken

Anhand der Anzahl der aufgetretenen Fälle können wir einen Klassifikator unter verschiedenen Metriken betrachten und bewerten. In unserem Beitrag haben wir uns dafür entschieden die folgenden Metriken zu verwenden:

Precision

Eine sehr einfache Metrik ist die Precision. Diese gibt das Verhältnis an, wie viele Objekte ein Klassifikator aus dem Testdatensatz richtig als positiv klassifiziert hat, zu denen die falsch als positiv klassifiziert wurden. Die Formel lautet:

(True Positives) / (True Positives + False Positives)

Problematisch ist diese Metrik besonders bei einer binären Klassifikation, bei der eine Klasse im Beispieldatensatz überrepräsentiert wird: Der Klassifikator kann einfach alle Objekte des Datensatzes der überrepräsentierten Klasse zuordnen, würde aber dennoch eine relativ hohe Precision erreichen.
An dem Beispiel der Fluggastdaten berechnet sich die Precision wie folgt:

277 / (277 + 93.821) = 0,29%

Recall

Im Gegensatz zur Precision, gibt der Recall Score, auch Trefferquote genannt, den Anteil der richtig als positiv klassifizierten Objekte an der Gesamtheit der positiven Objekte an. Das heißt wir erhalten ein Maß zur Bestimmung, wieviele positive Targets als solche erkannt werden. Die Formel lautet:

(True Positives) / (True Positives + False Negatives)

F-Score

Die F-Score setzt Precision und Recall zusammen, um eine möglichst gute Einschätzung darüber zu erhalten, wie genau die Targets der Objekte bestimmt werden.
Dabei berechnet sich der F-Score über das harmonische Mittel mit der Formel:

F = 2 * (Precision * Recall) / (Precision + Recall)

Da wir keine Informationen zu den False Negatives in unserem Beispiel haben, ist es uns nicht möglich, Recall und F-Score zu bestimmen. In unserem Artikel verfügen wir hingegen über diese Information, und können beide Kennzahlen berechnen.

Fortgeschrittene Metriken - Receiver Operating Characteristic

Als letzte Metrik verwenden wir die Receiver Operating Characteristic Kurve. Diese gibt an, wie sicher der Klassifikator seine Ergebnisse erzielt. Dabei wird das Verhältnis der Trefferquote zu den False Positive klassifizierten Daten in Abhängigkeit eines Schwellenwerts in einem Graphen dargestellt.

 

Reciever Operating Characteristic: Area under curve
Reciever Operating Characteristic: Area under curve

Dieser Schwellenwert gibt an, ab wann ein Objekt zur einen oder zur anderen Klasse zugeordnet wird. Dabei ist der Schwellenwert in der linken unteren Ecke des Graphen am größten, das heißt, dass das Modell das Target eines Objekts nur dann als positiv bewertet, wenn ein Objekt mit absoluter Sicherheit dem Target zugehörig ist. Demnach geht die False Positive Rate hier stark gegen 0. Je weiter man sich der rechten oberen Ecke annähert, desto geringer wird der Schwellenwert und desto wahrscheinlicher wird es, dass das Modell ein Objekt fälschlicherweise als positiv klassifiziert, obwohl es eigentlich negativ war (False Positive). Die Fläche unter dem Graphen (Area under curve) dient also als Metrik für die Qualität eines getesteten Modells.

Zeit

Auch wenn die für das Training benötigte Zeit keine Aussage über die Qualität eines Klassifikators liefert, ist es häufig wichtig, zu berücksichtigen wie schnell ein Klassifikator trainiert werden kann. Die verschiedenen Verfahren sind unterschiedlich komplex was dazu führt, dass die Unterschiede der Zeit- und Rechenaufwände signifikant sein können.

Diese Zeitunterschiede können mit zunehmender Datensatzgröße exponentiell anwachsen, weshalb der Aspekt der Zeit, die ein Klassifikator bis zum Erreichen einer gewissen Genauigkeit benötigt, nicht zu vernachlässigen ist, besonders dann, wenn man mit sehr großen Datenmengen arbeitet. Um diesen Aspekt zu verdeutlichen, haben wir unsere Modelle auf reduzierten Datensätzen trainiert und die zeitlichen Unterschiede gemessen. Dabei haben wir folgendes Ergebnis erhalten.

Logarithmische Veränderung der Trainingszeiten bei Veränderung der Datenmenge
Logarithmische Veränderung der Trainingszeiten bei Veränderung der Datenmenge

Machine Learning Klassifizierung in Python - Teil 1: Data-Profiling und Vorverarbeitung

Dies ist der erste Teil der Serie Automatisierte Klassifizierung in Python in der wir beispielhaft zeigen, wie man einen gegebenen Datensatz mithilfe von Machine Learning Klassifizierungsverfahren in Python klassifizieren kann.

In folgendem Beitrag zeigen wir die Analyse und Aufbereitung des frei verfügbaren "Adult" Datensatzes zur Klassifizierung. Ebenfalls haben wir unser Skript zusammen mit den Datensätzen und der Dokumentation auf GitHub veröffentlicht.

Der Datensatz entstammt dem Machine Learning Repository der University of California Irvine. Dieses beinhaltet derzeit 473 Datensätze (zuletzt aufgerufen: 10. Mai 2019), die für Machine Learning Anwendungen zur Verfügung stehen. Der “Adult” Datensatz’’ basiert auf US-Zensus Daten. Ziel ist es, anhand der gegebenen Daten zu bestimmen, ob eine Person mehr oder weniger als 50.000 USD im Jahr verdient.

Data-profiling

Der erste Schritt den wir vornehmen, bevor wir mit der eigentlichen Klassifizierung beginnen können, ist, dass wir uns die Struktur des Datensatzes anschauen. Dabei stellen wir fest, dass der Datensatz aus zusammengenommen ca. 45.000 Personendaten besteht und bereits in Trainings- und Testdaten aufgeteilt ist.

Ein Teil der Daten (ca. 7,5%) sind unvollständig da für einzelne Personen Datenpunkte (Features) nicht angegeben wurden. Aufgrund der relativ niedrigen Zahl der fehlerhafter Datensätze, werden wir diese zunächst einfach in der Analyse ignorieren.

Die Personendaten bestehen aus kontinuierlichen und kategorischen Features der Personen. Die kontinuierliche Daten sind: Alter, ‘final weight’, Bildungsjahre, Kapitalzuwachs, Kapitalverlust und Wochenstunden. Die kategorischen Daten sind: Beschäftigungsverhältnis, Abschluss, Familienstand, Beruf, Beziehung, Rasse, Geschlecht und das Geburtsland.

Unsere Zielvariable ist das Einkommen einer Person, genauer ob eine Person weniger oder mehr als 50.000 US-Dollar im Jahr verdient. Da unsere Zielvariable nur zwei verschiedene Werte annehmen kann, handelt es sich um eine binäre Klassifikation. Innerhalb des Datensatzes beträgt das Verhältnis zwischen Personen die weniger als 50.000 US-Dollar verdienen, zu jenen, die mehr verdienen ca. 3:1.

Analyse der Featureeigenschaften

Bei der Analyse der einzelnen Features ist das Feature ‘final weight besonders aufgefallen: Dieses gruppiert ähnliche Personen, basierend auf sozioökonomischen Faktoren und diese Wertung ist abhängig vom US-Bundesstaat in dem eine Person lebt.  Aufgrund des relativ kleinen Datensatzes und der ungenauen Dokumentation der zu Grunde liegenden Berechnung, haben wir uns entschieden, dieses Feature in der ersten Berechnung nicht zu berücksichtigen. Ein späterer direkter Vergleich zeigte, dass das Auslassen dieses Features in Einzelfällen zu einer Verbesserung der Klassifikationsergebnisse führte, aber nie zu einer Verschlechterung.

Um die Problemstellung zu lösen, anhand der genannten Features einer Person ihr Einkommen vorherzusagen, verwenden wir einen supervised Machine Learning Ansatz, da wir über viele gelabelte Daten verfügen. Anhand dieser kann der Algorithmus die Abhängigkeit der einzelnen Features zum Target abschätzen. Im zweiten Teil unseres Beitrags stellen wir dafür einige Verfahren vor, die wir bereits in unserem Blog behandelt haben. All diese Verfahren setzen jedoch zunächst eine sehr genaue Vorverarbeitung der Daten voraus, damit unser Modell in der Lage ist, diese zu bewerten und Werte wie etwa “Montag” oder “Dienstag” zu  interpretieren. Man spricht vom “cleaning” der Daten.

Vorverarbeitung der Daten

Wir müssen unsere Daten zunächst vorverarbeiten, um auf diese die verschiedenen Machine Learning Modelle anwenden zu können. Die verschiedenen Modelle vergleichen die einzelnen Features der Daten miteinander, um den Zusammenhang zum Target zu bestimmen. Dafür müssen die Daten in einer einheitlichen Form vorliegen, um dadurch eine Vergleichbarkeit zu ermöglichen. Deshalb sprechen wir vom säubern der Daten.

Mit der folgenden Funktion säubern wir unsere Daten. Die Funktionsweise erläutern wir in den folgenden Abschnitten:

def setup_data(self):
        """ set up the data for classification """
        traindata = self.remove_incomplete_data(self.traindata)
        testdata = self.remove_incomplete_data(self.testdata)
        
        self.y_train = self.set_target(traindata)
        self.y_test = self.set_target(testdata)

        # set dummies of combined train and test data with removed target variable
        fulldata = self.get_dummies(traindata.append(testdata, ignore_index=True).drop(self.target, axis=1).drop("fnlwgt", axis=1), self.categorical_features)
        self.x_train = fulldata[0:len(traindata)]
        self.x_test = fulldata[len(traindata):len(fulldata)]

Unser Datensatz ist zwar bereits in einen Trainings- und einen Testdatensatz im Verhältnis 2:1 aufgeteilt, für die Erzeugung von Dummyvariablen müssen wir ihn dennoch zwischenzeitlich zusammenführen, um ihn später wieder im gleichen Verhältnis aufteilen zu können. Dieses Vorgehen bietet den entscheidenden Vorteil, dass die entstehenden Datensätze unter allen Umständen die gleiche Form und Dimensionalität besitzen. Andernfalls kann es passieren, dass wenn ein Wert in entweder dem Trainings- oder Testdatensatz fehlt, der jeweils neue Datensatz weniger Spalten besitzt, bzw. die Spalten mit gleichem Index für verschiedene Feature-Werte stehen. Dadurch geht die Vergleichbarkeit der beiden Datensätze verloren.

Weiterhin kommen in dem Datensatz vereinzelt unbekannt Werte vor, die wir gezielt angehen müssen. Der Anteil an Daten mit unbekannten Werten des Datensatzes ist allerdings relativ klein (<10%). Deswegen ist es uns möglich, diese unvollständigen Daten außen vor zulassen und aus dem Datensatz zu entfernen. Dies erreichen wir in der Funktion "setup_data" durch den Aufruf unserer Funktion "remove_incomplete_data":

def remove_incomplete_data(self, data):
    """ Remove every row of the data that contains atleast 1 "?". """
    return data.replace("?", np.nan).dropna(0, "any")

Bei dieser werden alle Zeilen die mindestens ein "?" enthalten aus dem Datensatz entfernt. Wir tun dies, um sicherzustellen, dass der Algorithmus stets relevante Daten erhält und keine Relationen zwischen unbekannten Werten erstellt. Diese würden bei der späteren Erzeugung der Dummy-Variablen als gleiche Werte angesehen und nicht als unbekannt interpretiert werden. Nachdem wir die Funktion ausgeführt haben, besteht unser Datensatz jetzt noch aus 45.222 Einträgen, im Gegensatz zu den Vorherigen 48.842.

Zuweisen der Zielvariable

Im zweiten Teil der "setup_data" Funktion ordnen wir über den Funktionsaufruf "set_target" der Zielvariable die Werte 0 oder 1 zu, je nachdem, ob jemand mehr oder weniger als 50.000 US-Dollar im Jahr verdient.

def set_target(self, data):
    """ Set the target values of the target variables (0,1 for either case). """
    for i in range(len(data[self.target].unique())):
        data[self.target] = np.where(data[self.target] == data[self.target].unique()[i], i, data[self.target])
    return data[self.target].astype("int")

Kategorische Werte mit Dummy-Variablen ersetzen

Bevor wir nun damit beginnen, eine Klassifizierung der Daten vorzunehmen, müssen wir zunächst dafür sorgen, dass unser Modell in der Lage ist, mit kategorischen Werten umzugehen. Dafür erzeugen wir aus allen kategorischen Variablen sog. Dummy-Variablen über das one-hot encoding Verfahren. Dabei erhält jede mögliche Belegung einer kategorischen Variable eine eigene Variable, sodass anstelle einer einzelnen Variable, die unterschiedliche Werte annehmen kann, viele Variablen existieren, die jeweils nur den Wert 0 oder 1 annehmen können und die jeweils eine kategorische Belegung der ersetzen Variable repräsentieren.

Motivation

Ein Beispiel: Wir haben ein Objekt vom Typ “datum” mit dem Feature ‘wochentag = {'Montag', 'Dienstag', 'Mittwoch', ...}’. Nach der Erzeugung der Dummy-Variablen gibt es das Feature ‘wochentag’ nicht mehr. Stattdessen stellt jede mögliche Belegung ein eigenes Feature dar. Diese sind in unserem Beispiel: wochentag_dienstag, …, wochentag_sonntag. Je nachdem, welchen Wochentag das Feature vor der Erzeugung besaß, wird diese Variable auf 1 und die Restlichen auf 0 gesetzt.

Der aufmerksame Leser fragt sich an dieser Stelle bestimmt, wieso das Feature wochentag_montag nicht existiert. Der einfache Grund für das Auslassen liegt darin, dass sich aus der negativen Belegung der anderen Features implizit schließen lässt, dass ein Objekt den Wert wochentag_montag besitzt. Ein weiterer Vorteil besteht darin, dass eine zu starke Abhängigkeit, Multikollinearität, der einzelnen Variablen vermieden wird. Diese könnte sich negativ auf das Ergebnis auswirken, da die starke Abhängigkeit es erschweren kann, den genauen Effekt einer bestimmten Variable in einem Modell zu bestimmen. Die Erzeugung der Dummy-Variablen ist deshalb notwendig, da wie bereits erwähnt, ein Modell kein Wissen über einen Wochentag hat und wie es diesen interpretieren soll. Nach der Erzeugung der Dummyvariablen spielt dies auch keine Rolle mehr, da der Algorithmus nur noch unterscheidet, ob das Feature eines Objekt über den Wert 0 oder 1 verfügt. Dadurch wird ein Vergleich der einzelnen Objekte mit den jeweiligen Features möglich.

Umsetzung

Im letzten Teil unserer Funktion "setup_data" haben wir die Erzeugung der Dummys über den Aufruf der Funktion "get_dummies" wie folgt realisiert:

def get_dummies(self, data, categorical_features):
    """ Get the dummies of the categorical features for the given data. """
    for feature in self.categorical_features:
        # create dummyvariable with pd.get_dummies and drop all categorical variables with dataframe.drop
        data = data.join(pd.get_dummies(data[feature], prefix=feature, drop_first=True)).drop(feature, axis=1)
    return data

Wir erzeugen eine Schleife, die alle kategorischen Variablen des Datensatzes durchläuft. Bei jedem Durchlauf hängen wir dem Datensatz alle Dummyvariablen der jeweiligen kategorischen Variable mit Hilfe der Pandas Funktion "get_dummies" an. Anschließend entfernen wir diese kategorische Variable. Nach Abschluss der Schleifenanweisung enthält unser Datensatz keine kategorischen Variablen mehr. Stattdessen besitzt er die jeweiligen Dummyvariablen.

So erhalten wir aus den ursprünglichen Features:

          age   workclass
Person1   39    Local-gov
Person2   50    Federal-gov

Die Folgenden:

          age   workclass_Federal-gov  workclass_Local-gov  workclass_Never-worked
Person1   39    0                      1                    0
Person2   50    1                      0                    0

Hierbei wird der Grund für das zwischenzeitliche Zusammenfügen der beiden Datensätze nochmals deutlich: Falls z.B. der Wert “Local-gov” in nur einem der Datensätze vorhanden ist, verfügen bei der Erzeugung der Dummyvariablen die entstehenden Datensätze über verschiedene Dimensionalitäten, da in dem anderen Datensatz die gesamte Spalte fehlt.

Beispiel: Wenn das Modell beispielsweise einen großen Zusammenhang zwischen “Local-gov” und einem Einkommen von über 50.000 USD herstellt, verschiebt sich dieser Zusammenhang in dem anderen Datensatz zu dem Feature, dass den Platz von “Local-gov” belegt. Daraus resultiert wahrscheinlich ein falsches Ergebnis aber in jedem Fall ein falscher Zusammenhang.

Im letzten Teil der “setup_data” Funktion teilen wir die Datensätze wieder in einen Trainings- und Testdatensatz auf.

self.x_train = fulldata[0:len(traindata)]
self.x_test = fulldata[len(traindata):len(fulldata)]

Im zweiten Teil gehen wir darauf ein, wie wir die aufbereiteten Daten auf verschiedene Klassifikatoren anwenden und anschließend die Ergebnisse vergleichen und evaluieren können.