Blog

Wie Sie Ihre Scikit-learn-Modelle wie einen Weihnachtsbaum schmücken

Aktualisiert Oktober 21, 2025
7 Minuten
Weihnachtsbaum

© 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]
  1. Indem wir von BaseEstimator und ClassifierMixin erben und die Methoden fit() und predict() implementieren, implementiert unsere Dekoratorklasse die gleiche "Schnittstelle" (Python hat keine echten Schnittstellen) wie die anderen sklearn -Schätzer. Das bedeutet, dass sie in einer höheren Ebene verwendet werden kann Pipeline, oder in einer GridSearchCV.
  2. Wie bei den anderen sklearn Schä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.
  3. 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 von Dataframe.pipe()
  4. Alle Vorverarbeitungs- und Modellanpassungsvorgänge sind in einer Pipeline verkettet. 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 von fit() dafür verantwortlich zu machen. Dies verbessert die lose Kopplung und macht den Code einfacher zu pflegen und zu testen.
  5. In einer solchen Pipeline können wir alles verwenden, was die TransformerMixin wenn 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 einen FunctionTransformer fü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:

Contact

Let’s discuss how we can support your journey.