0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

同じ局所 MSE でも出力誤差が異なる条件を Python で検証する

0
Posted at

再帰状態を量子化すると、各時刻で生じた丸め誤差が後続の状態更新へ引き継がれます。評価する際は、量子化直後の平均二乗誤差(MSE)に加えて、時間方向に累積した状態誤差と、最終出力へ現れた誤差を測定する必要があります。局所誤差の大きさ、残存時間、出力に影響する方向を分ける考え方は、再帰状態の低ビット量子化を扱う議論にも現れます[1]。

本稿では、2 次元の線形再帰状態を Python で計算します。同じ局所 MSE を持つ誤差の方向を変える実験と、実際の丸め操作を 1 ステップごと・8 ステップごとに適用する実験を分け、出力誤差が決まる条件を確認します。

再帰状態の量子化を比較する条件を固定する

使用するのは Python 3.13 と NumPy 2.3.4 です。NumPy のバージョンを固定して環境を作成します[2]。

python3.13 -m venv .venv
. .venv/bin/activate
python -m pip install numpy==2.3.4
python --version
python -c "import numpy; print(numpy.__version__)"

以下のパラメーターを使用します。

項目 値 役割
状態の次元 2 誤差の方向を区別する
状態遷移行列 diag(0.99, 0.5) 状態の 2 成分の保持率を変える
出力読み出し (1.0, 0.1) 成分ごとの出力感度を変える
入力 (0.04, 0.09) 各ステップで加える一定値
時間長 128 ステップ 長期の累積を観察する
量子化刻み幅 0.125 丸め先の値の間隔を固定する
量子化間隔 1 または 8 ステップ 誤差の発生時刻を変える
数値型 float64 数値計算の型を固定する
乱数 なし 同じ結果を再現する

量子化前の状態を $x_t$、状態遷移行列を $A$、入力を $u_t$ とすると、状態更新は次式です。

x_t=Ax_{t-1}+u_t,\qquad x_0=\begin{pmatrix}0\\0\end{pmatrix}

出力は $y_t=Gx_t$ とし、$G=(1,0.1)$ を使います。第 1 成分の誤差は、第 2 成分の同じ大きさの誤差より出力へ 10 倍大きく寄与します。

局所量子化誤差と累積状態誤差を分けて計算する

量子化済みの状態を $\hat{x}_t$ とします。量子化前の更新値 $z_t$ に対し、量子化する時刻だけ丸めを実行します。

z_t=A\hat{x}_{t-1}+u_t
\hat{x}_t=\begin{cases}
Q_\Delta(z_t)&\text{量子化する時刻}\\
z_t&\text{それ以外}
\end{cases}
Q_\Delta(z)=\Delta\operatorname{round}(z/\Delta),\qquad \Delta=0.125

np.round は値がちょうど中間にある場合、偶数側へ丸めます。今回の量子化器は np.round を各成分へ適用するもので、有限ビットの符号範囲やクリッピングは設定していません[3]。

その時刻で新たに発生する局所誤差と、基準状態からの累積誤差を次のように定義します。

\varepsilon_t=\hat{x}_t-z_t,\qquad e_t=\hat{x}_t-x_t

両軌道に同じ $A$ と $u_t$ を適用しているため、$e_0=0$ なら次式が成り立ちます。

e_t=Ae_{t-1}+\varepsilon_t

出力誤差とその二乗は、次式で評価します。

\Delta y_t=Ge_t,\qquad (\Delta y_t)^2=e_t^\top G^\top Ge_t

次のコードを quantization_error.py として保存します。コード中の assert は、数値計算した状態差が誤差の再帰式と一致することを各ステップで確認します。

quantization_error.py
import numpy as np

A = np.diag(np.array([0.99, 0.5], dtype=np.float64))
G = np.array([1.0, 0.1], dtype=np.float64)
U = np.array([0.04, 0.09], dtype=np.float64)
STEPS = 128
DELTA = 0.125


def quantize(value):
    return DELTA * np.round(value / DELTA)


