Neulich im Machine-Learning-Projekt … der Testunfall und seine Folgen

18 November 2014
| |
Lesezeit: 5 Minutes

Wer kennt es nicht, das Testen? Ich kenne es, benutze es, sichere mich dadurch ab. Was man so macht als guter Entwickler. Neulich im Projekt habe ich das spontan vergessen! Warum und wieso? Das möchte ich erzählen.

Ein Kollege und ich hatten vor kurzem die Möglichkeit ein internes Projekt für das Emerging Technologies Center mit dem Thema Machine Learning – genauer gesagt Supervised Learning – durchzuführen. Als Technologie haben wir Python und Scikit-Learn – eine Python-Library für Machine-Learning – verwendet. Ein Supervised Learning Problem haben wir auf der Kaggle-Plattform gefunden. Dort veröffentlichen Unternehmen, Universitäten und andere Data Science Probleme, die dann von einer Community gelöst werden können.

Der grobe Ablauf

Möchte man ein Problem auf Kaggle lösen, bekommt man – nach Registrierung auf der Website – zwei Datensätze zu diesem Problem:

  1. Trainingsdaten
  2. Testdaten

Die Trainingsdaten werden zum Aufbau eines Modells benutzt: ein Algorithmus lernt von bekannten Daten die Zielwerte um diese auf unbekannte Daten (die Testdaten) vorherzusagen.

Hinter dem was hier so knapp als Aufbau bezeichnet wird, können Unmengen an Arbeit stecken. Dazu gehören die Vor- und Aufbereitung der Daten, Auswahl geeigneter Features, Wahl des Algorithmus und noch einiges mehr.

Am Ende kommt eine Datei heraus, die auf den Testdaten vorhersagen enthält. Diese wird zurück an Kaggle geliefert. Kaggle prüft die Vorhersagen (Kaggle kennt nämlich die Zielwerte der Testdaten) und liefert eine Metrik, die angibt, wie gut die Vorhersage ist.

Um zu prüfen, wie gut ein Modell ist, ohne jedes Mal eine Eingabe bei Kaggle machen zu müssen, ist es eine gute Praxis die Trainigsdaten zu splitten: Ein Großteil der Daten – zum Beispiel 75 % – werden zum Aufbau des Modells verwendet, die restlichen Daten – in dem Fall 25 % – werden zur Evaluation des Modells verwendet.

Ein paar Interna

Scikit-Learn unterstützt den gesamten Trainingsablauf durch Software-Konstrukte. So gibt es für Aufbereitung der Daten entsprechende Klassen, für das Splitten der Trainingsdaten, das Evaluieren und vieles mehr.

Was in unserem Projekt rausgekommen ist, war ein Python-Script, das die Trainingsdaten zuerst in mehreren Stufen bearbeitet, bevor das Modell trainiert wird. Dieses Modell wird dann auf den abgezwackten Trainingsdaten evaluiert. Ist das Evaluationsergebnis ausreichend, kann das Training beendet werden.

Am Ende des Trainings steht ein Python-Objekt, das als Eingabe die Testdaten erwartet, die gleichen Bearbeitungsschritte auf diesen Daten wie auch auf den Trainingsdaten anwendet und schließlich eine Vorhersage der Zielvariablen zu jedem Datensatz in Testdaten macht.

Erwartung & Realität

Die Dauer des Trainings kann von wenigen Sekunden bis hin zu vielen Stunden variieren – je nach Daten und Algorithmen. Da ich unterschiedliche Modelle ausprobieren wollte, habe ich sie – inklusive der Verarbeitungsschritte – zur späteren Verwendung auf Festplatte serialisiert (Scikit-Learn als auch Python bringen Mechanismen dafür mit). In einem anderen Script wurden diese Modelle dann wieder als Python-Objekte eingelesen, um die Testdaten durch Kaggle zu valideren. Meine damalige Erwartungshaltung bei der Kaggle-Eingabe:

Kaggle liefert mir geringfügig schlechtere Ergebnisse als meine eigene Evaluation.

Doch sie wurde nicht erfüllt – die Ergebnisse waren sehr viel schlechter. Das war zu diesem Zeitpunkt für mich völlig in Ordnung, da in der Trainingsphase wirklich viel falsch gemacht werden kann. Daher bin ich wieder einen Schritt zurück, habe erneut ein gutes Modell trainiert, für die spätere Verwendung gesichert und Kaggle-Eingaben gemacht – wieder mit der Erwartung ein besseres Ergebnis zu bekommen. Doch auch das hat keine Verbesserung gebracht. Das Spiel habe ich dann einige Male mitgemacht, wobei die Aufwände zum Training immer größer wurden:

  • noch mehr Vorbereitung
  • noch kompliziertere Modelle
  • noch mehr Rechenzeit

Letzteres war übrigens kein Problem, da AWS nahezu unbegrenzte Rechenkapazität zur Verfügung stellt.

Nachdem ich mir sicher war, dass die Trainingsphase wirklich funktioniert und kein Fehler dabei sein kann, habe ich den Validierungsmechanimus von Kaggle bzw. die Testdaten angefangen zu verdächtigen:

Wie kann etwas auf meinen Trainingsdaten so gut sein, im Test aber so schlecht?

Das konnte nur an der Gegenstelle liegen! Dass die Kaggle- Community trotz der vermeintlichen Fehler in der Validierung zu besseren Ergebnissen kommt, war mir ein völliges Rätsel. Und so habe ich mich immer tiefer verrannt, um am Ende völlig entnervt mit einem unbefriedigendem Erebnis aufzugeben – was nicht wirklich dramatisch war, da es nur ein kleiner Teil der Ziele im ETC-Projekt war.

