Фаза 04 · урок 05
Перенос обучения и тонкая настройка
Цель урока: Обучение ResNet-50 на ImageNet требует примерно 2 000 GPU-часов. Очень немногие команды располагают таким бюджетом для каждой выпускаемой задачи. На практике почти все команды поставляют предобученный backbone с новой головой, обученной…
Текущий релиз AlexBred.com: первые 100 уроков русскоязычной программы.
Содержание урока
- Цели обучения
- Проблема
- Концепция
- Извлечение признаков и тонкая настройка
- Почему замораживание вообще работает
- Дискриминационные скорости обучения
- Проблема BatchNorm
- Проектирование головы
- Послойное затухание LR
- Что оценивать
- Соберите сами
- Шаг 1: Загрузите предобученный backbone и изучите его
- Шаг 2: Извлечение признаков — заморозьте всё и замените голову
- Шаг 3: Дискриминационная тонкая настройка
- Шаг 4: Работа с BatchNorm
- Шаг 5: Минимальный сквозной цикл тонкой настройки
- Шаг 6: Постепенное размораживание
- Используйте
- Подготовьте к поставке
- Упражнения
- Ключевые термины
- Дополнительные материалы
Кто-то уже потратил миллион GPU-часов, чтобы научить сеть распознавать края, текстуры и части объектов. Воспользуйтесь этими признаками, прежде чем обучать собственную модель.
Тип: Сборка Языки: Python Предварительные требования: Фаза 04, урок 03 (CNN), фаза 04, урок 04 (Классификация изображений) Время: ~75 минут
Цели обучения
- Различать извлечение признаков и тонкую настройку (fine-tuning) и выбирать подходящий режим по размеру набора данных, расстоянию между доменами и вычислительному бюджету
- Загрузить предобученный backbone, заменить его классификационную голову и обучить только голову до рабочего базового уровня менее чем за 20 строк
- Постепенно размораживать слои, используя дискриминационные скорости обучения, чтобы ранние универсальные признаки получали меньшие обновления, чем поздние признаки, специфичные для задачи
- Диагностировать три типичные проблемы: дрейф признаков из-за слишком высокой LR на размороженных блоках, разрушение статистик BN на малых наборах данных и катастрофическое забывание
Проблема
Обучение ResNet-50 на ImageNet требует примерно 2 000 GPU-часов. Очень немногие команды располагают таким бюджетом для каждой выпускаемой задачи. На практике почти все команды поставляют предобученный backbone с новой головой, обученной на нескольких сотнях или тысячах изображений, специфичных для задачи.
Это не срезание углов. Первый свёрточный блок любой CNN, обученной на ImageNet, изучает края и фильтры, подобные Габоровским. Следующие несколько блоков изучают текстуры и простые мотивы. Средние блоки изучают части объектов. Финальные блоки изучают их сочетания, начинающие напоминать 1 000 категорий ImageNet. Первые 90 % этой иерархии почти без изменений переносятся на медицинскую визуализацию, промышленную инспекцию, спутниковые данные и любую другую задачу компьютерного зрения — потому что у природы ограниченный словарь краёв и текстур. Последние 10 % вы и обучаете.
При правильном переносе обучения вас ждут три ошибки: уничтожить предобученные признаки слишком большой скоростью обучения, лишить модель информации чрезмерным замораживанием и позволить скользящим статистикам BatchNorm сместиться к небольшому набору данных, на котором остальная сеть никогда не училась. В этом уроке вы намеренно пройдёте каждую из них.
Концепция
Извлечение признаков и тонкая настройка
Есть два режима; выбор определяется тем, насколько вы доверяете предобученным признакам и каким объёмом данных располагаете.
Практические правила:
| Размер набора данных | Расстояние между доменами | Рецепт |
|---|---|---|
| < 1k изображений | близок к ImageNet | Заморозьте backbone, обучайте только голову |
| 1k–10k | близок | Заморозьте первые 2–3 стадии, тонко настройте остальные |
| 10k–100k | любой | Тонко настройте всю модель с дискриминационной LR |
| 100k+ | далёк | Тонко настройте всё; при достаточно далёком домене рассмотрите обучение с нуля |
«Близкий к ImageNet» приблизительно означает естественные RGB-фотографии с содержимым, похожим на объекты. Медицинские КТ-снимки, спутниковые изображения сверху и микроскопия — далёкие домены: признаки всё ещё помогают, но вам придётся дать адаптироваться большему числу слоёв.
Почему замораживание вообще работает
Признаки ImageNet, которые изучает CNN, не специализированы для 1 000 категорий. Они специализированы для статистики естественных изображений: краёв определённых ориентаций, текстур, паттернов контраста, примитивов формы. Эта статистика стабильна почти во всех визуальных доменах, которые может назвать человек. Поэтому модель, обученная на ImageNet и оценённая zero-shot на CIFAR-10 лишь с новой линейной головой (без тонкой настройки backbone), достигает точности более 80 %. Голова учится тому, какие из уже изученных признаков взвешивать для этой задачи.
Дискриминационные скорости обучения
Если вы размораживаете модель, ранние слои должны обучаться медленнее поздних. Ранние слои кодируют универсальные признаки, которые важно сохранить; поздние слои кодируют структуру, специфичную для задачи, и их нужно заметно изменить.
Typical recipe:
stage 0 (stem + first group): lr = base_lr / 100 (mostly fixed)
stage 1: lr = base_lr / 10
stage 2: lr = base_lr / 3
stage 3 (last backbone group): lr = base_lr
head: lr = base_lr (or slightly higher)
В PyTorch это всего лишь список групп параметров, переданный оптимизатору. Одна модель, пять скоростей обучения, ни одной дополнительной строки кода.
Проблема BatchNorm
Слои BN хранят буферы running_mean и running_var, вычисленные на ImageNet. Если в вашей задаче иное распределение пикселей — другое освещение, датчик или цветовое пространство, — эти буферы ошибочны. Есть три варианта в порядке предпочтительности:
- Тонкая настройка с BN в режиме train. Дайте BN обновлять скользящие статистики вместе со всеми остальными параметрами. Это вариант по умолчанию для наборов данных среднего размера (>= 5k примеров).
- Заморозка BN в режиме eval. Сохраните статистики ImageNet и обучайте только веса. Это правильно, когда набор данных настолько мал, что скользящее среднее BN будет шумным.
- Замена BN на GroupNorm. Полностью устраняет проблему скользящего среднего. Этот подход используется в backbone-сетях для детекции и сегментации, где размер батча на GPU мал.
Ошибка в этом выборе незаметно снижает точность на 5–15 %.
Проектирование головы
Классификационная голова — это 1–3 линейных слоя и необязательный dropout. Каждый backbone из torchvision поставляется с головой по умолчанию, которую вы заменяете:
backbone.fc = nn.Linear(backbone.fc.in_features, num_classes) # ResNet
backbone.classifier[1] = nn.Linear(..., num_classes) # EfficientNet, MobileNet
backbone.heads.head = nn.Linear(..., num_classes) # torchvision ViT
Для небольших наборов данных обычно достаточно одного линейного слоя. Добавление скрытого слоя (Linear -> ReLU -> Dropout -> Linear) помогает, когда распределение задачи сильнее отличается от распределения, на котором обучался backbone.
Послойное затухание LR
Это более плавная версия дискриминационной LR, используемая в современной тонкой настройке (BEiT, DINOv2, тонкая настройка ViT-B). Вместо группировки слоёв по стадиям каждому слою назначается LR, немного меньшая, чем у следующего слоя.
lr_layer_k = base_lr * decay^(L - k)
При decay = 0.75 и L = 12 блоках трансформера первый блок обучается с 0.75^11 ≈ 0.04x от LR головы. Это важнее для тонкой настройки трансформеров, чем CNN, для которых обычно достаточно LR, сгруппированных по стадиям.
Что оценивать
В экспериментах с переносом обучения нужны два показателя, которые вы не отслеживали бы при обучении с нуля:
- Точность только предобученной модели — точность головы при замороженном backbone. Это ваш нижний предел.
- Точность после тонкой настройки — точность той же модели после сквозного обучения. Это ваш верхний предел.
Если точность после тонкой настройки ниже, чем у предобученной модели, у вас ошибка в скорости обучения или BN. Всегда выводите оба значения.
Соберите сами
Шаг 1: Загрузите предобученный backbone и изучите его
import torch
import torch.nn as nn
from torchvision.models import resnet18, ResNet18_Weights
backbone = resnet18(weights=ResNet18_Weights.IMAGENET1K_V1)
print(backbone)
print()
print("classifier head:", backbone.fc)
print("feature dim:", backbone.fc.in_features)
У ResNet18 четыре стадии (layer1..layer4), а также stem и голова fc. У каждого backbone для классификации в torchvision есть аналогичная структура.
Шаг 2: Извлечение признаков — заморозьте всё и замените голову
def make_feature_extractor(num_classes=10):
model = resnet18(weights=ResNet18_Weights.IMAGENET1K_V1)
for p in model.parameters():
p.requires_grad = False
model.fc = nn.Linear(model.fc.in_features, num_classes)
return model
model = make_feature_extractor(num_classes=10)
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
frozen = sum(p.numel() for p in model.parameters() if not p.requires_grad)
print(f"trainable: {trainable:>10,}")
print(f"frozen: {frozen:>10,}")
Обучаемой остаётся только model.fc. Backbone — замороженный экстрактор признаков.
Шаг 3: Дискриминационная тонкая настройка
Вспомогательная функция, создающая группы параметров со скоростями обучения, специфичными для стадий.
def discriminative_param_groups(model, base_lr=1e-3, decay=0.3):
stages = [
["conv1", "bn1"],
["layer1"],
["layer2"],
["layer3"],
["layer4"],
["fc"],
]
groups = []
for i, names in enumerate(stages):
lr = base_lr * (decay ** (len(stages) - 1 - i))
params = [p for n, p in model.named_parameters()
if any(n.startswith(k) for k in names)]
if params:
groups.append({"params": params, "lr": lr, "name": "_".join(names)})
return groups
model = resnet18(weights=ResNet18_Weights.IMAGENET1K_V1)
model.fc = nn.Linear(model.fc.in_features, 10)
for p in model.parameters():
p.requires_grad = True
groups = discriminative_param_groups(model)
for g in groups:
print(f"{g['name']:>10s} lr={g['lr']:.2e} params={sum(p.numel() for p in g['params']):>8,}")
decay=0.3 означает, что каждая стадия обучается с 30 % скорости следующей. fc получает base_lr, layer4 получает 0.3 * base_lr, а conv1 получает 0.3^5 * base_lr ≈ 0.00243 * base_lr. Это звучит экстремально, но эмпирически работает.
Шаг 4: Работа с BatchNorm
Вспомогательная функция для замораживания скользящих статистик BN без замораживания её весов.
def freeze_bn_stats(model):
for m in model.modules():
if isinstance(m, (nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d)):
m.eval()
for p in m.parameters():
p.requires_grad = False
return model
Вызывайте её после model.train() в начале каждой эпохи. model.train() переключает всё в режим обучения; эта функция отменяет переключение только для слоёв BN.
Шаг 5: Минимальный сквозной цикл тонкой настройки
from torch.optim import SGD
from torch.utils.data import DataLoader
from torch.optim.lr_scheduler import CosineAnnealingLR
import torch.nn.functional as F
def fine_tune(model, train_loader, val_loader, device, epochs=5, base_lr=1e-3, freeze_bn=False):
model = model.to(device)
groups = discriminative_param_groups(model, base_lr=base_lr)
optimizer = SGD(groups, momentum=0.9, weight_decay=1e-4, nesterov=True)
scheduler = CosineAnnealingLR(optimizer, T_max=epochs)
for epoch in range(epochs):
model.train()
if freeze_bn:
freeze_bn_stats(model)
tr_loss, tr_correct, tr_total = 0.0, 0, 0
for x, y in train_loader:
x, y = x.to(device), y.to(device)
logits = model(x)
loss = F.cross_entropy(logits, y, label_smoothing=0.1)
optimizer.zero_grad()
loss.backward()
optimizer.step()
tr_loss += loss.item() * x.size(0)
tr_total += x.size(0)
tr_correct += (logits.argmax(-1) == y).sum().item()
scheduler.step()
model.eval()
va_total, va_correct = 0, 0
with torch.no_grad():
for x, y in val_loader:
x, y = x.to(device), y.to(device)
pred = model(x).argmax(-1)
va_total += x.size(0)
va_correct += (pred == y).sum().item()
print(f"epoch {epoch} train {tr_loss/tr_total:.3f}/{tr_correct/tr_total:.3f} "
f"val {va_correct/va_total:.3f}")
return model
Пять эпох по этому рецепту на CIFAR-10 поднимают ResNet18-IMAGENET1K_V1 примерно с 70 % точности zero-shot линейного зонда до 93 % после тонкой настройки. Одна голова остановилась бы примерно на 86 %, ни разу не изменив backbone.
Шаг 6: Постепенное размораживание
Расписание, размораживающее по одной стадии за эпоху — от конца к началу. Оно смягчает дрейф признаков ценой нескольких дополнительных эпох.
def progressive_unfreeze_schedule(model):
stages = ["layer4", "layer3", "layer2", "layer1"]
yielded = set()
def start():
for p in model.parameters():
p.requires_grad = False
for p in model.fc.parameters():
p.requires_grad = True
def unfreeze(epoch):
if epoch < len(stages):
name = stages[epoch]
yielded.add(name)
for n, p in model.named_parameters():
if n.startswith(name):
p.requires_grad = True
return name
return None
return start, unfreeze
Вызовите start() один раз перед первой эпохой. Вызывайте unfreeze(epoch) в начале каждой эпохи. Пересобирайте оптимизатор всякий раз, когда меняется набор обучаемых параметров, иначе замороженные параметры сохранят кэшированные моменты, которые его запутают.
Используйте
Для большинства реальных задач достаточно torchvision.models и трёх строк. Более тяжёлые механизмы выше нужны, когда вы сталкиваетесь с проблемами, которые настройки библиотек по умолчанию не исправляют.
from torchvision.models import resnet50, ResNet50_Weights
model = resnet50(weights=ResNet50_Weights.IMAGENET1K_V2)
model.fc = nn.Linear(model.fc.in_features, num_classes)
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4)
Ещё два подходящих для продакшена варианта по умолчанию:
timmпоставляет около 800 предобученных backbone-сетей компьютерного зрения с единообразным API (timm.create_model("resnet50", pretrained=True, num_classes=10)). Для любой тонкой настройки за пределами зоопарка torchvision это стандартный выбор.- Для трансформеров
transformers.AutoModelForImageClassification.from_pretrained(name, num_labels=N)предоставляет ViT / BEiT / DeiT с той же семантикой загрузки, что и у текстовых моделей.
Подготовьте к поставке
Этот урок создаёт:
outputs/prompt-fine-tune-planner.md— промпт, выбирающий извлечение признаков, постепенную или сквозную тонкую настройку по размеру набора данных, расстоянию между доменами и вычислительному бюджету.outputs/skill-freeze-inspector.md— навык, который по модели PyTorch сообщает, какие параметры обучаемы, какие слои BatchNorm находятся в режиме eval и действительно ли оптимизатор получает обучаемые параметры.
Упражнения
- (Легко) Обучите
ResNet18как линейный зонд (backbone заморожен) и как модель с полной тонкой настройкой на одном и том же синтетическом наборе CIFAR. Выведите обе точности рядом. Объясните, какой разрыв говорит о хорошем переносе признаков, а какой — о плохом. - (Средне) Намеренно внесите ошибку: задайте для стадии backbone
base_lr = 1e-1вместо головы. Покажите, как взрывается обучающая потеря, затем восстановите обучение с помощьюdiscriminative_param_groups. Запишите LR, при которой каждая стадия начинает расходиться. - (Сложно) Возьмите набор данных медицинских изображений (например, CheXpert-small, PatchCamelyon или HAM10000) и сравните три режима: (a) предобученный на ImageNet замороженный backbone + линейная голова; (b) предобученная на ImageNet сквозная тонкая настройка; (c) обучение с нуля. Сообщите точность и вычислительную стоимость каждого. При каком размере набора данных обучение с нуля становится конкурентоспособным?
Ключевые термины
| Термин | Что обычно говорят | Что это действительно означает |
|---|---|---|
| Извлечение признаков | «Заморозить и обучить голову» | Параметры backbone заморожены, градиент получает только новая классификационная голова |
| Тонкая настройка | «Переобучить сквозным образом» | Все параметры обучаемы, обычно с намного меньшей LR, чем при обучении с нуля |
| Дискриминационная LR | «Меньшая LR для ранних слоёв» | Группы параметров оптимизатора, в которых LR ранних стадий составляет долю от LR поздних стадий |
| Послойное затухание LR | «Плавный градиент LR» | LR каждого слоя умножается на decay^(L - k); обычно используется при тонкой настройке трансформеров |
| Катастрофическое забывание | «Модель потеряла ImageNet» | Слишком высокая LR перезаписывает предобученные признаки до того, как будет усвоен сигнал новой задачи |
| Дрейф статистик BN | «Скользящее среднее неверно» | running_mean/var BatchNorm вычислены на распределении, отличном от текущей задачи, и незаметно ухудшают точность |
| Линейный зонд | «Замороженный backbone + линейная голова» | Оценка предобученных признаков — точность лучшего линейного классификатора поверх замороженного представления |
| Катастрофический коллапс | «Всё предсказывает один класс» | Возникает, когда LR при тонкой настройке достаточно высока, чтобы уничтожить признаки до стабилизации градиентов от головы |
Дополнительные материалы
- How transferable are features in deep neural networks? (Yosinski et al., 2014) — статья, количественно оценившая переносимость признаков между слоями
- Universal Language Model Fine-tuning (ULMFiT, Howard & Ruder, 2018) — исходный рецепт дискриминационных LR и постепенного размораживания; идеи напрямую переносятся на компьютерное зрение
- Документация timm — справочник по современным backbone-сетям компьютерного зрения и точным настройкам тонкой настройки, на которых они обучались
- A Simple Framework for Linear-Probe Evaluation (Kornblith et al., 2019) — почему точность линейного зонда важна и как её правильно сообщать
Источник: Transfer Learning & Fine-Tuning 04.04 — Классификация изображений · Фаза 04 — Компьютерное зрение · 04.06 — Обнаружение объектов: YOLO с нуля · Полный каталог