Skip to content

Uniformly handling of metric types for all loggers#2021

Merged
vfdev-5 merged 9 commits intopytorch:masterfrom
Muktan:pytorch-ignite-1965
Jun 4, 2021
Merged

Uniformly handling of metric types for all loggers#2021
vfdev-5 merged 9 commits intopytorch:masterfrom
Muktan:pytorch-ignite-1965

Conversation

@Muktan
Copy link
Contributor

@Muktan Muktan commented Jun 1, 2021

Fixes #1965

Description:
Checked all the output handler class but only wandb_logger had changes all other loggers check the instance types.
Most loggers added elif isinstance(value, torch.Tensor) and value.ndimension() == 1: condition and inside condition iterated over each item in value and logged it. But I have not added this for wandb_logger because it may change the initial logging behaviour.

Check list:

  • New tests are added (if a new feature is added)
  • New doc strings: description and/or example code are in RST format
  • Documentation is updated (if required)

@github-actions github-actions bot added the module: contrib Contrib module label Jun 1, 2021
@vfdev-5
Copy link
Collaborator

vfdev-5 commented Jun 1, 2021

@Muktan thanks for the quick PR !

Checked all the output handler class but only wandb_logger had changes all other loggers check the instance types.
Most loggers added elif isinstance(value, torch.Tensor) and value.ndimension() == 1: condition and inside condition iterated over each item in value and logged it. But I have not added this for wandb_logger because it may change the initial logging behaviour.

I think the point of the issue #1965 is to change to previous behaviour and make it uniform for all loggers.

Also, how about tqdm logger, I think we have to rework _OutputHandler as well :

for key, value in metrics.items():
if isinstance(value, torch.Tensor):
if value.ndimension() == 0:
rendered_metrics[key] = value.item()
elif value.ndimension() == 1:
for i, v in enumerate(value):
k = f"{key}_{i}"
rendered_metrics[k] = v.item()
else:
warnings.warn(f"ProgressBar can not log tensor with {value.ndimension()} dimensions")
else:
rendered_metrics[key] = value

@Muktan
Copy link
Contributor Author

Muktan commented Jun 1, 2021

@vfdev-5 thanks for reply!

I think the point of the issue #1965 is to change to previous behaviour and make it uniform for all loggers.
So do we have to change the actual behaviour.

For example for wandblogger it would be like:

rendered_metrics = {}

if self.tag is not None:
     for name, value in metrics.items():
        if isinstance(value, numbers.Number) or isinstance(value, torch.Tensor) and value.ndimension() == 0: 
             rendered_metrics[f"{self.tag}/{name}"] = value
        elif isinstance(value, torch.Tensor) and value.ndimension() == 1: 
             for i, v in enumerate(value): 
                 rendered_metrics[f"{self.tag}/{name}/{i}"] = v
        else: 
             warnings.warn(f"WandBLogger output_handler can not log metrics value type {type(value)}") 

logger.log(rendered_metrics, step=global_step, sync=self.sync)

But the problem with that would occur when we run tests for the file. we have a test case (test_output_handler_metric_names) having value (metrics={"a": 55.56, "c": "Some text"}) as a string which in this case will raise the warning.
So, shall we update the test case?

Also, how about tqdm logger, I think we have to rework _OutputHandler as well :

for key, value in metrics.items():
if isinstance(value, torch.Tensor):
if value.ndimension() == 0:
rendered_metrics[key] = value.item()
elif value.ndimension() == 1:
for i, v in enumerate(value):
k = f"{key}_{i}"
rendered_metrics[k] = v.item()
else:
warnings.warn(f"ProgressBar can not log tensor with {value.ndimension()} dimensions")
else:
rendered_metrics[key] = value

Yes, for tqdm logger we have to rework _OutputHandler.

I thought we have to make sure there is instance checking done. But it seems we need to follow the code structure of handling of metric types as in neptune, for all other loggers, correct?
In that test cases won't fail?

@vfdev-5
Copy link
Collaborator

