dsaint31 commited on
Commit
e59dbaa
ยท
verified ยท
1 Parent(s): 69933de

Add/Update backbone checkpoints (count=6)

Browse files
Files changed (37) hide show
  1. manifest_20260210_111913.json +41 -0
  2. models/google__efficientnet-b0/config.json +2 -1
  3. models/google__efficientnet-b0/ds_cfg.py +136 -45
  4. models/google__efficientnet-b0/ds_model.py +167 -167
  5. models/google__efficientnet-b0/ds_proc.py +332 -0
  6. models/google__efficientnet-b0/model.safetensors +1 -1
  7. models/google__efficientnet-b0/preprocessor_config.json +8 -0
  8. models/google__vit-base-patch16-224/config.json +2 -1
  9. models/google__vit-base-patch16-224/ds_cfg.py +136 -45
  10. models/google__vit-base-patch16-224/ds_model.py +167 -167
  11. models/google__vit-base-patch16-224/ds_proc.py +332 -0
  12. models/google__vit-base-patch16-224/model.safetensors +1 -1
  13. models/google__vit-base-patch16-224/preprocessor_config.json +8 -0
  14. models/microsoft__resnet-50/config.json +2 -1
  15. models/microsoft__resnet-50/ds_cfg.py +136 -45
  16. models/microsoft__resnet-50/ds_model.py +167 -167
  17. models/microsoft__resnet-50/ds_proc.py +332 -0
  18. models/microsoft__resnet-50/model.safetensors +1 -1
  19. models/microsoft__resnet-50/preprocessor_config.json +8 -0
  20. models/microsoft__swin-tiny-patch4-window7-224/config.json +2 -1
  21. models/microsoft__swin-tiny-patch4-window7-224/ds_cfg.py +136 -45
  22. models/microsoft__swin-tiny-patch4-window7-224/ds_model.py +167 -167
  23. models/microsoft__swin-tiny-patch4-window7-224/ds_proc.py +332 -0
  24. models/microsoft__swin-tiny-patch4-window7-224/model.safetensors +1 -1
  25. models/microsoft__swin-tiny-patch4-window7-224/preprocessor_config.json +8 -0
  26. models/timm__densenet121.tv_in1k/config.json +2 -1
  27. models/timm__densenet121.tv_in1k/ds_cfg.py +136 -45
  28. models/timm__densenet121.tv_in1k/ds_model.py +167 -167
  29. models/timm__densenet121.tv_in1k/ds_proc.py +332 -0
  30. models/timm__densenet121.tv_in1k/model.safetensors +1 -1
  31. models/timm__densenet121.tv_in1k/preprocessor_config.json +8 -0
  32. models/torchvision__densenet121/config.json +2 -1
  33. models/torchvision__densenet121/ds_cfg.py +136 -45
  34. models/torchvision__densenet121/ds_model.py +167 -167
  35. models/torchvision__densenet121/ds_proc.py +332 -0
  36. models/torchvision__densenet121/model.safetensors +1 -1
  37. models/torchvision__densenet121/preprocessor_config.json +8 -0
manifest_20260210_111913.json ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "timestamp": "20260210_111913",
3
+ "device": "mps",
4
+ "repo_id": "dsaint31/bb_mlp_224",
5
+ "revision": "main",
6
+ "tag": null,
7
+ "num_labels": 3,
8
+ "count": 6,
9
+ "items": [
10
+ {
11
+ "backbone": "google/vit-base-patch16-224",
12
+ "subdir": "models/google__vit-base-patch16-224",
13
+ "dirname": "google__vit-base-patch16-224"
14
+ },
15
+ {
16
+ "backbone": "microsoft/swin-tiny-patch4-window7-224",
17
+ "subdir": "models/microsoft__swin-tiny-patch4-window7-224",
18
+ "dirname": "microsoft__swin-tiny-patch4-window7-224"
19
+ },
20
+ {
21
+ "backbone": "microsoft/resnet-50",
22
+ "subdir": "models/microsoft__resnet-50",
23
+ "dirname": "microsoft__resnet-50"
24
+ },
25
+ {
26
+ "backbone": "google/efficientnet-b0",
27
+ "subdir": "models/google__efficientnet-b0",
28
+ "dirname": "google__efficientnet-b0"
29
+ },
30
+ {
31
+ "backbone": "timm/densenet121.tv_in1k",
32
+ "subdir": "models/timm__densenet121.tv_in1k",
33
+ "dirname": "timm__densenet121.tv_in1k"
34
+ },
35
+ {
36
+ "backbone": "torchvision/densenet121",
37
+ "subdir": "models/torchvision__densenet121",
38
+ "dirname": "torchvision__densenet121"
39
+ }
40
+ ]
41
+ }
models/google__efficientnet-b0/config.json CHANGED
@@ -24,10 +24,11 @@
24
  "num_labels": 3,
25
  "transformers_version": "5.1.0",
26
  "ds_provenance": {
27
- "created_at": "20260210_050905",
28
  "repo_id": "dsaint31/bb_mlp_224",
29
  "subdir": "models/google__efficientnet-b0",
30
  "wrapper_class": "BackboneWithMLPHeadForImageClassification",
 
31
  "backbone_id": "google/efficientnet-b0",
32
  "pretrained_injected": true,
33
  "policy_init_pretrained_forbidden": true,
 
24
  "num_labels": 3,
25
  "transformers_version": "5.1.0",
26
  "ds_provenance": {
27
+ "created_at": "20260210_111913",
28
  "repo_id": "dsaint31/bb_mlp_224",
29
  "subdir": "models/google__efficientnet-b0",
30
  "wrapper_class": "BackboneWithMLPHeadForImageClassification",
31
+ "processor_class": "BackboneMLPHead224ImageProcessor",
32
  "backbone_id": "google/efficientnet-b0",
33
  "pretrained_injected": true,
34
  "policy_init_pretrained_forbidden": true,
models/google__efficientnet-b0/ds_cfg.py CHANGED
@@ -1,84 +1,175 @@
1
- from typing import Literal, Union
2
-
3
- from transformers import AutoConfig
4
  from transformers import PretrainedConfig
 
5
  from ds_meta import BackboneID, BACKBONE_META
6
 
7
- # ============================================================
8
- # 3) Config
9
- # ============================================================
10
  class BackboneMLPHeadConfig(PretrainedConfig):
11
  """
12
- backbone_name_or_path๋Š” BACKBONE_META ํ‚ค(ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ)๋งŒ ํ—ˆ์šฉ.
13
-
14
- ์ค‘์š”ํ•œ ์ 
15
- ----------
16
- - transformers ๋‚ด๋ถ€ ๊ฒฝ๋กœ์—์„œ "๋ฌด์ธ์ž ์ƒ์„ฑ"์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ,
17
- backbone_name_or_path=None์ธ ๊ฒฝ์šฐ๋Š” ๊ฒ€์ฆ ์—†์ด ๊ธฐ๋ณธ๊ฐ’๋งŒ ์„ธํŒ…ํ•˜๊ณ  return
18
- ํ•˜๋Š” ๊ตฌํ˜„์ด ๋ฐ˜๋“œ์‹œ ํ•„์š”ํ•จ.
19
- - ๋‹จ, ์‹ค์ œ ์‚ฌ์šฉ์—์„œ๋Š” backbone_name_or_path๋ฅผ ๋ฐ˜๋“œ์‹œ ์ง€์ •ํ•ด์„œ ์ƒ์„ฑํ•ด์•ผ ํ•จ.
20
  """
21
 
22
- # AutoConfig๊ฐ€ ์ด ๋ชจ๋ธ์„ ์‹๋ณ„ํ•˜๋Š”๋ฐ ์‚ฌ์šฉํ•˜๋Š” ๋ฌธ์ž์—ด ID.
 
23
  model_type = "backbone-mlphead-224-fixed"
24
 
25
- # ์ƒ์„ฑ์ž์—์„œ ๋ชจ๋“  ์ธ์ž์— ๊ธฐ๋ณธ๊ฐ’ ๋ถ€์—ฌ ๋ฐ˜๋“œ์‹œ ํ•„์š”!
26
  def __init__(
27
  self,
28
  backbone_name_or_path: BackboneID | None = None,
29
  mlp_head_bottleneck: int = 256,
30
  mlp_head_dropout: float = 0.2,
31
- label2id: dict[str, int]|None = None,
32
- id2label: dict[int, str]|None = None,
33
  **kwargs,
34
  ):
35
- # ๋ฌด์ธ์ž ์ƒ์„ฑ ๊ฒฝ๋กœ ๋ฐฉ์–ด
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  if backbone_name_or_path is None:
 
 
 
 
 
 
 
37
  super().__init__(**kwargs)
 
 
 
38
  self.backbone_name_or_path = None
39
- self.mlp_head_bottleneck = int(mlp_head_bottleneck)
40
- self.mlp_head_dropout = float(mlp_head_dropout)
41
- # num_labels๋„ ๊ธฐ๋ณธ๊ฐ’ ํ•˜๋‚˜ ์ค˜์„œ 0 ๋ฐฉ์ง€(3์œผ๋กœ ๋‘๋“  0์œผ๋กœ ๋‘๋“  ์ •์ฑ…)
42
- # ์ตœ์†Œ ์•ˆ์ „: id2label์ด ์žˆ์œผ๋ฉด ๊ทธ ๊ธธ์ด, ์—†์œผ๋ฉด 0
 
 
 
 
 
 
 
43
  return
44
 
45
- # ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๊ฒ€์ฆ
 
 
 
 
 
 
 
 
46
  if backbone_name_or_path not in BACKBONE_META:
47
  raise ValueError(
48
  f"Unsupported backbone_name_or_path={backbone_name_or_path}. "
49
  f"Allowed: {sorted(BACKBONE_META.keys())}"
50
  )
51
- if id2label is None and label2id is None:
52
- raise ValueError("id2label/label2id ๋‘˜ ์ค‘ ํ•˜๋‚˜๋Š” ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.")
53
- if id2label is None:
54
- id2label = {v: k for k, v in label2id.items()}
55
- if label2id is None:
56
- label2id = {v: k for k, v in id2label.items()}
57
-
58
- num_labels = len(id2label)
59
- # kwargs์— ์ด๋ฏธ num_labels๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ
60
- if "num_labels" not in kwargs:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  kwargs["num_labels"] = num_labels
62
 
 
 
 
 
 
 
63
  super().__init__(
64
- label2id=label2id,
65
- id2label=id2label,
66
  **kwargs,
67
  )
68
-
69
- # 4) ์ค‘์š”: ๋ถ€๋ชจ ํ•„๋“œ์—๋„ num_labels ๋ฐ˜์˜(์ €์žฅ/๋กœ๋“œ ์•ˆ์ •)
70
- self.num_labels = num_labels
71
- self.label2id = label2id
72
- self.id2label = id2label
73
 
 
 
 
 
 
 
74
  self.backbone_name_or_path = backbone_name_or_path
75
- self.mlp_head_bottleneck = int(mlp_head_bottleneck)
76
- self.mlp_head_dropout = float(mlp_head_dropout)
 
 
 
 
77
 
78
  def to_dict(self):
 
 
79
  output = super().to_dict()
80
- # ๋ช…์‹œ์ ์œผ๋กœ ํฌํ•จ
81
- output["num_labels"] = self.num_labels
 
 
 
 
82
  return output
83
 
 
 
 
84
  BackboneMLPHeadConfig.register_for_auto_class("AutoConfig")
 
 
 
 
1
  from transformers import PretrainedConfig
2
+
3
  from ds_meta import BackboneID, BACKBONE_META
4
 
5
+
 
 
6
  class BackboneMLPHeadConfig(PretrainedConfig):
7
  """
8
+ Configuration for Backbone + MLP Head models.
9
+
10
+ Backbone + MLP Head ๋ชจ๋ธ์„ ์œ„ํ•œ ์„ค์ • ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.
 
 
 
 
 
11
  """
12
 
13
+ # This string is used by Hugging Face AutoConfig to identify the config class.
14
+ # Hugging Face AutoConfig๊ฐ€ ์ด config๋ฅผ ์‹๋ณ„ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” ๊ณ ์œ  ID์ž…๋‹ˆ๋‹ค.
15
  model_type = "backbone-mlphead-224-fixed"
16
 
 
17
  def __init__(
18
  self,
19
  backbone_name_or_path: BackboneID | None = None,
20
  mlp_head_bottleneck: int = 256,
21
  mlp_head_dropout: float = 0.2,
22
+ label2id: dict[str, int] | None = None,
23
+ id2label: dict[int, str] | None = None,
24
  **kwargs,
25
  ):
26
+ # ============================================================
27
+ # 0) Guard for argument-less construction
28
+ # 0) ๋ฌด์ธ์ž ์ƒ์„ฑ ๊ฒฝ๋กœ ๋ฐฉ์–ด
29
+ # ============================================================
30
+ # Transformers may internally construct this config without arguments
31
+ # (e.g., during AutoConfig resolution or Hub loading).
32
+ # Transformers ๋‚ด๋ถ€์—์„œ AutoConfig ๋˜๋Š” Hub ๋กœ๋“œ ๊ณผ์ • ์ค‘
33
+ # ์ธ์ž ์—†์ด config๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ฒฝ๋กœ๊ฐ€ ์‹ค์ œ๋กœ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.
34
+ #
35
+ # In this case, we must NOT validate or raise errors.
36
+ # ์ด ๊ฒฝ์šฐ ๊ฒ€์ฆ์ด๋‚˜ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
37
+ #
38
+ # The goal here is to provide a minimal, serialization-safe config.
39
+ # ๋ชฉํ‘œ๋Š” ์ตœ์†Œํ•œ์˜ ๊ฐ’๋งŒ ์ฑ„์›Œ ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™”๊ฐ€ ๊นจ์ง€์ง€ ์•Š๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.
40
  if backbone_name_or_path is None:
41
+ # num_labels may be implicitly assumed by downstream code,
42
+ # so we explicitly set a safe default.
43
+ # num_labels๋Š” downstream ์ฝ”๋“œ์—์„œ ์•”๋ฌต์ ์œผ๋กœ ๊ฐ€์ •๋˜๋ฏ€๋กœ
44
+ # ์•ˆ์ „ํ•œ ๊ธฐ๋ณธ๊ฐ’์„ ๋ช…์‹œ์ ์œผ๋กœ ๋„ฃ์–ด์ค๋‹ˆ๋‹ค.
45
+ if "num_labels" not in kwargs:
46
+ kwargs["num_labels"] = 0
47
+
48
  super().__init__(**kwargs)
49
+
50
+ # Backbone is intentionally unset in this path.
51
+ # ์ด ๊ฒฝ๋กœ์—์„œ๋Š” backbone์„ ์˜๋„์ ์œผ๋กœ ์„ค์ •ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
52
  self.backbone_name_or_path = None
53
+
54
+ # Store MLP head hyperparameters for completeness.
55
+ # MLP head ๊ด€๋ จ ํ•˜์ดํผํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ํ˜•ํƒœ ์œ ์ง€๋ฅผ ์œ„ํ•ด ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
56
+ self.mlp_head_bottleneck = int(mlp_head_bottleneck)
57
+ self.mlp_head_dropout = float(mlp_head_dropout)
58
+
59
+ # Empty label mappings ensure stable save/load behavior.
60
+ # ๋นˆ label ๋งคํ•‘์€ ์ €์žฅ/๋กœ๋“œ ์•ˆ์ •์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.
61
+ self.label2id = {}
62
+ self.id2label = {}
63
+ self.num_labels = int(kwargs.get("num_labels", 0))
64
  return
65
 
66
+ # ============================================================
67
+ # 1) Backbone whitelist validation
68
+ # 1) Backbone ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๊ฒ€์ฆ
69
+ # ============================================================
70
+ # Only backbones explicitly registered in BACKBONE_META are allowed.
71
+ # BACKBONE_META์— ๋“ฑ๋ก๋œ backbone๋งŒ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.
72
+ #
73
+ # This prevents accidental usage of unsupported or inconsistent backbones.
74
+ # ์ง€์›๋˜์ง€ ์•Š๊ฑฐ๋‚˜ ๋ถˆ์ผ์น˜ํ•œ backbone ์‚ฌ์šฉ์„ ์›์ฒœ ์ฐจ๋‹จํ•ฉ๋‹ˆ๋‹ค.
75
  if backbone_name_or_path not in BACKBONE_META:
76
  raise ValueError(
77
  f"Unsupported backbone_name_or_path={backbone_name_or_path}. "
78
  f"Allowed: {sorted(BACKBONE_META.keys())}"
79
  )
80
+
81
+ # ============================================================
82
+ # 2) Label mapping normalization
83
+ # 2) ๋ผ๋ฒจ ๋งคํ•‘ ์ •๊ทœํ™”
84
+ # ============================================================
85
+ # Both label2id and id2label may be None during pure loading scenarios.
86
+ # ๋‹จ์ˆœ ๋กœ๋“œ(from_pretrained) ์‹œ label2id/id2label์ด None์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
87
+ #
88
+ # We allow this here to keep Hub loading and verification stable.
89
+ # Hub ๋กœ๋“œ ๋ฐ ๊ฒ€์ฆ ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด ์—ฌ๊ธฐ์„œ๋Š” ์ด๋ฅผ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.
90
+ #
91
+ # Fail-fast validation should happen at model or training level instead.
92
+ # ์‹ค์ œ ์‚ฌ์šฉ ๊ฒ€์ฆ์€ ๋ชจ๋ธ ๋˜๋Š” ํ•™์Šต ๋‹จ๊ณ„์—์„œ fail-fast๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
93
+ if label2id is None and id2label is None:
94
+ # Respect num_labels if explicitly provided, otherwise default to 0.
95
+ # num_labels๊ฐ€ ์ฃผ์–ด์กŒ๋‹ค๋ฉด ์กด์ค‘ํ•˜๊ณ , ์•„๋‹ˆ๋ฉด 0์œผ๋กœ ๋‘ก๋‹ˆ๋‹ค.
96
+ num_labels = int(kwargs.get("num_labels", 0))
97
+ label2id_norm: dict[str, int] = {}
98
+ id2label_norm: dict[int, str] = {}
99
+ else:
100
+ # If only one mapping is provided, derive the other.
101
+ # ํ•˜๋‚˜๋งŒ ์ฃผ์–ด์ง„ ๊ฒฝ์šฐ ๋‚˜๋จธ์ง€๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
102
+ if id2label is None:
103
+ id2label = {v: k for k, v in label2id.items()}
104
+ if label2id is None:
105
+ label2id = {v: k for k, v in id2label.items()}
106
+
107
+ # Ensure both mappings are consistent in size.
108
+ # ๋‘ ๋งคํ•‘์˜ ํฌ๊ธฐ๊ฐ€ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
109
+ if len(label2id) != len(id2label):
110
+ raise ValueError(
111
+ f"label2id/id2label size mismatch: "
112
+ f"{len(label2id)} vs {len(id2label)}"
113
+ )
114
+
115
+ num_labels = len(id2label)
116
+ label2id_norm = dict(label2id)
117
+ id2label_norm = dict(id2label)
118
+
119
+ # ============================================================
120
+ # 3) num_labels consistency enforcement
121
+ # 3) num_labels ์ผ๊ด€์„ฑ ๊ฐ•์ œ
122
+ # ============================================================
123
+ # If num_labels is provided via kwargs, it must match inferred labels.
124
+ # kwargs๋กœ num_labels๊ฐ€ ๋“ค์–ด์˜จ ๊ฒฝ์šฐ ์ถ”๋ก ๋œ ๊ฐ’๊ณผ ๋ฐ˜๋“œ์‹œ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
125
+ if "num_labels" in kwargs:
126
+ if (label2id is not None or id2label is not None) and int(kwargs["num_labels"]) != num_labels:
127
+ raise ValueError(
128
+ f"kwargs['num_labels']={kwargs['num_labels']} "
129
+ f"!= inferred num_labels={num_labels}"
130
+ )
131
+ else:
132
  kwargs["num_labels"] = num_labels
133
 
134
+ # ============================================================
135
+ # 4) Parent initialization
136
+ # 4) ๋ถ€๋ชจ ํด๋ž˜์Šค ์ดˆ๊ธฐํ™”
137
+ # ============================================================
138
+ # Initialize PretrainedConfig with normalized label mappings.
139
+ # ์ •๊ทœํ™”๋œ ๋ผ๋ฒจ ๋งคํ•‘์„ ์‚ฌ์šฉํ•˜์—ฌ PretrainedConfig๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
140
  super().__init__(
141
+ label2id=label2id_norm,
142
+ id2label=id2label_norm,
143
  **kwargs,
144
  )
 
 
 
 
 
145
 
146
+ # ============================================================
147
+ # 5) Explicit attribute assignment for save/load stability
148
+ # 5) ์ €์žฅ/๋กœ๋“œ ์•ˆ์ •์„ฑ์„ ์œ„ํ•œ ๋ช…์‹œ์  ์†์„ฑ ๊ณ ์ •
149
+ # ============================================================
150
+ # Explicitly reassign critical fields to avoid subtle serialization issues.
151
+ # ๋ฏธ๋ฌ˜ํ•œ ์ง๋ ฌํ™” ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ํ•ต์‹ฌ ํ•„๋“œ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ๋‹ค์‹œ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
152
  self.backbone_name_or_path = backbone_name_or_path
153
+ self.mlp_head_bottleneck = int(mlp_head_bottleneck)
154
+ self.mlp_head_dropout = float(mlp_head_dropout)
155
+
156
+ self.label2id = label2id_norm
157
+ self.id2label = id2label_norm
158
+ self.num_labels = int(kwargs["num_labels"])
159
 
160
  def to_dict(self):
161
+ # Call the parent implementation first.
162
+ # ๋จผ์ € ๋ถ€๋ชจ ๊ตฌํ˜„์„ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
163
  output = super().to_dict()
164
+
165
+ # Force num_labels to be present and consistent.
166
+ # num_labels๊ฐ€ ๋ฐ˜๋“œ์‹œ ์กด์žฌํ•˜๊ณ  ์ผ๊ด€๋˜๋„๋ก ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค.
167
+ output["num_labels"] = int(
168
+ getattr(self, "num_labels", output.get("num_labels", 0))
169
+ )
170
  return output
171
 
172
+
173
+ # Register this config so it can be resolved via AutoConfig.
174
+ # AutoConfig๋ฅผ ํ†ตํ•ด ์ด config๊ฐ€ ํ•ด์„๋  ์ˆ˜ ์žˆ๋„๋ก ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.
175
  BackboneMLPHeadConfig.register_for_auto_class("AutoConfig")
models/google__efficientnet-b0/ds_model.py CHANGED
@@ -1,10 +1,9 @@
1
  #!/usr/bin/env python3
2
  # -*- coding: utf-8 -*-
3
 
4
- from __future__ import annotations
5
 
6
- from dataclasses import dataclass
7
- from typing import Dict, Any, Optional, Tuple, Set, List
8
 
9
  import torch
10
  import torch.nn as nn
@@ -12,146 +11,88 @@ import torch.nn.functional as F
12
 
13
  # --- transformers core ---
14
  from transformers.modeling_utils import PreTrainedModel
15
- from transformers import AutoModel, AutoConfig, PretrainedConfig
16
  from transformers.modeling_outputs import ImageClassifierOutput
17
 
18
  # --- torchvision ---
19
  from torchvision import models as tv_models
20
 
21
-
22
- # ============================================================
23
- # (0) Config
24
- # - ๊ธฐ์กด ds_cfg.BackboneMLPHeadConfig์™€ ํ•„๋“œ๋ช…์„ ๋งž์ถ”์—ˆ์Šต๋‹ˆ๋‹ค.
25
- # - ์ด๋ฏธ ds_cfg.py๊ฐ€ ์žˆ๋‹ค๋ฉด: ์ด ๋ธ”๋ก์„ ์ง€์šฐ๊ณ  "from ds_cfg import BackboneMLPHeadConfig"๋กœ ๋ฐ”๊ฟ”๋„ ๋ฉ๋‹ˆ๋‹ค.
26
- # ============================================================
27
- class BackboneMLPHeadConfig(PretrainedConfig):
28
- model_type = "backbone_mlphead"
29
-
30
- def __init__(
31
- self,
32
- backbone_name_or_path: Optional[str] = None,
33
- num_labels: int = 2,
34
- id2label: Optional[Dict[int, str]] = None,
35
- label2id: Optional[Dict[str, int]] = None,
36
- mlp_head_bottleneck: int = 256,
37
- mlp_head_dropout: float = 0.2,
38
- **kwargs,
39
- ):
40
- super().__init__(**kwargs)
41
- self.backbone_name_or_path = backbone_name_or_path
42
- self.num_labels = num_labels
43
- self.id2label = id2label or {i: f"LABEL_{i}" for i in range(num_labels)}
44
- self.label2id = label2id or {v: k for k, v in self.id2label.items()}
45
- self.mlp_head_bottleneck = mlp_head_bottleneck
46
- self.mlp_head_dropout = mlp_head_dropout
47
-
48
-
49
- # ============================================================
50
- # (1) BACKBONE_META
51
- # - ์ด๋ฏธ ds_meta.py๊ฐ€ ์žˆ๋‹ค๋ฉด: ์ด ๋ธ”๋ก์„ ์ง€์šฐ๊ณ  "from ds_meta import BACKBONE_META"๋กœ ๋ฐ”๊ฟ”๋„ ๋ฉ๋‹ˆ๋‹ค.
52
- # ============================================================
53
- BACKBONE_META: Dict[str, Dict[str, Any]] = {
54
- # transformers
55
- "google/vit-base-patch16-224": {
56
- "type": "vit",
57
- "feat_rule": "cls",
58
- "feat_dim": 768,
59
- "has_bn": False,
60
- "unfreeze": "last_n",
61
- },
62
- "microsoft/swin-tiny-patch4-window7-224": {
63
- "type": "swin",
64
- "feat_rule": "pool_or_mean",
65
- "feat_dim": 768,
66
- "has_bn": False,
67
- "unfreeze": "last_n",
68
- },
69
- "microsoft/resnet-50": {
70
- "type": "resnet",
71
- "feat_rule": "pool_or_gap",
72
- "feat_dim": 2048,
73
- "has_bn": True,
74
- "unfreeze": "last_n",
75
- },
76
- "google/efficientnet-b0": {
77
- "type": "efficientnet",
78
- "feat_rule": "pool_or_gap",
79
- "feat_dim": 1280,
80
- "has_bn": True,
81
- "unfreeze": "last_n",
82
- },
83
-
84
- # densenet
85
- "timm/densenet121.tv_in1k": {
86
- "type": "timm_densenet",
87
- "feat_rule": "timm_gap",
88
- "feat_dim": 1024,
89
- "has_bn": True,
90
- "unfreeze": "last_n",
91
- },
92
- "torchvision/densenet121": {
93
- "type": "torchvision_densenet",
94
- "feat_rule": "torchvision_densenet_gap",
95
- "feat_dim": 1024,
96
- "has_bn": True,
97
- "unfreeze": "last_n",
98
- },
99
- }
100
-
101
-
102
- # ============================================================
103
- # (2) MLP Head
104
- # - ์ด๋ฏธ mlp_head.py๊ฐ€ ์žˆ๋‹ค๋ฉด: ์ด ๋ธ”๋ก์„ ์ง€์šฐ๊ณ  "from mlp_head import MLPHead"๋กœ ๋ฐ”๊ฟ”๋„ ๋ฉ๋‹ˆ๋‹ค.
105
- # ============================================================
106
- class MLPHead(nn.Module):
107
- def __init__(self, in_dim: int, num_labels: int, bottleneck: int = 256, p: float = 0.2):
108
- super().__init__()
109
- self.fc2 = nn.Linear(in_dim, bottleneck)
110
- self.act2 = nn.GELU()
111
- self.drop2 = nn.Dropout(p)
112
- self.fc3 = nn.Linear(bottleneck, num_labels)
113
-
114
- def forward(self, x: torch.Tensor) -> torch.Tensor:
115
- x = self.drop2(self.act2(self.fc2(x)))
116
- return self.fc3(x)
117
-
118
 
119
  # ============================================================
120
  # (3) Model: backbone + MLP head
121
- # ํ•ต์‹ฌ ์„ค๊ณ„:
122
- # - __init__: backbone ๊ตฌ์กฐ(skeleton)๋งŒ ์ƒ์„ฑ. pretrained๋ฅผ ์ ˆ๋Œ€ ๋กœ๋“œํ•˜์ง€ ์•Š์Œ.
123
- # - pretrained ์ฃผ์ž…: model.load_backbone_pretrained_()๋ฅผ "fresh-start์—์„œ๋งŒ" ์ง์ ‘ ํ˜ธ์ถœ.
124
- # - from_pretrained override ์—†์Œ: HF ๊ธฐ๋ณธ ๋กœ๋”๊ฐ€ checkpoint๋ฅผ ๊ทธ๋Œ€๋กœ ๋ณต์›.
125
  # ============================================================
 
 
 
 
 
 
 
 
126
  class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
 
 
127
  config_class = BackboneMLPHeadConfig
128
 
129
  def __init__(self, config: BackboneMLPHeadConfig):
 
 
130
  super().__init__(config)
131
 
 
 
 
 
 
132
  if config.backbone_name_or_path is None:
133
- raise ValueError("config.backbone_name_or_path is None. Provide a valid backbone id.")
 
 
 
134
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  self._meta = BACKBONE_META[config.backbone_name_or_path]
136
 
137
- # (์ค‘์š”) backbone์€ ํ•ญ์ƒ skeleton๋งŒ ์ƒ์„ฑ
 
138
  self.backbone = self._build_backbone_skeleton(config.backbone_name_or_path)
139
 
140
- # head
 
141
  self.classifier = MLPHead(
142
  in_dim=int(self._meta["feat_dim"]),
143
- num_labels=config.num_labels,
144
- bottleneck=config.mlp_head_bottleneck,
145
- p=config.mlp_head_dropout,
146
  )
147
 
148
- # HF init hook (init_weights๋Š” head-only๋กœ override๋จ)
 
149
  self.post_init()
150
 
151
  def init_weights(self):
152
  """
153
- HF์˜ ๊ธฐ๋ณธ init_weights()๋Š” ๋ชจ๋ธ ์ „์ฒด์— apply(...)๋ฅผ ๊ฑธ ์ˆ˜ ์žˆ์œผ๋‹ˆ,
154
- head๋งŒ ์ดˆ๊ธฐํ™”ํ•œ๋‹ค.
 
 
 
155
  """
156
  if getattr(self, "classifier", None) is not None:
157
  self.classifier.apply(self._init_weights)
@@ -159,8 +100,11 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
159
 
160
  # ----------------------------
161
  # backbone skeleton builders
 
162
  # ----------------------------
163
  def _build_backbone_skeleton(self, backbone_id: str) -> nn.Module:
 
 
164
  meta = BACKBONE_META[backbone_id]
165
  t = meta["type"]
166
 
@@ -170,18 +114,24 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
170
  if t == "torchvision_densenet":
171
  return self._build_torchvision_densenet_skeleton(backbone_id)
172
 
173
- # transformers: config๋กœ ๊ตฌ์กฐ๋งŒ ์ƒ์„ฑ
 
174
  bb_cfg = AutoConfig.from_pretrained(backbone_id)
175
  return AutoModel.from_config(bb_cfg)
176
 
177
  @staticmethod
178
  def _build_timm_densenet_skeleton(hf_repo_id: str) -> nn.Module:
 
 
179
  try:
180
  import timm
181
  except Exception as e:
182
- raise ImportError("DenseNet(timm) backbone requires `timm`. Install: pip install timm") from e
 
 
183
 
184
- # ๊ตฌ์กฐ๋งŒ (pretrained=False)
 
185
  return timm.create_model(
186
  f"hf_hub:{hf_repo_id}",
187
  pretrained=False,
@@ -190,33 +140,33 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
190
 
191
  @staticmethod
192
  def _build_torchvision_densenet_skeleton(model_id: str) -> nn.Module:
193
- mapping = {
194
- "torchvision/densenet121": "densenet121",
195
- "torchvision/densenet169": "densenet169",
196
- "torchvision/densenet201": "densenet201",
197
- "torchvision/densenet161": "densenet161",
198
- }
199
- if model_id not in mapping:
200
- raise ValueError(f"Unsupported torchvision DenseNet id: {model_id}")
201
-
202
- arch = mapping[model_id]
203
- fn = getattr(tv_models, arch)
204
-
205
- # ๊ตฌ์กฐ๋งŒ (weights=None)
206
- m = fn(weights=None).eval()
207
  return m
208
 
209
  # ------------------------------------------------------------
210
- # pretrained ๋กœ๋”ฉ์€ "๋ช…์‹œ์  ํ˜ธ์ถœ"๋กœ๋งŒ ์ˆ˜ํ–‰ (fresh-start ์ „์šฉ)
 
211
  # ------------------------------------------------------------
212
  @torch.no_grad()
213
- def load_backbone_pretrained_(self, *, low_cpu_mem_usage: bool = False, device_map=None):
 
 
 
 
 
214
  """
215
- fresh-start ์ „์šฉ:
216
- model = BackboneWithMLPHeadForImageClassification(cfg)
217
- model.load_backbone_pretrained_()
218
 
219
- checkpoint reload(from_pretrained)์—์„œ๋Š” ํ˜ธ์ถœํ•˜์ง€ ๋งˆ์‹ญ์‹œ์˜ค.
 
220
  """
221
  bb = self.config.backbone_name_or_path
222
  meta = BACKBONE_META[bb]
@@ -230,48 +180,58 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
230
  self._load_torchvision_pretrained_into_skeleton_(bb)
231
  return
232
 
233
- # transformers pretrained -> skeleton์— ์ฃผ์ž…
 
234
  ref = AutoModel.from_pretrained(
235
  bb,
236
  low_cpu_mem_usage=low_cpu_mem_usage,
237
  device_map=device_map,
238
  )
 
 
 
239
  self.backbone.load_state_dict(ref.state_dict(), strict=False)
240
  del ref
241
 
242
  @torch.no_grad()
243
  def _load_timm_pretrained_into_skeleton_(self, hf_repo_id: str):
 
 
244
  import timm
 
 
 
245
  ref = timm.create_model(
246
  f"hf_hub:{hf_repo_id}",
247
  pretrained=True,
248
  num_classes=0,
249
  ).eval()
 
250
  self.backbone.load_state_dict(ref.state_dict(), strict=True)
251
  del ref
252
 
253
  @torch.no_grad()
254
  def _load_torchvision_pretrained_into_skeleton_(self, model_id: str):
255
- mapping = {
256
- "torchvision/densenet121": ("densenet121", tv_models.DenseNet121_Weights.DEFAULT),
257
- "torchvision/densenet169": ("densenet169", tv_models.DenseNet169_Weights.DEFAULT),
258
- "torchvision/densenet201": ("densenet201", tv_models.DenseNet201_Weights.DEFAULT),
259
- "torchvision/densenet161": ("densenet161", tv_models.DenseNet161_Weights.DEFAULT),
260
- }
261
- if model_id not in mapping:
262
- raise ValueError(f"Unsupported torchvision DenseNet id: {model_id}")
263
-
264
- arch, weights = mapping[model_id]
265
- fn = getattr(tv_models, arch)
266
- ref = fn(weights=weights).eval()
267
  self.backbone.load_state_dict(ref.state_dict(), strict=True)
268
  del ref
269
 
270
  # ----------------------------
271
  # feature extraction
 
272
  # ----------------------------
273
  @staticmethod
274
  def _pool_or_gap(outputs) -> torch.Tensor:
 
 
275
  if hasattr(outputs, "pooler_output") and outputs.pooler_output is not None:
276
  x = outputs.pooler_output
277
  if x.dim() == 2:
@@ -280,29 +240,42 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
280
  return x.flatten(1)
281
  raise RuntimeError(f"Unexpected pooler_output shape: {tuple(x.shape)}")
282
 
 
 
283
  x = outputs.last_hidden_state
284
  if x.dim() == 4:
285
  return x.mean(dim=(2, 3))
 
286
  raise RuntimeError(
287
  "Expected pooler_output or (B,C,H,W) last_hidden_state for CNN backbones. "
288
  f"Got last_hidden_state shape={tuple(x.shape)}"
289
  )
290
 
291
  def _extract_features(self, outputs, pixel_values: Optional[torch.Tensor] = None) -> torch.Tensor:
 
 
292
  rule = self._meta["feat_rule"]
293
 
294
  if rule == "cls":
 
 
295
  return outputs.last_hidden_state[:, 0, :]
296
 
297
  if rule == "pool_or_mean":
 
 
298
  if hasattr(outputs, "pooler_output") and outputs.pooler_output is not None:
299
  return outputs.pooler_output
300
  return outputs.last_hidden_state.mean(dim=1)
301
 
302
  if rule == "pool_or_gap":
 
 
303
  return self._pool_or_gap(outputs)
304
 
305
  if rule == "timm_gap":
 
 
306
  if not isinstance(outputs, torch.Tensor):
307
  raise TypeError(f"timm_gap expects Tensor features, got {type(outputs)}")
308
  if outputs.dim() != 4:
@@ -310,6 +283,8 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
310
  return outputs.mean(dim=(2, 3))
311
 
312
  if rule == "torchvision_densenet_gap":
 
 
313
  if not isinstance(outputs, torch.Tensor):
314
  raise TypeError(f"torchvision_densenet_gap expects Tensor, got {type(outputs)}")
315
  if outputs.dim() != 4:
@@ -327,9 +302,13 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
327
  return_dict=True,
328
  **kwargs,
329
  ):
 
 
330
  t = self._meta["type"]
331
 
332
  if t == "timm_densenet":
 
 
333
  if pixel_values is None:
334
  raise ValueError("timm DenseNet backbone requires pixel_values.")
335
  if pixel_values.dim() != 4:
@@ -341,6 +320,8 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
341
  attentions = None
342
 
343
  elif t == "torchvision_densenet":
 
 
344
  if pixel_values is None:
345
  raise ValueError("torchvision DenseNet backbone requires pixel_values.")
346
  if pixel_values.dim() != 4:
@@ -353,6 +334,8 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
353
  attentions = None
354
 
355
  else:
 
 
356
  outputs = self.backbone(
357
  pixel_values=pixel_values,
358
  output_attentions=output_attentions,
@@ -364,10 +347,14 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
364
  hidden_states = getattr(outputs, "hidden_states", None)
365
  attentions = getattr(outputs, "attentions", None)
366
 
 
 
367
  logits = self.classifier(feats)
368
 
369
  loss = None
370
  if labels is not None:
 
 
371
  loss = F.cross_entropy(logits, labels)
372
 
373
  if not return_dict:
@@ -383,20 +370,27 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
383
 
384
 
385
  # ============================================================
386
- # Freeze / Unfreeze utilities (๋‹น์‹  ์ฝ”๋“œ ์œ ์ง€)
 
387
  # ============================================================
388
  def _set_requires_grad(module: nn.Module, flag: bool):
 
 
389
  for p in module.parameters():
390
  p.requires_grad = flag
391
 
392
 
393
  def set_bn_eval(module: nn.Module):
 
 
394
  for m in module.modules():
395
  if isinstance(m, (nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d, nn.SyncBatchNorm)):
396
  m.eval()
397
 
398
 
399
  def freeze_backbone(model: BackboneWithMLPHeadForImageClassification, freeze_bn: bool = True):
 
 
400
  _set_requires_grad(model.backbone, False)
401
  _set_requires_grad(model.classifier, True)
402
 
@@ -406,6 +400,8 @@ def freeze_backbone(model: BackboneWithMLPHeadForImageClassification, freeze_bn:
406
 
407
 
408
  def finetune_train_mode(model: BackboneWithMLPHeadForImageClassification, keep_bn_eval: bool = True):
 
 
409
  model.train()
410
  meta = BACKBONE_META[model.config.backbone_name_or_path]
411
  if keep_bn_eval and meta.get("has_bn", False):
@@ -413,6 +409,8 @@ def finetune_train_mode(model: BackboneWithMLPHeadForImageClassification, keep_b
413
 
414
 
415
  def trainable_summary(model: nn.Module):
 
 
416
  total = sum(p.numel() for p in model.parameters())
417
  trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
418
  ratio = trainable / total if total > 0 else 0.0
@@ -425,6 +423,8 @@ def unfreeze_last_stage(
425
  last_n: int = 2,
426
  keep_bn_eval: bool = True,
427
  ):
 
 
428
  freeze_backbone(model, freeze_bn=keep_bn_eval)
429
 
430
  n = int(last_n)
@@ -438,12 +438,16 @@ def unfreeze_last_stage(
438
  bb_type = meta["type"]
439
 
440
  if bb_type == "vit":
 
 
441
  blocks = list(model.backbone.encoder.layer)
442
  for blk in blocks[-n:]:
443
  _set_requires_grad(blk, True)
444
  return
445
 
446
  if bb_type == "swin":
 
 
447
  stages = list(model.backbone.encoder.layers)
448
  blocks: List[nn.Module] = []
449
  for st in stages:
@@ -453,6 +457,8 @@ def unfreeze_last_stage(
453
  return
454
 
455
  if bb_type == "resnet":
 
 
456
  bb = model.backbone
457
  for name in ("layer1", "layer2", "layer3", "layer4"):
458
  if not hasattr(bb, name):
@@ -472,6 +478,8 @@ def unfreeze_last_stage(
472
  return
473
 
474
  if bb_type == "efficientnet":
 
 
475
  bb = model.backbone
476
  if not hasattr(bb, "features"):
477
  raise RuntimeError("Unexpected EfficientNet structure: missing features")
@@ -488,6 +496,8 @@ def unfreeze_last_stage(
488
  return
489
 
490
  if bb_type in ("timm_densenet", "torchvision_densenet"):
 
 
491
  bb = model.backbone
492
  if not hasattr(bb, "features"):
493
  raise RuntimeError("Unexpected DenseNet: missing features")
@@ -505,6 +515,8 @@ def unfreeze_last_stage(
505
  raise RuntimeError(f"Unexpected DenseNet features: missing {name}")
506
 
507
  def _denselayers(db: nn.Module) -> List[nn.Module]:
 
 
508
  return list(db.children())
509
 
510
  blocks: List[nn.Module] = []
@@ -526,20 +538,8 @@ def unfreeze_last_stage(
526
 
527
  # -------------------------
528
  # register
 
 
 
 
529
  BackboneWithMLPHeadForImageClassification.register_for_auto_class("AutoModelForImageClassification")
530
-
531
-
532
- # ==========================================================
533
-
534
- # # fresh-start
535
- # cfg = BackboneMLPHeadConfig(backbone_name_or_path="timm/densenet121.tv_in1k", num_labels=3)
536
- # model = BackboneWithMLPHeadForImageClassification(cfg)
537
-
538
- # # pretrained๋ฅผ ์“ฐ๋ ค๋ฉด ๋ช…์‹œ์ ์œผ๋กœ ํ˜ธ์ถœ
539
- # model.load_backbone_pretrained_()
540
-
541
- # # ์ดํ›„ ํ•™์Šต
542
-
543
- # # checkpoint์—์„œ ์‹œ์ž‘
544
- # model = BackboneWithMLPHeadForImageClassification.from_pretrained("checkpoints/xxx")
545
- # # ์—ฌ๊ธฐ์„œ๋Š” load_backbone_pretrained_() ํ˜ธ์ถœํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
 
1
  #!/usr/bin/env python3
2
  # -*- coding: utf-8 -*-
3
 
4
+ # src/ds_model.py
5
 
6
+ from typing import Optional, List
 
7
 
8
  import torch
9
  import torch.nn as nn
 
11
 
12
  # --- transformers core ---
13
  from transformers.modeling_utils import PreTrainedModel
14
+ from transformers import AutoModel, AutoConfig
15
  from transformers.modeling_outputs import ImageClassifierOutput
16
 
17
  # --- torchvision ---
18
  from torchvision import models as tv_models
19
 
20
+ from ds_cfg import BackboneMLPHeadConfig
21
+ from ds_meta import BACKBONE_META
22
+ from mlp_head import MLPHead
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  # ============================================================
25
  # (3) Model: backbone + MLP head
26
+ # (3) ๋ชจ๋ธ: backbone + MLP head
 
 
 
27
  # ============================================================
28
+ # Design principle: __init__ builds only a skeleton and MUST NOT load pretrained weights.
29
+ # ์„ค๊ณ„ ์›์น™: __init__์—์„œ๋Š” skeleton๋งŒ ๋งŒ๋“ค๊ณ  pretrained weight ๋กœ๋“œ๋Š” ์ ˆ๋Œ€ ํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
30
+ #
31
+ # Pretrained injection is allowed ONLY via an explicit call in fresh-start flows.
32
+ # pretrained ์ฃผ์ž…์€ fresh-start ํ๋ฆ„์—์„œ์˜ ๋ช…์‹œ์  ํ˜ธ์ถœ๋กœ๋งŒ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.
33
+ #
34
+ # HF from_pretrained should restore checkpoints as-is without side effects.
35
+ # HF from_pretrained๋Š” ๋ถ€์ž‘์šฉ ์—†์ด ์ฒดํฌํฌ์ธํŠธ๋ฅผ ๊ทธ๋Œ€๋กœ ๋ณต์›ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
36
  class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
37
+ # This links the model to its custom config for AutoClass usage.
38
+ # AutoClass ์‚ฌ์šฉ์„ ์œ„ํ•ด ๋ชจ๋ธ๊ณผ ์ปค์Šคํ…€ config๋ฅผ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค.
39
  config_class = BackboneMLPHeadConfig
40
 
41
  def __init__(self, config: BackboneMLPHeadConfig):
42
+ # PreTrainedModel expects a config object and stores it internally.
43
+ # PreTrainedModel์€ config ๊ฐ์ฒด๋ฅผ ๋ฐ›์•„ ๋‚ด๋ถ€์— ์ €์žฅํ•˜๋Š” ์ „์ œ๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค.
44
  super().__init__(config)
45
 
46
+ # Fail-fast: the model is not meant to be instantiated without a valid backbone id.
47
+ # fail-fast: ์œ ํšจํ•œ backbone id ์—†์ด ๋ชจ๋ธ์„ ๋งŒ๋“œ๋Š” ์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค๋Š” ํ—ˆ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
48
+ #
49
+ # Note: Transformers may create configs with no args, but models are conventionally created with configs.
50
+ # ์ฐธ๊ณ : Transformers๋Š” config ๋ฌด์ธ์ž ์ƒ์„ฑ์ด ์žˆ์„ ์ˆ˜ ์žˆ์œผ๋‚˜, ๋ชจ๋ธ์€ ๊ด€๋ก€์ ์œผ๋กœ config๋ฅผ ๋ฐ›์•„ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.
51
  if config.backbone_name_or_path is None:
52
+ raise ValueError(
53
+ "config.backbone_name_or_path is None. "
54
+ "Provide a valid backbone id (whitelist key in BACKBONE_META)."
55
+ )
56
 
57
+ # Fail-fast: training/inference requires a positive number of labels.
58
+ # fail-fast: ํ•™์Šต/์ถ”๋ก ์€ num_labels๊ฐ€ ์–‘์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.
59
+ #
60
+ # Config may exist in a minimal form for internal serialization paths, but the model should not.
61
+ # config๋Š” ๋‚ด๋ถ€ ์ง๋ ฌํ™” ๊ฒฝ๋กœ์—์„œ ์ตœ์†Œ ํ˜•ํƒœ๋กœ ์กด์žฌํ•  ์ˆ˜ ์žˆ์œผ๋‚˜ ๋ชจ๋ธ์€ ๊ทธ๋Ÿฌ๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
62
+ if int(getattr(config, "num_labels", 0)) <= 0:
63
+ raise ValueError(
64
+ f"config.num_labels must be > 0, got {getattr(config, 'num_labels', None)}. "
65
+ "Set num_labels (or id2label/label2id) when creating the config."
66
+ )
67
+
68
+ # Meta is a single source of truth for extraction and fine-tuning rules.
69
+ # meta๋Š” feature ์ถ”์ถœ ๋ฐ ๋ฏธ์„ธ์กฐ์ • ๊ทœ์น™์˜ ๋‹จ์ผ ๊ธฐ์ค€์ž…๋‹ˆ๋‹ค.
70
  self._meta = BACKBONE_META[config.backbone_name_or_path]
71
 
72
+ # Backbone skeleton is always created without pretrained weights.
73
+ # backbone skeleton์€ ํ•ญ์ƒ pretrained weight ์—†์ด ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
74
  self.backbone = self._build_backbone_skeleton(config.backbone_name_or_path)
75
 
76
+ # Head shape is driven by meta feat_dim and config.num_labels.
77
+ # head shape์€ meta์˜ feat_dim๊ณผ config.num_labels๋กœ ๊ฒฐ์ •๋ฉ๋‹ˆ๋‹ค.
78
  self.classifier = MLPHead(
79
  in_dim=int(self._meta["feat_dim"]),
80
+ num_labels=int(config.num_labels),
81
+ bottleneck=int(config.mlp_head_bottleneck),
82
+ p=float(config.mlp_head_dropout),
83
  )
84
 
85
+ # HF initialization hook, but we override init_weights to initialize head-only.
86
+ # HF ์ดˆ๊ธฐํ™” ํ›…์ด์ง€๋งŒ init_weights๋ฅผ overrideํ•˜์—ฌ head๋งŒ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
87
  self.post_init()
88
 
89
  def init_weights(self):
90
  """
91
+ Initialize only the head to avoid touching the backbone skeleton.
92
+ backbone skeleton์„ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๊ธฐ ์œ„ํ•ด head๋งŒ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
93
+
94
+ HF's default init may traverse the entire module tree, which is undesirable here.
95
+ HF ๊ธฐ๋ณธ init์€ ์ „์ฒด ๋ชจ๋“ˆ ํŠธ๋ฆฌ๋ฅผ ์ˆœํšŒํ•  ์ˆ˜ ์žˆ์–ด ์—ฌ๊ธฐ์„œ๋Š” ๋ถ€์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.
96
  """
97
  if getattr(self, "classifier", None) is not None:
98
  self.classifier.apply(self._init_weights)
 
100
 
101
  # ----------------------------
102
  # backbone skeleton builders
103
+ # backbone skeleton ์ƒ์„ฑ๊ธฐ
104
  # ----------------------------
105
  def _build_backbone_skeleton(self, backbone_id: str) -> nn.Module:
106
+ # Meta decides which loader path to use.
107
+ # meta๊ฐ€ ์–ด๋–ค ๋กœ๋” ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ• ์ง€ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.
108
  meta = BACKBONE_META[backbone_id]
109
  t = meta["type"]
110
 
 
114
  if t == "torchvision_densenet":
115
  return self._build_torchvision_densenet_skeleton(backbone_id)
116
 
117
+ # For transformers backbones: build a random-weight skeleton from config only.
118
+ # transformers ๋ฐฑ๋ณธ: config๋กœ๋ถ€ํ„ฐ ๋žœ๋ค ์ดˆ๊ธฐํ™” skeleton๋งŒ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
119
  bb_cfg = AutoConfig.from_pretrained(backbone_id)
120
  return AutoModel.from_config(bb_cfg)
121
 
122
  @staticmethod
123
  def _build_timm_densenet_skeleton(hf_repo_id: str) -> nn.Module:
124
+ # timm is an optional dependency and should be imported lazily.
125
+ # timm์€ ์˜ต์…˜ ์˜์กด์„ฑ์ด๋ฏ€๋กœ ์ง€์—ฐ import ํ•ฉ๋‹ˆ๋‹ค.
126
  try:
127
  import timm
128
  except Exception as e:
129
+ raise ImportError(
130
+ "DenseNet(timm) backbone requires `timm`. Install: pip install timm"
131
+ ) from e
132
 
133
+ # Build structure only (pretrained=False) and remove classifier head (num_classes=0).
134
+ # ๊ตฌ์กฐ๋งŒ ์ƒ์„ฑ(pretrained=False)ํ•˜๊ณ  ๋ถ„๋ฅ˜๊ธฐ head๋Š” ์ œ๊ฑฐ(num_classes=0)ํ•ฉ๋‹ˆ๋‹ค.
135
  return timm.create_model(
136
  f"hf_hub:{hf_repo_id}",
137
  pretrained=False,
 
140
 
141
  @staticmethod
142
  def _build_torchvision_densenet_skeleton(model_id: str) -> nn.Module:
143
+ # This project intentionally supports only torchvision/densenet121 in the 224 whitelist.
144
+ # ์ด ํ”„๋กœ์ ํŠธ๋Š” 224 ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ์—์„œ torchvision/densenet121๋งŒ ์˜๋„์ ์œผ๋กœ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.
145
+ if model_id != "torchvision/densenet121":
146
+ raise ValueError(f"Unsupported torchvision DenseNet id (224 whitelist only): {model_id}")
147
+
148
+ # Build structure only (weights=None) to avoid implicit pretrained loading.
149
+ # implicit pretrained ๋กœ๋“œ๋ฅผ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด ๊ตฌ์กฐ๋งŒ ์ƒ์„ฑ(weights=None)ํ•ฉ๋‹ˆ๋‹ค.
150
+ m = tv_models.densenet121(weights=None)
 
 
 
 
 
 
151
  return m
152
 
153
  # ------------------------------------------------------------
154
+ # Pretrained loading is explicit and fresh-start only
155
+ # pretrained ๋กœ๋”ฉ์€ ๋ช…์‹œ์  ํ˜ธ์ถœ์ด๋ฉฐ fresh-start ์ „์šฉ
156
  # ------------------------------------------------------------
157
  @torch.no_grad()
158
+ def load_backbone_pretrained_(
159
+ self,
160
+ *,
161
+ low_cpu_mem_usage: bool = False,
162
+ device_map=None,
163
+ ):
164
  """
165
+ Fresh-start only: inject pretrained backbone weights into the skeleton.
166
+ fresh-start ์ „์šฉ: skeleton backbone์— pretrained ๊ฐ€์ค‘์น˜๋ฅผ ์ฃผ์ž…ํ•ฉ๋‹ˆ๋‹ค.
 
167
 
168
+ Do NOT call this after from_pretrained() because it would overwrite checkpoint weights.
169
+ from_pretrained() ์ดํ›„ ํ˜ธ์ถœํ•˜๋ฉด ์ฒดํฌํฌ์ธํŠธ ๊ฐ€์ค‘์น˜๋ฅผ ๋ฎ์–ด์“ฐ๋ฏ€๋กœ ์ ˆ๋Œ€ ํ˜ธ์ถœํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
170
  """
171
  bb = self.config.backbone_name_or_path
172
  meta = BACKBONE_META[bb]
 
180
  self._load_torchvision_pretrained_into_skeleton_(bb)
181
  return
182
 
183
+ # For transformers backbones, load a reference pretrained model and copy weights into our skeleton.
184
+ # transformers ๋ฐฑ๋ณธ์€ reference pretrained ๋ชจ๋ธ์„ ๋กœ๋“œํ•œ ๋’ค skeleton์— ๊ฐ€์ค‘์น˜๋ฅผ ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค.
185
  ref = AutoModel.from_pretrained(
186
  bb,
187
  low_cpu_mem_usage=low_cpu_mem_usage,
188
  device_map=device_map,
189
  )
190
+
191
+ # strict=False is used to tolerate harmless key differences across minor versions.
192
+ # strict=False๋Š” ๋งˆ์ด๋„ˆ ๋ฒ„์ „ ์ฐจ์ด๋กœ ์ธํ•œ ๋ฌดํ•ดํ•œ ํ‚ค ์ฐจ์ด๋ฅผ ํ—ˆ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
193
  self.backbone.load_state_dict(ref.state_dict(), strict=False)
194
  del ref
195
 
196
  @torch.no_grad()
197
  def _load_timm_pretrained_into_skeleton_(self, hf_repo_id: str):
198
+ # timm must be present for timm backbones.
199
+ # timm ๋ฐฑ๋ณธ์—๋Š” timm ์„ค์น˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
200
  import timm
201
+
202
+ # Create a pretrained reference model and copy its weights strictly.
203
+ # pretrained reference ๋ชจ๋ธ์„ ๋งŒ๋“ค๊ณ  ๊ฐ€์ค‘์น˜๋ฅผ strictํ•˜๊ฒŒ ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค.
204
  ref = timm.create_model(
205
  f"hf_hub:{hf_repo_id}",
206
  pretrained=True,
207
  num_classes=0,
208
  ).eval()
209
+
210
  self.backbone.load_state_dict(ref.state_dict(), strict=True)
211
  del ref
212
 
213
  @torch.no_grad()
214
  def _load_torchvision_pretrained_into_skeleton_(self, model_id: str):
215
+ # This project intentionally supports only torchvision/densenet121 in the 224 whitelist.
216
+ # ์ด ํ”„๋กœ์ ํŠธ๋Š” 224 ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ์—์„œ torchvision/densenet121๋งŒ ์˜๋„์ ์œผ๋กœ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.
217
+ if model_id != "torchvision/densenet121":
218
+ raise ValueError(f"Unsupported torchvision DenseNet id (224 whitelist only): {model_id}")
219
+
220
+ # Use torchvision's default pretrained weights for densenet121.
221
+ # torchvision์˜ densenet121 ๊ธฐ๋ณธ pretrained weights๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
222
+ ref = tv_models.densenet121(weights=tv_models.DenseNet121_Weights.DEFAULT).eval()
223
+
 
 
 
224
  self.backbone.load_state_dict(ref.state_dict(), strict=True)
225
  del ref
226
 
227
  # ----------------------------
228
  # feature extraction
229
+ # feature ์ถ”์ถœ
230
  # ----------------------------
231
  @staticmethod
232
  def _pool_or_gap(outputs) -> torch.Tensor:
233
+ # Some transformers vision CNNs provide pooler_output explicitly.
234
+ # ์ผ๋ถ€ transformers vision CNN์€ pooler_output์„ ๋ช…์‹œ์ ์œผ๋กœ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
235
  if hasattr(outputs, "pooler_output") and outputs.pooler_output is not None:
236
  x = outputs.pooler_output
237
  if x.dim() == 2:
 
240
  return x.flatten(1)
241
  raise RuntimeError(f"Unexpected pooler_output shape: {tuple(x.shape)}")
242
 
243
+ # Otherwise we expect a CNN-style last_hidden_state=(B,C,H,W) and apply GAP.
244
+ # ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด CNN ์Šคํƒ€์ผ last_hidden_state=(B,C,H,W)๋ฅผ ๊ธฐ๋Œ€ํ•˜๊ณ  GAP์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.
245
  x = outputs.last_hidden_state
246
  if x.dim() == 4:
247
  return x.mean(dim=(2, 3))
248
+
249
  raise RuntimeError(
250
  "Expected pooler_output or (B,C,H,W) last_hidden_state for CNN backbones. "
251
  f"Got last_hidden_state shape={tuple(x.shape)}"
252
  )
253
 
254
  def _extract_features(self, outputs, pixel_values: Optional[torch.Tensor] = None) -> torch.Tensor:
255
+ # Feature rule is defined by BACKBONE_META and must remain stable across saves/loads.
256
+ # feature ๊ทœ์น™์€ BACKBONE_META๋กœ ์ •์˜๋˜๋ฉฐ ์ €์žฅ/๋กœ๋“œ ๊ฐ„ ์•ˆ์ •์ ์œผ๋กœ ์œ ์ง€๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
257
  rule = self._meta["feat_rule"]
258
 
259
  if rule == "cls":
260
+ # ViT-style: use CLS token embedding from last_hidden_state.
261
+ # ViT ์Šคํƒ€์ผ: last_hidden_state์—์„œ CLS ํ† ํฐ ์ž„๋ฒ ๋”ฉ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
262
  return outputs.last_hidden_state[:, 0, :]
263
 
264
  if rule == "pool_or_mean":
265
+ # Swin-style: prefer pooler_output if present, else mean-pool over tokens.
266
+ # Swin ์Šคํƒ€์ผ: pooler_output์ด ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉํ•˜๊ณ , ์—†์œผ๋ฉด ํ† ํฐ ํ‰๊ท  ํ’€๋ง์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
267
  if hasattr(outputs, "pooler_output") and outputs.pooler_output is not None:
268
  return outputs.pooler_output
269
  return outputs.last_hidden_state.mean(dim=1)
270
 
271
  if rule == "pool_or_gap":
272
+ # CNN-style: use pooler_output if present, else GAP over spatial dims.
273
+ # CNN ์Šคํƒ€์ผ: pooler_output์ด ์žˆ์œผ๋ฉด ์‚ฌ์šฉํ•˜๊ณ , ์—†์œผ๋ฉด ๊ณต๊ฐ„ ์ฐจ์› GAP์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
274
  return self._pool_or_gap(outputs)
275
 
276
  if rule == "timm_gap":
277
+ # timm forward_features returns a feature map (B,C,H,W) which we GAP to (B,C).
278
+ # timm forward_features๋Š” (B,C,H,W) feature map์„ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ ์ด๋ฅผ GAP์œผ๋กœ (B,C)๋กœ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
279
  if not isinstance(outputs, torch.Tensor):
280
  raise TypeError(f"timm_gap expects Tensor features, got {type(outputs)}")
281
  if outputs.dim() != 4:
 
283
  return outputs.mean(dim=(2, 3))
284
 
285
  if rule == "torchvision_densenet_gap":
286
+ # torchvision DenseNet features are feature maps (B,C,H,W) and require GAP.
287
+ # torchvision DenseNet features๋Š” (B,C,H,W) feature map์ด๋ฉฐ GAP์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
288
  if not isinstance(outputs, torch.Tensor):
289
  raise TypeError(f"torchvision_densenet_gap expects Tensor, got {type(outputs)}")
290
  if outputs.dim() != 4:
 
302
  return_dict=True,
303
  **kwargs,
304
  ):
305
+ # Type decides the backbone forward path and output format.
306
+ # type์ด backbone forward ๊ฒฝ๋กœ ๋ฐ ์ถœ๋ ฅ ํฌ๋งท์„ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.
307
  t = self._meta["type"]
308
 
309
  if t == "timm_densenet":
310
+ # timm DenseNet consumes pixel_values as a 4D tensor (B,C,H,W).
311
+ # timm DenseNet์€ pixel_values๋ฅผ 4D ํ…์„œ (B,C,H,W)๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค.
312
  if pixel_values is None:
313
  raise ValueError("timm DenseNet backbone requires pixel_values.")
314
  if pixel_values.dim() != 4:
 
320
  attentions = None
321
 
322
  elif t == "torchvision_densenet":
323
+ # torchvision DenseNet consumes pixel_values as a 4D tensor (B,C,H,W).
324
+ # torchvision DenseNet์€ pixel_values๋ฅผ 4D ํ…์„œ (B,C,H,W)๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค.
325
  if pixel_values is None:
326
  raise ValueError("torchvision DenseNet backbone requires pixel_values.")
327
  if pixel_values.dim() != 4:
 
334
  attentions = None
335
 
336
  else:
337
+ # Transformers vision models are called with pixel_values and return ModelOutput.
338
+ # transformers vision ๋ชจ๋ธ์€ pixel_values๋กœ ํ˜ธ์ถœ๋˜๋ฉฐ ModelOutput์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
339
  outputs = self.backbone(
340
  pixel_values=pixel_values,
341
  output_attentions=output_attentions,
 
347
  hidden_states = getattr(outputs, "hidden_states", None)
348
  attentions = getattr(outputs, "attentions", None)
349
 
350
+ # Classifier consumes (B, feat_dim) and returns logits (B, num_labels).
351
+ # classifier๋Š” (B, feat_dim)์„ ๋ฐ›์•„ logits (B, num_labels)๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
352
  logits = self.classifier(feats)
353
 
354
  loss = None
355
  if labels is not None:
356
+ # Cross entropy expects labels as class indices in [0, num_labels).
357
+ # cross entropy๋Š” labels๊ฐ€ [0, num_labels) ๋ฒ”์œ„์˜ class index์ด๊ธธ ๊ธฐ๋Œ€ํ•ฉ๋‹ˆ๋‹ค.
358
  loss = F.cross_entropy(logits, labels)
359
 
360
  if not return_dict:
 
370
 
371
 
372
  # ============================================================
373
+ # Freeze / Unfreeze utilities
374
+ # Freeze / Unfreeze ์œ ํ‹ธ๋ฆฌํ‹ฐ
375
  # ============================================================
376
  def _set_requires_grad(module: nn.Module, flag: bool):
377
+ # Toggle requires_grad for all parameters in a module.
378
+ # ๋ชจ๋“ˆ์˜ ๋ชจ๋“  ํŒŒ๋ผ๋ฏธํ„ฐ์— ๋Œ€ํ•ด requires_grad๋ฅผ ํ† ๊ธ€ํ•ฉ๋‹ˆ๋‹ค.
379
  for p in module.parameters():
380
  p.requires_grad = flag
381
 
382
 
383
  def set_bn_eval(module: nn.Module):
384
+ # Put BatchNorm layers into eval mode to freeze running stats.
385
+ # BatchNorm ๋ ˆ์ด์–ด๋ฅผ eval ๋ชจ๋“œ๋กœ ๋‘์–ด running stats๋ฅผ ๊ณ ์ •ํ•ฉ๋‹ˆ๋‹ค.
386
  for m in module.modules():
387
  if isinstance(m, (nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d, nn.SyncBatchNorm)):
388
  m.eval()
389
 
390
 
391
  def freeze_backbone(model: BackboneWithMLPHeadForImageClassification, freeze_bn: bool = True):
392
+ # Stage1: freeze backbone and train only the head.
393
+ # stage1: backbone์„ freezeํ•˜๊ณ  head๋งŒ ํ•™์Šตํ•ฉ๋‹ˆ๋‹ค.
394
  _set_requires_grad(model.backbone, False)
395
  _set_requires_grad(model.classifier, True)
396
 
 
400
 
401
 
402
  def finetune_train_mode(model: BackboneWithMLPHeadForImageClassification, keep_bn_eval: bool = True):
403
+ # Stage2: train mode, optionally keeping BN layers in eval for stability.
404
+ # stage2: train ๋ชจ๋“œ๋กœ ๋‘๋˜ ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด BN์„ eval๋กœ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
405
  model.train()
406
  meta = BACKBONE_META[model.config.backbone_name_or_path]
407
  if keep_bn_eval and meta.get("has_bn", False):
 
409
 
410
 
411
  def trainable_summary(model: nn.Module):
412
+ # Print a compact summary of trainable parameters.
413
+ # ํ•™์Šต ๊ฐ€๋Šฅ ํŒŒ๋ผ๋ฏธํ„ฐ ์š”์•ฝ์„ ๊ฐ„๋‹จํžˆ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.
414
  total = sum(p.numel() for p in model.parameters())
415
  trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
416
  ratio = trainable / total if total > 0 else 0.0
 
423
  last_n: int = 2,
424
  keep_bn_eval: bool = True,
425
  ):
426
+ # This utility implements BACKBONE_META['unfreeze']=="last_n" across supported backbones.
427
+ # ์ด ์œ ํ‹ธ์€ ์ง€์› ๋ฐฑ๋ณธ๋“ค์— ๋Œ€ํ•ด BACKBONE_META['unfreeze']=="last_n"์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
428
  freeze_backbone(model, freeze_bn=keep_bn_eval)
429
 
430
  n = int(last_n)
 
438
  bb_type = meta["type"]
439
 
440
  if bb_type == "vit":
441
+ # ViT blocks live under backbone.encoder.layer in the transformers implementation.
442
+ # ViT ๋ธ”๋ก์€ transformers ๊ตฌํ˜„์—์„œ backbone.encoder.layer ์•„๋ž˜์— ์žˆ์Šต๋‹ˆ๋‹ค.
443
  blocks = list(model.backbone.encoder.layer)
444
  for blk in blocks[-n:]:
445
  _set_requires_grad(blk, True)
446
  return
447
 
448
  if bb_type == "swin":
449
+ # Swin blocks are nested by stages and blocks; we flatten and unfreeze last n blocks.
450
+ # Swin ๋ธ”๋ก์€ stage์™€ block์œผ๋กœ ์ค‘์ฒฉ๋˜์–ด ์žˆ์–ด ํŽผ์นœ ํ›„ ๋งˆ์ง€๋ง‰ n๊ฐœ๋ฅผ unfreeze ํ•ฉ๋‹ˆ๋‹ค.
451
  stages = list(model.backbone.encoder.layers)
452
  blocks: List[nn.Module] = []
453
  for st in stages:
 
457
  return
458
 
459
  if bb_type == "resnet":
460
+ # ResNet uses layer1..layer4 stages; we unfreeze at block granularity.
461
+ # ResNet์€ layer1..layer4 stage๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ block ๋‹จ์œ„๋กœ unfreeze ํ•ฉ๋‹ˆ๋‹ค.
462
  bb = model.backbone
463
  for name in ("layer1", "layer2", "layer3", "layer4"):
464
  if not hasattr(bb, name):
 
478
  return
479
 
480
  if bb_type == "efficientnet":
481
+ # EfficientNet in transformers exposes features; we unfreeze from the tail blocks.
482
+ # transformers EfficientNet์€ features๋ฅผ ๋…ธ์ถœํ•˜๋ฉฐ ๋’ค์ชฝ ๋ธ”๋ก๋ถ€ํ„ฐ unfreeze ํ•ฉ๋‹ˆ๋‹ค.
483
  bb = model.backbone
484
  if not hasattr(bb, "features"):
485
  raise RuntimeError("Unexpected EfficientNet structure: missing features")
 
496
  return
497
 
498
  if bb_type in ("timm_densenet", "torchvision_densenet"):
499
+ # DenseNet exposes a .features module with named blocks; we unfreeze last n submodules.
500
+ # DenseNet์€ .features ๋ชจ๋“ˆ์— ๋ธ”๋ก๋“ค์ด ์ด๋ฆ„์œผ๋กœ ์กด์žฌํ•˜๋ฉฐ ๋งˆ์ง€๋ง‰ n๊ฐœ ์„œ๋ธŒ๋ชจ๋“ˆ์„ unfreeze ํ•ฉ๋‹ˆ๋‹ค.
501
  bb = model.backbone
502
  if not hasattr(bb, "features"):
503
  raise RuntimeError("Unexpected DenseNet: missing features")
 
515
  raise RuntimeError(f"Unexpected DenseNet features: missing {name}")
516
 
517
  def _denselayers(db: nn.Module) -> List[nn.Module]:
518
+ # Dense blocks contain multiple DenseLayer children; we return them for fine-grained unfreezing.
519
+ # denseblock์€ DenseLayer ์ž์‹๋“ค์„ ๊ฐ€์ง€๋ฏ€๋กœ ์„ธ๋ฐ€ํ•œ unfreeze๋ฅผ ์œ„ํ•ด ์ด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
520
  return list(db.children())
521
 
522
  blocks: List[nn.Module] = []
 
538
 
539
  # -------------------------
540
  # register
541
+ # register
542
+ # -------------------------
543
+ # Register for AutoModelForImageClassification so from_pretrained can resolve this custom class.
544
+ # from_pretrained๊ฐ€ ์ด ์ปค์Šคํ…€ ํด๋ž˜์Šค๋ฅผ ํ•ด์„ํ•  ์ˆ˜ ์žˆ๋„๋ก AutoModelForImageClassification์— ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.
545
  BackboneWithMLPHeadForImageClassification.register_for_auto_class("AutoModelForImageClassification")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
models/google__efficientnet-b0/ds_proc.py ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # src/ds_proc.py
5
+
6
+ # ============================================================
7
+ # (4) ImageProcessor (AutoImageProcessor integration)
8
+ # (4) ImageProcessor (AutoImageProcessor ์—ฐ๋™)
9
+ # ============================================================
10
+
11
+ from typing import Any
12
+
13
+ import numpy as np
14
+ import torch
15
+ from transformers import AutoImageProcessor, AutoConfig
16
+ from transformers.image_processing_base import ImageProcessingMixin
17
+ from transformers.utils.generic import TensorType
18
+
19
+ from ds_meta import BackboneID, BACKBONE_META
20
+
21
+
22
+ class BackboneMLPHead224ImageProcessor(ImageProcessingMixin):
23
+ """
24
+ This processor performs image preprocessing and outputs {"pixel_values": ...}.
25
+ ์ด processor๋Š” ์ด๋ฏธ์ง€ ์ „์ฒ˜๋ฆฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ณ  {"pixel_values": ...}๋ฅผ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.
26
+
27
+ Key requirements:
28
+ ํ•ต์‹ฌ ์š”๊ตฌ์‚ฌํ•ญ:
29
+
30
+ 1) save_pretrained() must produce a JSON-serializable preprocessor_config.json.
31
+ save_pretrained()๋Š” JSON ์ง๋ ฌํ™” ๊ฐ€๋Šฅํ•œ preprocessor_config.json์„ ์ƒ์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
32
+
33
+ 2) Runtime-only objects (delegate processor, timm/torchvision transforms) must NOT be serialized.
34
+ ๋Ÿฐํƒ€์ž„ ๊ฐ์ฒด(delegate processor, timm/torchvision transform)๋Š” ์ ˆ๋Œ€ ์ง๋ ฌํ™”ํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
35
+
36
+ 3) Runtime objects are rebuilt at init/load time based on backbone meta.
37
+ ๋Ÿฐํƒ€์ž„ ๊ฐ์ฒด๋Š” backbone meta์— ๋”ฐ๋ผ init/load ์‹œ์ ์— ์žฌ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค.
38
+ """
39
+
40
+ # HF vision models conventionally expect "pixel_values" as the primary input key.
41
+ # HF vision ๋ชจ๋ธ์€ ๊ด€๋ก€์ ์œผ๋กœ ์ž…๋ ฅ ํ‚ค๋กœ "pixel_values"๋ฅผ ๊ธฐ๋Œ€ํ•ฉ๋‹ˆ๋‹ค.
42
+ model_input_names = ["pixel_values"]
43
+
44
+ def __init__(self, backbone_name_or_path: BackboneID, is_training: bool = False, **kwargs):
45
+ # ImageProcessingMixin stores extra kwargs and manages auto_map metadata.
46
+ # ImageProcessingMixin์€ ์ถ”๊ฐ€ kwargs๋ฅผ ์ €์žฅํ•˜๊ณ  auto_map ๋ฉ”ํƒ€๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
47
+ super().__init__(**kwargs)
48
+
49
+ # Enforce whitelist via BACKBONE_META to keep behavior stable.
50
+ # ๋™์ž‘ ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด BACKBONE_META ๊ธฐ๋ฐ˜ ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค.
51
+ if backbone_name_or_path not in BACKBONE_META:
52
+ raise ValueError(
53
+ f"Unsupported backbone_name_or_path={backbone_name_or_path}. "
54
+ f"Allowed: {sorted(BACKBONE_META.keys())}"
55
+ )
56
+
57
+ # Serializable fields only: these should appear in preprocessor_config.json.
58
+ # ์ง๋ ฌํ™” ๊ฐ€๋Šฅํ•œ ํ•„๋“œ๋งŒ: ์ด ๊ฐ’๋“ค๋งŒ preprocessor_config.json์— ๋“ค์–ด๊ฐ€์•ผ ํ•ฉ๋‹ˆ๋‹ค.
59
+ self.backbone_name_or_path = backbone_name_or_path
60
+ self.is_training = bool(is_training)
61
+
62
+ # Runtime-only fields: must never be serialized.
63
+ # ๋Ÿฐํƒ€์ž„ ์ „์šฉ ํ•„๋“œ: ์ ˆ๋Œ€ ์ง๋ ฌํ™”๋˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
64
+ self._meta = None
65
+ self._delegate = None
66
+ self._timm_transform = None
67
+ self._torchvision_transform = None
68
+
69
+ # Build runtime objects according to backbone type.
70
+ # backbone type์— ๋”ฐ๋ผ ๋Ÿฐํƒ€์ž„ ๊ฐ์ฒด๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
71
+ self._build_runtime()
72
+
73
+ # ============================================================
74
+ # Runtime builders
75
+ # ๋Ÿฐํƒ€์ž„ ๋นŒ๋”
76
+ # ============================================================
77
+ def _build_runtime(self):
78
+ """
79
+ Build runtime delegate/transform based on BACKBONE_META["type"].
80
+ BACKBONE_META["type"]์— ๋”ฐ๋ผ ๋Ÿฐํƒ€์ž„ delegate/transform์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
81
+ """
82
+ meta = BACKBONE_META[self.backbone_name_or_path]
83
+ self._meta = meta
84
+
85
+ # Always reset runtime fields before rebuilding.
86
+ # ์žฌ๊ตฌ์„ฑ ์ „ ๋Ÿฐํƒ€์ž„ ํ•„๋“œ๋Š” ํ•ญ์ƒ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
87
+ self._delegate = None
88
+ self._timm_transform = None
89
+ self._torchvision_transform = None
90
+
91
+ t = meta["type"]
92
+
93
+ if t == "timm_densenet":
94
+ # timm DenseNet uses timm.data transforms for ImageNet-style preprocessing.
95
+ # timm DenseNet์€ ImageNet ์ „์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด timm.data transform์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
96
+ self._timm_transform = self._build_timm_transform(
97
+ backbone_id=self.backbone_name_or_path,
98
+ is_training=self.is_training,
99
+ )
100
+ return
101
+
102
+ if t == "torchvision_densenet":
103
+ # torchvision DenseNet requires torchvision-style preprocessing (resize/crop/tensor/normalize).
104
+ # torchvision DenseNet์€ torchvision ์Šคํƒ€์ผ ์ „์ฒ˜๋ฆฌ(resize/crop/tensor/normalize)๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
105
+ self._torchvision_transform = self._build_torchvision_densenet_transform(
106
+ is_training=self.is_training
107
+ )
108
+ return
109
+
110
+ # Default: transformers backbone delegates to its official AutoImageProcessor.
111
+ # ๊ธฐ๋ณธ: transformers ๋ฐฑ๋ณธ์€ ๊ณต์‹ AutoImageProcessor์— ์œ„์ž„ํ•ฉ๋‹ˆ๋‹ค.
112
+ self._delegate = AutoImageProcessor.from_pretrained(self.backbone_name_or_path)
113
+
114
+ @staticmethod
115
+ def _build_timm_transform(*, backbone_id: str, is_training: bool):
116
+ """
117
+ Create timm transform without storing non-serializable objects in config.
118
+ ๋น„์ง๋ ฌํ™” ๊ฐ์ฒด๋ฅผ config์— ์ €์žฅํ•˜์ง€ ์•Š๊ณ  timm transform์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
119
+ """
120
+ try:
121
+ import timm
122
+ from timm.data import resolve_model_data_config, create_transform
123
+ except Exception as e:
124
+ raise ImportError(
125
+ "timm backbone processor requires `timm`. Install: pip install timm"
126
+ ) from e
127
+
128
+ # We only need model metadata to resolve data config, so pretrained=False is preferred.
129
+ # data config ์ถ”์ถœ๋งŒ ํ•„์š”ํ•˜๋ฏ€๋กœ pretrained=False๋ฅผ ์šฐ์„  ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
130
+ m = timm.create_model(f"hf_hub:{backbone_id}", pretrained=False, num_classes=0)
131
+ dc = resolve_model_data_config(m)
132
+
133
+ # create_transform returns a torchvision-like callable that maps PIL -> torch.Tensor(C,H,W).
134
+ # create_transform์€ PIL -> torch.Tensor(C,H,W)๋กœ ๋งคํ•‘ํ•˜๋Š” callable์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
135
+ tfm = create_transform(**dc, is_training=is_training)
136
+ return tfm
137
+
138
+ @staticmethod
139
+ def _build_torchvision_densenet_transform(*, is_training: bool):
140
+ """
141
+ Build torchvision preprocessing for DenseNet-121 (224 pipeline).
142
+ DenseNet-121์šฉ torchvision ์ „์ฒ˜๋ฆฌ(224 ํŒŒ์ดํ”„๋ผ์ธ)๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
143
+ """
144
+ try:
145
+ from torchvision import transforms
146
+ except Exception as e:
147
+ raise ImportError(
148
+ "torchvision DenseNet processor requires `torchvision`. Install: pip install torchvision"
149
+ ) from e
150
+
151
+ # These are the standard ImageNet normalization stats used by torchvision weights.
152
+ # ์ด ๊ฐ’๋“ค์€ torchvision weights๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” ํ‘œ์ค€ ImageNet ์ •๊ทœํ™” ํ†ต๊ณ„์ž…๋‹ˆ๋‹ค.
153
+ mean = (0.485, 0.456, 0.406)
154
+ std = (0.229, 0.224, 0.225)
155
+
156
+ # Training pipeline typically uses RandomResizedCrop and horizontal flip.
157
+ # ํ•™์Šต ํŒŒ์ดํ”„๋ผ์ธ์€ ๋ณดํ†ต RandomResizedCrop๊ณผ ์ขŒ์šฐ๋ฐ˜์ „์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
158
+ if is_training:
159
+ return transforms.Compose(
160
+ [
161
+ transforms.RandomResizedCrop(224),
162
+ transforms.RandomHorizontalFlip(p=0.5),
163
+ transforms.ToTensor(),
164
+ transforms.Normalize(mean=mean, std=std),
165
+ ]
166
+ )
167
+
168
+ # Inference pipeline typically uses Resize(256) + CenterCrop(224).
169
+ # ์ถ”๋ก  ํŒŒ์ดํ”„๋ผ์ธ์€ ๋ณดํ†ต Resize(256) + CenterCrop(224)๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
170
+ return transforms.Compose(
171
+ [
172
+ transforms.Resize(256),
173
+ transforms.CenterCrop(224),
174
+ transforms.ToTensor(),
175
+ transforms.Normalize(mean=mean, std=std),
176
+ ]
177
+ )
178
+
179
+ # ============================================================
180
+ # Serialization
181
+ # ์ง๋ ฌํ™”
182
+ # ============================================================
183
+ def to_dict(self) -> dict[str, Any]:
184
+ """
185
+ Return a JSON-serializable dict for preprocessor_config.json.
186
+ preprocessor_config.json์— ๋“ค์–ด๊ฐˆ JSON ์ง๋ ฌํ™” dict๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
187
+
188
+ Important: do not leak runtime objects into the serialized dict.
189
+ ์ค‘์š”: ๋Ÿฐํƒ€์ž„ ๊ฐ์ฒด๊ฐ€ ์ง๋ ฌํ™” dict์— ์„ž์ด๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
190
+ """
191
+ # ImageProcessingMixin.to_dict() adds metadata such as image_processor_type/auto_map.
192
+ # ImageProcessingMixin.to_dict()๋Š” image_processor_type/auto_map ๊ฐ™์€ ๋ฉ”ํƒ€๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
193
+ d = super().to_dict()
194
+
195
+ # Force minimal stable fields for long-term compatibility.
196
+ # ์žฅ๊ธฐ ํ˜ธํ™˜์„ ์œ„ํ•ด ์ตœ์†Œ ์•ˆ์ • ํ•„๋“œ๋ฅผ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค.
197
+ d["image_processor_type"] = self.__class__.__name__
198
+ d["backbone_name_or_path"] = self.backbone_name_or_path
199
+ d["is_training"] = self.is_training
200
+
201
+ # Remove any runtime-only fields defensively.
202
+ # ๋Ÿฐํƒ€์ž„ ์ „์šฉ ํ•„๋“œ๋Š” ๋ณด์ˆ˜์ ์œผ๋กœ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.
203
+ for key in ["_meta", "_delegate", "_timm_transform", "_torchvision_transform"]:
204
+ d.pop(key, None)
205
+
206
+ return d
207
+
208
+ @classmethod
209
+ def from_dict(cls, image_processor_dict: dict[str, Any], **kwargs):
210
+ """
211
+ Standard load path used by BaseImageProcessor / AutoImageProcessor.
212
+ BaseImageProcessor / AutoImageProcessor๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” ํ‘œ์ค€ ๋กœ๋“œ ๊ฒฝ๋กœ์ž…๋‹ˆ๋‹ค.
213
+ """
214
+ backbone = image_processor_dict.get("backbone_name_or_path", None)
215
+ if backbone is None:
216
+ raise ValueError("preprocessor_config.json missing key: backbone_name_or_path")
217
+
218
+ is_training = bool(image_processor_dict.get("is_training", False))
219
+ return cls(backbone_name_or_path=backbone, is_training=is_training, **kwargs)
220
+
221
+ @classmethod
222
+ def from_pretrained(cls, pretrained_model_name_or_path: str, **kwargs):
223
+ """
224
+ Fallback path if AutoImageProcessor calls class.from_pretrained directly.
225
+ AutoImageProcessor๊ฐ€ class.from_pretrained๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋Š” ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•œ ๊ฒฝ๋กœ์ž…๋‹ˆ๋‹ค.
226
+
227
+ Strategy:
228
+ ์ „๋žต:
229
+
230
+ - Read config.json via AutoConfig and recover backbone_name_or_path.
231
+ AutoConfig๋กœ config.json์„ ์ฝ๊ณ  backbone_name_or_path๋ฅผ ๋ณต๊ตฌํ•ฉ๋‹ˆ๋‹ค.
232
+ """
233
+ cfg = AutoConfig.from_pretrained(pretrained_model_name_or_path, **kwargs)
234
+ backbone = getattr(cfg, "backbone_name_or_path", None)
235
+ if backbone is None:
236
+ raise ValueError("Cannot build processor: backbone_name_or_path not found in config.json")
237
+
238
+ # is_training is runtime-only and should default to False for inference/serving.
239
+ # is_training์€ ๋Ÿฐํƒ€์ž„ ์ „์šฉ์ด๋ฉฐ ์ถ”๋ก /์„œ๋น™ ๊ธฐ๋ณธ๊ฐ’์€ False๊ฐ€ ๋งž์Šต๋‹ˆ๋‹ค.
240
+ return cls(backbone_name_or_path=backbone, is_training=False)
241
+
242
+ # ============================================================
243
+ # Call interface
244
+ # ํ˜ธ์ถœ ์ธํ„ฐํŽ˜์ด์Šค
245
+ # ============================================================
246
+ @staticmethod
247
+ def _ensure_list(images: Any) -> list[Any]:
248
+ # Normalize scalar image input to a list for uniform processing.
249
+ # ๋‹จ์ผ ์ž…๋ ฅ์„ ๋ฆฌ์ŠคํŠธ๋กœ ์ •๊ทœํ™”ํ•˜์—ฌ ๋™์ผํ•œ ์ฒ˜๋ฆฌ ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
250
+ if isinstance(images, (list, tuple)):
251
+ return list(images)
252
+ return [images]
253
+
254
+ @staticmethod
255
+ def _to_pil_rgb(x: Any):
256
+ # Convert common image inputs into PIL RGB images.
257
+ # ์ผ๋ฐ˜์ ์ธ ์ž…๋ ฅ์„ PIL RGB ์ด๋ฏธ์ง€๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
258
+ from PIL import Image as PILImage
259
+
260
+ if isinstance(x, PILImage.Image):
261
+ return x.convert("RGB")
262
+ if isinstance(x, np.ndarray) and x.ndim == 3:
263
+ return PILImage.fromarray(x).convert("RGB")
264
+ raise TypeError(f"Unsupported image type: {type(x)}")
265
+
266
+ def __call__(
267
+ self,
268
+ images: Any | list[Any],
269
+ return_tensors: str | TensorType | None = "pt",
270
+ **kwargs,
271
+ ) -> dict[str, Any]:
272
+ """
273
+ Convert images into {"pixel_values": Tensor/ndarray}.
274
+ ์ด๋ฏธ์ง€๋ฅผ {"pixel_values": Tensor/ndarray}๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
275
+ """
276
+ images = self._ensure_list(images)
277
+
278
+ # Rebuild runtime if needed (e.g., right after deserialization).
279
+ # ์ง๋ ฌํ™” ๋ณต์› ์งํ›„ ๋“ฑ ๋Ÿฐํƒ€์ž„์ด ๋น„์–ด์žˆ์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์žฌ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
280
+ if (self._delegate is None) and (self._timm_transform is None) and (self._torchvision_transform is None):
281
+ self._build_runtime()
282
+
283
+ # timm path: PIL -> torch.Tensor(C,H,W) normalized float32.
284
+ # timm ๊ฒฝ๋กœ: PIL -> torch.Tensor(C,H,W) ์ •๊ทœํ™” float32์ž…๋‹ˆ๋‹ค.
285
+ if self._timm_transform is not None:
286
+ pv: list[torch.Tensor] = []
287
+ for im in images:
288
+ pil = self._to_pil_rgb(im)
289
+ t = self._timm_transform(pil)
290
+ if not isinstance(t, torch.Tensor):
291
+ raise RuntimeError("Unexpected timm transform output (expected torch.Tensor).")
292
+ pv.append(t)
293
+ pixel_values = torch.stack(pv, dim=0) # (B,C,H,W)
294
+ return self._format_return(pixel_values, return_tensors)
295
+
296
+ # torchvision path: PIL -> torch.Tensor(C,H,W) normalized float32.
297
+ # torchvision ๊ฒฝ๋กœ: PIL -> torch.Tensor(C,H,W) ์ •๊ทœํ™” float32์ž…๋‹ˆ๋‹ค.
298
+ if self._torchvision_transform is not None:
299
+ pv: list[torch.Tensor] = []
300
+ for im in images:
301
+ pil = self._to_pil_rgb(im)
302
+ t = self._torchvision_transform(pil)
303
+ if not isinstance(t, torch.Tensor):
304
+ raise RuntimeError("Unexpected torchvision transform output (expected torch.Tensor).")
305
+ pv.append(t)
306
+ pixel_values = torch.stack(pv, dim=0) # (B,C,H,W)
307
+ return self._format_return(pixel_values, return_tensors)
308
+
309
+ # transformers delegate path: rely on official processor behavior.
310
+ # transformers ์œ„์ž„ ๊ฒฝ๋กœ: ๊ณต์‹ processor ๋™์ž‘์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
311
+ if self._delegate is None:
312
+ raise RuntimeError("Processor runtime not built: delegate is None and no transforms are available.")
313
+
314
+ return self._delegate(images, return_tensors=return_tensors, **kwargs)
315
+
316
+ @staticmethod
317
+ def _format_return(pixel_values: torch.Tensor, return_tensors: str | TensorType | None) -> dict[str, Any]:
318
+ """
319
+ Format pixel_values according to return_tensors.
320
+ return_tensors์— ๋งž์ถฐ pixel_values ๋ฐ˜ํ™˜ ํฌ๋งท์„ ๋งž์ถฅ๋‹ˆ๋‹ค.
321
+ """
322
+ if return_tensors is None or return_tensors in ("pt", TensorType.PYTORCH):
323
+ return {"pixel_values": pixel_values}
324
+ if return_tensors in ("np", TensorType.NUMPY):
325
+ return {"pixel_values": pixel_values.detach().cpu().numpy()}
326
+ raise ValueError(f"Unsupported return_tensors={return_tensors}. Use 'pt' or 'np'.")
327
+
328
+
329
+ # Register this processor for AutoImageProcessor resolution.
330
+ # AutoImageProcessor ํ•ด์„์„ ์œ„ํ•ด ์ด processor๋ฅผ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.
331
+ if __name__ != "__main__":
332
+ BackboneMLPHead224ImageProcessor.register_for_auto_class("AutoImageProcessor")
models/google__efficientnet-b0/model.safetensors CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:a67fc215d299f5fe53f61da21d454c40f414d1221ff57167797cc276ee5e2991
3
  size 17558436
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:01add885928661dde020c6cc5adaf1e6b8d6684a9effa7ef2903ac8d03fc9a5c
3
  size 17558436
models/google__efficientnet-b0/preprocessor_config.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "auto_map": {
3
+ "AutoImageProcessor": "ds_proc.BackboneMLPHead224ImageProcessor"
4
+ },
5
+ "backbone_name_or_path": "google/efficientnet-b0",
6
+ "image_processor_type": "BackboneMLPHead224ImageProcessor",
7
+ "is_training": false
8
+ }
models/google__vit-base-patch16-224/config.json CHANGED
@@ -24,10 +24,11 @@
24
  "num_labels": 3,
25
  "transformers_version": "5.1.0",
26
  "ds_provenance": {
27
- "created_at": "20260210_050905",
28
  "repo_id": "dsaint31/bb_mlp_224",
29
  "subdir": "models/google__vit-base-patch16-224",
30
  "wrapper_class": "BackboneWithMLPHeadForImageClassification",
 
31
  "backbone_id": "google/vit-base-patch16-224",
32
  "pretrained_injected": true,
33
  "policy_init_pretrained_forbidden": true,
 
24
  "num_labels": 3,
25
  "transformers_version": "5.1.0",
26
  "ds_provenance": {
27
+ "created_at": "20260210_111913",
28
  "repo_id": "dsaint31/bb_mlp_224",
29
  "subdir": "models/google__vit-base-patch16-224",
30
  "wrapper_class": "BackboneWithMLPHeadForImageClassification",
31
+ "processor_class": "BackboneMLPHead224ImageProcessor",
32
  "backbone_id": "google/vit-base-patch16-224",
33
  "pretrained_injected": true,
34
  "policy_init_pretrained_forbidden": true,
models/google__vit-base-patch16-224/ds_cfg.py CHANGED
@@ -1,84 +1,175 @@
1
- from typing import Literal, Union
2
-
3
- from transformers import AutoConfig
4
  from transformers import PretrainedConfig
 
5
  from ds_meta import BackboneID, BACKBONE_META
6
 
7
- # ============================================================
8
- # 3) Config
9
- # ============================================================
10
  class BackboneMLPHeadConfig(PretrainedConfig):
11
  """
12
- backbone_name_or_path๋Š” BACKBONE_META ํ‚ค(ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ)๋งŒ ํ—ˆ์šฉ.
13
-
14
- ์ค‘์š”ํ•œ ์ 
15
- ----------
16
- - transformers ๋‚ด๋ถ€ ๊ฒฝ๋กœ์—์„œ "๋ฌด์ธ์ž ์ƒ์„ฑ"์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ,
17
- backbone_name_or_path=None์ธ ๊ฒฝ์šฐ๋Š” ๊ฒ€์ฆ ์—†์ด ๊ธฐ๋ณธ๊ฐ’๋งŒ ์„ธํŒ…ํ•˜๊ณ  return
18
- ํ•˜๋Š” ๊ตฌํ˜„์ด ๋ฐ˜๋“œ์‹œ ํ•„์š”ํ•จ.
19
- - ๋‹จ, ์‹ค์ œ ์‚ฌ์šฉ์—์„œ๋Š” backbone_name_or_path๋ฅผ ๋ฐ˜๋“œ์‹œ ์ง€์ •ํ•ด์„œ ์ƒ์„ฑํ•ด์•ผ ํ•จ.
20
  """
21
 
22
- # AutoConfig๊ฐ€ ์ด ๋ชจ๋ธ์„ ์‹๋ณ„ํ•˜๋Š”๋ฐ ์‚ฌ์šฉํ•˜๋Š” ๋ฌธ์ž์—ด ID.
 
23
  model_type = "backbone-mlphead-224-fixed"
24
 
25
- # ์ƒ์„ฑ์ž์—์„œ ๋ชจ๋“  ์ธ์ž์— ๊ธฐ๋ณธ๊ฐ’ ๋ถ€์—ฌ ๋ฐ˜๋“œ์‹œ ํ•„์š”!
26
  def __init__(
27
  self,
28
  backbone_name_or_path: BackboneID | None = None,
29
  mlp_head_bottleneck: int = 256,
30
  mlp_head_dropout: float = 0.2,
31
- label2id: dict[str, int]|None = None,
32
- id2label: dict[int, str]|None = None,
33
  **kwargs,
34
  ):
35
- # ๋ฌด์ธ์ž ์ƒ์„ฑ ๊ฒฝ๋กœ ๋ฐฉ์–ด
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  if backbone_name_or_path is None:
 
 
 
 
 
 
 
37
  super().__init__(**kwargs)
 
 
 
38
  self.backbone_name_or_path = None
39
- self.mlp_head_bottleneck = int(mlp_head_bottleneck)
40
- self.mlp_head_dropout = float(mlp_head_dropout)
41
- # num_labels๋„ ๊ธฐ๋ณธ๊ฐ’ ํ•˜๋‚˜ ์ค˜์„œ 0 ๋ฐฉ์ง€(3์œผ๋กœ ๋‘๋“  0์œผ๋กœ ๋‘๋“  ์ •์ฑ…)
42
- # ์ตœ์†Œ ์•ˆ์ „: id2label์ด ์žˆ์œผ๋ฉด ๊ทธ ๊ธธ์ด, ์—†์œผ๋ฉด 0
 
 
 
 
 
 
 
43
  return
44
 
45
- # ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๊ฒ€์ฆ
 
 
 
 
 
 
 
 
46
  if backbone_name_or_path not in BACKBONE_META:
47
  raise ValueError(
48
  f"Unsupported backbone_name_or_path={backbone_name_or_path}. "
49
  f"Allowed: {sorted(BACKBONE_META.keys())}"
50
  )
51
- if id2label is None and label2id is None:
52
- raise ValueError("id2label/label2id ๋‘˜ ์ค‘ ํ•˜๋‚˜๋Š” ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.")
53
- if id2label is None:
54
- id2label = {v: k for k, v in label2id.items()}
55
- if label2id is None:
56
- label2id = {v: k for k, v in id2label.items()}
57
-
58
- num_labels = len(id2label)
59
- # kwargs์— ์ด๋ฏธ num_labels๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ
60
- if "num_labels" not in kwargs:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  kwargs["num_labels"] = num_labels
62
 
 
 
 
 
 
 
63
  super().__init__(
64
- label2id=label2id,
65
- id2label=id2label,
66
  **kwargs,
67
  )
68
-
69
- # 4) ์ค‘์š”: ๋ถ€๋ชจ ํ•„๋“œ์—๋„ num_labels ๋ฐ˜์˜(์ €์žฅ/๋กœ๋“œ ์•ˆ์ •)
70
- self.num_labels = num_labels
71
- self.label2id = label2id
72
- self.id2label = id2label
73
 
 
 
 
 
 
 
74
  self.backbone_name_or_path = backbone_name_or_path
75
- self.mlp_head_bottleneck = int(mlp_head_bottleneck)
76
- self.mlp_head_dropout = float(mlp_head_dropout)
 
 
 
 
77
 
78
  def to_dict(self):
 
 
79
  output = super().to_dict()
80
- # ๋ช…์‹œ์ ์œผ๋กœ ํฌํ•จ
81
- output["num_labels"] = self.num_labels
 
 
 
 
82
  return output
83
 
 
 
 
84
  BackboneMLPHeadConfig.register_for_auto_class("AutoConfig")
 
 
 
 
1
  from transformers import PretrainedConfig
2
+
3
  from ds_meta import BackboneID, BACKBONE_META
4
 
5
+
 
 
6
  class BackboneMLPHeadConfig(PretrainedConfig):
7
  """
8
+ Configuration for Backbone + MLP Head models.
9
+
10
+ Backbone + MLP Head ๋ชจ๋ธ์„ ์œ„ํ•œ ์„ค์ • ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.
 
 
 
 
 
11
  """
12
 
13
+ # This string is used by Hugging Face AutoConfig to identify the config class.
14
+ # Hugging Face AutoConfig๊ฐ€ ์ด config๋ฅผ ์‹๋ณ„ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” ๊ณ ์œ  ID์ž…๋‹ˆ๋‹ค.
15
  model_type = "backbone-mlphead-224-fixed"
16
 
 
17
  def __init__(
18
  self,
19
  backbone_name_or_path: BackboneID | None = None,
20
  mlp_head_bottleneck: int = 256,
21
  mlp_head_dropout: float = 0.2,
22
+ label2id: dict[str, int] | None = None,
23
+ id2label: dict[int, str] | None = None,
24
  **kwargs,
25
  ):
26
+ # ============================================================
27
+ # 0) Guard for argument-less construction
28
+ # 0) ๋ฌด์ธ์ž ์ƒ์„ฑ ๊ฒฝ๋กœ ๋ฐฉ์–ด
29
+ # ============================================================
30
+ # Transformers may internally construct this config without arguments
31
+ # (e.g., during AutoConfig resolution or Hub loading).
32
+ # Transformers ๋‚ด๋ถ€์—์„œ AutoConfig ๋˜๋Š” Hub ๋กœ๋“œ ๊ณผ์ • ์ค‘
33
+ # ์ธ์ž ์—†์ด config๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ฒฝ๋กœ๊ฐ€ ์‹ค์ œ๋กœ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.
34
+ #
35
+ # In this case, we must NOT validate or raise errors.
36
+ # ์ด ๊ฒฝ์šฐ ๊ฒ€์ฆ์ด๋‚˜ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
37
+ #
38
+ # The goal here is to provide a minimal, serialization-safe config.
39
+ # ๋ชฉํ‘œ๋Š” ์ตœ์†Œํ•œ์˜ ๊ฐ’๋งŒ ์ฑ„์›Œ ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™”๊ฐ€ ๊นจ์ง€์ง€ ์•Š๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.
40
  if backbone_name_or_path is None:
41
+ # num_labels may be implicitly assumed by downstream code,
42
+ # so we explicitly set a safe default.
43
+ # num_labels๋Š” downstream ์ฝ”๋“œ์—์„œ ์•”๋ฌต์ ์œผ๋กœ ๊ฐ€์ •๋˜๋ฏ€๋กœ
44
+ # ์•ˆ์ „ํ•œ ๊ธฐ๋ณธ๊ฐ’์„ ๋ช…์‹œ์ ์œผ๋กœ ๋„ฃ์–ด์ค๋‹ˆ๋‹ค.
45
+ if "num_labels" not in kwargs:
46
+ kwargs["num_labels"] = 0
47
+
48
  super().__init__(**kwargs)
49
+
50
+ # Backbone is intentionally unset in this path.
51
+ # ์ด ๊ฒฝ๋กœ์—์„œ๋Š” backbone์„ ์˜๋„์ ์œผ๋กœ ์„ค์ •ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
52
  self.backbone_name_or_path = None
53
+
54
+ # Store MLP head hyperparameters for completeness.
55
+ # MLP head ๊ด€๋ จ ํ•˜์ดํผํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ํ˜•ํƒœ ์œ ์ง€๋ฅผ ์œ„ํ•ด ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
56
+ self.mlp_head_bottleneck = int(mlp_head_bottleneck)
57
+ self.mlp_head_dropout = float(mlp_head_dropout)
58
+
59
+ # Empty label mappings ensure stable save/load behavior.
60
+ # ๋นˆ label ๋งคํ•‘์€ ์ €์žฅ/๋กœ๋“œ ์•ˆ์ •์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.
61
+ self.label2id = {}
62
+ self.id2label = {}
63
+ self.num_labels = int(kwargs.get("num_labels", 0))
64
  return
65
 
66
+ # ============================================================
67
+ # 1) Backbone whitelist validation
68
+ # 1) Backbone ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๊ฒ€์ฆ
69
+ # ============================================================
70
+ # Only backbones explicitly registered in BACKBONE_META are allowed.
71
+ # BACKBONE_META์— ๋“ฑ๋ก๋œ backbone๋งŒ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.
72
+ #
73
+ # This prevents accidental usage of unsupported or inconsistent backbones.
74
+ # ์ง€์›๋˜์ง€ ์•Š๊ฑฐ๋‚˜ ๋ถˆ์ผ์น˜ํ•œ backbone ์‚ฌ์šฉ์„ ์›์ฒœ ์ฐจ๋‹จํ•ฉ๋‹ˆ๋‹ค.
75
  if backbone_name_or_path not in BACKBONE_META:
76
  raise ValueError(
77
  f"Unsupported backbone_name_or_path={backbone_name_or_path}. "
78
  f"Allowed: {sorted(BACKBONE_META.keys())}"
79
  )
80
+
81
+ # ============================================================
82
+ # 2) Label mapping normalization
83
+ # 2) ๋ผ๋ฒจ ๋งคํ•‘ ์ •๊ทœํ™”
84
+ # ============================================================
85
+ # Both label2id and id2label may be None during pure loading scenarios.
86
+ # ๋‹จ์ˆœ ๋กœ๋“œ(from_pretrained) ์‹œ label2id/id2label์ด None์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
87
+ #
88
+ # We allow this here to keep Hub loading and verification stable.
89
+ # Hub ๋กœ๋“œ ๋ฐ ๊ฒ€์ฆ ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด ์—ฌ๊ธฐ์„œ๋Š” ์ด๋ฅผ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.
90
+ #
91
+ # Fail-fast validation should happen at model or training level instead.
92
+ # ์‹ค์ œ ์‚ฌ์šฉ ๊ฒ€์ฆ์€ ๋ชจ๋ธ ๋˜๋Š” ํ•™์Šต ๋‹จ๊ณ„์—์„œ fail-fast๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
93
+ if label2id is None and id2label is None:
94
+ # Respect num_labels if explicitly provided, otherwise default to 0.
95
+ # num_labels๊ฐ€ ์ฃผ์–ด์กŒ๋‹ค๋ฉด ์กด์ค‘ํ•˜๊ณ , ์•„๋‹ˆ๋ฉด 0์œผ๋กœ ๋‘ก๋‹ˆ๋‹ค.
96
+ num_labels = int(kwargs.get("num_labels", 0))
97
+ label2id_norm: dict[str, int] = {}
98
+ id2label_norm: dict[int, str] = {}
99
+ else:
100
+ # If only one mapping is provided, derive the other.
101
+ # ํ•˜๋‚˜๋งŒ ์ฃผ์–ด์ง„ ๊ฒฝ์šฐ ๋‚˜๋จธ์ง€๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
102
+ if id2label is None:
103
+ id2label = {v: k for k, v in label2id.items()}
104
+ if label2id is None:
105
+ label2id = {v: k for k, v in id2label.items()}
106
+
107
+ # Ensure both mappings are consistent in size.
108
+ # ๋‘ ๋งคํ•‘์˜ ํฌ๊ธฐ๊ฐ€ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
109
+ if len(label2id) != len(id2label):
110
+ raise ValueError(
111
+ f"label2id/id2label size mismatch: "
112
+ f"{len(label2id)} vs {len(id2label)}"
113
+ )
114
+
115
+ num_labels = len(id2label)
116
+ label2id_norm = dict(label2id)
117
+ id2label_norm = dict(id2label)
118
+
119
+ # ============================================================
120
+ # 3) num_labels consistency enforcement
121
+ # 3) num_labels ์ผ๊ด€์„ฑ ๊ฐ•์ œ
122
+ # ============================================================
123
+ # If num_labels is provided via kwargs, it must match inferred labels.
124
+ # kwargs๋กœ num_labels๊ฐ€ ๋“ค์–ด์˜จ ๊ฒฝ์šฐ ์ถ”๋ก ๋œ ๊ฐ’๊ณผ ๋ฐ˜๋“œ์‹œ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
125
+ if "num_labels" in kwargs:
126
+ if (label2id is not None or id2label is not None) and int(kwargs["num_labels"]) != num_labels:
127
+ raise ValueError(
128
+ f"kwargs['num_labels']={kwargs['num_labels']} "
129
+ f"!= inferred num_labels={num_labels}"
130
+ )
131
+ else:
132
  kwargs["num_labels"] = num_labels
133
 
134
+ # ============================================================
135
+ # 4) Parent initialization
136
+ # 4) ๋ถ€๋ชจ ํด๋ž˜์Šค ์ดˆ๊ธฐํ™”
137
+ # ============================================================
138
+ # Initialize PretrainedConfig with normalized label mappings.
139
+ # ์ •๊ทœํ™”๋œ ๋ผ๋ฒจ ๋งคํ•‘์„ ์‚ฌ์šฉํ•˜์—ฌ PretrainedConfig๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
140
  super().__init__(
141
+ label2id=label2id_norm,
142
+ id2label=id2label_norm,
143
  **kwargs,
144
  )
 
 
 
 
 
145
 
146
+ # ============================================================
147
+ # 5) Explicit attribute assignment for save/load stability
148
+ # 5) ์ €์žฅ/๋กœ๋“œ ์•ˆ์ •์„ฑ์„ ์œ„ํ•œ ๋ช…์‹œ์  ์†์„ฑ ๊ณ ์ •
149
+ # ============================================================
150
+ # Explicitly reassign critical fields to avoid subtle serialization issues.
151
+ # ๋ฏธ๋ฌ˜ํ•œ ์ง๋ ฌํ™” ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ํ•ต์‹ฌ ํ•„๋“œ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ๋‹ค์‹œ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
152
  self.backbone_name_or_path = backbone_name_or_path
153
+ self.mlp_head_bottleneck = int(mlp_head_bottleneck)
154
+ self.mlp_head_dropout = float(mlp_head_dropout)
155
+
156
+ self.label2id = label2id_norm
157
+ self.id2label = id2label_norm
158
+ self.num_labels = int(kwargs["num_labels"])
159
 
160
  def to_dict(self):
161
+ # Call the parent implementation first.
162
+ # ๋จผ์ € ๋ถ€๋ชจ ๊ตฌํ˜„์„ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
163
  output = super().to_dict()
164
+
165
+ # Force num_labels to be present and consistent.
166
+ # num_labels๊ฐ€ ๋ฐ˜๋“œ์‹œ ์กด์žฌํ•˜๊ณ  ์ผ๊ด€๋˜๋„๋ก ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค.
167
+ output["num_labels"] = int(
168
+ getattr(self, "num_labels", output.get("num_labels", 0))
169
+ )
170
  return output
171
 
172
+
173
+ # Register this config so it can be resolved via AutoConfig.
174
+ # AutoConfig๋ฅผ ํ†ตํ•ด ์ด config๊ฐ€ ํ•ด์„๋  ์ˆ˜ ์žˆ๋„๋ก ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.
175
  BackboneMLPHeadConfig.register_for_auto_class("AutoConfig")
models/google__vit-base-patch16-224/ds_model.py CHANGED
@@ -1,10 +1,9 @@
1
  #!/usr/bin/env python3
2
  # -*- coding: utf-8 -*-
3
 
4
- from __future__ import annotations
5
 
6
- from dataclasses import dataclass
7
- from typing import Dict, Any, Optional, Tuple, Set, List
8
 
9
  import torch
10
  import torch.nn as nn
@@ -12,146 +11,88 @@ import torch.nn.functional as F
12
 
13
  # --- transformers core ---
14
  from transformers.modeling_utils import PreTrainedModel
15
- from transformers import AutoModel, AutoConfig, PretrainedConfig
16
  from transformers.modeling_outputs import ImageClassifierOutput
17
 
18
  # --- torchvision ---
19
  from torchvision import models as tv_models
20
 
21
-
22
- # ============================================================
23
- # (0) Config
24
- # - ๊ธฐ์กด ds_cfg.BackboneMLPHeadConfig์™€ ํ•„๋“œ๋ช…์„ ๋งž์ถ”์—ˆ์Šต๋‹ˆ๋‹ค.
25
- # - ์ด๋ฏธ ds_cfg.py๊ฐ€ ์žˆ๋‹ค๋ฉด: ์ด ๋ธ”๋ก์„ ์ง€์šฐ๊ณ  "from ds_cfg import BackboneMLPHeadConfig"๋กœ ๋ฐ”๊ฟ”๋„ ๋ฉ๋‹ˆ๋‹ค.
26
- # ============================================================
27
- class BackboneMLPHeadConfig(PretrainedConfig):
28
- model_type = "backbone_mlphead"
29
-
30
- def __init__(
31
- self,
32
- backbone_name_or_path: Optional[str] = None,
33
- num_labels: int = 2,
34
- id2label: Optional[Dict[int, str]] = None,
35
- label2id: Optional[Dict[str, int]] = None,
36
- mlp_head_bottleneck: int = 256,
37
- mlp_head_dropout: float = 0.2,
38
- **kwargs,
39
- ):
40
- super().__init__(**kwargs)
41
- self.backbone_name_or_path = backbone_name_or_path
42
- self.num_labels = num_labels
43
- self.id2label = id2label or {i: f"LABEL_{i}" for i in range(num_labels)}
44
- self.label2id = label2id or {v: k for k, v in self.id2label.items()}
45
- self.mlp_head_bottleneck = mlp_head_bottleneck
46
- self.mlp_head_dropout = mlp_head_dropout
47
-
48
-
49
- # ============================================================
50
- # (1) BACKBONE_META
51
- # - ์ด๋ฏธ ds_meta.py๊ฐ€ ์žˆ๋‹ค๋ฉด: ์ด ๋ธ”๋ก์„ ์ง€์šฐ๊ณ  "from ds_meta import BACKBONE_META"๋กœ ๋ฐ”๊ฟ”๋„ ๋ฉ๋‹ˆ๋‹ค.
52
- # ============================================================
53
- BACKBONE_META: Dict[str, Dict[str, Any]] = {
54
- # transformers
55
- "google/vit-base-patch16-224": {
56
- "type": "vit",
57
- "feat_rule": "cls",
58
- "feat_dim": 768,
59
- "has_bn": False,
60
- "unfreeze": "last_n",
61
- },
62
- "microsoft/swin-tiny-patch4-window7-224": {
63
- "type": "swin",
64
- "feat_rule": "pool_or_mean",
65
- "feat_dim": 768,
66
- "has_bn": False,
67
- "unfreeze": "last_n",
68
- },
69
- "microsoft/resnet-50": {
70
- "type": "resnet",
71
- "feat_rule": "pool_or_gap",
72
- "feat_dim": 2048,
73
- "has_bn": True,
74
- "unfreeze": "last_n",
75
- },
76
- "google/efficientnet-b0": {
77
- "type": "efficientnet",
78
- "feat_rule": "pool_or_gap",
79
- "feat_dim": 1280,
80
- "has_bn": True,
81
- "unfreeze": "last_n",
82
- },
83
-
84
- # densenet
85
- "timm/densenet121.tv_in1k": {
86
- "type": "timm_densenet",
87
- "feat_rule": "timm_gap",
88
- "feat_dim": 1024,
89
- "has_bn": True,
90
- "unfreeze": "last_n",
91
- },
92
- "torchvision/densenet121": {
93
- "type": "torchvision_densenet",
94
- "feat_rule": "torchvision_densenet_gap",
95
- "feat_dim": 1024,
96
- "has_bn": True,
97
- "unfreeze": "last_n",
98
- },
99
- }
100
-
101
-
102
- # ============================================================
103
- # (2) MLP Head
104
- # - ์ด๋ฏธ mlp_head.py๊ฐ€ ์žˆ๋‹ค๋ฉด: ์ด ๋ธ”๋ก์„ ์ง€์šฐ๊ณ  "from mlp_head import MLPHead"๋กœ ๋ฐ”๊ฟ”๋„ ๋ฉ๋‹ˆ๋‹ค.
105
- # ============================================================
106
- class MLPHead(nn.Module):
107
- def __init__(self, in_dim: int, num_labels: int, bottleneck: int = 256, p: float = 0.2):
108
- super().__init__()
109
- self.fc2 = nn.Linear(in_dim, bottleneck)
110
- self.act2 = nn.GELU()
111
- self.drop2 = nn.Dropout(p)
112
- self.fc3 = nn.Linear(bottleneck, num_labels)
113
-
114
- def forward(self, x: torch.Tensor) -> torch.Tensor:
115
- x = self.drop2(self.act2(self.fc2(x)))
116
- return self.fc3(x)
117
-
118
 
119
  # ============================================================
120
  # (3) Model: backbone + MLP head
121
- # ํ•ต์‹ฌ ์„ค๊ณ„:
122
- # - __init__: backbone ๊ตฌ์กฐ(skeleton)๋งŒ ์ƒ์„ฑ. pretrained๋ฅผ ์ ˆ๋Œ€ ๋กœ๋“œํ•˜์ง€ ์•Š์Œ.
123
- # - pretrained ์ฃผ์ž…: model.load_backbone_pretrained_()๋ฅผ "fresh-start์—์„œ๋งŒ" ์ง์ ‘ ํ˜ธ์ถœ.
124
- # - from_pretrained override ์—†์Œ: HF ๊ธฐ๋ณธ ๋กœ๋”๊ฐ€ checkpoint๋ฅผ ๊ทธ๋Œ€๋กœ ๋ณต์›.
125
  # ============================================================
 
 
 
 
 
 
 
 
126
  class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
 
 
127
  config_class = BackboneMLPHeadConfig
128
 
129
  def __init__(self, config: BackboneMLPHeadConfig):
 
 
130
  super().__init__(config)
131
 
 
 
 
 
 
132
  if config.backbone_name_or_path is None:
133
- raise ValueError("config.backbone_name_or_path is None. Provide a valid backbone id.")
 
 
 
134
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  self._meta = BACKBONE_META[config.backbone_name_or_path]
136
 
137
- # (์ค‘์š”) backbone์€ ํ•ญ์ƒ skeleton๋งŒ ์ƒ์„ฑ
 
138
  self.backbone = self._build_backbone_skeleton(config.backbone_name_or_path)
139
 
140
- # head
 
141
  self.classifier = MLPHead(
142
  in_dim=int(self._meta["feat_dim"]),
143
- num_labels=config.num_labels,
144
- bottleneck=config.mlp_head_bottleneck,
145
- p=config.mlp_head_dropout,
146
  )
147
 
148
- # HF init hook (init_weights๋Š” head-only๋กœ override๋จ)
 
149
  self.post_init()
150
 
151
  def init_weights(self):
152
  """
153
- HF์˜ ๊ธฐ๋ณธ init_weights()๋Š” ๋ชจ๋ธ ์ „์ฒด์— apply(...)๋ฅผ ๊ฑธ ์ˆ˜ ์žˆ์œผ๋‹ˆ,
154
- head๋งŒ ์ดˆ๊ธฐํ™”ํ•œ๋‹ค.
 
 
 
155
  """
156
  if getattr(self, "classifier", None) is not None:
157
  self.classifier.apply(self._init_weights)
@@ -159,8 +100,11 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
159
 
160
  # ----------------------------
161
  # backbone skeleton builders
 
162
  # ----------------------------
163
  def _build_backbone_skeleton(self, backbone_id: str) -> nn.Module:
 
 
164
  meta = BACKBONE_META[backbone_id]
165
  t = meta["type"]
166
 
@@ -170,18 +114,24 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
170
  if t == "torchvision_densenet":
171
  return self._build_torchvision_densenet_skeleton(backbone_id)
172
 
173
- # transformers: config๋กœ ๊ตฌ์กฐ๋งŒ ์ƒ์„ฑ
 
174
  bb_cfg = AutoConfig.from_pretrained(backbone_id)
175
  return AutoModel.from_config(bb_cfg)
176
 
177
  @staticmethod
178
  def _build_timm_densenet_skeleton(hf_repo_id: str) -> nn.Module:
 
 
179
  try:
180
  import timm
181
  except Exception as e:
182
- raise ImportError("DenseNet(timm) backbone requires `timm`. Install: pip install timm") from e
 
 
183
 
184
- # ๊ตฌ์กฐ๋งŒ (pretrained=False)
 
185
  return timm.create_model(
186
  f"hf_hub:{hf_repo_id}",
187
  pretrained=False,
@@ -190,33 +140,33 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
190
 
191
  @staticmethod
192
  def _build_torchvision_densenet_skeleton(model_id: str) -> nn.Module:
193
- mapping = {
194
- "torchvision/densenet121": "densenet121",
195
- "torchvision/densenet169": "densenet169",
196
- "torchvision/densenet201": "densenet201",
197
- "torchvision/densenet161": "densenet161",
198
- }
199
- if model_id not in mapping:
200
- raise ValueError(f"Unsupported torchvision DenseNet id: {model_id}")
201
-
202
- arch = mapping[model_id]
203
- fn = getattr(tv_models, arch)
204
-
205
- # ๊ตฌ์กฐ๋งŒ (weights=None)
206
- m = fn(weights=None).eval()
207
  return m
208
 
209
  # ------------------------------------------------------------
210
- # pretrained ๋กœ๋”ฉ์€ "๋ช…์‹œ์  ํ˜ธ์ถœ"๋กœ๋งŒ ์ˆ˜ํ–‰ (fresh-start ์ „์šฉ)
 
211
  # ------------------------------------------------------------
212
  @torch.no_grad()
213
- def load_backbone_pretrained_(self, *, low_cpu_mem_usage: bool = False, device_map=None):
 
 
 
 
 
214
  """
215
- fresh-start ์ „์šฉ:
216
- model = BackboneWithMLPHeadForImageClassification(cfg)
217
- model.load_backbone_pretrained_()
218
 
219
- checkpoint reload(from_pretrained)์—์„œ๋Š” ํ˜ธ์ถœํ•˜์ง€ ๋งˆ์‹ญ์‹œ์˜ค.
 
220
  """
221
  bb = self.config.backbone_name_or_path
222
  meta = BACKBONE_META[bb]
@@ -230,48 +180,58 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
230
  self._load_torchvision_pretrained_into_skeleton_(bb)
231
  return
232
 
233
- # transformers pretrained -> skeleton์— ์ฃผ์ž…
 
234
  ref = AutoModel.from_pretrained(
235
  bb,
236
  low_cpu_mem_usage=low_cpu_mem_usage,
237
  device_map=device_map,
238
  )
 
 
 
239
  self.backbone.load_state_dict(ref.state_dict(), strict=False)
240
  del ref
241
 
242
  @torch.no_grad()
243
  def _load_timm_pretrained_into_skeleton_(self, hf_repo_id: str):
 
 
244
  import timm
 
 
 
245
  ref = timm.create_model(
246
  f"hf_hub:{hf_repo_id}",
247
  pretrained=True,
248
  num_classes=0,
249
  ).eval()
 
250
  self.backbone.load_state_dict(ref.state_dict(), strict=True)
251
  del ref
252
 
253
  @torch.no_grad()
254
  def _load_torchvision_pretrained_into_skeleton_(self, model_id: str):
255
- mapping = {
256
- "torchvision/densenet121": ("densenet121", tv_models.DenseNet121_Weights.DEFAULT),
257
- "torchvision/densenet169": ("densenet169", tv_models.DenseNet169_Weights.DEFAULT),
258
- "torchvision/densenet201": ("densenet201", tv_models.DenseNet201_Weights.DEFAULT),
259
- "torchvision/densenet161": ("densenet161", tv_models.DenseNet161_Weights.DEFAULT),
260
- }
261
- if model_id not in mapping:
262
- raise ValueError(f"Unsupported torchvision DenseNet id: {model_id}")
263
-
264
- arch, weights = mapping[model_id]
265
- fn = getattr(tv_models, arch)
266
- ref = fn(weights=weights).eval()
267
  self.backbone.load_state_dict(ref.state_dict(), strict=True)
268
  del ref
269
 
270
  # ----------------------------
271
  # feature extraction
 
272
  # ----------------------------
273
  @staticmethod
274
  def _pool_or_gap(outputs) -> torch.Tensor:
 
 
275
  if hasattr(outputs, "pooler_output") and outputs.pooler_output is not None:
276
  x = outputs.pooler_output
277
  if x.dim() == 2:
@@ -280,29 +240,42 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
280
  return x.flatten(1)
281
  raise RuntimeError(f"Unexpected pooler_output shape: {tuple(x.shape)}")
282
 
 
 
283
  x = outputs.last_hidden_state
284
  if x.dim() == 4:
285
  return x.mean(dim=(2, 3))
 
286
  raise RuntimeError(
287
  "Expected pooler_output or (B,C,H,W) last_hidden_state for CNN backbones. "
288
  f"Got last_hidden_state shape={tuple(x.shape)}"
289
  )
290
 
291
  def _extract_features(self, outputs, pixel_values: Optional[torch.Tensor] = None) -> torch.Tensor:
 
 
292
  rule = self._meta["feat_rule"]
293
 
294
  if rule == "cls":
 
 
295
  return outputs.last_hidden_state[:, 0, :]
296
 
297
  if rule == "pool_or_mean":
 
 
298
  if hasattr(outputs, "pooler_output") and outputs.pooler_output is not None:
299
  return outputs.pooler_output
300
  return outputs.last_hidden_state.mean(dim=1)
301
 
302
  if rule == "pool_or_gap":
 
 
303
  return self._pool_or_gap(outputs)
304
 
305
  if rule == "timm_gap":
 
 
306
  if not isinstance(outputs, torch.Tensor):
307
  raise TypeError(f"timm_gap expects Tensor features, got {type(outputs)}")
308
  if outputs.dim() != 4:
@@ -310,6 +283,8 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
310
  return outputs.mean(dim=(2, 3))
311
 
312
  if rule == "torchvision_densenet_gap":
 
 
313
  if not isinstance(outputs, torch.Tensor):
314
  raise TypeError(f"torchvision_densenet_gap expects Tensor, got {type(outputs)}")
315
  if outputs.dim() != 4:
@@ -327,9 +302,13 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
327
  return_dict=True,
328
  **kwargs,
329
  ):
 
 
330
  t = self._meta["type"]
331
 
332
  if t == "timm_densenet":
 
 
333
  if pixel_values is None:
334
  raise ValueError("timm DenseNet backbone requires pixel_values.")
335
  if pixel_values.dim() != 4:
@@ -341,6 +320,8 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
341
  attentions = None
342
 
343
  elif t == "torchvision_densenet":
 
 
344
  if pixel_values is None:
345
  raise ValueError("torchvision DenseNet backbone requires pixel_values.")
346
  if pixel_values.dim() != 4:
@@ -353,6 +334,8 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
353
  attentions = None
354
 
355
  else:
 
 
356
  outputs = self.backbone(
357
  pixel_values=pixel_values,
358
  output_attentions=output_attentions,
@@ -364,10 +347,14 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
364
  hidden_states = getattr(outputs, "hidden_states", None)
365
  attentions = getattr(outputs, "attentions", None)
366
 
 
 
367
  logits = self.classifier(feats)
368
 
369
  loss = None
370
  if labels is not None:
 
 
371
  loss = F.cross_entropy(logits, labels)
372
 
373
  if not return_dict:
@@ -383,20 +370,27 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
383
 
384
 
385
  # ============================================================
386
- # Freeze / Unfreeze utilities (๋‹น์‹  ์ฝ”๋“œ ์œ ์ง€)
 
387
  # ============================================================
388
  def _set_requires_grad(module: nn.Module, flag: bool):
 
 
389
  for p in module.parameters():
390
  p.requires_grad = flag
391
 
392
 
393
  def set_bn_eval(module: nn.Module):
 
 
394
  for m in module.modules():
395
  if isinstance(m, (nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d, nn.SyncBatchNorm)):
396
  m.eval()
397
 
398
 
399
  def freeze_backbone(model: BackboneWithMLPHeadForImageClassification, freeze_bn: bool = True):
 
 
400
  _set_requires_grad(model.backbone, False)
401
  _set_requires_grad(model.classifier, True)
402
 
@@ -406,6 +400,8 @@ def freeze_backbone(model: BackboneWithMLPHeadForImageClassification, freeze_bn:
406
 
407
 
408
  def finetune_train_mode(model: BackboneWithMLPHeadForImageClassification, keep_bn_eval: bool = True):
 
 
409
  model.train()
410
  meta = BACKBONE_META[model.config.backbone_name_or_path]
411
  if keep_bn_eval and meta.get("has_bn", False):
@@ -413,6 +409,8 @@ def finetune_train_mode(model: BackboneWithMLPHeadForImageClassification, keep_b
413
 
414
 
415
  def trainable_summary(model: nn.Module):
 
 
416
  total = sum(p.numel() for p in model.parameters())
417
  trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
418
  ratio = trainable / total if total > 0 else 0.0
@@ -425,6 +423,8 @@ def unfreeze_last_stage(
425
  last_n: int = 2,
426
  keep_bn_eval: bool = True,
427
  ):
 
 
428
  freeze_backbone(model, freeze_bn=keep_bn_eval)
429
 
430
  n = int(last_n)
@@ -438,12 +438,16 @@ def unfreeze_last_stage(
438
  bb_type = meta["type"]
439
 
440
  if bb_type == "vit":
 
 
441
  blocks = list(model.backbone.encoder.layer)
442
  for blk in blocks[-n:]:
443
  _set_requires_grad(blk, True)
444
  return
445
 
446
  if bb_type == "swin":
 
 
447
  stages = list(model.backbone.encoder.layers)
448
  blocks: List[nn.Module] = []
449
  for st in stages:
@@ -453,6 +457,8 @@ def unfreeze_last_stage(
453
  return
454
 
455
  if bb_type == "resnet":
 
 
456
  bb = model.backbone
457
  for name in ("layer1", "layer2", "layer3", "layer4"):
458
  if not hasattr(bb, name):
@@ -472,6 +478,8 @@ def unfreeze_last_stage(
472
  return
473
 
474
  if bb_type == "efficientnet":
 
 
475
  bb = model.backbone
476
  if not hasattr(bb, "features"):
477
  raise RuntimeError("Unexpected EfficientNet structure: missing features")
@@ -488,6 +496,8 @@ def unfreeze_last_stage(
488
  return
489
 
490
  if bb_type in ("timm_densenet", "torchvision_densenet"):
 
 
491
  bb = model.backbone
492
  if not hasattr(bb, "features"):
493
  raise RuntimeError("Unexpected DenseNet: missing features")
@@ -505,6 +515,8 @@ def unfreeze_last_stage(
505
  raise RuntimeError(f"Unexpected DenseNet features: missing {name}")
506
 
507
  def _denselayers(db: nn.Module) -> List[nn.Module]:
 
 
508
  return list(db.children())
509
 
510
  blocks: List[nn.Module] = []
@@ -526,20 +538,8 @@ def unfreeze_last_stage(
526
 
527
  # -------------------------
528
  # register
 
 
 
 
529
  BackboneWithMLPHeadForImageClassification.register_for_auto_class("AutoModelForImageClassification")
530
-
531
-
532
- # ==========================================================
533
-
534
- # # fresh-start
535
- # cfg = BackboneMLPHeadConfig(backbone_name_or_path="timm/densenet121.tv_in1k", num_labels=3)
536
- # model = BackboneWithMLPHeadForImageClassification(cfg)
537
-
538
- # # pretrained๋ฅผ ์“ฐ๋ ค๋ฉด ๋ช…์‹œ์ ์œผ๋กœ ํ˜ธ์ถœ
539
- # model.load_backbone_pretrained_()
540
-
541
- # # ์ดํ›„ ํ•™์Šต
542
-
543
- # # checkpoint์—์„œ ์‹œ์ž‘
544
- # model = BackboneWithMLPHeadForImageClassification.from_pretrained("checkpoints/xxx")
545
- # # ์—ฌ๊ธฐ์„œ๋Š” load_backbone_pretrained_() ํ˜ธ์ถœํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
 
1
  #!/usr/bin/env python3
2
  # -*- coding: utf-8 -*-
3
 
4
+ # src/ds_model.py
5
 
6
+ from typing import Optional, List
 
7
 
8
  import torch
9
  import torch.nn as nn
 
11
 
12
  # --- transformers core ---
13
  from transformers.modeling_utils import PreTrainedModel
14
+ from transformers import AutoModel, AutoConfig
15
  from transformers.modeling_outputs import ImageClassifierOutput
16
 
17
  # --- torchvision ---
18
  from torchvision import models as tv_models
19
 
20
+ from ds_cfg import BackboneMLPHeadConfig
21
+ from ds_meta import BACKBONE_META
22
+ from mlp_head import MLPHead
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  # ============================================================
25
  # (3) Model: backbone + MLP head
26
+ # (3) ๋ชจ๋ธ: backbone + MLP head
 
 
 
27
  # ============================================================
28
+ # Design principle: __init__ builds only a skeleton and MUST NOT load pretrained weights.
29
+ # ์„ค๊ณ„ ์›์น™: __init__์—์„œ๋Š” skeleton๋งŒ ๋งŒ๋“ค๊ณ  pretrained weight ๋กœ๋“œ๋Š” ์ ˆ๋Œ€ ํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
30
+ #
31
+ # Pretrained injection is allowed ONLY via an explicit call in fresh-start flows.
32
+ # pretrained ์ฃผ์ž…์€ fresh-start ํ๋ฆ„์—์„œ์˜ ๋ช…์‹œ์  ํ˜ธ์ถœ๋กœ๋งŒ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.
33
+ #
34
+ # HF from_pretrained should restore checkpoints as-is without side effects.
35
+ # HF from_pretrained๋Š” ๋ถ€์ž‘์šฉ ์—†์ด ์ฒดํฌํฌ์ธํŠธ๋ฅผ ๊ทธ๋Œ€๋กœ ๋ณต์›ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
36
  class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
37
+ # This links the model to its custom config for AutoClass usage.
38
+ # AutoClass ์‚ฌ์šฉ์„ ์œ„ํ•ด ๋ชจ๋ธ๊ณผ ์ปค์Šคํ…€ config๋ฅผ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค.
39
  config_class = BackboneMLPHeadConfig
40
 
41
  def __init__(self, config: BackboneMLPHeadConfig):
42
+ # PreTrainedModel expects a config object and stores it internally.
43
+ # PreTrainedModel์€ config ๊ฐ์ฒด๋ฅผ ๋ฐ›์•„ ๋‚ด๋ถ€์— ์ €์žฅํ•˜๋Š” ์ „์ œ๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค.
44
  super().__init__(config)
45
 
46
+ # Fail-fast: the model is not meant to be instantiated without a valid backbone id.
47
+ # fail-fast: ์œ ํšจํ•œ backbone id ์—†์ด ๋ชจ๋ธ์„ ๋งŒ๋“œ๋Š” ์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค๋Š” ํ—ˆ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
48
+ #
49
+ # Note: Transformers may create configs with no args, but models are conventionally created with configs.
50
+ # ์ฐธ๊ณ : Transformers๋Š” config ๋ฌด์ธ์ž ์ƒ์„ฑ์ด ์žˆ์„ ์ˆ˜ ์žˆ์œผ๋‚˜, ๋ชจ๋ธ์€ ๊ด€๋ก€์ ์œผ๋กœ config๋ฅผ ๋ฐ›์•„ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.
51
  if config.backbone_name_or_path is None:
52
+ raise ValueError(
53
+ "config.backbone_name_or_path is None. "
54
+ "Provide a valid backbone id (whitelist key in BACKBONE_META)."
55
+ )
56
 
57
+ # Fail-fast: training/inference requires a positive number of labels.
58
+ # fail-fast: ํ•™์Šต/์ถ”๋ก ์€ num_labels๊ฐ€ ์–‘์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.
59
+ #
60
+ # Config may exist in a minimal form for internal serialization paths, but the model should not.
61
+ # config๋Š” ๋‚ด๋ถ€ ์ง๋ ฌํ™” ๊ฒฝ๋กœ์—์„œ ์ตœ์†Œ ํ˜•ํƒœ๋กœ ์กด์žฌํ•  ์ˆ˜ ์žˆ์œผ๋‚˜ ๋ชจ๋ธ์€ ๊ทธ๋Ÿฌ๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
62
+ if int(getattr(config, "num_labels", 0)) <= 0:
63
+ raise ValueError(
64
+ f"config.num_labels must be > 0, got {getattr(config, 'num_labels', None)}. "
65
+ "Set num_labels (or id2label/label2id) when creating the config."
66
+ )
67
+
68
+ # Meta is a single source of truth for extraction and fine-tuning rules.
69
+ # meta๋Š” feature ์ถ”์ถœ ๋ฐ ๋ฏธ์„ธ์กฐ์ • ๊ทœ์น™์˜ ๋‹จ์ผ ๊ธฐ์ค€์ž…๋‹ˆ๋‹ค.
70
  self._meta = BACKBONE_META[config.backbone_name_or_path]
71
 
72
+ # Backbone skeleton is always created without pretrained weights.
73
+ # backbone skeleton์€ ํ•ญ์ƒ pretrained weight ์—†์ด ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
74
  self.backbone = self._build_backbone_skeleton(config.backbone_name_or_path)
75
 
76
+ # Head shape is driven by meta feat_dim and config.num_labels.
77
+ # head shape์€ meta์˜ feat_dim๊ณผ config.num_labels๋กœ ๊ฒฐ์ •๋ฉ๋‹ˆ๋‹ค.
78
  self.classifier = MLPHead(
79
  in_dim=int(self._meta["feat_dim"]),
80
+ num_labels=int(config.num_labels),
81
+ bottleneck=int(config.mlp_head_bottleneck),
82
+ p=float(config.mlp_head_dropout),
83
  )
84
 
85
+ # HF initialization hook, but we override init_weights to initialize head-only.
86
+ # HF ์ดˆ๊ธฐํ™” ํ›…์ด์ง€๋งŒ init_weights๋ฅผ overrideํ•˜์—ฌ head๋งŒ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
87
  self.post_init()
88
 
89
  def init_weights(self):
90
  """
91
+ Initialize only the head to avoid touching the backbone skeleton.
92
+ backbone skeleton์„ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๊ธฐ ์œ„ํ•ด head๋งŒ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
93
+
94
+ HF's default init may traverse the entire module tree, which is undesirable here.
95
+ HF ๊ธฐ๋ณธ init์€ ์ „์ฒด ๋ชจ๋“ˆ ํŠธ๋ฆฌ๋ฅผ ์ˆœํšŒํ•  ์ˆ˜ ์žˆ์–ด ์—ฌ๊ธฐ์„œ๋Š” ๋ถ€์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.
96
  """
97
  if getattr(self, "classifier", None) is not None:
98
  self.classifier.apply(self._init_weights)
 
100
 
101
  # ----------------------------
102
  # backbone skeleton builders
103
+ # backbone skeleton ์ƒ์„ฑ๊ธฐ
104
  # ----------------------------
105
  def _build_backbone_skeleton(self, backbone_id: str) -> nn.Module:
106
+ # Meta decides which loader path to use.
107
+ # meta๊ฐ€ ์–ด๋–ค ๋กœ๋” ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ• ์ง€ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.
108
  meta = BACKBONE_META[backbone_id]
109
  t = meta["type"]
110
 
 
114
  if t == "torchvision_densenet":
115
  return self._build_torchvision_densenet_skeleton(backbone_id)
116
 
117
+ # For transformers backbones: build a random-weight skeleton from config only.
118
+ # transformers ๋ฐฑ๋ณธ: config๋กœ๋ถ€ํ„ฐ ๋žœ๋ค ์ดˆ๊ธฐํ™” skeleton๋งŒ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
119
  bb_cfg = AutoConfig.from_pretrained(backbone_id)
120
  return AutoModel.from_config(bb_cfg)
121
 
122
  @staticmethod
123
  def _build_timm_densenet_skeleton(hf_repo_id: str) -> nn.Module:
124
+ # timm is an optional dependency and should be imported lazily.
125
+ # timm์€ ์˜ต์…˜ ์˜์กด์„ฑ์ด๋ฏ€๋กœ ์ง€์—ฐ import ํ•ฉ๋‹ˆ๋‹ค.
126
  try:
127
  import timm
128
  except Exception as e:
129
+ raise ImportError(
130
+ "DenseNet(timm) backbone requires `timm`. Install: pip install timm"
131
+ ) from e
132
 
133
+ # Build structure only (pretrained=False) and remove classifier head (num_classes=0).
134
+ # ๊ตฌ์กฐ๋งŒ ์ƒ์„ฑ(pretrained=False)ํ•˜๊ณ  ๋ถ„๋ฅ˜๊ธฐ head๋Š” ์ œ๊ฑฐ(num_classes=0)ํ•ฉ๋‹ˆ๋‹ค.
135
  return timm.create_model(
136
  f"hf_hub:{hf_repo_id}",
137
  pretrained=False,
 
140
 
141
  @staticmethod
142
  def _build_torchvision_densenet_skeleton(model_id: str) -> nn.Module:
143
+ # This project intentionally supports only torchvision/densenet121 in the 224 whitelist.
144
+ # ์ด ํ”„๋กœ์ ํŠธ๋Š” 224 ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ์—์„œ torchvision/densenet121๋งŒ ์˜๋„์ ์œผ๋กœ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.
145
+ if model_id != "torchvision/densenet121":
146
+ raise ValueError(f"Unsupported torchvision DenseNet id (224 whitelist only): {model_id}")
147
+
148
+ # Build structure only (weights=None) to avoid implicit pretrained loading.
149
+ # implicit pretrained ๋กœ๋“œ๋ฅผ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด ๊ตฌ์กฐ๋งŒ ์ƒ์„ฑ(weights=None)ํ•ฉ๋‹ˆ๋‹ค.
150
+ m = tv_models.densenet121(weights=None)
 
 
 
 
 
 
151
  return m
152
 
153
  # ------------------------------------------------------------
154
+ # Pretrained loading is explicit and fresh-start only
155
+ # pretrained ๋กœ๋”ฉ์€ ๋ช…์‹œ์  ํ˜ธ์ถœ์ด๋ฉฐ fresh-start ์ „์šฉ
156
  # ------------------------------------------------------------
157
  @torch.no_grad()
158
+ def load_backbone_pretrained_(
159
+ self,
160
+ *,
161
+ low_cpu_mem_usage: bool = False,
162
+ device_map=None,
163
+ ):
164
  """
165
+ Fresh-start only: inject pretrained backbone weights into the skeleton.
166
+ fresh-start ์ „์šฉ: skeleton backbone์— pretrained ๊ฐ€์ค‘์น˜๋ฅผ ์ฃผ์ž…ํ•ฉ๋‹ˆ๋‹ค.
 
167
 
168
+ Do NOT call this after from_pretrained() because it would overwrite checkpoint weights.
169
+ from_pretrained() ์ดํ›„ ํ˜ธ์ถœํ•˜๋ฉด ์ฒดํฌํฌ์ธํŠธ ๊ฐ€์ค‘์น˜๋ฅผ ๋ฎ์–ด์“ฐ๋ฏ€๋กœ ์ ˆ๋Œ€ ํ˜ธ์ถœํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
170
  """
171
  bb = self.config.backbone_name_or_path
172
  meta = BACKBONE_META[bb]
 
180
  self._load_torchvision_pretrained_into_skeleton_(bb)
181
  return
182
 
183
+ # For transformers backbones, load a reference pretrained model and copy weights into our skeleton.
184
+ # transformers ๋ฐฑ๋ณธ์€ reference pretrained ๋ชจ๋ธ์„ ๋กœ๋“œํ•œ ๋’ค skeleton์— ๊ฐ€์ค‘์น˜๋ฅผ ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค.
185
  ref = AutoModel.from_pretrained(
186
  bb,
187
  low_cpu_mem_usage=low_cpu_mem_usage,
188
  device_map=device_map,
189
  )
190
+
191
+ # strict=False is used to tolerate harmless key differences across minor versions.
192
+ # strict=False๋Š” ๋งˆ์ด๋„ˆ ๋ฒ„์ „ ์ฐจ์ด๋กœ ์ธํ•œ ๋ฌดํ•ดํ•œ ํ‚ค ์ฐจ์ด๋ฅผ ํ—ˆ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
193
  self.backbone.load_state_dict(ref.state_dict(), strict=False)
194
  del ref
195
 
196
  @torch.no_grad()
197
  def _load_timm_pretrained_into_skeleton_(self, hf_repo_id: str):
198
+ # timm must be present for timm backbones.
199
+ # timm ๋ฐฑ๋ณธ์—๋Š” timm ์„ค์น˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
200
  import timm
201
+
202
+ # Create a pretrained reference model and copy its weights strictly.
203
+ # pretrained reference ๋ชจ๋ธ์„ ๋งŒ๋“ค๊ณ  ๊ฐ€์ค‘์น˜๋ฅผ strictํ•˜๊ฒŒ ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค.
204
  ref = timm.create_model(
205
  f"hf_hub:{hf_repo_id}",
206
  pretrained=True,
207
  num_classes=0,
208
  ).eval()
209
+
210
  self.backbone.load_state_dict(ref.state_dict(), strict=True)
211
  del ref
212
 
213
  @torch.no_grad()
214
  def _load_torchvision_pretrained_into_skeleton_(self, model_id: str):
215
+ # This project intentionally supports only torchvision/densenet121 in the 224 whitelist.
216
+ # ์ด ํ”„๋กœ์ ํŠธ๋Š” 224 ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ์—์„œ torchvision/densenet121๋งŒ ์˜๋„์ ์œผ๋กœ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.
217
+ if model_id != "torchvision/densenet121":
218
+ raise ValueError(f"Unsupported torchvision DenseNet id (224 whitelist only): {model_id}")
219
+
220
+ # Use torchvision's default pretrained weights for densenet121.
221
+ # torchvision์˜ densenet121 ๊ธฐ๋ณธ pretrained weights๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
222
+ ref = tv_models.densenet121(weights=tv_models.DenseNet121_Weights.DEFAULT).eval()
223
+
 
 
 
224
  self.backbone.load_state_dict(ref.state_dict(), strict=True)
225
  del ref
226
 
227
  # ----------------------------
228
  # feature extraction
229
+ # feature ์ถ”์ถœ
230
  # ----------------------------
231
  @staticmethod
232
  def _pool_or_gap(outputs) -> torch.Tensor:
233
+ # Some transformers vision CNNs provide pooler_output explicitly.
234
+ # ์ผ๋ถ€ transformers vision CNN์€ pooler_output์„ ๋ช…์‹œ์ ์œผ๋กœ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
235
  if hasattr(outputs, "pooler_output") and outputs.pooler_output is not None:
236
  x = outputs.pooler_output
237
  if x.dim() == 2:
 
240
  return x.flatten(1)
241
  raise RuntimeError(f"Unexpected pooler_output shape: {tuple(x.shape)}")
242
 
243
+ # Otherwise we expect a CNN-style last_hidden_state=(B,C,H,W) and apply GAP.
244
+ # ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด CNN ์Šคํƒ€์ผ last_hidden_state=(B,C,H,W)๋ฅผ ๊ธฐ๋Œ€ํ•˜๊ณ  GAP์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.
245
  x = outputs.last_hidden_state
246
  if x.dim() == 4:
247
  return x.mean(dim=(2, 3))
248
+
249
  raise RuntimeError(
250
  "Expected pooler_output or (B,C,H,W) last_hidden_state for CNN backbones. "
251
  f"Got last_hidden_state shape={tuple(x.shape)}"
252
  )
253
 
254
  def _extract_features(self, outputs, pixel_values: Optional[torch.Tensor] = None) -> torch.Tensor:
255
+ # Feature rule is defined by BACKBONE_META and must remain stable across saves/loads.
256
+ # feature ๊ทœ์น™์€ BACKBONE_META๋กœ ์ •์˜๋˜๋ฉฐ ์ €์žฅ/๋กœ๋“œ ๊ฐ„ ์•ˆ์ •์ ์œผ๋กœ ์œ ์ง€๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
257
  rule = self._meta["feat_rule"]
258
 
259
  if rule == "cls":
260
+ # ViT-style: use CLS token embedding from last_hidden_state.
261
+ # ViT ์Šคํƒ€์ผ: last_hidden_state์—์„œ CLS ํ† ํฐ ์ž„๋ฒ ๋”ฉ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
262
  return outputs.last_hidden_state[:, 0, :]
263
 
264
  if rule == "pool_or_mean":
265
+ # Swin-style: prefer pooler_output if present, else mean-pool over tokens.
266
+ # Swin ์Šคํƒ€์ผ: pooler_output์ด ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉํ•˜๊ณ , ์—†์œผ๋ฉด ํ† ํฐ ํ‰๊ท  ํ’€๋ง์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
267
  if hasattr(outputs, "pooler_output") and outputs.pooler_output is not None:
268
  return outputs.pooler_output
269
  return outputs.last_hidden_state.mean(dim=1)
270
 
271
  if rule == "pool_or_gap":
272
+ # CNN-style: use pooler_output if present, else GAP over spatial dims.
273
+ # CNN ์Šคํƒ€์ผ: pooler_output์ด ์žˆ์œผ๋ฉด ์‚ฌ์šฉํ•˜๊ณ , ์—†์œผ๋ฉด ๊ณต๊ฐ„ ์ฐจ์› GAP์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
274
  return self._pool_or_gap(outputs)
275
 
276
  if rule == "timm_gap":
277
+ # timm forward_features returns a feature map (B,C,H,W) which we GAP to (B,C).
278
+ # timm forward_features๋Š” (B,C,H,W) feature map์„ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ ์ด๋ฅผ GAP์œผ๋กœ (B,C)๋กœ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
279
  if not isinstance(outputs, torch.Tensor):
280
  raise TypeError(f"timm_gap expects Tensor features, got {type(outputs)}")
281
  if outputs.dim() != 4:
 
283
  return outputs.mean(dim=(2, 3))
284
 
285
  if rule == "torchvision_densenet_gap":
286
+ # torchvision DenseNet features are feature maps (B,C,H,W) and require GAP.
287
+ # torchvision DenseNet features๋Š” (B,C,H,W) feature map์ด๋ฉฐ GAP์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
288
  if not isinstance(outputs, torch.Tensor):
289
  raise TypeError(f"torchvision_densenet_gap expects Tensor, got {type(outputs)}")
290
  if outputs.dim() != 4:
 
302
  return_dict=True,
303
  **kwargs,
304
  ):
305
+ # Type decides the backbone forward path and output format.
306
+ # type์ด backbone forward ๊ฒฝ๋กœ ๋ฐ ์ถœ๋ ฅ ํฌ๋งท์„ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.
307
  t = self._meta["type"]
308
 
309
  if t == "timm_densenet":
310
+ # timm DenseNet consumes pixel_values as a 4D tensor (B,C,H,W).
311
+ # timm DenseNet์€ pixel_values๋ฅผ 4D ํ…์„œ (B,C,H,W)๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค.
312
  if pixel_values is None:
313
  raise ValueError("timm DenseNet backbone requires pixel_values.")
314
  if pixel_values.dim() != 4:
 
320
  attentions = None
321
 
322
  elif t == "torchvision_densenet":
323
+ # torchvision DenseNet consumes pixel_values as a 4D tensor (B,C,H,W).
324
+ # torchvision DenseNet์€ pixel_values๋ฅผ 4D ํ…์„œ (B,C,H,W)๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค.
325
  if pixel_values is None:
326
  raise ValueError("torchvision DenseNet backbone requires pixel_values.")
327
  if pixel_values.dim() != 4:
 
334
  attentions = None
335
 
336
  else:
337
+ # Transformers vision models are called with pixel_values and return ModelOutput.
338
+ # transformers vision ๋ชจ๋ธ์€ pixel_values๋กœ ํ˜ธ์ถœ๋˜๋ฉฐ ModelOutput์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
339
  outputs = self.backbone(
340
  pixel_values=pixel_values,
341
  output_attentions=output_attentions,
 
347
  hidden_states = getattr(outputs, "hidden_states", None)
348
  attentions = getattr(outputs, "attentions", None)
349
 
350
+ # Classifier consumes (B, feat_dim) and returns logits (B, num_labels).
351
+ # classifier๋Š” (B, feat_dim)์„ ๋ฐ›์•„ logits (B, num_labels)๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
352
  logits = self.classifier(feats)
353
 
354
  loss = None
355
  if labels is not None:
356
+ # Cross entropy expects labels as class indices in [0, num_labels).
357
+ # cross entropy๋Š” labels๊ฐ€ [0, num_labels) ๋ฒ”์œ„์˜ class index์ด๊ธธ ๊ธฐ๋Œ€ํ•ฉ๋‹ˆ๋‹ค.
358
  loss = F.cross_entropy(logits, labels)
359
 
360
  if not return_dict:
 
370
 
371
 
372
  # ============================================================
373
+ # Freeze / Unfreeze utilities
374
+ # Freeze / Unfreeze ์œ ํ‹ธ๋ฆฌํ‹ฐ
375
  # ============================================================
376
  def _set_requires_grad(module: nn.Module, flag: bool):
377
+ # Toggle requires_grad for all parameters in a module.
378
+ # ๋ชจ๋“ˆ์˜ ๋ชจ๋“  ํŒŒ๋ผ๋ฏธํ„ฐ์— ๋Œ€ํ•ด requires_grad๋ฅผ ํ† ๊ธ€ํ•ฉ๋‹ˆ๋‹ค.
379
  for p in module.parameters():
380
  p.requires_grad = flag
381
 
382
 
383
  def set_bn_eval(module: nn.Module):
384
+ # Put BatchNorm layers into eval mode to freeze running stats.
385
+ # BatchNorm ๋ ˆ์ด์–ด๋ฅผ eval ๋ชจ๋“œ๋กœ ๋‘์–ด running stats๋ฅผ ๊ณ ์ •ํ•ฉ๋‹ˆ๋‹ค.
386
  for m in module.modules():
387
  if isinstance(m, (nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d, nn.SyncBatchNorm)):
388
  m.eval()
389
 
390
 
391
  def freeze_backbone(model: BackboneWithMLPHeadForImageClassification, freeze_bn: bool = True):
392
+ # Stage1: freeze backbone and train only the head.
393
+ # stage1: backbone์„ freezeํ•˜๊ณ  head๋งŒ ํ•™์Šตํ•ฉ๋‹ˆ๋‹ค.
394
  _set_requires_grad(model.backbone, False)
395
  _set_requires_grad(model.classifier, True)
396
 
 
400
 
401
 
402
  def finetune_train_mode(model: BackboneWithMLPHeadForImageClassification, keep_bn_eval: bool = True):
403
+ # Stage2: train mode, optionally keeping BN layers in eval for stability.
404
+ # stage2: train ๋ชจ๋“œ๋กœ ๋‘๋˜ ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด BN์„ eval๋กœ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
405
  model.train()
406
  meta = BACKBONE_META[model.config.backbone_name_or_path]
407
  if keep_bn_eval and meta.get("has_bn", False):
 
409
 
410
 
411
  def trainable_summary(model: nn.Module):
412
+ # Print a compact summary of trainable parameters.
413
+ # ํ•™์Šต ๊ฐ€๋Šฅ ํŒŒ๋ผ๋ฏธํ„ฐ ์š”์•ฝ์„ ๊ฐ„๋‹จํžˆ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.
414
  total = sum(p.numel() for p in model.parameters())
415
  trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
416
  ratio = trainable / total if total > 0 else 0.0
 
423
  last_n: int = 2,
424
  keep_bn_eval: bool = True,
425
  ):
426
+ # This utility implements BACKBONE_META['unfreeze']=="last_n" across supported backbones.
427
+ # ์ด ์œ ํ‹ธ์€ ์ง€์› ๋ฐฑ๋ณธ๋“ค์— ๋Œ€ํ•ด BACKBONE_META['unfreeze']=="last_n"์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
428
  freeze_backbone(model, freeze_bn=keep_bn_eval)
429
 
430
  n = int(last_n)
 
438
  bb_type = meta["type"]
439
 
440
  if bb_type == "vit":
441
+ # ViT blocks live under backbone.encoder.layer in the transformers implementation.
442
+ # ViT ๋ธ”๋ก์€ transformers ๊ตฌํ˜„์—์„œ backbone.encoder.layer ์•„๋ž˜์— ์žˆ์Šต๋‹ˆ๋‹ค.
443
  blocks = list(model.backbone.encoder.layer)
444
  for blk in blocks[-n:]:
445
  _set_requires_grad(blk, True)
446
  return
447
 
448
  if bb_type == "swin":
449
+ # Swin blocks are nested by stages and blocks; we flatten and unfreeze last n blocks.
450
+ # Swin ๋ธ”๋ก์€ stage์™€ block์œผ๋กœ ์ค‘์ฒฉ๋˜์–ด ์žˆ์–ด ํŽผ์นœ ํ›„ ๋งˆ์ง€๋ง‰ n๊ฐœ๋ฅผ unfreeze ํ•ฉ๋‹ˆ๋‹ค.
451
  stages = list(model.backbone.encoder.layers)
452
  blocks: List[nn.Module] = []
453
  for st in stages:
 
457
  return
458
 
459
  if bb_type == "resnet":
460
+ # ResNet uses layer1..layer4 stages; we unfreeze at block granularity.
461
+ # ResNet์€ layer1..layer4 stage๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ block ๋‹จ์œ„๋กœ unfreeze ํ•ฉ๋‹ˆ๋‹ค.
462
  bb = model.backbone
463
  for name in ("layer1", "layer2", "layer3", "layer4"):
464
  if not hasattr(bb, name):
 
478
  return
479
 
480
  if bb_type == "efficientnet":
481
+ # EfficientNet in transformers exposes features; we unfreeze from the tail blocks.
482
+ # transformers EfficientNet์€ features๋ฅผ ๋…ธ์ถœํ•˜๋ฉฐ ๋’ค์ชฝ ๋ธ”๋ก๋ถ€ํ„ฐ unfreeze ํ•ฉ๋‹ˆ๋‹ค.
483
  bb = model.backbone
484
  if not hasattr(bb, "features"):
485
  raise RuntimeError("Unexpected EfficientNet structure: missing features")
 
496
  return
497
 
498
  if bb_type in ("timm_densenet", "torchvision_densenet"):
499
+ # DenseNet exposes a .features module with named blocks; we unfreeze last n submodules.
500
+ # DenseNet์€ .features ๋ชจ๋“ˆ์— ๋ธ”๋ก๋“ค์ด ์ด๋ฆ„์œผ๋กœ ์กด์žฌํ•˜๋ฉฐ ๋งˆ์ง€๋ง‰ n๊ฐœ ์„œ๋ธŒ๋ชจ๋“ˆ์„ unfreeze ํ•ฉ๋‹ˆ๋‹ค.
501
  bb = model.backbone
502
  if not hasattr(bb, "features"):
503
  raise RuntimeError("Unexpected DenseNet: missing features")
 
515
  raise RuntimeError(f"Unexpected DenseNet features: missing {name}")
516
 
517
  def _denselayers(db: nn.Module) -> List[nn.Module]:
518
+ # Dense blocks contain multiple DenseLayer children; we return them for fine-grained unfreezing.
519
+ # denseblock์€ DenseLayer ์ž์‹๋“ค์„ ๊ฐ€์ง€๋ฏ€๋กœ ์„ธ๋ฐ€ํ•œ unfreeze๋ฅผ ์œ„ํ•ด ์ด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
520
  return list(db.children())
521
 
522
  blocks: List[nn.Module] = []
 
538
 
539
  # -------------------------
540
  # register
541
+ # register
542
+ # -------------------------
543
+ # Register for AutoModelForImageClassification so from_pretrained can resolve this custom class.
544
+ # from_pretrained๊ฐ€ ์ด ์ปค์Šคํ…€ ํด๋ž˜์Šค๋ฅผ ํ•ด์„ํ•  ์ˆ˜ ์žˆ๋„๋ก AutoModelForImageClassification์— ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.
545
  BackboneWithMLPHeadForImageClassification.register_for_auto_class("AutoModelForImageClassification")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
models/google__vit-base-patch16-224/ds_proc.py ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # src/ds_proc.py
5
+
6
+ # ============================================================
7
+ # (4) ImageProcessor (AutoImageProcessor integration)
8
+ # (4) ImageProcessor (AutoImageProcessor ์—ฐ๋™)
9
+ # ============================================================
10
+
11
+ from typing import Any
12
+
13
+ import numpy as np
14
+ import torch
15
+ from transformers import AutoImageProcessor, AutoConfig
16
+ from transformers.image_processing_base import ImageProcessingMixin
17
+ from transformers.utils.generic import TensorType
18
+
19
+ from ds_meta import BackboneID, BACKBONE_META
20
+
21
+
22
+ class BackboneMLPHead224ImageProcessor(ImageProcessingMixin):
23
+ """
24
+ This processor performs image preprocessing and outputs {"pixel_values": ...}.
25
+ ์ด processor๋Š” ์ด๋ฏธ์ง€ ์ „์ฒ˜๋ฆฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ณ  {"pixel_values": ...}๋ฅผ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.
26
+
27
+ Key requirements:
28
+ ํ•ต์‹ฌ ์š”๊ตฌ์‚ฌํ•ญ:
29
+
30
+ 1) save_pretrained() must produce a JSON-serializable preprocessor_config.json.
31
+ save_pretrained()๋Š” JSON ์ง๋ ฌํ™” ๊ฐ€๋Šฅํ•œ preprocessor_config.json์„ ์ƒ์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
32
+
33
+ 2) Runtime-only objects (delegate processor, timm/torchvision transforms) must NOT be serialized.
34
+ ๋Ÿฐํƒ€์ž„ ๊ฐ์ฒด(delegate processor, timm/torchvision transform)๋Š” ์ ˆ๋Œ€ ์ง๋ ฌํ™”ํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
35
+
36
+ 3) Runtime objects are rebuilt at init/load time based on backbone meta.
37
+ ๋Ÿฐํƒ€์ž„ ๊ฐ์ฒด๋Š” backbone meta์— ๋”ฐ๋ผ init/load ์‹œ์ ์— ์žฌ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค.
38
+ """
39
+
40
+ # HF vision models conventionally expect "pixel_values" as the primary input key.
41
+ # HF vision ๋ชจ๋ธ์€ ๊ด€๋ก€์ ์œผ๋กœ ์ž…๋ ฅ ํ‚ค๋กœ "pixel_values"๋ฅผ ๊ธฐ๋Œ€ํ•ฉ๋‹ˆ๋‹ค.
42
+ model_input_names = ["pixel_values"]
43
+
44
+ def __init__(self, backbone_name_or_path: BackboneID, is_training: bool = False, **kwargs):
45
+ # ImageProcessingMixin stores extra kwargs and manages auto_map metadata.
46
+ # ImageProcessingMixin์€ ์ถ”๊ฐ€ kwargs๋ฅผ ์ €์žฅํ•˜๊ณ  auto_map ๋ฉ”ํƒ€๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
47
+ super().__init__(**kwargs)
48
+
49
+ # Enforce whitelist via BACKBONE_META to keep behavior stable.
50
+ # ๋™์ž‘ ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด BACKBONE_META ๊ธฐ๋ฐ˜ ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค.
51
+ if backbone_name_or_path not in BACKBONE_META:
52
+ raise ValueError(
53
+ f"Unsupported backbone_name_or_path={backbone_name_or_path}. "
54
+ f"Allowed: {sorted(BACKBONE_META.keys())}"
55
+ )
56
+
57
+ # Serializable fields only: these should appear in preprocessor_config.json.
58
+ # ์ง๋ ฌํ™” ๊ฐ€๋Šฅํ•œ ํ•„๋“œ๋งŒ: ์ด ๊ฐ’๋“ค๋งŒ preprocessor_config.json์— ๋“ค์–ด๊ฐ€์•ผ ํ•ฉ๋‹ˆ๋‹ค.
59
+ self.backbone_name_or_path = backbone_name_or_path
60
+ self.is_training = bool(is_training)
61
+
62
+ # Runtime-only fields: must never be serialized.
63
+ # ๋Ÿฐํƒ€์ž„ ์ „์šฉ ํ•„๋“œ: ์ ˆ๋Œ€ ์ง๋ ฌํ™”๋˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
64
+ self._meta = None
65
+ self._delegate = None
66
+ self._timm_transform = None
67
+ self._torchvision_transform = None
68
+
69
+ # Build runtime objects according to backbone type.
70
+ # backbone type์— ๋”ฐ๋ผ ๋Ÿฐํƒ€์ž„ ๊ฐ์ฒด๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
71
+ self._build_runtime()
72
+
73
+ # ============================================================
74
+ # Runtime builders
75
+ # ๋Ÿฐํƒ€์ž„ ๋นŒ๋”
76
+ # ============================================================
77
+ def _build_runtime(self):
78
+ """
79
+ Build runtime delegate/transform based on BACKBONE_META["type"].
80
+ BACKBONE_META["type"]์— ๋”ฐ๋ผ ๋Ÿฐํƒ€์ž„ delegate/transform์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
81
+ """
82
+ meta = BACKBONE_META[self.backbone_name_or_path]
83
+ self._meta = meta
84
+
85
+ # Always reset runtime fields before rebuilding.
86
+ # ์žฌ๊ตฌ์„ฑ ์ „ ๋Ÿฐํƒ€์ž„ ํ•„๋“œ๋Š” ํ•ญ์ƒ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
87
+ self._delegate = None
88
+ self._timm_transform = None
89
+ self._torchvision_transform = None
90
+
91
+ t = meta["type"]
92
+
93
+ if t == "timm_densenet":
94
+ # timm DenseNet uses timm.data transforms for ImageNet-style preprocessing.
95
+ # timm DenseNet์€ ImageNet ์ „์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด timm.data transform์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
96
+ self._timm_transform = self._build_timm_transform(
97
+ backbone_id=self.backbone_name_or_path,
98
+ is_training=self.is_training,
99
+ )
100
+ return
101
+
102
+ if t == "torchvision_densenet":
103
+ # torchvision DenseNet requires torchvision-style preprocessing (resize/crop/tensor/normalize).
104
+ # torchvision DenseNet์€ torchvision ์Šคํƒ€์ผ ์ „์ฒ˜๋ฆฌ(resize/crop/tensor/normalize)๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
105
+ self._torchvision_transform = self._build_torchvision_densenet_transform(
106
+ is_training=self.is_training
107
+ )
108
+ return
109
+
110
+ # Default: transformers backbone delegates to its official AutoImageProcessor.
111
+ # ๊ธฐ๋ณธ: transformers ๋ฐฑ๋ณธ์€ ๊ณต์‹ AutoImageProcessor์— ์œ„์ž„ํ•ฉ๋‹ˆ๋‹ค.
112
+ self._delegate = AutoImageProcessor.from_pretrained(self.backbone_name_or_path)
113
+
114
+ @staticmethod
115
+ def _build_timm_transform(*, backbone_id: str, is_training: bool):
116
+ """
117
+ Create timm transform without storing non-serializable objects in config.
118
+ ๋น„์ง๋ ฌํ™” ๊ฐ์ฒด๋ฅผ config์— ์ €์žฅํ•˜์ง€ ์•Š๊ณ  timm transform์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
119
+ """
120
+ try:
121
+ import timm
122
+ from timm.data import resolve_model_data_config, create_transform
123
+ except Exception as e:
124
+ raise ImportError(
125
+ "timm backbone processor requires `timm`. Install: pip install timm"
126
+ ) from e
127
+
128
+ # We only need model metadata to resolve data config, so pretrained=False is preferred.
129
+ # data config ์ถ”์ถœ๋งŒ ํ•„์š”ํ•˜๋ฏ€๋กœ pretrained=False๋ฅผ ์šฐ์„  ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
130
+ m = timm.create_model(f"hf_hub:{backbone_id}", pretrained=False, num_classes=0)
131
+ dc = resolve_model_data_config(m)
132
+
133
+ # create_transform returns a torchvision-like callable that maps PIL -> torch.Tensor(C,H,W).
134
+ # create_transform์€ PIL -> torch.Tensor(C,H,W)๋กœ ๋งคํ•‘ํ•˜๋Š” callable์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
135
+ tfm = create_transform(**dc, is_training=is_training)
136
+ return tfm
137
+
138
+ @staticmethod
139
+ def _build_torchvision_densenet_transform(*, is_training: bool):
140
+ """
141
+ Build torchvision preprocessing for DenseNet-121 (224 pipeline).
142
+ DenseNet-121์šฉ torchvision ์ „์ฒ˜๋ฆฌ(224 ํŒŒ์ดํ”„๋ผ์ธ)๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
143
+ """
144
+ try:
145
+ from torchvision import transforms
146
+ except Exception as e:
147
+ raise ImportError(
148
+ "torchvision DenseNet processor requires `torchvision`. Install: pip install torchvision"
149
+ ) from e
150
+
151
+ # These are the standard ImageNet normalization stats used by torchvision weights.
152
+ # ์ด ๊ฐ’๋“ค์€ torchvision weights๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” ํ‘œ์ค€ ImageNet ์ •๊ทœํ™” ํ†ต๊ณ„์ž…๋‹ˆ๋‹ค.
153
+ mean = (0.485, 0.456, 0.406)
154
+ std = (0.229, 0.224, 0.225)
155
+
156
+ # Training pipeline typically uses RandomResizedCrop and horizontal flip.
157
+ # ํ•™์Šต ํŒŒ์ดํ”„๋ผ์ธ์€ ๋ณดํ†ต RandomResizedCrop๊ณผ ์ขŒ์šฐ๋ฐ˜์ „์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
158
+ if is_training:
159
+ return transforms.Compose(
160
+ [
161
+ transforms.RandomResizedCrop(224),
162
+ transforms.RandomHorizontalFlip(p=0.5),
163
+ transforms.ToTensor(),
164
+ transforms.Normalize(mean=mean, std=std),
165
+ ]
166
+ )
167
+
168
+ # Inference pipeline typically uses Resize(256) + CenterCrop(224).
169
+ # ์ถ”๋ก  ํŒŒ์ดํ”„๋ผ์ธ์€ ๋ณดํ†ต Resize(256) + CenterCrop(224)๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
170
+ return transforms.Compose(
171
+ [
172
+ transforms.Resize(256),
173
+ transforms.CenterCrop(224),
174
+ transforms.ToTensor(),
175
+ transforms.Normalize(mean=mean, std=std),
176
+ ]
177
+ )
178
+
179
+ # ============================================================
180
+ # Serialization
181
+ # ์ง๋ ฌํ™”
182
+ # ============================================================
183
+ def to_dict(self) -> dict[str, Any]:
184
+ """
185
+ Return a JSON-serializable dict for preprocessor_config.json.
186
+ preprocessor_config.json์— ๋“ค์–ด๊ฐˆ JSON ์ง๋ ฌํ™” dict๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
187
+
188
+ Important: do not leak runtime objects into the serialized dict.
189
+ ์ค‘์š”: ๋Ÿฐํƒ€์ž„ ๊ฐ์ฒด๊ฐ€ ์ง๋ ฌํ™” dict์— ์„ž์ด๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
190
+ """
191
+ # ImageProcessingMixin.to_dict() adds metadata such as image_processor_type/auto_map.
192
+ # ImageProcessingMixin.to_dict()๋Š” image_processor_type/auto_map ๊ฐ™์€ ๋ฉ”ํƒ€๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
193
+ d = super().to_dict()
194
+
195
+ # Force minimal stable fields for long-term compatibility.
196
+ # ์žฅ๊ธฐ ํ˜ธํ™˜์„ ์œ„ํ•ด ์ตœ์†Œ ์•ˆ์ • ํ•„๋“œ๋ฅผ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค.
197
+ d["image_processor_type"] = self.__class__.__name__
198
+ d["backbone_name_or_path"] = self.backbone_name_or_path
199
+ d["is_training"] = self.is_training
200
+
201
+ # Remove any runtime-only fields defensively.
202
+ # ๋Ÿฐํƒ€์ž„ ์ „์šฉ ํ•„๋“œ๋Š” ๋ณด์ˆ˜์ ์œผ๋กœ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.
203
+ for key in ["_meta", "_delegate", "_timm_transform", "_torchvision_transform"]:
204
+ d.pop(key, None)
205
+
206
+ return d
207
+
208
+ @classmethod
209
+ def from_dict(cls, image_processor_dict: dict[str, Any], **kwargs):
210
+ """
211
+ Standard load path used by BaseImageProcessor / AutoImageProcessor.
212
+ BaseImageProcessor / AutoImageProcessor๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” ํ‘œ์ค€ ๋กœ๋“œ ๊ฒฝ๋กœ์ž…๋‹ˆ๋‹ค.
213
+ """
214
+ backbone = image_processor_dict.get("backbone_name_or_path", None)
215
+ if backbone is None:
216
+ raise ValueError("preprocessor_config.json missing key: backbone_name_or_path")
217
+
218
+ is_training = bool(image_processor_dict.get("is_training", False))
219
+ return cls(backbone_name_or_path=backbone, is_training=is_training, **kwargs)
220
+
221
+ @classmethod
222
+ def from_pretrained(cls, pretrained_model_name_or_path: str, **kwargs):
223
+ """
224
+ Fallback path if AutoImageProcessor calls class.from_pretrained directly.
225
+ AutoImageProcessor๊ฐ€ class.from_pretrained๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋Š” ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•œ ๊ฒฝ๋กœ์ž…๋‹ˆ๋‹ค.
226
+
227
+ Strategy:
228
+ ์ „๋žต:
229
+
230
+ - Read config.json via AutoConfig and recover backbone_name_or_path.
231
+ AutoConfig๋กœ config.json์„ ์ฝ๊ณ  backbone_name_or_path๋ฅผ ๋ณต๊ตฌํ•ฉ๋‹ˆ๋‹ค.
232
+ """
233
+ cfg = AutoConfig.from_pretrained(pretrained_model_name_or_path, **kwargs)
234
+ backbone = getattr(cfg, "backbone_name_or_path", None)
235
+ if backbone is None:
236
+ raise ValueError("Cannot build processor: backbone_name_or_path not found in config.json")
237
+
238
+ # is_training is runtime-only and should default to False for inference/serving.
239
+ # is_training์€ ๋Ÿฐํƒ€์ž„ ์ „์šฉ์ด๋ฉฐ ์ถ”๋ก /์„œ๋น™ ๊ธฐ๋ณธ๊ฐ’์€ False๊ฐ€ ๋งž์Šต๋‹ˆ๋‹ค.
240
+ return cls(backbone_name_or_path=backbone, is_training=False)
241
+
242
+ # ============================================================
243
+ # Call interface
244
+ # ํ˜ธ์ถœ ์ธํ„ฐํŽ˜์ด์Šค
245
+ # ============================================================
246
+ @staticmethod
247
+ def _ensure_list(images: Any) -> list[Any]:
248
+ # Normalize scalar image input to a list for uniform processing.
249
+ # ๋‹จ์ผ ์ž…๋ ฅ์„ ๋ฆฌ์ŠคํŠธ๋กœ ์ •๊ทœํ™”ํ•˜์—ฌ ๋™์ผํ•œ ์ฒ˜๋ฆฌ ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
250
+ if isinstance(images, (list, tuple)):
251
+ return list(images)
252
+ return [images]
253
+
254
+ @staticmethod
255
+ def _to_pil_rgb(x: Any):
256
+ # Convert common image inputs into PIL RGB images.
257
+ # ์ผ๋ฐ˜์ ์ธ ์ž…๋ ฅ์„ PIL RGB ์ด๋ฏธ์ง€๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
258
+ from PIL import Image as PILImage
259
+
260
+ if isinstance(x, PILImage.Image):
261
+ return x.convert("RGB")
262
+ if isinstance(x, np.ndarray) and x.ndim == 3:
263
+ return PILImage.fromarray(x).convert("RGB")
264
+ raise TypeError(f"Unsupported image type: {type(x)}")
265
+
266
+ def __call__(
267
+ self,
268
+ images: Any | list[Any],
269
+ return_tensors: str | TensorType | None = "pt",
270
+ **kwargs,
271
+ ) -> dict[str, Any]:
272
+ """
273
+ Convert images into {"pixel_values": Tensor/ndarray}.
274
+ ์ด๋ฏธ์ง€๋ฅผ {"pixel_values": Tensor/ndarray}๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
275
+ """
276
+ images = self._ensure_list(images)
277
+
278
+ # Rebuild runtime if needed (e.g., right after deserialization).
279
+ # ์ง๋ ฌํ™” ๋ณต์› ์งํ›„ ๋“ฑ ๋Ÿฐํƒ€์ž„์ด ๋น„์–ด์žˆ์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์žฌ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
280
+ if (self._delegate is None) and (self._timm_transform is None) and (self._torchvision_transform is None):
281
+ self._build_runtime()
282
+
283
+ # timm path: PIL -> torch.Tensor(C,H,W) normalized float32.
284
+ # timm ๊ฒฝ๋กœ: PIL -> torch.Tensor(C,H,W) ์ •๊ทœํ™” float32์ž…๋‹ˆ๋‹ค.
285
+ if self._timm_transform is not None:
286
+ pv: list[torch.Tensor] = []
287
+ for im in images:
288
+ pil = self._to_pil_rgb(im)
289
+ t = self._timm_transform(pil)
290
+ if not isinstance(t, torch.Tensor):
291
+ raise RuntimeError("Unexpected timm transform output (expected torch.Tensor).")
292
+ pv.append(t)
293
+ pixel_values = torch.stack(pv, dim=0) # (B,C,H,W)
294
+ return self._format_return(pixel_values, return_tensors)
295
+
296
+ # torchvision path: PIL -> torch.Tensor(C,H,W) normalized float32.
297
+ # torchvision ๊ฒฝ๋กœ: PIL -> torch.Tensor(C,H,W) ์ •๊ทœํ™” float32์ž…๋‹ˆ๋‹ค.
298
+ if self._torchvision_transform is not None:
299
+ pv: list[torch.Tensor] = []
300
+ for im in images:
301
+ pil = self._to_pil_rgb(im)
302
+ t = self._torchvision_transform(pil)
303
+ if not isinstance(t, torch.Tensor):
304
+ raise RuntimeError("Unexpected torchvision transform output (expected torch.Tensor).")
305
+ pv.append(t)
306
+ pixel_values = torch.stack(pv, dim=0) # (B,C,H,W)
307
+ return self._format_return(pixel_values, return_tensors)
308
+
309
+ # transformers delegate path: rely on official processor behavior.
310
+ # transformers ์œ„์ž„ ๊ฒฝ๋กœ: ๊ณต์‹ processor ๋™์ž‘์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
311
+ if self._delegate is None:
312
+ raise RuntimeError("Processor runtime not built: delegate is None and no transforms are available.")
313
+
314
+ return self._delegate(images, return_tensors=return_tensors, **kwargs)
315
+
316
+ @staticmethod
317
+ def _format_return(pixel_values: torch.Tensor, return_tensors: str | TensorType | None) -> dict[str, Any]:
318
+ """
319
+ Format pixel_values according to return_tensors.
320
+ return_tensors์— ๋งž์ถฐ pixel_values ๋ฐ˜ํ™˜ ํฌ๋งท์„ ๋งž์ถฅ๋‹ˆ๋‹ค.
321
+ """
322
+ if return_tensors is None or return_tensors in ("pt", TensorType.PYTORCH):
323
+ return {"pixel_values": pixel_values}
324
+ if return_tensors in ("np", TensorType.NUMPY):
325
+ return {"pixel_values": pixel_values.detach().cpu().numpy()}
326
+ raise ValueError(f"Unsupported return_tensors={return_tensors}. Use 'pt' or 'np'.")
327
+
328
+
329
+ # Register this processor for AutoImageProcessor resolution.
330
+ # AutoImageProcessor ํ•ด์„์„ ์œ„ํ•ด ์ด processor๋ฅผ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.
331
+ if __name__ != "__main__":
332
+ BackboneMLPHead224ImageProcessor.register_for_auto_class("AutoImageProcessor")
models/google__vit-base-patch16-224/model.safetensors CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:3d8f30959f7f4875a6e7792d2e5d27ec06ea99c166fbe9bdb9aab09173fa784d
3
  size 346372132
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5ca403809fcb589b9b176f4712c6f6aa3c31d2cb7465aaaa6cd4391d46947e8e
3
  size 346372132
models/google__vit-base-patch16-224/preprocessor_config.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "auto_map": {
3
+ "AutoImageProcessor": "ds_proc.BackboneMLPHead224ImageProcessor"
4
+ },
5
+ "backbone_name_or_path": "google/vit-base-patch16-224",
6
+ "image_processor_type": "BackboneMLPHead224ImageProcessor",
7
+ "is_training": false
8
+ }
models/microsoft__resnet-50/config.json CHANGED
@@ -24,10 +24,11 @@
24
  "num_labels": 3,
25
  "transformers_version": "5.1.0",
26
  "ds_provenance": {
27
- "created_at": "20260210_050905",
28
  "repo_id": "dsaint31/bb_mlp_224",
29
  "subdir": "models/microsoft__resnet-50",
30
  "wrapper_class": "BackboneWithMLPHeadForImageClassification",
 
31
  "backbone_id": "microsoft/resnet-50",
32
  "pretrained_injected": true,
33
  "policy_init_pretrained_forbidden": true,
 
24
  "num_labels": 3,
25
  "transformers_version": "5.1.0",
26
  "ds_provenance": {
27
+ "created_at": "20260210_111913",
28
  "repo_id": "dsaint31/bb_mlp_224",
29
  "subdir": "models/microsoft__resnet-50",
30
  "wrapper_class": "BackboneWithMLPHeadForImageClassification",
31
+ "processor_class": "BackboneMLPHead224ImageProcessor",
32
  "backbone_id": "microsoft/resnet-50",
33
  "pretrained_injected": true,
34
  "policy_init_pretrained_forbidden": true,
models/microsoft__resnet-50/ds_cfg.py CHANGED
@@ -1,84 +1,175 @@
1
- from typing import Literal, Union
2
-
3
- from transformers import AutoConfig
4
  from transformers import PretrainedConfig
 
5
  from ds_meta import BackboneID, BACKBONE_META
6
 
7
- # ============================================================
8
- # 3) Config
9
- # ============================================================
10
  class BackboneMLPHeadConfig(PretrainedConfig):
11
  """
12
- backbone_name_or_path๋Š” BACKBONE_META ํ‚ค(ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ)๋งŒ ํ—ˆ์šฉ.
13
-
14
- ์ค‘์š”ํ•œ ์ 
15
- ----------
16
- - transformers ๋‚ด๋ถ€ ๊ฒฝ๋กœ์—์„œ "๋ฌด์ธ์ž ์ƒ์„ฑ"์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ,
17
- backbone_name_or_path=None์ธ ๊ฒฝ์šฐ๋Š” ๊ฒ€์ฆ ์—†์ด ๊ธฐ๋ณธ๊ฐ’๋งŒ ์„ธํŒ…ํ•˜๊ณ  return
18
- ํ•˜๋Š” ๊ตฌํ˜„์ด ๋ฐ˜๋“œ์‹œ ํ•„์š”ํ•จ.
19
- - ๋‹จ, ์‹ค์ œ ์‚ฌ์šฉ์—์„œ๋Š” backbone_name_or_path๋ฅผ ๋ฐ˜๋“œ์‹œ ์ง€์ •ํ•ด์„œ ์ƒ์„ฑํ•ด์•ผ ํ•จ.
20
  """
21
 
22
- # AutoConfig๊ฐ€ ์ด ๋ชจ๋ธ์„ ์‹๋ณ„ํ•˜๋Š”๋ฐ ์‚ฌ์šฉํ•˜๋Š” ๋ฌธ์ž์—ด ID.
 
23
  model_type = "backbone-mlphead-224-fixed"
24
 
25
- # ์ƒ์„ฑ์ž์—์„œ ๋ชจ๋“  ์ธ์ž์— ๊ธฐ๋ณธ๊ฐ’ ๋ถ€์—ฌ ๋ฐ˜๋“œ์‹œ ํ•„์š”!
26
  def __init__(
27
  self,
28
  backbone_name_or_path: BackboneID | None = None,
29
  mlp_head_bottleneck: int = 256,
30
  mlp_head_dropout: float = 0.2,
31
- label2id: dict[str, int]|None = None,
32
- id2label: dict[int, str]|None = None,
33
  **kwargs,
34
  ):
35
- # ๋ฌด์ธ์ž ์ƒ์„ฑ ๊ฒฝ๋กœ ๋ฐฉ์–ด
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  if backbone_name_or_path is None:
 
 
 
 
 
 
 
37
  super().__init__(**kwargs)
 
 
 
38
  self.backbone_name_or_path = None
39
- self.mlp_head_bottleneck = int(mlp_head_bottleneck)
40
- self.mlp_head_dropout = float(mlp_head_dropout)
41
- # num_labels๋„ ๊ธฐ๋ณธ๊ฐ’ ํ•˜๋‚˜ ์ค˜์„œ 0 ๋ฐฉ์ง€(3์œผ๋กœ ๋‘๋“  0์œผ๋กœ ๋‘๋“  ์ •์ฑ…)
42
- # ์ตœ์†Œ ์•ˆ์ „: id2label์ด ์žˆ์œผ๋ฉด ๊ทธ ๊ธธ์ด, ์—†์œผ๋ฉด 0
 
 
 
 
 
 
 
43
  return
44
 
45
- # ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๊ฒ€์ฆ
 
 
 
 
 
 
 
 
46
  if backbone_name_or_path not in BACKBONE_META:
47
  raise ValueError(
48
  f"Unsupported backbone_name_or_path={backbone_name_or_path}. "
49
  f"Allowed: {sorted(BACKBONE_META.keys())}"
50
  )
51
- if id2label is None and label2id is None:
52
- raise ValueError("id2label/label2id ๋‘˜ ์ค‘ ํ•˜๋‚˜๋Š” ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.")
53
- if id2label is None:
54
- id2label = {v: k for k, v in label2id.items()}
55
- if label2id is None:
56
- label2id = {v: k for k, v in id2label.items()}
57
-
58
- num_labels = len(id2label)
59
- # kwargs์— ์ด๋ฏธ num_labels๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ
60
- if "num_labels" not in kwargs:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  kwargs["num_labels"] = num_labels
62
 
 
 
 
 
 
 
63
  super().__init__(
64
- label2id=label2id,
65
- id2label=id2label,
66
  **kwargs,
67
  )
68
-
69
- # 4) ์ค‘์š”: ๋ถ€๋ชจ ํ•„๋“œ์—๋„ num_labels ๋ฐ˜์˜(์ €์žฅ/๋กœ๋“œ ์•ˆ์ •)
70
- self.num_labels = num_labels
71
- self.label2id = label2id
72
- self.id2label = id2label
73
 
 
 
 
 
 
 
74
  self.backbone_name_or_path = backbone_name_or_path
75
- self.mlp_head_bottleneck = int(mlp_head_bottleneck)
76
- self.mlp_head_dropout = float(mlp_head_dropout)
 
 
 
 
77
 
78
  def to_dict(self):
 
 
79
  output = super().to_dict()
80
- # ๋ช…์‹œ์ ์œผ๋กœ ํฌํ•จ
81
- output["num_labels"] = self.num_labels
 
 
 
 
82
  return output
83
 
 
 
 
84
  BackboneMLPHeadConfig.register_for_auto_class("AutoConfig")
 
 
 
 
1
  from transformers import PretrainedConfig
2
+
3
  from ds_meta import BackboneID, BACKBONE_META
4
 
5
+
 
 
6
  class BackboneMLPHeadConfig(PretrainedConfig):
7
  """
8
+ Configuration for Backbone + MLP Head models.
9
+
10
+ Backbone + MLP Head ๋ชจ๋ธ์„ ์œ„ํ•œ ์„ค์ • ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.
 
 
 
 
 
11
  """
12
 
13
+ # This string is used by Hugging Face AutoConfig to identify the config class.
14
+ # Hugging Face AutoConfig๊ฐ€ ์ด config๋ฅผ ์‹๋ณ„ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” ๊ณ ์œ  ID์ž…๋‹ˆ๋‹ค.
15
  model_type = "backbone-mlphead-224-fixed"
16
 
 
17
  def __init__(
18
  self,
19
  backbone_name_or_path: BackboneID | None = None,
20
  mlp_head_bottleneck: int = 256,
21
  mlp_head_dropout: float = 0.2,
22
+ label2id: dict[str, int] | None = None,
23
+ id2label: dict[int, str] | None = None,
24
  **kwargs,
25
  ):
26
+ # ============================================================
27
+ # 0) Guard for argument-less construction
28
+ # 0) ๋ฌด์ธ์ž ์ƒ์„ฑ ๊ฒฝ๋กœ ๋ฐฉ์–ด
29
+ # ============================================================
30
+ # Transformers may internally construct this config without arguments
31
+ # (e.g., during AutoConfig resolution or Hub loading).
32
+ # Transformers ๋‚ด๋ถ€์—์„œ AutoConfig ๋˜๋Š” Hub ๋กœ๋“œ ๊ณผ์ • ์ค‘
33
+ # ์ธ์ž ์—†์ด config๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ฒฝ๋กœ๊ฐ€ ์‹ค์ œ๋กœ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.
34
+ #
35
+ # In this case, we must NOT validate or raise errors.
36
+ # ์ด ๊ฒฝ์šฐ ๊ฒ€์ฆ์ด๋‚˜ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
37
+ #
38
+ # The goal here is to provide a minimal, serialization-safe config.
39
+ # ๋ชฉํ‘œ๋Š” ์ตœ์†Œํ•œ์˜ ๊ฐ’๋งŒ ์ฑ„์›Œ ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™”๊ฐ€ ๊นจ์ง€์ง€ ์•Š๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.
40
  if backbone_name_or_path is None:
41
+ # num_labels may be implicitly assumed by downstream code,
42
+ # so we explicitly set a safe default.
43
+ # num_labels๋Š” downstream ์ฝ”๋“œ์—์„œ ์•”๋ฌต์ ์œผ๋กœ ๊ฐ€์ •๋˜๋ฏ€๋กœ
44
+ # ์•ˆ์ „ํ•œ ๊ธฐ๋ณธ๊ฐ’์„ ๋ช…์‹œ์ ์œผ๋กœ ๋„ฃ์–ด์ค๋‹ˆ๋‹ค.
45
+ if "num_labels" not in kwargs:
46
+ kwargs["num_labels"] = 0
47
+
48
  super().__init__(**kwargs)
49
+
50
+ # Backbone is intentionally unset in this path.
51
+ # ์ด ๊ฒฝ๋กœ์—์„œ๋Š” backbone์„ ์˜๋„์ ์œผ๋กœ ์„ค์ •ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
52
  self.backbone_name_or_path = None
53
+
54
+ # Store MLP head hyperparameters for completeness.
55
+ # MLP head ๊ด€๋ จ ํ•˜์ดํผํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ํ˜•ํƒœ ์œ ์ง€๋ฅผ ์œ„ํ•ด ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
56
+ self.mlp_head_bottleneck = int(mlp_head_bottleneck)
57
+ self.mlp_head_dropout = float(mlp_head_dropout)
58
+
59
+ # Empty label mappings ensure stable save/load behavior.
60
+ # ๋นˆ label ๋งคํ•‘์€ ์ €์žฅ/๋กœ๋“œ ์•ˆ์ •์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.
61
+ self.label2id = {}
62
+ self.id2label = {}
63
+ self.num_labels = int(kwargs.get("num_labels", 0))
64
  return
65
 
66
+ # ============================================================
67
+ # 1) Backbone whitelist validation
68
+ # 1) Backbone ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๊ฒ€์ฆ
69
+ # ============================================================
70
+ # Only backbones explicitly registered in BACKBONE_META are allowed.
71
+ # BACKBONE_META์— ๋“ฑ๋ก๋œ backbone๋งŒ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.
72
+ #
73
+ # This prevents accidental usage of unsupported or inconsistent backbones.
74
+ # ์ง€์›๋˜์ง€ ์•Š๊ฑฐ๋‚˜ ๋ถˆ์ผ์น˜ํ•œ backbone ์‚ฌ์šฉ์„ ์›์ฒœ ์ฐจ๋‹จํ•ฉ๋‹ˆ๋‹ค.
75
  if backbone_name_or_path not in BACKBONE_META:
76
  raise ValueError(
77
  f"Unsupported backbone_name_or_path={backbone_name_or_path}. "
78
  f"Allowed: {sorted(BACKBONE_META.keys())}"
79
  )
80
+
81
+ # ============================================================
82
+ # 2) Label mapping normalization
83
+ # 2) ๋ผ๋ฒจ ๋งคํ•‘ ์ •๊ทœํ™”
84
+ # ============================================================
85
+ # Both label2id and id2label may be None during pure loading scenarios.
86
+ # ๋‹จ์ˆœ ๋กœ๋“œ(from_pretrained) ์‹œ label2id/id2label์ด None์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
87
+ #
88
+ # We allow this here to keep Hub loading and verification stable.
89
+ # Hub ๋กœ๋“œ ๋ฐ ๊ฒ€์ฆ ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด ์—ฌ๊ธฐ์„œ๋Š” ์ด๋ฅผ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.
90
+ #
91
+ # Fail-fast validation should happen at model or training level instead.
92
+ # ์‹ค์ œ ์‚ฌ์šฉ ๊ฒ€์ฆ์€ ๋ชจ๋ธ ๋˜๋Š” ํ•™์Šต ๋‹จ๊ณ„์—์„œ fail-fast๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
93
+ if label2id is None and id2label is None:
94
+ # Respect num_labels if explicitly provided, otherwise default to 0.
95
+ # num_labels๊ฐ€ ์ฃผ์–ด์กŒ๋‹ค๋ฉด ์กด์ค‘ํ•˜๊ณ , ์•„๋‹ˆ๋ฉด 0์œผ๋กœ ๋‘ก๋‹ˆ๋‹ค.
96
+ num_labels = int(kwargs.get("num_labels", 0))
97
+ label2id_norm: dict[str, int] = {}
98
+ id2label_norm: dict[int, str] = {}
99
+ else:
100
+ # If only one mapping is provided, derive the other.
101
+ # ํ•˜๋‚˜๋งŒ ์ฃผ์–ด์ง„ ๊ฒฝ์šฐ ๋‚˜๋จธ์ง€๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
102
+ if id2label is None:
103
+ id2label = {v: k for k, v in label2id.items()}
104
+ if label2id is None:
105
+ label2id = {v: k for k, v in id2label.items()}
106
+
107
+ # Ensure both mappings are consistent in size.
108
+ # ๋‘ ๋งคํ•‘์˜ ํฌ๊ธฐ๊ฐ€ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
109
+ if len(label2id) != len(id2label):
110
+ raise ValueError(
111
+ f"label2id/id2label size mismatch: "
112
+ f"{len(label2id)} vs {len(id2label)}"
113
+ )
114
+
115
+ num_labels = len(id2label)
116
+ label2id_norm = dict(label2id)
117
+ id2label_norm = dict(id2label)
118
+
119
+ # ============================================================
120
+ # 3) num_labels consistency enforcement
121
+ # 3) num_labels ์ผ๊ด€์„ฑ ๊ฐ•์ œ
122
+ # ============================================================
123
+ # If num_labels is provided via kwargs, it must match inferred labels.
124
+ # kwargs๋กœ num_labels๊ฐ€ ๋“ค์–ด์˜จ ๊ฒฝ์šฐ ์ถ”๋ก ๋œ ๊ฐ’๊ณผ ๋ฐ˜๋“œ์‹œ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
125
+ if "num_labels" in kwargs:
126
+ if (label2id is not None or id2label is not None) and int(kwargs["num_labels"]) != num_labels:
127
+ raise ValueError(
128
+ f"kwargs['num_labels']={kwargs['num_labels']} "
129
+ f"!= inferred num_labels={num_labels}"
130
+ )
131
+ else:
132
  kwargs["num_labels"] = num_labels
133
 
134
+ # ============================================================
135
+ # 4) Parent initialization
136
+ # 4) ๋ถ€๋ชจ ํด๋ž˜์Šค ์ดˆ๊ธฐํ™”
137
+ # ============================================================
138
+ # Initialize PretrainedConfig with normalized label mappings.
139
+ # ์ •๊ทœํ™”๋œ ๋ผ๋ฒจ ๋งคํ•‘์„ ์‚ฌ์šฉํ•˜์—ฌ PretrainedConfig๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
140
  super().__init__(
141
+ label2id=label2id_norm,
142
+ id2label=id2label_norm,
143
  **kwargs,
144
  )
 
 
 
 
 
145
 
146
+ # ============================================================
147
+ # 5) Explicit attribute assignment for save/load stability
148
+ # 5) ์ €์žฅ/๋กœ๋“œ ์•ˆ์ •์„ฑ์„ ์œ„ํ•œ ๋ช…์‹œ์  ์†์„ฑ ๊ณ ์ •
149
+ # ============================================================
150
+ # Explicitly reassign critical fields to avoid subtle serialization issues.
151
+ # ๋ฏธ๋ฌ˜ํ•œ ์ง๋ ฌํ™” ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ํ•ต์‹ฌ ํ•„๋“œ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ๋‹ค์‹œ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
152
  self.backbone_name_or_path = backbone_name_or_path
153
+ self.mlp_head_bottleneck = int(mlp_head_bottleneck)
154
+ self.mlp_head_dropout = float(mlp_head_dropout)
155
+
156
+ self.label2id = label2id_norm
157
+ self.id2label = id2label_norm
158
+ self.num_labels = int(kwargs["num_labels"])
159
 
160
  def to_dict(self):
161
+ # Call the parent implementation first.
162
+ # ๋จผ์ € ๋ถ€๋ชจ ๊ตฌํ˜„์„ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
163
  output = super().to_dict()
164
+
165
+ # Force num_labels to be present and consistent.
166
+ # num_labels๊ฐ€ ๋ฐ˜๋“œ์‹œ ์กด์žฌํ•˜๊ณ  ์ผ๊ด€๋˜๋„๋ก ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค.
167
+ output["num_labels"] = int(
168
+ getattr(self, "num_labels", output.get("num_labels", 0))
169
+ )
170
  return output
171
 
172
+
173
+ # Register this config so it can be resolved via AutoConfig.
174
+ # AutoConfig๋ฅผ ํ†ตํ•ด ์ด config๊ฐ€ ํ•ด์„๋  ์ˆ˜ ์žˆ๋„๋ก ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.
175
  BackboneMLPHeadConfig.register_for_auto_class("AutoConfig")
models/microsoft__resnet-50/ds_model.py CHANGED
@@ -1,10 +1,9 @@
1
  #!/usr/bin/env python3
2
  # -*- coding: utf-8 -*-
3
 
4
- from __future__ import annotations
5
 
6
- from dataclasses import dataclass
7
- from typing import Dict, Any, Optional, Tuple, Set, List
8
 
9
  import torch
10
  import torch.nn as nn
@@ -12,146 +11,88 @@ import torch.nn.functional as F
12
 
13
  # --- transformers core ---
14
  from transformers.modeling_utils import PreTrainedModel
15
- from transformers import AutoModel, AutoConfig, PretrainedConfig
16
  from transformers.modeling_outputs import ImageClassifierOutput
17
 
18
  # --- torchvision ---
19
  from torchvision import models as tv_models
20
 
21
-
22
- # ============================================================
23
- # (0) Config
24
- # - ๊ธฐ์กด ds_cfg.BackboneMLPHeadConfig์™€ ํ•„๋“œ๋ช…์„ ๋งž์ถ”์—ˆ์Šต๋‹ˆ๋‹ค.
25
- # - ์ด๋ฏธ ds_cfg.py๊ฐ€ ์žˆ๋‹ค๋ฉด: ์ด ๋ธ”๋ก์„ ์ง€์šฐ๊ณ  "from ds_cfg import BackboneMLPHeadConfig"๋กœ ๋ฐ”๊ฟ”๋„ ๋ฉ๋‹ˆ๋‹ค.
26
- # ============================================================
27
- class BackboneMLPHeadConfig(PretrainedConfig):
28
- model_type = "backbone_mlphead"
29
-
30
- def __init__(
31
- self,
32
- backbone_name_or_path: Optional[str] = None,
33
- num_labels: int = 2,
34
- id2label: Optional[Dict[int, str]] = None,
35
- label2id: Optional[Dict[str, int]] = None,
36
- mlp_head_bottleneck: int = 256,
37
- mlp_head_dropout: float = 0.2,
38
- **kwargs,
39
- ):
40
- super().__init__(**kwargs)
41
- self.backbone_name_or_path = backbone_name_or_path
42
- self.num_labels = num_labels
43
- self.id2label = id2label or {i: f"LABEL_{i}" for i in range(num_labels)}
44
- self.label2id = label2id or {v: k for k, v in self.id2label.items()}
45
- self.mlp_head_bottleneck = mlp_head_bottleneck
46
- self.mlp_head_dropout = mlp_head_dropout
47
-
48
-
49
- # ============================================================
50
- # (1) BACKBONE_META
51
- # - ์ด๋ฏธ ds_meta.py๊ฐ€ ์žˆ๋‹ค๋ฉด: ์ด ๋ธ”๋ก์„ ์ง€์šฐ๊ณ  "from ds_meta import BACKBONE_META"๋กœ ๋ฐ”๊ฟ”๋„ ๋ฉ๋‹ˆ๋‹ค.
52
- # ============================================================
53
- BACKBONE_META: Dict[str, Dict[str, Any]] = {
54
- # transformers
55
- "google/vit-base-patch16-224": {
56
- "type": "vit",
57
- "feat_rule": "cls",
58
- "feat_dim": 768,
59
- "has_bn": False,
60
- "unfreeze": "last_n",
61
- },
62
- "microsoft/swin-tiny-patch4-window7-224": {
63
- "type": "swin",
64
- "feat_rule": "pool_or_mean",
65
- "feat_dim": 768,
66
- "has_bn": False,
67
- "unfreeze": "last_n",
68
- },
69
- "microsoft/resnet-50": {
70
- "type": "resnet",
71
- "feat_rule": "pool_or_gap",
72
- "feat_dim": 2048,
73
- "has_bn": True,
74
- "unfreeze": "last_n",
75
- },
76
- "google/efficientnet-b0": {
77
- "type": "efficientnet",
78
- "feat_rule": "pool_or_gap",
79
- "feat_dim": 1280,
80
- "has_bn": True,
81
- "unfreeze": "last_n",
82
- },
83
-
84
- # densenet
85
- "timm/densenet121.tv_in1k": {
86
- "type": "timm_densenet",
87
- "feat_rule": "timm_gap",
88
- "feat_dim": 1024,
89
- "has_bn": True,
90
- "unfreeze": "last_n",
91
- },
92
- "torchvision/densenet121": {
93
- "type": "torchvision_densenet",
94
- "feat_rule": "torchvision_densenet_gap",
95
- "feat_dim": 1024,
96
- "has_bn": True,
97
- "unfreeze": "last_n",
98
- },
99
- }
100
-
101
-
102
- # ============================================================
103
- # (2) MLP Head
104
- # - ์ด๋ฏธ mlp_head.py๊ฐ€ ์žˆ๋‹ค๋ฉด: ์ด ๋ธ”๋ก์„ ์ง€์šฐ๊ณ  "from mlp_head import MLPHead"๋กœ ๋ฐ”๊ฟ”๋„ ๋ฉ๋‹ˆ๋‹ค.
105
- # ============================================================
106
- class MLPHead(nn.Module):
107
- def __init__(self, in_dim: int, num_labels: int, bottleneck: int = 256, p: float = 0.2):
108
- super().__init__()
109
- self.fc2 = nn.Linear(in_dim, bottleneck)
110
- self.act2 = nn.GELU()
111
- self.drop2 = nn.Dropout(p)
112
- self.fc3 = nn.Linear(bottleneck, num_labels)
113
-
114
- def forward(self, x: torch.Tensor) -> torch.Tensor:
115
- x = self.drop2(self.act2(self.fc2(x)))
116
- return self.fc3(x)
117
-
118
 
119
  # ============================================================
120
  # (3) Model: backbone + MLP head
121
- # ํ•ต์‹ฌ ์„ค๊ณ„:
122
- # - __init__: backbone ๊ตฌ์กฐ(skeleton)๋งŒ ์ƒ์„ฑ. pretrained๋ฅผ ์ ˆ๋Œ€ ๋กœ๋“œํ•˜์ง€ ์•Š์Œ.
123
- # - pretrained ์ฃผ์ž…: model.load_backbone_pretrained_()๋ฅผ "fresh-start์—์„œ๋งŒ" ์ง์ ‘ ํ˜ธ์ถœ.
124
- # - from_pretrained override ์—†์Œ: HF ๊ธฐ๋ณธ ๋กœ๋”๊ฐ€ checkpoint๋ฅผ ๊ทธ๋Œ€๋กœ ๋ณต์›.
125
  # ============================================================
 
 
 
 
 
 
 
 
126
  class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
 
 
127
  config_class = BackboneMLPHeadConfig
128
 
129
  def __init__(self, config: BackboneMLPHeadConfig):
 
 
130
  super().__init__(config)
131
 
 
 
 
 
 
132
  if config.backbone_name_or_path is None:
133
- raise ValueError("config.backbone_name_or_path is None. Provide a valid backbone id.")
 
 
 
134
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  self._meta = BACKBONE_META[config.backbone_name_or_path]
136
 
137
- # (์ค‘์š”) backbone์€ ํ•ญ์ƒ skeleton๋งŒ ์ƒ์„ฑ
 
138
  self.backbone = self._build_backbone_skeleton(config.backbone_name_or_path)
139
 
140
- # head
 
141
  self.classifier = MLPHead(
142
  in_dim=int(self._meta["feat_dim"]),
143
- num_labels=config.num_labels,
144
- bottleneck=config.mlp_head_bottleneck,
145
- p=config.mlp_head_dropout,
146
  )
147
 
148
- # HF init hook (init_weights๋Š” head-only๋กœ override๋จ)
 
149
  self.post_init()
150
 
151
  def init_weights(self):
152
  """
153
- HF์˜ ๊ธฐ๋ณธ init_weights()๋Š” ๋ชจ๋ธ ์ „์ฒด์— apply(...)๋ฅผ ๊ฑธ ์ˆ˜ ์žˆ์œผ๋‹ˆ,
154
- head๋งŒ ์ดˆ๊ธฐํ™”ํ•œ๋‹ค.
 
 
 
155
  """
156
  if getattr(self, "classifier", None) is not None:
157
  self.classifier.apply(self._init_weights)
@@ -159,8 +100,11 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
159
 
160
  # ----------------------------
161
  # backbone skeleton builders
 
162
  # ----------------------------
163
  def _build_backbone_skeleton(self, backbone_id: str) -> nn.Module:
 
 
164
  meta = BACKBONE_META[backbone_id]
165
  t = meta["type"]
166
 
@@ -170,18 +114,24 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
170
  if t == "torchvision_densenet":
171
  return self._build_torchvision_densenet_skeleton(backbone_id)
172
 
173
- # transformers: config๋กœ ๊ตฌ์กฐ๋งŒ ์ƒ์„ฑ
 
174
  bb_cfg = AutoConfig.from_pretrained(backbone_id)
175
  return AutoModel.from_config(bb_cfg)
176
 
177
  @staticmethod
178
  def _build_timm_densenet_skeleton(hf_repo_id: str) -> nn.Module:
 
 
179
  try:
180
  import timm
181
  except Exception as e:
182
- raise ImportError("DenseNet(timm) backbone requires `timm`. Install: pip install timm") from e
 
 
183
 
184
- # ๊ตฌ์กฐ๋งŒ (pretrained=False)
 
185
  return timm.create_model(
186
  f"hf_hub:{hf_repo_id}",
187
  pretrained=False,
@@ -190,33 +140,33 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
190
 
191
  @staticmethod
192
  def _build_torchvision_densenet_skeleton(model_id: str) -> nn.Module:
193
- mapping = {
194
- "torchvision/densenet121": "densenet121",
195
- "torchvision/densenet169": "densenet169",
196
- "torchvision/densenet201": "densenet201",
197
- "torchvision/densenet161": "densenet161",
198
- }
199
- if model_id not in mapping:
200
- raise ValueError(f"Unsupported torchvision DenseNet id: {model_id}")
201
-
202
- arch = mapping[model_id]
203
- fn = getattr(tv_models, arch)
204
-
205
- # ๊ตฌ์กฐ๋งŒ (weights=None)
206
- m = fn(weights=None).eval()
207
  return m
208
 
209
  # ------------------------------------------------------------
210
- # pretrained ๋กœ๋”ฉ์€ "๋ช…์‹œ์  ํ˜ธ์ถœ"๋กœ๋งŒ ์ˆ˜ํ–‰ (fresh-start ์ „์šฉ)
 
211
  # ------------------------------------------------------------
212
  @torch.no_grad()
213
- def load_backbone_pretrained_(self, *, low_cpu_mem_usage: bool = False, device_map=None):
 
 
 
 
 
214
  """
215
- fresh-start ์ „์šฉ:
216
- model = BackboneWithMLPHeadForImageClassification(cfg)
217
- model.load_backbone_pretrained_()
218
 
219
- checkpoint reload(from_pretrained)์—์„œ๋Š” ํ˜ธ์ถœํ•˜์ง€ ๋งˆ์‹ญ์‹œ์˜ค.
 
220
  """
221
  bb = self.config.backbone_name_or_path
222
  meta = BACKBONE_META[bb]
@@ -230,48 +180,58 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
230
  self._load_torchvision_pretrained_into_skeleton_(bb)
231
  return
232
 
233
- # transformers pretrained -> skeleton์— ์ฃผ์ž…
 
234
  ref = AutoModel.from_pretrained(
235
  bb,
236
  low_cpu_mem_usage=low_cpu_mem_usage,
237
  device_map=device_map,
238
  )
 
 
 
239
  self.backbone.load_state_dict(ref.state_dict(), strict=False)
240
  del ref
241
 
242
  @torch.no_grad()
243
  def _load_timm_pretrained_into_skeleton_(self, hf_repo_id: str):
 
 
244
  import timm
 
 
 
245
  ref = timm.create_model(
246
  f"hf_hub:{hf_repo_id}",
247
  pretrained=True,
248
  num_classes=0,
249
  ).eval()
 
250
  self.backbone.load_state_dict(ref.state_dict(), strict=True)
251
  del ref
252
 
253
  @torch.no_grad()
254
  def _load_torchvision_pretrained_into_skeleton_(self, model_id: str):
255
- mapping = {
256
- "torchvision/densenet121": ("densenet121", tv_models.DenseNet121_Weights.DEFAULT),
257
- "torchvision/densenet169": ("densenet169", tv_models.DenseNet169_Weights.DEFAULT),
258
- "torchvision/densenet201": ("densenet201", tv_models.DenseNet201_Weights.DEFAULT),
259
- "torchvision/densenet161": ("densenet161", tv_models.DenseNet161_Weights.DEFAULT),
260
- }
261
- if model_id not in mapping:
262
- raise ValueError(f"Unsupported torchvision DenseNet id: {model_id}")
263
-
264
- arch, weights = mapping[model_id]
265
- fn = getattr(tv_models, arch)
266
- ref = fn(weights=weights).eval()
267
  self.backbone.load_state_dict(ref.state_dict(), strict=True)
268
  del ref
269
 
270
  # ----------------------------
271
  # feature extraction
 
272
  # ----------------------------
273
  @staticmethod
274
  def _pool_or_gap(outputs) -> torch.Tensor:
 
 
275
  if hasattr(outputs, "pooler_output") and outputs.pooler_output is not None:
276
  x = outputs.pooler_output
277
  if x.dim() == 2:
@@ -280,29 +240,42 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
280
  return x.flatten(1)
281
  raise RuntimeError(f"Unexpected pooler_output shape: {tuple(x.shape)}")
282
 
 
 
283
  x = outputs.last_hidden_state
284
  if x.dim() == 4:
285
  return x.mean(dim=(2, 3))
 
286
  raise RuntimeError(
287
  "Expected pooler_output or (B,C,H,W) last_hidden_state for CNN backbones. "
288
  f"Got last_hidden_state shape={tuple(x.shape)}"
289
  )
290
 
291
  def _extract_features(self, outputs, pixel_values: Optional[torch.Tensor] = None) -> torch.Tensor:
 
 
292
  rule = self._meta["feat_rule"]
293
 
294
  if rule == "cls":
 
 
295
  return outputs.last_hidden_state[:, 0, :]
296
 
297
  if rule == "pool_or_mean":
 
 
298
  if hasattr(outputs, "pooler_output") and outputs.pooler_output is not None:
299
  return outputs.pooler_output
300
  return outputs.last_hidden_state.mean(dim=1)
301
 
302
  if rule == "pool_or_gap":
 
 
303
  return self._pool_or_gap(outputs)
304
 
305
  if rule == "timm_gap":
 
 
306
  if not isinstance(outputs, torch.Tensor):
307
  raise TypeError(f"timm_gap expects Tensor features, got {type(outputs)}")
308
  if outputs.dim() != 4:
@@ -310,6 +283,8 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
310
  return outputs.mean(dim=(2, 3))
311
 
312
  if rule == "torchvision_densenet_gap":
 
 
313
  if not isinstance(outputs, torch.Tensor):
314
  raise TypeError(f"torchvision_densenet_gap expects Tensor, got {type(outputs)}")
315
  if outputs.dim() != 4:
@@ -327,9 +302,13 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
327
  return_dict=True,
328
  **kwargs,
329
  ):
 
 
330
  t = self._meta["type"]
331
 
332
  if t == "timm_densenet":
 
 
333
  if pixel_values is None:
334
  raise ValueError("timm DenseNet backbone requires pixel_values.")
335
  if pixel_values.dim() != 4:
@@ -341,6 +320,8 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
341
  attentions = None
342
 
343
  elif t == "torchvision_densenet":
 
 
344
  if pixel_values is None:
345
  raise ValueError("torchvision DenseNet backbone requires pixel_values.")
346
  if pixel_values.dim() != 4:
@@ -353,6 +334,8 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
353
  attentions = None
354
 
355
  else:
 
 
356
  outputs = self.backbone(
357
  pixel_values=pixel_values,
358
  output_attentions=output_attentions,
@@ -364,10 +347,14 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
364
  hidden_states = getattr(outputs, "hidden_states", None)
365
  attentions = getattr(outputs, "attentions", None)
366
 
 
 
367
  logits = self.classifier(feats)
368
 
369
  loss = None
370
  if labels is not None:
 
 
371
  loss = F.cross_entropy(logits, labels)
372
 
373
  if not return_dict:
@@ -383,20 +370,27 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
383
 
384
 
385
  # ============================================================
386
- # Freeze / Unfreeze utilities (๋‹น์‹  ์ฝ”๋“œ ์œ ์ง€)
 
387
  # ============================================================
388
  def _set_requires_grad(module: nn.Module, flag: bool):
 
 
389
  for p in module.parameters():
390
  p.requires_grad = flag
391
 
392
 
393
  def set_bn_eval(module: nn.Module):
 
 
394
  for m in module.modules():
395
  if isinstance(m, (nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d, nn.SyncBatchNorm)):
396
  m.eval()
397
 
398
 
399
  def freeze_backbone(model: BackboneWithMLPHeadForImageClassification, freeze_bn: bool = True):
 
 
400
  _set_requires_grad(model.backbone, False)
401
  _set_requires_grad(model.classifier, True)
402
 
@@ -406,6 +400,8 @@ def freeze_backbone(model: BackboneWithMLPHeadForImageClassification, freeze_bn:
406
 
407
 
408
  def finetune_train_mode(model: BackboneWithMLPHeadForImageClassification, keep_bn_eval: bool = True):
 
 
409
  model.train()
410
  meta = BACKBONE_META[model.config.backbone_name_or_path]
411
  if keep_bn_eval and meta.get("has_bn", False):
@@ -413,6 +409,8 @@ def finetune_train_mode(model: BackboneWithMLPHeadForImageClassification, keep_b
413
 
414
 
415
  def trainable_summary(model: nn.Module):
 
 
416
  total = sum(p.numel() for p in model.parameters())
417
  trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
418
  ratio = trainable / total if total > 0 else 0.0
@@ -425,6 +423,8 @@ def unfreeze_last_stage(
425
  last_n: int = 2,
426
  keep_bn_eval: bool = True,
427
  ):
 
 
428
  freeze_backbone(model, freeze_bn=keep_bn_eval)
429
 
430
  n = int(last_n)
@@ -438,12 +438,16 @@ def unfreeze_last_stage(
438
  bb_type = meta["type"]
439
 
440
  if bb_type == "vit":
 
 
441
  blocks = list(model.backbone.encoder.layer)
442
  for blk in blocks[-n:]:
443
  _set_requires_grad(blk, True)
444
  return
445
 
446
  if bb_type == "swin":
 
 
447
  stages = list(model.backbone.encoder.layers)
448
  blocks: List[nn.Module] = []
449
  for st in stages:
@@ -453,6 +457,8 @@ def unfreeze_last_stage(
453
  return
454
 
455
  if bb_type == "resnet":
 
 
456
  bb = model.backbone
457
  for name in ("layer1", "layer2", "layer3", "layer4"):
458
  if not hasattr(bb, name):
@@ -472,6 +478,8 @@ def unfreeze_last_stage(
472
  return
473
 
474
  if bb_type == "efficientnet":
 
 
475
  bb = model.backbone
476
  if not hasattr(bb, "features"):
477
  raise RuntimeError("Unexpected EfficientNet structure: missing features")
@@ -488,6 +496,8 @@ def unfreeze_last_stage(
488
  return
489
 
490
  if bb_type in ("timm_densenet", "torchvision_densenet"):
 
 
491
  bb = model.backbone
492
  if not hasattr(bb, "features"):
493
  raise RuntimeError("Unexpected DenseNet: missing features")
@@ -505,6 +515,8 @@ def unfreeze_last_stage(
505
  raise RuntimeError(f"Unexpected DenseNet features: missing {name}")
506
 
507
  def _denselayers(db: nn.Module) -> List[nn.Module]:
 
 
508
  return list(db.children())
509
 
510
  blocks: List[nn.Module] = []
@@ -526,20 +538,8 @@ def unfreeze_last_stage(
526
 
527
  # -------------------------
528
  # register
 
 
 
 
529
  BackboneWithMLPHeadForImageClassification.register_for_auto_class("AutoModelForImageClassification")
530
-
531
-
532
- # ==========================================================
533
-
534
- # # fresh-start
535
- # cfg = BackboneMLPHeadConfig(backbone_name_or_path="timm/densenet121.tv_in1k", num_labels=3)
536
- # model = BackboneWithMLPHeadForImageClassification(cfg)
537
-
538
- # # pretrained๋ฅผ ์“ฐ๋ ค๋ฉด ๋ช…์‹œ์ ์œผ๋กœ ํ˜ธ์ถœ
539
- # model.load_backbone_pretrained_()
540
-
541
- # # ์ดํ›„ ํ•™์Šต
542
-
543
- # # checkpoint์—์„œ ์‹œ์ž‘
544
- # model = BackboneWithMLPHeadForImageClassification.from_pretrained("checkpoints/xxx")
545
- # # ์—ฌ๊ธฐ์„œ๋Š” load_backbone_pretrained_() ํ˜ธ์ถœํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
 
1
  #!/usr/bin/env python3
2
  # -*- coding: utf-8 -*-
3
 
4
+ # src/ds_model.py
5
 
6
+ from typing import Optional, List
 
7
 
8
  import torch
9
  import torch.nn as nn
 
11
 
12
  # --- transformers core ---
13
  from transformers.modeling_utils import PreTrainedModel
14
+ from transformers import AutoModel, AutoConfig
15
  from transformers.modeling_outputs import ImageClassifierOutput
16
 
17
  # --- torchvision ---
18
  from torchvision import models as tv_models
19
 
20
+ from ds_cfg import BackboneMLPHeadConfig
21
+ from ds_meta import BACKBONE_META
22
+ from mlp_head import MLPHead
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  # ============================================================
25
  # (3) Model: backbone + MLP head
26
+ # (3) ๋ชจ๋ธ: backbone + MLP head
 
 
 
27
  # ============================================================
28
+ # Design principle: __init__ builds only a skeleton and MUST NOT load pretrained weights.
29
+ # ์„ค๊ณ„ ์›์น™: __init__์—์„œ๋Š” skeleton๋งŒ ๋งŒ๋“ค๊ณ  pretrained weight ๋กœ๋“œ๋Š” ์ ˆ๋Œ€ ํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
30
+ #
31
+ # Pretrained injection is allowed ONLY via an explicit call in fresh-start flows.
32
+ # pretrained ์ฃผ์ž…์€ fresh-start ํ๋ฆ„์—์„œ์˜ ๋ช…์‹œ์  ํ˜ธ์ถœ๋กœ๋งŒ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.
33
+ #
34
+ # HF from_pretrained should restore checkpoints as-is without side effects.
35
+ # HF from_pretrained๋Š” ๋ถ€์ž‘์šฉ ์—†์ด ์ฒดํฌํฌ์ธํŠธ๋ฅผ ๊ทธ๋Œ€๋กœ ๋ณต์›ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
36
  class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
37
+ # This links the model to its custom config for AutoClass usage.
38
+ # AutoClass ์‚ฌ์šฉ์„ ์œ„ํ•ด ๋ชจ๋ธ๊ณผ ์ปค์Šคํ…€ config๋ฅผ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค.
39
  config_class = BackboneMLPHeadConfig
40
 
41
  def __init__(self, config: BackboneMLPHeadConfig):
42
+ # PreTrainedModel expects a config object and stores it internally.
43
+ # PreTrainedModel์€ config ๊ฐ์ฒด๋ฅผ ๋ฐ›์•„ ๋‚ด๋ถ€์— ์ €์žฅํ•˜๋Š” ์ „์ œ๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค.
44
  super().__init__(config)
45
 
46
+ # Fail-fast: the model is not meant to be instantiated without a valid backbone id.
47
+ # fail-fast: ์œ ํšจํ•œ backbone id ์—†์ด ๋ชจ๋ธ์„ ๋งŒ๋“œ๋Š” ์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค๋Š” ํ—ˆ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
48
+ #
49
+ # Note: Transformers may create configs with no args, but models are conventionally created with configs.
50
+ # ์ฐธ๊ณ : Transformers๋Š” config ๋ฌด์ธ์ž ์ƒ์„ฑ์ด ์žˆ์„ ์ˆ˜ ์žˆ์œผ๋‚˜, ๋ชจ๋ธ์€ ๊ด€๋ก€์ ์œผ๋กœ config๋ฅผ ๋ฐ›์•„ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.
51
  if config.backbone_name_or_path is None:
52
+ raise ValueError(
53
+ "config.backbone_name_or_path is None. "
54
+ "Provide a valid backbone id (whitelist key in BACKBONE_META)."
55
+ )
56
 
57
+ # Fail-fast: training/inference requires a positive number of labels.
58
+ # fail-fast: ํ•™์Šต/์ถ”๋ก ์€ num_labels๊ฐ€ ์–‘์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.
59
+ #
60
+ # Config may exist in a minimal form for internal serialization paths, but the model should not.
61
+ # config๋Š” ๋‚ด๋ถ€ ์ง๋ ฌํ™” ๊ฒฝ๋กœ์—์„œ ์ตœ์†Œ ํ˜•ํƒœ๋กœ ์กด์žฌํ•  ์ˆ˜ ์žˆ์œผ๋‚˜ ๋ชจ๋ธ์€ ๊ทธ๋Ÿฌ๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
62
+ if int(getattr(config, "num_labels", 0)) <= 0:
63
+ raise ValueError(
64
+ f"config.num_labels must be > 0, got {getattr(config, 'num_labels', None)}. "
65
+ "Set num_labels (or id2label/label2id) when creating the config."
66
+ )
67
+
68
+ # Meta is a single source of truth for extraction and fine-tuning rules.
69
+ # meta๋Š” feature ์ถ”์ถœ ๋ฐ ๋ฏธ์„ธ์กฐ์ • ๊ทœ์น™์˜ ๋‹จ์ผ ๊ธฐ์ค€์ž…๋‹ˆ๋‹ค.
70
  self._meta = BACKBONE_META[config.backbone_name_or_path]
71
 
72
+ # Backbone skeleton is always created without pretrained weights.
73
+ # backbone skeleton์€ ํ•ญ์ƒ pretrained weight ์—†์ด ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
74
  self.backbone = self._build_backbone_skeleton(config.backbone_name_or_path)
75
 
76
+ # Head shape is driven by meta feat_dim and config.num_labels.
77
+ # head shape์€ meta์˜ feat_dim๊ณผ config.num_labels๋กœ ๊ฒฐ์ •๋ฉ๋‹ˆ๋‹ค.
78
  self.classifier = MLPHead(
79
  in_dim=int(self._meta["feat_dim"]),
80
+ num_labels=int(config.num_labels),
81
+ bottleneck=int(config.mlp_head_bottleneck),
82
+ p=float(config.mlp_head_dropout),
83
  )
84
 
85
+ # HF initialization hook, but we override init_weights to initialize head-only.
86
+ # HF ์ดˆ๊ธฐํ™” ํ›…์ด์ง€๋งŒ init_weights๋ฅผ overrideํ•˜์—ฌ head๋งŒ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
87
  self.post_init()
88
 
89
  def init_weights(self):
90
  """
91
+ Initialize only the head to avoid touching the backbone skeleton.
92
+ backbone skeleton์„ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๊ธฐ ์œ„ํ•ด head๋งŒ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
93
+
94
+ HF's default init may traverse the entire module tree, which is undesirable here.
95
+ HF ๊ธฐ๋ณธ init์€ ์ „์ฒด ๋ชจ๋“ˆ ํŠธ๋ฆฌ๋ฅผ ์ˆœํšŒํ•  ์ˆ˜ ์žˆ์–ด ์—ฌ๊ธฐ์„œ๋Š” ๋ถ€์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.
96
  """
97
  if getattr(self, "classifier", None) is not None:
98
  self.classifier.apply(self._init_weights)
 
100
 
101
  # ----------------------------
102
  # backbone skeleton builders
103
+ # backbone skeleton ์ƒ์„ฑ๊ธฐ
104
  # ----------------------------
105
  def _build_backbone_skeleton(self, backbone_id: str) -> nn.Module:
106
+ # Meta decides which loader path to use.
107
+ # meta๊ฐ€ ์–ด๋–ค ๋กœ๋” ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ• ์ง€ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.
108
  meta = BACKBONE_META[backbone_id]
109
  t = meta["type"]
110
 
 
114
  if t == "torchvision_densenet":
115
  return self._build_torchvision_densenet_skeleton(backbone_id)
116
 
117
+ # For transformers backbones: build a random-weight skeleton from config only.
118
+ # transformers ๋ฐฑ๋ณธ: config๋กœ๋ถ€ํ„ฐ ๋žœ๋ค ์ดˆ๊ธฐํ™” skeleton๋งŒ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
119
  bb_cfg = AutoConfig.from_pretrained(backbone_id)
120
  return AutoModel.from_config(bb_cfg)
121
 
122
  @staticmethod
123
  def _build_timm_densenet_skeleton(hf_repo_id: str) -> nn.Module:
124
+ # timm is an optional dependency and should be imported lazily.
125
+ # timm์€ ์˜ต์…˜ ์˜์กด์„ฑ์ด๋ฏ€๋กœ ์ง€์—ฐ import ํ•ฉ๋‹ˆ๋‹ค.
126
  try:
127
  import timm
128
  except Exception as e:
129
+ raise ImportError(
130
+ "DenseNet(timm) backbone requires `timm`. Install: pip install timm"
131
+ ) from e
132
 
133
+ # Build structure only (pretrained=False) and remove classifier head (num_classes=0).
134
+ # ๊ตฌ์กฐ๋งŒ ์ƒ์„ฑ(pretrained=False)ํ•˜๊ณ  ๋ถ„๋ฅ˜๊ธฐ head๋Š” ์ œ๊ฑฐ(num_classes=0)ํ•ฉ๋‹ˆ๋‹ค.
135
  return timm.create_model(
136
  f"hf_hub:{hf_repo_id}",
137
  pretrained=False,
 
140
 
141
  @staticmethod
142
  def _build_torchvision_densenet_skeleton(model_id: str) -> nn.Module:
143
+ # This project intentionally supports only torchvision/densenet121 in the 224 whitelist.
144
+ # ์ด ํ”„๋กœ์ ํŠธ๋Š” 224 ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ์—์„œ torchvision/densenet121๋งŒ ์˜๋„์ ์œผ๋กœ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.
145
+ if model_id != "torchvision/densenet121":
146
+ raise ValueError(f"Unsupported torchvision DenseNet id (224 whitelist only): {model_id}")
147
+
148
+ # Build structure only (weights=None) to avoid implicit pretrained loading.
149
+ # implicit pretrained ๋กœ๋“œ๋ฅผ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด ๊ตฌ์กฐ๋งŒ ์ƒ์„ฑ(weights=None)ํ•ฉ๋‹ˆ๋‹ค.
150
+ m = tv_models.densenet121(weights=None)
 
 
 
 
 
 
151
  return m
152
 
153
  # ------------------------------------------------------------
154
+ # Pretrained loading is explicit and fresh-start only
155
+ # pretrained ๋กœ๋”ฉ์€ ๋ช…์‹œ์  ํ˜ธ์ถœ์ด๋ฉฐ fresh-start ์ „์šฉ
156
  # ------------------------------------------------------------
157
  @torch.no_grad()
158
+ def load_backbone_pretrained_(
159
+ self,
160
+ *,
161
+ low_cpu_mem_usage: bool = False,
162
+ device_map=None,
163
+ ):
164
  """
165
+ Fresh-start only: inject pretrained backbone weights into the skeleton.
166
+ fresh-start ์ „์šฉ: skeleton backbone์— pretrained ๊ฐ€์ค‘์น˜๋ฅผ ์ฃผ์ž…ํ•ฉ๋‹ˆ๋‹ค.
 
167
 
168
+ Do NOT call this after from_pretrained() because it would overwrite checkpoint weights.
169
+ from_pretrained() ์ดํ›„ ํ˜ธ์ถœํ•˜๋ฉด ์ฒดํฌํฌ์ธํŠธ ๊ฐ€์ค‘์น˜๋ฅผ ๋ฎ์–ด์“ฐ๋ฏ€๋กœ ์ ˆ๋Œ€ ํ˜ธ์ถœํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
170
  """
171
  bb = self.config.backbone_name_or_path
172
  meta = BACKBONE_META[bb]
 
180
  self._load_torchvision_pretrained_into_skeleton_(bb)
181
  return
182
 
183
+ # For transformers backbones, load a reference pretrained model and copy weights into our skeleton.
184
+ # transformers ๋ฐฑ๋ณธ์€ reference pretrained ๋ชจ๋ธ์„ ๋กœ๋“œํ•œ ๋’ค skeleton์— ๊ฐ€์ค‘์น˜๋ฅผ ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค.
185
  ref = AutoModel.from_pretrained(
186
  bb,
187
  low_cpu_mem_usage=low_cpu_mem_usage,
188
  device_map=device_map,
189
  )
190
+
191
+ # strict=False is used to tolerate harmless key differences across minor versions.
192
+ # strict=False๋Š” ๋งˆ์ด๋„ˆ ๋ฒ„์ „ ์ฐจ์ด๋กœ ์ธํ•œ ๋ฌดํ•ดํ•œ ํ‚ค ์ฐจ์ด๋ฅผ ํ—ˆ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
193
  self.backbone.load_state_dict(ref.state_dict(), strict=False)
194
  del ref
195
 
196
  @torch.no_grad()
197
  def _load_timm_pretrained_into_skeleton_(self, hf_repo_id: str):
198
+ # timm must be present for timm backbones.
199
+ # timm ๋ฐฑ๋ณธ์—๋Š” timm ์„ค์น˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
200
  import timm
201
+
202
+ # Create a pretrained reference model and copy its weights strictly.
203
+ # pretrained reference ๋ชจ๋ธ์„ ๋งŒ๋“ค๊ณ  ๊ฐ€์ค‘์น˜๋ฅผ strictํ•˜๊ฒŒ ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค.
204
  ref = timm.create_model(
205
  f"hf_hub:{hf_repo_id}",
206
  pretrained=True,
207
  num_classes=0,
208
  ).eval()
209
+
210
  self.backbone.load_state_dict(ref.state_dict(), strict=True)
211
  del ref
212
 
213
  @torch.no_grad()
214
  def _load_torchvision_pretrained_into_skeleton_(self, model_id: str):
215
+ # This project intentionally supports only torchvision/densenet121 in the 224 whitelist.
216
+ # ์ด ํ”„๋กœ์ ํŠธ๋Š” 224 ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ์—์„œ torchvision/densenet121๋งŒ ์˜๋„์ ์œผ๋กœ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.
217
+ if model_id != "torchvision/densenet121":
218
+ raise ValueError(f"Unsupported torchvision DenseNet id (224 whitelist only): {model_id}")
219
+
220
+ # Use torchvision's default pretrained weights for densenet121.
221
+ # torchvision์˜ densenet121 ๊ธฐ๋ณธ pretrained weights๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
222
+ ref = tv_models.densenet121(weights=tv_models.DenseNet121_Weights.DEFAULT).eval()
223
+
 
 
 
224
  self.backbone.load_state_dict(ref.state_dict(), strict=True)
225
  del ref
226
 
227
  # ----------------------------
228
  # feature extraction
229
+ # feature ์ถ”์ถœ
230
  # ----------------------------
231
  @staticmethod
232
  def _pool_or_gap(outputs) -> torch.Tensor:
233
+ # Some transformers vision CNNs provide pooler_output explicitly.
234
+ # ์ผ๋ถ€ transformers vision CNN์€ pooler_output์„ ๋ช…์‹œ์ ์œผ๋กœ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
235
  if hasattr(outputs, "pooler_output") and outputs.pooler_output is not None:
236
  x = outputs.pooler_output
237
  if x.dim() == 2:
 
240
  return x.flatten(1)
241
  raise RuntimeError(f"Unexpected pooler_output shape: {tuple(x.shape)}")
242
 
243
+ # Otherwise we expect a CNN-style last_hidden_state=(B,C,H,W) and apply GAP.
244
+ # ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด CNN ์Šคํƒ€์ผ last_hidden_state=(B,C,H,W)๋ฅผ ๊ธฐ๋Œ€ํ•˜๊ณ  GAP์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.
245
  x = outputs.last_hidden_state
246
  if x.dim() == 4:
247
  return x.mean(dim=(2, 3))
248
+
249
  raise RuntimeError(
250
  "Expected pooler_output or (B,C,H,W) last_hidden_state for CNN backbones. "
251
  f"Got last_hidden_state shape={tuple(x.shape)}"
252
  )
253
 
254
  def _extract_features(self, outputs, pixel_values: Optional[torch.Tensor] = None) -> torch.Tensor:
255
+ # Feature rule is defined by BACKBONE_META and must remain stable across saves/loads.
256
+ # feature ๊ทœ์น™์€ BACKBONE_META๋กœ ์ •์˜๋˜๋ฉฐ ์ €์žฅ/๋กœ๋“œ ๊ฐ„ ์•ˆ์ •์ ์œผ๋กœ ์œ ์ง€๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
257
  rule = self._meta["feat_rule"]
258
 
259
  if rule == "cls":
260
+ # ViT-style: use CLS token embedding from last_hidden_state.
261
+ # ViT ์Šคํƒ€์ผ: last_hidden_state์—์„œ CLS ํ† ํฐ ์ž„๋ฒ ๋”ฉ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
262
  return outputs.last_hidden_state[:, 0, :]
263
 
264
  if rule == "pool_or_mean":
265
+ # Swin-style: prefer pooler_output if present, else mean-pool over tokens.
266
+ # Swin ์Šคํƒ€์ผ: pooler_output์ด ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉํ•˜๊ณ , ์—†์œผ๋ฉด ํ† ํฐ ํ‰๊ท  ํ’€๋ง์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
267
  if hasattr(outputs, "pooler_output") and outputs.pooler_output is not None:
268
  return outputs.pooler_output
269
  return outputs.last_hidden_state.mean(dim=1)
270
 
271
  if rule == "pool_or_gap":
272
+ # CNN-style: use pooler_output if present, else GAP over spatial dims.
273
+ # CNN ์Šคํƒ€์ผ: pooler_output์ด ์žˆ์œผ๋ฉด ์‚ฌ์šฉํ•˜๊ณ , ์—†์œผ๋ฉด ๊ณต๊ฐ„ ์ฐจ์› GAP์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
274
  return self._pool_or_gap(outputs)
275
 
276
  if rule == "timm_gap":
277
+ # timm forward_features returns a feature map (B,C,H,W) which we GAP to (B,C).
278
+ # timm forward_features๋Š” (B,C,H,W) feature map์„ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ ์ด๋ฅผ GAP์œผ๋กœ (B,C)๋กœ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
279
  if not isinstance(outputs, torch.Tensor):
280
  raise TypeError(f"timm_gap expects Tensor features, got {type(outputs)}")
281
  if outputs.dim() != 4:
 
283
  return outputs.mean(dim=(2, 3))
284
 
285
  if rule == "torchvision_densenet_gap":
286
+ # torchvision DenseNet features are feature maps (B,C,H,W) and require GAP.
287
+ # torchvision DenseNet features๋Š” (B,C,H,W) feature map์ด๋ฉฐ GAP์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
288
  if not isinstance(outputs, torch.Tensor):
289
  raise TypeError(f"torchvision_densenet_gap expects Tensor, got {type(outputs)}")
290
  if outputs.dim() != 4:
 
302
  return_dict=True,
303
  **kwargs,
304
  ):
305
+ # Type decides the backbone forward path and output format.
306
+ # type์ด backbone forward ๊ฒฝ๋กœ ๋ฐ ์ถœ๋ ฅ ํฌ๋งท์„ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.
307
  t = self._meta["type"]
308
 
309
  if t == "timm_densenet":
310
+ # timm DenseNet consumes pixel_values as a 4D tensor (B,C,H,W).
311
+ # timm DenseNet์€ pixel_values๋ฅผ 4D ํ…์„œ (B,C,H,W)๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค.
312
  if pixel_values is None:
313
  raise ValueError("timm DenseNet backbone requires pixel_values.")
314
  if pixel_values.dim() != 4:
 
320
  attentions = None
321
 
322
  elif t == "torchvision_densenet":
323
+ # torchvision DenseNet consumes pixel_values as a 4D tensor (B,C,H,W).
324
+ # torchvision DenseNet์€ pixel_values๋ฅผ 4D ํ…์„œ (B,C,H,W)๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค.
325
  if pixel_values is None:
326
  raise ValueError("torchvision DenseNet backbone requires pixel_values.")
327
  if pixel_values.dim() != 4:
 
334
  attentions = None
335
 
336
  else:
337
+ # Transformers vision models are called with pixel_values and return ModelOutput.
338
+ # transformers vision ๋ชจ๋ธ์€ pixel_values๋กœ ํ˜ธ์ถœ๋˜๋ฉฐ ModelOutput์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
339
  outputs = self.backbone(
340
  pixel_values=pixel_values,
341
  output_attentions=output_attentions,
 
347
  hidden_states = getattr(outputs, "hidden_states", None)
348
  attentions = getattr(outputs, "attentions", None)
349
 
350
+ # Classifier consumes (B, feat_dim) and returns logits (B, num_labels).
351
+ # classifier๋Š” (B, feat_dim)์„ ๋ฐ›์•„ logits (B, num_labels)๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
352
  logits = self.classifier(feats)
353
 
354
  loss = None
355
  if labels is not None:
356
+ # Cross entropy expects labels as class indices in [0, num_labels).
357
+ # cross entropy๋Š” labels๊ฐ€ [0, num_labels) ๋ฒ”์œ„์˜ class index์ด๊ธธ ๊ธฐ๋Œ€ํ•ฉ๋‹ˆ๋‹ค.
358
  loss = F.cross_entropy(logits, labels)
359
 
360
  if not return_dict:
 
370
 
371
 
372
  # ============================================================
373
+ # Freeze / Unfreeze utilities
374
+ # Freeze / Unfreeze ์œ ํ‹ธ๋ฆฌํ‹ฐ
375
  # ============================================================
376
  def _set_requires_grad(module: nn.Module, flag: bool):
377
+ # Toggle requires_grad for all parameters in a module.
378
+ # ๋ชจ๋“ˆ์˜ ๋ชจ๋“  ํŒŒ๋ผ๋ฏธํ„ฐ์— ๋Œ€ํ•ด requires_grad๋ฅผ ํ† ๊ธ€ํ•ฉ๋‹ˆ๋‹ค.
379
  for p in module.parameters():
380
  p.requires_grad = flag
381
 
382
 
383
  def set_bn_eval(module: nn.Module):
384
+ # Put BatchNorm layers into eval mode to freeze running stats.
385
+ # BatchNorm ๋ ˆ์ด์–ด๋ฅผ eval ๋ชจ๋“œ๋กœ ๋‘์–ด running stats๋ฅผ ๊ณ ์ •ํ•ฉ๋‹ˆ๋‹ค.
386
  for m in module.modules():
387
  if isinstance(m, (nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d, nn.SyncBatchNorm)):
388
  m.eval()
389
 
390
 
391
  def freeze_backbone(model: BackboneWithMLPHeadForImageClassification, freeze_bn: bool = True):
392
+ # Stage1: freeze backbone and train only the head.
393
+ # stage1: backbone์„ freezeํ•˜๊ณ  head๋งŒ ํ•™์Šตํ•ฉ๋‹ˆ๋‹ค.
394
  _set_requires_grad(model.backbone, False)
395
  _set_requires_grad(model.classifier, True)
396
 
 
400
 
401
 
402
  def finetune_train_mode(model: BackboneWithMLPHeadForImageClassification, keep_bn_eval: bool = True):
403
+ # Stage2: train mode, optionally keeping BN layers in eval for stability.
404
+ # stage2: train ๋ชจ๋“œ๋กœ ๋‘๋˜ ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด BN์„ eval๋กœ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
405
  model.train()
406
  meta = BACKBONE_META[model.config.backbone_name_or_path]
407
  if keep_bn_eval and meta.get("has_bn", False):
 
409
 
410
 
411
  def trainable_summary(model: nn.Module):
412
+ # Print a compact summary of trainable parameters.
413
+ # ํ•™์Šต ๊ฐ€๋Šฅ ํŒŒ๋ผ๋ฏธํ„ฐ ์š”์•ฝ์„ ๊ฐ„๋‹จํžˆ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.
414
  total = sum(p.numel() for p in model.parameters())
415
  trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
416
  ratio = trainable / total if total > 0 else 0.0
 
423
  last_n: int = 2,
424
  keep_bn_eval: bool = True,
425
  ):
426
+ # This utility implements BACKBONE_META['unfreeze']=="last_n" across supported backbones.
427
+ # ์ด ์œ ํ‹ธ์€ ์ง€์› ๋ฐฑ๋ณธ๋“ค์— ๋Œ€ํ•ด BACKBONE_META['unfreeze']=="last_n"์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
428
  freeze_backbone(model, freeze_bn=keep_bn_eval)
429
 
430
  n = int(last_n)
 
438
  bb_type = meta["type"]
439
 
440
  if bb_type == "vit":
441
+ # ViT blocks live under backbone.encoder.layer in the transformers implementation.
442
+ # ViT ๋ธ”๋ก์€ transformers ๊ตฌํ˜„์—์„œ backbone.encoder.layer ์•„๋ž˜์— ์žˆ์Šต๋‹ˆ๋‹ค.
443
  blocks = list(model.backbone.encoder.layer)
444
  for blk in blocks[-n:]:
445
  _set_requires_grad(blk, True)
446
  return
447
 
448
  if bb_type == "swin":
449
+ # Swin blocks are nested by stages and blocks; we flatten and unfreeze last n blocks.
450
+ # Swin ๋ธ”๋ก์€ stage์™€ block์œผ๋กœ ์ค‘์ฒฉ๋˜์–ด ์žˆ์–ด ํŽผ์นœ ํ›„ ๋งˆ์ง€๋ง‰ n๊ฐœ๋ฅผ unfreeze ํ•ฉ๋‹ˆ๋‹ค.
451
  stages = list(model.backbone.encoder.layers)
452
  blocks: List[nn.Module] = []
453
  for st in stages:
 
457
  return
458
 
459
  if bb_type == "resnet":
460
+ # ResNet uses layer1..layer4 stages; we unfreeze at block granularity.
461
+ # ResNet์€ layer1..layer4 stage๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ block ๋‹จ์œ„๋กœ unfreeze ํ•ฉ๋‹ˆ๋‹ค.
462
  bb = model.backbone
463
  for name in ("layer1", "layer2", "layer3", "layer4"):
464
  if not hasattr(bb, name):
 
478
  return
479
 
480
  if bb_type == "efficientnet":
481
+ # EfficientNet in transformers exposes features; we unfreeze from the tail blocks.
482
+ # transformers EfficientNet์€ features๋ฅผ ๋…ธ์ถœํ•˜๋ฉฐ ๋’ค์ชฝ ๋ธ”๋ก๋ถ€ํ„ฐ unfreeze ํ•ฉ๋‹ˆ๋‹ค.
483
  bb = model.backbone
484
  if not hasattr(bb, "features"):
485
  raise RuntimeError("Unexpected EfficientNet structure: missing features")
 
496
  return
497
 
498
  if bb_type in ("timm_densenet", "torchvision_densenet"):
499
+ # DenseNet exposes a .features module with named blocks; we unfreeze last n submodules.
500
+ # DenseNet์€ .features ๋ชจ๋“ˆ์— ๋ธ”๋ก๋“ค์ด ์ด๋ฆ„์œผ๋กœ ์กด์žฌํ•˜๋ฉฐ ๋งˆ์ง€๋ง‰ n๊ฐœ ์„œ๋ธŒ๋ชจ๋“ˆ์„ unfreeze ํ•ฉ๋‹ˆ๋‹ค.
501
  bb = model.backbone
502
  if not hasattr(bb, "features"):
503
  raise RuntimeError("Unexpected DenseNet: missing features")
 
515
  raise RuntimeError(f"Unexpected DenseNet features: missing {name}")
516
 
517
  def _denselayers(db: nn.Module) -> List[nn.Module]:
518
+ # Dense blocks contain multiple DenseLayer children; we return them for fine-grained unfreezing.
519
+ # denseblock์€ DenseLayer ์ž์‹๋“ค์„ ๊ฐ€์ง€๋ฏ€๋กœ ์„ธ๋ฐ€ํ•œ unfreeze๋ฅผ ์œ„ํ•ด ์ด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
520
  return list(db.children())
521
 
522
  blocks: List[nn.Module] = []
 
538
 
539
  # -------------------------
540
  # register
541
+ # register
542
+ # -------------------------
543
+ # Register for AutoModelForImageClassification so from_pretrained can resolve this custom class.
544
+ # from_pretrained๊ฐ€ ์ด ์ปค์Šคํ…€ ํด๋ž˜์Šค๋ฅผ ํ•ด์„ํ•  ์ˆ˜ ์žˆ๋„๋ก AutoModelForImageClassification์— ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.
545
  BackboneWithMLPHeadForImageClassification.register_for_auto_class("AutoModelForImageClassification")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
models/microsoft__resnet-50/ds_proc.py ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # src/ds_proc.py
5
+
6
+ # ============================================================
7
+ # (4) ImageProcessor (AutoImageProcessor integration)
8
+ # (4) ImageProcessor (AutoImageProcessor ์—ฐ๋™)
9
+ # ============================================================
10
+
11
+ from typing import Any
12
+
13
+ import numpy as np
14
+ import torch
15
+ from transformers import AutoImageProcessor, AutoConfig
16
+ from transformers.image_processing_base import ImageProcessingMixin
17
+ from transformers.utils.generic import TensorType
18
+
19
+ from ds_meta import BackboneID, BACKBONE_META
20
+
21
+
22
+ class BackboneMLPHead224ImageProcessor(ImageProcessingMixin):
23
+ """
24
+ This processor performs image preprocessing and outputs {"pixel_values": ...}.
25
+ ์ด processor๋Š” ์ด๋ฏธ์ง€ ์ „์ฒ˜๋ฆฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ณ  {"pixel_values": ...}๋ฅผ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.
26
+
27
+ Key requirements:
28
+ ํ•ต์‹ฌ ์š”๊ตฌ์‚ฌํ•ญ:
29
+
30
+ 1) save_pretrained() must produce a JSON-serializable preprocessor_config.json.
31
+ save_pretrained()๋Š” JSON ์ง๋ ฌํ™” ๊ฐ€๋Šฅํ•œ preprocessor_config.json์„ ์ƒ์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
32
+
33
+ 2) Runtime-only objects (delegate processor, timm/torchvision transforms) must NOT be serialized.
34
+ ๋Ÿฐํƒ€์ž„ ๊ฐ์ฒด(delegate processor, timm/torchvision transform)๋Š” ์ ˆ๋Œ€ ์ง๋ ฌํ™”ํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
35
+
36
+ 3) Runtime objects are rebuilt at init/load time based on backbone meta.
37
+ ๋Ÿฐํƒ€์ž„ ๊ฐ์ฒด๋Š” backbone meta์— ๋”ฐ๋ผ init/load ์‹œ์ ์— ์žฌ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค.
38
+ """
39
+
40
+ # HF vision models conventionally expect "pixel_values" as the primary input key.
41
+ # HF vision ๋ชจ๋ธ์€ ๊ด€๋ก€์ ์œผ๋กœ ์ž…๋ ฅ ํ‚ค๋กœ "pixel_values"๋ฅผ ๊ธฐ๋Œ€ํ•ฉ๋‹ˆ๋‹ค.
42
+ model_input_names = ["pixel_values"]
43
+
44
+ def __init__(self, backbone_name_or_path: BackboneID, is_training: bool = False, **kwargs):
45
+ # ImageProcessingMixin stores extra kwargs and manages auto_map metadata.
46
+ # ImageProcessingMixin์€ ์ถ”๊ฐ€ kwargs๋ฅผ ์ €์žฅํ•˜๊ณ  auto_map ๋ฉ”ํƒ€๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
47
+ super().__init__(**kwargs)
48
+
49
+ # Enforce whitelist via BACKBONE_META to keep behavior stable.
50
+ # ๋™์ž‘ ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด BACKBONE_META ๊ธฐ๋ฐ˜ ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค.
51
+ if backbone_name_or_path not in BACKBONE_META:
52
+ raise ValueError(
53
+ f"Unsupported backbone_name_or_path={backbone_name_or_path}. "
54
+ f"Allowed: {sorted(BACKBONE_META.keys())}"
55
+ )
56
+
57
+ # Serializable fields only: these should appear in preprocessor_config.json.
58
+ # ์ง๋ ฌํ™” ๊ฐ€๋Šฅํ•œ ํ•„๋“œ๋งŒ: ์ด ๊ฐ’๋“ค๋งŒ preprocessor_config.json์— ๋“ค์–ด๊ฐ€์•ผ ํ•ฉ๋‹ˆ๋‹ค.
59
+ self.backbone_name_or_path = backbone_name_or_path
60
+ self.is_training = bool(is_training)
61
+
62
+ # Runtime-only fields: must never be serialized.
63
+ # ๋Ÿฐํƒ€์ž„ ์ „์šฉ ํ•„๋“œ: ์ ˆ๋Œ€ ์ง๋ ฌํ™”๋˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
64
+ self._meta = None
65
+ self._delegate = None
66
+ self._timm_transform = None
67
+ self._torchvision_transform = None
68
+
69
+ # Build runtime objects according to backbone type.
70
+ # backbone type์— ๋”ฐ๋ผ ๋Ÿฐํƒ€์ž„ ๊ฐ์ฒด๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
71
+ self._build_runtime()
72
+
73
+ # ============================================================
74
+ # Runtime builders
75
+ # ๋Ÿฐํƒ€์ž„ ๋นŒ๋”
76
+ # ============================================================
77
+ def _build_runtime(self):
78
+ """
79
+ Build runtime delegate/transform based on BACKBONE_META["type"].
80
+ BACKBONE_META["type"]์— ๋”ฐ๋ผ ๋Ÿฐํƒ€์ž„ delegate/transform์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
81
+ """
82
+ meta = BACKBONE_META[self.backbone_name_or_path]
83
+ self._meta = meta
84
+
85
+ # Always reset runtime fields before rebuilding.
86
+ # ์žฌ๊ตฌ์„ฑ ์ „ ๋Ÿฐํƒ€์ž„ ํ•„๋“œ๋Š” ํ•ญ์ƒ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
87
+ self._delegate = None
88
+ self._timm_transform = None
89
+ self._torchvision_transform = None
90
+
91
+ t = meta["type"]
92
+
93
+ if t == "timm_densenet":
94
+ # timm DenseNet uses timm.data transforms for ImageNet-style preprocessing.
95
+ # timm DenseNet์€ ImageNet ์ „์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด timm.data transform์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
96
+ self._timm_transform = self._build_timm_transform(
97
+ backbone_id=self.backbone_name_or_path,
98
+ is_training=self.is_training,
99
+ )
100
+ return
101
+
102
+ if t == "torchvision_densenet":
103
+ # torchvision DenseNet requires torchvision-style preprocessing (resize/crop/tensor/normalize).
104
+ # torchvision DenseNet์€ torchvision ์Šคํƒ€์ผ ์ „์ฒ˜๋ฆฌ(resize/crop/tensor/normalize)๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
105
+ self._torchvision_transform = self._build_torchvision_densenet_transform(
106
+ is_training=self.is_training
107
+ )
108
+ return
109
+
110
+ # Default: transformers backbone delegates to its official AutoImageProcessor.
111
+ # ๊ธฐ๋ณธ: transformers ๋ฐฑ๋ณธ์€ ๊ณต์‹ AutoImageProcessor์— ์œ„์ž„ํ•ฉ๋‹ˆ๋‹ค.
112
+ self._delegate = AutoImageProcessor.from_pretrained(self.backbone_name_or_path)
113
+
114
+ @staticmethod
115
+ def _build_timm_transform(*, backbone_id: str, is_training: bool):
116
+ """
117
+ Create timm transform without storing non-serializable objects in config.
118
+ ๋น„์ง๋ ฌํ™” ๊ฐ์ฒด๋ฅผ config์— ์ €์žฅํ•˜์ง€ ์•Š๊ณ  timm transform์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
119
+ """
120
+ try:
121
+ import timm
122
+ from timm.data import resolve_model_data_config, create_transform
123
+ except Exception as e:
124
+ raise ImportError(
125
+ "timm backbone processor requires `timm`. Install: pip install timm"
126
+ ) from e
127
+
128
+ # We only need model metadata to resolve data config, so pretrained=False is preferred.
129
+ # data config ์ถ”์ถœ๋งŒ ํ•„์š”ํ•˜๋ฏ€๋กœ pretrained=False๋ฅผ ์šฐ์„  ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
130
+ m = timm.create_model(f"hf_hub:{backbone_id}", pretrained=False, num_classes=0)
131
+ dc = resolve_model_data_config(m)
132
+
133
+ # create_transform returns a torchvision-like callable that maps PIL -> torch.Tensor(C,H,W).
134
+ # create_transform์€ PIL -> torch.Tensor(C,H,W)๋กœ ๋งคํ•‘ํ•˜๋Š” callable์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
135
+ tfm = create_transform(**dc, is_training=is_training)
136
+ return tfm
137
+
138
+ @staticmethod
139
+ def _build_torchvision_densenet_transform(*, is_training: bool):
140
+ """
141
+ Build torchvision preprocessing for DenseNet-121 (224 pipeline).
142
+ DenseNet-121์šฉ torchvision ์ „์ฒ˜๋ฆฌ(224 ํŒŒ์ดํ”„๋ผ์ธ)๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
143
+ """
144
+ try:
145
+ from torchvision import transforms
146
+ except Exception as e:
147
+ raise ImportError(
148
+ "torchvision DenseNet processor requires `torchvision`. Install: pip install torchvision"
149
+ ) from e
150
+
151
+ # These are the standard ImageNet normalization stats used by torchvision weights.
152
+ # ์ด ๊ฐ’๋“ค์€ torchvision weights๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” ํ‘œ์ค€ ImageNet ์ •๊ทœํ™” ํ†ต๊ณ„์ž…๋‹ˆ๋‹ค.
153
+ mean = (0.485, 0.456, 0.406)
154
+ std = (0.229, 0.224, 0.225)
155
+
156
+ # Training pipeline typically uses RandomResizedCrop and horizontal flip.
157
+ # ํ•™์Šต ํŒŒ์ดํ”„๋ผ์ธ์€ ๋ณดํ†ต RandomResizedCrop๊ณผ ์ขŒ์šฐ๋ฐ˜์ „์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
158
+ if is_training:
159
+ return transforms.Compose(
160
+ [
161
+ transforms.RandomResizedCrop(224),
162
+ transforms.RandomHorizontalFlip(p=0.5),
163
+ transforms.ToTensor(),
164
+ transforms.Normalize(mean=mean, std=std),
165
+ ]
166
+ )
167
+
168
+ # Inference pipeline typically uses Resize(256) + CenterCrop(224).
169
+ # ์ถ”๋ก  ํŒŒ์ดํ”„๋ผ์ธ์€ ๋ณดํ†ต Resize(256) + CenterCrop(224)๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
170
+ return transforms.Compose(
171
+ [
172
+ transforms.Resize(256),
173
+ transforms.CenterCrop(224),
174
+ transforms.ToTensor(),
175
+ transforms.Normalize(mean=mean, std=std),
176
+ ]
177
+ )
178
+
179
+ # ============================================================
180
+ # Serialization
181
+ # ์ง๋ ฌํ™”
182
+ # ============================================================
183
+ def to_dict(self) -> dict[str, Any]:
184
+ """
185
+ Return a JSON-serializable dict for preprocessor_config.json.
186
+ preprocessor_config.json์— ๋“ค์–ด๊ฐˆ JSON ์ง๋ ฌํ™” dict๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
187
+
188
+ Important: do not leak runtime objects into the serialized dict.
189
+ ์ค‘์š”: ๋Ÿฐํƒ€์ž„ ๊ฐ์ฒด๊ฐ€ ์ง๋ ฌํ™” dict์— ์„ž์ด๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
190
+ """
191
+ # ImageProcessingMixin.to_dict() adds metadata such as image_processor_type/auto_map.
192
+ # ImageProcessingMixin.to_dict()๋Š” image_processor_type/auto_map ๊ฐ™์€ ๋ฉ”ํƒ€๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
193
+ d = super().to_dict()
194
+
195
+ # Force minimal stable fields for long-term compatibility.
196
+ # ์žฅ๊ธฐ ํ˜ธํ™˜์„ ์œ„ํ•ด ์ตœ์†Œ ์•ˆ์ • ํ•„๋“œ๋ฅผ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค.
197
+ d["image_processor_type"] = self.__class__.__name__
198
+ d["backbone_name_or_path"] = self.backbone_name_or_path
199
+ d["is_training"] = self.is_training
200
+
201
+ # Remove any runtime-only fields defensively.
202
+ # ๋Ÿฐํƒ€์ž„ ์ „์šฉ ํ•„๋“œ๋Š” ๋ณด์ˆ˜์ ์œผ๋กœ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.
203
+ for key in ["_meta", "_delegate", "_timm_transform", "_torchvision_transform"]:
204
+ d.pop(key, None)
205
+
206
+ return d
207
+
208
+ @classmethod
209
+ def from_dict(cls, image_processor_dict: dict[str, Any], **kwargs):
210
+ """
211
+ Standard load path used by BaseImageProcessor / AutoImageProcessor.
212
+ BaseImageProcessor / AutoImageProcessor๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” ํ‘œ์ค€ ๋กœ๋“œ ๊ฒฝ๋กœ์ž…๋‹ˆ๋‹ค.
213
+ """
214
+ backbone = image_processor_dict.get("backbone_name_or_path", None)
215
+ if backbone is None:
216
+ raise ValueError("preprocessor_config.json missing key: backbone_name_or_path")
217
+
218
+ is_training = bool(image_processor_dict.get("is_training", False))
219
+ return cls(backbone_name_or_path=backbone, is_training=is_training, **kwargs)
220
+
221
+ @classmethod
222
+ def from_pretrained(cls, pretrained_model_name_or_path: str, **kwargs):
223
+ """
224
+ Fallback path if AutoImageProcessor calls class.from_pretrained directly.
225
+ AutoImageProcessor๊ฐ€ class.from_pretrained๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋Š” ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•œ ๊ฒฝ๋กœ์ž…๋‹ˆ๋‹ค.
226
+
227
+ Strategy:
228
+ ์ „๋žต:
229
+
230
+ - Read config.json via AutoConfig and recover backbone_name_or_path.
231
+ AutoConfig๋กœ config.json์„ ์ฝ๊ณ  backbone_name_or_path๋ฅผ ๋ณต๊ตฌํ•ฉ๋‹ˆ๋‹ค.
232
+ """
233
+ cfg = AutoConfig.from_pretrained(pretrained_model_name_or_path, **kwargs)
234
+ backbone = getattr(cfg, "backbone_name_or_path", None)
235
+ if backbone is None:
236
+ raise ValueError("Cannot build processor: backbone_name_or_path not found in config.json")
237
+
238
+ # is_training is runtime-only and should default to False for inference/serving.
239
+ # is_training์€ ๋Ÿฐํƒ€์ž„ ์ „์šฉ์ด๋ฉฐ ์ถ”๋ก /์„œ๋น™ ๊ธฐ๋ณธ๊ฐ’์€ False๊ฐ€ ๋งž์Šต๋‹ˆ๋‹ค.
240
+ return cls(backbone_name_or_path=backbone, is_training=False)
241
+
242
+ # ============================================================
243
+ # Call interface
244
+ # ํ˜ธ์ถœ ์ธํ„ฐํŽ˜์ด์Šค
245
+ # ============================================================
246
+ @staticmethod
247
+ def _ensure_list(images: Any) -> list[Any]:
248
+ # Normalize scalar image input to a list for uniform processing.
249
+ # ๋‹จ์ผ ์ž…๋ ฅ์„ ๋ฆฌ์ŠคํŠธ๋กœ ์ •๊ทœํ™”ํ•˜์—ฌ ๋™์ผํ•œ ์ฒ˜๋ฆฌ ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
250
+ if isinstance(images, (list, tuple)):
251
+ return list(images)
252
+ return [images]
253
+
254
+ @staticmethod
255
+ def _to_pil_rgb(x: Any):
256
+ # Convert common image inputs into PIL RGB images.
257
+ # ์ผ๋ฐ˜์ ์ธ ์ž…๋ ฅ์„ PIL RGB ์ด๋ฏธ์ง€๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
258
+ from PIL import Image as PILImage
259
+
260
+ if isinstance(x, PILImage.Image):
261
+ return x.convert("RGB")
262
+ if isinstance(x, np.ndarray) and x.ndim == 3:
263
+ return PILImage.fromarray(x).convert("RGB")
264
+ raise TypeError(f"Unsupported image type: {type(x)}")
265
+
266
+ def __call__(
267
+ self,
268
+ images: Any | list[Any],
269
+ return_tensors: str | TensorType | None = "pt",
270
+ **kwargs,
271
+ ) -> dict[str, Any]:
272
+ """
273
+ Convert images into {"pixel_values": Tensor/ndarray}.
274
+ ์ด๋ฏธ์ง€๋ฅผ {"pixel_values": Tensor/ndarray}๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
275
+ """
276
+ images = self._ensure_list(images)
277
+
278
+ # Rebuild runtime if needed (e.g., right after deserialization).
279
+ # ์ง๋ ฌํ™” ๋ณต์› ์งํ›„ ๋“ฑ ๋Ÿฐํƒ€์ž„์ด ๋น„์–ด์žˆ์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์žฌ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
280
+ if (self._delegate is None) and (self._timm_transform is None) and (self._torchvision_transform is None):
281
+ self._build_runtime()
282
+
283
+ # timm path: PIL -> torch.Tensor(C,H,W) normalized float32.
284
+ # timm ๊ฒฝ๋กœ: PIL -> torch.Tensor(C,H,W) ์ •๊ทœํ™” float32์ž…๋‹ˆ๋‹ค.
285
+ if self._timm_transform is not None:
286
+ pv: list[torch.Tensor] = []
287
+ for im in images:
288
+ pil = self._to_pil_rgb(im)
289
+ t = self._timm_transform(pil)
290
+ if not isinstance(t, torch.Tensor):
291
+ raise RuntimeError("Unexpected timm transform output (expected torch.Tensor).")
292
+ pv.append(t)
293
+ pixel_values = torch.stack(pv, dim=0) # (B,C,H,W)
294
+ return self._format_return(pixel_values, return_tensors)
295
+
296
+ # torchvision path: PIL -> torch.Tensor(C,H,W) normalized float32.
297
+ # torchvision ๊ฒฝ๋กœ: PIL -> torch.Tensor(C,H,W) ์ •๊ทœํ™” float32์ž…๋‹ˆ๋‹ค.
298
+ if self._torchvision_transform is not None:
299
+ pv: list[torch.Tensor] = []
300
+ for im in images:
301
+ pil = self._to_pil_rgb(im)
302
+ t = self._torchvision_transform(pil)
303
+ if not isinstance(t, torch.Tensor):
304
+ raise RuntimeError("Unexpected torchvision transform output (expected torch.Tensor).")
305
+ pv.append(t)
306
+ pixel_values = torch.stack(pv, dim=0) # (B,C,H,W)
307
+ return self._format_return(pixel_values, return_tensors)
308
+
309
+ # transformers delegate path: rely on official processor behavior.
310
+ # transformers ์œ„์ž„ ๊ฒฝ๋กœ: ๊ณต์‹ processor ๋™์ž‘์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
311
+ if self._delegate is None:
312
+ raise RuntimeError("Processor runtime not built: delegate is None and no transforms are available.")
313
+
314
+ return self._delegate(images, return_tensors=return_tensors, **kwargs)
315
+
316
+ @staticmethod
317
+ def _format_return(pixel_values: torch.Tensor, return_tensors: str | TensorType | None) -> dict[str, Any]:
318
+ """
319
+ Format pixel_values according to return_tensors.
320
+ return_tensors์— ๋งž์ถฐ pixel_values ๋ฐ˜ํ™˜ ํฌ๋งท์„ ๋งž์ถฅ๋‹ˆ๋‹ค.
321
+ """
322
+ if return_tensors is None or return_tensors in ("pt", TensorType.PYTORCH):
323
+ return {"pixel_values": pixel_values}
324
+ if return_tensors in ("np", TensorType.NUMPY):
325
+ return {"pixel_values": pixel_values.detach().cpu().numpy()}
326
+ raise ValueError(f"Unsupported return_tensors={return_tensors}. Use 'pt' or 'np'.")
327
+
328
+
329
+ # Register this processor for AutoImageProcessor resolution.
330
+ # AutoImageProcessor ํ•ด์„์„ ์œ„ํ•ด ์ด processor๋ฅผ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.
331
+ if __name__ != "__main__":
332
+ BackboneMLPHead224ImageProcessor.register_for_auto_class("AutoImageProcessor")
models/microsoft__resnet-50/model.safetensors CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:12f287e717506b8e251dddbc02e8471a753795da5c1ea503c3491817f61f8c63
3
  size 96388660
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:229f77319aec60efa8fff986455e304b9f4f2d46440550b69490da095a11d170
3
  size 96388660
models/microsoft__resnet-50/preprocessor_config.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "auto_map": {
3
+ "AutoImageProcessor": "ds_proc.BackboneMLPHead224ImageProcessor"
4
+ },
5
+ "backbone_name_or_path": "microsoft/resnet-50",
6
+ "image_processor_type": "BackboneMLPHead224ImageProcessor",
7
+ "is_training": false
8
+ }
models/microsoft__swin-tiny-patch4-window7-224/config.json CHANGED
@@ -24,10 +24,11 @@
24
  "num_labels": 3,
25
  "transformers_version": "5.1.0",
26
  "ds_provenance": {
27
- "created_at": "20260210_050905",
28
  "repo_id": "dsaint31/bb_mlp_224",
29
  "subdir": "models/microsoft__swin-tiny-patch4-window7-224",
30
  "wrapper_class": "BackboneWithMLPHeadForImageClassification",
 
31
  "backbone_id": "microsoft/swin-tiny-patch4-window7-224",
32
  "pretrained_injected": true,
33
  "policy_init_pretrained_forbidden": true,
 
24
  "num_labels": 3,
25
  "transformers_version": "5.1.0",
26
  "ds_provenance": {
27
+ "created_at": "20260210_111913",
28
  "repo_id": "dsaint31/bb_mlp_224",
29
  "subdir": "models/microsoft__swin-tiny-patch4-window7-224",
30
  "wrapper_class": "BackboneWithMLPHeadForImageClassification",
31
+ "processor_class": "BackboneMLPHead224ImageProcessor",
32
  "backbone_id": "microsoft/swin-tiny-patch4-window7-224",
33
  "pretrained_injected": true,
34
  "policy_init_pretrained_forbidden": true,
models/microsoft__swin-tiny-patch4-window7-224/ds_cfg.py CHANGED
@@ -1,84 +1,175 @@
1
- from typing import Literal, Union
2
-
3
- from transformers import AutoConfig
4
  from transformers import PretrainedConfig
 
5
  from ds_meta import BackboneID, BACKBONE_META
6
 
7
- # ============================================================
8
- # 3) Config
9
- # ============================================================
10
  class BackboneMLPHeadConfig(PretrainedConfig):
11
  """
12
- backbone_name_or_path๋Š” BACKBONE_META ํ‚ค(ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ)๋งŒ ํ—ˆ์šฉ.
13
-
14
- ์ค‘์š”ํ•œ ์ 
15
- ----------
16
- - transformers ๋‚ด๋ถ€ ๊ฒฝ๋กœ์—์„œ "๋ฌด์ธ์ž ์ƒ์„ฑ"์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ,
17
- backbone_name_or_path=None์ธ ๊ฒฝ์šฐ๋Š” ๊ฒ€์ฆ ์—†์ด ๊ธฐ๋ณธ๊ฐ’๋งŒ ์„ธํŒ…ํ•˜๊ณ  return
18
- ํ•˜๋Š” ๊ตฌํ˜„์ด ๋ฐ˜๋“œ์‹œ ํ•„์š”ํ•จ.
19
- - ๋‹จ, ์‹ค์ œ ์‚ฌ์šฉ์—์„œ๋Š” backbone_name_or_path๋ฅผ ๋ฐ˜๋“œ์‹œ ์ง€์ •ํ•ด์„œ ์ƒ์„ฑํ•ด์•ผ ํ•จ.
20
  """
21
 
22
- # AutoConfig๊ฐ€ ์ด ๋ชจ๋ธ์„ ์‹๋ณ„ํ•˜๋Š”๋ฐ ์‚ฌ์šฉํ•˜๋Š” ๋ฌธ์ž์—ด ID.
 
23
  model_type = "backbone-mlphead-224-fixed"
24
 
25
- # ์ƒ์„ฑ์ž์—์„œ ๋ชจ๋“  ์ธ์ž์— ๊ธฐ๋ณธ๊ฐ’ ๋ถ€์—ฌ ๋ฐ˜๋“œ์‹œ ํ•„์š”!
26
  def __init__(
27
  self,
28
  backbone_name_or_path: BackboneID | None = None,
29
  mlp_head_bottleneck: int = 256,
30
  mlp_head_dropout: float = 0.2,
31
- label2id: dict[str, int]|None = None,
32
- id2label: dict[int, str]|None = None,
33
  **kwargs,
34
  ):
35
- # ๋ฌด์ธ์ž ์ƒ์„ฑ ๊ฒฝ๋กœ ๋ฐฉ์–ด
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  if backbone_name_or_path is None:
 
 
 
 
 
 
 
37
  super().__init__(**kwargs)
 
 
 
38
  self.backbone_name_or_path = None
39
- self.mlp_head_bottleneck = int(mlp_head_bottleneck)
40
- self.mlp_head_dropout = float(mlp_head_dropout)
41
- # num_labels๋„ ๊ธฐ๋ณธ๊ฐ’ ํ•˜๋‚˜ ์ค˜์„œ 0 ๋ฐฉ์ง€(3์œผ๋กœ ๋‘๋“  0์œผ๋กœ ๋‘๋“  ์ •์ฑ…)
42
- # ์ตœ์†Œ ์•ˆ์ „: id2label์ด ์žˆ์œผ๋ฉด ๊ทธ ๊ธธ์ด, ์—†์œผ๋ฉด 0
 
 
 
 
 
 
 
43
  return
44
 
45
- # ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๊ฒ€์ฆ
 
 
 
 
 
 
 
 
46
  if backbone_name_or_path not in BACKBONE_META:
47
  raise ValueError(
48
  f"Unsupported backbone_name_or_path={backbone_name_or_path}. "
49
  f"Allowed: {sorted(BACKBONE_META.keys())}"
50
  )
51
- if id2label is None and label2id is None:
52
- raise ValueError("id2label/label2id ๋‘˜ ์ค‘ ํ•˜๋‚˜๋Š” ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.")
53
- if id2label is None:
54
- id2label = {v: k for k, v in label2id.items()}
55
- if label2id is None:
56
- label2id = {v: k for k, v in id2label.items()}
57
-
58
- num_labels = len(id2label)
59
- # kwargs์— ์ด๋ฏธ num_labels๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ
60
- if "num_labels" not in kwargs:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  kwargs["num_labels"] = num_labels
62
 
 
 
 
 
 
 
63
  super().__init__(
64
- label2id=label2id,
65
- id2label=id2label,
66
  **kwargs,
67
  )
68
-
69
- # 4) ์ค‘์š”: ๋ถ€๋ชจ ํ•„๋“œ์—๋„ num_labels ๋ฐ˜์˜(์ €์žฅ/๋กœ๋“œ ์•ˆ์ •)
70
- self.num_labels = num_labels
71
- self.label2id = label2id
72
- self.id2label = id2label
73
 
 
 
 
 
 
 
74
  self.backbone_name_or_path = backbone_name_or_path
75
- self.mlp_head_bottleneck = int(mlp_head_bottleneck)
76
- self.mlp_head_dropout = float(mlp_head_dropout)
 
 
 
 
77
 
78
  def to_dict(self):
 
 
79
  output = super().to_dict()
80
- # ๋ช…์‹œ์ ์œผ๋กœ ํฌํ•จ
81
- output["num_labels"] = self.num_labels
 
 
 
 
82
  return output
83
 
 
 
 
84
  BackboneMLPHeadConfig.register_for_auto_class("AutoConfig")
 
 
 
 
1
  from transformers import PretrainedConfig
2
+
3
  from ds_meta import BackboneID, BACKBONE_META
4
 
5
+
 
 
6
  class BackboneMLPHeadConfig(PretrainedConfig):
7
  """
8
+ Configuration for Backbone + MLP Head models.
9
+
10
+ Backbone + MLP Head ๋ชจ๋ธ์„ ์œ„ํ•œ ์„ค์ • ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.
 
 
 
 
 
11
  """
12
 
13
+ # This string is used by Hugging Face AutoConfig to identify the config class.
14
+ # Hugging Face AutoConfig๊ฐ€ ์ด config๋ฅผ ์‹๋ณ„ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” ๊ณ ์œ  ID์ž…๋‹ˆ๋‹ค.
15
  model_type = "backbone-mlphead-224-fixed"
16
 
 
17
  def __init__(
18
  self,
19
  backbone_name_or_path: BackboneID | None = None,
20
  mlp_head_bottleneck: int = 256,
21
  mlp_head_dropout: float = 0.2,
22
+ label2id: dict[str, int] | None = None,
23
+ id2label: dict[int, str] | None = None,
24
  **kwargs,
25
  ):
26
+ # ============================================================
27
+ # 0) Guard for argument-less construction
28
+ # 0) ๋ฌด์ธ์ž ์ƒ์„ฑ ๊ฒฝ๋กœ ๋ฐฉ์–ด
29
+ # ============================================================
30
+ # Transformers may internally construct this config without arguments
31
+ # (e.g., during AutoConfig resolution or Hub loading).
32
+ # Transformers ๋‚ด๋ถ€์—์„œ AutoConfig ๋˜๋Š” Hub ๋กœ๋“œ ๊ณผ์ • ์ค‘
33
+ # ์ธ์ž ์—†์ด config๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ฒฝ๋กœ๊ฐ€ ์‹ค์ œ๋กœ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.
34
+ #
35
+ # In this case, we must NOT validate or raise errors.
36
+ # ์ด ๊ฒฝ์šฐ ๊ฒ€์ฆ์ด๋‚˜ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
37
+ #
38
+ # The goal here is to provide a minimal, serialization-safe config.
39
+ # ๋ชฉํ‘œ๋Š” ์ตœ์†Œํ•œ์˜ ๊ฐ’๋งŒ ์ฑ„์›Œ ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™”๊ฐ€ ๊นจ์ง€์ง€ ์•Š๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.
40
  if backbone_name_or_path is None:
41
+ # num_labels may be implicitly assumed by downstream code,
42
+ # so we explicitly set a safe default.
43
+ # num_labels๋Š” downstream ์ฝ”๋“œ์—์„œ ์•”๋ฌต์ ์œผ๋กœ ๊ฐ€์ •๋˜๋ฏ€๋กœ
44
+ # ์•ˆ์ „ํ•œ ๊ธฐ๋ณธ๊ฐ’์„ ๋ช…์‹œ์ ์œผ๋กœ ๋„ฃ์–ด์ค๋‹ˆ๋‹ค.
45
+ if "num_labels" not in kwargs:
46
+ kwargs["num_labels"] = 0
47
+
48
  super().__init__(**kwargs)
49
+
50
+ # Backbone is intentionally unset in this path.
51
+ # ์ด ๊ฒฝ๋กœ์—์„œ๋Š” backbone์„ ์˜๋„์ ์œผ๋กœ ์„ค์ •ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
52
  self.backbone_name_or_path = None
53
+
54
+ # Store MLP head hyperparameters for completeness.
55
+ # MLP head ๊ด€๋ จ ํ•˜์ดํผํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ํ˜•ํƒœ ์œ ์ง€๋ฅผ ์œ„ํ•ด ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
56
+ self.mlp_head_bottleneck = int(mlp_head_bottleneck)
57
+ self.mlp_head_dropout = float(mlp_head_dropout)
58
+
59
+ # Empty label mappings ensure stable save/load behavior.
60
+ # ๋นˆ label ๋งคํ•‘์€ ์ €์žฅ/๋กœ๋“œ ์•ˆ์ •์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.
61
+ self.label2id = {}
62
+ self.id2label = {}
63
+ self.num_labels = int(kwargs.get("num_labels", 0))
64
  return
65
 
66
+ # ============================================================
67
+ # 1) Backbone whitelist validation
68
+ # 1) Backbone ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๊ฒ€์ฆ
69
+ # ============================================================
70
+ # Only backbones explicitly registered in BACKBONE_META are allowed.
71
+ # BACKBONE_META์— ๋“ฑ๋ก๋œ backbone๋งŒ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.
72
+ #
73
+ # This prevents accidental usage of unsupported or inconsistent backbones.
74
+ # ์ง€์›๋˜์ง€ ์•Š๊ฑฐ๋‚˜ ๋ถˆ์ผ์น˜ํ•œ backbone ์‚ฌ์šฉ์„ ์›์ฒœ ์ฐจ๋‹จํ•ฉ๋‹ˆ๋‹ค.
75
  if backbone_name_or_path not in BACKBONE_META:
76
  raise ValueError(
77
  f"Unsupported backbone_name_or_path={backbone_name_or_path}. "
78
  f"Allowed: {sorted(BACKBONE_META.keys())}"
79
  )
80
+
81
+ # ============================================================
82
+ # 2) Label mapping normalization
83
+ # 2) ๋ผ๋ฒจ ๋งคํ•‘ ์ •๊ทœํ™”
84
+ # ============================================================
85
+ # Both label2id and id2label may be None during pure loading scenarios.
86
+ # ๋‹จ์ˆœ ๋กœ๋“œ(from_pretrained) ์‹œ label2id/id2label์ด None์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
87
+ #
88
+ # We allow this here to keep Hub loading and verification stable.
89
+ # Hub ๋กœ๋“œ ๋ฐ ๊ฒ€์ฆ ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด ์—ฌ๊ธฐ์„œ๋Š” ์ด๋ฅผ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.
90
+ #
91
+ # Fail-fast validation should happen at model or training level instead.
92
+ # ์‹ค์ œ ์‚ฌ์šฉ ๊ฒ€์ฆ์€ ๋ชจ๋ธ ๋˜๋Š” ํ•™์Šต ๋‹จ๊ณ„์—์„œ fail-fast๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
93
+ if label2id is None and id2label is None:
94
+ # Respect num_labels if explicitly provided, otherwise default to 0.
95
+ # num_labels๊ฐ€ ์ฃผ์–ด์กŒ๋‹ค๋ฉด ์กด์ค‘ํ•˜๊ณ , ์•„๋‹ˆ๋ฉด 0์œผ๋กœ ๋‘ก๋‹ˆ๋‹ค.
96
+ num_labels = int(kwargs.get("num_labels", 0))
97
+ label2id_norm: dict[str, int] = {}
98
+ id2label_norm: dict[int, str] = {}
99
+ else:
100
+ # If only one mapping is provided, derive the other.
101
+ # ํ•˜๋‚˜๋งŒ ์ฃผ์–ด์ง„ ๊ฒฝ์šฐ ๋‚˜๋จธ์ง€๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
102
+ if id2label is None:
103
+ id2label = {v: k for k, v in label2id.items()}
104
+ if label2id is None:
105
+ label2id = {v: k for k, v in id2label.items()}
106
+
107
+ # Ensure both mappings are consistent in size.
108
+ # ๋‘ ๋งคํ•‘์˜ ํฌ๊ธฐ๊ฐ€ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
109
+ if len(label2id) != len(id2label):
110
+ raise ValueError(
111
+ f"label2id/id2label size mismatch: "
112
+ f"{len(label2id)} vs {len(id2label)}"
113
+ )
114
+
115
+ num_labels = len(id2label)
116
+ label2id_norm = dict(label2id)
117
+ id2label_norm = dict(id2label)
118
+
119
+ # ============================================================
120
+ # 3) num_labels consistency enforcement
121
+ # 3) num_labels ์ผ๊ด€์„ฑ ๊ฐ•์ œ
122
+ # ============================================================
123
+ # If num_labels is provided via kwargs, it must match inferred labels.
124
+ # kwargs๋กœ num_labels๊ฐ€ ๋“ค์–ด์˜จ ๊ฒฝ์šฐ ์ถ”๋ก ๋œ ๊ฐ’๊ณผ ๋ฐ˜๋“œ์‹œ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
125
+ if "num_labels" in kwargs:
126
+ if (label2id is not None or id2label is not None) and int(kwargs["num_labels"]) != num_labels:
127
+ raise ValueError(
128
+ f"kwargs['num_labels']={kwargs['num_labels']} "
129
+ f"!= inferred num_labels={num_labels}"
130
+ )
131
+ else:
132
  kwargs["num_labels"] = num_labels
133
 
134
+ # ============================================================
135
+ # 4) Parent initialization
136
+ # 4) ๋ถ€๋ชจ ํด๋ž˜์Šค ์ดˆ๊ธฐํ™”
137
+ # ============================================================
138
+ # Initialize PretrainedConfig with normalized label mappings.
139
+ # ์ •๊ทœํ™”๋œ ๋ผ๋ฒจ ๋งคํ•‘์„ ์‚ฌ์šฉํ•˜์—ฌ PretrainedConfig๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
140
  super().__init__(
141
+ label2id=label2id_norm,
142
+ id2label=id2label_norm,
143
  **kwargs,
144
  )
 
 
 
 
 
145
 
146
+ # ============================================================
147
+ # 5) Explicit attribute assignment for save/load stability
148
+ # 5) ์ €์žฅ/๋กœ๋“œ ์•ˆ์ •์„ฑ์„ ์œ„ํ•œ ๋ช…์‹œ์  ์†์„ฑ ๊ณ ์ •
149
+ # ============================================================
150
+ # Explicitly reassign critical fields to avoid subtle serialization issues.
151
+ # ๋ฏธ๋ฌ˜ํ•œ ์ง๋ ฌํ™” ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ํ•ต์‹ฌ ํ•„๋“œ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ๋‹ค์‹œ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
152
  self.backbone_name_or_path = backbone_name_or_path
153
+ self.mlp_head_bottleneck = int(mlp_head_bottleneck)
154
+ self.mlp_head_dropout = float(mlp_head_dropout)
155
+
156
+ self.label2id = label2id_norm
157
+ self.id2label = id2label_norm
158
+ self.num_labels = int(kwargs["num_labels"])
159
 
160
  def to_dict(self):
161
+ # Call the parent implementation first.
162
+ # ๋จผ์ € ๋ถ€๋ชจ ๊ตฌํ˜„์„ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
163
  output = super().to_dict()
164
+
165
+ # Force num_labels to be present and consistent.
166
+ # num_labels๊ฐ€ ๋ฐ˜๋“œ์‹œ ์กด์žฌํ•˜๊ณ  ์ผ๊ด€๋˜๋„๋ก ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค.
167
+ output["num_labels"] = int(
168
+ getattr(self, "num_labels", output.get("num_labels", 0))
169
+ )
170
  return output
171
 
172
+
173
+ # Register this config so it can be resolved via AutoConfig.
174
+ # AutoConfig๋ฅผ ํ†ตํ•ด ์ด config๊ฐ€ ํ•ด์„๋  ์ˆ˜ ์žˆ๋„๋ก ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.
175
  BackboneMLPHeadConfig.register_for_auto_class("AutoConfig")
models/microsoft__swin-tiny-patch4-window7-224/ds_model.py CHANGED
@@ -1,10 +1,9 @@
1
  #!/usr/bin/env python3
2
  # -*- coding: utf-8 -*-
3
 
4
- from __future__ import annotations
5
 
6
- from dataclasses import dataclass
7
- from typing import Dict, Any, Optional, Tuple, Set, List
8
 
9
  import torch
10
  import torch.nn as nn
@@ -12,146 +11,88 @@ import torch.nn.functional as F
12
 
13
  # --- transformers core ---
14
  from transformers.modeling_utils import PreTrainedModel
15
- from transformers import AutoModel, AutoConfig, PretrainedConfig
16
  from transformers.modeling_outputs import ImageClassifierOutput
17
 
18
  # --- torchvision ---
19
  from torchvision import models as tv_models
20
 
21
-
22
- # ============================================================
23
- # (0) Config
24
- # - ๊ธฐ์กด ds_cfg.BackboneMLPHeadConfig์™€ ํ•„๋“œ๋ช…์„ ๋งž์ถ”์—ˆ์Šต๋‹ˆ๋‹ค.
25
- # - ์ด๋ฏธ ds_cfg.py๊ฐ€ ์žˆ๋‹ค๋ฉด: ์ด ๋ธ”๋ก์„ ์ง€์šฐ๊ณ  "from ds_cfg import BackboneMLPHeadConfig"๋กœ ๋ฐ”๊ฟ”๋„ ๋ฉ๋‹ˆ๋‹ค.
26
- # ============================================================
27
- class BackboneMLPHeadConfig(PretrainedConfig):
28
- model_type = "backbone_mlphead"
29
-
30
- def __init__(
31
- self,
32
- backbone_name_or_path: Optional[str] = None,
33
- num_labels: int = 2,
34
- id2label: Optional[Dict[int, str]] = None,
35
- label2id: Optional[Dict[str, int]] = None,
36
- mlp_head_bottleneck: int = 256,
37
- mlp_head_dropout: float = 0.2,
38
- **kwargs,
39
- ):
40
- super().__init__(**kwargs)
41
- self.backbone_name_or_path = backbone_name_or_path
42
- self.num_labels = num_labels
43
- self.id2label = id2label or {i: f"LABEL_{i}" for i in range(num_labels)}
44
- self.label2id = label2id or {v: k for k, v in self.id2label.items()}
45
- self.mlp_head_bottleneck = mlp_head_bottleneck
46
- self.mlp_head_dropout = mlp_head_dropout
47
-
48
-
49
- # ============================================================
50
- # (1) BACKBONE_META
51
- # - ์ด๋ฏธ ds_meta.py๊ฐ€ ์žˆ๋‹ค๋ฉด: ์ด ๋ธ”๋ก์„ ์ง€์šฐ๊ณ  "from ds_meta import BACKBONE_META"๋กœ ๋ฐ”๊ฟ”๋„ ๋ฉ๋‹ˆ๋‹ค.
52
- # ============================================================
53
- BACKBONE_META: Dict[str, Dict[str, Any]] = {
54
- # transformers
55
- "google/vit-base-patch16-224": {
56
- "type": "vit",
57
- "feat_rule": "cls",
58
- "feat_dim": 768,
59
- "has_bn": False,
60
- "unfreeze": "last_n",
61
- },
62
- "microsoft/swin-tiny-patch4-window7-224": {
63
- "type": "swin",
64
- "feat_rule": "pool_or_mean",
65
- "feat_dim": 768,
66
- "has_bn": False,
67
- "unfreeze": "last_n",
68
- },
69
- "microsoft/resnet-50": {
70
- "type": "resnet",
71
- "feat_rule": "pool_or_gap",
72
- "feat_dim": 2048,
73
- "has_bn": True,
74
- "unfreeze": "last_n",
75
- },
76
- "google/efficientnet-b0": {
77
- "type": "efficientnet",
78
- "feat_rule": "pool_or_gap",
79
- "feat_dim": 1280,
80
- "has_bn": True,
81
- "unfreeze": "last_n",
82
- },
83
-
84
- # densenet
85
- "timm/densenet121.tv_in1k": {
86
- "type": "timm_densenet",
87
- "feat_rule": "timm_gap",
88
- "feat_dim": 1024,
89
- "has_bn": True,
90
- "unfreeze": "last_n",
91
- },
92
- "torchvision/densenet121": {
93
- "type": "torchvision_densenet",
94
- "feat_rule": "torchvision_densenet_gap",
95
- "feat_dim": 1024,
96
- "has_bn": True,
97
- "unfreeze": "last_n",
98
- },
99
- }
100
-
101
-
102
- # ============================================================
103
- # (2) MLP Head
104
- # - ์ด๋ฏธ mlp_head.py๊ฐ€ ์žˆ๋‹ค๋ฉด: ์ด ๋ธ”๋ก์„ ์ง€์šฐ๊ณ  "from mlp_head import MLPHead"๋กœ ๋ฐ”๊ฟ”๋„ ๋ฉ๋‹ˆ๋‹ค.
105
- # ============================================================
106
- class MLPHead(nn.Module):
107
- def __init__(self, in_dim: int, num_labels: int, bottleneck: int = 256, p: float = 0.2):
108
- super().__init__()
109
- self.fc2 = nn.Linear(in_dim, bottleneck)
110
- self.act2 = nn.GELU()
111
- self.drop2 = nn.Dropout(p)
112
- self.fc3 = nn.Linear(bottleneck, num_labels)
113
-
114
- def forward(self, x: torch.Tensor) -> torch.Tensor:
115
- x = self.drop2(self.act2(self.fc2(x)))
116
- return self.fc3(x)
117
-
118
 
119
  # ============================================================
120
  # (3) Model: backbone + MLP head
121
- # ํ•ต์‹ฌ ์„ค๊ณ„:
122
- # - __init__: backbone ๊ตฌ์กฐ(skeleton)๋งŒ ์ƒ์„ฑ. pretrained๋ฅผ ์ ˆ๋Œ€ ๋กœ๋“œํ•˜์ง€ ์•Š์Œ.
123
- # - pretrained ์ฃผ์ž…: model.load_backbone_pretrained_()๋ฅผ "fresh-start์—์„œ๋งŒ" ์ง์ ‘ ํ˜ธ์ถœ.
124
- # - from_pretrained override ์—†์Œ: HF ๊ธฐ๋ณธ ๋กœ๋”๊ฐ€ checkpoint๋ฅผ ๊ทธ๋Œ€๋กœ ๋ณต์›.
125
  # ============================================================
 
 
 
 
 
 
 
 
126
  class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
 
 
127
  config_class = BackboneMLPHeadConfig
128
 
129
  def __init__(self, config: BackboneMLPHeadConfig):
 
 
130
  super().__init__(config)
131
 
 
 
 
 
 
132
  if config.backbone_name_or_path is None:
133
- raise ValueError("config.backbone_name_or_path is None. Provide a valid backbone id.")
 
 
 
134
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  self._meta = BACKBONE_META[config.backbone_name_or_path]
136
 
137
- # (์ค‘์š”) backbone์€ ํ•ญ์ƒ skeleton๋งŒ ์ƒ์„ฑ
 
138
  self.backbone = self._build_backbone_skeleton(config.backbone_name_or_path)
139
 
140
- # head
 
141
  self.classifier = MLPHead(
142
  in_dim=int(self._meta["feat_dim"]),
143
- num_labels=config.num_labels,
144
- bottleneck=config.mlp_head_bottleneck,
145
- p=config.mlp_head_dropout,
146
  )
147
 
148
- # HF init hook (init_weights๋Š” head-only๋กœ override๋จ)
 
149
  self.post_init()
150
 
151
  def init_weights(self):
152
  """
153
- HF์˜ ๊ธฐ๋ณธ init_weights()๋Š” ๋ชจ๋ธ ์ „์ฒด์— apply(...)๋ฅผ ๊ฑธ ์ˆ˜ ์žˆ์œผ๋‹ˆ,
154
- head๋งŒ ์ดˆ๊ธฐํ™”ํ•œ๋‹ค.
 
 
 
155
  """
156
  if getattr(self, "classifier", None) is not None:
157
  self.classifier.apply(self._init_weights)
@@ -159,8 +100,11 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
159
 
160
  # ----------------------------
161
  # backbone skeleton builders
 
162
  # ----------------------------
163
  def _build_backbone_skeleton(self, backbone_id: str) -> nn.Module:
 
 
164
  meta = BACKBONE_META[backbone_id]
165
  t = meta["type"]
166
 
@@ -170,18 +114,24 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
170
  if t == "torchvision_densenet":
171
  return self._build_torchvision_densenet_skeleton(backbone_id)
172
 
173
- # transformers: config๋กœ ๊ตฌ์กฐ๋งŒ ์ƒ์„ฑ
 
174
  bb_cfg = AutoConfig.from_pretrained(backbone_id)
175
  return AutoModel.from_config(bb_cfg)
176
 
177
  @staticmethod
178
  def _build_timm_densenet_skeleton(hf_repo_id: str) -> nn.Module:
 
 
179
  try:
180
  import timm
181
  except Exception as e:
182
- raise ImportError("DenseNet(timm) backbone requires `timm`. Install: pip install timm") from e
 
 
183
 
184
- # ๊ตฌ์กฐ๋งŒ (pretrained=False)
 
185
  return timm.create_model(
186
  f"hf_hub:{hf_repo_id}",
187
  pretrained=False,
@@ -190,33 +140,33 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
190
 
191
  @staticmethod
192
  def _build_torchvision_densenet_skeleton(model_id: str) -> nn.Module:
193
- mapping = {
194
- "torchvision/densenet121": "densenet121",
195
- "torchvision/densenet169": "densenet169",
196
- "torchvision/densenet201": "densenet201",
197
- "torchvision/densenet161": "densenet161",
198
- }
199
- if model_id not in mapping:
200
- raise ValueError(f"Unsupported torchvision DenseNet id: {model_id}")
201
-
202
- arch = mapping[model_id]
203
- fn = getattr(tv_models, arch)
204
-
205
- # ๊ตฌ์กฐ๋งŒ (weights=None)
206
- m = fn(weights=None).eval()
207
  return m
208
 
209
  # ------------------------------------------------------------
210
- # pretrained ๋กœ๋”ฉ์€ "๋ช…์‹œ์  ํ˜ธ์ถœ"๋กœ๋งŒ ์ˆ˜ํ–‰ (fresh-start ์ „์šฉ)
 
211
  # ------------------------------------------------------------
212
  @torch.no_grad()
213
- def load_backbone_pretrained_(self, *, low_cpu_mem_usage: bool = False, device_map=None):
 
 
 
 
 
214
  """
215
- fresh-start ์ „์šฉ:
216
- model = BackboneWithMLPHeadForImageClassification(cfg)
217
- model.load_backbone_pretrained_()
218
 
219
- checkpoint reload(from_pretrained)์—์„œ๋Š” ํ˜ธ์ถœํ•˜์ง€ ๋งˆ์‹ญ์‹œ์˜ค.
 
220
  """
221
  bb = self.config.backbone_name_or_path
222
  meta = BACKBONE_META[bb]
@@ -230,48 +180,58 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
230
  self._load_torchvision_pretrained_into_skeleton_(bb)
231
  return
232
 
233
- # transformers pretrained -> skeleton์— ์ฃผ์ž…
 
234
  ref = AutoModel.from_pretrained(
235
  bb,
236
  low_cpu_mem_usage=low_cpu_mem_usage,
237
  device_map=device_map,
238
  )
 
 
 
239
  self.backbone.load_state_dict(ref.state_dict(), strict=False)
240
  del ref
241
 
242
  @torch.no_grad()
243
  def _load_timm_pretrained_into_skeleton_(self, hf_repo_id: str):
 
 
244
  import timm
 
 
 
245
  ref = timm.create_model(
246
  f"hf_hub:{hf_repo_id}",
247
  pretrained=True,
248
  num_classes=0,
249
  ).eval()
 
250
  self.backbone.load_state_dict(ref.state_dict(), strict=True)
251
  del ref
252
 
253
  @torch.no_grad()
254
  def _load_torchvision_pretrained_into_skeleton_(self, model_id: str):
255
- mapping = {
256
- "torchvision/densenet121": ("densenet121", tv_models.DenseNet121_Weights.DEFAULT),
257
- "torchvision/densenet169": ("densenet169", tv_models.DenseNet169_Weights.DEFAULT),
258
- "torchvision/densenet201": ("densenet201", tv_models.DenseNet201_Weights.DEFAULT),
259
- "torchvision/densenet161": ("densenet161", tv_models.DenseNet161_Weights.DEFAULT),
260
- }
261
- if model_id not in mapping:
262
- raise ValueError(f"Unsupported torchvision DenseNet id: {model_id}")
263
-
264
- arch, weights = mapping[model_id]
265
- fn = getattr(tv_models, arch)
266
- ref = fn(weights=weights).eval()
267
  self.backbone.load_state_dict(ref.state_dict(), strict=True)
268
  del ref
269
 
270
  # ----------------------------
271
  # feature extraction
 
272
  # ----------------------------
273
  @staticmethod
274
  def _pool_or_gap(outputs) -> torch.Tensor:
 
 
275
  if hasattr(outputs, "pooler_output") and outputs.pooler_output is not None:
276
  x = outputs.pooler_output
277
  if x.dim() == 2:
@@ -280,29 +240,42 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
280
  return x.flatten(1)
281
  raise RuntimeError(f"Unexpected pooler_output shape: {tuple(x.shape)}")
282
 
 
 
283
  x = outputs.last_hidden_state
284
  if x.dim() == 4:
285
  return x.mean(dim=(2, 3))
 
286
  raise RuntimeError(
287
  "Expected pooler_output or (B,C,H,W) last_hidden_state for CNN backbones. "
288
  f"Got last_hidden_state shape={tuple(x.shape)}"
289
  )
290
 
291
  def _extract_features(self, outputs, pixel_values: Optional[torch.Tensor] = None) -> torch.Tensor:
 
 
292
  rule = self._meta["feat_rule"]
293
 
294
  if rule == "cls":
 
 
295
  return outputs.last_hidden_state[:, 0, :]
296
 
297
  if rule == "pool_or_mean":
 
 
298
  if hasattr(outputs, "pooler_output") and outputs.pooler_output is not None:
299
  return outputs.pooler_output
300
  return outputs.last_hidden_state.mean(dim=1)
301
 
302
  if rule == "pool_or_gap":
 
 
303
  return self._pool_or_gap(outputs)
304
 
305
  if rule == "timm_gap":
 
 
306
  if not isinstance(outputs, torch.Tensor):
307
  raise TypeError(f"timm_gap expects Tensor features, got {type(outputs)}")
308
  if outputs.dim() != 4:
@@ -310,6 +283,8 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
310
  return outputs.mean(dim=(2, 3))
311
 
312
  if rule == "torchvision_densenet_gap":
 
 
313
  if not isinstance(outputs, torch.Tensor):
314
  raise TypeError(f"torchvision_densenet_gap expects Tensor, got {type(outputs)}")
315
  if outputs.dim() != 4:
@@ -327,9 +302,13 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
327
  return_dict=True,
328
  **kwargs,
329
  ):
 
 
330
  t = self._meta["type"]
331
 
332
  if t == "timm_densenet":
 
 
333
  if pixel_values is None:
334
  raise ValueError("timm DenseNet backbone requires pixel_values.")
335
  if pixel_values.dim() != 4:
@@ -341,6 +320,8 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
341
  attentions = None
342
 
343
  elif t == "torchvision_densenet":
 
 
344
  if pixel_values is None:
345
  raise ValueError("torchvision DenseNet backbone requires pixel_values.")
346
  if pixel_values.dim() != 4:
@@ -353,6 +334,8 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
353
  attentions = None
354
 
355
  else:
 
 
356
  outputs = self.backbone(
357
  pixel_values=pixel_values,
358
  output_attentions=output_attentions,
@@ -364,10 +347,14 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
364
  hidden_states = getattr(outputs, "hidden_states", None)
365
  attentions = getattr(outputs, "attentions", None)
366
 
 
 
367
  logits = self.classifier(feats)
368
 
369
  loss = None
370
  if labels is not None:
 
 
371
  loss = F.cross_entropy(logits, labels)
372
 
373
  if not return_dict:
@@ -383,20 +370,27 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
383
 
384
 
385
  # ============================================================
386
- # Freeze / Unfreeze utilities (๋‹น์‹  ์ฝ”๋“œ ์œ ์ง€)
 
387
  # ============================================================
388
  def _set_requires_grad(module: nn.Module, flag: bool):
 
 
389
  for p in module.parameters():
390
  p.requires_grad = flag
391
 
392
 
393
  def set_bn_eval(module: nn.Module):
 
 
394
  for m in module.modules():
395
  if isinstance(m, (nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d, nn.SyncBatchNorm)):
396
  m.eval()
397
 
398
 
399
  def freeze_backbone(model: BackboneWithMLPHeadForImageClassification, freeze_bn: bool = True):
 
 
400
  _set_requires_grad(model.backbone, False)
401
  _set_requires_grad(model.classifier, True)
402
 
@@ -406,6 +400,8 @@ def freeze_backbone(model: BackboneWithMLPHeadForImageClassification, freeze_bn:
406
 
407
 
408
  def finetune_train_mode(model: BackboneWithMLPHeadForImageClassification, keep_bn_eval: bool = True):
 
 
409
  model.train()
410
  meta = BACKBONE_META[model.config.backbone_name_or_path]
411
  if keep_bn_eval and meta.get("has_bn", False):
@@ -413,6 +409,8 @@ def finetune_train_mode(model: BackboneWithMLPHeadForImageClassification, keep_b
413
 
414
 
415
  def trainable_summary(model: nn.Module):
 
 
416
  total = sum(p.numel() for p in model.parameters())
417
  trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
418
  ratio = trainable / total if total > 0 else 0.0
@@ -425,6 +423,8 @@ def unfreeze_last_stage(
425
  last_n: int = 2,
426
  keep_bn_eval: bool = True,
427
  ):
 
 
428
  freeze_backbone(model, freeze_bn=keep_bn_eval)
429
 
430
  n = int(last_n)
@@ -438,12 +438,16 @@ def unfreeze_last_stage(
438
  bb_type = meta["type"]
439
 
440
  if bb_type == "vit":
 
 
441
  blocks = list(model.backbone.encoder.layer)
442
  for blk in blocks[-n:]:
443
  _set_requires_grad(blk, True)
444
  return
445
 
446
  if bb_type == "swin":
 
 
447
  stages = list(model.backbone.encoder.layers)
448
  blocks: List[nn.Module] = []
449
  for st in stages:
@@ -453,6 +457,8 @@ def unfreeze_last_stage(
453
  return
454
 
455
  if bb_type == "resnet":
 
 
456
  bb = model.backbone
457
  for name in ("layer1", "layer2", "layer3", "layer4"):
458
  if not hasattr(bb, name):
@@ -472,6 +478,8 @@ def unfreeze_last_stage(
472
  return
473
 
474
  if bb_type == "efficientnet":
 
 
475
  bb = model.backbone
476
  if not hasattr(bb, "features"):
477
  raise RuntimeError("Unexpected EfficientNet structure: missing features")
@@ -488,6 +496,8 @@ def unfreeze_last_stage(
488
  return
489
 
490
  if bb_type in ("timm_densenet", "torchvision_densenet"):
 
 
491
  bb = model.backbone
492
  if not hasattr(bb, "features"):
493
  raise RuntimeError("Unexpected DenseNet: missing features")
@@ -505,6 +515,8 @@ def unfreeze_last_stage(
505
  raise RuntimeError(f"Unexpected DenseNet features: missing {name}")
506
 
507
  def _denselayers(db: nn.Module) -> List[nn.Module]:
 
 
508
  return list(db.children())
509
 
510
  blocks: List[nn.Module] = []
@@ -526,20 +538,8 @@ def unfreeze_last_stage(
526
 
527
  # -------------------------
528
  # register
 
 
 
 
529
  BackboneWithMLPHeadForImageClassification.register_for_auto_class("AutoModelForImageClassification")
530
-
531
-
532
- # ==========================================================
533
-
534
- # # fresh-start
535
- # cfg = BackboneMLPHeadConfig(backbone_name_or_path="timm/densenet121.tv_in1k", num_labels=3)
536
- # model = BackboneWithMLPHeadForImageClassification(cfg)
537
-
538
- # # pretrained๋ฅผ ์“ฐ๋ ค๋ฉด ๋ช…์‹œ์ ์œผ๋กœ ํ˜ธ์ถœ
539
- # model.load_backbone_pretrained_()
540
-
541
- # # ์ดํ›„ ํ•™์Šต
542
-
543
- # # checkpoint์—์„œ ์‹œ์ž‘
544
- # model = BackboneWithMLPHeadForImageClassification.from_pretrained("checkpoints/xxx")
545
- # # ์—ฌ๊ธฐ์„œ๋Š” load_backbone_pretrained_() ํ˜ธ์ถœํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
 
1
  #!/usr/bin/env python3
2
  # -*- coding: utf-8 -*-
3
 
4
+ # src/ds_model.py
5
 
6
+ from typing import Optional, List
 
7
 
8
  import torch
9
  import torch.nn as nn
 
11
 
12
  # --- transformers core ---
13
  from transformers.modeling_utils import PreTrainedModel
14
+ from transformers import AutoModel, AutoConfig
15
  from transformers.modeling_outputs import ImageClassifierOutput
16
 
17
  # --- torchvision ---
18
  from torchvision import models as tv_models
19
 
20
+ from ds_cfg import BackboneMLPHeadConfig
21
+ from ds_meta import BACKBONE_META
22
+ from mlp_head import MLPHead
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  # ============================================================
25
  # (3) Model: backbone + MLP head
26
+ # (3) ๋ชจ๋ธ: backbone + MLP head
 
 
 
27
  # ============================================================
28
+ # Design principle: __init__ builds only a skeleton and MUST NOT load pretrained weights.
29
+ # ์„ค๊ณ„ ์›์น™: __init__์—์„œ๋Š” skeleton๋งŒ ๋งŒ๋“ค๊ณ  pretrained weight ๋กœ๋“œ๋Š” ์ ˆ๋Œ€ ํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
30
+ #
31
+ # Pretrained injection is allowed ONLY via an explicit call in fresh-start flows.
32
+ # pretrained ์ฃผ์ž…์€ fresh-start ํ๋ฆ„์—์„œ์˜ ๋ช…์‹œ์  ํ˜ธ์ถœ๋กœ๋งŒ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.
33
+ #
34
+ # HF from_pretrained should restore checkpoints as-is without side effects.
35
+ # HF from_pretrained๋Š” ๋ถ€์ž‘์šฉ ์—†์ด ์ฒดํฌํฌ์ธํŠธ๋ฅผ ๊ทธ๋Œ€๋กœ ๋ณต์›ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
36
  class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
37
+ # This links the model to its custom config for AutoClass usage.
38
+ # AutoClass ์‚ฌ์šฉ์„ ์œ„ํ•ด ๋ชจ๋ธ๊ณผ ์ปค์Šคํ…€ config๋ฅผ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค.
39
  config_class = BackboneMLPHeadConfig
40
 
41
  def __init__(self, config: BackboneMLPHeadConfig):
42
+ # PreTrainedModel expects a config object and stores it internally.
43
+ # PreTrainedModel์€ config ๊ฐ์ฒด๋ฅผ ๋ฐ›์•„ ๋‚ด๋ถ€์— ์ €์žฅํ•˜๋Š” ์ „์ œ๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค.
44
  super().__init__(config)
45
 
46
+ # Fail-fast: the model is not meant to be instantiated without a valid backbone id.
47
+ # fail-fast: ์œ ํšจํ•œ backbone id ์—†์ด ๋ชจ๋ธ์„ ๋งŒ๋“œ๋Š” ์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค๋Š” ํ—ˆ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
48
+ #
49
+ # Note: Transformers may create configs with no args, but models are conventionally created with configs.
50
+ # ์ฐธ๊ณ : Transformers๋Š” config ๋ฌด์ธ์ž ์ƒ์„ฑ์ด ์žˆ์„ ์ˆ˜ ์žˆ์œผ๋‚˜, ๋ชจ๋ธ์€ ๊ด€๋ก€์ ์œผ๋กœ config๋ฅผ ๋ฐ›์•„ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.
51
  if config.backbone_name_or_path is None:
52
+ raise ValueError(
53
+ "config.backbone_name_or_path is None. "
54
+ "Provide a valid backbone id (whitelist key in BACKBONE_META)."
55
+ )
56
 
57
+ # Fail-fast: training/inference requires a positive number of labels.
58
+ # fail-fast: ํ•™์Šต/์ถ”๋ก ์€ num_labels๊ฐ€ ์–‘์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.
59
+ #
60
+ # Config may exist in a minimal form for internal serialization paths, but the model should not.
61
+ # config๋Š” ๋‚ด๋ถ€ ์ง๋ ฌํ™” ๊ฒฝ๋กœ์—์„œ ์ตœ์†Œ ํ˜•ํƒœ๋กœ ์กด์žฌํ•  ์ˆ˜ ์žˆ์œผ๋‚˜ ๋ชจ๋ธ์€ ๊ทธ๋Ÿฌ๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
62
+ if int(getattr(config, "num_labels", 0)) <= 0:
63
+ raise ValueError(
64
+ f"config.num_labels must be > 0, got {getattr(config, 'num_labels', None)}. "
65
+ "Set num_labels (or id2label/label2id) when creating the config."
66
+ )
67
+
68
+ # Meta is a single source of truth for extraction and fine-tuning rules.
69
+ # meta๋Š” feature ์ถ”์ถœ ๋ฐ ๋ฏธ์„ธ์กฐ์ • ๊ทœ์น™์˜ ๋‹จ์ผ ๊ธฐ์ค€์ž…๋‹ˆ๋‹ค.
70
  self._meta = BACKBONE_META[config.backbone_name_or_path]
71
 
72
+ # Backbone skeleton is always created without pretrained weights.
73
+ # backbone skeleton์€ ํ•ญ์ƒ pretrained weight ์—†์ด ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
74
  self.backbone = self._build_backbone_skeleton(config.backbone_name_or_path)
75
 
76
+ # Head shape is driven by meta feat_dim and config.num_labels.
77
+ # head shape์€ meta์˜ feat_dim๊ณผ config.num_labels๋กœ ๊ฒฐ์ •๋ฉ๋‹ˆ๋‹ค.
78
  self.classifier = MLPHead(
79
  in_dim=int(self._meta["feat_dim"]),
80
+ num_labels=int(config.num_labels),
81
+ bottleneck=int(config.mlp_head_bottleneck),
82
+ p=float(config.mlp_head_dropout),
83
  )
84
 
85
+ # HF initialization hook, but we override init_weights to initialize head-only.
86
+ # HF ์ดˆ๊ธฐํ™” ํ›…์ด์ง€๋งŒ init_weights๋ฅผ overrideํ•˜์—ฌ head๋งŒ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
87
  self.post_init()
88
 
89
  def init_weights(self):
90
  """
91
+ Initialize only the head to avoid touching the backbone skeleton.
92
+ backbone skeleton์„ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๊ธฐ ์œ„ํ•ด head๋งŒ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
93
+
94
+ HF's default init may traverse the entire module tree, which is undesirable here.
95
+ HF ๊ธฐ๋ณธ init์€ ์ „์ฒด ๋ชจ๋“ˆ ํŠธ๋ฆฌ๋ฅผ ์ˆœํšŒํ•  ์ˆ˜ ์žˆ์–ด ์—ฌ๊ธฐ์„œ๋Š” ๋ถ€์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.
96
  """
97
  if getattr(self, "classifier", None) is not None:
98
  self.classifier.apply(self._init_weights)
 
100
 
101
  # ----------------------------
102
  # backbone skeleton builders
103
+ # backbone skeleton ์ƒ์„ฑ๊ธฐ
104
  # ----------------------------
105
  def _build_backbone_skeleton(self, backbone_id: str) -> nn.Module:
106
+ # Meta decides which loader path to use.
107
+ # meta๊ฐ€ ์–ด๋–ค ๋กœ๋” ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ• ์ง€ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.
108
  meta = BACKBONE_META[backbone_id]
109
  t = meta["type"]
110
 
 
114
  if t == "torchvision_densenet":
115
  return self._build_torchvision_densenet_skeleton(backbone_id)
116
 
117
+ # For transformers backbones: build a random-weight skeleton from config only.
118
+ # transformers ๋ฐฑ๋ณธ: config๋กœ๋ถ€ํ„ฐ ๋žœ๋ค ์ดˆ๊ธฐํ™” skeleton๋งŒ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
119
  bb_cfg = AutoConfig.from_pretrained(backbone_id)
120
  return AutoModel.from_config(bb_cfg)
121
 
122
  @staticmethod
123
  def _build_timm_densenet_skeleton(hf_repo_id: str) -> nn.Module:
124
+ # timm is an optional dependency and should be imported lazily.
125
+ # timm์€ ์˜ต์…˜ ์˜์กด์„ฑ์ด๋ฏ€๋กœ ์ง€์—ฐ import ํ•ฉ๋‹ˆ๋‹ค.
126
  try:
127
  import timm
128
  except Exception as e:
129
+ raise ImportError(
130
+ "DenseNet(timm) backbone requires `timm`. Install: pip install timm"
131
+ ) from e
132
 
133
+ # Build structure only (pretrained=False) and remove classifier head (num_classes=0).
134
+ # ๊ตฌ์กฐ๋งŒ ์ƒ์„ฑ(pretrained=False)ํ•˜๊ณ  ๋ถ„๋ฅ˜๊ธฐ head๋Š” ์ œ๊ฑฐ(num_classes=0)ํ•ฉ๋‹ˆ๋‹ค.
135
  return timm.create_model(
136
  f"hf_hub:{hf_repo_id}",
137
  pretrained=False,
 
140
 
141
  @staticmethod
142
  def _build_torchvision_densenet_skeleton(model_id: str) -> nn.Module:
143
+ # This project intentionally supports only torchvision/densenet121 in the 224 whitelist.
144
+ # ์ด ํ”„๋กœ์ ํŠธ๋Š” 224 ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ์—์„œ torchvision/densenet121๋งŒ ์˜๋„์ ์œผ๋กœ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.
145
+ if model_id != "torchvision/densenet121":
146
+ raise ValueError(f"Unsupported torchvision DenseNet id (224 whitelist only): {model_id}")
147
+
148
+ # Build structure only (weights=None) to avoid implicit pretrained loading.
149
+ # implicit pretrained ๋กœ๋“œ๋ฅผ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด ๊ตฌ์กฐ๋งŒ ์ƒ์„ฑ(weights=None)ํ•ฉ๋‹ˆ๋‹ค.
150
+ m = tv_models.densenet121(weights=None)
 
 
 
 
 
 
151
  return m
152
 
153
  # ------------------------------------------------------------
154
+ # Pretrained loading is explicit and fresh-start only
155
+ # pretrained ๋กœ๋”ฉ์€ ๋ช…์‹œ์  ํ˜ธ์ถœ์ด๋ฉฐ fresh-start ์ „์šฉ
156
  # ------------------------------------------------------------
157
  @torch.no_grad()
158
+ def load_backbone_pretrained_(
159
+ self,
160
+ *,
161
+ low_cpu_mem_usage: bool = False,
162
+ device_map=None,
163
+ ):
164
  """
165
+ Fresh-start only: inject pretrained backbone weights into the skeleton.
166
+ fresh-start ์ „์šฉ: skeleton backbone์— pretrained ๊ฐ€์ค‘์น˜๋ฅผ ์ฃผ์ž…ํ•ฉ๋‹ˆ๋‹ค.
 
167
 
168
+ Do NOT call this after from_pretrained() because it would overwrite checkpoint weights.
169
+ from_pretrained() ์ดํ›„ ํ˜ธ์ถœํ•˜๋ฉด ์ฒดํฌํฌ์ธํŠธ ๊ฐ€์ค‘์น˜๋ฅผ ๋ฎ์–ด์“ฐ๋ฏ€๋กœ ์ ˆ๋Œ€ ํ˜ธ์ถœํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
170
  """
171
  bb = self.config.backbone_name_or_path
172
  meta = BACKBONE_META[bb]
 
180
  self._load_torchvision_pretrained_into_skeleton_(bb)
181
  return
182
 
183
+ # For transformers backbones, load a reference pretrained model and copy weights into our skeleton.
184
+ # transformers ๋ฐฑ๋ณธ์€ reference pretrained ๋ชจ๋ธ์„ ๋กœ๋“œํ•œ ๋’ค skeleton์— ๊ฐ€์ค‘์น˜๋ฅผ ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค.
185
  ref = AutoModel.from_pretrained(
186
  bb,
187
  low_cpu_mem_usage=low_cpu_mem_usage,
188
  device_map=device_map,
189
  )
190
+
191
+ # strict=False is used to tolerate harmless key differences across minor versions.
192
+ # strict=False๋Š” ๋งˆ์ด๋„ˆ ๋ฒ„์ „ ์ฐจ์ด๋กœ ์ธํ•œ ๋ฌดํ•ดํ•œ ํ‚ค ์ฐจ์ด๋ฅผ ํ—ˆ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
193
  self.backbone.load_state_dict(ref.state_dict(), strict=False)
194
  del ref
195
 
196
  @torch.no_grad()
197
  def _load_timm_pretrained_into_skeleton_(self, hf_repo_id: str):
198
+ # timm must be present for timm backbones.
199
+ # timm ๋ฐฑ๋ณธ์—๋Š” timm ์„ค์น˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
200
  import timm
201
+
202
+ # Create a pretrained reference model and copy its weights strictly.
203
+ # pretrained reference ๋ชจ๋ธ์„ ๋งŒ๋“ค๊ณ  ๊ฐ€์ค‘์น˜๋ฅผ strictํ•˜๊ฒŒ ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค.
204
  ref = timm.create_model(
205
  f"hf_hub:{hf_repo_id}",
206
  pretrained=True,
207
  num_classes=0,
208
  ).eval()
209
+
210
  self.backbone.load_state_dict(ref.state_dict(), strict=True)
211
  del ref
212
 
213
  @torch.no_grad()
214
  def _load_torchvision_pretrained_into_skeleton_(self, model_id: str):
215
+ # This project intentionally supports only torchvision/densenet121 in the 224 whitelist.
216
+ # ์ด ํ”„๋กœ์ ํŠธ๋Š” 224 ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ์—์„œ torchvision/densenet121๋งŒ ์˜๋„์ ์œผ๋กœ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.
217
+ if model_id != "torchvision/densenet121":
218
+ raise ValueError(f"Unsupported torchvision DenseNet id (224 whitelist only): {model_id}")
219
+
220
+ # Use torchvision's default pretrained weights for densenet121.
221
+ # torchvision์˜ densenet121 ๊ธฐ๋ณธ pretrained weights๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
222
+ ref = tv_models.densenet121(weights=tv_models.DenseNet121_Weights.DEFAULT).eval()
223
+
 
 
 
224
  self.backbone.load_state_dict(ref.state_dict(), strict=True)
225
  del ref
226
 
227
  # ----------------------------
228
  # feature extraction
229
+ # feature ์ถ”์ถœ
230
  # ----------------------------
231
  @staticmethod
232
  def _pool_or_gap(outputs) -> torch.Tensor:
233
+ # Some transformers vision CNNs provide pooler_output explicitly.
234
+ # ์ผ๋ถ€ transformers vision CNN์€ pooler_output์„ ๋ช…์‹œ์ ์œผ๋กœ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
235
  if hasattr(outputs, "pooler_output") and outputs.pooler_output is not None:
236
  x = outputs.pooler_output
237
  if x.dim() == 2:
 
240
  return x.flatten(1)
241
  raise RuntimeError(f"Unexpected pooler_output shape: {tuple(x.shape)}")
242
 
243
+ # Otherwise we expect a CNN-style last_hidden_state=(B,C,H,W) and apply GAP.
244
+ # ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด CNN ์Šคํƒ€์ผ last_hidden_state=(B,C,H,W)๋ฅผ ๊ธฐ๋Œ€ํ•˜๊ณ  GAP์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.
245
  x = outputs.last_hidden_state
246
  if x.dim() == 4:
247
  return x.mean(dim=(2, 3))
248
+
249
  raise RuntimeError(
250
  "Expected pooler_output or (B,C,H,W) last_hidden_state for CNN backbones. "
251
  f"Got last_hidden_state shape={tuple(x.shape)}"
252
  )
253
 
254
  def _extract_features(self, outputs, pixel_values: Optional[torch.Tensor] = None) -> torch.Tensor:
255
+ # Feature rule is defined by BACKBONE_META and must remain stable across saves/loads.
256
+ # feature ๊ทœ์น™์€ BACKBONE_META๋กœ ์ •์˜๋˜๋ฉฐ ์ €์žฅ/๋กœ๋“œ ๊ฐ„ ์•ˆ์ •์ ์œผ๋กœ ์œ ์ง€๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
257
  rule = self._meta["feat_rule"]
258
 
259
  if rule == "cls":
260
+ # ViT-style: use CLS token embedding from last_hidden_state.
261
+ # ViT ์Šคํƒ€์ผ: last_hidden_state์—์„œ CLS ํ† ํฐ ์ž„๋ฒ ๋”ฉ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
262
  return outputs.last_hidden_state[:, 0, :]
263
 
264
  if rule == "pool_or_mean":
265
+ # Swin-style: prefer pooler_output if present, else mean-pool over tokens.
266
+ # Swin ์Šคํƒ€์ผ: pooler_output์ด ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉํ•˜๊ณ , ์—†์œผ๋ฉด ํ† ํฐ ํ‰๊ท  ํ’€๋ง์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
267
  if hasattr(outputs, "pooler_output") and outputs.pooler_output is not None:
268
  return outputs.pooler_output
269
  return outputs.last_hidden_state.mean(dim=1)
270
 
271
  if rule == "pool_or_gap":
272
+ # CNN-style: use pooler_output if present, else GAP over spatial dims.
273
+ # CNN ์Šคํƒ€์ผ: pooler_output์ด ์žˆ์œผ๋ฉด ์‚ฌ์šฉํ•˜๊ณ , ์—†์œผ๋ฉด ๊ณต๊ฐ„ ์ฐจ์› GAP์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
274
  return self._pool_or_gap(outputs)
275
 
276
  if rule == "timm_gap":
277
+ # timm forward_features returns a feature map (B,C,H,W) which we GAP to (B,C).
278
+ # timm forward_features๋Š” (B,C,H,W) feature map์„ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ ์ด๋ฅผ GAP์œผ๋กœ (B,C)๋กœ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
279
  if not isinstance(outputs, torch.Tensor):
280
  raise TypeError(f"timm_gap expects Tensor features, got {type(outputs)}")
281
  if outputs.dim() != 4:
 
283
  return outputs.mean(dim=(2, 3))
284
 
285
  if rule == "torchvision_densenet_gap":
286
+ # torchvision DenseNet features are feature maps (B,C,H,W) and require GAP.
287
+ # torchvision DenseNet features๋Š” (B,C,H,W) feature map์ด๋ฉฐ GAP์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
288
  if not isinstance(outputs, torch.Tensor):
289
  raise TypeError(f"torchvision_densenet_gap expects Tensor, got {type(outputs)}")
290
  if outputs.dim() != 4:
 
302
  return_dict=True,
303
  **kwargs,
304
  ):
305
+ # Type decides the backbone forward path and output format.
306
+ # type์ด backbone forward ๊ฒฝ๋กœ ๋ฐ ์ถœ๋ ฅ ํฌ๋งท์„ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.
307
  t = self._meta["type"]
308
 
309
  if t == "timm_densenet":
310
+ # timm DenseNet consumes pixel_values as a 4D tensor (B,C,H,W).
311
+ # timm DenseNet์€ pixel_values๋ฅผ 4D ํ…์„œ (B,C,H,W)๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค.
312
  if pixel_values is None:
313
  raise ValueError("timm DenseNet backbone requires pixel_values.")
314
  if pixel_values.dim() != 4:
 
320
  attentions = None
321
 
322
  elif t == "torchvision_densenet":
323
+ # torchvision DenseNet consumes pixel_values as a 4D tensor (B,C,H,W).
324
+ # torchvision DenseNet์€ pixel_values๋ฅผ 4D ํ…์„œ (B,C,H,W)๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค.
325
  if pixel_values is None:
326
  raise ValueError("torchvision DenseNet backbone requires pixel_values.")
327
  if pixel_values.dim() != 4:
 
334
  attentions = None
335
 
336
  else:
337
+ # Transformers vision models are called with pixel_values and return ModelOutput.
338
+ # transformers vision ๋ชจ๋ธ์€ pixel_values๋กœ ํ˜ธ์ถœ๋˜๋ฉฐ ModelOutput์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
339
  outputs = self.backbone(
340
  pixel_values=pixel_values,
341
  output_attentions=output_attentions,
 
347
  hidden_states = getattr(outputs, "hidden_states", None)
348
  attentions = getattr(outputs, "attentions", None)
349
 
350
+ # Classifier consumes (B, feat_dim) and returns logits (B, num_labels).
351
+ # classifier๋Š” (B, feat_dim)์„ ๋ฐ›์•„ logits (B, num_labels)๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
352
  logits = self.classifier(feats)
353
 
354
  loss = None
355
  if labels is not None:
356
+ # Cross entropy expects labels as class indices in [0, num_labels).
357
+ # cross entropy๋Š” labels๊ฐ€ [0, num_labels) ๋ฒ”์œ„์˜ class index์ด๊ธธ ๊ธฐ๋Œ€ํ•ฉ๋‹ˆ๋‹ค.
358
  loss = F.cross_entropy(logits, labels)
359
 
360
  if not return_dict:
 
370
 
371
 
372
  # ============================================================
373
+ # Freeze / Unfreeze utilities
374
+ # Freeze / Unfreeze ์œ ํ‹ธ๋ฆฌํ‹ฐ
375
  # ============================================================
376
  def _set_requires_grad(module: nn.Module, flag: bool):
377
+ # Toggle requires_grad for all parameters in a module.
378
+ # ๋ชจ๋“ˆ์˜ ๋ชจ๋“  ํŒŒ๋ผ๋ฏธํ„ฐ์— ๋Œ€ํ•ด requires_grad๋ฅผ ํ† ๊ธ€ํ•ฉ๋‹ˆ๋‹ค.
379
  for p in module.parameters():
380
  p.requires_grad = flag
381
 
382
 
383
  def set_bn_eval(module: nn.Module):
384
+ # Put BatchNorm layers into eval mode to freeze running stats.
385
+ # BatchNorm ๋ ˆ์ด์–ด๋ฅผ eval ๋ชจ๋“œ๋กœ ๋‘์–ด running stats๋ฅผ ๊ณ ์ •ํ•ฉ๋‹ˆ๋‹ค.
386
  for m in module.modules():
387
  if isinstance(m, (nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d, nn.SyncBatchNorm)):
388
  m.eval()
389
 
390
 
391
  def freeze_backbone(model: BackboneWithMLPHeadForImageClassification, freeze_bn: bool = True):
392
+ # Stage1: freeze backbone and train only the head.
393
+ # stage1: backbone์„ freezeํ•˜๊ณ  head๋งŒ ํ•™์Šตํ•ฉ๋‹ˆ๋‹ค.
394
  _set_requires_grad(model.backbone, False)
395
  _set_requires_grad(model.classifier, True)
396
 
 
400
 
401
 
402
  def finetune_train_mode(model: BackboneWithMLPHeadForImageClassification, keep_bn_eval: bool = True):
403
+ # Stage2: train mode, optionally keeping BN layers in eval for stability.
404
+ # stage2: train ๋ชจ๋“œ๋กœ ๋‘๋˜ ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด BN์„ eval๋กœ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
405
  model.train()
406
  meta = BACKBONE_META[model.config.backbone_name_or_path]
407
  if keep_bn_eval and meta.get("has_bn", False):
 
409
 
410
 
411
  def trainable_summary(model: nn.Module):
412
+ # Print a compact summary of trainable parameters.
413
+ # ํ•™์Šต ๊ฐ€๋Šฅ ํŒŒ๋ผ๋ฏธํ„ฐ ์š”์•ฝ์„ ๊ฐ„๋‹จํžˆ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.
414
  total = sum(p.numel() for p in model.parameters())
415
  trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
416
  ratio = trainable / total if total > 0 else 0.0
 
423
  last_n: int = 2,
424
  keep_bn_eval: bool = True,
425
  ):
426
+ # This utility implements BACKBONE_META['unfreeze']=="last_n" across supported backbones.
427
+ # ์ด ์œ ํ‹ธ์€ ์ง€์› ๋ฐฑ๋ณธ๋“ค์— ๋Œ€ํ•ด BACKBONE_META['unfreeze']=="last_n"์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
428
  freeze_backbone(model, freeze_bn=keep_bn_eval)
429
 
430
  n = int(last_n)
 
438
  bb_type = meta["type"]
439
 
440
  if bb_type == "vit":
441
+ # ViT blocks live under backbone.encoder.layer in the transformers implementation.
442
+ # ViT ๋ธ”๋ก์€ transformers ๊ตฌํ˜„์—์„œ backbone.encoder.layer ์•„๋ž˜์— ์žˆ์Šต๋‹ˆ๋‹ค.
443
  blocks = list(model.backbone.encoder.layer)
444
  for blk in blocks[-n:]:
445
  _set_requires_grad(blk, True)
446
  return
447
 
448
  if bb_type == "swin":
449
+ # Swin blocks are nested by stages and blocks; we flatten and unfreeze last n blocks.
450
+ # Swin ๋ธ”๋ก์€ stage์™€ block์œผ๋กœ ์ค‘์ฒฉ๋˜์–ด ์žˆ์–ด ํŽผ์นœ ํ›„ ๋งˆ์ง€๋ง‰ n๊ฐœ๋ฅผ unfreeze ํ•ฉ๋‹ˆ๋‹ค.
451
  stages = list(model.backbone.encoder.layers)
452
  blocks: List[nn.Module] = []
453
  for st in stages:
 
457
  return
458
 
459
  if bb_type == "resnet":
460
+ # ResNet uses layer1..layer4 stages; we unfreeze at block granularity.
461
+ # ResNet์€ layer1..layer4 stage๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ block ๋‹จ์œ„๋กœ unfreeze ํ•ฉ๋‹ˆ๋‹ค.
462
  bb = model.backbone
463
  for name in ("layer1", "layer2", "layer3", "layer4"):
464
  if not hasattr(bb, name):
 
478
  return
479
 
480
  if bb_type == "efficientnet":
481
+ # EfficientNet in transformers exposes features; we unfreeze from the tail blocks.
482
+ # transformers EfficientNet์€ features๋ฅผ ๋…ธ์ถœํ•˜๋ฉฐ ๋’ค์ชฝ ๋ธ”๋ก๋ถ€ํ„ฐ unfreeze ํ•ฉ๋‹ˆ๋‹ค.
483
  bb = model.backbone
484
  if not hasattr(bb, "features"):
485
  raise RuntimeError("Unexpected EfficientNet structure: missing features")
 
496
  return
497
 
498
  if bb_type in ("timm_densenet", "torchvision_densenet"):
499
+ # DenseNet exposes a .features module with named blocks; we unfreeze last n submodules.
500
+ # DenseNet์€ .features ๋ชจ๋“ˆ์— ๋ธ”๋ก๋“ค์ด ์ด๋ฆ„์œผ๋กœ ์กด์žฌํ•˜๋ฉฐ ๋งˆ์ง€๋ง‰ n๊ฐœ ์„œ๋ธŒ๋ชจ๋“ˆ์„ unfreeze ํ•ฉ๋‹ˆ๋‹ค.
501
  bb = model.backbone
502
  if not hasattr(bb, "features"):
503
  raise RuntimeError("Unexpected DenseNet: missing features")
 
515
  raise RuntimeError(f"Unexpected DenseNet features: missing {name}")
516
 
517
  def _denselayers(db: nn.Module) -> List[nn.Module]:
518
+ # Dense blocks contain multiple DenseLayer children; we return them for fine-grained unfreezing.
519
+ # denseblock์€ DenseLayer ์ž์‹๋“ค์„ ๊ฐ€์ง€๋ฏ€๋กœ ์„ธ๋ฐ€ํ•œ unfreeze๋ฅผ ์œ„ํ•ด ์ด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
520
  return list(db.children())
521
 
522
  blocks: List[nn.Module] = []
 
538
 
539
  # -------------------------
540
  # register
541
+ # register
542
+ # -------------------------
543
+ # Register for AutoModelForImageClassification so from_pretrained can resolve this custom class.
544
+ # from_pretrained๊ฐ€ ์ด ์ปค์Šคํ…€ ํด๋ž˜์Šค๋ฅผ ํ•ด์„ํ•  ์ˆ˜ ์žˆ๋„๋ก AutoModelForImageClassification์— ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.
545
  BackboneWithMLPHeadForImageClassification.register_for_auto_class("AutoModelForImageClassification")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
models/microsoft__swin-tiny-patch4-window7-224/ds_proc.py ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # src/ds_proc.py
5
+
6
+ # ============================================================
7
+ # (4) ImageProcessor (AutoImageProcessor integration)
8
+ # (4) ImageProcessor (AutoImageProcessor ์—ฐ๋™)
9
+ # ============================================================
10
+
11
+ from typing import Any
12
+
13
+ import numpy as np
14
+ import torch
15
+ from transformers import AutoImageProcessor, AutoConfig
16
+ from transformers.image_processing_base import ImageProcessingMixin
17
+ from transformers.utils.generic import TensorType
18
+
19
+ from ds_meta import BackboneID, BACKBONE_META
20
+
21
+
22
+ class BackboneMLPHead224ImageProcessor(ImageProcessingMixin):
23
+ """
24
+ This processor performs image preprocessing and outputs {"pixel_values": ...}.
25
+ ์ด processor๋Š” ์ด๋ฏธ์ง€ ์ „์ฒ˜๋ฆฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ณ  {"pixel_values": ...}๋ฅผ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.
26
+
27
+ Key requirements:
28
+ ํ•ต์‹ฌ ์š”๊ตฌ์‚ฌํ•ญ:
29
+
30
+ 1) save_pretrained() must produce a JSON-serializable preprocessor_config.json.
31
+ save_pretrained()๋Š” JSON ์ง๋ ฌํ™” ๊ฐ€๋Šฅํ•œ preprocessor_config.json์„ ์ƒ์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
32
+
33
+ 2) Runtime-only objects (delegate processor, timm/torchvision transforms) must NOT be serialized.
34
+ ๋Ÿฐํƒ€์ž„ ๊ฐ์ฒด(delegate processor, timm/torchvision transform)๋Š” ์ ˆ๋Œ€ ์ง๋ ฌํ™”ํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
35
+
36
+ 3) Runtime objects are rebuilt at init/load time based on backbone meta.
37
+ ๋Ÿฐํƒ€์ž„ ๊ฐ์ฒด๋Š” backbone meta์— ๋”ฐ๋ผ init/load ์‹œ์ ์— ์žฌ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค.
38
+ """
39
+
40
+ # HF vision models conventionally expect "pixel_values" as the primary input key.
41
+ # HF vision ๋ชจ๋ธ์€ ๊ด€๋ก€์ ์œผ๋กœ ์ž…๋ ฅ ํ‚ค๋กœ "pixel_values"๋ฅผ ๊ธฐ๋Œ€ํ•ฉ๋‹ˆ๋‹ค.
42
+ model_input_names = ["pixel_values"]
43
+
44
+ def __init__(self, backbone_name_or_path: BackboneID, is_training: bool = False, **kwargs):
45
+ # ImageProcessingMixin stores extra kwargs and manages auto_map metadata.
46
+ # ImageProcessingMixin์€ ์ถ”๊ฐ€ kwargs๋ฅผ ์ €์žฅํ•˜๊ณ  auto_map ๋ฉ”ํƒ€๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
47
+ super().__init__(**kwargs)
48
+
49
+ # Enforce whitelist via BACKBONE_META to keep behavior stable.
50
+ # ๋™์ž‘ ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด BACKBONE_META ๊ธฐ๋ฐ˜ ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค.
51
+ if backbone_name_or_path not in BACKBONE_META:
52
+ raise ValueError(
53
+ f"Unsupported backbone_name_or_path={backbone_name_or_path}. "
54
+ f"Allowed: {sorted(BACKBONE_META.keys())}"
55
+ )
56
+
57
+ # Serializable fields only: these should appear in preprocessor_config.json.
58
+ # ์ง๋ ฌํ™” ๊ฐ€๋Šฅํ•œ ํ•„๋“œ๋งŒ: ์ด ๊ฐ’๋“ค๋งŒ preprocessor_config.json์— ๋“ค์–ด๊ฐ€์•ผ ํ•ฉ๋‹ˆ๋‹ค.
59
+ self.backbone_name_or_path = backbone_name_or_path
60
+ self.is_training = bool(is_training)
61
+
62
+ # Runtime-only fields: must never be serialized.
63
+ # ๋Ÿฐํƒ€์ž„ ์ „์šฉ ํ•„๋“œ: ์ ˆ๋Œ€ ์ง๋ ฌํ™”๋˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
64
+ self._meta = None
65
+ self._delegate = None
66
+ self._timm_transform = None
67
+ self._torchvision_transform = None
68
+
69
+ # Build runtime objects according to backbone type.
70
+ # backbone type์— ๋”ฐ๋ผ ๋Ÿฐํƒ€์ž„ ๊ฐ์ฒด๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
71
+ self._build_runtime()
72
+
73
+ # ============================================================
74
+ # Runtime builders
75
+ # ๋Ÿฐํƒ€์ž„ ๋นŒ๋”
76
+ # ============================================================
77
+ def _build_runtime(self):
78
+ """
79
+ Build runtime delegate/transform based on BACKBONE_META["type"].
80
+ BACKBONE_META["type"]์— ๋”ฐ๋ผ ๋Ÿฐํƒ€์ž„ delegate/transform์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
81
+ """
82
+ meta = BACKBONE_META[self.backbone_name_or_path]
83
+ self._meta = meta
84
+
85
+ # Always reset runtime fields before rebuilding.
86
+ # ์žฌ๊ตฌ์„ฑ ์ „ ๋Ÿฐํƒ€์ž„ ํ•„๋“œ๋Š” ํ•ญ์ƒ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
87
+ self._delegate = None
88
+ self._timm_transform = None
89
+ self._torchvision_transform = None
90
+
91
+ t = meta["type"]
92
+
93
+ if t == "timm_densenet":
94
+ # timm DenseNet uses timm.data transforms for ImageNet-style preprocessing.
95
+ # timm DenseNet์€ ImageNet ์ „์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด timm.data transform์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
96
+ self._timm_transform = self._build_timm_transform(
97
+ backbone_id=self.backbone_name_or_path,
98
+ is_training=self.is_training,
99
+ )
100
+ return
101
+
102
+ if t == "torchvision_densenet":
103
+ # torchvision DenseNet requires torchvision-style preprocessing (resize/crop/tensor/normalize).
104
+ # torchvision DenseNet์€ torchvision ์Šคํƒ€์ผ ์ „์ฒ˜๋ฆฌ(resize/crop/tensor/normalize)๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
105
+ self._torchvision_transform = self._build_torchvision_densenet_transform(
106
+ is_training=self.is_training
107
+ )
108
+ return
109
+
110
+ # Default: transformers backbone delegates to its official AutoImageProcessor.
111
+ # ๊ธฐ๋ณธ: transformers ๋ฐฑ๋ณธ์€ ๊ณต์‹ AutoImageProcessor์— ์œ„์ž„ํ•ฉ๋‹ˆ๋‹ค.
112
+ self._delegate = AutoImageProcessor.from_pretrained(self.backbone_name_or_path)
113
+
114
+ @staticmethod
115
+ def _build_timm_transform(*, backbone_id: str, is_training: bool):
116
+ """
117
+ Create timm transform without storing non-serializable objects in config.
118
+ ๋น„์ง๋ ฌํ™” ๊ฐ์ฒด๋ฅผ config์— ์ €์žฅํ•˜์ง€ ์•Š๊ณ  timm transform์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
119
+ """
120
+ try:
121
+ import timm
122
+ from timm.data import resolve_model_data_config, create_transform
123
+ except Exception as e:
124
+ raise ImportError(
125
+ "timm backbone processor requires `timm`. Install: pip install timm"
126
+ ) from e
127
+
128
+ # We only need model metadata to resolve data config, so pretrained=False is preferred.
129
+ # data config ์ถ”์ถœ๋งŒ ํ•„์š”ํ•˜๋ฏ€๋กœ pretrained=False๋ฅผ ์šฐ์„  ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
130
+ m = timm.create_model(f"hf_hub:{backbone_id}", pretrained=False, num_classes=0)
131
+ dc = resolve_model_data_config(m)
132
+
133
+ # create_transform returns a torchvision-like callable that maps PIL -> torch.Tensor(C,H,W).
134
+ # create_transform์€ PIL -> torch.Tensor(C,H,W)๋กœ ๋งคํ•‘ํ•˜๋Š” callable์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
135
+ tfm = create_transform(**dc, is_training=is_training)
136
+ return tfm
137
+
138
+ @staticmethod
139
+ def _build_torchvision_densenet_transform(*, is_training: bool):
140
+ """
141
+ Build torchvision preprocessing for DenseNet-121 (224 pipeline).
142
+ DenseNet-121์šฉ torchvision ์ „์ฒ˜๋ฆฌ(224 ํŒŒ์ดํ”„๋ผ์ธ)๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
143
+ """
144
+ try:
145
+ from torchvision import transforms
146
+ except Exception as e:
147
+ raise ImportError(
148
+ "torchvision DenseNet processor requires `torchvision`. Install: pip install torchvision"
149
+ ) from e
150
+
151
+ # These are the standard ImageNet normalization stats used by torchvision weights.
152
+ # ์ด ๊ฐ’๋“ค์€ torchvision weights๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” ํ‘œ์ค€ ImageNet ์ •๊ทœํ™” ํ†ต๊ณ„์ž…๋‹ˆ๋‹ค.
153
+ mean = (0.485, 0.456, 0.406)
154
+ std = (0.229, 0.224, 0.225)
155
+
156
+ # Training pipeline typically uses RandomResizedCrop and horizontal flip.
157
+ # ํ•™์Šต ํŒŒ์ดํ”„๋ผ์ธ์€ ๋ณดํ†ต RandomResizedCrop๊ณผ ์ขŒ์šฐ๋ฐ˜์ „์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
158
+ if is_training:
159
+ return transforms.Compose(
160
+ [
161
+ transforms.RandomResizedCrop(224),
162
+ transforms.RandomHorizontalFlip(p=0.5),
163
+ transforms.ToTensor(),
164
+ transforms.Normalize(mean=mean, std=std),
165
+ ]
166
+ )
167
+
168
+ # Inference pipeline typically uses Resize(256) + CenterCrop(224).
169
+ # ์ถ”๋ก  ํŒŒ์ดํ”„๋ผ์ธ์€ ๋ณดํ†ต Resize(256) + CenterCrop(224)๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
170
+ return transforms.Compose(
171
+ [
172
+ transforms.Resize(256),
173
+ transforms.CenterCrop(224),
174
+ transforms.ToTensor(),
175
+ transforms.Normalize(mean=mean, std=std),
176
+ ]
177
+ )
178
+
179
+ # ============================================================
180
+ # Serialization
181
+ # ์ง๋ ฌํ™”
182
+ # ============================================================
183
+ def to_dict(self) -> dict[str, Any]:
184
+ """
185
+ Return a JSON-serializable dict for preprocessor_config.json.
186
+ preprocessor_config.json์— ๋“ค์–ด๊ฐˆ JSON ์ง๋ ฌํ™” dict๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
187
+
188
+ Important: do not leak runtime objects into the serialized dict.
189
+ ์ค‘์š”: ๋Ÿฐํƒ€์ž„ ๊ฐ์ฒด๊ฐ€ ์ง๋ ฌํ™” dict์— ์„ž์ด๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
190
+ """
191
+ # ImageProcessingMixin.to_dict() adds metadata such as image_processor_type/auto_map.
192
+ # ImageProcessingMixin.to_dict()๋Š” image_processor_type/auto_map ๊ฐ™์€ ๋ฉ”ํƒ€๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
193
+ d = super().to_dict()
194
+
195
+ # Force minimal stable fields for long-term compatibility.
196
+ # ์žฅ๊ธฐ ํ˜ธํ™˜์„ ์œ„ํ•ด ์ตœ์†Œ ์•ˆ์ • ํ•„๋“œ๋ฅผ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค.
197
+ d["image_processor_type"] = self.__class__.__name__
198
+ d["backbone_name_or_path"] = self.backbone_name_or_path
199
+ d["is_training"] = self.is_training
200
+
201
+ # Remove any runtime-only fields defensively.
202
+ # ๋Ÿฐํƒ€์ž„ ์ „์šฉ ํ•„๋“œ๋Š” ๋ณด์ˆ˜์ ์œผ๋กœ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.
203
+ for key in ["_meta", "_delegate", "_timm_transform", "_torchvision_transform"]:
204
+ d.pop(key, None)
205
+
206
+ return d
207
+
208
+ @classmethod
209
+ def from_dict(cls, image_processor_dict: dict[str, Any], **kwargs):
210
+ """
211
+ Standard load path used by BaseImageProcessor / AutoImageProcessor.
212
+ BaseImageProcessor / AutoImageProcessor๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” ํ‘œ์ค€ ๋กœ๋“œ ๊ฒฝ๋กœ์ž…๋‹ˆ๋‹ค.
213
+ """
214
+ backbone = image_processor_dict.get("backbone_name_or_path", None)
215
+ if backbone is None:
216
+ raise ValueError("preprocessor_config.json missing key: backbone_name_or_path")
217
+
218
+ is_training = bool(image_processor_dict.get("is_training", False))
219
+ return cls(backbone_name_or_path=backbone, is_training=is_training, **kwargs)
220
+
221
+ @classmethod
222
+ def from_pretrained(cls, pretrained_model_name_or_path: str, **kwargs):
223
+ """
224
+ Fallback path if AutoImageProcessor calls class.from_pretrained directly.
225
+ AutoImageProcessor๊ฐ€ class.from_pretrained๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋Š” ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•œ ๊ฒฝ๋กœ์ž…๋‹ˆ๋‹ค.
226
+
227
+ Strategy:
228
+ ์ „๋žต:
229
+
230
+ - Read config.json via AutoConfig and recover backbone_name_or_path.
231
+ AutoConfig๋กœ config.json์„ ์ฝ๊ณ  backbone_name_or_path๋ฅผ ๋ณต๊ตฌํ•ฉ๋‹ˆ๋‹ค.
232
+ """
233
+ cfg = AutoConfig.from_pretrained(pretrained_model_name_or_path, **kwargs)
234
+ backbone = getattr(cfg, "backbone_name_or_path", None)
235
+ if backbone is None:
236
+ raise ValueError("Cannot build processor: backbone_name_or_path not found in config.json")
237
+
238
+ # is_training is runtime-only and should default to False for inference/serving.
239
+ # is_training์€ ๋Ÿฐํƒ€์ž„ ์ „์šฉ์ด๋ฉฐ ์ถ”๋ก /์„œ๋น™ ๊ธฐ๋ณธ๊ฐ’์€ False๊ฐ€ ๋งž์Šต๋‹ˆ๋‹ค.
240
+ return cls(backbone_name_or_path=backbone, is_training=False)
241
+
242
+ # ============================================================
243
+ # Call interface
244
+ # ํ˜ธ์ถœ ์ธํ„ฐํŽ˜์ด์Šค
245
+ # ============================================================
246
+ @staticmethod
247
+ def _ensure_list(images: Any) -> list[Any]:
248
+ # Normalize scalar image input to a list for uniform processing.
249
+ # ๋‹จ์ผ ์ž…๋ ฅ์„ ๋ฆฌ์ŠคํŠธ๋กœ ์ •๊ทœํ™”ํ•˜์—ฌ ๋™์ผํ•œ ์ฒ˜๋ฆฌ ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
250
+ if isinstance(images, (list, tuple)):
251
+ return list(images)
252
+ return [images]
253
+
254
+ @staticmethod
255
+ def _to_pil_rgb(x: Any):
256
+ # Convert common image inputs into PIL RGB images.
257
+ # ์ผ๋ฐ˜์ ์ธ ์ž…๋ ฅ์„ PIL RGB ์ด๋ฏธ์ง€๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
258
+ from PIL import Image as PILImage
259
+
260
+ if isinstance(x, PILImage.Image):
261
+ return x.convert("RGB")
262
+ if isinstance(x, np.ndarray) and x.ndim == 3:
263
+ return PILImage.fromarray(x).convert("RGB")
264
+ raise TypeError(f"Unsupported image type: {type(x)}")
265
+
266
+ def __call__(
267
+ self,
268
+ images: Any | list[Any],
269
+ return_tensors: str | TensorType | None = "pt",
270
+ **kwargs,
271
+ ) -> dict[str, Any]:
272
+ """
273
+ Convert images into {"pixel_values": Tensor/ndarray}.
274
+ ์ด๋ฏธ์ง€๋ฅผ {"pixel_values": Tensor/ndarray}๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
275
+ """
276
+ images = self._ensure_list(images)
277
+
278
+ # Rebuild runtime if needed (e.g., right after deserialization).
279
+ # ์ง๋ ฌํ™” ๋ณต์› ์งํ›„ ๋“ฑ ๋Ÿฐํƒ€์ž„์ด ๋น„์–ด์žˆ์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์žฌ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
280
+ if (self._delegate is None) and (self._timm_transform is None) and (self._torchvision_transform is None):
281
+ self._build_runtime()
282
+
283
+ # timm path: PIL -> torch.Tensor(C,H,W) normalized float32.
284
+ # timm ๊ฒฝ๋กœ: PIL -> torch.Tensor(C,H,W) ์ •๊ทœํ™” float32์ž…๋‹ˆ๋‹ค.
285
+ if self._timm_transform is not None:
286
+ pv: list[torch.Tensor] = []
287
+ for im in images:
288
+ pil = self._to_pil_rgb(im)
289
+ t = self._timm_transform(pil)
290
+ if not isinstance(t, torch.Tensor):
291
+ raise RuntimeError("Unexpected timm transform output (expected torch.Tensor).")
292
+ pv.append(t)
293
+ pixel_values = torch.stack(pv, dim=0) # (B,C,H,W)
294
+ return self._format_return(pixel_values, return_tensors)
295
+
296
+ # torchvision path: PIL -> torch.Tensor(C,H,W) normalized float32.
297
+ # torchvision ๊ฒฝ๋กœ: PIL -> torch.Tensor(C,H,W) ์ •๊ทœํ™” float32์ž…๋‹ˆ๋‹ค.
298
+ if self._torchvision_transform is not None:
299
+ pv: list[torch.Tensor] = []
300
+ for im in images:
301
+ pil = self._to_pil_rgb(im)
302
+ t = self._torchvision_transform(pil)
303
+ if not isinstance(t, torch.Tensor):
304
+ raise RuntimeError("Unexpected torchvision transform output (expected torch.Tensor).")
305
+ pv.append(t)
306
+ pixel_values = torch.stack(pv, dim=0) # (B,C,H,W)
307
+ return self._format_return(pixel_values, return_tensors)
308
+
309
+ # transformers delegate path: rely on official processor behavior.
310
+ # transformers ์œ„์ž„ ๊ฒฝ๋กœ: ๊ณต์‹ processor ๋™์ž‘์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
311
+ if self._delegate is None:
312
+ raise RuntimeError("Processor runtime not built: delegate is None and no transforms are available.")
313
+
314
+ return self._delegate(images, return_tensors=return_tensors, **kwargs)
315
+
316
+ @staticmethod
317
+ def _format_return(pixel_values: torch.Tensor, return_tensors: str | TensorType | None) -> dict[str, Any]:
318
+ """
319
+ Format pixel_values according to return_tensors.
320
+ return_tensors์— ๋งž์ถฐ pixel_values ๋ฐ˜ํ™˜ ํฌ๋งท์„ ๋งž์ถฅ๋‹ˆ๋‹ค.
321
+ """
322
+ if return_tensors is None or return_tensors in ("pt", TensorType.PYTORCH):
323
+ return {"pixel_values": pixel_values}
324
+ if return_tensors in ("np", TensorType.NUMPY):
325
+ return {"pixel_values": pixel_values.detach().cpu().numpy()}
326
+ raise ValueError(f"Unsupported return_tensors={return_tensors}. Use 'pt' or 'np'.")
327
+
328
+
329
+ # Register this processor for AutoImageProcessor resolution.
330
+ # AutoImageProcessor ํ•ด์„์„ ์œ„ํ•ด ์ด processor๋ฅผ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.
331
+ if __name__ != "__main__":
332
+ BackboneMLPHead224ImageProcessor.register_for_auto_class("AutoImageProcessor")
models/microsoft__swin-tiny-patch4-window7-224/model.safetensors CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:045a8d82d86041b915ef92e1bfc62b1ff3dd85e034f87025c9e6e68d018c40ba
3
  size 111128348
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:61d4b000d39bc1301116050226ee05a3cc3851359f4dc38febbb5f397fbb844f
3
  size 111128348
models/microsoft__swin-tiny-patch4-window7-224/preprocessor_config.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "auto_map": {
3
+ "AutoImageProcessor": "ds_proc.BackboneMLPHead224ImageProcessor"
4
+ },
5
+ "backbone_name_or_path": "microsoft/swin-tiny-patch4-window7-224",
6
+ "image_processor_type": "BackboneMLPHead224ImageProcessor",
7
+ "is_training": false
8
+ }
models/timm__densenet121.tv_in1k/config.json CHANGED
@@ -24,10 +24,11 @@
24
  "num_labels": 3,
25
  "transformers_version": "5.1.0",
26
  "ds_provenance": {
27
- "created_at": "20260210_050905",
28
  "repo_id": "dsaint31/bb_mlp_224",
29
  "subdir": "models/timm__densenet121.tv_in1k",
30
  "wrapper_class": "BackboneWithMLPHeadForImageClassification",
 
31
  "backbone_id": "timm/densenet121.tv_in1k",
32
  "pretrained_injected": true,
33
  "policy_init_pretrained_forbidden": true,
 
24
  "num_labels": 3,
25
  "transformers_version": "5.1.0",
26
  "ds_provenance": {
27
+ "created_at": "20260210_111913",
28
  "repo_id": "dsaint31/bb_mlp_224",
29
  "subdir": "models/timm__densenet121.tv_in1k",
30
  "wrapper_class": "BackboneWithMLPHeadForImageClassification",
31
+ "processor_class": "BackboneMLPHead224ImageProcessor",
32
  "backbone_id": "timm/densenet121.tv_in1k",
33
  "pretrained_injected": true,
34
  "policy_init_pretrained_forbidden": true,
models/timm__densenet121.tv_in1k/ds_cfg.py CHANGED
@@ -1,84 +1,175 @@
1
- from typing import Literal, Union
2
-
3
- from transformers import AutoConfig
4
  from transformers import PretrainedConfig
 
5
  from ds_meta import BackboneID, BACKBONE_META
6
 
7
- # ============================================================
8
- # 3) Config
9
- # ============================================================
10
  class BackboneMLPHeadConfig(PretrainedConfig):
11
  """
12
- backbone_name_or_path๋Š” BACKBONE_META ํ‚ค(ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ)๋งŒ ํ—ˆ์šฉ.
13
-
14
- ์ค‘์š”ํ•œ ์ 
15
- ----------
16
- - transformers ๋‚ด๋ถ€ ๊ฒฝ๋กœ์—์„œ "๋ฌด์ธ์ž ์ƒ์„ฑ"์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ,
17
- backbone_name_or_path=None์ธ ๊ฒฝ์šฐ๋Š” ๊ฒ€์ฆ ์—†์ด ๊ธฐ๋ณธ๊ฐ’๋งŒ ์„ธํŒ…ํ•˜๊ณ  return
18
- ํ•˜๋Š” ๊ตฌํ˜„์ด ๋ฐ˜๋“œ์‹œ ํ•„์š”ํ•จ.
19
- - ๋‹จ, ์‹ค์ œ ์‚ฌ์šฉ์—์„œ๋Š” backbone_name_or_path๋ฅผ ๋ฐ˜๋“œ์‹œ ์ง€์ •ํ•ด์„œ ์ƒ์„ฑํ•ด์•ผ ํ•จ.
20
  """
21
 
22
- # AutoConfig๊ฐ€ ์ด ๋ชจ๋ธ์„ ์‹๋ณ„ํ•˜๋Š”๋ฐ ์‚ฌ์šฉํ•˜๋Š” ๋ฌธ์ž์—ด ID.
 
23
  model_type = "backbone-mlphead-224-fixed"
24
 
25
- # ์ƒ์„ฑ์ž์—์„œ ๋ชจ๋“  ์ธ์ž์— ๊ธฐ๋ณธ๊ฐ’ ๋ถ€์—ฌ ๋ฐ˜๋“œ์‹œ ํ•„์š”!
26
  def __init__(
27
  self,
28
  backbone_name_or_path: BackboneID | None = None,
29
  mlp_head_bottleneck: int = 256,
30
  mlp_head_dropout: float = 0.2,
31
- label2id: dict[str, int]|None = None,
32
- id2label: dict[int, str]|None = None,
33
  **kwargs,
34
  ):
35
- # ๋ฌด์ธ์ž ์ƒ์„ฑ ๊ฒฝ๋กœ ๋ฐฉ์–ด
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  if backbone_name_or_path is None:
 
 
 
 
 
 
 
37
  super().__init__(**kwargs)
 
 
 
38
  self.backbone_name_or_path = None
39
- self.mlp_head_bottleneck = int(mlp_head_bottleneck)
40
- self.mlp_head_dropout = float(mlp_head_dropout)
41
- # num_labels๋„ ๊ธฐ๋ณธ๊ฐ’ ํ•˜๋‚˜ ์ค˜์„œ 0 ๋ฐฉ์ง€(3์œผ๋กœ ๋‘๋“  0์œผ๋กœ ๋‘๋“  ์ •์ฑ…)
42
- # ์ตœ์†Œ ์•ˆ์ „: id2label์ด ์žˆ์œผ๋ฉด ๊ทธ ๊ธธ์ด, ์—†์œผ๋ฉด 0
 
 
 
 
 
 
 
43
  return
44
 
45
- # ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๊ฒ€์ฆ
 
 
 
 
 
 
 
 
46
  if backbone_name_or_path not in BACKBONE_META:
47
  raise ValueError(
48
  f"Unsupported backbone_name_or_path={backbone_name_or_path}. "
49
  f"Allowed: {sorted(BACKBONE_META.keys())}"
50
  )
51
- if id2label is None and label2id is None:
52
- raise ValueError("id2label/label2id ๋‘˜ ์ค‘ ํ•˜๋‚˜๋Š” ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.")
53
- if id2label is None:
54
- id2label = {v: k for k, v in label2id.items()}
55
- if label2id is None:
56
- label2id = {v: k for k, v in id2label.items()}
57
-
58
- num_labels = len(id2label)
59
- # kwargs์— ์ด๋ฏธ num_labels๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ
60
- if "num_labels" not in kwargs:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  kwargs["num_labels"] = num_labels
62
 
 
 
 
 
 
 
63
  super().__init__(
64
- label2id=label2id,
65
- id2label=id2label,
66
  **kwargs,
67
  )
68
-
69
- # 4) ์ค‘์š”: ๋ถ€๋ชจ ํ•„๋“œ์—๋„ num_labels ๋ฐ˜์˜(์ €์žฅ/๋กœ๋“œ ์•ˆ์ •)
70
- self.num_labels = num_labels
71
- self.label2id = label2id
72
- self.id2label = id2label
73
 
 
 
 
 
 
 
74
  self.backbone_name_or_path = backbone_name_or_path
75
- self.mlp_head_bottleneck = int(mlp_head_bottleneck)
76
- self.mlp_head_dropout = float(mlp_head_dropout)
 
 
 
 
77
 
78
  def to_dict(self):
 
 
79
  output = super().to_dict()
80
- # ๋ช…์‹œ์ ์œผ๋กœ ํฌํ•จ
81
- output["num_labels"] = self.num_labels
 
 
 
 
82
  return output
83
 
 
 
 
84
  BackboneMLPHeadConfig.register_for_auto_class("AutoConfig")
 
 
 
 
1
  from transformers import PretrainedConfig
2
+
3
  from ds_meta import BackboneID, BACKBONE_META
4
 
5
+
 
 
6
  class BackboneMLPHeadConfig(PretrainedConfig):
7
  """
8
+ Configuration for Backbone + MLP Head models.
9
+
10
+ Backbone + MLP Head ๋ชจ๋ธ์„ ์œ„ํ•œ ์„ค์ • ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.
 
 
 
 
 
11
  """
12
 
13
+ # This string is used by Hugging Face AutoConfig to identify the config class.
14
+ # Hugging Face AutoConfig๊ฐ€ ์ด config๋ฅผ ์‹๋ณ„ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” ๊ณ ์œ  ID์ž…๋‹ˆ๋‹ค.
15
  model_type = "backbone-mlphead-224-fixed"
16
 
 
17
  def __init__(
18
  self,
19
  backbone_name_or_path: BackboneID | None = None,
20
  mlp_head_bottleneck: int = 256,
21
  mlp_head_dropout: float = 0.2,
22
+ label2id: dict[str, int] | None = None,
23
+ id2label: dict[int, str] | None = None,
24
  **kwargs,
25
  ):
26
+ # ============================================================
27
+ # 0) Guard for argument-less construction
28
+ # 0) ๋ฌด์ธ์ž ์ƒ์„ฑ ๊ฒฝ๋กœ ๋ฐฉ์–ด
29
+ # ============================================================
30
+ # Transformers may internally construct this config without arguments
31
+ # (e.g., during AutoConfig resolution or Hub loading).
32
+ # Transformers ๋‚ด๋ถ€์—์„œ AutoConfig ๋˜๋Š” Hub ๋กœ๋“œ ๊ณผ์ • ์ค‘
33
+ # ์ธ์ž ์—†์ด config๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ฒฝ๋กœ๊ฐ€ ์‹ค์ œ๋กœ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.
34
+ #
35
+ # In this case, we must NOT validate or raise errors.
36
+ # ์ด ๊ฒฝ์šฐ ๊ฒ€์ฆ์ด๋‚˜ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
37
+ #
38
+ # The goal here is to provide a minimal, serialization-safe config.
39
+ # ๋ชฉํ‘œ๋Š” ์ตœ์†Œํ•œ์˜ ๊ฐ’๋งŒ ์ฑ„์›Œ ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™”๊ฐ€ ๊นจ์ง€์ง€ ์•Š๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.
40
  if backbone_name_or_path is None:
41
+ # num_labels may be implicitly assumed by downstream code,
42
+ # so we explicitly set a safe default.
43
+ # num_labels๋Š” downstream ์ฝ”๋“œ์—์„œ ์•”๋ฌต์ ์œผ๋กœ ๊ฐ€์ •๋˜๋ฏ€๋กœ
44
+ # ์•ˆ์ „ํ•œ ๊ธฐ๋ณธ๊ฐ’์„ ๋ช…์‹œ์ ์œผ๋กœ ๋„ฃ์–ด์ค๋‹ˆ๋‹ค.
45
+ if "num_labels" not in kwargs:
46
+ kwargs["num_labels"] = 0
47
+
48
  super().__init__(**kwargs)
49
+
50
+ # Backbone is intentionally unset in this path.
51
+ # ์ด ๊ฒฝ๋กœ์—์„œ๋Š” backbone์„ ์˜๋„์ ์œผ๋กœ ์„ค์ •ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
52
  self.backbone_name_or_path = None
53
+
54
+ # Store MLP head hyperparameters for completeness.
55
+ # MLP head ๊ด€๋ จ ํ•˜์ดํผํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ํ˜•ํƒœ ์œ ์ง€๋ฅผ ์œ„ํ•ด ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
56
+ self.mlp_head_bottleneck = int(mlp_head_bottleneck)
57
+ self.mlp_head_dropout = float(mlp_head_dropout)
58
+
59
+ # Empty label mappings ensure stable save/load behavior.
60
+ # ๋นˆ label ๋งคํ•‘์€ ์ €์žฅ/๋กœ๋“œ ์•ˆ์ •์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.
61
+ self.label2id = {}
62
+ self.id2label = {}
63
+ self.num_labels = int(kwargs.get("num_labels", 0))
64
  return
65
 
66
+ # ============================================================
67
+ # 1) Backbone whitelist validation
68
+ # 1) Backbone ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๊ฒ€์ฆ
69
+ # ============================================================
70
+ # Only backbones explicitly registered in BACKBONE_META are allowed.
71
+ # BACKBONE_META์— ๋“ฑ๋ก๋œ backbone๋งŒ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.
72
+ #
73
+ # This prevents accidental usage of unsupported or inconsistent backbones.
74
+ # ์ง€์›๋˜์ง€ ์•Š๊ฑฐ๋‚˜ ๋ถˆ์ผ์น˜ํ•œ backbone ์‚ฌ์šฉ์„ ์›์ฒœ ์ฐจ๋‹จํ•ฉ๋‹ˆ๋‹ค.
75
  if backbone_name_or_path not in BACKBONE_META:
76
  raise ValueError(
77
  f"Unsupported backbone_name_or_path={backbone_name_or_path}. "
78
  f"Allowed: {sorted(BACKBONE_META.keys())}"
79
  )
80
+
81
+ # ============================================================
82
+ # 2) Label mapping normalization
83
+ # 2) ๋ผ๋ฒจ ๋งคํ•‘ ์ •๊ทœํ™”
84
+ # ============================================================
85
+ # Both label2id and id2label may be None during pure loading scenarios.
86
+ # ๋‹จ์ˆœ ๋กœ๋“œ(from_pretrained) ์‹œ label2id/id2label์ด None์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
87
+ #
88
+ # We allow this here to keep Hub loading and verification stable.
89
+ # Hub ๋กœ๋“œ ๋ฐ ๊ฒ€์ฆ ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด ์—ฌ๊ธฐ์„œ๋Š” ์ด๋ฅผ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.
90
+ #
91
+ # Fail-fast validation should happen at model or training level instead.
92
+ # ์‹ค์ œ ์‚ฌ์šฉ ๊ฒ€์ฆ์€ ๋ชจ๋ธ ๋˜๋Š” ํ•™์Šต ๋‹จ๊ณ„์—์„œ fail-fast๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
93
+ if label2id is None and id2label is None:
94
+ # Respect num_labels if explicitly provided, otherwise default to 0.
95
+ # num_labels๊ฐ€ ์ฃผ์–ด์กŒ๋‹ค๋ฉด ์กด์ค‘ํ•˜๊ณ , ์•„๋‹ˆ๋ฉด 0์œผ๋กœ ๋‘ก๋‹ˆ๋‹ค.
96
+ num_labels = int(kwargs.get("num_labels", 0))
97
+ label2id_norm: dict[str, int] = {}
98
+ id2label_norm: dict[int, str] = {}
99
+ else:
100
+ # If only one mapping is provided, derive the other.
101
+ # ํ•˜๋‚˜๋งŒ ์ฃผ์–ด์ง„ ๊ฒฝ์šฐ ๋‚˜๋จธ์ง€๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
102
+ if id2label is None:
103
+ id2label = {v: k for k, v in label2id.items()}
104
+ if label2id is None:
105
+ label2id = {v: k for k, v in id2label.items()}
106
+
107
+ # Ensure both mappings are consistent in size.
108
+ # ๋‘ ๋งคํ•‘์˜ ํฌ๊ธฐ๊ฐ€ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
109
+ if len(label2id) != len(id2label):
110
+ raise ValueError(
111
+ f"label2id/id2label size mismatch: "
112
+ f"{len(label2id)} vs {len(id2label)}"
113
+ )
114
+
115
+ num_labels = len(id2label)
116
+ label2id_norm = dict(label2id)
117
+ id2label_norm = dict(id2label)
118
+
119
+ # ============================================================
120
+ # 3) num_labels consistency enforcement
121
+ # 3) num_labels ์ผ๊ด€์„ฑ ๊ฐ•์ œ
122
+ # ============================================================
123
+ # If num_labels is provided via kwargs, it must match inferred labels.
124
+ # kwargs๋กœ num_labels๊ฐ€ ๋“ค์–ด์˜จ ๊ฒฝ์šฐ ์ถ”๋ก ๋œ ๊ฐ’๊ณผ ๋ฐ˜๋“œ์‹œ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
125
+ if "num_labels" in kwargs:
126
+ if (label2id is not None or id2label is not None) and int(kwargs["num_labels"]) != num_labels:
127
+ raise ValueError(
128
+ f"kwargs['num_labels']={kwargs['num_labels']} "
129
+ f"!= inferred num_labels={num_labels}"
130
+ )
131
+ else:
132
  kwargs["num_labels"] = num_labels
133
 
134
+ # ============================================================
135
+ # 4) Parent initialization
136
+ # 4) ๋ถ€๋ชจ ํด๋ž˜์Šค ์ดˆ๊ธฐํ™”
137
+ # ============================================================
138
+ # Initialize PretrainedConfig with normalized label mappings.
139
+ # ์ •๊ทœํ™”๋œ ๋ผ๋ฒจ ๋งคํ•‘์„ ์‚ฌ์šฉํ•˜์—ฌ PretrainedConfig๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
140
  super().__init__(
141
+ label2id=label2id_norm,
142
+ id2label=id2label_norm,
143
  **kwargs,
144
  )
 
 
 
 
 
145
 
146
+ # ============================================================
147
+ # 5) Explicit attribute assignment for save/load stability
148
+ # 5) ์ €์žฅ/๋กœ๋“œ ์•ˆ์ •์„ฑ์„ ์œ„ํ•œ ๋ช…์‹œ์  ์†์„ฑ ๊ณ ์ •
149
+ # ============================================================
150
+ # Explicitly reassign critical fields to avoid subtle serialization issues.
151
+ # ๋ฏธ๋ฌ˜ํ•œ ์ง๋ ฌํ™” ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ํ•ต์‹ฌ ํ•„๋“œ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ๋‹ค์‹œ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
152
  self.backbone_name_or_path = backbone_name_or_path
153
+ self.mlp_head_bottleneck = int(mlp_head_bottleneck)
154
+ self.mlp_head_dropout = float(mlp_head_dropout)
155
+
156
+ self.label2id = label2id_norm
157
+ self.id2label = id2label_norm
158
+ self.num_labels = int(kwargs["num_labels"])
159
 
160
  def to_dict(self):
161
+ # Call the parent implementation first.
162
+ # ๋จผ์ € ๋ถ€๋ชจ ๊ตฌํ˜„์„ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
163
  output = super().to_dict()
164
+
165
+ # Force num_labels to be present and consistent.
166
+ # num_labels๊ฐ€ ๋ฐ˜๋“œ์‹œ ์กด์žฌํ•˜๊ณ  ์ผ๊ด€๋˜๋„๋ก ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค.
167
+ output["num_labels"] = int(
168
+ getattr(self, "num_labels", output.get("num_labels", 0))
169
+ )
170
  return output
171
 
172
+
173
+ # Register this config so it can be resolved via AutoConfig.
174
+ # AutoConfig๋ฅผ ํ†ตํ•ด ์ด config๊ฐ€ ํ•ด์„๋  ์ˆ˜ ์žˆ๋„๋ก ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.
175
  BackboneMLPHeadConfig.register_for_auto_class("AutoConfig")
models/timm__densenet121.tv_in1k/ds_model.py CHANGED
@@ -1,10 +1,9 @@
1
  #!/usr/bin/env python3
2
  # -*- coding: utf-8 -*-
3
 
4
- from __future__ import annotations
5
 
6
- from dataclasses import dataclass
7
- from typing import Dict, Any, Optional, Tuple, Set, List
8
 
9
  import torch
10
  import torch.nn as nn
@@ -12,146 +11,88 @@ import torch.nn.functional as F
12
 
13
  # --- transformers core ---
14
  from transformers.modeling_utils import PreTrainedModel
15
- from transformers import AutoModel, AutoConfig, PretrainedConfig
16
  from transformers.modeling_outputs import ImageClassifierOutput
17
 
18
  # --- torchvision ---
19
  from torchvision import models as tv_models
20
 
21
-
22
- # ============================================================
23
- # (0) Config
24
- # - ๊ธฐ์กด ds_cfg.BackboneMLPHeadConfig์™€ ํ•„๋“œ๋ช…์„ ๋งž์ถ”์—ˆ์Šต๋‹ˆ๋‹ค.
25
- # - ์ด๋ฏธ ds_cfg.py๊ฐ€ ์žˆ๋‹ค๋ฉด: ์ด ๋ธ”๋ก์„ ์ง€์šฐ๊ณ  "from ds_cfg import BackboneMLPHeadConfig"๋กœ ๋ฐ”๊ฟ”๋„ ๋ฉ๋‹ˆ๋‹ค.
26
- # ============================================================
27
- class BackboneMLPHeadConfig(PretrainedConfig):
28
- model_type = "backbone_mlphead"
29
-
30
- def __init__(
31
- self,
32
- backbone_name_or_path: Optional[str] = None,
33
- num_labels: int = 2,
34
- id2label: Optional[Dict[int, str]] = None,
35
- label2id: Optional[Dict[str, int]] = None,
36
- mlp_head_bottleneck: int = 256,
37
- mlp_head_dropout: float = 0.2,
38
- **kwargs,
39
- ):
40
- super().__init__(**kwargs)
41
- self.backbone_name_or_path = backbone_name_or_path
42
- self.num_labels = num_labels
43
- self.id2label = id2label or {i: f"LABEL_{i}" for i in range(num_labels)}
44
- self.label2id = label2id or {v: k for k, v in self.id2label.items()}
45
- self.mlp_head_bottleneck = mlp_head_bottleneck
46
- self.mlp_head_dropout = mlp_head_dropout
47
-
48
-
49
- # ============================================================
50
- # (1) BACKBONE_META
51
- # - ์ด๋ฏธ ds_meta.py๊ฐ€ ์žˆ๋‹ค๋ฉด: ์ด ๋ธ”๋ก์„ ์ง€์šฐ๊ณ  "from ds_meta import BACKBONE_META"๋กœ ๋ฐ”๊ฟ”๋„ ๋ฉ๋‹ˆ๋‹ค.
52
- # ============================================================
53
- BACKBONE_META: Dict[str, Dict[str, Any]] = {
54
- # transformers
55
- "google/vit-base-patch16-224": {
56
- "type": "vit",
57
- "feat_rule": "cls",
58
- "feat_dim": 768,
59
- "has_bn": False,
60
- "unfreeze": "last_n",
61
- },
62
- "microsoft/swin-tiny-patch4-window7-224": {
63
- "type": "swin",
64
- "feat_rule": "pool_or_mean",
65
- "feat_dim": 768,
66
- "has_bn": False,
67
- "unfreeze": "last_n",
68
- },
69
- "microsoft/resnet-50": {
70
- "type": "resnet",
71
- "feat_rule": "pool_or_gap",
72
- "feat_dim": 2048,
73
- "has_bn": True,
74
- "unfreeze": "last_n",
75
- },
76
- "google/efficientnet-b0": {
77
- "type": "efficientnet",
78
- "feat_rule": "pool_or_gap",
79
- "feat_dim": 1280,
80
- "has_bn": True,
81
- "unfreeze": "last_n",
82
- },
83
-
84
- # densenet
85
- "timm/densenet121.tv_in1k": {
86
- "type": "timm_densenet",
87
- "feat_rule": "timm_gap",
88
- "feat_dim": 1024,
89
- "has_bn": True,
90
- "unfreeze": "last_n",
91
- },
92
- "torchvision/densenet121": {
93
- "type": "torchvision_densenet",
94
- "feat_rule": "torchvision_densenet_gap",
95
- "feat_dim": 1024,
96
- "has_bn": True,
97
- "unfreeze": "last_n",
98
- },
99
- }
100
-
101
-
102
- # ============================================================
103
- # (2) MLP Head
104
- # - ์ด๋ฏธ mlp_head.py๊ฐ€ ์žˆ๋‹ค๋ฉด: ์ด ๋ธ”๋ก์„ ์ง€์šฐ๊ณ  "from mlp_head import MLPHead"๋กœ ๋ฐ”๊ฟ”๋„ ๋ฉ๋‹ˆ๋‹ค.
105
- # ============================================================
106
- class MLPHead(nn.Module):
107
- def __init__(self, in_dim: int, num_labels: int, bottleneck: int = 256, p: float = 0.2):
108
- super().__init__()
109
- self.fc2 = nn.Linear(in_dim, bottleneck)
110
- self.act2 = nn.GELU()
111
- self.drop2 = nn.Dropout(p)
112
- self.fc3 = nn.Linear(bottleneck, num_labels)
113
-
114
- def forward(self, x: torch.Tensor) -> torch.Tensor:
115
- x = self.drop2(self.act2(self.fc2(x)))
116
- return self.fc3(x)
117
-
118
 
119
  # ============================================================
120
  # (3) Model: backbone + MLP head
121
- # ํ•ต์‹ฌ ์„ค๊ณ„:
122
- # - __init__: backbone ๊ตฌ์กฐ(skeleton)๋งŒ ์ƒ์„ฑ. pretrained๋ฅผ ์ ˆ๋Œ€ ๋กœ๋“œํ•˜์ง€ ์•Š์Œ.
123
- # - pretrained ์ฃผ์ž…: model.load_backbone_pretrained_()๋ฅผ "fresh-start์—์„œ๋งŒ" ์ง์ ‘ ํ˜ธ์ถœ.
124
- # - from_pretrained override ์—†์Œ: HF ๊ธฐ๋ณธ ๋กœ๋”๊ฐ€ checkpoint๋ฅผ ๊ทธ๋Œ€๋กœ ๋ณต์›.
125
  # ============================================================
 
 
 
 
 
 
 
 
126
  class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
 
 
127
  config_class = BackboneMLPHeadConfig
128
 
129
  def __init__(self, config: BackboneMLPHeadConfig):
 
 
130
  super().__init__(config)
131
 
 
 
 
 
 
132
  if config.backbone_name_or_path is None:
133
- raise ValueError("config.backbone_name_or_path is None. Provide a valid backbone id.")
 
 
 
134
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  self._meta = BACKBONE_META[config.backbone_name_or_path]
136
 
137
- # (์ค‘์š”) backbone์€ ํ•ญ์ƒ skeleton๋งŒ ์ƒ์„ฑ
 
138
  self.backbone = self._build_backbone_skeleton(config.backbone_name_or_path)
139
 
140
- # head
 
141
  self.classifier = MLPHead(
142
  in_dim=int(self._meta["feat_dim"]),
143
- num_labels=config.num_labels,
144
- bottleneck=config.mlp_head_bottleneck,
145
- p=config.mlp_head_dropout,
146
  )
147
 
148
- # HF init hook (init_weights๋Š” head-only๋กœ override๋จ)
 
149
  self.post_init()
150
 
151
  def init_weights(self):
152
  """
153
- HF์˜ ๊ธฐ๋ณธ init_weights()๋Š” ๋ชจ๋ธ ์ „์ฒด์— apply(...)๋ฅผ ๊ฑธ ์ˆ˜ ์žˆ์œผ๋‹ˆ,
154
- head๋งŒ ์ดˆ๊ธฐํ™”ํ•œ๋‹ค.
 
 
 
155
  """
156
  if getattr(self, "classifier", None) is not None:
157
  self.classifier.apply(self._init_weights)
@@ -159,8 +100,11 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
159
 
160
  # ----------------------------
161
  # backbone skeleton builders
 
162
  # ----------------------------
163
  def _build_backbone_skeleton(self, backbone_id: str) -> nn.Module:
 
 
164
  meta = BACKBONE_META[backbone_id]
165
  t = meta["type"]
166
 
@@ -170,18 +114,24 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
170
  if t == "torchvision_densenet":
171
  return self._build_torchvision_densenet_skeleton(backbone_id)
172
 
173
- # transformers: config๋กœ ๊ตฌ์กฐ๋งŒ ์ƒ์„ฑ
 
174
  bb_cfg = AutoConfig.from_pretrained(backbone_id)
175
  return AutoModel.from_config(bb_cfg)
176
 
177
  @staticmethod
178
  def _build_timm_densenet_skeleton(hf_repo_id: str) -> nn.Module:
 
 
179
  try:
180
  import timm
181
  except Exception as e:
182
- raise ImportError("DenseNet(timm) backbone requires `timm`. Install: pip install timm") from e
 
 
183
 
184
- # ๊ตฌ์กฐ๋งŒ (pretrained=False)
 
185
  return timm.create_model(
186
  f"hf_hub:{hf_repo_id}",
187
  pretrained=False,
@@ -190,33 +140,33 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
190
 
191
  @staticmethod
192
  def _build_torchvision_densenet_skeleton(model_id: str) -> nn.Module:
193
- mapping = {
194
- "torchvision/densenet121": "densenet121",
195
- "torchvision/densenet169": "densenet169",
196
- "torchvision/densenet201": "densenet201",
197
- "torchvision/densenet161": "densenet161",
198
- }
199
- if model_id not in mapping:
200
- raise ValueError(f"Unsupported torchvision DenseNet id: {model_id}")
201
-
202
- arch = mapping[model_id]
203
- fn = getattr(tv_models, arch)
204
-
205
- # ๊ตฌ์กฐ๋งŒ (weights=None)
206
- m = fn(weights=None).eval()
207
  return m
208
 
209
  # ------------------------------------------------------------
210
- # pretrained ๋กœ๋”ฉ์€ "๋ช…์‹œ์  ํ˜ธ์ถœ"๋กœ๋งŒ ์ˆ˜ํ–‰ (fresh-start ์ „์šฉ)
 
211
  # ------------------------------------------------------------
212
  @torch.no_grad()
213
- def load_backbone_pretrained_(self, *, low_cpu_mem_usage: bool = False, device_map=None):
 
 
 
 
 
214
  """
215
- fresh-start ์ „์šฉ:
216
- model = BackboneWithMLPHeadForImageClassification(cfg)
217
- model.load_backbone_pretrained_()
218
 
219
- checkpoint reload(from_pretrained)์—์„œ๋Š” ํ˜ธ์ถœํ•˜์ง€ ๋งˆ์‹ญ์‹œ์˜ค.
 
220
  """
221
  bb = self.config.backbone_name_or_path
222
  meta = BACKBONE_META[bb]
@@ -230,48 +180,58 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
230
  self._load_torchvision_pretrained_into_skeleton_(bb)
231
  return
232
 
233
- # transformers pretrained -> skeleton์— ์ฃผ์ž…
 
234
  ref = AutoModel.from_pretrained(
235
  bb,
236
  low_cpu_mem_usage=low_cpu_mem_usage,
237
  device_map=device_map,
238
  )
 
 
 
239
  self.backbone.load_state_dict(ref.state_dict(), strict=False)
240
  del ref
241
 
242
  @torch.no_grad()
243
  def _load_timm_pretrained_into_skeleton_(self, hf_repo_id: str):
 
 
244
  import timm
 
 
 
245
  ref = timm.create_model(
246
  f"hf_hub:{hf_repo_id}",
247
  pretrained=True,
248
  num_classes=0,
249
  ).eval()
 
250
  self.backbone.load_state_dict(ref.state_dict(), strict=True)
251
  del ref
252
 
253
  @torch.no_grad()
254
  def _load_torchvision_pretrained_into_skeleton_(self, model_id: str):
255
- mapping = {
256
- "torchvision/densenet121": ("densenet121", tv_models.DenseNet121_Weights.DEFAULT),
257
- "torchvision/densenet169": ("densenet169", tv_models.DenseNet169_Weights.DEFAULT),
258
- "torchvision/densenet201": ("densenet201", tv_models.DenseNet201_Weights.DEFAULT),
259
- "torchvision/densenet161": ("densenet161", tv_models.DenseNet161_Weights.DEFAULT),
260
- }
261
- if model_id not in mapping:
262
- raise ValueError(f"Unsupported torchvision DenseNet id: {model_id}")
263
-
264
- arch, weights = mapping[model_id]
265
- fn = getattr(tv_models, arch)
266
- ref = fn(weights=weights).eval()
267
  self.backbone.load_state_dict(ref.state_dict(), strict=True)
268
  del ref
269
 
270
  # ----------------------------
271
  # feature extraction
 
272
  # ----------------------------
273
  @staticmethod
274
  def _pool_or_gap(outputs) -> torch.Tensor:
 
 
275
  if hasattr(outputs, "pooler_output") and outputs.pooler_output is not None:
276
  x = outputs.pooler_output
277
  if x.dim() == 2:
@@ -280,29 +240,42 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
280
  return x.flatten(1)
281
  raise RuntimeError(f"Unexpected pooler_output shape: {tuple(x.shape)}")
282
 
 
 
283
  x = outputs.last_hidden_state
284
  if x.dim() == 4:
285
  return x.mean(dim=(2, 3))
 
286
  raise RuntimeError(
287
  "Expected pooler_output or (B,C,H,W) last_hidden_state for CNN backbones. "
288
  f"Got last_hidden_state shape={tuple(x.shape)}"
289
  )
290
 
291
  def _extract_features(self, outputs, pixel_values: Optional[torch.Tensor] = None) -> torch.Tensor:
 
 
292
  rule = self._meta["feat_rule"]
293
 
294
  if rule == "cls":
 
 
295
  return outputs.last_hidden_state[:, 0, :]
296
 
297
  if rule == "pool_or_mean":
 
 
298
  if hasattr(outputs, "pooler_output") and outputs.pooler_output is not None:
299
  return outputs.pooler_output
300
  return outputs.last_hidden_state.mean(dim=1)
301
 
302
  if rule == "pool_or_gap":
 
 
303
  return self._pool_or_gap(outputs)
304
 
305
  if rule == "timm_gap":
 
 
306
  if not isinstance(outputs, torch.Tensor):
307
  raise TypeError(f"timm_gap expects Tensor features, got {type(outputs)}")
308
  if outputs.dim() != 4:
@@ -310,6 +283,8 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
310
  return outputs.mean(dim=(2, 3))
311
 
312
  if rule == "torchvision_densenet_gap":
 
 
313
  if not isinstance(outputs, torch.Tensor):
314
  raise TypeError(f"torchvision_densenet_gap expects Tensor, got {type(outputs)}")
315
  if outputs.dim() != 4:
@@ -327,9 +302,13 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
327
  return_dict=True,
328
  **kwargs,
329
  ):
 
 
330
  t = self._meta["type"]
331
 
332
  if t == "timm_densenet":
 
 
333
  if pixel_values is None:
334
  raise ValueError("timm DenseNet backbone requires pixel_values.")
335
  if pixel_values.dim() != 4:
@@ -341,6 +320,8 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
341
  attentions = None
342
 
343
  elif t == "torchvision_densenet":
 
 
344
  if pixel_values is None:
345
  raise ValueError("torchvision DenseNet backbone requires pixel_values.")
346
  if pixel_values.dim() != 4:
@@ -353,6 +334,8 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
353
  attentions = None
354
 
355
  else:
 
 
356
  outputs = self.backbone(
357
  pixel_values=pixel_values,
358
  output_attentions=output_attentions,
@@ -364,10 +347,14 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
364
  hidden_states = getattr(outputs, "hidden_states", None)
365
  attentions = getattr(outputs, "attentions", None)
366
 
 
 
367
  logits = self.classifier(feats)
368
 
369
  loss = None
370
  if labels is not None:
 
 
371
  loss = F.cross_entropy(logits, labels)
372
 
373
  if not return_dict:
@@ -383,20 +370,27 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
383
 
384
 
385
  # ============================================================
386
- # Freeze / Unfreeze utilities (๋‹น์‹  ์ฝ”๋“œ ์œ ์ง€)
 
387
  # ============================================================
388
  def _set_requires_grad(module: nn.Module, flag: bool):
 
 
389
  for p in module.parameters():
390
  p.requires_grad = flag
391
 
392
 
393
  def set_bn_eval(module: nn.Module):
 
 
394
  for m in module.modules():
395
  if isinstance(m, (nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d, nn.SyncBatchNorm)):
396
  m.eval()
397
 
398
 
399
  def freeze_backbone(model: BackboneWithMLPHeadForImageClassification, freeze_bn: bool = True):
 
 
400
  _set_requires_grad(model.backbone, False)
401
  _set_requires_grad(model.classifier, True)
402
 
@@ -406,6 +400,8 @@ def freeze_backbone(model: BackboneWithMLPHeadForImageClassification, freeze_bn:
406
 
407
 
408
  def finetune_train_mode(model: BackboneWithMLPHeadForImageClassification, keep_bn_eval: bool = True):
 
 
409
  model.train()
410
  meta = BACKBONE_META[model.config.backbone_name_or_path]
411
  if keep_bn_eval and meta.get("has_bn", False):
@@ -413,6 +409,8 @@ def finetune_train_mode(model: BackboneWithMLPHeadForImageClassification, keep_b
413
 
414
 
415
  def trainable_summary(model: nn.Module):
 
 
416
  total = sum(p.numel() for p in model.parameters())
417
  trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
418
  ratio = trainable / total if total > 0 else 0.0
@@ -425,6 +423,8 @@ def unfreeze_last_stage(
425
  last_n: int = 2,
426
  keep_bn_eval: bool = True,
427
  ):
 
 
428
  freeze_backbone(model, freeze_bn=keep_bn_eval)
429
 
430
  n = int(last_n)
@@ -438,12 +438,16 @@ def unfreeze_last_stage(
438
  bb_type = meta["type"]
439
 
440
  if bb_type == "vit":
 
 
441
  blocks = list(model.backbone.encoder.layer)
442
  for blk in blocks[-n:]:
443
  _set_requires_grad(blk, True)
444
  return
445
 
446
  if bb_type == "swin":
 
 
447
  stages = list(model.backbone.encoder.layers)
448
  blocks: List[nn.Module] = []
449
  for st in stages:
@@ -453,6 +457,8 @@ def unfreeze_last_stage(
453
  return
454
 
455
  if bb_type == "resnet":
 
 
456
  bb = model.backbone
457
  for name in ("layer1", "layer2", "layer3", "layer4"):
458
  if not hasattr(bb, name):
@@ -472,6 +478,8 @@ def unfreeze_last_stage(
472
  return
473
 
474
  if bb_type == "efficientnet":
 
 
475
  bb = model.backbone
476
  if not hasattr(bb, "features"):
477
  raise RuntimeError("Unexpected EfficientNet structure: missing features")
@@ -488,6 +496,8 @@ def unfreeze_last_stage(
488
  return
489
 
490
  if bb_type in ("timm_densenet", "torchvision_densenet"):
 
 
491
  bb = model.backbone
492
  if not hasattr(bb, "features"):
493
  raise RuntimeError("Unexpected DenseNet: missing features")
@@ -505,6 +515,8 @@ def unfreeze_last_stage(
505
  raise RuntimeError(f"Unexpected DenseNet features: missing {name}")
506
 
507
  def _denselayers(db: nn.Module) -> List[nn.Module]:
 
 
508
  return list(db.children())
509
 
510
  blocks: List[nn.Module] = []
@@ -526,20 +538,8 @@ def unfreeze_last_stage(
526
 
527
  # -------------------------
528
  # register
 
 
 
 
529
  BackboneWithMLPHeadForImageClassification.register_for_auto_class("AutoModelForImageClassification")
530
-
531
-
532
- # ==========================================================
533
-
534
- # # fresh-start
535
- # cfg = BackboneMLPHeadConfig(backbone_name_or_path="timm/densenet121.tv_in1k", num_labels=3)
536
- # model = BackboneWithMLPHeadForImageClassification(cfg)
537
-
538
- # # pretrained๋ฅผ ์“ฐ๋ ค๋ฉด ๋ช…์‹œ์ ์œผ๋กœ ํ˜ธ์ถœ
539
- # model.load_backbone_pretrained_()
540
-
541
- # # ์ดํ›„ ํ•™์Šต
542
-
543
- # # checkpoint์—์„œ ์‹œ์ž‘
544
- # model = BackboneWithMLPHeadForImageClassification.from_pretrained("checkpoints/xxx")
545
- # # ์—ฌ๊ธฐ์„œ๋Š” load_backbone_pretrained_() ํ˜ธ์ถœํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
 
1
  #!/usr/bin/env python3
2
  # -*- coding: utf-8 -*-
3
 
4
+ # src/ds_model.py
5
 
6
+ from typing import Optional, List
 
7
 
8
  import torch
9
  import torch.nn as nn
 
11
 
12
  # --- transformers core ---
13
  from transformers.modeling_utils import PreTrainedModel
14
+ from transformers import AutoModel, AutoConfig
15
  from transformers.modeling_outputs import ImageClassifierOutput
16
 
17
  # --- torchvision ---
18
  from torchvision import models as tv_models
19
 
20
+ from ds_cfg import BackboneMLPHeadConfig
21
+ from ds_meta import BACKBONE_META
22
+ from mlp_head import MLPHead
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  # ============================================================
25
  # (3) Model: backbone + MLP head
26
+ # (3) ๋ชจ๋ธ: backbone + MLP head
 
 
 
27
  # ============================================================
28
+ # Design principle: __init__ builds only a skeleton and MUST NOT load pretrained weights.
29
+ # ์„ค๊ณ„ ์›์น™: __init__์—์„œ๋Š” skeleton๋งŒ ๋งŒ๋“ค๊ณ  pretrained weight ๋กœ๋“œ๋Š” ์ ˆ๋Œ€ ํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
30
+ #
31
+ # Pretrained injection is allowed ONLY via an explicit call in fresh-start flows.
32
+ # pretrained ์ฃผ์ž…์€ fresh-start ํ๋ฆ„์—์„œ์˜ ๋ช…์‹œ์  ํ˜ธ์ถœ๋กœ๋งŒ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.
33
+ #
34
+ # HF from_pretrained should restore checkpoints as-is without side effects.
35
+ # HF from_pretrained๋Š” ๋ถ€์ž‘์šฉ ์—†์ด ์ฒดํฌํฌ์ธํŠธ๋ฅผ ๊ทธ๋Œ€๋กœ ๋ณต์›ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
36
  class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
37
+ # This links the model to its custom config for AutoClass usage.
38
+ # AutoClass ์‚ฌ์šฉ์„ ์œ„ํ•ด ๋ชจ๋ธ๊ณผ ์ปค์Šคํ…€ config๋ฅผ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค.
39
  config_class = BackboneMLPHeadConfig
40
 
41
  def __init__(self, config: BackboneMLPHeadConfig):
42
+ # PreTrainedModel expects a config object and stores it internally.
43
+ # PreTrainedModel์€ config ๊ฐ์ฒด๋ฅผ ๋ฐ›์•„ ๋‚ด๋ถ€์— ์ €์žฅํ•˜๋Š” ์ „์ œ๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค.
44
  super().__init__(config)
45
 
46
+ # Fail-fast: the model is not meant to be instantiated without a valid backbone id.
47
+ # fail-fast: ์œ ํšจํ•œ backbone id ์—†์ด ๋ชจ๋ธ์„ ๋งŒ๋“œ๋Š” ์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค๋Š” ํ—ˆ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
48
+ #
49
+ # Note: Transformers may create configs with no args, but models are conventionally created with configs.
50
+ # ์ฐธ๊ณ : Transformers๋Š” config ๋ฌด์ธ์ž ์ƒ์„ฑ์ด ์žˆ์„ ์ˆ˜ ์žˆ์œผ๋‚˜, ๋ชจ๋ธ์€ ๊ด€๋ก€์ ์œผ๋กœ config๋ฅผ ๋ฐ›์•„ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.
51
  if config.backbone_name_or_path is None:
52
+ raise ValueError(
53
+ "config.backbone_name_or_path is None. "
54
+ "Provide a valid backbone id (whitelist key in BACKBONE_META)."
55
+ )
56
 
57
+ # Fail-fast: training/inference requires a positive number of labels.
58
+ # fail-fast: ํ•™์Šต/์ถ”๋ก ์€ num_labels๊ฐ€ ์–‘์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.
59
+ #
60
+ # Config may exist in a minimal form for internal serialization paths, but the model should not.
61
+ # config๋Š” ๋‚ด๋ถ€ ์ง๋ ฌํ™” ๊ฒฝ๋กœ์—์„œ ์ตœ์†Œ ํ˜•ํƒœ๋กœ ์กด์žฌํ•  ์ˆ˜ ์žˆ์œผ๋‚˜ ๋ชจ๋ธ์€ ๊ทธ๋Ÿฌ๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
62
+ if int(getattr(config, "num_labels", 0)) <= 0:
63
+ raise ValueError(
64
+ f"config.num_labels must be > 0, got {getattr(config, 'num_labels', None)}. "
65
+ "Set num_labels (or id2label/label2id) when creating the config."
66
+ )
67
+
68
+ # Meta is a single source of truth for extraction and fine-tuning rules.
69
+ # meta๋Š” feature ์ถ”์ถœ ๋ฐ ๋ฏธ์„ธ์กฐ์ • ๊ทœ์น™์˜ ๋‹จ์ผ ๊ธฐ์ค€์ž…๋‹ˆ๋‹ค.
70
  self._meta = BACKBONE_META[config.backbone_name_or_path]
71
 
72
+ # Backbone skeleton is always created without pretrained weights.
73
+ # backbone skeleton์€ ํ•ญ์ƒ pretrained weight ์—†์ด ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
74
  self.backbone = self._build_backbone_skeleton(config.backbone_name_or_path)
75
 
76
+ # Head shape is driven by meta feat_dim and config.num_labels.
77
+ # head shape์€ meta์˜ feat_dim๊ณผ config.num_labels๋กœ ๊ฒฐ์ •๋ฉ๋‹ˆ๋‹ค.
78
  self.classifier = MLPHead(
79
  in_dim=int(self._meta["feat_dim"]),
80
+ num_labels=int(config.num_labels),
81
+ bottleneck=int(config.mlp_head_bottleneck),
82
+ p=float(config.mlp_head_dropout),
83
  )
84
 
85
+ # HF initialization hook, but we override init_weights to initialize head-only.
86
+ # HF ์ดˆ๊ธฐํ™” ํ›…์ด์ง€๋งŒ init_weights๋ฅผ overrideํ•˜์—ฌ head๋งŒ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
87
  self.post_init()
88
 
89
  def init_weights(self):
90
  """
91
+ Initialize only the head to avoid touching the backbone skeleton.
92
+ backbone skeleton์„ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๊ธฐ ์œ„ํ•ด head๋งŒ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
93
+
94
+ HF's default init may traverse the entire module tree, which is undesirable here.
95
+ HF ๊ธฐ๋ณธ init์€ ์ „์ฒด ๋ชจ๋“ˆ ํŠธ๋ฆฌ๋ฅผ ์ˆœํšŒํ•  ์ˆ˜ ์žˆ์–ด ์—ฌ๊ธฐ์„œ๋Š” ๋ถ€์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.
96
  """
97
  if getattr(self, "classifier", None) is not None:
98
  self.classifier.apply(self._init_weights)
 
100
 
101
  # ----------------------------
102
  # backbone skeleton builders
103
+ # backbone skeleton ์ƒ์„ฑ๊ธฐ
104
  # ----------------------------
105
  def _build_backbone_skeleton(self, backbone_id: str) -> nn.Module:
106
+ # Meta decides which loader path to use.
107
+ # meta๊ฐ€ ์–ด๋–ค ๋กœ๋” ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ• ์ง€ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.
108
  meta = BACKBONE_META[backbone_id]
109
  t = meta["type"]
110
 
 
114
  if t == "torchvision_densenet":
115
  return self._build_torchvision_densenet_skeleton(backbone_id)
116
 
117
+ # For transformers backbones: build a random-weight skeleton from config only.
118
+ # transformers ๋ฐฑ๋ณธ: config๋กœ๋ถ€ํ„ฐ ๋žœ๋ค ์ดˆ๊ธฐํ™” skeleton๋งŒ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
119
  bb_cfg = AutoConfig.from_pretrained(backbone_id)
120
  return AutoModel.from_config(bb_cfg)
121
 
122
  @staticmethod
123
  def _build_timm_densenet_skeleton(hf_repo_id: str) -> nn.Module:
124
+ # timm is an optional dependency and should be imported lazily.
125
+ # timm์€ ์˜ต์…˜ ์˜์กด์„ฑ์ด๋ฏ€๋กœ ์ง€์—ฐ import ํ•ฉ๋‹ˆ๋‹ค.
126
  try:
127
  import timm
128
  except Exception as e:
129
+ raise ImportError(
130
+ "DenseNet(timm) backbone requires `timm`. Install: pip install timm"
131
+ ) from e
132
 
133
+ # Build structure only (pretrained=False) and remove classifier head (num_classes=0).
134
+ # ๊ตฌ์กฐ๋งŒ ์ƒ์„ฑ(pretrained=False)ํ•˜๊ณ  ๋ถ„๋ฅ˜๊ธฐ head๋Š” ์ œ๊ฑฐ(num_classes=0)ํ•ฉ๋‹ˆ๋‹ค.
135
  return timm.create_model(
136
  f"hf_hub:{hf_repo_id}",
137
  pretrained=False,
 
140
 
141
  @staticmethod
142
  def _build_torchvision_densenet_skeleton(model_id: str) -> nn.Module:
143
+ # This project intentionally supports only torchvision/densenet121 in the 224 whitelist.
144
+ # ์ด ํ”„๋กœ์ ํŠธ๋Š” 224 ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ์—์„œ torchvision/densenet121๋งŒ ์˜๋„์ ์œผ๋กœ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.
145
+ if model_id != "torchvision/densenet121":
146
+ raise ValueError(f"Unsupported torchvision DenseNet id (224 whitelist only): {model_id}")
147
+
148
+ # Build structure only (weights=None) to avoid implicit pretrained loading.
149
+ # implicit pretrained ๋กœ๋“œ๋ฅผ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด ๊ตฌ์กฐ๋งŒ ์ƒ์„ฑ(weights=None)ํ•ฉ๋‹ˆ๋‹ค.
150
+ m = tv_models.densenet121(weights=None)
 
 
 
 
 
 
151
  return m
152
 
153
  # ------------------------------------------------------------
154
+ # Pretrained loading is explicit and fresh-start only
155
+ # pretrained ๋กœ๋”ฉ์€ ๋ช…์‹œ์  ํ˜ธ์ถœ์ด๋ฉฐ fresh-start ์ „์šฉ
156
  # ------------------------------------------------------------
157
  @torch.no_grad()
158
+ def load_backbone_pretrained_(
159
+ self,
160
+ *,
161
+ low_cpu_mem_usage: bool = False,
162
+ device_map=None,
163
+ ):
164
  """
165
+ Fresh-start only: inject pretrained backbone weights into the skeleton.
166
+ fresh-start ์ „์šฉ: skeleton backbone์— pretrained ๊ฐ€์ค‘์น˜๋ฅผ ์ฃผ์ž…ํ•ฉ๋‹ˆ๋‹ค.
 
167
 
168
+ Do NOT call this after from_pretrained() because it would overwrite checkpoint weights.
169
+ from_pretrained() ์ดํ›„ ํ˜ธ์ถœํ•˜๋ฉด ์ฒดํฌํฌ์ธํŠธ ๊ฐ€์ค‘์น˜๋ฅผ ๋ฎ์–ด์“ฐ๋ฏ€๋กœ ์ ˆ๋Œ€ ํ˜ธ์ถœํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
170
  """
171
  bb = self.config.backbone_name_or_path
172
  meta = BACKBONE_META[bb]
 
180
  self._load_torchvision_pretrained_into_skeleton_(bb)
181
  return
182
 
183
+ # For transformers backbones, load a reference pretrained model and copy weights into our skeleton.
184
+ # transformers ๋ฐฑ๋ณธ์€ reference pretrained ๋ชจ๋ธ์„ ๋กœ๋“œํ•œ ๋’ค skeleton์— ๊ฐ€์ค‘์น˜๋ฅผ ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค.
185
  ref = AutoModel.from_pretrained(
186
  bb,
187
  low_cpu_mem_usage=low_cpu_mem_usage,
188
  device_map=device_map,
189
  )
190
+
191
+ # strict=False is used to tolerate harmless key differences across minor versions.
192
+ # strict=False๋Š” ๋งˆ์ด๋„ˆ ๋ฒ„์ „ ์ฐจ์ด๋กœ ์ธํ•œ ๋ฌดํ•ดํ•œ ํ‚ค ์ฐจ์ด๋ฅผ ํ—ˆ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
193
  self.backbone.load_state_dict(ref.state_dict(), strict=False)
194
  del ref
195
 
196
  @torch.no_grad()
197
  def _load_timm_pretrained_into_skeleton_(self, hf_repo_id: str):
198
+ # timm must be present for timm backbones.
199
+ # timm ๋ฐฑ๋ณธ์—๋Š” timm ์„ค์น˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
200
  import timm
201
+
202
+ # Create a pretrained reference model and copy its weights strictly.
203
+ # pretrained reference ๋ชจ๋ธ์„ ๋งŒ๋“ค๊ณ  ๊ฐ€์ค‘์น˜๋ฅผ strictํ•˜๊ฒŒ ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค.
204
  ref = timm.create_model(
205
  f"hf_hub:{hf_repo_id}",
206
  pretrained=True,
207
  num_classes=0,
208
  ).eval()
209
+
210
  self.backbone.load_state_dict(ref.state_dict(), strict=True)
211
  del ref
212
 
213
  @torch.no_grad()
214
  def _load_torchvision_pretrained_into_skeleton_(self, model_id: str):
215
+ # This project intentionally supports only torchvision/densenet121 in the 224 whitelist.
216
+ # ์ด ํ”„๋กœ์ ํŠธ๋Š” 224 ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ์—์„œ torchvision/densenet121๋งŒ ์˜๋„์ ์œผ๋กœ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.
217
+ if model_id != "torchvision/densenet121":
218
+ raise ValueError(f"Unsupported torchvision DenseNet id (224 whitelist only): {model_id}")
219
+
220
+ # Use torchvision's default pretrained weights for densenet121.
221
+ # torchvision์˜ densenet121 ๊ธฐ๋ณธ pretrained weights๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
222
+ ref = tv_models.densenet121(weights=tv_models.DenseNet121_Weights.DEFAULT).eval()
223
+
 
 
 
224
  self.backbone.load_state_dict(ref.state_dict(), strict=True)
225
  del ref
226
 
227
  # ----------------------------
228
  # feature extraction
229
+ # feature ์ถ”์ถœ
230
  # ----------------------------
231
  @staticmethod
232
  def _pool_or_gap(outputs) -> torch.Tensor:
233
+ # Some transformers vision CNNs provide pooler_output explicitly.
234
+ # ์ผ๋ถ€ transformers vision CNN์€ pooler_output์„ ๋ช…์‹œ์ ์œผ๋กœ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
235
  if hasattr(outputs, "pooler_output") and outputs.pooler_output is not None:
236
  x = outputs.pooler_output
237
  if x.dim() == 2:
 
240
  return x.flatten(1)
241
  raise RuntimeError(f"Unexpected pooler_output shape: {tuple(x.shape)}")
242
 
243
+ # Otherwise we expect a CNN-style last_hidden_state=(B,C,H,W) and apply GAP.
244
+ # ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด CNN ์Šคํƒ€์ผ last_hidden_state=(B,C,H,W)๋ฅผ ๊ธฐ๋Œ€ํ•˜๊ณ  GAP์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.
245
  x = outputs.last_hidden_state
246
  if x.dim() == 4:
247
  return x.mean(dim=(2, 3))
248
+
249
  raise RuntimeError(
250
  "Expected pooler_output or (B,C,H,W) last_hidden_state for CNN backbones. "
251
  f"Got last_hidden_state shape={tuple(x.shape)}"
252
  )
253
 
254
  def _extract_features(self, outputs, pixel_values: Optional[torch.Tensor] = None) -> torch.Tensor:
255
+ # Feature rule is defined by BACKBONE_META and must remain stable across saves/loads.
256
+ # feature ๊ทœ์น™์€ BACKBONE_META๋กœ ์ •์˜๋˜๋ฉฐ ์ €์žฅ/๋กœ๋“œ ๊ฐ„ ์•ˆ์ •์ ์œผ๋กœ ์œ ์ง€๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
257
  rule = self._meta["feat_rule"]
258
 
259
  if rule == "cls":
260
+ # ViT-style: use CLS token embedding from last_hidden_state.
261
+ # ViT ์Šคํƒ€์ผ: last_hidden_state์—์„œ CLS ํ† ํฐ ์ž„๋ฒ ๋”ฉ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
262
  return outputs.last_hidden_state[:, 0, :]
263
 
264
  if rule == "pool_or_mean":
265
+ # Swin-style: prefer pooler_output if present, else mean-pool over tokens.
266
+ # Swin ์Šคํƒ€์ผ: pooler_output์ด ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉํ•˜๊ณ , ์—†์œผ๋ฉด ํ† ํฐ ํ‰๊ท  ํ’€๋ง์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
267
  if hasattr(outputs, "pooler_output") and outputs.pooler_output is not None:
268
  return outputs.pooler_output
269
  return outputs.last_hidden_state.mean(dim=1)
270
 
271
  if rule == "pool_or_gap":
272
+ # CNN-style: use pooler_output if present, else GAP over spatial dims.
273
+ # CNN ์Šคํƒ€์ผ: pooler_output์ด ์žˆ์œผ๋ฉด ์‚ฌ์šฉํ•˜๊ณ , ์—†์œผ๋ฉด ๊ณต๊ฐ„ ์ฐจ์› GAP์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
274
  return self._pool_or_gap(outputs)
275
 
276
  if rule == "timm_gap":
277
+ # timm forward_features returns a feature map (B,C,H,W) which we GAP to (B,C).
278
+ # timm forward_features๋Š” (B,C,H,W) feature map์„ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ ์ด๋ฅผ GAP์œผ๋กœ (B,C)๋กœ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
279
  if not isinstance(outputs, torch.Tensor):
280
  raise TypeError(f"timm_gap expects Tensor features, got {type(outputs)}")
281
  if outputs.dim() != 4:
 
283
  return outputs.mean(dim=(2, 3))
284
 
285
  if rule == "torchvision_densenet_gap":
286
+ # torchvision DenseNet features are feature maps (B,C,H,W) and require GAP.
287
+ # torchvision DenseNet features๋Š” (B,C,H,W) feature map์ด๋ฉฐ GAP์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
288
  if not isinstance(outputs, torch.Tensor):
289
  raise TypeError(f"torchvision_densenet_gap expects Tensor, got {type(outputs)}")
290
  if outputs.dim() != 4:
 
302
  return_dict=True,
303
  **kwargs,
304
  ):
305
+ # Type decides the backbone forward path and output format.
306
+ # type์ด backbone forward ๊ฒฝ๋กœ ๋ฐ ์ถœ๋ ฅ ํฌ๋งท์„ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.
307
  t = self._meta["type"]
308
 
309
  if t == "timm_densenet":
310
+ # timm DenseNet consumes pixel_values as a 4D tensor (B,C,H,W).
311
+ # timm DenseNet์€ pixel_values๋ฅผ 4D ํ…์„œ (B,C,H,W)๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค.
312
  if pixel_values is None:
313
  raise ValueError("timm DenseNet backbone requires pixel_values.")
314
  if pixel_values.dim() != 4:
 
320
  attentions = None
321
 
322
  elif t == "torchvision_densenet":
323
+ # torchvision DenseNet consumes pixel_values as a 4D tensor (B,C,H,W).
324
+ # torchvision DenseNet์€ pixel_values๋ฅผ 4D ํ…์„œ (B,C,H,W)๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค.
325
  if pixel_values is None:
326
  raise ValueError("torchvision DenseNet backbone requires pixel_values.")
327
  if pixel_values.dim() != 4:
 
334
  attentions = None
335
 
336
  else:
337
+ # Transformers vision models are called with pixel_values and return ModelOutput.
338
+ # transformers vision ๋ชจ๋ธ์€ pixel_values๋กœ ํ˜ธ์ถœ๋˜๋ฉฐ ModelOutput์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
339
  outputs = self.backbone(
340
  pixel_values=pixel_values,
341
  output_attentions=output_attentions,
 
347
  hidden_states = getattr(outputs, "hidden_states", None)
348
  attentions = getattr(outputs, "attentions", None)
349
 
350
+ # Classifier consumes (B, feat_dim) and returns logits (B, num_labels).
351
+ # classifier๋Š” (B, feat_dim)์„ ๋ฐ›์•„ logits (B, num_labels)๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
352
  logits = self.classifier(feats)
353
 
354
  loss = None
355
  if labels is not None:
356
+ # Cross entropy expects labels as class indices in [0, num_labels).
357
+ # cross entropy๋Š” labels๊ฐ€ [0, num_labels) ๋ฒ”์œ„์˜ class index์ด๊ธธ ๊ธฐ๋Œ€ํ•ฉ๋‹ˆ๋‹ค.
358
  loss = F.cross_entropy(logits, labels)
359
 
360
  if not return_dict:
 
370
 
371
 
372
  # ============================================================
373
+ # Freeze / Unfreeze utilities
374
+ # Freeze / Unfreeze ์œ ํ‹ธ๋ฆฌํ‹ฐ
375
  # ============================================================
376
  def _set_requires_grad(module: nn.Module, flag: bool):
377
+ # Toggle requires_grad for all parameters in a module.
378
+ # ๋ชจ๋“ˆ์˜ ๋ชจ๋“  ํŒŒ๋ผ๋ฏธํ„ฐ์— ๋Œ€ํ•ด requires_grad๋ฅผ ํ† ๊ธ€ํ•ฉ๋‹ˆ๋‹ค.
379
  for p in module.parameters():
380
  p.requires_grad = flag
381
 
382
 
383
  def set_bn_eval(module: nn.Module):
384
+ # Put BatchNorm layers into eval mode to freeze running stats.
385
+ # BatchNorm ๋ ˆ์ด์–ด๋ฅผ eval ๋ชจ๋“œ๋กœ ๋‘์–ด running stats๋ฅผ ๊ณ ์ •ํ•ฉ๋‹ˆ๋‹ค.
386
  for m in module.modules():
387
  if isinstance(m, (nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d, nn.SyncBatchNorm)):
388
  m.eval()
389
 
390
 
391
  def freeze_backbone(model: BackboneWithMLPHeadForImageClassification, freeze_bn: bool = True):
392
+ # Stage1: freeze backbone and train only the head.
393
+ # stage1: backbone์„ freezeํ•˜๊ณ  head๋งŒ ํ•™์Šตํ•ฉ๋‹ˆ๋‹ค.
394
  _set_requires_grad(model.backbone, False)
395
  _set_requires_grad(model.classifier, True)
396
 
 
400
 
401
 
402
  def finetune_train_mode(model: BackboneWithMLPHeadForImageClassification, keep_bn_eval: bool = True):
403
+ # Stage2: train mode, optionally keeping BN layers in eval for stability.
404
+ # stage2: train ๋ชจ๋“œ๋กœ ๋‘๋˜ ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด BN์„ eval๋กœ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
405
  model.train()
406
  meta = BACKBONE_META[model.config.backbone_name_or_path]
407
  if keep_bn_eval and meta.get("has_bn", False):
 
409
 
410
 
411
  def trainable_summary(model: nn.Module):
412
+ # Print a compact summary of trainable parameters.
413
+ # ํ•™์Šต ๊ฐ€๋Šฅ ํŒŒ๋ผ๋ฏธํ„ฐ ์š”์•ฝ์„ ๊ฐ„๋‹จํžˆ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.
414
  total = sum(p.numel() for p in model.parameters())
415
  trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
416
  ratio = trainable / total if total > 0 else 0.0
 
423
  last_n: int = 2,
424
  keep_bn_eval: bool = True,
425
  ):
426
+ # This utility implements BACKBONE_META['unfreeze']=="last_n" across supported backbones.
427
+ # ์ด ์œ ํ‹ธ์€ ์ง€์› ๋ฐฑ๋ณธ๋“ค์— ๋Œ€ํ•ด BACKBONE_META['unfreeze']=="last_n"์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
428
  freeze_backbone(model, freeze_bn=keep_bn_eval)
429
 
430
  n = int(last_n)
 
438
  bb_type = meta["type"]
439
 
440
  if bb_type == "vit":
441
+ # ViT blocks live under backbone.encoder.layer in the transformers implementation.
442
+ # ViT ๋ธ”๋ก์€ transformers ๊ตฌํ˜„์—์„œ backbone.encoder.layer ์•„๋ž˜์— ์žˆ์Šต๋‹ˆ๋‹ค.
443
  blocks = list(model.backbone.encoder.layer)
444
  for blk in blocks[-n:]:
445
  _set_requires_grad(blk, True)
446
  return
447
 
448
  if bb_type == "swin":
449
+ # Swin blocks are nested by stages and blocks; we flatten and unfreeze last n blocks.
450
+ # Swin ๋ธ”๋ก์€ stage์™€ block์œผ๋กœ ์ค‘์ฒฉ๋˜์–ด ์žˆ์–ด ํŽผ์นœ ํ›„ ๋งˆ์ง€๋ง‰ n๊ฐœ๋ฅผ unfreeze ํ•ฉ๋‹ˆ๋‹ค.
451
  stages = list(model.backbone.encoder.layers)
452
  blocks: List[nn.Module] = []
453
  for st in stages:
 
457
  return
458
 
459
  if bb_type == "resnet":
460
+ # ResNet uses layer1..layer4 stages; we unfreeze at block granularity.
461
+ # ResNet์€ layer1..layer4 stage๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ block ๋‹จ์œ„๋กœ unfreeze ํ•ฉ๋‹ˆ๋‹ค.
462
  bb = model.backbone
463
  for name in ("layer1", "layer2", "layer3", "layer4"):
464
  if not hasattr(bb, name):
 
478
  return
479
 
480
  if bb_type == "efficientnet":
481
+ # EfficientNet in transformers exposes features; we unfreeze from the tail blocks.
482
+ # transformers EfficientNet์€ features๋ฅผ ๋…ธ์ถœํ•˜๋ฉฐ ๋’ค์ชฝ ๋ธ”๋ก๋ถ€ํ„ฐ unfreeze ํ•ฉ๋‹ˆ๋‹ค.
483
  bb = model.backbone
484
  if not hasattr(bb, "features"):
485
  raise RuntimeError("Unexpected EfficientNet structure: missing features")
 
496
  return
497
 
498
  if bb_type in ("timm_densenet", "torchvision_densenet"):
499
+ # DenseNet exposes a .features module with named blocks; we unfreeze last n submodules.
500
+ # DenseNet์€ .features ๋ชจ๋“ˆ์— ๋ธ”๋ก๋“ค์ด ์ด๋ฆ„์œผ๋กœ ์กด์žฌํ•˜๋ฉฐ ๋งˆ์ง€๋ง‰ n๊ฐœ ์„œ๋ธŒ๋ชจ๋“ˆ์„ unfreeze ํ•ฉ๋‹ˆ๋‹ค.
501
  bb = model.backbone
502
  if not hasattr(bb, "features"):
503
  raise RuntimeError("Unexpected DenseNet: missing features")
 
515
  raise RuntimeError(f"Unexpected DenseNet features: missing {name}")
516
 
517
  def _denselayers(db: nn.Module) -> List[nn.Module]:
518
+ # Dense blocks contain multiple DenseLayer children; we return them for fine-grained unfreezing.
519
+ # denseblock์€ DenseLayer ์ž์‹๋“ค์„ ๊ฐ€์ง€๋ฏ€๋กœ ์„ธ๋ฐ€ํ•œ unfreeze๋ฅผ ์œ„ํ•ด ์ด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
520
  return list(db.children())
521
 
522
  blocks: List[nn.Module] = []
 
538
 
539
  # -------------------------
540
  # register
541
+ # register
542
+ # -------------------------
543
+ # Register for AutoModelForImageClassification so from_pretrained can resolve this custom class.
544
+ # from_pretrained๊ฐ€ ์ด ์ปค์Šคํ…€ ํด๋ž˜์Šค๋ฅผ ํ•ด์„ํ•  ์ˆ˜ ์žˆ๋„๋ก AutoModelForImageClassification์— ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.
545
  BackboneWithMLPHeadForImageClassification.register_for_auto_class("AutoModelForImageClassification")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
models/timm__densenet121.tv_in1k/ds_proc.py ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # src/ds_proc.py
5
+
6
+ # ============================================================
7
+ # (4) ImageProcessor (AutoImageProcessor integration)
8
+ # (4) ImageProcessor (AutoImageProcessor ์—ฐ๋™)
9
+ # ============================================================
10
+
11
+ from typing import Any
12
+
13
+ import numpy as np
14
+ import torch
15
+ from transformers import AutoImageProcessor, AutoConfig
16
+ from transformers.image_processing_base import ImageProcessingMixin
17
+ from transformers.utils.generic import TensorType
18
+
19
+ from ds_meta import BackboneID, BACKBONE_META
20
+
21
+
22
+ class BackboneMLPHead224ImageProcessor(ImageProcessingMixin):
23
+ """
24
+ This processor performs image preprocessing and outputs {"pixel_values": ...}.
25
+ ์ด processor๋Š” ์ด๋ฏธ์ง€ ์ „์ฒ˜๋ฆฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ณ  {"pixel_values": ...}๋ฅผ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.
26
+
27
+ Key requirements:
28
+ ํ•ต์‹ฌ ์š”๊ตฌ์‚ฌํ•ญ:
29
+
30
+ 1) save_pretrained() must produce a JSON-serializable preprocessor_config.json.
31
+ save_pretrained()๋Š” JSON ์ง๋ ฌํ™” ๊ฐ€๋Šฅํ•œ preprocessor_config.json์„ ์ƒ์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
32
+
33
+ 2) Runtime-only objects (delegate processor, timm/torchvision transforms) must NOT be serialized.
34
+ ๋Ÿฐํƒ€์ž„ ๊ฐ์ฒด(delegate processor, timm/torchvision transform)๋Š” ์ ˆ๋Œ€ ์ง๋ ฌํ™”ํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
35
+
36
+ 3) Runtime objects are rebuilt at init/load time based on backbone meta.
37
+ ๋Ÿฐํƒ€์ž„ ๊ฐ์ฒด๋Š” backbone meta์— ๋”ฐ๋ผ init/load ์‹œ์ ์— ์žฌ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค.
38
+ """
39
+
40
+ # HF vision models conventionally expect "pixel_values" as the primary input key.
41
+ # HF vision ๋ชจ๋ธ์€ ๊ด€๋ก€์ ์œผ๋กœ ์ž…๋ ฅ ํ‚ค๋กœ "pixel_values"๋ฅผ ๊ธฐ๋Œ€ํ•ฉ๋‹ˆ๋‹ค.
42
+ model_input_names = ["pixel_values"]
43
+
44
+ def __init__(self, backbone_name_or_path: BackboneID, is_training: bool = False, **kwargs):
45
+ # ImageProcessingMixin stores extra kwargs and manages auto_map metadata.
46
+ # ImageProcessingMixin์€ ์ถ”๊ฐ€ kwargs๋ฅผ ์ €์žฅํ•˜๊ณ  auto_map ๋ฉ”ํƒ€๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
47
+ super().__init__(**kwargs)
48
+
49
+ # Enforce whitelist via BACKBONE_META to keep behavior stable.
50
+ # ๋™์ž‘ ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด BACKBONE_META ๊ธฐ๋ฐ˜ ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค.
51
+ if backbone_name_or_path not in BACKBONE_META:
52
+ raise ValueError(
53
+ f"Unsupported backbone_name_or_path={backbone_name_or_path}. "
54
+ f"Allowed: {sorted(BACKBONE_META.keys())}"
55
+ )
56
+
57
+ # Serializable fields only: these should appear in preprocessor_config.json.
58
+ # ์ง๋ ฌํ™” ๊ฐ€๋Šฅํ•œ ํ•„๋“œ๋งŒ: ์ด ๊ฐ’๋“ค๋งŒ preprocessor_config.json์— ๋“ค์–ด๊ฐ€์•ผ ํ•ฉ๋‹ˆ๋‹ค.
59
+ self.backbone_name_or_path = backbone_name_or_path
60
+ self.is_training = bool(is_training)
61
+
62
+ # Runtime-only fields: must never be serialized.
63
+ # ๋Ÿฐํƒ€์ž„ ์ „์šฉ ํ•„๋“œ: ์ ˆ๋Œ€ ์ง๋ ฌํ™”๋˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
64
+ self._meta = None
65
+ self._delegate = None
66
+ self._timm_transform = None
67
+ self._torchvision_transform = None
68
+
69
+ # Build runtime objects according to backbone type.
70
+ # backbone type์— ๋”ฐ๋ผ ๋Ÿฐํƒ€์ž„ ๊ฐ์ฒด๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
71
+ self._build_runtime()
72
+
73
+ # ============================================================
74
+ # Runtime builders
75
+ # ๋Ÿฐํƒ€์ž„ ๋นŒ๋”
76
+ # ============================================================
77
+ def _build_runtime(self):
78
+ """
79
+ Build runtime delegate/transform based on BACKBONE_META["type"].
80
+ BACKBONE_META["type"]์— ๋”ฐ๋ผ ๋Ÿฐํƒ€์ž„ delegate/transform์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
81
+ """
82
+ meta = BACKBONE_META[self.backbone_name_or_path]
83
+ self._meta = meta
84
+
85
+ # Always reset runtime fields before rebuilding.
86
+ # ์žฌ๊ตฌ์„ฑ ์ „ ๋Ÿฐํƒ€์ž„ ํ•„๋“œ๋Š” ํ•ญ์ƒ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
87
+ self._delegate = None
88
+ self._timm_transform = None
89
+ self._torchvision_transform = None
90
+
91
+ t = meta["type"]
92
+
93
+ if t == "timm_densenet":
94
+ # timm DenseNet uses timm.data transforms for ImageNet-style preprocessing.
95
+ # timm DenseNet์€ ImageNet ์ „์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด timm.data transform์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
96
+ self._timm_transform = self._build_timm_transform(
97
+ backbone_id=self.backbone_name_or_path,
98
+ is_training=self.is_training,
99
+ )
100
+ return
101
+
102
+ if t == "torchvision_densenet":
103
+ # torchvision DenseNet requires torchvision-style preprocessing (resize/crop/tensor/normalize).
104
+ # torchvision DenseNet์€ torchvision ์Šคํƒ€์ผ ์ „์ฒ˜๋ฆฌ(resize/crop/tensor/normalize)๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
105
+ self._torchvision_transform = self._build_torchvision_densenet_transform(
106
+ is_training=self.is_training
107
+ )
108
+ return
109
+
110
+ # Default: transformers backbone delegates to its official AutoImageProcessor.
111
+ # ๊ธฐ๋ณธ: transformers ๋ฐฑ๋ณธ์€ ๊ณต์‹ AutoImageProcessor์— ์œ„์ž„ํ•ฉ๋‹ˆ๋‹ค.
112
+ self._delegate = AutoImageProcessor.from_pretrained(self.backbone_name_or_path)
113
+
114
+ @staticmethod
115
+ def _build_timm_transform(*, backbone_id: str, is_training: bool):
116
+ """
117
+ Create timm transform without storing non-serializable objects in config.
118
+ ๋น„์ง๋ ฌํ™” ๊ฐ์ฒด๋ฅผ config์— ์ €์žฅํ•˜์ง€ ์•Š๊ณ  timm transform์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
119
+ """
120
+ try:
121
+ import timm
122
+ from timm.data import resolve_model_data_config, create_transform
123
+ except Exception as e:
124
+ raise ImportError(
125
+ "timm backbone processor requires `timm`. Install: pip install timm"
126
+ ) from e
127
+
128
+ # We only need model metadata to resolve data config, so pretrained=False is preferred.
129
+ # data config ์ถ”์ถœ๋งŒ ํ•„์š”ํ•˜๋ฏ€๋กœ pretrained=False๋ฅผ ์šฐ์„  ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
130
+ m = timm.create_model(f"hf_hub:{backbone_id}", pretrained=False, num_classes=0)
131
+ dc = resolve_model_data_config(m)
132
+
133
+ # create_transform returns a torchvision-like callable that maps PIL -> torch.Tensor(C,H,W).
134
+ # create_transform์€ PIL -> torch.Tensor(C,H,W)๋กœ ๋งคํ•‘ํ•˜๋Š” callable์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
135
+ tfm = create_transform(**dc, is_training=is_training)
136
+ return tfm
137
+
138
+ @staticmethod
139
+ def _build_torchvision_densenet_transform(*, is_training: bool):
140
+ """
141
+ Build torchvision preprocessing for DenseNet-121 (224 pipeline).
142
+ DenseNet-121์šฉ torchvision ์ „์ฒ˜๋ฆฌ(224 ํŒŒ์ดํ”„๋ผ์ธ)๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
143
+ """
144
+ try:
145
+ from torchvision import transforms
146
+ except Exception as e:
147
+ raise ImportError(
148
+ "torchvision DenseNet processor requires `torchvision`. Install: pip install torchvision"
149
+ ) from e
150
+
151
+ # These are the standard ImageNet normalization stats used by torchvision weights.
152
+ # ์ด ๊ฐ’๋“ค์€ torchvision weights๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” ํ‘œ์ค€ ImageNet ์ •๊ทœํ™” ํ†ต๊ณ„์ž…๋‹ˆ๋‹ค.
153
+ mean = (0.485, 0.456, 0.406)
154
+ std = (0.229, 0.224, 0.225)
155
+
156
+ # Training pipeline typically uses RandomResizedCrop and horizontal flip.
157
+ # ํ•™์Šต ํŒŒ์ดํ”„๋ผ์ธ์€ ๋ณดํ†ต RandomResizedCrop๊ณผ ์ขŒ์šฐ๋ฐ˜์ „์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
158
+ if is_training:
159
+ return transforms.Compose(
160
+ [
161
+ transforms.RandomResizedCrop(224),
162
+ transforms.RandomHorizontalFlip(p=0.5),
163
+ transforms.ToTensor(),
164
+ transforms.Normalize(mean=mean, std=std),
165
+ ]
166
+ )
167
+
168
+ # Inference pipeline typically uses Resize(256) + CenterCrop(224).
169
+ # ์ถ”๋ก  ํŒŒ์ดํ”„๋ผ์ธ์€ ๋ณดํ†ต Resize(256) + CenterCrop(224)๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
170
+ return transforms.Compose(
171
+ [
172
+ transforms.Resize(256),
173
+ transforms.CenterCrop(224),
174
+ transforms.ToTensor(),
175
+ transforms.Normalize(mean=mean, std=std),
176
+ ]
177
+ )
178
+
179
+ # ============================================================
180
+ # Serialization
181
+ # ์ง๋ ฌํ™”
182
+ # ============================================================
183
+ def to_dict(self) -> dict[str, Any]:
184
+ """
185
+ Return a JSON-serializable dict for preprocessor_config.json.
186
+ preprocessor_config.json์— ๋“ค์–ด๊ฐˆ JSON ์ง๋ ฌํ™” dict๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
187
+
188
+ Important: do not leak runtime objects into the serialized dict.
189
+ ์ค‘์š”: ๋Ÿฐํƒ€์ž„ ๊ฐ์ฒด๊ฐ€ ์ง๋ ฌํ™” dict์— ์„ž์ด๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
190
+ """
191
+ # ImageProcessingMixin.to_dict() adds metadata such as image_processor_type/auto_map.
192
+ # ImageProcessingMixin.to_dict()๋Š” image_processor_type/auto_map ๊ฐ™์€ ๋ฉ”ํƒ€๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
193
+ d = super().to_dict()
194
+
195
+ # Force minimal stable fields for long-term compatibility.
196
+ # ์žฅ๊ธฐ ํ˜ธํ™˜์„ ์œ„ํ•ด ์ตœ์†Œ ์•ˆ์ • ํ•„๋“œ๋ฅผ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค.
197
+ d["image_processor_type"] = self.__class__.__name__
198
+ d["backbone_name_or_path"] = self.backbone_name_or_path
199
+ d["is_training"] = self.is_training
200
+
201
+ # Remove any runtime-only fields defensively.
202
+ # ๋Ÿฐํƒ€์ž„ ์ „์šฉ ํ•„๋“œ๋Š” ๋ณด์ˆ˜์ ์œผ๋กœ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.
203
+ for key in ["_meta", "_delegate", "_timm_transform", "_torchvision_transform"]:
204
+ d.pop(key, None)
205
+
206
+ return d
207
+
208
+ @classmethod
209
+ def from_dict(cls, image_processor_dict: dict[str, Any], **kwargs):
210
+ """
211
+ Standard load path used by BaseImageProcessor / AutoImageProcessor.
212
+ BaseImageProcessor / AutoImageProcessor๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” ํ‘œ์ค€ ๋กœ๋“œ ๊ฒฝ๋กœ์ž…๋‹ˆ๋‹ค.
213
+ """
214
+ backbone = image_processor_dict.get("backbone_name_or_path", None)
215
+ if backbone is None:
216
+ raise ValueError("preprocessor_config.json missing key: backbone_name_or_path")
217
+
218
+ is_training = bool(image_processor_dict.get("is_training", False))
219
+ return cls(backbone_name_or_path=backbone, is_training=is_training, **kwargs)
220
+
221
+ @classmethod
222
+ def from_pretrained(cls, pretrained_model_name_or_path: str, **kwargs):
223
+ """
224
+ Fallback path if AutoImageProcessor calls class.from_pretrained directly.
225
+ AutoImageProcessor๊ฐ€ class.from_pretrained๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋Š” ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•œ ๊ฒฝ๋กœ์ž…๋‹ˆ๋‹ค.
226
+
227
+ Strategy:
228
+ ์ „๋žต:
229
+
230
+ - Read config.json via AutoConfig and recover backbone_name_or_path.
231
+ AutoConfig๋กœ config.json์„ ์ฝ๊ณ  backbone_name_or_path๋ฅผ ๋ณต๊ตฌํ•ฉ๋‹ˆ๋‹ค.
232
+ """
233
+ cfg = AutoConfig.from_pretrained(pretrained_model_name_or_path, **kwargs)
234
+ backbone = getattr(cfg, "backbone_name_or_path", None)
235
+ if backbone is None:
236
+ raise ValueError("Cannot build processor: backbone_name_or_path not found in config.json")
237
+
238
+ # is_training is runtime-only and should default to False for inference/serving.
239
+ # is_training์€ ๋Ÿฐํƒ€์ž„ ์ „์šฉ์ด๋ฉฐ ์ถ”๋ก /์„œ๋น™ ๊ธฐ๋ณธ๊ฐ’์€ False๊ฐ€ ๋งž์Šต๋‹ˆ๋‹ค.
240
+ return cls(backbone_name_or_path=backbone, is_training=False)
241
+
242
+ # ============================================================
243
+ # Call interface
244
+ # ํ˜ธ์ถœ ์ธํ„ฐํŽ˜์ด์Šค
245
+ # ============================================================
246
+ @staticmethod
247
+ def _ensure_list(images: Any) -> list[Any]:
248
+ # Normalize scalar image input to a list for uniform processing.
249
+ # ๋‹จ์ผ ์ž…๋ ฅ์„ ๋ฆฌ์ŠคํŠธ๋กœ ์ •๊ทœํ™”ํ•˜์—ฌ ๋™์ผํ•œ ์ฒ˜๋ฆฌ ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
250
+ if isinstance(images, (list, tuple)):
251
+ return list(images)
252
+ return [images]
253
+
254
+ @staticmethod
255
+ def _to_pil_rgb(x: Any):
256
+ # Convert common image inputs into PIL RGB images.
257
+ # ์ผ๋ฐ˜์ ์ธ ์ž…๋ ฅ์„ PIL RGB ์ด๋ฏธ์ง€๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
258
+ from PIL import Image as PILImage
259
+
260
+ if isinstance(x, PILImage.Image):
261
+ return x.convert("RGB")
262
+ if isinstance(x, np.ndarray) and x.ndim == 3:
263
+ return PILImage.fromarray(x).convert("RGB")
264
+ raise TypeError(f"Unsupported image type: {type(x)}")
265
+
266
+ def __call__(
267
+ self,
268
+ images: Any | list[Any],
269
+ return_tensors: str | TensorType | None = "pt",
270
+ **kwargs,
271
+ ) -> dict[str, Any]:
272
+ """
273
+ Convert images into {"pixel_values": Tensor/ndarray}.
274
+ ์ด๋ฏธ์ง€๋ฅผ {"pixel_values": Tensor/ndarray}๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
275
+ """
276
+ images = self._ensure_list(images)
277
+
278
+ # Rebuild runtime if needed (e.g., right after deserialization).
279
+ # ์ง๋ ฌํ™” ๋ณต์› ์งํ›„ ๋“ฑ ๋Ÿฐํƒ€์ž„์ด ๋น„์–ด์žˆ์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์žฌ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
280
+ if (self._delegate is None) and (self._timm_transform is None) and (self._torchvision_transform is None):
281
+ self._build_runtime()
282
+
283
+ # timm path: PIL -> torch.Tensor(C,H,W) normalized float32.
284
+ # timm ๊ฒฝ๋กœ: PIL -> torch.Tensor(C,H,W) ์ •๊ทœํ™” float32์ž…๋‹ˆ๋‹ค.
285
+ if self._timm_transform is not None:
286
+ pv: list[torch.Tensor] = []
287
+ for im in images:
288
+ pil = self._to_pil_rgb(im)
289
+ t = self._timm_transform(pil)
290
+ if not isinstance(t, torch.Tensor):
291
+ raise RuntimeError("Unexpected timm transform output (expected torch.Tensor).")
292
+ pv.append(t)
293
+ pixel_values = torch.stack(pv, dim=0) # (B,C,H,W)
294
+ return self._format_return(pixel_values, return_tensors)
295
+
296
+ # torchvision path: PIL -> torch.Tensor(C,H,W) normalized float32.
297
+ # torchvision ๊ฒฝ๋กœ: PIL -> torch.Tensor(C,H,W) ์ •๊ทœํ™” float32์ž…๋‹ˆ๋‹ค.
298
+ if self._torchvision_transform is not None:
299
+ pv: list[torch.Tensor] = []
300
+ for im in images:
301
+ pil = self._to_pil_rgb(im)
302
+ t = self._torchvision_transform(pil)
303
+ if not isinstance(t, torch.Tensor):
304
+ raise RuntimeError("Unexpected torchvision transform output (expected torch.Tensor).")
305
+ pv.append(t)
306
+ pixel_values = torch.stack(pv, dim=0) # (B,C,H,W)
307
+ return self._format_return(pixel_values, return_tensors)
308
+
309
+ # transformers delegate path: rely on official processor behavior.
310
+ # transformers ์œ„์ž„ ๊ฒฝ๋กœ: ๊ณต์‹ processor ๋™์ž‘์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
311
+ if self._delegate is None:
312
+ raise RuntimeError("Processor runtime not built: delegate is None and no transforms are available.")
313
+
314
+ return self._delegate(images, return_tensors=return_tensors, **kwargs)
315
+
316
+ @staticmethod
317
+ def _format_return(pixel_values: torch.Tensor, return_tensors: str | TensorType | None) -> dict[str, Any]:
318
+ """
319
+ Format pixel_values according to return_tensors.
320
+ return_tensors์— ๋งž์ถฐ pixel_values ๋ฐ˜ํ™˜ ํฌ๋งท์„ ๋งž์ถฅ๋‹ˆ๋‹ค.
321
+ """
322
+ if return_tensors is None or return_tensors in ("pt", TensorType.PYTORCH):
323
+ return {"pixel_values": pixel_values}
324
+ if return_tensors in ("np", TensorType.NUMPY):
325
+ return {"pixel_values": pixel_values.detach().cpu().numpy()}
326
+ raise ValueError(f"Unsupported return_tensors={return_tensors}. Use 'pt' or 'np'.")
327
+
328
+
329
+ # Register this processor for AutoImageProcessor resolution.
330
+ # AutoImageProcessor ํ•ด์„์„ ์œ„ํ•ด ์ด processor๋ฅผ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.
331
+ if __name__ != "__main__":
332
+ BackboneMLPHead224ImageProcessor.register_for_auto_class("AutoImageProcessor")
models/timm__densenet121.tv_in1k/model.safetensors CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:5c7a629e31d700969130554fdfe93c8cd2f9344fb865b6903c3df151c0624ec6
3
  size 29293620
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:53c62717fc23ff4a775482d0f9ca8c730a2c04681a9eeefa01864847d713aa9f
3
  size 29293620
models/timm__densenet121.tv_in1k/preprocessor_config.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "auto_map": {
3
+ "AutoImageProcessor": "ds_proc.BackboneMLPHead224ImageProcessor"
4
+ },
5
+ "backbone_name_or_path": "timm/densenet121.tv_in1k",
6
+ "image_processor_type": "BackboneMLPHead224ImageProcessor",
7
+ "is_training": false
8
+ }
models/torchvision__densenet121/config.json CHANGED
@@ -24,10 +24,11 @@
24
  "num_labels": 3,
25
  "transformers_version": "5.1.0",
26
  "ds_provenance": {
27
- "created_at": "20260210_050905",
28
  "repo_id": "dsaint31/bb_mlp_224",
29
  "subdir": "models/torchvision__densenet121",
30
  "wrapper_class": "BackboneWithMLPHeadForImageClassification",
 
31
  "backbone_id": "torchvision/densenet121",
32
  "pretrained_injected": true,
33
  "policy_init_pretrained_forbidden": true,
 
24
  "num_labels": 3,
25
  "transformers_version": "5.1.0",
26
  "ds_provenance": {
27
+ "created_at": "20260210_111913",
28
  "repo_id": "dsaint31/bb_mlp_224",
29
  "subdir": "models/torchvision__densenet121",
30
  "wrapper_class": "BackboneWithMLPHeadForImageClassification",
31
+ "processor_class": "BackboneMLPHead224ImageProcessor",
32
  "backbone_id": "torchvision/densenet121",
33
  "pretrained_injected": true,
34
  "policy_init_pretrained_forbidden": true,
models/torchvision__densenet121/ds_cfg.py CHANGED
@@ -1,84 +1,175 @@
1
- from typing import Literal, Union
2
-
3
- from transformers import AutoConfig
4
  from transformers import PretrainedConfig
 
5
  from ds_meta import BackboneID, BACKBONE_META
6
 
7
- # ============================================================
8
- # 3) Config
9
- # ============================================================
10
  class BackboneMLPHeadConfig(PretrainedConfig):
11
  """
12
- backbone_name_or_path๋Š” BACKBONE_META ํ‚ค(ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ)๋งŒ ํ—ˆ์šฉ.
13
-
14
- ์ค‘์š”ํ•œ ์ 
15
- ----------
16
- - transformers ๋‚ด๋ถ€ ๊ฒฝ๋กœ์—์„œ "๋ฌด์ธ์ž ์ƒ์„ฑ"์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ,
17
- backbone_name_or_path=None์ธ ๊ฒฝ์šฐ๋Š” ๊ฒ€์ฆ ์—†์ด ๊ธฐ๋ณธ๊ฐ’๋งŒ ์„ธํŒ…ํ•˜๊ณ  return
18
- ํ•˜๋Š” ๊ตฌํ˜„์ด ๋ฐ˜๋“œ์‹œ ํ•„์š”ํ•จ.
19
- - ๋‹จ, ์‹ค์ œ ์‚ฌ์šฉ์—์„œ๋Š” backbone_name_or_path๋ฅผ ๋ฐ˜๋“œ์‹œ ์ง€์ •ํ•ด์„œ ์ƒ์„ฑํ•ด์•ผ ํ•จ.
20
  """
21
 
22
- # AutoConfig๊ฐ€ ์ด ๋ชจ๋ธ์„ ์‹๋ณ„ํ•˜๋Š”๋ฐ ์‚ฌ์šฉํ•˜๋Š” ๋ฌธ์ž์—ด ID.
 
23
  model_type = "backbone-mlphead-224-fixed"
24
 
25
- # ์ƒ์„ฑ์ž์—์„œ ๋ชจ๋“  ์ธ์ž์— ๊ธฐ๋ณธ๊ฐ’ ๋ถ€์—ฌ ๋ฐ˜๋“œ์‹œ ํ•„์š”!
26
  def __init__(
27
  self,
28
  backbone_name_or_path: BackboneID | None = None,
29
  mlp_head_bottleneck: int = 256,
30
  mlp_head_dropout: float = 0.2,
31
- label2id: dict[str, int]|None = None,
32
- id2label: dict[int, str]|None = None,
33
  **kwargs,
34
  ):
35
- # ๋ฌด์ธ์ž ์ƒ์„ฑ ๊ฒฝ๋กœ ๋ฐฉ์–ด
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  if backbone_name_or_path is None:
 
 
 
 
 
 
 
37
  super().__init__(**kwargs)
 
 
 
38
  self.backbone_name_or_path = None
39
- self.mlp_head_bottleneck = int(mlp_head_bottleneck)
40
- self.mlp_head_dropout = float(mlp_head_dropout)
41
- # num_labels๋„ ๊ธฐ๋ณธ๊ฐ’ ํ•˜๋‚˜ ์ค˜์„œ 0 ๋ฐฉ์ง€(3์œผ๋กœ ๋‘๋“  0์œผ๋กœ ๋‘๋“  ์ •์ฑ…)
42
- # ์ตœ์†Œ ์•ˆ์ „: id2label์ด ์žˆ์œผ๋ฉด ๊ทธ ๊ธธ์ด, ์—†์œผ๋ฉด 0
 
 
 
 
 
 
 
43
  return
44
 
45
- # ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๊ฒ€์ฆ
 
 
 
 
 
 
 
 
46
  if backbone_name_or_path not in BACKBONE_META:
47
  raise ValueError(
48
  f"Unsupported backbone_name_or_path={backbone_name_or_path}. "
49
  f"Allowed: {sorted(BACKBONE_META.keys())}"
50
  )
51
- if id2label is None and label2id is None:
52
- raise ValueError("id2label/label2id ๋‘˜ ์ค‘ ํ•˜๋‚˜๋Š” ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.")
53
- if id2label is None:
54
- id2label = {v: k for k, v in label2id.items()}
55
- if label2id is None:
56
- label2id = {v: k for k, v in id2label.items()}
57
-
58
- num_labels = len(id2label)
59
- # kwargs์— ์ด๋ฏธ num_labels๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ
60
- if "num_labels" not in kwargs:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  kwargs["num_labels"] = num_labels
62
 
 
 
 
 
 
 
63
  super().__init__(
64
- label2id=label2id,
65
- id2label=id2label,
66
  **kwargs,
67
  )
68
-
69
- # 4) ์ค‘์š”: ๋ถ€๋ชจ ํ•„๋“œ์—๋„ num_labels ๋ฐ˜์˜(์ €์žฅ/๋กœ๋“œ ์•ˆ์ •)
70
- self.num_labels = num_labels
71
- self.label2id = label2id
72
- self.id2label = id2label
73
 
 
 
 
 
 
 
74
  self.backbone_name_or_path = backbone_name_or_path
75
- self.mlp_head_bottleneck = int(mlp_head_bottleneck)
76
- self.mlp_head_dropout = float(mlp_head_dropout)
 
 
 
 
77
 
78
  def to_dict(self):
 
 
79
  output = super().to_dict()
80
- # ๋ช…์‹œ์ ์œผ๋กœ ํฌํ•จ
81
- output["num_labels"] = self.num_labels
 
 
 
 
82
  return output
83
 
 
 
 
84
  BackboneMLPHeadConfig.register_for_auto_class("AutoConfig")
 
 
 
 
1
  from transformers import PretrainedConfig
2
+
3
  from ds_meta import BackboneID, BACKBONE_META
4
 
5
+
 
 
6
  class BackboneMLPHeadConfig(PretrainedConfig):
7
  """
8
+ Configuration for Backbone + MLP Head models.
9
+
10
+ Backbone + MLP Head ๋ชจ๋ธ์„ ์œ„ํ•œ ์„ค์ • ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.
 
 
 
 
 
11
  """
12
 
13
+ # This string is used by Hugging Face AutoConfig to identify the config class.
14
+ # Hugging Face AutoConfig๊ฐ€ ์ด config๋ฅผ ์‹๋ณ„ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” ๊ณ ์œ  ID์ž…๋‹ˆ๋‹ค.
15
  model_type = "backbone-mlphead-224-fixed"
16
 
 
17
  def __init__(
18
  self,
19
  backbone_name_or_path: BackboneID | None = None,
20
  mlp_head_bottleneck: int = 256,
21
  mlp_head_dropout: float = 0.2,
22
+ label2id: dict[str, int] | None = None,
23
+ id2label: dict[int, str] | None = None,
24
  **kwargs,
25
  ):
26
+ # ============================================================
27
+ # 0) Guard for argument-less construction
28
+ # 0) ๋ฌด์ธ์ž ์ƒ์„ฑ ๊ฒฝ๋กœ ๋ฐฉ์–ด
29
+ # ============================================================
30
+ # Transformers may internally construct this config without arguments
31
+ # (e.g., during AutoConfig resolution or Hub loading).
32
+ # Transformers ๋‚ด๋ถ€์—์„œ AutoConfig ๋˜๋Š” Hub ๋กœ๋“œ ๊ณผ์ • ์ค‘
33
+ # ์ธ์ž ์—†์ด config๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ฒฝ๋กœ๊ฐ€ ์‹ค์ œ๋กœ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.
34
+ #
35
+ # In this case, we must NOT validate or raise errors.
36
+ # ์ด ๊ฒฝ์šฐ ๊ฒ€์ฆ์ด๋‚˜ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
37
+ #
38
+ # The goal here is to provide a minimal, serialization-safe config.
39
+ # ๋ชฉํ‘œ๋Š” ์ตœ์†Œํ•œ์˜ ๊ฐ’๋งŒ ์ฑ„์›Œ ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™”๊ฐ€ ๊นจ์ง€์ง€ ์•Š๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.
40
  if backbone_name_or_path is None:
41
+ # num_labels may be implicitly assumed by downstream code,
42
+ # so we explicitly set a safe default.
43
+ # num_labels๋Š” downstream ์ฝ”๋“œ์—์„œ ์•”๋ฌต์ ์œผ๋กœ ๊ฐ€์ •๋˜๋ฏ€๋กœ
44
+ # ์•ˆ์ „ํ•œ ๊ธฐ๋ณธ๊ฐ’์„ ๋ช…์‹œ์ ์œผ๋กœ ๋„ฃ์–ด์ค๋‹ˆ๋‹ค.
45
+ if "num_labels" not in kwargs:
46
+ kwargs["num_labels"] = 0
47
+
48
  super().__init__(**kwargs)
49
+
50
+ # Backbone is intentionally unset in this path.
51
+ # ์ด ๊ฒฝ๋กœ์—์„œ๋Š” backbone์„ ์˜๋„์ ์œผ๋กœ ์„ค์ •ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
52
  self.backbone_name_or_path = None
53
+
54
+ # Store MLP head hyperparameters for completeness.
55
+ # MLP head ๊ด€๋ จ ํ•˜์ดํผํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ํ˜•ํƒœ ์œ ์ง€๋ฅผ ์œ„ํ•ด ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
56
+ self.mlp_head_bottleneck = int(mlp_head_bottleneck)
57
+ self.mlp_head_dropout = float(mlp_head_dropout)
58
+
59
+ # Empty label mappings ensure stable save/load behavior.
60
+ # ๋นˆ label ๋งคํ•‘์€ ์ €์žฅ/๋กœ๋“œ ์•ˆ์ •์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.
61
+ self.label2id = {}
62
+ self.id2label = {}
63
+ self.num_labels = int(kwargs.get("num_labels", 0))
64
  return
65
 
66
+ # ============================================================
67
+ # 1) Backbone whitelist validation
68
+ # 1) Backbone ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๊ฒ€์ฆ
69
+ # ============================================================
70
+ # Only backbones explicitly registered in BACKBONE_META are allowed.
71
+ # BACKBONE_META์— ๋“ฑ๋ก๋œ backbone๋งŒ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.
72
+ #
73
+ # This prevents accidental usage of unsupported or inconsistent backbones.
74
+ # ์ง€์›๋˜์ง€ ์•Š๊ฑฐ๋‚˜ ๋ถˆ์ผ์น˜ํ•œ backbone ์‚ฌ์šฉ์„ ์›์ฒœ ์ฐจ๋‹จํ•ฉ๋‹ˆ๋‹ค.
75
  if backbone_name_or_path not in BACKBONE_META:
76
  raise ValueError(
77
  f"Unsupported backbone_name_or_path={backbone_name_or_path}. "
78
  f"Allowed: {sorted(BACKBONE_META.keys())}"
79
  )
80
+
81
+ # ============================================================
82
+ # 2) Label mapping normalization
83
+ # 2) ๋ผ๋ฒจ ๋งคํ•‘ ์ •๊ทœํ™”
84
+ # ============================================================
85
+ # Both label2id and id2label may be None during pure loading scenarios.
86
+ # ๋‹จ์ˆœ ๋กœ๋“œ(from_pretrained) ์‹œ label2id/id2label์ด None์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
87
+ #
88
+ # We allow this here to keep Hub loading and verification stable.
89
+ # Hub ๋กœ๋“œ ๋ฐ ๊ฒ€์ฆ ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด ์—ฌ๊ธฐ์„œ๋Š” ์ด๋ฅผ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.
90
+ #
91
+ # Fail-fast validation should happen at model or training level instead.
92
+ # ์‹ค์ œ ์‚ฌ์šฉ ๊ฒ€์ฆ์€ ๋ชจ๋ธ ๋˜๋Š” ํ•™์Šต ๋‹จ๊ณ„์—์„œ fail-fast๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
93
+ if label2id is None and id2label is None:
94
+ # Respect num_labels if explicitly provided, otherwise default to 0.
95
+ # num_labels๊ฐ€ ์ฃผ์–ด์กŒ๋‹ค๋ฉด ์กด์ค‘ํ•˜๊ณ , ์•„๋‹ˆ๋ฉด 0์œผ๋กœ ๋‘ก๋‹ˆ๋‹ค.
96
+ num_labels = int(kwargs.get("num_labels", 0))
97
+ label2id_norm: dict[str, int] = {}
98
+ id2label_norm: dict[int, str] = {}
99
+ else:
100
+ # If only one mapping is provided, derive the other.
101
+ # ํ•˜๋‚˜๋งŒ ์ฃผ์–ด์ง„ ๊ฒฝ์šฐ ๋‚˜๋จธ์ง€๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
102
+ if id2label is None:
103
+ id2label = {v: k for k, v in label2id.items()}
104
+ if label2id is None:
105
+ label2id = {v: k for k, v in id2label.items()}
106
+
107
+ # Ensure both mappings are consistent in size.
108
+ # ๋‘ ๋งคํ•‘์˜ ํฌ๊ธฐ๊ฐ€ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
109
+ if len(label2id) != len(id2label):
110
+ raise ValueError(
111
+ f"label2id/id2label size mismatch: "
112
+ f"{len(label2id)} vs {len(id2label)}"
113
+ )
114
+
115
+ num_labels = len(id2label)
116
+ label2id_norm = dict(label2id)
117
+ id2label_norm = dict(id2label)
118
+
119
+ # ============================================================
120
+ # 3) num_labels consistency enforcement
121
+ # 3) num_labels ์ผ๊ด€์„ฑ ๊ฐ•์ œ
122
+ # ============================================================
123
+ # If num_labels is provided via kwargs, it must match inferred labels.
124
+ # kwargs๋กœ num_labels๊ฐ€ ๋“ค์–ด์˜จ ๊ฒฝ์šฐ ์ถ”๋ก ๋œ ๊ฐ’๊ณผ ๋ฐ˜๋“œ์‹œ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
125
+ if "num_labels" in kwargs:
126
+ if (label2id is not None or id2label is not None) and int(kwargs["num_labels"]) != num_labels:
127
+ raise ValueError(
128
+ f"kwargs['num_labels']={kwargs['num_labels']} "
129
+ f"!= inferred num_labels={num_labels}"
130
+ )
131
+ else:
132
  kwargs["num_labels"] = num_labels
133
 
134
+ # ============================================================
135
+ # 4) Parent initialization
136
+ # 4) ๋ถ€๋ชจ ํด๋ž˜์Šค ์ดˆ๊ธฐํ™”
137
+ # ============================================================
138
+ # Initialize PretrainedConfig with normalized label mappings.
139
+ # ์ •๊ทœํ™”๋œ ๋ผ๋ฒจ ๋งคํ•‘์„ ์‚ฌ์šฉํ•˜์—ฌ PretrainedConfig๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
140
  super().__init__(
141
+ label2id=label2id_norm,
142
+ id2label=id2label_norm,
143
  **kwargs,
144
  )
 
 
 
 
 
145
 
146
+ # ============================================================
147
+ # 5) Explicit attribute assignment for save/load stability
148
+ # 5) ์ €์žฅ/๋กœ๋“œ ์•ˆ์ •์„ฑ์„ ์œ„ํ•œ ๋ช…์‹œ์  ์†์„ฑ ๊ณ ์ •
149
+ # ============================================================
150
+ # Explicitly reassign critical fields to avoid subtle serialization issues.
151
+ # ๋ฏธ๋ฌ˜ํ•œ ์ง๋ ฌํ™” ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ํ•ต์‹ฌ ํ•„๋“œ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ๋‹ค์‹œ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
152
  self.backbone_name_or_path = backbone_name_or_path
153
+ self.mlp_head_bottleneck = int(mlp_head_bottleneck)
154
+ self.mlp_head_dropout = float(mlp_head_dropout)
155
+
156
+ self.label2id = label2id_norm
157
+ self.id2label = id2label_norm
158
+ self.num_labels = int(kwargs["num_labels"])
159
 
160
  def to_dict(self):
161
+ # Call the parent implementation first.
162
+ # ๋จผ์ € ๋ถ€๋ชจ ๊ตฌํ˜„์„ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
163
  output = super().to_dict()
164
+
165
+ # Force num_labels to be present and consistent.
166
+ # num_labels๊ฐ€ ๋ฐ˜๋“œ์‹œ ์กด์žฌํ•˜๊ณ  ์ผ๊ด€๋˜๋„๋ก ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค.
167
+ output["num_labels"] = int(
168
+ getattr(self, "num_labels", output.get("num_labels", 0))
169
+ )
170
  return output
171
 
172
+
173
+ # Register this config so it can be resolved via AutoConfig.
174
+ # AutoConfig๋ฅผ ํ†ตํ•ด ์ด config๊ฐ€ ํ•ด์„๋  ์ˆ˜ ์žˆ๋„๋ก ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.
175
  BackboneMLPHeadConfig.register_for_auto_class("AutoConfig")
models/torchvision__densenet121/ds_model.py CHANGED
@@ -1,10 +1,9 @@
1
  #!/usr/bin/env python3
2
  # -*- coding: utf-8 -*-
3
 
4
- from __future__ import annotations
5
 
6
- from dataclasses import dataclass
7
- from typing import Dict, Any, Optional, Tuple, Set, List
8
 
9
  import torch
10
  import torch.nn as nn
@@ -12,146 +11,88 @@ import torch.nn.functional as F
12
 
13
  # --- transformers core ---
14
  from transformers.modeling_utils import PreTrainedModel
15
- from transformers import AutoModel, AutoConfig, PretrainedConfig
16
  from transformers.modeling_outputs import ImageClassifierOutput
17
 
18
  # --- torchvision ---
19
  from torchvision import models as tv_models
20
 
21
-
22
- # ============================================================
23
- # (0) Config
24
- # - ๊ธฐ์กด ds_cfg.BackboneMLPHeadConfig์™€ ํ•„๋“œ๋ช…์„ ๋งž์ถ”์—ˆ์Šต๋‹ˆ๋‹ค.
25
- # - ์ด๋ฏธ ds_cfg.py๊ฐ€ ์žˆ๋‹ค๋ฉด: ์ด ๋ธ”๋ก์„ ์ง€์šฐ๊ณ  "from ds_cfg import BackboneMLPHeadConfig"๋กœ ๋ฐ”๊ฟ”๋„ ๋ฉ๋‹ˆ๋‹ค.
26
- # ============================================================
27
- class BackboneMLPHeadConfig(PretrainedConfig):
28
- model_type = "backbone_mlphead"
29
-
30
- def __init__(
31
- self,
32
- backbone_name_or_path: Optional[str] = None,
33
- num_labels: int = 2,
34
- id2label: Optional[Dict[int, str]] = None,
35
- label2id: Optional[Dict[str, int]] = None,
36
- mlp_head_bottleneck: int = 256,
37
- mlp_head_dropout: float = 0.2,
38
- **kwargs,
39
- ):
40
- super().__init__(**kwargs)
41
- self.backbone_name_or_path = backbone_name_or_path
42
- self.num_labels = num_labels
43
- self.id2label = id2label or {i: f"LABEL_{i}" for i in range(num_labels)}
44
- self.label2id = label2id or {v: k for k, v in self.id2label.items()}
45
- self.mlp_head_bottleneck = mlp_head_bottleneck
46
- self.mlp_head_dropout = mlp_head_dropout
47
-
48
-
49
- # ============================================================
50
- # (1) BACKBONE_META
51
- # - ์ด๋ฏธ ds_meta.py๊ฐ€ ์žˆ๋‹ค๋ฉด: ์ด ๋ธ”๋ก์„ ์ง€์šฐ๊ณ  "from ds_meta import BACKBONE_META"๋กœ ๋ฐ”๊ฟ”๋„ ๋ฉ๋‹ˆ๋‹ค.
52
- # ============================================================
53
- BACKBONE_META: Dict[str, Dict[str, Any]] = {
54
- # transformers
55
- "google/vit-base-patch16-224": {
56
- "type": "vit",
57
- "feat_rule": "cls",
58
- "feat_dim": 768,
59
- "has_bn": False,
60
- "unfreeze": "last_n",
61
- },
62
- "microsoft/swin-tiny-patch4-window7-224": {
63
- "type": "swin",
64
- "feat_rule": "pool_or_mean",
65
- "feat_dim": 768,
66
- "has_bn": False,
67
- "unfreeze": "last_n",
68
- },
69
- "microsoft/resnet-50": {
70
- "type": "resnet",
71
- "feat_rule": "pool_or_gap",
72
- "feat_dim": 2048,
73
- "has_bn": True,
74
- "unfreeze": "last_n",
75
- },
76
- "google/efficientnet-b0": {
77
- "type": "efficientnet",
78
- "feat_rule": "pool_or_gap",
79
- "feat_dim": 1280,
80
- "has_bn": True,
81
- "unfreeze": "last_n",
82
- },
83
-
84
- # densenet
85
- "timm/densenet121.tv_in1k": {
86
- "type": "timm_densenet",
87
- "feat_rule": "timm_gap",
88
- "feat_dim": 1024,
89
- "has_bn": True,
90
- "unfreeze": "last_n",
91
- },
92
- "torchvision/densenet121": {
93
- "type": "torchvision_densenet",
94
- "feat_rule": "torchvision_densenet_gap",
95
- "feat_dim": 1024,
96
- "has_bn": True,
97
- "unfreeze": "last_n",
98
- },
99
- }
100
-
101
-
102
- # ============================================================
103
- # (2) MLP Head
104
- # - ์ด๋ฏธ mlp_head.py๊ฐ€ ์žˆ๋‹ค๋ฉด: ์ด ๋ธ”๋ก์„ ์ง€์šฐ๊ณ  "from mlp_head import MLPHead"๋กœ ๋ฐ”๊ฟ”๋„ ๋ฉ๋‹ˆ๋‹ค.
105
- # ============================================================
106
- class MLPHead(nn.Module):
107
- def __init__(self, in_dim: int, num_labels: int, bottleneck: int = 256, p: float = 0.2):
108
- super().__init__()
109
- self.fc2 = nn.Linear(in_dim, bottleneck)
110
- self.act2 = nn.GELU()
111
- self.drop2 = nn.Dropout(p)
112
- self.fc3 = nn.Linear(bottleneck, num_labels)
113
-
114
- def forward(self, x: torch.Tensor) -> torch.Tensor:
115
- x = self.drop2(self.act2(self.fc2(x)))
116
- return self.fc3(x)
117
-
118
 
119
  # ============================================================
120
  # (3) Model: backbone + MLP head
121
- # ํ•ต์‹ฌ ์„ค๊ณ„:
122
- # - __init__: backbone ๊ตฌ์กฐ(skeleton)๋งŒ ์ƒ์„ฑ. pretrained๋ฅผ ์ ˆ๋Œ€ ๋กœ๋“œํ•˜์ง€ ์•Š์Œ.
123
- # - pretrained ์ฃผ์ž…: model.load_backbone_pretrained_()๋ฅผ "fresh-start์—์„œ๋งŒ" ์ง์ ‘ ํ˜ธ์ถœ.
124
- # - from_pretrained override ์—†์Œ: HF ๊ธฐ๋ณธ ๋กœ๋”๊ฐ€ checkpoint๋ฅผ ๊ทธ๋Œ€๋กœ ๋ณต์›.
125
  # ============================================================
 
 
 
 
 
 
 
 
126
  class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
 
 
127
  config_class = BackboneMLPHeadConfig
128
 
129
  def __init__(self, config: BackboneMLPHeadConfig):
 
 
130
  super().__init__(config)
131
 
 
 
 
 
 
132
  if config.backbone_name_or_path is None:
133
- raise ValueError("config.backbone_name_or_path is None. Provide a valid backbone id.")
 
 
 
134
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  self._meta = BACKBONE_META[config.backbone_name_or_path]
136
 
137
- # (์ค‘์š”) backbone์€ ํ•ญ์ƒ skeleton๋งŒ ์ƒ์„ฑ
 
138
  self.backbone = self._build_backbone_skeleton(config.backbone_name_or_path)
139
 
140
- # head
 
141
  self.classifier = MLPHead(
142
  in_dim=int(self._meta["feat_dim"]),
143
- num_labels=config.num_labels,
144
- bottleneck=config.mlp_head_bottleneck,
145
- p=config.mlp_head_dropout,
146
  )
147
 
148
- # HF init hook (init_weights๋Š” head-only๋กœ override๋จ)
 
149
  self.post_init()
150
 
151
  def init_weights(self):
152
  """
153
- HF์˜ ๊ธฐ๋ณธ init_weights()๋Š” ๋ชจ๋ธ ์ „์ฒด์— apply(...)๋ฅผ ๊ฑธ ์ˆ˜ ์žˆ์œผ๋‹ˆ,
154
- head๋งŒ ์ดˆ๊ธฐํ™”ํ•œ๋‹ค.
 
 
 
155
  """
156
  if getattr(self, "classifier", None) is not None:
157
  self.classifier.apply(self._init_weights)
@@ -159,8 +100,11 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
159
 
160
  # ----------------------------
161
  # backbone skeleton builders
 
162
  # ----------------------------
163
  def _build_backbone_skeleton(self, backbone_id: str) -> nn.Module:
 
 
164
  meta = BACKBONE_META[backbone_id]
165
  t = meta["type"]
166
 
@@ -170,18 +114,24 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
170
  if t == "torchvision_densenet":
171
  return self._build_torchvision_densenet_skeleton(backbone_id)
172
 
173
- # transformers: config๋กœ ๊ตฌ์กฐ๋งŒ ์ƒ์„ฑ
 
174
  bb_cfg = AutoConfig.from_pretrained(backbone_id)
175
  return AutoModel.from_config(bb_cfg)
176
 
177
  @staticmethod
178
  def _build_timm_densenet_skeleton(hf_repo_id: str) -> nn.Module:
 
 
179
  try:
180
  import timm
181
  except Exception as e:
182
- raise ImportError("DenseNet(timm) backbone requires `timm`. Install: pip install timm") from e
 
 
183
 
184
- # ๊ตฌ์กฐ๋งŒ (pretrained=False)
 
185
  return timm.create_model(
186
  f"hf_hub:{hf_repo_id}",
187
  pretrained=False,
@@ -190,33 +140,33 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
190
 
191
  @staticmethod
192
  def _build_torchvision_densenet_skeleton(model_id: str) -> nn.Module:
193
- mapping = {
194
- "torchvision/densenet121": "densenet121",
195
- "torchvision/densenet169": "densenet169",
196
- "torchvision/densenet201": "densenet201",
197
- "torchvision/densenet161": "densenet161",
198
- }
199
- if model_id not in mapping:
200
- raise ValueError(f"Unsupported torchvision DenseNet id: {model_id}")
201
-
202
- arch = mapping[model_id]
203
- fn = getattr(tv_models, arch)
204
-
205
- # ๊ตฌ์กฐ๋งŒ (weights=None)
206
- m = fn(weights=None).eval()
207
  return m
208
 
209
  # ------------------------------------------------------------
210
- # pretrained ๋กœ๋”ฉ์€ "๋ช…์‹œ์  ํ˜ธ์ถœ"๋กœ๋งŒ ์ˆ˜ํ–‰ (fresh-start ์ „์šฉ)
 
211
  # ------------------------------------------------------------
212
  @torch.no_grad()
213
- def load_backbone_pretrained_(self, *, low_cpu_mem_usage: bool = False, device_map=None):
 
 
 
 
 
214
  """
215
- fresh-start ์ „์šฉ:
216
- model = BackboneWithMLPHeadForImageClassification(cfg)
217
- model.load_backbone_pretrained_()
218
 
219
- checkpoint reload(from_pretrained)์—์„œ๋Š” ํ˜ธ์ถœํ•˜์ง€ ๋งˆ์‹ญ์‹œ์˜ค.
 
220
  """
221
  bb = self.config.backbone_name_or_path
222
  meta = BACKBONE_META[bb]
@@ -230,48 +180,58 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
230
  self._load_torchvision_pretrained_into_skeleton_(bb)
231
  return
232
 
233
- # transformers pretrained -> skeleton์— ์ฃผ์ž…
 
234
  ref = AutoModel.from_pretrained(
235
  bb,
236
  low_cpu_mem_usage=low_cpu_mem_usage,
237
  device_map=device_map,
238
  )
 
 
 
239
  self.backbone.load_state_dict(ref.state_dict(), strict=False)
240
  del ref
241
 
242
  @torch.no_grad()
243
  def _load_timm_pretrained_into_skeleton_(self, hf_repo_id: str):
 
 
244
  import timm
 
 
 
245
  ref = timm.create_model(
246
  f"hf_hub:{hf_repo_id}",
247
  pretrained=True,
248
  num_classes=0,
249
  ).eval()
 
250
  self.backbone.load_state_dict(ref.state_dict(), strict=True)
251
  del ref
252
 
253
  @torch.no_grad()
254
  def _load_torchvision_pretrained_into_skeleton_(self, model_id: str):
255
- mapping = {
256
- "torchvision/densenet121": ("densenet121", tv_models.DenseNet121_Weights.DEFAULT),
257
- "torchvision/densenet169": ("densenet169", tv_models.DenseNet169_Weights.DEFAULT),
258
- "torchvision/densenet201": ("densenet201", tv_models.DenseNet201_Weights.DEFAULT),
259
- "torchvision/densenet161": ("densenet161", tv_models.DenseNet161_Weights.DEFAULT),
260
- }
261
- if model_id not in mapping:
262
- raise ValueError(f"Unsupported torchvision DenseNet id: {model_id}")
263
-
264
- arch, weights = mapping[model_id]
265
- fn = getattr(tv_models, arch)
266
- ref = fn(weights=weights).eval()
267
  self.backbone.load_state_dict(ref.state_dict(), strict=True)
268
  del ref
269
 
270
  # ----------------------------
271
  # feature extraction
 
272
  # ----------------------------
273
  @staticmethod
274
  def _pool_or_gap(outputs) -> torch.Tensor:
 
 
275
  if hasattr(outputs, "pooler_output") and outputs.pooler_output is not None:
276
  x = outputs.pooler_output
277
  if x.dim() == 2:
@@ -280,29 +240,42 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
280
  return x.flatten(1)
281
  raise RuntimeError(f"Unexpected pooler_output shape: {tuple(x.shape)}")
282
 
 
 
283
  x = outputs.last_hidden_state
284
  if x.dim() == 4:
285
  return x.mean(dim=(2, 3))
 
286
  raise RuntimeError(
287
  "Expected pooler_output or (B,C,H,W) last_hidden_state for CNN backbones. "
288
  f"Got last_hidden_state shape={tuple(x.shape)}"
289
  )
290
 
291
  def _extract_features(self, outputs, pixel_values: Optional[torch.Tensor] = None) -> torch.Tensor:
 
 
292
  rule = self._meta["feat_rule"]
293
 
294
  if rule == "cls":
 
 
295
  return outputs.last_hidden_state[:, 0, :]
296
 
297
  if rule == "pool_or_mean":
 
 
298
  if hasattr(outputs, "pooler_output") and outputs.pooler_output is not None:
299
  return outputs.pooler_output
300
  return outputs.last_hidden_state.mean(dim=1)
301
 
302
  if rule == "pool_or_gap":
 
 
303
  return self._pool_or_gap(outputs)
304
 
305
  if rule == "timm_gap":
 
 
306
  if not isinstance(outputs, torch.Tensor):
307
  raise TypeError(f"timm_gap expects Tensor features, got {type(outputs)}")
308
  if outputs.dim() != 4:
@@ -310,6 +283,8 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
310
  return outputs.mean(dim=(2, 3))
311
 
312
  if rule == "torchvision_densenet_gap":
 
 
313
  if not isinstance(outputs, torch.Tensor):
314
  raise TypeError(f"torchvision_densenet_gap expects Tensor, got {type(outputs)}")
315
  if outputs.dim() != 4:
@@ -327,9 +302,13 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
327
  return_dict=True,
328
  **kwargs,
329
  ):
 
 
330
  t = self._meta["type"]
331
 
332
  if t == "timm_densenet":
 
 
333
  if pixel_values is None:
334
  raise ValueError("timm DenseNet backbone requires pixel_values.")
335
  if pixel_values.dim() != 4:
@@ -341,6 +320,8 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
341
  attentions = None
342
 
343
  elif t == "torchvision_densenet":
 
 
344
  if pixel_values is None:
345
  raise ValueError("torchvision DenseNet backbone requires pixel_values.")
346
  if pixel_values.dim() != 4:
@@ -353,6 +334,8 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
353
  attentions = None
354
 
355
  else:
 
 
356
  outputs = self.backbone(
357
  pixel_values=pixel_values,
358
  output_attentions=output_attentions,
@@ -364,10 +347,14 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
364
  hidden_states = getattr(outputs, "hidden_states", None)
365
  attentions = getattr(outputs, "attentions", None)
366
 
 
 
367
  logits = self.classifier(feats)
368
 
369
  loss = None
370
  if labels is not None:
 
 
371
  loss = F.cross_entropy(logits, labels)
372
 
373
  if not return_dict:
@@ -383,20 +370,27 @@ class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
383
 
384
 
385
  # ============================================================
386
- # Freeze / Unfreeze utilities (๋‹น์‹  ์ฝ”๋“œ ์œ ์ง€)
 
387
  # ============================================================
388
  def _set_requires_grad(module: nn.Module, flag: bool):
 
 
389
  for p in module.parameters():
390
  p.requires_grad = flag
391
 
392
 
393
  def set_bn_eval(module: nn.Module):
 
 
394
  for m in module.modules():
395
  if isinstance(m, (nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d, nn.SyncBatchNorm)):
396
  m.eval()
397
 
398
 
399
  def freeze_backbone(model: BackboneWithMLPHeadForImageClassification, freeze_bn: bool = True):
 
 
400
  _set_requires_grad(model.backbone, False)
401
  _set_requires_grad(model.classifier, True)
402
 
@@ -406,6 +400,8 @@ def freeze_backbone(model: BackboneWithMLPHeadForImageClassification, freeze_bn:
406
 
407
 
408
  def finetune_train_mode(model: BackboneWithMLPHeadForImageClassification, keep_bn_eval: bool = True):
 
 
409
  model.train()
410
  meta = BACKBONE_META[model.config.backbone_name_or_path]
411
  if keep_bn_eval and meta.get("has_bn", False):
@@ -413,6 +409,8 @@ def finetune_train_mode(model: BackboneWithMLPHeadForImageClassification, keep_b
413
 
414
 
415
  def trainable_summary(model: nn.Module):
 
 
416
  total = sum(p.numel() for p in model.parameters())
417
  trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
418
  ratio = trainable / total if total > 0 else 0.0
@@ -425,6 +423,8 @@ def unfreeze_last_stage(
425
  last_n: int = 2,
426
  keep_bn_eval: bool = True,
427
  ):
 
 
428
  freeze_backbone(model, freeze_bn=keep_bn_eval)
429
 
430
  n = int(last_n)
@@ -438,12 +438,16 @@ def unfreeze_last_stage(
438
  bb_type = meta["type"]
439
 
440
  if bb_type == "vit":
 
 
441
  blocks = list(model.backbone.encoder.layer)
442
  for blk in blocks[-n:]:
443
  _set_requires_grad(blk, True)
444
  return
445
 
446
  if bb_type == "swin":
 
 
447
  stages = list(model.backbone.encoder.layers)
448
  blocks: List[nn.Module] = []
449
  for st in stages:
@@ -453,6 +457,8 @@ def unfreeze_last_stage(
453
  return
454
 
455
  if bb_type == "resnet":
 
 
456
  bb = model.backbone
457
  for name in ("layer1", "layer2", "layer3", "layer4"):
458
  if not hasattr(bb, name):
@@ -472,6 +478,8 @@ def unfreeze_last_stage(
472
  return
473
 
474
  if bb_type == "efficientnet":
 
 
475
  bb = model.backbone
476
  if not hasattr(bb, "features"):
477
  raise RuntimeError("Unexpected EfficientNet structure: missing features")
@@ -488,6 +496,8 @@ def unfreeze_last_stage(
488
  return
489
 
490
  if bb_type in ("timm_densenet", "torchvision_densenet"):
 
 
491
  bb = model.backbone
492
  if not hasattr(bb, "features"):
493
  raise RuntimeError("Unexpected DenseNet: missing features")
@@ -505,6 +515,8 @@ def unfreeze_last_stage(
505
  raise RuntimeError(f"Unexpected DenseNet features: missing {name}")
506
 
507
  def _denselayers(db: nn.Module) -> List[nn.Module]:
 
 
508
  return list(db.children())
509
 
510
  blocks: List[nn.Module] = []
@@ -526,20 +538,8 @@ def unfreeze_last_stage(
526
 
527
  # -------------------------
528
  # register
 
 
 
 
529
  BackboneWithMLPHeadForImageClassification.register_for_auto_class("AutoModelForImageClassification")
530
-
531
-
532
- # ==========================================================
533
-
534
- # # fresh-start
535
- # cfg = BackboneMLPHeadConfig(backbone_name_or_path="timm/densenet121.tv_in1k", num_labels=3)
536
- # model = BackboneWithMLPHeadForImageClassification(cfg)
537
-
538
- # # pretrained๋ฅผ ์“ฐ๋ ค๋ฉด ๋ช…์‹œ์ ์œผ๋กœ ํ˜ธ์ถœ
539
- # model.load_backbone_pretrained_()
540
-
541
- # # ์ดํ›„ ํ•™์Šต
542
-
543
- # # checkpoint์—์„œ ์‹œ์ž‘
544
- # model = BackboneWithMLPHeadForImageClassification.from_pretrained("checkpoints/xxx")
545
- # # ์—ฌ๊ธฐ์„œ๋Š” load_backbone_pretrained_() ํ˜ธ์ถœํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
 
1
  #!/usr/bin/env python3
2
  # -*- coding: utf-8 -*-
3
 
4
+ # src/ds_model.py
5
 
6
+ from typing import Optional, List
 
7
 
8
  import torch
9
  import torch.nn as nn
 
11
 
12
  # --- transformers core ---
13
  from transformers.modeling_utils import PreTrainedModel
14
+ from transformers import AutoModel, AutoConfig
15
  from transformers.modeling_outputs import ImageClassifierOutput
16
 
17
  # --- torchvision ---
18
  from torchvision import models as tv_models
19
 
20
+ from ds_cfg import BackboneMLPHeadConfig
21
+ from ds_meta import BACKBONE_META
22
+ from mlp_head import MLPHead
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  # ============================================================
25
  # (3) Model: backbone + MLP head
26
+ # (3) ๋ชจ๋ธ: backbone + MLP head
 
 
 
27
  # ============================================================
28
+ # Design principle: __init__ builds only a skeleton and MUST NOT load pretrained weights.
29
+ # ์„ค๊ณ„ ์›์น™: __init__์—์„œ๋Š” skeleton๋งŒ ๋งŒ๋“ค๊ณ  pretrained weight ๋กœ๋“œ๋Š” ์ ˆ๋Œ€ ํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
30
+ #
31
+ # Pretrained injection is allowed ONLY via an explicit call in fresh-start flows.
32
+ # pretrained ์ฃผ์ž…์€ fresh-start ํ๋ฆ„์—์„œ์˜ ๋ช…์‹œ์  ํ˜ธ์ถœ๋กœ๋งŒ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.
33
+ #
34
+ # HF from_pretrained should restore checkpoints as-is without side effects.
35
+ # HF from_pretrained๋Š” ๋ถ€์ž‘์šฉ ์—†์ด ์ฒดํฌํฌ์ธํŠธ๋ฅผ ๊ทธ๋Œ€๋กœ ๋ณต์›ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
36
  class BackboneWithMLPHeadForImageClassification(PreTrainedModel):
37
+ # This links the model to its custom config for AutoClass usage.
38
+ # AutoClass ์‚ฌ์šฉ์„ ์œ„ํ•ด ๋ชจ๋ธ๊ณผ ์ปค์Šคํ…€ config๋ฅผ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค.
39
  config_class = BackboneMLPHeadConfig
40
 
41
  def __init__(self, config: BackboneMLPHeadConfig):
42
+ # PreTrainedModel expects a config object and stores it internally.
43
+ # PreTrainedModel์€ config ๊ฐ์ฒด๋ฅผ ๋ฐ›์•„ ๋‚ด๋ถ€์— ์ €์žฅํ•˜๋Š” ์ „์ œ๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค.
44
  super().__init__(config)
45
 
46
+ # Fail-fast: the model is not meant to be instantiated without a valid backbone id.
47
+ # fail-fast: ์œ ํšจํ•œ backbone id ์—†์ด ๋ชจ๋ธ์„ ๋งŒ๋“œ๋Š” ์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค๋Š” ํ—ˆ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
48
+ #
49
+ # Note: Transformers may create configs with no args, but models are conventionally created with configs.
50
+ # ์ฐธ๊ณ : Transformers๋Š” config ๋ฌด์ธ์ž ์ƒ์„ฑ์ด ์žˆ์„ ์ˆ˜ ์žˆ์œผ๋‚˜, ๋ชจ๋ธ์€ ๊ด€๋ก€์ ์œผ๋กœ config๋ฅผ ๋ฐ›์•„ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.
51
  if config.backbone_name_or_path is None:
52
+ raise ValueError(
53
+ "config.backbone_name_or_path is None. "
54
+ "Provide a valid backbone id (whitelist key in BACKBONE_META)."
55
+ )
56
 
57
+ # Fail-fast: training/inference requires a positive number of labels.
58
+ # fail-fast: ํ•™์Šต/์ถ”๋ก ์€ num_labels๊ฐ€ ์–‘์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.
59
+ #
60
+ # Config may exist in a minimal form for internal serialization paths, but the model should not.
61
+ # config๋Š” ๋‚ด๋ถ€ ์ง๋ ฌํ™” ๊ฒฝ๋กœ์—์„œ ์ตœ์†Œ ํ˜•ํƒœ๋กœ ์กด์žฌํ•  ์ˆ˜ ์žˆ์œผ๋‚˜ ๋ชจ๋ธ์€ ๊ทธ๋Ÿฌ๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
62
+ if int(getattr(config, "num_labels", 0)) <= 0:
63
+ raise ValueError(
64
+ f"config.num_labels must be > 0, got {getattr(config, 'num_labels', None)}. "
65
+ "Set num_labels (or id2label/label2id) when creating the config."
66
+ )
67
+
68
+ # Meta is a single source of truth for extraction and fine-tuning rules.
69
+ # meta๋Š” feature ์ถ”์ถœ ๋ฐ ๋ฏธ์„ธ์กฐ์ • ๊ทœ์น™์˜ ๋‹จ์ผ ๊ธฐ์ค€์ž…๋‹ˆ๋‹ค.
70
  self._meta = BACKBONE_META[config.backbone_name_or_path]
71
 
72
+ # Backbone skeleton is always created without pretrained weights.
73
+ # backbone skeleton์€ ํ•ญ์ƒ pretrained weight ์—†์ด ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
74
  self.backbone = self._build_backbone_skeleton(config.backbone_name_or_path)
75
 
76
+ # Head shape is driven by meta feat_dim and config.num_labels.
77
+ # head shape์€ meta์˜ feat_dim๊ณผ config.num_labels๋กœ ๊ฒฐ์ •๋ฉ๋‹ˆ๋‹ค.
78
  self.classifier = MLPHead(
79
  in_dim=int(self._meta["feat_dim"]),
80
+ num_labels=int(config.num_labels),
81
+ bottleneck=int(config.mlp_head_bottleneck),
82
+ p=float(config.mlp_head_dropout),
83
  )
84
 
85
+ # HF initialization hook, but we override init_weights to initialize head-only.
86
+ # HF ์ดˆ๊ธฐํ™” ํ›…์ด์ง€๋งŒ init_weights๋ฅผ overrideํ•˜์—ฌ head๋งŒ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
87
  self.post_init()
88
 
89
  def init_weights(self):
90
  """
91
+ Initialize only the head to avoid touching the backbone skeleton.
92
+ backbone skeleton์„ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๊ธฐ ์œ„ํ•ด head๋งŒ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
93
+
94
+ HF's default init may traverse the entire module tree, which is undesirable here.
95
+ HF ๊ธฐ๋ณธ init์€ ์ „์ฒด ๋ชจ๋“ˆ ํŠธ๋ฆฌ๋ฅผ ์ˆœํšŒํ•  ์ˆ˜ ์žˆ์–ด ์—ฌ๊ธฐ์„œ๋Š” ๋ถ€์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.
96
  """
97
  if getattr(self, "classifier", None) is not None:
98
  self.classifier.apply(self._init_weights)
 
100
 
101
  # ----------------------------
102
  # backbone skeleton builders
103
+ # backbone skeleton ์ƒ์„ฑ๊ธฐ
104
  # ----------------------------
105
  def _build_backbone_skeleton(self, backbone_id: str) -> nn.Module:
106
+ # Meta decides which loader path to use.
107
+ # meta๊ฐ€ ์–ด๋–ค ๋กœ๋” ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ• ์ง€ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.
108
  meta = BACKBONE_META[backbone_id]
109
  t = meta["type"]
110
 
 
114
  if t == "torchvision_densenet":
115
  return self._build_torchvision_densenet_skeleton(backbone_id)
116
 
117
+ # For transformers backbones: build a random-weight skeleton from config only.
118
+ # transformers ๋ฐฑ๋ณธ: config๋กœ๋ถ€ํ„ฐ ๋žœ๋ค ์ดˆ๊ธฐํ™” skeleton๋งŒ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
119
  bb_cfg = AutoConfig.from_pretrained(backbone_id)
120
  return AutoModel.from_config(bb_cfg)
121
 
122
  @staticmethod
123
  def _build_timm_densenet_skeleton(hf_repo_id: str) -> nn.Module:
124
+ # timm is an optional dependency and should be imported lazily.
125
+ # timm์€ ์˜ต์…˜ ์˜์กด์„ฑ์ด๋ฏ€๋กœ ์ง€์—ฐ import ํ•ฉ๋‹ˆ๋‹ค.
126
  try:
127
  import timm
128
  except Exception as e:
129
+ raise ImportError(
130
+ "DenseNet(timm) backbone requires `timm`. Install: pip install timm"
131
+ ) from e
132
 
133
+ # Build structure only (pretrained=False) and remove classifier head (num_classes=0).
134
+ # ๊ตฌ์กฐ๋งŒ ์ƒ์„ฑ(pretrained=False)ํ•˜๊ณ  ๋ถ„๋ฅ˜๊ธฐ head๋Š” ์ œ๊ฑฐ(num_classes=0)ํ•ฉ๋‹ˆ๋‹ค.
135
  return timm.create_model(
136
  f"hf_hub:{hf_repo_id}",
137
  pretrained=False,
 
140
 
141
  @staticmethod
142
  def _build_torchvision_densenet_skeleton(model_id: str) -> nn.Module:
143
+ # This project intentionally supports only torchvision/densenet121 in the 224 whitelist.
144
+ # ์ด ํ”„๋กœ์ ํŠธ๋Š” 224 ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ์—์„œ torchvision/densenet121๋งŒ ์˜๋„์ ์œผ๋กœ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.
145
+ if model_id != "torchvision/densenet121":
146
+ raise ValueError(f"Unsupported torchvision DenseNet id (224 whitelist only): {model_id}")
147
+
148
+ # Build structure only (weights=None) to avoid implicit pretrained loading.
149
+ # implicit pretrained ๋กœ๋“œ๋ฅผ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด ๊ตฌ์กฐ๋งŒ ์ƒ์„ฑ(weights=None)ํ•ฉ๋‹ˆ๋‹ค.
150
+ m = tv_models.densenet121(weights=None)
 
 
 
 
 
 
151
  return m
152
 
153
  # ------------------------------------------------------------
154
+ # Pretrained loading is explicit and fresh-start only
155
+ # pretrained ๋กœ๋”ฉ์€ ๋ช…์‹œ์  ํ˜ธ์ถœ์ด๋ฉฐ fresh-start ์ „์šฉ
156
  # ------------------------------------------------------------
157
  @torch.no_grad()
158
+ def load_backbone_pretrained_(
159
+ self,
160
+ *,
161
+ low_cpu_mem_usage: bool = False,
162
+ device_map=None,
163
+ ):
164
  """
165
+ Fresh-start only: inject pretrained backbone weights into the skeleton.
166
+ fresh-start ์ „์šฉ: skeleton backbone์— pretrained ๊ฐ€์ค‘์น˜๋ฅผ ์ฃผ์ž…ํ•ฉ๋‹ˆ๋‹ค.
 
167
 
168
+ Do NOT call this after from_pretrained() because it would overwrite checkpoint weights.
169
+ from_pretrained() ์ดํ›„ ํ˜ธ์ถœํ•˜๋ฉด ์ฒดํฌํฌ์ธํŠธ ๊ฐ€์ค‘์น˜๋ฅผ ๋ฎ์–ด์“ฐ๋ฏ€๋กœ ์ ˆ๋Œ€ ํ˜ธ์ถœํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
170
  """
171
  bb = self.config.backbone_name_or_path
172
  meta = BACKBONE_META[bb]
 
180
  self._load_torchvision_pretrained_into_skeleton_(bb)
181
  return
182
 
183
+ # For transformers backbones, load a reference pretrained model and copy weights into our skeleton.
184
+ # transformers ๋ฐฑ๋ณธ์€ reference pretrained ๋ชจ๋ธ์„ ๋กœ๋“œํ•œ ๋’ค skeleton์— ๊ฐ€์ค‘์น˜๋ฅผ ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค.
185
  ref = AutoModel.from_pretrained(
186
  bb,
187
  low_cpu_mem_usage=low_cpu_mem_usage,
188
  device_map=device_map,
189
  )
190
+
191
+ # strict=False is used to tolerate harmless key differences across minor versions.
192
+ # strict=False๋Š” ๋งˆ์ด๋„ˆ ๋ฒ„์ „ ์ฐจ์ด๋กœ ์ธํ•œ ๋ฌดํ•ดํ•œ ํ‚ค ์ฐจ์ด๋ฅผ ํ—ˆ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
193
  self.backbone.load_state_dict(ref.state_dict(), strict=False)
194
  del ref
195
 
196
  @torch.no_grad()
197
  def _load_timm_pretrained_into_skeleton_(self, hf_repo_id: str):
198
+ # timm must be present for timm backbones.
199
+ # timm ๋ฐฑ๋ณธ์—๋Š” timm ์„ค์น˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
200
  import timm
201
+
202
+ # Create a pretrained reference model and copy its weights strictly.
203
+ # pretrained reference ๋ชจ๋ธ์„ ๋งŒ๋“ค๊ณ  ๊ฐ€์ค‘์น˜๋ฅผ strictํ•˜๊ฒŒ ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค.
204
  ref = timm.create_model(
205
  f"hf_hub:{hf_repo_id}",
206
  pretrained=True,
207
  num_classes=0,
208
  ).eval()
209
+
210
  self.backbone.load_state_dict(ref.state_dict(), strict=True)
211
  del ref
212
 
213
  @torch.no_grad()
214
  def _load_torchvision_pretrained_into_skeleton_(self, model_id: str):
215
+ # This project intentionally supports only torchvision/densenet121 in the 224 whitelist.
216
+ # ์ด ํ”„๋กœ์ ํŠธ๋Š” 224 ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ์—์„œ torchvision/densenet121๋งŒ ์˜๋„์ ์œผ๋กœ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.
217
+ if model_id != "torchvision/densenet121":
218
+ raise ValueError(f"Unsupported torchvision DenseNet id (224 whitelist only): {model_id}")
219
+
220
+ # Use torchvision's default pretrained weights for densenet121.
221
+ # torchvision์˜ densenet121 ๊ธฐ๋ณธ pretrained weights๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
222
+ ref = tv_models.densenet121(weights=tv_models.DenseNet121_Weights.DEFAULT).eval()
223
+
 
 
 
224
  self.backbone.load_state_dict(ref.state_dict(), strict=True)
225
  del ref
226
 
227
  # ----------------------------
228
  # feature extraction
229
+ # feature ์ถ”์ถœ
230
  # ----------------------------
231
  @staticmethod
232
  def _pool_or_gap(outputs) -> torch.Tensor:
233
+ # Some transformers vision CNNs provide pooler_output explicitly.
234
+ # ์ผ๋ถ€ transformers vision CNN์€ pooler_output์„ ๋ช…์‹œ์ ์œผ๋กœ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
235
  if hasattr(outputs, "pooler_output") and outputs.pooler_output is not None:
236
  x = outputs.pooler_output
237
  if x.dim() == 2:
 
240
  return x.flatten(1)
241
  raise RuntimeError(f"Unexpected pooler_output shape: {tuple(x.shape)}")
242
 
243
+ # Otherwise we expect a CNN-style last_hidden_state=(B,C,H,W) and apply GAP.
244
+ # ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด CNN ์Šคํƒ€์ผ last_hidden_state=(B,C,H,W)๋ฅผ ๊ธฐ๋Œ€ํ•˜๊ณ  GAP์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.
245
  x = outputs.last_hidden_state
246
  if x.dim() == 4:
247
  return x.mean(dim=(2, 3))
248
+
249
  raise RuntimeError(
250
  "Expected pooler_output or (B,C,H,W) last_hidden_state for CNN backbones. "
251
  f"Got last_hidden_state shape={tuple(x.shape)}"
252
  )
253
 
254
  def _extract_features(self, outputs, pixel_values: Optional[torch.Tensor] = None) -> torch.Tensor:
255
+ # Feature rule is defined by BACKBONE_META and must remain stable across saves/loads.
256
+ # feature ๊ทœ์น™์€ BACKBONE_META๋กœ ์ •์˜๋˜๋ฉฐ ์ €์žฅ/๋กœ๋“œ ๊ฐ„ ์•ˆ์ •์ ์œผ๋กœ ์œ ์ง€๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
257
  rule = self._meta["feat_rule"]
258
 
259
  if rule == "cls":
260
+ # ViT-style: use CLS token embedding from last_hidden_state.
261
+ # ViT ์Šคํƒ€์ผ: last_hidden_state์—์„œ CLS ํ† ํฐ ์ž„๋ฒ ๋”ฉ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
262
  return outputs.last_hidden_state[:, 0, :]
263
 
264
  if rule == "pool_or_mean":
265
+ # Swin-style: prefer pooler_output if present, else mean-pool over tokens.
266
+ # Swin ์Šคํƒ€์ผ: pooler_output์ด ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉํ•˜๊ณ , ์—†์œผ๋ฉด ํ† ํฐ ํ‰๊ท  ํ’€๋ง์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
267
  if hasattr(outputs, "pooler_output") and outputs.pooler_output is not None:
268
  return outputs.pooler_output
269
  return outputs.last_hidden_state.mean(dim=1)
270
 
271
  if rule == "pool_or_gap":
272
+ # CNN-style: use pooler_output if present, else GAP over spatial dims.
273
+ # CNN ์Šคํƒ€์ผ: pooler_output์ด ์žˆ์œผ๋ฉด ์‚ฌ์šฉํ•˜๊ณ , ์—†์œผ๋ฉด ๊ณต๊ฐ„ ์ฐจ์› GAP์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
274
  return self._pool_or_gap(outputs)
275
 
276
  if rule == "timm_gap":
277
+ # timm forward_features returns a feature map (B,C,H,W) which we GAP to (B,C).
278
+ # timm forward_features๋Š” (B,C,H,W) feature map์„ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ ์ด๋ฅผ GAP์œผ๋กœ (B,C)๋กœ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
279
  if not isinstance(outputs, torch.Tensor):
280
  raise TypeError(f"timm_gap expects Tensor features, got {type(outputs)}")
281
  if outputs.dim() != 4:
 
283
  return outputs.mean(dim=(2, 3))
284
 
285
  if rule == "torchvision_densenet_gap":
286
+ # torchvision DenseNet features are feature maps (B,C,H,W) and require GAP.
287
+ # torchvision DenseNet features๋Š” (B,C,H,W) feature map์ด๋ฉฐ GAP์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
288
  if not isinstance(outputs, torch.Tensor):
289
  raise TypeError(f"torchvision_densenet_gap expects Tensor, got {type(outputs)}")
290
  if outputs.dim() != 4:
 
302
  return_dict=True,
303
  **kwargs,
304
  ):
305
+ # Type decides the backbone forward path and output format.
306
+ # type์ด backbone forward ๊ฒฝ๋กœ ๋ฐ ์ถœ๋ ฅ ํฌ๋งท์„ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.
307
  t = self._meta["type"]
308
 
309
  if t == "timm_densenet":
310
+ # timm DenseNet consumes pixel_values as a 4D tensor (B,C,H,W).
311
+ # timm DenseNet์€ pixel_values๋ฅผ 4D ํ…์„œ (B,C,H,W)๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค.
312
  if pixel_values is None:
313
  raise ValueError("timm DenseNet backbone requires pixel_values.")
314
  if pixel_values.dim() != 4:
 
320
  attentions = None
321
 
322
  elif t == "torchvision_densenet":
323
+ # torchvision DenseNet consumes pixel_values as a 4D tensor (B,C,H,W).
324
+ # torchvision DenseNet์€ pixel_values๋ฅผ 4D ํ…์„œ (B,C,H,W)๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค.
325
  if pixel_values is None:
326
  raise ValueError("torchvision DenseNet backbone requires pixel_values.")
327
  if pixel_values.dim() != 4:
 
334
  attentions = None
335
 
336
  else:
337
+ # Transformers vision models are called with pixel_values and return ModelOutput.
338
+ # transformers vision ๋ชจ๋ธ์€ pixel_values๋กœ ํ˜ธ์ถœ๋˜๋ฉฐ ModelOutput์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
339
  outputs = self.backbone(
340
  pixel_values=pixel_values,
341
  output_attentions=output_attentions,
 
347
  hidden_states = getattr(outputs, "hidden_states", None)
348
  attentions = getattr(outputs, "attentions", None)
349
 
350
+ # Classifier consumes (B, feat_dim) and returns logits (B, num_labels).
351
+ # classifier๋Š” (B, feat_dim)์„ ๋ฐ›์•„ logits (B, num_labels)๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
352
  logits = self.classifier(feats)
353
 
354
  loss = None
355
  if labels is not None:
356
+ # Cross entropy expects labels as class indices in [0, num_labels).
357
+ # cross entropy๋Š” labels๊ฐ€ [0, num_labels) ๋ฒ”์œ„์˜ class index์ด๊ธธ ๊ธฐ๋Œ€ํ•ฉ๋‹ˆ๋‹ค.
358
  loss = F.cross_entropy(logits, labels)
359
 
360
  if not return_dict:
 
370
 
371
 
372
  # ============================================================
373
+ # Freeze / Unfreeze utilities
374
+ # Freeze / Unfreeze ์œ ํ‹ธ๋ฆฌํ‹ฐ
375
  # ============================================================
376
  def _set_requires_grad(module: nn.Module, flag: bool):
377
+ # Toggle requires_grad for all parameters in a module.
378
+ # ๋ชจ๋“ˆ์˜ ๋ชจ๋“  ํŒŒ๋ผ๋ฏธํ„ฐ์— ๋Œ€ํ•ด requires_grad๋ฅผ ํ† ๊ธ€ํ•ฉ๋‹ˆ๋‹ค.
379
  for p in module.parameters():
380
  p.requires_grad = flag
381
 
382
 
383
  def set_bn_eval(module: nn.Module):
384
+ # Put BatchNorm layers into eval mode to freeze running stats.
385
+ # BatchNorm ๋ ˆ์ด์–ด๋ฅผ eval ๋ชจ๋“œ๋กœ ๋‘์–ด running stats๋ฅผ ๊ณ ์ •ํ•ฉ๋‹ˆ๋‹ค.
386
  for m in module.modules():
387
  if isinstance(m, (nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d, nn.SyncBatchNorm)):
388
  m.eval()
389
 
390
 
391
  def freeze_backbone(model: BackboneWithMLPHeadForImageClassification, freeze_bn: bool = True):
392
+ # Stage1: freeze backbone and train only the head.
393
+ # stage1: backbone์„ freezeํ•˜๊ณ  head๋งŒ ํ•™์Šตํ•ฉ๋‹ˆ๋‹ค.
394
  _set_requires_grad(model.backbone, False)
395
  _set_requires_grad(model.classifier, True)
396
 
 
400
 
401
 
402
  def finetune_train_mode(model: BackboneWithMLPHeadForImageClassification, keep_bn_eval: bool = True):
403
+ # Stage2: train mode, optionally keeping BN layers in eval for stability.
404
+ # stage2: train ๋ชจ๋“œ๋กœ ๋‘๋˜ ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด BN์„ eval๋กœ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
405
  model.train()
406
  meta = BACKBONE_META[model.config.backbone_name_or_path]
407
  if keep_bn_eval and meta.get("has_bn", False):
 
409
 
410
 
411
  def trainable_summary(model: nn.Module):
412
+ # Print a compact summary of trainable parameters.
413
+ # ํ•™์Šต ๊ฐ€๋Šฅ ํŒŒ๋ผ๋ฏธํ„ฐ ์š”์•ฝ์„ ๊ฐ„๋‹จํžˆ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.
414
  total = sum(p.numel() for p in model.parameters())
415
  trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
416
  ratio = trainable / total if total > 0 else 0.0
 
423
  last_n: int = 2,
424
  keep_bn_eval: bool = True,
425
  ):
426
+ # This utility implements BACKBONE_META['unfreeze']=="last_n" across supported backbones.
427
+ # ์ด ์œ ํ‹ธ์€ ์ง€์› ๋ฐฑ๋ณธ๋“ค์— ๋Œ€ํ•ด BACKBONE_META['unfreeze']=="last_n"์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
428
  freeze_backbone(model, freeze_bn=keep_bn_eval)
429
 
430
  n = int(last_n)
 
438
  bb_type = meta["type"]
439
 
440
  if bb_type == "vit":
441
+ # ViT blocks live under backbone.encoder.layer in the transformers implementation.
442
+ # ViT ๋ธ”๋ก์€ transformers ๊ตฌํ˜„์—์„œ backbone.encoder.layer ์•„๋ž˜์— ์žˆ์Šต๋‹ˆ๋‹ค.
443
  blocks = list(model.backbone.encoder.layer)
444
  for blk in blocks[-n:]:
445
  _set_requires_grad(blk, True)
446
  return
447
 
448
  if bb_type == "swin":
449
+ # Swin blocks are nested by stages and blocks; we flatten and unfreeze last n blocks.
450
+ # Swin ๋ธ”๋ก์€ stage์™€ block์œผ๋กœ ์ค‘์ฒฉ๋˜์–ด ์žˆ์–ด ํŽผ์นœ ํ›„ ๋งˆ์ง€๋ง‰ n๊ฐœ๋ฅผ unfreeze ํ•ฉ๋‹ˆ๋‹ค.
451
  stages = list(model.backbone.encoder.layers)
452
  blocks: List[nn.Module] = []
453
  for st in stages:
 
457
  return
458
 
459
  if bb_type == "resnet":
460
+ # ResNet uses layer1..layer4 stages; we unfreeze at block granularity.
461
+ # ResNet์€ layer1..layer4 stage๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ block ๋‹จ์œ„๋กœ unfreeze ํ•ฉ๋‹ˆ๋‹ค.
462
  bb = model.backbone
463
  for name in ("layer1", "layer2", "layer3", "layer4"):
464
  if not hasattr(bb, name):
 
478
  return
479
 
480
  if bb_type == "efficientnet":
481
+ # EfficientNet in transformers exposes features; we unfreeze from the tail blocks.
482
+ # transformers EfficientNet์€ features๋ฅผ ๋…ธ์ถœํ•˜๋ฉฐ ๋’ค์ชฝ ๋ธ”๋ก๋ถ€ํ„ฐ unfreeze ํ•ฉ๋‹ˆ๋‹ค.
483
  bb = model.backbone
484
  if not hasattr(bb, "features"):
485
  raise RuntimeError("Unexpected EfficientNet structure: missing features")
 
496
  return
497
 
498
  if bb_type in ("timm_densenet", "torchvision_densenet"):
499
+ # DenseNet exposes a .features module with named blocks; we unfreeze last n submodules.
500
+ # DenseNet์€ .features ๋ชจ๋“ˆ์— ๋ธ”๋ก๋“ค์ด ์ด๋ฆ„์œผ๋กœ ์กด์žฌํ•˜๋ฉฐ ๋งˆ์ง€๋ง‰ n๊ฐœ ์„œ๋ธŒ๋ชจ๋“ˆ์„ unfreeze ํ•ฉ๋‹ˆ๋‹ค.
501
  bb = model.backbone
502
  if not hasattr(bb, "features"):
503
  raise RuntimeError("Unexpected DenseNet: missing features")
 
515
  raise RuntimeError(f"Unexpected DenseNet features: missing {name}")
516
 
517
  def _denselayers(db: nn.Module) -> List[nn.Module]:
518
+ # Dense blocks contain multiple DenseLayer children; we return them for fine-grained unfreezing.
519
+ # denseblock์€ DenseLayer ์ž์‹๋“ค์„ ๊ฐ€์ง€๋ฏ€๋กœ ์„ธ๋ฐ€ํ•œ unfreeze๋ฅผ ์œ„ํ•ด ์ด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
520
  return list(db.children())
521
 
522
  blocks: List[nn.Module] = []
 
538
 
539
  # -------------------------
540
  # register
541
+ # register
542
+ # -------------------------
543
+ # Register for AutoModelForImageClassification so from_pretrained can resolve this custom class.
544
+ # from_pretrained๊ฐ€ ์ด ์ปค์Šคํ…€ ํด๋ž˜์Šค๋ฅผ ํ•ด์„ํ•  ์ˆ˜ ์žˆ๋„๋ก AutoModelForImageClassification์— ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.
545
  BackboneWithMLPHeadForImageClassification.register_for_auto_class("AutoModelForImageClassification")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
models/torchvision__densenet121/ds_proc.py ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # src/ds_proc.py
5
+
6
+ # ============================================================
7
+ # (4) ImageProcessor (AutoImageProcessor integration)
8
+ # (4) ImageProcessor (AutoImageProcessor ์—ฐ๋™)
9
+ # ============================================================
10
+
11
+ from typing import Any
12
+
13
+ import numpy as np
14
+ import torch
15
+ from transformers import AutoImageProcessor, AutoConfig
16
+ from transformers.image_processing_base import ImageProcessingMixin
17
+ from transformers.utils.generic import TensorType
18
+
19
+ from ds_meta import BackboneID, BACKBONE_META
20
+
21
+
22
+ class BackboneMLPHead224ImageProcessor(ImageProcessingMixin):
23
+ """
24
+ This processor performs image preprocessing and outputs {"pixel_values": ...}.
25
+ ์ด processor๋Š” ์ด๋ฏธ์ง€ ์ „์ฒ˜๋ฆฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ณ  {"pixel_values": ...}๋ฅผ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.
26
+
27
+ Key requirements:
28
+ ํ•ต์‹ฌ ์š”๊ตฌ์‚ฌํ•ญ:
29
+
30
+ 1) save_pretrained() must produce a JSON-serializable preprocessor_config.json.
31
+ save_pretrained()๋Š” JSON ์ง๋ ฌํ™” ๊ฐ€๋Šฅํ•œ preprocessor_config.json์„ ์ƒ์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
32
+
33
+ 2) Runtime-only objects (delegate processor, timm/torchvision transforms) must NOT be serialized.
34
+ ๋Ÿฐํƒ€์ž„ ๊ฐ์ฒด(delegate processor, timm/torchvision transform)๋Š” ์ ˆ๋Œ€ ์ง๋ ฌํ™”ํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
35
+
36
+ 3) Runtime objects are rebuilt at init/load time based on backbone meta.
37
+ ๋Ÿฐํƒ€์ž„ ๊ฐ์ฒด๋Š” backbone meta์— ๋”ฐ๋ผ init/load ์‹œ์ ์— ์žฌ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค.
38
+ """
39
+
40
+ # HF vision models conventionally expect "pixel_values" as the primary input key.
41
+ # HF vision ๋ชจ๋ธ์€ ๊ด€๋ก€์ ์œผ๋กœ ์ž…๋ ฅ ํ‚ค๋กœ "pixel_values"๋ฅผ ๊ธฐ๋Œ€ํ•ฉ๋‹ˆ๋‹ค.
42
+ model_input_names = ["pixel_values"]
43
+
44
+ def __init__(self, backbone_name_or_path: BackboneID, is_training: bool = False, **kwargs):
45
+ # ImageProcessingMixin stores extra kwargs and manages auto_map metadata.
46
+ # ImageProcessingMixin์€ ์ถ”๊ฐ€ kwargs๋ฅผ ์ €์žฅํ•˜๊ณ  auto_map ๋ฉ”ํƒ€๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
47
+ super().__init__(**kwargs)
48
+
49
+ # Enforce whitelist via BACKBONE_META to keep behavior stable.
50
+ # ๋™์ž‘ ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด BACKBONE_META ๊ธฐ๋ฐ˜ ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค.
51
+ if backbone_name_or_path not in BACKBONE_META:
52
+ raise ValueError(
53
+ f"Unsupported backbone_name_or_path={backbone_name_or_path}. "
54
+ f"Allowed: {sorted(BACKBONE_META.keys())}"
55
+ )
56
+
57
+ # Serializable fields only: these should appear in preprocessor_config.json.
58
+ # ์ง๋ ฌํ™” ๊ฐ€๋Šฅํ•œ ํ•„๋“œ๋งŒ: ์ด ๊ฐ’๋“ค๋งŒ preprocessor_config.json์— ๋“ค์–ด๊ฐ€์•ผ ํ•ฉ๋‹ˆ๋‹ค.
59
+ self.backbone_name_or_path = backbone_name_or_path
60
+ self.is_training = bool(is_training)
61
+
62
+ # Runtime-only fields: must never be serialized.
63
+ # ๋Ÿฐํƒ€์ž„ ์ „์šฉ ํ•„๋“œ: ์ ˆ๋Œ€ ์ง๋ ฌํ™”๋˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
64
+ self._meta = None
65
+ self._delegate = None
66
+ self._timm_transform = None
67
+ self._torchvision_transform = None
68
+
69
+ # Build runtime objects according to backbone type.
70
+ # backbone type์— ๋”ฐ๋ผ ๋Ÿฐํƒ€์ž„ ๊ฐ์ฒด๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
71
+ self._build_runtime()
72
+
73
+ # ============================================================
74
+ # Runtime builders
75
+ # ๋Ÿฐํƒ€์ž„ ๋นŒ๋”
76
+ # ============================================================
77
+ def _build_runtime(self):
78
+ """
79
+ Build runtime delegate/transform based on BACKBONE_META["type"].
80
+ BACKBONE_META["type"]์— ๋”ฐ๋ผ ๋Ÿฐํƒ€์ž„ delegate/transform์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
81
+ """
82
+ meta = BACKBONE_META[self.backbone_name_or_path]
83
+ self._meta = meta
84
+
85
+ # Always reset runtime fields before rebuilding.
86
+ # ์žฌ๊ตฌ์„ฑ ์ „ ๋Ÿฐํƒ€์ž„ ํ•„๋“œ๋Š” ํ•ญ์ƒ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.
87
+ self._delegate = None
88
+ self._timm_transform = None
89
+ self._torchvision_transform = None
90
+
91
+ t = meta["type"]
92
+
93
+ if t == "timm_densenet":
94
+ # timm DenseNet uses timm.data transforms for ImageNet-style preprocessing.
95
+ # timm DenseNet์€ ImageNet ์ „์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด timm.data transform์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
96
+ self._timm_transform = self._build_timm_transform(
97
+ backbone_id=self.backbone_name_or_path,
98
+ is_training=self.is_training,
99
+ )
100
+ return
101
+
102
+ if t == "torchvision_densenet":
103
+ # torchvision DenseNet requires torchvision-style preprocessing (resize/crop/tensor/normalize).
104
+ # torchvision DenseNet์€ torchvision ์Šคํƒ€์ผ ์ „์ฒ˜๋ฆฌ(resize/crop/tensor/normalize)๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
105
+ self._torchvision_transform = self._build_torchvision_densenet_transform(
106
+ is_training=self.is_training
107
+ )
108
+ return
109
+
110
+ # Default: transformers backbone delegates to its official AutoImageProcessor.
111
+ # ๊ธฐ๋ณธ: transformers ๋ฐฑ๋ณธ์€ ๊ณต์‹ AutoImageProcessor์— ์œ„์ž„ํ•ฉ๋‹ˆ๋‹ค.
112
+ self._delegate = AutoImageProcessor.from_pretrained(self.backbone_name_or_path)
113
+
114
+ @staticmethod
115
+ def _build_timm_transform(*, backbone_id: str, is_training: bool):
116
+ """
117
+ Create timm transform without storing non-serializable objects in config.
118
+ ๋น„์ง๋ ฌํ™” ๊ฐ์ฒด๋ฅผ config์— ์ €์žฅํ•˜์ง€ ์•Š๊ณ  timm transform์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
119
+ """
120
+ try:
121
+ import timm
122
+ from timm.data import resolve_model_data_config, create_transform
123
+ except Exception as e:
124
+ raise ImportError(
125
+ "timm backbone processor requires `timm`. Install: pip install timm"
126
+ ) from e
127
+
128
+ # We only need model metadata to resolve data config, so pretrained=False is preferred.
129
+ # data config ์ถ”์ถœ๋งŒ ํ•„์š”ํ•˜๋ฏ€๋กœ pretrained=False๋ฅผ ์šฐ์„  ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
130
+ m = timm.create_model(f"hf_hub:{backbone_id}", pretrained=False, num_classes=0)
131
+ dc = resolve_model_data_config(m)
132
+
133
+ # create_transform returns a torchvision-like callable that maps PIL -> torch.Tensor(C,H,W).
134
+ # create_transform์€ PIL -> torch.Tensor(C,H,W)๋กœ ๋งคํ•‘ํ•˜๋Š” callable์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
135
+ tfm = create_transform(**dc, is_training=is_training)
136
+ return tfm
137
+
138
+ @staticmethod
139
+ def _build_torchvision_densenet_transform(*, is_training: bool):
140
+ """
141
+ Build torchvision preprocessing for DenseNet-121 (224 pipeline).
142
+ DenseNet-121์šฉ torchvision ์ „์ฒ˜๋ฆฌ(224 ํŒŒ์ดํ”„๋ผ์ธ)๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
143
+ """
144
+ try:
145
+ from torchvision import transforms
146
+ except Exception as e:
147
+ raise ImportError(
148
+ "torchvision DenseNet processor requires `torchvision`. Install: pip install torchvision"
149
+ ) from e
150
+
151
+ # These are the standard ImageNet normalization stats used by torchvision weights.
152
+ # ์ด ๊ฐ’๋“ค์€ torchvision weights๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” ํ‘œ์ค€ ImageNet ์ •๊ทœํ™” ํ†ต๊ณ„์ž…๋‹ˆ๋‹ค.
153
+ mean = (0.485, 0.456, 0.406)
154
+ std = (0.229, 0.224, 0.225)
155
+
156
+ # Training pipeline typically uses RandomResizedCrop and horizontal flip.
157
+ # ํ•™์Šต ํŒŒ์ดํ”„๋ผ์ธ์€ ๋ณดํ†ต RandomResizedCrop๊ณผ ์ขŒ์šฐ๋ฐ˜์ „์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
158
+ if is_training:
159
+ return transforms.Compose(
160
+ [
161
+ transforms.RandomResizedCrop(224),
162
+ transforms.RandomHorizontalFlip(p=0.5),
163
+ transforms.ToTensor(),
164
+ transforms.Normalize(mean=mean, std=std),
165
+ ]
166
+ )
167
+
168
+ # Inference pipeline typically uses Resize(256) + CenterCrop(224).
169
+ # ์ถ”๋ก  ํŒŒ์ดํ”„๋ผ์ธ์€ ๋ณดํ†ต Resize(256) + CenterCrop(224)๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
170
+ return transforms.Compose(
171
+ [
172
+ transforms.Resize(256),
173
+ transforms.CenterCrop(224),
174
+ transforms.ToTensor(),
175
+ transforms.Normalize(mean=mean, std=std),
176
+ ]
177
+ )
178
+
179
+ # ============================================================
180
+ # Serialization
181
+ # ์ง๋ ฌํ™”
182
+ # ============================================================
183
+ def to_dict(self) -> dict[str, Any]:
184
+ """
185
+ Return a JSON-serializable dict for preprocessor_config.json.
186
+ preprocessor_config.json์— ๋“ค์–ด๊ฐˆ JSON ์ง๋ ฌํ™” dict๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
187
+
188
+ Important: do not leak runtime objects into the serialized dict.
189
+ ์ค‘์š”: ๋Ÿฐํƒ€์ž„ ๊ฐ์ฒด๊ฐ€ ์ง๋ ฌํ™” dict์— ์„ž์ด๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
190
+ """
191
+ # ImageProcessingMixin.to_dict() adds metadata such as image_processor_type/auto_map.
192
+ # ImageProcessingMixin.to_dict()๋Š” image_processor_type/auto_map ๊ฐ™์€ ๋ฉ”ํƒ€๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
193
+ d = super().to_dict()
194
+
195
+ # Force minimal stable fields for long-term compatibility.
196
+ # ์žฅ๊ธฐ ํ˜ธํ™˜์„ ์œ„ํ•ด ์ตœ์†Œ ์•ˆ์ • ํ•„๋“œ๋ฅผ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค.
197
+ d["image_processor_type"] = self.__class__.__name__
198
+ d["backbone_name_or_path"] = self.backbone_name_or_path
199
+ d["is_training"] = self.is_training
200
+
201
+ # Remove any runtime-only fields defensively.
202
+ # ๋Ÿฐํƒ€์ž„ ์ „์šฉ ํ•„๋“œ๋Š” ๋ณด์ˆ˜์ ์œผ๋กœ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.
203
+ for key in ["_meta", "_delegate", "_timm_transform", "_torchvision_transform"]:
204
+ d.pop(key, None)
205
+
206
+ return d
207
+
208
+ @classmethod
209
+ def from_dict(cls, image_processor_dict: dict[str, Any], **kwargs):
210
+ """
211
+ Standard load path used by BaseImageProcessor / AutoImageProcessor.
212
+ BaseImageProcessor / AutoImageProcessor๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” ํ‘œ์ค€ ๋กœ๋“œ ๊ฒฝ๋กœ์ž…๋‹ˆ๋‹ค.
213
+ """
214
+ backbone = image_processor_dict.get("backbone_name_or_path", None)
215
+ if backbone is None:
216
+ raise ValueError("preprocessor_config.json missing key: backbone_name_or_path")
217
+
218
+ is_training = bool(image_processor_dict.get("is_training", False))
219
+ return cls(backbone_name_or_path=backbone, is_training=is_training, **kwargs)
220
+
221
+ @classmethod
222
+ def from_pretrained(cls, pretrained_model_name_or_path: str, **kwargs):
223
+ """
224
+ Fallback path if AutoImageProcessor calls class.from_pretrained directly.
225
+ AutoImageProcessor๊ฐ€ class.from_pretrained๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋Š” ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•œ ๊ฒฝ๋กœ์ž…๋‹ˆ๋‹ค.
226
+
227
+ Strategy:
228
+ ์ „๋žต:
229
+
230
+ - Read config.json via AutoConfig and recover backbone_name_or_path.
231
+ AutoConfig๋กœ config.json์„ ์ฝ๊ณ  backbone_name_or_path๋ฅผ ๋ณต๊ตฌํ•ฉ๋‹ˆ๋‹ค.
232
+ """
233
+ cfg = AutoConfig.from_pretrained(pretrained_model_name_or_path, **kwargs)
234
+ backbone = getattr(cfg, "backbone_name_or_path", None)
235
+ if backbone is None:
236
+ raise ValueError("Cannot build processor: backbone_name_or_path not found in config.json")
237
+
238
+ # is_training is runtime-only and should default to False for inference/serving.
239
+ # is_training์€ ๋Ÿฐํƒ€์ž„ ์ „์šฉ์ด๋ฉฐ ์ถ”๋ก /์„œ๋น™ ๊ธฐ๋ณธ๊ฐ’์€ False๊ฐ€ ๋งž์Šต๋‹ˆ๋‹ค.
240
+ return cls(backbone_name_or_path=backbone, is_training=False)
241
+
242
+ # ============================================================
243
+ # Call interface
244
+ # ํ˜ธ์ถœ ์ธํ„ฐํŽ˜์ด์Šค
245
+ # ============================================================
246
+ @staticmethod
247
+ def _ensure_list(images: Any) -> list[Any]:
248
+ # Normalize scalar image input to a list for uniform processing.
249
+ # ๋‹จ์ผ ์ž…๋ ฅ์„ ๋ฆฌ์ŠคํŠธ๋กœ ์ •๊ทœํ™”ํ•˜์—ฌ ๋™์ผํ•œ ์ฒ˜๋ฆฌ ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
250
+ if isinstance(images, (list, tuple)):
251
+ return list(images)
252
+ return [images]
253
+
254
+ @staticmethod
255
+ def _to_pil_rgb(x: Any):
256
+ # Convert common image inputs into PIL RGB images.
257
+ # ์ผ๋ฐ˜์ ์ธ ์ž…๋ ฅ์„ PIL RGB ์ด๋ฏธ์ง€๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
258
+ from PIL import Image as PILImage
259
+
260
+ if isinstance(x, PILImage.Image):
261
+ return x.convert("RGB")
262
+ if isinstance(x, np.ndarray) and x.ndim == 3:
263
+ return PILImage.fromarray(x).convert("RGB")
264
+ raise TypeError(f"Unsupported image type: {type(x)}")
265
+
266
+ def __call__(
267
+ self,
268
+ images: Any | list[Any],
269
+ return_tensors: str | TensorType | None = "pt",
270
+ **kwargs,
271
+ ) -> dict[str, Any]:
272
+ """
273
+ Convert images into {"pixel_values": Tensor/ndarray}.
274
+ ์ด๋ฏธ์ง€๋ฅผ {"pixel_values": Tensor/ndarray}๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
275
+ """
276
+ images = self._ensure_list(images)
277
+
278
+ # Rebuild runtime if needed (e.g., right after deserialization).
279
+ # ์ง๋ ฌํ™” ๋ณต์› ์งํ›„ ๋“ฑ ๋Ÿฐํƒ€์ž„์ด ๋น„์–ด์žˆ์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์žฌ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
280
+ if (self._delegate is None) and (self._timm_transform is None) and (self._torchvision_transform is None):
281
+ self._build_runtime()
282
+
283
+ # timm path: PIL -> torch.Tensor(C,H,W) normalized float32.
284
+ # timm ๊ฒฝ๋กœ: PIL -> torch.Tensor(C,H,W) ์ •๊ทœํ™” float32์ž…๋‹ˆ๋‹ค.
285
+ if self._timm_transform is not None:
286
+ pv: list[torch.Tensor] = []
287
+ for im in images:
288
+ pil = self._to_pil_rgb(im)
289
+ t = self._timm_transform(pil)
290
+ if not isinstance(t, torch.Tensor):
291
+ raise RuntimeError("Unexpected timm transform output (expected torch.Tensor).")
292
+ pv.append(t)
293
+ pixel_values = torch.stack(pv, dim=0) # (B,C,H,W)
294
+ return self._format_return(pixel_values, return_tensors)
295
+
296
+ # torchvision path: PIL -> torch.Tensor(C,H,W) normalized float32.
297
+ # torchvision ๊ฒฝ๋กœ: PIL -> torch.Tensor(C,H,W) ์ •๊ทœํ™” float32์ž…๋‹ˆ๋‹ค.
298
+ if self._torchvision_transform is not None:
299
+ pv: list[torch.Tensor] = []
300
+ for im in images:
301
+ pil = self._to_pil_rgb(im)
302
+ t = self._torchvision_transform(pil)
303
+ if not isinstance(t, torch.Tensor):
304
+ raise RuntimeError("Unexpected torchvision transform output (expected torch.Tensor).")
305
+ pv.append(t)
306
+ pixel_values = torch.stack(pv, dim=0) # (B,C,H,W)
307
+ return self._format_return(pixel_values, return_tensors)
308
+
309
+ # transformers delegate path: rely on official processor behavior.
310
+ # transformers ์œ„์ž„ ๊ฒฝ๋กœ: ๊ณต์‹ processor ๋™์ž‘์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
311
+ if self._delegate is None:
312
+ raise RuntimeError("Processor runtime not built: delegate is None and no transforms are available.")
313
+
314
+ return self._delegate(images, return_tensors=return_tensors, **kwargs)
315
+
316
+ @staticmethod
317
+ def _format_return(pixel_values: torch.Tensor, return_tensors: str | TensorType | None) -> dict[str, Any]:
318
+ """
319
+ Format pixel_values according to return_tensors.
320
+ return_tensors์— ๋งž์ถฐ pixel_values ๋ฐ˜ํ™˜ ํฌ๋งท์„ ๋งž์ถฅ๋‹ˆ๋‹ค.
321
+ """
322
+ if return_tensors is None or return_tensors in ("pt", TensorType.PYTORCH):
323
+ return {"pixel_values": pixel_values}
324
+ if return_tensors in ("np", TensorType.NUMPY):
325
+ return {"pixel_values": pixel_values.detach().cpu().numpy()}
326
+ raise ValueError(f"Unsupported return_tensors={return_tensors}. Use 'pt' or 'np'.")
327
+
328
+
329
+ # Register this processor for AutoImageProcessor resolution.
330
+ # AutoImageProcessor ํ•ด์„์„ ์œ„ํ•ด ์ด processor๋ฅผ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.
331
+ if __name__ != "__main__":
332
+ BackboneMLPHead224ImageProcessor.register_for_auto_class("AutoImageProcessor")
models/torchvision__densenet121/model.safetensors CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:969ad71b68ca03294c98303e609ea596d833f4381ff3039131e4a5239b72dd99
3
  size 33394052
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e8431398f3461657aea5362b326d351fcdd4c5c009bf126765d0dcd60f94aa72
3
  size 33394052
models/torchvision__densenet121/preprocessor_config.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "auto_map": {
3
+ "AutoImageProcessor": "ds_proc.BackboneMLPHead224ImageProcessor"
4
+ },
5
+ "backbone_name_or_path": "torchvision/densenet121",
6
+ "image_processor_type": "BackboneMLPHead224ImageProcessor",
7
+ "is_training": false
8
+ }