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 torch
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 / 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. Три стратегии трансферного обучения

3.1 Стратегия 1: Извлечение признаков (Заморозить все)

Заморозить все параметры предобученной модели и обучать только новую замененную классификационную голову.

Подходит для сценариев с очень малым количеством данных (несколько сотен изображений) или когда задача очень похожа на исходную задачу.

Пример

import torch
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
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
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 Модели классификации изображений

Пример

import torchvision.models as models

# Серия 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-1ParametersСкорость инференсаПрименимые сценарии
ResNet-1869.8%11.7MЧрезвычайно быстрыйОграниченные ресурсы, быстрое прототипирование
ResNet-5080.9%25.6MВысокаяПервый выбор для общего назначения
EfficientNet-B483.4%19.3MСредняяБаланс точности и эффективности
ConvNeXt-Base84.1%88.6MСредняяВысокоточная CNN
ViT-B/1681.1%86.6MСредняяСценарии с большими объёмами данных
ViT-L/1685.1%307MМедленнаяНаивысшая точность

4.2 Модели обнаружения объектов

Пример

import torchvision.models.detection as detection

# 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:

Пример

# pip install transformers
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 в обучении с переносом необходимо использовать те же параметры нормализации, что и при предварительном обучении; иначе распределения признаков не совпадут, и производительность значительно упадёт.

Пример

from torchvision import transforms

# Стандартные параметры нормализации 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

Пример

# PyTorch >= 0.13: получить стандартную предобработку напрямую из объекта весов, не нужно вручную задавать параметры
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 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

# Конфигурация
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}")

Инференс и предсказание

Пример

from PIL import Image

# Загрузить лучшую модель
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)

Пример

# 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       # Классификация тональности: положительная/нейтральная/отрицательная
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 torch.nn as nn
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
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. Лучшие практики трансферного обучения

Выбор скорости обучения

Пример

# Скорость обучения для backbone (предобученных слоёв)
# Обычно устанавливается в диапазоне 1/10 ~ 1/100 от скорости обучения head
backbone_lr = 1e-5   # Консервативная стратегия, рекомендуется при недостатке данных
head_lr     = 1e-3   # Новые слои инициализируются случайно и требуют более высокой скорости обучения

# Скорость обучения для трансформеров, таких как BERT / ViT
# Эти большие модели чрезвычайно чувствительны к скорости обучения; выход за пределы диапазона может повредить предобученные знания
bert_lr = 2e-5        # Рекомендуемый диапазон: 1e-5 ~ 5e-5

Распространённые проблемы и решения

Пример

# Проблема 1: Недостаточно памяти GPU
# Решение 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

Стратегия сохранения:

  • Сохраняйте только чекпоинт с лучшими метриками валидации
  • Также сохраняйте состояния модели / оптимизатора / планировщика
Другие расширения