File size: 8,236 Bytes
df41178
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
"""E8格子によるベクトル量子化(8次元・語彙の保存が不要)。

なぜ E8 か:
  8次元で最も密な球充填。一様雑音に対する形状利得は立方格子 Z^8 比 +0.65 dB
  (正規化二次モーメント G(E8)=0.0717 対 G(Z)=0.0833)。
  語彙(コードブック)を保存しなくてよく、最近点の探索が O(k) で済む。
  k-means の語彙は学習データ(重み自身)に過適合するが、格子は構造で勝負する。

構成:
  E8 = D8 ∪ (D8 + ½·1)        D8 = {整数点のうち座標和が偶数}
  最近点復号 (Conway & Sloane, Ch.20):
    D8: 各座標を丸め、和が奇数なら「丸め誤差が最大の座標」を逆側へ1動かす
    E8: 2つのコセットで復号し、近い方を採る

AdaRound(出力誤差で丸め先を選ぶ)を厳密に組み込める:
  距離を Σ_j w_j (y_j − c_j)² にしたとき、D8 の最近点は「座標ごとの丸め」か
  「そこから1座標だけ ±1 動かした点」のどちらかであることは変わらない。
  奇数和の直しで動かす座標を argmax|d_j| から argmin w_j(1−2|d_j|) に替えるだけ。
  w は (8,)(全行共通)でも (n,8)(行ごと)でもよい。

固定レートにするため、ノルム² ≤ R2 の格子点だけを語彙にする(球で切る):
  R2=10 → 56881 点 (15.80 bit / 8重み = 1.975 bit/重み)
  R2=12 → 117361 点 (16.84 bit / 8重み = 2.105 bit/重み)
球の外に落ちた入力は、原点方向へ縮めて復号し直し、さらに隣接格子点(240本)を
見て局所改善する。総当たりの最近点と一致することを test_e8_ball.py で確認済み。
"""
import numpy as np

# E8 のテータ級数: ノルム² → その殻にある格子点の数(検算済み: test_e8.py)
_SHELL = {0: 1, 2: 240, 4: 2160, 6: 6720, 8: 17520, 10: 30240,
          12: 60480, 14: 82560, 16: 140400}


def count(R2):
    """ノルム² ≤ R2 の格子点の数(=語彙の大きさ)"""
    return sum(c for n, c in _SHELL.items() if n <= R2 + 1e-9)


def bits(R2):
    """1重みあたりのビット数(8次元ぶんの語彙を 8 で割る)"""
    return float(np.log2(count(R2)) / 8.0)


def _bw(w, Y):
    """w を (n,8) に揃える(None はそのまま)"""
    if w is None:
        return None
    w = np.asarray(w, np.float32)
    return np.broadcast_to(w, Y.shape) if w.ndim == 1 else w


def _wdist(Y, C, W):
    E = Y - C
    if W is None:
        return (E * E).sum(1)
    return (E * E * W).sum(1)


def _d8(Y, W=None):
    """D8 の最近点。Y:(n,8)  W:(n,8) または None(重み付き距離)"""
    F = np.rint(Y)
    D = Y - F                                    # 丸め誤差 ∈ [-0.5, 0.5]
    odd = np.mod(F.sum(1), 2.0) != 0
    if odd.any():
        idx = np.nonzero(odd)[0]
        Do = D[idx]
        # 座標 j を「丸めと逆側」へ1動かす追加コストは w_j (1 − 2|d_j|)
        cost = 1.0 - 2.0 * np.abs(Do)
        if W is not None:
            cost = cost * W[idx]
        j = cost.argmin(1)
        r = np.arange(len(idx))
        step = np.where(Do[r, j] >= 0, 1.0, -1.0)
        F[idx, j] += step
    return F


def nearest(Y, w=None):
    """E8 の最近点(重み付き距離にも対応)。Y:(n,8)"""
    W = _bw(w, Y)
    C0 = _d8(Y, W)
    C1 = _d8(Y - 0.5, W) + 0.5
    pick = _wdist(Y, C1, W) < _wdist(Y, C0, W)
    return np.where(pick[:, None], C1, C0)


def _min_vectors():
    """E8 の最短ベクトル 240 本(隣接格子点への移動)。
    112本: (±1, ±1, 0^6) の並べ替え   128本: (±½)^8 で負号が偶数個"""
    import itertools
    out = []
    for i in range(8):
        for j in range(i + 1, 8):
            for si in (1.0, -1.0):
                for sj in (1.0, -1.0):
                    v = np.zeros(8, np.float32); v[i] = si; v[j] = sj
                    out.append(v)
    for signs in itertools.product((0.5, -0.5), repeat=8):
        if sum(1 for s in signs if s < 0) % 2 == 0:
            out.append(np.array(signs, np.float32))
    return np.array(out, np.float32)


