Upload folder using huggingface_hub
Browse files- README.md +173 -0
- conf/config.yaml +118 -0
- configuration.json +1 -0
- model/fuxi.py +142 -0
- scripts/data_loader.py +205 -0
- scripts/fake_data.py +144 -0
- scripts/inference.py +162 -0
- scripts/result.py +214 -0
- scripts/train_long.py +284 -0
- scripts/train_medium.py +285 -0
- scripts/train_short.py +267 -0
- weight/.gitkeep +0 -0
README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
frameworks: PyTorch
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
- zh
|
| 6 |
+
license: apache-2.0
|
| 7 |
+
tags:
|
| 8 |
+
- OneScience
|
| 9 |
+
- Earth Science
|
| 10 |
+
- Weather Forecast
|
| 11 |
+
- Short-to-Medium-Range Weather Forecast
|
| 12 |
+
- ERA5
|
| 13 |
+
tasks: []
|
| 14 |
+
datasets:
|
| 15 |
+
- OneScience/ERA5
|
| 16 |
+
---
|
| 17 |
+
<p align="center">
|
| 18 |
+
<strong>
|
| 19 |
+
<span style="font-size: 30px;">FuXi</span>
|
| 20 |
+
</strong>
|
| 21 |
+
</p>
|
| 22 |
+
|
| 23 |
+
# Model Introduction
|
| 24 |
+
|
| 25 |
+
FuXi is a global weather forecast foundation model jointly developed by Fudan University and multiple institutions. It is the first end-to-end machine learning framework capable of independently performing data assimilation (DA) and cyclic forecasting.
|
| 26 |
+
|
| 27 |
+
Paper: FuXi: A cascade machine learning forecasting system for 15-day global weather forecast
|
| 28 |
+
|
| 29 |
+
https://arxiv.org/abs/2306.12873
|
| 30 |
+
|
| 31 |
+
# Model Description
|
| 32 |
+
|
| 33 |
+
The FuXi model is trained through a three-stage cascaded approach: short → medium → long. Its training input primarily consists of ERA5 reanalysis data.
|
| 34 |
+
|
| 35 |
+
# Use Cases
|
| 36 |
+
|
| 37 |
+
| Scenario | Description |
|
| 38 |
+
| :---: | :--- |
|
| 39 |
+
| Weather Forecast Training | Train FuXi (short/medium/long three stages) using ERA5 HDF5 data |
|
| 40 |
+
| Local Quick Validation | Use synthetic data to verify data loading, model training & inference, and inference result visualization. |
|
| 41 |
+
| ModelScope / OneCode Execution | Download as a standalone model package, install dependencies, and run scripts directly. |
|
| 42 |
+
| Multi-GPU Training | Launch multi-process training via `torchrun`. |
|
| 43 |
+
|
| 44 |
+
# Usage Guide
|
| 45 |
+
|
| 46 |
+
## 1. OneCode Usage
|
| 47 |
+
|
| 48 |
+
Experience intelligent one-click AI4S programming through the OneCode online environment:
|
| 49 |
+
|
| 50 |
+
[Click to Experience Intelligent One-Click AI4S Programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 51 |
+
|
| 52 |
+
## 2. Manual Installation and Usage
|
| 53 |
+
|
| 54 |
+
**Hardware Requirements**
|
| 55 |
+
|
| 56 |
+
- A GPU or DCU is recommended.
|
| 57 |
+
- CPU can be used for import and small-scale connectivity verification; full training and inference will be slow.
|
| 58 |
+
- DCU users must install DTK in advance. DTK 25.04.2 or above, or the OneScience recommended version matching your cluster, is recommended.
|
| 59 |
+
|
| 60 |
+
### Download the Model Package
|
| 61 |
+
|
| 62 |
+
```bash
|
| 63 |
+
modelscope download --model OneScience/FuXi --local_dir ./FuXi
|
| 64 |
+
cd FuXi
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
### Install the Runtime Environment
|
| 68 |
+
|
| 69 |
+
**DCU Environment**
|
| 70 |
+
|
| 71 |
+
```bash
|
| 72 |
+
# Please activate DTK and CONDA first
|
| 73 |
+
conda create -n onescience311 python=3.11 -y
|
| 74 |
+
conda activate onescience311
|
| 75 |
+
# uv installation is supported
|
| 76 |
+
pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
**GPU Environment**
|
| 80 |
+
```bash
|
| 81 |
+
# Please activate CONDA first
|
| 82 |
+
conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
|
| 83 |
+
conda activate onescience311
|
| 84 |
+
# uv installation is supported
|
| 85 |
+
pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
### Training Data Introduction
|
| 89 |
+
|
| 90 |
+
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:
|
| 91 |
+
|
| 92 |
+
```bash
|
| 93 |
+
modelscope download --dataset OneScience/ERA5 --local_dir ./data
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
### Training
|
| 97 |
+
|
| 98 |
+
FuXi consists of 3 stages and **must be executed in order**. The inference result of each stage serves as the input for the next stage:
|
| 99 |
+
|
| 100 |
+
**short (train) → short (inference) → medium (train) → medium (inference) → long (train) → long (inference)**
|
| 101 |
+
|
| 102 |
+
**1) Train the short model (train from scratch, as the starting entry point)**
|
| 103 |
+
|
| 104 |
+
Single GPU:
|
| 105 |
+
|
| 106 |
+
```bash
|
| 107 |
+
python scripts/train_short.py
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
Multi-GPU:
|
| 111 |
+
|
| 112 |
+
```bash
|
| 113 |
+
torchrun --nproc_per_node=8 --nnodes=1 --rdzv_id=1000 --rdzv_backend=c10d --max_restarts=0 --master_addr="localhost" --master_port=29500 scripts/train_short.py
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
**2) Short inference (generate input data for medium)**
|
| 117 |
+
|
| 118 |
+
```bash
|
| 119 |
+
python scripts/inference.py short
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
**3) Train the medium model (requires short weights + short inference results)**
|
| 123 |
+
|
| 124 |
+
```bash
|
| 125 |
+
python scripts/train_medium.py
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
**4) Medium inference (generate input data for long)**
|
| 129 |
+
|
| 130 |
+
```bash
|
| 131 |
+
python scripts/inference.py medium
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
**5) Train the long model (requires medium weights + medium inference results)**
|
| 135 |
+
|
| 136 |
+
```bash
|
| 137 |
+
python scripts/train_long.py
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
### Training Weights
|
| 141 |
+
|
| 142 |
+
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.
|
| 143 |
+
|
| 144 |
+
### Inference
|
| 145 |
+
|
| 146 |
+
Each stage can perform inference independently:
|
| 147 |
+
|
| 148 |
+
```bash
|
| 149 |
+
python scripts/inference.py short
|
| 150 |
+
python scripts/inference.py medium
|
| 151 |
+
python scripts/inference.py long
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
Inference results will be saved to `result/output/<stage>/`.
|
| 155 |
+
|
| 156 |
+
### Evaluation and Visualization
|
| 157 |
+
|
| 158 |
+
```bash
|
| 159 |
+
python scripts/result.py short
|
| 160 |
+
python scripts/result.py medium
|
| 161 |
+
python scripts/result.py long
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
# OneScience Official Information
|
| 165 |
+
|
| 166 |
+
| Platform | OneScience Main Repository | Skills Repository |
|
| 167 |
+
| --- | --- | --- |
|
| 168 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 169 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 170 |
+
|
| 171 |
+
# Citation & License
|
| 172 |
+
|
| 173 |
+
- This repository is a reproduction of the original FuXi paper.
|
conf/config.yaml
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model:
|
| 2 |
+
|
| 3 |
+
start_epoch: 0
|
| 4 |
+
max_epoch: 1
|
| 5 |
+
train_lr: 2.5E-4
|
| 6 |
+
finetune_lr: 1E-7
|
| 7 |
+
patch_size: [2, 4, 4]
|
| 8 |
+
embed_dim: 1536
|
| 9 |
+
num_groups: 32
|
| 10 |
+
num_heads: 8
|
| 11 |
+
window_size: 7
|
| 12 |
+
short_num_steps: 20
|
| 13 |
+
medium_num_steps: 40
|
| 14 |
+
long_num_steps: 60
|
| 15 |
+
finetune_step: 1
|
| 16 |
+
step_change_freq: 200
|
| 17 |
+
input_steps: 2
|
| 18 |
+
num_blocks: 8
|
| 19 |
+
checkpoint_dir : "./data/checkpoints"
|
| 20 |
+
patience: 50
|
| 21 |
+
|
| 22 |
+
# 整个数据读取流程
|
| 23 |
+
datapipe:
|
| 24 |
+
name: "ERA5"
|
| 25 |
+
task: "weather_forecasting"
|
| 26 |
+
|
| 27 |
+
# dataset设定
|
| 28 |
+
dataset:
|
| 29 |
+
type: "hdf5"
|
| 30 |
+
data_dir: './data/' # "$ONESCIENCE_DATASETS_DIR/ERA5/newh5/"
|
| 31 |
+
train_time: [2000,2001]
|
| 32 |
+
val_time: [2022]
|
| 33 |
+
test_time: [2023]
|
| 34 |
+
img_size: [2, 721, 1440]
|
| 35 |
+
verbose: true
|
| 36 |
+
cache: false
|
| 37 |
+
# 气象变量
|
| 38 |
+
|
| 39 |
+
channels: ['10m_u_component_of_wind', '10m_v_component_of_wind', '2m_temperature', 'mean_sea_level_pressure', 'total_precipitation',
|
| 40 |
+
'geopotential_50', 'geopotential_100', 'geopotential_150', 'geopotential_200', 'geopotential_250',
|
| 41 |
+
'geopotential_300', 'geopotential_400', 'geopotential_500', 'geopotential_600', 'geopotential_700',
|
| 42 |
+
'geopotential_850', 'geopotential_925', 'geopotential_1000',
|
| 43 |
+
'relative_humidity_50', 'relative_humidity_100', 'relative_humidity_150', 'relative_humidity_200', 'relative_humidity_250',
|
| 44 |
+
'relative_humidity_300', 'relative_humidity_400', 'relative_humidity_500', 'relative_humidity_600', 'relative_humidity_700',
|
| 45 |
+
'relative_humidity_850','relative_humidity_925', 'relative_humidity_1000',
|
| 46 |
+
'u_component_of_wind_50', 'u_component_of_wind_100', 'u_component_of_wind_150', 'u_component_of_wind_200', 'u_component_of_wind_250',
|
| 47 |
+
'u_component_of_wind_300', 'u_component_of_wind_400', 'u_component_of_wind_500', 'u_component_of_wind_600','u_component_of_wind_700',
|
| 48 |
+
'u_component_of_wind_850', 'u_component_of_wind_925', 'u_component_of_wind_1000',
|
| 49 |
+
'v_component_of_wind_50', 'v_component_of_wind_100', 'v_component_of_wind_150', 'v_component_of_wind_200', 'v_component_of_wind_250',
|
| 50 |
+
'v_component_of_wind_300', 'v_component_of_wind_400', 'v_component_of_wind_500', 'v_component_of_wind_600','v_component_of_wind_700',
|
| 51 |
+
'v_component_of_wind_850', 'v_component_of_wind_925', 'v_component_of_wind_1000',
|
| 52 |
+
'temperature_50', 'temperature_100', 'temperature_150', 'temperature_200', 'temperature_250',
|
| 53 |
+
'temperature_300', 'temperature_400', 'temperature_500', 'temperature_600', 'temperature_700',
|
| 54 |
+
'temperature_850', 'temperature_925', 'temperature_1000'
|
| 55 |
+
]
|
| 56 |
+
|
| 57 |
+
variables:
|
| 58 |
+
- "u10" # 10m U wind component
|
| 59 |
+
- "v10" # 10m V wind component
|
| 60 |
+
- "t2m" # 2m temperature
|
| 61 |
+
- "msl" # Mean sea level pressure
|
| 62 |
+
- "z500" # Geopotential at 500 hPa
|
| 63 |
+
- "t850" # Temperature at 850 hPa
|
| 64 |
+
|
| 65 |
+
# 时间配置
|
| 66 |
+
time_range: ["2000-01-01", "2020-12-31"]
|
| 67 |
+
time_steps: 1
|
| 68 |
+
time_res: 6
|
| 69 |
+
|
| 70 |
+
# 空间配置
|
| 71 |
+
spatial_resolution: [0.25, 0.25]
|
| 72 |
+
|
| 73 |
+
# 采样配置
|
| 74 |
+
num_samples: -1 # -1 表示使用全部数据
|
| 75 |
+
shuffle: true
|
| 76 |
+
random_seed: 42
|
| 77 |
+
|
| 78 |
+
# 领域特定配置
|
| 79 |
+
extra:
|
| 80 |
+
levels: [500, 850, 1000]
|
| 81 |
+
lat_range: [-90, 90]
|
| 82 |
+
lon_range: [0, 360]
|
| 83 |
+
|
| 84 |
+
# 数据转换配置
|
| 85 |
+
transforms:
|
| 86 |
+
- type: "Normalize"
|
| 87 |
+
params:
|
| 88 |
+
mean: [0.0, 0.0, 288.0, 101325.0, 50000.0, 270.0]
|
| 89 |
+
std: [5.0, 5.0, 15.0, 1000.0, 5000.0, 10.0]
|
| 90 |
+
keys: ["input", "target"]
|
| 91 |
+
|
| 92 |
+
- type: "ToTensor"
|
| 93 |
+
params:
|
| 94 |
+
keys: null # null表示转换所有numpy数组
|
| 95 |
+
|
| 96 |
+
# 其他配置
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
# DataLoader配置
|
| 100 |
+
dataloader:
|
| 101 |
+
mask_dtype: "float32"
|
| 102 |
+
batch_size: 1
|
| 103 |
+
num_workers: 1
|
| 104 |
+
pin_memory: true
|
| 105 |
+
drop_last: true
|
| 106 |
+
shuffle: false # 使用sampler时设为false
|
| 107 |
+
prefetch_factor: 2
|
| 108 |
+
persistent_workers: true
|
| 109 |
+
|
| 110 |
+
# 分布式配置
|
| 111 |
+
distributed:
|
| 112 |
+
enabled: true
|
| 113 |
+
sampler: "DistributedSampler"
|
| 114 |
+
rank: 0
|
| 115 |
+
world_size: 4
|
| 116 |
+
shuffle: true
|
| 117 |
+
seed: 42
|
| 118 |
+
drop_last: true
|
configuration.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"framework":"Pytorch","task":"other"}
|
model/fuxi.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from torch import nn
|
| 3 |
+
from torch.nn import functional as F
|
| 4 |
+
|
| 5 |
+
from onescience.modules.embedding.fuxiembedding import FuxiEmbedding
|
| 6 |
+
from onescience.modules.fc.fuxifc import FuxiFC
|
| 7 |
+
from onescience.modules.transformer.fuxitransformer import FuxiTransformer
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class Fuxi(nn.Module):
|
| 11 |
+
"""
|
| 12 |
+
Fuxi 的主模型实现。
|
| 13 |
+
|
| 14 |
+
该模型使用以下组件完成输入编码、二维 trunk 特征提取与 patch 级输出恢复:
|
| 15 |
+
|
| 16 |
+
- `OneEmbedding(style="FuxiEmbedding")`
|
| 17 |
+
- 将 `(TimeSteps, Height, Width)` 三维时空块映射为 patch 特征
|
| 18 |
+
- `OneTransformer(style="FuxiTransformer")`
|
| 19 |
+
- 在二维特征图上执行下采样、Swin trunk、上采样
|
| 20 |
+
- `OneFC(style="FuxiFC")`
|
| 21 |
+
- 将每个二维网格位置的 embedding 特征映射为 patch 级输出变量
|
| 22 |
+
|
| 23 |
+
在当前实现中:
|
| 24 |
+
|
| 25 |
+
- 输入包含多个时间步的二维气象场
|
| 26 |
+
- `patch_size[0]` 默认与 `TimeSteps` 相同,使 embedding 后时间维压缩为 1
|
| 27 |
+
- trunk 只处理二维特征图
|
| 28 |
+
- 最终通过 patch 重排与双线性插值恢复到目标空间分辨率
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
img_size (tuple[int, int, int]):
|
| 32 |
+
输入空间尺寸 `(TimeSteps, Height, Width)`。
|
| 33 |
+
patch_size (tuple[int, int, int]):
|
| 34 |
+
patch 切分尺寸 `(PatchTimeSteps, PatchHeight, PatchWidth)`。
|
| 35 |
+
in_chans (int):
|
| 36 |
+
输入变量通道数。
|
| 37 |
+
out_chans (int):
|
| 38 |
+
输出变量通道数。
|
| 39 |
+
embed_dim (int):
|
| 40 |
+
embedding 特征维度。
|
| 41 |
+
num_groups (int):
|
| 42 |
+
trunk 中采样模块的 `GroupNorm` 分组数。
|
| 43 |
+
num_heads (int):
|
| 44 |
+
`SwinTransformerV2Stage` 的注意力头数。
|
| 45 |
+
window_size (int | tuple[int, int]):
|
| 46 |
+
trunk 局部窗口大小。
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
def __init__(
|
| 50 |
+
self,
|
| 51 |
+
img_size=(2, 721, 1440),
|
| 52 |
+
patch_size=(2, 4, 4),
|
| 53 |
+
in_chans=70,
|
| 54 |
+
out_chans=70,
|
| 55 |
+
embed_dim=1536,
|
| 56 |
+
num_groups=32,
|
| 57 |
+
num_heads=8,
|
| 58 |
+
window_size=7,
|
| 59 |
+
):
|
| 60 |
+
super().__init__()
|
| 61 |
+
|
| 62 |
+
TimeSteps, Height, Width = img_size
|
| 63 |
+
PatchTimeSteps, PatchHeight, PatchWidth = patch_size
|
| 64 |
+
if TimeSteps != PatchTimeSteps:
|
| 65 |
+
raise ValueError(
|
| 66 |
+
"Current Fuxi model expects patch_size[0] to equal img_size[0] "
|
| 67 |
+
"so the embedding output time dimension is 1 before squeeze"
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
EmbeddedHeight = Height // PatchHeight
|
| 71 |
+
EmbeddedWidth = Width // PatchWidth
|
| 72 |
+
TransformerInputResolution = (
|
| 73 |
+
EmbeddedHeight // 2,
|
| 74 |
+
EmbeddedWidth // 2,
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
self.cube_embedding = FuxiEmbedding(
|
| 78 |
+
img_size=img_size,
|
| 79 |
+
patch_size=patch_size,
|
| 80 |
+
in_chans=in_chans,
|
| 81 |
+
embed_dim=embed_dim,
|
| 82 |
+
)
|
| 83 |
+
self.u_transformer = FuxiTransformer(
|
| 84 |
+
embed_dim=embed_dim,
|
| 85 |
+
num_groups=num_groups,
|
| 86 |
+
input_resolution=TransformerInputResolution,
|
| 87 |
+
num_heads=num_heads,
|
| 88 |
+
window_size=window_size,
|
| 89 |
+
)
|
| 90 |
+
self.fc = FuxiFC(
|
| 91 |
+
in_channels=embed_dim,
|
| 92 |
+
out_channels=out_chans * PatchHeight * PatchWidth,
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
self.patch_size = patch_size
|
| 96 |
+
self.transformer_input_resolution = TransformerInputResolution
|
| 97 |
+
self.embedded_resolution = (EmbeddedHeight, EmbeddedWidth)
|
| 98 |
+
self.out_chans = out_chans
|
| 99 |
+
self.img_size = img_size
|
| 100 |
+
|
| 101 |
+
def forward(self, x):
|
| 102 |
+
"""
|
| 103 |
+
Args:
|
| 104 |
+
x (torch.Tensor):
|
| 105 |
+
输入张量,形状为 `(Batch, in_chans, TimeSteps, Height, Width)`。
|
| 106 |
+
|
| 107 |
+
Returns:
|
| 108 |
+
torch.Tensor:
|
| 109 |
+
输出张量,形状为 `(Batch, out_chans, Height, Width)`。
|
| 110 |
+
"""
|
| 111 |
+
Batch, _, _, _, _ = x.shape
|
| 112 |
+
_, PatchHeight, PatchWidth = self.patch_size
|
| 113 |
+
EmbeddedHeight, EmbeddedWidth = self.embedded_resolution
|
| 114 |
+
|
| 115 |
+
x = self.cube_embedding(x)
|
| 116 |
+
if x.shape[2] != 1:
|
| 117 |
+
raise ValueError(
|
| 118 |
+
f"Expected embedding time dimension 1 before squeeze, but received {x.shape[2]}"
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
x = x.squeeze(2)
|
| 122 |
+
x = self.u_transformer(x)
|
| 123 |
+
x = self.fc(x.permute(0, 2, 3, 1))
|
| 124 |
+
x = x.reshape(
|
| 125 |
+
Batch,
|
| 126 |
+
EmbeddedHeight,
|
| 127 |
+
EmbeddedWidth,
|
| 128 |
+
PatchHeight,
|
| 129 |
+
PatchWidth,
|
| 130 |
+
self.out_chans,
|
| 131 |
+
).permute(0, 1, 3, 2, 4, 5)
|
| 132 |
+
x = x.reshape(
|
| 133 |
+
Batch,
|
| 134 |
+
EmbeddedHeight * PatchHeight,
|
| 135 |
+
EmbeddedWidth * PatchWidth,
|
| 136 |
+
self.out_chans,
|
| 137 |
+
)
|
| 138 |
+
x = x.permute(0, 3, 1, 2)
|
| 139 |
+
|
| 140 |
+
x = F.interpolate(x, size=self.img_size[1:], mode="bilinear")
|
| 141 |
+
|
| 142 |
+
return x
|
scripts/data_loader.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import glob
|
| 3 |
+
import h5py
|
| 4 |
+
import pytz
|
| 5 |
+
import numpy as np
|
| 6 |
+
import torch
|
| 7 |
+
|
| 8 |
+
from datetime import datetime, timedelta
|
| 9 |
+
from torch.utils.data import Dataset, DataLoader
|
| 10 |
+
from torch.utils.data.distributed import DistributedSampler
|
| 11 |
+
|
| 12 |
+
from onescience.datapipes.climate.utils.invariant import latlon_grid
|
| 13 |
+
from onescience.datapipes.climate.utils.zenith_angle import cos_zenith_angle
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class ERA5Datapipe:
|
| 17 |
+
def __init__(
|
| 18 |
+
self,
|
| 19 |
+
dataset_dir,
|
| 20 |
+
used_years,
|
| 21 |
+
used_variables,
|
| 22 |
+
pattern='medium',
|
| 23 |
+
distributed=False,
|
| 24 |
+
input_steps=1,
|
| 25 |
+
output_steps=1,
|
| 26 |
+
normalize=True,
|
| 27 |
+
batch_size=1,
|
| 28 |
+
num_workers=4,
|
| 29 |
+
):
|
| 30 |
+
self.dataset_dir = dataset_dir
|
| 31 |
+
self.used_years = used_years
|
| 32 |
+
self.used_variables = used_variables
|
| 33 |
+
self.pattern = pattern
|
| 34 |
+
self.distributed = distributed
|
| 35 |
+
self.input_steps = input_steps
|
| 36 |
+
self.output_steps = output_steps
|
| 37 |
+
self.normalize = normalize
|
| 38 |
+
self.batch_size = batch_size
|
| 39 |
+
self.num_workers = num_workers
|
| 40 |
+
|
| 41 |
+
def get_dataloader(self, mode):
|
| 42 |
+
dataset = ERA5Dataset(
|
| 43 |
+
dataset_dir=self.dataset_dir,
|
| 44 |
+
used_years=self.used_years,
|
| 45 |
+
used_variables=self.used_variables,
|
| 46 |
+
pattern=self.pattern,
|
| 47 |
+
input_steps=self.input_steps,
|
| 48 |
+
output_steps=self.output_steps,
|
| 49 |
+
normalize=self.normalize,
|
| 50 |
+
)
|
| 51 |
+
is_train = (mode == 'train')
|
| 52 |
+
|
| 53 |
+
sampler = DistributedSampler(dataset, shuffle=is_train) if self.distributed else None
|
| 54 |
+
|
| 55 |
+
return DataLoader(
|
| 56 |
+
dataset,
|
| 57 |
+
batch_size=self.batch_size,
|
| 58 |
+
num_workers=self.num_workers,
|
| 59 |
+
pin_memory=True,
|
| 60 |
+
shuffle=(is_train and not self.distributed),
|
| 61 |
+
sampler=sampler,
|
| 62 |
+
drop_last=self.distributed,
|
| 63 |
+
), sampler
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class ERA5Dataset(Dataset):
|
| 67 |
+
def __init__(
|
| 68 |
+
self,
|
| 69 |
+
dataset_dir,
|
| 70 |
+
used_years,
|
| 71 |
+
used_variables,
|
| 72 |
+
pattern='medium',
|
| 73 |
+
input_steps=1,
|
| 74 |
+
output_steps=1,
|
| 75 |
+
normalize=True,
|
| 76 |
+
):
|
| 77 |
+
self.dataset_dir = dataset_dir
|
| 78 |
+
self.used_years = used_years
|
| 79 |
+
self.used_variables = used_variables
|
| 80 |
+
self.pattern = pattern
|
| 81 |
+
self.input_steps = input_steps
|
| 82 |
+
self.output_steps = output_steps
|
| 83 |
+
self.normalize = normalize
|
| 84 |
+
|
| 85 |
+
self._init_avail_samples()
|
| 86 |
+
self._init_normalized_files()
|
| 87 |
+
self._init_npy_files()
|
| 88 |
+
self._init_latlon_grid()
|
| 89 |
+
|
| 90 |
+
def _init_avail_samples(self):
|
| 91 |
+
h5_files = sorted(glob.glob(os.path.join(self.dataset_dir, "data", "*.h5")))
|
| 92 |
+
available_years = [int(os.path.basename(f).replace(".h5", "")) for f in h5_files]
|
| 93 |
+
|
| 94 |
+
missing_years = [y for y in self.used_years if y not in available_years]
|
| 95 |
+
if missing_years:
|
| 96 |
+
raise ValueError(f"❌ Years not found in dataset: {missing_years}")
|
| 97 |
+
|
| 98 |
+
# ── 读取变量信息 & 校验 ───────────────────────────────
|
| 99 |
+
with h5py.File(h5_files[0], "r") as f:
|
| 100 |
+
ds = f["fields"]
|
| 101 |
+
self.T, self.C, self.H, self.W = ds.shape
|
| 102 |
+
all_variables = [v.decode() if isinstance(v, bytes) else v for v in ds.attrs["variables"]]
|
| 103 |
+
self.time_step = int(ds.attrs["time_step"])
|
| 104 |
+
|
| 105 |
+
missing_vars = [v for v in self.used_variables if v not in all_variables]
|
| 106 |
+
if missing_vars:
|
| 107 |
+
raise ValueError(f"❌ Variables not found in dataset: {missing_vars}")
|
| 108 |
+
|
| 109 |
+
# ── 建立索引 ──────────────────────────────────────────
|
| 110 |
+
self.channel_indices = [all_variables.index(v) for v in self.used_variables]
|
| 111 |
+
|
| 112 |
+
def _init_normalized_files(self):
|
| 113 |
+
# ── 从每年 h5 内嵌的 global_means/global_stds 读取归一化统计量 ──
|
| 114 |
+
h5_files = sorted(glob.glob(os.path.join(self.dataset_dir, "data", "*.h5")))
|
| 115 |
+
with h5py.File(h5_files[0], "r") as f:
|
| 116 |
+
mu = f["global_means"][:] # [1, C, 1, 1]
|
| 117 |
+
std = f["global_stds"][:]
|
| 118 |
+
self.mu = torch.as_tensor(mu[:, self.channel_indices, :, :], dtype=torch.float32)
|
| 119 |
+
self.sd = torch.as_tensor(std[:, self.channel_indices, :, :], dtype=torch.float32)
|
| 120 |
+
|
| 121 |
+
def _init_npy_files(self):
|
| 122 |
+
"""读取前一阶段模型输出的 npy 文件列表(作为 invar 输入)"""
|
| 123 |
+
self.files = {}
|
| 124 |
+
for year in self.used_years:
|
| 125 |
+
if self.pattern == 'medium':
|
| 126 |
+
path = os.path.join('./result/short/data/', str(year))
|
| 127 |
+
else:
|
| 128 |
+
path = os.path.join('./result/medium/data', str(year))
|
| 129 |
+
files = sorted(glob.glob(os.path.join(path, "*.npy")))
|
| 130 |
+
if not files:
|
| 131 |
+
raise ValueError(f"❌ No npy files found for year {year} under {path}")
|
| 132 |
+
self.files[year] = files
|
| 133 |
+
|
| 134 |
+
n_files = len(files)
|
| 135 |
+
self.samples_per_year = n_files - self.output_steps - (self.input_steps - 1)
|
| 136 |
+
self.total_samples = len(self.used_years) * self.samples_per_year
|
| 137 |
+
|
| 138 |
+
if not torch.distributed.is_initialized() or torch.distributed.get_rank() == 0:
|
| 139 |
+
print('\n')
|
| 140 |
+
print('-' * 50)
|
| 141 |
+
print(f"📂 Pattern: {self.pattern}, used: {self.used_years} years")
|
| 142 |
+
print(f'📂 each year contains {self.samples_per_year} usable samples '
|
| 143 |
+
f'({n_files} files, input {self.input_steps}, output {self.output_steps})')
|
| 144 |
+
print(f'📂 total usable samples: {self.total_samples}')
|
| 145 |
+
print('-' * 50, '\n')
|
| 146 |
+
|
| 147 |
+
def _init_latlon_grid(self):
|
| 148 |
+
latlon = latlon_grid(bounds=((90, -90), (0, 360)), shape=(self.H, self.W))
|
| 149 |
+
self.latlon_torch = torch.tensor(np.stack(latlon, axis=0), dtype=torch.float32)
|
| 150 |
+
|
| 151 |
+
def _filename_to_index(self, filename):
|
| 152 |
+
"""将 YYYYMMDDHH 格式的文件名转换为年度 h5 文件中的时间步索引"""
|
| 153 |
+
dt = datetime.strptime(filename, "%Y%m%d%H")
|
| 154 |
+
year_start = datetime(dt.year, 1, 1)
|
| 155 |
+
hours = (dt - year_start).total_seconds() / 3600
|
| 156 |
+
return int(hours / self.time_step)
|
| 157 |
+
|
| 158 |
+
def __len__(self):
|
| 159 |
+
return self.total_samples
|
| 160 |
+
|
| 161 |
+
def __getitem__(self, idx):
|
| 162 |
+
year_idx = idx // self.samples_per_year
|
| 163 |
+
step_idx = idx % self.samples_per_year
|
| 164 |
+
year = self.used_years[year_idx]
|
| 165 |
+
files = self.files[year]
|
| 166 |
+
|
| 167 |
+
# ── invar: 从前一阶段模型输出的 npy 文件读取 ──────────
|
| 168 |
+
invar_list = []
|
| 169 |
+
for i in range(step_idx, step_idx + self.input_steps):
|
| 170 |
+
data = np.load(files[i])
|
| 171 |
+
data = np.squeeze(data) # [C, H, W]
|
| 172 |
+
invar_list.append(data)
|
| 173 |
+
|
| 174 |
+
# ── outvar: 从年度 h5 文件读取真实 ERA5 标签 ──────────
|
| 175 |
+
outvar_list = []
|
| 176 |
+
time_index = []
|
| 177 |
+
h5_path = os.path.join(self.dataset_dir, 'data', f'{year}.h5')
|
| 178 |
+
with h5py.File(h5_path, "r") as f:
|
| 179 |
+
for i in range(step_idx + self.input_steps, step_idx + self.input_steps + self.output_steps):
|
| 180 |
+
fname = os.path.basename(files[i])[:-4] # 去掉 .npy,得到 YYYYMMDDHH
|
| 181 |
+
t_idx = self._filename_to_index(fname)
|
| 182 |
+
data = f["fields"][t_idx] # [C, H, W]
|
| 183 |
+
data = data[self.channel_indices]
|
| 184 |
+
outvar_list.append(data)
|
| 185 |
+
time_index.append(fname)
|
| 186 |
+
|
| 187 |
+
invar = np.stack(invar_list, axis=0) # [T, C, H, W]
|
| 188 |
+
outvar = np.stack(outvar_list, axis=0) # [T, C, H, W]
|
| 189 |
+
invar = torch.as_tensor(invar, dtype=torch.float32)
|
| 190 |
+
outvar = torch.as_tensor(outvar, dtype=torch.float32)
|
| 191 |
+
|
| 192 |
+
if self.normalize:
|
| 193 |
+
invar = (invar - self.mu) / self.sd
|
| 194 |
+
outvar = (outvar - self.mu) / self.sd
|
| 195 |
+
|
| 196 |
+
# ── 太阳天顶角 ────────────────────────────────────────
|
| 197 |
+
start_time = datetime(year, 1, 1, tzinfo=pytz.utc)
|
| 198 |
+
timestamps = np.array([
|
| 199 |
+
(start_time + timedelta(hours=(step_idx + t) * self.time_step)).timestamp()
|
| 200 |
+
for t in range(self.output_steps)
|
| 201 |
+
])
|
| 202 |
+
timestamps = torch.from_numpy(timestamps)
|
| 203 |
+
cos_zenith = cos_zenith_angle(timestamps, latlon=self.latlon_torch).float()
|
| 204 |
+
|
| 205 |
+
return invar.squeeze(0), outvar.squeeze(0), cos_zenith, step_idx, time_index
|
scripts/fake_data.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import h5py
|
| 3 |
+
import numpy as np
|
| 4 |
+
import xarray as xr
|
| 5 |
+
from onescience.utils.YParams import YParams
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
# 各数据集固定的空间和时间维度
|
| 9 |
+
DATASET_DIMS = {"T": 10, "H": 721, "W": 1440, "time_step": 6}
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def generate_fake_h5(data_dir, var_names, years, dims):
|
| 13 |
+
"""
|
| 14 |
+
为每个年份生成一个空 h5 文件。
|
| 15 |
+
利用 HDF5 chunked 数据集未写入 chunk 即返回 fill_value=0 的特性,
|
| 16 |
+
文件实际只含元数据,极小,但 shape 与真实数据完全一致。
|
| 17 |
+
均值/标准差也作为数据集内嵌进每年的 h5,与 era5.py 新版读取方式对应。
|
| 18 |
+
"""
|
| 19 |
+
os.makedirs(os.path.join(data_dir, "data"), exist_ok=True)
|
| 20 |
+
T, C = dims["T"], len(var_names)
|
| 21 |
+
H, W = dims["H"], dims["W"]
|
| 22 |
+
|
| 23 |
+
means = np.zeros((1, C, 1, 1), dtype=np.float32)
|
| 24 |
+
stds = np.ones((1, C, 1, 1), dtype=np.float32)
|
| 25 |
+
|
| 26 |
+
for year in years:
|
| 27 |
+
path = os.path.join(data_dir, "data", f"{year}.h5")
|
| 28 |
+
with h5py.File(path, "w") as f:
|
| 29 |
+
ds = f.create_dataset(
|
| 30 |
+
"fields",
|
| 31 |
+
shape=(T, C, H, W),
|
| 32 |
+
dtype="float32",
|
| 33 |
+
chunks=(1, C, H, W),
|
| 34 |
+
fillvalue=0.0,
|
| 35 |
+
)
|
| 36 |
+
ds.attrs["variables"] = var_names
|
| 37 |
+
ds.attrs["time_step"] = dims["time_step"]
|
| 38 |
+
f.create_dataset("global_means", data=means)
|
| 39 |
+
f.create_dataset("global_stds", data=stds)
|
| 40 |
+
|
| 41 |
+
size_kb = os.path.getsize(path) / 1024
|
| 42 |
+
print(f" {year}.h5 shape=({T},{C},{H},{W}) "
|
| 43 |
+
f"logical={T*C*H*W*4/1024**3:.1f}GB actual={size_kb:.1f}KB")
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def generate_fake_npy(result_dir, n_vars, years, dims):
|
| 47 |
+
"""
|
| 48 |
+
为 short/medium 阶段生成假的模型输出 npy 文件(供 train_medium/train_long 输入)。
|
| 49 |
+
只创建一个真实的 npy 文件,其余使用 symlink。
|
| 50 |
+
"""
|
| 51 |
+
T = dims["T"]
|
| 52 |
+
H, W = dims["H"], dims["W"]
|
| 53 |
+
time_step = dims["time_step"]
|
| 54 |
+
|
| 55 |
+
data_root = os.path.join(result_dir, "data")
|
| 56 |
+
os.makedirs(data_root, exist_ok=True)
|
| 57 |
+
|
| 58 |
+
real_year = years[0]
|
| 59 |
+
real_dir = os.path.join(data_root, str(real_year))
|
| 60 |
+
os.makedirs(real_dir, exist_ok=True)
|
| 61 |
+
|
| 62 |
+
timestamp = f'{real_year}010100'
|
| 63 |
+
real_path = os.path.join(real_dir, f"{timestamp}.npy")
|
| 64 |
+
real_data = np.zeros((n_vars, H, W), dtype=np.float32)
|
| 65 |
+
np.save(real_path, real_data)
|
| 66 |
+
|
| 67 |
+
# 当前年份其余时间步 symlink
|
| 68 |
+
for i in range(T):
|
| 69 |
+
ts = (np.datetime64(f"{real_year}-01-01T00") + np.timedelta64(i * time_step, "h")
|
| 70 |
+
).astype(str).replace("-", "").replace("T", "")[:10]
|
| 71 |
+
path = os.path.join(real_dir, f"{ts}.npy")
|
| 72 |
+
if path != real_path and not os.path.exists(path):
|
| 73 |
+
os.symlink(f"{timestamp}.npy", path)
|
| 74 |
+
|
| 75 |
+
# 其他年份 symlink
|
| 76 |
+
for y in years[1:]:
|
| 77 |
+
y_dir = os.path.join(data_root, str(y))
|
| 78 |
+
os.makedirs(y_dir, exist_ok=True)
|
| 79 |
+
for i in range(T):
|
| 80 |
+
ts = (np.datetime64(f"{y}-01-01T00") + np.timedelta64(i * time_step, "h")
|
| 81 |
+
).astype(str).replace("-", "").replace("T", "")[:10]
|
| 82 |
+
path = os.path.join(y_dir, f"{ts}.npy")
|
| 83 |
+
if not os.path.exists(path):
|
| 84 |
+
rel = os.path.relpath(real_path, y_dir)
|
| 85 |
+
os.symlink(rel, path)
|
| 86 |
+
|
| 87 |
+
print(f" ✅ Fake npy data generated → {result_dir}")
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def get_static(data_dir, var, name):
|
| 91 |
+
os.makedirs(data_dir, exist_ok=True)
|
| 92 |
+
ds = xr.Dataset(
|
| 93 |
+
data_vars={
|
| 94 |
+
f"{var}": (("valid_time", "latitude", "longitude"),
|
| 95 |
+
np.random.rand(1, 721, 1440).astype(np.float32))
|
| 96 |
+
},
|
| 97 |
+
coords={
|
| 98 |
+
"valid_time": ["2015-12-31"],
|
| 99 |
+
"latitude": np.linspace(90, -90, 721, dtype=np.float64),
|
| 100 |
+
"longitude": np.linspace(0, 359.75, 1440, dtype=np.float64),
|
| 101 |
+
"number": 0,
|
| 102 |
+
"expver": "",
|
| 103 |
+
},
|
| 104 |
+
attrs={
|
| 105 |
+
"GRIB_centre": "ecmf",
|
| 106 |
+
"GRIB_centreDescription": "European Centre for Medium-Range Weather Forecasts",
|
| 107 |
+
"GRIB_subCentre": "0",
|
| 108 |
+
"Conventions": "CF-1.7",
|
| 109 |
+
"institution": "European Centre for Medium-Range Weather Forecasts",
|
| 110 |
+
"history": "Generated manually",
|
| 111 |
+
}
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
ds.to_netcdf(f"{data_dir}/{name}.nc")
|
| 115 |
+
arr = np.random.randn(721, 1440).astype(np.float32)
|
| 116 |
+
np.save(f'{data_dir}/land_mask.npy', arr)
|
| 117 |
+
np.save(f'{data_dir}/soil_type.npy', arr)
|
| 118 |
+
np.save(f'{data_dir}/topography.npy', arr)
|
| 119 |
+
print(f"✅ Static data: {arr.shape}, dtype: {arr.dtype}, save to {data_dir}")
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
if __name__ == "__main__":
|
| 123 |
+
cfg_datapipe = YParams("conf/config.yaml", "datapipe")
|
| 124 |
+
|
| 125 |
+
if cfg_datapipe.dataset.data_dir.startswith("/public/") or cfg_datapipe.dataset.data_dir.startswith("/work/"):
|
| 126 |
+
print("请检查 config,确保各 *_dir 指向本地测试路径而非生产路径。")
|
| 127 |
+
exit()
|
| 128 |
+
|
| 129 |
+
years = cfg_datapipe.dataset.train_time + cfg_datapipe.dataset.val_time + cfg_datapipe.dataset.test_time
|
| 130 |
+
atm_vars = cfg_datapipe.dataset.channels
|
| 131 |
+
n_vars = len(atm_vars)
|
| 132 |
+
|
| 133 |
+
# 主 ERA5 数据(供 train_short 直接读取,归一化参数已内嵌进每年 h5)
|
| 134 |
+
generate_fake_h5(cfg_datapipe.dataset.data_dir, atm_vars, years, DATASET_DIMS)
|
| 135 |
+
|
| 136 |
+
# 前阶段推理输出:result/short 供 train_medium,result/medium 供 train_long
|
| 137 |
+
for stage in ['short', 'medium']:
|
| 138 |
+
generate_fake_npy(f'./result/{stage}', n_vars, years, DATASET_DIMS)
|
| 139 |
+
|
| 140 |
+
static_dir = os.path.join(cfg_datapipe.dataset.data_dir, "static")
|
| 141 |
+
get_static(static_dir, 'z', 'geopotential')
|
| 142 |
+
get_static(static_dir, 'lsm', 'land_sea_mask')
|
| 143 |
+
|
| 144 |
+
print("\n✅ Fake datasets generated.")
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
import glob
|
| 5 |
+
import numpy as np
|
| 6 |
+
import h5py
|
| 7 |
+
from tqdm import tqdm
|
| 8 |
+
from onescience.models.fuxi import Fuxi
|
| 9 |
+
from onescience.utils.YParams import YParams
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def get_stats(data_dir, channels):
|
| 13 |
+
"""从新版 h5 中读取变量列表与归一化参数(均值/标准差)"""
|
| 14 |
+
h5_files = sorted(glob.glob(os.path.join(data_dir, "data", "*.h5")))
|
| 15 |
+
with h5py.File(h5_files[0], "r") as f:
|
| 16 |
+
ds = f["fields"]
|
| 17 |
+
all_variables = [v.decode() if isinstance(v, bytes) else v for v in ds.attrs["variables"]]
|
| 18 |
+
mu = f["global_means"][:] # [1, C, 1, 1]
|
| 19 |
+
std = f["global_stds"][:]
|
| 20 |
+
|
| 21 |
+
channel_indices = [all_variables.index(v) for v in channels]
|
| 22 |
+
means = mu[:, channel_indices, :, :]
|
| 23 |
+
stds = std[:, channel_indices, :, :]
|
| 24 |
+
return means, stds
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
if __name__ == "__main__":
|
| 28 |
+
if len(sys.argv) != 2:
|
| 29 |
+
print("Usage: input the mode: : short, medium, or long...")
|
| 30 |
+
sys.exit(1)
|
| 31 |
+
|
| 32 |
+
mode = sys.argv[1]
|
| 33 |
+
if mode not in ['short', 'medium', 'long']:
|
| 34 |
+
print(f'❌ ❌ Please input the mode: short, medium, or long...')
|
| 35 |
+
exit()
|
| 36 |
+
|
| 37 |
+
current_path = os.getcwd()
|
| 38 |
+
sys.path.append(current_path)
|
| 39 |
+
|
| 40 |
+
## Model config init
|
| 41 |
+
config_file_path = os.path.join(current_path, "conf/config.yaml")
|
| 42 |
+
cfg = YParams(config_file_path, "model")
|
| 43 |
+
## DataLoader init
|
| 44 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 45 |
+
cfg_data.dataloader.batch_size = 1
|
| 46 |
+
means, stds = get_stats(cfg_data.dataset.data_dir, cfg_data.dataset.channels)
|
| 47 |
+
|
| 48 |
+
if mode == 'short':
|
| 49 |
+
from onescience.datapipes.climate import ERA5Datapipe
|
| 50 |
+
train_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=False,
|
| 55 |
+
input_steps=2,
|
| 56 |
+
batch_size=1,
|
| 57 |
+
num_workers=4,
|
| 58 |
+
)
|
| 59 |
+
train_dataloader, train_sampler = train_datapipe.get_dataloader("train")
|
| 60 |
+
val_datapipe = ERA5Datapipe(
|
| 61 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 62 |
+
used_variables=cfg_data.dataset.channels,
|
| 63 |
+
used_years=cfg_data.dataset.val_time,
|
| 64 |
+
distributed=False,
|
| 65 |
+
input_steps=2,
|
| 66 |
+
batch_size=1,
|
| 67 |
+
num_workers=4,
|
| 68 |
+
)
|
| 69 |
+
val_dataloader, val_sampler = val_datapipe.get_dataloader("valid")
|
| 70 |
+
test_datapipe = ERA5Datapipe(
|
| 71 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 72 |
+
used_variables=cfg_data.dataset.channels,
|
| 73 |
+
used_years=cfg_data.dataset.test_time,
|
| 74 |
+
distributed=False,
|
| 75 |
+
input_steps=2,
|
| 76 |
+
batch_size=1,
|
| 77 |
+
num_workers=4,
|
| 78 |
+
)
|
| 79 |
+
test_dataloader, _ = test_datapipe.get_dataloader("test")
|
| 80 |
+
else:
|
| 81 |
+
from data_loader import ERA5Datapipe
|
| 82 |
+
train_datapipe = ERA5Datapipe(
|
| 83 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 84 |
+
used_variables=cfg_data.dataset.channels,
|
| 85 |
+
used_years=cfg_data.dataset.train_time,
|
| 86 |
+
pattern=mode,
|
| 87 |
+
distributed=False,
|
| 88 |
+
input_steps=2,
|
| 89 |
+
batch_size=1,
|
| 90 |
+
num_workers=4,
|
| 91 |
+
)
|
| 92 |
+
train_dataloader, train_sampler = train_datapipe.get_dataloader("train")
|
| 93 |
+
val_datapipe = ERA5Datapipe(
|
| 94 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 95 |
+
used_variables=cfg_data.dataset.channels,
|
| 96 |
+
used_years=cfg_data.dataset.val_time,
|
| 97 |
+
pattern=mode,
|
| 98 |
+
distributed=False,
|
| 99 |
+
input_steps=2,
|
| 100 |
+
batch_size=1,
|
| 101 |
+
num_workers=4,
|
| 102 |
+
)
|
| 103 |
+
val_dataloader, val_sampler = val_datapipe.get_dataloader("valid")
|
| 104 |
+
test_datapipe = ERA5Datapipe(
|
| 105 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 106 |
+
used_variables=cfg_data.dataset.channels,
|
| 107 |
+
used_years=cfg_data.dataset.test_time,
|
| 108 |
+
pattern=mode,
|
| 109 |
+
distributed=False,
|
| 110 |
+
input_steps=2,
|
| 111 |
+
batch_size=1,
|
| 112 |
+
num_workers=4,
|
| 113 |
+
)
|
| 114 |
+
test_dataloader, _ = test_datapipe.get_dataloader("test")
|
| 115 |
+
|
| 116 |
+
ckpt = torch.load(f"{cfg.checkpoint_dir}/model_{mode}_bak.pth", map_location="cuda:0")
|
| 117 |
+
model = Fuxi(img_size=cfg_data.dataset.img_size,
|
| 118 |
+
patch_size=cfg.patch_size,
|
| 119 |
+
in_chans=len(cfg_data.dataset.channels),
|
| 120 |
+
out_chans=len(cfg_data.dataset.channels),
|
| 121 |
+
embed_dim=cfg.embed_dim,
|
| 122 |
+
num_groups=cfg.num_groups,
|
| 123 |
+
num_heads=cfg.num_heads,
|
| 124 |
+
window_size=cfg.window_size
|
| 125 |
+
).to("cuda:0")
|
| 126 |
+
model.load_state_dict(ckpt["model_state_dict"])
|
| 127 |
+
|
| 128 |
+
model.eval()
|
| 129 |
+
save_path = f'./result/{mode}/data/'
|
| 130 |
+
if mode != 'long':
|
| 131 |
+
with torch.no_grad():
|
| 132 |
+
print(f"📂 infer results will be generated to './result/{mode}/data/'")
|
| 133 |
+
for data in tqdm(train_dataloader, desc="Inferring trainset", unit="batch"):
|
| 134 |
+
invar = data[0].to("cuda:0", dtype=torch.float32) # B, T, C, H, W
|
| 135 |
+
invar = invar.permute(0, 2, 1, 3, 4) # B, C, T, H, W
|
| 136 |
+
filename = data[4][-1][0]
|
| 137 |
+
pred_var = model(invar).cpu().numpy()
|
| 138 |
+
pred_var = pred_var * stds + means
|
| 139 |
+
os.makedirs(f'{save_path}/{filename[:4]}', exist_ok=True)
|
| 140 |
+
np.save(f"{save_path}/{filename[:4]}/{filename}.npy", pred_var)
|
| 141 |
+
|
| 142 |
+
with torch.no_grad():
|
| 143 |
+
print(f"📂 infer results will be generated to './result/{mode}/data/'")
|
| 144 |
+
for data in tqdm(val_dataloader, desc="Inferring validset", unit="batch"):
|
| 145 |
+
invar = data[0].to("cuda:0", dtype=torch.float32)
|
| 146 |
+
invar = invar.permute(0, 2, 1, 3, 4)
|
| 147 |
+
filename = data[4][-1][0]
|
| 148 |
+
pred_var = model(invar).cpu().numpy()
|
| 149 |
+
pred_var = pred_var * stds + means
|
| 150 |
+
os.makedirs(f'{save_path}/{filename[:4]}', exist_ok=True)
|
| 151 |
+
np.save(f"{save_path}/{filename[:4]}/{filename}.npy", pred_var)
|
| 152 |
+
|
| 153 |
+
with torch.no_grad():
|
| 154 |
+
print(f"📂 infer results will be generated to './result/{mode}/data/'")
|
| 155 |
+
for data in tqdm(test_dataloader, desc="Inferring testset", unit="batch"):
|
| 156 |
+
invar = data[0].to("cuda:0", dtype=torch.float32)
|
| 157 |
+
invar = invar.permute(0, 2, 1, 3, 4)
|
| 158 |
+
filename = data[4][-1][0]
|
| 159 |
+
pred_var = model(invar).cpu().numpy()
|
| 160 |
+
pred_var = pred_var * stds + means
|
| 161 |
+
os.makedirs(f'{save_path}/{filename[:4]}', exist_ok=True)
|
| 162 |
+
np.save(f"{save_path}/{filename[:4]}/{filename}.npy", pred_var)
|
scripts/result.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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, test_years, mode):
|
| 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 = []
|
| 31 |
+
for year in test_years:
|
| 32 |
+
result_dir = f'./result/{mode}/data/{year}/'
|
| 33 |
+
if os.path.exists(result_dir):
|
| 34 |
+
files = [f for f in os.listdir(result_dir) if f.endswith('.npy')]
|
| 35 |
+
total_files.extend(files)
|
| 36 |
+
total_files.sort()
|
| 37 |
+
return total_files, channel_indices, time_step
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def filename_to_index(filename, time_step):
|
| 41 |
+
"""将 YYYYMMDDHH 格式的文件名转换为年度 h5 文件中的时间步索引"""
|
| 42 |
+
dt = datetime.strptime(filename, "%Y%m%d%H")
|
| 43 |
+
year_start = datetime(dt.year, 1, 1)
|
| 44 |
+
hours = (dt - year_start).total_seconds() / 3600
|
| 45 |
+
return int(hours / time_step)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def get_result(total_files, channel_indices, time_step, data_dir, clim_mean, mode):
|
| 49 |
+
channel_rmse = np.zeros(len(channel_indices))
|
| 50 |
+
channel_acc = np.zeros(len(channel_indices))
|
| 51 |
+
clim_mean = clim_mean[0, :, :, :]
|
| 52 |
+
if not os.path.exists(f'./result/{mode}_rmse.npy') or not os.path.exists(f'./result/{mode}_acc.npy'):
|
| 53 |
+
numerator = np.zeros(len(channel_indices))
|
| 54 |
+
pred_sq_sum = np.zeros(len(channel_indices))
|
| 55 |
+
label_sq_sum = np.zeros(len(channel_indices))
|
| 56 |
+
for file in tqdm(total_files, unit="files"):
|
| 57 |
+
fname = file[:-4] # 去掉 .npy
|
| 58 |
+
year = fname[:4]
|
| 59 |
+
t_idx = filename_to_index(fname, time_step)
|
| 60 |
+
with h5py.File(os.path.join(data_dir, 'data', f'{year}.h5'), "r") as f:
|
| 61 |
+
label = f["fields"][t_idx] # [C, H, W]
|
| 62 |
+
label = label[channel_indices]
|
| 63 |
+
pred = np.load(f'result/{mode}/data/{year}/{file}').squeeze()
|
| 64 |
+
|
| 65 |
+
label_anom = label - clim_mean
|
| 66 |
+
pred_anom = pred - clim_mean
|
| 67 |
+
# 累加
|
| 68 |
+
numerator += np.sum(pred_anom * label_anom, axis=(1, 2))
|
| 69 |
+
pred_sq_sum += np.sum(pred_anom ** 2, axis=(1, 2))
|
| 70 |
+
label_sq_sum += np.sum(label_anom ** 2, axis=(1, 2))
|
| 71 |
+
channel_rmse += np.sqrt(np.mean((label - pred) ** 2, axis=(1, 2)))
|
| 72 |
+
|
| 73 |
+
channel_rmse /= len(total_files)
|
| 74 |
+
channel_acc = numerator / (np.sqrt(pred_sq_sum * label_sq_sum) + 1e-8)
|
| 75 |
+
np.save(f'./result/{mode}_acc.npy', channel_acc)
|
| 76 |
+
np.save(f'./result/{mode}_rmse.npy', channel_rmse)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def show_result(mode):
|
| 80 |
+
channel_rmse = np.load(f'./result/{mode}_rmse.npy')
|
| 81 |
+
channel_acc = np.load(f'./result/{mode}_acc.npy')
|
| 82 |
+
|
| 83 |
+
channels = [cfg_data.dataset.channels[i] for i in range(len(channel_indices))]
|
| 84 |
+
w = 24 # 最长 channel 名宽度
|
| 85 |
+
|
| 86 |
+
# 表头
|
| 87 |
+
print(f"┌{'─' * (w + 2)}┬{'─' * 14}┬{'─' * 14}┐")
|
| 88 |
+
print(f"│ {'Channel':<{w}} │ {'RMSE':>12} │ {'ACC':>12} │")
|
| 89 |
+
print(f"├{'─' * (w + 2)}┼{'─' * 14}┼{'─' * 14}┤")
|
| 90 |
+
# 数据行
|
| 91 |
+
for i, ch in enumerate(channels):
|
| 92 |
+
print(f"│ {ch:<{w}} │ {channel_rmse[i]:>12.4f} | {channel_acc[i]:>12.4f} |")
|
| 93 |
+
print(f"├{'─' * (w + 2)}┼{'─' * 14}┼{'─' * 14}┤")
|
| 94 |
+
print(f"│ {'Average':<{w}} │ {np.mean(channel_rmse):>12.4f} │ {np.mean(channel_acc):>12.4f} │")
|
| 95 |
+
print(f"└{'─' * (w + 2)}┴{'─' * 14}┴{'─' * 14}┘")
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def plot(label, pred, var, filename):
|
| 99 |
+
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
|
| 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 |
+
vmin = min(label.min(), pred.min())
|
| 107 |
+
vmax = max(label.max(), pred.max())
|
| 108 |
+
|
| 109 |
+
diff = label - pred
|
| 110 |
+
rmse = np.sqrt(np.mean(diff ** 2))
|
| 111 |
+
diff_abs_max = np.abs(diff).max()
|
| 112 |
+
|
| 113 |
+
plot_configs = [
|
| 114 |
+
{'data': label, 'title': 'Truth', 'cmap': 'viridis', 'vmin': vmin, 'vmax': vmax},
|
| 115 |
+
{'data': pred, 'title': 'Prediction', 'cmap': 'viridis', 'vmin': vmin, 'vmax': vmax},
|
| 116 |
+
{'data': diff, 'title': f'Difference (RMSE={rmse:.2f})', 'cmap': 'RdBu_r', 'vmin': -diff_abs_max, 'vmax': diff_abs_max},
|
| 117 |
+
]
|
| 118 |
+
|
| 119 |
+
for ax, cfg in zip(axes, plot_configs):
|
| 120 |
+
im = ax.imshow(cfg['data'], cmap=cfg['cmap'], vmin=cfg['vmin'], vmax=cfg['vmax'])
|
| 121 |
+
ax.set_title(cfg['title'], fontsize=12, pad=4)
|
| 122 |
+
ax.set_xlabel('Longitude')
|
| 123 |
+
ax.set_ylabel('Latitude')
|
| 124 |
+
ax.set_xticks(xticks)
|
| 125 |
+
ax.set_xticklabels(xtick_labels)
|
| 126 |
+
ax.set_yticks(yticks)
|
| 127 |
+
ax.set_yticklabels(ytick_labels)
|
| 128 |
+
plt.colorbar(im, ax=ax, orientation='horizontal')
|
| 129 |
+
|
| 130 |
+
fig.suptitle(var, fontsize=14, fontweight='bold', y=0.98)
|
| 131 |
+
plt.savefig(filename, dpi=300, bbox_inches='tight')
|
| 132 |
+
plt.close()
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def plot_loss(train_loss, valid_loss):
|
| 136 |
+
mask = ~(np.isnan(train_loss) | np.isnan(valid_loss))
|
| 137 |
+
train_loss = train_loss[mask]
|
| 138 |
+
valid_loss = valid_loss[mask]
|
| 139 |
+
|
| 140 |
+
fig, ax = plt.subplots(figsize=(5, 3.5))
|
| 141 |
+
colors = {'train': '#2563EB', 'valid': '#EA580C'}
|
| 142 |
+
epochs = np.arange(1, len(train_loss) + 1)
|
| 143 |
+
|
| 144 |
+
ax.plot(epochs, train_loss, color=colors['train'], linewidth=1.5, label='Train')
|
| 145 |
+
ax.plot(epochs, valid_loss, color=colors['valid'], linewidth=1.5, label='Valid', linestyle='--')
|
| 146 |
+
min_idx = np.argmin(valid_loss)
|
| 147 |
+
ax.scatter(epochs[min_idx], valid_loss[min_idx],
|
| 148 |
+
color=colors['valid'], s=40, zorder=5, edgecolors='white')
|
| 149 |
+
ax.annotate(f'Best: {valid_loss[min_idx]:.3f}',
|
| 150 |
+
xy=(epochs[min_idx], valid_loss[min_idx]),
|
| 151 |
+
xytext=(10, 10), textcoords='offset points', fontsize=8, color=colors['valid'],
|
| 152 |
+
arrowprops=dict(arrowstyle='-', color=colors['valid'], lw=0.5))
|
| 153 |
+
|
| 154 |
+
ax.set(xlabel='Epoch', ylabel='Loss', xlim=(0, len(train_loss) + 1))
|
| 155 |
+
ax.legend(frameon=False, loc='upper right')
|
| 156 |
+
ax.grid(True, linestyle='--', alpha=0.3)
|
| 157 |
+
ax.spines[['top', 'right']].set_visible(False)
|
| 158 |
+
|
| 159 |
+
plt.tight_layout()
|
| 160 |
+
plt.savefig('./result/loss.png', dpi=300, bbox_inches='tight')
|
| 161 |
+
plt.close()
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
if __name__ == "__main__":
|
| 165 |
+
if len(sys.argv) != 2:
|
| 166 |
+
print("Usage: input the mode: : short, medium, or long...")
|
| 167 |
+
sys.exit(1)
|
| 168 |
+
|
| 169 |
+
mode = sys.argv[1]
|
| 170 |
+
if mode not in ['short', 'medium', 'long']:
|
| 171 |
+
print(f'❌ ❌ Please input the mode: short, medium, or long...')
|
| 172 |
+
exit()
|
| 173 |
+
|
| 174 |
+
current_path = os.getcwd()
|
| 175 |
+
sys.path.append(current_path)
|
| 176 |
+
config_file_path = os.path.join(current_path, 'conf/config.yaml')
|
| 177 |
+
cfg = YParams(config_file_path, 'model')
|
| 178 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 179 |
+
|
| 180 |
+
train_loss = np.load(f'./data/checkpoints/tr_{mode}_loss.npy')
|
| 181 |
+
valid_loss = np.load(f'./data/checkpoints/va_{mode}_loss.npy')
|
| 182 |
+
plot_loss(train_loss, valid_loss)
|
| 183 |
+
|
| 184 |
+
data_dir = cfg_data.dataset.data_dir
|
| 185 |
+
test_years = cfg_data.dataset.test_time
|
| 186 |
+
total_files, channel_indices, time_step = get_metadata(data_dir, cfg_data.dataset.channels, test_years, mode)
|
| 187 |
+
|
| 188 |
+
# Load data & Compute RMSE/ACC per channel
|
| 189 |
+
h5_files = sorted(glob.glob(os.path.join(data_dir, "data", "*.h5")))
|
| 190 |
+
with h5py.File(h5_files[0], "r") as f:
|
| 191 |
+
mu = f["global_means"][:]
|
| 192 |
+
clim_mean = mu[:, channel_indices, :, :]
|
| 193 |
+
get_result(total_files, channel_indices, time_step, data_dir, clim_mean, mode)
|
| 194 |
+
show_result(mode)
|
| 195 |
+
|
| 196 |
+
##### 默认绘制 test_time 第一年的第一个时间步,用户可自行指定日期和变量 #####
|
| 197 |
+
test_year = cfg_data.dataset.test_time[0]
|
| 198 |
+
eg_files = [f'{test_year}010206']
|
| 199 |
+
channel_index = [cfg_data.dataset.channels.index(v) for v in ['2m_temperature', 'geopotential_500', 'temperature_500']]
|
| 200 |
+
|
| 201 |
+
selected_var = [cfg_data.dataset.channels[int(i)] for i in channel_index]
|
| 202 |
+
print(f"seleted date: {eg_files}")
|
| 203 |
+
print(f"selected channels: {selected_var}")
|
| 204 |
+
for file in eg_files:
|
| 205 |
+
year = file[:4]
|
| 206 |
+
t_idx = filename_to_index(file, time_step)
|
| 207 |
+
with h5py.File(os.path.join(data_dir, 'data', f'{year}.h5'), "r") as f:
|
| 208 |
+
label = f["fields"][t_idx] # [C, H, W]
|
| 209 |
+
label = label[channel_indices]
|
| 210 |
+
pred = np.load(f'result/{mode}/data/{year}/{file}.npy').squeeze()
|
| 211 |
+
for i in range(len(selected_var)):
|
| 212 |
+
filename = f'./result/{mode}_{file}_{selected_var[i]}.png'
|
| 213 |
+
plot(label[channel_index[i]], pred[channel_index[i]], selected_var[i], filename)
|
| 214 |
+
print(f'✅plot {filename}')
|
scripts/train_long.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 tqdm import tqdm
|
| 15 |
+
from torch.nn.parallel import DistributedDataParallel
|
| 16 |
+
from model.fuxi import Fuxi
|
| 17 |
+
from scripts.data_loader import ERA5Datapipe
|
| 18 |
+
from onescience.utils.YParams import YParams
|
| 19 |
+
from onescience.metrics.climate.loss import LatitudeWeightedLoss
|
| 20 |
+
from onescience.memory.checkpoint import replace_function
|
| 21 |
+
|
| 22 |
+
from apex import optimizers
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def main():
|
| 26 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
| 27 |
+
logger = logging.getLogger()
|
| 28 |
+
|
| 29 |
+
## Model config init
|
| 30 |
+
config_file_path = os.path.join(current_path, "conf/config.yaml")
|
| 31 |
+
cfg = YParams(config_file_path, "model")
|
| 32 |
+
|
| 33 |
+
## Distributed config init
|
| 34 |
+
cfg.world_size = 1
|
| 35 |
+
if "WORLD_SIZE" in os.environ:
|
| 36 |
+
cfg.world_size = int(os.environ["WORLD_SIZE"])
|
| 37 |
+
world_rank = 0
|
| 38 |
+
local_rank = 0
|
| 39 |
+
if cfg.world_size > 1:
|
| 40 |
+
dist.init_process_group(backend="nccl", init_method="env://")
|
| 41 |
+
local_rank = int(os.environ["LOCAL_RANK"])
|
| 42 |
+
world_rank = dist.get_rank()
|
| 43 |
+
if not os.path.exists(f"{cfg.checkpoint_dir}/model_medium_bak.pth"):
|
| 44 |
+
if world_rank == 0:
|
| 45 |
+
print(f'❌❌The Fuxi medium model must be trained before this model.')
|
| 46 |
+
exit()
|
| 47 |
+
## DataLoader init
|
| 48 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 49 |
+
datapipe = ERA5Datapipe(
|
| 50 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 51 |
+
used_variables=cfg_data.dataset.channels,
|
| 52 |
+
used_years=cfg_data.dataset.train_time,
|
| 53 |
+
pattern='long',
|
| 54 |
+
distributed=dist.is_initialized(),
|
| 55 |
+
output_steps=2,
|
| 56 |
+
input_steps=2,
|
| 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 |
+
pattern='long',
|
| 66 |
+
distributed=dist.is_initialized(),
|
| 67 |
+
output_steps=2,
|
| 68 |
+
input_steps=2,
|
| 69 |
+
batch_size=cfg_data.dataloader.batch_size,
|
| 70 |
+
num_workers=cfg_data.dataloader.num_workers
|
| 71 |
+
)
|
| 72 |
+
val_dataloader, val_sampler = datapipe.get_dataloader("valid")
|
| 73 |
+
|
| 74 |
+
## Model init
|
| 75 |
+
model = Fuxi(img_size=cfg_data.dataset.img_size,
|
| 76 |
+
patch_size=cfg.patch_size,
|
| 77 |
+
in_chans=len(cfg_data.dataset.channels),
|
| 78 |
+
out_chans=len(cfg_data.dataset.channels),
|
| 79 |
+
embed_dim=cfg.embed_dim,
|
| 80 |
+
num_groups=cfg.num_groups,
|
| 81 |
+
num_heads=cfg.num_heads,
|
| 82 |
+
window_size=cfg.window_size
|
| 83 |
+
).to(local_rank)
|
| 84 |
+
optimizer = optimizers.FusedAdam(model.parameters(), lr=cfg.train_lr)
|
| 85 |
+
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, factor=0.2, patience=5, mode="min")
|
| 86 |
+
loss_obj = LatitudeWeightedLoss(loss_type="l1", normalize=True).to(local_rank)
|
| 87 |
+
|
| 88 |
+
## Train process init
|
| 89 |
+
os.makedirs(cfg.checkpoint_dir, exist_ok=True)
|
| 90 |
+
train_loss_file = f"{cfg.checkpoint_dir}/tr_long_loss.npy"
|
| 91 |
+
valid_loss_file = f"{cfg.checkpoint_dir}/va_long_loss.npy"
|
| 92 |
+
best_valid_loss = 1.0e6
|
| 93 |
+
best_loss_epoch = 0
|
| 94 |
+
train_losses = np.empty((0,), dtype=np.float32)
|
| 95 |
+
valid_losses = np.empty((0,), dtype=np.float32)
|
| 96 |
+
current_epoch = 0
|
| 97 |
+
|
| 98 |
+
## Get model params count
|
| 99 |
+
if cfg.world_size == 1:
|
| 100 |
+
total_params = sum(p.numel() for p in model.parameters())
|
| 101 |
+
print("\n\n")
|
| 102 |
+
print("-" * 50)
|
| 103 |
+
print(f"📂 now params is {total_params}, {total_params / 1e6:.2f}M, {total_params / 1e9:.2f}B")
|
| 104 |
+
print("-" * 50, "\n")
|
| 105 |
+
|
| 106 |
+
## Load model weight if there exist well-trained model
|
| 107 |
+
|
| 108 |
+
if not os.path.exists(f"{cfg.checkpoint_dir}/model_medium_bak.pth"):
|
| 109 |
+
print('⚠️ ⚠️ Please train to get medium model first...')
|
| 110 |
+
exit()
|
| 111 |
+
|
| 112 |
+
if os.path.exists(f"{cfg.checkpoint_dir}/model_long_bak.pth"):
|
| 113 |
+
if world_rank == 0:
|
| 114 |
+
print("\n\n")
|
| 115 |
+
print("-" * 50)
|
| 116 |
+
print(f"✅ There has a long-pattern model weight, load and continue training...")
|
| 117 |
+
print(f'If you want to finetune a new model, ensure there is no model_short_bak.pth file in {cfg.checkpoint_dir}')
|
| 118 |
+
print("-" * 50, "\n")
|
| 119 |
+
ckpt = torch.load(f"{cfg.checkpoint_dir}/model_long_bak.pth", map_location=f'cuda:{local_rank}', weights_only=False)
|
| 120 |
+
model.load_state_dict(ckpt["model_state_dict"])
|
| 121 |
+
optimizer.load_state_dict(ckpt["optimizer_state_dict"])
|
| 122 |
+
scheduler.load_state_dict(ckpt["scheduler_state_dict"])
|
| 123 |
+
best_valid_loss = ckpt["best_valid_loss"]
|
| 124 |
+
best_loss_epoch = ckpt["best_loss_epoch"]
|
| 125 |
+
current_epoch = ckpt["current_epoch"]
|
| 126 |
+
train_losses = np.load(f"{cfg.checkpoint_dir}/tr_long_loss.npy")
|
| 127 |
+
valid_losses = np.load(f"{cfg.checkpoint_dir}/va_long_loss.npy")
|
| 128 |
+
else:
|
| 129 |
+
if world_rank == 0:
|
| 130 |
+
print("\n\n")
|
| 131 |
+
print("-" * 50)
|
| 132 |
+
print(f"✅ Load medium model and continue to finetune...")
|
| 133 |
+
print("-" * 50, "\n")
|
| 134 |
+
ckpt = torch.load(f"{cfg.checkpoint_dir}/model_medium_bak.pth", map_location=f'cuda:{local_rank}', weights_only=False)
|
| 135 |
+
model.load_state_dict(ckpt["model_state_dict"])
|
| 136 |
+
optimizer.load_state_dict(ckpt["optimizer_state_dict"])
|
| 137 |
+
scheduler.load_state_dict(ckpt["scheduler_state_dict"])
|
| 138 |
+
|
| 139 |
+
## Distributed model
|
| 140 |
+
if cfg.world_size > 1:
|
| 141 |
+
model = DistributedDataParallel(model, device_ids=[local_rank], output_device=local_rank, find_unused_parameters=True)
|
| 142 |
+
|
| 143 |
+
world_rank == 0 and logger.info(f"start training ...")
|
| 144 |
+
|
| 145 |
+
for epoch in range(current_epoch, cfg.finetune_step):
|
| 146 |
+
if epoch > cfg.step_change_freq:
|
| 147 |
+
num_rollout_steps = epoch // cfg.step_change_freq + 2
|
| 148 |
+
if num_rollout_steps > 12: # Paper: 2~12 curriculum training schedule, then skip to 20.
|
| 149 |
+
num_rollout_steps = cfg.long_num_steps - cfg.medium_num_steps
|
| 150 |
+
if epoch % cfg.step_change_freq == 0 and world_rank == 0:
|
| 151 |
+
logger.info(f"⚠️ ⚠️ Switching to {num_rollout_steps}-step rollout!")
|
| 152 |
+
datapipe = ERA5Datapipe(
|
| 153 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 154 |
+
used_variables=cfg_data.dataset.channels,
|
| 155 |
+
used_years=cfg_data.dataset.train_time,
|
| 156 |
+
pattern='long',
|
| 157 |
+
distributed=dist.is_initialized(),
|
| 158 |
+
output_steps=num_rollout_steps,
|
| 159 |
+
input_steps=2,
|
| 160 |
+
batch_size=cfg_data.dataloader.batch_size,
|
| 161 |
+
num_workers=cfg_data.dataloader.num_workers
|
| 162 |
+
)
|
| 163 |
+
train_dataloader, train_sampler = datapipe.get_dataloader("train")
|
| 164 |
+
datapipe = ERA5Datapipe(
|
| 165 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 166 |
+
used_variables=cfg_data.dataset.channels,
|
| 167 |
+
used_years=cfg_data.dataset.val_time,
|
| 168 |
+
pattern='long',
|
| 169 |
+
distributed=dist.is_initialized(),
|
| 170 |
+
output_steps=num_rollout_steps,
|
| 171 |
+
input_steps=2,
|
| 172 |
+
batch_size=cfg_data.dataloader.batch_size,
|
| 173 |
+
num_workers=cfg_data.dataloader.num_workers
|
| 174 |
+
)
|
| 175 |
+
val_dataloader, val_sampler = datapipe.get_dataloader("valid")
|
| 176 |
+
|
| 177 |
+
if dist.is_initialized():
|
| 178 |
+
train_sampler.set_epoch(epoch)
|
| 179 |
+
val_sampler.set_epoch(epoch)
|
| 180 |
+
|
| 181 |
+
model.train()
|
| 182 |
+
train_loss = 0
|
| 183 |
+
start_time = time.time()
|
| 184 |
+
for j, data in enumerate(train_dataloader):
|
| 185 |
+
invar = data[0].to(local_rank, dtype=torch.float32) # B, T, C, H, W
|
| 186 |
+
invar = invar.permute(0, 2, 1, 3, 4) # B, C, T, H, W
|
| 187 |
+
outvar = data[1].to(local_rank, dtype=torch.float32)
|
| 188 |
+
for t in range(outvar.shape[1]):
|
| 189 |
+
if t < outvar.shape[1] - 1:
|
| 190 |
+
with torch.no_grad():
|
| 191 |
+
outvar_pred = model(invar)
|
| 192 |
+
# B, 70, 2, 721, 1440
|
| 193 |
+
invar[:, :, 0] = invar[:, :, -1]
|
| 194 |
+
invar[:, :, -1] = outvar_pred.detach()
|
| 195 |
+
else:
|
| 196 |
+
with replace_function(model, ["cube_embedding", "u_transformer"], cfg.world_size > 1):
|
| 197 |
+
outvar_pred = model(invar)
|
| 198 |
+
loss = loss_obj(outvar_pred, outvar[:, t])
|
| 199 |
+
optimizer.zero_grad()
|
| 200 |
+
loss.backward()
|
| 201 |
+
optimizer.step()
|
| 202 |
+
train_loss += loss.item()
|
| 203 |
+
if world_rank == 0:
|
| 204 |
+
logger.info(f'Train: Epoch {epoch}-{j+1}/{len(train_dataloader)} '
|
| 205 |
+
f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
|
| 206 |
+
f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
|
| 207 |
+
f'loss:{train_loss / (j+1): .04f}')
|
| 208 |
+
|
| 209 |
+
train_loss /= len(train_dataloader)
|
| 210 |
+
|
| 211 |
+
model.eval()
|
| 212 |
+
valid_loss = 0
|
| 213 |
+
with torch.no_grad():
|
| 214 |
+
start_time = time.time()
|
| 215 |
+
for j, data in enumerate(val_dataloader):
|
| 216 |
+
invar = data[0].to(local_rank, dtype=torch.float32) # B, T, C, H, W
|
| 217 |
+
invar = invar.permute(0, 2, 1, 3, 4) # B, C, T, H, W
|
| 218 |
+
outvar = data[1].to(local_rank, dtype=torch.float32)
|
| 219 |
+
for t in range(outvar.shape[1]):
|
| 220 |
+
outvar_pred = model(invar)
|
| 221 |
+
# B, 70, 2, 721, 1440
|
| 222 |
+
invar[:, :, 0] = invar[:, :, -1]
|
| 223 |
+
invar[:, :, -1] = outvar_pred.detach()
|
| 224 |
+
loss = loss_obj(outvar_pred, outvar[:, -1])
|
| 225 |
+
|
| 226 |
+
if cfg.world_size > 1:
|
| 227 |
+
loss_tensor = loss.detach().to(local_rank)
|
| 228 |
+
dist.all_reduce(loss_tensor)
|
| 229 |
+
loss = loss_tensor.item() / cfg.world_size
|
| 230 |
+
valid_loss += loss
|
| 231 |
+
else:
|
| 232 |
+
valid_loss += loss.item()
|
| 233 |
+
if world_rank == 0:
|
| 234 |
+
logger.info(f'Valid: Epoch {epoch}-{j+1}/{len(val_dataloader)} '
|
| 235 |
+
f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
|
| 236 |
+
f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
|
| 237 |
+
f'loss:{valid_loss / (j+1): .04f}')
|
| 238 |
+
|
| 239 |
+
valid_loss /= len(val_dataloader)
|
| 240 |
+
is_save_ckp = False
|
| 241 |
+
if valid_loss < best_valid_loss:
|
| 242 |
+
best_valid_loss = valid_loss
|
| 243 |
+
best_loss_epoch = epoch
|
| 244 |
+
world_rank == 0 and save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, cfg.checkpoint_dir, epoch)
|
| 245 |
+
is_save_ckp = True
|
| 246 |
+
|
| 247 |
+
scheduler.step(valid_loss)
|
| 248 |
+
|
| 249 |
+
if world_rank == 0:
|
| 250 |
+
logger.info(f"Epoch [{epoch + 1}/{cfg.max_epoch}], "
|
| 251 |
+
f"Train Loss: {train_loss:.4f}, "
|
| 252 |
+
f"Valid Loss: {valid_loss:.4f}, "
|
| 253 |
+
f"Best loss at Epoch: {best_loss_epoch + 1}"
|
| 254 |
+
+ (", saving checkpoint" if is_save_ckp else "")
|
| 255 |
+
)
|
| 256 |
+
train_losses = np.append(train_losses, train_loss)
|
| 257 |
+
valid_losses = np.append(valid_losses, valid_loss)
|
| 258 |
+
|
| 259 |
+
np.save(train_loss_file, train_losses)
|
| 260 |
+
np.save(valid_loss_file, valid_losses)
|
| 261 |
+
|
| 262 |
+
if epoch - best_loss_epoch > cfg.patience:
|
| 263 |
+
print(f"Loss has not decrease in {cfg.patience} epochs, stopping training...")
|
| 264 |
+
exit()
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, model_path, epoch):
|
| 268 |
+
model_to_save = model.module if hasattr(model, "module") else model
|
| 269 |
+
state = {"model_state_dict": model_to_save.state_dict(),
|
| 270 |
+
"optimizer_state_dict": optimizer.state_dict(),
|
| 271 |
+
"scheduler_state_dict": scheduler.state_dict(),
|
| 272 |
+
"best_valid_loss": best_valid_loss,
|
| 273 |
+
"best_loss_epoch": best_loss_epoch,
|
| 274 |
+
"current_epoch": epoch
|
| 275 |
+
}
|
| 276 |
+
torch.save(state, f"{model_path}/model_long.pth")
|
| 277 |
+
### the weight file saving may interrupted due to DCU queue limit, get a backup to ensure there at least has one model
|
| 278 |
+
os.system(f"mv {model_path}/model_long.pth {model_path}/model_long_bak.pth")
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
if __name__ == "__main__":
|
| 282 |
+
current_path = os.getcwd()
|
| 283 |
+
sys.path.append(current_path)
|
| 284 |
+
main()
|
scripts/train_medium.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 tqdm import tqdm
|
| 15 |
+
from torch.nn.parallel import DistributedDataParallel
|
| 16 |
+
from model.fuxi import Fuxi
|
| 17 |
+
from scripts.data_loader import ERA5Datapipe
|
| 18 |
+
from onescience.utils.YParams import YParams
|
| 19 |
+
from onescience.metrics.climate.loss import LatitudeWeightedLoss
|
| 20 |
+
from onescience.memory.checkpoint import replace_function
|
| 21 |
+
|
| 22 |
+
from apex import optimizers
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def main():
|
| 26 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
| 27 |
+
logger = logging.getLogger()
|
| 28 |
+
|
| 29 |
+
## Model config init
|
| 30 |
+
config_file_path = os.path.join(current_path, "conf/config.yaml")
|
| 31 |
+
cfg = YParams(config_file_path, "model")
|
| 32 |
+
|
| 33 |
+
## Distributed config init
|
| 34 |
+
cfg.world_size = 1
|
| 35 |
+
if "WORLD_SIZE" in os.environ:
|
| 36 |
+
cfg.world_size = int(os.environ["WORLD_SIZE"])
|
| 37 |
+
world_rank = 0
|
| 38 |
+
local_rank = 0
|
| 39 |
+
if cfg.world_size > 1:
|
| 40 |
+
dist.init_process_group(backend="nccl", init_method="env://")
|
| 41 |
+
local_rank = int(os.environ["LOCAL_RANK"])
|
| 42 |
+
world_rank = dist.get_rank()
|
| 43 |
+
if not os.path.exists(f"{cfg.checkpoint_dir}/model_short_bak.pth"):
|
| 44 |
+
if world_rank == 0:
|
| 45 |
+
print(f'❌❌The Fuxi short model must be trained before this model.')
|
| 46 |
+
exit()
|
| 47 |
+
## DataLoader init
|
| 48 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 49 |
+
datapipe = ERA5Datapipe(
|
| 50 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 51 |
+
used_variables=cfg_data.dataset.channels,
|
| 52 |
+
used_years=cfg_data.dataset.train_time,
|
| 53 |
+
pattern='medium',
|
| 54 |
+
distributed=dist.is_initialized(),
|
| 55 |
+
output_steps=2,
|
| 56 |
+
input_steps=2,
|
| 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 |
+
pattern='medium',
|
| 66 |
+
distributed=dist.is_initialized(),
|
| 67 |
+
output_steps=2,
|
| 68 |
+
input_steps=2,
|
| 69 |
+
batch_size=cfg_data.dataloader.batch_size,
|
| 70 |
+
num_workers=cfg_data.dataloader.num_workers
|
| 71 |
+
)
|
| 72 |
+
val_dataloader, val_sampler = datapipe.get_dataloader("valid")
|
| 73 |
+
|
| 74 |
+
## Model init
|
| 75 |
+
model = Fuxi(img_size=cfg_data.dataset.img_size,
|
| 76 |
+
patch_size=cfg.patch_size,
|
| 77 |
+
in_chans=len(cfg_data.dataset.channels),
|
| 78 |
+
out_chans=len(cfg_data.dataset.channels),
|
| 79 |
+
embed_dim=cfg.embed_dim,
|
| 80 |
+
num_groups=cfg.num_groups,
|
| 81 |
+
num_heads=cfg.num_heads,
|
| 82 |
+
window_size=cfg.window_size
|
| 83 |
+
).to(local_rank)
|
| 84 |
+
optimizer = optimizers.FusedAdam(model.parameters(), lr=cfg.train_lr)
|
| 85 |
+
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, factor=0.2, patience=5, mode="min")
|
| 86 |
+
loss_obj = LatitudeWeightedLoss(loss_type="l1", normalize=True).to(local_rank)
|
| 87 |
+
|
| 88 |
+
## Train process init
|
| 89 |
+
os.makedirs(cfg.checkpoint_dir, exist_ok=True)
|
| 90 |
+
train_loss_file = f"{cfg.checkpoint_dir}/tr_medium_loss.npy"
|
| 91 |
+
valid_loss_file = f"{cfg.checkpoint_dir}/va_medium_loss.npy"
|
| 92 |
+
best_valid_loss = 1.0e6
|
| 93 |
+
best_loss_epoch = 0
|
| 94 |
+
train_losses = np.empty((0,), dtype=np.float32)
|
| 95 |
+
valid_losses = np.empty((0,), dtype=np.float32)
|
| 96 |
+
current_epoch = 0
|
| 97 |
+
|
| 98 |
+
## Get model params count
|
| 99 |
+
if cfg.world_size == 1:
|
| 100 |
+
total_params = sum(p.numel() for p in model.parameters())
|
| 101 |
+
print("\n\n")
|
| 102 |
+
print("-" * 50)
|
| 103 |
+
print(f"📂 now params is {total_params}, {total_params / 1e6:.2f}M, {total_params / 1e9:.2f}B")
|
| 104 |
+
print("-" * 50, "\n")
|
| 105 |
+
|
| 106 |
+
## Load model weight if there exist well-trained model
|
| 107 |
+
|
| 108 |
+
## Load model weight if there exist well-trained model
|
| 109 |
+
if not os.path.exists(f"{cfg.checkpoint_dir}/model_short_bak.pth"):
|
| 110 |
+
print('⚠️ ⚠️ Please train to get short model first...')
|
| 111 |
+
exit()
|
| 112 |
+
|
| 113 |
+
if os.path.exists(f"{cfg.checkpoint_dir}/model_medium_bak.pth"):
|
| 114 |
+
if world_rank == 0:
|
| 115 |
+
print("\n\n")
|
| 116 |
+
print("-" * 50)
|
| 117 |
+
print(f"✅ There has a medium-pattern model weight, load and continue training...")
|
| 118 |
+
print(f'If you want to finetune a new model, ensure there is no model_short_bak.pth file in {cfg.checkpoint_dir}')
|
| 119 |
+
print("-" * 50, "\n")
|
| 120 |
+
ckpt = torch.load(f"{cfg.checkpoint_dir}/model_medium_bak.pth", map_location=f'cuda:{local_rank}', weights_only=False)
|
| 121 |
+
model.load_state_dict(ckpt["model_state_dict"])
|
| 122 |
+
optimizer.load_state_dict(ckpt["optimizer_state_dict"])
|
| 123 |
+
scheduler.load_state_dict(ckpt["scheduler_state_dict"])
|
| 124 |
+
best_valid_loss = ckpt["best_valid_loss"]
|
| 125 |
+
best_loss_epoch = ckpt["best_loss_epoch"]
|
| 126 |
+
current_epoch = ckpt["current_epoch"]
|
| 127 |
+
train_losses = np.load(f"{cfg.checkpoint_dir}/tr_medium_loss.npy")
|
| 128 |
+
valid_losses = np.load(f"{cfg.checkpoint_dir}/va_medium_loss.npy")
|
| 129 |
+
else:
|
| 130 |
+
if world_rank == 0:
|
| 131 |
+
print("\n\n")
|
| 132 |
+
print("-" * 50)
|
| 133 |
+
print(f"✅ Load short model and continue to finetune...")
|
| 134 |
+
print("-" * 50, "\n")
|
| 135 |
+
ckpt = torch.load(f"{cfg.checkpoint_dir}/model_short_bak.pth", map_location=f'cuda:{local_rank}', weights_only=False)
|
| 136 |
+
model.load_state_dict(ckpt["model_state_dict"])
|
| 137 |
+
optimizer.load_state_dict(ckpt["optimizer_state_dict"])
|
| 138 |
+
scheduler.load_state_dict(ckpt["scheduler_state_dict"])
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
## Distributed model
|
| 142 |
+
if cfg.world_size > 1:
|
| 143 |
+
model = DistributedDataParallel(model, device_ids=[local_rank], output_device=local_rank, find_unused_parameters=True)
|
| 144 |
+
|
| 145 |
+
world_rank == 0 and logger.info(f"start training ...")
|
| 146 |
+
|
| 147 |
+
for epoch in range(current_epoch, cfg.finetune_step):
|
| 148 |
+
if epoch % cfg.step_change_freq == 0:
|
| 149 |
+
num_rollout_steps = epoch // cfg.step_change_freq + 2
|
| 150 |
+
if num_rollout_steps > 12: # Paper: 2~12 curriculum training schedule, then skip to 20.
|
| 151 |
+
num_rollout_steps = cfg.medium_num_steps - cfg.short_num_steps
|
| 152 |
+
world_rank == 0 and logger.info(f"⚠️ ⚠️ Switching to {num_rollout_steps}-step rollout!")
|
| 153 |
+
datapipe = ERA5Datapipe(
|
| 154 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 155 |
+
used_variables=cfg_data.dataset.channels,
|
| 156 |
+
used_years=cfg_data.dataset.train_time,
|
| 157 |
+
pattern='medium',
|
| 158 |
+
distributed=dist.is_initialized(),
|
| 159 |
+
output_steps=num_rollout_steps,
|
| 160 |
+
input_steps=2,
|
| 161 |
+
batch_size=cfg_data.dataloader.batch_size,
|
| 162 |
+
num_workers=cfg_data.dataloader.num_workers
|
| 163 |
+
)
|
| 164 |
+
train_dataloader, train_sampler = datapipe.get_dataloader("train")
|
| 165 |
+
datapipe = ERA5Datapipe(
|
| 166 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 167 |
+
used_variables=cfg_data.dataset.channels,
|
| 168 |
+
used_years=cfg_data.dataset.val_time,
|
| 169 |
+
pattern='medium',
|
| 170 |
+
distributed=dist.is_initialized(),
|
| 171 |
+
output_steps=num_rollout_steps,
|
| 172 |
+
input_steps=2,
|
| 173 |
+
batch_size=cfg_data.dataloader.batch_size,
|
| 174 |
+
num_workers=cfg_data.dataloader.num_workers
|
| 175 |
+
)
|
| 176 |
+
val_dataloader, val_sampler = datapipe.get_dataloader("valid")
|
| 177 |
+
|
| 178 |
+
if dist.is_initialized():
|
| 179 |
+
train_sampler.set_epoch(epoch)
|
| 180 |
+
val_sampler.set_epoch(epoch)
|
| 181 |
+
|
| 182 |
+
model.train()
|
| 183 |
+
train_loss = 0
|
| 184 |
+
start_time = time.time()
|
| 185 |
+
for j, data in enumerate(train_dataloader):
|
| 186 |
+
invar = data[0].to(local_rank, dtype=torch.float32) # B, T, C, H, W
|
| 187 |
+
invar = invar.permute(0, 2, 1, 3, 4) # B, C, T, H, W
|
| 188 |
+
outvar = data[1].to(local_rank, dtype=torch.float32)
|
| 189 |
+
for t in range(outvar.shape[1]):
|
| 190 |
+
if t < outvar.shape[1] - 1:
|
| 191 |
+
with torch.no_grad():
|
| 192 |
+
outvar_pred = model(invar)
|
| 193 |
+
# B, 70, 2, 721, 1440
|
| 194 |
+
invar[:, :, 0] = invar[:, :, -1]
|
| 195 |
+
invar[:, :, -1] = outvar_pred.detach()
|
| 196 |
+
else:
|
| 197 |
+
with replace_function(model, ["cube_embedding", "u_transformer"], cfg.world_size > 1):
|
| 198 |
+
outvar_pred = model(invar)
|
| 199 |
+
loss = loss_obj(outvar_pred, outvar[:, t])
|
| 200 |
+
optimizer.zero_grad()
|
| 201 |
+
loss.backward()
|
| 202 |
+
optimizer.step()
|
| 203 |
+
train_loss += loss.item()
|
| 204 |
+
if world_rank == 0:
|
| 205 |
+
logger.info(f'Train: Epoch {epoch}-{j+1}/{len(train_dataloader)} '
|
| 206 |
+
f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
|
| 207 |
+
f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
|
| 208 |
+
f'loss:{train_loss / (j+1): .04f}')
|
| 209 |
+
|
| 210 |
+
train_loss /= len(train_dataloader)
|
| 211 |
+
|
| 212 |
+
model.eval()
|
| 213 |
+
valid_loss = 0
|
| 214 |
+
with torch.no_grad():
|
| 215 |
+
start_time = time.time()
|
| 216 |
+
for j, data in enumerate(val_dataloader):
|
| 217 |
+
invar = data[0].to(local_rank, dtype=torch.float32) # B, T, C, H, W
|
| 218 |
+
invar = invar.permute(0, 2, 1, 3, 4) # B, C, T, H, W
|
| 219 |
+
outvar = data[1].to(local_rank, dtype=torch.float32)
|
| 220 |
+
for t in range(outvar.shape[1]):
|
| 221 |
+
outvar_pred = model(invar)
|
| 222 |
+
# B, 70, 2, 721, 1440
|
| 223 |
+
invar[:, :, 0] = invar[:, :, -1]
|
| 224 |
+
invar[:, :, -1] = outvar_pred.detach()
|
| 225 |
+
loss = loss_obj(outvar_pred, outvar[:, -1])
|
| 226 |
+
|
| 227 |
+
if cfg.world_size > 1:
|
| 228 |
+
loss_tensor = loss.detach().to(local_rank)
|
| 229 |
+
dist.all_reduce(loss_tensor)
|
| 230 |
+
loss = loss_tensor.item() / cfg.world_size
|
| 231 |
+
valid_loss += loss
|
| 232 |
+
else:
|
| 233 |
+
valid_loss += loss.item()
|
| 234 |
+
if world_rank == 0:
|
| 235 |
+
logger.info(f'Valid: Epoch {epoch}-{j+1}/{len(val_dataloader)} '
|
| 236 |
+
f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
|
| 237 |
+
f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
|
| 238 |
+
f'loss:{valid_loss / (j+1): .04f}')
|
| 239 |
+
|
| 240 |
+
valid_loss /= len(val_dataloader)
|
| 241 |
+
is_save_ckp = False
|
| 242 |
+
if valid_loss < best_valid_loss:
|
| 243 |
+
best_valid_loss = valid_loss
|
| 244 |
+
best_loss_epoch = epoch
|
| 245 |
+
world_rank == 0 and save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, cfg.checkpoint_dir, epoch)
|
| 246 |
+
is_save_ckp = True
|
| 247 |
+
|
| 248 |
+
scheduler.step(valid_loss)
|
| 249 |
+
|
| 250 |
+
if world_rank == 0:
|
| 251 |
+
logger.info(f"Epoch [{epoch + 1}/{cfg.max_epoch}], "
|
| 252 |
+
f"Train Loss: {train_loss:.4f}, "
|
| 253 |
+
f"Valid Loss: {valid_loss:.4f}, "
|
| 254 |
+
f"Best loss at Epoch: {best_loss_epoch + 1}"
|
| 255 |
+
+ (", saving checkpoint" if is_save_ckp else "")
|
| 256 |
+
)
|
| 257 |
+
train_losses = np.append(train_losses, train_loss)
|
| 258 |
+
valid_losses = np.append(valid_losses, valid_loss)
|
| 259 |
+
|
| 260 |
+
np.save(train_loss_file, train_losses)
|
| 261 |
+
np.save(valid_loss_file, valid_losses)
|
| 262 |
+
|
| 263 |
+
if epoch - best_loss_epoch > cfg.patience:
|
| 264 |
+
print(f"Loss has not decrease in {cfg.patience} epochs, stopping training...")
|
| 265 |
+
exit()
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, model_path, epoch):
|
| 269 |
+
model_to_save = model.module if hasattr(model, "module") else model
|
| 270 |
+
state = {"model_state_dict": model_to_save.state_dict(),
|
| 271 |
+
"optimizer_state_dict": optimizer.state_dict(),
|
| 272 |
+
"scheduler_state_dict": scheduler.state_dict(),
|
| 273 |
+
"best_valid_loss": best_valid_loss,
|
| 274 |
+
"best_loss_epoch": best_loss_epoch,
|
| 275 |
+
"current_epoch": epoch
|
| 276 |
+
}
|
| 277 |
+
torch.save(state, f"{model_path}/model_medium.pth")
|
| 278 |
+
### the weight file saving may interrupted due to DCU queue limit, get a backup to ensure there at least has one model
|
| 279 |
+
os.system(f"mv {model_path}/model_medium.pth {model_path}/model_medium_bak.pth")
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
if __name__ == "__main__":
|
| 283 |
+
current_path = os.getcwd()
|
| 284 |
+
sys.path.append(current_path)
|
| 285 |
+
main()
|
scripts/train_short.py
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 tqdm import tqdm
|
| 15 |
+
from torch.nn.parallel import DistributedDataParallel
|
| 16 |
+
from model.fuxi import Fuxi
|
| 17 |
+
from onescience.datapipes.climate import ERA5Datapipe
|
| 18 |
+
from onescience.utils.YParams import YParams
|
| 19 |
+
from onescience.metrics.climate.loss import LatitudeWeightedLoss
|
| 20 |
+
from onescience.memory.checkpoint import replace_function
|
| 21 |
+
|
| 22 |
+
from apex import optimizers
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def main():
|
| 26 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
| 27 |
+
logger = logging.getLogger()
|
| 28 |
+
|
| 29 |
+
## Model config init
|
| 30 |
+
config_file_path = os.path.join(current_path, "conf/config.yaml")
|
| 31 |
+
cfg = YParams(config_file_path, "model")
|
| 32 |
+
|
| 33 |
+
## Distributed config init
|
| 34 |
+
cfg.world_size = 1
|
| 35 |
+
if "WORLD_SIZE" in os.environ:
|
| 36 |
+
cfg.world_size = int(os.environ["WORLD_SIZE"])
|
| 37 |
+
world_rank = 0
|
| 38 |
+
local_rank = 0
|
| 39 |
+
if cfg.world_size > 1:
|
| 40 |
+
dist.init_process_group(backend="nccl", init_method="env://")
|
| 41 |
+
local_rank = int(os.environ["LOCAL_RANK"])
|
| 42 |
+
world_rank = dist.get_rank()
|
| 43 |
+
|
| 44 |
+
## DataLoader init
|
| 45 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 46 |
+
datapipe = ERA5Datapipe(
|
| 47 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 48 |
+
used_variables=cfg_data.dataset.channels,
|
| 49 |
+
used_years=cfg_data.dataset.train_time,
|
| 50 |
+
distributed=dist.is_initialized(),
|
| 51 |
+
output_steps=2,
|
| 52 |
+
input_steps=2,
|
| 53 |
+
batch_size=cfg_data.dataloader.batch_size,
|
| 54 |
+
num_workers=cfg_data.dataloader.num_workers
|
| 55 |
+
)
|
| 56 |
+
train_dataloader, train_sampler = datapipe.get_dataloader("train")
|
| 57 |
+
datapipe = ERA5Datapipe(
|
| 58 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 59 |
+
used_variables=cfg_data.dataset.channels,
|
| 60 |
+
used_years=cfg_data.dataset.val_time,
|
| 61 |
+
distributed=dist.is_initialized(),
|
| 62 |
+
output_steps=2,
|
| 63 |
+
input_steps=2,
|
| 64 |
+
batch_size=cfg_data.dataloader.batch_size,
|
| 65 |
+
num_workers=cfg_data.dataloader.num_workers
|
| 66 |
+
)
|
| 67 |
+
val_dataloader, val_sampler = datapipe.get_dataloader("valid")
|
| 68 |
+
|
| 69 |
+
## Model init
|
| 70 |
+
model = Fuxi(img_size=cfg_data.dataset.img_size,
|
| 71 |
+
patch_size=cfg.patch_size,
|
| 72 |
+
in_chans=len(cfg_data.dataset.channels),
|
| 73 |
+
out_chans=len(cfg_data.dataset.channels),
|
| 74 |
+
embed_dim=cfg.embed_dim,
|
| 75 |
+
num_groups=cfg.num_groups,
|
| 76 |
+
num_heads=cfg.num_heads,
|
| 77 |
+
window_size=cfg.window_size
|
| 78 |
+
).to(local_rank)
|
| 79 |
+
optimizer = optimizers.FusedAdam(model.parameters(), lr=cfg.train_lr)
|
| 80 |
+
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, factor=0.2, patience=5, mode="min")
|
| 81 |
+
loss_obj = LatitudeWeightedLoss(loss_type="l1", normalize=True).to(local_rank)
|
| 82 |
+
|
| 83 |
+
## Train process init
|
| 84 |
+
os.makedirs(cfg.checkpoint_dir, exist_ok=True)
|
| 85 |
+
train_loss_file = f"{cfg.checkpoint_dir}/tr_short_loss.npy"
|
| 86 |
+
valid_loss_file = f"{cfg.checkpoint_dir}/va_short_loss.npy"
|
| 87 |
+
best_valid_loss = 1.0e6
|
| 88 |
+
best_loss_epoch = 0
|
| 89 |
+
train_losses = np.empty((0,), dtype=np.float32)
|
| 90 |
+
valid_losses = np.empty((0,), dtype=np.float32)
|
| 91 |
+
current_epoch = 0
|
| 92 |
+
|
| 93 |
+
## Get model params count
|
| 94 |
+
if cfg.world_size == 1:
|
| 95 |
+
total_params = sum(p.numel() for p in model.parameters())
|
| 96 |
+
print("\n\n")
|
| 97 |
+
print("-" * 50)
|
| 98 |
+
print(f"📂 now params is {total_params}, {total_params / 1e6:.2f}M, {total_params / 1e9:.2f}B")
|
| 99 |
+
print("-" * 50, "\n")
|
| 100 |
+
|
| 101 |
+
## Load model weight if there exist well-trained model
|
| 102 |
+
if os.path.exists(f"{cfg.checkpoint_dir}/model_short_bak.pth"):
|
| 103 |
+
if world_rank == 0:
|
| 104 |
+
print("\n\n")
|
| 105 |
+
print("-" * 50)
|
| 106 |
+
print(f"✅ There has a short-pattern model weight, load and continue training...")
|
| 107 |
+
print(f'If you want to finetune a new model, ensure there is no model_short_bak.pth file in {cfg.checkpoint_dir}')
|
| 108 |
+
print("-" * 50, "\n")
|
| 109 |
+
ckpt = torch.load(f"{cfg.checkpoint_dir}/model_short_bak.pth", map_location=f'cuda:{local_rank}', weights_only=False)
|
| 110 |
+
model.load_state_dict(ckpt["model_state_dict"])
|
| 111 |
+
optimizer.load_state_dict(ckpt["optimizer_state_dict"])
|
| 112 |
+
scheduler.load_state_dict(ckpt["scheduler_state_dict"])
|
| 113 |
+
best_valid_loss = ckpt["best_valid_loss"]
|
| 114 |
+
best_loss_epoch = ckpt["best_loss_epoch"]
|
| 115 |
+
current_epoch = ckpt["current_epoch"]
|
| 116 |
+
train_losses = np.load(f"{cfg.checkpoint_dir}/tr_short_loss.npy")
|
| 117 |
+
valid_losses = np.load(f"{cfg.checkpoint_dir}/va_short_loss.npy")
|
| 118 |
+
else:
|
| 119 |
+
if world_rank == 0:
|
| 120 |
+
print("\n\n")
|
| 121 |
+
print("-" * 50)
|
| 122 |
+
print(f"✅ No checkpoint found, training short model from scratch...")
|
| 123 |
+
print("-" * 50, "\n")
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
## Distributed model
|
| 127 |
+
if cfg.world_size > 1:
|
| 128 |
+
model = DistributedDataParallel(model, device_ids=[local_rank], output_device=local_rank, find_unused_parameters=True)
|
| 129 |
+
|
| 130 |
+
world_rank == 0 and logger.info(f"start training ...")
|
| 131 |
+
|
| 132 |
+
for epoch in range(current_epoch, cfg.finetune_step):
|
| 133 |
+
if epoch > cfg.step_change_freq:
|
| 134 |
+
num_rollout_steps = epoch // cfg.step_change_freq + 2
|
| 135 |
+
if num_rollout_steps > 12: # Paper: 2~12 curriculum training schedule, then skip to 20.
|
| 136 |
+
num_rollout_steps = cfg.short_num_steps
|
| 137 |
+
if epoch % cfg.step_change_freq == 0 and world_rank == 0:
|
| 138 |
+
logger.info(f"⚠️ ⚠️ Switching to {num_rollout_steps}-step rollout!")
|
| 139 |
+
datapipe = ERA5Datapipe(
|
| 140 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 141 |
+
used_variables=cfg_data.dataset.channels,
|
| 142 |
+
used_years=cfg_data.dataset.train_time,
|
| 143 |
+
distributed=dist.is_initialized(),
|
| 144 |
+
output_steps=num_rollout_steps,
|
| 145 |
+
input_steps=2,
|
| 146 |
+
batch_size=cfg_data.dataloader.batch_size,
|
| 147 |
+
num_workers=cfg_data.dataloader.num_workers
|
| 148 |
+
)
|
| 149 |
+
train_dataloader, train_sampler = datapipe.get_dataloader("train")
|
| 150 |
+
datapipe = ERA5Datapipe(
|
| 151 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 152 |
+
used_variables=cfg_data.dataset.channels,
|
| 153 |
+
used_years=cfg_data.dataset.val_time,
|
| 154 |
+
distributed=dist.is_initialized(),
|
| 155 |
+
output_steps=num_rollout_steps,
|
| 156 |
+
input_steps=2,
|
| 157 |
+
batch_size=cfg_data.dataloader.batch_size,
|
| 158 |
+
num_workers=cfg_data.dataloader.num_workers
|
| 159 |
+
)
|
| 160 |
+
val_dataloader, val_sampler = datapipe.get_dataloader("valid")
|
| 161 |
+
|
| 162 |
+
if dist.is_initialized():
|
| 163 |
+
train_sampler.set_epoch(epoch)
|
| 164 |
+
val_sampler.set_epoch(epoch)
|
| 165 |
+
model.train()
|
| 166 |
+
train_loss = 0
|
| 167 |
+
start_time = time.time()
|
| 168 |
+
for j, data in enumerate(train_dataloader):
|
| 169 |
+
invar = data[0].to(local_rank, dtype=torch.float32) # B, T, C, H, W
|
| 170 |
+
invar = invar.permute(0, 2, 1, 3, 4) # B, C, T, H, W
|
| 171 |
+
outvar = data[1].to(local_rank, dtype=torch.float32)
|
| 172 |
+
for t in range(outvar.shape[1]):
|
| 173 |
+
if t < outvar.shape[1] - 1:
|
| 174 |
+
with torch.no_grad():
|
| 175 |
+
outvar_pred = model(invar)
|
| 176 |
+
# B, 70, 2, 721, 1440
|
| 177 |
+
invar[:, :, 0] = invar[:, :, -1]
|
| 178 |
+
invar[:, :, -1] = outvar_pred.detach()
|
| 179 |
+
else:
|
| 180 |
+
with replace_function(model, ["cube_embedding", "u_transformer"], cfg.world_size > 1):
|
| 181 |
+
outvar_pred = model(invar)
|
| 182 |
+
loss = loss_obj(outvar_pred, outvar[:, t])
|
| 183 |
+
|
| 184 |
+
optimizer.zero_grad()
|
| 185 |
+
loss.backward()
|
| 186 |
+
optimizer.step()
|
| 187 |
+
train_loss += loss.item()
|
| 188 |
+
if world_rank == 0:
|
| 189 |
+
logger.info(f'Train: Epoch {epoch}-{j+1}/{len(train_dataloader)} '
|
| 190 |
+
f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
|
| 191 |
+
f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
|
| 192 |
+
f'loss:{train_loss / (j+1): .04f}')
|
| 193 |
+
train_loss /= len(train_dataloader)
|
| 194 |
+
|
| 195 |
+
model.eval()
|
| 196 |
+
valid_loss = 0
|
| 197 |
+
with torch.no_grad():
|
| 198 |
+
start_time = time.time()
|
| 199 |
+
for j, data in enumerate(val_dataloader):
|
| 200 |
+
invar = data[0].to(local_rank, dtype=torch.float32) # B, T, C, H, W
|
| 201 |
+
invar = invar.permute(0, 2, 1, 3, 4) # B, C, T, H, W
|
| 202 |
+
outvar = data[1].to(local_rank, dtype=torch.float32)
|
| 203 |
+
for t in range(outvar.shape[1]):
|
| 204 |
+
outvar_pred = model(invar)
|
| 205 |
+
# B, 70, 2, 721, 1440
|
| 206 |
+
invar[:, :, 0] = invar[:, :, -1]
|
| 207 |
+
invar[:, :, -1] = outvar_pred.detach()
|
| 208 |
+
loss = loss_obj(outvar_pred, outvar[:, -1])
|
| 209 |
+
|
| 210 |
+
if cfg.world_size > 1:
|
| 211 |
+
loss_tensor = loss.detach().to(local_rank)
|
| 212 |
+
dist.all_reduce(loss_tensor)
|
| 213 |
+
loss = loss_tensor.item() / cfg.world_size
|
| 214 |
+
valid_loss += loss
|
| 215 |
+
else:
|
| 216 |
+
valid_loss += loss.item()
|
| 217 |
+
if world_rank == 0:
|
| 218 |
+
logger.info(f'Valid: Epoch {epoch}-{j+1}/{len(val_dataloader)} '
|
| 219 |
+
f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
|
| 220 |
+
f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
|
| 221 |
+
f'loss:{valid_loss / (j+1): .04f}')
|
| 222 |
+
valid_loss /= len(val_dataloader)
|
| 223 |
+
is_save_ckp = False
|
| 224 |
+
if valid_loss < best_valid_loss:
|
| 225 |
+
best_valid_loss = valid_loss
|
| 226 |
+
best_loss_epoch = epoch
|
| 227 |
+
world_rank == 0 and save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, cfg.checkpoint_dir, epoch)
|
| 228 |
+
is_save_ckp = True
|
| 229 |
+
|
| 230 |
+
scheduler.step(valid_loss)
|
| 231 |
+
|
| 232 |
+
if world_rank == 0:
|
| 233 |
+
logger.info(f"Epoch [{epoch + 1}/{cfg.max_epoch}], "
|
| 234 |
+
f"Train Loss: {train_loss:.4f}, "
|
| 235 |
+
f"Valid Loss: {valid_loss:.4f}, "
|
| 236 |
+
f"Best loss at Epoch: {best_loss_epoch + 1}"
|
| 237 |
+
+ (", saving checkpoint" if is_save_ckp else "")
|
| 238 |
+
)
|
| 239 |
+
train_losses = np.append(train_losses, train_loss)
|
| 240 |
+
valid_losses = np.append(valid_losses, valid_loss)
|
| 241 |
+
|
| 242 |
+
np.save(train_loss_file, train_losses)
|
| 243 |
+
np.save(valid_loss_file, valid_losses)
|
| 244 |
+
|
| 245 |
+
if epoch - best_loss_epoch > cfg.patience:
|
| 246 |
+
print(f"Loss has not decrease in {cfg.patience} epochs, stopping training...")
|
| 247 |
+
exit()
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, model_path, epoch):
|
| 251 |
+
model_to_save = model.module if hasattr(model, "module") else model
|
| 252 |
+
state = {"model_state_dict": model_to_save.state_dict(),
|
| 253 |
+
"optimizer_state_dict": optimizer.state_dict(),
|
| 254 |
+
"scheduler_state_dict": scheduler.state_dict(),
|
| 255 |
+
"best_valid_loss": best_valid_loss,
|
| 256 |
+
"best_loss_epoch": best_loss_epoch,
|
| 257 |
+
"current_epoch": epoch
|
| 258 |
+
}
|
| 259 |
+
torch.save(state, f"{model_path}/model_short.pth")
|
| 260 |
+
### the weight file saving may interrupted due to DCU queue limit, get a backup to ensure there at least has one model
|
| 261 |
+
os.system(f"mv {model_path}/model_short.pth {model_path}/model_short_bak.pth")
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
if __name__ == "__main__":
|
| 265 |
+
current_path = os.getcwd()
|
| 266 |
+
sys.path.append(current_path)
|
| 267 |
+
main()
|
weight/.gitkeep
ADDED
|
File without changes
|