Repository: DeepRegNet/DeepReg

Issue:
rename neg_weight to background_weight
## Subject of the documentation

As in https://github.com/DeepRegNet/DeepReg/blob/859df2a91564d16c43ea761ad272de568b8784bb/deepreg/model/loss/label.py#L224 and some other losses, we have the option to change the weight of background class (we only consider binary label for now). The naming `neg_weight` is not intuitive enough, therefore we'd better rename it.

In this issue, we can also double check all label losses to support `background_weight`.

Discussion:

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4356c5ac..32c6fdeb 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -36,7 +36,7 @@ repos:
     hooks:
       - id: prettier
   - repo: https://gitlab.com/pycqa/flake8
-    rev: 3.8.4
+    rev: 3.9.0
     hooks:
       - id: flake8
 #  - repo: https://github.com/pycqa/pydocstyle
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f8032720..54043792 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@ compatible with the updates.
 
 ### Changed
 
+- Renamed `neg_weight` to `background_weight`.
 - Renamed `log_dir` to `exp_name` and `log_root` to `log_dir` respectively.
 - Uniformed local-net, global-net, u-net under a single u-net structure.
 - Simplified custom layer definitions.
diff --git a/deepreg/config/v011.py b/deepreg/config/v011.py
index 69ef2610..6020a322 100644
--- a/deepreg/config/v011.py
+++ b/deepreg/config/v011.py
@@ -129,6 +129,11 @@ def parse_label_loss(loss_config: dict) -> dict:
     if loss_config["label"]["name"] == "dice_generalized":
         loss_config["label"]["name"] = "dice"
 
+    # rename neg_weight to background_weight
+    if "neg_weight" in loss_config["label"]:
+        background_weight = loss_config["label"].pop("neg_weight")
+        loss_config["label"]["background_weight"] = background_weight
+
     return loss_config
 
 
diff --git a/deepreg/loss/label.py b/deepreg/loss/label.py
index 3d3c5c93..0d2c0b15 100644
--- a/deepreg/loss/label.py
+++ b/deepreg/loss/label.py
@@ -102,14 +102,15 @@ class DiceScore(MultiScaleLoss):
     Define dice score.
 
     The formulation is:
-        0. pos_w + neg_w = 1
+
+        0. w_fg + w_bg = 1
         1. let y_prod = y_true * y_pred and y_sum  = y_true + y_pred
-        2. num = 2 *  (pos_w * y_true * y_pred + neg_w * (1−y_true) * (1−y_pred))
-               = 2 *  ((pos_w+neg_w) * y_prod - neg_w * y_sum + neg_w)
-               = 2 *  (y_prod - neg_w * y_sum + neg_w)
-        3. denom = (pos_w * (y_true + y_pred) + neg_w * (1−y_true + 1−y_pred))
-                 = (pos_w-neg_w) * y_sum + 2 * neg_w
-                 = (1-2*neg_w) * y_sum + 2 * neg_w
+        2. num = 2 *  (w_fg * y_true * y_pred + w_bg * (1−y_true) * (1−y_pred))
+               = 2 *  ((w_fg+w_bg) * y_prod - w_bg * y_sum + w_bg)
+               = 2 *  (y_prod - w_bg * y_sum + w_bg)
+        3. denom = (w_fg * (y_true + y_pred) + w_bg * (1−y_true + 1−y_pred))
+                 = (w_fg-w_bg) * y_sum + 2 * w_bg
+                 = (1-2*w_bg) * y_sum + 2 * w_bg
         4. dice score = num / denom
 
     where num and denom are summed over all axes except the batch axis.
