PyTorch-Transferlernen

Transfer Learning ist eine Technik, bei der ein auf einem großskalierten Datensatz vortrainiertes Modell genommen und auf eine neue Aufgabe mit einer geringeren Datenmenge für das Training übertragen wird.

Es ist eine der heute am weitesten verbreiteten Techniken in der Praxis des Deep Learning — in den meisten Fällen erzielt Transfer Learning bessere Ergebnisse, ist schneller und benötigt weniger Daten als das Training von Grund auf.


1. Kernidee des Transfer Learnings

Die von tiefen neuronalen Netzwerken auf ImageNet gelernten Merkmale sind universell:

  • Flache Schichten: lernen allgemeine Low-Level-Merkmale (Kanten, Texturen, Farbverläufe)
  • Mittlere Schichten: lernen allgemeine Mid-Level-Merkmale (Formen, Teile, Texturkombinationen)
  • Tiefe Schichten: erlernen aufgabenspezifische High-Level-Merkmale (Gesichter, Räder, Text)

Diese Low-Level- und Mid-Level-Merkmale sind für die überwiegende Mehrheit der Vision-Aufgaben wirksam und müssen nicht neu gelernt werden.

Wann sollte man Transfer Learning einsetzen?

DatenmengeÄhnlichkeit zur QuellaufgabeEmpfohlene Strategie
Klein (< 1000)HochNur den finalen Klassifikationskopf ersetzen, das gesamte Backbone einfrieren.
Klein (< 1000)NiedrigFlachere Schichten feinabstimmen, tiefere Schichten einfrieren.
Mittel (1000~10000)HochDas gesamte Netzwerk mit einer kleinen Lernrate feinabstimmen.
Mittel (1000~10000)NiedrigTiefere Schichten feinabstimmen, flachere Schichten einfrieren.
Groß (> 10000)AnyAlles feinabstimmen oder Training von Grund auf in Betracht ziehen.

Vergleich der drei Kernstrategien

Die Schichtenstruktur eines vortrainierten Modells (z. B. ResNet50) ist wie folgt:

  • Conv-Schicht 1~3 (Low-Level-Merkmale: Kanten/Texturen): normalerweise eingefroren
  • Conv-Schicht 4~6 (Mid-Level-Merkmale: Formen/Teile): optional eingefroren
  • Conv-Schicht 7~N (High-Level-Merkmale: semantische Informationen): feinabgestimmt
  • Classifier-Head: ersetzt und trainiert
┌─────────────────────────────────────────────────┐
│              预训练模型(如 ResNet50)             │
│  ┌──────────────────────────────────────────┐   │
│  │  Conv Layer 1~3(低级特征:边缘/纹理)      │  ← 通常冻结
│  ├──────────────────────────────────────────┤   │
│  │  Conv Layer 4~6(中级特征:形状/部件)      │  ← 可选冻结
│  ├──────────────────────────────────────────┤   │
│  │  Conv Layer 7~N(高级特征:语义信息)       │  ← 微调
│  ├──────────────────────────────────────────┤   │
│  │  Classifier Head(分类头)                 │  ← 替换 & 训练
│  └──────────────────────────────────────────┘   │
└─────────────────────────────────────────────────┘

2. Laden vortrainierter Modelle

PyTorch bietet viele offizielle vortrainierte Modelle über torchvision.models, und sie zu laden ist sehr einfach.

Beispiel

import torch
import torchvision.models as models

# Vortrainiertes Modell laden (lädt Gewichte automatisch herunter)
# PyTorch >= 0.13 empfiehlt die neue Syntax: den weights-Parameter verwenden
from torchvision.models import ResNet50_Weights

model = models.resnet50(weights=ResNet50_Weights.IMAGENET1K_V2)

# Alte Syntax (funktioniert weiterhin, löst aber eine Veraltungswarnung aus)
model = models.resnet50(pretrained=True)

# Vortrainierte Gewichte nicht laden (nur die Netzwerkarchitektur verwenden)
model = models.resnet50(weights=None)

Anzeigen der Modellstruktur

Beispiel

# Die vollständige Struktur ausgeben
print(model)

# Nur die letzten Schichten anzeigen (Klassifikationskopf)
print(model.fc)
# Linear(in_features=2048, out_features=1000, bias=True)

# Anzahl der Parameter zählen
total_params    = sum(p.numel() for p in model.parameters())
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Total parameters: {total_params:,}")
print(f"Trainierbare Parameter: {trainable_params:,}")

Namen der Klassifikationsköpfe für verschiedene Modelle

Verschiedene Modelle haben unterschiedliche Attributnamen für ihre Klassifikationsköpfe; beim Transferlernen müssen Sie die entsprechende Schicht ersetzen:

