feat: enhance training and experiment tracking functionality

- Updated `Activities` class to improve garbage collection handling.
- Enhanced error messaging in `ExperimentTracking` for better clarity on update failures.
- Refactored `Training` class to streamline exception handling and improve type hints.
- Introduced new methods in `TrainModelParams` for better handling of experiment run IDs and model metadata.
- Added functionality to extract model equations in `DataManagerRepository` for linear regression models.
This commit is contained in:
vitor-aignosi
2026-04-06 15:05:57 -03:00
parent 1352d1ac8f
commit 6b1df7c3a7
22 changed files with 1751 additions and 2085 deletions

View File

@@ -84,6 +84,8 @@ class TrainModelParams:
Args:
data: Dictionary containing training parameters with keys matching the
attribute names (e.g. variable_columns, data_model_kwargs, model_kwargs, opt_params).
model_metadata may be omitted or None until load_model_metadata fills it.
experiment_run_id may be an int or numeric string.
Returns:
TrainModelParams: Validated instance with all fields populated
@@ -111,7 +113,7 @@ class TrainModelParams:
train_size=cls._check_none(data.get('train_size'), int, 'train_size'),
shuffle=cls._check_none(data.get('shuffle'), bool, 'shuffle'),
random_state=cls._check_none(data.get('random_state', 42), int, 'random_state'),
experiment_run_id=cls._check_none(data.get('experiment_run_id'), int, 'experiment_run_id'),
experiment_run_id=cls._coerce_experiment_run_id(data.get('experiment_run_id')),
model_name=model_name,
experiment_name=model_name + '_experiment',
val_file_name=data.get('val_file_name'),
@@ -121,7 +123,7 @@ class TrainModelParams:
model_type=cls._check_none(data.get('model_type'), str, 'model_type'),
model_id=data.get('model_id'),
model_metadata=cls._check_none(data.get('model_metadata'), dict, 'model_metadata'),
model_metadata=cls._parse_optional_model_metadata(data.get('model_metadata')),
)
def to_dict(self) -> dict[str, Any]:
@@ -181,6 +183,60 @@ class TrainModelParams:
return value
@staticmethod
def _coerce_experiment_run_id(value: Any) -> int:
"""
Coerce experiment_run_id to int.
Workflow clients may send numeric strings; this keeps from_dict aligned with
workflow validation.
Args:
value: Raw experiment_run_id from the payload.
Returns:
int: Parsed experiment run id.
Raises:
ValueError: If the value is None.
TypeError: If the value cannot be coerced to a non-boolean integer.
"""
if value is None:
raise ValueError('experiment_run_id is required and cannot be None.')
if isinstance(value, bool):
raise TypeError('experiment_run_id must be an integer, got bool.')
if isinstance(value, int):
return value
if isinstance(value, str) and value.strip().isdigit():
return int(value.strip())
if isinstance(value, float) and value.is_integer():
return int(value)
raise TypeError(
f'experiment_run_id must be an integer or numeric string, but got {type(value).__name__}.'
)
@staticmethod
def _parse_optional_model_metadata(value: Any) -> dict | None:
"""
Parse model_metadata for from_dict before load_model_metadata fills the index.
Args:
value: model_metadata from the payload, or None if not sent yet.
Returns:
dict | None: Dict when provided; None when absent (filled later by load_model_metadata).
Raises:
TypeError: If value is neither None nor a dict.
"""
if value is None:
return None
if isinstance(value, dict):
return value
raise TypeError(
f'model_metadata must be a dict or None, but got {type(value).__name__}.'
)
def validate_business_rules(self) -> None:
"""
Validate business rules and constraints for training parameters.

View File

@@ -51,9 +51,3 @@ class TrainModelResult:
test_data_path: str | None = None
run_dir: str | None = None
def to_dict(self) -> dict[str, Any]:
"""
Convert TrainModelResult to a dictionary.
"""
return self.__dict__

View File

@@ -25,6 +25,7 @@ import pandas as pd
from sientia_do.observability.logger import Logger
from sientia_do.observability.sientia_monitoring import SientiaMonitoring
from sientia_do.temporal.constants import DATETIME_FORMAT_WITH_TZ
from sientia_model.wrappers.sientia_model import SientiaModel
from model_manager.sientia.metrics import mae, mse, r2
from model_manager.utils.models.train_model_params import TrainModelParams
@@ -206,10 +207,75 @@ class DataManagerRepository(SientiaMonitoring):
if pred.shape[1] == 1:
return pred.iloc[:, 0]
raise ValueError('y_pred/y_train_pred must be a Series or single-column DataFrame')
def _extract_model_equation(
self, regr: Any, params: TrainModelParams
) -> dict:
"""
Extract the linear regression equation coefficients and create equation metadata.
This method extracts the coefficients and intercept from the trained model
and creates a structured dictionary containing the equation information
for serialization as JSON artifact.
Args:
regr: Trained LinearRegressionModel object
params: Training parameters containing variable information
Returns:
dict: Equation metadata containing:
- target_variable: Name of the target variable
- coefficients: Dictionary mapping variable names to coefficients
- intercept: Model intercept value
- equation_string: Human-readable equation string
- latex_equation: LaTeX formatted equation
"""
coefficients = regr.regr.coef_
intercept = regr.regr.intercept_
# Get feature names - for polynomial models, use poly_feature_names
model_kwargs = params.model_kwargs or {}
degree = model_kwargs.get('degree', 1)
poly_feature_names = model_kwargs.get('poly_feature_names', None)
if degree > 1 and poly_feature_names:
feature_names = poly_feature_names
else:
feature_names = params.variable_columns
# Create coefficients dictionary
coefficients_dict = {}
for i, var in enumerate(feature_names):
if i < len(coefficients):
coefficients_dict[var] = float(coefficients[i])
# Create equation string
equation_parts = [f'{coef:.6f} * {var}' for var, coef in coefficients_dict.items()]
equation_string = f'{params.target_variable} = {intercept:.6f} + ' + ' + '.join(
equation_parts
)
# Create LaTeX equation
latex_parts = [f'{coef:.6f} \\cdot {var}' for var, coef in coefficients_dict.items()]
latex_equation = f'{params.target_variable} = {intercept:.6f} + ' + ' + '.join(latex_parts)
return {
'target_variable': params.target_variable,
'coefficients': coefficients_dict,
'intercept': float(intercept),
'equation_string': equation_string,
'latex_equation': latex_equation,
'model_type': params.model_name,
'degree': degree,
'interaction_only': model_kwargs.get('interaction_only', False),
'original_features': feature_names,
}
def compute_regression_metrics(
self,
tmr: TrainModelResult,
wrapper: SientiaModel,
) -> TrainModelResult:
"""
Compute regression metrics for training results.
@@ -252,6 +318,9 @@ class DataManagerRepository(SientiaMonitoring):
tmr.mae_val = mae(y_true_val, y_pred_val)
tmr.r2_val = r2(y_true_val, y_pred_val)
if params.model_type == 'linear_regression':
tmr.equation = self._extract_model_equation(wrapper.model, params)
return tmr
def _configure_datetime_index(