shadax 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- shadax/__init__.py +50 -0
- shadax/config.py +294 -0
- shadax/core.py +34 -0
- shadax/encoder.py +301 -0
- shadax/heads.py +285 -0
- shadax/model.py +606 -0
- shadax/modules.py +316 -0
- shadax/network.py +204 -0
- shadax/py.typed +0 -0
- shadax/ssl.py +325 -0
- shadax/training.py +346 -0
- shadax-0.2.0.dist-info/METADATA +508 -0
- shadax-0.2.0.dist-info/RECORD +16 -0
- shadax-0.2.0.dist-info/WHEEL +5 -0
- shadax-0.2.0.dist-info/licenses/LICENSE +21 -0
- shadax-0.2.0.dist-info/top_level.txt +1 -0
shadax/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SHADAX - Self-supervised Hierarchical Adaptive Hybrid Algorithm.
|
|
3
|
+
|
|
4
|
+
A production-ready Python implementation of the SHADA algorithm: a four-stage
|
|
5
|
+
hierarchical encoder shared across the image and text modalities, four task
|
|
6
|
+
heads (classification, detection, segmentation, language modeling), two
|
|
7
|
+
self-supervised objectives and a four-phase training pipeline, all behind a
|
|
8
|
+
familiar sklearn-style estimator.
|
|
9
|
+
|
|
10
|
+
Public API:
|
|
11
|
+
SHADA: High-level sklearn-style estimator (fit/predict/score).
|
|
12
|
+
SHADAConfig: Configuration dataclass (the model-shape contract).
|
|
13
|
+
create_config: Tier-aware configuration factory.
|
|
14
|
+
SHADANet: The unified encoder + head + SSL ``nn.Module``.
|
|
15
|
+
HierarchicalEncoder: The shared multi-modal backbone.
|
|
16
|
+
ModelTier / TaskType / TrainingPhase / Modality: Controlled vocabularies.
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
>>> from shadax import SHADA
|
|
20
|
+
>>> model = SHADA(tier="base", num_classes=10)
|
|
21
|
+
>>> model.fit(X_train, y_train) # doctest: +SKIP
|
|
22
|
+
>>> predictions = model.predict(X_test) # doctest: +SKIP
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
__version__ = "0.2.0"
|
|
26
|
+
__author__ = "Omar"
|
|
27
|
+
|
|
28
|
+
from .config import (
|
|
29
|
+
Modality,
|
|
30
|
+
ModelTier,
|
|
31
|
+
SHADAConfig,
|
|
32
|
+
TaskType,
|
|
33
|
+
TrainingPhase,
|
|
34
|
+
create_config,
|
|
35
|
+
)
|
|
36
|
+
from .encoder import HierarchicalEncoder
|
|
37
|
+
from .model import SHADA
|
|
38
|
+
from .network import SHADANet
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"SHADA",
|
|
42
|
+
"SHADAConfig",
|
|
43
|
+
"create_config",
|
|
44
|
+
"ModelTier",
|
|
45
|
+
"TaskType",
|
|
46
|
+
"TrainingPhase",
|
|
47
|
+
"Modality",
|
|
48
|
+
"HierarchicalEncoder",
|
|
49
|
+
"SHADANet",
|
|
50
|
+
]
|
shadax/config.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SHADA configuration layer.
|
|
3
|
+
|
|
4
|
+
This module is the *contract* that every other module in :mod:`shadax`
|
|
5
|
+
depends on. It is intentionally free of any heavy dependency (no PyTorch),
|
|
6
|
+
so it can be imported cheaply and reasoned about in isolation.
|
|
7
|
+
|
|
8
|
+
It defines:
|
|
9
|
+
|
|
10
|
+
* :class:`ModelTier`, :class:`TaskType`, :class:`TrainingPhase`,
|
|
11
|
+
:class:`Modality` -- the controlled vocabularies used across the library.
|
|
12
|
+
* :class:`SHADAConfig` -- the single source of truth for the model shape.
|
|
13
|
+
* :data:`_TIER_CONFIGS` -- the per-tier architectural presets.
|
|
14
|
+
* :func:`create_config` -- the factory used by the high level API.
|
|
15
|
+
|
|
16
|
+
Spatial contract (images)
|
|
17
|
+
--------------------------
|
|
18
|
+
The hierarchical encoder reduces the spatial resolution by a fixed total
|
|
19
|
+
factor of ``32`` (a ``/4`` convolutional stem followed by three ``/2`` stage
|
|
20
|
+
downsamples). Input height and width must therefore be divisible by ``32``;
|
|
21
|
+
:func:`SHADAConfig.validate` and the encoder both enforce this.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from enum import Enum
|
|
28
|
+
from typing import Dict, List
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"ModelTier",
|
|
32
|
+
"TaskType",
|
|
33
|
+
"TrainingPhase",
|
|
34
|
+
"Modality",
|
|
35
|
+
"SHADAConfig",
|
|
36
|
+
"create_config",
|
|
37
|
+
"STEM_REDUCTION",
|
|
38
|
+
"STAGE_REDUCTION",
|
|
39
|
+
"TOTAL_REDUCTION",
|
|
40
|
+
"NUM_STAGES",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
# --------------------------------------------------------------------------- #
|
|
44
|
+
# Architectural constants (the spatial contract).
|
|
45
|
+
# --------------------------------------------------------------------------- #
|
|
46
|
+
NUM_STAGES: int = 4
|
|
47
|
+
STEM_REDUCTION: int = 4 # convolutional stem downsamples H, W by 4.
|
|
48
|
+
STAGE_REDUCTION: int = 2 # each inter-stage downsample halves H, W.
|
|
49
|
+
# stem (/4) + 3 downsamples (/2 each) -> /32 total.
|
|
50
|
+
TOTAL_REDUCTION: int = STEM_REDUCTION * (STAGE_REDUCTION ** (NUM_STAGES - 1))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ModelTier(str, Enum):
|
|
54
|
+
"""Model size tiers, ordered from smallest to largest."""
|
|
55
|
+
|
|
56
|
+
NANO = "nano"
|
|
57
|
+
BASE = "base"
|
|
58
|
+
LARGE = "large"
|
|
59
|
+
XL = "xl"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TaskType(str, Enum):
|
|
63
|
+
"""Supported downstream tasks.
|
|
64
|
+
|
|
65
|
+
Each value maps to a concrete task head in :mod:`shadax.heads` and to a
|
|
66
|
+
loss in :mod:`shadax.training`.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
CLASSIFICATION = "classification"
|
|
70
|
+
DETECTION = "detection"
|
|
71
|
+
SEGMENTATION = "segmentation"
|
|
72
|
+
LANGUAGE_MODEL = "lm"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class TrainingPhase(str, Enum):
|
|
76
|
+
"""The four phases of the SHADA training pipeline.
|
|
77
|
+
|
|
78
|
+
* ``PRETRAIN`` -- self-supervised pretraining (no labels required).
|
|
79
|
+
* ``MULTITASK`` -- joint optimisation of the supervised objective and the
|
|
80
|
+
self-supervised objective.
|
|
81
|
+
* ``FINETUNE`` -- purely supervised optimisation of the target task.
|
|
82
|
+
* ``DEPLOY`` -- inference / frozen-weights mode (no optimisation).
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
PRETRAIN = "pretrain"
|
|
86
|
+
MULTITASK = "multitask"
|
|
87
|
+
FINETUNE = "finetune"
|
|
88
|
+
DEPLOY = "deploy"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Modality(str, Enum):
|
|
92
|
+
"""Input modalities understood by the encoder."""
|
|
93
|
+
|
|
94
|
+
IMAGE = "image"
|
|
95
|
+
TEXT = "text"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class SHADAConfig:
|
|
100
|
+
"""Single source of truth for the SHADA architecture.
|
|
101
|
+
|
|
102
|
+
Attributes:
|
|
103
|
+
tier: Model size tier name (``nano``/``base``/``large``/``xl``).
|
|
104
|
+
encoder_dims: Channel width of each of the four encoder stages.
|
|
105
|
+
encoder_depths: Number of transformer blocks in each stage.
|
|
106
|
+
num_heads: Number of attention heads in each stage.
|
|
107
|
+
mlp_ratio: Hidden expansion ratio of the feed-forward sub-layer.
|
|
108
|
+
dropout: Dropout probability used throughout the network.
|
|
109
|
+
in_channels: Number of input image channels (e.g. 3 for RGB).
|
|
110
|
+
image_size: Reference image size used for defaults; the encoder is
|
|
111
|
+
resolution-adaptive, so this is only a hint (any H, W divisible by
|
|
112
|
+
:data:`TOTAL_REDUCTION` works).
|
|
113
|
+
max_seq_len: Maximum text sequence length supported.
|
|
114
|
+
vocab_size: Vocabulary size for the text modality / language model.
|
|
115
|
+
task: Primary :class:`TaskType` value (stored as ``str``).
|
|
116
|
+
num_classes: Number of output classes. Interpreted per task:
|
|
117
|
+
classification -> number of labels, detection -> object
|
|
118
|
+
categories, segmentation -> per-pixel classes. Ignored for ``lm``
|
|
119
|
+
(which predicts over ``vocab_size``).
|
|
120
|
+
mask_ratio: Fraction of image patches masked during self-supervised
|
|
121
|
+
pretraining (masked image modeling).
|
|
122
|
+
text_mask_ratio: Fraction of text tokens masked during masked language
|
|
123
|
+
modeling.
|
|
124
|
+
decoder_dim: Width of the lightweight self-supervised decoder.
|
|
125
|
+
decoder_depth: Number of blocks in the self-supervised decoder.
|
|
126
|
+
pad_token_id: Token id treated as padding / ignored by the LM loss.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
tier: str = "base"
|
|
130
|
+
encoder_dims: List[int] = field(default_factory=lambda: [128, 256, 512, 1024])
|
|
131
|
+
encoder_depths: List[int] = field(default_factory=lambda: [3, 4, 6, 3])
|
|
132
|
+
num_heads: List[int] = field(default_factory=lambda: [4, 8, 16, 32])
|
|
133
|
+
mlp_ratio: float = 4.0
|
|
134
|
+
dropout: float = 0.1
|
|
135
|
+
in_channels: int = 3
|
|
136
|
+
image_size: int = 224
|
|
137
|
+
max_seq_len: int = 1024
|
|
138
|
+
vocab_size: int = 50257
|
|
139
|
+
task: str = "classification"
|
|
140
|
+
num_classes: int = 1000
|
|
141
|
+
mask_ratio: float = 0.75
|
|
142
|
+
text_mask_ratio: float = 0.15
|
|
143
|
+
decoder_dim: int = 256
|
|
144
|
+
decoder_depth: int = 2
|
|
145
|
+
pad_token_id: int = 0
|
|
146
|
+
|
|
147
|
+
def __post_init__(self) -> None:
|
|
148
|
+
self.validate()
|
|
149
|
+
|
|
150
|
+
# ----------------------------------------------------------------- #
|
|
151
|
+
# Convenience accessors used by the rest of the library.
|
|
152
|
+
# ----------------------------------------------------------------- #
|
|
153
|
+
@property
|
|
154
|
+
def embed_dim(self) -> int:
|
|
155
|
+
"""Width of the first (stem) stage."""
|
|
156
|
+
return self.encoder_dims[0]
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def final_dim(self) -> int:
|
|
160
|
+
"""Width of the last encoder stage (the global feature dimension)."""
|
|
161
|
+
return self.encoder_dims[-1]
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def num_stages(self) -> int:
|
|
165
|
+
return len(self.encoder_dims)
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def task_type(self) -> TaskType:
|
|
169
|
+
"""The :class:`TaskType` enum corresponding to ``self.task``."""
|
|
170
|
+
return TaskType(self.task)
|
|
171
|
+
|
|
172
|
+
def stage_reduction(self, stage: int) -> int:
|
|
173
|
+
"""Total spatial reduction factor at the output of ``stage`` (0-based)."""
|
|
174
|
+
return STEM_REDUCTION * (STAGE_REDUCTION ** stage)
|
|
175
|
+
|
|
176
|
+
def validate(self) -> "SHADAConfig":
|
|
177
|
+
"""Validate internal consistency. Returns ``self`` for chaining.
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
ValueError: if the per-stage lists are inconsistent or any value
|
|
181
|
+
is out of range.
|
|
182
|
+
"""
|
|
183
|
+
n = NUM_STAGES
|
|
184
|
+
for name, seq in (
|
|
185
|
+
("encoder_dims", self.encoder_dims),
|
|
186
|
+
("encoder_depths", self.encoder_depths),
|
|
187
|
+
("num_heads", self.num_heads),
|
|
188
|
+
):
|
|
189
|
+
if len(seq) != n:
|
|
190
|
+
raise ValueError(
|
|
191
|
+
f"{name} must have exactly {n} entries, got {len(seq)}: {seq!r}"
|
|
192
|
+
)
|
|
193
|
+
for dim, heads in zip(self.encoder_dims, self.num_heads):
|
|
194
|
+
if dim % heads != 0:
|
|
195
|
+
raise ValueError(
|
|
196
|
+
f"each encoder dim must be divisible by its head count; "
|
|
197
|
+
f"dim={dim} is not divisible by num_heads={heads}"
|
|
198
|
+
)
|
|
199
|
+
if self.task not in {t.value for t in TaskType}:
|
|
200
|
+
raise ValueError(
|
|
201
|
+
f"task must be one of {[t.value for t in TaskType]}, got {self.task!r}"
|
|
202
|
+
)
|
|
203
|
+
if not (0.0 <= self.mask_ratio < 1.0):
|
|
204
|
+
raise ValueError(f"mask_ratio must be in [0, 1), got {self.mask_ratio}")
|
|
205
|
+
if not (0.0 <= self.text_mask_ratio < 1.0):
|
|
206
|
+
raise ValueError(
|
|
207
|
+
f"text_mask_ratio must be in [0, 1), got {self.text_mask_ratio}"
|
|
208
|
+
)
|
|
209
|
+
if self.num_classes < 1:
|
|
210
|
+
raise ValueError(f"num_classes must be >= 1, got {self.num_classes}")
|
|
211
|
+
return self
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# --------------------------------------------------------------------------- #
|
|
215
|
+
# Per-tier presets. Only architectural fields are stored here; task-specific
|
|
216
|
+
# fields (task, num_classes, ...) are supplied by create_config / the user.
|
|
217
|
+
# --------------------------------------------------------------------------- #
|
|
218
|
+
_TIER_CONFIGS: Dict[str, Dict] = {
|
|
219
|
+
"nano": {
|
|
220
|
+
"encoder_dims": [64, 128, 256, 512],
|
|
221
|
+
"encoder_depths": [2, 2, 4, 2],
|
|
222
|
+
"num_heads": [2, 4, 8, 16],
|
|
223
|
+
"max_seq_len": 512,
|
|
224
|
+
"decoder_dim": 128,
|
|
225
|
+
"decoder_depth": 1,
|
|
226
|
+
},
|
|
227
|
+
"base": {
|
|
228
|
+
"encoder_dims": [128, 256, 512, 1024],
|
|
229
|
+
"encoder_depths": [3, 4, 6, 3],
|
|
230
|
+
"num_heads": [4, 8, 16, 32],
|
|
231
|
+
"max_seq_len": 1024,
|
|
232
|
+
"decoder_dim": 256,
|
|
233
|
+
"decoder_depth": 2,
|
|
234
|
+
},
|
|
235
|
+
"large": {
|
|
236
|
+
"encoder_dims": [192, 384, 768, 1536],
|
|
237
|
+
"encoder_depths": [3, 4, 18, 3],
|
|
238
|
+
"num_heads": [6, 12, 24, 48],
|
|
239
|
+
"max_seq_len": 2048,
|
|
240
|
+
"decoder_dim": 384,
|
|
241
|
+
"decoder_depth": 2,
|
|
242
|
+
},
|
|
243
|
+
"xl": {
|
|
244
|
+
"encoder_dims": [256, 512, 1024, 2048],
|
|
245
|
+
"encoder_depths": [3, 4, 24, 3],
|
|
246
|
+
"num_heads": [8, 16, 32, 64],
|
|
247
|
+
"max_seq_len": 4096,
|
|
248
|
+
"decoder_dim": 512,
|
|
249
|
+
"decoder_depth": 4,
|
|
250
|
+
},
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def create_config(
|
|
255
|
+
tier: str = "base",
|
|
256
|
+
task: str = "classification",
|
|
257
|
+
num_classes: int = 1000,
|
|
258
|
+
**overrides,
|
|
259
|
+
) -> SHADAConfig:
|
|
260
|
+
"""Create a :class:`SHADAConfig` with tier-specific defaults.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
tier: Model tier (``nano``/``base``/``large``/``xl``).
|
|
264
|
+
task: Task type (``classification``/``detection``/``segmentation``/``lm``).
|
|
265
|
+
num_classes: Number of output classes (see :class:`SHADAConfig`).
|
|
266
|
+
**overrides: Any :class:`SHADAConfig` field to override.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
A validated :class:`SHADAConfig` instance.
|
|
270
|
+
|
|
271
|
+
Raises:
|
|
272
|
+
ValueError: if ``tier`` is unknown or the resulting config is invalid.
|
|
273
|
+
|
|
274
|
+
Example:
|
|
275
|
+
>>> config = create_config("base", task="classification", num_classes=10)
|
|
276
|
+
>>> config.final_dim
|
|
277
|
+
1024
|
|
278
|
+
"""
|
|
279
|
+
if isinstance(tier, ModelTier):
|
|
280
|
+
tier = tier.value
|
|
281
|
+
if tier not in _TIER_CONFIGS:
|
|
282
|
+
raise ValueError(
|
|
283
|
+
f"tier must be one of {list(_TIER_CONFIGS)}, got {tier!r}"
|
|
284
|
+
)
|
|
285
|
+
if isinstance(task, TaskType):
|
|
286
|
+
task = task.value
|
|
287
|
+
kwargs = {
|
|
288
|
+
**_TIER_CONFIGS[tier],
|
|
289
|
+
"tier": tier,
|
|
290
|
+
"task": task,
|
|
291
|
+
"num_classes": num_classes,
|
|
292
|
+
**overrides,
|
|
293
|
+
}
|
|
294
|
+
return SHADAConfig(**kwargs)
|
shadax/core.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Backward-compatibility shim for :mod:`shadax.core`.
|
|
3
|
+
|
|
4
|
+
The original monolithic ``core.py`` has been split into focused modules
|
|
5
|
+
(:mod:`shadax.config`, :mod:`shadax.network`, :mod:`shadax.model`, ...). This
|
|
6
|
+
module preserves the old import surface so that existing code such as::
|
|
7
|
+
|
|
8
|
+
from shadax.core import SHADA, SHADAConfig, create_config
|
|
9
|
+
from shadax.core import ModelTier, TaskType, TrainingPhase
|
|
10
|
+
|
|
11
|
+
keeps working unchanged. Everything is re-exported from its new home.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from shadax.config import (
|
|
17
|
+
Modality,
|
|
18
|
+
ModelTier,
|
|
19
|
+
SHADAConfig,
|
|
20
|
+
TaskType,
|
|
21
|
+
TrainingPhase,
|
|
22
|
+
create_config,
|
|
23
|
+
)
|
|
24
|
+
from shadax.model import SHADA
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"SHADA",
|
|
28
|
+
"SHADAConfig",
|
|
29
|
+
"create_config",
|
|
30
|
+
"ModelTier",
|
|
31
|
+
"TaskType",
|
|
32
|
+
"TrainingPhase",
|
|
33
|
+
"Modality",
|
|
34
|
+
]
|
shadax/encoder.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SHADA hierarchical multi-modal encoder.
|
|
3
|
+
|
|
4
|
+
This module defines :class:`HierarchicalEncoder`, the four-stage backbone that
|
|
5
|
+
turns either an image ``(B, C, H, W)`` or a text token sequence ``(B, L)`` into
|
|
6
|
+
a hierarchy of feature maps plus pooled global features.
|
|
7
|
+
|
|
8
|
+
The encoder is built entirely from the primitives in :mod:`shadax.modules` and
|
|
9
|
+
driven by a :class:`shadax.config.SHADAConfig`. It has two input paths that
|
|
10
|
+
share their per-stage Transformer blocks:
|
|
11
|
+
|
|
12
|
+
* **Image path** -- a :class:`~shadax.modules.ConvStem` (``/4``), then four
|
|
13
|
+
stages each consisting of a :class:`~shadax.modules.ConditionalPositionalEncoding`
|
|
14
|
+
and a list of :class:`~shadax.modules.TransformerBlock` s operating on the
|
|
15
|
+
flattened tokens, with a :class:`~shadax.modules.StageDownsample` (``/2``)
|
|
16
|
+
between consecutive stages. Total spatial reduction is
|
|
17
|
+
:data:`~shadax.config.TOTAL_REDUCTION` (= 32).
|
|
18
|
+
* **Text path** -- a :class:`~shadax.modules.TextEmbedding`, then the same four
|
|
19
|
+
stages of Transformer blocks (text tokens are already a sequence), with a
|
|
20
|
+
length-preserving :class:`~shadax.modules.TextStageProject` between stages.
|
|
21
|
+
|
|
22
|
+
The Transformer blocks are shared across modalities: both paths ultimately call
|
|
23
|
+
them on token sequences ``(B, N, dim)``.
|
|
24
|
+
|
|
25
|
+
The dictionary returned by :meth:`HierarchicalEncoder.forward` is a hard
|
|
26
|
+
contract consumed by :mod:`shadax.heads` and :mod:`shadax.ssl`; its keys and
|
|
27
|
+
shapes must not change.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
33
|
+
|
|
34
|
+
import torch
|
|
35
|
+
import torch.nn as nn
|
|
36
|
+
|
|
37
|
+
from shadax.config import (
|
|
38
|
+
NUM_STAGES,
|
|
39
|
+
STEM_REDUCTION,
|
|
40
|
+
TOTAL_REDUCTION,
|
|
41
|
+
Modality,
|
|
42
|
+
SHADAConfig,
|
|
43
|
+
)
|
|
44
|
+
from shadax.modules import (
|
|
45
|
+
ConditionalPositionalEncoding,
|
|
46
|
+
ConvStem,
|
|
47
|
+
StageDownsample,
|
|
48
|
+
TextEmbedding,
|
|
49
|
+
TextStageProject,
|
|
50
|
+
TransformerBlock,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
__all__ = ["HierarchicalEncoder"]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class _Stage(nn.Module):
|
|
58
|
+
"""A single encoder stage: positional encoding plus Transformer blocks.
|
|
59
|
+
|
|
60
|
+
The conditional positional encoding is only used by the image path (it
|
|
61
|
+
operates on spatial maps); the Transformer block list is shared between the
|
|
62
|
+
image and text paths, both of which feed it token sequences.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
dim: Channel / token width of this stage.
|
|
66
|
+
depth: Number of Transformer blocks in this stage.
|
|
67
|
+
num_heads: Number of attention heads per block.
|
|
68
|
+
mlp_ratio: Hidden expansion ratio of each block's feed-forward layer.
|
|
69
|
+
dropout: Dropout probability used throughout the stage.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
dim: int,
|
|
75
|
+
depth: int,
|
|
76
|
+
num_heads: int,
|
|
77
|
+
mlp_ratio: float,
|
|
78
|
+
dropout: float,
|
|
79
|
+
) -> None:
|
|
80
|
+
super().__init__()
|
|
81
|
+
self.cpe = ConditionalPositionalEncoding(dim)
|
|
82
|
+
self.blocks = nn.ModuleList(
|
|
83
|
+
[
|
|
84
|
+
TransformerBlock(dim, num_heads, mlp_ratio=mlp_ratio, dropout=dropout)
|
|
85
|
+
for _ in range(depth)
|
|
86
|
+
]
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def run_blocks(
|
|
90
|
+
self, tokens: torch.Tensor, attn_mask: Optional[torch.Tensor] = None
|
|
91
|
+
) -> torch.Tensor:
|
|
92
|
+
"""Run every Transformer block in this stage over a token sequence.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
tokens: Token sequence of shape ``(B, N, dim)``.
|
|
96
|
+
attn_mask: Optional additive float attention mask ``(N, N)``.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Token sequence of shape ``(B, N, dim)``.
|
|
100
|
+
"""
|
|
101
|
+
for block in self.blocks:
|
|
102
|
+
tokens = block(tokens, attn_mask=attn_mask)
|
|
103
|
+
return tokens
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class HierarchicalEncoder(nn.Module):
|
|
107
|
+
"""Four-stage hierarchical encoder shared across image and text modalities.
|
|
108
|
+
|
|
109
|
+
Built from a :class:`~shadax.config.SHADAConfig`, the encoder exposes a
|
|
110
|
+
single :meth:`forward` that branches on ``modality``. Both branches produce
|
|
111
|
+
four feature maps of increasing channel width (``config.encoder_dims``),
|
|
112
|
+
plus a final token sequence and pooled global features.
|
|
113
|
+
|
|
114
|
+
For the image modality, ``H`` and ``W`` must each be divisible by
|
|
115
|
+
:data:`~shadax.config.TOTAL_REDUCTION` (= 32).
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
config: The validated SHADA configuration describing the model shape.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def __init__(self, config: SHADAConfig) -> None:
|
|
122
|
+
super().__init__()
|
|
123
|
+
self.config = config
|
|
124
|
+
dims: List[int] = config.encoder_dims
|
|
125
|
+
depths: List[int] = config.encoder_depths
|
|
126
|
+
heads: List[int] = config.num_heads
|
|
127
|
+
mlp_ratio: float = config.mlp_ratio
|
|
128
|
+
dropout: float = config.dropout
|
|
129
|
+
|
|
130
|
+
# Image entry point.
|
|
131
|
+
self.stem = ConvStem(config.in_channels, dims[0], dropout=dropout)
|
|
132
|
+
|
|
133
|
+
# Text entry point.
|
|
134
|
+
self.text_embed = TextEmbedding(
|
|
135
|
+
vocab_size=config.vocab_size,
|
|
136
|
+
embed_dim=dims[0],
|
|
137
|
+
max_seq_len=config.max_seq_len,
|
|
138
|
+
dropout=dropout,
|
|
139
|
+
pad_token_id=config.pad_token_id,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Per-stage modules (Transformer blocks shared across modalities).
|
|
143
|
+
self.stages = nn.ModuleList(
|
|
144
|
+
[
|
|
145
|
+
_Stage(dims[i], depths[i], heads[i], mlp_ratio, dropout)
|
|
146
|
+
for i in range(NUM_STAGES)
|
|
147
|
+
]
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Inter-stage reductions: image spatial downsamples and text channel
|
|
151
|
+
# projections (3 each, one between every pair of consecutive stages).
|
|
152
|
+
self.downsamples = nn.ModuleList(
|
|
153
|
+
[StageDownsample(dims[i], dims[i + 1]) for i in range(NUM_STAGES - 1)]
|
|
154
|
+
)
|
|
155
|
+
self.text_projects = nn.ModuleList(
|
|
156
|
+
[TextStageProject(dims[i], dims[i + 1]) for i in range(NUM_STAGES - 1)]
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# ------------------------------------------------------------------ #
|
|
160
|
+
# Forward dispatch.
|
|
161
|
+
# ------------------------------------------------------------------ #
|
|
162
|
+
def forward(
|
|
163
|
+
self,
|
|
164
|
+
x: torch.Tensor,
|
|
165
|
+
modality: Union[str, Modality] = "image",
|
|
166
|
+
causal: bool = False,
|
|
167
|
+
) -> Dict[str, Any]:
|
|
168
|
+
"""Encode an input into a hierarchy of features.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
x: Either an image ``(B, in_channels, H, W)`` (image modality) or a
|
|
172
|
+
long tensor of token ids ``(B, L)`` (text modality).
|
|
173
|
+
modality: ``"image"``/``"text"`` (or the corresponding
|
|
174
|
+
:class:`~shadax.config.Modality` enum value).
|
|
175
|
+
causal: Text only. If ``True``, a causal attention mask is applied
|
|
176
|
+
so each position may attend only to itself and earlier
|
|
177
|
+
positions.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
A dict with exactly the keys ``"feature_maps"`` (list of 4
|
|
181
|
+
tensors), ``"tokens"`` ``(B, N_last, dims[-1])``,
|
|
182
|
+
``"global_features"`` ``(B, dims[-1])``, ``"hw"`` (an ``(Hs, Ws)``
|
|
183
|
+
int tuple for images or ``None`` for text) and ``"modality"`` (the
|
|
184
|
+
resolved modality string).
|
|
185
|
+
|
|
186
|
+
Raises:
|
|
187
|
+
ValueError: for the image modality if ``H`` or ``W`` is not
|
|
188
|
+
divisible by :data:`~shadax.config.TOTAL_REDUCTION`, or if
|
|
189
|
+
``modality`` is not a recognised value.
|
|
190
|
+
"""
|
|
191
|
+
modality = Modality(modality)
|
|
192
|
+
if modality is Modality.IMAGE:
|
|
193
|
+
return self._forward_image(x)
|
|
194
|
+
if modality is Modality.TEXT:
|
|
195
|
+
return self._forward_text(x, causal=causal)
|
|
196
|
+
raise ValueError(f"unsupported modality: {modality!r}")
|
|
197
|
+
|
|
198
|
+
def forward_features(
|
|
199
|
+
self,
|
|
200
|
+
x: torch.Tensor,
|
|
201
|
+
modality: Union[str, Modality] = "image",
|
|
202
|
+
causal: bool = False,
|
|
203
|
+
) -> List[torch.Tensor]:
|
|
204
|
+
"""Return only the list of four hierarchical feature maps.
|
|
205
|
+
|
|
206
|
+
Convenience wrapper around :meth:`forward` that discards everything
|
|
207
|
+
except the ``"feature_maps"`` entry.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
x: Input image or token ids (see :meth:`forward`).
|
|
211
|
+
modality: Input modality (see :meth:`forward`).
|
|
212
|
+
causal: Whether to apply a causal mask (text only).
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
The list of four feature-map tensors.
|
|
216
|
+
"""
|
|
217
|
+
return self.forward(x, modality=modality, causal=causal)["feature_maps"]
|
|
218
|
+
|
|
219
|
+
# ------------------------------------------------------------------ #
|
|
220
|
+
# Modality-specific paths.
|
|
221
|
+
# ------------------------------------------------------------------ #
|
|
222
|
+
def _forward_image(self, x: torch.Tensor) -> Dict[str, Any]:
|
|
223
|
+
"""Image path of :meth:`forward` (see its docstring)."""
|
|
224
|
+
_, _, height, width = x.shape
|
|
225
|
+
if height % TOTAL_REDUCTION != 0 or width % TOTAL_REDUCTION != 0:
|
|
226
|
+
raise ValueError(
|
|
227
|
+
f"image height and width must each be divisible by "
|
|
228
|
+
f"{TOTAL_REDUCTION}, got H={height}, W={width}"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
m = self.stem(x) # (B, dims[0], H/4, W/4)
|
|
232
|
+
feature_maps: List[torch.Tensor] = []
|
|
233
|
+
|
|
234
|
+
for i, stage in enumerate(self.stages):
|
|
235
|
+
m = stage.cpe(m)
|
|
236
|
+
b, c, h, w = m.shape
|
|
237
|
+
tokens = m.flatten(2).transpose(1, 2) # (B, H*W, C)
|
|
238
|
+
tokens = stage.run_blocks(tokens)
|
|
239
|
+
m = tokens.transpose(1, 2).reshape(b, c, h, w) # (B, C, H, W)
|
|
240
|
+
feature_maps.append(m)
|
|
241
|
+
if i < NUM_STAGES - 1:
|
|
242
|
+
m = self.downsamples[i](m)
|
|
243
|
+
|
|
244
|
+
last = feature_maps[-1]
|
|
245
|
+
b, c, h, w = last.shape
|
|
246
|
+
tokens = last.flatten(2).transpose(1, 2) # (B, N_last, dims[-1])
|
|
247
|
+
global_features = tokens.mean(dim=1) # (B, dims[-1])
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
"feature_maps": feature_maps,
|
|
251
|
+
"tokens": tokens,
|
|
252
|
+
"global_features": global_features,
|
|
253
|
+
"hw": (h, w),
|
|
254
|
+
"modality": Modality.IMAGE.value,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
def _forward_text(self, x: torch.Tensor, causal: bool = False) -> Dict[str, Any]:
|
|
258
|
+
"""Text path of :meth:`forward` (see its docstring)."""
|
|
259
|
+
t = self.text_embed(x) # (B, L, dims[0])
|
|
260
|
+
seq_len = t.shape[1]
|
|
261
|
+
|
|
262
|
+
attn_mask: Optional[torch.Tensor] = None
|
|
263
|
+
if causal:
|
|
264
|
+
attn_mask = self._causal_mask(seq_len, device=t.device, dtype=t.dtype)
|
|
265
|
+
|
|
266
|
+
feature_maps: List[torch.Tensor] = []
|
|
267
|
+
for i, stage in enumerate(self.stages):
|
|
268
|
+
t = stage.run_blocks(t, attn_mask=attn_mask) # (B, L, dims[i])
|
|
269
|
+
feature_maps.append(t)
|
|
270
|
+
if i < NUM_STAGES - 1:
|
|
271
|
+
t = self.text_projects[i](t) # (B, L, dims[i+1])
|
|
272
|
+
|
|
273
|
+
tokens = t # (B, L, dims[-1])
|
|
274
|
+
global_features = tokens.mean(dim=1) # (B, dims[-1])
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
"feature_maps": feature_maps,
|
|
278
|
+
"tokens": tokens,
|
|
279
|
+
"global_features": global_features,
|
|
280
|
+
"hw": None,
|
|
281
|
+
"modality": Modality.TEXT.value,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
@staticmethod
|
|
285
|
+
def _causal_mask(
|
|
286
|
+
seq_len: int, device: torch.device, dtype: torch.dtype
|
|
287
|
+
) -> torch.Tensor:
|
|
288
|
+
"""Build an additive causal attention mask.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
seq_len: Sequence length ``L``.
|
|
292
|
+
device: Device of the produced mask.
|
|
293
|
+
dtype: Floating dtype of the produced mask.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
A ``(L, L)`` float tensor that is ``0`` on and below the diagonal
|
|
297
|
+
and ``-inf`` above it, suitable for passing as ``attn_mask`` to
|
|
298
|
+
:class:`~shadax.modules.TransformerBlock`.
|
|
299
|
+
"""
|
|
300
|
+
mask = torch.full((seq_len, seq_len), float("-inf"), device=device, dtype=dtype)
|
|
301
|
+
return torch.triu(mask, diagonal=1)
|