Permutation-Mixed Orthogonal Quantization / experiment.py

Mechanism failed

Raw ⬇ ZIP
  1import json
  2import math
  3import os
  4import numpy as np
  5from scipy.linalg import hadamard
  6
  7SEED = 2532
  8rng = np.random.default_rng(SEED)
  9
 10
 11def signed_perm(d, rng):
 12    p = rng.permutation(d)
 13    s = rng.choice(np.array([-1.0, 1.0]), size=d)
 14    # Sigma z = s * z[p].
 15    S = np.zeros((d, d))
 16    S[np.arange(d), p] = s
 17    return S
 18
 19
 20def make_u(d, rng):
 21    H = hadamard(d).astype(float) / math.sqrt(d)
 22    A = H
 23    B = H
 24    return A @ signed_perm(d, rng) @ B
 25
 26
 27def coherence_sweep(dims=(16, 32, 64, 128, 256), trials=200):
 28    rows = []
 29    for d in dims:
 30        vals = []
 31        ortho = []
 32        for _ in range(trials):
 33            U = make_u(d, rng)
 34            vals.append(math.sqrt(d) * np.max(np.abs(U)))
 35            ortho.append(np.linalg.norm(U.T @ U - np.eye(d), ord='fro'))
 36        vals = np.asarray(vals)
 37        predicted = math.sqrt(2.0 * math.log(d))
 38        rows.append({
 39            'd': d,
 40            'coherence_mu_mean': float(vals.mean()),
 41            'coherence_mu_p95': float(np.quantile(vals, .95)),
 42            'predicted_sqrt_2logd': predicted,
 43            'ratio_mean_to_prediction': float(vals.mean() / predicted),
 44            'orthogonality_fro_mean': float(np.mean(ortho)),
 45        })
 46    return rows
 47
 48
 49def quantize(z, bits=4):
 50    q = 2 ** (bits - 1) - 1
 51    delta = np.max(np.abs(z), axis=-1, keepdims=True) / max(q, 1)
 52    delta = np.maximum(delta, 1e-12)
 53    return np.clip(np.rint(z / delta), -q, q) * delta
 54
 55
 56def make_vectors(d, n, sparsity, rng):
 57    # Each vector has k active coordinates with Gaussian values and one controlled
 58    # outlier scale. This stresses block-max quantization while retaining random orientation.
 59    k = max(1, int(round(d * sparsity)))
 60    X = np.zeros((n, d))
 61    for i in range(n):
 62        idx = rng.choice(d, size=k, replace=False)
 63        X[i, idx] = rng.normal(size=k)
 64    # Normalize every block so comparisons have identical input energy.
 65    X /= np.linalg.norm(X, axis=1, keepdims=True)
 66    return X
 67
 68
 69def quantization_sweep(dims=(64, 128, 256), sparsities=(1/256, 1/64, 1/16, 1/4, 1.0), n=1000, bits=4):
 70    rows = []
 71    for d in dims:
 72        # one transform per dimension, frozen as proposed
 73        U = make_u(d, rng)
 74        for sp in sparsities:
 75            X = make_vectors(d, n, sp, rng)
 76            # Baseline: coordinate quantization; permutation-only is mathematically
 77            # identical to this max-scale quantizer and is included as a check.
 78            Y0 = quantize(X, bits)
 79            perm = rng.permutation(d)
 80            Xperm = X[:, perm]
 81            Yp = quantize(Xperm, bits)[:, np.argsort(perm)]
 82            # Full preconditioner and exact inverse.
 83            Z = X @ U.T
 84            Zq = quantize(Z, bits)
 85            Xhat = Zq @ U
 86            mse0 = float(np.mean((Y0 - X) ** 2))
 87            msep = float(np.mean((Yp - X[:, rng.permutation(d)]) ** 2))
 88            msem = float(np.mean((Xhat - X) ** 2))
 89            # normalized by signal energy, which is 1 per row
 90            rows.append({
 91                'd': d, 'sparsity': sp, 'baseline_nmse': mse0,
 92                'perm_nmse': msep, 'mixed_nmse': msem,
 93                'mixed_gain_vs_baseline': mse0 / max(msem, 1e-30),
 94                'baseline_max_abs_mean': float(np.mean(np.max(np.abs(X), axis=1))),
 95                'mixed_max_abs_mean': float(np.mean(np.max(np.abs(Z), axis=1))),
 96            })
 97    return rows
 98
 99
100def sampling_check(d=128, fractions=(1/32, 1/8, 1/2), trials=3000):
101    # For unbiased coordinate sampling, theory predicts E||xhat-x||^2 = d/m - 1
102    # for every unit vector. Mixing should additionally reduce the largest sampled
103    # coordinate energy for sparse inputs, as predicted by incoherence.
104    U = make_u(d, rng)
105    x = np.zeros(d); x[0] = 1.0
106    z = U @ x
107    rows = []
108    for frac in fractions:
109        m = max(1, int(round(d * frac)))
110        errs0, errsm = [], []
111        for _ in range(trials):
112            I = rng.choice(d, size=m, replace=False)
113            y0 = np.zeros(d); y0[I] = x[I] * d / m
114            yz = np.zeros(d); yz[I] = z[I] * d / m
115            xr = U.T @ yz
116            errs0.append(np.sum((y0-x)**2))
117            errsm.append(np.sum((xr-x)**2))
118        rows.append({'d': d, 'm': m, 'predicted_nmse': d/m-1,
119                     'identity_mean_nmse': float(np.mean(errs0)),
120                     'mixed_mean_nmse': float(np.mean(errsm)),
121                     'mixed_p95_nmse': float(np.quantile(errsm,.95)),
122                     'identity_max_coordinate_energy': 1.0,
123                     'mixed_max_coordinate_energy': float(np.max(z*z))})
124    return rows
125
126
127def main():
128    coh = coherence_sweep()
129    quant = quantization_sweep()
130    # A simple quantitative prediction from incoherence: for a k-sparse unit vector,
131    # a dense incoherent transform should have max coordinate O(sqrt(log d/d)),
132    # while identity has max=1. Report the observed ratio at the sparsest setting.
133    prediction = []
134    for d in (64, 128, 256):
135        r = next(x for x in quant if x['d'] == d and abs(x['sparsity'] - 1/256) < 1e-12)
136        predicted_max = math.sqrt(2 * math.log(d) / d)
137        prediction.append({
138            'd': d,
139            'predicted_mixed_max_scale': predicted_max,
140            'observed_mixed_max_abs': r['mixed_max_abs_mean'],
141            'observed_identity_max_abs': r['baseline_max_abs_mean'],
142            'predicted_identity_max_abs': 1.0,
143            'observed_mixed_to_identity': r['mixed_max_abs_mean'] / r['baseline_max_abs_mean'],
144        })
145    # Prediction 1: coherence is approximately sqrt(2 log d / d), up to a
146    # slowly varying constant; estimate the observed log-log slope after removing log.
147    ds = np.array([x['d'] for x in coh], float)
148    ys = np.array([x['coherence_mu_mean'] for x in coh]) / np.sqrt(ds)
149    slope = float(np.polyfit(np.log(ds), np.log(ys), 1)[0])
150    sampling = sampling_check()
151    result = {'seed': SEED, 'coherence': coh, 'coherence_scaled_loglog_slope': slope,
152              'quantization': quant, 'sparse_spreading_prediction': prediction,
153              'sampling': sampling}
154    with open('results.json', 'w') as f:
155        json.dump(result, f, indent=2)
156    print(json.dumps(result, indent=2))
157
158
159if __name__ == '__main__':
160    main()