ML-003: Predictor mit Sensor-Validierung und Batch-Interface
This commit is contained in:
33
app/ml/predictor.py
Normal file
33
app/ml/predictor.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from functools import lru_cache
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
|
from app.ml.feature_store import FeatureVector
|
||||||
|
from app.ml.training import TrainingPipeline, TrainedArtifact
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Predictor:
|
||||||
|
def __init__(self, pipeline: TrainingPipeline) -> None:
|
||||||
|
self._pipeline = pipeline
|
||||||
|
|
||||||
|
def predict(self, artifact_id: str, entity: FeatureVector) -> str:
|
||||||
|
artifact = self._pipeline.export(artifact_id)
|
||||||
|
if entity.sensor_id not in artifact.supported_sensors:
|
||||||
|
raise ValueError(
|
||||||
|
f"Sensor '{entity.sensor_id}' wird vom Modell '{artifact_id}' nicht unterstützt."
|
||||||
|
)
|
||||||
|
return f"{artifact_id}:{entity.sensor_id}:{entity.values}"
|
||||||
|
|
||||||
|
def predict_batch(self, artifact_id: str, entities: Sequence[FeatureVector]) -> list[str]:
|
||||||
|
return [self.predict(artifact_id, entity) for entity in entities]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def default_artifact(pipeline: TrainingPipeline) -> TrainedArtifact:
|
||||||
|
artifacts = list(pipeline._artifacts)
|
||||||
|
if not artifacts:
|
||||||
|
raise ValueError("Kein trainiertes Modell gefunden.")
|
||||||
|
return pipeline.export(artifacts[-1])
|
||||||
48
tests/ml/test_predictor.py
Normal file
48
tests/ml/test_predictor.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.ml.feature_store import FeatureStore, FeatureVector
|
||||||
|
from app.ml.predictor import Predictor
|
||||||
|
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 predictor() -> Predictor:
|
||||||
|
store = FeatureStore()
|
||||||
|
store.add_batch([_vector("sensor.kitchen", 19.0), _vector("sensor.bedroom", 18.5)])
|
||||||
|
pipeline = TrainingPipeline(store)
|
||||||
|
pipeline.run("artifact_v1")
|
||||||
|
return Predictor(pipeline)
|
||||||
|
|
||||||
|
|
||||||
|
def test_predict_returns_expected_format() -> None:
|
||||||
|
p = predictor()
|
||||||
|
result = p.predict("artifact_v1", _vector("sensor.kitchen", 21.0))
|
||||||
|
assert result == "artifact_v1:sensor.kitchen:{'temperature': 21.0}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_predict_rejects_unknown_sensor() -> None:
|
||||||
|
p = predictor()
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
p.predict("artifact_v1", _vector("sensor.unknown", 10.0))
|
||||||
|
|
||||||
|
|
||||||
|
def test_predict_batch_matches_single_calls() -> None:
|
||||||
|
p = predictor()
|
||||||
|
entities = [_vector("sensor.kitchen", 21.0), _vector("sensor.bedroom", 19.0)]
|
||||||
|
assert p.predict_batch("artifact_v1", entities) == [
|
||||||
|
p.predict("artifact_v1", item) for item in entities
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_artifact_returns_last_registered() -> None:
|
||||||
|
store = FeatureStore()
|
||||||
|
store.add_batch([_vector("sensor.kitchen", 19.0), _vector("sensor.bedroom", 18.5)])
|
||||||
|
pipeline = TrainingPipeline(store)
|
||||||
|
pipeline.run("first")
|
||||||
|
pipeline.run("second")
|
||||||
|
assert Predictor.default_artifact(pipeline).artifact_id == "second"
|
||||||
Reference in New Issue
Block a user