Artikelserie

Machine Learning mit Klassifikationsverfahren: Data Analytics in der Praxis Teil 2
Keine Kommentare

In diesem Artikel beschäftigen wir uns mit einer der weitverbreitetsten Problemstellungen des Machine Learnings: der Klassifikation. Bei der Klassifikation geht es darum, aufgrund von Beobachtungen eine Entscheidung darüber zu treffen, in welche Klasse das beobachtete Ereignis fällt.

Klassifikationsentscheidungen treffen wir als Menschen laufend: Wer ist die Person, die vor mir steht? Wie ist das Wetter heute? Was für ein Obst liegt da im Korb? Welches Lied läuft da im Radio? Diese Fragen haben alle eins gemeinsam: Im Gegensatz zu einer klar beschreibbaren Entscheidungsregel wie „Ist die Raumtemperatur höher als 20° C?“ ist hier die Entscheidung nicht so einfach in prozeduralen Code überführbar. Der für die Entscheidung benötigte Input ist vielschichtiger als ein einzelner Sensorwert, und es gibt durchaus Situationen, in denen man nicht so klar entscheiden kann.

Natürlich kann auch bei solchen Fragen versucht werden, die Problemstellung zu durchdringen und prozedurale Entscheidungsbäume abzuleiten. Das ist in den allermeisten Fällen jedoch zu aufwendig und daher nicht praktikabel. Mit Machine Learning kann man diesen Prozess allerdings automatisieren.

Artikelserie

Das Experiment

Um uns der Klassifikation zu nähern, verwenden wir in diesem Artikel eine Aufgabenstellung, die wir im Kontext der Entwicklung von docoyo.Trackable (eine Tracking-Lösung, die Nico und Masa für die industrielle Produktion entwickelt haben) untersucht haben. Als eine von mehreren Technologien verwendet docoyo.Trackable Bluetooth-Beacons für das Tracking. Die Beacons werden an Werkstücken bzw. Transportbehältern angebracht und ihre Signale von Scannern empfangen, die in der Werkstatt/-halle verteilt sind. Die Signale werden an einen zentralen Server weitergeleitet und dort für die Positionsbestimmung genutzt. Über genau diese Serverkomponente machen wir uns in diesem Artikel Gedanken.

Ein naiver (aber durchaus valider) Ansatz, um die Position von getrackten Elementen zu bestimmen, ist das Platzieren von Scannern an allen interessanten Stellen, z. B. an Arbeitsplätzen, Stellflächen für Kanban-Puffer, an Lagerplätzen oder an Kontrollpunkten – wir nennen solche Stellen „Zonen“. Solange die Scanner günstig platziert sind, darf man annehmen, dass sich ein Beacon jeweils in der Nähe desjenigen Scanners befindet, der den stärksten Signalempfang aufweist. Die Situation in der Realität sieht jedoch oftmals komplizierter aus: Um die Anzahl der Scanner nicht ausufern zu lassen, möchten wir eine höhere Anzahl von Zonen erkennen können, als es Scanner gibt. In dieser Situation ist die Stärkste-Signal-Regel alleine nicht mehr ausreichend. So eine Situation haben wir für diesen Artikel als Experiment bei Masa zu Hause aufgebaut. Der Plan in Abbildung 1 zeigt das Erdgeschoss mit drei Scannern.

Abb. 1: Scannerpositionierung

Abb. 1: Scannerpositionierung

Wir haben die Scanner möglichst weit voneinander entfernt im Dreieck positioniert, damit die Signale der Beacons sich möglichst gut unterscheiden. Würden wir uns im Freifeld ohne Störeinflüsse befinden, würden mit der Stärkste-Signale-Regel die in Abbildung 2 zu sehenden Zonen erkannt werden können.

Abb. 2: Scannerbereiche bilden Räume nicht gut ab

Abb. 2: Scannerbereiche bilden Räume nicht gut ab

Es ist offensichtlich, dass es nicht ausreicht zu erkennen, dass sich ein Beacon in einem dieser Kreise befindet. Vielmehr möchten wir gerne erkennen, in welchem Raum sich ein Beacon befindet. Und diese sind nun einmal nicht kreisförmig.

