Files
2026-07-13 12:36:30 +08:00

203 lines
8.1 KiB
Python

import abc
from collections import defaultdict
from functools import partial
from typing import Dict
from typing import List
from typing import Union
import torch
from easygraph._global import AUTHOR_EMAIL
def format_metric_configs(task: str, metric_configs: List[Union[str, Dict[str, dict]]]):
r"""Format metric_configs.
Args:
``task`` (``str``): The type of the task. The supported types include: ``classification``, ``retrieval`` and ``recommender``.
``metric_configs`` (``Dict[str, Dict[str, Union[str, int]]]``): The metric configs.
"""
task = task.lower()
if task == "classification":
import easygraph.ml_metrics.classification as module
available_metrics = module.available_classification_metrics()
else:
raise ValueError(
f"Task {task} is not supported yet. Please email '{AUTHOR_EMAIL}' to"
" add it."
)
metric_list = []
for metric in metric_configs:
if isinstance(metric, str):
marker, func_name = metric, metric
assert func_name in available_metrics, (
f"{func_name} is not supported yet. Please email '{AUTHOR_EMAIL}' to"
" add it."
)
func = getattr(module, func_name)
elif isinstance(metric, dict):
assert len(metric) == 1
func_name = list(metric.keys())[0]
assert func_name in available_metrics, (
f"{func_name} is not supported yet. Please email '{AUTHOR_EMAIL}' to"
" add it."
)
params = metric[func_name]
func = getattr(module, func_name)
func = partial(func, **params)
markder_list = []
for k, v in params.items():
_m = f"{k}@"
if isinstance(v, str):
_m += v
elif isinstance(v, int):
_m += str(v)
elif isinstance(v, float):
_m += f"{v:.4f}"
elif isinstance(v, list) or isinstance(v, tuple) or isinstance(v, set):
_m += "_".join([str(_v) for _v in v])
else:
_m += str(v)
markder_list.append(_m)
marker = f"{func_name} -> {' | '.join(markder_list)}"
else:
raise ValueError
metric_list.append({"marker": marker, "func": func, "func_name": func_name})
return metric_list
class BaseEvaluator:
r"""The base class for task-specified metric evaluators.
Args:
``task`` (``str``): The type of the task. The supported types include: ``classification``, ``retrieval`` and ``recommender``.
``metric_configs`` (``List[Union[str, Dict[str, dict]]]``): The metric configurations. The key is the metric name and the value is the metric parameters.
``validate_index`` (``int``): The specified metric index used for validation. Defaults to ``0``.
"""
def __init__(
self,
task: str,
metric_configs: List[Union[str, Dict[str, dict]]],
validate_index: int = 0,
):
self.validate_index = validate_index
metric_configs = format_metric_configs(task, metric_configs)
assert validate_index >= 0 and validate_index < len(
metric_configs
), "The specified validate metric index is out of range."
self.marker_list, self.func_list = [], []
for metric in metric_configs:
self.marker_list.append(metric["marker"])
self.func_list.append(metric["func"])
# init batch data containers
self.validate_res = []
self.test_res_dict = defaultdict(list)
self.last_validate_res, self.last_test_res = None, {}
@abc.abstractmethod
def __repr__(self) -> str:
r"""Print the Evaluator information."""
def validate_add_batch(
self, batch_y_true: torch.Tensor, batch_y_pred: torch.Tensor
):
import numpy as np
r"""Add batch data for validation.
Args:
``batch_y_true`` (``torch.Tensor``): The ground truth data. Size :math:`(N_{batch}, -)`.
``batch_y_pred`` (``torch.Tensor``): The predicted data. Size :math:`(N_{batch}, -)`.
"""
batch_res = self.func_list[self.validate_index](
batch_y_true, batch_y_pred, ret_batch=True
)
batch_res = np.array(batch_res)
if len(batch_res.shape) == 1:
batch_res = batch_res[:, np.newaxis]
self.validate_res.append(batch_res)
def validate_epoch_res(self):
r"""For all added batch data, return the result of the evaluation on the specified ``validate_index``-th metric.
"""
import numpy as np
if self.validate_res == [] and self.last_validate_res is not None:
return self.last_validate_res
assert self.validate_res != [], "No batch data added for validation."
self.last_validate_res = np.vstack(self.validate_res).mean(0).item()
# clear batch cache
self.validate_res = []
return self.last_validate_res
def test_add_batch(self, batch_y_true: torch.Tensor, batch_y_pred: torch.Tensor):
r"""Add batch data for testing.
Args:
``batch_y_true`` (``torch.Tensor``): The ground truth data. Size :math:`(N_{batch}, -)`.
``batch_y_pred`` (``torch.Tensor``): The predicted data. Size :math:`(N_{batch}, -)`.
"""
import numpy as np
for name, func in zip(self.marker_list, self.func_list):
batch_res = func(batch_y_true, batch_y_pred, ret_batch=True)
if not isinstance(batch_res, tuple):
batch_res = np.array(batch_res)
if len(batch_res.shape) == 1:
batch_res = batch_res[:, np.newaxis]
self.test_res_dict[name].append(batch_res)
else:
if self.test_res_dict[name] == []:
self.test_res_dict[name] = [list() for _ in range(len(batch_res))]
for idx, batch_sub_res in enumerate(batch_res):
batch_sub_res = np.array(batch_sub_res)
if len(batch_sub_res.shape) == 1:
batch_sub_res = batch_sub_res[:, np.newaxis]
self.test_res_dict[name][idx].append(batch_sub_res)
def test_epoch_res(self):
r"""For all added batch data, return results of the evaluation on all the ml_metrics in ``metric_configs``.
"""
import numpy as np
if self.test_res_dict == {} and self.last_test_res is not None:
return self.last_test_res
assert self.test_res_dict != {}, "No batch data added for testing."
for name, res_list in self.test_res_dict.items():
if not isinstance(res_list[0], list):
self.last_test_res[name] = (
np.vstack(res_list).mean(0).squeeze().tolist()
)
else:
self.last_test_res[name] = [
np.vstack(sub_res_list).mean(0).squeeze().tolist()
for sub_res_list in res_list
]
# clear batch cache
self.test_res_dict = defaultdict(list)
return self.last_test_res
def validate(self, y_true: torch.LongTensor, y_pred: torch.Tensor):
r"""Return the result of the evaluation on the specified ``validate_index``-th metric.
Args:
``y_true`` (``torch.LongTensor``): The ground truth labels. Size :math:`(N_{samples}, -)`.
``y_pred`` (``torch.Tensor``): The predicted labels. Size :math:`(N_{samples}, -)`.
"""
return self.func_list[self.validate_index](y_true, y_pred)
def test(self, y_true: torch.LongTensor, y_pred: torch.Tensor):
r"""Return results of the evaluation on all the ml_metrics in ``metric_configs``.
Args:
``y_true`` (``torch.LongTensor``): The ground truth labels. Size :math:`(N_{samples}, -)`.
``y_pred`` (``torch.Tensor``): The predicted labels. Size :math:`(N_{samples}, -)`.
"""
return {
name: func(y_true, y_pred)
for name, func in zip(self.marker_list, self.func_list)
}