Feature Extraction
Transformers
Safetensors
English
lessvit
hyperspectral
remote-sensing
geospatial
vision-transformer
custom_code
Instructions to use GFM-Bench/LESSViT-Base with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use GFM-Bench/LESSViT-Base with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("feature-extraction", model="GFM-Bench/LESSViT-Base", trust_remote_code=True)# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("GFM-Bench/LESSViT-Base", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Release LESSViT checkpoint and directly loadable encoder
Browse files- README.md +168 -0
- SHA256SUMS +14 -0
- attention_ops.py +432 -0
- config.json +52 -0
- hyperspectral_patch_embed.py +57 -0
- load_encoder.py +41 -0
- low_rank_attention.py +340 -0
- model.safetensors +3 -0
- modeling_lessvit.py +37 -0
- pos_chan_embed.py +361 -0
- pretraining/architecture.json +50 -0
- pretraining/model.safetensors +3 -0
- requirements.txt +7 -0
- spatial_spectral_low_rank_vit.py +704 -0
- validation.json +20 -0
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 |
+
}
|