0xgr3y commited on
Commit
bdc11b4
Β·
verified Β·
1 Parent(s): 78d1e44

Update Readme: v6 final (EfficientNetV2-S, 97.77% acc, T-scaling ECE 0.0053)

Browse files
Files changed (1) hide show
  1. README.md +257 -216
README.md CHANGED
@@ -4,11 +4,14 @@ pipeline_tag: image-classification
4
  tags:
5
  - tensorflow
6
  - keras
 
 
 
7
  - image-classification
8
- - densenet121
9
  - architecture
10
  - building
11
- - fgvc
12
  - transfer-learning
13
  - gem-pooling
14
  - focal-loss
@@ -17,12 +20,13 @@ tags:
17
  - grad-cam
18
  - calibration
19
  - roc-auc
 
20
  library_name: keras
21
  language: en
22
  datasets:
23
  - 0xgr3y/arch-building-dataset
24
  widget:
25
- - src: https://cdn-uploads.huggingface.co/production/uploads/66cdac913f233bf2c7b4f590/HzXxNze2jmCkV5KPY_fpQ.png
26
  example_title: Bridge Classification
27
  model-index:
28
  - name: Architectural Building Image Classifier
@@ -36,46 +40,58 @@ model-index:
36
  split: test
37
  metrics:
38
  - type: accuracy
39
- value: 0.9688
40
  name: Test Accuracy
41
  - type: accuracy
42
- value: 0.9658
43
  name: Validation Accuracy (SWA)
44
  - type: accuracy
45
- value: 0.968
46
  name: TTA Accuracy
 
 
 
 
 
 
 
 
 
 
 
 
47
  ---
48
 
49
- ![Arch-Building-Image-Classification](results/greyscope-labs-architecture-classification-cnn-transferlearning.PNG)
50
 
51
- # Fine-Grained Image Classification of World Architecture: A DenseNet121 Transfer Learning Approach with Layered Regularization
52
 
53
  ### Architectural Building Image Classifier
54
 
55
- Fine-Grained Image Classification (FGIC) of world architectural buildings using CNN transfer learning with DenseNet121, enhanced with GeM Pooling, Focal Loss, Discriminative AdamW (LR), Stochastic Weight Averaging (SWA), Grad-CAM explainability, and calibration analysis.
56
 
57
  <table>
58
- <tr><td><strong>Architecture</strong></td><td>DenseNet121 + GeM Pooling + Focal Loss + SWA</td></tr>
59
  <tr><td><strong>Task</strong></td><td>Fine-Grained Image Classification (FGIC)</td></tr>
60
- <tr><td><strong>Test Accuracy</strong></td><td>96.88%</td></tr>
61
- <tr><td><strong>Classes</strong></td><td>8 (Barn, Bridge, Castle, Mosque, Skyscraper, Stadium, Temple, Windmill)</td></tr>
62
  <tr><td><strong>Input Size</strong></td><td>320 Γ— 320 pixels</td></tr>
63
- <tr><td><strong>Parameters</strong></td><td>9,466,953</td></tr>
64
  <tr><td><strong>Framework</strong></td><td>TensorFlow / Keras 3</td></tr>
65
  <tr><td><strong>License</strong></td><td><a href="https://www.apache.org/licenses/LICENSE-2.0">Apache-2.0</a></td></tr>
66
  </table>
67
 
68
  ## Model Description
69
 
70
- A fine-grained image classification model for world architectural buildings. Built on DenseNet121 pretrained on ImageNet, enhanced with GeM Pooling (learnable generalized mean pooling), Focal Loss, Discriminative AdamW and Stochastic Weight Averaging (SWA). Extended with Grad-CAM explainability visualization, ROC-AUC evaluation, ECE calibration analysis, and t-SNE embedding visualization.
71
 
72
  **Key architectural contributions:**
73
 
74
  - **GeM Pooling** (Radenovic et al., CVPR 2018) β€” replaces global average pooling with a learnable power parameter (p=3.0) that emphasizes high-activation features, yielding stronger discriminative representations for FGIC tasks
75
  - **Focal Loss** (Lin et al., ICCV 2017, gamma=2.0) β€” down-weights well-classified examples to focus gradient updates on hard-to-classify building pairs
76
- - **DiscriminativeAdamW** β€” extends AdamW with per-layer learning rate multipliers: conv4_block receives LR Γ— 0.1 (pretrained features require smaller updates), while conv5_block and the custom head receive LR Γ— 1.0
77
  - **SWA with BN re-estimation** (Izmailov et al., UAI 2018) β€” 10-epoch post-training weight averaging with constant LR 1e-4, followed by 100-step batch normalization statistics re-estimation
78
- - **Grad-CAM** (Selvaraju et al., ICCV 2017) β€” gradient-weighted class activation mapping for explainability, targeting *conv5_block16_concat*
79
  - **ECE Calibration** (Guo et al., ICML 2017) β€” Expected Calibration Error with 15-bin reliability diagram to assess prediction confidence reliability
80
 
81
  ## Architecture
