Publish RISE-UNet reproduction
Browse files- .gitattributes +1 -34
- conf/config.yaml +31 -0
- config.json +1 -0
- model/rise_unet.py +29 -0
- scripts/fake_data.py +5 -0
- scripts/inference.py +8 -0
- scripts/result.py +8 -0
- scripts/train.py +17 -0
- weight/.gitkeep +0 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,2 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.
|
| 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 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
conf/config.yaml
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
seed: 42
|
| 2 |
+
data:
|
| 3 |
+
format_version: rise_unet_synthetic_v1
|
| 4 |
+
manifest: data/manifest.json
|
| 5 |
+
initializations: 4
|
| 6 |
+
ensemble_members: 11
|
| 7 |
+
grid: [48, 96]
|
| 8 |
+
input_channels: 8
|
| 9 |
+
forecast_weeks: 5
|
| 10 |
+
model:
|
| 11 |
+
input_channels: 8
|
| 12 |
+
width: 12
|
| 13 |
+
dropout: 0.15
|
| 14 |
+
train:
|
| 15 |
+
epochs: 1
|
| 16 |
+
learning_rate: 0.001
|
| 17 |
+
crps_spread_factor: 0.08
|
| 18 |
+
paths:
|
| 19 |
+
checkpoint: result/checkpoints/rise_unet.pt
|
| 20 |
+
training_metrics: result/training/metrics.json
|
| 21 |
+
predictions: result/output/predictions.npz
|
| 22 |
+
evaluation: result/evaluation/metrics.json
|
| 23 |
+
figure: result/evaluation/comparison.png
|
| 24 |
+
paper_model:
|
| 25 |
+
grid: [48, 96]
|
| 26 |
+
ensemble_members: 11
|
| 27 |
+
training_years: [2000, 2015]
|
| 28 |
+
validation_years: [2016, 2017]
|
| 29 |
+
test_years: [2018, 2019]
|
| 30 |
+
forecast_weeks: 5
|
| 31 |
+
encoder_filters: [32, 64, 128, 256]
|
config.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"model_name":"RISE-UNet","model_type":"rise_unet","architectures":["RISEUNet"],"framework":"PyTorch","domain":"soil-moisture-drought","task":"subseasonal-ensemble-forecasting","implementation":{"entry_point":"model/rise_unet.py","scope":"core-method engineering reproduction"},"architecture":{"grid":[48,96],"ensemble_members":11,"forecast_weeks":5,"engineering_input_channels":8,"core":["residual inception","squeeze excitation","UNet encoder-decoder","deep supervision","recursive forecast"]},"data":{"target":"GLEAM 0-100 cm root-zone soil moisture anomaly","sources":["GLEAM","ERA5","GEFSv12","ECMWF S2S"],"training_years":[2000,2015],"validation_years":[2016,2017],"test_years":[2018,2019],"resolution_degrees":0.5,"synthetic":true},"configuration_sources":["conf/config.yaml","model/rise_unet.py","scripts/fake_data.py","scripts/train.py","scripts/inference.py","scripts/result.py"]}
|
model/rise_unet.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Compact residual-inception squeeze-excitation UNet."""
|
| 2 |
+
import json,math
|
| 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 |
+
def load_config(root):return yaml.safe_load((Path(root)/"conf/config.yaml").read_text())
|
| 11 |
+
def synthetic_initialization(i,members=11):
|
| 12 |
+
y,x=torch.meshgrid(torch.linspace(-1,1,48),torch.linspace(-2,2,96),indexing="ij");base=torch.sin(2*x)*torch.cos(2*y)+.2*i;xs=[]
|
| 13 |
+
for m in range(members):xs.append(torch.stack([base+.03*m+.05*j+torch.sin(x*(j+1))*.1 for j in range(8)]))
|
| 14 |
+
targets=torch.stack([base-.12*w+.08*torch.sin(x*w+y) for w in range(1,6)]);return torch.stack(xs),targets
|
| 15 |
+
|
| 16 |
+
class RISEBlock(nn.Module):
|
| 17 |
+
def __init__(self,cin,cout,dropout):
|
| 18 |
+
super().__init__();q=max(1,cout//4);self.paths=nn.ModuleList([nn.Conv2d(cin,q,k,padding=k//2) for k in (3,5,7)]);self.pool=nn.Sequential(nn.MaxPool2d(5,1,2),nn.Conv2d(cin,cout-3*q,1));self.norm=nn.BatchNorm2d(cout);self.skip=nn.Conv2d(cin,cout,1);self.se=nn.Sequential(nn.AdaptiveAvgPool2d(1),nn.Conv2d(cout,max(1,cout//4),1),nn.ReLU(),nn.Conv2d(max(1,cout//4),cout,1),nn.Sigmoid());self.drop=nn.Dropout2d(dropout)
|
| 19 |
+
def forward(self,x):
|
| 20 |
+
z=torch.cat([p(x) for p in self.paths]+[self.pool(x)],1);z=F.relu(self.norm(z));z=self.drop(z);return F.relu(z+self.skip(x))*self.se(z)
|
| 21 |
+
class RISEUNet(nn.Module):
|
| 22 |
+
def __init__(self,input_channels=8,width=12,dropout=.15):
|
| 23 |
+
super().__init__();self.e1=RISEBlock(input_channels,width,dropout);self.e2=RISEBlock(width,width*2,dropout);self.e3=RISEBlock(width*2,width*4,dropout);self.d2=RISEBlock(width*6,width*2,dropout);self.d1=RISEBlock(width*3,width,dropout);self.deep=nn.Conv2d(width*2,1,1);self.out=nn.Conv2d(width,1,1);self.model_config={"input_channels":input_channels,"width":width,"dropout":dropout}
|
| 24 |
+
def forward(self,x):
|
| 25 |
+
if x.shape[1:]!=(8,48,96):raise ValueError("expected [B,8,48,96]")
|
| 26 |
+
a=self.e1(x);b=self.e2(F.max_pool2d(a,2));c=self.e3(F.max_pool2d(b,2));u=self.d2(torch.cat((F.interpolate(c,size=b.shape[-2:],mode="bilinear"),b),1));v=self.d1(torch.cat((F.interpolate(u,size=a.shape[-2:],mode="bilinear"),a),1));d=F.interpolate(self.deep(u),size=(48,96),mode="bilinear");o=self.out(v);return (d,o,(d+o)/2)
|
| 27 |
+
def crps_exp(outputs,target,members=11,f=.08):
|
| 28 |
+
pred=outputs[-1].reshape(-1,members,48,96);truth=target.reshape(-1,members,48,96);return (abs(pred-truth).mean(1)-f*pred.std(1)).mean()+sum(abs(o-target).mean() for o in outputs[:-1])*.1
|
| 29 |
+
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,json
|
| 3 |
+
ROOT=Path(__file__).resolve().parents[1];sys.path.insert(0,str(ROOT))
|
| 4 |
+
from model.rise_unet import *
|
| 5 |
+
c=load_config(ROOT);x,y=synthetic_initialization(0);assert x.shape==(11,8,48,96) and y.shape==(5,48,96);p=ROOT/c["data"]["manifest"];p.parent.mkdir(parents=True,exist_ok=True);p.write_text(json.dumps({"format_version":c["data"]["format_version"],"input_shape":[11,8,48,96],"target_shape":[5,48,96],"forecast_weeks":[1,2,3,4,5],"resolution_degrees":0.5,"sources":["GLEAM","ERA5","GEFSv12","ECMWF S2S"],"synthetic":True},indent=2));print(p)
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.rise_unet import *
|
| 5 |
+
c=load_config(ROOT);ck=torch.load(ROOT/c["paths"]["checkpoint"],map_location="cpu",weights_only=True);m=RISEUNet(**ck["model_config"]);m.load_state_dict(ck["model"]);m.train();x,t=synthetic_initialization(20);pred=[]
|
| 6 |
+
with torch.no_grad():
|
| 7 |
+
for w in range(5):o=m(x)[-1];pred.append(o[:,0].numpy());x=torch.cat((x[:,1:],o),1)
|
| 8 |
+
p=ROOT/c["paths"]["predictions"];p.parent.mkdir(parents=True,exist_ok=True);np.savez_compressed(p,ensemble=np.stack(pred,1),target=t.numpy(),lead_weeks=np.arange(1,6),dropout_ensemble=np.bool_(True));print(p)
|
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.rise_unet import load_config,write_json
|
| 6 |
+
c=load_config(ROOT);d=np.load(ROOT/c["paths"]["predictions"]);e=d["ensemble"];t=d["target"];mean=e.mean(0);acc=[]
|
| 7 |
+
for w in range(5):acc.append(float(np.corrcoef(mean[w].ravel(),t[w].ravel())[0,1]))
|
| 8 |
+
crps=(abs(e-t[None]).mean(0)-.5*abs(e[:,None]-e[None,:]).mean((0,1))).mean((1,2));q=np.quantile(t,.2);obs=t<q;hit=mean<q;tp=(obs&hit).sum((1,2));fp=(~obs&hit).sum((1,2));fn=(obs&~hit).sum((1,2));gss=(tp-(tp+fp)*(tp+fn)/obs[0].size)/(tp+fp+fn-(tp+fp)*(tp+fn)/obs[0].size+1e-8);write_json(ROOT/c["paths"]["evaluation"],{"acc":acc,"crps":crps.tolist(),"drought_gss":gss.tolist(),"threshold":"synthetic 20th percentile","synthetic":True});fig,ax=plt.subplots(1,2,figsize=(9,3.5));ax[0].plot(d["lead_weeks"],acc,"o-",label="ACC");ax[0].plot(d["lead_weeks"],crps,"s-",label="CRPS");ax[0].legend();im=ax[1].imshow(mean[2]-t[2],cmap="coolwarm");ax[1].set_title("Week-3 error");fig.colorbar(im,ax=ax[1]);fig.tight_layout();p=ROOT/c["paths"]["figure"];p.parent.mkdir(parents=True,exist_ok=True);fig.savefig(p,dpi=150);print(p)
|
scripts/train.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
import sys,os,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.rise_unet 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"]);torch.set_num_threads(2);base=RISEUNet(**c["model"]);m=DDP(base) if distributed else base;opt=torch.optim.Adam(m.parameters(),lr=c["train"]["learning_rate"]);losses=[]
|
| 10 |
+
for i in range(rank,c["data"]["initializations"],world):
|
| 11 |
+
x,t=synthetic_initialization(i);current=x
|
| 12 |
+
for w in range(5):out=m(current);target=t[w][None,None].expand(11,1,-1,-1);loss=crps_exp(out,target,f=c["train"]["crps_spread_factor"]);opt.zero_grad();loss.backward();opt.step();losses.append(float(loss));current=torch.cat((current[:,1:],out[-1].detach()),1)
|
| 13 |
+
v=torch.tensor([sum(losses),len(losses)],dtype=torch.float64)
|
| 14 |
+
if distributed:dist.all_reduce(v)
|
| 15 |
+
p=ROOT/c["paths"]["checkpoint"]
|
| 16 |
+
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"],{"crps_exp":float(v[0]/v[1]),"world_size":world});print(p)
|
| 17 |
+
if distributed:dist.destroy_process_group()
|
weight/.gitkeep
ADDED
|
File without changes
|