Upload Scale-MAE model package
Browse files- .gitattributes +5 -27
- conf/config.yaml +44 -0
- config.json +71 -0
- configuration.json +12 -0
- model/scalemae.py +107 -0
- scripts/fake_data.py +26 -0
- scripts/inference.py +17 -0
- scripts/result.py +84 -0
- scripts/train.py +47 -0
- weight/.gitkeep +0 -0
.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 |
-
|
| 27 |
-
*.
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
conf/config.yaml
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
seed: 42
|
| 2 |
+
data:
|
| 3 |
+
root: data
|
| 4 |
+
protocol: synthetic_multiscale_geospatial_rgb
|
| 5 |
+
input_size: 16
|
| 6 |
+
target_size: 32
|
| 7 |
+
channels: 3
|
| 8 |
+
num_classes: 4
|
| 9 |
+
train_samples: 40
|
| 10 |
+
test_samples: 16
|
| 11 |
+
gsd_values: [0.5, 1.0, 2.0, 4.0]
|
| 12 |
+
model:
|
| 13 |
+
input_size: 16
|
| 14 |
+
target_size: 32
|
| 15 |
+
patch_size: 4
|
| 16 |
+
in_channels: 3
|
| 17 |
+
embed_dim: 64
|
| 18 |
+
encoder_depth: 2
|
| 19 |
+
encoder_heads: 4
|
| 20 |
+
decoder_dim: 48
|
| 21 |
+
decoder_depth: 1
|
| 22 |
+
decoder_heads: 4
|
| 23 |
+
mask_ratio: 0.75
|
| 24 |
+
reference_gsd: 1.0
|
| 25 |
+
blur_kernel: 5
|
| 26 |
+
band_config:
|
| 27 |
+
low: {kernel: 5, target: low_frequency}
|
| 28 |
+
high: {residual: true, target: high_frequency}
|
| 29 |
+
training:
|
| 30 |
+
epochs: 3
|
| 31 |
+
batch_size: 8
|
| 32 |
+
learning_rate: 0.0005
|
| 33 |
+
weight_decay: 0.05
|
| 34 |
+
num_workers: 0
|
| 35 |
+
gradient_accumulation: 1
|
| 36 |
+
amp: true
|
| 37 |
+
warmup_fraction: 0.1
|
| 38 |
+
runtime:
|
| 39 |
+
device: auto
|
| 40 |
+
paths:
|
| 41 |
+
checkpoint: result/checkpoints/scalemae.pt
|
| 42 |
+
training_metrics: result/training/metrics.json
|
| 43 |
+
inference_dir: result/output
|
| 44 |
+
evaluation_dir: result/evaluation
|
config.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_name": "Scale-MAE",
|
| 3 |
+
"model_type": "scale_mae",
|
| 4 |
+
"architectures": [
|
| 5 |
+
"ScaleMAE"
|
| 6 |
+
],
|
| 7 |
+
"framework": "PyTorch",
|
| 8 |
+
"domain": "earth-observation",
|
| 9 |
+
"task": "multiscale-geospatial-representation-learning",
|
| 10 |
+
"implementation": {
|
| 11 |
+
"entry_point": "model/scalemae.py",
|
| 12 |
+
"scope": "GSD-aware masked autoencoding with paired low- and high-frequency target reconstruction",
|
| 13 |
+
"train_script": "scripts/train.py",
|
| 14 |
+
"inference_script": "scripts/inference.py",
|
| 15 |
+
"evaluation_script": "scripts/result.py",
|
| 16 |
+
"synthetic_data_script": "scripts/fake_data.py"
|
| 17 |
+
},
|
| 18 |
+
"architecture": {
|
| 19 |
+
"family": "scale-aware masked autoencoder",
|
| 20 |
+
"input_size": 16,
|
| 21 |
+
"target_size": 32,
|
| 22 |
+
"patch_size": 4,
|
| 23 |
+
"input_patches": 16,
|
| 24 |
+
"in_channels": 3,
|
| 25 |
+
"embed_dim": 64,
|
| 26 |
+
"encoder_depth": 2,
|
| 27 |
+
"encoder_heads": 4,
|
| 28 |
+
"decoder_dim": 48,
|
| 29 |
+
"decoder_depth": 1,
|
| 30 |
+
"decoder_heads": 4,
|
| 31 |
+
"mask_ratio": 0.75,
|
| 32 |
+
"reference_gsd": 1.0,
|
| 33 |
+
"blur_kernel": 5,
|
| 34 |
+
"band_config": {
|
| 35 |
+
"low": {"kernel": 5, "target": "low_frequency"},
|
| 36 |
+
"high": {"residual": true, "target": "high_frequency"}
|
| 37 |
+
}
|
| 38 |
+
},
|
| 39 |
+
"data": {
|
| 40 |
+
"datasets": [
|
| 41 |
+
"FMoW-RGB",
|
| 42 |
+
"RESISC-45",
|
| 43 |
+
"EuroSAT",
|
| 44 |
+
"UCMerced",
|
| 45 |
+
"AID",
|
| 46 |
+
"MLRSNet"
|
| 47 |
+
],
|
| 48 |
+
"protocol": "synthetic_multiscale_geospatial_rgb",
|
| 49 |
+
"format": "NPZ",
|
| 50 |
+
"input_key": "images",
|
| 51 |
+
"input_shape": ["N", 3, 16, 16],
|
| 52 |
+
"target_key": "targets",
|
| 53 |
+
"target_shape": ["N", 3, 32, 32],
|
| 54 |
+
"gsd_key": "gsd",
|
| 55 |
+
"gsd_shape": ["N"],
|
| 56 |
+
"gsd_values": [0.5, 1.0, 2.0, 4.0],
|
| 57 |
+
"label_key": "labels",
|
| 58 |
+
"label_shape": ["N"],
|
| 59 |
+
"num_classes": 4,
|
| 60 |
+
"default_train_file": "data/train.npz",
|
| 61 |
+
"default_test_file": "data/test.npz"
|
| 62 |
+
},
|
| 63 |
+
"configuration_sources": [
|
| 64 |
+
"conf/config.yaml",
|
| 65 |
+
"model/scalemae.py",
|
| 66 |
+
"scripts/fake_data.py",
|
| 67 |
+
"scripts/train.py",
|
| 68 |
+
"scripts/inference.py",
|
| 69 |
+
"scripts/result.py"
|
| 70 |
+
]
|
| 71 |
+
}
|
configuration.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"framework": "PyTorch",
|
| 3 |
+
"task": "remote_sensing_representation_learning",
|
| 4 |
+
"model": "Scale-MAE",
|
| 5 |
+
"input_format": "BCHW_with_GSD",
|
| 6 |
+
"protocol": "scale_aware_masked_autoencoding",
|
| 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/scalemae.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Scale-aware masked autoencoder for paired remote-sensing resolutions."""
|
| 2 |
+
import math
|
| 3 |
+
import torch
|
| 4 |
+
from torch import nn
|
| 5 |
+
from torch.nn import functional as F
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def sincos_2d(height, width, dim, device, dtype, scale=1.0):
|
| 9 |
+
if dim % 4:
|
| 10 |
+
raise ValueError("embedding dimension must be divisible by four")
|
| 11 |
+
y, x = torch.meshgrid(torch.arange(height, device=device, dtype=dtype),
|
| 12 |
+
torch.arange(width, device=device, dtype=dtype), indexing="ij")
|
| 13 |
+
quarter = dim // 4
|
| 14 |
+
omega = torch.exp(-math.log(10000.0) * torch.arange(quarter, device=device, dtype=dtype) /
|
| 15 |
+
max(quarter - 1, 1))
|
| 16 |
+
x, y = x.flatten()[:, None] * scale, y.flatten()[:, None] * scale
|
| 17 |
+
return torch.cat((torch.sin(x * omega), torch.cos(x * omega),
|
| 18 |
+
torch.sin(y * omega), torch.cos(y * omega)), dim=1)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class ScaleMAE(nn.Module):
|
| 22 |
+
def __init__(self, input_size=32, target_size=64, patch_size=4, in_channels=3,
|
| 23 |
+
embed_dim=64, encoder_depth=2, encoder_heads=4, decoder_dim=48,
|
| 24 |
+
decoder_depth=1, decoder_heads=4, mask_ratio=0.75, reference_gsd=1.0,
|
| 25 |
+
blur_kernel=5, band_config=None, **legacy):
|
| 26 |
+
super().__init__()
|
| 27 |
+
input_size = legacy.get("image_size", input_size)
|
| 28 |
+
self.input_size, self.target_size, self.patch_size = input_size, target_size, patch_size
|
| 29 |
+
self.in_channels, self.embed_dim, self.mask_ratio = in_channels, embed_dim, mask_ratio
|
| 30 |
+
self.reference_gsd, self.blur_kernel = reference_gsd, blur_kernel
|
| 31 |
+
if input_size % patch_size or target_size % patch_size:
|
| 32 |
+
raise ValueError("input_size and target_size must be divisible by patch_size")
|
| 33 |
+
self.input_grid = input_size // patch_size
|
| 34 |
+
self.target_grid = target_size // patch_size
|
| 35 |
+
self.num_patches = self.input_grid ** 2
|
| 36 |
+
patch_dim = in_channels * patch_size ** 2
|
| 37 |
+
self.patch_embed = nn.Linear(patch_dim, embed_dim)
|
| 38 |
+
self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim))
|
| 39 |
+
self.mask_token = nn.Parameter(torch.zeros(1, 1, decoder_dim))
|
| 40 |
+
enc = nn.TransformerEncoderLayer(embed_dim, encoder_heads, embed_dim * 4,
|
| 41 |
+
dropout=0, activation="gelu", batch_first=True, norm_first=True)
|
| 42 |
+
self.encoder, self.encoder_norm = nn.TransformerEncoder(enc, encoder_depth), nn.LayerNorm(embed_dim)
|
| 43 |
+
self.decoder_input = nn.Linear(embed_dim, decoder_dim)
|
| 44 |
+
dec = nn.TransformerEncoderLayer(decoder_dim, decoder_heads, decoder_dim * 4,
|
| 45 |
+
dropout=0, activation="gelu", batch_first=True, norm_first=True)
|
| 46 |
+
self.decoder, self.decoder_norm = nn.TransformerEncoder(dec, decoder_depth), nn.LayerNorm(decoder_dim)
|
| 47 |
+
self.fpn = nn.Sequential(nn.Conv2d(decoder_dim, decoder_dim, 3, padding=1), nn.GELU(),
|
| 48 |
+
nn.Conv2d(decoder_dim, decoder_dim, 3, padding=1), nn.GELU())
|
| 49 |
+
self.low_head = nn.Conv2d(decoder_dim, in_channels, 1)
|
| 50 |
+
self.high_head = nn.Conv2d(decoder_dim, in_channels, 1)
|
| 51 |
+
self.band_config = band_config or {"low": {"kernel": blur_kernel}, "high": {"residual": True}}
|
| 52 |
+
nn.init.normal_(self.cls_token, std=.02); nn.init.normal_(self.mask_token, std=.02)
|
| 53 |
+
|
| 54 |
+
def patchify(self, images):
|
| 55 |
+
b, c, h, w = images.shape; p = self.patch_size
|
| 56 |
+
if (c, h, w) != (self.in_channels, self.input_size, self.input_size):
|
| 57 |
+
raise ValueError(f"expected input {(self.in_channels, self.input_size, self.input_size)}, got {(c,h,w)}")
|
| 58 |
+
return images.reshape(b, c, h//p, p, w//p, p).permute(0,2,4,1,3,5).reshape(b, -1, c*p*p)
|
| 59 |
+
|
| 60 |
+
def bandpass_targets(self, target):
|
| 61 |
+
k = int(self.band_config.get("low", {}).get("kernel", self.blur_kernel))
|
| 62 |
+
if k < 3 or k % 2 == 0: raise ValueError("low-frequency kernel must be odd and at least three")
|
| 63 |
+
low = F.avg_pool2d(target, k, 1, k//2, count_include_pad=False)
|
| 64 |
+
return low, target - low
|
| 65 |
+
|
| 66 |
+
def _positions(self, gsd, grid, dim):
|
| 67 |
+
scales = gsd.to(dtype=self.cls_token.dtype).flatten() / self.reference_gsd
|
| 68 |
+
base = sincos_2d(grid, grid, dim, gsd.device, self.cls_token.dtype)
|
| 69 |
+
return base.unsqueeze(0) * scales[:, None, None]
|
| 70 |
+
|
| 71 |
+
def forward(self, images, gsd, target=None, mask_ratio=None, return_features=True):
|
| 72 |
+
if target is None: target = F.interpolate(images, (self.target_size, self.target_size), mode="bilinear", align_corners=False)
|
| 73 |
+
if target.shape[-2:] != (self.target_size, self.target_size):
|
| 74 |
+
raise ValueError("target resolution does not match target_size")
|
| 75 |
+
if target.shape[:2] != (images.shape[0], self.in_channels):
|
| 76 |
+
raise ValueError("target batch or channel dimensions do not match images")
|
| 77 |
+
b = images.shape[0]; patches = self.patchify(images); n = patches.shape[1]
|
| 78 |
+
ratio = self.mask_ratio if mask_ratio is None else float(mask_ratio)
|
| 79 |
+
if not 0 <= ratio < 1: raise ValueError("mask_ratio must be in [0, 1)")
|
| 80 |
+
noise = torch.rand(b, n, device=images.device)
|
| 81 |
+
ids_shuffle = noise.argsort(dim=1); ids_restore = ids_shuffle.argsort(dim=1)
|
| 82 |
+
keep = max(1, int(n * (1 - ratio))); ids_keep = ids_shuffle[:, :keep]
|
| 83 |
+
visible = torch.gather(self.patch_embed(patches), 1, ids_keep[..., None].expand(-1, -1, self.embed_dim))
|
| 84 |
+
visible = visible + torch.gather(self._positions(gsd, self.input_grid, self.embed_dim), 1,
|
| 85 |
+
ids_keep[..., None].expand(-1, -1, self.embed_dim))
|
| 86 |
+
encoded = self.encoder_norm(self.encoder(torch.cat((self.cls_token.expand(b,-1,-1), visible), 1)))
|
| 87 |
+
cls, visible_encoded = encoded[:, :1], encoded[:, 1:]
|
| 88 |
+
decoded = self.decoder_input(visible_encoded)
|
| 89 |
+
full = self.mask_token.to(decoded.dtype).expand(b, n, -1).clone(); full.scatter_(1, ids_keep[..., None].expand(-1,-1,decoded.shape[-1]), decoded)
|
| 90 |
+
full = full + self._positions(gsd, self.input_grid, full.shape[-1])
|
| 91 |
+
full = self.decoder_norm(self.decoder(full))
|
| 92 |
+
fmap = full.transpose(1, 2).reshape(b, -1, self.input_grid, self.input_grid)
|
| 93 |
+
fmap = F.interpolate(fmap, (self.target_grid, self.target_grid), mode="bilinear", align_corners=False)
|
| 94 |
+
fmap = self.fpn(fmap)
|
| 95 |
+
low = F.interpolate(self.low_head(fmap), (self.target_size, self.target_size), mode="bilinear", align_corners=False)
|
| 96 |
+
high = F.interpolate(self.high_head(fmap), (self.target_size, self.target_size), mode="bilinear", align_corners=False)
|
| 97 |
+
low_target, high_target = self.bandpass_targets(target)
|
| 98 |
+
mask = torch.ones(b, n, dtype=torch.bool, device=images.device); mask.scatter_(1, ids_keep, False)
|
| 99 |
+
pixel_mask = F.interpolate(mask.view(b, 1, self.input_grid, self.input_grid).float(),
|
| 100 |
+
(self.target_size, self.target_size), mode="nearest")
|
| 101 |
+
masked = pixel_mask.sum().clamp_min(1.0) * self.in_channels
|
| 102 |
+
low_loss = ((low-low_target).square() * pixel_mask).sum() / masked
|
| 103 |
+
high_loss = ((high-high_target).square() * pixel_mask).sum() / masked
|
| 104 |
+
return {"loss": low_loss + high_loss, "low_loss": low_loss, "high_loss": high_loss,
|
| 105 |
+
"reconstruction": low + high, "low_reconstruction": low, "high_reconstruction": high,
|
| 106 |
+
"low_target": low_target, "high_target": high_target, "mask": mask,
|
| 107 |
+
"ids_restore": ids_restore, "features": cls[:, 0] if return_features else None}
|
scripts/fake_data.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Create paired low/high resolution scenes with labels for kNN evaluation."""
|
| 2 |
+
import argparse, json
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import numpy as np, yaml
|
| 5 |
+
from PIL import Image
|
| 6 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 7 |
+
|
| 8 |
+
def make_split(count, cfg, rng):
|
| 9 |
+
size, target, channels, classes = cfg["input_size"], cfg["target_size"], cfg["channels"], cfg["num_classes"]
|
| 10 |
+
labels = np.arange(count, dtype=np.int64) % classes; rng.shuffle(labels)
|
| 11 |
+
gsd = rng.choice(np.asarray(cfg["gsd_values"], dtype=np.float32), count)
|
| 12 |
+
y, x = np.mgrid[:target, :target].astype(np.float32); images = np.empty((count, channels, size, size), np.float32); targets = np.empty((count, channels, target, target), np.float32)
|
| 13 |
+
for i, label in enumerate(labels):
|
| 14 |
+
pattern = np.sin((x + label*2)*np.pi*(label+1)/target) + np.cos((y-label*2)*np.pi*(label+1)/target)
|
| 15 |
+
pattern = (pattern-pattern.min())/(pattern.max()-pattern.min())
|
| 16 |
+
scene = np.stack([np.roll(pattern, label*c, axis=c%2) for c in range(channels)])
|
| 17 |
+
targets[i] = np.clip(scene + rng.normal(0, .02 + .01*gsd[i], scene.shape), 0, 1)
|
| 18 |
+
images[i] = np.asarray([Image.fromarray((targets[i,c]*255).astype('uint8')).resize((size,size), Image.Resampling.BOX) for c in range(channels)], dtype=np.float32)/255
|
| 19 |
+
return images, targets, labels, gsd
|
| 20 |
+
|
| 21 |
+
def main():
|
| 22 |
+
p=argparse.ArgumentParser(); p.add_argument("--config", default=str(ROOT/"conf/config.yaml")); a=p.parse_args(); cfg=yaml.safe_load(Path(a.config).read_text()); d=cfg["data"]; out=ROOT/d["root"]; out.mkdir(exist_ok=True); rng=np.random.default_rng(cfg["seed"])
|
| 23 |
+
for split,n in (("train",d["train_samples"]),("test",d["test_samples"])):
|
| 24 |
+
images,targets,labels,gsd=make_split(n,d,rng); np.savez_compressed(out/f"{split}.npz", images=images, targets=targets, labels=labels, gsd=gsd)
|
| 25 |
+
(out/"format.json").write_text(json.dumps({"images":"float32 BCHW input resolution","targets":"float32 BCHW target resolution","labels":"int64","gsd":"metres per pixel"},indent=2)+"\n"); print("created",out/"train.npz",out/"test.npz")
|
| 26 |
+
if __name__ == "__main__": main()
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Deterministic feature extraction and paired-resolution reconstruction."""
|
| 2 |
+
import argparse, importlib.util
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import numpy as np, torch, yaml
|
| 5 |
+
ROOT=Path(__file__).resolve().parents[1]
|
| 6 |
+
def load(cfg):
|
| 7 |
+
s=importlib.util.spec_from_file_location("scalemae",ROOT/"model/scalemae.py"); m=importlib.util.module_from_spec(s); s.loader.exec_module(m); return m.ScaleMAE(**cfg["model"])
|
| 8 |
+
def run(model,a,device,ratio):
|
| 9 |
+
with torch.no_grad(): return model(torch.from_numpy(a["images"]).to(device),torch.from_numpy(a["gsd"]).to(device),target=torch.from_numpy(a["targets"]).to(device),mask_ratio=ratio)
|
| 10 |
+
def main():
|
| 11 |
+
p=argparse.ArgumentParser(); p.add_argument("--config",type=Path,default=ROOT/"conf/config.yaml"); p.add_argument("--data",type=Path); p.add_argument("--train-data",type=Path); p.add_argument("--checkpoint",type=Path); p.add_argument("--output-dir",type=Path); p.add_argument("--device",choices=("auto","cpu","cuda")); a=p.parse_args(); cfg=yaml.safe_load(a.config.read_text()); ck=a.checkpoint or ROOT/cfg["paths"]["checkpoint"]
|
| 12 |
+
if not ck.exists(): raise FileNotFoundError("Run training before inference")
|
| 13 |
+
requested=a.device or cfg["runtime"]["device"]
|
| 14 |
+
if requested == "cuda" and not torch.cuda.is_available(): raise RuntimeError("CUDA requested but unavailable")
|
| 15 |
+
device=torch.device("cuda" if torch.cuda.is_available() and requested!="cpu" else "cpu"); torch.manual_seed(cfg["seed"]); model=load(cfg); model.load_state_dict(torch.load(ck,map_location=device,weights_only=False)["model"]); model.to(device).eval(); train=np.load(a.train_data or ROOT/cfg["data"]["root"]/"train.npz"); test=np.load(a.data or ROOT/cfg["data"]["root"]/"test.npz"); tr=run(model,train,device,0.0); te=run(model,test,device,cfg["model"]["mask_ratio"])
|
| 16 |
+
out=a.output_dir or ROOT/cfg["paths"]["inference_dir"]; out.mkdir(parents=True,exist_ok=True); np.savez_compressed(out/"reconstruction.npz",target=test["targets"],prediction=te["reconstruction"].cpu().numpy(),low_target=te["low_target"].cpu().numpy(),low_prediction=te["low_reconstruction"].cpu().numpy(),high_target=te["high_target"].cpu().numpy(),high_prediction=te["high_reconstruction"].cpu().numpy(),mask=te["mask"].cpu().numpy(),gsd=test["gsd"],labels=test["labels"],test_features=te["features"].cpu().numpy(),train_features=tr["features"].cpu().numpy(),train_labels=train["labels"],train_gsd=train["gsd"]); print("inference=",out/"reconstruction.npz")
|
| 17 |
+
if __name__=="__main__": main()
|
scripts/result.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Evaluate Scale-MAE reconstruction, kNN transfer and GSD sensitivity."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import argparse
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
import matplotlib.pyplot as plt
|
| 8 |
+
import numpy as np
|
| 9 |
+
import yaml
|
| 10 |
+
|
| 11 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def knn_predict(train_features, train_labels, test_features, k=3):
|
| 15 |
+
train = train_features / np.maximum(np.linalg.norm(train_features, axis=1, keepdims=True), 1e-8)
|
| 16 |
+
test = test_features / np.maximum(np.linalg.norm(test_features, axis=1, keepdims=True), 1e-8)
|
| 17 |
+
nearest = np.argsort(-(test @ train.T), axis=1)[:, :min(k, len(train))]
|
| 18 |
+
return np.asarray([np.bincount(train_labels[row]).argmax() for row in nearest])
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def rgb(image):
|
| 22 |
+
return np.clip(image[:3].transpose(1, 2, 0), 0, 1)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def main():
|
| 26 |
+
parser = argparse.ArgumentParser(description="Evaluate Scale-MAE outputs")
|
| 27 |
+
parser.add_argument("--config", default=str(ROOT / "conf/config.yaml"))
|
| 28 |
+
args = parser.parse_args()
|
| 29 |
+
cfg = yaml.safe_load(Path(args.config).read_text())
|
| 30 |
+
source = ROOT / cfg["paths"]["inference_dir"] / "reconstruction.npz"
|
| 31 |
+
if not source.exists():
|
| 32 |
+
raise FileNotFoundError("Run inference before evaluation")
|
| 33 |
+
data = np.load(source)
|
| 34 |
+
for key in ("prediction", "target", "test_features", "train_features", "labels", "gsd"):
|
| 35 |
+
if key not in data: raise ValueError(f"inference archive missing {key}")
|
| 36 |
+
if not np.isfinite(data["prediction"]).all(): raise FloatingPointError("non-finite inference output")
|
| 37 |
+
prediction = knn_predict(data["train_features"], data["train_labels"], data["test_features"])
|
| 38 |
+
accuracy = float(np.mean(prediction == data["labels"]))
|
| 39 |
+
mse = np.mean((data["prediction"] - data["target"]) ** 2, axis=(1, 2, 3))
|
| 40 |
+
low_mse = float(np.mean((data["low_prediction"] - data["low_target"]) ** 2))
|
| 41 |
+
high_mse = float(np.mean((data["high_prediction"] - data["high_target"]) ** 2))
|
| 42 |
+
gsd_values = sorted(np.unique(data["gsd"]).tolist())
|
| 43 |
+
gsd_mse = {str(value): float(np.mean(mse[data["gsd"] == value])) for value in gsd_values}
|
| 44 |
+
gsd_accuracy = {str(value): float(np.mean(prediction[data["gsd"] == value] == data["labels"][data["gsd"] == value]))
|
| 45 |
+
for value in gsd_values}
|
| 46 |
+
result = {"reconstruction_mse": float(np.mean(mse)), "low_frequency_mse": low_mse,
|
| 47 |
+
"high_frequency_mse": high_mse, "knn_accuracy": accuracy,
|
| 48 |
+
"gsd_reconstruction_mse": gsd_mse, "gsd_knn_accuracy": gsd_accuracy,
|
| 49 |
+
"data_source": "synthetic", "protocol": cfg["data"]["protocol"]}
|
| 50 |
+
output = ROOT / cfg["paths"]["evaluation_dir"]
|
| 51 |
+
output.mkdir(parents=True, exist_ok=True)
|
| 52 |
+
(output / "metrics.json").write_text(json.dumps(result, indent=2) + "\n")
|
| 53 |
+
|
| 54 |
+
figure, axes = plt.subplots(2, 3, figsize=(9, 6))
|
| 55 |
+
images = ((data["target"][0], "Original"), (data["low_target"][0], "Low target"),
|
| 56 |
+
(data["high_target"][0] + 0.5, "High target"), (data["prediction"][0], "Reconstruction"),
|
| 57 |
+
(data["low_prediction"][0], "Low prediction"), (data["high_prediction"][0] + 0.5, "High prediction"))
|
| 58 |
+
for axis, (image, title) in zip(axes.flat, images):
|
| 59 |
+
axis.imshow(rgb(image)); axis.set_title(title); axis.axis("off")
|
| 60 |
+
figure.tight_layout(); figure.savefig(output / "bandpass_reconstruction.png", dpi=160); plt.close(figure)
|
| 61 |
+
|
| 62 |
+
figure, axis = plt.subplots(figsize=(6, 3.5))
|
| 63 |
+
axis.plot(gsd_values, [gsd_mse[str(x)] for x in gsd_values], marker="o", color="#1f77b4")
|
| 64 |
+
axis.set(xlabel="GSD (m/pixel)", ylabel="Reconstruction MSE", title="Scale-aware Reconstruction")
|
| 65 |
+
axis.grid(alpha=0.25)
|
| 66 |
+
figure.tight_layout(); figure.savefig(output / "gsd_reconstruction_error.png", dpi=160); plt.close(figure)
|
| 67 |
+
|
| 68 |
+
figure, axis = plt.subplots(figsize=(6, 3.5))
|
| 69 |
+
axis.plot(gsd_values, [gsd_accuracy[str(x)] for x in gsd_values], marker="s", color="#d62728")
|
| 70 |
+
axis.set(xlabel="GSD (m/pixel)", ylabel="kNN accuracy", title="Scale-aware Feature Transfer")
|
| 71 |
+
axis.set_ylim(0, 1.05); axis.grid(alpha=0.25)
|
| 72 |
+
figure.tight_layout(); figure.savefig(output / "gsd_knn_accuracy.png", dpi=160); plt.close(figure)
|
| 73 |
+
|
| 74 |
+
figure, axis = plt.subplots(figsize=(5, 3))
|
| 75 |
+
axis.bar(["Low frequency", "High frequency"], [low_mse, high_mse], color=["#2a9d8f", "#e76f51"])
|
| 76 |
+
axis.set(ylabel="MSE", title="Bandpass Reconstruction Error")
|
| 77 |
+
figure.tight_layout(); figure.savefig(output / "frequency_error.png", dpi=160); plt.close(figure)
|
| 78 |
+
|
| 79 |
+
np.save(output / "features.npy", data["test_features"])
|
| 80 |
+
print(json.dumps(result, indent=2)); print("evaluation=", output)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
if __name__ == "__main__":
|
| 84 |
+
main()
|
scripts/train.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Train Scale-MAE with DDP, AMP, accumulation, warmup and cosine decay."""
|
| 2 |
+
import argparse, importlib.util, json, os, random
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import numpy as np, torch, yaml
|
| 5 |
+
from torch import distributed as dist
|
| 6 |
+
from torch.nn.parallel import DistributedDataParallel
|
| 7 |
+
from torch.utils.data import DataLoader, Dataset, DistributedSampler
|
| 8 |
+
ROOT=Path(__file__).resolve().parents[1]
|
| 9 |
+
class NPZDataset(Dataset):
|
| 10 |
+
def __init__(self,path):
|
| 11 |
+
a=np.load(path); self.images,self.targets,self.gsd=a["images"],a["targets"],a["gsd"]
|
| 12 |
+
def __len__(self): return len(self.images)
|
| 13 |
+
def __getitem__(self,i): return torch.from_numpy(self.images[i]), torch.from_numpy(self.targets[i]), torch.tensor(self.gsd[i], dtype=torch.float32)
|
| 14 |
+
def model_class():
|
| 15 |
+
spec=importlib.util.spec_from_file_location("scalemae",ROOT/"model/scalemae.py"); mod=importlib.util.module_from_spec(spec); spec.loader.exec_module(mod); return mod.ScaleMAE
|
| 16 |
+
def main():
|
| 17 |
+
p=argparse.ArgumentParser(); p.add_argument("--config",type=Path,default=ROOT/"conf/config.yaml"); p.add_argument("--data",type=Path); p.add_argument("--output",type=Path); p.add_argument("--epochs",type=int); p.add_argument("--batch-size",type=int); p.add_argument("--device",choices=("auto","cpu","cuda")); a=p.parse_args(); cfg=yaml.safe_load(a.config.read_text())
|
| 18 |
+
if a.epochs is not None: cfg["training"]["epochs"] = a.epochs
|
| 19 |
+
if a.batch_size is not None: cfg["training"]["batch_size"] = a.batch_size
|
| 20 |
+
world=int(os.environ.get("WORLD_SIZE","1")); rank=int(os.environ.get("RANK","0")); local=int(os.environ.get("LOCAL_RANK","0")); requested=a.device or cfg["runtime"]["device"]
|
| 21 |
+
if requested == "cuda" and not torch.cuda.is_available(): raise RuntimeError("CUDA requested but unavailable")
|
| 22 |
+
cuda=torch.cuda.is_available() and requested!="cpu"; device=torch.device(f"cuda:{local}" if cuda else "cpu")
|
| 23 |
+
if cuda: torch.cuda.set_device(local)
|
| 24 |
+
if world>1: dist.init_process_group("nccl" if cuda else "gloo")
|
| 25 |
+
seed=cfg["seed"]+rank; random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
|
| 26 |
+
source=a.data or ROOT/cfg["data"]["root"]/"train.npz"
|
| 27 |
+
if not source.exists(): raise FileNotFoundError("Run scripts/fake_data.py first")
|
| 28 |
+
ds=NPZDataset(source); sampler=DistributedSampler(ds,num_replicas=world,rank=rank) if world>1 else None; loader=DataLoader(ds,batch_size=cfg["training"]["batch_size"],shuffle=sampler is None,sampler=sampler,num_workers=cfg["training"]["num_workers"])
|
| 29 |
+
model=model_class()(**cfg["model"]).to(device); model=DistributedDataParallel(model,device_ids=[local]) if world>1 else model; base=model.module if hasattr(model,"module") else model
|
| 30 |
+
opt=torch.optim.AdamW(model.parameters(),lr=cfg["training"]["learning_rate"],betas=(.9,.95),weight_decay=cfg["training"]["weight_decay"]); steps=max(1,cfg["training"]["epochs"]*((len(ds)+cfg["training"]["batch_size"]-1)//cfg["training"]["batch_size"])); warm=max(1,int(steps*cfg["training"]["warmup_fraction"])); accum=cfg["training"]["gradient_accumulation"]
|
| 31 |
+
amp=torch.amp.GradScaler("cuda", enabled=cuda and cfg["training"]["amp"]); history=[]; opt.zero_grad(set_to_none=True); step=0
|
| 32 |
+
for epoch in range(cfg["training"]["epochs"]):
|
| 33 |
+
if sampler: sampler.set_epoch(epoch)
|
| 34 |
+
model.train(); totals=torch.zeros(3,device=device)
|
| 35 |
+
for batch_idx,(images,targets,gsd) in enumerate(loader):
|
| 36 |
+
with torch.autocast(device_type="cuda",enabled=cuda and cfg["training"]["amp"]): out=model(images.to(device),gsd.to(device),target=targets.to(device)); loss=out["loss"]/accum
|
| 37 |
+
if not torch.isfinite(loss): raise FloatingPointError("non-finite training loss")
|
| 38 |
+
amp.scale(loss).backward(); totals += torch.stack([out["loss"].detach(),out["low_loss"].detach(),out["high_loss"].detach()])
|
| 39 |
+
if (batch_idx+1)%accum==0 or batch_idx+1==len(loader):
|
| 40 |
+
amp.unscale_(opt); torch.nn.utils.clip_grad_norm_(model.parameters(),1.0); amp.step(opt); amp.update(); opt.zero_grad(set_to_none=True); step+=1; lr=cfg["training"]["learning_rate"]*(step/warm if step<=warm else .5*(1+np.cos(np.pi*(step-warm)/max(1,steps-warm)))); [g.update(lr=lr) for g in opt.param_groups]
|
| 41 |
+
if world>1: dist.all_reduce(totals); totals/=world
|
| 42 |
+
totals/=max(len(loader),1); history.append({"epoch":epoch+1,"loss":totals[0].item(),"low_frequency_loss":totals[1].item(),"high_frequency_loss":totals[2].item()})
|
| 43 |
+
if rank==0: print(history[-1])
|
| 44 |
+
if rank==0:
|
| 45 |
+
ck=a.output or ROOT/cfg["paths"]["checkpoint"]; met=ROOT/cfg["paths"]["training_metrics"]; ck.parent.mkdir(parents=True,exist_ok=True); met.parent.mkdir(parents=True,exist_ok=True); torch.save({"model":base.state_dict(),"config":cfg,"optimizer":opt.state_dict(),"epoch":cfg["training"]["epochs"],"seed":cfg["seed"]},ck); met.write_text(json.dumps({"history":history,"protocol":cfg["data"]["protocol"],"data_source":"synthetic"},indent=2)+"\n"); print("checkpoint=",ck)
|
| 46 |
+
if world>1: dist.destroy_process_group()
|
| 47 |
+
if __name__=="__main__": main()
|
weight/.gitkeep
ADDED
|
File without changes
|