@@ -83,9 +99,9 @@ A fine-grained image classification model for world architectural buildings. Bui
83
  ```
84
  Input (320, 320, 3)
85
  β”‚
86
- DenseNet121 (ImageNet pretrained, 427 layers, 7.04M params)
87
  β”‚
88
- Conv2D(256, 3Γ—3, ReLU, padding=same) β†’ 2,359,552 params
89
  BatchNormalization β†’ 1,024 params
90
  MaxPooling2D(2Γ—2) β†’ 0 params
91
  β”‚
@@ -102,8 +118,8 @@ Output (8 classes)
102
 
103
  | Component | Output Shape | Parameters |
104
  |-----------|-------------|------------|
105
- | DenseNet121 (Functional) | (None, 10, 10, 1024) | 7,037,504 |
106
- | Conv2D 256 3Γ—3 | (None, 10, 10, 256) | 2,359,552 |
107
  | BatchNormalization | (None, 10, 10, 256) | 1,024 |
108
  | MaxPooling2D 2Γ—2 | (None, 5, 5, 256) | 0 |
109
  | GeM Pooling p=3.0 | (None, 256) | 1 |
@@ -111,10 +127,10 @@ Output (8 classes)
111
  | BatchNormalization | (None, 256) | 1,024 |
112
  | Dropout 0.4 | (None, 256) | 0 |
113
  | Dense 8 Softmax | (None, 8) | 2,056 |
114
- | **Total** | | **9,466,953** |
115
- | Trainable (Phase 1) | | **2,428,425** (9.27 MB) |
116
- | Trainable (Phase 2) | | **7,884,297** (30.07 MB) |
117
- | Non-trainable (Phase 1) | | **7,038,528** (26.85 MB) |
118
 
119
  ## Performance
120
 
@@ -122,32 +138,32 @@ Output (8 classes)
122
 
123
  | Metric | Value |
124
  |--------|-------|
125
- | Test Accuracy | 96.88% |
126
- | Validation Accuracy (SWA) | 96.58% |
127
- | Test-Time Augmentation | 96.80% |
128
- | Test Loss | 0.4485 |
129
- | Overfitting Gap (Train βˆ’ Test) | 3.00% |
130
- | Macro Avg Precision | 0.9691 |
131
- | Macro Avg Recall | 0.9688 |
132
- | Macro Avg F1-Score | 0.9687 |
133
- | Top-2 Accuracy | 98.59% |
134
- | Top-3 Accuracy | 99.33% |
135
- | Macro ROC-AUC (OvR) | 0.9986 |
136
- | ECE (15 bins) | 0.1438 |
137
 
138
  ### Per-Class Results
139
 
140
- | Class | Precision | Recall | F1-Score | Support |
141
- |-------|-----------|--------|----------|---------|
142
- | Barn | 0.9645 | 0.9702 | 0.9674 | 168 |
143
- | Bridge | 0.9588 | 0.9702 | 0.9645 | 168 |
144
- | Castle | 0.9649 | 0.9821 | 0.9735 | 168 |
145
- | Mosque | 0.9649 | 0.9821 | 0.9735 | 168 |
146
- | Skyscraper | 0.9708 | 0.9881 | 0.9794 | 168 |
147
- | Stadium | 0.9936 | 0.9286 | 0.9600 | 168 |
148
- | Temple | 0.9816 | 0.9524 | 0.9668 | 168 |
149
- | Windmill | 0.9535 | 0.9762 | 0.9647 | 168 |
150
- | **Macro Avg** | **0.9691** | **0.9688** | **0.9687** | **1,344** |
151
 
152
  ### Model Selection
153
 
@@ -155,26 +171,26 @@ Four candidate models were evaluated on the validation set:
155
 
156
  | Checkpoint | Val Accuracy | Val Loss | Description |
157
  |------------|-------------|----------|-------------|
158
- | `best_phase1.keras` | 89.21% | 1.2231 | Phase 1 checkpoint (backbone frozen) |
159
- | `best_phase2.keras` | 92.04% | 0.6171 | Phase 2 checkpoint (conv4+conv5 unfrozen) |
160
- | `best_phase2_ema.keras` | 89.36% | 0.8183 | Phase 2 EMA shadow weights |
161
- | **`best_phase2_swa.keras`** | **96.58%** | **0.4256** | **SWA averaged weights ← SELECTED** |
162
 
163
  ### SWA Progression
164
 
165
  | SWA Epoch | Val Accuracy | Val Loss |
166
  |-----------|-------------|----------|
167
- | 1 | 93.38% | 0.5580 |
168
- | 2 | 93.60% | 0.5738 |
169
- | 3 | 92.86% | 0.5725 |
170
- | 4 | 95.24% | 0.4806 |
171
- | 5 | 95.68% | 0.4529 |
172
- | 6 | 96.35% | 0.4548 |
173
- | 7 | 94.27% | 0.5141 |
174
- | 8 | 94.12% | 0.5147 |
175
- | 9 | 94.49% | 0.5243 |
176
- | 10 | 96.50% | 0.4424 |
177
- | **SWA + BN (final)** | **96.58%** | **0.4256** |
178
 
179
  ![Training Curves](results/training_curves.png)
180
 
@@ -194,14 +210,14 @@ Four candidate models were evaluated on the validation set:
194
 
195
  ## Training Details
196
 
197
- ### Progressive Training
198
 
199
- Two-phase strategy train with SWA post-processing:
200
 
201
  | Phase | Description | Backbone | Optimizer | LR | Max Epochs | Actual Epochs | CutMix+Mixup | FocalLoss LS |
202
  |-------|-------------|----------|-----------|-----|-----------|---------------|---------------|-------------|
203
  | **Phase 1** β€” Feature Extraction | Train custom head only | Frozen (all) | AdamW (wd=2e-5) | 0.001 + CosineDecay + Warmup 3ep | 25 | 1 | Yes (50/50 alternation) | 0.1 |
204
- | **Phase 2** β€” Selective Fine-Tuning | Load best_phase1 β†’ fine-tune | conv4_block + conv5_block unfrozen (BN frozen) | DiscriminativeAdamW (conv4=0.1Γ—) | 3e-4 + CosineDecay + Warmup 5ep | 50 | 6 + 10 SWA | No | 0.05 |
205
 
206
  > ΒΉ Phase 1 stops when `val_accuracy β‰₯ 85%` threshold (myCallback).
207
 
@@ -215,13 +231,13 @@ Two-phase strategy train with SWA post-processing:
215
  | Learning Rate | 0.001 | 3Γ—10⁻⁴ |
216
  | LR Schedule | WarmupCosineDecay (warmup=3) | WarmupCosineDecay (warmup=5) |
217
  | Weight Decay | 2Γ—10⁻⁡ | 2Γ—10⁻⁡ |
218
- | LR Multiplier (conv4) | β€” | 0.1Γ— |
219
- | LR Multiplier (conv5+head) | β€” | 1.0Γ— |
220
  | Loss | FocalLoss (gamma=2.0, LS=0.1) | FocalLoss (gamma=2.0, LS=0.05) |
221
  | Batch Size | 32 | 32 |
222
  | Early Stopping Patience | 7 | 12 |
223
  | myCallback Threshold | val_acc β‰₯ 0.85 | val_acc β‰₯ 0.92 |
224
- | EMA Decay | 0.999 | 0.999 |
225
  | SWA Epochs | β€” | 10 (post-training) |
226
  | SWA LR | β€” | 1Γ—10⁻⁴ (constant) |
227
  | BN Re-estimation Steps | β€” | 100 |
@@ -233,25 +249,26 @@ Two-phase strategy train with SWA post-processing:
233
 
234
  | Technique | Implementation | Reference |
235
  |-----------|---------------|-----------|
236
- | Transfer Learning | DenseNet121 backbone frozen in Phase 1 | Yosinski et al., NeurIPS 2014 |
237
- | Selective Fine-Tuning | Unfreeze conv4+conv5 only, BN stays frozen | Howard & Ruder, ACL 2018 |
238
- | Discriminative LR | conv4=0.1Γ—, conv5+head=1.0Γ— | Howard & Ruder, ACL 2018 |
239
  | CutMix + Mixup | Alternation per batch (50/50), Phase 1 only | Yun et al., ICCV 2019; Zhang et al., ICLR 2018 |
240
  | Focal Loss | gamma=2.0, down-weights easy examples | Lin et al., ICCV 2017 |
241
  | Label Smoothing | 0.1 (Phase 1) β†’ 0.05 (Phase 2) | Szegedy et al., CVPR 2016 |
242
  | GeM Pooling | p=3.0 learnable, replaces GAP | Radenovic et al., CVPR 2018 |
243
  | Dropout | 0.4 after Dense(256)+BN | Srivastava et al., JMLR 2014 |
244
  | Batch Normalization | After Conv2D and Dense; frozen during fine-tuning | Ioffe & Szegedy, arXiv 2015 |
245
- | EMA | Shadow weights, decay=0.999 | Tarvainen & Valpola, NeurIPS 2017 |
246
  | SWA | 10-epoch post-training, constant LR 1e-4 | Izmailov et al., UAI 2018 |
247
- | Data Augmentation | Rotation Β±15Β°, shift Β±10%, zoom Β±20%, brightness 0.75–1.15, horizontal flip | Perez & Wang, arXiv 2017 |
 
248
  | Test-Time Augmentation | 6 augmentation variants, averaged | Shanmugam et al., ICML 2020 |
249
  | WarmupCosineDecay | Linear warmup + cosine annealing | Loshchilov & Hutter, ICLR 2017 (SGDR) |
250
  | Early Stopping | Patience 7 (Phase 1) / 12 (Phase 2) | Prechelt, Neural Networks 1998 |
251
 
252
  ### Dataset
253
 
254
- See more data studio curation [World Architectural Buildings Dataset for Multi‑Class Image Classification](https://huggingface.co/datasets/0xgr3y/arch-building-dataset) β€” 13,440 images (8 classes Γ— 1,680, balanced) sourced from Pexels with perceptual (pHash) and exact (SHA256) deduplication.
255
 
256
  | Split | Images | Percentage |
257
  |-------|--------|------------|
@@ -261,7 +278,7 @@ See more data studio curation [World Architectural Buildings Dataset for Multi
261
 
262
  ### Data Preprocessing
263
 
264
- - **Normalization:** `preprocess_input` from `tf.keras.applications.densenet` (ImageNet distribution)
265
  - **Input resolution:** 320Γ—320 (higher than ImageNet default 224Γ—224 to capture fine-grained architectural details β€” textures, ornaments, facade patterns)
266
  - **Augmentation:** Applied to training set only; validation and test sets use clean preprocessing
267
  - **Split method:** `splitfolders.ratio` from `dataset/`, seed=42
@@ -270,19 +287,25 @@ See more data studio curation [World Architectural Buildings Dataset for Multi
270
 
271
  | File | Description |
272
  |------|-------------|
273
- | `best_phase2_swa.keras` | Best model β€” SWA averaged weights |
274
- | `best_phase2.keras` | Phase 2 checkpoint |
275
- | `saved_model/` | TensorFlow SavedModel format (portable, for TF Serving) |
276
- | `tflite/model.tflite` | TensorFlow Lite model (mobile/embedded) |
 
 
 
 
 
277
  | `tflite/label.txt` | Class label names for TF-Lite |
278
- | `tfjs_model/` | TensorFlow.js model (browser, 10 weight shards + model.json) |
279
  | `config.json` | Model configuration and evaluation metrics |
280
  | `label_mapping.json` | Class name ↔ ID mapping with training config and architecture info |
281
  | `preprocessor_config.json` | Input preprocessing specification (320Γ—320) |
282
  | `confusion_pairs.json` | Auto-detected confusion pairs from confusion matrix (threshold >5%) |
283
  | `class_confidence_stats.json` | Per-class mean/std/p5/p95 confidence distribution |
284
  | `model_benchmark.json` | Model parameters, sizes, speed, Top-K, AUC, ECE, TTA metrics |
285
- | `calibration_data.json` | ECE, bin accuracies/confidences, per-class AUC for calibration analysis |
 
286
  | `results/training_curves.png` | Training/validation accuracy and loss curves |
287
  | `results/confusion_matrix.png` | Confusion matrix on test set |
288
  | `results/per_class_accuracy.png` | Per-class accuracy bar chart |
@@ -300,127 +323,41 @@ See more data studio curation [World Architectural Buildings Dataset for Multi
300
 
301
  ### Gradio Space
302
 
303
- Try the live classify building: [Architecture Building Image Classifier with Space](https://huggingface.co/spaces/0xgr3y/arch-building-classifier)
304
 
305
- ### Python β€” Keras
306
 
307
- ```python
308
- from huggingface_hub import hf_hub_download
309
- import tensorflow as tf
310
- from tensorflow.keras.applications.densenet import preprocess_input
311
- from tensorflow.keras.layers import Layer
312
- from PIL import Image
313
- import numpy as np
314
 
