Publish MetNet-2 reproduction
Browse files- .gitattributes +2 -34
- README.md +148 -0
- README_zh.md +149 -0
- conf/config.yaml +42 -0
- config.json +41 -0
- configuration.json +12 -0
- model/metnet_2.py +298 -0
- scripts/fake_data.py +15 -0
- scripts/inference.py +41 -0
- scripts/result.py +32 -0
- scripts/train.py +65 -0
- weight/.gitkeep +0 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,3 @@
|
|
| 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 |
-
*.
|
| 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
|
| 3 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
tags:
|
| 6 |
+
- OneScience
|
| 7 |
+
- earth-science
|
| 8 |
+
- precipitation-forecasting
|
| 9 |
+
- MetNet-2
|
| 10 |
+
frameworks: PyTorch
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
<p align="center">
|
| 14 |
+
<strong><span style="font-size: 30px;">MetNet-2</span></strong>
|
| 15 |
+
</p>
|
| 16 |
+
|
| 17 |
+
# Model Introduction
|
| 18 |
+
|
| 19 |
+
MetNet-2 generates high-resolution probabilistic precipitation forecasts up to 12 hours ahead from radar, satellite, and atmospheric-state inputs for short-range forecasting and extreme-precipitation risk analysis.
|
| 20 |
+
|
| 21 |
+
Paper: Deep learning for twelve hour precipitation forecasts
|
| 22 |
+
https://doi.org/10.1038/s41467-022-32483-x
|
| 23 |
+
|
| 24 |
+
# Model Description
|
| 25 |
+
|
| 26 |
+
The method was proposed by the Google Research team. The paper constructs its 2017-2020 training and test data from MRMS radar, GOES satellite imagery, and HRRR assimilated atmospheric states. The model predicts grid-cell precipitation probability distributions at two-minute intervals up to 12 hours ahead.
|
| 27 |
+
|
| 28 |
+
# Use Cases
|
| 29 |
+
|
| 30 |
+
| Use Case | Description |
|
| 31 |
+
| :---: | :--- |
|
| 32 |
+
| Categorical precipitation forecasting | Validate the 512-category conditional distribution and 12-hour lead-time protocol. |
|
| 33 |
+
| Core-method validation | Validate ConvLSTM, lead-time FiLM, and multiscale dilated residual stacks. |
|
| 34 |
+
| Local engineering validation | Exercise the complete logical `641×512×512` contract through a deterministic procedural field without materializing the full input. |
|
| 35 |
+
| ModelScope/OneCode execution | Validate structured data, training, inference, probabilistic precipitation metrics, and visualization in ModelScope or OneCode environments. |
|
| 36 |
+
| Multi-GPU training | Validate distributed training and the checkpoint workflow through `torchrun`. |
|
| 37 |
+
|
| 38 |
+
# Usage Instructions
|
| 39 |
+
|
| 40 |
+
## 1.OneCode
|
| 41 |
+
|
| 42 |
+
Experience intelligent, one-click AI4S programming through the OneCode online environment:
|
| 43 |
+
|
| 44 |
+
[Try intelligent, one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 45 |
+
|
| 46 |
+
## 2. Download and Installation
|
| 47 |
+
|
| 48 |
+
```bash
|
| 49 |
+
hf download OneScience-Group/MetNet-2 --local-dir ./MetNet-2
|
| 50 |
+
cd MetNet-2
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
### Environment Dependencies
|
| 54 |
+
|
| 55 |
+
**Hardware Requirements**
|
| 56 |
+
|
| 57 |
+
- A GPU or DCU is recommended.
|
| 58 |
+
- A CPU can be used for connectivity validation with the default small-sample configuration.
|
| 59 |
+
- DCU users must install DTK in advance. DTK 25.04.2 or later, or the OneScience-recommended version matching the current cluster, is recommended.
|
| 60 |
+
|
| 61 |
+
**DCU Environment**
|
| 62 |
+
|
| 63 |
+
```bash
|
| 64 |
+
# Activate DTK and Conda first
|
| 65 |
+
conda create -n onescience311 python=3.11 -y
|
| 66 |
+
conda activate onescience311
|
| 67 |
+
pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
**GPU Environment**
|
| 71 |
+
|
| 72 |
+
```bash
|
| 73 |
+
# Activate Conda first
|
| 74 |
+
conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
|
| 75 |
+
conda activate onescience311
|
| 76 |
+
pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
### Training Data
|
| 80 |
+
|
| 81 |
+
The paper training inputs combine MRMS, HRRR, GOES, static geography, and temporal information under a 641-channel, `512×512` spatial-domain protocol. This repository creates eight deterministic window records and constructs only the selected `32×32` windows plus their halos at runtime while retaining the complete logical shape and channel grouping. The synthetic data validate engineering connectivity only and do not represent real meteorological distributions, paper-scale training, or paper performance.
|
| 82 |
+
|
| 83 |
+
```bash
|
| 84 |
+
python scripts/fake_data.py
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
### Training
|
| 88 |
+
|
| 89 |
+
For single-GPU training, use:
|
| 90 |
+
|
| 91 |
+
```bash
|
| 92 |
+
python scripts/train.py
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
For multi-GPU training, use:
|
| 96 |
+
|
| 97 |
+
```bash
|
| 98 |
+
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
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
The default reduces sample count, network width, residual blocks, and training steps without reducing the 641 channels, 512 precipitation categories, or 12-hour lead protocol. Formal experiments require real MRMS, GOES, and HRRR data and full computing resources, with artifacts saved to:
|
| 102 |
+
|
| 103 |
+
```text
|
| 104 |
+
result/checkpoints/metnet_2.pt
|
| 105 |
+
result/training/metrics.json
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
### Trained Weights
|
| 109 |
+
|
| 110 |
+
The paper does not provide directly loadable official model weights, and this repository bundles no weights under `weight/`. The locally trained checkpoint is saved to `result/checkpoints/metnet_2.pt` and must not be represented as an official pretrained weight.
|
| 111 |
+
|
| 112 |
+
### Inference
|
| 113 |
+
|
| 114 |
+
```bash
|
| 115 |
+
python scripts/inference.py
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
Inference loads the training checkpoint and generates 512-category precipitation probabilities and their CDF for selected spatial windows while preserving coverage and completeness metadata. Complete numerical results are saved to:
|
| 119 |
+
|
| 120 |
+
```text
|
| 121 |
+
result/output/predictions.npz
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
### Evaluation and Visualization
|
| 125 |
+
|
| 126 |
+
```bash
|
| 127 |
+
python scripts/result.py
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
Evaluation computes discrete CRPS, Brier Score, and CSI at multiple precipitation thresholds and generates target, expected-rate, and error comparisons. Synthetic-data results validate engineering only and do not represent paper performance; outputs are saved to:
|
| 131 |
+
|
| 132 |
+
```text
|
| 133 |
+
result/evaluation/metrics.json
|
| 134 |
+
result/evaluation/comparison.png
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
# Official OneScience Information
|
| 138 |
+
|
| 139 |
+
| Platform | OneScience Main Repository | Skills Repository |
|
| 140 |
+
| --- | --- | --- |
|
| 141 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 142 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 143 |
+
|
| 144 |
+
# Citation and License
|
| 145 |
+
|
| 146 |
+
This repository is an independent engineering reproduction of the public MetNet-2 specifications.
|
| 147 |
+
|
| 148 |
+
Use of this repository's code, official model weights, and data remains subject to the licenses and terms of their respective projects.
|
README_zh.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
language:
|
| 4 |
+
- zh
|
| 5 |
+
- en
|
| 6 |
+
tags:
|
| 7 |
+
- OneScience
|
| 8 |
+
- 地球科学
|
| 9 |
+
- 降水预报
|
| 10 |
+
- MetNet-2
|
| 11 |
+
frameworks: PyTorch
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
<p align="center">
|
| 15 |
+
<strong><span style="font-size: 30px;">MetNet-2</span></strong>
|
| 16 |
+
</p>
|
| 17 |
+
|
| 18 |
+
# 模型介绍
|
| 19 |
+
|
| 20 |
+
MetNet-2 根据雷达、卫星和大气状态生成未来 12 小时的高分辨率概率降水预报,主要用于短时降水预测和极端降水风险分析。
|
| 21 |
+
|
| 22 |
+
论文:Deep learning for twelve hour precipitation forecasts
|
| 23 |
+
https://doi.org/10.1038/s41467-022-32483-x
|
| 24 |
+
|
| 25 |
+
# 模型描述
|
| 26 |
+
|
| 27 |
+
该方法由 Google Research 团队提出。论文使用 MRMS 雷达、GOES 卫星和 HRRR 同化大气状态构建 2017-2020 年训练与测试数据。模型适用于以 2 分钟间隔预测未来 12 小时的逐网格降水概率分布。
|
| 28 |
+
|
| 29 |
+
# 适用场景
|
| 30 |
+
|
| 31 |
+
| 场景 | 说明 |
|
| 32 |
+
| :---: | :--- |
|
| 33 |
+
| 降水分类预报 | 验证 512 类条件降水分布及 12 小时时效协议。 |
|
| 34 |
+
| 核心方法验证 | 验证 ConvLSTM、lead-time FiLM 和多尺度膨胀残差栈。 |
|
| 35 |
+
| 本地工程验证 | 使用确定性 procedural field 验证完整 `641×512×512` 逻辑协议,不物化完整输入。 |
|
| 36 |
+
| ModelScope/OneCode 运行 | 在 ModelScope 或 OneCode 环境中验证结构化数据、训练、推理、概率降水指标和可视化流程。 |
|
| 37 |
+
| 多卡训练 | 通过 `torchrun` 验证分布式训练和 checkpoint 流程。 |
|
| 38 |
+
|
| 39 |
+
# 使用说明
|
| 40 |
+
|
| 41 |
+
## 1.OneCode
|
| 42 |
+
|
| 43 |
+
可通过 OneCode 在线环境体验智能化一键式 AI4S 编程:
|
| 44 |
+
|
| 45 |
+
[点击体验智能化一键式 AI4S 编程](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 46 |
+
|
| 47 |
+
## 2.下载安装
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
modelscope download --model OneScience/MetNet-2 --local_dir ./MetNet-2
|
| 51 |
+
cd MetNet-2
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
### 环境依赖
|
| 55 |
+
|
| 56 |
+
**硬件要求**
|
| 57 |
+
|
| 58 |
+
- 推荐使用 GPU 或 DCU 运行。
|
| 59 |
+
- CPU 可用于默认小样本配置的连通性验证。
|
| 60 |
+
- DCU 用户需预先安装 DTK,建议使用 DTK 25.04.2 以上版本或与当前集群匹配的 OneScience 推荐版本。
|
| 61 |
+
|
| 62 |
+
**DCU环境**
|
| 63 |
+
|
| 64 |
+
```bash
|
| 65 |
+
# 请首先激活 DTK 及 Conda
|
| 66 |
+
conda create -n onescience311 python=3.11 -y
|
| 67 |
+
conda activate onescience311
|
| 68 |
+
pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
**GPU环境**
|
| 72 |
+
|
| 73 |
+
```bash
|
| 74 |
+
# 请首先激活 Conda
|
| 75 |
+
conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
|
| 76 |
+
conda activate onescience311
|
| 77 |
+
pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
### 训练数据介绍
|
| 81 |
+
|
| 82 |
+
论文训练数据由 MRMS、HRRR、GOES、静态地理和时间信息组成,并遵循 641 通道、`512×512` 空间域协议。本仓库生成 8 条确定性窗口记录,运行时仅构造选定的 `32×32` 窗口及 halo,同时保留完整逻辑形状和通道分组。合成数据只验证工程连通性,不代表真实气象数据分布、论文训练规模或论文性能。
|
| 83 |
+
|
| 84 |
+
```bash
|
| 85 |
+
python scripts/fake_data.py
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
### 训练
|
| 89 |
+
|
| 90 |
+
单卡训练可使用:
|
| 91 |
+
|
| 92 |
+
```bash
|
| 93 |
+
python scripts/train.py
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
多卡训练可使用:
|
| 97 |
+
|
| 98 |
+
```bash
|
| 99 |
+
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
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
默认配置仅缩小样本数、网络宽度、残差块数量和训练步数,不缩小 641 通道、512 个降水类别和 12 小时时效协议。正式实验需要真实 MRMS、GOES 和 HRRR 数据及完整计算资源,训练产物保存到:
|
| 103 |
+
|
| 104 |
+
```text
|
| 105 |
+
result/checkpoints/metnet_2.pt
|
| 106 |
+
result/training/metrics.json
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
### 训练权重
|
| 110 |
+
|
| 111 |
+
论文未提供可直接加载的官方模型权重,本仓库不在 `weight/` 中内置权重。本地训练生成的 checkpoint 保存到 `result/checkpoints/metnet_2.pt`,不得描述为官方预训练权重。
|
| 112 |
+
|
| 113 |
+
### 推理
|
| 114 |
+
|
| 115 |
+
```bash
|
| 116 |
+
python scripts/inference.py
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
推理加载训练 checkpoint,为选定空间窗口生成 512 类降水概率及对应 CDF,并明确保存覆盖范围和完整性标记。完整数值结果保存到:
|
| 120 |
+
|
| 121 |
+
```text
|
| 122 |
+
result/output/predictions.npz
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
### 评估和可视化
|
| 126 |
+
|
| 127 |
+
```bash
|
| 128 |
+
python scripts/result.py
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
评估计算离散 CRPS、Brier Score 和不同降水阈值下的 CSI,并生成目标、期望降水率和误差对比图。虚拟数据结果仅用于验证工程流程,不代表论文正式性能;结果保存到:
|
| 132 |
+
|
| 133 |
+
```text
|
| 134 |
+
result/evaluation/metrics.json
|
| 135 |
+
result/evaluation/comparison.png
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
# OneScience官方信息
|
| 139 |
+
|
| 140 |
+
| 平台 | OneScience 主仓库 | Skills 仓库 |
|
| 141 |
+
| --- | --- | --- |
|
| 142 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 143 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 144 |
+
|
| 145 |
+
# 引用与许可证
|
| 146 |
+
|
| 147 |
+
本仓库为 MetNet-2 公开规格的独立工程复现版本。
|
| 148 |
+
|
| 149 |
+
本仓库代码、官方模型权重和数据的使用仍应以各自项目中的许可证及使用条款为准。
|
conf/config.yaml
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
seed: 2026
|
| 2 |
+
data:
|
| 3 |
+
path: data/metnet2_fake.npz
|
| 4 |
+
format_version: metnet2_selected_windows_v1
|
| 5 |
+
logical_shape: [641, 512, 512]
|
| 6 |
+
channel_groups: {mrms_radar_history: 33, hrrr_atmosphere_history: 484, goes_satellite_history: 96, static_geography: 24, time_coordinates: 4}
|
| 7 |
+
window: 32
|
| 8 |
+
halo: 8
|
| 9 |
+
samples: 8
|
| 10 |
+
model:
|
| 11 |
+
input_channels: 641
|
| 12 |
+
classes: 512
|
| 13 |
+
width: 8
|
| 14 |
+
stacks: 1
|
| 15 |
+
dilations: [1, 2, 4, 8, 16, 32, 64, 128]
|
| 16 |
+
lead_max_minutes: 720
|
| 17 |
+
training:
|
| 18 |
+
batch_size: 1
|
| 19 |
+
steps: 1
|
| 20 |
+
learning_rate: 0.001
|
| 21 |
+
class_chunk: 64
|
| 22 |
+
runtime:
|
| 23 |
+
device: auto
|
| 24 |
+
num_threads: 2
|
| 25 |
+
inference:
|
| 26 |
+
lead_minutes: 2
|
| 27 |
+
coverage: selected 32x32 window
|
| 28 |
+
is_complete: false
|
| 29 |
+
paper_model:
|
| 30 |
+
input_channels: 641
|
| 31 |
+
classes: 512
|
| 32 |
+
stacks: 3
|
| 33 |
+
dilations: [1, 2, 4, 8, 16, 32, 64, 128]
|
| 34 |
+
lead_times_minutes: {start: 2, stop: 720, step: 2}
|
| 35 |
+
spatial_upscale: 4
|
| 36 |
+
methods: [ConvLSTM, lead-time FiLM, dilated residual stacks, center crop, bilinear upsampling]
|
| 37 |
+
paths:
|
| 38 |
+
checkpoint: result/checkpoints/metnet_2.pt
|
| 39 |
+
training_metrics: result/training/metrics.json
|
| 40 |
+
predictions: result/output/predictions.npz
|
| 41 |
+
evaluation_metrics: result/evaluation/metrics.json
|
| 42 |
+
comparison: result/evaluation/comparison.png
|
config.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_name": "MetNet-2",
|
| 3 |
+
"model_type": "metnet_2",
|
| 4 |
+
"architectures": ["MetNet2"],
|
| 5 |
+
"framework": "PyTorch",
|
| 6 |
+
"domain": "weather",
|
| 7 |
+
"task": "probabilistic-precipitation-forecasting",
|
| 8 |
+
"implementation": {
|
| 9 |
+
"entry_point": "model/metnet_2.py",
|
| 10 |
+
"scope": "core-method and logical full-dimension sampled-window engineering reproduction",
|
| 11 |
+
"train_script": "scripts/train.py",
|
| 12 |
+
"inference_script": "scripts/inference.py",
|
| 13 |
+
"evaluation_script": "scripts/result.py",
|
| 14 |
+
"synthetic_data_script": "scripts/fake_data.py"
|
| 15 |
+
},
|
| 16 |
+
"architecture": {
|
| 17 |
+
"logical_input_shape": ["B", 641, 512, 512],
|
| 18 |
+
"logical_output_shape": ["B", 512, 512, 512],
|
| 19 |
+
"engineering_window": [32, 32],
|
| 20 |
+
"classes": 512,
|
| 21 |
+
"lead_minutes": [2, 720, 2],
|
| 22 |
+
"core": ["ConvLSTM", "lead-time FiLM", "dilated residual blocks", "spatial and class chunking"]
|
| 23 |
+
},
|
| 24 |
+
"data": {
|
| 25 |
+
"datasets": ["MRMS", "HRRR", "GOES"],
|
| 26 |
+
"format_version": "metnet2_selected_windows_v1",
|
| 27 |
+
"input_channels": 641,
|
| 28 |
+
"precipitation_range_mm_h": [0.0, 102.4],
|
| 29 |
+
"coverage": "selected 32x32 target windows",
|
| 30 |
+
"is_complete_global": false,
|
| 31 |
+
"synthetic": true
|
| 32 |
+
},
|
| 33 |
+
"configuration_sources": [
|
| 34 |
+
"conf/config.yaml",
|
| 35 |
+
"model/metnet_2.py",
|
| 36 |
+
"scripts/fake_data.py",
|
| 37 |
+
"scripts/train.py",
|
| 38 |
+
"scripts/inference.py",
|
| 39 |
+
"scripts/result.py"
|
| 40 |
+
]
|
| 41 |
+
}
|
configuration.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"framework": "PyTorch",
|
| 3 |
+
"task": "probabilistic precipitation forecasting",
|
| 4 |
+
"model": "MetNet-2",
|
| 5 |
+
"input_format": "logical BCHW: [B,641,512,512] with sampled windows for engineering execution",
|
| 6 |
+
"protocol": "lead-conditioned categorical precipitation forecasting from 2 to 720 minutes",
|
| 7 |
+
"default_config": "conf/config.yaml",
|
| 8 |
+
"training": "scripts/train.py",
|
| 9 |
+
"inference": "scripts/inference.py",
|
| 10 |
+
"evaluation": "scripts/result.py",
|
| 11 |
+
"visualization": "scripts/result.py"
|
| 12 |
+
}
|
model/metnet_2.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shape-faithful, memory-bounded MetNet-2 engineering implementation."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import json
|
| 5 |
+
import os
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Iterable
|
| 8 |
+
|
| 9 |
+
import numpy as np
|
| 10 |
+
import torch
|
| 11 |
+
from torch import Tensor, nn
|
| 12 |
+
import torch.nn.functional as F
|
| 13 |
+
from torch.utils.data import Dataset
|
| 14 |
+
import yaml
|
| 15 |
+
|
| 16 |
+
CHANNEL_GROUPS = (
|
| 17 |
+
("mrms_radar_history", 33), ("hrrr_atmosphere_history", 484),
|
| 18 |
+
("goes_satellite_history", 96), ("static_geography", 24),
|
| 19 |
+
("time_coordinates", 4),
|
| 20 |
+
)
|
| 21 |
+
LOGICAL_SHAPE = (641, 512, 512)
|
| 22 |
+
CLASS_RATES = np.linspace(0.0, 102.4, 512, dtype=np.float32)
|
| 23 |
+
assert sum(size for _, size in CHANNEL_GROUPS) == LOGICAL_SHAPE[0]
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def load_config(path: str | Path = "conf/config.yaml") -> dict:
|
| 27 |
+
with Path(path).open(encoding="utf-8") as handle:
|
| 28 |
+
return yaml.safe_load(handle)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class ProceduralField:
|
| 32 |
+
"""Generate crops of a logical [641, 512, 512] field without materializing it."""
|
| 33 |
+
|
| 34 |
+
shape = LOGICAL_SHAPE
|
| 35 |
+
|
| 36 |
+
def __init__(self, seed: int):
|
| 37 |
+
self.seed = int(seed)
|
| 38 |
+
|
| 39 |
+
def window(self, y: int, x: int, size: int, halo: int = 0) -> Tensor:
|
| 40 |
+
if size <= 0 or halo < 0 or not (0 <= y < 512 and 0 <= x < 512):
|
| 41 |
+
raise ValueError("invalid selected-window coordinates")
|
| 42 |
+
yy = torch.arange(y - halo, y + size + halo).clamp(0, 511).float()
|
| 43 |
+
xx = torch.arange(x - halo, x + size + halo).clamp(0, 511).float()
|
| 44 |
+
channels = torch.arange(641).float()[:, None, None]
|
| 45 |
+
return (torch.sin((channels + self.seed) * .017 + yy[None, :, None] * .031)
|
| 46 |
+
+ torch.cos((channels + 3 * self.seed) * .011 + xx[None, None, :] * .023)).float()
|
| 47 |
+
|
| 48 |
+
def target_window(self, y: int, x: int, size: int, lead: int) -> Tensor:
|
| 49 |
+
yy = torch.arange(y, y + size)[:, None]
|
| 50 |
+
xx = torch.arange(x, x + size)[None, :]
|
| 51 |
+
return ((yy * 7 + xx * 11 + self.seed + lead // 2) % 512).long()
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class WindowDataset(Dataset):
|
| 55 |
+
def __init__(self, data_path: str | Path, split: str = "train"):
|
| 56 |
+
with np.load(data_path) as data:
|
| 57 |
+
required = ("seed", "split", "y", "x", "size", "halo", "lead_minutes")
|
| 58 |
+
missing = set(required).difference(data.files)
|
| 59 |
+
if missing:
|
| 60 |
+
raise ValueError(f"dataset is missing fields: {sorted(missing)}")
|
| 61 |
+
indices = np.flatnonzero(data["split"].astype(str) == split)
|
| 62 |
+
self.records = [{key: data[key][i].item() for key in required} for i in indices]
|
| 63 |
+
|
| 64 |
+
def __len__(self) -> int:
|
| 65 |
+
return len(self.records)
|
| 66 |
+
|
| 67 |
+
def __getitem__(self, index: int) -> tuple[Tensor, Tensor, Tensor]:
|
| 68 |
+
record = self.records[index]
|
| 69 |
+
field = ProceduralField(record["seed"])
|
| 70 |
+
args = record["y"], record["x"], record["size"]
|
| 71 |
+
return (field.window(*args, record["halo"]),
|
| 72 |
+
field.target_window(*args, record["lead_minutes"]),
|
| 73 |
+
torch.tensor(record["lead_minutes"], dtype=torch.long))
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def write_fake_data(path: str | Path, samples: int = 8, window: int = 32, halo: int = 8) -> Path:
|
| 77 |
+
if samples < 3 or window != 32 or window + 2 * halo > 512:
|
| 78 |
+
raise ValueError("fake data requires at least 3 selected 32x32 windows with a valid halo")
|
| 79 |
+
records = []
|
| 80 |
+
for i in range(samples):
|
| 81 |
+
records.append({
|
| 82 |
+
"id": f"sample-{i:04d}", "seed": 1000 + i,
|
| 83 |
+
"split": "train" if i < samples - 2 else "test",
|
| 84 |
+
"y": (i * 47) % (512 - window + 1), "x": (i * 83) % (512 - window + 1),
|
| 85 |
+
"size": window, "halo": halo, "lead_minutes": 2 + 2 * (i % 360),
|
| 86 |
+
})
|
| 87 |
+
output = Path(path)
|
| 88 |
+
output.parent.mkdir(parents=True, exist_ok=True)
|
| 89 |
+
np.savez_compressed(output, **{key: np.asarray([r[key] for r in records]) for key in records[0]})
|
| 90 |
+
return output
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
class LeadFiLMConv(nn.Module):
|
| 94 |
+
def __init__(self, cin: int, cout: int, dilation: int = 1):
|
| 95 |
+
super().__init__()
|
| 96 |
+
self.conv = nn.Conv2d(cin, cout, 3, padding=dilation, dilation=dilation)
|
| 97 |
+
self.film = nn.Linear(cout, 2 * cout)
|
| 98 |
+
|
| 99 |
+
def forward(self, x: Tensor, lead: Tensor) -> Tensor:
|
| 100 |
+
result = self.conv(x)
|
| 101 |
+
add, multiply = self.film(lead).chunk(2, dim=1)
|
| 102 |
+
return result * (1.0 + torch.tanh(multiply)[:, :, None, None]) + add[:, :, None, None]
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
class ConvLSTMCell(nn.Module):
|
| 106 |
+
def __init__(self, cin: int, hidden: int):
|
| 107 |
+
super().__init__()
|
| 108 |
+
self.hidden = hidden
|
| 109 |
+
self.gates = nn.Conv2d(cin + hidden, 4 * hidden, 3, padding=1)
|
| 110 |
+
|
| 111 |
+
def forward(self, x: Tensor, state: tuple[Tensor, Tensor] | None = None) -> tuple[Tensor, Tensor]:
|
| 112 |
+
if state is None:
|
| 113 |
+
shape = (x.shape[0], self.hidden, x.shape[-2], x.shape[-1])
|
| 114 |
+
state = x.new_zeros(shape), x.new_zeros(shape)
|
| 115 |
+
hidden, cell = state
|
| 116 |
+
in_gate, forget, candidate, out_gate = self.gates(torch.cat((x, hidden), 1)).chunk(4, 1)
|
| 117 |
+
cell = torch.sigmoid(forget) * cell + torch.sigmoid(in_gate) * torch.tanh(candidate)
|
| 118 |
+
return torch.sigmoid(out_gate) * torch.tanh(cell), cell
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
class DilatedResidualBlock(nn.Module):
|
| 122 |
+
def __init__(self, width: int, dilation: int):
|
| 123 |
+
super().__init__()
|
| 124 |
+
self.conv1 = LeadFiLMConv(width, width, dilation)
|
| 125 |
+
self.conv2 = LeadFiLMConv(width, width, dilation)
|
| 126 |
+
|
| 127 |
+
def forward(self, x: Tensor, lead: Tensor) -> Tensor:
|
| 128 |
+
return x + self.conv2(F.relu(self.conv1(F.relu(x), lead)), lead)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
class MetNet2(nn.Module):
|
| 132 |
+
"""MetNet-2 concept model retaining the 641-channel and 512-class contracts."""
|
| 133 |
+
|
| 134 |
+
def __init__(self, input_channels: int = 641, classes: int = 512, width: int = 8,
|
| 135 |
+
stacks: int = 1, dilations: Iterable[int] = (1, 2, 4, 8, 16, 32, 64, 128),
|
| 136 |
+
lead_max_minutes: int = 720):
|
| 137 |
+
super().__init__()
|
| 138 |
+
if input_channels != 641 or classes != 512:
|
| 139 |
+
raise ValueError("MetNet-2 requires 641 input channels and 512 output classes")
|
| 140 |
+
self.input_channels, self.classes = input_channels, classes
|
| 141 |
+
self.width, self.stacks = width, stacks
|
| 142 |
+
self.dilations = tuple(dilations)
|
| 143 |
+
self.lead_max_minutes, self.upscale = lead_max_minutes, 4
|
| 144 |
+
self.lead_embedding = nn.Sequential(nn.Linear(1, width), nn.SiLU(), nn.Linear(width, width))
|
| 145 |
+
self.input_projection = nn.Conv2d(input_channels, width, 1)
|
| 146 |
+
self.temporal = ConvLSTMCell(width, width)
|
| 147 |
+
self.blocks = nn.ModuleList(DilatedResidualBlock(width, dilation)
|
| 148 |
+
for _ in range(stacks) for dilation in self.dilations)
|
| 149 |
+
self.spatial = LeadFiLMConv(width, width)
|
| 150 |
+
self.head = nn.Conv2d(width, classes, 1)
|
| 151 |
+
|
| 152 |
+
def _lead(self, minutes: Tensor) -> Tensor:
|
| 153 |
+
if torch.any((minutes < 2) | (minutes > self.lead_max_minutes) | (minutes % 2 != 0)):
|
| 154 |
+
raise ValueError("lead time must be 2..720 minutes in 2-minute increments")
|
| 155 |
+
return self.lead_embedding((minutes.float() / self.lead_max_minutes).unsqueeze(1))
|
| 156 |
+
|
| 157 |
+
def _features(self, x: Tensor, lead_minutes: Tensor, output_size: int) -> Tensor:
|
| 158 |
+
if x.ndim != 4 or x.shape[1] != 641:
|
| 159 |
+
raise ValueError("x must have shape [B, 641, H, W]")
|
| 160 |
+
if output_size <= 0 or output_size % self.upscale:
|
| 161 |
+
raise ValueError("output_size must be positive and divisible by four")
|
| 162 |
+
lead = self._lead(lead_minutes.to(x.device))
|
| 163 |
+
features, _ = self.temporal(self.input_projection(x))
|
| 164 |
+
for block in self.blocks:
|
| 165 |
+
features = block(features, lead)
|
| 166 |
+
features = self.spatial(F.relu(features), lead)
|
| 167 |
+
crop = output_size // self.upscale
|
| 168 |
+
if min(features.shape[-2:]) < crop:
|
| 169 |
+
raise ValueError("input window is smaller than the requested output")
|
| 170 |
+
top, left = (features.shape[-2] - crop) // 2, (features.shape[-1] - crop) // 2
|
| 171 |
+
return F.interpolate(features[:, :, top:top + crop, left:left + crop], size=(output_size, output_size),
|
| 172 |
+
mode="bilinear", align_corners=False)
|
| 173 |
+
|
| 174 |
+
def forward_window(self, x: Tensor, lead_minutes: Tensor, output_size: int = 32,
|
| 175 |
+
class_slice: tuple[int, int] | None = None) -> Tensor:
|
| 176 |
+
features = self._features(x, lead_minutes, output_size)
|
| 177 |
+
start, end = class_slice or (0, self.classes)
|
| 178 |
+
if not (0 <= start < end <= self.classes):
|
| 179 |
+
raise ValueError("invalid class slice")
|
| 180 |
+
return F.conv2d(features, self.head.weight[start:end], self.head.bias[start:end])
|
| 181 |
+
|
| 182 |
+
def forward(self, x: Tensor, lead_minutes: Tensor, output_size: int = 32) -> Tensor:
|
| 183 |
+
return self.forward_window(x, lead_minutes, output_size)
|
| 184 |
+
|
| 185 |
+
@torch.no_grad()
|
| 186 |
+
def assemble_full(self, source: ProceduralField, lead_minutes: int, output_path: str | Path,
|
| 187 |
+
tile: int = 32, halo: int = 8, class_chunk: int = 64,
|
| 188 |
+
output: str = "probability", device: str | torch.device = "cpu") -> Path:
|
| 189 |
+
"""Stream a complete [512, 512, 512] probability or CDF array to disk."""
|
| 190 |
+
if output not in {"probability", "cdf"}:
|
| 191 |
+
raise ValueError("output must be probability or cdf")
|
| 192 |
+
path = Path(output_path)
|
| 193 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 194 |
+
array = np.lib.format.open_memmap(path, mode="w+", dtype=np.float16, shape=(512, 512, 512))
|
| 195 |
+
self.eval().to(device)
|
| 196 |
+
lead = torch.tensor([lead_minutes], device=device)
|
| 197 |
+
for y in range(0, 512, tile):
|
| 198 |
+
for x0 in range(0, 512, tile):
|
| 199 |
+
size = min(tile, 512 - y, 512 - x0)
|
| 200 |
+
features = self._features(source.window(y, x0, size, halo).unsqueeze(0).to(device), lead, size)[0]
|
| 201 |
+
maximum = None
|
| 202 |
+
for start in range(0, 512, class_chunk):
|
| 203 |
+
logits = F.conv2d(features.unsqueeze(0), self.head.weight[start:start + class_chunk],
|
| 204 |
+
self.head.bias[start:start + class_chunk])[0]
|
| 205 |
+
value = logits.amax(0)
|
| 206 |
+
maximum = value if maximum is None else torch.maximum(maximum, value)
|
| 207 |
+
denominator = torch.zeros_like(maximum)
|
| 208 |
+
chunks = []
|
| 209 |
+
for start in range(0, 512, class_chunk):
|
| 210 |
+
logits = F.conv2d(features.unsqueeze(0), self.head.weight[start:start + class_chunk],
|
| 211 |
+
self.head.bias[start:start + class_chunk])[0]
|
| 212 |
+
exponent = torch.exp(logits - maximum)
|
| 213 |
+
denominator += exponent.sum(0)
|
| 214 |
+
chunks.append(exponent)
|
| 215 |
+
cumulative = torch.zeros_like(maximum)
|
| 216 |
+
for start, exponent in zip(range(0, 512, class_chunk), chunks):
|
| 217 |
+
values = exponent / denominator
|
| 218 |
+
if output == "cdf":
|
| 219 |
+
values = values.cumsum(0) + cumulative
|
| 220 |
+
cumulative = values[-1]
|
| 221 |
+
array[start:start + values.shape[0], y:y + size, x0:x0 + size] = values.cpu().numpy()
|
| 222 |
+
array.flush()
|
| 223 |
+
return path
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def build_model(config: dict, paper: bool = False) -> MetNet2:
|
| 227 |
+
values = dict(config["model"])
|
| 228 |
+
if paper:
|
| 229 |
+
values.update({key: value for key, value in config["paper_model"].items()
|
| 230 |
+
if key in {"input_channels", "classes", "stacks", "dilations"}})
|
| 231 |
+
dilations = tuple(values.get("dilations", ()))
|
| 232 |
+
if dilations != (1, 2, 4, 8, 16, 32, 64, 128):
|
| 233 |
+
raise ValueError("each dilation stack must use rates 1,2,4,8,16,32,64,128")
|
| 234 |
+
if paper and values["stacks"] != 3:
|
| 235 |
+
raise ValueError("the paper model requires three dilation stacks")
|
| 236 |
+
return MetNet2(**values)
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
def categorical_nll_chunked(model: MetNet2, x: Tensor, lead: Tensor, target: Tensor,
|
| 240 |
+
output_size: int = 32, class_chunk: int = 64) -> Tensor:
|
| 241 |
+
"""Compute exact categorical NLL while applying the class head in chunks."""
|
| 242 |
+
features = model._features(x, lead, output_size)
|
| 243 |
+
selected, logsumexp = torch.zeros_like(target, dtype=features.dtype), None
|
| 244 |
+
for start in range(0, model.classes, class_chunk):
|
| 245 |
+
end = min(start + class_chunk, model.classes)
|
| 246 |
+
logits = F.conv2d(features, model.head.weight[start:end], model.head.bias[start:end])
|
| 247 |
+
part = torch.logsumexp(logits, dim=1)
|
| 248 |
+
logsumexp = part if logsumexp is None else torch.logaddexp(logsumexp, part)
|
| 249 |
+
mask = (target >= start) & (target < end)
|
| 250 |
+
picked = logits.gather(1, (target - start).clamp(0, end - start - 1).unsqueeze(1)).squeeze(1)
|
| 251 |
+
selected = torch.where(mask, picked, selected)
|
| 252 |
+
return (logsumexp - selected).mean()
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
def save_checkpoint(path: str | Path, model: nn.Module, model_config: dict) -> None:
|
| 256 |
+
if int(os.environ.get("RANK", "0")) != 0:
|
| 257 |
+
return
|
| 258 |
+
module = model.module if hasattr(model, "module") else model
|
| 259 |
+
destination = Path(path)
|
| 260 |
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
| 261 |
+
temporary = Path(f"{destination}.tmp")
|
| 262 |
+
torch.save({"model": module.state_dict(), "model_config": model_config,
|
| 263 |
+
"format_version": "metnet_2_v1"}, temporary)
|
| 264 |
+
os.replace(temporary, destination)
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def load_checkpoint(path: str | Path, model: nn.Module) -> dict:
|
| 268 |
+
checkpoint = torch.load(path, map_location="cpu", weights_only=True)
|
| 269 |
+
if set(checkpoint) != {"model", "model_config", "format_version"}:
|
| 270 |
+
raise ValueError("checkpoint must contain model, model_config, and format_version")
|
| 271 |
+
model.load_state_dict(checkpoint["model"])
|
| 272 |
+
return checkpoint
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
def scores(probabilities: np.ndarray, target: np.ndarray,
|
| 276 |
+
thresholds: tuple[float, ...] = (.2, 1., 2., 4., 8.)) -> dict:
|
| 277 |
+
if probabilities.shape[0] != 512 or target.shape != probabilities.shape[1:]:
|
| 278 |
+
raise ValueError("expected probabilities [512,H,W] and target [H,W]")
|
| 279 |
+
cdf = np.cumsum(probabilities.astype(np.float32), axis=0)
|
| 280 |
+
observed_cdf = (np.arange(512)[:, None, None] >= target[None]).astype(np.float32)
|
| 281 |
+
result = {"discrete_crps": float(np.mean(np.sum((cdf - observed_cdf) ** 2, axis=0)))}
|
| 282 |
+
brier, csi = {}, {}
|
| 283 |
+
for threshold in thresholds:
|
| 284 |
+
index = min(511, int(round(threshold / .2)))
|
| 285 |
+
event_probability = 1.0 - cdf[index - 1] if index else np.ones_like(cdf[0])
|
| 286 |
+
observed, forecast = target >= index, event_probability >= .5
|
| 287 |
+
hits = np.logical_and(forecast, observed).sum()
|
| 288 |
+
denominator = hits + np.logical_and(forecast, ~observed).sum() + np.logical_and(~forecast, observed).sum()
|
| 289 |
+
brier[str(threshold)] = float(np.mean((event_probability - observed) ** 2))
|
| 290 |
+
csi[str(threshold)] = float(hits / denominator) if denominator else 1.0
|
| 291 |
+
result.update(brier=brier, csi=csi)
|
| 292 |
+
return result
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
def write_json(path: str | Path, value: dict) -> None:
|
| 296 |
+
destination = Path(path)
|
| 297 |
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
| 298 |
+
destination.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8")
|
scripts/fake_data.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
import argparse
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import sys
|
| 5 |
+
|
| 6 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 7 |
+
from model.metnet_2 import load_config, write_fake_data
|
| 8 |
+
|
| 9 |
+
parser = argparse.ArgumentParser(description="Create selected MetNet-2 validation windows")
|
| 10 |
+
parser.add_argument("--config", default="conf/config.yaml")
|
| 11 |
+
parser.add_argument("--output", default=None)
|
| 12 |
+
args = parser.parse_args()
|
| 13 |
+
config = load_config(args.config)
|
| 14 |
+
output = args.output or config["data"]["path"]
|
| 15 |
+
print(write_fake_data(output, config["data"]["samples"], config["data"]["window"], config["data"]["halo"]))
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
import argparse
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
import torch
|
| 7 |
+
from model.metnet_2 import CLASS_RATES, ProceduralField, build_model, load_checkpoint, load_config
|
| 8 |
+
|
| 9 |
+
parser = argparse.ArgumentParser(description="Run selected-window or streamed full-domain inference")
|
| 10 |
+
parser.add_argument("--config", default="conf/config.yaml")
|
| 11 |
+
parser.add_argument("--lead", type=int, default=None)
|
| 12 |
+
parser.add_argument("--full", action="store_true")
|
| 13 |
+
parser.add_argument("--cdf", action="store_true")
|
| 14 |
+
args = parser.parse_args()
|
| 15 |
+
config = load_config(args.config)
|
| 16 |
+
torch.set_num_threads(config["runtime"]["num_threads"])
|
| 17 |
+
device = torch.device("cuda" if config["runtime"]["device"] == "auto" and torch.cuda.is_available()
|
| 18 |
+
else "cpu" if config["runtime"]["device"] == "auto" else config["runtime"]["device"])
|
| 19 |
+
model = build_model(config).to(device)
|
| 20 |
+
load_checkpoint(config["paths"]["checkpoint"], model)
|
| 21 |
+
field, lead = ProceduralField(2001), args.lead or config["inference"]["lead_minutes"]
|
| 22 |
+
if args.full:
|
| 23 |
+
output = Path(config["paths"]["predictions"]).with_suffix(".npy")
|
| 24 |
+
print(model.assemble_full(field, lead, output, config["data"]["window"], config["data"]["halo"],
|
| 25 |
+
config["training"]["class_chunk"], "cdf" if args.cdf else "probability", device))
|
| 26 |
+
else:
|
| 27 |
+
window = config["data"]["window"]
|
| 28 |
+
model.eval()
|
| 29 |
+
with torch.no_grad():
|
| 30 |
+
logits = model(field.window(0, 0, window, config["data"]["halo"]).unsqueeze(0).to(device),
|
| 31 |
+
torch.tensor([lead], device=device), window)[0]
|
| 32 |
+
probabilities = logits.softmax(0).cpu().numpy().astype(np.float32)
|
| 33 |
+
if not np.isfinite(probabilities).all():
|
| 34 |
+
raise FloatingPointError("inference probabilities are not finite")
|
| 35 |
+
output = Path(config["paths"]["predictions"])
|
| 36 |
+
output.parent.mkdir(parents=True, exist_ok=True)
|
| 37 |
+
np.savez_compressed(output, probabilities=probabilities, cdf=np.cumsum(probabilities, axis=0),
|
| 38 |
+
target=field.target_window(0, 0, window, lead).numpy(), rates=CLASS_RATES,
|
| 39 |
+
lead_minutes=np.int32(lead), coverage=np.array(config["inference"]["coverage"]),
|
| 40 |
+
is_complete=np.bool_(config["inference"]["is_complete"]))
|
| 41 |
+
print(output)
|
scripts/result.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
import argparse
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
import matplotlib.pyplot as plt
|
| 6 |
+
import numpy as np
|
| 7 |
+
from model.metnet_2 import load_config, scores, write_json
|
| 8 |
+
|
| 9 |
+
parser = argparse.ArgumentParser(description="Evaluate and visualize MetNet-2 predictions")
|
| 10 |
+
parser.add_argument("--config", default="conf/config.yaml")
|
| 11 |
+
args = parser.parse_args()
|
| 12 |
+
config = load_config(args.config)
|
| 13 |
+
with np.load(config["paths"]["predictions"]) as data:
|
| 14 |
+
probabilities, target, rates = data["probabilities"], data["target"], data["rates"]
|
| 15 |
+
metrics = {str(int(data["lead_minutes"])): scores(probabilities, target)}
|
| 16 |
+
if not all(np.isfinite(value) for value in [metrics[next(iter(metrics))]["discrete_crps"],
|
| 17 |
+
*metrics[next(iter(metrics))]["brier"].values(),
|
| 18 |
+
*metrics[next(iter(metrics))]["csi"].values()]):
|
| 19 |
+
raise FloatingPointError("evaluation metrics are not finite")
|
| 20 |
+
write_json(config["paths"]["evaluation_metrics"], metrics)
|
| 21 |
+
expected, truth = (probabilities * rates[:, None, None]).sum(0), rates[target]
|
| 22 |
+
figure, axes = plt.subplots(1, 3, figsize=(11, 3.5), constrained_layout=True)
|
| 23 |
+
for axis, image, title in zip(axes, (truth, expected, expected - truth), ("Target", "Expected rate", "Error")):
|
| 24 |
+
plot = axis.imshow(image, cmap="viridis")
|
| 25 |
+
axis.set_title(title)
|
| 26 |
+
axis.set_axis_off()
|
| 27 |
+
figure.colorbar(plot, ax=axis, shrink=.75)
|
| 28 |
+
comparison = Path(config["paths"]["comparison"])
|
| 29 |
+
comparison.parent.mkdir(parents=True, exist_ok=True)
|
| 30 |
+
figure.savefig(comparison, dpi=140)
|
| 31 |
+
plt.close(figure)
|
| 32 |
+
print(config["paths"]["evaluation_metrics"], comparison)
|
scripts/train.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
import argparse
|
| 3 |
+
import os
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn.functional as F
|
| 8 |
+
from torch.nn.parallel import DistributedDataParallel
|
| 9 |
+
from torch.utils.data import DataLoader, DistributedSampler
|
| 10 |
+
from model.metnet_2 import (WindowDataset, build_model, categorical_nll_chunked,
|
| 11 |
+
load_config, save_checkpoint, write_json)
|
| 12 |
+
|
| 13 |
+
parser = argparse.ArgumentParser(description="Train MetNet-2 on selected windows")
|
| 14 |
+
parser.add_argument("--config", default="conf/config.yaml")
|
| 15 |
+
parser.add_argument("--steps", type=int, default=None)
|
| 16 |
+
args = parser.parse_args()
|
| 17 |
+
config = load_config(args.config)
|
| 18 |
+
rank = int(os.environ.get("RANK", "0"))
|
| 19 |
+
world_size = int(os.environ.get("WORLD_SIZE", "1"))
|
| 20 |
+
local_rank = int(os.environ.get("LOCAL_RANK", "0"))
|
| 21 |
+
distributed = world_size > 1
|
| 22 |
+
requested = config["runtime"]["device"]
|
| 23 |
+
use_cuda = (requested != "cpu" and torch.cuda.is_available()
|
| 24 |
+
and (not distributed or torch.cuda.device_count() >= world_size))
|
| 25 |
+
if distributed:
|
| 26 |
+
torch.distributed.init_process_group(backend="nccl" if use_cuda else "gloo")
|
| 27 |
+
torch.manual_seed(config["seed"] + rank)
|
| 28 |
+
torch.set_num_threads(config["runtime"]["num_threads"])
|
| 29 |
+
device = torch.device(f"cuda:{local_rank}" if use_cuda else "cpu" if requested == "auto" else requested)
|
| 30 |
+
if use_cuda:
|
| 31 |
+
device = torch.device(f"cuda:{local_rank}")
|
| 32 |
+
torch.cuda.set_device(device)
|
| 33 |
+
model = build_model(config).to(device)
|
| 34 |
+
if distributed:
|
| 35 |
+
model = DistributedDataParallel(model, device_ids=[local_rank] if device.type == "cuda" else None)
|
| 36 |
+
optimizer = torch.optim.Adam(model.parameters(), lr=config["training"]["learning_rate"])
|
| 37 |
+
dataset = WindowDataset(config["data"]["path"])
|
| 38 |
+
sampler = DistributedSampler(dataset, shuffle=True, seed=config["seed"]) if distributed else None
|
| 39 |
+
loader = DataLoader(dataset, batch_size=config["training"]["batch_size"], shuffle=sampler is None, sampler=sampler)
|
| 40 |
+
steps = args.steps if args.steps is not None else config["training"]["steps"]
|
| 41 |
+
losses = []
|
| 42 |
+
model.train()
|
| 43 |
+
for step, (inputs, target, lead) in enumerate(loader):
|
| 44 |
+
if step >= steps:
|
| 45 |
+
break
|
| 46 |
+
optimizer.zero_grad(set_to_none=True)
|
| 47 |
+
logits = model(inputs.to(device), lead.to(device), config["data"]["window"])
|
| 48 |
+
loss = F.cross_entropy(logits, target.to(device))
|
| 49 |
+
if not torch.isfinite(loss):
|
| 50 |
+
raise FloatingPointError("training loss is not finite")
|
| 51 |
+
loss.backward()
|
| 52 |
+
optimizer.step()
|
| 53 |
+
losses.append(float(loss))
|
| 54 |
+
print(f"step={step} nll={losses[-1]:.6f}")
|
| 55 |
+
if not losses:
|
| 56 |
+
raise RuntimeError("training produced no optimization steps")
|
| 57 |
+
summary = torch.tensor([sum(losses), len(losses)], dtype=torch.float64, device=device)
|
| 58 |
+
if distributed:
|
| 59 |
+
torch.distributed.all_reduce(summary)
|
| 60 |
+
if rank == 0:
|
| 61 |
+
save_checkpoint(config["paths"]["checkpoint"], model, config["model"])
|
| 62 |
+
write_json(config["paths"]["training_metrics"],
|
| 63 |
+
{"steps": int(summary[1]), "mean_nll": float(summary[0] / summary[1]), "world_size": world_size})
|
| 64 |
+
if distributed:
|
| 65 |
+
torch.distributed.destroy_process_group()
|
weight/.gitkeep
ADDED
|
File without changes
|