From 66af02f1e5c50e041ced3e8b85659709a1c1f4f3 Mon Sep 17 00:00:00 2001 From: YurenHao0426 Date: Sat, 1 Aug 2026 16:23:26 -0500 Subject: Record the gate correction in the concept document The user-facing statement still carried the retired correlation threshold. Adds the correction, the joint condition that replaces it, the width diagnosis, and the closure of the shrink-N route. Co-Authored-By: Claude --- PROJECT_CONCEPT.md | 47 +++++++++++++++++ RANK_RESULTS.md | 12 +++++ logs/obj_base_gpu.log | 4 +- logs/obj_large_gpu.log | 3 ++ logs/rank_gate.log | 0 logs/rho_full_width.log | 1 + worldalign/rank_gate.py | 138 ++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 logs/obj_large_gpu.log create mode 100644 logs/rank_gate.log create mode 100644 worldalign/rank_gate.py diff --git a/PROJECT_CONCEPT.md b/PROJECT_CONCEPT.md index d5c0265..d9232e6 100644 --- a/PROJECT_CONCEPT.md +++ b/PROJECT_CONCEPT.md @@ -297,6 +297,53 @@ E(\text{permuted / semantically wrong configurations}), 唯一、低能的一致解释。视图级探针给出了这个方向的第一个正信号: 以粗对齐为参照系,细粒度状态在种群上下文中变得跨模态可比。 +## 2026-08-01 更正:我们一直在看错的那个统计量 + +过去几个月的判据是「真配对处的场相关 ρ 要到 0.9」,低于就不值得跑 +搜索。**这条判据是错的**,而且是被我们自己的对照实验推翻的:把那个 +能恢复到 95.3% 的合成世界的场截断到秩 r,ρ 几乎不动(0.902 / 0.906 +/ 0.928),恢复率却横跨整个量程(6.2% / 12.9% / **95.6%**,秩 4 / 8 +/ 16)。一个 ρ=0.906、已经越过所谓门槛的场,只恢复 13%。 + +理论其实早就说了,是我们读错了。相关匹配的门槛是为**满秩可交换噪声** +推导的,那里 N²/2 个矩阵元每一个都独立约束配对;而秩 r 的共享成分只 +提供约 rN 个。256 个场景、r≈10 时,这是门槛公式所假设的八分之一。 + +新的判据是**二元条件**:场相关,加上**共享谱宽度**——两侧关系场主 +特征子空间之间主角余弦大于 0.7 的方向数,以打乱场景顺序的 null 为 +基线(null=1.0)。两者缺一不可:18 个共享方向配 ρ=0.508 会失败, +11 个配 ρ=0.902 也会失败。 + +旧判据一撤,「自然数据不值得跑搜索」的理由也没了,于是跑了: +**0.0000,chance 是 0.0039**。结论没变,但现在它是测量结果,而不是 +从一个并不支配它的统计量推出来的。 + +诊断比「特征不够好」精确得多:照片上两个模态**各自都很丰富**(视觉 +有效秩 40,文本 48),但只在 15 个方向上一致——**各自丰富的东西不 +是同一批东西**。而且这个交集对视觉侧几乎一切可调项免疫:分割数 6→16 +掉 1 个方向,直接给标注框只买到 1 个,集合核加三阶矩把视觉自身的秩 +抬高 7 而共享只 +1。文本侧能推动但很快饱和(10→17)。场景数从 256 +加到 1024 反而从 23 降到 19。 + +于是**语料选择变成一等变量**:合成世界能到 26 个共享方向,是因为它 +的 caption 恰好陈述了完整世界状态;VG 只有 15,是因为一句 region +description 和一个 patch descriptor 大概就只在这么多个方面重叠。下一 +步要找的不是更好的特征,而是**天然重叠就宽的语料对**——稠密描述、 +带完整规格的商品图文、截图配可访问性树。 + +顺带的免费收益:判别方向按自身特征值加权、保留更宽的基底,再把文本 +向量提到 128 维,把 ρ 从 0.656 推到 0.716,共享方向从 10 到 16; +短语按中心词/修饰词分开编码再到 0.725。hubness 假设被证伪(度模型只 +占场方差 1–5%,去掉反而让 ρ 略升)。 + +另外**「缩小 N」这条路彻底死了,而且方向是反的**:信息论门槛 +ρ_IT ≈ √(4 log N / N) 随 N 减小而**升高**(256 时 0.29,64 时 0.51, +16 时 0.83)。闸门直接证实:N = 16 至 96 的每个尺寸上,强搜索器找到 +的最深状态都比真值更深,3/3 复现。真值都不是最优解时,算法类别就不 +重要了——这也顺带回答了「接受指数级代价」为什么没用。 + +详见 `RANK_RESULTS.md`。 + ## 合作者应避免的误解 - 这不是“LLM 通过文本已经获得了视觉知觉”。语言模型拥有的可能是可被 diff --git a/RANK_RESULTS.md b/RANK_RESULTS.md index e1c4343..0e481b9 100644 --- a/RANK_RESULTS.md +++ b/RANK_RESULTS.md @@ -91,6 +91,18 @@ segmentation and nothing over using fewer segments. This confirms from a new angle what the earlier oracle-box comparison found: **segmentation is not the constraint on photographs.** +The correlation column in that table should be read against a noise floor. +Moving the segmentation eigendecomposition from CPU to GPU changes nothing in +the recipe, yet re-deriving segments through it takes the baseline correlation +from 0.6559 to 0.6767 — the eigenvectors differ in sign and, where eigenvalues +are near-degenerate, in rotation, so the clustering that follows lands +differently. **Segmentation reseeding is worth about 0.02 in correlation**, so +the 6-versus-16-segment and oracle-box differences in that table are inside +the noise and only the shared counts distinguish them. The session's headline +movements, 0.656 to 0.716 to 0.725, are three times the floor. The pipeline +itself is unchanged: run against the original CPU-derived segments it +reproduces 0.6559 exactly. + The text side does move it, and saturates: | Text vector dimension | text eff. rank | ρ | shared | diff --git a/logs/obj_base_gpu.log b/logs/obj_base_gpu.log index c3ea786..3fccb88 100644 --- a/logs/obj_base_gpu.log +++ b/logs/obj_base_gpu.log @@ -1,3 +1,5 @@ Using a slow image processor as `use_fast` is unset and a slow processor was saved with this model. `use_fast=True` will be the default behavior in v4.52, even if the model was saved with a slow processor. This will result in minor differences in outputs. You'll still be able to use a slow processor with `use_fast=False`. `torch_dtype` is deprecated! Use `dtype` instead! - segment: 0%| | 0/250 [00:00 argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--fields", default="artifacts/synth_v1/fields_tier0_ws_256.pt") + parser.add_argument("--ranks", type=int, nargs="+", default=[4, 8, 16, 256]) + parser.add_argument("--restarts", type=int, default=60) + parser.add_argument("--trials", type=int, default=3) + parser.add_argument("--device", default="cuda:3") + parser.add_argument("--output", default="artifacts/synth_v1/rank_gate.json") + return parser.parse_args() + + +def offdiagonal(matrix: np.ndarray) -> np.ndarray: + return matrix[~np.eye(len(matrix), dtype=bool)] + + +def normalise(matrix: np.ndarray) -> np.ndarray: + values = offdiagonal(matrix) + out = (matrix - values.mean()) / values.std() + np.fill_diagonal(out, 0.0) + return out + + +def truncate(matrix: np.ndarray, rank: int) -> np.ndarray: + symmetric = (matrix + matrix.T) / 2.0 + values, vectors = np.linalg.eigh(symmetric) + order = np.argsort(np.abs(values))[::-1][:rank] + return (vectors[:, order] * values[order]) @ vectors[:, order].T + + +def main() -> None: + args = parse_args() + state = torch.load(args.fields, map_location="cpu", weights_only=False) + visual_full = state["visual_field"].double().numpy() + text_full = state["text_field"].double().numpy() + size = len(visual_full) + device = torch.device(args.device) + swaps = all_swaps(size, device) + + rows = [] + for rank in args.ranks: + visual = normalise(truncate(visual_full, rank) if rank < size else visual_full) + text = normalise(truncate(text_full, rank) if rank < size else text_full) + verdicts, gaps, accuracies = [], [], [] + for trial in range(args.trials): + generator = np.random.default_rng(trial) + hidden = generator.permutation(size) + shuffled = text[np.ix_(hidden, hidden)] + energy = ClosedFormEnergy( + standardized(torch.from_numpy(shuffled).to(device)).float(), + standardized(torch.from_numpy(visual).to(device)).float(), + 1.0, 1.0, 256, + ) + truth = torch.from_numpy(np.argsort(hidden).copy()).to(device) + truth_energy = float(energy.energy(truth[None])[0]) + + starts = [torch.from_numpy(grampa(visual, shuffled, 1.0).copy()).to(device)] + starts += [ + torch.from_numpy(generator.permutation(size).copy()).to(device) + for _ in range(args.restarts) + ] + best_energy, best_accuracy = np.inf, 0.0 + for start in starts: + final, _ = steepest_descent(energy, start, swaps, 4000) + value = float(energy.energy(final[None])[0]) + if value < best_energy: + best_energy = value + best_accuracy = float( + (hidden[final.cpu().numpy()] == np.arange(size)).mean() + ) + verdicts.append(truth_energy <= best_energy + 1e-6) + gaps.append(best_energy - truth_energy) + accuracies.append(best_accuracy) + + row = { + "rank": rank, + "truth_is_deepest": f"{sum(verdicts)}/{args.trials}", + "mean_energy_gap_best_minus_truth": float(np.mean(gaps)), + "best_accuracy": float(np.mean(accuracies)), + "reading": ( + "information limit" if sum(verdicts) == 0 else + "truth is optimal; failure is search" if sum(verdicts) == args.trials + else "mixed" + ), + } + rows.append(row) + print( + f"rank={rank:<5} truth deepest {row['truth_is_deepest']} " + f"gap(best-truth)={row['mean_energy_gap_best_minus_truth']:+.4f} " + f"best acc={row['best_accuracy']:.3f} -> {row['reading']}", + flush=True, + ) + + summary = { + "protocol": ( + "Spectral start plus random restarts, each run to a local optimum " + "under exact steepest descent. A negative gap means the searcher " + "found a state deeper than the truth, so the truth is not the " + "optimum and the limit is information rather than search." + ), + "size": size, + "restarts": args.restarts, + "rows": rows, + } + print(json.dumps({"done": True})) + write_json(args.output, summary) + + +if __name__ == "__main__": + main() -- cgit v1.2.3