はじめに
ChatGPT のようなモデルは数百億〜数兆個のパラメータを持っていて、動かすには大きな GPU が必要です。
このパラメータ、実は かなり「水増し」されている のをご存知でしょうか?
「水増し」と言うとネガティブですが、より正確には:
同じ AI が、何通りもの形で記録されている
ということです。例えば、3 層のシンプルなニューラルネットでも、まったく同じ予測をする「双子」のモデル が、ざっと $3.8 \times 10^{215}$ 個 くらいあります。
(参考: 宇宙にある原子の数が $10^{80}$ 個)
この「双子だらけ」の状態を整理すると、精度を 1 ミリも落とさずに モデルを 30〜50% 軽くできます。PyTorch で 2〜3 行追加するだけで。
本記事では:
- なぜ同じ AI に双子が何通りもあるのか
- どうやって整理するのか
- どれくらい軽くなるか (実測値)
を、コード付きで説明します。
この記事のスタンス: pruning や量子化、LoRA とは競合しません。学習済みモデルの 前処理 として、組み合わせて使える地味だけど効く手法です。
1. なぜ「双子のモデル」がたくさんあるのか
1.1 ニューロンの順番は本質ではない
ニューラルネットの隠れ層は、たくさんのニューロン (= 数値の入った箱) が並んだものです。
例えば隠れ層に 3 個のニューロン A, B, C があるとします:
入力 → [A, B, C] → 出力
ここで、ニューロンの 並び順を入れ替え てみましょう:
入力 → [C, A, B] → 出力
並びを変えただけで、各ニューロンが「次の層のどこにつながるか」を 対応して付け替えれば、出力は完全に同じ になります。
これは当たり前といえば当たり前です。
「同じ料理でも、材料を冷蔵庫のどの棚に入れるかは自由」みたいなものです。
問題は、その「並び替え方」が 天文学的にたくさんある こと。
隠れ層に 128 個のニューロンがあれば、並び替え方は
128! = 128 \times 127 \times 126 \times \cdots \times 1 \approx 3.8 \times 10^{215} \text{ 通り}
もあります。
しかも、これは「単純な並び替え」だけの話です。
実際にはもっと一般的な「混ぜ合わせ」も許されます (詳細は後述)。
これらは全て 同じ AI なのに、重みの数値としては全部違う。
ということは、この自由度の分だけ重みデータには無駄がある わけです。
1.2 PyTorch で「双子」を確かめる
実際に「並び替えても同じ」を確認してみましょう。
import torch
# 学習済みモデルの重み (ここではランダム値で代用)
W1 = torch.randn(128, 64) # 入力 64 → 隠れ 128
W2 = torch.randn(10, 128) # 隠れ 128 → 出力 10
x = torch.randn(64)
# 隠れ層 128 個のニューロンを並び替える
P = torch.eye(128)[torch.randperm(128)] # 並び替えの行列
# 並び替えた「双子」の重み
W1_twin = P @ W1
W2_twin = W2 @ P.T
# 元と双子で、出力が同じか確認
y_orig = W2 @ (W1 @ x)
y_twin = W2_twin @ (W1_twin @ x)
print((y_orig - y_twin).abs().max().item())
# → 約 1e-14 (浮動小数点の誤差レベル、実質ゼロ)
出力差はほぼゼロ (浮動小数点の丸め誤差レベル)。同じ AI が、別の重みの形で表現できている ことが確認できました。
2. どうやって「整理」するのか
「双子だらけ」の状態を整理するというのは、たくさんの双子の中から「代表」を 1 人選ぶ ことに相当します。
例えば「身長順に並んでいる」状態を「代表」と決めれば、他の並び方は全部「代表と同じグループだよ」と言えますよね。
ニューラルネットでも同じことをします。重みの行列を「ある決まった形」に揃える わけです。
2.1 「決まった形」= 上三角形
その「決まった形」として便利なのが、上三角形 です。
元の行列 上三角化した行列
┌──────────┐ ┌──────────┐
│■ ■ ■ ■ ■│ │■ ■ ■ ■ ■│
│■ ■ ■ ■ ■│ │ ■ ■ ■ ■│
│■ ■ ■ ■ ■│ → │ ■ ■ ■│
│■ ■ ■ ■ ■│ │ ■ ■│
│■ ■ ■ ■ ■│ │ ■│
└──────────┘ └──────────┘
全部に値がある 下半分は全部 0
数値が並んだ正方形の表 (= 行列) を、右上半分にだけ数値が入っていて、左下半分は全部 0 という形に揃えます。
下半分が 0 だけ なら、その部分は保存しなくていいですよね?
これが圧縮の正体です。
2.2 上三角形にする道具 = QR 分解
行列を上三角形に変形するのに使うのが QR 分解 という標準的な操作です。
名前は厳めしいですが、要するに、
どんな行列 $W$ も、直交行列 $Q$ と 上三角行列 $R$ の掛け算に分解できる
つまり $W = QR$
という線形代数の事実があります。
直交行列というのは「向きを変えるだけで大きさは変えない行列」のことで、回転や鏡映だと思ってください。
PyTorch では 1 行で計算できます:
Q, R = torch.linalg.qr(W1, mode='complete')
# W1 = Q @ R が成り立つ
# Q: 直交行列 (回転に相当)
# R: 上三角行列 (これが欲しい)
NumPy でも同じ書き方です。中身は線形代数の標準アルゴリズムで、計算コストも軽い。
2.3 隣の層との辻褄を合わせる
ただし、片方の重み $W_1$ を勝手に変形すると、次の層の重み $W_2$ との辻褄が合わなくなります。出力が変わってしまうからです。
そこで、変形した分を $W_2$ 側で打ち消し ます。
直交行列 $Q$ の逆行列は単に転置するだけ ($Q^{-1} = Q^T$) なので、これも軽い計算です。
具体的には:
- $W_1$ を $R$ に置き換える (上三角化)
- $W_2$ を $W_2 Q$ に置き換える (辻褄合わせ)
すると、
W_2' \cdot W_1' \cdot x = (W_2 Q) \cdot R \cdot x = W_2 (QR) x = W_2 W_1 x
となって、出力 $y$ は完全に元と同じ になります。
2.4 全部まとめると 2 行
これを関数にまとめると:
def gauge_fix(W1, W2):
"""
関数 W2 @ W1 を変えずに、W1 を上三角形に変形する。
"""
Q, R = torch.linalg.qr(W1, mode='complete')
return R, W2 @ Q
たったこれだけ。$W_1$ の下三角部分はぴったり 0 になるので、その分はディスクに保存しなくて済みます。
補足: なぜ「gauge_fix」という名前なのか
関数名にした「gauge (ゲージ)」は、もともと物理学の用語で、「同じ現象を表す複数の表現のうち、どれを採用するかの『目盛りの取り方』」 を指します。
例えば地図を作るとき、「北を上にするか東を上にするか」「縮尺をいくつにするか」は地形そのものとは関係ない、見やすさのための取り決めですよね。これが gauge です。
「gauge fix (ゲージ固定)」は、その目盛りの取り方を 1 つに決め打ちする こと。本記事でやっているのは:
- 同じ AI を表す重みは無数にある (= 目盛りの取り方が無数にある)
- そのうち「$W_1$ が上三角形」という 1 つに決め打ちする (= gauge を固定する)
物理学では Maxwell 方程式や量子場の理論で似たことをやっていて、そこから機械学習や数学にも輸入された言い回しです。Git Re-Basin など最近のモデルマージ研究でもこの用語が使われています。
3. 実際にどれくらい軽くなるか
実測してみましょう。
import torch
torch.manual_seed(0)
W1 = torch.randn(128, 128)
W2 = torch.randn(64, 128)
x = torch.randn(128)
# 元の出力
y_orig = W2 @ (W1 @ x)
# gauge_fix を適用
W1_new, W2_new = gauge_fix(W1, W2)
y_new = W2_new @ (W1_new @ x)
# 出力差
print("出力誤差:", (y_orig - y_new).abs().max().item())
# → 約 1e-13 (実質ゼロ)
# W1_new の下三角部分はぴったり 0?
print("下三角の最大値:", W1_new.tril(diagonal=-1).abs().max().item())
# → 0.0
# W1_new の中で 0 になった要素数
n_zero = (W1_new.abs() < 1e-10).sum().item()
print(f"0 になった要素: {n_zero} / {W1_new.numel()}")
# → 8128 / 16384 (約 49.6%)
つまり $W_1$ のうち 約半分 がゼロになりました。
$W_2$ も含めた合計で見ると、保存サイズの 約 33% が削れます。
3.1 層の形状ごとの実測値
色々な層構成で試した結果 (NumPy, seed=42):
| 層の形状 ($H$=隠れ, $I$=入力, $O$=出力) | 元のパラメータ数 | 削減後 | 削減率 |
|---|---|---|---|
| $H=64, I=32, O=10$ (小型 MLP) | 2,688 | 1,168 | 56.55% |
| $H=128, I=128, O=64$ (普通の MLP) | 24,576 | 16,448 | 33.07% |
| $H=32, I=64, O=10$ (隠れ層が細い) | 2,368 | 1,872 | 20.95% |
| $H=512, I=128, O=64$ (隠れ層が太い) | 73,728 | 30,752 | 58.27% |
| $H=2048, I=512, O=512$ (Transformer FFN 相当) | 2,097,152 | 1,179,904 | 43.74% |
傾向: 隠れ層が太いほど削減率が大きくなります。Transformer の Feed-Forward 層 (典型的に $d_\text{ff} = 4 \times d_\text{model}$) や、ボトルネック構造の上り側で特に効きます。
検証コード全体:
import numpy as np
def gauge_fix_np(W1, W2):
Q, R = np.linalg.qr(W1, mode='complete')
return R, W2 @ Q
np.random.seed(42)
for H, I, O in [(64,32,10), (128,128,64), (32,64,10), (512,128,64), (2048,512,512)]:
W1 = np.random.randn(H, I)
W2 = np.random.randn(O, H)
W1c, W2c = gauge_fix_np(W1, W2)
n_zero = int(np.sum(np.abs(W1c) < 1e-10))
reduction = 100 * n_zero / (W1.size + W2.size)
print(f"H={H}, I={I}, O={O} → {reduction:.2f}% 削減")
4. 落とし穴: ReLU が間にあると使えない
ここまでの話には 大事な前提 があります。それは「$W_1$ と $W_2$ が直接掛け算されている」こと。
実際のニューラルネットでは、$W_1$ と $W_2$ の間に活性化関数 (ReLU, GELU, SiLU など) が挟まりますよね:
y = W2 @ ReLU(W1 @ x) # ReLU が挟まる
この場合、上で説明した素朴な方法は 使えません。
4.1 なぜ使えないか
ReLU は「負の値を 0 にする」関数です。
重み $W_1$ を変形すると、どの値が負になるか自体が変わってしまう ので、ReLU の挙動が変わって関数等価性が壊れます。
数式で書くと:
\text{ReLU}(QR \cdot x) \neq Q \cdot \text{ReLU}(R \cdot x)
(一般には等しくない)
4.2 ReLU を超える方法: 並び替えとスケーリングだけ許す
ReLU を通っても等価性が保たれる「変形」には、強い制限 があります。
具体的には:
- ニューロンの並び替え (順列): OK
- 各ニューロンを正の数で拡大・縮小: OK
- それ以外の混ぜ合わせ: NG
この制限の下でも、それなりに有用な前処理ができます:
def gauge_fix_relu_compatible(W1, W2):
"""
ReLU 互換の整理:
- 隠れ層のニューロンを W1 の各行ノルム降順にソート
- 各行ノルムを 1 に正規化 (スケールを W2 に押し付ける)
"""
norms = W1.norm(dim=1, keepdim=True)
perm = norms.squeeze().argsort(descending=True)
W1_n = W1[perm] / norms[perm] # 各行ノルム = 1 に正規化
W2_n = W2[:, perm] * norms[perm].squeeze() # スケールを W2 に移送
return W1_n, W2_n
これは パラメータ数は減らない けど、他の手法と組み合わせる前処理 として効きます:
| 用途 | 効果 |
|---|---|
| モデル平均 (アンサンブル前処理) | 異なる初期値で学習した 2 つのモデルの「ニューロン順」が揃う → 重みの平均が機能する |
| 量子化 | 各行ノルムが 1 に揃う → 量子化の桁落ちが均等になる |
| 蒸留 | 教師と生徒のニューロン対応がつきやすくなる |
特に モデル平均 は、Git Re-Basin (ICLR 2023) という論文で精力的に研究されているテーマで、本記事の前処理と非常に相性が良いです。
参考: Ainsworth et al., Git Re-Basin (ICLR 2023)
5. 既存手法とのすみわけ
「重み圧縮」の手法は色々あります。本記事の gauge_fix がどう違うか、表にまとめます:
| 手法 | 何をするか | 関数の出力 | gauge_fix との関係 |
|---|---|---|---|
| gauge_fix (本記事) | 上三角化で冗長性を除去 | 完全不変 | — |
| Pruning | 値の小さい重みを 0 にする | 多少変わる | gauge_fix → pruning の順で使うと相性◎ |
| 量子化 | float → int8/int4 | 多少変わる | gauge fix の正規化版で桁落ちを均等化 |
| SVD 低ランク近似 | rank を落として近似 | 変わる | 別目的 (近似 vs 完全等価) |
| LoRA | 低ランク差分で fine-tune | 学習で変わる | 学習前の前処理として gauge fix が使える |
| Git Re-Basin | 並び替えてモデル平均 | — | gauge_fix_relu_compatible が直接使える前処理 |
重要: gauge_fix は 既存手法を置き換えるもの ではなく、前処理として組み合わせて使う ものです。
6. 注意点とハマりどころ
学習中に gauge fix を適用するのはやめましょう: 関数は不変ですが、SGD などの最適化軌道は変わります (重み空間上での勾配の方向が変わるため)。学習済みモデルの後処理として適用するのが安全 です。
Conv 層には別の工夫が必要: Conv 層は空間方向に重みを共有しているので、本記事の方法をそのまま適用するとアウトです。チャンネル次元についてのみ適用するなど、構造に合わせた拡張が必要です。
バイアス項の扱い: y = W2 (W1 @ x + b1) + b2 のようにバイアスがある場合、$b_1$ にも $Q^T b_1$ の補正を入れる必要があります。実装するときは忘れずに。
7. まとめ: 明日からできること
- 学習済みモデルから、線形層が 2 つ続いている箇所 を見つける (Transformer の FFN, MLP head, 全結合層など)
- 間に活性化関数が ない 区間 → gauge_fix($W1, W2$) でそのまま圧縮
- 間に ReLU/GELU が ある 区間 → gauge_fix_relu_compatible($W1, W2$) で並び替え + 正規化
- 結果として:
- 線形チェーン部分: 20〜58% パラメータが消える (層の形状による)
- ReLU 入りの場合: パラメータ数は変わらないが、量子化・モデル平均・蒸留の精度が上がる
QR 分解 1 行から始まる、地味だけど確実に効く前処理です。
おまけ: 数学的背景に興味がある方へ
ここまでで実装に必要な内容は全部終わりです。以下は興味のある方向け。
「なぜ重みに冗長性があって、それを QR 分解で除去できるのか」は、quiver 表現論 という数学の枠組みで自然に説明できます。
- ニューラルネットの線形層チェーンは、数学的には「Aₙ 型 quiver の表現」と呼ばれる構造そのもの
- Gabriel の定理 (1972) によれば、この構造には標準形が存在し、有限個の部品の組み合わせに一意分解できる
- 本記事の
gauge_fixは、その標準形への変換を Aₙ 型 (= 線形チェーン) の場合に書き下したもの
理論的な詳細・他のアーキテクチャ (Transformer の attention 部分など) への拡張・厳密な証明は、以下の関連記事をご参照ください:
- 数学的詳細: Zenn 記事
- フル版 PDF (理論+実装): GitHub リポジトリ
参考文献
- Ainsworth, S., Hayase, J., Srinivasa, S. (2022). Git Re-Basin: Merging Models modulo Permutation Symmetries. ICLR 2023.
- Entezari, R. et al. (2022). The Role of Permutation Invariance in Linear Mode Connectivity of Neural Networks. ICLR 2022.
- Ganev, I., Walters, R. (2022). The QR decomposition for radial neural networks.
- Armenta, M., Jodoin, P.-M. (2021). The Representation Theory of Neural Networks. Mathematics, 9(24), 3216.
質問・指摘・「うちのモデルで試したら○%減った」報告など、コメント大歓迎です 🙋