Repository: pytorch/ignite

Issue:
Add tag to OptimizerParamsHandler 
Idea is to improve `OptimizerParamsHandler` (`tensorboard_logger`, `visdom_logger`, ...) by adding a tag free field in order to be able to log several optimizers in different plots. This can be helpful when logging lr of generator/descriminator optimizers in GAN.

```python
tb_logger.attach(trainer,
                 log_handler=OptimizerParamsHandler(optimizer_D, "lr", tag="descriminator"), 
                 event_name=Events.EPOCH_STARTED)

tb_logger.attach(trainer,
                 log_handler=OptimizerParamsHandler(optimizer_G, "lr", tag="generator"), 
                 event_name=Events.EPOCH_STARTED)
```


Discussion:

Resolving patch:
diff --git a/ignite/contrib/handlers/base_logger.py b/ignite/contrib/handlers/base_logger.py
--- a/ignite/contrib/handlers/base_logger.py
+++ b/ignite/contrib/handlers/base_logger.py
@@ -50,13 +50,14 @@ class BaseOptimizerParamsHandler(BaseHandler):
     Base handler for logging optimizer parameters
     """
 
-    def __init__(self, optimizer, param_name="lr"):
+    def __init__(self, optimizer, param_name="lr", tag=None):
         if not isinstance(optimizer, torch.optim.Optimizer):
             raise TypeError("Argument optimizer should be of type torch.optim.Optimizer, "
                             "but given {}".format(type(optimizer)))
 
         self.optimizer = optimizer
         self.param_name = param_name
+        self.tag = tag
 
 
 class BaseOutputHandler(BaseHandler):
diff --git a/ignite/contrib/handlers/tensorboard_logger.py b/ignite/contrib/handlers/tensorboard_logger.py
--- a/ignite/contrib/handlers/tensorboard_logger.py
+++ b/ignite/contrib/handlers/tensorboard_logger.py
@@ -87,17 +87,19 @@ class OptimizerParamsHandler(BaseOptimizerParamsHandler):
     Args:
         optimizer (torch.optim.Optimizer): torch optimizer which parameters to log
         param_name (str): parameter name
+        tag (str, optional): common title for all produced plots. For example, 'generator'
     """
 
-    def __init__(self, optimizer, param_name="lr"):
-        super(OptimizerParamsHandler, self).__init__(optimizer, param_name)
+    def __init__(self, optimizer, param_name="lr", tag=None):
+        super(OptimizerParamsHandler, self).__init__(optimizer, param_name, tag)
 
     def __call__(self, engine, logger, event_name):
         if not isinstance(logger, TensorboardLogger):
             raise RuntimeError("Handler 'OptimizerParamsHandler' works only with TensorboardLogger")
 
         global_step = engine.state.get_event_attrib_value(event_name)
-        params = {"{}/group_{}".format(self.param_name, i): float(param_group[self.param_name])
+        tag_prefix = "{}/".format(self.tag) if self.tag else ""
+        params = {"{}{}/group_{}".format(tag_prefix, self.param_name, i): float(param_group[self.param_name])
                   for i, param_group in enumerate(self.optimizer.param_groups)}
 
         for k, v in params.items():
diff --git a/ignite/contrib/handlers/visdom_logger.py b/ignite/contrib/handlers/visdom_logger.py
--- a/ignite/contrib/handlers/visdom_logger.py
+++ b/ignite/contrib/handlers/visdom_logger.py
@@ -150,11 +150,12 @@ class OptimizerParamsHandler(BaseOptimizerParamsHandler, _BaseVisDrawer):
     Args:
         optimizer (torch.optim.Optimizer): torch optimizer which parameters to log
         param_name (str): parameter name
+        tag (str, optional): common title for all produced plots. For example, 'generator'
         show_legend (bool, optional): flag to show legend in the window
     """
 
-    def __init__(self, optimizer, param_name="lr", show_legend=False):
-        super(OptimizerParamsHandler, self).__init__(optimizer, param_name)
+    def __init__(self, optimizer, param_name="lr", tag=None, show_legend=False):
+        super(OptimizerParamsHandler, self).__init__(optimizer, param_name, tag)
         _BaseVisDrawer.__init__(self, show_legend=show_legend)
 
     def __call__(self, engine, logger, event_name):
@@ -162,7 +163,8 @@ def __call__(self, engine, logger, event_name):
             raise RuntimeError("Handler 'OptimizerParamsHandler' works only with VisdomLogger")
 
         global_step = engine.state.get_event_attrib_value(event_name)
-        params = {"{}/group_{}".format(self.param_name, i): float(param_group[self.param_name])
+        tag_prefix = "{}/".format(self.tag) if self.tag else ""
+        params = {"{}{}/group_{}".format(tag_prefix, self.param_name, i): float(param_group[self.param_name])
                   for i, param_group in enumerate(self.optimizer.param_groups)}
 
         for k, v in params.items():


Test patch:
diff --git a/tests/ignite/contrib/handlers/test_tensorboard_logger.py b/tests/ignite/contrib/handlers/test_tensorboard_logger.py
--- a/tests/ignite/contrib/handlers/test_tensorboard_logger.py
+++ b/tests/ignite/contrib/handlers/test_tensorboard_logger.py
@@ -47,6 +47,13 @@ def test_optimizer_params():
     wrapper(mock_engine, mock_logger, Events.ITERATION_STARTED)
     mock_logger.writer.add_scalar.assert_called_once_with("lr/group_0", 0.01, 123)
 
+    wrapper = OptimizerParamsHandler(optimizer, param_name="lr", tag="generator")
+    mock_logger = MagicMock(spec=TensorboardLogger)
+    mock_logger.writer = MagicMock()
+
+    wrapper(mock_engine, mock_logger, Events.ITERATION_STARTED)
+    mock_logger.writer.add_scalar.assert_called_once_with("generator/lr/group_0", 0.01, 123)
+
 
 def test_output_handler_with_wrong_logger_type():
 
diff --git a/tests/ignite/contrib/handlers/test_visdom_logger.py b/tests/ignite/contrib/handlers/test_visdom_logger.py
--- a/tests/ignite/contrib/handlers/test_visdom_logger.py
+++ b/tests/ignite/contrib/handlers/test_visdom_logger.py
@@ -73,6 +73,23 @@ def test_optimizer_params():
         name="lr/group_0"
     )
 
+    wrapper = OptimizerParamsHandler(optimizer=optimizer, param_name="lr", tag="generator")
+    mock_logger = MagicMock(spec=VisdomLogger)
+    mock_logger.vis = MagicMock()
+    mock_logger.executor = _DummyExecutor()
+
+    wrapper(mock_engine, mock_logger, Events.ITERATION_STARTED)
+
+    assert len(wrapper.windows) == 1 and "generator/lr/group_0" in wrapper.windows
+    assert wrapper.windows["generator/lr/group_0"]['win'] is not None
+
+    mock_logger.vis.line.assert_called_once_with(
+        X=[123, ], Y=[0.01, ], env=mock_logger.vis.env,
+        win=None, update=None,
+        opts=wrapper.windows['generator/lr/group_0']['opts'],
+        name="generator/lr/group_0"
+    )
+
 
 def test_output_handler_with_wrong_logger_type():