Învățare prin transfer: reutilizarea modelelor pre-instruite
Imaginează-ți că trebuie să înveți un copil să recunoască rasele de câini. Dacă acel copil a învățat deja pentru a recunoaște forme, culori, texturi și structuri anatomice generale, sarcina devine enorm mai mult simplu. Nu trebuie să începi de la zero: poți transfer cunoștințele deja dobândite la nou sarcina. Acesta este exact ceea ce Transfer de învățare în învăţarea profundă.
În acest al doilea articol al seriei Viziune pe computer cu învățare profundă, vom explora în profunzime despre Transfer Learning: de ce funcționează, ce strategii există, cum să alegeți modelul pre-instruit corect și cum să implementați conducte complete în PyTorch. Vom analiza un studiu de caz industrial realitatea și tehnicile avansate pe care profesioniștii le folosesc în fiecare zi.
Prezentare generală a seriei
| # | Articol | Concentrează-te |
|---|---|---|
| 1 | CNN: Rețele convoluționale | Arhitectură, instruire, implementare |
| 2 | Sunteți aici - Transfer Learning and Fine-Tuning | Modele pre-antrenate, adaptare domeniului |
| 3 | Detectarea obiectelor cu YOLO | Detectarea obiectelor în timp real |
| 4 | Segmentarea semantică | Clasificare la nivel de pixeli |
| 5 | Generare de imagini cu GAN și difuzie | Generarea de imagini sintetice |
| 6 | Implementare și optimizare Edge | Modele pe dispozitive încorporate |
Ce vei învăța
- Ce este Transfer Learning și de ce funcționează (ierarhii de caracteristici în CNN)
- Principalele strategii: extragerea caracteristicilor, reglajul fin, adaptarea domeniului
- Cum să alegi modelul pre-antrenat potrivit (ResNet, EfficientNet, ViT, ConvNeXt)
- Implementare completă în PyTorch: de la pregătirea datelor până la implementare
- Tehnici avansate: rate de învățare discriminative, dezghețare treptată, încălzire a ratei de învățare
- Mărirea datelor optimizată pentru Transfer Learning
- Studiu de caz practic: clasificarea defectelor industriale cu ResNet-50
- Transfer Learning aplicat la detectarea obiectelor (Faster R-CNN, YOLO)
- Greșeli frecvente și cum să le evitați
1. Ce este Transfer Learning
Il Transfer de învățare și o tehnică de învățare automată în care un model antrenat pe o sarcină (numită sarcina sursă) este refolosită ca punct de plecare pentru o altă sarcină (a spus sarcinile țintă). În loc să antrenați o rețea neuronală de la zero pe milioane de imagini, să luăm un model deja antrenat (de obicei pe ImageNet, 1,2 milioane de imagini în 1000 de clase) și o adaptăm la problema noastră specifică.
1.1 Analogia umană
Creierul nostru funcționează în mod constant în modul de învățare prin transfer. Un chirurg care învață ceva nou tehnica operațională nu trebuie să reînvețe anatomia, fiziologia și abilitățile manuale de bază. Un muzician clasic care trece la jazz transferă tehnica instrumentală, lectura partiturii și teoria armonică. Un programator Python care învață Rust transferă concepte de programare, depanare logica mentală și algoritmică. În toate aceste cazuri, cunoștințe anterioare accelera învăţând enorm noul domeniu.
1.2 de ce funcționează: Ierarhia caracteristicilor
Motivul fundamental pentru care Transfer Learning funcționează în CNN-uri constă în ierarhia caracteristicilor învăţat. Cercetătorii au demonstrat că CNN-urile instruiți pe ImageNet, învață caracteristici organizate în niveluri de abstractizare crescândă:
Ierarhia caracteristicilor în CNN-uri
| Straturi | Caracteristica învățată | Specificitate | Transferabilitate |
|---|---|---|---|
| Straturile 1-2 | Margini, colțuri, degrade de culoare | Generic (agnostic de sarcină) | Foarte sus |
| Straturile 3-4 | Texturi, modele repetate, motive geometrice | Semi-generic | Ridicat |
| Straturile 5-6 | Părți ale obiectelor (ochi, roți, ferestre) | Semispecific | Medie |
| Stratul 7+ | Obiecte complete, scene, compoziții | Specific sarcinii | Scăzut |
Primele straturi învață caracteristici universal: margini, texturi și degrade care sunt util pentru orice sarcină vizuală. Straturile intermediare captează modele mai complexe, dar totuși relativ generic. Numai ultimele straturi sunt foarte specifice sarcinii originale. Aceasta înseamnă că putem reutiliza cea mai mare parte a rețelei ca un puternic extractor de caracteristici și adaptăm doar părțile finale la sarcina noastră.
ImageNet Pre-trained CNN (es. ResNet-50):
Input Immagine
|
v
[Layer 1-2] ---> Bordi orizzontali, verticali, diagonali
| Gradienti di colore, blob
| UNIVERSALI: utili per QUALSIASI immagine
v
[Layer 3-4] ---> Texture (pelo, metallo, legno, tessuto)
| Pattern geometrici, griglie
| SEMI-GENERICHE: utili per molti domini
v
[Layer 5-6] ---> Parti di oggetti (occhi, ruote, ali)
| Composizioni locali
| SEMI-SPECIFICHE: dipendono dal dominio
v
[Layer 7+] ---> Classi intere (gatto, auto, uccello)
| Feature altamente specifiche per ImageNet
| TASK-SPECIFICHE: da sostituire/adattare
v
[Classifier] ---> 1000 classi ImageNet
SEMPRE da sostituire per il tuo task
Definiție formală
Dat un domeniu sursă D_s cu sarcini T_s și un domeniu țintă D_t cu sarcini T_t, Transfer Learning își propune să îmbunătățească funcția de învățare f_t în domeniul țintă folosind cunoștințe extras din D_s și T_s, unde D_s != D_t sau T_s != T_t. Practic, greutățile theta învățate de la sarcina sursă sunt utilizate ca inițializare (theta_0) pentru antrenament pe sarcina țintă, în loc de inițializare aleatorie.
2. Transfer de strategii de învățare
Nu există o modalitate unică de a aplica Transfer Learning. Strategia optimă depinde de dimensiunea setului de date țintă, asemănarea acestuia cu setul de date sursă și resurse resursele de calcul disponibile. Să ne uităm la cele patru strategii principale.
2.1 Extragerea caracteristicilor (Freeze Backbone, Train Classifier)
Cea mai simplă și directă strategie: da îngheață (înghețare) întreaga rețea pre-antrenată și este folosit ca un extractor de caracteristici fixe. Singura parte care este antrenată este una nouă clasificator adăugat deasupra. Greutățile coloanei vertebrale nu sunt actualizate în timpul antrenamentului.
Când să-l folosești: Set de date țintă mic (sute/cateva mii de imagini) e domeniu similar cu sursa (de exemplu, clasificarea raselor de câini atunci când modelul este antrenat în prealabil pe ImageNet care conţine multe imagini cu câini).
Pre-trained ResNet-50 (ImageNet):
+---------------------------------------------------+
| [Conv layers] --> [Res blocks] --> [Global AvgPool] | CONGELATO (frozen)
| 25.5M parametri - NON aggiornati | - requires_grad = False
+---------------------------------------------------+
|
v
Feature vector (2048-dim)
|
v
+-------------------+
| [Linear 2048->N] | ADDESTRATO (trainable)
| N = tue classi | - requires_grad = True
+-------------------+
|
v
Output: N classi
Vantaggi:
+ Training velocissimo (pochi parametri da ottimizzare)
+ Non richiede GPU potente
+ Rischio minimo di overfitting
+ Bastano pochi dati
Svantaggi:
- Meno flessibile (le feature sono fisse)
- Performance limitata se il dominio e molto diverso
2.2 Ajustare fină (Deblocați unele/Toate straturile)
La reglaj fin, după inițializarea rețelei cu greutăți pre-antrenate, da se dezgheț unele sau toate straturile și reantrenați întreaga rețea (sau o parte a acesteia) cu o rată de învățare foarte scăzută. Straturile pre-antrenate sunt ușor actualizate pentru a se potrivi la noul domeniu, păstrând cunoștințele deja dobândite.
Când să-l folosești: Set de date țintă mediu-mare (mii-zeci de mii de imagini) și/sau domeniu moderat diferit de sursă.
Strategia di Fine-Tuning Progressivo:
Fase 1 - Feature Extraction (5-10 epochs):
[Backbone CONGELATO] --> [Nuovo Classifier] ADDESTRATO (lr=1e-3)
Fase 2 - Partial Fine-Tuning (10-20 epochs):
[Layer 1-3 CONGELATI] --> [Layer 4 SCONGELATO lr=1e-5] --> [Classifier lr=1e-4]
Fase 3 - Full Fine-Tuning (opzionale, 5-10 epochs):
[TUTTI i layer SCONGELATI lr=1e-6] --> [Classifier lr=1e-5]
Learning Rates progressivi:
Layer iniziali: lr = 1e-6 (feature generiche, cambiare poco)
Layer intermedi: lr = 1e-5 (adattare gradualmente)
Layer finali: lr = 1e-4 (adattare al nuovo dominio)
Classifier: lr = 1e-3 (apprendere da zero)
2.3 Adaptarea domeniului
La Adaptarea domeniului și o formă specializată de Transfer Learning folosită atunci când domeniul sursă și domeniul țintă au aceleași clase, dar au distribuții de date diferite. De exemplu, un model instruit pe fotografii profesionale de produse trebuie să funcționeze pe fotografiile făcute la fabrică cu iluminare variabilă. Tehnici precum LA NAIBA (Domain-Adversarial Neural Network) adăugați un discriminator de domeniu care forțează rețeaua pentru a învăța caracteristicile invariante de domeniu.
2.4 Transfer Zero-Shot și Few-Shot
Odată cu apariția modelelor ca CLIP (Pre-antrenament contrastant limbaj-imagine), Este posibil să clasificați imaginile în categorii niciodata vazuta în timpul antrenamentului (zero-shot) sau cu foarte puține exemple (puţine-împuşcat). CLIP învață o reprezentare comună text-imagine: dat un mesaj text, cum ar fi „o fotografie a unui defect sudare”, modelul poate clasifica imagini fără nicio pregătire specifică.
Compararea strategiilor de învățare prin transfer
| Strategie | Datele necesare | Timp de antrenament | Performanţă | Risc de supraadaptare |
|---|---|---|---|---|
| Extragerea caracteristicilor | 100-1000 | Minute | Bun | Foarte scăzut |
| Reglaj fin parțial | 1000-10000 | Ore | Foarte bun | Bas |
| Reglaj fin complet | 10000+ | Ore-Zile | Excelent | Mediu |
| Adaptarea domeniului | Variabilă | Ore | Bine-Excelent | Mediu |
| Zero-Shot (CLIP) | 0 | Nimeni | Variabilă | Nimeni |
3. Modele pre-instruite pentru computer Vision
Alegerea modelului pre-antrenat este o decizie crucială. Fiecare arhitectură are compromisuri diferă între precizie, viteza de inferență, dimensiunea modelului și cerințele de memorie. Iată o prezentare generală a celor mai folosite modele în 2025-2026.
Tabel de comparație Modele pre-antrenate
| Model | Parametrii | ImageNet Top-1 | Tip | Utilizare ideală |
|---|---|---|---|---|
| ResNet-50 | 25,6 milioane | 76,1% (v1) / 80,9% (v2) | CNN | Linie de bază solidă, implementare ușoară |
| EfficientNet-B0 | 5,3 milioane | 77,1% | CNN | Mobil, margine, resurse limitate |
| EfficientNet-B7 | 66M | 84,3% | CNN | Precizie maximă CNN |
| ViT-B/16 | 86M | 77,9% (ImageNet-1k) | Transformatoare | Seturi mari de date, pregătire preliminară la scară largă |
| ConvNeXt-T | 28,6 milioane | 82,1% | CNN modern | Cel mai bun compromis precizie/viteză |
| ConvNeXt-B | 88,6 milioane | 83,8% | CNN modern | Când aveți nevoie de precizie ridicată cu CNN |
| Swin-T | 28,3 milioane | 81,3% | Transformatoare | Detectare și segmentare |
| CLIP ViT-B/32 | 151M (vizualizare) | 63,2% (zero-shot) | Multimodal | Zero-shot, căutare vizuală |
| DINOv2 ViT-S/14 | 22M | 81,1% (sondă liniară) | Autosupravegheat | Caracteristici generice, puține date etichetate |
3.1 ResNet-50: Calul de lucru
ResNet-50 rămâne cel mai popular model pentru Transfer Learning datorită acestuia simplitate, stabilitate în antrenament și suport extins pentru ecosistem. Omite conexiunile (introdus în articolul anterior) vă permit să antrenați rețele profunde fără probleme de gradient. Versiunea V2 a greutăților (IMAGENET1K_V2), antrenată cu tehnici moderne precum Mixup, CutMix și Random Erasing, atinge un impresionant top-1 de 80,9%.
3.2 EfficientNet: Scalabilitate compusă
Familia EfficientNet folosește o metodă de scalare compusă ce scara uniform adâncimea, lățimea și rezoluția rețelei. EfficientNet-B0 este ideal pentru dispozitive cu resurse limitate (5,3 milioane de parametri), în timp ce B7 oferă cea mai mare precizie (84,3%) cu prețul unui model mult mai mare (66M parametri).
3.3 Vision Transformer (ViT) și Swin Transformer
I Transformatoare de vedere aplicați arhitectura Transformer (inițial creat pentru NLP) la viziunea computerizată. Imaginea este împărțită în patch-uri (de exemplu, 16x16 pixeli), fiecare plasture este tratat ca un „token” și procesat cu atenție personală. ViT excel atunci când sunt pre-antrenați pe seturi de date mari (ImageNet-21k, JFT-300M), dar poate fi mai puțin eficient cu seturi de date mici în comparație cu CNN-urile. Swin Transformer introduce atenție la ferestrele glisante (geamuri deplasate), făcându-l mai eficient și mai ales potrivit pentru sarcini dense, cum ar fi detectarea și segmentarea.
3.4 ConvNeXt: CNN modernizat
ConvNext demonstrează că CNN-urile pot concura cu Transformers dacă sunt modernizate cu aceleași tehnici de antrenament (AdamW, Mixup, layer scale, Stochastic Depth). ConvNeXt-T atinge 82,1% cu doar 28,6 milioane de parametri, oferind un compromis excelent între precizie, viteza și simplitatea implementării.
3.5 DINOv2: Învățare auto-supravegheată
DINOv2 și un model antrenat cu învățare auto-supravegheată (fără etichete) pe un set de date uriaș (imagini LVD-142M). Caracteristicile extrase sunt extrem de generice și transferabil: un clasificator liniar simplu adăugat deasupra obține rezultate competitiv cu reglarea fină completă a modelelor supravegheate. Și mai ales util când aveți puține date etichetate în domeniul țintă.
4. Când să folosiți Transfer Learning: The Decision Matrix
Alegerea strategiei depinde de doi factori cheie: dimensiunea setului de date tinta iar cel similitudine între domeniul sursă și domeniul țintă. Aceasta generează patru cadrane de decizie.
SOMIGLIANZA CON SOURCE DOMAIN
Alta Bassa
+-------------------------+-------------------------+
| | |
Grande | QUADRANTE 1 | QUADRANTE 2 |
(10k+) | Fine-tuning completo | Fine-tuning con |
| - Unfreeze tutti i | cautela |
D | layer | - Unfreeze solo layer |
A | - Learning rate basso | finali |
T | - Alta performance | - Learning rate molto |
A | | basso per backbone |
S | Es: Razze di cani | - Augmentation forte |
E | (ImageNet contiene | |
T | già molti cani) | Es: Immagini medicali |
| | (molto diverse da |
S +-------------------------+ ImageNet) |
I | +-------------------------+
Z | QUADRANTE 3 | QUADRANTE 4 |
E | Feature Extraction | Opzioni limitate |
| - Freeze backbone | - Prova feature |
Piccolo | - Solo train | extraction |
(100-1k) | classifier | - Augmentation molto |
| - No overfitting | aggressiva |
| - Training veloce | - Considera raccolta |
| | più dati |
| Es: 200 foto di fiori | - DINOv2 / CLIP |
| (simili a ImageNet) | (self-supervised) |
+-------------------------+-------------------------+
Regulă practică
În 2025-2026, răspunsul la întrebarea „Ar trebui să folosesc Transfer Learning?” și aproape întotdeauna si. Antrenarea unui CNN de la zero și justificată doar în cazuri foarte specifice: Seturi uriașe de date (milioane de imagini), domeniu radical diferit de imaginile naturale (de exemplu, spectrograme, semnale radar) sau anumite constrângeri arhitecturale.
5. Implementare cu PyTorch
Să trecem la practică. Vom implementa Transfer Learning pas cu pas în PyTorch, începând de la încărcarea unui model pre-antrenat până la antrenamentul complet.
5.1 Încărcarea unui model pre-antrenat
PyTorch oferă două API-uri pentru încărcarea modelelor pre-antrenate. API-ul modern (introdus în
torchvision 0.13+) folosește enumerarea Weights care oferă informații detaliate
asupra greutăților, inclusiv transformările de preprocesare necesare.
import torch
import torch.nn as nn
from torchvision import models
from torchvision.models import ResNet50_Weights
# ---- API moderna (raccomandata): usa l'enum Weights ----
# Carica ResNet-50 con pesi IMAGENET1K_V2 (80.9% top-1)
weights = ResNet50_Weights.IMAGENET1K_V2
model = models.resnet50(weights=weights)
# I pesi includono le trasformazioni di preprocessing
preprocess = weights.transforms()
# Questo crea automaticamente: Resize(232) -> CenterCrop(224)
# -> ToTensor() -> Normalize(mean, std)
print(f"Modello caricato: {sum(p.numel() for p in model.parameters()):,} parametri")
print(f"Device: {next(model.parameters()).device}")
# ---- API legacy (deprecata ma ancora funzionante) ----
# model = models.resnet50(pretrained=True) # NON usare più
# ---- Ispeziona la struttura del modello ----
# ResNet-50 ha: conv1, bn1, relu, maxpool, layer1-4, avgpool, fc
print(model)
5.2 Extragerea caracteristicilor: Înghețați și înlocuiți clasificatorul
Pentru a folosi ResNet-50 ca extract de caracteristici, trebuie să: (1) să înghețăm toți parametrii backbone, (2) înlocuiți stratul final complet conectat cu unul nou potrivit pentru numărul nostru de clase.
import torch
import torch.nn as nn
from torchvision import models
from torchvision.models import ResNet50_Weights
def create_feature_extractor(num_classes: int, device: str = 'cuda') -> nn.Module:
"""Crea un feature extractor basato su ResNet-50 pre-trained.
Args:
num_classes: Numero di classi per il task target
device: Device su cui caricare il modello
Returns:
Modello con backbone congelato e classificatore trainable
"""
# 1. Carica il modello pre-addestrato
weights = ResNet50_Weights.IMAGENET1K_V2
model = models.resnet50(weights=weights)
# 2. CONGELA tutti i parametri del backbone
for param in model.parameters():
param.requires_grad = False
# 3. Sostituisci il classificatore finale
# Il layer fc originale: Linear(2048 -> 1000)
num_features = model.fc.in_features # 2048
model.fc = nn.Sequential(
nn.Linear(num_features, 512),
nn.ReLU(inplace=True),
nn.Dropout(0.3),
nn.Linear(512, num_classes)
)
# I nuovi layer hanno requires_grad=True di default
# 4. Sposta sul device
model = model.to(device)
# Verifica parametri trainable vs congelati
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
total = sum(p.numel() for p in model.parameters())
frozen = total - trainable
print(f"Parametri totali: {total:,}")
print(f"Parametri congelati: {frozen:,} ({frozen/total*100:.1f}%)")
print(f"Parametri trainable: {trainable:,} ({trainable/total*100:.1f}%)")
return model
# Esempio: classificazione binaria (difettoso / non difettoso)
model = create_feature_extractor(num_classes=2)
# Output:
# Parametri totali: 24,607,170
# Parametri congelati: 23,508,032 (95.5%)
# Parametri trainable: 1,099,138 (4.5%)
5.3 Augmentarea datelor pentru transfer de învățare
Mărirea datelor este fundamentală în Transfer Learning, în special cu seturi de date mici. Transformările trebuie să fie compatibile cu preprocesarea modelului pre-antrenat: în special, normalizarea trebuie să utilizeze media ImageNet și abaterea standard (medie = [0,485, 0,456, 0,406], std = [0,229, 0,224, 0,225]).
from torchvision import transforms
from torchvision.transforms import v2 # API v2 (PyTorch 2.0+)
from torch.utils.data import DataLoader
from torchvision.datasets import ImageFolder
# ---- Normalizzazione ImageNet (OBBLIGATORIA per modelli pre-trained) ----
IMAGENET_MEAN = [0.485, 0.456, 0.406]
IMAGENET_STD = [0.229, 0.224, 0.225]
# ---- Trasformazioni per il training (con augmentation) ----
train_transforms = transforms.Compose([
transforms.RandomResizedCrop(224, scale=(0.7, 1.0)),
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomVerticalFlip(p=0.2),
transforms.RandomRotation(degrees=15),
transforms.ColorJitter(
brightness=0.3,
contrast=0.3,
saturation=0.2,
hue=0.1
),
transforms.RandomAffine(degrees=0, translate=(0.1, 0.1)),
transforms.RandomGrayscale(p=0.05),
transforms.ToTensor(),
transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),
transforms.RandomErasing(p=0.2, scale=(0.02, 0.15)),
])
# ---- Trasformazioni per validazione/test (SENZA augmentation) ----
val_transforms = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),
])
# ---- Caricamento dataset con ImageFolder ----
# Struttura directory richiesta:
# data/
# train/
# classe_A/ (img1.jpg, img2.jpg, ...)
# classe_B/ (img1.jpg, img2.jpg, ...)
# val/
# classe_A/ (...)
# classe_B/ (...)
train_dataset = ImageFolder(root='data/train', transform=train_transforms)
val_dataset = ImageFolder(root='data/val', transform=val_transforms)
train_loader = DataLoader(
train_dataset,
batch_size=32,
shuffle=True,
num_workers=4,
pin_memory=True,
drop_last=True
)
val_loader = DataLoader(
val_dataset,
batch_size=64,
shuffle=False,
num_workers=4,
pin_memory=True
)
print(f"Classi: {train_dataset.classes}")
print(f"Training: {len(train_dataset)} immagini")
print(f"Validation: {len(val_dataset)} immagini")
RandAugment și TrivialAugmentWide
Pentru a simplifica alegerea creșterilor, PyTorch oferă politici automate.
RandAugment aplicați N transformări aleatoare cu intensitatea M.
TrivialAugmentWide aplică o singură transformare cu intensitate aleatorie,
și este adesea mai eficient decât strategiile complexe. Doar înlocuiți transformările manuale
cu o linie:
transforms.TrivialAugmentWide() sau
transforms.RandAugment(num_ops=2, magnitude=9).
5.4 Ciclul de antrenament complet
Bucla de antrenament pentru Transfer Learning este similară cu cea standard, dar cu câteva precauții: rate de învățare mai scăzute, încălzire treptată și monitorizare atentă a supraajustării.
import torch
import torch.nn as nn
from torch.optim import AdamW
from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts
from typing import Tuple
import time
def train_one_epoch(
model: nn.Module,
train_loader,
criterion: nn.Module,
optimizer: torch.optim.Optimizer,
device: str,
epoch: int
) -> Tuple[float, float]:
"""Addestra il modello per un'epoca."""
model.train()
running_loss = 0.0
correct = 0
total = 0
for batch_idx, (images, labels) in enumerate(train_loader):
images = images.to(device, non_blocking=True)
labels = labels.to(device, non_blocking=True)
# Forward pass
outputs = model(images)
loss = criterion(outputs, labels)
# Backward pass
optimizer.zero_grad(set_to_none=True) # Più efficiente di zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
# Metriche
running_loss += loss.item() * images.size(0)
_, predicted = outputs.max(1)
total += labels.size(0)
correct += predicted.eq(labels).sum().item()
epoch_loss = running_loss / total
epoch_acc = 100.0 * correct / total
return epoch_loss, epoch_acc
@torch.no_grad()
def evaluate(
model: nn.Module,
val_loader,
criterion: nn.Module,
device: str
) -> Tuple[float, float]:
"""Valuta il modello sul validation set."""
model.eval()
running_loss = 0.0
correct = 0
total = 0
for images, labels in val_loader:
images = images.to(device, non_blocking=True)
labels = labels.to(device, non_blocking=True)
outputs = model(images)
loss = criterion(outputs, labels)
running_loss += loss.item() * images.size(0)
_, predicted = outputs.max(1)
total += labels.size(0)
correct += predicted.eq(labels).sum().item()
val_loss = running_loss / total
val_acc = 100.0 * correct / total
return val_loss, val_acc
def train_transfer_learning(
model: nn.Module,
train_loader,
val_loader,
num_epochs: int = 30,
lr: float = 1e-3,
device: str = 'cuda'
) -> dict:
"""Pipeline completa di training per Transfer Learning."""
model = model.to(device)
# Ottimizzatore: solo parametri trainable (non congelati)
trainable_params = [p for p in model.parameters() if p.requires_grad]
optimizer = AdamW(trainable_params, lr=lr, weight_decay=1e-4)
# Scheduler: cosine annealing con warm restarts
scheduler = CosineAnnealingWarmRestarts(optimizer, T_0=10, T_mult=2)
# Loss function con label smoothing
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
# Tracking
best_val_acc = 0.0
history = {'train_loss': [], 'val_loss': [], 'train_acc': [], 'val_acc': []}
for epoch in range(num_epochs):
start_time = time.time()
# Training
train_loss, train_acc = train_one_epoch(
model, train_loader, criterion, optimizer, device, epoch
)
# Validation
val_loss, val_acc = evaluate(model, val_loader, criterion, device)
# Scheduler step
scheduler.step()
# Salvataggio metriche
history['train_loss'].append(train_loss)
history['val_loss'].append(val_loss)
history['train_acc'].append(train_acc)
history['val_acc'].append(val_acc)
elapsed = time.time() - start_time
print(
f"Epoch {epoch+1}/{num_epochs} "
f"[{elapsed:.1f}s] "
f"Train: {train_loss:.4f} ({train_acc:.1f}%) | "
f"Val: {val_loss:.4f} ({val_acc:.1f}%)"
)
# Salva il miglior modello
if val_acc > best_val_acc:
best_val_acc = val_acc
torch.save({
'epoch': epoch,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'val_acc': val_acc,
}, 'best_model.pth')
print(f" --> Nuovo miglior modello salvato! Val Acc: {val_acc:.2f}%")
print(f"\nMiglior Val Accuracy: {best_val_acc:.2f}%")
return history
6. Reglare fină avansată
Reglarea fină de bază (deblocarea tuturor straturilor cu o singură rată de învățare) adesea nu și strategia optimă. Tehnicile avansate vă permit să obțineți performanțe semnificative mai bine, mai ales cu seturi de date de dimensiuni medii.
6.1 Rate discriminatorii de învățare
Ideea este simplă, dar puternică: atribuiți rate diferite de învățare la grupuri de straturi diferite. Straturile inițiale (care au învățat caracteristici generice) trebuie actualizate foarte putin, straturile intermediare putin mai mult si clasificatorul final cu rata de invatare mai înalt. Acest lucru păstrează cunoștințele din straturile inițiale în timp ce le adaptează pe cele finale.
from torchvision import models
from torchvision.models import ResNet50_Weights
import torch.nn as nn
from torch.optim import AdamW
def create_finetuning_model(num_classes: int, device: str = 'cuda'):
"""Crea modello con gruppi di parametri per discriminative LR."""
weights = ResNet50_Weights.IMAGENET1K_V2
model = models.resnet50(weights=weights)
# Sostituisci il classificatore
num_features = model.fc.in_features
model.fc = nn.Sequential(
nn.Linear(num_features, 512),
nn.ReLU(inplace=True),
nn.Dropout(0.3),
nn.Linear(512, num_classes)
)
model = model.to(device)
# Definisci gruppi di parametri con LR diversi
# Gruppo 1: Layer iniziali (conv1, bn1, layer1, layer2) - LR molto basso
# Gruppo 2: Layer intermedi (layer3) - LR medio
# Gruppo 3: Layer finali (layer4) - LR più alto
# Gruppo 4: Classificatore (fc) - LR massimo
base_lr = 1e-4
param_groups = [
{
'params': list(model.conv1.parameters()) +
list(model.bn1.parameters()) +
list(model.layer1.parameters()) +
list(model.layer2.parameters()),
'lr': base_lr * 0.01, # 1e-6
'name': 'early_layers'
},
{
'params': list(model.layer3.parameters()),
'lr': base_lr * 0.1, # 1e-5
'name': 'mid_layers'
},
{
'params': list(model.layer4.parameters()),
'lr': base_lr, # 1e-4
'name': 'late_layers'
},
{
'params': list(model.fc.parameters()),
'lr': base_lr * 10, # 1e-3
'name': 'classifier'
},
]
optimizer = AdamW(param_groups, weight_decay=1e-4)
# Verifica i learning rate
for group in optimizer.param_groups:
n_params = sum(p.numel() for p in group['params'])
print(f"{group.get('name', 'unknown'):<15} LR={group['lr']:.2e} "
f"Params={n_params:,}")
return model, optimizer
6.2 Dezghețare treptată
În loc să dezghețe toate straturile deodată, ea dezghețare treptată se trece de la ultimele straturi la primul, epocă după epocă. Acest lucru dă timp clasificatorului pentru adaptați înainte de a modifica caracteristicile de bază, evitând actualizările distructive.
import torch.nn as nn
from torchvision import models
from torchvision.models import ResNet50_Weights
def gradual_unfreeze_training(
num_classes: int,
train_loader,
val_loader,
device: str = 'cuda'
):
"""Training con scongelamento graduale dei layer."""
# Carica modello e congela tutto
weights = ResNet50_Weights.IMAGENET1K_V2
model = models.resnet50(weights=weights)
# Congela tutti i layer
for param in model.parameters():
param.requires_grad = False
# Sostituisci il classificatore (trainable di default)
model.fc = nn.Sequential(
nn.Linear(model.fc.in_features, 512),
nn.ReLU(inplace=True),
nn.Dropout(0.3),
nn.Linear(512, num_classes)
)
model = model.to(device)
# Definisci le fasi di unfreezing
unfreeze_schedule = [
# (epoch_start, layers_to_unfreeze, learning_rate)
(0, [], 1e-3), # Solo classifier
(5, [model.layer4], 5e-4), # + layer4
(10, [model.layer3], 2e-4), # + layer3
(15, [model.layer2], 1e-4), # + layer2
(20, [model.layer1, model.conv1, model.bn1], 5e-5), # Tutto
]
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
current_phase = 0
for epoch in range(30):
# Controlla se dobbiamo scongeleare nuovi layer
if (current_phase < len(unfreeze_schedule) - 1 and
epoch >= unfreeze_schedule[current_phase + 1][0]):
current_phase += 1
phase = unfreeze_schedule[current_phase]
# Scongela i nuovi layer
for layer_group in phase[1]:
for param in layer_group.parameters():
param.requires_grad = True
# Ricrea optimizer con nuovo LR
trainable_params = [
p for p in model.parameters() if p.requires_grad
]
optimizer = torch.optim.AdamW(
trainable_params, lr=phase[2], weight_decay=1e-4
)
n_trainable = sum(
p.numel() for p in model.parameters() if p.requires_grad
)
print(f"\n=== Fase {current_phase}: Epoch {epoch} ===")
print(f"Parametri trainable: {n_trainable:,}")
print(f"Learning rate: {phase[2]:.2e}")
elif epoch == 0:
# Prima fase: solo classifier
optimizer = torch.optim.AdamW(
model.fc.parameters(), lr=1e-3, weight_decay=1e-4
)
# Training e validation standard
# (usa le funzioni train_one_epoch e evaluate definite prima)
train_loss, train_acc = train_one_epoch(
model, train_loader, criterion, optimizer, device, epoch
)
val_loss, val_acc = evaluate(
model, val_loader, criterion, device
)
print(
f"Epoch {epoch+1}/30 | "
f"Train: {train_loss:.4f} ({train_acc:.1f}%) | "
f"Val: {val_loss:.4f} ({val_acc:.1f}%)"
)
6.3 Încălzirea ratei de învățare și recoacerea cosinusului
O încălzire treptată a ratei de învățare în primele epoci previne actualizările prea agresive care ar putea distruge greutățile pre-antrenate. După încălzire, un mic program de recoacere reduce treptat rata de învățare pentru o reglare fină din ce în ce mai delicată.
import math
from torch.optim.lr_scheduler import LambdaLR
def get_cosine_schedule_with_warmup(
optimizer,
num_warmup_steps: int,
num_training_steps: int,
min_lr_ratio: float = 0.01
):
"""Cosine annealing con linear warmup.
Args:
optimizer: L'ottimizzatore PyTorch
num_warmup_steps: Numero di step di warmup
num_training_steps: Numero totale di step di training
min_lr_ratio: Rapporto LR minimo / LR massimo
Returns:
LambdaLR scheduler
"""
def lr_lambda(current_step: int) -> float:
# Fase di warmup: LR cresce linearmente da 0 a max
if current_step < num_warmup_steps:
return float(current_step) / float(max(1, num_warmup_steps))
# Fase di cosine annealing: LR decresce con coseno
progress = float(current_step - num_warmup_steps) / float(
max(1, num_training_steps - num_warmup_steps)
)
cosine_decay = 0.5 * (1.0 + math.cos(math.pi * progress))
return max(min_lr_ratio, cosine_decay)
return LambdaLR(optimizer, lr_lambda)
# Esempio di utilizzo
num_epochs = 30
steps_per_epoch = len(train_loader)
total_steps = num_epochs * steps_per_epoch
warmup_steps = 3 * steps_per_epoch # 3 epoche di warmup
scheduler = get_cosine_schedule_with_warmup(
optimizer,
num_warmup_steps=warmup_steps,
num_training_steps=total_steps
)
# Nel training loop, chiama scheduler.step() DOPO ogni batch
# for batch in train_loader:
# ...
# optimizer.step()
# scheduler.step() # Step per batch, non per epoca
Learning Rate durante il Training (warmup + cosine annealing):
LR
^
| ___
| / \
| / \
| / \
| / \
| / \___
| / \___
| / \___
|/ \___
+--+--------+--------+--------+--------+---> Epoch
0 warmup 5 10 20 30
Fase 1 (Epoch 0-3): Linear Warmup
LR cresce linearmente da ~0 a LR_max
Fase 2 (Epoch 3-30): Cosine Annealing
LR decresce seguendo una curva coseno fino a min_lr
6.4 Mixup și CutMix Augmentation
Confuzii e CutMix sunt tehnici avansate de augmentare deosebit de eficient în reglaj fin. Mixup creează noi mostre ca o combinație liniară a două imagini și etichetele lor corespunzătoare. CutMix taie și lipește dreptunghiuri între diferite imagini. Ambele funcționează ca regulatori puternici și îmbunătățesc generalizarea.
from torchvision.transforms import v2
# CutMix e MixUp applicati a livello di batch (non di singola immagine)
cutmix = v2.CutMix(num_classes=10)
mixup = v2.MixUp(num_classes=10, alpha=0.2)
# Combina entrambi con probabilità 50/50
cutmix_or_mixup = v2.RandomChoice([cutmix, mixup])
# Nel training loop:
# for images, labels in train_loader:
# images, labels = cutmix_or_mixup(images, labels)
# # Nota: labels ora sono soft (one-hot probabilistico)
# # La CrossEntropyLoss gestisce automaticamente soft labels
# outputs = model(images)
# loss = criterion(outputs, labels)
7. Augmentarea avansată a datelor pentru învățare prin transfer
Alegerea creșterii datelor are un impact uriaș asupra performanței, mai ales cu seturi de date mici. Să vedem cele mai eficiente opțiuni în 2025-2026.
7.1 Albumentații: creșterea nivelului profesional
Albumentații și o bibliotecă specializată pentru creșterea datelor de imagine, semnificativ mai rapid decât torchvision.se transformă datorită utilizării OpenCV. Oferă transformări deosebit de utile pentru domenii specializate (medical, industrial, satelit).
import albumentations as A
from albumentations.pytorch import ToTensorV2
import cv2
import numpy as np
from torch.utils.data import Dataset
from PIL import Image
def get_albumentations_transforms(is_training: bool = True):
"""Pipeline di augmentation con Albumentations."""
if is_training:
return A.Compose([
A.RandomResizedCrop(height=224, width=224, scale=(0.7, 1.0)),
A.HorizontalFlip(p=0.5),
A.VerticalFlip(p=0.2),
# Augmentation geometriche
A.ShiftScaleRotate(
shift_limit=0.1,
scale_limit=0.15,
rotate_limit=15,
border_mode=cv2.BORDER_REFLECT_101,
p=0.5
),
# Augmentation di colore/illuminazione
A.OneOf([
A.RandomBrightnessContrast(
brightness_limit=0.3, contrast_limit=0.3, p=1.0
),
A.HueSaturationValue(
hue_shift_limit=10, sat_shift_limit=30,
val_shift_limit=20, p=1.0
),
A.CLAHE(clip_limit=4.0, p=1.0),
], p=0.7),
# Rumore e blur
A.OneOf([
A.GaussNoise(var_limit=(10, 50), p=1.0),
A.GaussianBlur(blur_limit=(3, 5), p=1.0),
A.MotionBlur(blur_limit=5, p=1.0),
], p=0.3),
# Dropout spaziale
A.CoarseDropout(
max_holes=8, max_height=16, max_width=16,
min_holes=1, min_height=8, min_width=8,
fill_value=0, p=0.3
),
# Normalizzazione ImageNet
A.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]
),
ToTensorV2(),
])
else:
return A.Compose([
A.Resize(256, 256),
A.CenterCrop(224, 224),
A.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]
),
ToTensorV2(),
])
class AlbumentationsDataset(Dataset):
"""Dataset wrapper che usa Albumentations per le trasformazioni."""
def __init__(self, image_paths: list, labels: list, transform=None):
self.image_paths = image_paths
self.labels = labels
self.transform = transform
def __len__(self) -> int:
return len(self.image_paths)
def __getitem__(self, idx: int):
# Leggi immagine con OpenCV (BGR -> RGB)
image = cv2.imread(self.image_paths[idx])
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
label = self.labels[idx]
if self.transform:
augmented = self.transform(image=image)
image = augmented['image']
return image, label
8. Studiu de caz: Clasificarea defectelor industriale cu ResNet-50
Să punem totul împreună cu un caz real de utilizare: un sistem industrial de control al calității care clasifică defectele componentelor electronice. Setul de date conține imagini de înaltă rezoluție de plăci de circuite imprimate (PCB) cu patru clase: OK (fara defecte), punte_de lipire (punte de sudura), lipsă_componentă (componentă lipsă) e zgâria (zgâria).
Configurarea studiului de caz
- Seturi de date: 4000 de imagini (1000 per clasă), împărțite 70/15/15
- Rezoluţie: 512x512 pixeli, RGB
- Model: ResNet-50 pre-antrenat pe ImageNet (greutăți V2)
- Strategie: Extragerea caracteristicilor -> Reglaj fin treptat
- Hardware: GPU NVIDIA cu 8 GB+ VRAM
"""
Pipeline completa per classificazione difetti industriali.
Transfer Learning con ResNet-50 e gradual unfreezing.
"""
import torch
import torch.nn as nn
from torch.optim import AdamW
from torch.utils.data import DataLoader
from torchvision import models, transforms
from torchvision.models import ResNet50_Weights
from torchvision.datasets import ImageFolder
from sklearn.metrics import (
classification_report,
confusion_matrix,
f1_score
)
import numpy as np
from pathlib import Path
# ============================================================
# 1. CONFIGURAZIONE
# ============================================================
class Config:
"""Configurazione centralizzata del training."""
data_dir = Path('data/pcb_defects')
num_classes = 4
class_names = ['OK', 'missing_component', 'scratch', 'solder_bridge']
# Training
batch_size = 32
num_workers = 4
phase1_epochs = 10 # Feature extraction
phase2_epochs = 15 # Fine-tuning layer4
phase3_epochs = 10 # Full fine-tuning
phase1_lr = 1e-3
phase2_lr = 1e-4
phase3_lr = 1e-5
# Device
device = 'cuda' if torch.cuda.is_available() else 'cpu'
# ============================================================
# 2. PREPARAZIONE DATI
# ============================================================
IMAGENET_MEAN = [0.485, 0.456, 0.406]
IMAGENET_STD = [0.229, 0.224, 0.225]
train_transforms = transforms.Compose([
transforms.RandomResizedCrop(224, scale=(0.7, 1.0)),
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomVerticalFlip(p=0.5),
transforms.RandomRotation(degrees=20),
transforms.ColorJitter(brightness=0.3, contrast=0.3),
transforms.RandomAffine(degrees=0, translate=(0.1, 0.1)),
transforms.ToTensor(),
transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),
transforms.RandomErasing(p=0.2),
])
val_transforms = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),
])
cfg = Config()
train_dataset = ImageFolder(cfg.data_dir / 'train', transform=train_transforms)
val_dataset = ImageFolder(cfg.data_dir / 'val', transform=val_transforms)
test_dataset = ImageFolder(cfg.data_dir / 'test', transform=val_transforms)
train_loader = DataLoader(
train_dataset, batch_size=cfg.batch_size,
shuffle=True, num_workers=cfg.num_workers, pin_memory=True
)
val_loader = DataLoader(
val_dataset, batch_size=cfg.batch_size * 2,
shuffle=False, num_workers=cfg.num_workers, pin_memory=True
)
test_loader = DataLoader(
test_dataset, batch_size=cfg.batch_size * 2,
shuffle=False, num_workers=cfg.num_workers, pin_memory=True
)
# ============================================================
# 3. MODELLO
# ============================================================
def build_model(num_classes: int, device: str) -> nn.Module:
"""Crea il modello ResNet-50 per il Transfer Learning."""
weights = ResNet50_Weights.IMAGENET1K_V2
model = models.resnet50(weights=weights)
# Congela tutto
for param in model.parameters():
param.requires_grad = False
# Nuovo classificatore
model.fc = nn.Sequential(
nn.Linear(2048, 512),
nn.ReLU(inplace=True),
nn.Dropout(0.4),
nn.Linear(512, 128),
nn.ReLU(inplace=True),
nn.Dropout(0.2),
nn.Linear(128, num_classes)
)
return model.to(device)
# ============================================================
# 4. TRAINING MULTI-FASE
# ============================================================
def run_training():
"""Esegue il training multi-fase con gradual unfreezing."""
model = build_model(cfg.num_classes, cfg.device)
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
best_val_acc = 0.0
# ---- Fase 1: Feature Extraction ----
print("=" * 60)
print("FASE 1: Feature Extraction (solo classifier)")
print("=" * 60)
optimizer = AdamW(model.fc.parameters(), lr=cfg.phase1_lr, weight_decay=1e-4)
for epoch in range(cfg.phase1_epochs):
train_loss, train_acc = train_one_epoch(
model, train_loader, criterion, optimizer, cfg.device, epoch
)
val_loss, val_acc = evaluate(
model, val_loader, criterion, cfg.device
)
print(f" Epoch {epoch+1}/{cfg.phase1_epochs} | "
f"Train: {train_acc:.1f}% | Val: {val_acc:.1f}%")
if val_acc > best_val_acc:
best_val_acc = val_acc
torch.save(model.state_dict(), 'best_model.pth')
# ---- Fase 2: Fine-tuning layer4 ----
print("\n" + "=" * 60)
print("FASE 2: Fine-tuning layer4 + classifier")
print("=" * 60)
for param in model.layer4.parameters():
param.requires_grad = True
optimizer = AdamW([
{'params': model.layer4.parameters(), 'lr': cfg.phase2_lr * 0.1},
{'params': model.fc.parameters(), 'lr': cfg.phase2_lr},
], weight_decay=1e-4)
for epoch in range(cfg.phase2_epochs):
train_loss, train_acc = train_one_epoch(
model, train_loader, criterion, optimizer, cfg.device, epoch
)
val_loss, val_acc = evaluate(
model, val_loader, criterion, cfg.device
)
print(f" Epoch {epoch+1}/{cfg.phase2_epochs} | "
f"Train: {train_acc:.1f}% | Val: {val_acc:.1f}%")
if val_acc > best_val_acc:
best_val_acc = val_acc
torch.save(model.state_dict(), 'best_model.pth')
# ---- Fase 3: Full Fine-tuning ----
print("\n" + "=" * 60)
print("FASE 3: Full Fine-tuning (tutti i layer)")
print("=" * 60)
for param in model.parameters():
param.requires_grad = True
optimizer = AdamW([
{'params': model.conv1.parameters(), 'lr': cfg.phase3_lr * 0.01},
{'params': model.layer1.parameters(), 'lr': cfg.phase3_lr * 0.1},
{'params': model.layer2.parameters(), 'lr': cfg.phase3_lr * 0.1},
{'params': model.layer3.parameters(), 'lr': cfg.phase3_lr},
{'params': model.layer4.parameters(), 'lr': cfg.phase3_lr * 2},
{'params': model.fc.parameters(), 'lr': cfg.phase3_lr * 10},
], weight_decay=1e-4)
for epoch in range(cfg.phase3_epochs):
train_loss, train_acc = train_one_epoch(
model, train_loader, criterion, optimizer, cfg.device, epoch
)
val_loss, val_acc = evaluate(
model, val_loader, criterion, cfg.device
)
print(f" Epoch {epoch+1}/{cfg.phase3_epochs} | "
f"Train: {train_acc:.1f}% | Val: {val_acc:.1f}%")
if val_acc > best_val_acc:
best_val_acc = val_acc
torch.save(model.state_dict(), 'best_model.pth')
print(f"\nMiglior Val Accuracy: {best_val_acc:.2f}%")
return model
8.1 Evaluare și metrici
Pentru un sistem de control al calității, acuratețea globală nu este suficientă. Trebuie să analizăm precizie, rechemare și scor F1 pentru fiecare clasă de defect. Un fals negativ (defect nedetectat) costă mult mai mult decât un fals pozitiv (piesa bună aruncată).
@torch.no_grad()
def full_evaluation(
model: nn.Module,
test_loader,
class_names: list,
device: str = 'cuda'
) -> dict:
"""Valutazione completa con metriche dettagliate."""
model.eval()
all_preds = []
all_labels = []
all_probs = []
for images, labels in test_loader:
images = images.to(device)
outputs = model(images)
probs = torch.softmax(outputs, dim=1)
_, predicted = outputs.max(1)
all_preds.extend(predicted.cpu().numpy())
all_labels.extend(labels.numpy())
all_probs.extend(probs.cpu().numpy())
all_preds = np.array(all_preds)
all_labels = np.array(all_labels)
# Classification Report
report = classification_report(
all_labels, all_preds,
target_names=class_names,
digits=4
)
print("Classification Report:")
print(report)
# Confusion Matrix
cm = confusion_matrix(all_labels, all_preds)
print("\nConfusion Matrix:")
print(f"{'':.<15} " + " ".join(f"{name:>12}" for name in class_names))
for i, row in enumerate(cm):
print(f"{class_names[i]:<15} " + " ".join(f"{val:>12d}" for val in row))
# F1-Score (macro e weighted)
f1_macro = f1_score(all_labels, all_preds, average='macro')
f1_weighted = f1_score(all_labels, all_preds, average='weighted')
print(f"\nF1-Score Macro: {f1_macro:.4f}")
print(f"F1-Score Weighted: {f1_weighted:.4f}")
return {
'report': report,
'confusion_matrix': cm,
'f1_macro': f1_macro,
'f1_weighted': f1_weighted,
'predictions': all_preds,
'labels': all_labels,
'probabilities': np.array(all_probs)
}
# Esegui la valutazione
model.load_state_dict(torch.load('best_model.pth', weights_only=True))
results = full_evaluation(model, test_loader, Config.class_names)
8.2 Implementare cu TorchScript
Pentru a aduce modelul în producție, trecem la TorchScript: un format optimizat care poate rula fără Python, ideal pentru integrarea în sistemele C++, aplicații mobile de înaltă performanță sau servere de inferență.
import torch
def export_model(model: torch.nn.Module, save_dir: str = 'exported_models'):
"""Esporta il modello in formati ottimizzati per la produzione."""
from pathlib import Path
save_path = Path(save_dir)
save_path.mkdir(exist_ok=True)
model.eval()
model = model.cpu()
dummy_input = torch.randn(1, 3, 224, 224)
# ---- TorchScript (tracing) ----
scripted = torch.jit.trace(model, dummy_input)
scripted_path = save_path / 'model_scripted.pt'
scripted.save(str(scripted_path))
print(f"TorchScript salvato: {scripted_path} "
f"({scripted_path.stat().st_size / 1024 / 1024:.1f} MB)")
# ---- ONNX ----
onnx_path = save_path / 'model.onnx'
torch.onnx.export(
model,
dummy_input,
str(onnx_path),
input_names=['input'],
output_names=['output'],
dynamic_axes={
'input': {0: 'batch_size'},
'output': {0: 'batch_size'},
},
opset_version=17
)
print(f"ONNX salvato: {onnx_path} "
f"({onnx_path.stat().st_size / 1024 / 1024:.1f} MB)")
# ---- Verifica ----
loaded = torch.jit.load(str(scripted_path))
with torch.no_grad():
orig_out = model(dummy_input)
loaded_out = loaded(dummy_input)
diff = (orig_out - loaded_out).abs().max().item()
print(f"Differenza max originale vs TorchScript: {diff:.2e}")
return scripted_path, onnx_path
export_model(model)
9. Transfer de învățare pentru detectarea obiectelor
Transfer Learning nu se limitează la clasificare. Toate cadrele majore ale detectarea obiectelor folosește coloana vertebrală pre-antrenată ca extractoare de caracteristici. Să vedem cum să aplicați reglajul la Faster R-CNN și YOLO.
9.1 Reglaj fin R-CNN mai rapid
R-CNN mai rapid cu coloana vertebrală ResNet-50-FPN și detectorul de referință în interior viziunea cu torță. FPN (Feature Pyramid Network) extrage caracteristici multi-scale, care sunt fundamentale pentru detectează obiecte de diferite dimensiuni. Reglarea fină necesită înlocuirea capului clasificare cu predictori de casete.
import torchvision
from torchvision.models.detection import (
fasterrcnn_resnet50_fpn_v2,
FasterRCNN_ResNet50_FPN_V2_Weights
)
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor
def get_detection_model(num_classes: int) -> torchvision.models.detection.FasterRCNN:
"""Crea un Faster R-CNN pre-trained per il fine-tuning.
Args:
num_classes: Numero di classi target + 1 (background)
Returns:
Modello Faster R-CNN pronto per il fine-tuning
"""
# Carica il modello pre-trained su COCO (91 classi)
weights = FasterRCNN_ResNet50_FPN_V2_Weights.DEFAULT
model = fasterrcnn_resnet50_fpn_v2(weights=weights)
# Sostituisci la testa del box predictor
in_features = model.roi_heads.box_predictor.cls_score.in_features
model.roi_heads.box_predictor = FastRCNNPredictor(
in_features, num_classes
)
return model
# Esempio: rilevamento difetti (3 tipi + background = 4 classi)
det_model = get_detection_model(num_classes=4)
# Training: Faster R-CNN usa una loss composita
# (classification_loss + box_regression_loss + RPN losses)
# Non serve definire la loss manualmente: model(images, targets)
# restituisce direttamente le losses in training mode
9.2 Reglaj YOLO fin
Pentru YOLO, Transfer Learning este și mai ușoară datorită CLI-ului Ultralitice. YOLO utilizează o coloană vertebrală pre-antrenată (adesea CSPDarknet sau variante) și acceptă reglarea fină cu o singură linie de comandă sau câteva linii de cod.
from ultralytics import YOLO
# Carica modello pre-trained su COCO
model = YOLO('yolo11n.pt') # nano (più veloce) o yolo11s.pt, yolo11m.pt
# Fine-tuning sul tuo dataset
# Il dataset deve essere in formato YOLO (images/ + labels/ con .txt)
results = model.train(
data='pcb_defects.yaml', # Config del dataset
epochs=100,
imgsz=640,
batch=16,
lr0=0.001, # Learning rate iniziale
lrf=0.01, # Learning rate finale (ratio)
warmup_epochs=3,
freeze=10, # Congela i primi 10 layer del backbone
augment=True, # Augmentation automatica
patience=20, # Early stopping
device='0' # GPU 0
)
# Valutazione sul test set
metrics = model.val(data='pcb_defects.yaml', split='test')
print(f"mAP50: {metrics.box.map50:.4f}")
print(f"mAP50-95: {metrics.box.map:.4f}")
10. Greșeli frecvente și cum să le eviți
Transfer Learning pare simplă în teorie, dar în practică există multe capcane care poate strica spectacolele. Iată care sunt cele mai frecvente erori.
Top 10 greșeli în învățarea prin transfer
| # | Greşeală | Consecinţă | Soluţie |
|---|---|---|---|
| 1 | Rata de învățare prea mare | Distruge greutățile pre-antrenate | Utilizați LR de 10-100 ori mai mic decât antrenamentul de la zero (1e-4 sau mai puțin) |
| 2 | Normalizare greșită | Caracteristici complet incorecte | Utilizați ÎNTOTDEAUNA media/std a ImageNet pentru modelele pre-antrenate pe ImageNet |
| 3 | Fără creșterea datelor | Supraechipare cu seturi de date mici | Augmentare agresivă: flip, rotation, color jitter, CutMix |
| 4 | BatchNorm nu este în modul de evaluare | Statistici instabile cu loturi mici | model.eval() pentru caracteristicile înghețate sau îngheață în mod explicit BN-urile |
| 5 | Imaginile introduse nu sunt scalate | Erori sau performanțe groaznice | Redimensionați la 224x224 (sau dimensiunea așteptată de model) |
| 6 | Reglaj fin fără încălzire | Actualizările inițiale sunt prea agresive | Încălzire liniară pentru 3-5 epoci |
| 7 | Același LR pentru toate straturile | Straturile inițiale au fost supraeditate | Rate discriminatorii de învățare |
| 8 | Set de date prea mic fără regularizare | Supraadaptare severă | Abandonarea, scăderea greutății, netezirea etichetei, oprirea timpurie |
| 9 | Nu salva cel mai bun model | Utilizați cel mai recent model de epocă (suprainstalat) | Modelați punctul de control pe baza valorii de precizie/pierdere |
| 10 | Ignorați dezechilibrul de clasă | Model orientat spre clasa majoritară | Pierdere ponderată, supraeșantionare, pierdere focală |
def freeze_batchnorm(model: nn.Module):
"""Congela i layer BatchNorm anche quando il modello e in train mode.
Importante quando si fa fine-tuning con batch size piccoli:
le statistiche di batch sarebbero instabili e rovinerebbero
le running_mean e running_var accumulate durante il pre-training.
"""
for module in model.modules():
if isinstance(module, (nn.BatchNorm2d, nn.BatchNorm1d)):
module.eval() # Usa running stats, non batch stats
module.weight.requires_grad = False
module.bias.requires_grad = False
# Utilizzo nel training loop:
model.train()
freeze_batchnorm(model) # BN resta in eval mode
11. Arborele de decizie: alegeți modelul și strategia
Pentru a vă ușura alegerea, iată un arbore decizional practic care acoperă cazuri de utilizare mai frecvente.
HAI UN DATASET ETICHETTATO?
/ \
SI NO
/ \
Quante immagini? Usa CLIP (zero-shot)
/ | \ oppure DINOv2 + kNN
<500 500-10k >10k
| | |
Feature Partial Full
Extraction Fine-tune Fine-tune
| | |
Modello? Modello? Modello?
| | |
v v v
Serve velocità? Serve massima accuracy?
/ \ / \
SI NO SI NO
| | | |
EfficientNet ConvNeXt-T ConvNeXt-B ResNet-50
-B0/B1 o ResNet-50 o ViT-L (baseline)
Serve multi-scala (detection/segmentation)?
--> Swin Transformer o backbone con FPN
Dominio molto diverso da ImageNet?
--> DINOv2 (self-supervised) come backbone
Budget computazionale limitato?
--> EfficientNet-B0 con feature extraction
Recomandare de practică 2025-2026
Pentru majoritatea proiectelor de viziune computerizată, începeți cu ResNet-50 V2 ca linie de bază. Dacă aveți nevoie de mai multă precizie, treceți la ConvNeXt-T (cel mai bun compromis precizie/viteză). Pentru sarcini cu foarte puține date, utilizați DINOv2 ca un extractor de caracteristici. Pentru implementare edge/mobile, alegeți EfficientNet-B0 o MobileNet-V3.
12. Concluzii
Transfer Learning a democratizat viziunea computerizată. Ceea ce odată cerea milioane de imagini, săptămâni de instruire și hardware scump, poate fi realizat astăzi cu câteva sute de imagini, un singur GPU și câteva ore de muncă. Intuițiile cheia acestui articol sunt:
- Caracteristicile CNN sunt ierarhice și transferabile: captează primele straturi modele universale (margini, texturi), acestea din urmă sunt specifice sarcinii. Acest lucru face Transferul Învățare posibilă și eficientă.
- Strategia depinde de date și domeniu: puține date în domeniu similar? Caracteristici extractie. O mulțime de date în diferite domenii? Reglarea fină completă cu prudență. Matricea cu 4 cadrane ghidează decizia.
- Detaliile fac diferența: rate discriminatorii de învățare, dezghețare treptată, încălzirea, normalizarea corectă și creșterea adecvată a datelor pot îmbunătăți performanța cu 5-15% comparativ cu reglajul fin naiv.
- Ecosistemul este matur: PyTorch, torchvision și Ultralytics oferă API-uri simple și modele pre-antrenate de înaltă calitate. Nu există aproape niciodată un motiv pentru a te antrena de la zero.
În următorul articol din serie, vom aplica Transfer LearningDetectarea obiectelor cu YOLO: nu doar clasificare Ce există într-o imagine, dar și Unde este găsit, cu casete de delimitare în timp real.
Articolul următor
Articolul 3: Detectarea obiectelor cu YOLO - Detectarea obiectelor în timp real, Arhitectură YOLO, detectare fără ancore, instruire pentru seturi de date personalizate și implementare pentru aplicații în timp real.