ModellAttribut des Klassifikationskopfs
ResNet / RegNetmodel.fc
VGG / AlexNetmodel.classifier[-1]
DenseNetmodel.classifier
EfficientNetmodel.classifier[-1]
MobileNetV2/V3model.classifier[-1]
ViT (Vision Transformer)model.heads.head
ConvNeXtmodel.classifier[-1]
Inception V3model.fc
Swin Transformermodel.head

3. Drei Transfer-Learning-Strategien

3.1 Strategie 1: Merkmalsextraktion (Alles einfrieren)

Alle Parameter des vortrainierten Modells einfrieren und nur den neu ersetzten Klassifikationskopf trainieren.

Geeignet für Szenarien mit sehr wenigen Daten (einige hundert Bilder) oder wenn die Aufgabe der Quellaufgabe sehr ähnlich ist.

Beispiel

import torch
import torch.nn as nn
import torchvision.models as models
from torchvision.models import ResNet18_Weights

NUM_CLASSES = 5   # Anzahl der Klassen für die Zielaufgabe

# Schritt 1: Das vortrainierte Modell laden
model = models.resnet18(weights=ResNet18_Weights.IMAGENET1K_V1)

# Schritt 2: Alle Parameter einfrieren
for param in model.parameters():
    param.requires_grad = False

# Schritt 3: Den Klassifikationskopf ersetzen (diese Parameter haben standardmäßig requires_grad=True)
in_features = model.fc.in_features    # 512
model.fc = nn.Linear(in_features, NUM_CLASSES)

# Überprüfen: Nur der Klassifikationskopf ist trainierbar
trainable = [(n, p.shape) for n, p in model.named_parameters() if p.requires_grad]
print(f"Anzahl der trainierbaren Schichten: {len(trainable)}")
for name, shape in trainable:
    print(f" {name}: {shape}")
# Output:
# fc.weight: torch.Size([5, 512])
#   fc.bias:   torch.Size([5])

# Schritt 4: Nur die trainierbaren Parameter an den Optimierer übergeben (effizienter)
optimizer = torch.optim.Adam(
    filter(lambda p: p.requires_grad, model.parameters()),
    lr=1e-3
)
# Oder ein gleichwertiger, sauberer Ansatz:
optimizer = torch.optim.Adam(model.fc.parameters(), lr=1e-3)

Die Feature-Extraktion ist die einfachste und am häufigsten verwendete Transfer-Learning-Strategie, besonders geeignet für Szenarien mit geringen Datenmengen.

3.2 Strategie 2: Fine-Tuning

Alle oder einige der vorab trainierten Schichten auftauen und das gesamte Modell mit einer kleineren Lernrate trainieren.

Geeignet für Szenarien mit einer mittleren Datenmenge oder wenn die Aufgabe von der Quellaufgabe abweicht.

Beispiel

import torch
import torch.nn as nn
import torchvision.models as models

NUM_CLASSES = 10
model = models.resnet50(weights='IMAGENET1K_V2')

# Methode A: Vollständiges Fine-Tuning (alle Schichten auftauen)
# Zuerst einfrieren
for param in model.parameters():
    param.requires_grad = False

# Dann auftauen (entspricht vollständigem Fine-Tuning; dieser Stil wird häufig in Szenarien mit schrittweisem Auftauen verwendet)
for param in model.parameters():
    param.requires_grad = True

# Klassifikationskopf ersetzen
model.fc = nn.Linear(model.fc.in_features, NUM_CLASSES)

# Vollständiges Fine-Tuning: kleine Lernrate für das Backbone und große Lernrate für den Kopf verwenden (siehe Strategie 3)
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4, momentum=0.9)


# Methode B: Die letzten N Schichten auftauen (partielles Fine-Tuning)
model = models.resnet50(weights='IMAGENET1K_V2')

# Zuerst alle Schichten einfrieren
for param in model.parameters():
    param.requires_grad = False

# Nur layer4 und fc auftauen (der letzte Block und der Klassifikationskopf von ResNet)
for param in model.layer4.parameters():
    param.requires_grad = True

model.fc = nn.Linear(model.fc.in_features, NUM_CLASSES)  # fc ist standardmäßig trainierbar

print("Trainierbare Parameter:")
for name, param in model.named_parameters():
    if param.requires_grad:
        print(f" {name}")

3.3 Strategie 3: Schichtweise differenzielle Lernraten

Verwenden Sie eine kleine Lernrate für das Backbone (um vorab trainiertes Wissen zu bewahren) und eine große Lernrate für den Klassifikationskopf (um sich schnell an die neue Aufgabe anzupassen).

Dies ist die in der Industrie am häufigsten verwendete Fine-Tuning-Strategie und bietet die beste Gesamtleistung.

Beispiel

import torch
import torch.nn as nn
import torchvision.models as models

