© 2017 Fleur Duivis
Die meisten Anwendungen für maschinelles Lernen haben die gleiche High-Level-Architektur mit Komponenten für:
- Einlesen von Daten aus beliebigen Quellen
- Bereinigen dieser Daten und Berechnen neuer Funktionen
- Anwenden eines oder mehrerer Modelle auf die verfeinerten Daten
- Bereitstellung von Aggregaten und Modellvorhersageergebnissen für Endbenutzer
Meine Erfahrung aus Beratungsprojekten hat mich gelehrt, dass, wenn Anwendungen wie diese wachsen, neue Modelle hinzugefügt werden, die neue oder andere Funktionen erfordern, die wiederum neue oder andere Methoden der Bereinigung und Vorverarbeitung erfordern. Zeitdruck und begrenzte Fähigkeiten im Software-Engineering führen oft dazu, dass die Bereinigungs- und Merkmalsberechnungskomponente zu einem immer größer werdenden Monolithen aus unstrukturiertem Code wird, was zu verschiedenen Symptomen führt:
- Das Team verliert den Überblick darüber, welches Merkmal von welchem Modell verwendet wird, was häufig zu Redundanz und Unordnung führt (z. B. mehrere Merkmale/Spalten mit denselben oder fast denselben Daten),
- Obskure Merkmale, die nur von einem einzigen Modell verwendet werden, werden für alle eingehenden Daten vorberechnet und gespeichert, anstatt für die Teilmenge der Daten, die von dem eigentlichen Modell verwendet wird,
- Verschiedene Modelle erfordern unterschiedliche (und manchmal widersprüchliche) Strategien, z. B. für die Skalierung oder die Nullbehandlung von Eingabedaten, was zu einer unnötig komplexen Entscheidungslogik führt,
- Wenn Sie Modelle von der Analyse- in die Produktionsumgebung verschieben, ist es schwierig sicherzustellen, dass alle Datenvorverarbeitungen, die in der Analyseumgebung vorgenommen wurden, auf dem Produktionssystem genau so durchgeführt werden;
Im Moment erstelle ich ein MVP zur Validierung einer Startup-Idee, die auf der Analyse von Sportdaten basiert. Lassen Sie uns also die oben genannten Probleme anhand von Beispielen aus diesem Bereich genauer untersuchen.
Globale Funktionen vs. modellspezifische Funktionen
Meine Zielgruppe sind (Amateur-)Sportler, die GPS-basierte Geräte verwenden, um ihre Lauf- oder Fahrradaktivitäten zu verfolgen. Ihre Geräte erzeugen Datenströme mit sekundengenauen Messungen von Signalen wie Standort, Herzfrequenz, Zeit, Gesamtdistanz usw. Unsere Komponente zur Berechnung von Merkmalen verarbeitet diese Datenströme und fasst sie zu Merkmalen auf Aktivitätsebene zusammen, wie z.B. die Gesamtstrecke oder die durchschnittliche Herzfrequenz.
Es ist nicht schwer, sich vorzustellen, dass die Entfernung einer Aktivität ein wichtiges globales Merkmal ist: Sie kann von vielen Modellen verwendet werden, um Vorhersagen zu treffen, und ihre Verteilung verrät uns interessante Dinge über die Nutzerpopulation. Daher ist es sinnvoll, dieses Merkmal global zu berechnen und für eine spätere Wiederverwendung zu speichern.
Stellen Sie sich im Gegenteil ein Aktivitätsmerkmal wie die Gruppengröße vor, d.h. wie viele andere Sportler an der gleichen Aktivität teilgenommen haben. Beim Radfahren kann das Fahren in einer Gruppe aufgrund des deutlich geringeren Luftwiderstands einen erheblichen Vorteil bringen. Dieser Effekt ist jedoch nicht linear zur Gruppengröße: Der Unterschied zwischen dem Fahren allein oder mit 3 anderen ist viel größer als der Unterschied zwischen dem Fahren in einer Gruppe von 40 oder 80 Personen. Aus diesem Grund ist es sinnvoll, eine Logarithmus-Transformation auf die Gruppengröße anzuwenden, wenn diese in bestimmten Modellen für das Radfahren verwendet wird.
Bei Laufaktivitäten gibt es jedoch aufgrund der geringeren Geschwindigkeiten keinen nennenswerten Vorteil, in einer Gruppe zu sein, und somit auch keine Notwendigkeit für die Gruppengröße als Merkmal in Modellen. In diesem Fall macht es keinen Sinn, den Logarithmus der Gruppengröße als globales Merkmal zu berechnen und zu speichern.
Die größte Herausforderung besteht also darin, die gesamte modellspezifische Logik zu kapseln und das Durcheinander in der globalen Komponente zur Merkmalsberechnung zu reduzieren. Es stellt sich heraus, dass das API-Design von Scikit-learn bei dieser Aufgabe sehr hilfreich sein kann.
Scikit-learn Schätzer einrichten
Während ich Scikit-learn seit einiger Zeit verwende, ist mir nie in den Sinn gekommen, dass die Implementierung von z.B.. BaseEstimator und ClassifierMixin könnte einen anderen Grund haben als die Implementierung eines brandneuen ML-Algorithmus. Bis ich während einer Refactoring-Sitzung plötzlich die Verbindung mit dem Decorator-Designmuster herstellte. Auf der Grundlage dieses Musters können wir auch Scikit-learn-Schätzer "dekorieren". Die Basiskomponente wäre einer der vorhandenen Sklearn-Schätzer, z.B. LogisticRegression. Der Dekorator erbt dann von dieser Basiskomponente (oder implementiert die Mixins) und hat einen Verweis auf diesen Schätzer als Instanzvariable. Ein (hypothetisches) Beispiel wäre ein Klassifikator, der erkennt, welche Fahrradaktivitäten Mountainbike-Fahrten sind:
importieren numpy als np von sklearn.base importieren KlassifikatorMixin, BaseEstimator, TransformerMixin von sklearn.linear_model importieren LogistischeRegression von sklearn.pipeline importieren make_pipeline von sklearn.preprocessing importieren FunktionsTransformator # Der Dekorateur Klasse MTBDetection(BaseEstimator, KlassifikatorMixin): # 1 def __init__(selbst, Schätzer=LogistischeRegression(), null_strategy='Median'): # 2 selbst.Schätzer = Schätzer selbst.null_strategy = null_strategy selbst.extra_transformations = { # 3 'gruppe_größe': np.log2 } def _handle_nulls(selbst, X): # Logik für die Behandlung von Nullen, mit self.null_strategy def Spalten_umwandeln(selbst, X): # 3 return X.zuweisen(**{Spalte: transform_function(X[Spalte]) für Spalte, transform_function in selbst.extra_transformations.iteritems()}) def fit(selbst, X, y): # Optional: Fügen Sie eine Logik ein, um unsere Pipeline basierend auf den Eigenschaften von X zu konfigurieren. selbst.fitted_estimator_ = make_pipeline( # 4 ColumnSelector([ durchschnittliche_Geschwindigkeit, 'Entfernung', 'gruppe_größe' # usw. ]), FunktionsTransformator(selbst._handle_nulls, validieren=False), FunktionsTransformator(selbst.Spalten_umwandeln, validieren=False), selbst.Schätzer ).fit(X, y) return selbst def vorhersagen(selbst, X): return selbst.fitted_estimator_.vorhersagen(X) # Ein zustandsabhängiger Transformator Klasse ColumnSelector(TransformerMixin): # 5 def __init__(selbst, Spalten): selbst.Spalten = Spalten def fit(selbst, X, y=Keine): return selbst def transformieren(selbst, X): return X.Ort[:, selbst.Spalten]
- Indem wir von
BaseEstimatorundClassifierMixinerben und die Methodenfit()undpredict()implementieren, implementiert unsere Dekoratorklasse die gleiche "Schnittstelle" (Python hat keine echten Schnittstellen) wie die anderensklearn-Schätzer. Das bedeutet, dass sie in einer höheren Ebene verwendet werden kannPipeline, oder in einerGridSearchCV. - Wie bei den anderen
sklearnSchätzern sollten alle Parameter, die für die Anpassung eines Modells verwendet werden, dem Konstruktor mit einem Standardwert übergeben werden. In diesem Beispiel haben wir einen Parameter, der eine hypothetische Strategie zur Behandlung von Nullen angibt. Durch die Übergabe des "inneren" Schätzers an unseren Konstruktor können wir diesen Dekorator für verschiedene Arten von Klassifikatoren verwenden. - Dies ist nur ein triviales Beispiel, um zu zeigen, wie wir den Dekorator verwenden können, um potenziell komplexe Konfigurationen für unsere Modellanpassung und Vorhersagelogik zu speichern. Beachten Sie, dass
Dataframe.assign()kann (neue) Spalten nur in einzelnen Schritten berechnen. Für Ketten von Spaltentransformationen, die voneinander abhängen, können wir die Verwendung vonDataframe.pipe() - Alle Vorverarbeitungs- und Modellanpassungsvorgänge sind in einer
Pipelineverkettet. Dadurch wird sichergestellt, dass beim Trainieren des Modells alle Vorverarbeitungsvorgänge (und deren möglicher Zustand) zusammen mit dem angepassten Modell gespeichert werden. Dies macht uns weniger anfällig dafür, bei der Anwendung des Modells zu einem anderen Zeitpunkt versehentlich andere Vorverarbeitungsschritte zu verwenden. Darüber hinaus können die in der Pipeline zu verwendenden Parameter in diesem Moment dynamisch festgelegt werden, anstatt den Aufrufer vonfit()dafür verantwortlich zu machen. Dies verbessert die lose Kopplung und macht den Code einfacher zu pflegen und zu testen. - In einer solchen Pipeline können wir alles verwenden, was die
TransformerMixinwenn es notwendig ist, den Zustand des Transformators zu erfassen (z.B. bei einem Skalierer, der sich den Mittelwert und die Standardabweichung der ursprünglichen Daten merken muss, mit denen er angepasst wurde). Im obigen Beispiel wird ein trivialer Transformator für die Auswahl von Spalten verwendet. Alternativ können wir auch einenFunctionTransformerfür zustandslose Transformationen.
Vorbehalte
Bei der Verwendung des obigen Ansatzes bin ich auf einige Probleme gestoßen (die nicht unbedingt mit dem Decorator-Muster zusammenhängen):
- Wenn die Vorverarbeitungspipeline komplexer wird als im obigen Beispiel, besteht die Möglichkeit, dass ein oder mehrere Transformatoren die Reihenfolge der Zeilen in den Eingabedaten ändern, z. B. durch Gruppierung nach oder Verknüpfungsoperationen. Während die Reihenfolge bei der Anpassung des Modells nicht wichtig ist, werden die von
zurückgegebenen vorausgesagten Bezeichnungen nicht in der gleichen Reihenfolge wie die Beobachtungen sein. Stellen Sie also immer sicher, dass die Reihenfolge der Beobachtungen in der Pipeline beibehalten wird. - Wenn wir in einem Jupyter-Notizbuch an der Entwicklung eines Modells arbeiten, würde der Code für dieses Modell wahrscheinlich in einer IDE bearbeitet und unter
%autoreloadin das Notizbuch übertragen werden. Nachdem wir den Modellcode aktualisiert und das Modell neu trainiert haben, würden wir es dann mit Pickle in das Produktionssystem übertragen. Es hat sich herausgestellt, dass Pickle Schwierigkeiten hat (und mit sinnlosen Fehlermeldungen abstürzt), wenn Objekte serialisiert werden, deren Klassendefinitionen durch (automatische) Neuladungen aktualisiert wurden.
Fazit
Die Anwendung des Decorator-Patterns für die Kapselung modellspezifischer Vorverarbeitung verbessert die Korrektheit, Wartbarkeit und Testbarkeit unseres Codes sowie den einfachen Transport von Modellen zwischen Analyse- und Produktionsumgebungen durch Serialisierungsmechanismen wie Pickle erheblich.
Verbessern Sie Ihre Python-Kenntnisse, lernen Sie von den Experten!
Bei GoDataDriven bieten wir eine Vielzahl von Python-Kursen an, die von den besten Experten auf diesem Gebiet unterrichtet werden. Kommen Sie zu uns und verbessern Sie Ihr Python-Spiel:
- Data Science with Python Foundation - Möchten Sie den Schritt von der Datenanalyse und -visualisierung zu echter Datenwissenschaft machen? Dies ist der richtige Kurs.
- Advanced Data Science with Python - Lernen Sie, Ihre Modelle wie ein Profi zu produzieren und Python für maschinelles Lernen zu verwenden.
Unsere Ideen
Weitere Blogs
Contact



