Пакетная нормализация и Dropout в PyTorch
Пакетная нормализация и Dropout — две из наиболее важных техник регуляризации и стабилизации при обучении глубоких нейронных сетей. Первая решает проблему внутреннего сдвига ковариат во время обучения, делая более глубокие сети обучаемыми; вторая предотвращает переобучение путём случайного отключения нейронов, улучшая обобщающую способность модели. Обычно их применяют вместе, и они являются стандартными компонентами современных нейронных сетей.
1. Пакетная нормализация
1.1 Основные принципы
Во время обучения глубоких сетей небольшие изменения параметров предыдущего слоя непрерывно усиливаются по мере углубления слоёв, из-за чего входное распределение последующих слоёв постоянно меняется — это явление называетсявнутренним сдвигом ковариат. Пакетная нормализация решает эту проблему путём стандартизации выхода каждого слоя, принудительно возвращая значения активаций к стабильному распределению.
Формула нормализации:
\[\hat{x}_i = \frac{x_i - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}}\]
\[y_i = \gamma \hat{x}_i + \beta\]
где:
- \(\mu_B\), \(\sigma_B^2\) — это среднее и дисперсия текущего батча
- \(\epsilon\) — небольшая константа для предотвращения деления на ноль (по умолчанию
1e-5) - \(\gamma\) и \(\beta\) — обучаемые параметры масштабирования и сдвига, которые позволяют сети самостоятельно определять форму итогового распределения
Преимущества пакетной нормализации:
- Позволяет использовать более высокие скорости обучения, ускоряя обучение
- Снижает чувствительность к инициализации, делая обучение более стабильным
- Оказывает определённый регуляризующий эффект, снижая зависимость от Dropout
- Смягчает проблему исчезающих/взрывающихся градиентов, делая более глубокие сети обучаемыми
1.2 BatchNorm1d / 2d / 3d
PyTorch предоставляет три версии в зависимости от размерности входных данных, с одинаковым использованием, отличающиеся только формой обрабатываемых данных.
BatchNorm1d: для полносвязных слоёв / данных последовательностей
Пример
import torch.nn as nn
# Форма входа: (N, C) или (N, C, L)
# N=batch_size, C=количество признаков/каналов, L=длина последовательности
bn1d = nn.BatchNorm1d(
num_features=128, # количество признаков/каналов
eps=1e-5, # небольшая константа для предотвращения деления на ноль (по умолчанию 1e-5)
momentum=0.1, # momentum для скользящего среднего (по умолчанию 0.1)
affine=True, # обучать ли gamma и beta (по умолчанию True)
track_running_stats=True, # отслеживать ли скользящее среднее/дисперсию (по умолчанию True)
)
# используется после полносвязных слоёв
x = torch.randn(32, 128) # (batch=32, features=128)
out = bn1d(x)
print(out.shape) # torch.Size([32, 128])
# данные последовательности (например, после 1D-свёртки)
x_seq = torch.randn(32, 128, 50) # (batch, channels, seq_len)
out_seq = bn1d(x_seq)
print(out_seq.shape) # torch.Size([32, 128, 50])
BatchNorm2d: для карт признаков изображений CNN
Пример
# нормализовать независимо для каждого канала C
bn2d = nn.BatchNorm2d(num_features=64) # 64 канала
x = torch.randn(32, 64, 28, 28) # (batch, channels, height, width)
out = bn2d(x)
print(out.shape) # torch.Size([32, 64, 28, 28])
BatchNorm3d: для 3D-свёртки (видео / медицинская визуализация)
Пример
bn3d = nn.BatchNorm3d(num_features=32)
x = torch.randn(4, 32, 16, 32, 32) # (batch, channels, depth, h, w)
out = bn3d(x)
print(out.shape) # torch.Size([4, 32, 16, 32, 32])
Просмотр обучаемых параметров
Пример
print(f"форма weight (gamma): {bn.weight.shape}") # torch.Size([64])
print(f"форма bias (beta): {bn.bias.shape}") # torch.Size([64])
print(f"форма running_mean: {bn.running_mean.shape}") # torch.Size([64])
print(f"форма running_var: {bn.running_var.shape}") # torch.Size([64])
# running_mean / running_var не являются обучаемыми параметрами
# Это статистики скользящего среднего, используемые при инференсе
print(f"количество обучаемых параметров: {sum(p.numel() for p in bn.parameters())}") # 128(64×2)
Значение параметра momentum
Пример
# running_mean = (1 - momentum) × running_mean + momentum × batch_mean
# т.е. чем больше momentum, тем больше вес статистики текущего батча
# по умолчанию 0.1: обычно подходит
# при маленьком размере батча рассмотрите уменьшение: momentum=0.01, чтобы избежать нестабильной статистики
bn = nn.BatchNorm2d(64, momentum=0.01)
# momentum=None: использует кумулятивное скользящее среднее (CMA), подходит для очень маленьких батчей
bn = nn.BatchNorm2d(64, momentum=None)
1.3 LayerNorm Слойная нормализация
ПараНормализует все признаки одного образцаНе зависит от батча, подходит для сценариев с маленьким или переменным размером батча.Стандартный метод нормализации для трансформеров。
BatchNorm:在 batch 维度计算均值/方差(跨样本,同通道) LayerNorm:在特征维度计算均值/方差(同样本,跨特征)
Пример
import torch.nn as nn
# ── Для полносвязных / Transformer ────────────────────────
# normalized_shape: нормализовать по последним нескольким измерениям
ln = nn.LayerNorm(normalized_shape=512)
x = torch.randn(32, 10, 512) # (batch, seq_len, embed_dim)
out = ln(x)
print(out.shape) # torch.Size([32, 10, 512])
# ── Нормализация по нескольким измерениям ───────────────────────────────
ln_2d = nn.LayerNorm([64, 28, 28]) # нормализовать по трём измерениям C, H, W
x = torch.randn(8, 64, 28, 28)
out = ln_2d(x)
# ── Типичное использование в Transformer ──────────────────────
class TransformerBlock(nn.Module):
def __init__(self, embed_dim, num_heads, ff_dim):
super().__init__()
self.attn = nn.MultiheadAttention(embed_dim, num_heads, batch_first=True)
self.ff = nn.Sequential(
nn.Linear(embed_dim, ff_dim),
nn.GELU(),
nn.Linear(ff_dim, embed_dim),
)
self.norm1 = nn.LayerNorm(embed_dim) # ← нормализовать после каждого подслоя
self.norm2 = nn.LayerNorm(embed_dim)
def forward(self, x):
attn_out, _ = self.attn(x, x, x)
x = self.norm1(x + attn_out) # остаточное соединение + нормализация
x = self.norm2(x + self.ff(x))
return x
1.4 GroupNorm Групповая нормализация
Разделите каналы на несколько группgroup, и нормализуйте внутри каждой группы. Это не зависит от размера батча и превосходит BatchNorm в сценариях с малым батчем (например, обнаружение объектов, сегментация медицинских изображений).
Пример
# Особый случай: num_groups=1 эквивалентно LayerNorm
# num_groups=num_channels эквивалентно InstanceNorm
gn = nn.GroupNorm(
num_groups=32, # количество групп (обычно: 8, 16, 32)
num_channels=256, # количество каналов
eps=1e-5,
affine=True,
)
x = torch.randn(4, 256, 56, 56) # Небольшой батч (4 изображения), 256 каналов
out = gn(x)
print(out.shape) # torch.Size([4, 256, 56, 56])
# GroupNorm полностью не затрагивается при batch=1 (например, инференс на одном изображении)
x_single = torch.randn(1, 256, 56, 56)
out_single = gn(x_single) # Работает нормально
1.5 InstanceNorm Нормализация по экземпляру
ПараКаждый канал каждого образцаНезависимо нормализуется, не затрагивая размерность батча. Обычно используется впереносе стиля изображений— сохраняет собственные стилевые характеристики каждого изображения, предотвращая влияние статистик других изображений в батче.
Пример
in2d = nn.InstanceNorm2d(
num_features=64,
affine=False, # По умолчанию False (не обучает gamma/beta)
)
x = torch.randn(8, 64, 128, 128)
out = in2d(x)
print(out.shape) # torch.Size([8, 64, 128, 128])
1.6 Сравнение методов нормализации
| Метод | Размерность нормализации | Зависимость от батча | Применимые сценарии |
|---|---|---|---|
BatchNorm |
По батчу (тот же канал) | Сильная зависимость | Классификация изображений CNN (большой батч) |
LayerNorm |
По признакам (один и тот же образец) | Нет зависимости | Transformer、NLP、RNN |
GroupNorm |
Каналы внутри группы (один и тот же образец) | Нет зависимости | Обнаружение объектов при малом батче, сегментация |
InstanceNorm |
Пространственная размерность (один и тот же образец и один и тот же канал) | Нет зависимости | Перенос стиля изображения, генеративные модели |
Краткий справочник по рекомендациям выбора
# Transformer / BERT / GPT → LayerNorm
# batch_size < 8, обнаружение/сегментация объектов → GroupNorm(num_groups=32)
# Перенос стиля изображения / CycleGAN → InstanceNorm2d
2. Dropout
2.1 Основные принципы
Dropout случайным образом устанавливает выходы некоторых нейронов в 0 во время обучения (с вероятностьюp), заставляя сеть не полагаться ни на один отдельный нейрон, тем самым изучая более устойчивые распределённые представления признаков и эффективно предотвращая переобучение.
Во время обучения:
\[y_i = \begin{cases} 0 & \text{с вероятностью } p \\ \dfrac{x_i}{1-p} & \text{с вероятностью } 1-p \end{cases}\]
Сохранённые нейроны масштабируются на \(\frac{1}{1-p}\), чтобы ожидаемое значение осталось неизменным (то есть Inverted Dropout).
Во время инференса: Dropout автоматически отключается, все нейроны нормально участвуют в вычислениях, что эквивалентно усреднению ансамбля нескольких подсетей.
训练时示意(p=0.5): 输入: [1.0, 2.0, 3.0, 4.0, 5.0] 掩码: [ 1, 0, 1, 0, 1 ] ← 随机生成 输出: [2.0, 0.0, 6.0, 0.0, 10.0] ← 保留的值乘以 1/(1-0.5)=2 推理时: 输出: [1.0, 2.0, 3.0, 4.0, 5.0] ← 原样输出
2.2 Dropout / Dropout2d / Dropout3d
Dropout: используется для полносвязных слоёв
Пример
import torch.nn as nn
dropout = nn.Dropout(p=0.5) # p: вероятность обнуления
x = torch.ones(2, 10)
print("Режим обучения:")
dropout.train()
print(dropout(x)) # Примерно 50% значений равны 0, сохранённые значения равны 2.0
print("\n"Режим оценки:")
dropout.eval()
print(dropout(x)) # Все 1.0, Dropout отключён
Пример вывода:
训练模式:
tensor([[2., 0., 2., 0., 2., 0., 0., 2., 2., 0.],
[0., 2., 0., 2., 0., 2., 2., 0., 0., 2.]])
评估模式:
tensor([[1., 1., 1., 1., 1., 1., 1., 1., 1., 1.],
[1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]])
Dropout2d: для карт признаков CNN (отключение всего канала)
Dropout2dСлучайно отключает целые каналы, т. е. все пространственные позиции канала обнуляются вместе. Это больше подходит для свёрточных признаков, чем поэлементный Dropout, потому что активации соседних пикселей сильно коррелированы, из-за чего поэлементное отключение менее эффективно.
Пример
# Вход: (N, C, H, W)
x = torch.ones(4, 16, 8, 8) # 4 изображения, 16 каналов
out = dropout2d(x)
print(out.shape) # torch.Size([4, 16, 8, 8])
# Проверка: полное отключение канала
# Статус отключения каналов для 0-го изображения (не ноль = канал сохранён, все нули = канал отключён)
kept_channels = (out[0].sum(dim=(1, 2)) != 0).sum().item()
print(f"Изображение 0 сохранило {kept_channels}/16 каналов")
Dropout3d: используется для 3D-свёртки
Пример
dropout3d = nn.Dropout3d(p=0.2)
x = torch.ones(2, 8, 16, 16, 16)
out = dropout3d(x)
print(out.shape) # torch.Size([2, 8, 16, 16, 16])
Рекомендации по выбору значения p
Пример
nn.Dropout(p=0.5) # Классическое значение, стандартное для скрытых слоёв
# Полносвязные слои (узкие скрытые слои) или небольшие сети: более низкий Dropout
nn.Dropout(p=0.2) # Избежать недообучения
# Dropout2d для свёрточных слоёв: обычно ниже
nn.Dropout2d(p=0.1) # Свёрточные слои уже обладают некоторым регуляризующим эффектом
# Dropout перед классификационной головой (например, EfficientNet / ViT)
nn.Dropout(p=0.3) # Обычно между 0.2 и 0.5
# Dropout в attention / FFN трансформера
nn.Dropout(p=0.1) # Значение по умолчанию в BERT, в трансформерах обычно используются меньшие значения
2.3 AlphaDropout
Специально разработан дляфункции активации SELUвариант Dropout автоматически корректирует среднее и дисперсию после отбрасывания нейронов, сохраняя свойство самонормализации.
Пример
class SelfNormalizingNet(nn.Module):
def __init__(self, in_dim, hidden_dim, num_classes):
super().__init__()
self.net = nn.Sequential(
nn.Linear(in_dim, hidden_dim),
nn.SELU(),
nn.AlphaDropout(p=0.1), # ← Используется с SELU
nn.Linear(hidden_dim, hidden_dim),
nn.SELU(),
nn.AlphaDropout(p=0.1),
nn.Linear(hidden_dim, num_classes),
)
def forward(self, x):
return self.net(x)
Примечание:AlphaDropout имеет смысл только при использовании функции активации SELU; с другими функциями активации используйте обычный Dropout напрямую.
3. Режим обучения и режим оценки
Поведение BatchNorm и Dropout во время обучения и оценкиполностью различается, и их необходимо правильно переключать; это одна из самых распространённых ошибок у новичков.
Сравнение различий в поведении
| Компонент | model.train() |
model.eval() |
|---|---|---|
| BatchNorm | Нормализует, используя среднее/дисперсию текущего батча;Обновляет running_mean/var | Нормализует, используя running_mean/var;Без обновления |
| Dropout | Случайно обнуляет (с вероятностью p) | Отключено, все нейроны выводят как обычно |
Правильное использование
Пример
import torch.nn as nn
model = nn.Sequential(
nn.Linear(128, 256),
nn.BatchNorm1d(256),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(256, 10),
)
# ── Фаза обучения ──────────────────────────────────────
model.train() # ← Переключение в режим обучения (состояние по умолчанию)
for inputs, labels in train_loader:
optimizer.zero_grad()
outputs = model(inputs) # BN использует статистику батча, Dropout случайно отключает
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# ── Фаза оценки/валидации ─────────────────────────────────
model.eval() # ← Переключение в режим оценки (очень важно!)
with torch.no_grad(): # Также отключить вычисление градиентов
for inputs, labels in val_loader:
outputs = model(inputs) # BN использует скользящую статистику, Dropout отключён
# ...
# ── Инференс на одном изображении ──────────────────────────────────
model.eval()
with torch.inference_mode(): # Быстрее, чем no_grad, только для инференса
output = model(single_input.unsqueeze(0))
pred = output.argmax(1).item()
Убедитесь, что переключение режима корректно
Пример
x = torch.randn(8, 128)
model.train()
out_train_1 = model(x)
out_train_2 = model(x)
print("Режим обучения: один и тот же вывод дважды?", torch.allclose(out_train_1, out_train_2))
# False ← Маска Dropout отличается каждый раз, выходные данные различаются (нормально)
model.eval()
out_eval_1 = model(x)
out_eval_2 = model(x)
print("Режим оценки: один и тот же вывод дважды?", torch.allclose(out_eval_1, out_eval_2))
# True ← Dropout отключён, BN использует фиксированную статистику, выход детерминирован (нормально)
4. Размещение в сети
Размещение BatchNorm
Существует два подхода к размещению BatchNorm, и оба используются на практике:
Подход 1: перед функцией активации (оригинальная статья)
nn.Sequential(
nn.Conv2d(32, 64, 3, padding=1),
nn.BatchNorm2d(64), # BN перед активацией
nn.ReLU(inplace=True),
)
Подход 2: после функции активации (некоторые исследования указывают на лучшую производительность)
nn.Sequential(
nn.Conv2d(32, 64, 3, padding=1),
nn.ReLU(inplace=True),
nn.BatchNorm2d(64), # BN после активации
)
Практический совет:Придерживайтесь схемы из статьи, на которую вы ссылаетесь; если нет особых требований, Conv → BN → ReLU более распространён.
Размещение Dropout
Полносвязные сети: после функции активации
nn.Linear(512, 256),
nn.ReLU(),
nn.Dropout(0.5), # После ReLU
nn.Linear(256, 128),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(128, 10), # Обычно перед выходным слоем не используют Dropout
)
CNN: после BN (если используется BN, Dropout можно опустить)
# Dropout обычно добавляют только перед полносвязными слоями или классификационной головой
nn.Sequential(
nn.Conv2d(64, 128, 3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
# Обычно здесь нет Dropout (BN уже обеспечивает регуляризацию)
)
Transformer: внутри FFN и слоёв внимания
def __init__(self, d_model, d_ff, dropout=0.1):
super().__init__()
self.net = nn.Sequential(
nn.Linear(d_model, d_ff),
nn.GELU(),
nn.Dropout(dropout), # Добавьте Dropout внутри FFN
nn.Linear(d_ff, d_model),
nn.Dropout(dropout), # Добавьте ещё раз перед выходом
)
def forward(self, x):
return self.net(x)
Следует ли использовать BN и Dropout вместе?
Полносвязные сети: можно использовать вместе
nn.Linear(512, 256),
nn.BatchNorm1d(256),
nn.ReLU(),
nn.Dropout(0.3), # BN + Dropout сосуществуют, распространены и эффективны
nn.Linear(256, 10),
)
Свёрточные сети: обычно выбирают одно или другое, либо используют Dropout только в головной части.
Причина: операция нормализации BN и Dropout могут вызвать проблему «сдвига дисперсии». То есть во время обучения Dropout изменяет дисперсию значений активаций, а во время инференса Dropout отключается, из-за чего статистика, которую BN видит во время инференса, оказывается несогласованной со статистикой во время обучения.
Рекомендуемая практика:
Вариант A: использовать только BN в свёрточных слоях, а только Dropout — в полносвязных/классификационных головах
Пример
def __init__(self, num_classes):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 64, 3, padding=1),
nn.BatchNorm2d(64), # Свёрточная часть: только BN
nn.ReLU(inplace=True),
nn.Conv2d(64, 128, 3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
nn.AdaptiveAvgPool2d(1),
)
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear(128, 256),
nn.ReLU(),
nn.Dropout(0.5), # Полносвязная часть: только Dropout
nn.Linear(256, num_classes),
)
def forward(self, x):
return self.classifier(self.features(x))
5. Полные примеры сетей
Пример 1: Полносвязная сеть классификации (BN + Dropout)
Пример
import torch.nn as nn
class MLPClassifier(nn.Module):
"""
Полносвязный классификатор с BatchNorm1d и Dropout
Подходит для задач классификации табличных данных
"""
def __init__(self, in_dim, hidden_dims, num_classes, dropout=0.5):
super().__init__()
layers = []
prev_dim = in_dim
for hidden_dim in hidden_dims:
layers += [
nn.Linear(prev_dim, hidden_dim),
nn.BatchNorm1d(hidden_dim),
nn.ReLU(inplace=True),
nn.Dropout(dropout),
]
prev_dim = hidden_dim
layers.append(nn.Linear(prev_dim, num_classes))
self.net = nn.Sequential(*layers)
def forward(self, x):
return self.net(x)
model = MLPClassifier(
in_dim=784,
hidden_dims=[512, 256, 128],
num_classes=10,
dropout=0.4
)
# Проверить форму выхода
x = torch.randn(32, 784)
model.eval()
print(model(x).shape) # torch.Size([32, 10])
Пример 2: классификация изображений с помощью CNN (в основном BN)
Пример
"""Стандартный сверточный блок: Conv → BN → ReLU"""
def __init__(self, in_ch, out_ch, stride=1):
super().__init__()
self.block = nn.Sequential(
nn.Conv2d(in_ch, out_ch, 3, stride=stride, padding=1, bias=False),
nn.BatchNorm2d(out_ch), # bias=False: beta в BN уже служит смещением
nn.ReLU(inplace=True),
)
def forward(self, x):
return self.block(x)
class SmallCNN(nn.Module):
"""Небольшая CNN для CIFAR-10"""
def __init__(self, num_classes=10):
super().__init__()
self.features = nn.Sequential(
ConvBNBlock(3, 32), # 32×32 → 32×32
ConvBNBlock(32, 64, stride=2), # 32×32 → 16×16
ConvBNBlock(64, 128, stride=2), # 16×16 → 8×8
ConvBNBlock(128, 256, stride=2), # 8×8 → 4×4
nn.AdaptiveAvgPool2d(1), # 4×4 → 1×1
)
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear(256, 128),
nn.ReLU(),
nn.Dropout(0.5), # Использовать Dropout только в полносвязных слоях
nn.Linear(128, num_classes),
)
def forward(self, x):
return self.classifier(self.features(x))
model = SmallCNN(num_classes=10)
x = torch.randn(16, 3, 32, 32)
model.eval()
print(model(x).shape) # torch.Size([16, 10])
Пример 3: Полный цикл обучения
Пример
import torch.nn as nn
import torch.optim as optim
from torch.optim.lr_scheduler import CosineAnnealingLR
DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
EPOCHS = 50
model = SmallCNN(num_classes=10).to(DEVICE)
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)
scheduler = CosineAnnealingLR(optimizer, T_max=EPOCHS)
def run_epoch(model, loader, optimizer=None, train=True):
if train:
model.train() # ← BN использует статистику батча, Dropout активируется
else:
model.eval() # ← BN использует скользящую статистику, Dropout отключается
total_loss, correct = 0.0, 0
ctx = torch.enable_grad() if train else torch.no_grad()
with ctx:
for imgs, labels in loader:
imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)
if train:
optimizer.zero_grad()
outputs = model(imgs)
loss = criterion(outputs, labels)
if train:
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
best_acc = 0.0
for epoch in range(1, EPOCHS + 1):
train_loss, train_acc = run_epoch(model, train_loader, optimizer, train=True)
val_loss, val_acc = run_epoch(model, val_loader, train=False)
scheduler.step()
print(f"Эпоха {epoch:2d}/{EPOCHS} | "
f"Обучение {train_loss:.4f}/{train_acc:.4f} | "
f"Валидация {val_loss:.4f}/{val_acc:.4f}")
if val_acc > best_acc:
best_acc = val_acc
torch.save(model.state_dict(), 'best.pth')
6. Руководство по настройке гиперпараметров
Настройка параметров BatchNorm
Пример
# Значение по умолчанию 0.1 работает в большинстве случаев
# batch_size очень мал (< 8) → статистики нестабильны → уменьшите momentum
nn.BatchNorm2d(64, momentum=0.01)
# Много шагов обучения, стабильное распределение данных → можно соответствующим образом увеличить momentum
nn.BatchNorm2d(64, momentum=0.2)
# ── affine=False (не обучать gamma/beta) ───────────────
# Редко используется, обычно оставляют значение по умолчанию True
# Если только вы явно не хотите, чтобы BN изменял масштаб и смещение признаков
nn.BatchNorm2d(64, affine=False)
# ── track_running_stats=False ──────────────────────
# При каждом выводе также используются текущие статистики батча (поведение при обучении и выводе совпадает)
# Подходит для особых сценариев, таких как онлайн-обучение и потоковые данные
nn.BatchNorm2d(64, track_running_stats=False)
Стратегии настройки Dropout
Пример
# Точность на обучении намного выше точности на валидации (сильное переобучение) → увеличьте p
# Точность на обучении и точность на валидации близки, но обе низкие (недообучение) → уменьшите p или удалите Dropout
# ── Справочник рекомендуемых значений p ───────────────────────────────
p_values = {
'Большие полносвязные скрытые слои (> 1024)': 0.5,
'Средние полносвязные скрытые слои (256~1024)': 0.3,
'Малые полносвязные скрытые слои (< 256)': 0.2,
'Свёрточные слои CNN (Dropout2d)': 0.1,
'Перед классификационной головой': 0.3,
'FFN трансформера': 0.1,
'Механизм внимания Transformer': 0.1,
}
# ── Динамический Dropout (корректируется по мере обучения) ──────────────────
# В некоторых практиках p мало на ранних этапах обучения и увеличивается позже (усиливать регуляризацию по мере усложнения модели)
class DynamicDropoutNet(nn.Module):
def __init__(self, initial_p=0.1):
super().__init__()
self.dropout = nn.Dropout(initial_p)
self.fc = nn.Linear(256, 10)
def set_dropout(self, p):
self.dropout.p = p
def forward(self, x):
return self.fc(self.dropout(x))
model = DynamicDropoutNet(initial_p=0.1)
# В середине обучения увеличьте Dropout
model.set_dropout(0.4)
7. Типичные ошибки и предостережения
Ошибка 1: Забыли переключиться в режим eval()
Неправильный код
model.train() # Состояние по умолчанию
for imgs, labels in val_loader:
with torch.no_grad():
outputs = model(imgs) # Dropout всё ещё случайно отключает нейроны! BN всё ещё обновляется!
# Результаты недетерминированы, точность искусственно занижена, а running_mean/var загрязнён
Правильный код
model.eval()
with torch.no_grad():
outputs = model(imgs) # Детерминированный вывод
Ошибка 2: Добавление bias к слою Conv и затем использование BN
Неправильный код
nn.Conv2d(64, 128, 3, padding=1, bias=True), # По умолчанию True
nn.BatchNorm2d(128),
Правильный код
nn.Conv2d(64, 128, 3, padding=1, bias=False), # ← bias=False
nn.BatchNorm2d(128),
Ошибка 3: Использование BatchNorm с размером батча=1
Неправильный код
# Вывод на одном образце нормален (использует текущую статистику), но обучение на одном образце приведет к сбою
model.train()
x = torch.randn(1, 64, 32, 32)
out = bn2d(x) # ← batch=1 в режиме обучения вызовет ошибку или выдаст NaN
Решение
nn.GroupNorm(num_groups=32, num_channels=64)
# Решение 2: Использовать LayerNorm
nn.LayerNorm([64, 32, 32])
# Решение 3: Переключиться на eval() во время инференса, не влияет на BN
model.eval()
out = bn2d(x) # batch=1 в режиме инференса — это совершенно нормально
Ошибка 4: Dropout вызывает сдвиг дисперсии в BN
Проблема
# Во время инференса Dropout отключён, и BN использует running_var, накопленную при обучении (содержащую эффект Dropout)
# Статистики не совпадают, результаты инференса смещены
nn.Sequential(
nn.Conv2d(64, 128, 3),
nn.Dropout2d(0.5), # ← Используйте Dropout в свёрточных слоях
nn.BatchNorm2d(128), # ← Дисперсия, которую видит BN, изменяется из-за Dropout
)
Решение
nn.Sequential(
nn.Conv2d(64, 128, 3),
nn.BatchNorm2d(128), # сначала BN (на него не влияет Dropout)
nn.ReLU(),
nn.Dropout2d(0.1), # Dropout после
)
# Решение 2: используйте только BN в свёрточных слоях и только Dropout в полносвязных слоях (наиболее рекомендуется)
Ошибка 5: Забыли переключить замороженные слои BN в режим eval
Неправильный код
# Иначе новое распределение данных загрязнит предобученную статистику
# Ошибка: параметры заморожены, но running_mean/var слоёв BN всё ещё обновляются
model = models.resnet50(weights='IMAGENET1K_V2')
for param in model.parameters():
param.requires_grad = False # Замораживаются только обучаемые параметры; статистики BN всё ещё обновляются
Правильный код
def freeze_bn(model):
for module in model.modules():
if isinstance(module, (nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d)):
module.eval() # Исправить running_mean/var
for param in module.parameters():
param.requires_grad = False
model = models.resnet50(weights='IMAGENET1K_V2')
freeze_bn(model) # Заморозить все BN
model.fc = nn.Linear(model.fc.in_features, 10) # Только новый FC-слой можно обучать
Быстрый диагностический контрольный список
| Явление | Возможная причина | Пункт проверки |
|---|---|---|
| Loss колеблется непредсказуемо | Забыли [model.eval()]model.eval() |
Убедитесь, что вызываете [model.eval()] перед оценкойeval() |
| Большой разрыв между точностью на обучении и валидации | Dropout pСлишком большое значение p в Dropout |
Попробуйте уменьшить ppили удалите Dropout |
| NaN/inf при обучении | BatchNorm используется с batch=1 | Переключитесь на GroupNorm или LayerNorm |
| Плохая эффективность transfer learning | Испорчена статистика running stats у BN | Вызовите [.eval()] для BN-слоёв backboneeval() |
| Нестабильные результаты инференса модели | Не переключились в eval() во время инференсаeval() |
Перед инференсом необходимо вызвать [model.eval()]model.eval() |
| Чрезмерное количество параметров Conv | Conv+BN не отключил bias | Установите [bias=False]bias=False |