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 Quellaufgabe | Empfohlene Strategie |
|---|---|---|
| Klein (< 1000) | Hoch | Nur den finalen Klassifikationskopf ersetzen, das gesamte Backbone einfrieren. |
| Klein (< 1000) | Niedrig | Flachere Schichten feinabstimmen, tiefere Schichten einfrieren. |
| Mittel (1000~10000) | Hoch | Das gesamte Netzwerk mit einer kleinen Lernrate feinabstimmen. |
| Mittel (1000~10000) | Niedrig | Tiefere Schichten feinabstimmen, flachere Schichten einfrieren. |
| Groß (> 10000) | Any | Alles 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 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
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:
| Modell | Attribut des Klassifikationskopfs |
|---|---|
| ResNet / RegNet | model.fc |
| VGG / AlexNet | model.classifier[-1] |
| DenseNet | model.classifier |
| EfficientNet | model.classifier[-1] |
| MobileNetV2/V3 | model.classifier[-1] |
| ViT (Vision Transformer) | model.heads.head |
| ConvNeXt | model.classifier[-1] |
| Inception V3 | model.fc |
| Swin Transformer | model.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.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.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.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
# 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):
| Modell | Top-1-Genauigkeit | Parameters | Inferenzgeschwindigkeit | Anwendbare Szenarien |
|---|---|---|---|---|
| ResNet-18 | 69.8% | 11.7M | Extrem schnell | Ressourcenbeschränkt, schnelles Prototyping |
| ResNet-50 | 80.9% | 25.6M | Schnell | Erste Wahl für allgemeine Zwecke |
| EfficientNet-B4 | 83.4% | 19.3M | Mittel | Ausgewogenheit zwischen Genauigkeit und Effizienz |
| ConvNeXt-Base | 84.1% | 88.6M | Mittel | CNN mit hoher Genauigkeit |
| ViT-B/16 | 81.1% | 86.6M | Mittel | Szenarien mit großen Datenmengen |
| ViT-L/16 | 85.1% | 307M | Langsam | Höchste Genauigkeit |
4.2 Objekterkennungsmodelle
Beispiel
# 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
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
# 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
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 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
# 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
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 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.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
# 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
# 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
| Problem | Empfohlener Ansatz |
|---|---|
| Datengröße < 500 | Alle Ebenen einfrieren, nur den Klassifikationskopf trainieren |
| Datengröße 500~2000 | Die ersten 2/3 einfrieren, die letzten 1/3 feinabstimmen |
| Datengröße 2000~10000 | Vollständiges Fine-Tuning, Backbone bei 1e-5, Kopf bei 1e-3 |
| Datengröße > 10000 | Vollständiges Fine-Tuning, erwägen Sie eine höhere Lernrate oder Training von Grund auf |
| Langsame Trainingsgeschwindigkeit | Zuerst einfrieren und für einige Epochen trainieren, dann auftauen; AMP-Mixed-Precision verwenden |
| Leistungsengpass | Wechseln Sie zu einem größeren/neueren Backbone; versuchen Sie ConvNeXt / ViT |
| Modell muss in Produktion bereitgestellt werden | Bevorzugen Sie leichte Modelle wie EfficientNet-B0 / MobileNetV3 |
| Chinesische NLP-Aufgaben | BERT-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