전이 학습: 사전 훈련된 모델 재사용
아이에게 개 품종을 알아보도록 가르쳐야 한다고 상상해 보세요. 그 아이가 이미 배웠다면 모양, 색상, 질감 및 일반적인 해부학적 구조를 인식하는 작업은 훨씬 더 많아집니다. 간단하다. 처음부터 시작할 필요는 없습니다. 할 수 있습니다. 옮기다 새로운 것에 대해 이미 습득한 지식 작업. 이것이 바로 전이 학습 딥러닝에서.
이 시리즈의 두 번째 기사에서는 딥러닝을 이용한 컴퓨터 비전, 우리는 탐구할 것입니다 전이 학습에 대한 심층 정보: 작동 이유, 존재하는 전략, 모델 선택 방법 사전 훈련된 권리와 PyTorch에서 완전한 파이프라인을 구현하는 방법. 산업 사례 연구를 살펴보겠습니다. 현실과 전문가들이 매일 사용하는 첨단 기술.
시리즈 개요
| # | Articolo | 집중하다 |
|---|---|---|
| 1 | CNN: 컨볼루셔널 네트워크 | 아키텍처, 교육, 배포 |
| 2 | 현재 위치 - 전이 학습 및 미세 조정 | 사전 훈련된 모델, 도메인 적응 |
| 3 | YOLO를 이용한 객체 감지 | 실시간 객체 감지 |
| 4 | 의미론적 분할 | 픽셀 수준 분류 |
| 5 | GAN 및 확산을 사용한 이미지 생성 | 합성 이미지 생성 |
| 6 | 엣지 배포 및 최적화 | 임베디드 장치의 모델 |
무엇을 배울 것인가
- 전이 학습이란 무엇이며 작동하는 이유(CNN의 기능 계층)
- 주요 전략: 특징 추출, 미세 조정, 도메인 적응
- 올바른 사전 학습 모델을 선택하는 방법(ResNet, EfficientNet, ViT, ConvNeXt)
- PyTorch의 완벽한 구현: 데이터 준비부터 배포까지
- 고급 기술: 차별적인 학습 속도, 점진적인 동결 해제, 학습 속도 준비
- 전이 학습에 최적화된 데이터 증대
- 실제 사례 연구: ResNet-50을 사용한 산업 결함 분류
- 객체 감지에 적용된 전이 학습(Faster R-CNN, YOLO)
- 흔히 발생하는 실수와 이를 방지하는 방법
1. 전이학습이란?
Il 전이 학습 훈련된 모델을 학습하는 머신러닝 기술 작업(라고 함)에 대해 소스 태스크)는 다른 작업의 시작점으로 재사용됩니다. (말했다 대상 작업). 수백만 개의 이미지에 대해 처음부터 신경망을 훈련시키는 대신, 이미 훈련된 모델을 사용하겠습니다(일반적으로 ImageNet에서는 1000개의 클래스에 120만 개의 이미지). 그리고 그것을 우리의 특정 문제에 맞게 조정합니다.
1.1 인간 비유
우리의 두뇌는 지속적으로 전이 학습 모드로 작동합니다. 새로운 것을 배우는 외과의사 수술 기술은 해부학, 생리학, 기본적인 손 기술을 다시 배울 필요가 없습니다. 음악가 재즈로 전환되는 클래식은 기악기법, 악보읽기, 이론을 전수한다. 고조파. Rust를 배우는 Python 프로그래머는 프로그래밍 개념을 전달하고 디버깅합니다. 정신적, 알고리즘적 논리. 이 모든 경우에는 사전 지식이 필요합니다. 가속하다 새로운 영역을 엄청나게 학습합니다.
1.2 작동 이유: 기능 계층 구조
CNN에서 Transfer Learning이 작동하는 근본적인 이유는 다음과 같습니다. 기능 계층 배웠다. 연구자들은 CNN이 ImageNet에서 교육을 받은 학생들은 추상화 수준이 높아지면서 구성된 기능을 학습합니다.
CNN의 기능 계층 구조
| 레이어 | 학습된 기능 | 특성 | 양도성 |
|---|---|---|---|
| 레이어 1~2 | 가장자리, 모서리, 색상 그라데이션 | 일반(작업에 구애받지 않음) | 매우 높음 |
| 레이어 3-4 | 질감, 반복되는 패턴, 기하학적 모티프 | 준일반 | 높은 |
| 레이어 5-6 | 물체의 일부(눈, 바퀴, 창문) | 준특이성 | 평균 |
| 레이어 7+ | 완전한 개체, 장면, 구성 | 작업별 | 낮은 |
첫 번째 레이어는 기능을 학습합니다. 만능인: 가장자리, 질감 및 그라데이션 모든 시각적 작업에 유용합니다. 중간 레이어는 더 복잡하지만 여전히 패턴을 포착합니다. 합리적으로 일반적입니다. 마지막 몇 개의 레이어만이 원래 작업에 매우 구체적입니다. 이는 대부분의 네트워크를 강력한 네트워크로 재사용할 수 있음을 의미합니다. 특징 추출기 마지막 부분만 우리 작업에 적용합니다.
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
공식적인 정의
작업 T_s가 포함된 소스 도메인 D_s와 작업 T_t가 포함된 대상 도메인 D_t가 주어지면 전이 학습 지식을 활용하여 목표 도메인에서 학습 기능을 향상시키는 것을 목표로 합니다. D_s 및 T_s에서 추출됩니다. 여기서 D_s != D_t 또는 T_s != T_t입니다. 기본적으로 세타 가중치는 학습되었습니다. 소스 작업의 데이터는 대상 작업에 대한 훈련을 위한 초기화(theta_0)로 사용됩니다. 무작위 초기화 대신.
2. 전이학습 전략
전이 학습을 적용하는 유일한 방법은 없습니다. 최적의 전략은 다음에 따라 달라집니다. 대상 데이터 세트의 크기, 소스 데이터 세트 및 리소스와의 유사성 사용할 수 있는 컴퓨팅 리소스. 4가지 주요 전략을 살펴보겠습니다.
2.1 특징 추출(Freeze Backbone, Train Classifier)
가장 간단하고 직접적인 전략: 예 얼다 (동결) 사전 훈련된 전체 네트워크 고정된 특징 추출기로 사용됩니다. 훈련된 유일한 부분은 새로운 부분이다 상단에 분류기가 추가되었습니다. 훈련 중에는 백본 가중치가 업데이트되지 않습니다.
사용 시기: 소규모 대상 데이터 세트(수백/수천 개의 이미지) e 소스와 유사한 도메인(예: 모델이 ImageNet에서 사전 훈련된 경우 개 품종 분류) 개 이미지가 많이 포함되어 있습니다).
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 미세 조정(일부/전체 레이어 고정 해제)
Fine-tuning에서는 미리 훈련된 가중치로 네트워크를 초기화한 후 yes 그들은 해동된다 일부 또는 모든 레이어를 재교육하고 전체 네트워크(또는 그 일부)를 재교육합니다. 학습률이 매우 낮습니다. 사전 훈련된 레이어는 다음을 수용하기 위해 약간 업데이트되었습니다. 이미 획득한 지식을 보존하면서 새로운 영역으로 이동합니다.
사용 시기: 중대형 대상 데이터 세트(수만 개의 이미지) 및/또는 소스와 약간 다른 도메인.
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 도메인 적응
La 도메인 적응 그리고 다음과 같은 경우에 사용되는 특수한 형태의 전이 학습 소스 도메인과 대상 도메인은 동일한 클래스를 공유하지만 데이터 분포가 있습니다. 다르다. 예를 들어, 전문적인 제품 사진에 대해 훈련된 모델이 작동해야 합니다. 공장에서 다양한 조명으로 촬영한 사진입니다. 다음과 같은 기술 젠장 (Domain-Adversarial Neural Network) 네트워크를 강제하는 도메인 판별자를 추가합니다. 도메인 불변 기능을 학습합니다.
2.4 제로샷 및 퓨샷 전송
같은 모델이 등장하면서 클립 (대조적 언어-이미지 사전 훈련), 이미지를 카테고리로 분류하는 것이 가능합니다 본 적이 없다 훈련 중에 (제로샷) 또는 아주 적은 예(소수의). CLIP은 배운다 결합된 텍스트-이미지 표현: "결함 사진"과 같은 텍스트 프롬프트가 제공됩니다. 용접'을 사용하면 모델은 특별한 교육 없이도 이미지를 분류할 수 있습니다.
전이 학습 전략 비교
| 전략 | 필요한 데이터 | 훈련 시간 | 성능 | 과적합 위험 |
|---|---|---|---|---|
| 특징 추출 | 100-1000 | Minuti | 좋은 | 매우 낮음 |
| 부분 미세 조정 | 1000-10000 | 시간 | 매우 좋은 | 베이스 |
| 전체 미세 조정 | 10000+ | 시간-일 | 훌륭한 | 중간 |
| 도메인 적응 | 변하기 쉬운 | 시간 | 좋음-뛰어남 | 중간 |
| 제로샷(CLIP) | 0 | 아무도 | 변하기 쉬운 | 아무도 |
3. 컴퓨터 비전을 위한 사전 훈련된 모델
사전 훈련된 모델을 선택하는 것은 중요한 결정입니다. 모든 아키텍처에는 장단점이 있습니다. 정확도, 추론 속도, 모델 크기 및 메모리 요구 사항이 다릅니다. 다음은 2025~2026년에 가장 많이 사용된 모델에 대한 개요입니다.
비교표 사전 훈련된 모델
| 모델 | 매개변수 | ImageNet 상위 1위 | 유형 | 이상적인 사용 |
|---|---|---|---|---|
| ResNet-50 | 25.6M | 76.1%(v1) / 80.9%(v2) | CNN | 견고한 기준, 손쉬운 배포 |
| EfficientNet-B0 | 530만 | 77.1% | CNN | 모바일, 엣지, 제한된 리소스 |
| EfficientNet-B7 | 66M | 84.3% | CNN | 최대 CNN 정확도 |
| ViT-B/16 | 86M | 77.9% (이미지넷-1k) | 트랜스포머 | 대규모 데이터 세트, 대규모 사전 학습 |
| ConvNeXt-T | 28.6M | 82.1% | 현대 CNN | 최고의 정확도/속도 절충 |
| ConvNeXt-B | 88.6M | 83.8% | 현대 CNN | CNN으로 높은 정확도가 필요할 때 |
| 스윈-T | 28.3M | 81.3% | 트랜스포머 | 탐지 및 세분화 |
| 클립 ViT-B/32 | 151M(조회) | 63.2%(제로샷) | 다중 모드 | 제로샷, 시각적 검색 |
| DINOV2 ViT-S/14 | 22M | 81.1%(선형 프로브) | 자체 감독 | 일반적인 특징, 라벨이 붙은 데이터가 거의 없음 |
3.1 ResNet-50: 주력 제품
ResNet-50 덕분에 전이 학습의 가장 인기 있는 모델로 남아 있습니다. 단순성, 훈련 안정성 및 광범위한 생태계 지원. 연결 건너뛰기(도입됨 이전 기사에서) Vanishing Gradient 문제 없이 심층 네트워크를 훈련할 수 있습니다. Mixup, CutMix 및 같은 최신 기술로 훈련된 가중치(IMAGENET1K_V2)의 V2 버전 Random Erasing은 인상적인 80.9%의 상위 1위를 달성했습니다.
3.2 EfficientNet: 복합 확장성
가족 EfficientNet 의 방법을 사용한다 복합 스케일링 정말 대단한 규모야 네트워크의 깊이, 너비 및 해상도를 균일하게 유지합니다. EfficientNet-B0은 다음에 이상적입니다. 리소스가 제한된 장치(매개변수 530만 개), B7은 최대 정확도를 제공합니다. (84.3%) 훨씬 더 큰 모델(66M 매개변수)의 비용이 듭니다.
3.3 ViT(Vision Transformer) 및 Swin Transformer
I 비전 트랜스포머 Transformer 아키텍처를 적용합니다(원래 NLP용으로 생성됨)을 컴퓨터 비전으로 변환합니다. 이미지는 패치(예: 16x16픽셀)로 분할됩니다. 각 패치는 "토큰"으로 처리되며 self-attention으로 처리됩니다. ViT 엑셀 대규모 데이터 세트(ImageNet-21k, JFT-300M)에 대해 사전 훈련된 경우 CNN에 비해 작은 데이터 세트에 효과적입니다. 스윈 트랜스포머 소개하다 슬라이딩 윈도우(이동된 윈도우)에 주의를 기울여 보다 효율적이고 특히 탐지, 분할 등 밀도가 높은 작업에 적합합니다.
3.4 ConvNeXt: 현대화된 CNN
ConvNeXt CNN이 현대화되면 Transformer와 경쟁할 수 있음을 증명합니다. 동일한 훈련 기술(AdamW, Mixup, 레이어 스케일, Stochastic Depth)을 사용합니다. ConvNeXt-T 2,860만 개의 매개변수만으로 82.1%에 도달하여 정확도와 배포 속도와 단순성.
3.5 DINOv2: 자기 지도 학습
DINOV2 자기 지도 학습으로 훈련된 모델(라벨 없음) 선별된 대규모 데이터 세트(LVD-142M 이미지) 추출된 특징은 매우 일반적입니다. 전송 가능: 상단에 추가된 간단한 선형 분류기가 결과를 얻습니다. 지도 모델의 완전한 미세 조정으로 경쟁력을 갖출 수 있습니다. 특히 다음과 같은 경우에 유용합니다. 대상 도메인에 라벨이 지정된 데이터가 거의 없습니다.
4. 전이 학습을 사용해야 하는 경우: 결정 매트릭스
전략의 선택은 두 가지 주요 요소에 따라 달라집니다. 데이터 세트의 크기 목표 그리고 유사 원본 도메인과 대상 도메인 사이. 이는 4개의 결정 사분면을 생성합니다.
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) |
+-------------------------+-------------------------+
실제 규칙
2025~2026년에는 “전이학습을 사용해야 하는가?”라는 질문에 대한 답변이 나왔습니다. 그리고 거의 항상 si. CNN을 처음부터 훈련하고 매우 특정한 경우에만 정당화됩니다. 거대한 데이터 세트(수백만 개의 이미지), 자연 이미지와 근본적으로 다른 도메인 (예: 스펙트로그램, 레이더 신호) 또는 특정 아키텍처 제약.
5. PyTorch를 이용한 구현
연습을 계속해 봅시다. PyTorch에서 Transfer Learning을 단계별로 구현하겠습니다. 사전 훈련된 모델을 로드하는 것부터 시작하여 전체 훈련까지.
5.1 사전 훈련된 모델 로딩
PyTorch는 사전 훈련된 모델을 로드하기 위한 두 가지 API를 제공합니다. 최신 API(다음에서 소개됨)
torchvision 0.13+)은 열거형을 사용합니다. Weights 자세한 정보를 제공하는
필요한 전처리 변환을 포함한 가중치에 대한 것입니다.
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 특징 추출: 분류기 동결 및 교체
ResNet-50을 특징 추출기로 사용하려면 다음이 필요합니다. (1) 모든 백본 매개변수를 고정하고, (2) 마지막 완전 연결 레이어를 클래스 수에 적합한 새 레이어로 교체합니다.
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 전이학습을 위한 데이터 증강
데이터 증강은 전이 학습의 기본이며, 특히 작은 데이터 세트의 경우 더욱 그렇습니다. 변환은 사전 학습된 모델의 전처리와 호환되어야 합니다. 특히 정규화에서는 ImageNet 평균과 표준편차를 사용해야 합니다. (평균=[0.485, 0.456, 0.406], 표준=[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 및 TrivialAugmentWide
강화 선택을 단순화하기 위해 PyTorch는 자동 정책을 제공합니다.
랜드증강 강도 M으로 N개의 무작위 변환을 적용합니다.
TrivialAugmentWide 임의의 강도로 단일 변환을 적용합니다.
복잡한 전략보다 더 효과적인 경우가 많습니다. 수동 변환만 대체
한 줄로 :
transforms.TrivialAugmentWide() 또는
transforms.RandAugment(num_ops=2, magnitude=9).
5.4 완전한 훈련 루프
전이 학습의 훈련 루프는 표준 루프와 유사하지만 몇 가지 주의 사항이 있습니다. 학습률을 낮추고 점진적인 워밍업을 수행하며 과적합을 주의 깊게 모니터링합니다.
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. 고급 미세 조정
기본 미세 조정(단일 학습률로 모든 레이어 고정 해제)은 종종 그리고 최적의 전략. 고급 기술을 사용하면 상당한 성능을 얻을 수 있습니다. 특히 중간 규모의 데이터 세트에서는 더 좋습니다.
6.1 차별적인 학습률
아이디어는 간단하지만 강력합니다. 할당 다양한 학습률 레이어 그룹에 다르다. 일반 기능을 학습한 초기 레이어를 업데이트해야 합니다. 아주 조금, 중간 레이어를 조금 더 만들고 학습률이 있는 최종 분류기 키가 크다. 이는 최종 레이어를 조정하면서 초기 레이어의 지식을 보존합니다.
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 점진적 동결 해제
모든 레이어를 한 번에 녹이는 대신 점진적인 해동 그것은 마지막 레이어에서 첫 번째 레이어로, 에포크마다 진행됩니다. 이는 분류자에게 다음과 같은 시간을 제공합니다. 기본 기능을 수정하기 전에 적응하여 파괴적인 업데이트를 피하세요.
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 학습률 준비 및 코사인 어닐링
초기 에포크의 점진적인 학습 속도 준비는 너무 공격적인 업데이트를 방지합니다. 이는 사전 훈련된 가중치를 파괴할 수 있습니다. 워밍업 후 약간의 어닐링 일정 점점 더 섬세한 미세 조정을 위해 학습 속도를 점차 감소시킵니다.
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 혼합 및 CutMix 확장
혼동 e 컷믹스 그것은 진보된 증강 기술이다 특히 미세 조정에 효과적입니다. Mixup은 선형 조합으로 새로운 샘플을 생성합니다. 두 개의 이미지와 각각의 라벨이 포함되어 있습니다. CutMix는 서로 다른 이미지 사이에 직사각형을 잘라서 붙여넣습니다. 둘 다 강력한 정규화 도구로 작동하고 일반화를 향상시킵니다.
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. 전이 학습을 위한 고급 데이터 증강
데이터 증대의 선택은 성능에 큰 영향을 미칩니다. 작은 데이터 세트로. 2025~2026년에 가장 효과적인 옵션을 살펴보겠습니다.
7.1 앨범화: 전문가 수준 강화
앨범화 이미지 데이터 증대를 위한 전문 라이브러리, OpenCV를 사용하므로 torchvision.transforms보다 훨씬 빠릅니다. 그것은 제공합니다 특수 영역(의료, 산업, 위성)에 특히 유용한 변환입니다.
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. 사례 연구: ResNet-50을 사용한 산업 결함 분류
실제 사용 사례인 산업 품질 검사 시스템을 통해 이 모든 것을 종합해 보겠습니다. 전자 부품의 결함을 분류하는 것입니다. 데이터세트에는 고해상도 이미지가 포함되어 있습니다. 4가지 등급의 인쇄 회로 기판(PCB): OK (결함 없음), 솔더 브릿지 (용접 브릿지), 누락된 구성요소 (누락된 구성요소) e 할퀴다 (할퀴다).
사례 연구 설정
- 데이터세트: 이미지 4,000개(클래스당 1,000개), 70/15/15 분할
- 해결: 512x512픽셀, RGB
- 모델: ImageNet에서 사전 훈련된 ResNet-50(V2 가중치)
- 전략: 특징 추출 -> 점진적인 미세 조정
- 하드웨어: 8GB 이상의 VRAM을 갖춘 NVIDIA GPU
"""
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 평가 및 지표
품질 검사 시스템의 경우 글로벌 정확도로는 충분하지 않습니다. 우리는 분석해야 한다 각 결함 클래스에 대한 정밀도, 재현율 및 F1 점수. 위음성(검출되지 않은 결함) 이는 거짓 긍정(좋은 부분이 폐기됨)보다 훨씬 더 많은 비용이 듭니다.
@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 TorchScript를 사용한 배포
모델을 프로덕션에 적용하기 위해 다음으로 변환합니다. 토치스크립트: 형식 Python 없이도 실행할 수 있고 C++ 시스템에 통합하는 데 이상적입니다. 고성능 모바일 애플리케이션 또는 추론 서버.
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. 객체 감지를 위한 전이 학습
전이 학습은 분류에만 국한되지 않습니다. 모든 주요 프레임워크 객체 감지는 미리 훈련된 백본을 특징 추출기로 사용합니다. 보자 Faster R-CNN과 YOLO에 미세 조정을 적용하는 방법.
9.1 더 빠른 R-CNN 미세 조정
더 빠른 R-CNN ResNet-50-FPN 백본과 참조 검출기를 사용하여 토치비전. FPN(Feature Pyramid Network)은 다음과 같은 기본이 되는 다중 규모 특징을 추출합니다. 다양한 크기의 물체를 감지합니다. 미세 조정을 하려면 헤드를 교체해야 합니다. 상자 예측자 분류.
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 YOLO 미세 조정
YOLO의 경우 CLI 덕분에 전이 학습이 더욱 쉬워졌습니다. 울트라리틱스. YOLO는 사전 훈련된 백본(종종 CSPDarknet 또는 변형) 단일 명령줄 또는 몇 줄의 코드로 미세 조정을 지원합니다.
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. 일반적인 실수와 이를 피하는 방법
전이 학습은 이론적으로는 간단해 보이지만 실제로는 많은 함정이 있습니다. 공연을 망칠 수 있습니다. 가장 자주 발생하는 오류는 다음과 같습니다.
전이 학습의 10가지 실수
| # | 실수 | 결과 | 해결책 |
|---|---|---|---|
| 1 | 학습률이 너무 높음 | 사전 훈련된 가중치를 파괴합니다. | 처음부터 훈련하는 것보다 10-100x 낮은 LR 사용(1e-4 이하) |
| 2 | 잘못된 정규화 | 완전히 잘못된 기능 | ImageNet의 사전 훈련된 모델에는 항상 ImageNet의 평균/표준을 사용하십시오. |
| 3 | 데이터 증대 없음 | 작은 데이터세트로 과적합 | 공격적인 증강: 뒤집기, 회전, 색상 지터, CutMix |
| 4 | BatchNorm이 평가 모드가 아닙니다. | 소규모 배치의 불안정한 통계 | 고정된 기능의 경우 model.eval() 또는 BN을 명시적으로 고정합니다. |
| 5 | 입력 이미지의 크기가 조정되지 않음 | 오류 또는 끔찍한 성능 | 224x224(또는 모델에서 예상하는 크기)로 크기 조정 |
| 6 | 워밍업 없이 미세 조정 | 초기 업데이트가 너무 공격적임 | 3~5 에포크에 대한 선형 준비 |
| 7 | 모든 레이어에 대해 동일한 LR | 초기 레이어가 과도하게 편집됨 | 차별적인 학습률 |
| 8 | 정규화가 이루어지지 않은 데이터세트가 너무 작습니다. | 심각한 과적합 | 탈락, 중량 감소, 라벨 스무딩, 조기 정지 |
| 9 | 최고의 모델을 저장하지 마세요 | 최신 시대 모델 사용(과적합) | 정확도/손실 값을 기반으로 한 모델 체크포인트 |
| 10 | 클래스 불균형 무시 | 다수 계층에 편향된 모델 | 가중 손실, 오버샘플링, 초점 손실 |
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. 의사결정나무: 모델 및 전략 선택
더 쉽게 선택할 수 있도록 사용 사례를 다루는 실용적인 의사결정 트리가 있습니다. 더 일반적입니다.
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
실습 권장 사항 2025-2026
대부분의 컴퓨터 비전 프로젝트의 경우 다음으로 시작하세요. ResNet-50 V2 기준으로. 더 많은 정확성이 필요하면 다음으로 이동하세요. ConvNeXt-T (최고의 정확도/속도 절충). 데이터가 거의 없는 작업의 경우 다음을 사용하세요. DINOV2 특징 추출기로. 엣지/모바일 배포의 경우 다음을 선택하세요. EfficientNet-B0 o MobileNet-V3.
12. 결론
전이 학습은 컴퓨터 비전을 민주화했습니다. 한때 필요했던 것 수백만 장의 이미지, 몇 주간의 교육 및 고가의 하드웨어를 지금 당장 달성할 수 있습니다. 수백 개의 이미지, 단일 GPU 및 몇 시간의 작업이 필요합니다. 직관 이 기사의 핵심은 다음과 같습니다.
- CNN 기능은 계층적이며 전송 가능합니다. 첫 번째 레이어 캡처 보편적인 패턴(가장자리, 텍스처), 후자는 작업별로 다릅니다. 이렇게 하면 전송이 이루어집니다. 학습이 가능하고 효과적입니다.
- 전략은 데이터와 도메인에 따라 다릅니다. 비슷한 도메인에 데이터가 거의 없나요? 특징 추출. 다른 도메인에 많은 데이터가 있습니까? 주의해서 전체 미세 조정을 수행하세요. 4사분면 행렬 결정을 안내합니다.
- 세부 사항이 차이를 만듭니다. 차별적인 학습률, 점진적인 동결 해제, 준비, 올바른 정규화 및 적절한 데이터 확대를 통해 성능을 향상할 수 있습니다. 순진한 미세 조정에 비해 5-15% 정도.
- 생태계는 성숙해졌습니다. PyTorch, torchvision 및 Ultralytics는 간단한 API를 제공합니다. 고품질의 사전 훈련된 모델을 제공합니다. 처음부터 훈련할 이유가 거의 없습니다.
시리즈의 다음 기사에서는 전이 학습을 다음에 적용할 것입니다.객체 감지 욜로와 함께: 단순한 분류가 아닌 무엇 이미지에도 있지만 어디 실시간 경계 상자를 사용하여 검색됩니다.
다음 기사
기사 3: YOLO를 사용한 객체 감지 - 실시간 객체 감지, YOLO 아키텍처, 앵커 없는 감지, 맞춤형 데이터세트에 대한 교육 및 배포 실시간 애플리케이션용.







