Publish NNCAM reproduction
Browse files- .gitattributes +7 -35
- README.md +148 -0
- README_zh.md +149 -0
- conf/config.yaml +39 -0
- config.json +42 -0
- configuration.json +12 -0
- model/nncam.py +88 -0
- scripts/fake_data.py +28 -0
- scripts/inference.py +42 -0
- scripts/result.py +51 -0
- scripts/train.py +75 -0
- weight/.gitkeep +0 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,7 @@
|
|
| 1 |
-
*.
|
| 2 |
-
*.
|
| 3 |
-
*.
|
| 4 |
-
*.
|
| 5 |
-
*.
|
| 6 |
-
*.
|
| 7 |
-
*.
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
*.py text eol=lf
|
| 2 |
+
*.md text eol=lf
|
| 3 |
+
*.json text eol=lf
|
| 4 |
+
*.yaml text eol=lf
|
| 5 |
+
*.npz binary
|
| 6 |
+
*.pt binary
|
| 7 |
+
*.png binary
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
tags:
|
| 6 |
+
- OneScience
|
| 7 |
+
- Earth Science
|
| 8 |
+
- Climate Parameterization
|
| 9 |
+
- Subgrid Processes
|
| 10 |
+
frameworks: PyTorch
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
<p align="center">
|
| 14 |
+
<strong><span style="font-size: 30px;">NNCAM</span></strong>
|
| 15 |
+
</p>
|
| 16 |
+
|
| 17 |
+
# Model Introduction
|
| 18 |
+
|
| 19 |
+
NNCAM predicts physical tendencies and fluxes produced by cloud, convection, and radiation subgrid processes from atmospheric-column states for data-driven climate-model parameterization research.
|
| 20 |
+
|
| 21 |
+
Paper: Deep learning to represent subgrid processes in climate models
|
| 22 |
+
https://gmd.copernicus.org/articles/11/3999/2018/
|
| 23 |
+
|
| 24 |
+
# Model Description
|
| 25 |
+
|
| 26 |
+
The method was proposed by research teams from Ludwig Maximilian University of Munich, the University of California Irvine, and Columbia University. The paper trains on approximately 140 million atmospheric-column samples from one year of SPCAM aquaplanet simulation. The model predicts 65 heating, moistening, radiative-flux, and precipitation outputs from a 94-dimensional atmospheric-column state.
|
| 27 |
+
|
| 28 |
+
# Use Cases
|
| 29 |
+
|
| 30 |
+
| Use Case | Description |
|
| 31 |
+
| :---: | :--- |
|
| 32 |
+
| Subgrid-process parameterization | Predict physical tendencies and fluxes from temperature, humidity, wind, and surface forcing. |
|
| 33 |
+
| Atmospheric-column diagnostics | Validate heating, moistening, radiation, and precipitation relationships over 30 levels. |
|
| 34 |
+
| Local engineering validation | Validate training, inference, conservation diagnostics, and visualization with structured synthetic samples. |
|
| 35 |
+
| ModelScope/OneCode execution | Validate structured data, training, inference, parameterization metrics, and visualization in ModelScope or OneCode environments. |
|
| 36 |
+
| Multi-GPU training | Validate distributed training and the checkpoint workflow through `torchrun`. |
|
| 37 |
+
|
| 38 |
+
# Usage Instructions
|
| 39 |
+
|
| 40 |
+
## 1.OneCode
|
| 41 |
+
|
| 42 |
+
Experience intelligent, one-click AI4S programming through the OneCode online environment:
|
| 43 |
+
|
| 44 |
+
[Try intelligent, one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 45 |
+
|
| 46 |
+
## 2. Download and Installation
|
| 47 |
+
|
| 48 |
+
```bash
|
| 49 |
+
hf download OneScience-Group/NNCAM --local-dir ./NNCAM
|
| 50 |
+
cd NNCAM
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
### Environment Dependencies
|
| 54 |
+
|
| 55 |
+
**Hardware Requirements**
|
| 56 |
+
|
| 57 |
+
- A GPU or DCU is recommended.
|
| 58 |
+
- A CPU can be used for connectivity validation with the default small-sample configuration.
|
| 59 |
+
- DCU users must install DTK in advance. DTK 25.04.2 or later, or the OneScience-recommended version matching the current cluster, is recommended.
|
| 60 |
+
|
| 61 |
+
**DCU Environment**
|
| 62 |
+
|
| 63 |
+
```bash
|
| 64 |
+
# Activate DTK and Conda first
|
| 65 |
+
conda create -n onescience311 python=3.11 -y
|
| 66 |
+
conda activate onescience311
|
| 67 |
+
pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
**GPU Environment**
|
| 71 |
+
|
| 72 |
+
```bash
|
| 73 |
+
# Activate Conda first
|
| 74 |
+
conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
|
| 75 |
+
conda activate onescience311
|
| 76 |
+
pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
### Training Data
|
| 80 |
+
|
| 81 |
+
The paper uses SPCAM aquaplanet simulations with a 30-minute timestep and 30 vertical levels. Inputs are `[B,94]` temperature, humidity, wind, and surface-forcing columns, and targets are `[B,65]` heating, moistening, four radiative fluxes, and precipitation. This repository uses a small structured synthetic dataset for engineering validation only and does not represent the real SPCAM distribution, training scale, or paper performance.
|
| 82 |
+
|
| 83 |
+
```bash
|
| 84 |
+
python scripts/fake_data.py
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
### Training
|
| 88 |
+
|
| 89 |
+
For single-GPU training, use:
|
| 90 |
+
|
| 91 |
+
```bash
|
| 92 |
+
python scripts/train.py
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
For multi-GPU training, use:
|
| 96 |
+
|
| 97 |
+
```bash
|
| 98 |
+
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
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
The default reduces the paper's nine 256-node layers to four 32-node layers and shortens training without reducing the 94 inputs, 65 outputs, or 30-level vertical protocol. Formal experiments require real SPCAM data and the paper-scale model, with artifacts saved to:
|
| 102 |
+
|
| 103 |
+
```text
|
| 104 |
+
result/checkpoints/nncam.pt
|
| 105 |
+
result/training/metrics.json
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
### Trained Weights
|
| 109 |
+
|
| 110 |
+
The paper does not provide directly loadable official model weights, and this repository bundles no weights under `weight/`. The locally trained checkpoint is saved to `result/checkpoints/nncam.pt` and must not be represented as an official pretrained weight.
|
| 111 |
+
|
| 112 |
+
### Inference
|
| 113 |
+
|
| 114 |
+
```bash
|
| 115 |
+
python scripts/inference.py
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
Inference loads the training checkpoint and generates subgrid tendencies, radiative fluxes, and precipitation from complete atmospheric-column states. Complete numerical outputs are saved to:
|
| 119 |
+
|
| 120 |
+
```text
|
| 121 |
+
result/output/predictions.npz
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
### Evaluation and Visualization
|
| 125 |
+
|
| 126 |
+
```bash
|
| 127 |
+
python scripts/result.py
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
Evaluation computes grouped RMSE and R² and generates grouped-error and precipitation-prediction comparisons. Synthetic-data results validate engineering only and do not represent paper performance; outputs are saved to:
|
| 131 |
+
|
| 132 |
+
```text
|
| 133 |
+
result/evaluation/metrics.json
|
| 134 |
+
result/evaluation/comparison.png
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
# Official OneScience Information
|
| 138 |
+
|
| 139 |
+
| Platform | OneScience Main Repository | Skills Repository |
|
| 140 |
+
| --- | --- | --- |
|
| 141 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 142 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 143 |
+
|
| 144 |
+
# Citation and License
|
| 145 |
+
|
| 146 |
+
This repository is an independent engineering reproduction of the public NNCAM specifications.
|
| 147 |
+
|
| 148 |
+
Use of this repository's code, official model weights, and data remains subject to the licenses and terms of their respective projects.
|
README_zh.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
language:
|
| 4 |
+
- zh
|
| 5 |
+
- en
|
| 6 |
+
tags:
|
| 7 |
+
- OneScience
|
| 8 |
+
- 地球科学
|
| 9 |
+
- 气候参数化
|
| 10 |
+
- 次网格过程
|
| 11 |
+
frameworks: PyTorch
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
<p align="center">
|
| 15 |
+
<strong><span style="font-size: 30px;">NNCAM</span></strong>
|
| 16 |
+
</p>
|
| 17 |
+
|
| 18 |
+
# 模型介绍
|
| 19 |
+
|
| 20 |
+
NNCAM 根据大气柱状态预测云、对流和辐射等次网格过程产生的物理倾向与通量,主要用于数据驱动气候模式参数化研究。
|
| 21 |
+
|
| 22 |
+
论文:Deep learning to represent subgrid processes in climate models
|
| 23 |
+
https://gmd.copernicus.org/articles/11/3999/2018/
|
| 24 |
+
|
| 25 |
+
# 模型描述
|
| 26 |
+
|
| 27 |
+
该方法由慕尼黑大学、加州大学欧文分校和哥伦比亚大学的研究团队提出。论文使用 SPCAM 水行星模拟一年的约 1.4 亿个大气柱样本训练网络。模型适用于从 94 维大气柱状态预测 65 维加热、湿化、辐射通量和降水输出。
|
| 28 |
+
|
| 29 |
+
# 适用场景
|
| 30 |
+
|
| 31 |
+
| 场景 | 说明 |
|
| 32 |
+
| :---: | :--- |
|
| 33 |
+
| 次网格过程参数化 | 根据温度、湿度、风和地表强迫预测物理倾向与通量。 |
|
| 34 |
+
| 大气柱诊断 | 验证 30 层大气柱的加热、湿化、辐射和降水关系。 |
|
| 35 |
+
| 本地工程验证 | 使用结构化虚拟样本验证训练、推理、守恒诊断和可视化流程。 |
|
| 36 |
+
| ModelScope/OneCode 运行 | 在 ModelScope 或 OneCode 环境中验证结构化数据、训练、推理、参数化指标和可视化流程。 |
|
| 37 |
+
| 多卡训练 | 通过 `torchrun` 验证分布式训练和 checkpoint 流程。 |
|
| 38 |
+
|
| 39 |
+
# 使用说明
|
| 40 |
+
|
| 41 |
+
## 1.OneCode
|
| 42 |
+
|
| 43 |
+
可通过 OneCode 在线环境体验智能化一键式 AI4S 编程:
|
| 44 |
+
|
| 45 |
+
[点击体验智能化一键式 AI4S 编程](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 46 |
+
|
| 47 |
+
## 2.下载安装
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
modelscope download --model OneScience/NNCAM --local_dir ./NNCAM
|
| 51 |
+
cd NNCAM
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
### 环境依赖
|
| 55 |
+
|
| 56 |
+
**硬件要求**
|
| 57 |
+
|
| 58 |
+
- 推荐使用 GPU 或 DCU 运行。
|
| 59 |
+
- CPU 可用于默认小样本配置的连通性验证。
|
| 60 |
+
- DCU 用户需预先安装 DTK,建议使用 DTK 25.04.2 以上版本或与当前集群匹配的 OneScience 推荐版本。
|
| 61 |
+
|
| 62 |
+
**DCU环境**
|
| 63 |
+
|
| 64 |
+
```bash
|
| 65 |
+
# 请首先激活 DTK 及 Conda
|
| 66 |
+
conda create -n onescience311 python=3.11 -y
|
| 67 |
+
conda activate onescience311
|
| 68 |
+
pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
**GPU环境**
|
| 72 |
+
|
| 73 |
+
```bash
|
| 74 |
+
# 请首先激活 Conda
|
| 75 |
+
conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
|
| 76 |
+
conda activate onescience311
|
| 77 |
+
pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
### 训练数据介绍
|
| 81 |
+
|
| 82 |
+
论文训练数据来自 SPCAM 水行星模拟,时间步为 30 分钟并包含 30 个垂直层。输入为 `[B,94]` 的温度、湿度、风和地表强迫,目标为 `[B,65]` 的加热、湿化、四个辐射通量及降水。本仓库使用少量具有垂直和物理关联的虚拟样本验证工程流程,不代表 SPCAM 的真实数据分布、训练规模或论文性能。
|
| 83 |
+
|
| 84 |
+
```bash
|
| 85 |
+
python scripts/fake_data.py
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
### 训练
|
| 89 |
+
|
| 90 |
+
单卡训练可使用:
|
| 91 |
+
|
| 92 |
+
```bash
|
| 93 |
+
python scripts/train.py
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
多卡训练可使用:
|
| 97 |
+
|
| 98 |
+
```bash
|
| 99 |
+
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
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
默认配置将网络由论文的 9 层、每层 256 个节点缩小为 4 层、每层 32 个节点,并减少训练轮数,但不缩小 94 维输入、65 维输出和 30 层垂直协议。正式实验需要真实 SPCAM 数据和论文规模模型,训练产物保存到:
|
| 103 |
+
|
| 104 |
+
```text
|
| 105 |
+
result/checkpoints/nncam.pt
|
| 106 |
+
result/training/metrics.json
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
### 训练权重
|
| 110 |
+
|
| 111 |
+
论文未提供可直接加载的官方模型权重,本仓库不在 `weight/` 中内置权重。本地训练生成的 checkpoint 保存到 `result/checkpoints/nncam.pt`,不得描述为官方预训练权重。
|
| 112 |
+
|
| 113 |
+
### 推理
|
| 114 |
+
|
| 115 |
+
```bash
|
| 116 |
+
python scripts/inference.py
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
推理加载训练 checkpoint,根据完整大气柱状态生成次网格倾向、辐射通量和降水预测。完整数值结果保存到:
|
| 120 |
+
|
| 121 |
+
```text
|
| 122 |
+
result/output/predictions.npz
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
### 评估和可视化
|
| 126 |
+
|
| 127 |
+
```bash
|
| 128 |
+
python scripts/result.py
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
评估按输出组计算 RMSE 和 R²,并生成分组误差及降水预测对比图。虚拟数据结果仅用于验证工程流程,不代表论文正式性能;结果保存到:
|
| 132 |
+
|
| 133 |
+
```text
|
| 134 |
+
result/evaluation/metrics.json
|
| 135 |
+
result/evaluation/comparison.png
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
# OneScience官方信息
|
| 139 |
+
|
| 140 |
+
| 平台 | OneScience 主仓库 | Skills 仓库 |
|
| 141 |
+
| --- | --- | --- |
|
| 142 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 143 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 144 |
+
|
| 145 |
+
# 引用与许可证
|
| 146 |
+
|
| 147 |
+
本仓库为 NNCAM 公开规格的独立工程复现版本。
|
| 148 |
+
|
| 149 |
+
本仓库代码、官方模型权重和数据的使用仍应以各自项目中的许可证及使用条款为准。
|
conf/config.yaml
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
project:
|
| 2 |
+
name: NNCAM
|
| 3 |
+
format_version: "1.0"
|
| 4 |
+
seed: 42
|
| 5 |
+
|
| 6 |
+
dimensions:
|
| 7 |
+
input: 94
|
| 8 |
+
output: 65
|
| 9 |
+
levels: 30
|
| 10 |
+
|
| 11 |
+
paths:
|
| 12 |
+
data: data/nncam_fake.npz
|
| 13 |
+
checkpoint: result/checkpoints/nncam.pt
|
| 14 |
+
training_metrics: result/training/metrics.json
|
| 15 |
+
predictions: result/output/predictions.npz
|
| 16 |
+
evaluation_metrics: result/evaluation/metrics.json
|
| 17 |
+
figure: result/evaluation/comparison.png
|
| 18 |
+
|
| 19 |
+
data:
|
| 20 |
+
samples: 192
|
| 21 |
+
seed: 42
|
| 22 |
+
|
| 23 |
+
model:
|
| 24 |
+
depth: 4
|
| 25 |
+
width: 32
|
| 26 |
+
negative_slope: 0.3
|
| 27 |
+
|
| 28 |
+
training:
|
| 29 |
+
epochs: 3
|
| 30 |
+
batch_size: 64
|
| 31 |
+
learning_rate: 0.001
|
| 32 |
+
|
| 33 |
+
paper_model:
|
| 34 |
+
depth: 9
|
| 35 |
+
width: 256
|
| 36 |
+
epochs: 18
|
| 37 |
+
batch_size: 1024
|
| 38 |
+
parameters: 567361
|
| 39 |
+
note: Recorded paper-scale architecture; enabled only by the explicit --paper-model flag.
|
config.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_name": "NNCAM",
|
| 3 |
+
"model_type": "nncam",
|
| 4 |
+
"architectures": ["NNCAM"],
|
| 5 |
+
"framework": "PyTorch",
|
| 6 |
+
"domain": "atmospheric-physics",
|
| 7 |
+
"task": "climate-model-subgrid-parameterization",
|
| 8 |
+
"implementation": {
|
| 9 |
+
"entry_point": "model/nncam.py",
|
| 10 |
+
"scope": "core-method and full-column reduced-sample engineering reproduction",
|
| 11 |
+
"train_script": "scripts/train.py",
|
| 12 |
+
"inference_script": "scripts/inference.py",
|
| 13 |
+
"evaluation_script": "scripts/result.py",
|
| 14 |
+
"synthetic_data_script": "scripts/fake_data.py"
|
| 15 |
+
},
|
| 16 |
+
"architecture": {
|
| 17 |
+
"input_shape": ["B", 94],
|
| 18 |
+
"output_shape": ["B", 65],
|
| 19 |
+
"engineering_depth": 4,
|
| 20 |
+
"engineering_width": 32,
|
| 21 |
+
"paper_depth": 9,
|
| 22 |
+
"paper_width": 256,
|
| 23 |
+
"activation": "LeakyReLU"
|
| 24 |
+
},
|
| 25 |
+
"data": {
|
| 26 |
+
"dataset": "SPCAM aquaplanet simulation",
|
| 27 |
+
"format_version": "1.0",
|
| 28 |
+
"vertical_levels": 30,
|
| 29 |
+
"input_variables": ["T", "Q", "V", "Ps", "Sin", "H", "E"],
|
| 30 |
+
"output_variables": ["dT", "dQ", "SWtoa", "SWsfc", "LWtoa", "LWsfc", "P"],
|
| 31 |
+
"input_layout": "NF",
|
| 32 |
+
"synthetic": true
|
| 33 |
+
},
|
| 34 |
+
"configuration_sources": [
|
| 35 |
+
"conf/config.yaml",
|
| 36 |
+
"model/nncam.py",
|
| 37 |
+
"scripts/fake_data.py",
|
| 38 |
+
"scripts/train.py",
|
| 39 |
+
"scripts/inference.py",
|
| 40 |
+
"scripts/result.py"
|
| 41 |
+
]
|
| 42 |
+
}
|
configuration.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"framework": "PyTorch",
|
| 3 |
+
"task": "atmospheric subgrid-process parameterization",
|
| 4 |
+
"model": "NNCAM",
|
| 5 |
+
"input_format": "NF: atmospheric column state [N,94]",
|
| 6 |
+
"protocol": "SPCAM-supervised offline tendency prediction and engineering column diagnostics",
|
| 7 |
+
"default_config": "conf/config.yaml",
|
| 8 |
+
"training": "scripts/train.py",
|
| 9 |
+
"inference": "scripts/inference.py",
|
| 10 |
+
"evaluation": "scripts/result.py",
|
| 11 |
+
"visualization": "scripts/result.py"
|
| 12 |
+
}
|
model/nncam.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
import torch
|
| 5 |
+
from torch import nn
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
INPUT_GROUPS = {"T": slice(0, 30), "Q": slice(30, 60), "V": slice(60, 90), "Ps": slice(90, 91), "Sin": slice(91, 92), "H": slice(92, 93), "E": slice(93, 94)}
|
| 9 |
+
OUTPUT_GROUPS = {"dT": slice(0, 30), "dQ": slice(30, 60), "SWtoa": slice(60, 61), "SWsfc": slice(61, 62), "LWtoa": slice(62, 63), "LWsfc": slice(63, 64), "P": slice(64, 65)}
|
| 10 |
+
CP = 1004.0
|
| 11 |
+
LV = 2.5e6
|
| 12 |
+
OUTPUT_SCALE = np.r_[np.full(30, CP), np.full(30, LV), np.full(4, 1e-3), 2e-2].astype(np.float32)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class NNCAM(nn.Module):
|
| 16 |
+
"""Fully connected 94-to-65 atmospheric-column parameterization."""
|
| 17 |
+
|
| 18 |
+
def __init__(self, input_dim=94, output_dim=65, width=32, depth=4, negative_slope=0.3):
|
| 19 |
+
super().__init__()
|
| 20 |
+
if input_dim != 94 or output_dim != 65:
|
| 21 |
+
raise ValueError("NNCAM requires 94 inputs and 65 outputs")
|
| 22 |
+
if width < 1 or depth < 1:
|
| 23 |
+
raise ValueError("width and depth must be positive")
|
| 24 |
+
layers = []
|
| 25 |
+
in_features = input_dim
|
| 26 |
+
for _ in range(depth):
|
| 27 |
+
layers.extend((nn.Linear(in_features, width), nn.LeakyReLU(negative_slope)))
|
| 28 |
+
in_features = width
|
| 29 |
+
layers.append(nn.Linear(in_features, output_dim))
|
| 30 |
+
self.network = nn.Sequential(*layers)
|
| 31 |
+
self.model_config = {"input_dim": input_dim, "output_dim": output_dim, "width": width, "depth": depth, "negative_slope": negative_slope}
|
| 32 |
+
|
| 33 |
+
def forward(self, x):
|
| 34 |
+
if x.ndim != 2 or x.shape[1] != 94:
|
| 35 |
+
raise ValueError(f"expected input [B, 94], got {tuple(x.shape)}")
|
| 36 |
+
output = self.network(x)
|
| 37 |
+
if output.shape != (x.shape[0], 65):
|
| 38 |
+
raise RuntimeError(f"expected output [B, 65], got {tuple(output.shape)}")
|
| 39 |
+
return output
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def build_model(width=32, depth=4, negative_slope=0.3):
|
| 43 |
+
return NNCAM(width=width, depth=depth, negative_slope=negative_slope)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def generate_fake_data(n_samples=192, seed=42):
|
| 47 |
+
rng = np.random.default_rng(seed)
|
| 48 |
+
sigma = np.linspace(0.02, 1.0, 30, dtype=np.float32)[None, :]
|
| 49 |
+
lat = rng.uniform(-math.pi / 2, math.pi / 2, (n_samples, 1)).astype(np.float32)
|
| 50 |
+
time = rng.uniform(0, 2 * math.pi, (n_samples, 1)).astype(np.float32)
|
| 51 |
+
ps = rng.normal(101000.0, 1800.0, (n_samples, 1)).astype(np.float32)
|
| 52 |
+
insolation = np.maximum(0.0, 950.0 * np.cos(lat) * (0.65 + 0.35 * np.sin(time))).astype(np.float32)
|
| 53 |
+
height = rng.uniform(0.0, 2500.0, (n_samples, 1)).astype(np.float32)
|
| 54 |
+
evaporation = (35.0 + 80.0 * np.maximum(np.cos(lat), 0.0) + rng.normal(0, 4, (n_samples, 1))).astype(np.float32)
|
| 55 |
+
temperature = 205.0 + 83.0 * sigma**0.24 - 0.006 * height + 4.0 * np.cos(lat) * sigma
|
| 56 |
+
temperature += rng.normal(0, 1.2, temperature.shape)
|
| 57 |
+
humidity = (0.00005 + 0.017 * sigma**3 * np.maximum(np.cos(lat), 0.15)) * rng.lognormal(0, 0.12, temperature.shape)
|
| 58 |
+
wind = 18.0 * np.sin(lat) * (1.0 - sigma) + 5.0 * np.sin(time + 3.0 * sigma) + rng.normal(0, 2, temperature.shape)
|
| 59 |
+
instability = np.maximum(temperature[:, -1:] - temperature[:, 18:19] - 25.0, 0.0)
|
| 60 |
+
moisture = humidity[:, -8:].mean(1, keepdims=True)
|
| 61 |
+
precipitation = np.maximum(0.0, 4.0e-5 * instability * moisture / 0.012 + rng.normal(0, 1.5e-6, (n_samples, 1))).astype(np.float32)
|
| 62 |
+
heating_shape = np.exp(-((sigma - 0.55) / 0.23) ** 2)
|
| 63 |
+
drying_shape = np.exp(-((sigma - 0.78) / 0.18) ** 2)
|
| 64 |
+
dt = (precipitation * LV / CP / 86400.0 * heating_shape - 8e-6 * (temperature - temperature.mean(1, keepdims=True))).astype(np.float32)
|
| 65 |
+
dq = (-precipitation / 86400.0 * drying_shape + evaporation / LV / 30.0 / 86400.0).astype(np.float32)
|
| 66 |
+
cloud = np.clip(moisture / 0.014, 0.0, 1.0)
|
| 67 |
+
targets = (dt, dq, 0.30 * insolation, insolation * (0.72 - 0.18 * cloud), 215.0 + 0.65 * (temperature[:, -1:] - 273.0) - 24.0 * cloud, 330.0 + 1.1 * (temperature[:, -1:] - 285.0) + 18.0 * cloud, precipitation)
|
| 68 |
+
x = np.concatenate((temperature, humidity, wind, ps, insolation, height, evaporation), axis=1).astype(np.float32)
|
| 69 |
+
y = np.concatenate(targets, axis=1).astype(np.float32)
|
| 70 |
+
return x, y, lat[:, 0], time[:, 0]
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def fit_normalizer(values):
|
| 74 |
+
mean = values.mean(0).astype(np.float32)
|
| 75 |
+
scale = np.maximum(np.ptp(values, axis=0), values.std(0))
|
| 76 |
+
return mean, np.where(scale > 1e-12, scale, 1.0).astype(np.float32)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def normalize_input(values, mean, scale):
|
| 80 |
+
return (values - mean) / scale
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def scale_output(values):
|
| 84 |
+
return values * OUTPUT_SCALE
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def unscale_output(values):
|
| 88 |
+
return values / OUTPUT_SCALE
|
scripts/fake_data.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
import argparse
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
|
| 7 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 8 |
+
import sys
|
| 9 |
+
sys.path.insert(0, str(ROOT))
|
| 10 |
+
from model.nncam import generate_fake_data
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def main():
|
| 14 |
+
parser = argparse.ArgumentParser(description="Create deterministic synthetic NNCAM columns.")
|
| 15 |
+
parser.add_argument("--samples", type=int, default=192)
|
| 16 |
+
parser.add_argument("--seed", type=int, default=42)
|
| 17 |
+
parser.add_argument("--output", type=Path, default=ROOT / "data/nncam_fake.npz")
|
| 18 |
+
args = parser.parse_args()
|
| 19 |
+
x, y, lat, time = generate_fake_data(args.samples, args.seed)
|
| 20 |
+
assert x.shape == (args.samples, 94) and y.shape == (args.samples, 65)
|
| 21 |
+
assert np.isfinite(x).all() and np.isfinite(y).all() and (y[:, 64] >= 0).all()
|
| 22 |
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
| 23 |
+
np.savez_compressed(args.output, x=x, y=y, lat=lat, time=time)
|
| 24 |
+
print(f"saved {args.output}: x={x.shape}, y={y.shape}, finite=true")
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
if __name__ == "__main__":
|
| 28 |
+
main()
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
import argparse
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
import torch
|
| 7 |
+
|
| 8 |
+
from model.nncam import NNCAM, unscale_output
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def main():
|
| 15 |
+
parser = argparse.ArgumentParser(description="Run offline NNCAM inference.")
|
| 16 |
+
parser.add_argument("--data", type=Path, default=ROOT / "data/nncam_fake.npz")
|
| 17 |
+
parser.add_argument("--checkpoint", type=Path, default=ROOT / "result/checkpoints/nncam.pt")
|
| 18 |
+
parser.add_argument("--output", type=Path, default=ROOT / "result/output/predictions.npz")
|
| 19 |
+
args = parser.parse_args()
|
| 20 |
+
checkpoint = torch.load(args.checkpoint, map_location="cpu", weights_only=True)
|
| 21 |
+
required = {"model", "model_config", "format_version", "normalization"}
|
| 22 |
+
if not required.issubset(checkpoint):
|
| 23 |
+
raise ValueError(f"checkpoint missing {sorted(required - checkpoint.keys())}")
|
| 24 |
+
model = NNCAM(**checkpoint["model_config"])
|
| 25 |
+
model.load_state_dict(checkpoint["model"])
|
| 26 |
+
model.eval()
|
| 27 |
+
with np.load(args.data) as data:
|
| 28 |
+
x, truth, lat, time = (data[name] for name in ("x", "y", "lat", "time"))
|
| 29 |
+
norm = checkpoint["normalization"]
|
| 30 |
+
normalized = (torch.from_numpy(x) - norm["input_mean"]) / norm["input_scale"]
|
| 31 |
+
with torch.no_grad():
|
| 32 |
+
scaled = model(normalized) * norm["target_scale"] + norm["target_mean"]
|
| 33 |
+
prediction = unscale_output(scaled.numpy()).astype(np.float32)
|
| 34 |
+
if prediction.shape != truth.shape or not np.isfinite(prediction).all():
|
| 35 |
+
raise RuntimeError(f"invalid prediction shape or values: {prediction.shape}")
|
| 36 |
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
| 37 |
+
np.savez_compressed(args.output, input=x, truth=truth, prediction=prediction, lat=lat, time=time)
|
| 38 |
+
print(f"saved {args.output}: prediction={prediction.shape}, finite=true")
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
if __name__ == "__main__":
|
| 42 |
+
main()
|
scripts/result.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
import argparse
|
| 3 |
+
import json
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
import matplotlib
|
| 7 |
+
matplotlib.use("Agg")
|
| 8 |
+
import matplotlib.pyplot as plt
|
| 9 |
+
import numpy as np
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def score(truth, prediction):
|
| 16 |
+
rmse = float(np.sqrt(np.mean((truth - prediction) ** 2)))
|
| 17 |
+
denominator = float(np.sum((truth - truth.mean()) ** 2))
|
| 18 |
+
return {"rmse": rmse, "r2": float(1.0 - np.sum((truth - prediction) ** 2) / denominator) if denominator else None}
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def main():
|
| 22 |
+
parser = argparse.ArgumentParser(description="Evaluate NNCAM predictions.")
|
| 23 |
+
parser.add_argument("--input", type=Path, default=ROOT / "result/output/predictions.npz")
|
| 24 |
+
parser.add_argument("--metrics", type=Path, default=ROOT / "result/evaluation/metrics.json")
|
| 25 |
+
parser.add_argument("--figure", type=Path, default=ROOT / "result/evaluation/comparison.png")
|
| 26 |
+
args = parser.parse_args()
|
| 27 |
+
with np.load(args.input) as data:
|
| 28 |
+
truth, prediction = data["truth"], data["prediction"]
|
| 29 |
+
if truth.shape != prediction.shape or truth.ndim != 2 or truth.shape[1] != 65:
|
| 30 |
+
raise ValueError(f"expected matching [N,65] arrays, got {truth.shape}, {prediction.shape}")
|
| 31 |
+
groups = {"dT": slice(0, 30), "dQ": slice(30, 60), "SW": slice(60, 62), "LW": slice(62, 64), "P": slice(64, 65)}
|
| 32 |
+
metrics = {name: score(truth[:, indices], prediction[:, indices]) for name, indices in groups.items()}
|
| 33 |
+
metrics["overall"] = score(truth, prediction)
|
| 34 |
+
values = np.array([value for group in metrics.values() for value in group.values() if value is not None])
|
| 35 |
+
if not np.isfinite(values).all():
|
| 36 |
+
raise RuntimeError("evaluation metrics contain non-finite values")
|
| 37 |
+
args.metrics.parent.mkdir(parents=True, exist_ok=True)
|
| 38 |
+
args.metrics.write_text(json.dumps(metrics, indent=2), encoding="utf-8")
|
| 39 |
+
names = list(groups)
|
| 40 |
+
fig, axes = plt.subplots(1, 2, figsize=(10, 4), constrained_layout=True)
|
| 41 |
+
axes[0].bar(names, [metrics[name]["rmse"] for name in names])
|
| 42 |
+
axes[0].set_title("Grouped RMSE")
|
| 43 |
+
axes[1].scatter(truth[:, 64], prediction[:, 64], s=12, alpha=0.7)
|
| 44 |
+
axes[1].set(xlabel="True precipitation", ylabel="Predicted precipitation", title="Precipitation comparison")
|
| 45 |
+
fig.savefig(args.figure, dpi=150)
|
| 46 |
+
plt.close(fig)
|
| 47 |
+
print(f"saved {args.metrics} and {args.figure}; shape={prediction.shape}, finite=true")
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
if __name__ == "__main__":
|
| 51 |
+
main()
|
scripts/train.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
import argparse
|
| 3 |
+
import json
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
import torch
|
| 8 |
+
from torch.utils.data import DataLoader, TensorDataset
|
| 9 |
+
|
| 10 |
+
from model.nncam import build_model, fit_normalizer, normalize_input, scale_output
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def main():
|
| 17 |
+
parser = argparse.ArgumentParser(description="Train NNCAM on the prepared NPZ dataset.")
|
| 18 |
+
parser.add_argument("--data", type=Path, default=ROOT / "data/nncam_fake.npz")
|
| 19 |
+
parser.add_argument("--checkpoint", type=Path, default=ROOT / "result/checkpoints/nncam.pt")
|
| 20 |
+
parser.add_argument("--metrics", type=Path, default=ROOT / "result/training/metrics.json")
|
| 21 |
+
parser.add_argument("--epochs", type=int)
|
| 22 |
+
parser.add_argument("--batch-size", type=int)
|
| 23 |
+
parser.add_argument("--width", type=int)
|
| 24 |
+
parser.add_argument("--depth", type=int)
|
| 25 |
+
parser.add_argument("--paper-model", action="store_true", help="Explicitly use depth=9, width=256, epochs=18, batch_size=1024 (567361 parameters).")
|
| 26 |
+
parser.add_argument("--lr", type=float, default=1e-3)
|
| 27 |
+
parser.add_argument("--seed", type=int, default=42)
|
| 28 |
+
args = parser.parse_args()
|
| 29 |
+
if not args.data.is_file():
|
| 30 |
+
raise FileNotFoundError(f"missing dataset {args.data}; run python scripts/fake_data.py first")
|
| 31 |
+
defaults = {"depth": 9, "width": 256, "epochs": 18, "batch_size": 1024} if args.paper_model else {"depth": 4, "width": 32, "epochs": 3, "batch_size": 64}
|
| 32 |
+
depth, width = args.depth or defaults["depth"], args.width or defaults["width"]
|
| 33 |
+
epochs, batch_size = args.epochs or defaults["epochs"], args.batch_size or defaults["batch_size"]
|
| 34 |
+
torch.manual_seed(args.seed)
|
| 35 |
+
with np.load(args.data) as data:
|
| 36 |
+
x, y = data["x"].astype(np.float32), data["y"].astype(np.float32)
|
| 37 |
+
if x.ndim != 2 or x.shape[1] != 94 or y.shape != (x.shape[0], 65):
|
| 38 |
+
raise ValueError(f"expected x=[N,94], y=[N,65], got {x.shape}, {y.shape}")
|
| 39 |
+
input_mean, input_scale = fit_normalizer(x)
|
| 40 |
+
scaled_y = scale_output(y)
|
| 41 |
+
target_mean, target_scale = fit_normalizer(scaled_y)
|
| 42 |
+
dataset = TensorDataset(torch.from_numpy(normalize_input(x, input_mean, input_scale)), torch.from_numpy(normalize_input(scaled_y, target_mean, target_scale)))
|
| 43 |
+
loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
|
| 44 |
+
model = build_model(width=width, depth=depth)
|
| 45 |
+
optimizer = torch.optim.Adam(model.parameters(), lr=args.lr)
|
| 46 |
+
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=3, gamma=0.2)
|
| 47 |
+
history = []
|
| 48 |
+
for epoch in range(epochs):
|
| 49 |
+
total = 0.0
|
| 50 |
+
for xb, yb in loader:
|
| 51 |
+
optimizer.zero_grad(set_to_none=True)
|
| 52 |
+
loss = torch.nn.functional.mse_loss(model(xb), yb)
|
| 53 |
+
loss.backward()
|
| 54 |
+
optimizer.step()
|
| 55 |
+
total += loss.item() * len(xb)
|
| 56 |
+
history.append(total / len(dataset))
|
| 57 |
+
scheduler.step()
|
| 58 |
+
print(f"epoch={epoch + 1:02d} loss={history[-1]:.6f}")
|
| 59 |
+
parameter_count = sum(parameter.numel() for parameter in model.parameters())
|
| 60 |
+
checkpoint = {
|
| 61 |
+
"format_version": 1,
|
| 62 |
+
"model": model.state_dict(),
|
| 63 |
+
"model_config": model.model_config,
|
| 64 |
+
"normalization": {"input_mean": torch.from_numpy(input_mean), "input_scale": torch.from_numpy(input_scale), "target_mean": torch.from_numpy(target_mean), "target_scale": torch.from_numpy(target_scale)},
|
| 65 |
+
"training": {"epochs": epochs, "batch_size": batch_size, "learning_rate": args.lr, "paper_model": args.paper_model, "parameters": parameter_count},
|
| 66 |
+
}
|
| 67 |
+
args.checkpoint.parent.mkdir(parents=True, exist_ok=True)
|
| 68 |
+
args.metrics.parent.mkdir(parents=True, exist_ok=True)
|
| 69 |
+
torch.save(checkpoint, args.checkpoint)
|
| 70 |
+
args.metrics.write_text(json.dumps({"loss": history, "final_loss": history[-1], "parameters": parameter_count, "model_config": model.model_config}, indent=2), encoding="utf-8")
|
| 71 |
+
print(f"saved {args.checkpoint}; parameters={parameter_count}; paper_model={args.paper_model}")
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
if __name__ == "__main__":
|
| 75 |
+
main()
|
weight/.gitkeep
ADDED
|
File without changes
|