신경망 가지치기: 모델 복잡성 감소
ResNet-50 모델에는 2,500만 개 이상의 매개변수가 있습니다. GPT-3에는 1,750억 개가 있습니다. 그래도 연구 계통학에서는 이러한 매개변수의 대부분이 중복되어 있음을 보여줍니다. 훈련된 신경망 그들은 더 많은 것을 잃을 수 있다 무게의 90% 크게 저하되지 않고 정확성. 그만큼 전정 — 불필요한 매개변수를 체계적으로 제거하는 기술 — 딥 러닝 모델의 계산 복잡성을 줄이는 가장 강력한 도구 중 하나입니다.
매개변수의 수치 정밀도를 감소시키는 양자화와 달리 li 가지치기는 완전히 제거하다. 결과적으로 더 작고 빠르며 저렴한 모델이 될 수 있습니다. 실행 비용이 많이 듭니다. 특히 구조화된 가지치기, 그 전체 뉴런, 필터 또는 주의 헤드를 제거하여 하드웨어의 실제 속도를 향상시킵니다. 스파르시타에 대한 지원을 요청하세요.
이 가이드에서 우리는 가지치기에 대해 심도있게 탐구합니다. 복권 가설 크기에 따른 가지치기부터 움직임에 따른 가지치기까지, PyTorch를 이용한 실제 구현까지 Transformers, 최대 반복 작업 흐름 및 양자화와의 조합.
무엇을 배울 것인가
- 구조화된 가지치기와 구조화되지 않은 가지치기의 차이점과 각각을 언제 사용하는지
- 크기 가지치기: 가장 간단하고 효과적인 방법
- 최신 Transformer 및 LLM을 위한 이동 가지치기
- 복권 가설: 가지치기가 작동하는 이유를 설명하는 이론
- 완전한 예제가 포함된 PyTorch 가지치기 API
- 재학습을 통한 반복적인 가지치기 워크플로
- 고급 구조적 가지치기를 위한 토치 가지치기
- 최대 압축을 위한 가지치기 + 양자화 조합
- 정확성, 메모리, 속도에 대한 실제 벤치마크
- 모범 사례 및 일반적인 안티 패턴
왜 가지치기를 하는가? 중복 문제
현대 신경망은 지나치게 매개변수화되어 있는 것으로 악명 높습니다. 이 중복성과 부분적으로 의도적: 대규모 네트워크는 더 쉽게 훈련되고 더 잘 일반화되지만 현재로서는 배포로 인해 불필요한 계산 부담이 발생합니다. 세 가지 주요 경험적 관찰 가지치기 동기 부여:
- 중량 중복성: 퇴화된 가지치기 연구는 훈련된 네트워크에서 다음을 보여줍니다. 무게 분포는 0 주위에 강하게 집중되어 있습니다. 작은 가중치 제거 규모는 예측에 최소한의 영향을 미칩니다.
- 복권 가설(Frankle & Carlin, 2019): 훈련된 각 신경망 원래 값으로 다시 초기화되고 훈련된 "승리" 하위 네트워크를 포함합니다. 단독으로 전체 네트워크에 필적하는 성능을 달성합니다.
- 도구로서의 과잉 매개변수화: 추가 매개변수는 훈련용입니다. (더 부드러운 풍경, 국소 최소치로부터의 탈출), 그러나 추론에는 필요하지 않습니다.
가지치기의 영향: 실제 데이터
ResNet 및 BERT에 대한 연구에 따르면 모델은 다음을 놓칠 수 있습니다. 매개변수의 70-90% 정확도 손실은 1~2% 미만입니다. BERT 기반의 구조화된 Transformer 가지치기 희소성이 50%이면 FLOP가 감소합니다. 2x 추론 속도 향상 의 1.5배 원래 정확도의 99% 이상을 유지합니다. LLM 맥락에서, Transformers의 블록 가지치기 기술은 최대 속도 향상을 보여주었습니다. SQuAD에서는 2.4배 F1의 손실은 단 1%에 불과합니다.
구조화된 가지치기와 구조화되지 않은 가지치기
가지치기의 주요 차이점은 접근 방식 간의 차이입니다. 구조화된 e 구조화되지 않은. 선택은 대상 하드웨어 및 목표에 따라 다릅니다. 배포:
| 나는 기다린다 | 구조화되지 않음 | 구조화됨 |
|---|---|---|
| 제거되는 내용 | 개별 가중치(임의) | 뉴런, 필터, 채널, 주의 헤드, 레이어 |
| 결과적인 확산 | 불규칙(희소 행렬) | 레귤러(소형) |
| 표준 CPU/GPU의 실제 속도 향상 | 없음(희소 작업 없음) | 예, 밀도 높은 작업으로 즉시 실행 가능 |
| 희소 하드웨어(희소 CPU, Cerebras)의 속도 향상 | Si | Si |
| 메모리 감소 | 명시적인 희소 형식에만 해당 | 항상 (작은 크기) |
| 희소성이 동일한 정확도 | 개선하다 | 약간 낮음 |
| 구현 복잡성 | 단순한 | 더 복잡함(종속성 재계산) |
Il 구조화되지 않은 가지치기 더욱 유연해졌습니다. 무게를 제거할 수 있습니다. 위치에 관계없이. 문제는 결과 행렬이 조밀하게 유지된다는 것입니다. 메모리(명시적 0)에 있으며 최신 하드웨어는 고르지 않은 희소성으로 인해 이점을 얻지 못합니다. 특정 지원 없이(NVIDIA는 Ampere GPU를 통해 2:4 희소성에 대한 지원을 도입했습니다. 하지만 특정 패턴이 필요합니다). 그만큼 구조화된 가지치기, 구조 제거 완전하고 검증 가능한 더 작은 모델 생성: 512개의 뉴런이 있는 선형 레이어 256에서 잘라낸 부분은 단순히 선형(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 하나를 적용 마스크 가중치에 대한 이진수, 영점 조정
선택된 것들은 원래의 조밀한 구조를 유지합니다. 결과 모델은 다음을 차지합니다.
동일한 메모리를 사용하고 전달하는 데 동일한 시간이 걸립니다. 실제 속도 향상을 얻으려면 다음이 필요합니다.
구조화된 가지치기(구조의 물리적 제거 포함) 또는 희소 특정 라이브러리
운영. 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)은 이 문제를 다룹니다. 근본적으로 다른 접근 방식: 가중치를 제거하는 대신 작은, 제거 그런 사람들 미세 조정 중 0에 접근. 즉, 기준 및 현재 가중치 값이 아닌 가지치기 대상을 기준으로 한 가중치 기울기입니다.
이동 가지치기는 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) 및 다음 중 하나 가지치기에 대한 가장 영향력 있는 이론적 결과: 각 조밀한 신경망에는 하나 이상의 하위 네트워크가 포함되어 있습니다. 희소("당첨 티켓"). 추출하고 원래 초기 값으로 다시 초기화하면 자체적으로 훈련하여 전체 네트워크와 비슷하거나 그보다 우수한 정확도를 달성할 수 있습니다. 더 적거나 동일한 훈련 시간에.
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에는 많은 학습-자르기-재시작 주기가 필요합니다. 대형 모델에 비쌉니다. LLM의 경우 GMP(점진적 크기 정리) 재초기화가 필요하지 않습니다.
- 확장성: 원래 LTH는 소형 모델에서 작동합니다. BERT 및 GPT의 경우 초기 가중치로 다시 초기화하면 명확한 이점이 없습니다. 가지치기 + 미세조정이 사용됩니다 현재 체중에 대해.
- 전이 학습: 2020년 연구(Chen et al.)에 따르면 "승리" 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) | 크기 unstr. | 50% | 84.1% | 1.0x* | 440MB* |
| BERT 기반(MNLI) | 움직임 가지치기 | 70% | 83.5% | 1.0x* | 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마다 0이 아닌 2개의 특정 패턴에서 50% 희소성).
- CPU(배포 추론): 높은 희소성(>80%)으로 구조화되지 않은 가지치기 Intel oneDNN과 같은 라이브러리를 사용하거나 CSR/CSC 형식으로 변환하여 속도를 높일 수 있습니다. 그러나 구조화된 가지치기는 여전히 더 예측 가능합니다.
- 엣지 장치(Jetson, Raspberry Pi): 구조화된 가지치기 + INT8 양자화 또는 GGUF. 모델 축소가 중요합니다. 매개변수가 2배 더 적더라도 차이를 만들 수 있습니다. 실행 가능한 것과 실행 불가능한 것 사이.
- 모바일(ARM): INT8 양자화와 함께 XNNPACK 또는 CoreML과 같은 라이브러리 사용 실제 하드웨어 가속을 위한 구조화된 가지치기.
모범 사례 및 안티 패턴
가지치기 모범 사례
- 일회성이 아닌 반복적 가지치기를 사용합니다. 재교육을 통해 단계당 10~20% 정리 중간. 한번의 공격적인 70% 제거는 거의 항상 정확도를 상당히 떨어뜨립니다. 되돌릴 수 없습니다.
- 각 단계 후에 재교육을 적용합니다. 1~3번의 미세 조정 epoch 이후에도 각 가지치기 작업은 손실된 정확도를 대부분 복구합니다. 학습률 낮아야 합니다(원래 훈련보다 10~100배 낮아야 함).
- 대상 하드웨어에 따라 방법을 선택하십시오. 속도 향상을 위한 구조화된 가지치기 표준 하드웨어에서는 실제입니다. 희소 기능이 있는 하드웨어에 액세스할 수 있는 경우에만 구조화되지 않습니다.
- 중요한 레이어를 정리하지 마세요. 각 네트워크의 첫 번째 및 마지막 레이어(임베딩, 분류기)가 가장 민감합니다. 이러한 레이어에서 가지치기를 제외하거나 크게 줄입니다.
- 가지치기 중 체중 분포를 모니터링합니다. 하나의 무게가 너무 많으면 동일한 레이어가 정리되면(>80%) 레이어가 붕괴될 수 있습니다. 레이어당 최소 제한을 설정합니다.
- 손실뿐만 아니라 작업 지표를 평가합니다. 훈련 손실은 포착되지 않을 수 있습니다 엣지 케이스의 성능 저하. 도메인별 지표(F1, BLEU, 테스트 세트의 정확도)를 사용합니다.
피해야 할 안티패턴
-
표준 GPU에서 구조화되지 않은 가지치기로 인한 속도 향상은 기대하지 마세요.
API
torch.nn.utils.prune가중치를 0으로 설정하지만 물리적으로 제거하지는 않습니다. 전용 희소 연산 없이는 추론 시간이 줄어들지 않습니다. -
다음을 통합하지 않고 마스크와 분동을 혼합하지 마십시오. 수출하기 전 또는
모델을 배포하고 항상 호출하세요.
prune.remove(module, 'weight')에 대한 마스크를 매개변수에 통합합니다. 그렇지 않으면 모델에 메모리 오버헤드도 발생합니다. 이식 불가능한 종속성. - 너무 작은 검증 데이터세트를 사용하지 마세요: 공격적인 가지치기 정확도를 모니터링하는 데 사용되는 검증 세트에 과적합이 발생할 수 있습니다. 사용 최종 평가를 위해 보류된 테스트 세트입니다.
- 정규화 레이어를 무시하지 마세요: BatchNorm 및 LayerNorm 유지 관리 이전 레이어의 차원과 관련된 통계입니다. 체계적으로 가지치기를 한 후, 정규화 통계를 다시 교정해야 합니다(교정 데이터 세트에서 다시 실행).
- 수렴되지 않은 모델에는 가지치기를 적용하지 마세요. 가지치기가 가장 효과가 좋다 잘 훈련된 모델에 대해 아직 수율이 수렴되지 않은 모델에 적용 예측할 수 없는 결과.
2025~2026년 가지치기: 최신 기술
가지치기 분야는 LLM의 등장으로 크게 발전했습니다. 주요 동향 2025-2026년에는 다음이 포함됩니다:
- SparseGPT 및 완다: 필요하지 않은 LLM을 위한 원샷 가지치기 방법 재교육. SparseGPT(Frantar & Alistarh, 2023)는 대략적인 역행렬을 사용합니다. Hessian은 가지치기 오류를 보상하면서 나머지 가중치를 업데이트합니다. 완다(Sun et al., 2023)은 가중치 크기와 입력 활성화 규범의 곱을 기준으로 사용합니다.
- 2:4 희소성(NVIDIA): 하드웨어 지원 구조적 희소성 패턴 Ampere 및 Hopper GPU: 4개 요소마다 정확히 2개의 0이 아닌 값. 속도 향상을 생성합니다. 밀도가 높은 모델과 거의 동일한 정확도로 A100/H100의 희소 작업에서 ~1.5-2x.
- 기업(2025): 폐쇄형 원샷 표현 - 구조화된 가지치기 보존 Vision Transformers용 — 실질적이고 최소한의 하드웨어 속도 향상을 통해 DeiT-Tiny에서 DeiT-Huge까지 확장 가능 정확성 상실.
- 가지치기 + 증류: 가지치기와 지식 증류의 결합 (이 시리즈의 이전 기사)는 최상의 결과를 생성합니다. 가지치기 모델이 원래 교사 모델의 감독으로 훈련되었습니다.
결론
신경망 가지치기는 세계에서 가장 성숙하고 다양한 압축 기술 중 하나입니다. 딥러닝. 가지치기의 차이점 이해하기 구조화된 e 구조화되지 않은 그리고 기본: 첫 번째는 하드웨어의 실제 속도 향상을 생성합니다. 표준인 경우 후자는 특정 희소성 지원이 필요하지만 더 많은 유연성을 제공합니다.
Il 재훈련을 통한 반복적 가지치기 품질에 대한 표준으로 남아 있습니다. 결과. 거기 복권 가설 근본적인 이론적 통찰력을 제공합니다. 매우 큰 모델에는 실질적인 제한이 있음에도 불구하고 가지치기가 작동하는 이유에 대해 설명합니다. 최신 LLM의 경우 SparseGPT 및 Wanda와 같은 방법이 실행 가능한 원샷 대안을 제공합니다.
조합 가지치기 + 양자화 그리고 주요 도로는 최대로 압축: 보완적인 방식으로 매개변수 수와 수치 정밀도를 줄입니다. 시작점보다 10~15배 작은 설치 공간으로 모델을 얻을 수 있습니다. 대부분의 프로덕션 사용 사례에 허용되는 정확도입니다.
다음 단계
- 다음 기사: Ollama: 노트북 및 Raspberry에서 로컬 LLM 실행
- 이전 기사: 증류 모델: 지식 이전
- 관련된: 양자화 모델: GPTQ, AWQ, INT8
- 관련된: LoRA 및 QLoRA를 통한 미세 조정
- MLOps 시리즈: 모델 제공 및 배포







