PyTorch: трансферное обучение
Трансферное обучение — это метод, который заключается в том, чтобы взять модель, предварительно обученную на крупномасштабном наборе данных, и перенести её на новую задачу с меньшим объёмом данных для обучения.
Это одна из наиболее широко используемых техник в современной практике глубокого обучения — в большинстве случаев трансферное обучение показывает лучшие результаты, работает быстрее и требует меньше данных, чем обучение с нуля.
1. Основная идея трансферного обучения
Признаки, изученные глубокими нейронными сетями на ImageNet, универсальны:
- Начальные слои: изучают общие низкоуровневые признаки (края, текстуры, цветовые градиенты)
- Средние слои: изучают общие признаки среднего уровня (формы, части, комбинации текстур)
- Глубокие слои: изучают высокоуровневые признаки, специфичные для задачи (лица, колёса, текст)
Эти низко- и среднеуровневые признаки эффективны для подавляющего большинства задач компьютерного зрения и не нуждаются в повторном обучении.
Когда использовать перенос обучения?
| Объём данных | Сходство с исходной задачей | Рекомендуемая стратегия |
|---|---|---|
| Малый (< 1000) | Высокая | Заменить только финальную голову классификации, заморозить всю базовую сеть. |
| Малый (< 1000) | Низкая | Дообучить более поверхностные слои, заморозить более глубокие слои. |
| Средний (1000~10000) | Высокая | Дообучить всю сеть с небольшой скоростью обучения. |
| Средний (1000~10000) | Низкая | Дообучить более глубокие слои, заморозить более поверхностные слои. |
| Большой (> 10000) | Any | Тонкая настройка всего или рассмотрите обучение с нуля. |
Сравнение трёх основных стратегий
Структура слоёв предобученной модели (например, ResNet50) выглядит следующим образом:
- Свёрточный слой 1~3 (низкоуровневые признаки: края/текстуры): обычно заморожен
- Свёрточный слой 4~6 (среднеуровневые признаки: формы/части): опционально заморожен
- Свёрточный слой 7~N (высокоуровневые признаки: семантическая информация): дообучается
- Голова классификатора: заменяется и обучается
┌─────────────────────────────────────────────────┐ │ 预训练模型(如 ResNet50) │ │ ┌──────────────────────────────────────────┐ │ │ │ Conv Layer 1~3(低级特征:边缘/纹理) │ ← 通常冻结 │ ├──────────────────────────────────────────┤ │ │ │ Conv Layer 4~6(中级特征:形状/部件) │ ← 可选冻结 │ ├──────────────────────────────────────────┤ │ │ │ Conv Layer 7~N(高级特征:语义信息) │ ← 微调 │ ├──────────────────────────────────────────┤ │ │ │ Classifier Head(分类头) │ ← 替换 & 训练 │ └──────────────────────────────────────────┘ │ └─────────────────────────────────────────────────┘
2. Загрузка предобученных моделей
PyTorch предоставляет множество официальных предобученных моделей через torchvision.models, и загружать их очень просто.
Пример
import torchvision.models as models
# Загрузить предобученную модель (автоматически скачивает веса)
# PyTorch >= 0.13 рекомендует новый синтаксис: используйте параметр weights
from torchvision.models import ResNet50_Weights
model = models.resnet50(weights=ResNet50_Weights.IMAGENET1K_V2)
# Старый синтаксис (всё ещё работает, но вызовет предупреждение об устаревании)
model = models.resnet50(pretrained=True)
# Не загружать предварительно обученные веса (использовать только архитектуру сети)
model = models.resnet50(weights=None)
Просмотр структуры модели
Пример
print(model)
# Просмотреть только последние несколько слоёв (классификационная головка)
print(model.fc)
# Linear(in_features=2048, out_features=1000, bias=True)
# Подсчет количества параметров
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_params:,}")
print(f"Обучаемые параметры: {trainable_params:,}")
Названия классификационных голов для различных моделей
У разных моделей разные имена атрибутов для их классификационных голов; при трансферном обучении нужно заменить соответствующий слой:
| Модель | Атрибут классификационной головы |
|---|---|
| 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. Три стратегии трансферного обучения
3.1 Стратегия 1: Извлечение признаков (Заморозить все)
Заморозить все параметры предобученной модели и обучать только новую замененную классификационную голову.
Подходит для сценариев с очень малым количеством данных (несколько сотен изображений) или когда задача очень похожа на исходную задачу.
Пример
import torch.nn as nn
import torchvision.models as models
from torchvision.models import ResNet18_Weights
NUM_CLASSES = 5 # Количество классов для целевой задачи
# Шаг 1: Загрузить предобученную модель
model = models.resnet18(weights=ResNet18_Weights.IMAGENET1K_V1)
# Шаг 2: Заморозить все параметры
for param in model.parameters():
param.requires_grad = False
# Шаг 3: Заменить классификационную голову (у этих параметров requires_grad=True по умолчанию)
in_features = model.fc.in_features # 512
model.fc = nn.Linear(in_features, NUM_CLASSES)
# Проверка: только классификационная голова обучаема
trainable = [(n, p.shape) for n, p in model.named_parameters() if p.requires_grad]
print(f"Количество обучаемых слоёв: {len(trainable)}")
for name, shape in trainable:
print(f" {name}: {shape}")
# Вывод:
# fc.weight: torch.Size([5, 512])
# fc.bias: torch.Size([5])
# Шаг 4: Передавайте оптимизатору только обучаемые параметры (более эффективно)
optimizer = torch.optim.Adam(
filter(lambda p: p.requires_grad, model.parameters()),
lr=1e-3
)
# Или эквивалентный, более чистый подход:
optimizer = torch.optim.Adam(model.fc.parameters(), lr=1e-3)
Извлечение признаков — это самая простая и наиболее часто используемая стратегия переноса обучения, особенно подходящая для сценариев с небольшим объёмом данных.
3.2 Стратегия 2: Тонкая настройка
Разморозьте все или некоторые предобученные слои и обучите всю модель с меньшей скоростью обучения.
Подходит для сценариев со средним объёмом данных или когда задача отличается от исходной задачи.
Пример
import torch.nn as nn
import torchvision.models as models
NUM_CLASSES = 10
model = models.resnet50(weights='IMAGENET1K_V2')
# Метод A: Полная тонкая настройка (разморозить все слои)
# Сначала заморозить
for param in model.parameters():
param.requires_grad = False
# Затем разморозить (эквивалентно полной тонкой настройке; этот стиль обычно используется в сценариях постепенной разморозки)
for param in model.parameters():
param.requires_grad = True
# Заменить классификационную голову
model.fc = nn.Linear(model.fc.in_features, NUM_CLASSES)
# Полное дообучение: используйте небольшую скорость обучения для основной части и большую скорость обучения для головы (см. стратегию 3)
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4, momentum=0.9)
# Метод B: разморозить последние N слоёв (частичное дообучение)
model = models.resnet50(weights='IMAGENET1K_V2')
# Сначала заморозить все слои
for param in model.parameters():
param.requires_grad = False
# Разморозить только layer4 и fc (последний блок и классификационная голова ResNet)
for param in model.layer4.parameters():
param.requires_grad = True
model.fc = nn.Linear(model.fc.in_features, NUM_CLASSES) # fc по умолчанию обучаемый
print("Обучаемые параметры:")
for name, param in model.named_parameters():
if param.requires_grad:
print(f" {name}")
3.3 Стратегия 3: Послойные дифференцированные скорости обучения
Используйте маленькую скорость обучения для базовой сети (чтобы сохранить предварительно обученные знания) и большую скорость обучения для классификационной головы (чтобы быстро адаптироваться к новой задаче).
Это наиболее часто используемая стратегия тонкой настройки в индустрии, обеспечивающая лучшую общую производительность.
Пример
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)
# Вариант 1: Две скорости обучения (базовая сеть против головы)
optimizer = torch.optim.Adam([
{'params': model.fc.parameters(), 'lr': 1e-3}, # Классификационная голова: большая скорость обучения
{'params': [p for n, p in model.named_parameters() # Базовая сеть: маленькая скорость обучения
if not n.startswith('fc')],
'lr': 1e-5},
])
# Вариант 2: Послойно затухающая скорость обучения (наиболее детализированный)
# Слои, более близкие к выходу, имеют большие скорости обучения
layer_groups = [
(model.layer1, 1e-5), # Самый поверхностный слой, наименьшая скорость обучения
(model.layer2, 3e-5),
(model.layer3, 1e-4),
(model.layer4, 3e-4), # Самый глубокий слой магистральной сети
(model.fc, 1e-3), # Классификационная головка, наибольшая скорость обучения
]
param_groups = [
{'params': layer.parameters(), 'lr': lr}
for layer, lr in layer_groups
]
optimizer = torch.optim.Adam(param_groups)
# Вариант 3: постепенное размораживание
# На раннем этапе обучения обучайте только головку, затем постепенно размораживайте больше слоёв (рекомендуется fastai)
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):
"""Разморозить последние num_layers блоков слоёв ResNet"""
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
# Эпоха 1-5: обучаем только классификационную голову
# Эпоха 6-10: размораживаем layer4
unfreeze_layers(model, num_layers=1)
# Эпоха 11+: размораживаем больше слоёв
unfreeze_layers(model, num_layers=3)
4. Распространённые предобученные модели
4.1 Модели классификации изображений
Пример
# Серия ResNet (самая классическая, подходит для большинства задач)
resnet18 = models.resnet18(weights='IMAGENET1K_V1') # Лёгкая, подходит для периферийных устройств
resnet50 = models.resnet50(weights='IMAGENET1K_V2') # Сбалансированный выбор
resnet101 = models.resnet101(weights='IMAGENET1K_V2') # Мощнее, но медленнее
# Серия EfficientNet (чрезвычайно высокое соотношение точности и эффективности)
effnet_b0 = models.efficientnet_b0(weights='IMAGENET1K_V1') # Самый лёгкий
effnet_b4 = models.efficientnet_b4(weights='IMAGENET1K_V1') # Сбалансированный
effnet_b7 = models.efficientnet_b7(weights='IMAGENET1K_V1') # Самый мощный
# Vision Transformer (лучший выбор для задач с крупномасштабными данными)
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 (развёртывание на мобильных/встраиваемых устройствах)
mobilenet_v3 = models.mobilenet_v3_small(weights='IMAGENET1K_V1')
# ConvNeXt (модернизированная CNN, производительность близка к ViT)
convnext_t = models.convnext_tiny(weights='IMAGENET1K_V1')
convnext_b = models.convnext_base(weights='IMAGENET1K_V1')
Сравнение производительности популярных моделей (точность ImageNet Top-1):
| Модель | Точность Top-1 | Parameters | Скорость инференса | Применимые сценарии |
|---|---|---|---|---|
| ResNet-18 | 69.8% | 11.7M | Чрезвычайно быстрый | Ограниченные ресурсы, быстрое прототипирование |
| ResNet-50 | 80.9% | 25.6M | Высокая | Первый выбор для общего назначения |
| EfficientNet-B4 | 83.4% | 19.3M | Средняя | Баланс точности и эффективности |
| ConvNeXt-Base | 84.1% | 88.6M | Средняя | Высокоточная CNN |
| ViT-B/16 | 81.1% | 86.6M | Средняя | Сценарии с большими объёмами данных |
| ViT-L/16 | 85.1% | 307M | Медленная | Наивысшая точность |
4.2 Модели обнаружения объектов
Пример
# Faster R-CNN (классический двухстадийный детектор)
faster_rcnn = detection.fasterrcnn_resnet50_fpn(weights='DEFAULT')
# SSD (одностадийный детектор, быстрый)
ssd = detection.ssd300_vgg16(weights='DEFAULT')
# RetinaNet
retinanet = detection.retinanet_resnet50_fpn(weights='DEFAULT')
# FCOS
fcos = detection.fcos_resnet50_fpn(weights='DEFAULT')
# Заменить классификационную голову Faster R-CNN (адаптировать под новое количество классов)
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor
NUM_CLASSES = 5 + 1 # 5 классов объектов + 1 фоновый класс
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 Текстовые модели (HuggingFace)
Перенос обучения для задач NLP обычно использует библиотеку transformers от HuggingFace:
Пример
from transformers import (
BertForSequenceClassification,
RobertaForSequenceClassification,
AutoModelForSequenceClassification,
AutoTokenizer,
)
NUM_CLASSES = 3
# BERT (лучший выбор для классификации текста)
model = BertForSequenceClassification.from_pretrained(
'bert-base-chinese', # Китайский BERT
num_labels=NUM_CLASSES
)
tokenizer = AutoTokenizer.from_pretrained('bert-base-chinese')
# RoBERTa (улучшенный BERT, мощнее)
model = RobertaForSequenceClassification.from_pretrained(
'roberta-base',
num_labels=NUM_CLASSES
)
# Универсальный метод загрузки (автоматически определяет тип модели)
model = AutoModelForSequenceClassification.from_pretrained(
'hfl/chinese-roberta-wwm-ext', # Китайский RoBERTa
num_labels=NUM_CLASSES
)
5. Предобработка и аугментация данных
При использовании предварительно обученных весов ImageNet в обучении с переносом необходимо использовать те же параметры нормализации, что и при предварительном обучении; иначе распределения признаков не совпадут, и производительность значительно упадёт.
Пример
# Стандартные параметры нормализации ImageNet (общие для всех предварительно обученных моделей torchvision)
IMAGENET_MEAN = [0.485, 0.456, 0.406]
IMAGENET_STD = [0.229, 0.224, 0.225]
# Обучающий набор: с аугментацией данных
train_transforms = transforms.Compose([
transforms.RandomResizedCrop(224), # Случайное обрезание и изменение размера
transforms.RandomHorizontalFlip(p=0.5), # Случайное горизонтальное отражение
transforms.ColorJitter( # Случайное изменение цвета
brightness=0.2, contrast=0.2,
saturation=0.2, hue=0.1
),
transforms.RandomRotation(degrees=15), # Случайный поворот
transforms.ToTensor(),
transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD),
])
# Валидационный/тестовый набор: без случайной аугментации
val_transforms = transforms.Compose([
transforms.Resize(256), # Сначала изменение размера до 256
transforms.CenterCrop(224), # Затем центральный кроп до 224
transforms.ToTensor(),
transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD),
])
# Загрузка набора данных (структура каталогов: 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"Количество обучающих примеров: {len(train_dataset)}")
print(f"Список классов: {train_dataset.classes}")
print(f"Сопоставление классов: {train_dataset.class_to_idx}")
Использование официально рекомендуемой предобработки torchvision
Пример
from torchvision.models import ResNet50_Weights
weights = ResNet50_Weights.IMAGENET1K_V2
model = models.resnet50(weights=weights)
preprocess = weights.transforms() # Автоматически возвращает соответствующий конвейер предобработки
# preprocess уже включает Resize(232), CenterCrop(224), Normalize и т. д.
# Для обучения просто добавьте аугментацию данных поверх этого
Необходимо использовать параметры нормализации ImageNet [0.485, 0.456, 0.406] и [0.229, 0.224, 0.225]; иначе предобученные признаки не будут корректно согласованы.
6. Полный практикум: бинарная классификация изображений
На примере классификации кошек и собак здесь показан полный конвейер трансферного обучения — от подготовки данных до обучения и оценки:
Пример
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
# Конфигурация
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'
# Подготовка данных
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) # Заменить преобразование 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)
# Построение модели переноса обучения
weights = EfficientNet_B0_Weights.IMAGENET1K_V1
model = models.efficientnet_b0(weights=weights)
# Заморозить основную сеть
for param in model.parameters():
param.requires_grad = False
# Заменить голову классификации (структура головы EfficientNet-B0)
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)
# Оптимизатор: послойная скорость обучения
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)
# Функции обучения и валидации
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
# Поэтапное обучение
print("=== Этап 1: Обучаем только классификационную голову (Эпоха 1-5) ===")
best_acc = 0.0
for epoch in range(1, EPOCHS + 1):
# Разморозить backbone на эпохе 5, переход к этапу полной тонкой настройки
if epoch == 6:
print("\n=== Этап 2: Разморозить backbone и полностью дообучить (Эпоха 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"Эпоха {epoch:2d}/{EPOCHS} | "
f"Потери на обучении: {train_loss:.4f}, Точность: {train_acc:.4f} | "
f"Потери на валидации: {val_loss:.4f}, Точность: {val_acc:.4f} | "
f"Скорость обучения: {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" ✓ Сохранена лучшая модель Acc={best_acc:.4f}")
print(f"\n"Обучение завершено, лучшая точность валидации: {best_acc:.4f}")
Инференс и предсказание
Пример
# Загрузить лучшую модель
model.load_state_dict(torch.load('best_model.pth', map_location=DEVICE))
model.eval()
# Предсказать одно изображение
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"Результат предсказания: {class_names[pred]}")
return class_names[pred]
class_names = train_set.dataset.classes # ['cat', 'dog']
predict('test_cat.jpg', model, class_names)
7. Полный практикум: классификация текста (BERT)
Пример
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 # Классификация тональности: положительная/нейтральная/отрицательная
MAX_LEN = 128
BATCH_SIZE = 16
EPOCHS = 5
LR = 2e-5 # Рекомендуемый диапазон скорости обучения для тонкой настройки BERT: 1e-5 ~ 5e-5
# Пользовательский набор данных
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),
}
# Загрузить предобученную модель BERT
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)
# Заморозить нижние N слоёв (необязательно)
# BERT-base содержит 12 слоёв Transformer; можно заморозить первые несколько слоёв, чтобы сэкономить вычисления
FREEZE_LAYERS = 6 # Заморозить первые 6 слоёв
for i, layer in enumerate(model.bert.encoder.layer):
if i < FREEZE_LAYERS:
for param in layer.parameters():
param.requires_grad = False
print(f"Заморожены первые {FREEZE_LAYERS} слоёв; это сокращает обновления параметров примерно на {FREEZE_LAYERS/12*100:.0f}%")
# Загрузка данных
# Пример данных (на практике замените на реальный набор данных)
train_texts = ["Этот фильм потрясающий!", "Обслуживание ужасное, разочаровывающее", "Так себе, ничего особенного"]
train_labels = [2, 0, 1] # 0: негативный 1: нейтральный 2: позитивный
train_dataset = SentimentDataset(train_texts, train_labels, tokenizer, MAX_LEN)
train_loader = DataLoader(train_dataset, BATCH_SIZE, shuffle=True)
# Оптимизатор и планировщик
# Стандарт BERT: AdamW + линейный разогрев
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% шагов, используемых для разогрева
scheduler = get_linear_schedule_with_warmup(
optimizer,
num_warmup_steps=warmup_steps,
num_training_steps=total_steps,
)
# Цикл обучения
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 уже вычисляет CE loss внутри
logits = outputs.logits
loss.backward()
# Стандарт тонкой настройки BERT: отсечение градиентов для предотвращения взрыва градиентов
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"Эпоха {epoch}/{EPOCHS} | Потери: {avg_loss:.4f} | Точность: {acc:.4f}")
# Инференс
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: 'Негативный', 1: 'Нейтральный', 2: 'Позитивный'}
print(predict_sentiment("Качество этого товара действительно хорошее!", model, tokenizer, id2label))
8. Советы по модификации архитектуры модели
Общий метод замены классификационной головы
Пример
import torchvision.models as models
def build_transfer_model(arch, num_classes, pretrained=True, dropout=0.5):
"""
Универсальная функция построения модели переноса, которая автоматически определяет и заменяет классификационную голову
Поддерживаются: 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"Неподдерживаемая архитектура: {arch}")
return model
# Пример использования
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)
Добавление промежуточных слоёв извлечения признаков
Пример
import torch.nn as nn
import torchvision.models as models
class TransferWithAttention(nn.Module):
"""Добавить пользовательский модуль внимания и голову классификации после предобученного бэкбона"""
def __init__(self, num_classes, dropout=0.5):
super().__init__()
backbone = models.resnet50(weights='IMAGENET1K_V2')
# Удалить исходную голову классификации, оставить экстрактор признаков
self.backbone = nn.Sequential(*list(backbone.children())[:-1])
self.feat_dim = 2048 # Размерность признаков ResNet50
# Настраиваемое гейтирование внимания
self.attention = nn.Sequential(
nn.Linear(self.feat_dim, 512),
nn.Tanh(),
nn.Linear(512, 1),
nn.Sigmoid()
)
# Головка классификации
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 # Взвешенные признаки
return self.classifier(feat)
9. Лучшие практики трансферного обучения
Выбор скорости обучения
Пример
# Обычно устанавливается в диапазоне 1/10 ~ 1/100 от скорости обучения head
backbone_lr = 1e-5 # Консервативная стратегия, рекомендуется при недостатке данных
head_lr = 1e-3 # Новые слои инициализируются случайно и требуют более высокой скорости обучения
# Скорость обучения для трансформеров, таких как BERT / ViT
# Эти большие модели чрезвычайно чувствительны к скорости обучения; выход за пределы диапазона может повредить предобученные знания
bert_lr = 2e-5 # Рекомендуемый диапазон: 1e-5 ~ 5e-5
Распространённые проблемы и решения
Пример
# Решение 1: Уменьшить размер батча
# Решение 2: Использовать градиентный чекпоинтинг (обмен времени на память)
from torch.utils.checkpoint import checkpoint_sequential
model.features = lambda x: checkpoint_sequential(model.features, 4, x)
# Решение 3: Заморозить больше слоёв (уменьшить вычисления при обратном распространении)
# Проблема 2: Высокая точность на обучении, низкая точность на валидации (переобучение)
# Решение 1: Усилить аугментацию данных
# Решение 2: Увеличить Dropout
model.classifier = nn.Sequential(nn.Dropout(0.5), nn.Linear(in_f, num_classes))
# Решение 3: Использовать Label Smoothing
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
# Решение 4: Заморозить больше слоёв backbone, чтобы уменьшить количество обучаемых параметров
# Проблема 3: Нестабильное обучение, колебания функции потерь
# Решение 1: Уменьшить скорость обучения (попробуйте уменьшить её вдвое)
# Решение 2: Добавить warmup
# Решение 3: Отсечение градиентов
nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# Решение 4: Сменить оптимизатор (Adam → AdamW)
# Проблема 4: Параметры нормализации не совпадают
# Ошибка: Использование пользовательской нормализации не соответствует предобученной модели
transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
# Правильно: Нужно использовать среднее и стандартное отклонение ImageNet
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
Краткая справка по общей стратегии
| Проблема | Рекомендуемый подход |
|---|---|
| Размер данных < 500 | Заморозить все слои, обучать только классификационную голову |
| Размер данных 500~2000 | Заморозить первые 2/3, дообучить последнюю 1/3 |
| Размер данных 2000~10000 | Полное дообучение, backbone на 1e-5, голова на 1e-3 |
| Размер данных > 10000 | Полное дообучение: рассмотрите большую скорость обучения или обучение с нуля |
| Медленная скорость тренировки | Сначала заморозьте и обучайте несколько эпох, затем разморозьте; используйте смешанную точность AMP |
| Узкое место производительности | Переключитесь на более крупный/более новый backbone; попробуйте ConvNeXt / ViT |
| Модель необходимо развернуть в продакшене | Отдайте приоритет легковесным моделям, таким как EfficientNet-B0 / MobileNetV3 |
| Задачи NLP на китайском языке | BERT-base-chinese или chinese-roberta-wwm-ext |
Рекомендуемая полная стратегия обучения
Этап 1 (Эпоха 1~N/4):
- Заморозьте backbone, обучайте только классификационную голову
- Используйте более высокую скорость обучения (1e-3), чтобы параметры головы быстро сошлись
Этап 2 (Эпоха N/4~N):
- Разморозьте бэкбон, полное дообучение
- Бэкбон использует низкую скорость обучения (1e-5), у головы остаётся (1e-3)
- Совместите с косинусным отжигом или ReduceLROnPlateau
Стратегия сохранения:
- Сохраняйте только чекпоинт с лучшими метриками валидации
- Также сохраняйте состояния модели / оптимизатора / планировщика