SatFetch / benchmark_cross_modal.py
karansharmaworkspace's picture
Upload 68 files
f343f06 verified
Raw
History Blame Contribute Delete
11.1 kB
"""
Benchmark cross-modal retrieval approaches.
Tests:
1. Single-index with modality filtering
2. Multi-index search
3. Hybrid search
4. Cross-modal alignment with projection heads
"""
import torch
import numpy as np
import time
from pathlib import Path
from typing import Dict, List, Tuple
# Add src to path
import sys
sys.path.insert(0, str(Path(__file__).parent))
from src.features.cross_modal import CrossModalAligner, CrossModalConfig
from src.retrieval.cross_modal_retrieval import CrossModalRetrieval
def load_gallery_data():
"""Load real gallery embeddings and metadata."""
import json
data_dir = Path("data/processed")
# Load embeddings
embeddings = torch.load(data_dir / "gallery_embeddings.pt", weights_only=True)
# Load metadata
with open(data_dir / "gallery_metadata.json") as f:
metadata = json.load(f)
return embeddings.numpy().astype(np.float32), metadata
def split_by_modality(embeddings: np.ndarray, metadata: List[dict]) -> Dict[str, np.ndarray]:
"""Split embeddings by modality."""
modalities = {}
for i, entry in enumerate(metadata):
mod = entry["modality"]
if mod not in modalities:
modalities[mod] = []
modalities[mod].append(embeddings[i])
return {mod: np.array(embs) for mod, embs in modalities.items()}
def compute_recall_at_k(retrieved_indices: List[int], ground_truth_idx: int, k: int) -> float:
"""Compute Recall@K."""
if ground_truth_idx in retrieved_indices[:k]:
return 1.0
return 0.0
def benchmark_single_index(
embeddings: np.ndarray,
metadata: List[dict],
n_queries: int = 50,
k: int = 5
) -> Dict:
"""Benchmark single-index approach."""
print("\n=== Single-Index Benchmark ===")
retrieval = CrossModalRetrieval(embed_dim=embeddings.shape[1])
retrieval.build_single_index(embeddings, [m["modality"] for m in metadata], metadata)
# Generate queries (use gallery images as queries)
query_indices = np.random.choice(len(embeddings), n_queries, replace=False)
results = {
"same_modal": [],
"cross_modal": [],
}
for idx in query_indices:
query = embeddings[idx:idx+1]
query_mod = metadata[idx]["modality"]
query_class = metadata[idx]["class"]
# Same-modal search
same_result = retrieval.search(query, query_mod, target_modality=query_mod, k=k)
same_recall = compute_recall_at_k(
[metadata[i]["class"] for i in same_result.indices],
query_class,
k
)
results["same_modal"].append(same_recall)
# Cross-modal search (find different modality with same class)
cross_targets = [m for m in ["optical", "sar", "multispectral"] if m != query_mod]
for target_mod in cross_targets:
cross_result = retrieval.search(query, query_mod, target_mod, k=k)
cross_recall = compute_recall_at_k(
[metadata[i]["class"] for i in cross_result.indices],
query_class,
k
)
results["cross_modal"].append(cross_recall)
return {
"same_modal_recall@k": np.mean(results["same_modal"]),
"cross_modal_recall@k": np.mean(results["cross_modal"]),
}
def benchmark_multi_index(
embeddings_by_mod: Dict[str, np.ndarray],
metadata: List[dict],
n_queries: int = 50,
k: int = 5
) -> Dict:
"""Benchmark multi-index approach."""
print("\n=== Multi-Index Benchmark ===")
# Build metadata by modality
metadata_by_mod = {}
for entry in metadata:
mod = entry["modality"]
if mod not in metadata_by_mod:
metadata_by_mod[mod] = []
metadata_by_mod[mod].append(entry)
retrieval = CrossModalRetrieval(embed_dim=768)
retrieval.build_multi_index(embeddings_by_mod, metadata_by_mod)
results = {
"same_modal": [],
"cross_modal": [],
}
# Generate queries
all_embeddings = np.concatenate(list(embeddings_by_mod.values()))
query_indices = np.random.choice(len(all_embeddings), n_queries, replace=False)
for idx in query_indices:
query = all_embeddings[idx:idx+1]
# Determine query modality
offset = 0
query_mod = None
for mod, embs in embeddings_by_mod.items():
if idx < offset + len(embs):
query_mod = mod
break
offset += len(embs)
if query_mod is None:
continue
# Same-modal search
same_result = retrieval.search(query, query_mod, target_modality=query_mod, k=k)
same_recall = 1.0 if any(True for _ in same_result.indices) else 0.0
results["same_modal"].append(same_recall)
# Cross-modal search
cross_targets = [m for m in embeddings_by_mod.keys() if m != query_mod]
cross_result = retrieval.search(query, query_mod, k=k)
cross_recall = 1.0 if any(True for _ in cross_result.indices) else 0.0
results["cross_modal"].append(cross_recall)
return {
"same_modal_recall@k": np.mean(results["same_modal"]),
"cross_modal_recall@k": np.mean(results["cross_modal"]),
}
def benchmark_hybrid(
embeddings_by_mod: Dict[str, np.ndarray],
metadata: List[dict],
n_queries: int = 50,
k: int = 5
) -> Dict:
"""Benchmark hybrid search approach."""
print("\n=== Hybrid Search Benchmark ===")
metadata_by_mod = {}
for entry in metadata:
mod = entry["modality"]
if mod not in metadata_by_mod:
metadata_by_mod[mod] = []
metadata_by_mod[mod].append(entry)
retrieval = CrossModalRetrieval(embed_dim=768)
retrieval.build_multi_index(embeddings_by_mod, metadata_by_mod)
results = []
all_embeddings = np.concatenate(list(embeddings_by_mod.values()))
query_indices = np.random.choice(len(all_embeddings), n_queries, replace=False)
for idx in query_indices:
query = all_embeddings[idx:idx+1]
offset = 0
query_mod = None
for mod, embs in embeddings_by_mod.items():
if idx < offset + len(embs):
query_mod = mod
break
offset += len(embs)
if query_mod is None:
continue
result = retrieval.search_hybrid(query, query_mod, k=k)
results.append(1.0 if len(result.indices) > 0 else 0.0)
return {
"hybrid_recall@k": np.mean(results),
}
def benchmark_cross_modal_alignment(
embeddings: np.ndarray,
metadata: List[dict],
n_queries: int = 50,
k: int = 5
) -> Dict:
"""Benchmark cross-modal alignment with projection heads."""
print("\n=== Cross-Modal Alignment Benchmark ===")
config = CrossModalConfig(
embed_dim=embeddings.shape[1],
projection_dim=256,
use_wavelength_encoding=True,
use_domain_adaptation=True,
)
aligner = CrossModalAligner(config)
# Project all embeddings
projected = {}
for mod in ["optical", "sar", "multispectral"]:
mask = [m["modality"] == mod for m in metadata]
mod_embeddings = embeddings[mask]
projected[mod] = aligner.project(
torch.tensor(mod_embeddings), mod
).detach().numpy()
results = {
"same_modal": [],
"cross_modal": [],
}
query_indices = np.random.choice(len(embeddings), n_queries, replace=False)
for idx in query_indices:
query = embeddings[idx:idx+1]
query_mod = metadata[idx]["modality"]
query_class = metadata[idx]["class"]
# Project query
query_proj = aligner.project(
torch.tensor(query), query_mod
).detach().numpy()
# Same-modal search
same_proj = projected[query_mod]
similarities = query_proj @ same_proj.T
topk_idx = np.argsort(similarities[0])[::-1][:k]
same_recall = compute_recall_at_k(
[metadata[i]["class"] for i in topk_idx],
query_class,
k
)
results["same_modal"].append(same_recall)
# Cross-modal search
for target_mod in ["optical", "sar", "multispectral"]:
if target_mod == query_mod:
continue
target_proj = projected[target_mod]
similarities = query_proj @ target_proj.T
topk_idx = np.argsort(similarities[0])[::-1][:k]
cross_recall = compute_recall_at_k(
[metadata[i]["class"] for i in topk_idx],
query_class,
k
)
results["cross_modal"].append(cross_recall)
return {
"same_modal_recall@k": np.mean(results["same_modal"]),
"cross_modal_recall@k": np.mean(results["cross_modal"]),
}
def main():
"""Run all benchmarks."""
print("=" * 60)
print("Cross-Modal Retrieval Benchmark")
print("=" * 60)
# Load data
print("\nLoading gallery data...")
embeddings, metadata = load_gallery_data()
print(f"Loaded {len(metadata)} embeddings of dimension {embeddings.shape[1]}")
# Split by modality
embeddings_by_mod = split_by_modality(embeddings, metadata)
print(f"Modalities: {list(embeddings_by_mod.keys())}")
for mod, embs in embeddings_by_mod.items():
print(f" {mod}: {len(embs)} images")
# Run benchmarks
n_queries = min(50, len(metadata))
k = 5
results = {}
# 1. Single-index
t0 = time.time()
results["single"] = benchmark_single_index(embeddings, metadata, n_queries, k)
results["single"]["time"] = time.time() - t0
# 2. Multi-index
t0 = time.time()
results["multi"] = benchmark_multi_index(embeddings_by_mod, metadata, n_queries, k)
results["multi"]["time"] = time.time() - t0
# 3. Hybrid
t0 = time.time()
results["hybrid"] = benchmark_hybrid(embeddings_by_mod, metadata, n_queries, k)
results["hybrid"]["time"] = time.time() - t0
# 4. Cross-modal alignment
t0 = time.time()
results["alignment"] = benchmark_cross_modal_alignment(embeddings, metadata, n_queries, k)
results["alignment"]["time"] = time.time() - t0
# Print results
print("\n" + "=" * 60)
print("Results Summary")
print("=" * 60)
print(f"\n{'Method':<20} {'Same-Modal R@5':<18} {'Cross-Modal R@5':<18} {'Time (s)':<10}")
print("-" * 66)
for method, res in results.items():
same = res.get("same_modal_recall@k", 0)
cross = res.get("cross_modal_recall@k", 0) or res.get("hybrid_recall@k", 0)
t = res.get("time", 0)
print(f"{method:<20} {same:<18.4f} {cross:<18.4f} {t:<10.3f}")
print("\n" + "=" * 60)
print("Recommendation: Use the method with highest cross-modal recall")
print("=" * 60)
if __name__ == "__main__":
main()