Add engineering reproduction package
Browse files- .gitattributes +5 -27
- README.md +151 -0
- README_zh.md +154 -0
- conf/config.yaml +57 -0
- config.json +109 -0
- configuration.json +11 -0
- model/alphaearthfoundations.py +234 -0
- scripts/fake_data.py +118 -0
- scripts/inference.py +60 -0
- scripts/result.py +99 -0
- scripts/train.py +199 -0
- weight/.gitkeep +0 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,13 @@
|
|
| 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 |
*.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 |
-
|
| 27 |
-
*.
|
|
|
|
|
|
|
|
|
|
| 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 |
*.bin filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
*.h5 filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
*.onnx filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 5 |
*.pth filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 6 |
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 12 |
*.tar filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: cc-by-4.0
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
tags:
|
| 6 |
+
- OneScience
|
| 7 |
+
- Earth Science
|
| 8 |
+
- Multi-Source Earth Observation
|
| 9 |
+
- Geospatial Embeddings
|
| 10 |
+
frameworks: PyTorch
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
<p align="center">
|
| 14 |
+
<strong>
|
| 15 |
+
<span style="font-size: 30px;">AlphaEarthFoundations</span>
|
| 16 |
+
</strong>
|
| 17 |
+
</p>
|
| 18 |
+
|
| 19 |
+
# Model Introduction
|
| 20 |
+
|
| 21 |
+
AlphaEarthFoundations is a multi-source spatiotemporal embedding field model for global Earth observation mapping. It jointly encodes sparse and asynchronous data, including optical imagery, radar, LiDAR, climate, elevation, land cover, and geographic text, into 64-dimensional unit-sphere embeddings that support classification, regression, and change detection with limited labels.
|
| 22 |
+
|
| 23 |
+
Paper: AlphaEarth Foundations: An embedding field model for accurate and efficient global mapping from sparse label data
|
| 24 |
+
https://arxiv.org/abs/2507.22291
|
| 25 |
+
|
| 26 |
+
# Model Description
|
| 27 |
+
|
| 28 |
+
AlphaEarth Foundations was proposed by research teams at Google DeepMind and Google. The model was trained on more than three billion observations. It takes Sentinel-2, Sentinel-1, and Landsat-8/9 time-series imagery as input and uses data including PALSAR-2, ERA5-Land, GEDI, GRACE, Copernicus DEM, NLCD, and geographic text as learning targets. The model is suitable for multi-source Earth observation representation learning, thematic mapping with sparse labels, biophysical variable estimation, and time-series change detection.
|
| 29 |
+
|
| 30 |
+
# Use Cases
|
| 31 |
+
|
| 32 |
+
| Use Case | Description |
|
| 33 |
+
| :---: | :--- |
|
| 34 |
+
| Multi-source spatiotemporal representation | Fuse asynchronous time-series observations from Sentinel-2, Sentinel-1, and Landsat-8/9. |
|
| 35 |
+
| Mapping with sparse labels | Train kNN or linear predictors using 64-dimensional embeddings and a small number of point labels. |
|
| 36 |
+
| Biophysical variable estimation | Regress continuous variables such as land surface emissivity and evapotranspiration from embeddings. |
|
| 37 |
+
| Land surface change detection | Compare unit-sphere embeddings from different validity periods to perform supervised or unsupervised change detection. |
|
| 38 |
+
| Local engineering validation | Use a small amount of synthetic data that preserves the paper's actual dimensions to validate training, inference, quantization, evaluation, and visualization workflows. |
|
| 39 |
+
| Multi-GPU training | Launch distributed data-parallel training with `torchrun`. |
|
| 40 |
+
|
| 41 |
+
# Usage Instructions
|
| 42 |
+
|
| 43 |
+
## 1.OneCode
|
| 44 |
+
|
| 45 |
+
Experience intelligent, one-click AI4S programming through the OneCode online environment:
|
| 46 |
+
|
| 47 |
+
[Try intelligent, one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 48 |
+
|
| 49 |
+
## 2.Download and Installation
|
| 50 |
+
|
| 51 |
+
```bash
|
| 52 |
+
hf download OneScience-Group/AlphaEarthFoundations --local-dir ./AlphaEarthFoundations
|
| 53 |
+
cd AlphaEarthFoundations
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
### Environment Dependencies
|
| 57 |
+
|
| 58 |
+
**Hardware Requirements**
|
| 59 |
+
|
| 60 |
+
- A GPU or DCU is recommended.
|
| 61 |
+
- A CPU can be used to verify connectivity with the default small-sample configuration; full training and global inference require large-scale acceleration resources.
|
| 62 |
+
- DCU users must install DTK in advance. DTK 25.04.2 or later, or the OneScience-recommended version matching the current cluster, is recommended.
|
| 63 |
+
|
| 64 |
+
**DCU Environment**
|
| 65 |
+
|
| 66 |
+
```bash
|
| 67 |
+
# Activate DTK and CONDA first
|
| 68 |
+
conda create -n onescience311 python=3.11 -y
|
| 69 |
+
conda activate onescience311
|
| 70 |
+
# uv installation is supported
|
| 71 |
+
pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
**GPU Environment**
|
| 75 |
+
|
| 76 |
+
```bash
|
| 77 |
+
# Activate CONDA first
|
| 78 |
+
conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
|
| 79 |
+
conda activate onescience311
|
| 80 |
+
# uv installation is supported
|
| 81 |
+
pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
### Training Data
|
| 85 |
+
|
| 86 |
+
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 preserves the paper's 1.28 km × 1.28 km extent, 10-meter grid, 65 Sentinel-2 frames, 17 Sentinel-1 frames, 21 Landsat frames, and the explicitly disclosed input-source and training-target channel specifications; only the number of samples and the default internal model width are reduced.
|
| 87 |
+
|
| 88 |
+
```bash
|
| 89 |
+
python scripts/fake_data.py
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
### Training
|
| 93 |
+
|
| 94 |
+
```bash
|
| 95 |
+
python scripts/train.py
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
For multi-GPU training, use:
|
| 99 |
+
|
| 100 |
+
```bash
|
| 101 |
+
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
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
The default configuration is intended for rapid workflow validation. Formal experiments should use the multi-temporal data, complete task labels, model configuration, and training schedule corresponding to the paper.
|
| 105 |
+
|
| 106 |
+
```text
|
| 107 |
+
result/checkpoints/alphaearthfoundations.pt
|
| 108 |
+
result/training/metrics.json
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
### Trained Weights
|
| 112 |
+
|
| 113 |
+
This repository does not include synthetic or trained weights. Training generates `result/checkpoints/alphaearthfoundations.pt`; Google and Google DeepMind have not released the paper's v2.0/v2.1 model weights or checkpoints.
|
| 114 |
+
|
| 115 |
+
### Inference
|
| 116 |
+
|
| 117 |
+
```bash
|
| 118 |
+
python scripts/inference.py
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
Inference loads the training checkpoint and generates float32 unit-sphere embeddings, the paper's `s8²` signed int8 quantized embeddings, and reconstruction results for nine target categories. The outputs are saved to:
|
| 122 |
+
|
| 123 |
+
```text
|
| 124 |
+
result/output/predictions.npz
|
| 125 |
+
```
|
| 126 |
+
|
| 127 |
+
### Evaluation and Visualization
|
| 128 |
+
|
| 129 |
+
```bash
|
| 130 |
+
python scripts/result.py
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
The evaluation reports reconstruction MAE or classification error rates for each training source, the mean embedding norm, and `s8²` quantization error. It also follows the paper's lightweight transfer approach by running kNN with `k=1`, kNN with `k=3`, unregularized linear classification, and linear regression; reporting Balanced Accuracy and R²; and generating comparison plots of the A01, A16, and A09 embedding axes against the land-cover target. Results on synthetic data are intended only to validate the engineering workflow and do not represent the paper's complete performance on 15 real downstream datasets.
|
| 134 |
+
|
| 135 |
+
```text
|
| 136 |
+
result/evaluation/metrics.json
|
| 137 |
+
result/evaluation/comparison.png
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
# Official OneScience Information
|
| 141 |
+
|
| 142 |
+
| Platform | OneScience Main Repository | Skills Repository |
|
| 143 |
+
| --- | --- | --- |
|
| 144 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 145 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 146 |
+
|
| 147 |
+
# Citation and License
|
| 148 |
+
|
| 149 |
+
This repository is an engineering reproduction of the original AlphaEarth Foundations paper.
|
| 150 |
+
|
| 151 |
+
Use of the code and data in this repository remains subject to the licenses and terms of use of their respective projects.
|
README_zh.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: CC-BY-4.0
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
- zh
|
| 6 |
+
tags:
|
| 7 |
+
- OneScience
|
| 8 |
+
- 地球科学
|
| 9 |
+
- 多源地球观测
|
| 10 |
+
- 地理空间嵌入
|
| 11 |
+
frameworks: PyTorch
|
| 12 |
+
datasets:
|
| 13 |
+
- Google Satellite Embedding V1 Annual
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
<p align="center">
|
| 17 |
+
<strong>
|
| 18 |
+
<span style="font-size: 30px;">AlphaEarthFoundations</span>
|
| 19 |
+
</strong>
|
| 20 |
+
</p>
|
| 21 |
+
|
| 22 |
+
# 模型介绍
|
| 23 |
+
|
| 24 |
+
AlphaEarthFoundations 是面向全球地球观测制图的多源时空嵌入场模型,将光学、雷达、LiDAR、气候、高程、土地覆盖和地理文本等稀疏且异步的数据统一编码为 64 维单位球嵌入,以少量标签支持分类、回归和变化检测。
|
| 25 |
+
|
| 26 |
+
论文:AlphaEarth Foundations: An embedding field model for accurate and efficient global mapping from sparse label data
|
| 27 |
+
https://arxiv.org/abs/2507.22291
|
| 28 |
+
|
| 29 |
+
# 模型描述
|
| 30 |
+
|
| 31 |
+
AlphaEarth Foundations 由 Google DeepMind 与 Google 的研究团队提出。模型使用超过 30 亿次观测训练,输入 Sentinel-2、Sentinel-1 和 Landsat-8/9 时序影像,并以 PALSAR-2、ERA5-Land、GEDI、GRACE、Copernicus DEM、NLCD 和地理文本等数据作为学习目标。模型适用于多源地球观测表征学习、稀疏标签专题制图、生物物理变量估计和时序变化检测。
|
| 32 |
+
|
| 33 |
+
# 适用场景
|
| 34 |
+
|
| 35 |
+
| 场景 | 说明 |
|
| 36 |
+
| :---: | :--- |
|
| 37 |
+
| 多源时空表征 | 融合 Sentinel-2、Sentinel-1 和 Landsat-8/9 的异步时序观测。 |
|
| 38 |
+
| 稀疏标签制图 | 使用 64 维 embedding 和少量点标签训练 kNN 或线性预测器。 |
|
| 39 |
+
| 生物物理变量估计 | 基于 embedding 回归地表发射率、蒸散量等连续变量。 |
|
| 40 |
+
| 地表变化检测 | 比较不同有效时段的单位球 embedding,执行监督或无监督变化检测。 |
|
| 41 |
+
| 本地工程验证 | 使用保持论文真实维度的少量虚拟数据检查训练、推理、量化、评估和可视化流程。 |
|
| 42 |
+
| 多卡训练 | 通过 `torchrun` 启动分布式数据并行训练。 |
|
| 43 |
+
|
| 44 |
+
# 使用说明
|
| 45 |
+
|
| 46 |
+
## 1.OneCode
|
| 47 |
+
|
| 48 |
+
可通过 OneCode 在线环境体验智能化一键式 AI4S 编程:
|
| 49 |
+
|
| 50 |
+
[点击体验智能化一键式 AI4S 编程](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 51 |
+
|
| 52 |
+
## 2.下载安装
|
| 53 |
+
|
| 54 |
+
```bash
|
| 55 |
+
modelscope download --model OneScience/AlphaEarthFoundations --local_dir ./AlphaEarthFoundations
|
| 56 |
+
cd AlphaEarthFoundations
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
### 环境依赖
|
| 60 |
+
|
| 61 |
+
**硬件要求**
|
| 62 |
+
|
| 63 |
+
- 推荐使用 GPU 或 DCU 运行。
|
| 64 |
+
- CPU 可用于默认小样本配置的连通性验证,完整训练和全球推理需要大规模加速资源。
|
| 65 |
+
- DCU 用户需预先安装 DTK,建议使用 DTK 25.04.2 以上版本或与当前集群匹配的 OneScience 推荐版本。
|
| 66 |
+
|
| 67 |
+
**DCU环境**
|
| 68 |
+
|
| 69 |
+
```bash
|
| 70 |
+
# 请首先激活DTK及CONDA
|
| 71 |
+
conda create -n onescience311 python=3.11 -y
|
| 72 |
+
conda activate onescience311
|
| 73 |
+
# 支持uv安装
|
| 74 |
+
pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
**GPU环境**
|
| 78 |
+
|
| 79 |
+
```bash
|
| 80 |
+
# 请首先激活CONDA
|
| 81 |
+
conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
|
| 82 |
+
conda activate onescience311
|
| 83 |
+
# 支持uv安装
|
| 84 |
+
pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
### 训练数据介绍
|
| 88 |
+
|
| 89 |
+
本仓库使用少量虚拟样本验证工程流程,训练数据和测试数据分别保存为 `data/train.npz` 和 `data/test.npz`。虚拟数据保持论文的 1.28 km × 1.28 km、10 米网格、Sentinel-2 65 帧、Sentinel-1 17 帧、Landsat 21 帧,以及论文明确公开的输入源和训练目标通道规格;仅样本数量和默认模型内部宽度被缩小。
|
| 90 |
+
|
| 91 |
+
```bash
|
| 92 |
+
python scripts/fake_data.py
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
### 训练
|
| 96 |
+
|
| 97 |
+
```bash
|
| 98 |
+
python scripts/train.py
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
多卡训练可使用:
|
| 102 |
+
|
| 103 |
+
```bash
|
| 104 |
+
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
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
默认配置面向快速流程验证;开展正式实验时,应使用论文对应的多时相数据、完整任务标签、模型配置和训练周期。
|
| 108 |
+
|
| 109 |
+
```text
|
| 110 |
+
result/checkpoints/alphaearthfoundations.pt
|
| 111 |
+
result/training/metrics.json
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
### 训练权重
|
| 115 |
+
|
| 116 |
+
本仓库不内置虚拟权重或训练权重。执行训练后生成 `result/checkpoints/alphaearthfoundations.pt`;Google 和 Google DeepMind 尚未公开论文 v2.0/v2.1 模型权重或 checkpoint。
|
| 117 |
+
|
| 118 |
+
### 推理
|
| 119 |
+
|
| 120 |
+
```bash
|
| 121 |
+
python scripts/inference.py
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
推理加载训练 checkpoint,生成 float32 单位球 embedding、论文 `s8²` 有符号 int8 量化 embedding 和九类目标重建结果,并保存到:
|
| 125 |
+
|
| 126 |
+
```text
|
| 127 |
+
result/output/predictions.npz
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
### 评估和可视化
|
| 131 |
+
|
| 132 |
+
```bash
|
| 133 |
+
python scripts/result.py
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
评估报告各训练源的重建 MAE 或分类错误率、embedding 平均范数和 `s8²` 量化误差;同时按论文的轻量迁移方式执行 kNN `k=1`、kNN `k=3`、无正则线性分类和线性回归,报告 Balanced Accuracy 与 R²,并生成 A01/A16/A09 三个 embedding 轴与土地覆盖目标的对比图。虚拟数据结���仅用于验证工程流程,不代表论文在 15 个真实下游数据集上的完整性能。
|
| 137 |
+
|
| 138 |
+
```text
|
| 139 |
+
result/evaluation/metrics.json
|
| 140 |
+
result/evaluation/comparison.png
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
# OneScience官方信息
|
| 144 |
+
|
| 145 |
+
| 平台 | OneScience 主仓库 | Skills 仓库 |
|
| 146 |
+
| --- | --- | --- |
|
| 147 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 148 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 149 |
+
|
| 150 |
+
# 引用与许可证
|
| 151 |
+
|
| 152 |
+
本仓库为 AlphaEarth Foundations 原始论文的工程复现版本。
|
| 153 |
+
|
| 154 |
+
本仓库代码和数据的使用仍应以各自项目中的许可证及使用条款为准。
|
conf/config.yaml
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
seed: 42
|
| 2 |
+
data:
|
| 3 |
+
root: data
|
| 4 |
+
format_version: aef_synthetic_paper_spec_v3
|
| 5 |
+
train_samples: 2
|
| 6 |
+
test_samples: 1
|
| 7 |
+
image_size: 128
|
| 8 |
+
input_sources:
|
| 9 |
+
sentinel2: {timesteps: 65, channels: 5, geometry_dim: 0}
|
| 10 |
+
sentinel1: {timesteps: 17, channels: 5, geometry_dim: 2}
|
| 11 |
+
landsat: {timesteps: 21, channels: 7, geometry_dim: 0}
|
| 12 |
+
target_sources:
|
| 13 |
+
sentinel2: {channels: 5, type: continuous, weight: 1.0, loss_grid_m: 10, shift_pixels: 2, geometry_dim: 0}
|
| 14 |
+
sentinel1: {channels: 5, type: continuous, weight: 1.0, loss_grid_m: 10, shift_pixels: 2, geometry_dim: 2}
|
| 15 |
+
landsat: {channels: 7, type: continuous, weight: 1.0, loss_grid_m: 30, geometry_dim: 0}
|
| 16 |
+
palsar2: {channels: 3, type: continuous, weight: 1.0, loss_grid_m: 30, geometry_dim: 2}
|
| 17 |
+
era5_land: {channels: 12, type: continuous, weight: 1.0, loss_grid_m: 10, geometry_dim: 0}
|
| 18 |
+
gedi: {channels: 101, type: continuous, weight: 1.0, loss_grid_m: 20, geometry_dim: 0}
|
| 19 |
+
grace: {channels: 1, type: continuous, weight: 0.5, loss_grid_m: 1280, geometry_dim: 0}
|
| 20 |
+
glo30: {channels: 1, type: continuous, weight: 1.0, loss_grid_m: 30, geometry_dim: 0, derived_channels: undisclosed}
|
| 21 |
+
nlcd: {channels: 16, type: categorical, weight: 0.5, loss_grid_m: 30, geometry_dim: 0}
|
| 22 |
+
model:
|
| 23 |
+
precision_dim: 8
|
| 24 |
+
time_dim: 16
|
| 25 |
+
space_dim: 32
|
| 26 |
+
embedding_dim: 64
|
| 27 |
+
num_blocks: 1
|
| 28 |
+
num_heads: 4
|
| 29 |
+
decoder_hidden_dim: 32
|
| 30 |
+
vmf_kappa: 8000.0
|
| 31 |
+
max_geometry_dim: 2
|
| 32 |
+
paper_model:
|
| 33 |
+
precision_dim: 128
|
| 34 |
+
time_dim: 512
|
| 35 |
+
space_dim: 1024
|
| 36 |
+
embedding_dim: 64
|
| 37 |
+
num_blocks: 15
|
| 38 |
+
parameter_count: approximately_480M
|
| 39 |
+
train:
|
| 40 |
+
epochs: 1
|
| 41 |
+
batch_size: 2
|
| 42 |
+
learning_rate: 0.0005
|
| 43 |
+
weight_decay: 0.000001
|
| 44 |
+
reconstruction_weight: 1.0
|
| 45 |
+
uniformity_weight: 0.05
|
| 46 |
+
consistency_weight: 0.02
|
| 47 |
+
text_weight: 0.001
|
| 48 |
+
source_dropout: {sentinel2: 0.0, sentinel1: 0.3, landsat: 0.3}
|
| 49 |
+
frame_dropout: {sentinel2: 0.5, sentinel1: 0.3, landsat: 0.3}
|
| 50 |
+
num_workers: 0
|
| 51 |
+
runtime:
|
| 52 |
+
device: auto
|
| 53 |
+
paths:
|
| 54 |
+
checkpoint: result/checkpoints/alphaearthfoundations.pt
|
| 55 |
+
training_metrics: result/training/metrics.json
|
| 56 |
+
inference_dir: result/output
|
| 57 |
+
evaluation_dir: result/evaluation
|
config.json
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_name": "AlphaEarthFoundations",
|
| 3 |
+
"model_type": "alphaearthfoundations",
|
| 4 |
+
"architectures": [
|
| 5 |
+
"AlphaEarthFoundations",
|
| 6 |
+
"STPBlock",
|
| 7 |
+
"ConditionalDecoder"
|
| 8 |
+
],
|
| 9 |
+
"framework": "PyTorch",
|
| 10 |
+
"domain": "earth-observation",
|
| 11 |
+
"task": "multi-source-geospatial-embedding",
|
| 12 |
+
"implementation": {
|
| 13 |
+
"entry_point": "model/alphaearthfoundations.py",
|
| 14 |
+
"scope": "engineering reproduction of a multi-source spatiotemporal embedding field for sparse-label global Earth observation mapping",
|
| 15 |
+
"train_script": "scripts/train.py",
|
| 16 |
+
"inference_script": "scripts/inference.py",
|
| 17 |
+
"evaluation_script": "scripts/result.py",
|
| 18 |
+
"synthetic_data_script": "scripts/fake_data.py"
|
| 19 |
+
},
|
| 20 |
+
"architecture": {
|
| 21 |
+
"family": "spatiotemporal precision-time-space embedding field",
|
| 22 |
+
"input_sources": [
|
| 23 |
+
"sentinel2",
|
| 24 |
+
"sentinel1",
|
| 25 |
+
"landsat"
|
| 26 |
+
],
|
| 27 |
+
"precision_dim": 8,
|
| 28 |
+
"time_dim": 16,
|
| 29 |
+
"space_dim": 32,
|
| 30 |
+
"embedding_dim": 64,
|
| 31 |
+
"num_blocks": 1,
|
| 32 |
+
"num_heads": 4,
|
| 33 |
+
"decoder_hidden_dim": 32,
|
| 34 |
+
"vmf_kappa": 8000.0,
|
| 35 |
+
"max_geometry_dim": 2,
|
| 36 |
+
"embedding_normalization": "L2 unit sphere",
|
| 37 |
+
"paper_model": {
|
| 38 |
+
"precision_dim": 128,
|
| 39 |
+
"time_dim": 512,
|
| 40 |
+
"space_dim": 1024,
|
| 41 |
+
"embedding_dim": 64,
|
| 42 |
+
"num_blocks": 15,
|
| 43 |
+
"parameter_count": "approximately 480M"
|
| 44 |
+
}
|
| 45 |
+
},
|
| 46 |
+
"data": {
|
| 47 |
+
"datasets": [
|
| 48 |
+
"Google Satellite Embedding V1 Annual"
|
| 49 |
+
],
|
| 50 |
+
"protocol": "aef_synthetic_paper_spec_v3",
|
| 51 |
+
"format": "NPZ",
|
| 52 |
+
"train_file": "data/train.npz",
|
| 53 |
+
"test_file": "data/test.npz",
|
| 54 |
+
"image_size": 128,
|
| 55 |
+
"spatial_extent_km": [
|
| 56 |
+
1.28,
|
| 57 |
+
1.28
|
| 58 |
+
],
|
| 59 |
+
"grid_resolution_m": 10,
|
| 60 |
+
"input_sources": {
|
| 61 |
+
"sentinel2": {
|
| 62 |
+
"timesteps": 65,
|
| 63 |
+
"channels": 5,
|
| 64 |
+
"geometry_dim": 0
|
| 65 |
+
},
|
| 66 |
+
"sentinel1": {
|
| 67 |
+
"timesteps": 17,
|
| 68 |
+
"channels": 5,
|
| 69 |
+
"geometry_dim": 2
|
| 70 |
+
},
|
| 71 |
+
"landsat": {
|
| 72 |
+
"timesteps": 21,
|
| 73 |
+
"channels": 7,
|
| 74 |
+
"geometry_dim": 0
|
| 75 |
+
}
|
| 76 |
+
},
|
| 77 |
+
"target_sources": {
|
| 78 |
+
"sentinel2": {"channels": 5, "type": "continuous", "loss_grid_m": 10},
|
| 79 |
+
"sentinel1": {"channels": 5, "type": "continuous", "loss_grid_m": 10},
|
| 80 |
+
"landsat": {"channels": 7, "type": "continuous", "loss_grid_m": 30},
|
| 81 |
+
"palsar2": {"channels": 3, "type": "continuous", "loss_grid_m": 30},
|
| 82 |
+
"era5_land": {"channels": 12, "type": "continuous", "loss_grid_m": 10},
|
| 83 |
+
"gedi": {"channels": 101, "type": "continuous", "loss_grid_m": 20},
|
| 84 |
+
"grace": {"channels": 1, "type": "continuous", "loss_grid_m": 1280},
|
| 85 |
+
"glo30": {"channels": 1, "type": "continuous", "loss_grid_m": 30},
|
| 86 |
+
"nlcd": {"channels": 16, "type": "categorical", "loss_grid_m": 30}
|
| 87 |
+
},
|
| 88 |
+
"required_metadata": [
|
| 89 |
+
"format_version",
|
| 90 |
+
"data_source",
|
| 91 |
+
"timestamps",
|
| 92 |
+
"frame_available",
|
| 93 |
+
"channel_available",
|
| 94 |
+
"pixel_valid",
|
| 95 |
+
"geometry",
|
| 96 |
+
"support_period",
|
| 97 |
+
"valid_period",
|
| 98 |
+
"text_target"
|
| 99 |
+
]
|
| 100 |
+
},
|
| 101 |
+
"configuration_sources": [
|
| 102 |
+
"conf/config.yaml",
|
| 103 |
+
"model/alphaearthfoundations.py",
|
| 104 |
+
"scripts/fake_data.py",
|
| 105 |
+
"scripts/train.py",
|
| 106 |
+
"scripts/inference.py",
|
| 107 |
+
"scripts/result.py"
|
| 108 |
+
]
|
| 109 |
+
}
|
configuration.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"framework": "PyTorch",
|
| 3 |
+
"task": "geospatial-embedding-field",
|
| 4 |
+
"model": "AlphaEarthFoundations",
|
| 5 |
+
"paper": "https://arxiv.org/abs/2507.22291",
|
| 6 |
+
"dataset": "GOOGLE/SATELLITE_EMBEDDING/V1/ANNUAL",
|
| 7 |
+
"license": "CC-BY-4.0",
|
| 8 |
+
"allow_remote": true,
|
| 9 |
+
"model": {"type": "AlphaEarthFoundations", "repo_id": "OneScience/AlphaEarthFoundations"},
|
| 10 |
+
"pipeline": {"type": "earth-observation-embedding"}
|
| 11 |
+
}
|
model/alphaearthfoundations.py
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Engineering reproduction of AlphaEarth Foundations from the paper 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 sinusoidal_timecode(timestamps, dim, origin=None, scale=365.25 * 24 * 3600 * 1000):
|
| 11 |
+
if origin is None:
|
| 12 |
+
origin = timestamps.amin(dim=1, keepdim=True)
|
| 13 |
+
values = (timestamps.double() - origin.double()) / scale
|
| 14 |
+
values = values.float()
|
| 15 |
+
frequencies = torch.exp(
|
| 16 |
+
torch.arange(0, dim, 2, device=timestamps.device) * (-math.log(10000.0) / dim)
|
| 17 |
+
)
|
| 18 |
+
angles = values.unsqueeze(-1) * frequencies
|
| 19 |
+
code = torch.zeros(*timestamps.shape, dim, device=timestamps.device)
|
| 20 |
+
code[..., 0::2] = angles.sin()
|
| 21 |
+
code[..., 1::2] = angles.cos()
|
| 22 |
+
return code
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class STPBlock(nn.Module):
|
| 26 |
+
"""Parallel precision, time and space operators with learned pyramid exchange."""
|
| 27 |
+
|
| 28 |
+
def __init__(self, precision_dim, time_dim, space_dim, num_heads):
|
| 29 |
+
super().__init__()
|
| 30 |
+
self.precision = nn.Sequential(
|
| 31 |
+
nn.GroupNorm(1, precision_dim),
|
| 32 |
+
nn.Conv2d(precision_dim, precision_dim, 3, padding=1),
|
| 33 |
+
nn.GELU(),
|
| 34 |
+
nn.Conv2d(precision_dim, precision_dim, 3, padding=1),
|
| 35 |
+
)
|
| 36 |
+
self.time_norm = nn.LayerNorm(time_dim)
|
| 37 |
+
self.time_attention = nn.MultiheadAttention(time_dim, num_heads, batch_first=True)
|
| 38 |
+
self.space_norm = nn.LayerNorm(space_dim)
|
| 39 |
+
self.space_attention = nn.MultiheadAttention(space_dim, num_heads, batch_first=True)
|
| 40 |
+
self.to_precision = nn.ModuleList([nn.Conv2d(time_dim, precision_dim, 1), nn.Conv2d(space_dim, precision_dim, 1)])
|
| 41 |
+
self.to_time = nn.Conv2d(precision_dim, time_dim, 1)
|
| 42 |
+
self.to_space = nn.Conv2d(precision_dim, space_dim, 1)
|
| 43 |
+
|
| 44 |
+
def forward(self, precision, time, space, frame_available):
|
| 45 |
+
batch, frames = precision.shape[:2]
|
| 46 |
+
p_size, t_size, s_size = precision.shape[-2:], time.shape[-2:], space.shape[-2:]
|
| 47 |
+
p = precision.flatten(0, 1)
|
| 48 |
+
p = p + self.precision(p)
|
| 49 |
+
|
| 50 |
+
sequence = time.permute(0, 3, 4, 1, 2).reshape(-1, frames, time.shape[2])
|
| 51 |
+
normalized = self.time_norm(sequence)
|
| 52 |
+
time_mask = (~frame_available.bool())[:, None, None, :].expand(batch, *t_size, frames).reshape(-1, frames)
|
| 53 |
+
sequence = sequence + self.time_attention(
|
| 54 |
+
normalized, normalized, normalized, key_padding_mask=time_mask, need_weights=False
|
| 55 |
+
)[0]
|
| 56 |
+
time = sequence.reshape(batch, *t_size, frames, -1).permute(0, 3, 4, 1, 2)
|
| 57 |
+
|
| 58 |
+
available = frame_available[:, :, None, None, None].to(space.dtype)
|
| 59 |
+
spatial = (space * available).sum(dim=1) / available.sum(dim=1).clamp_min(1)
|
| 60 |
+
spatial = spatial.flatten(2).transpose(1, 2)
|
| 61 |
+
normalized = self.space_norm(spatial)
|
| 62 |
+
spatial = spatial + self.space_attention(normalized, normalized, normalized, need_weights=False)[0]
|
| 63 |
+
spatial = spatial.transpose(1, 2).reshape(batch, -1, *s_size)
|
| 64 |
+
space = space + spatial[:, None]
|
| 65 |
+
|
| 66 |
+
t_flat, s_flat = time.flatten(0, 1), space.flatten(0, 1)
|
| 67 |
+
precision = p + self.to_precision[0](F.interpolate(t_flat, p_size, mode="bilinear", align_corners=False))
|
| 68 |
+
precision = precision + self.to_precision[1](F.interpolate(s_flat, p_size, mode="bilinear", align_corners=False))
|
| 69 |
+
time = time + self.to_time(F.interpolate(p, t_size, mode="bilinear", align_corners=False)).unflatten(0, (batch, frames))
|
| 70 |
+
space = space + self.to_space(F.interpolate(p, s_size, mode="bilinear", align_corners=False)).unflatten(0, (batch, frames))
|
| 71 |
+
return precision.unflatten(0, (batch, frames)), time, space
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class ConditionalDecoder(nn.Module):
|
| 75 |
+
def __init__(self, embedding_dim, condition_dim, hidden_dim, output_dim):
|
| 76 |
+
super().__init__()
|
| 77 |
+
self.condition = nn.Linear(condition_dim, hidden_dim)
|
| 78 |
+
self.network = nn.Sequential(
|
| 79 |
+
nn.Conv2d(embedding_dim + hidden_dim, hidden_dim, 1),
|
| 80 |
+
nn.GELU(),
|
| 81 |
+
nn.Conv2d(hidden_dim, hidden_dim, 1),
|
| 82 |
+
nn.GELU(),
|
| 83 |
+
nn.Conv2d(hidden_dim, output_dim, 1),
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
def forward(self, embedding, condition):
|
| 87 |
+
context = self.condition(condition)[:, :, None, None].expand(-1, -1, *embedding.shape[-2:])
|
| 88 |
+
return self.network(torch.cat([embedding, context], dim=1))
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
class AlphaEarthFoundations(nn.Module):
|
| 92 |
+
def __init__(self, input_sources, target_sources, config):
|
| 93 |
+
super().__init__()
|
| 94 |
+
p_dim, t_dim, s_dim = config["precision_dim"], config["time_dim"], config["space_dim"]
|
| 95 |
+
self.input_names = list(input_sources)
|
| 96 |
+
self.target_sources = target_sources
|
| 97 |
+
self.embedding_dim = config["embedding_dim"]
|
| 98 |
+
self.vmf_kappa = float(config["vmf_kappa"])
|
| 99 |
+
self.projectors = nn.ModuleDict({
|
| 100 |
+
name: nn.Sequential(nn.Conv2d(spec["channels"], p_dim, 3, stride=2, padding=1), nn.GELU())
|
| 101 |
+
for name, spec in input_sources.items()
|
| 102 |
+
})
|
| 103 |
+
self.time_projector = nn.Conv2d(p_dim, t_dim, 3, stride=4, padding=1)
|
| 104 |
+
self.space_projector = nn.Conv2d(p_dim, s_dim, 3, stride=8, padding=1)
|
| 105 |
+
self.time_context = nn.Linear(t_dim, t_dim)
|
| 106 |
+
self.blocks = nn.ModuleList([
|
| 107 |
+
STPBlock(p_dim, t_dim, s_dim, config["num_heads"]) for _ in range(config["num_blocks"])
|
| 108 |
+
])
|
| 109 |
+
self.summary_query = nn.Linear(t_dim * 2, p_dim)
|
| 110 |
+
self.embedding_head = nn.Conv2d(p_dim, self.embedding_dim, 1)
|
| 111 |
+
self.embedding_upsample = nn.ConvTranspose2d(p_dim, p_dim, 4, stride=2, padding=1)
|
| 112 |
+
condition_dim = t_dim + config["max_geometry_dim"]
|
| 113 |
+
self.decoders = nn.ModuleDict({
|
| 114 |
+
name: ConditionalDecoder(self.embedding_dim, condition_dim, config["decoder_hidden_dim"], spec["channels"])
|
| 115 |
+
for name, spec in target_sources.items()
|
| 116 |
+
})
|
| 117 |
+
|
| 118 |
+
def _summarize(self, precision, availability, period, origin):
|
| 119 |
+
period_codes = sinusoidal_timecode(period, self.time_context.in_features, origin)
|
| 120 |
+
query = self.summary_query(period_codes.flatten(1))
|
| 121 |
+
scores = (precision * query[:, None, :, None, None]).sum(dim=2).mean(dim=(-1, -2))
|
| 122 |
+
scores = scores.masked_fill(~availability.bool(), torch.finfo(scores.dtype).min)
|
| 123 |
+
summary = (precision * scores.softmax(dim=1)[:, :, None, None, None]).sum(dim=1)
|
| 124 |
+
return F.normalize(self.embedding_head(self.embedding_upsample(summary)), dim=1)
|
| 125 |
+
|
| 126 |
+
def forward(self, sources, timestamps, valid_period, frame_available, target_times=None,
|
| 127 |
+
target_geometry=None, target_periods=None):
|
| 128 |
+
precision_parts, code_parts = [], []
|
| 129 |
+
origin = torch.cat(list(timestamps.values()), dim=1).amin(dim=1, keepdim=True)
|
| 130 |
+
for name in self.input_names:
|
| 131 |
+
values = sources[name]
|
| 132 |
+
batch, frames = values.shape[:2]
|
| 133 |
+
projected = self.projectors[name](values.flatten(0, 1)).unflatten(0, (batch, frames))
|
| 134 |
+
precision_parts.append(projected)
|
| 135 |
+
code_parts.append(sinusoidal_timecode(timestamps[name], self.time_context.in_features, origin))
|
| 136 |
+
availability = torch.cat([frame_available[name] for name in self.input_names], dim=1)
|
| 137 |
+
precision = torch.cat(precision_parts, dim=1)
|
| 138 |
+
codes = torch.cat(code_parts, dim=1)
|
| 139 |
+
time = self.time_projector(precision.flatten(0, 1)).unflatten(0, precision.shape[:2])
|
| 140 |
+
time = time + self.time_context(codes)[:, :, :, None, None]
|
| 141 |
+
space = self.space_projector(precision.flatten(0, 1)).unflatten(0, precision.shape[:2])
|
| 142 |
+
for block in self.blocks:
|
| 143 |
+
precision, time, space = block(precision, time, space, availability)
|
| 144 |
+
|
| 145 |
+
embedding = self._summarize(precision, availability, valid_period, origin)
|
| 146 |
+
outputs = {"embedding": embedding}
|
| 147 |
+
if target_times is not None:
|
| 148 |
+
outputs["reconstructions"] = {}
|
| 149 |
+
for name in self.target_sources:
|
| 150 |
+
source_embedding = self._summarize(precision, availability, target_periods[name], origin)
|
| 151 |
+
if self.training:
|
| 152 |
+
source_embedding = F.normalize(
|
| 153 |
+
source_embedding + torch.randn_like(source_embedding) / math.sqrt(self.vmf_kappa), dim=1
|
| 154 |
+
)
|
| 155 |
+
relative_time = (
|
| 156 |
+
(target_times[name] - target_periods[name][:, 0]).float()
|
| 157 |
+
/ (target_periods[name][:, 1] - target_periods[name][:, 0]).float().clamp_min(1)
|
| 158 |
+
)
|
| 159 |
+
time_code = sinusoidal_timecode(
|
| 160 |
+
relative_time[:, None], self.time_context.in_features,
|
| 161 |
+
torch.zeros_like(relative_time[:, None]), scale=1.0
|
| 162 |
+
)[:, 0]
|
| 163 |
+
geometry = target_geometry[name]
|
| 164 |
+
outputs["reconstructions"][name] = self.decoders[name](source_embedding, torch.cat([time_code, geometry], dim=1))
|
| 165 |
+
return outputs
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def _pool_continuous(values, grid_m):
|
| 169 |
+
if grid_m == 10:
|
| 170 |
+
return values
|
| 171 |
+
size = max(1, round(values.shape[-1] * 10 / grid_m))
|
| 172 |
+
return F.adaptive_avg_pool2d(values, (size, size))
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def _shift_invariant_l1(prediction, target, mask, radius):
|
| 176 |
+
losses = []
|
| 177 |
+
for dy in range(-radius, radius + 1):
|
| 178 |
+
for dx in range(-radius, radius + 1):
|
| 179 |
+
shifted = torch.roll(prediction, (dy, dx), dims=(-2, -1))
|
| 180 |
+
valid = mask.clone()
|
| 181 |
+
if dy > 0: valid[..., :dy, :] = 0
|
| 182 |
+
if dy < 0: valid[..., dy:, :] = 0
|
| 183 |
+
if dx > 0: valid[..., :, :dx] = 0
|
| 184 |
+
if dx < 0: valid[..., :, dx:] = 0
|
| 185 |
+
losses.append((torch.abs(shifted - target) * valid).sum() / valid.sum().clamp_min(1))
|
| 186 |
+
return torch.stack(losses).amin()
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def compute_losses(teacher, student, targets, masks, text_target, target_sources, weights):
|
| 190 |
+
reconstruction = teacher["embedding"].new_zeros(())
|
| 191 |
+
components = {}
|
| 192 |
+
for name, spec in target_sources.items():
|
| 193 |
+
prediction, target, mask = teacher["reconstructions"][name], targets[name], masks[name]
|
| 194 |
+
grid_m = int(spec["loss_grid_m"])
|
| 195 |
+
if spec["type"] == "categorical":
|
| 196 |
+
size = max(1, round(prediction.shape[-1] * 10 / grid_m))
|
| 197 |
+
prediction = F.adaptive_avg_pool2d(prediction, (size, size))
|
| 198 |
+
one_hot = F.one_hot(target.long(), num_classes=prediction.shape[1]).permute(0, 3, 1, 2).float()
|
| 199 |
+
target = F.adaptive_avg_pool2d(one_hot, (size, size)).argmax(dim=1)
|
| 200 |
+
mask = F.adaptive_avg_pool2d(mask, (size, size))
|
| 201 |
+
value = F.cross_entropy(prediction, target, reduction="none")
|
| 202 |
+
value = (value * mask[:, 0]).sum() / mask[:, 0].sum().clamp_min(1)
|
| 203 |
+
else:
|
| 204 |
+
if spec.get("shift_pixels", 0):
|
| 205 |
+
value = _shift_invariant_l1(prediction, target, mask, int(spec["shift_pixels"]))
|
| 206 |
+
else:
|
| 207 |
+
prediction, target, mask = (_pool_continuous(item, grid_m) for item in (prediction, target, mask))
|
| 208 |
+
value = (torch.abs(prediction - target) * mask).sum() / mask.sum().clamp_min(1)
|
| 209 |
+
components[f"reconstruction_{name}"] = value
|
| 210 |
+
reconstruction = reconstruction + float(spec["weight"]) * value
|
| 211 |
+
flat = teacher["embedding"].permute(0, 2, 3, 1).reshape(-1, teacher["embedding"].shape[1])
|
| 212 |
+
rotated = torch.roll(flat, max(1, flat.shape[0] // 2), dims=0)
|
| 213 |
+
uniformity = (flat * rotated).sum(dim=1).abs().mean()
|
| 214 |
+
consistency = 1.0 - (teacher["embedding"] * student["embedding"]).sum(dim=1).mean()
|
| 215 |
+
pooled = F.normalize(teacher["embedding"].mean(dim=(2, 3)), dim=1)
|
| 216 |
+
normalized_text = F.normalize(text_target, dim=1)
|
| 217 |
+
logits = pooled @ normalized_text.transpose(0, 1)
|
| 218 |
+
labels = torch.arange(len(logits), device=logits.device)
|
| 219 |
+
text_alignment = 0.5 * (F.cross_entropy(logits, labels) + F.cross_entropy(logits.transpose(0, 1), labels))
|
| 220 |
+
total = (weights["reconstruction"] * reconstruction + weights["uniformity"] * uniformity
|
| 221 |
+
+ weights["consistency"] * consistency + weights["text"] * text_alignment)
|
| 222 |
+
components.update(reconstruction=reconstruction, uniformity=uniformity,
|
| 223 |
+
consistency=consistency, text_alignment=text_alignment, total=total)
|
| 224 |
+
return total, components
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def quantize_embeddings(embedding, power=2, scale=127.5):
|
| 228 |
+
transformed = embedding.abs().pow(1.0 / power) * embedding.sign()
|
| 229 |
+
return torch.round(transformed * scale).clamp(-127, 127).to(torch.int8)
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def dequantize_embeddings(quantized, power=2, scale=127.5):
|
| 233 |
+
values = quantized.float() / scale
|
| 234 |
+
return values.abs().pow(power) * values.sign()
|
scripts/fake_data.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Generate sparse synthetic observations following the paper-confirmed data contract."""
|
| 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 |
+
DAY_MS = 86_400_000
|
| 12 |
+
TARGET_ONLY_SLOTS = 3
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def load_config():
|
| 16 |
+
return yaml.safe_load((ROOT / "conf/config.yaml").read_text())
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def standardized_signal(rng, count, frames, channels, size, source_phase):
|
| 20 |
+
y, x = np.mgrid[-1:1:complex(size), -1:1:complex(size)].astype(np.float32)
|
| 21 |
+
base = np.sin(3 * np.pi * x) * np.cos(2 * np.pi * y) + 0.4 * x + 0.2 * y
|
| 22 |
+
values = np.empty((count, frames, channels, size, size), np.float32)
|
| 23 |
+
for sample in range(count):
|
| 24 |
+
phase = rng.uniform(0, 2 * np.pi) + source_phase
|
| 25 |
+
for step in range(frames):
|
| 26 |
+
seasonal = np.sin(2 * np.pi * step / max(frames, 1) + phase)
|
| 27 |
+
for channel in range(channels):
|
| 28 |
+
values[sample, step, channel] = base + 0.08 * channel + 0.25 * seasonal
|
| 29 |
+
values[sample] += rng.normal(0, 0.04, values[sample].shape)
|
| 30 |
+
values -= values.mean(axis=(0, 1, 3, 4), keepdims=True)
|
| 31 |
+
values /= values.std(axis=(0, 1, 3, 4), keepdims=True).clip(1e-6)
|
| 32 |
+
return np.clip(values, -6, 6).astype(np.float32)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def quality_mask(rng, count, frames, channels, size, sparse=False):
|
| 36 |
+
probability = 0.08 if not sparse else 0.92
|
| 37 |
+
mask = rng.random((count, frames, 1, size, size)) > probability
|
| 38 |
+
if not sparse:
|
| 39 |
+
mask[:, :, :, :2] = False
|
| 40 |
+
return np.repeat(mask, channels, axis=2).astype(np.float32)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def make_split(path, count, config, seed):
|
| 44 |
+
rng = np.random.default_rng(seed)
|
| 45 |
+
data, size = config["data"], config["data"]["image_size"]
|
| 46 |
+
start = np.datetime64("2020-01-01", "ms").astype(np.int64)
|
| 47 |
+
payload = {"format_version": np.asarray(data["format_version"]), "data_source": np.asarray("synthetic")}
|
| 48 |
+
|
| 49 |
+
for source_index, (name, spec) in enumerate(data["input_sources"].items()):
|
| 50 |
+
frames, channels = spec["timesteps"], spec["channels"]
|
| 51 |
+
payload[name] = standardized_signal(rng, count, frames, channels, size, source_index)
|
| 52 |
+
days = np.linspace(0, 364, frames, dtype=np.int64)
|
| 53 |
+
payload[f"timestamps_{name}"] = np.tile(start + days * DAY_MS + source_index, (count, 1))
|
| 54 |
+
available = np.ones((count, frames), np.bool_)
|
| 55 |
+
available[:, -max(1, frames // 10):] = False
|
| 56 |
+
payload[f"frame_available_{name}"] = available
|
| 57 |
+
channel_available = np.ones((count, frames, channels), np.bool_)
|
| 58 |
+
if name == "sentinel1":
|
| 59 |
+
channel_available[..., :4] = False
|
| 60 |
+
for sample in range(count):
|
| 61 |
+
for step in range(frames):
|
| 62 |
+
pair = (0, 1) if (sample + step) % 2 == 0 else (2, 3)
|
| 63 |
+
channel_available[sample, step, list(pair)] = True
|
| 64 |
+
payload[f"channel_available_{name}"] = channel_available
|
| 65 |
+
payload[f"pixel_valid_{name}"] = quality_mask(rng, count, frames, channels, size)
|
| 66 |
+
geometry = np.zeros((count, frames, config["model"]["max_geometry_dim"]), np.float32)
|
| 67 |
+
if name == "sentinel1":
|
| 68 |
+
geometry[..., 0] = rng.uniform(-np.pi, np.pi, (count, frames))
|
| 69 |
+
geometry[..., 1] = rng.uniform(1.6, 1.8, (count, frames))
|
| 70 |
+
payload[f"geometry_{name}"] = geometry
|
| 71 |
+
|
| 72 |
+
for target_index, (name, spec) in enumerate(data["target_sources"].items()):
|
| 73 |
+
if name in data["input_sources"]:
|
| 74 |
+
continue
|
| 75 |
+
channels = 1 if spec["type"] == "categorical" else spec["channels"]
|
| 76 |
+
values = standardized_signal(rng, count, TARGET_ONLY_SLOTS, channels, size, target_index + 3)
|
| 77 |
+
if spec["type"] == "categorical":
|
| 78 |
+
bins = np.linspace(values.min(), values.max(), spec["channels"] + 1)[1:-1]
|
| 79 |
+
values = np.digitize(values[:, :, 0], bins).astype(np.int64)
|
| 80 |
+
payload[f"target_sequence_{name}"] = values
|
| 81 |
+
payload[f"target_timestamps_{name}"] = np.tile(
|
| 82 |
+
start + np.asarray([30, 182, 330], np.int64) * DAY_MS + target_index, (count, 1)
|
| 83 |
+
)
|
| 84 |
+
mask_channels = 1 if spec["type"] == "categorical" else spec["channels"]
|
| 85 |
+
payload[f"target_pixel_valid_{name}"] = quality_mask(
|
| 86 |
+
rng, count, TARGET_ONLY_SLOTS, mask_channels, size, sparse=name == "gedi"
|
| 87 |
+
)
|
| 88 |
+
geometry = np.zeros((count, TARGET_ONLY_SLOTS, config["model"]["max_geometry_dim"]), np.float32)
|
| 89 |
+
if name == "palsar2":
|
| 90 |
+
geometry[..., 0] = rng.integers(0, 2, (count, TARGET_ONLY_SLOTS))
|
| 91 |
+
geometry[..., 1] = rng.integers(0, 2, (count, TARGET_ONLY_SLOTS))
|
| 92 |
+
payload[f"target_geometry_{name}"] = geometry
|
| 93 |
+
payload[f"target_frame_available_{name}"] = np.ones((count, TARGET_ONLY_SLOTS), np.bool_)
|
| 94 |
+
|
| 95 |
+
payload["support_period"] = np.tile(np.array([start, start + 365 * DAY_MS], np.int64), (count, 1))
|
| 96 |
+
payload["valid_period"] = np.tile(np.array([start + 60 * DAY_MS, start + 300 * DAY_MS], np.int64), (count, 1))
|
| 97 |
+
text = rng.normal(size=(count, config["model"]["embedding_dim"])).astype(np.float32)
|
| 98 |
+
payload["text_target"] = text / np.linalg.norm(text, axis=1, keepdims=True)
|
| 99 |
+
np.savez_compressed(path, **payload)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def main():
|
| 103 |
+
parser = argparse.ArgumentParser()
|
| 104 |
+
parser.add_argument("--force", action="store_true")
|
| 105 |
+
args = parser.parse_args()
|
| 106 |
+
config = load_config()
|
| 107 |
+
data_dir = ROOT / config["data"]["root"]
|
| 108 |
+
data_dir.mkdir(parents=True, exist_ok=True)
|
| 109 |
+
for offset, (name, count) in enumerate((("train.npz", config["data"]["train_samples"]),
|
| 110 |
+
("test.npz", config["data"]["test_samples"]))):
|
| 111 |
+
target = data_dir / name
|
| 112 |
+
if args.force or not target.exists():
|
| 113 |
+
make_split(target, count, config, config["seed"] + offset)
|
| 114 |
+
print(f"generated={target.relative_to(ROOT)} samples={count} format={config['data']['format_version']}")
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
if __name__ == "__main__":
|
| 118 |
+
main()
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Generate float and paper-style signed-int8 annual embedding fields."""
|
| 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 |
+
sys.path.insert(0, str(ROOT / "scripts"))
|
| 14 |
+
from model.alphaearthfoundations import AlphaEarthFoundations, dequantize_embeddings, quantize_embeddings
|
| 15 |
+
from train import AEFDataset, device_from_config, unpack
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def main():
|
| 19 |
+
config = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
|
| 20 |
+
torch.manual_seed(config["seed"])
|
| 21 |
+
device = device_from_config(config)
|
| 22 |
+
checkpoint = torch.load(ROOT / config["paths"]["checkpoint"], map_location=device, weights_only=True)
|
| 23 |
+
if checkpoint["format_version"] != config["data"]["format_version"]:
|
| 24 |
+
raise ValueError("Checkpoint and data formats do not match")
|
| 25 |
+
model = AlphaEarthFoundations(checkpoint["input_sources"], checkpoint["target_sources"], checkpoint["model_config"]).to(device)
|
| 26 |
+
model.load_state_dict(checkpoint["model"])
|
| 27 |
+
model.eval()
|
| 28 |
+
dataset = AEFDataset(ROOT / config["data"]["root"] / "test.npz", config)
|
| 29 |
+
embeddings, quantized, restored = [], [], []
|
| 30 |
+
reconstruction = {name: [] for name in config["data"]["target_sources"]}
|
| 31 |
+
selected_targets = {name: [] for name in config["data"]["target_sources"]}
|
| 32 |
+
selected_masks = {name: [] for name in config["data"]["target_sources"]}
|
| 33 |
+
with torch.no_grad():
|
| 34 |
+
for index in range(len(dataset)):
|
| 35 |
+
batch = {key: value.unsqueeze(0) for key, value in dataset[index].items()}
|
| 36 |
+
(sources, timestamps, frame_available, targets, masks, target_times,
|
| 37 |
+
target_periods, geometry) = unpack(batch, config, device)
|
| 38 |
+
output = model(sources, timestamps, batch["valid_period"].to(device), frame_available,
|
| 39 |
+
target_times, geometry, target_periods)
|
| 40 |
+
q = quantize_embeddings(output["embedding"])
|
| 41 |
+
embeddings.append(output["embedding"].cpu().numpy())
|
| 42 |
+
quantized.append(q.cpu().numpy())
|
| 43 |
+
restored.append(dequantize_embeddings(q).cpu().numpy())
|
| 44 |
+
for name, values in output["reconstructions"].items():
|
| 45 |
+
reconstruction[name].append(values.cpu().numpy())
|
| 46 |
+
selected_targets[name].append(targets[name].cpu().numpy())
|
| 47 |
+
selected_masks[name].append(masks[name].cpu().numpy())
|
| 48 |
+
output_dir = ROOT / config["paths"]["inference_dir"]
|
| 49 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 50 |
+
payload = {"embedding": np.concatenate(embeddings), "embedding_s8_power2": np.concatenate(quantized),
|
| 51 |
+
"embedding_dequantized": np.concatenate(restored)}
|
| 52 |
+
payload.update({f"reconstruction_{name}": np.concatenate(values) for name, values in reconstruction.items()})
|
| 53 |
+
payload.update({f"target_{name}": np.concatenate(values) for name, values in selected_targets.items()})
|
| 54 |
+
payload.update({f"mask_{name}": np.concatenate(values) for name, values in selected_masks.items()})
|
| 55 |
+
np.savez_compressed(output_dir / "predictions.npz", **payload)
|
| 56 |
+
print(f"predictions={(output_dir / 'predictions.npz').relative_to(ROOT)}")
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
if __name__ == "__main__":
|
| 60 |
+
main()
|
scripts/result.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Evaluate reconstruction, spherical embeddings, quantization and low-shot transfer."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
import matplotlib.pyplot as plt
|
| 7 |
+
import numpy as np
|
| 8 |
+
import yaml
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def balanced_accuracy(target, prediction):
|
| 15 |
+
scores = [(prediction[target == label] == label).mean() for label in np.unique(target)]
|
| 16 |
+
return float(np.mean(scores))
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def transfer_metrics(embedding, class_target, regression_target, seed):
|
| 20 |
+
rng = np.random.default_rng(seed)
|
| 21 |
+
features = embedding.transpose(0, 2, 3, 1).reshape(-1, embedding.shape[1])
|
| 22 |
+
classes = class_target.reshape(-1)
|
| 23 |
+
regression = regression_target.reshape(-1)
|
| 24 |
+
train_indices, test_indices = [], []
|
| 25 |
+
for label in np.unique(classes):
|
| 26 |
+
indices = np.flatnonzero(classes == label)
|
| 27 |
+
rng.shuffle(indices)
|
| 28 |
+
split = min(10, max(1, len(indices) // 3))
|
| 29 |
+
train_indices.extend(indices[:split])
|
| 30 |
+
test_indices.extend(indices[split:])
|
| 31 |
+
train_indices, test_indices = np.asarray(train_indices), np.asarray(test_indices)
|
| 32 |
+
x_train, x_test = features[train_indices], features[test_indices]
|
| 33 |
+
y_train, y_test = classes[train_indices], classes[test_indices]
|
| 34 |
+
distances = ((x_test[:, None] - x_train[None]) ** 2).sum(axis=-1)
|
| 35 |
+
transfer = {}
|
| 36 |
+
for k in (1, 3):
|
| 37 |
+
neighbors = np.argpartition(distances, min(k, len(x_train)) - 1, axis=1)[:, :k]
|
| 38 |
+
votes = y_train[neighbors]
|
| 39 |
+
prediction = np.asarray([np.bincount(row).argmax() for row in votes])
|
| 40 |
+
transfer[f"knn_k{k}_balanced_accuracy"] = balanced_accuracy(y_test, prediction)
|
| 41 |
+
labels = np.unique(classes)
|
| 42 |
+
one_hot = np.stack([np.where(y_train == label, 1.0, -1.0) for label in labels], axis=1)
|
| 43 |
+
design = np.column_stack([x_train, np.ones(len(x_train))])
|
| 44 |
+
coefficients = np.linalg.lstsq(design, one_hot, rcond=None)[0]
|
| 45 |
+
class_prediction = labels[np.argmax(np.column_stack([x_test, np.ones(len(x_test))]) @ coefficients, axis=1)]
|
| 46 |
+
transfer["linear_balanced_accuracy"] = balanced_accuracy(y_test, class_prediction)
|
| 47 |
+
regression_coefficients = np.linalg.lstsq(design, regression[train_indices], rcond=None)[0]
|
| 48 |
+
regression_prediction = np.column_stack([x_test, np.ones(len(x_test))]) @ regression_coefficients
|
| 49 |
+
residual = ((regression[test_indices] - regression_prediction) ** 2).sum()
|
| 50 |
+
total = ((regression[test_indices] - regression[test_indices].mean()) ** 2).sum()
|
| 51 |
+
transfer["linear_regression_r2"] = float(1.0 - residual / max(total, 1e-12))
|
| 52 |
+
transfer["train_pixels"] = int(len(train_indices))
|
| 53 |
+
transfer["test_pixels"] = int(len(test_indices))
|
| 54 |
+
return transfer
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def main():
|
| 58 |
+
config = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
|
| 59 |
+
predictions = np.load(ROOT / config["paths"]["inference_dir"] / "predictions.npz")
|
| 60 |
+
embedding, restored = predictions["embedding"], predictions["embedding_dequantized"]
|
| 61 |
+
metrics = {
|
| 62 |
+
"samples": int(len(embedding)),
|
| 63 |
+
"mean_embedding_norm": float(np.linalg.norm(embedding, axis=1).mean()),
|
| 64 |
+
"s8_power2_quantization_mae": float(np.abs(embedding - restored).mean()),
|
| 65 |
+
"reconstruction_mae": {},
|
| 66 |
+
"low_shot_transfer": transfer_metrics(
|
| 67 |
+
embedding, predictions["target_nlcd"], predictions["target_sentinel2"][:, 0], config["seed"]
|
| 68 |
+
),
|
| 69 |
+
}
|
| 70 |
+
for name, spec in config["data"]["target_sources"].items():
|
| 71 |
+
prediction = predictions[f"reconstruction_{name}"]
|
| 72 |
+
target = predictions[f"target_{name}"]
|
| 73 |
+
mask = predictions[f"mask_{name}"]
|
| 74 |
+
if spec["type"] == "categorical":
|
| 75 |
+
metrics["reconstruction_mae"][name] = float(
|
| 76 |
+
(((prediction.argmax(axis=1) != target) * mask[:, 0]).sum()) / max(mask[:, 0].sum(), 1)
|
| 77 |
+
)
|
| 78 |
+
else:
|
| 79 |
+
metrics["reconstruction_mae"][name] = float((np.abs(prediction - target) * mask).sum() / max(mask.sum(), 1))
|
| 80 |
+
evaluation_dir = ROOT / config["paths"]["evaluation_dir"]
|
| 81 |
+
evaluation_dir.mkdir(parents=True, exist_ok=True)
|
| 82 |
+
(evaluation_dir / "metrics.json").write_text(json.dumps(metrics, indent=2) + "\n")
|
| 83 |
+
rgb = embedding[0, [1, 16, 9]].transpose(1, 2, 0)
|
| 84 |
+
rgb = np.clip((rgb + 0.3) / 0.6, 0, 1)
|
| 85 |
+
figure, axes = plt.subplots(1, 2, figsize=(8, 4))
|
| 86 |
+
axes[0].imshow(rgb)
|
| 87 |
+
axes[0].set_title("AEF axes A01/A16/A09")
|
| 88 |
+
axes[1].imshow(predictions["target_nlcd"][0], cmap="tab20", vmin=0, vmax=15)
|
| 89 |
+
axes[1].set_title("Synthetic NLCD target")
|
| 90 |
+
for axis in axes:
|
| 91 |
+
axis.axis("off")
|
| 92 |
+
figure.tight_layout()
|
| 93 |
+
figure.savefig(evaluation_dir / "comparison.png", dpi=160)
|
| 94 |
+
plt.close(figure)
|
| 95 |
+
print(json.dumps(metrics, indent=2))
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
if __name__ == "__main__":
|
| 99 |
+
main()
|
scripts/train.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Train AEF reconstruction, uniformity, consistency and text objectives."""
|
| 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.alphaearthfoundations import AlphaEarthFoundations, compute_losses
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class AEFDataset(Dataset):
|
| 21 |
+
def __init__(self, path, config):
|
| 22 |
+
self.data = np.load(path)
|
| 23 |
+
self.input_sources = config["data"]["input_sources"]
|
| 24 |
+
self.target_sources = config["data"]["target_sources"]
|
| 25 |
+
format_version = str(self.data["format_version"])
|
| 26 |
+
if format_version != config["data"]["format_version"]:
|
| 27 |
+
raise ValueError(f"Expected format {config['data']['format_version']}, got {format_version}")
|
| 28 |
+
for name, spec in self.input_sources.items():
|
| 29 |
+
expected = (spec["timesteps"], spec["channels"], config["data"]["image_size"], config["data"]["image_size"])
|
| 30 |
+
if self.data[name].shape[1:] != expected:
|
| 31 |
+
raise ValueError(f"{name} has shape {self.data[name].shape[1:]}, expected {expected}")
|
| 32 |
+
|
| 33 |
+
def __len__(self):
|
| 34 |
+
return len(self.data["valid_period"])
|
| 35 |
+
|
| 36 |
+
def __getitem__(self, index):
|
| 37 |
+
item = {name: torch.from_numpy(self.data[name][index]).float() for name in self.input_sources}
|
| 38 |
+
item.update({f"timestamps_{name}": torch.from_numpy(self.data[f"timestamps_{name}"][index]).long()
|
| 39 |
+
for name in self.input_sources})
|
| 40 |
+
for name in self.input_sources:
|
| 41 |
+
item[f"frame_available_{name}"] = torch.from_numpy(self.data[f"frame_available_{name}"][index])
|
| 42 |
+
item[f"channel_available_{name}"] = torch.from_numpy(self.data[f"channel_available_{name}"][index])
|
| 43 |
+
item[f"pixel_valid_{name}"] = torch.from_numpy(self.data[f"pixel_valid_{name}"][index]).float()
|
| 44 |
+
item[f"geometry_{name}"] = torch.from_numpy(self.data[f"geometry_{name}"][index]).float()
|
| 45 |
+
for name, spec in self.target_sources.items():
|
| 46 |
+
if name in self.input_sources:
|
| 47 |
+
continue
|
| 48 |
+
values = torch.from_numpy(self.data[f"target_sequence_{name}"][index])
|
| 49 |
+
item[f"target_sequence_{name}"] = values.long() if spec["type"] == "categorical" else values.float()
|
| 50 |
+
item[f"target_timestamps_{name}"] = torch.from_numpy(self.data[f"target_timestamps_{name}"][index]).long()
|
| 51 |
+
item[f"target_pixel_valid_{name}"] = torch.from_numpy(self.data[f"target_pixel_valid_{name}"][index]).float()
|
| 52 |
+
item[f"target_geometry_{name}"] = torch.from_numpy(self.data[f"target_geometry_{name}"][index]).float()
|
| 53 |
+
item[f"target_frame_available_{name}"] = torch.from_numpy(self.data[f"target_frame_available_{name}"][index])
|
| 54 |
+
item["support_period"] = torch.from_numpy(self.data["support_period"][index]).long()
|
| 55 |
+
item["valid_period"] = torch.from_numpy(self.data["valid_period"][index]).long()
|
| 56 |
+
item["text_target"] = torch.from_numpy(self.data["text_target"][index]).float()
|
| 57 |
+
return item
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def device_from_config(config, local_rank=0):
|
| 61 |
+
requested = config["runtime"]["device"]
|
| 62 |
+
if requested == "auto":
|
| 63 |
+
return torch.device("cuda", local_rank) if torch.cuda.is_available() else torch.device("cpu")
|
| 64 |
+
return torch.device(requested)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _select_indices(available):
|
| 68 |
+
selected = []
|
| 69 |
+
for row in available:
|
| 70 |
+
candidates = torch.nonzero(row, as_tuple=False).flatten()
|
| 71 |
+
choice = torch.randint(len(candidates), (), device=candidates.device)
|
| 72 |
+
selected.append(candidates[choice])
|
| 73 |
+
return torch.stack(selected).to(available.device)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _gather(values, indices):
|
| 77 |
+
return values[torch.arange(len(values), device=values.device), indices]
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def unpack(batch, config, device, remove_input_targets=True):
|
| 81 |
+
input_names, target_specs = config["data"]["input_sources"], config["data"]["target_sources"]
|
| 82 |
+
sources, frame_available = {}, {}
|
| 83 |
+
timestamps = {name: batch[f"timestamps_{name}"].to(device) for name in input_names}
|
| 84 |
+
targets, masks, target_times, target_periods, geometry = {}, {}, {}, {}, {}
|
| 85 |
+
for name in input_names:
|
| 86 |
+
values = batch[name].to(device)
|
| 87 |
+
available = batch[f"frame_available_{name}"].to(device).clone()
|
| 88 |
+
channel_available = batch[f"channel_available_{name}"].to(device)
|
| 89 |
+
values = values * channel_available[:, :, :, None, None]
|
| 90 |
+
indices = _select_indices(available)
|
| 91 |
+
targets[name] = _gather(values, indices)
|
| 92 |
+
masks[name] = _gather(batch[f"pixel_valid_{name}"].to(device), indices) * _gather(channel_available, indices)[:, :, None, None]
|
| 93 |
+
target_times[name] = _gather(timestamps[name], indices)
|
| 94 |
+
target_periods[name] = torch.stack([
|
| 95 |
+
target_times[name] - 5 * 86_400_000, target_times[name] + 5 * 86_400_000
|
| 96 |
+
], dim=1)
|
| 97 |
+
geometry[name] = _gather(batch[f"geometry_{name}"].to(device), indices)
|
| 98 |
+
if remove_input_targets:
|
| 99 |
+
available[torch.arange(len(available), device=device), indices] = False
|
| 100 |
+
sources[name] = values * available[:, :, None, None, None]
|
| 101 |
+
frame_available[name] = available
|
| 102 |
+
for name in target_specs:
|
| 103 |
+
if name in input_names:
|
| 104 |
+
continue
|
| 105 |
+
available = batch[f"target_frame_available_{name}"].to(device)
|
| 106 |
+
indices = _select_indices(available)
|
| 107 |
+
targets[name] = _gather(batch[f"target_sequence_{name}"].to(device), indices)
|
| 108 |
+
masks[name] = _gather(batch[f"target_pixel_valid_{name}"].to(device), indices)
|
| 109 |
+
target_times[name] = _gather(batch[f"target_timestamps_{name}"].to(device), indices)
|
| 110 |
+
target_periods[name] = torch.stack([
|
| 111 |
+
target_times[name] - 5 * 86_400_000, target_times[name] + 5 * 86_400_000
|
| 112 |
+
], dim=1)
|
| 113 |
+
geometry[name] = _gather(batch[f"target_geometry_{name}"].to(device), indices)
|
| 114 |
+
return sources, timestamps, frame_available, targets, masks, target_times, target_periods, geometry
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def perturb_sources(sources, frame_available, timestamps, support_period, config):
|
| 118 |
+
perturbed, perturbed_available = {}, {}
|
| 119 |
+
source_dropout, frame_dropout = config["train"]["source_dropout"], config["train"]["frame_dropout"]
|
| 120 |
+
for name, values in sources.items():
|
| 121 |
+
output, available = values + 0.01 * torch.randn_like(values), frame_available[name].clone()
|
| 122 |
+
source_mask = torch.rand(values.shape[0], 1, 1, 1, 1, device=values.device) < source_dropout[name]
|
| 123 |
+
strategy = torch.randint(3, (1,), device=values.device).item()
|
| 124 |
+
if strategy == 0:
|
| 125 |
+
dropped = torch.rand_like(available.float()) < frame_dropout[name]
|
| 126 |
+
else:
|
| 127 |
+
midpoint = support_period[:, :1] + (support_period[:, 1:] - support_period[:, :1]) // 2
|
| 128 |
+
dropped = timestamps[name] >= midpoint if strategy == 1 else timestamps[name] < midpoint
|
| 129 |
+
available = available & ~dropped & ~source_mask[:, 0, 0, 0]
|
| 130 |
+
perturbed[name] = output * available[:, :, None, None, None]
|
| 131 |
+
perturbed_available[name] = available
|
| 132 |
+
return perturbed, perturbed_available
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def main():
|
| 136 |
+
config = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
|
| 137 |
+
torch.manual_seed(config["seed"])
|
| 138 |
+
distributed = int(os.environ.get("WORLD_SIZE", "1")) > 1
|
| 139 |
+
local_rank = int(os.environ.get("LOCAL_RANK", "0"))
|
| 140 |
+
if distributed:
|
| 141 |
+
torch.distributed.init_process_group("nccl" if torch.cuda.is_available() else "gloo")
|
| 142 |
+
rank = torch.distributed.get_rank() if distributed else 0
|
| 143 |
+
device = device_from_config(config, local_rank)
|
| 144 |
+
if device.type == "cuda":
|
| 145 |
+
torch.cuda.set_device(device)
|
| 146 |
+
|
| 147 |
+
dataset = AEFDataset(ROOT / config["data"]["root"] / "train.npz", config)
|
| 148 |
+
sampler = DistributedSampler(dataset, shuffle=True) if distributed else None
|
| 149 |
+
loader = DataLoader(dataset, batch_size=config["train"]["batch_size"], sampler=sampler,
|
| 150 |
+
shuffle=sampler is None, num_workers=config["train"]["num_workers"])
|
| 151 |
+
model = AlphaEarthFoundations(config["data"]["input_sources"], config["data"]["target_sources"], config["model"]).to(device)
|
| 152 |
+
if distributed:
|
| 153 |
+
model = DistributedDataParallel(model, device_ids=[local_rank] if device.type == "cuda" else None)
|
| 154 |
+
optimizer = torch.optim.Adam(model.parameters(), lr=config["train"]["learning_rate"],
|
| 155 |
+
weight_decay=config["train"]["weight_decay"])
|
| 156 |
+
weights = {name: config["train"][f"{name}_weight"] for name in ("reconstruction", "uniformity", "consistency", "text")}
|
| 157 |
+
history = []
|
| 158 |
+
for epoch in range(config["train"]["epochs"]):
|
| 159 |
+
if sampler:
|
| 160 |
+
sampler.set_epoch(epoch)
|
| 161 |
+
model.train()
|
| 162 |
+
totals = {}
|
| 163 |
+
for batch in loader:
|
| 164 |
+
sources, timestamps, frame_available, targets, masks, target_times, target_periods, geometry = unpack(batch, config, device)
|
| 165 |
+
valid_period = batch["valid_period"].to(device)
|
| 166 |
+
teacher = model(sources, timestamps, valid_period, frame_available, target_times, geometry, target_periods)
|
| 167 |
+
perturbed, perturbed_available = perturb_sources(
|
| 168 |
+
sources, frame_available, timestamps, batch["support_period"].to(device), config
|
| 169 |
+
)
|
| 170 |
+
student = model(perturbed, timestamps, valid_period, perturbed_available)
|
| 171 |
+
loss, components = compute_losses(teacher, student, targets, masks, batch["text_target"].to(device),
|
| 172 |
+
config["data"]["target_sources"], weights)
|
| 173 |
+
optimizer.zero_grad(set_to_none=True)
|
| 174 |
+
loss.backward()
|
| 175 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
| 176 |
+
optimizer.step()
|
| 177 |
+
for name, value in components.items():
|
| 178 |
+
totals[name] = totals.get(name, 0.0) + float(value.detach())
|
| 179 |
+
metrics = {name: value / len(loader) for name, value in totals.items()}
|
| 180 |
+
history.append({"epoch": epoch + 1, **metrics})
|
| 181 |
+
if rank == 0:
|
| 182 |
+
print(f"epoch={epoch + 1} total_loss={metrics['total']:.6f} reconstruction={metrics['reconstruction']:.6f}")
|
| 183 |
+
|
| 184 |
+
if rank == 0:
|
| 185 |
+
checkpoint = ROOT / config["paths"]["checkpoint"]
|
| 186 |
+
metrics_path = ROOT / config["paths"]["training_metrics"]
|
| 187 |
+
checkpoint.parent.mkdir(parents=True, exist_ok=True)
|
| 188 |
+
metrics_path.parent.mkdir(parents=True, exist_ok=True)
|
| 189 |
+
state = model.module.state_dict() if distributed else model.state_dict()
|
| 190 |
+
torch.save({"model": state, "model_config": config["model"], "input_sources": config["data"]["input_sources"],
|
| 191 |
+
"target_sources": config["data"]["target_sources"], "format_version": config["data"]["format_version"]}, checkpoint)
|
| 192 |
+
metrics_path.write_text(json.dumps({"history": history}, indent=2) + "\n")
|
| 193 |
+
print(f"checkpoint={checkpoint.relative_to(ROOT)}")
|
| 194 |
+
if distributed:
|
| 195 |
+
torch.distributed.destroy_process_group()
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
if __name__ == "__main__":
|
| 199 |
+
main()
|
weight/.gitkeep
ADDED
|
File without changes
|