zhangrenchao commited on
Commit
387a20d
·
verified ·
1 Parent(s): d430a5c

Add Chinese Hugging Face model card and configuration

Browse files
.gitattributes CHANGED
@@ -1,35 +1,13 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
  *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
  *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
  *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
  *.pt filter=lfs diff=lfs merge=lfs -text
23
  *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
  *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
 
 
 
28
  *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
1
  *.bin filter=lfs diff=lfs merge=lfs -text
 
 
 
 
2
  *.h5 filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
3
  *.onnx filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
4
  *.pt filter=lfs diff=lfs merge=lfs -text
5
  *.pth filter=lfs diff=lfs merge=lfs -text
 
6
  *.safetensors filter=lfs diff=lfs merge=lfs -text
7
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
8
+ *.npy filter=lfs diff=lfs merge=lfs -text
9
+ *.npz filter=lfs diff=lfs merge=lfs -text
10
+ *.pickle filter=lfs diff=lfs merge=lfs -text
11
+ *.pkl filter=lfs diff=lfs merge=lfs -text
12
  *.tar filter=lfs diff=lfs merge=lfs -text
 
 
 
 
13
  *.zip filter=lfs diff=lfs merge=lfs -text
 
 
README.md ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ frameworks: PyTorch
3
+ language:
4
+ - zh
5
+ - en
6
+ license: apache-2.0
7
+ tags:
8
+ - OneScience
9
+ - Earth Science
10
+ - Remote Sensing
11
+ - Masked Image Modeling
12
+ - Sentinel-2
13
+ - SpectralGPT
14
+ - arxiv:2311.07113
15
+ tasks: []
16
+ datasets: []
17
+ ---
18
+ <p align="center">
19
+ <strong>
20
+ <span style="font-size: 30px;">SpectralGPT</span>
21
+ </strong>
22
+ </p>
23
+
24
+ # 模型介绍
25
+
26
+ SpectralGPT 通过三维空间-光谱掩码建模和多目标重建,从大规模多光谱遥感影像中学习通用表征,主要用于少标签条件下的场景分类、语义分割和变化检测。
27
+
28
+ 论文:SpectralGPT: Spectral Remote Sensing Foundation Model
29
+
30
+ https://arxiv.org/abs/2311.07113
31
+
32
+ # 模型描述
33
+
34
+ SpectralGPT 由中国科学院空天信息创新研究院等机构提出,使用 fMoW-S2 和 BigEarthNet-S2 的 12 波段 Sentinel-2 影像训练。模型适用于单标签或多标签场景分类、语义分割和变化检测等遥感任务。
35
+
36
+ # 适用场景
37
+
38
+ | 场景 | 说明 |
39
+ | :---: | :--- |
40
+ | 多光谱掩码预训练 | 使用归一化的 12 波段 Sentinel-2 多光谱影像训练 SpectralGPT。 |
41
+ | 遥感场景理解 | 使用学习到的多光谱表征进行场景分类、语义分割和变化检测。 |
42
+ | 本地快速验证 | 使用虚拟数据检查数据读取、掩码重建训练、推理、评估和可视化流程。 |
43
+ | Hugging Face / OneCode 运行 | 作为独立模型包下载后直接安装依赖并运行脚本。 |
44
+ | 多卡训练 | 通过 `torchrun` 启动多进程训练。 |
45
+
46
+ # 使用说明
47
+
48
+ ## 1. OneCode 使用
49
+
50
+ 可通过 OneCode 在线环境体验智能化一键式 AI4S 编程:
51
+
52
+ [点击体验智能化一键式 AI4S 编程](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
53
+
54
+ ## 2. 手动安装使用
55
+
56
+ **硬件要求**
57
+
58
+ - 推荐使用 GPU 或 DCU 运行。
59
+ - CPU 可以用于导入和小配置连通性验证,完整训练和推理速度较慢。
60
+ - DCU 用户需要预先安装 DTK,建议使用 DTK 25.04.2 以上版本或与当前集群匹配的 OneScience 推荐版本。
61
+
62
+ ### 下载模型包
63
+
64
+ ```bash
65
+ hf download OneScience-Group/SpectralGPT --local-dir ./SpectralGPT
66
+ cd SpectralGPT
67
+ ```
68
+
69
+ ### 安装运行环境
70
+
71
+ **DCU环境**
72
+
73
+ ```bash
74
+ # 请首先激活 DTK 及 CONDA
75
+ conda create -n onescience311 python=3.11 -y
76
+ conda activate onescience311
77
+ # 支持 uv 安装
78
+ pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
79
+ ```
80
+
81
+ **GPU环境**
82
+
83
+ ```bash
84
+ # 请首先激活 CONDA
85
+ conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
86
+ conda activate onescience311
87
+ # 支持 uv 安装
88
+ pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
89
+ ```
90
+
91
+ ### 训练数据介绍
92
+
93
+ 论文使用 Sentinel-2 的 12 个主波段并排除 B10,各波段缩放到 `[0,1]`。fMoW-S2 包含 882,779 张影像,其中 712,874 张用于第一阶段预训练;BigEarthNet-S2 包含 590,326 张影像,其中 354,196 张用于后续预训练。OneScience 社区目前未提供可直接用于本仓库训练的 fMoW-S2、BigEarthNet-S2 或 EuroSAT 数据,因此本仓库默认使用 `scripts/fake_data.py` 生成 8 张小规模虚拟多光谱影像进行流程验证。
94
+
95
+ EuroSAT 官方数据资源:
96
+
97
+ ```text
98
+ 官方 Zenodo 数据页:
99
+ https://zenodo.org/records/7711810
100
+
101
+ 多光谱数据下载:
102
+ https://madm.dfki.de/files/sentinel/EuroSATallBands.zip
103
+ ```
104
+
105
+ 解压 EuroSAT 多光谱数据后,可使用以下命令排除 B10、缩放到 `[0,1]` 并转换为本仓库使用的 12 波段 NPZ 数据:
106
+
107
+ ```bash
108
+ python scripts/fake_data.py --real-dir ./data/EuroSAT_MS
109
+ ```
110
+
111
+ 虚拟数据保存为 `data/fake_spectralgpt.npz`,只用于验证数据读取、掩码重建训练、检查点保存与加载、推理、评估和可视化流程,不代表真实 Sentinel-2 影像,也不能用于复现论文指标。真实数据训练不应先运行不带 `--real-dir` 的虚拟数据命令,并需要使用一致的波段顺序、空间尺寸和数值缩放方式。
112
+
113
+ ### 训练
114
+
115
+ 使用虚拟数据快速验证:
116
+
117
+ ```bash
118
+ python scripts/fake_data.py
119
+ python scripts/train.py
120
+ ```
121
+
122
+ 使用真实 EuroSAT 多光谱 TIFF 训练:
123
+
124
+ ```bash
125
+ python scripts/fake_data.py --real-dir ./data/EuroSAT_MS
126
+ python scripts/train.py
127
+ ```
128
+
129
+ 虚拟数据和真实数据共用同一个训练脚本。需要调整训练规模时,可修改 `conf/config.yaml` 中的 `data.samples`、`model.image_size`、`training.epochs` 和 `training.batch_size`。
130
+
131
+ 单卡:
132
+
133
+ ```bash
134
+ python scripts/train.py
135
+ ```
136
+
137
+ 多卡:
138
+
139
+ ```bash
140
+ torchrun --nproc_per_node=8 --nnodes=1 --rdzv_id=1000 --rdzv_backend=c10d --max_restarts=0 --master_addr="localhost" --master_port=29500 scripts/train.py
141
+ ```
142
+
143
+ 训练输出:
144
+
145
+ ```text
146
+ result/checkpoints/best.pth
147
+ result/checkpoints/last.pth
148
+ 每轮输出 reconstruction_loss。
149
+ ```
150
+
151
+ 训练日志和检查点中的 `data_source` 与 `protocol` 用于标识当前使用的是虚拟数据还是真实数据���使用虚拟数据生成的检查点只用于流程验证,不具备真实遥感表征能力。
152
+
153
+ ### 训练权重
154
+
155
+ 本仓库将在 `weight/` 文件夹内提供基于 Sentinel-2 多光谱遥感数据训练的权重,权重文件即将上传,预计将于近期完成。
156
+
157
+ ### 推理
158
+
159
+ ```bash
160
+ python scripts/inference.py
161
+ ```
162
+
163
+ 推理读取训练数据路径配置和训练生成的检查点,并输出多光谱影像重建结果。
164
+
165
+ 推理结果输出到:
166
+
167
+ ```text
168
+ result/output/reconstruction.npz
169
+ ```
170
+
171
+ ### 评估和可视化
172
+
173
+ ```bash
174
+ python scripts/result.py
175
+ ```
176
+
177
+ 评估和可视化输出到:
178
+
179
+ ```text
180
+ result/output/metrics.json
181
+ result/output/reconstruction.ppm
182
+ ```
183
+
184
+ 指标包括重建 MSE、MAE、PSNR 和逐波段 RMSE;可视化并排显示输入与重建的假彩色合成图。使用虚拟数据时,评估结果只用于确认代码流程能够运行,不展示或报告虚拟数据数值;使用真实数据时,指标表示当前真实影像上的掩码重建结果。
185
+
186
+ # OneScience 官方信息
187
+
188
+ | 平台 | OneScience 主仓库 | Skills 仓库 |
189
+ | --- | --- | --- |
190
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
191
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
192
+
193
+ # 引用与许可证
194
+
195
+ - 本仓库为 SpectralGPT 原始论文的复现版本。
196
+ - 本模型包采用 Apache License 2.0;原始数据与论文作者发布的模型权重遵循各自来源的许可条款。
conf/config.yaml ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ model:
2
+ image_size: 24
3
+ in_channels: 12
4
+ patch_size: 8
5
+ spectral_patch_size: 3
6
+ embed_dim: 48
7
+ encoder_depth: 2
8
+ encoder_heads: 4
9
+ decoder_dim: 32
10
+ decoder_depth: 1
11
+ decoder_heads: 4
12
+ mask_ratio: 0.90
13
+ spectral_loss_weight: 1.0
14
+ data:
15
+ path: ./data/fake_spectralgpt.npz
16
+ samples: 8
17
+ protocol: synthetic_sentinel2_npz
18
+ training:
19
+ epochs: 2
20
+ batch_size: 2
21
+ learning_rate: 0.0001
22
+ weight_decay: 0.05
23
+ save_dir: ./result/checkpoints
24
+ checkpoint: ./result/checkpoints/best.pth
25
+ runtime:
26
+ device: auto
27
+ seed: 42
28
+ output_dir: ./result/output
config.json ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "SpectralGPT",
3
+ "model_type": "spectralgpt",
4
+ "architectures": [
5
+ "SpectralGPT"
6
+ ],
7
+ "framework": "PyTorch",
8
+ "domain": "earth-science",
9
+ "task": "remote-sensing-masked-image-modeling",
10
+ "implementation": {
11
+ "entry_point": "model/spectralgpt.py",
12
+ "scope": "compact 12-band Sentinel-2 masked autoencoder reproduction"
13
+ },
14
+ "architecture": {
15
+ "family": "spectral-spatial masked autoencoder",
16
+ "input_format": "NCHW multispectral images",
17
+ "input_channels": 12,
18
+ "image_size": 24,
19
+ "patch_size": 8,
20
+ "spectral_patch_size": 3,
21
+ "embed_dim": 48,
22
+ "encoder_depth": 2,
23
+ "decoder_dim": 32,
24
+ "decoder_depth": 1,
25
+ "mask_ratio": 0.9,
26
+ "training_objective": "masked reconstruction with spectral loss"
27
+ },
28
+ "data": {
29
+ "datasets": [
30
+ "fMoW-S2",
31
+ "BigEarthNet-S2"
32
+ ],
33
+ "official_paper": "https://arxiv.org/abs/2311.07113",
34
+ "protocol": "synthetic_sentinel2_npz",
35
+ "synthetic_samples": 8
36
+ },
37
+ "metrics": [
38
+ "mse",
39
+ "mae",
40
+ "psnr_db",
41
+ "per_band_rmse"
42
+ ],
43
+ "configuration_sources": [
44
+ "conf/config.yaml",
45
+ "model/spectralgpt.py"
46
+ ]
47
+ }
configuration.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "framework": "PyTorch",
3
+ "task": "remote_sensing_masked_image_modeling",
4
+ "model": "SpectralGPT",
5
+ "input_format": "NCHW_NPZ",
6
+ "protocol": "synthetic_sentinel2_npz",
7
+ "default_config": "conf/config.yaml",
8
+ "train": "scripts/train.py",
9
+ "inference": "scripts/inference.py",
10
+ "evaluation": "scripts/result.py",
11
+ "visualization": "scripts/result.py"
12
+ }
model/spectralgpt.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from torch import nn
3
+
4
+
5
+ class SpectralGPT(nn.Module):
6
+ """Compact SpectralGPT masked autoencoder for 12-band spectral images."""
7
+
8
+ def __init__(self, image_size=24, in_channels=12, patch_size=8,
9
+ spectral_patch_size=3, embed_dim=48, encoder_depth=2,
10
+ encoder_heads=4, decoder_dim=32, decoder_depth=1,
11
+ decoder_heads=4, mask_ratio=0.9, spectral_loss_weight=1.0):
12
+ super().__init__()
13
+ if image_size % patch_size or in_channels % spectral_patch_size:
14
+ raise ValueError("Image and spectral dimensions must be divisible by token sizes")
15
+ self.image_size = image_size
16
+ self.in_channels = in_channels
17
+ self.patch_size = patch_size
18
+ self.spectral_patch_size = spectral_patch_size
19
+ self.spatial_tokens = (image_size // patch_size) ** 2
20
+ self.spectral_tokens = in_channels // spectral_patch_size
21
+ self.num_tokens = self.spatial_tokens * self.spectral_tokens
22
+ self.token_pixels = patch_size * patch_size * spectral_patch_size
23
+ self.mask_ratio = mask_ratio
24
+ self.spectral_loss_weight = spectral_loss_weight
25
+
26
+ self.patch_embed = nn.Conv3d(
27
+ 1, embed_dim,
28
+ kernel_size=(spectral_patch_size, patch_size, patch_size),
29
+ stride=(spectral_patch_size, patch_size, patch_size),
30
+ )
31
+ self.spatial_pos = nn.Parameter(torch.zeros(1, self.spatial_tokens, embed_dim))
32
+ self.spectral_pos = nn.Parameter(torch.zeros(1, self.spectral_tokens, embed_dim))
33
+ encoder_layer = nn.TransformerEncoderLayer(
34
+ embed_dim, encoder_heads, embed_dim * 4, batch_first=True, norm_first=True
35
+ )
36
+ self.encoder = nn.TransformerEncoder(encoder_layer, encoder_depth)
37
+ self.encoder_norm = nn.LayerNorm(embed_dim)
38
+ self.decoder_embed = nn.Linear(embed_dim, decoder_dim)
39
+ self.mask_token = nn.Parameter(torch.zeros(1, 1, decoder_dim))
40
+ self.decoder_pos = nn.Linear(embed_dim, decoder_dim, bias=False)
41
+ decoder_layer = nn.TransformerEncoderLayer(
42
+ decoder_dim, decoder_heads, decoder_dim * 4, batch_first=True, norm_first=True
43
+ )
44
+ self.decoder = nn.TransformerEncoder(decoder_layer, decoder_depth)
45
+ self.decoder_norm = nn.LayerNorm(decoder_dim)
46
+ self.decoder_pred = nn.Linear(decoder_dim, self.token_pixels)
47
+ nn.init.normal_(self.spatial_pos, std=0.02)
48
+ nn.init.normal_(self.spectral_pos, std=0.02)
49
+ nn.init.normal_(self.mask_token, std=0.02)
50
+
51
+ def _positions(self):
52
+ return (self.spatial_pos[:, None] + self.spectral_pos[:, :, None]).reshape(
53
+ 1, self.num_tokens, -1
54
+ )
55
+
56
+ def patchify(self, images):
57
+ p, k = self.patch_size, self.spectral_patch_size
58
+ n, c, h, w = images.shape
59
+ if (c, h, w) != (self.in_channels, self.image_size, self.image_size):
60
+ raise ValueError(f"Expected [N,{self.in_channels},{self.image_size},{self.image_size}]")
61
+ x = images.reshape(n, c // k, k, h // p, p, w // p, p)
62
+ x = x.permute(0, 1, 3, 5, 2, 4, 6)
63
+ return x.reshape(n, self.num_tokens, self.token_pixels)
64
+
65
+ def unpatchify(self, tokens):
66
+ p, k = self.patch_size, self.spectral_patch_size
67
+ n = tokens.shape[0]
68
+ s = self.image_size // p
69
+ x = tokens.reshape(n, self.spectral_tokens, s, s, k, p, p)
70
+ x = x.permute(0, 1, 4, 2, 5, 3, 6)
71
+ return x.reshape(n, self.in_channels, self.image_size, self.image_size)
72
+
73
+ @staticmethod
74
+ def random_masking(tokens, mask_ratio):
75
+ n, length, dim = tokens.shape
76
+ keep = max(1, int(length * (1.0 - mask_ratio)))
77
+ order = torch.argsort(torch.rand(n, length, device=tokens.device), dim=1)
78
+ restore = torch.argsort(order, dim=1)
79
+ keep_ids = order[:, :keep]
80
+ visible = torch.gather(tokens, 1, keep_ids.unsqueeze(-1).expand(-1, -1, dim))
81
+ mask = torch.ones(n, length, device=tokens.device)
82
+ mask[:, :keep] = 0
83
+ mask = torch.gather(mask, 1, restore)
84
+ return visible, mask, restore
85
+
86
+ def forward(self, images, mask_ratio=None):
87
+ ratio = self.mask_ratio if mask_ratio is None else mask_ratio
88
+ embedded = self.patch_embed(images.unsqueeze(1)).flatten(2).transpose(1, 2)
89
+ positions = self._positions()
90
+ visible, mask, restore = self.random_masking(embedded + positions, ratio)
91
+ latent = self.encoder_norm(self.encoder(visible))
92
+ decoded_visible = self.decoder_embed(latent)
93
+ missing = self.num_tokens - decoded_visible.shape[1]
94
+ full = torch.cat([decoded_visible, self.mask_token.expand(images.shape[0], missing, -1)], 1)
95
+ full = torch.gather(full, 1, restore.unsqueeze(-1).expand(-1, -1, full.shape[-1]))
96
+ prediction = self.decoder_pred(self.decoder_norm(self.decoder(full + self.decoder_pos(positions))))
97
+ target = self.patchify(images)
98
+ token_error = (prediction - target).pow(2).mean(-1)
99
+ token_loss = (token_error * mask).sum() / mask.sum().clamp_min(1)
100
+ n = images.shape[0]
101
+ predicted_grid = prediction.reshape(n, self.spectral_tokens, self.spatial_tokens, -1)
102
+ target_grid = target.reshape(n, self.spectral_tokens, self.spatial_tokens, -1)
103
+ spectral_loss = (predicted_grid[:, 1:] - target_grid[:, 1:]).pow(2).mean()
104
+ loss = token_loss + self.spectral_loss_weight * spectral_loss
105
+ return {
106
+ "loss": loss,
107
+ "token_loss": token_loss,
108
+ "spectral_loss": spectral_loss,
109
+ "prediction": prediction,
110
+ "mask": mask,
111
+ "reconstruction": self.unpatchify(prediction),
112
+ }
scripts/fake_data.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ from pathlib import Path
3
+
4
+ import numpy as np
5
+ import yaml
6
+
7
+
8
+ def real_images(directory, size, samples):
9
+ import tifffile
10
+
11
+ paths = sorted(Path(directory).rglob("*.tif"))[:samples]
12
+ if not paths:
13
+ raise FileNotFoundError(f"No TIFF files found under {directory}")
14
+ images = []
15
+ for path in paths:
16
+ image = tifffile.imread(path).astype(np.float32)
17
+ if image.ndim != 3:
18
+ raise ValueError(f"Expected a 13-band TIFF, got {image.shape} from {path}")
19
+ if image.shape[0] == 13:
20
+ image = image.transpose(1, 2, 0)
21
+ if image.shape[-1] != 13:
22
+ raise ValueError(f"Expected 13 Sentinel-2 bands, got {image.shape} from {path}")
23
+ image = np.delete(image, 10, axis=-1) # Exclude Sentinel-2 B10 as in the paper.
24
+ y = np.linspace(0, image.shape[0] - 1, size).round().astype(int)
25
+ x = np.linspace(0, image.shape[1] - 1, size).round().astype(int)
26
+ image = image[y][:, x]
27
+ images.append(np.clip(image / 10000.0, 0, 1).transpose(2, 0, 1))
28
+ return np.asarray(images, dtype=np.float32)
29
+
30
+
31
+ def main():
32
+ parser = argparse.ArgumentParser(description="Generate compact 12-band spectral data")
33
+ parser.add_argument("--config", default="conf/config.yaml")
34
+ parser.add_argument("--real-dir", help="Convert official 13-band Sentinel-2 TIFF files")
35
+ args = parser.parse_args()
36
+ with open(args.config, encoding="utf-8") as handle:
37
+ config = yaml.safe_load(handle)
38
+ size = config["model"]["image_size"]
39
+ samples = config["data"]["samples"]
40
+ if args.real_dir:
41
+ images = real_images(args.real_dir, size, samples)
42
+ source = "real"
43
+ protocol = "real_sentinel2_npz"
44
+ else:
45
+ rng = np.random.default_rng(config["runtime"]["seed"])
46
+ y, x = np.mgrid[0:size, 0:size].astype(np.float32) / max(size - 1, 1)
47
+ images = []
48
+ for index in range(samples):
49
+ phase = rng.uniform(0, 2 * np.pi)
50
+ bands = []
51
+ for band in range(12):
52
+ pattern = 0.45 + 0.22 * np.sin((band + 1) * x + phase)
53
+ pattern += 0.18 * np.cos((band / 3 + 1) * y - phase)
54
+ pattern += rng.normal(0, 0.025, (size, size))
55
+ bands.append(np.clip(pattern, 0, 1))
56
+ images.append(bands)
57
+ images = np.asarray(images, dtype=np.float32)
58
+ source = "synthetic"
59
+ protocol = config["data"]["protocol"]
60
+ output = Path(config["data"]["path"])
61
+ output.parent.mkdir(parents=True, exist_ok=True)
62
+ np.savez_compressed(output, images=images, data_source=np.asarray(source), protocol=np.asarray(protocol))
63
+ print(f"saved: {output} shape={images.shape} data_source={source} protocol={protocol}")
64
+
65
+
66
+ if __name__ == "__main__":
67
+ main()
scripts/inference.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ from pathlib import Path
3
+ import sys
4
+
5
+ import numpy as np
6
+ import torch
7
+ import yaml
8
+
9
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
10
+ from model.spectralgpt import SpectralGPT
11
+
12
+
13
+ def main():
14
+ parser = argparse.ArgumentParser(description="Run SpectralGPT reconstruction")
15
+ parser.add_argument("--config", default="conf/config.yaml")
16
+ parser.add_argument("--checkpoint")
17
+ args = parser.parse_args()
18
+ with open(args.config, encoding="utf-8") as handle:
19
+ config = yaml.safe_load(handle)
20
+ requested = config["runtime"]["device"]
21
+ device = torch.device("cuda" if torch.cuda.is_available() and requested != "cpu" else "cpu")
22
+ torch.manual_seed(config["runtime"]["seed"])
23
+ model = SpectralGPT(**config["model"]).to(device)
24
+ checkpoint_path = args.checkpoint or config["training"]["checkpoint"]
25
+ if not Path(checkpoint_path).exists():
26
+ raise FileNotFoundError(
27
+ f"Missing checkpoint: {checkpoint_path}. Run `python scripts/train.py` first."
28
+ )
29
+ checkpoint = torch.load(checkpoint_path, map_location=device, weights_only=False)
30
+ model.load_state_dict(checkpoint["model"])
31
+ model.eval()
32
+ data_path = Path(config["data"]["path"])
33
+ if not data_path.exists():
34
+ raise FileNotFoundError(
35
+ f"Missing inference data: {data_path}. Run `python scripts/fake_data.py` first."
36
+ )
37
+ with np.load(data_path) as data:
38
+ images = torch.from_numpy(data["images"]).to(device)
39
+ data_source = str(data["data_source"]) if "data_source" in data.files else "unknown"
40
+ protocol = str(data["protocol"]) if "protocol" in data.files else "unknown"
41
+ with torch.no_grad():
42
+ output = model(images)
43
+ output_dir = Path(config["runtime"]["output_dir"])
44
+ output_dir.mkdir(parents=True, exist_ok=True)
45
+ np.savez_compressed(output_dir / "reconstruction.npz",
46
+ inputs=images.cpu().numpy(),
47
+ reconstructions=output["reconstruction"].cpu().numpy(),
48
+ masks=output["mask"].cpu().numpy(),
49
+ data_source=np.asarray(data_source),
50
+ protocol=np.asarray(protocol))
51
+ print(
52
+ f"saved: {output_dir / 'reconstruction.npz'} "
53
+ f"data_source={data_source} protocol={protocol}"
54
+ )
55
+
56
+
57
+ if __name__ == "__main__":
58
+ main()
scripts/result.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import json
3
+ from pathlib import Path
4
+
5
+ import numpy as np
6
+ import yaml
7
+
8
+
9
+ def rgb(image):
10
+ array = np.clip(image[[3, 2, 1]], 0, 1).transpose(1, 2, 0)
11
+ return (array * 255).astype(np.uint8)
12
+
13
+
14
+ def main():
15
+ parser = argparse.ArgumentParser(description="Evaluate and visualize reconstruction")
16
+ parser.add_argument("--config", default="conf/config.yaml")
17
+ args = parser.parse_args()
18
+ with open(args.config, encoding="utf-8") as handle:
19
+ config = yaml.safe_load(handle)
20
+ output_dir = Path(config["runtime"]["output_dir"])
21
+ reconstruction_path = output_dir / "reconstruction.npz"
22
+ if not reconstruction_path.exists():
23
+ raise FileNotFoundError(
24
+ f"Missing inference output: {reconstruction_path}. "
25
+ "Run `python scripts/inference.py` first."
26
+ )
27
+ with np.load(reconstruction_path) as data:
28
+ inputs = data["inputs"]
29
+ reconstructions = data["reconstructions"]
30
+ data_source = str(data["data_source"]) if "data_source" in data.files else "unknown"
31
+ protocol = str(data["protocol"]) if "protocol" in data.files else "unknown"
32
+ mse = float(np.mean((inputs - reconstructions) ** 2))
33
+ mae = float(np.mean(np.abs(inputs - reconstructions)))
34
+ psnr = float(-10 * np.log10(max(mse, 1e-12)))
35
+ spectral_rmse = np.sqrt(np.mean((inputs - reconstructions) ** 2, axis=(0, 2, 3)))
36
+ metrics = {"mse": mse, "mae": mae, "psnr_db": psnr,
37
+ "data_source": data_source, "protocol": protocol,
38
+ "per_band_rmse": spectral_rmse.tolist()}
39
+ with open(output_dir / "metrics.json", "w", encoding="utf-8") as handle:
40
+ json.dump(metrics, handle, indent=2)
41
+ panel = np.concatenate([rgb(inputs[0]), rgb(reconstructions[0])], axis=1)
42
+ with open(output_dir / "reconstruction.ppm", "wb") as handle:
43
+ handle.write(f"P6\n{panel.shape[1]} {panel.shape[0]}\n255\n".encode("ascii"))
44
+ handle.write(panel.tobytes())
45
+ print(json.dumps(metrics, indent=2))
46
+ print(f"saved: {output_dir / 'metrics.json'}")
47
+ print(f"saved: {output_dir / 'reconstruction.ppm'}")
48
+
49
+
50
+ if __name__ == "__main__":
51
+ main()
scripts/train.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import os
3
+ from pathlib import Path
4
+ import sys
5
+
6
+ import numpy as np
7
+ import torch
8
+ import yaml
9
+ from torch.nn.parallel import DistributedDataParallel
10
+ from torch.utils.data import DataLoader, Dataset, DistributedSampler
11
+
12
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
13
+ from model.spectralgpt import SpectralGPT
14
+
15
+
16
+ class SpectralDataset(Dataset):
17
+ def __init__(self, path):
18
+ with np.load(path) as data:
19
+ if "images" not in data.files:
20
+ raise ValueError(f"Dataset {path} is missing the images array")
21
+ self.images = data["images"].copy()
22
+ self.data_source = str(data["data_source"]) if "data_source" in data.files else "unknown"
23
+ self.protocol = str(data["protocol"]) if "protocol" in data.files else "unknown"
24
+
25
+ def __len__(self):
26
+ return len(self.images)
27
+
28
+ def __getitem__(self, index):
29
+ return torch.from_numpy(self.images[index])
30
+
31
+
32
+ def load_config(path):
33
+ with open(path, encoding="utf-8") as handle:
34
+ return yaml.safe_load(handle)
35
+
36
+
37
+ def build_model(config):
38
+ return SpectralGPT(**config["model"])
39
+
40
+
41
+ def main():
42
+ parser = argparse.ArgumentParser(description="Train compact SpectralGPT")
43
+ parser.add_argument("--config", default="conf/config.yaml")
44
+ parser.add_argument("--data")
45
+ parser.add_argument("--epochs", type=int)
46
+ args = parser.parse_args()
47
+ config = load_config(args.config)
48
+ distributed = int(os.environ.get("WORLD_SIZE", "1")) > 1
49
+ local_rank = int(os.environ.get("LOCAL_RANK", "0"))
50
+ if distributed:
51
+ torch.distributed.init_process_group("nccl" if torch.cuda.is_available() else "gloo")
52
+ requested = config["runtime"]["device"]
53
+ use_accelerator = torch.cuda.is_available() and requested != "cpu"
54
+ device = torch.device(f"cuda:{local_rank}" if use_accelerator else "cpu")
55
+ torch.manual_seed(config["runtime"]["seed"] + local_rank)
56
+ data_path = Path(args.data or config["data"]["path"])
57
+ if not data_path.exists():
58
+ raise FileNotFoundError(
59
+ f"Missing training data: {data_path}. "
60
+ "Run `python scripts/fake_data.py` for a synthetic connectivity test."
61
+ )
62
+ dataset = SpectralDataset(data_path)
63
+ if dataset.images.ndim != 4 or tuple(dataset.images.shape[1:]) != (
64
+ config["model"]["in_channels"], config["model"]["image_size"], config["model"]["image_size"]
65
+ ):
66
+ raise ValueError(
67
+ f"Expected images shaped [N,{config['model']['in_channels']},"
68
+ f"{config['model']['image_size']},{config['model']['image_size']}], "
69
+ f"got {dataset.images.shape}"
70
+ )
71
+ if local_rank == 0:
72
+ print(
73
+ f"data_source={dataset.data_source} protocol={dataset.protocol} "
74
+ f"samples={len(dataset)}"
75
+ )
76
+ sampler = DistributedSampler(dataset, shuffle=True) if distributed else None
77
+ loader = DataLoader(dataset, batch_size=config["training"]["batch_size"],
78
+ sampler=sampler, shuffle=sampler is None)
79
+ model = build_model(config).to(device)
80
+ if distributed:
81
+ model = DistributedDataParallel(model, device_ids=[local_rank] if use_accelerator else None)
82
+ optimizer = torch.optim.AdamW(model.parameters(), lr=config["training"]["learning_rate"],
83
+ weight_decay=config["training"]["weight_decay"], betas=(0.9, 0.95))
84
+ save_dir = Path(config["training"]["save_dir"])
85
+ best = float("inf")
86
+ for epoch in range(args.epochs or config["training"]["epochs"]):
87
+ if sampler is not None:
88
+ sampler.set_epoch(epoch)
89
+ model.train()
90
+ losses = []
91
+ for images in loader:
92
+ output = model(images.to(device))
93
+ optimizer.zero_grad()
94
+ output["loss"].backward()
95
+ optimizer.step()
96
+ losses.append(output["loss"].item())
97
+ mean_loss = float(np.mean(losses))
98
+ if local_rank == 0:
99
+ print(f"epoch={epoch + 1} reconstruction_loss={mean_loss:.6f}")
100
+ save_dir.mkdir(parents=True, exist_ok=True)
101
+ state = model.module.state_dict() if distributed else model.state_dict()
102
+ checkpoint = {"model": state, "epoch": epoch + 1, "loss": mean_loss,
103
+ "data_source": dataset.data_source, "protocol": dataset.protocol,
104
+ "format": "spectralgpt-compact-v1"}
105
+ torch.save(checkpoint, save_dir / "last.pth")
106
+ if mean_loss < best:
107
+ best = mean_loss
108
+ torch.save(checkpoint, save_dir / "best.pth")
109
+ if local_rank == 0:
110
+ print(f"saved: {save_dir / 'best.pth'}")
111
+ if distributed:
112
+ torch.distributed.destroy_process_group()
113
+
114
+
115
+ if __name__ == "__main__":
116
+ main()
weight/.gitkeep ADDED
File without changes