def compare_directions():
    e1 = np.array([0.02, 0.0], dtype=np.float64)
    e2 = np.array([0.0, 0.02], dtype=np.float64)
    assert np.isclose(np.mean(e1**2), np.mean(e2**2))
    print("direction: local_mse, output_error, output_squared_error")
    for name, error in (("axis1", e1), ("axis2", e2)):
        output_error = float(G @ error)
        print(f"  {name}: {np.mean(error**2):.6f}, {output_error:.6f}, {output_error**2:.6f}")


def compare_retention():
    print("retention: steps, factor(0.99), factor(0.5)")
    for t in (10, 50, 100):
        print(f"  {t}: {0.99**t:.9g}, {0.5**t:.9g}")


def simulate(interval):
    exact = np.zeros(2, dtype=np.float64)
    quantized = np.zeros(2, dtype=np.float64)
    previous_error = np.zeros(2, dtype=np.float64)
    local_mses = []

    for t in range(1, STEPS + 1):
        exact = A @ exact + U
        before_quantization = A @ quantized + U
        if t % interval == 0:
            quantized = quantize(before_quantization)
            local_mses.append(float(np.mean((quantized - before_quantization)**2)))
        else:
            quantized = before_quantization

        local_error = quantized - before_quantization
        state_error = quantized - exact
        assert np.allclose(state_error, A @ previous_error + local_error,
                           rtol=0, atol=1e-12)
        previous_error = state_error

    output_error = float(G @ state_error)
    return len(local_mses), float(np.mean(local_mses)), float(np.mean(state_error**2)), output_error, output_error**2


def compare_intervals():
    print("interval: operations, local_mse, final_state_mse, output_error, output_squared_error")
    for interval in (1, 8):
        operations, local_mse, state_mse, error, squared = simulate(interval)
        print(f"  {interval}: {operations}, {local_mse:.9f}, {state_mse:.9f}, {error:.9f}, {squared:.9f}")


if __name__ == "__main__":
    compare_directions()
    compare_retention()
    compare_intervals()

実行します。

python quantization_error.py

等しい局所 MSE でも誤差方向によって出力が変わる

最初の実験では、量子化器による丸めを使わず、方向だけが異なる 2 つの誤差を直接与えます。これにより、誤差の発生条件と出力感度を切り分けます。

\varepsilon^{(1)}=\begin{pmatrix}0.02\\0\end{pmatrix},\qquad
\varepsilon^{(2)}=\begin{pmatrix}0\\0.02\end{pmatrix}

局所 MSE は各成分の二乗誤差の平均です。2 つの誤差は、どちらも $(0.02)^2/2=0.0002$ です。

誤差方向 局所 MSE 直後の出力誤差 出力二乗誤差
第 1 成分 0.000200 0.020000 0.000400
第 2 成分 0.000200 0.002000 0.000004

出力二乗誤差は 100 倍異なります。局所 MSE は誤差ベクトルの大きさを測りますが、出力二乗誤差は $G^\top G$ によって方向別に重み付けした値です。量子化方式の比較では、後続の読み出しに対してどの方向へ誤差が現れるかも評価項目になります。

状態保持率によって量子化誤差の寿命が変わる

ある時刻に誤差を 1 回だけ与え、その後の量子化誤差をゼロとすると、$k$ ステップ後の誤差は $A^k\varepsilon$ です。保持率が $a$ の成分は、$k$ ステップ後に $a^k$ 倍となります。

経過ステップ 保持率 0.99 保持率 0.5
10 0.904382075 0.0009765625
50 0.605006067 約 8.88 × 10⁻¹⁶
100 0.366032341 約 7.89 × 10⁻³¹

同じ時刻に同じ大きさの誤差が生じても、保持率 0.99 の成分には 100 ステップ後も約 36.6% が残ります。保持率 0.5 の成分は早期に減衰します。複数の時刻で誤差が発生する場合には、これらの残留分が時刻ごとに加算されます。

量子化間隔を変えて累積誤差を比較する

次は実際に np.round を用い、毎ステップ量子化する場合と、8 ステップごとに量子化する場合を比較します。量子化しない時刻は float64 の状態更新を続けます。

