Upload folder using huggingface_hub
Browse files- README.md +42 -16
- config.json +5 -20
- scripts/inference.py +34 -10
- scripts/result.py +53 -8
- scripts/train.py +46 -15
README.md
CHANGED
|
@@ -13,9 +13,8 @@ tags:
|
|
| 13 |
- arxiv:2309.15214
|
| 14 |
tasks: []
|
| 15 |
datasets:
|
| 16 |
-
- OneScience/ERA5
|
| 17 |
---
|
| 18 |
-
|
| 19 |
<p align="center">
|
| 20 |
<strong>
|
| 21 |
<span style="font-size: 30px;">CorrDiff</span>
|
|
@@ -32,17 +31,17 @@ https://arxiv.org/abs/2309.15214
|
|
| 32 |
|
| 33 |
# Model Description
|
| 34 |
|
| 35 |
-
CorrDiff was proposed by NVIDIA and its collaborators
|
| 36 |
The model is suitable for converting coarse-resolution global weather fields into high-resolution regional weather fields and producing probabilistic weather predictions.
|
| 37 |
|
| 38 |
# Use Cases
|
| 39 |
|
| 40 |
| Scenario | Description |
|
| 41 |
| :---: | :--- |
|
| 42 |
-
|
|
| 43 |
-
| Local quick validation | Use synthetic data to check data loading,
|
| 44 |
-
|
|
| 45 |
-
| Multi-GPU training | Launch multi-process training with `torchrun`. |
|
| 46 |
|
| 47 |
# Usage Guide
|
| 48 |
|
|
@@ -91,18 +90,28 @@ pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simp
|
|
| 91 |
|
| 92 |
### Training Data Introduction
|
| 93 |
|
| 94 |
-
|
| 95 |
|
| 96 |
```bash
|
| 97 |
-
|
| 98 |
```
|
| 99 |
|
| 100 |
-
|
|
|
|
|
|
|
| 101 |
|
| 102 |
```bash
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
```
|
| 105 |
|
|
|
|
|
|
|
| 106 |
### Training
|
| 107 |
|
| 108 |
Single GPU:
|
|
@@ -120,14 +129,16 @@ torchrun --nproc_per_node=8 --nnodes=1 --rdzv_id=1000 --rdzv_backend=c10d --max_
|
|
| 120 |
Training outputs:
|
| 121 |
|
| 122 |
```text
|
| 123 |
-
data/checkpoints/
|
| 124 |
-
data/checkpoints/
|
| 125 |
-
data/checkpoints/
|
| 126 |
```
|
| 127 |
|
|
|
|
|
|
|
| 128 |
### Training Weights
|
| 129 |
|
| 130 |
-
|
| 131 |
|
| 132 |
### Inference
|
| 133 |
|
|
@@ -135,7 +146,13 @@ This repository provides weights trained on 39 years of ERA5 reanalysis data in
|
|
| 135 |
python scripts/inference.py
|
| 136 |
```
|
| 137 |
|
| 138 |
-
Prediction
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
### Evaluation and Visualization
|
| 141 |
|
|
@@ -143,6 +160,15 @@ Prediction results are written to `result/output/` by default.
|
|
| 143 |
python scripts/result.py
|
| 144 |
```
|
| 145 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
# Official OneScience Resources
|
| 147 |
|
| 148 |
| Platform | OneScience Main Repository | Skills Repository |
|
|
|
|
| 13 |
- arxiv:2309.15214
|
| 14 |
tasks: []
|
| 15 |
datasets:
|
| 16 |
+
- OneScience-Group/ERA5
|
| 17 |
---
|
|
|
|
| 18 |
<p align="center">
|
| 19 |
<strong>
|
| 20 |
<span style="font-size: 30px;">CorrDiff</span>
|
|
|
|
| 31 |
|
| 32 |
# Model Description
|
| 33 |
|
| 34 |
+
CorrDiff was proposed by NVIDIA and its collaborators and was trained with coarse-resolution ERA5 reanalysis and high-resolution WRF regional model data from Taiwan's Central Weather Administration.
|
| 35 |
The model is suitable for converting coarse-resolution global weather fields into high-resolution regional weather fields and producing probabilistic weather predictions.
|
| 36 |
|
| 37 |
# Use Cases
|
| 38 |
|
| 39 |
| Scenario | Description |
|
| 40 |
| :---: | :--- |
|
| 41 |
+
| Regional weather downscaling training | Train CorrDiff with time-aligned coarse-resolution ERA5 inputs and high-resolution CWA-WRF targets. |
|
| 42 |
+
| Local quick validation | Use synthetic paired data to check data loading, two-stage training, ensemble inference, and result visualization. |
|
| 43 |
+
| Hugging Face / OneCode execution | Download the standalone model package, install dependencies, and run the scripts directly. |
|
| 44 |
+
| Multi-GPU training | Launch multi-process training with `torchrun` after adapting the real-data pipeline for distributed training. |
|
| 45 |
|
| 46 |
# Usage Guide
|
| 47 |
|
|
|
|
| 90 |
|
| 91 |
### Training Data Introduction
|
| 92 |
|
| 93 |
+
This repository uses synthetic paired data by default to validate training, ensemble inference, and evaluation:
|
| 94 |
|
| 95 |
```bash
|
| 96 |
+
python scripts/fake_data.py --output data/era5_corrdiff.npz
|
| 97 |
```
|
| 98 |
|
| 99 |
+
The synthetic file contains `input` with shape `[N, 12, 36, 36]` and `target` with shape `[N, 4, 448, 448]`. It is intended for pipeline validation only and does not represent real weather predictions.
|
| 100 |
+
|
| 101 |
+
ERA5 inputs can be downloaded from the OneScience community:
|
| 102 |
|
| 103 |
```bash
|
| 104 |
+
hf download --repo-type dataset OneScience-Group/ERA5 --local-dir ./data/era5
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
The paired CWA-WRF resource can be downloaded from NVIDIA NGC:
|
| 108 |
+
|
| 109 |
+
```bash
|
| 110 |
+
ngc registry resource download-version "nvidia/modulus/modulus_datasets_cwa:v1"
|
| 111 |
```
|
| 112 |
|
| 113 |
+
ERA5 alone is insufficient for supervised CorrDiff training. Real data must be time-aligned and preprocessed into the paired `input` and `target` interface expected by the scripts. Verify the data path in `conf/config.yaml` before training.
|
| 114 |
+
|
| 115 |
### Training
|
| 116 |
|
| 117 |
Single GPU:
|
|
|
|
| 129 |
Training outputs:
|
| 130 |
|
| 131 |
```text
|
| 132 |
+
data/checkpoints/regression_model.pth
|
| 133 |
+
data/checkpoints/diffusion_model.pth
|
| 134 |
+
data/checkpoints/training_history.npz
|
| 135 |
```
|
| 136 |
|
| 137 |
+
Training first fits the conditional-mean regression model and then freezes it while fitting the residual diffusion model. `training_history.npz` stores the regression and diffusion denoising losses.
|
| 138 |
+
|
| 139 |
### Training Weights
|
| 140 |
|
| 141 |
+
The training command generates `regression_model.pth` and `diffusion_model.pth` from `data/era5_corrdiff.npz`. Weights produced with synthetic data validate the training and inference pipeline only and do not provide real weather forecasting skill. This repository does not present them as pretrained weights reproducing the paper.
|
| 142 |
|
| 143 |
### Inference
|
| 144 |
|
|
|
|
| 146 |
python scripts/inference.py
|
| 147 |
```
|
| 148 |
|
| 149 |
+
Prediction output:
|
| 150 |
+
|
| 151 |
+
```text
|
| 152 |
+
result/output/predictions.npz
|
| 153 |
+
```
|
| 154 |
+
|
| 155 |
+
The file stores the ensemble predictions, ensemble mean, ensemble standard deviation, input, and target generated by the current trained weights.
|
| 156 |
|
| 157 |
### Evaluation and Visualization
|
| 158 |
|
|
|
|
| 160 |
python scripts/result.py
|
| 161 |
```
|
| 162 |
|
| 163 |
+
Evaluation outputs:
|
| 164 |
+
|
| 165 |
+
```text
|
| 166 |
+
result/output/metrics.json
|
| 167 |
+
result/output/prediction_comparison.png
|
| 168 |
+
```
|
| 169 |
+
|
| 170 |
+
`metrics.json` stores MAE and CRPS for the four output variables. `prediction_comparison.png` compares the ensemble mean, ensemble standard deviation, and target. Metrics and figures generated from synthetic data are pipeline checks, not paper results.
|
| 171 |
+
|
| 172 |
# Official OneScience Resources
|
| 173 |
|
| 174 |
| Platform | OneScience Main Repository | Skills Repository |
|
config.json
CHANGED
|
@@ -9,42 +9,27 @@
|
|
| 9 |
"task": "regional-weather-downscaling",
|
| 10 |
"implementation": {
|
| 11 |
"entry_point": "model/corrdiff.py",
|
| 12 |
-
"scope": "compact two-stage
|
| 13 |
},
|
| 14 |
"architecture": {
|
| 15 |
"family": "residual corrective diffusion model",
|
| 16 |
-
"input_grid_shape": [
|
| 17 |
-
|
| 18 |
-
36
|
| 19 |
-
],
|
| 20 |
-
"output_grid_shape": [
|
| 21 |
-
448,
|
| 22 |
-
448
|
| 23 |
-
],
|
| 24 |
"input_channels": 12,
|
| 25 |
"output_channels": 4,
|
| 26 |
"base_channels": 32,
|
| 27 |
"mean_model": "RegressionUNet",
|
| 28 |
"residual_model": "ResidualDiffusionUNet",
|
| 29 |
-
"conditioning": [
|
| 30 |
-
"bilinearly upsampled coarse input",
|
| 31 |
-
"high-resolution regression mean",
|
| 32 |
-
"noise level sigma"
|
| 33 |
-
],
|
| 34 |
"normalization": "group_norm",
|
| 35 |
"activation": "silu",
|
| 36 |
-
"sampling_steps": 4
|
| 37 |
-
"sigma_max": 1.0,
|
| 38 |
-
"sigma_min": 0.01
|
| 39 |
},
|
| 40 |
"data": {
|
| 41 |
"dataset": "ERA5 and CWA-WRF",
|
| 42 |
"input_spatial_resolution_km": 25,
|
| 43 |
"output_spatial_resolution_km": 2,
|
| 44 |
"time_step_hours": 1,
|
| 45 |
-
"
|
| 46 |
-
"output_steps": 1,
|
| 47 |
-
"protocol": "synthetic_era5_corrdiff"
|
| 48 |
},
|
| 49 |
"configuration_sources": [
|
| 50 |
"conf/config.yaml",
|
|
|
|
| 9 |
"task": "regional-weather-downscaling",
|
| 10 |
"implementation": {
|
| 11 |
"entry_point": "model/corrdiff.py",
|
| 12 |
+
"scope": "compact two-stage CorrDiff reproduction"
|
| 13 |
},
|
| 14 |
"architecture": {
|
| 15 |
"family": "residual corrective diffusion model",
|
| 16 |
+
"input_grid_shape": [36, 36],
|
| 17 |
+
"output_grid_shape": [448, 448],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
"input_channels": 12,
|
| 19 |
"output_channels": 4,
|
| 20 |
"base_channels": 32,
|
| 21 |
"mean_model": "RegressionUNet",
|
| 22 |
"residual_model": "ResidualDiffusionUNet",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
"normalization": "group_norm",
|
| 24 |
"activation": "silu",
|
| 25 |
+
"sampling_steps": 4
|
|
|
|
|
|
|
| 26 |
},
|
| 27 |
"data": {
|
| 28 |
"dataset": "ERA5 and CWA-WRF",
|
| 29 |
"input_spatial_resolution_km": 25,
|
| 30 |
"output_spatial_resolution_km": 2,
|
| 31 |
"time_step_hours": 1,
|
| 32 |
+
"protocol": "paired-downscaling"
|
|
|
|
|
|
|
| 33 |
},
|
| 34 |
"configuration_sources": [
|
| 35 |
"conf/config.yaml",
|
scripts/inference.py
CHANGED
|
@@ -1,35 +1,59 @@
|
|
| 1 |
import argparse
|
| 2 |
from pathlib import Path
|
|
|
|
|
|
|
| 3 |
import numpy as np
|
| 4 |
import torch
|
| 5 |
|
| 6 |
-
import sys
|
| 7 |
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 8 |
from model.corrdiff import CorrDiff
|
| 9 |
|
| 10 |
|
| 11 |
def main():
|
| 12 |
-
parser = argparse.ArgumentParser(description="
|
| 13 |
parser.add_argument("--data", default="data/era5_corrdiff.npz")
|
| 14 |
-
parser.add_argument("--checkpoint", default="data/checkpoints/
|
| 15 |
-
parser.add_argument("--
|
|
|
|
| 16 |
parser.add_argument("--output", default="result/output/predictions.npz")
|
|
|
|
| 17 |
args = parser.parse_args()
|
|
|
|
| 18 |
data = np.load(args.data)
|
| 19 |
coarse = torch.from_numpy(data["input"])
|
|
|
|
| 20 |
model = CorrDiff()
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
model.eval()
|
|
|
|
| 25 |
samples = []
|
| 26 |
with torch.no_grad():
|
| 27 |
-
for
|
|
|
|
| 28 |
samples.append(model(coarse).numpy().astype("float32"))
|
|
|
|
|
|
|
| 29 |
output = Path(args.output)
|
| 30 |
output.parent.mkdir(parents=True, exist_ok=True)
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
print(f"saved: {output}")
|
| 34 |
|
| 35 |
|
|
|
|
| 1 |
import argparse
|
| 2 |
from pathlib import Path
|
| 3 |
+
import sys
|
| 4 |
+
|
| 5 |
import numpy as np
|
| 6 |
import torch
|
| 7 |
|
|
|
|
| 8 |
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 9 |
from model.corrdiff import CorrDiff
|
| 10 |
|
| 11 |
|
| 12 |
def main():
|
| 13 |
+
parser = argparse.ArgumentParser(description="Generate a CorrDiff ensemble")
|
| 14 |
parser.add_argument("--data", default="data/era5_corrdiff.npz")
|
| 15 |
+
parser.add_argument("--regression-checkpoint", default="data/checkpoints/regression_model.pth")
|
| 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 |
data = np.load(args.data)
|
| 23 |
coarse = torch.from_numpy(data["input"])
|
| 24 |
+
target = data["target"].astype("float32") if "target" in data else None
|
| 25 |
model = CorrDiff()
|
| 26 |
+
regression_checkpoint = Path(args.regression_checkpoint)
|
| 27 |
+
diffusion_checkpoint = Path(args.diffusion_checkpoint)
|
| 28 |
+
if regression_checkpoint.exists():
|
| 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 |
samples = []
|
| 37 |
with torch.no_grad():
|
| 38 |
+
for member in range(args.ensemble_size):
|
| 39 |
+
torch.manual_seed(args.seed + member)
|
| 40 |
samples.append(model(coarse).numpy().astype("float32"))
|
| 41 |
+
|
| 42 |
+
ensemble = np.stack(samples)
|
| 43 |
output = Path(args.output)
|
| 44 |
output.parent.mkdir(parents=True, exist_ok=True)
|
| 45 |
+
payload = {
|
| 46 |
+
"ensemble": ensemble,
|
| 47 |
+
"ensemble_mean": ensemble.mean(axis=0),
|
| 48 |
+
"ensemble_std": ensemble.std(axis=0),
|
| 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 |
|
scripts/result.py
CHANGED
|
@@ -1,20 +1,65 @@
|
|
| 1 |
import argparse
|
|
|
|
| 2 |
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import numpy as np
|
| 4 |
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
def main():
|
| 7 |
-
parser = argparse.ArgumentParser(description="Evaluate CorrDiff
|
| 8 |
parser.add_argument("--prediction", default="result/output/predictions.npz")
|
| 9 |
parser.add_argument("--target", default="data/era5_corrdiff.npz")
|
|
|
|
| 10 |
args = parser.parse_args()
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
|
| 20 |
if __name__ == "__main__":
|
|
|
|
| 1 |
import argparse
|
| 2 |
+
import json
|
| 3 |
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
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(axis=0)
|
| 16 |
+
sorted_ensemble = np.sort(ensemble, axis=0)
|
| 17 |
+
members = ensemble.shape[0]
|
| 18 |
+
weights = 2 * np.arange(1, members + 1) - members - 1
|
| 19 |
+
second = (sorted_ensemble * weights[:, None, None, None, None]).sum(axis=0)
|
| 20 |
+
return first - second / members**2
|
| 21 |
+
|
| 22 |
+
|
| 23 |
def main():
|
| 24 |
+
parser = argparse.ArgumentParser(description="Evaluate CorrDiff predictions")
|
| 25 |
parser.add_argument("--prediction", default="result/output/predictions.npz")
|
| 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 |
+
predictions = np.load(args.prediction)
|
| 31 |
+
ensemble = predictions["ensemble"]
|
| 32 |
+
mean = predictions["ensemble_mean"]
|
| 33 |
+
std = predictions["ensemble_std"]
|
| 34 |
+
target = predictions["target"] if "target" in predictions else np.load(args.target)["target"]
|
| 35 |
+
|
| 36 |
+
output = Path(args.output_dir)
|
| 37 |
+
output.mkdir(parents=True, exist_ok=True)
|
| 38 |
+
mae = np.abs(mean - target).mean(axis=(0, 2, 3))
|
| 39 |
+
crps = crps_ensemble(ensemble, target).mean(axis=(0, 2, 3))
|
| 40 |
+
metrics = {
|
| 41 |
+
name: {"mae": float(mae[channel]), "crps": float(crps[channel])}
|
| 42 |
+
for channel, name in enumerate(VARIABLES)
|
| 43 |
+
}
|
| 44 |
+
(output / "metrics.json").write_text(json.dumps(metrics, indent=2), encoding="utf-8")
|
| 45 |
+
|
| 46 |
+
figure, axes = plt.subplots(4, 3, figsize=(12, 14))
|
| 47 |
+
for channel, name in enumerate(VARIABLES):
|
| 48 |
+
axes[channel, 0].imshow(mean[0, channel], cmap="viridis")
|
| 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__":
|
scripts/train.py
CHANGED
|
@@ -10,34 +10,65 @@ from model.corrdiff import CorrDiff
|
|
| 10 |
|
| 11 |
|
| 12 |
def main():
|
| 13 |
-
parser = argparse.ArgumentParser(description="Train
|
| 14 |
parser.add_argument("--data", default="data/era5_corrdiff.npz")
|
| 15 |
parser.add_argument("--steps", type=int, default=2)
|
| 16 |
-
parser.add_argument("--
|
| 17 |
args = parser.parse_args()
|
| 18 |
data = np.load(args.data)
|
| 19 |
coarse = torch.from_numpy(data["input"])
|
| 20 |
target = torch.from_numpy(data["target"])
|
| 21 |
model = CorrDiff()
|
| 22 |
-
|
|
|
|
| 23 |
model.train()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
for step in range(args.steps):
|
| 25 |
mean = model.mean(coarse)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
residual = target - mean
|
| 27 |
sigma = torch.rand(coarse.shape[0]).clamp_min(0.01)
|
| 28 |
noisy = residual + sigma[:, None, None, None] * torch.randn_like(residual)
|
| 29 |
-
predicted = model.denoise(noisy, coarse, mean
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
|
| 43 |
if __name__ == "__main__":
|
|
|
|
| 10 |
|
| 11 |
|
| 12 |
def main():
|
| 13 |
+
parser = argparse.ArgumentParser(description="Train CorrDiff regression and diffusion stages")
|
| 14 |
parser.add_argument("--data", default="data/era5_corrdiff.npz")
|
| 15 |
parser.add_argument("--steps", type=int, default=2)
|
| 16 |
+
parser.add_argument("--checkpoint-dir", default="data/checkpoints")
|
| 17 |
args = parser.parse_args()
|
| 18 |
data = np.load(args.data)
|
| 19 |
coarse = torch.from_numpy(data["input"])
|
| 20 |
target = torch.from_numpy(data["target"])
|
| 21 |
model = CorrDiff()
|
| 22 |
+
regression_optimizer = torch.optim.Adam(model.regression.parameters(), lr=2e-4)
|
| 23 |
+
diffusion_optimizer = torch.optim.Adam(model.diffusion.parameters(), lr=2e-4)
|
| 24 |
model.train()
|
| 25 |
+
regression_losses = []
|
| 26 |
+
diffusion_losses = []
|
| 27 |
+
|
| 28 |
+
# Stage 1: learn the conditional mean.
|
| 29 |
for step in range(args.steps):
|
| 30 |
mean = model.mean(coarse)
|
| 31 |
+
regression_loss = F.mse_loss(mean, target)
|
| 32 |
+
regression_optimizer.zero_grad()
|
| 33 |
+
regression_loss.backward()
|
| 34 |
+
regression_optimizer.step()
|
| 35 |
+
regression_losses.append(regression_loss.item())
|
| 36 |
+
print(f"regression step={step + 1} loss={regression_loss.item():.6f}")
|
| 37 |
+
|
| 38 |
+
# Stage 2: freeze the mean model and learn its stochastic residual.
|
| 39 |
+
model.regression.eval()
|
| 40 |
+
for parameter in model.regression.parameters():
|
| 41 |
+
parameter.requires_grad_(False)
|
| 42 |
+
for step in range(args.steps):
|
| 43 |
+
with torch.no_grad():
|
| 44 |
+
mean = model.mean(coarse)
|
| 45 |
residual = target - mean
|
| 46 |
sigma = torch.rand(coarse.shape[0]).clamp_min(0.01)
|
| 47 |
noisy = residual + sigma[:, None, None, None] * torch.randn_like(residual)
|
| 48 |
+
predicted = model.denoise(noisy, coarse, mean, sigma)
|
| 49 |
+
diffusion_loss = F.mse_loss(predicted, residual)
|
| 50 |
+
diffusion_optimizer.zero_grad()
|
| 51 |
+
diffusion_loss.backward()
|
| 52 |
+
diffusion_optimizer.step()
|
| 53 |
+
diffusion_losses.append(diffusion_loss.item())
|
| 54 |
+
print(f"diffusion step={step + 1} loss={diffusion_loss.item():.6f}")
|
| 55 |
+
|
| 56 |
+
checkpoint_dir = Path(args.checkpoint_dir)
|
| 57 |
+
checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
| 58 |
+
torch.save(
|
| 59 |
+
{"model": model.regression.state_dict(), "format": "corrdiff-regression-v1"},
|
| 60 |
+
checkpoint_dir / "regression_model.pth",
|
| 61 |
+
)
|
| 62 |
+
torch.save(
|
| 63 |
+
{"model": model.diffusion.state_dict(), "format": "corrdiff-diffusion-v1"},
|
| 64 |
+
checkpoint_dir / "diffusion_model.pth",
|
| 65 |
+
)
|
| 66 |
+
np.savez(
|
| 67 |
+
checkpoint_dir / "training_history.npz",
|
| 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__":
|