NUM_CLASSES = 8
model = models.resnet50(weights='IMAGENET1K_V2')
model.fc = nn.Linear(model.fc.in_features, NUM_CLASSES)

# Option 1: Zwei Lernraten (Backbone vs. Head)
optimizer = torch.optim.Adam([
    {'params': model.fc.parameters(),  'lr': 1e-3},     # Klassifikationskopf: große Lernrate
    {'params': [p for n, p in model.named_parameters()  # Backbone: kleine Lernrate
                if not n.startswith('fc')],
     'lr': 1e-5},
])


# Option 2: Schichtweise abnehmende Lernrate (am feingranularsten)
# Schichten näher an der Ausgabe haben größere Lernraten
layer_groups = [
    (model.layer1, 1e-5),    # Flachste Schicht, kleinste Lernrate
    (model.layer2, 3e-5),
    (model.layer3, 1e-4),
    (model.layer4, 3e-4),    # Tiefste Backbone-Schicht
    (model.fc,     1e-3),    # Klassifikationskopf, größte Lernrate
]

param_groups = [
    {'params': layer.parameters(), 'lr': lr}
    for layer, lr in layer_groups
]
optimizer = torch.optim.Adam(param_groups)


# Option 3: Allmähliches Auftauen
# In der frühen Trainingsphase nur den Kopf trainieren, dann allmählich weitere Schichten auftauen (von fastai empfohlen)
model = models.resnet50(weights='IMAGENET1K_V2')
for param in model.parameters():
    param.requires_grad = False
model.fc = nn.Linear(model.fc.in_features, NUM_CLASSES)

def unfreeze_layers(model, num_layers):
    """Die letzten num_layers Schichtblöcke von ResNet auftauen"""
    layers = [model.layer4, model.layer3, model.layer2, model.layer1]
    for i in range(min(num_layers, len(layers))):
        for param in layers[i].parameters():
            param.requires_grad = True

# Epoche 1-5: Nur den Klassifikationskopf trainieren
# Epoche 6-10: layer4 auftauen
unfreeze_layers(model, num_layers=1)
# Epoche 11+: weitere Schichten auftauen
unfreeze_layers(model, num_layers=3)

4. Gängige vortrainierte Modelle

4.1 Bildklassifikationsmodelle

Beispiel

import torchvision.models as models

# ResNet-Serie (am klassischsten, für die meisten Aufgaben geeignet)
resnet18  = models.resnet18(weights='IMAGENET1K_V1')   # Leichtgewichtig, für Edge-Geräte geeignet
resnet50  = models.resnet50(weights='IMAGENET1K_V2')   # Ausgewogene Wahl
resnet101 = models.resnet101(weights='IMAGENET1K_V2')  # Stärker, langsamer

# EfficientNet-Serie (extrem hohes Verhältnis von Genauigkeit zu Effizienz)
effnet_b0 = models.efficientnet_b0(weights='IMAGENET1K_V1')  # Am leichtesten
effnet_b4 = models.efficientnet_b4(weights='IMAGENET1K_V1')  # Ausgewogen
effnet_b7 = models.efficientnet_b7(weights='IMAGENET1K_V1')  # Stärkste

# Vision Transformer (erste Wahl für Aufgaben mit großen Datenmengen)
vit_b16 = models.vit_b_16(weights='IMAGENET1K_V1')    # ViT-Base/16
vit_l16 = models.vit_l_16(weights='IMAGENET1K_V1')    # ViT-Large/16

# MobileNet (Mobileinsatz/Embedded-Bereitstellung)
mobilenet_v3 = models.mobilenet_v3_small(weights='IMAGENET1K_V1')

# ConvNeXt (modernisierte CNN, Leistung nahe an ViT)
convnext_t = models.convnext_tiny(weights='IMAGENET1K_V1')
convnext_b = models.convnext_base(weights='IMAGENET1K_V1')

Leistungsvergleich gängiger Modelle (ImageNet Top-1-Genauigkeit):

ModellTop-1-GenauigkeitParametersInferenzgeschwindigkeitAnwendbare Szenarien
ResNet-1869.8%11.7MExtrem schnellRessourcenbeschränkt, schnelles Prototyping
ResNet-5080.9%25.6MSchnellErste Wahl für allgemeine Zwecke
EfficientNet-B483.4%19.3MMittelAusgewogenheit zwischen Genauigkeit und Effizienz
ConvNeXt-Base84.1%88.6MMittelCNN mit hoher Genauigkeit
ViT-B/1681.1%86.6MMittelSzenarien mit großen Datenmengen
ViT-L/1685.1%307MLangsamHöchste Genauigkeit

4.2 Objekterkennungsmodelle

Beispiel

import torchvision.models.detection as detection

# Faster R-CNN (klassischer zweistufiger Detektor)
faster_rcnn = detection.fasterrcnn_resnet50_fpn(weights='DEFAULT')

