zhangrenchao commited on
Commit
0face05
·
verified ·
1 Parent(s): 986404c

Upload folder using huggingface_hub

Browse files
Files changed (5) hide show
  1. README.md +42 -16
  2. config.json +5 -20
  3. scripts/inference.py +34 -10
  4. scripts/result.py +53 -8
  5. 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 for kilometer-scale regional weather downscaling and was trained with coarse-resolution ERA5 reanalysis and high-resolution WRF regional model data from Taiwan's Central Weather Administration.
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
- | Weather forecast training | Train CorrDiff with ERA5 HDF5 data. |
43
- | Local quick validation | Use synthetic data to check data loading, model training, inference, and result visualization. |
44
- | ModelScope / OneCode execution | Download the standalone model package, install dependencies, and run the scripts directly. |
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
- The OneScience community provides an ERA5 data slice that can be downloaded as follows:
95
 
96
  ```bash
97
- hf download --repo-type dataset OneScience-Group/ERA5 --local-dir ./data
98
  ```
99
 
100
- When real data is unavailable, generate synthetic data for pipeline validation:
 
 
101
 
102
  ```bash
103
- python scripts/fake_data.py --output data/era5_corrdiff.npz
 
 
 
 
 
 
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/model_bak.pth
124
- data/checkpoints/trloss.npy
125
- data/checkpoints/valoss.npy
126
  ```
127
 
 
 
128
  ### Training Weights
129
 
130
- This repository provides weights trained on 39 years of ERA5 reanalysis data in the `weight/` folder. The weight files will be uploaded soon and are expected to be available in the near future.
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 results are written to `result/output/` by default.
 
 
 
 
 
 
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 reproduction of the CorrDiff regression and residual diffusion architecture"
13
  },
14
  "architecture": {
15
  "family": "residual corrective diffusion model",
16
- "input_grid_shape": [
17
- 36,
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
- "input_steps": 1,
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="Run CorrDiff inference")
13
  parser.add_argument("--data", default="data/era5_corrdiff.npz")
14
- parser.add_argument("--checkpoint", default="data/checkpoints/model_bak.pth")
15
- parser.add_argument("--ensemble-size", type=int, default=1)
 
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
- checkpoint = Path(args.checkpoint)
22
- if checkpoint.exists():
23
- model.load_state_dict(torch.load(checkpoint, map_location="cpu")["model"])
 
 
 
 
 
24
  model.eval()
 
25
  samples = []
26
  with torch.no_grad():
27
- for _ in range(args.ensemble_size):
 
28
  samples.append(model(coarse).numpy().astype("float32"))
 
 
29
  output = Path(args.output)
30
  output.parent.mkdir(parents=True, exist_ok=True)
31
- np.savez_compressed(output, prediction=np.stack(samples), input=data["input"])
32
- print(f"prediction: {np.stack(samples).shape}")
 
 
 
 
 
 
 
 
 
 
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 NPZ predictions")
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
- prediction = np.load(args.prediction)["prediction"]
12
- target = np.load(args.target)["target"]
13
- mae = np.abs(prediction.mean(axis=0) - target).mean()
14
- output = Path(args.prediction).with_name("metrics.npz")
15
- np.savez(output, mae=np.array(mae, dtype="float32"))
16
- print(f"ensemble={prediction.shape[0]} mae={mae:.6f}")
17
- print(f"saved: {output}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 compact CorrDiff on NPZ data")
14
  parser.add_argument("--data", default="data/era5_corrdiff.npz")
15
  parser.add_argument("--steps", type=int, default=2)
16
- parser.add_argument("--output", default="data/checkpoints/model_bak.pth")
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
- optimizer = torch.optim.Adam(model.parameters(), lr=2e-4)
 
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.detach(), sigma)
30
- loss = F.mse_loss(mean, target) + F.mse_loss(predicted, residual)
31
- optimizer.zero_grad()
32
- loss.backward()
33
- optimizer.step()
34
- print(f"step={step + 1} loss={loss.item():.6f}")
35
- output = Path(args.output)
36
- output.parent.mkdir(parents=True, exist_ok=True)
37
- torch.save({"model": model.state_dict(), "format": "corrdiff-compact-v1"}, output)
38
- np.save(output.parent / "trloss.npy", np.asarray([loss.item()], dtype="float32"))
39
- np.save(output.parent / "valoss.npy", np.asarray([loss.item()], dtype="float32"))
40
- print(f"saved: {output}")
 
 
 
 
 
 
 
 
 
 
 
 
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__":