In diesem Blogbeitrag stelle ich Ihnen pydargs als hilfreiches Tool für die Konfigurationsverwaltung von Python-Projekten vor.
Die Entwicklung der Konfiguration in einem Projekt für maschinelles Lernen
In meiner Laufbahn als Datenwissenschaftler habe ich viele Projekte gesehen, die als einfache Proof-of-Concept-Skripte oder Notebooks begannen, dann zu Minimal-Produkten konsolidiert wurden und schließlich zu großen Codebasen heranwuchsen, als sie verbessert und erweitert wurden. Wenn man nicht aufpasst, landen die Konfigurationswerte für die Modelle, Datenquellen usw. überall in der Codebasis verstreut.
Schließlich wird ein Teil dieser Konfiguration an einem einzigen Ort konsolidiert, zum Beispiel in einer constants.py Datei mit globalen Variablen oder in einer Datenklasse. Oft bleiben große Teile zurück, da sie sich ohnehin nie ändern, und andere Teile werden dem Argument-Parser hinzugefügt, da sie sich so oft ändern.
Das Endergebnis ist eine Konfiguration, die teilweise konsolidiert, teilweise konfigurierbar und teilweise im Code versteckt ist. Das macht es schwierig, einen guten Überblick über die gesamte Konfiguration zu erhalten, und erschwert oft die Durchführung von schnellen Experimenten, bei denen ein kleiner Teil der Konfiguration geändert wird.
Die Datenklasse als Konfiguration
Der erste Schritt zur Vermeidung von Spaghetti-Konfiguration besteht darin, die gesamte Konfiguration von Anfang an an einem zentralen Ort zu speichern, vorzugsweise in Form einer Datenklasse oder etwas Ähnlichem. Für ein kleines Projekt zum maschinellen Lernen, das einen Gradient-Boosting-Klassifikator verwendet, könnte ein solches Konfigurationsobjekt etwa so aussehen:
from dataclasses import dataclass
from pathlib import Path
from typing import Literal
@dataclass
class Configuration:
mode: Literal["train", "predict"]
input_path: Path = Path("input_data/")
output_path: Path = Path("output_data/")
model_path: Path = Path("model/model.joblib")
cv: bool = True
cv_folds: int = 10
hp_n_estimators: int = 100
hp_learning_rate: float = 0.5
hp_max_depth: int = 8
Diese enthält gewissermaßen alle Parameter, die Sie ändern möchten: vom Modus (ob trainiert oder vorhergesagt werden soll) über Pfade zu Daten und Modellobjekten bis hin zu Einstellungen für die Kreuzvalidierung und Hyperparametern. In einem echten Projekt wird es wahrscheinlich mehr Parameter geben, bis zu Hunderten bei großen Projekten.
Eine Datenklasse bietet Ihnen viele Vorteile gegenüber der Verwendung eines Wörterbuchs als Konfigurationsobjekt. Der wichtigste ist die Unterstützung für statische Code-Analyse-Tools wie ruff und mypy, die Ihnen helfen, Fehler zu finden, bevor Sie sie einführen.
Verwendung einer Konfigurations-Datenklasse
Nachfolgend finden Sie ein einfaches Beispiel für die Verwendung einer solchen Konfigurationsdatenklasse. Das Feld mode bestimmt, welche Funktion aufgerufen wird, und die Funktion predict verwendet wiederum andere Felder, um die Daten und das Modell zu laden und die Vorhersagen an den gewünschten Ort zu schreiben.
from sys import argv
def main():
mode = argv[1]
config = Configuration(mode=mode)
run(config)
def run(config: Configuration) -> None:
if config.mode == "train":
train(config)
else:
predict(config)
def predict(config: Configuration) -> None:
input_data = load_prediction_data(config.input_path)
model = load_model(config.model_path)
predictions = model.predict(input_data)
store_predictions(predictions, config.output_path)
def train(config: Configuration) -> None:
...
Die Funktion main kann als Skript in den Paket-Metadaten registriert werden (siehe meinen Beitrag über setuptools), um einen einfachen Zugriff von der Kommandozeile aus zu ermöglichen. Das obige Beispiel erlaubt (erfordert sogar) die Angabe des Modus als Kommandozeilenargument, aber alle anderen Parameter können nur durch Änderung des Codes geändert werden. Ein weiteres Problem ist, dass jeder Wert für mode akzeptiert wird, was zu unerwartetem Verhalten führen kann.
Eine weitaus bessere Lösung ist die Verwendung von ArgumentParser:
from argparse import ArgumentParser
def main():
parser = ArgumentParser()
parser.add_argument("mode", type=str, choices=["train", "predict"])
parser.add_argument("--input-path", type=Path, default=Path("input_data/"))
parser.add_argument("--output_path", type=Path,default=Path("output_data/"))
parser.add_argument("--model_path", type=Path, default=Path("model/model.joblib"))
namespace = parser.parse_args()
config = Configuration(
mode=namespace["mode"],
input_path=namespace["input_path"],
output_path=namespace["output_path"],
model_path=namespace["model_path"]
)
run(config)
Der Code in diesem Beispiel prüft, ob die angegebene mode gültig ist, ermöglicht die Änderung der Ein- und Ausgabepfade durch Befehlszeilenoptionen und liefert hilfreiche Fehlermeldungen, wenn die Eingabe nicht korrekt ist. Beachten Sie jedoch, dass nicht alle Parameter als Argumente hinzugefügt werden und dass die Standardwerte hier dupliziert werden. Das bedeutet, dass ein neuer Parameter in der Konfiguration an drei Stellen hinzugefügt werden müsste (in der Datenklasse, im Aufruf von
Pydargs
An dieser Stelle kommt pydargs zur Hilfe. Pydargs konfiguriert die
from pydargs import parse
def main():
config = parse(Configuration)
run(config)
Als Bonus unterstützt pydargs:
- eine Vielzahl von Eingabetypen, wie z.B. Literale, Listen, Daten und mehr,
- verschachtelte Datenklassen, die es Ihnen ermöglichen, Teile Ihrer Konfiguration - z.B. Hyperparameter - in eine Unterkonfigurations-Datenklasse zu trennen, und
- pydantische Datenklassen für die zusätzliche Validierung der Eingabe.
Sehen Sie sich das hier an und räumen Sie mit dem Konfigurationswirrwarr in Ihrem Projekt auf!
Verfasst von
Rogier van der Geer
Unsere Ideen
Weitere Blogs
Contact