# SSD (Single-Stage-Detektor, schnell)
ssd = detection.ssd300_vgg16(weights='DEFAULT')

# RetinaNet
retinanet = detection.retinanet_resnet50_fpn(weights='DEFAULT')

# FCOS
fcos = detection.fcos_resnet50_fpn(weights='DEFAULT')

# Ersetze den Klassifizierungs-Head von Faster R-CNN (an neue Anzahl von Klassen anpassen)
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor

NUM_CLASSES = 5 + 1   # 5 Objektklassen + 1 Hintergrundklasse

faster_rcnn = detection.fasterrcnn_resnet50_fpn(weights='DEFAULT')
in_features = faster_rcnn.roi_heads.box_predictor.cls_score.in_features
faster_rcnn.roi_heads.box_predictor = FastRCNNPredictor(in_features, NUM_CLASSES)

4.3 Textmodelle (HuggingFace)

Transfer Learning für NLP-Aufgaben verwendet typischerweise die HuggingFace-Transformers-Bibliothek:

Beispiel

# pip install transformers
from transformers import (
    BertForSequenceClassification,
    RobertaForSequenceClassification,
    AutoModelForSequenceClassification,
    AutoTokenizer,
)

NUM_CLASSES = 3

# BERT (erste Wahl für die Textklassifikation)
model = BertForSequenceClassification.from_pretrained(
    'bert-base-chinese',     # Chinesisches BERT
    num_labels=NUM_CLASSES
)
tokenizer = AutoTokenizer.from_pretrained('bert-base-chinese')

# RoBERTa (verbessertes BERT, leistungsstärker)
model = RobertaForSequenceClassification.from_pretrained(
    'roberta-base',
    num_labels=NUM_CLASSES
)

# Generische Lademethode (erkennt den Modelltyp automatisch)
model = AutoModelForSequenceClassification.from_pretrained(
    'hfl/chinese-roberta-wwm-ext',    # Chinesisches RoBERTa
    num_labels=NUM_CLASSES
)

5. Datenvorverarbeitung und Augmentierung

Wenn Sie beim Transferlernen vortrainierte ImageNet-Gewichte verwenden, müssen Sie dieselben Normalisierungsparameter wie beim Vortraining verwenden; andernfalls stimmen die Merkmalsverteilungen nicht überein und die Leistung sinkt deutlich.

Beispiel

from torchvision import transforms

# ImageNet-Standardnormalisierungsparameter (gemeinsam für alle vortrainierten torchvision-Modelle)
IMAGENET_MEAN = [0.485, 0.456, 0.406]
IMAGENET_STD  = [0.229, 0.224, 0.225]

# Trainingsset: mit Datenaugmentation
train_transforms = transforms.Compose([
    transforms.RandomResizedCrop(224),          # Zufälliges Zuschneiden und Größenänderung
    transforms.RandomHorizontalFlip(p=0.5),    # Zufälliges horizontales Spiegeln
    transforms.ColorJitter(                    # Farb-Jitter
        brightness=0.2, contrast=0.2,
        saturation=0.2, hue=0.1
    ),
    transforms.RandomRotation(degrees=15),     # Zufällige Drehung
    transforms.ToTensor(),
    transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD),
])

# Validierungs-/Testset: keine zufällige Augmentation
val_transforms = transforms.Compose([
    transforms.Resize(256),                    # Zuerst auf 256 skalieren
    transforms.CenterCrop(224),                # Dann mittig auf 224 zuschneiden
    transforms.ToTensor(),
    transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD),
])

# Datensatz laden (Verzeichnisstruktur: root/class_name/img.jpg)
from torchvision.datasets import ImageFolder
from torch.utils.data import DataLoader

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)
val_loader   = DataLoader(val_dataset,   batch_size=32, shuffle=False,
                          num_workers=4, pin_memory=True)

print(f"Anzahl der Trainingsbilder: {len(train_dataset)}")
print(f"Klassenliste: {train_dataset.classes}")
print(f"Klassenzuordnung: {train_dataset.class_to_idx}")

Verwenden der offiziell empfohlenen Vorverarbeitung von torchvision

Beispiel

# PyTorch >= 0.13: Standardvorverarbeitung direkt aus dem weights-Objekt beziehen, keine manuelle Angabe von Parametern erforderlich
from torchvision.models import ResNet50_Weights

weights   = ResNet50_Weights.IMAGENET1K_V2
model     = models.resnet50(weights=weights)
preprocess = weights.transforms()   # Gibt automatisch die entsprechende Vorverarbeitungspipeline zurück

# preprocess enthält bereits Resize(232), CenterCrop(224), Normalize usw.
# Für das Training fügen Sie einfach zusätzlich eine Daten-Augmentierung hinzu

