diff options
| author | YurenHao0426 <Blackhao0426@gmail.com> | 2026-08-01 16:15:41 -0500 |
|---|---|---|
| committer | YurenHao0426 <Blackhao0426@gmail.com> | 2026-08-01 16:15:41 -0500 |
| commit | 08fd63b8fee62ccdc284380c9832900ee83f9ede (patch) | |
| tree | 53d49406a0b778e25c7604a7ae0fe5d2ecf15367 /worldalign/natural_objects.py | |
| parent | 58b9c84dae293359f498fdf6afd533df5c9d3c25 (diff) | |
Retire the correlation gate: the shared spectrum governs recovery
A controlled truncation refutes the project's central go/no-go rule.
Projecting the recovering synthetic fields to rank r holds the field
correlation at 0.902-0.929 while recovery moves 6.2% -> 12.9% -> 95.6%
across ranks 4, 8, 16. A field past the supposed 0.9 threshold recovers
13%, so correlation neither predicts nor forbids recovery and the width
of the shared spectrum is what moves it.
The gate becomes a joint condition on correlation and shared width,
measured by principal angles against a scene-shuffled null. Neither
suffices alone: 18 shared directions at 0.508 fails, 11 at 0.902 fails.
With the old gate retired, natural data was finally searched: 0.0000
against 0.0039 chance. The old verdict was right, its reasoning was not.
Also closes route D by measurement. rho_IT ~ sqrt(4 log N / N) rises as N
falls, and at N = 16 through 96 the deepest state a strong searcher
reaches is deeper than the truth in 3/3 replicates at every size.
Free gains: eigenvalue-weighted projection over a wide basis with
128-dim text vectors takes the correlation 0.656 -> 0.716 and shared
width 10 -> 16. Hubness refuted as an inflation hypothesis.
Moving the per-image segmentation eigendecomposition onto the GPU cut
batch time from 130s to 1.9s.
Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'worldalign/natural_objects.py')
| -rw-r--r-- | worldalign/natural_objects.py | 47 |
1 files changed, 34 insertions, 13 deletions
diff --git a/worldalign/natural_objects.py b/worldalign/natural_objects.py index 65e035b..6e18259 100644 --- a/worldalign/natural_objects.py +++ b/worldalign/natural_objects.py @@ -62,7 +62,8 @@ def parse_args() -> argparse.Namespace: def spectral_segments( - features: torch.Tensor, grid: int, segments: int, seed: int + features: torch.Tensor, grid: int, segments: int, seed: int, + device: torch.device | str = "cpu", ) -> np.ndarray: """Segment a patch grid by clustering the normalised affinity spectrum. @@ -70,22 +71,42 @@ def spectral_segments( stay connected, then the leading eigenvectors of the normalised Laplacian are clustered. This is the standard unsupervised recipe over self-supervised features. + + The eigendecomposition dominates the run: one dense symmetric problem of + side grid^2 per image, which on a contended CPU costs more than the + forward pass that produced the features. Running it on the accelerator + that is already holding the model turns a multi-hour extraction into a + short one, and the spatial prior is built once and cached rather than + rebuilt per image. """ from sklearn.cluster import KMeans - normalised = F.normalize(features.double(), dim=-1) - affinity = (normalised @ normalised.T).clamp_min(0).numpy() - coordinates = np.stack( - np.meshgrid(np.arange(grid), np.arange(grid), indexing="ij"), -1 - ).reshape(-1, 2).astype(np.float64) - distance = ((coordinates[:, None, :] - coordinates[None, :, :]) ** 2).sum(-1) - affinity = affinity * np.exp(-distance / (2 * (grid / 4.0) ** 2)) + normalised = F.normalize(features.to(device).double(), dim=-1) + affinity = (normalised @ normalised.T).clamp_min(0) + affinity = affinity * _spatial_prior(grid, device) degree = affinity.sum(1) - laplacian = affinity / np.sqrt(np.outer(degree, degree) + 1e-9) - values, vectors = np.linalg.eigh(laplacian) + laplacian = affinity / torch.sqrt(torch.outer(degree, degree) + 1e-9) + values, vectors = torch.linalg.eigh(laplacian) embedding = vectors[:, -segments:] - embedding /= np.linalg.norm(embedding, axis=1, keepdims=True).clip(1e-9) - return KMeans(segments, n_init=10, random_state=seed).fit_predict(embedding) + embedding = embedding / embedding.norm(dim=1, keepdim=True).clamp_min(1e-9) + return KMeans(segments, n_init=10, random_state=seed).fit_predict( + embedding.cpu().numpy() + ) + + +_PRIOR_CACHE: dict[tuple, torch.Tensor] = {} + + +def _spatial_prior(grid: int, device: torch.device | str) -> torch.Tensor: + """Gaussian locality weight on the patch lattice, built once per grid.""" + key = (grid, str(device)) + if key not in _PRIOR_CACHE: + axis = torch.arange(grid, dtype=torch.float64, device=device) + rows, cols = torch.meshgrid(axis, axis, indexing="ij") + coordinates = torch.stack([rows.flatten(), cols.flatten()], dim=-1) + distance = torch.cdist(coordinates, coordinates) ** 2 + _PRIOR_CACHE[key] = torch.exp(-distance / (2 * (grid / 4.0) ** 2)) + return _PRIOR_CACHE[key] def describe_segments( @@ -220,7 +241,7 @@ def main() -> None: ) else: labels = spectral_segments( - patches[position], grid, args.segments, args.seed + patches[position], grid, args.segments, args.seed, args.device ) described = describe_segments( labels, patches[position], raw[position], grid, args.min_patches |
