Repository: pytorch/ignite

Issue:
Rename Epoch to Iterations when using epoch_length with max_epochs=1
## 🚀 Feature

Per the discussion in https://github.com/pytorch/ignite/issues/1263, we'd like to ensure that the progress bar uses the name "Iterations" instead of "Epoch" when the `epoch_length` is passed to the trainer for `max_epochs=1`.


Discussion:
I'm looking to get into open source, and managed to get a working development build of ignite on my system. This looks like a good issue to start on. If nobody is currently working on it, I would like to give it a shot. 

An initial look through the source code seems to have the word "Epoch" located in a number of places throughout the code. Particularly in `mnist_with_tqdm_logger.py`, `handlers.py`, `dcgan.py`, `mnist_save_resume_engine.py`, `mnist_with_tensorboard_on_tpu.py`, `mnist_with_tensorboard.py`, `mnist_with_visdom.py`, `mnist.py`, `tqdm_logger.py`, `engine.py`, `events.py`, and in the test files `test_common.py` and `test_tqdm_logger.py`.

If these instances of "Epoch" do in fact need to change to "Iterations", I'm thinking that some kind of flag should be set on the initial run of the trainer when we detect that `epoch_length` is passed with `max_epochs=1`.

If this solution seems reasonable, I would appreciate guidance on where command-line arguments are passed in, as well as whether there is an already established way for passing this kind of "global information" throughout the rest of the code.
@M3L6H thanks for proposing to work on this issue !
Here idea is to tweak this part of the code : 
https://github.com/pytorch/ignite/blob/2778893db943e9869c2ce266c740c1310bc3b9de/ignite/contrib/handlers/tqdm_logger.py#L253
By default, if no "desc" is provided, it is set as "Epoch" and tqdm description uses that here : 
https://github.com/pytorch/ignite/blob/2778893db943e9869c2ce266c740c1310bc3b9de/ignite/contrib/handlers/tqdm_logger.py#L57

We can check engine's information before setting 
```python
default_desc = "Epoch" if ...
desc = self.tqdm_kwargs.get("desc", default_desc)
```
In other places, where you see "Epoch" is hard-coded, I think it is OK for those cases.

