From 86788dcae38cd44c55b91513e82eea67715664cd Mon Sep 17 00:00:00 2001 From: ID6109 Date: Fri, 9 Jun 2023 16:07:03 +0530 Subject: [PATCH 1/8] Add the DarkNet files + license header --- keras_cv/models/backbones/darknet/__init__.py | 13 +++++++++++++ .../models/backbones/darknet/darknet_backbone.py | 13 +++++++++++++ .../backbones/darknet/darknet_backbone_presets.py | 13 +++++++++++++ .../darknet/darknet_backbone_presets_test.py | 13 +++++++++++++ .../backbones/darknet/darknet_backbone_test.py | 13 +++++++++++++ 5 files changed, 65 insertions(+) create mode 100644 keras_cv/models/backbones/darknet/__init__.py create mode 100644 keras_cv/models/backbones/darknet/darknet_backbone.py create mode 100644 keras_cv/models/backbones/darknet/darknet_backbone_presets.py create mode 100644 keras_cv/models/backbones/darknet/darknet_backbone_presets_test.py create mode 100644 keras_cv/models/backbones/darknet/darknet_backbone_test.py diff --git a/keras_cv/models/backbones/darknet/__init__.py b/keras_cv/models/backbones/darknet/__init__.py new file mode 100644 index 0000000000..3992ffb59a --- /dev/null +++ b/keras_cv/models/backbones/darknet/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2023 The KerasCV Authors +# +# 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 +# +# https://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. diff --git a/keras_cv/models/backbones/darknet/darknet_backbone.py b/keras_cv/models/backbones/darknet/darknet_backbone.py new file mode 100644 index 0000000000..3992ffb59a --- /dev/null +++ b/keras_cv/models/backbones/darknet/darknet_backbone.py @@ -0,0 +1,13 @@ +# Copyright 2023 The KerasCV Authors +# +# 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 +# +# https://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. diff --git a/keras_cv/models/backbones/darknet/darknet_backbone_presets.py b/keras_cv/models/backbones/darknet/darknet_backbone_presets.py new file mode 100644 index 0000000000..3992ffb59a --- /dev/null +++ b/keras_cv/models/backbones/darknet/darknet_backbone_presets.py @@ -0,0 +1,13 @@ +# Copyright 2023 The KerasCV Authors +# +# 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 +# +# https://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. diff --git a/keras_cv/models/backbones/darknet/darknet_backbone_presets_test.py b/keras_cv/models/backbones/darknet/darknet_backbone_presets_test.py new file mode 100644 index 0000000000..3992ffb59a --- /dev/null +++ b/keras_cv/models/backbones/darknet/darknet_backbone_presets_test.py @@ -0,0 +1,13 @@ +# Copyright 2023 The KerasCV Authors +# +# 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 +# +# https://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. diff --git a/keras_cv/models/backbones/darknet/darknet_backbone_test.py b/keras_cv/models/backbones/darknet/darknet_backbone_test.py new file mode 100644 index 0000000000..3992ffb59a --- /dev/null +++ b/keras_cv/models/backbones/darknet/darknet_backbone_test.py @@ -0,0 +1,13 @@ +# Copyright 2023 The KerasCV Authors +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. From bb83cc3d398e97bb0256b788d42b7c82dab9a1f3 Mon Sep 17 00:00:00 2001 From: ID6109 Date: Fri, 9 Jun 2023 16:29:34 +0530 Subject: [PATCH 2/8] Move the code from the old file to the new files --- keras_cv/models/__init__.py | 2 + .../backbones/darknet/darknet_backbone.py | 301 +++++++++++++++++ .../darknet/darknet_backbone_test.py | 37 +++ keras_cv/models/legacy/__init__.py | 2 - keras_cv/models/legacy/darknet.py | 314 ------------------ keras_cv/models/legacy/darknet_test.py | 51 --- 6 files changed, 340 insertions(+), 367 deletions(-) delete mode 100644 keras_cv/models/legacy/darknet.py delete mode 100644 keras_cv/models/legacy/darknet_test.py diff --git a/keras_cv/models/__init__.py b/keras_cv/models/__init__.py index 933c7ffbe2..147268a369 100644 --- a/keras_cv/models/__init__.py +++ b/keras_cv/models/__init__.py @@ -31,6 +31,8 @@ from keras_cv.models.backbones.csp_darknet.csp_darknet_backbone import ( CSPDarkNetXLBackbone, ) +from keras_cv.models.backbones.darknet.darknet_backbone import DarkNet21 +from keras_cv.models.backbones.darknet.darknet_backbone import DarkNet53 from keras_cv.models.backbones.efficientnet_v2.efficientnet_v2_aliases import ( EfficientNetV2B0Backbone, ) diff --git a/keras_cv/models/backbones/darknet/darknet_backbone.py b/keras_cv/models/backbones/darknet/darknet_backbone.py index 3992ffb59a..60c11ef89f 100644 --- a/keras_cv/models/backbones/darknet/darknet_backbone.py +++ b/keras_cv/models/backbones/darknet/darknet_backbone.py @@ -11,3 +11,304 @@ # 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. + +"""DarkNet models for KerasCV. +Reference: + - [YoloV3 Paper](https://arxiv.org/abs/1804.02767) + - [YoloV3 implementation](https://github.com/ultralytics/yolov3) +""" + +import tensorflow as tf +from tensorflow import keras +from tensorflow.keras import layers + +from keras_cv.models.backbones.csp_darknet.csp_darknet_utils import ( + DarknetConvBlock, +) +from keras_cv.models.backbones.csp_darknet.csp_darknet_utils import ( + ResidualBlocks, +) +from keras_cv.models.backbones.csp_darknet.csp_darknet_utils import ( + SpatialPyramidPoolingBottleneck, +) +from keras_cv.models.legacy import utils +from keras_cv.models.legacy.weights import parse_weights + +BASE_DOCSTRING = """Represents the {name} architecture. + + Although the {name} architecture is commonly used for detection tasks, it is + possible to extract the intermediate dark2 to dark5 layers from the model + for creating a feature pyramid Network. + + Reference: + - [YoloV3 Paper](https://arxiv.org/abs/1804.02767) + - [YoloV3 implementation](https://github.com/ultralytics/yolov3) + + For transfer learning use cases, make sure to read the + [guide to transfer learning & fine-tuning](https://keras.io/guides/transfer_learning/). + + Args: + include_rescaling: bool, whether to rescale the inputs. If set to + True, inputs will be passed through a `Rescaling(1/255.0)` layer. + include_top: bool, whether to include the fully-connected layer at the + top of the network. If provided, `num_classes` must be provided. + num_classes: integer, optional number of classes to classify images + into. Only to be specified if `include_top` is True. + weights: one of `None` (random initialization), or a pretrained weight + file path. + input_shape: optional shape tuple, defaults to (None, None, 3). + input_tensor: optional Keras tensor (i.e., output of `layers.Input()`) + to use as image input for the model. + pooling: optional pooling mode for feature extraction when `include_top` + is `False`. + - `None` means that the output of the model will be the 4D tensor + output of the last convolutional block. + - `avg` means that global average pooling will be applied to the + output of the last convolutional block, and thus the output of + the model will be a 2D tensor. + - `max` means that global max pooling will be applied. + name: string, optional name to pass to the model, defaults to "{name}". + + Returns: + A `keras.Model` instance. +""" # noqa: E501 + + +@keras.utils.register_keras_serializable(package="keras_cv.models") +class DarkNet(keras.Model): + + """Represents the DarkNet architecture. + + The DarkNet architecture is commonly used for detection tasks. It is + possible to extract the intermediate dark2 to dark5 layers from the model + for creating a feature pyramid Network. + + Reference: + - [YoloV3 Paper](https://arxiv.org/abs/1804.02767) + - [YoloV3 implementation](https://github.com/ultralytics/yolov3) + For transfer learning use cases, make sure to read the + [guide to transfer learning & fine-tuning](https://keras.io/guides/transfer_learning/). + + Args: + blocks: integer, numbers of building blocks from the layer dark2 to + layer dark5. + include_rescaling: bool, whether to rescale the inputs. If set to True, + inputs will be passed through a `Rescaling(1/255.0)` layer. + include_top: bool, whether to include the fully-connected layer at the + top of the network. If provided, `num_classes` must be provided. + num_classes: integer, optional number of classes to classify images + into. Only to be specified if `include_top` is True. + weights: one of `None` (random initialization) or a pretrained weight + file path. + input_shape: optional shape tuple, defaults to (None, None, 3). + input_tensor: optional Keras tensor (i.e., output of `layers.Input()`) + to use as image input for the model. + pooling: optional pooling mode for feature extraction when `include_top` + is `False`. + - `None` means that the output of the model will be the 4D tensor + output of the last convolutional block. + - `avg` means that global average pooling will be applied to the + output of the last convolutional block, and thus the output of + the model will be a 2D tensor. + - `max` means that global max pooling will be applied. + classifier_activation: A `str` or callable. The activation function to + use on the "top" layer. Ignored unless `include_top=True`. Set + `classifier_activation=None` to return the logits of the "top" + layer. + name: string, optional name to pass to the model, defaults to "DarkNet". + + Returns: + A `keras.Model` instance. + """ # noqa: E501 + + def __init__( + self, + blocks, + include_rescaling, + include_top, + num_classes=None, + weights=None, + input_shape=(None, None, 3), + input_tensor=None, + pooling=None, + classifier_activation="softmax", + name="DarkNet", + **kwargs, + ): + if weights and not tf.io.gfile.exists(weights): + raise ValueError( + "The `weights` argument should be either `None` or the path to " + "the weights file to be loaded. Weights file not found at " + f"location: {weights}" + ) + + if include_top and not num_classes: + raise ValueError( + "If `include_top` is True, you should specify `num_classes`. " + f"Received: num_classes={num_classes}" + ) + + inputs = utils.parse_model_inputs(input_shape, input_tensor) + + x = inputs + if include_rescaling: + x = layers.Rescaling(1 / 255.0)(x) + + # stem + x = DarknetConvBlock( + filters=32, + kernel_size=3, + strides=1, + activation="leaky_relu", + name="stem_conv", + )(x) + x = ResidualBlocks( + filters=64, num_blocks=1, name="stem_residual_block" + )(x) + + # filters for the ResidualBlock outputs + filters = [128, 256, 512, 1024] + + # layer_num is used for naming the residual blocks + # (starts with dark2, hence 2) + layer_num = 2 + + for filter, block in zip(filters, blocks): + x = ResidualBlocks( + filters=filter, + num_blocks=block, + name=f"dark{layer_num}_residual_block", + )(x) + layer_num += 1 + + # remaining dark5 layers + x = DarknetConvBlock( + filters=512, + kernel_size=1, + strides=1, + activation="leaky_relu", + name="dark5_conv1", + )(x) + x = DarknetConvBlock( + filters=1024, + kernel_size=3, + strides=1, + activation="leaky_relu", + name="dark5_conv2", + )(x) + x = SpatialPyramidPoolingBottleneck( + 512, activation="leaky_relu", name="dark5_spp" + )(x) + x = DarknetConvBlock( + filters=1024, + kernel_size=3, + strides=1, + activation="leaky_relu", + name="dark5_conv3", + )(x) + x = DarknetConvBlock( + filters=512, + kernel_size=1, + strides=1, + activation="leaky_relu", + name="dark5_conv4", + )(x) + + if include_top: + x = layers.GlobalAveragePooling2D(name="avg_pool")(x) + x = layers.Dense( + num_classes, + activation=classifier_activation, + name="predictions", + )(x) + elif pooling == "avg": + x = layers.GlobalAveragePooling2D(name="avg_pool")(x) + elif pooling == "max": + x = layers.GlobalMaxPooling2D(name="max_pool")(x) + + super().__init__(inputs=inputs, outputs=x, name=name, **kwargs) + + if weights is not None: + self.load_weights(weights) + + self.blocks = blocks + self.include_rescaling = include_rescaling + self.include_top = include_top + self.num_classes = num_classes + self.input_tensor = input_tensor + self.pooling = pooling + self.classifier_activation = classifier_activation + + def get_config(self): + return { + "blocks": self.blocks, + "include_rescaling": self.include_rescaling, + "include_top": self.include_top, + "num_classes": self.num_classes, + "input_shape": self.input_shape[1:], + "input_tensor": self.input_tensor, + "pooling": self.pooling, + "classifier_activation": self.classifier_activation, + "name": self.name, + "trainable": self.trainable, + } + + @classmethod + def from_config(cls, config): + return cls(**config) + + +def DarkNet21( + *, + include_rescaling, + include_top, + num_classes=None, + weights=None, + input_shape=(None, None, 3), + input_tensor=None, + pooling=None, + name="DarkNet21", + **kwargs, +): + return DarkNet( + [1, 2, 2, 1], + include_rescaling=include_rescaling, + include_top=include_top, + num_classes=num_classes, + weights=parse_weights(weights, include_top, "darknet"), + input_shape=input_shape, + input_tensor=input_tensor, + pooling=pooling, + name=name, + **kwargs, + ) + + +def DarkNet53( + *, + include_rescaling, + include_top, + num_classes=None, + weights=None, + input_shape=(None, None, 3), + input_tensor=None, + pooling=None, + name="DarkNet53", + **kwargs, +): + return DarkNet( + [2, 8, 8, 4], + include_rescaling=include_rescaling, + include_top=include_top, + num_classes=num_classes, + weights=parse_weights(weights, include_top, "darknet53"), + input_shape=input_shape, + input_tensor=input_tensor, + pooling=pooling, + name=name, + **kwargs, + ) + + +setattr(DarkNet21, "__doc__", BASE_DOCSTRING.format(name="DarkNet21")) +setattr(DarkNet53, "__doc__", BASE_DOCSTRING.format(name="DarkNet53")) diff --git a/keras_cv/models/backbones/darknet/darknet_backbone_test.py b/keras_cv/models/backbones/darknet/darknet_backbone_test.py index 3992ffb59a..f101208ce9 100644 --- a/keras_cv/models/backbones/darknet/darknet_backbone_test.py +++ b/keras_cv/models/backbones/darknet/darknet_backbone_test.py @@ -11,3 +11,40 @@ # 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 tensorflow as tf +from absl.testing import parameterized + +from keras_cv.models.backbones.darknet import darknet_backbone as darknet +from keras_cv.models.legacy.models_test import ModelsTest + +MODEL_LIST = [ + (darknet.DarkNet21, 512, {}), + (darknet.DarkNet53, 512, {}), +] + + +class DarkNetTest(ModelsTest, tf.test.TestCase, parameterized.TestCase): + @parameterized.parameters(*MODEL_LIST) + def test_application_base(self, app, _, args): + super()._test_application_base(app, _, args) + + @parameterized.parameters(*MODEL_LIST) + def test_application_with_rescaling(self, app, last_dim, args): + super()._test_application_with_rescaling(app, last_dim, args) + + @parameterized.parameters(*MODEL_LIST) + def test_application_pooling(self, app, last_dim, args): + super()._test_application_pooling(app, last_dim, args) + + @parameterized.parameters(*MODEL_LIST) + def test_application_variable_input_channels(self, app, last_dim, args): + super()._test_application_variable_input_channels(app, last_dim, args) + + @parameterized.parameters(*MODEL_LIST) + def test_model_can_be_used_as_backbone(self, app, last_dim, args): + super()._test_model_can_be_used_as_backbone(app, last_dim, args) + + +if __name__ == "__main__": + tf.test.main() diff --git a/keras_cv/models/legacy/__init__.py b/keras_cv/models/legacy/__init__.py index 33a769bf2f..f5e1a04b89 100644 --- a/keras_cv/models/legacy/__init__.py +++ b/keras_cv/models/legacy/__init__.py @@ -22,8 +22,6 @@ from keras_cv.models.legacy.convnext import ConvNeXtSmall from keras_cv.models.legacy.convnext import ConvNeXtTiny from keras_cv.models.legacy.convnext import ConvNeXtXLarge -from keras_cv.models.legacy.darknet import DarkNet21 -from keras_cv.models.legacy.darknet import DarkNet53 from keras_cv.models.legacy.densenet import DenseNet121 from keras_cv.models.legacy.densenet import DenseNet169 from keras_cv.models.legacy.densenet import DenseNet201 diff --git a/keras_cv/models/legacy/darknet.py b/keras_cv/models/legacy/darknet.py deleted file mode 100644 index ea7fd429f2..0000000000 --- a/keras_cv/models/legacy/darknet.py +++ /dev/null @@ -1,314 +0,0 @@ -# Copyright 2022 The KerasCV Authors -# -# 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 -# -# https://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. - -"""DarkNet models for KerasCV. -Reference: - - [YoloV3 Paper](https://arxiv.org/abs/1804.02767) - - [YoloV3 implementation](https://github.com/ultralytics/yolov3) -""" - -import tensorflow as tf -from tensorflow import keras -from tensorflow.keras import layers - -from keras_cv.models.backbones.csp_darknet.csp_darknet_utils import ( - DarknetConvBlock, -) -from keras_cv.models.backbones.csp_darknet.csp_darknet_utils import ( - ResidualBlocks, -) -from keras_cv.models.backbones.csp_darknet.csp_darknet_utils import ( - SpatialPyramidPoolingBottleneck, -) -from keras_cv.models.legacy import utils -from keras_cv.models.legacy.weights import parse_weights - -BASE_DOCSTRING = """Represents the {name} architecture. - - Although the {name} architecture is commonly used for detection tasks, it is - possible to extract the intermediate dark2 to dark5 layers from the model - for creating a feature pyramid Network. - - Reference: - - [YoloV3 Paper](https://arxiv.org/abs/1804.02767) - - [YoloV3 implementation](https://github.com/ultralytics/yolov3) - - For transfer learning use cases, make sure to read the - [guide to transfer learning & fine-tuning](https://keras.io/guides/transfer_learning/). - - Args: - include_rescaling: bool, whether to rescale the inputs. If set to - True, inputs will be passed through a `Rescaling(1/255.0)` layer. - include_top: bool, whether to include the fully-connected layer at the - top of the network. If provided, `num_classes` must be provided. - num_classes: integer, optional number of classes to classify images - into. Only to be specified if `include_top` is True. - weights: one of `None` (random initialization), or a pretrained weight - file path. - input_shape: optional shape tuple, defaults to (None, None, 3). - input_tensor: optional Keras tensor (i.e., output of `layers.Input()`) - to use as image input for the model. - pooling: optional pooling mode for feature extraction when `include_top` - is `False`. - - `None` means that the output of the model will be the 4D tensor - output of the last convolutional block. - - `avg` means that global average pooling will be applied to the - output of the last convolutional block, and thus the output of - the model will be a 2D tensor. - - `max` means that global max pooling will be applied. - name: string, optional name to pass to the model, defaults to "{name}". - - Returns: - A `keras.Model` instance. -""" # noqa: E501 - - -@keras.utils.register_keras_serializable(package="keras_cv.models") -class DarkNet(keras.Model): - - """Represents the DarkNet architecture. - - The DarkNet architecture is commonly used for detection tasks. It is - possible to extract the intermediate dark2 to dark5 layers from the model - for creating a feature pyramid Network. - - Reference: - - [YoloV3 Paper](https://arxiv.org/abs/1804.02767) - - [YoloV3 implementation](https://github.com/ultralytics/yolov3) - For transfer learning use cases, make sure to read the - [guide to transfer learning & fine-tuning](https://keras.io/guides/transfer_learning/). - - Args: - blocks: integer, numbers of building blocks from the layer dark2 to - layer dark5. - include_rescaling: bool, whether to rescale the inputs. If set to True, - inputs will be passed through a `Rescaling(1/255.0)` layer. - include_top: bool, whether to include the fully-connected layer at the - top of the network. If provided, `num_classes` must be provided. - num_classes: integer, optional number of classes to classify images - into. Only to be specified if `include_top` is True. - weights: one of `None` (random initialization) or a pretrained weight - file path. - input_shape: optional shape tuple, defaults to (None, None, 3). - input_tensor: optional Keras tensor (i.e., output of `layers.Input()`) - to use as image input for the model. - pooling: optional pooling mode for feature extraction when `include_top` - is `False`. - - `None` means that the output of the model will be the 4D tensor - output of the last convolutional block. - - `avg` means that global average pooling will be applied to the - output of the last convolutional block, and thus the output of - the model will be a 2D tensor. - - `max` means that global max pooling will be applied. - classifier_activation: A `str` or callable. The activation function to - use on the "top" layer. Ignored unless `include_top=True`. Set - `classifier_activation=None` to return the logits of the "top" - layer. - name: string, optional name to pass to the model, defaults to "DarkNet". - - Returns: - A `keras.Model` instance. - """ # noqa: E501 - - def __init__( - self, - blocks, - include_rescaling, - include_top, - num_classes=None, - weights=None, - input_shape=(None, None, 3), - input_tensor=None, - pooling=None, - classifier_activation="softmax", - name="DarkNet", - **kwargs, - ): - if weights and not tf.io.gfile.exists(weights): - raise ValueError( - "The `weights` argument should be either `None` or the path to " - "the weights file to be loaded. Weights file not found at " - f"location: {weights}" - ) - - if include_top and not num_classes: - raise ValueError( - "If `include_top` is True, you should specify `num_classes`. " - f"Received: num_classes={num_classes}" - ) - - inputs = utils.parse_model_inputs(input_shape, input_tensor) - - x = inputs - if include_rescaling: - x = layers.Rescaling(1 / 255.0)(x) - - # stem - x = DarknetConvBlock( - filters=32, - kernel_size=3, - strides=1, - activation="leaky_relu", - name="stem_conv", - )(x) - x = ResidualBlocks( - filters=64, num_blocks=1, name="stem_residual_block" - )(x) - - # filters for the ResidualBlock outputs - filters = [128, 256, 512, 1024] - - # layer_num is used for naming the residual blocks - # (starts with dark2, hence 2) - layer_num = 2 - - for filter, block in zip(filters, blocks): - x = ResidualBlocks( - filters=filter, - num_blocks=block, - name=f"dark{layer_num}_residual_block", - )(x) - layer_num += 1 - - # remaining dark5 layers - x = DarknetConvBlock( - filters=512, - kernel_size=1, - strides=1, - activation="leaky_relu", - name="dark5_conv1", - )(x) - x = DarknetConvBlock( - filters=1024, - kernel_size=3, - strides=1, - activation="leaky_relu", - name="dark5_conv2", - )(x) - x = SpatialPyramidPoolingBottleneck( - 512, activation="leaky_relu", name="dark5_spp" - )(x) - x = DarknetConvBlock( - filters=1024, - kernel_size=3, - strides=1, - activation="leaky_relu", - name="dark5_conv3", - )(x) - x = DarknetConvBlock( - filters=512, - kernel_size=1, - strides=1, - activation="leaky_relu", - name="dark5_conv4", - )(x) - - if include_top: - x = layers.GlobalAveragePooling2D(name="avg_pool")(x) - x = layers.Dense( - num_classes, - activation=classifier_activation, - name="predictions", - )(x) - elif pooling == "avg": - x = layers.GlobalAveragePooling2D(name="avg_pool")(x) - elif pooling == "max": - x = layers.GlobalMaxPooling2D(name="max_pool")(x) - - super().__init__(inputs=inputs, outputs=x, name=name, **kwargs) - - if weights is not None: - self.load_weights(weights) - - self.blocks = blocks - self.include_rescaling = include_rescaling - self.include_top = include_top - self.num_classes = num_classes - self.input_tensor = input_tensor - self.pooling = pooling - self.classifier_activation = classifier_activation - - def get_config(self): - return { - "blocks": self.blocks, - "include_rescaling": self.include_rescaling, - "include_top": self.include_top, - "num_classes": self.num_classes, - "input_shape": self.input_shape[1:], - "input_tensor": self.input_tensor, - "pooling": self.pooling, - "classifier_activation": self.classifier_activation, - "name": self.name, - "trainable": self.trainable, - } - - @classmethod - def from_config(cls, config): - return cls(**config) - - -def DarkNet21( - *, - include_rescaling, - include_top, - num_classes=None, - weights=None, - input_shape=(None, None, 3), - input_tensor=None, - pooling=None, - name="DarkNet21", - **kwargs, -): - return DarkNet( - [1, 2, 2, 1], - include_rescaling=include_rescaling, - include_top=include_top, - num_classes=num_classes, - weights=parse_weights(weights, include_top, "darknet"), - input_shape=input_shape, - input_tensor=input_tensor, - pooling=pooling, - name=name, - **kwargs, - ) - - -def DarkNet53( - *, - include_rescaling, - include_top, - num_classes=None, - weights=None, - input_shape=(None, None, 3), - input_tensor=None, - pooling=None, - name="DarkNet53", - **kwargs, -): - return DarkNet( - [2, 8, 8, 4], - include_rescaling=include_rescaling, - include_top=include_top, - num_classes=num_classes, - weights=parse_weights(weights, include_top, "darknet53"), - input_shape=input_shape, - input_tensor=input_tensor, - pooling=pooling, - name=name, - **kwargs, - ) - - -setattr(DarkNet21, "__doc__", BASE_DOCSTRING.format(name="DarkNet21")) -setattr(DarkNet53, "__doc__", BASE_DOCSTRING.format(name="DarkNet53")) diff --git a/keras_cv/models/legacy/darknet_test.py b/keras_cv/models/legacy/darknet_test.py deleted file mode 100644 index 1cf091a53f..0000000000 --- a/keras_cv/models/legacy/darknet_test.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2022 The KerasCV Authors -# -# 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 -# -# https://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 tensorflow as tf -from absl.testing import parameterized - -from keras_cv.models.legacy import darknet - -from .models_test import ModelsTest - -MODEL_LIST = [ - (darknet.DarkNet21, 512, {}), - (darknet.DarkNet53, 512, {}), -] - - -class DarkNetTest(ModelsTest, tf.test.TestCase, parameterized.TestCase): - @parameterized.parameters(*MODEL_LIST) - def test_application_base(self, app, _, args): - super()._test_application_base(app, _, args) - - @parameterized.parameters(*MODEL_LIST) - def test_application_with_rescaling(self, app, last_dim, args): - super()._test_application_with_rescaling(app, last_dim, args) - - @parameterized.parameters(*MODEL_LIST) - def test_application_pooling(self, app, last_dim, args): - super()._test_application_pooling(app, last_dim, args) - - @parameterized.parameters(*MODEL_LIST) - def test_application_variable_input_channels(self, app, last_dim, args): - super()._test_application_variable_input_channels(app, last_dim, args) - - @parameterized.parameters(*MODEL_LIST) - def test_model_can_be_used_as_backbone(self, app, last_dim, args): - super()._test_model_can_be_used_as_backbone(app, last_dim, args) - - -if __name__ == "__main__": - tf.test.main() From 87b53001510009e96a511949123e9b4c144bdf04 Mon Sep 17 00:00:00 2001 From: ID6109 Date: Fri, 9 Jun 2023 16:47:19 +0530 Subject: [PATCH 3/8] Add the presets --- keras_cv/models/backbones/backbone_presets.py | 3 ++ .../darknet/darknet_backbone_presets.py | 48 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/keras_cv/models/backbones/backbone_presets.py b/keras_cv/models/backbones/backbone_presets.py index 4b2ee1a1ff..f458bbf454 100644 --- a/keras_cv/models/backbones/backbone_presets.py +++ b/keras_cv/models/backbones/backbone_presets.py @@ -15,6 +15,7 @@ """All Backbone presets""" from keras_cv.models.backbones.csp_darknet import csp_darknet_backbone_presets +from keras_cv.models.backbones.darknet import darknet_backbone_presets from keras_cv.models.backbones.efficientnet_v2 import ( efficientnet_v2_backbone_presets, ) @@ -28,6 +29,7 @@ **mobilenet_v3_backbone_presets.backbone_presets_no_weights, **csp_darknet_backbone_presets.backbone_presets_no_weights, **efficientnet_v2_backbone_presets.backbone_presets_no_weights, + **darknet_backbone_presets.backbone_presets_no_weights, } backbone_presets_with_weights = { @@ -36,6 +38,7 @@ **mobilenet_v3_backbone_presets.backbone_presets_with_weights, **csp_darknet_backbone_presets.backbone_presets_with_weights, **efficientnet_v2_backbone_presets.backbone_presets_with_weights, + **darknet_backbone_presets.backbone_presets_with_weights, } backbone_presets = { diff --git a/keras_cv/models/backbones/darknet/darknet_backbone_presets.py b/keras_cv/models/backbones/darknet/darknet_backbone_presets.py index 3992ffb59a..04a72d8582 100644 --- a/keras_cv/models/backbones/darknet/darknet_backbone_presets.py +++ b/keras_cv/models/backbones/darknet/darknet_backbone_presets.py @@ -11,3 +11,51 @@ # 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. +"""DarkNet model preset configurations.""" + +backbone_presets_no_weights = { + "darknet21": { + "metadata": { + "description": "DarkNet model with 21 layers.", + }, + "class_name": "keras_cv.models>DarkNetBackbone", + "config": { + "stackwise_blocks": [1, 2, 2, 1], + "include_rescaling": True, + "input_shape": (None, None, 3), + "input_tensor": None, + }, + }, + "darknet53": { + "metadata": { + "description": "DarkNet model with 53 layers.", + }, + "class_name": "keras_cv.models>DarkNetBackbone", + "config": { + "stackwise_blocks": [2, 8, 8, 4], + "include_rescaling": True, + "input_shape": (None, None, 3), + "input_tensor": None, + }, + }, +} + +backbone_presets_with_weights = { + "darknet53_imagenet": { + "metadata": { + "description": ( + "DarkNet model with 53 layers. " + "Trained on Imagenet 2012 classification task." + ), + }, + "class_name": "keras_cv.models>DarkNetBackbone", + "config": backbone_presets_no_weights["darknet53"]["config"], + "weights_url": "https://storage.googleapis.com/keras-cv/models/darknet53/imagenet/classification-v0-notop.h5", # noqa: E501 + "weights_hash": "8dcce43163e4b4a63e74330ba1902e520211db72d895b0b090b6bfe103e7a8a5", # noqa: E501 + }, +} + +backbone_presets = { + **backbone_presets_no_weights, + **backbone_presets_with_weights, +} From a992b9a83e26ba588f7ae8fb0679237ea66afe27 Mon Sep 17 00:00:00 2001 From: ID6109 Date: Fri, 9 Jun 2023 17:16:02 +0530 Subject: [PATCH 4/8] Change the base class --- keras_cv/models/__init__.py | 4 +- .../backbones/darknet/darknet_backbone.py | 114 ++++++++---------- 2 files changed, 51 insertions(+), 67 deletions(-) diff --git a/keras_cv/models/__init__.py b/keras_cv/models/__init__.py index 147268a369..05af95d743 100644 --- a/keras_cv/models/__init__.py +++ b/keras_cv/models/__init__.py @@ -31,8 +31,8 @@ from keras_cv.models.backbones.csp_darknet.csp_darknet_backbone import ( CSPDarkNetXLBackbone, ) -from keras_cv.models.backbones.darknet.darknet_backbone import DarkNet21 -from keras_cv.models.backbones.darknet.darknet_backbone import DarkNet53 +from keras_cv.models.backbones.darknet.darknet_backbone import DarkNet21Backbone +from keras_cv.models.backbones.darknet.darknet_backbone import DarkNet53Backbone from keras_cv.models.backbones.efficientnet_v2.efficientnet_v2_aliases import ( EfficientNetV2B0Backbone, ) diff --git a/keras_cv/models/backbones/darknet/darknet_backbone.py b/keras_cv/models/backbones/darknet/darknet_backbone.py index 60c11ef89f..874b65bb05 100644 --- a/keras_cv/models/backbones/darknet/darknet_backbone.py +++ b/keras_cv/models/backbones/darknet/darknet_backbone.py @@ -18,10 +18,12 @@ - [YoloV3 implementation](https://github.com/ultralytics/yolov3) """ -import tensorflow as tf +import copy + from tensorflow import keras from tensorflow.keras import layers +from keras_cv.models.backbones.backbone import Backbone from keras_cv.models.backbones.csp_darknet.csp_darknet_utils import ( DarknetConvBlock, ) @@ -31,8 +33,15 @@ from keras_cv.models.backbones.csp_darknet.csp_darknet_utils import ( SpatialPyramidPoolingBottleneck, ) +from keras_cv.models.backbones.darknet.darknet_backbone_presets import ( + backbone_presets, +) +from keras_cv.models.backbones.darknet.darknet_backbone_presets import ( + backbone_presets_with_weights, +) from keras_cv.models.legacy import utils from keras_cv.models.legacy.weights import parse_weights +from keras_cv.utils.python_utils import classproperty BASE_DOCSTRING = """Represents the {name} architecture. @@ -75,7 +84,7 @@ @keras.utils.register_keras_serializable(package="keras_cv.models") -class DarkNet(keras.Model): +class DarkNetBackbone(Backbone): """Represents the DarkNet architecture. @@ -125,29 +134,10 @@ def __init__( self, blocks, include_rescaling, - include_top, - num_classes=None, - weights=None, input_shape=(None, None, 3), input_tensor=None, - pooling=None, - classifier_activation="softmax", - name="DarkNet", **kwargs, ): - if weights and not tf.io.gfile.exists(weights): - raise ValueError( - "The `weights` argument should be either `None` or the path to " - "the weights file to be loaded. Weights file not found at " - f"location: {weights}" - ) - - if include_top and not num_classes: - raise ValueError( - "If `include_top` is True, you should specify `num_classes`. " - f"Received: num_classes={num_classes}" - ) - inputs = utils.parse_model_inputs(input_shape, input_tensor) x = inputs @@ -155,6 +145,7 @@ def __init__( x = layers.Rescaling(1 / 255.0)(x) # stem + pyramid_level_inputs = {} x = DarknetConvBlock( filters=32, kernel_size=3, @@ -162,9 +153,11 @@ def __init__( activation="leaky_relu", name="stem_conv", )(x) + pyramid_level_inputs[2] = x.node.layer.name x = ResidualBlocks( filters=64, num_blocks=1, name="stem_residual_block" )(x) + pyramid_level_inputs[3] = x.node.layer.name # filters for the ResidualBlock outputs filters = [128, 256, 512, 1024] @@ -180,6 +173,7 @@ def __init__( name=f"dark{layer_num}_residual_block", )(x) layer_num += 1 + pyramid_level_inputs[layer_num + 1] = x.node.layer.name # remaining dark5 layers x = DarknetConvBlock( @@ -189,6 +183,7 @@ def __init__( activation="leaky_relu", name="dark5_conv1", )(x) + pyramid_level_inputs[8] = x.node.layer.name x = DarknetConvBlock( filters=1024, kernel_size=3, @@ -196,6 +191,7 @@ def __init__( activation="leaky_relu", name="dark5_conv2", )(x) + pyramid_level_inputs[9] = x.node.layer.name x = SpatialPyramidPoolingBottleneck( 512, activation="leaky_relu", name="dark5_spp" )(x) @@ -206,6 +202,7 @@ def __init__( activation="leaky_relu", name="dark5_conv3", )(x) + pyramid_level_inputs[10] = x.node.layer.name x = DarknetConvBlock( filters=512, kernel_size=1, @@ -213,52 +210,39 @@ def __init__( activation="leaky_relu", name="dark5_conv4", )(x) + pyramid_level_inputs[11] = x.node.layer.name - if include_top: - x = layers.GlobalAveragePooling2D(name="avg_pool")(x) - x = layers.Dense( - num_classes, - activation=classifier_activation, - name="predictions", - )(x) - elif pooling == "avg": - x = layers.GlobalAveragePooling2D(name="avg_pool")(x) - elif pooling == "max": - x = layers.GlobalMaxPooling2D(name="max_pool")(x) - - super().__init__(inputs=inputs, outputs=x, name=name, **kwargs) - - if weights is not None: - self.load_weights(weights) + super().__init__(inputs=inputs, outputs=x, **kwargs) + self.pyramid_level_inputs = pyramid_level_inputs self.blocks = blocks self.include_rescaling = include_rescaling - self.include_top = include_top - self.num_classes = num_classes self.input_tensor = input_tensor - self.pooling = pooling - self.classifier_activation = classifier_activation def get_config(self): - return { - "blocks": self.blocks, - "include_rescaling": self.include_rescaling, - "include_top": self.include_top, - "num_classes": self.num_classes, - "input_shape": self.input_shape[1:], - "input_tensor": self.input_tensor, - "pooling": self.pooling, - "classifier_activation": self.classifier_activation, - "name": self.name, - "trainable": self.trainable, - } - - @classmethod - def from_config(cls, config): - return cls(**config) - - -def DarkNet21( + config = super().get_config() + config.update( + { + "blocks": self.blocks, + "include_rescaling": self.include_rescaling, + "input_shape": self.input_shape[1:], + "input_tensor": self.input_tensor, + } + ) + return config + + @classproperty + def presets(cls): + """Dictionary of preset names and configurations.""" + return copy.deepcopy(backbone_presets) + + @classproperty + def presets_with_weights(cls): + """Dictionary of preset names and configurations that include weights.""" # noqa: E501 + return copy.deepcopy(backbone_presets_with_weights) + + +def DarkNet21Backbone( *, include_rescaling, include_top, @@ -270,7 +254,7 @@ def DarkNet21( name="DarkNet21", **kwargs, ): - return DarkNet( + return DarkNetBackbone( [1, 2, 2, 1], include_rescaling=include_rescaling, include_top=include_top, @@ -284,7 +268,7 @@ def DarkNet21( ) -def DarkNet53( +def DarkNet53Backbone( *, include_rescaling, include_top, @@ -296,7 +280,7 @@ def DarkNet53( name="DarkNet53", **kwargs, ): - return DarkNet( + return DarkNetBackbone( [2, 8, 8, 4], include_rescaling=include_rescaling, include_top=include_top, @@ -310,5 +294,5 @@ def DarkNet53( ) -setattr(DarkNet21, "__doc__", BASE_DOCSTRING.format(name="DarkNet21")) -setattr(DarkNet53, "__doc__", BASE_DOCSTRING.format(name="DarkNet53")) +setattr(DarkNet21Backbone, "__doc__", BASE_DOCSTRING.format(name="DarkNet21")) +setattr(DarkNet53Backbone, "__doc__", BASE_DOCSTRING.format(name="DarkNet53")) From 507de110c8eab566eaa81ed2f9634509ff951b69 Mon Sep 17 00:00:00 2001 From: ID6109 Date: Fri, 9 Jun 2023 22:25:51 +0530 Subject: [PATCH 5/8] Make the architectures extend the Backbone class --- .../backbones/darknet/darknet_backbone.py | 107 ++++++++++-------- 1 file changed, 58 insertions(+), 49 deletions(-) diff --git a/keras_cv/models/backbones/darknet/darknet_backbone.py b/keras_cv/models/backbones/darknet/darknet_backbone.py index 874b65bb05..5b78010a21 100644 --- a/keras_cv/models/backbones/darknet/darknet_backbone.py +++ b/keras_cv/models/backbones/darknet/darknet_backbone.py @@ -40,7 +40,6 @@ backbone_presets_with_weights, ) from keras_cv.models.legacy import utils -from keras_cv.models.legacy.weights import parse_weights from keras_cv.utils.python_utils import classproperty BASE_DOCSTRING = """Represents the {name} architecture. @@ -242,56 +241,66 @@ def presets_with_weights(cls): return copy.deepcopy(backbone_presets_with_weights) -def DarkNet21Backbone( - *, - include_rescaling, - include_top, - num_classes=None, - weights=None, - input_shape=(None, None, 3), - input_tensor=None, - pooling=None, - name="DarkNet21", - **kwargs, -): - return DarkNetBackbone( - [1, 2, 2, 1], - include_rescaling=include_rescaling, - include_top=include_top, - num_classes=num_classes, - weights=parse_weights(weights, include_top, "darknet"), - input_shape=input_shape, - input_tensor=input_tensor, - pooling=pooling, - name=name, +class DarkNet21Backbone(DarkNetBackbone): + def __new__( + cls, + include_rescaling=True, + input_shape=(None, None, 3), + input_tensor=None, **kwargs, - ) - - -def DarkNet53Backbone( - *, - include_rescaling, - include_top, - num_classes=None, - weights=None, - input_shape=(None, None, 3), - input_tensor=None, - pooling=None, - name="DarkNet53", - **kwargs, -): - return DarkNetBackbone( - [2, 8, 8, 4], - include_rescaling=include_rescaling, - include_top=include_top, - num_classes=num_classes, - weights=parse_weights(weights, include_top, "darknet53"), - input_shape=input_shape, - input_tensor=input_tensor, - pooling=pooling, - name=name, + ): + # Pack args in kwargs + kwargs.update( + { + "include_rescaling": include_rescaling, + "input_shape": input_shape, + "input_tensor": input_tensor, + } + ) + return DarkNetBackbone.from_preset("darknet21", **kwargs) + + @classproperty + def presets(cls): + """Dictionary of preset names and configurations.""" + return {} + + @classproperty + def presets_with_weights(cls): + """Dictionary of preset names and configurations that include weights.""" # noqa: E501 + return {} + + +class DarkNet53Backbone(DarkNetBackbone): + def __new__( + cls, + include_rescaling=True, + input_shape=(None, None, 3), + input_tensor=None, **kwargs, - ) + ): + # Pack args in kwargs + kwargs.update( + { + "include_rescaling": include_rescaling, + "input_shape": input_shape, + "input_tensor": input_tensor, + } + ) + return DarkNetBackbone.from_preset("darknet53", **kwargs) + + @classproperty + def presets(cls): + """Dictionary of preset names and configurations.""" + return { + "darknet53_imagenet": copy.deepcopy( + backbone_presets["darknet53_imagenet"] + ), + } + + @classproperty + def presets_with_weights(cls): + """Dictionary of preset names and configurations that include weights.""" # noqa: E501 + return cls.presets setattr(DarkNet21Backbone, "__doc__", BASE_DOCSTRING.format(name="DarkNet21")) From 6925c96344671044503954998a62a5ba5912e7e9 Mon Sep 17 00:00:00 2001 From: ID6109 Date: Fri, 9 Jun 2023 22:38:52 +0530 Subject: [PATCH 6/8] Update the docstrings --- .../backbones/darknet/darknet_backbone.py | 124 ++++++++---------- 1 file changed, 55 insertions(+), 69 deletions(-) diff --git a/keras_cv/models/backbones/darknet/darknet_backbone.py b/keras_cv/models/backbones/darknet/darknet_backbone.py index 5b78010a21..3668b8dcae 100644 --- a/keras_cv/models/backbones/darknet/darknet_backbone.py +++ b/keras_cv/models/backbones/darknet/darknet_backbone.py @@ -42,45 +42,6 @@ from keras_cv.models.legacy import utils from keras_cv.utils.python_utils import classproperty -BASE_DOCSTRING = """Represents the {name} architecture. - - Although the {name} architecture is commonly used for detection tasks, it is - possible to extract the intermediate dark2 to dark5 layers from the model - for creating a feature pyramid Network. - - Reference: - - [YoloV3 Paper](https://arxiv.org/abs/1804.02767) - - [YoloV3 implementation](https://github.com/ultralytics/yolov3) - - For transfer learning use cases, make sure to read the - [guide to transfer learning & fine-tuning](https://keras.io/guides/transfer_learning/). - - Args: - include_rescaling: bool, whether to rescale the inputs. If set to - True, inputs will be passed through a `Rescaling(1/255.0)` layer. - include_top: bool, whether to include the fully-connected layer at the - top of the network. If provided, `num_classes` must be provided. - num_classes: integer, optional number of classes to classify images - into. Only to be specified if `include_top` is True. - weights: one of `None` (random initialization), or a pretrained weight - file path. - input_shape: optional shape tuple, defaults to (None, None, 3). - input_tensor: optional Keras tensor (i.e., output of `layers.Input()`) - to use as image input for the model. - pooling: optional pooling mode for feature extraction when `include_top` - is `False`. - - `None` means that the output of the model will be the 4D tensor - output of the last convolutional block. - - `avg` means that global average pooling will be applied to the - output of the last convolutional block, and thus the output of - the model will be a 2D tensor. - - `max` means that global max pooling will be applied. - name: string, optional name to pass to the model, defaults to "{name}". - - Returns: - A `keras.Model` instance. -""" # noqa: E501 - @keras.utils.register_keras_serializable(package="keras_cv.models") class DarkNetBackbone(Backbone): @@ -98,40 +59,34 @@ class DarkNetBackbone(Backbone): [guide to transfer learning & fine-tuning](https://keras.io/guides/transfer_learning/). Args: - blocks: integer, numbers of building blocks from the layer dark2 to - layer dark5. + stackwise_blocks: integer, numbers of building blocks from the layer + dark2 to layer dark5. include_rescaling: bool, whether to rescale the inputs. If set to True, inputs will be passed through a `Rescaling(1/255.0)` layer. - include_top: bool, whether to include the fully-connected layer at the - top of the network. If provided, `num_classes` must be provided. - num_classes: integer, optional number of classes to classify images - into. Only to be specified if `include_top` is True. - weights: one of `None` (random initialization) or a pretrained weight - file path. input_shape: optional shape tuple, defaults to (None, None, 3). input_tensor: optional Keras tensor (i.e., output of `layers.Input()`) to use as image input for the model. - pooling: optional pooling mode for feature extraction when `include_top` - is `False`. - - `None` means that the output of the model will be the 4D tensor - output of the last convolutional block. - - `avg` means that global average pooling will be applied to the - output of the last convolutional block, and thus the output of - the model will be a 2D tensor. - - `max` means that global max pooling will be applied. - classifier_activation: A `str` or callable. The activation function to - use on the "top" layer. Ignored unless `include_top=True`. Set - `classifier_activation=None` to return the logits of the "top" - layer. - name: string, optional name to pass to the model, defaults to "DarkNet". - - Returns: - A `keras.Model` instance. + + Examples: + ```python + input_data = tf.ones(shape=(8, 224, 224, 3)) + + # Pretrained backbone + model = keras_cv.models.DarkNetBackbone.from_preset("darknet53_imagenet") + output = model(input_data) + + # Randomly initialized backbone with a custom config + model = DarkNetBackbone( + stackwise_blocks=[2, 8, 8, 4], + include_rescaling=False, + ) + output = model(input_data) + ``` """ # noqa: E501 def __init__( self, - blocks, + stackwise_blocks, include_rescaling, input_shape=(None, None, 3), input_tensor=None, @@ -165,7 +120,7 @@ def __init__( # (starts with dark2, hence 2) layer_num = 2 - for filter, block in zip(filters, blocks): + for filter, block in zip(filters, stackwise_blocks): x = ResidualBlocks( filters=filter, num_blocks=block, @@ -214,7 +169,7 @@ def __init__( super().__init__(inputs=inputs, outputs=x, **kwargs) self.pyramid_level_inputs = pyramid_level_inputs - self.blocks = blocks + self.stackwise_blocks = stackwise_blocks self.include_rescaling = include_rescaling self.input_tensor = input_tensor @@ -222,7 +177,7 @@ def get_config(self): config = super().get_config() config.update( { - "blocks": self.blocks, + "stackwise_blocks": self.stackwise_blocks, "include_rescaling": self.include_rescaling, "input_shape": self.input_shape[1:], "input_tensor": self.input_tensor, @@ -241,6 +196,37 @@ def presets_with_weights(cls): return copy.deepcopy(backbone_presets_with_weights) +ALIAS_DOCSTRING = """DarkNet model with {num_layers} layers. + + Although the DarkNet architecture is commonly used for detection tasks, it + is possible to extract the intermediate dark2 to dark5 layers from the model + for creating a feature pyramid Network. + + Reference: + - [YoloV3 Paper](https://arxiv.org/abs/1804.02767) + - [YoloV3 implementation](https://github.com/ultralytics/yolov3) + + For transfer learning use cases, make sure to read the + [guide to transfer learning & fine-tuning](https://keras.io/guides/transfer_learning/). + + Args: + include_rescaling: bool, whether to rescale the inputs. If set to + True, inputs will be passed through a `Rescaling(1/255.0)` layer. + input_shape: optional shape tuple, defaults to (None, None, 3). + input_tensor: optional Keras tensor (i.e., output of `layers.Input()`) + to use as image input for the model. + + Examples: + ```python + input_data = tf.ones(shape=(8, 224, 224, 3)) + + # Randomly initialized backbone + model = DarkNet{num_layers}Backbone() + output = model(input_data) + ``` +""" # noqa: E501 + + class DarkNet21Backbone(DarkNetBackbone): def __new__( cls, @@ -303,5 +289,5 @@ def presets_with_weights(cls): return cls.presets -setattr(DarkNet21Backbone, "__doc__", BASE_DOCSTRING.format(name="DarkNet21")) -setattr(DarkNet53Backbone, "__doc__", BASE_DOCSTRING.format(name="DarkNet53")) +setattr(DarkNet21Backbone, "__doc__", ALIAS_DOCSTRING.format(num_layers=21)) +setattr(DarkNet53Backbone, "__doc__", ALIAS_DOCSTRING.format(num_layers=53)) From d672b5d9c92ae4bf9ca08b40481efc8bb8068081 Mon Sep 17 00:00:00 2001 From: ID6109 Date: Fri, 9 Jun 2023 23:21:02 +0530 Subject: [PATCH 7/8] Adding unit tests --- keras_cv/models/__init__.py | 1 + .../backbones/darknet/darknet_backbone.py | 2 +- .../darknet/darknet_backbone_presets_test.py | 79 ++++++++++ .../darknet/darknet_backbone_test.py | 139 +++++++++++++++--- 4 files changed, 198 insertions(+), 23 deletions(-) diff --git a/keras_cv/models/__init__.py b/keras_cv/models/__init__.py index 05af95d743..5e09fb232d 100644 --- a/keras_cv/models/__init__.py +++ b/keras_cv/models/__init__.py @@ -33,6 +33,7 @@ ) from keras_cv.models.backbones.darknet.darknet_backbone import DarkNet21Backbone from keras_cv.models.backbones.darknet.darknet_backbone import DarkNet53Backbone +from keras_cv.models.backbones.darknet.darknet_backbone import DarkNetBackbone from keras_cv.models.backbones.efficientnet_v2.efficientnet_v2_aliases import ( EfficientNetV2B0Backbone, ) diff --git a/keras_cv/models/backbones/darknet/darknet_backbone.py b/keras_cv/models/backbones/darknet/darknet_backbone.py index 3668b8dcae..470d7c4d3c 100644 --- a/keras_cv/models/backbones/darknet/darknet_backbone.py +++ b/keras_cv/models/backbones/darknet/darknet_backbone.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""DarkNet models for KerasCV. +"""DarkNet backbone model. Reference: - [YoloV3 Paper](https://arxiv.org/abs/1804.02767) - [YoloV3 implementation](https://github.com/ultralytics/yolov3) diff --git a/keras_cv/models/backbones/darknet/darknet_backbone_presets_test.py b/keras_cv/models/backbones/darknet/darknet_backbone_presets_test.py index 3992ffb59a..cd761ea9bc 100644 --- a/keras_cv/models/backbones/darknet/darknet_backbone_presets_test.py +++ b/keras_cv/models/backbones/darknet/darknet_backbone_presets_test.py @@ -11,3 +11,82 @@ # 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. +"""Tests for loading pretrained model presets.""" + +import pytest +import tensorflow as tf +from absl.testing import parameterized + +from keras_cv.models.backbones.darknet.darknet_backbone import DarkNet53Backbone +from keras_cv.models.backbones.darknet.darknet_backbone import DarkNetBackbone + + +@pytest.mark.large +class DarkNetPresetSmokeTest(tf.test.TestCase, parameterized.TestCase): + """ + A smoke test for DarkNet presets we run continuously. + This only tests the smallest weights we have available. Run with: + `pytest keras_cv/models/backbones/darknet/darknet_backbone_presets_test.py --run_large` # noqa: E501 + """ + + def setUp(self): + self.input_batch = tf.ones(shape=(2, 224, 224, 3)) + + def test_backbone_output(self): + model = DarkNetBackbone.from_preset("darknet53") + model(self.input_batch) + + def test_backbone_output_with_weights(self): + model = DarkNetBackbone.from_preset("darknet53_imagenet") + + # The forward pass from a preset should be stable! + # This test should catch cases where we unintentionally change our + # network code in a way that would invalidate our preset weights. + # We should only update these numbers if we are updating a weights + # file, or have found a discrepancy with the upstream source. + + outputs = model(tf.ones(shape=(1, 512, 512, 3))) + expected = [-0.04739833, 2.6341133, -0.03298496, 1.7416457, 0.10866892] + # Keep a high tolerance, so we are robust to different hardware. + self.assertAllClose( + outputs[0, 0, 0, :5], expected, atol=0.01, rtol=0.01 + ) + + def test_applications_model_output(self): + model = DarkNet53Backbone() + model(self.input_batch) + + def test_applications_model_output_with_preset(self): + model = DarkNet53Backbone.from_preset("darknet53_imagenet") + model(self.input_batch) + + def test_preset_docstring(self): + """Check we did our docstring formatting correctly.""" + for name in DarkNetBackbone.presets: + self.assertRegex(DarkNetBackbone.from_preset.__doc__, name) + + def test_unknown_preset_error(self): + # Not a preset name + with self.assertRaises(ValueError): + DarkNetBackbone.from_preset("darknet53_clowntown") + + def test_load_weights_error(self): + # Try to load weights when none available + with self.assertRaises(ValueError): + DarkNetBackbone.from_preset("darknet53", load_weights=True) + + +@pytest.mark.extra_large +class DarkNetPresetFullTest(tf.test.TestCase, parameterized.TestCase): + """ + Test the full enumeration of our preset. + This tests every preset for DarkNet and is only run manually. + Run with: + `pytest keras_cv/models/backbones/darknet/darknet_backbone_presets_test.py --run_extra_large` # noqa: E501 + """ + + def test_load_darknet(self): + input_data = tf.ones(shape=(2, 224, 224, 3)) + for preset in DarkNetBackbone.presets: + model = DarkNetBackbone.from_preset(preset) + model(input_data) diff --git a/keras_cv/models/backbones/darknet/darknet_backbone_test.py b/keras_cv/models/backbones/darknet/darknet_backbone_test.py index f101208ce9..0342ec2254 100644 --- a/keras_cv/models/backbones/darknet/darknet_backbone_test.py +++ b/keras_cv/models/backbones/darknet/darknet_backbone_test.py @@ -12,38 +12,133 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os + import tensorflow as tf from absl.testing import parameterized +from tensorflow import keras + +from keras_cv.models.backbones.darknet.darknet_backbone import DarkNet21Backbone +from keras_cv.models.backbones.darknet.darknet_backbone import DarkNet53Backbone +from keras_cv.models.backbones.darknet.darknet_backbone import DarkNetBackbone +from keras_cv.utils.train import get_feature_extractor + + +class DarkNetBackboneTest(tf.test.TestCase, parameterized.TestCase): + def setUp(self): + self.input_batch = tf.ones(shape=(2, 224, 224, 3)) + + def test_valid_call(self): + model = DarkNetBackbone( + stackwise_blocks=[2, 8, 8, 4], + include_rescaling=False, + ) + model(self.input_batch) + + def test_valid_call_applications_model(self): + model = DarkNet53Backbone() + model(self.input_batch) + + def test_valid_call_with_rescaling(self): + model = DarkNetBackbone( + stackwise_blocks=[2, 8, 8, 4], + include_rescaling=True, + ) + model(self.input_batch) + + @parameterized.named_parameters( + ("tf_format", "tf", "model"), + ("keras_format", "keras_v3", "model.keras"), + ) + def test_saved_model(self, save_format, filename): + model = DarkNetBackbone( + stackwise_blocks=[2, 8, 8, 4], + include_rescaling=False, + ) + model_output = model(self.input_batch) + save_path = os.path.join(self.get_temp_dir(), filename) + model.save(save_path, save_format=save_format) + restored_model = keras.models.load_model(save_path) + + # Check we got the real object back. + self.assertIsInstance(restored_model, DarkNetBackbone) -from keras_cv.models.backbones.darknet import darknet_backbone as darknet -from keras_cv.models.legacy.models_test import ModelsTest + # Check that output matches. + restored_output = restored_model(self.input_batch) + self.assertAllClose(model_output, restored_output) -MODEL_LIST = [ - (darknet.DarkNet21, 512, {}), - (darknet.DarkNet53, 512, {}), -] + @parameterized.named_parameters( + ("tf_format", "tf", "model"), + ("keras_format", "keras_v3", "model.keras"), + ) + def test_saved_alias_model(self, save_format, filename): + model = DarkNet53Backbone() + model_output = model(self.input_batch) + save_path = os.path.join(self.get_temp_dir(), filename) + model.save(save_path, save_format=save_format) + restored_model = keras.models.load_model(save_path) + # Check we got the real object back. + # Note that these aliases serialized as the base class + self.assertIsInstance(restored_model, DarkNetBackbone) -class DarkNetTest(ModelsTest, tf.test.TestCase, parameterized.TestCase): - @parameterized.parameters(*MODEL_LIST) - def test_application_base(self, app, _, args): - super()._test_application_base(app, _, args) + # Check that output matches. + restored_output = restored_model(self.input_batch) + self.assertAllClose(model_output, restored_output) - @parameterized.parameters(*MODEL_LIST) - def test_application_with_rescaling(self, app, last_dim, args): - super()._test_application_with_rescaling(app, last_dim, args) + def test_create_backbone_model_from_alias_model(self): + model = DarkNet53Backbone( + include_rescaling=False, + ) + backbone_model = get_feature_extractor( + model, + model.pyramid_level_inputs.values(), + model.pyramid_level_inputs.keys(), + ) + inputs = tf.keras.Input(shape=[256, 256, 3]) + outputs = backbone_model(inputs) + self.assertLen(outputs, 4) + self.assertEquals(list(outputs.keys()), [2, 3, 4, 5]) + self.assertEquals(outputs[2].shape, [None, 64, 64, 256]) + self.assertEquals(outputs[3].shape, [None, 32, 32, 512]) + self.assertEquals(outputs[4].shape, [None, 16, 16, 1024]) + self.assertEquals(outputs[5].shape, [None, 8, 8, 2048]) - @parameterized.parameters(*MODEL_LIST) - def test_application_pooling(self, app, last_dim, args): - super()._test_application_pooling(app, last_dim, args) + def test_create_backbone_model_with_level_config(self): + model = DarkNetBackbone( + stackwise_blocks=[2, 8, 8, 4], + include_rescaling=False, + input_shape=[256, 256, 3], + ) + levels = [3, 4] + layer_names = [model.pyramid_level_inputs[level] for level in [3, 4]] + backbone_model = get_feature_extractor(model, layer_names, levels) + inputs = tf.keras.Input(shape=[256, 256, 3]) + outputs = backbone_model(inputs) + self.assertLen(outputs, 2) + self.assertEquals(list(outputs.keys()), [3, 4]) + self.assertEquals(outputs[3].shape, [None, 32, 32, 512]) + self.assertEquals(outputs[4].shape, [None, 16, 16, 1024]) - @parameterized.parameters(*MODEL_LIST) - def test_application_variable_input_channels(self, app, last_dim, args): - super()._test_application_variable_input_channels(app, last_dim, args) + @parameterized.named_parameters( + ("one_channel", 1), + ("four_channels", 4), + ) + def test_application_variable_input_channels(self, num_channels): + model = DarkNetBackbone( + stackwise_blocks=[2, 8, 8, 4], + input_shape=(None, None, num_channels), + include_rescaling=False, + ) + self.assertEqual(model.output_shape, (None, None, None, 2048)) - @parameterized.parameters(*MODEL_LIST) - def test_model_can_be_used_as_backbone(self, app, last_dim, args): - super()._test_model_can_be_used_as_backbone(app, last_dim, args) + @parameterized.named_parameters( + ("21", DarkNet21Backbone), + ("53", DarkNet53Backbone), + ) + def test_specific_arch_forward_pass(self, arch_class): + backbone = arch_class() + backbone(tf.random.uniform(shape=[2, 256, 256, 3])) if __name__ == "__main__": From a28f133fd045faebc7ddc2665d76c4430d7935c9 Mon Sep 17 00:00:00 2001 From: ID6109 Date: Fri, 9 Jun 2023 23:39:12 +0530 Subject: [PATCH 8/8] Fixing unit tests --- keras_cv/models/backbones/darknet/darknet_backbone_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/keras_cv/models/backbones/darknet/darknet_backbone_test.py b/keras_cv/models/backbones/darknet/darknet_backbone_test.py index 0342ec2254..2cecfb0ad9 100644 --- a/keras_cv/models/backbones/darknet/darknet_backbone_test.py +++ b/keras_cv/models/backbones/darknet/darknet_backbone_test.py @@ -117,8 +117,8 @@ def test_create_backbone_model_with_level_config(self): outputs = backbone_model(inputs) self.assertLen(outputs, 2) self.assertEquals(list(outputs.keys()), [3, 4]) - self.assertEquals(outputs[3].shape, [None, 32, 32, 512]) - self.assertEquals(outputs[4].shape, [None, 16, 16, 1024]) + self.assertEquals(outputs[3].shape, [None, 128, 128, 64]) + self.assertEquals(outputs[4].shape, [None, 64, 64, 128]) @parameterized.named_parameters( ("one_channel", 1), @@ -130,7 +130,7 @@ def test_application_variable_input_channels(self, num_channels): input_shape=(None, None, num_channels), include_rescaling=False, ) - self.assertEqual(model.output_shape, (None, None, None, 2048)) + self.assertEqual(model.output_shape, (None, None, None, 512)) @parameterized.named_parameters( ("21", DarkNet21Backbone),