Repository: Project-MONAI/MONAI

Issue:
resume DynUnet in v0.5.3 for MONAILabel
**Is your feature request related to a problem? Please describe.**
the previous version of DynUNet is still needed for MONAILabel v0.1
see also https://github.com/Project-MONAI/MONAILabel/issues/152


Discussion:

Resolving patch:
diff --git a/monai/networks/blocks/dynunet_block_v1.py b/monai/networks/blocks/dynunet_block_v1.py
new file mode 100644
--- /dev/null
+++ b/monai/networks/blocks/dynunet_block_v1.py
@@ -0,0 +1,152 @@
+# Copyright 2020 - 2021 MONAI Consortium
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Sequence, Union
+
+import numpy as np
+import torch.nn as nn
+
+from monai.networks.blocks.dynunet_block import UnetBasicBlock, UnetResBlock, UnetUpBlock, get_conv_layer
+from monai.networks.layers.factories import Norm
+from monai.networks.layers.utils import get_act_layer
+
+
+class _UnetResBlockV1(UnetResBlock):
+    """
+    UnetResBlock for backward compatibility purpose.
+    """
+
+    def __init__(
+        self,
+        spatial_dims: int,
+        in_channels: int,
+        out_channels: int,
+        kernel_size: Union[Sequence[int], int],
+        stride: Union[Sequence[int], int],
+        norm_name: str,
+    ):
+        nn.Module.__init__(self)
+        self.conv1 = get_conv_layer(
+            spatial_dims,
+            in_channels,
+            out_channels,
+            kernel_size=kernel_size,
+            stride=stride,
+            conv_only=True,
+        )
+        self.conv2 = get_conv_layer(
+            spatial_dims,
+            out_channels,
+            out_channels,
+            kernel_size=kernel_size,
+            stride=1,
+            conv_only=True,
+        )
+        self.conv3 = get_conv_layer(
+            spatial_dims,
+            in_channels,
+            out_channels,
+            kernel_size=1,
+            stride=stride,
+            conv_only=True,
+        )
+        self.lrelu = get_act_layer(("leakyrelu", {"inplace": True, "negative_slope": 0.01}))
+        self.norm1 = _get_norm_layer(spatial_dims, out_channels, norm_name)
+        self.norm2 = _get_norm_layer(spatial_dims, out_channels, norm_name)
+        self.norm3 = _get_norm_layer(spatial_dims, out_channels, norm_name)
+        self.downsample = in_channels != out_channels
+        stride_np = np.atleast_1d(stride)
+        if not np.all(stride_np == 1):
+            self.downsample = True
+
+
+class _UnetBasicBlockV1(UnetBasicBlock):
+    """
+    UnetBasicBlock for backward compatibility purpose.
+    """
+
+    def __init__(
+        self,
+        spatial_dims: int,
+        in_channels: int,
+        out_channels: int,
+        kernel_size: Union[Sequence[int], int],
+        stride: Union[Sequence[int], int],
+        norm_name: str,
+    ):
+        nn.Module.__init__(self)
+        self.conv1 = get_conv_layer(
+            spatial_dims,
+            in_channels,
+            out_channels,
+            kernel_size=kernel_size,
+            stride=stride,
+            conv_only=True,
+        )
+        self.conv2 = get_conv_layer(
+            spatial_dims,
+            out_channels,
+            out_channels,
+            kernel_size=kernel_size,
+            stride=1,
+            conv_only=True,
+        )
+        self.lrelu = get_act_layer(("leakyrelu", {"inplace": True, "negative_slope": 0.01}))
+        self.norm1 = _get_norm_layer(spatial_dims, out_channels, norm_name)
+        self.norm2 = _get_norm_layer(spatial_dims, out_channels, norm_name)
+
+
+class _UnetUpBlockV1(UnetUpBlock):
+    """
+    UnetUpBlock for backward compatibility purpose.
+    """
+
+    def __init__(
+        self,
+        spatial_dims: int,
+        in_channels: int,
+        out_channels: int,
+        kernel_size: Union[Sequence[int], int],
+        stride: Union[Sequence[int], int],
+        upsample_kernel_size: Union[Sequence[int], int],
+        norm_name: str,
+    ):
+        nn.Module.__init__(self)
+        upsample_stride = upsample_kernel_size
+        self.transp_conv = get_conv_layer(
+            spatial_dims,
+            in_channels,
+            out_channels,
+            kernel_size=upsample_kernel_size,
+            stride=upsample_stride,
+            conv_only=True,
+            is_transposed=True,
+        )
+        self.conv_block = _UnetBasicBlockV1(
+            spatial_dims,
+            out_channels + out_channels,
+            out_channels,
+            kernel_size=kernel_size,
+            stride=1,
+            norm_name=norm_name,
+        )
+
+
+def _get_norm_layer(spatial_dims: int, out_channels: int, norm_name: str, num_groups: int = 16):
+    if norm_name not in ["batch", "instance", "group"]:
+        raise ValueError(f"Unsupported normalization mode: {norm_name}")
+    if norm_name == "group":
+        if out_channels % num_groups != 0:
+            raise AssertionError("out_channels should be divisible by num_groups.")
+        norm = Norm[norm_name, spatial_dims](num_groups=num_groups, num_channels=out_channels, affine=True)
+    else:
+        norm = Norm[norm_name, spatial_dims](out_channels, affine=True)
+    return norm
diff --git a/monai/networks/nets/dynunet_v1.py b/monai/networks/nets/dynunet_v1.py
new file mode 100644
--- /dev/null
+++ b/monai/networks/nets/dynunet_v1.py
@@ -0,0 +1,140 @@
+# Copyright 2020 - 2021 MONAI Consortium
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from typing import List, Sequence, Union
+
+import torch
+import torch.nn as nn
+
+from monai.networks.blocks.dynunet_block_v1 import _UnetBasicBlockV1, _UnetResBlockV1, _UnetUpBlockV1
+from monai.networks.nets.dynunet import DynUNet, DynUNetSkipLayer
+from monai.utils import deprecated
+
+__all__ = ["DynUNetV1", "DynUnetV1", "DynunetV1"]
+
+
+@deprecated(
+    since="0.6.0",
+    removed="0.7.0",
+    msg_suffix="This module is for backward compatibility purpose only. Please use `DynUNet` instead.",
+)
+class DynUNetV1(DynUNet):
+    """
+    This a deprecated reimplementation of a dynamic UNet (DynUNet), please use `monai.networks.nets.DynUNet` instead.
+
+    Args:
+        spatial_dims: number of spatial dimensions.
+        in_channels: number of input channels.
+        out_channels: number of output channels.
+        kernel_size: convolution kernel size.
+        strides: convolution strides for each blocks.
+        upsample_kernel_size: convolution kernel size for transposed convolution layers.
+        norm_name: [``"batch"``, ``"instance"``, ``"group"``]. Defaults to "instance".
+        deep_supervision: whether to add deep supervision head before output. Defaults to ``False``.
+        deep_supr_num: number of feature maps that will output during deep supervision head. Defaults to 1.
+        res_block: whether to use residual connection based convolution blocks during the network.
+            Defaults to ``False``.
+    """
+
+    def __init__(
+        self,
+        spatial_dims: int,
+        in_channels: int,
+        out_channels: int,
+        kernel_size: Sequence[Union[Sequence[int], int]],
+        strides: Sequence[Union[Sequence[int], int]],
+        upsample_kernel_size: Sequence[Union[Sequence[int], int]],
+        norm_name: str = "instance",
+        deep_supervision: bool = False,
+        deep_supr_num: int = 1,
+        res_block: bool = False,
+    ):
+        nn.Module.__init__(self)
+        self.spatial_dims = spatial_dims
+        self.in_channels = in_channels
+        self.out_channels = out_channels
+        self.kernel_size = kernel_size
+        self.strides = strides
+        self.upsample_kernel_size = upsample_kernel_size
+        self.norm_name = norm_name
+        self.conv_block = _UnetResBlockV1 if res_block else _UnetBasicBlockV1  # type: ignore
+        self.filters = [min(2 ** (5 + i), 320 if spatial_dims == 3 else 512) for i in range(len(strides))]
+        self.input_block = self.get_input_block()
+        self.downsamples = self.get_downsamples()
+        self.bottleneck = self.get_bottleneck()
+        self.upsamples = self.get_upsamples()
+        self.output_block = self.get_output_block(0)
+        self.deep_supervision = deep_supervision
+        self.deep_supervision_heads = self.get_deep_supervision_heads()
+        self.deep_supr_num = deep_supr_num
+        self.apply(self.initialize_weights)
+        self.check_kernel_stride()
+        self.check_deep_supr_num()
+
+        # initialize the typed list of supervision head outputs so that Torchscript can recognize what's going on
+        self.heads: List[torch.Tensor] = [torch.rand(1)] * (len(self.deep_supervision_heads) + 1)
+
+        def create_skips(index, downsamples, upsamples, superheads, bottleneck):
+            """
+            Construct the UNet topology as a sequence of skip layers terminating with the bottleneck layer. This is
+            done recursively from the top down since a recursive nn.Module subclass is being used to be compatible
+            with Torchscript. Initially the length of `downsamples` will be one more than that of `superheads`
+            since the `input_block` is passed to this function as the first item in `downsamples`, however this
+            shouldn't be associated with a supervision head.
+            """
+
+            if len(downsamples) != len(upsamples):
+                raise AssertionError(f"{len(downsamples)} != {len(upsamples)}")
+            if (len(downsamples) - len(superheads)) not in (1, 0):
+                raise AssertionError(f"{len(downsamples)}-(0,1) != {len(superheads)}")
+
+            if len(downsamples) == 0:  # bottom of the network, pass the bottleneck block
+                return bottleneck
+            if index == 0:  # don't associate a supervision head with self.input_block
+                current_head, rest_heads = nn.Identity(), superheads
+            elif not self.deep_supervision:  # bypass supervision heads by passing nn.Identity in place of a real one
+                current_head, rest_heads = nn.Identity(), superheads[1:]
+            else:
+                current_head, rest_heads = superheads[0], superheads[1:]
+
+            # create the next layer down, this will stop at the bottleneck layer
+            next_layer = create_skips(1 + index, downsamples[1:], upsamples[1:], rest_heads, bottleneck)
+
+            return DynUNetSkipLayer(index, self.heads, downsamples[0], upsamples[0], current_head, next_layer)
+
+        self.skip_layers = create_skips(
+            0,
+            [self.input_block] + list(self.downsamples),
+            self.upsamples[::-1],
+            self.deep_supervision_heads,
+            self.bottleneck,
+        )
+
+    def get_upsamples(self):
+        inp, out = self.filters[1:][::-1], self.filters[:-1][::-1]
+        strides, kernel_size = self.strides[1:][::-1], self.kernel_size[1:][::-1]
+        upsample_kernel_size = self.upsample_kernel_size[::-1]
+        return self.get_module_list(inp, out, kernel_size, strides, _UnetUpBlockV1, upsample_kernel_size)
+
+    @staticmethod
+    def initialize_weights(module):
+        name = module.__class__.__name__.lower()
+        if "conv3d" in name or "conv2d" in name:
+            nn.init.kaiming_normal_(module.weight, a=0.01)
+            if module.bias is not None:
+                nn.init.constant_(module.bias, 0)
+        elif "norm" in name:
+            nn.init.normal_(module.weight, 1.0, 0.02)
+            nn.init.zeros_(module.bias)
+
+
+DynUnetV1 = DynunetV1 = DynUNetV1


