OneScience commited on
Commit
ff5b2f1
·
verified ·
1 Parent(s): 5575fc9

Upload folder using huggingface_hub

Browse files
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