Repository: pytorch/ignite

Issue:
Use `score_name` as metric's key in Checkpoint
## 🚀 Feature

We can provide a simplification of our code when saving the best model by metric. 

Currently, to save 2 best models according to the validation accuracy, we are doing:
```python
best_model_handler = Checkpoint(
    {"model": model},
    save_handler,
    filename_prefix="best",
    n_saved=2,
    score_name="accuracy",
    score_function=Checkpoint.get_default_score_fn("accuracy"),
)
evaluator.add_event_handler(Events.COMPLETED, best_model_handler)
```

However, we can fetch define `score_function` as `Checkpoint.get_default_score_fn("accuracy")` by default if `score_name` is set. Thus, we can remove one line of code:
```python
best_model_handler = Checkpoint(
    {"model": model},
    save_handler,
    filename_prefix="best",
    n_saved=2,
    score_name="accuracy",
)
evaluator.add_event_handler(Events.COMPLETED, best_model_handler)
```




Discussion:
Do we want to delete `score_function` argument? or that would be BC breaking?
> Do we want to delete score_function argument?

Definitely, not

Resolving patch:
diff --git a/ignite/handlers/checkpoint.py b/ignite/handlers/checkpoint.py
--- a/ignite/handlers/checkpoint.py
+++ b/ignite/handlers/checkpoint.py
@@ -84,7 +84,8 @@ class Checkpoint(Serializable):
             :class:`~ignite.engine.engine.Engine` object, and returning a score (`float`). Objects with highest scores
             will be retained.
         score_name: If ``score_function`` not None, it is possible to store its value using
-            ``score_name``. See Notes for more details.
+            ``score_name``. If ``score_function`` is None, ``score_name`` can be used alone to define ``score_function``
+            as ``Checkpoint.get_default_score_fn(score_name)`` by default.
         n_saved: Number of objects that should be kept on disk. Older files will be removed. If set to
             `None`, all objects are kept.
         global_step_transform: global step transform function to output a desired global step.
@@ -225,29 +226,33 @@ class Checkpoint(Serializable):
 
             trainer = ...
             evaluator = ...
-            # Setup Accuracy metric computation on evaluator
+            # Setup Accuracy metric computation on evaluator.
+            # evaluator.state.metrics contain 'accuracy',
+            # which will be used to define ``score_function`` automatically.
             # Run evaluation on epoch completed event
             # ...
 
-            score_function = Checkpoint.get_default_score_fn("accuracy")
-
             to_save = {'model': model}
             handler = Checkpoint(
                 to_save, DiskSaver('/tmp/models', create_dir=True),
                 n_saved=2, filename_prefix='best',
-                score_function=score_function, score_name="val_acc",
+                score_name="accuracy",
                 global_step_transform=global_step_from_engine(trainer)
             )
 
             evaluator.add_event_handler(Events.COMPLETED, handler)
 
             trainer.run(data_loader, max_epochs=10)
-            > ["best_model_9_val_acc=0.77.pt", "best_model_10_val_acc=0.78.pt", ]
+            > ["best_model_9_accuracy=0.77.pt", "best_model_10_accuracy=0.78.pt", ]
 
     .. versionchanged:: 0.4.3
 
         - Checkpoint can save model with same filename.
         - Added ``greater_or_equal`` argument.
