Avaliação e Qualidade do Modelo Gates

Na entrega contínua para aprendizado de máquina (CD4ML), tratamos código, dados e modelos como cidadãos de primeira classe. Assim como o código é guardado por testes unitários, os modelos devem ser guardados pela estatística Portões de qualidade para evitar regressões antes da implantação.


1. mergulho profundo em métricas de avaliação

Avaliamos nosso modelo usando três métricas de regressão complementares:

Erro absoluto (MAE)

O MAE mede a magnitude média dos erros de previsão, sem considerar sua direção:

MAE=1n∑i=1n∣yi−y^i∣\text{MAE} = \frac{1}{n} \sum_{i=1}^n |y_i - \hat{y}_i|

  • Interpretação: Expresso na unidade do alvo (número de embarques de táxi). Um MAE de 11,5 significa que, em média, nossas previsões perdem a contagem real de embarque por hora em aproximadamente 11,5 viagens.
  • Robustness: Altamente robusto para outliers estatísticos. É útil para representar comportamentos de erro típicos.

Erro de Raiz Média Quadrada (RMSE)

RMSE é a raiz quadrada da média dos erros de previsão ao quadrado:

RMSE=1n∑i=1n(yi−y^i)2\text{RMSE} = \sqrt{\frac{1}{n} \sum_{i=1}^n (y_i - \hat{y}_i)^2}

  • Interpretação: Ao dividir os erros antes da média, o RMSE penaliza mais fortemente os erros maiores.
  • Significado de Negócios: Altamente crítico para a logística da frota. Uma enorme falha de previsão (por exemplo, prevendo 10 embarques quando 200 ocorrem) é operacionalmente desastrosa porque deixa centenas de passageiros encalhados. Os despachantes preferem um modelo com um MAE ligeiramente maior se mantiverem um RMSE significativamente menor (menos erros catastróficos).

Coeficiente de Determinação (R2R^2)

R2R^2 representa a proporção de variância na variável alvo que é previsível a partir das características independentes:

$$R^2 = 1 - \frac{\sum_{i=1}^n (y_i - \hat{y}i)^2}{\sum{i=1}^n (y_i - \bar{y})^2}$$

  • Interpretação: Scale-free metric ranging from −∞-\infty to 11. An R2R^2 of 0.650.65 indicates that our model explains 65% of the demand variance compared to a naive historical baseline model (yˉ\bar{y}).

2. Portões automatizados da qualidade

Para automatizar a implantação com segurança, nosso pipeline implementa um mecanismo de gating:

metrics.require_within(config.evaluation)
  1. Recuperar Métricas Candida candidatas: O pipeline puxa a versão do modelo candidato de MLflow e executa previsões no conjunto de teste.
  2. Avaliar os limiares: As métricas calculadas são comparadas com os limites máximos de erro definidos em configs/project.yaml:
    • max_mae: Máximo MAE permitido.
    • max_rmse: Máximo RMSE permitido.
  3. Bloco de Implantação: Se o MAE ou RMSE do modelo candidato exceder os limiares, o script de avaliação levanta uma exceção de validação e aborta. Isso interrompe as etapas de implantação a jusante, mantendo o modelo de produção anterior ativo.

3. Implementação do Pipeline

Abaixo está o código que executa a avaliação do modelo e o gating:

Sourcesrc/nyc_taxi_demand_forecasting/pipelines/evaluate.py
import logging
from importlib import import_module
from pathlib import Path
from typing import Any

import mlflow
import numpy as np
from mlflow.client import MlflowClient
from mlops_shared.evaluation import (
    RegressionMetricCalculator,
    RegressionMetrics,
)

from nyc_taxi_demand_forecasting.configuration import ProjectConfigLoader
from nyc_taxi_demand_forecasting.features.hourly_demand import (
    FEATURE_COLUMNS,
    FEATURE_REFS,
    load_entity_df,
)
from nyc_taxi_demand_forecasting.models.registry import latest_model_version
from nyc_taxi_demand_forecasting.models.training import DemandDatasetSplitter


def _tag_evaluation_outcome(
    client: MlflowClient,
    model_name: str,
    model_version: str,
    outcome: str,
    metrics: RegressionMetrics,
) -> None:
    for key, value in (
        ("evaluated", outcome),
        ("mae", str(round(metrics.mae, 4))),
        ("rmse", str(round(metrics.rmse, 4))),
    ):
        client.set_model_version_tag(
            name=model_name,
            version=model_version,
            key=key,
            value=value,
        )


def run(config_path: Path) -> None:
    config = ProjectConfigLoader().load(config_path)

    # 1. Load entities dataframe and fetch test historical features from Feast
    entity_df = load_entity_df(config.features.training_dataset_path)

    feature_store_type = import_module("feast").FeatureStore
    store = feature_store_type(repo_path=str(config.feast.repo_path))
    training_data = store.get_historical_features(
        entity_df=entity_df,
        features=list(FEATURE_REFS),
    ).to_df()

    # Split dataset (only evaluate on test split)
    _, test_frame = DemandDatasetSplitter().split(
        training_data, config.training.test_size
    )

    # 2. Connect to MLflow and get the latest registered model version
    mlflow.set_tracking_uri(config.mlflow.tracking_uri)
    client = MlflowClient()

    model_name = config.mlflow.registered_model_name
    latest_version = latest_model_version(client, model_name)
    model_uri = f"models:/{model_name}/{latest_version}"

    # 3. Load the model and make predictions
    model = mlflow.pyfunc.load_model(model_uri)
    # pyfunc emits an ndarray, so normalize the output shape instead of
    # asserting a Series — same contract the api/monitor sites carry.
    raw_predictions: Any = model.predict(  # pyright: ignore[reportUnknownMemberType]
        test_frame.loc[:, list(FEATURE_COLUMNS)]
    )
    predictions = np.asarray(raw_predictions).ravel()

    # 4. Compute metrics
    metrics = RegressionMetricCalculator().calculate(
        test_frame[config.training.target_column], predictions
    )

    logging.getLogger(__name__).info(
        "model_evaluation_completed",
        extra={
            "mae": metrics.mae,
            "rmse": metrics.rmse,
            "r2": metrics.r2,
            "version": latest_version,
        },
    )

    # 5. Gate before tagging — only a pass marks the version deployable via
    # the candidate alias; a failure records its truthful outcome so a
    # gate-failed version is distinguishable from a never-evaluated one.
    try:
        metrics.require_within(config.evaluation)
    except ValueError:
        _tag_evaluation_outcome(
            client, model_name, latest_version, "failed", metrics
        )
        raise
    _tag_evaluation_outcome(
        client, model_name, latest_version, "true", metrics
    )
    client.set_registered_model_alias(
        name=model_name,
        alias="candidate",
        version=latest_version,
    )