Tunderea rețelelor neuronale: reducerea complexității modelului
Un model ResNet-50 are peste 25 de milioane de parametri. GPT-3 are 175 miliarde. Cu toate acestea, cercetare sistematica demonstrează că majoritatea acestor parametri sunt redundanți: rețele neuronale antrenate pot pierde mai mult decât 90% din greutatea sa fără degradare semnificativă de precizie. The tăierea — tehnica de eliminare sistematică a parametrilor de prisos — și unul dintre cele mai puternice instrumente pentru reducerea complexității computaționale a modelelor de învățare profundă.
Spre deosebire de cuantificare, care reduce precizia numerică a parametrilor, tăierea li elimina complet. Rezultatul poate fi un model mai mic, mai rapid și mai ieftin scump de rulat - mai ales atunci când se aplică tăierea structurată, că elimină neuroni întregi, filtre sau capete de atenție, producând accelerații reale pe hardware fără cere sprijin pentru sparsita.
În acest ghid explorăm tăierea în profunzime: din teoria Ipoteza biletului de loterie la implementări practice cu PyTorch, de la tăierea după mărime la tăierea mișcării prin Transformatori, până la fluxuri de lucru iterative și combinații cu cuantizare.
Ce vei învăța
- Diferența dintre tăierea structurată și nestructurată și când să folosiți fiecare
- Tăierea mărimii: cea mai simplă și eficientă metodă
- Tăierea mișcării pentru transformatoare și LLM moderne
- Ipoteza biletului de loterie: teoria care explică de ce funcționează tăierea
- API-ul de tăiere PyTorch cu exemple complete
- Flux de lucru iterativ de tăiere cu recalificare
- Torch-Pruning pentru tăiere structurată avansată
- Combinație de tăiere + cuantizare pentru compresie maximă
- Benchmark-uri din lumea reală pentru precizie, memorie și viteză
- Cele mai bune practici și anti-modele comune
De ce Tunderea? Problema redundanței
Rețelele neuronale moderne sunt în mod notoriu supraparametrate. Această redundanță și parțial intenționat: rețelele mai mari se antrenează mai ușor și se generalizează mai bine, dar în acest moment de desfășurare aduce cu sine o greutate computațională inutilă. Trei observații empirice cheie motivați tăierea:
- Redundanță de greutate: Studiile de tăiere degenerate demonstrează că în rețelele antrenate distribuția greutății este puternic concentrată în jurul zero. Îndepărtați greutățile minore magnitudinea are un impact minim asupra previziunilor.
- Ipoteza biletului de loterie (Frankle & Carlin, 2019): Fiecare rețea neuronală antrenată conține o subrețea „câștigătoare” care dacă este reinițializată cu valorile originale și antrenată singur, atinge performanțe comparabile cu întreaga rețea.
- Supraparametrizarea ca instrument: Parametrii suplimentari sunt pentru antrenament (peisaj mai lin, evadare din minimele locale), dar nu sunt necesare pentru deducere.
Impactul tăierii: date reale
Cercetările efectuate pe ResNet și BERT arată că modelele pot rata 70-90% din parametri cu o pierdere de precizie mai mică de 1-2%. Tăierea transformatorului structurat pe baza BERT cu 50% sparsitate produce o reducere a FLOP-urilor 2x și o accelerare a inferenței de 1,5x menținând peste 99% din precizia inițială. În contextul LLM, Tehnicile de tăiere în bloc pentru Transformers au arătat o viteză de până la 2,4x pe SQuAD cu doar 1% pierdere de F1.
Tunderea structurată vs. nestructurată
Distincția cheie în tăiere este între abordări structurat e nestructurat. Alegerea depinde de hardware-ul și obiectivele țintă de implementare:
| astept | Nestructurat | Structurat |
|---|---|---|
| Ce elimină | Greutăți individuale (arbitrare) | Neuroni, filtre, canale, capete de atenție, straturi |
| Răspândirea rezultată | Neregulat (matrice rară) | Obișnuit (dimensiune mică) |
| Accelerare reală pe CPU/GPU standard | Niciuna (fără operațiuni rare) | Da, imediat cu operațiuni dense |
| Accelerare pe hardware rar (procesor rar, Cerebras) | Si | Si |
| Reducerea memoriei | Numai cu format rar explicit | Întotdeauna (dimensiune mică) |
| Acuratețe cu o rază egală | Îmbunătăţi | Puțin mai jos |
| Complexitatea implementării | Simplu | Mai complex (recalcularea dependențelor) |
Il tăierea nestructurată și mai flexibil: poate elimina orice greutate indiferent de locația sa. Problema este că matricea rezultată rămâne densă în memorie (zerouri explicite), iar hardware-ul modern nu beneficiază de lipsa neuniformă fără suport specific (NVIDIA a introdus suport pentru dispersitatea 2:4 cu GPU-uri Ampere, dar necesită modele specifice). The tăierea structurată, îndepărtarea structurilor complet, produce modele verificabil mai mici: un strat Linear cu 512 neuroni tăiat la 256 devine pur și simplu Linear(in, 256), efectuat cu operații standard dense.
Tăierea mărimii: metoda fundamentală
Il tăieri de amploare și cea mai simplă și surprinzător de eficientă abordare: eliminați greutăți cu valoare absolută mai mică decât un prag. Logica intuitivă și asta cântărește cele mici contribuie putin la semnalul transmis de retea. În ciuda simplității sale, atunci când este combinată cu recalificarea iterativă, produce rezultate competitive cu metode mult mai bune sofisticat.
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%}")
Avertisment: PyTorch Native Tuning nu accelerează inferența
API-ul torch.nn.utils.prune aplica unul masca binar pe ponderi, zero
cele alese dar menţinând structura densă iniţială. Modelul rezultat ocupă
aceeași memorie și durează același timp pentru a trece înainte. Pentru a obține accelerații reale, aveți nevoie de:
tăiere structurată (cu îndepărtarea fizică a structurilor) sau biblioteci specifice rare
operațiuni. Tăierea nativă PyTorch este excelentă pentru experimentare și pentru QAT (Quantization-Aware
Antrenament) cu raritate, dar nu pentru desfășurare directă.
Tunderea Structurată cu Torch-Prunning
Biblioteca Torță-Tăiere (Fang et al., CVPR 2023) rezolvă problema de tăiere structurată reală: eliminarea unui filtru dintr-un strat Conv2D necesită și o actualizare următorul strat (care așteaptă N canale de intrare, nu N-k). Mânere de tăiere cu torță automat aceste dependențe printr-un grafic de dependență (DepGraph), susținând arhitecturi complexe, inclusiv ViT, LLM, YOLO și modele cu conexiuni de ignorare.
# 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
Tunderea în mișcare pentru transformatoare
Tăierea mărimii funcționează bine pentru CNN, dar Transformers prezintă o provocare diferită: Greutățile de atenție pot avea magnitudini mici, dar sunt critice pentru comportamentul model. The tăierea în mișcare (Sanh et al., 2020) abordează această problemă cu o abordare radical diferită: în loc să îndepărteze greutăţi mic, elimină cei care sunt apropiindu-se de zero în timpul reglajului fin. Cu alte cuvinte, criteriul și gradientul de greutate în raport cu ținta de tăiere, nu valoarea curentă a greutății.
Tăierea în mișcare a demonstrat beneficii semnificative pentru tăierea modelelor BERT: la o rată redusă (80-97%), tăierea în mișcare depășește mărimea tăierii cu 10-20 de puncte procente pe benchmark-uri NLP, cum ar fi MNLI și SQuAD.
# 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")
Ipoteza biletului de loterie: teoria submodelului câștigător
La Ipoteza biletului de loterie (LTH, Frankle & Carlin, NeurIPS 2019) și unul dintre Cele mai influente descoperiri teoretice în tăiere: fiecare rețea neuronală densă conține una sau mai multe subrețele rare („bilete câștigătoare”) care, dacă sunt extrase și reinițializate cu valorile lor inițiale inițiale, pot fi antrenați singuri atingând o precizie comparabilă sau superioară rețelei complete, în mai puțin sau în același timp de antrenament.
LTH are implicații practice importante: sugerează că modelul mare este în primul rând util pentru găsi structura potrivită, nu pentru capacitățile intrinseci ale parametrilor săi. Procesul standard pentru găsirea unui bilet câștigător esteTunderea iterativă a mărimii (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 în practică: limitări
- Costul de calcul: IMP necesită multe cicluri train-prune-reinit, făcându-l scump pentru modelele mari. Pentru LLM-uri, variante mai eficiente, cum ar fi GMP (Gradual Tăierea mărimii) care nu necesită reinițializare.
- Scalabilitate: LTH original funcționează pe modele mici. Pentru BERT și GPT, reinițializarea la ponderile inițiale nu produce beneficii clare; se folosește tăierea + reglarea fină pe greutățile curente.
- Transferați învățarea: Cercetările din 2020 (Chen și colab.) arată că „a câștiga biletele” ale modelelor pre-instruite precum BERT sunt transferabile la sarcinile din aval, de deschidere aplicatii interesante.
Flux de lucru de tăiere iterativă cu reinstruire
Cel mai eficient flux de lucru în producție nu este tăierea unică (eliminați imediat 50% din greutăți) dar cel tăierea iterativă cu recalificare: tăiați treptat, lăsând plasa timpul să „refacem” la fiecare pas. Acest lucru produce modele mult mai precise având în vedere aceeași raritate a țintei.
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
Tăiere + Cuantizare: Compresie maximă
Tunderea și cuantizarea sunt tehnici complementare și se combină eficient. Tunderea reduce numărul de parametri; cuantizarea reduce precizia fiecăruia parametrul rămas. Aplicate împreună, produc modele extrem de compacte. Această combinație este cunoscută ca "cuantificare rară" o „modele rare cuantizate”.
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
Criterii de referință: acuratețe, accelerare și memorie
Rezultatele tăierii variază semnificativ în funcție de model, sarcină și metodă. Următorul tabel prezintă valori de referință orientative pentru BERT-base și ResNet-50, pe baza rezultate din literatură și experimente practice:
| Model | Metodă | împrăștiat | Precizie | Accelerare | Memorie |
|---|---|---|---|---|---|
| bazat pe BERT (MNLI) | Linia de referință FP16 | 0% | 84,6% | 1,0x | 440 MB |
| bazat pe BERT (MNLI) | Magnitudine unstr. | 50% | 84,1% | 1,0x* | 440 MB* |
| bazat pe BERT (MNLI) | Tunderea în mișcare | 70% | 83,5% | 1,0x* | 440 MB* |
| bazat pe BERT (MNLI) | Tăierea capului 30% | 30% capete | 84,0% | 1,3x | 310 MB |
| bazat pe BERT (SQuAD) | Tunderea blocului str. | 50% | F1 -1% | 2,4x | 220 MB |
| ResNet-50 (ImageNet) | Tunderea cu filtru L1 | 40% | Top-1 -0,5% | 1,5x | -40% |
| ResNet-50 (ImageNet) | Tăiere iterativă | 70% | Top-1 -1,2% | 2,1x | -65% |
* Tăiere nestructurată: fără accelerare pe hardware standard fără operațiuni rare dedicate.
Recomandări în funcție de tipul de hardware țintă
- GPU NVIDIA standard: Preferați tăierea structurată (Tăierea cu torță, tăierea capului). Tăierea nestructurată nu este de niciun beneficiu fără un suport rar dedicat, dacă nu este utilizat Formatul NVIDIA Ampere 2:4 sparsity (50% sparsity în modele specifice 2 non-zero la fiecare 4).
- CPU (deducere de implementare): Tăieri nestructurate cu rarefiate mare (>80%) poate aduce o accelerare cu biblioteci precum Intel oneDNN sau cu conversia în format CSR/CSC. Dar tăierea structurată rămâne mai previzibilă.
- Dispozitive Edge (Jetson, Raspberry Pi): Tăiere structurată + cuantizare INT8 sau GGUF. Reducerea modelului este critică: chiar și de 2 ori mai puțini parametri pot face diferența între executabil și neexecutabil.
- Mobil (ARM): Utilizați biblioteci precum XNNPACK sau CoreML cu cuantizare INT8 și tăierea structurată pentru accelerarea hardware reală.
Cele mai bune practici și anti-modele
Cele mai bune practici pentru tăiere
- Utilizați tăierea iterativă, nu unică: Tunde 10-20% pe pas cu recalificare intermediar. O singură îndepărtare agresivă de 70% degradează aproape întotdeauna acuratețea cu o cantitate semnificativă ireversibile.
- Aplicați recalificare după fiecare pas: Chiar și 1-3 epoci de reglare fină mai târziu fiecare rundă de tăiere recuperează cea mai mare parte a preciziei pierdute. Rata de învățare trebuie să fie scăzut (de 10-100 de ori mai mic decât antrenamentul inițial).
- Alegeți metoda în funcție de hardware-ul țintă: Tăiere structurată pentru accelerare real pe hardware standard; nestructurat numai dacă aveți acces la hardware cu capacitate redusă.
- Nu tăiați straturile critice: Primul și ultimul strat al fiecărei rețele (încorporare, clasificator) sunt cele mai sensibile. Excludeți sau reduceți puternic tăierea pe aceste straturi.
- Monitorizați distribuția greutății în timpul tăierii: Dacă prea multe greutăți de una același strat sunt tăiate (>80%), stratul se poate prăbuși. Setați o limită minimă pentru fiecare strat.
- Evaluați valorile sarcinii, nu doar pierderile: Pierderea antrenamentului poate să nu fie capturată degradări pe carcase marginale. Utilizați valori specifice domeniului (F1, BLEU, precizie pe setul de testare).
Anti-modele de evitat
-
Nu vă așteptați la o accelerare de la tăierea nestructurată pe GPU-urile standard:
API-ul
torch.nn.utils.prunepune la zero greutățile, dar nu le îndepărtează fizic. Timpul de inferență nu scade fără operațiuni rare dedicate. -
Nu amestecați măști și greutăți fără a consolida: Înainte de a exporta sau
distribuiți modelul, sunați întotdeauna
prune.remove(module, 'weight')pentru consolidați masca în parametru. În caz contrar, modelul are și suprasarcina de memorie dependențe neportabile. - Nu utilizați un set de date de validare prea mic: Tăiere agresivă poate cauza supraadaptarea setului de validare utilizat pentru a monitoriza acuratețea. Folosiți a set de teste susținute pentru evaluarea finală.
- Nu ignora straturile de normalizare: BatchNorm și LayerNorm mențin statistici legate de dimensiunile straturilor anterioare. După tăierea structurată, cel Statisticile de normalizare trebuie recalibrate (reluare pe setul de date de calibrare).
- Nu aplicați tăierea pe modele neconvergente: Tunderea funcționează cel mai bine pe modele bine antrenate. Aplicând-o pe un model care nu a convergit încă randamentele rezultate imprevizibile.
Tunderea în 2025-2026: Stadiul tehnicii
Domeniul tăierii a evoluat semnificativ odată cu creșterea LLM-urilor. Principalele tendințe în 2025-2026 includ:
- SparseGPT și Wanda: Metode de tăiere unică pentru LLM-uri care nu necesită recalificare. SparseGPT (Frantar & Alistarh, 2023) folosește inversul aproximativ al matricei Hessian pentru a actualiza greutățile rămase, compensând eroarea de tăiere. Wanda (Sun și colab., 2023) utilizează ca criterii produsul mărimii greutății și normele de activare a intrării.
- Sparsitate 2:4 (NVIDIA): Model de sparsity structurat suportat hardware pe GPU Ampere și Hopper: exact 2 valori diferite de zero la fiecare 4 elemente. Produce accelerații de ~1,5-2x în operațiuni rare pe A100/H100 cu o precizie aproape identică cu modelul dens.
- CORP (2025): Tăierea structurată în formă închisă pentru Vision Transformers - se extinde de la DeiT-Tiny la DeiT-Huge cu accelerații hardware reale și minime pierderea preciziei.
- Tunderea + Distilarea: Combinarea tăierii cu distilarea cunoștințelor (articolul anterior din această serie) produce cele mai bune rezultate: vine modelul tăiat instruit cu supravegherea modelului original de profesor.
Concluzii
Tăierea rețelei neuronale este una dintre cele mai mature și versatile tehnici de compresie din lume învăţare profundă. Înțelegerea distincției dintre tăiere structurat e nestructurat și fundamental: primul produce accelerații reale pe hardware standard, acesta din urmă necesită un suport specific de sparsity, dar oferă mai multă flexibilitate.
Il tăierea iterativă cu recalificare rămâne standardul de aur pentru calitate rezultate. Acolo Ipoteza biletului de loterie oferă o perspectivă teoretică fundamentală de ce funcționează tăierea, în ciuda faptului că are limitări practice pentru modelele foarte mari. Pentru LLM-urile moderne, metode precum SparseGPT și Wanda oferă alternative viabile one-shot.
Combinația tăiere + cuantizare iar drumul principal la maxim compresie: reduceți numărul parametrilor și precizia lor numerică în mod complementar vă permite să obțineți modele cu o amprentă de 10-15x mai mică decât punctul de plecare, menținând Precizie acceptabilă pentru majoritatea cazurilor de utilizare în producție.
Următorii pași
- Articolul următor: Ollama: Rulați Local LLM pe laptop și Raspberry
- Articolul precedent: Modele de distilare: transfer de cunoștințe
- Înrudit: Modele de cuantizare: GPTQ, AWQ, INT8
- Înrudit: Reglaj fin cu LoRA și QLoRA
- Seria MLOps: Servirea și implementarea modelelor







