転移学習: 事前トレーニングされたモデルの再利用
子供に犬の品種を見分ける方法を教えなければならないことを想像してみてください。もしその子がすでに学んでいるなら 形、色、質感、一般的な解剖学的構造を認識するには、その作業は非常に複雑になります。 シンプル。ゼロから始める必要はありません。 移行 すでに得た知識を新しい人に タスク。これはまさに、 転移学習 ディープラーニングでは。
このシリーズの 2 番目の記事では、 深層学習を使用したコンピューター ビジョン、探索してみます 転移学習について詳しく説明します。転移学習が機能する理由、どのような戦略が存在するか、モデルの選択方法 事前にトレーニングされた正しい方法と、PyTorch で完全なパイプラインを実装する方法。業界の事例を見てみましょう プロフェッショナルが日常的に使用している高度なテクニックと現実。
シリーズ概要
| # | アイテム | 集中 |
|---|---|---|
| 1 | CNN: 畳み込みネットワーク | アーキテクチャ、トレーニング、展開 |
| 2 | 現在位置 - 転移学習と微調整 | 事前トレーニングされたモデル、ドメイン適応 |
| 3 | YOLO による物体検出 | リアルタイムの物体検出 |
| 4 | セマンティックセグメンテーション | ピクセルレベルの分類 |
| 5 | GAN による画像生成と拡散 | 合成画像生成 |
| 6 | エッジの導入と最適化 | 組み込みデバイス上のモデル |
何を学ぶか
- 転移学習とは何か、そしてそれが機能する理由 (CNN の機能階層)
- 主な戦略: 特徴抽出、微調整、ドメイン適応
- 適切な事前トレーニング済みモデル (ResNet、EfficientNet、ViT、ConvNeXt) を選択する方法
- PyTorch での完全な実装: データの準備からデプロイメントまで
- 高度なテクニック: 学習率の識別、段階的なフリーズ解除、学習率のウォームアップ
- 転移学習用に最適化されたデータ拡張
- 実践的なケーススタディ: ResNet-50 による産業上の欠陥分類
- 転移学習を物体検出に適用 (高速 R-CNN、YOLO)
- よくある間違いとその回避方法
1.転移学習とは
Il 転移学習 訓練されたモデルを使用する機械学習技術 タスク (と呼ばれる) ソースタスク) は、別のタスクの開始点として再利用されます (言った 対象タスク)。何百万もの画像を使ってニューラル ネットワークをゼロからトレーニングするのではなく、 すでにトレーニング済みのモデルを考えてみましょう (通常は ImageNet 上にあり、1000 クラスに 120 万枚の画像があります) そしてそれを私たちの特定の問題に適応させます。
1.1 人間の例え
私たちの脳は常に転移学習モードで動作しています。新しいことを学ぶ外科医 手術技術を習得するために、解剖学、生理学、および基本的な手先のスキルを学び直す必要はありません。ミュージシャン クラシックからジャズに移行することで、楽器のテクニック、楽譜の読み方、理論が伝わります。 高調波。 Rust を学習している Python プログラマーがプログラミングの概念を伝え、デバッグを行う 精神的およびアルゴリズム的なロジック。これらすべての場合において、事前の知識 加速する 新しい領域を徹底的に学びます。
1.2 なぜ機能するのか: 機能階層
CNN で転移学習が機能する根本的な理由は次のとおりです。 機能階層 学んだ。研究者らは、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 が与えられた場合、転移学習 知識を用いて対象領域における学習関数 f_t の改善を目指す D_s と T_s から抽出されます (D_s != D_t または T_s != T_t)。基本的に、シータの重みは学習されます ソース タスクからのデータは、ターゲット タスクでのトレーニングの初期化 (theta_0) として使用されます。 ランダムな初期化の代わりに。
2. 転移学習戦略
転移学習を適用する単一の方法はありません。最適な戦略は以下によって異なります。 ターゲット データセットのサイズ、ソース データセットおよびリソースとの類似性 利用可能な計算リソース。 4つの主な戦略を見てみましょう。
2.1 特徴抽出 (バックボーンの凍結、分類器のトレーニング)
最もシンプルで直接的な戦略: はい フリーズする 事前トレーニングされたネットワーク全体を(フリーズ) 固定特徴抽出器として使用されます。トレーニングされる唯一の部分は新しい部分です 分類子が上に追加されました。バックボーンの重みはトレーニング中に更新されません。
いつ使用するか: 小規模なターゲット データセット (数百または数千の画像) ソースに類似したドメイン (例: モデルが 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 微調整 (一部/すべてのレイヤーのフリーズ解除)
微調整では、事前トレーニングされた重みでネットワークを初期化した後、はい 彼らは解ける 一部またはすべての層を再トレーニングし、ネットワーク全体 (またはその一部) を再トレーニングします。 学習率が非常に低い。事前トレーニングされたレイヤーは、以下に対応するためにわずかに更新されています。 すでに取得した知識を維持したまま、新しいドメインに移行します。
いつ使用するか: 中規模から大規模のターゲット データセット (数千から数万の画像) および/またはソースと適度に異なるドメイン。
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 ドメイン適応 および次の場合に使用される特殊な形式の転移学習 ソース ドメインとターゲット ドメインは同じクラスを共有しますが、データが分散されています。 違う。たとえば、プロの製品写真でトレーニングされたモデルは機能する必要があります。 工場で照明を変えて撮影した写真について。のようなテクニック くそ (ドメイン敵対的ニューラル ネットワーク) ネットワークを強制するドメイン識別子を追加します。 ドメイン不変の特徴を学習します。
2.4 ゼロショット転送とフューショット転送
のようなモデルの登場により、 クリップ (対照言語イメージ事前トレーニング)、 画像をカテゴリに分類することが可能 見たことない トレーニング中に (ゼロショット) または非常に少ない例 (数発のショット)。クリップが学習します テキストと画像の結合表現: 「欠陥の写真」などのテキスト プロンプトが与えられる 「溶接」の場合、モデルは特別なトレーニングなしで画像を分類できます。
転移学習戦略の比較
| 戦略 | 必要なデータ | トレーニング時間 | パフォーマンス | 過剰適合のリスク |
|---|---|---|---|---|
| 特徴抽出 | 100-1000 | Minuti | 良い | 非常に低い |
| 部分的な微調整 | 1000-10000 | 営業時間 | とても良い | ベース |
| 完全な微調整 | 10000+ | 時間-日 | 素晴らしい | 中くらい |
| ドメイン適応 | 変数 | 営業時間 | 良好~良好 | 中くらい |
| ゼロショット(CLIP) | 0 | 誰でもない | 変数 | 誰でもない |
3. コンピュータビジョン用の事前トレーニング済みモデル
事前トレーニングされたモデルを選択することは重要な決定です。すべてのアーキテクチャにはトレードオフがあります 精度、推論速度、モデルのサイズ、メモリ要件は異なります。 ここでは、2025 年から 2026 年に最も使用されているモデルの概要を示します。
比較表 事前トレーニング済みモデル
| モデル | パラメータ | イメージネットトップ1 | タイプ | 理想的な使用法 |
|---|---|---|---|---|
| レスネット-50 | 25.6M | 76.1% (v1) / 80.9% (v2) | CNN | 堅固なベースライン、簡単な導入 |
| EfficientNet-B0 | 5.3M | 77.1% | CNN | モバイル、エッジ、限られたリソース |
| EfficientNet-B7 | 66M | 84.3% | CNN | 最大の CNN 精度 |
| ViT-B/16 | 86M | 77.9% (ImageNet-1k) | トランスフォーマー | 大規模なデータセット、大規模な事前トレーニング |
| ConvNext-T | 28.6M | 82.1% | 現代の CNN | 最高の精度と速度のトレードオフ |
| ConvNext-B | 88.6M | 83.8% | 現代の CNN | CNNで高い精度が必要な場合 |
| Swin-T | 28.3M | 81.3% | トランスフォーマー | 検出とセグメンテーション |
| クリップ ViT-B/32 | 151M(眺望) | 63.2% (ゼロショット) | マルチモーダル | ゼロショット、ビジュアル検索 |
| DINOv2 ViT-S/14 | 22M | 81.1% (リニアプローブ) | 自己監視型 | 一般的な特徴、ラベル付きデータはほとんどない |
3.1 ResNet-50: 主力製品
レスネット-50 転移学習の最も人気のあるモデルであり続けているのは、 シンプルさ、トレーニングの安定性、幅広いエコシステムのサポート。スキップ接続 (導入されました) 前の記事で)、勾配消失問題を発生させずにディープ ネットワークをトレーニングできるようになります。 Mixup、CutMix などの最新のテクニックでトレーニングされたウェイトの V2 バージョン (IMAGENET1K_V2) ランダム消去により、トップ 1 率は 80.9% という驚異的な数字に達します。
3.2 EfficientNet: 複合スケーラビリティ
家族 EfficientNet の方法を使用します 複合スケーリング なんてスケールなんだ ネットワークの深さ、幅、解像度を均一にします。 EfficientNet-B0 は次のような用途に最適です。 リソースが限られているデバイス (530 万パラメータ) に対し、B7 は最高の精度を提供します (84.3%)、はるかに大規模なモデル (6,600 万パラメータ) を犠牲にします。
3.3 ビジョントランスフォーマー (ViT) と Swin トランスフォーマー
I ビジョントランスフォーマー Transformer アーキテクチャを適用します (元々は NLP 用に作成されました) をコンピューター ビジョンに適用します。画像はパッチ (例: 16x16 ピクセル) に分割されます。 各パッチは「トークン」として扱われ、セルフアテンションで処理されます。 ViTエクセル 大規模なデータセット (ImageNet-21k、JFT-300M) で事前トレーニングされた場合は、より少ない可能性があります CNN と比較して小規模なデータセットで効果的です。 スイングトランス 紹介します スライディング ウィンドウ (シフト ウィンドウ) に注意を払い、より効率的で特に 検出やセグメンテーションなどの高密度のタスクに適しています。
3.4 ConvNext: 最新化された CNN
次への変換 CNN が近代化されればトランスフォーマーと競合できることを証明 同じトレーニング手法 (AdamW、Mixup、レイヤー スケール、Stochastic Depth) を使用します。 ConvNext-T わずか 2,860 万のパラメータで 82.1% に達し、精度と 導入のスピードとシンプルさ。
3.5 DINOv2: 自己教師あり学習
DINOv2 自己教師あり学習でトレーニングされたモデル (ラベルなし) 厳選された巨大なデータセット (LVD-142M 画像) 上で。抽出された特徴は非常に汎用的です 転送可能: 上部に追加された単純な線形分類器が結果を達成します 教師ありモデルの完全な微調整により競争力を高めます。そして特に便利なのは、 ターゲット ドメインにラベル付けされたデータはほとんどありません。
4. 転移学習をいつ使用するか: 意思決定マトリックス
戦略の選択は、次の 2 つの重要な要素によって決まります。 データセットのサイズ ターゲット そして 類似性 between the source domain and the target domain. これにより、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 で転移学習を段階的に実装していきます。 事前トレーニングされたモデルのロードから始まり、完全なトレーニングまで。
5.1 事前トレーニング済みモデルのロード
PyTorch は、事前トレーニングされたモデルをロードするための 2 つの 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 個のランダム変換を適用します。
トリビアルオーグメントワイド ランダムな強度で単一の変換を適用します。
そして多くの場合、複雑な戦略よりも効果的です。手動変換を置き換えるだけです
1 行で:
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 学習率のウォームアップとコサインアニーリング
初期のエポックにおける段階的な学習率のウォームアップにより、過度の更新を防止します which could destroy pre-trained weights.ウォームアップ後、少しのアニーリングスケジュール 学習率を徐々に下げて、より繊細な微調整を可能にします。
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 ミックスアップとカットミックス拡張
取り違え e カットミックス それらは高度な拡張技術です 特に微調整に効果的です。 Mixup は線形結合として新しいサンプルを作成します 2 つの画像とそれぞれのラベル。 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 を使用した工業用欠陥の分類
実際の使用例ですべてをまとめてみましょう: 産業用品質検査システム 電子部品の欠陥を分類するものです。データセットには高解像度の画像が含まれています プリント基板 (PCB) の 4 つのクラス: OK (欠陥なし)、 はんだブリッジ (溶接ブリッジ)、 不足しているコンポーネント (コンポーネントが欠落しています) e scratch (傷)。
ケーススタディのセットアップ
- データセット: 4000 画像 (クラスあたり 1000)、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 ~ 100 倍低い LR を使用します (1e-4 以下) |
| 2 | 間違った正規化 | 完全に間違った機能 | ImageNet 上の事前トレーニング済みモデルには常に ImageNet の平均値/標準値を使用してください |
| 3 | データ拡張なし | 小さなデータセットでの過学習 | 積極的な拡張: 反転、回転、カラージッター、カットミックス |
| 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
ほとんどのコンピューター ビジョン プロジェクトでは、次のことから始めます。 レスネット-50 V2 ベースラインとして。さらに正確さが必要な場合は、次へ進んでください。 ConvNext-T (最高の精度と速度のトレードオフ)。データが非常に少ないタスクの場合は、次を使用します。 DINOv2 特徴抽出器として。エッジ/モバイル展開の場合は、選択します EfficientNet-B0 o モバイルネット-V3.
12. 結論
転移学習はコンピュータ ビジョンを民主化しました。かつて必要だったもの 数百万枚の画像、数週間のトレーニング、高価なハードウェアを駆使すれば、今日でも達成可能です 数百枚の画像、単一の GPU、数時間の作業を必要とします。直感 この記事の鍵となるのは次のとおりです。
- CNN の機能は階層化されており、転送可能です。 最初の層のキャプチャ 普遍的なパターン (エッジ、テクスチャ)、後者はタスク固有です。これにより転送が行われます 学習が可能かつ効果的です。
- 戦略はデータとドメインによって異なります。 同様のドメインにデータがほとんどない場合特長 抽出。異なるドメインに大量のデータがありますか?慎重に完全な微調整を行ってください。 4象限マトリックス 決定を導きます。
- 詳細が違いを生みます。 識別学習率、段階的な凍結解除、 ウォームアップ、正しい正規化、適切なデータ拡張によりパフォーマンスが向上します。 単純な微調整と比較して 5 ~ 15% 減少します。
- エコシステムは成熟しています: PyTorch、torchvision、Ultralytics はシンプルな API を提供します 高品質の事前トレーニング済みモデル。ゼロからトレーニングする理由はほとんどありません。
シリーズの次の記事では、転移学習を次の目的に適用します。物体検出 ヨロと一緒に: 分類するだけではありません cosa 画像にもありますが、 どこ リアルタイム境界ボックスを使用して検出されます。
次の記事
第 3 条: YOLO による物体検出 - リアルタイムの物体検出、 YOLO アーキテクチャ、アンカーフリー検出、カスタム データセットのトレーニングとデプロイメント リアルタイムアプリケーション向け。