Test patch:
diff --git a/tests/test_dynunet_v1.py b/tests/test_dynunet_v1.py
new file mode 100644
--- /dev/null
+++ b/tests/test_dynunet_v1.py
@@ -0,0 +1,128 @@
+# Copyright 2020 - 2021 MONAI Consortium
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+from typing import Any, Sequence, Union
+
+import torch
+from parameterized import parameterized
+
+from monai.networks import eval_mode
+from monai.networks.nets.dynunet_v1 import DynUNetV1
+from tests.utils import skip_if_quick, test_script_save
+
+device = "cuda" if torch.cuda.is_available() else "cpu"
+
+strides: Sequence[Union[Sequence[int], int]]
+kernel_size: Sequence[Any]
+expected_shape: Sequence[Any]
+
+TEST_CASE_DYNUNET_2D = []
+for kernel_size in [(3, 3, 3, 1), ((3, 1), 1, (3, 3), (1, 1))]:
+    for strides in [(1, 1, 1, 1), (2, 2, 2, 1)]:
+        for in_channels in [2, 3]:
+            for res_block in [True, False]:
+                out_channels = 2
+                in_size = 64
+                spatial_dims = 2
+                expected_shape = (1, out_channels, *[in_size // strides[0]] * spatial_dims)
+                test_case = [
+                    {
+                        "spatial_dims": spatial_dims,
+                        "in_channels": in_channels,
+                        "out_channels": out_channels,
+                        "kernel_size": kernel_size,
+                        "strides": strides,
+                        "upsample_kernel_size": strides[1:],
+                        "norm_name": "batch",
+                        "deep_supervision": False,
+                        "res_block": res_block,
+                    },
+                    (1, in_channels, in_size, in_size),
+                    expected_shape,
+                ]
+                TEST_CASE_DYNUNET_2D.append(test_case)
+
+TEST_CASE_DYNUNET_3D = []  # in 3d cases, also test anisotropic kernel/strides
+for out_channels in [2, 3]:
+    for res_block in [True, False]:
+        in_channels = 1
+        in_size = 64
+        expected_shape = (1, out_channels, 64, 32, 64)
+        test_case = [
+            {
+                "spatial_dims": 3,
+                "in_channels": in_channels,
+                "out_channels": out_channels,
+                "kernel_size": (3, (1, 1, 3), 3, 3),
+                "strides": ((1, 2, 1), 2, 2, 1),
+                "upsample_kernel_size": (2, 2, 1),
+                "norm_name": "instance",
+                "deep_supervision": False,
+                "res_block": res_block,
+            },
+            (1, in_channels, in_size, in_size, in_size),
+            expected_shape,
+        ]
+        TEST_CASE_DYNUNET_3D.append(test_case)
+
+TEST_CASE_DEEP_SUPERVISION = []
+for spatial_dims in [2, 3]:
+    for res_block in [True, False]:
+        for deep_supr_num in [1, 2]:
+            for strides in [(1, 2, 1, 2, 1), (2, 2, 2, 1), (2, 1, 1, 2, 2)]:
+                scale = strides[0]
+                test_case = [
+                    {
+                        "spatial_dims": spatial_dims,
+                        "in_channels": 1,
+                        "out_channels": 2,
+                        "kernel_size": [3] * len(strides),
+                        "strides": strides,
+                        "upsample_kernel_size": strides[1:],
+                        "norm_name": "group",
+                        "deep_supervision": True,
+                        "deep_supr_num": deep_supr_num,
+                        "res_block": res_block,
+                    },
+                    (1, 1, *[in_size] * spatial_dims),
+                    (1, 1 + deep_supr_num, 2, *[in_size // scale] * spatial_dims),
+                ]
+                TEST_CASE_DEEP_SUPERVISION.append(test_case)
+
+
+@skip_if_quick
+class TestDynUNet(unittest.TestCase):
+    @parameterized.expand(TEST_CASE_DYNUNET_2D + TEST_CASE_DYNUNET_3D)
+    def test_shape(self, input_param, input_shape, expected_shape):
+        net = DynUNetV1(**input_param).to(device)
+        with eval_mode(net):
+            result = net(torch.randn(input_shape).to(device))
+            self.assertEqual(result.shape, expected_shape)
+
+    def test_script(self):
+        input_param, input_shape, _ = TEST_CASE_DYNUNET_2D[0]
+        net = DynUNetV1(**input_param)
+        test_data = torch.randn(input_shape)
+        test_script_save(net, test_data)
+
+
+class TestDynUNetDeepSupervision(unittest.TestCase):
+    @parameterized.expand(TEST_CASE_DEEP_SUPERVISION)
+    def test_shape(self, input_param, input_shape, expected_shape):
+        net = DynUNetV1(**input_param).to(device)
+        with torch.no_grad():
+            results = net(torch.randn(input_shape).to(device))
+            self.assertEqual(results.shape, expected_shape)
+
+
+if __name__ == "__main__":
+    unittest.main()