Upload folder using huggingface_hub
Browse files- README.md +140 -0
- conf/config.yaml +145 -0
- configuration.json +1 -0
- model/fengwu.py +278 -0
- scripts/fake_data.py +99 -0
- scripts/inference.py +81 -0
- scripts/result.py +217 -0
- scripts/train.py +236 -0
- weight/.gitkeep +0 -0
README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
- zh
|
| 6 |
+
tags:
|
| 7 |
+
- OneScience
|
| 8 |
+
- Earth Science
|
| 9 |
+
- Weather Forecast
|
| 10 |
+
- Short-to-Medium-Range Weather Forecast
|
| 11 |
+
- ERA5
|
| 12 |
+
frameworks: PyTorch
|
| 13 |
+
datasets:
|
| 14 |
+
- OneScience/ERA5
|
| 15 |
+
---
|
| 16 |
+
<p align="center">
|
| 17 |
+
<strong>
|
| 18 |
+
<span style="font-size: 30px;">FengWu</span>
|
| 19 |
+
</strong>
|
| 20 |
+
</p>
|
| 21 |
+
|
| 22 |
+
# Model Introduction
|
| 23 |
+
|
| 24 |
+
FengWu is a global medium-range weather forecast foundation model jointly released by the Shanghai Artificial Intelligence Laboratory and multiple universities. It has been adopted by organizations such as the Hong Kong Observatory for operational weather forecasting.
|
| 25 |
+
|
| 26 |
+
Paper: FengWu: Pushing the Skillful Global Medium-range Weather Forecast beyond 10 Days Lead
|
| 27 |
+
|
| 28 |
+
https://arxiv.org/abs/2304.02948
|
| 29 |
+
|
| 30 |
+
# Model Description
|
| 31 |
+
|
| 32 |
+
The FengWu model is built on a multi-modal and multi-task deep learning approach, without relying on traditional physical equations. It is trained entirely on ERA5 reanalysis data.
|
| 33 |
+
|
| 34 |
+
# Use Cases
|
| 35 |
+
|
| 36 |
+
| Scenario | Description |
|
| 37 |
+
| :---: | :--- |
|
| 38 |
+
| Weather Forecast Training | Train FengWu using ERA5 HDF5 data |
|
| 39 |
+
| Local Quick Validation | Use synthetic data to verify data loading, model training & inference, and inference result visualization. |
|
| 40 |
+
| ModelScope / OneCode Execution | Download as a standalone model package, install dependencies, and run scripts directly. |
|
| 41 |
+
| Multi-GPU Training | Launch multi-process training via `torchrun`. |
|
| 42 |
+
|
| 43 |
+
# Usage Guide
|
| 44 |
+
|
| 45 |
+
## 1. OneCode Usage
|
| 46 |
+
|
| 47 |
+
Experience intelligent one-click AI4S programming through the OneCode online environment:
|
| 48 |
+
|
| 49 |
+
[Click to Experience Intelligent One-Click AI4S Programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 50 |
+
|
| 51 |
+
## 2. Manual Installation and Usage
|
| 52 |
+
|
| 53 |
+
**Hardware Requirements**
|
| 54 |
+
|
| 55 |
+
- A GPU or DCU is recommended.
|
| 56 |
+
- CPU can be used for import and small-scale connectivity verification; full training and inference will be slow.
|
| 57 |
+
- DCU users must install DTK in advance. DTK 25.04.2 or above, or the OneScience recommended version matching your cluster, is recommended.
|
| 58 |
+
|
| 59 |
+
### Download the Model Package
|
| 60 |
+
|
| 61 |
+
```bash
|
| 62 |
+
modelscope download --model OneScience/FengWu --local_dir ./FengWu
|
| 63 |
+
cd FengWu
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
### Install the Runtime Environment
|
| 67 |
+
|
| 68 |
+
**DCU Environment**
|
| 69 |
+
|
| 70 |
+
```bash
|
| 71 |
+
# Please activate DTK and CONDA first
|
| 72 |
+
conda create -n onescience311 python=3.11 -y
|
| 73 |
+
conda activate onescience311
|
| 74 |
+
# uv installation is supported
|
| 75 |
+
pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
**GPU Environment**
|
| 79 |
+
```bash
|
| 80 |
+
# Please activate CONDA first
|
| 81 |
+
conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
|
| 82 |
+
conda activate onescience311
|
| 83 |
+
# uv installation is supported
|
| 84 |
+
pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
### Training Data Introduction
|
| 88 |
+
|
| 89 |
+
The OneScience community provides ERA5 data for training (due to file size limits, the current repository contains a slice of the full dataset). Users can download it with the command below and confirm that the data path in `conf/config.yaml` is set correctly:
|
| 90 |
+
|
| 91 |
+
```bash
|
| 92 |
+
modelscope download --dataset OneScience/ERA5 --local_dir ./data
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
### Training
|
| 96 |
+
|
| 97 |
+
Single GPU:
|
| 98 |
+
|
| 99 |
+
```bash
|
| 100 |
+
python scripts/train.py
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
Multi-GPU:
|
| 104 |
+
|
| 105 |
+
```bash
|
| 106 |
+
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
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
Training will save `model_bak.pth` under `data/checkpoints/`.
|
| 110 |
+
|
| 111 |
+
### Training Weights
|
| 112 |
+
|
| 113 |
+
This repository provides weights trained on 39 years of ERA5 reanalysis data in the `weights/` folder. The weight files will be uploaded soon and are expected to be available in the near future.
|
| 114 |
+
|
| 115 |
+
### Inference
|
| 116 |
+
|
| 117 |
+
```bash
|
| 118 |
+
python scripts/inference.py
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
Inference results will be saved to `result/output/`.
|
| 122 |
+
|
| 123 |
+
### Evaluation and Visualization
|
| 124 |
+
|
| 125 |
+
```bash
|
| 126 |
+
python scripts/result.py
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
You can specify a date and variable at the end of `result.py` for visualization.
|
| 130 |
+
|
| 131 |
+
# OneScience Official Information
|
| 132 |
+
|
| 133 |
+
| Platform | OneScience Main Repository | Skills Repository |
|
| 134 |
+
| --- | --- | --- |
|
| 135 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 136 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 137 |
+
|
| 138 |
+
# Citation & License
|
| 139 |
+
|
| 140 |
+
- This repository is a reproduction of the original FengWu paper.
|
conf/config.yaml
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model:
|
| 2 |
+
|
| 3 |
+
start_epoch: 0
|
| 4 |
+
max_epoch: 1
|
| 5 |
+
lr: 1E-3
|
| 6 |
+
num_blocks: 8
|
| 7 |
+
patch_size: [4, 4]
|
| 8 |
+
embed_dim: 192
|
| 9 |
+
num_heads: [6, 12, 12, 6]
|
| 10 |
+
window_size: [2, 6, 12]
|
| 11 |
+
pressure_level: 37
|
| 12 |
+
checkpoint_dir: "./data/checkpoints"
|
| 13 |
+
patience: 50
|
| 14 |
+
|
| 15 |
+
# 整个数据读取流程
|
| 16 |
+
datapipe:
|
| 17 |
+
name: "ERA5"
|
| 18 |
+
task: "weather_forecasting"
|
| 19 |
+
|
| 20 |
+
# dataset设定
|
| 21 |
+
dataset:
|
| 22 |
+
type: "hdf5"
|
| 23 |
+
data_dir: './data/' # "$ONESCIENCE_DATASETS_DIR/ERA5/newh5/"
|
| 24 |
+
|
| 25 |
+
train_time: [2000, 2001]
|
| 26 |
+
val_time: [2002]
|
| 27 |
+
test_time: [2003]
|
| 28 |
+
img_size: [721, 1440]
|
| 29 |
+
verbose: true
|
| 30 |
+
cache: false
|
| 31 |
+
# 气象变量
|
| 32 |
+
|
| 33 |
+
channels: ['10m_u_component_of_wind', '10m_v_component_of_wind', '2m_temperature', 'mean_sea_level_pressure',
|
| 34 |
+
|
| 35 |
+
'geopotential_1', 'geopotential_2', 'geopotential_3', 'geopotential_5', 'geopotential_7',
|
| 36 |
+
'geopotential_10', 'geopotential_20', 'geopotential_30', 'geopotential_50', 'geopotential_70',
|
| 37 |
+
'geopotential_100', 'geopotential_125', 'geopotential_150', 'geopotential_175', 'geopotential_200',
|
| 38 |
+
'geopotential_225', 'geopotential_250', 'geopotential_300', 'geopotential_350', 'geopotential_400',
|
| 39 |
+
'geopotential_450', 'geopotential_500', 'geopotential_550', 'geopotential_600', 'geopotential_650',
|
| 40 |
+
'geopotential_700', 'geopotential_750', 'geopotential_775', 'geopotential_800', 'geopotential_825',
|
| 41 |
+
'geopotential_850', 'geopotential_875', 'geopotential_900', 'geopotential_925', 'geopotential_950',
|
| 42 |
+
'geopotential_975', 'geopotential_1000',
|
| 43 |
+
|
| 44 |
+
'relative_humidity_1', 'relative_humidity_2', 'relative_humidity_3', 'relative_humidity_5', 'relative_humidity_7',
|
| 45 |
+
'relative_humidity_10', 'relative_humidity_20', 'relative_humidity_30', 'relative_humidity_50', 'relative_humidity_70',
|
| 46 |
+
'relative_humidity_100', 'relative_humidity_125', 'relative_humidity_150', 'relative_humidity_175', 'relative_humidity_200',
|
| 47 |
+
'relative_humidity_225', 'relative_humidity_250', 'relative_humidity_300', 'relative_humidity_350', 'relative_humidity_400',
|
| 48 |
+
'relative_humidity_450', 'relative_humidity_500', 'relative_humidity_550', 'relative_humidity_600', 'relative_humidity_650',
|
| 49 |
+
'relative_humidity_700', 'relative_humidity_750', 'relative_humidity_775', 'relative_humidity_800', 'relative_humidity_825',
|
| 50 |
+
'relative_humidity_850', 'relative_humidity_875', 'relative_humidity_900', 'relative_humidity_925', 'relative_humidity_950',
|
| 51 |
+
'relative_humidity_975', 'relative_humidity_1000',
|
| 52 |
+
|
| 53 |
+
'u_component_of_wind_1', 'u_component_of_wind_2', 'u_component_of_wind_3', 'u_component_of_wind_5', 'u_component_of_wind_7',
|
| 54 |
+
'u_component_of_wind_10', 'u_component_of_wind_20', 'u_component_of_wind_30', 'u_component_of_wind_50', 'u_component_of_wind_70',
|
| 55 |
+
'u_component_of_wind_100', 'u_component_of_wind_125', 'u_component_of_wind_150', 'u_component_of_wind_175', 'u_component_of_wind_200',
|
| 56 |
+
'u_component_of_wind_225', 'u_component_of_wind_250', 'u_component_of_wind_300', 'u_component_of_wind_350', 'u_component_of_wind_400',
|
| 57 |
+
'u_component_of_wind_450', 'u_component_of_wind_500', 'u_component_of_wind_550', 'u_component_of_wind_600', 'u_component_of_wind_650',
|
| 58 |
+
'u_component_of_wind_700', 'u_component_of_wind_750', 'u_component_of_wind_775', 'u_component_of_wind_800', 'u_component_of_wind_825',
|
| 59 |
+
'u_component_of_wind_850', 'u_component_of_wind_875', 'u_component_of_wind_900', 'u_component_of_wind_925', 'u_component_of_wind_950',
|
| 60 |
+
'u_component_of_wind_975', 'u_component_of_wind_1000',
|
| 61 |
+
|
| 62 |
+
'v_component_of_wind_1', 'v_component_of_wind_2', 'v_component_of_wind_3', 'v_component_of_wind_5', 'v_component_of_wind_7',
|
| 63 |
+
'v_component_of_wind_10', 'v_component_of_wind_20', 'v_component_of_wind_30', 'v_component_of_wind_50', 'v_component_of_wind_70',
|
| 64 |
+
'v_component_of_wind_100', 'v_component_of_wind_125', 'v_component_of_wind_150', 'v_component_of_wind_175', 'v_component_of_wind_200',
|
| 65 |
+
'v_component_of_wind_225', 'v_component_of_wind_250', 'v_component_of_wind_300', 'v_component_of_wind_350', 'v_component_of_wind_400',
|
| 66 |
+
'v_component_of_wind_450', 'v_component_of_wind_500', 'v_component_of_wind_550', 'v_component_of_wind_600', 'v_component_of_wind_650',
|
| 67 |
+
'v_component_of_wind_700', 'v_component_of_wind_750', 'v_component_of_wind_775', 'v_component_of_wind_800', 'v_component_of_wind_825',
|
| 68 |
+
'v_component_of_wind_850', 'v_component_of_wind_875', 'v_component_of_wind_900', 'v_component_of_wind_925', 'v_component_of_wind_950',
|
| 69 |
+
'v_component_of_wind_975', 'v_component_of_wind_1000',
|
| 70 |
+
|
| 71 |
+
'temperature_1', 'temperature_2', 'temperature_3', 'temperature_5', 'temperature_7',
|
| 72 |
+
'temperature_10', 'temperature_20', 'temperature_30', 'temperature_50', 'temperature_70',
|
| 73 |
+
'temperature_100', 'temperature_125', 'temperature_150', 'temperature_175', 'temperature_200',
|
| 74 |
+
'temperature_225', 'temperature_250', 'temperature_300', 'temperature_350', 'temperature_400',
|
| 75 |
+
'temperature_450', 'temperature_500', 'temperature_550', 'temperature_600', 'temperature_650',
|
| 76 |
+
'temperature_700', 'temperature_750', 'temperature_775', 'temperature_800', 'temperature_825',
|
| 77 |
+
'temperature_850', 'temperature_875', 'temperature_900', 'temperature_925', 'temperature_950',
|
| 78 |
+
'temperature_975', 'temperature_1000',
|
| 79 |
+
]
|
| 80 |
+
|
| 81 |
+
variables:
|
| 82 |
+
- "u10" # 10m U wind component
|
| 83 |
+
- "v10" # 10m V wind component
|
| 84 |
+
- "t2m" # 2m temperature
|
| 85 |
+
- "msl" # Mean sea level pressure
|
| 86 |
+
- "z500" # Geopotential at 500 hPa
|
| 87 |
+
- "t850" # Temperature at 850 hPa
|
| 88 |
+
|
| 89 |
+
# 时间配置
|
| 90 |
+
time_range: ["2000-01-01", "2020-12-31"]
|
| 91 |
+
time_steps: 1
|
| 92 |
+
time_res: 6
|
| 93 |
+
|
| 94 |
+
# 空间配置
|
| 95 |
+
spatial_resolution: [0.25, 0.25]
|
| 96 |
+
|
| 97 |
+
# 采样配置
|
| 98 |
+
num_samples: -1 # -1 表示使用全部数据
|
| 99 |
+
shuffle: true
|
| 100 |
+
random_seed: 42
|
| 101 |
+
|
| 102 |
+
# 领域特定配置
|
| 103 |
+
extra:
|
| 104 |
+
levels: [500, 850, 1000]
|
| 105 |
+
lat_range: [-90, 90]
|
| 106 |
+
lon_range: [0, 360]
|
| 107 |
+
|
| 108 |
+
# 数据转换配置
|
| 109 |
+
transforms:
|
| 110 |
+
- type: "Normalize"
|
| 111 |
+
params:
|
| 112 |
+
mean: [0.0, 0.0, 288.0, 101325.0, 50000.0, 270.0]
|
| 113 |
+
std: [5.0, 5.0, 15.0, 1000.0, 5000.0, 10.0]
|
| 114 |
+
keys: ["input", "target"]
|
| 115 |
+
|
| 116 |
+
- type: "ToTensor"
|
| 117 |
+
params:
|
| 118 |
+
keys: null # null表示转换所有numpy数组
|
| 119 |
+
|
| 120 |
+
# 其他配置
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
# DataLoader配置
|
| 124 |
+
dataloader:
|
| 125 |
+
mask_dtype: "float32"
|
| 126 |
+
batch_size: 1
|
| 127 |
+
num_workers: 1
|
| 128 |
+
pin_memory: true
|
| 129 |
+
drop_last: true
|
| 130 |
+
shuffle: false # 使用sampler时设为false
|
| 131 |
+
prefetch_factor: 2
|
| 132 |
+
persistent_workers: true
|
| 133 |
+
|
| 134 |
+
# 分布式配置
|
| 135 |
+
distributed:
|
| 136 |
+
enabled: true
|
| 137 |
+
sampler: "DistributedSampler"
|
| 138 |
+
rank: 0
|
| 139 |
+
world_size: 4
|
| 140 |
+
shuffle: true
|
| 141 |
+
seed: 42
|
| 142 |
+
drop_last: true
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
|
configuration.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"framework":"Pytorch","task":"other"}
|
model/fengwu.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
from dataclasses import dataclass
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
from torch import nn
|
| 7 |
+
from onescience.modules.encoder.fengwuencoder import FengWuEncoder
|
| 8 |
+
from onescience.modules.decoder.fengwudecoder import FengWuDecoder
|
| 9 |
+
from onescience.modules.fuser.fengwufuser import FengWuFuser
|
| 10 |
+
|
| 11 |
+
from onescience.models.meta import ModelMetaData
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class MetaData(ModelMetaData):
|
| 16 |
+
name: str = "Fengwu"
|
| 17 |
+
# Optimization
|
| 18 |
+
jit: bool = False # ONNX Ops Conflict
|
| 19 |
+
cuda_graphs: bool = True
|
| 20 |
+
amp: bool = True
|
| 21 |
+
# Inference
|
| 22 |
+
onnx_cpu: bool = False # No FFT op on CPU
|
| 23 |
+
onnx_gpu: bool = True
|
| 24 |
+
onnx_runtime: bool = True
|
| 25 |
+
# Physics informed
|
| 26 |
+
var_dim: int = 1
|
| 27 |
+
func_torch: bool = False
|
| 28 |
+
auto_grad: bool = False
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class Fengwu(nn.Module):
|
| 32 |
+
"""
|
| 33 |
+
FengWu 的主模型实现。
|
| 34 |
+
|
| 35 |
+
该模型由多个变量分支编码器、一个中分辨率三维 fuser,以及多个变量分支解码器组成。
|
| 36 |
+
|
| 37 |
+
结构顺序为:
|
| 38 |
+
|
| 39 |
+
- 多个 `FengWuEncoder`
|
| 40 |
+
- 分别编码 surface、Z、R、U、V、T 六个变量分支
|
| 41 |
+
- `FengWuFuser`
|
| 42 |
+
- 在统一三维网格 `(Variables, Height, Width)` 上融合中分辨率特征
|
| 43 |
+
- 多个 `FengWuDecoder`
|
| 44 |
+
- 分别恢复各变量分支输出
|
| 45 |
+
|
| 46 |
+
与 Pangu 不同,FengWu 不把所有变量直接拼成单一路径输入,而是先按变量族分支编码,
|
| 47 |
+
再在中分辨率层面做跨变量三维融合。
|
| 48 |
+
|
| 49 |
+
Reference:
|
| 50 |
+
- `FengWu: Pushing the Skillful Global Medium-range Weather Forecast beyond 10 Days Lead`
|
| 51 |
+
- https://arxiv.org/pdf/2304.02948.pdf
|
| 52 |
+
|
| 53 |
+
Args:
|
| 54 |
+
img_size (tuple[int, int]):
|
| 55 |
+
输入场空间尺寸 `(Height, Width)`。
|
| 56 |
+
pressure_level (int):
|
| 57 |
+
高空变量的层数。
|
| 58 |
+
embed_dim (int):
|
| 59 |
+
编码器高分辨率特征维度。
|
| 60 |
+
patch_size (tuple[int, int]):
|
| 61 |
+
二维 patch 切分尺寸 `(PatchHeight, PatchWidth)`。
|
| 62 |
+
num_heads (tuple[int, int, int, int]):
|
| 63 |
+
注意力头数配置,约定顺序为:
|
| 64 |
+
`(EncoderHighHeads, EncoderMiddleHeads, DecoderMiddleHeads, DecoderHighHeads)`。
|
| 65 |
+
window_size (tuple[int, int, int]):
|
| 66 |
+
FengWuFuser 的三维窗口大小。
|
| 67 |
+
"""
|
| 68 |
+
|
| 69 |
+
def __init__(
|
| 70 |
+
self,
|
| 71 |
+
img_size=(721, 1440),
|
| 72 |
+
pressure_level=37,
|
| 73 |
+
embed_dim=192,
|
| 74 |
+
patch_size=(4, 4),
|
| 75 |
+
num_heads=(6, 12, 12, 6),
|
| 76 |
+
window_size=(2, 6, 12),
|
| 77 |
+
):
|
| 78 |
+
super().__init__()
|
| 79 |
+
input_resolution = (
|
| 80 |
+
math.ceil(img_size[0] / patch_size[0]),
|
| 81 |
+
math.ceil(img_size[1] / patch_size[1]),
|
| 82 |
+
)
|
| 83 |
+
middle_resolution = (
|
| 84 |
+
math.ceil(input_resolution[0] / 2),
|
| 85 |
+
math.ceil(input_resolution[1] / 2),
|
| 86 |
+
)
|
| 87 |
+
encoder_num_heads = (num_heads[0], num_heads[1])
|
| 88 |
+
decoder_num_heads = (num_heads[3], num_heads[2])
|
| 89 |
+
drop_path = np.linspace(0, 0.2, 8).tolist()
|
| 90 |
+
|
| 91 |
+
self.encoder_surface = FengWuEncoder(
|
| 92 |
+
input_resolution=input_resolution,
|
| 93 |
+
middle_resolution=middle_resolution,
|
| 94 |
+
in_chans=4,
|
| 95 |
+
img_size=img_size,
|
| 96 |
+
patch_size=patch_size,
|
| 97 |
+
dim=embed_dim,
|
| 98 |
+
num_heads=encoder_num_heads,
|
| 99 |
+
window_size=window_size[1:],
|
| 100 |
+
drop_path=drop_path,
|
| 101 |
+
)
|
| 102 |
+
self.encoder_z = FengWuEncoder(
|
| 103 |
+
input_resolution=input_resolution,
|
| 104 |
+
middle_resolution=middle_resolution,
|
| 105 |
+
in_chans=pressure_level,
|
| 106 |
+
img_size=img_size,
|
| 107 |
+
patch_size=patch_size,
|
| 108 |
+
dim=embed_dim,
|
| 109 |
+
num_heads=encoder_num_heads,
|
| 110 |
+
window_size=window_size[1:],
|
| 111 |
+
drop_path=drop_path,
|
| 112 |
+
)
|
| 113 |
+
self.encoder_r = FengWuEncoder(
|
| 114 |
+
input_resolution=input_resolution,
|
| 115 |
+
middle_resolution=middle_resolution,
|
| 116 |
+
in_chans=pressure_level,
|
| 117 |
+
img_size=img_size,
|
| 118 |
+
patch_size=patch_size,
|
| 119 |
+
dim=embed_dim,
|
| 120 |
+
num_heads=encoder_num_heads,
|
| 121 |
+
window_size=window_size[1:],
|
| 122 |
+
drop_path=drop_path,
|
| 123 |
+
)
|
| 124 |
+
self.encoder_u = FengWuEncoder(
|
| 125 |
+
input_resolution=input_resolution,
|
| 126 |
+
middle_resolution=middle_resolution,
|
| 127 |
+
in_chans=pressure_level,
|
| 128 |
+
img_size=img_size,
|
| 129 |
+
patch_size=patch_size,
|
| 130 |
+
dim=embed_dim,
|
| 131 |
+
num_heads=encoder_num_heads,
|
| 132 |
+
window_size=window_size[1:],
|
| 133 |
+
drop_path=drop_path,
|
| 134 |
+
)
|
| 135 |
+
self.encoder_v = FengWuEncoder(
|
| 136 |
+
input_resolution=input_resolution,
|
| 137 |
+
middle_resolution=middle_resolution,
|
| 138 |
+
in_chans=pressure_level,
|
| 139 |
+
img_size=img_size,
|
| 140 |
+
patch_size=patch_size,
|
| 141 |
+
dim=embed_dim,
|
| 142 |
+
num_heads=encoder_num_heads,
|
| 143 |
+
window_size=window_size[1:],
|
| 144 |
+
drop_path=drop_path,
|
| 145 |
+
)
|
| 146 |
+
self.encoder_t = FengWuEncoder(
|
| 147 |
+
input_resolution=input_resolution,
|
| 148 |
+
middle_resolution=middle_resolution,
|
| 149 |
+
in_chans=pressure_level,
|
| 150 |
+
img_size=img_size,
|
| 151 |
+
patch_size=patch_size,
|
| 152 |
+
dim=embed_dim,
|
| 153 |
+
num_heads=encoder_num_heads,
|
| 154 |
+
window_size=window_size[1:],
|
| 155 |
+
drop_path=drop_path,
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
self.fuser = FengWuFuser(
|
| 159 |
+
input_resolution=(6, *middle_resolution),
|
| 160 |
+
dim=embed_dim * 2,
|
| 161 |
+
num_heads=num_heads[2],
|
| 162 |
+
window_size=window_size,
|
| 163 |
+
drop_path=drop_path[2:],
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
self.decoder_surface = FengWuDecoder(
|
| 167 |
+
output_resolution=input_resolution,
|
| 168 |
+
middle_resolution=middle_resolution,
|
| 169 |
+
out_chans=4,
|
| 170 |
+
img_size=img_size,
|
| 171 |
+
patch_size=patch_size,
|
| 172 |
+
dim=embed_dim,
|
| 173 |
+
num_heads=decoder_num_heads,
|
| 174 |
+
window_size=window_size[1:],
|
| 175 |
+
drop_path=drop_path,
|
| 176 |
+
)
|
| 177 |
+
self.decoder_z = FengWuDecoder(
|
| 178 |
+
output_resolution=input_resolution,
|
| 179 |
+
middle_resolution=middle_resolution,
|
| 180 |
+
out_chans=pressure_level,
|
| 181 |
+
img_size=img_size,
|
| 182 |
+
patch_size=patch_size,
|
| 183 |
+
dim=embed_dim,
|
| 184 |
+
num_heads=decoder_num_heads,
|
| 185 |
+
window_size=window_size[1:],
|
| 186 |
+
drop_path=drop_path,
|
| 187 |
+
)
|
| 188 |
+
self.decoder_r = FengWuDecoder(
|
| 189 |
+
output_resolution=input_resolution,
|
| 190 |
+
middle_resolution=middle_resolution,
|
| 191 |
+
out_chans=pressure_level,
|
| 192 |
+
img_size=img_size,
|
| 193 |
+
patch_size=patch_size,
|
| 194 |
+
dim=embed_dim,
|
| 195 |
+
num_heads=decoder_num_heads,
|
| 196 |
+
window_size=window_size[1:],
|
| 197 |
+
drop_path=drop_path,
|
| 198 |
+
)
|
| 199 |
+
self.decoder_u = FengWuDecoder(
|
| 200 |
+
output_resolution=input_resolution,
|
| 201 |
+
middle_resolution=middle_resolution,
|
| 202 |
+
out_chans=pressure_level,
|
| 203 |
+
img_size=img_size,
|
| 204 |
+
patch_size=patch_size,
|
| 205 |
+
dim=embed_dim,
|
| 206 |
+
num_heads=decoder_num_heads,
|
| 207 |
+
window_size=window_size[1:],
|
| 208 |
+
drop_path=drop_path,
|
| 209 |
+
)
|
| 210 |
+
self.decoder_v = FengWuDecoder(
|
| 211 |
+
output_resolution=input_resolution,
|
| 212 |
+
middle_resolution=middle_resolution,
|
| 213 |
+
out_chans=pressure_level,
|
| 214 |
+
img_size=img_size,
|
| 215 |
+
patch_size=patch_size,
|
| 216 |
+
dim=embed_dim,
|
| 217 |
+
num_heads=decoder_num_heads,
|
| 218 |
+
window_size=window_size[1:],
|
| 219 |
+
drop_path=drop_path,
|
| 220 |
+
)
|
| 221 |
+
self.decoder_t = FengWuDecoder(
|
| 222 |
+
output_resolution=input_resolution,
|
| 223 |
+
middle_resolution=middle_resolution,
|
| 224 |
+
out_chans=pressure_level,
|
| 225 |
+
img_size=img_size,
|
| 226 |
+
patch_size=patch_size,
|
| 227 |
+
dim=embed_dim,
|
| 228 |
+
num_heads=decoder_num_heads,
|
| 229 |
+
window_size=window_size[1:],
|
| 230 |
+
drop_path=drop_path,
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
self.img_size = img_size
|
| 234 |
+
self.pressure_level = pressure_level
|
| 235 |
+
self.patch_size = patch_size
|
| 236 |
+
self.input_resolution = input_resolution
|
| 237 |
+
self.middle_resolution = middle_resolution
|
| 238 |
+
|
| 239 |
+
def forward(self, surface, z, r, u, v, t):
|
| 240 |
+
surface, skip_surface = self.encoder_surface(surface)
|
| 241 |
+
z, skip_z = self.encoder_z(z)
|
| 242 |
+
r, skip_r = self.encoder_r(r)
|
| 243 |
+
u, skip_u = self.encoder_u(u)
|
| 244 |
+
v, skip_v = self.encoder_v(v)
|
| 245 |
+
t, skip_t = self.encoder_t(t)
|
| 246 |
+
|
| 247 |
+
x = torch.concat(
|
| 248 |
+
[
|
| 249 |
+
surface.unsqueeze(1),
|
| 250 |
+
z.unsqueeze(1),
|
| 251 |
+
r.unsqueeze(1),
|
| 252 |
+
u.unsqueeze(1),
|
| 253 |
+
v.unsqueeze(1),
|
| 254 |
+
t.unsqueeze(1),
|
| 255 |
+
],
|
| 256 |
+
dim=1,
|
| 257 |
+
)
|
| 258 |
+
Batch, Variables, NumTokensPerVariable, Channels = x.shape
|
| 259 |
+
x = x.reshape(Batch, -1, Channels)
|
| 260 |
+
x = self.fuser(x)
|
| 261 |
+
|
| 262 |
+
x = x.reshape(Batch, Variables, NumTokensPerVariable, Channels)
|
| 263 |
+
surface, z, r, u, v, t = (
|
| 264 |
+
x[:, 0, :, :],
|
| 265 |
+
x[:, 1, :, :],
|
| 266 |
+
x[:, 2, :, :],
|
| 267 |
+
x[:, 3, :, :],
|
| 268 |
+
x[:, 4, :, :],
|
| 269 |
+
x[:, 5, :, :],
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
surface = self.decoder_surface([surface, skip_surface])
|
| 273 |
+
z = self.decoder_z([z, skip_z])
|
| 274 |
+
r = self.decoder_r([r, skip_r])
|
| 275 |
+
u = self.decoder_u([u, skip_u])
|
| 276 |
+
v = self.decoder_v([v, skip_v])
|
| 277 |
+
t = self.decoder_t([t, skip_t])
|
| 278 |
+
return surface, z, r, u, v, t
|
scripts/fake_data.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
root_path = Path(__file__).parent.parent
|
| 5 |
+
sys.path.append(str(root_path))
|
| 6 |
+
import h5py
|
| 7 |
+
import numpy as np
|
| 8 |
+
import xarray as xr
|
| 9 |
+
from onescience.utils.YParams import YParams
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# 各数据集固定的空间和时间维度
|
| 13 |
+
DATASET_DIMS = {"T": 10, "H": 721, "W": 1440, "time_step": 6}
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def generate_fake_h5(data_dir, var_names, years, dims):
|
| 17 |
+
"""
|
| 18 |
+
为每个年份生成一个空 h5 文件。
|
| 19 |
+
利用 HDF5 chunked 数据集未写入 chunk 即返回 fill_value=0 的特性,
|
| 20 |
+
文件实际只含元数据,极小,但 shape 与真实数据完全一致。
|
| 21 |
+
均值/标准差也作为数据集内嵌进每年的 h5,与 era5.py 新版读取方式对应。
|
| 22 |
+
"""
|
| 23 |
+
os.makedirs(os.path.join(data_dir, "data"), exist_ok=True)
|
| 24 |
+
T, C = dims["T"], len(var_names)
|
| 25 |
+
H, W = dims["H"], dims["W"]
|
| 26 |
+
|
| 27 |
+
means = np.zeros((1, C, 1, 1), dtype=np.float32)
|
| 28 |
+
stds = np.ones((1, C, 1, 1), dtype=np.float32)
|
| 29 |
+
|
| 30 |
+
for year in years:
|
| 31 |
+
path = os.path.join(data_dir, "data", f"{year}.h5")
|
| 32 |
+
with h5py.File(path, "w") as f:
|
| 33 |
+
ds = f.create_dataset(
|
| 34 |
+
"fields",
|
| 35 |
+
shape=(T, C, H, W),
|
| 36 |
+
dtype="float32",
|
| 37 |
+
chunks=(1, C, H, W),
|
| 38 |
+
fillvalue=0.0,
|
| 39 |
+
)
|
| 40 |
+
ds.attrs["variables"] = var_names
|
| 41 |
+
ds.attrs["time_step"] = dims["time_step"]
|
| 42 |
+
f.create_dataset("global_means", data=means)
|
| 43 |
+
f.create_dataset("global_stds", data=stds)
|
| 44 |
+
|
| 45 |
+
size_kb = os.path.getsize(path) / 1024
|
| 46 |
+
print(f" {year}.h5 shape=({T},{C},{H},{W}) "
|
| 47 |
+
f"logical={T*C*H*W*4/1024**3:.1f}GB actual={size_kb:.1f}KB")
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def get_static(data_dir, var, name):
|
| 51 |
+
os.makedirs(data_dir, exist_ok=True)
|
| 52 |
+
ds = xr.Dataset(
|
| 53 |
+
data_vars={
|
| 54 |
+
f"{var}": (("valid_time", "latitude", "longitude"),
|
| 55 |
+
np.random.rand(1, 721, 1440).astype(np.float32))
|
| 56 |
+
},
|
| 57 |
+
coords={
|
| 58 |
+
"valid_time": ["2015-12-31"],
|
| 59 |
+
"latitude": np.linspace(90, -90, 721, dtype=np.float64),
|
| 60 |
+
"longitude": np.linspace(0, 359.75, 1440, dtype=np.float64),
|
| 61 |
+
"number": 0,
|
| 62 |
+
"expver": "",
|
| 63 |
+
},
|
| 64 |
+
attrs={
|
| 65 |
+
"GRIB_centre": "ecmf",
|
| 66 |
+
"GRIB_centreDescription": "European Centre for Medium-Range Weather Forecasts",
|
| 67 |
+
"GRIB_subCentre": "0",
|
| 68 |
+
"Conventions": "CF-1.7",
|
| 69 |
+
"institution": "European Centre for Medium-Range Weather Forecasts",
|
| 70 |
+
"history": "Generated manually",
|
| 71 |
+
}
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
ds.to_netcdf(f"{data_dir}/{name}.nc")
|
| 75 |
+
arr = np.random.randn(721, 1440).astype(np.float32)
|
| 76 |
+
np.save(f'{data_dir}/land_mask.npy', arr)
|
| 77 |
+
np.save(f'{data_dir}/soil_type.npy', arr)
|
| 78 |
+
np.save(f'{data_dir}/topography.npy', arr)
|
| 79 |
+
print(f"✅ Static data: {arr.shape}, dtype: {arr.dtype}, save to {data_dir}")
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
if __name__ == "__main__":
|
| 83 |
+
cfg_datapipe = YParams("conf/config.yaml", "datapipe")
|
| 84 |
+
|
| 85 |
+
if cfg_datapipe.dataset.data_dir.startswith("/public/") or cfg_datapipe.dataset.data_dir.startswith("/work2/"):
|
| 86 |
+
print("请检查 config,确保各 *_dir 指向本地测试路径而非生产路径。")
|
| 87 |
+
exit()
|
| 88 |
+
|
| 89 |
+
years = cfg_datapipe.dataset.train_time + cfg_datapipe.dataset.val_time + cfg_datapipe.dataset.test_time
|
| 90 |
+
atm_vars = cfg_datapipe.dataset.channels
|
| 91 |
+
|
| 92 |
+
generate_fake_h5(cfg_datapipe.dataset.data_dir, atm_vars, years, DATASET_DIMS)
|
| 93 |
+
|
| 94 |
+
static_dir = os.path.join(cfg_datapipe.dataset.data_dir, "static")
|
| 95 |
+
get_static(static_dir, 'z', 'geopotential')
|
| 96 |
+
get_static(static_dir, 'lsm', 'land_sea_mask')
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
print("\n✅ Fake datasets generated.")
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
root_path = Path(__file__).parent.parent
|
| 6 |
+
sys.path.append(str(root_path))
|
| 7 |
+
import glob
|
| 8 |
+
import numpy as np
|
| 9 |
+
import h5py
|
| 10 |
+
from tqdm import tqdm
|
| 11 |
+
from model.fengwu import Fengwu
|
| 12 |
+
from onescience.utils.YParams import YParams
|
| 13 |
+
from onescience.datapipes.climate import ERA5Datapipe
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def get_stats(data_dir, channels):
|
| 17 |
+
"""从新版 h5 中读取变量列表与归一化参数(均值/标准差)"""
|
| 18 |
+
h5_files = sorted(glob.glob(os.path.join(data_dir, "data", "*.h5")))
|
| 19 |
+
with h5py.File(h5_files[0], "r") as f:
|
| 20 |
+
ds = f["fields"]
|
| 21 |
+
all_variables = [v.decode() if isinstance(v, bytes) else v for v in ds.attrs["variables"]]
|
| 22 |
+
mu = f["global_means"][:] # [1, C, 1, 1]
|
| 23 |
+
std = f["global_stds"][:]
|
| 24 |
+
|
| 25 |
+
channel_indices = [all_variables.index(v) for v in channels]
|
| 26 |
+
means = mu[:, channel_indices, :, :]
|
| 27 |
+
stds = std[:, channel_indices, :, :]
|
| 28 |
+
return means, stds
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
if __name__ == "__main__":
|
| 32 |
+
current_path = os.getcwd()
|
| 33 |
+
sys.path.append(current_path)
|
| 34 |
+
|
| 35 |
+
## Model config init
|
| 36 |
+
config_file_path = os.path.join(current_path, "conf/config.yaml")
|
| 37 |
+
cfg = YParams(config_file_path, "model")
|
| 38 |
+
|
| 39 |
+
## DataLoader init
|
| 40 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 41 |
+
means, stds = get_stats(cfg_data.dataset.data_dir, cfg_data.dataset.channels)
|
| 42 |
+
|
| 43 |
+
datapipe = ERA5Datapipe(
|
| 44 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 45 |
+
used_variables=cfg_data.dataset.channels,
|
| 46 |
+
used_years=cfg_data.dataset.test_time,
|
| 47 |
+
distributed=False,
|
| 48 |
+
batch_size=1,
|
| 49 |
+
num_workers=4,
|
| 50 |
+
)
|
| 51 |
+
test_dataloader, _ = datapipe.get_dataloader("test")
|
| 52 |
+
|
| 53 |
+
ckpt = torch.load(f"{cfg.checkpoint_dir}/model_bak.pth", map_location="cuda:0")
|
| 54 |
+
model = Fengwu(img_size=cfg_data.dataset.img_size,
|
| 55 |
+
pressure_level=cfg.pressure_level,
|
| 56 |
+
embed_dim=cfg.embed_dim,
|
| 57 |
+
patch_size=cfg.patch_size,
|
| 58 |
+
num_heads=cfg.num_heads,
|
| 59 |
+
window_size=cfg.window_size,
|
| 60 |
+
).to('cuda:0')
|
| 61 |
+
model.load_state_dict(ckpt["model_state_dict"])
|
| 62 |
+
|
| 63 |
+
model.eval()
|
| 64 |
+
os.makedirs('result/output/', exist_ok=True)
|
| 65 |
+
print(f"📂 samples will be generated to './result/output/'")
|
| 66 |
+
with torch.no_grad():
|
| 67 |
+
for data in tqdm(test_dataloader, desc="Inferring testset", unit="batch"):
|
| 68 |
+
invar = data[0].to("cuda:0", dtype=torch.float32)
|
| 69 |
+
outvar = data[1].to("cuda:0", dtype=torch.float32)
|
| 70 |
+
filename = data[4][-1][0]
|
| 71 |
+
surface = invar[:, :4, :, :]
|
| 72 |
+
z = invar[:, 4:41, :, :]
|
| 73 |
+
r = invar[:, 41:78, :, :]
|
| 74 |
+
u = invar[:, 78:115, :, :]
|
| 75 |
+
v = invar[:, 115:152, :, :]
|
| 76 |
+
t = invar[:, 152:189, :, :]
|
| 77 |
+
|
| 78 |
+
surface_p, z_p, r_p, u_p, v_p, t_p = model(surface, z, r, u, v, t)
|
| 79 |
+
pred_var = torch.concat([surface_p, z_p, r_p, u_p, v_p, t_p], dim=1).cpu().numpy()
|
| 80 |
+
pred_var = pred_var * stds + means
|
| 81 |
+
np.save(f"result/output/{filename}.npy", pred_var)
|
scripts/result.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
root_path = Path(__file__).parent.parent
|
| 7 |
+
sys.path.append(str(root_path))
|
| 8 |
+
import glob
|
| 9 |
+
import h5py
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
from tqdm import tqdm
|
| 12 |
+
from onescience.utils.fcn.YParams import YParams
|
| 13 |
+
from matplotlib import rcParams
|
| 14 |
+
|
| 15 |
+
# rcParams['font.family'] = 'serif'
|
| 16 |
+
# rcParams['font.serif'] = ['DejaVu Serif']
|
| 17 |
+
rcParams['mathtext.fontset'] = 'stix'
|
| 18 |
+
rcParams['axes.linewidth'] = 0.9
|
| 19 |
+
rcParams['xtick.major.width'] = 0.9
|
| 20 |
+
rcParams['ytick.major.width'] = 0.9
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def get_metadata(data_dir, channels):
|
| 24 |
+
"""从新版 h5 attrs 中读取变量列表和 time_step"""
|
| 25 |
+
h5_files = sorted(glob.glob(os.path.join(data_dir, "data", "*.h5")))
|
| 26 |
+
with h5py.File(h5_files[0], "r") as f:
|
| 27 |
+
ds = f["fields"]
|
| 28 |
+
all_variables = [v.decode() if isinstance(v, bytes) else v for v in ds.attrs["variables"]]
|
| 29 |
+
time_step = int(ds.attrs["time_step"])
|
| 30 |
+
|
| 31 |
+
channel_indices = [all_variables.index(v) for v in channels]
|
| 32 |
+
|
| 33 |
+
total_files = [f for f in os.listdir('./result/output/') if f.endswith('.npy')]
|
| 34 |
+
total_files.sort()
|
| 35 |
+
return total_files, channel_indices, time_step
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def filename_to_index(filename, time_step):
|
| 39 |
+
"""将 YYYYMMDDHH 格式的文件名转换为年度 h5 文件中的时间步索引"""
|
| 40 |
+
dt = datetime.strptime(filename, "%Y%m%d%H")
|
| 41 |
+
year_start = datetime(dt.year, 1, 1)
|
| 42 |
+
hours = (dt - year_start).total_seconds() / 3600
|
| 43 |
+
return int(hours / time_step)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def get_result(total_files, channel_indices, time_step, data_dir, clim_mean):
|
| 47 |
+
channel_rmse = np.zeros(len(channel_indices))
|
| 48 |
+
channel_acc = np.zeros(len(channel_indices))
|
| 49 |
+
clim_mean = clim_mean[0, :, :, :]
|
| 50 |
+
if not os.path.exists('./result/rmse.npy') or not os.path.exists('result/acc.npy'):
|
| 51 |
+
numerator = np.zeros(len(channel_indices))
|
| 52 |
+
pred_sq_sum = np.zeros(len(channel_indices))
|
| 53 |
+
label_sq_sum = np.zeros(len(channel_indices))
|
| 54 |
+
for file in tqdm(total_files, unit="files"):
|
| 55 |
+
fname = file[:-4] # 去掉 .npy
|
| 56 |
+
year = fname[:4]
|
| 57 |
+
t_idx = filename_to_index(fname, time_step)
|
| 58 |
+
with h5py.File(os.path.join(data_dir, 'data', f'{year}.h5'), "r") as f:
|
| 59 |
+
label = f["fields"][t_idx] # [C, H, W]
|
| 60 |
+
label = label[channel_indices]
|
| 61 |
+
pred = np.load(f'result/output/{file}').squeeze()
|
| 62 |
+
|
| 63 |
+
label_anom = label - clim_mean
|
| 64 |
+
pred_anom = pred - clim_mean
|
| 65 |
+
# 累加
|
| 66 |
+
numerator += np.sum(pred_anom * label_anom, axis=(1, 2))
|
| 67 |
+
pred_sq_sum += np.sum(pred_anom ** 2, axis=(1, 2))
|
| 68 |
+
label_sq_sum += np.sum(label_anom ** 2, axis=(1, 2))
|
| 69 |
+
|
| 70 |
+
channel_rmse += np.sqrt(np.mean((label - pred) ** 2, axis=(1, 2)))
|
| 71 |
+
channel_rmse /= len(total_files)
|
| 72 |
+
channel_acc = numerator / (np.sqrt(pred_sq_sum * label_sq_sum) + 1e-8)
|
| 73 |
+
np.save('./result/acc.npy', channel_acc)
|
| 74 |
+
np.save('./result/rmse.npy', channel_rmse)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def show_result():
|
| 78 |
+
channel_rmse = np.load('./result/rmse.npy')
|
| 79 |
+
channel_acc = np.load('./result/acc.npy')
|
| 80 |
+
|
| 81 |
+
channels = [cfg_data.dataset.channels[i] for i in range(len(channel_indices))]
|
| 82 |
+
w = 24 # 最长 channel 名宽度
|
| 83 |
+
|
| 84 |
+
# 表头
|
| 85 |
+
print(f"┌{'─' * (w + 2)}┬{'─' * 14}┬{'─' * 14}┐")
|
| 86 |
+
print(f"│ {'Channel':<{w}} │ {'RMSE':>12} │ {'ACC':>12} │")
|
| 87 |
+
print(f"├{'─' * (w + 2)}┼{'─' * 14}┼{'─' * 14}┤")
|
| 88 |
+
# 数据行
|
| 89 |
+
for i, ch in enumerate(channels):
|
| 90 |
+
print(f"│ {ch:<{w}} │ {channel_rmse[i]:>12.4f} | {channel_acc[i]:>12.4f} |")
|
| 91 |
+
print(f"├{'─' * (w + 2)}┼{'─' * 14}┼{'─' * 14}┤")
|
| 92 |
+
print(f"│ {'Average':<{w}} │ {np.mean(channel_rmse):>12.4f} │ {np.mean(channel_acc):>12.4f} │")
|
| 93 |
+
print(f"└{'─' * (w + 2)}┴{'─' * 14}┴{'─' * 14}┘")
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def plot(label, pred, var, filename):
|
| 97 |
+
# 基础设置
|
| 98 |
+
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
|
| 99 |
+
|
| 100 |
+
# 坐标轴标签
|
| 101 |
+
xtick_labels = ['180°W', '90°W', '0°', '90°E', '180°E']
|
| 102 |
+
ytick_labels = ['90°S', '45°S', '0°', '45°N', '90°N']
|
| 103 |
+
xticks = np.linspace(0, label.shape[-1] - 1, 5)
|
| 104 |
+
yticks = np.linspace(0, label.shape[-2] - 1, 5)
|
| 105 |
+
|
| 106 |
+
# 计算统一色条范围
|
| 107 |
+
vmin = min(label.min(), pred.min())
|
| 108 |
+
vmax = max(label.max(), pred.max())
|
| 109 |
+
|
| 110 |
+
# 计算差异和 RMSE
|
| 111 |
+
diff = label - pred
|
| 112 |
+
rmse = np.sqrt(np.mean(diff ** 2))
|
| 113 |
+
diff_abs_max = np.abs(diff).max()
|
| 114 |
+
|
| 115 |
+
# 绘图配置
|
| 116 |
+
plot_configs = [
|
| 117 |
+
{'data': label, 'title': 'Truth', 'cmap': 'viridis', 'vmin': vmin, 'vmax': vmax},
|
| 118 |
+
{'data': pred, 'title': 'Prediction', 'cmap': 'viridis', 'vmin': vmin, 'vmax': vmax},
|
| 119 |
+
{'data': diff, 'title': f'Difference (RMSE={rmse:.2f})', 'cmap': 'RdBu_r', 'vmin': -diff_abs_max, 'vmax': diff_abs_max},
|
| 120 |
+
]
|
| 121 |
+
|
| 122 |
+
# 统一绘制
|
| 123 |
+
for ax, cfg in zip(axes, plot_configs):
|
| 124 |
+
im = ax.imshow(cfg['data'], cmap=cfg['cmap'], vmin=cfg['vmin'], vmax=cfg['vmax'])
|
| 125 |
+
ax.set_title(cfg['title'], fontsize=12, pad=4)
|
| 126 |
+
ax.set_xlabel('Longitude')
|
| 127 |
+
ax.set_ylabel('Latitude')
|
| 128 |
+
ax.set_xticks(xticks)
|
| 129 |
+
ax.set_xticklabels(xtick_labels)
|
| 130 |
+
ax.set_yticks(yticks)
|
| 131 |
+
ax.set_yticklabels(ytick_labels)
|
| 132 |
+
plt.colorbar(im, ax=ax, orientation='horizontal')
|
| 133 |
+
|
| 134 |
+
# 总标题
|
| 135 |
+
fig.suptitle(var, fontsize=14, fontweight='bold', y=0.98)
|
| 136 |
+
|
| 137 |
+
plt.savefig(filename, dpi=300, bbox_inches='tight')
|
| 138 |
+
plt.close()
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def plot_loss(train_loss, valid_loss):
|
| 142 |
+
|
| 143 |
+
mask = ~(np.isnan(train_loss) | np.isnan(valid_loss))
|
| 144 |
+
train_loss = train_loss[mask]
|
| 145 |
+
valid_loss = valid_loss[mask]
|
| 146 |
+
|
| 147 |
+
fig, ax = plt.subplots(figsize=(5, 3.5))
|
| 148 |
+
# 配置
|
| 149 |
+
colors = {'train': '#2563EB', 'valid': '#EA580C'}
|
| 150 |
+
epochs = np.arange(1, len(train_loss) + 1)
|
| 151 |
+
|
| 152 |
+
# 绑定曲线
|
| 153 |
+
ax.plot(epochs, train_loss, color=colors['train'], linewidth=1.5, label='Train')
|
| 154 |
+
ax.plot(epochs, valid_loss, color=colors['valid'], linewidth=1.5, label='Valid', linestyle='--')
|
| 155 |
+
# 标注最小值
|
| 156 |
+
min_idx = np.argmin(valid_loss)
|
| 157 |
+
ax.scatter(epochs[min_idx], valid_loss[min_idx],
|
| 158 |
+
color=colors['valid'], s=40, zorder=5, edgecolors='white')
|
| 159 |
+
ax.annotate(f'Best: {valid_loss[min_idx]:.3f}',
|
| 160 |
+
xy=(epochs[min_idx], valid_loss[min_idx]),
|
| 161 |
+
xytext=(10, 10), textcoords='offset points', fontsize=8, color=colors['valid'],
|
| 162 |
+
arrowprops=dict(arrowstyle='-', color=colors['valid'], lw=0.5))
|
| 163 |
+
|
| 164 |
+
# 坐标轴
|
| 165 |
+
ax.set(xlabel='Epoch', ylabel='Loss', xlim=(0, len(train_loss) + 1))
|
| 166 |
+
|
| 167 |
+
# 样式
|
| 168 |
+
ax.legend(frameon=False, loc='upper right')
|
| 169 |
+
ax.grid(True, linestyle='--', alpha=0.3)
|
| 170 |
+
ax.spines[['top', 'right']].set_visible(False)
|
| 171 |
+
|
| 172 |
+
plt.tight_layout()
|
| 173 |
+
plt.savefig('./result/loss.png', dpi=300, bbox_inches='tight')
|
| 174 |
+
plt.close()
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
if __name__ == "__main__":
|
| 178 |
+
current_path = os.getcwd()
|
| 179 |
+
sys.path.append(current_path)
|
| 180 |
+
config_file_path = os.path.join(current_path, 'conf/config.yaml')
|
| 181 |
+
cfg = YParams(config_file_path, 'model')
|
| 182 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 183 |
+
|
| 184 |
+
train_loss = np.load('./data/checkpoints/trloss.npy')
|
| 185 |
+
valid_loss = np.load('./data/checkpoints/valoss.npy')
|
| 186 |
+
plot_loss(train_loss, valid_loss)
|
| 187 |
+
|
| 188 |
+
data_dir = cfg_data.dataset.data_dir
|
| 189 |
+
total_files, channel_indices, time_step = get_metadata(data_dir, cfg_data.dataset.channels)
|
| 190 |
+
|
| 191 |
+
# Load data & Compute RMSE/ACC per channel
|
| 192 |
+
h5_files = sorted(glob.glob(os.path.join(data_dir, "data", "*.h5")))
|
| 193 |
+
with h5py.File(h5_files[0], "r") as f:
|
| 194 |
+
mu = f["global_means"][:]
|
| 195 |
+
clim_mean = mu[:, channel_indices, :, :]
|
| 196 |
+
get_result(total_files, channel_indices, time_step, data_dir, clim_mean)
|
| 197 |
+
show_result()
|
| 198 |
+
|
| 199 |
+
##### 默认绘制 test_time 第一年的第一个时间步,用户可自行指定日期和变量 #####
|
| 200 |
+
test_year = cfg_data.dataset.test_time[0]
|
| 201 |
+
eg_files = [f'{test_year}010206']
|
| 202 |
+
channel_index = [cfg_data.dataset.channels.index(v) for v in ['2m_temperature', 'geopotential_500', 'temperature_500']]
|
| 203 |
+
|
| 204 |
+
selected_var = [cfg_data.dataset.channels[int(i)] for i in channel_index]
|
| 205 |
+
print(f"seleted date: {eg_files}")
|
| 206 |
+
print(f"selected channels: {selected_var}")
|
| 207 |
+
for file in eg_files:
|
| 208 |
+
year = file[:4]
|
| 209 |
+
t_idx = filename_to_index(file, time_step)
|
| 210 |
+
with h5py.File(os.path.join(data_dir, 'data', f'{year}.h5'), "r") as f:
|
| 211 |
+
label = f["fields"][t_idx] # [C, H, W]
|
| 212 |
+
label = label[channel_indices]
|
| 213 |
+
pred = np.load(f'result/output/{file}.npy').squeeze()
|
| 214 |
+
for i in range(len(selected_var)):
|
| 215 |
+
filename = f'./result/{file}_{selected_var[i]}.png'
|
| 216 |
+
plot(label[channel_index[i]], pred[channel_index[i]], selected_var[i], filename)
|
| 217 |
+
print(f'✅plot {filename}')
|
scripts/train.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
root_path = Path(__file__).parent.parent
|
| 6 |
+
sys.path.append(str(root_path))
|
| 7 |
+
import numpy as np
|
| 8 |
+
import torch.distributed as dist
|
| 9 |
+
import logging
|
| 10 |
+
import time
|
| 11 |
+
from tqdm import tqdm
|
| 12 |
+
from torch.nn.parallel import DistributedDataParallel
|
| 13 |
+
from model.fengwu import Fengwu
|
| 14 |
+
from onescience.datapipes.climate import ERA5Datapipe
|
| 15 |
+
from onescience.utils.YParams import YParams
|
| 16 |
+
from onescience.memory.checkpoint import replace_function
|
| 17 |
+
from onescience.utils.fcn.darcy_loss import LpLoss
|
| 18 |
+
from apex import optimizers
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def loss_func(x, y):
|
| 22 |
+
return torch.nn.functional.l1_loss(x, y)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def main():
|
| 26 |
+
|
| 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:
|
| 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 |
+
|
| 45 |
+
## DataLoader init
|
| 46 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 47 |
+
datapipe = ERA5Datapipe(
|
| 48 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 49 |
+
used_variables=cfg_data.dataset.channels,
|
| 50 |
+
used_years=cfg_data.dataset.train_time,
|
| 51 |
+
distributed=dist.is_initialized()
|
| 52 |
+
)
|
| 53 |
+
train_dataloader, train_sampler = datapipe.get_dataloader("train")
|
| 54 |
+
datapipe = ERA5Datapipe(
|
| 55 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 56 |
+
used_variables=cfg_data.dataset.channels,
|
| 57 |
+
used_years=cfg_data.dataset.val_time,
|
| 58 |
+
distributed=dist.is_initialized()
|
| 59 |
+
)
|
| 60 |
+
val_dataloader, val_sampler = datapipe.get_dataloader("valid")
|
| 61 |
+
|
| 62 |
+
## Model init
|
| 63 |
+
model = Fengwu(img_size=cfg_data.dataset.img_size,
|
| 64 |
+
pressure_level=cfg.pressure_level,
|
| 65 |
+
embed_dim=cfg.embed_dim,
|
| 66 |
+
patch_size=cfg.patch_size,
|
| 67 |
+
num_heads=cfg.num_heads,
|
| 68 |
+
window_size=cfg.window_size,
|
| 69 |
+
).to(local_rank)
|
| 70 |
+
optimizer = optimizers.FusedAdam(model.parameters(), lr=cfg.lr)
|
| 71 |
+
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer,factor=0.2,patience=5,mode="min")
|
| 72 |
+
loss_obj = LpLoss()
|
| 73 |
+
|
| 74 |
+
## Train process init
|
| 75 |
+
os.makedirs(cfg.checkpoint_dir, exist_ok=True)
|
| 76 |
+
train_loss_file = f"{cfg.checkpoint_dir}/trloss.npy"
|
| 77 |
+
valid_loss_file = f"{cfg.checkpoint_dir}/valoss.npy"
|
| 78 |
+
best_valid_loss = 1.0e6
|
| 79 |
+
best_loss_epoch = 0
|
| 80 |
+
train_losses = np.empty((0,), dtype=np.float32)
|
| 81 |
+
valid_losses = np.empty((0,), dtype=np.float32)
|
| 82 |
+
|
| 83 |
+
## Get model params count
|
| 84 |
+
if cfg.world_size == 1:
|
| 85 |
+
total_params = sum(p.numel() for p in model.parameters())
|
| 86 |
+
print("\n\n")
|
| 87 |
+
print("-" * 50)
|
| 88 |
+
print(f"📂 now params is {total_params}, {total_params / 1e6:.2f}M, {total_params / 1e9:.2f}B")
|
| 89 |
+
print("-" * 50, "\n")
|
| 90 |
+
|
| 91 |
+
## Load model weight if there exist well-trained model
|
| 92 |
+
if os.path.exists(f"{cfg.checkpoint_dir}/model_bak.pth"):
|
| 93 |
+
if world_rank == 0:
|
| 94 |
+
print("\n\n")
|
| 95 |
+
print("-" * 50)
|
| 96 |
+
print(f"✅ There has a model weight, load and continue training...")
|
| 97 |
+
print(f'If you want to train a new model, ensure there is no *.pth file in {cfg.checkpoint_dir}')
|
| 98 |
+
print("-" * 50, "\n")
|
| 99 |
+
ckpt = torch.load(f"{cfg.checkpoint_dir}/model_bak.pth", map_location=f'cuda:{local_rank}', weights_only=False)
|
| 100 |
+
model.load_state_dict(ckpt["model_state_dict"])
|
| 101 |
+
optimizer.load_state_dict(ckpt["optimizer_state_dict"])
|
| 102 |
+
scheduler.load_state_dict(ckpt["scheduler_state_dict"])
|
| 103 |
+
best_valid_loss = ckpt["best_valid_loss"]
|
| 104 |
+
best_loss_epoch = ckpt["best_loss_epoch"]
|
| 105 |
+
train_losses = np.load(train_loss_file)
|
| 106 |
+
valid_losses = np.load(valid_loss_file)
|
| 107 |
+
|
| 108 |
+
## Distributed model
|
| 109 |
+
if cfg.world_size > 1:
|
| 110 |
+
model = DistributedDataParallel(model, device_ids=[local_rank], output_device=local_rank, find_unused_parameters=True)
|
| 111 |
+
|
| 112 |
+
world_rank == 0 and logger.info(f"start training ...")
|
| 113 |
+
|
| 114 |
+
for epoch in range(cfg.max_epoch):
|
| 115 |
+
if dist.is_initialized():
|
| 116 |
+
train_sampler.set_epoch(epoch)
|
| 117 |
+
val_sampler.set_epoch(epoch)
|
| 118 |
+
|
| 119 |
+
model.train()
|
| 120 |
+
train_loss = 0
|
| 121 |
+
start_time = time.time()
|
| 122 |
+
for j, data in enumerate(train_dataloader):
|
| 123 |
+
invar = data[0].to(local_rank, dtype=torch.float32)
|
| 124 |
+
outvar = data[1].to(local_rank, dtype=torch.float32)
|
| 125 |
+
surface = invar[:, :4, :, :]
|
| 126 |
+
z = invar[:, 4:41, :, :]
|
| 127 |
+
r = invar[:, 41:78, :, :]
|
| 128 |
+
u = invar[:, 78:115, :, :]
|
| 129 |
+
v = invar[:, 115:152, :, :]
|
| 130 |
+
t = invar[:, 152:189, :, :]
|
| 131 |
+
|
| 132 |
+
with replace_function(model,
|
| 133 |
+
["encoder_surface","encoder_z","encoder_r","encoder_u","encoder_v","encoder_t","fuser"],
|
| 134 |
+
cfg.world_size > 1,):
|
| 135 |
+
surface_p, z_p, r_p, u_p, v_p, t_p = model(surface, z, r, u, v, t)
|
| 136 |
+
|
| 137 |
+
outvar_pred = torch.concat([surface_p, z_p, r_p, u_p, v_p, t_p],dim=1)
|
| 138 |
+
|
| 139 |
+
loss = loss_obj(outvar, outvar_pred)
|
| 140 |
+
|
| 141 |
+
optimizer.zero_grad()
|
| 142 |
+
loss.backward()
|
| 143 |
+
optimizer.step()
|
| 144 |
+
train_loss += loss.item()
|
| 145 |
+
if world_rank == 0:
|
| 146 |
+
logger.info(f'Train: Epoch {epoch}-{j+1}/{len(train_dataloader)} '
|
| 147 |
+
f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
|
| 148 |
+
f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
|
| 149 |
+
f'loss:{train_loss / (j+1): .04f}')
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
train_loss /= len(train_dataloader)
|
| 153 |
+
|
| 154 |
+
model.eval()
|
| 155 |
+
valid_loss = 0
|
| 156 |
+
val_batch_time = time.time()
|
| 157 |
+
with torch.no_grad():
|
| 158 |
+
for j, data in enumerate(val_dataloader):
|
| 159 |
+
invar = data[0].to(local_rank, dtype=torch.float32)
|
| 160 |
+
outvar = data[1].to(local_rank, dtype=torch.float32)
|
| 161 |
+
surface = invar[:, :4, :, :]
|
| 162 |
+
z = invar[:, 4:41, :, :]
|
| 163 |
+
r = invar[:, 41:78, :, :]
|
| 164 |
+
u = invar[:, 78:115, :, :]
|
| 165 |
+
v = invar[:, 115:152, :, :]
|
| 166 |
+
t = invar[:, 152:189, :, :]
|
| 167 |
+
|
| 168 |
+
surface, z, r, u, v, t = model(surface, z, r, u, v, t)
|
| 169 |
+
|
| 170 |
+
outvar_pred = torch.concat(
|
| 171 |
+
[surface_p, z_p, r_p, u_p, v_p, t_p], dim=1)
|
| 172 |
+
|
| 173 |
+
loss = loss_obj(outvar, outvar_pred)
|
| 174 |
+
|
| 175 |
+
if cfg.world_size > 1:
|
| 176 |
+
loss_tensor = loss.detach().to(local_rank)
|
| 177 |
+
dist.all_reduce(loss_tensor)
|
| 178 |
+
loss = loss_tensor.item() / cfg.world_size
|
| 179 |
+
valid_loss += loss
|
| 180 |
+
else:
|
| 181 |
+
valid_loss += loss.item()
|
| 182 |
+
|
| 183 |
+
if world_rank == 0:
|
| 184 |
+
logger.info(f'Valid: Epoch {epoch}-{j+1}/{len(val_dataloader)} '
|
| 185 |
+
f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
|
| 186 |
+
f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
|
| 187 |
+
f'loss:{valid_loss / (j+1): .04f}')
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
valid_loss /= len(val_dataloader)
|
| 191 |
+
is_save_ckp = False
|
| 192 |
+
if valid_loss < best_valid_loss:
|
| 193 |
+
best_valid_loss = valid_loss
|
| 194 |
+
best_loss_epoch = epoch
|
| 195 |
+
world_rank == 0 and save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, cfg.checkpoint_dir)
|
| 196 |
+
is_save_ckp = True
|
| 197 |
+
|
| 198 |
+
scheduler.step(valid_loss)
|
| 199 |
+
|
| 200 |
+
if world_rank == 0:
|
| 201 |
+
logger.info(f"Epoch [{epoch + 1}/{cfg.max_epoch}], "
|
| 202 |
+
f"Train Loss: {train_loss:.4f}, "
|
| 203 |
+
f"Valid Loss: {valid_loss:.4f}, "
|
| 204 |
+
f"Best loss at Epoch: {best_loss_epoch + 1}"
|
| 205 |
+
+ (", saving checkpoint" if is_save_ckp else "")
|
| 206 |
+
)
|
| 207 |
+
train_losses = np.append(train_losses, train_loss)
|
| 208 |
+
valid_losses = np.append(valid_losses, valid_loss)
|
| 209 |
+
|
| 210 |
+
np.save(train_loss_file, train_losses)
|
| 211 |
+
np.save(valid_loss_file, valid_losses)
|
| 212 |
+
|
| 213 |
+
if epoch - best_loss_epoch > cfg.patience:
|
| 214 |
+
print(f"Loss has not decrease in {cfg.patience} epochs, stopping training...")
|
| 215 |
+
exit()
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def save_checkpoint(model, optimizer, scheduler, best_valid_loss,
|
| 219 |
+
best_loss_epoch, model_path):
|
| 220 |
+
model_to_save = model.module if hasattr(model, "module") else model
|
| 221 |
+
state = {
|
| 222 |
+
"model_state_dict": model_to_save.state_dict(),
|
| 223 |
+
"optimizer_state_dict": optimizer.state_dict(),
|
| 224 |
+
"scheduler_state_dict": scheduler.state_dict(),
|
| 225 |
+
"best_valid_loss": best_valid_loss,
|
| 226 |
+
"best_loss_epoch": best_loss_epoch,
|
| 227 |
+
}
|
| 228 |
+
torch.save(state, f"{model_path}/model.pth")
|
| 229 |
+
### the weight file saving may interrupted due to DCU queue limit, get a backup to ensure there at least has one model
|
| 230 |
+
os.system(f"mv {model_path}/model.pth {model_path}/model_bak.pth")
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
if __name__ == "__main__":
|
| 234 |
+
current_path = os.getcwd()
|
| 235 |
+
sys.path.append(current_path)
|
| 236 |
+
main()
|
weight/.gitkeep
ADDED
|
File without changes
|