From 259541fa8aa5eef8be66c609734f8c0468d4db66 Mon Sep 17 00:00:00 2001 From: masadcv Date: Sun, 4 Apr 2021 00:01:58 +0100 Subject: [PATCH 01/16] adding init efficientnet support Signed-off-by: masadcv --- monai/networks/blocks/activation.py | 50 +++ monai/networks/layers/factories.py | 7 + monai/networks/nets/__init__.py | 1 + monai/networks/nets/efficientnet.py | 639 ++++++++++++++++++++++++++++ 4 files changed, 697 insertions(+) create mode 100755 monai/networks/nets/efficientnet.py diff --git a/monai/networks/blocks/activation.py b/monai/networks/blocks/activation.py index ef6c74f282..b422783f00 100644 --- a/monai/networks/blocks/activation.py +++ b/monai/networks/blocks/activation.py @@ -43,6 +43,56 @@ def forward(self, input: torch.Tensor) -> torch.Tensor: return input * torch.sigmoid(self.alpha * input) +class SwishImplementation(torch.autograd.Function): + r"""Memory efficient implementation for training + Follows recommendation from: + https://github.com/lukemelas/EfficientNet-PyTorch/issues/18#issuecomment-511677853 + + Results in ~ 30% memory saving during training as compared to Swish() + """ + @staticmethod + def forward(ctx: torch.autograd.Function, input: torch.Tensor): + result = input * torch.sigmoid(input) + ctx.save_for_backward(input) + return result + + @staticmethod + def backward(ctx: torch.autograd.Function, grad_output: torch.Tensor): + input = ctx.saved_tensors[0] + sigmoid_input = torch.sigmoid(input) + return grad_output * (sigmoid_input * (1 + input * (1 - sigmoid_input))) + + +class MemoryEfficientSwish(nn.Module): + r"""Applies the element-wise function: + + .. math:: + \text{Swish}(x) = x * \text{Sigmoid}(\alpha * x) for constant value alpha=1. + + Citation: Searching for Activation Functions, Ramachandran et al., 2017, https://arxiv.org/abs/1710.05941. + + Memory efficient implementation for training following recommendation from: + https://github.com/lukemelas/EfficientNet-PyTorch/issues/18#issuecomment-511677853 + + Results in ~ 30% memory saving during training as compared to Swish() + + Shape: + - Input: :math:`(N, *)` where `*` means, any number of additional + dimensions + - Output: :math:`(N, *)`, same shape as the input + + + Examples:: + + >>> m = Act['memswish']() + >>> input = torch.randn(2) + >>> output = m(input) + """ + + def forward(self, input: torch.Tensor): + return SwishImplementation.apply(input) + + class Mish(nn.Module): r"""Applies the element-wise function: diff --git a/monai/networks/layers/factories.py b/monai/networks/layers/factories.py index ec36b2ed95..9165a8ebe4 100644 --- a/monai/networks/layers/factories.py +++ b/monai/networks/layers/factories.py @@ -256,6 +256,13 @@ def swish_factory(): return Swish +@Act.factory_function("memswish") +def memswish_factory(): + from monai.networks.blocks.activation import MemoryEfficientSwish + + return MemoryEfficientSwish + + @Act.factory_function("mish") def mish_factory(): from monai.networks.blocks.activation import Mish diff --git a/monai/networks/nets/__init__.py b/monai/networks/nets/__init__.py index 6876293bdb..a112348fad 100644 --- a/monai/networks/nets/__init__.py +++ b/monai/networks/nets/__init__.py @@ -15,6 +15,7 @@ from .classifier import Classifier, Critic, Discriminator from .densenet import DenseNet, DenseNet121, DenseNet169, DenseNet201, DenseNet264 from .dynunet import DynUNet, DynUnet, Dynunet +from .efficientnet import EfficientNetBN, get_efficientnet_image_size from .fullyconnectednet import FullyConnectedNet, VarFullyConnectedNet from .generator import Generator from .highresnet import HighResBlock, HighResNet diff --git a/monai/networks/nets/efficientnet.py b/monai/networks/nets/efficientnet.py new file mode 100755 index 0000000000..ce1c1a02d9 --- /dev/null +++ b/monai/networks/nets/efficientnet.py @@ -0,0 +1,639 @@ +"""Implementation based on: https://github.com/lukemelas/EfficientNet-PyTorch +With significant modifications to refactor/rewrite the code for MONAI +""" +import collections +import re +import math +from typing import List + +import torch +from torch import nn +from torch.nn import functional as F +from torch.utils import model_zoo + +from monai.networks.layers.factories import Act, Conv, Norm, Pad, Pool + + +__all__ = [ + "get_efficientnet_image_size", + "EfficientNetBN" +] + +VALID_MODELS = ( + "efficientnet-b0", "efficientnet-b1", "efficientnet-b2", "efficientnet-b3", + "efficientnet-b4", "efficientnet-b5", "efficientnet-b6", "efficientnet-b7", + "efficientnet-b8", +) + +BLOCK_ARGS = [ + "r1_k3_s11_e1_i32_o16_se0.25", + "r2_k3_s22_e6_i16_o24_se0.25", + "r2_k5_s22_e6_i24_o40_se0.25", + "r3_k3_s22_e6_i40_o80_se0.25", + "r3_k5_s11_e6_i80_o112_se0.25", + "r4_k5_s22_e6_i112_o192_se0.25", + "r1_k3_s11_e6_i192_o320_se0.25", +] + +efficientnet_params = { + # Coefficients: width,depth,res,dropout + "efficientnet-b0": (1.0, 1.0, 224, 0.2), + "efficientnet-b1": (1.0, 1.1, 240, 0.2), + "efficientnet-b2": (1.1, 1.2, 260, 0.3), + "efficientnet-b3": (1.2, 1.4, 300, 0.3), + "efficientnet-b4": (1.4, 1.8, 380, 0.4), + "efficientnet-b5": (1.6, 2.2, 456, 0.4), + "efficientnet-b6": (1.8, 2.6, 528, 0.5), + "efficientnet-b7": (2.0, 3.1, 600, 0.5), + "efficientnet-b8": (2.2, 3.6, 672, 0.5), +} + +url_map = { + "efficientnet-b0": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b0-355c32eb.pth", + "efficientnet-b1": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b1-f1951068.pth", + "efficientnet-b2": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b2-8bb594d6.pth", + "efficientnet-b3": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b3-5fb5a3c3.pth", + "efficientnet-b4": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b4-6ed6700e.pth", + "efficientnet-b5": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b5-b6417697.pth", + "efficientnet-b6": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b6-c76e70fd.pth", + "efficientnet-b7": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b7-dcc49843.pth", +} + + +class MBConvBlock(nn.Module): + """Mobile Inverted Residual Bottleneck Block. + + Args: + block_args (namedtuple): BlockArgs, defined in utils.py. + global_params (namedtuple): GlobalParam, defined in utils.py. + image_size (tuple or list): [image_height, image_width]. + + References: + [1] https://arxiv.org/abs/1704.04861 (MobileNet v1) + [2] https://arxiv.org/abs/1801.04381 (MobileNet v2) + [3] https://arxiv.org/abs/1905.02244 (MobileNet v3) + """ + + def __init__( + self, + spatial_dims: int, + in_channels: int, + out_channels: int, + kernel_size: int, + stride: int, + image_size: List[int], + expand_ratio: float = 1.0, + se_ratio: float = 0.25, + id_skip: bool = True, + batch_norm_momentum: float = 0.99, + batch_norm_epsilon: float = 1e-3, + drop_connect_rate: float = 0.2 + ): + super().__init__() + self.spatial_dims = spatial_dims + self.in_channels = in_channels + self.out_channels = out_channels + self.id_skip = id_skip + self.stride = stride + self.expand_ratio = expand_ratio + self.has_se = (se_ratio is not None) and (0 < se_ratio <= 1) + self._drop_connect_rate = drop_connect_rate + + NdConv = Conv["conv", self.spatial_dims] + NdBatchNorm = Norm["batch", self.spatial_dims] + NdAdaptivePool = Pool["adaptiveavg", self.spatial_dims] + + # self._block_args = block_args + bn_mom = 1 - batch_norm_momentum # pytorch"s difference from tensorflow + bn_eps = batch_norm_epsilon + + # Expansion phase (Inverted Bottleneck) + inp = in_channels # number of input channels + oup = in_channels * expand_ratio # number of output channels + if expand_ratio != 1: + self._expand_conv = NdConv(in_channels=inp, out_channels=oup, kernel_size=1, bias=False) + self._expand_conv_padding = make_same_padder(self._expand_conv, image_size) + + self._bn0 = NdBatchNorm(num_features=oup, momentum=bn_mom, eps=bn_eps) + + # Depthwise convolution phase + self._depthwise_conv = NdConv( + in_channels=oup, out_channels=oup, groups=oup, # groups makes it depthwise + kernel_size=kernel_size, stride=self.stride, bias=False) + self._depthwise_conv_padding = make_same_padder(self._depthwise_conv, image_size) + self._bn1 = NdBatchNorm(num_features=oup, momentum=bn_mom, eps=bn_eps) + image_size = calculate_output_image_size(image_size, self.stride) + + # Squeeze and Excitation layer, if desired + if self.has_se: + self._se_adaptpool = NdAdaptivePool(1) + num_squeezed_channels = max(1, int(in_channels * se_ratio)) + self._se_reduce = NdConv(in_channels=oup, out_channels=num_squeezed_channels, kernel_size=1) + self._se_reduce_padding = make_same_padder(self._se_reduce, (1, 1)) + self._se_expand = NdConv(in_channels=num_squeezed_channels, out_channels=oup, kernel_size=1) + self._se_expand_padding = make_same_padder(self._se_expand, (1, 1)) + + # Pointwise convolution phase + final_oup = out_channels + self._project_conv = NdConv(in_channels=oup, out_channels=final_oup, kernel_size=1, bias=False) + self._project_conv_padding = make_same_padder(self._project_conv, image_size) + self._bn2 = NdBatchNorm(num_features=final_oup, momentum=bn_mom, eps=bn_eps) + # self._swish = MemoryEfficientSwish() + self._swish = Act['memswish']() + + def forward(self, inputs): + """MBConvBlock"s forward function. + + Args: + inputs (tensor): Input tensor. + drop_connect_rate (bool): Drop connect rate (float, between 0 and 1). + + Returns: + Output of this block after processing. + """ + + # Expansion and Depthwise Convolution + x = inputs + if self.expand_ratio != 1: + x = self._expand_conv(self._expand_conv_padding(inputs)) + x = self._bn0(x) + x = self._swish(x) + + x = self._depthwise_conv(self._depthwise_conv_padding(x)) + x = self._bn1(x) + x = self._swish(x) + + # Squeeze and Excitation + if self.has_se: + # x_squeezed = F.adaptive_avg_pool2d(x, 1) + x_squeezed = self._se_adaptpool(x) + x_squeezed = self._se_reduce(self._se_reduce_padding(x_squeezed)) + x_squeezed = self._swish(x_squeezed) + x_squeezed = self._se_expand(self._se_expand_padding(x_squeezed)) + x = torch.sigmoid(x_squeezed) * x + + # Pointwise Convolution + x = self._project_conv(self._project_conv_padding(x)) + x = self._bn2(x) + + # Skip connection and drop connect + input_filters, output_filters = self.in_channels, self.out_channels + + # stride needs to be a list + assert isinstance(self.stride, list) + is_stride_one = all([s == 1 for s in self.stride]) + + if self.id_skip and is_stride_one and input_filters == output_filters: + # The combination of skip connection and drop connect brings about stochastic depth. + if self._drop_connect_rate: + x = drop_connect(x, p=self._drop_connect_rate, training=self.training) + x = x + inputs # skip connection + return x + + def set_swish(self, memory_efficient=True): + """Sets swish function as memory efficient (for training) or standard (for export). + + Args: + memory_efficient (bool): Whether to use memory-efficient version of swish. + """ + self._swish = Act['memswish']() if memory_efficient else Act['swish'](alpha=1.0) + + +class EfficientNet(nn.Module): + """EfficientNet model. + Most easily loaded with the .from_name or .from_pretrained methods. + + Args: + blocks_args (list[namedtuple]): A list of BlockArgs to construct blocks. + global_params (namedtuple): A set of GlobalParams shared between blocks. + + References: + [1] https://arxiv.org/abs/1905.11946 (EfficientNet) + + Example: + + + import torch + >>> from monai.networks.nets import get_efficientnet_image_size, EfficientNetBN + >>> image_size = get_efficientnet_image_size("efficientnet-b0") + >>> inputs = torch.rand(1, 3, image_size, image_size) + >>> model = EfficientNetBN("efficientnet-b0") + >>> model.eval() + >>> outputs = model(inputs) + """ + + def __init__( + self, + blocks_args: List[str], + spatial_dims: int = 2, + in_channels: int = 3, + num_classes: int = 1000, + width_coefficient: float = 1.0, + depth_coefficient: float = 1.0, + dropout_rate: float = 0.2, + image_size: int = 224, + batch_norm_momentum: float = 0.99, + batch_norm_epsilon: float = 1e-3, + drop_connect_rate: float = 0.2, + depth_divisor=8, + ): + super().__init__() + + blocks_args = decode_block_list(blocks_args) + + assert isinstance(blocks_args, list), "blocks_args should be a list" + assert len(blocks_args) > 0, "block args must be greater than 0" + + # self._global_params = global_params + self._blocks_args = blocks_args + + self.spatial_dims = spatial_dims + self.num_classes = num_classes + self.in_channels = in_channels + + # Batch norm parameters + bn_mom = 1 - batch_norm_momentum + bn_eps = batch_norm_epsilon + + if isinstance(image_size, int): + image_size = [image_size] * self.spatial_dims + + # select the type of N-Dimensional layers to use + # these are based on spatial dims and selected from MONAI factories + NdConv = Conv["conv", self.spatial_dims] + NdBatchNorm = Norm["batch", self.spatial_dims] + NdAdaptivePool = Pool["adaptiveavg", self.spatial_dims] + + # Stem + stride = [2] + out_channels = round_filters(32, width_coefficient, depth_divisor) # number of output channels + self._conv_stem = NdConv(self.in_channels, out_channels, kernel_size=3, stride=stride, bias=False) + self._conv_stem_padding = make_same_padder(self._conv_stem, image_size) + self._bn0 = NdBatchNorm(num_features=out_channels, momentum=bn_mom, eps=bn_eps) + image_size = calculate_output_image_size(image_size, stride) + + # Build blocks + self._blocks = [] + num_blocks = 0 + + # Update block input and output filters based on depth multiplier. + for idx, block_args in enumerate(self._blocks_args): + block_args = block_args._replace( + input_filters=round_filters(block_args.input_filters, width_coefficient, depth_divisor), + output_filters=round_filters(block_args.output_filters, width_coefficient, depth_divisor), + num_repeat=round_repeats(block_args.num_repeat, depth_coefficient) + ) + self._blocks_args[idx] = block_args + + # calculate the total number of blocks - needed for drop_connect estimation + num_blocks += block_args.num_repeat + + idx = 0 + for block_args in self._blocks_args: + + drop_connect_rate = drop_connect_rate + if drop_connect_rate: + drop_connect_rate *= float(idx) / num_blocks # scale drop connect_rate + + # The first block needs to take care of stride and filter size increase. + self._blocks.append(MBConvBlock(self.spatial_dims, block_args.input_filters, block_args.output_filters, block_args.kernel_size, + block_args.stride, image_size, block_args.expand_ratio, block_args.se_ratio, block_args.id_skip, + batch_norm_momentum, batch_norm_epsilon, drop_connect_rate=drop_connect_rate)) + idx += 1 + + image_size = calculate_output_image_size(image_size, block_args.stride) + if block_args.num_repeat > 1: # modify block_args to keep same output size + block_args = block_args._replace(input_filters=block_args.output_filters, stride=[1]) + for _ in range(block_args.num_repeat - 1): + drop_connect_rate = drop_connect_rate + if drop_connect_rate: + drop_connect_rate *= float(idx) / num_blocks # scale drop connect_rate + self._blocks.append(MBConvBlock(self.spatial_dims, block_args.input_filters, block_args.output_filters, block_args.kernel_size, block_args.stride, image_size, block_args.expand_ratio, + block_args.se_ratio, block_args.id_skip, batch_norm_momentum, batch_norm_epsilon, + drop_connect_rate=drop_connect_rate)) + idx += 1 + self._blocks = nn.Sequential(*self._blocks) + assert len(self._blocks) == num_blocks + + # Head + head_in_channels = block_args.output_filters + out_channels = round_filters(1280, width_coefficient, depth_divisor) + self._conv_head = NdConv(head_in_channels, out_channels, kernel_size=1, bias=False) + self._conv_head_padding = make_same_padder(self._conv_head, image_size) + self._bn1 = NdBatchNorm(num_features=out_channels, momentum=bn_mom, eps=bn_eps) + + # Final linear layer + self._avg_pooling = NdAdaptivePool(1) + self._dropout = nn.Dropout(dropout_rate) + self._fc = nn.Linear(out_channels, self.num_classes) + + # swish activation to use - using memory efficient swish by default + # can be switched to normal swish using set_swish function call + self._swish = Act['memswish']() + + def set_swish(self, memory_efficient=True): + """Sets swish function as memory efficient (for training) or standard (for export). + + Args: + memory_efficient (bool): Whether to use memory-efficient version of swish. + + """ + self._swish = Act['memswish']() if memory_efficient else Act['swish'](alpha=1.0) + for block in self._blocks: + block.set_swish(memory_efficient) + + def forward(self, inputs): + """EfficientNet"s forward function. + Calls extract_features to extract features, applies final linear layer, and returns logits. + + Args: + inputs (tensor): Input tensor. + + Returns: + Output of this model after processing. + """ + # Convolution layers + # Stem + x = self._conv_stem(self._conv_stem_padding(inputs)) + x = self._swish(self._bn0(x)) + # Blocks + x = self._blocks(x) + # Head + x = self._conv_head(self._conv_head_padding(x)) + x = self._swish(self._bn1(x)) + + # Pooling and final linear layer + x = self._avg_pooling(x) + + x = x.flatten(start_dim=1) + x = self._dropout(x) + x = self._fc(x) + return x + + def _initialize_weight(self): + # weight init as per Tensorflow Official impl + # https://github.com/tensorflow/tpu/blob/master/models/official/efficientnet/efficientnet_model.py#L61 + # code based on: https://github.com/rwightman/gen-efficientnet-pytorch/blob/master/geffnet/efficientnet_builder.py + for n, m in self.named_modules(): + if isinstance(m, (nn.Conv1d, nn.Conv2d, nn.Conv3d)): + fan_out = math.prod(m.kernel_size) * m.out_channels + m.weight.data.normal_(0, math.sqrt(2.0 / fan_out)) + if m.bias is not None: + m.bias.data.zero_() + elif isinstance(m, (nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d)): + m.weight.data.fill_(1.0) + m.bias.data.zero_() + elif isinstance(m, nn.Linear): + fan_out = m.weight.size(0) # fan-out + fan_in = 0 + init_range = 1.0 / math.sqrt(fan_in + fan_out) + m.weight.data.uniform_(-init_range, init_range) + m.bias.data.zero_() + + +class EfficientNetBN(EfficientNet): + + # model_name mandatory as there is is EfficientNetBN itself, it needs the N \in [0, 1, 2, 3, 4, 5, 6, 7, 8] to be a model + def __init__(self, model_name, pretrained=True, progress=True, spatial_dims=2, in_channels=3, num_classes=1000): + block_args = BLOCK_ARGS + assert model_name in VALID_MODELS, "model_name should be one of: " + ", ".join(VALID_MODELS) + + wc, dc, isize, dr = efficientnet_params[model_name] + model = super(EfficientNetBN, self).__init__(block_args, spatial_dims=spatial_dims, in_channels=in_channels, num_classes=num_classes, + width_coefficient=wc, depth_coefficient=dc, dropout_rate=dr, image_size=isize) + + is_default_model = (spatial_dims == 2) and (in_channels == 3) + pretrained = pretrained and model_name in url_map + + loadable_from_file = pretrained and is_default_model + + if loadable_from_file: + # skip loading fc layers for transfer learning applications + load_fc = (num_classes == 1000) + model_url = url_map[model_name] + + # only pretrained for when `spatial_dims` is 2 + _load_state_dict(self, model_url, progress, load_fc) + else: + print("Skipping loading pretrained weights for non-default {}, pretrained={}, is_default_model={}".format( + model_name, pretrained, is_default_model)) + print('Initializing weights for {}'.format(model_name)) + self._initialize_weight() + + +def _load_state_dict(model: nn.Module, model_url: str, progress: bool, load_fc: bool) -> bool: + state_dict = model_zoo.load_url(model_url, progress=progress) + if load_fc: + ret = model.load_state_dict(state_dict, strict=False) + assert not ret.missing_keys, "Missing keys when loading pretrained weights: {}".format(ret.missing_keys) + else: + state_dict.pop("_fc.weight") + state_dict.pop("_fc.bias") + ret = model.load_state_dict(state_dict, strict=False) + assert set(ret.missing_keys) == set( + ["_fc.weight", "_fc.bias"]), "Missing keys when loading pretrained weights: {}".format(ret.missing_keys) + + assert not ret.unexpected_keys, "Missing keys when loading pretrained weights: {}".format(ret.unexpected_keys) + + +def get_efficientnet_image_size(model_name): + """Get the input image size for a given efficientnet model. + """ + assert model_name in VALID_MODELS, "model_name should be one of: " + ", ".join(VALID_MODELS) + _, _, res, _ = efficientnet_params[model_name] + return res + + +def round_filters(filters, width_coefficient=None, depth_divisor=None): + """Calculate and round number of filters based on width multiplier. + Use width_coefficient, depth_divisor of global_params. + + Args: + filters (int): Filters number to be calculated. + global_params (namedtuple): Global params of the model. + + Returns: + new_filters: New filters number after calculating. + """ + multiplier = width_coefficient + if not multiplier: + return filters + divisor = depth_divisor + filters *= multiplier + + # follow the formula transferred from official TensorFlow implementation + new_filters = max(divisor, int(filters + divisor / 2) // divisor * divisor) + if new_filters < 0.9 * filters: # prevent rounding by more than 10% + new_filters += divisor + return int(new_filters) + + +def round_repeats(repeats, depth_coefficient=None): + """Calculate module"s repeat number of a block based on depth multiplier. + Use depth_coefficient of global_params. + + Args: + repeats (int): num_repeat to be calculated. + global_params (namedtuple): Global params of the model. + + Returns: + new repeat: New repeat number after calculating. + """ + multiplier = depth_coefficient + if not multiplier: + return repeats + # follow the formula transferred from official TensorFlow implementation + return int(math.ceil(multiplier * repeats)) + + +def drop_connect(inputs, p, training): + """Drop connect. + + Args: + input (tensor: BCWH): Input of this structure. + p (float: 0.0~1.0): Probability of drop connection. + training (bool): The running mode. + + Returns: + output: Output after drop connection. + """ + assert 0 <= p <= 1, "p must be in range of [0,1]" + + if not training: + return inputs + + batch_size = inputs.shape[0] + keep_prob = 1 - p + + # generate binary_tensor mask according to probability (p for 0, 1-p for 1) + random_tensor = keep_prob + random_tensor += torch.rand([batch_size, 1, 1, 1], + dtype=inputs.dtype, device=inputs.device) + binary_tensor = torch.floor(random_tensor) + + output = inputs / keep_prob * binary_tensor + return output + + +def calculate_output_image_size(input_image_size, stride): + """Calculates the output image size when using Conv2dSamePadding with a stride. + Necessary for static padding. Thanks to mannatsingh for pointing this out. + + Args: + input_image_size (int, tuple or list): Size of input image. + stride (int, tuple or list): Conv2d operation"s stride. + + Returns: + output_image_size: A list [H,W]. + """ + if input_image_size is None: + return None + # image_height, image_width = input_image_size + # stride = stride if isinstance(stride, int) else stride[0] + # image_height = int(math.ceil(image_height / stride)) + # image_width = int(math.ceil(image_width / stride)) + # return [image_height, image_width] + num_dims = len(input_image_size) + assert isinstance(stride, list) + + if len(stride) != len(input_image_size): + stride = stride * num_dims + + # image_height, image_width = input_image_size + # stride = stride if isinstance(stride, int) else stride[0] + # image_height = int(math.ceil(image_height / stride)) + # image_width = int(math.ceil(image_width / stride)) + # return [image_height, image_width] + + return [int(math.ceil(im_sz / st)) for im_sz, st in zip(input_image_size, stride)] + + +def get_same_padding_conv2d(image_size, kernel_size, dilation, stride): + num_dims = len(kernel_size) + + # additional checks to populate dilation and stride (in case they are integers or single entry list) + if isinstance(stride, int): + stride = (stride,) * num_dims + elif len(stride) == 1: + stride = stride * num_dims + + if isinstance(dilation, int): + dilation = (dilation,) * num_dims + elif len(dilation) == 1: + dilation = dilation * num_dims + + # _kernel_size = kernel_size + _pad_size = [max((math.ceil(_i_s / _s) - 1) * _s + (_k_s - 1) * _d + 1 - _i_s, 0) + for _i_s, _k_s, _d, _s in zip(image_size, kernel_size, dilation, stride)] + _paddings = [(_p // 2, _p - _p // 2) for _p in _pad_size] + + # unroll list of tuples to tuples, + # reversed as constandpadnd expects paddings starting with last dimenion + _paddings = [outer for inner in reversed( + _paddings) for outer in inner] + return _paddings + + +def make_same_padder(conv_op, image_size): + padding = get_same_padding_conv2d(image_size, conv_op.kernel_size, conv_op.dilation, conv_op.stride) + padder = Pad["constantpad", len(padding) // 2] + if sum(padding) > 0: + return padder(padding=padding, value=0) + else: + return nn.Identity() + + +def decode_block_list(string_list): + """Decode a list of string notations to specify blocks inside the network. + + Args: + string_list (list[str]): A list of strings, each string is a notation of block. + + Returns: + blocks_args: A list of BlockArgs namedtuples of block args. + """ + # Parameters for an individual model block + BlockArgs = collections.namedtuple("BlockArgs", [ + "num_repeat", "kernel_size", "stride", "expand_ratio", + "input_filters", "output_filters", "se_ratio", "id_skip"]) + BlockArgs.__new__.__defaults__ = (None,) * len(BlockArgs._fields) + + def _decode_block_string(block_string): + """Get a block through a string notation of arguments. + + Args: + block_string (str): A string notation of arguments. + Examples: "r1_k3_s11_e1_i32_o16_se0.25_noskip". + + Returns: + BlockArgs: The namedtuple defined at the top of this file. + """ + assert isinstance(block_string, str) + + ops = block_string.split("_") + options = {} + for op in ops: + splits = re.split(r"(\d.*)", op) + if len(splits) >= 2: + key, value = splits[:2] + options[key] = value + + # Check stride + assert (("s" in options and len(options["s"]) == 1) or + (len(options["s"]) == 2 and options["s"][0] == options["s"][1])) + + return BlockArgs( + num_repeat=int(options["r"]), + kernel_size=int(options["k"]), + stride=[int(options["s"][0])], + expand_ratio=int(options["e"]), + input_filters=int(options["i"]), + output_filters=int(options["o"]), + se_ratio=float(options["se"]) if "se" in options else None, + id_skip=("noskip" not in block_string)) + + assert isinstance(string_list, list) + blocks_args = [] + for b_s in string_list: + blocks_args.append(_decode_block_string(b_s)) + return blocks_args From 12bc476cd6e70897bfb46a323921a5258c923939 Mon Sep 17 00:00:00 2001 From: masadcv Date: Sun, 4 Apr 2021 02:41:19 +0100 Subject: [PATCH 02/16] fixing flake8 and further refactoring Signed-off-by: masadcv --- monai/networks/blocks/activation.py | 7 +- monai/networks/nets/efficientnet.py | 283 ++++++++++++++++------------ 2 files changed, 166 insertions(+), 124 deletions(-) mode change 100755 => 100644 monai/networks/nets/efficientnet.py diff --git a/monai/networks/blocks/activation.py b/monai/networks/blocks/activation.py index b422783f00..f751dfd5b3 100644 --- a/monai/networks/blocks/activation.py +++ b/monai/networks/blocks/activation.py @@ -44,12 +44,13 @@ def forward(self, input: torch.Tensor) -> torch.Tensor: class SwishImplementation(torch.autograd.Function): - r"""Memory efficient implementation for training - Follows recommendation from: + r"""Memory efficient implementation for training + Follows recommendation from: https://github.com/lukemelas/EfficientNet-PyTorch/issues/18#issuecomment-511677853 Results in ~ 30% memory saving during training as compared to Swish() """ + @staticmethod def forward(ctx: torch.autograd.Function, input: torch.Tensor): result = input * torch.sigmoid(input) @@ -71,7 +72,7 @@ class MemoryEfficientSwish(nn.Module): Citation: Searching for Activation Functions, Ramachandran et al., 2017, https://arxiv.org/abs/1710.05941. - Memory efficient implementation for training following recommendation from: + Memory efficient implementation for training following recommendation from: https://github.com/lukemelas/EfficientNet-PyTorch/issues/18#issuecomment-511677853 Results in ~ 30% memory saving during training as compared to Swish() diff --git a/monai/networks/nets/efficientnet.py b/monai/networks/nets/efficientnet.py old mode 100755 new mode 100644 index ce1c1a02d9..db0c6b2463 --- a/monai/networks/nets/efficientnet.py +++ b/monai/networks/nets/efficientnet.py @@ -1,39 +1,29 @@ +# 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. + """Implementation based on: https://github.com/lukemelas/EfficientNet-PyTorch -With significant modifications to refactor/rewrite the code for MONAI +#With significant modifications to refactor/rewrite the code for MONAI """ import collections -import re import math +import re from typing import List import torch from torch import nn -from torch.nn import functional as F from torch.utils import model_zoo from monai.networks.layers.factories import Act, Conv, Norm, Pad, Pool - -__all__ = [ - "get_efficientnet_image_size", - "EfficientNetBN" -] - -VALID_MODELS = ( - "efficientnet-b0", "efficientnet-b1", "efficientnet-b2", "efficientnet-b3", - "efficientnet-b4", "efficientnet-b5", "efficientnet-b6", "efficientnet-b7", - "efficientnet-b8", -) - -BLOCK_ARGS = [ - "r1_k3_s11_e1_i32_o16_se0.25", - "r2_k3_s22_e6_i16_o24_se0.25", - "r2_k5_s22_e6_i24_o40_se0.25", - "r3_k3_s22_e6_i40_o80_se0.25", - "r3_k5_s11_e6_i80_o112_se0.25", - "r4_k5_s22_e6_i112_o192_se0.25", - "r1_k3_s11_e6_i192_o320_se0.25", -] +__all__ = ["EfficientNetBN", "get_efficientnet_image_size"] efficientnet_params = { # Coefficients: width,depth,res,dropout @@ -45,7 +35,6 @@ "efficientnet-b5": (1.6, 2.2, 456, 0.4), "efficientnet-b6": (1.8, 2.6, 528, 0.5), "efficientnet-b7": (2.0, 3.1, 600, 0.5), - "efficientnet-b8": (2.2, 3.6, 672, 0.5), } url_map = { @@ -87,7 +76,7 @@ def __init__( id_skip: bool = True, batch_norm_momentum: float = 0.99, batch_norm_epsilon: float = 1e-3, - drop_connect_rate: float = 0.2 + drop_connect_rate: float = 0.2, ): super().__init__() self.spatial_dims = spatial_dims @@ -99,11 +88,10 @@ def __init__( self.has_se = (se_ratio is not None) and (0 < se_ratio <= 1) self._drop_connect_rate = drop_connect_rate - NdConv = Conv["conv", self.spatial_dims] - NdBatchNorm = Norm["batch", self.spatial_dims] - NdAdaptivePool = Pool["adaptiveavg", self.spatial_dims] + nd_conv = Conv["conv", self.spatial_dims] + nd_batchnorm = Norm["batch", self.spatial_dims] + nd_adaptivepool = Pool["adaptiveavg", self.spatial_dims] - # self._block_args = block_args bn_mom = 1 - batch_norm_momentum # pytorch"s difference from tensorflow bn_eps = batch_norm_epsilon @@ -111,35 +99,40 @@ def __init__( inp = in_channels # number of input channels oup = in_channels * expand_ratio # number of output channels if expand_ratio != 1: - self._expand_conv = NdConv(in_channels=inp, out_channels=oup, kernel_size=1, bias=False) - self._expand_conv_padding = make_same_padder(self._expand_conv, image_size) + self._expand_conv = nd_conv(in_channels=inp, out_channels=oup, kernel_size=1, bias=False) + self._expand_conv_padding = _make_same_padder(self._expand_conv, image_size) - self._bn0 = NdBatchNorm(num_features=oup, momentum=bn_mom, eps=bn_eps) + self._bn0 = nd_batchnorm(num_features=oup, momentum=bn_mom, eps=bn_eps) # Depthwise convolution phase - self._depthwise_conv = NdConv( - in_channels=oup, out_channels=oup, groups=oup, # groups makes it depthwise - kernel_size=kernel_size, stride=self.stride, bias=False) - self._depthwise_conv_padding = make_same_padder(self._depthwise_conv, image_size) - self._bn1 = NdBatchNorm(num_features=oup, momentum=bn_mom, eps=bn_eps) - image_size = calculate_output_image_size(image_size, self.stride) + self._depthwise_conv = nd_conv( + in_channels=oup, + out_channels=oup, + groups=oup, # groups makes it depthwise + kernel_size=kernel_size, + stride=self.stride, + bias=False, + ) + self._depthwise_conv_padding = _make_same_padder(self._depthwise_conv, image_size) + self._bn1 = nd_batchnorm(num_features=oup, momentum=bn_mom, eps=bn_eps) + image_size = _calculate_output_image_size(image_size, self.stride) # Squeeze and Excitation layer, if desired if self.has_se: - self._se_adaptpool = NdAdaptivePool(1) + self._se_adaptpool = nd_adaptivepool(1) num_squeezed_channels = max(1, int(in_channels * se_ratio)) - self._se_reduce = NdConv(in_channels=oup, out_channels=num_squeezed_channels, kernel_size=1) - self._se_reduce_padding = make_same_padder(self._se_reduce, (1, 1)) - self._se_expand = NdConv(in_channels=num_squeezed_channels, out_channels=oup, kernel_size=1) - self._se_expand_padding = make_same_padder(self._se_expand, (1, 1)) + self._se_reduce = nd_conv(in_channels=oup, out_channels=num_squeezed_channels, kernel_size=1) + self._se_reduce_padding = _make_same_padder(self._se_reduce, (1, 1)) + self._se_expand = nd_conv(in_channels=num_squeezed_channels, out_channels=oup, kernel_size=1) + self._se_expand_padding = _make_same_padder(self._se_expand, (1, 1)) # Pointwise convolution phase final_oup = out_channels - self._project_conv = NdConv(in_channels=oup, out_channels=final_oup, kernel_size=1, bias=False) - self._project_conv_padding = make_same_padder(self._project_conv, image_size) - self._bn2 = NdBatchNorm(num_features=final_oup, momentum=bn_mom, eps=bn_eps) + self._project_conv = nd_conv(in_channels=oup, out_channels=final_oup, kernel_size=1, bias=False) + self._project_conv_padding = _make_same_padder(self._project_conv, image_size) + self._bn2 = nd_batchnorm(num_features=final_oup, momentum=bn_mom, eps=bn_eps) # self._swish = MemoryEfficientSwish() - self._swish = Act['memswish']() + self._swish = Act["memswish"]() def forward(self, inputs): """MBConvBlock"s forward function. @@ -165,7 +158,6 @@ def forward(self, inputs): # Squeeze and Excitation if self.has_se: - # x_squeezed = F.adaptive_avg_pool2d(x, 1) x_squeezed = self._se_adaptpool(x) x_squeezed = self._se_reduce(self._se_reduce_padding(x_squeezed)) x_squeezed = self._swish(x_squeezed) @@ -196,7 +188,7 @@ def set_swish(self, memory_efficient=True): Args: memory_efficient (bool): Whether to use memory-efficient version of swish. """ - self._swish = Act['memswish']() if memory_efficient else Act['swish'](alpha=1.0) + self._swish = Act["memswish"]() if memory_efficient else Act["swish"](alpha=1.0) class EfficientNet(nn.Module): @@ -239,12 +231,11 @@ def __init__( ): super().__init__() - blocks_args = decode_block_list(blocks_args) + blocks_args = _decode_block_list(blocks_args) assert isinstance(blocks_args, list), "blocks_args should be a list" assert len(blocks_args) > 0, "block args must be greater than 0" - # self._global_params = global_params self._blocks_args = blocks_args self.spatial_dims = spatial_dims @@ -260,17 +251,17 @@ def __init__( # select the type of N-Dimensional layers to use # these are based on spatial dims and selected from MONAI factories - NdConv = Conv["conv", self.spatial_dims] - NdBatchNorm = Norm["batch", self.spatial_dims] - NdAdaptivePool = Pool["adaptiveavg", self.spatial_dims] + nd_conv = Conv["conv", self.spatial_dims] + nd_batchnorm = Norm["batch", self.spatial_dims] + nd_adaptivepool = Pool["adaptiveavg", self.spatial_dims] # Stem stride = [2] - out_channels = round_filters(32, width_coefficient, depth_divisor) # number of output channels - self._conv_stem = NdConv(self.in_channels, out_channels, kernel_size=3, stride=stride, bias=False) - self._conv_stem_padding = make_same_padder(self._conv_stem, image_size) - self._bn0 = NdBatchNorm(num_features=out_channels, momentum=bn_mom, eps=bn_eps) - image_size = calculate_output_image_size(image_size, stride) + out_channels = _round_filters(32, width_coefficient, depth_divisor) # number of output channels + self._conv_stem = nd_conv(self.in_channels, out_channels, kernel_size=3, stride=stride, bias=False) + self._conv_stem_padding = _make_same_padder(self._conv_stem, image_size) + self._bn0 = nd_batchnorm(num_features=out_channels, momentum=bn_mom, eps=bn_eps) + image_size = _calculate_output_image_size(image_size, stride) # Build blocks self._blocks = [] @@ -279,9 +270,9 @@ def __init__( # Update block input and output filters based on depth multiplier. for idx, block_args in enumerate(self._blocks_args): block_args = block_args._replace( - input_filters=round_filters(block_args.input_filters, width_coefficient, depth_divisor), - output_filters=round_filters(block_args.output_filters, width_coefficient, depth_divisor), - num_repeat=round_repeats(block_args.num_repeat, depth_coefficient) + input_filters=_round_filters(block_args.input_filters, width_coefficient, depth_divisor), + output_filters=_round_filters(block_args.output_filters, width_coefficient, depth_divisor), + num_repeat=_round_repeats(block_args.num_repeat, depth_coefficient), ) self._blocks_args[idx] = block_args @@ -296,40 +287,66 @@ def __init__( drop_connect_rate *= float(idx) / num_blocks # scale drop connect_rate # The first block needs to take care of stride and filter size increase. - self._blocks.append(MBConvBlock(self.spatial_dims, block_args.input_filters, block_args.output_filters, block_args.kernel_size, - block_args.stride, image_size, block_args.expand_ratio, block_args.se_ratio, block_args.id_skip, - batch_norm_momentum, batch_norm_epsilon, drop_connect_rate=drop_connect_rate)) + self._blocks.append( + MBConvBlock( + self.spatial_dims, + block_args.input_filters, + block_args.output_filters, + block_args.kernel_size, + block_args.stride, + image_size, + block_args.expand_ratio, + block_args.se_ratio, + block_args.id_skip, + batch_norm_momentum, + batch_norm_epsilon, + drop_connect_rate=drop_connect_rate, + ) + ) idx += 1 - image_size = calculate_output_image_size(image_size, block_args.stride) + image_size = _calculate_output_image_size(image_size, block_args.stride) if block_args.num_repeat > 1: # modify block_args to keep same output size block_args = block_args._replace(input_filters=block_args.output_filters, stride=[1]) for _ in range(block_args.num_repeat - 1): drop_connect_rate = drop_connect_rate if drop_connect_rate: drop_connect_rate *= float(idx) / num_blocks # scale drop connect_rate - self._blocks.append(MBConvBlock(self.spatial_dims, block_args.input_filters, block_args.output_filters, block_args.kernel_size, block_args.stride, image_size, block_args.expand_ratio, - block_args.se_ratio, block_args.id_skip, batch_norm_momentum, batch_norm_epsilon, - drop_connect_rate=drop_connect_rate)) + self._blocks.append( + MBConvBlock( + self.spatial_dims, + block_args.input_filters, + block_args.output_filters, + block_args.kernel_size, + block_args.stride, + image_size, + block_args.expand_ratio, + block_args.se_ratio, + block_args.id_skip, + batch_norm_momentum, + batch_norm_epsilon, + drop_connect_rate=drop_connect_rate, + ) + ) idx += 1 self._blocks = nn.Sequential(*self._blocks) assert len(self._blocks) == num_blocks # Head head_in_channels = block_args.output_filters - out_channels = round_filters(1280, width_coefficient, depth_divisor) - self._conv_head = NdConv(head_in_channels, out_channels, kernel_size=1, bias=False) - self._conv_head_padding = make_same_padder(self._conv_head, image_size) - self._bn1 = NdBatchNorm(num_features=out_channels, momentum=bn_mom, eps=bn_eps) + out_channels = _round_filters(1280, width_coefficient, depth_divisor) + self._conv_head = nd_conv(head_in_channels, out_channels, kernel_size=1, bias=False) + self._conv_head_padding = _make_same_padder(self._conv_head, image_size) + self._bn1 = nd_batchnorm(num_features=out_channels, momentum=bn_mom, eps=bn_eps) # Final linear layer - self._avg_pooling = NdAdaptivePool(1) + self._avg_pooling = nd_adaptivepool(1) self._dropout = nn.Dropout(dropout_rate) self._fc = nn.Linear(out_channels, self.num_classes) # swish activation to use - using memory efficient swish by default # can be switched to normal swish using set_swish function call - self._swish = Act['memswish']() + self._swish = Act["memswish"]() def set_swish(self, memory_efficient=True): """Sets swish function as memory efficient (for training) or standard (for export). @@ -338,7 +355,7 @@ def set_swish(self, memory_efficient=True): memory_efficient (bool): Whether to use memory-efficient version of swish. """ - self._swish = Act['memswish']() if memory_efficient else Act['swish'](alpha=1.0) + self._swish = Act["memswish"]() if memory_efficient else Act["swish"](alpha=1.0) for block in self._blocks: block.set_swish(memory_efficient) @@ -374,7 +391,7 @@ def _initialize_weight(self): # weight init as per Tensorflow Official impl # https://github.com/tensorflow/tpu/blob/master/models/official/efficientnet/efficientnet_model.py#L61 # code based on: https://github.com/rwightman/gen-efficientnet-pytorch/blob/master/geffnet/efficientnet_builder.py - for n, m in self.named_modules(): + for _, m in self.named_modules(): if isinstance(m, (nn.Conv1d, nn.Conv2d, nn.Conv3d)): fan_out = math.prod(m.kernel_size) * m.out_channels m.weight.data.normal_(0, math.sqrt(2.0 / fan_out)) @@ -392,15 +409,32 @@ def _initialize_weight(self): class EfficientNetBN(EfficientNet): - # model_name mandatory as there is is EfficientNetBN itself, it needs the N \in [0, 1, 2, 3, 4, 5, 6, 7, 8] to be a model def __init__(self, model_name, pretrained=True, progress=True, spatial_dims=2, in_channels=3, num_classes=1000): - block_args = BLOCK_ARGS - assert model_name in VALID_MODELS, "model_name should be one of: " + ", ".join(VALID_MODELS) + block_args = [ + "r1_k3_s11_e1_i32_o16_se0.25", + "r2_k3_s22_e6_i16_o24_se0.25", + "r2_k5_s22_e6_i24_o40_se0.25", + "r3_k3_s22_e6_i40_o80_se0.25", + "r3_k5_s11_e6_i80_o112_se0.25", + "r4_k5_s22_e6_i112_o192_se0.25", + "r1_k3_s11_e6_i192_o320_se0.25", + ] + assert model_name in efficientnet_params.keys(), "model_name should be one of {} ".format( + ", ".join(efficientnet_params.keys()) + ) wc, dc, isize, dr = efficientnet_params[model_name] - model = super(EfficientNetBN, self).__init__(block_args, spatial_dims=spatial_dims, in_channels=in_channels, num_classes=num_classes, - width_coefficient=wc, depth_coefficient=dc, dropout_rate=dr, image_size=isize) + model = super(EfficientNetBN, self).__init__( + block_args, + spatial_dims=spatial_dims, + in_channels=in_channels, + num_classes=num_classes, + width_coefficient=wc, + depth_coefficient=dc, + dropout_rate=dr, + image_size=isize, + ) is_default_model = (spatial_dims == 2) and (in_channels == 3) pretrained = pretrained and model_name in url_map @@ -409,15 +443,18 @@ def __init__(self, model_name, pretrained=True, progress=True, spatial_dims=2, i if loadable_from_file: # skip loading fc layers for transfer learning applications - load_fc = (num_classes == 1000) + load_fc = num_classes == 1000 model_url = url_map[model_name] # only pretrained for when `spatial_dims` is 2 _load_state_dict(self, model_url, progress, load_fc) else: - print("Skipping loading pretrained weights for non-default {}, pretrained={}, is_default_model={}".format( - model_name, pretrained, is_default_model)) - print('Initializing weights for {}'.format(model_name)) + print( + "Skipping loading pretrained weights for non-default {}, pretrained={}, is_default_model={}".format( + model_name, pretrained, is_default_model + ) + ) + print("Initializing weights for {}".format(model_name)) self._initialize_weight() @@ -431,20 +468,22 @@ def _load_state_dict(model: nn.Module, model_url: str, progress: bool, load_fc: state_dict.pop("_fc.bias") ret = model.load_state_dict(state_dict, strict=False) assert set(ret.missing_keys) == set( - ["_fc.weight", "_fc.bias"]), "Missing keys when loading pretrained weights: {}".format(ret.missing_keys) + "_fc.weight", "_fc.bias" + ), "Missing keys when loading pretrained weights: {}".format(ret.missing_keys) assert not ret.unexpected_keys, "Missing keys when loading pretrained weights: {}".format(ret.unexpected_keys) def get_efficientnet_image_size(model_name): - """Get the input image size for a given efficientnet model. - """ - assert model_name in VALID_MODELS, "model_name should be one of: " + ", ".join(VALID_MODELS) + """Get the input image size for a given efficientnet model.""" + assert model_name in efficientnet_params.keys(), "model_name should be one of {} ".format( + ", ".join(efficientnet_params.keys()) + ) _, _, res, _ = efficientnet_params[model_name] return res -def round_filters(filters, width_coefficient=None, depth_divisor=None): +def _round_filters(filters, width_coefficient=None, depth_divisor=None): """Calculate and round number of filters based on width multiplier. Use width_coefficient, depth_divisor of global_params. @@ -468,7 +507,7 @@ def round_filters(filters, width_coefficient=None, depth_divisor=None): return int(new_filters) -def round_repeats(repeats, depth_coefficient=None): +def _round_repeats(repeats, depth_coefficient=None): """Calculate module"s repeat number of a block based on depth multiplier. Use depth_coefficient of global_params. @@ -507,15 +546,14 @@ def drop_connect(inputs, p, training): # generate binary_tensor mask according to probability (p for 0, 1-p for 1) random_tensor = keep_prob - random_tensor += torch.rand([batch_size, 1, 1, 1], - dtype=inputs.dtype, device=inputs.device) + random_tensor += torch.rand([batch_size, 1, 1, 1], dtype=inputs.dtype, device=inputs.device) binary_tensor = torch.floor(random_tensor) output = inputs / keep_prob * binary_tensor return output -def calculate_output_image_size(input_image_size, stride): +def _calculate_output_image_size(input_image_size, stride): """Calculates the output image size when using Conv2dSamePadding with a stride. Necessary for static padding. Thanks to mannatsingh for pointing this out. @@ -528,27 +566,17 @@ def calculate_output_image_size(input_image_size, stride): """ if input_image_size is None: return None - # image_height, image_width = input_image_size - # stride = stride if isinstance(stride, int) else stride[0] - # image_height = int(math.ceil(image_height / stride)) - # image_width = int(math.ceil(image_width / stride)) - # return [image_height, image_width] + num_dims = len(input_image_size) assert isinstance(stride, list) if len(stride) != len(input_image_size): stride = stride * num_dims - # image_height, image_width = input_image_size - # stride = stride if isinstance(stride, int) else stride[0] - # image_height = int(math.ceil(image_height / stride)) - # image_width = int(math.ceil(image_width / stride)) - # return [image_height, image_width] - return [int(math.ceil(im_sz / st)) for im_sz, st in zip(input_image_size, stride)] -def get_same_padding_conv2d(image_size, kernel_size, dilation, stride): +def _get_same_padding_conv2d(image_size, kernel_size, dilation, stride): num_dims = len(kernel_size) # additional checks to populate dilation and stride (in case they are integers or single entry list) @@ -563,19 +591,20 @@ def get_same_padding_conv2d(image_size, kernel_size, dilation, stride): dilation = dilation * num_dims # _kernel_size = kernel_size - _pad_size = [max((math.ceil(_i_s / _s) - 1) * _s + (_k_s - 1) * _d + 1 - _i_s, 0) - for _i_s, _k_s, _d, _s in zip(image_size, kernel_size, dilation, stride)] + _pad_size = [ + max((math.ceil(_i_s / _s) - 1) * _s + (_k_s - 1) * _d + 1 - _i_s, 0) + for _i_s, _k_s, _d, _s in zip(image_size, kernel_size, dilation, stride) + ] _paddings = [(_p // 2, _p - _p // 2) for _p in _pad_size] # unroll list of tuples to tuples, # reversed as constandpadnd expects paddings starting with last dimenion - _paddings = [outer for inner in reversed( - _paddings) for outer in inner] + _paddings = [outer for inner in reversed(_paddings) for outer in inner] return _paddings -def make_same_padder(conv_op, image_size): - padding = get_same_padding_conv2d(image_size, conv_op.kernel_size, conv_op.dilation, conv_op.stride) +def _make_same_padder(conv_op, image_size): + padding = _get_same_padding_conv2d(image_size, conv_op.kernel_size, conv_op.dilation, conv_op.stride) padder = Pad["constantpad", len(padding) // 2] if sum(padding) > 0: return padder(padding=padding, value=0) @@ -583,7 +612,7 @@ def make_same_padder(conv_op, image_size): return nn.Identity() -def decode_block_list(string_list): +def _decode_block_list(string_list): """Decode a list of string notations to specify blocks inside the network. Args: @@ -593,9 +622,19 @@ def decode_block_list(string_list): blocks_args: A list of BlockArgs namedtuples of block args. """ # Parameters for an individual model block - BlockArgs = collections.namedtuple("BlockArgs", [ - "num_repeat", "kernel_size", "stride", "expand_ratio", - "input_filters", "output_filters", "se_ratio", "id_skip"]) + BlockArgs = collections.namedtuple( + "BlockArgs", + [ + "num_repeat", + "kernel_size", + "stride", + "expand_ratio", + "input_filters", + "output_filters", + "se_ratio", + "id_skip", + ], + ) BlockArgs.__new__.__defaults__ = (None,) * len(BlockArgs._fields) def _decode_block_string(block_string): @@ -619,8 +658,9 @@ def _decode_block_string(block_string): options[key] = value # Check stride - assert (("s" in options and len(options["s"]) == 1) or - (len(options["s"]) == 2 and options["s"][0] == options["s"][1])) + assert ("s" in options and len(options["s"]) == 1) or ( + len(options["s"]) == 2 and options["s"][0] == options["s"][1] + ) return BlockArgs( num_repeat=int(options["r"]), @@ -630,7 +670,8 @@ def _decode_block_string(block_string): input_filters=int(options["i"]), output_filters=int(options["o"]), se_ratio=float(options["se"]) if "se" in options else None, - id_skip=("noskip" not in block_string)) + id_skip=("noskip" not in block_string), + ) assert isinstance(string_list, list) blocks_args = [] From 9cda5e24fc426e5fddaa5ef790910e6ed7d23ead Mon Sep 17 00:00:00 2001 From: masadcv Date: Mon, 5 Apr 2021 21:07:26 +0100 Subject: [PATCH 03/16] adding unittests for efficiennet Signed-off-by: masadcv --- monai/networks/blocks/activation.py | 4 +- monai/networks/nets/efficientnet.py | 126 ++++++++------- tests/test_efficientnet.py | 228 ++++++++++++++++++++++++++++ tests/testing_data/kitty_test.jpg | Bin 0 -> 69360 bytes 4 files changed, 305 insertions(+), 53 deletions(-) create mode 100644 tests/test_efficientnet.py create mode 100644 tests/testing_data/kitty_test.jpg diff --git a/monai/networks/blocks/activation.py b/monai/networks/blocks/activation.py index f751dfd5b3..5f62900c3b 100644 --- a/monai/networks/blocks/activation.py +++ b/monai/networks/blocks/activation.py @@ -52,13 +52,13 @@ class SwishImplementation(torch.autograd.Function): """ @staticmethod - def forward(ctx: torch.autograd.Function, input: torch.Tensor): + def forward(ctx, input): result = input * torch.sigmoid(input) ctx.save_for_backward(input) return result @staticmethod - def backward(ctx: torch.autograd.Function, grad_output: torch.Tensor): + def backward(ctx, grad_output): input = ctx.saved_tensors[0] sigmoid_input = torch.sigmoid(input) return grad_output * (sigmoid_input * (1 + input * (1 - sigmoid_input))) diff --git a/monai/networks/nets/efficientnet.py b/monai/networks/nets/efficientnet.py index db0c6b2463..617743fcda 100644 --- a/monai/networks/nets/efficientnet.py +++ b/monai/networks/nets/efficientnet.py @@ -26,7 +26,7 @@ __all__ = ["EfficientNetBN", "get_efficientnet_image_size"] efficientnet_params = { - # Coefficients: width,depth,res,dropout + # Coefficients: width_mult, depth_mult, image_size, dropout_rate "efficientnet-b0": (1.0, 1.0, 224, 0.2), "efficientnet-b1": (1.0, 1.1, 240, 0.2), "efficientnet-b2": (1.1, 1.2, 260, 0.3), @@ -69,15 +69,15 @@ def __init__( in_channels: int, out_channels: int, kernel_size: int, - stride: int, + stride: List[int], image_size: List[int], - expand_ratio: float = 1.0, - se_ratio: float = 0.25, + expand_ratio: int, + se_ratio: float, id_skip: bool = True, batch_norm_momentum: float = 0.99, batch_norm_epsilon: float = 1e-3, drop_connect_rate: float = 0.2, - ): + ) -> None: super().__init__() self.spatial_dims = spatial_dims self.in_channels = in_channels @@ -98,11 +98,19 @@ def __init__( # Expansion phase (Inverted Bottleneck) inp = in_channels # number of input channels oup = in_channels * expand_ratio # number of output channels - if expand_ratio != 1: + if self.expand_ratio != 1: self._expand_conv = nd_conv(in_channels=inp, out_channels=oup, kernel_size=1, bias=False) self._expand_conv_padding = _make_same_padder(self._expand_conv, image_size) self._bn0 = nd_batchnorm(num_features=oup, momentum=bn_mom, eps=bn_eps) + else: + # need to have the following to fix JIT error: + # Module 'MBConvBlock' has no attribute '_expand_conv' + + # FIXME: find a better way to bypass JIT error + self._expand_conv = nn.Identity() + self._expand_conv_padding = nn.Identity() + self._bn0 = nn.Identity() # Depthwise convolution phase self._depthwise_conv = nd_conv( @@ -134,7 +142,7 @@ def __init__( # self._swish = MemoryEfficientSwish() self._swish = Act["memswish"]() - def forward(self, inputs): + def forward(self, inputs: torch.Tensor): """MBConvBlock"s forward function. Args: @@ -148,7 +156,7 @@ def forward(self, inputs): # Expansion and Depthwise Convolution x = inputs if self.expand_ratio != 1: - x = self._expand_conv(self._expand_conv_padding(inputs)) + x = self._expand_conv(self._expand_conv_padding(x)) x = self._bn0(x) x = self._swish(x) @@ -172,7 +180,6 @@ def forward(self, inputs): input_filters, output_filters = self.in_channels, self.out_channels # stride needs to be a list - assert isinstance(self.stride, list) is_stride_one = all([s == 1 for s in self.stride]) if self.id_skip and is_stride_one and input_filters == output_filters: @@ -182,7 +189,7 @@ def forward(self, inputs): x = x + inputs # skip connection return x - def set_swish(self, memory_efficient=True): + def set_swish(self, memory_efficient: bool = True) -> None: """Sets swish function as memory efficient (for training) or standard (for export). Args: @@ -216,7 +223,7 @@ class EfficientNet(nn.Module): def __init__( self, - blocks_args: List[str], + blocks_args_str: List[str], spatial_dims: int = 2, in_channels: int = 3, num_classes: int = 1000, @@ -228,10 +235,13 @@ def __init__( batch_norm_epsilon: float = 1e-3, drop_connect_rate: float = 0.2, depth_divisor=8, - ): + ) -> None: super().__init__() - blocks_args = _decode_block_list(blocks_args) + if spatial_dims not in (1, 2, 3): + raise AssertionError("spatial_dims can only be 1, 2 or 3.") + + blocks_args = _decode_block_list(blocks_args_str) assert isinstance(blocks_args, list), "blocks_args should be a list" assert len(blocks_args) > 0, "block args must be greater than 0" @@ -242,13 +252,12 @@ def __init__( self.num_classes = num_classes self.in_channels = in_channels + current_image_size = [image_size] * self.spatial_dims + # Batch norm parameters bn_mom = 1 - batch_norm_momentum bn_eps = batch_norm_epsilon - if isinstance(image_size, int): - image_size = [image_size] * self.spatial_dims - # select the type of N-Dimensional layers to use # these are based on spatial dims and selected from MONAI factories nd_conv = Conv["conv", self.spatial_dims] @@ -259,12 +268,12 @@ def __init__( stride = [2] out_channels = _round_filters(32, width_coefficient, depth_divisor) # number of output channels self._conv_stem = nd_conv(self.in_channels, out_channels, kernel_size=3, stride=stride, bias=False) - self._conv_stem_padding = _make_same_padder(self._conv_stem, image_size) + self._conv_stem_padding = _make_same_padder(self._conv_stem, current_image_size) self._bn0 = nd_batchnorm(num_features=out_channels, momentum=bn_mom, eps=bn_eps) - image_size = _calculate_output_image_size(image_size, stride) + current_image_size = _calculate_output_image_size(current_image_size, stride) # Build blocks - self._blocks = [] + self._blocks = nn.Sequential() num_blocks = 0 # Update block input and output filters based on depth multiplier. @@ -287,56 +296,58 @@ def __init__( drop_connect_rate *= float(idx) / num_blocks # scale drop connect_rate # The first block needs to take care of stride and filter size increase. - self._blocks.append( + self._blocks.add_module( + str(idx), MBConvBlock( self.spatial_dims, block_args.input_filters, block_args.output_filters, block_args.kernel_size, block_args.stride, - image_size, + current_image_size, block_args.expand_ratio, block_args.se_ratio, block_args.id_skip, batch_norm_momentum, batch_norm_epsilon, drop_connect_rate=drop_connect_rate, - ) + ), ) idx += 1 - image_size = _calculate_output_image_size(image_size, block_args.stride) + current_image_size = _calculate_output_image_size(current_image_size, block_args.stride) if block_args.num_repeat > 1: # modify block_args to keep same output size block_args = block_args._replace(input_filters=block_args.output_filters, stride=[1]) for _ in range(block_args.num_repeat - 1): drop_connect_rate = drop_connect_rate if drop_connect_rate: drop_connect_rate *= float(idx) / num_blocks # scale drop connect_rate - self._blocks.append( + self._blocks.add_module( + str(idx), MBConvBlock( self.spatial_dims, block_args.input_filters, block_args.output_filters, block_args.kernel_size, block_args.stride, - image_size, + current_image_size, block_args.expand_ratio, block_args.se_ratio, block_args.id_skip, batch_norm_momentum, batch_norm_epsilon, drop_connect_rate=drop_connect_rate, - ) + ), ) idx += 1 - self._blocks = nn.Sequential(*self._blocks) + # self._blocks = nn.Sequential(* self._blocks) assert len(self._blocks) == num_blocks # Head head_in_channels = block_args.output_filters out_channels = _round_filters(1280, width_coefficient, depth_divisor) self._conv_head = nd_conv(head_in_channels, out_channels, kernel_size=1, bias=False) - self._conv_head_padding = _make_same_padder(self._conv_head, image_size) + self._conv_head_padding = _make_same_padder(self._conv_head, current_image_size) self._bn1 = nd_batchnorm(num_features=out_channels, momentum=bn_mom, eps=bn_eps) # Final linear layer @@ -345,10 +356,13 @@ def __init__( self._fc = nn.Linear(out_channels, self.num_classes) # swish activation to use - using memory efficient swish by default - # can be switched to normal swish using set_swish function call + # can be switched to normal swish using self.set_swish() function call self._swish = Act["memswish"]() - def set_swish(self, memory_efficient=True): + # initialize weights + self._initialize_weights() + + def set_swish(self, memory_efficient: bool = True) -> None: """Sets swish function as memory efficient (for training) or standard (for export). Args: @@ -359,7 +373,7 @@ def set_swish(self, memory_efficient=True): for block in self._blocks: block.set_swish(memory_efficient) - def forward(self, inputs): + def forward(self, inputs: torch.Tensor): """EfficientNet"s forward function. Calls extract_features to extract features, applies final linear layer, and returns logits. @@ -387,7 +401,7 @@ def forward(self, inputs): x = self._fc(x) return x - def _initialize_weight(self): + def _initialize_weights(self) -> None: # weight init as per Tensorflow Official impl # https://github.com/tensorflow/tpu/blob/master/models/official/efficientnet/efficientnet_model.py#L61 # code based on: https://github.com/rwightman/gen-efficientnet-pytorch/blob/master/geffnet/efficientnet_builder.py @@ -409,9 +423,18 @@ def _initialize_weight(self): class EfficientNetBN(EfficientNet): - # model_name mandatory as there is is EfficientNetBN itself, it needs the N \in [0, 1, 2, 3, 4, 5, 6, 7, 8] to be a model - def __init__(self, model_name, pretrained=True, progress=True, spatial_dims=2, in_channels=3, num_classes=1000): - block_args = [ + # model_name mandatory as there is no EfficientNetBN itself, it needs the N \in [0, 1, 2, 3, 4, 5, 6, 7] to be a model + def __init__( + self, + model_name: str, + pretrained: bool = True, + progress: bool = True, + spatial_dims: int = 2, + in_channels: int = 3, + num_classes: int = 1000, + ) -> None: + + blocks_args_str = [ "r1_k3_s11_e1_i32_o16_se0.25", "r2_k3_s22_e6_i16_o24_se0.25", "r2_k5_s22_e6_i24_o40_se0.25", @@ -426,7 +449,7 @@ def __init__(self, model_name, pretrained=True, progress=True, spatial_dims=2, i wc, dc, isize, dr = efficientnet_params[model_name] model = super(EfficientNetBN, self).__init__( - block_args, + blocks_args_str=blocks_args_str, spatial_dims=spatial_dims, in_channels=in_channels, num_classes=num_classes, @@ -454,11 +477,9 @@ def __init__(self, model_name, pretrained=True, progress=True, spatial_dims=2, i model_name, pretrained, is_default_model ) ) - print("Initializing weights for {}".format(model_name)) - self._initialize_weight() -def _load_state_dict(model: nn.Module, model_url: str, progress: bool, load_fc: bool) -> bool: +def _load_state_dict(model: nn.Module, model_url: str, progress: bool, load_fc: bool) -> None: state_dict = model_zoo.load_url(model_url, progress=progress) if load_fc: ret = model.load_state_dict(state_dict, strict=False) @@ -467,14 +488,15 @@ def _load_state_dict(model: nn.Module, model_url: str, progress: bool, load_fc: state_dict.pop("_fc.weight") state_dict.pop("_fc.bias") ret = model.load_state_dict(state_dict, strict=False) - assert set(ret.missing_keys) == set( - "_fc.weight", "_fc.bias" - ), "Missing keys when loading pretrained weights: {}".format(ret.missing_keys) + assert set(ret.missing_keys) == { + "_fc.weight", + "_fc.bias", + }, "Missing keys when loading pretrained weights: {}".format(ret.missing_keys) assert not ret.unexpected_keys, "Missing keys when loading pretrained weights: {}".format(ret.unexpected_keys) -def get_efficientnet_image_size(model_name): +def get_efficientnet_image_size(model_name: str) -> int: """Get the input image size for a given efficientnet model.""" assert model_name in efficientnet_params.keys(), "model_name should be one of {} ".format( ", ".join(efficientnet_params.keys()) @@ -525,7 +547,7 @@ def _round_repeats(repeats, depth_coefficient=None): return int(math.ceil(multiplier * repeats)) -def drop_connect(inputs, p, training): +def drop_connect(inputs: torch.Tensor, p: float, training: bool) -> torch.Tensor: """Drop connect. Args: @@ -541,15 +563,17 @@ def drop_connect(inputs, p, training): if not training: return inputs - batch_size = inputs.shape[0] - keep_prob = 1 - p + batch_size: int = inputs.shape[0] + keep_prob: float = 1 - p # generate binary_tensor mask according to probability (p for 0, 1-p for 1) - random_tensor = keep_prob - random_tensor += torch.rand([batch_size, 1, 1, 1], dtype=inputs.dtype, device=inputs.device) - binary_tensor = torch.floor(random_tensor) + # random_tensor = keep_prob + random_tensor: torch.Tensor = torch.rand([batch_size, 1, 1, 1], dtype=inputs.dtype, device=inputs.device) + random_tensor += keep_prob + + binary_tensor: torch.Tensor = torch.floor(random_tensor) - output = inputs / keep_prob * binary_tensor + output: torch.Tensor = inputs / keep_prob * binary_tensor return output @@ -607,7 +631,7 @@ def _make_same_padder(conv_op, image_size): padding = _get_same_padding_conv2d(image_size, conv_op.kernel_size, conv_op.dilation, conv_op.stride) padder = Pad["constantpad", len(padding) // 2] if sum(padding) > 0: - return padder(padding=padding, value=0) + return padder(padding=padding, value=0.0) else: return nn.Identity() diff --git a/tests/test_efficientnet.py b/tests/test_efficientnet.py new file mode 100644 index 0000000000..d8c810fb34 --- /dev/null +++ b/tests/test_efficientnet.py @@ -0,0 +1,228 @@ +# 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 TYPE_CHECKING +from unittest import skipUnless + +import torch +from parameterized import parameterized + +from monai.networks import eval_mode +from monai.networks.nets import EfficientNetBN, get_efficientnet_image_size +from monai.utils import optional_import +from tests.utils import test_script_save + +if TYPE_CHECKING: + import torchvision + + has_torchvision = True +else: + torchvision, has_torchvision = optional_import("torchvision") + +if TYPE_CHECKING: + import PIL + + has_pil = True +else: + PIL, has_pil = optional_import("PIL") + + +def get_model_names(): + return ["efficientnet-b{}".format(d) for d in range(8)] + + +def get_expected_model_shape(model_name): + model_input_shapes = { + "efficientnet-b0": 224, + "efficientnet-b1": 240, + "efficientnet-b2": 260, + "efficientnet-b3": 300, + "efficientnet-b4": 380, + "efficientnet-b5": 456, + "efficientnet-b6": 528, + "efficientnet-b7": 600, + } + return model_input_shapes[model_name] + + +def make_shape_cases(models, spatial_dims, batches, pretrained, in_channels=3, num_classes=1000): + ret_test = [] + for spatial_dim in spatial_dims: # selected spatial_dims + for batch in batches: # check single batch as well as multiple batch input + for model in models: # selected models + for is_pretrained in pretrained: # pretrained or not pretrained + kwargs = { + "model_name": model, + "pretrained": is_pretrained, + "progress": False, + "spatial_dims": spatial_dim, + "in_channels": in_channels, + "num_classes": num_classes, + } + ret_test.append( + [ + kwargs, + ( + batch, + in_channels, + ) + + (get_expected_model_shape(model),) * spatial_dim, + (batch, num_classes), + ] + ) + return ret_test + + +# create list of selected models to speed up redundant tests +# only test the models B0, B3 +SEL_MODELS = [get_model_names()[i] for i in [0, 3, 7]] + +# pretrained=False cases +# 1D models are cheap so do test for all models in 1D +CASES_1D = make_shape_cases( + models=get_model_names(), spatial_dims=[1], batches=[1, 4], pretrained=[False], in_channels=3, num_classes=1000 +) + +# 2D and 3D models are expensive so use selected models +CASES_2D = make_shape_cases( + models=SEL_MODELS, spatial_dims=[2], batches=[1, 4], pretrained=[False], in_channels=3, num_classes=1000 +) +CASES_3D = make_shape_cases( + models=[SEL_MODELS[0]], spatial_dims=[3], batches=[1], pretrained=[False], in_channels=3, num_classes=1000 +) + +# pretrained=True cases +# tabby kitty test with pretrained model +# needs 'testing_data/kitty_test.jpg' +CASES_KITTY_TRAINED = [ + ( + { + "model_name": "efficientnet-b0", + "pretrained": True, + "progress": False, + "spatial_dims": 2, + "in_channels": 3, + "num_classes": 1000, + }, + "testing_data/kitty_test.jpg", + 285, # ~ Egyptian cat + ), + ( + { + "model_name": "efficientnet-b7", + "pretrained": True, + "progress": False, + "spatial_dims": 2, + "in_channels": 3, + "num_classes": 1000, + }, + "testing_data/kitty_test.jpg", + 285, # ~ Egyptian cat + ), +] + +# varying num_classes and in_channels +CASES_VARITAIONS = [] + +# change num_classes test +# 10 classes +# 2D +CASES_VARITAIONS.extend( + make_shape_cases( + models=SEL_MODELS, spatial_dims=[2], batches=[1], pretrained=[False, True], in_channels=3, num_classes=10 + ) +) +# 3D +# CASES_VARITAIONS.extend( +# make_shape_cases( +# models=[SEL_MODELS[0]], spatial_dims=[3], batches=[1], pretrained=[False], in_channels=3, num_classes=10 +# ) +# ) + +# change in_channels test +# 1 channel +# 2D +CASES_VARITAIONS.extend( + make_shape_cases( + models=SEL_MODELS, spatial_dims=[2], batches=[1], pretrained=[False, True], in_channels=1, num_classes=1000 + ) +) +# 8 channel +# 2D +CASES_VARITAIONS.extend( + make_shape_cases( + models=SEL_MODELS, spatial_dims=[2], batches=[1], pretrained=[False, True], in_channels=8, num_classes=1000 + ) +) +# 3D +# CASES_VARITAIONS.extend( +# make_shape_cases( +# models=[SEL_MODELS[0]], spatial_dims=[3], batches=[1], pretrained=[False], in_channels=1, num_classes=1000 +# ) +# ) + + +class TestEFFICIENTNET(unittest.TestCase): + @parameterized.expand(CASES_1D + CASES_2D + CASES_3D + CASES_VARITAIONS) + def test_shape(self, input_param, input_shape, expected_shape): + device = "cuda" if torch.cuda.is_available() else "cpu" + print(input_param) + net = EfficientNetBN(**input_param).to(device) + with eval_mode(net): + result = net(torch.randn(input_shape).to(device)) + self.assertEqual(result.shape, expected_shape) + + @parameterized.expand(CASES_KITTY_TRAINED) + @skipUnless(has_torchvision, "Requires `torchvision` package.") + @skipUnless(has_pil, "Requires `pillow` package.") + def test_kitty_pretrained(self, input_param, image_path, expected_label): + device = "cuda" if torch.cuda.is_available() else "cpu" + # Open image + image_size = get_efficientnet_image_size(input_param["model_name"]) + img = PIL.Image.open("testdata/cat.jpeg") + tfms = torchvision.transforms.Compose( + [ + torchvision.transforms.Resize(image_size), + torchvision.transforms.CenterCrop(image_size), + torchvision.transforms.ToTensor(), + torchvision.transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), + ] + ) + img = tfms(img).unsqueeze(0).to(device) + net = EfficientNetBN(**input_param).to(device) + with eval_mode(net): + result = net(img) + pred_label = torch.argmax(result, dim=-1) + self.assertEqual(pred_label, expected_label) + + def test_ill_arg(self): + with self.assertRaises(AssertionError): + # wrong spatial_dims + EfficientNetBN(model_name="efficientnet-b0", spatial_dims=4) + # wrong model_name + EfficientNetBN(model_name="efficientnet-b10", spatial_dims=3) + + def test_func_get_efficientnet_input_shape(self): + for model in get_model_names(): + result_shape = get_efficientnet_image_size(model_name=model) + expected_shape = get_expected_model_shape(model) + self.assertEqual(result_shape, expected_shape) + + def test_script(self): + net = EfficientNetBN(model_name="efficientnet-b0", spatial_dims=2, in_channels=3, num_classes=1000) + net.set_swish(memory_efficient=False) # at the moment custom memory efficient swish is not exportable with jit + test_data = torch.randn(1, 3, 224, 224) + test_script_save(net, test_data) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/testing_data/kitty_test.jpg b/tests/testing_data/kitty_test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2aaaf34d92dddfba0b623d88be9f8c3539d697cf GIT binary patch literal 69360 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 Date: Mon, 5 Apr 2021 21:34:40 +0100 Subject: [PATCH 04/16] making unittests backwards compatible python<3.8 Signed-off-by: masadcv --- monai/networks/nets/efficientnet.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monai/networks/nets/efficientnet.py b/monai/networks/nets/efficientnet.py index 617743fcda..25bd57fe9f 100644 --- a/monai/networks/nets/efficientnet.py +++ b/monai/networks/nets/efficientnet.py @@ -14,7 +14,9 @@ """ import collections import math +import operator import re +from functools import reduce from typing import List import torch @@ -407,7 +409,7 @@ def _initialize_weights(self) -> None: # code based on: https://github.com/rwightman/gen-efficientnet-pytorch/blob/master/geffnet/efficientnet_builder.py for _, m in self.named_modules(): if isinstance(m, (nn.Conv1d, nn.Conv2d, nn.Conv3d)): - fan_out = math.prod(m.kernel_size) * m.out_channels + fan_out = reduce(operator.mul, m.kernel_size, 1) * m.out_channels m.weight.data.normal_(0, math.sqrt(2.0 / fan_out)) if m.bias is not None: m.bias.data.zero_() From b718b3bd648d517f67fca29de7bce2658d1eed89 Mon Sep 17 00:00:00 2001 From: masadcv Date: Mon, 5 Apr 2021 22:22:51 +0100 Subject: [PATCH 05/16] fixed kitty unittests file path Signed-off-by: masadcv --- tests/test_efficientnet.py | 7 ++++--- tests/testing_data/kitty_test.jpg | Bin 69360 -> 20045 bytes 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_efficientnet.py b/tests/test_efficientnet.py index d8c810fb34..3671f643db 100644 --- a/tests/test_efficientnet.py +++ b/tests/test_efficientnet.py @@ -9,6 +9,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os import unittest from typing import TYPE_CHECKING from unittest import skipUnless @@ -113,7 +114,7 @@ def make_shape_cases(models, spatial_dims, batches, pretrained, in_channels=3, n "in_channels": 3, "num_classes": 1000, }, - "testing_data/kitty_test.jpg", + os.path.join(os.path.dirname(__file__), "testing_data/kitty_test.jpg"), 285, # ~ Egyptian cat ), ( @@ -125,7 +126,7 @@ def make_shape_cases(models, spatial_dims, batches, pretrained, in_channels=3, n "in_channels": 3, "num_classes": 1000, }, - "testing_data/kitty_test.jpg", + os.path.join(os.path.dirname(__file__), "testing_data/kitty_test.jpg"), 285, # ~ Egyptian cat ), ] @@ -188,7 +189,7 @@ def test_kitty_pretrained(self, input_param, image_path, expected_label): device = "cuda" if torch.cuda.is_available() else "cpu" # Open image image_size = get_efficientnet_image_size(input_param["model_name"]) - img = PIL.Image.open("testdata/cat.jpeg") + img = PIL.Image.open(image_path) tfms = torchvision.transforms.Compose( [ torchvision.transforms.Resize(image_size), diff --git a/tests/testing_data/kitty_test.jpg b/tests/testing_data/kitty_test.jpg index 2aaaf34d92dddfba0b623d88be9f8c3539d697cf..ee94ced01e867507f3334d4f22011c03681449d3 100644 GIT binary patch literal 20045 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 Date: Tue, 6 Apr 2021 02:45:37 +0100 Subject: [PATCH 06/16] adding docstrings and minor refactoring Signed-off-by: masadcv --- monai/networks/nets/efficientnet.py | 392 +++++++++++++++++----------- tests/test_efficientnet.py | 22 +- 2 files changed, 254 insertions(+), 160 deletions(-) diff --git a/monai/networks/nets/efficientnet.py b/monai/networks/nets/efficientnet.py index 25bd57fe9f..cd615f3cb8 100644 --- a/monai/networks/nets/efficientnet.py +++ b/monai/networks/nets/efficientnet.py @@ -9,9 +9,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Implementation based on: https://github.com/lukemelas/EfficientNet-PyTorch -#With significant modifications to refactor/rewrite the code for MONAI -""" import collections import math import operator @@ -28,7 +25,7 @@ __all__ = ["EfficientNetBN", "get_efficientnet_image_size"] efficientnet_params = { - # Coefficients: width_mult, depth_mult, image_size, dropout_rate + # model_name: (width_mult, depth_mult, image_size, dropout_rate) "efficientnet-b0": (1.0, 1.0, 224, 0.2), "efficientnet-b1": (1.0, 1.1, 240, 0.2), "efficientnet-b2": (1.1, 1.2, 260, 0.3), @@ -39,32 +36,8 @@ "efficientnet-b7": (2.0, 3.1, 600, 0.5), } -url_map = { - "efficientnet-b0": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b0-355c32eb.pth", - "efficientnet-b1": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b1-f1951068.pth", - "efficientnet-b2": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b2-8bb594d6.pth", - "efficientnet-b3": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b3-5fb5a3c3.pth", - "efficientnet-b4": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b4-6ed6700e.pth", - "efficientnet-b5": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b5-b6417697.pth", - "efficientnet-b6": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b6-c76e70fd.pth", - "efficientnet-b7": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b7-dcc49843.pth", -} - class MBConvBlock(nn.Module): - """Mobile Inverted Residual Bottleneck Block. - - Args: - block_args (namedtuple): BlockArgs, defined in utils.py. - global_params (namedtuple): GlobalParam, defined in utils.py. - image_size (tuple or list): [image_height, image_width]. - - References: - [1] https://arxiv.org/abs/1704.04861 (MobileNet v1) - [2] https://arxiv.org/abs/1801.04381 (MobileNet v2) - [3] https://arxiv.org/abs/1905.02244 (MobileNet v3) - """ - def __init__( self, spatial_dims: int, @@ -80,19 +53,46 @@ def __init__( batch_norm_epsilon: float = 1e-3, drop_connect_rate: float = 0.2, ) -> None: + """ + Mobile Inverted Residual Bottleneck Block. + + Args: + spatial_dims: number of spatial dimensions. + in_channels: number of input channels. + out_classes: number of output channels. + kernel_size: size of the kernel for conv ops. + stride: stride to use for conv ops. + image_size: input image resolution. + expand_ratio: expansion ratio for inverted bottleneck. + se_ratio: squeeze-excitation ratio for se layers. + id_skip: whether to use skip connection. + batch_norm_momentum: momentum for batch norm. + batch_norm_epsilon: epsilon for batch norm. + drop_connect_rate: dropconnect rate for drop connection (individual weights) layers. + + References: + [1] https://arxiv.org/abs/1704.04861 (MobileNet v1) + [2] https://arxiv.org/abs/1801.04381 (MobileNet v2) + [3] https://arxiv.org/abs/1905.02244 (MobileNet v3) + """ + super().__init__() - self.spatial_dims = spatial_dims + + # select the type of N-Dimensional layers to use + # these are based on spatial dims and selected from MONAI factories + conv_type: Type[Union[nn.Conv1d, nn.Conv2d, nn.Conv3d]] = Conv["conv", spatial_dims] + batchnorm_type: Type[Union[nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d]] = Norm["batch", spatial_dims] + adaptivepool_type: Type[Union[nn.AdaptiveAvgPool1d, nn.AdaptiveAvgPool2d, nn.AdaptiveAvgPool3d]] = Pool[ + "adaptiveavg", spatial_dims + ] + self.in_channels = in_channels self.out_channels = out_channels self.id_skip = id_skip self.stride = stride self.expand_ratio = expand_ratio self.has_se = (se_ratio is not None) and (0 < se_ratio <= 1) - self._drop_connect_rate = drop_connect_rate - - nd_conv = Conv["conv", self.spatial_dims] - nd_batchnorm = Norm["batch", self.spatial_dims] - nd_adaptivepool = Pool["adaptiveavg", self.spatial_dims] + self.drop_connect_rate = drop_connect_rate bn_mom = 1 - batch_norm_momentum # pytorch"s difference from tensorflow bn_eps = batch_norm_epsilon @@ -101,10 +101,10 @@ def __init__( inp = in_channels # number of input channels oup = in_channels * expand_ratio # number of output channels if self.expand_ratio != 1: - self._expand_conv = nd_conv(in_channels=inp, out_channels=oup, kernel_size=1, bias=False) + self._expand_conv = conv_type(in_channels=inp, out_channels=oup, kernel_size=1, bias=False) self._expand_conv_padding = _make_same_padder(self._expand_conv, image_size) - self._bn0 = nd_batchnorm(num_features=oup, momentum=bn_mom, eps=bn_eps) + self._bn0 = batchnorm_type(num_features=oup, momentum=bn_mom, eps=bn_eps) else: # need to have the following to fix JIT error: # Module 'MBConvBlock' has no attribute '_expand_conv' @@ -115,7 +115,7 @@ def __init__( self._bn0 = nn.Identity() # Depthwise convolution phase - self._depthwise_conv = nd_conv( + self._depthwise_conv = conv_type( in_channels=oup, out_channels=oup, groups=oup, # groups makes it depthwise @@ -124,24 +124,23 @@ def __init__( bias=False, ) self._depthwise_conv_padding = _make_same_padder(self._depthwise_conv, image_size) - self._bn1 = nd_batchnorm(num_features=oup, momentum=bn_mom, eps=bn_eps) + self._bn1 = batchnorm_type(num_features=oup, momentum=bn_mom, eps=bn_eps) image_size = _calculate_output_image_size(image_size, self.stride) # Squeeze and Excitation layer, if desired if self.has_se: - self._se_adaptpool = nd_adaptivepool(1) + self._se_adaptpool = adaptivepool_type(1) num_squeezed_channels = max(1, int(in_channels * se_ratio)) - self._se_reduce = nd_conv(in_channels=oup, out_channels=num_squeezed_channels, kernel_size=1) + self._se_reduce = conv_type(in_channels=oup, out_channels=num_squeezed_channels, kernel_size=1) self._se_reduce_padding = _make_same_padder(self._se_reduce, (1, 1)) - self._se_expand = nd_conv(in_channels=num_squeezed_channels, out_channels=oup, kernel_size=1) + self._se_expand = conv_type(in_channels=num_squeezed_channels, out_channels=oup, kernel_size=1) self._se_expand_padding = _make_same_padder(self._se_expand, (1, 1)) # Pointwise convolution phase final_oup = out_channels - self._project_conv = nd_conv(in_channels=oup, out_channels=final_oup, kernel_size=1, bias=False) + self._project_conv = conv_type(in_channels=oup, out_channels=final_oup, kernel_size=1, bias=False) self._project_conv_padding = _make_same_padder(self._project_conv, image_size) - self._bn2 = nd_batchnorm(num_features=final_oup, momentum=bn_mom, eps=bn_eps) - # self._swish = MemoryEfficientSwish() + self._bn2 = batchnorm_type(num_features=final_oup, momentum=bn_mom, eps=bn_eps) self._swish = Act["memswish"]() def forward(self, inputs: torch.Tensor): @@ -154,7 +153,6 @@ def forward(self, inputs: torch.Tensor): Returns: Output of this block after processing. """ - # Expansion and Depthwise Convolution x = inputs if self.expand_ratio != 1: @@ -186,8 +184,8 @@ def forward(self, inputs: torch.Tensor): if self.id_skip and is_stride_one and input_filters == output_filters: # The combination of skip connection and drop connect brings about stochastic depth. - if self._drop_connect_rate: - x = drop_connect(x, p=self._drop_connect_rate, training=self.training) + if self.drop_connect_rate: + x = drop_connect(x, p=self.drop_connect_rate, training=self.training) x = x + inputs # skip connection return x @@ -201,28 +199,6 @@ def set_swish(self, memory_efficient: bool = True) -> None: class EfficientNet(nn.Module): - """EfficientNet model. - Most easily loaded with the .from_name or .from_pretrained methods. - - Args: - blocks_args (list[namedtuple]): A list of BlockArgs to construct blocks. - global_params (namedtuple): A set of GlobalParams shared between blocks. - - References: - [1] https://arxiv.org/abs/1905.11946 (EfficientNet) - - Example: - - - import torch - >>> from monai.networks.nets import get_efficientnet_image_size, EfficientNetBN - >>> image_size = get_efficientnet_image_size("efficientnet-b0") - >>> inputs = torch.rand(1, 3, image_size, image_size) - >>> model = EfficientNetBN("efficientnet-b0") - >>> model.eval() - >>> outputs = model(inputs) - """ - def __init__( self, blocks_args_str: List[str], @@ -238,43 +214,84 @@ def __init__( drop_connect_rate: float = 0.2, depth_divisor=8, ) -> None: + """ + EfficientNet based on `Rethinking Model Scaling for Convolutional Neural Networks `_. + Adapted from `EfficientNet-PyTorch + `_. + + Args: + blocks_args_str: block definitions. + spatial_dims: number of spatial dimensions. + in_channels: number of input channels. + num_classes: number of output classes. + width_coefficient: width multiplier coefficient (w in paper). + depth_coefficient: depth multiplier coefficient (d in paper). + dropout_rate: dropout rate for dropout layers. + image_size: input image resolution. + batch_norm_momentum: momentum for batch norm. + batch_norm_epsilon: epsilon for batch norm. + drop_connect_rate: dropconnect rate for drop connection (individual weights) layers. + depth_divisor: depth divisor for channel rounding. + + Examples:: + + # for pretrained spatial 2D ImageNet + >>> image_size = get_efficientnet_image_size("efficientnet-b0") + >>> inputs = torch.rand(1, 3, image_size, image_size) + >>> model = EfficientNetBN("efficientnet-b0", pretrained=True) + >>> model.eval() + >>> outputs = model(inputs) + + # create spatial 2D + >>> model = EfficientNetBN("efficientnet-b0", spatial_dims=2) + + # create spatial 3D + >>> model = EfficientNetBN("efficientnet-b0", spatial_dims=3) + + # create EfficientNetB7 for spatial 2D + >>> model = EfficientNetBN("efficientnet-b7", spatial_dims=2) + + """ super().__init__() if spatial_dims not in (1, 2, 3): raise AssertionError("spatial_dims can only be 1, 2 or 3.") + # select the type of N-Dimensional layers to use + # these are based on spatial dims and selected from MONAI factories + conv_type: Type[Union[nn.Conv1d, nn.Conv2d, nn.Conv3d]] = Conv["conv", spatial_dims] + batchnorm_type: Type[Union[nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d]] = Norm["batch", spatial_dims] + adaptivepool_type: Type[Union[nn.AdaptiveAvgPool1d, nn.AdaptiveAvgPool2d, nn.AdaptiveAvgPool3d]] = Pool[ + "adaptiveavg", spatial_dims + ] + + # decode blocks args into arguments for MBConvBlock blocks_args = _decode_block_list(blocks_args_str) + # checks for successful decoding of blocks_args_str assert isinstance(blocks_args, list), "blocks_args should be a list" assert len(blocks_args) > 0, "block args must be greater than 0" self._blocks_args = blocks_args - - self.spatial_dims = spatial_dims self.num_classes = num_classes self.in_channels = in_channels - current_image_size = [image_size] * self.spatial_dims + # expand input image dimensions to list + current_image_size = [image_size] * spatial_dims - # Batch norm parameters - bn_mom = 1 - batch_norm_momentum + # Parameters for batch norm + bn_mom = 1 - batch_norm_momentum # 1 - bn_m to convert tensorflow's arg to pytorch bn compatible bn_eps = batch_norm_epsilon - # select the type of N-Dimensional layers to use - # these are based on spatial dims and selected from MONAI factories - nd_conv = Conv["conv", self.spatial_dims] - nd_batchnorm = Norm["batch", self.spatial_dims] - nd_adaptivepool = Pool["adaptiveavg", self.spatial_dims] - # Stem stride = [2] out_channels = _round_filters(32, width_coefficient, depth_divisor) # number of output channels - self._conv_stem = nd_conv(self.in_channels, out_channels, kernel_size=3, stride=stride, bias=False) + self._conv_stem = conv_type(self.in_channels, out_channels, kernel_size=3, stride=stride, bias=False) self._conv_stem_padding = _make_same_padder(self._conv_stem, current_image_size) - self._bn0 = nd_batchnorm(num_features=out_channels, momentum=bn_mom, eps=bn_eps) + self._bn0 = batchnorm_type(num_features=out_channels, momentum=bn_mom, eps=bn_eps) current_image_size = _calculate_output_image_size(current_image_size, stride) - # Build blocks + # Build MBConv blocks self._blocks = nn.Sequential() num_blocks = 0 @@ -290,7 +307,8 @@ def __init__( # calculate the total number of blocks - needed for drop_connect estimation num_blocks += block_args.num_repeat - idx = 0 + # Create and add MBConvBlocks to self._blocks + idx = 0 # block index counter for block_args in self._blocks_args: drop_connect_rate = drop_connect_rate @@ -301,7 +319,7 @@ def __init__( self._blocks.add_module( str(idx), MBConvBlock( - self.spatial_dims, + spatial_dims, block_args.input_filters, block_args.output_filters, block_args.kernel_size, @@ -315,11 +333,13 @@ def __init__( drop_connect_rate=drop_connect_rate, ), ) - idx += 1 + idx += 1 # increment blocks index counter current_image_size = _calculate_output_image_size(current_image_size, block_args.stride) if block_args.num_repeat > 1: # modify block_args to keep same output size block_args = block_args._replace(input_filters=block_args.output_filters, stride=[1]) + + # Repeat block for num_repeat required for _ in range(block_args.num_repeat - 1): drop_connect_rate = drop_connect_rate if drop_connect_rate: @@ -327,7 +347,7 @@ def __init__( self._blocks.add_module( str(idx), MBConvBlock( - self.spatial_dims, + spatial_dims, block_args.input_filters, block_args.output_filters, block_args.kernel_size, @@ -341,19 +361,20 @@ def __init__( drop_connect_rate=drop_connect_rate, ), ) - idx += 1 - # self._blocks = nn.Sequential(* self._blocks) + idx += 1 # increment blocks index counter + + # Sanity check to see if len(self._blocks) equal expected num_blocks assert len(self._blocks) == num_blocks # Head head_in_channels = block_args.output_filters out_channels = _round_filters(1280, width_coefficient, depth_divisor) - self._conv_head = nd_conv(head_in_channels, out_channels, kernel_size=1, bias=False) + self._conv_head = conv_type(head_in_channels, out_channels, kernel_size=1, bias=False) self._conv_head_padding = _make_same_padder(self._conv_head, current_image_size) - self._bn1 = nd_batchnorm(num_features=out_channels, momentum=bn_mom, eps=bn_eps) + self._bn1 = batchnorm_type(num_features=out_channels, momentum=bn_mom, eps=bn_eps) # Final linear layer - self._avg_pooling = nd_adaptivepool(1) + self._avg_pooling = adaptivepool_type(1) self._dropout = nn.Dropout(dropout_rate) self._fc = nn.Linear(out_channels, self.num_classes) @@ -365,10 +386,11 @@ def __init__( self._initialize_weights() def set_swish(self, memory_efficient: bool = True) -> None: - """Sets swish function as memory efficient (for training) or standard (for export). + """ + Sets swish function as memory efficient (for training) or standard (for JIT export). Args: - memory_efficient (bool): Whether to use memory-efficient version of swish. + memory_efficient: whether to use memory-efficient version of swish. """ self._swish = Act["memswish"]() if memory_efficient else Act["swish"](alpha=1.0) @@ -376,14 +398,14 @@ def set_swish(self, memory_efficient: bool = True) -> None: block.set_swish(memory_efficient) def forward(self, inputs: torch.Tensor): - """EfficientNet"s forward function. - Calls extract_features to extract features, applies final linear layer, and returns logits. - + """ Args: - inputs (tensor): Input tensor. + inputs: input should have spatially N dimensions + ``(Batch, in_channels, dim_0[, dim_1, ..., dim_N])``, N is defined by `dimensions`. Returns: - Output of this model after processing. + A torch Tensor of classification prediction in shape + ``(Batch, num_classes)``. """ # Convolution layers # Stem @@ -404,9 +426,12 @@ def forward(self, inputs: torch.Tensor): return x def _initialize_weights(self) -> None: - # weight init as per Tensorflow Official impl - # https://github.com/tensorflow/tpu/blob/master/models/official/efficientnet/efficientnet_model.py#L61 - # code based on: https://github.com/rwightman/gen-efficientnet-pytorch/blob/master/geffnet/efficientnet_builder.py + """ + Args: + None, initializes weights for conv/linear/batchnorm layers + following weight init methods from `official Tensorflow EfficientNet implementation `_. + Adapted from `EfficientNet-PyTorch's init method `_. + """ for _, m in self.named_modules(): if isinstance(m, (nn.Conv1d, nn.Conv2d, nn.Conv3d)): fan_out = reduce(operator.mul, m.kernel_size, 1) * m.out_channels @@ -417,7 +442,7 @@ def _initialize_weights(self) -> None: m.weight.data.fill_(1.0) m.bias.data.zero_() elif isinstance(m, nn.Linear): - fan_out = m.weight.size(0) # fan-out + fan_out = m.weight.size(0) fan_in = 0 init_range = 1.0 / math.sqrt(fan_in + fan_out) m.weight.data.uniform_(-init_range, init_range) @@ -425,7 +450,6 @@ def _initialize_weights(self) -> None: class EfficientNetBN(EfficientNet): - # model_name mandatory as there is no EfficientNetBN itself, it needs the N \in [0, 1, 2, 3, 4, 5, 6, 7] to be a model def __init__( self, model_name: str, @@ -435,7 +459,20 @@ def __init__( in_channels: int = 3, num_classes: int = 1000, ) -> None: + """ + Generic wrapper around EfficientNet, used to initialize EfficientNet-B0 to EfficientNet-B7 models + model_name is mandatory argument as there is no EfficientNetBN itself, it needs the N \in [0, 1, 2, 3, 4, 5, 6, 7] to be a model + + Args: + model_name: name of model to initialize, can be from [efficientnet-b0, ..., efficientnet-b7]. + pretrained: whether to initialize pretrained ImageNet weights, only available for spatial_dims=2. + progress: whether to show download progress for pretrained weights download. + spatial_dims: number of spatial dimensions. + in_channels: number of input channels. + num_classes: number of output classes. + """ + # block args for EfficientNet-B0 to EfficientNet-B7 blocks_args_str = [ "r1_k3_s11_e1_i32_o16_se0.25", "r2_k3_s22_e6_i16_o24_se0.25", @@ -449,7 +486,10 @@ def __init__( ", ".join(efficientnet_params.keys()) ) + # get network parameters wc, dc, isize, dr = efficientnet_params[model_name] + + # create model and initialize random weights model = super(EfficientNetBN, self).__init__( blocks_args_str=blocks_args_str, spatial_dims=spatial_dims, @@ -461,18 +501,16 @@ def __init__( image_size=isize, ) + # attempt to load pretrained is_default_model = (spatial_dims == 2) and (in_channels == 3) - pretrained = pretrained and model_name in url_map - loadable_from_file = pretrained and is_default_model if loadable_from_file: # skip loading fc layers for transfer learning applications load_fc = num_classes == 1000 - model_url = url_map[model_name] # only pretrained for when `spatial_dims` is 2 - _load_state_dict(self, model_url, progress, load_fc) + _load_state_dict(self, model_name, progress, load_fc) else: print( "Skipping loading pretrained weights for non-default {}, pretrained={}, is_default_model={}".format( @@ -481,8 +519,20 @@ def __init__( ) -def _load_state_dict(model: nn.Module, model_url: str, progress: bool, load_fc: bool) -> None: +def _load_state_dict(model: nn.Module, model_name: str, progress: bool, load_fc: bool) -> None: + url_map = { + "efficientnet-b0": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b0-355c32eb.pth", + "efficientnet-b1": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b1-f1951068.pth", + "efficientnet-b2": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b2-8bb594d6.pth", + "efficientnet-b3": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b3-5fb5a3c3.pth", + "efficientnet-b4": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b4-6ed6700e.pth", + "efficientnet-b5": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b5-b6417697.pth", + "efficientnet-b6": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b6-c76e70fd.pth", + "efficientnet-b7": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b7-dcc49843.pth", + } + model_url = url_map[model_name] state_dict = model_zoo.load_url(model_url, progress=progress) + if load_fc: ret = model.load_state_dict(state_dict, strict=False) assert not ret.missing_keys, "Missing keys when loading pretrained weights: {}".format(ret.missing_keys) @@ -499,7 +549,16 @@ def _load_state_dict(model: nn.Module, model_url: str, progress: bool, load_fc: def get_efficientnet_image_size(model_name: str) -> int: - """Get the input image size for a given efficientnet model.""" + """ + Get the input image size for a given efficientnet model. + + Args: + model_name: name of model to initialize, can be from [efficientnet-b0, ..., efficientnet-b7]. + + Returns: + Image size for single spatial dimension as integer. + + """ assert model_name in efficientnet_params.keys(), "model_name should be one of {} ".format( ", ".join(efficientnet_params.keys()) ) @@ -508,15 +567,16 @@ def get_efficientnet_image_size(model_name: str) -> int: def _round_filters(filters, width_coefficient=None, depth_divisor=None): - """Calculate and round number of filters based on width multiplier. - Use width_coefficient, depth_divisor of global_params. + """ + Calculate and round number of filters based on width coefficient multiplier and depth divisor. Args: - filters (int): Filters number to be calculated. - global_params (namedtuple): Global params of the model. + filters: number of input filters. + width_coefficient: width coefficient for model. + depth_divisor: depth divisor to use. Returns: - new_filters: New filters number after calculating. + new_filters: new number of filters after calculation. """ multiplier = width_coefficient if not multiplier: @@ -532,15 +592,15 @@ def _round_filters(filters, width_coefficient=None, depth_divisor=None): def _round_repeats(repeats, depth_coefficient=None): - """Calculate module"s repeat number of a block based on depth multiplier. - Use depth_coefficient of global_params. + """ + Re-calculate module's repeat number of a block based on depth coefficient multiplier. Args: - repeats (int): num_repeat to be calculated. - global_params (namedtuple): Global params of the model. + repeats: number of original repeats. + depth_coefficient: depth coefficient for model. Returns: - new repeat: New repeat number after calculating. + new repeat: new number of repeat after calculating. """ multiplier = depth_coefficient if not multiplier: @@ -550,15 +610,19 @@ def _round_repeats(repeats, depth_coefficient=None): def drop_connect(inputs: torch.Tensor, p: float, training: bool) -> torch.Tensor: - """Drop connect. + """ + Drop connect layer that drops individual connections. + Differs from dropout as dropconnect drops connections instead of whole neurons as in dropout. + Based on `Deep Networks with Stochastic Depth `_. + Adapted from `Official Tensorflow EfficientNet utils `_. Args: - input (tensor: BCWH): Input of this structure. - p (float: 0.0~1.0): Probability of drop connection. - training (bool): The running mode. + input: input tensor with [B, C, dim_1, dim_2, ..., dim_N] where N=spatial_dims. + p: probability to use for dropping connections. + training: whether in training or evaluation mode. Returns: - output: Output after drop connection. + output: output tensor after applying drop connection. """ assert 0 <= p <= 1, "p must be in range of [0,1]" @@ -580,15 +644,16 @@ def drop_connect(inputs: torch.Tensor, p: float, training: bool) -> torch.Tensor def _calculate_output_image_size(input_image_size, stride): - """Calculates the output image size when using Conv2dSamePadding with a stride. - Necessary for static padding. Thanks to mannatsingh for pointing this out. + """ + Calculates the output image size when using _make_same_padder with a stride. + Necessary for static padding. Args: - input_image_size (int, tuple or list): Size of input image. - stride (int, tuple or list): Conv2d operation"s stride. + input_image_size: input image/feature spatial size. + stride: Conv2d operation"s stride. Returns: - output_image_size: A list [H,W]. + output_image_size: output image/feature spatial size. """ if input_image_size is None: return None @@ -602,35 +667,62 @@ def _calculate_output_image_size(input_image_size, stride): return [int(math.ceil(im_sz / st)) for im_sz, st in zip(input_image_size, stride)] -def _get_same_padding_conv2d(image_size, kernel_size, dilation, stride): - num_dims = len(kernel_size) +def _get_same_padding_convNd(image_size, kernel_size, dilation, stride): + """ + Helper for getting padding (nn.ConstantPadNd) to be used to get SAME padding + conv operations similar to Tensorflow's SAME padding. - # additional checks to populate dilation and stride (in case they are integers or single entry list) - if isinstance(stride, int): - stride = (stride,) * num_dims - elif len(stride) == 1: - stride = stride * num_dims + This function is generalized for MONAI's N-Dimensional spatial operations (e.g. Conv1D, Conv2D, Conv3D) + + Args: + image_size: input image/feature spatial size. + kernel_size: conv kernel's spatial size. + dilation: conv dilation rate for Atrous conv. + stride: stride for conv operation. + + Returns: + paddings for ConstantPadXd padder to be used on input tensor to conv op. + """ + num_dims = len(kernel_size) - if isinstance(dilation, int): - dilation = (dilation,) * num_dims - elif len(dilation) == 1: + # additional checks to populate dilation and stride (in case they are single entry list) + if len(dilation) == 1: dilation = dilation * num_dims - # _kernel_size = kernel_size + if len(stride) == 1: + stride = stride * num_dims + + # equation to calculate (pad^+ + pad^-) size _pad_size = [ max((math.ceil(_i_s / _s) - 1) * _s + (_k_s - 1) * _d + 1 - _i_s, 0) for _i_s, _k_s, _d, _s in zip(image_size, kernel_size, dilation, stride) ] + # distribute paddings into pad^+ and pad^- following Tensorflow's same padding strategy _paddings = [(_p // 2, _p - _p // 2) for _p in _pad_size] # unroll list of tuples to tuples, - # reversed as constandpadnd expects paddings starting with last dimenion + # reversed as nn.ConstantPadXd expects paddings starting with last dimenion _paddings = [outer for inner in reversed(_paddings) for outer in inner] return _paddings def _make_same_padder(conv_op, image_size): - padding = _get_same_padding_conv2d(image_size, conv_op.kernel_size, conv_op.dilation, conv_op.stride) + """ + Helper for initializing ConstantPadNd with SAME padding similar to Tensorflow. + Uses output of _get_same_padding_convNd() to get the padding size. + Generalized for N-Dimensional spatial operatoins e.g. Conv1D, Conv2D, Conv3D + + Args: + conv_op: nn.ConvNd operation to extract parameters for op from + image_size: input image/feature spatial size + + Returns: + If padding required then nn.ConstandNd() padder initialized to paddings otherwise nn.Identity() + """ + # calculate padding required + padding = _get_same_padding_convNd(image_size, conv_op.kernel_size, conv_op.dilation, conv_op.stride) + + # initialize and return padder padder = Pad["constantpad", len(padding) // 2] if sum(padding) > 0: return padder(padding=padding, value=0.0) @@ -639,13 +731,14 @@ def _make_same_padder(conv_op, image_size): def _decode_block_list(string_list): - """Decode a list of string notations to specify blocks inside the network. + """ + Decode a list of string notations to specify blocks inside the network. Args: - string_list (list[str]): A list of strings, each string is a notation of block. + string_list: a list of strings, each string is a notation of block. Returns: - blocks_args: A list of BlockArgs namedtuples of block args. + blocks_args: a list of BlockArgs namedtuples of block args. """ # Parameters for an individual model block BlockArgs = collections.namedtuple( @@ -664,14 +757,15 @@ def _decode_block_list(string_list): BlockArgs.__new__.__defaults__ = (None,) * len(BlockArgs._fields) def _decode_block_string(block_string): - """Get a block through a string notation of arguments. + """ + Get a block through a string notation of arguments. Args: block_string (str): A string notation of arguments. - Examples: "r1_k3_s11_e1_i32_o16_se0.25_noskip". + Examples: "r1_k3_s11_e1_i32_o16_se0.25". Returns: - BlockArgs: The namedtuple defined at the top of this file. + BlockArgs: namedtuple defined at the top of this function. """ assert isinstance(block_string, str) diff --git a/tests/test_efficientnet.py b/tests/test_efficientnet.py index 3671f643db..babc80834e 100644 --- a/tests/test_efficientnet.py +++ b/tests/test_efficientnet.py @@ -56,7 +56,7 @@ def get_expected_model_shape(model_name): def make_shape_cases(models, spatial_dims, batches, pretrained, in_channels=3, num_classes=1000): - ret_test = [] + ret_tests = [] for spatial_dim in spatial_dims: # selected spatial_dims for batch in batches: # check single batch as well as multiple batch input for model in models: # selected models @@ -69,7 +69,7 @@ def make_shape_cases(models, spatial_dims, batches, pretrained, in_channels=3, n "in_channels": in_channels, "num_classes": num_classes, } - ret_test.append( + ret_tests.append( [ kwargs, ( @@ -80,11 +80,11 @@ def make_shape_cases(models, spatial_dims, batches, pretrained, in_channels=3, n (batch, num_classes), ] ) - return ret_test + return ret_tests # create list of selected models to speed up redundant tests -# only test the models B0, B3 +# only test the models B0, B3, B7 SEL_MODELS = [get_model_names()[i] for i in [0, 3, 7]] # pretrained=False cases @@ -132,18 +132,18 @@ def make_shape_cases(models, spatial_dims, batches, pretrained, in_channels=3, n ] # varying num_classes and in_channels -CASES_VARITAIONS = [] +CASES_VARIATIONS = [] # change num_classes test # 10 classes # 2D -CASES_VARITAIONS.extend( +CASES_VARIATIONS.extend( make_shape_cases( models=SEL_MODELS, spatial_dims=[2], batches=[1], pretrained=[False, True], in_channels=3, num_classes=10 ) ) # 3D -# CASES_VARITAIONS.extend( +# CASES_VARIATIONS.extend( # make_shape_cases( # models=[SEL_MODELS[0]], spatial_dims=[3], batches=[1], pretrained=[False], in_channels=3, num_classes=10 # ) @@ -152,20 +152,20 @@ def make_shape_cases(models, spatial_dims, batches, pretrained, in_channels=3, n # change in_channels test # 1 channel # 2D -CASES_VARITAIONS.extend( +CASES_VARIATIONS.extend( make_shape_cases( models=SEL_MODELS, spatial_dims=[2], batches=[1], pretrained=[False, True], in_channels=1, num_classes=1000 ) ) # 8 channel # 2D -CASES_VARITAIONS.extend( +CASES_VARIATIONS.extend( make_shape_cases( models=SEL_MODELS, spatial_dims=[2], batches=[1], pretrained=[False, True], in_channels=8, num_classes=1000 ) ) # 3D -# CASES_VARITAIONS.extend( +# CASES_VARIATIONS.extend( # make_shape_cases( # models=[SEL_MODELS[0]], spatial_dims=[3], batches=[1], pretrained=[False], in_channels=1, num_classes=1000 # ) @@ -173,7 +173,7 @@ def make_shape_cases(models, spatial_dims, batches, pretrained, in_channels=3, n class TestEFFICIENTNET(unittest.TestCase): - @parameterized.expand(CASES_1D + CASES_2D + CASES_3D + CASES_VARITAIONS) + @parameterized.expand(CASES_1D + CASES_2D + CASES_3D + CASES_VARIATIONS) def test_shape(self, input_param, input_shape, expected_shape): device = "cuda" if torch.cuda.is_available() else "cpu" print(input_param) From dfc0a6ea80cbfca742362178dfa4ffa5e87657ef Mon Sep 17 00:00:00 2001 From: masadcv Date: Tue, 6 Apr 2021 04:18:15 +0100 Subject: [PATCH 07/16] fix flake8-py3 failing test Signed-off-by: masadcv --- monai/networks/nets/efficientnet.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/monai/networks/nets/efficientnet.py b/monai/networks/nets/efficientnet.py index cd615f3cb8..857e6d2561 100644 --- a/monai/networks/nets/efficientnet.py +++ b/monai/networks/nets/efficientnet.py @@ -14,7 +14,7 @@ import operator import re from functools import reduce -from typing import List +from typing import List, Type, Union import torch from torch import nn @@ -429,8 +429,11 @@ def _initialize_weights(self) -> None: """ Args: None, initializes weights for conv/linear/batchnorm layers - following weight init methods from `official Tensorflow EfficientNet implementation `_. - Adapted from `EfficientNet-PyTorch's init method `_. + following weight init methods from + `official Tensorflow EfficientNet implementation + `_. + Adapted from `EfficientNet-PyTorch's init method + `_. """ for _, m in self.named_modules(): if isinstance(m, (nn.Conv1d, nn.Conv2d, nn.Conv3d)): @@ -461,7 +464,8 @@ def __init__( ) -> None: """ Generic wrapper around EfficientNet, used to initialize EfficientNet-B0 to EfficientNet-B7 models - model_name is mandatory argument as there is no EfficientNetBN itself, it needs the N \in [0, 1, 2, 3, 4, 5, 6, 7] to be a model + model_name is mandatory argument as there is no EfficientNetBN itself, + it needs the N in [0, 1, 2, 3, 4, 5, 6, 7] to be a model Args: model_name: name of model to initialize, can be from [efficientnet-b0, ..., efficientnet-b7]. @@ -614,7 +618,8 @@ def drop_connect(inputs: torch.Tensor, p: float, training: bool) -> torch.Tensor Drop connect layer that drops individual connections. Differs from dropout as dropconnect drops connections instead of whole neurons as in dropout. Based on `Deep Networks with Stochastic Depth `_. - Adapted from `Official Tensorflow EfficientNet utils `_. + Adapted from `Official Tensorflow EfficientNet utils + `_. Args: input: input tensor with [B, C, dim_1, dim_2, ..., dim_N] where N=spatial_dims. @@ -667,7 +672,7 @@ def _calculate_output_image_size(input_image_size, stride): return [int(math.ceil(im_sz / st)) for im_sz, st in zip(input_image_size, stride)] -def _get_same_padding_convNd(image_size, kernel_size, dilation, stride): +def _get_same_padding_conv_nd(image_size, kernel_size, dilation, stride): """ Helper for getting padding (nn.ConstantPadNd) to be used to get SAME padding conv operations similar to Tensorflow's SAME padding. @@ -709,7 +714,7 @@ def _get_same_padding_convNd(image_size, kernel_size, dilation, stride): def _make_same_padder(conv_op, image_size): """ Helper for initializing ConstantPadNd with SAME padding similar to Tensorflow. - Uses output of _get_same_padding_convNd() to get the padding size. + Uses output of _get_same_padding_conv_nd() to get the padding size. Generalized for N-Dimensional spatial operatoins e.g. Conv1D, Conv2D, Conv3D Args: @@ -720,7 +725,7 @@ def _make_same_padder(conv_op, image_size): If padding required then nn.ConstandNd() padder initialized to paddings otherwise nn.Identity() """ # calculate padding required - padding = _get_same_padding_convNd(image_size, conv_op.kernel_size, conv_op.dilation, conv_op.stride) + padding = _get_same_padding_conv_nd(image_size, conv_op.kernel_size, conv_op.dilation, conv_op.stride) # initialize and return padder padder = Pad["constantpad", len(padding) // 2] From 09401615dd37abb1cfd5cdea7f9b3aed7e0dc747 Mon Sep 17 00:00:00 2001 From: masadcv Date: Tue, 6 Apr 2021 14:03:29 +0100 Subject: [PATCH 08/16] generalize drop_connect for n-dim, fix/add unittests, remove assert Signed-off-by: masadcv --- monai/networks/nets/__init__.py | 2 +- monai/networks/nets/efficientnet.py | 188 ++++++++++++++++------------ tests/test_activations.py | 11 +- tests/test_efficientnet.py | 56 +++++++-- 4 files changed, 165 insertions(+), 92 deletions(-) diff --git a/monai/networks/nets/__init__.py b/monai/networks/nets/__init__.py index a112348fad..bfb3fba6ee 100644 --- a/monai/networks/nets/__init__.py +++ b/monai/networks/nets/__init__.py @@ -15,7 +15,7 @@ from .classifier import Classifier, Critic, Discriminator from .densenet import DenseNet, DenseNet121, DenseNet169, DenseNet201, DenseNet264 from .dynunet import DynUNet, DynUnet, Dynunet -from .efficientnet import EfficientNetBN, get_efficientnet_image_size +from .efficientnet import EfficientNetBN, drop_connect, get_efficientnet_image_size from .fullyconnectednet import FullyConnectedNet, VarFullyConnectedNet from .generator import Generator from .highresnet import HighResBlock, HighResNet diff --git a/monai/networks/nets/efficientnet.py b/monai/networks/nets/efficientnet.py index 857e6d2561..7ee20ffa33 100644 --- a/monai/networks/nets/efficientnet.py +++ b/monai/networks/nets/efficientnet.py @@ -14,7 +14,7 @@ import operator import re from functools import reduce -from typing import List, Type, Union +from typing import List, Tuple, Type, Union import torch from torch import nn @@ -22,18 +22,18 @@ from monai.networks.layers.factories import Act, Conv, Norm, Pad, Pool -__all__ = ["EfficientNetBN", "get_efficientnet_image_size"] +__all__ = ["EfficientNetBN", "get_efficientnet_image_size", "drop_connect"] efficientnet_params = { - # model_name: (width_mult, depth_mult, image_size, dropout_rate) - "efficientnet-b0": (1.0, 1.0, 224, 0.2), - "efficientnet-b1": (1.0, 1.1, 240, 0.2), - "efficientnet-b2": (1.1, 1.2, 260, 0.3), - "efficientnet-b3": (1.2, 1.4, 300, 0.3), - "efficientnet-b4": (1.4, 1.8, 380, 0.4), - "efficientnet-b5": (1.6, 2.2, 456, 0.4), - "efficientnet-b6": (1.8, 2.6, 528, 0.5), - "efficientnet-b7": (2.0, 3.1, 600, 0.5), + # model_name: (width_mult, depth_mult, image_size, dropout_rate, dropconnect_rate) + "efficientnet-b0": (1.0, 1.0, 224, 0.2, 0.2), + "efficientnet-b1": (1.0, 1.1, 240, 0.2, 0.2), + "efficientnet-b2": (1.1, 1.2, 260, 0.3, 0.2), + "efficientnet-b3": (1.2, 1.4, 300, 0.3, 0.2), + "efficientnet-b4": (1.4, 1.8, 380, 0.4, 0.2), + "efficientnet-b5": (1.6, 2.2, 456, 0.4, 0.2), + "efficientnet-b6": (1.8, 2.6, 528, 0.5, 0.2), + "efficientnet-b7": (2.0, 3.1, 600, 0.5, 0.2), } @@ -44,7 +44,7 @@ def __init__( in_channels: int, out_channels: int, kernel_size: int, - stride: List[int], + stride: Union[int, Tuple[int]], image_size: List[int], expand_ratio: int, se_ratio: float, @@ -80,11 +80,9 @@ def __init__( # select the type of N-Dimensional layers to use # these are based on spatial dims and selected from MONAI factories - conv_type: Type[Union[nn.Conv1d, nn.Conv2d, nn.Conv3d]] = Conv["conv", spatial_dims] - batchnorm_type: Type[Union[nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d]] = Norm["batch", spatial_dims] - adaptivepool_type: Type[Union[nn.AdaptiveAvgPool1d, nn.AdaptiveAvgPool2d, nn.AdaptiveAvgPool3d]] = Pool[ - "adaptiveavg", spatial_dims - ] + conv_type = Conv["conv", spatial_dims] + batchnorm_type = Norm["batch", spatial_dims] + adaptivepool_type = Pool["adaptiveavg", spatial_dims] self.in_channels = in_channels self.out_channels = out_channels @@ -107,7 +105,7 @@ def __init__( self._bn0 = batchnorm_type(num_features=oup, momentum=bn_mom, eps=bn_eps) else: # need to have the following to fix JIT error: - # Module 'MBConvBlock' has no attribute '_expand_conv' + # "Module 'MBConvBlock' has no attribute '_expand_conv'" # FIXME: find a better way to bypass JIT error self._expand_conv = nn.Identity() @@ -132,9 +130,9 @@ def __init__( self._se_adaptpool = adaptivepool_type(1) num_squeezed_channels = max(1, int(in_channels * se_ratio)) self._se_reduce = conv_type(in_channels=oup, out_channels=num_squeezed_channels, kernel_size=1) - self._se_reduce_padding = _make_same_padder(self._se_reduce, (1, 1)) + self._se_reduce_padding = _make_same_padder(self._se_reduce, [1, 1]) self._se_expand = conv_type(in_channels=num_squeezed_channels, out_channels=oup, kernel_size=1) - self._se_expand_padding = _make_same_padder(self._se_expand, (1, 1)) + self._se_expand_padding = _make_same_padder(self._se_expand, [1, 1]) # Pointwise convolution phase final_oup = out_channels @@ -180,10 +178,10 @@ def forward(self, inputs: torch.Tensor): input_filters, output_filters = self.in_channels, self.out_channels # stride needs to be a list - is_stride_one = all([s == 1 for s in self.stride]) + is_stride_one = self.stride == 1 if isinstance(self.stride, int) else all([s == 1 for s in self.stride]) if self.id_skip and is_stride_one and input_filters == output_filters: - # The combination of skip connection and drop connect brings about stochastic depth. + # the combination of skip connection and drop connect brings about stochastic depth. if self.drop_connect_rate: x = drop_connect(x, p=self.drop_connect_rate, training=self.training) x = x + inputs # skip connection @@ -255,7 +253,7 @@ def __init__( super().__init__() if spatial_dims not in (1, 2, 3): - raise AssertionError("spatial_dims can only be 1, 2 or 3.") + raise ValueError("spatial_dims can only be 1, 2 or 3.") # select the type of N-Dimensional layers to use # these are based on spatial dims and selected from MONAI factories @@ -269,33 +267,37 @@ def __init__( blocks_args = _decode_block_list(blocks_args_str) # checks for successful decoding of blocks_args_str - assert isinstance(blocks_args, list), "blocks_args should be a list" - assert len(blocks_args) > 0, "block args must be greater than 0" + if not isinstance(blocks_args, list): + raise ValueError("blocks_args should be a list") + + if len(blocks_args) < 1: + raise ValueError("block args must be greater than 0") self._blocks_args = blocks_args self.num_classes = num_classes self.in_channels = in_channels + self.drop_connect_rate = drop_connect_rate # expand input image dimensions to list current_image_size = [image_size] * spatial_dims - # Parameters for batch norm + # parameters for batch norm bn_mom = 1 - batch_norm_momentum # 1 - bn_m to convert tensorflow's arg to pytorch bn compatible bn_eps = batch_norm_epsilon # Stem - stride = [2] + stride = 2 out_channels = _round_filters(32, width_coefficient, depth_divisor) # number of output channels self._conv_stem = conv_type(self.in_channels, out_channels, kernel_size=3, stride=stride, bias=False) self._conv_stem_padding = _make_same_padder(self._conv_stem, current_image_size) self._bn0 = batchnorm_type(num_features=out_channels, momentum=bn_mom, eps=bn_eps) current_image_size = _calculate_output_image_size(current_image_size, stride) - # Build MBConv blocks + # build MBConv blocks self._blocks = nn.Sequential() num_blocks = 0 - # Update block input and output filters based on depth multiplier. + # update block input and output filters based on depth multiplier. for idx, block_args in enumerate(self._blocks_args): block_args = block_args._replace( input_filters=_round_filters(block_args.input_filters, width_coefficient, depth_divisor), @@ -307,15 +309,15 @@ def __init__( # calculate the total number of blocks - needed for drop_connect estimation num_blocks += block_args.num_repeat - # Create and add MBConvBlocks to self._blocks + # create and add MBConvBlocks to self._blocks idx = 0 # block index counter for block_args in self._blocks_args: - drop_connect_rate = drop_connect_rate - if drop_connect_rate: - drop_connect_rate *= float(idx) / num_blocks # scale drop connect_rate + blk_drop_connect_rate = self.drop_connect_rate + if blk_drop_connect_rate: + blk_drop_connect_rate *= float(idx) / num_blocks # scale drop connect_rate - # The first block needs to take care of stride and filter size increase. + # the first block needs to take care of stride and filter size increase. self._blocks.add_module( str(idx), MBConvBlock( @@ -330,20 +332,21 @@ def __init__( block_args.id_skip, batch_norm_momentum, batch_norm_epsilon, - drop_connect_rate=drop_connect_rate, + drop_connect_rate=blk_drop_connect_rate, ), ) idx += 1 # increment blocks index counter current_image_size = _calculate_output_image_size(current_image_size, block_args.stride) if block_args.num_repeat > 1: # modify block_args to keep same output size - block_args = block_args._replace(input_filters=block_args.output_filters, stride=[1]) + block_args = block_args._replace(input_filters=block_args.output_filters, stride=1) - # Repeat block for num_repeat required + # repeat block for num_repeat required for _ in range(block_args.num_repeat - 1): - drop_connect_rate = drop_connect_rate - if drop_connect_rate: - drop_connect_rate *= float(idx) / num_blocks # scale drop connect_rate + blk_drop_connect_rate = self.drop_connect_rate + if blk_drop_connect_rate: + blk_drop_connect_rate *= float(idx) / num_blocks # scale drop connect_rate + self._blocks.add_module( str(idx), MBConvBlock( @@ -358,13 +361,14 @@ def __init__( block_args.id_skip, batch_norm_momentum, batch_norm_epsilon, - drop_connect_rate=drop_connect_rate, + drop_connect_rate=blk_drop_connect_rate, ), ) idx += 1 # increment blocks index counter - # Sanity check to see if len(self._blocks) equal expected num_blocks - assert len(self._blocks) == num_blocks + # sanity check to see if len(self._blocks) equal expected num_blocks + if len(self._blocks) != num_blocks: + raise ValueError("number of blocks created != num_blocks") # Head head_in_channels = block_args.output_filters @@ -373,7 +377,7 @@ def __init__( self._conv_head_padding = _make_same_padder(self._conv_head, current_image_size) self._bn1 = batchnorm_type(num_features=out_channels, momentum=bn_mom, eps=bn_eps) - # Final linear layer + # final linear layer self._avg_pooling = adaptivepool_type(1) self._dropout = nn.Dropout(dropout_rate) self._fc = nn.Linear(out_channels, self.num_classes) @@ -407,7 +411,6 @@ def forward(self, inputs: torch.Tensor): A torch Tensor of classification prediction in shape ``(Batch, num_classes)``. """ - # Convolution layers # Stem x = self._conv_stem(self._conv_stem_padding(inputs)) x = self._swish(self._bn0(x)) @@ -486,12 +489,15 @@ def __init__( "r4_k5_s22_e6_i112_o192_se0.25", "r1_k3_s11_e6_i192_o320_se0.25", ] - assert model_name in efficientnet_params.keys(), "model_name should be one of {} ".format( - ", ".join(efficientnet_params.keys()) - ) + if model_name not in efficientnet_params.keys(): + raise ValueError( + "invalid model_name {} found, should be one of {} ".format( + model_name, ", ".join(efficientnet_params.keys()) + ) + ) # get network parameters - wc, dc, isize, dr = efficientnet_params[model_name] + wc, dc, isize, dout_r, dconnect_r = efficientnet_params[model_name] # create model and initialize random weights model = super(EfficientNetBN, self).__init__( @@ -501,8 +507,9 @@ def __init__( num_classes=num_classes, width_coefficient=wc, depth_coefficient=dc, - dropout_rate=dr, + dropout_rate=dout_r, image_size=isize, + drop_connect_rate=dconnect_r, ) # attempt to load pretrained @@ -539,17 +546,17 @@ def _load_state_dict(model: nn.Module, model_name: str, progress: bool, load_fc: if load_fc: ret = model.load_state_dict(state_dict, strict=False) - assert not ret.missing_keys, "Missing keys when loading pretrained weights: {}".format(ret.missing_keys) + if ret.missing_keys: + raise ValueError("Found missing keys when loading pretrained weights: {}".format(ret.missing_keys)) else: state_dict.pop("_fc.weight") state_dict.pop("_fc.bias") ret = model.load_state_dict(state_dict, strict=False) - assert set(ret.missing_keys) == { - "_fc.weight", - "_fc.bias", - }, "Missing keys when loading pretrained weights: {}".format(ret.missing_keys) + if set(ret.missing_keys) != {"_fc.weight", "_fc.bias"}: + raise ValueError("Found missing keys when loading pretrained weights: {}".format(ret.missing_keys)) - assert not ret.unexpected_keys, "Missing keys when loading pretrained weights: {}".format(ret.unexpected_keys) + if ret.unexpected_keys: + raise ValueError("Missing keys when loading pretrained weights: {}".format(ret.unexpected_keys)) def get_efficientnet_image_size(model_name: str) -> int: @@ -563,10 +570,14 @@ def get_efficientnet_image_size(model_name: str) -> int: Image size for single spatial dimension as integer. """ - assert model_name in efficientnet_params.keys(), "model_name should be one of {} ".format( - ", ".join(efficientnet_params.keys()) - ) - _, _, res, _ = efficientnet_params[model_name] + if model_name not in efficientnet_params.keys(): + raise ValueError( + "invalid model_name {} found, should be one of {} ".format( + model_name, ", ".join(efficientnet_params.keys()) + ) + ) + + _, _, res, _, _ = efficientnet_params[model_name] return res @@ -629,17 +640,20 @@ def drop_connect(inputs: torch.Tensor, p: float, training: bool) -> torch.Tensor Returns: output: output tensor after applying drop connection. """ - assert 0 <= p <= 1, "p must be in range of [0,1]" + if p < 0 or p > 1: + raise ValueError("p must be in range of [0, 1], found {}".format(p)) if not training: return inputs batch_size: int = inputs.shape[0] keep_prob: float = 1 - p + num_dims: int = len(inputs.shape) - 2 + + random_tensor_shape = [batch_size, 1] + [1] * num_dims # generate binary_tensor mask according to probability (p for 0, 1-p for 1) - # random_tensor = keep_prob - random_tensor: torch.Tensor = torch.rand([batch_size, 1, 1, 1], dtype=inputs.dtype, device=inputs.device) + random_tensor: torch.Tensor = torch.rand(random_tensor_shape, dtype=inputs.dtype, device=inputs.device) random_tensor += keep_prob binary_tensor: torch.Tensor = torch.floor(random_tensor) @@ -648,7 +662,7 @@ def drop_connect(inputs: torch.Tensor, p: float, training: bool) -> torch.Tensor return output -def _calculate_output_image_size(input_image_size, stride): +def _calculate_output_image_size(input_image_size, stride: Union[int, Tuple[int]]): """ Calculates the output image size when using _make_same_padder with a stride. Necessary for static padding. @@ -664,15 +678,19 @@ def _calculate_output_image_size(input_image_size, stride): return None num_dims = len(input_image_size) - assert isinstance(stride, list) + if isinstance(stride, tuple): + all_strides_equal = all([stride[0] == d for d in stride]) + if not all_strides_equal: + raise ValueError("Unequal strides are not possible, got {}".format(stride)) - if len(stride) != len(input_image_size): - stride = stride * num_dims + stride = stride[0] - return [int(math.ceil(im_sz / st)) for im_sz, st in zip(input_image_size, stride)] + return [int(math.ceil(im_sz / stride)) for im_sz in input_image_size] -def _get_same_padding_conv_nd(image_size, kernel_size, dilation, stride): +def _get_same_padding_conv_nd( + image_size: List[int], kernel_size: Tuple[int, ...], dilation: Tuple[int, ...], stride: Tuple[int, ...] +) -> List[int]: """ Helper for getting padding (nn.ConstantPadNd) to be used to get SAME padding conv operations similar to Tensorflow's SAME padding. @@ -690,7 +708,7 @@ def _get_same_padding_conv_nd(image_size, kernel_size, dilation, stride): """ num_dims = len(kernel_size) - # additional checks to populate dilation and stride (in case they are single entry list) + # additional checks to populate dilation and stride (in case they are single entry tuple) if len(dilation) == 1: dilation = dilation * num_dims @@ -698,20 +716,20 @@ def _get_same_padding_conv_nd(image_size, kernel_size, dilation, stride): stride = stride * num_dims # equation to calculate (pad^+ + pad^-) size - _pad_size = [ + _pad_size: List[int] = [ max((math.ceil(_i_s / _s) - 1) * _s + (_k_s - 1) * _d + 1 - _i_s, 0) for _i_s, _k_s, _d, _s in zip(image_size, kernel_size, dilation, stride) ] # distribute paddings into pad^+ and pad^- following Tensorflow's same padding strategy - _paddings = [(_p // 2, _p - _p // 2) for _p in _pad_size] + _paddings: List[Tuple[int, int]] = [(_p // 2, _p - _p // 2) for _p in _pad_size] # unroll list of tuples to tuples, # reversed as nn.ConstantPadXd expects paddings starting with last dimenion - _paddings = [outer for inner in reversed(_paddings) for outer in inner] - return _paddings + _paddings_ret: List[int] = [outer for inner in reversed(_paddings) for outer in inner] + return _paddings_ret -def _make_same_padder(conv_op, image_size): +def _make_same_padder(conv_op, image_size: List[int]): """ Helper for initializing ConstantPadNd with SAME padding similar to Tensorflow. Uses output of _get_same_padding_conv_nd() to get the padding size. @@ -761,7 +779,7 @@ def _decode_block_list(string_list): ) BlockArgs.__new__.__defaults__ = (None,) * len(BlockArgs._fields) - def _decode_block_string(block_string): + def _decode_block_string(block_string: str): """ Get a block through a string notation of arguments. @@ -772,8 +790,6 @@ def _decode_block_string(block_string): Returns: BlockArgs: namedtuple defined at the top of this function. """ - assert isinstance(block_string, str) - ops = block_string.split("_") options = {} for op in ops: @@ -782,15 +798,19 @@ def _decode_block_string(block_string): key, value = splits[:2] options[key] = value - # Check stride - assert ("s" in options and len(options["s"]) == 1) or ( - len(options["s"]) == 2 and options["s"][0] == options["s"][1] + # check stride + stride_check = ( + ("s" in options and len(options["s"]) == 1) + or (len(options["s"]) == 2 and options["s"][0] == options["s"][1]) + or (len(options["s"]) == 3 and options["s"][0] == options["s"][1] and options["s"][0] == options["s"][2]) ) + if not stride_check: + raise ValueError("invalid stride option recieved") return BlockArgs( num_repeat=int(options["r"]), kernel_size=int(options["k"]), - stride=[int(options["s"][0])], + stride=int(options["s"][0]), expand_ratio=int(options["e"]), input_filters=int(options["i"]), output_filters=int(options["o"]), @@ -798,7 +818,9 @@ def _decode_block_string(block_string): id_skip=("noskip" not in block_string), ) - assert isinstance(string_list, list) + if not isinstance(string_list, list): + raise ValueError("string_list should be a list") + blocks_args = [] for b_s in string_list: blocks_args.append(_decode_block_string(b_s)) diff --git a/tests/test_activations.py b/tests/test_activations.py index 1614642d6d..5ed9ec2046 100644 --- a/tests/test_activations.py +++ b/tests/test_activations.py @@ -48,6 +48,15 @@ ] TEST_CASE_5 = [ + "memswish", + torch.tensor([[[[-10, -8, -6, -4, -2], [0, 2, 4, 6, 8]]]], dtype=torch.float32), + torch.tensor( + [[[[-4.54e-04, -2.68e-03, -1.48e-02, -7.19e-02, -2.38e-01], [0.00e00, 1.76e00, 3.93e00, 5.99e00, 8.00e00]]]] + ), + (1, 1, 2, 5), +] + +TEST_CASE_6 = [ "mish", torch.tensor([[[[-10, -8, -6, -4, -2], [0, 2, 4, 6, 8]]]], dtype=torch.float32), torch.tensor( @@ -64,7 +73,7 @@ def test_value_shape(self, input_param, img, out, expected_shape): torch.testing.assert_allclose(result, out) self.assertTupleEqual(result.shape, expected_shape) - @parameterized.expand([TEST_CASE_4, TEST_CASE_5]) + @parameterized.expand([TEST_CASE_4, TEST_CASE_5, TEST_CASE_6]) def test_monai_activations_value_shape(self, input_param, img, out, expected_shape): act = Act[input_param]() result = act(img) diff --git a/tests/test_efficientnet.py b/tests/test_efficientnet.py index babc80834e..a1fc3ac424 100644 --- a/tests/test_efficientnet.py +++ b/tests/test_efficientnet.py @@ -18,9 +18,9 @@ from parameterized import parameterized from monai.networks import eval_mode -from monai.networks.nets import EfficientNetBN, get_efficientnet_image_size +from monai.networks.nets import EfficientNetBN, drop_connect, get_efficientnet_image_size from monai.utils import optional_import -from tests.utils import test_script_save +from tests.utils import skip_if_quick, test_pretrained_networks, test_script_save if TYPE_CHECKING: import torchvision @@ -114,7 +114,7 @@ def make_shape_cases(models, spatial_dims, batches, pretrained, in_channels=3, n "in_channels": 3, "num_classes": 1000, }, - os.path.join(os.path.dirname(__file__), "testing_data/kitty_test.jpg"), + os.path.join(os.path.dirname(__file__), "testing_data", "kitty_test.jpg"), 285, # ~ Egyptian cat ), ( @@ -126,7 +126,7 @@ def make_shape_cases(models, spatial_dims, batches, pretrained, in_channels=3, n "in_channels": 3, "num_classes": 1000, }, - os.path.join(os.path.dirname(__file__), "testing_data/kitty_test.jpg"), + os.path.join(os.path.dirname(__file__), "testing_data", "kitty_test.jpg"), 285, # ~ Egyptian cat ), ] @@ -177,19 +177,29 @@ class TestEFFICIENTNET(unittest.TestCase): def test_shape(self, input_param, input_shape, expected_shape): device = "cuda" if torch.cuda.is_available() else "cpu" print(input_param) + + # initialize model net = EfficientNetBN(**input_param).to(device) + + # run inference with random tensor with eval_mode(net): result = net(torch.randn(input_shape).to(device)) + + # check output shape self.assertEqual(result.shape, expected_shape) @parameterized.expand(CASES_KITTY_TRAINED) + @skip_if_quick @skipUnless(has_torchvision, "Requires `torchvision` package.") @skipUnless(has_pil, "Requires `pillow` package.") def test_kitty_pretrained(self, input_param, image_path, expected_label): device = "cuda" if torch.cuda.is_available() else "cpu" - # Open image + + # open image image_size = get_efficientnet_image_size(input_param["model_name"]) img = PIL.Image.open(image_path) + + # defin ImageNet transform tfms = torchvision.transforms.Compose( [ torchvision.transforms.Resize(image_size), @@ -198,15 +208,47 @@ def test_kitty_pretrained(self, input_param, image_path, expected_label): torchvision.transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ] ) + + # preprocess and prepare image tensor img = tfms(img).unsqueeze(0).to(device) - net = EfficientNetBN(**input_param).to(device) + + # initialize a pretrained model + net = test_pretrained_networks(EfficientNetBN, input_param, device) + + # run inference with eval_mode(net): result = net(img) pred_label = torch.argmax(result, dim=-1) + + # check output self.assertEqual(pred_label, expected_label) + def test_drop_connect_layer(self): + p_list = [float(d + 1) / 10 for d in range(9)] + + # testing 1D, 2D and 3D shape + for rand_tensor_shape in [(512, 16, 4), (384, 16, 4, 4), (256, 16, 4, 4, 4)]: + + # test validation mode, out tensor == in tensor + training = False + for p in p_list: + in_tensor = torch.rand(rand_tensor_shape) + out_tensor = drop_connect(in_tensor, p, training=training) + self.assertTrue(torch.equal(out_tensor, in_tensor)) + + # test training mode, sum(out tensor != in tensor)/out_tensor.size() == p + training = True + for p in p_list: + in_tensor = torch.rand(rand_tensor_shape) + out_tensor = drop_connect(in_tensor, p, training=training) + + p_calculated = 1 - torch.sum(torch.isclose(in_tensor, out_tensor * (1 - p))) / in_tensor.numel() + p_calculated = p_calculated.cpu().numpy() + # use rounding to 1 decimal place to account for rounding errors due to finite set in/out + self.assertAlmostEqual(p_calculated, p, places=1) + def test_ill_arg(self): - with self.assertRaises(AssertionError): + with self.assertRaises(ValueError): # wrong spatial_dims EfficientNetBN(model_name="efficientnet-b0", spatial_dims=4) # wrong model_name From d83a053f1c14ea4f90787bef1be4263e00076d5c Mon Sep 17 00:00:00 2001 From: masadcv Date: Wed, 7 Apr 2021 01:19:39 +0100 Subject: [PATCH 09/16] fix failing unittest, CC0-license image for test Signed-off-by: masadcv --- tests/test_efficientnet.py | 48 +++++++++++++++++++----------- tests/testing_data/kitty_test.jpg | Bin 20045 -> 61168 bytes 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/tests/test_efficientnet.py b/tests/test_efficientnet.py index a1fc3ac424..eaeb5942dd 100644 --- a/tests/test_efficientnet.py +++ b/tests/test_efficientnet.py @@ -104,6 +104,7 @@ def make_shape_cases(models, spatial_dims, batches, pretrained, in_channels=3, n # pretrained=True cases # tabby kitty test with pretrained model # needs 'testing_data/kitty_test.jpg' +# image from: https://commons.wikimedia.org/wiki/File:Tabby_cat_with_blue_eyes-3336579.jpg CASES_KITTY_TRAINED = [ ( { @@ -115,7 +116,19 @@ def make_shape_cases(models, spatial_dims, batches, pretrained, in_channels=3, n "num_classes": 1000, }, os.path.join(os.path.dirname(__file__), "testing_data", "kitty_test.jpg"), - 285, # ~ Egyptian cat + 282, # ~ tiger cat + ), + ( + { + "model_name": "efficientnet-b3", + "pretrained": True, + "progress": False, + "spatial_dims": 2, + "in_channels": 3, + "num_classes": 1000, + }, + os.path.join(os.path.dirname(__file__), "testing_data", "kitty_test.jpg"), + 282, # ~ tiger cat ), ( { @@ -127,7 +140,7 @@ def make_shape_cases(models, spatial_dims, batches, pretrained, in_channels=3, n "num_classes": 1000, }, os.path.join(os.path.dirname(__file__), "testing_data", "kitty_test.jpg"), - 285, # ~ Egyptian cat + 282, # ~ tiger cat ), ] @@ -143,11 +156,11 @@ def make_shape_cases(models, spatial_dims, batches, pretrained, in_channels=3, n ) ) # 3D -# CASES_VARIATIONS.extend( -# make_shape_cases( -# models=[SEL_MODELS[0]], spatial_dims=[3], batches=[1], pretrained=[False], in_channels=3, num_classes=10 -# ) -# ) +CASES_VARIATIONS.extend( + make_shape_cases( + models=[SEL_MODELS[0]], spatial_dims=[3], batches=[1], pretrained=[False], in_channels=3, num_classes=10 + ) +) # change in_channels test # 1 channel @@ -165,11 +178,11 @@ def make_shape_cases(models, spatial_dims, batches, pretrained, in_channels=3, n ) ) # 3D -# CASES_VARIATIONS.extend( -# make_shape_cases( -# models=[SEL_MODELS[0]], spatial_dims=[3], batches=[1], pretrained=[False], in_channels=1, num_classes=1000 -# ) -# ) +CASES_VARIATIONS.extend( + make_shape_cases( + models=[SEL_MODELS[0]], spatial_dims=[3], batches=[1], pretrained=[False], in_channels=1, num_classes=1000 + ) +) class TestEFFICIENTNET(unittest.TestCase): @@ -225,27 +238,28 @@ def test_kitty_pretrained(self, input_param, image_path, expected_label): def test_drop_connect_layer(self): p_list = [float(d + 1) / 10 for d in range(9)] - # testing 1D, 2D and 3D shape for rand_tensor_shape in [(512, 16, 4), (384, 16, 4, 4), (256, 16, 4, 4, 4)]: # test validation mode, out tensor == in tensor training = False for p in p_list: - in_tensor = torch.rand(rand_tensor_shape) + in_tensor = torch.rand(rand_tensor_shape) + 0.1 out_tensor = drop_connect(in_tensor, p, training=training) self.assertTrue(torch.equal(out_tensor, in_tensor)) # test training mode, sum(out tensor != in tensor)/out_tensor.size() == p + # use tolerance of 0.175 to account for rounding errors due to finite set in/out + tol = 0.175 training = True for p in p_list: - in_tensor = torch.rand(rand_tensor_shape) + in_tensor = torch.rand(rand_tensor_shape) + 0.1 out_tensor = drop_connect(in_tensor, p, training=training) p_calculated = 1 - torch.sum(torch.isclose(in_tensor, out_tensor * (1 - p))) / in_tensor.numel() p_calculated = p_calculated.cpu().numpy() - # use rounding to 1 decimal place to account for rounding errors due to finite set in/out - self.assertAlmostEqual(p_calculated, p, places=1) + + self.assertTrue(abs(p_calculated - p) < tol) def test_ill_arg(self): with self.assertRaises(ValueError): diff --git a/tests/testing_data/kitty_test.jpg b/tests/testing_data/kitty_test.jpg index ee94ced01e867507f3334d4f22011c03681449d3..f103760de515b22696bdd5a27e6a7b59d244eec8 100644 GIT binary patch literal 61168 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 Wof+B_Dv9sKzNP#`qPy-+AOG17 Date: Thu, 8 Apr 2021 02:24:19 +0100 Subject: [PATCH 10/16] refactoring code for review Signed-off-by: masadcv --- monai/networks/nets/efficientnet.py | 301 ++++++++++++++-------------- 1 file changed, 151 insertions(+), 150 deletions(-) diff --git a/monai/networks/nets/efficientnet.py b/monai/networks/nets/efficientnet.py index 7ee20ffa33..e0bdd03370 100644 --- a/monai/networks/nets/efficientnet.py +++ b/monai/networks/nets/efficientnet.py @@ -14,7 +14,7 @@ import operator import re from functools import reduce -from typing import List, Tuple, Type, Union +from typing import List, Optional, Tuple, Type, Union import torch from torch import nn @@ -44,14 +44,14 @@ def __init__( in_channels: int, out_channels: int, kernel_size: int, - stride: Union[int, Tuple[int]], + stride: int, image_size: List[int], expand_ratio: int, - se_ratio: float, - id_skip: bool = True, - batch_norm_momentum: float = 0.99, - batch_norm_epsilon: float = 1e-3, - drop_connect_rate: float = 0.2, + se_ratio: Optional[float] = None, + id_skip: Optional[bool] = True, + batch_norm_momentum: Optional[float] = 0.99, + batch_norm_epsilon: Optional[float] = 1e-3, + drop_connect_rate: Optional[float] = 0.2, ) -> None: """ Mobile Inverted Residual Bottleneck Block. @@ -145,8 +145,7 @@ def forward(self, inputs: torch.Tensor): """MBConvBlock"s forward function. Args: - inputs (tensor): Input tensor. - drop_connect_rate (bool): Drop connect rate (float, between 0 and 1). + inputs: Input tensor. Returns: Output of this block after processing. @@ -175,12 +174,7 @@ def forward(self, inputs: torch.Tensor): x = self._bn2(x) # Skip connection and drop connect - input_filters, output_filters = self.in_channels, self.out_channels - - # stride needs to be a list - is_stride_one = self.stride == 1 if isinstance(self.stride, int) else all([s == 1 for s in self.stride]) - - if self.id_skip and is_stride_one and input_filters == output_filters: + if self.id_skip and self.stride == 1 and self.in_channels == self.out_channels: # the combination of skip connection and drop connect brings about stochastic depth. if self.drop_connect_rate: x = drop_connect(x, p=self.drop_connect_rate, training=self.training) @@ -210,7 +204,7 @@ def __init__( batch_norm_momentum: float = 0.99, batch_norm_epsilon: float = 1e-3, drop_connect_rate: float = 0.2, - depth_divisor=8, + depth_divisor: int = 8, ) -> None: """ EfficientNet based on `Rethinking Model Scaling for Convolutional Neural Networks `_. @@ -268,10 +262,10 @@ def __init__( # checks for successful decoding of blocks_args_str if not isinstance(blocks_args, list): - raise ValueError("blocks_args should be a list") + raise ValueError("blocks_args must be a list") - if len(blocks_args) < 1: - raise ValueError("block args must be greater than 0") + if blocks_args == []: + raise ValueError("block_args must be non-empty") self._blocks_args = blocks_args self.num_classes = num_classes @@ -297,7 +291,7 @@ def __init__( self._blocks = nn.Sequential() num_blocks = 0 - # update block input and output filters based on depth multiplier. + # update baseline blocks to input/output filters and number of repeats based on width and depth multipliers. for idx, block_args in enumerate(self._blocks_args): block_args = block_args._replace( input_filters=_round_filters(block_args.input_filters, width_coefficient, depth_divisor), @@ -321,17 +315,17 @@ def __init__( self._blocks.add_module( str(idx), MBConvBlock( - spatial_dims, - block_args.input_filters, - block_args.output_filters, - block_args.kernel_size, - block_args.stride, - current_image_size, - block_args.expand_ratio, - block_args.se_ratio, - block_args.id_skip, - batch_norm_momentum, - batch_norm_epsilon, + spatial_dims=spatial_dims, + in_channels=block_args.input_filters, + out_channels=block_args.output_filters, + kernel_size=block_args.kernel_size, + stride=block_args.stride, + image_size=current_image_size, + expand_ratio=block_args.expand_ratio, + se_ratio=block_args.se_ratio, + id_skip=block_args.id_skip, + batch_norm_momentum=batch_norm_momentum, + batch_norm_epsilon=batch_norm_epsilon, drop_connect_rate=blk_drop_connect_rate, ), ) @@ -350,17 +344,17 @@ def __init__( self._blocks.add_module( str(idx), MBConvBlock( - spatial_dims, - block_args.input_filters, - block_args.output_filters, - block_args.kernel_size, - block_args.stride, - current_image_size, - block_args.expand_ratio, - block_args.se_ratio, - block_args.id_skip, - batch_norm_momentum, - batch_norm_epsilon, + spatial_dims=spatial_dims, + in_channels=block_args.input_filters, + out_channels=block_args.output_filters, + kernel_size=block_args.kernel_size, + stride=block_args.stride, + image_size=current_image_size, + expand_ratio=block_args.expand_ratio, + se_ratio=block_args.se_ratio, + id_skip=block_args.id_skip, + batch_norm_momentum=batch_norm_momentum, + batch_norm_epsilon=batch_norm_epsilon, drop_connect_rate=blk_drop_connect_rate, ), ) @@ -491,7 +485,7 @@ def __init__( ] if model_name not in efficientnet_params.keys(): raise ValueError( - "invalid model_name {} found, should be one of {} ".format( + "invalid model_name {} found, must be one of {} ".format( model_name, ", ".join(efficientnet_params.keys()) ) ) @@ -530,35 +524,6 @@ def __init__( ) -def _load_state_dict(model: nn.Module, model_name: str, progress: bool, load_fc: bool) -> None: - url_map = { - "efficientnet-b0": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b0-355c32eb.pth", - "efficientnet-b1": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b1-f1951068.pth", - "efficientnet-b2": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b2-8bb594d6.pth", - "efficientnet-b3": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b3-5fb5a3c3.pth", - "efficientnet-b4": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b4-6ed6700e.pth", - "efficientnet-b5": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b5-b6417697.pth", - "efficientnet-b6": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b6-c76e70fd.pth", - "efficientnet-b7": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b7-dcc49843.pth", - } - model_url = url_map[model_name] - state_dict = model_zoo.load_url(model_url, progress=progress) - - if load_fc: - ret = model.load_state_dict(state_dict, strict=False) - if ret.missing_keys: - raise ValueError("Found missing keys when loading pretrained weights: {}".format(ret.missing_keys)) - else: - state_dict.pop("_fc.weight") - state_dict.pop("_fc.bias") - ret = model.load_state_dict(state_dict, strict=False) - if set(ret.missing_keys) != {"_fc.weight", "_fc.bias"}: - raise ValueError("Found missing keys when loading pretrained weights: {}".format(ret.missing_keys)) - - if ret.unexpected_keys: - raise ValueError("Missing keys when loading pretrained weights: {}".format(ret.unexpected_keys)) - - def get_efficientnet_image_size(model_name: str) -> int: """ Get the input image size for a given efficientnet model. @@ -572,66 +537,25 @@ def get_efficientnet_image_size(model_name: str) -> int: """ if model_name not in efficientnet_params.keys(): raise ValueError( - "invalid model_name {} found, should be one of {} ".format( - model_name, ", ".join(efficientnet_params.keys()) - ) + "invalid model_name {} found, must be one of {} ".format(model_name, ", ".join(efficientnet_params.keys())) ) _, _, res, _, _ = efficientnet_params[model_name] return res -def _round_filters(filters, width_coefficient=None, depth_divisor=None): - """ - Calculate and round number of filters based on width coefficient multiplier and depth divisor. - - Args: - filters: number of input filters. - width_coefficient: width coefficient for model. - depth_divisor: depth divisor to use. - - Returns: - new_filters: new number of filters after calculation. - """ - multiplier = width_coefficient - if not multiplier: - return filters - divisor = depth_divisor - filters *= multiplier - - # follow the formula transferred from official TensorFlow implementation - new_filters = max(divisor, int(filters + divisor / 2) // divisor * divisor) - if new_filters < 0.9 * filters: # prevent rounding by more than 10% - new_filters += divisor - return int(new_filters) - - -def _round_repeats(repeats, depth_coefficient=None): - """ - Re-calculate module's repeat number of a block based on depth coefficient multiplier. - - Args: - repeats: number of original repeats. - depth_coefficient: depth coefficient for model. - - Returns: - new repeat: new number of repeat after calculating. - """ - multiplier = depth_coefficient - if not multiplier: - return repeats - # follow the formula transferred from official TensorFlow implementation - return int(math.ceil(multiplier * repeats)) - - def drop_connect(inputs: torch.Tensor, p: float, training: bool) -> torch.Tensor: """ Drop connect layer that drops individual connections. Differs from dropout as dropconnect drops connections instead of whole neurons as in dropout. + Based on `Deep Networks with Stochastic Depth `_. Adapted from `Official Tensorflow EfficientNet utils `_. + This function is generalized for MONAI's N-Dimensional spatial activations + e.g. 1D activations [B, C, H], 2D activations [B, C, H, W] and 3D activations [B, C, H, W, D] + Args: input: input tensor with [B, C, dim_1, dim_2, ..., dim_N] where N=spatial_dims. p: probability to use for dropping connections. @@ -640,7 +564,7 @@ def drop_connect(inputs: torch.Tensor, p: float, training: bool) -> torch.Tensor Returns: output: output tensor after applying drop connection. """ - if p < 0 or p > 1: + if p < 0.0 or p > 1.0: raise ValueError("p must be in range of [0, 1], found {}".format(p)) if not training: @@ -650,42 +574,48 @@ def drop_connect(inputs: torch.Tensor, p: float, training: bool) -> torch.Tensor keep_prob: float = 1 - p num_dims: int = len(inputs.shape) - 2 - random_tensor_shape = [batch_size, 1] + [1] * num_dims + # build dimensions for random tensor, use num_dims to populate appropriate spatial dims + random_tensor_shape: List[int] = [batch_size, 1] + [1] * num_dims # generate binary_tensor mask according to probability (p for 0, 1-p for 1) random_tensor: torch.Tensor = torch.rand(random_tensor_shape, dtype=inputs.dtype, device=inputs.device) random_tensor += keep_prob + # round to form binary tensor binary_tensor: torch.Tensor = torch.floor(random_tensor) + # drop connect using binary tensor output: torch.Tensor = inputs / keep_prob * binary_tensor return output -def _calculate_output_image_size(input_image_size, stride: Union[int, Tuple[int]]): - """ - Calculates the output image size when using _make_same_padder with a stride. - Necessary for static padding. - - Args: - input_image_size: input image/feature spatial size. - stride: Conv2d operation"s stride. - - Returns: - output_image_size: output image/feature spatial size. - """ - if input_image_size is None: - return None - - num_dims = len(input_image_size) - if isinstance(stride, tuple): - all_strides_equal = all([stride[0] == d for d in stride]) - if not all_strides_equal: - raise ValueError("Unequal strides are not possible, got {}".format(stride)) +def _load_state_dict(model: nn.Module, model_name: str, progress: bool, load_fc: bool) -> None: + url_map = { + "efficientnet-b0": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b0-355c32eb.pth", + "efficientnet-b1": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b1-f1951068.pth", + "efficientnet-b2": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b2-8bb594d6.pth", + "efficientnet-b3": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b3-5fb5a3c3.pth", + "efficientnet-b4": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b4-6ed6700e.pth", + "efficientnet-b5": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b5-b6417697.pth", + "efficientnet-b6": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b6-c76e70fd.pth", + "efficientnet-b7": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b7-dcc49843.pth", + } + model_url = url_map[model_name] + state_dict = model_zoo.load_url(model_url, progress=progress) - stride = stride[0] + if load_fc: + ret = model.load_state_dict(state_dict, strict=False) + if ret.missing_keys: + raise ValueError("Found missing keys when loading pretrained weights: {}".format(ret.missing_keys)) + else: + state_dict.pop("_fc.weight") + state_dict.pop("_fc.bias") + ret = model.load_state_dict(state_dict, strict=False) + if set(ret.missing_keys) != {"_fc.weight", "_fc.bias"}: + raise ValueError("Found missing keys when loading pretrained weights: {}".format(ret.missing_keys)) - return [int(math.ceil(im_sz / stride)) for im_sz in input_image_size] + if ret.unexpected_keys: + raise ValueError("Missing keys when loading pretrained weights: {}".format(ret.unexpected_keys)) def _get_same_padding_conv_nd( @@ -723,17 +653,18 @@ def _get_same_padding_conv_nd( # distribute paddings into pad^+ and pad^- following Tensorflow's same padding strategy _paddings: List[Tuple[int, int]] = [(_p // 2, _p - _p // 2) for _p in _pad_size] - # unroll list of tuples to tuples, - # reversed as nn.ConstantPadXd expects paddings starting with last dimenion + # unroll list of tuples to tuples, and then to list + # reversed as nn.ConstantPadXd expects paddings starting with last dimension _paddings_ret: List[int] = [outer for inner in reversed(_paddings) for outer in inner] return _paddings_ret -def _make_same_padder(conv_op, image_size: List[int]): +def _make_same_padder(conv_op: Type[Union[nn.Conv1d, nn.Conv2d, nn.Conv3d]], image_size: List[int]): """ Helper for initializing ConstantPadNd with SAME padding similar to Tensorflow. Uses output of _get_same_padding_conv_nd() to get the padding size. - Generalized for N-Dimensional spatial operatoins e.g. Conv1D, Conv2D, Conv3D + + This function is generalized for MONAI's N-Dimensional spatial operations (e.g. Conv1D, Conv2D, Conv3D) Args: conv_op: nn.ConvNd operation to extract parameters for op from @@ -743,7 +674,7 @@ def _make_same_padder(conv_op, image_size: List[int]): If padding required then nn.ConstandNd() padder initialized to paddings otherwise nn.Identity() """ # calculate padding required - padding = _get_same_padding_conv_nd(image_size, conv_op.kernel_size, conv_op.dilation, conv_op.stride) + padding: List[int] = _get_same_padding_conv_nd(image_size, conv_op.kernel_size, conv_op.dilation, conv_op.stride) # initialize and return padder padder = Pad["constantpad", len(padding) // 2] @@ -753,7 +684,77 @@ def _make_same_padder(conv_op, image_size: List[int]): return nn.Identity() -def _decode_block_list(string_list): +def _round_filters( + filters: int, width_coefficient: Optional[float] = None, depth_divisor: Optional[float] = None +) -> int: + """ + Calculate and round number of filters based on width coefficient multiplier and depth divisor. + + Args: + filters: number of input filters. + width_coefficient: width coefficient for model. + depth_divisor: depth divisor to use. + + Returns: + new_filters: new number of filters after calculation. + """ + multiplier = width_coefficient + if not multiplier: + return filters + divisor = depth_divisor + filters *= multiplier + + # follow the formula transferred from official TensorFlow implementation + new_filters = max(divisor, int(filters + divisor / 2) // divisor * divisor) + if new_filters < 0.9 * filters: # prevent rounding by more than 10% + new_filters += divisor + return int(new_filters) + + +def _round_repeats(repeats: int, depth_coefficient: Optional[float] = None) -> int: + """ + Re-calculate module's repeat number of a block based on depth coefficient multiplier. + + Args: + repeats: number of original repeats. + depth_coefficient: depth coefficient for model. + + Returns: + new repeat: new number of repeat after calculating. + """ + if not depth_coefficient: + return repeats + # follow the formula transferred from official TensorFlow implementation + return int(math.ceil(depth_coefficient * repeats)) + + +def _calculate_output_image_size(input_image_size: List[int], stride: Union[int, Tuple[int]]): + """ + Calculates the output image size when using _make_same_padder with a stride. + Necessary for static padding. + + Args: + input_image_size: input image/feature spatial size. + stride: Conv2d operation"s stride. + + Returns: + output_image_size: output image/feature spatial size. + """ + if input_image_size is None: + return None + + num_dims = len(input_image_size) + if isinstance(stride, tuple): + all_strides_equal = all([stride[0] == s for s in stride]) + if not all_strides_equal: + raise ValueError("unequal strides are not possible, got {}".format(stride)) + + stride = stride[0] + + return [int(math.ceil(im_sz / stride)) for im_sz in input_image_size] + + +def _decode_block_list(string_list: List[str]): """ Decode a list of string notations to specify blocks inside the network. @@ -779,7 +780,7 @@ def _decode_block_list(string_list): ) BlockArgs.__new__.__defaults__ = (None,) * len(BlockArgs._fields) - def _decode_block_string(block_string: str): + def _decode_block_string(block_string: str) -> BlockArgs: """ Get a block through a string notation of arguments. @@ -819,7 +820,7 @@ def _decode_block_string(block_string: str): ) if not isinstance(string_list, list): - raise ValueError("string_list should be a list") + raise ValueError("string_list must be a list") blocks_args = [] for b_s in string_list: From 89febad77c8616503b84491b015ef2db89c4457e Mon Sep 17 00:00:00 2001 From: masadcv Date: Thu, 8 Apr 2021 12:10:37 +0100 Subject: [PATCH 11/16] WIP fix mypy type hint errors Signed-off-by: masadcv --- monai/networks/nets/efficientnet.py | 93 +++++++++++++++++------------ tests/min_tests.py | 1 + tests/test_efficientnet.py | 2 +- 3 files changed, 57 insertions(+), 39 deletions(-) diff --git a/monai/networks/nets/efficientnet.py b/monai/networks/nets/efficientnet.py index e0bdd03370..dd4f92bb2f 100644 --- a/monai/networks/nets/efficientnet.py +++ b/monai/networks/nets/efficientnet.py @@ -9,12 +9,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import collections import math import operator import re from functools import reduce -from typing import List, Optional, Tuple, Type, Union +from typing import List, NamedTuple, Optional, Tuple, Type, Union import torch from torch import nn @@ -47,10 +46,10 @@ def __init__( stride: int, image_size: List[int], expand_ratio: int, - se_ratio: Optional[float] = None, + se_ratio: Optional[float], id_skip: Optional[bool] = True, - batch_norm_momentum: Optional[float] = 0.99, - batch_norm_epsilon: Optional[float] = 1e-3, + batch_norm_momentum: float = 0.99, + batch_norm_epsilon: float = 1e-3, drop_connect_rate: Optional[float] = 0.2, ) -> None: """ @@ -89,10 +88,15 @@ def __init__( self.id_skip = id_skip self.stride = stride self.expand_ratio = expand_ratio - self.has_se = (se_ratio is not None) and (0 < se_ratio <= 1) self.drop_connect_rate = drop_connect_rate - bn_mom = 1 - batch_norm_momentum # pytorch"s difference from tensorflow + if (se_ratio is not None) and (0.0 < se_ratio <= 1.0): + self.has_se = True + self.se_ratio = se_ratio + else: + self.has_se = False + + bn_mom = 1.0 - batch_norm_momentum # pytorch"s difference from tensorflow bn_eps = batch_norm_epsilon # Expansion phase (Inverted Bottleneck) @@ -128,7 +132,7 @@ def __init__( # Squeeze and Excitation layer, if desired if self.has_se: self._se_adaptpool = adaptivepool_type(1) - num_squeezed_channels = max(1, int(in_channels * se_ratio)) + num_squeezed_channels = max(1, int(in_channels * self.se_ratio)) self._se_reduce = conv_type(in_channels=oup, out_channels=num_squeezed_channels, kernel_size=1) self._se_reduce_padding = _make_same_padder(self._se_reduce, [1, 1]) self._se_expand = conv_type(in_channels=num_squeezed_channels, out_channels=oup, kernel_size=1) @@ -659,7 +663,7 @@ def _get_same_padding_conv_nd( return _paddings_ret -def _make_same_padder(conv_op: Type[Union[nn.Conv1d, nn.Conv2d, nn.Conv3d]], image_size: List[int]): +def _make_same_padder(conv_op: Union[nn.Conv1d, nn.Conv2d, nn.Conv3d], image_size: List[int]): """ Helper for initializing ConstantPadNd with SAME padding similar to Tensorflow. Uses output of _get_same_padding_conv_nd() to get the padding size. @@ -684,9 +688,7 @@ def _make_same_padder(conv_op: Type[Union[nn.Conv1d, nn.Conv2d, nn.Conv3d]], ima return nn.Identity() -def _round_filters( - filters: int, width_coefficient: Optional[float] = None, depth_divisor: Optional[float] = None -) -> int: +def _round_filters(filters: int, width_coefficient: Optional[float], depth_divisor: float) -> int: """ Calculate and round number of filters based on width coefficient multiplier and depth divisor. @@ -698,20 +700,22 @@ def _round_filters( Returns: new_filters: new number of filters after calculation. """ - multiplier = width_coefficient - if not multiplier: + + if not width_coefficient: return filters - divisor = depth_divisor - filters *= multiplier + + multiplier: float = width_coefficient + divisor: float = depth_divisor + filters_float: float = filters * multiplier # follow the formula transferred from official TensorFlow implementation - new_filters = max(divisor, int(filters + divisor / 2) // divisor * divisor) - if new_filters < 0.9 * filters: # prevent rounding by more than 10% + new_filters: float = max(divisor, int(filters_float + divisor / 2) // divisor * divisor) + if new_filters < 0.9 * filters_float: # prevent rounding by more than 10% new_filters += divisor return int(new_filters) -def _round_repeats(repeats: int, depth_coefficient: Optional[float] = None) -> int: +def _round_repeats(repeats: int, depth_coefficient: Optional[float]) -> int: """ Re-calculate module's repeat number of a block based on depth coefficient multiplier. @@ -724,6 +728,7 @@ def _round_repeats(repeats: int, depth_coefficient: Optional[float] = None) -> i """ if not depth_coefficient: return repeats + # follow the formula transferred from official TensorFlow implementation return int(math.ceil(depth_coefficient * repeats)) @@ -764,23 +769,35 @@ def _decode_block_list(string_list: List[str]): Returns: blocks_args: a list of BlockArgs namedtuples of block args. """ + # TODO: remove after different python tests have passed on github + # BlockArgs = collections.namedtuple( + # "BlockArgs", + # [ + # "num_repeat", + # "kernel_size", + # "stride", + # "expand_ratio", + # "input_filters", + # "output_filters", + # "se_ratio", + # "id_skip", + # ], + # ) + # BlockArgs.__new__.__defaults__ = (None,) * len(BlockArgs._fields) # type: ignore # Parameters for an individual model block - BlockArgs = collections.namedtuple( - "BlockArgs", - [ - "num_repeat", - "kernel_size", - "stride", - "expand_ratio", - "input_filters", - "output_filters", - "se_ratio", - "id_skip", - ], - ) - BlockArgs.__new__.__defaults__ = (None,) * len(BlockArgs._fields) - - def _decode_block_string(block_string: str) -> BlockArgs: + # namedtuple with defaults for mypy help from: + # https://stackoverflow.com/a/53255358 + class BlockArgs(NamedTuple): + num_repeat: int + kernel_size: int + stride: int + expand_ratio: int + input_filters: int + output_filters: int + id_skip: bool + se_ratio: Optional[float] = None + + def _decode_block_string(block_string: str): """ Get a block through a string notation of arguments. @@ -815,14 +832,14 @@ def _decode_block_string(block_string: str) -> BlockArgs: expand_ratio=int(options["e"]), input_filters=int(options["i"]), output_filters=int(options["o"]), - se_ratio=float(options["se"]) if "se" in options else None, id_skip=("noskip" not in block_string), + se_ratio=float(options["se"]) if "se" in options else None, ) if not isinstance(string_list, list): raise ValueError("string_list must be a list") blocks_args = [] - for b_s in string_list: - blocks_args.append(_decode_block_string(b_s)) + for current_string in string_list: + blocks_args.append(_decode_block_string(current_string)) return blocks_args diff --git a/tests/min_tests.py b/tests/min_tests.py index 98f6d822a7..586956eec0 100644 --- a/tests/min_tests.py +++ b/tests/min_tests.py @@ -33,6 +33,7 @@ def run_testsuit(): "test_cachedataset_parallel", "test_dataset", "test_detect_envelope", + "test_efficientnet", "test_iterable_dataset", "test_ensemble_evaluator", "test_handler_checkpoint_loader", diff --git a/tests/test_efficientnet.py b/tests/test_efficientnet.py index eaeb5942dd..d3a18e83c7 100644 --- a/tests/test_efficientnet.py +++ b/tests/test_efficientnet.py @@ -256,7 +256,7 @@ def test_drop_connect_layer(self): in_tensor = torch.rand(rand_tensor_shape) + 0.1 out_tensor = drop_connect(in_tensor, p, training=training) - p_calculated = 1 - torch.sum(torch.isclose(in_tensor, out_tensor * (1 - p))) / in_tensor.numel() + p_calculated = 1.0 - torch.sum(torch.isclose(in_tensor, out_tensor * (1.0 - p))) / in_tensor.numel() p_calculated = p_calculated.cpu().numpy() self.assertTrue(abs(p_calculated - p) < tol) From 3d2fa094b414bee7bceaab8ffbbcd69c2aa23b02 Mon Sep 17 00:00:00 2001 From: masadcv Date: Thu, 8 Apr 2021 14:30:53 +0100 Subject: [PATCH 12/16] fix cuda test error Signed-off-by: masadcv --- tests/test_efficientnet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_efficientnet.py b/tests/test_efficientnet.py index d3a18e83c7..bb2153a8c7 100644 --- a/tests/test_efficientnet.py +++ b/tests/test_efficientnet.py @@ -256,7 +256,7 @@ def test_drop_connect_layer(self): in_tensor = torch.rand(rand_tensor_shape) + 0.1 out_tensor = drop_connect(in_tensor, p, training=training) - p_calculated = 1.0 - torch.sum(torch.isclose(in_tensor, out_tensor * (1.0 - p))) / in_tensor.numel() + p_calculated = 1.0 - torch.sum(torch.isclose(in_tensor, out_tensor * (1.0 - p))) / float(in_tensor.numel()) p_calculated = p_calculated.cpu().numpy() self.assertTrue(abs(p_calculated - p) < tol) From 410a406fdcb939a65f82a130a695bf54730ea304 Mon Sep 17 00:00:00 2001 From: masadcv Date: Thu, 8 Apr 2021 14:51:06 +0100 Subject: [PATCH 13/16] WIP fix test errors Signed-off-by: masadcv --- tests/test_efficientnet.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_efficientnet.py b/tests/test_efficientnet.py index bb2153a8c7..32a21a939c 100644 --- a/tests/test_efficientnet.py +++ b/tests/test_efficientnet.py @@ -256,7 +256,9 @@ def test_drop_connect_layer(self): in_tensor = torch.rand(rand_tensor_shape) + 0.1 out_tensor = drop_connect(in_tensor, p, training=training) - p_calculated = 1.0 - torch.sum(torch.isclose(in_tensor, out_tensor * (1.0 - p))) / float(in_tensor.numel()) + p_calculated = 1.0 - torch.sum(torch.isclose(in_tensor, out_tensor * (1.0 - p))) / float( + in_tensor.numel() + ) p_calculated = p_calculated.cpu().numpy() self.assertTrue(abs(p_calculated - p) < tol) From 45c2d91b1554354dbc1ccbd1016ba407554a2d64 Mon Sep 17 00:00:00 2001 From: masadcv Date: Thu, 8 Apr 2021 15:16:09 +0100 Subject: [PATCH 14/16] adding non-default shape tests Signed-off-by: masadcv --- tests/test_efficientnet.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_efficientnet.py b/tests/test_efficientnet.py index 32a21a939c..753725a1c9 100644 --- a/tests/test_efficientnet.py +++ b/tests/test_efficientnet.py @@ -201,6 +201,27 @@ def test_shape(self, input_param, input_shape, expected_shape): # check output shape self.assertEqual(result.shape, expected_shape) + @parameterized.expand(CASES_1D + CASES_2D + CASES_3D) + def test_non_default_shapes(self, input_param, input_shape, expected_shape): + device = "cuda" if torch.cuda.is_available() else "cpu" + print(input_param) + + # initialize model + net = EfficientNetBN(**input_param).to(device) + + # override input shape with different variations + num_dims = len(input_shape) - 2 + non_default_sizes = [128, 256, 512] + for candidate_size in non_default_sizes: + input_shape = input_shape[0:2] + (candidate_size,) * num_dims + print(input_shape) + # run inference with random tensor + with eval_mode(net): + result = net(torch.randn(input_shape).to(device)) + + # check output shape + self.assertEqual(result.shape, expected_shape) + @parameterized.expand(CASES_KITTY_TRAINED) @skip_if_quick @skipUnless(has_torchvision, "Requires `torchvision` package.") From d51249a396f1a8512ae4bb93b6f6f9fb1292f459 Mon Sep 17 00:00:00 2001 From: masadcv Date: Thu, 8 Apr 2021 15:28:48 +0100 Subject: [PATCH 15/16] remove 3d case from non-default shape test Signed-off-by: masadcv --- tests/test_efficientnet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_efficientnet.py b/tests/test_efficientnet.py index 753725a1c9..ab9bb4ecb9 100644 --- a/tests/test_efficientnet.py +++ b/tests/test_efficientnet.py @@ -201,7 +201,7 @@ def test_shape(self, input_param, input_shape, expected_shape): # check output shape self.assertEqual(result.shape, expected_shape) - @parameterized.expand(CASES_1D + CASES_2D + CASES_3D) + @parameterized.expand(CASES_1D + CASES_2D) def test_non_default_shapes(self, input_param, input_shape, expected_shape): device = "cuda" if torch.cuda.is_available() else "cpu" print(input_param) From 1774a48019912172f9f98e50a5700b7f65a205d9 Mon Sep 17 00:00:00 2001 From: masadcv Date: Thu, 8 Apr 2021 21:08:00 +0100 Subject: [PATCH 16/16] refactoring and updating docs Signed-off-by: masadcv --- docs/source/networks.rst | 10 ++++ monai/networks/blocks/__init__.py | 2 +- monai/networks/blocks/activation.py | 8 +-- monai/networks/nets/__init__.py | 2 +- monai/networks/nets/efficientnet.py | 88 +++++++++++++++-------------- tests/test_efficientnet.py | 4 +- 6 files changed, 64 insertions(+), 50 deletions(-) diff --git a/docs/source/networks.rst b/docs/source/networks.rst index abf75bda1d..baee107620 100644 --- a/docs/source/networks.rst +++ b/docs/source/networks.rst @@ -35,6 +35,11 @@ Blocks .. autoclass:: Swish :members: +`MemoryEfficientSwish` +~~~~~~~~~~~~~~~~~~~~~~ +.. autoclass:: MemoryEfficientSwish + :members: + `Mish` ~~~~~~ .. autoclass:: Mish @@ -292,6 +297,11 @@ Nets .. autoclass:: DenseNet :members: +`EfficientNet` +~~~~~~~~~~~~~~ +.. autoclass:: EfficientNet + :members: + `SegResNet` ~~~~~~~~~~~ .. autoclass:: SegResNet diff --git a/monai/networks/blocks/__init__.py b/monai/networks/blocks/__init__.py index cdf7bc3f6d..ed6ac12430 100644 --- a/monai/networks/blocks/__init__.py +++ b/monai/networks/blocks/__init__.py @@ -10,7 +10,7 @@ # limitations under the License. from .acti_norm import ADN -from .activation import Mish, Swish +from .activation import MemoryEfficientSwish, Mish, Swish from .aspp import SimpleASPP from .convolutions import Convolution, ResidualUnit from .crf import CRF diff --git a/monai/networks/blocks/activation.py b/monai/networks/blocks/activation.py index 5f62900c3b..f6a04e830e 100644 --- a/monai/networks/blocks/activation.py +++ b/monai/networks/blocks/activation.py @@ -17,7 +17,7 @@ class Swish(nn.Module): r"""Applies the element-wise function: .. math:: - \text{Swish}(x) = x * \text{Sigmoid}(\alpha * x) for constant value alpha. + \text{Swish}(x) = x * \text{Sigmoid}(\alpha * x) ~~~~\text{for constant value}~ \alpha. Citation: Searching for Activation Functions, Ramachandran et al., 2017, https://arxiv.org/abs/1710.05941. @@ -68,15 +68,15 @@ class MemoryEfficientSwish(nn.Module): r"""Applies the element-wise function: .. math:: - \text{Swish}(x) = x * \text{Sigmoid}(\alpha * x) for constant value alpha=1. - - Citation: Searching for Activation Functions, Ramachandran et al., 2017, https://arxiv.org/abs/1710.05941. + \text{Swish}(x) = x * \text{Sigmoid}(\alpha * x) ~~~~\text{for constant value}~ \alpha=1. Memory efficient implementation for training following recommendation from: https://github.com/lukemelas/EfficientNet-PyTorch/issues/18#issuecomment-511677853 Results in ~ 30% memory saving during training as compared to Swish() + Citation: Searching for Activation Functions, Ramachandran et al., 2017, https://arxiv.org/abs/1710.05941. + Shape: - Input: :math:`(N, *)` where `*` means, any number of additional dimensions diff --git a/monai/networks/nets/__init__.py b/monai/networks/nets/__init__.py index bfb3fba6ee..91f46debf6 100644 --- a/monai/networks/nets/__init__.py +++ b/monai/networks/nets/__init__.py @@ -15,7 +15,7 @@ from .classifier import Classifier, Critic, Discriminator from .densenet import DenseNet, DenseNet121, DenseNet169, DenseNet201, DenseNet264 from .dynunet import DynUNet, DynUnet, Dynunet -from .efficientnet import EfficientNetBN, drop_connect, get_efficientnet_image_size +from .efficientnet import EfficientNet, EfficientNetBN, drop_connect, get_efficientnet_image_size from .fullyconnectednet import FullyConnectedNet, VarFullyConnectedNet from .generator import Generator from .highresnet import HighResBlock, HighResNet diff --git a/monai/networks/nets/efficientnet.py b/monai/networks/nets/efficientnet.py index dd4f92bb2f..d8754e3f78 100644 --- a/monai/networks/nets/efficientnet.py +++ b/monai/networks/nets/efficientnet.py @@ -74,7 +74,6 @@ def __init__( [2] https://arxiv.org/abs/1801.04381 (MobileNet v2) [3] https://arxiv.org/abs/1905.02244 (MobileNet v3) """ - super().__init__() # select the type of N-Dimensional layers to use @@ -143,6 +142,9 @@ def __init__( self._project_conv = conv_type(in_channels=oup, out_channels=final_oup, kernel_size=1, bias=False) self._project_conv_padding = _make_same_padder(self._project_conv, image_size) self._bn2 = batchnorm_type(num_features=final_oup, momentum=bn_mom, eps=bn_eps) + + # swish activation to use - using memory efficient swish by default + # can be switched to normal swish using self.set_swish() function call self._swish = Act["memswish"]() def forward(self, inputs: torch.Tensor): @@ -292,8 +294,8 @@ def __init__( current_image_size = _calculate_output_image_size(current_image_size, stride) # build MBConv blocks - self._blocks = nn.Sequential() num_blocks = 0 + self._blocks = nn.Sequential() # update baseline blocks to input/output filters and number of repeats based on width and depth multipliers. for idx, block_args in enumerate(self._blocks_args): @@ -310,10 +312,11 @@ def __init__( # create and add MBConvBlocks to self._blocks idx = 0 # block index counter for block_args in self._blocks_args: - blk_drop_connect_rate = self.drop_connect_rate + + # scale drop connect_rate if blk_drop_connect_rate: - blk_drop_connect_rate *= float(idx) / num_blocks # scale drop connect_rate + blk_drop_connect_rate *= float(idx) / num_blocks # the first block needs to take care of stride and filter size increase. self._blocks.add_module( @@ -339,12 +342,15 @@ def __init__( if block_args.num_repeat > 1: # modify block_args to keep same output size block_args = block_args._replace(input_filters=block_args.output_filters, stride=1) - # repeat block for num_repeat required + # add remaining block repeated num_repeat times for _ in range(block_args.num_repeat - 1): blk_drop_connect_rate = self.drop_connect_rate + + # scale drop connect_rate if blk_drop_connect_rate: - blk_drop_connect_rate *= float(idx) / num_blocks # scale drop connect_rate + blk_drop_connect_rate *= float(idx) / num_blocks + # add blocks self._blocks.add_module( str(idx), MBConvBlock( @@ -384,7 +390,7 @@ def __init__( # can be switched to normal swish using self.set_swish() function call self._swish = Act["memswish"]() - # initialize weights + # initialize weights using Tensorflow's init method from official impl. self._initialize_weights() def set_swish(self, memory_efficient: bool = True) -> None: @@ -487,6 +493,8 @@ def __init__( "r4_k5_s22_e6_i112_o192_se0.25", "r1_k3_s11_e6_i192_o320_se0.25", ] + + # check if model_name is valid model if model_name not in efficientnet_params.keys(): raise ValueError( "invalid model_name {} found, must be one of {} ".format( @@ -495,7 +503,7 @@ def __init__( ) # get network parameters - wc, dc, isize, dout_r, dconnect_r = efficientnet_params[model_name] + weight_coeff, depth_coeff, image_size, drpout_rate, drpconnect_rate = efficientnet_params[model_name] # create model and initialize random weights model = super(EfficientNetBN, self).__init__( @@ -503,11 +511,11 @@ def __init__( spatial_dims=spatial_dims, in_channels=in_channels, num_classes=num_classes, - width_coefficient=wc, - depth_coefficient=dc, - dropout_rate=dout_r, - image_size=isize, - drop_connect_rate=dconnect_r, + width_coefficient=weight_coeff, + depth_coefficient=depth_coeff, + dropout_rate=drpout_rate, + image_size=image_size, + drop_connect_rate=drpconnect_rate, ) # attempt to load pretrained @@ -539,11 +547,13 @@ def get_efficientnet_image_size(model_name: str) -> int: Image size for single spatial dimension as integer. """ + # check if model_name is valid model if model_name not in efficientnet_params.keys(): raise ValueError( "invalid model_name {} found, must be one of {} ".format(model_name, ", ".join(efficientnet_params.keys())) ) + # return input image size (all dims equal so only need to return for one dim) _, _, res, _, _ = efficientnet_params[model_name] return res @@ -571,9 +581,11 @@ def drop_connect(inputs: torch.Tensor, p: float, training: bool) -> torch.Tensor if p < 0.0 or p > 1.0: raise ValueError("p must be in range of [0, 1], found {}".format(p)) + # eval mode: drop_connect is switched off - so return input without modifying if not training: return inputs + # train mode: calculate and apply drop_connect batch_size: int = inputs.shape[0] keep_prob: float = 1 - p num_dims: int = len(inputs.shape) - 2 @@ -604,20 +616,25 @@ def _load_state_dict(model: nn.Module, model_name: str, progress: bool, load_fc: "efficientnet-b6": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b6-c76e70fd.pth", "efficientnet-b7": "https://github.com/lukemelas/EfficientNet-PyTorch/releases/download/1.0/efficientnet-b7-dcc49843.pth", } + # load state dict from url model_url = url_map[model_name] state_dict = model_zoo.load_url(model_url, progress=progress) - if load_fc: + # load state dict into model parameters + if load_fc: # load everything ret = model.load_state_dict(state_dict, strict=False) if ret.missing_keys: raise ValueError("Found missing keys when loading pretrained weights: {}".format(ret.missing_keys)) - else: + else: # skip final FC layers, for transfer learning cases state_dict.pop("_fc.weight") state_dict.pop("_fc.bias") ret = model.load_state_dict(state_dict, strict=False) + + # check if no other keys missing except FC layer parameters if set(ret.missing_keys) != {"_fc.weight", "_fc.bias"}: raise ValueError("Found missing keys when loading pretrained weights: {}".format(ret.missing_keys)) + # check for any unexpected keys if ret.unexpected_keys: raise ValueError("Missing keys when loading pretrained weights: {}".format(ret.unexpected_keys)) @@ -638,11 +655,12 @@ def _get_same_padding_conv_nd( stride: stride for conv operation. Returns: - paddings for ConstantPadXd padder to be used on input tensor to conv op. + paddings for ConstantPadNd padder to be used on input tensor to conv op. """ + # get number of spatial dimensions, corresponds to kernel size length num_dims = len(kernel_size) - # additional checks to populate dilation and stride (in case they are single entry tuple) + # additional checks to populate dilation and stride (in case they are single entry tuples) if len(dilation) == 1: dilation = dilation * num_dims @@ -658,7 +676,7 @@ def _get_same_padding_conv_nd( _paddings: List[Tuple[int, int]] = [(_p // 2, _p - _p // 2) for _p in _pad_size] # unroll list of tuples to tuples, and then to list - # reversed as nn.ConstantPadXd expects paddings starting with last dimension + # reversed as nn.ConstantPadNd expects paddings starting with last dimension _paddings_ret: List[int] = [outer for inner in reversed(_paddings) for outer in inner] return _paddings_ret @@ -729,14 +747,14 @@ def _round_repeats(repeats: int, depth_coefficient: Optional[float]) -> int: if not depth_coefficient: return repeats - # follow the formula transferred from official TensorFlow implementation + # follow the formula transferred from official TensorFlow impl. return int(math.ceil(depth_coefficient * repeats)) def _calculate_output_image_size(input_image_size: List[int], stride: Union[int, Tuple[int]]): """ Calculates the output image size when using _make_same_padder with a stride. - Necessary for static padding. + Required for static padding. Args: input_image_size: input image/feature spatial size. @@ -745,10 +763,10 @@ def _calculate_output_image_size(input_image_size: List[int], stride: Union[int, Returns: output_image_size: output image/feature spatial size. """ - if input_image_size is None: - return None - + # get number of spatial dimensions, corresponds to image spatial size length num_dims = len(input_image_size) + + # checks to extract integer stride in case tuple was received if isinstance(stride, tuple): all_strides_equal = all([stride[0] == s for s in stride]) if not all_strides_equal: @@ -756,6 +774,7 @@ def _calculate_output_image_size(input_image_size: List[int], stride: Union[int, stride = stride[0] + # return output image size return [int(math.ceil(im_sz / stride)) for im_sz in input_image_size] @@ -769,21 +788,6 @@ def _decode_block_list(string_list: List[str]): Returns: blocks_args: a list of BlockArgs namedtuples of block args. """ - # TODO: remove after different python tests have passed on github - # BlockArgs = collections.namedtuple( - # "BlockArgs", - # [ - # "num_repeat", - # "kernel_size", - # "stride", - # "expand_ratio", - # "input_filters", - # "output_filters", - # "se_ratio", - # "id_skip", - # ], - # ) - # BlockArgs.__new__.__defaults__ = (None,) * len(BlockArgs._fields) # type: ignore # Parameters for an individual model block # namedtuple with defaults for mypy help from: # https://stackoverflow.com/a/53255358 @@ -836,10 +840,10 @@ def _decode_block_string(block_string: str): se_ratio=float(options["se"]) if "se" in options else None, ) - if not isinstance(string_list, list): - raise ValueError("string_list must be a list") - - blocks_args = [] + # convert block strings into BlockArgs for each entry in string_list list + blocks_args: List[BlockArgs] = [] for current_string in string_list: blocks_args.append(_decode_block_string(current_string)) + + # return blocks_args list, to be used for arguments of MBConv layers in EfficientNet return blocks_args diff --git a/tests/test_efficientnet.py b/tests/test_efficientnet.py index ab9bb4ecb9..7ef56c52a9 100644 --- a/tests/test_efficientnet.py +++ b/tests/test_efficientnet.py @@ -258,7 +258,7 @@ def test_kitty_pretrained(self, input_param, image_path, expected_label): self.assertEqual(pred_label, expected_label) def test_drop_connect_layer(self): - p_list = [float(d + 1) / 10 for d in range(9)] + p_list = [float(d + 1) / 10.0 for d in range(9)] # testing 1D, 2D and 3D shape for rand_tensor_shape in [(512, 16, 4), (384, 16, 4, 4), (256, 16, 4, 4, 4)]: @@ -269,7 +269,7 @@ def test_drop_connect_layer(self): out_tensor = drop_connect(in_tensor, p, training=training) self.assertTrue(torch.equal(out_tensor, in_tensor)) - # test training mode, sum(out tensor != in tensor)/out_tensor.size() == p + # test training mode, sum((out tensor * (1.0 - p)) != in tensor)/out_tensor.size() == p # use tolerance of 0.175 to account for rounding errors due to finite set in/out tol = 0.175 training = True