_MIN = _min_vectors()
# 縮め方の刻み。粗い5刻み+局所改善2周では遠い入力で総当たりに 20% 負けた。
# 11刻み+3周なら全ての検証入力で総当たりと一致(test_e8_ball.py)。
_SHRINK = (0.97, 0.94, 0.90, 0.85, 0.80, 0.72, 0.62, 0.50, 0.35, 0.20, 0.0)


def _improve_in_ball(Yo, C, W, R2, rounds=3, chunk=2048):
    """球内の候補 C を、隣接格子点(240本)を見て局所的に改善する。
    「原点方向へ縮めて復号」は光線上しか探さないので、球の殻の上で横にずれた
    最近点を取りこぼす。隣を見に行けば総当たりに並ぶ(test_e8_ball.py)。"""
    C = C.copy()
    for _ in range(rounds):
        moved = False
        for i in range(0, len(Yo), chunk):
            y = Yo[i:i + chunk]; c = C[i:i + chunk]
            cand = c[:, None, :] + _MIN[None, :, :]                  # (m,240,8)
            ok = (cand * cand).sum(2) <= R2 + 1e-9
            E = cand - y[:, None, :]
            if W is None:
                d = (E * E).sum(2)
            else:
                d = (E * E * W[i:i + chunk][:, None, :]).sum(2)
            d[~ok] = np.inf
            j = d.argmin(1)
            dj = d[np.arange(len(y)), j]
            d0 = _wdist(y, c, None if W is None else W[i:i + chunk])
            imp = dj < d0 - 1e-9
            if imp.any():
                moved = True
                c[imp] = cand[np.nonzero(imp)[0], j[imp]]
        if not moved:
            break
    return C


def quantize(Y, w=None, R2=10.0, rounds=3):
    """ノルム² ≤ R2 の E8 格子点へ量子化する。Y:(n,8)  w:(8,)/(n,8)/None"""
    Y = np.asarray(Y, np.float32)
    W = _bw(w, Y)
    C = nearest(Y, W)
    out = (C * C).sum(1) > R2 + 1e-9
    if out.any():
        idx = np.nonzero(out)[0]
        Yo = Y[idx]
        Wo = None if W is None else np.ascontiguousarray(W[idx])
        best = np.zeros_like(Yo)
        bestd = np.full(len(idx), np.inf)
        for t in _SHRINK:
            Ct = nearest(Yo * t, Wo)
            ok = (Ct * Ct).sum(1) <= R2 + 1e-9
            d = _wdist(Yo, Ct, Wo)
            d[~ok] = np.inf
            imp = d < bestd
            best[imp] = Ct[imp]
            bestd[imp] = d[imp]
        C[idx] = _improve_in_ball(Yo, best, Wo, R2, rounds=rounds)
    return C


def quantize_c(Y, w=None, R2=10.0, compand=1.0, rounds=3):
    """圧伸つきの量子化。y → sign(y)|y|^p を格子に載せ、復元後 1/p 乗で戻す。
    p<1 で原点付近が細かく、裾が粗くなる(裾の重い分布向け)。p=1 なら quantize と同じ。"""
    if compand == 1.0:
        return quantize(Y, w, R2, rounds)
    Y = np.asarray(Y, np.float32)
    Yc = np.sign(Y) * np.abs(Y) ** compand
    C = quantize(Yc, w, R2, rounds)
    return (np.sign(C) * np.abs(C) ** (1.0 / compand)).astype(np.float32)


def fit_scale(Y, R2=10.0, grid=None, sample=24000, seed=0, compand=1.0):
    """格子の目盛り α を探す: min_α Σ‖y − α·Q(y/α)‖²(標本で近似・粗→細の2段)。
    Y:(n,8) は正規化済みの部分ベクトル。圧伸があれば圧伸した空間で目盛りを当てる。"""
    rng = np.random.default_rng(seed)
    Y = np.asarray(Y, np.float32)
    if len(Y) > sample:
        Y = Y[rng.choice(len(Y), sample, replace=False)]
    Yc = Y if compand == 1.0 else np.sign(Y) * np.abs(Y) ** compand
    rms = float(np.sqrt((Yc * Yc).sum(1).mean())) + 1e-12
    base = rms / np.sqrt(R2)

    def err(a):
        C = quantize_c(Y / a, None, R2, compand, rounds=1) * a
        return float(((Y - C) ** 2).sum())

    if grid is None:
        grid = base * np.exp(np.linspace(np.log(0.25), np.log(1.6), 9))
    errs = [err(a) for a in grid]
    i = int(np.argmin(errs)); a0 = float(grid[i])
    # 細: 最良点の両隣の間を 5 点で刻む
    lo = float(grid[max(i - 1, 0)]); hi = float(grid[min(i + 1, len(grid) - 1)])
    fine = np.exp(np.linspace(np.log(lo), np.log(hi), 7))[1:-1]
    for a in fine:
        e = err(a)
        if e < errs[i]:
            errs[i], a0 = e, float(a)
    return a0