Files
2026-07-13 13:22:34 +08:00

129 lines
4.4 KiB
Python

import logging
from dataclasses import dataclass
from typing import Any, Callable
import numpy as np
from mlflow.metrics.base import MetricValue
from mlflow.models.evaluation.base import EvaluationMetric
_logger = logging.getLogger(__name__)
@dataclass
class MetricDefinition:
"""
A dataclass representing a metric definition used in model evaluation.
Attributes:
function: The metric function to be called for evaluation.
name: The name of the metric.
index: The index of the metric in the ``extra_metrics`` argument of ``mlflow.evaluate``.
version: (Optional) The metric version. For example v1.
genai_metric_args: (Optional) A dictionary containing arguments specified by users when
calling make_genai_metric or make_genai_metric_from_prompt.
Those args are persisted so that we can deserialize the same metric object later.
"""
function: Callable[..., Any]
name: str
index: int
version: str | None = None
genai_metric_args: dict[str, Any] | None = None
@classmethod
def from_index_and_metric(cls, index: int, metric: EvaluationMetric):
return cls(
function=metric.eval_fn,
index=index,
name=metric.name,
version=metric.version,
genai_metric_args=metric.genai_metric_args,
)
def evaluate(self, eval_fn_args) -> MetricValue | None:
"""
This function calls the metric function and performs validations on the returned
result to ensure that they are in the expected format. It will warn and will not log metrics
that are in the wrong format.
Args:
eval_fn_args: A dictionary of args needed to compute the eval metrics.
Returns:
MetricValue
"""
if self.index < 0:
exception_header = f"Did not log builtin metric '{self.name}' because it"
else:
exception_header = (
f"Did not log metric '{self.name}' at index "
f"{self.index} in the `extra_metrics` parameter because it"
)
metric: MetricValue = self.function(*eval_fn_args)
def _is_numeric(value):
return isinstance(value, (int, float, np.number))
def _is_string(value):
return isinstance(value, str)
if metric is None:
_logger.warning(f"{exception_header} returned None.")
return
if _is_numeric(metric):
return MetricValue(aggregate_results={self.name: metric})
if not isinstance(metric, MetricValue):
_logger.warning(f"{exception_header} did not return a MetricValue.")
return
scores = metric.scores
justifications = metric.justifications
aggregates = metric.aggregate_results
if scores is not None:
if not isinstance(scores, list):
_logger.warning(
f"{exception_header} must return MetricValue with scores as a list."
)
return
if any(not (_is_numeric(s) or _is_string(s) or s is None) for s in scores):
_logger.warning(
f"{exception_header} must return MetricValue with numeric or string scores."
)
return
if justifications is not None:
if not isinstance(justifications, list):
_logger.warning(
f"{exception_header} must return MetricValue with justifications as a list."
)
return
if any(not (_is_string(just) or just is None) for just in justifications):
_logger.warning(
f"{exception_header} must return MetricValue with string justifications."
)
return
if aggregates is not None:
if not isinstance(aggregates, dict):
_logger.warning(
f"{exception_header} must return MetricValue with aggregate_results as a dict."
)
return
if any(
not (isinstance(k, str) and (_is_numeric(v) or v is None))
for k, v in aggregates.items()
):
_logger.warning(
f"{exception_header} must return MetricValue with aggregate_results with "
"str keys and numeric values."
)
return
return metric