315
- # ========---Custom Layers (must match training definition)---========
316
-
317
- class GeMPooling(Layer):
318
- def __init__(self, p=3.0, eps=1e-6, **kwargs):
319
- super().__init__(**kwargs)
320
- self.p_init = p
321
- self.eps = eps
322
- def build(self, input_shape):
323
- self.p = self.add_weight(name="gem_p", shape=(), dtype=tf.float32,
324
- initializer=tf.keras.initializers.Constant(self.p_init), trainable=True)
325
- super().build(input_shape)
326
- def call(self, x):
327
- x = tf.maximum(x, self.eps)
328
- x = tf.pow(x, self.p)
329
- x = tf.reduce_mean(x, axis=[1, 2], keepdims=False)
330
- return tf.pow(x, 1.0 / self.p)
331
- def get_config(self):
332
- return {**super().get_config(), "p": self.p_init, "eps": self.eps}
333
-
334
- class FocalLoss(tf.keras.losses.Loss):
335
- def __init__(self, gamma=2.0, alpha=None, label_smoothing=0.0, **kwargs):
336
- super().__init__(**kwargs)
337
- self.gamma = gamma
338
- self.alpha = alpha
339
- self.label_smoothing = label_smoothing
340
- def call(self, y_true, y_pred):
341
- y_pred = tf.clip_by_value(y_pred, 1e-7, 1.0 - 1e-7)
342
- if self.label_smoothing > 0:
343
- y_true = y_true * (1.0 - self.label_smoothing) + \
344
- (self.label_smoothing / tf.cast(tf.shape(y_true)[-1], tf.float32))
345
- ce = -y_true * tf.math.log(y_pred)
346
- weight = tf.pow(1.0 - y_pred, self.gamma)
347
- fl = weight * ce
348
- if self.alpha is not None:
349
- alpha_t = y_true * self.alpha
350
- fl = alpha_t * fl
351
- return tf.reduce_mean(tf.reduce_sum(fl, axis=-1))
352
- def get_config(self):
353
- return {**super().get_config(), "gamma": self.gamma,
354
- "alpha": self.alpha, "label_smoothing": self.label_smoothing}
355
-
356
- class DiscriminativeAdamW(tf.keras.optimizers.AdamW):
357
- def __init__(self, lr_multipliers=None, backbone_layer_idx=0, **kwargs):
358
- super().__init__(**kwargs)
359
- self.lr_multipliers = lr_multipliers or {}
360
- self.backbone_layer_idx = backbone_layer_idx
361
- self._var_mult_cache = {}
362
- def _build_var_cache(self, model):
363
- self._var_mult_cache = {}
364
- base_model = model.layers[self.backbone_layer_idx]
365
- for layer in base_model.layers:
366
- mult = 1.0
367
- for pattern, m in self.lr_multipliers.items():
368
- if pattern in layer.name:
369
- mult = m
370
- break
371
- for var in layer.trainable_variables:
372
- self._var_mult_cache[id(var)] = mult
373
- def _get_multiplier(self, var):
374
- return self._var_mult_cache.get(id(var), 1.0)
375
- def apply_gradients(self, grads_and_vars, **kwargs):
376
- scaled_gv = []
377
- for grad, var in grads_and_vars:
378
- if grad is not None:
379
- mult = self._get_multiplier(var)
380
- if mult != 1.0:
381
- grad = grad * tf.cast(mult, grad.dtype)
382
- scaled_gv.append((grad, var))
383
- else:
384
- scaled_gv.append((grad, var))
385
- return super().apply_gradients(scaled_gv, **kwargs)
386
- def get_config(self):
387
- return {**super().get_config(), "lr_multipliers": self.lr_multipliers,
388
- "backbone_layer_idx": self.backbone_layer_idx}
389
-
390
- # =====================---Load Model---==========================
391
-
392
- LABELS_PATH = hf_hub_download("0xgr3y/Arch-Building-Image-Classification", "label_mapping.json")
393
- import json
394
- with open(LABELS_PATH) as f:
395
- LABELS = json.load(f)["labels"]
396
- # ["barn","bridge","castle","mosque","skyscraper","stadium","temple","windmill"]
397
 
398
- custom_objects = {
399
- "GeMPooling": GeMPooling,
400
- "FocalLoss": FocalLoss,
401
- "DiscriminativeAdamW": DiscriminativeAdamW,
402
- }
403
 
404
- model_path = hf_hub_download("0xgr3y/Arch-Building-Image-Classification", "best_phase2_swa.keras")
405
- model = tf.keras.models.load_model(model_path, custom_objects=custom_objects, compile=False)
406
-
407
- # =======================---Inference---==========================
408
 
