miutti's picture
Source: kernel, desktop app, tools (snapshot of the GitHub repo)
df41178 verified
Raw History Blame Contribute Delete
2.42 kB
"""アダマール回転(インコヒーレンス処理)。
重み行列は少数の巨大な値と大量の小さい値で出来ていて、これが量子化に最悪。
直交行列で回転すると値が均されて「全部そこそこの大きさ」になり、
同じビット数でも表現しやすくなる。
肝は **回転行列を保存しなくていい** こと。アダマール行列は構造で決まり、
符号反転はシード1個で再現できるので、オーバーヘッドは実質ゼロ。
推論時は入力 x 側も同じ回転をかける(O(n log n)で安い)。
"""
import numpy as np
def fwht(a):
"""高速ウォルシュ・アダマール変換(最終軸に対して、正規化なし)。"""
a = a.copy()
n = a.shape[-1]
assert n & (n - 1) == 0, "長さは2のべき乗が必要"
h = 1
while h < n:
a = a.reshape(*a.shape[:-1], n // (2 * h), 2, h)
x = a[..., 0, :].copy()
y = a[..., 1, :].copy()
a[..., 0, :] = x + y
a[..., 1, :] = x - y
a = a.reshape(*a.shape[:-3], n)
h *= 2
return a
def signs(n, seed):
rng = np.random.default_rng(seed)
return rng.choice(np.array([-1.0, 1.0], np.float32), size=n)
def rotate_rows(A, d, inverse=False):
"""A の最終軸に Q = H·diag(d)/√n を掛ける。inverse=True で Qᵀ。"""
n = A.shape[-1]
s = 1.0 / np.sqrt(n)
# ★ 2026-09-07 訂正: ここの説明が逆だった。実測で確かめた事実は:
# inverse=False → A @ Qᵀ
# inverse=True → A @ Q (Q = H·diag(d)/√n)
# 上の docstring は逆に書いてあった。**直したのは説明であって計算ではない。**
# 往復(False→True)で元に戻ることは確認済みなので、
# これまでの実験結果は1ビットも変わらない。
if not inverse:
return fwht(A * d) * s # A Q^T 相当(Hは対称)
return fwht(A) * d * s
def make(n, seed=0):
"""(重み用の回転関数, 活性化用の回転関数) を返す。両者は必ず打ち消し合う。"""
d = signs(n, seed)
def rot_W(W): # W' = W Q
return rotate_rows(W, d, inverse=False)
def rot_x(X): # x' = Qᵀ x (X は (n, sample))
return rotate_rows(X.T, d, inverse=False).T
return rot_W, rot_x, d