To make sure that everything is OK and in a good format, please take a look at [CONTRIBUTING](https://github.com/pytorch/ignite/blob/master/CONTRIBUTING.md) for details about code formatting and unit tests to add.

And feel free to ask here if you need more guidance.
@vfdev-5 thanks for the reply!

I've started work on the fix. Currently my solution looks like the following
```python
state_dict = engine.state_dict()
max_epochs = state_dict["max_epochs"] if "max_epochs" in state_dict else None

default_desc = "Iterations" if max_epochs and max_epochs == 1 else "Epoch"
desc = self.tqdm_kwargs.get("desc", default_desc)
```

I have also written a test
```python
def test_pbar_with_max_epochs_set_to_one(capsys):
    n_epochs = 1
    loader = [1, 2]
    engine = Engine(update_fn)

    pbar = ProgressBar()
    pbar.attach(engine, ["a"])

    engine.run(loader, max_epochs=n_epochs)

    captured = capsys.readouterr()
    err = captured.err.split("\r")
    err = list(map(lambda x: x.strip(), err))
    err = list(filter(None, err))
    expected = "Iterations [1/1]: [1/2]  50%|█████     , a=1 [00:00<00:00]"
    assert err[-1] == expected
```

However, when running the test, it fails, with the following output
```python
capsys = <_pytest.capture.CaptureFixture object at 0x7f540d32b9e8>

    def test_pbar_with_max_epochs_set_to_one(capsys):
        n_epochs = 1
        loader = [1, 2]
        engine = Engine(update_fn)
    
        pbar = ProgressBar()
        pbar.attach(engine, ["a"])
    
        engine.run(loader, max_epochs=n_epochs)
    
        captured = capsys.readouterr()
        err = captured.err.split("\r")
        err = list(map(lambda x: x.strip(), err))
        err = list(filter(None, err))
        expected = "Iterations [1/1]: [1/2]  50%|█████     , a=1 [00:00<00:00]"
>       assert err[-1] == expected
E       AssertionError: assert 'Epoch: [1/2]  50%|█████     , a=1 [00:00<00:00]' == 'Iterations [1/1]: [1/2]  50%|█████     , a=1 [00:00<00:00]'
E         - Iterations [1/1]: [1/2]  50%|█████     , a=1 [00:00<00:00]
E         ? ^^^^^^^ ^^^^^^^^
E         + Epoch: [1/2]  50%|█████     , a=1 [00:00<00:00]
E         ? ^^ ^^
```

From what I can see, it looks like the code just isn't working. However, I'm not sure if it is an issue with how I am trying to access max_epochs, (maybe using `state_dict` is not correct?), or whether it is an issue with one of the hard coded "Epochs" in code? My other concern is that maybe I wrote the test incorrectly, since in the output it is saying "Epoch: [1/2]", which I would imagine should be "Epoch: [1/1]" if the `max_epochs` was correctly set?
@M3L6H 👍 for working on the issue 

I think you are right, my bad, we can not fetch correctly `max_epochs` at this step. Let me explain that with the example of how would we use the progress bar : 
```python
engine = Engine(update_fn)
# engine.state.max_epochs is None

pbar = ProgressBar()
pbar.attach(engine, ["a"])
# but engine.state.max_epochs is still None

# only here it will be set up as n_epochs    
engine.run(loader, max_epochs=n_epochs)
```

Well, we have to do it differently, I think. For example, let's set `desc` as None by default:
```python
desc = self.tqdm_kwargs.get("desc", None)
```
and when it is used with tqdm bar, provided engine is fully set up, thus we can set desc if `self.tag is None` by either "Epoch" or "Iteration" : 
https://github.com/pytorch/ignite/blob/2778893db943e9869c2ce266c740c1310bc3b9de/ignite/contrib/handlers/tqdm_logger.py#L52


Btw, please open a draft PR and we can iterate over the modifications to get it right.

Resolving patch:
diff --git a/ignite/contrib/handlers/tqdm_logger.py b/ignite/contrib/handlers/tqdm_logger.py
--- a/ignite/contrib/handlers/tqdm_logger.py
+++ b/ignite/contrib/handlers/tqdm_logger.py
@@ -49,7 +49,10 @@ def __call__(self, engine, logger, event_name):
         if logger.pbar is None:
             logger._reset(pbar_total=pbar_total)
 
-        desc = self.tag
+        max_epochs = engine.state.max_epochs
+        default_desc = "Iteration" if max_epochs == 1 else "Epoch"
+
+        desc = self.tag or default_desc
         max_num_of_closing_events = self.get_max_number_events(self.closing_event_name, engine)
         if max_num_of_closing_events > 1:
             global_step = engine.state.get_event_attrib_value(self.closing_event_name)
@@ -95,7 +98,8 @@ class ProgressBar(BaseLogger):
             formatting, see `tqdm docs <https://tqdm.github.io/docs/tqdm/>`_.
         **tqdm_kwargs: kwargs passed to tqdm progress bar.
             By default, progress bar description displays "Epoch [5/10]" where 5 is the current epoch and 10 is the
-            number of epochs. If tqdm_kwargs defines `desc`, e.g. "Predictions", than the description is
+            number of epochs; however, if ``max_epochs`` are set to 1, the progress bar instead displays
+            "Iteration: [5/10]". If tqdm_kwargs defines `desc`, e.g. "Predictions", than the description is
             "Predictions [5/10]" if number of epochs is more than one otherwise it is simply "Predictions".
 
     Examples:
@@ -250,7 +254,7 @@ def attach(
         Note: accepted output value types are numbers, 0d and 1d torch tensors and strings
 
         """
-        desc = self.tqdm_kwargs.get("desc", "Epoch")
+        desc = self.tqdm_kwargs.get("desc", None)
 
         if event_name not in engine._allowed_events:
             raise ValueError("Logging event {} is not in allowed events for this engine".format(event_name.name))


Test patch:
diff --git a/tests/ignite/contrib/handlers/test_tqdm_logger.py b/tests/ignite/contrib/handlers/test_tqdm_logger.py
--- a/tests/ignite/contrib/handlers/test_tqdm_logger.py
+++ b/tests/ignite/contrib/handlers/test_tqdm_logger.py
@@ -141,7 +141,7 @@ def step(engine, batch):
     err = list(map(lambda x: x.strip(), err))
     err = list(filter(None, err))
     actual = err[-1]
-    expected = "Epoch: [1/2]  50%|█████     , batchloss=0.5 [00:00<00:00]"
+    expected = "Iteration: [1/2]  50%|█████     , batchloss=0.5 [00:00<00:00]"
     assert actual == expected
 
 
@@ -172,7 +172,7 @@ def step(engine, batch):
     err = list(map(lambda x: x.strip(), err))
     err = list(filter(None, err))
     actual = err[-1]
-    expected = "Epoch: [1/2]  50%|█████     , another batchloss=1.5, batchloss=0.5 [00:00<00:00]"
+    expected = "Iteration: [1/2]  50%|█████     , another batchloss=1.5, batchloss=0.5 [00:00<00:00]"
     assert actual == expected
 
 
@@ -349,6 +349,24 @@ def test_pbar_on_epochs(capsys):
     assert actual == expected
 
 
+def test_pbar_with_max_epochs_set_to_one(capsys):
+    n_epochs = 1
+    loader = [1, 2]
+    engine = Engine(update_fn)
+
+    pbar = ProgressBar()
+    pbar.attach(engine, ["a"])
+
+    engine.run(loader, max_epochs=n_epochs)
+
+    captured = capsys.readouterr()
+    err = captured.err.split("\r")
+    err = list(map(lambda x: x.strip(), err))
+    err = list(filter(None, err))
+    expected = "Iteration: [1/2]  50%|█████     , a=1 [00:00<00:00]"
+    assert err[-1] == expected
+
+
 def test_pbar_wrong_events_order():
 
     engine = Engine(update_fn)
@@ -416,7 +434,7 @@ def test_pbar_on_callable_events(capsys):
     err = list(map(lambda x: x.strip(), err))
     err = list(filter(None, err))
     actual = err[-1]
-    expected = "Epoch: [90/100]  90%|█████████  [00:00<00:00]"
+    expected = "Iteration: [90/100]  90%|█████████  [00:00<00:00]"
     assert actual == expected
 
 
@@ -432,7 +450,7 @@ def test_tqdm_logger_epoch_length(capsys):
     err = list(map(lambda x: x.strip(), err))
     err = list(filter(None, err))
     actual = err[-1]
-    expected = "Epoch: [50/50] 100%|██████████ [00:00<00:00]"
+    expected = "Iteration: [50/50] 100%|██████████ [00:00<00:00]"
     assert actual == expected