Es nagte … und der Teufel ist ein Eichhörnchen!

Etwa eine Woche nach Projektende – und mit genügend innerem Abstand – habe ich mir den Code nochmal in Ruhe angesehen. Dabei sind mir folgende Dinge aufgefallen:

  • Der Code war durchaus verständlich → was schon mal sehr hilfreich war
  • Das Trainingsskript bestand aus etwa 120 Zeilen Code → durchaus überschaubar
  • Der Code war einigermaßen modularisiert → das macht Sinn

Mit Abstand das Auffälligste am Code war aber die fehlende Absicherung:

Es gab nicht den kleinsten Hauch von Testabdeckung!

Das Schreiben der ersten Zeile Code im Projekt lag etwa 4 Wochen zurück. Ich erinnerte mich noch dunkel an meine ersten Gehversuche in Python und Scikit-Learn – beides Neuland für mich:

  • alles ist neu und aufregend
  • man schreibt ein bisschen Code, führt ihn aus
  • schreibt ein bisschen mehr und führt ihn wieder aus

Und exakt aus diesen ersten Gehversuchen ist ein 120-zeiliges Script gewachsen mit keinerlei Testabdeckung.

Das war aber nicht alles. Mein Fokus während des Projektes lag fast vollständig auf Daten und Algorithmen. Ich war so gefangen darin, dass ich gar keine andere Ursache als Fehler zulassen konnte. Noch etwas weiter:

Ich war davon überzeugt, dass der Test des Modells durch die Validierung während des Trainings und durch Kaggle ausreichend ist.

Das ist aber nur bedingt richtig! Das Modell an sich wird zwar geprüft, nicht aber der gesamte Code der zu solch einem Modell führt. Und das ist nicht wenig Code mit nicht wenig Fehlerpotential.

Nachdem ich mich endlich davon befreit hatte, den Fehler in den Daten und Algorithmen zu suchen, ist mir die Ursache geradezu entgegen gesprungen – auch ohne Testabdeckung: Der Serialisierungsmechanismus hat nicht so funktioniert, wie ich es erwartet hatte. Das eingelesene Objekt war stark fehlerhaft:

Stark fehlerhaft deshalb, weil es die Arbeit nicht verweigert hat und stattdessen falsche Ergebnisse geliefert hat.

Nachdem ich meinen Bug nach so langer Zeit gefunden und gefixt habe, hat auch mein Modell in der Kaggle-Validierung endlich brauchbare Ergebnisse geliefert.

Warum nur keine Tests?

Warum habe ich es geschafft, etwas das zum täglichen Entwickeln so tief in mir verwurzelt ist, einfach nicht zu machen? Ich bin mir sicher, dass es nicht den einen ultimativen Grund dafür gibt. Aber es gibt 3 kleinere Gründe, die mich in Kombination ausgehebelt haben:

Das Neuland!

Die meisten werden das wohl kennen: Man probiert bei neuen Dingen einfach gerne mal aus, d.h. einfachmal ein bisschen coden, den Debugger anfahren oder einfach das Script mal ausführen. Dabei sind Tests eher hinderlich und das finde ich auch völlig in Ordnung so!

Der schleichende Übergang!

Auch das wird der ein oder andere vermutlich aus dem Alltag kennen: Schnell wird aus einem Provisorium – in meinem Fall das schrittweise Herantasten an Python und Scikit-Learn – Code, der dann auch tatsächlich weiter verwendet wird.

Der Tunnelblick!

Aufgrund der Aufgabenstellung habe ich einen wirklich starken Tunnelblick entwickelt. Und zwar so stark, dass ich einfach nicht in der Lage war einen anderen Fehler als den der Daten bzw. des Modells zuzulassen. Ich habe lediglich das Modell – also das Resultat – durch Testdaten abgesichert. Nicht aber den Code, der mich dahin geführt hat.

Und was habe ich dabei gelernt?

Wenn ich etwas Neues machen möchte, will ich mich nicht mit anfangs unwichtigen Dingen aufhalten. Das etwas anfangs Unwichtiges später sehr wichtig sein kann, muss nicht ausgeführt werden. Wenn ich das nächste Neuland betrete werde ich genau so vorgehen wie bisher. Baut auf dem Code dann aber etwas auf, was über eine Spielwiese hinaus genaut, muss er auf jeden Fall überprüft werden — sei es durch eigene Überprüfung oder Reviews durch einen Kollegen.

Ein schleichender Übergang ist schon ziemlich gemein: man kennt das aus dem Projektalltag, wo eben schnell mal etwas gehackt wird und dann bis ans Ende aller Zeit bestehen bleibt. Auswege kenne ich keine. In echten Projekten versuche ich es dadurch abzuschwächen, dass auch meine Provisorien ein Mindestmaß an Qualitätskriterien aufweisen. Vielleicht muss man sich manchmal daran erinnern — Plakate mit großen Dos und Don’ts sollen da auch helfen!

Und zuletzt mein Tunnelblick: Das wird mir sicher wieder passieren. Wichtig für die Zukunft ist mir, immer mal wieder etwas Abstand zu bekommen, den Kopf frei zu machen – zum Beispiel bei einer Partie Tischkicker mit den Kollegen – oder sich einfach mal die Zeit nehmen und mit Gleichgesinnten sprechen.

Und Sie? Was machen Sie, wenn es keinen Ausweg aus dem Tunnel gibt oder sich ein Provisorium hartnäckig hält?

Kommentare (0)

×

Updates

Schreiben Sie sich jetzt ein für unsere zwei-wöchentlichen Updates per E-Mail.

This field is required
This field is required
This field is required

Mich interessiert

Select at least one category
You were signed up successfully.