From d1a92f21b04f9e0a1638b70a0253334f7ba216d8 Mon Sep 17 00:00:00 2001 From: IMvision12 Date: Fri, 28 Aug 2026 19:53:35 -0700 Subject: [PATCH 1/4] Add RegNet Models --- tests/base/model_test_registry.py | 16 + zeromodels/conversion/weight_transfer_util.py | 1 + zeromodels/models/__init__.py | 1 + zeromodels/models/regnet/__init__.py | 4 + .../regnet/convert_regnet_hf_to_keras.py | 129 +++++ zeromodels/models/regnet/regnet_config.py | 56 ++ zeromodels/models/regnet/regnet_model.py | 533 ++++++++++++++++++ 7 files changed, 740 insertions(+) create mode 100644 zeromodels/models/regnet/__init__.py create mode 100644 zeromodels/models/regnet/convert_regnet_hf_to_keras.py create mode 100644 zeromodels/models/regnet/regnet_config.py create mode 100644 zeromodels/models/regnet/regnet_model.py 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/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..1ec86faa --- /dev/null +++ b/zeromodels/models/regnet/convert_regnet_hf_to_keras.py @@ -0,0 +1,129 @@ +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 transformers + from huggingface_hub import hf_hub_download + + 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) From 06df068dc472d74cc812501e5cee63fada5e0e27 Mon Sep 17 00:00:00 2001 From: IMvision12 Date: Fri, 28 Aug 2026 22:38:23 -0700 Subject: [PATCH 2/4] Add docs --- README.md | 1 + docs/regnet.md | 161 ++++++++++++++++++ website/mkdocs.yml | 1 + .../regnet/convert_regnet_hf_to_keras.py | 7 + 4 files changed, 170 insertions(+) create mode 100644 docs/regnet.md 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..fc207fa8 --- /dev/null +++ b/docs/regnet.md @@ -0,0 +1,161 @@ +# RegNet + +
+Weights: RegNet loads the official Facebook checkpoints straight from the Hub, +converting on the fly: +RegNetImageClassify.from_weights("hf:facebook/regnet-y-040") +(12 X + 12 Y variants). Pre-converted Keras mirrors live under +zeromodels/regnet-<variant>. +
+ +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("hf:facebook/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 checkpoints `facebook/regnet-y-{320,640,1280,10b}-seer` load the same way. + +## Basic Usage + +```python +import keras +import numpy as np +from PIL import Image +from zeromodels.models.regnet import RegNetImageClassify + +model = RegNetImageClassify.from_weights("hf:facebook/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("hf:facebook/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("hf:facebook/regnet-y-040") # expects (B, 3, H, W) +``` + +## Loading Fine-tuned and Community Weights + +Any Hugging Face repo whose `model_type` is `"regnet"` (the official `facebook/regnet-*` +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/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/models/regnet/convert_regnet_hf_to_keras.py b/zeromodels/models/regnet/convert_regnet_hf_to_keras.py index 1ec86faa..7c98a097 100644 --- a/zeromodels/models/regnet/convert_regnet_hf_to_keras.py +++ b/zeromodels/models/regnet/convert_regnet_hf_to_keras.py @@ -85,9 +85,16 @@ def transfer_regnet_weights(keras_model, state_dict): _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}") From 5ab20753adaea408fe3a34f31e883ca081be76ba Mon Sep 17 00:00:00 2001 From: IMvision12 Date: Fri, 28 Aug 2026 22:48:07 -0700 Subject: [PATCH 3/4] Docs --- docs/regnet.md | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/docs/regnet.md b/docs/regnet.md index fc207fa8..c95aede3 100644 --- a/docs/regnet.md +++ b/docs/regnet.md @@ -1,11 +1,11 @@ # RegNet
-Weights: RegNet loads the official Facebook checkpoints straight from the Hub, -converting on the fly: -RegNetImageClassify.from_weights("hf:facebook/regnet-y-040") -(12 X + 12 Y variants). Pre-converted Keras mirrors live under -zeromodels/regnet-<variant>. +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 @@ -80,7 +80,7 @@ Typed config (`model_type="regnet"`) holding the fields above; serialized into e ## Model Variants -For `RegNetImageClassify.from_weights("hf:facebook/regnet-")`. The number is the +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 | @@ -90,7 +90,8 @@ model's compute in units of 0.1 GFLOPs (`002` = 0.2 GF ... `320` = 32 GF). 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 checkpoints `facebook/regnet-y-{320,640,1280,10b}-seer` load the same way. +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 @@ -100,7 +101,7 @@ import numpy as np from PIL import Image from zeromodels.models.regnet import RegNetImageClassify -model = RegNetImageClassify.from_weights("hf:facebook/regnet-y-040") +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] @@ -120,7 +121,7 @@ For detection / segmentation, take the four stage outputs: ```python from zeromodels.models.regnet import RegNetModel -backbone = RegNetModel.from_weights("hf:facebook/regnet-y-040", as_backbone=True) +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 @@ -140,13 +141,14 @@ bit-exact.** A model reads `keras.config.image_data_format()` when it is **const import keras keras.config.set_image_data_format("channels_first") -model = RegNetImageClassify.from_weights("hf:facebook/regnet-y-040") # expects (B, 3, H, W) +model = RegNetImageClassify.from_weights("zeromodels/regnet-y-040") # expects (B, 3, H, W) ``` ## Loading Fine-tuned and Community Weights -Any Hugging Face repo whose `model_type` is `"regnet"` (the official `facebook/regnet-*` -checkpoints or any fine-tune) loads with the `hf:` prefix, converting on the fly: +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 From 0c85fc5358134a2d43a061488624a4bfdb5a9714 Mon Sep 17 00:00:00 2001 From: IMvision12 Date: Fri, 28 Aug 2026 22:48:26 -0700 Subject: [PATCH 4/4] format --- docs/regnet.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/regnet.md b/docs/regnet.md index c95aede3..65c7a225 100644 --- a/docs/regnet.md +++ b/docs/regnet.md @@ -141,7 +141,9 @@ bit-exact.** A model reads `keras.config.image_data_format()` when it is **const import keras keras.config.set_image_data_format("channels_first") -model = RegNetImageClassify.from_weights("zeromodels/regnet-y-040") # expects (B, 3, H, W) +model = RegNetImageClassify.from_weights( + "zeromodels/regnet-y-040" +) # expects (B, 3, H, W) ``` ## Loading Fine-tuned and Community Weights