Upload folder using huggingface_hub
Browse files- README.md +157 -0
- conf/config.yaml +111 -0
- configuration.json +1 -0
- model/fourcastnet.py +160 -0
- scripts/fake_data.py +95 -0
- scripts/inference.py +72 -0
- scripts/result.py +201 -0
- scripts/train.py +199 -0
- weight/.gitkeep +0 -0
README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
- zh
|
| 6 |
+
tags:
|
| 7 |
+
- OneScience
|
| 8 |
+
- Earth Science
|
| 9 |
+
- Weather Forecast
|
| 10 |
+
- Short-to-Medium-Range Weather Forecast
|
| 11 |
+
- ERA5
|
| 12 |
+
frameworks: PyTorch
|
| 13 |
+
datasets:
|
| 14 |
+
- OneScience/ERA5
|
| 15 |
+
---
|
| 16 |
+
<p align="center">
|
| 17 |
+
<strong>
|
| 18 |
+
<span style="font-size: 30px;">FourCastNet</span>
|
| 19 |
+
</strong>
|
| 20 |
+
</p>
|
| 21 |
+
|
| 22 |
+
# Model Introduction
|
| 23 |
+
|
| 24 |
+
FourCastNet (Fourier Forecasting Neural Network) is a global weather forecast model based on AFNO (Adaptive Fourier Neural Operator), jointly developed by NVIDIA and multiple top-tier academic institutions.
|
| 25 |
+
|
| 26 |
+
Paper: FourCastNet: A Global Data-driven High-resolution Weather Forecasting Model
|
| 27 |
+
|
| 28 |
+
https://arxiv.org/abs/2202.11214
|
| 29 |
+
|
| 30 |
+
# Model Description
|
| 31 |
+
|
| 32 |
+
FourCastNet is a global high-resolution weather forecast model based on the Adaptive Fourier Neural Operator, suitable for short-to-medium-range global weather forecast research.
|
| 33 |
+
|
| 34 |
+
# Use Cases
|
| 35 |
+
|
| 36 |
+
| Scenario | Description |
|
| 37 |
+
| :---: | :--- |
|
| 38 |
+
| Global Weather Forecast Research | Train a FourCastNet-style AFNO forecast model using annual ERA5 HDF5 data. |
|
| 39 |
+
| Local Quick Validation | Use synthetic data to verify data loading, training entry points, inference, and result scripts. |
|
| 40 |
+
| ModelScope / OneCode Execution | Download as a standalone model package, install dependencies, and run scripts directly. |
|
| 41 |
+
| Multi-GPU Training | Launch multi-process training via `torchrun`. |
|
| 42 |
+
|
| 43 |
+
# Usage Guide
|
| 44 |
+
|
| 45 |
+
## 1. OneCode Usage
|
| 46 |
+
|
| 47 |
+
Experience intelligent one-click AI4S programming through the OneCode online environment:
|
| 48 |
+
|
| 49 |
+
[Click to Experience Intelligent One-Click AI4S Programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 50 |
+
|
| 51 |
+
## 2. Manual Installation and Usage
|
| 52 |
+
|
| 53 |
+
**Hardware Requirements**
|
| 54 |
+
|
| 55 |
+
- A GPU or DCU is recommended.
|
| 56 |
+
- CPU can be used for import and small-scale connectivity verification; full training and inference will be slow.
|
| 57 |
+
- DCU users must install DTK in advance. DTK 25.04.2 or above, or the OneScience recommended version matching your cluster, is recommended.
|
| 58 |
+
|
| 59 |
+
### Download the Model Package
|
| 60 |
+
|
| 61 |
+
```bash
|
| 62 |
+
modelscope download --model OneScience/FourCastNet --local_dir ./FourCastNet
|
| 63 |
+
cd FourCastNet
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
### Install the Runtime Environment
|
| 67 |
+
|
| 68 |
+
**DCU Environment**
|
| 69 |
+
|
| 70 |
+
```bash
|
| 71 |
+
# Please activate DTK and CONDA first
|
| 72 |
+
conda create -n onescience311 python=3.11 -y
|
| 73 |
+
conda activate onescience311
|
| 74 |
+
# uv installation is supported
|
| 75 |
+
pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
**GPU Environment**
|
| 79 |
+
```bash
|
| 80 |
+
# Please activate CONDA first
|
| 81 |
+
conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
|
| 82 |
+
conda activate onescience311
|
| 83 |
+
# uv installation is supported
|
| 84 |
+
pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
### Training Data Introduction
|
| 88 |
+
|
| 89 |
+
The OneScience community provides ERA5 data for training (due to file size limits, the current repository contains a slice of the full dataset). Users can download it with the command below and confirm that the data path in `conf/config.yaml` is set correctly:
|
| 90 |
+
|
| 91 |
+
```bash
|
| 92 |
+
modelscope download --dataset OneScience/ERA5 --local_dir ./data
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
### Training
|
| 96 |
+
|
| 97 |
+
Single GPU:
|
| 98 |
+
|
| 99 |
+
```bash
|
| 100 |
+
python scripts/train.py
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
Multi-GPU:
|
| 104 |
+
|
| 105 |
+
```bash
|
| 106 |
+
torchrun --nproc_per_node=8 --nnodes=1 --rdzv_id=1000 --rdzv_backend=c10d --max_restarts=0 --master_addr="localhost" --master_port=29500 scripts/train.py
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
Training outputs:
|
| 110 |
+
|
| 111 |
+
```text
|
| 112 |
+
data/checkpoints/model_bak.pth
|
| 113 |
+
data/checkpoints/trloss.npy
|
| 114 |
+
data/checkpoints/valoss.npy
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
### Training Weights
|
| 118 |
+
|
| 119 |
+
This repository provides weights trained on ERA5 reanalysis data in the `weights/` folder. The weight files will be uploaded soon and are expected to be available in the near future.
|
| 120 |
+
|
| 121 |
+
### Inference
|
| 122 |
+
|
| 123 |
+
Inference reads `data/checkpoints/model_bak.pth` by default:
|
| 124 |
+
|
| 125 |
+
```bash
|
| 126 |
+
python scripts/inference.py
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
Prediction results are output to:
|
| 130 |
+
|
| 131 |
+
```text
|
| 132 |
+
result/output/
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
### Evaluation and Visualization
|
| 136 |
+
|
| 137 |
+
```bash
|
| 138 |
+
python scripts/result.py
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
Output contents include:
|
| 142 |
+
|
| 143 |
+
- `result/rmse.npy`
|
| 144 |
+
- `result/acc.npy`
|
| 145 |
+
- `result/loss.png`
|
| 146 |
+
- Forecast comparison plots for specified dates and variables
|
| 147 |
+
|
| 148 |
+
# OneScience Official Information
|
| 149 |
+
|
| 150 |
+
| Platform | OneScience Main Repository | Skills Repository |
|
| 151 |
+
| --- | --- | --- |
|
| 152 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 153 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 154 |
+
|
| 155 |
+
# Citation & License
|
| 156 |
+
|
| 157 |
+
- This repository is an independent reproduction of FourCastNet, with the architecture design adapted from the original paper by Pathak et al. (2022).
|
conf/config.yaml
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ERA5数据集配置示例
|
| 2 |
+
model:
|
| 3 |
+
start_epoch: 0
|
| 4 |
+
max_epoch: 100
|
| 5 |
+
lr: 1E-3
|
| 6 |
+
num_blocks: 8
|
| 7 |
+
patch_size: [8, 8]
|
| 8 |
+
patience: 50
|
| 9 |
+
checkpoint_dir: "./data/checkpoints"
|
| 10 |
+
|
| 11 |
+
# 整个数据读取流程
|
| 12 |
+
datapipe:
|
| 13 |
+
name: "ERA5"
|
| 14 |
+
task: "weather_forecasting"
|
| 15 |
+
|
| 16 |
+
# dataset设定
|
| 17 |
+
dataset:
|
| 18 |
+
type: "hdf5"
|
| 19 |
+
data_dir: './data/' # "$ONESCIENCE_DATASETS_DIR/ERA5/newh5/"
|
| 20 |
+
|
| 21 |
+
train_time: [1951, 1952]
|
| 22 |
+
val_time: [1953]
|
| 23 |
+
test_time: [1954]
|
| 24 |
+
img_size: [721, 1440]
|
| 25 |
+
verbose: true
|
| 26 |
+
cache: false
|
| 27 |
+
# 气象变量
|
| 28 |
+
|
| 29 |
+
channels: ['10m_u_component_of_wind', '10m_v_component_of_wind', '2m_temperature', 'surface_pressure',
|
| 30 |
+
'mean_sea_level_pressure', 'total_column_water_vapour', # 6 surface
|
| 31 |
+
'u_component_of_wind_1000',
|
| 32 |
+
'v_component_of_wind_1000',
|
| 33 |
+
|
| 34 |
+
'temperature_850',
|
| 35 |
+
'u_component_of_wind_850',
|
| 36 |
+
'v_component_of_wind_850',
|
| 37 |
+
'geopotential_850',
|
| 38 |
+
'relative_humidity_850',
|
| 39 |
+
|
| 40 |
+
'temperature_500',
|
| 41 |
+
'u_component_of_wind_500',
|
| 42 |
+
'v_component_of_wind_500',
|
| 43 |
+
'geopotential_500',
|
| 44 |
+
'relative_humidity_500',
|
| 45 |
+
|
| 46 |
+
'geopotential_50',
|
| 47 |
+
]
|
| 48 |
+
|
| 49 |
+
variables:
|
| 50 |
+
- "u10" # 10m U wind component
|
| 51 |
+
- "v10" # 10m V wind component
|
| 52 |
+
- "t2m" # 2m temperature
|
| 53 |
+
- "msl" # Mean sea level pressure
|
| 54 |
+
- "z500" # Geopotential at 500 hPa
|
| 55 |
+
- "t850" # Temperature at 850 hPa
|
| 56 |
+
|
| 57 |
+
# 时间配置
|
| 58 |
+
time_range: ["2000-01-01", "2020-12-31"]
|
| 59 |
+
time_steps: 1
|
| 60 |
+
time_res: 6
|
| 61 |
+
|
| 62 |
+
# 空间配置
|
| 63 |
+
spatial_resolution: [0.25, 0.25]
|
| 64 |
+
|
| 65 |
+
# 采样配置
|
| 66 |
+
num_samples: -1 # -1 表示使用全部数据
|
| 67 |
+
shuffle: true
|
| 68 |
+
random_seed: 42
|
| 69 |
+
|
| 70 |
+
# 领域特定配置
|
| 71 |
+
extra:
|
| 72 |
+
levels: [500, 850, 1000]
|
| 73 |
+
lat_range: [-90, 90]
|
| 74 |
+
lon_range: [0, 360]
|
| 75 |
+
|
| 76 |
+
# 数据转换配置
|
| 77 |
+
transforms:
|
| 78 |
+
- type: "Normalize"
|
| 79 |
+
params:
|
| 80 |
+
mean: [0.0, 0.0, 288.0, 101325.0, 50000.0, 270.0]
|
| 81 |
+
std: [5.0, 5.0, 15.0, 1000.0, 5000.0, 10.0]
|
| 82 |
+
keys: ["input", "target"]
|
| 83 |
+
|
| 84 |
+
- type: "ToTensor"
|
| 85 |
+
params:
|
| 86 |
+
keys: null # null表示转换所有numpy数组
|
| 87 |
+
|
| 88 |
+
# 其他配置
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
# DataLoader配置
|
| 92 |
+
dataloader:
|
| 93 |
+
mask_dtype: "float32"
|
| 94 |
+
batch_size: 4
|
| 95 |
+
num_workers: 1
|
| 96 |
+
pin_memory: true
|
| 97 |
+
drop_last: true
|
| 98 |
+
shuffle: false # 使用sampler时设为false
|
| 99 |
+
prefetch_factor: 2
|
| 100 |
+
persistent_workers: true
|
| 101 |
+
|
| 102 |
+
# 分布式配置
|
| 103 |
+
distributed:
|
| 104 |
+
enabled: true
|
| 105 |
+
sampler: "DistributedSampler"
|
| 106 |
+
rank: 0
|
| 107 |
+
world_size: 4
|
| 108 |
+
shuffle: true
|
| 109 |
+
seed: 42
|
| 110 |
+
drop_last: true
|
| 111 |
+
|
configuration.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"framework":"Pytorch","task":"other"}
|
model/fourcastnet.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn as nn
|
| 4 |
+
from timm.models.layers import trunc_normal_
|
| 5 |
+
from einops import rearrange
|
| 6 |
+
|
| 7 |
+
from onescience.modules.embedding.fourcastnetembedding import FourCastNetEmbedding
|
| 8 |
+
from onescience.modules.fuser.fourcastnetfuser import FourCastNetFuser
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class FourCastNet(nn.Module):
|
| 12 |
+
"""
|
| 13 |
+
FourCastNet 的主模型实现。
|
| 14 |
+
|
| 15 |
+
该模型使用以下组件完成输入编码与主干特征提取:
|
| 16 |
+
|
| 17 |
+
- `OneEmbedding(style="FourCastNetEmbedding")`
|
| 18 |
+
- 将二维气象场切分为二维 patch token 序列
|
| 19 |
+
- `OneFuser(style="FourCastNetFuser")`
|
| 20 |
+
- 在二维 patch 网格上重复执行 AFNO 频域混合与 MLP 通道混合
|
| 21 |
+
|
| 22 |
+
在当前实现中:
|
| 23 |
+
|
| 24 |
+
- 输入为二维单时刻气象场 `(Batch, Channels, Height, Width)`
|
| 25 |
+
- patch embedding 输出会加上可学习位置编码
|
| 26 |
+
- token 序列随后恢复成 `(PatchGridHeight, PatchGridWidth)` 二维网格
|
| 27 |
+
- 多层 `FourCastNetFuser` 在 patch 网格上完成主干特征提取
|
| 28 |
+
- 最终通过线性头恢复回目标变量场
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
img_size (tuple[int, int]):
|
| 32 |
+
输入空间尺寸 `(Height, Width)`。
|
| 33 |
+
patch_size (tuple[int, int]):
|
| 34 |
+
patch 切分尺寸 `(PatchHeight, PatchWidth)`。
|
| 35 |
+
in_chans (int):
|
| 36 |
+
输入变量通道数。
|
| 37 |
+
out_chans (int):
|
| 38 |
+
输出变量通道数。
|
| 39 |
+
embed_dim (int):
|
| 40 |
+
patch embedding 特征维度。
|
| 41 |
+
depth (int):
|
| 42 |
+
主干 `FourCastNetFuser` 堆叠层数。
|
| 43 |
+
mlp_ratio (float):
|
| 44 |
+
每层 MLP 隐层放大倍数。
|
| 45 |
+
drop_rate (float):
|
| 46 |
+
dropout 比例。
|
| 47 |
+
drop_path_rate (float):
|
| 48 |
+
按层递增的 Stochastic Depth 最大比例。
|
| 49 |
+
num_blocks (int):
|
| 50 |
+
AFNO 的通道分块数。
|
| 51 |
+
sparsity_threshold (float):
|
| 52 |
+
AFNO 的 soft shrink 阈值。
|
| 53 |
+
hard_thresholding_fraction (float):
|
| 54 |
+
AFNO 保留的频率模式比例。
|
| 55 |
+
"""
|
| 56 |
+
|
| 57 |
+
def __init__(
|
| 58 |
+
self,
|
| 59 |
+
img_size=(720, 1440),
|
| 60 |
+
patch_size=(8, 8),
|
| 61 |
+
in_chans=19,
|
| 62 |
+
out_chans=19,
|
| 63 |
+
embed_dim=768,
|
| 64 |
+
depth=12,
|
| 65 |
+
mlp_ratio=4.0,
|
| 66 |
+
drop_rate=0.0,
|
| 67 |
+
drop_path_rate=0.0,
|
| 68 |
+
num_blocks=8,
|
| 69 |
+
sparsity_threshold=0.01,
|
| 70 |
+
hard_thresholding_fraction=1.0,
|
| 71 |
+
):
|
| 72 |
+
super().__init__()
|
| 73 |
+
self.img_size = img_size
|
| 74 |
+
self.patch_size = patch_size
|
| 75 |
+
self.in_chans = in_chans
|
| 76 |
+
self.out_chans = out_chans
|
| 77 |
+
self.num_features = self.embed_dim = embed_dim
|
| 78 |
+
self.num_blocks = num_blocks
|
| 79 |
+
|
| 80 |
+
num_patches = (img_size[1] // patch_size[1]) * (img_size[0] // patch_size[0])
|
| 81 |
+
drop_path = np.linspace(0, drop_path_rate, depth).tolist()
|
| 82 |
+
|
| 83 |
+
self.pos_embed = nn.Parameter(torch.zeros(1, num_patches, embed_dim))
|
| 84 |
+
self.pos_drop = nn.Dropout(p=drop_rate)
|
| 85 |
+
self.patch_grid_height = img_size[0] // self.patch_size[0]
|
| 86 |
+
self.patch_grid_width = img_size[1] // self.patch_size[1]
|
| 87 |
+
|
| 88 |
+
self.patch_embed = FourCastNetEmbedding(
|
| 89 |
+
img_size=img_size,
|
| 90 |
+
patch_size=patch_size,
|
| 91 |
+
in_chans=in_chans,
|
| 92 |
+
embed_dim=embed_dim,
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
self.blocks = nn.ModuleList([
|
| 96 |
+
FourCastNetFuser(
|
| 97 |
+
dim=embed_dim,
|
| 98 |
+
mlp_ratio=mlp_ratio,
|
| 99 |
+
drop=drop_rate,
|
| 100 |
+
drop_path=drop_path[i],
|
| 101 |
+
num_blocks=num_blocks,
|
| 102 |
+
sparsity_threshold=sparsity_threshold,
|
| 103 |
+
hard_thresholding_fraction=hard_thresholding_fraction,
|
| 104 |
+
)
|
| 105 |
+
for i in range(depth)
|
| 106 |
+
])
|
| 107 |
+
|
| 108 |
+
self.head = nn.Linear(
|
| 109 |
+
embed_dim,
|
| 110 |
+
self.out_chans * self.patch_size[0] * self.patch_size[1],
|
| 111 |
+
bias=False,
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
trunc_normal_(self.pos_embed, std=0.02)
|
| 115 |
+
self.apply(self._init_weights)
|
| 116 |
+
|
| 117 |
+
def _init_weights(self, m):
|
| 118 |
+
if isinstance(m, nn.Linear):
|
| 119 |
+
trunc_normal_(m.weight, std=0.02)
|
| 120 |
+
if isinstance(m, nn.Linear) and m.bias is not None:
|
| 121 |
+
nn.init.constant_(m.bias, 0)
|
| 122 |
+
elif isinstance(m, nn.LayerNorm):
|
| 123 |
+
nn.init.constant_(m.bias, 0)
|
| 124 |
+
nn.init.constant_(m.weight, 1.0)
|
| 125 |
+
|
| 126 |
+
@torch.jit.ignore
|
| 127 |
+
def no_weight_decay(self):
|
| 128 |
+
return {'pos_embed', 'cls_token'}
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def forward(self, x):
|
| 132 |
+
"""
|
| 133 |
+
Args:
|
| 134 |
+
x (torch.Tensor):
|
| 135 |
+
输入张量,形状为 `(Batch, Channels, Height, Width)`。
|
| 136 |
+
|
| 137 |
+
Returns:
|
| 138 |
+
torch.Tensor:
|
| 139 |
+
输出张量,形状为 `(Batch, out_chans, Height, Width)`。
|
| 140 |
+
"""
|
| 141 |
+
Batch = x.shape[0]
|
| 142 |
+
|
| 143 |
+
x = self.patch_embed(x)
|
| 144 |
+
x = x + self.pos_embed
|
| 145 |
+
x = self.pos_drop(x)
|
| 146 |
+
|
| 147 |
+
x = x.reshape(Batch, self.patch_grid_height, self.patch_grid_width, self.embed_dim)
|
| 148 |
+
for blk in self.blocks:
|
| 149 |
+
x = blk(x)
|
| 150 |
+
|
| 151 |
+
x = self.head(x)
|
| 152 |
+
x = rearrange(
|
| 153 |
+
x,
|
| 154 |
+
"b h w (p1 p2 c_out) -> b c_out (h p1) (w p2)",
|
| 155 |
+
p1=self.patch_size[0],
|
| 156 |
+
p2=self.patch_size[1],
|
| 157 |
+
h=self.patch_grid_height,
|
| 158 |
+
w=self.patch_grid_width,
|
| 159 |
+
)
|
| 160 |
+
return x
|
scripts/fake_data.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 get_static(data_dir, var, name):
|
| 47 |
+
os.makedirs(data_dir, exist_ok=True)
|
| 48 |
+
ds = xr.Dataset(
|
| 49 |
+
data_vars={
|
| 50 |
+
f"{var}": (("valid_time", "latitude", "longitude"),
|
| 51 |
+
np.random.rand(1, 721, 1440).astype(np.float32))
|
| 52 |
+
},
|
| 53 |
+
coords={
|
| 54 |
+
"valid_time": ["2015-12-31"],
|
| 55 |
+
"latitude": np.linspace(90, -90, 721, dtype=np.float64),
|
| 56 |
+
"longitude": np.linspace(0, 359.75, 1440, dtype=np.float64),
|
| 57 |
+
"number": 0,
|
| 58 |
+
"expver": "",
|
| 59 |
+
},
|
| 60 |
+
attrs={
|
| 61 |
+
"GRIB_centre": "ecmf",
|
| 62 |
+
"GRIB_centreDescription": "European Centre for Medium-Range Weather Forecasts",
|
| 63 |
+
"GRIB_subCentre": "0",
|
| 64 |
+
"Conventions": "CF-1.7",
|
| 65 |
+
"institution": "European Centre for Medium-Range Weather Forecasts",
|
| 66 |
+
"history": "Generated manually",
|
| 67 |
+
}
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
ds.to_netcdf(f"{data_dir}/{name}.nc")
|
| 71 |
+
arr = np.random.randn(721, 1440).astype(np.float32)
|
| 72 |
+
np.save(f'{data_dir}/land_mask.npy', arr)
|
| 73 |
+
np.save(f'{data_dir}/soil_type.npy', arr)
|
| 74 |
+
np.save(f'{data_dir}/topography.npy', arr)
|
| 75 |
+
print(f"✅ Static data: {arr.shape}, dtype: {arr.dtype}, save to {data_dir}")
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
if __name__ == "__main__":
|
| 79 |
+
cfg_datapipe = YParams("conf/config.yaml", "datapipe")
|
| 80 |
+
|
| 81 |
+
if cfg_datapipe.dataset.data_dir.startswith("/public/") or cfg_datapipe.dataset.data_dir.startswith("/work2/"):
|
| 82 |
+
print("请检查 config,确保各 *_dir 指向本地测试路径而非生产路径。")
|
| 83 |
+
exit()
|
| 84 |
+
|
| 85 |
+
years = cfg_datapipe.dataset.train_time + cfg_datapipe.dataset.val_time + cfg_datapipe.dataset.test_time
|
| 86 |
+
atm_vars = cfg_datapipe.dataset.channels
|
| 87 |
+
|
| 88 |
+
generate_fake_h5(cfg_datapipe.dataset.data_dir, atm_vars, years, DATASET_DIMS)
|
| 89 |
+
|
| 90 |
+
static_dir = os.path.join(cfg_datapipe.dataset.data_dir, "static")
|
| 91 |
+
get_static(static_dir, 'z', 'geopotential')
|
| 92 |
+
get_static(static_dir, 'lsm', 'land_sea_mask')
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
print("\n✅ Fake datasets generated.")
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 glob
|
| 10 |
+
import numpy as np
|
| 11 |
+
import h5py
|
| 12 |
+
from tqdm import tqdm
|
| 13 |
+
from model.fourcastnet import FourCastNet
|
| 14 |
+
from onescience.utils.YParams import YParams
|
| 15 |
+
from onescience.datapipes.climate import ERA5Datapipe
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def get_stats(data_dir, channels):
|
| 19 |
+
"""从新版 h5 中读取变量列表与归一化参数(均值/标准差)"""
|
| 20 |
+
h5_files = sorted(glob.glob(os.path.join(data_dir, "data", "*.h5")))
|
| 21 |
+
with h5py.File(h5_files[0], "r") as f:
|
| 22 |
+
ds = f["fields"]
|
| 23 |
+
all_variables = [v.decode() if isinstance(v, bytes) else v for v in ds.attrs["variables"]]
|
| 24 |
+
mu = f["global_means"][:] # [1, C, 1, 1]
|
| 25 |
+
std = f["global_stds"][:]
|
| 26 |
+
|
| 27 |
+
channel_indices = [all_variables.index(v) for v in channels]
|
| 28 |
+
means = mu[:, channel_indices, :, :]
|
| 29 |
+
stds = std[:, channel_indices, :, :]
|
| 30 |
+
return means, stds
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
if __name__ == "__main__":
|
| 34 |
+
current_path = os.getcwd()
|
| 35 |
+
sys.path.append(current_path)
|
| 36 |
+
|
| 37 |
+
## Model config init
|
| 38 |
+
config_file_path = os.path.join(current_path, "conf/config.yaml")
|
| 39 |
+
cfg = YParams(config_file_path, "model")
|
| 40 |
+
|
| 41 |
+
## DataLoader init
|
| 42 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 43 |
+
means, stds = get_stats(cfg_data.dataset.data_dir, cfg_data.dataset.channels)
|
| 44 |
+
|
| 45 |
+
cfg['N_in_channels'] = len(cfg_data.dataset.channels)
|
| 46 |
+
cfg['N_out_channels'] = len(cfg_data.dataset.channels)
|
| 47 |
+
|
| 48 |
+
datapipe = ERA5Datapipe(
|
| 49 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 50 |
+
used_variables=cfg_data.dataset.channels,
|
| 51 |
+
used_years=cfg_data.dataset.test_time,
|
| 52 |
+
distributed=False,
|
| 53 |
+
batch_size=1,
|
| 54 |
+
num_workers=4,
|
| 55 |
+
)
|
| 56 |
+
test_dataloader, _ = datapipe.get_dataloader("test")
|
| 57 |
+
|
| 58 |
+
ckpt = torch.load(f"{cfg.checkpoint_dir}/model_bak.pth", map_location="cuda:0")
|
| 59 |
+
model = FourCastNet().to('cuda:0')
|
| 60 |
+
model.load_state_dict(ckpt["model_state_dict"])
|
| 61 |
+
|
| 62 |
+
model.eval()
|
| 63 |
+
os.makedirs('result/output/', exist_ok=True)
|
| 64 |
+
print(f"📂 infer results will be generated to './result/output/'")
|
| 65 |
+
with torch.no_grad():
|
| 66 |
+
for data in tqdm(test_dataloader, desc="Inferring testset", unit="batch"):
|
| 67 |
+
invar = data[0].to('cuda:0', dtype=torch.float32)
|
| 68 |
+
filename = data[4][-1][0]
|
| 69 |
+
invar = invar[:, :, :-1, :]
|
| 70 |
+
pred_var = model(invar).cpu().numpy()
|
| 71 |
+
pred_var = pred_var * stds + means
|
| 72 |
+
np.save(f"result/output/{filename}.npy", pred_var)
|
scripts/result.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
import glob
|
| 6 |
+
import h5py
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
from tqdm import tqdm
|
| 9 |
+
from onescience.utils.fcn.YParams import YParams
|
| 10 |
+
from matplotlib import rcParams
|
| 11 |
+
|
| 12 |
+
# rcParams['font.family'] = 'serif'
|
| 13 |
+
# rcParams['font.serif'] = ['DejaVu Serif']
|
| 14 |
+
rcParams['mathtext.fontset'] = 'stix'
|
| 15 |
+
rcParams['axes.linewidth'] = 0.9
|
| 16 |
+
rcParams['xtick.major.width'] = 0.9
|
| 17 |
+
rcParams['ytick.major.width'] = 0.9
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def get_metadata(data_dir, channels):
|
| 21 |
+
"""从新版 h5 attrs 中读取变量列表和 time_step"""
|
| 22 |
+
h5_files = sorted(glob.glob(os.path.join(data_dir, "data", "*.h5")))
|
| 23 |
+
with h5py.File(h5_files[0], "r") as f:
|
| 24 |
+
ds = f["fields"]
|
| 25 |
+
all_variables = [v.decode() if isinstance(v, bytes) else v for v in ds.attrs["variables"]]
|
| 26 |
+
time_step = int(ds.attrs["time_step"])
|
| 27 |
+
|
| 28 |
+
channel_indices = [all_variables.index(v) for v in channels]
|
| 29 |
+
|
| 30 |
+
total_files = [f for f in os.listdir('./result/output/') if f.endswith('.npy')]
|
| 31 |
+
total_files.sort()
|
| 32 |
+
return total_files, channel_indices, time_step
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def filename_to_index(filename, time_step):
|
| 36 |
+
"""将 YYYYMMDDHH 格式的文件名转换为年度 h5 文件中的时间步索引"""
|
| 37 |
+
dt = datetime.strptime(filename, "%Y%m%d%H")
|
| 38 |
+
year_start = datetime(dt.year, 1, 1)
|
| 39 |
+
hours = (dt - year_start).total_seconds() / 3600
|
| 40 |
+
return int(hours / time_step)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def get_result(total_files, channel_indices, time_step, data_dir, clim_mean):
|
| 44 |
+
channel_rmse = np.zeros(len(channel_indices))
|
| 45 |
+
channel_acc = np.zeros(len(channel_indices))
|
| 46 |
+
clim_mean = clim_mean[0, :, :, :]
|
| 47 |
+
if not os.path.exists('./result/rmse.npy') or not os.path.exists('result/acc.npy'):
|
| 48 |
+
numerator = np.zeros(len(channel_indices))
|
| 49 |
+
pred_sq_sum = np.zeros(len(channel_indices))
|
| 50 |
+
label_sq_sum = np.zeros(len(channel_indices))
|
| 51 |
+
for file in tqdm(total_files, unit="files"):
|
| 52 |
+
fname = file[:-4] # 去掉 .npy
|
| 53 |
+
year = fname[:4]
|
| 54 |
+
t_idx = filename_to_index(fname, time_step)
|
| 55 |
+
with h5py.File(os.path.join(data_dir, 'data', f'{year}.h5'), "r") as f:
|
| 56 |
+
label = f["fields"][t_idx] # [C, H, W]
|
| 57 |
+
label = label[channel_indices]
|
| 58 |
+
label = label[:, :-1, :]
|
| 59 |
+
pred = np.load(f'result/output/{file}').squeeze()
|
| 60 |
+
|
| 61 |
+
label_anom = label - clim_mean
|
| 62 |
+
pred_anom = pred - clim_mean
|
| 63 |
+
# 累加
|
| 64 |
+
numerator += np.sum(pred_anom * label_anom, axis=(1, 2))
|
| 65 |
+
pred_sq_sum += np.sum(pred_anom ** 2, axis=(1, 2))
|
| 66 |
+
label_sq_sum += np.sum(label_anom ** 2, axis=(1, 2))
|
| 67 |
+
|
| 68 |
+
channel_rmse += np.sqrt(np.mean((label - pred) ** 2, axis=(1, 2)))
|
| 69 |
+
channel_rmse /= len(total_files)
|
| 70 |
+
channel_acc = numerator / (np.sqrt(pred_sq_sum * label_sq_sum) + 1e-8)
|
| 71 |
+
np.save('./result/acc.npy', channel_acc)
|
| 72 |
+
np.save('./result/rmse.npy', channel_rmse)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def show_result():
|
| 76 |
+
channel_rmse = np.load('./result/rmse.npy')
|
| 77 |
+
channel_acc = np.load('./result/acc.npy')
|
| 78 |
+
|
| 79 |
+
channels = [cfg_data.dataset.channels[i] for i in range(len(channel_indices))]
|
| 80 |
+
w = 24 # 最长 channel 名宽度
|
| 81 |
+
|
| 82 |
+
# 表头
|
| 83 |
+
print(f"┌{'─' * (w + 2)}┬{'─' * 14}┬{'─' * 14}┐")
|
| 84 |
+
print(f"│ {'Channel':<{w}} │ {'RMSE':>12} │ {'ACC':>12} │")
|
| 85 |
+
print(f"├{'─' * (w + 2)}┼{'─' * 14}┼{'─' * 14}┤")
|
| 86 |
+
# 数据行
|
| 87 |
+
for i, ch in enumerate(channels):
|
| 88 |
+
print(f"│ {ch:<{w}} │ {channel_rmse[i]:>12.4f} | {channel_acc[i]:>12.4f} |")
|
| 89 |
+
print(f"├{'─' * (w + 2)}┼{'─' * 14}┼{'─' * 14}┤")
|
| 90 |
+
print(f"│ {'Average':<{w}} │ {np.mean(channel_rmse):>12.4f} │ {np.mean(channel_acc):>12.4f} │")
|
| 91 |
+
print(f"└{'─' * (w + 2)}┴{'─' * 14}┴{'─' * 14}┘")
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def plot(label, pred, var, filename):
|
| 95 |
+
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
|
| 96 |
+
|
| 97 |
+
xtick_labels = ['180°W', '90°W', '0°', '90°E', '180°E']
|
| 98 |
+
ytick_labels = ['90°S', '45°S', '0°', '45°N', '90°N']
|
| 99 |
+
xticks = np.linspace(0, label.shape[-1] - 1, 5)
|
| 100 |
+
yticks = np.linspace(0, label.shape[-2] - 1, 5)
|
| 101 |
+
|
| 102 |
+
vmin = min(label.min(), pred.min())
|
| 103 |
+
vmax = max(label.max(), pred.max())
|
| 104 |
+
|
| 105 |
+
diff = label - pred
|
| 106 |
+
rmse = np.sqrt(np.mean(diff ** 2))
|
| 107 |
+
diff_abs_max = np.abs(diff).max()
|
| 108 |
+
|
| 109 |
+
plot_configs = [
|
| 110 |
+
{'data': label, 'title': 'Truth', 'cmap': 'viridis', 'vmin': vmin, 'vmax': vmax},
|
| 111 |
+
{'data': pred, 'title': 'Prediction', 'cmap': 'viridis', 'vmin': vmin, 'vmax': vmax},
|
| 112 |
+
{'data': diff, 'title': f'Difference (RMSE={rmse:.2f})', 'cmap': 'RdBu_r', 'vmin': -diff_abs_max, 'vmax': diff_abs_max},
|
| 113 |
+
]
|
| 114 |
+
|
| 115 |
+
for ax, cfg in zip(axes, plot_configs):
|
| 116 |
+
im = ax.imshow(cfg['data'], cmap=cfg['cmap'], vmin=cfg['vmin'], vmax=cfg['vmax'])
|
| 117 |
+
ax.set_title(cfg['title'], fontsize=12, pad=4)
|
| 118 |
+
ax.set_xlabel('Longitude')
|
| 119 |
+
ax.set_ylabel('Latitude')
|
| 120 |
+
ax.set_xticks(xticks)
|
| 121 |
+
ax.set_xticklabels(xtick_labels)
|
| 122 |
+
ax.set_yticks(yticks)
|
| 123 |
+
ax.set_yticklabels(ytick_labels)
|
| 124 |
+
plt.colorbar(im, ax=ax, orientation='horizontal')
|
| 125 |
+
|
| 126 |
+
fig.suptitle(var, fontsize=14, fontweight='bold', y=0.98)
|
| 127 |
+
plt.savefig(filename, dpi=300, bbox_inches='tight')
|
| 128 |
+
plt.close()
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def plot_loss(train_loss, valid_loss):
|
| 132 |
+
mask = ~(np.isnan(train_loss) | np.isnan(valid_loss))
|
| 133 |
+
train_loss = train_loss[mask]
|
| 134 |
+
valid_loss = valid_loss[mask]
|
| 135 |
+
|
| 136 |
+
fig, ax = plt.subplots(figsize=(5, 3.5))
|
| 137 |
+
colors = {'train': '#2563EB', 'valid': '#EA580C'}
|
| 138 |
+
epochs = np.arange(1, len(train_loss) + 1)
|
| 139 |
+
|
| 140 |
+
ax.plot(epochs, train_loss, color=colors['train'], linewidth=1.5, label='Train')
|
| 141 |
+
ax.plot(epochs, valid_loss, color=colors['valid'], linewidth=1.5, label='Valid', linestyle='--')
|
| 142 |
+
min_idx = np.argmin(valid_loss)
|
| 143 |
+
ax.scatter(epochs[min_idx], valid_loss[min_idx],
|
| 144 |
+
color=colors['valid'], s=40, zorder=5, edgecolors='white')
|
| 145 |
+
ax.annotate(f'Best: {valid_loss[min_idx]:.3f}',
|
| 146 |
+
xy=(epochs[min_idx], valid_loss[min_idx]),
|
| 147 |
+
xytext=(10, 10), textcoords='offset points', fontsize=8, color=colors['valid'],
|
| 148 |
+
arrowprops=dict(arrowstyle='-', color=colors['valid'], lw=0.5))
|
| 149 |
+
|
| 150 |
+
ax.set(xlabel='Epoch', ylabel='Loss', xlim=(0, len(train_loss) + 1))
|
| 151 |
+
ax.legend(frameon=False, loc='upper right')
|
| 152 |
+
ax.grid(True, linestyle='--', alpha=0.3)
|
| 153 |
+
ax.spines[['top', 'right']].set_visible(False)
|
| 154 |
+
|
| 155 |
+
plt.tight_layout()
|
| 156 |
+
plt.savefig('./result/loss.png', dpi=300, bbox_inches='tight')
|
| 157 |
+
plt.close()
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
if __name__ == "__main__":
|
| 161 |
+
current_path = os.getcwd()
|
| 162 |
+
sys.path.append(current_path)
|
| 163 |
+
config_file_path = os.path.join(current_path, 'conf/config.yaml')
|
| 164 |
+
cfg = YParams(config_file_path, 'model')
|
| 165 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 166 |
+
|
| 167 |
+
train_loss = np.load('./data/checkpoints/trloss.npy')
|
| 168 |
+
valid_loss = np.load('./data/checkpoints/valoss.npy')
|
| 169 |
+
plot_loss(train_loss, valid_loss)
|
| 170 |
+
|
| 171 |
+
data_dir = cfg_data.dataset.data_dir
|
| 172 |
+
total_files, channel_indices, time_step = get_metadata(data_dir, cfg_data.dataset.channels)
|
| 173 |
+
|
| 174 |
+
# Load data & Compute RMSE/ACC per channel
|
| 175 |
+
h5_files = sorted(glob.glob(os.path.join(data_dir, "data", "*.h5")))
|
| 176 |
+
with h5py.File(h5_files[0], "r") as f:
|
| 177 |
+
mu = f["global_means"][:]
|
| 178 |
+
clim_mean = mu[:, channel_indices, :, :]
|
| 179 |
+
get_result(total_files, channel_indices, time_step, data_dir, clim_mean)
|
| 180 |
+
show_result()
|
| 181 |
+
|
| 182 |
+
##### 默认绘制 test_time 第一年的第一个时间步,用户可自行指定日期和变量 #####
|
| 183 |
+
test_year = cfg_data.dataset.test_time[0]
|
| 184 |
+
eg_files = [f'{test_year}010206']
|
| 185 |
+
channel_index = [cfg_data.dataset.channels.index(v) for v in ['2m_temperature', 'geopotential_500', 'temperature_500']]
|
| 186 |
+
|
| 187 |
+
selected_var = [cfg_data.dataset.channels[int(i)] for i in channel_index]
|
| 188 |
+
print(f"seleted date: {eg_files}")
|
| 189 |
+
print(f"selected channels: {selected_var}")
|
| 190 |
+
for file in eg_files:
|
| 191 |
+
year = file[:4]
|
| 192 |
+
t_idx = filename_to_index(file, time_step)
|
| 193 |
+
with h5py.File(os.path.join(data_dir, 'data', f'{year}.h5'), "r") as f:
|
| 194 |
+
label = f["fields"][t_idx] # [C, H, W]
|
| 195 |
+
label = label[channel_indices]
|
| 196 |
+
label = label[:, :-1, :]
|
| 197 |
+
pred = np.load(f'result/output/{file}.npy').squeeze()
|
| 198 |
+
for i in range(len(selected_var)):
|
| 199 |
+
filename = f'./result/{file}_{selected_var[i]}.png'
|
| 200 |
+
plot(label[channel_index[i]], pred[channel_index[i]], selected_var[i], filename)
|
| 201 |
+
print(f'✅plot {filename}')
|
scripts/train.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 torch.nn.parallel import DistributedDataParallel
|
| 15 |
+
from model.fourcastnet import FourCastNet
|
| 16 |
+
from onescience.datapipes.climate import ERA5Datapipe
|
| 17 |
+
from onescience.utils.YParams import YParams
|
| 18 |
+
from onescience.utils.fcn.darcy_loss import LpLoss
|
| 19 |
+
|
| 20 |
+
from apex import optimizers
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def main():
|
| 24 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
| 25 |
+
logger = logging.getLogger()
|
| 26 |
+
|
| 27 |
+
## Model config init
|
| 28 |
+
config_file_path = os.path.join(current_path, "conf/config.yaml")
|
| 29 |
+
cfg = YParams(config_file_path, "model")
|
| 30 |
+
|
| 31 |
+
## Distributed config init
|
| 32 |
+
cfg.world_size = 1
|
| 33 |
+
if "WORLD_SIZE" in os.environ:
|
| 34 |
+
cfg.world_size = int(os.environ["WORLD_SIZE"])
|
| 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 |
+
## DataLoader init
|
| 43 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 44 |
+
cfg['N_in_channels'] = len(cfg_data.dataset.channels)
|
| 45 |
+
cfg['N_out_channels'] = len(cfg_data.dataset.channels)
|
| 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 |
+
)
|
| 52 |
+
train_dataloader, train_sampler = datapipe.get_dataloader("train")
|
| 53 |
+
datapipe = ERA5Datapipe(
|
| 54 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 55 |
+
used_variables=cfg_data.dataset.channels,
|
| 56 |
+
used_years=cfg_data.dataset.val_time,
|
| 57 |
+
distributed=dist.is_initialized()
|
| 58 |
+
)
|
| 59 |
+
val_dataloader, val_sampler = datapipe.get_dataloader("valid")
|
| 60 |
+
|
| 61 |
+
# Model init
|
| 62 |
+
model = FourCastNet().to(local_rank)
|
| 63 |
+
optimizer = optimizers.FusedAdam(model.parameters(), lr=cfg.lr)
|
| 64 |
+
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, factor=0.2, patience=5, mode='min')
|
| 65 |
+
loss_obj = LpLoss()
|
| 66 |
+
|
| 67 |
+
## Train process init
|
| 68 |
+
os.makedirs(cfg.checkpoint_dir, exist_ok=True)
|
| 69 |
+
train_loss_file = f"{cfg.checkpoint_dir}/trloss.npy"
|
| 70 |
+
valid_loss_file = f"{cfg.checkpoint_dir}/valoss.npy"
|
| 71 |
+
best_valid_loss = 1.0e6
|
| 72 |
+
best_loss_epoch = 0
|
| 73 |
+
train_losses = np.empty((0,), dtype=np.float32)
|
| 74 |
+
valid_losses = np.empty((0,), dtype=np.float32)
|
| 75 |
+
|
| 76 |
+
## Get model params count
|
| 77 |
+
if cfg.world_size == 1:
|
| 78 |
+
total_params = sum(p.numel() for p in model.parameters())
|
| 79 |
+
print("\n\n")
|
| 80 |
+
print("-" * 50)
|
| 81 |
+
print(f"📂 now params is {total_params}, {total_params / 1e6:.2f}M, {total_params / 1e9:.2f}B")
|
| 82 |
+
print("-" * 50, "\n")
|
| 83 |
+
|
| 84 |
+
## Load model weight if there exist well-trained model
|
| 85 |
+
if os.path.exists(f"{cfg.checkpoint_dir}/model_bak.pth"):
|
| 86 |
+
if world_rank == 0:
|
| 87 |
+
print("\n\n")
|
| 88 |
+
print("-" * 50)
|
| 89 |
+
print(f"✅ There has a model weight, load and continue training...")
|
| 90 |
+
print(f'If you want to train a new model, ensure there is no *.pth file in {cfg.checkpoint_dir}')
|
| 91 |
+
print("-" * 50, "\n")
|
| 92 |
+
ckpt = torch.load(f"{cfg.checkpoint_dir}/model_bak.pth", map_location=f'cuda:{local_rank}', weights_only=False)
|
| 93 |
+
model.load_state_dict(ckpt["model_state_dict"])
|
| 94 |
+
optimizer.load_state_dict(ckpt["optimizer_state_dict"])
|
| 95 |
+
scheduler.load_state_dict(ckpt["scheduler_state_dict"])
|
| 96 |
+
best_valid_loss = ckpt["best_valid_loss"]
|
| 97 |
+
best_loss_epoch = ckpt["best_loss_epoch"]
|
| 98 |
+
train_losses = np.load(train_loss_file)
|
| 99 |
+
valid_losses = np.load(valid_loss_file)
|
| 100 |
+
|
| 101 |
+
## Distributed model
|
| 102 |
+
if cfg.world_size > 1:
|
| 103 |
+
model = DistributedDataParallel(model, device_ids=[local_rank], output_device=local_rank)
|
| 104 |
+
world_rank == 0 and logger.info(f"start training ...")
|
| 105 |
+
|
| 106 |
+
for epoch in range(cfg.max_epoch):
|
| 107 |
+
if dist.is_initialized():
|
| 108 |
+
train_sampler.set_epoch(epoch)
|
| 109 |
+
val_sampler.set_epoch(epoch)
|
| 110 |
+
model.train()
|
| 111 |
+
train_loss = 0
|
| 112 |
+
start_time = time.time()
|
| 113 |
+
for j, data in enumerate(train_dataloader):
|
| 114 |
+
invar = data[0].to(local_rank, dtype=torch.float32)
|
| 115 |
+
outvar = data[1].to(local_rank, dtype=torch.float32)
|
| 116 |
+
invar = invar[:, :, :-1, :]
|
| 117 |
+
outvar = outvar[:, :, :-1, :]
|
| 118 |
+
outvar_pred = model(invar)
|
| 119 |
+
loss = loss_obj(outvar, outvar_pred)
|
| 120 |
+
optimizer.zero_grad()
|
| 121 |
+
loss.backward()
|
| 122 |
+
optimizer.step()
|
| 123 |
+
train_loss += loss.item()
|
| 124 |
+
if world_rank == 0:
|
| 125 |
+
logger.info(f'Train: Epoch {epoch}-{j+1}/{len(train_dataloader)} '
|
| 126 |
+
f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
|
| 127 |
+
f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
|
| 128 |
+
f'loss:{train_loss / (j+1): .04f}')
|
| 129 |
+
|
| 130 |
+
train_loss /= len(train_dataloader)
|
| 131 |
+
|
| 132 |
+
model.eval()
|
| 133 |
+
valid_loss = 0
|
| 134 |
+
with torch.no_grad():
|
| 135 |
+
start_time = time.time()
|
| 136 |
+
for j, data in enumerate(val_dataloader):
|
| 137 |
+
invar = data[0].to(local_rank, dtype=torch.float32)
|
| 138 |
+
outvar = data[1].to(local_rank, dtype=torch.float32)
|
| 139 |
+
invar = invar[:, :, :-1, :]
|
| 140 |
+
outvar = outvar[:, :, :-1, :]
|
| 141 |
+
outvar_pred = model(invar)
|
| 142 |
+
loss = loss_obj(outvar, outvar_pred)
|
| 143 |
+
|
| 144 |
+
if cfg.world_size > 1:
|
| 145 |
+
loss_tensor = loss.detach().to(local_rank) # torch.tensor(loss, device=local_rank)
|
| 146 |
+
dist.all_reduce(loss_tensor)
|
| 147 |
+
loss = loss_tensor.item() / cfg.world_size
|
| 148 |
+
valid_loss += loss
|
| 149 |
+
else:
|
| 150 |
+
valid_loss += loss.item()
|
| 151 |
+
if world_rank == 0:
|
| 152 |
+
logger.info(f'Valid: Epoch {epoch}-{j+1}/{len(val_dataloader)} '
|
| 153 |
+
f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
|
| 154 |
+
f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
|
| 155 |
+
f'loss:{valid_loss / (j+1): .04f}')
|
| 156 |
+
|
| 157 |
+
valid_loss /= len(val_dataloader)
|
| 158 |
+
is_save_ckp = False
|
| 159 |
+
if valid_loss < best_valid_loss:
|
| 160 |
+
best_valid_loss = valid_loss
|
| 161 |
+
best_loss_epoch = epoch
|
| 162 |
+
world_rank == 0 and save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, cfg.checkpoint_dir)
|
| 163 |
+
is_save_ckp = True
|
| 164 |
+
scheduler.step(valid_loss)
|
| 165 |
+
|
| 166 |
+
if world_rank == 0:
|
| 167 |
+
logger.info(f"Epoch [{epoch + 1}/{cfg.max_epoch}], "
|
| 168 |
+
f"Train Loss: {train_loss:.4f}, "
|
| 169 |
+
f"Valid Loss: {valid_loss:.4f}, "
|
| 170 |
+
f"Best loss at Epoch: {best_loss_epoch + 1}"
|
| 171 |
+
+ (", saving checkpoint" if is_save_ckp else "")
|
| 172 |
+
)
|
| 173 |
+
train_losses = np.append(train_losses, train_loss)
|
| 174 |
+
valid_losses = np.append(valid_losses, valid_loss)
|
| 175 |
+
np.save(train_loss_file, train_losses)
|
| 176 |
+
np.save(valid_loss_file, valid_losses)
|
| 177 |
+
|
| 178 |
+
if epoch - best_loss_epoch > cfg.patience:
|
| 179 |
+
print(f"Loss has not decrease in {cfg.patience} epochs, stopping training...")
|
| 180 |
+
exit()
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, model_path):
|
| 184 |
+
model_to_save = model.module if hasattr(model, "module") else model
|
| 185 |
+
state = {"model_state_dict": model_to_save.state_dict(),
|
| 186 |
+
"optimizer_state_dict": optimizer.state_dict(),
|
| 187 |
+
"scheduler_state_dict": scheduler.state_dict(),
|
| 188 |
+
"best_valid_loss": best_valid_loss,
|
| 189 |
+
"best_loss_epoch": best_loss_epoch,
|
| 190 |
+
}
|
| 191 |
+
torch.save(state, f"{model_path}/model.pth")
|
| 192 |
+
### the weight file saving may interrupted due to DCU queue limit, get a backup to ensure there at least has one model
|
| 193 |
+
os.system(f"mv {model_path}/model.pth {model_path}/model_bak.pth")
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
if __name__ == "__main__":
|
| 197 |
+
current_path = os.getcwd()
|
| 198 |
+
sys.path.append(current_path)
|
| 199 |
+
main()
|
weight/.gitkeep
ADDED
|
File without changes
|