Dropout та комбіновані стратегії регуляризації
Боротьба з перенавчанням — коли модель «запам'ятовує» замість «розуміє»
У попередній статті ми опанували Batch Normalization — потужну техніку стабілізації навчання глибоких мереж. BN дозволяє використовувати великі learning rates, зменшує залежність від ініціалізації та навіть діє як імпліцитна регуляризація. Але що, якщо ваша модель надто добре навчається на тренувальних даних?
Уявіть ситуацію: ви навчаєте CNN на датасеті з 5000 зображень. Після 50 епох train accuracy досягає 99.5% — фантастика! Ви радісно тестуєте модель на новому датасеті і... отримуєте лише 65%. Що пішло не так?
Це класичний випадок перенавчання (overfitting): модель не вивчила загальні закономірності розпізнавання об'єктів, а запам'ятала конкретні тренувальні зображення. Вона «вивчила підручник напам'ять», але не зрозуміла матеріал. При зустрічі з новими даними модель безпорадна.
📚 Аналогія: екзамен у студентів
Студент A (Overfit): Вивчив напам'ять усі приклади з підручника. На екзамені отримує ті самі завдання — 100%. Змініть формулювання — провал.
Студент B (Генералізація): Зрозумів концепції та підходи. На екзамені вирішує нові завдання за аналогією — 85%. Стабільний результат на будь-яких варіантах.
Ми хочемо, щоб наші моделі були як студент B!
💾 Ємність моделі vs розмір датасету
Перенавчання — одна з центральних проблем машинного навчання. За останні роки розроблено безліч технік боротьби з ним. У цій статті ми детально розглянемо Dropout — елегантну та надзвичайно ефективну техніку, запропоновану у 2012 році, яка досі широко використовується.
🎯 Мета статті
- Зрозуміти природу перенавчання та його візуальні ознаки
- Опанувати ідею та математику Dropout: випадкове вимикання нейронів
- Дослідити, чому Dropout запобігає ко-адаптації нейронів
- Навчитися правильно використовувати Dropout у PyTorch (train/eval режими)
- Комбінувати регуляризаційні техніки: Dropout, BatchNorm, Weight Decay, Data Augmentation
- Провести експеримент на CIFAR-10 з різними стратегіями
🔑 Ключові терміни
- Overfitting (перенавчання): модель занадто точно підлаштовується під тренувальні дані, втрачаючи здатність генералізувати
- Dropout: випадкове вимикання нейронів з ймовірністю Завантаження... під час навчання
- Co-adaptation: нейрони «спеціалізуються» на корекції помилок один одного замість вивчення загальних ознак
- Ensemble Effect: Dropout створює ансамбль експоненціально багатьох підмереж
- Inverted Dropout: масштабування активацій на Завантаження... під час тренування (стандарт PyTorch)
Нагадування: що таке перенавчання
Перш ніж перейти до розв'язання, переконаймося, що ми чітко розуміємо проблему. Перенавчання виникає, коли модель має занадто багато параметрів відносно кількості тренувальних даних.
Візуалізація перенавчання
Розглянемо класичну задачу поліноміальної регресії. Маємо 20 точок даних, згенерованих з функції Завантаження.... Спробуємо апроксимувати їх поліномами різного ступеня.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
# Генерація даних
np.random.seed(42)
n_samples = 20
X_train = np.sort(np.random.uniform(0, 1, n_samples))
y_train = np.sin(2 * np.pi * X_train) + np.random.normal(0, 0.1, n_samples)
# Тестові дані (справжня функція)
X_test = np.linspace(0, 1, 100)
y_test_true = np.sin(2 * np.pi * X_test)
# Апроксимація поліномами різних ступенів
degrees = [1, 4, 15]
models = {}
for degree in degrees:
poly_features = PolynomialFeatures(degree=degree)
X_train_poly = poly_features.fit_transform(X_train.reshape(-1, 1))
X_test_poly = poly_features.transform(X_test.reshape(-1, 1))
model = LinearRegression()
model.fit(X_train_poly, y_train)
y_train_pred = model.predict(X_train_poly)
y_test_pred = model.predict(X_test_poly)
train_mse = mean_squared_error(y_train, y_train_pred)
test_mse = mean_squared_error(y_test_true, y_test_pred)
models[degree] = {
'model': model,
'y_test_pred': y_test_pred,
'train_mse': train_mse,
'test_mse': test_mse
}
# Візуалізація
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
for idx, degree in enumerate(degrees):
ax = axes[idx]
# Справжня функція
ax.plot(X_test, y_test_true, 'g--', linewidth=2, alpha=0.7, label='Справжня функція')
# Тренувальні точки
ax.scatter(X_train, y_train, color='blue', s=80, alpha=0.7,
edgecolor='black', linewidth=1.5, label='Тренувальні дані', zorder=5)
# Апроксимація
ax.plot(X_test, models[degree]['y_test_pred'], 'r-', linewidth=2.5,
label=f'Поліном степеня {degree}')
ax.set_xlabel('x', fontsize=12)
ax.set_ylabel('y', fontsize=12)
ax.set_ylim([-1.5, 1.5])
ax.grid(alpha=0.3)
ax.legend(fontsize=10)
# Статистика
train_mse = models[degree]['train_mse']
test_mse = models[degree]['test_mse']
if degree == 1:
title = f'Underfitting (ступінь {degree})'
status = '❌ Занадто проста'
elif degree == 4:
title = f'Хороша генералізація (ступінь {degree})'
status = '✅ Баланс'
else:
title = f'Overfitting (ступінь {degree})'
status = '❌ Надто складна'
ax.set_title(f'{title}\nTrain MSE: {train_mse:.4f}, Test MSE: {test_mse:.4f}',
fontsize=11, fontweight='bold')
ax.text(0.5, -1.3, status, ha='center', fontsize=12,
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))
plt.tight_layout()
plt.show()

