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?

学習済みモデルの重み、実は半分は「双子」です — PyTorchで2行加えるだけで30%軽くなる前処理

0
Last updated at Posted at 2026-04-25

はじめに

ChatGPT のようなモデルは数百億〜数兆個のパラメータを持っていて、動かすには大きな GPU が必要です。

このパラメータ、実は かなり「水増し」されている のをご存知でしょうか?

「水増し」と言うとネガティブですが、より正確には:

同じ AI が、何通りもの形で記録されている

ということです。例えば、3 層のシンプルなニューラルネットでも、まったく同じ予測をする「双子」のモデル が、ざっと $3.8 \times 10^{215}$ 個 くらいあります。
(参考: 宇宙にある原子の数が $10^{80}$ 個)

この「双子だらけ」の状態を整理すると、精度を 1 ミリも落とさずに モデルを 30〜50% 軽くできます。PyTorch で 2〜3 行追加するだけで。

本記事では:

  1. なぜ同じ AI に双子が何通りもあるのか
  2. どうやって整理するのか
  3. どれくらい軽くなるか (実測値)

を、コード付きで説明します。

この記事のスタンス: 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. まとめ: 明日からできること

  1. 学習済みモデルから、線形層が 2 つ続いている箇所 を見つける (Transformer の FFN, MLP head, 全結合層など)
  2. 間に活性化関数が ない 区間 → gauge_fix($W1, W2$) でそのまま圧縮
  3. 間に ReLU/GELU が ある 区間 → gauge_fix_relu_compatible($W1, W2$) で並び替え + 正規化
  4. 結果として:
    • 線形チェーン部分: 20〜58% パラメータが消える (層の形状による)
    • ReLU 入りの場合: パラメータ数は変わらないが、量子化・モデル平均・蒸留の精度が上がる

QR 分解 1 行から始まる、地味だけど確実に効く前処理です。


おまけ: 数学的背景に興味がある方へ

ここまでで実装に必要な内容は全部終わりです。以下は興味のある方向け。

「なぜ重みに冗長性があって、それを QR 分解で除去できるのか」は、quiver 表現論 という数学の枠組みで自然に説明できます。

  • ニューラルネットの線形層チェーンは、数学的には「Aₙ 型 quiver の表現」と呼ばれる構造そのもの
  • Gabriel の定理 (1972) によれば、この構造には標準形が存在し、有限個の部品の組み合わせに一意分解できる
  • 本記事の gauge_fix は、その標準形への変換を Aₙ 型 (= 線形チェーン) の場合に書き下したもの

理論的な詳細・他のアーキテクチャ (Transformer の attention 部分など) への拡張・厳密な証明は、以下の関連記事をご参照ください:


参考文献


質問・指摘・「うちのモデルで試したら○%減った」報告など、コメント大歓迎です 🙋

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?