Skip to content

Nested metrics are being duplicated when logging. #3513

@gabrielfruet

Description

@gabrielfruet

🚀 Feature

When we use nested metrics (i.e. ClassificationReport) we have a structure like this:

{
   'macro_avg': {
         'precision': ...,
         'f1-score': ...,
         'recall': ...,
    }
}

def _wrapper(
re: torch.Tensor, pr: torch.Tensor, f: torch.Tensor, a_re: torch.Tensor, a_pr: torch.Tensor, a_f: torch.Tensor
) -> Union[Collection[str], Dict]:
if pr.shape != re.shape:
raise ValueError(
"Internal error: Precision and Recall have mismatched shapes: "
f"{pr.shape} vs {re.shape}. Please, open an issue "
"with a reference on this error. Thank you!"
)
dict_obj = {}
for idx, p_label in enumerate(pr):
dict_obj[_get_label_for_class(idx)] = {
"precision": p_label.item(),
"recall": re[idx].item(),
f"f{beta}-score": f[idx].item(),
}
dict_obj["macro avg"] = {
"precision": a_pr.item(),
"recall": a_re.item(),
f"f{beta}-score": a_f.item(),
}
return dict_obj if output_dict else json.dumps(dict_obj)

Currently it goes through this code path when adding the metrics to the engine state metrics.

if isinstance(result, Mapping):
if name in result.keys():
raise ValueError(f"Argument name '{name}' is conflicting with mapping keys: {list(result.keys())}")
for key, value in result.items():
engine.state.metrics[key] = value
engine.state.metrics[name] = result

Which will fill the metrics dict in the next way:

{
   '$ENGINE_METRIC_NAME': {
         macro_avg': {
           'precision': ...,
           'f1-score': ...,
           'recall': ...
          },
    },
    macro_avg': {
         'precision': ...,
         'f1-score': ...,
         'recall': ...
     },
}

$ENGINE_METRIC_NAME is the one defined here:

    classification_report.attach(evaluator, "$ENGINE_METRIC_NAME")

The main problem in that is for when we want to log metrics using TensorBoard logger for example, we need to either specify what metrics we want or all.

my code below

def setup_tblogger(
...
    train_metric_names: list[str] | str = "all",
...
) -> TensorboardLogger:

    logger.attach(
        trainer,
        log_handler=OutputHandler(
            tag="train",
            metric_names=train_metric_names, # list or all
            global_step_transform=global_step_from_engine(trainer),
        ),
        event_name=Events.EPOCH_STARTED,
    )

And in the case of classification report, we end with something not much desirable, that is repeated metrics in the TensorBoard logger, since we will flatten and get something like this:

$ENGINE_METRIC_NAME/macro_avg/precision
macro_avg/precision

We could potentially list only the metrics we want, or a blacklist of metrics that we don't want (not supported), but in any way, would be just a workaround of the real problem.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions