zhangrenchao commited on
Commit
7002f4e
·
verified ·
1 Parent(s): 987946b

Add RemoteCLIP paper reproduction package

Browse files
.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
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
 
 
 
28
  *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
1
  *.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