mirror of
https://github.com/SikongJueluo/Mini-Nav.git
synced 2026-07-12 20:15:31 +08:00
feat(retrieval-benchmark): add support for external pre-prepared CAM retrieval datasets with recall@k metric
- Add just recipes for preparing CIFAR10/100 hash artifacts and running benchmarks - Add CAM_RETRIEVAL_DATASET env var support in Makefile - Add load_retrieval_dataset_npz() to load pre-prepared retrieval datasets - Add label_hits counter and recall@k metric for retrieval evaluation - Rename macro_recall to retrieval_recall to clarify semantics
This commit is contained in:
16
.justfile
16
.justfile
@@ -81,3 +81,19 @@ cam-test-retrieval-no-noise:
|
|||||||
# Run CAM retrieval benchmark with read noise enabled
|
# Run CAM retrieval benchmark with read noise enabled
|
||||||
cam-test-retrieval-read-noise:
|
cam-test-retrieval-read-noise:
|
||||||
just remote "make -C hw/sim clean && make -C hw/sim test-benchmark-retrieval TOPK_K=5 WRITE_NOISE_EN=0 READ_NOISE_EN=1 READ_NOISE_RATE_NUM=1 READ_NOISE_RATE_DEN=100 READ_NOISE_BITS=8"
|
just remote "make -C hw/sim clean && make -C hw/sim test-benchmark-retrieval TOPK_K=5 WRITE_NOISE_EN=0 READ_NOISE_EN=1 READ_NOISE_RATE_NUM=1 READ_NOISE_RATE_DEN=100 READ_NOISE_BITS=8"
|
||||||
|
|
||||||
|
# Prepare CIFAR10 hash artifact for CAM retrieval smoke benchmark
|
||||||
|
cam-prepare-retrieval-cifar10 ROWS="512" QUERIES="128":
|
||||||
|
just remote "python scripts/prepare_cam_retrieval_dataset.py --dataset cifar10 --num-rows {{ROWS}} --max-queries {{QUERIES}} --compressor-path outputs/hash_compressor.pt"
|
||||||
|
|
||||||
|
# Prepare CIFAR100 hash artifact for CAM retrieval benchmark
|
||||||
|
cam-prepare-retrieval-cifar100 ROWS="512" QUERIES="128":
|
||||||
|
just remote "python scripts/prepare_cam_retrieval_dataset.py --dataset cifar100 --num-rows {{ROWS}} --max-queries {{QUERIES}} --compressor-path outputs/hash_compressor.pt"
|
||||||
|
|
||||||
|
# Run CAM retrieval benchmark on a prepared artifact without hardware noise
|
||||||
|
cam-test-retrieval-artifact DATASET_PATH NUM_ROWS="4096":
|
||||||
|
just remote "make -C hw/sim clean && make -C hw/sim test-benchmark-retrieval TOPK_K=5 NUM_ROWS={{NUM_ROWS}} WRITE_NOISE_EN=0 READ_NOISE_EN=0 CAM_RETRIEVAL_DATASET={{ DATASET_PATH }}"
|
||||||
|
|
||||||
|
# Run CAM retrieval benchmark on a prepared artifact with read noise enabled
|
||||||
|
cam-test-retrieval-artifact-read-noise DATASET_PATH NUM_ROWS="4096":
|
||||||
|
just remote "make -C hw/sim clean && make -C hw/sim test-benchmark-retrieval TOPK_K=5 NUM_ROWS={{NUM_ROWS}} WRITE_NOISE_EN=0 READ_NOISE_EN=1 READ_NOISE_RATE_NUM=1 READ_NOISE_RATE_DEN=100 READ_NOISE_BITS=8 CAM_RETRIEVAL_DATASET={{ DATASET_PATH }}"
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ COCOTB_TEST_MODULES := benchmarks.retrieval.test_retrieval_benchmark
|
|||||||
VERILOG_SOURCES := $(RTL_CAM_TOP)
|
VERILOG_SOURCES := $(RTL_CAM_TOP)
|
||||||
|
|
||||||
TOPK_K ?= 5
|
TOPK_K ?= 5
|
||||||
|
NUM_ROWS ?= 4096
|
||||||
WRITE_NOISE_EN ?= 0
|
WRITE_NOISE_EN ?= 0
|
||||||
READ_NOISE_EN ?= 0
|
READ_NOISE_EN ?= 0
|
||||||
|
|
||||||
|
CAM_RETRIEVAL_DATASET ?=
|
||||||
|
export CAM_RETRIEVAL_DATASET
|
||||||
|
|
||||||
include $(SIM_ROOT)/mk/cocotb-common.mk
|
include $(SIM_ROOT)/mk/cocotb-common.mk
|
||||||
|
|||||||
@@ -54,14 +54,16 @@ class MetricAccumulator:
|
|||||||
recall_sum: float = 0.0
|
recall_sum: float = 0.0
|
||||||
f1_sum: float = 0.0
|
f1_sum: float = 0.0
|
||||||
exact_matches: int = 0
|
exact_matches: int = 0
|
||||||
|
label_hits: int = 0
|
||||||
count: int = 0
|
count: int = 0
|
||||||
|
|
||||||
def add(self, precision: float, recall: float, f1: float, exact: bool) -> "MetricAccumulator":
|
def add(self, precision: float, recall: float, f1: float, label_hit: bool, exact: bool) -> "MetricAccumulator":
|
||||||
return MetricAccumulator(
|
return MetricAccumulator(
|
||||||
precision_sum=self.precision_sum + precision,
|
precision_sum=self.precision_sum + precision,
|
||||||
recall_sum=self.recall_sum + recall,
|
recall_sum=self.recall_sum + recall,
|
||||||
f1_sum=self.f1_sum + f1,
|
f1_sum=self.f1_sum + f1,
|
||||||
exact_matches=self.exact_matches + int(exact),
|
exact_matches=self.exact_matches + int(exact),
|
||||||
|
label_hits=self.label_hits + int(label_hit),
|
||||||
count=self.count + 1,
|
count=self.count + 1,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -69,15 +71,17 @@ class MetricAccumulator:
|
|||||||
if self.count == 0:
|
if self.count == 0:
|
||||||
return {
|
return {
|
||||||
"macro_precision": 0.0,
|
"macro_precision": 0.0,
|
||||||
"macro_recall": 0.0,
|
"retrieval_recall": 0.0,
|
||||||
"macro_f1": 0.0,
|
"macro_f1": 0.0,
|
||||||
"exact_match_rate": 0.0,
|
"exact_match_rate": 0.0,
|
||||||
|
"recall@k": 0.0,
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
"macro_precision": self.precision_sum / self.count,
|
"macro_precision": self.precision_sum / self.count,
|
||||||
"macro_recall": self.recall_sum / self.count,
|
"retrieval_recall": self.recall_sum / self.count,
|
||||||
"macro_f1": self.f1_sum / self.count,
|
"macro_f1": self.f1_sum / self.count,
|
||||||
"exact_match_rate": self.exact_matches / self.count,
|
"exact_match_rate": self.exact_matches / self.count,
|
||||||
|
"recall@k": self.label_hits / self.count,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -96,6 +100,37 @@ def _flip_exact_bits(rng: np.random.Generator, width: int, n_bits: int) -> int:
|
|||||||
return mask
|
return mask
|
||||||
|
|
||||||
|
|
||||||
|
def words_le_to_int(words: np.ndarray) -> int:
|
||||||
|
value = 0
|
||||||
|
for idx, word in enumerate(words.tolist()):
|
||||||
|
value |= int(word) << (64 * idx)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def load_retrieval_dataset_npz(path: str | os.PathLike[str]) -> RetrievalDataset:
|
||||||
|
dataset_path = Path(path)
|
||||||
|
if not dataset_path.is_absolute():
|
||||||
|
dataset_path = _project_root() / dataset_path
|
||||||
|
if not dataset_path.exists():
|
||||||
|
raise AssertionError(f"CAM_RETRIEVAL_DATASET not found: {dataset_path}")
|
||||||
|
loaded = np.load(dataset_path)
|
||||||
|
rows = [words_le_to_int(words) for words in loaded["rows_words"]]
|
||||||
|
queries = [words_le_to_int(words) for words in loaded["queries_words"]]
|
||||||
|
row_labels = [int(x) for x in loaded["row_labels"].tolist()]
|
||||||
|
query_labels = [int(x) for x in loaded["query_labels"].tolist()]
|
||||||
|
|
||||||
|
return RetrievalDataset(
|
||||||
|
rows=rows,
|
||||||
|
row_labels=row_labels,
|
||||||
|
queries=queries,
|
||||||
|
query_labels=query_labels,
|
||||||
|
num_classes=len(set(row_labels)),
|
||||||
|
positives_per_class=0,
|
||||||
|
queries_per_class=0,
|
||||||
|
seed=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def make_clustered_dataset(
|
def make_clustered_dataset(
|
||||||
*,
|
*,
|
||||||
num_rows: int,
|
num_rows: int,
|
||||||
@@ -195,8 +230,8 @@ def write_outputs(out_dir: Path, result: dict) -> None:
|
|||||||
"run_id", "mode", "num_rows", "hash_bits", "lanes", "topk_k",
|
"run_id", "mode", "num_rows", "hash_bits", "lanes", "topk_k",
|
||||||
"write_noise_en", "read_noise_en", "write_noise_rate_num",
|
"write_noise_en", "read_noise_en", "write_noise_rate_num",
|
||||||
"write_noise_rate_den", "read_noise_rate_num", "read_noise_rate_den",
|
"write_noise_rate_den", "read_noise_rate_num", "read_noise_rate_den",
|
||||||
"num_queries", "k", "macro_precision", "macro_recall", "macro_f1",
|
"num_queries", "k", "macro_precision", "retrieval_recall", "macro_f1",
|
||||||
"exact_match_rate", "status",
|
"recall@k", "exact_match_rate", "status",
|
||||||
]
|
]
|
||||||
with metrics_csv.open("w", newline="", encoding="utf-8") as f:
|
with metrics_csv.open("w", newline="", encoding="utf-8") as f:
|
||||||
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
||||||
@@ -218,8 +253,9 @@ def write_outputs(out_dir: Path, result: dict) -> None:
|
|||||||
"num_queries": result["dataset"]["num_queries"],
|
"num_queries": result["dataset"]["num_queries"],
|
||||||
"k": int(k),
|
"k": int(k),
|
||||||
"macro_precision": metrics["macro_precision"],
|
"macro_precision": metrics["macro_precision"],
|
||||||
"macro_recall": metrics["macro_recall"],
|
"retrieval_recall": metrics["retrieval_recall"],
|
||||||
"macro_f1": metrics["macro_f1"],
|
"macro_f1": metrics["macro_f1"],
|
||||||
|
"recall@k": metrics["recall@k"],
|
||||||
"exact_match_rate": metrics["exact_match_rate"],
|
"exact_match_rate": metrics["exact_match_rate"],
|
||||||
"status": result["status"],
|
"status": result["status"],
|
||||||
}
|
}
|
||||||
@@ -233,13 +269,13 @@ def write_outputs(out_dir: Path, result: dict) -> None:
|
|||||||
f"- status: `{result['status']}`",
|
f"- status: `{result['status']}`",
|
||||||
f"- num_queries: `{result['dataset']['num_queries']}`",
|
f"- num_queries: `{result['dataset']['num_queries']}`",
|
||||||
"",
|
"",
|
||||||
"| k | macro_precision | macro_recall | macro_f1 | exact_match_rate |",
|
"| k | macro_precision | retrieval_recall | macro_f1 | recall@k | exact_match_rate |",
|
||||||
"|---:|---:|---:|---:|---:|",
|
"|---:|---:|---:|---:|---:|---:|",
|
||||||
]
|
]
|
||||||
for k, metrics in result["metrics"].items():
|
for k, metrics in result["metrics"].items():
|
||||||
lines.append(
|
lines.append(
|
||||||
f"| {k} | {metrics['macro_precision']:.6f} | {metrics['macro_recall']:.6f} | "
|
f"| {k} | {metrics['macro_precision']:.6f} | {metrics['retrieval_recall']:.6f} | "
|
||||||
f"{metrics['macro_f1']:.6f} | {metrics['exact_match_rate']:.6f} |"
|
f"{metrics['macro_f1']:.6f} | {metrics['recall@k']:.6f} | {metrics['exact_match_rate']:.6f} |"
|
||||||
)
|
)
|
||||||
lines.extend([
|
lines.extend([
|
||||||
"",
|
"",
|
||||||
@@ -270,7 +306,12 @@ async def cam_retrieval_benchmark(dut):
|
|||||||
if num_rows % lanes != 0:
|
if num_rows % lanes != 0:
|
||||||
raise AssertionError("Retrieval benchmark requires NUM_ROWS divisible by LANES")
|
raise AssertionError("Retrieval benchmark requires NUM_ROWS divisible by LANES")
|
||||||
|
|
||||||
dataset = make_clustered_dataset(num_rows=num_rows, hash_bits=hash_bits)
|
dataset_path = os.environ.get("CAM_RETRIEVAL_DATASET")
|
||||||
|
if not dataset_path:
|
||||||
|
raise AssertionError("CAM_RETRIEVAL_DATASET is required; run scripts/prepare_cam_retrieval_dataset.py first")
|
||||||
|
dataset = load_retrieval_dataset_npz(dataset_path)
|
||||||
|
if len(dataset.rows) != num_rows:
|
||||||
|
raise AssertionError(f"artifact row count {len(dataset.rows)} must equal DUT NUM_ROWS {num_rows}")
|
||||||
await write_rows(dut, dataset.rows)
|
await write_rows(dut, dataset.rows)
|
||||||
|
|
||||||
accumulators = {k: MetricAccumulator() for k in BENCHMARK_KS}
|
accumulators = {k: MetricAccumulator() for k in BENCHMARK_KS}
|
||||||
@@ -296,7 +337,9 @@ async def cam_retrieval_benchmark(dut):
|
|||||||
for k in BENCHMARK_KS:
|
for k in BENCHMARK_KS:
|
||||||
precision, recall, f1 = compute_metrics(dut_topk, dataset.row_labels, query_label, k)
|
precision, recall, f1 = compute_metrics(dut_topk, dataset.row_labels, query_label, k)
|
||||||
exact = dut_topk[:k] == golden_topk[:k]
|
exact = dut_topk[:k] == golden_topk[:k]
|
||||||
accumulators[k] = accumulators[k].add(precision, recall, f1, exact)
|
retrieved_labels = [dataset.row_labels[idx] for idx in dut_topk[:k]]
|
||||||
|
label_hit = query_label in retrieved_labels
|
||||||
|
accumulators[k] = accumulators[k].add(precision, recall, f1, label_hit, exact)
|
||||||
|
|
||||||
run_id = os.environ.get("CAM_RETRIEVAL_RUN_ID") or f"{datetime.now().strftime('%Y-%m-%d-%H%M%S')}-{mode}"
|
run_id = os.environ.get("CAM_RETRIEVAL_RUN_ID") or f"{datetime.now().strftime('%Y-%m-%d-%H%M%S')}-{mode}"
|
||||||
result = {
|
result = {
|
||||||
@@ -331,9 +374,9 @@ async def cam_retrieval_benchmark(dut):
|
|||||||
for k in BENCHMARK_KS:
|
for k in BENCHMARK_KS:
|
||||||
metrics = result["metrics"][str(k)]
|
metrics = result["metrics"][str(k)]
|
||||||
dut._log.info(
|
dut._log.info(
|
||||||
"RETRIEVAL_RESULT mode=%s k=%d precision=%.6f recall=%.6f f1=%.6f exact_match=%.6f output_dir=%s",
|
"RETRIEVAL_RESULT mode=%s k=%d precision=%.6f retrieval_recall=%.6f f1=%.6f recall_at_k=%.6f exact_match=%.6f output_dir=%s",
|
||||||
mode, k, metrics["macro_precision"], metrics["macro_recall"],
|
mode, k, metrics["macro_precision"], metrics["retrieval_recall"],
|
||||||
metrics["macro_f1"], metrics["exact_match_rate"],
|
metrics["macro_f1"], metrics["recall@k"], metrics["exact_match_rate"],
|
||||||
str(out_dir.relative_to(_project_root())),
|
str(out_dir.relative_to(_project_root())),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
234
scripts/prepare_cam_retrieval_dataset.py
Normal file
234
scripts/prepare_cam_retrieval_dataset.py
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
import typer
|
||||||
|
from datasets import load_dataset
|
||||||
|
from rich.progress import track
|
||||||
|
from torch.utils.data import DataLoader
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
MINI_NAV_ROOT = PROJECT_ROOT / "mini-nav"
|
||||||
|
if str(MINI_NAV_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(MINI_NAV_ROOT))
|
||||||
|
|
||||||
|
from compressors.model_loader import get_dino_dim, load_dino_model, load_hash_compressor # noqa: E402
|
||||||
|
from utils import get_device # noqa: E402
|
||||||
|
|
||||||
|
DEFAULT_COMPRESSOR_PATH = Path("outputs/hash_compressor.pt")
|
||||||
|
DEFAULT_OUTPUT_ROOT = Path("outputs/cam_retrieval_benchmark/datasets")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PreparedArtifact:
|
||||||
|
npz_path: Path
|
||||||
|
metadata_path: Path
|
||||||
|
num_rows: int
|
||||||
|
num_queries: int
|
||||||
|
|
||||||
|
|
||||||
|
def dataset_config(dataset: str) -> tuple[str, str]:
|
||||||
|
if dataset == "cifar10":
|
||||||
|
return "uoft-cs/cifar10", "label"
|
||||||
|
if dataset == "cifar100":
|
||||||
|
return "uoft-cs/cifar100", "fine_label"
|
||||||
|
raise ValueError("dataset must be cifar10 or cifar100")
|
||||||
|
|
||||||
|
|
||||||
|
def stratified_indices(labels: list[int], *, total: int, seed: int) -> list[int]:
|
||||||
|
if total <= 0:
|
||||||
|
raise ValueError("total must be greater than 0")
|
||||||
|
if total > len(labels):
|
||||||
|
raise ValueError("total cannot exceed number of labels")
|
||||||
|
|
||||||
|
rng = np.random.default_rng(seed)
|
||||||
|
by_label: dict[int, list[int]] = {}
|
||||||
|
for idx, label in enumerate(labels):
|
||||||
|
by_label.setdefault(int(label), []).append(idx)
|
||||||
|
|
||||||
|
label_ids = sorted(by_label)
|
||||||
|
base = total // len(label_ids)
|
||||||
|
remainder = total % len(label_ids)
|
||||||
|
selected: list[int] = []
|
||||||
|
|
||||||
|
for offset, label in enumerate(label_ids):
|
||||||
|
want = base + (1 if offset < remainder else 0)
|
||||||
|
candidates = list(by_label[label])
|
||||||
|
if want > len(candidates):
|
||||||
|
raise ValueError(f"not enough examples for label {label}: need {want}, have {len(candidates)}")
|
||||||
|
chosen = rng.choice(candidates, size=want, replace=False).tolist()
|
||||||
|
selected.extend(int(i) for i in chosen)
|
||||||
|
|
||||||
|
selected.sort()
|
||||||
|
return selected
|
||||||
|
|
||||||
|
|
||||||
|
def pack_bits_to_words_le(bits: np.ndarray, *, hash_bits: int) -> np.ndarray:
|
||||||
|
if bits.ndim != 2:
|
||||||
|
raise ValueError("bits must have shape [N, hash_bits]")
|
||||||
|
if bits.shape[1] != hash_bits:
|
||||||
|
raise ValueError(f"expected {hash_bits} bits, got {bits.shape[1]}")
|
||||||
|
if hash_bits % 64 != 0:
|
||||||
|
raise ValueError("hash_bits must be divisible by 64")
|
||||||
|
|
||||||
|
words = np.zeros((bits.shape[0], hash_bits // 64), dtype=np.uint64)
|
||||||
|
bits_u8 = bits.astype(np.uint8, copy=False)
|
||||||
|
for word_idx in range(hash_bits // 64):
|
||||||
|
word = np.zeros(bits.shape[0], dtype=np.uint64)
|
||||||
|
for bit in range(64):
|
||||||
|
word |= bits_u8[:, word_idx * 64 + bit].astype(np.uint64) << np.uint64(bit)
|
||||||
|
words[:, word_idx] = word
|
||||||
|
return words
|
||||||
|
|
||||||
|
|
||||||
|
def words_le_to_int(words: np.ndarray) -> int:
|
||||||
|
value = 0
|
||||||
|
for idx, word in enumerate(words.tolist()):
|
||||||
|
value |= int(word) << (64 * idx)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@torch.no_grad()
|
||||||
|
def encode_dataset_bits(
|
||||||
|
dataset,
|
||||||
|
*,
|
||||||
|
img_column: str,
|
||||||
|
batch_size: int,
|
||||||
|
dino_model: str,
|
||||||
|
compressor_path: Path,
|
||||||
|
hash_bits: int,
|
||||||
|
) -> np.ndarray:
|
||||||
|
if not compressor_path.exists():
|
||||||
|
raise FileNotFoundError(f"missing HashCompressor weights: {compressor_path}")
|
||||||
|
|
||||||
|
device = get_device()
|
||||||
|
processor, dino = load_dino_model(dino_model)
|
||||||
|
compressor = load_hash_compressor(
|
||||||
|
input_dim=get_dino_dim(dino_model),
|
||||||
|
hash_bits=hash_bits,
|
||||||
|
compressor_path=str(compressor_path),
|
||||||
|
).to(device)
|
||||||
|
compressor.eval()
|
||||||
|
|
||||||
|
loader = DataLoader(dataset.with_format("torch"), batch_size=batch_size, shuffle=False, num_workers=4)
|
||||||
|
all_bits: list[np.ndarray] = []
|
||||||
|
for batch in track(loader, description="Encoding CIFAR hash bits"):
|
||||||
|
images = batch[img_column]
|
||||||
|
inputs = processor(images=images, return_tensors="pt").to(device)
|
||||||
|
tokens = dino(**inputs).last_hidden_state
|
||||||
|
bits = compressor.encode(tokens).detach().cpu().numpy().astype(np.uint8)
|
||||||
|
all_bits.append(bits)
|
||||||
|
return np.concatenate(all_bits, axis=0)
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_artifact(
|
||||||
|
*,
|
||||||
|
dataset_name: Literal["cifar10", "cifar100"],
|
||||||
|
num_rows: int,
|
||||||
|
max_queries: int,
|
||||||
|
compressor_path: Path,
|
||||||
|
output_root: Path,
|
||||||
|
dino_model: str = "facebook/dinov2-large",
|
||||||
|
hash_bits: int = 512,
|
||||||
|
batch_size: int = 64,
|
||||||
|
seed: int = 20260522,
|
||||||
|
) -> PreparedArtifact:
|
||||||
|
hf_id, label_column = dataset_config(dataset_name)
|
||||||
|
loaded = load_dataset(hf_id)
|
||||||
|
train = loaded["train"]
|
||||||
|
test = loaded["test"] if "test" in loaded else loaded["validation"]
|
||||||
|
|
||||||
|
row_indices = stratified_indices([int(x) for x in train[label_column]], total=num_rows, seed=seed)
|
||||||
|
query_indices = stratified_indices([int(x) for x in test[label_column]], total=max_queries, seed=seed + 1)
|
||||||
|
|
||||||
|
row_subset = train.select(row_indices)
|
||||||
|
query_subset = test.select(query_indices)
|
||||||
|
|
||||||
|
row_bits = encode_dataset_bits(
|
||||||
|
row_subset,
|
||||||
|
img_column="img",
|
||||||
|
batch_size=batch_size,
|
||||||
|
dino_model=dino_model,
|
||||||
|
compressor_path=compressor_path,
|
||||||
|
hash_bits=hash_bits,
|
||||||
|
)
|
||||||
|
query_bits = encode_dataset_bits(
|
||||||
|
query_subset,
|
||||||
|
img_column="img",
|
||||||
|
batch_size=batch_size,
|
||||||
|
dino_model=dino_model,
|
||||||
|
compressor_path=compressor_path,
|
||||||
|
hash_bits=hash_bits,
|
||||||
|
)
|
||||||
|
|
||||||
|
rows_words = pack_bits_to_words_le(row_bits, hash_bits=hash_bits)
|
||||||
|
queries_words = pack_bits_to_words_le(query_bits, hash_bits=hash_bits)
|
||||||
|
row_labels = np.array([int(x) for x in row_subset[label_column]], dtype=np.int64)
|
||||||
|
query_labels = np.array([int(x) for x in query_subset[label_column]], dtype=np.int64)
|
||||||
|
|
||||||
|
output_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
stem = f"{dataset_name}_hash{hash_bits}_rows{num_rows}_queries{max_queries}"
|
||||||
|
npz_path = output_root / f"{stem}.npz"
|
||||||
|
metadata_path = output_root / f"{stem}.json"
|
||||||
|
|
||||||
|
np.savez_compressed(
|
||||||
|
npz_path,
|
||||||
|
rows_words=rows_words,
|
||||||
|
row_labels=row_labels,
|
||||||
|
queries_words=queries_words,
|
||||||
|
query_labels=query_labels,
|
||||||
|
)
|
||||||
|
metadata = {
|
||||||
|
"dataset": dataset_name,
|
||||||
|
"hf_id": hf_id,
|
||||||
|
"label_column": label_column,
|
||||||
|
"compressor_path": str(compressor_path),
|
||||||
|
"dino_model": dino_model,
|
||||||
|
"hash_bits": hash_bits,
|
||||||
|
"bit_source": "HashCompressor.encode(tokens)",
|
||||||
|
"bit_values": "{0,1} int32",
|
||||||
|
"num_rows": int(num_rows),
|
||||||
|
"num_queries": int(max_queries),
|
||||||
|
"rows_source_split": "train",
|
||||||
|
"queries_source_split": "test",
|
||||||
|
"seed": int(seed),
|
||||||
|
}
|
||||||
|
metadata_path.write_text(json.dumps(metadata, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||||
|
return PreparedArtifact(npz_path=npz_path, metadata_path=metadata_path, num_rows=num_rows, num_queries=max_queries)
|
||||||
|
|
||||||
|
|
||||||
|
def main(
|
||||||
|
dataset: Literal["cifar10", "cifar100"] = typer.Option("cifar100"),
|
||||||
|
num_rows: int = typer.Option(512, min=5),
|
||||||
|
max_queries: int = typer.Option(128, min=1),
|
||||||
|
compressor_path: Path = typer.Option(DEFAULT_COMPRESSOR_PATH),
|
||||||
|
output_root: Path = typer.Option(DEFAULT_OUTPUT_ROOT),
|
||||||
|
dino_model: str = typer.Option("facebook/dinov2-large"),
|
||||||
|
hash_bits: int = typer.Option(512),
|
||||||
|
batch_size: int = typer.Option(64, min=1),
|
||||||
|
seed: int = typer.Option(20260522),
|
||||||
|
) -> None:
|
||||||
|
artifact = prepare_artifact(
|
||||||
|
dataset_name=dataset,
|
||||||
|
num_rows=num_rows,
|
||||||
|
max_queries=max_queries,
|
||||||
|
compressor_path=compressor_path,
|
||||||
|
output_root=output_root,
|
||||||
|
dino_model=dino_model,
|
||||||
|
hash_bits=hash_bits,
|
||||||
|
batch_size=batch_size,
|
||||||
|
seed=seed,
|
||||||
|
)
|
||||||
|
print(f"CAM_RETRIEVAL_DATASET={artifact.npz_path}")
|
||||||
|
print(f"CAM_RETRIEVAL_METADATA={artifact.metadata_path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
typer.run(main)
|
||||||
61
tests/test_prepare_cam_retrieval_dataset.py
Normal file
61
tests/test_prepare_cam_retrieval_dataset.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from scripts.prepare_cam_retrieval_dataset import (
|
||||||
|
dataset_config,
|
||||||
|
pack_bits_to_words_le,
|
||||||
|
stratified_indices,
|
||||||
|
words_le_to_int,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_dataset_config_resolves_cifar10_and_cifar100() -> None:
|
||||||
|
assert dataset_config("cifar10") == ("uoft-cs/cifar10", "label")
|
||||||
|
assert dataset_config("cifar100") == ("uoft-cs/cifar100", "fine_label")
|
||||||
|
|
||||||
|
|
||||||
|
def test_dataset_config_rejects_unknown_dataset() -> None:
|
||||||
|
try:
|
||||||
|
dataset_config("mnist")
|
||||||
|
except ValueError as exc:
|
||||||
|
assert "dataset must be cifar10 or cifar100" in str(exc)
|
||||||
|
else:
|
||||||
|
raise AssertionError("expected ValueError")
|
||||||
|
|
||||||
|
|
||||||
|
def test_stratified_indices_balances_labels_and_is_deterministic() -> None:
|
||||||
|
labels = [0, 0, 0, 1, 1, 1, 2, 2, 2]
|
||||||
|
|
||||||
|
first = stratified_indices(labels, total=6, seed=123)
|
||||||
|
second = stratified_indices(labels, total=6, seed=123)
|
||||||
|
|
||||||
|
assert first == second
|
||||||
|
selected_labels = [labels[i] for i in first]
|
||||||
|
assert selected_labels.count(0) == 2
|
||||||
|
assert selected_labels.count(1) == 2
|
||||||
|
assert selected_labels.count(2) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_stratified_indices_fills_remainder() -> None:
|
||||||
|
labels = [0, 0, 0, 1, 1, 1]
|
||||||
|
|
||||||
|
indices = stratified_indices(labels, total=5, seed=7)
|
||||||
|
|
||||||
|
assert len(indices) == 5
|
||||||
|
assert len(set(indices)) == 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_pack_bits_to_words_le_roundtrip() -> None:
|
||||||
|
bits = np.zeros((2, 128), dtype=np.uint8)
|
||||||
|
bits[0, 0] = 1
|
||||||
|
bits[0, 65] = 1
|
||||||
|
bits[1, 63] = 1
|
||||||
|
bits[1, 127] = 1
|
||||||
|
|
||||||
|
words = pack_bits_to_words_le(bits, hash_bits=128)
|
||||||
|
|
||||||
|
assert words.dtype == np.uint64
|
||||||
|
assert words.shape == (2, 2)
|
||||||
|
assert words_le_to_int(words[0]) == (1 << 0) | (1 << 65)
|
||||||
|
assert words_le_to_int(words[1]) == (1 << 63) | (1 << 127)
|
||||||
Reference in New Issue
Block a user