ニューラル ネットワークの枝刈り: モデルの複雑さを軽減する
ResNet-50 モデルには 2,500 万を超えるパラメーターがあります。 GPT-3には1750億あります。まだ研究中 系統的には、これらのパラメーターのほとんどが冗長であることが実証されています: 訓練されたニューラル ネットワーク 彼らは以上のものを失う可能性があります 重量の90% 重大な劣化なし 精度のこと。の 剪定 ― 余分なパラメータを計画的に削除する手法 ― 深層学習モデルの計算の複雑さを軽減するための最も強力なツールの 1 つです。
パラメータの数値精度を下げる量子化とは異なり、枝刈りは 完全に排除する。その結果、より小型、高速、より安価なモデルが可能になります 実行コストがかかる — 特に、 構造化された剪定、それ ニューロン全体、フィルター、またはアテンションヘッドを削除し、ハードウェア上で実際のスピードアップを実現します。 スパルシタのサポートをリクエストします。
このガイドでは、理論から枝刈りについて詳しく説明します。 宝くじ仮説 大きさによる枝刈りから動きによる枝刈りまで、PyTorch を使用した実際の実装まで。 トランスフォーマー、反復ワークフローおよび量子化との組み合わせまで。
何を学ぶか
- 構造化プルーニングと非構造化プルーニングの違い、およびそれぞれをいつ使用するか
- 規模の剪定: 最もシンプルで効果的な方法
- 最新のトランスフォーマーと LLM の動きの枝刈り
- 宝くじ仮説: 剪定が機能する理由を説明する理論
- PyTorch プルーニング API と完全な例
- 再トレーニングを伴う反復的な枝刈りワークフロー
- 高度な構造化プルーニングのためのトーチプルーニング
- プルーニングと量子化の組み合わせにより最大の圧縮率を実現
- 精度、メモリ、速度の現実世界のベンチマーク
- ベスト プラクティスと一般的なアンチパターン
なぜ剪定するのか?冗長性の問題
最新のニューラル ネットワークはパラメータが過剰であることで悪名高いです。この冗長性と部分的には 意図的: ネットワークが大きいほどトレーニングが容易になり、一般化が容易になりますが、現時点では 導入には不必要な計算量が伴います。 3 つの重要な経験的観察 剪定の動機を与える:
- 重量の冗長性: 縮退枝刈りの研究により、訓練されたネットワークでは次のことが実証されています。 重量分布はゼロ付近に強く集中します。マイナーウェイトを削除する 大きさが予測に与える影響は最小限です。
- 宝くじの仮説 (Frankle & Carlin、2019): 訓練された各ニューラルネットワーク 元の値で再初期化され、トレーニングされた場合に「勝利」するサブネットワークが含まれます。 単独でも、ネットワーク全体と同等のパフォーマンスを実現します。
- ツールとしての過剰パラメータ化: 追加のパラメータはトレーニング用です (滑らかな風景、極小値からの脱出) しかし、それらは推論には必要ありません。
枝刈りの影響: 実データ
ResNet と BERT に関する調査では、モデルが パラメータの 70 ~ 90% 精度の低下は 1 ~ 2% 未満です。 BERT ベースの構造化トランスフォーマー プルーニング 50% のスパース性により FLOP が削減されます 2x そして推論の高速化 の 1.5倍 元の精度の 99% 以上を維持します。 LLM のコンテキストでは、 Transformer のブロック プルーニング技術により、最大で高速化が見られました。 SQuaD で 2.4 倍 F1 の損失はわずか 1% です。
構造化プルーニングと非構造化プルーニング
枝刈りにおける主な違いは、アプローチ間の違いです。 構造化された e 構造化されていない。選択はターゲットのハードウェアと目的によって異なります 展開の:
| 待ってます | 非構造化 | 構造化された |
|---|---|---|
| 除去するもの | 個別の重み(任意) | ニューロン、フィルター、チャネル、アテンションヘッド、レイヤー |
| 結果として生じるスプレッド | 不規則(疎行列) | レギュラー(Sサイズ) |
| 標準の CPU/GPU での実際の高速化 | なし (スパース操作なし) | はい、高密度の操作で即座に実行可能 |
| スパースなハードウェア (スパースな CPU、Cerebras) での高速化 | Si | Si |
| メモリ削減 | 明示的なスパース形式の場合のみ | いつも(Sサイズ) |
| 等しいスパース性での精度 | 改善する | やや低め |
| 実装の複雑さ | 単純 | より複雑 (依存関係の再計算) |
Il 非構造化剪定 より柔軟: あらゆる重量を取り除くことができます 場所に関係なく。問題は、結果として得られる行列が密なままであることです。 メモリ内 (明示的なゼロ) であり、最新のハードウェアは不均一なスパース性の恩恵を受けません 特別なサポートなし (NVIDIA は、Ampere GPU による 2:4 スパース性のサポートを導入しました。 ただし、特定のパターンが必要です)。の 構造化された剪定、構造物を削除する 完成し、検証可能なほど小さいモデルが生成されます: 512 個のニューロンを含む線形層 256 でプルーニングされたものは単純に Linear(in, 256) となり、標準の密な操作で実行されます。
規模の剪定: 基本的な方法
Il 規模の剪定 そして最もシンプルで驚くほど効果的なアプローチは次のとおりです。 絶対値がしきい値より小さい重みを削除します。直感的なロジックとその重み 小さいものは、ネットワークによって送信される信号にほとんど寄与しません。そのシンプルさにも関わらず、 反復的な再トレーニングと組み合わせると、より優れた方法に匹敵する結果が得られます。 洗練された。
import torch
import torch.nn as nn
import torch.nn.utils.prune as prune
import numpy as np
# ===================================================================
# MAGNITUDE PRUNING CON PYTORCH NATIVE API
# ===================================================================
class ConvNet(nn.Module):
"""Modello CNN semplice per dimostrare il pruning."""
def __init__(self, num_classes=10):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 64, 3, padding=1),
nn.ReLU(),
nn.Conv2d(64, 128, 3, padding=1),
nn.ReLU(),
nn.AdaptiveAvgPool2d(4)
)
self.classifier = nn.Sequential(
nn.Linear(128 * 4 * 4, 256),
nn.ReLU(),
nn.Linear(256, num_classes)
)
def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1)
return self.classifier(x)
model = ConvNet()
# --- Pruning L1 non strutturato (magnitude-based) ---
# Rimuove il 30% dei pesi con valore assoluto minore
prune.l1_unstructured(
model.features[0], # Layer da pruning
name='weight', # Parametro da prunare
amount=0.30 # Percentuale da rimuovere (30%)
)
# --- Pruning Random (baseline di confronto) ---
prune.random_unstructured(
model.features[2],
name='weight',
amount=0.30
)
# --- Analisi sparsita risultante ---
def compute_sparsity(module):
"""Calcola la sparsita effettiva di un modulo."""
total = 0
zeros = 0
for param in module.parameters():
total += param.numel()
zeros += (param == 0).sum().item()
return zeros / total if total > 0 else 0.0
print("Sparsita Conv1:", f"{compute_sparsity(model.features[0]):.1%}")
print("Sparsita Conv2:", f"{compute_sparsity(model.features[2]):.1%}")
# --- Verifica la struttura interna del pruning ---
# PyTorch crea weight_orig (originale) + weight_mask (0/1)
print("\nParametri di model.features[0] dopo pruning:")
for name, param in model.features[0].named_parameters():
print(f" {name}: shape={param.shape}")
for name, buf in model.features[0].named_buffers():
print(f" buffer {name}: shape={buf.shape}")
# --- Rimozione della maschera (make permanent) ---
# Dopo retraining, si consolida: il modello torna a usare 'weight'
prune.remove(model.features[0], 'weight')
print("\nDopo prune.remove: parametri di model.features[0]:")
for name, _ in model.features[0].named_parameters():
print(f" {name}")
# --- Global Pruning: pruning globale su tutto il modello ---
# Più efficace del pruning per-layer: usa una soglia globale
parameters_to_prune = (
(model.features[0], 'weight'),
(model.features[2], 'weight'),
(model.classifier[0], 'weight'),
(model.classifier[2], 'weight'),
)
prune.global_unstructured(
parameters_to_prune,
pruning_method=prune.L1Unstructured,
amount=0.40, # Rimuove 40% globalmente (non per layer)
)
# Sparsita finale per layer
for module_name, module in model.named_modules():
if isinstance(module, (nn.Conv2d, nn.Linear)):
if hasattr(module, 'weight_mask'):
sparsity = (module.weight_mask == 0).float().mean().item()
print(f"{module_name}: sparsita {sparsity:.1%}")
警告: PyTorch ネイティブ プルーニングでは推論は高速化されません
API torch.nn.utils.prune 1つを適用します マスク 重みのバイナリ、ゼロ化
選択されたものですが、元の緻密な構造は維持されています。結果のモデルは
同じメモリで、前方パスにかかる時間も同じです。実際のスピードアップを実現するには、次のものが必要です。
構造化プルーニング (構造の物理的な削除を伴う)、またはスパース固有のライブラリ
操作。 PyTorch ネイティブ プルーニングは、実験や QAT (量子化対応) に最適です。
トレーニング) はまばらですが、直接展開するためのものではありません。
トーチプルーニングによる構造化プルーニング
図書館 トーチ剪定 (Fang et al.、CVPR 2023) は次の問題を解決します。 実際の構造化プルーニング: Conv2D レイヤーからフィルターを削除するには更新も必要です 次の層 (N-k ではなく N 個の入力チャネルが必要です)。トーチプルーニングハンドル これらの依存関係は、依存関係グラフを介して自動的に作成されます (デプグラフ)、サポート ViT、LLM、YOLO、スキップ接続を備えたモデルなどの複雑なアーキテクチャ。
# pip install torch-pruning
import torch
import torch.nn as nn
import torch_pruning as tp
# ===================================================================
# PRUNING STRUTTURATO CON TORCH-PRUNING
# ===================================================================
class ResidualBlock(nn.Module):
"""Blocco residuale: Torch-Pruning gestisce la skip connection automaticamente."""
def __init__(self, channels=64):
super().__init__()
self.conv1 = nn.Conv2d(channels, channels, 3, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(channels)
self.relu = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(channels, channels, 3, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(channels)
def forward(self, x):
residual = x
out = self.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
return self.relu(out + residual) # Skip connection
class SimpleResNet(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
self.stem = nn.Sequential(
nn.Conv2d(3, 64, 3, padding=1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True)
)
self.layer1 = ResidualBlock(64)
self.layer2 = ResidualBlock(64)
self.pool = nn.AdaptiveAvgPool2d(1)
self.fc = nn.Linear(64, num_classes)
def forward(self, x):
x = self.stem(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.pool(x).view(x.size(0), -1)
return self.fc(x)
model = SimpleResNet()
model.eval()
# Input di esempio per tracciare le dipendenze
example_input = torch.randn(1, 3, 32, 32)
# --- Costruzione del grafo delle dipendenze ---
DG = tp.DependencyGraph().build_dependency(model, example_inputs=example_input)
# --- Analisi del modello PRIMA del pruning ---
macs_before, params_before = tp.utils.count_ops_and_params(model, example_input)
print(f"Parametri PRIMA: {params_before / 1e6:.2f}M")
print(f"MACs PRIMA: {macs_before / 1e9:.3f}G")
# --- Definizione della strategia di pruning ---
# Pruning per magnitudine L1 dei filtri (L2 disponibile con tp.strategy.L2Strategy)
pruner = tp.pruner.MagnitudePruner(
model,
example_inputs=example_input,
importance=tp.importance.MagnitudeImportance(p=1), # L1 norm
iterative_steps=5, # Pruning iterativo in 5 step
ch_sparsity=0.5, # Rimuove il 50% dei canali
ignored_layers=[model.fc], # Non pruning il classificatore finale
)
# --- Esecuzione del pruning (un singolo step) ---
pruner.step()
# --- Analisi del modello DOPO il pruning ---
macs_after, params_after = tp.utils.count_ops_and_params(model, example_input)
print(f"\nParametri DOPO: {params_after / 1e6:.2f}M")
print(f"MACs DOPO: {macs_after / 1e9:.3f}G")
print(f"Riduzione parametri: {(1 - params_after/params_before):.1%}")
print(f"Riduzione MACs: {(1 - macs_after/macs_before):.1%}")
# --- Verifica architettura post-pruning ---
print("\nArchitettura post-pruning:")
for name, module in model.named_modules():
if isinstance(module, nn.Conv2d):
print(f" {name}: Conv2d({module.in_channels}, {module.out_channels}, ...)")
# Output tipico:
# Parametri PRIMA: 0.15M | MACs PRIMA: 0.009G
# Parametri DOPO: 0.04M | MACs DOPO: 0.003G
# Riduzione parametri: 75% | Riduzione MACs: 72%
# layer1.conv1: Conv2d(32, 32, ...) <- da 64 a 32 canali
トランスフォーマーの動きの剪定
マグニチュード プルーニングは CNN ではうまく機能しますが、Transformer では別の課題が生じます。 注意の重みは小さいかもしれませんが、 モデル。の 動きの剪定 (Sanh et al., 2020) はこの問題に取り組んでいます 根本的に異なるアプローチ: ウェイトを削除する代わりに 小さい、削除します いる人たち 微調整中にゼロに近づく。言い換えれば、 現在の重み値ではなく、基準と枝刈りターゲットに対する相対的な重み勾配。
動きの枝刈りは、BERT モデルの枝刈りに大きな利点があることを実証しました。 高いスパース性 (80 ~ 97%) では、移動枝刈りがマグニチュード枝刈りを 10 ~ 20 ポイント上回ります。 MNLI や SQuAD などの NLP ベンチマークの割合。
# Movement Pruning per Transformer con Hugging Face + SparseML
# pip install transformers datasets sparseml
import torch
import torch.nn as nn
from transformers import AutoModelForSequenceClassification, AutoTokenizer
from torch.optim import AdamW
# ===================================================================
# MOVEMENT PRUNING MANUALE (concetto base)
# ===================================================================
class MovementPruningLinear(nn.Module):
"""
Layer Linear con movement pruning.
Mantiene uno score per ogni peso: lo score viene ottimizzato
durante il training. I pesi con score basso vengono pruned.
"""
def __init__(self, in_features, out_features, pruning_ratio=0.5):
super().__init__()
self.weight = nn.Parameter(torch.randn(out_features, in_features) * 0.01)
self.bias = nn.Parameter(torch.zeros(out_features))
# Score inizializzati a zero: durante il training salgono per i pesi importanti
self.scores = nn.Parameter(torch.zeros_like(self.weight))
self.pruning_ratio = pruning_ratio
self.mask = None
def update_mask(self):
"""Aggiorna la maschera basandosi sugli score correnti."""
k = int(self.scores.numel() * (1 - self.pruning_ratio))
# Top-k scores: mantieni i pesi con score più alto
threshold = torch.kthvalue(self.scores.flatten(), self.scores.numel() - k).values
self.mask = (self.scores >= threshold).float().detach()
def forward(self, x):
# Applica la maschera durante il forward pass
if self.mask is None:
self.update_mask()
masked_weight = self.weight * self.mask
return nn.functional.linear(x, masked_weight, self.bias)
# ===================================================================
# PRUNING PRATICO CON TRANSFORMERS + torch.nn.utils.prune
# ===================================================================
def prune_transformer_attention_heads(model, heads_to_prune):
"""
Pruna specifici attention heads da un modello BERT-like.
heads_to_prune: dict {layer_idx: [head_idx_1, head_idx_2, ...]}
"""
model.prune_heads(heads_to_prune)
return model
# Esempio: pruning degli attention heads meno importanti
# Identificazione heads da pruning (basata su Taylor importance)
def compute_head_importance(model, dataloader, device):
"""
Calcola l'importanza di ogni attention head usando Taylor expansion.
Un head e importante se rimuoverlo aumenta molto la loss.
"""
model.eval()
head_importance = torch.zeros(
model.config.num_hidden_layers,
model.config.num_attention_heads
).to(device)
for batch in dataloader:
batch = {k: v.to(device) for k, v in batch.items()}
outputs = model(**batch, output_attentions=True)
loss = outputs.loss
loss.backward()
# Accumula gradienti per stimare l'importanza
for layer_idx, layer in enumerate(model.bert.encoder.layer):
attn = layer.attention.self
# Importanza approssimata: |grad * weight| sommato per head
grad = attn.value.weight.grad
weight = attn.value.weight
if grad is not None:
importance = (grad * weight).abs().view(
model.config.num_attention_heads, -1
).sum(dim=-1)
head_importance[layer_idx] += importance
return head_importance
# ===================================================================
# STRUCTURED PRUNING DI ATTENTION HEADS CON BERT
# ===================================================================
model_name = "bert-base-uncased"
# model = AutoModelForSequenceClassification.from_pretrained(model_name)
# tokenizer = AutoTokenizer.from_pretrained(model_name)
# Strategia di pruning: rimuovi il 30% degli heads meno importanti
# Assumendo head_importance calcolata come sopra:
# heads_to_prune = {}
# n_heads_to_prune = int(0.3 * 12 * 12) # 30% di 144 heads totali (12 layers x 12 heads)
# flat_importance = head_importance.flatten()
# _, indices = flat_importance.sort()
# for idx in indices[:n_heads_to_prune]:
# layer_idx = idx.item() // 12
# head_idx = idx.item() % 12
# if layer_idx not in heads_to_prune:
# heads_to_prune[layer_idx] = []
# heads_to_prune[layer_idx].append(head_idx)
# pruned_model = prune_transformer_attention_heads(model, heads_to_prune)
print("Movement pruning e head importance pruning: schema implementato.")
print("Risultati tipici su BERT-base con 40% pruning attention:")
print(" - Speedup inferenza: 1.3-1.5x")
print(" - Dimensione modello: -35%")
print(" - Accuratezza GLUE: -0.5 a -1.5 punti")
宝くじの仮説: 当選サブモデル理論
La 宝くじ仮説 (LTH、Frankle & Carlin、NeurIPS 2019) および次のうちの 1 つ 枝刈りにおいて最も影響力のある理論的発見: 各高密度ニューラル ネットワークには 1 つ以上のサブネットワークが含まれています スパース (「勝ちチケット」) を抽出し、元の初期値で再初期化すると、 独自にトレーニングすることができ、完全なネットワークと同等またはそれ以上の精度を達成できます。 同じかそれ以下のトレーニング時間で。
LTH には重要な実用的な意味があります。つまり、大規模なモデルが主に有用であることを示唆しています。 のために 探す パラメータの本質的な機能ではなく、適切な構造。 当たりチケットを見つけるための標準的なプロセスは次のとおりです。反復規模枝刈り (IMP).
import torch
import torch.nn as nn
import copy
from typing import Dict, List
# ===================================================================
# ITERATIVE MAGNITUDE PRUNING (Lottery Ticket Hypothesis)
# ===================================================================
def save_initial_weights(model: nn.Module) -> Dict[str, torch.Tensor]:
"""Salva i pesi iniziali del modello (prima del training)."""
return {
name: param.data.clone()
for name, param in model.named_parameters()
if 'weight' in name
}
def apply_mask_and_reinit(
model: nn.Module,
initial_weights: Dict[str, torch.Tensor],
masks: Dict[str, torch.Tensor]
) -> nn.Module:
"""
Reimposta i pesi ai valori iniziali con le maschere di pruning applicate.
Questo e il passo critico della LTH: reinizializzare (non random, ma ai valori originali).
"""
with torch.no_grad():
for name, param in model.named_parameters():
if name in initial_weights and name in masks:
param.data = initial_weights[name] * masks[name]
return model
def compute_pruning_masks(
model: nn.Module,
pruning_ratio: float
) -> Dict[str, torch.Tensor]:
"""Calcola le maschere di pruning per magnitude (L1)."""
masks = {}
for name, param in model.named_parameters():
if 'weight' in name and param.dim() > 1:
# Soglia globale per layer
threshold = torch.quantile(param.abs(), pruning_ratio)
masks[name] = (param.abs() >= threshold).float()
return masks
def iterative_magnitude_pruning(
model: nn.Module,
train_fn,
eval_fn,
n_rounds: int = 5,
prune_per_round: float = 0.20,
epochs_per_round: int = 10
):
"""
Implementazione dell'Iterative Magnitude Pruning (LTH).
Algoritmo:
1. Salva i pesi iniziali (w0)
2. Addestra per N epoche
3. Pruna il P% dei pesi con magnitudine minore
4. Reinizializza i pesi sopravvissuti a w0
5. Ripeti dal passo 2
"""
# Step 1: Salva i pesi iniziali
initial_weights = save_initial_weights(model)
masks = {name: torch.ones_like(param)
for name, param in model.named_parameters()
if 'weight' in name}
cumulative_pruned = 0.0
results = []
for round_idx in range(n_rounds):
print(f"\n--- Round IMP {round_idx + 1}/{n_rounds} ---")
# Step 2: Addestra il modello (con le maschere correnti applicate)
train_fn(model, epochs=epochs_per_round, masks=masks)
# Step 3: Calcola nuove maschere di pruning
effective_prune = 1 - (1 - prune_per_round) ** (round_idx + 1)
new_masks = compute_pruning_masks(model, effective_prune)
# Step 4: Reinizializza con pesi iniziali e nuove maschere
model = apply_mask_and_reinit(model, initial_weights, new_masks)
masks = new_masks
# Valutazione
accuracy = eval_fn(model)
total_sparsity = sum(
(m == 0).float().mean().item()
for m in masks.values()
) / len(masks)
results.append({
'round': round_idx + 1,
'accuracy': accuracy,
'sparsity': total_sparsity
})
print(f"Accuratezza: {accuracy:.2%} | Sparsita: {total_sparsity:.1%}")
return model, results
# Risultati tipici IMP su ResNet-20 / CIFAR-10:
# Round 1 (20% pruned): 91.8% accuracy (baseline: 91.9%)
# Round 2 (36% pruned): 91.7% accuracy
# Round 3 (49% pruned): 91.5% accuracy
# Round 4 (59% pruned): 91.2% accuracy
# Round 5 (67% pruned): 90.8% accuracy <- "winning ticket"
# Round 8 (83% pruned): 89.1% accuracy <- accuratezza inizia a degradare
# Round 10 (89% pruned): 87.3% accuracy <- soglia tipica fine utilita
LTH の実践: 制限事項
- 計算コスト: IMP は多くの train-prune-reinit サイクルを必要とするため、 大型モデルの場合は高価です。 LLM の場合、GMP (段階的) などのより効率的なバリアント Magnitude Pruning) は再初期化を必要としません。
- スケーラビリティ: オリジナルの LTH は小型モデルで動作します。 BERT と GPT の場合、 初期重みへの再初期化には明確な利点はありません。剪定 + 微調整が使用されます 現在の体重について。
- 転移学習: 2020 年の研究 (Chen ら) では、「勝利」が示されています。 BERT のような事前トレーニング済みモデルのチケットは、下流のタスクに転送可能であり、オープン 興味深いアプリケーション。
再トレーニングを伴う反復的な枝刈りワークフロー
運用環境で最も効果的なワークフローは、ワンショット プルーニング (ウェイトの 50% をすぐに削除する) ではありません。 しかし、 再トレーニングを伴う反復枝刈り:ネットを残して徐々に剪定します 各ステップで「回復」するための時間。これにより、大幅に正確なモデルが生成されます 同じターゲットのスパース性が与えられた場合。
import torch
import torch.nn as nn
import torch.nn.utils.prune as prune
from torch.optim.lr_scheduler import CosineAnnealingLR
# ===================================================================
# WORKFLOW PRUNING ITERATIVO COMPLETO
# ===================================================================
def get_global_sparsity(model: nn.Module) -> float:
"""Calcola la sparsita globale del modello."""
total_params = 0
zero_params = 0
for name, param in model.named_parameters():
if 'weight' in name:
total_params += param.numel()
zero_params += (param == 0).sum().item()
return zero_params / total_params if total_params > 0 else 0.0
def iterative_pruning_with_finetuning(
model: nn.Module,
train_loader,
val_loader,
target_sparsity: float = 0.70,
n_pruning_steps: int = 7,
finetune_epochs_per_step: int = 3,
lr: float = 1e-4,
device: str = 'cuda'
):
"""
Pruning iterativo con fine-tuning post-pruning.
Strategia: aumenta la sparsita gradualmente usando una schedule
cubica (più aggressiva all'inizio, più conservativa alla fine).
"""
model = model.to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-5)
criterion = nn.CrossEntropyLoss()
history = []
# Schedule di sparsita cubica
sparsity_schedule = [
1 - (1 - target_sparsity * (step / n_pruning_steps) ** 3)
for step in range(1, n_pruning_steps + 1)
]
print(f"Schedule sparsita: {[f'{s:.1%}' for s in sparsity_schedule]}")
for step_idx, target_sparsity_step in enumerate(sparsity_schedule):
print(f"\n=== Step {step_idx + 1}/{n_pruning_steps} | Target sparsita: {target_sparsity_step:.1%} ===")
# Raccoglie tutti i parametri weight del modello
parameters_to_prune = [
(module, 'weight')
for name, module in model.named_modules()
if isinstance(module, (nn.Linear, nn.Conv2d))
]
# Pruning globale L1
prune.global_unstructured(
parameters_to_prune,
pruning_method=prune.L1Unstructured,
amount=target_sparsity_step
)
actual_sparsity = get_global_sparsity(model)
print(f"Sparsita effettiva: {actual_sparsity:.1%}")
# Fine-tuning post-pruning
scheduler = CosineAnnealingLR(optimizer, T_max=finetune_epochs_per_step)
for epoch in range(finetune_epochs_per_step):
model.train()
train_loss = 0.0
for batch_x, batch_y in train_loader:
batch_x, batch_y = batch_x.to(device), batch_y.to(device)
optimizer.zero_grad()
out = model(batch_x)
loss = criterion(out, batch_y)
loss.backward()
optimizer.step()
train_loss += loss.item()
scheduler.step()
# Valutazione
model.eval()
correct = total = 0
with torch.no_grad():
for batch_x, batch_y in val_loader:
batch_x, batch_y = batch_x.to(device), batch_y.to(device)
pred = model(batch_x).argmax(dim=1)
correct += (pred == batch_y).sum().item()
total += batch_y.size(0)
val_acc = correct / total
history.append({'step': step_idx+1, 'sparsity': actual_sparsity, 'val_acc': val_acc})
print(f"Val accuracy: {val_acc:.2%}")
# Consolida le maschere (rende il pruning permanente)
for module, param_name in parameters_to_prune:
try:
prune.remove(module, param_name)
except ValueError:
pass # Già rimosso
return model, history
プルーニング + 量子化: 最大圧縮
プルーニングと量子化は補完的な技術であり、効果的に組み合わせることができます。 枝刈りによりパラメータの数が減ります。量子化によりそれぞれの精度が低下します 残りのパラメータ。これらを組み合わせて適用すると、非常にコンパクトなモデルが作成されます。 この組み合わせは次のように知られています 「スパース量子化」 o 「量子化されたスパースモデル」.
import torch
import torch.nn as nn
import torch.nn.utils.prune as prune
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
# ===================================================================
# COMBINAZIONE PRUNING + QUANTIZZAZIONE
# ===================================================================
# --- Approccio 1: Pruning strutturato + Quantizzazione INT8 ---
# Pruna prima (rimuove strutture), poi quantizza il modello ridotto
def prune_and_quantize_pipeline(model_name: str, prune_ratio: float = 0.30):
"""
Pipeline: carica modello -> pruning strutturato -> quantizzazione INT8.
"""
# Step 1: Carica modello full precision
from transformers import AutoModelForSequenceClassification
model = AutoModelForSequenceClassification.from_pretrained(
model_name,
torch_dtype=torch.float32
)
print(f"Parametri originali: {sum(p.numel() for p in model.parameters()) / 1e6:.1f}M")
# Step 2: Pruning L1 non strutturato globale
parameters_to_prune = [
(module, 'weight')
for name, module in model.named_modules()
if isinstance(module, nn.Linear) and 'classifier' not in name
]
prune.global_unstructured(
parameters_to_prune,
pruning_method=prune.L1Unstructured,
amount=prune_ratio
)
# Consolida maschere
for module, param_name in parameters_to_prune:
prune.remove(module, param_name)
# Conta parametri zero
zero_params = sum(
(param == 0).sum().item()
for name, param in model.named_parameters()
if 'weight' in name
)
total_params = sum(
param.numel()
for name, param in model.named_parameters()
if 'weight' in name
)
print(f"Sparsita dopo pruning: {zero_params/total_params:.1%}")
# Step 3: Quantizzazione dinamica INT8 del modello pruned
model_quantized = torch.quantization.quantize_dynamic(
model,
{nn.Linear}, # Quantizza solo layer Linear
dtype=torch.qint8
)
return model_quantized
# --- Approccio 2: QLoRA su modello pre-pruned ---
# Per LLM: usa modelli già pruned + quantizzazione NF4 per fine-tuning
config_nf4 = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True
)
# Molti modelli su HuggingFace Hub sono già pruned E quantizzati:
# es. "microsoft/phi-2" (2.7B), "TinyLlama/TinyLlama-1.1B-Chat-v1.0"
# Questi sono modelli "distilled + pruned" during pretraining.
# --- Benchmark memoria: pruning + quantizzazione ---
compression_results = [
{"metodo": "FP32 (baseline)", "sparsita": "0%", "precisione": "FP32", "size_mb": 440},
{"metodo": "Pruning 50%", "sparsita": "50%", "precisione": "FP32", "size_mb": 220},
{"metodo": "Quantizzazione INT8", "sparsita": "0%", "precisione": "INT8", "size_mb": 110},
{"metodo": "Pruning 50% + INT8", "sparsita": "50%", "precisione": "INT8", "size_mb": 55},
{"metodo": "Pruning 70% + INT4", "sparsita": "70%", "precisione": "INT4", "size_mb": 33},
]
print(f"\n{'Metodo':<28} {'Sparsita':>10} {'Precisione':>12} {'Dimensione':>12}")
print("-" * 65)
for r in compression_results:
print(f"{r['metodo']:<28} {r['sparsita']:>10} {r['precisione']:>12} {r['size_mb']:>10} MB")
# Output (modello BERT-base ~440MB in FP32):
# Metodo Sparsita Precisione Dimensione
# FP32 (baseline) 0% FP32 440 MB
# Pruning 50% 50% FP32 220 MB
# Quantizzazione INT8 0% INT8 110 MB
# Pruning 50% + INT8 50% INT8 55 MB
# Pruning 70% + INT4 70% INT4 33 MB
ベンチマーク: 精度、スピードアップ、メモリ
枝刈りの結果は、モデル、タスク、および方法によって大きく異なります。 次の表は、BERT ベースと ResNet-50 のベンチマークを示しています。 文献結果と実際の実験:
| モデル | 方法 | 散らばっている | 正確さ | 高速化 | メモリ |
|---|---|---|---|---|---|
| BERT ベース (MNLI) | ベースライン FP16 | 0% | 84.6% | 1.0倍 | 440MB |
| BERT ベース (MNLI) | 強度のマグニチュード | 50% | 84.1% | 1.0倍* | 440MB* |
| BERT ベース (MNLI) | 動きの枝刈り | 70% | 83.5% | 1.0倍* | 440MB* |
| BERT ベース (MNLI) | 頭刈り 30% | 30% の頭 | 84.0% | 1.3倍 | 310MB |
| BERT ベース (SQuAD) | ブロック枝刈りstr. | 50% | F1 -1% | 2.4倍 | 220MB |
| ResNet-50 (イメージネット) | L1フィルターの枝刈り | 40% | トップ1 -0.5% | 1.5倍 | -40% |
| ResNet-50 (イメージネット) | 反復枝刈り | 70% | トップ1 -1.2% | 2.1倍 | -65% |
* 非構造化プルーニング: 専用のスパース演算を使用しない標準ハードウェアでは高速化はありません。
ターゲット ハードウェア タイプ別の推奨事項
- 標準 NVIDIA GPU: 構造化された剪定 (トーチ剪定、頭剪定) を優先します。 非構造化プルーニングは、使用しない限り、専用のスパース サポートがなければ役に立ちません。 NVIDIA Ampere の 2:4 スパース形式 (4 つごとに 2 つの非ゼロの特定のパターンで 50% のスパース性)。
- CPU (デプロイ推論): 高いスパース性 (>80%) による非構造化プルーニング Intel oneDNN などのライブラリまたは CSR/CSC 形式への変換を使用して高速化できます。 しかし、構造化された枝刈りは依然としてより予測可能です。
- エッジデバイス (Jetson、Raspberry Pi): 構造化プルーニング + INT8 量子化 またはGGUF。モデルの削減は非常に重要です。パラメーターが 2 分の 1 少ないだけでも違いが生じる可能性があります。 実行可能ファイルと実行不可能なファイルの間。
- モバイル (ARM): INT8 量子化を備えた XNNPACK や CoreML などのライブラリを使用する そして実際のハードウェアアクセラレーションのための構造化されたプルーニング。
ベストプラクティスとアンチパターン
剪定のベストプラクティス
- ワンショットではなく、反復的な枝刈りを使用します。 再トレーニングによりステップごとに 10 ~ 20% を枝刈り 中間。 1 回の攻撃的な 70% 除去は、ほとんどの場合、精度を大幅に低下させます。 不可逆的な。
- 各ステップの後に再トレーニングを適用します。 1 ~ 3 回の微調整エポック後でも 各ラウンドの枝刈りにより、失われた精度のほとんどが回復します。学習率 低くする必要があります (元のトレーニングの 10 ~ 100 分の 1)。
- ターゲット ハードウェアに基づいて方法を選択します。 高速化のための構造化されたプルーニング 標準ハードウェアでは実数。スパース対応ハードウェアにアクセスできる場合にのみ非構造化されます。
- 重要なレイヤーを削除しないでください。 各ネットワークの最初と最後の層 (埋め込み、 分類子) が最も敏感です。これらのレイヤーの枝刈りを除外するか、大幅に減らします。
- 剪定中の重量分布を監視します。 1 つの重みが多すぎる場合 同じレイヤーが枝刈りされると (>80%)、レイヤーが崩壊する可能性があります。レイヤーごとの最小制限を設定します。
- 損失だけでなく、タスクの指標を評価します。 トレーニングロスは捕捉できない可能性があります エッジケースの劣化。ドメイン固有のメトリクス (F1、BLEU、テスト セットの精度) を使用します。
避けるべきアンチパターン
-
標準 GPU での非構造化プルーニングによる高速化は期待できません。
API
torch.nn.utils.pruneウェイトをゼロにしますが、物理的に削除しません。 専用のスパース演算がなければ、推論時間は短縮されません。 -
統合せずにマスクとウェイトを混合しないでください。 エクスポートする前、または
モデルを配布し、常に呼び出します
prune.remove(module, 'weight')のために マスクをパラメータに統合します。そうしないと、モデルにもメモリのオーバーヘッドが発生します 移植性のない依存関係。 - 小さすぎる検証データセットは使用しないでください。 積極的な剪定 精度の監視に使用される検証セットでオーバーフィッティングが発生する可能性があります。を使用します。 最終評価のために保持されたテストセット。
- 正規化層を無視しないでください。 BatchNorm と LayerNorm は維持します 前のレイヤーの寸法に関連する統計。構造化された枝刈りの後、 正規化統計を再調整する必要があります (調整データセットで再実行します)。
- 非収束モデルには枝刈りを適用しないでください。 剪定が一番効果的 よく訓練されたモデルで。まだ収束していないモデルに適用すると、収量が得られます 予測不可能な結果。
2025 ~ 2026 年の剪定: 最先端の技術
剪定の分野は、LLM の台頭により大幅に進化しました。主な傾向 2025 年から 2026 年には次のものが含まれます。
- SparseGPT と Wanda: 必要のない LLM のワンショット プルーニング方法 再訓練中。 SparseGPT (Frantar & Alistarh、2023) は、行列の近似逆行列を使用します。 ヘシアンを使用して残りの重みを更新し、枝刈り誤差を補正します。ワンダ (Sun et al., 2023) は、基準として重みの大きさと入力活性化ノルムの積を使用します。
- 2:4 スパーシティ (NVIDIA): ハードウェアでサポートされる構造化されたスパースパターン Ampere および Hopper GPU では、4 要素ごとに 2 つのゼロ以外の値。高速化を実現します A100/H100 でのスパース操作では最大 1.5 ~ 2 倍で、デンス モデルとほぼ同じ精度が得られます。
- コープ (2025): 閉じた形式のワンショット表現 - 構造を維持した枝刈り ビジョントランスフォーマー向け — 実際の最小限のハードウェアスピードアップで DeiT-Tiny から DeiT-Huge までスケールアップ 精度の低下。
- 剪定 + 蒸留: 枝刈りと知識の蒸留の組み合わせ (このシリーズの前の記事) 最良の結果が得られます: 枝刈りされたモデルが得られます オリジナルの教師モデルの監督のもとでトレーニングされました。
結論
ニューラル ネットワーク プルーニングは、世界で最も成熟した汎用性の高い圧縮技術の 1 つです。 深い学習。剪定の違いを理解する 構造化された e 構造化されていない そして基本: 1 つ目はハードウェアでの実際の高速化を実現します。 標準では、後者は特定のスパース性サポートを必要としますが、より高い柔軟性を提供します。
Il 再トレーニングを伴う反復枝刈り 品質のゴールドスタンダードであり続ける 結果。そこには 宝くじ仮説 基本的な理論的洞察を提供します 非常に大規模なモデルには実際的な制限があるにもかかわらず、プルーニングが機能する理由について説明します。 最新の LLM の場合、SparseGPT や Wanda などのメソッドが、実行可能なワンショットの代替手段を提供します。
組み合わせ 枝刈り + 量子化 そして最大までの幹線道路 圧縮: パラメータの数とその数値精度を補完的な方法で削減します。 を使用すると、開始点よりも 10 ~ 15 倍小さい設置面積のモデルを取得できます。 ほとんどの実稼働ユースケースで許容可能な精度。
次のステップ
- 次の記事: Ollama: ラップトップとラズベリーでローカル LLM を実行する
- 前の記事: 蒸留モデル: 知識の伝達
- 関連している: 量子化モデル: GPTQ、AWQ、INT8
- 関連している: LoRA と QLoRA による微調整
- MLOps シリーズ: モデルの提供と展開







