Skip to content

Commit 13e168a

Browse files
Modify and Register Swin Transformer model presets. (#2731)
* fix conversion script * convert head * add .bin conversion * nit * register presets
1 parent a689a1c commit 13e168a

3 files changed

Lines changed: 178 additions & 82 deletions

File tree

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,71 @@
11
"""Swin Transformer model preset configurations."""
22

33
# Metadata for loading pretrained model weights.
4-
# Presets will be added after weights are uploaded to Kaggle.
5-
backbone_presets = {}
4+
backbone_presets = {
5+
"swin_tiny_patch4_window7_224": {
6+
"metadata": {
7+
"description": (
8+
"Swin-Tiny model pre-trained on the ImageNet 1k dataset "
9+
"with image resolution of 224x224."
10+
),
11+
"params": 27_519_354,
12+
"path": "swin_transformer",
13+
},
14+
"kaggle_handle": "kaggle://keras/swin-transformer/keras/swin_tiny_patch4_window7_224/1",
15+
},
16+
"swin_small_patch4_window7_224": {
17+
"metadata": {
18+
"description": (
19+
"Swin-Small model pre-trained on the ImageNet 1k dataset "
20+
"with image resolution of 224x224."
21+
),
22+
"params": 48_837_258,
23+
"path": "swin_transformer",
24+
},
25+
"kaggle_handle": "kaggle://keras/swin-transformer/keras/swin_small_patch4_window7_224/1",
26+
},
27+
"swin_base_patch4_window7_224": {
28+
"metadata": {
29+
"description": (
30+
"Swin-Base model pre-trained on the ImageNet 1k dataset "
31+
"with image resolution of 224x224."
32+
),
33+
"params": 86_743_224,
34+
"path": "swin_transformer",
35+
},
36+
"kaggle_handle": "kaggle://keras/swin-transformer/keras/swin_base_patch4_window7_224/1",
37+
},
38+
"swin_base_patch4_window12_384": {
39+
"metadata": {
40+
"description": (
41+
"Swin-Base model pre-trained on the ImageNet 1k dataset "
42+
"with image resolution of 384x384."
43+
),
44+
"params": 86_878_584,
45+
"path": "swin_transformer",
46+
},
47+
"kaggle_handle": "kaggle://keras/swin-transformer/keras/swin_base_patch4_window12_384/1",
48+
},
49+
"swin_large_patch4_window7_224": {
50+
"metadata": {
51+
"description": (
52+
"Swin-Large model pre-trained on the ImageNet 1k dataset "
53+
"with image resolution of 224x224."
54+
),
55+
"params": 194_995_476,
56+
"path": "swin_transformer",
57+
},
58+
"kaggle_handle": "kaggle://keras/swin-transformer/keras/swin_large_patch4_window7_224/1",
59+
},
60+
"swin_large_patch4_window12_384": {
61+
"metadata": {
62+
"description": (
63+
"Swin-Large model pre-trained on the ImageNet 1k dataset "
64+
"with image resolution of 384x384."
65+
),
66+
"params": 195_198_516,
67+
"path": "swin_transformer",
68+
},
69+
"kaggle_handle": "kaggle://keras/swin-transformer/keras/swin_large_patch4_window12_384/1",
70+
},
71+
}

keras_hub/src/utils/transformers/convert_swin_transformer.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,19 @@ def load_image_converter_config(preset, transformers_config):
2121
scale = [rescale_factor / s for s in std]
2222
offset = [-m / s for m, s in zip(mean, std)]
2323

24-
raw_size = preprocessor_config.get(
25-
"crop_size", preprocessor_config.get("size", 224)
26-
)
27-
if isinstance(raw_size, dict):
28-
image_size = (raw_size["height"], raw_size["width"])
24+
size = preprocessor_config["size"]
25+
if isinstance(size, dict):
26+
image_size = (size["height"], size["width"])
2927
else:
30-
image_size = (int(raw_size), int(raw_size))
28+
image_size = (int(size), int(size))
3129

3230
return {
3331
"image_size": image_size,
3432
"scale": scale,
3533
"offset": offset,
3634
"interpolation": "bicubic",
3735
"antialias": True,
36+
"crop_to_aspect_ratio": False,
3837
}
3938

4039

@@ -129,3 +128,16 @@ def port_dense(keras_layer, weight_key):
129128

130129
# 3. Final norm
131130
port_ln(backbone.norm, "layernorm")
131+
132+
133+
def convert_head(task, loader, transformers_config):
134+
prefix = "classifier."
135+
loader.port_weight(
136+
task.output_dense.kernel,
137+
hf_weight_key=prefix + "weight",
138+
hook_fn=lambda x, _: x.T,
139+
)
140+
loader.port_weight(
141+
task.output_dense.bias,
142+
hf_weight_key=prefix + "bias",
143+
)

tools/checkpoint_conversion/convert_swin_transformer_checkpoints.py

Lines changed: 92 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,13 @@
1717
from absl import flags
1818
from PIL import Image
1919
from transformers import AutoImageProcessor
20-
from transformers import SwinModel
20+
from transformers import SwinForImageClassification
2121

2222
import keras_hub
23-
from keras_hub.src.models.swin_transformer.swin_transformer_backbone import (
24-
SwinTransformerBackbone,
25-
)
26-
from keras_hub.src.models.swin_transformer.swin_transformer_image_classifier_preprocessor import ( # noqa: E501
27-
SwinTransformerImageClassifierPreprocessor,
28-
)
29-
from keras_hub.src.models.swin_transformer.swin_transformer_image_converter import ( # noqa: E501
30-
SwinTransformerImageConverter,
23+
from keras_hub.src.models.swin_transformer.swin_transformer_image_classifier import ( # noqa: E501
24+
SwinTransformerImageClassifier,
3125
)
26+
from keras_hub.src.utils.transformers import convert_swin_transformer
3227

3328
FLAGS = flags.FLAGS
3429

@@ -55,6 +50,38 @@
5550
f"Must be one of {','.join(PRESET_MAP.keys())}",
5651
required=True,
5752
)
53+
54+
55+
class StateDictLoader:
56+
"""Minimal loader adapter to port weights from a HF PyTorch state_dict."""
57+
58+
def __init__(self, state_dict):
59+
self.state_dict = state_dict
60+
self.prefix = None
61+
62+
def get_tensor(self, hf_weight_key):
63+
if hf_weight_key in self.state_dict:
64+
return self.state_dict[hf_weight_key]
65+
66+
if self.prefix is not None:
67+
full_key = self.prefix + hf_weight_key
68+
if full_key in self.state_dict:
69+
return self.state_dict[full_key]
70+
71+
for full_key in self.state_dict:
72+
if full_key.endswith(hf_weight_key) and full_key != hf_weight_key:
73+
self.prefix = full_key[: -len(hf_weight_key)]
74+
return self.state_dict[full_key]
75+
76+
raise KeyError(f"Missing key in HF state_dict: {hf_weight_key}")
77+
78+
def port_weight(self, keras_variable, hf_weight_key, hook_fn=None):
79+
hf_tensor = self.get_tensor(hf_weight_key)
80+
if hook_fn:
81+
hf_tensor = hook_fn(hf_tensor, list(keras_variable.shape))
82+
keras_variable.assign(hf_tensor)
83+
84+
5885
flags.DEFINE_string(
5986
"upload_uri",
6087
None,
@@ -63,36 +90,11 @@
6390
)
6491

6592

66-
def convert_image_converter(hf_image_processor):
67-
"""Build a SwinTransformerImageConverter from a HuggingFace processor."""
68-
config = hf_image_processor.to_dict()
69-
# crop_size is the actual model input resolution after resize+crop.
70-
image_size = (
71-
config["crop_size"]["height"],
72-
config["crop_size"]["width"],
73-
)
74-
std = config["image_std"]
75-
mean = config["image_mean"]
76-
rescale_factor = config["rescale_factor"]
77-
scale = [rescale_factor / s for s in std]
78-
offset = [-m / s for m, s in zip(mean, std)]
79-
return SwinTransformerImageConverter(
80-
image_size=image_size,
81-
scale=scale,
82-
offset=offset,
83-
antialias=True,
84-
interpolation="bicubic",
85-
crop_to_aspect_ratio=False,
86-
)
87-
88-
89-
def validate_output(
90-
keras_backbone, keras_image_converter, hf_model, hf_processor
91-
):
92-
"""Validate converted model outputs match HuggingFace."""
93-
# Compare number of parameters between Keras and HF backbone.
94-
keras_params = keras_backbone.count_params()
95-
hf_params = sum(p.numel() for p in hf_model.parameters())
93+
def validate_output(keras_model, keras_image_converter, hf_model, hf_processor):
94+
"""Validate converted classifier outputs match HuggingFace."""
95+
# Compare number of parameters between Keras and HF classifier.
96+
keras_params = keras_model.count_params()
97+
hf_params = hf_model.num_parameters()
9698
print(f"🔶 Keras model params: {keras_params:,}")
9799
print(f"🔶 HF model params: {hf_params:,}")
98100
assert keras_params == hf_params, (
@@ -116,22 +118,24 @@ def validate_output(
116118
)
117119
)
118120
with torch.no_grad():
119-
hf_features = (
120-
hf_model(**hf_inputs).last_hidden_state.detach().cpu().numpy()
121-
)
121+
hf_logits = hf_model(**hf_inputs).logits.detach().cpu().numpy()
122122

123-
keras_features = keras.ops.convert_to_numpy(
124-
keras_backbone(keras_preprocessed, training=False)
123+
keras_logits = keras.ops.convert_to_numpy(
124+
keras_model(keras_preprocessed, training=False)
125125
)
126-
127-
print("🔶 Keras output (first token, first 10 dims):")
128-
print(f" {keras_features[0, 0, :10]}")
129-
print("🔶 HF output (first token, first 10 dims):")
130-
print(f" {hf_features[0, 0, :10]}")
131-
132-
modeling_diff = np.mean(np.abs(keras_features - hf_features))
133-
max_diff = np.max(np.abs(keras_features - hf_features))
134-
relative_error = modeling_diff / np.mean(np.abs(hf_features))
126+
keras_label = np.argmax(keras_logits[0])
127+
hf_label = np.argmax(hf_logits[0])
128+
129+
print("🔶 Keras output (first 10 logits):")
130+
print(f" {keras_logits[0, :10]}")
131+
print("🔶 HF output (first 10 logits):")
132+
print(f" {hf_logits[0, :10]}")
133+
print(f"🔶 Keras label: {keras_label}")
134+
print(f"🔶 HF label: {hf_label}")
135+
136+
modeling_diff = np.mean(np.abs(keras_logits - hf_logits))
137+
max_diff = np.max(np.abs(keras_logits - hf_logits))
138+
relative_error = modeling_diff / np.mean(np.abs(hf_logits))
135139
print(f"🔶 Modeling difference (mean): {modeling_diff:.6f}")
136140
print(f"🔶 Modeling difference (max): {max_diff:.6f}")
137141
print(f"🔶 Relative error: {relative_error * 100:.4f}%")
@@ -169,33 +173,47 @@ def main(_):
169173
print(f"🏃 Converting {preset}")
170174

171175
# Load HuggingFace model and processor.
172-
hf_model = SwinModel.from_pretrained(hf_preset)
173-
hf_processor = AutoImageProcessor.from_pretrained(
174-
hf_preset,
175-
do_center_crop=False,
176-
)
177-
# Align resize target with crop_size so both steps land on the same size.
178-
hf_processor.size = hf_processor.crop_size
176+
hf_model = SwinForImageClassification.from_pretrained(hf_preset)
177+
hf_processor = AutoImageProcessor.from_pretrained(hf_preset)
179178
hf_model.eval()
180179

181-
# Load backbone via on-the-fly conversion (uses convert_swin_transformer.py
182-
# under the hood).
183-
keras_backbone = SwinTransformerBackbone.from_preset(f"hf://{hf_preset}")
184-
print("✅ KerasHub backbone loaded via on-the-fly conversion.")
185-
print(f" Parameters: {keras_backbone.count_params():,}")
186-
187-
keras_image_converter = convert_image_converter(hf_processor)
188-
keras_preprocessor = SwinTransformerImageClassifierPreprocessor(
189-
image_converter=keras_image_converter
180+
# Load preset architecture/config only and port HF weights manually.
181+
# This avoids requiring model.safetensors for presets that only publish
182+
# pytorch_model.bin.
183+
keras_model = SwinTransformerImageClassifier.from_preset(
184+
f"hf://{hf_preset}",
185+
load_weights=False,
186+
num_classes=len(hf_model.config.id2label),
190187
)
191-
192-
validate_output(
193-
keras_backbone, keras_image_converter, hf_model, hf_processor
188+
hf_state_dict = {
189+
key: value.detach().cpu().numpy()
190+
for key, value in hf_model.state_dict().items()
191+
}
192+
loader = StateDictLoader(hf_state_dict)
193+
hf_config = hf_model.config.to_dict()
194+
convert_swin_transformer.convert_weights(
195+
keras_model.backbone,
196+
loader,
197+
hf_config,
194198
)
199+
convert_swin_transformer.convert_head(
200+
keras_model,
201+
loader,
202+
hf_config,
203+
)
204+
print("✅ KerasHub classifier loaded via on-the-fly conversion.")
205+
print(f" Parameters: {keras_model.count_params():,}")
206+
207+
keras_preprocessor = keras_model.preprocessor
208+
keras_image_converter = keras_preprocessor.image_converter
209+
# Keep resize target aligned with converter image size.
210+
image_height, image_width = keras_image_converter.image_size
211+
hf_processor.size = {"height": image_height, "width": image_width}
212+
213+
validate_output(keras_model, keras_image_converter, hf_model, hf_processor)
195214
print("✅ Output validated.")
196215

197-
keras_backbone.save_to_preset(f"./{preset}")
198-
keras_preprocessor.save_to_preset(f"./{preset}")
216+
keras_model.save_to_preset(f"./{preset}")
199217
print(f"🏁 Preset saved to ./{preset}.")
200218

201219
upload_uri = FLAGS.upload_uri

0 commit comments

Comments
 (0)