Upload folder using huggingface_hub
Browse files- README.md +158 -0
- conf/config.yaml +141 -0
- configuration.json +1 -0
- model/xihe.py +180 -0
- scripts/fake_data.py +88 -0
- scripts/inference.py +75 -0
- scripts/result.py +208 -0
- scripts/train.py +221 -0
- weight/.gitkeep +1 -0
README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
- zh
|
| 6 |
+
tags:
|
| 7 |
+
- OneScience
|
| 8 |
+
- Earth Science
|
| 9 |
+
- Ocean Forecast
|
| 10 |
+
- Subsurface Forecast
|
| 11 |
+
- Sea Temperature and Salinity Forecast
|
| 12 |
+
- CMEMS
|
| 13 |
+
frameworks: PyTorch
|
| 14 |
+
datasets:
|
| 15 |
+
- OneScience/CMEMS
|
| 16 |
+
---
|
| 17 |
+
<p align="center">
|
| 18 |
+
<strong>
|
| 19 |
+
<span style="font-size: 30px;">XiHe</span>
|
| 20 |
+
</strong>
|
| 21 |
+
</p>
|
| 22 |
+
|
| 23 |
+
# Model Introduction
|
| 24 |
+
|
| 25 |
+
XiHe is the world's first data-driven, eddy-resolving global ocean forecast model, jointly proposed by the College of Meteorology and Oceanography at the National University of Defense Technology and multiple universities and research institutions.
|
| 26 |
+
|
| 27 |
+
Paper: XiHe: A Data-Driven Model for Global Ocean Eddy-Resolving Forecasting
|
| 28 |
+
|
| 29 |
+
https://arxiv.org/abs/2402.02995
|
| 30 |
+
|
| 31 |
+
# Model Description
|
| 32 |
+
|
| 33 |
+
XiHe is a Transformer model designed for high-resolution global ocean forecasting, suitable for global ocean eddy-resolving forecast research.
|
| 34 |
+
|
| 35 |
+
# Use Cases
|
| 36 |
+
|
| 37 |
+
| Scenario | Description |
|
| 38 |
+
| :---: | :--- |
|
| 39 |
+
| Global Ocean Forecast Research | Train an XiHe-style ocean forecast model using annual CMEMS HDF5 data. |
|
| 40 |
+
| Local Quick Validation | Use synthetic data to verify data loading, training entry points, inference, and result scripts. |
|
| 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/XiHe --local_dir ./XiHe
|
| 64 |
+
cd XiHe
|
| 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 CMEMS 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 `config/config.yaml` is set correctly:
|
| 91 |
+
|
| 92 |
+
```bash
|
| 93 |
+
modelscope download --dataset OneScience/CMEMS --local_dir ./data
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
### Training
|
| 97 |
+
|
| 98 |
+
Single GPU:
|
| 99 |
+
|
| 100 |
+
```bash
|
| 101 |
+
python scripts/train.py
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
Multi-GPU:
|
| 105 |
+
|
| 106 |
+
```bash
|
| 107 |
+
torchrun --nproc_per_node=8 --nnodes=1 --rdzv_id=1000 --rdzv_backend=c10d --max_restarts=0 --master_addr="localhost" --master_port=29500 scripts/train.py
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
Training outputs:
|
| 111 |
+
|
| 112 |
+
```text
|
| 113 |
+
data/checkpoints/model_bak.pth
|
| 114 |
+
data/checkpoints/trloss.npy
|
| 115 |
+
data/checkpoints/valoss.npy
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
### Training Weights
|
| 119 |
+
|
| 120 |
+
This repository provides weights trained on GLORYS12 global ocean reanalysis data in the `weights/` folder. The weight files will be uploaded soon and are expected to be available in the near future.
|
| 121 |
+
|
| 122 |
+
### Inference
|
| 123 |
+
|
| 124 |
+
Inference reads `data/checkpoints/model_bak.pth` by default:
|
| 125 |
+
|
| 126 |
+
```bash
|
| 127 |
+
python scripts/inference.py
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
Prediction results are output to:
|
| 131 |
+
|
| 132 |
+
```text
|
| 133 |
+
result/output/
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
### Evaluation and Visualization
|
| 137 |
+
|
| 138 |
+
```bash
|
| 139 |
+
python scripts/result.py
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
Output contents include:
|
| 143 |
+
|
| 144 |
+
- `result/rmse.npy`
|
| 145 |
+
- `result/acc.npy`
|
| 146 |
+
- `result/loss.png`
|
| 147 |
+
- Forecast comparison plots for specified dates and variables
|
| 148 |
+
|
| 149 |
+
# OneScience Official Information
|
| 150 |
+
|
| 151 |
+
| Platform | OneScience Main Repository | Skills Repository |
|
| 152 |
+
| --- | --- | --- |
|
| 153 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 154 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 155 |
+
|
| 156 |
+
# Citation & License
|
| 157 |
+
|
| 158 |
+
- This repository is a reproduction of the original XiHe paper.
|
conf/config.yaml
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ERA5数据集配置示例
|
| 2 |
+
model:
|
| 3 |
+
start_epoch: 0
|
| 4 |
+
max_epoch: 9999
|
| 5 |
+
lr: 5E-5 #学习率
|
| 6 |
+
|
| 7 |
+
betas: [0.9, 0.95]
|
| 8 |
+
weight_decay: 0.01
|
| 9 |
+
|
| 10 |
+
embed_dim: 192
|
| 11 |
+
num_blocks: 8
|
| 12 |
+
patch_size: [6, 12]
|
| 13 |
+
patience: 50
|
| 14 |
+
checkpoint_dir: "./data/checkpoints" #相对路径
|
| 15 |
+
|
| 16 |
+
in_chans: 96
|
| 17 |
+
out_chans: 96
|
| 18 |
+
num_groups: 64
|
| 19 |
+
depth: 2
|
| 20 |
+
num_heads: [6, 12, 12, 6]
|
| 21 |
+
img_size: [2041, 4320]
|
| 22 |
+
mask: "./data/land_mask.npy"
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
datapipe:
|
| 27 |
+
name: "CMEMS"
|
| 28 |
+
task: "ocean_forecasting"
|
| 29 |
+
|
| 30 |
+
dataset:
|
| 31 |
+
type: "hdf5"
|
| 32 |
+
stats_dir: './data/stats/' # "$ONESCIENCE_DATASETS_DIR/ERA5/newh5/stats/"
|
| 33 |
+
static_dir: './data/static/' # "$ONESCIENCE_DATASETS_DIR/ERA5/newh5/static/"
|
| 34 |
+
data_dir: './data/' # "$ONESCIENCE_DATASETS_DIR/ERA5/newh5/"
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
train_time: [2010]
|
| 38 |
+
val_time: [2013]
|
| 39 |
+
test_time: [2014]
|
| 40 |
+
img_size: [2041, 4320]
|
| 41 |
+
verbose: true
|
| 42 |
+
cache: false
|
| 43 |
+
|
| 44 |
+
# 气象变量
|
| 45 |
+
|
| 46 |
+
channels: ["sea_surface_height_above_geoid","10m_u_component_of_wind","10m_v_component_of_wind", "sea_surface_temperature",
|
| 47 |
+
"sea_water_potential_temperature_1",sea_water_salinity_1,"eastward_sea_water_velocity_1","northward_sea_water_velocity_1",
|
| 48 |
+
"sea_water_potential_temperature_2",sea_water_salinity_2,"eastward_sea_water_velocity_2","northward_sea_water_velocity_2",
|
| 49 |
+
"sea_water_potential_temperature_3",sea_water_salinity_3,"eastward_sea_water_velocity_3","northward_sea_water_velocity_3",
|
| 50 |
+
"sea_water_potential_temperature_4",sea_water_salinity_4,"eastward_sea_water_velocity_4","northward_sea_water_velocity_4",
|
| 51 |
+
"sea_water_potential_temperature_5",sea_water_salinity_5,"eastward_sea_water_velocity_5","northward_sea_water_velocity_5",
|
| 52 |
+
"sea_water_potential_temperature_6",sea_water_salinity_6,"eastward_sea_water_velocity_6","northward_sea_water_velocity_6",
|
| 53 |
+
"sea_water_potential_temperature_7",sea_water_salinity_7,"eastward_sea_water_velocity_7","northward_sea_water_velocity_7",
|
| 54 |
+
"sea_water_potential_temperature_8",sea_water_salinity_8,"eastward_sea_water_velocity_8","northward_sea_water_velocity_8",
|
| 55 |
+
"sea_water_potential_temperature_9",sea_water_salinity_9,"eastward_sea_water_velocity_9","northward_sea_water_velocity_9",
|
| 56 |
+
"sea_water_potential_temperature_10",sea_water_salinity_10,"eastward_sea_water_velocity_10","northward_sea_water_velocity_10",
|
| 57 |
+
"sea_water_potential_temperature_11",sea_water_salinity_11,"eastward_sea_water_velocity_11","northward_sea_water_velocity_11",
|
| 58 |
+
"sea_water_potential_temperature_12",sea_water_salinity_12,"eastward_sea_water_velocity_12","northward_sea_water_velocity_12",
|
| 59 |
+
# "sea_surface_height_above_geoid","10m_u_component_of_wind","10m_v_component_of_wind", "sea_surface_temperature",
|
| 60 |
+
"sea_water_potential_temperature_13",sea_water_salinity_13,"eastward_sea_water_velocity_13","northward_sea_water_velocity_13",
|
| 61 |
+
"sea_water_potential_temperature_14",sea_water_salinity_14,"eastward_sea_water_velocity_14","northward_sea_water_velocity_14",
|
| 62 |
+
"sea_water_potential_temperature_15",sea_water_salinity_15,"eastward_sea_water_velocity_15","northward_sea_water_velocity_15",
|
| 63 |
+
"sea_water_potential_temperature_16",sea_water_salinity_16,"eastward_sea_water_velocity_16","northward_sea_water_velocity_16",
|
| 64 |
+
"sea_water_potential_temperature_17",sea_water_salinity_17,"eastward_sea_water_velocity_17","northward_sea_water_velocity_17",
|
| 65 |
+
"sea_water_potential_temperature_18",sea_water_salinity_18,"eastward_sea_water_velocity_18","northward_sea_water_velocity_18",
|
| 66 |
+
"sea_water_potential_temperature_19",sea_water_salinity_19,"eastward_sea_water_velocity_19","northward_sea_water_velocity_19",
|
| 67 |
+
"sea_water_potential_temperature_20",sea_water_salinity_20,"eastward_sea_water_velocity_20","northward_sea_water_velocity_20",
|
| 68 |
+
"sea_water_potential_temperature_21",sea_water_salinity_21,"eastward_sea_water_velocity_21","northward_sea_water_velocity_21",
|
| 69 |
+
"sea_water_potential_temperature_22",sea_water_salinity_22,"eastward_sea_water_velocity_22","northward_sea_water_velocity_22",
|
| 70 |
+
"sea_water_potential_temperature_23",sea_water_salinity_23,"eastward_sea_water_velocity_23","northward_sea_water_velocity_23",
|
| 71 |
+
]
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
variables:
|
| 78 |
+
- "u10" # 10m U wind component
|
| 79 |
+
- "v10" # 10m V wind component
|
| 80 |
+
- "uo" # 'uo': 'eastward_sea_water_velocity',
|
| 81 |
+
- "vo" # 'vo': 'northward_sea_water_velocity',
|
| 82 |
+
- "so" # 'so': 'sea_water_salinity',
|
| 83 |
+
- "thetao" # 'thetao': 'sea_water_potential_temperature',
|
| 84 |
+
- "ssh" # 'zos': 'sea_surface_height_above_geoid'
|
| 85 |
+
- "sst" # 'analysed_sst': 'sea_surface_temperature',
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
# 时间配置
|
| 89 |
+
time_range: ["2010-01-01", "2014-12-31"]
|
| 90 |
+
time_steps: 1
|
| 91 |
+
time_res: 24
|
| 92 |
+
|
| 93 |
+
# 空间配置
|
| 94 |
+
spatial_resolution: [1/12, 1/12]
|
| 95 |
+
|
| 96 |
+
# 采样配置
|
| 97 |
+
num_samples: -1 # -1 表示使用全部数据
|
| 98 |
+
shuffle: true
|
| 99 |
+
random_seed: 42
|
| 100 |
+
|
| 101 |
+
# 领域特定配置
|
| 102 |
+
extra:
|
| 103 |
+
levels: [0.495, 2.65, 5.08, 7.93, 11.41, 15.81, 21.60, 29.44,40.34, 55.76, 77.85, 92.32, 109.73, 130.67, 155.85,186.13, 222.48, 266.04, 318.13, 380.21, 453.94,541.09, 643.57]
|
| 104 |
+
lat_range: [-90, 90]
|
| 105 |
+
lon_range: [0, 360]
|
| 106 |
+
|
| 107 |
+
# 数据转换配置
|
| 108 |
+
transforms:
|
| 109 |
+
- type: "Normalize"
|
| 110 |
+
params:
|
| 111 |
+
mean: [0.0, 0.0, 288.0, 101325.0, 50000.0, 270.0]
|
| 112 |
+
std: [5.0, 5.0, 15.0, 1000.0, 5000.0, 10.0]
|
| 113 |
+
keys: ["input", "target"]
|
| 114 |
+
|
| 115 |
+
- type: "ToTensor"
|
| 116 |
+
params:
|
| 117 |
+
keys: null # null表示转换所有numpy数组
|
| 118 |
+
|
| 119 |
+
# 其他配置
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
# DataLoader配置
|
| 123 |
+
dataloader:
|
| 124 |
+
mask_dtype: "float32"
|
| 125 |
+
batch_size: 1
|
| 126 |
+
num_workers: 0
|
| 127 |
+
pin_memory: false
|
| 128 |
+
drop_last: true
|
| 129 |
+
shuffle: false # 使用sampler时设为false
|
| 130 |
+
# prefetch_factor: 2
|
| 131 |
+
persistent_workers: false
|
| 132 |
+
|
| 133 |
+
# 分布式配置
|
| 134 |
+
distributed:
|
| 135 |
+
enabled: false #开关
|
| 136 |
+
sampler: "DistributedSampler"
|
| 137 |
+
rank: 0
|
| 138 |
+
world_size: 8
|
| 139 |
+
shuffle: true
|
| 140 |
+
seed: 42
|
| 141 |
+
drop_last: true
|
configuration.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"framework":"Pytorch","task":"other"}
|
model/xihe.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import os
|
| 3 |
+
import torch
|
| 4 |
+
import sys
|
| 5 |
+
import numpy as np
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
from onescience.utils.YParams import YParams
|
| 9 |
+
from onescience.models.meta import ModelMetaData
|
| 10 |
+
from onescience.modules.embedding.xiheembedding import XiheEmbedding
|
| 11 |
+
from onescience.modules.fuser.xihefuse import XiheFuser
|
| 12 |
+
from onescience.modules.recovery.xihepatchrecovery import XihePatchRecovery
|
| 13 |
+
from onescience.modules.sample.pangudownsample import PanguDownSample
|
| 14 |
+
from onescience.modules.sample.xiheupsample import XiheUpSample
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@dataclass
|
| 19 |
+
class MetaData(ModelMetaData):
|
| 20 |
+
name: str = "Xihe"
|
| 21 |
+
# Optimization
|
| 22 |
+
jit: bool = False # ONNX Ops Conflict
|
| 23 |
+
cuda_graphs: bool = True
|
| 24 |
+
amp: bool = True
|
| 25 |
+
|
| 26 |
+
# Inference
|
| 27 |
+
onnx_cpu: bool = False # No FFT op on CPU
|
| 28 |
+
onnx_gpu: bool = True
|
| 29 |
+
onnx_runtime: bool = True
|
| 30 |
+
|
| 31 |
+
# Physics informed
|
| 32 |
+
var_dim: int = 1
|
| 33 |
+
func_torch: bool = False
|
| 34 |
+
auto_grad: bool = False
|
| 35 |
+
|
| 36 |
+
class TensorWithMask:
|
| 37 |
+
def __init__(self, x, mask):
|
| 38 |
+
self.x = x
|
| 39 |
+
self.mask = mask
|
| 40 |
+
self.y=None
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class Xihe(nn.Module):
|
| 44 |
+
"""
|
| 45 |
+
Xihe A PyTorch impl of: `XiHe: A Data-Driven Model for Global Ocean Eddy-Resolving Forecasting`
|
| 46 |
+
https://arxiv.org/abs/2402.02995
|
| 47 |
+
"""
|
| 48 |
+
def __init__(
|
| 49 |
+
self,
|
| 50 |
+
config,
|
| 51 |
+
img_size=(2041, 4320),
|
| 52 |
+
patch_size=(6, 12),
|
| 53 |
+
window_size=(6, 12),
|
| 54 |
+
embed_dim=192,
|
| 55 |
+
num_heads=(6, 12, 12, 6),
|
| 56 |
+
in_chans=96,
|
| 57 |
+
depth=1,
|
| 58 |
+
mask_full=None,
|
| 59 |
+
out_chans=94,
|
| 60 |
+
num_groups=32,
|
| 61 |
+
|
| 62 |
+
):
|
| 63 |
+
super().__init__()
|
| 64 |
+
self.img_size = config.img_size
|
| 65 |
+
self.patch_size =config.patch_size
|
| 66 |
+
# 正确初始化 mask_full
|
| 67 |
+
self.mask=config.mask
|
| 68 |
+
mask_full = np.load(self.mask)
|
| 69 |
+
self.mask_full = mask_full if mask_full is not None else None
|
| 70 |
+
self.mask_h_w=None
|
| 71 |
+
self.out_chans=config.out_chans
|
| 72 |
+
self.in_chans=config.in_chans
|
| 73 |
+
self.num_groups=config.num_groups
|
| 74 |
+
self.embed_dim=config.embed_dim
|
| 75 |
+
|
| 76 |
+
self.skip_proj = nn.Linear(2*self.embed_dim, self.embed_dim)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
self.patchembed2d = XiheEmbedding()
|
| 80 |
+
self.patchrecovery2d = XihePatchRecovery()
|
| 81 |
+
|
| 82 |
+
# patch 后的 3D 分辨率: (Pl=1, Lat_out, Lon_out)
|
| 83 |
+
H_out = math.ceil(img_size[0] / patch_size[0])
|
| 84 |
+
W_out = math.ceil(img_size[1] / patch_size[1])
|
| 85 |
+
input_resolution = (1, H_out, W_out)
|
| 86 |
+
|
| 87 |
+
self.mask_h_w=input_resolution
|
| 88 |
+
# 3D 窗口:把 2D 窗口扩成 (1, win_lat, win_lon)
|
| 89 |
+
window_size_3d = (1, window_size[0], window_size[1]) # window_size (tuple[int]): Window size [pressure levels, latitude, longitude].
|
| 90 |
+
|
| 91 |
+
# 防止过拟合,随机丢弃一部分
|
| 92 |
+
if depth > 1:
|
| 93 |
+
drop_path = np.linspace(0, 0.2, depth).tolist()
|
| 94 |
+
else:
|
| 95 |
+
drop_path = 0.0
|
| 96 |
+
|
| 97 |
+
self.block1=XiheFuser(dim=self.embed_dim,input_resolution=input_resolution,num_local=1)
|
| 98 |
+
|
| 99 |
+
self.downsample = PanguDownSample(
|
| 100 |
+
in_dim=self.embed_dim,
|
| 101 |
+
input_resolution=(H_out, W_out),
|
| 102 |
+
output_resolution=(H_out // 2, W_out // 2))
|
| 103 |
+
|
| 104 |
+
input_resolution = (1, H_out // 2, W_out // 2)
|
| 105 |
+
self.mask_h_w=input_resolution
|
| 106 |
+
|
| 107 |
+
self.block2=XiheFuser(dim=2*self.embed_dim,input_resolution=input_resolution,num_local=2)
|
| 108 |
+
|
| 109 |
+
self.block3=XiheFuser(dim=2*self.embed_dim,input_resolution=input_resolution,num_local=2)
|
| 110 |
+
self.block4=XiheFuser(dim=2*self.embed_dim,input_resolution=input_resolution,num_local=2)
|
| 111 |
+
|
| 112 |
+
self.upsample = XiheUpSample(in_dim=2*self.embed_dim,out_dim=embed_dim,input_resolution=(H_out // 2, W_out // 2), output_resolution=(H_out, W_out), )
|
| 113 |
+
input_resolution = (1, H_out, W_out)
|
| 114 |
+
self.block5=XiheFuser(dim=self.embed_dim,input_resolution=input_resolution,num_local=1)
|
| 115 |
+
def change_mask(self,mask_full, x, h_out, w_out):
|
| 116 |
+
#根据当前层特征分辨率,自动生成掩码(海洋=1,陆地=0)
|
| 117 |
+
if not torch.is_tensor(mask_full):
|
| 118 |
+
mask_full = torch.tensor(mask_full, dtype=torch.float32)
|
| 119 |
+
else:
|
| 120 |
+
mask_full = mask_full
|
| 121 |
+
|
| 122 |
+
H, W = mask_full.shape
|
| 123 |
+
patch_h = math.ceil(H / h_out)
|
| 124 |
+
patch_w = math.ceil(W / w_out)
|
| 125 |
+
|
| 126 |
+
mask_coarse = torch.zeros((h_out, w_out), dtype=torch.float32)
|
| 127 |
+
for i in range(h_out):
|
| 128 |
+
for j in range(w_out):
|
| 129 |
+
h0, h1 = i * patch_h, min((i + 1) * patch_h, H)
|
| 130 |
+
w0, w1 = j * patch_w, min((j + 1) * patch_w, W)
|
| 131 |
+
patch = mask_full[h0:h1, w0:w1]
|
| 132 |
+
mask_coarse[i, j] = 1.0 if torch.any(patch > 0.5) else 0.0
|
| 133 |
+
|
| 134 |
+
mask_coarse = mask_coarse.to(x.device, dtype=x.dtype)
|
| 135 |
+
B = x.shape[0]
|
| 136 |
+
mask_coarse = mask_coarse.unsqueeze(0).unsqueeze(0).repeat(B, 1, 1, 1) #broadcast
|
| 137 |
+
return mask_coarse
|
| 138 |
+
|
| 139 |
+
def forward(self, x: torch.Tensor):
|
| 140 |
+
x = self.patchembed2d(x) # (B, C=embed_dim, H', W')
|
| 141 |
+
x = x.flatten(2).transpose(1, 2) # (B, N=H'*W', C)
|
| 142 |
+
B, N, C = x.shape
|
| 143 |
+
mask_full=self.mask_full
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
if mask_full is not None: # mask1
|
| 147 |
+
H_out = math.ceil(self.img_size[0] / self.patch_size[0])
|
| 148 |
+
W_out = math.ceil(self.img_size[1] / self.patch_size[1])
|
| 149 |
+
mask1 = self.change_mask(mask_full, x, h_out=H_out, w_out=W_out)
|
| 150 |
+
else:
|
| 151 |
+
mask1 = None
|
| 152 |
+
|
| 153 |
+
obj1 = TensorWithMask(x, mask1)
|
| 154 |
+
x=self.block1(obj1) # (B, N, C) 经过 3D 全局注意力
|
| 155 |
+
x1=x
|
| 156 |
+
x=self.downsample(x) # (B, N, C) 经过 2D 下采样
|
| 157 |
+
|
| 158 |
+
if mask_full is not None: # mask2
|
| 159 |
+
_,H_out,W_out = self.mask_h_w
|
| 160 |
+
mask2 = self.change_mask(mask_full, x, h_out=H_out, w_out=W_out)
|
| 161 |
+
else:
|
| 162 |
+
mask2 = None
|
| 163 |
+
obj2 = TensorWithMask(x, mask2)
|
| 164 |
+
x=self.block2(obj2)
|
| 165 |
+
obj2 = TensorWithMask(x, mask2)
|
| 166 |
+
x=self.block3(obj2)
|
| 167 |
+
obj2 = TensorWithMask(x, mask2)
|
| 168 |
+
x=self.block4(obj2)
|
| 169 |
+
x=self.upsample(x)
|
| 170 |
+
obj1 = TensorWithMask(x, mask1)
|
| 171 |
+
x=self.block5(obj1)
|
| 172 |
+
x_out = torch.cat([x, x1], dim=-1) # (B, N, 2C)
|
| 173 |
+
x_out = self.skip_proj(x_out)
|
| 174 |
+
# B, N, C = x.shape
|
| 175 |
+
# H_, W_ = 341, 360 # 对应 patch grid 尺寸
|
| 176 |
+
H_ = math.ceil(self.img_size[0] / self.patch_size[0])
|
| 177 |
+
W_ = math.ceil(self.img_size[1] / self.patch_size[1])
|
| 178 |
+
x_out = x_out.transpose(1, 2).reshape(B, C, H_, W_)
|
| 179 |
+
x=self.patchrecovery2d(x_out)
|
| 180 |
+
return x
|
scripts/fake_data.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
import h5py
|
| 5 |
+
import numpy as np
|
| 6 |
+
|
| 7 |
+
from onescience.utils.YParams import YParams
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
DATASET_DIMS = {"T": 10, "H": 2041, "W": 4320, "time_step": 24}
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def generate_fake_h5(data_dir, var_names, years, dims):
|
| 14 |
+
os.makedirs(os.path.join(data_dir, "data"), exist_ok=True)
|
| 15 |
+
t_dim, c_dim = dims["T"], len(var_names)
|
| 16 |
+
h_dim, w_dim = dims["H"], dims["W"]
|
| 17 |
+
|
| 18 |
+
for year in years:
|
| 19 |
+
path = os.path.join(data_dir, "data", f"{year}.h5")
|
| 20 |
+
with h5py.File(path, "w") as f:
|
| 21 |
+
ds = f.create_dataset(
|
| 22 |
+
"fields",
|
| 23 |
+
shape=(t_dim, c_dim, h_dim, w_dim),
|
| 24 |
+
dtype="float32",
|
| 25 |
+
chunks=(1, c_dim, h_dim, w_dim),
|
| 26 |
+
fillvalue=0.0,
|
| 27 |
+
)
|
| 28 |
+
ds.attrs["variables"] = var_names
|
| 29 |
+
ds.attrs["time_step"] = dims["time_step"]
|
| 30 |
+
|
| 31 |
+
size_kb = os.path.getsize(path) / 1024
|
| 32 |
+
print(
|
| 33 |
+
f" {year}.h5 shape=({t_dim},{c_dim},{h_dim},{w_dim}) "
|
| 34 |
+
f"logical={t_dim * c_dim * h_dim * w_dim * 4 / 1024**3:.1f}GB actual={size_kb:.1f}KB"
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def generate_metadata(data_dir, var_names, years):
|
| 39 |
+
metadata = {
|
| 40 |
+
"years": [str(year) for year in years],
|
| 41 |
+
"variables": var_names,
|
| 42 |
+
}
|
| 43 |
+
with open(os.path.join(data_dir, "metadata.json"), "w") as f:
|
| 44 |
+
json.dump(metadata, f, indent=2)
|
| 45 |
+
print(f" metadata saved -> {os.path.join(data_dir, 'metadata.json')}")
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def generate_stats(stats_dir, n_vars):
|
| 49 |
+
os.makedirs(stats_dir, exist_ok=True)
|
| 50 |
+
shape = (1, n_vars, 1, 1)
|
| 51 |
+
np.save(os.path.join(stats_dir, "global_means.npy"), np.zeros(shape, dtype=np.float32))
|
| 52 |
+
np.save(os.path.join(stats_dir, "global_stds.npy"), np.ones(shape, dtype=np.float32))
|
| 53 |
+
print(f" stats saved -> {stats_dir}")
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def generate_mask(save_path, shape, one_ratio=0.7, seed=42):
|
| 57 |
+
np.random.seed(seed)
|
| 58 |
+
mask = (np.random.rand(*shape) < one_ratio).astype(np.float32)
|
| 59 |
+
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
| 60 |
+
np.save(save_path, mask)
|
| 61 |
+
print(f" mask saved -> {save_path}")
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def main():
|
| 65 |
+
cfg_model = YParams("conf/config.yaml", "model")
|
| 66 |
+
cfg_datapipe = YParams("conf/config.yaml", "datapipe")
|
| 67 |
+
|
| 68 |
+
if cfg_datapipe.dataset.data_dir.startswith("/public/onestore"):
|
| 69 |
+
print("Please check config and ensure local test paths are used.")
|
| 70 |
+
exit()
|
| 71 |
+
|
| 72 |
+
years = (
|
| 73 |
+
cfg_datapipe.dataset.train_time
|
| 74 |
+
+ cfg_datapipe.dataset.val_time
|
| 75 |
+
+ cfg_datapipe.dataset.test_time
|
| 76 |
+
)
|
| 77 |
+
channels = cfg_datapipe.dataset.channels
|
| 78 |
+
|
| 79 |
+
generate_fake_h5(cfg_datapipe.dataset.data_dir, channels, years, DATASET_DIMS)
|
| 80 |
+
generate_metadata(cfg_datapipe.dataset.data_dir, channels, years)
|
| 81 |
+
generate_stats(cfg_datapipe.dataset.stats_dir, len(channels))
|
| 82 |
+
generate_mask(cfg_model.mask, (DATASET_DIMS["H"], DATASET_DIMS["W"]))
|
| 83 |
+
|
| 84 |
+
print("\n✅ Fake CMEMS datasets generated.")
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
if __name__ == "__main__":
|
| 88 |
+
main()
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
|
| 8 |
+
import glob
|
| 9 |
+
import os
|
| 10 |
+
|
| 11 |
+
import h5py
|
| 12 |
+
import numpy as np
|
| 13 |
+
import torch
|
| 14 |
+
|
| 15 |
+
from tqdm import tqdm
|
| 16 |
+
|
| 17 |
+
from onescience.datapipes.climate import CMEMSDatapipe
|
| 18 |
+
from model.xihe import Xihe
|
| 19 |
+
from onescience.utils.YParams import YParams
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def get_stats(data_dir, stats_dir, channels):
|
| 23 |
+
h5_files = sorted(glob.glob(os.path.join(data_dir, "data", "*.h5")))
|
| 24 |
+
with h5py.File(h5_files[0], "r") as f:
|
| 25 |
+
ds = f["fields"]
|
| 26 |
+
all_variables = [v.decode() if isinstance(v, bytes) else v for v in ds.attrs["variables"]]
|
| 27 |
+
|
| 28 |
+
channel_indices = [all_variables.index(v) for v in channels]
|
| 29 |
+
mu = np.load(os.path.join(stats_dir, "global_means.npy"))
|
| 30 |
+
std = np.load(os.path.join(stats_dir, "global_stds.npy"))
|
| 31 |
+
means = mu[:, channel_indices, :, :]
|
| 32 |
+
stds = std[:, channel_indices, :, :]
|
| 33 |
+
return means, stds
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
if __name__ == "__main__":
|
| 37 |
+
current_path = os.getcwd()
|
| 38 |
+
sys.path.append(current_path)
|
| 39 |
+
|
| 40 |
+
config_file_path = os.path.join(current_path, "conf/config.yaml")
|
| 41 |
+
cfg = YParams(config_file_path, "model")
|
| 42 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 43 |
+
|
| 44 |
+
means, stds = get_stats(
|
| 45 |
+
cfg_data.dataset.data_dir,
|
| 46 |
+
cfg_data.dataset.stats_dir,
|
| 47 |
+
cfg_data.dataset.channels,
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
datapipe = CMEMSDatapipe(
|
| 51 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 52 |
+
used_variables=cfg_data.dataset.channels,
|
| 53 |
+
used_years=cfg_data.dataset.test_time,
|
| 54 |
+
distributed=False,
|
| 55 |
+
batch_size=1,
|
| 56 |
+
num_workers=cfg_data.dataloader.num_workers,
|
| 57 |
+
)
|
| 58 |
+
test_dataloader, _ = datapipe.get_dataloader("test")
|
| 59 |
+
|
| 60 |
+
ckpt = torch.load(f"{cfg.checkpoint_dir}/model_bak.pth", map_location="cuda:0")
|
| 61 |
+
model = Xihe(cfg).to("cuda:0")
|
| 62 |
+
model.load_state_dict(ckpt["model_state_dict"])
|
| 63 |
+
|
| 64 |
+
model.eval()
|
| 65 |
+
os.makedirs("result/output/", exist_ok=True)
|
| 66 |
+
print("📂 samples will be generated to './result/output/'")
|
| 67 |
+
with torch.no_grad():
|
| 68 |
+
for data in tqdm(test_dataloader, desc="Inferring testset", unit="batch"):
|
| 69 |
+
invar = data[0].to("cuda:0", dtype=torch.float32)
|
| 70 |
+
filename = data[4][-1][0]
|
| 71 |
+
|
| 72 |
+
outvar_pred = model(invar)
|
| 73 |
+
pred_var = outvar_pred.cpu().numpy()
|
| 74 |
+
pred_var = pred_var * stds + means
|
| 75 |
+
np.save(f"result/output/{filename}.npy", pred_var)
|
scripts/result.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import glob
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
|
| 7 |
+
import h5py
|
| 8 |
+
import matplotlib.pyplot as plt
|
| 9 |
+
import numpy as np
|
| 10 |
+
|
| 11 |
+
from matplotlib import rcParams
|
| 12 |
+
from tqdm import tqdm
|
| 13 |
+
|
| 14 |
+
from onescience.utils.fcn.YParams import YParams
|
| 15 |
+
|
| 16 |
+
rcParams["mathtext.fontset"] = "stix"
|
| 17 |
+
rcParams["axes.linewidth"] = 0.9
|
| 18 |
+
rcParams["xtick.major.width"] = 0.9
|
| 19 |
+
rcParams["ytick.major.width"] = 0.9
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def get_metadata(data_dir, channels):
|
| 23 |
+
h5_files = sorted(glob.glob(os.path.join(data_dir, "data", "*.h5")))
|
| 24 |
+
with h5py.File(h5_files[0], "r") as f:
|
| 25 |
+
ds = f["fields"]
|
| 26 |
+
all_variables = [v.decode() if isinstance(v, bytes) else v for v in ds.attrs["variables"]]
|
| 27 |
+
time_step = int(ds.attrs["time_step"])
|
| 28 |
+
|
| 29 |
+
channel_indices = [all_variables.index(v) for v in channels]
|
| 30 |
+
total_files = sorted(f for f in os.listdir("./result/output/") if f.endswith(".npy"))
|
| 31 |
+
return total_files, channel_indices, time_step
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def filename_to_index(filename, time_step):
|
| 35 |
+
dt = datetime.strptime(filename, "%Y%m%d%H")
|
| 36 |
+
year_start = datetime(dt.year, 1, 1)
|
| 37 |
+
hours = (dt - year_start).total_seconds() / 3600
|
| 38 |
+
return int(hours / time_step)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def get_result(total_files, channel_indices, time_step, data_dir, clim_mean):
|
| 42 |
+
channel_rmse = np.zeros(len(channel_indices))
|
| 43 |
+
channel_acc = np.zeros(len(channel_indices))
|
| 44 |
+
clim_mean = clim_mean[0, :, :, :]
|
| 45 |
+
if not os.path.exists("./result/rmse.npy") or not os.path.exists("result/acc.npy"):
|
| 46 |
+
numerator = np.zeros(len(channel_indices))
|
| 47 |
+
pred_sq_sum = np.zeros(len(channel_indices))
|
| 48 |
+
label_sq_sum = np.zeros(len(channel_indices))
|
| 49 |
+
for file in tqdm(total_files, unit="files"):
|
| 50 |
+
fname = file[:-4]
|
| 51 |
+
year = fname[:4]
|
| 52 |
+
t_idx = filename_to_index(fname, time_step)
|
| 53 |
+
with h5py.File(os.path.join(data_dir, "data", f"{year}.h5"), "r") as f:
|
| 54 |
+
label = f["fields"][t_idx]
|
| 55 |
+
label = label[channel_indices]
|
| 56 |
+
|
| 57 |
+
pred = np.load(f"result/output/{file}").squeeze()
|
| 58 |
+
label_anom = label - clim_mean
|
| 59 |
+
pred_anom = pred - clim_mean
|
| 60 |
+
|
| 61 |
+
numerator += np.sum(pred_anom * label_anom, axis=(1, 2))
|
| 62 |
+
pred_sq_sum += np.sum(pred_anom ** 2, axis=(1, 2))
|
| 63 |
+
label_sq_sum += np.sum(label_anom ** 2, axis=(1, 2))
|
| 64 |
+
channel_rmse += np.sqrt(np.mean((label - pred) ** 2, axis=(1, 2)))
|
| 65 |
+
|
| 66 |
+
channel_rmse /= len(total_files)
|
| 67 |
+
channel_acc = numerator / (np.sqrt(pred_sq_sum * label_sq_sum) + 1e-8)
|
| 68 |
+
np.save("./result/acc.npy", channel_acc)
|
| 69 |
+
np.save("./result/rmse.npy", channel_rmse)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def show_result():
|
| 73 |
+
channel_rmse = np.load("./result/rmse.npy")
|
| 74 |
+
channel_acc = np.load("./result/acc.npy")
|
| 75 |
+
channels = [cfg_data.dataset.channels[i] for i in range(len(channel_indices))]
|
| 76 |
+
w = 36
|
| 77 |
+
|
| 78 |
+
print(f"┌{'─' * (w + 2)}┬{'─' * 14}┬{'─' * 14}┐")
|
| 79 |
+
print(f"│ {'Channel':<{w}} │ {'RMSE':>12} │ {'ACC':>12} │")
|
| 80 |
+
print(f"├{'─' * (w + 2)}┼{'─' * 14}┼{'─' * 14}┤")
|
| 81 |
+
for i, ch in enumerate(channels):
|
| 82 |
+
print(f"│ {ch:<{w}} │ {channel_rmse[i]:>12.4f} | {channel_acc[i]:>12.4f} |")
|
| 83 |
+
print(f"├{'─' * (w + 2)}┼{'─' * 14}┼{'─' * 14}┤")
|
| 84 |
+
print(f"│ {'Average':<{w}} │ {np.mean(channel_rmse):>12.4f} │ {np.mean(channel_acc):>12.4f} │")
|
| 85 |
+
print(f"└{'─' * (w + 2)}┴{'─' * 14}┴{'─' * 14}┘")
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def plot(label, pred, var, filename):
|
| 89 |
+
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
|
| 90 |
+
|
| 91 |
+
xtick_labels = ["180°W", "90°W", "0°", "90°E", "180°E"]
|
| 92 |
+
ytick_labels = ["90°S", "45°S", "0°", "45°N", "90°N"]
|
| 93 |
+
xticks = np.linspace(0, label.shape[-1] - 1, 5)
|
| 94 |
+
yticks = np.linspace(0, label.shape[-2] - 1, 5)
|
| 95 |
+
|
| 96 |
+
vmin = min(label.min(), pred.min())
|
| 97 |
+
vmax = max(label.max(), pred.max())
|
| 98 |
+
diff = label - pred
|
| 99 |
+
rmse = np.sqrt(np.mean(diff ** 2))
|
| 100 |
+
diff_abs_max = np.abs(diff).max()
|
| 101 |
+
|
| 102 |
+
plot_configs = [
|
| 103 |
+
{"data": label, "title": "Truth", "cmap": "viridis", "vmin": vmin, "vmax": vmax},
|
| 104 |
+
{"data": pred, "title": "Prediction", "cmap": "viridis", "vmin": vmin, "vmax": vmax},
|
| 105 |
+
{
|
| 106 |
+
"data": diff,
|
| 107 |
+
"title": f"Difference (RMSE={rmse:.2f})",
|
| 108 |
+
"cmap": "RdBu_r",
|
| 109 |
+
"vmin": -diff_abs_max,
|
| 110 |
+
"vmax": diff_abs_max,
|
| 111 |
+
},
|
| 112 |
+
]
|
| 113 |
+
|
| 114 |
+
for ax, cfg in zip(axes, plot_configs):
|
| 115 |
+
im = ax.imshow(cfg["data"], cmap=cfg["cmap"], vmin=cfg["vmin"], vmax=cfg["vmax"])
|
| 116 |
+
ax.set_title(cfg["title"], fontsize=12, pad=4)
|
| 117 |
+
ax.set_xlabel("Longitude")
|
| 118 |
+
ax.set_ylabel("Latitude")
|
| 119 |
+
ax.set_xticks(xticks)
|
| 120 |
+
ax.set_xticklabels(xtick_labels)
|
| 121 |
+
ax.set_yticks(yticks)
|
| 122 |
+
ax.set_yticklabels(ytick_labels)
|
| 123 |
+
plt.colorbar(im, ax=ax, orientation="horizontal")
|
| 124 |
+
|
| 125 |
+
fig.suptitle(var, fontsize=14, fontweight="bold", y=0.98)
|
| 126 |
+
plt.savefig(filename, dpi=300, bbox_inches="tight")
|
| 127 |
+
plt.close()
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def plot_loss(train_loss, valid_loss):
|
| 131 |
+
mask = ~(np.isnan(train_loss) | np.isnan(valid_loss))
|
| 132 |
+
train_loss = train_loss[mask]
|
| 133 |
+
valid_loss = valid_loss[mask]
|
| 134 |
+
|
| 135 |
+
fig, ax = plt.subplots(figsize=(5, 3.5))
|
| 136 |
+
colors = {"train": "#2563EB", "valid": "#EA580C"}
|
| 137 |
+
epochs = np.arange(1, len(train_loss) + 1)
|
| 138 |
+
|
| 139 |
+
ax.plot(epochs, train_loss, color=colors["train"], linewidth=1.5, label="Train")
|
| 140 |
+
ax.plot(epochs, valid_loss, color=colors["valid"], linewidth=1.5, label="Valid", linestyle="--")
|
| 141 |
+
|
| 142 |
+
min_idx = np.argmin(valid_loss)
|
| 143 |
+
ax.scatter(epochs[min_idx], valid_loss[min_idx], color=colors["valid"], s=40, zorder=5, edgecolors="white")
|
| 144 |
+
ax.annotate(
|
| 145 |
+
f"Best: {valid_loss[min_idx]:.3f}",
|
| 146 |
+
xy=(epochs[min_idx], valid_loss[min_idx]),
|
| 147 |
+
xytext=(10, 10),
|
| 148 |
+
textcoords="offset points",
|
| 149 |
+
fontsize=8,
|
| 150 |
+
color=colors["valid"],
|
| 151 |
+
arrowprops=dict(arrowstyle="-", color=colors["valid"], lw=0.5),
|
| 152 |
+
)
|
| 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 |
+
current_path = os.getcwd()
|
| 166 |
+
sys.path.append(current_path)
|
| 167 |
+
config_file_path = os.path.join(current_path, "conf/config.yaml")
|
| 168 |
+
cfg = YParams(config_file_path, "model")
|
| 169 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 170 |
+
|
| 171 |
+
train_loss = np.load("./data/checkpoints/trloss.npy")
|
| 172 |
+
valid_loss = np.load("./data/checkpoints/valoss.npy")
|
| 173 |
+
plot_loss(train_loss, valid_loss)
|
| 174 |
+
|
| 175 |
+
data_dir = cfg_data.dataset.data_dir
|
| 176 |
+
total_files, channel_indices, time_step = get_metadata(data_dir, cfg_data.dataset.channels)
|
| 177 |
+
|
| 178 |
+
mu = np.load(os.path.join(cfg_data.dataset.stats_dir, "global_means.npy"))
|
| 179 |
+
clim_mean = mu[:, channel_indices, :, :]
|
| 180 |
+
get_result(total_files, channel_indices, time_step, data_dir, clim_mean)
|
| 181 |
+
show_result()
|
| 182 |
+
|
| 183 |
+
test_year = cfg_data.dataset.test_time[0]
|
| 184 |
+
eg_files = [f"{test_year}010200"]
|
| 185 |
+
channel_index = [
|
| 186 |
+
cfg_data.dataset.channels.index(v)
|
| 187 |
+
for v in [
|
| 188 |
+
"sea_surface_height_above_geoid",
|
| 189 |
+
"sea_water_potential_temperature_1",
|
| 190 |
+
"sea_water_salinity_4",
|
| 191 |
+
]
|
| 192 |
+
]
|
| 193 |
+
|
| 194 |
+
selected_var = [cfg_data.dataset.channels[int(i)] for i in channel_index]
|
| 195 |
+
print(f"seleted date: {eg_files}")
|
| 196 |
+
print(f"selected channels: {selected_var}")
|
| 197 |
+
for file in eg_files:
|
| 198 |
+
year = file[:4]
|
| 199 |
+
t_idx = filename_to_index(file, time_step)
|
| 200 |
+
with h5py.File(os.path.join(data_dir, "data", f"{year}.h5"), "r") as f:
|
| 201 |
+
label = f["fields"][t_idx]
|
| 202 |
+
label = label[channel_indices]
|
| 203 |
+
|
| 204 |
+
pred = np.load(f"result/output/{file}.npy").squeeze()
|
| 205 |
+
for i in range(len(selected_var)):
|
| 206 |
+
filename = f"./result/{file}_{selected_var[i]}.png"
|
| 207 |
+
plot(label[channel_index[i]], pred[channel_index[i]], selected_var[i], filename)
|
| 208 |
+
print(f"✅plot {filename}")
|
scripts/train.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
|
| 8 |
+
import logging
|
| 9 |
+
import os
|
| 10 |
+
import time
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
import torch
|
| 14 |
+
import torch.distributed as dist
|
| 15 |
+
|
| 16 |
+
from torch.nn.parallel import DistributedDataParallel
|
| 17 |
+
|
| 18 |
+
from onescience.datapipes.climate import CMEMSDatapipe
|
| 19 |
+
from model.xihe import Xihe
|
| 20 |
+
from onescience.utils.YParams import YParams
|
| 21 |
+
from onescience.utils.fcn.darcy_loss import LpLoss
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def main():
|
| 25 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
| 26 |
+
logger = logging.getLogger()
|
| 27 |
+
|
| 28 |
+
config_file_path = os.path.join(current_path, "conf/config.yaml")
|
| 29 |
+
cfg = YParams(config_file_path, "model")
|
| 30 |
+
|
| 31 |
+
cfg.world_size = 1
|
| 32 |
+
if "WORLD_SIZE" in os.environ:
|
| 33 |
+
cfg.world_size = int(os.environ["WORLD_SIZE"])
|
| 34 |
+
|
| 35 |
+
world_rank = 0
|
| 36 |
+
local_rank = 0
|
| 37 |
+
if cfg.world_size > 1:
|
| 38 |
+
dist.init_process_group(backend="nccl", init_method="env://")
|
| 39 |
+
local_rank = int(os.environ["LOCAL_RANK"])
|
| 40 |
+
world_rank = dist.get_rank()
|
| 41 |
+
|
| 42 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 43 |
+
datapipe = CMEMSDatapipe(
|
| 44 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 45 |
+
used_variables=cfg_data.dataset.channels,
|
| 46 |
+
used_years=cfg_data.dataset.train_time,
|
| 47 |
+
distributed=dist.is_initialized(),
|
| 48 |
+
batch_size=cfg_data.dataloader.batch_size,
|
| 49 |
+
num_workers=cfg_data.dataloader.num_workers,
|
| 50 |
+
)
|
| 51 |
+
train_dataloader, train_sampler = datapipe.get_dataloader("train")
|
| 52 |
+
datapipe = CMEMSDatapipe(
|
| 53 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 54 |
+
used_variables=cfg_data.dataset.channels,
|
| 55 |
+
used_years=cfg_data.dataset.val_time,
|
| 56 |
+
distributed=dist.is_initialized(),
|
| 57 |
+
batch_size=cfg_data.dataloader.batch_size,
|
| 58 |
+
num_workers=cfg_data.dataloader.num_workers,
|
| 59 |
+
)
|
| 60 |
+
val_dataloader, val_sampler = datapipe.get_dataloader("valid")
|
| 61 |
+
|
| 62 |
+
model = Xihe(config=cfg).to(local_rank)
|
| 63 |
+
optimizer = torch.optim.AdamW(
|
| 64 |
+
model.parameters(),
|
| 65 |
+
lr=cfg.lr,
|
| 66 |
+
betas=tuple(cfg.betas),
|
| 67 |
+
weight_decay=cfg.weight_decay,
|
| 68 |
+
)
|
| 69 |
+
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
|
| 70 |
+
optimizer, factor=0.2, patience=5, mode="min"
|
| 71 |
+
)
|
| 72 |
+
loss_obj = LpLoss()
|
| 73 |
+
|
| 74 |
+
os.makedirs(cfg.checkpoint_dir, exist_ok=True)
|
| 75 |
+
train_loss_file = f"{cfg.checkpoint_dir}/trloss.npy"
|
| 76 |
+
valid_loss_file = f"{cfg.checkpoint_dir}/valoss.npy"
|
| 77 |
+
best_valid_loss = 1.0e6
|
| 78 |
+
best_loss_epoch = 0
|
| 79 |
+
train_losses = np.empty((0,), dtype=np.float32)
|
| 80 |
+
valid_losses = np.empty((0,), dtype=np.float32)
|
| 81 |
+
|
| 82 |
+
if cfg.world_size == 1:
|
| 83 |
+
total_params = sum(p.numel() for p in model.parameters())
|
| 84 |
+
print("\n")
|
| 85 |
+
print("-" * 50)
|
| 86 |
+
print(f"📂 now params is {total_params}, {total_params / 1e6:.2f}M, {total_params / 1e9:.2f}B")
|
| 87 |
+
print("-" * 50, "\n")
|
| 88 |
+
|
| 89 |
+
if os.path.exists(f"{cfg.checkpoint_dir}/model_bak.pth"):
|
| 90 |
+
if world_rank == 0:
|
| 91 |
+
print("\n")
|
| 92 |
+
print("-" * 50)
|
| 93 |
+
print("✅ There has a model weight, load and continue training...")
|
| 94 |
+
print(f"If you want to train a new model, ensure there is no *.pth file in {cfg.checkpoint_dir}")
|
| 95 |
+
print("-" * 50, "\n")
|
| 96 |
+
|
| 97 |
+
ckpt = torch.load(
|
| 98 |
+
f"{cfg.checkpoint_dir}/model_bak.pth",
|
| 99 |
+
map_location=f"cuda:{local_rank}",
|
| 100 |
+
weights_only=False,
|
| 101 |
+
)
|
| 102 |
+
model.load_state_dict(ckpt["model_state_dict"])
|
| 103 |
+
optimizer.load_state_dict(ckpt["optimizer_state_dict"])
|
| 104 |
+
scheduler.load_state_dict(ckpt["scheduler_state_dict"])
|
| 105 |
+
best_valid_loss = ckpt["best_valid_loss"]
|
| 106 |
+
best_loss_epoch = ckpt["best_loss_epoch"]
|
| 107 |
+
train_losses = np.load(train_loss_file)
|
| 108 |
+
valid_losses = np.load(valid_loss_file)
|
| 109 |
+
|
| 110 |
+
if cfg.world_size > 1:
|
| 111 |
+
model = DistributedDataParallel(model, device_ids=[local_rank], output_device=local_rank)
|
| 112 |
+
|
| 113 |
+
world_rank == 0 and logger.info("start training ...")
|
| 114 |
+
|
| 115 |
+
for epoch in range(cfg.max_epoch):
|
| 116 |
+
if dist.is_initialized():
|
| 117 |
+
train_sampler.set_epoch(epoch)
|
| 118 |
+
val_sampler.set_epoch(epoch)
|
| 119 |
+
|
| 120 |
+
model.train()
|
| 121 |
+
train_loss = 0.0
|
| 122 |
+
start_time = time.time()
|
| 123 |
+
for j, data in enumerate(train_dataloader):
|
| 124 |
+
invar = data[0].to(local_rank, dtype=torch.float32)
|
| 125 |
+
outvar = data[1].to(local_rank, dtype=torch.float32)
|
| 126 |
+
outvar_pred = model(invar)
|
| 127 |
+
loss = loss_obj(outvar, outvar_pred)
|
| 128 |
+
|
| 129 |
+
optimizer.zero_grad()
|
| 130 |
+
loss.backward()
|
| 131 |
+
optimizer.step()
|
| 132 |
+
train_loss += loss.item()
|
| 133 |
+
|
| 134 |
+
if world_rank == 0:
|
| 135 |
+
logger.info(
|
| 136 |
+
f"Train: Epoch {epoch}-{j + 1}/{len(train_dataloader)} "
|
| 137 |
+
f"[cost {int((time.time() - start_time) // 60):02}:{int((time.time() - start_time) % 60):02}] "
|
| 138 |
+
f"[{(time.time() - start_time) / (j + 1): .02f}s/{cfg_data.dataloader.batch_size}batch] "
|
| 139 |
+
f"loss:{train_loss / (j + 1): .04f}"
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
train_loss /= len(train_dataloader)
|
| 143 |
+
|
| 144 |
+
model.eval()
|
| 145 |
+
valid_loss = 0.0
|
| 146 |
+
val_start_time = time.time()
|
| 147 |
+
with torch.no_grad():
|
| 148 |
+
for j, data in enumerate(val_dataloader):
|
| 149 |
+
invar = data[0].to(local_rank, dtype=torch.float32)
|
| 150 |
+
outvar = data[1].to(local_rank, dtype=torch.float32)
|
| 151 |
+
outvar_pred = model(invar)
|
| 152 |
+
loss = loss_obj(outvar, outvar_pred)
|
| 153 |
+
|
| 154 |
+
if cfg.world_size > 1:
|
| 155 |
+
loss_tensor = loss.detach().to(local_rank)
|
| 156 |
+
dist.all_reduce(loss_tensor)
|
| 157 |
+
loss = loss_tensor.item() / cfg.world_size
|
| 158 |
+
valid_loss += loss
|
| 159 |
+
else:
|
| 160 |
+
valid_loss += loss.item()
|
| 161 |
+
|
| 162 |
+
if world_rank == 0:
|
| 163 |
+
logger.info(
|
| 164 |
+
f"Valid: Epoch {epoch}-{j + 1}/{len(val_dataloader)} "
|
| 165 |
+
f"[cost {int((time.time() - val_start_time) // 60):02}:{int((time.time() - val_start_time) % 60):02}] "
|
| 166 |
+
f"[{(time.time() - val_start_time) / (j + 1): .02f}s/{cfg_data.dataloader.batch_size}batch] "
|
| 167 |
+
f"loss:{valid_loss / (j + 1): .04f}"
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
valid_loss /= len(val_dataloader)
|
| 171 |
+
is_save_ckp = False
|
| 172 |
+
if valid_loss < best_valid_loss:
|
| 173 |
+
best_valid_loss = valid_loss
|
| 174 |
+
best_loss_epoch = epoch
|
| 175 |
+
world_rank == 0 and save_checkpoint(
|
| 176 |
+
model,
|
| 177 |
+
optimizer,
|
| 178 |
+
scheduler,
|
| 179 |
+
best_valid_loss,
|
| 180 |
+
best_loss_epoch,
|
| 181 |
+
cfg.checkpoint_dir,
|
| 182 |
+
)
|
| 183 |
+
is_save_ckp = True
|
| 184 |
+
|
| 185 |
+
scheduler.step(valid_loss)
|
| 186 |
+
|
| 187 |
+
if world_rank == 0:
|
| 188 |
+
logger.info(
|
| 189 |
+
f"Epoch [{epoch + 1}/{cfg.max_epoch}], "
|
| 190 |
+
f"Train Loss: {train_loss:.4f}, "
|
| 191 |
+
f"Valid Loss: {valid_loss:.4f}, "
|
| 192 |
+
f"Best loss at Epoch: {best_loss_epoch + 1}"
|
| 193 |
+
+ (", saving checkpoint" if is_save_ckp else "")
|
| 194 |
+
)
|
| 195 |
+
train_losses = np.append(train_losses, train_loss)
|
| 196 |
+
valid_losses = np.append(valid_losses, valid_loss)
|
| 197 |
+
np.save(train_loss_file, train_losses)
|
| 198 |
+
np.save(valid_loss_file, valid_losses)
|
| 199 |
+
|
| 200 |
+
if epoch - best_loss_epoch > cfg.patience:
|
| 201 |
+
print(f"Loss has not decrease in {cfg.patience} epochs, stopping training...")
|
| 202 |
+
exit()
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, model_path):
|
| 206 |
+
model_to_save = model.module if hasattr(model, "module") else model
|
| 207 |
+
state = {
|
| 208 |
+
"model_state_dict": model_to_save.state_dict(),
|
| 209 |
+
"optimizer_state_dict": optimizer.state_dict(),
|
| 210 |
+
"scheduler_state_dict": scheduler.state_dict(),
|
| 211 |
+
"best_valid_loss": best_valid_loss,
|
| 212 |
+
"best_loss_epoch": best_loss_epoch,
|
| 213 |
+
}
|
| 214 |
+
torch.save(state, f"{model_path}/model.pth")
|
| 215 |
+
os.system(f"mv {model_path}/model.pth {model_path}/model_bak.pth")
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
if __name__ == "__main__":
|
| 219 |
+
current_path = os.getcwd()
|
| 220 |
+
sys.path.append(current_path)
|
| 221 |
+
main()
|
weight/.gitkeep
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|