Transfer uczenia się: ponowne wykorzystanie wstępnie przeszkolonych modeli
Wyobraź sobie, że musisz nauczyć dziecko rozpoznawania ras psów. Jeśli to dziecko już się nauczyło rozpoznawanie kształtów, kolorów, tekstur i ogólnych struktur anatomicznych, zadanie staje się znacznie większe proste. Nie musisz zaczynać od zera: możesz przenosić wiedzę już zdobytą do nowej zadanie. To jest dokładnie to, co Nauczanie transferowe w głębokim uczeniu się.
W drugim artykule z tej serii Widzenie komputerowe z głębokim uczeniem się, będziemy zwiedzać szczegółowo o Transfer Learning: dlaczego to działa, jakie istnieją strategie, jak wybrać model wstępnie przeszkolony i jak wdrożyć kompletne potoki w PyTorch. Przyjrzymy się studium przypadku przemysłowego rzeczywistością i zaawansowanymi technikami, z których profesjonaliści korzystają na co dzień.
Przegląd serii
| # | Przedmiot | Centrum |
|---|---|---|
| 1 | CNN: Sieci konwolucyjne | Architektura, szkolenia, wdrożenie |
| 2 | Jesteś tutaj – Transfer wiedzy i dostrajanie | Wstępnie wytrenowane modele, adaptacja domeny |
| 3 | Wykrywanie obiektów za pomocą YOLO | Wykrywanie obiektów w czasie rzeczywistym |
| 4 | Semantyczna segmentacja | Klasyfikacja na poziomie pikseli |
| 5 | Generowanie obrazu za pomocą GAN i dyfuzji | Generowanie obrazu syntetycznego |
| 6 | Wdrożenie i optymalizacja brzegowa | Modele na urządzeniach wbudowanych |
Czego się nauczysz
- Co to jest nauka transferu i dlaczego to działa (hierarchie funkcji w CNN)
- Główne strategie: ekstrakcja cech, dostrajanie, adaptacja domeny
- Jak wybrać odpowiedni, wstępnie wytrenowany model (ResNet, EfficientNet, ViT, ConvNeXt)
- Kompletne wdrożenie w PyTorch: od przygotowania danych po wdrożenie
- Zaawansowane techniki: dyskryminacyjne szybkości uczenia się, stopniowe odmrażanie, rozgrzewanie szybkości uczenia się
- Powiększanie danych zoptymalizowane pod kątem transferu uczenia się
- Praktyczne studium przypadku: klasyfikacja wad przemysłowych za pomocą ResNet-50
- Transfer Learning zastosowany do wykrywania obiektów (szybciej R-CNN, YOLO)
- Najczęstsze błędy i sposoby ich unikania
1. Czym jest nauka transferowa
Il Nauczanie transferowe oraz technika uczenia maszynowego, w której wytrenowany model na zadaniu (tzw zadanie źródłowe) jest ponownie wykorzystywany jako punkt wyjścia do innego zadania (powiedział docelowe zadania). Zamiast trenować sieć neuronową od podstaw na milionach obrazów, weźmy już wytrenowany model (zwykle w ImageNet, 1,2 miliona obrazów w 1000 klasach) i dostosowujemy go do naszego konkretnego problemu.
1.1 Analogia ludzka
Nasz mózg stale działa w trybie uczenia się transferowego. Chirurg uczy się czegoś nowego techniki operacyjnej nie musi uczyć się na nowo anatomii, fizjologii i podstawowych umiejętności manualnych. Muzyk klasyka przechodząca w jazz przenosi technikę instrumentalną, odczytanie partytury i teorię harmoniczny. Programista Pythona uczący się Rusta przekazuje koncepcje programowania, debugowanie logika mentalna i algorytmiczna. We wszystkich tych przypadkach wcześniejsza wiedza przyśpieszyć ogromnie ucząc się nowej domeny.
1.2 dlaczego to działa: Hierarchia funkcji
Podstawowy powód, dla którego Transfer Learning sprawdza się w CNN, leży w tym, że: hierarchia funkcji uczony. Naukowcy wykazali, że CNN przeszkoleni w ImageNet uczą się funkcji zorganizowanych na poziomach rosnącej abstrakcji:
Hierarchia funkcji w CNN
| Warstwy | Funkcja poznana | Specyficzność | Możliwość przenoszenia |
|---|---|---|---|
| Warstwy 1-2 | Krawędzie, rogi, przejścia kolorów | Ogólny (niezależny od zadania) | Bardzo wysoki |
| Warstwy 3-4 | Tekstury, powtarzające się wzory, motywy geometryczne | Półogólne | Wysoki |
| Warstwy 5-6 | Części obiektów (oczy, koła, okna) | Półspecyficzne | Przeciętny |
| Warstwa 7+ | Kompletne obiekty, sceny, kompozycje | Specyficzne dla zadania | Niski |
Pierwsze warstwy uczą się funkcji uniwersalny: krawędzie, tekstury i gradienty, które są przydatne do każdego zadania wizualnego. Warstwy pośrednie uchwycą bardziej złożone, ale wciąż wzory w miarę ogólne. Tylko kilka ostatnich warstw jest wysoce specyficznych dla pierwotnego zadania. Oznacza to, że możemy ponownie wykorzystać większość sieci jako potężną ekstraktor funkcji i dostosuj do naszego zadania tylko końcowe części.
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
Definicja formalna
Biorąc pod uwagę domenę źródłową D_s z zadaniami T_s i domenę docelową D_t z zadaniami T_t, Transfer Learning ma na celu poprawę funkcji uczenia się f_t w domenie docelowej z wykorzystaniem wiedzy wyodrębniony z D_s i T_s, gdzie D_s != D_t lub T_s != T_t. Zasadniczo nauczyłem się ciężarów theta z zadania źródłowego są wykorzystywane jako inicjalizacja (theta_0) do szkolenia w zadaniu docelowym, zamiast losowej inicjalizacji.
2. Przenieś strategie uczenia się
Nie ma jednego sposobu zastosowania Transfer Learning. Optymalna strategia zależy od wielkość docelowego zbioru danych, jego podobieństwo do źródłowego zbioru danych i zasobów dostępne zasoby obliczeniowe. Przyjrzyjmy się czterem głównym strategiom.
2.1 Ekstrakcja cech (zamrożenie szkieletu, klasyfikator pociągu)
Najprostsza i najbardziej bezpośrednia strategia: tak zawiesza się (zamrożenie) całej wstępnie wyszkolonej sieci i jest używany jako ekstraktor stałych cech. Jedyna część, która jest przeszkolona, jest nowa klasyfikator dodany na górze. Obciążenia kręgosłupa nie są aktualizowane podczas treningu.
Kiedy go używać: Mały docelowy zbiór danych (setki/kilka tysięcy obrazów) e domena podobna do źródła (np. klasyfikacja ras psów, gdy model jest wstępnie szkolony w ImageNet). który zawiera wiele zdjęć psów).
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 Dostrajanie (odmrażanie niektórych/wszystkich warstw)
W dostrajaniu, po zainicjowaniu sieci wstępnie wytrenowanymi ciężarami, tak rozmrażają się niektóre lub wszystkie warstwy i przeszkolić całą sieć (lub jej część) z bardzo niskim tempem uczenia się. Wstępnie wyszkolone warstwy zostały nieco zaktualizowane, aby je dostosować do nowej domeny, zachowując już zdobytą wiedzę.
Kiedy go używać: Średnio-duży docelowy zbiór danych (tysiące-dziesiątki tysięcy obrazów) i/lub domena umiarkowanie różna od źródła.
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 Dostosowanie domeny
La Adaptacja domeny oraz wyspecjalizowana forma uczenia się transferowego stosowana, gdy domena źródłowa i domena docelowa mają te same klasy, ale mają dystrybucję danych inny. Przykładowo modelka wyszkolona na profesjonalnych zdjęciach produktów musi się sprawdzić na zdjęciach wykonanych fabrycznie przy zmiennym oświetleniu. Techniki takie jak CHOLERA (Domain-Adversarial Neural Network) dodaj dyskryminator domeny, który wymusza działanie sieci aby poznać funkcje niezmienne w domenie.
2.4 Transfer zerowy i kilka strzałów
Wraz z pojawieniem się modeli takich jak KLIPS (Wstępny trening języka kontrastowego), Możliwe jest klasyfikowanie obrazów w kategorie nigdy nie widziałem podczas szkolenia (strzał zerowy) lub z bardzo małą liczbą przykładów (kilka strzałów). CLIP się uczy wspólna reprezentacja tekstowo-obrazowa: z podpowiedzią tekstową, taką jak „zdjęcie wady”. spawanie”, model może klasyfikować obrazy bez specjalnego przeszkolenia.
Porównanie strategii uczenia się transferowego
| Strategia | Niezbędne dane | Czas szkolenia | Wydajność | Ryzyko przeuczenia |
|---|---|---|---|---|
| Ekstrakcja cech | 100-1000 | Protokół | Dobry | Bardzo niski |
| Częściowe dostrojenie | 1000-10000 | Godziny | Bardzo dobry | Bas |
| Pełne dostrajanie | 10000+ | Godziny-Dni | Doskonały | Średni |
| Adaptacja domeny | Zmienny | Godziny | Dobrze-doskonałe | Średni |
| Zero-Shot (CLIP) | 0 | Nikt | Zmienny | Nikt |
3. Wstępnie wytrenowane modele widzenia komputerowego
Wybór wstępnie wytrenowanego modelu jest kluczową decyzją. Każda architektura ma kompromisy różnią się między dokładnością, szybkością wnioskowania, rozmiarem modelu i wymaganiami dotyczącymi pamięci. Oto przegląd najczęściej używanych modeli w latach 2025-2026.
Tabela porównawcza wstępnie przeszkolonych modeli
| Model | Parametry | ImageNet Top-1 | Typ | Idealne zastosowanie |
|---|---|---|---|---|
| ResNet-50 | 25,6 mln | 76,1% (v1) / 80,9% (v2) | CNN | Solidna linia bazowa, łatwe wdrożenie |
| EfficientNet-B0 | 5,3 mln | 77,1% | CNN | Mobilne, brzegowe, ograniczone zasoby |
| EfficientNet-B7 | 66M | 84,3% | CNN | Maksymalna dokładność CNN |
| ViT-B/16 | 86M | 77,9% (ImageNet-1k) | Transformatory | Duże zbiory danych, szkolenia wstępne na dużą skalę |
| ConvNeXt-T | 28,6 mln | 82,1% | Nowoczesne CNN | Najlepszy kompromis między dokładnością a szybkością |
| ConvNeXt-B | 88,6 mln | 83,8% | Nowoczesne CNN | Gdy potrzebujesz dużej dokładności w CNN |
| Swin-T | 28,3 mln | 81,3% | Transformatory | Detekcja i segmentacja |
| KLIP ViT-B/32 | 151M (widok) | 63,2% (strzał zerowy) | Multimodalny | Zero-shot, wyszukiwanie wizualne |
| DINOv2 ViT-S/14 | 22M | 81,1% (sonda liniowa) | Samonadzorowany | Funkcje ogólne, kilka danych oznaczonych etykietami |
3.1 ResNet-50: koń pociągowy
ResNet-50 pozostaje najpopularniejszym modelem uczenia się transferowego dzięki swoim możliwościom prostota, stabilność szkolenia i szerokie wsparcie ekosystemowe. Pomiń połączenia (wprowadzono w poprzednim artykule) pozwalają na uczenie głębokich sieci bez znikających problemów z gradientem. Wersja V2 ciężarków (IMAGENET1K_V2), trenowana nowoczesnymi technikami takimi jak Mixup, CutMix i Random Erasing osiąga imponujący wynik 80,9% na pierwszej pozycji.
3.2 EfficientNet: Złożona skalowalność
Rodzina Efektywna sieć stosuje metodę skalowanie złożone co za skala jednakowa głębokość, szerokość i rozdzielczość sieci. EfficientNet-B0 jest idealny do urządzenia o ograniczonych zasobach (parametry 5,3M), natomiast B7 oferuje najwyższą dokładność (84,3%) kosztem znacznie większego modelu (parametry 66M).
3.3 Transformator wizyjny (ViT) i transformator obrotowy
I Transformatory wizji zastosuj architekturę Transformer (pierwotnie stworzony dla NLP) do wizji komputerowej. Obraz jest podzielony na obszary (np. 16x16 pikseli), każda łatka traktowana jest jako „token” i przetwarzana z należytą uwagą. ViT Excel po wstępnym przeszkoleniu na dużych zbiorach danych (ImageNet-21k, JFT-300M), ale może być mniejszy skuteczne w przypadku małych zbiorów danych w porównaniu z CNN. Transformator świń przedstawia uwagę na okna przesuwne (przesunięte okna), dzięki czemu jest bardziej wydajny i szczególnie nadaje się do gęstych zadań, takich jak wykrywanie i segmentacja.
3.4 ConvNeXt: Zmodernizowane CNN
ConvNeXt udowadnia, że CNN mogą konkurować z Transformersami, jeśli zostaną zmodernizowane z tymi samymi technikami szkoleniowymi (AdamW, Mixup, skala warstw, głębokość stochastyczna). ConvNeXt-T osiąga 82,1% przy zaledwie 28,6 mln parametrów, oferując doskonały kompromis pomiędzy dokładnością, szybkość i prostota wdrożenia.
3.5 DINOv2: Uczenie się samonadzorowane
DINOv2 oraz model wytrenowany w procesie samonadzoru (bez etykiet) na ogromnym, wyselekcjonowanym zbiorze danych (obrazy LVD-142M). Wyodrębnione funkcje są niezwykle ogólne i przenoszalne: prosty klasyfikator liniowy dodany na górze pozwala uzyskać wyniki konkurencyjne dzięki pełnemu dostrojeniu nadzorowanych modeli. A szczególnie przydatne, gdy masz mało danych oznaczonych etykietą w domenie docelowej.
4. Kiedy stosować naukę transferową: matryca decyzyjna
Wybór strategii zależy od dwóch kluczowych czynników: wielkość zbioru danych cel e la podobieństwo pomiędzy domeną źródłową a domeną docelową. Generuje to cztery kwadranty decyzyjne.
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) |
+-------------------------+-------------------------+
Praktyczna zasada
W latach 2025-2026 odpowiedź na pytanie „Czy warto korzystać z Transfer Learning?” i prawie zawsze si. Szkolenie CNN od podstaw i uzasadnione tylko w bardzo konkretnych przypadkach: Ogromne zbiory danych (miliony obrazów), domena radykalnie różniąca się od naturalnych obrazów (np. spektrogramy, sygnały radarowe) lub szczególne ograniczenia architektoniczne.
5. Implementacja za pomocą PyTorch
Przejdźmy do ćwiczeń. Krok po kroku wdrożymy Transfer Learning w PyTorch, począwszy od załadowania wstępnie wytrenowanego modelu aż do pełnego szkolenia.
5.1 Ładowanie wstępnie wyszkolonego modelu
PyTorch oferuje dwa interfejsy API do ładowania wstępnie wyszkolonych modeli. Nowoczesne API (wprowadzone w
torchvision 0.13+) używa wyliczenia Weights który dostarcza szczegółowych informacji
na masach, łącznie z wymaganymi przekształceniami wstępnymi.
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 Ekstrakcja cech: Zablokuj i zamień klasyfikator
Aby użyć ResNet-50 jako ekstraktora funkcji, musimy: (1) zamrozić wszystkie parametry szkieletu, (2) zastąpić ostatnią w pełni połączoną warstwę nową, odpowiednią dla naszej liczby klas.
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 Powiększanie danych w celu uczenia się transferowego
Powiększanie danych ma fundamentalne znaczenie w Transfer Learning, zwłaszcza w przypadku małych zbiorów danych. Transformacje muszą być kompatybilne z przetwarzaniem wstępnym wstępnie wytrenowanego modelu: w szczególności normalizacja musi wykorzystywać średnią i odchylenie standardowe ImageNet (średnia=[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
Aby uprościć wybór ulepszeń, PyTorch oferuje automatyczne zasady.
RandAugment zastosuj N losowych transformacji o intensywności M.
TrivialAugmentWide stosuje pojedynczą transformację o losowym natężeniu,
i często jest bardziej skuteczny niż złożone strategie. Po prostu zamień ręczne przekształcenia
z jedną linijką:
transforms.TrivialAugmentWide() Lub
transforms.RandAugment(num_ops=2, magnitude=9).
5.4 Pełna pętla treningowa
Pętla szkoleniowa dla Transfer Learning jest podobna do standardowej, ale z pewnymi środkami ostrożności: niższe tempo uczenia się, stopniowe rozgrzewanie i uważne monitorowanie nadmiernego dopasowania.
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. Zaawansowane dostrajanie
Podstawowe dostrajanie (odmrażanie wszystkich warstw przy jednej szybkości uczenia się) często nie skutkuje i optymalną strategię. Zaawansowane techniki pozwalają osiągnąć znaczną wydajność lepiej, zwłaszcza w przypadku średnich zbiorów danych.
6.1 Dyskryminacyjne współczynniki uczenia się
Pomysł jest prosty, ale potężny: przydzielaj różne tempo uczenia się do grup warstw inny. Warstwy początkowe (które nauczyły się cech ogólnych) wymagają aktualizacji bardzo mało, warstwy pośrednie trochę więcej i końcowy klasyfikator z szybkością uczenia się wyższy. Pozwala to zachować wiedzę w warstwach początkowych, dostosowując jednocześnie warstwy końcowe.
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 Stopniowe rozmrażanie
Zamiast rozmrażać wszystkie warstwy na raz, wystarczy stopniowe rozmrażanie przechodzi od ostatnich warstw do pierwszej, epoka po epoce. Daje to klasyfikatorowi czas na dostosuj się przed modyfikacją podstawowych funkcji, unikając destrukcyjnych aktualizacji.
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 Rozgrzewanie szybkości uczenia się i wyżarzanie cosinusowe
Stopniowe zwiększanie szybkości uczenia się we wczesnych epokach zapobiega zbyt agresywnym aktualizacjom które mogłyby zniszczyć wcześniej wytrenowane ciężary. Po rozgrzewce mały harmonogram wyżarzania stopniowo zmniejsza szybkość uczenia się w celu uzyskania coraz delikatniejszego dostrajania.
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 Mieszanie i wzmacnianie CutMix
Pomieszania e WytnijMiks są to zaawansowane techniki augmentacji szczególnie skuteczny w dostrajaniu. Mixup tworzy nowe próbki jako kombinację liniową dwóch obrazów i ich odpowiednich etykiet. CutMix wycina i wkleja prostokąty pomiędzy różnymi obrazami. Oba działają jako potężne regularyzatory i poprawiają generalizację.
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. Zaawansowane powiększanie danych na potrzeby uczenia się transferowego
Wybór sposobu powiększania danych ma ogromny wpływ na wydajność, szczególnie z małymi zbiorami danych. Zobaczmy najskuteczniejsze opcje w latach 2025-2026.
7.1 Albumentacje: Powiększanie na poziomie profesjonalnym
Albumentacje oraz specjalistyczną bibliotekę do powiększania danych obrazowych, znacznie szybciej niż torchvision.transforms dzięki zastosowaniu OpenCV. Oferuje przekształcenia szczególnie przydatne w dziedzinach specjalistycznych (medycznych, przemysłowych, satelitarnych).
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. Studium przypadku: Klasyfikacja wad przemysłowych za pomocą ResNet-50
Połączmy to wszystko z prawdziwym przypadkiem użycia: przemysłowym systemem kontroli jakości która klasyfikuje wady elementów elektronicznych. Zbiór danych zawiera obrazy o wysokiej rozdzielczości płytek drukowanych (PCB) w czterech klasach: OK (bez wad), mostek lutowniczy (mostek spawalniczy), brakujący_komponent (brakujący element) e zadrapanie (zadrapanie).
Konfiguracja studium przypadku
- Zbiory danych: 4000 obrazów (1000 na klasę), podzielone 70/15/15
- Rezolucja: 512 x 512 pikseli, RGB
- Model: ResNet-50 wstępnie przeszkolony w ImageNet (wagi V2)
- Strategia: Ekstrakcja cech -> Stopniowe dostrajanie
- Sprzęt komputerowy: Karta graficzna NVIDIA z 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 Ocena i metryki
W przypadku systemu kontroli jakości globalna dokładność nie wystarczy. Musimy przeanalizować precyzja, powtarzalność i wynik F1 dla każdej klasy defektów. Fałszywy wynik negatywny (niewykryta wada) kosztuje znacznie więcej niż fałszywy alarm (dobry kawałek wyrzucony).
@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 Wdrożenie za pomocą TorchScript
Aby wprowadzić model do produkcji, konwertujemy do TorchScript: format zoptymalizowany do pracy bez Pythona, idealny do integracji z systemami C++, wysokowydajne aplikacje mobilne lub serwery wnioskowania.
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. Przenieś naukę do wykrywania obiektów
Transfer Learning nie ogranicza się do klasyfikacji. Wszystkie główne frameworki wykrywanie obiektów wykorzystuje wstępnie wyszkolone szkielety jako ekstraktory cech. Zobaczmy jak zastosować dostrajanie w szybszym R-CNN i YOLO.
9.1 Dostrajanie Szybsze R-CNN
Szybszy R-CNN ze szkieletem ResNet-50-FPN i detektorem referencyjnym widzenie pochodni. Sieć FPN (Feature Pyramid Network) wyodrębnia cechy wieloskalowe, które są podstawą wykrywać obiekty o różnych rozmiarach. Dostrojenie wymaga wymiany głowicy klasyfikacja predyktorów skrzynkowych.
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
Dostrajanie 9.2 YOLO
Dla YOLO nauka transferu jest jeszcze łatwiejsza dzięki CLI Ultralityka. YOLO używa wstępnie wytrenowanego szkieletu (często CSPDarknet lub warianty) i obsługuje dostrajanie za pomocą jednego wiersza poleceń lub kilku wierszy kodu.
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. Typowe błędy i jak ich unikać
Transfer Learning wydaje się prosty w teorii, ale w praktyce istnieje wiele pułapek co może zrujnować występy. Oto najczęstsze błędy.
10 najważniejszych błędów w nauczaniu transferowym
| # | Błąd | Konsekwencja | Rozwiązanie |
|---|---|---|---|
| 1 | Zbyt wysoka szybkość uczenia się | Niszczy wcześniej wytrenowane ciężary | Użyj LR 10-100x mniejszego niż trenując od zera (1e-4 lub mniej) |
| 2 | Błędna normalizacja | Całkowicie nieprawidłowe funkcje | ZAWSZE używaj średniej/std ImageNet dla wstępnie wytrenowanych modeli w ImageNet |
| 3 | Brak powiększania danych | Nadmierne dopasowanie w przypadku małych zbiorów danych | Agresywne wzmacnianie: odwracanie, obracanie, drżenie kolorów, CutMix |
| 4 | BatchNorm nie jest w trybie eval | Niestabilne statystyki w przypadku małych partii | model.eval() dla zablokowanych funkcji lub jawnie blokuje BN |
| 5 | Obrazy wejściowe nie są skalowane | Błędy lub straszna wydajność | Zmień rozmiar na 224x224 (lub rozmiar oczekiwany przez model) |
| 6 | Dostrajanie bez rozgrzewki | Początkowe aktualizacje są zbyt agresywne | Rozgrzewanie liniowe przez 3-5 epok |
| 7 | Ten sam LR dla wszystkich warstw | Warstwy początkowe zostały przeedytowane | Dyskryminacyjne wskaźniki uczenia się |
| 8 | Zbiór danych jest zbyt mały bez regularyzacji | Poważne nadmierne dopasowanie | Porzucenie, spadek wagi, wygładzenie etykiety, wczesne zatrzymanie |
| 9 | Nie zapisuj najlepszego modelu | Użyj najnowszego modelu epoki (overfited) | Modeluj punkt kontrolny w oparciu o dokładność/wartość straty |
| 10 | Ignoruj brak równowagi klasowej | Model nastawiony na klasę większościową | Utrata ważona, nadpróbkowanie, utrata ogniskowej |
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. Drzewo decyzyjne: wybierz model i strategię
Aby ułatwić Ci wybór, poniżej znajduje się praktyczne drzewo decyzyjne obejmujące przypadki użycia bardziej powszechne.
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
Zalecenie praktyczne 2025-2026
W przypadku większości projektów związanych z wizją komputerową zacznij od ResNet-50 V2 jako punkt odniesienia. Jeśli potrzebujesz większej dokładności, przejdź dalej ConvNeXt-T (najlepszy kompromis między dokładnością a szybkością). W przypadku zadań z bardzo małą ilością danych użyj DINOv2 jako ekstraktor cech. W przypadku wdrożenia brzegowego/mobilnego wybierz EfficientNet-B0 o MobileNet-V3.
12. Wnioski
Transfer Learning zdemokratyzował wizję komputerową. Co kiedyś było wymagane miliony obrazów, tygodnie szkoleń i drogi sprzęt, można to osiągnąć już dziś z kilkoma setkami zdjęć, jednym procesorem graficznym i kilkoma godzinami pracy. Intuicje key to this article are:
- Funkcje CNN są hierarchiczne i można je przenosić: the first layers capture wzory uniwersalne (krawędzie, tekstury), te ostatnie są specyficzne dla zadania. To sprawia, że Transfer Nauka możliwa i skuteczna.
- Strategia zależy od danych i domeny: mało danych w podobnej domenie? Funkcje extraction. Dużo danych w różnych domenach? Pełne dostrajanie z zachowaniem ostrożności. Macierz 4-ćwiartkowa kieruje decyzją.
- Szczegóły robią różnicę: dyskryminacyjne tempo uczenia się, stopniowe odmrażanie, rozgrzewka, prawidłowa normalizacja i odpowiednie uzupełnienie danych mogą poprawić wydajność o 5-15% w porównaniu do naiwnego dostrajania.
- Ekosystem jest dojrzały: PyTorch, torchvision i Ultralytics oferują proste interfejsy API i wysokiej jakości, wstępnie wytrenowane modele. Prawie nigdy nie ma powodu, aby trenować od zera.
W kolejnym artykule z tej serii zastosujemy Transfer Learning doWykrywanie obiektów z YOLO: nie tylko klasyfikowanie Co jest na obrazie, ale także Gdzie zostanie znaleziony, z ramkami ograniczającymi w czasie rzeczywistym.
Następny artykuł
Artykuł 3: Wykrywanie obiektów za pomocą YOLO - Wykrywanie obiektów w czasie rzeczywistym, Architektura YOLO, wykrywanie bez kotwic, szkolenie w zakresie niestandardowych zbiorów danych i wdrażanie do zastosowań w czasie rzeczywistym.