Unsere Hypothese ist, dass wir die Räume mithilfe von Klassifikationsverfahren trotzdem erkennen können. Wir möchten, dass sich unser Machine-Learning-Modell aufgrund der Beobachtung der jeweils empfangenen Signalstärken eines Beacons an den drei Scannern für einen Raum entscheidet. Machine-Learning-Modelle benötigen Daten, um zu lernen. Um in möglichst kurzer Zeit möglichst viele Daten zu sammeln, haben wir in unserem Experiment 36 Beacons im Erdgeschoss mehr oder weniger gleichmäßig verteilt (Abb. 3).

Abb. 3: Beacon-Verteilung im EG

Abb. 3: Beacon-Verteilung im EG

Nun haben wir über Nacht die Signale aufgezeichnet, die von den Scannern aufgefangen und an den zentralen Server weitergeleitet wurden. Über acht Stunden hinweg wurden ca. 1,5 Mio. Zeilen in eine CSV-Datei geloggt, die jeweils die in Listing 1 gezeigte Struktur haben.

Timestamp,BeaconType,ScannerID,RSSI,UUID,Major,Minor,Name,TXPower,Battery
2017-02-02 23:11:08.347756,0,b827eb3f7749,91,f7826da64fa24e988024bc5b71e0893e,4e98,698a,jYGu,12,94
2017-02-02 23:11:08.405497,0,b827eb4f4f2c,91,f7826da64fa24e988024bc5b71e0893e,5aa7,de4e,XEzz,12,64
2017-02-02 23:11:08.406028,0,b827eb4f4f2c,96,f7826da64fa24e988024bc5b71e0893e,4de0,c728,3tPN,12,55
2017-02-02 23:11:08.423392,0,b827eb3f7749,97,f7826da64fa24e988024bc5b71e0893e,c7a5,6c12,t6Eh,12,58
2017-02-02 23:11:08.424777,0,b827eb3f7749,73,f7826da64fa24e988024bc5b71e0893e,8651,80ab,NYjS,12,62

Die Rohdaten können für eigene Experimente auf unserer Website heruntergeladen werden. Jede Zeile im Beispiel entspricht einem Beacon-Advertising-Paket, das von einem der drei Scanner aufgefangen wurde. Es gibt also bis zu drei Zeilen pro ausgesendetem Beacon-Advertising-Paket im Logfile. Diese treffen wegen Verzögerungen in der Treiberschicht des Bluetoothempfängers und wegen Latenzzeiten in der Netzwerkkommunikation zwischen den Scannern und dem Server naturgemäß nicht gleichzeitig ein. Doch wie gewinnt man aus einem solchen Strom von Signalereignissen eine geeignete Datenbasis zum Anlernen des Machine-Learning-Modells?

Feature-Engineering

Es hilft, sich vorzustellen, wie man sich dieser Aufgabe als Mensch stellen würde. Unsere Machine-Learning-Aufgabe lautet abstrakt formuliert: „Wie sehen typische RSSIs im Raum X, Raum Y, Raum Z aus?“ Um diese Frage zu beantworten, würde man pro Beacon jeweils die eintreffenden Signalstärken beobachten und sich dazu den Raum notieren, in dem sich der Beacon befindet. Dabei ist zu berücksichtigen, dass die Signalstärken naturgemäß schwanken, die Signale jedoch in etwa gleich bleiben, solange man die Beacons nicht verschiebt oder andere größere Störquellen in die Räume gebracht werden. Das heißt, man würde die RSSIs (Received Signal Strength Indicator) über ein gewisses Zeitfenster – sagen wir über 15 Sekunden – mitteln und weiterverwenden. Genau diese Art von Vorverarbeitung von Rohdaten vor dem eigentlichen Training nennt man Feature-Engineering.

Wir verwenden wieder das im letzten Artikel bereits eingeführte Toolset aus Python, Pandas, scikit-learn und arbeiten im Editor Spyder (siehe auch Kasten „Quereinstieg: Schnellinstallation der benötigten Tools“).

Quereinstieg: Schnellinstallation der benötigten Tools

Wenn Sie den letzten Artikel nicht mehr zur Hand haben, können Sie sich die Umgebung schnell aufsetzen, indem Sie folgende Schritte ausführen:

  • Anaconda herunterladen und installieren. Das ist ein Bundle aus Python-Interpreter und für Data Analytics oft verwendeten Bibliotheken.
  • Spyder wie beschrieben installieren. Das ist ein interaktiver Python-Editor, der es Ihnen erlaubt, den zu entwickelnden Code parallel in einer Konsole zeilenweise auszuführen und zu debuggen.
  • Falls Sie bei einem der folgenden Beispiele eine Fehlermeldung bekommen sollten, dass eine Bibliothek nicht verfügbar ist, führen Sie auf der Kommandozeile einfach pip install [Bibliothek-Name] aus.