@@ -118,7 +119,7 @@ class DiceScore(MultiScaleLoss):
     def __init__(
         self,
         binary: bool = False,
-        neg_weight: float = 0.0,
+        background_weight: float = 0.0,
         scales: Optional[List] = None,
         kernel: str = "gaussian",
         reduction: str = tf.keras.losses.Reduction.SUM,
@@ -128,7 +129,7 @@ class DiceScore(MultiScaleLoss):
         Init.
 
         :param binary: if True, project y_true, y_pred to 0 or 1.
-        :param neg_weight: weight for negative class.
+        :param background_weight: weight for background, where y == 0.
         :param scales: list of scalars or None, if None, do not apply any scaling.
         :param kernel: gaussian or cauchy.
         :param reduction: using SUM reduction over batch axis,
@@ -136,9 +137,9 @@ class DiceScore(MultiScaleLoss):
         :param name: str, name of the loss.
         """
         super().__init__(scales=scales, kernel=kernel, reduction=reduction, name=name)
-        assert 0 <= neg_weight <= 1
+        assert 0 <= background_weight <= 1
         self.binary = binary
-        self.neg_weight = neg_weight
+        self.background_weight = background_weight
 
     def _call(self, y_true: tf.Tensor, y_pred: tf.Tensor) -> tf.Tensor:
         """
@@ -159,15 +160,19 @@ class DiceScore(MultiScaleLoss):
         y_prod = tf.reduce_mean(y_true * y_pred, axis=1)
         y_sum = tf.reduce_mean(y_true, axis=1) + tf.reduce_mean(y_pred, axis=1)
 
-        numerator = 2 * (y_prod - self.neg_weight * y_sum + self.neg_weight)
-        denominator = (1 - 2 * self.neg_weight) * y_sum + 2 * self.neg_weight
+        numerator = 2 * (
+            y_prod - self.background_weight * y_sum + self.background_weight
+        )
+        denominator = (
+            1 - 2 * self.background_weight
+        ) * y_sum + 2 * self.background_weight
         return (numerator + EPS) / (denominator + EPS)
 
     def get_config(self) -> dict:
         """Return the config dictionary for recreating this class."""
         config = super().get_config()
         config["binary"] = self.binary
-        config["neg_weight"] = self.neg_weight
+        config["background_weight"] = self.background_weight
         return config
 
 
@@ -182,13 +187,13 @@ class CrossEntropy(MultiScaleLoss):
     Define weighted cross-entropy.
 
     The formulation is:
-        loss = − pos_w * y_true log(y_pred) - neg_w * (1−y_true) log(1−y_pred)
+        loss = − w_fg * y_true log(y_pred) - w_bg * (1−y_true) log(1−y_pred)
     """
 
     def __init__(
         self,
         binary: bool = False,
-        neg_weight: float = 0.0,
+        background_weight: float = 0.0,
         scales: Optional[List] = None,
         kernel: str = "gaussian",
         reduction: str = tf.keras.losses.Reduction.SUM,
@@ -198,7 +203,7 @@ class CrossEntropy(MultiScaleLoss):
         Init.
 
         :param binary: if True, project y_true, y_pred to 0 or 1
-        :param neg_weight: weight for negative class
+        :param background_weight: weight for background, where y == 0.
         :param scales: list of scalars or None, if None, do not apply any scaling.
         :param kernel: gaussian or cauchy.
         :param reduction: using SUM reduction over batch axis,
@@ -206,9 +211,9 @@ class CrossEntropy(MultiScaleLoss):
         :param name: str, name of the loss.
         """
         super().__init__(scales=scales, kernel=kernel, reduction=reduction, name=name)
-        assert 0 <= neg_weight <= 1
+        assert 0 <= background_weight <= 1
         self.binary = binary
-        self.neg_weight = neg_weight
+        self.background_weight = background_weight
 
     def _call(self, y_true: tf.Tensor, y_pred: tf.Tensor) -> tf.Tensor:
         """
@@ -226,15 +231,20 @@ class CrossEntropy(MultiScaleLoss):
         y_true = tf.keras.layers.Flatten()(y_true)
         y_pred = tf.keras.layers.Flatten()(y_pred)
 
-        loss_pos = tf.reduce_mean(y_true * tf.math.log(y_pred + EPS), axis=1)
-        loss_neg = tf.reduce_mean((1 - y_true) * tf.math.log(1 - y_pred + EPS), axis=1)
-        return -(1 - self.neg_weight) * loss_pos - self.neg_weight * loss_neg
+        loss_foreground = tf.reduce_mean(y_true * tf.math.log(y_pred + EPS), axis=1)
+        loss_background = tf.reduce_mean(
+            (1 - y_true) * tf.math.log(1 - y_pred + EPS), axis=1
+        )
+        return (
+            -(1 - self.background_weight) * loss_foreground
+            - self.background_weight * loss_background
+        )
 
     def get_config(self) -> dict:
         """Return the config dictionary for recreating this class."""
         config = super().get_config()
         config["binary"] = self.binary
-        config["neg_weight"] = self.neg_weight
+        config["background_weight"] = self.background_weight
         return config
 
 
diff --git a/docs/source/docs/configuration.md b/docs/source/docs/configuration.md
index 869fabc6..11be4932 100644
--- a/docs/source/docs/configuration.md
+++ b/docs/source/docs/configuration.md
@@ -406,21 +406,21 @@ at the same indent level:
 - `dice`: Calls a Dice loss on the labels, requires the following arguments:
 
   - `binary`: bool, default is false. If true, the tensors are thresholded at 0.5.
-  - `neg_weight`: float, default=0.0. `neg_weight` weights the foreground and background
-    classes by replacing the labels of 1s and 0s with (1-neg_weight) and neg_weight,
-    respectively.
+  - `background_weight`: float, default=0.0. `background_weight` weights the foreground
+    and background classes by replacing the labels of 1s and 0s with
+    `(1-background_weight)` and `background_weight`, respectively.
 
 - `cross-entropy`: Calls a cross-entropy loss between labels, requires the following
   arguments:
 
   - `binary`: bool, default is false. If true, the tensors are thresholded at 0.5.
-  - `neg_weight`: float, default=0.0. `neg_weight` weights the foreground and background
-    classes by replacing the labels of 1s and 0s with (1-neg_weight) and neg_weight,
-    respectively.
+  - `background_weight`: float, default=0.0. `background_weight` weights the foreground
+    and background classes by replacing the labels of 1s and 0s with
+    `(1-background_weight)` and `background_weight`, respectively.
 
 - `jaccard`: - `binary`: bool, default is false. If true, the tensors are thresholded at
 0.5.
-<!-- - `neg_weight`: float, default=0.0. `neg_weight` weights the foreground and background classes by replacing the labels of 1s and 0s with (1-neg_weight) and neg_weight, respectively. -->
+<!-- - `background_weight`: float, default=0.0. `background_weight` weights the foreground and background classes by replacing the labels of 1s and 0s with (1-background_weight) and background_weight, respectively. -->
 
 #### Regularization
 


Test patch:
diff --git a/test/unit/test_config_v011.py b/test/unit/test_config_v011.py
index f4855d77..b7f429a3 100644
--- a/test/unit/test_config_v011.py
+++ b/test/unit/test_config_v011.py
@@ -86,7 +86,7 @@ def test_parse_loss():
 
 
 class TestParseImageLoss:
-    def test_parse_old_loss(self):
+    def test_parse_outdated_loss(self):
         loss_config = {
             "image": {
                 "name": "lncc",
@@ -153,8 +153,8 @@ class TestParseLabelLoss:
             ),
         ],
     )
-    def test_parse_old_loss(self, name_loss: str, expected_config: dict):
-        loss_config = {
+    def test_parse_outdated_loss(self, name_loss: str, expected_config: dict):
+        outdated_config = {
             "label": {
                 "name": name_loss,
                 "single_scale": {
@@ -168,9 +168,27 @@ class TestParseLabelLoss:
         }
 
         if name_loss == "multi_scale":
-            loss_config["label"]["weight"] = 2.0  # type: ignore
+            outdated_config["label"]["weight"] = 2.0  # type: ignore
 
-        got = parse_label_loss(loss_config=loss_config)
+        got = parse_label_loss(loss_config=outdated_config)
+        assert got == expected_config
+
+    def test_parse_background_weight(self):
+        outdated_config = {
+            "label": {
+                "name": "dice",
+                "weight": 1.0,
+                "neg_weight": 2.0,
+            },
+        }
+        expected_config = {
+            "label": {
+                "name": "dice",
+                "weight": 1.0,
+                "background_weight": 2.0,
+            },
+        }
+        got = parse_label_loss(loss_config=outdated_config)
         assert got == expected_config
 
     def test_parse_multiple_loss(self):
@@ -200,7 +218,9 @@ class TestParseRegularizationLoss:
             ("gradient-l1", "gradient", {"l1": True}),
         ],
     )
-    def test_parse_old_loss(self, energy_type: str, loss_name: str, extra_args: dict):
+    def test_parse_outdated_loss(
+        self, energy_type: str, loss_name: str, extra_args: dict
+    ):
 
         loss_config = {
             "regularization": {
diff --git a/test/unit/test_loss_label.py b/test/unit/test_loss_label.py
index 6ca5eb29..470bc35d 100644
--- a/test/unit/test_loss_label.py
+++ b/test/unit/test_loss_label.py
@@ -44,7 +44,7 @@ class TestDiceScore:
         return np.ones(shape=self.shape) * 0.3
 
     @pytest.mark.parametrize(
-        "binary,neg_weight,scales,expected",
+        "binary,background_weight,scales,expected",
         [
             (True, 0.0, None, 0.0),
             (False, 0.0, None, 0.4),
@@ -53,22 +53,22 @@ class TestDiceScore:
             (False, 0.2, [0, 1], 0.46030036),
         ],
     )
-    def test_call(self, y_true, y_pred, binary, neg_weight, scales, expected):
+    def test_call(self, y_true, y_pred, binary, background_weight, scales, expected):
         expected = np.array([expected] * self.shape[0])  # call returns (batch, )
-        got = label.DiceScore(binary=binary, neg_weight=neg_weight, scales=scales).call(
-            y_true=y_true, y_pred=y_pred
-        )
+        got = label.DiceScore(
+            binary=binary, background_weight=background_weight, scales=scales
+        ).call(y_true=y_true, y_pred=y_pred)
         assert is_equal_tf(got, expected)
-        got = label.DiceLoss(binary=binary, neg_weight=neg_weight, scales=scales).call(
-            y_true=y_true, y_pred=y_pred
-        )
+        got = label.DiceLoss(
+            binary=binary, background_weight=background_weight, scales=scales
+        ).call(y_true=y_true, y_pred=y_pred)
         assert is_equal_tf(got, -expected)
 
     def test_get_config(self):
         got = label.DiceScore().get_config()
         expected = dict(
             binary=False,
-            neg_weight=0.0,
+            background_weight=0.0,
             scales=None,
             kernel="gaussian",
             reduction=tf.keras.losses.Reduction.SUM,
@@ -89,7 +89,7 @@ class TestCrossEntropy:
         return np.ones(shape=self.shape) * 0.3
 
     @pytest.mark.parametrize(
-        "binary,neg_weight,scales,expected",
+        "binary,background_weight,scales,expected",
         [
             (True, 0.0, None, -np.log(1.0e-7)),
             (False, 0.0, None, -0.6 * np.log(0.3)),
@@ -98,10 +98,10 @@ class TestCrossEntropy:
             (False, 0.2, [0, 1], 0.5239637),
         ],
     )
-    def test_call(self, y_true, y_pred, binary, neg_weight, scales, expected):
+    def test_call(self, y_true, y_pred, binary, background_weight, scales, expected):
         expected = np.array([expected] * self.shape[0])  # call returns (batch, )
         got = label.CrossEntropy(
-            binary=binary, neg_weight=neg_weight, scales=scales
+            binary=binary, background_weight=background_weight, scales=scales
         ).call(y_true=y_true, y_pred=y_pred)
         assert is_equal_tf(got, expected)
 
@@ -109,7 +109,7 @@ class TestCrossEntropy:
         got = label.CrossEntropy().get_config()
         expected = dict(
             binary=False,
-            neg_weight=0.0,
+            background_weight=0.0,
             scales=None,
             kernel="gaussian",
             reduction=tf.keras.losses.Reduction.SUM,