vfdev-5 commented Jun 1, 2021

But the problem with that would occur when we run tests for the file. we have a test case (test_output_handler_metric_names) having value (metrics={"a": 55.56, "c": "Some text"}) as a string which in this case will raise the warning.
So, shall we update the test case?

That's a good point. I think logging a metric as string is not a common usage. Yes, I think updating the test cases is a solution.

I thought we have to make sure there is instance checking done. But it seems we need to follow the code structure of handling of metric types as in neptune, for all other loggers, correct?

We would like to have more or less similar implementation for all loggers. Yes, Neptune logger is an example and should behave as Tensorboard, Visdom and some others...

@Muktan
Copy link
Contributor Author

Muktan commented Jun 1, 2021

That's a good point. I think logging a metric as string is not a common usage. Yes, I think updating the test cases is a solution.

Ok, so I would update the test case.

We would like to have more or less similar implementation for all loggers. Yes, Neptune logger is an example and should behave as Tensorboard, Visdom and some others...

Okay, I will try to make a similar code structure for all the other loggers. Also, does the above wandb implementation look correct?

@vfdev-5
Copy link
Collaborator

vfdev-5 commented Jun 1, 2021

Okay, I will try to make a similar code structure for all the other loggers. Also, does the above wandb implementation look correct?

It looks like correct. I was also exploring mlflow logger implementation and explicit call v.item() probably could make sense here as well ?

Let me list all implementations and let's decide:

  • ClearML

    for key, value in metrics.items():
    if isinstance(value, numbers.Number) or isinstance(value, torch.Tensor) and value.ndimension() == 0:
    logger.clearml_logger.report_scalar(title=self.tag, series=key, iteration=global_step, value=value)
    elif isinstance(value, torch.Tensor) and value.ndimension() == 1:
    for i, v in enumerate(value):
    logger.clearml_logger.report_scalar(
    title=f"{self.tag}/{key}", series=str(i), iteration=global_step, value=v.item()
    )
    else:
    warnings.warn(f"ClearMLLogger output_handler can not log metrics value type {type(value)}")

  • MLflow

    rendered_metrics = {} # type: Dict[str, float]
    for key, value in metrics.items():
    if isinstance(value, numbers.Number):
    rendered_metrics[f"{self.tag} {key}"] = value # type: ignore[assignment]
    elif isinstance(value, torch.Tensor) and value.ndimension() == 0:
    rendered_metrics[f"{self.tag} {key}"] = value.item()
    elif isinstance(value, torch.Tensor) and value.ndimension() == 1:
    for i, v in enumerate(value):
    rendered_metrics[f"{self.tag} {key} {i}"] = v.item()
    else:
    warnings.warn(f"MLflowLogger output_handler can not log metrics value type {type(value)}")

  • Neptune

    for key, value in metrics.items():
    if isinstance(value, numbers.Number) or isinstance(value, torch.Tensor) and value.ndimension() == 0:
    logger.log_metric(f"{self.tag}/{key}", x=global_step, y=value)
    elif isinstance(value, torch.Tensor) and value.ndimension() == 1:
    for i, v in enumerate(value):
    logger.log_metric(f"{self.tag}/{key}/{i}", x=global_step, y=v.item())
    else:
    warnings.warn(f"NeptuneLogger output_handler can not log metrics value type {type(value)}")

  • Polyaxon

    rendered_metrics = {"step": global_step} # type: Dict[str, Union[float, numbers.Number]]
    for key, value in metrics.items():
    if isinstance(value, numbers.Number):
    rendered_metrics[f"{self.tag}/{key}"] = value
    elif isinstance(value, torch.Tensor) and value.ndimension() == 0:
    rendered_metrics[f"{self.tag}/{key}"] = value.item()
    elif isinstance(value, torch.Tensor) and value.ndimension() == 1:
    for i, v in enumerate(value):
    rendered_metrics[f"{self.tag}/{key}/{i}"] = v.item()
    else:
    warnings.warn(f"PolyaxonLogger output_handler can not log metrics value type {type(value)}")

  • TensorBoard

    for key, value in metrics.items():
    if isinstance(value, numbers.Number) or isinstance(value, torch.Tensor) and value.ndimension() == 0:
    logger.writer.add_scalar(f"{self.tag}/{key}", value, global_step)
    elif isinstance(value, torch.Tensor) and value.ndimension() == 1:
    for i, v in enumerate(value):
    logger.writer.add_scalar(f"{self.tag}/{key}/{i}", v.item(), global_step)
    else:
    warnings.warn(f"TensorboardLogger output_handler can not log metrics value type {type(value)}")

  • TQDM

    rendered_metrics = OrderedDict()
    for key, value in metrics.items():
    if isinstance(value, torch.Tensor):
    if value.ndimension() == 0:
    rendered_metrics[key] = value.item()
    elif value.ndimension() == 1:
    for i, v in enumerate(value):
    k = f"{key}_{i}"
    rendered_metrics[k] = v.item()
    else:
    warnings.warn(f"ProgressBar can not log tensor with {value.ndimension()} dimensions")
    else:
    rendered_metrics[key] = value

  • Visdom

    for key, value in metrics.items():
    values = [] # type: List[Union[float, torch.Tensor]]
    keys = []
    if isinstance(value, numbers.Number) or isinstance(value, torch.Tensor) and value.ndimension() == 0:
    values.append(value) # type: ignore[arg-type]
    keys.append(key)
    elif isinstance(value, torch.Tensor) and value.ndimension() == 1:
    values = value # type: ignore[assignment]
    keys = [f"{key}/{i}" for i in range(len(value))]
    else:
    warnings.warn(f"VisdomLogger output_handler can not log metrics value type {type(value)}")
    for k, v in zip(keys, values):
    k = f"{self.tag}/{k}"
    self.add_scalar(logger, k, v, event_name, global_step)

  • W&B

    metrics = self._setup_output_metrics(engine)
    if self.tag is not None:
    metrics = {f"{self.tag}/{name}": value for name, value in metrics.items()}
    logger.log(metrics, step=global_step, sync=self.sync)