Zunächst lesen wir die o. g. CSV-Datei mithilfe von Pandas ein und geben den einzelnen Spalten aussagekräftige Namen:

df = pd.read_csv('log.csv', header=None, names=['ts', 'beacon_type', 'scanner_mac', 'rssi', 'uuid', 'major', 'minor', 'name', 'tx_power', 'battery_level'], parse_dates=['ts'])

Mit der folgenden Zeile setzen wir einen Index auf die Timestamp-Spalte, d. h. der Timestamp wird der Zugriffsschlüssel für die Zeilen im DataFrame.

df = df.set_index('ts')

Von den eingelesenen Spalten benötigen wir jedoch im weiteren Verlauf neben dem Timestamp nur die Scanner-MAC-Adresse, RSSI und Name des Beacons. Wir filtern also aus:

df_flt = df[['scanner_mac', 'rssi', 'name']]

Nun restrukturieren wir den DataFrame so um, dass wir eine Pivot-Tabelle erhalten, in der Timestamp die Zeilen und das Tupel aus Scanner-MAC-Adresse und Beacon-Name die Pivot-Spalten darstellt:

df_flt = df_flt.reset_index()
df_flt_pv = df_flt.pivot_table(index='ts', columns=['scanner_mac', 'name'], values='rssi')

Nun kommt eine entscheidende Operation. Wir bilden für jeweils ein 15-Sekunden-Fenster die durchschnittlichen RSSIs pro Zelle ab:

df_flt_pv = df_flt_pv.resample('15S').mean()

Um die Pivot-Tabelle für die weiteren Schritte wieder flach zu klopfen, bedienen wir uns eines Tricks. Zunächst ziehen wir das Feld name als Pivot-Spalte heraus und verwenden es als zweite Ebene des Zeilenindex:

df_flt_pv = df_flt_pv.stack('name')

Gleich danach schmeißen wir das Feld name wieder als Index – also als Gruppierungsebene für Zeilen – heraus und machen daraus eine reguläre Spalte:

df_flt_pv = df_flt_pv.reset_index('name')

Zu diesem Zeitpunkt sieht das DataFrame df_flt_pv so aus, dass wir pro 15-Sekunden-Zeitfenster und Beacon-Name jeweils den durchschnittliche RSSI pro Scanner erhalten (Listing 2).

scanner_mac   name  b827eb3f7749  b827eb4f4f2c  b827eb7b2b56
ts                                                          
2017-02-02 23:11:00  3tPN        NaN        96.0         NaN
2017-02-02 23:11:00  5kl6        NaN         NaN        93.0
2017-02-02 23:11:00  B0XS       92.0         NaN         NaN
2017-02-02 23:11:00  Dvrb        NaN         NaN        78.0
2017-02-02 23:11:00  NYjS       73.0         NaN        98.0
2017-02-02 23:11:00  R2vA       92.0         NaN         NaN
...

Im DataFrame df_flt_pv fehlt noch die Information darüber, in welchem Raum ein Beacon liegt. Diese Liste haben wir händisch erstellt und in einer anderen CSV-Datei beacons.csv abgelegt. Sie sieht wie folgt aus:

BeaconName,RoomNo
XEzz,3
3tPN,1
t6Eh,3
NYjS,3
Dvrb,2
xKbY,2
...

Diese Liste lesen wir in einen anderen DataFrame ein:

zones = pd.read_csv('beacons.csv', header=None, names=['name', 'zone'])

Nun müssen wir nur noch eine Merge-Operation durchführen, die die beiden DataFrames miteinander verknüpft:

df_learn = pd.merge(df_flt_pv, zones, on='name')

Bei dieser Aktion verlieren wir auch gleichzeitig die Timestamp-Spalte als Index, was aber kein Problem ist, weil wir annehmen, dass der Zeitstempel für das Training keine Rolle spielt. Weil der Klassifikator nicht damit zurechtkommt, wenn Inputwerte fehlen, füllen wir nun abschließend die Lücken mit einem Standardwert aus, der für ein sehr schwaches Signal steht:

df_learn_nona = df_learn.fillna(100)

Das final transformierte DataFrame sieht jetzt wie in Listing 3 aus.

name  b827eb3f7749  b827eb4f4f2c  b827eb7b2b56  zone
0   3tPN         100.0          96.0         100.0     1
1   5kl6         100.0         100.0          93.0     2
2   B0XS          92.0         100.0         100.0     3
3   Dvrb         100.0         100.0          78.0     2
4   NYjS          73.0         100.0          98.0     3
5   R2vA          92.0         100.0         100.0     2
6   R7qv          71.0         100.0         100.0     3

Zum Trainieren von Machine-Learning-Modellen (ML-Modelle), aber auch für zahlreiche Vorverarbeitungen steht in Python die mächtige Bibliothek scikit-learn zur Verfügung. Sie ist sehr gut designt und hat ein einheitliches Interface für das Anlernen von Modellen. Allerdings funktioniert die Bibliothek nicht direkt zusammen mit Pandas DataFrames, stattdessen erwartet sie die Eingaben als NumPy-Arrays. NumPy wiederum ist eine sehr mächtige Bibliothek für numerische Berechnungen, die bereits seit den Anfängen von Python für die Ausführung von numerischen Berechnungen zur Verfügung steht.

Entsprechend muss der DataFrame in ein NumPy-Array konvertiert werden, was allerdings kein Problem darstellt, weil Pandas intern schon mit NumPy-Arrays arbeitet. Entsprechend kann beim vorliegenden DataFrame einfach auf das Feld value zugegriffen werden.

array = df_learn_nona[df_learn_nona.columns.drop('name')].values

