Files
caption-test/scripts/compare_runs.py
pikaliov abb3337f8d Rewrite: GatedFusion architecture + UAV-GeoLoc dataset
Architecture v2:
- Query branch: drone + text -> GatedFusion -> proj -> query_emb
- Gallery branch: satellite -> proj -> gallery_emb
- Single InfoNCE loss (asymmetric 0.6/0.4)
- GatedFusion: learnable gated addition (sigma(alpha)*img + (1-sigma(alpha))*text)
- Baseline mode: gate=1.0 (text ignored)

Dataset:
- UAV-GeoLoc loader with template captions from path metadata
- 27 terrain types with predefined features
- Random positive crop sampling per epoch

Configs: balanced (gate=0.7), baseline (no text), text_heavy (gate=0.3)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 17:13:00 +03:00

130 lines
4.0 KiB
Python

from __future__ import annotations
"""Compare baseline (no text) vs caption-fused runs. Compute Delta R@1 report.
Usage:
python -m scripts.compare_runs \
--baseline_report out/caption_test/baseline_no_text/eval_report.json \
--full_report out/caption_test/balanced/eval_report.json \
--output out/caption_test/comparison.md
"""
import argparse
import json
from pathlib import Path
_DIRECTIONS = (
"query_to_gallery",
"gallery_to_query",
)
_KS = (1, 5, 10)
def _load_metrics(report_path: Path) -> dict[str, float]:
with report_path.open("r", encoding="utf-8") as f:
data = json.load(f)
return data.get("metrics", data)
def _format_row(name: str, baseline: dict[str, float], full: dict[str, float]) -> str:
cells = [name]
for k in _KS:
key = f"r@{k}_{name}"
b = baseline.get(key, float("nan"))
f_ = full.get(key, float("nan"))
delta = f_ - b if (b == b and f_ == f_) else float("nan")
cells.append(f"{b:.4f} -> {f_:.4f} (D {delta:+.4f})")
return "| " + " | ".join(cells) + " |"
def _interpret_delta(delta: float) -> str:
if delta >= 0.03:
return "PASS -- captions informative (D R@1 >= +3%)"
if delta >= 0.01:
return "MARGINAL -- consider VLM refinement (+1% <= D < +3%)"
if delta >= 0:
return "WEAK -- captions add little signal (< +1%)"
return "HARMFUL -- captions confuse model (D < 0)"
def build_comparison_markdown(
baseline: dict[str, float],
full: dict[str, float],
) -> str:
lines: list[str] = ["# Caption Quality Test: Comparison Report", ""]
primary = "query_to_gallery"
primary_key = f"r@1_{primary}"
primary_delta = full.get(primary_key, 0.0) - baseline.get(primary_key, 0.0)
verdict = _interpret_delta(primary_delta)
lines.append(f"## Primary metric: D R@1 (query->gallery) = {primary_delta:+.4f}")
lines.append("")
lines.append(f"**Verdict:** {verdict}")
lines.append("")
# Gate value.
gate = full.get("gate", None)
if gate is not None:
lines.append(f"**Fusion gate:** {gate:.4f} (1.0 = text ignored, 0.0 = image ignored)")
lines.append("")
lines.append("## All directions x K")
lines.append("")
header = "| Direction | R@1 base -> full | R@5 base -> full | R@10 base -> full |"
sep = "|---|---|---|---|"
lines.extend([header, sep])
for direction in _DIRECTIONS:
lines.append(_format_row(direction, baseline, full))
lines.append("")
lines.append("## Decision rule")
lines.append("")
lines.append("- D R@1 >= +3% -> captions pass, proceed to production")
lines.append("- +1% <= D R@1 < +3% -> add VLM refinement, re-run")
lines.append("- D R@1 < +1% -> redesign caption pipeline")
lines.append("- D R@1 < 0 -> critical bug, investigate")
lines.append("")
return "\n".join(lines)
def main() -> None:
parser = argparse.ArgumentParser(description="Compare baseline vs caption runs.")
parser.add_argument("--baseline_report", type=Path, required=True)
parser.add_argument("--full_report", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
baseline = _load_metrics(args.baseline_report)
full = _load_metrics(args.full_report)
md = build_comparison_markdown(baseline=baseline, full=full)
args.output.parent.mkdir(parents=True, exist_ok=True)
with args.output.open("w", encoding="utf-8") as f:
f.write(md)
summary = {
"baseline_metrics": baseline,
"full_metrics": full,
"deltas": {
f"delta_r@{k}_{d}": (
full.get(f"r@{k}_{d}", 0.0) - baseline.get(f"r@{k}_{d}", 0.0)
)
for d in _DIRECTIONS
for k in _KS
},
}
summary_path = args.output.with_suffix(".json")
with summary_path.open("w", encoding="utf-8") as f:
json.dump(summary, f, indent=2)
print(f"Comparison saved: {args.output}")
print(f"Summary saved: {summary_path}")
if __name__ == "__main__":
main()