409
- img = Image.open("skyscraper_00000.jpg").convert("RGB").resize((320, 320))
410
- arr = np.expand_dims(preprocess_input(np.array(img, dtype=np.float32)), axis=0)
411
- preds = model.predict(arr, verbose=0)[0]
412
- print(f"Predicted: {LABELS[np.argmax(preds)]} ({np.max(preds)*100:.1f}%)")
413
- ```
414
 
415
- ### Python β€” TensorFlow Lite
 
416
 
417
- Download the model first:
418
- ```python
419
- from huggingface_hub import hf_hub_download
420
- hf_hub_download("0xgr3y/Arch-Building-Image-Classification", "tflite/model.tflite", local_dir=".")
421
- hf_hub_download("0xgr3y/Arch-Building-Image-Classification", "tflite/label.txt", local_dir=".")
 
 
422
  ```
423
 
 
 
424
  ```python
425
  import numpy as np
426
  import tensorflow as tf
@@ -428,19 +365,26 @@ from huggingface_hub import hf_hub_download
428
  from PIL import Image
429
  import json
430
 
 
 
 
 
 
 
 
431
  labels_path = hf_hub_download("0xgr3y/Arch-Building-Image-Classification", "label_mapping.json")
 
432
  with open(labels_path) as f:
433
  LABELS = json.load(f)["labels"]
434
 
435
- interpreter = tf.lite.Interpreter(model_path="tflite/model.tflite")
436
  interpreter.allocate_tensors()
437
-
438
  input_details = interpreter.get_input_details()
439
  output_details = interpreter.get_output_details()
440
 
441
  img = Image.open("skyscraper_00000.jpg").convert("RGB").resize((320, 320))
442
- arr = np.expand_dims(np.array(img, dtype=np.float32), axis=0)
443
- arr = tf.keras.applications.densenet.preprocess_input(arr)
444
 
445
  interpreter.set_tensor(input_details[0]["index"], arr)
446
  interpreter.invoke()
@@ -451,39 +395,135 @@ for i in top3_idx:
451
  print(f" {LABELS[i]}: {preds[i]*100:.1f}%")
452
  ```
453
 
454
- ### Python β€” SavedModel
455
-
456
- Download the model first:
457
- ```python
458
- from huggingface_hub import snapshot_download
459
- snapshot_download("0xgr3y/Arch-Building-Image-Classification", allow_patterns=["saved_model/*"], local_dir=".")
460
- ```
461
-
462
- Requires the custom layer definitions from the Keras section above.
463
 
464
  ```python
 
465
  import tensorflow as tf
466
- import numpy as np
467
  from huggingface_hub import hf_hub_download
 
 
 
 
468
  from PIL import Image
 
469
  import json
470
 
 
471
  labels_path = hf_hub_download("0xgr3y/Arch-Building-Image-Classification", "label_mapping.json")
472
- with open(labels_path) as f:
473
- LABELS = json.load(f)["labels"]
474
 
475
- custom_objects = {"GeMPooling": GeMPooling, "FocalLoss": FocalLoss,
476
- "DiscriminativeAdamW": DiscriminativeAdamW}
477
 
478
- model = tf.keras.models.load_model("saved_model", custom_objects=custom_objects, compile=False)
 
479
 
480
  img = Image.open("skyscraper_00000.jpg").convert("RGB").resize((320, 320))
481
- arr = np.expand_dims(tf.keras.applications.densenet.preprocess_input(
482
- np.array(img, dtype=np.float32)), axis=0)
483
  preds = model.predict(arr, verbose=0)[0]
484
  print(f"Predicted: {LABELS[np.argmax(preds)]} ({np.max(preds)*100:.1f}%)")
485
  ```
486
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
487
  ## Intended Use
488
 
489
  - Architectural style classification from building photographs
@@ -496,6 +536,7 @@ print(f"Predicted: {LABELS[np.argmax(preds)]} ({np.max(preds)*100:.1f}%)")
496
  - Trained on Pexels stock photography β€” performance may differ on user-generated or field photographs
497
  - Limited to 8 architectural classes (barn, bridge, castle, mosque, skyscraper, stadium, temple, windmill)
498
  - Confusion pair analysis found **0 significant pairs** (threshold >5%) β€” all 8 classes are well-distinguished by the model; see `confusion_pairs.json` for details
 
499
  - Inference confidence can be low on atypical examples
500
 
501
  ![Misclassification Examples](results/misclassification_examples.png)
@@ -512,11 +553,11 @@ print(f"Predicted: {LABELS[np.argmax(preds)]} ({np.max(preds)*100:.1f}%)")
512
 
513
  - **Gradio Space (Live):** [arch-building-classifier Space](https://huggingface.co/spaces/0xgr3y/arch-building-classifier)
514
  - **Dataset Studio:** [0xgr3y/arch-building-dataset](https://huggingface.co/datasets/0xgr3y/arch-building-dataset)
515
- - **GitHub Repo:** [arcxteam/arch-building-classifier](https://github.com/arcxteam/arch-building-classifier)
516
 
517
  ## References
518
 
519
- 1. Huang, G., Liu, Z., Van Der Maaten, L., & Weinberger, K. Q. (2017). Densely Connected Convolutional Networks. *CVPR 2017*. [arXiv:1608.06993](https://arxiv.org/abs/1608.06993)
520
  2. Radenovic, F., Tolias, G., & Chum, O. (2018). Fine-Tuning CNN Image Retrieval with No Human Annotation. *IEEE TPAMI*. [arXiv:1711.02512](https://arxiv.org/abs/1711.02512)
521
  3. Lin, T.-Y., Goyal, P., Girshick, R., He, K., & Dollar, P. (2017). Focal Loss for Dense Object Detection. *ICCV 2017*. [arXiv:1708.02002](https://arxiv.org/abs/1708.02002)
522
  4. Izmailov, P., Podoprikhin, D., Garipov, T., Vetrov, D., & Wilson, A. G. (2018). Averaging Weights Leads to Wider Optima and Better Generalization. *UAI 2018*. [arXiv:1803.05407](https://arxiv.org/abs/1803.05407)
@@ -544,7 +585,7 @@ print(f"Predicted: {LABELS[np.argmax(preds)]} ({np.max(preds)*100:.1f}%)")
544
  ```bibtex