- Ступінь 1 (лінійна): Занадто проста модель, не може захопити синусоїдальну природу даних. Високі помилки і на train, і на test. Це underfitting.
- Ступінь 4: Оптимальний баланс. Модель захоплює загальну форму синусоїди, але не підлаштовується під шум. Низькі помилки на обох датасетах.
- Ступінь 15: Модель проходить через кожну тренувальну точку (train MSE ≈ 0), але між ними робить дикі коливання. Це overfitting — модель запам'ятала шум.
Ознаки перенавчання у нейронних мережах
Для нейронних мереж перенавчання має схожі симптоми:
📉 Gap між train та validation loss
Здорова модель: train loss ≈ val loss ± 5-10%
Overfit: train loss значно менша за val loss (gap > 20%)
Графік: train loss продовжує зменшуватися, а val loss зупиняється або зростає.
🎯 Дуже висока точність на train, низька на val
Приклад: Train accuracy = 98%, Val accuracy = 72%
Модель «вивчила напам'ять» тренувальні приклади, але не може розпізнати нові.
🔧 Модель чутлива до малих змін входу
Dropout — випадкове вимикання нейронів
У 2012 році Geoffrey Hinton зі співавторами запропонували елегантну техніку боротьби з перенавчанням: Dropout. Ідея шокуюче проста: під час навчання випадково вимикати частину нейронів у кожній ітерації.
Ідея та інтуїція
Уявіть команду з 10 інженерів, що працюють над проєктом:
❌ Без Dropout: ко-адаптація
Кожен інженер спеціалізується на вузькій задачі: «Олександр виправляє баги Марії, Марія компенсує недоліки коду Дмитра, Дмитро покладається на перевірки Олександра...»
Проблема: Якщо хтось захворіє, вся система колапсує. Команда ко-адаптувалася — кожен залежить від інших замість самостійної роботи.
✅ З Dropout: незалежність
Кожного дня випадково 50% команди відсутні. Кожен інженер змушений писати робастний код, який працює незалежно від інших.
Результат: Коли вся команда працює разом (inference), система надзвичайно стабільна та універсальна.
Так само працює Dropout у нейронних мережах:
Математика Dropout
Нехай Завантаження... — активації прихованого шару. Під час навчання:
Крок 1. Генерація маски:
Де Завантаження... — dropout rate (ймовірність вимкнення нейрона). Зазвичай Завантаження... для FC шарів, Завантаження... для Conv.
Крок 2. Застосування маски (Inverted Dropout):
Чому ділимо на Завантаження...?
Під час навчання в середньому лише Завантаження... нейронів активні. Щоб очікуване значення активацій залишалося незмінним, ми масштабуємо на Завантаження....
Приклад: якщо Завантаження..., то в середньому половина нейронів активна, тому множимо на Завантаження....
Під час inference (eval mode):
Всі нейрони активні, без масштабування. Завдяки Inverted Dropout під час тренування, очікувані значення вже правильні.
- Standard Dropout: Масштабування на Завантаження... під час inference
- Inverted Dropout: Масштабування на Завантаження... під час training (використовується у PyTorch)
Чому Dropout працює — теоретичне обґрунтування
Dropout не просто «магічно» покращує генералізацію. Існують глибокі теоретичні причини його ефективності.
Причина 1: Запобігання ко-адаптації нейронів
Без Dropout нейрони можуть ко-адаптуватися: один нейрон спеціалізується на виправленні помилок іншого. Це призводить до складних залежностей між нейронами, які не генералізуються на нові дані.
Приклад: Уявімо два нейрона у прихованому шарі:
- Нейрон A виявляє «очі» на зображенні
- Нейрон B коригує помилки A, виявляючи «псевдо-очі» (блики, тіні)
Якщо A завжди активний, B стає залежним від A. При новому зображенні, де A помиляється, B також збивається.
З Dropout кожен нейрон змушений бути self-sufficient — працювати навіть коли інші нейрони вимкнені.
Причина 2: Ансамбль експоненціально багатьох моделей
Розглянемо мережу з Завантаження... нейронами у прихованому шарі. Кількість можливих підмереж (комбінацій активних/вимкнених нейронів):
Для Завантаження... (типовий розмір шару) це Завантаження... різних моделей!
Dropout під час навчання = тренування ансамблю
Кожна міні-батч ітерація навчає іншу підмережу. Після завершення навчання у нас є величезний ансамбль моделей, які «голосують» разом під час inference.
Причина 3: Додавання шуму для регуляризації
Dropout додає мультиплікативний шум до активацій:
Це схоже на data augmentation, але для внутрішніх представлень замість входів. Модель вчиться бути стійкою до випадкових «збоїв» у проміжних шарах.
Dropout у PyTorch — train та eval режими
Як і Batch Normalization, Dropout поводиться по-різному під час навчання та inference. Це критично важливо розуміти!
import torch
import torch.nn as nn
# Модель з Dropout
class MLPWithDropout(nn.Module):
def __init__(self):
super(MLPWithDropout, self).__init__()
self.fc1 = nn.Linear(100, 256)
self.dropout1 = nn.Dropout(p=0.5)
self.fc2 = nn.Linear(256, 128)
self.dropout2 = nn.Dropout(p=0.5)
self.fc3 = nn.Linear(128, 10)
self.relu = nn.ReLU()
def forward(self, x):
x = self.relu(self.fc1(x))
x = self.dropout1(x) # Dropout після активації
x = self.relu(self.fc2(x))
x = self.dropout2(x)
x = self.fc3(x) # Останній шар БЕЗ Dropout
return x
model = MLPWithDropout()
x = torch.randn(4, 100) # Батч з 4 прикладів
print("🔬 Експеримент: Dropout у train vs eval режимах\n")
# Train mode
model.train()
out1_train = model(x)
out2_train = model(x)
print("📊 Training Mode (Dropout активний):")
print(f" 1-й forward: {out1_train[0, :3].detach().numpy()}")
print(f" 2-й forward: {out2_train[0, :3].detach().numpy()}")
print(f" Різниця: {torch.norm(out1_train - out2_train).item():.6f}")
print(f" Виходи РІЗНІ через випадкову маску! ✅\n")
# Eval mode
model.eval()
out1_eval = model(x)
out2_eval = model(x)
print("📊 Evaluation Mode (Dropout вимкнений):")
print(f" 1-й forward: {out1_eval[0, :3].detach().numpy()}")
print(f" 2-й forward: {out2_eval[0, :3].detach().numpy()}")
print(f" Різниця: {torch.norm(out1_eval - out2_eval).item():.6f}")
print(f" Виходи ІДЕНТИЧНІ (детермінований inference)! ✅")
🔬 Експеримент: Dropout у train vs eval режимах
📊 Training Mode (Dropout активний):
1-й forward: [-0.421 1.254 -0.892]
2-й forward: [ 0.184 -0.673 1.109]
Різниця: 2.548721 ← Виходи РІЗНІ через випадкову маску! ✅
📊 Evaluation Mode (Dropout вимкнений):
1-й forward: [-0.105 0.428 -0.312]
2-й forward: [-0.105 0.428 -0.312]
Різниця: 0.000000 ← Виходи ІДЕНТИЧНІ (детермінований inference)! ✅
# ❌ НЕПРАВИЛЬНО
model.train() # Навчання завершено, але режим не змінено
# Тестування з активним Dropout!
for images, labels in test_loader:
outputs = model(images) # Випадкові результати кожного разу!
# ✅ ПРАВИЛЬНО
model.eval() # ОБОВ'ЯЗКОВО перед inference
with torch.no_grad():
for images, labels in test_loader:
outputs = model(images) # Детермінований, стабільний результат
Де розміщувати Dropout у архітектурі
Не всі шари потребують Dropout. Існують перевірені практикою паттерни.
Для Fully Connected мереж (MLP)
Стандартний паттерн:
Linear → ReLU → Dropout(0.5) → Linear → ReLU → Dropout(0.5) → Linear
Правила:
- Dropout після активації (ReLU)
- Dropout rate 0.5 для великих FC шарів (512+ нейронів)
- НЕ застосовувати до останнього output шару
Для Convolutional мереж (CNN)
Рекомендовано:
# Conv блоки: БЕЗ Dropout (BatchNorm достатньо)
Conv → BatchNorm → ReLU → Pool
Conv → BatchNorm → ReLU → Pool
# Classifier (FC шари): З Dropout
Flatten → Linear → ReLU → Dropout(0.5) → Linear
Чому не використовувати Dropout у Conv шарах?
- Просторова кореляція: Сусідні пікселі корелюють, тому вимикання окремих feature maps менш ефективне
- BatchNorm вже регуляризує: Conv шари з BN рідко перенавчуються
- Dropout2d дорогий: Вимикає цілі feature maps, що значно уповільнює навчання
nn.Dropout2d, який вимикає цілі канали замість окремих пікселів:nn.Dropout2d(p=0.1) # Вимикає 10% каналів
Таблиця рекомендацій
| Тип шару | Dropout rate | Коментар |
|---|---|---|
| Input шар | 0.1-0.2 | Рідко використовується (краще data augmentation) |
| Conv шари | Не застосовувати | BatchNorm + data augmentation достатньо |
| FC шари (великі 512+) | 0.5 | Класичне значення з оригінальної статті |
| FC шари (малі < 256) | 0.2-0.3 | Менший rate для менших шарів |
| Output шар | Ніколи! | Псує калібрацію ймовірностей |
| LSTM/GRU | 0.2-0.3 | Між шарами, не всередині рекурентних з'єднань |
Комбінування регуляризаційних технік
Рідко використовується лише одна техніка регуляризації. Найкращі результати досягаються комбінуванням кількох методів.
🛡️ Batch Normalization
Ефект: Стабілізація навчання + імпліцитна регуляризація через шум у батч-статистиці
Застосування: Після кожного Conv/Linear перед активацією
Сила: Середня (регуляризація — побічний ефект, не основна мета)
💧 Dropout
Ефект: Запобігання ко-адаптації нейронів + ансамбль ефект
Застосування: Після активацій у FC шарах (p=0.2-0.5)
Сила: Сильна для великих FC шарів
⚖️ Weight Decay (L2 Regularization)
Ефект: Штраф за великі ваги, підштовхує до простіших розв'язків
Застосування: Параметр оптимізатора (weight_decay=5e-4)
Сила: Помірна, але універсальна
🎨 Data Augmentation
Ефект: Збільшення розмаїття даних, модель бачить варіації кожного зображення
Застосування: Трансформації при завантаженні (transforms.RandomHorizontalFlip())
Сила: Найпотужніша для комп'ютерного зору!
Стратегії комбінування
Експеримент: комбінування регуляризаційних технік на CIFAR-10
Порівняємо 5 стратегій:
- Baseline — без регуляризації (очікуємо overfit)
- BatchNorm only
- Dropout only (p=0.5)
- BatchNorm + Dropout (p=0.3, зменшений через BN)
- Повний стек: BatchNorm + Dropout + Weight Decay + Data Augmentation
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
import time
# 5 різних моделей
class CNN_Baseline(nn.Module):
"""Без регуляризації"""
def __init__(self):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),
nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),
nn.Conv2d(128, 256, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),
)
self.classifier = nn.Sequential(
nn.Linear(256*4*4, 512), nn.ReLU(),
nn.Linear(512, 10)
)
def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1)
return self.classifier(x)
class CNN_BatchNormOnly(nn.Module):
"""Лише BatchNorm"""
def __init__(self):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2),
nn.Conv2d(64, 128, 3, padding=1), nn.BatchNorm2d(128), nn.ReLU(), nn.MaxPool2d(2),
nn.Conv2d(128, 256, 3, padding=1), nn.BatchNorm2d(256), nn.ReLU(), nn.MaxPool2d(2),
)
self.classifier = nn.Sequential(
nn.Linear(256*4*4, 512), nn.BatchNorm1d(512), nn.ReLU(),
nn.Linear(512, 10)
)
def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1)
return self.classifier(x)
class CNN_DropoutOnly(nn.Module):
"""Лише Dropout"""
def __init__(self):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),
nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),
nn.Conv2d(128, 256, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),
)
self.classifier = nn.Sequential(
nn.Linear(256*4*4, 512), nn.ReLU(), nn.Dropout(0.5),
nn.Linear(512, 10)
)
def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1)
return self.classifier(x)
class CNN_BN_Dropout(nn.Module):
"""BatchNorm + Dropout (зменшений)"""
def __init__(self):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2),
nn.Conv2d(64, 128, 3, padding=1), nn.BatchNorm2d(128), nn.ReLU(), nn.MaxPool2d(2),
nn.Conv2d(128, 256, 3, padding=1), nn.BatchNorm2d(256), nn.ReLU(), nn.MaxPool2d(2),
)
self.classifier = nn.Sequential(
nn.Linear(256*4*4, 512), nn.BatchNorm1d(512), nn.ReLU(), nn.Dropout(0.3),
nn.Linear(512, 10)
)
def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1)
return self.classifier(x)
# Трансформації
transform_simple = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
])
transform_augmented = transforms.Compose([
transforms.RandomHorizontalFlip(),
transforms.RandomCrop(32, padding=4),
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
])
# Датасети
train_simple = datasets.CIFAR10('./data', train=True, download=True, transform=transform_simple)
train_augmented = datasets.CIFAR10('./data', train=True, download=True, transform=transform_augmented)
test_dataset = datasets.CIFAR10('./data', train=False, download=True, transform=transform_simple)
def train_model(model, train_loader, test_loader, epochs=20, lr=0.01, weight_decay=0):
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=lr, momentum=0.9, weight_decay=weight_decay)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
history = {'train_acc': [], 'test_acc': []}
for epoch in range(epochs):
# Train
model.train()
train_correct, train_total = 0, 0
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
_, predicted = outputs.max(1)
train_total += labels.size(0)
train_correct += predicted.eq(labels).sum().item()
# Test
model.eval()
test_correct, test_total = 0, 0
with torch.no_grad():
for images, labels in test_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
_, predicted = outputs.max(1)
test_total += labels.size(0)
test_correct += predicted.eq(labels).sum().item()
train_acc = 100. * train_correct / train_total
test_acc = 100. * test_correct / test_total
history['train_acc'].append(train_acc)
history['test_acc'].append(test_acc)
scheduler.step()
return history
# Експеримент
configs = [
("Baseline (без регул.)", CNN_Baseline(), train_simple, 0.01, 0),
("BatchNorm only", CNN_BatchNormOnly(), train_simple, 0.1, 0),
("Dropout only", CNN_DropoutOnly(), train_simple, 0.01, 0),
("BN + Dropout", CNN_BN_Dropout(), train_simple, 0.1, 0),
("Повний стек", CNN_BN_Dropout(), train_augmented, 0.1, 5e-4),
]
results = {}
test_loader = DataLoader(test_dataset, batch_size=128, shuffle=False, num_workers=2)
print("🚀 Експеримент: порівняння регуляризаційних стратегій\n")
for name, model, train_data, lr, wd in configs:
print(f"📊 Навчання: {name}")
torch.manual_seed(42)
train_loader = DataLoader(train_data, batch_size=128, shuffle=True, num_workers=2)
history = train_model(model, train_loader, test_loader, epochs=20, lr=lr, weight_decay=wd)
results[name] = history
final_train = history['train_acc'][-1]
final_test = history['test_acc'][-1]
gap = final_train - final_test
print(f" Фінал: Train={final_train:.2f}%, Test={final_test:.2f}%, Gap={gap:.2f}%\n")
print("🎉 Експеримент завершено!")
🚀 Експеримент: порівняння регуляризаційних стратегій
📊 Навчання: Baseline (без регул.)
- Фінал: Train=92.34%, Test=68.52%, Gap=23.82% ← Сильне перенавчання!
📊 Навчання: BatchNorm only
- Фінал: Train=89.67%, Test=76.84%, Gap=12.83%
📊 Навчання: Dropout only
- Фінал: Train=84.23%, Test=74.15%, Gap=10.08%
📊 Навчання: BN + Dropout
- Фінал: Train=88.45%, Test=79.23%, Gap=9.22%
📊 Навчання: Повний стек
- Фінал: Train=91.12%, Test=83.67%, Gap=7.45% 🏆
🎉 Експеримент завершено!
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 2, figsize=(16, 6))
colors = {
"Baseline (без регул.)": '#ef4444',
"BatchNorm only": '#f59e0b',
"Dropout only": '#3b82f6',
"BN + Dropout": '#8b5cf6',
"Повний стек": '#22c55e'
}
# Графік 1: Test Accuracy
ax1 = axes[0]
for name, history in results.items():
ax1.plot(range(1, 21), history['test_acc'],
linewidth=2.5, label=name, color=colors[name], marker='o', markersize=4)
ax1.set_xlabel('Епоха', fontsize=13)
ax1.set_ylabel('Test Accuracy (%)', fontsize=13)
ax1.set_title('Точність на тестовому датасеті', fontsize=15, fontweight='bold')
ax1.legend(fontsize=10, loc='lower right')
ax1.grid(alpha=0.3)
ax1.set_ylim([60, 90])
# Графік 2: Overfitting Gap
ax2 = axes[1]
final_gaps = {}
for name, history in results.items():
gap = history['train_acc'][-1] - history['test_acc'][-1]
final_gaps[name] = gap
names = list(final_gaps.keys())
gaps = list(final_gaps.values())
bars = ax2.barh(names, gaps, color=[colors[n] for n in names], alpha=0.7)
for i, (bar, gap) in enumerate(zip(bars, gaps)):
ax2.text(gap + 0.5, i, f'{gap:.1f}%', va='center', fontsize=11, fontweight='bold')
ax2.set_xlabel('Train-Test Accuracy Gap (%)', fontsize=13)
ax2.set_title('Міра перенавчання (менше = краще)', fontsize=15, fontweight='bold')
ax2.grid(axis='x', alpha=0.3)
ax2.invert_yaxis()
plt.tight_layout()
plt.show()
# Підсумкова таблиця
print("\n📊 Порівняльна таблиця:\n")
print(f"{'Стратегія':<25} | {'Train Acc':<10} | {'Test Acc':<10} | {'Gap':<8} | {'Overfit?':<10}")
print("-" * 80)
for name, history in results.items():
train = history['train_acc'][-1]
test = history['test_acc'][-1]
gap = train - test
overfit = "❌ Так" if gap > 15 else "⚠️ Помірно" if gap > 10 else "✅ Ні"
print(f"{name:<25} | {train:<10.2f} | {test:<10.2f} | {gap:<8.2f} | {overfit:<10}")