In diesem Fall wird zusätzlich die Spalte name entfernt, weil sie für das Modell keine relevanten Informationen enthält. Das Resultierende array enthält jetzt folgende Daten:

 array([[  99.        ,   71.        ,   90.        ,    2.        ],
       [ 101.5       ,   72.35294118,   88.375     ,    2.        ],
       [ 100.        ,   71.66666667,   88.57142857,    2.        ],
       ...,

Das ist also eine genaue Abbildung des DataFrames, in einer zweidimensionalen Matrix, in der die ersten drei Spalten die RSSI-Werte der drei Scanner und die vierte Spalte die vorherzusagende Zone ist. Die ersten drei Spalten bilden damit die Features, die verwendet werden sollen, um den Zielwert in der vierten Spalte vorherzusagen. Entsprechend müssen sie auch getrennt werden, um ein scikit-learn-ML-Modell zu trainieren.

X = array[:,:3]
y =  array[:,3].astype(int)

Eine generell verwendete Konvention ist es, die Featurematrix als X zu bezeichnen und den Zielvektor als y. Die Konvertierung des Zielvektors als Integer ist hier vor allem zur Verdeutlichung, dass es sich um diskrete Klassen handelt. Nun sind wir soweit, dass wir die Klassifikatoren trainieren können.

Modellauswahl

Das beste Modell zu finden, hat sowohl mit einer Vorüberlegung auf Basis der Eingabedaten als auch etwas mit Ausprobieren zu tun. Wir verwenden hier zwei der bekanntesten Klassifikatoren: die logistische Regression und Random Forest.

Die logistische Regression ist ein sehr einfaches und damit gut interpretierbares Modell, um eine Unterscheidung zwischen zwei Klassen zu treffen. Eine Erweiterung des Modells zur Erkennung mehrerer unabhängiger Klassen kann man realisieren, indem man mehrere Modelle trainiert, von denen jedes eine Entscheidung für oder gegen eine Klasse trifft. Eine wichtige Eigenschaft der logistischen Regression ist die Annahme, dass die Werte der Features eine Sortierung besitzen und somit ein natürlicher Übergang zwischen den Klassen besteht, wenn die Werte größer bzw. kleiner werden.

Im Gegensatz dazu beruht ein Random-Forest-Klassifikator auf Entscheidungsbäumen. Diese wiederum bestehen aus Knoten, die jeweils eine Entscheidung auf einem Feature abbilden. Das kann folgendermaßen aussehen:

Rssi Scanner A > -78
  Rssi Scanner B < -56 Rssi Scanner C > -57
Zone 1    Zone 2      Zone 1    Zone 99

Die einzelnen Entscheidungsbäume werden vom Klassifikator verwaltet und die Entscheidung für eine Klasse wird im einfachsten Fall anhand der gemittelten Vorhersage aller Entscheidungsbäume getroffen.

Das Training

Ein wichtiger Schritt, der vor dem Training durchgeführt werden sollte, ist die Aufteilung des Datensatzes in Test- und Trainingsdaten. Diese Aufteilung bietet neben der Kreuzvalidierung eine weitere Sicherheit darüber, ob das Modell zu stark an die Trainingsdaten angenähert oder zu stark generalisiert wurde. Im Allgemeinen empfiehlt es sich, jeweils ein eigenes Experiment zu machen, in dem Trainings- und Testdaten aufgenommen werden. Im Falle der Beacon-Signale wäre das eine Messung an mehreren Tagen und dann die gleiche Messung noch einmal eine Woche später. So kann sichergestellt werden, dass die aufgenommenen Daten allgemeingültig sind, genauso wie ein darauf trainiertes Modell.

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.3, random_state=0)

Das Training eines Modells sollte immer mit der so genannten Kreuzvalidierung erfolgen. Bei der Kreuzvalidierung wird für das Training nur ein Teil des Trainingsdatensatzes und der verbleibende Teil für die Bewertung der Modellqualität verwendet. Bei einer fünffachen Kreuzvalidierung, wie wir sie hier verwenden, wird das Modell mit 80 Prozent der Eingabedaten trainiert und die restlichen 20 Prozent werden zur Validierung verwendet. Das Ganze wird fünfmal in anderer Kombination wiederholt, sodass jeder Teil der Datenmenge mal zum Training eines Modells, ein anderes Mal zur Validierung verwendet wurde. Aus den fünf Modellen kann dann sowohl das Beste ausgewählt als auch beobachtet werden, wie stark die Vorhersagequalität von der Datenauswahl abhängig ist. Mit der folgenden Zeile definieren wir die Splitstrategie für den nachfolgenden Trainingslauf:

skf = StratifiedKFold(n_splits=5)

Die StratifiedKFold-Strategie teilt den Datensatz so auf, dass sie die Häufigkeiten der einzelnen Klassen in den Splits erhält. Bei unserem Datensatz bedeutet dies eine Aufteilung, bei der in jedem Teil proportional viele Messungen aus jedem Raum enthalten sind.

Für die Kreuzvalidierung bietet scikit-learn spezielle Versionen einiger Modelle an, wie z. B. die LogisticRegresssionCV. Weil für den Random Forest kein solches Modell existiert, bietet es sich an, das Metamodell GridSearchCV zu nutzen. GridSearchCV bietet darüber hinaus die Möglichkeit, Modelle mit verschiedenen Parametersätzen zu trainieren – eine übliche Praxis, um das Modell zu verbessern. Zunächst trainieren wir eine logistische Regression:

lr_cv = GridSearchCV(param_grid={'C': [1] }, estimator=LogisticRegression(), cv=skf)
lr_cv.fit(X_train, y_train)

Der Ausdruck ‚C‘:[1] ist ein Standardwert eines Parameters für die logistische Regression. Zur Kreuzvalidierung wird die kurz vorher definierte StratifiedKFold-Strategie verwendet.

Anschließend stecken wir die beiseitegelegten Testdaten X_test in das trainierte Modell, um es die Wahrscheinlichkeiten für die Räume vorhersagen zu lassen:

lr_y_ref = lr_cv.best_estimator_.predict_proba(X_test)

Das Ergebnis ist eine Matrix mit vier Spalten und genauso vielen Zeilen, die mit X_test eingegeben wurden. Jede Zeile der Matrix bildet eine Modellvorhersage ab, in der für jeden Raum eine Wahrscheinlichkeit des Aufenthalts vorhergesagt wird. Intern arbeitet der Klassifikator mit mehreren Modellen, die jeweils eine Wahrscheinlichkeit für einen Aufenthalt in einem Raum bestimmen. Diese Wahrscheinlichkeiten werden dann skaliert, damit das Ergebnis einer Vorhersage als Gesamtwahrscheinlichkeit immer 100 Prozent zurückliefert. So kann man sich durch ein schnelles Zusammenfügen des wahren Raums mit den vorhergesagten Ergebnissen stichprobenartig testen (Listing 4).

pd.DataFrame(lr_y_ref[1000:1010, :], y_test[1000:1010])

Wahr  1           2         3        99
2   0.380738  0.595777  0.000104  0.023380
1   0.673888  0.009459  0.001279  0.315374
1   0.670049  0.001124  0.153993  0.174835
1   0.733558  0.007563  0.117235  0.141643
3   0.032285  0.243624  0.609321  0.114770
99  0.753693  0.016763  0.013791  0.215753
1   0.646478  0.000351  0.041981  0.311190
1   0.537935  0.004847  0.041809  0.415408
2   0.011304  0.710406  0.276332  0.001959
2   0.094731  0.645157  0.220391  0.039721

Anhand der Ausgabe kann schnell erkannt werden, dass in diesem kurzen Abschnitt die Vorhersage der Klasse 1, 2 und 3 sehr gut funktioniert, da die jeweilige Klasse den höchsten Wahrscheinlichkeitswert besitzt. Das Gleiche führen wir nun auch mit dem RandomForestClassifier durch, wobei die stichprobenhafte Evaluierung dem Leser überlassen wird:

rf_cv = GridSearchCV(param_grid={'n_estimators': [10] }, estimator=RandomForestClassifier(), cv=skf)
rf_cv.fit(X_train, y_train)
rf_y_ref = rf_cv.best_estimator_.predict_proba(X_test)

Qualitätsprüfung

Auf den ersten Blick scheinen beide Modelle recht gut zu funktionieren. Anhand von wenigen Stichproben lässt sich jedoch wenig zuverlässig die tatsächliche Qualität des Modells beurteilen. Umgangssprachlich formuliert ist ein gutes Modell ein solches, das richtige Vorhersagen macht. „Richtigkeit“ kann dabei von zwei verschiedenen Gesichtspunkten bewertet werden:

  • Eine zutreffende Tatsache wird vom Modell richtigerweise vorhergesagt (True Positive).
  • Eine nicht zutreffende Tatsache wird vom Modell nicht fälschlicherweise vorhergesagt (False Positive).

Was wie Haarspalterei klingt, kann in der Realität von großer Bedeutung sein: Was würde passieren, wenn ein Modell fälschlicherweise eine Krankheit diagnostiziert, die zur Verabreichung von gefährlichen Medikamenten führt? In einem solchen Fall möchte man die False-Positive-Rate so niedrig wie nur möglich halten. Bei der Erkennung von Eindringlingen in Computernetzwerke hingegen ist man dankbar, wenn man auch den kleinsten Verdacht gemeldet bekommt, d. h. die True-Positive-Rate soll möglichst hoch sein. False Positives sind hingegen nicht so problematisch. Leider stehen die Maximierung der True-Positive-Rate und die Minimierung der False-Positive-Rate in Konkurrenz zueinander. Ebenfalls entscheidend für die Vorhersagequalität ist die Wahl der Entscheidungsschwelle, d. h. den vom Modell ermittelten Wahrscheinlichkeitswert für eine Klasse, bei dem man eine Klasse als zutreffend ansieht.

DevOps Docker Camp

Sie lernen die Konzepte von Docker und bauen Schritt für Schritt eine eigene Infrastruktur für und mit Docker auf.

Ein bewährtes Verfahren zur Bewertung der Vorhersagequalität, bei der dieser Trade-off visualisiert wird, ist die Bestimmung der Receiver Operating Characteristic Curve (ROC-Kurve). Eine ROC-Kurve trägt für ein bestimmtes Modell und eine bestimmte Entscheidungsschwelle die True-Positive-Rate gegenüber der False-Positive-Rate auf. Die ROC-Kurve lässt sich mit scikit-learn komfortabel bestimmen und plotten. Wir führen dies hier beispielhaft für den Random Forest durch. Zunächst werden alle Variablen initialisiert:

classes = [1,2,3,99]
rf_fpr = dict()
rf_tpr = dict()
rf_roc_auc = dict()
threshold = dict()

Dann wird über die einzelnen Klassen iteriert.

for i, c in enumerate(classes):

Für jede der Klassen können nun mithilfe der Funktion roc_curve über die vorhergesagten Wahrscheinlichkeiten für die ausgewählte Klasse c die Werte für die True-Positve-Rate (rf_tpr) und False-Positive-Rate (rf_tpr) bestimmt werden. Im Threshold wird die Entscheidungsschwelle abgelegt, bei der das Wertepaar (tpr/fpr) gültig ist:

rf_fpr[i], rf_tpr[i], threshold[i] = roc_curve(y_test, rf_y_ref[:, i], pos_label=c)

Die Fläche unter der ROC-Kurve (Abb. 4) hat für die Bewertung des Klassifikators eine wichtige Bedeutung. Weil ein idealer Klassifikator eine Fläche von 1 hat, drückt sie in nur einer Zahl die Güte des gelernten Klassifikators aus:

rf_roc_auc[i] = auc(rf_fpr[i], rf_tpr[i])

Die ROC-Kurven für die einzelnen Klassifikatoren der beiden Modelle sehen dann wie in Abbildung 4 zu sehen aus.

Abb. 4: ROC-Kurven der Modelle

Abb. 4: ROC-Kurven der Modelle

Wählt man auf einer ROC-Kurve eine niedrige False-Positive-Rate, sinkt auch die True-Positive-Rate. Kann man mit hohen False-Positive-Raten leben, erzielt man auch hohe True-Positive-Raten. Die diagonale Gerade stellt dabei ein umgangssprachlich unnützes Modell dar, das bei einer fünfzigprozentigen Wahrscheinlichkeit für die richtige Vorhersage von zutreffenden Klassen gleichzeitig auch eine fünfzigprozentige Wahrscheinlichkeit mit sich bringt, dass das Modell eine Klasse vorhersagt, obwohl sie nicht zutreffend ist. Ein gutes Modell erkennt man also an einer ROC-Kurve, die möglichst weit weg von dieser Diagonalen verläuft.

Entsprechend ist im obigen Diagramm schnell ersichtlich, dass die Klassifikationsgüte des Random Forest die der logistischen Regression weit übersteigt. Allerdings ist auch die Klassifikationsgüte der logistischen Regressionsmodelle, die als Fläche unter der Kurve gemessen wird, mit ca. 90 Prozent im Mittel ziemlich gut. Ob es gut genug ist, lässt sich allerdings nur am konkreten Fall beurteilen.

Für eine solche Bewertung kann die Wahrheitsmatrix der Klassifikatoren herangezogen werden. Die Wahrheitsmatrix sagt für jeden Raum (die Zeilen und Spalten in Tabelle 1 und Tabelle 2) aus, wie viele der Klassifikationen als Ergebnis richtig vorhergesagt wurden (Diagonale) und wie viele der Klassifikationen als Ergebnis einer falschen Klasse zugeordnet wurden (alle anderen Felder). Für die logistische Regression sieht die Wahrheitsmatrix wie in Tabelle 1 zu sehen aus.

Vorhergesagt
Wohnzimmer Küche Sonstige n/a
Tatsächlich Wohnzimmer 8682 61 61 381
Küche 58 8943 265 4
Sonstige 96 404 5088 1
n/a 476 6 4 3952

Tabelle 1: Wahrheitsmatrix für die logische Regression

Vorhergesagt
Wohnzimmer Küche Sonstige n/a
Tatsächlich Wohnzimmer 7803 596 247 539
Küche 83 8310 876 1
Sonstige 584 1130 3872 3
n/a 3815 0 211 412

Tabelle 2: Wahrheitsmatrix für Random Forest

Schaut man z. B. in die erste Zeile für die logistische Regression, sieht man, dass 8 682 Eingabewerte richtig als Klasse 1 (Wohnzimmer) klassifiziert wurden. Jeweils 61 wurden fälschlicherweise als Klasse 2 (Küche) bzw. 3 (sonstige Räume) erkannt, und 381 als Klasse n/a (steht in unserem Experiment für fremde Beacons).

Neben den statistischen Auswertungen kann ein erstelltes Modell natürlich auch im praktischen Versuch oder durch generierte Testdaten überprüft werden. Generell gilt aber: Das Modell kann nur das gut vorhersagen, was es auch kennt. In unserem Fall sind wenig Messwerte enthalten, die direkt an den Scannern aufgenommen wurden. Welche Auswirkungen das auf das Modell hat, kann vom findigen Leser gerne im Selbstversuch probiert werden.

Entwickler Magazin

Entwickler Magazin abonnierenDieser Artikel ist im Entwickler Magazin erschienen.

Natürlich können Sie das Entwickler Magazin über den entwickler.kiosk auch digital im Browser oder auf Ihren Android- und iOS-Devices lesen. In unserem Shop ist das Entwickler Magazin ferner im Abonnement oder als Einzelheft erhältlich.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu:
X
- Gib Deinen Standort ein -
- or -