Publish CRAI-ClimateExtremes reproduction
Browse files- .gitattributes +1 -34
- README.md +149 -0
- README_zh.md +150 -0
- conf/config.yaml +18 -0
- config.json +38 -0
- configuration.json +12 -0
- model/crai_climateextremes.py +93 -0
- scripts/fake_data.py +71 -0
- scripts/inference.py +58 -0
- scripts/result.py +81 -0
- scripts/train.py +113 -0
- weight/.gitkeep +0 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,2 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
tags:
|
| 6 |
+
- OneScience
|
| 7 |
+
- Earth Science
|
| 8 |
+
- Climate Extremes
|
| 9 |
+
- Spatial Reconstruction
|
| 10 |
+
frameworks: PyTorch
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
<p align="center">
|
| 14 |
+
<strong><span style="font-size: 30px;">CRAI-ClimateExtremes</span></strong>
|
| 15 |
+
</p>
|
| 16 |
+
|
| 17 |
+
# Model Introduction
|
| 18 |
+
|
| 19 |
+
CRAI-ClimateExtremes reconstructs missing regions from incomplete climate-extreme index fields and validity masks for historical-extreme analysis and sparse-observation reconstruction.
|
| 20 |
+
|
| 21 |
+
Paper: Artificial intelligence reveals past climate extremes by reconstructing historical records
|
| 22 |
+
https://doi.org/10.1038/s41467-024-53464-2
|
| 23 |
+
|
| 24 |
+
# Model Description
|
| 25 |
+
|
| 26 |
+
The method was proposed by research teams from the German Climate Computing Center, the Met Office, the University of Hamburg, and collaborating institutions. The paper uses 45 historical simulations from eight CMIP6 models, HadEX-CAM observations, and reanalysis products including ERA5. The model reconstructs monthly TX90p, TN90p, TX10p, and TN10p temperature-extreme indices.
|
| 27 |
+
|
| 28 |
+
# Use Cases
|
| 29 |
+
|
| 30 |
+
| Use Case | Description |
|
| 31 |
+
| :---: | :--- |
|
| 32 |
+
| Climate-extreme reconstruction | Reconstruct four monthly temperature-extreme indices from incomplete fields and validity masks. |
|
| 33 |
+
| Irregular-gap handling | Validate partial convolution, mask propagation, and missing-region loss. |
|
| 34 |
+
| Local engineering validation | Validate the end-to-end workflow with structured synthetic data on the complete `144×192` grid. |
|
| 35 |
+
| ModelScope/OneCode execution | Validate structured data, training, inference, climate-reconstruction 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/CRAI-ClimateExtremes --local-dir ./CRAI-ClimateExtremes
|
| 50 |
+
cd CRAI-ClimateExtremes
|
| 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 uses monthly CMIP6 extreme indices from 1901-2014 and applies HadEX-CAM missing-data masks to construct training samples. Inputs contain an index field and validity mask `[B,2,144,192]`, while targets are complete `[B,1,144,192]` index fields. This repository uses a small structured synthetic dataset for engineering validation only and does not represent the real CMIP6 or HadEX-CAM distributions, training scale, 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 configuration reduces samples, network width, epochs, and ensemble members without reducing the `144×192` grid or four-index protocol. Formal experiments require real CMIP6 and HadEX-CAM data and paper-scale training, with artifacts saved to:
|
| 102 |
+
|
| 103 |
+
```text
|
| 104 |
+
result/checkpoints/crai_climateextremes.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/crai_climateextremes.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 unified ensemble checkpoint, reconstructs missing extreme indices, and saves the member mean and spread. Complete numerical outputs are saved to:
|
| 119 |
+
|
| 120 |
+
```text
|
| 121 |
+
result/output/predictions.npz
|
| 122 |
+
result/output/metadata.json
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
### Evaluation and Visualization
|
| 126 |
+
|
| 127 |
+
```bash
|
| 128 |
+
python scripts/result.py
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
Evaluation computes missing-region RMSE, Spearman correlation, bias, and neighborhood spatial correlation and generates a target, input, and reconstruction comparison. Synthetic-data results validate engineering only and do not represent paper performance; outputs are saved to:
|
| 132 |
+
|
| 133 |
+
```text
|
| 134 |
+
result/evaluation/metrics.json
|
| 135 |
+
result/evaluation/comparison.png
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
# Official OneScience Information
|
| 139 |
+
|
| 140 |
+
| Platform | OneScience Main Repository | Skills Repository |
|
| 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 |
+
# Citation and License
|
| 146 |
+
|
| 147 |
+
This repository is an independent engineering reproduction of the public CRAI-ClimateExtremes specifications.
|
| 148 |
+
|
| 149 |
+
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,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
language:
|
| 4 |
+
- zh
|
| 5 |
+
- en
|
| 6 |
+
tags:
|
| 7 |
+
- OneScience
|
| 8 |
+
- 地球科学
|
| 9 |
+
- 气候极端
|
| 10 |
+
- 空间重建
|
| 11 |
+
frameworks: PyTorch
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
<p align="center">
|
| 15 |
+
<strong><span style="font-size: 30px;">CRAI-ClimateExtremes</span></strong>
|
| 16 |
+
</p>
|
| 17 |
+
|
| 18 |
+
# 模型介绍
|
| 19 |
+
|
| 20 |
+
CRAI-ClimateExtremes 根据不完整的气候极端指数场及其有效掩码重建缺测区域,主要用于历史气候极端事件分析和稀疏观测资料重建。
|
| 21 |
+
|
| 22 |
+
论文:Artificial intelligence reveals past climate extremes by reconstructing historical records
|
| 23 |
+
https://doi.org/10.1038/s41467-024-53464-2
|
| 24 |
+
|
| 25 |
+
# 模型描述
|
| 26 |
+
|
| 27 |
+
该方法由德国气候计算中心、英国气象局和汉堡大学等机构的研究团队提出。论文使用 8 个 CMIP6 模式的 45 个历史模拟、HadEX-CAM 观测资料以及 ERA5 等再分析数据。模型适用于重建 TX90p、TN90p、TX10p 和 TN10p 月尺度温度极端指数。
|
| 28 |
+
|
| 29 |
+
# 适用场景
|
| 30 |
+
|
| 31 |
+
| 场景 | 说明 |
|
| 32 |
+
| :---: | :--- |
|
| 33 |
+
| 气候极端指数重建 | 从不完整指数场和有效掩码重建四类月尺度温度极端指数。 |
|
| 34 |
+
| 不规则缺测处理 | 验证 partial convolution、掩码传播和缺失区域损失。 |
|
| 35 |
+
| 本地工程验证 | 使用完整 `144×192` 网格的结构化虚拟数据验证端到端流程。 |
|
| 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/CRAI-ClimateExtremes --local_dir ./CRAI-ClimateExtremes
|
| 51 |
+
cd CRAI-ClimateExtremes
|
| 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 |
+
论文使用 1901-2014 年 CMIP6 月尺度极端指数,并应用 HadEX-CAM 缺测掩码构造训练样本。输入为指数场与有效掩码 `[B,2,144,192]`,目标为 `[B,1,144,192]` 的完整指数场。本仓库使用少量结构化虚拟样本验证工程流程,不代表 CMIP6 或 HadEX-CAM 的真实数据分布、训练规模或论文性能。
|
| 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 |
+
默认配置仅缩小样本数、网络宽度、训练轮数和 ensemble 成员数,不缩小 `144×192` 空间网格和四类指数协议。正式实验需要真实 CMIP6 与 HadEX-CAM 数据和论文规模训练配置,训练产物保存到:
|
| 103 |
+
|
| 104 |
+
```text
|
| 105 |
+
result/checkpoints/crai_climateextremes.pt
|
| 106 |
+
result/training/metrics.json
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
### 训练权重
|
| 110 |
+
|
| 111 |
+
论文未提供可直接加载的官方模型权重,本仓库不在 `weight/` 中内置权重。本地训练生成的 checkpoint 保存到 `result/checkpoints/crai_climateextremes.pt`,不得描述为官方预训练权重。
|
| 112 |
+
|
| 113 |
+
### 推理
|
| 114 |
+
|
| 115 |
+
```bash
|
| 116 |
+
python scripts/inference.py
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
推理加载统一 ensemble checkpoint,重建缺测极端指数并保存成员均值和离散度。完整数值结果保存到:
|
| 120 |
+
|
| 121 |
+
```text
|
| 122 |
+
result/output/predictions.npz
|
| 123 |
+
result/output/metadata.json
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
### 评估和可视化
|
| 127 |
+
|
| 128 |
+
```bash
|
| 129 |
+
python scripts/result.py
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
评估计算缺失区域 RMSE、Spearman 相关系数、bias 和邻域空间相关,并生成目标、输入和重建结果对比图。虚拟数据结果仅用于工程验证,不代表论文正式性能;结果保存到:
|
| 133 |
+
|
| 134 |
+
```text
|
| 135 |
+
result/evaluation/metrics.json
|
| 136 |
+
result/evaluation/comparison.png
|
| 137 |
+
```
|
| 138 |
+
|
| 139 |
+
# OneScience官方信息
|
| 140 |
+
|
| 141 |
+
| 平台 | OneScience 主仓库 | Skills 仓库 |
|
| 142 |
+
| --- | --- | --- |
|
| 143 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 144 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 145 |
+
|
| 146 |
+
# 引用与许可证
|
| 147 |
+
|
| 148 |
+
本仓库为 CRAI-ClimateExtremes 公开规格的独立工程复现版本��
|
| 149 |
+
|
| 150 |
+
本仓库代码、官方模型权重和数据的使用仍应以各自项目中的许可证及使用条款为准。
|
conf/config.yaml
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
seed: 42
|
| 2 |
+
data_path: data/crai_fake.npz
|
| 3 |
+
height: 144
|
| 4 |
+
width: 192
|
| 5 |
+
base_channels: 8
|
| 6 |
+
batch_size: 2
|
| 7 |
+
epochs: 1
|
| 8 |
+
max_iterations: 8
|
| 9 |
+
learning_rate: 0.00005
|
| 10 |
+
ensemble_members: 2
|
| 11 |
+
paper_batch_size: 16
|
| 12 |
+
paper_iterations: 1000000
|
| 13 |
+
paper_ensemble_members: 20
|
| 14 |
+
paper_model: false
|
| 15 |
+
num_samples: 8
|
| 16 |
+
checkpoint_path: result/checkpoints/crai_climateextremes.pt
|
| 17 |
+
output_dir: result/output
|
| 18 |
+
evaluation_dir: result/evaluation
|
config.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_name": "CRAI-ClimateExtremes",
|
| 3 |
+
"model_type": "crai_climateextremes",
|
| 4 |
+
"architectures": ["CRAIClimateExtremes"],
|
| 5 |
+
"framework": "PyTorch",
|
| 6 |
+
"domain": "earth-science",
|
| 7 |
+
"task": "climate-extreme-index-reconstruction",
|
| 8 |
+
"implementation": {
|
| 9 |
+
"entry_point": "model/crai_climateextremes.py",
|
| 10 |
+
"scope": "core-method and full-grid reduced-sample 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 |
+
"input_shape": ["B", 2, 144, 192],
|
| 18 |
+
"output_shape": ["B", 1, 144, 192],
|
| 19 |
+
"core": "partial-convolution U-Net with mask propagation and skip connections",
|
| 20 |
+
"output_range_percent": [0, 100]
|
| 21 |
+
},
|
| 22 |
+
"data": {
|
| 23 |
+
"dataset": "HadEX-CAM and CMIP6-derived extreme indices",
|
| 24 |
+
"format_version": "1.0",
|
| 25 |
+
"resolution_degrees": [1.25, 1.875],
|
| 26 |
+
"indices": ["TX90p", "TN90p", "TX10p", "TN10p"],
|
| 27 |
+
"input_layout": "BCHW",
|
| 28 |
+
"synthetic": true
|
| 29 |
+
},
|
| 30 |
+
"configuration_sources": [
|
| 31 |
+
"conf/config.yaml",
|
| 32 |
+
"model/crai_climateextremes.py",
|
| 33 |
+
"scripts/fake_data.py",
|
| 34 |
+
"scripts/train.py",
|
| 35 |
+
"scripts/inference.py",
|
| 36 |
+
"scripts/result.py"
|
| 37 |
+
]
|
| 38 |
+
}
|
configuration.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"framework": "PyTorch",
|
| 3 |
+
"task": "climate extreme index reconstruction",
|
| 4 |
+
"model": "CRAI-ClimateExtremes",
|
| 5 |
+
"input_format": "BCHW: incomplete index field and validity mask [B,2,144,192]",
|
| 6 |
+
"protocol": "partial-convolution reconstruction of masked HadEX-resolution climate-extreme fields",
|
| 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/crai_climateextremes.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Independent PyTorch implementation of the CRAI reconstruction architecture."""
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
from torch import nn
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class PartialConv2d(nn.Module):
|
| 9 |
+
"""Convolution normalized by valid pixels, with the corresponding mask update."""
|
| 10 |
+
|
| 11 |
+
def __init__(self, in_channels, out_channels, kernel_size=3, stride=1):
|
| 12 |
+
super().__init__()
|
| 13 |
+
padding = kernel_size // 2
|
| 14 |
+
self.input_conv = nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding)
|
| 15 |
+
self.kernel_area = float(kernel_size * kernel_size)
|
| 16 |
+
self.stride = stride
|
| 17 |
+
self.padding = padding
|
| 18 |
+
self.kernel_size = kernel_size
|
| 19 |
+
self.register_buffer("mask_kernel", torch.ones(1, 1, kernel_size, kernel_size))
|
| 20 |
+
|
| 21 |
+
def forward(self, values, mask):
|
| 22 |
+
if mask.shape[1] != 1:
|
| 23 |
+
mask = mask.amax(dim=1, keepdim=True)
|
| 24 |
+
with torch.no_grad():
|
| 25 |
+
valid_count = F.conv2d(
|
| 26 |
+
mask, self.mask_kernel, stride=self.stride, padding=self.padding
|
| 27 |
+
)
|
| 28 |
+
new_mask = (valid_count > 0).to(values.dtype)
|
| 29 |
+
scale = self.kernel_area / valid_count.clamp_min(1.0)
|
| 30 |
+
raw = self.input_conv(values * mask)
|
| 31 |
+
if self.input_conv.bias is not None:
|
| 32 |
+
bias = self.input_conv.bias.view(1, -1, 1, 1)
|
| 33 |
+
raw = (raw - bias) * scale + bias
|
| 34 |
+
else:
|
| 35 |
+
raw = raw * scale
|
| 36 |
+
return raw * new_mask, new_mask
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class PConvBlock(nn.Module):
|
| 40 |
+
def __init__(self, in_channels, out_channels, stride=1):
|
| 41 |
+
super().__init__()
|
| 42 |
+
self.partial = PartialConv2d(in_channels, out_channels, 3, stride)
|
| 43 |
+
self.norm = nn.GroupNorm(1, out_channels)
|
| 44 |
+
|
| 45 |
+
def forward(self, values, mask):
|
| 46 |
+
values, mask = self.partial(values, mask)
|
| 47 |
+
return F.leaky_relu(self.norm(values), 0.2), mask
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class CRAIClimateExtremes(nn.Module):
|
| 51 |
+
"""Partial-convolution U-Net producing bounded extreme-index percentages."""
|
| 52 |
+
|
| 53 |
+
def __init__(self, base_channels=8):
|
| 54 |
+
super().__init__()
|
| 55 |
+
c = base_channels
|
| 56 |
+
self.enc1 = PConvBlock(1, c)
|
| 57 |
+
self.enc2 = PConvBlock(c, 2 * c, stride=2)
|
| 58 |
+
self.enc3 = PConvBlock(2 * c, 4 * c, stride=2)
|
| 59 |
+
self.bottleneck = PConvBlock(4 * c, 8 * c, stride=2)
|
| 60 |
+
self.dec3 = PConvBlock(8 * c + 4 * c, 4 * c)
|
| 61 |
+
self.dec2 = PConvBlock(4 * c + 2 * c, 2 * c)
|
| 62 |
+
self.dec1 = PConvBlock(2 * c + c, c)
|
| 63 |
+
self.output = nn.Conv2d(c, 1, 1)
|
| 64 |
+
|
| 65 |
+
@staticmethod
|
| 66 |
+
def _upsample(values, mask, size):
|
| 67 |
+
return (
|
| 68 |
+
F.interpolate(values, size=size, mode="bilinear", align_corners=False),
|
| 69 |
+
F.interpolate(mask, size=size, mode="nearest"),
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
def forward(self, inputs):
|
| 73 |
+
if inputs.ndim != 4 or inputs.shape[1] != 2:
|
| 74 |
+
raise ValueError("expected [B, 2, H, W] containing index and valid mask")
|
| 75 |
+
values = inputs[:, :1] / 100.0
|
| 76 |
+
mask = (inputs[:, 1:2] > 0.5).to(values.dtype)
|
| 77 |
+
e1, m1 = self.enc1(values, mask)
|
| 78 |
+
e2, m2 = self.enc2(e1, m1)
|
| 79 |
+
e3, m3 = self.enc3(e2, m2)
|
| 80 |
+
x, m = self.bottleneck(e3, m3)
|
| 81 |
+
x, m = self._upsample(x, m, e3.shape[-2:])
|
| 82 |
+
x, m = self.dec3(torch.cat((x, e3), 1), torch.maximum(m, m3))
|
| 83 |
+
x, m = self._upsample(x, m, e2.shape[-2:])
|
| 84 |
+
x, m = self.dec2(torch.cat((x, e2), 1), torch.maximum(m, m2))
|
| 85 |
+
x, m = self._upsample(x, m, e1.shape[-2:])
|
| 86 |
+
x, _ = self.dec1(torch.cat((x, e1), 1), torch.maximum(m, m1))
|
| 87 |
+
return 100.0 * torch.sigmoid(self.output(x))
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
if __name__ == "__main__":
|
| 91 |
+
model = CRAIClimateExtremes()
|
| 92 |
+
output = model(torch.rand(2, 2, 144, 192))
|
| 93 |
+
print(tuple(output.shape), float(output.min()), float(output.max()))
|
scripts/fake_data.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Create structurally realistic full-grid samples with irregular HadEX-style masks."""
|
| 2 |
+
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import json
|
| 5 |
+
import numpy as np
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
INDICES = np.array(["TX90p", "TN90p", "TX10p", "TN10p"])
|
| 9 |
+
H, W = 144, 192
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def europe_mask(lat, lon):
|
| 13 |
+
yy, xx = np.meshgrid(lat, lon, indexing="ij")
|
| 14 |
+
broad = (yy >= 30) & (yy <= 72) & (xx >= -25) & (xx <= 45)
|
| 15 |
+
# A coarse geographic silhouette keeps the scientific global-grid contract.
|
| 16 |
+
atlantic_cut = (xx < -10) & (yy < 44)
|
| 17 |
+
southeast_cut = (xx > 30) & (yy < 40)
|
| 18 |
+
north_cut = (yy > 68) & ((xx < 5) | (xx > 30))
|
| 19 |
+
return (broad & ~atlantic_cut & ~southeast_cut & ~north_cut).astype(np.float32)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def main():
|
| 23 |
+
rng = np.random.default_rng(42)
|
| 24 |
+
root = Path(__file__).resolve().parents[1]
|
| 25 |
+
output = root / "data"
|
| 26 |
+
output.mkdir(exist_ok=True)
|
| 27 |
+
lat = np.linspace(-89.375, 89.375, H, dtype=np.float32)
|
| 28 |
+
lon = np.linspace(-179.0625, 179.0625, W, dtype=np.float32)
|
| 29 |
+
land = europe_mask(lat, lon)
|
| 30 |
+
yy, xx = np.meshgrid(lat, lon, indexing="ij")
|
| 31 |
+
n = 8
|
| 32 |
+
target = np.zeros((n, 1, H, W), dtype=np.float32)
|
| 33 |
+
valid = np.zeros_like(target)
|
| 34 |
+
index_ids = np.arange(n, dtype=np.int64) % 4
|
| 35 |
+
for sample in range(n):
|
| 36 |
+
phase = 0.55 * sample
|
| 37 |
+
field = 50 + 21 * np.sin(np.deg2rad(2.3 * xx) + phase)
|
| 38 |
+
field += 16 * np.cos(np.deg2rad(3.2 * yy) - 0.4 * phase)
|
| 39 |
+
field += 5 * np.sin(np.deg2rad(xx + yy) * 4 + phase)
|
| 40 |
+
field += rng.normal(0, 1.2, (H, W))
|
| 41 |
+
if index_ids[sample] >= 2:
|
| 42 |
+
field = 100 - field
|
| 43 |
+
target[sample, 0] = np.clip(field, 0, 100) * land
|
| 44 |
+
observed = land.copy()
|
| 45 |
+
observed[rng.random((H, W)) < (0.35 + 0.04 * (sample % 3))] = 0
|
| 46 |
+
for _ in range(5):
|
| 47 |
+
cy, cx = rng.integers(45, 99), rng.integers(78, 121)
|
| 48 |
+
ry, rx = rng.integers(3, 11), rng.integers(4, 15)
|
| 49 |
+
hole = ((np.arange(H)[:, None] - cy) / ry) ** 2
|
| 50 |
+
hole = hole + ((np.arange(W)[None, :] - cx) / rx) ** 2
|
| 51 |
+
observed[hole < 1] = 0
|
| 52 |
+
valid[sample, 0] = observed
|
| 53 |
+
observed_values = target * valid
|
| 54 |
+
np.savez_compressed(
|
| 55 |
+
output / "crai_fake.npz", target=target, observed=observed_values,
|
| 56 |
+
valid_mask=valid, europe_mask=land, index_ids=index_ids,
|
| 57 |
+
index_names=INDICES, latitude=lat, longitude=lon,
|
| 58 |
+
)
|
| 59 |
+
metadata = {
|
| 60 |
+
"kind": "structured_synthetic",
|
| 61 |
+
"shape": [n, 1, H, W],
|
| 62 |
+
"grid_resolution": {"longitude_degrees": 1.875, "latitude_degrees": 1.25},
|
| 63 |
+
"indices": INDICES.tolist(),
|
| 64 |
+
"mask": "global grid with coarse Europe land support and irregular missing regions",
|
| 65 |
+
}
|
| 66 |
+
(output / "metadata.json").write_text(json.dumps(metadata, indent=2) + "\n")
|
| 67 |
+
print(f"wrote {output / 'crai_fake.npz'} with shape {target.shape}")
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
if __name__ == "__main__":
|
| 71 |
+
main()
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Run bounded ensemble reconstruction from a unified checkpoint."""
|
| 2 |
+
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import argparse
|
| 5 |
+
import json
|
| 6 |
+
import sys
|
| 7 |
+
import numpy as np
|
| 8 |
+
import torch
|
| 9 |
+
import yaml
|
| 10 |
+
|
| 11 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 12 |
+
sys.path.insert(0, str(ROOT))
|
| 13 |
+
|
| 14 |
+
from model.crai_climateextremes import CRAIClimateExtremes
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def main():
|
| 18 |
+
parser = argparse.ArgumentParser()
|
| 19 |
+
parser.add_argument("--config", type=Path, default=ROOT / "conf/config.yaml")
|
| 20 |
+
args = parser.parse_args()
|
| 21 |
+
config_path = args.config if args.config.is_absolute() else ROOT / args.config
|
| 22 |
+
with open(config_path, encoding="utf-8") as handle:
|
| 23 |
+
cfg = yaml.safe_load(handle)
|
| 24 |
+
data = np.load(ROOT / cfg["data_path"])
|
| 25 |
+
inputs = torch.from_numpy(np.concatenate((data["observed"], data["valid_mask"]), axis=1))
|
| 26 |
+
checkpoint_path = ROOT / cfg["checkpoint_path"]
|
| 27 |
+
if not checkpoint_path.is_file():
|
| 28 |
+
raise FileNotFoundError(f"checkpoint not found: {checkpoint_path}; run scripts/train.py first")
|
| 29 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 30 |
+
checkpoint = torch.load(checkpoint_path, map_location=device, weights_only=True)
|
| 31 |
+
if checkpoint.get("format_version") != "1.0" or not isinstance(checkpoint.get("model"), list):
|
| 32 |
+
raise ValueError(f"unsupported checkpoint format: {checkpoint_path}")
|
| 33 |
+
model_config = checkpoint.get("model_config", {})
|
| 34 |
+
predictions = []
|
| 35 |
+
for state in checkpoint["model"]:
|
| 36 |
+
model = CRAIClimateExtremes(**model_config).to(device)
|
| 37 |
+
model.load_state_dict(state); model.eval()
|
| 38 |
+
with torch.no_grad():
|
| 39 |
+
predictions.append(model(inputs.to(device)).cpu().numpy())
|
| 40 |
+
if not predictions:
|
| 41 |
+
raise ValueError(f"checkpoint contains no ensemble members: {checkpoint_path}")
|
| 42 |
+
members = np.stack(predictions)
|
| 43 |
+
output = ROOT / cfg["output_dir"]
|
| 44 |
+
output.mkdir(parents=True, exist_ok=True)
|
| 45 |
+
np.savez_compressed(
|
| 46 |
+
output / "predictions.npz", prediction=members.mean(0),
|
| 47 |
+
ensemble_std=members.std(0), target=data["target"],
|
| 48 |
+
observed=data["observed"], valid_mask=data["valid_mask"],
|
| 49 |
+
europe_mask=data["europe_mask"], index_ids=data["index_ids"],
|
| 50 |
+
index_names=data["index_names"],
|
| 51 |
+
)
|
| 52 |
+
metadata = {"ensemble_members": len(predictions), "checkpoint_semantics": "member state list in one checkpoint", "output_range": [0, 100]}
|
| 53 |
+
(output / "metadata.json").write_text(json.dumps(metadata, indent=2) + "\n")
|
| 54 |
+
print(f"predicted {inputs.shape[0]} samples with {len(predictions)} members")
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
if __name__ == "__main__":
|
| 58 |
+
main()
|
scripts/result.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Evaluate missing-region reconstruction and create a comparison figure."""
|
| 2 |
+
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import argparse
|
| 5 |
+
import json
|
| 6 |
+
import numpy as np
|
| 7 |
+
import torch
|
| 8 |
+
import torch.nn.functional as F
|
| 9 |
+
import yaml
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def correlation(a, b):
|
| 13 |
+
if a.size < 2 or np.std(a) == 0 or np.std(b) == 0:
|
| 14 |
+
return 0.0
|
| 15 |
+
return float(np.corrcoef(a, b)[0, 1])
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def rankdata(values):
|
| 19 |
+
order = np.argsort(values, kind="mergesort")
|
| 20 |
+
ranks = np.empty(len(values), dtype=np.float64)
|
| 21 |
+
ranks[order] = np.arange(len(values), dtype=np.float64)
|
| 22 |
+
unique, inverse, counts = np.unique(values, return_inverse=True, return_counts=True)
|
| 23 |
+
del unique
|
| 24 |
+
for group, count in enumerate(counts):
|
| 25 |
+
if count > 1:
|
| 26 |
+
positions = np.flatnonzero(inverse == group)
|
| 27 |
+
ranks[positions] = ranks[positions].mean()
|
| 28 |
+
return ranks
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def main():
|
| 32 |
+
parser = argparse.ArgumentParser()
|
| 33 |
+
root = Path(__file__).resolve().parents[1]
|
| 34 |
+
parser.add_argument("--config", type=Path, default=root / "conf/config.yaml")
|
| 35 |
+
args = parser.parse_args()
|
| 36 |
+
config_path = args.config if args.config.is_absolute() else root / args.config
|
| 37 |
+
with open(config_path, encoding="utf-8") as handle:
|
| 38 |
+
cfg = yaml.safe_load(handle)
|
| 39 |
+
archive = np.load(root / cfg["output_dir"] / "predictions.npz")
|
| 40 |
+
pred, target = archive["prediction"], archive["target"]
|
| 41 |
+
missing = archive["europe_mask"][None, None] * (1 - archive["valid_mask"])
|
| 42 |
+
selected = missing.astype(bool)
|
| 43 |
+
error = pred[selected] - target[selected]
|
| 44 |
+
sample_spearman = []
|
| 45 |
+
for i in range(len(pred)):
|
| 46 |
+
mask = selected[i, 0]
|
| 47 |
+
sample_spearman.append(correlation(rankdata(pred[i, 0][mask]), rankdata(target[i, 0][mask])))
|
| 48 |
+
kernel = torch.ones(1, 1, 3, 3) / 8
|
| 49 |
+
kernel[0, 0, 1, 1] = 0
|
| 50 |
+
pred_neighbor = F.conv2d(torch.from_numpy(pred), kernel, padding=1).numpy()
|
| 51 |
+
target_neighbor = F.conv2d(torch.from_numpy(target), kernel, padding=1).numpy()
|
| 52 |
+
metrics = {
|
| 53 |
+
"missing_rmse": float(np.sqrt(np.mean(error ** 2))),
|
| 54 |
+
"sample_spearman_mean": float(np.mean(sample_spearman)),
|
| 55 |
+
"sample_spearman": [float(x) for x in sample_spearman],
|
| 56 |
+
"missing_bias": float(np.mean(error)),
|
| 57 |
+
"neighborhood_spatial_correlation_prediction": correlation(pred[selected], pred_neighbor[selected]),
|
| 58 |
+
"neighborhood_spatial_correlation_target": correlation(target[selected], target_neighbor[selected]),
|
| 59 |
+
"missing_points": int(selected.sum()),
|
| 60 |
+
}
|
| 61 |
+
output = root / cfg["evaluation_dir"]
|
| 62 |
+
output.mkdir(parents=True, exist_ok=True)
|
| 63 |
+
(output / "metrics.json").write_text(json.dumps(metrics, indent=2) + "\n")
|
| 64 |
+
import matplotlib
|
| 65 |
+
matplotlib.use("Agg")
|
| 66 |
+
import matplotlib.pyplot as plt
|
| 67 |
+
fig, axes = plt.subplots(1, 3, figsize=(13, 4), constrained_layout=True)
|
| 68 |
+
sample = 0
|
| 69 |
+
fields = [archive["observed"][sample, 0], target[sample, 0], pred[sample, 0]]
|
| 70 |
+
titles = ["Irregular observations", "Synthetic truth", "CRAI reconstruction"]
|
| 71 |
+
for axis, field, title in zip(axes, fields, titles):
|
| 72 |
+
image = axis.imshow(np.where(archive["europe_mask"] > 0, field, np.nan), origin="lower", vmin=0, vmax=100, cmap="RdYlBu_r")
|
| 73 |
+
axis.set_title(title); axis.set_axis_off()
|
| 74 |
+
fig.colorbar(image, ax=axes, label="Extreme index (%)", shrink=0.8)
|
| 75 |
+
fig.suptitle(f"Missing RMSE={metrics['missing_rmse']:.3f} | Spearman={metrics['sample_spearman_mean']:.3f} | Bias={metrics['missing_bias']:.3f}")
|
| 76 |
+
fig.savefig(output / "comparison.png", dpi=160); plt.close(fig)
|
| 77 |
+
print(json.dumps(metrics))
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
if __name__ == "__main__":
|
| 81 |
+
main()
|
scripts/train.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Train independent CRAI ensemble members, optionally under torchrun DDP."""
|
| 2 |
+
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import argparse
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
import random
|
| 8 |
+
import sys
|
| 9 |
+
import numpy as np
|
| 10 |
+
import torch
|
| 11 |
+
from torch import distributed as dist
|
| 12 |
+
from torch.nn.parallel import DistributedDataParallel
|
| 13 |
+
from torch.utils.data import DataLoader, Dataset, DistributedSampler
|
| 14 |
+
import yaml
|
| 15 |
+
|
| 16 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 17 |
+
sys.path.insert(0, str(ROOT))
|
| 18 |
+
|
| 19 |
+
from model.crai_climateextremes import CRAIClimateExtremes
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class ClimateDataset(Dataset):
|
| 23 |
+
def __init__(self, archive, limit):
|
| 24 |
+
self.observed = torch.from_numpy(archive["observed"][:limit])
|
| 25 |
+
self.valid = torch.from_numpy(archive["valid_mask"][:limit])
|
| 26 |
+
self.target = torch.from_numpy(archive["target"][:limit])
|
| 27 |
+
land = torch.from_numpy(archive["europe_mask"])[None, None]
|
| 28 |
+
self.missing = land * (1.0 - self.valid)
|
| 29 |
+
|
| 30 |
+
def __len__(self):
|
| 31 |
+
return len(self.target)
|
| 32 |
+
|
| 33 |
+
def __getitem__(self, index):
|
| 34 |
+
return torch.cat((self.observed[index], self.valid[index]), 0), self.target[index], self.missing[index]
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def load_config(path):
|
| 38 |
+
with open(path, encoding="utf-8") as handle:
|
| 39 |
+
return yaml.safe_load(handle)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def main():
|
| 43 |
+
parser = argparse.ArgumentParser()
|
| 44 |
+
parser.add_argument("--config", type=Path, default=ROOT / "conf/config.yaml")
|
| 45 |
+
parser.add_argument("--paper-model", action="store_true")
|
| 46 |
+
args = parser.parse_args()
|
| 47 |
+
config_path = args.config if args.config.is_absolute() else ROOT / args.config
|
| 48 |
+
cfg = load_config(config_path)
|
| 49 |
+
use_paper = args.paper_model or cfg["paper_model"]
|
| 50 |
+
batch_size = cfg["paper_batch_size"] if use_paper else cfg["batch_size"]
|
| 51 |
+
iterations = cfg["paper_iterations"] if use_paper else cfg["max_iterations"]
|
| 52 |
+
members = cfg["paper_ensemble_members"] if use_paper else cfg["ensemble_members"]
|
| 53 |
+
rank, world = int(os.getenv("RANK", 0)), int(os.getenv("WORLD_SIZE", 1))
|
| 54 |
+
if world > 1:
|
| 55 |
+
dist.init_process_group("nccl" if torch.cuda.is_available() else "gloo")
|
| 56 |
+
device = torch.device(f"cuda:{int(os.getenv('LOCAL_RANK', 0))}" if torch.cuda.is_available() else "cpu")
|
| 57 |
+
if device.type == "cuda":
|
| 58 |
+
torch.cuda.set_device(device)
|
| 59 |
+
archive = np.load(ROOT / cfg["data_path"])
|
| 60 |
+
dataset = ClimateDataset(archive, cfg["num_samples"])
|
| 61 |
+
checkpoint_path = ROOT / cfg["checkpoint_path"]
|
| 62 |
+
if rank == 0:
|
| 63 |
+
checkpoint_path.parent.mkdir(parents=True, exist_ok=True)
|
| 64 |
+
(ROOT / "result/training").mkdir(parents=True, exist_ok=True)
|
| 65 |
+
if world > 1:
|
| 66 |
+
dist.barrier()
|
| 67 |
+
records, member_states = [], []
|
| 68 |
+
for member in range(members):
|
| 69 |
+
seed = int(cfg["seed"]) + member
|
| 70 |
+
random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
|
| 71 |
+
sampler = DistributedSampler(dataset, shuffle=True, seed=seed) if world > 1 else None
|
| 72 |
+
loader = DataLoader(dataset, batch_size=batch_size, shuffle=sampler is None, sampler=sampler)
|
| 73 |
+
model = CRAIClimateExtremes(cfg["base_channels"]).to(device)
|
| 74 |
+
if world > 1:
|
| 75 |
+
model = DistributedDataParallel(model, device_ids=[device.index] if device.type == "cuda" else None)
|
| 76 |
+
optimizer = torch.optim.Adam(model.parameters(), lr=cfg["learning_rate"])
|
| 77 |
+
step, losses = 0, []
|
| 78 |
+
for epoch in range(cfg["epochs"] if not use_paper else 10**9):
|
| 79 |
+
if sampler is not None:
|
| 80 |
+
sampler.set_epoch(epoch)
|
| 81 |
+
for inputs, target, missing in loader:
|
| 82 |
+
inputs, target, missing = inputs.to(device), target.to(device), missing.to(device)
|
| 83 |
+
prediction = model(inputs)
|
| 84 |
+
loss = (torch.abs(prediction - target) * missing).sum() / missing.sum().clamp_min(1)
|
| 85 |
+
optimizer.zero_grad(); loss.backward(); optimizer.step()
|
| 86 |
+
losses.append(float(loss.detach()))
|
| 87 |
+
step += 1
|
| 88 |
+
if step >= iterations:
|
| 89 |
+
break
|
| 90 |
+
if step >= iterations:
|
| 91 |
+
break
|
| 92 |
+
raw_model = model.module if isinstance(model, DistributedDataParallel) else model
|
| 93 |
+
if rank == 0:
|
| 94 |
+
member_states.append({key: value.detach().cpu() for key, value in raw_model.state_dict().items()})
|
| 95 |
+
records.append({"member": member, "iterations": step, "final_missing_mae": losses[-1]})
|
| 96 |
+
if rank == 0:
|
| 97 |
+
torch.save(
|
| 98 |
+
{
|
| 99 |
+
"format_version": "1.0",
|
| 100 |
+
"model_config": {"base_channels": cfg["base_channels"]},
|
| 101 |
+
"model": member_states,
|
| 102 |
+
},
|
| 103 |
+
checkpoint_path,
|
| 104 |
+
)
|
| 105 |
+
payload = {"paper_model": bool(use_paper), "world_size": world, "members": records}
|
| 106 |
+
(ROOT / "result/training/metrics.json").write_text(json.dumps(payload, indent=2) + "\n")
|
| 107 |
+
print(json.dumps(payload))
|
| 108 |
+
if world > 1:
|
| 109 |
+
dist.destroy_process_group()
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
if __name__ == "__main__":
|
| 113 |
+
main()
|
weight/.gitkeep
ADDED
|
File without changes
|