OneScience commited on
Commit
2429091
·
verified ·
1 Parent(s): 76b95dc

Upload folder using huggingface_hub

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