Sie müssen die ImageNet-Normalisierungsparameter [0.485, 0.456, 0.406] und [0.229, 0.224, 0.225] verwenden; andernfalls werden die vortrainierten Features nicht korrekt ausgerichtet.


6. Vollständige praktische Übung: Binäre Bildklassifikation

Am Beispiel der Katze-vs.-Hund-Klassifikation wird die vollständige Transfer-Learning-Pipeline von der Datenvorbereitung über das Training bis zur Evaluierung gezeigt:

Beispiel

import os
import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim.lr_scheduler import CosineAnnealingLR
from torchvision import models, transforms, datasets
from torch.utils.data import DataLoader, random_split
from torchvision.models import EfficientNet_B0_Weights

# Konfiguration
DEVICE      = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
NUM_CLASSES = 2
BATCH_SIZE  = 32
EPOCHS      = 20
BASE_LR     = 1e-3
DATA_DIR    = 'data/cats_and_dogs'

# Datenvorbereitung
train_tfm = transforms.Compose([
    transforms.RandomResizedCrop(224),
    transforms.RandomHorizontalFlip(),
    transforms.ColorJitter(brightness=0.3, contrast=0.3),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406],
                         [0.229, 0.224, 0.225]),
])
val_tfm = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406],
                         [0.229, 0.224, 0.225]),
])

full_dataset = datasets.ImageFolder(DATA_DIR, transform=train_tfm)

n_val   = int(len(full_dataset) * 0.2)
n_train = len(full_dataset) - n_val
train_set, val_set = random_split(full_dataset, [n_train, n_val])
val_set.dataset = datasets.ImageFolder(DATA_DIR, transform=val_tfm)  # Ersetze den Transform von val

train_loader = DataLoader(train_set, BATCH_SIZE, shuffle=True,
                          num_workers=4, pin_memory=True)
val_loader   = DataLoader(val_set,   BATCH_SIZE, shuffle=False,
                          num_workers=4, pin_memory=True)

# Transfermodell erstellen
weights = EfficientNet_B0_Weights.IMAGENET1K_V1
model   = models.efficientnet_b0(weights=weights)

# Backbone einfrieren
for param in model.parameters():
    param.requires_grad = False

# Klassifikationskopf ersetzen (EfficientNet-B0-Kopfstruktur)
in_features = model.classifier[1].in_features  # 1280
model.classifier = nn.Sequential(
    nn.Dropout(p=0.2, inplace=True),
    nn.Linear(in_features, NUM_CLASSES),
)

model = model.to(DEVICE)

# Optimierer: schichtweise Lernrate
optimizer = optim.Adam([
    {'params': model.classifier.parameters(), 'lr': BASE_LR},
    {'params': model.features.parameters(),   'lr': BASE_LR * 0.1},
])
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
scheduler = CosineAnnealingLR(optimizer, T_max=EPOCHS, eta_min=1e-6)

# Trainings- und Validierungsfunktionen
def train_epoch(model, loader, optimizer, criterion):
    model.train()
    total_loss, correct = 0.0, 0
    for imgs, labels in loader:
        imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)
        optimizer.zero_grad()
        outputs = model(imgs)
        loss    = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        total_loss += loss.item() * imgs.size(0)
        correct    += (outputs.argmax(1) == labels).sum().item()
    n = len(loader.dataset)
    return total_loss / n, correct / n

def eval_epoch(model, loader, criterion):
    model.eval()
    total_loss, correct = 0.0, 0
    with torch.no_grad():
        for imgs, labels in loader:
            imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)
            outputs = model(imgs)
            loss    = criterion(outputs, labels)
            total_loss += loss.item() * imgs.size(0)
            correct    += (outputs.argmax(1) == labels).sum().item()
    n = len(loader.dataset)
    return total_loss / n, correct / n

# Stufenweises Training
print("=== Phase 1: Nur den Klassifikationskopf trainieren (Epoche 1-5) ===")
best_acc = 0.0

for epoch in range(1, EPOCHS + 1):
    # Backbone bei Epoche 5 entsperren, Übergang zur vollständigen Feinabstimmung
    if epoch == 6:
        print("\n=== Phase 2: Backbone entsperren und vollständig feinabstimmen (Epoche 6-20) === ")
        for param in model.features.parameters():
            param.requires_grad = True

    train_loss, train_acc = train_epoch(model, train_loader, optimizer, criterion)
    val_loss,   val_acc   = eval_epoch(model,  val_loader,   criterion)
    scheduler.step()

    print(f"Epoche {epoch:2d}/{EPOCHS} | "
          f"Trainingsverlust: {train_loss:.4f}, Genauigkeit: {train_acc:.4f} | "
          f"Validierungsverlust: {val_loss:.4f}, Genauigkeit: {val_acc:.4f} | "
          f"Lernrate: {scheduler.get_last_lr()[0]:.2e}")

    if val_acc > best_acc:
        best_acc = val_acc
        torch.save(model.state_dict(), 'best_model.pth')
        print(f" ✓ Bestes Modell gespeichert Acc={best_acc:.4f}")

