feat: add routed model training pipeline
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from sklearn.ensemble import RandomForestClassifier
|
||||
from sklearn.gaussian_process import GaussianProcessRegressor
|
||||
|
||||
from lmpm.training.dataset import (
|
||||
CATEGORICAL_FEATURE_FIELDS,
|
||||
NUMERIC_FEATURE_FIELDS,
|
||||
TrainingDataset,
|
||||
TrainingRecord,
|
||||
)
|
||||
from lmpm.training.models import (
|
||||
build_estimator,
|
||||
load_training_bundle,
|
||||
save_training_bundle,
|
||||
train_models,
|
||||
)
|
||||
from lmpm.training.routing import default_model_routes
|
||||
|
||||
|
||||
def make_record(number: int, material_name: str) -> TrainingRecord:
|
||||
features = {
|
||||
field: float(number + index + 1)
|
||||
for index, field in enumerate(NUMERIC_FEATURE_FIELDS)
|
||||
}
|
||||
features.update(
|
||||
{
|
||||
CATEGORICAL_FEATURE_FIELDS[0]: "bidirectional",
|
||||
CATEGORICAL_FEATURE_FIELDS[1]: "20x20",
|
||||
}
|
||||
)
|
||||
return TrainingRecord(
|
||||
metadata={
|
||||
"experiment_id": f"LAS-2026-{number:04d}",
|
||||
"material_name": material_name,
|
||||
},
|
||||
features=features,
|
||||
targets={
|
||||
"is_cut_through": number % 2 == 0,
|
||||
"carbonized_edge_width": number / 10,
|
||||
"etching_depth": number * 10.0,
|
||||
"is_fire_smolder": False,
|
||||
"pattern_clarity_score": float(number),
|
||||
"presentation_balance_score": float(10 - number),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def training_dataset() -> TrainingDataset:
|
||||
return TrainingDataset(
|
||||
records=(
|
||||
make_record(1, "Acrylic"),
|
||||
make_record(2, "Acrylic"),
|
||||
make_record(3, "Basswood"),
|
||||
make_record(4, "Basswood"),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_model_factory_builds_the_routed_estimator_types():
|
||||
routes = {route.target_name: route for route in default_model_routes()}
|
||||
|
||||
assert isinstance(
|
||||
build_estimator(routes["etching_depth"]), GaussianProcessRegressor
|
||||
)
|
||||
assert isinstance(build_estimator(routes["is_cut_through"]), RandomForestClassifier)
|
||||
|
||||
|
||||
def test_training_runs_leakage_safe_lomo_and_persists_bundle(tmp_path):
|
||||
bundle = train_models(training_dataset())
|
||||
|
||||
evaluation = bundle.evaluations["etching_depth"]
|
||||
assert [fold.held_out_material for fold in evaluation.folds] == [
|
||||
"Acrylic",
|
||||
"Basswood",
|
||||
]
|
||||
assert evaluation.mean_metrics["mae"] is not None
|
||||
assert bundle.models["etching_depth"].route.model_family == (
|
||||
"gaussian_process_regressor"
|
||||
)
|
||||
|
||||
features = bundle.models["etching_depth"].preprocessor.transform(training_dataset())
|
||||
predictions = bundle.models["etching_depth"].estimator.predict(features.values)
|
||||
assert isinstance(predictions, np.ndarray)
|
||||
assert predictions.shape == (4,)
|
||||
|
||||
artifact_path = save_training_bundle(bundle, tmp_path / "models.joblib")
|
||||
restored = load_training_bundle(artifact_path)
|
||||
assert restored.report() == bundle.report()
|
||||
|
||||
|
||||
def test_training_requires_two_distinct_materials():
|
||||
dataset = TrainingDataset(
|
||||
records=(make_record(1, "Acrylic"), make_record(2, "Acrylic"))
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="at least two materials"):
|
||||
train_models(dataset)
|
||||
Reference in New Issue
Block a user