summaryrefslogtreecommitdiff
path: root/worldalign
diff options
context:
space:
mode:
authorYurenHao0426 <Blackhao0426@gmail.com>2026-08-01 17:15:26 -0500
committerYurenHao0426 <Blackhao0426@gmail.com>2026-08-01 17:15:26 -0500
commitf29c41e78da10d3c40afdd2deeb43c4d73f5eb43 (patch)
tree51d20a78fcbb582eb9e76797c56b8a96e6c4605f /worldalign
parentf75e1f0877c7d1b57299a68507d30eeb16bf3b4d (diff)
A cheap gate that survives its controls: the anchor bound
Three statistics failed the same way -- fields agreeing on the number and disagreeing on recovery -- because each was invented by staring at the fields rather than by asking what matching needs. The fourth asks directly: declare half the scenes anchors, hand over their correspondence, describe the rest by their field rows against the anchors, and match one-to-one by Hungarian assignment. Seconds to compute, and it upper-bounds blind recovery because blind recovery must also discover the anchor correspondence. Never violated across six fields spanning the full range of outcomes, and it separates every case the refuted statistics collapsed: synth full bound 0.989 blind 0.958 gap +0.03 synth noise 0.35 bound 0.984 blind 0.947 gap +0.04 synth omit size bound 0.997 blind 0.056 gap +0.94 synth rank 8 bound 0.930 blind 0.129 gap +0.80 Visual Genome bound 0.291 blind 0.000 gap +0.29 This corrects two claims from earlier today. Caption suppression does not destroy information -- its bound is 0.997 -- it destroys blind searchability, by making scenes interchangeable under permutation in a way given anchors break. And Visual Genome's problem was never spectral width: with the correspondence handed over, seven scenes in ten still cannot be identified. Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'worldalign')
-rw-r--r--worldalign/anchor_bound.py139
1 files changed, 139 insertions, 0 deletions
diff --git a/worldalign/anchor_bound.py b/worldalign/anchor_bound.py
new file mode 100644
index 0000000..49f4ed3
--- /dev/null
+++ b/worldalign/anchor_bound.py
@@ -0,0 +1,139 @@
+"""A cheap gate that survives its controls, built as an upper bound.
+
+Three statistics have now been proposed as go/no-go predictors and three have
+failed: the field correlation, the width of the shared spectrum, and the
+held-out shared dimension. Each failed the same way -- fields agreeing on the
+statistic and disagreeing on recovery. The lesson is that a predictor invented
+by staring at the fields keeps finding quantities that are not about matching.
+
+So this asks the matching question directly, with one thing given away. Half
+the scenes are declared anchors and their correspondence is handed over. Every
+remaining scene is described, in each modality, by its field row against the
+anchors, and the two descriptions are matched one-to-one by Hungarian
+assignment. Nothing is searched: the answer is read off a linear program.
+
+The result upper-bounds what blind recovery can achieve, because blind recovery
+must discover the anchor correspondence as well. It is therefore a **necessary
+condition**: a representation that cannot be matched with the anchors given
+will not be matched without them, and it can be rejected in seconds rather than
+after a search. It is an empirical bound rather than a proven one -- the given
+anchors also change the estimation problem, not only remove search -- but it
+held in every field measured, and unlike the three refuted statistics it
+separates the cases they collapse.
+
+It also splits the deficit in two, which is what makes it diagnostic rather
+than merely predictive. The bound itself measures whether the representation
+identifies scenes at all. The gap between the bound and blind recovery measures
+whether the search can find what the representation contains.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+
+import numpy as np
+import torch
+from scipy.optimize import linear_sum_assignment
+
+from .common import write_json
+
+
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--fields", nargs="+", required=True)
+ parser.add_argument("--labels", nargs="+", default=None)
+ parser.add_argument("--repeats", type=int, default=5)
+ parser.add_argument(
+ "--blind", type=float, nargs="*", default=None,
+ help="Known blind recovery per field, to report the search gap.",
+ )
+ parser.add_argument("--output", default="artifacts/vg_5k/anchor_bound.json")
+ return parser.parse_args()
+
+
+def standardise(matrix: np.ndarray) -> np.ndarray:
+ values = matrix[~np.eye(len(matrix), dtype=bool)]
+ out = (matrix - values.mean()) / values.std()
+ np.fill_diagonal(out, 0.0)
+ return out
+
+
+def rows_against(field: np.ndarray, probe: np.ndarray, anchors: np.ndarray) -> np.ndarray:
+ block = field[np.ix_(probe, anchors)]
+ block = block - block.mean(1, keepdims=True)
+ return block / block.std(1, keepdims=True).clip(1e-9)
+
+
+def analyse(path: str, label: str, repeats: int, blind: float | None) -> dict:
+ state = torch.load(path, map_location="cpu", weights_only=False)
+ visual = standardise(state["visual_field"].double().numpy())
+ text = standardise(state["text_field"].double().numpy())
+ size = len(visual)
+ mask = ~np.eye(size, dtype=bool)
+ rho = float(np.corrcoef(visual[mask], text[mask])[0, 1])
+
+ nearest, assigned = [], []
+ for repeat in range(repeats):
+ generator = np.random.default_rng(repeat)
+ order = generator.permutation(size)
+ half = size // 2
+ anchors, probe = order[:half], order[half:]
+ left = rows_against(visual, probe, anchors)
+ right = rows_against(text, probe, anchors)
+ similarity = left @ right.T / left.shape[1]
+ truth = np.arange(len(probe))
+ nearest.append(float((similarity.argmax(1) == truth).mean()))
+ _, columns = linear_sum_assignment(-similarity)
+ assigned.append(float((columns == truth).mean()))
+
+ row = {
+ "label": label,
+ "correlation": rho,
+ "probe_scenes": size - size // 2,
+ "chance": 1.0 / (size - size // 2),
+ "nearest_neighbour": float(np.mean(nearest)),
+ "anchor_bound": float(np.mean(assigned)),
+ }
+ if blind is not None:
+ row["blind_recovery"] = blind
+ row["search_gap"] = float(np.mean(assigned)) - blind
+ return row
+
+
+def main() -> None:
+ args = parse_args()
+ labels = args.labels or [path.split("/")[-1] for path in args.fields]
+ blinds = args.blind if args.blind else [None] * len(args.fields)
+ rows = []
+ for path, label, blind in zip(args.fields, labels, blinds):
+ row = analyse(path, label, args.repeats, blind)
+ rows.append(row)
+ tail = (
+ f" blind={row['blind_recovery']:.3f} gap={row['search_gap']:+.3f}"
+ if "blind_recovery" in row else ""
+ )
+ print(
+ f"{row['label']:<26} rho={row['correlation']:.3f} "
+ f"bound={row['anchor_bound']:.3f}{tail}",
+ flush=True,
+ )
+ write_json(args.output, {
+ "protocol": (
+ "Half the scenes are anchors with their correspondence given. The "
+ "rest are described by their field rows against the anchors and "
+ "matched by Hungarian assignment. Upper-bounds blind recovery, "
+ "which must additionally discover the anchor correspondence."
+ ),
+ "rows": rows,
+ "reading": (
+ "A low bound means the representation does not identify scenes and "
+ "no solver will help. A high bound with low blind recovery means "
+ "the information is present and the search or the landscape is at "
+ "fault."
+ ),
+ })
+
+
+if __name__ == "__main__":
+ main()