luoxd96 commited on
Commit
b612017
·
verified ·
1 Parent(s): 04b383f

Add transformers model card layout (vitb/vits AutoModel)

Browse files
LICENSE ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright (c) 2026 Xiangde Luo
2
+
3
+ This work (source code, documentation, and released model weights) is licensed under the
4
+ Creative Commons Attribution-NonCommercial 4.0 International License (CC BY-NC 4.0).
5
+
6
+ You may copy, redistribute, and adapt the material for non-commercial purposes only,
7
+ provided appropriate credit is given. Commercial use is not permitted.
8
+
9
+ Full license text:
10
+ https://creativecommons.org/licenses/by-nc/4.0/legalcode
11
+ Human-readable summary:
12
+ https://creativecommons.org/licenses/by-nc/4.0/
13
+
14
+ THE MATERIAL IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
README.md ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: cc-by-nc-4.0
3
+ library_name: transformers
4
+ tags:
5
+ - pathology
6
+ - vision
7
+ - vit
8
+ - feature-extraction
9
+ - knowledge-distillation
10
+ - pytorch
11
+ pipeline_tag: image-feature-extraction
12
+ ---
13
+
14
+ # PathAGG
15
+
16
+ Multi-teacher pathology foundation students distilled with **CRADIOv4**-style aggregation from [Virchow2](https://huggingface.co/paige-ai/Virchow2), [UNI2-h](https://huggingface.co/MahmoodLab/UNI2-h), and [H1](https://huggingface.co/bioptimus/H-optimus-1) (H-optimus-1).
17
+
18
+ Trained on **~66M** TCGA + HISTAI patches (~**1000 GPU-hours** / student on 8× H100).
19
+ Code & EVA dumps: [Luoxd1996/PathAGG](https://github.com/Luoxd1996/PathAGG).
20
+
21
+ | Variant | `subfolder` | Embed | Official EVA avg (9 tasks) |
22
+ |---------|-------------|------:|---------------------------:|
23
+ | ViT-B/14 | `vitb` | 768 | **79.57** |
24
+ | ViT-S/14 | `vits` | 384 | **77.93** |
25
+
26
+ **License: [CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/) — non-commercial / research only.**
27
+
28
+ ## Load (recommended)
29
+
30
+ ```bash
31
+ pip install torch timm transformers torchvision Pillow
32
+ ```
33
+
34
+ ```python
35
+ import torch
36
+ from PIL import Image
37
+ from torchvision import transforms
38
+ from transformers import AutoModel
39
+
40
+ preprocess = transforms.Compose([
41
+ transforms.Resize(224, interpolation=transforms.InterpolationMode.BICUBIC),
42
+ transforms.CenterCrop(224),
43
+ transforms.ToTensor(),
44
+ transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225)),
45
+ ])
46
+
47
+ model = AutoModel.from_pretrained(
48
+ "luoxd96/PathAGG",
49
+ subfolder="vitb", # or "vits"
50
+ trust_remote_code=True,
51
+ ).eval().cuda()
52
+
53
+ img = Image.open("patch.png").convert("RGB")
54
+ x = preprocess(img).unsqueeze(0).cuda()
55
+
56
+ with torch.inference_mode():
57
+ cls = model(x) # [1, 768] or [1, 384]
58
+ cls, patch = model(x, return_patch=True) # patch: [1, 256, D]
59
+ ```
60
+
61
+ ## EVA results (%)
62
+
63
+ | Task | Split | ViT-S | ViT-B |
64
+ |------|-------|------:|------:|
65
+ | BreakHis | val | 74.36 | 84.62 |
66
+ | CRC | val | 95.93 | 96.41 |
67
+ | Gleason | val | 78.72 | 77.33 |
68
+ | MHIST | val | 81.85 | 82.20 |
69
+ | PCam | test | 93.88 | 93.75 |
70
+ | Cam16Small | test | 83.59 | 85.03 |
71
+ | PANDASmall | test | 66.28 | 67.93 |
72
+ | CoNSeP | val | 63.58 | 64.04 |
73
+ | MoNuSAC | val | 63.22 | 64.82 |
74
+ | **Official Avg** | | **77.93** | **79.57** |
modeling_pathagg.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PathAGG Hugging Face loaders (trust_remote_code).
2
+
3
+ Usage:
4
+ from transformers import AutoModel
5
+ model = AutoModel.from_pretrained("luoxd96/PathAGG", subfolder="vitb", trust_remote_code=True)
6
+ cls = model(images) # [B, D]
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ import torch
15
+ import torch.nn as nn
16
+ from transformers import PretrainedConfig, PreTrainedModel
17
+ from transformers.utils import cached_file
18
+
19
+
20
+ class PathAGGConfig(PretrainedConfig):
21
+ model_type = "pathagg"
22
+
23
+ def __init__(
24
+ self,
25
+ variant: str = "vitb",
26
+ img_size: int = 224,
27
+ patch_size: int = 14,
28
+ embed_dim: int = 768,
29
+ depth: int = 12,
30
+ num_heads: int = 12,
31
+ num_register_tokens: int = 4,
32
+ mlp_ratio: float = 4.0,
33
+ qkv_bias: bool = True,
34
+ init_values: float | None = None,
35
+ no_embed_class: bool = False,
36
+ return_patch: bool = False,
37
+ **kwargs: Any,
38
+ ) -> None:
39
+ super().__init__(**kwargs)
40
+ self.variant = variant
41
+ self.img_size = img_size
42
+ self.patch_size = patch_size
43
+ self.embed_dim = embed_dim
44
+ self.depth = depth
45
+ self.num_heads = num_heads
46
+ self.num_register_tokens = num_register_tokens
47
+ self.mlp_ratio = mlp_ratio
48
+ self.qkv_bias = qkv_bias
49
+ self.init_values = init_values
50
+ self.no_embed_class = no_embed_class
51
+ self.return_patch = return_patch
52
+
53
+
54
+ def _build_backbone(config: PathAGGConfig) -> nn.Module:
55
+ from timm.models.vision_transformer import VisionTransformer
56
+
57
+ kwargs: dict[str, Any] = dict(
58
+ img_size=config.img_size,
59
+ patch_size=config.patch_size,
60
+ in_chans=3,
61
+ num_classes=0,
62
+ global_pool="",
63
+ embed_dim=config.embed_dim,
64
+ depth=config.depth,
65
+ num_heads=config.num_heads,
66
+ mlp_ratio=config.mlp_ratio,
67
+ qkv_bias=config.qkv_bias,
68
+ reg_tokens=config.num_register_tokens,
69
+ )
70
+ if config.init_values is not None:
71
+ kwargs["init_values"] = config.init_values
72
+ if config.no_embed_class:
73
+ kwargs["no_embed_class"] = True
74
+ return VisionTransformer(**kwargs)
75
+
76
+
77
+ class PathAGGModel(PreTrainedModel):
78
+ """Pathology multi-teacher KD student. Forward returns CLS ``[B, D]`` by default."""
79
+
80
+ config_class = PathAGGConfig
81
+ base_model_prefix = "pathagg"
82
+ _no_split_modules = ["Block"]
83
+
84
+ def __init__(self, config: PathAGGConfig) -> None:
85
+ super().__init__(config)
86
+ self.return_patch = bool(config.return_patch)
87
+ self.model = _build_backbone(config)
88
+ # Do not call post_init() random re-init; weights come from checkpoint.
89
+
90
+ @property
91
+ def embed_dim(self) -> int:
92
+ return int(self.model.embed_dim)
93
+
94
+ @property
95
+ def num_register_tokens(self) -> int:
96
+ return int(getattr(self.model, "reg_tokens", getattr(self.model, "num_register_tokens", 4)))
97
+
98
+ def forward_features(self, x: torch.Tensor) -> torch.Tensor:
99
+ return self.model.forward_features(x)
100
+
101
+ def forward(
102
+ self,
103
+ pixel_values: torch.Tensor | None = None,
104
+ x: torch.Tensor | None = None,
105
+ return_patch: bool | None = None,
106
+ **kwargs: Any,
107
+ ):
108
+ if pixel_values is None and x is None:
109
+ raise ValueError("provide pixel_values=... or x=...")
110
+ images = pixel_values if pixel_values is not None else x
111
+ tokens = self.forward_features(images)
112
+ cls = tokens[:, 0]
113
+ use_patch = self.return_patch if return_patch is None else bool(return_patch)
114
+ if not use_patch:
115
+ return cls
116
+ num_prefix = int(getattr(self.model, "num_prefix_tokens", 1 + self.num_register_tokens))
117
+ patch = tokens[:, num_prefix:]
118
+ return cls, patch
119
+
120
+ @classmethod
121
+ def from_pretrained(cls, pretrained_model_name_or_path, *model_args, **kwargs):
122
+ """Load config + weights without transformers gamma↔weight remapping (LayerScale)."""
123
+ subfolder = kwargs.pop("subfolder", "")
124
+ local_files_only = kwargs.pop("local_files_only", False)
125
+ revision = kwargs.pop("revision", None)
126
+ cache_dir = kwargs.pop("cache_dir", None)
127
+ token = kwargs.pop("token", None)
128
+ if token is None:
129
+ token = kwargs.pop("use_auth_token", None)
130
+ kwargs.pop("trust_remote_code", None)
131
+ kwargs.pop("torch_dtype", None)
132
+ kwargs.pop("device_map", None)
133
+ kwargs.pop("low_cpu_mem_usage", None)
134
+ return_patch = kwargs.pop("return_patch", None)
135
+ config = kwargs.pop("config", None)
136
+
137
+ if config is None:
138
+ config = PathAGGConfig.from_pretrained(
139
+ pretrained_model_name_or_path,
140
+ subfolder=subfolder,
141
+ local_files_only=local_files_only,
142
+ revision=revision,
143
+ cache_dir=cache_dir,
144
+ token=token,
145
+ **kwargs,
146
+ )
147
+ if return_patch is not None:
148
+ config.return_patch = bool(return_patch)
149
+
150
+ model = cls(config)
151
+
152
+ weight_file = cached_file(
153
+ pretrained_model_name_or_path,
154
+ "pytorch_model.bin",
155
+ subfolder=subfolder,
156
+ local_files_only=local_files_only,
157
+ revision=revision,
158
+ cache_dir=cache_dir,
159
+ token=token,
160
+ )
161
+ if weight_file is None:
162
+ raise FileNotFoundError(
163
+ f"pytorch_model.bin not found under {pretrained_model_name_or_path!r} (subfolder={subfolder!r})"
164
+ )
165
+ try:
166
+ state = torch.load(weight_file, map_location="cpu", weights_only=True)
167
+ except TypeError:
168
+ state = torch.load(weight_file, map_location="cpu")
169
+
170
+ # transformers may have renamed LayerScale gamma→weight in some pipelines; normalize back.
171
+ fixed = {}
172
+ for k, v in state.items():
173
+ if k.endswith(".ls1.weight") or k.endswith(".ls2.weight"):
174
+ fixed[k[: -len(".weight")] + ".gamma"] = v
175
+ else:
176
+ fixed[k] = v
177
+ missing, unexpected = model.load_state_dict(fixed, strict=True)
178
+ if missing or unexpected:
179
+ raise RuntimeError(f"load failed: missing={missing}, unexpected={unexpected}")
180
+ model.eval()
181
+ return model
182
+
183
+
184
+ def get_preprocess(img_size: int = 224):
185
+ """ImageNet normalize preprocess (PIL → tensor)."""
186
+ from torchvision import transforms
187
+
188
+ return transforms.Compose(
189
+ [
190
+ transforms.Resize(img_size, interpolation=transforms.InterpolationMode.BICUBIC),
191
+ transforms.CenterCrop(img_size),
192
+ transforms.ToTensor(),
193
+ transforms.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),
194
+ ]
195
+ )
196
+
197
+
198
+ __all__ = [
199
+ "PathAGGConfig",
200
+ "PathAGGModel",
201
+ "get_preprocess",
202
+ ]
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ torch>=2.0
2
+ timm>=0.9.12
3
+ transformers>=4.40.0
4
+ torchvision>=0.15
5
+ Pillow>=9.0
6
+ huggingface_hub>=0.23.0
vitb/config.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": ["PathAGGModel"],
3
+ "auto_map": {
4
+ "AutoConfig": "modeling_pathagg.PathAGGConfig",
5
+ "AutoModel": "modeling_pathagg.PathAGGModel"
6
+ },
7
+ "model_type": "pathagg",
8
+ "variant": "vitb",
9
+ "img_size": 224,
10
+ "patch_size": 14,
11
+ "embed_dim": 768,
12
+ "depth": 12,
13
+ "num_heads": 12,
14
+ "num_register_tokens": 4,
15
+ "mlp_ratio": 4.0,
16
+ "qkv_bias": true,
17
+ "init_values": null,
18
+ "no_embed_class": false,
19
+ "return_patch": false,
20
+ "torch_dtype": "float32",
21
+ "transformers_version": "4.40.0"
22
+ }
vitb/modeling_pathagg.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PathAGG Hugging Face loaders (trust_remote_code).
2
+
3
+ Usage:
4
+ from transformers import AutoModel
5
+ model = AutoModel.from_pretrained("luoxd96/PathAGG", subfolder="vitb", trust_remote_code=True)
6
+ cls = model(images) # [B, D]
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ import torch
15
+ import torch.nn as nn
16
+ from transformers import PretrainedConfig, PreTrainedModel
17
+ from transformers.utils import cached_file
18
+
19
+
20
+ class PathAGGConfig(PretrainedConfig):
21
+ model_type = "pathagg"
22
+
23
+ def __init__(
24
+ self,
25
+ variant: str = "vitb",
26
+ img_size: int = 224,
27
+ patch_size: int = 14,
28
+ embed_dim: int = 768,
29
+ depth: int = 12,
30
+ num_heads: int = 12,
31
+ num_register_tokens: int = 4,
32
+ mlp_ratio: float = 4.0,
33
+ qkv_bias: bool = True,
34
+ init_values: float | None = None,
35
+ no_embed_class: bool = False,
36
+ return_patch: bool = False,
37
+ **kwargs: Any,
38
+ ) -> None:
39
+ super().__init__(**kwargs)
40
+ self.variant = variant
41
+ self.img_size = img_size
42
+ self.patch_size = patch_size
43
+ self.embed_dim = embed_dim
44
+ self.depth = depth
45
+ self.num_heads = num_heads
46
+ self.num_register_tokens = num_register_tokens
47
+ self.mlp_ratio = mlp_ratio
48
+ self.qkv_bias = qkv_bias
49
+ self.init_values = init_values
50
+ self.no_embed_class = no_embed_class
51
+ self.return_patch = return_patch
52
+
53
+
54
+ def _build_backbone(config: PathAGGConfig) -> nn.Module:
55
+ from timm.models.vision_transformer import VisionTransformer
56
+
57
+ kwargs: dict[str, Any] = dict(
58
+ img_size=config.img_size,
59
+ patch_size=config.patch_size,
60
+ in_chans=3,
61
+ num_classes=0,
62
+ global_pool="",
63
+ embed_dim=config.embed_dim,
64
+ depth=config.depth,
65
+ num_heads=config.num_heads,
66
+ mlp_ratio=config.mlp_ratio,
67
+ qkv_bias=config.qkv_bias,
68
+ reg_tokens=config.num_register_tokens,
69
+ )
70
+ if config.init_values is not None:
71
+ kwargs["init_values"] = config.init_values
72
+ if config.no_embed_class:
73
+ kwargs["no_embed_class"] = True
74
+ return VisionTransformer(**kwargs)
75
+
76
+
77
+ class PathAGGModel(PreTrainedModel):
78
+ """Pathology multi-teacher KD student. Forward returns CLS ``[B, D]`` by default."""
79
+
80
+ config_class = PathAGGConfig
81
+ base_model_prefix = "pathagg"
82
+ _no_split_modules = ["Block"]
83
+
84
+ def __init__(self, config: PathAGGConfig) -> None:
85
+ super().__init__(config)
86
+ self.return_patch = bool(config.return_patch)
87
+ self.model = _build_backbone(config)
88
+ # Do not call post_init() random re-init; weights come from checkpoint.
89
+
90
+ @property
91
+ def embed_dim(self) -> int:
92
+ return int(self.model.embed_dim)
93
+
94
+ @property
95
+ def num_register_tokens(self) -> int:
96
+ return int(getattr(self.model, "reg_tokens", getattr(self.model, "num_register_tokens", 4)))
97
+
98
+ def forward_features(self, x: torch.Tensor) -> torch.Tensor:
99
+ return self.model.forward_features(x)
100
+
101
+ def forward(
102
+ self,
103
+ pixel_values: torch.Tensor | None = None,
104
+ x: torch.Tensor | None = None,
105
+ return_patch: bool | None = None,
106
+ **kwargs: Any,
107
+ ):
108
+ if pixel_values is None and x is None:
109
+ raise ValueError("provide pixel_values=... or x=...")
110
+ images = pixel_values if pixel_values is not None else x
111
+ tokens = self.forward_features(images)
112
+ cls = tokens[:, 0]
113
+ use_patch = self.return_patch if return_patch is None else bool(return_patch)
114
+ if not use_patch:
115
+ return cls
116
+ num_prefix = int(getattr(self.model, "num_prefix_tokens", 1 + self.num_register_tokens))
117
+ patch = tokens[:, num_prefix:]
118
+ return cls, patch
119
+
120
+ @classmethod
121
+ def from_pretrained(cls, pretrained_model_name_or_path, *model_args, **kwargs):
122
+ """Load config + weights without transformers gamma↔weight remapping (LayerScale)."""
123
+ subfolder = kwargs.pop("subfolder", "")
124
+ local_files_only = kwargs.pop("local_files_only", False)
125
+ revision = kwargs.pop("revision", None)
126
+ cache_dir = kwargs.pop("cache_dir", None)
127
+ token = kwargs.pop("token", None)
128
+ if token is None:
129
+ token = kwargs.pop("use_auth_token", None)
130
+ kwargs.pop("trust_remote_code", None)
131
+ kwargs.pop("torch_dtype", None)
132
+ kwargs.pop("device_map", None)
133
+ kwargs.pop("low_cpu_mem_usage", None)
134
+ return_patch = kwargs.pop("return_patch", None)
135
+ config = kwargs.pop("config", None)
136
+
137
+ if config is None:
138
+ config = PathAGGConfig.from_pretrained(
139
+ pretrained_model_name_or_path,
140
+ subfolder=subfolder,
141
+ local_files_only=local_files_only,
142
+ revision=revision,
143
+ cache_dir=cache_dir,
144
+ token=token,
145
+ **kwargs,
146
+ )
147
+ if return_patch is not None:
148
+ config.return_patch = bool(return_patch)
149
+
150
+ model = cls(config)
151
+
152
+ weight_file = cached_file(
153
+ pretrained_model_name_or_path,
154
+ "pytorch_model.bin",
155
+ subfolder=subfolder,
156
+ local_files_only=local_files_only,
157
+ revision=revision,
158
+ cache_dir=cache_dir,
159
+ token=token,
160
+ )
161
+ if weight_file is None:
162
+ raise FileNotFoundError(
163
+ f"pytorch_model.bin not found under {pretrained_model_name_or_path!r} (subfolder={subfolder!r})"
164
+ )
165
+ try:
166
+ state = torch.load(weight_file, map_location="cpu", weights_only=True)
167
+ except TypeError:
168
+ state = torch.load(weight_file, map_location="cpu")
169
+
170
+ # transformers may have renamed LayerScale gamma→weight in some pipelines; normalize back.
171
+ fixed = {}
172
+ for k, v in state.items():
173
+ if k.endswith(".ls1.weight") or k.endswith(".ls2.weight"):
174
+ fixed[k[: -len(".weight")] + ".gamma"] = v
175
+ else:
176
+ fixed[k] = v
177
+ missing, unexpected = model.load_state_dict(fixed, strict=True)
178
+ if missing or unexpected:
179
+ raise RuntimeError(f"load failed: missing={missing}, unexpected={unexpected}")
180
+ model.eval()
181
+ return model
182
+
183
+
184
+ def get_preprocess(img_size: int = 224):
185
+ """ImageNet normalize preprocess (PIL → tensor)."""
186
+ from torchvision import transforms
187
+
188
+ return transforms.Compose(
189
+ [
190
+ transforms.Resize(img_size, interpolation=transforms.InterpolationMode.BICUBIC),
191
+ transforms.CenterCrop(img_size),
192
+ transforms.ToTensor(),
193
+ transforms.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),
194
+ ]
195
+ )
196
+
197
+
198
+ __all__ = [
199
+ "PathAGGConfig",
200
+ "PathAGGModel",
201
+ "get_preprocess",
202
+ ]
vitb/pytorch_model.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:74c871a8f8756b7b0470b37001768a17cc1f68753081a8134c45354a76c752d7
3
+ size 342914855
vits/config.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": ["PathAGGModel"],
3
+ "auto_map": {
4
+ "AutoConfig": "modeling_pathagg.PathAGGConfig",
5
+ "AutoModel": "modeling_pathagg.PathAGGModel"
6
+ },
7
+ "model_type": "pathagg",
8
+ "variant": "vits",
9
+ "img_size": 224,
10
+ "patch_size": 14,
11
+ "embed_dim": 384,
12
+ "depth": 12,
13
+ "num_heads": 6,
14
+ "num_register_tokens": 4,
15
+ "mlp_ratio": 4.0,
16
+ "qkv_bias": true,
17
+ "init_values": 1e-5,
18
+ "no_embed_class": true,
19
+ "return_patch": false,
20
+ "torch_dtype": "float32",
21
+ "transformers_version": "4.40.0"
22
+ }
vits/modeling_pathagg.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PathAGG Hugging Face loaders (trust_remote_code).
2
+
3
+ Usage:
4
+ from transformers import AutoModel
5
+ model = AutoModel.from_pretrained("luoxd96/PathAGG", subfolder="vitb", trust_remote_code=True)
6
+ cls = model(images) # [B, D]
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ import torch
15
+ import torch.nn as nn
16
+ from transformers import PretrainedConfig, PreTrainedModel
17
+ from transformers.utils import cached_file
18
+
19
+
20
+ class PathAGGConfig(PretrainedConfig):
21
+ model_type = "pathagg"
22
+
23
+ def __init__(
24
+ self,
25
+ variant: str = "vitb",
26
+ img_size: int = 224,
27
+ patch_size: int = 14,
28
+ embed_dim: int = 768,
29
+ depth: int = 12,
30
+ num_heads: int = 12,
31
+ num_register_tokens: int = 4,
32
+ mlp_ratio: float = 4.0,
33
+ qkv_bias: bool = True,
34
+ init_values: float | None = None,
35
+ no_embed_class: bool = False,
36
+ return_patch: bool = False,
37
+ **kwargs: Any,
38
+ ) -> None:
39
+ super().__init__(**kwargs)
40
+ self.variant = variant
41
+ self.img_size = img_size
42
+ self.patch_size = patch_size
43
+ self.embed_dim = embed_dim
44
+ self.depth = depth
45
+ self.num_heads = num_heads
46
+ self.num_register_tokens = num_register_tokens
47
+ self.mlp_ratio = mlp_ratio
48
+ self.qkv_bias = qkv_bias
49
+ self.init_values = init_values
50
+ self.no_embed_class = no_embed_class
51
+ self.return_patch = return_patch
52
+
53
+
54
+ def _build_backbone(config: PathAGGConfig) -> nn.Module:
55
+ from timm.models.vision_transformer import VisionTransformer
56
+
57
+ kwargs: dict[str, Any] = dict(
58
+ img_size=config.img_size,
59
+ patch_size=config.patch_size,
60
+ in_chans=3,
61
+ num_classes=0,
62
+ global_pool="",
63
+ embed_dim=config.embed_dim,
64
+ depth=config.depth,
65
+ num_heads=config.num_heads,
66
+ mlp_ratio=config.mlp_ratio,
67
+ qkv_bias=config.qkv_bias,
68
+ reg_tokens=config.num_register_tokens,
69
+ )
70
+ if config.init_values is not None:
71
+ kwargs["init_values"] = config.init_values
72
+ if config.no_embed_class:
73
+ kwargs["no_embed_class"] = True
74
+ return VisionTransformer(**kwargs)
75
+
76
+
77
+ class PathAGGModel(PreTrainedModel):
78
+ """Pathology multi-teacher KD student. Forward returns CLS ``[B, D]`` by default."""
79
+
80
+ config_class = PathAGGConfig
81
+ base_model_prefix = "pathagg"
82
+ _no_split_modules = ["Block"]
83
+
84
+ def __init__(self, config: PathAGGConfig) -> None:
85
+ super().__init__(config)
86
+ self.return_patch = bool(config.return_patch)
87
+ self.model = _build_backbone(config)
88
+ # Do not call post_init() random re-init; weights come from checkpoint.
89
+
90
+ @property
91
+ def embed_dim(self) -> int:
92
+ return int(self.model.embed_dim)
93
+
94
+ @property
95
+ def num_register_tokens(self) -> int:
96
+ return int(getattr(self.model, "reg_tokens", getattr(self.model, "num_register_tokens", 4)))
97
+
98
+ def forward_features(self, x: torch.Tensor) -> torch.Tensor:
99
+ return self.model.forward_features(x)
100
+
101
+ def forward(
102
+ self,
103
+ pixel_values: torch.Tensor | None = None,
104
+ x: torch.Tensor | None = None,
105
+ return_patch: bool | None = None,
106
+ **kwargs: Any,
107
+ ):
108
+ if pixel_values is None and x is None:
109
+ raise ValueError("provide pixel_values=... or x=...")
110
+ images = pixel_values if pixel_values is not None else x
111
+ tokens = self.forward_features(images)
112
+ cls = tokens[:, 0]
113
+ use_patch = self.return_patch if return_patch is None else bool(return_patch)
114
+ if not use_patch:
115
+ return cls
116
+ num_prefix = int(getattr(self.model, "num_prefix_tokens", 1 + self.num_register_tokens))
117
+ patch = tokens[:, num_prefix:]
118
+ return cls, patch
119
+
120
+ @classmethod
121
+ def from_pretrained(cls, pretrained_model_name_or_path, *model_args, **kwargs):
122
+ """Load config + weights without transformers gamma↔weight remapping (LayerScale)."""
123
+ subfolder = kwargs.pop("subfolder", "")
124
+ local_files_only = kwargs.pop("local_files_only", False)
125
+ revision = kwargs.pop("revision", None)
126
+ cache_dir = kwargs.pop("cache_dir", None)
127
+ token = kwargs.pop("token", None)
128
+ if token is None:
129
+ token = kwargs.pop("use_auth_token", None)
130
+ kwargs.pop("trust_remote_code", None)
131
+ kwargs.pop("torch_dtype", None)
132
+ kwargs.pop("device_map", None)
133
+ kwargs.pop("low_cpu_mem_usage", None)
134
+ return_patch = kwargs.pop("return_patch", None)
135
+ config = kwargs.pop("config", None)
136
+
137
+ if config is None:
138
+ config = PathAGGConfig.from_pretrained(
139
+ pretrained_model_name_or_path,
140
+ subfolder=subfolder,
141
+ local_files_only=local_files_only,
142
+ revision=revision,
143
+ cache_dir=cache_dir,
144
+ token=token,
145
+ **kwargs,
146
+ )
147
+ if return_patch is not None:
148
+ config.return_patch = bool(return_patch)
149
+
150
+ model = cls(config)
151
+
152
+ weight_file = cached_file(
153
+ pretrained_model_name_or_path,
154
+ "pytorch_model.bin",
155
+ subfolder=subfolder,
156
+ local_files_only=local_files_only,
157
+ revision=revision,
158
+ cache_dir=cache_dir,
159
+ token=token,
160
+ )
161
+ if weight_file is None:
162
+ raise FileNotFoundError(
163
+ f"pytorch_model.bin not found under {pretrained_model_name_or_path!r} (subfolder={subfolder!r})"
164
+ )
165
+ try:
166
+ state = torch.load(weight_file, map_location="cpu", weights_only=True)
167
+ except TypeError:
168
+ state = torch.load(weight_file, map_location="cpu")
169
+
170
+ # transformers may have renamed LayerScale gamma→weight in some pipelines; normalize back.
171
+ fixed = {}
172
+ for k, v in state.items():
173
+ if k.endswith(".ls1.weight") or k.endswith(".ls2.weight"):
174
+ fixed[k[: -len(".weight")] + ".gamma"] = v
175
+ else:
176
+ fixed[k] = v
177
+ missing, unexpected = model.load_state_dict(fixed, strict=True)
178
+ if missing or unexpected:
179
+ raise RuntimeError(f"load failed: missing={missing}, unexpected={unexpected}")
180
+ model.eval()
181
+ return model
182
+
183
+
184
+ def get_preprocess(img_size: int = 224):
185
+ """ImageNet normalize preprocess (PIL → tensor)."""
186
+ from torchvision import transforms
187
+
188
+ return transforms.Compose(
189
+ [
190
+ transforms.Resize(img_size, interpolation=transforms.InterpolationMode.BICUBIC),
191
+ transforms.CenterCrop(img_size),
192
+ transforms.ToTensor(),
193
+ transforms.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),
194
+ ]
195
+ )
196
+
197
+
198
+ __all__ = [
199
+ "PathAGGConfig",
200
+ "PathAGGModel",
201
+ "get_preprocess",
202
+ ]
vits/pytorch_model.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5b54b6ea8748aa8b97234fb7d0e53aecddd6de01a958946f20e0f273f514a84a
3
+ size 86592207