Publish WeatherBench reproduction
Browse files- .gitattributes +6 -35
- conf/config.yaml +30 -0
- config.json +1 -0
- model/weatherbench.py +24 -0
- scripts/fake_data.py +5 -0
- scripts/inference.py +10 -0
- scripts/result.py +8 -0
- scripts/train.py +16 -0
- weight/.gitkeep +0 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,6 @@
|
|
| 1 |
-
*.
|
| 2 |
-
*.
|
| 3 |
-
*.
|
| 4 |
-
*.
|
| 5 |
-
*.
|
| 6 |
-
*.
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
*.py text eol=lf
|
| 2 |
+
*.md text eol=lf
|
| 3 |
+
*.json text eol=lf
|
| 4 |
+
*.yaml text eol=lf
|
| 5 |
+
*.pt binary
|
| 6 |
+
*.npz binary
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
conf/config.yaml
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
seed: 42
|
| 2 |
+
data:
|
| 3 |
+
format_version: weatherbench_v1
|
| 4 |
+
path: data/weatherbench.npz
|
| 5 |
+
grid: [32, 64]
|
| 6 |
+
samples: 24
|
| 7 |
+
channels: [Z500, T850]
|
| 8 |
+
model:
|
| 9 |
+
hidden_channels: 16
|
| 10 |
+
layers: 5
|
| 11 |
+
kernel_size: 5
|
| 12 |
+
train:
|
| 13 |
+
epochs: 2
|
| 14 |
+
learning_rate: 0.001
|
| 15 |
+
paths:
|
| 16 |
+
checkpoint: result/checkpoints/weatherbench_cnn.pt
|
| 17 |
+
training_metrics: result/training/metrics.json
|
| 18 |
+
predictions: result/output/predictions.npz
|
| 19 |
+
evaluation: result/evaluation/metrics.json
|
| 20 |
+
figure: result/evaluation/comparison.png
|
| 21 |
+
paper_model:
|
| 22 |
+
grid: [32, 64]
|
| 23 |
+
input_channels: [Z500, T850]
|
| 24 |
+
conv_layers: 5
|
| 25 |
+
hidden_channels: 64
|
| 26 |
+
kernel_size: 5
|
| 27 |
+
activation: ELU
|
| 28 |
+
optimizer: Adam
|
| 29 |
+
loss: MSE
|
| 30 |
+
test_years: [2017, 2018]
|
config.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"model_name":"WeatherBench-CNN","model_type":"weatherbench_cnn","architectures":["WeatherBenchCNN"],"framework":"PyTorch","domain":"weather","task":"medium-range-forecasting-benchmark","implementation":{"entry_point":"model/weatherbench.py","scope":"L2 benchmark and paper CNN reproduction"},"architecture":{"input_channels":["Z500","T850"],"output_channels":["Z500","T850"],"grid":[32,64],"conv_layers":5,"paper_hidden_channels":64,"kernel_size":5,"activation":"ELU","longitude_boundary":"periodic"},"data":{"source":"ERA5","years":[1979,2018],"training_years":[1979,2015],"validation_year":2016,"test_years":[2017,2018],"pressure_levels":13,"synthetic":true},"configuration_sources":["conf/config.yaml","model/weatherbench.py","scripts/fake_data.py","scripts/train.py","scripts/inference.py","scripts/result.py"]}
|
model/weatherbench.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""WeatherBench five-layer fully convolutional baseline."""
|
| 2 |
+
import json
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
from torch import nn
|
| 7 |
+
import torch.nn.functional as F
|
| 8 |
+
import yaml
|
| 9 |
+
|
| 10 |
+
PRESSURE_LEVELS=(50,100,150,200,250,300,400,500,600,700,850,925,1000)
|
| 11 |
+
def load_config(root):return yaml.safe_load((Path(root)/"conf/config.yaml").read_text())
|
| 12 |
+
def periodic_conv(x,conv,pad=2):x=F.pad(x,(pad,pad,0,0),mode="circular");x=F.pad(x,(0,0,pad,pad),mode="replicate");return conv(x)
|
| 13 |
+
class WeatherBenchCNN(nn.Module):
|
| 14 |
+
def __init__(self,hidden_channels=16,layers=5,kernel_size=5):
|
| 15 |
+
super().__init__();chs=[2]+[hidden_channels]*(layers-1)+[2];self.convs=nn.ModuleList([nn.Conv2d(chs[i],chs[i+1],kernel_size) for i in range(layers)]);self.model_config={"hidden_channels":hidden_channels,"layers":layers,"kernel_size":kernel_size}
|
| 16 |
+
def forward(self,x):
|
| 17 |
+
if x.shape[1:]!=(2,32,64):raise ValueError("expected [B,2,32,64]")
|
| 18 |
+
for c in self.convs[:-1]:x=F.elu(periodic_conv(x,c))
|
| 19 |
+
return periodic_conv(x,self.convs[-1])
|
| 20 |
+
def synthetic_state(i,lead=0):
|
| 21 |
+
lat,lon=torch.meshgrid(torch.linspace(-90,90,32),torch.arange(64).float()*360/64,indexing="ij");phase=.12*(i+lead);z=50000+3000*torch.cos(torch.deg2rad(lat))*torch.sin(torch.deg2rad(lon)+phase);t=270-35*abs(lat)/90+4*torch.cos(torch.deg2rad(lon*2)-phase);return torch.stack((z/50000,t/270)).float()
|
| 22 |
+
def weighted_rmse(p,t,lat):w=np.cos(np.deg2rad(lat));w=w/w.mean();return np.mean(np.sqrt(np.mean((p-t)**2*w[None,None,:,None],axis=(1,2,3))))
|
| 23 |
+
def weighted_acc(p,t,clim,lat):w=np.cos(np.deg2rad(lat))[None,None,:,None];a=p-clim;b=t-clim;return float((w*a*b).sum()/np.sqrt((w*a*a).sum()*(w*b*b).sum()))
|
| 24 |
+
def write_json(path,obj):path=Path(path);path.parent.mkdir(parents=True,exist_ok=True);path.write_text(json.dumps(obj,indent=2)+"\n")
|
scripts/fake_data.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
import sys,numpy as np
|
| 3 |
+
ROOT=Path(__file__).resolve().parents[1];sys.path.insert(0,str(ROOT))
|
| 4 |
+
from model.weatherbench import *
|
| 5 |
+
c=load_config(ROOT);x=np.stack([synthetic_state(i).numpy() for i in range(24)]);y6=np.stack([synthetic_state(i,1).numpy() for i in range(24)]);y3=np.stack([synthetic_state(i,12).numpy() for i in range(24)]);y5=np.stack([synthetic_state(i,20).numpy() for i in range(24)]);split=np.array([0]*16+[1]*4+[2]*4);path=ROOT/c["data"]["path"];path.parent.mkdir(parents=True,exist_ok=True);np.savez_compressed(path,input=x,target_6h=y6,target_3d=y3,target_5d=y5,split=split,latitude=np.linspace(-90,90,32),longitude=np.linspace(0,360,64,endpoint=False),pressure_levels=np.array(PRESSURE_LEVELS));print(path)
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
import sys,numpy as np,torch
|
| 3 |
+
ROOT=Path(__file__).resolve().parents[1];sys.path.insert(0,str(ROOT))
|
| 4 |
+
from model.weatherbench import *
|
| 5 |
+
c=load_config(ROOT);d=np.load(ROOT/c["data"]["path"]);ck=torch.load(ROOT/c["paths"]["checkpoint"],map_location="cpu",weights_only=True);m=WeatherBenchCNN(**ck["model_config"]);m.load_state_dict(ck["model"]);x=torch.tensor(d["input"][d["split"]==2]);p=[]
|
| 6 |
+
with torch.no_grad():
|
| 7 |
+
for step in range(20):
|
| 8 |
+
x=m(x)
|
| 9 |
+
if step in (11,19):p.append(x.numpy())
|
| 10 |
+
path=ROOT/c["paths"]["predictions"];path.parent.mkdir(parents=True,exist_ok=True);np.savez_compressed(path,prediction=np.stack(p,1),target=np.stack((d["target_3d"][d["split"]==2],d["target_5d"][d["split"]==2]),1),initial=d["input"][d["split"]==2],lead_days=np.array([3,5]),latitude=d["latitude"],longitude=d["longitude"]);print(path)
|
scripts/result.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
import sys,numpy as np
|
| 3 |
+
import matplotlib;matplotlib.use("Agg");import matplotlib.pyplot as plt
|
| 4 |
+
ROOT=Path(__file__).resolve().parents[1];sys.path.insert(0,str(ROOT))
|
| 5 |
+
from model.weatherbench import load_config,weighted_rmse,weighted_acc,write_json
|
| 6 |
+
c=load_config(ROOT);d=np.load(ROOT/c["paths"]["predictions"]);p=d["prediction"];t=d["target"];clim=t.mean((0,1),keepdims=True);metrics={}
|
| 7 |
+
for k,name in enumerate(("Z500","T850")):metrics[name]={"rmse":[weighted_rmse(p[:,j,k:k+1],t[:,j,k:k+1],d["latitude"]) for j in range(2)],"acc":[weighted_acc(p[:,j,k:k+1],t[:,j,k:k+1],clim[:,:,k:k+1],d["latitude"]) for j in range(2)],"persistence_rmse":[weighted_rmse(d["initial"][:,k:k+1],t[:,j,k:k+1],d["latitude"]) for j in range(2)]}
|
| 8 |
+
metrics["synthetic"]=True;write_json(ROOT/c["paths"]["evaluation"],metrics);fig,ax=plt.subplots(1,2,figsize=(9,3.5));ax[0].plot(d["lead_days"],metrics["Z500"]["rmse"],"o-",label="CNN");ax[0].plot(d["lead_days"],metrics["Z500"]["persistence_rmse"],"s-",label="persistence");ax[0].legend();im=ax[1].imshow(p[0,1,0]-t[0,1,0],cmap="coolwarm");ax[1].set_title("5-day Z500 error");fig.colorbar(im,ax=ax[1]);fig.tight_layout();path=ROOT/c["paths"]["figure"];path.parent.mkdir(parents=True,exist_ok=True);fig.savefig(path,dpi=150);print(path)
|
scripts/train.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
import sys,os,numpy as np,torch
|
| 3 |
+
import torch.distributed as dist
|
| 4 |
+
from torch.nn.parallel import DistributedDataParallel as DDP
|
| 5 |
+
ROOT=Path(__file__).resolve().parents[1];sys.path.insert(0,str(ROOT))
|
| 6 |
+
from model.weatherbench import *
|
| 7 |
+
c=load_config(ROOT);rank=int(os.getenv("RANK",0));world=int(os.getenv("WORLD_SIZE",1));distributed=world>1
|
| 8 |
+
if distributed:dist.init_process_group("gloo")
|
| 9 |
+
torch.manual_seed(c["seed"]);d=np.load(ROOT/c["data"]["path"]);ids=np.where(d["split"]==0)[0];base=WeatherBenchCNN(**c["model"]);m=DDP(base) if distributed else base;opt=torch.optim.Adam(m.parameters(),lr=c["train"]["learning_rate"]);losses=[]
|
| 10 |
+
for _ in range(c["train"]["epochs"]):
|
| 11 |
+
for i in ids[rank::world]:x=torch.tensor(d["input"][i:i+1]);y=torch.tensor(d["target_6h"][i:i+1]);loss=((m(x)-y)**2).mean();opt.zero_grad();loss.backward();opt.step();losses.append(float(loss))
|
| 12 |
+
v=torch.tensor([sum(losses),len(losses)],dtype=torch.float64)
|
| 13 |
+
if distributed:dist.all_reduce(v)
|
| 14 |
+
p=ROOT/c["paths"]["checkpoint"]
|
| 15 |
+
if rank==0:p.parent.mkdir(parents=True,exist_ok=True);torch.save({"model":base.state_dict(),"model_config":c["model"]},p);write_json(ROOT/c["paths"]["training_metrics"],{"mse":float(v[0]/v[1]),"world_size":world});print(p)
|
| 16 |
+
if distributed:dist.destroy_process_group()
|
weight/.gitkeep
ADDED
|
File without changes
|