545
  @misc{saugani2026_arch_building,
546
  title={Fine-Grained Image Classification of World Architecture:
547
- A DenseNet121 Transfer Learning Approach with Layered Regularization},
548
  author={Saugani},
549
  year={2026},
550
  publisher={Hugging Face},
 
4
  tags:
5
  - tensorflow
6
  - keras
7
+ - safetensors
8
+ - tflite
9
+ - tensorflow.js
10
  - image-classification
11
+ - efficientnetv2
12
  - architecture
13
  - building
14
+ - fgic
15
  - transfer-learning
16
  - gem-pooling
17
  - focal-loss
 
20
  - grad-cam
21
  - calibration
22
  - roc-auc
23
+ - temperature-scaling
24
  library_name: keras
25
  language: en
26
  datasets:
27
  - 0xgr3y/arch-building-dataset
28
  widget:
29
+ - src: results/greyscope-labs-architecture-classification-efficientnetv2.jpg
30
  example_title: Bridge Classification
31
  model-index:
32
  - name: Architectural Building Image Classifier
 
40
  split: test
41
  metrics:
42
  - type: accuracy
43
+ value: 0.9777
44
  name: Test Accuracy
45
  - type: accuracy
46
+ value: 0.9836
47
  name: Validation Accuracy (SWA)
48
  - type: accuracy
49
+ value: 0.9799
50
  name: TTA Accuracy
51
+ - type: f1
52
+ value: 0.9777
53
+ name: Macro F1
54
+ - type: precision
55
+ value: 0.9777
56
+ name: Macro Precision
57
+ - type: recall
58
+ value: 0.9777
59
+ name: Macro Recall
60
+ - type: roc_auc
61
+ value: 0.9985
62
+ name: Macro ROC-AUC (OvR)
63
  ---
64
 
65
+ ![Arch-Building-Image-Classification](results/greyscope-labs-architecture-classification-efficientnetv2.jpg)
66
 
67
+ # Fine-Grained Image Classification of World Architecture: An EfficientNetV2-S Transfer Learning Approach with Layered Regularization
68
 
69
  ### Architectural Building Image Classifier
70
 
71
+ Fine-Grained Image Classification (FGIC) of world architectural buildings using CNN transfer learning with EfficientNetV2-S, enhanced with GeM Pooling, Focal Loss, Discriminative AdamW (LR), Stochastic Weight Averaging (SWA), Grad-CAM explainability, and calibration analysis.
72
 
73
  <table>
74
+ <tr><td><strong>Architecture</strong></td><td>EfficientNetV2-S + GeM Pooling + Focal Loss + SWA</td></tr>
75
  <tr><td><strong>Task</strong></td><td>Fine-Grained Image Classification (FGIC)</td></tr>
76
+ <tr><td><strong>Test Accuracy</strong></td><td>97.77%</td></tr>
77
+ <tr><td><strong>Classes</strong></td><td>8 (barn, bridge, castle, mosque, skyscraper, stadium, temple, windmill)</td></tr>
78
  <tr><td><strong>Input Size</strong></td><td>320 Γ— 320 pixels</td></tr>
79
+ <tr><td><strong>Parameters</strong></td><td>23,350,633</td></tr>
80
  <tr><td><strong>Framework</strong></td><td>TensorFlow / Keras 3</td></tr>
81
  <tr><td><strong>License</strong></td><td><a href="https://www.apache.org/licenses/LICENSE-2.0">Apache-2.0</a></td></tr>
82
  </table>
83
 
84
  ## Model Description
85
 
86
+ A fine-grained image classification model for world architectural buildings. Built on EfficientNetV2-S pretrained on ImageNet, enhanced with GeM Pooling (learnable generalized mean pooling), Focal Loss, Discriminative AdamW and Stochastic Weight Averaging (SWA). Extended with Grad-CAM explainability visualization, ROC-AUC evaluation, ECE calibration analysis, and t-SNE embedding visualization.
87
 
88
  **Key architectural contributions:**
89
 
90
  - **GeM Pooling** (Radenovic et al., CVPR 2018) β€” replaces global average pooling with a learnable power parameter (p=3.0) that emphasizes high-activation features, yielding stronger discriminative representations for FGIC tasks
91
  - **Focal Loss** (Lin et al., ICCV 2017, gamma=2.0) β€” down-weights well-classified examples to focus gradient updates on hard-to-classify building pairs
92
+ - **DiscriminativeAdamW LR** β€” extends AdamW with per-variable LR scaling on block6 (Γ—0.1) via update_step override, combined with selective fine-tuning (block6+top_conv unfrozen, BN frozen). LR scaling produces truly discriminative updates β€” block6 variables receive 10Γ— smaller learning rate than head variables
93
  - **SWA with BN re-estimation** (Izmailov et al., UAI 2018) β€” 10-epoch post-training weight averaging with constant LR 1e-4, followed by 100-step batch normalization statistics re-estimation
94
+ - **Grad-CAM** (Selvaraju et al., ICCV 2017) β€” gradient-weighted class activation mapping for explainability, targeting *top_conv* (last Conv2D layer)
95
  - **ECE Calibration** (Guo et al., ICML 2017) β€” Expected Calibration Error with 15-bin reliability diagram to assess prediction confidence reliability
96
 
97
  ## Architecture
 
99
  ```
100
  Input (320, 320, 3)
101
  β”‚
102
+ EfficientNetV2-S (ImageNet pretrained, 513 layers, 20.33M params)
103
  β”‚
104
+ Conv2D(256, 3Γ—3, ReLU, padding=same) β†’ 2,949,376 params
105
  BatchNormalization β†’ 1,024 params
106
  MaxPooling2D(2Γ—2) β†’ 0 params
107
  β”‚
 
118
 
119
  | Component | Output Shape | Parameters |
120
  |-----------|-------------|------------|
121
+ | EfficientNetV2-S (Functional) | (None, 10, 10, 1280) | 20,331,360 |
122
+ | Conv2D 256 3Γ—3 | (None, 10, 10, 256) | 2,949,376 |
123
  | BatchNormalization | (None, 10, 10, 256) | 1,024 |
124
  | MaxPooling2D 2Γ—2 | (None, 5, 5, 256) | 0 |
125
  | GeM Pooling p=3.0 | (None, 256) | 1 |
 
127
  | BatchNormalization | (None, 256) | 1,024 |
128
  | Dropout 0.4 | (None, 256) | 0 |
129
  | Dense 8 Softmax | (None, 8) | 2,056 |
130
+ | **Total** | | **23,350,633** |
131
+ | Trainable (Phase 1) | | **3,018,249** (11.51 MB) |
132
+ | Trainable (Phase 2) | | **17,810,225** (67.94 MB) |
133
+ | Non-trainable (Phase 1) | | **20,332,384** (77.56 MB) |
134
 
135
  ## Performance
136
 
 
138
 
139
  | Metric | Value |
140
  |--------|-------|
141
+ | Test Accuracy | 97.77% |
142
+ | Validation Accuracy (SWA) | 98.36% |
143
+ | Test-Time Augmentation | 97.99% |
144
+ | Test Loss | 0.4262 |
145
+ | Overfitting Gap (Train βˆ’ Test) | 2.11% |
146
+ | Macro Avg Precision | 0.9777 |
147
+ | Macro Avg Recall | 0.9777 |
148
+ | Macro Avg F1-Score | 0.9777 |
149
+ | Top-2 Accuracy | 99.26% |
150
+ | Top-3 Accuracy | 99.70% |
151
+ | Macro ROC-AUC (OvR) | 0.9985 |
152
+ | ECE (15 bins) | 0.1204 (pre-T-scaling; post-T-scaling: 0.0053, T=0.54) |
153
 
154
  ### Per-Class Results
155
 
156
+ | Class | Precision | Recall | F1-Score | AUC (OvR) | Support |
157
+ |-------|-----------|--------|----------|-----------|---------|
158
+ | barn | 0.9760 | 0.9702 | 0.9731 | 0.9950 | 168 |
159
+ | bridge | 0.9591 | 0.9762 | 0.9676 | 0.9983 | 168 |
160
+ | castle | 0.9763 | 0.9821 | 0.9792 | 0.9996 | 168 |
161
+ | mosque | 0.9763 | 0.9821 | 0.9792 | 0.9987 | 168 |
162
+ | skyscraper | 0.9940 | 0.9940 | 0.9940 | 0.9999 | 168 |
163
+ | stadium | 0.9820 | 0.9762 | 0.9791 | 0.9999 | 168 |
164
+ | temple | 0.9816 | 0.9524 | 0.9668 | 0.9976 | 168 |
165
+ | windmill | 0.9765 | 0.9881 | 0.9822 | 0.9987 | 168 |
166
+ | **Macro Avg** | **0.9777** | **0.9777** | **0.9777** | **0.9985** | **1,344** |
167
 
168
  ### Model Selection
169
 
 
171
 
172
  | Checkpoint | Val Accuracy | Val Loss | Description |
173
  |------------|-------------|----------|-------------|
174
+ | `head_training.keras` | 92.34% | 1.0109 | Phase 1 checkpoint (backbone frozen) |
175
+ | `fine_tuning.keras` | 96.28% | 0.5655 | Phase 2 checkpoint (block6+top_conv unfrozen) |
176
+ | `fine_tuning_ema.keras` | 93.53% | 0.6007 | Phase 2 EMA (per-step Polyak averaging) |
177
+ | **`fine_tuning_swa.keras`** | **98.36%** | **0.4109** | **SWA averaged weights ← SELECTED** |
178
 
179
  ### SWA Progression
180
 
181
  | SWA Epoch | Val Accuracy | Val Loss |
182
  |-----------|-------------|----------|
183
+ | 1 | 95.76% | 0.5831 |
184
+ | 2 | 97.62% | 0.5116 |
185
+ | 3 | 97.69% | 0.4748 |
186
+ | 4 | 96.95% | 0.4390 |
187
+ | 5 | 97.47% | 0.4490 |
188
+ | 6 | 97.84% | 0.4416 |
189
+ | 7 | 98.14% | 0.4055 |
190
+ | 8 | 97.32% | 0.4359 |
191
+ | 9 | 97.02% | 0.4519 |
192
+ | 10 | 97.54% | 0.4226 |
193
+ | **SWA + BN (final)** | **98.36%** | **0.4109** |
194
 
195
  ![Training Curves](results/training_curves.png)
196
 
 
210
 
211
  ## Training Details
212
 
213
+ ### Training Strategy
214
 
215
+ Two-phase progressive training with SWA post-processing:
216
 
217
  | Phase | Description | Backbone | Optimizer | LR | Max Epochs | Actual Epochs | CutMix+Mixup | FocalLoss LS |
218
  |-------|-------------|----------|-----------|-----|-----------|---------------|---------------|-------------|
219
  | **Phase 1** β€” Feature Extraction | Train custom head only | Frozen (all) | AdamW (wd=2e-5) | 0.001 + CosineDecay + Warmup 3ep | 25 | 1 | Yes (50/50 alternation) | 0.1 |
220
+ | **Phase 2** β€” Selective Fine-Tuning | Load head_training β†’ fine-tune | block6 + top_conv unfrozen (BN frozen) | DiscriminativeAdamW (block6=0.1Γ—) | 3e-4 + CosineDecay + Warmup 5ep | 50 | 1 + 10 SWA | No | 0.05 |
221
 
222
  > ΒΉ Phase 1 stops when `val_accuracy β‰₯ 85%` threshold (myCallback).
223
 
 
231
  | Learning Rate | 0.001 | 3Γ—10⁻⁴ |
232
  | LR Schedule | WarmupCosineDecay (warmup=3) | WarmupCosineDecay (warmup=5) |
233
  | Weight Decay | 2Γ—10⁻⁡ | 2Γ—10⁻⁡ |
234
+ | LR Multiplier (block6) | β€” | 0.1Γ— (LR scaling via update_step, truly discriminative) |
235
+ | LR Multiplier (top_conv+head) | β€” | 1.0Γ— |
236
  | Loss | FocalLoss (gamma=2.0, LS=0.1) | FocalLoss (gamma=2.0, LS=0.05) |
237
  | Batch Size | 32 | 32 |
238
  | Early Stopping Patience | 7 | 12 |
239
  | myCallback Threshold | val_acc β‰₯ 0.85 | val_acc β‰₯ 0.92 |
240
+ | EMA Decay (per-step) | 0.999 | 0.999 |
241
  | SWA Epochs | β€” | 10 (post-training) |
242
  | SWA LR | β€” | 1Γ—10⁻⁴ (constant) |
243
  | BN Re-estimation Steps | β€” | 100 |
 
249
 
250
  | Technique | Implementation | Reference |
251
  |-----------|---------------|-----------|
252
+ | Transfer Learning | EfficientNetV2-S backbone frozen in Phase 1 | Yosinski et al., NeurIPS 2014 |
253
+ | Selective Fine-Tuning | Unfreeze block6+top_conv only, BN stays frozen | Howard & Ruder, ACL 2018 |
254
+ | Discriminative LR Scaling | block6 LRΓ—0.1 via update_step (truly discriminative β€” 10Γ— smaller updates for pretrained features) | Howard & Ruder, ACL 2018 |
255
  | CutMix + Mixup | Alternation per batch (50/50), Phase 1 only | Yun et al., ICCV 2019; Zhang et al., ICLR 2018 |
256
  | Focal Loss | gamma=2.0, down-weights easy examples | Lin et al., ICCV 2017 |
257
  | Label Smoothing | 0.1 (Phase 1) β†’ 0.05 (Phase 2) | Szegedy et al., CVPR 2016 |
258
  | GeM Pooling | p=3.0 learnable, replaces GAP | Radenovic et al., CVPR 2018 |
259
  | Dropout | 0.4 after Dense(256)+BN | Srivastava et al., JMLR 2014 |
260
  | Batch Normalization | After Conv2D and Dense; frozen during fine-tuning | Ioffe & Szegedy, arXiv 2015 |
261
+ | EMA (per-step) | Shadow weights, decay=0.999, Polyak averaging | Tarvainen & Valpola, NeurIPS 2017 |
262
  | SWA | 10-epoch post-training, constant LR 1e-4 | Izmailov et al., UAI 2018 |
263
+ | Data Augmentation | Rotation Β±15Β°, shift Β±10%, shear Β±0.1 rad, zoom Β±20%, brightness 0.75–1.15, channel shift Β±10.0, horizontal flip | Perez & Wang, arXiv 2017 |
264
+ | Random Erasing | p=0.5, area [0.02–0.15], aspect [0.3–3.3], applied pre-normalization | Zhong et al., AAAI 2020 |
265
  | Test-Time Augmentation | 6 augmentation variants, averaged | Shanmugam et al., ICML 2020 |
266
  | WarmupCosineDecay | Linear warmup + cosine annealing | Loshchilov & Hutter, ICLR 2017 (SGDR) |
267
  | Early Stopping | Patience 7 (Phase 1) / 12 (Phase 2) | Prechelt, Neural Networks 1998 |
268
 
269
  ### Dataset
270
 
271
+ See the dataset curation page for [World Architectural Buildings Dataset for Multi‑Class Image Classification](https://huggingface.co/datasets/0xgr3y/arch-building-dataset) β€” 13,440 images (8 classes Γ— 1,680, balanced) sourced from Pexels with perceptual (pHash) and exact (SHA256) deduplication.
272
 
273
  | Split | Images | Percentage |
274
  |-------|--------|------------|
 
278
 
279
  ### Data Preprocessing
280
 
281
+ - **Normalization:** `preprocess_input` from `tf.keras.applications.efficientnet_v2` (ImageNet distribution)
282
  - **Input resolution:** 320Γ—320 (higher than ImageNet default 224Γ—224 to capture fine-grained architectural details β€” textures, ornaments, facade patterns)
283
  - **Augmentation:** Applied to training set only; validation and test sets use clean preprocessing
284
  - **Split method:** `splitfolders.ratio` from `dataset/`, seed=42
 
287
 
288
  | File | Description |
289
  |------|-------------|
290
+ | `fine_tuning_swa.keras` | Best model β€” SWA averaged weights (226.75 MB) |
291
+ | `fine_tuning_swa.weights.h5` | Best model weights β€” clean, no ProtectAI flag (158.31 MB) |
292
+ | `fine_tuning_swa.safetensors` | Best model weights β€” HF standard, cross-framework (157.10 MB) |
293
+ | `build_model.py` | Architecture reconstruction + CLI inference (20.7 KB) |
294
+ | `head_training.keras` | Phase 1 checkpoint |
295
+ | `fine_tuning.keras` | Phase 2 checkpoint |
296
+ | `fine_tuning_ema.keras` | Phase 2 EMA checkpoint |
297
+ | `saved_model/` | TensorFlow SavedModel format (183.29 MB, for TF Serving) |
298
+ | `tflite/model.tflite` | TensorFlow Lite model (88.36 MB, mobile/embedded) |
299
  | `tflite/label.txt` | Class label names for TF-Lite |
300
+ | `tfjs_model/` | TensorFlow.js model (89.54 MB, 23 shards + model.json) |
301
  | `config.json` | Model configuration and evaluation metrics |
302
  | `label_mapping.json` | Class name ↔ ID mapping with training config and architecture info |
303
  | `preprocessor_config.json` | Input preprocessing specification (320Γ—320) |
304
  | `confusion_pairs.json` | Auto-detected confusion pairs from confusion matrix (threshold >5%) |
305
  | `class_confidence_stats.json` | Per-class mean/std/p5/p95 confidence distribution |
306
  | `model_benchmark.json` | Model parameters, sizes, speed, Top-K, AUC, ECE, TTA metrics |
307
+ | `calibration_data.json` | ECE, bin accuracies/confidences, per-class AUC, Temperature Scaling (T_opt) for calibrated inference |
308
+ | `temperature_config.json` | Temperature Scaling parameters (T, ECE before/after, method) β€” Guo et al., ICML 2017 |
309
  | `results/training_curves.png` | Training/validation accuracy and loss curves |
310
  | `results/confusion_matrix.png` | Confusion matrix on test set |
311
  | `results/per_class_accuracy.png` | Per-class accuracy bar chart |
 
323
 
324
  ### Gradio Space
325
 
326
+ Try the live building classifier: [Architecture Building Image Classifier with Space](https://huggingface.co/spaces/0xgr3y/arch-building-classifier)
327
 
328
+ ### Python β€” build_model.py (recommended)
329
 
330
+ `build_model.py` is a standalone module that provides:
331
+ - **Custom class definitions** (`GeMPooling`, `FocalLoss`, `DiscriminativeAdamW`) with `@register_keras_serializable` β€” importing the module registers all custom classes globally, so `load_model()` works without explicit `custom_objects`.
332
+ - **`ArchBuildingClassifier`** β€” high-level wrapper class with `build()`, `from_weights()`, `from_keras()`, `predict()`, `predict_batch()` methods.
333
+ - **`CUSTOM_OBJECTS`** dict β€” fallback for explicit `custom_objects=` in `load_model()`.
334
+ - **`build_model()`** β€” backward-compatible function that returns a raw `tf.keras.Model`.
 
 
335
 
336
+ Upload `build_model.py` to the same directory as your script or add it to `PYTHONPATH`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
 
338
+ > **Note:** Filenames below use `fine_tuning_swa` as an example. The actual best checkpoint filename depends on training results β€” check the repo for the actual `.keras`, `.weights.h5`, and `.safetensors` filenames.
 
 
 
 
339
 
340
+ ```python
341
+ from build_model import ArchBuildingClassifier
342
+ from huggingface_hub import hf_hub_download
 
343
 
344
+ # Download weights (clean format)
345
+ weights_path = hf_hub_download("0xgr3y/Arch-Building-Image-Classification", "fine_tuning_swa.weights.h5")
 
 
 
346
 
347
+ # Load model: architecture + weights
348
+ clf = ArchBuildingClassifier.from_weights(weights_path)
349
 
350
+ # Inference
351
+ from PIL import Image
352
+ import numpy as np
353
+ label, confidence, top3 = clf.predict(Image.open("skyscraper_00000.jpg"))
354
+ print(f"Predicted: {label} ({confidence:.1%})")
355
+ for cls, prob in top3:
356
+ print(f" {cls}: {prob:.1%}")
357
  ```
358
 
359
+ ### Python β€” TF-Lite (fastest inference)
360
+
361
  ```python
362
  import numpy as np
363
  import tensorflow as tf
 
365
  from PIL import Image
366
  import json
367
 
368
+ try:
369
+ from tensorflow.keras.applications.efficientnet_v2 import preprocess_input
370
+ except (ImportError, ModuleNotFoundError):
371
+ from tensorflow.keras.applications.efficientnet import preprocess_input
372
+
373
+ # Download
374
+ model_path = hf_hub_download("0xgr3y/Arch-Building-Image-Classification", "tflite/model.tflite")
375
  labels_path = hf_hub_download("0xgr3y/Arch-Building-Image-Classification", "label_mapping.json")
376
+
377
  with open(labels_path) as f:
378
  LABELS = json.load(f)["labels"]
379
 
380
+ interpreter = tf.lite.Interpreter(model_path=model_path)
381
  interpreter.allocate_tensors()
 
382
  input_details = interpreter.get_input_details()
383
  output_details = interpreter.get_output_details()
384
 
385
  img = Image.open("skyscraper_00000.jpg").convert("RGB").resize((320, 320))
386
+ arr = np.expand_dims(preprocess_input(
387
+ np.array(img, dtype=np.float32)), axis=0)
388
 
389
  interpreter.set_tensor(input_details[0]["index"], arr)
390
  interpreter.invoke()
 
395
  print(f" {LABELS[i]}: {preds[i]*100:.1f}%")
396
  ```
397
 
398
+ ### Python β€” Keras (convenient)
 
 
 
 
 
 
 
 
399
 
400
  ```python
401
+ import build_model # registers custom classes via @register_keras_serializable
402
  import tensorflow as tf
 
403
  from huggingface_hub import hf_hub_download
404
+ try:
405
+ from tensorflow.keras.applications.efficientnet_v2 import preprocess_input
406
+ except (ImportError, ModuleNotFoundError):
407
+ from tensorflow.keras.applications.efficientnet import preprocess_input
408
  from PIL import Image
409
+ import numpy as np
410
  import json
411
 
412
+ model_path = hf_hub_download("0xgr3y/Arch-Building-Image-Classification", "fine_tuning_swa.keras")
413
  labels_path = hf_hub_download("0xgr3y/Arch-Building-Image-Classification", "label_mapping.json")
 
 
414
 
415
+ model = tf.keras.models.load_model(model_path, compile=False) # custom_objects not needed
 
416
 
417
+ with open(labels_path) as f:
418
+ LABELS = json.load(f)["labels"]
419
 
420
  img = Image.open("skyscraper_00000.jpg").convert("RGB").resize((320, 320))
421
+ arr = np.expand_dims(preprocess_input(np.array(img, dtype=np.float32)), axis=0)
 
422
  preds = model.predict(arr, verbose=0)[0]
423
  print(f"Predicted: {LABELS[np.argmax(preds)]} ({np.max(preds)*100:.1f}%)")
424
  ```
425
 
426
+ ### Python β€” SavedModel (TF Serving)
427
+
428
+ ```python
429
+ from huggingface_hub import snapshot_download
430
+ import tensorflow as tf
431
+ import numpy as np
432
+ from PIL import Image
433
+
434
+ try:
435
+ from tensorflow.keras.applications.efficientnet_v2 import preprocess_input
436
+ except (ImportError, ModuleNotFoundError):
437
+ from tensorflow.keras.applications.efficientnet import preprocess_input
438
+
439
+ snapshot_download("0xgr3y/Arch-Building-Image-Classification", allow_patterns=["saved_model/*"], local_dir=".")
440
+
441
+ # Load SavedModel (created via model.export() β€” inference-only, no custom_objects needed)
442
+ loaded = tf.saved_model.load("saved_model")
443
+
444
+ img = Image.open("skyscraper_00000.jpg").convert("RGB").resize((320, 320))
445
+ arr = tf.constant(np.expand_dims(preprocess_input(np.array(img, dtype=np.float32)), axis=0))
446
+ preds = loaded(arr).numpy()[0]
447
+
448
+ top3_idx = np.argsort(preds)[::-1][:3]
449
+ for i in top3_idx:
450
+ print(f" Class {i}: {preds[i]*100:.1f}%")
451
+ ```
452
+
453
+ ### Python β€” safetensors (HF standard, cross-framework)
454
+
455
+ > **Note:** safetensors stores raw weight tensors without architecture metadata. To load, reconstruct the architecture with `build_model.py` first, then map tensors manually. For most use cases, `.weights.h5` (via `ArchBuildingClassifier.from_weights()`) is simpler and equally clean.
456
+
457
+ ```python
458
+ from safetensors.numpy import load_file
459
+ from build_model import ArchBuildingClassifier
460
+ from PIL import Image
461
+
462
+ # Reconstruct architecture
463
+ clf = ArchBuildingClassifier.build()
464
+
465
+ # Load safetensors tensors
466
+ tensors = load_file("fine_tuning_swa.safetensors")
467
+
468
+ # Map tensors to model weights (iterate layers, not .variables β€” Keras 3 compatible)
469
+ for layer in clf.keras_model.layers:
470
+ for w in layer.weights:
471
+ name = w.name.replace(':', '_').replace('/', '_')
472
+ if name in tensors:
473
+ w.assign(tensors[name])
474
+
475
+ # Inference
476
+ label, confidence, top3 = clf.predict(Image.open("skyscraper_00000.jpg"))
477
+ ```
478
+
479
+ ## Inference Verification
480
+
481
+ Keras vs TFLite consistency was verified on 8 random test samples (1 per class):
482
+
483
+ | Metric | Result |
484
+ |--------|--------|
485
+ | Keras correct | 7/8 (88%) |
486
+ | TFLite correct | 7/8 (88%) |
487
+ | Keras vs TFLite match | **8/8 (100%)** β€” identical predictions |
488
+ | Keras inference speed | 358.0 ms |
489
+ | TFLite inference speed | 170.0 ms |
490
+
491
+ > The 1 misclassification (castle→barn, 65% confidence) is consistent with the 97.77% test accuracy. The 8/8 match confirms TFLite conversion preserves model behavior exactly.
492
+
493
+ ## Security Notice (PAIT-KERAS-301)
494
+
495
+ The `.keras` files in this repository are flagged **"Unsafe"** by [Protect AI Guardian](https://protectai.com/insights/models/0xgr3y/Arch-Building-Image-Classification) (threat: PAIT-KERAS-301). This is a **structural false positive**, not a malware detection:
496
+
497
+ - **What the scanner checks:** String-matching of `class_name` fields in the Keras v3 config against a whitelist of built-in Keras layers.
498
+ - **Why flagged:** The model contains a custom layer (`GeMPooling`) β€” a non-standard class name triggers the flag.
499
+ - **What it does NOT check:** The scanner does not analyze the Python code of the custom class, does not look for `eval()`/`exec()`/`os.system()`, and does not detect actual malware.
500
+ - **Other scanners:** VirusTotal βœ…, JFrog βœ…, HF Picklescan βœ… β€” all clean. Only Protect AI flags this file.
501
+
502
+ **The custom classes are safe and open source:**
503
+ - `GeMPooling` β€” Generalized Mean Pooling (Radenovic et al., CVPR 2018). Pure tensor ops: `tf.pow`, `tf.reduce_mean`, `tf.maximum`.
504
+ - `FocalLoss` β€” Focal Loss (Lin et al., ICCV 2017). Pure tensor ops.
505
+ - `DiscriminativeAdamW` β€” AdamW subclass with gradient scaling. No file I/O, no network calls, no arbitrary code.
506
+
507
+ Full source code for all custom classes is available in [`build_model.py`](https://huggingface.co/0xgr3y/Arch-Building-Image-Classification/blob/main/build_model.py) and the training notebook for public audit.
508
+
509
+ ## Multi-Format Deployment Guide
510
+
511
+ This model is provided in multiple formats to suit different deployment scenarios. Formats marked βœ… are **not flagged** by Protect AI (no custom class serialization).
512
+
513
+ | Format | File | Size | Protect AI | Inference Speed | Best For |
514
+ |--------|------|------|------------|-----------------|----------|
515
+ | **TF-Lite** βœ… | `tflite/model.tflite` | ~88 MB | βœ… Safe | **170.0 ms** (fastest) | Mobile, edge, embedded, HF Space |
516
+ | **SavedModel** βœ… | `saved_model/` | ~183 MB | βœ… Safe | β€” | TensorFlow Serving, cloud backend |
517
+ | **TFJS** βœ… | `tfjs_model/` | ~90 MB | βœ… Safe | β€” | Browser, Node.js (no backend) |
518
+ | **Weights H5** βœ… | `fine_tuning_swa.weights.h5` | ~158 MB | βœ… Safe | β€” | Programmatic load via `build_model.py` |
519
+ | **safetensors** βœ… | `fine_tuning_swa.safetensors` | ~157 MB | βœ… Safe | β€” | HF standard, cross-framework |
520
+ | **Build Script** βœ… | `build_model.py` | ~21 KB | βœ… Safe | β€” | Architecture reconstruction + `load_weights()` |
521
+ | **Keras** ⚠️ | `fine_tuning_swa.keras` | ~227 MB | ⚠️ Flagged | 358.0 ms | Developer reference, fine-tuning |
522
+
523
+ ### Load Examples
524
+
525
+ See **Usage** section above for complete load + inference examples for each format.
526
+
527
  ## Intended Use
528
 
529
  - Architectural style classification from building photographs
 
536
  - Trained on Pexels stock photography β€” performance may differ on user-generated or field photographs
537
  - Limited to 8 architectural classes (barn, bridge, castle, mosque, skyscraper, stadium, temple, windmill)
538
  - Confusion pair analysis found **0 significant pairs** (threshold >5%) β€” all 8 classes are well-distinguished by the model; see `confusion_pairs.json` for details
539
+ - Barn and windmill share 3 cross-class duplicates (0.02% of dataset) β€” left as-is due to negligible impact
540
  - Inference confidence can be low on atypical examples
541
 
542
  ![Misclassification Examples](results/misclassification_examples.png)
 
553
 
554
  - **Gradio Space (Live):** [arch-building-classifier Space](https://huggingface.co/spaces/0xgr3y/arch-building-classifier)
555
  - **Dataset Studio:** [0xgr3y/arch-building-dataset](https://huggingface.co/datasets/0xgr3y/arch-building-dataset)
556
+ - **GitHub Repo:** [arcxteam/building-architectural-image-classifier](https://github.com/arcxteam/building-architectural-image-classifier)
557
 
558
  ## References
559
 
560
+ 1. Tan, M., & Le, Q. V. (2021). EfficientNetV2: Smaller Models and Faster Training. *ICML 2021*. [arXiv:2104.00298](https://arxiv.org/abs/2104.00298)
561
  2. Radenovic, F., Tolias, G., & Chum, O. (2018). Fine-Tuning CNN Image Retrieval with No Human Annotation. *IEEE TPAMI*. [arXiv:1711.02512](https://arxiv.org/abs/1711.02512)
562
  3. Lin, T.-Y., Goyal, P., Girshick, R., He, K., & Dollar, P. (2017). Focal Loss for Dense Object Detection. *ICCV 2017*. [arXiv:1708.02002](https://arxiv.org/abs/1708.02002)
563
  4. Izmailov, P., Podoprikhin, D., Garipov, T., Vetrov, D., & Wilson, A. G. (2018). Averaging Weights Leads to Wider Optima and Better Generalization. *UAI 2018*. [arXiv:1803.05407](https://arxiv.org/abs/1803.05407)
 
585
  ```bibtex
586
  @misc{saugani2026_arch_building,
587
  title={Fine-Grained Image Classification of World Architecture:
588
+ An EfficientNetV2-S Transfer Learning Approach with Layered Regularization},
589
  author={Saugani},
590
  year={2026},
591
  publisher={Hugging Face},