Add engineering reproduction package
Browse files- .gitattributes +2 -33
- README.md +159 -0
- README_zh.md +162 -0
- conf/config.yaml +63 -0
- config.json +110 -0
- configuration.json +12 -0
- model/clayfoundation.py +222 -0
- scripts/fake_data.py +91 -0
- scripts/inference.py +54 -0
- scripts/result.py +64 -0
- scripts/train.py +121 -0
- weight/.gitkeep +0 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,4 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 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 |
-
*.
|
| 24 |
-
*.
|
| 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 |
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
tags:
|
| 6 |
+
- OneScience
|
| 7 |
+
- Earth Science
|
| 8 |
+
- Multi-Source Earth Observation
|
| 9 |
+
- Remote Sensing Foundation Model
|
| 10 |
+
frameworks: PyTorch
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
<p align="center">
|
| 14 |
+
<strong>
|
| 15 |
+
<span style="font-size: 30px;">Clay Foundation Model</span>
|
| 16 |
+
</strong>
|
| 17 |
+
</p>
|
| 18 |
+
|
| 19 |
+
# Model Introduction
|
| 20 |
+
|
| 21 |
+
Clay Foundation Model is an independent engineering reproduction of the public Clay v1.5 specification. It jointly encodes satellite imagery from different sensors, band center wavelengths, ground sample distance, time, and latitude/longitude into Earth observation embeddings, and reconstructs input bands with a Masked Autoencoder. It can serve as a representation backbone for land-cover classification, regression, change detection, and other downstream remote-sensing tasks.
|
| 22 |
+
|
| 23 |
+
Official documentation: Clay Foundation Model v1.5
|
| 24 |
+
https://clay-foundation.github.io/model/release-notes/specification.html
|
| 25 |
+
|
| 26 |
+
# Model Description
|
| 27 |
+
|
| 28 |
+
Clay Foundation Model was proposed by the Clay Foundation. The model was trained on multi-sensor Earth observation data including Sentinel-2, Landsat, Sentinel-1, NAIP, LINZ, and MODIS. It is suitable for remote-sensing image representation, multispectral reconstruction, land-cover classification, regression, change detection, and related tasks.
|
| 29 |
+
|
| 30 |
+
# Use Cases
|
| 31 |
+
|
| 32 |
+
| Use Case | Description |
|
| 33 |
+
| :---: | :--- |
|
| 34 |
+
| Multi-sensor representation | Use one model to process imagery with different numbers of bands from sensors such as Sentinel-2, Landsat, and Sentinel-1. |
|
| 35 |
+
| Dynamic spectral encoding | Dynamically generate Patch Embeddings from the center wavelengths of input bands to validate integration methods for new sensors. |
|
| 36 |
+
| Remote-sensing image reconstruction | Use a Masked Autoencoder to reconstruct masked patches in multispectral imagery. |
|
| 37 |
+
| Local engineering validation | Use a small amount of synthetic data to validate training, inference, evaluation, visualization, and checkpoint workflows. |
|
| 38 |
+
| Multi-GPU training | Launch distributed data-parallel training with `torchrun`. |
|
| 39 |
+
|
| 40 |
+
# Usage Instructions
|
| 41 |
+
|
| 42 |
+
## 1.OneCode
|
| 43 |
+
|
| 44 |
+
Experience intelligent, one-click AI4S programming through the OneCode online environment:
|
| 45 |
+
|
| 46 |
+
[Try intelligent, one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 47 |
+
|
| 48 |
+
## 2.Download and Installation
|
| 49 |
+
|
| 50 |
+
```bash
|
| 51 |
+
hf download OneScience-Group/ClayFoundation --local-dir ./ClayFoundation
|
| 52 |
+
cd ClayFoundation
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
### Environment Dependencies
|
| 56 |
+
|
| 57 |
+
**Hardware Requirements**
|
| 58 |
+
|
| 59 |
+
- A GPU or DCU is recommended.
|
| 60 |
+
- A CPU can be used to verify connectivity with the default small-sample configuration; training the official-size model requires large-scale acceleration resources.
|
| 61 |
+
- DCU users must install DTK in advance. DTK 25.04.2 or later, or the OneScience-recommended version matching the current cluster, is recommended.
|
| 62 |
+
|
| 63 |
+
**DCU Environment**
|
| 64 |
+
|
| 65 |
+
```bash
|
| 66 |
+
# Activate DTK and CONDA first
|
| 67 |
+
conda create -n onescience311 python=3.11 -y
|
| 68 |
+
conda activate onescience311
|
| 69 |
+
# uv installation is supported
|
| 70 |
+
pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
**GPU Environment**
|
| 74 |
+
|
| 75 |
+
```bash
|
| 76 |
+
# Activate CONDA first
|
| 77 |
+
conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
|
| 78 |
+
conda activate onescience311
|
| 79 |
+
# uv installation is supported
|
| 80 |
+
pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
### Training Data
|
| 84 |
+
|
| 85 |
+
This repository uses a small number of synthetic samples to validate the engineering workflow. The training and test data are stored in `data/train.npz` and `data/test.npz`, respectively. The synthetic data contains 10-band Sentinel-2, 6-band Landsat, and 2-band Sentinel-1 imagery, with center wavelengths and ground sample distances corresponding to the official metadata for each sensor. Each sample also includes week, hour, latitude, and longitude metadata; a synthetic teacher representation target; and classification and regression targets for lightweight evaluation.
|
| 86 |
+
|
| 87 |
+
The default input size is 64×64, with four training samples and two test samples. This scale is intended only to validate the dynamic-band interface, spatiotemporal metadata encoding, MAE training, and multi-sensor inference workflow. It does not represent the data distribution or training scale of the approximately 70 million global remote-sensing chips used by the official model.
|
| 88 |
+
|
| 89 |
+
```bash
|
| 90 |
+
python scripts/fake_data.py
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
### Training
|
| 94 |
+
|
| 95 |
+
```bash
|
| 96 |
+
python scripts/train.py
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
For multi-GPU training, use:
|
| 100 |
+
|
| 101 |
+
```bash
|
| 102 |
+
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
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
The default configuration is intended for rapid workflow validation. Formal experiments should use real remote-sensing data, the official-size configuration, a DINOv2 teacher, and a complete training schedule.
|
| 106 |
+
|
| 107 |
+
```text
|
| 108 |
+
result/checkpoints/clayfoundation.pt
|
| 109 |
+
result/training/metrics.json
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
### Trained Weights
|
| 113 |
+
|
| 114 |
+
This repository does not include synthetic or official weights in `weight/`. The Clay Foundation has released the official Clay v1.5 checkpoint:
|
| 115 |
+
|
| 116 |
+
https://huggingface.co/made-with-clay/Clay/resolve/main/v1.5/clay-v1.5.ckpt
|
| 117 |
+
|
| 118 |
+
This repository is a scaled-down independent engineering implementation. Its model parameter names and dimensions are incompatible with the official Encoder weights of approximately 1.25 GB. To use the official weights, use the model implementation and data preprocessing workflow provided by the official Clay repository.
|
| 119 |
+
|
| 120 |
+
### Inference
|
| 121 |
+
|
| 122 |
+
```bash
|
| 123 |
+
python scripts/inference.py
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
Inference loads the training checkpoint and separately processes Sentinel-2, Landsat, and Sentinel-1 test imagery. It generates Encoder embeddings, L2-normalized projected embeddings, complete multispectral MAE reconstructions, reconstruction loss, and teacher representation alignment loss, and saves them to:
|
| 127 |
+
|
| 128 |
+
```text
|
| 129 |
+
result/output/predictions.npz
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
### Evaluation and Visualization
|
| 133 |
+
|
| 134 |
+
```bash
|
| 135 |
+
python scripts/result.py
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
The evaluation measures image reconstruction and representation quality across sensors and reports reconstruction error, embedding norms, and representation similarity. It also generates comparison plots of the input imagery, reconstructed results, and absolute errors. Results on synthetic data are intended only to validate the engineering workflow and do not represent the official Clay v1.5 training loss, real downstream-task performance, or cross-region generalization capability.
|
| 139 |
+
|
| 140 |
+
```text
|
| 141 |
+
result/evaluation/metrics.json
|
| 142 |
+
result/evaluation/comparison.png
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
# Official OneScience Information
|
| 146 |
+
|
| 147 |
+
| Platform | OneScience Main Repository | Skills Repository |
|
| 148 |
+
| --- | --- | --- |
|
| 149 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 150 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 151 |
+
|
| 152 |
+
# Citation and License
|
| 153 |
+
|
| 154 |
+
This repository is an independent engineering reproduction of the public Clay Foundation Model v1.5 specification. The official Clay source code and model weights are licensed under Apache-2.0.
|
| 155 |
+
|
| 156 |
+
Official Clay repository:
|
| 157 |
+
https://github.com/Clay-foundation/model
|
| 158 |
+
|
| 159 |
+
Use of this repository's code, the official model weights, and the data remains subject to the licenses and terms of use of their respective projects.
|
README_zh.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: Apache-2.0
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
- zh
|
| 6 |
+
tags:
|
| 7 |
+
- OneScience
|
| 8 |
+
- 地球科学
|
| 9 |
+
- 多源地球观测
|
| 10 |
+
- 遥感基础模型
|
| 11 |
+
frameworks: PyTorch
|
| 12 |
+
datasets:
|
| 13 |
+
- Synthetic Multi-Sensor Earth Observation Chips
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
<p align="center">
|
| 17 |
+
<strong>
|
| 18 |
+
<span style="font-size: 30px;">Clay Foundation Model</span>
|
| 19 |
+
</strong>
|
| 20 |
+
</p>
|
| 21 |
+
|
| 22 |
+
# 模型介绍
|
| 23 |
+
|
| 24 |
+
Clay Foundation Model 是 Clay v1.5 公开规格的独立工程复现模型,将不同传感器的卫星影像、波段中心波长、地面采样距离、时间和经纬度统一编码为地球观测 embedding,并通过 Masked Autoencoder 重建输入波段,可作为地物分类、回归、变化检测和其他下游遥感任务的表征骨干网络。
|
| 25 |
+
|
| 26 |
+
官方文档:Clay Foundation Model v1.5
|
| 27 |
+
https://clay-foundation.github.io/model/release-notes/specification.html
|
| 28 |
+
|
| 29 |
+
# 模型描述
|
| 30 |
+
|
| 31 |
+
Clay Foundation Model 由 Clay Foundation 提出。模型使用 Sentinel-2、Landsat、Sentinel-1、NAIP、LINZ 和 MODIS 等多传感器地球观测数据训练。模型适用于遥感影像表征、多波段重建、地物分类、回归和变化检测等任务。
|
| 32 |
+
|
| 33 |
+
# 适用场景
|
| 34 |
+
|
| 35 |
+
| 场景 | 说明 |
|
| 36 |
+
| :---: | :--- |
|
| 37 |
+
| 多传感器表征 | 使用同一模型处理 Sentinel-2、Landsat 和 Sentinel-1 等不同波段数量的影像。 |
|
| 38 |
+
| 动态光谱编码 | 根据输入波段的中心波长动态生成 Patch Embedding,验证新传感器接入方式。 |
|
| 39 |
+
| 遥感影像重建 | 使用 Masked Autoencoder 对被遮挡的多波段影像 Patch 进行重建。 |
|
| 40 |
+
| 本地工程验证 | 使用少量虚拟数据检查训练、推理、评估、可视化和 checkpoint 流程。 |
|
| 41 |
+
| 多卡训练 | 通过 `torchrun` 启动分布式数据并行训练。 |
|
| 42 |
+
|
| 43 |
+
# 使用说明
|
| 44 |
+
|
| 45 |
+
## 1.OneCode
|
| 46 |
+
|
| 47 |
+
可通过 OneCode 在线环境体验智能化一键式 AI4S 编程:
|
| 48 |
+
|
| 49 |
+
[点击体验智能化一键式 AI4S 编程](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 50 |
+
|
| 51 |
+
## 2.下载安装
|
| 52 |
+
|
| 53 |
+
```bash
|
| 54 |
+
modelscope download --model OneScience/ClayFoundation --local_dir ./ClayFoundation
|
| 55 |
+
cd ClayFoundation
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
### 环境依赖
|
| 59 |
+
|
| 60 |
+
**硬件要求**
|
| 61 |
+
|
| 62 |
+
- 推荐使用 GPU 或 DCU 运行。
|
| 63 |
+
- CPU 可用于默认小样本配置的连通性验证,官方尺寸模型训练需要大规模加速资源。
|
| 64 |
+
- DCU 用户需预先安装 DTK,建议使用 DTK 25.04.2 以上版本或与当前集群匹配的 OneScience 推荐版本。
|
| 65 |
+
|
| 66 |
+
**DCU环境**
|
| 67 |
+
|
| 68 |
+
```bash
|
| 69 |
+
# 请首先激活DTK及CONDA
|
| 70 |
+
conda create -n onescience311 python=3.11 -y
|
| 71 |
+
conda activate onescience311
|
| 72 |
+
# 支持uv安装
|
| 73 |
+
pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
**GPU环境**
|
| 77 |
+
|
| 78 |
+
```bash
|
| 79 |
+
# 请首先激活CONDA
|
| 80 |
+
conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
|
| 81 |
+
conda activate onescience311
|
| 82 |
+
# 支持uv安装
|
| 83 |
+
pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
### 训练数据介绍
|
| 87 |
+
|
| 88 |
+
本仓库使用少量虚拟样本验证工程流程,训练数据和测试数据分别保存为 `data/train.npz` 和 `data/test.npz`。虚拟数据包含 Sentinel-2 10 波段、Landsat 6 波段和 Sentinel-1 2 波段影像,并为每个传感器提供官方元数据对应的中心波长和地面采样距离。样本同时包含 week、hour、latitude、longitude 元数据、合成 teacher 表征目标,以及用于轻量评估的分类和回归目标。
|
| 89 |
+
|
| 90 |
+
默认输入尺寸为 64×64,训练集包含 4 个样本,测试集包含 2 个样本。该规模只用于验证动态波段接口、时空元数据编码、MAE 训练和多传感器推理流程,不代表官方约 7000 万全球遥感 chips 的数据分布与训练规模。
|
| 91 |
+
|
| 92 |
+
```bash
|
| 93 |
+
python scripts/fake_data.py
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
### 训练
|
| 97 |
+
|
| 98 |
+
```bash
|
| 99 |
+
python scripts/train.py
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
多卡训练可使用:
|
| 103 |
+
|
| 104 |
+
```bash
|
| 105 |
+
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
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
默认配置面向快速流程验证;开展正式实验时,应使用真实遥感数据、官方尺寸配置、DINOv2 teacher 和完整训练周期。
|
| 109 |
+
|
| 110 |
+
```text
|
| 111 |
+
result/checkpoints/clayfoundation.pt
|
| 112 |
+
result/training/metrics.json
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
### 训练权重
|
| 116 |
+
|
| 117 |
+
本仓库不在 `weight/` 中内置虚拟权重或官方权重。Clay Foundation 已公开 Clay v1.5 官方 checkpoint:
|
| 118 |
+
|
| 119 |
+
https://huggingface.co/made-with-clay/Clay/resolve/main/v1.5/clay-v1.5.ckpt
|
| 120 |
+
|
| 121 |
+
本仓库是缩小的独立工程实现,模型参数名称和尺寸不与官方约 1.25 GB Encoder 权重兼容。需要使用官方权重时,应采用 Clay 官方仓库提供的模型实现和数据预处理流程。
|
| 122 |
+
|
| 123 |
+
### 推理
|
| 124 |
+
|
| 125 |
+
```bash
|
| 126 |
+
python scripts/inference.py
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
推理加载训练 checkpoint,分别处理 Sentinel-2、Landsat 和 Sentinel-1 测试影像,生成 Encoder embedding、L2 归一化投影 embedding、完整多波段 MAE 重建结果,以及重建损失和 teacher 表征对齐损失,并保存到:
|
| 130 |
+
|
| 131 |
+
```text
|
| 132 |
+
result/output/predictions.npz
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
### 评估和可视化
|
| 136 |
+
|
| 137 |
+
```bash
|
| 138 |
+
python scripts/result.py
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
评估模型在不同传感器上的影像重建与表征效果,并统计重建误差、embedding 范数和表征相似度。评估过程同时生成输入影像、重建结果和绝对误差对比图。虚拟数据结果仅用于验证工程流程,不代表官方 Clay v1.5 的训练损失、真实下游任务性能或跨区域泛化能力。
|
| 142 |
+
|
| 143 |
+
```text
|
| 144 |
+
result/evaluation/metrics.json
|
| 145 |
+
result/evaluation/comparison.png
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
# OneScience官方信息
|
| 149 |
+
|
| 150 |
+
| 平台 | OneScience 主仓库 | Skills 仓库 |
|
| 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 |
+
# 引用与许可证
|
| 156 |
+
|
| 157 |
+
本仓库为 Clay Foundation Model v1.5 公开规格的独立工程复现版本。Clay 官方源码和模型权重采用 Apache-2.0 许可证。
|
| 158 |
+
|
| 159 |
+
Clay 官方仓库:
|
| 160 |
+
https://github.com/Clay-foundation/model
|
| 161 |
+
|
| 162 |
+
本仓库代码、官方模型权重和数据的使用仍应以各自项目中的许可证及使用条款为准。
|
conf/config.yaml
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
seed: 42
|
| 2 |
+
data:
|
| 3 |
+
root: data
|
| 4 |
+
format_version: clay_synthetic_v1_5_engineering_v1
|
| 5 |
+
train_samples: 4
|
| 6 |
+
test_samples: 2
|
| 7 |
+
image_size: 64
|
| 8 |
+
sensors:
|
| 9 |
+
sentinel-2-l2a:
|
| 10 |
+
channels: 10
|
| 11 |
+
gsd: 10.0
|
| 12 |
+
wavelengths_nm: [493, 560, 665, 704, 740, 783, 842, 865, 1610, 2190]
|
| 13 |
+
rgb_indices: [2, 1, 0]
|
| 14 |
+
landsat-c2l2-sr:
|
| 15 |
+
channels: 6
|
| 16 |
+
gsd: 30.0
|
| 17 |
+
wavelengths_nm: [650, 560, 480, 860, 1600, 2200]
|
| 18 |
+
rgb_indices: [0, 1, 2]
|
| 19 |
+
sentinel-1-rtc:
|
| 20 |
+
channels: 2
|
| 21 |
+
gsd: 10.0
|
| 22 |
+
wavelengths_nm: [3500, 4000]
|
| 23 |
+
rgb_indices: [0, 1, 0]
|
| 24 |
+
model:
|
| 25 |
+
patch_size: 8
|
| 26 |
+
mask_ratio: 0.75
|
| 27 |
+
encoder_dim: 96
|
| 28 |
+
encoder_depth: 2
|
| 29 |
+
encoder_heads: 4
|
| 30 |
+
decoder_dim: 64
|
| 31 |
+
decoder_depth: 1
|
| 32 |
+
decoder_heads: 4
|
| 33 |
+
wave_dim: 32
|
| 34 |
+
wave_latents: 16
|
| 35 |
+
teacher_dim: 128
|
| 36 |
+
norm_pix_loss: false
|
| 37 |
+
paper_model:
|
| 38 |
+
patch_size: 8
|
| 39 |
+
mask_ratio: 0.75
|
| 40 |
+
encoder_dim: 1024
|
| 41 |
+
encoder_depth: 24
|
| 42 |
+
encoder_heads: 16
|
| 43 |
+
decoder_dim: 512
|
| 44 |
+
decoder_depth: 4
|
| 45 |
+
decoder_heads: 4
|
| 46 |
+
parameter_count: 632M_including_DINOv2_teacher
|
| 47 |
+
encoder_parameter_count: 311M
|
| 48 |
+
teacher: DINOv2_304M
|
| 49 |
+
train:
|
| 50 |
+
epochs: 1
|
| 51 |
+
batch_size: 2
|
| 52 |
+
learning_rate: 0.0005
|
| 53 |
+
weight_decay: 0.000001
|
| 54 |
+
reconstruction_weight: 0.95
|
| 55 |
+
representation_weight: 0.05
|
| 56 |
+
num_workers: 0
|
| 57 |
+
runtime:
|
| 58 |
+
device: auto
|
| 59 |
+
paths:
|
| 60 |
+
checkpoint: result/checkpoints/clayfoundation.pt
|
| 61 |
+
training_metrics: result/training/metrics.json
|
| 62 |
+
inference_dir: result/output
|
| 63 |
+
evaluation_dir: result/evaluation
|
config.json
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_name": "ClayFoundation",
|
| 3 |
+
"model_type": "clayfoundation",
|
| 4 |
+
"architectures": [
|
| 5 |
+
"ClayFoundation",
|
| 6 |
+
"DynamicEmbedding",
|
| 7 |
+
"Transformer",
|
| 8 |
+
"DynamicDecoder"
|
| 9 |
+
],
|
| 10 |
+
"framework": "PyTorch",
|
| 11 |
+
"domain": "earth-observation",
|
| 12 |
+
"task": "multi-sensor-remote-sensing-representation-learning",
|
| 13 |
+
"implementation": {
|
| 14 |
+
"entry_point": "model/clayfoundation.py",
|
| 15 |
+
"scope": "scaled-down independent engineering reproduction of the Clay v1.5 multi-sensor masked-autoencoder specification",
|
| 16 |
+
"train_script": "scripts/train.py",
|
| 17 |
+
"inference_script": "scripts/inference.py",
|
| 18 |
+
"evaluation_script": "scripts/result.py",
|
| 19 |
+
"synthetic_data_script": "scripts/fake_data.py"
|
| 20 |
+
},
|
| 21 |
+
"architecture": {
|
| 22 |
+
"family": "multi-sensor masked autoencoder with wavelength-conditioned dynamic embeddings",
|
| 23 |
+
"patch_size": 8,
|
| 24 |
+
"mask_ratio": 0.75,
|
| 25 |
+
"encoder_dim": 96,
|
| 26 |
+
"encoder_depth": 2,
|
| 27 |
+
"encoder_heads": 4,
|
| 28 |
+
"decoder_dim": 64,
|
| 29 |
+
"decoder_depth": 1,
|
| 30 |
+
"decoder_heads": 4,
|
| 31 |
+
"wave_dim": 32,
|
| 32 |
+
"wave_latents": 16,
|
| 33 |
+
"teacher_dim": 128,
|
| 34 |
+
"norm_pix_loss": false,
|
| 35 |
+
"metadata": [
|
| 36 |
+
"week",
|
| 37 |
+
"hour",
|
| 38 |
+
"latitude",
|
| 39 |
+
"longitude",
|
| 40 |
+
"ground_sample_distance",
|
| 41 |
+
"band_center_wavelengths"
|
| 42 |
+
],
|
| 43 |
+
"paper_model": {
|
| 44 |
+
"patch_size": 8,
|
| 45 |
+
"mask_ratio": 0.75,
|
| 46 |
+
"encoder_dim": 1024,
|
| 47 |
+
"encoder_depth": 24,
|
| 48 |
+
"encoder_heads": 16,
|
| 49 |
+
"decoder_dim": 512,
|
| 50 |
+
"decoder_depth": 4,
|
| 51 |
+
"decoder_heads": 4,
|
| 52 |
+
"parameter_count": "632M including DINOv2 teacher",
|
| 53 |
+
"encoder_parameter_count": "311M",
|
| 54 |
+
"teacher": "DINOv2 304M"
|
| 55 |
+
}
|
| 56 |
+
},
|
| 57 |
+
"data": {
|
| 58 |
+
"datasets": [
|
| 59 |
+
"Synthetic Multi-Sensor Earth Observation Chips"
|
| 60 |
+
],
|
| 61 |
+
"protocol": "clay_synthetic_v1_5_engineering_v1",
|
| 62 |
+
"format": "NPZ",
|
| 63 |
+
"train_file": "data/train.npz",
|
| 64 |
+
"test_file": "data/test.npz",
|
| 65 |
+
"image_size": 64,
|
| 66 |
+
"train_samples": 4,
|
| 67 |
+
"test_samples": 2,
|
| 68 |
+
"sensors": {
|
| 69 |
+
"sentinel-2-l2a": {
|
| 70 |
+
"channels": 10,
|
| 71 |
+
"gsd_m": 10.0,
|
| 72 |
+
"wavelengths_nm": [493, 560, 665, 704, 740, 783, 842, 865, 1610, 2190],
|
| 73 |
+
"rgb_indices": [2, 1, 0]
|
| 74 |
+
},
|
| 75 |
+
"landsat-c2l2-sr": {
|
| 76 |
+
"channels": 6,
|
| 77 |
+
"gsd_m": 30.0,
|
| 78 |
+
"wavelengths_nm": [650, 560, 480, 860, 1600, 2200],
|
| 79 |
+
"rgb_indices": [0, 1, 2]
|
| 80 |
+
},
|
| 81 |
+
"sentinel-1-rtc": {
|
| 82 |
+
"channels": 2,
|
| 83 |
+
"gsd_m": 10.0,
|
| 84 |
+
"wavelengths_nm": [3500, 4000],
|
| 85 |
+
"rgb_indices": [0, 1, 0]
|
| 86 |
+
}
|
| 87 |
+
},
|
| 88 |
+
"required_metadata": [
|
| 89 |
+
"format_version",
|
| 90 |
+
"data_source",
|
| 91 |
+
"time",
|
| 92 |
+
"latlon",
|
| 93 |
+
"class_target",
|
| 94 |
+
"regression_target",
|
| 95 |
+
"teacher_target",
|
| 96 |
+
"pixels",
|
| 97 |
+
"valid",
|
| 98 |
+
"wavelengths",
|
| 99 |
+
"gsd"
|
| 100 |
+
]
|
| 101 |
+
},
|
| 102 |
+
"configuration_sources": [
|
| 103 |
+
"conf/config.yaml",
|
| 104 |
+
"model/clayfoundation.py",
|
| 105 |
+
"scripts/fake_data.py",
|
| 106 |
+
"scripts/train.py",
|
| 107 |
+
"scripts/inference.py",
|
| 108 |
+
"scripts/result.py"
|
| 109 |
+
]
|
| 110 |
+
}
|
configuration.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"framework": "PyTorch",
|
| 3 |
+
"task": "earth_observation_representation_learning",
|
| 4 |
+
"model": "Clay Foundation Model",
|
| 5 |
+
"input_format": "BCHW",
|
| 6 |
+
"protocol": "engineering_mae_training_and_inference",
|
| 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/clayfoundation.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pure-PyTorch engineering reproduction of the Clay v1.5 model specification."""
|
| 2 |
+
|
| 3 |
+
import math
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
from torch import nn
|
| 7 |
+
from torch.nn import functional as F
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def fourier_encode(values, dim, max_frequency=10000.0):
|
| 11 |
+
"""Encode scalar metadata while preserving exactly ``dim`` output features."""
|
| 12 |
+
if dim < 1:
|
| 13 |
+
return values.new_zeros(*values.shape, 0)
|
| 14 |
+
pairs = (dim + 1) // 2
|
| 15 |
+
frequencies = torch.exp(
|
| 16 |
+
torch.linspace(0, math.log(max_frequency), pairs, device=values.device, dtype=values.dtype)
|
| 17 |
+
)
|
| 18 |
+
angles = values.unsqueeze(-1) * frequencies
|
| 19 |
+
return torch.cat((angles.sin(), angles.cos()), dim=-1)[..., :dim]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def position_encoding_2d(height, width, dim, gsd, device, dtype):
|
| 23 |
+
if dim % 4:
|
| 24 |
+
raise ValueError("spatial position dimension must be divisible by four")
|
| 25 |
+
y, x = torch.meshgrid(
|
| 26 |
+
torch.arange(height, device=device, dtype=dtype),
|
| 27 |
+
torch.arange(width, device=device, dtype=dtype),
|
| 28 |
+
indexing="ij",
|
| 29 |
+
)
|
| 30 |
+
scale = torch.as_tensor(gsd, device=device, dtype=dtype) / 10.0
|
| 31 |
+
quarter = dim // 4
|
| 32 |
+
frequencies = torch.exp(
|
| 33 |
+
torch.arange(quarter, device=device, dtype=dtype) * (-math.log(10000.0) / max(quarter, 1))
|
| 34 |
+
)
|
| 35 |
+
x_angles = x.reshape(-1, 1) * scale * frequencies
|
| 36 |
+
y_angles = y.reshape(-1, 1) * scale * frequencies
|
| 37 |
+
return torch.cat((x_angles.sin(), x_angles.cos(), y_angles.sin(), y_angles.cos()), dim=-1)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def patchify(pixels, patch_size):
|
| 41 |
+
batch, channels, height, width = pixels.shape
|
| 42 |
+
if height % patch_size or width % patch_size:
|
| 43 |
+
raise ValueError("image dimensions must be divisible by patch_size")
|
| 44 |
+
return pixels.reshape(
|
| 45 |
+
batch, channels, height // patch_size, patch_size, width // patch_size, patch_size
|
| 46 |
+
).permute(0, 2, 4, 1, 3, 5).reshape(batch, -1, channels * patch_size * patch_size)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def unpatchify(patches, channels, height, width, patch_size):
|
| 50 |
+
batch = patches.shape[0]
|
| 51 |
+
return patches.reshape(
|
| 52 |
+
batch, height // patch_size, width // patch_size, channels, patch_size, patch_size
|
| 53 |
+
).permute(0, 3, 1, 4, 2, 5).reshape(batch, channels, height, width)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class Transformer(nn.Module):
|
| 57 |
+
def __init__(self, dim, depth, heads, mlp_ratio=4):
|
| 58 |
+
super().__init__()
|
| 59 |
+
layer = nn.TransformerEncoderLayer(
|
| 60 |
+
dim, heads, int(dim * mlp_ratio), activation="gelu", batch_first=True, norm_first=True
|
| 61 |
+
)
|
| 62 |
+
self.layers = nn.TransformerEncoder(layer, depth)
|
| 63 |
+
self.norm = nn.LayerNorm(dim)
|
| 64 |
+
|
| 65 |
+
def forward(self, values):
|
| 66 |
+
return self.norm(self.layers(values))
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class DynamicEmbedding(nn.Module):
|
| 70 |
+
"""Create sensor-agnostic patches by conditioning per-band kernels on wavelength."""
|
| 71 |
+
|
| 72 |
+
def __init__(self, patch_size, embed_dim, wave_dim, wave_latents):
|
| 73 |
+
super().__init__()
|
| 74 |
+
self.patch_size = patch_size
|
| 75 |
+
self.wave_dim = wave_dim
|
| 76 |
+
self.wave_mlp = nn.Sequential(nn.Linear(wave_dim, wave_dim), nn.GELU(), nn.Linear(wave_dim, wave_dim))
|
| 77 |
+
self.latents = nn.Parameter(torch.randn(wave_latents, wave_dim) * 0.02)
|
| 78 |
+
self.cross_attention = nn.MultiheadAttention(wave_dim, 4, batch_first=True)
|
| 79 |
+
self.kernel = nn.Linear(wave_dim, patch_size * patch_size * embed_dim)
|
| 80 |
+
self.bias = nn.Parameter(torch.zeros(embed_dim))
|
| 81 |
+
self.embed_dim = embed_dim
|
| 82 |
+
|
| 83 |
+
def forward(self, pixels, wavelengths):
|
| 84 |
+
batch, channels, height, width = pixels.shape
|
| 85 |
+
if wavelengths.ndim == 1:
|
| 86 |
+
wavelengths = wavelengths[None].expand(batch, -1)
|
| 87 |
+
if wavelengths.shape != (batch, channels):
|
| 88 |
+
raise ValueError(f"wavelengths must have shape {(batch, channels)}, got {tuple(wavelengths.shape)}")
|
| 89 |
+
wave_features = fourier_encode(wavelengths / 1000.0, self.wave_dim)
|
| 90 |
+
wave_features = self.wave_mlp(wave_features)
|
| 91 |
+
queries = self.latents[None].expand(batch, -1, -1)
|
| 92 |
+
context = self.cross_attention(queries, wave_features, wave_features, need_weights=False)[0].mean(dim=1)
|
| 93 |
+
conditioned = wave_features + context[:, None]
|
| 94 |
+
kernels = self.kernel(conditioned).reshape(
|
| 95 |
+
batch, channels, self.embed_dim, self.patch_size, self.patch_size
|
| 96 |
+
)
|
| 97 |
+
patches = []
|
| 98 |
+
for index in range(batch):
|
| 99 |
+
patches.append(F.conv2d(pixels[index:index + 1], kernels[index].permute(1, 0, 2, 3),
|
| 100 |
+
stride=self.patch_size) + self.bias[None, :, None, None])
|
| 101 |
+
return torch.cat(patches).flatten(2).transpose(1, 2), conditioned
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
class DynamicDecoder(nn.Module):
|
| 105 |
+
def __init__(self, patch_size, decoder_dim, wave_dim):
|
| 106 |
+
super().__init__()
|
| 107 |
+
self.patch_size = patch_size
|
| 108 |
+
self.wave_mlp = nn.Sequential(nn.Linear(wave_dim, decoder_dim), nn.GELU(), nn.Linear(decoder_dim, decoder_dim))
|
| 109 |
+
self.output = nn.Linear(decoder_dim, patch_size * patch_size)
|
| 110 |
+
|
| 111 |
+
def forward(self, tokens, wave_features):
|
| 112 |
+
wave_context = self.wave_mlp(wave_features)
|
| 113 |
+
joint = tokens[:, :, None, :] + wave_context[:, None, :, :]
|
| 114 |
+
return self.output(joint).permute(0, 1, 2, 3).flatten(2)
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
class ClayFoundation(nn.Module):
|
| 118 |
+
def __init__(self, config):
|
| 119 |
+
super().__init__()
|
| 120 |
+
self.config = dict(config)
|
| 121 |
+
self.patch_size = int(config["patch_size"])
|
| 122 |
+
self.mask_ratio = float(config["mask_ratio"])
|
| 123 |
+
enc_dim, dec_dim = int(config["encoder_dim"]), int(config["decoder_dim"])
|
| 124 |
+
if enc_dim < 12 or (enc_dim - 8) % 4:
|
| 125 |
+
raise ValueError("encoder_dim - 8 must be positive and divisible by four")
|
| 126 |
+
if dec_dim < 12 or (dec_dim - 8) % 4:
|
| 127 |
+
raise ValueError("decoder_dim - 8 must be positive and divisible by four")
|
| 128 |
+
self.dynamic_embedding = DynamicEmbedding(
|
| 129 |
+
self.patch_size, enc_dim, int(config["wave_dim"]), int(config["wave_latents"])
|
| 130 |
+
)
|
| 131 |
+
self.cls_token = nn.Parameter(torch.randn(1, 1, enc_dim) * 0.02)
|
| 132 |
+
self.encoder = Transformer(enc_dim, int(config["encoder_depth"]), int(config["encoder_heads"]))
|
| 133 |
+
self.encoder_to_decoder = nn.Linear(enc_dim, dec_dim)
|
| 134 |
+
self.mask_token = nn.Parameter(torch.randn(1, 1, dec_dim) * 0.02)
|
| 135 |
+
self.decoder = Transformer(dec_dim, int(config["decoder_depth"]), int(config["decoder_heads"]))
|
| 136 |
+
self.wave_to_decoder = nn.Linear(int(config["wave_dim"]), int(config["wave_dim"]))
|
| 137 |
+
self.dynamic_decoder = DynamicDecoder(self.patch_size, dec_dim, int(config["wave_dim"]))
|
| 138 |
+
self.representation_head = nn.Linear(enc_dim, int(config["teacher_dim"]))
|
| 139 |
+
self.norm_pix_loss = bool(config.get("norm_pix_loss", False))
|
| 140 |
+
|
| 141 |
+
@staticmethod
|
| 142 |
+
def _metadata_encoding(time, latlon, dim):
|
| 143 |
+
values = torch.cat((time, latlon), dim=1)
|
| 144 |
+
widths = [dim // 4] * 4
|
| 145 |
+
for index in range(dim % 4):
|
| 146 |
+
widths[index] += 1
|
| 147 |
+
return torch.cat([fourier_encode(values[:, index], widths[index]) for index in range(4)], dim=1)
|
| 148 |
+
|
| 149 |
+
def _add_encoding(self, tokens, time, latlon, gsd):
|
| 150 |
+
batch, length, dim = tokens.shape
|
| 151 |
+
grid = int(math.sqrt(length))
|
| 152 |
+
if grid * grid != length:
|
| 153 |
+
raise ValueError("Clay reproduction requires a square patch grid")
|
| 154 |
+
spatial = position_encoding_2d(grid, grid, dim - 8, gsd, tokens.device, tokens.dtype)
|
| 155 |
+
metadata = self._metadata_encoding(time, latlon, 8)
|
| 156 |
+
encoding = torch.cat((spatial[None].expand(batch, -1, -1), metadata[:, None].expand(-1, length, -1)), dim=-1)
|
| 157 |
+
return tokens + encoding
|
| 158 |
+
|
| 159 |
+
def encode(self, pixels, time, latlon, gsd, wavelengths):
|
| 160 |
+
patches, _ = self.dynamic_embedding(pixels, wavelengths)
|
| 161 |
+
patches = self._add_encoding(patches, time, latlon, gsd)
|
| 162 |
+
cls = self.cls_token.expand(len(pixels), -1, -1)
|
| 163 |
+
encoded = self.encoder(torch.cat((cls, patches), dim=1))
|
| 164 |
+
return encoded[:, 0], encoded[:, 1:]
|
| 165 |
+
|
| 166 |
+
def forward(self, pixels, time, latlon, gsd, wavelengths, teacher_target=None, mask_ratio=None):
|
| 167 |
+
ratio = self.mask_ratio if mask_ratio is None else float(mask_ratio)
|
| 168 |
+
patches, wave_features = self.dynamic_embedding(pixels, wavelengths)
|
| 169 |
+
patches = self._add_encoding(patches, time, latlon, gsd)
|
| 170 |
+
batch, length, _ = patches.shape
|
| 171 |
+
keep = max(1, length - int(length * ratio))
|
| 172 |
+
noise = torch.rand(batch, length, device=pixels.device)
|
| 173 |
+
ordering = noise.argsort(dim=1)
|
| 174 |
+
unmasked_indices, masked_indices = ordering[:, :keep], ordering[:, keep:]
|
| 175 |
+
gather = unmasked_indices[:, :, None].expand(-1, -1, patches.shape[-1])
|
| 176 |
+
visible = patches.gather(1, gather)
|
| 177 |
+
encoded = self.encoder(torch.cat((self.cls_token.expand(batch, -1, -1), visible), dim=1))
|
| 178 |
+
embedding = encoded[:, 0]
|
| 179 |
+
|
| 180 |
+
decoded_visible = self.encoder_to_decoder(encoded[:, 1:])
|
| 181 |
+
decoder_tokens = self.mask_token.expand(batch, length, -1).clone()
|
| 182 |
+
decoder_tokens.scatter_(1, unmasked_indices[:, :, None].expand(-1, -1, decoded_visible.shape[-1]), decoded_visible)
|
| 183 |
+
grid = int(math.sqrt(length))
|
| 184 |
+
spatial = position_encoding_2d(grid, grid, decoder_tokens.shape[-1] - 8, gsd,
|
| 185 |
+
decoder_tokens.device, decoder_tokens.dtype)
|
| 186 |
+
metadata = self._metadata_encoding(time, latlon, 8)
|
| 187 |
+
decoder_tokens = decoder_tokens + torch.cat((spatial[None].expand(batch, -1, -1),
|
| 188 |
+
metadata[:, None].expand(-1, length, -1)), dim=-1)
|
| 189 |
+
decoded = self.decoder(decoder_tokens)
|
| 190 |
+
predicted_patches = self.dynamic_decoder(decoded, self.wave_to_decoder(wave_features))
|
| 191 |
+
target_patches = patchify(pixels, self.patch_size)
|
| 192 |
+
if self.norm_pix_loss:
|
| 193 |
+
mean = target_patches.mean(dim=-1, keepdim=True)
|
| 194 |
+
variance = target_patches.var(dim=-1, keepdim=True)
|
| 195 |
+
target_patches = (target_patches - mean) / (variance + 1e-6).sqrt()
|
| 196 |
+
mask = torch.zeros(batch, length, device=pixels.device)
|
| 197 |
+
mask.scatter_(1, masked_indices, 1.0)
|
| 198 |
+
patch_loss = (predicted_patches - target_patches).abs().mean(dim=-1)
|
| 199 |
+
reconstruction_loss = (patch_loss * mask).sum() / mask.sum().clamp_min(1)
|
| 200 |
+
projected = F.normalize(self.representation_head(embedding), dim=1)
|
| 201 |
+
if teacher_target is None:
|
| 202 |
+
representation_loss = embedding.new_zeros(())
|
| 203 |
+
else:
|
| 204 |
+
representation_loss = 1.0 - (projected * F.normalize(teacher_target, dim=1)).sum(dim=1).mean()
|
| 205 |
+
reconstruction = unpatchify(predicted_patches, pixels.shape[1], pixels.shape[2], pixels.shape[3], self.patch_size)
|
| 206 |
+
return {
|
| 207 |
+
"embedding": embedding,
|
| 208 |
+
"projected_embedding": projected,
|
| 209 |
+
"reconstruction": reconstruction,
|
| 210 |
+
"mask": mask,
|
| 211 |
+
"reconstruction_loss": reconstruction_loss,
|
| 212 |
+
"representation_loss": representation_loss,
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def compute_loss(outputs, reconstruction_weight=0.95, representation_weight=0.05):
|
| 217 |
+
total = reconstruction_weight * outputs["reconstruction_loss"] + representation_weight * outputs["representation_loss"]
|
| 218 |
+
return total, {
|
| 219 |
+
"reconstruction": outputs["reconstruction_loss"],
|
| 220 |
+
"representation": outputs["representation_loss"],
|
| 221 |
+
"total": total,
|
| 222 |
+
}
|
scripts/fake_data.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Generate deterministic multi-sensor chips for the Clay engineering workflow."""
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
import yaml
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def load_config():
|
| 14 |
+
return yaml.safe_load((ROOT / "conf/config.yaml").read_text())
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def make_split(path, count, config, seed):
|
| 18 |
+
rng = np.random.default_rng(seed)
|
| 19 |
+
size = int(config["data"]["image_size"])
|
| 20 |
+
y, x = np.mgrid[-1:1:complex(size), -1:1:complex(size)].astype(np.float32)
|
| 21 |
+
payload = {
|
| 22 |
+
"format_version": np.asarray(config["data"]["format_version"]),
|
| 23 |
+
"data_source": np.asarray("synthetic"),
|
| 24 |
+
"time": np.empty((count, 2), np.float32),
|
| 25 |
+
"latlon": np.empty((count, 2), np.float32),
|
| 26 |
+
"class_target": np.empty(count, np.int64),
|
| 27 |
+
"regression_target": np.empty(count, np.float32),
|
| 28 |
+
"teacher_target": np.empty((count, config["model"]["teacher_dim"]), np.float32),
|
| 29 |
+
}
|
| 30 |
+
projection = rng.normal(size=(6, config["model"]["teacher_dim"])).astype(np.float32)
|
| 31 |
+
for sample in range(count):
|
| 32 |
+
phase = rng.uniform(0, 2 * np.pi)
|
| 33 |
+
week = rng.uniform(0, 2 * np.pi)
|
| 34 |
+
hour = rng.uniform(0, 2 * np.pi)
|
| 35 |
+
latitude = rng.uniform(-math_pi_over_two(), math_pi_over_two())
|
| 36 |
+
longitude = rng.uniform(-np.pi, np.pi)
|
| 37 |
+
payload["time"][sample] = (week, hour)
|
| 38 |
+
payload["latlon"][sample] = (latitude, longitude)
|
| 39 |
+
payload["class_target"][sample] = int(np.sin(phase) > 0)
|
| 40 |
+
payload["regression_target"][sample] = np.cos(phase) + 0.2 * np.sin(latitude)
|
| 41 |
+
descriptor = np.asarray([
|
| 42 |
+
np.sin(phase), np.cos(phase), np.sin(week), np.cos(week),
|
| 43 |
+
np.sin(latitude), np.cos(longitude),
|
| 44 |
+
], np.float32)
|
| 45 |
+
target = descriptor @ projection
|
| 46 |
+
payload["teacher_target"][sample] = target / np.linalg.norm(target).clip(1e-6)
|
| 47 |
+
|
| 48 |
+
for sensor_index, (name, spec) in enumerate(config["data"]["sensors"].items()):
|
| 49 |
+
channels = int(spec["channels"])
|
| 50 |
+
pixels = np.empty((count, channels, size, size), np.float32)
|
| 51 |
+
valid = np.ones((count, 1, size, size), np.float32)
|
| 52 |
+
for sample in range(count):
|
| 53 |
+
phase = np.arctan2(payload["teacher_target"][sample, 0], payload["teacher_target"][sample, 1])
|
| 54 |
+
base = np.sin((2.0 + sensor_index) * np.pi * x + phase) * np.cos(2 * np.pi * y - phase)
|
| 55 |
+
base += 0.35 * x + 0.15 * y
|
| 56 |
+
for channel in range(channels):
|
| 57 |
+
spectral = 0.12 * channel + 0.2 * np.sin(phase + channel / max(channels, 1))
|
| 58 |
+
pixels[sample, channel] = base + spectral + rng.normal(0, 0.03, (size, size))
|
| 59 |
+
valid[sample, :, :2] = 0
|
| 60 |
+
pixels -= pixels.mean(axis=(0, 2, 3), keepdims=True)
|
| 61 |
+
pixels /= pixels.std(axis=(0, 2, 3), keepdims=True).clip(1e-6)
|
| 62 |
+
payload[f"pixels_{name}"] = np.clip(pixels, -6, 6).astype(np.float32)
|
| 63 |
+
payload[f"valid_{name}"] = valid
|
| 64 |
+
payload[f"wavelengths_{name}"] = np.tile(np.asarray(spec["wavelengths_nm"], np.float32), (count, 1))
|
| 65 |
+
payload[f"gsd_{name}"] = np.full(count, float(spec["gsd"]), np.float32)
|
| 66 |
+
np.savez_compressed(path, **payload)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def math_pi_over_two():
|
| 70 |
+
return np.pi / 2
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def main():
|
| 74 |
+
parser = argparse.ArgumentParser()
|
| 75 |
+
parser.add_argument("--force", action="store_true")
|
| 76 |
+
args = parser.parse_args()
|
| 77 |
+
config = load_config()
|
| 78 |
+
data_dir = ROOT / config["data"]["root"]
|
| 79 |
+
data_dir.mkdir(parents=True, exist_ok=True)
|
| 80 |
+
for offset, (filename, count) in enumerate((
|
| 81 |
+
("train.npz", config["data"]["train_samples"]),
|
| 82 |
+
("test.npz", config["data"]["test_samples"]),
|
| 83 |
+
)):
|
| 84 |
+
path = data_dir / filename
|
| 85 |
+
if args.force or not path.exists():
|
| 86 |
+
make_split(path, int(count), config, int(config["seed"]) + offset)
|
| 87 |
+
print(f"generated={path.relative_to(ROOT)} samples={count} format={config['data']['format_version']}")
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
if __name__ == "__main__":
|
| 91 |
+
main()
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Generate Clay embeddings and MAE reconstructions for every configured sensor."""
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
import torch
|
| 8 |
+
import yaml
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 12 |
+
sys.path.insert(0, str(ROOT))
|
| 13 |
+
from model.clayfoundation import ClayFoundation
|
| 14 |
+
from train import ClayDataset, device_from_config
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def main():
|
| 18 |
+
config = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
|
| 19 |
+
device = device_from_config(config)
|
| 20 |
+
checkpoint = torch.load(ROOT / config["paths"]["checkpoint"], map_location=device, weights_only=True)
|
| 21 |
+
if checkpoint["format_version"] != config["data"]["format_version"]:
|
| 22 |
+
raise ValueError("checkpoint and data formats are incompatible")
|
| 23 |
+
model = ClayFoundation(checkpoint["model_config"]).to(device)
|
| 24 |
+
model.load_state_dict(checkpoint["model"])
|
| 25 |
+
model.eval()
|
| 26 |
+
dataset = ClayDataset(ROOT / config["data"]["root"] / "test.npz", config)
|
| 27 |
+
raw = dataset.data
|
| 28 |
+
payload = {
|
| 29 |
+
"format_version": np.asarray(config["data"]["format_version"]),
|
| 30 |
+
"class_target": raw["class_target"],
|
| 31 |
+
"regression_target": raw["regression_target"],
|
| 32 |
+
}
|
| 33 |
+
time = torch.from_numpy(raw["time"]).to(device)
|
| 34 |
+
latlon = torch.from_numpy(raw["latlon"]).to(device)
|
| 35 |
+
teacher = torch.from_numpy(raw["teacher_target"]).to(device)
|
| 36 |
+
with torch.no_grad():
|
| 37 |
+
for name, spec in config["data"]["sensors"].items():
|
| 38 |
+
pixels = torch.from_numpy(raw[f"pixels_{name}"]).to(device)
|
| 39 |
+
waves = torch.from_numpy(raw[f"wavelengths_{name}"]).to(device)
|
| 40 |
+
outputs = model(pixels, time, latlon, float(spec["gsd"]), waves, teacher, mask_ratio=0.0)
|
| 41 |
+
payload[f"embedding_{name}"] = outputs["embedding"].cpu().numpy()
|
| 42 |
+
payload[f"projected_embedding_{name}"] = outputs["projected_embedding"].cpu().numpy()
|
| 43 |
+
payload[f"reconstruction_{name}"] = outputs["reconstruction"].cpu().numpy()
|
| 44 |
+
payload[f"pixels_{name}"] = raw[f"pixels_{name}"]
|
| 45 |
+
payload[f"reconstruction_loss_{name}"] = np.asarray(float(outputs["reconstruction_loss"]))
|
| 46 |
+
payload[f"representation_loss_{name}"] = np.asarray(float(outputs["representation_loss"]))
|
| 47 |
+
output = ROOT / config["paths"]["inference_dir"] / "predictions.npz"
|
| 48 |
+
output.parent.mkdir(parents=True, exist_ok=True)
|
| 49 |
+
np.savez_compressed(output, **payload)
|
| 50 |
+
print(f"predictions={output.relative_to(ROOT)}")
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
if __name__ == "__main__":
|
| 54 |
+
main()
|
scripts/result.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Evaluate reconstruction and cross-sensor representation consistency."""
|
| 2 |
+
|
| 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 |
+
import yaml
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def main():
|
| 17 |
+
config = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
|
| 18 |
+
predictions = np.load(ROOT / config["paths"]["inference_dir"] / "predictions.npz")
|
| 19 |
+
sensors = list(config["data"]["sensors"])
|
| 20 |
+
metrics = {"samples": int(len(predictions["class_target"])), "sensors": {}}
|
| 21 |
+
projected = []
|
| 22 |
+
for name in sensors:
|
| 23 |
+
embedding = predictions[f"embedding_{name}"]
|
| 24 |
+
projection = predictions[f"projected_embedding_{name}"]
|
| 25 |
+
projected.append(projection)
|
| 26 |
+
metrics["sensors"][name] = {
|
| 27 |
+
"reconstruction_mae": float(np.abs(predictions[f"reconstruction_{name}"] - predictions[f"pixels_{name}"]).mean()),
|
| 28 |
+
"embedding_norm": float(np.linalg.norm(embedding, axis=1).mean()),
|
| 29 |
+
"projected_embedding_norm": float(np.linalg.norm(projection, axis=1).mean()),
|
| 30 |
+
"representation_loss": float(predictions[f"representation_loss_{name}"]),
|
| 31 |
+
}
|
| 32 |
+
similarities = []
|
| 33 |
+
for left in range(len(projected)):
|
| 34 |
+
for right in range(left + 1, len(projected)):
|
| 35 |
+
similarities.append((projected[left] * projected[right]).sum(axis=1))
|
| 36 |
+
metrics["mean_cross_sensor_cosine_similarity"] = float(np.concatenate(similarities).mean())
|
| 37 |
+
output_dir = ROOT / config["paths"]["evaluation_dir"]
|
| 38 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 39 |
+
(output_dir / "metrics.json").write_text(json.dumps(metrics, indent=2) + "\n")
|
| 40 |
+
|
| 41 |
+
figure, axes = plt.subplots(len(sensors), 3, figsize=(9, 3 * len(sensors)), squeeze=False)
|
| 42 |
+
for row, name in enumerate(sensors):
|
| 43 |
+
rgb = config["data"]["sensors"][name]["rgb_indices"]
|
| 44 |
+
source = predictions[f"pixels_{name}"][0, rgb].transpose(1, 2, 0)
|
| 45 |
+
reconstruction = predictions[f"reconstruction_{name}"][0, rgb].transpose(1, 2, 0)
|
| 46 |
+
for image in (source, reconstruction):
|
| 47 |
+
image -= image.min()
|
| 48 |
+
image /= image.max() + 1e-6
|
| 49 |
+
axes[row, 0].imshow(source)
|
| 50 |
+
axes[row, 0].set_title(f"{name} input")
|
| 51 |
+
axes[row, 1].imshow(reconstruction)
|
| 52 |
+
axes[row, 1].set_title("MAE reconstruction")
|
| 53 |
+
axes[row, 2].imshow(np.abs(source - reconstruction).mean(axis=2), cmap="magma")
|
| 54 |
+
axes[row, 2].set_title("absolute error")
|
| 55 |
+
for axis in axes[row]:
|
| 56 |
+
axis.axis("off")
|
| 57 |
+
figure.tight_layout()
|
| 58 |
+
figure.savefig(output_dir / "comparison.png", dpi=150)
|
| 59 |
+
plt.close(figure)
|
| 60 |
+
print(f"metrics={output_dir.relative_to(ROOT) / 'metrics.json'}")
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
if __name__ == "__main__":
|
| 64 |
+
main()
|
scripts/train.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Train the reduced Clay MAE on deterministic multi-sensor synthetic chips."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
import torch
|
| 10 |
+
import yaml
|
| 11 |
+
from torch.nn.parallel import DistributedDataParallel
|
| 12 |
+
from torch.utils.data import DataLoader, Dataset, DistributedSampler
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 16 |
+
sys.path.insert(0, str(ROOT))
|
| 17 |
+
from model.clayfoundation import ClayFoundation, compute_loss
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class ClayDataset(Dataset):
|
| 21 |
+
def __init__(self, path, config):
|
| 22 |
+
self.data = np.load(path)
|
| 23 |
+
self.sensors = config["data"]["sensors"]
|
| 24 |
+
if str(self.data["format_version"]) != config["data"]["format_version"]:
|
| 25 |
+
raise ValueError("incompatible synthetic data format")
|
| 26 |
+
size = int(config["data"]["image_size"])
|
| 27 |
+
for name, spec in self.sensors.items():
|
| 28 |
+
expected = (int(spec["channels"]), size, size)
|
| 29 |
+
if self.data[f"pixels_{name}"].shape[1:] != expected:
|
| 30 |
+
raise ValueError(f"{name} shape does not match {expected}")
|
| 31 |
+
|
| 32 |
+
def __len__(self):
|
| 33 |
+
return len(self.data["time"])
|
| 34 |
+
|
| 35 |
+
def __getitem__(self, index):
|
| 36 |
+
item = {
|
| 37 |
+
"time": torch.from_numpy(self.data["time"][index]),
|
| 38 |
+
"latlon": torch.from_numpy(self.data["latlon"][index]),
|
| 39 |
+
"teacher_target": torch.from_numpy(self.data["teacher_target"][index]),
|
| 40 |
+
}
|
| 41 |
+
for name in self.sensors:
|
| 42 |
+
item[f"pixels_{name}"] = torch.from_numpy(self.data[f"pixels_{name}"][index])
|
| 43 |
+
item[f"valid_{name}"] = torch.from_numpy(self.data[f"valid_{name}"][index])
|
| 44 |
+
item[f"waves_{name}"] = torch.from_numpy(self.data[f"wavelengths_{name}"][index])
|
| 45 |
+
return item
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def device_from_config(config, local_rank=0):
|
| 49 |
+
requested = config["runtime"]["device"]
|
| 50 |
+
if requested == "auto":
|
| 51 |
+
return torch.device("cuda", local_rank) if torch.cuda.is_available() else torch.device("cpu")
|
| 52 |
+
return torch.device(requested)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def main():
|
| 56 |
+
config = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
|
| 57 |
+
torch.manual_seed(int(config["seed"]))
|
| 58 |
+
distributed = int(os.environ.get("WORLD_SIZE", "1")) > 1
|
| 59 |
+
local_rank = int(os.environ.get("LOCAL_RANK", "0"))
|
| 60 |
+
if distributed:
|
| 61 |
+
torch.distributed.init_process_group("nccl" if torch.cuda.is_available() else "gloo")
|
| 62 |
+
rank = torch.distributed.get_rank() if distributed else 0
|
| 63 |
+
device = device_from_config(config, local_rank)
|
| 64 |
+
if device.type == "cuda":
|
| 65 |
+
torch.cuda.set_device(device)
|
| 66 |
+
dataset = ClayDataset(ROOT / config["data"]["root"] / "train.npz", config)
|
| 67 |
+
sampler = DistributedSampler(dataset, shuffle=True) if distributed else None
|
| 68 |
+
loader = DataLoader(dataset, batch_size=int(config["train"]["batch_size"]), sampler=sampler,
|
| 69 |
+
shuffle=sampler is None, num_workers=int(config["train"]["num_workers"]))
|
| 70 |
+
model = ClayFoundation(config["model"]).to(device)
|
| 71 |
+
if distributed:
|
| 72 |
+
model = DistributedDataParallel(model, device_ids=[local_rank] if device.type == "cuda" else None)
|
| 73 |
+
optimizer = torch.optim.AdamW(model.parameters(), lr=float(config["train"]["learning_rate"]),
|
| 74 |
+
weight_decay=float(config["train"]["weight_decay"]), betas=(0.9, 0.95))
|
| 75 |
+
history = []
|
| 76 |
+
for epoch in range(int(config["train"]["epochs"])):
|
| 77 |
+
if sampler:
|
| 78 |
+
sampler.set_epoch(epoch)
|
| 79 |
+
model.train()
|
| 80 |
+
totals = {"reconstruction": 0.0, "representation": 0.0, "total": 0.0}
|
| 81 |
+
steps = 0
|
| 82 |
+
for batch in loader:
|
| 83 |
+
optimizer.zero_grad(set_to_none=True)
|
| 84 |
+
sensor_losses = []
|
| 85 |
+
components = []
|
| 86 |
+
for name, spec in config["data"]["sensors"].items():
|
| 87 |
+
pixels = batch[f"pixels_{name}"].to(device) * batch[f"valid_{name}"].to(device)
|
| 88 |
+
outputs = model(pixels, batch["time"].to(device), batch["latlon"].to(device),
|
| 89 |
+
float(spec["gsd"]), batch[f"waves_{name}"].to(device),
|
| 90 |
+
batch["teacher_target"].to(device))
|
| 91 |
+
loss, values = compute_loss(outputs, float(config["train"]["reconstruction_weight"]),
|
| 92 |
+
float(config["train"]["representation_weight"]))
|
| 93 |
+
sensor_losses.append(loss)
|
| 94 |
+
components.append(values)
|
| 95 |
+
loss = torch.stack(sensor_losses).mean()
|
| 96 |
+
loss.backward()
|
| 97 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
| 98 |
+
optimizer.step()
|
| 99 |
+
for key in totals:
|
| 100 |
+
totals[key] += sum(float(item[key].detach()) for item in components) / len(components)
|
| 101 |
+
steps += 1
|
| 102 |
+
metrics = {key: value / max(steps, 1) for key, value in totals.items()}
|
| 103 |
+
history.append({"epoch": epoch + 1, **metrics})
|
| 104 |
+
if rank == 0:
|
| 105 |
+
print(f"epoch={epoch + 1} total_loss={metrics['total']:.6f} reconstruction={metrics['reconstruction']:.6f}")
|
| 106 |
+
if rank == 0:
|
| 107 |
+
checkpoint = ROOT / config["paths"]["checkpoint"]
|
| 108 |
+
metrics_path = ROOT / config["paths"]["training_metrics"]
|
| 109 |
+
checkpoint.parent.mkdir(parents=True, exist_ok=True)
|
| 110 |
+
metrics_path.parent.mkdir(parents=True, exist_ok=True)
|
| 111 |
+
state = model.module.state_dict() if distributed else model.state_dict()
|
| 112 |
+
torch.save({"model": state, "model_config": config["model"], "sensors": config["data"]["sensors"],
|
| 113 |
+
"format_version": config["data"]["format_version"]}, checkpoint)
|
| 114 |
+
metrics_path.write_text(json.dumps({"history": history}, indent=2) + "\n")
|
| 115 |
+
print(f"checkpoint={checkpoint.relative_to(ROOT)}")
|
| 116 |
+
if distributed:
|
| 117 |
+
torch.distributed.destroy_process_group()
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
if __name__ == "__main__":
|
| 121 |
+
main()
|
weight/.gitkeep
ADDED
|
File without changes
|