128 ステップを計算した結果は次のとおりです。

interval: operations, local_mse, final_state_mse, output_error, output_squared_error
  1: 128, 0.001179956, 4.192005688, -2.900493329, 8.412861553
  8: 16, 0.002029791, 0.001712367, -0.025493329, 0.000649910
量子化間隔 量子化回数 実行時の平均局所 MSE 最終状態 MSE 最終出力二乗誤差
1 128 0.001179956 4.192005688 8.412861553
8 16 0.002029791 0.001712367 0.000649910

この条件では、8 ステップごとの量子化は 1 回当たりの平均局所 MSE が大きい一方で、最終出力二乗誤差は小さくなりました。毎ステップ量子化では、第 1 成分の初期更新量 0.04 が刻み幅 0.125 の半分 0.0625 より小さく、毎回 0 に丸められます。そのため第 1 成分の更新が量子化状態に蓄積されず、基準状態との差が広がります。8 ステップ間隔では量子化前の更新値が蓄積してから丸められます。

平均局所 MSE は量子化した時刻だけで計算しています。量子化を省略した時刻のゼロ誤差を平均へ含めると、操作 1 回当たりの局所精度と実行回数の効果を混同するためです。

局所 MSE と出力誤差を使い分ける

量子化の評価では、目的に応じて指標を分けます。

測定対象 計算式 判断できること
局所 MSE $\operatorname{mean}(\varepsilon_t^2)$ 量子化 1 回の再構成誤差
最終状態 MSE $\operatorname{mean}(e_T^2)$ 更新を経て残った状態差
最終出力二乗誤差 $(Ge_T)^2$ 指定した出力読み出しへの影響
量子化回数 $N_{\mathrm{quant}}$ 丸め操作を行った時刻の個数

今回の例では、局所 MSE の小さい方式が最終出力二乗誤差でも優位になるという順位関係は成立しませんでした。実務では、保持する状態を局所 MSE で検査し、さらに実際の状態更新と出力読み出しを経た誤差を計測することで、異なる評価目的を区別できます。

再帰状態モデルの検証結果を適用できる範囲

この実験は固定の線形状態遷移行列と 2 次元状態を使った検証です。実際の再帰型 LLM では、状態が行列となり、更新や読み出しも入力に依存します。STEPQuant は誤差の大きさと残存時間に基づいてビット幅を配分し、キー行の出力への影響と行・列方向の状態分布を使って量子化尺度を調整しています[4]。LeapQuant は窓単位で量子化し、高精度の更新を保持する構成によって誤差蓄積を抑えています[5]。

本稿の 8 ステップ間隔の計算は、窓単位の量子化で誤差発生頻度が変わる効果を取り出したものです。高精度更新のバッファー管理、有限ビットの格納形式、メモリ転送量、推論速度はモデル化していません。この数値例から実 LLM の精度改善率や実行性能を推定することはできません。

状態遷移行列、量子化刻み幅、入力、読み出し行列が変わると、誤差の減衰と重ね合わせも変わります。量子化を比較するときは、局所 MSE と量子化回数に加え、対象モデルの更新系列で最終状態誤差と最終出力誤差を測定することが判断基準になります。

参考文献

  1. id774, 低ビット量子化では誤差の発生時刻と寿命と方向へ精度を配る(2026-10-24). https://blog.id774.net/entry/2026/10/24/5746/
  2. NumPy Developers, numpy (2.3.4)(2025-10-15). https://pypi.org/project/numpy/2.3.4/
  3. NumPy Developers, numpy.round. https://numpy.org/doc/stable/reference/generated/numpy.round.html
  4. Bingchen Yao et al., STEPQuant: When and Where Errors Matter in Delta-Rule Recurrent State Quantization(2026-09-29). https://arxiv.org/abs/2609.38169v1
  5. Yi Pan et al., LeapQuant: Efficient Linear Attention with Accurate Recurrent State Quantization(2026-09-29). https://arxiv.org/abs/2609.38166v1
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?