Ehzoahis commited on
Commit
586325c
·
verified ·
1 Parent(s): 588e46a

Release LESSViT checkpoint and directly loadable encoder

Browse files
README.md ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: transformers
3
+ language:
4
+ - en
5
+ tags:
6
+ - lessvit
7
+ - hyperspectral
8
+ - remote-sensing
9
+ - geospatial
10
+ - vision-transformer
11
+ - custom_code
12
+ arxiv: 2605.18541
13
+ ---
14
+
15
+ # LESSViT-b2-d8-r1
16
+
17
+ Pretrained **LESSViT** encoder for hyperspectral representation learning, exported
18
+ from the `LESSVIT_b2_d8_r1` checkpoint. The root of this repository contains an
19
+ encoder-only Hugging Face model, with standalone implementation files and a
20
+ working `AutoModel` interface. No project checkout is required.
21
+
22
+ - [Paper](https://arxiv.org/abs/2605.18541)
23
+ - [Project website and updated manuscript](https://uiuctml.github.io/LESSViT/)
24
+ - [Source repository](https://github.com/uiuctml/LESSViT)
25
+
26
+ ## Install
27
+
28
+ Reference environment: Python 3.11.
29
+
30
+ ```bash
31
+ pip install torch==2.7.1 torchvision==0.22.1 transformers==4.53.3 \
32
+ huggingface-hub==0.34.4 timm==1.0.18 numpy==2.2.6 safetensors
33
+ ```
34
+
35
+ Choose matching PyTorch/Torchvision wheels for your hardware. This release uses
36
+ custom model code, so `trust_remote_code=True` is required. You can pin `revision`
37
+ to a repository commit SHA for reproducible loading.
38
+
39
+ ## Load the encoder directly
40
+
41
+ ```python
42
+ import torch
43
+ from transformers import AutoModel
44
+
45
+ encoder = AutoModel.from_pretrained(
46
+ "GFM-Bench/LESSViT-b2-d8-r1",
47
+ trust_remote_code=True,
48
+ ).eval()
49
+
50
+ # Synthetic example: [batch, spectral channels, height, width].
51
+ x = torch.randn(1, 10, 32, 32)
52
+ wavelengths_nm = torch.linspace(450, 2300, 10)[None, :]
53
+ # Fixed full-EnMAP reference bounds for this example. Keep reference bounds
54
+ # consistent across your downstream training and evaluation configurations.
55
+ wavelengths = (wavelengths_nm - 418.24) / (2445.53 - 418.24)
56
+
57
+ with torch.no_grad():
58
+ output = encoder(
59
+ optical=x,
60
+ optical_channel_wv=wavelengths,
61
+ spatial_resolution=30, # meters per pixel
62
+ )
63
+
64
+ features = output.last_hidden_state
65
+ print(features.shape) # [1, 11, 5, 768]
66
+ global_features = features[:, 0, 0] # [1, 768], classification
67
+ spatial_features = features[:, 0, 1:] # [1, 4, 768], dense prediction
68
+ band_patch_features = features[:, 1:, 1:] # [1, 10, 4, 768]
69
+ ```
70
+
71
+ `load_encoder.py` contains the same runnable example:
72
+
73
+ ```bash
74
+ python load_encoder.py --model-id GFM-Bench/LESSViT-b2-d8-r1 --device cpu
75
+ ```
76
+
77
+ To run offline after downloading a complete snapshot, pass its directory as
78
+ `--model-id`. No network or original repository imports are needed for a local
79
+ snapshot.
80
+
81
+ ### Inputs and outputs
82
+
83
+ - `optical`: float tensor `[B, C, H, W]`; height and width should be multiples of 16.
84
+ - `optical_channel_wv`: normalized wavelengths `[1, C]` or `[B, C]`, matching input
85
+ channel order. Use fixed reference bounds for the task, not new min/max bounds
86
+ for every spectral subset. The numerical bounds above are an EnMAP example,
87
+ not a universal normalization rule for every sensor.
88
+ - `spatial_resolution`: spatial resolution in meters per pixel.
89
+ - `last_hidden_state`: `[B, C+1, N+1, 768]`, where
90
+ `N = (H / 16) * (W / 16)`. Index 0 contains aggregation tokens.
91
+
92
+ For real data, use dataset/sensor-specific intensity normalization and matching
93
+ wavelength metadata. The source repository includes dataset statistics,
94
+ transforms, SRF resampling, and collators. Random values in the example only
95
+ verify that loading and inference work.
96
+
97
+ ## Attach a downstream head
98
+
99
+ ```python
100
+ # Use `encoder` and example tensors from above.
101
+ encoder.train()
102
+ head = torch.nn.Linear(encoder.config.embed_dim, 14)
103
+ features = encoder(
104
+ optical=x, optical_channel_wv=wavelengths, spatial_resolution=30
105
+ ).last_hidden_state[:, 0, 0]
106
+ logits = head(features)
107
+ loss = torch.nn.functional.cross_entropy(logits, torch.zeros(1, dtype=torch.long))
108
+ loss.backward() # gradients through both encoder and head
109
+ ```
110
+
111
+ For multi-label classification use binary cross entropy with logits. For dense
112
+ prediction, reshape `spatial_features` into the patch grid and attach a spatial
113
+ decoder. Task-specific heads are not included and must be trained. MAE masking
114
+ and random channel dropout are disabled in this encoder interface, including
115
+ when calling `.train()`; ordinary configured dropout remains active in training.
116
+
117
+ ## Architecture
118
+
119
+ | Setting | Value |
120
+ |---|---|
121
+ | Encoder depth | 12 |
122
+ | Embedding dimension | 768 |
123
+ | Attention heads | 12 |
124
+ | Patch size | 16 |
125
+ | Spectral dimension per head | 2 |
126
+ | Attention rank | 1 |
127
+ | Positional encoding | Spatial–spectral RoPE |
128
+ | LayerScale initialization | 1.0 |
129
+
130
+ `d8` in the checkpoint name refers to the **pretraining decoder**, not the
131
+ encoder. The encoder-only release has approximately 83.2M parameters.
132
+
133
+ ## Files and provenance
134
+
135
+ - Root `model.safetensors`: extracted encoder, with legacy attention keys
136
+ remapped to the current implementation. No encoder tensors were omitted.
137
+ - Root `config.json`: explicit encoder architecture and Hugging Face auto-map.
138
+ - `pretraining/model.safetensors`: the original full checkpoint, copied
139
+ byte-for-byte, including the MAE decoder. It uses the original checkpoint key
140
+ layout and is **not** the root `AutoModel` format.
141
+ - `pretraining/architecture.json`: architectural fields from the accompanying
142
+ training config. Machine-specific paths and training service settings are
143
+ omitted. This is provenance metadata, not an AutoModel config.
144
+ - `validation.json`: export verification results.
145
+ - `SHA256SUMS`: file hashes.
146
+
147
+ Encoder implementation is packaged from LESSViT source commit
148
+ `20fb37c10541f1a29eb2a53999afdc0734499bc3`, with a small Hugging Face wrapper to
149
+ preserve architecture settings across config serialization and disable MAE
150
+ masking for encoder use. Loading checks compare every exported encoder tensor
151
+ exactly to the supplied checkpoint after key remapping. Local AutoModel loading,
152
+ forward-output equivalence, and encoder backward propagation were tested on CPU.
153
+ These checks do not independently reproduce the paper's benchmark scores or
154
+ establish the checkpoint's complete training-data provenance.
155
+
156
+ ## Citation
157
+
158
+ ```bibtex
159
+ @misc{si2026lessvitrobusthyperspectralrepresentation,
160
+ title={LESSViT: Robust Hyperspectral Representation Learning under Spectral Configuration Shift},
161
+ author={Haozhe Si and Yuxuan Wan and Yuqing Wang and Minh N. Do and Han Zhao},
162
+ year={2026},
163
+ eprint={2605.18541},
164
+ archivePrefix={arXiv},
165
+ primaryClass={cs.CV},
166
+ url={https://arxiv.org/abs/2605.18541}
167
+ }
168
+ ```
SHA256SUMS ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 66bd1865a04156e70e66415709e2ff39fe7dcf71f686002499a6da650274c28c README.md
2
+ d6fcf9ed303752ba5ea7792d3fa00e16ac35cd561b6df23e5a19f4d219be6aac attention_ops.py
3
+ 5aee6c840ac260cde0131ba16ac4c637da27cbe63f94aa0cd5d72f6171436e5d config.json
4
+ 0e049d15e1a8229824e4b87f0d3312f55e58c0a2985bd36a69fa5f7790d024de hyperspectral_patch_embed.py
5
+ 780ee1bf2b7119a3cbc3a4f84f5e185c3f0a8c455f9c23deb401f15556333efe load_encoder.py
6
+ e1e7af883a62f663010397bc46fcae04801ed2465ed830e8bb488a044f05f809 low_rank_attention.py
7
+ 935719728265cf0765b32afe634803ab73071025f104c1c708b98142488b8311 model.safetensors
8
+ 1b29142386ee2d45deb01325fdf7cc63e91fd1503ca1017ac0d47db9341e3111 modeling_lessvit.py
9
+ 4c994cbd89ca4dc5f31ca711b8c7c27b0ec6b3fea7755162e9279fff927c47b7 pos_chan_embed.py
10
+ fe35811d65813247e513749d8fab57b082d553d0e371b78ddc39c60023817f19 pretraining/architecture.json
11
+ 8e5dce5229b24255d31dc619c005ad6355784eff7ee9e07036aba6f36a1c606a pretraining/model.safetensors
12
+ 9d58c64770e91d2f2dfae27194b600f680c95649791ead9510fc0bd83fedd5c2 requirements.txt
13
+ 5d822f2a1439cea4bba10696277a6ec56cea63e337fd1e05cdec5d2f0ce19ab8 spatial_spectral_low_rank_vit.py
14
+ 68301b5051270d261572715d0cbb4e003d4b901dbb9e202387cae29a93afe6d6 validation.json
attention_ops.py ADDED
@@ -0,0 +1,432 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+
3
+ import torch
4
+ import torch.nn as nn
5
+ import torch.nn.functional as F
6
+ from timm.layers import Mlp, DropPath, use_fused_attn
7
+
8
+ from .low_rank_attention import AttentionBranch, LowDimPool, LowRankAttention
9
+ from .pos_chan_embed import rope_rotate_half
10
+
11
+ __all__ = [
12
+ 'SpatialSpectralAttention',
13
+ 'LESSAttention',
14
+ 'FullSpatialSpectralAttention',
15
+ 'AdditiveSpatialSpectralAttention',
16
+ 'build_attention',
17
+ 'LayerScale',
18
+ 'LowRankBlock',
19
+ ]
20
+
21
+
22
+ class SpatialSpectralAttention(nn.Module):
23
+ """
24
+ Common interface for the block-level spatial-spectral attention operator.
25
+
26
+ forward(x, spatial_mask=None, rope=None) -> Tensor
27
+ x: [B, C, HW, D] (C and HW here are whatever the caller's grid axis sizes
28
+ currently are, e.g. C+1/HW+1 when CLS tokens are folded in).
29
+ spatial_mask: optional perception-field mask, [HW, HW] or None.
30
+ rope: optional (sin_hw, cos_hw, sin_c, cos_c) tuple from
31
+ RopePositionChannelEmbedding, or None.
32
+ returns: [B, C, HW, D], same shape as x.
33
+ """
34
+ pass
35
+
36
+
37
+ class LESSAttention(SpatialSpectralAttention):
38
+ """
39
+ Arm A: the existing LESS (Kronecker) attention. Thin composition of the
40
+ unmodified LowDimPool + LowRankAttention classes behind the unified interface;
41
+ no math differs from the original LowRankBlock.forward.
42
+ """
43
+ def __init__(
44
+ self,
45
+ dim: int,
46
+ channel_dim: int,
47
+ spatial_dim: int,
48
+ num_heads: int = 8,
49
+ qkv_bias: bool = False,
50
+ qk_norm: bool = False,
51
+ attn_drop: float = 0.,
52
+ proj_drop: float = 0.,
53
+ norm_layer: nn.Module = nn.LayerNorm,
54
+ rank: int = 1,
55
+ skip_pool: bool = False,
56
+ ) -> None:
57
+ super().__init__()
58
+ self.low_dim_pool = LowDimPool(
59
+ dim=dim,
60
+ channel_dim=channel_dim,
61
+ spatial_dim=spatial_dim,
62
+ num_heads=num_heads,
63
+ qkv_bias=qkv_bias,
64
+ qk_norm=qk_norm,
65
+ attn_drop=attn_drop,
66
+ proj_drop=proj_drop,
67
+ norm_layer=norm_layer,
68
+ skip_pool=skip_pool,
69
+ )
70
+ self.channel_norm = norm_layer(channel_dim)
71
+ self.spatial_norm = norm_layer(spatial_dim)
72
+ self.attn = LowRankAttention(
73
+ dim=dim,
74
+ channel_dim=channel_dim,
75
+ spatial_dim=spatial_dim,
76
+ num_heads=num_heads,
77
+ qkv_bias=qkv_bias,
78
+ qk_norm=qk_norm,
79
+ attn_drop=attn_drop,
80
+ proj_drop=proj_drop,
81
+ norm_layer=norm_layer,
82
+ rank=rank,
83
+ )
84
+
85
+ def forward(self, x: torch.Tensor, spatial_mask: torch.Tensor = None, rope: tuple = None) -> torch.Tensor:
86
+ x_c, x_s, _ = self.low_dim_pool(x)
87
+ return self.attn(self.channel_norm(x_c), self.spatial_norm(x_s), spatial_mask, rope)
88
+
89
+
90
+ class AdditiveSpatialSpectralAttention(SpatialSpectralAttention):
91
+ """
92
+ Arm C: *identical* channel/spatial branch attentions as arm A -- same
93
+ LowDimPool, same AttentionBranch class, same channel_dim/spatial_dim (so
94
+ c_head_dim/s_head_dim keep arm A's low-rank widths, e.g. 4 and 16 for ViT-S,
95
+ NOT widened to D_h). This isolates the fusion mechanism specifically, rather
96
+ than also widening the branch attentions (an earlier version of this class
97
+ set channel_dim=spatial_dim=dim, which inflated LowDimPool's projections and
98
+ the branch attentions themselves and produced a ~3x arm A param count -- this
99
+ version's param delta over arm A is just P_c/P_s, i.e. genuinely "slight").
100
+
101
+ Fusion: each branch's per-head output is projected *up* to the full head_dim
102
+ D_h only at the fusion step, then broadcast-added:
103
+
104
+ Y[c, n] = P_c(Y_C[c]) + P_s(Y_S[n]), P_c: R^c_head_dim -> R^D_h,
105
+ P_s: R^s_head_dim -> R^D_h
106
+
107
+ P_c/P_s are shared across heads (a single small matrix broadcast over the
108
+ heads dimension, mirroring how the Kronecker fusion also has no per-head-
109
+ distinct fusion parameters). `fusion_init_scale` rescales P_c/P_s's
110
+ initialization so that arm C's post-fusion activation std at initialization
111
+ matches arm A's (see ablation_sanity_checks.py check #4) -- the two fusion
112
+ mechanisms are algebraically different (product vs. sum) so matching
113
+ variance requires an explicit scale, not just "sensible" init.
114
+ """
115
+ def __init__(
116
+ self,
117
+ dim: int,
118
+ channel_dim: int,
119
+ spatial_dim: int,
120
+ num_heads: int = 8,
121
+ qkv_bias: bool = False,
122
+ qk_norm: bool = False,
123
+ attn_drop: float = 0.,
124
+ proj_drop: float = 0.,
125
+ norm_layer: nn.Module = nn.LayerNorm,
126
+ rank: int = 1,
127
+ skip_pool: bool = False,
128
+ fusion_init_scale: float = 1.0,
129
+ ) -> None:
130
+ super().__init__()
131
+ assert channel_dim % num_heads == 0, 'channel_dim should be divisible by num_heads'
132
+ assert spatial_dim % num_heads == 0, 'spatial_dim should be divisible by num_heads'
133
+ assert dim % num_heads == 0, 'dim should be divisible by num_heads'
134
+
135
+ self.num_heads = num_heads
136
+ self.dim = dim
137
+ self.head_dim = dim // num_heads
138
+ self.c_head_dim = channel_dim // num_heads
139
+ self.s_head_dim = spatial_dim // num_heads
140
+ self.rank = rank
141
+
142
+ self.low_dim_pool = LowDimPool(
143
+ dim=dim,
144
+ channel_dim=channel_dim,
145
+ spatial_dim=spatial_dim,
146
+ num_heads=num_heads,
147
+ qkv_bias=qkv_bias,
148
+ qk_norm=qk_norm,
149
+ attn_drop=attn_drop,
150
+ proj_drop=proj_drop,
151
+ norm_layer=norm_layer,
152
+ skip_pool=skip_pool,
153
+ )
154
+ self.channel_norm = norm_layer(channel_dim)
155
+ self.spatial_norm = norm_layer(spatial_dim)
156
+
157
+ self.channel_branch = AttentionBranch(channel_dim, num_heads, self.c_head_dim, qkv_bias, qk_norm, attn_drop, norm_layer, rank)
158
+ self.spatial_branch = AttentionBranch(spatial_dim, num_heads, self.s_head_dim, qkv_bias, qk_norm, attn_drop, norm_layer, rank)
159
+
160
+ self.P_c = nn.Linear(self.c_head_dim, self.head_dim)
161
+ self.P_s = nn.Linear(self.s_head_dim, self.head_dim)
162
+ with torch.no_grad():
163
+ # Scale the whole affine map (weight AND bias) so that P(x) -> scale*P(x)
164
+ # exactly, uniformly rescaling arm C's post-fusion output to match arm A's
165
+ # activation std at init (see ablation_sanity_checks.check_fusion_init_scale_calibration).
166
+ self.P_c.weight.mul_(fusion_init_scale)
167
+ self.P_c.bias.mul_(fusion_init_scale)
168
+ self.P_s.weight.mul_(fusion_init_scale)
169
+ self.P_s.bias.mul_(fusion_init_scale)
170
+
171
+ self.proj = nn.Linear(dim, dim)
172
+ self.proj_drop = nn.Dropout(proj_drop)
173
+
174
+ def forward(self, x_grid: torch.Tensor, spatial_mask: torch.Tensor = None, rope: tuple = None) -> torch.Tensor:
175
+ x_c, x_s, _ = self.low_dim_pool(x_grid)
176
+ C = x_c.shape[1]
177
+ HW = x_s.shape[1]
178
+
179
+ sin_hw, cos_hw, sin_c, cos_c = rope if rope is not None else (None, None, None, None)
180
+
181
+ xc = self.channel_branch(self.channel_norm(x_c), sin=sin_c, cos=cos_c) # B, rank, num_heads, C, c_head_dim
182
+ xs = self.spatial_branch(self.spatial_norm(x_s), spatial_mask, sin=sin_hw, cos=cos_hw) # B, rank, num_heads, HW, s_head_dim
183
+
184
+ xc = xc.sum(dim=1) / self.rank # B, num_heads, C, c_head_dim
185
+ xs = xs.sum(dim=1) / self.rank # B, num_heads, HW, s_head_dim
186
+
187
+ Yc = self.P_c(xc) # B, num_heads, C, head_dim
188
+ Ys = self.P_s(xs) # B, num_heads, HW, head_dim
189
+
190
+ Y = Yc.unsqueeze(3) + Ys.unsqueeze(2) # B, num_heads, C, HW, head_dim
191
+ B = Y.shape[0]
192
+ Y = Y.permute(0, 2, 3, 1, 4).reshape(B, C, HW, -1) # B, C, HW, D
193
+ Y = self.proj(Y)
194
+ Y = self.proj_drop(Y)
195
+ return Y
196
+
197
+
198
+ def _apply_axis_rope(x: torch.Tensor, sin: Optional[torch.Tensor], cos: Optional[torch.Tensor], axis: int) -> torch.Tensor:
199
+ """
200
+ Rotate a sub-block of the head dimension of `x` ([B, num_heads, C, HW, W])
201
+ along one grid axis (axis=2 for the channel axis, axis=3 for the position
202
+ axis), skipping index 0 along that axis (the channel-CLS row when axis=2, the
203
+ spatial-CLS column when axis=3). sin/cos: [B, L, W] where L == x.shape[axis]-1.
204
+
205
+ Reuses the exact rotate-half formula from pos_chan_embed.rope_apply; only the
206
+ broadcasting is generalized here to the extra grid axis that arm A's per-branch
207
+ tensors never had.
208
+ """
209
+ if sin is None:
210
+ return x
211
+ L = x.shape[axis] - 1
212
+ x_cls, x_rest = x.split([1, L], dim=axis)
213
+ view_shape = [x.shape[0]] + [1] * (x.dim() - 2) + [x.shape[-1]]
214
+ view_shape[axis] = L
215
+ sin = sin.reshape(view_shape)
216
+ cos = cos.reshape(view_shape)
217
+ x_rot = x_rest * cos + rope_rotate_half(x_rest) * sin
218
+ return torch.cat([x_cls, x_rot], dim=axis)
219
+
220
+
221
+ class FullSpatialSpectralAttention(SpatialSpectralAttention):
222
+ """
223
+ Arm B: standard full softmax attention over the flattened (channel, position)
224
+ token grid (paper Appendix E, Algorithm 2). No low-rank branch factorization,
225
+ no Kronecker fusion -- a single Q/K/V, softmax over all C*HW tokens.
226
+
227
+ Joint RoPE: partitions the head dimension D_h into a spatial (u,v) sub-block
228
+ of width `spatial_split_dim` and a spectral (lambda) sub-block of width
229
+ `spectral_split_dim` (spatial_split_dim + spectral_split_dim == D_h). Each
230
+ sub-block is rotated independently -- spatial sub-block using (sin_hw, cos_hw)
231
+ broadcast over the channel axis, spectral sub-block using (sin_c, cos_c)
232
+ broadcast over the position axis -- reusing RopePositionChannelEmbedding's
233
+ existing frequency construction unmodified (see model config: rope is built
234
+ with rope_spatial_split_dim/rope_channel_split_dim instead of the per-branch
235
+ spatial_dim/channel_dim that arms A/C use). Rotation is applied while the grid
236
+ is still 2D-indexed (before the C/HW flatten), so each axis's CLS-skip is
237
+ independent: real tokens (c>=1, n>=1) get both rotations; the channel-CLS row
238
+ (c=0) gets spatial rotation only; the spatial-CLS column (n=0) gets spectral
239
+ rotation only; the global CLS (0,0) gets neither.
240
+ """
241
+ def __init__(
242
+ self,
243
+ dim: int,
244
+ num_heads: int = 8,
245
+ qkv_bias: bool = False,
246
+ qk_norm: bool = False,
247
+ attn_drop: float = 0.,
248
+ proj_drop: float = 0.,
249
+ norm_layer: nn.Module = nn.LayerNorm,
250
+ ) -> None:
251
+ super().__init__()
252
+ assert dim % num_heads == 0, 'dim should be divisible by num_heads'
253
+ self.num_heads = num_heads
254
+ self.head_dim = dim // num_heads
255
+ assert self.head_dim % 2 == 0, 'head_dim must be even for RoPE rotate-half'
256
+ self.scale = self.head_dim ** -0.5
257
+ self.fused_attn = use_fused_attn()
258
+
259
+ self.spatial_split_dim = self.head_dim // 2
260
+ self.spectral_split_dim = self.head_dim - self.spatial_split_dim
261
+ assert self.spatial_split_dim % 2 == 0 and self.spectral_split_dim % 2 == 0, \
262
+ 'both rope sub-blocks must be even for rotate-half'
263
+
264
+ self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
265
+ self.q_norm = norm_layer(self.head_dim) if qk_norm else nn.Identity()
266
+ self.k_norm = norm_layer(self.head_dim) if qk_norm else nn.Identity()
267
+ self.attn_drop = nn.Dropout(attn_drop)
268
+ self.proj = nn.Linear(dim, dim)
269
+ self.proj_drop = nn.Dropout(proj_drop)
270
+
271
+ def apply_joint_rope(self, q: torch.Tensor, k: torch.Tensor, rope: tuple):
272
+ sin_hw, cos_hw, sin_c, cos_c = rope
273
+
274
+ def rotate(t: torch.Tensor) -> torch.Tensor:
275
+ t_spa, t_spec = t.split([self.spatial_split_dim, self.spectral_split_dim], dim=-1)
276
+ t_spa = _apply_axis_rope(t_spa, sin_hw, cos_hw, axis=3) # rotate along the HW (position) axis
277
+ t_spec = _apply_axis_rope(t_spec, sin_c, cos_c, axis=2) # rotate along the C (channel) axis
278
+ return torch.cat([t_spa, t_spec], dim=-1)
279
+
280
+ return rotate(q), rotate(k)
281
+
282
+ def forward(self, x: torch.Tensor, spatial_mask: torch.Tensor = None, rope: tuple = None) -> torch.Tensor:
283
+ if spatial_mask is not None:
284
+ raise NotImplementedError(
285
+ 'perception_field_mask is not supported for attn_type="full" in this ablation '
286
+ '(not exercised by the shared base config -- fail loudly rather than silently ignore it)'
287
+ )
288
+ B, C, HW, D = x.shape
289
+ qkv = self.qkv(x).reshape(B, C, HW, 3, self.num_heads, self.head_dim).permute(3, 0, 4, 1, 2, 5)
290
+ q, k, v = qkv.unbind(0) # B, num_heads, C, HW, head_dim
291
+
292
+ if rope is not None:
293
+ q, k = self.apply_joint_rope(q, k, rope)
294
+
295
+ q, k = self.q_norm(q), self.k_norm(k)
296
+
297
+ q = q.reshape(B, self.num_heads, C * HW, self.head_dim)
298
+ k = k.reshape(B, self.num_heads, C * HW, self.head_dim)
299
+ v = v.reshape(B, self.num_heads, C * HW, self.head_dim)
300
+
301
+ if self.fused_attn:
302
+ out = F.scaled_dot_product_attention(
303
+ q, k, v,
304
+ dropout_p=self.attn_drop.p if self.training else 0.,
305
+ )
306
+ else:
307
+ attn = (q @ k.transpose(-2, -1)) * self.scale
308
+ attn = attn.softmax(dim=-1)
309
+ attn = self.attn_drop(attn)
310
+ out = attn @ v
311
+
312
+ out = out.reshape(B, self.num_heads, C, HW, self.head_dim)
313
+ out = out.permute(0, 2, 3, 1, 4).reshape(B, C, HW, D)
314
+ out = self.proj(out)
315
+ out = self.proj_drop(out)
316
+ return out
317
+
318
+
319
+ def build_attention(
320
+ attn_type: str,
321
+ *,
322
+ dim: int,
323
+ channel_dim: int,
324
+ spatial_dim: int,
325
+ num_heads: int,
326
+ rank: int = 1,
327
+ qkv_bias: bool = False,
328
+ qk_norm: bool = False,
329
+ attn_drop: float = 0.,
330
+ proj_drop: float = 0.,
331
+ norm_layer: nn.Module = nn.LayerNorm,
332
+ skip_pool: bool = False,
333
+ fusion_init_scale: float = 1.0,
334
+ ) -> SpatialSpectralAttention:
335
+ if attn_type == 'less':
336
+ return LESSAttention(
337
+ dim=dim, channel_dim=channel_dim, spatial_dim=spatial_dim, num_heads=num_heads,
338
+ qkv_bias=qkv_bias, qk_norm=qk_norm, attn_drop=attn_drop, proj_drop=proj_drop,
339
+ norm_layer=norm_layer, rank=rank, skip_pool=skip_pool,
340
+ )
341
+ elif attn_type == 'additive':
342
+ return AdditiveSpatialSpectralAttention(
343
+ dim=dim, channel_dim=channel_dim, spatial_dim=spatial_dim, num_heads=num_heads,
344
+ qkv_bias=qkv_bias, qk_norm=qk_norm, attn_drop=attn_drop, proj_drop=proj_drop,
345
+ norm_layer=norm_layer, rank=rank, skip_pool=skip_pool, fusion_init_scale=fusion_init_scale,
346
+ )
347
+ elif attn_type == 'full':
348
+ return FullSpatialSpectralAttention(
349
+ dim=dim, num_heads=num_heads, qkv_bias=qkv_bias, qk_norm=qk_norm,
350
+ attn_drop=attn_drop, proj_drop=proj_drop, norm_layer=norm_layer,
351
+ )
352
+ else:
353
+ raise ValueError(f'Unknown attn_type: {attn_type!r}, expected one of "less", "full", "additive"')
354
+
355
+
356
+ class LayerScale(nn.Module):
357
+ def __init__(
358
+ self,
359
+ dim: int,
360
+ init_values: float = 1e-5,
361
+ inplace: bool = False,
362
+ ) -> None:
363
+ super().__init__()
364
+ self.inplace = inplace
365
+ self.gamma = nn.Parameter(init_values * torch.ones(dim))
366
+
367
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
368
+ return x.mul_(self.gamma) if self.inplace else x * self.gamma
369
+
370
+
371
+ class LowRankBlock(nn.Module):
372
+ def __init__(
373
+ self,
374
+ dim: int,
375
+ num_heads: int,
376
+ channel_dim: int,
377
+ spatial_dim: int,
378
+ mlp_ratio: float = 4.,
379
+ qkv_bias: bool = False,
380
+ qk_norm: bool = False,
381
+ proj_drop: float = 0.,
382
+ attn_drop: float = 0.,
383
+ drop_path: float = 0.,
384
+ init_values: Optional[float] = None,
385
+ act_layer: nn.Module = nn.GELU,
386
+ norm_layer: nn.Module = nn.LayerNorm,
387
+ mlp_layer: nn.Module = Mlp,
388
+ skip_pool: bool = False,
389
+ rank: int = 1,
390
+ use_rope_embed: bool = False,
391
+ attn_type: str = 'less',
392
+ fusion_init_scale: float = 1.0,
393
+ ) -> None:
394
+ super().__init__()
395
+ self.norm1 = norm_layer(dim)
396
+
397
+ self.attn = build_attention(
398
+ attn_type,
399
+ dim=dim,
400
+ channel_dim=channel_dim,
401
+ spatial_dim=spatial_dim,
402
+ num_heads=num_heads,
403
+ rank=rank,
404
+ qkv_bias=qkv_bias,
405
+ qk_norm=qk_norm,
406
+ attn_drop=attn_drop,
407
+ proj_drop=proj_drop,
408
+ norm_layer=norm_layer,
409
+ skip_pool=skip_pool,
410
+ fusion_init_scale=fusion_init_scale,
411
+ )
412
+ self.ls1 = LayerScale(dim, init_values=init_values) if init_values else nn.Identity()
413
+ self.drop_path1 = DropPath(drop_path) if drop_path > 0. else nn.Identity()
414
+
415
+ self.norm2 = norm_layer(dim)
416
+ self.mlp = mlp_layer(
417
+ in_features=dim,
418
+ hidden_features=int(dim * mlp_ratio),
419
+ act_layer=act_layer,
420
+ drop=proj_drop,
421
+ )
422
+ self.ls2 = LayerScale(dim, init_values=init_values) if init_values else nn.Identity()
423
+ self.drop_path2 = DropPath(drop_path) if drop_path > 0. else nn.Identity()
424
+
425
+ self.use_rope_embed = use_rope_embed
426
+
427
+ def forward(self, x: torch.Tensor, spatial_mask: torch.Tensor = None, pos_chan_embedding: torch.Tensor = None) -> torch.Tensor:
428
+ rope = pos_chan_embedding if self.use_rope_embed else None
429
+
430
+ x = x + self.drop_path1(self.ls1(self.attn(self.norm1(x), spatial_mask, rope)))
431
+ x = x + self.drop_path2(self.ls2(self.mlp(self.norm2(x))))
432
+ return x
config.json ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "LESSViTModel"
4
+ ],
5
+ "attention_radius": 640,
6
+ "attn_drop": 0.1,
7
+ "attn_type": "less",
8
+ "channel_dim": 24,
9
+ "channel_dropout": null,
10
+ "channel_embed_dims_per_head": 2,
11
+ "channel_mask_ratio": 0.0,
12
+ "decoder_channel_dim": 64,
13
+ "decoder_depth": 8,
14
+ "decoder_embed_dim": 512,
15
+ "decoder_num_heads": 16,
16
+ "decoder_out_chans": 1,
17
+ "decoder_rope_channel_split_dim": 256,
18
+ "decoder_rope_spatial_split_dim": 256,
19
+ "decoder_spatial_dim": 128,
20
+ "depth": 12,
21
+ "drop_path_rate": 0.1,
22
+ "drop_path_uniform": false,
23
+ "embed_dim": 768,
24
+ "fusion_init_scale": 1.0,
25
+ "init_values": 1.0,
26
+ "mask_ratio": 0.0,
27
+ "mlp_ratio": 4.0,
28
+ "model_type": "lessvit",
29
+ "norm_pix_loss": true,
30
+ "num_heads": 12,
31
+ "num_tokens": 1,
32
+ "patch_size": 16,
33
+ "pos_chan_embed_residual": true,
34
+ "pretrain": false,
35
+ "proj_drop": 0.1,
36
+ "qk_norm": false,
37
+ "qkv_bias": true,
38
+ "rank": 1,
39
+ "return_dict": true,
40
+ "rope_channel_split_dim": 384,
41
+ "rope_embed_base": 100.0,
42
+ "rope_spatial_split_dim": 384,
43
+ "spatial_dim": 384,
44
+ "torch_dtype": "float32",
45
+ "transformers_version": "4.53.3",
46
+ "use_perception_field_mask": true,
47
+ "use_rope_embed": true,
48
+ "auto_map": {
49
+ "AutoConfig": "modeling_lessvit.LESSViTConfig",
50
+ "AutoModel": "modeling_lessvit.LESSViTModel"
51
+ }
52
+ }
hyperspectral_patch_embed.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import numpy as np
4
+
5
+ class HyperspectralPatchEmbed(nn.Module):
6
+ """Image to Patch Embedding."""
7
+
8
+ def __init__(
9
+ self,
10
+ patch_size: int = 16,
11
+ embed_dim: int = 768,
12
+ ):
13
+ super().__init__()
14
+ self.patch_size = patch_size
15
+
16
+ # self.proj = nn.Conv3d(
17
+ # 1,
18
+ # embed_dim,
19
+ # kernel_size=(1, patch_size, patch_size),
20
+ # stride=(1, patch_size, patch_size),
21
+ # )
22
+
23
+ self.proj = nn.Sequential(
24
+ nn.Conv3d(
25
+ 1,
26
+ embed_dim*2, #2
27
+ kernel_size=(1, patch_size, patch_size),
28
+ stride=(1, patch_size, patch_size),
29
+ ),
30
+ nn.ReLU(),
31
+ nn.Conv3d(
32
+ embed_dim*2,
33
+ embed_dim,
34
+ kernel_size=(1, 1, 1),
35
+ stride=(1, 1, 1),
36
+ ),
37
+ )
38
+
39
+ self.initialize_weights()
40
+
41
+ def initialize_weights(self):
42
+ try:
43
+ w = self.proj.weight.data
44
+ torch.nn.init.xavier_uniform_(w.view([w.shape[0], -1]))
45
+ except:
46
+ w1 = self.proj[0].weight.data
47
+ w2 = self.proj[2].weight.data
48
+ torch.nn.init.xavier_uniform_(w1.view([w1.shape[0], -1]))
49
+ torch.nn.init.xavier_uniform_(w2.view([w2.shape[0], -1]))
50
+
51
+ def forward(self, x):
52
+ # x: B, Cin, H, W
53
+ assert x.shape[2] % self.patch_size == 0, "image height should be divisible by patch size"
54
+ assert x.shape[3] % self.patch_size == 0, "image width should be divisible by patch size"
55
+ x = self.proj(x.unsqueeze(1)) # B, D, C, H, W
56
+ B, D, C, H, W = x.shape
57
+ return x.permute(0, 2, 3, 4, 1).reshape(B, C, H*W, D) # B, C, H, W, D
load_encoder.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Load the released encoder and extract features from a synthetic HSI batch."""
2
+ import argparse
3
+ import torch
4
+ from transformers import AutoModel
5
+
6
+
7
+ def load_encoder(model_id="GFM-Bench/LESSViT-b2-d8-r1", device="cpu", revision=None):
8
+ model = AutoModel.from_pretrained(
9
+ model_id, trust_remote_code=True, revision=revision,
10
+ )
11
+ return model.to(device).eval()
12
+
13
+
14
+ def main():
15
+ parser = argparse.ArgumentParser(description=__doc__)
16
+ parser.add_argument('--model-id', default='GFM-Bench/LESSViT-b2-d8-r1',
17
+ help='Hugging Face model ID or local snapshot directory')
18
+ parser.add_argument('--revision', default=None, help='Optional commit SHA')
19
+ parser.add_argument('--device', default='cpu')
20
+ args = parser.parse_args()
21
+ torch.set_num_threads(4)
22
+ encoder = load_encoder(args.model_id, args.device, args.revision)
23
+ # Synthetic values demonstrate tensor shapes, not preprocessing for real imagery.
24
+ image = torch.randn(1, 10, 32, 32, device=args.device)
25
+ wavelengths_nm = torch.linspace(450, 2300, 10, device=args.device)[None]
26
+ # Example: fixed full-EnMAP wavelength bounds. Use the same reference bounds
27
+ # for train/eval in your downstream pipeline; do not renormalize each subset.
28
+ wavelengths = (wavelengths_nm - 418.24) / (2445.53 - 418.24)
29
+ with torch.no_grad():
30
+ output = encoder(optical=image, optical_channel_wv=wavelengths,
31
+ spatial_resolution=30)
32
+ grid = output.last_hidden_state
33
+ assert torch.isfinite(grid).all()
34
+ print('Token grid:', tuple(grid.shape))
35
+ print('Global features:', tuple(grid[:, 0, 0].shape))
36
+ print('Spatial features:', tuple(grid[:, 0, 1:].shape))
37
+ print('Per-band patch features:', tuple(grid[:, 1:, 1:].shape))
38
+
39
+
40
+ if __name__ == '__main__':
41
+ main()
low_rank_attention.py ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+
3
+ import torch
4
+ import torch.nn as nn
5
+ import torch.nn.functional as F
6
+ import torch.utils.checkpoint
7
+
8
+ from timm.layers import use_fused_attn
9
+ from itertools import product
10
+ from typing import Optional
11
+
12
+ from .pos_chan_embed import rope_apply
13
+ import math
14
+
15
+ __all__ = ['AttentionPool', 'AttentionBranch', 'LowRankAttention', 'LowDimPool', 'get_perception_field_mask']
16
+
17
+ def get_perception_field_mask(num_patches, patch_size, spacial_resolution, attention_radius, cls_token=False):
18
+ """
19
+ Create a distance mask for the image.
20
+ spacial_resolution: the resolution of the image in meters/pixel
21
+ attention_radius: the radius of the attention in meters
22
+ """
23
+ points = list(product(range(int(math.sqrt(num_patches))), range(int(math.sqrt(num_patches)))))
24
+ idxs = []
25
+ points_array = torch.tensor(points).to(torch.float32)
26
+ distances = torch.cdist(points_array, points_array, p=2) * patch_size * spacial_resolution
27
+ idxs = (distances <= attention_radius).flatten().tolist()
28
+ mask = torch.tensor(idxs).to(torch.float32).reshape(num_patches, num_patches)
29
+ if cls_token:
30
+ # add a new row and column of ones to the mask for cls_token
31
+ new_row = torch.ones(num_patches, 1)
32
+ new_col = torch.ones(1, num_patches + 1)
33
+ mask = torch.cat([new_row, mask], dim=1)
34
+ mask = torch.cat([new_col, mask], dim=0)
35
+ # convert to boolean
36
+ mask = mask > 0 # HW HW
37
+ return mask
38
+
39
+ class AvgPool(nn.Module):
40
+ def __init__(self, dim: int, dim_out: int):
41
+ super().__init__()
42
+ self.linear = nn.Linear(dim, dim_out, bias=False)
43
+
44
+ def forward(self, x: torch.Tensor, mask: torch.Tensor = None) -> torch.Tensor:
45
+ """
46
+ Args:
47
+ x (torch.Tensor): input tensor of shape B C N D
48
+ mask (torch.Tensor): mask tensor of shape B N
49
+ """
50
+ B, C, N, D = x.shape
51
+ if mask is not None:
52
+ mask = mask.unsqueeze(1).unsqueeze(-1).expand(B, C, N, 1)
53
+ x = (x * mask).sum(dim=2) / mask.sum(dim=2)
54
+ else:
55
+ x = x.mean(dim=2)
56
+ x = self.linear(x)
57
+ return x # B C D
58
+
59
+ class ProjectionPool(nn.Module):
60
+ def __init__(self, dim: int, dim_out: int, norm_layer: nn.Module = nn.LayerNorm, **kwargs):
61
+ super().__init__()
62
+ self.linear = nn.Linear(dim, dim_out, bias=True)
63
+ self.norm = norm_layer(dim_out)
64
+
65
+ def forward(self, x: torch.Tensor, mask: torch.Tensor = None) -> torch.Tensor:
66
+ # x is B C N D
67
+ x = x[:, :, 0] # B C D
68
+ x = self.linear(x)
69
+ x = self.norm(x)
70
+ return x # B C D
71
+
72
+ class AttentionPool(nn.Module):
73
+ def __init__(
74
+ self,
75
+ dim: int,
76
+ num_heads: int = 1,
77
+ qkv_bias: bool = False,
78
+ qk_norm: bool = False,
79
+ attn_drop: float = 0.,
80
+ norm_layer: nn.Module = nn.LayerNorm,
81
+ proj_drop: float = 0.,
82
+ ) -> None:
83
+ """
84
+ AttentionPool module for low-rank attention in vision transformers.
85
+
86
+ This module implements a pooling mechanism using multi-head attention.
87
+ It processes input tensors by applying attention across the feature dimension,
88
+ resulting in a lower-dimensional output. This is particularly useful for
89
+ reducing the computational complexity in vision transformer architectures.
90
+
91
+ Args:
92
+ dim (int): Input dimension.
93
+ dim_out (int): Output dimension.
94
+ num_heads (int): Number of attention heads.
95
+ qkv_bias (bool): If True, add bias to query, key, value projections.
96
+ qk_norm (bool): If True, apply normalization to query and key.
97
+ attn_drop (float): Dropout rate for attention weights.
98
+ proj_drop (float): Dropout rate for output.
99
+ norm_layer (nn.Module): Normalization layer to use.
100
+
101
+ This method will use the cls token to pool the feature.
102
+ """
103
+ super().__init__()
104
+ assert dim % num_heads == 0, 'dim should be divisible by num_heads'
105
+ self.head_dim = dim // num_heads
106
+ self.num_heads = num_heads
107
+ self.dim = dim
108
+ self.dim_out = dim
109
+ self.scale = (dim // num_heads) ** -0.5 # Scaling factor for attention scores
110
+ self.fused_attn = use_fused_attn()
111
+
112
+ self.q = nn.Linear(dim, dim, bias=qkv_bias)
113
+ self.kv = nn.Linear(dim, dim*2, bias=qkv_bias)
114
+ self.q_norm = norm_layer(self.head_dim) if qk_norm else nn.Identity()
115
+ self.k_norm = norm_layer(self.head_dim) if qk_norm else nn.Identity()
116
+
117
+ self.attn_drop = nn.Dropout(attn_drop)
118
+
119
+ self.proj = nn.Linear(dim, dim)
120
+ self.proj_drop = nn.Dropout(proj_drop)
121
+
122
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
123
+ B, C, N, D = x.shape
124
+
125
+ x = x.reshape(B * C, N, D) # B*C, N+1, D
126
+ x_cls = x[:, :1, :] # B*C, 1, D
127
+
128
+ q = self.q(x_cls).reshape(B * C, 1, self.num_heads, self.head_dim).permute(0, 2, 1, 3) # B*C, num_heads, 1, head_dim
129
+ kv = self.kv(x).reshape(B * C, N, 2, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4) # 2, B*C, num_heads, N, head_dim
130
+ k, v = kv.unbind(0) # B*C, num_heads, N, head_dim
131
+ q, k = self.q_norm(q), self.k_norm(k)
132
+
133
+ if self.fused_attn:
134
+ x = F.scaled_dot_product_attention(
135
+ q, k, v,
136
+ dropout_p=self.attn_drop.p if self.training else 0.,
137
+ ) # B*C, num_heads, 1, D
138
+ else:
139
+ attn = (q @ k.transpose(-2, -1)) * self.scale # B*C, num_heads, N, 1
140
+ attn = attn.softmax(dim=-1)
141
+ attn = self.attn_drop(attn)
142
+ x = (attn @ v).transpose(1, 2) # B*C, num_heads, 1, D
143
+
144
+ x = x.reshape(B, C, self.dim) # B C dim, take only cls token
145
+
146
+ x = self.proj(x)
147
+ x = self.proj_drop(x)
148
+ return x
149
+
150
+ class LowDimPool(nn.Module):
151
+ def __init__(
152
+ self,
153
+ dim: int,
154
+ channel_dim: int,
155
+ spatial_dim: int,
156
+ num_heads: int = 8,
157
+ qkv_bias: bool = False,
158
+ qk_norm: bool = False,
159
+ attn_drop: float = 0.,
160
+ proj_drop: float = 0.,
161
+ norm_layer: nn.Module = nn.LayerNorm,
162
+ skip_pool: bool = False,
163
+ ) -> None:
164
+ super().__init__()
165
+ self.num_heads = num_heads
166
+
167
+ self.channel_norm = norm_layer(channel_dim)
168
+ self.spatial_norm = norm_layer(spatial_dim)
169
+ # self.norm = norm_layer(dim)
170
+
171
+ if skip_pool:
172
+ self.channel_pool = lambda x: x[:, :, 0]
173
+ self.spatial_pool = lambda x: x[:, :, 0]
174
+ else:
175
+ self.channel_pool = AttentionPool(dim=channel_dim, num_heads=num_heads, norm_layer=norm_layer, qkv_bias=qkv_bias, qk_norm=qk_norm, attn_drop=attn_drop, proj_drop=proj_drop) # B C N D -> B C dim
176
+ self.spatial_pool = AttentionPool(dim=spatial_dim, num_heads=num_heads, norm_layer=norm_layer, qkv_bias=qkv_bias, qk_norm=qk_norm, attn_drop=attn_drop, proj_drop=proj_drop) # B HW N D -> B HW dim
177
+
178
+ self.channel_linear = nn.Linear(dim, channel_dim) # B C dim -> B C channel_dim
179
+ self.spatial_linear = nn.Linear(dim, spatial_dim) # B HW dim -> B HW spatial_dim
180
+
181
+ def forward(self, x: torch.Tensor, pos_chan_embedding: torch.Tensor = None) -> torch.Tensor:
182
+ # x is B C HW D
183
+ B, C, HW, D = x.shape
184
+ x_c = self.channel_linear(x) # B, C, HW, channel_dim
185
+ x_s = self.spatial_linear(x) # B, C, HW, spatial_dim
186
+
187
+ x_c = x_c[:, :, 0] + self.channel_pool(self.channel_norm(x_c)) # B, C, dim
188
+ x_s = x_s[:, 0] + self.spatial_pool(self.spatial_norm(x_s.transpose(1, 2))) # B, HW, dim
189
+ return x_c, x_s, x
190
+
191
+ class AttentionBranch(nn.Module):
192
+ def __init__(
193
+ self,
194
+ dim: int,
195
+ num_heads: int,
196
+ head_dim: int,
197
+ qkv_bias: bool = False,
198
+ qk_norm: bool = False,
199
+ attn_drop: float = 0.,
200
+ norm_layer: nn.Module = nn.LayerNorm,
201
+ rank: int = 1,
202
+ ) -> None:
203
+ super().__init__()
204
+ assert dim % num_heads == 0, 'dim should be divisible by num_heads'
205
+ self.num_heads = num_heads
206
+ self.head_dim = head_dim
207
+ self.scale = head_dim ** -0.5
208
+ self.fused_attn = use_fused_attn()
209
+
210
+ self.qk = nn.Linear(dim, num_heads * head_dim * rank * 2, bias=qkv_bias)
211
+ self.v = nn.Linear(dim, num_heads * head_dim * rank, bias=qkv_bias)
212
+ self.q_norm = norm_layer(head_dim) if qk_norm else nn.Identity()
213
+ self.k_norm = norm_layer(head_dim) if qk_norm else nn.Identity()
214
+ self.attn_drop = nn.Dropout(attn_drop)
215
+ self.rank = rank
216
+
217
+ def apply_rope(self, q: torch.Tensor, k: torch.Tensor, sin: Optional[torch.Tensor], cos: Optional[torch.Tensor]):
218
+ if sin is None and cos is None:
219
+ return q, k
220
+
221
+ assert sin is not None and cos is not None, "Both sin and cos must be provided for RoPE."
222
+ q_dtype = q.dtype
223
+ k_dtype = k.dtype
224
+ rope_dtype = sin.dtype
225
+
226
+ q = q.to(dtype=rope_dtype)
227
+ k = k.to(dtype=rope_dtype)
228
+
229
+ q = rope_apply(q, sin, cos)
230
+ k = rope_apply(k, sin, cos)
231
+
232
+ q = q.to(dtype=q_dtype)
233
+ k = k.to(dtype=k_dtype)
234
+
235
+ return q, k
236
+
237
+ def forward(self, x: torch.Tensor, mask: torch.Tensor = None, sin: torch.Tensor = None, cos: torch.Tensor = None) -> torch.Tensor:
238
+ B, N, D = x.shape
239
+ v = self.v(x).reshape(B, N, self.num_heads, self.rank, self.head_dim).permute(0, 3, 2, 1, 4) # B, rank, num_heads, N, D
240
+ qk = self.qk(x).reshape(B, N, 2, self.num_heads, self.rank, self.head_dim).permute(2, 0, 4, 3, 1, 5) # 2, B, rank, num_heads, N, head_dim
241
+ q, k = qk.unbind(0)
242
+ q, k = self.apply_rope(q, k, sin, cos)
243
+ q, k = self.q_norm(q), self.k_norm(k) #B, rank, num_heads, N, head_dim
244
+
245
+ if self.fused_attn:
246
+ x = F.scaled_dot_product_attention(
247
+ q, k, v, attn_mask=mask,
248
+ dropout_p=self.attn_drop.p if self.training else 0.,
249
+ ) # B, rank, num_heads, N, D
250
+ else:
251
+ L, S = q.shape[-2], k.shape[-2]
252
+ attn = (q @ k.transpose(-2, -1)) * self.scale
253
+ if mask is not None:
254
+ atten_bias = torch.zeros(L, S, device=attn.device)
255
+ atten_bias = atten_bias.masked_fill(mask == 0, -torch.inf)
256
+ attn = attn + atten_bias
257
+ attn = attn.softmax(dim=-1)
258
+ attn = self.attn_drop(attn)
259
+ x = attn @ v # B, rank, num_heads, N, D
260
+
261
+ return x # B, rank, num_heads, N, D
262
+
263
+ class LowRankAttention(nn.Module):
264
+ def __init__(
265
+ self,
266
+ dim: int,
267
+ channel_dim: int,
268
+ spatial_dim: int,
269
+ num_heads: int = 8,
270
+ qkv_bias: bool = False,
271
+ qk_norm: bool = False,
272
+ attn_drop: float = 0.,
273
+ proj_drop: float = 0.,
274
+ norm_layer: nn.Module = nn.LayerNorm,
275
+ rank: int = 1,
276
+ ) -> None:
277
+ """
278
+ LowRankAttention module for low-rank attention in vision transformers.
279
+
280
+ This module implements a low-rank attention mechanism using multi-head attention.
281
+ It processes input tensors by applying attention across the feature dimension,
282
+ resulting in a lower-dimensional output. This is particularly useful for
283
+ reducing the computational complexity in vision transformer architectures.
284
+
285
+ Args:
286
+ dim (int): Input dimension.
287
+ channel_dim (int): Channel dimension.
288
+ spatial_dim (int): Spatial dimension.
289
+ num_heads (int): Number of attention heads.
290
+ qkv_bias (bool): If True, add bias to query, key, value projections.
291
+ qk_norm (bool): If True, apply normalization to query and key.
292
+ attn_drop (float): Dropout rate for attention weights.
293
+ proj_drop (float): Dropout rate for output.
294
+ norm_layer (nn.Module): Normalization layer to use.
295
+ dim_ratio (float): Dimension ratio for low-rank approximation.
296
+ pool (bool): If True, use pooling.
297
+ """
298
+ super().__init__()
299
+ assert channel_dim % num_heads == 0, 'channel_dim should be divisible by num_heads'
300
+ assert spatial_dim % num_heads == 0, 'spatial_dim should be divisible by num_heads'
301
+
302
+ self.num_heads = num_heads
303
+ self.dim = dim
304
+ self.channel_dim = channel_dim
305
+ self.spatial_dim = spatial_dim
306
+
307
+ self.c_head_dim = self.channel_dim // num_heads
308
+ self.s_head_dim = self.spatial_dim // num_heads
309
+ self.head_dim = self.dim // num_heads
310
+
311
+ self.rank = rank
312
+
313
+ assert self.head_dim == self.c_head_dim * self.s_head_dim, 'head_dim should be equal to c_head_dim * s_head_dim'
314
+
315
+ self.channel_branch = AttentionBranch(channel_dim, num_heads, self.c_head_dim, qkv_bias, qk_norm, attn_drop, norm_layer, rank)
316
+ self.spatial_branch = AttentionBranch(spatial_dim, num_heads, self.s_head_dim, qkv_bias, qk_norm, attn_drop, norm_layer, rank)
317
+
318
+ self.proj = nn.Linear(dim, dim)
319
+ self.proj_drop = nn.Dropout(proj_drop)
320
+
321
+ def forward(self, x_c: torch.Tensor, x_s: torch.Tensor, spatial_mask: torch.Tensor = None, rope: tuple = None) -> torch.Tensor:
322
+ B, C, D = x_c.shape
323
+ HW = x_s.shape[1]
324
+
325
+ sin_hw, cos_hw, sin_c, cos_c = rope if rope is not None else (None, None, None, None)
326
+
327
+ xc = self.channel_branch(x_c, sin=sin_c, cos=cos_c) # B, rank, num_heads, C, c_head_dim
328
+ xs = self.spatial_branch(x_s, spatial_mask, sin=sin_hw, cos=cos_hw) # B, rank, num_heads, HW, s_head_dim
329
+ x = torch.einsum('...ca,...nb->...cnab', xc, xs).flatten(-2) # B, rank, num_heads, C, HW, D
330
+ x = x.sum(dim=1) / self.rank # B, num_heads, C, HW, D
331
+ x = x.permute(0, 2, 3, 1, 4).reshape(B, C, HW, -1) # B, C, HW, D
332
+ x = self.proj(x) # B, C, HW, D
333
+ x = self.proj_drop(x) # B, C, HW, D
334
+
335
+ return x
336
+
337
+ # LayerScale and LowRankBlock moved to attention_ops.py, which composes this
338
+ # module's AttentionBranch/LowDimPool/LowRankAttention (arm A) alongside the
339
+ # arm B/C attention operators behind a single build_attention(attn_type, ...)
340
+ # factory.
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:935719728265cf0765b32afe634803ab73071025f104c1c708b98142488b8311
3
+ size 332935996
modeling_lessvit.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hugging Face interface for the original LESSViT encoder."""
2
+ # Explicit dependencies let Transformers copy the full local/remote module tree.
3
+ from .pos_chan_embed import RopePositionChannelEmbedding
4
+ from .hyperspectral_patch_embed import HyperspectralPatchEmbed
5
+ from .low_rank_attention import LowRankAttention
6
+ from .attention_ops import LowRankBlock
7
+ from .spatial_spectral_low_rank_vit import (
8
+ SpatialSpectralLowRankViTConfig,
9
+ SpatialSpectralLowRankViTEncoder,
10
+ )
11
+
12
+
13
+ class LESSViTConfig(SpatialSpectralLowRankViTConfig):
14
+ model_type = "lessvit"
15
+
16
+ def __init__(self, channel_embed_dims_per_head=2, **kwargs):
17
+ # The original config constructor recomputes these derived dimensions.
18
+ kwargs.pop("channel_dim", None)
19
+ kwargs.pop("spatial_dim", None)
20
+ kwargs.pop("pretrain", None)
21
+ kwargs.pop("mask_ratio", None)
22
+ kwargs.pop("channel_mask_ratio", None)
23
+ kwargs.pop("channel_dropout", None)
24
+ super().__init__(channel_embed_dims_per_head=channel_embed_dims_per_head,
25
+ mask_ratio=0.0, channel_mask_ratio=0.0,
26
+ channel_dropout=None, **kwargs)
27
+ self.channel_embed_dims_per_head = channel_embed_dims_per_head
28
+ self.pretrain = False
29
+
30
+
31
+ class LESSViTModel(SpatialSpectralLowRankViTEncoder):
32
+ config_class = LESSViTConfig
33
+ main_input_name = "optical"
34
+
35
+ def __init__(self, config):
36
+ super().__init__(config)
37
+ self.post_init()
pos_chan_embed.py ADDED
@@ -0,0 +1,361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+
4
+ # This source code is licensed under the license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+ # --------------------------------------------------------
7
+ # Position embedding utils
8
+ # --------------------------------------------------------
9
+
10
+ from typing import Any
11
+ import numpy as np
12
+
13
+ import math
14
+ import torch
15
+ import torch.nn as nn
16
+ from torch import Tensor, nn
17
+ from typing import Literal
18
+
19
+
20
+ class PositionalChannelEmbedding():
21
+ def __init__(self, embed_dim):
22
+ self.embed_dim = embed_dim
23
+ self.spactial_resolution = None
24
+ self.channel_ids = None
25
+ self.num_patches = None
26
+ self.pos_embed = None
27
+ self.channel_embed = None
28
+ self.cls_token = torch.zeros(1, 1, 1, embed_dim)
29
+
30
+
31
+ def interpolate_pos_channel_embed(self, pos_embed: torch.Tensor, channel_embed: torch.Tensor, cls_token: bool = False):
32
+ """
33
+ pos_embed: (1, HW, D)
34
+ channel_embed: (1, C, D)
35
+ return pos_embed: (1, C+1, HW+1, D) if cls_token else (1, C, HW, D)
36
+ """
37
+ n_chan = channel_embed.shape[1] # C
38
+ n_pos = pos_embed.shape[1] # HW
39
+
40
+ if cls_token:
41
+ # Add cls token embeddings
42
+ pos_cls_embed = torch.zeros(1, 1, self.embed_dim, device=pos_embed.device)
43
+ pos_embed = torch.cat([pos_cls_embed, pos_embed], dim=1) # (1, HW+1, D)
44
+
45
+ chan_cls_embed = torch.zeros(1, 1, self.embed_dim, device=channel_embed.device)
46
+ channel_embed = torch.cat([chan_cls_embed, channel_embed], dim=1) # (1, C+1, D)
47
+
48
+ n_chan += 1
49
+ n_pos += 1
50
+
51
+ interpolated_pos_embed = pos_embed.unsqueeze(1).repeat(1, n_chan, 1, 1) # 1 C HW D
52
+ interpolated_channel_embed = channel_embed.unsqueeze(2).repeat(1, 1, n_pos, 1) # 1 C HW D
53
+
54
+ pos_channel_embed = (interpolated_channel_embed + interpolated_pos_embed) / 2 # 1 C HW D
55
+
56
+ return pos_channel_embed
57
+
58
+ def get_pos_embed(self, tokens: torch.Tensor, spatial_resolution: float, cls_token: bool = True):
59
+ _, HW, _ = tokens.shape
60
+ grid_size = int(np.sqrt(HW))
61
+ assert grid_size * grid_size == HW, "HW must be a square"
62
+
63
+ if self.spactial_resolution == spatial_resolution and self.num_patches == HW:
64
+ pos_embed = self.pos_embed
65
+ else:
66
+ pos_embed = get_2d_sincos_pos_embed(self.embed_dim, grid_size, spatial_resolution, cls_token=False) # (1, HW, D)
67
+ self.pos_embed = pos_embed
68
+ self.spactial_resolution = spatial_resolution
69
+ self.num_patches = HW
70
+
71
+ if cls_token:
72
+ pos_embed = torch.cat([torch.zeros_like(pos_embed[:, :1, :]), pos_embed], dim=1)
73
+
74
+ return pos_embed
75
+
76
+ def get_channel_embed(self, tokens: torch.Tensor, channel_ids: torch.Tensor, cls_token: bool = True):
77
+ """
78
+ channel_ids: (1, C)
79
+ return channel_embed: (1, C, D)
80
+ """
81
+ _, C, _ = tokens.shape
82
+ channel_ids = channel_ids.squeeze(0).detach().cpu().numpy()
83
+ if self.channel_ids is not None and tuple(self.channel_ids) == tuple(channel_ids):
84
+ channel_embed = self.channel_embed
85
+ else:
86
+ channel_embed = get_1d_sincos_channel_embed(self.embed_dim, channel_ids, cls_token=False) # (1, C, D)
87
+ self.channel_embed = channel_embed
88
+ self.channel_ids = channel_ids
89
+
90
+ if cls_token:
91
+ channel_embed = torch.cat([torch.zeros_like(channel_embed[:, :1, :]), channel_embed], dim=1)
92
+ return channel_embed
93
+
94
+
95
+ def __call__(self, tokens: torch.Tensor, spatial_resolution: float, channel_ids: torch.Tensor, cls_token: bool = True):
96
+ _, C, HW, _ = tokens.shape
97
+ assert channel_ids.shape == (1, C), "channel_ids must be the same length as the number of channels"
98
+ grid_size = int(np.sqrt(HW))
99
+ assert grid_size * grid_size == HW, "HW must be a square"
100
+ channel_ids = channel_ids.squeeze(0).detach().cpu().numpy()
101
+
102
+ if self.channel_ids is not None and tuple(self.channel_ids) == tuple(channel_ids):
103
+ channel_embed = self.channel_embed
104
+ else:
105
+ channel_embed = get_1d_sincos_channel_embed(self.embed_dim, channel_ids, cls_token=False) # (1, C, D)
106
+ self.channel_embed = channel_embed
107
+ self.channel_ids = channel_ids
108
+
109
+ if self.spactial_resolution == spatial_resolution and self.num_patches == HW:
110
+ pos_embed = self.pos_embed
111
+ else:
112
+ pos_embed = get_2d_sincos_pos_embed(self.embed_dim, grid_size, spatial_resolution, cls_token=False) # (1, HW, D)
113
+ self.pos_embed = pos_embed
114
+ self.spactial_resolution = spatial_resolution
115
+ self.num_patches = HW
116
+
117
+ return self.interpolate_pos_channel_embed(pos_embed, channel_embed, cls_token)
118
+
119
+ # --------------------------------------------------------
120
+ # 2D sine-cosine position embedding
121
+ # References:
122
+ # Transformer: https://github.com/tensorflow/models/blob/master/official/nlp/transformer/model_utils.py
123
+ # MoCo v3: https://github.com/facebookresearch/moco-v3
124
+ # --------------------------------------------------------
125
+ def get_2d_sincos_pos_embed(embed_dim, grid_size, spactial_resolution, cls_token=False):
126
+ """
127
+ grid_size: int of the grid height and width
128
+ return:
129
+ pos_embed: [grid_size*grid_size, embed_dim] or [1+grid_size*grid_size, embed_dim] (w/ or w/o cls_token)
130
+ """
131
+ grid_h = np.arange(grid_size, dtype=np.float32)
132
+ grid_w = np.arange(grid_size, dtype=np.float32)
133
+ grid = np.meshgrid(grid_w, grid_h) # here w goes first
134
+ grid = np.stack(grid, axis=0) * spactial_resolution
135
+
136
+ grid = grid.reshape([2, 1, grid_size, grid_size])
137
+ pos_embed = get_2d_sincos_pos_embed_from_grid(embed_dim, grid)
138
+ if cls_token:
139
+ pos_embed = np.concatenate([np.zeros([1, embed_dim]), pos_embed], axis=0)
140
+ pos_embed = torch.from_numpy(pos_embed).float().unsqueeze(0)
141
+ return pos_embed
142
+
143
+ def get_1d_sincos_channel_embed(embed_dim, channel_idx, cls_token=False):
144
+ """
145
+ embed_dim: output dimension for each position
146
+ channel_idx: a list of channel_idx to be encoded: size (C,)
147
+ out: (C, D)
148
+ """
149
+ channel_embed = get_1d_sincos_pos_embed_from_grid(embed_dim, channel_idx)
150
+ if cls_token:
151
+ channel_embed = np.concatenate([np.zeros([1, embed_dim]), channel_embed], axis=0)
152
+ channel_embed = torch.from_numpy(channel_embed).float().unsqueeze(0)
153
+ return channel_embed
154
+
155
+
156
+ def get_2d_sincos_pos_embed_from_grid(embed_dim, grid):
157
+ assert embed_dim % 2 == 0
158
+
159
+ # use half of dimensions to encode grid_h
160
+ emb_h = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[0]) # (H*W, D/2)
161
+ emb_w = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[1]) # (H*W, D/2)
162
+
163
+ emb = np.concatenate([emb_h, emb_w], axis=1) # (H*W, D)
164
+ return emb
165
+
166
+
167
+ def get_1d_sincos_pos_embed_from_grid(embed_dim, pos):
168
+ """
169
+ embed_dim: output dimension for each position
170
+ pos: a list of positions to be encoded: size (M,)
171
+ out: (M, D)
172
+ """
173
+ assert embed_dim % 2 == 0
174
+ omega = np.arange(embed_dim // 2, dtype=np.float32)
175
+ omega /= embed_dim / 2.
176
+ omega = 1. / 10000**omega # (D/2,)
177
+
178
+ pos = pos.reshape(-1) # (M,)
179
+ out = np.einsum('m,d->md', pos, omega) # (M, D/2), outer product
180
+
181
+ emb_sin = np.sin(out) # (M, D/2)
182
+ emb_cos = np.cos(out) # (M, D/2)
183
+
184
+ emb = np.concatenate([emb_sin, emb_cos], axis=1) # (M, D)
185
+ return emb
186
+
187
+
188
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
189
+ #
190
+ # This software may be used and distributed in accordance with
191
+ # the terms of the DINOv3 License Agreement.
192
+ # --------------------------------------------------------
193
+ # RoPE positional embedding with no mixing of coordinates (axial) and no learnable weights
194
+ # Supports two parametrizations of the rope parameters: either using `base` or `min_period` and `max_period`.
195
+ class RopePositionChannelEmbedding(nn.Module):
196
+ def __init__(
197
+ self,
198
+ # embed_dim: int,
199
+ spatial_dim: int,
200
+ channel_dim: int,
201
+ *,
202
+ num_heads: int,
203
+ base: float | None = 100.0,
204
+ min_period: float | None = None,
205
+ max_period: float | None = None,
206
+ normalize_coords: Literal["min", "max", "separate"] = "separate",
207
+ shift_coords: float | None = None,
208
+ jitter_coords: float | None = None,
209
+ rescale_coords: float | None = None,
210
+ dtype: torch.dtype | None = None,
211
+ device: torch.device | None = None,
212
+ ):
213
+ super().__init__()
214
+ assert spatial_dim % (4 * num_heads) == 0 and spatial_dim % (4 * num_heads) == 0
215
+ both_periods = min_period is not None and max_period is not None
216
+ if (base is None and not both_periods) or (base is not None and both_periods):
217
+ raise ValueError("Either `base` or `min_period`+`max_period` must be provided.")
218
+
219
+ D_head_hw = spatial_dim // num_heads
220
+ D_head_c = channel_dim // num_heads
221
+ self.base = base
222
+ self.min_period = min_period
223
+ self.max_period = max_period
224
+ self.D_head_hw = D_head_hw
225
+ self.D_head_c = D_head_c
226
+ self.normalize_coords = normalize_coords
227
+ self.shift_coords = shift_coords
228
+ self.jitter_coords = jitter_coords
229
+ self.rescale_coords = rescale_coords
230
+
231
+ # Needs persistent=True because we do teacher.load_state_dict(student.state_dict()) to initialize the teacher
232
+ self.dtype = dtype # Don't rely on self.periods.dtype
233
+ self.register_buffer(
234
+ "periods_hw",
235
+ torch.empty(D_head_hw // 4, device=device, dtype=dtype),
236
+ persistent=True,
237
+ )
238
+ self.register_buffer(
239
+ "periods_c",
240
+ torch.empty(D_head_c // 2, device=device, dtype=dtype),
241
+ persistent=True,
242
+ )
243
+ self._init_weights()
244
+
245
+ def forward(self, *, H: int, W: int, C: int, optical_channel_wv) -> list[Tensor, ...]:
246
+ device = self.periods_hw.device
247
+ dtype = self.dtype
248
+ dd = {"device": device, "dtype": dtype}
249
+
250
+ # Prepare coords in range [-1, +1]
251
+ if self.normalize_coords == "max":
252
+ max_HW = max(H, W)
253
+ coords_h = torch.arange(0.5, H, **dd) / max_HW # [H]
254
+ coords_w = torch.arange(0.5, W, **dd) / max_HW # [W]
255
+ elif self.normalize_coords == "min":
256
+ min_HW = min(H, W)
257
+ coords_h = torch.arange(0.5, H, **dd) / min_HW # [H]
258
+ coords_w = torch.arange(0.5, W, **dd) / min_HW # [W]
259
+ elif self.normalize_coords == "separate":
260
+ coords_h = torch.arange(0.5, H, **dd) / H # [H]
261
+ coords_w = torch.arange(0.5, W, **dd) / W # [W]
262
+ else:
263
+ raise ValueError(f"Unknown normalize_coords: {self.normalize_coords}")
264
+ coords_hw = torch.stack(torch.meshgrid(coords_h, coords_w, indexing="ij"), dim=-1) # [H, W, 2]
265
+ coords_hw = coords_hw.flatten(0, 1) # [HW, 2]
266
+ coords_hw = 2.0 * coords_hw - 1.0 # Shift range [0, 1] to [-1, +1]
267
+
268
+ # coords_c = torch.arange(0.5, C, **dd) / C # [C]
269
+ # coords_c = 2.0 * coords_c - 1.0
270
+ coords_c = 2 * optical_channel_wv.squeeze() - 1.0 # [C]
271
+
272
+ # Shift coords by adding a uniform value in [-shift, shift]
273
+ if self.training and self.shift_coords is not None:
274
+ shift_hw = torch.empty(2, **dd).uniform_(-self.shift_coords, self.shift_coords)
275
+ coords_hw += shift_hw[None, :]
276
+
277
+ shift_c = torch.empty(1, **dd).uniform_(-self.shift_coords, self.shift_coords)
278
+ coords_c += shift_c
279
+
280
+ # Jitter coords by multiplying the range [-1, 1] by a log-uniform value in [1/jitter, jitter]
281
+ if self.training and self.jitter_coords is not None:
282
+ jitter_max = np.log(self.jitter_coords)
283
+ jitter_min = -jitter_max
284
+ jitter_hw = torch.empty(2, **dd).uniform_(jitter_min, jitter_max).exp()
285
+ coords_hw *= jitter_hw[None, :]
286
+
287
+ jitter_c = torch.empty(1, **dd).uniform_(jitter_min, jitter_max).exp()
288
+ coords_c *= jitter_c
289
+
290
+ # Rescale coords by multiplying the range [-1, 1] by a log-uniform value in [1/rescale, rescale]
291
+ if self.training and self.rescale_coords is not None:
292
+ rescale_max = np.log(self.rescale_coords)
293
+ rescale_min = -rescale_max
294
+ rescale = torch.empty(1, **dd).uniform_(rescale_min, rescale_max).exp()
295
+ coords_hw *= rescale_hw
296
+ coords_c *= rescale_c
297
+
298
+ # Prepare angles and sin/cos
299
+ angles_hw = 2 * math.pi * coords_hw[:, :, None] / self.periods_hw[None, None, :] # [HW, 2, D//4]
300
+ angles_hw = angles_hw.flatten(1, 2) # [HW, D//2]
301
+ angles_hw = angles_hw.tile(2) # [HW, D]
302
+ cos_hw = torch.cos(angles_hw) # [HW, D]
303
+ sin_hw = torch.sin(angles_hw) # [HW, D]
304
+
305
+ angles_c = 2 * math.pi * coords_c[:, None] / self.periods_c[None, :] # [C, D//2]
306
+ angles_c = angles_c.tile(2) # [C, D]
307
+ cos_c = torch.cos(angles_c) # [C, D]
308
+ sin_c = torch.sin(angles_c) # [C, D]
309
+
310
+ # rope = {
311
+ # 'spatial_rope': (sin_hw, cos_hw), # 2 * [HW, D]
312
+ # 'spectral_rope': (sin_c, cos_c) # 2 * [C, D]
313
+ # }
314
+ # return (sin, cos) # 2 * [HW, D]
315
+ return [sin_hw, cos_hw, sin_c, cos_c]
316
+
317
+ def _init_weights(self):
318
+ device = self.periods_hw.device
319
+ dtype = self.dtype
320
+ if self.base is not None:
321
+ periods_hw = self.base ** (
322
+ 2 * torch.arange(self.D_head_hw // 4, device=device, dtype=dtype) / (self.D_head_hw // 2)
323
+ ) # [D//4]
324
+ else:
325
+ base = self.max_period / self.min_period
326
+ exponents = torch.linspace(0, 1, self.D_head_hw // 4, device=device, dtype=dtype) # [D//4] range [0, 1]
327
+ periods_hw = base**exponents # range [1, max_period / min_period]
328
+ periods_hw = periods_hw / base # range [min_period / max_period, 1]
329
+ periods_hw = periods_hw * self.max_period # range [min_period, max_period]
330
+
331
+ if self.base is not None:
332
+ periods_c = self.base ** (
333
+ 2 * torch.arange(self.D_head_c // 2, device=device, dtype=dtype) / (self.D_head_c)
334
+ ) # [D//2]
335
+ else:
336
+ base = self.max_period / self.min_period
337
+ exponents = torch.linspace(0, 1, self.D_head_c // 2, device=device, dtype=dtype)
338
+ periods_c = base**exponents
339
+ periods_c = periods_c / base
340
+ periods_c = periods_c * self.max_period
341
+
342
+ self.periods_hw.data = periods_hw
343
+ self.periods_c.data = periods_c
344
+
345
+ # RoPE-related functions:
346
+ def rope_rotate_half(x: Tensor) -> Tensor:
347
+ # x: [ x0 x1 x2 x3 x4 x5]
348
+ # out: [-x3 -x4 -x5 x0 x1 x2]
349
+ x1, x2 = x.chunk(2, dim=-1)
350
+ return torch.cat([-x2, x1], dim=-1)
351
+
352
+
353
+ def rope_apply(x: Tensor, sin: Tensor, cos: Tensor) -> Tensor:
354
+ # x: [..., D], eg [x0, x1, x2, x3, x4, x5]
355
+ # sin: [..., D], eg [sin0, sin1, sin2, sin0, sin1, sin2]
356
+ # cos: [..., D], eg [cos0, cos1, cos2, cos0, cos1, cos2]
357
+ x_ = x[:, :, :, 1:, :]
358
+ # print(x_.shape, cos.unsqueeze(1).unsqueeze(1).shape)
359
+ x_ = (x_ * cos.unsqueeze(1).unsqueeze(1)) + (rope_rotate_half(x_) * sin.unsqueeze(1).unsqueeze(1))
360
+ x = torch.cat((x[:, :, :, :1, :], x_), dim=3)
361
+ return x
pretraining/architecture.json ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "SpatialSpectralMAEViT"
4
+ ],
5
+ "attention_radius": 640,
6
+ "attn_drop": 0.1,
7
+ "channel_dim": 24,
8
+ "channel_dropout": [
9
+ 0.75,
10
+ 0.85
11
+ ],
12
+ "channel_mask_ratio": 0.75,
13
+ "decoder_attention_radius": [
14
+ 640
15
+ ],
16
+ "decoder_channel_dim": 32,
17
+ "decoder_depth": 8,
18
+ "decoder_embed_dim": 512,
19
+ "decoder_history_length": [
20
+ 4
21
+ ],
22
+ "decoder_num_heads": 16,
23
+ "decoder_out_chans": 155,
24
+ "decoder_out_dims": 1,
25
+ "decoder_spatial_dim": 256,
26
+ "decoder_temporal_embed_dims_per_head": 4,
27
+ "depth": 12,
28
+ "drop_path_rate": 0.1,
29
+ "drop_path_uniform": false,
30
+ "embed_dim": 768,
31
+ "init_values": 1.0,
32
+ "mask_ratio": 0.75,
33
+ "mlp_ratio": 4.0,
34
+ "model_type": "multi_modal_low_rank_vit",
35
+ "norm_pix_loss": false,
36
+ "num_heads": 12,
37
+ "patch_size": 16,
38
+ "pos_chan_embed_residual": true,
39
+ "pretrain": true,
40
+ "proj_drop": 0.1,
41
+ "qk_norm": false,
42
+ "qkv_bias": true,
43
+ "rank": 1,
44
+ "return_dict": false,
45
+ "rope_embed_base": 100.0,
46
+ "spatial_dim": 384,
47
+ "use_perception_field_mask": true,
48
+ "use_rope_embed": true,
49
+ "channel_embed_dims_per_head": 2
50
+ }
pretraining/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8e5dce5229b24255d31dc619c005ad6355784eff7ee9e07036aba6f36a1c606a
3
+ size 430541280
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ torch==2.7.1
2
+ torchvision==0.22.1
3
+ transformers==4.53.3
4
+ huggingface-hub==0.34.4
5
+ timm==1.0.18
6
+ numpy==2.2.6
7
+ safetensors>=0.4.3,<1
spatial_spectral_low_rank_vit.py ADDED
@@ -0,0 +1,704 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import random
3
+ import torch.nn as nn
4
+ from transformers import PretrainedConfig, PreTrainedModel
5
+ from transformers.modeling_outputs import BaseModelOutput
6
+ from typing import Optional, Tuple, Dict, Any, List
7
+ from functools import partial
8
+ import numpy as np
9
+ from .hyperspectral_patch_embed import HyperspectralPatchEmbed
10
+ from .pos_chan_embed import PositionalChannelEmbedding, RopePositionChannelEmbedding
11
+ from .low_rank_attention import get_perception_field_mask
12
+ from .attention_ops import LowRankBlock
13
+ import torch.nn.functional as F
14
+ import math
15
+
16
+ class SpatialSpectralLowRankViTConfig(PretrainedConfig):
17
+ model_type = "multi_modal_low_rank_vit"
18
+
19
+ def __init__(
20
+ self,
21
+ patch_size: int = 16,
22
+ embed_dim: int = 768,
23
+ channel_embed_dims_per_head: int = 4,
24
+ depth: int = 12,
25
+ num_heads: int = 12,
26
+ mlp_ratio: float = 4.0,
27
+ qkv_bias: bool = True,
28
+ qk_norm: bool = False,
29
+ drop_path_rate: float = 0.0,
30
+ drop_path_uniform: bool = False,
31
+ init_values: Optional[float] = None,
32
+ attn_drop: float = 0.0,
33
+ proj_drop: float = 0.0,
34
+ mask_ratio: float = 0.75,
35
+ channel_mask_ratio: float = 0.5,
36
+ pos_chan_embed_residual: bool = True,
37
+ rank: int = 1,
38
+ # Decoder-specific parameters
39
+ decoder_embed_dim: int = 512,
40
+ decoder_depth: int = 8,
41
+ decoder_channel_embed_dims_per_head: int = 4,
42
+ decoder_num_heads: int = 16,
43
+ decoder_out_chans: int = 1,
44
+ # return dict
45
+ return_dict: bool = False,
46
+ norm_pix_loss: bool = True,
47
+ use_perception_field_mask: bool = False,
48
+ attention_radius: int = 640,
49
+ use_rope_embed: bool = False,
50
+ rope_embed_base: float = 100.0,
51
+ channel_dropout: Optional[List[float]] = None,
52
+ classes: Optional[int] = None,
53
+ attn_type: str = "less",
54
+ fusion_init_scale: float = 1.0,
55
+ **kwargs
56
+ ):
57
+ super().__init__(**kwargs)
58
+ assert attn_type in ("less", "full", "additive"), f"Unknown attn_type: {attn_type!r}"
59
+ self.attn_type = attn_type
60
+ self.fusion_init_scale = fusion_init_scale
61
+ self.patch_size = patch_size
62
+ self.embed_dim = embed_dim
63
+ # arm C (additive) reuses arm A's channel_dim/spatial_dim derivation
64
+ # unchanged -- it keeps the same low-rank branch widths as arm A and only
65
+ # projects up to head_dim at the fusion step (see
66
+ # AdditiveSpatialSpectralAttention), so no attn_type-specific override is
67
+ # needed here (unlike an earlier version of this design).
68
+ self.channel_dim = channel_embed_dims_per_head * num_heads
69
+ self.spatial_dim = embed_dim // self.channel_dim * num_heads
70
+ # Joint-RoPE head-dim partition for attn_type == "full": splits head_dim
71
+ # (embed_dim // num_heads) into a spatial (u,v) sub-block and a spectral
72
+ # (lambda) sub-block, default 50/50. Unused by "less"/"additive".
73
+ head_dim = embed_dim // num_heads
74
+ rope_spatial_split = head_dim // 2
75
+ rope_spectral_split = head_dim - rope_spatial_split
76
+ self.rope_spatial_split_dim = rope_spatial_split * num_heads
77
+ self.rope_channel_split_dim = rope_spectral_split * num_heads
78
+ self.depth = depth
79
+ self.num_heads = num_heads
80
+ self.mlp_ratio = mlp_ratio
81
+ self.qkv_bias = qkv_bias
82
+ self.qk_norm = qk_norm
83
+ self.drop_path_rate = drop_path_rate
84
+ self.drop_path_uniform = drop_path_uniform
85
+ self.init_values = init_values
86
+ self.attn_drop = attn_drop
87
+ self.proj_drop = proj_drop
88
+ self.num_tokens = 1
89
+ self.return_dict = return_dict
90
+ self.mask_ratio = mask_ratio
91
+ self.channel_mask_ratio = channel_mask_ratio
92
+ self.pretrain = True
93
+ self.rank = rank
94
+ # Decoder-specific attributes
95
+ self.decoder_embed_dim = decoder_embed_dim
96
+ self.decoder_channel_dim = decoder_channel_embed_dims_per_head * decoder_num_heads
97
+ self.decoder_spatial_dim = decoder_embed_dim // self.decoder_channel_dim * decoder_num_heads
98
+ decoder_head_dim = decoder_embed_dim // decoder_num_heads
99
+ decoder_rope_spatial_split = decoder_head_dim // 2
100
+ decoder_rope_spectral_split = decoder_head_dim - decoder_rope_spatial_split
101
+ self.decoder_rope_spatial_split_dim = decoder_rope_spatial_split * decoder_num_heads
102
+ self.decoder_rope_channel_split_dim = decoder_rope_spectral_split * decoder_num_heads
103
+ self.decoder_depth = decoder_depth
104
+ self.decoder_num_heads = decoder_num_heads
105
+ self.decoder_out_chans = decoder_out_chans
106
+
107
+ # MAE-specific attributes
108
+ self.norm_pix_loss = norm_pix_loss
109
+
110
+ # Perception field mask
111
+ self.use_perception_field_mask = use_perception_field_mask
112
+ self.attention_radius = attention_radius
113
+
114
+ # Positional channel embedding residual
115
+ self.pos_chan_embed_residual = pos_chan_embed_residual
116
+
117
+ # RoPe embedding
118
+ self.use_rope_embed = use_rope_embed
119
+ self.rope_embed_base = rope_embed_base
120
+
121
+ self.channel_dropout = channel_dropout
122
+
123
+ @property
124
+ def encoder_config(self):
125
+ return {k: v for k, v in self.__dict__.items() if not k.startswith('decoder_')}
126
+
127
+ @property
128
+ def decoder_config(self):
129
+ return {
130
+ 'embed_dim': self.decoder_embed_dim,
131
+ 'depth': self.decoder_depth,
132
+ 'num_heads': self.decoder_num_heads,
133
+ 'out_chans': self.decoder_out_chans,
134
+ 'mlp_ratio': self.mlp_ratio,
135
+ 'qkv_bias': self.qkv_bias,
136
+ 'qk_norm': self.qk_norm,
137
+ 'drop_path_rate': self.drop_path_rate,
138
+ 'drop_path_uniform': self.drop_path_uniform,
139
+ 'init_values': self.init_values,
140
+ 'attn_drop': self.attn_drop,
141
+ 'proj_drop': self.proj_drop,
142
+ 'channel_dim': self.channel_dim,
143
+ 'spatial_dim': self.spatial_dim,
144
+ 'patch_size': self.patch_size,
145
+ 'return_dict': self.return_dict,
146
+ }
147
+
148
+ class SpatialSpectralLowRankViTEncoder(PreTrainedModel):
149
+ config_class = SpatialSpectralLowRankViTConfig
150
+
151
+ def __init__(self, config: SpatialSpectralLowRankViTConfig):
152
+ super().__init__(config)
153
+ self.config = config
154
+ norm_layer = partial(nn.LayerNorm, eps=1e-6)
155
+
156
+ # Patch embedding layers for optical and radar inputs
157
+ self.optical_patch_embed = HyperspectralPatchEmbed(config.patch_size, config.embed_dim)
158
+ self.radar_patch_embed = HyperspectralPatchEmbed(config.patch_size, config.embed_dim)
159
+
160
+ # Learnable tokens and embeddings
161
+ self.cls_token = nn.Parameter(torch.zeros(1, 1, config.embed_dim))
162
+ self.spatial_cls_token = nn.Parameter(torch.zeros(1, 1, config.embed_dim))
163
+ self.channel_cls_token = nn.Parameter(torch.zeros(1, 1, config.embed_dim))
164
+ if not config.use_rope_embed:
165
+ self.pos_chan_embed = PositionalChannelEmbedding(config.embed_dim)
166
+ else:
167
+ # attn_type == "full" rotates a joint head-dim split (see
168
+ # FullSpatialSpectralAttention) rather than arms A/C's per-branch widths.
169
+ rope_spatial_dim = config.rope_spatial_split_dim if config.attn_type == "full" else config.spatial_dim
170
+ rope_channel_dim = config.rope_channel_split_dim if config.attn_type == "full" else config.channel_dim
171
+ self.pos_chan_embed = RopePositionChannelEmbedding(
172
+ # embed_dim=config.embed_dim,
173
+ spatial_dim=rope_spatial_dim,
174
+ channel_dim=rope_channel_dim,
175
+ num_heads=config.num_heads,
176
+ base=config.rope_embed_base if hasattr(config, 'rope_embed_base') else 100.0
177
+ )
178
+
179
+ if config.drop_path_uniform is True:
180
+ dpr = [config.drop_path_rate] * config.depth
181
+ else:
182
+ dpr = [x.item() for x in torch.linspace(0, config.drop_path_rate, config.depth)]
183
+
184
+ # Create transformer blocks
185
+ self.blocks = nn.ModuleList([
186
+ LowRankBlock(
187
+ dim=config.embed_dim,
188
+ num_heads=config.num_heads,
189
+ channel_dim=config.channel_dim,
190
+ spatial_dim=config.spatial_dim,
191
+ mlp_ratio=config.mlp_ratio,
192
+ qkv_bias=config.qkv_bias,
193
+ qk_norm=config.qk_norm,
194
+ proj_drop=config.proj_drop,
195
+ attn_drop=config.attn_drop,
196
+ drop_path=dpr[i],
197
+ init_values=config.init_values,
198
+ norm_layer=norm_layer,
199
+ rank=config.rank,
200
+ use_rope_embed=config.use_rope_embed,
201
+ attn_type=config.attn_type,
202
+ fusion_init_scale=config.fusion_init_scale,
203
+ )
204
+ for i in range(config.depth)
205
+ ])
206
+
207
+ # Final normalization layer
208
+ self.norm = norm_layer(config.embed_dim)
209
+ self.head = nn.Identity()
210
+
211
+ # Initialize weights
212
+ self.initialize_weights()
213
+
214
+ def initialize_weights(self):
215
+ if self.config.use_rope_embed:
216
+ assert isinstance(self.pos_chan_embed, RopePositionChannelEmbedding)
217
+ self.pos_chan_embed._init_weights()
218
+
219
+ torch.nn.init.normal_(self.cls_token, std=.02)
220
+ torch.nn.init.normal_(self.spatial_cls_token, std=.02)
221
+ torch.nn.init.normal_(self.channel_cls_token, std=.02)
222
+
223
+ self.apply(self._init_weights)
224
+
225
+ def _init_weights(self, m):
226
+ if isinstance(m, nn.Linear):
227
+ torch.nn.init.xavier_uniform_(m.weight)
228
+ if isinstance(m, nn.Linear) and m.bias is not None:
229
+ nn.init.constant_(m.bias, 0)
230
+ elif isinstance(m, nn.LayerNorm):
231
+ nn.init.constant_(m.bias, 0)
232
+ nn.init.constant_(m.weight, 1.0)
233
+
234
+ def forward(self, optical=None, radar=None, optical_channel_wv=None, radar_channel_wv=None, spatial_resolution=10, mask_ratio=None, channel_mask_ratio=None):
235
+ if self.config.channel_dropout is not None and not self.training:
236
+ assert len(self.config.channel_dropout) in [1, 2], f"channel_dropout should be a float or a list of two floats, but got {self.config.channel_dropout}"
237
+ for i in range(len(self.config.channel_dropout)):
238
+ assert 0.0 <= self.config.channel_dropout[i] < 1.0, f"channel_dropout should be between 0 and 1, but got {self.config.channel_dropout}"
239
+ channel_dropout = sorted(self.config.channel_dropout)
240
+ num_channels = optical.shape[1] if optical is not None else radar.shape[1]
241
+
242
+ if len(channel_dropout) == 1:
243
+ C = max(1, int(num_channels * (1 - channel_dropout[0])))
244
+ else:
245
+ C_min = max(1, int(num_channels * (1 - channel_dropout[1])))
246
+ C_max = max(1, int(num_channels * (1 - channel_dropout[0])))
247
+ C = random.randint(C_min, C_max)
248
+ idx_to_keep = sorted(random.sample(range(num_channels), C))
249
+ if optical is not None:
250
+ optical = optical[:, idx_to_keep, :, :]
251
+ optical_channel_wv = optical_channel_wv[:, idx_to_keep]
252
+ if radar is not None:
253
+ radar = radar[:, idx_to_keep, :, :]
254
+ radar_channel_wv = radar_channel_wv[:, idx_to_keep]
255
+ assert optical is not None or radar is not None, "At least one of optical and radar should be provided"
256
+ assert optical_channel_wv is None or len(optical_channel_wv.shape) == 2, "If optical ids are provided, they should be a 2D tensor"
257
+ assert radar_channel_wv is None or len(radar_channel_wv.shape) == 2, "If radar ids are provided, they should be a 2D tensor"
258
+ mask_ratio = self.config.mask_ratio if mask_ratio is None else mask_ratio
259
+ channel_mask_ratio = self.config.channel_mask_ratio if channel_mask_ratio is None else channel_mask_ratio
260
+ hidden_states = []
261
+
262
+ dummy_loss = 0 # dummy loss to avoid empty loss error
263
+ if optical is not None:
264
+ optical = self.optical_patch_embed(optical) # B, Co, HW, D
265
+ assert optical_channel_wv is not None, "Optical ids should be provided"
266
+ assert optical_channel_wv.shape[1] == optical.shape[1], "Optical ids should have the same number of channels as the optical data"
267
+ else:
268
+ optical_channel_wv = None
269
+ for param in self.optical_patch_embed.parameters():
270
+ dummy_loss += 0.0 * torch.sum(param)
271
+ if radar is not None:
272
+ radar = self.radar_patch_embed(radar) # B, Cr, HW, D
273
+ assert radar_channel_wv is not None, "Radar ids should be provided"
274
+ assert radar_channel_wv.shape[1] == radar.shape[1], "Radar ids should have the same number of channels as the radar data"
275
+ else:
276
+ radar_channel_wv = None
277
+ for param in self.radar_patch_embed.parameters():
278
+ dummy_loss += 0.0 * torch.sum(param)
279
+
280
+ channel_ids = self.maybe_concat(optical_channel_wv, radar_channel_wv) # 1, C = Co + Cr
281
+ x = self.maybe_concat(optical, radar) # B, C, HW, D
282
+ num_patches = x.shape[2]
283
+
284
+ # Create the perception field mask to simulate convolutional attention
285
+ if self.config.use_perception_field_mask:
286
+ perception_field_mask = get_perception_field_mask(num_patches, self.config.patch_size, spatial_resolution, attention_radius=self.config.attention_radius, cls_token=False).to(x.device)
287
+ else:
288
+ perception_field_mask = None
289
+
290
+ B, C, HW, D = x.shape
291
+ H = W = int(HW ** 0.5)
292
+ # Create cls tokens for both Cin and HW dimensions
293
+ channel_cls_token = self.channel_cls_token.expand(B, 1, HW, D)
294
+ spatial_cls_token = self.spatial_cls_token.expand(B, C, 1, D)
295
+ cls_token = self.cls_token.expand(B, 1, 1, D)
296
+ spatial_cls_token = torch.cat((cls_token, spatial_cls_token), dim=1) # B C+1 HW D
297
+
298
+ # Append cls token to Cin dimension
299
+ x = torch.cat((channel_cls_token, x), dim=1)
300
+
301
+ # Append cls token to HW dimension
302
+ x = torch.cat((spatial_cls_token, x), dim=2)
303
+
304
+ # Add positional and channel embedding
305
+ if not self.config.use_rope_embed:
306
+ pos_chan_embed = self.pos_chan_embed(x[:, 1:, 1:, :], channel_ids=channel_ids, spatial_resolution=spatial_resolution, cls_token=True).to(x.device, dtype=x.dtype)
307
+ pos_chan_embed = pos_chan_embed.repeat(B, 1, 1, 1)
308
+ else:
309
+ pos_chan_embed = self.pos_chan_embed(H=H, W=W, C=C, optical_channel_wv=channel_ids) #
310
+ # below is current walkaround to make RoPE compatibale with masking
311
+ pos_chan_embed = [embed.repeat(B, 1, 1) for embed in pos_chan_embed]
312
+
313
+ # Apply masks after positional embedding
314
+ if self.training and self.config.pretrain:
315
+ # mask the channel
316
+ x_ = x[:, 1:, :, :] # B C HW+1 D
317
+ pos_chan_embed_ = pos_chan_embed[:, 1:, :, :] if not self.config.use_rope_embed else pos_chan_embed
318
+ x_, pos_chan_embed_, channel_mask, channel_ids_restore = self.random_channel_masking(x_, pos_chan_embedding=pos_chan_embed_, channel_mask_ratio=channel_mask_ratio, use_rope_embed=self.config.use_rope_embed) # B N HW+1 D, B C, B C
319
+ x = torch.cat((x[:, :1, :, :], x_), dim=1) # B N+1 HW+1 D
320
+ pos_chan_embed = torch.cat((pos_chan_embed[:, :1, :, :], pos_chan_embed_), dim=1) if not self.config.use_rope_embed else pos_chan_embed_ # B N+1 HW+1 D
321
+ # mask the position
322
+ x_ = x[:, :, 1:, :] # B N+1 HW D
323
+ pos_chan_embed_ = pos_chan_embed[:, :, 1:, :] if not self.config.use_rope_embed else pos_chan_embed
324
+ x_, pos_chan_embed_, pos_mask, pos_ids_restore, perception_field_mask = self.random_pos_masking(x_, pos_chan_embedding=pos_chan_embed_, mask_ratio=mask_ratio, perception_field_mask=perception_field_mask, use_rope_embed=self.config.use_rope_embed) # B N+1 L D, B HW, B HW, L L
325
+ x = torch.cat((x[:, :, :1, :], x_), dim=2) # B N+1 L+1 D
326
+ pos_chan_embed = torch.cat((pos_chan_embed[:, :, :1, :], pos_chan_embed_), dim=2) if not self.config.use_rope_embed else pos_chan_embed_ # B N+1 L+1 D
327
+ else:
328
+ channel_mask = pos_mask = channel_ids_restore = pos_ids_restore = None
329
+
330
+ x = x + pos_chan_embed if not self.config.use_rope_embed else x # B N+1 L+1 D
331
+ pos_chan_embed = tuple(pos_chan_embed) if self.config.use_rope_embed else pos_chan_embed
332
+
333
+ if perception_field_mask is not None:
334
+ # append cls token
335
+ L = perception_field_mask.shape[0]
336
+ new_row = torch.ones(L, 1, device=perception_field_mask.device)
337
+ new_col = torch.ones(1, L + 1, device=perception_field_mask.device)
338
+ perception_field_mask = torch.cat([new_row, perception_field_mask], dim=1)
339
+ perception_field_mask = torch.cat([new_col, perception_field_mask], dim=0)
340
+ perception_field_mask = perception_field_mask > 0 # L L bool
341
+
342
+ # Apply transformer blocks
343
+ for i,blk in enumerate(self.blocks):
344
+ if not self.config.use_rope_embed:
345
+ pos_chan_embedding = pos_chan_embed*2**(-i) if self.config.pos_chan_embed_residual else pos_chan_embed
346
+ else:
347
+ pos_chan_embedding = pos_chan_embed
348
+ x = blk(x, spatial_mask=perception_field_mask, pos_chan_embedding=pos_chan_embedding)
349
+
350
+ # Apply final layer norm
351
+ x = self.norm(x) # B N+1 L+1 D
352
+ cls_token = x[:, 0, 0]
353
+ patch_tokens = x[:, 1:, 1:] # B N+1 L+1 D -> B N L D
354
+
355
+ if self.training and self.config.pretrain:
356
+ return x + dummy_loss, channel_mask, channel_ids_restore, pos_mask, pos_ids_restore
357
+ elif self.config.return_dict:
358
+ return BaseModelOutput(
359
+ last_hidden_state=x,
360
+ hidden_states=hidden_states,
361
+ attentions=None,
362
+ )
363
+ else:
364
+ return x, cls_token, patch_tokens, hidden_states
365
+
366
+ def maybe_concat(self, x, y):
367
+ if x is not None and y is not None:
368
+ return torch.cat((x, y), dim=1)
369
+ elif x is not None:
370
+ return x
371
+ elif y is not None:
372
+ return y
373
+
374
+ def random_pos_masking(self, x, pos_chan_embedding=None, mask_ratio=0.75, batch_wise_mask=False, perception_field_mask=None, use_rope_embed=False):
375
+ """
376
+ Perform per-sample random masking by per-sample shuffling.
377
+ Per-sample shuffling is done by argsort random noise.
378
+ x: [B, C, HW, D], sequence
379
+ perception_field_mask: [HW, HW] or None, if not None, use distance attention mask is used.
380
+ """
381
+ if perception_field_mask is not None:
382
+ batch_wise_mask = True # if distance mask is provided, we need to use batch-wise masking
383
+
384
+ B, C, HW, D = x.shape # batch, channels, length, dim
385
+ if mask_ratio == 0:
386
+ mask = torch.zeros([B, HW], device=x.device)
387
+ ids_restore = torch.arange(HW, device=x.device).unsqueeze(0).repeat(B, 1)
388
+ if pos_chan_embedding is not None:
389
+ return x, pos_chan_embedding, mask, ids_restore, perception_field_mask
390
+ else:
391
+ return x, mask, ids_restore, perception_field_mask
392
+
393
+ x = x.permute(0, 2, 1, 3) # B, HW, C, D
394
+ len_keep = int(HW * (1 - mask_ratio)) # L
395
+
396
+ if batch_wise_mask:
397
+ noise = torch.rand(1, HW, device=x.device) # noise in [0, 1]
398
+ noise = noise.repeat(B, 1)
399
+ else:
400
+ noise = torch.rand(B, HW, device=x.device) # noise in [0, 1]
401
+
402
+ # sort noise for each sample
403
+ ids_shuffle = torch.argsort(noise, dim=1) # ascend: small is keep, large is remove
404
+ ids_restore = torch.argsort(ids_shuffle, dim=1) # B, HW
405
+
406
+ # keep the first subset
407
+ ids_keep = ids_shuffle[:, :len_keep] # B, L
408
+ x_masked = torch.gather(x, dim=1, index=ids_keep.unsqueeze(-1).unsqueeze(-1).repeat(1, 1, C, D)) # B, L, C, D
409
+
410
+ if pos_chan_embedding is not None:
411
+ if use_rope_embed:
412
+ pos_chan_embedding[0] = torch.gather(pos_chan_embedding[0], dim=1, index=ids_keep.unsqueeze(-1).repeat(1, 1, pos_chan_embedding[0].shape[-1])) # B, L, D
413
+ pos_chan_embedding[1] = torch.gather(pos_chan_embedding[1], dim=1, index=ids_keep.unsqueeze(-1).repeat(1, 1, pos_chan_embedding[1].shape[-1])) # B, L, D
414
+ pos_chan_embed_masked = pos_chan_embedding
415
+ else:
416
+ pos_chan_embedding = pos_chan_embedding.permute(0, 2, 1, 3) # B, HW, C, D
417
+ pos_chan_embed_masked = torch.gather(pos_chan_embedding, dim=1, index=ids_keep.unsqueeze(-1).unsqueeze(-1).repeat(1, 1, C, D)) # B, L, C, D
418
+ pos_chan_embed_masked = pos_chan_embed_masked.permute(0, 2, 1, 3) # B, C, L, D
419
+
420
+ if perception_field_mask is not None:
421
+ # remove the corresponding ids in perception_field_mask
422
+ ids_keep_ = ids_keep[0].cpu() # L
423
+ perception_field_mask = perception_field_mask[ids_keep_, :] # L HW
424
+ perception_field_mask = perception_field_mask[:, ids_keep_] # L L
425
+
426
+ # generate the binary mask: 0 is keep, 1 is remove
427
+ mask = torch.ones([B, HW], device=x.device)
428
+ mask[:, :len_keep] = 0
429
+ # unshuffle to get the binary mask
430
+ mask = torch.gather(mask, dim=1, index=ids_restore) # B, HW
431
+
432
+ x_masked = x_masked.permute(0, 2, 1, 3) # [B, C, L, D]
433
+ if pos_chan_embedding is not None:
434
+ return x_masked, pos_chan_embed_masked, mask, ids_restore, perception_field_mask
435
+ else:
436
+ return x_masked, mask, ids_restore, perception_field_mask
437
+
438
+ def random_channel_masking(self, x, pos_chan_embedding=None, channel_mask_ratio=0.5, batch_wise_mask=False, use_rope_embed=False):
439
+ """
440
+ Perform per-sample random masking by per-sample shuffling.
441
+ Per-sample shuffling is done by argsort random noise.
442
+ x: [B, C, HW, D], sequence
443
+ pos_chan_embedding: [B, C, HW, D], positional embedding
444
+ Return:
445
+ x_masked: [B, N, HW, D]
446
+ mask: [B, C]
447
+ ids_restore: [B, C]
448
+ """
449
+ B, C, HW, D = x.shape
450
+ if channel_mask_ratio == 0:
451
+ mask = torch.zeros([B, C], device=x.device)
452
+ ids_restore = torch.arange(C, device=x.device).unsqueeze(0).repeat(B, 1)
453
+ if pos_chan_embedding is not None:
454
+ return x, pos_chan_embedding, mask, ids_restore
455
+ else:
456
+ return x, mask, ids_restore
457
+
458
+ len_keep = max(int(C * (1 - channel_mask_ratio)), 2) # at least 2 channel is kept, N
459
+
460
+ if batch_wise_mask:
461
+ noise = torch.rand(1, C, device=x.device) # noise in [0, 1]
462
+ noise = noise.repeat(B, 1)
463
+ else:
464
+ noise = torch.rand(B, C, device=x.device) # noise in [0, 1]
465
+
466
+ # sort noise for each sample
467
+ ids_shuffle = torch.argsort(noise, dim=1) # B C
468
+ ids_restore = torch.argsort(ids_shuffle, dim=1) # B C
469
+
470
+ # keep the first subset
471
+ ids_keep = ids_shuffle[:, :len_keep]
472
+ x_masked = torch.gather(x, dim=1, index=ids_keep.unsqueeze(-1).unsqueeze(-1).repeat(1, 1, HW, D)) # B, N, HW, D
473
+ if pos_chan_embedding is not None:
474
+ if use_rope_embed:
475
+ pos_chan_embedding[2] = torch.gather(pos_chan_embedding[2], dim=1, index=ids_keep.unsqueeze(-1).repeat(1, 1, pos_chan_embedding[2].shape[-1])) # B, L, D
476
+ pos_chan_embedding[3] = torch.gather(pos_chan_embedding[3], dim=1, index=ids_keep.unsqueeze(-1).repeat(1, 1, pos_chan_embedding[3].shape[-1])) # B, L, D
477
+ pos_chan_embed_masked = pos_chan_embedding
478
+ else:
479
+ pos_chan_embed_masked = torch.gather(pos_chan_embedding, dim=1, index=ids_keep.unsqueeze(-1).unsqueeze(-1).repeat(1, 1, HW, D)) # B, N, HW, D
480
+
481
+ # generate the binary mask: 0 is keep, 1 is remove
482
+ mask = torch.ones([B, C], device=x.device) # B C
483
+ mask[:, :len_keep] = 0
484
+ # unshuffle to get the binary mask
485
+ mask = torch.gather(mask, dim=1, index=ids_restore) # B C
486
+
487
+ if pos_chan_embedding is not None:
488
+ return x_masked, pos_chan_embed_masked, mask, ids_restore
489
+ else:
490
+ return x_masked, mask, ids_restore
491
+
492
+ class SpatialSpectralLowRankViTDecoder(PreTrainedModel):
493
+ config_class = SpatialSpectralLowRankViTConfig
494
+
495
+ def __init__(self, config: SpatialSpectralLowRankViTConfig):
496
+ super().__init__(config)
497
+ self.config = config
498
+ norm_layer = partial(nn.LayerNorm, eps=1e-6)
499
+
500
+ self.decoder_embed = nn.Linear(config.embed_dim, config.decoder_embed_dim, bias=True)
501
+ # self.decoder_embed_norm = norm_layer(config.decoder_embed_dim)
502
+ self.mask_token = nn.Parameter(torch.zeros(1, 1, 1, config.decoder_embed_dim))
503
+ self.spatial_mask_token = nn.Parameter(torch.zeros(1, 1, 1, config.decoder_embed_dim))
504
+ self.channel_mask_token = nn.Parameter(torch.zeros(1, 1, 1, config.decoder_embed_dim))
505
+
506
+ if config.drop_path_uniform is True:
507
+ dpr = [config.drop_path_rate] * config.decoder_depth
508
+ else:
509
+ dpr = [x.item() for x in torch.linspace(0, config.drop_path_rate, config.decoder_depth)]
510
+
511
+ self.decoder_blocks = nn.ModuleList([
512
+ LowRankBlock(
513
+ dim=config.decoder_embed_dim,
514
+ num_heads=config.decoder_num_heads,
515
+ channel_dim=config.decoder_channel_dim,
516
+ spatial_dim=config.decoder_spatial_dim,
517
+ mlp_ratio=config.mlp_ratio,
518
+ qkv_bias=config.qkv_bias,
519
+ qk_norm=config.qk_norm,
520
+ proj_drop=config.proj_drop,
521
+ attn_drop=config.attn_drop,
522
+ drop_path=dpr[i],
523
+ init_values=config.init_values,
524
+ norm_layer=norm_layer,
525
+ skip_pool=False,
526
+ rank=config.rank,
527
+ attn_type=config.attn_type,
528
+ fusion_init_scale=config.fusion_init_scale,
529
+ )
530
+ for i in range(config.decoder_depth)
531
+ ])
532
+
533
+ self.decoder_norm = norm_layer(config.decoder_embed_dim)
534
+ self.decoder_optical_pred = nn.Linear(config.decoder_embed_dim, config.patch_size**2, bias=True)
535
+ self.decoder_radar_pred = nn.Linear(config.decoder_embed_dim, config.patch_size**2, bias=True)
536
+
537
+ if not config.use_rope_embed:
538
+ self.pos_chan_embed = PositionalChannelEmbedding(config.decoder_embed_dim)
539
+ else:
540
+ decoder_rope_spatial_dim = config.decoder_rope_spatial_split_dim if config.attn_type == "full" else config.decoder_spatial_dim
541
+ decoder_rope_channel_dim = config.decoder_rope_channel_split_dim if config.attn_type == "full" else config.decoder_channel_dim
542
+ self.pos_chan_embed = RopePositionChannelEmbedding(
543
+ # embed_dim=config.decoder_embed_dim,
544
+ spatial_dim=decoder_rope_spatial_dim,
545
+ channel_dim=decoder_rope_channel_dim,
546
+ num_heads=config.decoder_num_heads,
547
+ base=config.rope_embed_base if hasattr(config, 'rope_embed_base') else 100.0
548
+ )
549
+
550
+ self.initialize_weights()
551
+
552
+ def initialize_weights(self):
553
+ torch.nn.init.normal_(self.mask_token, std=.02)
554
+ torch.nn.init.normal_(self.spatial_mask_token, std=.02)
555
+ torch.nn.init.normal_(self.channel_mask_token, std=.02)
556
+ self.apply(self._init_weights)
557
+
558
+ def _init_weights(self, m):
559
+ if isinstance(m, nn.Linear):
560
+ torch.nn.init.xavier_uniform_(m.weight)
561
+ if isinstance(m, nn.Linear) and m.bias is not None:
562
+ nn.init.constant_(m.bias, 0)
563
+ elif isinstance(m, nn.LayerNorm):
564
+ nn.init.constant_(m.bias, 0)
565
+ nn.init.constant_(m.weight, 1.0)
566
+
567
+ def forward(self, x, pos_ids_restore, channel_ids_restore, optical_channel_wv, radar_channel_wv, spatial_resolution, restore_input_dim=False):
568
+ hidden_states = []
569
+
570
+ # embed tokens
571
+ x = self.decoder_embed(x) # B N+1 L+1 D
572
+
573
+ if radar_channel_wv is not None:
574
+ channel_wv = torch.cat((optical_channel_wv, radar_channel_wv), dim=1) # 1 C = Co + Cr or B C
575
+ n_radar_channels = radar_channel_wv.shape[1]
576
+ else:
577
+ channel_wv = optical_channel_wv
578
+ n_radar_channels = None
579
+ n_optical_channels = optical_channel_wv.shape[1]
580
+
581
+ # remove cls token
582
+ B, N, L, D = x[:, 1:, 1:, :].shape
583
+ C = channel_ids_restore.shape[1]
584
+ HW = pos_ids_restore.shape[1]
585
+
586
+ # append mask tokens to sequence
587
+ channel_mask_token = self.channel_mask_token.expand(B, 1, HW - L, -1)
588
+ mask_tokens = self.mask_token.expand(B, N, HW - L, -1)
589
+ mask_tokens = torch.cat([channel_mask_token, mask_tokens], dim=1)
590
+ x_ = torch.cat([x[:, :, 1:], mask_tokens], dim=2) # B N+1 HW D, remove cls token
591
+ x_ = torch.gather(x_, dim=2, index=pos_ids_restore.unsqueeze(1).unsqueeze(-1).repeat(1, N + 1, 1, D)) # unshuffle, B N+1 HW D
592
+ x = torch.cat([x[:, :, :1], x_], dim=2) # B N+1 HW+1 D, add cls token
593
+
594
+ # append channel mask tokens to sequence
595
+ spatial_mask_token = self.spatial_mask_token.expand(B, C - N, 1, -1)
596
+ mask_tokens = self.mask_token.expand(B, C - N, HW, -1)
597
+ mask_tokens = torch.cat([spatial_mask_token, mask_tokens], dim=2)
598
+ x_ = torch.cat([x[:, 1:], mask_tokens], dim=1) # B, C, HW+1, D, remove cls token
599
+ x_ = torch.gather(x_, dim=1, index=channel_ids_restore.unsqueeze(-1).unsqueeze(-1).repeat(1, 1, HW + 1, D)) # unshuffle, B C HW+1 D
600
+ x = torch.cat([x[:, :1], x_], dim=1) # B C+1 HW+1 D, add cls token
601
+
602
+ if n_radar_channels is not None and C == optical_channel_wv.shape[1]:
603
+ # missing radar channels from encoder
604
+ n_radar_channels = radar_channel_wv.shape[1]
605
+ # extend x to C+n_radar_channels to the end
606
+ spatial_mask_token = self.spatial_mask_token.expand(B, n_radar_channels, 1, -1)
607
+ mask_tokens = self.mask_token.expand(B, n_radar_channels, HW, -1) # B C HW D
608
+ mask_tokens = torch.cat([spatial_mask_token, mask_tokens], dim=2)
609
+ x = torch.cat([x, mask_tokens], dim=1)
610
+ elif n_radar_channels is not None and C == radar_channel_wv.shape[1]:
611
+ # missing optical channels from encoder
612
+ n_optical_channels = optical_channel_wv.shape[1]
613
+ # extend x to C+n_optical_channels to the front behind the cls token
614
+ spatial_mask_token = self.spatial_mask_token.expand(B, n_optical_channels, 1, -1)
615
+ mask_tokens = self.mask_token.expand(B, n_optical_channels, HW, -1)
616
+ mask_tokens = torch.cat([spatial_mask_token, mask_tokens], dim=2)
617
+ x = torch.cat([x[:, :1, :, :], mask_tokens, x[:, 1:, :, :]], dim=1)
618
+
619
+ if not self.config.use_rope_embed:
620
+ pos_chan_embed = self.pos_chan_embed(x[:, 1:, 1:, :], channel_ids=channel_wv, spatial_resolution=spatial_resolution, cls_token=True).to(x.device, dtype=x.dtype)
621
+ x = x + pos_chan_embed
622
+ else:
623
+ assert math.sqrt(HW).is_integer(), "For RoPE, the input num of tokens must be a perfect square."
624
+ H = W = int(math.sqrt(HW))
625
+ pos_chan_embed = self.pos_chan_embed(H=H, W=W, C=C, optical_channel_wv=channel_wv)
626
+ pos_chan_embed = [embed.repeat(B, 1, 1) for embed in pos_chan_embed] # walk around for RoPE
627
+ pos_chan_embed = tuple(pos_chan_embed)
628
+
629
+ num_patches = x.shape[2] - 1
630
+ if self.config.use_perception_field_mask:
631
+ perception_field_mask = get_perception_field_mask(num_patches, self.config.patch_size, spatial_resolution, attention_radius=self.config.attention_radius, cls_token=True).to(x.device)
632
+ else:
633
+ perception_field_mask = None
634
+
635
+ for i, blk in enumerate(self.decoder_blocks):
636
+ if not self.config.use_rope_embed:
637
+ pos_chan_embedding = pos_chan_embed*2**(-i) if self.config.pos_chan_embed_residual else None
638
+ else:
639
+ pos_chan_embedding = pos_chan_embed
640
+ x = blk(x, spatial_mask=perception_field_mask, pos_chan_embedding=pos_chan_embedding)
641
+
642
+ x = self.decoder_norm(x)
643
+ x = x[:, 1:, 1:, :]
644
+
645
+ x_optical = x[:, :n_optical_channels, :, :]
646
+ x_radar = x[:, n_optical_channels:, :, :]
647
+
648
+ x_optical = self.decoder_optical_pred(x_optical)
649
+ x_radar = self.decoder_radar_pred(x_radar)
650
+
651
+ x = torch.cat([x_optical, x_radar], dim=1) # B C HW patch_size**2
652
+
653
+ if restore_input_dim:
654
+ x = self.unpatchify(x)
655
+
656
+ if self.config.return_dict:
657
+ # Return as BaseModelOutput for Hugging Face compatibility
658
+ return BaseModelOutput(
659
+ last_hidden_state=x,
660
+ hidden_states=hidden_states,
661
+ attentions=None, # You can modify this if you have attention weights to return
662
+ )
663
+ else:
664
+ return x, hidden_states
665
+
666
+ def unpatchify(self, x):
667
+ """
668
+ x: (B, C, HW, patch_size**2)
669
+ imgs: (B, C, H, W)
670
+ """
671
+ p = self.config.patch_size
672
+ h = w = int(x.shape[2]**.5)
673
+ assert h * w == x.shape[2]
674
+
675
+ x = x.reshape(shape=(x.shape[0], x.shape[1], h, w, p, p))
676
+ x = torch.einsum('bchwpq->bchpwq', x)
677
+ imgs = x.reshape(shape=(x.shape[0], x.shape[1], h * p, h * p))
678
+ return imgs
679
+
680
+ def forward_target(self, imgs):
681
+ """
682
+ imgs: [B, C, H, W]
683
+ """
684
+ target = self.patchify(imgs)
685
+ if self.config.norm_pix_loss:
686
+ mean = target.mean(dim=-1, keepdim=True)
687
+ var = target.var(dim=-1, keepdim=True)
688
+ target = (target - mean) / (var + 1.e-6)**.5
689
+ return target # B C HW patch_size**2
690
+
691
+ def patchify(self, imgs):
692
+ """
693
+ imgs: (B, C, H, W)
694
+ x: (B, C, HW, patch_size**2)
695
+ """
696
+ p = self.config.patch_size
697
+ assert imgs.shape[2] == imgs.shape[3] and imgs.shape[2] % p == 0
698
+
699
+ h = w = imgs.shape[2] // p
700
+ x = imgs.reshape(shape=(imgs.shape[0], imgs.shape[1], h, p, w, p))
701
+ x = torch.einsum('bchpwq->bchwpq', x)
702
+ x = x.reshape(shape=(imgs.shape[0], imgs.shape[1], h * w, p**2))
703
+ return x
704
+
validation.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "encoder_parameters": 83219328,
3
+ "verified_tensors": 543,
4
+ "remapped_tensors": 408,
5
+ "output_shape": [
6
+ 1,
7
+ 11,
8
+ 5,
9
+ 768
10
+ ],
11
+ "auto_model_loading_info": {
12
+ "missing_keys": [],
13
+ "unexpected_keys": [],
14
+ "mismatched_keys": [],
15
+ "error_msgs": []
16
+ },
17
+ "forward_exact_match": true,
18
+ "encoder_backward": "passed",
19
+ "original_checkpoint_preserved": true
20
+ }