Actually, I think polyaxon and mlflow and tqdm do the data type dispatch in the correct way:

if value is a number:
    log or append it to a dict as is: value
elif value is a 0-d Tensor:
    log or append it to a dict using: value.item() 
elif value is a 1-d Tensor:
    loop over values and
        log or append it to a dict using: v.item() 
else:
    warn about unhandled dtype

Let's implement this logic to all OutputHandlers. What do you think ?

@Muktan
Copy link
Contributor Author

Muktan commented Jun 1, 2021

Yes, value.item() for type tensor seems more apt.

@Muktan Muktan changed the title Added handling of metric types for Wandblogger Uniformly handling of metric types for all loggers Jun 1, 2021
Copy link
Collaborator

@vfdev-5 vfdev-5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot @Muktan ! Looks good to me !

@vfdev-5
Copy link
Collaborator

vfdev-5 commented Jun 2, 2021

@Muktan could you please fix mypy issues ?

@Muktan Muktan requested a review from vfdev-5 June 3, 2021 05:02
@Muktan
Copy link
Contributor Author

Muktan commented Jun 3, 2021

@vfdev-5 shall I add the test for missing lines as indicated by codecov?

@vfdev-5
Copy link
Collaborator

vfdev-5 commented Jun 3, 2021

@vfdev-5 shall I add the test for missing lines as indicated by codecov?

@Muktan yes, please

@vfdev-5
Copy link
Collaborator

vfdev-5 commented Jun 4, 2021

Thanks for the update @Muktan ! Looks good, let's what CI and codecov says.

@vfdev-5 vfdev-5 merged commit d16d15e into pytorch:master Jun 4, 2021
@Muktan Muktan deleted the pytorch-ignite-1965 branch June 4, 2021 14:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

module: contrib Contrib module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make sure that all exp tracking system loggers handle metric types in the same way

2 participants