+
+    .. versionchanged:: 0.5.0
+
+        - `score_name` can be used to define `score_function` automatically without providing `score_function`.
     """
 
     Item = NamedTuple("Item", [("priority", int), ("filename", str)])
@@ -284,9 +289,6 @@ def __init__(
         if not (callable(save_handler) or isinstance(save_handler, BaseSaveHandler)):
             raise TypeError("Argument `save_handler` should be callable or inherit from BaseSaveHandler")
 
-        if score_function is None and score_name is not None:
-            raise ValueError("If `score_name` is provided, then `score_function` " "should be also provided.")
-
         if global_step_transform is not None and not callable(global_step_transform):
             raise TypeError(f"global_step_transform should be a function, got {type(global_step_transform)} instead.")
 
@@ -295,6 +297,8 @@ def __init__(
         self.save_handler = save_handler
         self.score_function = score_function
         self.score_name = score_name
+        if self.score_name is not None and self.score_function is None:
+            self.score_function = self.get_default_score_fn(self.score_name)
         self.n_saved = n_saved
         self.ext = "pt"
         self.global_step_transform = global_step_transform


Test patch:
diff --git a/tests/ignite/handlers/test_checkpoint.py b/tests/ignite/handlers/test_checkpoint.py
--- a/tests/ignite/handlers/test_checkpoint.py
+++ b/tests/ignite/handlers/test_checkpoint.py
@@ -53,11 +53,6 @@ def test_checkpoint_wrong_input():
     with pytest.raises(TypeError, match=r"Argument `save_handler` should be callable"):
         Checkpoint(to_save, 12, "prefix")
 
-    with pytest.raises(
-        ValueError, match=r"If `score_name` is provided, then `score_function` should be also provided."
-    ):
-        Checkpoint(to_save, lambda x: x, score_name="acc")
-
     with pytest.raises(TypeError, match=r"global_step_transform should be a function."):
         Checkpoint(to_save, lambda x: x, score_function=lambda e: 123, score_name="acc", global_step_transform=123)
 
@@ -274,6 +269,45 @@ def _test(to_save, obj, name):
     _test(to_save, {"model": model.state_dict(), "optimizer": optimizer.state_dict()}, "checkpoint")
 
 
+def test_checkpoint_with_score_name_only():
+    def _test(to_save, obj, name):
+        save_handler = MagicMock(spec=BaseSaveHandler)
+
+        trainer = Engine(lambda e, b: None)
+        evaluator = Engine(lambda e, b: None)
+        trainer.state = State(epoch=11, iteration=1)
+
+        checkpointer = Checkpoint(
+            to_save,
+            save_handler=save_handler,
+            global_step_transform=lambda _1, _2: trainer.state.epoch,
+            score_name="val_acc",
+        )
+
+        evaluator.state = State(epoch=1, iteration=1000, metrics={"val_acc": 0.77})
+
+        checkpointer(evaluator)
+        assert save_handler.call_count == 1
+
+        metadata = {"basename": name, "score_name": "val_acc", "priority": 0.77}
+        save_handler.assert_called_with(obj, f"{name}_11_val_acc=0.7700.pt", metadata)
+
+        trainer.state.epoch = 12
+        evaluator.state.metrics["val_acc"] = 0.78
+
+        checkpointer(evaluator)
+        assert save_handler.call_count == 2
+        metadata["priority"] = 0.78
+        save_handler.assert_called_with(obj, f"{name}_12_val_acc=0.7800.pt", metadata)
+        assert save_handler.remove.call_count == 1
+        save_handler.remove.assert_called_with(f"{name}_11_val_acc=0.7700.pt")
+        assert checkpointer.last_checkpoint == f"{name}_12_val_acc=0.7800.pt"
+
+    model = DummyModel()
+    to_save = {"model": model}
+    _test(to_save, model.state_dict(), "model")
+
+
 def test_checkpoint_with_score_name_and_function():
     def _test(to_save, obj, name):
         save_handler = MagicMock(spec=BaseSaveHandler)
@@ -506,9 +540,6 @@ def test_model_checkpoint_args_validation(dirname):
     with pytest.raises(ValueError, match=r"Directory path '\S+' is not found"):
         ModelCheckpoint(os.path.join(dirname, "non_existing_dir"), _PREFIX, create_dir=False)
 
-    with pytest.raises(ValueError, match=r"If `score_name` is provided, then `score_function` "):
-        ModelCheckpoint(existing, _PREFIX, create_dir=False, score_name="test")
-
     with pytest.raises(TypeError, match=r"global_step_transform should be a function"):
         ModelCheckpoint(existing, _PREFIX, create_dir=False, global_step_transform=1234)