📊 Порівняльна таблиця:
| Стратегія | Train Acc | Test Acc | Gap | Overfit? |
|---|---|---|---|---|
| Baseline (без регул.) | 92.34% | 68.52% | 23.82% | ❌ Так |
| BatchNorm only | 89.67% | 76.84% | 12.83% | ⚠️ Помірно |
| Dropout only | 84.23% | 74.15% | 10.08% | ⚠️ Помірно |
| BN + Dropout | 88.45% | 79.23% | 9.22% | ✅ Ні |
| Повний стек | 91.12% | 83.67% | 7.45% | ✅ Ні |
🏆 Переможець: Повний стек
83.67% test accuracy — найкращий результат!
Композиція:
- BatchNorm для стабілізації
- Dropout (p=0.3) для ансамблю
- Weight Decay (5e-4) для простоти ваг
- Data Augmentation для розмаїття даних
Gap лише 7.45% — мінімальне перенавчання.
⚠️ Baseline: катастрофа
Gap 23.82% — модель запам'ятала тренувальні дані замість вивчення паттернів.
68.52% test accuracy — гірше за BN+Dropout на 15%!
💡 Ключовий інсайт
Одна техніка добре, дві краще, три — найкраще!
BatchNorm + Dropout краще за кожну окремо.
Додавання Data Augmentation дає ще +4% test accuracy.
Рекомендації для різних розмірів датасетів
Стратегія регуляризації залежить від розміру датасету та складності моделі.
📦 Малий датасет (< 10K)
Приклад: Власний датасет медичних знімків (5000 зображень)
Стратегія:
- ✅ Data Augmentation (агресивна): horizontal flip, rotation ±15°, color jitter, random erasing
- ✅ Dropout (p=0.5-0.6): Сильна регуляризація FC шарів
- ✅ Weight Decay (1e-3): Більший штраф
- ✅ BatchNorm: Завжди
- ✅ Transfer Learning: Використовуйте pretrained модель!
Ризик: Дуже високий overfit. Модель може досягти 100% train accuracy при 60% val.
📊 Середній датасет (10K-100K)
Приклад: CIFAR-10 (50K), Fashion-MNIST (60K)
Стратегія:
- ✅ Data Augmentation (помірна): horizontal flip, random crop
- ✅ BatchNorm: Обов'язково
- ✅ Dropout (p=0.2-0.3): Помірний
- ✅ Weight Decay (5e-4): Стандартний
- ⚠️ Transfer Learning: Опційно (можна навчити з нуля)
Баланс: Достатньо даних для навчання з нуля, але регуляризація все ще важлива.
🗄️ Великий датасет (> 100K)
Приклад: ImageNet (1.2M), MS COCO (300K)
Стратегія:
- ✅ Data Augmentation (легка): horizontal flip, minimal crop
- ✅ BatchNorm: Так
- ⚠️ Dropout (p=0.1-0.2 або ні): Може не знадобитися
- ⚠️ Weight Decay (1e-4): Менший
- ❌ Transfer Learning: Навчаємо з нуля
Особливість: Великий датасет сам по собі регуляризує. Надмірна регуляризація може погіршити результати (underfitting).
Висновки та практичні поради
ТАК, але з обережністю.
BatchNorm вже додає регуляризацію, тому:
- Зменшіть Dropout rate з 0.5 до 0.2-0.3
- Або використовуйте лише один з них
Експериментально підтверджено: BN + Dropout(0.3) краще за Dropout(0.5) без BN.
Data Augmentation — значно важливіша для зображень!
Порядок пріоритетів:
- Data Augmentation — найбільший ефект
- BatchNorm — стабілізація + регуляризація
- Weight Decay — універсальна регуляризація
- Dropout — додатковий буст для великих FC шарів
Для малих датасетів використовуйте все разом.
Емпіричне правило:
| Умова | Dropout rate |
|---|---|
| Без BatchNorm, великі FC шари | 0.5 (класика) |
| З BatchNorm, великі FC шари | 0.2-0.3 |
| Малі FC шари (< 256) | 0.1-0.2 |
| Conv шари | 0 або 0.1 (рідко) |
| RNN/LSTM | 0.2-0.3 між шарами |
Золоте правило: Почніть з 0.3, збільшуйте якщо overfit, зменшуйте якщо underfit.
Залежить від розміру нового датасету:
- < 1000 прикладів: Сильна регуляризація (Dropout 0.5, freeze більшість шарів)
- 1K-10K: Помірна (Dropout 0.3, fine-tune останні кілька шарів)
- > 10K: Легка (Dropout 0.2 або ні, fine-tune всю мережу)
Pretrained ваги вже містять узагальнені ознаки, тому overfit менший.
# Conv blocks
Conv → BatchNorm → ReLU → (Pool)
# Classifier
Flatten → Linear → BatchNorm → ReLU → Dropout(0.3) → Linear
# Optimizer
SGD(lr=0.1, momentum=0.9, weight_decay=5e-4)
# Data
RandomHorizontalFlip() + RandomCrop(padding=4) + Normalize()
У наступній статті ми розглянемо проєктування глибоких мереж — як будувати архітектури з 20+ шарами за допомогою Residual Connections та інших архітектурних патернів.
Batch Normalization — прорив у глибокому навчанні
Internal Covariate Shift, математика Batch Normalization, переваги та обмеження, train/eval режими, практична реалізація у PyTorch та експериментальне порівняння з базовою архітектурою.
Проєктування глибоких мереж — від теорії до практики
Проблема degradation у дуже глибоких мережах, ідея Skip Connections, реалізація Residual Blocks, побудова повноцінної ResNet-подібної архітектури, best practices та порівняльний експеримент.