Add RemoteCLIP paper reproduction package
Browse files- .gitattributes +5 -27
- README.md +183 -0
- conf/config.yaml +27 -0
- config.json +50 -0
- configuration.json +12 -0
- model/remoteclip.py +72 -0
- scripts/fake_data.py +55 -0
- scripts/inference.py +66 -0
- scripts/result.py +69 -0
- scripts/train.py +132 -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,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
frameworks: PyTorch
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
license: apache-2.0
|
| 6 |
+
tags:
|
| 7 |
+
- OneScience
|
| 8 |
+
- Earth Science
|
| 9 |
+
- Remote Sensing
|
| 10 |
+
- Vision-Language Model
|
| 11 |
+
- Image-Text Retrieval
|
| 12 |
+
- RemoteCLIP
|
| 13 |
+
- arxiv:2306.11029
|
| 14 |
+
tasks: []
|
| 15 |
+
datasets:
|
| 16 |
+
- RSITMD
|
| 17 |
+
- RSICD
|
| 18 |
+
- UCM-Captions
|
| 19 |
+
---
|
| 20 |
+
<p align="center">
|
| 21 |
+
<strong>
|
| 22 |
+
<span style="font-size: 30px;">RemoteCLIP</span>
|
| 23 |
+
</strong>
|
| 24 |
+
</p>
|
| 25 |
+
|
| 26 |
+
# Model Introduction
|
| 27 |
+
|
| 28 |
+
RemoteCLIP is a vision-language foundation model for remote sensing. It learns semantically rich visual representations by aligning remote sensing images with text descriptions and supports image-text retrieval, zero-shot classification, few-shot classification, linear probing, k-NN classification, and remote sensing object counting.
|
| 29 |
+
|
| 30 |
+
Paper: RemoteCLIP: A Vision Language Foundation Model for Remote Sensing
|
| 31 |
+
|
| 32 |
+
https://arxiv.org/abs/2306.11029
|
| 33 |
+
|
| 34 |
+
# Model Description
|
| 35 |
+
|
| 36 |
+
RemoteCLIP was proposed by researchers from Hohai University, the Hong Kong University of Science and Technology, Nanjing Forestry University, the Chinese Academy of Forestry, and Griffith University. It is continually pretrained on RSITMD, RSICD, UCM-Captions, and remote sensing detection, segmentation, and UAV imagery converted into image-text pairs through B2C and M2B, and is suitable for remote sensing image-text retrieval and open-vocabulary recognition.
|
| 37 |
+
|
| 38 |
+
# Use Cases
|
| 39 |
+
|
| 40 |
+
| Scenario | Description |
|
| 41 |
+
| :---: | :--- |
|
| 42 |
+
| Remote sensing image-text contrastive training | Train RemoteCLIP with paired remote sensing images and text descriptions. |
|
| 43 |
+
| Remote sensing image-text retrieval | Retrieve text from images or remote sensing images from text queries. |
|
| 44 |
+
| Local quick validation | Use synthetic image-text pairs to validate training, inference, evaluation, and visualization. |
|
| 45 |
+
| Hugging Face / OneCode execution | Download the standalone model package, install dependencies, and run the scripts directly. |
|
| 46 |
+
| Multi-GPU training | Launch distributed multi-process training with `torchrun`. |
|
| 47 |
+
|
| 48 |
+
# Usage Guide
|
| 49 |
+
|
| 50 |
+
## 1. OneCode Usage
|
| 51 |
+
|
| 52 |
+
Experience intelligent one-click AI4S programming through the OneCode online environment:
|
| 53 |
+
|
| 54 |
+
[Click to Experience Intelligent One-Click AI4S Programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 55 |
+
|
| 56 |
+
## 2. Manual Installation and Usage
|
| 57 |
+
|
| 58 |
+
**Hardware Requirements**
|
| 59 |
+
|
| 60 |
+
- A GPU or DCU is recommended.
|
| 61 |
+
- CPU can be used for import and small-scale connectivity verification; full training and inference will be slow.
|
| 62 |
+
- DCU users must install DTK in advance. DTK 25.04.2 or above, or the OneScience recommended version matching the cluster, is recommended.
|
| 63 |
+
|
| 64 |
+
### Download the Model Package
|
| 65 |
+
|
| 66 |
+
```bash
|
| 67 |
+
hf download OneScience-Group/RemoteCLIP --local-dir ./RemoteCLIP
|
| 68 |
+
cd RemoteCLIP
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
### Install the Runtime Environment
|
| 72 |
+
|
| 73 |
+
**DCU Environment**
|
| 74 |
+
|
| 75 |
+
```bash
|
| 76 |
+
# Please activate DTK and CONDA first
|
| 77 |
+
conda create -n onescience311 python=3.11 -y
|
| 78 |
+
conda activate onescience311
|
| 79 |
+
# uv installation is supported
|
| 80 |
+
pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
**GPU Environment**
|
| 84 |
+
|
| 85 |
+
```bash
|
| 86 |
+
# Please activate CONDA first
|
| 87 |
+
conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
|
| 88 |
+
conda activate onescience311
|
| 89 |
+
# uv installation is supported
|
| 90 |
+
pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
### Training Data Introduction
|
| 94 |
+
|
| 95 |
+
The paper unifies three groups of heterogeneous remote sensing datasets into paired image-text data. RET-3 contains RSITMD, RSICD, and UCM-Captions; DET-10 contains satellite and UAV object detection datasets; SEG-4 contains the iSAID, LoveDA, Potsdam, and Vaihingen semantic segmentation datasets. Detection annotations are converted into descriptions through Box-to-Caption, while segmentation masks are converted to boxes through Mask-to-Box before caption generation. The resulting pretraining collection is approximately 12 times larger than the original remote sensing image-text datasets combined.
|
| 96 |
+
|
| 97 |
+
The paper datasets remain subject to their respective download and usage licenses. The authors' official code and model resources are available at:
|
| 98 |
+
|
| 99 |
+
```text
|
| 100 |
+
https://github.com/ChenDelong1999/RemoteCLIP
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
The complete paper pretraining dataset is not included in this model repository. Generate compact synthetic data for local validation with:
|
| 104 |
+
|
| 105 |
+
```bash
|
| 106 |
+
python scripts/fake_data.py
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
This command creates 32 training pairs and 8 test pairs in `data/remoteclip.npz`. Synthetic data only validates image and text encoding, bidirectional InfoNCE training, checkpoint loading, retrieval inference, evaluation, and similarity-matrix visualization. The complete synthetic pipeline has been verified successfully, but synthetic training values are not reported as model performance.
|
| 110 |
+
|
| 111 |
+
For real-data training, organize remote sensing images and text descriptions into paired samples following the paper or the official repository and convert them to `data/remoteclip.npz`. Do not run `scripts/fake_data.py` for real-data training. Set `data.protocol` in `conf/config.yaml` to `real_remoteclip` and update the image size, vocabulary size, context length, and training parameters for the converted dataset.
|
| 112 |
+
|
| 113 |
+
### Training
|
| 114 |
+
|
| 115 |
+
Generate synthetic data before local quick validation:
|
| 116 |
+
|
| 117 |
+
```bash
|
| 118 |
+
python scripts/fake_data.py
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
Single GPU or CPU:
|
| 122 |
+
|
| 123 |
+
```bash
|
| 124 |
+
python scripts/train.py
|
| 125 |
+
```
|
| 126 |
+
|
| 127 |
+
Multi-GPU:
|
| 128 |
+
|
| 129 |
+
```bash
|
| 130 |
+
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
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
Training outputs:
|
| 134 |
+
|
| 135 |
+
```text
|
| 136 |
+
result/checkpoints/remoteclip.pt
|
| 137 |
+
result/training/metrics.json
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
### Training Weights
|
| 141 |
+
|
| 142 |
+
Pretrained weights based on paired remote sensing image-text data will be provided under `weight/`. The weight file will be uploaded in a future update. Checkpoints produced from synthetic data validate the pipeline only and do not provide real remote sensing image-text retrieval capability.
|
| 143 |
+
|
| 144 |
+
### Inference
|
| 145 |
+
|
| 146 |
+
```bash
|
| 147 |
+
python scripts/inference.py
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
Inference extracts image features, text features, and the cross-modal similarity matrix from the test pairs.
|
| 151 |
+
|
| 152 |
+
Prediction output:
|
| 153 |
+
|
| 154 |
+
```text
|
| 155 |
+
result/output/retrieval.npz
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
### Evaluation and Visualization
|
| 159 |
+
|
| 160 |
+
```bash
|
| 161 |
+
python scripts/result.py
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
Evaluation outputs:
|
| 165 |
+
|
| 166 |
+
```text
|
| 167 |
+
result/evaluation/metrics.json
|
| 168 |
+
result/evaluation/similarity_matrix.png
|
| 169 |
+
```
|
| 170 |
+
|
| 171 |
+
The evaluation script computes image-to-text and text-to-image R@1, R@5, and mean recall, and renders the image-text similarity matrix. Synthetic data is used only to confirm that evaluation and visualization outputs can be generated successfully; its numerical values are not reported as model performance.
|
| 172 |
+
|
| 173 |
+
# Official OneScience Resources
|
| 174 |
+
|
| 175 |
+
| Platform | OneScience Main Repository | Skills Repository |
|
| 176 |
+
| --- | --- | --- |
|
| 177 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 178 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 179 |
+
|
| 180 |
+
# Citation and License
|
| 181 |
+
|
| 182 |
+
- This repository is a reproduction of the original RemoteCLIP paper.
|
| 183 |
+
- The reproduction code is released under Apache License 2.0. The original datasets and official model weights remain subject to their respective source licenses.
|
conf/config.yaml
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
seed: 42
|
| 2 |
+
data:
|
| 3 |
+
path: data/remoteclip.npz
|
| 4 |
+
protocol: synthetic_remoteclip
|
| 5 |
+
train_samples: 32
|
| 6 |
+
test_samples: 8
|
| 7 |
+
image_size: 32
|
| 8 |
+
vocabulary_size: 64
|
| 9 |
+
context_length: 8
|
| 10 |
+
num_classes: 4
|
| 11 |
+
model:
|
| 12 |
+
embed_dim: 32
|
| 13 |
+
vision_width: 32
|
| 14 |
+
text_width: 32
|
| 15 |
+
text_layers: 2
|
| 16 |
+
text_heads: 4
|
| 17 |
+
train:
|
| 18 |
+
epochs: 5
|
| 19 |
+
batch_size: 8
|
| 20 |
+
learning_rate: 0.001
|
| 21 |
+
weight_decay: 0.0001
|
| 22 |
+
num_workers: 0
|
| 23 |
+
paths:
|
| 24 |
+
checkpoint: result/checkpoints/remoteclip.pt
|
| 25 |
+
training_metrics: result/training/metrics.json
|
| 26 |
+
inference_dir: result/output
|
| 27 |
+
evaluation_dir: result/evaluation
|
config.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_name": "RemoteCLIP",
|
| 3 |
+
"model_type": "remoteclip",
|
| 4 |
+
"architectures": [
|
| 5 |
+
"RemoteCLIP"
|
| 6 |
+
],
|
| 7 |
+
"framework": "PyTorch",
|
| 8 |
+
"domain": "earth-science",
|
| 9 |
+
"task": "remote-sensing-image-text-retrieval",
|
| 10 |
+
"implementation": {
|
| 11 |
+
"entry_point": "model/remoteclip.py",
|
| 12 |
+
"scope": "compact image-text contrastive RemoteCLIP reproduction"
|
| 13 |
+
},
|
| 14 |
+
"architecture": {
|
| 15 |
+
"family": "dual-encoder vision-language model",
|
| 16 |
+
"input_format": "image-text NPZ pairs",
|
| 17 |
+
"image_channels": 3,
|
| 18 |
+
"image_size": 32,
|
| 19 |
+
"vocabulary_size": 64,
|
| 20 |
+
"context_length": 8,
|
| 21 |
+
"embed_dim": 32,
|
| 22 |
+
"vision_width": 32,
|
| 23 |
+
"text_width": 32,
|
| 24 |
+
"text_layers": 2,
|
| 25 |
+
"text_heads": 4,
|
| 26 |
+
"training_objective": "bidirectional InfoNCE"
|
| 27 |
+
},
|
| 28 |
+
"data": {
|
| 29 |
+
"datasets": [
|
| 30 |
+
"RSITMD",
|
| 31 |
+
"RSICD",
|
| 32 |
+
"UCM-Captions"
|
| 33 |
+
],
|
| 34 |
+
"official_repo": "ChenDelong1999/RemoteCLIP",
|
| 35 |
+
"protocol": "synthetic_remoteclip",
|
| 36 |
+
"synthetic_train_samples": 32,
|
| 37 |
+
"synthetic_test_samples": 8
|
| 38 |
+
},
|
| 39 |
+
"metrics": [
|
| 40 |
+
"image_to_text_r1",
|
| 41 |
+
"image_to_text_r5",
|
| 42 |
+
"text_to_image_r1",
|
| 43 |
+
"text_to_image_r5",
|
| 44 |
+
"mean_recall"
|
| 45 |
+
],
|
| 46 |
+
"configuration_sources": [
|
| 47 |
+
"conf/config.yaml",
|
| 48 |
+
"model/remoteclip.py"
|
| 49 |
+
]
|
| 50 |
+
}
|
configuration.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"framework": "PyTorch",
|
| 3 |
+
"task": "remote_sensing_image_text_retrieval",
|
| 4 |
+
"model": "RemoteCLIP",
|
| 5 |
+
"input_format": "image_text_npz",
|
| 6 |
+
"protocol": "synthetic_remoteclip",
|
| 7 |
+
"default_config": "conf/config.yaml",
|
| 8 |
+
"train": "scripts/train.py",
|
| 9 |
+
"inference": "scripts/inference.py",
|
| 10 |
+
"evaluation": "scripts/result.py",
|
| 11 |
+
"visualization": "scripts/result.py"
|
| 12 |
+
}
|
model/remoteclip.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Compact RemoteCLIP image-text contrastive model."""
|
| 2 |
+
|
| 3 |
+
import math
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
from torch import nn
|
| 7 |
+
from torch.nn import functional as F
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class RemoteCLIP(nn.Module):
|
| 11 |
+
def __init__(
|
| 12 |
+
self,
|
| 13 |
+
vocabulary_size=64,
|
| 14 |
+
context_length=8,
|
| 15 |
+
embed_dim=32,
|
| 16 |
+
vision_width=32,
|
| 17 |
+
text_width=32,
|
| 18 |
+
text_layers=2,
|
| 19 |
+
text_heads=4,
|
| 20 |
+
):
|
| 21 |
+
super().__init__()
|
| 22 |
+
self.context_length = context_length
|
| 23 |
+
self.image_encoder = nn.Sequential(
|
| 24 |
+
nn.Conv2d(3, vision_width, 5, stride=2, padding=2),
|
| 25 |
+
nn.GELU(),
|
| 26 |
+
nn.Conv2d(vision_width, vision_width * 2, 3, stride=2, padding=1),
|
| 27 |
+
nn.GELU(),
|
| 28 |
+
nn.AdaptiveAvgPool2d(1),
|
| 29 |
+
nn.Flatten(),
|
| 30 |
+
nn.Linear(vision_width * 2, embed_dim),
|
| 31 |
+
)
|
| 32 |
+
self.token_embedding = nn.Embedding(vocabulary_size, text_width, padding_idx=0)
|
| 33 |
+
self.position_embedding = nn.Parameter(torch.zeros(1, context_length, text_width))
|
| 34 |
+
layer = nn.TransformerEncoderLayer(
|
| 35 |
+
text_width,
|
| 36 |
+
text_heads,
|
| 37 |
+
text_width * 4,
|
| 38 |
+
dropout=0.0,
|
| 39 |
+
activation="gelu",
|
| 40 |
+
batch_first=True,
|
| 41 |
+
norm_first=True,
|
| 42 |
+
)
|
| 43 |
+
self.text_encoder = nn.TransformerEncoder(layer, text_layers)
|
| 44 |
+
self.text_projection = nn.Linear(text_width, embed_dim)
|
| 45 |
+
self.logit_scale = nn.Parameter(torch.tensor(math.log(1 / 0.07)))
|
| 46 |
+
nn.init.normal_(self.position_embedding, std=0.01)
|
| 47 |
+
|
| 48 |
+
def encode_image(self, images):
|
| 49 |
+
return F.normalize(self.image_encoder(images), dim=-1)
|
| 50 |
+
|
| 51 |
+
def encode_text(self, tokens):
|
| 52 |
+
features = self.text_encoder(self.token_embedding(tokens) + self.position_embedding)
|
| 53 |
+
mask = tokens.ne(0).unsqueeze(-1)
|
| 54 |
+
pooled = (features * mask).sum(1) / mask.sum(1).clamp_min(1)
|
| 55 |
+
return F.normalize(self.text_projection(pooled), dim=-1)
|
| 56 |
+
|
| 57 |
+
def forward(self, images, tokens):
|
| 58 |
+
image_features = self.encode_image(images)
|
| 59 |
+
text_features = self.encode_text(tokens)
|
| 60 |
+
scale = self.logit_scale.exp().clamp(max=100)
|
| 61 |
+
logits = scale * image_features @ text_features.t()
|
| 62 |
+
targets = torch.arange(logits.shape[0], device=logits.device)
|
| 63 |
+
loss = (
|
| 64 |
+
F.cross_entropy(logits, targets)
|
| 65 |
+
+ F.cross_entropy(logits.t(), targets)
|
| 66 |
+
) / 2
|
| 67 |
+
return {
|
| 68 |
+
"loss": loss,
|
| 69 |
+
"logits_per_image": logits,
|
| 70 |
+
"image_features": image_features,
|
| 71 |
+
"text_features": text_features,
|
| 72 |
+
}
|
scripts/fake_data.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Generate deterministic RemoteCLIP-format image-text pairs."""
|
| 2 |
+
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
import yaml
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def make_split(count, config, seed):
|
| 13 |
+
rng = np.random.default_rng(seed)
|
| 14 |
+
data = config["data"]
|
| 15 |
+
size = data["image_size"]
|
| 16 |
+
classes = data["num_classes"]
|
| 17 |
+
images = np.empty((count, 3, size, size), dtype=np.float32)
|
| 18 |
+
tokens = np.zeros((count, data["context_length"]), dtype=np.int64)
|
| 19 |
+
labels = np.arange(count, dtype=np.int64) % classes
|
| 20 |
+
y, x = np.mgrid[0:size, 0:size].astype(np.float32) / max(size - 1, 1)
|
| 21 |
+
for index, label in enumerate(labels):
|
| 22 |
+
image = np.zeros((3, size, size), dtype=np.float32)
|
| 23 |
+
image[label % 3] = 0.55 + 0.35 * np.sin((label + 1) * np.pi * x)
|
| 24 |
+
image[(label + 1) % 3] += 0.25 * np.cos((label + 1) * np.pi * y)
|
| 25 |
+
images[index] = np.clip(image + rng.normal(0, 0.02, image.shape), 0, 1)
|
| 26 |
+
tokens[index, :4] = [label + 1, 16 + label, 32 + label, 48 + label]
|
| 27 |
+
return images, tokens, labels
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def main():
|
| 31 |
+
with (ROOT / "conf" / "config.yaml").open(encoding="utf-8") as handle:
|
| 32 |
+
config = yaml.safe_load(handle)
|
| 33 |
+
train = make_split(config["data"]["train_samples"], config, config["seed"])
|
| 34 |
+
test = make_split(config["data"]["test_samples"], config, config["seed"] + 1)
|
| 35 |
+
output = ROOT / config["data"]["path"]
|
| 36 |
+
output.parent.mkdir(parents=True, exist_ok=True)
|
| 37 |
+
np.savez_compressed(
|
| 38 |
+
output,
|
| 39 |
+
train_images=train[0],
|
| 40 |
+
train_tokens=train[1],
|
| 41 |
+
train_labels=train[2],
|
| 42 |
+
test_images=test[0],
|
| 43 |
+
test_tokens=test[1],
|
| 44 |
+
test_labels=test[2],
|
| 45 |
+
data_source=np.asarray("synthetic"),
|
| 46 |
+
protocol=np.asarray(config["data"]["protocol"]),
|
| 47 |
+
)
|
| 48 |
+
print(
|
| 49 |
+
f"generated={output.relative_to(ROOT)} train={len(train[0])} test={len(test[0])} "
|
| 50 |
+
f"data_source=synthetic protocol={config['data']['protocol']}"
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
if __name__ == "__main__":
|
| 55 |
+
main()
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Extract RemoteCLIP features and image-text similarity scores."""
|
| 2 |
+
|
| 3 |
+
import importlib.util
|
| 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 |
+
|
| 13 |
+
|
| 14 |
+
def load_model_class():
|
| 15 |
+
spec = importlib.util.spec_from_file_location("remoteclip_model", ROOT / "model" / "remoteclip.py")
|
| 16 |
+
module = importlib.util.module_from_spec(spec)
|
| 17 |
+
spec.loader.exec_module(module)
|
| 18 |
+
return module.RemoteCLIP
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def main():
|
| 22 |
+
with (ROOT / "conf" / "config.yaml").open(encoding="utf-8") as handle:
|
| 23 |
+
config = yaml.safe_load(handle)
|
| 24 |
+
checkpoint_path = ROOT / config["paths"]["checkpoint"]
|
| 25 |
+
data_path = ROOT / config["data"]["path"]
|
| 26 |
+
if not checkpoint_path.exists():
|
| 27 |
+
raise FileNotFoundError("Missing checkpoint. Run `python scripts/train.py` first.")
|
| 28 |
+
if not data_path.exists():
|
| 29 |
+
raise FileNotFoundError("Missing data. Run `python scripts/fake_data.py` first.")
|
| 30 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 31 |
+
checkpoint = torch.load(checkpoint_path, map_location=device, weights_only=False)
|
| 32 |
+
RemoteCLIP = load_model_class()
|
| 33 |
+
model = RemoteCLIP(
|
| 34 |
+
vocabulary_size=config["data"]["vocabulary_size"],
|
| 35 |
+
context_length=config["data"]["context_length"],
|
| 36 |
+
**config["model"],
|
| 37 |
+
).to(device)
|
| 38 |
+
model.load_state_dict(checkpoint["model"])
|
| 39 |
+
model.eval()
|
| 40 |
+
archive = np.load(data_path)
|
| 41 |
+
images = torch.from_numpy(archive["test_images"]).to(device)
|
| 42 |
+
tokens = torch.from_numpy(archive["test_tokens"]).to(device)
|
| 43 |
+
with torch.inference_mode():
|
| 44 |
+
image_features = model.encode_image(images)
|
| 45 |
+
text_features = model.encode_text(tokens)
|
| 46 |
+
similarities = image_features @ text_features.t()
|
| 47 |
+
output_dir = ROOT / config["paths"]["inference_dir"]
|
| 48 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 49 |
+
np.savez_compressed(
|
| 50 |
+
output_dir / "retrieval.npz",
|
| 51 |
+
similarities=similarities.cpu().numpy(),
|
| 52 |
+
image_features=image_features.cpu().numpy(),
|
| 53 |
+
text_features=text_features.cpu().numpy(),
|
| 54 |
+
labels=archive["test_labels"],
|
| 55 |
+
images=archive["test_images"],
|
| 56 |
+
data_source=archive["data_source"],
|
| 57 |
+
protocol=archive["protocol"],
|
| 58 |
+
)
|
| 59 |
+
print(
|
| 60 |
+
f"output={output_dir.relative_to(ROOT)} samples={len(images)} "
|
| 61 |
+
f"data_source={str(archive['data_source'])} protocol={str(archive['protocol'])}"
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
if __name__ == "__main__":
|
| 66 |
+
main()
|
scripts/result.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Evaluate bidirectional retrieval and render a similarity matrix."""
|
| 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 recall(similarities, k, transpose=False):
|
| 15 |
+
scores = similarities.T if transpose else similarities
|
| 16 |
+
topk = np.argsort(-scores, axis=1)[:, :k]
|
| 17 |
+
targets = np.arange(len(scores))[:, None]
|
| 18 |
+
return float((topk == targets).any(axis=1).mean())
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def main():
|
| 22 |
+
with (ROOT / "conf" / "config.yaml").open(encoding="utf-8") as handle:
|
| 23 |
+
config = yaml.safe_load(handle)
|
| 24 |
+
input_path = ROOT / config["paths"]["inference_dir"] / "retrieval.npz"
|
| 25 |
+
if not input_path.exists():
|
| 26 |
+
raise FileNotFoundError("Missing inference output. Run `python scripts/inference.py` first.")
|
| 27 |
+
archive = np.load(input_path)
|
| 28 |
+
similarities = archive["similarities"]
|
| 29 |
+
limit = len(similarities)
|
| 30 |
+
metrics = {
|
| 31 |
+
"image_to_text_r1": recall(similarities, 1),
|
| 32 |
+
"image_to_text_r5": recall(similarities, min(5, limit)),
|
| 33 |
+
"text_to_image_r1": recall(similarities, 1, True),
|
| 34 |
+
"text_to_image_r5": recall(similarities, min(5, limit), True),
|
| 35 |
+
"mean_recall": 0.0,
|
| 36 |
+
"samples": limit,
|
| 37 |
+
"data_source": str(archive["data_source"]),
|
| 38 |
+
"protocol": str(archive["protocol"]),
|
| 39 |
+
}
|
| 40 |
+
metrics["mean_recall"] = float(
|
| 41 |
+
np.mean(
|
| 42 |
+
[
|
| 43 |
+
metrics["image_to_text_r1"],
|
| 44 |
+
metrics["image_to_text_r5"],
|
| 45 |
+
metrics["text_to_image_r1"],
|
| 46 |
+
metrics["text_to_image_r5"],
|
| 47 |
+
]
|
| 48 |
+
)
|
| 49 |
+
)
|
| 50 |
+
output_dir = ROOT / config["paths"]["evaluation_dir"]
|
| 51 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 52 |
+
(output_dir / "metrics.json").write_text(
|
| 53 |
+
json.dumps(metrics, indent=2) + "\n", encoding="utf-8"
|
| 54 |
+
)
|
| 55 |
+
figure, axis = plt.subplots(figsize=(5, 4))
|
| 56 |
+
image = axis.imshow(similarities, cmap="viridis")
|
| 57 |
+
axis.set_xlabel("Text index")
|
| 58 |
+
axis.set_ylabel("Image index")
|
| 59 |
+
axis.set_title("RemoteCLIP image-text similarity")
|
| 60 |
+
figure.colorbar(image, ax=axis)
|
| 61 |
+
figure.tight_layout()
|
| 62 |
+
figure.savefig(output_dir / "similarity_matrix.png", dpi=120)
|
| 63 |
+
plt.close(figure)
|
| 64 |
+
print(json.dumps(metrics))
|
| 65 |
+
print(f"evaluation={output_dir.relative_to(ROOT)}")
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
if __name__ == "__main__":
|
| 69 |
+
main()
|
scripts/train.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Train compact RemoteCLIP with bidirectional InfoNCE."""
|
| 2 |
+
|
| 3 |
+
import importlib.util
|
| 4 |
+
import json
|
| 5 |
+
import os
|
| 6 |
+
import random
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
import numpy as np
|
| 10 |
+
import torch
|
| 11 |
+
import yaml
|
| 12 |
+
from torch import distributed as dist
|
| 13 |
+
from torch.nn.parallel import DistributedDataParallel
|
| 14 |
+
from torch.utils.data import DataLoader, Dataset, DistributedSampler
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def load_model_class():
|
| 21 |
+
spec = importlib.util.spec_from_file_location("remoteclip_model", ROOT / "model" / "remoteclip.py")
|
| 22 |
+
module = importlib.util.module_from_spec(spec)
|
| 23 |
+
spec.loader.exec_module(module)
|
| 24 |
+
return module.RemoteCLIP
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class PairDataset(Dataset):
|
| 28 |
+
def __init__(self, path):
|
| 29 |
+
archive = np.load(path)
|
| 30 |
+
self.images = archive["train_images"]
|
| 31 |
+
self.tokens = archive["train_tokens"]
|
| 32 |
+
self.data_source = str(archive["data_source"]) if "data_source" in archive.files else "unknown"
|
| 33 |
+
self.protocol = str(archive["protocol"]) if "protocol" in archive.files else "unknown"
|
| 34 |
+
|
| 35 |
+
def __len__(self):
|
| 36 |
+
return len(self.images)
|
| 37 |
+
|
| 38 |
+
def __getitem__(self, index):
|
| 39 |
+
return torch.from_numpy(self.images[index]), torch.from_numpy(self.tokens[index])
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def main():
|
| 43 |
+
with (ROOT / "conf" / "config.yaml").open(encoding="utf-8") as handle:
|
| 44 |
+
config = yaml.safe_load(handle)
|
| 45 |
+
data_path = ROOT / config["data"]["path"]
|
| 46 |
+
if not data_path.exists():
|
| 47 |
+
raise FileNotFoundError(
|
| 48 |
+
f"Missing training data: {data_path.relative_to(ROOT)}. "
|
| 49 |
+
"Run `python scripts/fake_data.py` first."
|
| 50 |
+
)
|
| 51 |
+
world_size = int(os.environ.get("WORLD_SIZE", "1"))
|
| 52 |
+
local_rank = int(os.environ.get("LOCAL_RANK", "0"))
|
| 53 |
+
if world_size > 1:
|
| 54 |
+
dist.init_process_group("nccl" if torch.cuda.is_available() else "gloo")
|
| 55 |
+
device = torch.device(f"cuda:{local_rank}" if torch.cuda.is_available() else "cpu")
|
| 56 |
+
if device.type == "cuda":
|
| 57 |
+
torch.cuda.set_device(local_rank)
|
| 58 |
+
seed = config["seed"] + local_rank
|
| 59 |
+
random.seed(seed)
|
| 60 |
+
np.random.seed(seed)
|
| 61 |
+
torch.manual_seed(seed)
|
| 62 |
+
dataset = PairDataset(data_path)
|
| 63 |
+
sampler = DistributedSampler(dataset, shuffle=True) if world_size > 1 else None
|
| 64 |
+
loader = DataLoader(
|
| 65 |
+
dataset,
|
| 66 |
+
batch_size=config["train"]["batch_size"],
|
| 67 |
+
shuffle=sampler is None,
|
| 68 |
+
sampler=sampler,
|
| 69 |
+
num_workers=config["train"]["num_workers"],
|
| 70 |
+
)
|
| 71 |
+
RemoteCLIP = load_model_class()
|
| 72 |
+
model = RemoteCLIP(
|
| 73 |
+
vocabulary_size=config["data"]["vocabulary_size"],
|
| 74 |
+
context_length=config["data"]["context_length"],
|
| 75 |
+
**config["model"],
|
| 76 |
+
).to(device)
|
| 77 |
+
if world_size > 1:
|
| 78 |
+
model = DistributedDataParallel(model, device_ids=[local_rank] if device.type == "cuda" else None)
|
| 79 |
+
optimizer = torch.optim.AdamW(
|
| 80 |
+
model.parameters(),
|
| 81 |
+
lr=config["train"]["learning_rate"],
|
| 82 |
+
weight_decay=config["train"]["weight_decay"],
|
| 83 |
+
)
|
| 84 |
+
history = []
|
| 85 |
+
for epoch in range(config["train"]["epochs"]):
|
| 86 |
+
if sampler is not None:
|
| 87 |
+
sampler.set_epoch(epoch)
|
| 88 |
+
model.train()
|
| 89 |
+
total = 0.0
|
| 90 |
+
for images, tokens in loader:
|
| 91 |
+
output = model(images.to(device), tokens.to(device))
|
| 92 |
+
optimizer.zero_grad(set_to_none=True)
|
| 93 |
+
output["loss"].backward()
|
| 94 |
+
optimizer.step()
|
| 95 |
+
total += output["loss"].item()
|
| 96 |
+
loss = total / len(loader)
|
| 97 |
+
history.append({"epoch": epoch + 1, "contrastive_loss": loss})
|
| 98 |
+
if local_rank == 0:
|
| 99 |
+
print(f"epoch={epoch + 1} contrastive_loss={loss:.6f}")
|
| 100 |
+
if local_rank == 0:
|
| 101 |
+
checkpoint = ROOT / config["paths"]["checkpoint"]
|
| 102 |
+
metrics = ROOT / config["paths"]["training_metrics"]
|
| 103 |
+
checkpoint.parent.mkdir(parents=True, exist_ok=True)
|
| 104 |
+
metrics.parent.mkdir(parents=True, exist_ok=True)
|
| 105 |
+
base_model = model.module if hasattr(model, "module") else model
|
| 106 |
+
torch.save(
|
| 107 |
+
{
|
| 108 |
+
"model": base_model.state_dict(),
|
| 109 |
+
"config": config,
|
| 110 |
+
"data_source": dataset.data_source,
|
| 111 |
+
"protocol": dataset.protocol,
|
| 112 |
+
},
|
| 113 |
+
checkpoint,
|
| 114 |
+
)
|
| 115 |
+
metrics.write_text(
|
| 116 |
+
json.dumps(
|
| 117 |
+
{"history": history, "data_source": dataset.data_source, "protocol": dataset.protocol},
|
| 118 |
+
indent=2,
|
| 119 |
+
)
|
| 120 |
+
+ "\n",
|
| 121 |
+
encoding="utf-8",
|
| 122 |
+
)
|
| 123 |
+
print(
|
| 124 |
+
f"checkpoint={checkpoint.relative_to(ROOT)} data_source={dataset.data_source} "
|
| 125 |
+
f"protocol={dataset.protocol}"
|
| 126 |
+
)
|
| 127 |
+
if world_size > 1:
|
| 128 |
+
dist.destroy_process_group()
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
if __name__ == "__main__":
|
| 132 |
+
main()
|
weight/.gitkeep
ADDED
|
File without changes
|