print(f"\n"Training abgeschlossen, beste Validierungsgenauigkeit: {best_acc:.4f}")

Inferenz und Vorhersage

Beispiel

from PIL import Image

# Bestes Modell laden
model.load_state_dict(torch.load('best_model.pth', map_location=DEVICE))
model.eval()

# Ein einzelnes Bild vorhersagen
def predict(image_path, model, class_names):
    img = Image.open(image_path).convert('RGB')
    tensor = val_tfm(img).unsqueeze(0).to(DEVICE)  # (1, 3, 224, 224)

    with torch.inference_mode():
        logits = model(tensor)
        probs  = torch.softmax(logits, dim=1)[0]
        pred   = probs.argmax().item()

    for cls, prob in zip(class_names, probs.tolist()):
        print(f" {cls}: {prob:.4f}")
    print(f"Vorhersageergebnis: {class_names[pred]}")
    return class_names[pred]

class_names = train_set.dataset.classes  # ['cat', 'dog']
predict('test_cat.jpg', model, class_names)

7. Vollständige Praxisübung: Textklassifikation (BERT)

Beispiel

# pip install transformers datasets
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
from transformers import (
    BertTokenizer,
    BertForSequenceClassification,
    AdamW,
    get_linear_schedule_with_warmup,
)

DEVICE      = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
MODEL_NAME  = 'bert-base-chinese'
NUM_CLASSES = 3       # Sentimentklassifikation: positiv/neutral/negativ
MAX_LEN     = 128
BATCH_SIZE  = 16
EPOCHS      = 5
LR          = 2e-5    # Empfohlener Lernratenbereich für BERT-Fine-Tuning: 1e-5 ~ 5e-5

# Benutzerdefiniertes Dataset
class SentimentDataset(Dataset):
    def __init__(self, texts, labels, tokenizer, max_len):
        self.texts     = texts
        self.labels    = labels
        self.tokenizer = tokenizer
        self.max_len   = max_len

    def __len__(self):
        return len(self.texts)

    def __getitem__(self, idx):
        encoding = self.tokenizer(
            self.texts[idx],
            max_length=self.max_len,
            padding='max_length',
            truncation=True,
            return_tensors='pt',
        )
        return {
            'input_ids': encoding['input_ids'].squeeze(0),
            'attention_mask': encoding['attention_mask'].squeeze(0),
            'label': torch.tensor(self.labels[idx], dtype=torch.long),
        }

# Vortrainiertes BERT-Modell laden
tokenizer = BertTokenizer.from_pretrained(MODEL_NAME)
model     = BertForSequenceClassification.from_pretrained(
    MODEL_NAME,
    num_labels=NUM_CLASSES,
    hidden_dropout_prob=0.1,
)
model = model.to(DEVICE)

# Untere N Schichten einfrieren (optional)
# BERT-base hat 12 Transformer-Schichten; Sie können die ersten paar Schichten einfrieren, um Rechenaufwand zu sparen
FREEZE_LAYERS = 6   # Die ersten 6 Schichten einfrieren

for i, layer in enumerate(model.bert.encoder.layer):
    if i < FREEZE_LAYERS:
        for param in layer.parameters():
            param.requires_grad = False

print(f"Die ersten {FREEZE_LAYERS} Schichten wurden eingefroren, wodurch Parameter-Updates um etwa {FREEZE_LAYERS/12*100:.0f}% reduziert werden")

# Daten laden
# Beispieldaten (in der Praxis durch einen echten Datensatz ersetzen)
train_texts  = ["Dieser Film ist großartig!", "Der Service ist furchtbar, enttäuschend", "Na ja, nichts Besonderes"]
train_labels = [2, 0, 1]  # 0: negativ 1: neutral 2: positiv

train_dataset = SentimentDataset(train_texts, train_labels, tokenizer, MAX_LEN)
train_loader  = DataLoader(train_dataset, BATCH_SIZE, shuffle=True)

# Optimierer und Scheduler
# BERT-Standard: AdamW + lineares Warmup
no_decay = ['bias', 'LayerNorm.weight']
optimizer_grouped = [
    {'params': [p for n, p in model.named_parameters()
                if not any(nd in n for nd in no_decay)], 'weight_decay': 0.01},
    {'params': [p for n, p in model.named_parameters()
                if     any(nd in n for nd in no_decay)], 'weight_decay': 0.0},
]
optimizer = AdamW(optimizer_grouped, lr=LR)

total_steps   = len(train_loader) * EPOCHS
warmup_steps  = int(total_steps * 0.1)   # 10% der Schritte werden für Warmup verwendet
scheduler = get_linear_schedule_with_warmup(
    optimizer,
    num_warmup_steps=warmup_steps,
    num_training_steps=total_steps,
)

# Trainingsschleife
for epoch in range(1, EPOCHS + 1):
    model.train()
    total_loss, correct = 0.0, 0

    for batch in train_loader:
        input_ids      = batch['input_ids'].to(DEVICE)
        attention_mask = batch['attention_mask'].to(DEVICE)
        labels         = batch['label'].to(DEVICE)

        optimizer.zero_grad()
        outputs = model(input_ids=input_ids,
                        attention_mask=attention_mask,
                        labels=labels)

        loss   = outputs.loss           # BERT berechnet den CE-Verlust bereits intern
        logits = outputs.logits

        loss.backward()
        # BERT-Fine-Tuning-Standard: Gradientenclipping zur Vermeidung von Gradientenexplosion
        nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
        optimizer.step()
        scheduler.step()

        total_loss += loss.item()
        correct += (logits.argmax(1) == labels).sum().item()

    avg_loss = total_loss / len(train_loader)
    acc      = correct / len(train_dataset)
    print(f"Epoche {epoch}/{EPOCHS} | Verlust: {avg_loss:.4f} | Genauigkeit: {acc:.4f}")

# Inferenz
def predict_sentiment(text, model, tokenizer, id2label):
    model.eval()
    encoding = tokenizer(text, max_length=MAX_LEN, padding='max_length',
                         truncation=True, return_tensors='pt')
    with torch.inference_mode():
        outputs = model(
            input_ids      = encoding['input_ids'].to(DEVICE),
            attention_mask = encoding['attention_mask'].to(DEVICE),
        )
    pred = outputs.logits.argmax(1).item()
    return id2label[pred]

id2label = {0: 'Negativ', 1: 'Neutral', 2: 'Positiv'}
print(predict_sentiment("Die Qualität dieses Produkts ist wirklich gut!", model, tokenizer, id2label))

8. Tipps zur Modifikation der Modellarchitektur

Allgemeine Methode zum Ersetzen des Klassifikationskopfs

Beispiel

import torch.nn as nn
import torchvision.models as models

def build_transfer_model(arch, num_classes, pretrained=True, dropout=0.5):
    """
Generische Funktion zum Erstellen eines Transfermodells, die automatisch den Klassifikationskopf erkennt und ersetzt
Unterstützt: resnet, efficientnet, densenet, mobilenet, vit, convnext
    """

    weights = 'IMAGENET1K_V1' if pretrained else None
    model   = getattr(models, arch)(weights=weights)
    name    = arch.lower()

    if 'resnet' in name or 'resnext' in name or 'inception' in name:
        in_f = model.fc.in_features
        model.fc = nn.Sequential(
            nn.Dropout(dropout),
            nn.Linear(in_f, num_classes)
        )

    elif 'efficientnet' in name or 'mobilenet' in name or 'convnext' in name:
        in_f = model.classifier[-1].in_features
        model.classifier[-1] = nn.Sequential(
            nn.Dropout(dropout),
            nn.Linear(in_f, num_classes)
        )

    elif 'densenet' in name:
        in_f = model.classifier.in_features
        model.classifier = nn.Linear(in_f, num_classes)

    elif 'vit' in name or 'swin' in name:
        in_f = model.heads.head.in_features
        model.heads.head = nn.Linear(in_f, num_classes)

    else:
        raise ValueError(f"Nicht unterstützte Architektur: {arch}")

    return model


# Verwendungsbeispiel
model_resnet  = build_transfer_model('resnet50',        num_classes=10)
model_effnet  = build_transfer_model('efficientnet_b3', num_classes=10)
model_vit     = build_transfer_model('vit_b_16',        num_classes=10)

Hinzufügen von Zwischen-Feature-Extraktionsschichten

Beispiel

import torch
import torch.nn as nn
import torchvision.models as models

class TransferWithAttention(nn.Module):
    """Ein benutzerdefiniertes Attention-Modul und einen Klassifikationskopf nach dem vortrainierten Backbone hinzufügen"""
    def __init__(self, num_classes, dropout=0.5):
        super().__init__()
        backbone = models.resnet50(weights='IMAGENET1K_V2')

        # Ursprünglichen Klassifikationskopf entfernen, Feature-Extractor beibehalten
        self.backbone  = nn.Sequential(*list(backbone.children())[:-1])
        self.feat_dim  = 2048   # ResNet50-Merkmalsdimension

        # Benutzerdefiniertes Attention-Gating
        self.attention = nn.Sequential(
            nn.Linear(self.feat_dim, 512),
            nn.Tanh(),
            nn.Linear(512, 1),
            nn.Sigmoid()
        )

        # Klassifizierungskopf
        self.classifier = nn.Sequential(
            nn.Dropout(dropout),
            nn.Linear(self.feat_dim, 256),
            nn.ReLU(),
            nn.Linear(256, num_classes),
        )

    def forward(self, x):
        feat   = self.backbone(x).flatten(1)          # (N, 2048)
        weight = self.attention(feat)                  # (N, 1)
        feat   = feat * weight                         # Gewichtete Features
        return self.classifier(feat)

9. Best Practices für Transfer Learning

Auswahl der Lernrate

Beispiel

# Lernrate für Backbone (vortrainierte Schichten)
# Normalerweise auf 1/10 ~ 1/100 der Head-Lernrate gesetzt
backbone_lr = 1e-5   # Konservative Strategie, empfohlen bei knappen Daten
head_lr     = 1e-3   # Neue Schichten starten mit zufälliger Initialisierung und benötigen eine größere Lernrate

# Lernrate für Transformer wie BERT / ViT
# Diese großen Modelle sind extrem empfindlich gegenüber der Lernrate; ein Überschreiten des Bereichs kann vortrainiertes Wissen beschädigen
bert_lr = 2e-5        # Empfohlener Bereich: 1e-5 ~ 5e-5

Häufige Probleme und Lösungen

Beispiel

# Problem 1: Unzureichender GPU-Speicher
# Lösung 1: Batchgröße reduzieren
# Lösung 2: Gradient-Checkpointing verwenden (Zeit gegen Speicher tauschen)
from torch.utils.checkpoint import checkpoint_sequential
model.features = lambda x: checkpoint_sequential(model.features, 4, x)

# Lösung 3: Mehr Schichten einfrieren (Backpropagation-Berechnung reduzieren)

# Problem 2: Hohe Trainingsgenauigkeit, niedrige Validierungsgenauigkeit (Überanpassung)
# Lösung 1: Daten-Augmentierung verstärken
# Lösung 2: Dropout erhöhen
model.classifier = nn.Sequential(nn.Dropout(0.5), nn.Linear(in_f, num_classes))
# Lösung 3: Label Smoothing verwenden
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
# Lösung 4: Mehr Backbone-Schichten einfrieren, um trainierbare Parameter zu reduzieren

# Problem 3: Instabiles Training, Loss-Oszillation
# Lösung 1: Lernrate reduzieren (versuchen Sie, sie zu halbieren)
# Lösung 2: Warmup hinzufügen
# Lösung 3: Gradient Clipping
nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# Lösung 4: Optimierer ändern (Adam → AdamW)

# Problem 4: Normalisierungsparameter stimmen nicht überein
# Fehler: Verwendung einer benutzerdefinierten Normalisierung passt nicht zum vortrainierten Modell
transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
# Korrekt: Mittelwert und Standardabweichung von ImageNet müssen verwendet werden
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])

