概要
プルーニング(Pruning:枝刈り)とは、ニューラルネットワークモデルから重要度の低いパラメータ(重み)や構造を削除することで、モデルを小型化・高速化する技術です。樹木の不要な枝を刈り取るように、ネットワークの冗長な部分を取り除いてもモデルの性能(精度)をほぼ維持できるという知見に基づいています。
プルーニングで得られる主な効果は以下のとおりです。
| 効果 | 典型的な数値 |
|---|---|
| モデルパラメータ削減 | 50〜90%削減 |
| モデルファイルサイズ削減 | 圧縮と組み合わせで50〜80%削減 |
| 推論速度向上 | 構造化プルーニングで2〜5倍 |
| メモリ使用量削減 | スパース表現で最大数倍 |
| 精度劣化 | 通常1〜3%(手法による) |
プルーニングは量子化と並ぶモデル圧縮の主要技術であり、組み込みデバイスやマイコン(MCU)への展開において特に重要です。
歴史・背景
プルーニングの概念は機械学習の黎明期から存在しました。1989年にYann LeCunが「Optimal Brain Damage(最適脳損傷)」という論文でニューラルネットワークの不要な重みを除去する手法を提案したのが最初期の研究の一つです。
主要な研究マイルストーン:
| 年 | 研究・出来事 |
|---|---|
| 1989年 | Yann LeCun「Optimal Brain Damage」—重みの重要度に基づくプルーニング |
| 1992年 | Hassibi & Stork「Optimal Brain Surgeon」—二階微分を使った精密な枝刈り |
| 2015年 | Han et al.「Deep Compression」—プルーニング+量子化+ハフマン符号化でAlexNetを35倍圧縮 |
| 2018年 | Frankle & Carlin「The Lottery Ticket Hypothesis」—小さなサブネットワークが元の精度を維持できるという理論 |
| 2019年 | TensorFlow Model Optimization ToolkitにSparsityプルーニングAPI追加 |
| 2021年 | Structured Pruningのため Torch.nn.utils.pruningが成熟 |
| 2022年 | SparseGPT—LLM向け大規模プルーニング手法の登場 |
The Lottery Ticket Hypothesis(宝くじ仮説):
2018年にMITのFrankleとCarlinが提唱した重要な理論で、「ランダム初期化された大きなネットワークには、小さくても元の精度を維持できる『当たりくじ』サブネットワークが存在する」という仮説です。これによりプルーニングの理論的裏付けが深まりました。
技術仕様
プルーニングの分類体系
プルーニング
├── 非構造化プルーニング(Unstructured Pruning)
│ ├── 重みプルーニング(Weight Pruning)
│ │ ├── 大きさベース(Magnitude-based)
│ │ ├── 勾配ベース(Gradient-based)
│ │ └── 重要度ベース(Saliency-based)
│ └── ニューロンプルーニング(個別ニューロン削除)
│
└── 構造化プルーニング(Structured Pruning)
├── フィルタープルーニング(Conv2Dのフィルタ削除)
├── チャンネルプルーニング(チャンネル次元の削除)
├── レイヤープルーニング(レイヤーごと削除)
└── アテンションヘッドプルーニング(Transformerの場合)
非構造化 vs 構造化プルーニングの比較:
| 比較軸 | 非構造化 | 構造化 |
|---|---|---|
| 削減率 | 高い(90%以上も可能) | 中程度(50〜70%) |
| 精度維持 | 高い | 若干低い |
| ハードウェア加速 | 困難(スパース演算が必要) | 容易(通常のDense演算) |
| 実装の複雑さ | 低 | 中〜高 |
| 汎用性 | 高 | ハードウェア依存 |
スパース性(Sparsity)の定義
import numpy as np
def calculate_sparsity(weights):
"""重み行列のスパース性を計算"""
total = weights.size
zeros = np.count_nonzero(weights == 0)
sparsity = zeros / total
print(f"総パラメータ数: {total:,}")
print(f"ゼロパラメータ数: {zeros:,}")
print(f"スパース性: {sparsity * 100:.1f}%")
return sparsity
# 例: 50%スパースな重み行列
weights = np.random.randn(512, 512)
weights[np.abs(weights) < 0.5] = 0 # 閾値未満をゼロに
sparsity = calculate_sparsity(weights)
動作原理
大きさベースプルーニング(Magnitude Pruning)
最もシンプルで一般的な手法で、絶対値の小さい重みを重要度が低いとみなしてゼロに設定します。
import torch
import torch.nn.utils.prune as prune
model = MyModel()
model.load_state_dict(torch.load('model.pth'))
# 特定レイヤーの重みを30%プルーニング
prune.l1_unstructured(
model.conv1,
name='weight',
amount=0.3 # 30%をゼロに設定
)
# 全Conv2Dレイヤーを40%プルーニング
parameters_to_prune = [
(module, 'weight')
for module in model.modules()
if isinstance(module, torch.nn.Conv2d)
]
prune.global_unstructured(
parameters_to_prune,
pruning_method=prune.L1Unstructured,
amount=0.4,
)
# プルーニングを確定(マスクを適用して永続化)
for module, name in parameters_to_prune:
prune.remove(module, name)
# スパース性確認
total_params = 0
zero_params = 0
for name, param in model.named_parameters():
total_params += param.numel()
zero_params += (param == 0).sum().item()
print(f"グローバルスパース性: {zero_params/total_params*100:.1f}%")
構造化プルーニング(チャンネルプルーニング)
チャンネル全体を削除することで、特別なスパース演算なしに速度向上が得られます。
import torch
import torch.nn as nn
import numpy as np
def channel_pruning(model, prune_ratio=0.3):
"""L1ノルムの小さいチャンネルを削除"""
new_model_cfg = []
for name, module in model.named_modules():
if isinstance(module, nn.Conv2d):
# 各出力チャンネルのL1ノルムを計算
weight = module.weight.data # [out_ch, in_ch, kH, kW]
l1_norms = weight.abs().sum(dim=(1, 2, 3))
# プルーニングするチャンネル数
n_prune = int(module.out_channels * prune_ratio)
# 重要度の低いチャンネルを特定
_, sorted_idx = torch.sort(l1_norms)
prune_idx = sorted_idx[:n_prune]
keep_idx = sorted_idx[n_prune:]
print(f"{name}: {module.out_channels}ch → "
f"{len(keep_idx)}ch "
f"({n_prune}ch削除)")
new_model_cfg.append({
'layer': name,
'keep_channels': keep_idx.tolist()
})
return new_model_cfg
TensorFlow Model Optimization Toolkitを使ったプルーニング
import tensorflow as tf
import tensorflow_model_optimization as tfmot
# ベースモデルの準備
base_model = tf.keras.applications.MobileNetV2(
input_shape=(224, 224, 3),
weights='imagenet',
include_top=True
)
# プルーニングスケジュール(学習に合わせてスパース性を増加)
pruning_schedule = tfmot.sparsity.keras.PolynomialDecay(
initial_sparsity=0.0,
final_sparsity=0.5, # 最終的に50%スパース
begin_step=2000,
end_step=10000,
frequency=100
)
# プルーニングラッパーの適用
model_for_pruning = tfmot.sparsity.keras.prune_low_magnitude(
base_model,
pruning_schedule=pruning_schedule
)
# ファインチューニング
callbacks = [tfmot.sparsity.keras.UpdatePruningStep()]
model_for_pruning.compile(
optimizer='adam',
loss='categorical_crossentropy',
metrics=['accuracy']
)
model_for_pruning.fit(
train_dataset,
epochs=5,
callbacks=callbacks
)
# プルーニング後のモデルを保存用に変換
final_model = tfmot.sparsity.keras.strip_pruning(model_for_pruning)
final_model.save('pruned_model')
# TFLiteに変換(量子化も同時適用)
converter = tf.lite.TFLiteConverter.from_saved_model('pruned_model')
converter.optimizations = [tf.lite.Optimize.DEFAULT]
tflite_pruned_quantized = converter.convert()
反復的プルーニング(Iterative Pruning)
一度に大きくプルーニングするよりも、段階的に行う方が精度維持に有利です。
反復的プルーニングサイクル:
1. 元モデルを学習
2. スパース性10%でプルーニング
3. ファインチューニング(数エポック)
4. スパース性20%でプルーニング
5. ファインチューニング
6. ...繰り返し...
7. スパース性80%まで達成
8. 最終ファインチューニング
用途・ユースケース
組み込み・エッジデバイスへの展開
モデルをマイコン(MCU)に展開する際、フラッシュメモリ容量とRAMが制約となります。プルーニングで50%以上のモデル削減を達成した後、量子化と組み合わせることで大幅なサイズ削減が可能です。
元モデル(ResNet-18相当):
パラメータ数: 1100万
FP32モデルサイズ: 44MB
→ MCU展開不可能
プルーニング(80%スパース)+ 量子化(INT8)後:
残パラメータ数: 220万
モデルサイズ: 2.2MB(20倍圧縮)
→ STM32H7(Flash 2MB)に展開可能
モデルアーキテクチャの探索
プルーニングを系統的に行うことで、タスクに最適な軽量アーキテクチャを発見できます。これはNeural Architecture Search(NAS)と密接に関連します。
クラウドモデルの高速化
エッジデバイスだけでなく、クラウドサーバーでもスループット向上のためにプルーニングが使われます。NVIDIAのAmpere GPU以降は構造化スパース(2:4スパース:4要素中2要素がゼロ)に対応したハードウェアアクセラレーションが利用できます。
実装・開発のポイント
プルーニングと量子化の組み合わせ
プルーニングと量子化を組み合わせることで相乗効果が得られます。
プルーニング(スパース化)+ 量子化(INT8)+ 圧縮(zip/zstd)
= 最大20〜50倍のモデル圧縮が可能
推奨ワークフロー:
1. まずQATで量子化認識学習
2. 量子化した状態でプルーニング実施
3. 再度ファインチューニング
4. モデルを圧縮(スパース重みはゼロが多いため高圧縮率)
精度とスパース性のトレードオフ確認
def sweep_sparsity(model, test_loader, sparsity_levels):
"""様々なスパース性で精度を評価"""
results = {}
for sparsity in sparsity_levels:
# プルーニング適用
pruned_model = apply_global_pruning(model, sparsity)
# 精度評価
accuracy = evaluate(pruned_model, test_loader)
model_size = get_model_size(pruned_model)
results[sparsity] = {
'accuracy': accuracy,
'size_mb': model_size
}
print(f"Sparsity: {sparsity*100:.0f}%, "
f"Accuracy: {accuracy*100:.2f}%, "
f"Size: {model_size:.2f}MB")
return results
sparsity_levels = [0.0, 0.1, 0.2, 0.3, 0.5, 0.7, 0.8, 0.9]
results = sweep_sparsity(model, test_loader, sparsity_levels)
ハードウェア対応を考慮したプルーニング
プルーニングの効果を実際の推論速度改善に結びつけるには、ハードウェアの特性に合わせた粒度のプルーニングが重要です。
| ハードウェア | 推奨プルーニング方式 | 理由 |
|---|---|---|
| 汎用CPU | 非構造化(スパース行列ライブラリ使用) | AVX-512対応 |
| ARM MCU | 構造化(チャンネル削除) | スパース演算非対応 |
| NVIDIA GPU | 2:4構造化スパース | Ampere以降HW対応 |
| NPU | 構造化(チャンネル単位) | NPUはDenseのみ対応 |
他技術との比較
| 比較軸 | プルーニング | 量子化 | 知識蒸留 | NAS |
|---|---|---|---|---|
| 主な効果 | パラメータ削減 | ビット幅削減 | モデル小型化 | 最適構造探索 |
| 再学習必要性 | あり | 不要(PTQ) | あり | あり |
| 実装難易度 | 中 | 低〜中 | 高 | 最高 |
| 専用ハード必要 | 場合による | NPUで最大効果 | 不要 | 不要 |
| 組み合わせやすさ | ○量子化と相性良 | ○プルーニングと相性良 | △ | △ |
プルーニングは量子化と組み合わせることで、TinyMLの実現に不可欠なモデル圧縮を達成します。TensorFlow LiteやONNXに変換する前の前処理として、プルーニングを行うことでマイコンへの展開可能性が大きく向上します。