Přenos učení: Opětovné použití předem vyškolených modelů
Představte si, že byste museli učit dítě poznávat psí plemena. Pokud se to dítě už naučilo Rozpoznat tvary, barvy, textury a obecné anatomické struktury je úkol mnohem náročnější jednoduché. Nemusíte začínat od nuly: můžete převod již nabyté znalosti na nové úkol. To je přesně to, co Přenos učení v hlubokém učení.
V tomto druhém článku seriálu Počítačové vidění s hlubokým učením, prozkoumáme do hloubky o Transfer Learning: proč to funguje, jaké existují strategie, jak vybrat model předem vyškolený správně a jak implementovat kompletní potrubí v PyTorch. Podíváme se na průmyslovou případovou studii realitu a pokročilé techniky, které profesionálové používají každý den.
Přehled série
| # | Položka | Soustředit |
|---|---|---|
| 1 | CNN: Konvoluční sítě | Architektura, školení, nasazení |
| 2 | Jste zde – Přenos učení a jemné ladění | Předtrénované modely, adaptace domény |
| 3 | Detekce objektů pomocí YOLO | Detekce objektů v reálném čase |
| 4 | Sémantická segmentace | Klasifikace na úrovni pixelů |
| 5 | Generování obrazu pomocí GAN a difúze | Syntetické generování obrazu |
| 6 | Edge Deployment a optimalizace | Modely na vestavěných zařízeních |
Co se naučíte
- Co je Transfer Learning a proč funguje (hierarchie funkcí v CNN)
- Hlavní strategie: extrakce vlastností, jemné ladění, adaptace domény
- Jak vybrat správný předtrénovaný model (ResNet, EfficientNet, ViT, ConvNeXt)
- Kompletní implementace v PyTorch: od přípravy dat až po nasazení
- Pokročilé techniky: rozlišovací rychlost učení, postupné rozmrazování, zahřívání rychlosti učení
- Rozšíření dat optimalizované pro přenos učení
- Praktická případová studie: klasifikace průmyslových vad pomocí ResNet-50
- Transfer Learning aplikovaný na detekci objektů (Faster R-CNN, YOLO)
- Časté chyby a jak se jim vyvarovat
1. Co je transferové učení
Il Přenos učení a technika strojového učení, ve které je trénovaný model na úkolu (tzv zdrojový úkol) se znovu použije jako výchozí bod pro jiný úkol (řekl cílové úkoly). Místo trénování neuronové sítě od nuly na milionech obrázků, Vezměme si již natrénovaný model (obvykle na ImageNet, 1,2 milionu obrázků v 1000 třídách) a přizpůsobíme jej našemu konkrétnímu problému.
1.1 Lidská analogie
Náš mozek neustále pracuje v režimu transferového učení. Chirurg se učí něco nového operační technika se nemusí znovu učit anatomii, fyziologii a základní manuální dovednosti. Hudebník klasika, která přechází do jazzu, přenáší instrumentální techniku, čtení partitury a teorii harmonický. Programátor Pythonu, který se učí Rust, přenáší koncepty programování, ladí mentální a algoritmická logika. Ve všech těchto případech předchozí znalosti urychlit enormně se naučit novou doménu.
1.2 proč to funguje: Hierarchie funkcí
Základní důvod, proč Transfer Learning funguje v CNN, spočívá v hierarchie funkcí naučil. Výzkumníci prokázali, že CNN vyškolení na ImageNet se učí funkce organizované v úrovních zvyšující se abstrakce:
Hierarchie funkcí v CNN
| Vrstvy | Funkce se naučila | Specifičnost | Přenositelnost |
|---|---|---|---|
| Vrstvy 1-2 | Hrany, rohy, barevné přechody | Obecný (agnostický) | Velmi vysoká |
| Vrstvy 3-4 | Textury, opakované vzory, geometrické motivy | Pologenerický | Vysoký |
| Vrstvy 5-6 | Části předmětů (oči, kola, okna) | Polospecifické | Průměrný |
| Vrstva 7+ | Kompletní objekty, scény, kompozice | Úkolově specifické | Nízký |
První vrstvy se učí funkce univerzální: hrany, textury a přechody, které jsou užitečné pro jakýkoli vizuální úkol. Mezivrstvy zachycují složitější, ale stále vzory přiměřeně generické. Pouze posledních několik vrstev je vysoce specifických pro původní úlohu. To znamená, že většinu sítě můžeme znovu použít jako výkonnou extraktor funkcí a našemu úkolu přizpůsobíme pouze závěrečné části.
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
Formální definice
Daná zdrojová doména D_s s úkoly T_s a cílová doména D_t s úkoly T_t, Transfer Learning si klade za cíl zlepšit funkci učení f_t v cílové doméně pomocí znalostí extrahováno z D_s a T_s, kde D_s != D_t nebo T_s != T_t. V podstatě se naučily theta váhy ze zdrojové úlohy se používají jako inicializace (theta_0) pro trénování na cílové úloze, místo náhodné inicializace.
2. Přenos vzdělávacích strategií
Neexistuje jediný způsob, jak aplikovat Transfer Learning. Optimální strategie závisí na velikost cílového datového souboru, jeho podobnost se zdrojovým datovým souborem a zdroji dostupné výpočetní zdroje. Podívejme se na čtyři hlavní strategie.
2.1 Extrakce funkcí (zmrazení páteře, klasifikátor vlaku)
Nejjednodušší a nejpřímější strategie: ano zamrzne (zmrazit) celou předtrénovanou síť a používá se jako extraktor pevných funkcí. Jediná část, která je natrénovaná, je nová nahoře přidán klasifikátor. Závaží páteře se během tréninku neaktualizují.
Kdy jej použít: Malý cílový soubor dat (stovky/několik tisíc snímků) e doména podobná zdroji (např. klasifikace psích plemen, když je model předem vycvičen na ImageNet která obsahuje mnoho obrázků psů).
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 Jemné doladění (rozmrazit některé/všechny vrstvy)
V jemném doladění po inicializaci sítě s předem natrénovanými závažími ano rozmrazují některé nebo všechny vrstvy a znovu natrénovat celou síť (nebo její část) s velmi nízkou mírou učení. Předtrénované vrstvy jsou mírně aktualizovány, aby se přizpůsobily do nové domény, při zachování již získaných znalostí.
Kdy jej použít: Středně velká cílová datová sada (tisíce až desítky tisíc obrázků) a/nebo doména mírně odlišná od zdroje.
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 Přizpůsobení domény
La Adaptace domény a specializovaná forma Transfer Learning využívaná při zdrojová doména a cílová doména sdílejí stejné třídy, ale mají distribuci dat jiný. Fungovat musí například modelka vyškolená na profesionální produktové fotografie na fotografiích pořízených ve výrobě s proměnným osvětlením. Techniky jako SAKRA (Domain-Adversarial Neural Network) přidejte doménový diskriminátor, který vynutí síť naučit se doménově invariantní funkce.
2.4 Přenos Zero-Shot a Few-Shot
S příchodem modelů jako KLIP (Předškolení s kontrastním jazykem a obrazem), Obrázky je možné roztřídit do kategorií nikdy neviděl během tréninku (nulový výstřel) nebo s velmi málo příklady (málo výstřelů). CLIP se učí společná textově-obrázková reprezentace: dostaneme textovou výzvu, jako je „fotografie defektu svařování“, model může klasifikovat snímky bez jakéhokoli specifického školení.
Srovnání strategií transferového učení
| Strategie | Nezbytné údaje | Čas tréninku | Výkon | Riziko přemontování |
|---|---|---|---|---|
| Extrakce funkcí | 100-1000 | Zápis | Dobrý | Velmi nízké |
| Částečné jemné doladění | 1000-10000 | hodiny | Velmi dobré | Bas |
| Úplné jemné doladění | 10 000+ | Hodiny-dny | Vynikající | Střední |
| Adaptace domény | Variabilní | hodiny | Dobré-Výborné | Střední |
| Zero-Shot (CLIP) | 0 | Nikdo | Variabilní | Nikdo |
3. Předtrénované modely pro počítačové vidění
Výběr předtrénovaného modelu je zásadní rozhodnutí. Každá architektura má kompromisy rozdíl mezi přesností, rychlostí odvození, velikostí modelu a požadavky na paměť. Zde je přehled nejpoužívanějších modelů v letech 2025-2026.
Srovnávací tabulka Předtrénované modely
| Model | Parametry | ImageNet Top-1 | Typ | Ideální použití |
|---|---|---|---|---|
| ResNet-50 | 25,6 mil | 76,1 % (v1) / 80,9 % (v2) | CNN | Pevná základní linie, snadné nasazení |
| EfficientNet-B0 | 5,3 mil | 77,1 % | CNN | Mobilní, okrajové, omezené zdroje |
| EfficientNet-B7 | 66 mil | 84,3 % | CNN | Maximální přesnost CNN |
| ViT-B/16 | 86 mil | 77,9 % (ImageNet-1k) | Transformátory | Velké datové sady, rozsáhlé předškolení |
| ConvNeXt-T | 28,6 mil | 82,1 % | Moderní CNN | Nejlepší kompromis mezi přesností a rychlostí |
| ConvNeXt-B | 88,6 mil | 83,8 % | Moderní CNN | Když potřebujete vysokou přesnost s CNN |
| Swin-T | 28,3 mil | 81,3 % | Transformátory | Detekce a segmentace |
| CLIP ViT-B/32 | 151 mil. (zobrazit) | 63,2 % (nulový záběr) | Multimodální | Zero-shot, vizuální vyhledávání |
| DINOv2 ViT-S/14 | 22 mil | 81,1 % (lineární sonda) | Samokontrolovaný | Obecné funkce, málo označených dat |
3.1 ResNet-50: The Workhorse
ResNet-50 zůstává nejoblíbenějším modelem pro Transfer Learning díky svému jednoduchost, stabilita v tréninku a široká podpora ekosystémů. Přeskočit spojení (představeno v předchozím článku) umožňují trénovat hluboké sítě bez mizejících problémů s gradientem. Verze závaží V2 (IMAGENET1K_V2), trénovaná moderními technikami jako Mixup, CutMix a Random Erasing dosahuje působivých 80,9 % top-1.
3.2 EfficientNet: Složená škálovatelnost
Rodina EfficientNet používá metodu složené škálování jaké měřítko rovnoměrně hloubku, šířku a rozlišení sítě. EfficientNet-B0 je ideální pro zařízení s omezenými zdroji (parametry 5,3M), zatímco B7 nabízí nejvyšší přesnost (84,3 %) za cenu mnohem většího modelu (66M parametrů).
3.3 Vision Transformer (ViT) a Swin Transformer
I Vision Transformers použít architekturu Transformer (původně vytvořené pro NLP) k počítačovému vidění. Obrázek je rozdělen do polí (např. 16x16 pixelů), každá náplast je považována za "token" a zpracovává se s vlastní pozorností. ViT excel při předtrénování na velké datové sady (ImageNet-21k, JFT-300M), ale může být méně efektivní s malými datovými soubory ve srovnání s CNN. Swin Transformer představuje věnujte pozornost posuvným oknům (posunutá okna), čímž je efektivnější a zvláště vhodné pro náročné úkoly, jako je detekce a segmentace.
3.4 ConvNeXt: Modernizovaná CNN
ConvNeXt dokazuje, že CNN mohou konkurovat Transformers, pokud budou modernizovány se stejnými tréninkovými technikami (AdamW, Mixup, škála vrstev, Stochastic Depth). ConvNeXt-T dosahuje 82,1 % pouze s 28,6 miliony parametry, což nabízí vynikající kompromis mezi přesností, rychlost a jednoduchost nasazení.
3.5 DINOv2: Učení s vlastním dohledem
DINOv2 a model trénovaný výukou s vlastním dohledem (bez štítků) na obrovském kurátorském souboru dat (snímky LVD-142M). Extrahované funkce jsou velmi obecné a přenosné: jednoduchý lineární klasifikátor přidaný navrch dosahuje výsledků konkurenceschopné s kompletním doladěním kontrolovaných modelů. A zvláště užitečné, když máte málo dat označených v cílové doméně.
4. Kdy použít přenosové učení: rozhodovací matice
Volba strategie závisí na dvou klíčových faktorech: velikost datové sady cíl e la podobnost mezi zdrojovou doménou a cílovou doménou. To generuje čtyři rozhodovací kvadranty.
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) |
+-------------------------+-------------------------+
Praktické pravidlo
V letech 2025–2026 bude odpověď na otázku „Mám používat Transfer Learning?“ a téměř vždy si. Školení CNN od nuly a odůvodněné pouze ve velmi specifických případech: Obrovské datové sady (miliony obrázků), doména radikálně odlišná od přirozených obrázků (např. spektrogramy, radarové signály) nebo konkrétní architektonická omezení.
5. Implementace pomocí PyTorch
Pojďme k praxi. Implementujeme Transfer Learning krok za krokem v PyTorch, od načtení předem natrénovaného modelu až po úplný trénink.
5.1 Načtení předem trénovaného modelu
PyTorch nabízí dvě API pro načítání předem trénovaných modelů. Moderní API (zavedené v
torchvision 0.13+) používá enum Weights který poskytuje podrobné informace
na vahách, včetně požadovaných předzpracování transformací.
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 Extrakce funkcí: Zmrazení a nahrazení klasifikátoru
Chcete-li použít ResNet-50 jako extraktor funkcí, musíme: (1) zmrazit všechny parametry páteře, (2) vyměňte konečnou plně spojenou vrstvu za novou vhodnou pro náš počet tříd.
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 Rozšíření dat pro přenos učení
Rozšiřování dat je v Transfer Learning zásadní, zejména u malých datových sad. Transformace musí být kompatibilní s předzpracováním předem trénovaného modelu: normalizace musí zejména používat střední hodnotu a směrodatnou odchylku ImageNet (průměr = [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 a TrivialAugmentWide
Pro zjednodušení výběru rozšíření nabízí PyTorch automatické zásady.
RandAugment použijte N náhodných transformací s intenzitou M.
TrivialAugmentWide aplikuje jedinou transformaci s náhodnou intenzitou,
a je často účinnější než komplexní strategie. Stačí nahradit ruční transformace
s jedním řádkem:
transforms.TrivialAugmentWide() nebo
transforms.RandAugment(num_ops=2, magnitude=9).
5.4 Kompletní tréninková smyčka
Tréninková smyčka pro Transfer Learning je podobná standardní, ale s určitými opatřeními: nižší rychlost učení, postupné zahřívání a pečlivé sledování nadměrného vybavení.
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. Pokročilé jemné ladění
Základní jemné doladění (rozmrazení všech vrstev s jedinou rychlostí učení) často ne a optimální strategii. Pokročilé techniky vám umožní dosáhnout výrazného výkonu lepší, zejména u středně velkých souborů dat.
6.1 Diskriminační míra učení
Myšlenka je jednoduchá, ale účinná: přiřadit různé míry učení do skupin vrstev jiný. Počáteční vrstvy (které se naučily obecné funkce) je třeba aktualizovat velmi málo, mezivrstvy o něco více a konečný klasifikátor s rychlostí učení vyšší. Tím se zachovají znalosti v počátečních vrstvách a zároveň se přizpůsobí ty finální.
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 Postupné rozmrazování
Namísto rozmrazování všech vrstev najednou to postupné odmrazování postupuje od posledních vrstev k první, epocha po epoše. To dává klasifikátoru čas přizpůsobte se před úpravou základních funkcí a vyhněte se destruktivním aktualizacím.
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 Zahřívání rychlosti učení a kosinové žíhání
Postupné zahřívání rychlosti učení v raných epochách zabraňuje příliš agresivním aktualizacím které by mohly zničit předem trénované váhy. Po zahřátí malý rozvrh žíhání postupně snižuje rychlost učení pro stále jemnější jemné doladění.
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 a rozšíření CutMix
Záměny e CutMix jsou to pokročilé augmentační techniky zvláště efektivní při jemném ladění. Mixup vytváří nové vzorky jako lineární kombinaci dvou obrázků a jejich příslušných štítků. CutMix vyjme a vloží obdélníky mezi různé obrázky. Oba fungují jako silné regularizátory a zlepšují zobecnění.
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. Advanced Data Augmentation for Transfer Learning
Volba augmentace dat má obrovský dopad na výkon, zejm s malými datovými soubory. Podívejme se na nejúčinnější možnosti v letech 2025-2026.
7.1 Albumentace: Rozšíření na profesionální úrovni
Albumentace a specializovanou knihovnu pro rozšiřování obrazových dat, výrazně rychleji než torchvision.transformuje díky použití OpenCV. Nabízí transformace zvláště užitečné pro specializované oblasti (lékařské, průmyslové, satelitní).
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. Případová studie: Klasifikace průmyslových vad pomocí ResNet-50
Let's put it all together with a real use case: an industrial quality inspection system which classifies defects in electronic components. Datový soubor obsahuje obrázky ve vysokém rozlišení of printed circuit boards (PCBs) with four classes: OK (žádné vady), pájecí_můstek (svařovací můstek), chybějící_komponenta (chybějící součástka) e poškrábat (poškrábat).
Nastavení případové studie
- datové sady: 4000 obrázků (1000 na třídu), rozdělených 70/15/15
- Rezoluce: 512 x 512 pixelů, RGB
- Model: ResNet-50 předtrénovaný na ImageNet (závaží V2)
- Strategie: Extrakce funkcí -> Postupné dolaďování
- Železářské zboží: GPU NVIDIA s 8GB+ 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 Hodnocení a metriky
Pro systém kontroly kvality nestačí globální přesnost. Musíme analyzovat přesnost, odvolání a skóre F1 pro každou třídu defektů. Falešně negativní (nezjištěná vada) stojí to mnohem víc než falešně pozitivní (vyřazený dobrý kus).
@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 Nasazení s TorchScriptem
Abychom uvedli model do výroby, převedeme na TorchScript: formát optimalizované, které lze spustit bez Pythonu, ideální pro integraci do systémů C++, vysoce výkonné mobilní aplikace nebo inferenční servery.
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. Přenos učení pro detekci objektů
Transfer Learning není omezen na klasifikaci. Všechny hlavní rámce detekce objektů využívá předtrénované páteře jako extraktory funkcí. Podívejme se jak aplikovat jemné ladění na Faster R-CNN a YOLO.
9.1 Jemné doladění Faster R-CNN
Rychlejší R-CNN s páteří ResNet-50-FPN a referenčním detektorem pochodeň. FPN (Feature Pyramid Network) extrahuje víceúrovňové prvky, které jsou zásadní pro detekovat objekty různých velikostí. Jemné doladění vyžaduje výměnu hlavy klasifikace box prediktoru.
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 Jemné doladění YOLO
Pro YOLO je Transfer Learning ještě jednodušší díky CLI Ultralytika. YOLO používá předem vyškolenou páteř (často CSPDarknet nebo varianty) a podporuje jemné doladění pomocí jediného příkazového řádku nebo několika řádků kódu.
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. Časté chyby a jak se jim vyvarovat
Transfer Learning se zdá teoreticky jednoduchý, ale v praxi existuje mnoho úskalí což může zkazit představení. Zde jsou nejčastější chyby.
10 nejčastějších chyb v transferovém učení
| # | Chyba | Následek | Řešení |
|---|---|---|---|
| 1 | Míra učení je příliš vysoká | Ničí předem natrénované váhy | Použijte LR 10-100x nižší než trénink od nuly (1e-4 nebo méně) |
| 2 | Špatná normalizace | Zcela nesprávné vlastnosti | VŽDY používejte průměr/std ImageNet pro předem trénované modely na ImageNet |
| 3 | Žádné rozšiřování dat | Přeplnění malými datovými soubory | Agresivní augmentace: flip, rotace, barevný jitter, CutMix |
| 4 | BatchNorm není v režimu eval | Nestabilní statistiky s malými dávkami | model.eval() pro zmrazené prvky nebo explicitně zmrazí BN |
| 5 | Vstupní obrázky nejsou zmenšeny | Chyby nebo hrozný výkon | Změnit velikost na 224 x 224 (nebo velikost očekávanou modelem) |
| 6 | Jemné doladění bez zahřívání | Počáteční aktualizace jsou příliš agresivní | Lineární zahřívání pro 3-5 epoch |
| 7 | Stejný LR pro všechny vrstvy | Počáteční vrstvy byly přepracovány | Diskriminační míry učení |
| 8 | Datový soubor je bez regularizace příliš malý | Silné přemontování | Výpadek, úbytek hmotnosti, vyhlazování štítků, předčasné zastavení |
| 9 | Neukládejte nejlepší model | Použít nejnovější model epochy (přemontovaný) | Modelový kontrolní bod založený na přesnosti/hodnotě ztráty |
| 10 | Ignorujte nevyváženost třídy | Model zaujatý vůči většinové třídě | Vážená ztráta, převzorkování, ztráta ohniska |
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. Rozhodovací strom: Vyberte model a strategii
Abychom vám výběr usnadnili, je zde praktický rozhodovací strom zahrnující případy použití běžnější.
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
Doporučení pro praxi 2025-2026
U většiny projektů počítačového vidění začněte s ResNet-50 V2 jako základní linii. Pokud potřebujete větší přesnost, přejděte na ConvNeXt-T (nejlepší kompromis mezi přesností a rychlostí). Pro úlohy s velmi malým množstvím dat použijte DINOv2 jako extraktor funkcí. Pro nasazení na okrajích/mobilech vyberte EfficientNet-B0 o MobileNet-V3.
12. Závěry
Transfer Learning demokratizoval počítačové vidění. Co kdysi vyžadovalo miliony obrázků, týdny školení a drahý hardware, toho lze dnes dosáhnout s několika stovkami obrázků, jedním GPU a několika hodinami práce. Intuice klíčem k tomuto článku jsou:
- Funkce CNN jsou hierarchické a přenositelné: zachycují první vrstvy univerzální vzory (hrany, textury), ty druhé jsou specifické pro daný úkol. Tím dojde k převodu Učení možné a efektivní.
- Strategie závisí na datech a doméně: málo dat v podobné doméně? Vlastnosti extrakce. Mnoho dat v jiné doméně? Úplné doladění opatrně. 4kvadrantová matice řídí rozhodnutí.
- Podrobnosti dělají rozdíl: diskriminační míry učení, postupné rozmrazování, zahřívání, správná normalizace a vhodné rozšíření dat může zlepšit výkon o 5-15 % oproti naivnímu dolaďování.
- Ekosystém je vyspělý: PyTorch, torchvision a Ultralytics nabízejí jednoduchá API a vysoce kvalitní předtrénované modely. Téměř nikdy není důvod trénovat od nuly.
V dalším článku ze série aplikujeme Transfer Learning naDetekce objektů s YOLO: nejen klasifikovat Co je na obrázku, ale také Kde s ohraničujícími rámečky v reálném čase.
Další článek
Článek 3: Detekce objektů pomocí YOLO - detekce objektů v reálném čase, Architektura YOLO, detekce bez kotvy, školení o vlastních datových sadách a nasazení pro aplikace v reálném čase.







