Add engineering reproduction package
Browse files- .gitattributes +3 -32
- README.md +148 -0
- README_zh.md +151 -0
- conf/config.yaml +45 -0
- config.json +42 -0
- configuration.json +12 -0
- model/convlstm.py +95 -0
- scripts/fake_data.py +50 -0
- scripts/inference.py +46 -0
- scripts/result.py +78 -0
- scripts/train.py +95 -0
- weight/.gitkeep +0 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,6 @@
|
|
| 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 |
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.
|
| 25 |
-
*.
|
| 26 |
-
|
| 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 |
*.h5 filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 3 |
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.npz 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 Nowcasting
|
| 9 |
+
- Spatiotemporal Sequence Prediction
|
| 10 |
+
frameworks: PyTorch
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
<p align="center">
|
| 14 |
+
<strong>
|
| 15 |
+
<span style="font-size: 30px;">ConvLSTM</span>
|
| 16 |
+
</strong>
|
| 17 |
+
</p>
|
| 18 |
+
|
| 19 |
+
# Model Introduction
|
| 20 |
+
|
| 21 |
+
ConvLSTM is a convolutional recurrent neural network for spatiotemporal sequence prediction. It replaces the input-to-state and state-to-state transformations of LSTM with spatial convolutions, thereby modeling local spatial correlations while preserving long-term temporal dependencies.
|
| 22 |
+
|
| 23 |
+
Paper: Convolutional LSTM Network: A Machine Learning Approach for Precipitation Nowcasting
|
| 24 |
+
https://papers.nips.cc/paper_files/paper/2015/hash/07563a3fe3bbe7e3ba84431ad9d055af-Abstract.html
|
| 25 |
+
|
| 26 |
+
# Model Description
|
| 27 |
+
|
| 28 |
+
ConvLSTM was proposed by researchers at the Hong Kong University of Science and Technology and the Hong Kong Observatory. The model is trained and evaluated using the 97 days with the highest rainfall in the 2011 to 2013 Hong Kong weather radar data, as well as synthetic Moving-MNIST sequences. The model is suitable for precipitation nowcasting, video prediction, and general spatiotemporal sequence prediction tasks.
|
| 29 |
+
|
| 30 |
+
# Use Cases
|
| 31 |
+
|
| 32 |
+
| Scenario | Description |
|
| 33 |
+
| :---: | :--- |
|
| 34 |
+
| Precipitation nowcasting | Predict the next 15 time steps from 5 historical radar maps. |
|
| 35 |
+
| Spatiotemporal sequence modeling | Jointly learn spatial and temporal correlations with a convolutional gating structure. |
|
| 36 |
+
| Multi-step image prediction | Continuously generate future images through an Encoder-Forecaster structure. |
|
| 37 |
+
| Local workflow validation | Use synthetic radar sequences to check training, inference, evaluation, visualization, and checkpoint workflows. |
|
| 38 |
+
| Multi-GPU training | Launch distributed data-parallel training with `torchrun`. |
|
| 39 |
+
|
| 40 |
+
# Usage Instructions
|
| 41 |
+
|
| 42 |
+
## 1. OneCode
|
| 43 |
+
|
| 44 |
+
Experience intelligent, one-click AI4S programming through the OneCode online environment:
|
| 45 |
+
|
| 46 |
+
[Try intelligent, one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 47 |
+
|
| 48 |
+
## 2. Download and Installation
|
| 49 |
+
|
| 50 |
+
```bash
|
| 51 |
+
hf download OneScience-Group/ConvLSTM --local-dir ./ConvLSTM
|
| 52 |
+
cd ConvLSTM
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
### Environment Dependencies
|
| 56 |
+
|
| 57 |
+
**Hardware Requirements**
|
| 58 |
+
|
| 59 |
+
- A GPU or DCU is recommended.
|
| 60 |
+
- A CPU can be used for connectivity validation with the default small-sample configuration.
|
| 61 |
+
- DCU users must install DTK in advance. DTK 25.04.2 or later, or the OneScience-recommended version matching the current cluster, is recommended.
|
| 62 |
+
|
| 63 |
+
**DCU Environment**
|
| 64 |
+
|
| 65 |
+
```bash
|
| 66 |
+
# Activate DTK and Conda first
|
| 67 |
+
conda create -n onescience311 python=3.11 -y
|
| 68 |
+
conda activate onescience311
|
| 69 |
+
pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
**GPU Environment**
|
| 73 |
+
|
| 74 |
+
```bash
|
| 75 |
+
# Activate Conda first
|
| 76 |
+
conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
|
| 77 |
+
conda activate onescience311
|
| 78 |
+
pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
### Training Data
|
| 82 |
+
|
| 83 |
+
This repository uses a small number of synthetic samples to validate the engineering workflow. The synthetic data contains 20 consecutive single-channel `float32` radar echo images of `100×100` pixels at 6-minute intervals. The first 5 frames are used as input and the following 15 frames as prediction targets. This data is only used to validate ConvLSTM's spatiotemporal encoding, 15-step prediction, training, inference, and evaluation workflows; it does not represent the official radar data distribution or training scale.
|
| 84 |
+
|
| 85 |
+
```bash
|
| 86 |
+
python scripts/fake_data.py
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
### Training
|
| 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 retains two Encoder layers, two Forecaster layers, `3×3` convolutions, channel-wise peephole connections, and the full prediction length; it only reduces the number of samples, hidden channels, and training epochs.
|
| 102 |
+
|
| 103 |
+
```text
|
| 104 |
+
result/checkpoints/convlstm.pt
|
| 105 |
+
result/training/metrics.json
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
### Training Weights
|
| 109 |
+
|
| 110 |
+
This repository does not include synthetic or trained weights, and the paper does not provide a directly downloadable original Theano pretrained checkpoint.
|
| 111 |
+
|
| 112 |
+
### Inference
|
| 113 |
+
|
| 114 |
+
```bash
|
| 115 |
+
python scripts/inference.py
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
Inference loads the training checkpoint and generates 15 future radar echo predictions from 5 historical radar maps. The inference results contain the input sequence, ground-truth target, predicted sequence, and corresponding lead-time information in minutes.
|
| 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 |
+
Following the paper, the evaluation converts radar echoes to rainfall rates using the Z-R relationship and computes Rainfall-MSE, CSI, FAR, POD, and Correlation. The results include per-step metrics for all 15 forecast lead times and overall summary metrics, and generate comparison plots of targets, predictions, and absolute errors for selected lead times. Results on synthetic data are only for validating the engineering workflow and do not represent metrics on the paper's actual radar data.
|
| 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 publicly available specifications in the ConvLSTM paper.
|
| 147 |
+
|
| 148 |
+
Use of the code and data in this repository remains subject to the licenses and terms of use of their respective projects.
|
README_zh.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: Apache-2.0
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
- zh
|
| 6 |
+
tags:
|
| 7 |
+
- OneScience
|
| 8 |
+
- 地球科学
|
| 9 |
+
- 降水临近预报
|
| 10 |
+
- 时空序列预测
|
| 11 |
+
frameworks: PyTorch
|
| 12 |
+
datasets:
|
| 13 |
+
- Hong Kong weather radar echo dataset
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
<p align="center">
|
| 17 |
+
<strong>
|
| 18 |
+
<span style="font-size: 30px;">ConvLSTM</span>
|
| 19 |
+
</strong>
|
| 20 |
+
</p>
|
| 21 |
+
|
| 22 |
+
# 模型介绍
|
| 23 |
+
|
| 24 |
+
ConvLSTM 是面向时空序列预测的卷积循环神经网络,将 LSTM 的输入到状态和状态到状态变换替换为空间卷积,从而在保留长期时间依赖的同时建模局部空间相关性。
|
| 25 |
+
|
| 26 |
+
论文:Convolutional LSTM Network: A Machine Learning Approach for Precipitation Nowcasting
|
| 27 |
+
https://papers.nips.cc/paper_files/paper/2015/hash/07563a3fe3bbe7e3ba84431ad9d055af-Abstract.html
|
| 28 |
+
|
| 29 |
+
# 模型描述
|
| 30 |
+
|
| 31 |
+
ConvLSTM 由香港科技大学与香港天文台的研究人员提出。模型使用 2011 至 2013 年香港天气雷达数据中降雨量最高的 97 天,以及 Moving-MNIST 合成序列进行训练和评估。模型适用于降水临近预报、视频预测和一般时空序列预测任务。
|
| 32 |
+
|
| 33 |
+
# 适用场景
|
| 34 |
+
|
| 35 |
+
| 场景 | 说明 |
|
| 36 |
+
| :---: | :--- |
|
| 37 |
+
| 降水临近预报 | 根据 5 张历史雷达图预测未来 15 个时间步。 |
|
| 38 |
+
| 时空序列建模 | 使用卷积门控结构联合学习空间和时间相关性。 |
|
| 39 |
+
| 多步图像预测 | 通过 Encoder-Forecaster 结构连续生成未来图像。 |
|
| 40 |
+
| 本地工程验证 | 使用虚拟雷达序列检查训练、推理、评估、可视化和 checkpoint 流程。 |
|
| 41 |
+
| 多卡训练 | 通过 `torchrun` 启动分布式数据并行训练。 |
|
| 42 |
+
|
| 43 |
+
# 使用说明
|
| 44 |
+
|
| 45 |
+
## 1.OneCode
|
| 46 |
+
|
| 47 |
+
可通过 OneCode 在线环境体验智能化一键式 AI4S 编程:
|
| 48 |
+
|
| 49 |
+
[点击体验智能化一键式 AI4S 编程](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 50 |
+
|
| 51 |
+
## 2.下载安装
|
| 52 |
+
|
| 53 |
+
```bash
|
| 54 |
+
modelscope download --model OneScience/ConvLSTM --local_dir ./ConvLSTM
|
| 55 |
+
cd ConvLSTM
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
### 环境依赖
|
| 59 |
+
|
| 60 |
+
**硬件要求**
|
| 61 |
+
|
| 62 |
+
- 推荐使用 GPU 或 DCU 运行。
|
| 63 |
+
- CPU 可用于默认小样本配置的连通性验证。
|
| 64 |
+
- DCU 用户需预先安装 DTK,建议使用 DTK 25.04.2 以上版本或与当前集群匹配的 OneScience 推荐版本。
|
| 65 |
+
|
| 66 |
+
**DCU环境**
|
| 67 |
+
|
| 68 |
+
```bash
|
| 69 |
+
# 请首先激活DTK及CONDA
|
| 70 |
+
conda create -n onescience311 python=3.11 -y
|
| 71 |
+
conda activate onescience311
|
| 72 |
+
pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
**GPU环境**
|
| 76 |
+
|
| 77 |
+
```bash
|
| 78 |
+
# 请首先激活CONDA
|
| 79 |
+
conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
|
| 80 |
+
conda activate onescience311
|
| 81 |
+
pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
### 训练数据介绍
|
| 85 |
+
|
| 86 |
+
本仓库使用少量虚拟样本验证工程流程,虚拟数据包含连续 20 帧、时间间隔为 6 分钟的 `100×100` 单通道 `float32` 雷达回波图,其中前 5 帧作为输入,后 15 帧作为预测目标。该数据仅用于验证 ConvLSTM 的时空编码、15 步预测、训练、推理和评估流程,不代表官方雷达数据分布与训练规模。
|
| 87 |
+
|
| 88 |
+
```bash
|
| 89 |
+
python scripts/fake_data.py
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
### 训练
|
| 93 |
+
|
| 94 |
+
```bash
|
| 95 |
+
python scripts/train.py
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
多卡训练可使用:
|
| 99 |
+
|
| 100 |
+
```bash
|
| 101 |
+
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
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
默认配置保持两层 Encoder、两层 Forecaster、`3×3` 卷积、逐通道 peephole 和完整预测长度,只缩小样本数量、隐藏通道和训练周期。
|
| 105 |
+
|
| 106 |
+
```text
|
| 107 |
+
result/checkpoints/convlstm.pt
|
| 108 |
+
result/training/metrics.json
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
### 训练权重
|
| 112 |
+
|
| 113 |
+
本仓库不内置虚拟权重或训练权重,论文未提供可直接下载的原始 Theano 预训练 checkpoint。
|
| 114 |
+
|
| 115 |
+
### 推理
|
| 116 |
+
|
| 117 |
+
```bash
|
| 118 |
+
python scripts/inference.py
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
推理加载训练 checkpoint,根据 5 张历史雷达图生成未来 15 张雷达回波预测。推理结果包含输入序列、真实目标、预测序列及对应的分钟时效信息。
|
| 122 |
+
|
| 123 |
+
```text
|
| 124 |
+
result/output/predictions.npz
|
| 125 |
+
```
|
| 126 |
+
|
| 127 |
+
### 评估和可视化
|
| 128 |
+
|
| 129 |
+
```bash
|
| 130 |
+
python scripts/result.py
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
评估按照论文的 Z-R 关系将雷达回波转换为降雨率,并计算 Rainfall-MSE、CSI、FAR、POD 和 Correlation。结果同时包含 15 个预测时效的分步指标及整体汇总指标,并生成部分时效的目标、预测和绝对误差对比图。虚拟数据结果仅用于验证工程流程,不代表论文真实雷达数据指标。
|
| 134 |
+
|
| 135 |
+
```text
|
| 136 |
+
result/evaluation/metrics.json
|
| 137 |
+
result/evaluation/comparison.png
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
# OneScience官方信息
|
| 141 |
+
|
| 142 |
+
| 平台 | OneScience 主仓库 | Skills 仓库 |
|
| 143 |
+
| --- | --- | --- |
|
| 144 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 145 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 146 |
+
|
| 147 |
+
# 引用与许可证
|
| 148 |
+
|
| 149 |
+
本仓库为 ConvLSTM 论文公开规格的独立工程复现版本。
|
| 150 |
+
|
| 151 |
+
本仓库代码和数据的使用仍应以各自项目中的许可证及使用条款为准。
|
conf/config.yaml
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
seed: 42
|
| 2 |
+
data:
|
| 3 |
+
root: data
|
| 4 |
+
format_version: convlstm_hko_synthetic_engineering_v1
|
| 5 |
+
train_samples: 4
|
| 6 |
+
test_samples: 2
|
| 7 |
+
input_frames: 5
|
| 8 |
+
output_frames: 15
|
| 9 |
+
channels: 1
|
| 10 |
+
height: 100
|
| 11 |
+
width: 100
|
| 12 |
+
interval_minutes: 6
|
| 13 |
+
patch_size: 2
|
| 14 |
+
rainfall_threshold_mm_h: 0.5
|
| 15 |
+
zr_a: 118.239
|
| 16 |
+
zr_b: 1.5241
|
| 17 |
+
radar_db_min: 0.0
|
| 18 |
+
radar_db_max: 70.0
|
| 19 |
+
model:
|
| 20 |
+
input_channels: 1
|
| 21 |
+
patch_size: 2
|
| 22 |
+
hidden_channels: [16, 16]
|
| 23 |
+
kernel_size: 3
|
| 24 |
+
output_frames: 15
|
| 25 |
+
paper_model:
|
| 26 |
+
input_channels: 1
|
| 27 |
+
patch_size: 2
|
| 28 |
+
hidden_channels: [64, 64]
|
| 29 |
+
kernel_size: 3
|
| 30 |
+
output_frames: 15
|
| 31 |
+
train:
|
| 32 |
+
epochs: 1
|
| 33 |
+
batch_size: 1
|
| 34 |
+
learning_rate: 0.001
|
| 35 |
+
rmsprop_alpha: 0.9
|
| 36 |
+
weight_decay: 0.0
|
| 37 |
+
gradient_clip_norm: 10.0
|
| 38 |
+
num_workers: 0
|
| 39 |
+
runtime:
|
| 40 |
+
device: auto
|
| 41 |
+
paths:
|
| 42 |
+
checkpoint: result/checkpoints/convlstm.pt
|
| 43 |
+
training_metrics: result/training/metrics.json
|
| 44 |
+
inference_dir: result/output
|
| 45 |
+
evaluation_dir: result/evaluation
|
config.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_name": "ConvLSTM",
|
| 3 |
+
"model_type": "convlstm",
|
| 4 |
+
"architectures": ["ConvLSTM", "ConvLSTMCell"],
|
| 5 |
+
"framework": "PyTorch",
|
| 6 |
+
"domain": "atmosphere",
|
| 7 |
+
"task": "precipitation-nowcasting",
|
| 8 |
+
"implementation": {
|
| 9 |
+
"entry_point": "model/convlstm.py",
|
| 10 |
+
"scope": "peephole ConvLSTM encoder-forecaster for fifteen-step radar echo prediction",
|
| 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 |
+
"family": "two-layer ConvLSTM encoder and two-layer unconditional forecaster",
|
| 18 |
+
"input_channels": 1,
|
| 19 |
+
"patch_size": 2,
|
| 20 |
+
"patched_channels": 4,
|
| 21 |
+
"hidden_channels": [16, 16],
|
| 22 |
+
"kernel_size": 3,
|
| 23 |
+
"peephole": true,
|
| 24 |
+
"output_frames": 15,
|
| 25 |
+
"output_head": "1x1 convolution over concatenated forecaster states"
|
| 26 |
+
},
|
| 27 |
+
"data": {
|
| 28 |
+
"datasets": ["Hong Kong weather radar echo dataset"],
|
| 29 |
+
"protocol": "convlstm_hko_synthetic_engineering_v1",
|
| 30 |
+
"format": "NPZ",
|
| 31 |
+
"train_file": "data/train.npz",
|
| 32 |
+
"test_file": "data/test.npz",
|
| 33 |
+
"input_shape": ["N", 5, 1, 100, 100],
|
| 34 |
+
"target_shape": ["N", 15, 1, 100, 100],
|
| 35 |
+
"interval_minutes": 6,
|
| 36 |
+
"forecast_horizon_minutes": 90,
|
| 37 |
+
"rainfall_threshold_mm_h": 0.5,
|
| 38 |
+
"zr_parameters": {"a": 118.239, "b": 1.5241},
|
| 39 |
+
"required_metadata": ["format_version", "data_source"]
|
| 40 |
+
},
|
| 41 |
+
"configuration_sources": ["conf/config.yaml", "model/convlstm.py", "scripts/fake_data.py", "scripts/train.py", "scripts/inference.py", "scripts/result.py"]
|
| 42 |
+
}
|
configuration.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"framework": "PyTorch",
|
| 3 |
+
"task": "precipitation_nowcasting",
|
| 4 |
+
"model": "ConvLSTM",
|
| 5 |
+
"input_format": "BTCHW",
|
| 6 |
+
"protocol": "convlstm_encoder_forecaster",
|
| 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/convlstm.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Peephole ConvLSTM encoder-forecaster for precipitation nowcasting."""
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
from torch import nn
|
| 5 |
+
from torch.nn import functional as F
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def patchify(sequence, patch_size):
|
| 9 |
+
batch, steps, channels, height, width = sequence.shape
|
| 10 |
+
flattened = sequence.flatten(0, 1)
|
| 11 |
+
patched = F.pixel_unshuffle(flattened, patch_size)
|
| 12 |
+
return patched.unflatten(0, (batch, steps))
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def unpatchify(sequence, patch_size):
|
| 16 |
+
batch, steps = sequence.shape[:2]
|
| 17 |
+
images = F.pixel_shuffle(sequence.flatten(0, 1), patch_size)
|
| 18 |
+
return images.unflatten(0, (batch, steps))
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class ConvLSTMCell(nn.Module):
|
| 22 |
+
def __init__(self, input_channels, hidden_channels, kernel_size):
|
| 23 |
+
super().__init__()
|
| 24 |
+
padding = kernel_size // 2
|
| 25 |
+
self.hidden_channels = hidden_channels
|
| 26 |
+
self.input_conv = None if input_channels == 0 else nn.Conv2d(
|
| 27 |
+
input_channels, 4 * hidden_channels, kernel_size, padding=padding
|
| 28 |
+
)
|
| 29 |
+
self.hidden_conv = nn.Conv2d(hidden_channels, 4 * hidden_channels, kernel_size,
|
| 30 |
+
padding=padding, bias=False)
|
| 31 |
+
self.peephole_input = nn.Parameter(torch.zeros(1, hidden_channels, 1, 1))
|
| 32 |
+
self.peephole_forget = nn.Parameter(torch.zeros(1, hidden_channels, 1, 1))
|
| 33 |
+
self.peephole_output = nn.Parameter(torch.zeros(1, hidden_channels, 1, 1))
|
| 34 |
+
self.bias = nn.Parameter(torch.zeros(1, 4 * hidden_channels, 1, 1))
|
| 35 |
+
|
| 36 |
+
def forward(self, values, state):
|
| 37 |
+
hidden, cell = state
|
| 38 |
+
gates = self.hidden_conv(hidden) + self.bias
|
| 39 |
+
if values is not None:
|
| 40 |
+
if self.input_conv is None:
|
| 41 |
+
raise ValueError("this ConvLSTM cell has no external input projection")
|
| 42 |
+
gates = gates + self.input_conv(values)
|
| 43 |
+
input_gate, forget_gate, candidate, output_gate = gates.chunk(4, dim=1)
|
| 44 |
+
input_gate = torch.sigmoid(input_gate + self.peephole_input * cell)
|
| 45 |
+
forget_gate = torch.sigmoid(forget_gate + self.peephole_forget * cell)
|
| 46 |
+
cell = forget_gate * cell + input_gate * torch.tanh(candidate)
|
| 47 |
+
output_gate = torch.sigmoid(output_gate + self.peephole_output * cell)
|
| 48 |
+
hidden = output_gate * torch.tanh(cell)
|
| 49 |
+
return hidden, cell
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class ConvLSTM(nn.Module):
|
| 53 |
+
def __init__(self, config):
|
| 54 |
+
super().__init__()
|
| 55 |
+
self.patch_size = int(config["patch_size"])
|
| 56 |
+
self.output_frames = int(config["output_frames"])
|
| 57 |
+
patch_channels = int(config["input_channels"]) * self.patch_size ** 2
|
| 58 |
+
hidden = [int(value) for value in config["hidden_channels"]]
|
| 59 |
+
kernel = int(config["kernel_size"])
|
| 60 |
+
self.encoder = nn.ModuleList([
|
| 61 |
+
ConvLSTMCell(patch_channels, hidden[0], kernel),
|
| 62 |
+
ConvLSTMCell(hidden[0], hidden[1], kernel),
|
| 63 |
+
])
|
| 64 |
+
self.forecaster = nn.ModuleList([
|
| 65 |
+
ConvLSTMCell(0, hidden[0], kernel),
|
| 66 |
+
ConvLSTMCell(hidden[0], hidden[1], kernel),
|
| 67 |
+
])
|
| 68 |
+
self.output = nn.Conv2d(sum(hidden), patch_channels, 1)
|
| 69 |
+
|
| 70 |
+
@staticmethod
|
| 71 |
+
def _zero_state(batch, channels, height, width, reference):
|
| 72 |
+
zeros = reference.new_zeros(batch, channels, height, width)
|
| 73 |
+
return zeros, zeros.clone()
|
| 74 |
+
|
| 75 |
+
def forward(self, sequence, return_states=False):
|
| 76 |
+
patched = patchify(sequence, self.patch_size)
|
| 77 |
+
batch, _, _, height, width = patched.shape
|
| 78 |
+
states = [self._zero_state(batch, cell.hidden_channels, height, width, sequence)
|
| 79 |
+
for cell in self.encoder]
|
| 80 |
+
for step in range(patched.shape[1]):
|
| 81 |
+
values = patched[:, step]
|
| 82 |
+
for index, cell in enumerate(self.encoder):
|
| 83 |
+
states[index] = cell(values, states[index])
|
| 84 |
+
values = states[index][0]
|
| 85 |
+
forecast_states = [(hidden.clone(), cell.clone()) for hidden, cell in states]
|
| 86 |
+
predictions, traces = [], []
|
| 87 |
+
for _ in range(self.output_frames):
|
| 88 |
+
forecast_states[0] = self.forecaster[0](None, forecast_states[0])
|
| 89 |
+
forecast_states[1] = self.forecaster[1](forecast_states[0][0], forecast_states[1])
|
| 90 |
+
hidden = torch.cat((forecast_states[0][0], forecast_states[1][0]), dim=1)
|
| 91 |
+
predictions.append(self.output(hidden))
|
| 92 |
+
traces.append([state[0] for state in forecast_states])
|
| 93 |
+
logits = torch.stack(predictions, dim=1)
|
| 94 |
+
images = unpatchify(logits.sigmoid(), self.patch_size)
|
| 95 |
+
return (images, logits, traces) if return_states else (images, logits)
|
scripts/fake_data.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Generate 5-to-15 synthetic radar sequences at the paper's 100x100 size."""
|
| 2 |
+
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
import yaml
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def make_split(path, count, config, seed):
|
| 13 |
+
rng = np.random.default_rng(seed)
|
| 14 |
+
data = config["data"]
|
| 15 |
+
total = int(data["input_frames"]) + int(data["output_frames"])
|
| 16 |
+
height, width = int(data["height"]), int(data["width"])
|
| 17 |
+
y, x = np.mgrid[-1:1:complex(height), -1:1:complex(width)].astype(np.float32)
|
| 18 |
+
sequences = np.empty((count, total, 1, height, width), np.float32)
|
| 19 |
+
for sample in range(count):
|
| 20 |
+
centers = rng.uniform(-0.55, 0.55, (3, 2))
|
| 21 |
+
velocities = rng.uniform(-0.035, 0.035, (3, 2))
|
| 22 |
+
amplitudes = rng.uniform(0.25, 0.95, 3)
|
| 23 |
+
scales = rng.uniform(0.10, 0.28, 3)
|
| 24 |
+
for step in range(total):
|
| 25 |
+
field = np.zeros((height, width), np.float32)
|
| 26 |
+
for storm in range(3):
|
| 27 |
+
cy, cx = centers[storm] + velocities[storm] * step
|
| 28 |
+
distance = ((x - cx) ** 2 + (y - cy) ** 2) / (2 * scales[storm] ** 2)
|
| 29 |
+
field += amplitudes[storm] * np.exp(-distance)
|
| 30 |
+
sequences[sample, step, 0] = np.clip(field + rng.normal(0, 0.01, field.shape), 0, 1)
|
| 31 |
+
split = int(data["input_frames"])
|
| 32 |
+
np.savez_compressed(path, format_version=np.asarray(data["format_version"]),
|
| 33 |
+
data_source=np.asarray("synthetic_hko_radar_like"),
|
| 34 |
+
inputs=sequences[:, :split], targets=sequences[:, split:])
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def main():
|
| 38 |
+
config = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
|
| 39 |
+
output = ROOT / config["data"]["root"]
|
| 40 |
+
output.mkdir(parents=True, exist_ok=True)
|
| 41 |
+
for offset, (filename, count) in enumerate((("train.npz", config["data"]["train_samples"]),
|
| 42 |
+
("test.npz", config["data"]["test_samples"]))):
|
| 43 |
+
target = output / filename
|
| 44 |
+
if not target.exists():
|
| 45 |
+
make_split(target, int(count), config, int(config["seed"]) + offset)
|
| 46 |
+
print(f"generated={target.relative_to(ROOT)} input=5x1x100x100 target=15x1x100x100")
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
if __name__ == "__main__":
|
| 50 |
+
main()
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Forecast fifteen 6-minute radar frames from five observations."""
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
import torch
|
| 8 |
+
import yaml
|
| 9 |
+
from torch.utils.data import DataLoader
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 13 |
+
sys.path.insert(0, str(ROOT))
|
| 14 |
+
from model.convlstm import ConvLSTM
|
| 15 |
+
from train import RadarDataset, device_from_config
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def main():
|
| 19 |
+
config = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
|
| 20 |
+
device = device_from_config(config)
|
| 21 |
+
checkpoint = torch.load(ROOT / config["paths"]["checkpoint"], map_location=device, weights_only=True)
|
| 22 |
+
model = ConvLSTM(checkpoint["model_config"]).to(device)
|
| 23 |
+
model.load_state_dict(checkpoint["model"])
|
| 24 |
+
model.eval()
|
| 25 |
+
loader = DataLoader(RadarDataset(ROOT / config["data"]["root"] / "test.npz", config), batch_size=1)
|
| 26 |
+
inputs_all, targets_all, predictions_all = [], [], []
|
| 27 |
+
with torch.no_grad():
|
| 28 |
+
for inputs, targets in loader:
|
| 29 |
+
prediction, _ = model(inputs.to(device))
|
| 30 |
+
inputs_all.append(inputs.numpy())
|
| 31 |
+
targets_all.append(targets.numpy())
|
| 32 |
+
predictions_all.append(prediction.cpu().numpy())
|
| 33 |
+
output = ROOT / config["paths"]["inference_dir"] / "predictions.npz"
|
| 34 |
+
output.parent.mkdir(parents=True, exist_ok=True)
|
| 35 |
+
np.savez_compressed(output, inputs=np.concatenate(inputs_all), targets=np.concatenate(targets_all),
|
| 36 |
+
predictions=np.concatenate(predictions_all),
|
| 37 |
+
input_lead_minutes=np.arange(-24, 1, int(config["data"]["interval_minutes"]), dtype=np.int64),
|
| 38 |
+
forecast_lead_minutes=np.arange(1, int(config["data"]["output_frames"]) + 1, dtype=np.int64)
|
| 39 |
+
* int(config["data"]["interval_minutes"]),
|
| 40 |
+
normalized_value_range=np.asarray([0.0, 1.0], np.float32),
|
| 41 |
+
data_type=np.asarray("normalized_radar_echo_grayscale"))
|
| 42 |
+
print(f"predictions={output.relative_to(ROOT)}")
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
if __name__ == "__main__":
|
| 46 |
+
main()
|
scripts/result.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Compute the paper's rainfall metrics and visualize the 90-minute forecast."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
import matplotlib
|
| 7 |
+
matplotlib.use("Agg")
|
| 8 |
+
import matplotlib.pyplot as plt
|
| 9 |
+
import numpy as np
|
| 10 |
+
import yaml
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def to_rainfall(values, config):
|
| 17 |
+
data = config["data"]
|
| 18 |
+
radar_db = values * (float(data["radar_db_max"]) - float(data["radar_db_min"])) + float(data["radar_db_min"])
|
| 19 |
+
return 10 ** ((radar_db - 10 * np.log10(float(data["zr_a"]))) / (10 * float(data["zr_b"])))
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def main():
|
| 23 |
+
config = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
|
| 24 |
+
data = np.load(ROOT / config["paths"]["inference_dir"] / "predictions.npz")
|
| 25 |
+
prediction, target = data["predictions"], data["targets"]
|
| 26 |
+
predicted_rain, target_rain = to_rainfall(prediction, config), to_rainfall(target, config)
|
| 27 |
+
threshold = float(config["data"]["rainfall_threshold_mm_h"])
|
| 28 |
+
forecast, observed = predicted_rain >= threshold, target_rain >= threshold
|
| 29 |
+
hits, misses = np.logical_and(forecast, observed).sum(), np.logical_and(~forecast, observed).sum()
|
| 30 |
+
false_alarms = np.logical_and(forecast, ~observed).sum()
|
| 31 |
+
eps = 1e-9
|
| 32 |
+
frame_correlation, per_step = [], []
|
| 33 |
+
for step in range(prediction.shape[1]):
|
| 34 |
+
left, right = prediction[:, step].reshape(-1), target[:, step].reshape(-1)
|
| 35 |
+
correlation = float(np.dot(left, right) / (np.sqrt(np.dot(left, left) * np.dot(right, right)) + eps))
|
| 36 |
+
frame_correlation.append(correlation)
|
| 37 |
+
step_forecast, step_observed = forecast[:, step], observed[:, step]
|
| 38 |
+
step_hits = np.logical_and(step_forecast, step_observed).sum()
|
| 39 |
+
step_misses = np.logical_and(~step_forecast, step_observed).sum()
|
| 40 |
+
step_false_alarms = np.logical_and(step_forecast, ~step_observed).sum()
|
| 41 |
+
per_step.append({
|
| 42 |
+
"lead_minutes": int(data["forecast_lead_minutes"][step]),
|
| 43 |
+
"rainfall_mse": float(np.mean((predicted_rain[:, step] - target_rain[:, step]) ** 2)),
|
| 44 |
+
"csi": float(step_hits / (step_hits + step_misses + step_false_alarms + eps)),
|
| 45 |
+
"far": float(step_false_alarms / (step_hits + step_false_alarms + eps)),
|
| 46 |
+
"pod": float(step_hits / (step_hits + step_misses + eps)),
|
| 47 |
+
"correlation": correlation,
|
| 48 |
+
})
|
| 49 |
+
metrics = {
|
| 50 |
+
"samples": int(len(prediction)),
|
| 51 |
+
"binary_cross_entropy": float(-(target * np.log(prediction.clip(1e-7, 1 - 1e-7)) +
|
| 52 |
+
(1 - target) * np.log((1 - prediction).clip(1e-7, 1))).mean()),
|
| 53 |
+
"rainfall_mse": float(np.mean((predicted_rain - target_rain) ** 2)),
|
| 54 |
+
"csi": float(hits / (hits + misses + false_alarms + eps)),
|
| 55 |
+
"far": float(false_alarms / (hits + false_alarms + eps)),
|
| 56 |
+
"pod": float(hits / (hits + misses + eps)),
|
| 57 |
+
"correlation": float(np.mean(frame_correlation)),
|
| 58 |
+
"per_forecast_step": per_step,
|
| 59 |
+
}
|
| 60 |
+
output = ROOT / config["paths"]["evaluation_dir"]
|
| 61 |
+
output.mkdir(parents=True, exist_ok=True)
|
| 62 |
+
(output / "metrics.json").write_text(json.dumps(metrics, indent=2) + "\n")
|
| 63 |
+
steps = [0, 2, 5, 8, 11, 14]
|
| 64 |
+
figure, axes = plt.subplots(3, len(steps), figsize=(15, 7))
|
| 65 |
+
for column, step in enumerate(steps):
|
| 66 |
+
axes[0, column].imshow(target[0, step, 0], cmap="turbo", vmin=0, vmax=1)
|
| 67 |
+
axes[1, column].imshow(prediction[0, step, 0], cmap="turbo", vmin=0, vmax=1)
|
| 68 |
+
axes[2, column].imshow(np.abs(target[0, step, 0] - prediction[0, step, 0]), cmap="magma", vmin=0, vmax=1)
|
| 69 |
+
axes[0, column].set_title(f"+{(step + 1) * 6} min")
|
| 70 |
+
for axis in axes[:, column]:
|
| 71 |
+
axis.axis("off")
|
| 72 |
+
figure.tight_layout()
|
| 73 |
+
figure.savefig(output / "comparison.png", dpi=150)
|
| 74 |
+
plt.close(figure)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
if __name__ == "__main__":
|
| 78 |
+
main()
|
scripts/train.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Train the ConvLSTM radar encoder-forecaster with full-sequence BPTT."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
import torch
|
| 10 |
+
import yaml
|
| 11 |
+
from torch.nn.parallel import DistributedDataParallel
|
| 12 |
+
from torch.utils.data import DataLoader, Dataset, DistributedSampler
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 16 |
+
sys.path.insert(0, str(ROOT))
|
| 17 |
+
from model.convlstm import ConvLSTM
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class RadarDataset(Dataset):
|
| 21 |
+
def __init__(self, path, config):
|
| 22 |
+
self.data = np.load(path)
|
| 23 |
+
data = config["data"]
|
| 24 |
+
if str(self.data["format_version"]) != data["format_version"]:
|
| 25 |
+
raise ValueError("incompatible radar data format")
|
| 26 |
+
expected_input = (int(data["input_frames"]), int(data["channels"]), int(data["height"]), int(data["width"]))
|
| 27 |
+
expected_target = (int(data["output_frames"]), int(data["channels"]), int(data["height"]), int(data["width"]))
|
| 28 |
+
if self.data["inputs"].shape[1:] != expected_input or self.data["targets"].shape[1:] != expected_target:
|
| 29 |
+
raise ValueError("radar tensors do not preserve the paper dimensions")
|
| 30 |
+
|
| 31 |
+
def __len__(self):
|
| 32 |
+
return len(self.data["inputs"])
|
| 33 |
+
|
| 34 |
+
def __getitem__(self, index):
|
| 35 |
+
return torch.from_numpy(self.data["inputs"][index]).float(), torch.from_numpy(self.data["targets"][index]).float()
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def device_from_config(config, rank=0):
|
| 39 |
+
if config["runtime"]["device"] == "auto":
|
| 40 |
+
return torch.device("cuda", rank) if torch.cuda.is_available() else torch.device("cpu")
|
| 41 |
+
return torch.device(config["runtime"]["device"])
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def main():
|
| 45 |
+
config = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
|
| 46 |
+
torch.manual_seed(int(config["seed"]))
|
| 47 |
+
distributed = int(os.environ.get("WORLD_SIZE", "1")) > 1
|
| 48 |
+
local_rank = int(os.environ.get("LOCAL_RANK", "0"))
|
| 49 |
+
if distributed:
|
| 50 |
+
torch.distributed.init_process_group("nccl" if torch.cuda.is_available() else "gloo")
|
| 51 |
+
rank = torch.distributed.get_rank() if distributed else 0
|
| 52 |
+
device = device_from_config(config, local_rank)
|
| 53 |
+
dataset = RadarDataset(ROOT / config["data"]["root"] / "train.npz", config)
|
| 54 |
+
sampler = DistributedSampler(dataset, shuffle=True) if distributed else None
|
| 55 |
+
loader = DataLoader(dataset, batch_size=int(config["train"]["batch_size"]), sampler=sampler,
|
| 56 |
+
shuffle=sampler is None, num_workers=int(config["train"]["num_workers"]))
|
| 57 |
+
model = ConvLSTM(config["model"]).to(device)
|
| 58 |
+
if distributed:
|
| 59 |
+
model = DistributedDataParallel(model, device_ids=[local_rank] if device.type == "cuda" else None)
|
| 60 |
+
optimizer = torch.optim.RMSprop(model.parameters(), lr=float(config["train"]["learning_rate"]),
|
| 61 |
+
alpha=float(config["train"]["rmsprop_alpha"]),
|
| 62 |
+
weight_decay=float(config["train"]["weight_decay"]))
|
| 63 |
+
history = []
|
| 64 |
+
for epoch in range(int(config["train"]["epochs"])):
|
| 65 |
+
model.train()
|
| 66 |
+
total, steps = 0.0, 0
|
| 67 |
+
for inputs, targets in loader:
|
| 68 |
+
_, logits = model(inputs.to(device))
|
| 69 |
+
patched_target = torch.nn.functional.pixel_unshuffle(targets.to(device).flatten(0, 1),
|
| 70 |
+
int(config["model"]["patch_size"])).unflatten(0, targets.shape[:2])
|
| 71 |
+
loss = torch.nn.functional.binary_cross_entropy_with_logits(logits, patched_target)
|
| 72 |
+
optimizer.zero_grad(set_to_none=True)
|
| 73 |
+
loss.backward()
|
| 74 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), float(config["train"]["gradient_clip_norm"]))
|
| 75 |
+
optimizer.step()
|
| 76 |
+
total += float(loss.detach())
|
| 77 |
+
steps += 1
|
| 78 |
+
metrics = {"epoch": epoch + 1, "binary_cross_entropy": total / max(steps, 1)}
|
| 79 |
+
history.append(metrics)
|
| 80 |
+
if rank == 0:
|
| 81 |
+
print(f"epoch={epoch + 1} binary_cross_entropy={metrics['binary_cross_entropy']:.6f}")
|
| 82 |
+
if rank == 0:
|
| 83 |
+
checkpoint, metrics_path = ROOT / config["paths"]["checkpoint"], ROOT / config["paths"]["training_metrics"]
|
| 84 |
+
checkpoint.parent.mkdir(parents=True, exist_ok=True)
|
| 85 |
+
metrics_path.parent.mkdir(parents=True, exist_ok=True)
|
| 86 |
+
state = model.module.state_dict() if distributed else model.state_dict()
|
| 87 |
+
torch.save({"model": state, "model_config": config["model"],
|
| 88 |
+
"format_version": config["data"]["format_version"]}, checkpoint)
|
| 89 |
+
metrics_path.write_text(json.dumps({"history": history}, indent=2) + "\n")
|
| 90 |
+
if distributed:
|
| 91 |
+
torch.distributed.destroy_process_group()
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
if __name__ == "__main__":
|
| 95 |
+
main()
|
weight/.gitkeep
ADDED
|
File without changes
|