Upload folder using huggingface_hub
Browse files- .DS_Store +0 -0
- .ms_upload_cache +1 -0
- README.md +179 -0
- conf/config.yaml +66 -0
- config.json +104 -0
- configuration.json +1 -0
- model/__pycache__/graphdop.cpython-311.pyc +0 -0
- model/graphdop.py +275 -0
- scripts/fake_data.py +74 -0
- scripts/inference.py +90 -0
- scripts/result.py +203 -0
- scripts/train.py +222 -0
- weight/.gitkeep +1 -0
.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
.ms_upload_cache
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"version": 3, "repo_id": "OneScience/GraphDOP", "files": {"README.md|1787118190.0|6673": {"hash": "014371f316b8c1a1c437e6c5fedd7a93415921b4871ad37485cf1e682df834e8", "size": 6673, "status": "c"}, "conf/config.yaml|1787189077.0|2517": {"hash": "04ee6d048c400bf199bea31516dfd7c8db381cdbfd34cd9ec37b7bc1e2b1e922", "size": 2517, "status": "c"}, "model/__pycache__/graphdop.cpython-311.pyc|1787129468.0|20086": {"hash": "2ca2bd82b23d7b8a3e58209ebd5ddf898689f9929949fde90ffc7474f13fa439", "size": 20086, "status": "c"}, "scripts/fake_data.py|1787118238.0|3109": {"hash": "9ecb538acc95858926ef9f0a1f3bcbe0a228cc6a4122c4872ec946179ac885ba", "size": 3109, "status": "c"}, "scripts/inference.py|1787060565.0|3460": {"hash": "c3623c7078ace0c3c91407a7c835131adb287015f7dd84a938d473058be3bef4", "size": 3460, "status": "c"}, "scripts/result.py|1787060586.0|8607": {"hash": "2b2b68ec178f50b303f66c4a3e89d40e133a3a1559da3aaf77ad9fbcffff74e2", "size": 8607, "status": "c"}, "configuration.json|1787060533.0|39": {"hash": "6b6f30f1e82ad5243c2ff583dcd33b77949442fbf942d80d9f236ffd7a9fe796", "size": 39, "status": "c"}, "model/graphdop.py|1787060478.0|11202": {"hash": "3ed51f897038bbad17a8fcd215dbee3e37a50f6d2e66353222175132b1fe608d", "size": 11202, "status": "c"}, "scripts/train.py|1787060554.0|9302": {"hash": "b17e5ae03dedb73ae34643054dc1b1e3c89f1468dac6a8804b4966e6e90c8b33", "size": 9302, "status": "c"}, "weight/.gitkeep|1787060604.0|1": {"hash": "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b", "size": 1, "status": "c"}}}
|
README.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
- zh
|
| 6 |
+
tags:
|
| 7 |
+
- OneScience
|
| 8 |
+
- Earth science
|
| 9 |
+
- Weather forecasting
|
| 10 |
+
- Medium-range weather forecasting
|
| 11 |
+
- Observation-driven
|
| 12 |
+
- Graph neural networks
|
| 13 |
+
- ERA5
|
| 14 |
+
frameworks: PyTorch
|
| 15 |
+
datasets:
|
| 16 |
+
- OneScience/ERA5
|
| 17 |
+
---
|
| 18 |
+
<p align="center">
|
| 19 |
+
<strong>
|
| 20 |
+
<span style="font-size: 30px;">GraphDOP</span>
|
| 21 |
+
</strong>
|
| 22 |
+
</p>
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
# Model Introduction
|
| 26 |
+
|
| 27 |
+
GraphDOP (Graph-based Direct Observation Prediction) was proposed by ECMWF. It is an end-to-end observation-driven (AI-DOP) weather forecasting model based on graph neural networks (GNNs). The model uses only Earth-system observations (polar-orbiting/geostationary satellite brightness temperatures, occultation bending angles, scatterometer backscatter, radar altimetry, radiosonde and conventional surface observations, and so on) as inputs and training targets. It does not use any physics-based reanalysis field and can produce medium-range forecasts beyond 5 days.
|
| 28 |
+
|
| 29 |
+
Paper:GraphDOP: Towards skilful data-driven medium-range weather forecasts learnt and initialised directly from observations
|
| 30 |
+
|
| 31 |
+
https://arxiv.org/abs/2412.15687
|
| 32 |
+
|
| 33 |
+
# Model Description
|
| 34 |
+
|
| 35 |
+
GraphDOP uses an encoder-processor-decoder architecture: the GNN encoder maps observations in the input window to an O96 (approximately 1°) latent grid according to spatial proximity, the Transformer processor advances the atmospheric state in latent space, and the GNN decoder maps the latent grid back to target observation locations and outputs forecasts channel by channel. The training objective is channel-weighted mean squared error (WMSE). This repository is a minimal reproduction based on the paper and is integrated with the OneScience data loading and training workflow.
|
| 36 |
+
|
| 37 |
+
# Use Cases
|
| 38 |
+
|
| 39 |
+
| Scenario | Description |
|
| 40 |
+
| :---: | :--- |
|
| 41 |
+
| Observation-driven medium-range weather forecasting research | Learn atmospheric-state representations directly from observations and forecast future windows. |
|
| 42 |
+
| Graph + Transformer latent-space model research | Reproducible encoder-processor-decoder architecture and WMSE objective. |
|
| 43 |
+
| Local quick validation | Use synthetic data to check data loading, training, inference, and result scripts. |
|
| 44 |
+
| ModelScope/OneCode execution | Download the model package, install dependencies, and run the scripts directly. |
|
| 45 |
+
| Multi-card training | Launch multi-process training with `torchrun`. |
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
# Usage
|
| 49 |
+
|
| 50 |
+
## 1. OneCode Usage
|
| 51 |
+
|
| 52 |
+
Use the OneCode online environment for intelligent one-click AI4S programming:
|
| 53 |
+
|
| 54 |
+
[Try intelligent one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 55 |
+
|
| 56 |
+
## 2. Manual Installation and Usage
|
| 57 |
+
|
| 58 |
+
**Hardware Requirements**
|
| 59 |
+
|
| 60 |
+
- GPU or DCU is recommended.
|
| 61 |
+
- CPU can be used for imports and small-configuration connectivity validation, but full training and inference are slower.
|
| 62 |
+
- DCU users must install DTK beforehand. DTK 25.04.2 or later, or the OneScience-recommended version matching the current cluster, is recommended.
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
### Download the Model Package
|
| 66 |
+
|
| 67 |
+
```bash
|
| 68 |
+
hf download OneScience-Group/GraphDOP --local-dir ./GraphDOP
|
| 69 |
+
cd GraphDOP
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
### Install the Runtime Environment
|
| 73 |
+
|
| 74 |
+
**DCU Environment**
|
| 75 |
+
|
| 76 |
+
```bash
|
| 77 |
+
# Activate DTK and CONDA first
|
| 78 |
+
conda create -n onescience311 python=3.11 -y
|
| 79 |
+
conda activate onescience311
|
| 80 |
+
# uv installation is supported
|
| 81 |
+
pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
**GPU Environment**
|
| 85 |
+
```bash
|
| 86 |
+
# Activate CONDA first
|
| 87 |
+
conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
|
| 88 |
+
conda activate onescience311
|
| 89 |
+
# uv installation is supported
|
| 90 |
+
pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
### Training Data
|
| 94 |
+
|
| 95 |
+
The OneScience community provides ERA5 data for training (the current repository contains complete data slices subject to data-file size limits). Download it with the command below and confirm that the data path in `conf/config.yaml` is correct:
|
| 96 |
+
|
| 97 |
+
```bash
|
| 98 |
+
hf download --repo-type dataset OneScience-Group/ERA5 --local-dir ./data
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
For a quick workflow validation, run the synthetic data script first:
|
| 102 |
+
|
| 103 |
+
```bash
|
| 104 |
+
python scripts/fake_data.py
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
> Note: `scripts/fake_data.py` generates `[T, C, H, W]` data from the model input/output windows and `grid_shape`. Because ERA5Datapipe supports only regular grids, this project approximates the irregular Level-1 observations in the paper with six observation-grid channels.
|
| 108 |
+
|
| 109 |
+
### Training
|
| 110 |
+
|
| 111 |
+
Single card:
|
| 112 |
+
|
| 113 |
+
```bash
|
| 114 |
+
python scripts/train.py
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
Multiple cards:
|
| 118 |
+
|
| 119 |
+
```bash
|
| 120 |
+
torchrun --nproc_per_node=8 --nnodes=1 --rdzv_id=1000 --rdzv_backend=c10d --max_restarts=0 --master_addr="localhost" --master_port=29500 scripts/train.py
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
Training outputs:
|
| 124 |
+
|
| 125 |
+
```text
|
| 126 |
+
data/checkpoints/model_bak.pth
|
| 127 |
+
data/checkpoints/trloss.npy
|
| 128 |
+
data/checkpoints/valoss.npy
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
### Training Weights
|
| 132 |
+
The `weight/` folder is reserved for model weights. Pretrained weights are not provided by default; users may train the model using the paper configuration. The paper model (1024 latent channels, an O96 latent grid, and 70k steps on 64×H100 GPUs) has no publicly released weights.
|
| 133 |
+
|
| 134 |
+
### Inference
|
| 135 |
+
|
| 136 |
+
Inference reads `data/checkpoints/model_bak.pth`:
|
| 137 |
+
|
| 138 |
+
```bash
|
| 139 |
+
python scripts/inference.py
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
Prediction results are written frame by frame to:
|
| 143 |
+
|
| 144 |
+
```text
|
| 145 |
+
result/output/
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
### Evaluation and Visualization
|
| 149 |
+
|
| 150 |
+
```bash
|
| 151 |
+
python scripts/result.py
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
Outputs include:
|
| 155 |
+
|
| 156 |
+
- `result/rmse.npy`
|
| 157 |
+
- `result/acc.npy`
|
| 158 |
+
- `result/loss.png`
|
| 159 |
+
- Forecast comparison plots for the specified date and variables
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
# Official Source and Reproduction Notes
|
| 163 |
+
|
| 164 |
+
- The paper is an ECMWF AI-DOP preprint, and no official implementation is publicly available (the source is based on PyTorch Geometric and builds dynamic observation graphs per batch). This repository's `model/graphdop.py` is a pure PyTorch minimal reproduction that preserves the paper's encoder-processor-decoder GNN structure and WMSE objective.
|
| 165 |
+
- Differences from the paper (due to limitations of the OneScience gridded data pipeline): the paper consumes irregular raw Level-1 observations, with each observation connected to its nearest latent-grid neighbors using a dynamically constructed graph. This reproduction uses ERA5 gridded h5 channels as a placeholder observation field and fixes the latent grid to a regular 8-neighbor graph. The edge features (azimuth and Haversine distance) match the paper. Training augmentations such as random observation dropout (25% for satellites/50% for conventional observations) are not implemented.
|
| 166 |
+
- `conf/config.yaml` uses a small configuration (32×32 grid, 8×8 latent grid, latent_dim=64) for connectivity validation by default; paper-level reproduction requires an O96 latent grid, latent_dim=1024, and a larger data scale.
|
| 167 |
+
- The following details are not disclosed in the paper and are assumptions in this reproduction:per-channel weights `w_{c,i}` (currently all 1 by default), graph-construction details, and the specific implementation of windowed attention in the processor.
|
| 168 |
+
|
| 169 |
+
# Official OneScience Information
|
| 170 |
+
|
| 171 |
+
| Platform | OneScience Main Repository | Skills Repository |
|
| 172 |
+
| --- | --- | --- |
|
| 173 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 174 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 175 |
+
|
| 176 |
+
# Citation and License
|
| 177 |
+
|
| 178 |
+
- This repository is an independent GraphDOP reproduction (the model code is an original minimal implementation), with the architecture design based on the paper by Alexe et al. (2024).
|
| 179 |
+
- Please cite:Alexe, M., E. Boucher, P. Lean, E. Pinnington, P. Laloyaux, A. McNally et al. GraphDOP: Towards skilful data-driven medium-range weather forecasts learnt and initialised directly from observations. arXiv:2412.15687, 2024.
|
conf/config.yaml
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# GraphDOP 训练配置示例
|
| 2 |
+
# 论文配置:输入一个 12 小时观测窗口(O96 reduced Gaussian 网格约 1°、40320 潜节点),
|
| 3 |
+
# 经 GNN 编码器映射到潜网格、Transformer 处理器推进时间、GNN 解码器预测下一窗口观测;
|
| 4 |
+
# 潜空间通道 1024,WMSE 目标,18 年数据(2004-2021)训练,64×H100 70k 步。
|
| 5 |
+
# 当前为连通性验证小配置:虚拟数据 32×32 网格、潜网格 8×8、latent_dim=64。
|
| 6 |
+
model:
|
| 7 |
+
start_epoch: 0
|
| 8 |
+
max_epoch: 100
|
| 9 |
+
lr: 1E-3 # 论文起始 lr=1e-3,cosine 退火到 3e-7(warmup 1000 步)
|
| 10 |
+
patience: 50
|
| 11 |
+
checkpoint_dir: "./data/checkpoints"
|
| 12 |
+
|
| 13 |
+
# GraphDOP 结构参数(论文值见注释)
|
| 14 |
+
in_channels: 6 # 观测通道数(论文为多仪器通道,如 ATMS/AMSU-A/IASI 等)
|
| 15 |
+
out_channels: 6 # 预报通道数
|
| 16 |
+
input_steps: 2 # 输入窗口帧数(论文为单个 12h 窗口;time_step=6h 故取 2 帧)
|
| 17 |
+
output_steps: 2 # 输出窗口帧数(论文为下一个 12h 窗口)
|
| 18 |
+
grid_shape: [32, 32] # 观测网格尺寸(论文 O96 约 1°)
|
| 19 |
+
mesh_shape: [8, 8] # 潜网格尺寸(论文 O96 reduced Gaussian 40320 节点)
|
| 20 |
+
latent_dim: 64 # 潜空间通道数(论文 1024)
|
| 21 |
+
num_encoder_layers: 2 # 编码器 GNN 消息传递层数
|
| 22 |
+
num_decoder_layers: 2 # 解码器 GNN 消息传递层数
|
| 23 |
+
num_processor_blocks: 1 # 处理器 Transformer 块数
|
| 24 |
+
n_heads: 4
|
| 25 |
+
hidden_dim: 64
|
| 26 |
+
channel_weights: [1, 1, 1, 1, 1, 1] # WMSE 逐通道权重(论文 w_{c,i} 经验值)
|
| 27 |
+
|
| 28 |
+
# 整个数据读取流程
|
| 29 |
+
datapipe:
|
| 30 |
+
name: "ERA5"
|
| 31 |
+
task: "weather_forecasting"
|
| 32 |
+
|
| 33 |
+
dataset:
|
| 34 |
+
type: "hdf5"
|
| 35 |
+
data_dir: './data/'
|
| 36 |
+
train_time: [1951, 1952]
|
| 37 |
+
val_time: [1953]
|
| 38 |
+
test_time: [1954]
|
| 39 |
+
img_size: [32, 32]
|
| 40 |
+
verbose: true
|
| 41 |
+
cache: false
|
| 42 |
+
|
| 43 |
+
# 气象变量(论文观测类型占位:ATMS 亮温/掩星弯角/散射计后向散射/雷达高度计有效波高/常规观测)
|
| 44 |
+
channels: ['atms_brightness_temperature', 'gpsro_bending_angle', 'ascat_sigma0',
|
| 45 |
+
'significant_wave_height', '2m_temperature', '10m_wind_speed']
|
| 46 |
+
|
| 47 |
+
# DataLoader 配置
|
| 48 |
+
dataloader:
|
| 49 |
+
mask_dtype: "float32"
|
| 50 |
+
batch_size: 4
|
| 51 |
+
num_workers: 1
|
| 52 |
+
pin_memory: true
|
| 53 |
+
drop_last: true
|
| 54 |
+
shuffle: false
|
| 55 |
+
prefetch_factor: 2
|
| 56 |
+
persistent_workers: true
|
| 57 |
+
|
| 58 |
+
# 分布式配置
|
| 59 |
+
distributed:
|
| 60 |
+
enabled: true
|
| 61 |
+
sampler: "DistributedSampler"
|
| 62 |
+
rank: 0
|
| 63 |
+
world_size: 2
|
| 64 |
+
shuffle: true
|
| 65 |
+
seed: 42
|
| 66 |
+
drop_last: true
|
config.json
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_name": "GraphDOP",
|
| 3 |
+
"model_type": "graphdop",
|
| 4 |
+
"architectures": [
|
| 5 |
+
"GraphDOP"
|
| 6 |
+
],
|
| 7 |
+
"framework": "PyTorch",
|
| 8 |
+
"domain": "climate-and-atmosphere",
|
| 9 |
+
"task": "observation-driven-medium-range-weather-forecasting",
|
| 10 |
+
"implementation": {
|
| 11 |
+
"entry_point": "model/graphdop.py",
|
| 12 |
+
"scope": "pure-PyTorch minimal reproduction using gridded ERA5 placeholders and fixed regular-mesh graphs instead of the paper's irregular Level-1 observations and dynamic graphs"
|
| 13 |
+
},
|
| 14 |
+
"architecture": {
|
| 15 |
+
"family": "GNN encoder-Transformer processor-GNN decoder",
|
| 16 |
+
"input_format": "B T C H W",
|
| 17 |
+
"encoder": "per-grid-cell MLP, adaptive pooling to the latent mesh, then residual mean-aggregation GNN layers",
|
| 18 |
+
"processor": "pre-normalized Transformer encoder over latent-mesh tokens with learned positional embeddings",
|
| 19 |
+
"decoder": "latent-mesh GNN, bilinear upsampling, and a per-grid-cell output MLP",
|
| 20 |
+
"edge_features": [
|
| 21 |
+
"forward bearing",
|
| 22 |
+
"Haversine distance"
|
| 23 |
+
],
|
| 24 |
+
"activation": "GELU",
|
| 25 |
+
"normalization": "LayerNorm",
|
| 26 |
+
"loss": "channel-weighted mean squared error",
|
| 27 |
+
"repository_default_config": {
|
| 28 |
+
"purpose": "connectivity validation with synthetic gridded data",
|
| 29 |
+
"grid_shape": [
|
| 30 |
+
32,
|
| 31 |
+
32
|
| 32 |
+
],
|
| 33 |
+
"mesh_shape": [
|
| 34 |
+
8,
|
| 35 |
+
8
|
| 36 |
+
],
|
| 37 |
+
"in_channels": 6,
|
| 38 |
+
"out_channels": 6,
|
| 39 |
+
"input_steps": 2,
|
| 40 |
+
"output_steps": 2,
|
| 41 |
+
"latent_dim": 64,
|
| 42 |
+
"num_encoder_layers": 2,
|
| 43 |
+
"num_decoder_layers": 2,
|
| 44 |
+
"num_processor_blocks": 1,
|
| 45 |
+
"attention_heads": 4,
|
| 46 |
+
"hidden_dim": 64,
|
| 47 |
+
"channel_weights": [
|
| 48 |
+
1,
|
| 49 |
+
1,
|
| 50 |
+
1,
|
| 51 |
+
1,
|
| 52 |
+
1,
|
| 53 |
+
1
|
| 54 |
+
]
|
| 55 |
+
},
|
| 56 |
+
"paper_reference_config": {
|
| 57 |
+
"latent_grid": "O96 reduced Gaussian grid with 40320 nodes",
|
| 58 |
+
"latent_dim": 1024,
|
| 59 |
+
"observation_graph": "dynamic graph over irregular Level-1 observations",
|
| 60 |
+
"training_steps": 70000,
|
| 61 |
+
"training_hardware": "64 H100 GPUs"
|
| 62 |
+
}
|
| 63 |
+
},
|
| 64 |
+
"data": {
|
| 65 |
+
"dataset": "ERA5",
|
| 66 |
+
"role": "regular-grid placeholder for the paper's multi-instrument observations",
|
| 67 |
+
"variables": [
|
| 68 |
+
"atms_brightness_temperature",
|
| 69 |
+
"gpsro_bending_angle",
|
| 70 |
+
"ascat_sigma0",
|
| 71 |
+
"significant_wave_height",
|
| 72 |
+
"2m_temperature",
|
| 73 |
+
"10m_wind_speed"
|
| 74 |
+
],
|
| 75 |
+
"time_step_hours": 6,
|
| 76 |
+
"input_length": 2,
|
| 77 |
+
"output_length": 2,
|
| 78 |
+
"channels": 6,
|
| 79 |
+
"spatial_size": [
|
| 80 |
+
32,
|
| 81 |
+
32
|
| 82 |
+
],
|
| 83 |
+
"storage_format": "HDF5 fields with T C H W layout",
|
| 84 |
+
"train_years": [
|
| 85 |
+
1951,
|
| 86 |
+
1952
|
| 87 |
+
],
|
| 88 |
+
"validation_years": [
|
| 89 |
+
1953
|
| 90 |
+
],
|
| 91 |
+
"test_years": [
|
| 92 |
+
1954
|
| 93 |
+
]
|
| 94 |
+
},
|
| 95 |
+
"configuration_sources": [
|
| 96 |
+
"README.md",
|
| 97 |
+
"conf/config.yaml",
|
| 98 |
+
"model/graphdop.py",
|
| 99 |
+
"scripts/train.py",
|
| 100 |
+
"scripts/inference.py",
|
| 101 |
+
"scripts/fake_data.py",
|
| 102 |
+
"configuration.json"
|
| 103 |
+
]
|
| 104 |
+
}
|
configuration.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"framework":"Pytorch","task":"other"}
|
model/__pycache__/graphdop.cpython-311.pyc
ADDED
|
Binary file (20.1 kB). View file
|
|
|
model/graphdop.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding=utf-8
|
| 2 |
+
#
|
| 3 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
#
|
| 5 |
+
# Minimal reproduction of GraphDOP (ECMWF, "Towards skilful medium-range
|
| 6 |
+
# forecasts learnt directly from observations", 2025 preprint) following the
|
| 7 |
+
# encoder -- processor -- decoder design:
|
| 8 |
+
#
|
| 9 |
+
# * Encoder: a GNN that projects gridded "observations" inside the input
|
| 10 |
+
# window onto a latent mesh (a coarse regular lat/lon grid), using graph
|
| 11 |
+
# edges with (forward bearing, haversine distance) features.
|
| 12 |
+
# * Processor: a transformer that advances the latent atmospheric state
|
| 13 |
+
# forward in time, once per output frame (latent-space rollout).
|
| 14 |
+
# * Decoder: a GNN that maps the latent mesh back onto the target grid and
|
| 15 |
+
# predicts per-channel observations with instrument-like output MLPs.
|
| 16 |
+
#
|
| 17 |
+
# Differences from the paper (documented in README.md): the paper consumes
|
| 18 |
+
# irregular, instrument-specific Level-1 observations with dynamic graphs built
|
| 19 |
+
# per batch (PyTorch Geometric); here the OneScience ERA5-h5 gridded pipeline is
|
| 20 |
+
# used as the observation placeholder, and the graphs are fixed regular-grid
|
| 21 |
+
# meshes. The weighted MSE objective is kept (per-channel weights).
|
| 22 |
+
import math
|
| 23 |
+
|
| 24 |
+
import torch
|
| 25 |
+
import torch.nn as nn
|
| 26 |
+
import torch.nn.functional as F
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _latlon_grid(shape):
|
| 30 |
+
"""Regular lat/lon coordinates for a (H, W) grid, North-to-South rows."""
|
| 31 |
+
H, W = shape
|
| 32 |
+
lat = torch.linspace(90.0, -90.0, H)
|
| 33 |
+
lon = torch.linspace(0.0, 360.0 - 360.0 / W, W)
|
| 34 |
+
return lat, lon
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _haversine(lat1, lon1, lat2, lon2):
|
| 38 |
+
"""Haversine distance in metres between points given in degrees."""
|
| 39 |
+
R = 6371000.0
|
| 40 |
+
p1 = torch.deg2rad(lat1)
|
| 41 |
+
p2 = torch.deg2rad(lat2)
|
| 42 |
+
dp = torch.deg2rad(lat2 - lat1)
|
| 43 |
+
dl = torch.deg2rad(lon2 - lon1)
|
| 44 |
+
a = torch.sin(dp / 2) ** 2 + torch.cos(p1) * torch.cos(p2) * torch.sin(dl / 2) ** 2
|
| 45 |
+
return 2 * R * torch.asin(torch.sqrt(a.clamp(0, 1)))
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _bearing(lat1, lon1, lat2, lon2):
|
| 49 |
+
"""Initial forward bearing in radians from point 1 to point 2."""
|
| 50 |
+
p1 = torch.deg2rad(lat1)
|
| 51 |
+
p2 = torch.deg2rad(lat2)
|
| 52 |
+
dl = torch.deg2rad(lon2 - lon1)
|
| 53 |
+
y = torch.sin(dl) * torch.cos(p2)
|
| 54 |
+
x = torch.cos(p1) * torch.sin(p2) - torch.sin(p1) * torch.cos(p2) * torch.cos(dl)
|
| 55 |
+
return torch.atan2(y, x)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def build_mesh_graph(mesh_shape):
|
| 59 |
+
"""
|
| 60 |
+
Build a fixed 8-neighbourhood graph over a regular latent mesh.
|
| 61 |
+
Longitude wraps around; edge features are (forward bearing [rad],
|
| 62 |
+
haversine distance [km]).
|
| 63 |
+
"""
|
| 64 |
+
H, W = mesh_shape
|
| 65 |
+
lat, lon = _latlon_grid(mesh_shape)
|
| 66 |
+
lat = lat.view(-1, 1).expand(H, W)
|
| 67 |
+
lon = lon.view(1, -1).expand(H, W)
|
| 68 |
+
|
| 69 |
+
src_list, dst_list, feat_list = [], [], []
|
| 70 |
+
for i in range(H):
|
| 71 |
+
for j in range(W):
|
| 72 |
+
for di, dj in ((-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)):
|
| 73 |
+
ni, nj = i + di, (j + dj) % W
|
| 74 |
+
if not (0 <= ni < H):
|
| 75 |
+
continue
|
| 76 |
+
s = i * W + j
|
| 77 |
+
d = ni * W + nj
|
| 78 |
+
dist_km = _haversine(lat[i, j], lon[i, j], lat[ni, nj], lon[ni, nj]) / 1000.0
|
| 79 |
+
bear = _bearing(lat[i, j], lon[i, j], lat[ni, nj], lon[ni, nj])
|
| 80 |
+
src_list.append(s)
|
| 81 |
+
dst_list.append(d)
|
| 82 |
+
feat_list.append(torch.stack([bear / math.pi, dist_km / 1000.0]))
|
| 83 |
+
edge_index = torch.stack([torch.as_tensor(src_list), torch.as_tensor(dst_list)], dim=0)
|
| 84 |
+
edge_attr = torch.stack(feat_list)
|
| 85 |
+
return edge_index, edge_attr
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def _mlp(in_dim, out_dim, hidden_dim, n_layers=2):
|
| 89 |
+
dims = [in_dim] + [hidden_dim] * (n_layers - 1) + [out_dim]
|
| 90 |
+
layers = []
|
| 91 |
+
for i in range(len(dims) - 1):
|
| 92 |
+
layers.append(nn.Linear(dims[i], dims[i + 1]))
|
| 93 |
+
if i < len(dims) - 2:
|
| 94 |
+
layers.append(nn.GELU())
|
| 95 |
+
return nn.Sequential(*layers)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
class GNNLayer(nn.Module):
|
| 99 |
+
"""Message-passing layer with edge features (mean-aggregate, residual)."""
|
| 100 |
+
|
| 101 |
+
def __init__(self, dim, edge_dim=2, hidden_dim=64):
|
| 102 |
+
super().__init__()
|
| 103 |
+
self.edge_mlp = _mlp(2 * dim + edge_dim, dim, hidden_dim)
|
| 104 |
+
self.node_mlp = _mlp(dim, dim, hidden_dim)
|
| 105 |
+
self.norm = nn.LayerNorm(dim)
|
| 106 |
+
|
| 107 |
+
def forward(self, x, edge_index, edge_attr):
|
| 108 |
+
B, N, D = x.shape
|
| 109 |
+
src, dst = edge_index
|
| 110 |
+
offsets = torch.arange(B, device=x.device) * N
|
| 111 |
+
src_b = (src.unsqueeze(0) + offsets.view(B, 1)).reshape(-1)
|
| 112 |
+
dst_b = (dst.unsqueeze(0) + offsets.view(B, 1)).reshape(-1)
|
| 113 |
+
edge_attr_b = edge_attr.unsqueeze(0).expand(B, -1, -1).reshape(-1, edge_attr.size(1))
|
| 114 |
+
xb = x.reshape(B * N, D)
|
| 115 |
+
msg = self.edge_mlp(torch.cat([xb[src_b], xb[dst_b], edge_attr_b], dim=1))
|
| 116 |
+
agg = torch.zeros_like(xb)
|
| 117 |
+
agg.index_add_(0, dst_b, msg)
|
| 118 |
+
cnt = torch.bincount(dst_b, minlength=B * N).clamp(min=1).unsqueeze(1)
|
| 119 |
+
agg = agg / cnt
|
| 120 |
+
agg = agg.reshape(B, N, D)
|
| 121 |
+
return self.norm(x + self.node_mlp(agg))
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
class ObsEncoder(nn.Module):
|
| 125 |
+
"""
|
| 126 |
+
Maps the observation grid onto the latent mesh with a per-cell input MLP,
|
| 127 |
+
an adaptive pooling to the mesh resolution, and graph message passing.
|
| 128 |
+
"""
|
| 129 |
+
|
| 130 |
+
def __init__(self, in_channels, latent_dim, mesh_shape, num_layers=2, hidden_dim=64):
|
| 131 |
+
super().__init__()
|
| 132 |
+
self.in_channels = in_channels
|
| 133 |
+
self.input_mlp = _mlp(in_channels, latent_dim, hidden_dim)
|
| 134 |
+
self.gnn = nn.ModuleList([GNNLayer(latent_dim, hidden_dim=hidden_dim) for _ in range(num_layers)])
|
| 135 |
+
self.mesh_shape = mesh_shape
|
| 136 |
+
self.edge_index, self.edge_attr = build_mesh_graph(mesh_shape)
|
| 137 |
+
|
| 138 |
+
def forward(self, x):
|
| 139 |
+
B, C, H, W = x.shape
|
| 140 |
+
feat = x.permute(0, 2, 3, 1).reshape(-1, C)
|
| 141 |
+
feat = self.input_mlp(feat).reshape(B, H, W, -1).permute(0, 3, 1, 2)
|
| 142 |
+
mesh = F.adaptive_avg_pool2d(feat, self.mesh_shape)
|
| 143 |
+
mesh = mesh.permute(0, 2, 3, 1).reshape(B, -1, mesh.size(1))
|
| 144 |
+
edge_index, edge_attr = self.edge_index.to(x.device), self.edge_attr.to(x.device)
|
| 145 |
+
for layer in self.gnn:
|
| 146 |
+
mesh = layer(mesh, edge_index, edge_attr)
|
| 147 |
+
return mesh
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
class LatentProcessor(nn.Module):
|
| 151 |
+
"""Transformer over latent mesh tokens that advances the state in time."""
|
| 152 |
+
|
| 153 |
+
def __init__(self, latent_dim, mesh_shape, num_blocks=1, n_heads=4, hidden_dim=128):
|
| 154 |
+
super().__init__()
|
| 155 |
+
n_nodes = mesh_shape[0] * mesh_shape[1]
|
| 156 |
+
self.pos_emb = nn.Parameter(torch.zeros(1, n_nodes, latent_dim))
|
| 157 |
+
nn.init.trunc_normal_(self.pos_emb, std=0.02)
|
| 158 |
+
block = nn.TransformerEncoderLayer(
|
| 159 |
+
d_model=latent_dim, nhead=n_heads, dim_feedforward=hidden_dim,
|
| 160 |
+
dropout=0.0, activation="gelu", batch_first=True, norm_first=True,
|
| 161 |
+
)
|
| 162 |
+
self.blocks = nn.ModuleList([block for _ in range(num_blocks)])
|
| 163 |
+
|
| 164 |
+
def forward(self, mesh):
|
| 165 |
+
tokens = mesh + self.pos_emb
|
| 166 |
+
for block in self.blocks:
|
| 167 |
+
tokens = block(tokens)
|
| 168 |
+
return tokens
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
class ObsDecoder(nn.Module):
|
| 172 |
+
"""
|
| 173 |
+
Maps the latent mesh back onto the target grid (bilinear upsample) and
|
| 174 |
+
predicts per-channel observations with an output MLP.
|
| 175 |
+
"""
|
| 176 |
+
|
| 177 |
+
def __init__(self, latent_dim, out_channels, grid_shape, mesh_shape, num_layers=2, hidden_dim=64):
|
| 178 |
+
super().__init__()
|
| 179 |
+
self.gnn = nn.ModuleList([GNNLayer(latent_dim, hidden_dim=hidden_dim) for _ in range(num_layers)])
|
| 180 |
+
self.grid_shape = grid_shape
|
| 181 |
+
self.mesh_shape = mesh_shape
|
| 182 |
+
self.edge_index, self.edge_attr = build_mesh_graph(mesh_shape)
|
| 183 |
+
self.output_mlp = _mlp(latent_dim, out_channels, hidden_dim)
|
| 184 |
+
|
| 185 |
+
def forward(self, mesh):
|
| 186 |
+
B, N, D = mesh.shape
|
| 187 |
+
edge_index = self.edge_index.to(mesh.device)
|
| 188 |
+
edge_attr = self.edge_attr.to(mesh.device)
|
| 189 |
+
for layer in self.gnn:
|
| 190 |
+
mesh = layer(mesh, edge_index, edge_attr)
|
| 191 |
+
H, W = self.grid_shape
|
| 192 |
+
Hm, Wm = self.mesh_shape
|
| 193 |
+
mesh = mesh.transpose(1, 2).reshape(B, D, Hm, Wm)
|
| 194 |
+
grid = F.interpolate(mesh, size=self.grid_shape, mode="bilinear", align_corners=False)
|
| 195 |
+
grid = grid.permute(0, 2, 3, 1).reshape(B, H * W, D)
|
| 196 |
+
return self.output_mlp(grid).reshape(B, H, W, -1).permute(0, 3, 1, 2)
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
class GraphDOP(nn.Module):
|
| 200 |
+
"""
|
| 201 |
+
Config-driven GraphDOP wrapper.
|
| 202 |
+
|
| 203 |
+
Args:
|
| 204 |
+
in_channels: Number of observation channels per frame.
|
| 205 |
+
out_channels: Number of forecast channels per frame.
|
| 206 |
+
input_steps: Number of input (observation window) frames.
|
| 207 |
+
output_steps: Number of forecast frames.
|
| 208 |
+
grid_shape: Spatial shape of the (gridded) observation field.
|
| 209 |
+
mesh_shape: Latent mesh resolution (each dimension, powers of two fine).
|
| 210 |
+
latent_dim: Feature dimension of latent mesh tokens.
|
| 211 |
+
num_encoder_layers / num_decoder_layers: GNN message-passing layers.
|
| 212 |
+
num_processor_blocks: Transformer blocks in the processor.
|
| 213 |
+
n_heads: Attention heads of the processor.
|
| 214 |
+
channel_weights: Per-channel weights for the weighted MSE objective.
|
| 215 |
+
"""
|
| 216 |
+
|
| 217 |
+
def __init__(
|
| 218 |
+
self,
|
| 219 |
+
in_channels=6,
|
| 220 |
+
out_channels=6,
|
| 221 |
+
input_steps=2,
|
| 222 |
+
output_steps=2,
|
| 223 |
+
grid_shape=(32, 32),
|
| 224 |
+
mesh_shape=(8, 8),
|
| 225 |
+
latent_dim=64,
|
| 226 |
+
num_encoder_layers=2,
|
| 227 |
+
num_decoder_layers=2,
|
| 228 |
+
num_processor_blocks=1,
|
| 229 |
+
n_heads=4,
|
| 230 |
+
hidden_dim=64,
|
| 231 |
+
channel_weights=None,
|
| 232 |
+
):
|
| 233 |
+
super().__init__()
|
| 234 |
+
self.in_channels = int(in_channels)
|
| 235 |
+
self.out_channels = int(out_channels)
|
| 236 |
+
self.input_steps = int(input_steps)
|
| 237 |
+
self.output_steps = int(output_steps)
|
| 238 |
+
self.grid_shape = (int(grid_shape[0]), int(grid_shape[1]))
|
| 239 |
+
self.mesh_shape = (int(mesh_shape[0]), int(mesh_shape[1]))
|
| 240 |
+
|
| 241 |
+
self.encoder = ObsEncoder(
|
| 242 |
+
self.in_channels, int(latent_dim), self.mesh_shape, num_layers=int(num_encoder_layers), hidden_dim=int(hidden_dim)
|
| 243 |
+
)
|
| 244 |
+
self.processor = LatentProcessor(
|
| 245 |
+
int(latent_dim), self.mesh_shape, num_blocks=int(num_processor_blocks), n_heads=int(n_heads), hidden_dim=int(hidden_dim)
|
| 246 |
+
)
|
| 247 |
+
self.decoder = ObsDecoder(
|
| 248 |
+
int(latent_dim), self.out_channels, self.grid_shape, self.mesh_shape,
|
| 249 |
+
num_layers=int(num_decoder_layers), hidden_dim=int(hidden_dim),
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
if channel_weights is None:
|
| 253 |
+
channel_weights = torch.ones(self.out_channels)
|
| 254 |
+
self.register_buffer("channel_weights", torch.as_tensor(channel_weights, dtype=torch.float32))
|
| 255 |
+
|
| 256 |
+
def forward(self, x):
|
| 257 |
+
"""
|
| 258 |
+
Args:
|
| 259 |
+
x: Observation frames, shape [batch, input_steps, C, H, W].
|
| 260 |
+
Returns:
|
| 261 |
+
Forecast frames, shape [batch, output_steps, C, H, W].
|
| 262 |
+
"""
|
| 263 |
+
latents = torch.stack([self.encoder(x[:, t]) for t in range(self.input_steps)], dim=0)
|
| 264 |
+
latent = latents.mean(dim=0)
|
| 265 |
+
outs = []
|
| 266 |
+
for _ in range(self.output_steps):
|
| 267 |
+
latent = self.processor(latent)
|
| 268 |
+
outs.append(self.decoder(latent))
|
| 269 |
+
return torch.stack(outs, dim=1)
|
| 270 |
+
|
| 271 |
+
def wmse_loss(self, pred, target):
|
| 272 |
+
"""Weighted mean squared error objective (Eq. 1 of the paper)."""
|
| 273 |
+
diff = (pred - target) ** 2
|
| 274 |
+
w = self.channel_weights.view(1, 1, self.out_channels, 1, 1)
|
| 275 |
+
return (diff * w).mean()
|
scripts/fake_data.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import h5py
|
| 3 |
+
import numpy as np
|
| 4 |
+
from onescience.utils.YParams import YParams
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
# GraphDOP 的当前 OneScience 适配层把不规则观测投影为规则网格通道。
|
| 8 |
+
def get_dims(cfg_model, cfg_data):
|
| 9 |
+
H, W = map(int, cfg_model.grid_shape)
|
| 10 |
+
if tuple(map(int, cfg_data.dataset.img_size)) != (H, W):
|
| 11 |
+
raise ValueError("model.grid_shape and datapipe.dataset.img_size must match")
|
| 12 |
+
input_steps = int(cfg_model.input_steps)
|
| 13 |
+
output_steps = int(cfg_model.output_steps)
|
| 14 |
+
samples = int(cfg_data.dataloader.batch_size)
|
| 15 |
+
T = input_steps + output_steps + samples - 1
|
| 16 |
+
return {
|
| 17 |
+
"T": T, "H": H, "W": W, "time_step": 6,
|
| 18 |
+
"input_steps": input_steps, "output_steps": output_steps,
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def generate_fake_h5(data_dir, var_names, years, dims):
|
| 23 |
+
"""
|
| 24 |
+
为每个年份生成一个空 h5 文件。
|
| 25 |
+
利用 HDF5 chunked 数据集未写入 chunk 即返回 fill_value=0 的特性,
|
| 26 |
+
文件实际只含元数据,极小,但 shape 与真实数据完全一致。
|
| 27 |
+
均值/标准差也作为数据集内嵌进每年的 h5,与 era5.py 新版读取方式对应。
|
| 28 |
+
|
| 29 |
+
注意:ERA5Datapipe 要求 samples_per_year = T - input_steps - output_steps + 1 >= 1,
|
| 30 |
+
T 由 input_steps、output_steps 与 batch_size 自动计算。
|
| 31 |
+
"""
|
| 32 |
+
os.makedirs(os.path.join(data_dir, "data"), exist_ok=True)
|
| 33 |
+
T, C = dims["T"], len(var_names)
|
| 34 |
+
H, W = dims["H"], dims["W"]
|
| 35 |
+
|
| 36 |
+
means = np.zeros((1, C, 1, 1), dtype=np.float32)
|
| 37 |
+
stds = np.ones((1, C, 1, 1), dtype=np.float32)
|
| 38 |
+
|
| 39 |
+
for year in years:
|
| 40 |
+
path = os.path.join(data_dir, "data", f"{year}.h5")
|
| 41 |
+
with h5py.File(path, "w") as f:
|
| 42 |
+
ds = f.create_dataset(
|
| 43 |
+
"fields",
|
| 44 |
+
shape=(T, C, H, W),
|
| 45 |
+
dtype="float32",
|
| 46 |
+
chunks=(1, C, H, W),
|
| 47 |
+
fillvalue=0.0,
|
| 48 |
+
)
|
| 49 |
+
ds.attrs["variables"] = var_names
|
| 50 |
+
ds.attrs["time_step"] = dims["time_step"]
|
| 51 |
+
f.create_dataset("global_means", data=means)
|
| 52 |
+
f.create_dataset("global_stds", data=stds)
|
| 53 |
+
|
| 54 |
+
size_kb = os.path.getsize(path) / 1024
|
| 55 |
+
print(f" {year}.h5 shape=({T},{C},{H},{W}) "
|
| 56 |
+
f"logical={T*C*H*W*4/1024**3:.1f}GB actual={size_kb:.1f}KB")
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
if __name__ == "__main__":
|
| 60 |
+
cfg_model = YParams("conf/config.yaml", "model")
|
| 61 |
+
cfg_datapipe = YParams("conf/config.yaml", "datapipe")
|
| 62 |
+
|
| 63 |
+
if cfg_datapipe.dataset.data_dir.startswith("/public/") or cfg_datapipe.dataset.data_dir.startswith("/work2/"):
|
| 64 |
+
print("请检查 config,确保各 *_dir 指向本地测试路径而非生产路径。")
|
| 65 |
+
exit()
|
| 66 |
+
|
| 67 |
+
years = cfg_datapipe.dataset.train_time + cfg_datapipe.dataset.val_time + cfg_datapipe.dataset.test_time
|
| 68 |
+
atm_vars = cfg_datapipe.dataset.channels
|
| 69 |
+
if len(atm_vars) != int(cfg_model.in_channels) or len(atm_vars) != int(cfg_model.out_channels):
|
| 70 |
+
raise ValueError("channel count must match model input/output channels")
|
| 71 |
+
|
| 72 |
+
generate_fake_h5(cfg_datapipe.dataset.data_dir, atm_vars, years, get_dims(cfg_model, cfg_datapipe))
|
| 73 |
+
|
| 74 |
+
print("\n✅ Fake datasets generated.")
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
# 获取项目根目录(inference.py上级的上级)
|
| 5 |
+
root_path = Path(__file__).parent.parent
|
| 6 |
+
sys.path.append(str(root_path))
|
| 7 |
+
import torch
|
| 8 |
+
import os
|
| 9 |
+
import glob
|
| 10 |
+
import numpy as np
|
| 11 |
+
import h5py
|
| 12 |
+
from tqdm import tqdm
|
| 13 |
+
from model.graphdop import GraphDOP
|
| 14 |
+
from onescience.utils.YParams import YParams
|
| 15 |
+
from onescience.datapipes.climate import ERA5Datapipe
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def get_stats(data_dir, channels):
|
| 19 |
+
"""从新版 h5 中读取变量列表与归一化参数(均值/标准差)"""
|
| 20 |
+
h5_files = sorted(glob.glob(os.path.join(data_dir, "data", "*.h5")))
|
| 21 |
+
with h5py.File(h5_files[0], "r") as f:
|
| 22 |
+
ds = f["fields"]
|
| 23 |
+
all_variables = [v.decode() if isinstance(v, bytes) else v for v in ds.attrs["variables"]]
|
| 24 |
+
mu = f["global_means"][:] # [1, C, 1, 1]
|
| 25 |
+
std = f["global_stds"][:]
|
| 26 |
+
|
| 27 |
+
channel_indices = [all_variables.index(v) for v in channels]
|
| 28 |
+
means = mu[:, channel_indices, :, :]
|
| 29 |
+
stds = std[:, channel_indices, :, :]
|
| 30 |
+
return means, stds
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
if __name__ == "__main__":
|
| 34 |
+
current_path = os.getcwd()
|
| 35 |
+
sys.path.append(current_path)
|
| 36 |
+
|
| 37 |
+
## Model config init
|
| 38 |
+
config_file_path = os.path.join(current_path, "conf/config.yaml")
|
| 39 |
+
cfg = YParams(config_file_path, "model")
|
| 40 |
+
|
| 41 |
+
## DataLoader init
|
| 42 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 43 |
+
means, stds = get_stats(cfg_data.dataset.data_dir, cfg_data.dataset.channels)
|
| 44 |
+
|
| 45 |
+
cfg['N_in_channels'] = len(cfg_data.dataset.channels)
|
| 46 |
+
cfg['N_out_channels'] = len(cfg_data.dataset.channels)
|
| 47 |
+
|
| 48 |
+
datapipe = ERA5Datapipe(
|
| 49 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 50 |
+
used_variables=cfg_data.dataset.channels,
|
| 51 |
+
used_years=cfg_data.dataset.test_time,
|
| 52 |
+
distributed=False,
|
| 53 |
+
input_steps=cfg.input_steps,
|
| 54 |
+
output_steps=cfg.output_steps,
|
| 55 |
+
batch_size=1,
|
| 56 |
+
num_workers=4,
|
| 57 |
+
)
|
| 58 |
+
test_dataloader, _ = datapipe.get_dataloader("test")
|
| 59 |
+
|
| 60 |
+
device = "cuda:0" if torch.cuda.is_available() else "cpu"
|
| 61 |
+
ckpt = torch.load(f"{cfg.checkpoint_dir}/model_bak.pth", map_location=device, weights_only=False)
|
| 62 |
+
model = GraphDOP(
|
| 63 |
+
in_channels=cfg['N_in_channels'],
|
| 64 |
+
out_channels=cfg['N_out_channels'],
|
| 65 |
+
input_steps=cfg.input_steps,
|
| 66 |
+
output_steps=cfg.output_steps,
|
| 67 |
+
grid_shape=cfg.grid_shape,
|
| 68 |
+
mesh_shape=cfg.mesh_shape,
|
| 69 |
+
latent_dim=cfg.latent_dim,
|
| 70 |
+
num_encoder_layers=cfg.num_encoder_layers,
|
| 71 |
+
num_decoder_layers=cfg.num_decoder_layers,
|
| 72 |
+
num_processor_blocks=cfg.num_processor_blocks,
|
| 73 |
+
n_heads=cfg.n_heads,
|
| 74 |
+
hidden_dim=cfg.hidden_dim,
|
| 75 |
+
channel_weights=cfg.channel_weights,
|
| 76 |
+
).to(device)
|
| 77 |
+
model.load_state_dict(ckpt["model_state_dict"])
|
| 78 |
+
|
| 79 |
+
model.eval()
|
| 80 |
+
os.makedirs('result/output/', exist_ok=True)
|
| 81 |
+
print(f"📂 infer results will be generated to './result/output/'")
|
| 82 |
+
with torch.no_grad():
|
| 83 |
+
for data in tqdm(test_dataloader, desc="Inferring testset", unit="batch"):
|
| 84 |
+
invar = data[0].to(device, dtype=torch.float32) # [1, input_steps, C, H, W]
|
| 85 |
+
pred = model(invar).cpu().numpy() # [1, output_steps, C, H, W]
|
| 86 |
+
for t in range(pred.shape[1]):
|
| 87 |
+
fname = data[4][cfg.input_steps + t][0] # 该预测帧对应的时刻
|
| 88 |
+
pred_var = pred[0, t] # [C, H, W]
|
| 89 |
+
pred_var = pred_var * stds + means
|
| 90 |
+
np.save(f"result/output/{fname}.npy", pred_var)
|
scripts/result.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
import glob
|
| 6 |
+
import h5py
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
from tqdm import tqdm
|
| 9 |
+
from onescience.utils.fcn.YParams import YParams
|
| 10 |
+
from matplotlib import rcParams
|
| 11 |
+
|
| 12 |
+
# rcParams['font.family'] = 'serif'
|
| 13 |
+
# rcParams['font.serif'] = ['DejaVu Serif']
|
| 14 |
+
rcParams['mathtext.fontset'] = 'stix'
|
| 15 |
+
rcParams['axes.linewidth'] = 0.9
|
| 16 |
+
rcParams['xtick.major.width'] = 0.9
|
| 17 |
+
rcParams['ytick.major.width'] = 0.9
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def get_metadata(data_dir, channels):
|
| 21 |
+
"""从新版 h5 attrs 中读取变量列表和 time_step"""
|
| 22 |
+
h5_files = sorted(glob.glob(os.path.join(data_dir, "data", "*.h5")))
|
| 23 |
+
with h5py.File(h5_files[0], "r") as f:
|
| 24 |
+
ds = f["fields"]
|
| 25 |
+
all_variables = [v.decode() if isinstance(v, bytes) else v for v in ds.attrs["variables"]]
|
| 26 |
+
time_step = int(ds.attrs["time_step"])
|
| 27 |
+
|
| 28 |
+
channel_indices = [all_variables.index(v) for v in channels]
|
| 29 |
+
|
| 30 |
+
total_files = [f for f in os.listdir('./result/output/') if f.endswith('.npy')]
|
| 31 |
+
total_files.sort()
|
| 32 |
+
return total_files, channel_indices, time_step
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def filename_to_index(filename, time_step):
|
| 36 |
+
"""将 YYYYMMDDHH 格式的文件名转换为年度 h5 文件中的时间步索引"""
|
| 37 |
+
dt = datetime.strptime(filename, "%Y%m%d%H")
|
| 38 |
+
year_start = datetime(dt.year, 1, 1)
|
| 39 |
+
hours = (dt - year_start).total_seconds() / 3600
|
| 40 |
+
return int(hours / time_step)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def get_result(total_files, channel_indices, time_step, data_dir, clim_mean):
|
| 44 |
+
channel_rmse = np.zeros(len(channel_indices))
|
| 45 |
+
channel_acc = np.zeros(len(channel_indices))
|
| 46 |
+
clim_mean = clim_mean[0, :, :, :]
|
| 47 |
+
if not os.path.exists('./result/rmse.npy') or not os.path.exists('result/acc.npy'):
|
| 48 |
+
numerator = np.zeros(len(channel_indices))
|
| 49 |
+
pred_sq_sum = np.zeros(len(channel_indices))
|
| 50 |
+
label_sq_sum = np.zeros(len(channel_indices))
|
| 51 |
+
for file in tqdm(total_files, unit="files"):
|
| 52 |
+
fname = file[:-4] # 去掉 .npy
|
| 53 |
+
year = fname[:4]
|
| 54 |
+
t_idx = filename_to_index(fname, time_step)
|
| 55 |
+
with h5py.File(os.path.join(data_dir, 'data', f'{year}.h5'), "r") as f:
|
| 56 |
+
label = f["fields"][t_idx] # [C, H, W]
|
| 57 |
+
label = label[channel_indices]
|
| 58 |
+
pred = np.load(f'result/output/{file}').squeeze()
|
| 59 |
+
if pred.ndim == 2:
|
| 60 |
+
pred = pred[np.newaxis] # 单通道时 squeeze 会压缩掉通道维,恢复为 [C, H, W]
|
| 61 |
+
|
| 62 |
+
label_anom = label - clim_mean
|
| 63 |
+
pred_anom = pred - clim_mean
|
| 64 |
+
# 累加
|
| 65 |
+
numerator += np.sum(pred_anom * label_anom, axis=(1, 2))
|
| 66 |
+
pred_sq_sum += np.sum(pred_anom ** 2, axis=(1, 2))
|
| 67 |
+
label_sq_sum += np.sum(label_anom ** 2, axis=(1, 2))
|
| 68 |
+
|
| 69 |
+
channel_rmse += np.sqrt(np.mean((label - pred) ** 2, axis=(1, 2)))
|
| 70 |
+
channel_rmse /= len(total_files)
|
| 71 |
+
channel_acc = numerator / (np.sqrt(pred_sq_sum * label_sq_sum) + 1e-8)
|
| 72 |
+
np.save('./result/acc.npy', channel_acc)
|
| 73 |
+
np.save('./result/rmse.npy', channel_rmse)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def show_result():
|
| 77 |
+
channel_rmse = np.load('./result/rmse.npy')
|
| 78 |
+
channel_acc = np.load('./result/acc.npy')
|
| 79 |
+
|
| 80 |
+
channels = [cfg_data.dataset.channels[i] for i in range(len(channel_indices))]
|
| 81 |
+
w = 24 # 最长 channel 名宽度
|
| 82 |
+
|
| 83 |
+
# 表头
|
| 84 |
+
print(f"┌{'─' * (w + 2)}┬{'─' * 14}┬{'─' * 14}┐")
|
| 85 |
+
print(f"│ {'Channel':<{w}} │ {'RMSE':>12} │ {'ACC':>12} │")
|
| 86 |
+
print(f"├{'─' * (w + 2)}┼{'─' * 14}┼{'─' * 14}┤")
|
| 87 |
+
# 数据行
|
| 88 |
+
for i, ch in enumerate(channels):
|
| 89 |
+
print(f"│ {ch:<{w}} │ {channel_rmse[i]:>12.4f} | {channel_acc[i]:>12.4f} |")
|
| 90 |
+
print(f"├{'─' * (w + 2)}┼{'─' * 14}┼{'─' * 14}┤")
|
| 91 |
+
print(f"│ {'Average':<{w}} │ {np.mean(channel_rmse):>12.4f} │ {np.mean(channel_acc):>12.4f} │")
|
| 92 |
+
print(f"└{'─' * (w + 2)}┴{'─' * 14}┴{'─' * 14}┘")
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def plot(label, pred, var, filename):
|
| 96 |
+
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
|
| 97 |
+
|
| 98 |
+
xtick_labels = ['180°W', '90°W', '0°', '90°E', '180°E']
|
| 99 |
+
ytick_labels = ['90°S', '45°S', '0°', '45°N', '90°N']
|
| 100 |
+
xticks = np.linspace(0, label.shape[-1] - 1, 5)
|
| 101 |
+
yticks = np.linspace(0, label.shape[-2] - 1, 5)
|
| 102 |
+
|
| 103 |
+
vmin = min(label.min(), pred.min())
|
| 104 |
+
vmax = max(label.max(), pred.max())
|
| 105 |
+
|
| 106 |
+
diff = label - pred
|
| 107 |
+
rmse = np.sqrt(np.mean(diff ** 2))
|
| 108 |
+
diff_abs_max = np.abs(diff).max()
|
| 109 |
+
|
| 110 |
+
plot_configs = [
|
| 111 |
+
{'data': label, 'title': 'Truth', 'cmap': 'viridis', 'vmin': vmin, 'vmax': vmax},
|
| 112 |
+
{'data': pred, 'title': 'Prediction', 'cmap': 'viridis', 'vmin': vmin, 'vmax': vmax},
|
| 113 |
+
{'data': diff, 'title': f'Difference (RMSE={rmse:.2f})', 'cmap': 'RdBu_r', 'vmin': -diff_abs_max, 'vmax': diff_abs_max},
|
| 114 |
+
]
|
| 115 |
+
|
| 116 |
+
for ax, cfg in zip(axes, plot_configs):
|
| 117 |
+
im = ax.imshow(cfg['data'], cmap=cfg['cmap'], vmin=cfg['vmin'], vmax=cfg['vmax'])
|
| 118 |
+
ax.set_title(cfg['title'], fontsize=12, pad=4)
|
| 119 |
+
ax.set_xlabel('Longitude')
|
| 120 |
+
ax.set_ylabel('Latitude')
|
| 121 |
+
ax.set_xticks(xticks)
|
| 122 |
+
ax.set_xticklabels(xtick_labels)
|
| 123 |
+
ax.set_yticks(yticks)
|
| 124 |
+
ax.set_yticklabels(ytick_labels)
|
| 125 |
+
plt.colorbar(im, ax=ax, orientation='horizontal')
|
| 126 |
+
|
| 127 |
+
fig.suptitle(var, fontsize=14, fontweight='bold', y=0.98)
|
| 128 |
+
plt.savefig(filename, dpi=300, bbox_inches='tight')
|
| 129 |
+
plt.close()
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def plot_loss(train_loss, valid_loss):
|
| 133 |
+
mask = ~(np.isnan(train_loss) | np.isnan(valid_loss))
|
| 134 |
+
train_loss = train_loss[mask]
|
| 135 |
+
valid_loss = valid_loss[mask]
|
| 136 |
+
|
| 137 |
+
fig, ax = plt.subplots(figsize=(5, 3.5))
|
| 138 |
+
colors = {'train': '#2563EB', 'valid': '#EA580C'}
|
| 139 |
+
epochs = np.arange(1, len(train_loss) + 1)
|
| 140 |
+
|
| 141 |
+
ax.plot(epochs, train_loss, color=colors['train'], linewidth=1.5, label='Train')
|
| 142 |
+
ax.plot(epochs, valid_loss, color=colors['valid'], linewidth=1.5, label='Valid', linestyle='--')
|
| 143 |
+
min_idx = np.argmin(valid_loss)
|
| 144 |
+
ax.scatter(epochs[min_idx], valid_loss[min_idx],
|
| 145 |
+
color=colors['valid'], s=40, zorder=5, edgecolors='white')
|
| 146 |
+
ax.annotate(f'Best: {valid_loss[min_idx]:.3f}',
|
| 147 |
+
xy=(epochs[min_idx], valid_loss[min_idx]),
|
| 148 |
+
xytext=(10, 10), textcoords='offset points', fontsize=8, color=colors['valid'],
|
| 149 |
+
arrowprops=dict(arrowstyle='-', color=colors['valid'], lw=0.5))
|
| 150 |
+
|
| 151 |
+
ax.set(xlabel='Epoch', ylabel='Loss', xlim=(0, len(train_loss) + 1))
|
| 152 |
+
ax.legend(frameon=False, loc='upper right')
|
| 153 |
+
ax.grid(True, linestyle='--', alpha=0.3)
|
| 154 |
+
ax.spines[['top', 'right']].set_visible(False)
|
| 155 |
+
|
| 156 |
+
plt.tight_layout()
|
| 157 |
+
plt.savefig('./result/loss.png', dpi=300, bbox_inches='tight')
|
| 158 |
+
plt.close()
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
if __name__ == "__main__":
|
| 162 |
+
current_path = os.getcwd()
|
| 163 |
+
sys.path.append(current_path)
|
| 164 |
+
config_file_path = os.path.join(current_path, 'conf/config.yaml')
|
| 165 |
+
cfg = YParams(config_file_path, 'model')
|
| 166 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 167 |
+
|
| 168 |
+
train_loss = np.load('./data/checkpoints/trloss.npy')
|
| 169 |
+
valid_loss = np.load('./data/checkpoints/valoss.npy')
|
| 170 |
+
plot_loss(train_loss, valid_loss)
|
| 171 |
+
|
| 172 |
+
data_dir = cfg_data.dataset.data_dir
|
| 173 |
+
total_files, channel_indices, time_step = get_metadata(data_dir, cfg_data.dataset.channels)
|
| 174 |
+
|
| 175 |
+
# Load data & Compute RMSE/ACC per channel
|
| 176 |
+
h5_files = sorted(glob.glob(os.path.join(data_dir, "data", "*.h5")))
|
| 177 |
+
with h5py.File(h5_files[0], "r") as f:
|
| 178 |
+
mu = f["global_means"][:]
|
| 179 |
+
clim_mean = mu[:, channel_indices, :, :]
|
| 180 |
+
get_result(total_files, channel_indices, time_step, data_dir, clim_mean)
|
| 181 |
+
show_result()
|
| 182 |
+
|
| 183 |
+
##### 默认绘制第一个预测输出的时刻与全部通道,用户可自行指定 #####
|
| 184 |
+
eg_files = [total_files[0][:-4]]
|
| 185 |
+
# 最多绘制 3 个通道的对比图
|
| 186 |
+
channel_index = list(range(min(3, len(cfg_data.dataset.channels))))
|
| 187 |
+
|
| 188 |
+
selected_var = [cfg_data.dataset.channels[int(i)] for i in channel_index]
|
| 189 |
+
print(f"seleted date: {eg_files}")
|
| 190 |
+
print(f"selected channels: {selected_var}")
|
| 191 |
+
for file in eg_files:
|
| 192 |
+
year = file[:4]
|
| 193 |
+
t_idx = filename_to_index(file, time_step)
|
| 194 |
+
with h5py.File(os.path.join(data_dir, 'data', f'{year}.h5'), "r") as f:
|
| 195 |
+
label = f["fields"][t_idx] # [C, H, W]
|
| 196 |
+
label = label[channel_indices]
|
| 197 |
+
pred = np.load(f'result/output/{file}.npy').squeeze()
|
| 198 |
+
if pred.ndim == 2:
|
| 199 |
+
pred = pred[np.newaxis] # 单通道时 squeeze 会压缩掉通道维,恢复为 [C, H, W]
|
| 200 |
+
for i in range(len(selected_var)):
|
| 201 |
+
filename = f'./result/{file}_{selected_var[i]}.png'
|
| 202 |
+
plot(label[channel_index[i]], pred[channel_index[i]], selected_var[i], filename)
|
| 203 |
+
print(f'✅plot {filename}')
|
scripts/train.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
# 获取项目根目录(train.py上级的上级)
|
| 5 |
+
root_path = Path(__file__).parent.parent
|
| 6 |
+
sys.path.append(str(root_path))
|
| 7 |
+
import torch
|
| 8 |
+
import os
|
| 9 |
+
import numpy as np
|
| 10 |
+
import torch.distributed as dist
|
| 11 |
+
import logging
|
| 12 |
+
import time
|
| 13 |
+
|
| 14 |
+
from model.graphdop import GraphDOP
|
| 15 |
+
from onescience.datapipes.climate import ERA5Datapipe
|
| 16 |
+
from onescience.utils.YParams import YParams
|
| 17 |
+
|
| 18 |
+
try:
|
| 19 |
+
from apex import optimizers
|
| 20 |
+
_FUSED_ADAM = True
|
| 21 |
+
except Exception:
|
| 22 |
+
_FUSED_ADAM = False
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def main():
|
| 26 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
| 27 |
+
logger = logging.getLogger()
|
| 28 |
+
|
| 29 |
+
## Model config init
|
| 30 |
+
config_file_path = os.path.join(current_path, "conf/config.yaml")
|
| 31 |
+
cfg = YParams(config_file_path, "model")
|
| 32 |
+
|
| 33 |
+
## Distributed config init
|
| 34 |
+
cfg.world_size = 1
|
| 35 |
+
if "WORLD_SIZE" in os.environ:
|
| 36 |
+
cfg.world_size = int(os.environ["WORLD_SIZE"])
|
| 37 |
+
world_rank = 0
|
| 38 |
+
local_rank = 0
|
| 39 |
+
if cfg.world_size > 1 and torch.cuda.is_available():
|
| 40 |
+
dist.init_process_group(backend="nccl", init_method="env://")
|
| 41 |
+
local_rank = int(os.environ["LOCAL_RANK"])
|
| 42 |
+
world_rank = dist.get_rank()
|
| 43 |
+
device = f"cuda:{local_rank}" if torch.cuda.is_available() else "cpu"
|
| 44 |
+
|
| 45 |
+
## DataLoader init
|
| 46 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 47 |
+
cfg['N_in_channels'] = len(cfg_data.dataset.channels)
|
| 48 |
+
cfg['N_out_channels'] = len(cfg_data.dataset.channels)
|
| 49 |
+
datapipe = ERA5Datapipe(
|
| 50 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 51 |
+
used_variables=cfg_data.dataset.channels,
|
| 52 |
+
used_years=cfg_data.dataset.train_time,
|
| 53 |
+
distributed=dist.is_initialized(),
|
| 54 |
+
input_steps=cfg.input_steps,
|
| 55 |
+
output_steps=cfg.output_steps,
|
| 56 |
+
batch_size=cfg_data.dataloader.batch_size,
|
| 57 |
+
num_workers=cfg_data.dataloader.num_workers,
|
| 58 |
+
)
|
| 59 |
+
train_dataloader, train_sampler = datapipe.get_dataloader("train")
|
| 60 |
+
datapipe = ERA5Datapipe(
|
| 61 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 62 |
+
used_variables=cfg_data.dataset.channels,
|
| 63 |
+
used_years=cfg_data.dataset.val_time,
|
| 64 |
+
distributed=dist.is_initialized(),
|
| 65 |
+
input_steps=cfg.input_steps,
|
| 66 |
+
output_steps=cfg.output_steps,
|
| 67 |
+
batch_size=cfg_data.dataloader.batch_size,
|
| 68 |
+
num_workers=cfg_data.dataloader.num_workers,
|
| 69 |
+
)
|
| 70 |
+
val_dataloader, val_sampler = datapipe.get_dataloader("valid")
|
| 71 |
+
|
| 72 |
+
# Model init
|
| 73 |
+
model = GraphDOP(
|
| 74 |
+
in_channels=cfg['N_in_channels'],
|
| 75 |
+
out_channels=cfg['N_out_channels'],
|
| 76 |
+
input_steps=cfg.input_steps,
|
| 77 |
+
output_steps=cfg.output_steps,
|
| 78 |
+
grid_shape=cfg.grid_shape,
|
| 79 |
+
mesh_shape=cfg.mesh_shape,
|
| 80 |
+
latent_dim=cfg.latent_dim,
|
| 81 |
+
num_encoder_layers=cfg.num_encoder_layers,
|
| 82 |
+
num_decoder_layers=cfg.num_decoder_layers,
|
| 83 |
+
num_processor_blocks=cfg.num_processor_blocks,
|
| 84 |
+
n_heads=cfg.n_heads,
|
| 85 |
+
hidden_dim=cfg.hidden_dim,
|
| 86 |
+
channel_weights=cfg.channel_weights,
|
| 87 |
+
).to(device)
|
| 88 |
+
|
| 89 |
+
if _FUSED_ADAM:
|
| 90 |
+
optimizer = optimizers.FusedAdam(model.parameters(), lr=cfg.lr)
|
| 91 |
+
else:
|
| 92 |
+
optimizer = torch.optim.Adam(model.parameters(), lr=cfg.lr)
|
| 93 |
+
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, factor=0.2, patience=5, mode='min')
|
| 94 |
+
|
| 95 |
+
## Train process init
|
| 96 |
+
os.makedirs(cfg.checkpoint_dir, exist_ok=True)
|
| 97 |
+
train_loss_file = f"{cfg.checkpoint_dir}/trloss.npy"
|
| 98 |
+
valid_loss_file = f"{cfg.checkpoint_dir}/valoss.npy"
|
| 99 |
+
best_valid_loss = float("inf")
|
| 100 |
+
best_loss_epoch = 0
|
| 101 |
+
train_losses = np.empty((0,), dtype=np.float32)
|
| 102 |
+
valid_losses = np.empty((0,), dtype=np.float32)
|
| 103 |
+
|
| 104 |
+
## Get model params count
|
| 105 |
+
if cfg.world_size == 1:
|
| 106 |
+
total_params = sum(p.numel() for p in model.parameters())
|
| 107 |
+
print("\n\n")
|
| 108 |
+
print("-" * 50)
|
| 109 |
+
print(f"📂 now params is {total_params}, {total_params / 1e6:.2f}M, {total_params / 1e9:.2f}B")
|
| 110 |
+
print("-" * 50, "\n")
|
| 111 |
+
|
| 112 |
+
## Load model weight if there exist well-trained model
|
| 113 |
+
if os.path.exists(f"{cfg.checkpoint_dir}/model_bak.pth"):
|
| 114 |
+
if world_rank == 0:
|
| 115 |
+
print("\n\n")
|
| 116 |
+
print("-" * 50)
|
| 117 |
+
print(f"✅ There has a model weight, load and continue training...")
|
| 118 |
+
print(f'If you want to train a new model, ensure there is no *.pth file in {cfg.checkpoint_dir}')
|
| 119 |
+
print("-" * 50, "\n")
|
| 120 |
+
ckpt = torch.load(f"{cfg.checkpoint_dir}/model_bak.pth", map_location=device, weights_only=False)
|
| 121 |
+
model.load_state_dict(ckpt["model_state_dict"])
|
| 122 |
+
optimizer.load_state_dict(ckpt["optimizer_state_dict"])
|
| 123 |
+
scheduler.load_state_dict(ckpt["scheduler_state_dict"])
|
| 124 |
+
best_valid_loss = ckpt["best_valid_loss"]
|
| 125 |
+
best_loss_epoch = ckpt["best_loss_epoch"]
|
| 126 |
+
train_losses = np.load(train_loss_file)
|
| 127 |
+
valid_losses = np.load(valid_loss_file)
|
| 128 |
+
|
| 129 |
+
## Distributed model
|
| 130 |
+
if dist.is_initialized():
|
| 131 |
+
model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[local_rank], output_device=local_rank)
|
| 132 |
+
world_rank == 0 and logger.info(f"start training ...")
|
| 133 |
+
|
| 134 |
+
for epoch in range(cfg.max_epoch):
|
| 135 |
+
if dist.is_initialized():
|
| 136 |
+
train_sampler.set_epoch(epoch)
|
| 137 |
+
val_sampler.set_epoch(epoch)
|
| 138 |
+
model.train()
|
| 139 |
+
train_loss = 0
|
| 140 |
+
start_time = time.time()
|
| 141 |
+
for j, data in enumerate(train_dataloader):
|
| 142 |
+
invar = data[0].to(device, dtype=torch.float32) # [B, input_steps, C, H, W]
|
| 143 |
+
outvar = data[1].to(device, dtype=torch.float32) # [B, output_steps, C, H, W]
|
| 144 |
+
outvar_pred = model(invar) # [B, output_steps, C, H, W]
|
| 145 |
+
loss = model.wmse_loss(outvar_pred, outvar) # 论文式(1) WMSE
|
| 146 |
+
optimizer.zero_grad()
|
| 147 |
+
loss.backward()
|
| 148 |
+
optimizer.step()
|
| 149 |
+
train_loss += loss.item()
|
| 150 |
+
if world_rank == 0:
|
| 151 |
+
logger.info(f'Train: Epoch {epoch}-{j+1}/{len(train_dataloader)} '
|
| 152 |
+
f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
|
| 153 |
+
f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
|
| 154 |
+
f'loss:{train_loss / (j+1): .04f}')
|
| 155 |
+
|
| 156 |
+
train_loss /= len(train_dataloader)
|
| 157 |
+
|
| 158 |
+
model.eval()
|
| 159 |
+
valid_loss = 0
|
| 160 |
+
with torch.no_grad():
|
| 161 |
+
start_time = time.time()
|
| 162 |
+
for j, data in enumerate(val_dataloader):
|
| 163 |
+
invar = data[0].to(device, dtype=torch.float32)
|
| 164 |
+
outvar = data[1].to(device, dtype=torch.float32)
|
| 165 |
+
outvar_pred = model(invar)
|
| 166 |
+
loss = model.wmse_loss(outvar_pred, outvar)
|
| 167 |
+
|
| 168 |
+
if dist.is_initialized():
|
| 169 |
+
loss_tensor = loss.detach().to(device)
|
| 170 |
+
dist.all_reduce(loss_tensor)
|
| 171 |
+
loss = loss_tensor.item() / cfg.world_size
|
| 172 |
+
valid_loss += loss
|
| 173 |
+
else:
|
| 174 |
+
valid_loss += loss.item()
|
| 175 |
+
if world_rank == 0:
|
| 176 |
+
logger.info(f'Valid: Epoch {epoch}-{j+1}/{len(val_dataloader)} '
|
| 177 |
+
f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
|
| 178 |
+
f'loss:{valid_loss / (j+1): .04f}')
|
| 179 |
+
|
| 180 |
+
valid_loss /= len(val_dataloader)
|
| 181 |
+
is_save_ckp = False
|
| 182 |
+
if valid_loss < best_valid_loss:
|
| 183 |
+
best_valid_loss = valid_loss
|
| 184 |
+
best_loss_epoch = epoch
|
| 185 |
+
world_rank == 0 and save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, cfg.checkpoint_dir)
|
| 186 |
+
is_save_ckp = True
|
| 187 |
+
scheduler.step(valid_loss)
|
| 188 |
+
|
| 189 |
+
if world_rank == 0:
|
| 190 |
+
logger.info(f"Epoch [{epoch + 1}/{cfg.max_epoch}], "
|
| 191 |
+
f"Train Loss: {train_loss:.4f}, "
|
| 192 |
+
f"Valid Loss: {valid_loss:.4f}, "
|
| 193 |
+
f"Best loss at Epoch: {best_loss_epoch + 1}"
|
| 194 |
+
+ (", saving checkpoint" if is_save_ckp else "")
|
| 195 |
+
)
|
| 196 |
+
train_losses = np.append(train_losses, train_loss)
|
| 197 |
+
valid_losses = np.append(valid_losses, valid_loss)
|
| 198 |
+
np.save(train_loss_file, train_losses)
|
| 199 |
+
np.save(valid_loss_file, valid_losses)
|
| 200 |
+
|
| 201 |
+
if epoch - best_loss_epoch > cfg.patience:
|
| 202 |
+
print(f"Loss has not decrease in {cfg.patience} epochs, stopping training...")
|
| 203 |
+
exit()
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, model_path):
|
| 207 |
+
model_to_save = model.module if hasattr(model, "module") else model
|
| 208 |
+
state = {"model_state_dict": model_to_save.state_dict(),
|
| 209 |
+
"optimizer_state_dict": optimizer.state_dict(),
|
| 210 |
+
"scheduler_state_dict": scheduler.state_dict(),
|
| 211 |
+
"best_valid_loss": best_valid_loss,
|
| 212 |
+
"best_loss_epoch": best_loss_epoch,
|
| 213 |
+
}
|
| 214 |
+
torch.save(state, f"{model_path}/model.pth")
|
| 215 |
+
### the weight file saving may interrupted due to DCU queue limit, get a backup to ensure there at least has one model
|
| 216 |
+
os.system(f"mv {model_path}/model.pth {model_path}/model_bak.pth")
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
if __name__ == "__main__":
|
| 220 |
+
current_path = os.getcwd()
|
| 221 |
+
sys.path.append(current_path)
|
| 222 |
+
main()
|
weight/.gitkeep
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|