Upload folder using huggingface_hub
Browse files- .DS_Store +0 -0
- .ms_upload_cache +1 -0
- LICENSE +21 -0
- README.md +179 -0
- conf/config.yaml +65 -0
- config.json +74 -0
- configuration.json +1 -0
- model/__pycache__/dgmr.cpython-311.pyc +0 -0
- model/dgmr.py +114 -0
- model/dgmr_official/__init__.py +6 -0
- model/dgmr_official/__pycache__/__init__.cpython-311.pyc +0 -0
- model/dgmr_official/__pycache__/common.cpython-311.pyc +0 -0
- model/dgmr_official/__pycache__/discriminators.cpython-311.pyc +0 -0
- model/dgmr_official/__pycache__/generators.cpython-311.pyc +0 -0
- model/dgmr_official/__pycache__/losses.cpython-311.pyc +0 -0
- model/dgmr_official/common.py +497 -0
- model/dgmr_official/discriminators.py +232 -0
- model/dgmr_official/generators.py +212 -0
- model/dgmr_official/layers/Attention.py +85 -0
- model/dgmr_official/layers/ConvGRU.py +111 -0
- model/dgmr_official/layers/CoordConv.py +81 -0
- model/dgmr_official/layers/__init__.py +5 -0
- model/dgmr_official/layers/__pycache__/Attention.cpython-311.pyc +0 -0
- model/dgmr_official/layers/__pycache__/ConvGRU.cpython-311.pyc +0 -0
- model/dgmr_official/layers/__pycache__/CoordConv.cpython-311.pyc +0 -0
- model/dgmr_official/layers/__pycache__/__init__.cpython-311.pyc +0 -0
- model/dgmr_official/layers/__pycache__/utils.cpython-311.pyc +0 -0
- model/dgmr_official/layers/utils.py +18 -0
- model/dgmr_official/losses.py +92 -0
- scripts/fake_data.py +74 -0
- scripts/inference.py +87 -0
- scripts/result.py +203 -0
- scripts/train.py +252 -0
- weight/.gitkeep +1 -0
.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
.ms_upload_cache
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"version": 3, "repo_id": "OneScience/Deep-Generative-Model-of-Rain", "files": {"README.md|1787118190.0|6414": {"hash": "ae8b571106cc431550791a52cd3d4c6710031e84363fda4e4a07a3da2ce590d5", "size": 6414, "status": "c"}, "model/__pycache__/dgmr.cpython-311.pyc|1787129401.0|5498": {"hash": "319d638e43312b0c32a8a81e3283ebf22a0c12842183c09091a40a588d08ba69", "size": 5498, "status": "c"}, "conf/config.yaml|1787189067.0|2278": {"hash": "5f07acfc8e3e487a48ebea90399a0207f3ac805a64d16ae7e64da0c749fc2ec8", "size": 2278, "status": "c"}, "model/dgmr_official/__pycache__/__init__.cpython-311.pyc|1787129401.0|506": {"hash": "55bd930076be8ece868e147ca91dbeebee990f54f13d2e470e68221ce5fb631a", "size": 506, "status": "c"}, "LICENSE|1787047051.0|1073": {"hash": "6a9b90043bd7e6836db9a58125c81141a4ee4b8a538aabe33f6dc5180ecff09f", "size": 1073, "status": "c"}, "configuration.json|1787059848.0|39": {"hash": "6b6f30f1e82ad5243c2ff583dcd33b77949442fbf942d80d9f236ffd7a9fe796", "size": 39, "status": "c"}, "model/dgmr.py|1787047183.0|4699": {"hash": "7568f5283b46786a9c034a4cbe5dae727f465bc6184091e239e8f5227bba12a7", "size": 4699, "status": "c"}, "model/dgmr_official/__init__.py|1787047065.0|275": {"hash": "203ab3311b15f33e6130ef33d2da92b820aee85278ba2883ddecdc2af957e790", "size": 275, "status": "c"}, "model/dgmr_official/__pycache__/common.cpython-311.pyc|1787129401.0|20655": {"hash": "52fa87eb304c8ee51b27c65095747576f5b7bf101362b22609f7f4ae87ad299b", "size": 20655, "status": "c"}, "model/dgmr_official/__pycache__/discriminators.cpython-311.pyc|1787129401.0|10887": {"hash": "944d0b3e3fb10fe60264dc396a777d0d10bc3b7a0f55a6676985cb3bfd326d7d", "size": 10887, "status": "c"}, "model/dgmr_official/__pycache__/generators.cpython-311.pyc|1787129402.0|11778": {"hash": "2d7c85d6ed1eab70efce07cc40f227282fd85fa8cd115b79187f59a4a7e7cb81", "size": 11778, "status": "c"}, "model/dgmr_official/__pycache__/losses.cpython-311.pyc|1787129402.0|5091": {"hash": "0e3704bb86d91c76e3a42989c87f622d6c1f989665ea13fe962e00ff10d1efa8", "size": 5091, "status": "c"}, "model/dgmr_official/common.py|1787047241.0|17391": {"hash": "91883f5d9de44bf2821bfa01991fa1b2280f74e2f63089ea1d3c4ad75aea1afa", "size": 17391, "status": "c"}, "model/dgmr_official/discriminators.py|1787047246.0|8891": {"hash": "8f6d93f6008527e3882467d2d57f4244f050076c7bcc633b8dcfb4f33410f260", "size": 8891, "status": "c"}, "model/dgmr_official/generators.py|1787047246.0|7749": {"hash": "1e9ceb835b6d0b7f5aae389169569c29dc8419ccb21c1145f50a8f7c22470102", "size": 7749, "status": "c"}, "model/dgmr_official/layers/Attention.py|1787047051.0|2824": {"hash": "a0e04195b17d9fba45f1b58e37a58524c855f949b86bee52eb370511527e3b57", "size": 2824, "status": "c"}, "model/dgmr_official/layers/ConvGRU.py|1787047051.0|3474": {"hash": "8e97e7df8deeaa3e497365fbb5932b1a7ae2d13cf82656f82e2c40d9985c7e41", "size": 3474, "status": "c"}, "model/dgmr_official/layers/CoordConv.py|1787047051.0|2482": {"hash": "e076d5d5034bcf1bc8a16693fbbf8e348e83b65399c66250dad6b9c6bbcd9915", "size": 2482, "status": "c"}, "model/dgmr_official/layers/__init__.py|1787047051.0|134": {"hash": "37a61b078b8814e0a8c5dce59c246f4cbf83acc6a885a60e7a5afc7bf347d27b", "size": 134, "status": "c"}, "model/dgmr_official/layers/__pycache__/Attention.cpython-311.pyc|1787129401.0|3816": {"hash": "80461302aaefcdbd65d0c30e5bba4e6fbec53f20d2f0b287d3011ba5065b2ac7", "size": 3816, "status": "c"}, "model/dgmr_official/layers/__pycache__/ConvGRU.cpython-311.pyc|1787129401.0|4885": {"hash": "e12ad2510292b13e7a135eeeb59a50f7e4d4221d984fa5c1eccccdb495910f32", "size": 4885, "status": "c"}, "model/dgmr_official/layers/__pycache__/CoordConv.cpython-311.pyc|1787129401.0|4614": {"hash": "870a9883bfc11c72463b31ae5b8c3362147f74c8760870169fbb66067f66f446", "size": 4614, "status": "c"}, "model/dgmr_official/layers/__pycache__/__init__.cpython-311.pyc|1787129401.0|423": {"hash": "ca06f7dc005aca0179c32edcdfbde241b54ca923694ab232a4c1f220b5be73b2", "size": 423, "status": "c"}, "model/dgmr_official/layers/__pycache__/utils.cpython-311.pyc|1787129401.0|1008": {"hash": "dcb03036c637d7fed95d1d5f794857a1d4662a77b1b366a978fc985cb9b8d84e", "size": 1008, "status": "c"}, "model/dgmr_official/layers/utils.py|1787047246.0|528": {"hash": "37926142863e60911d80c7208ee72380a1dccbc759c14101208c6c4abfe3271f", "size": 528, "status": "c"}, "model/dgmr_official/losses.py|1787047133.0|2873": {"hash": "38a72b2b11ca5cd6f2920ac5fbb32fb70bb6c8c6eda7e34a04e381d1ec5571b5", "size": 2873, "status": "c"}, "scripts/fake_data.py|1787118238.0|3066": {"hash": "88dc76ffa492214503a7446d8880234fe14ec88504b1a97bcc28b232f005f8f5", "size": 3066, "status": "c"}, "scripts/inference.py|1787060096.0|3338": {"hash": "e52039c1c3fe77bd130b2379b5d15e8db518e41bb92c206ca8f6f107ea77ecd3", "size": 3338, "status": "c"}, "scripts/train.py|1787060016.0|11345": {"hash": "62c589a0058374338138d065856a6290a839c614cf62133efa4f338621fa761e", "size": 11345, "status": "c"}, "weight/.gitkeep|1787059908.0|1": {"hash": "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b", "size": 1, "status": "c"}, "scripts/result.py|1787129360.0|8669": {"hash": "2041021d7575eb66e89f2577174a338e6cd2f063826f556a8880bd344306a05c", "size": 8669, "status": "c"}}}
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2021 Open Climate Fix
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
- zh
|
| 6 |
+
tags:
|
| 7 |
+
- OneScience
|
| 8 |
+
- Earth science
|
| 9 |
+
- Weather forecasting
|
| 10 |
+
- Short-term precipitation nowcasting
|
| 11 |
+
- Deep generative models
|
| 12 |
+
- Generative adversarial networks
|
| 13 |
+
frameworks: PyTorch
|
| 14 |
+
datasets:
|
| 15 |
+
- OneScience/ERA5
|
| 16 |
+
---
|
| 17 |
+
<p align="center">
|
| 18 |
+
<strong>
|
| 19 |
+
<span style="font-size: 30px;">DGMR</span>
|
| 20 |
+
</strong>
|
| 21 |
+
</p>
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# Model Introduction
|
| 25 |
+
|
| 26 |
+
DGMR (Deep Generative Model of Radar) was proposed by DeepMind in 2021. It is a short-term precipitation nowcasting model based on a conditional generative adversarial network (cGAN). The generator consists of a latent conditioning stack, a context conditioning stack, and an autoregressive sampler based on multiple ConvGRU layers. The discriminator evaluates both spatial and temporal dimensions, and the training objective combines hinge GAN loss with a grid-cell regularizer.
|
| 27 |
+
|
| 28 |
+
Paper:Skillful Precipitation Nowcasting using Deep Generative Models of Radar
|
| 29 |
+
|
| 30 |
+
https://arxiv.org/abs/2104.00954
|
| 31 |
+
|
| 32 |
+
# Model Description
|
| 33 |
+
|
| 34 |
+
DGMR is a probabilistic short-term precipitation nowcasting model: it takes four consecutive radar echo frames and generates 18 future frames at once (5-minute steps, 90 minutes in total, in the paper). Its output is a sample rather than a deterministic estimate. This repository is organized from the official Open Climate Fix PyTorch implementation (`openclimatefix/dgmr`, MIT License) and integrated with the OneScience data loading and training workflow.
|
| 35 |
+
|
| 36 |
+
# Use Cases
|
| 37 |
+
|
| 38 |
+
| Scenario | Description |
|
| 39 |
+
| :---: | :--- |
|
| 40 |
+
| Short-term precipitation nowcasting research | Train a cGAN on radar echo sequences to generate future radar echo frames. |
|
| 41 |
+
| Probabilistic forecast output | Obtain multiple generated samples of future fields through latent-variable sampling. |
|
| 42 |
+
| Local quick validation | Use synthetic data to check data loading, training, inference, and result scripts. |
|
| 43 |
+
| ModelScope/OneCode execution | Download the model package, install dependencies, and run the scripts directly. |
|
| 44 |
+
| Multi-card training | Launch multi-process training with `torchrun`. |
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
# Usage
|
| 48 |
+
|
| 49 |
+
## 1. OneCode Usage
|
| 50 |
+
|
| 51 |
+
Use the OneCode online environment for intelligent one-click AI4S programming:
|
| 52 |
+
|
| 53 |
+
[Try intelligent one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 54 |
+
|
| 55 |
+
## 2. Manual Installation and Usage
|
| 56 |
+
|
| 57 |
+
**Hardware Requirements**
|
| 58 |
+
|
| 59 |
+
- GPU or DCU is recommended.
|
| 60 |
+
- CPU can be used for imports and small-configuration connectivity validation, but full training and inference are slower.
|
| 61 |
+
- DCU users must install DTK beforehand. DTK 25.04.2 or later, or the OneScience-recommended version matching the current cluster, is recommended.
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
### Download the Model Package
|
| 65 |
+
|
| 66 |
+
```bash
|
| 67 |
+
hf download OneScience-Group/DGMR --local-dir ./DGMR
|
| 68 |
+
cd DGMR
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
### Install the Runtime Environment
|
| 72 |
+
|
| 73 |
+
**DCU Environment**
|
| 74 |
+
|
| 75 |
+
```bash
|
| 76 |
+
# Activate DTK and CONDA first
|
| 77 |
+
conda create -n onescience311 python=3.11 -y
|
| 78 |
+
conda activate onescience311
|
| 79 |
+
# uv installation is supported
|
| 80 |
+
pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
**GPU Environment**
|
| 84 |
+
```bash
|
| 85 |
+
# Activate CONDA first
|
| 86 |
+
conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
|
| 87 |
+
conda activate onescience311
|
| 88 |
+
# uv installation is supported
|
| 89 |
+
pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
### Training Data
|
| 93 |
+
|
| 94 |
+
The OneScience community provides ERA5 data for training (the current repository contains complete data slices subject to data-file size limits). Download it with the command below and confirm that the data path in `conf/config.yaml` is correct:
|
| 95 |
+
|
| 96 |
+
```bash
|
| 97 |
+
hf download --repo-type dataset OneScience-Group/ERA5 --local-dir ./data
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
For a quick workflow validation, run the synthetic data script first:
|
| 101 |
+
|
| 102 |
+
```bash
|
| 103 |
+
python scripts/fake_data.py
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
> Note: `scripts/fake_data.py` generates a single-channel radar sequence from `num_context`, `forecast_steps`, batch size, and radar grid size. The current small configuration uses 4 input frames, 6 output frames, and a 128×128 grid.
|
| 107 |
+
|
| 108 |
+
### Training
|
| 109 |
+
|
| 110 |
+
Single card:
|
| 111 |
+
|
| 112 |
+
```bash
|
| 113 |
+
python scripts/train.py
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
Multiple cards:
|
| 117 |
+
|
| 118 |
+
```bash
|
| 119 |
+
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
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
Training outputs:
|
| 123 |
+
|
| 124 |
+
```text
|
| 125 |
+
data/checkpoints/model_bak.pth
|
| 126 |
+
data/checkpoints/trloss.npy
|
| 127 |
+
data/checkpoints/valoss.npy
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
### Training Weights
|
| 131 |
+
The `weight/` folder is reserved for model weights. Pretrained weights are not provided by default; users may train the model using the paper configuration. Official DeepMind pretrained weights require access approval and differ from the current configuration in channel count and grid size, so they must be aligned before loading.
|
| 132 |
+
|
| 133 |
+
### Inference
|
| 134 |
+
|
| 135 |
+
Inference reads `data/checkpoints/model_bak.pth`:
|
| 136 |
+
|
| 137 |
+
```bash
|
| 138 |
+
python scripts/inference.py
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
Prediction results are written frame by frame to:
|
| 142 |
+
|
| 143 |
+
```text
|
| 144 |
+
result/output/
|
| 145 |
+
```
|
| 146 |
+
|
| 147 |
+
### Evaluation and Visualization
|
| 148 |
+
|
| 149 |
+
```bash
|
| 150 |
+
python scripts/result.py
|
| 151 |
+
```
|
| 152 |
+
|
| 153 |
+
Outputs include:
|
| 154 |
+
|
| 155 |
+
- `result/rmse.npy`
|
| 156 |
+
- `result/acc.npy`
|
| 157 |
+
- `result/loss.png`
|
| 158 |
+
- Forecast comparison plots for the specified date and variables
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
# Official Source and Reproduction Notes
|
| 162 |
+
|
| 163 |
+
- The model implementation comes from the official Open Climate Fix PyTorch package `openclimatefix/dgmr` (MIT License). The related network modules (common/layers/generators/discriminators/losses) are embedded unchanged in `model/dgmr_official/` (with the HuggingFace hub mixin and pytorch_lightning training loop removed); `model/dgmr.py` is only a YAML-driven thin wrapper.
|
| 164 |
+
- `conf/config.yaml` uses a small configuration (`forecast_steps=6`, `output_shape=128`, `latent_channels=384`, `context_channels=192`) for connectivity validation by default; paper-level reproduction requires 4→18 frames, a 256×256 grid, `latent_channels=768`, and `context_channels=384`.
|
| 165 |
+
- The paper-level configuration (`num_context=4`, `forecast_steps=18`) requires at least `T >= num_context + forecast_steps + 1 = 23` frames per year; the current synthetic data uses `T=10` (`time_step=6h`), so the default configuration uses 4→6 frames.
|
| 166 |
+
- The discriminator contains BatchNorm1d, so the training batch must be `>= 2` (with synthetic data, the training set uses two samples from two years, exactly one batch).
|
| 167 |
+
- The following details are not disclosed in the paper and are assumptions in this reproduction:data normalization statistics (identity normalization is currently used; real statistics will be supplied with the data), and the number of randomly sampled time frames in the discriminator.
|
| 168 |
+
|
| 169 |
+
# Official OneScience Information
|
| 170 |
+
|
| 171 |
+
| Platform | OneScience Main Repository | Skills Repository |
|
| 172 |
+
| --- | --- | --- |
|
| 173 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 174 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 175 |
+
|
| 176 |
+
# Citation and License
|
| 177 |
+
|
| 178 |
+
- This repository is an independent organization and adaptation of DGMR. The model source is based on the Open Climate Fix `openclimatefix/dgmr` implementation and follows the MIT License.
|
| 179 |
+
- Please cite:Ravuri et al. Skilful Precipitation Nowcasting using Deep Generative Models of Radar. Nature 597, 672-677, 2021.
|
conf/config.yaml
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# DGMR 训练配置示例
|
| 2 |
+
# 论文配置:4 帧雷达观测上下文(num_context=4)预测 18 帧(forecast_steps=18),
|
| 3 |
+
# 空间 256×256 单通道,latent_channels=768 / context_channels=384(生成器约 13.4M 参数)。
|
| 4 |
+
# 当前默认配置为连通性验证小配置:空间 128×128,预测 6 帧;fake_data.py
|
| 5 |
+
# 根据 num_context、forecast_steps 与 batch_size 自动推导每年所需时间长度 T。
|
| 6 |
+
model:
|
| 7 |
+
start_epoch: 0
|
| 8 |
+
max_epoch: 100
|
| 9 |
+
lr: 1E-4 # 论文使用 Adam lr=1e-4 分别训练生成器与判别器
|
| 10 |
+
lr_disc: 1E-4
|
| 11 |
+
patience: 50
|
| 12 |
+
checkpoint_dir: "./data/checkpoints"
|
| 13 |
+
|
| 14 |
+
# DGMR 结构参数(论文值见注释)
|
| 15 |
+
forecast_steps: 6 # 预测帧数,论文为 18;小配置取 6
|
| 16 |
+
num_context: 4 # 上下文(观测)帧数,论文为 4
|
| 17 |
+
input_channels: 1 # 每帧通道数,论文为 1(雷达回波)
|
| 18 |
+
output_shape: 128 # 空间尺寸(需 32 整除),论文为 256
|
| 19 |
+
conv_type: "standard" # standard / coord / 3d
|
| 20 |
+
latent_channels: 384 # 论文为 768
|
| 21 |
+
context_channels: 192 # 论文为 384
|
| 22 |
+
generation_steps: 6 # 网格单元正则器 Monte-Carlo 采样次数,论文为 6
|
| 23 |
+
grid_lambda: 20.0 # 网格单元正则器权重,论文为 20
|
| 24 |
+
precip_weight_cap: 24.0 # 网格单元权重函数上限,论文为 24
|
| 25 |
+
|
| 26 |
+
# 整个数据读取流程
|
| 27 |
+
datapipe:
|
| 28 |
+
name: "ERA5"
|
| 29 |
+
task: "weather_forecasting"
|
| 30 |
+
|
| 31 |
+
dataset:
|
| 32 |
+
type: "hdf5"
|
| 33 |
+
data_dir: './data/'
|
| 34 |
+
train_time: [1951, 1952]
|
| 35 |
+
val_time: [1953]
|
| 36 |
+
test_time: [1954]
|
| 37 |
+
img_size: [128, 128]
|
| 38 |
+
verbose: true
|
| 39 |
+
cache: false
|
| 40 |
+
|
| 41 |
+
# 气象变量(DGMR 面向单通道雷达回波场,此处为占位变量名)
|
| 42 |
+
channels: ['rain_radar']
|
| 43 |
+
|
| 44 |
+
# DataLoader 配置
|
| 45 |
+
# 注意:DGMR 判别器内部含 BatchNorm1d,训练 batch 必须 >= 2。
|
| 46 |
+
# fake_data.py 会保证每个年份至少产生 batch_size 个完整时间窗口。
|
| 47 |
+
dataloader:
|
| 48 |
+
mask_dtype: "float32"
|
| 49 |
+
batch_size: 2
|
| 50 |
+
num_workers: 1
|
| 51 |
+
pin_memory: true
|
| 52 |
+
drop_last: true
|
| 53 |
+
shuffle: false
|
| 54 |
+
prefetch_factor: 2
|
| 55 |
+
persistent_workers: true
|
| 56 |
+
|
| 57 |
+
# 分布式配置
|
| 58 |
+
distributed:
|
| 59 |
+
enabled: true
|
| 60 |
+
sampler: "DistributedSampler"
|
| 61 |
+
rank: 0
|
| 62 |
+
world_size: 2
|
| 63 |
+
shuffle: true
|
| 64 |
+
seed: 42
|
| 65 |
+
drop_last: true
|
config.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_name": "DGMR",
|
| 3 |
+
"model_type": "dgmr",
|
| 4 |
+
"architectures": [
|
| 5 |
+
"DGMR"
|
| 6 |
+
],
|
| 7 |
+
"framework": "PyTorch",
|
| 8 |
+
"domain": "climate-and-atmosphere",
|
| 9 |
+
"task": "precipitation-nowcasting",
|
| 10 |
+
"implementation": {
|
| 11 |
+
"entry_point": "model/dgmr.py",
|
| 12 |
+
"scope": "YAML-configured DGMR conditional GAN wrapper with vendored Open Climate Fix generator, spatial and temporal discriminators, and training losses"
|
| 13 |
+
},
|
| 14 |
+
"architecture": {
|
| 15 |
+
"family": "conditional generative adversarial network for probabilistic radar nowcasting",
|
| 16 |
+
"generator": "context conditioning stack plus latent conditioning stack and a four-scale ConvGRU autoregressive sampler",
|
| 17 |
+
"discriminator": "combined spatial and temporal discriminators with spectrally normalized residual blocks",
|
| 18 |
+
"training_objective": "hinge GAN loss plus weighted grid-cell regularizer",
|
| 19 |
+
"input_format": "BTCHW",
|
| 20 |
+
"output_format": "BTCHW",
|
| 21 |
+
"activation": "ReLU",
|
| 22 |
+
"normalization": "BatchNorm",
|
| 23 |
+
"repository_default_config": {
|
| 24 |
+
"purpose": "small connectivity-validation configuration",
|
| 25 |
+
"num_context": 4,
|
| 26 |
+
"forecast_steps": 6,
|
| 27 |
+
"input_channels": 1,
|
| 28 |
+
"output_shape": 128,
|
| 29 |
+
"conv_type": "standard",
|
| 30 |
+
"latent_channels": 384,
|
| 31 |
+
"context_channels": 192,
|
| 32 |
+
"generation_steps": 6,
|
| 33 |
+
"grid_lambda": 20.0,
|
| 34 |
+
"precip_weight_cap": 24.0
|
| 35 |
+
},
|
| 36 |
+
"paper_configuration": {
|
| 37 |
+
"num_context": 4,
|
| 38 |
+
"forecast_steps": 18,
|
| 39 |
+
"input_channels": 1,
|
| 40 |
+
"output_shape": 256,
|
| 41 |
+
"latent_channels": 768,
|
| 42 |
+
"context_channels": 384,
|
| 43 |
+
"generation_steps": 6,
|
| 44 |
+
"grid_lambda": 20.0,
|
| 45 |
+
"precip_weight_cap": 24.0
|
| 46 |
+
}
|
| 47 |
+
},
|
| 48 |
+
"data": {
|
| 49 |
+
"dataset": "ERA5Datapipe-compatible HDF5 placeholder radar sequences",
|
| 50 |
+
"variable": "rain_radar",
|
| 51 |
+
"input_length": 4,
|
| 52 |
+
"output_length": 6,
|
| 53 |
+
"channels": 1,
|
| 54 |
+
"default_smoke_spatial_size": [
|
| 55 |
+
128,
|
| 56 |
+
128
|
| 57 |
+
],
|
| 58 |
+
"paper_frame_interval_minutes": 5,
|
| 59 |
+
"paper_output_length": 18,
|
| 60 |
+
"paper_spatial_size": [
|
| 61 |
+
256,
|
| 62 |
+
256
|
| 63 |
+
],
|
| 64 |
+
"normalization": "identity statistics in synthetic data; real-data statistics are read from HDF5"
|
| 65 |
+
},
|
| 66 |
+
"configuration_sources": [
|
| 67 |
+
"conf/config.yaml",
|
| 68 |
+
"model/dgmr.py",
|
| 69 |
+
"model/dgmr_official/",
|
| 70 |
+
"scripts/train.py",
|
| 71 |
+
"scripts/fake_data.py",
|
| 72 |
+
"README.md"
|
| 73 |
+
]
|
| 74 |
+
}
|
configuration.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"framework":"Pytorch","task":"other"}
|
model/__pycache__/dgmr.cpython-311.pyc
ADDED
|
Binary file (5.5 kB). View file
|
|
|
model/dgmr.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding=utf-8
|
| 2 |
+
#
|
| 3 |
+
# SPDX-License-Identifier: MIT
|
| 4 |
+
#
|
| 5 |
+
# Copyright (c) 2021 Open Climate Fix
|
| 6 |
+
#
|
| 7 |
+
# This module is a thin configuration wrapper around the DGMR (Deep
|
| 8 |
+
# Generative Model of Radar) architecture from Ravuri et al. (2021,
|
| 9 |
+
# "Skilful Precipitation Nowcasting using Deep Generative Models of Radar",
|
| 10 |
+
# Nature 597), as re-implemented in PyTorch by Open Climate Fix
|
| 11 |
+
# (``openclimatefix/skillful_nowcasting``, MIT License). The network modules
|
| 12 |
+
# are vendored verbatim (minus HuggingFace hub mixins) under
|
| 13 |
+
# ``dgmr_official/``; only config plumbing and the plain ``forward`` are
|
| 14 |
+
# added here for YAML-driven usage.
|
| 15 |
+
import torch
|
| 16 |
+
import torch.nn as nn
|
| 17 |
+
|
| 18 |
+
from model.dgmr_official.common import ContextConditioningStack, LatentConditioningStack
|
| 19 |
+
from model.dgmr_official.discriminators import Discriminator
|
| 20 |
+
from model.dgmr_official.generators import Generator, Sampler
|
| 21 |
+
from model.dgmr_official.losses import GridCellLoss
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def weight_fn(y, precip_weight_cap=24.0):
|
| 25 |
+
"""
|
| 26 |
+
Weight function for the grid cell loss: w(y) = max(y + 1, cap).
|
| 27 |
+
"""
|
| 28 |
+
return torch.max(y + 1, torch.tensor(precip_weight_cap, device=y.device))
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class DGMR(nn.Module):
|
| 32 |
+
"""
|
| 33 |
+
Config-driven DGMR wrapper (generator + discriminator).
|
| 34 |
+
|
| 35 |
+
The generator is a conditional GAN generator that takes ``num_context``
|
| 36 |
+
observed radar frames of shape [B, T, C, H, W] and produces
|
| 37 |
+
``forecast_steps`` future frames of the same spatial size. The
|
| 38 |
+
discriminator scores full sequences (context + forecast) spatially and
|
| 39 |
+
temporally; during GAN training the hinge losses plus the grid-cell
|
| 40 |
+
regularizer are applied (see ``dgmr_official/losses.py`` and the paper).
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
forecast_steps: Number of frames to predict in the future (paper: 18).
|
| 44 |
+
num_context: Number of input/context frames (paper: 4).
|
| 45 |
+
input_channels: Number of channels per frame (paper: 1, radar).
|
| 46 |
+
output_shape: Spatial size of the frames; must be divisible by 32
|
| 47 |
+
(paper: 256). Discriminators additionally need >= 128 px.
|
| 48 |
+
conv_type: Convolution flavour used by the conditioning stack,
|
| 49 |
+
one of "standard" / "coord" / "3d".
|
| 50 |
+
latent_channels / context_channels: DGMR architecture sizes
|
| 51 |
+
(paper: 768 / 384).
|
| 52 |
+
generation_steps: Number of Monte-Carlo generator samples used when
|
| 53 |
+
computing the grid-cell regularizer during training (paper: 6).
|
| 54 |
+
grid_lambda: Weight of the grid-cell regularizer (paper: 20).
|
| 55 |
+
precip_weight_cap: Ceiling for the grid-cell weight function (paper: 24).
|
| 56 |
+
"""
|
| 57 |
+
|
| 58 |
+
def __init__(
|
| 59 |
+
self,
|
| 60 |
+
forecast_steps: int = 18,
|
| 61 |
+
num_context: int = 4,
|
| 62 |
+
input_channels: int = 1,
|
| 63 |
+
output_shape: int = 256,
|
| 64 |
+
conv_type: str = "standard",
|
| 65 |
+
latent_channels: int = 768,
|
| 66 |
+
context_channels: int = 384,
|
| 67 |
+
generation_steps: int = 6,
|
| 68 |
+
grid_lambda: float = 20.0,
|
| 69 |
+
precip_weight_cap: float = 24.0,
|
| 70 |
+
):
|
| 71 |
+
super().__init__()
|
| 72 |
+
self.forecast_steps = int(forecast_steps)
|
| 73 |
+
self.num_context = int(num_context)
|
| 74 |
+
self.input_channels = int(input_channels)
|
| 75 |
+
self.output_shape = int(output_shape)
|
| 76 |
+
self.conv_type = conv_type
|
| 77 |
+
self.latent_channels = int(latent_channels)
|
| 78 |
+
self.context_channels = int(context_channels)
|
| 79 |
+
self.generation_steps = int(generation_steps)
|
| 80 |
+
self.grid_lambda = float(grid_lambda)
|
| 81 |
+
self.precip_weight_cap = float(precip_weight_cap)
|
| 82 |
+
|
| 83 |
+
self.conditioning_stack = ContextConditioningStack(
|
| 84 |
+
input_channels=self.input_channels,
|
| 85 |
+
conv_type=self.conv_type,
|
| 86 |
+
output_channels=self.context_channels,
|
| 87 |
+
)
|
| 88 |
+
self.latent_stack = LatentConditioningStack(
|
| 89 |
+
shape=(
|
| 90 |
+
8 * self.input_channels,
|
| 91 |
+
self.output_shape // 32,
|
| 92 |
+
self.output_shape // 32,
|
| 93 |
+
),
|
| 94 |
+
output_channels=self.latent_channels,
|
| 95 |
+
)
|
| 96 |
+
self.sampler = Sampler(
|
| 97 |
+
forecast_steps=self.forecast_steps,
|
| 98 |
+
latent_channels=self.latent_channels,
|
| 99 |
+
context_channels=self.context_channels,
|
| 100 |
+
)
|
| 101 |
+
self.generator = Generator(self.conditioning_stack, self.latent_stack, self.sampler)
|
| 102 |
+
self.discriminator = Discriminator(self.input_channels)
|
| 103 |
+
self.grid_regularizer = GridCellLoss(
|
| 104 |
+
weight_fn=weight_fn, precip_weight_cap=self.precip_weight_cap
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 108 |
+
"""
|
| 109 |
+
Args:
|
| 110 |
+
x: Observed radar frames, shape [batch, num_context, C, H, W].
|
| 111 |
+
Returns:
|
| 112 |
+
Forecast frames, shape [batch, forecast_steps, C, H, W].
|
| 113 |
+
"""
|
| 114 |
+
return self.generator(x)
|
model/dgmr_official/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Vendored DGMR (Deep Generative Model of Radar) modules.
|
| 2 |
+
|
| 3 |
+
Stripped from the ``dgmr`` PyTorch package (Open Climate Fix, MIT License):
|
| 4 |
+
the ``PyTorchModelHubMixin`` HuggingFace mixin and the Lightning training
|
| 5 |
+
loop are removed; the model modules themselves are verbatim.
|
| 6 |
+
"""
|
model/dgmr_official/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (506 Bytes). View file
|
|
|
model/dgmr_official/__pycache__/common.cpython-311.pyc
ADDED
|
Binary file (20.7 kB). View file
|
|
|
model/dgmr_official/__pycache__/discriminators.cpython-311.pyc
ADDED
|
Binary file (10.9 kB). View file
|
|
|
model/dgmr_official/__pycache__/generators.cpython-311.pyc
ADDED
|
Binary file (11.8 kB). View file
|
|
|
model/dgmr_official/__pycache__/losses.cpython-311.pyc
ADDED
|
Binary file (5.09 kB). View file
|
|
|
model/dgmr_official/common.py
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Modules for generator blocks."""
|
| 2 |
+
|
| 3 |
+
from typing import Tuple
|
| 4 |
+
|
| 5 |
+
import einops
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn.functional as F
|
| 8 |
+
|
| 9 |
+
from torch.distributions import normal
|
| 10 |
+
from torch.nn.modules.pixelshuffle import PixelUnshuffle
|
| 11 |
+
from torch.nn.utils.parametrizations import spectral_norm
|
| 12 |
+
|
| 13 |
+
from .layers import AttentionLayer
|
| 14 |
+
from .layers.utils import get_conv_layer
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class GBlock(torch.nn.Module):
|
| 18 |
+
"""Residual generator block without upsampling."""
|
| 19 |
+
|
| 20 |
+
def __init__(
|
| 21 |
+
self,
|
| 22 |
+
input_channels: int = 12,
|
| 23 |
+
output_channels: int = 12,
|
| 24 |
+
conv_type: str = "standard",
|
| 25 |
+
spectral_normalized_eps=0.0001,
|
| 26 |
+
):
|
| 27 |
+
"""
|
| 28 |
+
G Block from Skillful Nowcasting, see https://arxiv.org/pdf/2104.00954.pdf.
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
input_channels: Number of input channels
|
| 32 |
+
output_channels: Number of output channels
|
| 33 |
+
conv_type: Type of convolution desired, see satflow/models/utils.py for options
|
| 34 |
+
spectral_normalized_eps: constrains the spectral norm of the weights.
|
| 35 |
+
"""
|
| 36 |
+
super().__init__()
|
| 37 |
+
self.output_channels = output_channels
|
| 38 |
+
self.bn1 = torch.nn.BatchNorm2d(input_channels)
|
| 39 |
+
self.bn2 = torch.nn.BatchNorm2d(input_channels)
|
| 40 |
+
self.relu = torch.nn.ReLU()
|
| 41 |
+
# Upsample in the 1x1
|
| 42 |
+
conv2d = get_conv_layer(conv_type)
|
| 43 |
+
self.conv_1x1 = spectral_norm(
|
| 44 |
+
conv2d(
|
| 45 |
+
in_channels=input_channels,
|
| 46 |
+
out_channels=output_channels,
|
| 47 |
+
kernel_size=1,
|
| 48 |
+
),
|
| 49 |
+
eps=spectral_normalized_eps,
|
| 50 |
+
)
|
| 51 |
+
# Upsample 2D conv
|
| 52 |
+
self.first_conv_3x3 = spectral_norm(
|
| 53 |
+
conv2d(
|
| 54 |
+
in_channels=input_channels,
|
| 55 |
+
out_channels=input_channels,
|
| 56 |
+
kernel_size=3,
|
| 57 |
+
padding=1,
|
| 58 |
+
),
|
| 59 |
+
eps=spectral_normalized_eps,
|
| 60 |
+
)
|
| 61 |
+
self.last_conv_3x3 = spectral_norm(
|
| 62 |
+
conv2d(
|
| 63 |
+
in_channels=input_channels, out_channels=output_channels, kernel_size=3, padding=1
|
| 64 |
+
),
|
| 65 |
+
eps=spectral_normalized_eps,
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 69 |
+
"""Apply the forward function."""
|
| 70 |
+
# Optionally spectrally normalized 1x1 convolution
|
| 71 |
+
if x.shape[1] != self.output_channels:
|
| 72 |
+
sc = self.conv_1x1(x)
|
| 73 |
+
else:
|
| 74 |
+
sc = x
|
| 75 |
+
|
| 76 |
+
x2 = self.bn1(x)
|
| 77 |
+
x2 = self.relu(x2)
|
| 78 |
+
x2 = self.first_conv_3x3(x2) # Make sure size is doubled
|
| 79 |
+
x2 = self.bn2(x2)
|
| 80 |
+
x2 = self.relu(x2)
|
| 81 |
+
x2 = self.last_conv_3x3(x2)
|
| 82 |
+
# Sum combine, residual connection
|
| 83 |
+
x = x2 + sc
|
| 84 |
+
return x
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
class UpsampleGBlock(torch.nn.Module):
|
| 88 |
+
"""Residual generator block with upsampling."""
|
| 89 |
+
|
| 90 |
+
def __init__(
|
| 91 |
+
self,
|
| 92 |
+
input_channels: int = 12,
|
| 93 |
+
output_channels: int = 12,
|
| 94 |
+
conv_type: str = "standard",
|
| 95 |
+
spectral_normalized_eps=0.0001,
|
| 96 |
+
):
|
| 97 |
+
"""
|
| 98 |
+
G Block from Skillful Nowcasting, see https://arxiv.org/pdf/2104.00954.pdf.
|
| 99 |
+
|
| 100 |
+
Args:
|
| 101 |
+
input_channels: Number of input channels.
|
| 102 |
+
output_channels: Number of output channels.
|
| 103 |
+
conv_type: Type of convolution desired, see satflow/models/utils.py for options.
|
| 104 |
+
spectral_normalized_eps: constrains the spectral norm of the weights.
|
| 105 |
+
"""
|
| 106 |
+
super().__init__()
|
| 107 |
+
self.output_channels = output_channels
|
| 108 |
+
self.bn1 = torch.nn.BatchNorm2d(input_channels)
|
| 109 |
+
self.bn2 = torch.nn.BatchNorm2d(input_channels)
|
| 110 |
+
self.relu = torch.nn.ReLU()
|
| 111 |
+
# Upsample in the 1x1
|
| 112 |
+
conv2d = get_conv_layer(conv_type)
|
| 113 |
+
self.conv_1x1 = spectral_norm(
|
| 114 |
+
conv2d(
|
| 115 |
+
in_channels=input_channels,
|
| 116 |
+
out_channels=output_channels,
|
| 117 |
+
kernel_size=1,
|
| 118 |
+
),
|
| 119 |
+
eps=spectral_normalized_eps,
|
| 120 |
+
)
|
| 121 |
+
self.upsample = torch.nn.Upsample(scale_factor=2, mode="nearest")
|
| 122 |
+
# Upsample 2D conv
|
| 123 |
+
self.first_conv_3x3 = spectral_norm(
|
| 124 |
+
conv2d(
|
| 125 |
+
in_channels=input_channels,
|
| 126 |
+
out_channels=input_channels,
|
| 127 |
+
kernel_size=3,
|
| 128 |
+
padding=1,
|
| 129 |
+
),
|
| 130 |
+
eps=spectral_normalized_eps,
|
| 131 |
+
)
|
| 132 |
+
self.last_conv_3x3 = spectral_norm(
|
| 133 |
+
conv2d(
|
| 134 |
+
in_channels=input_channels, out_channels=output_channels, kernel_size=3, padding=1
|
| 135 |
+
),
|
| 136 |
+
eps=spectral_normalized_eps,
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 140 |
+
"""Apply the forward function."""
|
| 141 |
+
# Spectrally nsormalized 1x1 convolution
|
| 142 |
+
sc = self.upsample(x)
|
| 143 |
+
sc = self.conv_1x1(sc)
|
| 144 |
+
|
| 145 |
+
x2 = self.bn1(x)
|
| 146 |
+
x2 = self.relu(x2)
|
| 147 |
+
# Upsample
|
| 148 |
+
x2 = self.upsample(x2)
|
| 149 |
+
x2 = self.first_conv_3x3(x2) # Make sure size is doubled
|
| 150 |
+
x2 = self.bn2(x2)
|
| 151 |
+
x2 = self.relu(x2)
|
| 152 |
+
x2 = self.last_conv_3x3(x2)
|
| 153 |
+
# Sum combine, residual connection
|
| 154 |
+
x = x2 + sc
|
| 155 |
+
return x
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
class DBlock(torch.nn.Module):
|
| 159 |
+
"""D block class."""
|
| 160 |
+
|
| 161 |
+
def __init__(
|
| 162 |
+
self,
|
| 163 |
+
input_channels: int = 12,
|
| 164 |
+
output_channels: int = 12,
|
| 165 |
+
conv_type: str = "standard",
|
| 166 |
+
first_relu: bool = True,
|
| 167 |
+
keep_same_output: bool = False,
|
| 168 |
+
):
|
| 169 |
+
"""
|
| 170 |
+
D and 3D Block from Skillful Nowcasting, see https://arxiv.org/pdf/2104.00954.pdf.
|
| 171 |
+
|
| 172 |
+
Args:
|
| 173 |
+
input_channels: Number of input channels
|
| 174 |
+
output_channels: Number of output channels
|
| 175 |
+
conv_type: Convolution type, see satflow/models/utils.py for options
|
| 176 |
+
first_relu: Whether to have an ReLU before the first 3x3 convolution
|
| 177 |
+
keep_same_output: Whether the output should have the same spatial dimensions
|
| 178 |
+
as input, if False, downscales by 2
|
| 179 |
+
"""
|
| 180 |
+
super().__init__()
|
| 181 |
+
self.input_channels = input_channels
|
| 182 |
+
self.output_channels = output_channels
|
| 183 |
+
self.first_relu = first_relu
|
| 184 |
+
self.keep_same_output = keep_same_output
|
| 185 |
+
self.conv_type = conv_type
|
| 186 |
+
conv2d = get_conv_layer(conv_type)
|
| 187 |
+
if conv_type == "3d":
|
| 188 |
+
# 3D Average pooling
|
| 189 |
+
self.pooling = torch.nn.AvgPool3d(kernel_size=2, stride=2)
|
| 190 |
+
else:
|
| 191 |
+
self.pooling = torch.nn.AvgPool2d(kernel_size=2, stride=2)
|
| 192 |
+
self.conv_1x1 = spectral_norm(
|
| 193 |
+
conv2d(
|
| 194 |
+
in_channels=input_channels,
|
| 195 |
+
out_channels=output_channels,
|
| 196 |
+
kernel_size=1,
|
| 197 |
+
)
|
| 198 |
+
)
|
| 199 |
+
self.first_conv_3x3 = spectral_norm(
|
| 200 |
+
conv2d(
|
| 201 |
+
in_channels=input_channels,
|
| 202 |
+
out_channels=output_channels,
|
| 203 |
+
kernel_size=3,
|
| 204 |
+
padding=1,
|
| 205 |
+
)
|
| 206 |
+
)
|
| 207 |
+
self.last_conv_3x3 = spectral_norm(
|
| 208 |
+
conv2d(
|
| 209 |
+
in_channels=output_channels,
|
| 210 |
+
out_channels=output_channels,
|
| 211 |
+
kernel_size=3,
|
| 212 |
+
padding=1,
|
| 213 |
+
stride=1,
|
| 214 |
+
)
|
| 215 |
+
)
|
| 216 |
+
# Downsample at end of 3x3
|
| 217 |
+
self.relu = torch.nn.ReLU()
|
| 218 |
+
# Concatenate to double final channels and keep reduced spatial extent
|
| 219 |
+
|
| 220 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 221 |
+
"""Apply the D residual block."""
|
| 222 |
+
if self.input_channels != self.output_channels:
|
| 223 |
+
x1 = self.conv_1x1(x)
|
| 224 |
+
if not self.keep_same_output:
|
| 225 |
+
x1 = self.pooling(x1)
|
| 226 |
+
else:
|
| 227 |
+
x1 = x
|
| 228 |
+
|
| 229 |
+
if self.first_relu:
|
| 230 |
+
x = self.relu(x)
|
| 231 |
+
x = self.first_conv_3x3(x)
|
| 232 |
+
x = self.relu(x)
|
| 233 |
+
x = self.last_conv_3x3(x)
|
| 234 |
+
|
| 235 |
+
if not self.keep_same_output:
|
| 236 |
+
x = self.pooling(x)
|
| 237 |
+
x = x1 + x # Sum the outputs should be half spatial and double channels
|
| 238 |
+
return x
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
class LBlock(torch.nn.Module):
|
| 242 |
+
"""Residual block for the Latent Stack."""
|
| 243 |
+
|
| 244 |
+
def __init__(
|
| 245 |
+
self,
|
| 246 |
+
input_channels: int = 12,
|
| 247 |
+
output_channels: int = 12,
|
| 248 |
+
kernel_size: int = 3,
|
| 249 |
+
conv_type: str = "standard",
|
| 250 |
+
):
|
| 251 |
+
"""
|
| 252 |
+
Initialize the L-block.
|
| 253 |
+
|
| 254 |
+
L-Block for increasing the number of channels in the input
|
| 255 |
+
from Skillful Nowcasting, see https://arxiv.org/pdf/2104.00954.pdf
|
| 256 |
+
Args:
|
| 257 |
+
input_channels: Number of input channels
|
| 258 |
+
output_channels: Number of output channels
|
| 259 |
+
conv_type: Which type of convolution desired, see satflow/models/utils.py for options
|
| 260 |
+
"""
|
| 261 |
+
super().__init__()
|
| 262 |
+
# Output size should be channel_out - channel_in
|
| 263 |
+
self.input_channels = input_channels
|
| 264 |
+
self.output_channels = output_channels
|
| 265 |
+
conv2d = get_conv_layer(conv_type)
|
| 266 |
+
self.conv_1x1 = conv2d(
|
| 267 |
+
in_channels=input_channels,
|
| 268 |
+
out_channels=output_channels - input_channels,
|
| 269 |
+
kernel_size=1,
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
self.first_conv_3x3 = conv2d(
|
| 273 |
+
input_channels,
|
| 274 |
+
out_channels=output_channels,
|
| 275 |
+
kernel_size=kernel_size,
|
| 276 |
+
padding=1,
|
| 277 |
+
stride=1,
|
| 278 |
+
)
|
| 279 |
+
self.relu = torch.nn.ReLU()
|
| 280 |
+
self.last_conv_3x3 = conv2d(
|
| 281 |
+
in_channels=output_channels,
|
| 282 |
+
out_channels=output_channels,
|
| 283 |
+
kernel_size=kernel_size,
|
| 284 |
+
padding=1,
|
| 285 |
+
stride=1,
|
| 286 |
+
)
|
| 287 |
+
|
| 288 |
+
def forward(self, x) -> torch.Tensor:
|
| 289 |
+
"""Apply the L residual block to this tensor."""
|
| 290 |
+
if self.input_channels < self.output_channels:
|
| 291 |
+
sc = self.conv_1x1(x)
|
| 292 |
+
sc = torch.cat([x, sc], dim=1)
|
| 293 |
+
else:
|
| 294 |
+
sc = x
|
| 295 |
+
|
| 296 |
+
x2 = self.relu(x)
|
| 297 |
+
x2 = self.first_conv_3x3(x2)
|
| 298 |
+
x2 = self.relu(x2)
|
| 299 |
+
x2 = self.last_conv_3x3(x2)
|
| 300 |
+
return x2 + sc
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
class ContextConditioningStack(torch.nn.Module):
|
| 304 |
+
"""Context conditioning stack."""
|
| 305 |
+
|
| 306 |
+
def __init__(
|
| 307 |
+
self,
|
| 308 |
+
input_channels: int = 1,
|
| 309 |
+
output_channels: int = 768,
|
| 310 |
+
num_context_steps: int = 4,
|
| 311 |
+
conv_type: str = "standard",
|
| 312 |
+
):
|
| 313 |
+
"""
|
| 314 |
+
Conditioning Stack using the context images from Skillful Nowcasting, see https://arxiv.org/pdf/2104.00954.pdf.
|
| 315 |
+
|
| 316 |
+
Args:
|
| 317 |
+
input_channels: Number of input channels per timestep
|
| 318 |
+
output_channels: Number of output channels for the lowest block
|
| 319 |
+
num_context_steps: number of context steps (int)
|
| 320 |
+
conv_type: Type of 2D convolution to use, see satflow/models/utils.py for options
|
| 321 |
+
**kwargs: Allow initialize of the parameters above through key pairs
|
| 322 |
+
"""
|
| 323 |
+
super().__init__()
|
| 324 |
+
|
| 325 |
+
conv2d = get_conv_layer(conv_type)
|
| 326 |
+
self.space2depth = PixelUnshuffle(downscale_factor=2)
|
| 327 |
+
# Process each observation processed separately with 4 downsample blocks
|
| 328 |
+
# Concatenate across channel dimension, and for each output, 3x3 spectrally
|
| 329 |
+
# normalized convolution to reduce number of channels by 2, followed by ReLU
|
| 330 |
+
self.d1 = DBlock(
|
| 331 |
+
input_channels=4 * input_channels,
|
| 332 |
+
output_channels=((output_channels // 4) * input_channels) // num_context_steps,
|
| 333 |
+
conv_type=conv_type,
|
| 334 |
+
)
|
| 335 |
+
self.d2 = DBlock(
|
| 336 |
+
input_channels=((output_channels // 4) * input_channels) // num_context_steps,
|
| 337 |
+
output_channels=((output_channels // 2) * input_channels) // num_context_steps,
|
| 338 |
+
conv_type=conv_type,
|
| 339 |
+
)
|
| 340 |
+
self.d3 = DBlock(
|
| 341 |
+
input_channels=((output_channels // 2) * input_channels) // num_context_steps,
|
| 342 |
+
output_channels=(output_channels * input_channels) // num_context_steps,
|
| 343 |
+
conv_type=conv_type,
|
| 344 |
+
)
|
| 345 |
+
self.d4 = DBlock(
|
| 346 |
+
input_channels=(output_channels * input_channels) // num_context_steps,
|
| 347 |
+
output_channels=(output_channels * 2 * input_channels) // num_context_steps,
|
| 348 |
+
conv_type=conv_type,
|
| 349 |
+
)
|
| 350 |
+
self.conv1 = spectral_norm(
|
| 351 |
+
conv2d(
|
| 352 |
+
in_channels=(output_channels // 4) * input_channels,
|
| 353 |
+
out_channels=(output_channels // 8) * input_channels,
|
| 354 |
+
kernel_size=3,
|
| 355 |
+
padding=1,
|
| 356 |
+
)
|
| 357 |
+
)
|
| 358 |
+
|
| 359 |
+
self.conv2 = spectral_norm(
|
| 360 |
+
conv2d(
|
| 361 |
+
in_channels=(output_channels // 2) * input_channels,
|
| 362 |
+
out_channels=(output_channels // 4) * input_channels,
|
| 363 |
+
kernel_size=3,
|
| 364 |
+
padding=1,
|
| 365 |
+
)
|
| 366 |
+
)
|
| 367 |
+
|
| 368 |
+
self.conv3 = spectral_norm(
|
| 369 |
+
conv2d(
|
| 370 |
+
in_channels=output_channels * input_channels,
|
| 371 |
+
out_channels=(output_channels // 2) * input_channels,
|
| 372 |
+
kernel_size=3,
|
| 373 |
+
padding=1,
|
| 374 |
+
)
|
| 375 |
+
)
|
| 376 |
+
|
| 377 |
+
self.conv4 = spectral_norm(
|
| 378 |
+
conv2d(
|
| 379 |
+
in_channels=output_channels * 2 * input_channels,
|
| 380 |
+
out_channels=output_channels * input_channels,
|
| 381 |
+
kernel_size=3,
|
| 382 |
+
padding=1,
|
| 383 |
+
)
|
| 384 |
+
)
|
| 385 |
+
|
| 386 |
+
self.relu = torch.nn.ReLU()
|
| 387 |
+
|
| 388 |
+
def forward(
|
| 389 |
+
self, x: torch.Tensor
|
| 390 |
+
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 391 |
+
"""Generate the condition representation."""
|
| 392 |
+
# Each timestep processed separately
|
| 393 |
+
x = self.space2depth(x)
|
| 394 |
+
steps = x.size(1) # Number of timesteps
|
| 395 |
+
scale_1 = []
|
| 396 |
+
scale_2 = []
|
| 397 |
+
scale_3 = []
|
| 398 |
+
scale_4 = []
|
| 399 |
+
for i in range(steps):
|
| 400 |
+
s1 = self.d1(x[:, i, :, :, :])
|
| 401 |
+
s2 = self.d2(s1)
|
| 402 |
+
s3 = self.d3(s2)
|
| 403 |
+
s4 = self.d4(s3)
|
| 404 |
+
scale_1.append(s1)
|
| 405 |
+
scale_2.append(s2)
|
| 406 |
+
scale_3.append(s3)
|
| 407 |
+
scale_4.append(s4)
|
| 408 |
+
scale_1 = torch.stack(scale_1, dim=1) # B, T, C, H, W and want along C dimension
|
| 409 |
+
scale_2 = torch.stack(scale_2, dim=1) # B, T, C, H, W and want along C dimension
|
| 410 |
+
scale_3 = torch.stack(scale_3, dim=1) # B, T, C, H, W and want along C dimension
|
| 411 |
+
scale_4 = torch.stack(scale_4, dim=1) # B, T, C, H, W and want along C dimension
|
| 412 |
+
# Mixing layer
|
| 413 |
+
scale_1 = self._mixing_layer(scale_1, self.conv1)
|
| 414 |
+
scale_2 = self._mixing_layer(scale_2, self.conv2)
|
| 415 |
+
scale_3 = self._mixing_layer(scale_3, self.conv3)
|
| 416 |
+
scale_4 = self._mixing_layer(scale_4, self.conv4)
|
| 417 |
+
return scale_1, scale_2, scale_3, scale_4
|
| 418 |
+
|
| 419 |
+
def _mixing_layer(self, inputs, conv_block):
|
| 420 |
+
"""Combine the inputs and then passed into the convolution stack."""
|
| 421 |
+
# Convert from [batch_size, time, h, w, c] -> [batch_size, h, w, c * time]
|
| 422 |
+
# then perform convolution on the output while preserving number of c.
|
| 423 |
+
stacked_inputs = einops.rearrange(inputs, "b t c h w -> b (c t) h w")
|
| 424 |
+
return F.relu(conv_block(stacked_inputs))
|
| 425 |
+
|
| 426 |
+
|
| 427 |
+
class LatentConditioningStack(torch.nn.Module):
|
| 428 |
+
"""Latent conditioning stack class."""
|
| 429 |
+
|
| 430 |
+
def __init__(
|
| 431 |
+
self,
|
| 432 |
+
shape: (int, int, int) = (8, 8, 8),
|
| 433 |
+
output_channels: int = 768,
|
| 434 |
+
use_attention: bool = True,
|
| 435 |
+
):
|
| 436 |
+
"""
|
| 437 |
+
Latent conditioning stack from Skillful Nowcasting, see https://arxiv.org/pdf/2104.00954.pdf.
|
| 438 |
+
|
| 439 |
+
Args:
|
| 440 |
+
shape: Shape of the latent space, Should be (H/32,W/32,x) of the final image shape
|
| 441 |
+
output_channels: Number of output channels for the conditioning stack
|
| 442 |
+
use_attention: Whether to have a self-attention block or not
|
| 443 |
+
**kwargs: allow initialize of the parameters above through key pairs
|
| 444 |
+
"""
|
| 445 |
+
super().__init__()
|
| 446 |
+
|
| 447 |
+
self.shape = shape
|
| 448 |
+
self.use_attention = use_attention
|
| 449 |
+
self.distribution = normal.Normal(loc=torch.Tensor([0.0]), scale=torch.Tensor([1.0]))
|
| 450 |
+
|
| 451 |
+
self.conv_3x3 = spectral_norm(
|
| 452 |
+
torch.nn.Conv2d(
|
| 453 |
+
in_channels=shape[0], out_channels=shape[0], kernel_size=(3, 3), padding=1
|
| 454 |
+
)
|
| 455 |
+
)
|
| 456 |
+
self.l_block1 = LBlock(input_channels=shape[0], output_channels=output_channels // 32)
|
| 457 |
+
self.l_block2 = LBlock(
|
| 458 |
+
input_channels=output_channels // 32, output_channels=output_channels // 16
|
| 459 |
+
)
|
| 460 |
+
self.l_block3 = LBlock(
|
| 461 |
+
input_channels=output_channels // 16, output_channels=output_channels // 4
|
| 462 |
+
)
|
| 463 |
+
if self.use_attention:
|
| 464 |
+
self.att_block = AttentionLayer(
|
| 465 |
+
input_channels=output_channels // 4, output_channels=output_channels // 4
|
| 466 |
+
)
|
| 467 |
+
self.l_block4 = LBlock(input_channels=output_channels // 4, output_channels=output_channels)
|
| 468 |
+
|
| 469 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 470 |
+
"""
|
| 471 |
+
Apply convolution, l blocks and spatial attention module to the tensor.
|
| 472 |
+
|
| 473 |
+
Args:
|
| 474 |
+
x: tensor on the correct device, to move over the latent distribution
|
| 475 |
+
|
| 476 |
+
Returns:
|
| 477 |
+
tensor
|
| 478 |
+
|
| 479 |
+
"""
|
| 480 |
+
# Independent draws from Norma ldistribution
|
| 481 |
+
z = self.distribution.sample(self.shape)
|
| 482 |
+
# Batch is at end for some reason, reshape
|
| 483 |
+
z = torch.permute(z, (3, 0, 1, 2)).type_as(x)
|
| 484 |
+
|
| 485 |
+
# 3x3 Convolution
|
| 486 |
+
z = self.conv_3x3(z)
|
| 487 |
+
|
| 488 |
+
# 3 L Blocks to increase number of channels
|
| 489 |
+
z = self.l_block1(z)
|
| 490 |
+
z = self.l_block2(z)
|
| 491 |
+
z = self.l_block3(z)
|
| 492 |
+
# Spatial attention module
|
| 493 |
+
z = self.att_block(z)
|
| 494 |
+
|
| 495 |
+
# L block to increase number of channel to 768
|
| 496 |
+
z = self.l_block4(z)
|
| 497 |
+
return z
|
model/dgmr_official/discriminators.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Discriminators."""
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn.functional as F
|
| 5 |
+
|
| 6 |
+
from torch.nn.modules.pixelshuffle import PixelUnshuffle
|
| 7 |
+
from torch.nn.utils.parametrizations import spectral_norm
|
| 8 |
+
|
| 9 |
+
from .common import DBlock
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class Discriminator(torch.nn.Module):
|
| 13 |
+
"""Discriminators class."""
|
| 14 |
+
|
| 15 |
+
def __init__(
|
| 16 |
+
self,
|
| 17 |
+
input_channels: int = 12,
|
| 18 |
+
num_spatial_frames: int = 8,
|
| 19 |
+
conv_type: str = "standard",
|
| 20 |
+
):
|
| 21 |
+
"""
|
| 22 |
+
Initialize the discriminator.
|
| 23 |
+
|
| 24 |
+
Args:
|
| 25 |
+
input_channels: Number of input channels (int)
|
| 26 |
+
num_spatial_frames: Number of spatial frames (int)
|
| 27 |
+
conv_type: the specified convolution type (str)
|
| 28 |
+
**kwargs: allow initialize of the parameters above through key pairs
|
| 29 |
+
"""
|
| 30 |
+
super().__init__()
|
| 31 |
+
|
| 32 |
+
self.spatial_discriminator = SpatialDiscriminator(
|
| 33 |
+
input_channels=input_channels, num_timesteps=num_spatial_frames, conv_type=conv_type
|
| 34 |
+
)
|
| 35 |
+
self.temporal_discriminator = TemporalDiscriminator(
|
| 36 |
+
input_channels=input_channels, conv_type=conv_type
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 40 |
+
"""Mixes the spatial loss and temporal loss of the tensor prior to returning it."""
|
| 41 |
+
spatial_loss = self.spatial_discriminator(x)
|
| 42 |
+
temporal_loss = self.temporal_discriminator(x)
|
| 43 |
+
|
| 44 |
+
return torch.cat([spatial_loss, temporal_loss], dim=1)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class TemporalDiscriminator(torch.nn.Module):
|
| 48 |
+
"""Temporal Discriminator class."""
|
| 49 |
+
|
| 50 |
+
def __init__(
|
| 51 |
+
self,
|
| 52 |
+
input_channels: int = 12,
|
| 53 |
+
num_layers: int = 3,
|
| 54 |
+
conv_type: str = "standard",
|
| 55 |
+
):
|
| 56 |
+
"""
|
| 57 |
+
Temporal Discriminator from the Skillful Nowcasting, see https://arxiv.org/pdf/2104.00954.pdf.
|
| 58 |
+
|
| 59 |
+
Args:
|
| 60 |
+
input_channels: Number of channels per timestep
|
| 61 |
+
crop_size: Size of the crop, in the paper half the width of the input images
|
| 62 |
+
num_layers: Number of intermediate DBlock layers to use
|
| 63 |
+
conv_type: Type of 2d convolutions to use, see satflow/models/utils.py for options
|
| 64 |
+
**kwargs: allow initialize of the parameters above through key pairs
|
| 65 |
+
"""
|
| 66 |
+
super().__init__()
|
| 67 |
+
|
| 68 |
+
self.downsample = torch.nn.AvgPool3d(kernel_size=(1, 2, 2), stride=(1, 2, 2))
|
| 69 |
+
self.space2depth = PixelUnshuffle(downscale_factor=2)
|
| 70 |
+
internal_chn = 48
|
| 71 |
+
self.d1 = DBlock(
|
| 72 |
+
input_channels=4 * input_channels,
|
| 73 |
+
output_channels=internal_chn * input_channels,
|
| 74 |
+
conv_type="3d",
|
| 75 |
+
first_relu=False,
|
| 76 |
+
)
|
| 77 |
+
self.d2 = DBlock(
|
| 78 |
+
input_channels=internal_chn * input_channels,
|
| 79 |
+
output_channels=2 * internal_chn * input_channels,
|
| 80 |
+
conv_type="3d",
|
| 81 |
+
)
|
| 82 |
+
self.intermediate_dblocks = torch.nn.ModuleList()
|
| 83 |
+
for _ in range(num_layers):
|
| 84 |
+
internal_chn *= 2
|
| 85 |
+
self.intermediate_dblocks.append(
|
| 86 |
+
DBlock(
|
| 87 |
+
input_channels=internal_chn * input_channels,
|
| 88 |
+
output_channels=2 * internal_chn * input_channels,
|
| 89 |
+
conv_type=conv_type,
|
| 90 |
+
)
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
self.d_last = DBlock(
|
| 94 |
+
input_channels=2 * internal_chn * input_channels,
|
| 95 |
+
output_channels=2 * internal_chn * input_channels,
|
| 96 |
+
keep_same_output=True,
|
| 97 |
+
conv_type=conv_type,
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
self.fc = spectral_norm(torch.nn.Linear(2 * internal_chn * input_channels, 1))
|
| 101 |
+
self.relu = torch.nn.ReLU()
|
| 102 |
+
self.bn = torch.nn.BatchNorm1d(2 * internal_chn * input_channels)
|
| 103 |
+
|
| 104 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 105 |
+
"""Apply the forward function."""
|
| 106 |
+
x = self.downsample(x)
|
| 107 |
+
|
| 108 |
+
x = self.space2depth(x)
|
| 109 |
+
# Have to move time and channels
|
| 110 |
+
x = torch.permute(x, dims=(0, 2, 1, 3, 4))
|
| 111 |
+
# 2 residual 3D blocks to halve resolution if image, double number of channels and reduce
|
| 112 |
+
# number of time steps
|
| 113 |
+
x = self.d1(x)
|
| 114 |
+
x = self.d2(x)
|
| 115 |
+
# Convert back to T x C x H x W
|
| 116 |
+
x = torch.permute(x, dims=(0, 2, 1, 3, 4))
|
| 117 |
+
# Per Timestep part now, same as spatial discriminator
|
| 118 |
+
representations = []
|
| 119 |
+
for idx in range(x.size(1)):
|
| 120 |
+
# Intermediate DBlocks
|
| 121 |
+
# Three residual D Blocks to halve the resolution of the image and double
|
| 122 |
+
# the number of channels.
|
| 123 |
+
rep = x[:, idx, :, :, :]
|
| 124 |
+
for d in self.intermediate_dblocks:
|
| 125 |
+
rep = d(rep)
|
| 126 |
+
# One more D Block without downsampling or increase number of channels
|
| 127 |
+
rep = self.d_last(rep)
|
| 128 |
+
|
| 129 |
+
rep = torch.sum(F.relu(rep), dim=[2, 3])
|
| 130 |
+
rep = self.bn(rep)
|
| 131 |
+
rep = self.fc(rep)
|
| 132 |
+
|
| 133 |
+
representations.append(rep)
|
| 134 |
+
# The representations are summed together before the ReLU
|
| 135 |
+
x = torch.stack(representations, dim=1)
|
| 136 |
+
# Should be [Batch, N, 1]
|
| 137 |
+
x = torch.sum(x, keepdim=True, dim=1)
|
| 138 |
+
return x
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
class SpatialDiscriminator(torch.nn.Module):
|
| 142 |
+
"""Spatial Discriminator class."""
|
| 143 |
+
|
| 144 |
+
def __init__(
|
| 145 |
+
self,
|
| 146 |
+
input_channels: int = 12,
|
| 147 |
+
num_timesteps: int = 8,
|
| 148 |
+
num_layers: int = 4,
|
| 149 |
+
conv_type: str = "standard",
|
| 150 |
+
):
|
| 151 |
+
"""
|
| 152 |
+
Spatial discriminator from Skillful Nowcasting, see https://arxiv.org/pdf/2104.00954.pdf.
|
| 153 |
+
|
| 154 |
+
Args:
|
| 155 |
+
input_channels: Number of input channels per timestep
|
| 156 |
+
num_timesteps: Number of timesteps to use, in the paper 8/18 timesteps were chosen
|
| 157 |
+
num_layers: Number of intermediate DBlock layers to use
|
| 158 |
+
conv_type: Type of 2d convolutions to use, see satflow/models/utils.py for options
|
| 159 |
+
**kwargs: allow initialize of the parameters above through key pairs
|
| 160 |
+
"""
|
| 161 |
+
super().__init__()
|
| 162 |
+
# Randomly, uniformly, select 8 timesteps to do this on from the input
|
| 163 |
+
self.num_timesteps = num_timesteps
|
| 164 |
+
# First step is mean pooling 2x2 to reduce input by half
|
| 165 |
+
self.mean_pool = torch.nn.AvgPool2d(2)
|
| 166 |
+
self.space2depth = PixelUnshuffle(downscale_factor=2)
|
| 167 |
+
internal_chn = 24
|
| 168 |
+
self.d1 = DBlock(
|
| 169 |
+
input_channels=4 * input_channels,
|
| 170 |
+
output_channels=2 * internal_chn * input_channels,
|
| 171 |
+
first_relu=False,
|
| 172 |
+
conv_type=conv_type,
|
| 173 |
+
)
|
| 174 |
+
self.intermediate_dblocks = torch.nn.ModuleList()
|
| 175 |
+
for _ in range(num_layers):
|
| 176 |
+
internal_chn *= 2
|
| 177 |
+
self.intermediate_dblocks.append(
|
| 178 |
+
DBlock(
|
| 179 |
+
input_channels=internal_chn * input_channels,
|
| 180 |
+
output_channels=2 * internal_chn * input_channels,
|
| 181 |
+
conv_type=conv_type,
|
| 182 |
+
)
|
| 183 |
+
)
|
| 184 |
+
self.d6 = DBlock(
|
| 185 |
+
input_channels=2 * internal_chn * input_channels,
|
| 186 |
+
output_channels=2 * internal_chn * input_channels,
|
| 187 |
+
keep_same_output=True,
|
| 188 |
+
conv_type=conv_type,
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
# Spectrally normalized linear layer for binary classification
|
| 192 |
+
self.fc = spectral_norm(torch.nn.Linear(2 * internal_chn * input_channels, 1))
|
| 193 |
+
self.relu = torch.nn.ReLU()
|
| 194 |
+
self.bn = torch.nn.BatchNorm1d(2 * internal_chn * input_channels)
|
| 195 |
+
|
| 196 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 197 |
+
"""Apply the forward function to the tensor."""
|
| 198 |
+
# x should be the chosen 8 or so
|
| 199 |
+
idxs = torch.randint(low=0, high=x.size()[1], size=(self.num_timesteps,))
|
| 200 |
+
representations = []
|
| 201 |
+
for idx in idxs:
|
| 202 |
+
rep = self.mean_pool(x[:, idx, :, :, :]) # 128x128
|
| 203 |
+
rep = self.space2depth(rep) # 64x64x4
|
| 204 |
+
rep = self.d1(rep) # 32x32
|
| 205 |
+
# Intermediate DBlocks
|
| 206 |
+
for d in self.intermediate_dblocks:
|
| 207 |
+
rep = d(rep)
|
| 208 |
+
rep = self.d6(rep) # 2x2
|
| 209 |
+
rep = torch.sum(F.relu(rep), dim=[2, 3])
|
| 210 |
+
rep = self.bn(rep)
|
| 211 |
+
rep = self.fc(rep)
|
| 212 |
+
"""
|
| 213 |
+
Pseudocode from DeepMind
|
| 214 |
+
# Sum-pool the representations and feed to spectrally normalized lin. layer.
|
| 215 |
+
y = tf.reduce_sum(tf.nn.relu(y), axis=[1, 2])
|
| 216 |
+
y = layers.BatchNorm(calc_sigma=False)(y)
|
| 217 |
+
output_layer = layers.Linear(output_size=1)
|
| 218 |
+
output = output_layer(y)
|
| 219 |
+
|
| 220 |
+
# Take the sum across the t samples. Note: we apply the ReLU to
|
| 221 |
+
# (1 - score_real) and (1 + score_generated) in the loss.
|
| 222 |
+
output = tf.reshape(output, [b, n, 1])
|
| 223 |
+
output = tf.reduce_sum(output, keepdims=True, axis=1)
|
| 224 |
+
return output
|
| 225 |
+
"""
|
| 226 |
+
representations.append(rep)
|
| 227 |
+
|
| 228 |
+
# The representations are summed together before the ReLU
|
| 229 |
+
x = torch.stack(representations, dim=1)
|
| 230 |
+
# Should be [Batch, N, 1]
|
| 231 |
+
x = torch.sum(x, keepdim=True, dim=1)
|
| 232 |
+
return x
|
model/dgmr_official/generators.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Generators."""
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
from typing import List
|
| 5 |
+
|
| 6 |
+
import einops
|
| 7 |
+
import torch
|
| 8 |
+
import torch.nn.functional as F
|
| 9 |
+
|
| 10 |
+
from torch.nn.modules.pixelshuffle import PixelShuffle
|
| 11 |
+
from torch.nn.utils.parametrizations import spectral_norm
|
| 12 |
+
|
| 13 |
+
from .common import GBlock, UpsampleGBlock
|
| 14 |
+
from .layers import ConvGRU
|
| 15 |
+
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
+
logger.setLevel(logging.WARN)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class Sampler(torch.nn.Module):
|
| 21 |
+
"""Sampler class."""
|
| 22 |
+
|
| 23 |
+
def __init__(
|
| 24 |
+
self,
|
| 25 |
+
forecast_steps: int = 18,
|
| 26 |
+
latent_channels: int = 768,
|
| 27 |
+
context_channels: int = 384,
|
| 28 |
+
output_channels: int = 1,
|
| 29 |
+
):
|
| 30 |
+
"""
|
| 31 |
+
Sampler from the Skillful Nowcasting, see https://arxiv.org/pdf/2104.00954.pdf.
|
| 32 |
+
|
| 33 |
+
The sampler takes the output from the Latent and Context conditioning stacks and
|
| 34 |
+
creates one stack of ConvGRU layers per future timestep.
|
| 35 |
+
|
| 36 |
+
Args:
|
| 37 |
+
forecast_steps: Number of forecast steps (int)
|
| 38 |
+
latent_channels: Number of input channels to the lowest ConvGRU layer (int)
|
| 39 |
+
context_channels: Number of context channels (int)
|
| 40 |
+
output_channels: Number of output channels (int)
|
| 41 |
+
**kwargs: allow initialize of the parameters above through key pairs
|
| 42 |
+
"""
|
| 43 |
+
super().__init__()
|
| 44 |
+
|
| 45 |
+
self.forecast_steps = forecast_steps
|
| 46 |
+
|
| 47 |
+
self.convGRU1 = ConvGRU(
|
| 48 |
+
input_channels=latent_channels + context_channels,
|
| 49 |
+
output_channels=context_channels,
|
| 50 |
+
kernel_size=3,
|
| 51 |
+
)
|
| 52 |
+
self.gru_conv_1x1 = spectral_norm(
|
| 53 |
+
torch.nn.Conv2d(
|
| 54 |
+
in_channels=context_channels, out_channels=latent_channels, kernel_size=(1, 1)
|
| 55 |
+
)
|
| 56 |
+
)
|
| 57 |
+
self.g1 = GBlock(input_channels=latent_channels, output_channels=latent_channels)
|
| 58 |
+
self.up_g1 = UpsampleGBlock(
|
| 59 |
+
input_channels=latent_channels, output_channels=latent_channels // 2
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
self.convGRU2 = ConvGRU(
|
| 63 |
+
input_channels=latent_channels // 2 + context_channels // 2,
|
| 64 |
+
output_channels=context_channels // 2,
|
| 65 |
+
kernel_size=3,
|
| 66 |
+
)
|
| 67 |
+
self.gru_conv_1x1_2 = spectral_norm(
|
| 68 |
+
torch.nn.Conv2d(
|
| 69 |
+
in_channels=context_channels // 2,
|
| 70 |
+
out_channels=latent_channels // 2,
|
| 71 |
+
kernel_size=(1, 1),
|
| 72 |
+
)
|
| 73 |
+
)
|
| 74 |
+
self.g2 = GBlock(input_channels=latent_channels // 2, output_channels=latent_channels // 2)
|
| 75 |
+
self.up_g2 = UpsampleGBlock(
|
| 76 |
+
input_channels=latent_channels // 2, output_channels=latent_channels // 4
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
self.convGRU3 = ConvGRU(
|
| 80 |
+
input_channels=latent_channels // 4 + context_channels // 4,
|
| 81 |
+
output_channels=context_channels // 4,
|
| 82 |
+
kernel_size=3,
|
| 83 |
+
)
|
| 84 |
+
self.gru_conv_1x1_3 = spectral_norm(
|
| 85 |
+
torch.nn.Conv2d(
|
| 86 |
+
in_channels=context_channels // 4,
|
| 87 |
+
out_channels=latent_channels // 4,
|
| 88 |
+
kernel_size=(1, 1),
|
| 89 |
+
)
|
| 90 |
+
)
|
| 91 |
+
self.g3 = GBlock(input_channels=latent_channels // 4, output_channels=latent_channels // 4)
|
| 92 |
+
self.up_g3 = UpsampleGBlock(
|
| 93 |
+
input_channels=latent_channels // 4, output_channels=latent_channels // 8
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
self.convGRU4 = ConvGRU(
|
| 97 |
+
input_channels=latent_channels // 8 + context_channels // 8,
|
| 98 |
+
output_channels=context_channels // 8,
|
| 99 |
+
kernel_size=3,
|
| 100 |
+
)
|
| 101 |
+
self.gru_conv_1x1_4 = spectral_norm(
|
| 102 |
+
torch.nn.Conv2d(
|
| 103 |
+
in_channels=context_channels // 8,
|
| 104 |
+
out_channels=latent_channels // 8,
|
| 105 |
+
kernel_size=(1, 1),
|
| 106 |
+
)
|
| 107 |
+
)
|
| 108 |
+
self.g4 = GBlock(input_channels=latent_channels // 8, output_channels=latent_channels // 8)
|
| 109 |
+
self.up_g4 = UpsampleGBlock(
|
| 110 |
+
input_channels=latent_channels // 8, output_channels=latent_channels // 16
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
self.bn = torch.nn.BatchNorm2d(latent_channels // 16)
|
| 114 |
+
self.relu = torch.nn.ReLU()
|
| 115 |
+
self.conv_1x1 = spectral_norm(
|
| 116 |
+
torch.nn.Conv2d(
|
| 117 |
+
in_channels=latent_channels // 16,
|
| 118 |
+
out_channels=4 * output_channels,
|
| 119 |
+
kernel_size=(1, 1),
|
| 120 |
+
)
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
self.depth2space = PixelShuffle(upscale_factor=2)
|
| 124 |
+
|
| 125 |
+
def forward(
|
| 126 |
+
self, conditioning_states: List[torch.Tensor], latent_dim: torch.Tensor
|
| 127 |
+
) -> torch.Tensor:
|
| 128 |
+
"""
|
| 129 |
+
Perform the sampling from Skillful Nowcasting with GANs.
|
| 130 |
+
|
| 131 |
+
Args:
|
| 132 |
+
conditioning_states: Outputs from the `ContextConditioningStack` with the 4 input
|
| 133 |
+
states, ordered from largest to smallest spatially latent_dim: Output from
|
| 134 |
+
`LatentConditioningStack` for input into the ConvGRUs
|
| 135 |
+
|
| 136 |
+
latent_dim: (torch.Tensor)
|
| 137 |
+
|
| 138 |
+
Returns:
|
| 139 |
+
forecast_steps-length output of images for future timesteps
|
| 140 |
+
|
| 141 |
+
"""
|
| 142 |
+
# Iterate through each forecast step
|
| 143 |
+
# Initialize with conditioning state for first one, output for second one
|
| 144 |
+
init_states = conditioning_states
|
| 145 |
+
# Expand latent dim to match batch size
|
| 146 |
+
latent_dim = einops.repeat(
|
| 147 |
+
latent_dim, "b c h w -> (repeat b) c h w", repeat=init_states[0].shape[0]
|
| 148 |
+
)
|
| 149 |
+
hidden_states = [latent_dim] * self.forecast_steps
|
| 150 |
+
|
| 151 |
+
# Layer 4 (bottom most)
|
| 152 |
+
hidden_states = self.convGRU1(hidden_states, init_states[3])
|
| 153 |
+
hidden_states = [self.gru_conv_1x1(h) for h in hidden_states]
|
| 154 |
+
hidden_states = [self.g1(h) for h in hidden_states]
|
| 155 |
+
hidden_states = [self.up_g1(h) for h in hidden_states]
|
| 156 |
+
|
| 157 |
+
# Layer 3.
|
| 158 |
+
hidden_states = self.convGRU2(hidden_states, init_states[2])
|
| 159 |
+
hidden_states = [self.gru_conv_1x1_2(h) for h in hidden_states]
|
| 160 |
+
hidden_states = [self.g2(h) for h in hidden_states]
|
| 161 |
+
hidden_states = [self.up_g2(h) for h in hidden_states]
|
| 162 |
+
|
| 163 |
+
# Layer 2.
|
| 164 |
+
hidden_states = self.convGRU3(hidden_states, init_states[1])
|
| 165 |
+
hidden_states = [self.gru_conv_1x1_3(h) for h in hidden_states]
|
| 166 |
+
hidden_states = [self.g3(h) for h in hidden_states]
|
| 167 |
+
hidden_states = [self.up_g3(h) for h in hidden_states]
|
| 168 |
+
|
| 169 |
+
# Layer 1 (top-most).
|
| 170 |
+
hidden_states = self.convGRU4(hidden_states, init_states[0])
|
| 171 |
+
hidden_states = [self.gru_conv_1x1_4(h) for h in hidden_states]
|
| 172 |
+
hidden_states = [self.g4(h) for h in hidden_states]
|
| 173 |
+
hidden_states = [self.up_g4(h) for h in hidden_states]
|
| 174 |
+
|
| 175 |
+
# Output layer.
|
| 176 |
+
hidden_states = [F.relu(self.bn(h)) for h in hidden_states]
|
| 177 |
+
hidden_states = [self.conv_1x1(h) for h in hidden_states]
|
| 178 |
+
hidden_states = [self.depth2space(h) for h in hidden_states]
|
| 179 |
+
|
| 180 |
+
# Convert forecasts to a torch Tensor
|
| 181 |
+
forecasts = torch.stack(hidden_states, dim=1)
|
| 182 |
+
return forecasts
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
class Generator(torch.nn.Module):
|
| 186 |
+
"""Generator class."""
|
| 187 |
+
|
| 188 |
+
def __init__(
|
| 189 |
+
self,
|
| 190 |
+
conditioning_stack: torch.nn.Module,
|
| 191 |
+
latent_stack: torch.nn.Module,
|
| 192 |
+
sampler: torch.nn.Module,
|
| 193 |
+
):
|
| 194 |
+
"""
|
| 195 |
+
Wrap the three parts of the generator for simpler calling.
|
| 196 |
+
|
| 197 |
+
Args:
|
| 198 |
+
conditioning_stack: (torch.nn.Module)
|
| 199 |
+
latent_stack: (torch.nn.Module)
|
| 200 |
+
sampler: Combines the conditioning information and latent information (torch.nn.Module)
|
| 201 |
+
"""
|
| 202 |
+
super().__init__()
|
| 203 |
+
self.conditioning_stack = conditioning_stack
|
| 204 |
+
self.latent_stack = latent_stack
|
| 205 |
+
self.sampler = sampler
|
| 206 |
+
|
| 207 |
+
def forward(self, x: torch.Tensor):
|
| 208 |
+
"""Apply a forward pass on the tensor."""
|
| 209 |
+
conditioning_states = self.conditioning_stack(x)
|
| 210 |
+
latent_dim = self.latent_stack(x)
|
| 211 |
+
x = self.sampler(conditioning_states, latent_dim)
|
| 212 |
+
return x
|
model/dgmr_official/layers/Attention.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Attention module."""
|
| 2 |
+
|
| 3 |
+
import einops
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn as nn
|
| 6 |
+
from torch.nn import functional as F
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def attention_einsum(q, k, v):
|
| 10 |
+
"""Apply the attention operator to tensors of shape [h, w, c]."""
|
| 11 |
+
# Reshape 3D tensors to 2D tensor with first dimension L = h x w.
|
| 12 |
+
k = einops.rearrange(k, "h w c -> (h w) c") # [h, w, c] -> [L, c]
|
| 13 |
+
v = einops.rearrange(v, "h w c -> (h w) c") # [h, w, c] -> [L, c]
|
| 14 |
+
|
| 15 |
+
# Einstein summation corresponding to the query * key operation.
|
| 16 |
+
beta = F.softmax(torch.einsum("hwc, Lc->hwL", q, k), dim=-1)
|
| 17 |
+
|
| 18 |
+
# Einstein summation corresponding to the attention * value operation.
|
| 19 |
+
out = torch.einsum("hwL, Lc->hwc", beta, v)
|
| 20 |
+
return out
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class AttentionLayer(torch.nn.Module):
|
| 24 |
+
"""Attention layer class."""
|
| 25 |
+
|
| 26 |
+
def __init__(
|
| 27 |
+
self, input_channels: int, output_channels: int, ratio_kq: int = 8, ratio_v: int = 8
|
| 28 |
+
):
|
| 29 |
+
"""Initialize the attention layer."""
|
| 30 |
+
super(AttentionLayer, self).__init__()
|
| 31 |
+
|
| 32 |
+
self.ratio_kq = ratio_kq
|
| 33 |
+
self.ratio_v = ratio_v
|
| 34 |
+
self.output_channels = output_channels
|
| 35 |
+
self.input_channels = input_channels
|
| 36 |
+
|
| 37 |
+
# Compute query, key and value using 1x1 convolutions.
|
| 38 |
+
self.query = torch.nn.Conv2d(
|
| 39 |
+
in_channels=input_channels,
|
| 40 |
+
out_channels=self.output_channels // self.ratio_kq,
|
| 41 |
+
kernel_size=(1, 1),
|
| 42 |
+
padding="valid",
|
| 43 |
+
bias=False,
|
| 44 |
+
)
|
| 45 |
+
self.key = torch.nn.Conv2d(
|
| 46 |
+
in_channels=input_channels,
|
| 47 |
+
out_channels=self.output_channels // self.ratio_kq,
|
| 48 |
+
kernel_size=(1, 1),
|
| 49 |
+
padding="valid",
|
| 50 |
+
bias=False,
|
| 51 |
+
)
|
| 52 |
+
self.value = torch.nn.Conv2d(
|
| 53 |
+
in_channels=input_channels,
|
| 54 |
+
out_channels=self.output_channels // self.ratio_v,
|
| 55 |
+
kernel_size=(1, 1),
|
| 56 |
+
padding="valid",
|
| 57 |
+
bias=False,
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
self.last_conv = torch.nn.Conv2d(
|
| 61 |
+
in_channels=self.output_channels // 8,
|
| 62 |
+
out_channels=self.output_channels,
|
| 63 |
+
kernel_size=(1, 1),
|
| 64 |
+
padding="valid",
|
| 65 |
+
bias=False,
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
# Learnable gain parameter
|
| 69 |
+
self.gamma = nn.Parameter(torch.zeros(1))
|
| 70 |
+
|
| 71 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 72 |
+
"""Apply the forward function."""
|
| 73 |
+
# Compute query, key and value using 1x1 convolutions.
|
| 74 |
+
query = self.query(x)
|
| 75 |
+
key = self.key(x)
|
| 76 |
+
value = self.value(x)
|
| 77 |
+
# Apply the attention operation.
|
| 78 |
+
out = []
|
| 79 |
+
for b in range(x.shape[0]):
|
| 80 |
+
# Apply to each in batch
|
| 81 |
+
out.append(attention_einsum(query[b], key[b], value[b]))
|
| 82 |
+
out = torch.stack(out, dim=0)
|
| 83 |
+
out = self.gamma * self.last_conv(out)
|
| 84 |
+
# Residual connection.
|
| 85 |
+
return out + x
|
model/dgmr_official/layers/ConvGRU.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Implementation of Conv GRU and cell module."""
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn.functional as F
|
| 5 |
+
from torch.nn.utils.parametrizations import spectral_norm
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class ConvGRUCell(torch.nn.Module):
|
| 9 |
+
"""A ConvGRU implementation."""
|
| 10 |
+
|
| 11 |
+
def __init__(
|
| 12 |
+
self,
|
| 13 |
+
input_channels: int,
|
| 14 |
+
output_channels: int,
|
| 15 |
+
kernel_size: int = 3,
|
| 16 |
+
sn_eps: float = 0.0001,
|
| 17 |
+
):
|
| 18 |
+
"""Conv GRU class.
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
input_channels: number of input channels (int)
|
| 22 |
+
output_channels: number of output channels (int)
|
| 23 |
+
kernel_size: kernel size of the convolutions. Default: 3.
|
| 24 |
+
sn_eps: constant for spectral normalization. Default: 1e-4.
|
| 25 |
+
"""
|
| 26 |
+
super().__init__()
|
| 27 |
+
self._kernel_size: int = kernel_size
|
| 28 |
+
self._sn_eps: float = sn_eps
|
| 29 |
+
self.read_gate_conv = spectral_norm(
|
| 30 |
+
torch.nn.Conv2d(
|
| 31 |
+
in_channels=input_channels,
|
| 32 |
+
out_channels=output_channels,
|
| 33 |
+
kernel_size=(kernel_size, kernel_size),
|
| 34 |
+
padding=1,
|
| 35 |
+
),
|
| 36 |
+
eps=sn_eps,
|
| 37 |
+
)
|
| 38 |
+
self.update_gate_conv = spectral_norm(
|
| 39 |
+
torch.nn.Conv2d(
|
| 40 |
+
in_channels=input_channels,
|
| 41 |
+
out_channels=output_channels,
|
| 42 |
+
kernel_size=(kernel_size, kernel_size),
|
| 43 |
+
padding=1,
|
| 44 |
+
),
|
| 45 |
+
eps=sn_eps,
|
| 46 |
+
)
|
| 47 |
+
self.output_conv = spectral_norm(
|
| 48 |
+
torch.nn.Conv2d(
|
| 49 |
+
in_channels=input_channels,
|
| 50 |
+
out_channels=output_channels,
|
| 51 |
+
kernel_size=(kernel_size, kernel_size),
|
| 52 |
+
padding=1,
|
| 53 |
+
),
|
| 54 |
+
eps=sn_eps,
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
def forward(self, x, prev_state):
|
| 58 |
+
"""
|
| 59 |
+
Conv GRU forward, returning the current+new state.
|
| 60 |
+
|
| 61 |
+
Args:
|
| 62 |
+
x: Input tensor
|
| 63 |
+
prev_state: Previous state
|
| 64 |
+
|
| 65 |
+
Returns:
|
| 66 |
+
New tensor plus the new state
|
| 67 |
+
"""
|
| 68 |
+
# Concatenate the inputs and previous state along the channel axis.
|
| 69 |
+
xh = torch.cat([x, prev_state], dim=1)
|
| 70 |
+
|
| 71 |
+
# Read gate of the GRU.
|
| 72 |
+
read_gate = F.sigmoid(self.read_gate_conv(xh))
|
| 73 |
+
|
| 74 |
+
# Update gate of the GRU.
|
| 75 |
+
update_gate = F.sigmoid(self.update_gate_conv(xh))
|
| 76 |
+
|
| 77 |
+
# Gate the inputs.
|
| 78 |
+
gated_input = torch.cat([x, read_gate * prev_state], dim=1)
|
| 79 |
+
|
| 80 |
+
# Gate the cell and state / outputs.
|
| 81 |
+
c = F.relu(self.output_conv(gated_input))
|
| 82 |
+
out = update_gate * prev_state + (1.0 - update_gate) * c
|
| 83 |
+
new_state = out
|
| 84 |
+
|
| 85 |
+
return out, new_state
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
class ConvGRU(torch.nn.Module):
|
| 89 |
+
"""ConvGRU Cell wrapper to replace tf.static_rnn in TF implementation."""
|
| 90 |
+
|
| 91 |
+
def __init__(
|
| 92 |
+
self,
|
| 93 |
+
input_channels: int,
|
| 94 |
+
output_channels: int,
|
| 95 |
+
kernel_size: int = 3,
|
| 96 |
+
sn_eps=0.0001,
|
| 97 |
+
):
|
| 98 |
+
"""Initialize the convolution layer."""
|
| 99 |
+
super().__init__()
|
| 100 |
+
self.cell = ConvGRUCell(input_channels, output_channels, kernel_size, sn_eps)
|
| 101 |
+
|
| 102 |
+
def forward(self, x: torch.Tensor, hidden_state=None) -> torch.Tensor:
|
| 103 |
+
"""Apply the forward function on each cell prior to returning it as a stack."""
|
| 104 |
+
outputs = []
|
| 105 |
+
for step in range(len(x)):
|
| 106 |
+
# Compute current timestep
|
| 107 |
+
output, hidden_state = self.cell(x[step], hidden_state)
|
| 108 |
+
outputs.append(output)
|
| 109 |
+
# Stack outputs to return as tensor
|
| 110 |
+
outputs = torch.stack(outputs, dim=0)
|
| 111 |
+
return outputs
|
model/dgmr_official/layers/CoordConv.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The coordinate convolution class."""
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn as nn
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class AddCoords(nn.Module):
|
| 8 |
+
"""argument input tensors with spatial information."""
|
| 9 |
+
|
| 10 |
+
def __init__(self, with_r: bool = False):
|
| 11 |
+
"""
|
| 12 |
+
Initialize the add coordinates class.
|
| 13 |
+
|
| 14 |
+
Args:
|
| 15 |
+
with_r: a condition to check if radical distance should included in the spatial
|
| 16 |
+
information (bool) default = false
|
| 17 |
+
"""
|
| 18 |
+
super().__init__()
|
| 19 |
+
self.with_r: bool = with_r
|
| 20 |
+
|
| 21 |
+
def forward(self, input_tensor):
|
| 22 |
+
"""
|
| 23 |
+
Add spatial information to the input tensor.
|
| 24 |
+
|
| 25 |
+
Args:
|
| 26 |
+
input_tensor: shape(batch, channel, x_dim, y_dim)
|
| 27 |
+
"""
|
| 28 |
+
batch_size, _, x_dim, y_dim = input_tensor.size()
|
| 29 |
+
|
| 30 |
+
xx_channel = torch.arange(x_dim).repeat(1, y_dim, 1)
|
| 31 |
+
yy_channel = torch.arange(y_dim).repeat(1, x_dim, 1).transpose(1, 2)
|
| 32 |
+
|
| 33 |
+
xx_channel = xx_channel.float() / (x_dim - 1)
|
| 34 |
+
yy_channel = yy_channel.float() / (y_dim - 1)
|
| 35 |
+
|
| 36 |
+
xx_channel = xx_channel * 2 - 1
|
| 37 |
+
yy_channel = yy_channel * 2 - 1
|
| 38 |
+
|
| 39 |
+
xx_channel = xx_channel.repeat(batch_size, 1, 1, 1).transpose(2, 3)
|
| 40 |
+
yy_channel = yy_channel.repeat(batch_size, 1, 1, 1).transpose(2, 3)
|
| 41 |
+
|
| 42 |
+
ret = torch.cat(
|
| 43 |
+
[input_tensor, xx_channel.type_as(input_tensor), yy_channel.type_as(input_tensor)],
|
| 44 |
+
dim=1,
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
if self.with_r:
|
| 48 |
+
rr = torch.sqrt(
|
| 49 |
+
torch.pow(xx_channel.type_as(input_tensor) - 0.5, 2)
|
| 50 |
+
+ torch.pow(yy_channel.type_as(input_tensor) - 0.5, 2)
|
| 51 |
+
)
|
| 52 |
+
ret = torch.cat([ret, rr], dim=1)
|
| 53 |
+
|
| 54 |
+
return ret
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class CoordConv(nn.Module):
|
| 58 |
+
"""Coordinate convolution class."""
|
| 59 |
+
|
| 60 |
+
def __init__(self, in_channels, out_channels, with_r=False, **kwargs):
|
| 61 |
+
"""
|
| 62 |
+
Initialize the coordinate convolution.
|
| 63 |
+
|
| 64 |
+
Args:
|
| 65 |
+
in_channels : number of input channels
|
| 66 |
+
out_channels : number of output channels,
|
| 67 |
+
with_r : boolean =False,
|
| 68 |
+
**kwargs : dict[str, Unknown]
|
| 69 |
+
"""
|
| 70 |
+
super().__init__()
|
| 71 |
+
self.addcoords = AddCoords(with_r=with_r)
|
| 72 |
+
in_size = in_channels + 2
|
| 73 |
+
if with_r:
|
| 74 |
+
in_size += 1
|
| 75 |
+
self.conv = nn.Conv2d(in_size, out_channels, **kwargs)
|
| 76 |
+
|
| 77 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 78 |
+
"""Apply a forward pass on the input tensor."""
|
| 79 |
+
ret = self.addcoords(x)
|
| 80 |
+
ret = self.conv(ret)
|
| 81 |
+
return ret
|
model/dgmr_official/layers/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Submodules for the layers."""
|
| 2 |
+
|
| 3 |
+
from .Attention import AttentionLayer
|
| 4 |
+
from .ConvGRU import ConvGRU
|
| 5 |
+
from .CoordConv import CoordConv
|
model/dgmr_official/layers/__pycache__/Attention.cpython-311.pyc
ADDED
|
Binary file (3.82 kB). View file
|
|
|
model/dgmr_official/layers/__pycache__/ConvGRU.cpython-311.pyc
ADDED
|
Binary file (4.89 kB). View file
|
|
|
model/dgmr_official/layers/__pycache__/CoordConv.cpython-311.pyc
ADDED
|
Binary file (4.61 kB). View file
|
|
|
model/dgmr_official/layers/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (423 Bytes). View file
|
|
|
model/dgmr_official/layers/__pycache__/utils.cpython-311.pyc
ADDED
|
Binary file (1.01 kB). View file
|
|
|
model/dgmr_official/layers/utils.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Utilities functions for the layer submodules."""
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
from . import CoordConv
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def get_conv_layer(conv_type: str = "standard") -> torch.nn.Module:
|
| 9 |
+
"""Return a conv layer based on the passed in string name."""
|
| 10 |
+
if conv_type == "standard":
|
| 11 |
+
conv_layer = torch.nn.Conv2d
|
| 12 |
+
elif conv_type == "coord":
|
| 13 |
+
conv_layer = CoordConv
|
| 14 |
+
elif conv_type == "3d":
|
| 15 |
+
conv_layer = torch.nn.Conv3d
|
| 16 |
+
else:
|
| 17 |
+
raise ValueError(f"{conv_type} is not a recognized Conv method")
|
| 18 |
+
return conv_layer
|
model/dgmr_official/losses.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Loss functions for DGMR (trimmed from Open Climate Fix ``dgmr`` package).
|
| 2 |
+
|
| 3 |
+
Only the GAN losses actually used by the DGMR objective are kept; the SSIM
|
| 4 |
+
variants (which depend on ``pytorch_msssim``) are omitted.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
import torch.nn as nn
|
| 9 |
+
from torch.nn import functional as F
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class GridCellLoss(nn.Module):
|
| 13 |
+
"""Grid Cell Regularizer loss from Skillful Nowcasting, see https://arxiv.org/pdf/2104.00954.pdf."""
|
| 14 |
+
|
| 15 |
+
def __init__(self, weight_fn=None, precip_weight_cap=24.0):
|
| 16 |
+
"""
|
| 17 |
+
Initialize GridCellLoss.
|
| 18 |
+
|
| 19 |
+
Args:
|
| 20 |
+
weight_fn: A function to compute weights for the loss.
|
| 21 |
+
precip_weight_cap: Custom ceiling value for the weight function.
|
| 22 |
+
"""
|
| 23 |
+
super().__init__()
|
| 24 |
+
self.weight_fn = lambda y: weight_fn(y, precip_weight_cap) if weight_fn else None
|
| 25 |
+
|
| 26 |
+
def forward(self, generated_images, targets):
|
| 27 |
+
"""
|
| 28 |
+
Forward function.
|
| 29 |
+
|
| 30 |
+
Calculates the grid cell regularizer value, assumes generated images are the mean
|
| 31 |
+
predictions from 6 calls to the generater (Monte Carlo estimation of the
|
| 32 |
+
expectations for the latent variable)
|
| 33 |
+
|
| 34 |
+
Args:
|
| 35 |
+
generated_images: Mean generated images from the generator
|
| 36 |
+
targets: Ground truth future frames
|
| 37 |
+
|
| 38 |
+
Returns:
|
| 39 |
+
Grid Cell Regularizer term
|
| 40 |
+
"""
|
| 41 |
+
difference = generated_images - targets
|
| 42 |
+
if self.weight_fn is not None:
|
| 43 |
+
weights = self.weight_fn(targets)
|
| 44 |
+
difference = difference * weights
|
| 45 |
+
difference = difference.norm(p=1)
|
| 46 |
+
return difference / targets.size(1) * targets.size(3) * targets.size(4)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class NowcastingLoss(nn.Module):
|
| 50 |
+
"""Nowcast Loss class."""
|
| 51 |
+
|
| 52 |
+
def __init__(self):
|
| 53 |
+
"""Initialize function."""
|
| 54 |
+
super().__init__()
|
| 55 |
+
|
| 56 |
+
def forward(self, x, real_flag):
|
| 57 |
+
"""Apply the relu function to the input tensor."""
|
| 58 |
+
if real_flag is True:
|
| 59 |
+
x = -x
|
| 60 |
+
return F.relu(1.0 + x).mean()
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def loss_hinge_disc(score_generated, score_real):
|
| 64 |
+
"""Discriminator hinge loss."""
|
| 65 |
+
l1 = F.relu(1.0 - score_real)
|
| 66 |
+
loss = torch.mean(l1)
|
| 67 |
+
l2 = F.relu(1.0 + score_generated)
|
| 68 |
+
loss += torch.mean(l2)
|
| 69 |
+
return loss
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def loss_hinge_gen(score_generated):
|
| 73 |
+
"""Generate hinge loss."""
|
| 74 |
+
loss = -torch.mean(score_generated)
|
| 75 |
+
return loss
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def grid_cell_regularizer(generated_samples, batch_targets):
|
| 79 |
+
"""Grid cell regularizer.
|
| 80 |
+
|
| 81 |
+
Args:
|
| 82 |
+
generated_samples: Tensor of size [n_samples, batch_size, 18, 256, 256, 1].
|
| 83 |
+
batch_targets: Tensor of size [batch_size, 18, 256, 256, 1].
|
| 84 |
+
|
| 85 |
+
Returns:
|
| 86 |
+
loss: A tensor of shape [batch_size].
|
| 87 |
+
"""
|
| 88 |
+
gen_mean = torch.mean(generated_samples, dim=0)
|
| 89 |
+
# TODO Possibly change clip here?
|
| 90 |
+
weights = torch.clip(batch_targets, 0.0, 24.0)
|
| 91 |
+
loss = torch.mean(torch.abs(gen_mean - batch_targets) * weights)
|
| 92 |
+
return loss
|
scripts/fake_data.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import h5py
|
| 3 |
+
import numpy as np
|
| 4 |
+
from onescience.utils.YParams import YParams
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
# DGMR 使用 4 帧上下文预测后续雷达帧;T 随模型配置自动变化。
|
| 8 |
+
def get_dims(cfg_model, cfg_data):
|
| 9 |
+
H, W = map(int, cfg_data.dataset.img_size)
|
| 10 |
+
if (H, W) != (int(cfg_model.output_shape), int(cfg_model.output_shape)):
|
| 11 |
+
raise ValueError("dataset.img_size must match model.output_shape")
|
| 12 |
+
input_steps = int(cfg_model.num_context)
|
| 13 |
+
output_steps = int(cfg_model.forecast_steps)
|
| 14 |
+
samples = int(cfg_data.dataloader.batch_size)
|
| 15 |
+
T = input_steps + output_steps + samples - 1
|
| 16 |
+
return {
|
| 17 |
+
"T": T, "H": H, "W": W, "time_step": 1,
|
| 18 |
+
"input_steps": input_steps, "output_steps": output_steps,
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def generate_fake_h5(data_dir, var_names, years, dims):
|
| 23 |
+
"""
|
| 24 |
+
为每个年份生成一个空 h5 文件。
|
| 25 |
+
利用 HDF5 chunked 数据集未写入 chunk 即返回 fill_value=0 的特性,
|
| 26 |
+
文件实际只含元数据,极小,但 shape 与真实数据完全一致。
|
| 27 |
+
均值/标准差也作为数据集内嵌进每年的 h5,与 era5.py 新版读取方式对应。
|
| 28 |
+
|
| 29 |
+
注意:ERA5Datapipe 要求 samples_per_year = T - input_steps - output_steps + 1 >= 1,
|
| 30 |
+
T 由 num_context、forecast_steps 与 batch_size 自动计算。
|
| 31 |
+
"""
|
| 32 |
+
os.makedirs(os.path.join(data_dir, "data"), exist_ok=True)
|
| 33 |
+
T, C = dims["T"], len(var_names)
|
| 34 |
+
H, W = dims["H"], dims["W"]
|
| 35 |
+
|
| 36 |
+
means = np.zeros((1, C, 1, 1), dtype=np.float32)
|
| 37 |
+
stds = np.ones((1, C, 1, 1), dtype=np.float32)
|
| 38 |
+
|
| 39 |
+
for year in years:
|
| 40 |
+
path = os.path.join(data_dir, "data", f"{year}.h5")
|
| 41 |
+
with h5py.File(path, "w") as f:
|
| 42 |
+
ds = f.create_dataset(
|
| 43 |
+
"fields",
|
| 44 |
+
shape=(T, C, H, W),
|
| 45 |
+
dtype="float32",
|
| 46 |
+
chunks=(1, C, H, W),
|
| 47 |
+
fillvalue=0.0,
|
| 48 |
+
)
|
| 49 |
+
ds.attrs["variables"] = var_names
|
| 50 |
+
ds.attrs["time_step"] = dims["time_step"]
|
| 51 |
+
f.create_dataset("global_means", data=means)
|
| 52 |
+
f.create_dataset("global_stds", data=stds)
|
| 53 |
+
|
| 54 |
+
size_kb = os.path.getsize(path) / 1024
|
| 55 |
+
print(f" {year}.h5 shape=({T},{C},{H},{W}) "
|
| 56 |
+
f"logical={T*C*H*W*4/1024**3:.1f}GB actual={size_kb:.1f}KB")
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
if __name__ == "__main__":
|
| 60 |
+
cfg_model = YParams("conf/config.yaml", "model")
|
| 61 |
+
cfg_datapipe = YParams("conf/config.yaml", "datapipe")
|
| 62 |
+
|
| 63 |
+
if cfg_datapipe.dataset.data_dir.startswith("/public/") or cfg_datapipe.dataset.data_dir.startswith("/work2/"):
|
| 64 |
+
print("请检查 config,确保各 *_dir 指向本地测试路径而非生产路径。")
|
| 65 |
+
exit()
|
| 66 |
+
|
| 67 |
+
years = cfg_datapipe.dataset.train_time + cfg_datapipe.dataset.val_time + cfg_datapipe.dataset.test_time
|
| 68 |
+
atm_vars = cfg_datapipe.dataset.channels
|
| 69 |
+
if len(atm_vars) != int(cfg_model.input_channels):
|
| 70 |
+
raise ValueError("channel count must match model.input_channels")
|
| 71 |
+
|
| 72 |
+
generate_fake_h5(cfg_datapipe.dataset.data_dir, atm_vars, years, get_dims(cfg_model, cfg_datapipe))
|
| 73 |
+
|
| 74 |
+
print("\n✅ Fake datasets generated.")
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
# 获取项目根目录(inference.py上级的上级)
|
| 5 |
+
root_path = Path(__file__).parent.parent
|
| 6 |
+
sys.path.append(str(root_path))
|
| 7 |
+
import torch
|
| 8 |
+
import os
|
| 9 |
+
import glob
|
| 10 |
+
import numpy as np
|
| 11 |
+
import h5py
|
| 12 |
+
from tqdm import tqdm
|
| 13 |
+
from model.dgmr import DGMR
|
| 14 |
+
from onescience.utils.YParams import YParams
|
| 15 |
+
from onescience.datapipes.climate import ERA5Datapipe
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def get_stats(data_dir, channels):
|
| 19 |
+
"""从新版 h5 中读取变量列表与归一化参数(均值/标准差)"""
|
| 20 |
+
h5_files = sorted(glob.glob(os.path.join(data_dir, "data", "*.h5")))
|
| 21 |
+
with h5py.File(h5_files[0], "r") as f:
|
| 22 |
+
ds = f["fields"]
|
| 23 |
+
all_variables = [v.decode() if isinstance(v, bytes) else v for v in ds.attrs["variables"]]
|
| 24 |
+
mu = f["global_means"][:] # [1, C, 1, 1]
|
| 25 |
+
std = f["global_stds"][:]
|
| 26 |
+
|
| 27 |
+
channel_indices = [all_variables.index(v) for v in channels]
|
| 28 |
+
means = mu[:, channel_indices, :, :]
|
| 29 |
+
stds = std[:, channel_indices, :, :]
|
| 30 |
+
return means, stds
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
if __name__ == "__main__":
|
| 34 |
+
current_path = os.getcwd()
|
| 35 |
+
sys.path.append(current_path)
|
| 36 |
+
|
| 37 |
+
## Model config init
|
| 38 |
+
config_file_path = os.path.join(current_path, "conf/config.yaml")
|
| 39 |
+
cfg = YParams(config_file_path, "model")
|
| 40 |
+
|
| 41 |
+
## DataLoader init
|
| 42 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 43 |
+
means, stds = get_stats(cfg_data.dataset.data_dir, cfg_data.dataset.channels)
|
| 44 |
+
|
| 45 |
+
cfg['N_in_channels'] = len(cfg_data.dataset.channels)
|
| 46 |
+
cfg['N_out_channels'] = len(cfg_data.dataset.channels)
|
| 47 |
+
|
| 48 |
+
datapipe = ERA5Datapipe(
|
| 49 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 50 |
+
used_variables=cfg_data.dataset.channels,
|
| 51 |
+
used_years=cfg_data.dataset.test_time,
|
| 52 |
+
distributed=False,
|
| 53 |
+
input_steps=cfg.num_context,
|
| 54 |
+
output_steps=cfg.forecast_steps,
|
| 55 |
+
batch_size=1,
|
| 56 |
+
num_workers=4,
|
| 57 |
+
)
|
| 58 |
+
test_dataloader, _ = datapipe.get_dataloader("test")
|
| 59 |
+
|
| 60 |
+
device = "cuda:0" if torch.cuda.is_available() else "cpu"
|
| 61 |
+
ckpt = torch.load(f"{cfg.checkpoint_dir}/model_bak.pth", map_location=device, weights_only=False)
|
| 62 |
+
model = DGMR(
|
| 63 |
+
forecast_steps=cfg.forecast_steps,
|
| 64 |
+
num_context=cfg.num_context,
|
| 65 |
+
input_channels=cfg.input_channels,
|
| 66 |
+
output_shape=cfg.output_shape,
|
| 67 |
+
conv_type=cfg.conv_type,
|
| 68 |
+
latent_channels=cfg.latent_channels,
|
| 69 |
+
context_channels=cfg.context_channels,
|
| 70 |
+
generation_steps=cfg.generation_steps,
|
| 71 |
+
grid_lambda=cfg.grid_lambda,
|
| 72 |
+
precip_weight_cap=cfg.precip_weight_cap,
|
| 73 |
+
).to(device)
|
| 74 |
+
model.load_state_dict(ckpt["model_state_dict"])
|
| 75 |
+
|
| 76 |
+
model.eval()
|
| 77 |
+
os.makedirs('result/output/', exist_ok=True)
|
| 78 |
+
print(f"📂 infer results will be generated to './result/output/'")
|
| 79 |
+
with torch.no_grad():
|
| 80 |
+
for data in tqdm(test_dataloader, desc="Inferring testset", unit="batch"):
|
| 81 |
+
invar = data[0].to(device, dtype=torch.float32) # [1, num_context, C, H, W]
|
| 82 |
+
gen = model.generator(invar).cpu().numpy() # [1, forecast_steps, C, H, W]
|
| 83 |
+
for t in range(gen.shape[1]):
|
| 84 |
+
fname = data[4][cfg.num_context + t][0] # 该预测帧对应的时刻
|
| 85 |
+
pred_var = gen[0, t] # [C, H, W]
|
| 86 |
+
pred_var = pred_var * stds + means
|
| 87 |
+
np.save(f"result/output/{fname}.npy", pred_var)
|
scripts/result.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
import glob
|
| 6 |
+
import h5py
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
from tqdm import tqdm
|
| 9 |
+
from onescience.utils.fcn.YParams import YParams
|
| 10 |
+
from matplotlib import rcParams
|
| 11 |
+
|
| 12 |
+
# rcParams['font.family'] = 'serif'
|
| 13 |
+
# rcParams['font.serif'] = ['DejaVu Serif']
|
| 14 |
+
rcParams['mathtext.fontset'] = 'stix'
|
| 15 |
+
rcParams['axes.linewidth'] = 0.9
|
| 16 |
+
rcParams['xtick.major.width'] = 0.9
|
| 17 |
+
rcParams['ytick.major.width'] = 0.9
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def get_metadata(data_dir, channels):
|
| 21 |
+
"""从新版 h5 attrs 中读取变量列表和 time_step"""
|
| 22 |
+
h5_files = sorted(glob.glob(os.path.join(data_dir, "data", "*.h5")))
|
| 23 |
+
with h5py.File(h5_files[0], "r") as f:
|
| 24 |
+
ds = f["fields"]
|
| 25 |
+
all_variables = [v.decode() if isinstance(v, bytes) else v for v in ds.attrs["variables"]]
|
| 26 |
+
time_step = int(ds.attrs["time_step"])
|
| 27 |
+
|
| 28 |
+
channel_indices = [all_variables.index(v) for v in channels]
|
| 29 |
+
|
| 30 |
+
total_files = [f for f in os.listdir('./result/output/') if f.endswith('.npy')]
|
| 31 |
+
total_files.sort()
|
| 32 |
+
return total_files, channel_indices, time_step
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def filename_to_index(filename, time_step):
|
| 36 |
+
"""将 YYYYMMDDHH 格式的文件名转换为年度 h5 文件中的时间步索引"""
|
| 37 |
+
dt = datetime.strptime(filename, "%Y%m%d%H")
|
| 38 |
+
year_start = datetime(dt.year, 1, 1)
|
| 39 |
+
hours = (dt - year_start).total_seconds() / 3600
|
| 40 |
+
return int(hours / time_step)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def get_result(total_files, channel_indices, time_step, data_dir, clim_mean):
|
| 44 |
+
channel_rmse = np.zeros(len(channel_indices))
|
| 45 |
+
channel_acc = np.zeros(len(channel_indices))
|
| 46 |
+
clim_mean = clim_mean[0, :, :, :]
|
| 47 |
+
if not os.path.exists('./result/rmse.npy') or not os.path.exists('result/acc.npy'):
|
| 48 |
+
numerator = np.zeros(len(channel_indices))
|
| 49 |
+
pred_sq_sum = np.zeros(len(channel_indices))
|
| 50 |
+
label_sq_sum = np.zeros(len(channel_indices))
|
| 51 |
+
for file in tqdm(total_files, unit="files"):
|
| 52 |
+
fname = file[:-4] # 去掉 .npy
|
| 53 |
+
year = fname[:4]
|
| 54 |
+
t_idx = filename_to_index(fname, time_step)
|
| 55 |
+
with h5py.File(os.path.join(data_dir, 'data', f'{year}.h5'), "r") as f:
|
| 56 |
+
label = f["fields"][t_idx] # [C, H, W]
|
| 57 |
+
label = label[channel_indices]
|
| 58 |
+
pred = np.load(f'result/output/{file}').squeeze()
|
| 59 |
+
if pred.ndim == 2:
|
| 60 |
+
pred = pred[np.newaxis] # 单通道时 squeeze 会压缩掉通道维,恢复为 [C, H, W]
|
| 61 |
+
|
| 62 |
+
label_anom = label - clim_mean
|
| 63 |
+
pred_anom = pred - clim_mean
|
| 64 |
+
# 累加
|
| 65 |
+
numerator += np.sum(pred_anom * label_anom, axis=(1, 2))
|
| 66 |
+
pred_sq_sum += np.sum(pred_anom ** 2, axis=(1, 2))
|
| 67 |
+
label_sq_sum += np.sum(label_anom ** 2, axis=(1, 2))
|
| 68 |
+
|
| 69 |
+
channel_rmse += np.sqrt(np.mean((label - pred) ** 2, axis=(1, 2)))
|
| 70 |
+
channel_rmse /= len(total_files)
|
| 71 |
+
channel_acc = numerator / (np.sqrt(pred_sq_sum * label_sq_sum) + 1e-8)
|
| 72 |
+
np.save('./result/acc.npy', channel_acc)
|
| 73 |
+
np.save('./result/rmse.npy', channel_rmse)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def show_result():
|
| 77 |
+
channel_rmse = np.load('./result/rmse.npy')
|
| 78 |
+
channel_acc = np.load('./result/acc.npy')
|
| 79 |
+
|
| 80 |
+
channels = [cfg_data.dataset.channels[i] for i in range(len(channel_indices))]
|
| 81 |
+
w = 24 # 最长 channel 名宽度
|
| 82 |
+
|
| 83 |
+
# 表头
|
| 84 |
+
print(f"┌{'─' * (w + 2)}┬{'─' * 14}┬{'─' * 14}┐")
|
| 85 |
+
print(f"│ {'Channel':<{w}} │ {'RMSE':>12} │ {'ACC':>12} │")
|
| 86 |
+
print(f"├{'─' * (w + 2)}┼{'─' * 14}┼{'─' * 14}┤")
|
| 87 |
+
# 数据行
|
| 88 |
+
for i, ch in enumerate(channels):
|
| 89 |
+
print(f"│ {ch:<{w}} │ {channel_rmse[i]:>12.4f} | {channel_acc[i]:>12.4f} |")
|
| 90 |
+
print(f"├{'─' * (w + 2)}┼{'─' * 14}┼{'─' * 14}┤")
|
| 91 |
+
print(f"│ {'Average':<{w}} │ {np.mean(channel_rmse):>12.4f} │ {np.mean(channel_acc):>12.4f} │")
|
| 92 |
+
print(f"└{'─' * (w + 2)}┴{'─' * 14}┴{'─' * 14}┘")
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def plot(label, pred, var, filename):
|
| 96 |
+
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
|
| 97 |
+
|
| 98 |
+
xtick_labels = ['180°W', '90°W', '0°', '90°E', '180°E']
|
| 99 |
+
ytick_labels = ['90°S', '45°S', '0°', '45°N', '90°N']
|
| 100 |
+
xticks = np.linspace(0, label.shape[-1] - 1, 5)
|
| 101 |
+
yticks = np.linspace(0, label.shape[-2] - 1, 5)
|
| 102 |
+
|
| 103 |
+
vmin = min(label.min(), pred.min())
|
| 104 |
+
vmax = max(label.max(), pred.max())
|
| 105 |
+
|
| 106 |
+
diff = label - pred
|
| 107 |
+
rmse = np.sqrt(np.mean(diff ** 2))
|
| 108 |
+
diff_abs_max = np.abs(diff).max()
|
| 109 |
+
|
| 110 |
+
plot_configs = [
|
| 111 |
+
{'data': label, 'title': 'Truth', 'cmap': 'viridis', 'vmin': vmin, 'vmax': vmax},
|
| 112 |
+
{'data': pred, 'title': 'Prediction', 'cmap': 'viridis', 'vmin': vmin, 'vmax': vmax},
|
| 113 |
+
{'data': diff, 'title': f'Difference (RMSE={rmse:.2f})', 'cmap': 'RdBu_r', 'vmin': -diff_abs_max, 'vmax': diff_abs_max},
|
| 114 |
+
]
|
| 115 |
+
|
| 116 |
+
for ax, cfg in zip(axes, plot_configs):
|
| 117 |
+
im = ax.imshow(cfg['data'], cmap=cfg['cmap'], vmin=cfg['vmin'], vmax=cfg['vmax'])
|
| 118 |
+
ax.set_title(cfg['title'], fontsize=12, pad=4)
|
| 119 |
+
ax.set_xlabel('Longitude')
|
| 120 |
+
ax.set_ylabel('Latitude')
|
| 121 |
+
ax.set_xticks(xticks)
|
| 122 |
+
ax.set_xticklabels(xtick_labels)
|
| 123 |
+
ax.set_yticks(yticks)
|
| 124 |
+
ax.set_yticklabels(ytick_labels)
|
| 125 |
+
plt.colorbar(im, ax=ax, orientation='horizontal')
|
| 126 |
+
|
| 127 |
+
fig.suptitle(var, fontsize=14, fontweight='bold', y=0.98)
|
| 128 |
+
plt.savefig(filename, dpi=300, bbox_inches='tight')
|
| 129 |
+
plt.close()
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def plot_loss(train_loss, valid_loss):
|
| 133 |
+
mask = ~(np.isnan(train_loss) | np.isnan(valid_loss))
|
| 134 |
+
train_loss = train_loss[mask]
|
| 135 |
+
valid_loss = valid_loss[mask]
|
| 136 |
+
|
| 137 |
+
fig, ax = plt.subplots(figsize=(5, 3.5))
|
| 138 |
+
colors = {'train': '#2563EB', 'valid': '#EA580C'}
|
| 139 |
+
epochs = np.arange(1, len(train_loss) + 1)
|
| 140 |
+
|
| 141 |
+
ax.plot(epochs, train_loss, color=colors['train'], linewidth=1.5, label='Train')
|
| 142 |
+
ax.plot(epochs, valid_loss, color=colors['valid'], linewidth=1.5, label='Valid', linestyle='--')
|
| 143 |
+
min_idx = np.argmin(valid_loss)
|
| 144 |
+
ax.scatter(epochs[min_idx], valid_loss[min_idx],
|
| 145 |
+
color=colors['valid'], s=40, zorder=5, edgecolors='white')
|
| 146 |
+
ax.annotate(f'Best: {valid_loss[min_idx]:.3f}',
|
| 147 |
+
xy=(epochs[min_idx], valid_loss[min_idx]),
|
| 148 |
+
xytext=(10, 10), textcoords='offset points', fontsize=8, color=colors['valid'],
|
| 149 |
+
arrowprops=dict(arrowstyle='-', color=colors['valid'], lw=0.5))
|
| 150 |
+
|
| 151 |
+
ax.set(xlabel='Epoch', ylabel='Loss', xlim=(0, len(train_loss) + 1))
|
| 152 |
+
ax.legend(frameon=False, loc='upper right')
|
| 153 |
+
ax.grid(True, linestyle='--', alpha=0.3)
|
| 154 |
+
ax.spines[['top', 'right']].set_visible(False)
|
| 155 |
+
|
| 156 |
+
plt.tight_layout()
|
| 157 |
+
plt.savefig('./result/loss.png', dpi=300, bbox_inches='tight')
|
| 158 |
+
plt.close()
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
if __name__ == "__main__":
|
| 162 |
+
current_path = os.getcwd()
|
| 163 |
+
sys.path.append(current_path)
|
| 164 |
+
config_file_path = os.path.join(current_path, 'conf/config.yaml')
|
| 165 |
+
cfg = YParams(config_file_path, 'model')
|
| 166 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 167 |
+
|
| 168 |
+
train_loss = np.load('./data/checkpoints/trloss.npy')
|
| 169 |
+
valid_loss = np.load('./data/checkpoints/valoss.npy')
|
| 170 |
+
plot_loss(train_loss, valid_loss)
|
| 171 |
+
|
| 172 |
+
data_dir = cfg_data.dataset.data_dir
|
| 173 |
+
total_files, channel_indices, time_step = get_metadata(data_dir, cfg_data.dataset.channels)
|
| 174 |
+
|
| 175 |
+
# Load data & Compute RMSE/ACC per channel
|
| 176 |
+
h5_files = sorted(glob.glob(os.path.join(data_dir, "data", "*.h5")))
|
| 177 |
+
with h5py.File(h5_files[0], "r") as f:
|
| 178 |
+
mu = f["global_means"][:]
|
| 179 |
+
clim_mean = mu[:, channel_indices, :, :]
|
| 180 |
+
get_result(total_files, channel_indices, time_step, data_dir, clim_mean)
|
| 181 |
+
show_result()
|
| 182 |
+
|
| 183 |
+
##### 默认绘制 result/output 中第一个预测结果,用户可自行指定日期和变量 #####
|
| 184 |
+
eg_files = [total_files[0][:-4]]
|
| 185 |
+
# DGMR 为单通道场,默认绘制该通道(后续帧的时刻可通过 h5 time_step 推算)
|
| 186 |
+
channel_index = list(range(len(cfg_data.dataset.channels)))
|
| 187 |
+
|
| 188 |
+
selected_var = [cfg_data.dataset.channels[int(i)] for i in channel_index]
|
| 189 |
+
print(f"seleted date: {eg_files}")
|
| 190 |
+
print(f"selected channels: {selected_var}")
|
| 191 |
+
for file in eg_files:
|
| 192 |
+
year = file[:4]
|
| 193 |
+
t_idx = filename_to_index(file, time_step)
|
| 194 |
+
with h5py.File(os.path.join(data_dir, 'data', f'{year}.h5'), "r") as f:
|
| 195 |
+
label = f["fields"][t_idx] # [C, H, W]
|
| 196 |
+
label = label[channel_indices]
|
| 197 |
+
pred = np.load(f'result/output/{file}.npy').squeeze()
|
| 198 |
+
if pred.ndim == 2:
|
| 199 |
+
pred = pred[np.newaxis] # 单通道时 squeeze 会压缩掉通道维,恢复为 [C, H, W]
|
| 200 |
+
for i in range(len(selected_var)):
|
| 201 |
+
filename = f'./result/{file}_{selected_var[i]}.png'
|
| 202 |
+
plot(label[channel_index[i]], pred[channel_index[i]], selected_var[i], filename)
|
| 203 |
+
print(f'✅plot {filename}')
|
scripts/train.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
# 获取项目根目录(train.py上级的上级)
|
| 5 |
+
root_path = Path(__file__).parent.parent
|
| 6 |
+
sys.path.append(str(root_path))
|
| 7 |
+
import torch
|
| 8 |
+
import os
|
| 9 |
+
import numpy as np
|
| 10 |
+
import torch.distributed as dist
|
| 11 |
+
import logging
|
| 12 |
+
import time
|
| 13 |
+
|
| 14 |
+
from model.dgmr import DGMR
|
| 15 |
+
from model.dgmr_official.losses import loss_hinge_disc, loss_hinge_gen
|
| 16 |
+
from onescience.datapipes.climate import ERA5Datapipe
|
| 17 |
+
from onescience.utils.YParams import YParams
|
| 18 |
+
|
| 19 |
+
try:
|
| 20 |
+
from apex import optimizers
|
| 21 |
+
_FUSED_ADAM = True
|
| 22 |
+
except Exception:
|
| 23 |
+
_FUSED_ADAM = False
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def main():
|
| 27 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
| 28 |
+
logger = logging.getLogger()
|
| 29 |
+
|
| 30 |
+
## Model config init
|
| 31 |
+
config_file_path = os.path.join(current_path, "conf/config.yaml")
|
| 32 |
+
cfg = YParams(config_file_path, "model")
|
| 33 |
+
|
| 34 |
+
## Distributed config init
|
| 35 |
+
cfg.world_size = 1
|
| 36 |
+
if "WORLD_SIZE" in os.environ:
|
| 37 |
+
cfg.world_size = int(os.environ["WORLD_SIZE"])
|
| 38 |
+
world_rank = 0
|
| 39 |
+
local_rank = 0
|
| 40 |
+
if cfg.world_size > 1 and torch.cuda.is_available():
|
| 41 |
+
dist.init_process_group(backend="nccl", init_method="env://")
|
| 42 |
+
local_rank = int(os.environ["LOCAL_RANK"])
|
| 43 |
+
world_rank = dist.get_rank()
|
| 44 |
+
device = f"cuda:{local_rank}" if torch.cuda.is_available() else "cpu"
|
| 45 |
+
|
| 46 |
+
## DataLoader init
|
| 47 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 48 |
+
cfg['N_in_channels'] = len(cfg_data.dataset.channels)
|
| 49 |
+
cfg['N_out_channels'] = len(cfg_data.dataset.channels)
|
| 50 |
+
datapipe = ERA5Datapipe(
|
| 51 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 52 |
+
used_variables=cfg_data.dataset.channels,
|
| 53 |
+
used_years=cfg_data.dataset.train_time,
|
| 54 |
+
distributed=dist.is_initialized(),
|
| 55 |
+
input_steps=cfg.num_context,
|
| 56 |
+
output_steps=cfg.forecast_steps,
|
| 57 |
+
batch_size=cfg_data.dataloader.batch_size,
|
| 58 |
+
num_workers=cfg_data.dataloader.num_workers,
|
| 59 |
+
)
|
| 60 |
+
train_dataloader, train_sampler = datapipe.get_dataloader("train")
|
| 61 |
+
datapipe = ERA5Datapipe(
|
| 62 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 63 |
+
used_variables=cfg_data.dataset.channels,
|
| 64 |
+
used_years=cfg_data.dataset.val_time,
|
| 65 |
+
distributed=dist.is_initialized(),
|
| 66 |
+
input_steps=cfg.num_context,
|
| 67 |
+
output_steps=cfg.forecast_steps,
|
| 68 |
+
batch_size=cfg_data.dataloader.batch_size,
|
| 69 |
+
num_workers=cfg_data.dataloader.num_workers,
|
| 70 |
+
)
|
| 71 |
+
val_dataloader, val_sampler = datapipe.get_dataloader("valid")
|
| 72 |
+
|
| 73 |
+
# Model init
|
| 74 |
+
model = DGMR(
|
| 75 |
+
forecast_steps=cfg.forecast_steps,
|
| 76 |
+
num_context=cfg.num_context,
|
| 77 |
+
input_channels=cfg.input_channels,
|
| 78 |
+
output_shape=cfg.output_shape,
|
| 79 |
+
conv_type=cfg.conv_type,
|
| 80 |
+
latent_channels=cfg.latent_channels,
|
| 81 |
+
context_channels=cfg.context_channels,
|
| 82 |
+
generation_steps=cfg.generation_steps,
|
| 83 |
+
grid_lambda=cfg.grid_lambda,
|
| 84 |
+
precip_weight_cap=cfg.precip_weight_cap,
|
| 85 |
+
).to(device)
|
| 86 |
+
|
| 87 |
+
# 生成器与判别器使用独立的 Adam 优化器(论文 lr=1e-4)
|
| 88 |
+
if _FUSED_ADAM:
|
| 89 |
+
optimizer_g = optimizers.FusedAdam(model.generator.parameters(), lr=cfg.lr)
|
| 90 |
+
optimizer_d = optimizers.FusedAdam(model.discriminator.parameters(), lr=cfg.lr_disc)
|
| 91 |
+
else:
|
| 92 |
+
optimizer_g = torch.optim.Adam(model.generator.parameters(), lr=cfg.lr)
|
| 93 |
+
optimizer_d = torch.optim.Adam(model.discriminator.parameters(), lr=cfg.lr_disc)
|
| 94 |
+
scheduler_g = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer_g, factor=0.2, patience=5, mode='min')
|
| 95 |
+
scheduler_d = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer_d, factor=0.2, patience=5, mode='min')
|
| 96 |
+
|
| 97 |
+
## Train process init
|
| 98 |
+
os.makedirs(cfg.checkpoint_dir, exist_ok=True)
|
| 99 |
+
train_loss_file = f"{cfg.checkpoint_dir}/trloss.npy"
|
| 100 |
+
valid_loss_file = f"{cfg.checkpoint_dir}/valoss.npy"
|
| 101 |
+
best_valid_loss = float("inf")
|
| 102 |
+
best_loss_epoch = 0
|
| 103 |
+
train_losses = np.empty((0,), dtype=np.float32)
|
| 104 |
+
valid_losses = np.empty((0,), dtype=np.float32)
|
| 105 |
+
|
| 106 |
+
## Get model params count
|
| 107 |
+
if cfg.world_size == 1:
|
| 108 |
+
total_params = sum(p.numel() for p in model.parameters())
|
| 109 |
+
print("\n\n")
|
| 110 |
+
print("-" * 50)
|
| 111 |
+
print(f"📂 now params is {total_params}, {total_params / 1e6:.2f}M, {total_params / 1e9:.2f}B")
|
| 112 |
+
print("-" * 50, "\n")
|
| 113 |
+
|
| 114 |
+
## Load model weight if there exist well-trained model
|
| 115 |
+
if os.path.exists(f"{cfg.checkpoint_dir}/model_bak.pth"):
|
| 116 |
+
if world_rank == 0:
|
| 117 |
+
print("\n\n")
|
| 118 |
+
print("-" * 50)
|
| 119 |
+
print(f"✅ There has a model weight, load and continue training...")
|
| 120 |
+
print(f'If you want to train a new model, ensure there is no *.pth file in {cfg.checkpoint_dir}')
|
| 121 |
+
print("-" * 50, "\n")
|
| 122 |
+
ckpt = torch.load(f"{cfg.checkpoint_dir}/model_bak.pth", map_location=device, weights_only=False)
|
| 123 |
+
model.load_state_dict(ckpt["model_state_dict"])
|
| 124 |
+
optimizer_g.load_state_dict(ckpt["optimizer_g_state_dict"])
|
| 125 |
+
optimizer_d.load_state_dict(ckpt["optimizer_d_state_dict"])
|
| 126 |
+
scheduler_g.load_state_dict(ckpt["scheduler_g_state_dict"])
|
| 127 |
+
scheduler_d.load_state_dict(ckpt["scheduler_d_state_dict"])
|
| 128 |
+
best_valid_loss = ckpt["best_valid_loss"]
|
| 129 |
+
best_loss_epoch = ckpt["best_loss_epoch"]
|
| 130 |
+
train_losses = np.load(train_loss_file)
|
| 131 |
+
valid_losses = np.load(valid_loss_file)
|
| 132 |
+
|
| 133 |
+
## Distributed model
|
| 134 |
+
if dist.is_initialized():
|
| 135 |
+
model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[local_rank], output_device=local_rank)
|
| 136 |
+
world_rank == 0 and logger.info(f"start training ...")
|
| 137 |
+
|
| 138 |
+
for epoch in range(cfg.max_epoch):
|
| 139 |
+
if dist.is_initialized():
|
| 140 |
+
train_sampler.set_epoch(epoch)
|
| 141 |
+
val_sampler.set_epoch(epoch)
|
| 142 |
+
model.train()
|
| 143 |
+
train_loss = 0
|
| 144 |
+
start_time = time.time()
|
| 145 |
+
for j, data in enumerate(train_dataloader):
|
| 146 |
+
invar = data[0].to(device, dtype=torch.float32) # [B, num_context, C, H, W]
|
| 147 |
+
outvar = data[1].to(device, dtype=torch.float32) # [B, forecast_steps, C, H, W]
|
| 148 |
+
full_real = torch.cat([invar, outvar], dim=1) # 上下文 + 真实未来帧
|
| 149 |
+
|
| 150 |
+
# --- 判别器(hinge loss,生成图像 detach 不传梯度) ---
|
| 151 |
+
gen_images = model.generator(invar) # [B, forecast_steps, C, H, W]
|
| 152 |
+
full_fake = torch.cat([invar, gen_images], dim=1)
|
| 153 |
+
score_real = model.discriminator(full_real)
|
| 154 |
+
score_generated = model.discriminator(full_fake.detach())
|
| 155 |
+
disc_loss = loss_hinge_disc(score_generated, score_real)
|
| 156 |
+
optimizer_d.zero_grad()
|
| 157 |
+
disc_loss.backward()
|
| 158 |
+
optimizer_d.step()
|
| 159 |
+
|
| 160 |
+
# --- 生成器(hinge + 网格单元正则器,MC 采样估计期望) ---
|
| 161 |
+
score_generated = model.discriminator(full_fake)
|
| 162 |
+
gen_loss = loss_hinge_gen(score_generated)
|
| 163 |
+
gen_samples = torch.stack(
|
| 164 |
+
[model.generator(invar) for _ in range(cfg.generation_steps)], dim=0
|
| 165 |
+
).mean(dim=0) # 取 MC 均值作为生成均值图像
|
| 166 |
+
grid_loss = model.grid_regularizer(gen_samples, outvar)
|
| 167 |
+
gen_loss = gen_loss + cfg.grid_lambda * grid_loss
|
| 168 |
+
optimizer_g.zero_grad()
|
| 169 |
+
gen_loss.backward()
|
| 170 |
+
optimizer_g.step()
|
| 171 |
+
|
| 172 |
+
loss = gen_loss.item() + disc_loss.item()
|
| 173 |
+
train_loss += loss
|
| 174 |
+
if world_rank == 0:
|
| 175 |
+
logger.info(f'Train: Epoch {epoch}-{j+1}/{len(train_dataloader)} '
|
| 176 |
+
f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
|
| 177 |
+
f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
|
| 178 |
+
f'gen_loss:{gen_loss.item(): .04f} disc_loss:{disc_loss.item(): .04f} '
|
| 179 |
+
f'loss:{train_loss / (j+1): .04f}')
|
| 180 |
+
|
| 181 |
+
train_loss /= len(train_dataloader)
|
| 182 |
+
|
| 183 |
+
# 验证只使用网格单元正则器(确定性,单个生成样本)
|
| 184 |
+
model.eval()
|
| 185 |
+
valid_loss = 0
|
| 186 |
+
with torch.no_grad():
|
| 187 |
+
start_time = time.time()
|
| 188 |
+
for j, data in enumerate(val_dataloader):
|
| 189 |
+
invar = data[0].to(device, dtype=torch.float32)
|
| 190 |
+
outvar = data[1].to(device, dtype=torch.float32)
|
| 191 |
+
gen_images = model.generator(invar)
|
| 192 |
+
grid_loss = model.grid_regularizer(gen_images, outvar)
|
| 193 |
+
loss = grid_loss
|
| 194 |
+
|
| 195 |
+
if dist.is_initialized():
|
| 196 |
+
loss_tensor = loss.detach().to(device)
|
| 197 |
+
dist.all_reduce(loss_tensor)
|
| 198 |
+
loss = loss_tensor.item() / cfg.world_size
|
| 199 |
+
valid_loss += loss
|
| 200 |
+
else:
|
| 201 |
+
valid_loss += loss.item()
|
| 202 |
+
if world_rank == 0:
|
| 203 |
+
logger.info(f'Valid: Epoch {epoch}-{j+1}/{len(val_dataloader)} '
|
| 204 |
+
f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
|
| 205 |
+
f'loss:{valid_loss / (j+1): .04f}')
|
| 206 |
+
|
| 207 |
+
valid_loss /= len(val_dataloader)
|
| 208 |
+
is_save_ckp = False
|
| 209 |
+
if valid_loss < best_valid_loss:
|
| 210 |
+
best_valid_loss = valid_loss
|
| 211 |
+
best_loss_epoch = epoch
|
| 212 |
+
world_rank == 0 and save_checkpoint(model, optimizer_g, optimizer_d, scheduler_g, scheduler_d, best_valid_loss, best_loss_epoch, cfg.checkpoint_dir)
|
| 213 |
+
is_save_ckp = True
|
| 214 |
+
scheduler_g.step(valid_loss)
|
| 215 |
+
scheduler_d.step(valid_loss)
|
| 216 |
+
|
| 217 |
+
if world_rank == 0:
|
| 218 |
+
logger.info(f"Epoch [{epoch + 1}/{cfg.max_epoch}], "
|
| 219 |
+
f"Train Loss: {train_loss:.4f}, "
|
| 220 |
+
f"Valid Loss: {valid_loss:.4f}, "
|
| 221 |
+
f"Best loss at Epoch: {best_loss_epoch + 1}"
|
| 222 |
+
+ (", saving checkpoint" if is_save_ckp else "")
|
| 223 |
+
)
|
| 224 |
+
train_losses = np.append(train_losses, train_loss)
|
| 225 |
+
valid_losses = np.append(valid_losses, valid_loss)
|
| 226 |
+
np.save(train_loss_file, train_losses)
|
| 227 |
+
np.save(valid_loss_file, valid_losses)
|
| 228 |
+
|
| 229 |
+
if epoch - best_loss_epoch > cfg.patience:
|
| 230 |
+
print(f"Loss has not decrease in {cfg.patience} epochs, stopping training...")
|
| 231 |
+
exit()
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def save_checkpoint(model, optimizer_g, optimizer_d, scheduler_g, scheduler_d, best_valid_loss, best_loss_epoch, model_path):
|
| 235 |
+
model_to_save = model.module if hasattr(model, "module") else model
|
| 236 |
+
state = {"model_state_dict": model_to_save.state_dict(),
|
| 237 |
+
"optimizer_g_state_dict": optimizer_g.state_dict(),
|
| 238 |
+
"optimizer_d_state_dict": optimizer_d.state_dict(),
|
| 239 |
+
"scheduler_g_state_dict": scheduler_g.state_dict(),
|
| 240 |
+
"scheduler_d_state_dict": scheduler_d.state_dict(),
|
| 241 |
+
"best_valid_loss": best_valid_loss,
|
| 242 |
+
"best_loss_epoch": best_loss_epoch,
|
| 243 |
+
}
|
| 244 |
+
torch.save(state, f"{model_path}/model.pth")
|
| 245 |
+
### the weight file saving may interrupted due to DCU queue limit, get a backup to ensure there at least has one model
|
| 246 |
+
os.system(f"mv {model_path}/model.pth {model_path}/model_bak.pth")
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
if __name__ == "__main__":
|
| 250 |
+
current_path = os.getcwd()
|
| 251 |
+
sys.path.append(current_path)
|
| 252 |
+
main()
|
weight/.gitkeep
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|