diff --git a/keras_cv/models/__init__.py b/keras_cv/models/__init__.py index d5cc7b3c28..3e0cb0b69a 100644 --- a/keras_cv/models/__init__.py +++ b/keras_cv/models/__init__.py @@ -31,6 +31,9 @@ from keras_cv.models.backbones.csp_darknet.csp_darknet_backbone import ( CSPDarkNetXLBackbone, ) +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.densenet.densenet_aliases import ( DenseNet121Backbone, ) diff --git a/keras_cv/models/backbones/backbone_presets.py b/keras_cv/models/backbones/backbone_presets.py index cfd2c0d509..284ee4dace 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.densenet import densenet_backbone_presets from keras_cv.models.backbones.efficientnet_v2 import ( efficientnet_v2_backbone_presets, @@ -30,6 +31,7 @@ **csp_darknet_backbone_presets.backbone_presets_no_weights, **efficientnet_v2_backbone_presets.backbone_presets_no_weights, **densenet_backbone_presets.backbone_presets_no_weights, + **darknet_backbone_presets.backbone_presets_no_weights, } backbone_presets_with_weights = { @@ -39,6 +41,7 @@ **csp_darknet_backbone_presets.backbone_presets_with_weights, **efficientnet_v2_backbone_presets.backbone_presets_with_weights, **densenet_backbone_presets.backbone_presets_with_weights, + **darknet_backbone_presets.backbone_presets_with_weights, } backbone_presets = { 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..470d7c4d3c --- /dev/null +++ b/keras_cv/models/backbones/darknet/darknet_backbone.py @@ -0,0 +1,293 @@ +# 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. + +"""DarkNet backbone model. +Reference: + - [YoloV3 Paper](https://arxiv.org/abs/1804.02767) + - [YoloV3 implementation](https://github.com/ultralytics/yolov3) +""" + +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, +) +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.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.utils.python_utils import classproperty + + +@keras.utils.register_keras_serializable(package="keras_cv.models") +class DarkNetBackbone(Backbone): + + """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: + 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. + 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)) + + # 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, + stackwise_blocks, + include_rescaling, + input_shape=(None, None, 3), + input_tensor=None, + **kwargs, + ): + inputs = utils.parse_model_inputs(input_shape, input_tensor) + + x = inputs + if include_rescaling: + x = layers.Rescaling(1 / 255.0)(x) + + # stem + pyramid_level_inputs = {} + x = DarknetConvBlock( + filters=32, + kernel_size=3, + strides=1, + 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] + + # layer_num is used for naming the residual blocks + # (starts with dark2, hence 2) + layer_num = 2 + + for filter, block in zip(filters, stackwise_blocks): + x = ResidualBlocks( + filters=filter, + num_blocks=block, + 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( + filters=512, + kernel_size=1, + strides=1, + activation="leaky_relu", + name="dark5_conv1", + )(x) + pyramid_level_inputs[8] = x.node.layer.name + x = DarknetConvBlock( + filters=1024, + kernel_size=3, + strides=1, + 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) + x = DarknetConvBlock( + filters=1024, + kernel_size=3, + strides=1, + activation="leaky_relu", + name="dark5_conv3", + )(x) + pyramid_level_inputs[10] = x.node.layer.name + x = DarknetConvBlock( + filters=512, + kernel_size=1, + strides=1, + activation="leaky_relu", + name="dark5_conv4", + )(x) + pyramid_level_inputs[11] = x.node.layer.name + + super().__init__(inputs=inputs, outputs=x, **kwargs) + + self.pyramid_level_inputs = pyramid_level_inputs + self.stackwise_blocks = stackwise_blocks + self.include_rescaling = include_rescaling + self.input_tensor = input_tensor + + def get_config(self): + config = super().get_config() + config.update( + { + "stackwise_blocks": self.stackwise_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) + + +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, + 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("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__", ALIAS_DOCSTRING.format(num_layers=21)) +setattr(DarkNet53Backbone, "__doc__", ALIAS_DOCSTRING.format(num_layers=53)) 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..04a72d8582 --- /dev/null +++ b/keras_cv/models/backbones/darknet/darknet_backbone_presets.py @@ -0,0 +1,61 @@ +# 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. +"""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, +} 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..cd761ea9bc --- /dev/null +++ b/keras_cv/models/backbones/darknet/darknet_backbone_presets_test.py @@ -0,0 +1,92 @@ +# 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. +"""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 new file mode 100644 index 0000000000..2cecfb0ad9 --- /dev/null +++ b/keras_cv/models/backbones/darknet/darknet_backbone_test.py @@ -0,0 +1,145 @@ +# 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. + +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) + + # Check that output matches. + restored_output = restored_model(self.input_batch) + self.assertAllClose(model_output, restored_output) + + @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) + + # Check that output matches. + restored_output = restored_model(self.input_batch) + self.assertAllClose(model_output, restored_output) + + 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]) + + 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, 128, 128, 64]) + self.assertEquals(outputs[4].shape, [None, 64, 64, 128]) + + @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, 512)) + + @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__": + tf.test.main() diff --git a/keras_cv/models/legacy/__init__.py b/keras_cv/models/legacy/__init__.py index a5d346d5f0..7f3de58568 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.efficientnet_lite import EfficientNetLiteB0 from keras_cv.models.legacy.efficientnet_lite import EfficientNetLiteB1 from keras_cv.models.legacy.efficientnet_lite import EfficientNetLiteB2 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()