Update CorrDiff model package
Browse files- conf/config.yaml +39 -17
- config.json +78 -20
- configuration.json +11 -1
- model/corrdiff.py +93 -52
- scripts/fake_data.py +31 -12
- scripts/inference.py +50 -44
- scripts/result.py +43 -45
- scripts/train.py +146 -56
conf/config.yaml
CHANGED
|
@@ -1,21 +1,43 @@
|
|
| 1 |
-
|
| 2 |
-
model:
|
| 3 |
-
regression_config: conf/config_training_taiwan_regression.yaml
|
| 4 |
-
diffusion_config: conf/config_training_taiwan_diffusion.yaml
|
| 5 |
-
generation_config: conf/config_generate_taiwan.yaml
|
| 6 |
-
input_channels: 12
|
| 7 |
-
output_channels: 4
|
| 8 |
-
input_shape: [36, 36]
|
| 9 |
-
output_shape: [448, 448]
|
| 10 |
-
ensemble_size: 32
|
| 11 |
-
|
| 12 |
data:
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
input_variables: [tcwv, t2m, u10m, v10m, t500, z500, u500, v500, t850, z850, u850, v850]
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
runtime:
|
| 19 |
device: auto
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
seed: 42
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
data:
|
| 3 |
+
path: data/corrdiff.npz
|
| 4 |
+
protocol: corrdiff_npz_v1
|
| 5 |
+
input_key: input
|
| 6 |
+
target_key: target
|
| 7 |
+
input_shape: [12, 36, 36]
|
| 8 |
+
target_shape: [4, 448, 448]
|
| 9 |
input_variables: [tcwv, t2m, u10m, v10m, t500, z500, u500, v500, t850, z850, u850, v850]
|
| 10 |
+
target_variables: [t2m, u10m, v10m, maximum_radar_reflectivity]
|
| 11 |
+
fake_samples: 2
|
| 12 |
+
model:
|
| 13 |
+
in_channels: 12
|
| 14 |
+
out_channels: 4
|
| 15 |
+
base_channels: 8
|
| 16 |
+
feature_size: 28
|
| 17 |
+
output_size: 448
|
| 18 |
+
sigma_data: 0.5
|
| 19 |
+
training:
|
| 20 |
+
regression_epochs: 1
|
| 21 |
+
diffusion_epochs: 1
|
| 22 |
+
batch_size: 1
|
| 23 |
+
max_batches_per_epoch: 1
|
| 24 |
+
learning_rate: 0.0002
|
| 25 |
+
num_workers: 0
|
| 26 |
+
amp: true
|
| 27 |
+
p_mean: -1.2
|
| 28 |
+
p_std: 1.2
|
| 29 |
+
sampling:
|
| 30 |
+
batch_size: 1
|
| 31 |
+
ensemble_size: 2
|
| 32 |
+
steps: 3
|
| 33 |
+
sigma_min: 0.002
|
| 34 |
+
sigma_max: 5.0
|
| 35 |
+
rho: 7.0
|
| 36 |
+
solver: heun
|
| 37 |
runtime:
|
| 38 |
device: auto
|
| 39 |
+
paths:
|
| 40 |
+
checkpoint: result/checkpoints/corrdiff.pt
|
| 41 |
+
training_metrics: result/training/metrics.json
|
| 42 |
+
predictions: result/output/predictions.npz
|
| 43 |
+
evaluation_dir: result/evaluation
|
config.json
CHANGED
|
@@ -2,37 +2,95 @@
|
|
| 2 |
"model_name": "CorrDiff",
|
| 3 |
"model_type": "corrdiff",
|
| 4 |
"architectures": [
|
| 5 |
-
"CorrDiff"
|
|
|
|
|
|
|
| 6 |
],
|
| 7 |
"framework": "PyTorch",
|
| 8 |
"domain": "atmosphere",
|
| 9 |
-
"task": "
|
| 10 |
"implementation": {
|
| 11 |
"entry_point": "model/corrdiff.py",
|
| 12 |
-
"scope": "
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
},
|
| 14 |
"architecture": {
|
| 15 |
-
"family": "residual corrective diffusion
|
| 16 |
-
"
|
| 17 |
-
"
|
| 18 |
-
"
|
| 19 |
-
"
|
| 20 |
-
"
|
| 21 |
-
"
|
| 22 |
-
"
|
| 23 |
-
"
|
| 24 |
-
"
|
| 25 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
},
|
| 27 |
"data": {
|
| 28 |
-
"
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
"protocol": "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
},
|
| 34 |
"configuration_sources": [
|
| 35 |
"conf/config.yaml",
|
| 36 |
-
"model/corrdiff.py"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
]
|
| 38 |
}
|
|
|
|
| 2 |
"model_name": "CorrDiff",
|
| 3 |
"model_type": "corrdiff",
|
| 4 |
"architectures": [
|
| 5 |
+
"CorrDiff",
|
| 6 |
+
"RegressionUNet",
|
| 7 |
+
"ResidualDenoiser"
|
| 8 |
],
|
| 9 |
"framework": "PyTorch",
|
| 10 |
"domain": "atmosphere",
|
| 11 |
+
"task": "probabilistic-weather-downscaling",
|
| 12 |
"implementation": {
|
| 13 |
"entry_point": "model/corrdiff.py",
|
| 14 |
+
"scope": "conditional-regression mean prediction followed by residual EDM ensemble generation for kilometer-scale atmospheric downscaling",
|
| 15 |
+
"train_script": "scripts/train.py",
|
| 16 |
+
"inference_script": "scripts/inference.py",
|
| 17 |
+
"evaluation_script": "scripts/result.py",
|
| 18 |
+
"synthetic_data_script": "scripts/fake_data.py"
|
| 19 |
},
|
| 20 |
"architecture": {
|
| 21 |
+
"family": "conditional regression plus residual corrective diffusion",
|
| 22 |
+
"in_channels": 12,
|
| 23 |
+
"out_channels": 4,
|
| 24 |
+
"base_channels": 8,
|
| 25 |
+
"feature_size": 28,
|
| 26 |
+
"output_size": 448,
|
| 27 |
+
"sigma_data": 0.5,
|
| 28 |
+
"regression_component": "RegressionUNet",
|
| 29 |
+
"diffusion_component": "ResidualDenoiser",
|
| 30 |
+
"diffusion_preconditioning": "EDM",
|
| 31 |
+
"noise_schedule": "Karras",
|
| 32 |
+
"sampling": {
|
| 33 |
+
"ensemble_size": 2,
|
| 34 |
+
"steps": 3,
|
| 35 |
+
"sigma_min": 0.002,
|
| 36 |
+
"sigma_max": 5.0,
|
| 37 |
+
"rho": 7.0,
|
| 38 |
+
"solver": "heun"
|
| 39 |
+
}
|
| 40 |
},
|
| 41 |
"data": {
|
| 42 |
+
"datasets": [
|
| 43 |
+
"ERA5",
|
| 44 |
+
"CWA-WRF"
|
| 45 |
+
],
|
| 46 |
+
"protocol": "corrdiff_npz_v1",
|
| 47 |
+
"format": "NPZ",
|
| 48 |
+
"default_file": "data/corrdiff.npz",
|
| 49 |
+
"input_key": "input",
|
| 50 |
+
"input_shape": [
|
| 51 |
+
"N",
|
| 52 |
+
12,
|
| 53 |
+
36,
|
| 54 |
+
36
|
| 55 |
+
],
|
| 56 |
+
"target_key": "target",
|
| 57 |
+
"target_shape": [
|
| 58 |
+
"N",
|
| 59 |
+
4,
|
| 60 |
+
448,
|
| 61 |
+
448
|
| 62 |
+
],
|
| 63 |
+
"input_variables": [
|
| 64 |
+
"tcwv",
|
| 65 |
+
"t2m",
|
| 66 |
+
"u10m",
|
| 67 |
+
"v10m",
|
| 68 |
+
"t500",
|
| 69 |
+
"z500",
|
| 70 |
+
"u500",
|
| 71 |
+
"v500",
|
| 72 |
+
"t850",
|
| 73 |
+
"z850",
|
| 74 |
+
"u850",
|
| 75 |
+
"v850"
|
| 76 |
+
],
|
| 77 |
+
"target_variables": [
|
| 78 |
+
"t2m",
|
| 79 |
+
"u10m",
|
| 80 |
+
"v10m",
|
| 81 |
+
"maximum_radar_reflectivity"
|
| 82 |
+
],
|
| 83 |
+
"required_metadata": [
|
| 84 |
+
"protocol",
|
| 85 |
+
"data_source"
|
| 86 |
+
]
|
| 87 |
},
|
| 88 |
"configuration_sources": [
|
| 89 |
"conf/config.yaml",
|
| 90 |
+
"model/corrdiff.py",
|
| 91 |
+
"scripts/fake_data.py",
|
| 92 |
+
"scripts/train.py",
|
| 93 |
+
"scripts/inference.py",
|
| 94 |
+
"scripts/result.py"
|
| 95 |
]
|
| 96 |
}
|
configuration.json
CHANGED
|
@@ -1 +1,11 @@
|
|
| 1 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"framework": "PyTorch",
|
| 3 |
+
"task": "probabilistic_weather_downscaling",
|
| 4 |
+
"model": "CorrDiff",
|
| 5 |
+
"license": "Apache-2.0",
|
| 6 |
+
"protocol": "corrdiff_npz_v1",
|
| 7 |
+
"default_config": "conf/config.yaml",
|
| 8 |
+
"train": "scripts/train.py",
|
| 9 |
+
"inference": "scripts/inference.py",
|
| 10 |
+
"evaluation": "scripts/result.py"
|
| 11 |
+
}
|
model/corrdiff.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
-
"""Compact
|
| 2 |
|
| 3 |
import math
|
|
|
|
| 4 |
import torch
|
| 5 |
from torch import nn
|
| 6 |
from torch.nn import functional as F
|
|
@@ -9,12 +10,13 @@ from torch.nn import functional as F
|
|
| 9 |
class ConvBlock(nn.Module):
|
| 10 |
def __init__(self, in_channels, out_channels):
|
| 11 |
super().__init__()
|
|
|
|
| 12 |
self.block = nn.Sequential(
|
| 13 |
nn.Conv2d(in_channels, out_channels, 3, padding=1),
|
| 14 |
-
nn.GroupNorm(
|
| 15 |
nn.SiLU(),
|
| 16 |
nn.Conv2d(out_channels, out_channels, 3, padding=1),
|
| 17 |
-
nn.GroupNorm(
|
| 18 |
nn.SiLU(),
|
| 19 |
)
|
| 20 |
self.skip = nn.Conv2d(in_channels, out_channels, 1)
|
|
@@ -24,79 +26,118 @@ class ConvBlock(nn.Module):
|
|
| 24 |
|
| 25 |
|
| 26 |
class RegressionUNet(nn.Module):
|
| 27 |
-
"""
|
| 28 |
|
| 29 |
-
def __init__(self, in_channels=12, out_channels=4, base_channels=
|
|
|
|
| 30 |
super().__init__()
|
| 31 |
-
self.
|
|
|
|
|
|
|
| 32 |
ConvBlock(in_channels, base_channels),
|
| 33 |
ConvBlock(base_channels, base_channels * 2),
|
| 34 |
-
ConvBlock(base_channels * 2, base_channels
|
| 35 |
-
|
| 36 |
-
self.head = nn.Sequential(
|
| 37 |
-
ConvBlock(base_channels * 4, base_channels * 2),
|
| 38 |
-
nn.Conv2d(base_channels * 2, out_channels, 1),
|
| 39 |
)
|
| 40 |
|
| 41 |
-
def forward(self,
|
| 42 |
-
x =
|
| 43 |
-
|
| 44 |
-
x = self.
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
x = self.head[0](x)
|
| 48 |
-
x = F.interpolate(x, size=(448, 448), mode="bilinear", align_corners=False)
|
| 49 |
-
return self.head[1](x)
|
| 50 |
|
| 51 |
|
| 52 |
-
class
|
| 53 |
-
"""
|
| 54 |
|
| 55 |
-
def __init__(self, condition_channels=16, out_channels=4, base_channels=
|
|
|
|
| 56 |
super().__init__()
|
| 57 |
-
self.
|
|
|
|
| 58 |
nn.Linear(1, base_channels), nn.SiLU(), nn.Linear(base_channels, base_channels)
|
| 59 |
)
|
| 60 |
-
self.
|
| 61 |
-
|
| 62 |
ConvBlock(base_channels, base_channels * 2),
|
| 63 |
ConvBlock(base_channels * 2, base_channels),
|
| 64 |
nn.Conv2d(base_channels, out_channels, 1),
|
| 65 |
)
|
| 66 |
|
| 67 |
-
def forward(self,
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
x =
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
|
|
|
| 75 |
|
| 76 |
|
| 77 |
class CorrDiff(nn.Module):
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
def __init__(self, in_channels=12, out_channels=4, base_channels=32):
|
| 81 |
super().__init__()
|
| 82 |
-
self.
|
| 83 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
def mean(self, coarse):
|
| 86 |
return self.regression(coarse)
|
| 87 |
|
| 88 |
-
def
|
| 89 |
-
coarse = F.interpolate(coarse,
|
| 90 |
-
return
|
| 91 |
|
| 92 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
mean = self.mean(coarse)
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
for
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Compact but faithful conditional-regression and residual-EDM CorrDiff."""
|
| 2 |
|
| 3 |
import math
|
| 4 |
+
|
| 5 |
import torch
|
| 6 |
from torch import nn
|
| 7 |
from torch.nn import functional as F
|
|
|
|
| 10 |
class ConvBlock(nn.Module):
|
| 11 |
def __init__(self, in_channels, out_channels):
|
| 12 |
super().__init__()
|
| 13 |
+
groups = min(8, out_channels)
|
| 14 |
self.block = nn.Sequential(
|
| 15 |
nn.Conv2d(in_channels, out_channels, 3, padding=1),
|
| 16 |
+
nn.GroupNorm(groups, out_channels),
|
| 17 |
nn.SiLU(),
|
| 18 |
nn.Conv2d(out_channels, out_channels, 3, padding=1),
|
| 19 |
+
nn.GroupNorm(groups, out_channels),
|
| 20 |
nn.SiLU(),
|
| 21 |
)
|
| 22 |
self.skip = nn.Conv2d(in_channels, out_channels, 1)
|
|
|
|
| 26 |
|
| 27 |
|
| 28 |
class RegressionUNet(nn.Module):
|
| 29 |
+
"""Deterministic conditional mean, computed cheaply before 448 px recovery."""
|
| 30 |
|
| 31 |
+
def __init__(self, in_channels=12, out_channels=4, base_channels=16, feature_size=56,
|
| 32 |
+
output_size=448):
|
| 33 |
super().__init__()
|
| 34 |
+
self.feature_size = feature_size
|
| 35 |
+
self.output_size = output_size
|
| 36 |
+
self.net = nn.Sequential(
|
| 37 |
ConvBlock(in_channels, base_channels),
|
| 38 |
ConvBlock(base_channels, base_channels * 2),
|
| 39 |
+
ConvBlock(base_channels * 2, base_channels),
|
| 40 |
+
nn.Conv2d(base_channels, out_channels, 1),
|
|
|
|
|
|
|
|
|
|
| 41 |
)
|
| 42 |
|
| 43 |
+
def forward(self, coarse):
|
| 44 |
+
x = F.interpolate(coarse, (self.feature_size, self.feature_size), mode="bilinear",
|
| 45 |
+
align_corners=False)
|
| 46 |
+
x = self.net(x)
|
| 47 |
+
return F.interpolate(x, (self.output_size, self.output_size), mode="bilinear",
|
| 48 |
+
align_corners=False)
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
|
| 51 |
+
class ResidualDenoiser(nn.Module):
|
| 52 |
+
"""Noise-conditional network used inside EDM preconditioning."""
|
| 53 |
|
| 54 |
+
def __init__(self, condition_channels=16, out_channels=4, base_channels=16,
|
| 55 |
+
feature_size=56):
|
| 56 |
super().__init__()
|
| 57 |
+
self.feature_size = feature_size
|
| 58 |
+
self.noise_mlp = nn.Sequential(
|
| 59 |
nn.Linear(1, base_channels), nn.SiLU(), nn.Linear(base_channels, base_channels)
|
| 60 |
)
|
| 61 |
+
self.input = ConvBlock(condition_channels, base_channels)
|
| 62 |
+
self.body = nn.Sequential(
|
| 63 |
ConvBlock(base_channels, base_channels * 2),
|
| 64 |
ConvBlock(base_channels * 2, base_channels),
|
| 65 |
nn.Conv2d(base_channels, out_channels, 1),
|
| 66 |
)
|
| 67 |
|
| 68 |
+
def forward(self, noisy, condition, c_noise):
|
| 69 |
+
size = noisy.shape[-2:]
|
| 70 |
+
x = torch.cat((noisy, condition), dim=1)
|
| 71 |
+
x = F.interpolate(x, (self.feature_size, self.feature_size), mode="bilinear",
|
| 72 |
+
align_corners=False)
|
| 73 |
+
x = self.input(x)
|
| 74 |
+
x = x + self.noise_mlp(c_noise[:, None].float())[:, :, None, None].to(x.dtype)
|
| 75 |
+
x = self.body(x)
|
| 76 |
+
return F.interpolate(x, size, mode="bilinear", align_corners=False)
|
| 77 |
|
| 78 |
|
| 79 |
class CorrDiff(nn.Module):
|
| 80 |
+
def __init__(self, in_channels=12, out_channels=4, base_channels=16, feature_size=56,
|
| 81 |
+
output_size=448, sigma_data=0.5):
|
|
|
|
| 82 |
super().__init__()
|
| 83 |
+
self.sigma_data = sigma_data
|
| 84 |
+
self.output_size = output_size
|
| 85 |
+
self.regression = RegressionUNet(in_channels, out_channels, base_channels,
|
| 86 |
+
feature_size, output_size)
|
| 87 |
+
self.diffusion = ResidualDenoiser(in_channels + 2 * out_channels, out_channels,
|
| 88 |
+
base_channels, feature_size)
|
| 89 |
|
| 90 |
def mean(self, coarse):
|
| 91 |
return self.regression(coarse)
|
| 92 |
|
| 93 |
+
def condition(self, coarse, mean):
|
| 94 |
+
coarse = F.interpolate(coarse, mean.shape[-2:], mode="bilinear", align_corners=False)
|
| 95 |
+
return torch.cat((coarse, mean), dim=1)
|
| 96 |
|
| 97 |
+
def denoise(self, noisy_residual, coarse, mean, sigma):
|
| 98 |
+
sigma = sigma.reshape(-1, 1, 1, 1).to(noisy_residual.dtype)
|
| 99 |
+
sigma_data = self.sigma_data
|
| 100 |
+
c_skip = sigma_data**2 / (sigma.square() + sigma_data**2)
|
| 101 |
+
c_out = sigma * sigma_data / (sigma.square() + sigma_data**2).sqrt()
|
| 102 |
+
c_in = (sigma.square() + sigma_data**2).rsqrt()
|
| 103 |
+
c_noise = sigma.flatten().log() / 4
|
| 104 |
+
network = self.diffusion(c_in * noisy_residual, self.condition(coarse, mean), c_noise)
|
| 105 |
+
return c_skip * noisy_residual + c_out * network
|
| 106 |
+
|
| 107 |
+
@staticmethod
|
| 108 |
+
def karras_schedule(steps, sigma_min, sigma_max, rho, device):
|
| 109 |
+
ramp = torch.linspace(0, 1, steps, device=device)
|
| 110 |
+
maximum = sigma_max ** (1 / rho)
|
| 111 |
+
minimum = sigma_min ** (1 / rho)
|
| 112 |
+
sigmas = (maximum + ramp * (minimum - maximum)) ** rho
|
| 113 |
+
return torch.cat((sigmas, sigmas.new_zeros(1)))
|
| 114 |
+
|
| 115 |
+
def sample(self, coarse, steps=4, sigma_min=0.002, sigma_max=5.0, rho=7.0,
|
| 116 |
+
solver="heun"):
|
| 117 |
mean = self.mean(coarse)
|
| 118 |
+
sigmas = self.karras_schedule(steps, sigma_min, sigma_max, rho, coarse.device)
|
| 119 |
+
x = torch.randn_like(mean) * sigmas[0]
|
| 120 |
+
for index, (current, following) in enumerate(zip(sigmas[:-1], sigmas[1:])):
|
| 121 |
+
sigma = current.expand(coarse.shape[0])
|
| 122 |
+
denoised = self.denoise(x, coarse, mean, sigma)
|
| 123 |
+
derivative = (x - denoised) / current
|
| 124 |
+
proposal = x + (following - current) * derivative
|
| 125 |
+
if solver == "heun" and index < len(sigmas) - 2:
|
| 126 |
+
next_sigma = following.expand(coarse.shape[0])
|
| 127 |
+
next_denoised = self.denoise(proposal, coarse, mean, next_sigma)
|
| 128 |
+
next_derivative = (proposal - next_denoised) / following
|
| 129 |
+
x = x + (following - current) * (derivative + next_derivative) / 2
|
| 130 |
+
else:
|
| 131 |
+
x = proposal
|
| 132 |
+
return mean + x
|
| 133 |
+
|
| 134 |
+
def forward(self, coarse, mode="sample", mean=None, noisy=None, sigma=None,
|
| 135 |
+
**sample_options):
|
| 136 |
+
if mode == "mean":
|
| 137 |
+
return self.mean(coarse)
|
| 138 |
+
if mode == "denoise":
|
| 139 |
+
return self.denoise(noisy, coarse, mean, sigma)
|
| 140 |
+
return self.sample(coarse, **sample_options)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
__all__ = ["CorrDiff", "RegressionUNet", "ResidualDenoiser"]
|
scripts/fake_data.py
CHANGED
|
@@ -1,22 +1,41 @@
|
|
| 1 |
-
|
|
|
|
| 2 |
import argparse
|
|
|
|
|
|
|
| 3 |
import numpy as np
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
|
| 6 |
def main():
|
| 7 |
parser = argparse.ArgumentParser()
|
| 8 |
-
parser.add_argument("--
|
| 9 |
-
parser.add_argument("--
|
| 10 |
args = parser.parse_args()
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
|
| 22 |
if __name__ == "__main__":
|
|
|
|
| 1 |
+
"""Generate a tiny, structurally realistic CorrDiff NPZ dataset."""
|
| 2 |
+
|
| 3 |
import argparse
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
import numpy as np
|
| 7 |
+
import yaml
|
| 8 |
+
|
| 9 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 10 |
|
| 11 |
|
| 12 |
def main():
|
| 13 |
parser = argparse.ArgumentParser()
|
| 14 |
+
parser.add_argument("--config", default=str(ROOT / "conf/config.yaml"))
|
| 15 |
+
parser.add_argument("--output")
|
| 16 |
args = parser.parse_args()
|
| 17 |
+
config = yaml.safe_load(Path(args.config).read_text(encoding="utf-8"))
|
| 18 |
+
data = config["data"]
|
| 19 |
+
count = data["fake_samples"]
|
| 20 |
+
rng = np.random.default_rng(config["seed"])
|
| 21 |
+
coarse = rng.normal(size=(count, *data["input_shape"])).astype("float32")
|
| 22 |
+
# Correlated targets make the regression/backward smoke test meaningful.
|
| 23 |
+
y = np.linspace(-1, 1, data["target_shape"][1], dtype="float32")
|
| 24 |
+
x = np.linspace(-1, 1, data["target_shape"][2], dtype="float32")
|
| 25 |
+
yy, xx = np.meshgrid(y, x, indexing="ij")
|
| 26 |
+
target = np.empty((count, *data["target_shape"]), dtype="float32")
|
| 27 |
+
coarse_signal = coarse.mean(axis=(2, 3))
|
| 28 |
+
for sample in range(count):
|
| 29 |
+
for channel in range(data["target_shape"][0]):
|
| 30 |
+
target[sample, channel] = coarse_signal[sample, channel] + 0.3 * np.sin(
|
| 31 |
+
(channel + 1) * np.pi * xx
|
| 32 |
+
) + 0.2 * np.cos((channel + 1) * np.pi * yy)
|
| 33 |
+
target += rng.normal(0, 0.05, target.shape).astype("float32")
|
| 34 |
+
output = Path(args.output) if args.output else ROOT / data["path"]
|
| 35 |
+
output.parent.mkdir(parents=True, exist_ok=True)
|
| 36 |
+
np.savez_compressed(output, input=coarse, target=target,
|
| 37 |
+
protocol=np.asarray(data["protocol"]), data_source=np.asarray("synthetic"))
|
| 38 |
+
print(f"saved={output} input={coarse.shape} target={target.shape}")
|
| 39 |
|
| 40 |
|
| 41 |
if __name__ == "__main__":
|
scripts/inference.py
CHANGED
|
@@ -1,60 +1,66 @@
|
|
|
|
|
|
|
|
| 1 |
import argparse
|
| 2 |
-
from pathlib import Path
|
| 3 |
import sys
|
|
|
|
| 4 |
|
| 5 |
import numpy as np
|
| 6 |
import torch
|
|
|
|
| 7 |
|
| 8 |
-
|
|
|
|
| 9 |
from model.corrdiff import CorrDiff
|
| 10 |
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
def main():
|
| 13 |
-
parser = argparse.ArgumentParser(
|
| 14 |
-
parser.add_argument("--
|
| 15 |
-
parser.add_argument("--
|
| 16 |
-
parser.add_argument("--diffusion-checkpoint", default="data/checkpoints/diffusion_model.pth")
|
| 17 |
-
parser.add_argument("--ensemble-size", type=int, default=32)
|
| 18 |
-
parser.add_argument("--output", default="result/output/predictions.npz")
|
| 19 |
-
parser.add_argument("--seed", type=int, default=42)
|
| 20 |
args = parser.parse_args()
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
state = torch.load(regression_checkpoint, map_location="cpu", weights_only=False)
|
| 30 |
-
model.regression.load_state_dict(state["model"])
|
| 31 |
-
if diffusion_checkpoint.exists():
|
| 32 |
-
state = torch.load(diffusion_checkpoint, map_location="cpu", weights_only=False)
|
| 33 |
-
model.diffusion.load_state_dict(state["model"])
|
| 34 |
model.eval()
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
output.parent.mkdir(parents=True, exist_ok=True)
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
"input": data["input"].astype("float32"),
|
| 50 |
-
}
|
| 51 |
-
if target is not None:
|
| 52 |
-
payload["target"] = target
|
| 53 |
-
np.savez_compressed(output, **payload)
|
| 54 |
-
print(f"ensemble: {ensemble.shape}")
|
| 55 |
-
print(f"ensemble mean: {payload['ensemble_mean'].shape}")
|
| 56 |
-
print(f"ensemble std: {payload['ensemble_std'].shape}")
|
| 57 |
-
print(f"saved: {output}")
|
| 58 |
|
| 59 |
|
| 60 |
if __name__ == "__main__":
|
|
|
|
| 1 |
+
"""Generate a checkpoint-backed CorrDiff ensemble in device-sized batches."""
|
| 2 |
+
|
| 3 |
import argparse
|
|
|
|
| 4 |
import sys
|
| 5 |
+
from pathlib import Path
|
| 6 |
|
| 7 |
import numpy as np
|
| 8 |
import torch
|
| 9 |
+
import yaml
|
| 10 |
|
| 11 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 12 |
+
sys.path.insert(0, str(ROOT))
|
| 13 |
from model.corrdiff import CorrDiff
|
| 14 |
|
| 15 |
|
| 16 |
+
def scalar(archive, key):
|
| 17 |
+
if key not in archive or archive[key].ndim != 0:
|
| 18 |
+
raise ValueError(f"NPZ metadata {key} must be present as a scalar")
|
| 19 |
+
return str(archive[key].item())
|
| 20 |
+
|
| 21 |
+
|
| 22 |
def main():
|
| 23 |
+
parser = argparse.ArgumentParser()
|
| 24 |
+
parser.add_argument("--config", default=str(ROOT / "conf/config.yaml"))
|
| 25 |
+
parser.add_argument("--checkpoint")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
args = parser.parse_args()
|
| 27 |
+
config = yaml.safe_load(Path(args.config).read_text(encoding="utf-8"))
|
| 28 |
+
checkpoint_path = Path(args.checkpoint) if args.checkpoint else ROOT / config["paths"]["checkpoint"]
|
| 29 |
+
if not checkpoint_path.is_file():
|
| 30 |
+
raise FileNotFoundError(f"Checkpoint is required: {checkpoint_path}")
|
| 31 |
+
device = torch.device("cuda" if torch.cuda.is_available() and config["runtime"]["device"] != "cpu" else "cpu")
|
| 32 |
+
state = torch.load(checkpoint_path, map_location="cpu", weights_only=False)
|
| 33 |
+
model = CorrDiff(**config["model"]).to(device)
|
| 34 |
+
model.load_state_dict(state["model"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
model.eval()
|
| 36 |
+
archive = np.load(ROOT / config["data"]["path"])
|
| 37 |
+
protocol, data_source = scalar(archive, "protocol"), scalar(archive, "data_source")
|
| 38 |
+
if protocol != config["data"]["protocol"] or state.get("protocol") != protocol:
|
| 39 |
+
raise ValueError("Data, checkpoint, and configured protocols must match")
|
| 40 |
+
if not data_source or state.get("data_source") != data_source:
|
| 41 |
+
raise ValueError("Data and checkpoint data_source metadata must match")
|
| 42 |
+
coarse = archive[config["data"]["input_key"]]
|
| 43 |
+
target = archive[config["data"]["target_key"]]
|
| 44 |
+
if coarse.ndim != 4 or target.ndim != 4 or tuple(coarse.shape[1:]) != tuple(config["data"]["input_shape"]) or tuple(target.shape[1:]) != tuple(config["data"]["target_shape"]) or len(coarse) != len(target):
|
| 45 |
+
raise ValueError("Invalid CorrDiff NPZ tensor contract")
|
| 46 |
+
member_count = config["sampling"]["ensemble_size"]
|
| 47 |
+
ensemble = np.empty((member_count, len(coarse), *config["data"]["target_shape"]), dtype="float32")
|
| 48 |
+
options = {key: config["sampling"][key] for key in ("steps", "sigma_min", "sigma_max", "rho", "solver")}
|
| 49 |
+
batch_size = config["sampling"]["batch_size"]
|
| 50 |
+
with torch.inference_mode():
|
| 51 |
+
for member in range(member_count):
|
| 52 |
+
torch.manual_seed(config["seed"] + member)
|
| 53 |
+
for start in range(0, len(coarse), batch_size):
|
| 54 |
+
stop = min(start + batch_size, len(coarse))
|
| 55 |
+
batch = torch.from_numpy(coarse[start:stop]).to(device)
|
| 56 |
+
ensemble[member, start:stop] = model.sample(batch, **options).cpu().numpy()
|
| 57 |
+
del batch
|
| 58 |
+
output = ROOT / config["paths"]["predictions"]
|
| 59 |
output.parent.mkdir(parents=True, exist_ok=True)
|
| 60 |
+
np.savez_compressed(output, ensemble=ensemble, ensemble_mean=ensemble.mean(0),
|
| 61 |
+
ensemble_std=ensemble.std(0), target=target, protocol=np.asarray(protocol),
|
| 62 |
+
data_source=np.asarray(data_source), checkpoint=np.asarray(str(checkpoint_path)))
|
| 63 |
+
print(f"saved={output} ensemble={ensemble.shape} batch_size={batch_size}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
|
| 66 |
if __name__ == "__main__":
|
scripts/result.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
|
|
|
|
|
| 1 |
import argparse
|
| 2 |
import json
|
| 3 |
from pathlib import Path
|
|
@@ -6,60 +8,56 @@ import matplotlib
|
|
| 6 |
matplotlib.use("Agg")
|
| 7 |
import matplotlib.pyplot as plt
|
| 8 |
import numpy as np
|
|
|
|
| 9 |
|
| 10 |
-
|
| 11 |
-
VARIABLES = ["t2m", "u10m", "v10m", "radar_reflectivity"]
|
| 12 |
|
| 13 |
|
| 14 |
def crps_ensemble(ensemble, target):
|
| 15 |
-
first = np.abs(ensemble - target[None]).mean(
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
weights = 2 * np.arange(1,
|
| 19 |
-
|
| 20 |
-
return first - second / members**2
|
| 21 |
|
| 22 |
|
| 23 |
def main():
|
| 24 |
-
parser = argparse.ArgumentParser(
|
| 25 |
-
parser.add_argument("--
|
| 26 |
-
parser.add_argument("--target", default="data/era5_corrdiff.npz")
|
| 27 |
-
parser.add_argument("--output-dir", default="result/output")
|
| 28 |
args = parser.parse_args()
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
output.mkdir(parents=True, exist_ok=True)
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
axes[channel, 0].set_title(f"{name}: ensemble mean")
|
| 50 |
-
axes[channel, 1].imshow(std[0, channel], cmap="magma")
|
| 51 |
-
axes[channel, 1].set_title(f"{name}: ensemble std")
|
| 52 |
-
axes[channel, 2].imshow(target[0, channel], cmap="viridis")
|
| 53 |
-
axes[channel, 2].set_title(f"{name}: target")
|
| 54 |
-
for axis in axes[channel]:
|
| 55 |
-
axis.set_xticks([])
|
| 56 |
-
axis.set_yticks([])
|
| 57 |
-
figure.tight_layout()
|
| 58 |
-
figure.savefig(output / "prediction_comparison.png", dpi=150)
|
| 59 |
-
plt.close(figure)
|
| 60 |
-
|
| 61 |
-
print(json.dumps(metrics, indent=2))
|
| 62 |
-
print(f"saved evaluation outputs to: {output}")
|
| 63 |
|
| 64 |
|
| 65 |
if __name__ == "__main__":
|
|
|
|
| 1 |
+
"""Compute deterministic and probabilistic CorrDiff metrics and plots."""
|
| 2 |
+
|
| 3 |
import argparse
|
| 4 |
import json
|
| 5 |
from pathlib import Path
|
|
|
|
| 8 |
matplotlib.use("Agg")
|
| 9 |
import matplotlib.pyplot as plt
|
| 10 |
import numpy as np
|
| 11 |
+
import yaml
|
| 12 |
|
| 13 |
+
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
| 14 |
|
| 15 |
|
| 16 |
def crps_ensemble(ensemble, target):
|
| 17 |
+
first = np.abs(ensemble - target[None]).mean(0)
|
| 18 |
+
sorted_members = np.sort(ensemble, axis=0)
|
| 19 |
+
m = ensemble.shape[0]
|
| 20 |
+
weights = (2 * np.arange(1, m + 1) - m - 1).reshape(m, 1, 1, 1, 1)
|
| 21 |
+
return first - (sorted_members * weights).sum(0) / m**2
|
|
|
|
| 22 |
|
| 23 |
|
| 24 |
def main():
|
| 25 |
+
parser = argparse.ArgumentParser()
|
| 26 |
+
parser.add_argument("--config", default=str(ROOT / "conf/config.yaml"))
|
|
|
|
|
|
|
| 27 |
args = parser.parse_args()
|
| 28 |
+
config = yaml.safe_load(Path(args.config).read_text(encoding="utf-8"))
|
| 29 |
+
archive = np.load(ROOT / config["paths"]["predictions"])
|
| 30 |
+
if "protocol" not in archive or archive["protocol"].ndim != 0 or str(archive["protocol"].item()) != config["data"]["protocol"]:
|
| 31 |
+
raise ValueError("Prediction protocol does not match the configured protocol")
|
| 32 |
+
if "data_source" not in archive or archive["data_source"].ndim != 0 or not str(archive["data_source"].item()):
|
| 33 |
+
raise ValueError("Prediction data_source must be a non-empty scalar")
|
| 34 |
+
protocol = str(archive["protocol"].item())
|
| 35 |
+
data_source = str(archive["data_source"].item())
|
| 36 |
+
ensemble, target = archive["ensemble"], archive["target"]
|
| 37 |
+
mean, spread = ensemble.mean(0), ensemble.std(0)
|
| 38 |
+
axes = (0, 2, 3)
|
| 39 |
+
mae = np.abs(mean - target).mean(axis=axes)
|
| 40 |
+
rmse = np.sqrt(((mean - target) ** 2).mean(axis=axes))
|
| 41 |
+
crps = crps_ensemble(ensemble, target).mean(axis=axes)
|
| 42 |
+
spread_value = spread.mean(axis=axes)
|
| 43 |
+
names = config["data"]["target_variables"]
|
| 44 |
+
metrics = {name: {"mae": float(mae[i]), "rmse": float(rmse[i]), "crps": float(crps[i]),
|
| 45 |
+
"ensemble_spread": float(spread_value[i])} for i, name in enumerate(names)}
|
| 46 |
+
metrics["aggregate"] = {key: float(np.mean([metrics[n][key] for n in names]))
|
| 47 |
+
for key in ("mae", "rmse", "crps", "ensemble_spread")}
|
| 48 |
+
output = ROOT / config["paths"]["evaluation_dir"]
|
| 49 |
output.mkdir(parents=True, exist_ok=True)
|
| 50 |
+
payload = {"metrics": metrics, "protocol": protocol, "data_source": data_source}
|
| 51 |
+
(output / "metrics.json").write_text(json.dumps(payload, indent=2) + "\n")
|
| 52 |
+
figure, plot_axes = plt.subplots(len(names), 4, figsize=(13, 3 * len(names)))
|
| 53 |
+
for channel, name in enumerate(names):
|
| 54 |
+
fields = (target[0, channel], mean[0, channel], spread[0, channel], mean[0, channel] - target[0, channel])
|
| 55 |
+
titles = ("target", "ensemble mean", "ensemble spread", "mean error")
|
| 56 |
+
for axis, field, title in zip(plot_axes[channel], fields, titles):
|
| 57 |
+
axis.imshow(field, cmap="coolwarm" if title == "mean error" else "viridis")
|
| 58 |
+
axis.set_title(f"{name}: {title}"); axis.axis("off")
|
| 59 |
+
figure.tight_layout(); figure.savefig(output / "ensemble_diagnostics.png", dpi=120); plt.close(figure)
|
| 60 |
+
print(json.dumps(payload, indent=2)); print(f"evaluation={output}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
|
| 63 |
if __name__ == "__main__":
|
scripts/train.py
CHANGED
|
@@ -1,74 +1,164 @@
|
|
|
|
|
|
|
|
| 1 |
import argparse
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
from pathlib import Path
|
|
|
|
| 3 |
import numpy as np
|
| 4 |
import torch
|
|
|
|
|
|
|
| 5 |
from torch.nn import functional as F
|
|
|
|
|
|
|
| 6 |
|
| 7 |
-
|
| 8 |
-
sys.path.insert(0, str(
|
| 9 |
from model.corrdiff import CorrDiff
|
| 10 |
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
def main():
|
| 13 |
-
parser = argparse.ArgumentParser(
|
| 14 |
-
parser.add_argument("--
|
| 15 |
-
parser.add_argument("--steps", type=int, default=2)
|
| 16 |
-
parser.add_argument("--checkpoint-dir", default="data/checkpoints")
|
| 17 |
args = parser.parse_args()
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
-
# Stage 1
|
| 29 |
-
for
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
for parameter in model.regression.parameters():
|
| 41 |
parameter.requires_grad_(False)
|
| 42 |
-
for
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
{"model":
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
{"
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
regression_loss=np.asarray(regression_losses, dtype="float32"),
|
| 69 |
-
diffusion_loss=np.asarray(diffusion_losses, dtype="float32"),
|
| 70 |
-
)
|
| 71 |
-
print(f"saved checkpoints to: {checkpoint_dir}")
|
| 72 |
|
| 73 |
|
| 74 |
if __name__ == "__main__":
|
|
|
|
| 1 |
+
"""Train CorrDiff's conditional mean, then its frozen-mean residual EDM."""
|
| 2 |
+
|
| 3 |
import argparse
|
| 4 |
+
import json
|
| 5 |
+
import os
|
| 6 |
+
import random
|
| 7 |
+
import sys
|
| 8 |
+
from contextlib import nullcontext
|
| 9 |
from pathlib import Path
|
| 10 |
+
|
| 11 |
import numpy as np
|
| 12 |
import torch
|
| 13 |
+
import yaml
|
| 14 |
+
from torch import distributed as dist
|
| 15 |
from torch.nn import functional as F
|
| 16 |
+
from torch.nn.parallel import DistributedDataParallel as DDP
|
| 17 |
+
from torch.utils.data import DataLoader, DistributedSampler, TensorDataset
|
| 18 |
|
| 19 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 20 |
+
sys.path.insert(0, str(ROOT))
|
| 21 |
from model.corrdiff import CorrDiff
|
| 22 |
|
| 23 |
|
| 24 |
+
def scalar(archive, key, default=None):
|
| 25 |
+
if key not in archive:
|
| 26 |
+
if default is not None:
|
| 27 |
+
return default
|
| 28 |
+
raise ValueError(f"NPZ is missing required metadata: {key}")
|
| 29 |
+
value = archive[key]
|
| 30 |
+
if value.ndim != 0:
|
| 31 |
+
raise ValueError(f"NPZ metadata {key} must be a scalar")
|
| 32 |
+
return str(value.item())
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def reduced_average(total, count, device, distributed):
|
| 36 |
+
values = torch.tensor([total, count], dtype=torch.float64, device=device)
|
| 37 |
+
if distributed:
|
| 38 |
+
dist.all_reduce(values, op=dist.ReduceOp.SUM)
|
| 39 |
+
if values[1].item() == 0:
|
| 40 |
+
raise RuntimeError("Training stage processed no batches")
|
| 41 |
+
return (values[0] / values[1]).item()
|
| 42 |
+
|
| 43 |
+
|
| 44 |
def main():
|
| 45 |
+
parser = argparse.ArgumentParser()
|
| 46 |
+
parser.add_argument("--config", default=str(ROOT / "conf/config.yaml"))
|
|
|
|
|
|
|
| 47 |
args = parser.parse_args()
|
| 48 |
+
config = yaml.safe_load(Path(args.config).read_text(encoding="utf-8"))
|
| 49 |
+
global_rank = int(os.getenv("RANK", 0))
|
| 50 |
+
local_rank = int(os.getenv("LOCAL_RANK", 0))
|
| 51 |
+
world = int(os.getenv("WORLD_SIZE", 1))
|
| 52 |
+
distributed = world > 1
|
| 53 |
+
if distributed:
|
| 54 |
+
dist.init_process_group("nccl" if torch.cuda.is_available() else "gloo")
|
| 55 |
+
use_cuda = torch.cuda.is_available() and config["runtime"]["device"] != "cpu"
|
| 56 |
+
device = torch.device(f"cuda:{local_rank}" if use_cuda else "cpu")
|
| 57 |
+
if use_cuda:
|
| 58 |
+
torch.cuda.set_device(local_rank)
|
| 59 |
+
seed = config["seed"] + global_rank
|
| 60 |
+
random.seed(seed)
|
| 61 |
+
np.random.seed(seed)
|
| 62 |
+
torch.manual_seed(seed)
|
| 63 |
+
|
| 64 |
+
archive = np.load(ROOT / config["data"]["path"])
|
| 65 |
+
protocol = scalar(archive, "protocol")
|
| 66 |
+
data_source = scalar(archive, "data_source")
|
| 67 |
+
if protocol != config["data"]["protocol"]:
|
| 68 |
+
raise ValueError(f"Expected protocol {config['data']['protocol']}, got {protocol}")
|
| 69 |
+
if not data_source:
|
| 70 |
+
raise ValueError("data_source must be a non-empty scalar")
|
| 71 |
+
coarse = torch.from_numpy(archive[config["data"]["input_key"]])
|
| 72 |
+
target = torch.from_numpy(archive[config["data"]["target_key"]])
|
| 73 |
+
if coarse.ndim != 4 or target.ndim != 4:
|
| 74 |
+
raise ValueError("CorrDiff input and target must be NCHW tensors")
|
| 75 |
+
if len(coarse) != len(target) or tuple(coarse.shape[1:]) != tuple(config["data"]["input_shape"]) or tuple(target.shape[1:]) != tuple(config["data"]["target_shape"]):
|
| 76 |
+
raise ValueError("NPZ tensor shapes do not match config")
|
| 77 |
+
dataset = TensorDataset(coarse, target)
|
| 78 |
+
sampler = DistributedSampler(dataset, shuffle=True) if distributed else None
|
| 79 |
+
loader = DataLoader(dataset, batch_size=config["training"]["batch_size"], sampler=sampler,
|
| 80 |
+
shuffle=sampler is None, num_workers=config["training"]["num_workers"])
|
| 81 |
+
model = CorrDiff(**config["model"]).to(device)
|
| 82 |
+
if distributed:
|
| 83 |
+
model = DDP(model, device_ids=[local_rank] if use_cuda else None,
|
| 84 |
+
find_unused_parameters=True)
|
| 85 |
+
base = model.module if distributed else model
|
| 86 |
+
reg_opt = torch.optim.AdamW(base.regression.parameters(), lr=config["training"]["learning_rate"])
|
| 87 |
+
diff_opt = torch.optim.AdamW(base.diffusion.parameters(), lr=config["training"]["learning_rate"])
|
| 88 |
+
amp = bool(config["training"]["amp"] and use_cuda)
|
| 89 |
+
scaler = torch.amp.GradScaler("cuda", enabled=amp)
|
| 90 |
+
autocast = (lambda: torch.amp.autocast("cuda", enabled=True)) if amp else nullcontext
|
| 91 |
+
history = []
|
| 92 |
|
| 93 |
+
# Stage 1 is completed in full before any residual-EDM update occurs.
|
| 94 |
+
for epoch in range(config["training"]["regression_epochs"]):
|
| 95 |
+
if sampler is not None:
|
| 96 |
+
sampler.set_epoch(epoch)
|
| 97 |
+
model.train()
|
| 98 |
+
total = count = 0
|
| 99 |
+
for batch_index, (coarse_batch, target_batch) in enumerate(loader):
|
| 100 |
+
coarse_batch, target_batch = coarse_batch.to(device), target_batch.to(device)
|
| 101 |
+
reg_opt.zero_grad(set_to_none=True)
|
| 102 |
+
with autocast():
|
| 103 |
+
loss = F.mse_loss(model(coarse_batch, mode="mean"), target_batch)
|
| 104 |
+
scaler.scale(loss).backward()
|
| 105 |
+
scaler.step(reg_opt)
|
| 106 |
+
scaler.update()
|
| 107 |
+
total += loss.item()
|
| 108 |
+
count += 1
|
| 109 |
+
if batch_index + 1 >= config["training"]["max_batches_per_epoch"]:
|
| 110 |
+
break
|
| 111 |
+
value = reduced_average(total, count, device, distributed)
|
| 112 |
+
record = {"stage": "regression", "epoch": epoch + 1, "regression_mse": value}
|
| 113 |
+
history.append(record)
|
| 114 |
+
if global_rank == 0:
|
| 115 |
+
print(json.dumps(record))
|
| 116 |
|
| 117 |
+
base.regression.eval()
|
| 118 |
+
for parameter in base.regression.parameters():
|
|
|
|
| 119 |
parameter.requires_grad_(False)
|
| 120 |
+
for epoch in range(config["training"]["diffusion_epochs"]):
|
| 121 |
+
if sampler is not None:
|
| 122 |
+
sampler.set_epoch(config["training"]["regression_epochs"] + epoch)
|
| 123 |
+
base.diffusion.train()
|
| 124 |
+
total = count = 0
|
| 125 |
+
for batch_index, (coarse_batch, target_batch) in enumerate(loader):
|
| 126 |
+
coarse_batch, target_batch = coarse_batch.to(device), target_batch.to(device)
|
| 127 |
+
with torch.no_grad():
|
| 128 |
+
mean = base.mean(coarse_batch)
|
| 129 |
+
residual = target_batch - mean
|
| 130 |
+
sigma = (torch.randn(len(coarse_batch), device=device) * config["training"]["p_std"] + config["training"]["p_mean"]).exp()
|
| 131 |
+
noisy = residual + sigma[:, None, None, None] * torch.randn_like(residual)
|
| 132 |
+
diff_opt.zero_grad(set_to_none=True)
|
| 133 |
+
with autocast():
|
| 134 |
+
denoised = model(coarse_batch, mode="denoise", mean=mean, noisy=noisy, sigma=sigma)
|
| 135 |
+
weight = (sigma.square() + base.sigma_data**2) / (sigma * base.sigma_data).square()
|
| 136 |
+
loss = (weight[:, None, None, None] * (denoised - residual).square()).mean()
|
| 137 |
+
scaler.scale(loss).backward()
|
| 138 |
+
scaler.step(diff_opt)
|
| 139 |
+
scaler.update()
|
| 140 |
+
total += loss.item()
|
| 141 |
+
count += 1
|
| 142 |
+
if batch_index + 1 >= config["training"]["max_batches_per_epoch"]:
|
| 143 |
+
break
|
| 144 |
+
value = reduced_average(total, count, device, distributed)
|
| 145 |
+
record = {"stage": "diffusion", "epoch": epoch + 1, "edm_loss": value}
|
| 146 |
+
history.append(record)
|
| 147 |
+
if global_rank == 0:
|
| 148 |
+
print(json.dumps(record))
|
| 149 |
|
| 150 |
+
if global_rank == 0:
|
| 151 |
+
checkpoint = ROOT / config["paths"]["checkpoint"]
|
| 152 |
+
checkpoint.parent.mkdir(parents=True, exist_ok=True)
|
| 153 |
+
torch.save({"model": base.state_dict(), "config": config, "format": "corrdiff-edm-v3",
|
| 154 |
+
"protocol": protocol, "data_source": data_source}, checkpoint)
|
| 155 |
+
metrics = ROOT / config["paths"]["training_metrics"]
|
| 156 |
+
metrics.parent.mkdir(parents=True, exist_ok=True)
|
| 157 |
+
metrics.write_text(json.dumps({"history": history, "protocol": protocol,
|
| 158 |
+
"data_source": data_source}, indent=2) + "\n")
|
| 159 |
+
print(f"checkpoint={checkpoint}")
|
| 160 |
+
if distributed:
|
| 161 |
+
dist.destroy_process_group()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
|
| 163 |
|
| 164 |
if __name__ == "__main__":
|