From 63d10a6c4f5cd1e24d07991747d81eef275a5559 Mon Sep 17 00:00:00 2001 From: Pino Date: Thu, 11 Jun 2026 17:08:16 +0200 Subject: [PATCH] ML-006: Training- und Evaluations-Workflow vorbereiten --- app/ml/training.py | 2 +- docs/ml_training.md | 39 ++++++++++++++++++++++++++++ tests/ml/test_training_evaluation.py | 33 +++++++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 docs/ml_training.md create mode 100644 tests/ml/test_training_evaluation.py diff --git a/app/ml/training.py b/app/ml/training.py index 33eb84a..a31f5bd 100644 --- a/app/ml/training.py +++ b/app/ml/training.py @@ -25,7 +25,7 @@ class TrainingPipeline: if not vectors: raise ValueError("FeatureStore enthält keine Trainingsdaten.") - sensors = tuple({vector.sensor_id for vector in vectors}) + sensors = tuple(sorted({vector.sensor_id for vector in vectors})) artifact = TrainedArtifact(artifact_id=artifact_id, supported_sensors=sensors) self._artifacts[artifact_id] = artifact logger.info("Training abgeschlossen für %s mit %d Sensoren", artifact_id, len(sensors)) diff --git a/docs/ml_training.md b/docs/ml_training.md new file mode 100644 index 0000000..ea6a94a --- /dev/null +++ b/docs/ml_training.md @@ -0,0 +1,39 @@ +# ML Training- und Evaluations-Workflow + +Dieser Workflow beschreibt, wie Modelle trainiert, evaluiert und an der Serving-Layer registriert werden. + +## 1. Daten sammeln + +Alle Trainingsvektoren werden über `FeatureStore.add(...)` oder `add_batch(...)` eingepflegt. Jeder Vektor enthält eine Sensor-ID sowie ein Dictionary mit Merkmalen. + +## 2. Modell trainieren + +```python +store = FeatureStore() +store.add(FeatureVector(sensor_id="sensor.kitchen", values={"temperature": 21.0})) +pipeline = TrainingPipeline(store) +artifact = pipeline.run("my_artifact") +pipeline.export("my_artifact") +``` + +`TrainingPipeline.run(...)` erzeugt ein `TrainedArtifact` mit den unterstützten Sensor-IDs. + +## 3. Modell evaluieren + +```python +evaluator = Evaluator(pipeline) +report = evaluator.evaluate(artifact.artifact_id, predictions) +``` + +Der Report enthält: +- `artifact_id` +- `sample_size` +- Metriken wie `coverage` und `unknown_rate` mit Default-Schwellenwerten + +## 4. Modell registrieren + +Das trainierte Artefakt kann anschließend über `ModelRegistry.register(artifact)` bereitgestellt werden. Die ML-Serving-API stellt es unter `/ml/predict` und `/ml/batch` zur Verfügung. + +## Hinweise +- Für reproduzierbare Sensor-Reihenfolgen wird in `TrainingPipeline.run(...)` eine sortierte Sensor-Liste verwendet. +- Fehlende Trainingsdaten lösen `ValueError` aus; nicht registrierte Artefakte lösen `KeyError` aus. \ No newline at end of file diff --git a/tests/ml/test_training_evaluation.py b/tests/ml/test_training_evaluation.py new file mode 100644 index 0000000..9442329 --- /dev/null +++ b/tests/ml/test_training_evaluation.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from app.ml.evaluation import Evaluator, EvalReport, Metric +from app.ml.feature_store import FeatureStore, FeatureVector +from app.ml.training import TrainingPipeline + + +def _vector(sensor_id: str, temperature: float, label: str | None = None) -> FeatureVector: + return FeatureVector(sensor_id=sensor_id, values={"temperature": temperature}, label=label) + + +def test_end_to_end_training_then_evaluation() -> None: + store = FeatureStore() + store.add_batch([_vector("sensor.kitchen", 19.0), _vector("sensor.bedroom", 18.5)]) + pipeline = TrainingPipeline(store) + artifact = pipeline.run("artifact_v1") + + evaluator = Evaluator(pipeline) + predictions = [ + "artifact_v1:sensor.kitchen:{'temperature': 21.0}", + "artifact_v1:sensor.bedroom:{'temperature': 18.5}", + ] + report = evaluator.evaluate(artifact.artifact_id, predictions) + assert isinstance(report, EvalReport) + assert report.sample_size == len(predictions) + assert any(metric.name == "coverage" for metric in report.metrics) + + +def test_metric_helpers_are_serializable() -> None: + metric = Metric(name="coverage", value=0.85, threshold=0.8) + assert metric.name == "coverage" + assert metric.value == 0.85 + assert metric.threshold == 0.8 \ No newline at end of file