Kurzreferenz zur Gesamtstrategie

ProblemEmpfohlener Ansatz
Datengröße < 500Alle Ebenen einfrieren, nur den Klassifikationskopf trainieren
Datengröße 500~2000Die ersten 2/3 einfrieren, die letzten 1/3 feinabstimmen
Datengröße 2000~10000Vollständiges Fine-Tuning, Backbone bei 1e-5, Kopf bei 1e-3
Datengröße > 10000Vollständiges Fine-Tuning, erwägen Sie eine höhere Lernrate oder Training von Grund auf
Langsame TrainingsgeschwindigkeitZuerst einfrieren und für einige Epochen trainieren, dann auftauen; AMP-Mixed-Precision verwenden
LeistungsengpassWechseln Sie zu einem größeren/neueren Backbone; versuchen Sie ConvNeXt / ViT
Modell muss in Produktion bereitgestellt werdenBevorzugen Sie leichte Modelle wie EfficientNet-B0 / MobileNetV3
Chinesische NLP-AufgabenBERT-base-chinese oder chinese-roberta-wwm-ext

Empfohlene vollständige Trainingsstrategie

Phase 1 (Epoche 1~N/4):

  • Backbone einfrieren, nur den Klassifikationskopf trainieren
  • Verwenden Sie eine größere Lernrate (1e-3), damit die Head-Parameter schnell konvergieren

Phase 2 (Epoche N/4~N):

  • Backbone entsperren, vollständiges Fine-Tuning
  • Backbone verwendet eine kleine Lernrate (1e-5), Head behält (1e-3) bei
  • Kombinieren Sie dies mit Cosine Annealing oder ReduceLROnPlateau

Speicherstrategie:

  • Nur den Checkpoint mit den besten Validierungsmetriken speichern
  • Zusätzlich Modell-/Optimierer-/Scheduler-Zustände speichern
Andere Erweiterungen