mirror of
https://github.com/SikongJueluo/Mini-Nav.git
synced 2026-07-12 20:15:31 +08:00
feat(benchmark): support multi-dataset evaluation with configurable top-k list
- Evaluate multiple datasets in a single run (CIFAR10 + CIFAR100) - Report Recall@K for a list of K values from one underlying search - Save results to disk: summary.md, metrics.csv, per-dataset predictions.csv, confusion_matrix.csv - Richer evaluation output: query_labels, topk_ids, topk_labels for downstream analysis - Add --dataset and --top-k CLI overrides for benchmark command - Update config schema: dataset→datasets, top_k→top_k_list
This commit is contained in:
28
.justfile
28
.justfile
@@ -25,8 +25,12 @@ ssh:
|
|||||||
-L 127.0.0.1:2718:172.30.0.2:2718 \
|
-L 127.0.0.1:2718:172.30.0.2:2718 \
|
||||||
{{ remote_ssh_target }}
|
{{ remote_ssh_target }}
|
||||||
|
|
||||||
docker:
|
docker cmd="":
|
||||||
docker exec -it {{ remote_docker_container }} bash
|
@if [ -z "{{ cmd }}" ]; then \
|
||||||
|
docker exec -it -w {{ env("REMOTE_WORKDIR") }} {{ remote_docker_container }} bash; \
|
||||||
|
else \
|
||||||
|
docker exec -i -w {{ env("REMOTE_WORKDIR") }} {{ remote_docker_container }} bash -lc {{ quote(cmd) }}; \
|
||||||
|
fi
|
||||||
|
|
||||||
marimo +notebook:
|
marimo +notebook:
|
||||||
uv run marimo edit {{ notebook }} --host 0.0.0.0 --port 2718 --no-token
|
uv run marimo edit {{ notebook }} --host 0.0.0.0 --port 2718 --no-token
|
||||||
@@ -84,36 +88,36 @@ cam-test-retrieval-write-noise:
|
|||||||
|
|
||||||
# Prepare CIFAR10 hash artifact for CAM retrieval smoke benchmark
|
# Prepare CIFAR10 hash artifact for CAM retrieval smoke benchmark
|
||||||
cam-prepare-retrieval-cifar10 ROWS="512" QUERIES="128":
|
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"
|
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
|
# Prepare CIFAR100 hash artifact for CAM retrieval benchmark
|
||||||
cam-prepare-retrieval-cifar100 ROWS="512" QUERIES="128":
|
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"
|
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
|
# Run CAM retrieval benchmark on a prepared artifact without hardware noise
|
||||||
cam-test-retrieval-artifact DATASET_PATH NUM_ROWS="4096":
|
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 CAM_RETRIEVAL_DATASET={{ DATASET_PATH }}"
|
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 CAM_RETRIEVAL_DATASET={{ DATASET_PATH }}"
|
||||||
|
|
||||||
# Run CAM retrieval benchmark on a prepared artifact with write noise enabled (Phase 2: read noise removed)
|
# Run CAM retrieval benchmark on a prepared artifact with write noise enabled (Phase 2: read noise removed)
|
||||||
cam-test-retrieval-artifact-write-noise DATASET_PATH NUM_ROWS="4096":
|
cam-test-retrieval-artifact-write-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=1 WRITE_NOISE_RATE_NUM=1 WRITE_NOISE_RATE_DEN=100 CAM_RETRIEVAL_DATASET={{ DATASET_PATH }}"
|
just remote "make -C hw/sim clean && make -C hw/sim test-benchmark-retrieval TOPK_K=5 NUM_ROWS={{ NUM_ROWS }} WRITE_NOISE_EN=1 WRITE_NOISE_RATE_NUM=1 WRITE_NOISE_RATE_DEN=100 CAM_RETRIEVAL_DATASET={{ DATASET_PATH }}"
|
||||||
|
|
||||||
# ── CAM retrieval benchmark noise sweep ────────────────────────────────────────
|
# ── CAM retrieval benchmark noise sweep ────────────────────────────────────────
|
||||||
|
|
||||||
# Run noise sweep on a prepared dataset (0%–100%, step 10%)
|
# Run noise sweep on a prepared dataset (0%–100%, step 10%)
|
||||||
|
|
||||||
# Usage: just cam-benchmark-retrieval-sweep DATASET=outputs/.../cifar10_hash512_rows512_queries128.npz NUM_ROWS=512
|
# Usage: just cam-benchmark-retrieval-sweep DATASET=outputs/.../cifar10_hash512_rows512_queries128.npz NUM_ROWS=512
|
||||||
cam-benchmark-retrieval-sweep DATASET NUM_ROWS="512":
|
cam-benchmark-retrieval-sweep DATASET NUM_ROWS="512":
|
||||||
just remote "python scripts/run_retrieval_noise_sweep.py --dataset {{DATASET}} --num-rows {{NUM_ROWS}} --output docs/cam_retrieval_noise_sweep.md"
|
just remote "python scripts/run_retrieval_noise_sweep.py --dataset {{ DATASET }} --num-rows {{ NUM_ROWS }} --output docs/cam_retrieval_noise_sweep.md"
|
||||||
|
|
||||||
# Prepare CIFAR10 dataset + run full noise sweep (all-in-one)
|
# Prepare CIFAR10 dataset + run full noise sweep (all-in-one)
|
||||||
cam-benchmark-sweep-cifar10 ROWS="512" QUERIES="128":
|
cam-benchmark-sweep-cifar10 ROWS="512" QUERIES="128":
|
||||||
just cam-prepare-retrieval-cifar10 {{ROWS}} {{QUERIES}}
|
just cam-prepare-retrieval-cifar10 {{ ROWS }} {{ QUERIES }}
|
||||||
just remote "python scripts/run_retrieval_noise_sweep.py --dataset outputs/cam_retrieval_benchmark/datasets/cifar10_hash512_rows{{ROWS}}_queries{{QUERIES}}.npz --num-rows {{ROWS}} --output docs/cam_retrieval_noise_sweep_cifar10.md"
|
just remote "python scripts/run_retrieval_noise_sweep.py --dataset outputs/cam_retrieval_benchmark/datasets/cifar10_hash512_rows{{ ROWS }}_queries{{ QUERIES }}.npz --num-rows {{ ROWS }} --output docs/cam_retrieval_noise_sweep_cifar10.md"
|
||||||
|
|
||||||
# Prepare CIFAR100 dataset + run full noise sweep (all-in-one)
|
# Prepare CIFAR100 dataset + run full noise sweep (all-in-one)
|
||||||
cam-benchmark-sweep-cifar100 ROWS="512" QUERIES="128":
|
cam-benchmark-sweep-cifar100 ROWS="512" QUERIES="128":
|
||||||
just cam-prepare-retrieval-cifar100 {{ROWS}} {{QUERIES}}
|
just cam-prepare-retrieval-cifar100 {{ ROWS }} {{ QUERIES }}
|
||||||
just remote "python scripts/run_retrieval_noise_sweep.py --dataset outputs/cam_retrieval_benchmark/datasets/cifar100_hash512_rows{{ROWS}}_queries{{QUERIES}}.npz --num-rows {{ROWS}} --output docs/cam_retrieval_noise_sweep_cifar100.md"
|
just remote "python scripts/run_retrieval_noise_sweep.py --dataset outputs/cam_retrieval_benchmark/datasets/cifar100_hash512_rows{{ ROWS }}_queries{{ QUERIES }}.npz --num-rows {{ ROWS }} --output docs/cam_retrieval_noise_sweep_cifar100.md"
|
||||||
|
|
||||||
# ── Remote ↔ local sync ────────────────────────────────────────────────────────
|
# ── Remote ↔ local sync ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
"""Benchmark runner for executing evaluations."""
|
"""Benchmark runner for executing evaluations."""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, cast
|
from typing import Any, Callable, cast
|
||||||
|
|
||||||
@@ -23,7 +26,7 @@ def _create_task(config: BenchmarkConfig, model_config: ModelConfig | None) -> A
|
|||||||
Returns:
|
Returns:
|
||||||
Benchmark task instance.
|
Benchmark task instance.
|
||||||
"""
|
"""
|
||||||
task_kwargs: dict[str, Any] = {"top_k": config.task.top_k}
|
task_kwargs: dict[str, Any] = {"top_k_list": config.task.top_k_list}
|
||||||
|
|
||||||
if config.task.type == "retrieval" and model_config is not None:
|
if config.task.type == "retrieval" and model_config is not None:
|
||||||
task_kwargs.update(
|
task_kwargs.update(
|
||||||
@@ -68,19 +71,23 @@ def create_dataset(config: DatasetSourceConfig) -> Any:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_table_name(config: BenchmarkConfig, model_name: str) -> str:
|
def _get_table_name(
|
||||||
"""Generate database table name from config and model name.
|
config: BenchmarkConfig,
|
||||||
|
model_name: str,
|
||||||
|
dataset_path: str,
|
||||||
|
) -> str:
|
||||||
|
"""Generate database table name from config, model, and dataset.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: Benchmark configuration.
|
config: Benchmark configuration.
|
||||||
model_name: Model name for table naming.
|
model_name: Model name for table naming.
|
||||||
|
dataset_path: Dataset identifier or path (e.g. "uoft-cs/cifar100").
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Formatted table name.
|
Formatted table name.
|
||||||
"""
|
"""
|
||||||
prefix = config.model_table_prefix
|
prefix = config.model_table_prefix
|
||||||
# Use dataset path as part of table name (sanitize)
|
dataset_name = Path(dataset_path).name.lower().replace("-", "_")
|
||||||
dataset_name = Path(config.dataset.path).name.lower().replace("-", "_")
|
|
||||||
return f"{prefix}_{dataset_name}_{model_name}"
|
return f"{prefix}_{dataset_name}_{model_name}"
|
||||||
|
|
||||||
|
|
||||||
@@ -88,6 +95,7 @@ def _ensure_table(
|
|||||||
config: BenchmarkConfig,
|
config: BenchmarkConfig,
|
||||||
model_name: str,
|
model_name: str,
|
||||||
vector_dim: int,
|
vector_dim: int,
|
||||||
|
dataset_path: str,
|
||||||
) -> lancedb.table.Table:
|
) -> lancedb.table.Table:
|
||||||
"""Ensure the LanceDB table exists with correct schema.
|
"""Ensure the LanceDB table exists with correct schema.
|
||||||
|
|
||||||
@@ -95,6 +103,7 @@ def _ensure_table(
|
|||||||
config: Benchmark configuration.
|
config: Benchmark configuration.
|
||||||
model_name: Model name for table naming.
|
model_name: Model name for table naming.
|
||||||
vector_dim: Feature vector dimension.
|
vector_dim: Feature vector dimension.
|
||||||
|
dataset_path: Dataset identifier or path.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
LanceDB table instance.
|
LanceDB table instance.
|
||||||
@@ -102,7 +111,7 @@ def _ensure_table(
|
|||||||
import pyarrow as pa
|
import pyarrow as pa
|
||||||
from database import db_manager
|
from database import db_manager
|
||||||
|
|
||||||
table_name = _get_table_name(config, model_name)
|
table_name = _get_table_name(config, model_name, dataset_path)
|
||||||
|
|
||||||
# Build expected schema
|
# Build expected schema
|
||||||
schema = pa.schema(
|
schema = pa.schema(
|
||||||
@@ -132,7 +141,11 @@ def _ensure_table(
|
|||||||
|
|
||||||
|
|
||||||
def _print_benchmark_info(
|
def _print_benchmark_info(
|
||||||
config: BenchmarkConfig, vector_dim: int, table_name: str, table_count: int
|
config: BenchmarkConfig,
|
||||||
|
vector_dim: int,
|
||||||
|
table_name: str,
|
||||||
|
table_count: int,
|
||||||
|
dataset_path: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Print benchmark configuration info using Rich table.
|
"""Print benchmark configuration info using Rich table.
|
||||||
|
|
||||||
@@ -141,12 +154,13 @@ def _print_benchmark_info(
|
|||||||
vector_dim: Feature vector dimension.
|
vector_dim: Feature vector dimension.
|
||||||
table_name: Database table name.
|
table_name: Database table name.
|
||||||
table_count: Number of entries in the table.
|
table_count: Number of entries in the table.
|
||||||
|
dataset_path: Current dataset identifier or path.
|
||||||
"""
|
"""
|
||||||
table = Table(title="Benchmark Configuration", show_header=False)
|
table = Table(title="Benchmark Configuration", show_header=False)
|
||||||
table.add_column("Key", style="cyan", no_wrap=True)
|
table.add_column("Key", style="cyan", no_wrap=True)
|
||||||
table.add_column("Value", style="magenta")
|
table.add_column("Value", style="magenta")
|
||||||
|
|
||||||
table.add_row("Dataset", f"{config.dataset.source_type} - {config.dataset.path}")
|
table.add_row("Dataset", dataset_path)
|
||||||
table.add_row("Model Output Dimension", str(vector_dim))
|
table.add_row("Model Output Dimension", str(vector_dim))
|
||||||
table.add_row("Table Name", table_name)
|
table.add_row("Table Name", table_name)
|
||||||
table.add_row("Table Entries", str(table_count))
|
table.add_row("Table Entries", str(table_count))
|
||||||
@@ -158,13 +172,23 @@ def _print_benchmark_results(results: dict[str, Any]) -> None:
|
|||||||
"""Print benchmark results using Rich table.
|
"""Print benchmark results using Rich table.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
results: Final benchmark metrics.
|
results: Final benchmark metrics for a single dataset.
|
||||||
"""
|
"""
|
||||||
table = Table(title="Benchmark Results", show_header=False)
|
table = Table(title="Benchmark Results", show_header=False)
|
||||||
table.add_column("Metric", style="cyan", no_wrap=True)
|
table.add_column("Metric", style="cyan", no_wrap=True)
|
||||||
table.add_column("Value", style="green")
|
table.add_column("Value", style="green")
|
||||||
|
|
||||||
|
# Print recalls first
|
||||||
|
recalls = results.get("recalls", {})
|
||||||
|
for k_str, v in recalls.items():
|
||||||
|
if isinstance(v, float):
|
||||||
|
table.add_row(k_str, f"{v:.4f}")
|
||||||
|
|
||||||
for key, value in results.items():
|
for key, value in results.items():
|
||||||
|
if key == "recalls":
|
||||||
|
continue
|
||||||
|
if isinstance(value, (list, dict)):
|
||||||
|
continue
|
||||||
if isinstance(value, float):
|
if isinstance(value, float):
|
||||||
table.add_row(key, f"{value:.4f}")
|
table.add_row(key, f"{value:.4f}")
|
||||||
continue
|
continue
|
||||||
@@ -173,20 +197,147 @@ def _print_benchmark_results(results: dict[str, Any]) -> None:
|
|||||||
console.print(table)
|
console.print(table)
|
||||||
|
|
||||||
|
|
||||||
|
def _save_benchmark_outputs(
|
||||||
|
all_results: dict[str, dict[str, Any]],
|
||||||
|
output_root: Path,
|
||||||
|
model_name: str,
|
||||||
|
) -> Path:
|
||||||
|
"""Save multi-dataset benchmark results to disk.
|
||||||
|
|
||||||
|
Writes to ``output_root/{run_id}/``:
|
||||||
|
- summary.md — overall summary with per-dataset recall table
|
||||||
|
- metrics.csv — one row per (dataset, K) combination
|
||||||
|
- {dataset_name}/predictions.csv — per-sample predictions
|
||||||
|
- {dataset_name}/confusion_matrix.csv — row-normalized (Top-1)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
all_results: Dict mapping dataset name to evaluate() result dict.
|
||||||
|
output_root: Parent directory (e.g. ``outputs/benchmark``).
|
||||||
|
model_name: Model identifier for the run_id.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to the created run directory.
|
||||||
|
"""
|
||||||
|
run_id = f"{datetime.now().strftime('%Y-%m-%d-%H%M%S')}-{model_name}"
|
||||||
|
out_dir = output_root / run_id
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# --- metrics.csv: one row per (dataset, k) ---
|
||||||
|
csv_keys = ["model", "run_id", "dataset", "k", "recall@k", "total", "num_classes"]
|
||||||
|
all_csv_rows: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
# --- summary.md lines ---
|
||||||
|
md_lines = [
|
||||||
|
"# Benchmark Results",
|
||||||
|
"",
|
||||||
|
f"- **run_id**: `{run_id}`",
|
||||||
|
f"- **model**: `{model_name}`",
|
||||||
|
"",
|
||||||
|
"## Per-Dataset Recall",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
|
||||||
|
for dataset_name, results in all_results.items():
|
||||||
|
recalls = results.get("recalls", {})
|
||||||
|
total = results.get("total", 0)
|
||||||
|
num_classes = results.get("num_classes", 0)
|
||||||
|
|
||||||
|
md_lines.append(f"### {dataset_name}")
|
||||||
|
md_lines.append("")
|
||||||
|
md_lines.append(f"- total queries: `{total}`")
|
||||||
|
md_lines.append(f"- num_classes: `{num_classes}`")
|
||||||
|
md_lines.append("")
|
||||||
|
md_lines.append("| K | Recall@K |")
|
||||||
|
md_lines.append("|---:|---:|")
|
||||||
|
|
||||||
|
for k_str, recall_val in sorted(recalls.items()):
|
||||||
|
k = int(k_str.replace("recall@", ""))
|
||||||
|
md_lines.append(f"| {k} | {recall_val:.4f} |")
|
||||||
|
all_csv_rows.append({
|
||||||
|
"model": model_name,
|
||||||
|
"run_id": run_id,
|
||||||
|
"dataset": dataset_name,
|
||||||
|
"k": k,
|
||||||
|
"recall@k": recall_val,
|
||||||
|
"total": total,
|
||||||
|
"num_classes": num_classes,
|
||||||
|
})
|
||||||
|
|
||||||
|
md_lines.append("")
|
||||||
|
|
||||||
|
# Per-dataset subdirectory
|
||||||
|
dataset_dir = out_dir / dataset_name
|
||||||
|
dataset_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# --- predictions.csv (per-sample) ---
|
||||||
|
query_labels = results.get("query_labels", [])
|
||||||
|
topk_labels = results.get("topk_labels", [])
|
||||||
|
if query_labels and topk_labels:
|
||||||
|
max_k = max(len(preds) for preds in topk_labels) if topk_labels else 1
|
||||||
|
fieldnames = ["idx", "true_label"] + [
|
||||||
|
f"top{i+1}_label" for i in range(max_k)
|
||||||
|
]
|
||||||
|
with (dataset_dir / "predictions.csv").open(
|
||||||
|
"w", newline="", encoding="utf-8"
|
||||||
|
) as f:
|
||||||
|
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
||||||
|
writer.writeheader()
|
||||||
|
for idx, (true_label, preds) in enumerate(
|
||||||
|
zip(query_labels, topk_labels)
|
||||||
|
):
|
||||||
|
row_data: dict[str, Any] = {
|
||||||
|
"idx": idx,
|
||||||
|
"true_label": true_label,
|
||||||
|
}
|
||||||
|
for i in range(max_k):
|
||||||
|
row_data[f"top{i+1}_label"] = (
|
||||||
|
preds[i] if i < len(preds) else ""
|
||||||
|
)
|
||||||
|
writer.writerow(row_data)
|
||||||
|
|
||||||
|
# --- confusion_matrix.csv ---
|
||||||
|
cm = results.get("confusion_matrix")
|
||||||
|
if cm is not None:
|
||||||
|
with (dataset_dir / "confusion_matrix.csv").open(
|
||||||
|
"w", newline="", encoding="utf-8"
|
||||||
|
) as fh:
|
||||||
|
writer = csv.writer(fh)
|
||||||
|
for row in cm:
|
||||||
|
writer.writerow([f"{v:.6f}" for v in row])
|
||||||
|
|
||||||
|
# Write metrics.csv
|
||||||
|
if all_csv_rows:
|
||||||
|
with (out_dir / "metrics.csv").open("w", newline="", encoding="utf-8") as f:
|
||||||
|
writer = csv.DictWriter(f, fieldnames=csv_keys)
|
||||||
|
writer.writeheader()
|
||||||
|
for row in all_csv_rows:
|
||||||
|
writer.writerow(row)
|
||||||
|
|
||||||
|
# Write summary.md
|
||||||
|
(out_dir / "summary.md").write_text("\n".join(md_lines) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
console.print(f"[green]Results saved to:[/green] {out_dir}")
|
||||||
|
return out_dir
|
||||||
|
|
||||||
|
|
||||||
def run_benchmark(
|
def run_benchmark(
|
||||||
model: Any,
|
model: Any,
|
||||||
processor: Any,
|
processor: Any,
|
||||||
config: BenchmarkConfig,
|
config: BenchmarkConfig,
|
||||||
model_config: ModelConfig | None = None,
|
model_config: ModelConfig | None = None,
|
||||||
model_name: str = "model",
|
model_name: str = "model",
|
||||||
|
output_root: Path | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Run benchmark evaluation.
|
"""Run benchmark evaluation across one or more datasets.
|
||||||
|
|
||||||
Workflow:
|
Workflow:
|
||||||
1. Create dataset from configuration
|
1. Load model / processor once
|
||||||
2. Create benchmark task from configuration
|
2. For each dataset in config.datasets:
|
||||||
3. Build evaluation database from training set
|
a. Create dataset, get splits
|
||||||
4. Evaluate on test set
|
b. Build evaluation database
|
||||||
|
c. Evaluate with all K values in top_k_list
|
||||||
|
3. Print per-dataset results
|
||||||
|
4. Save all results to disk (if ``output_root`` provided)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
model: Feature extraction model.
|
model: Feature extraction model.
|
||||||
@@ -194,28 +345,15 @@ def run_benchmark(
|
|||||||
config: Benchmark configuration.
|
config: Benchmark configuration.
|
||||||
model_config: Optional model configuration for task-owned loading.
|
model_config: Optional model configuration for task-owned loading.
|
||||||
model_name: Model name for table naming.
|
model_name: Model name for table naming.
|
||||||
|
output_root: Optional root directory for saving results.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary containing evaluation results.
|
Dict mapping dataset name to evaluation result dict.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If benchmark is not enabled in config.
|
ValueError: If benchmark is not enabled in config.
|
||||||
"""
|
"""
|
||||||
# Create dataset
|
# ── Resolve model / processor once ──────────────────────────
|
||||||
console.print(
|
|
||||||
f"[cyan]Loading dataset:[/cyan] {config.dataset.source_type} - {config.dataset.path}"
|
|
||||||
)
|
|
||||||
dataset = create_dataset(config.dataset)
|
|
||||||
|
|
||||||
# Get train and test splits
|
|
||||||
train_dataset = dataset.get_train_split()
|
|
||||||
test_dataset = dataset.get_test_split()
|
|
||||||
|
|
||||||
if train_dataset is None or test_dataset is None:
|
|
||||||
raise ValueError(
|
|
||||||
f"Dataset {config.dataset.path} does not have train/test splits"
|
|
||||||
)
|
|
||||||
|
|
||||||
task = _create_task(config, model_config)
|
task = _create_task(config, model_config)
|
||||||
|
|
||||||
resolver = getattr(task, "prepare_benchmark", None)
|
resolver = getattr(task, "prepare_benchmark", None)
|
||||||
@@ -224,17 +362,16 @@ def run_benchmark(
|
|||||||
Callable[[Any, Any, str], tuple[Any, Any, str]],
|
Callable[[Any, Any, str], tuple[Any, Any, str]],
|
||||||
resolver,
|
resolver,
|
||||||
)
|
)
|
||||||
model, processor, model_name = prepare_benchmark(
|
model, processor, model_name = prepare_benchmark(model, processor, model_name)
|
||||||
model,
|
|
||||||
processor,
|
|
||||||
model_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
if model is None or processor is None:
|
if model is None or processor is None:
|
||||||
raise ValueError("Benchmark task did not provide a valid model and processor")
|
raise ValueError("Benchmark task did not provide a valid model and processor")
|
||||||
|
|
||||||
# Infer vector dimension from a sample
|
# ── Infer vector dimension from first dataset ─────────────────
|
||||||
sample = train_dataset[0]
|
first_cfg = config.datasets[0]
|
||||||
|
first_dataset = create_dataset(first_cfg)
|
||||||
|
first_train = first_dataset.get_train_split()
|
||||||
|
sample = first_train[0]
|
||||||
sample_image = sample["img"]
|
sample_image = sample["img"]
|
||||||
|
|
||||||
from utils.feature_extractor import infer_vector_dim
|
from utils.feature_extractor import infer_vector_dim
|
||||||
@@ -242,28 +379,77 @@ def run_benchmark(
|
|||||||
vector_dim = infer_vector_dim(processor, model, sample_image)
|
vector_dim = infer_vector_dim(processor, model, sample_image)
|
||||||
console.print(f"[cyan]Model output dimension:[/cyan] {vector_dim}")
|
console.print(f"[cyan]Model output dimension:[/cyan] {vector_dim}")
|
||||||
|
|
||||||
# Ensure table exists with correct schema
|
# ── Evaluate each dataset ────────────────────────────────────
|
||||||
table = _ensure_table(config, model_name, vector_dim)
|
all_results: dict[str, dict[str, Any]] = {}
|
||||||
table_name = _get_table_name(config, model_name)
|
|
||||||
|
|
||||||
# Check if database is already built
|
for dataset_cfg in config.datasets:
|
||||||
table_count = table.count_rows()
|
|
||||||
if table_count > 0:
|
|
||||||
console.print(
|
console.print(
|
||||||
f"[yellow]Table '{table_name}' already has {table_count} entries, skipping database build.[/yellow]"
|
f"\n[bold cyan]Dataset:[/bold cyan] "
|
||||||
|
f"{dataset_cfg.source_type} - {dataset_cfg.path}"
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
console.print(
|
dataset = create_dataset(dataset_cfg)
|
||||||
f"[cyan]Building database[/cyan] with {len(train_dataset)} training samples..."
|
train_dataset = dataset.get_train_split()
|
||||||
)
|
test_dataset = dataset.get_test_split()
|
||||||
task.build_database(model, processor, train_dataset, table, config.batch_size)
|
|
||||||
|
if train_dataset is None or test_dataset is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"Dataset {dataset_cfg.path} does not have train/test splits"
|
||||||
|
)
|
||||||
|
|
||||||
|
dataset_name = Path(dataset_cfg.path).name.lower()
|
||||||
|
table = _ensure_table(config, model_name, vector_dim, dataset_cfg.path)
|
||||||
|
table_name = _get_table_name(config, model_name, dataset_cfg.path)
|
||||||
|
|
||||||
|
# Build database (skip if cached)
|
||||||
table_count = table.count_rows()
|
table_count = table.count_rows()
|
||||||
|
expected_count = len(train_dataset)
|
||||||
|
|
||||||
_print_benchmark_info(config, vector_dim, table_name, table_count)
|
if table_count == expected_count:
|
||||||
|
console.print(
|
||||||
|
f"[yellow]Table '{table_name}' already has {table_count} entries, "
|
||||||
|
f"skipping database build.[/yellow]"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if table_count > 0:
|
||||||
|
console.print(
|
||||||
|
f"[yellow]Table '{table_name}' has {table_count} entries "
|
||||||
|
f"(expected {expected_count}), rebuilding.[/yellow]"
|
||||||
|
)
|
||||||
|
# Rebuild: drop and recreate
|
||||||
|
from database import db_manager
|
||||||
|
db_manager.db.drop_table(table_name)
|
||||||
|
table = _ensure_table(config, model_name, vector_dim, dataset_cfg.path)
|
||||||
|
|
||||||
# Run evaluation (results with Rich table will be printed by the task)
|
console.print(
|
||||||
console.print(f"[cyan]Evaluating[/cyan] on {len(test_dataset)} test samples...")
|
f"[cyan]Building database[/cyan] with {expected_count} training samples..."
|
||||||
results = task.evaluate(model, processor, test_dataset, table, config.batch_size)
|
)
|
||||||
_print_benchmark_results(results)
|
task.build_database(
|
||||||
|
model, processor, train_dataset, table, config.batch_size,
|
||||||
|
label_column=dataset_cfg.label_column,
|
||||||
|
)
|
||||||
|
table_count = table.count_rows()
|
||||||
|
|
||||||
return results
|
_print_benchmark_info(
|
||||||
|
config, vector_dim, table_name, table_count,
|
||||||
|
dataset_path=dataset_cfg.path,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Evaluate
|
||||||
|
console.print(
|
||||||
|
f"[cyan]Evaluating[/cyan] on {len(test_dataset)} test samples..."
|
||||||
|
)
|
||||||
|
results = task.evaluate(
|
||||||
|
model, processor, test_dataset, table, config.batch_size,
|
||||||
|
label_column=dataset_cfg.label_column,
|
||||||
|
)
|
||||||
|
all_results[dataset_name] = results
|
||||||
|
|
||||||
|
# Print per-dataset results
|
||||||
|
_print_benchmark_results(results)
|
||||||
|
|
||||||
|
# ── Save outputs ─────────────────────────────────────────────
|
||||||
|
if output_root is not None:
|
||||||
|
_save_benchmark_outputs(all_results, output_root, model_name)
|
||||||
|
|
||||||
|
return all_results
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from typing import TYPE_CHECKING, Any, cast
|
from typing import TYPE_CHECKING, Any, cast
|
||||||
|
|
||||||
import lancedb
|
import lancedb
|
||||||
|
import numpy as np
|
||||||
import pyarrow as pa
|
import pyarrow as pa
|
||||||
import torch
|
import torch
|
||||||
import torch.nn.functional as F
|
import torch.nn.functional as F
|
||||||
@@ -81,6 +82,7 @@ def _establish_eval_database(
|
|||||||
model: nn.Module,
|
model: nn.Module,
|
||||||
table: lancedb.table.Table,
|
table: lancedb.table.Table,
|
||||||
dataloader: DataLoader[Any],
|
dataloader: DataLoader[Any],
|
||||||
|
label_column: str = "label",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Extract features from training images and store them in a database table.
|
"""Extract features from training images and store them in a database table.
|
||||||
|
|
||||||
@@ -89,6 +91,7 @@ def _establish_eval_database(
|
|||||||
model: Feature extraction model.
|
model: Feature extraction model.
|
||||||
table: LanceDB table to store features.
|
table: LanceDB table to store features.
|
||||||
dataloader: DataLoader for the training dataset.
|
dataloader: DataLoader for the training dataset.
|
||||||
|
label_column: Column name for labels in the batch dict.
|
||||||
"""
|
"""
|
||||||
# Extract all features using the utility function
|
# Extract all features using the utility function
|
||||||
all_features = extract_batch_features(processor, model, dataloader)
|
all_features = extract_batch_features(processor, model, dataloader)
|
||||||
@@ -97,7 +100,7 @@ def _establish_eval_database(
|
|||||||
# Store features to database
|
# Store features to database
|
||||||
global_idx = 0
|
global_idx = 0
|
||||||
for batch in track(dataloader, description="Storing eval database"):
|
for batch in track(dataloader, description="Storing eval database"):
|
||||||
labels = batch[config.benchmark.dataset.label_column]
|
labels = batch[label_column]
|
||||||
labels_list = labels.tolist()
|
labels_list = labels.tolist()
|
||||||
batch_size = len(labels_list)
|
batch_size = len(labels_list)
|
||||||
|
|
||||||
@@ -120,7 +123,8 @@ def _evaluate_recall(
|
|||||||
table: lancedb.table.Table,
|
table: lancedb.table.Table,
|
||||||
dataloader: DataLoader[Any],
|
dataloader: DataLoader[Any],
|
||||||
top_k: int,
|
top_k: int,
|
||||||
) -> tuple[int, int]:
|
label_column: str = "label",
|
||||||
|
) -> dict[str, Any]:
|
||||||
"""Evaluate Recall@K by searching the database for each test image.
|
"""Evaluate Recall@K by searching the database for each test image.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -129,9 +133,15 @@ def _evaluate_recall(
|
|||||||
table: LanceDB table to search against.
|
table: LanceDB table to search against.
|
||||||
dataloader: DataLoader for the test dataset.
|
dataloader: DataLoader for the test dataset.
|
||||||
top_k: Number of top results to retrieve.
|
top_k: Number of top results to retrieve.
|
||||||
|
label_column: Column name for labels in the batch dict.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A tuple of (correct_count, total_count).
|
A dict with keys:
|
||||||
|
- correct: Number of correct predictions
|
||||||
|
- total: Total number of test samples
|
||||||
|
- query_labels: True labels for each query (length N)
|
||||||
|
- topk_ids: IDs of top-K retrieved items (N x K)
|
||||||
|
- topk_labels: Labels of top-K retrieved items (N x K)
|
||||||
"""
|
"""
|
||||||
# Extract all features using the utility function
|
# Extract all features using the utility function
|
||||||
all_features = extract_batch_features(processor, model, dataloader)
|
all_features = extract_batch_features(processor, model, dataloader)
|
||||||
@@ -140,9 +150,12 @@ def _evaluate_recall(
|
|||||||
correct = 0
|
correct = 0
|
||||||
total = 0
|
total = 0
|
||||||
feature_idx = 0
|
feature_idx = 0
|
||||||
|
query_labels: list[int] = []
|
||||||
|
topk_ids: list[list[int]] = []
|
||||||
|
topk_labels: list[list[int]] = []
|
||||||
|
|
||||||
for batch in track(dataloader, description=f"Evaluating Recall@{top_k}"):
|
for batch in track(dataloader, description=f"Evaluating Recall@{top_k}"):
|
||||||
labels = batch[config.benchmark.dataset.label_column]
|
labels = batch[label_column]
|
||||||
labels_list = labels.tolist()
|
labels_list = labels.tolist()
|
||||||
|
|
||||||
for j in range(len(labels_list)):
|
for j in range(len(labels_list)):
|
||||||
@@ -151,19 +164,67 @@ def _evaluate_recall(
|
|||||||
|
|
||||||
results = (
|
results = (
|
||||||
table.search(feature)
|
table.search(feature)
|
||||||
.select(["label", "_distance"])
|
.select(["id", "label", "_distance"])
|
||||||
.limit(top_k)
|
.limit(top_k)
|
||||||
.to_polars()
|
.to_polars()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
retrieved_ids = results["id"].to_list()
|
||||||
retrieved_labels = results["label"].to_list()
|
retrieved_labels = results["label"].to_list()
|
||||||
if true_label in retrieved_labels:
|
if true_label in retrieved_labels:
|
||||||
correct += 1
|
correct += 1
|
||||||
total += 1
|
total += 1
|
||||||
|
|
||||||
|
query_labels.append(true_label)
|
||||||
|
topk_ids.append(retrieved_ids)
|
||||||
|
topk_labels.append(retrieved_labels)
|
||||||
|
|
||||||
feature_idx += len(labels_list)
|
feature_idx += len(labels_list)
|
||||||
|
|
||||||
return correct, total
|
return {
|
||||||
|
"correct": correct,
|
||||||
|
"total": total,
|
||||||
|
"query_labels": query_labels,
|
||||||
|
"topk_ids": topk_ids,
|
||||||
|
"topk_labels": topk_labels,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_confusion_matrix(
|
||||||
|
query_labels: list[int],
|
||||||
|
topk_labels: list[list[int]],
|
||||||
|
num_classes: int,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""Build row-normalized confusion matrix from Top-1 predictions.
|
||||||
|
|
||||||
|
For each query, the true label is compared against the first
|
||||||
|
retrieved label (Top-1). The resulting matrix is row-normalized
|
||||||
|
so that each row sums to 1.0, representing "given true class X,
|
||||||
|
what fraction of queries were predicted as each class Y".
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query_labels: True labels for each query (length N).
|
||||||
|
topk_labels: Top-K retrieved labels for each query (N x K).
|
||||||
|
num_classes: Total number of label classes.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Row-normalized confusion matrix as (num_classes, num_classes)
|
||||||
|
float64 array. Rows with zero queries remain all-zero.
|
||||||
|
"""
|
||||||
|
cm = np.zeros((num_classes, num_classes), dtype=np.int64)
|
||||||
|
for y_true, top_labels in zip(query_labels, topk_labels):
|
||||||
|
y_pred = top_labels[0] # Top-1
|
||||||
|
cm[int(y_true), int(y_pred)] += 1
|
||||||
|
|
||||||
|
# Row normalize: each row sums to 1.0
|
||||||
|
row_sums = cm.sum(axis=1, keepdims=True)
|
||||||
|
cm_norm = np.divide(
|
||||||
|
cm.astype(np.float64),
|
||||||
|
row_sums,
|
||||||
|
out=np.zeros_like(cm, dtype=np.float64),
|
||||||
|
where=row_sums > 0,
|
||||||
|
)
|
||||||
|
return cm_norm
|
||||||
|
|
||||||
|
|
||||||
@RegisterTask("retrieval")
|
@RegisterTask("retrieval")
|
||||||
@@ -173,6 +234,7 @@ class RetrievalTask(BaseBenchmarkTask):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
top_k: int = 10,
|
top_k: int = 10,
|
||||||
|
top_k_list: list[int] | None = None,
|
||||||
dino_model: str = "facebook/dinov2-large",
|
dino_model: str = "facebook/dinov2-large",
|
||||||
compression_dim: int = 512,
|
compression_dim: int = 512,
|
||||||
compressor_path: str | None = None,
|
compressor_path: str | None = None,
|
||||||
@@ -180,13 +242,22 @@ class RetrievalTask(BaseBenchmarkTask):
|
|||||||
"""Initialize retrieval task.
|
"""Initialize retrieval task.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
top_k: Number of top results to retrieve for recall calculation.
|
top_k: Maximum K for retrieval search.
|
||||||
|
Deprecated — prefer ``top_k_list``.
|
||||||
|
top_k_list: List of K values to evaluate (all derived from
|
||||||
|
a single max-K search). When not provided, ``[top_k]``
|
||||||
|
is used as fallback.
|
||||||
dino_model: DINO model name used for feature extraction.
|
dino_model: DINO model name used for feature extraction.
|
||||||
compression_dim: Output dimension of the hash compressor.
|
compression_dim: Output dimension of the hash compressor.
|
||||||
compressor_path: Optional path to trained hash compressor weights.
|
compressor_path: Optional path to trained hash compressor weights.
|
||||||
"""
|
"""
|
||||||
super().__init__(top_k=top_k)
|
if top_k_list is not None:
|
||||||
self.top_k = top_k
|
top_k_list = sorted(set(top_k_list))
|
||||||
|
else:
|
||||||
|
top_k_list = [top_k]
|
||||||
|
super().__init__(top_k_list=top_k_list)
|
||||||
|
self.top_k_list: list[int] = top_k_list
|
||||||
|
self.max_k: int = max(self.top_k_list)
|
||||||
self.dino_model = dino_model
|
self.dino_model = dino_model
|
||||||
self.compression_dim = compression_dim
|
self.compression_dim = compression_dim
|
||||||
self.compressor_path = compressor_path
|
self.compressor_path = compressor_path
|
||||||
@@ -251,6 +322,7 @@ class RetrievalTask(BaseBenchmarkTask):
|
|||||||
train_dataset: Any,
|
train_dataset: Any,
|
||||||
table: lancedb.table.Table,
|
table: lancedb.table.Table,
|
||||||
batch_size: int,
|
batch_size: int,
|
||||||
|
label_column: str = "label",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Build the evaluation database from training data.
|
"""Build the evaluation database from training data.
|
||||||
|
|
||||||
@@ -260,6 +332,7 @@ class RetrievalTask(BaseBenchmarkTask):
|
|||||||
train_dataset: Training dataset.
|
train_dataset: Training dataset.
|
||||||
table: LanceDB table to store features.
|
table: LanceDB table to store features.
|
||||||
batch_size: Batch size for DataLoader.
|
batch_size: Batch size for DataLoader.
|
||||||
|
label_column: Column name for labels in the batch dict.
|
||||||
"""
|
"""
|
||||||
# Get a sample image to infer vector dimension
|
# Get a sample image to infer vector dimension
|
||||||
sample = train_dataset[0]
|
sample = train_dataset[0]
|
||||||
@@ -282,7 +355,7 @@ class RetrievalTask(BaseBenchmarkTask):
|
|||||||
shuffle=False,
|
shuffle=False,
|
||||||
num_workers=4,
|
num_workers=4,
|
||||||
)
|
)
|
||||||
_establish_eval_database(processor, model, table, train_loader)
|
_establish_eval_database(processor, model, table, train_loader, label_column)
|
||||||
|
|
||||||
def evaluate(
|
def evaluate(
|
||||||
self,
|
self,
|
||||||
@@ -291,6 +364,7 @@ class RetrievalTask(BaseBenchmarkTask):
|
|||||||
test_dataset: Any,
|
test_dataset: Any,
|
||||||
table: lancedb.table.Table,
|
table: lancedb.table.Table,
|
||||||
batch_size: int,
|
batch_size: int,
|
||||||
|
label_column: str = "label",
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Evaluate the model on the test dataset.
|
"""Evaluate the model on the test dataset.
|
||||||
|
|
||||||
@@ -300,13 +374,17 @@ class RetrievalTask(BaseBenchmarkTask):
|
|||||||
test_dataset: Test dataset.
|
test_dataset: Test dataset.
|
||||||
table: LanceDB table to search against.
|
table: LanceDB table to search against.
|
||||||
batch_size: Batch size for DataLoader.
|
batch_size: Batch size for DataLoader.
|
||||||
|
label_column: Column name for labels in the batch dict.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary containing evaluation results with keys:
|
Dictionary containing evaluation results with keys:
|
||||||
- accuracy: Recall@K accuracy (0.0 ~ 1.0)
|
- recalls: Dict of ``{"recall@K": value}`` for each K
|
||||||
- correct: Number of correct predictions
|
|
||||||
- total: Total number of test samples
|
- total: Total number of test samples
|
||||||
- top_k: The K value used
|
- top_k_list: List of K values evaluated
|
||||||
|
- num_classes: Number of label classes
|
||||||
|
- confusion_matrix: Row-normalized confusion matrix (Top-1)
|
||||||
|
- query_labels: True labels for each query sample
|
||||||
|
- topk_labels: Top-K retrieved labels per query sample
|
||||||
"""
|
"""
|
||||||
test_loader = DataLoader(
|
test_loader = DataLoader(
|
||||||
test_dataset.with_format("torch"),
|
test_dataset.with_format("torch"),
|
||||||
@@ -314,15 +392,39 @@ class RetrievalTask(BaseBenchmarkTask):
|
|||||||
shuffle=False,
|
shuffle=False,
|
||||||
num_workers=4,
|
num_workers=4,
|
||||||
)
|
)
|
||||||
correct, total = _evaluate_recall(
|
eval_data = _evaluate_recall(
|
||||||
processor, model, table, test_loader, self.top_k
|
processor, model, table, test_loader, self.max_k, label_column
|
||||||
)
|
)
|
||||||
|
|
||||||
accuracy = correct / total if total > 0 else 0.0
|
total = eval_data["total"]
|
||||||
|
query_labels = eval_data["query_labels"]
|
||||||
|
topk_labels = eval_data["topk_labels"]
|
||||||
|
|
||||||
|
# Compute Recall@k for each k in top_k_list
|
||||||
|
recalls: dict[str, float] = {}
|
||||||
|
for k in self.top_k_list:
|
||||||
|
hits = sum(
|
||||||
|
1
|
||||||
|
for true, preds in zip(query_labels, topk_labels)
|
||||||
|
if true in preds[:k]
|
||||||
|
)
|
||||||
|
recalls[f"recall@{k}"] = hits / total if total > 0 else 0.0
|
||||||
|
|
||||||
|
# Infer number of classes from the data
|
||||||
|
all_labels = query_labels + [
|
||||||
|
label for labels in topk_labels for label in labels
|
||||||
|
]
|
||||||
|
num_classes = max(all_labels) + 1 if all_labels else 1
|
||||||
|
|
||||||
|
# Confusion matrix from Top-1 only
|
||||||
|
cm = _build_confusion_matrix(query_labels, topk_labels, num_classes)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"accuracy": accuracy,
|
"recalls": recalls,
|
||||||
"correct": correct,
|
|
||||||
"total": total,
|
"total": total,
|
||||||
"top_k": self.top_k,
|
"top_k_list": self.top_k_list,
|
||||||
|
"num_classes": num_classes,
|
||||||
|
"confusion_matrix": cm.tolist(),
|
||||||
|
"query_labels": query_labels,
|
||||||
|
"topk_labels": topk_labels,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"""Benchmark CLI command."""
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
from commands import app
|
from commands import app
|
||||||
|
|
||||||
@@ -8,19 +10,59 @@ def benchmark(
|
|||||||
model_path: str | None = typer.Option(
|
model_path: str | None = typer.Option(
|
||||||
None, "--model", "-m", help="Path to compressor model weights"
|
None, "--model", "-m", help="Path to compressor model weights"
|
||||||
),
|
),
|
||||||
|
dataset: list[str] | None = typer.Option(
|
||||||
|
None,
|
||||||
|
"--dataset",
|
||||||
|
"-d",
|
||||||
|
help=(
|
||||||
|
"Override datasets (HuggingFace path, repeatable). "
|
||||||
|
"Each value creates a single-entry dataset list."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
top_k: list[int] | None = typer.Option(
|
||||||
|
None,
|
||||||
|
"--top-k",
|
||||||
|
"-k",
|
||||||
|
help="Override top-K values (repeatable). E.g. --top-k 1 --top-k 5 --top-k 10",
|
||||||
|
),
|
||||||
):
|
):
|
||||||
|
"""Run benchmark evaluation across configured datasets.
|
||||||
|
|
||||||
|
Without flags, runs all datasets and K values from config.yaml.
|
||||||
|
Use --dataset to override datasets, --top-k to override K values.
|
||||||
|
"""
|
||||||
from benchmarks import run_benchmark
|
from benchmarks import run_benchmark
|
||||||
from configs import cfg_manager
|
from configs import cfg_manager
|
||||||
|
from configs.models import DatasetSourceConfig
|
||||||
|
|
||||||
config = cfg_manager.get()
|
config = cfg_manager.get()
|
||||||
benchmark_cfg = config.benchmark
|
benchmark_cfg = config.benchmark
|
||||||
|
|
||||||
if model_path:
|
if model_path:
|
||||||
config.model.compressor_path = model_path
|
config.model.compressor_path = model_path
|
||||||
|
|
||||||
_ = run_benchmark(
|
# CLI overrides
|
||||||
|
if dataset:
|
||||||
|
benchmark_cfg.datasets = [
|
||||||
|
DatasetSourceConfig(
|
||||||
|
source_type="huggingface",
|
||||||
|
path=d,
|
||||||
|
)
|
||||||
|
for d in dataset
|
||||||
|
]
|
||||||
|
|
||||||
|
if top_k:
|
||||||
|
benchmark_cfg.task.top_k_list = sorted(set(top_k))
|
||||||
|
|
||||||
|
model_name = "hash_compressor" if config.model.compressor_path else "dinov2"
|
||||||
|
output_root = config.output.directory / "benchmark"
|
||||||
|
|
||||||
|
results = run_benchmark(
|
||||||
model=None,
|
model=None,
|
||||||
processor=None,
|
processor=None,
|
||||||
config=benchmark_cfg,
|
config=benchmark_cfg,
|
||||||
model_config=config.model,
|
model_config=config.model,
|
||||||
model_name="hash_compressor" if config.model.compressor_path else "dinov2",
|
model_name=model_name,
|
||||||
|
output_root=output_root,
|
||||||
)
|
)
|
||||||
|
return results
|
||||||
|
|||||||
@@ -22,14 +22,18 @@ dataset:
|
|||||||
seed: 42
|
seed: 42
|
||||||
|
|
||||||
benchmark:
|
benchmark:
|
||||||
dataset:
|
datasets:
|
||||||
source_type: "huggingface"
|
- source_type: "huggingface"
|
||||||
path: "uoft-cs/cifar100"
|
path: "uoft-cs/cifar100"
|
||||||
img_column: "img"
|
img_column: "img"
|
||||||
label_column: "fine_label"
|
label_column: "fine_label"
|
||||||
|
- source_type: "huggingface"
|
||||||
|
path: "uoft-cs/cifar10"
|
||||||
|
img_column: "img"
|
||||||
|
label_column: "label"
|
||||||
task:
|
task:
|
||||||
name: "recall_at_k"
|
name: "recall_at_k"
|
||||||
type: "retrieval"
|
type: "retrieval"
|
||||||
top_k: 1
|
top_k_list: [1, 5, 10]
|
||||||
batch_size: 64
|
batch_size: 64
|
||||||
model_table_prefix: "benchmark"
|
model_table_prefix: "benchmark"
|
||||||
|
|||||||
@@ -129,7 +129,24 @@ class BenchmarkTaskConfig(BaseModel):
|
|||||||
|
|
||||||
name: str = Field(default="recall_at_k", description="Task name")
|
name: str = Field(default="recall_at_k", description="Task name")
|
||||||
type: str = Field(default="retrieval", description="Task type")
|
type: str = Field(default="retrieval", description="Task type")
|
||||||
top_k: int = Field(default=10, gt=0, description="Top K for recall evaluation")
|
top_k_list: list[int] = Field(
|
||||||
|
default=[1, 5, 10],
|
||||||
|
description="Top-K values to evaluate (all derived from a single max-K search)",
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_k(self) -> int:
|
||||||
|
"""Maximum K for the underlying search; all values in top_k_list <= max_k."""
|
||||||
|
return max(self.top_k_list) if self.top_k_list else 1
|
||||||
|
|
||||||
|
@field_validator("top_k_list", mode="after")
|
||||||
|
@classmethod
|
||||||
|
def validate_top_k_list(cls, v: list[int]) -> list[int]:
|
||||||
|
if not v:
|
||||||
|
raise ValueError("top_k_list must contain at least one value")
|
||||||
|
if any(k <= 0 for k in v):
|
||||||
|
raise ValueError("top_k_list values must be positive")
|
||||||
|
return sorted(set(v))
|
||||||
|
|
||||||
# Multi-object retrieval specific settings
|
# Multi-object retrieval specific settings
|
||||||
gamma: float = Field(
|
gamma: float = Field(
|
||||||
@@ -148,7 +165,10 @@ class BenchmarkConfig(BaseModel):
|
|||||||
|
|
||||||
model_config = ConfigDict(extra="ignore")
|
model_config = ConfigDict(extra="ignore")
|
||||||
|
|
||||||
dataset: DatasetSourceConfig = Field(default_factory=DatasetSourceConfig)
|
datasets: list[DatasetSourceConfig] = Field(
|
||||||
|
default_factory=lambda: [DatasetSourceConfig()],
|
||||||
|
description="Dataset configurations to evaluate (supports multiple).",
|
||||||
|
)
|
||||||
task: BenchmarkTaskConfig = Field(default_factory=BenchmarkTaskConfig)
|
task: BenchmarkTaskConfig = Field(default_factory=BenchmarkTaskConfig)
|
||||||
batch_size: int = Field(default=64, gt=0, description="Batch size for DataLoader")
|
batch_size: int = Field(default=64, gt=0, description="Batch size for DataLoader")
|
||||||
model_table_prefix: str = Field(
|
model_table_prefix: str = Field(
|
||||||
|
|||||||
Reference in New Issue
Block a user