summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--RANK_RESULTS.md67
-rw-r--r--artifacts/vg_5k/anchor_bound.json66
-rw-r--r--logs/rho_full_width.log1
-rw-r--r--worldalign/anchor_bound.py139
4 files changed, 271 insertions, 2 deletions
diff --git a/RANK_RESULTS.md b/RANK_RESULTS.md
index d520e5e..ffee8c9 100644
--- a/RANK_RESULTS.md
+++ b/RANK_RESULTS.md
@@ -1,7 +1,16 @@
# The gate was measuring the wrong thing
-*2026-08-01. Supersedes the correlation threshold used as the project's
-go/no-go statistic since the synthetic world results.*
+*2026-08-01. Retires the correlation threshold that has been the project's
+go/no-go statistic since the synthetic world results, records two replacement
+statistics that were proposed and refuted the same day, and settles on a third
+that survives its controls: an upper bound obtained by giving the solver half
+the correspondence and asking it to recover the rest.*
+
+**Result in one line.** The correlation does not govern recovery in either
+direction; what does is whether a representation can identify scenes at all,
+measured by matching against anchors whose correspondence is given. Visual
+Genome scores 0.291 on that bound against 0.98 for the synthetic world that
+recovers, and its blind recovery is 0.0000 against 0.0039 chance.
## The correlation does not govern recovery
@@ -102,6 +111,60 @@ than everything found (recoverable), tied with what is found (symmetry), or
shallower than what is found (information deficit). The gate is running on both
fields; the sections below should be read against this correction.
+## Resolution: ask the matching question, and give one thing away
+
+Three statistics failed the same way — fields agreeing on the number and
+disagreeing on recovery — and the pattern in the failures is that each was
+invented by staring at the fields rather than by asking what matching needs.
+The one that works asks directly, with a single concession.
+
+Declare half the scenes anchors and hand over their correspondence. Describe
+every remaining scene, in each modality, by its field row against the anchors.
+Match the two descriptions one-to-one by Hungarian assignment. Nothing is
+searched; the answer is a linear program, and it costs seconds.
+
+This **upper-bounds blind recovery**, because blind recovery must additionally
+discover the anchor correspondence. It held in every field measured:
+
+| Field | ρ | anchor bound | blind recovery | search gap |
+|---|---|---|---|---|
+| synthetic, full | 0.928 | 0.989 | 0.958 | +0.031 |
+| synthetic, noise 0.35 | 0.828 | 0.984 | 0.947 | +0.037 |
+| synthetic, caption omits size | 0.830 | **0.997** | 0.056 | **+0.941** |
+| synthetic, rank 8 | 0.906 | 0.930 | 0.129 | +0.801 |
+| Visual Genome, best | 0.731 | **0.291** | 0.000 | +0.291 |
+| Visual Genome, baseline | 0.656 | 0.195 | 0.000 | +0.195 |
+
+**The bound splits the deficit in two, and the two halves call for opposite
+work.** The bound itself says whether the representation identifies scenes at
+all; the gap between bound and blind recovery says whether the search can find
+what the representation contains.
+
+Read down the table and the three regimes are unmistakable. The two recovering
+fields have a bound near one and a gap of three points — representation good,
+search working. Caption-omission and rank-8 truncation have a bound of 0.93 to
+**0.997** and lose almost everything to the search: the information is intact,
+perfectly so in the omission case, and blind matching cannot reach it. Visual
+Genome is the opposite failure — its bound is 0.291, so **even with the
+correspondence handed over, seven scenes in ten cannot be identified**, and no
+solver improvement addresses that.
+
+This corrects two things I wrote earlier today. The caption-suppression ladder
+does not show information being destroyed; it shows blind searchability being
+destroyed while the information stays. Suppressing a factor makes scenes that
+differed only in that factor interchangeable *under permutation*, which the
+given anchors break and blind matching does not. And Visual Genome's problem
+was never the width of its shared spectrum — it is that a region description
+and a patch descriptor, compared through relation profiles, identify a
+photograph about 29% of the time.
+
+The bound is an empirical upper bound rather than a proven one: handing over
+anchors removes search but also changes the estimation problem. It was not
+violated in six fields spanning the full range of outcomes, and unlike the
+three refuted statistics it separates every case they collapsed. It is now the
+project's cheap gate, and a representation that cannot pass it should not be
+searched.
+
## What the shared spectrum measures
Each field is eigendecomposed and the principal angles between the two leading
diff --git a/artifacts/vg_5k/anchor_bound.json b/artifacts/vg_5k/anchor_bound.json
new file mode 100644
index 0000000..2de1390
--- /dev/null
+++ b/artifacts/vg_5k/anchor_bound.json
@@ -0,0 +1,66 @@
+{
+ "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": [
+ {
+ "label": "synth-full",
+ "correlation": 0.928480988366389,
+ "probe_scenes": 128,
+ "chance": 0.0078125,
+ "nearest_neighbour": 0.9375,
+ "anchor_bound": 0.9890625,
+ "blind_recovery": 0.958,
+ "search_gap": 0.031062499999999993
+ },
+ {
+ "label": "synth-noise035",
+ "correlation": 0.8276655471168534,
+ "probe_scenes": 128,
+ "chance": 0.0078125,
+ "nearest_neighbour": 0.896875,
+ "anchor_bound": 0.984375,
+ "blind_recovery": 0.947,
+ "search_gap": 0.03737500000000005
+ },
+ {
+ "label": "synth-omitsize",
+ "correlation": 0.8301825436675786,
+ "probe_scenes": 128,
+ "chance": 0.0078125,
+ "nearest_neighbour": 0.89375,
+ "anchor_bound": 0.996875,
+ "blind_recovery": 0.056,
+ "search_gap": 0.9408749999999999
+ },
+ {
+ "label": "synth-rank8",
+ "correlation": 0.9058447537972103,
+ "probe_scenes": 128,
+ "chance": 0.0078125,
+ "nearest_neighbour": 0.746875,
+ "anchor_bound": 0.9296875,
+ "blind_recovery": 0.129,
+ "search_gap": 0.8006875
+ },
+ {
+ "label": "natural-best",
+ "correlation": 0.7309253746041517,
+ "probe_scenes": 128,
+ "chance": 0.0078125,
+ "nearest_neighbour": 0.1671875,
+ "anchor_bound": 0.290625,
+ "blind_recovery": 0.0,
+ "search_gap": 0.290625
+ },
+ {
+ "label": "natural-baseline",
+ "correlation": 0.6558829559111277,
+ "probe_scenes": 128,
+ "chance": 0.0078125,
+ "nearest_neighbour": 0.1296875,
+ "anchor_bound": 0.1953125,
+ "blind_recovery": 0.0,
+ "search_gap": 0.1953125
+ }
+ ],
+ "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."
+} \ No newline at end of file
diff --git a/logs/rho_full_width.log b/logs/rho_full_width.log
index b81ce33..b33469d 100644
--- a/logs/rho_full_width.log
+++ b/logs/rho_full_width.log
@@ -2,3 +2,4 @@ noise=0.0 rho=0.928 shared= 21.0 recovery=0.961
noise=0.2 rho=0.893 shared= 18.0 recovery=0.971
noise=0.35 rho=0.828 shared= 15.7 recovery=0.947
noise=0.5 rho=0.743 shared= 13.3 recovery=0.707
+noise=0.7 rho=0.624 shared= 11.3 recovery=0.201
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()