エッジAI

プルーニング

不要な部分を削りモデルを小型化する技術。

概要

プルーニング(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 GPU2:4構造化スパースAmpere以降HW対応
NPU構造化(チャンネル単位)NPUはDenseのみ対応

他技術との比較

比較軸プルーニング量子化知識蒸留NAS
主な効果パラメータ削減ビット幅削減モデル小型化最適構造探索
再学習必要性あり不要(PTQ)ありあり
実装難易度低〜中最高
専用ハード必要場合によるNPUで最大効果不要不要
組み合わせやすさ○量子化と相性良○プルーニングと相性良

プルーニングは量子化と組み合わせることで、TinyMLの実現に不可欠なモデル圧縮を達成します。TensorFlow LiteONNXに変換する前の前処理として、プルーニングを行うことでマイコンへの展開可能性が大きく向上します。

関連用語

参考リンク