diff --git a/README.md b/README.md
index 8b584ce2..5fbf2212 100644
--- a/README.md
+++ b/README.md
@@ -121,6 +121,7 @@ The Markdown sources live in [`docs/`](docs/) if you would rather read them in t
| PoolFormer | [MetaFormer is Actually What You Need for Vision](https://arxiv.org/abs/2111.11418) | `timm` |
| PVT | [Pyramid Vision Transformer: A Versatile Backbone for Dense Prediction without Convolutions](https://arxiv.org/abs/2102.12122) | `transformers` |
| PVTv2 | [PVTv2: Improved Baselines with Pyramid Vision Transformer](https://arxiv.org/abs/2106.13797) | `transformers` |
+ | RegNet | [Designing Network Design Spaces](https://arxiv.org/abs/2003.13678) | `transformers` |
| Res2Net | [Res2Net: A New Multi-scale Backbone Architecture](https://arxiv.org/abs/1904.01169) | `timm` |
| ResMLP | [ResMLP: Feedforward networks for image classification with data-efficient training](https://arxiv.org/abs/2105.03404) | `timm` |
| ResNet | [Deep Residual Learning for Image Recognition](https://arxiv.org/abs/1512.03385) | `timm` |
diff --git a/docs/regnet.md b/docs/regnet.md
new file mode 100644
index 00000000..65c7a225
--- /dev/null
+++ b/docs/regnet.md
@@ -0,0 +1,165 @@
+# RegNet
+
+
+
Weights: pretrained Keras weights live on Hugging Face under
+
zeromodels/regnet-<variant>
+(12
X + 12
Y variants; each repo carries
zm_config.json +
+
model.weights.h5). Load with
+
from_weights("zeromodels/regnet-y-040").
+
+
+RegNet (Designing Network Design Spaces) is a family of ConvNets whose per-stage widths and
+depths follow a simple **quantized-linear rule** found by searching design spaces rather than
+tuning individual architectures. It is a 3x3 stride-2 stem followed by **four stages** of
+residual blocks; each block is a `1x1 -> 3x3 grouped -> 1x1` bottleneck, and the **RegNet-Y**
+variant inserts a **Squeeze-and-Excitation** module. The 3x3 convolution is split into
+`out_channels // groups_width` groups. Width grows and resolution halves each stage, so a
+single backbone yields a standard CNN feature pyramid usable for classification and dense
+prediction.
+
+**Paper**: [Designing Network Design Spaces](https://arxiv.org/abs/2003.13678)
+
+RegNet comes in two families: **X** (plain bottleneck) and **Y** (+ Squeeze-and-Excitation,
+the stronger and more common one).
+
+## API
+
+### RegNetImageClassify
+
+```python
+RegNetImageClassify(
+ embedding_size=32,
+ hidden_sizes=(128, 192, 512, 1088),
+ depths=(2, 6, 12, 2),
+ groups_width=64,
+ layer_type="y",
+ downsample_in_first_stage=True,
+ image_size=224,
+ include_normalization=True,
+ normalization_mode="imagenet",
+ num_classes=1000,
+ classifier_activation="linear",
+ name="RegNetImageClassify",
+)
+```
+
+The classifier: the backbone plus a GlobalAveragePooling2D + dense head.
+`include_normalization=True` means the model takes **raw `[0, 255]` pixels** and applies
+ImageNet mean/std internally, so there is no separate image processor to construct.
+
+**Parameters**
+
+- **embedding_size** (`int`): output width of the 3x3 stride-2 stem.
+- **hidden_sizes** / **depths** (`tuple`): per-stage output width and block count.
+- **groups_width** (`int`): channels per group of the 3x3 grouped convolution (a block's group count is `out_channels // groups_width`).
+- **layer_type** (`str`): `"y"` (adds Squeeze-and-Excitation) or `"x"`.
+- **downsample_in_first_stage** (`bool`): whether the first stage downsamples. `True` for the standard checkpoints (RegNet has no pooling stem).
+- **image_size** (`int`, *optional*, defaults to `224`): resolution the model is built for.
+- **include_normalization** (`bool`, *optional*, defaults to `True`): bake ImageNet normalization into the graph.
+- **num_classes** (`int`, *optional*, defaults to `1000`): classifier outputs.
+
+`from_weights` fills the architectural fields from the variant's config, so you normally pass
+only the repo id.
+
+**Call** `model(pixel_values, training=False)`. **Returns** class logits of shape
+`(B, num_classes)`.
+
+### RegNetModel
+
+The backbone alone. With `as_backbone=True` it returns the four stage feature maps
+(the pyramid) instead of just the last one, for detection or segmentation necks.
+
+```python
+RegNetModel(as_backbone=False, layer_type="y", groups_width=64, ..., include_normalization=True)
+```
+
+### RegNetConfig
+
+Typed config (`model_type="regnet"`) holding the fields above; serialized into each Hub repo's
+`zm_config.json`.
+
+## Model Variants
+
+For `RegNetImageClassify.from_weights("zeromodels/regnet-")`. The number is the
+model's compute in units of 0.1 GFLOPs (`002` = 0.2 GF ... `320` = 32 GF).
+
+| Family | Variants |
+|------------------------|--------------------------------------------------------------------------|
+| **RegNet-X** (plain) | `regnet-x-{002,004,006,008,016,032,040,064,080,120,160,320}` |
+| **RegNet-Y** (+ SE) | `regnet-y-{002,004,006,008,016,032,040,064,080,120,160,320}` |
+
+At matched compute the Y family (with Squeeze-and-Excitation) is generally stronger; e.g.
+`regnet-y-320` reaches ~80.9% ImageNet-1k top-1. All are 224x224, 1000 classes. The larger
+self-supervised `facebook/regnet-y-{320,640,1280,10b}-seer` checkpoints are not mirrored here
+but load on the fly with the `hf:` prefix (see [below](#loading-fine-tuned-and-community-weights)).
+
+## Basic Usage
+
+```python
+import keras
+import numpy as np
+from PIL import Image
+from zeromodels.models.regnet import RegNetImageClassify
+
+model = RegNetImageClassify.from_weights("zeromodels/regnet-y-040")
+
+image = Image.open("assets/data/coco_bear.jpg").convert("RGB").resize((224, 224))
+pixels = np.asarray(image, "float32")[None] # (1, 224, 224, 3), raw [0, 255]
+
+logits = model(pixels, training=False)
+top5 = np.argsort(keras.ops.convert_to_numpy(logits)[0])[-5:][::-1]
+print("top-5 ImageNet-1k class ids:", top5.tolist())
+```
+
+Normalization is inside the model, so pass raw pixels. Map the class ids to the
+[ImageNet-1k label list](https://huggingface.co/datasets/imagenet-1k) to read names.
+
+## Feature Pyramid
+
+For detection / segmentation, take the four stage outputs:
+
+```python
+from zeromodels.models.regnet import RegNetModel
+
+backbone = RegNetModel.from_weights("zeromodels/regnet-y-040", as_backbone=True)
+feats = backbone(np.zeros((1, 224, 224, 3), "float32"), training=False)
+print([tuple(f.shape) for f in feats])
+# spatial 56 / 28 / 14 / 7 (strides 4, 8, 16, 32); channels are the variant's hidden_sizes
+```
+
+The strides are 4, 8, 16, 32, matching a standard CNN backbone. RegNet is fully
+convolutional (no learned position embeddings), so any input resolution works with no
+weight interpolation: build the model at the target `image_size`.
+
+## Data Format
+
+**The model supports both `channels_last` and `channels_first`, and the two are
+bit-exact.** A model reads `keras.config.image_data_format()` when it is **constructed**
+(there is no `data_format` argument); set the format before building.
+
+```python
+import keras
+
+keras.config.set_image_data_format("channels_first")
+model = RegNetImageClassify.from_weights(
+ "zeromodels/regnet-y-040"
+) # expects (B, 3, H, W)
+```
+
+## Loading Fine-tuned and Community Weights
+
+The `zeromodels/regnet-*` repos above are pre-converted. Any **other** Hugging Face repo whose
+`model_type` is `"regnet"` (the upstream `facebook/regnet-*` and `-seer` checkpoints, or any
+fine-tune) loads with the `hf:` prefix, converting on the fly:
+
+```python
+from zeromodels.models.regnet import RegNetImageClassify
+
+model = RegNetImageClassify.from_weights("hf:facebook/regnet-x-320")
+model = RegNetImageClassify.from_weights("hf:/regnet-finetuned-on-my-data")
+
+# Architecture only, randomly initialized
+model = RegNetImageClassify.from_weights("hf:facebook/regnet-y-040", load_weights=False)
+```
+
+`RegNetModel` accepts `hf:` the same way.
diff --git a/tests/base/model_test_registry.py b/tests/base/model_test_registry.py
index e352be92..46d19d47 100644
--- a/tests/base/model_test_registry.py
+++ b/tests/base/model_test_registry.py
@@ -423,6 +423,22 @@
"input_shape": (2, 32, 32, 3),
"expected_output_shape": (2, 1000),
},
+ "RegNetImageClassify": {
+ "module": "zeromodels.models.regnet",
+ "model_cls": "RegNetImageClassify",
+ "model_type": "classification",
+ "init_kwargs": {
+ "embedding_size": 8,
+ "hidden_sizes": (16, 32, 64, 128),
+ "depths": (1, 1, 1, 1),
+ "groups_width": 8,
+ "layer_type": "y",
+ "image_size": (32, 32, 3),
+ "num_classes": 1000,
+ },
+ "input_shape": (2, 32, 32, 3),
+ "expected_output_shape": (2, 1000),
+ },
"Res2NetImageClassify": {
"module": "zeromodels.models.res2net",
"model_cls": "Res2NetImageClassify",
diff --git a/website/mkdocs.yml b/website/mkdocs.yml
index 5d449ba3..d37d9d36 100644
--- a/website/mkdocs.yml
+++ b/website/mkdocs.yml
@@ -184,6 +184,7 @@ nav:
- MobileViTV2: mobilevitv2.md
- PVT: pvt.md
- PVTv2: pvt_v2.md
+ - RegNet: regnet.md
- RF-DETR: rf_detr.md
- RT-DETR: rt_detr.md
- RT-DETRv2: rt_detr_v2.md
diff --git a/zeromodels/conversion/weight_transfer_util.py b/zeromodels/conversion/weight_transfer_util.py
index 7aeebece..678c2c4e 100644
--- a/zeromodels/conversion/weight_transfer_util.py
+++ b/zeromodels/conversion/weight_transfer_util.py
@@ -212,6 +212,7 @@ def transform_conv_weights(
"sr",
"reduction", # spatial_reduction / sequence_reduction (PVT SRA conv)
"proj", # patch-embed proj / projection conv (only reached for 4D weights)
+ "attention", # Squeeze-and-Excitation 1x1 gate conv (RegNet-Y SE)
]
):
# Standard 2D convolution
diff --git a/zeromodels/models/__init__.py b/zeromodels/models/__init__.py
index 16eeb55c..4d3ca1ed 100644
--- a/zeromodels/models/__init__.py
+++ b/zeromodels/models/__init__.py
@@ -100,6 +100,7 @@
qwen3_next,
qwen3_vl,
qwen3_vl_moe,
+ regnet,
res2net,
resmlp,
resnet,
diff --git a/zeromodels/models/regnet/__init__.py b/zeromodels/models/regnet/__init__.py
new file mode 100644
index 00000000..da84ecc3
--- /dev/null
+++ b/zeromodels/models/regnet/__init__.py
@@ -0,0 +1,4 @@
+from zeromodels.models.regnet.regnet_config import RegNetConfig
+from zeromodels.models.regnet.regnet_model import RegNetImageClassify, RegNetModel
+
+__all__ = ["RegNetImageClassify", "RegNetModel", "RegNetConfig"]
diff --git a/zeromodels/models/regnet/convert_regnet_hf_to_keras.py b/zeromodels/models/regnet/convert_regnet_hf_to_keras.py
new file mode 100644
index 00000000..7c98a097
--- /dev/null
+++ b/zeromodels/models/regnet/convert_regnet_hf_to_keras.py
@@ -0,0 +1,136 @@
+import gc
+import json
+
+import keras
+from tqdm import tqdm
+
+from zeromodels.conversion import verify_cls_model_equivalence
+from zeromodels.conversion.exceptions import (
+ WeightMappingError,
+ WeightShapeMismatchError,
+)
+from zeromodels.conversion.hf_download_utils import download_hf_state_dict
+from zeromodels.conversion.weight_split_util import split_model_weights
+from zeromodels.conversion.weight_transfer_util import (
+ compare_keras_torch_names,
+ transfer_weights,
+)
+from zeromodels.models.regnet import RegNetImageClassify
+
+# Hosted variant -> HF (transformers) repo id, for every standard RegNet X/Y FLOP
+# variant (002=0.2 GF ... 320=32 GF). The arch is read from each repo's config.json
+# via RegNetImageClassify.config_from_hf, so no per-variant arch table is needed
+# here; ``hf:facebook/regnet-*`` also loads any of these on the fly. (The larger
+# self-supervised "-seer" checkpoints are not listed but load the same way.)
+REGNET_FLOPS = (
+ "002",
+ "004",
+ "006",
+ "008",
+ "016",
+ "032",
+ "040",
+ "064",
+ "080",
+ "120",
+ "160",
+ "320",
+)
+REGNET_VARIANTS = {
+ f"regnet_{layer_type}_{flops}": f"facebook/regnet-{layer_type}-{flops}"
+ for layer_type in ("x", "y")
+ for flops in REGNET_FLOPS
+}
+
+# keras weight name ``{layer.name}_{weight.name}`` -> HF (torch) name. The keras
+# layers are named as the HF module path with ``.`` replaced by ``_``, so ``_ ->
+# .`` alone reconstructs the path; the rest renames Keras weight suffixes.
+WEIGHT_NAME_MAPPING = {
+ "_": ".",
+ "kernel": "weight",
+ "gamma": "weight",
+ "beta": "bias",
+ "moving.mean": "running_mean",
+ "moving.variance": "running_var",
+}
+
+
+def transfer_regnet_weights(keras_model, state_dict):
+ trainable, non_trainable = split_model_weights(keras_model)
+
+ for keras_weight, keras_name in tqdm(
+ trainable + non_trainable, desc="Transferring weights to Keras"
+ ):
+ torch_name = keras_name
+ for old, new in WEIGHT_NAME_MAPPING.items():
+ torch_name = torch_name.replace(old, new)
+
+ if torch_name not in state_dict:
+ raise WeightMappingError(keras_name, torch_name)
+
+ torch_weight = state_dict[torch_name]
+ if not compare_keras_torch_names(
+ keras_name, keras_weight, torch_name, torch_weight
+ ):
+ raise WeightShapeMismatchError(
+ keras_name, keras_weight.shape, torch_name, torch_weight.shape
+ )
+ transfer_weights(keras_name, keras_weight, torch_weight)
+
+
+if __name__ == "__main__":
+ import importlib.metadata as _meta
+
+ _orig_version = _meta.version
+ _meta.version = lambda name: (
+ "0.23.0" if name == "tokenizers" else _orig_version(name)
+ )
+ import torch
+ import transformers
+ from huggingface_hub import hf_hub_download
+
+ # Compare in true float32: cuDNN TF32 on a GPU inflates the conv/BN diff to
+ # ~1e-2 (HF runs on CPU), which can spuriously trip the 1e-2 parity gate on the
+ # SE ("y") variants. Disabling TF32 restores the real ~5e-6 conversion fidelity.
+ torch.backends.cudnn.allow_tf32 = False
+ torch.backends.cuda.matmul.allow_tf32 = False
+
+ for variant, hf_id in REGNET_VARIANTS.items():
+ print(f"\n{'=' * 60}")
+ print(f"Converting: {variant} <- {hf_id}")
+ print(f"{'=' * 60}")
+
+ with open(hf_hub_download(hf_id, "config.json"), encoding="utf-8") as f:
+ hf_config = json.load(f)
+ state = download_hf_state_dict(hf_id)
+ keras_model = RegNetImageClassify(
+ **RegNetImageClassify.config_from_hf(hf_config),
+ include_normalization=False,
+ )
+ transfer_regnet_weights(keras_model, state)
+
+ hf_model = transformers.RegNetForImageClassification.from_pretrained(
+ hf_id
+ ).eval()
+ results = verify_cls_model_equivalence(
+ model_a=hf_model,
+ model_b=keras_model,
+ input_shape=keras_model.input_shape[1:],
+ output_specs={"num_classes": keras_model.output_shape[-1]},
+ comparison_type="hf_to_keras",
+ run_performance=False,
+ atol=1e-2,
+ rtol=1e-2,
+ )
+ if not results["standard_input"]:
+ raise ValueError(
+ "Model equivalence test failed - outputs do not match for standard input"
+ )
+
+ out_path = f"{variant}.weights.h5"
+ keras_model.save_weights(out_path)
+ print(f" Saved -> {out_path}")
+
+ del keras_model, state, hf_model
+ keras.backend.clear_session()
+ gc.collect()
diff --git a/zeromodels/models/regnet/regnet_config.py b/zeromodels/models/regnet/regnet_config.py
new file mode 100644
index 00000000..8df5256e
--- /dev/null
+++ b/zeromodels/models/regnet/regnet_config.py
@@ -0,0 +1,56 @@
+from zeromodels.base import BaseConfig
+
+
+class RegNetConfig(BaseConfig):
+ r"""Configuration for [`RegNetModel`] / [`RegNetImageClassify`].
+
+ RegNet (Designing Network Design Spaces) is a quantized-linear ConvNet: a
+ 3x3 stride-2 stem followed by four stages of residual blocks whose width and
+ depth follow a simple parametric rule. Each block is a 1x1 -> 3x3 grouped ->
+ 1x1 bottleneck (the ``"y"`` variant adds a Squeeze-and-Excitation module),
+ with the 3x3 convolution split into ``out_channels // groups_width`` groups.
+ The defaults describe ``regnet-y-040``; the hosted variants override
+ ``hidden_sizes`` / ``depths`` / ``groups_width``. One ``zm_config.json``
+ (declaring the canonical [`RegNetImageClassify`]) sits on each variant's repo,
+ and both the backbone and the classifier load from it. Fields mirror the model
+ constructor and serialize flat.
+
+ Args:
+ embedding_size (`int`, *optional*, defaults to 32):
+ Output width of the 3x3 stride-2 stem.
+ hidden_sizes (`tuple`, *optional*, defaults to `(128, 192, 512, 1088)`):
+ Output width per stage.
+ depths (`tuple`, *optional*, defaults to `(2, 6, 12, 2)`):
+ Number of residual blocks per stage.
+ groups_width (`int`, *optional*, defaults to 64):
+ Channels per group of the 3x3 grouped convolution (the group count of
+ a block is ``out_channels // groups_width``).
+ layer_type (`str`, *optional*, defaults to `"y"`):
+ Block variant: `"y"` adds a Squeeze-and-Excitation module, `"x"` does
+ not.
+ downsample_in_first_stage (`bool`, *optional*, defaults to `True`):
+ Whether the first stage downsamples (stride 2). RegNet has no
+ pooling stem, so this is `True` for the standard checkpoints.
+ num_classes (`int`, *optional*, defaults to 1000):
+ Number of classifier output classes (used by
+ [`RegNetImageClassify`]; the backbone ignores it).
+
+ Examples:
+
+ ```python
+ >>> from zeromodels.models.regnet import RegNetConfig, RegNetImageClassify
+
+ >>> configuration = RegNetConfig()
+ >>> model = RegNetImageClassify(configuration)
+ >>> configuration = model.config
+ ```"""
+
+ model_type = "regnet"
+
+ embedding_size: int = 32
+ hidden_sizes: tuple = (128, 192, 512, 1088)
+ depths: tuple = (2, 6, 12, 2)
+ groups_width: int = 64
+ layer_type: str = "y"
+ downsample_in_first_stage: bool = True
+ num_classes: int = 1000
diff --git a/zeromodels/models/regnet/regnet_model.py b/zeromodels/models/regnet/regnet_model.py
new file mode 100644
index 00000000..a261a39c
--- /dev/null
+++ b/zeromodels/models/regnet/regnet_model.py
@@ -0,0 +1,533 @@
+import keras
+from keras import layers, utils
+
+from zeromodels.base import BaseModel
+from zeromodels.conversion import copy_weights_by_path_suffix
+from zeromodels.utils import standardize_input_shape
+from zeromodels.utils.image_util import normalize_image_for_classify_models
+
+from .regnet_config import RegNetConfig
+
+REGNET_HUB_SIBLINGS = frozenset({"RegNetModel", "RegNetImageClassify"})
+
+
+def regnet_conv_layer(
+ x,
+ filters,
+ channels_axis,
+ data_format,
+ kernel_size=3,
+ strides=1,
+ groups=1,
+ use_activation=True,
+ name=None,
+):
+ """RegNetConvLayer: bias-free conv, batch norm, optional ReLU.
+
+ Args:
+ x: Input tensor.
+ filters: Number of output channels.
+ channels_axis: Channel axis (-1 for channels_last, 1 for channels_first).
+ data_format: ``"channels_last"`` or ``"channels_first"``.
+ kernel_size: Convolution kernel size.
+ strides: Convolution stride.
+ groups: Number of groups for the grouped convolution.
+ use_activation: Whether to apply a ReLU after the batch norm.
+ name: Prefix; the conv is ``{name}_convolution`` and the norm is
+ ``{name}_normalization`` (matching the HF module path).
+
+ Returns:
+ Output tensor for the block.
+ """
+ if strides > 1:
+ pad = kernel_size // 2
+ x = layers.ZeroPadding2D(padding=(pad, pad), data_format=data_format)(x)
+ padding = "valid"
+ else:
+ padding = "same"
+
+ x = layers.Conv2D(
+ filters,
+ kernel_size,
+ strides=strides,
+ padding=padding,
+ groups=groups,
+ use_bias=False,
+ data_format=data_format,
+ name=f"{name}_convolution",
+ )(x)
+ x = layers.BatchNormalization(
+ axis=channels_axis, epsilon=1e-5, momentum=0.1, name=f"{name}_normalization"
+ )(x)
+ if use_activation:
+ x = layers.ReLU()(x)
+ return x
+
+
+def regnet_se_layer(x, reduced_channels, channels_axis, data_format, name=None):
+ """Squeeze-and-Excitation (RegNetSELayer): pooled 1x1 conv bottleneck gate.
+
+ Mirrors HF's ``pooler -> attention[0] (conv) -> ReLU -> attention[2] (conv)
+ -> Sigmoid -> multiply`` using 1x1 convolutions (both biased) so the weights
+ map directly.
+ """
+ filters = x.shape[channels_axis]
+ se = layers.GlobalAveragePooling2D(data_format=data_format, keepdims=True)(x)
+ se = layers.Conv2D(
+ reduced_channels,
+ 1,
+ use_bias=True,
+ data_format=data_format,
+ name=f"{name}_attention_0",
+ )(se)
+ se = layers.ReLU()(se)
+ se = layers.Conv2D(
+ filters,
+ 1,
+ use_bias=True,
+ activation="sigmoid",
+ data_format=data_format,
+ name=f"{name}_attention_2",
+ )(se)
+ return layers.Multiply()([x, se])
+
+
+def regnet_block(
+ x,
+ in_channels,
+ out_channels,
+ groups_width,
+ layer_type,
+ channels_axis,
+ data_format,
+ strides=1,
+ name=None,
+):
+ """One RegNet residual block (``"x"`` bottleneck or ``"y"`` = + SE).
+
+ ``1x1 -> 3x3 grouped (stride) -> [SE] -> 1x1`` with a strided 1x1 shortcut when
+ the shape changes, then a residual add and ReLU.
+ """
+ groups = max(1, out_channels // groups_width)
+ should_shortcut = in_channels != out_channels or strides != 1
+
+ residual = x
+ if should_shortcut:
+ residual = regnet_conv_layer(
+ x,
+ out_channels,
+ kernel_size=1,
+ strides=strides,
+ use_activation=False,
+ channels_axis=channels_axis,
+ data_format=data_format,
+ name=f"{name}_shortcut",
+ )
+
+ h = regnet_conv_layer(
+ x,
+ out_channels,
+ kernel_size=1,
+ channels_axis=channels_axis,
+ data_format=data_format,
+ name=f"{name}_layer_0",
+ )
+ h = regnet_conv_layer(
+ h,
+ out_channels,
+ kernel_size=3,
+ strides=strides,
+ groups=groups,
+ channels_axis=channels_axis,
+ data_format=data_format,
+ name=f"{name}_layer_1",
+ )
+ if layer_type == "y":
+ h = regnet_se_layer(
+ h,
+ reduced_channels=int(round(in_channels / 4)),
+ channels_axis=channels_axis,
+ data_format=data_format,
+ name=f"{name}_layer_2",
+ )
+ last_idx = 3
+ else:
+ last_idx = 2
+ h = regnet_conv_layer(
+ h,
+ out_channels,
+ kernel_size=1,
+ use_activation=False,
+ channels_axis=channels_axis,
+ data_format=data_format,
+ name=f"{name}_layer_{last_idx}",
+ )
+
+ h = layers.Add()([h, residual])
+ h = layers.ReLU()(h)
+ return h
+
+
+def regnet_backbone_feature(
+ inputs,
+ embedding_size,
+ hidden_sizes,
+ depths,
+ groups_width,
+ layer_type,
+ downsample_in_first_stage,
+ channels_axis,
+ data_format,
+ return_stages=False,
+):
+ """Build the RegNet stem + four stages.
+
+ Returns the final stage feature map, or a list of per-stage feature maps
+ (one per stage) when ``return_stages=True``.
+ """
+ x = regnet_conv_layer(
+ inputs,
+ embedding_size,
+ kernel_size=3,
+ strides=2,
+ channels_axis=channels_axis,
+ data_format=data_format,
+ name="regnet_embedder_embedder",
+ )
+
+ in_channels = embedding_size
+ stages = []
+ for i, (out_channels, depth) in enumerate(zip(hidden_sizes, depths)):
+ stage_stride = 2 if (i > 0 or downsample_in_first_stage) else 1
+ for j in range(depth):
+ x = regnet_block(
+ x,
+ in_channels if j == 0 else out_channels,
+ out_channels,
+ groups_width=groups_width,
+ layer_type=layer_type,
+ channels_axis=channels_axis,
+ data_format=data_format,
+ strides=stage_stride if j == 0 else 1,
+ name=f"regnet_encoder_stages_{i}_layers_{j}",
+ )
+ in_channels = out_channels
+ stages.append(x)
+
+ if return_stages:
+ return stages
+ return x
+
+
+@keras.saving.register_keras_serializable(package="zeromodels")
+class RegNetModel(BaseModel):
+ """Instantiates the RegNet backbone.
+
+ RegNet is a quantized-linear ConvNet: a 3x3 stride-2 stem feeds four stages
+ of ``1x1 -> 3x3 grouped -> [SE] -> 1x1`` residual blocks whose widths and
+ depths follow a simple parametric rule. The output tensor is the last layer
+ output before the classifier head: the final stage's 4D feature map
+ ``(B, H, W, C)``, unpooled and head-free. :class:`RegNetImageClassify`
+ composes this model and applies a GlobalAveragePooling2D + Dense head to
+ produce logits.
+
+ References:
+ - [Designing Network Design Spaces](https://arxiv.org/abs/2003.13678)
+
+ Args:
+ embedding_size: Integer, output width of the stem. Defaults to `32`.
+ hidden_sizes: Tuple of ints, output width per stage.
+ Defaults to `(128, 192, 512, 1088)`.
+ depths: Tuple of ints, number of blocks per stage.
+ Defaults to `(2, 6, 12, 2)`.
+ groups_width: Integer, channels per group of the 3x3 grouped conv (the
+ group count of a block is ``out_channels // groups_width``).
+ Defaults to `64`.
+ layer_type: String, `"y"` (with Squeeze-and-Excitation) or `"x"`.
+ Defaults to `"y"`.
+ downsample_in_first_stage: Boolean, whether the first stage downsamples
+ (stride 2). Defaults to `True`.
+ include_normalization: Boolean, whether to prepend image normalization.
+ When True, inputs should be uint8 in ``[0, 255]``. Defaults to `True`.
+ normalization_mode: String normalization preset. Defaults to
+ `"imagenet"`. Only used when ``include_normalization=True``.
+ image_size: Input image spec (int, ``(H, W)``, or a 3-tuple ordered for
+ the active ``keras.config.image_data_format()``). Defaults to `224`.
+ input_tensor: Optional Keras tensor as input. Defaults to `None`.
+ as_backbone: Boolean, when True returns a list of per-stage feature maps.
+ Defaults to `False`.
+ name: String model name. Defaults to `"RegNetModel"`.
+
+ Returns:
+ A Keras `Model` instance.
+ """
+
+ BASE_WEIGHT_CONFIG = None
+ config_class = RegNetConfig
+ HUB_REPO_SIBLINGS = REGNET_HUB_SIBLINGS
+ HF_MODEL_TYPE = "regnet"
+
+ @classmethod
+ def from_hub_repo(cls, repo_id, load_weights=True, skip_mismatch=False, **kwargs):
+ model = cls.build_from_hub_repo(repo_id, **kwargs)
+ if load_weights:
+ src = RegNetImageClassify.from_weights(repo_id, skip_mismatch=skip_mismatch)
+ copy_weights_by_path_suffix(src, model)
+ del src
+ return model
+
+ @classmethod
+ def config_from_hf(cls, hf_config):
+ return {
+ "embedding_size": hf_config["embedding_size"],
+ "hidden_sizes": tuple(hf_config["hidden_sizes"]),
+ "depths": tuple(hf_config["depths"]),
+ "groups_width": hf_config["groups_width"],
+ "layer_type": hf_config["layer_type"],
+ "downsample_in_first_stage": hf_config.get(
+ "downsample_in_first_stage", True
+ ),
+ }
+
+ @classmethod
+ def transfer_from_hf(cls, keras_model, state_dict):
+ from .convert_regnet_hf_to_keras import transfer_regnet_weights
+
+ transfer_regnet_weights(keras_model, state_dict)
+
+ def __init__(
+ self,
+ embedding_size=32,
+ hidden_sizes=(128, 192, 512, 1088),
+ depths=(2, 6, 12, 2),
+ groups_width=64,
+ layer_type="y",
+ downsample_in_first_stage=True,
+ image_size=224,
+ include_normalization=True,
+ normalization_mode="imagenet",
+ input_tensor=None,
+ as_backbone=False,
+ name="RegNetModel",
+ **kwargs,
+ ):
+ for k in ("num_classes", "classifier_activation"):
+ kwargs.pop(k, None)
+
+ data_format = keras.config.image_data_format()
+ channels_axis = -1 if data_format == "channels_last" else 1
+
+ image_size = standardize_input_shape(image_size, data_format)
+
+ if input_tensor is None:
+ img_input = layers.Input(shape=image_size)
+ elif not utils.is_keras_tensor(input_tensor):
+ img_input = layers.Input(tensor=input_tensor, shape=image_size)
+ else:
+ img_input = input_tensor
+
+ x = (
+ normalize_image_for_classify_models(img_input, normalization_mode)
+ if include_normalization
+ else img_input
+ )
+ x = regnet_backbone_feature(
+ x,
+ embedding_size=embedding_size,
+ hidden_sizes=hidden_sizes,
+ depths=depths,
+ groups_width=groups_width,
+ layer_type=layer_type,
+ downsample_in_first_stage=downsample_in_first_stage,
+ channels_axis=channels_axis,
+ data_format=data_format,
+ return_stages=as_backbone,
+ )
+
+ super().__init__(inputs=img_input, outputs=x, name=name, **kwargs)
+
+ self.embedding_size = embedding_size
+ self.hidden_sizes = hidden_sizes
+ self.depths = depths
+ self.groups_width = groups_width
+ self.layer_type = layer_type
+ self.downsample_in_first_stage = downsample_in_first_stage
+ self.image_size = image_size
+ self.include_normalization = include_normalization
+ self.normalization_mode = normalization_mode
+ self.input_tensor = input_tensor
+ self.as_backbone = as_backbone
+
+ def get_config(self):
+ config = super().get_config()
+ config.update(
+ {
+ "embedding_size": self.embedding_size,
+ "hidden_sizes": self.hidden_sizes,
+ "depths": self.depths,
+ "groups_width": self.groups_width,
+ "layer_type": self.layer_type,
+ "downsample_in_first_stage": self.downsample_in_first_stage,
+ "image_size": self.image_size,
+ "include_normalization": self.include_normalization,
+ "normalization_mode": self.normalization_mode,
+ "input_tensor": self.input_tensor,
+ "as_backbone": self.as_backbone,
+ "name": self.name,
+ "trainable": self.trainable,
+ }
+ )
+ return config
+
+ @classmethod
+ def from_config(cls, config):
+ return cls(**config)
+
+
+@keras.saving.register_keras_serializable(package="zeromodels")
+class RegNetImageClassify(BaseModel):
+ """Instantiates the RegNet classifier.
+
+ Wraps a :class:`RegNetModel` backbone and attaches a GlobalAveragePooling2D +
+ Dense head to produce ``num_classes`` class logits. All architectural
+ parameters forward to the underlying :class:`RegNetModel`; only
+ ``num_classes`` and ``classifier_activation`` are head-specific.
+
+ References:
+ - [Designing Network Design Spaces](https://arxiv.org/abs/2003.13678)
+
+ Args:
+ embedding_size: Integer, output width of the stem. Defaults to `32`.
+ hidden_sizes: Tuple of ints, output width per stage.
+ Defaults to `(128, 192, 512, 1088)`.
+ depths: Tuple of ints, number of blocks per stage.
+ Defaults to `(2, 6, 12, 2)`.
+ groups_width: Integer, channels per group of the 3x3 grouped conv.
+ Defaults to `64`.
+ layer_type: String, `"y"` (with SE) or `"x"`. Defaults to `"y"`.
+ downsample_in_first_stage: Boolean, whether the first stage downsamples.
+ Defaults to `True`.
+ include_normalization: Boolean, whether to prepend image normalization.
+ Defaults to `True`.
+ normalization_mode: String normalization preset. Defaults to `"imagenet"`.
+ image_size: Input image spec. Defaults to `224`.
+ input_tensor: Optional Keras tensor as input. Defaults to `None`.
+ num_classes: Integer, number of output classes. Defaults to `1000`.
+ classifier_activation: String/callable for the final Dense. Use
+ `"linear"` for logits or `"softmax"` for probabilities.
+ Defaults to `"linear"`.
+ name: String model name. The internal backbone is named
+ `f"{name}_backbone"`. Defaults to `"RegNetImageClassify"`.
+
+ Returns:
+ A Keras `Model` instance.
+ """
+
+ BASE_WEIGHT_CONFIG = None
+ config_class = RegNetConfig
+ HUB_REPO_SIBLINGS = REGNET_HUB_SIBLINGS
+ HF_MODEL_TYPE = "regnet"
+
+ @classmethod
+ def config_from_hf(cls, hf_config):
+ return {
+ "embedding_size": hf_config["embedding_size"],
+ "hidden_sizes": tuple(hf_config["hidden_sizes"]),
+ "depths": tuple(hf_config["depths"]),
+ "groups_width": hf_config["groups_width"],
+ "layer_type": hf_config["layer_type"],
+ "downsample_in_first_stage": hf_config.get(
+ "downsample_in_first_stage", True
+ ),
+ "num_classes": hf_config.get("num_labels", 1000),
+ }
+
+ @classmethod
+ def transfer_from_hf(cls, keras_model, state_dict):
+ from .convert_regnet_hf_to_keras import transfer_regnet_weights
+
+ transfer_regnet_weights(keras_model, state_dict)
+
+ def __init__(
+ self,
+ embedding_size=32,
+ hidden_sizes=(128, 192, 512, 1088),
+ depths=(2, 6, 12, 2),
+ groups_width=64,
+ layer_type="y",
+ downsample_in_first_stage=True,
+ image_size=224,
+ include_normalization=True,
+ normalization_mode="imagenet",
+ input_tensor=None,
+ num_classes=1000,
+ classifier_activation="linear",
+ name="RegNetImageClassify",
+ **kwargs,
+ ):
+ data_format = keras.config.image_data_format()
+
+ backbone = RegNetModel(
+ embedding_size=embedding_size,
+ hidden_sizes=hidden_sizes,
+ depths=depths,
+ groups_width=groups_width,
+ layer_type=layer_type,
+ downsample_in_first_stage=downsample_in_first_stage,
+ image_size=image_size,
+ include_normalization=include_normalization,
+ normalization_mode=normalization_mode,
+ input_tensor=input_tensor,
+ name=f"{name}_backbone",
+ )
+
+ x = layers.GlobalAveragePooling2D(data_format=data_format, name="avg_pool")(
+ backbone.output
+ )
+ out = layers.Dense(
+ num_classes,
+ activation=classifier_activation,
+ kernel_initializer="zeros",
+ name="classifier_1",
+ )(x)
+
+ super().__init__(inputs=backbone.input, outputs=out, name=name, **kwargs)
+
+ self.embedding_size = embedding_size
+ self.hidden_sizes = hidden_sizes
+ self.depths = depths
+ self.groups_width = groups_width
+ self.layer_type = layer_type
+ self.downsample_in_first_stage = downsample_in_first_stage
+ self.image_size = backbone.image_size
+ self.include_normalization = include_normalization
+ self.normalization_mode = normalization_mode
+ self.input_tensor = input_tensor
+ self.num_classes = num_classes
+ self.classifier_activation = classifier_activation
+
+ def get_config(self):
+ config = super().get_config()
+ config.update(
+ {
+ "embedding_size": self.embedding_size,
+ "hidden_sizes": self.hidden_sizes,
+ "depths": self.depths,
+ "groups_width": self.groups_width,
+ "layer_type": self.layer_type,
+ "downsample_in_first_stage": self.downsample_in_first_stage,
+ "image_size": self.image_size,
+ "include_normalization": self.include_normalization,
+ "normalization_mode": self.normalization_mode,
+ "input_tensor": self.input_tensor,
+ "num_classes": self.num_classes,
+ "classifier_activation": self.classifier_activation,
+ "name": self.name,
+ "trainable": self.trainable,
+ }
+ )
+ return config
+
+ @classmethod
+ def from_config(cls, config):
+ return cls(**config)