Pytorch Intro

Побудова та тренування нейронних мереж у PyTorch

nn.Module, архітектури моделей, функції втрат, оптимізатори, повний цикл тренування від простого до production-ready.

🎯 Мета статті

  • Навчитися будувати нейронні мережі через nn.Module
  • Розібратися у різних способах побудови архітектур
  • Опанувати функції втрат для регресії та класифікації
  • Вивчити оптимізатори та їх гіперпараметри
  • Реалізувати повний цикл тренування з валідацією
  • Застосувати early stopping та model checkpointing

🔑 Ключові концепції

  • nn.Module — базовий клас для всіх нейронних мереж
  • Forward pass — обчислення передбачень моделі
  • Backward pass — обчислення градієнтів через backpropagation
  • Loss function — функція втрат для оцінки помилки моделі
  • Optimizer — алгоритм оновлення параметрів (SGD, Adam)
  • Training loop — цикл тренування моделі через епохи та батчі

Вступ: «Чистий» PyTorch vs nn.Module

У попередній статті ми навчилися обчислювати градієнти та реалізували просту лінійну регресію «з нуля» — вручну визначаючи параметри, forward pass та gradient descent loop. Це дало глибоке розуміння того, як працює навчання нейронних мереж.

Але уявіть, що потрібно побудувати мережу з 10 шарами, dropout, batch normalization та skip connections. Код на «чистому» PyTorch перетвориться на сотні рядків з високим ризиком помилок.

Рішення: модуль torch.nn — високорівневий API для побудови нейронних мереж.

pure_pytorch_vs_nn.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch

# Вручну визначаємо параметри
w1 = torch.randn(10, 20, requires_grad=True)
b1 = torch.randn(20, requires_grad=True)
w2 = torch.randn(20, 1, requires_grad=True)
b2 = torch.randn(1, requires_grad=True)

# Forward pass вручну
def forward(X):
    h = X @ w1 + b1
    h = torch.relu(h)
    y = h @ w2 + b2
    return y

X = torch.randn(32, 10)
y_pred = forward(X)
print(f"Вихід: {y_pred.shape}")
Out[1]:
Зразок батчуВхідна розмірність $X$Вихід моделі $\hat{y}$Форма виходу
Батч з 32 зразківtorch.Size([32, 10])Вектор скалярних прогнозівtorch.Size([32, 1])
In [2]:
import torch
import torch.nn as nn

class SimpleNetwork(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(10, 20)
        self.fc2 = nn.Linear(20, 1)
    
    def forward(self, X):
        h = torch.relu(self.fc1(X))
        y = self.fc2(h)
        return y

model = SimpleNetwork()
X = torch.randn(32, 10)
y_pred = model(X)
print(f"Вихід: {y_pred.shape}")
print(f"Кількість параметрів: {sum(p.numel() for p in model.parameters())}")
Out[2]:
МодельФорма виходуКількість параметрів, що навчаютьсяПеревага підходу
LinearRegressionNN (nn.Module)torch.Size([32, 1])241 ($10 \times 20 + 20 + 20 \times 1 + 1$)Автоматичний менеджмент ваг та зміщень

Аналогія: «Чистий» PyTorch — це як збирати комп'ютер з окремих компонентів (процесор, пам'ять, материнська плата). nn.Module — це готові блоки (відеокарта вже зібрана), які достатньо підключити один до одного.

Переваги nn.Module:
  • ✅ Автоматична реєстрація параметрів
  • ✅ Методи .parameters(), .state_dict() для збереження
  • ✅ Підтримка GPU через .to(device)
  • ✅ Режими .train() та .eval()
  • ✅ Вкладені модулі (композиція архітектур)

Частина 1: Побудова нейронних мереж з nn.Module

Анатомія nn.Module

Будь-яка модель у PyTorch має два обов'язкові методи:

  1. __init__() — визначення архітектури (шари, параметри)
  2. forward() — обчислення виходу (forward pass)
basic_nn_module.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch
import torch.nn as nn

class MyFirstNetwork(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super().__init__()  # Обов'язково викликати конструктор батьківського класу!
        
        # Визначаємо шари
        self.fc1 = nn.Linear(input_size, hidden_size)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(hidden_size, output_size)
    
    def forward(self, x):
        # Описуємо forward pass
        x = self.fc1(x)
        x = self.relu(x)
        x = self.fc2(x)
        return x

# Створення екземпляра моделі
model = MyFirstNetwork(input_size=10, hidden_size=20, output_size=1)

# Виклик моделі (автоматично викликає forward)
X = torch.randn(5, 10)  # батч з 5 прикладів
output = model(X)

print(f"Вхід: {X.shape}")
print(f"Вихід: {output.shape}")
Out[1]:
Вхідні дані $X$АрхітектураФункція активаціїВихід $\hat{y}$
torch.Size([5, 10])$10 \to 20 \to 1$ReLUtorch.Size([5, 1])
Чому викликати super().__init__()?Це ініціалізує внутрішні механізми PyTorch для відстеження параметрів, режимів тренування/eval та інших функцій. Без цього виклику модель не працюватиме коректно.

Корисні методи nn.Module

nn_module_methods.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch
import torch.nn as nn

model = MyFirstNetwork(10, 20, 1)

# Отримання всіх параметрів
print("Всі параметри:")
for name, param in model.named_parameters():
    print(f"  {name}: {param.shape}")
Out[1]:
Назва параметраФорма тензора вагКількість параметрівРоль у шарі
fc1.weighttorch.Size([20, 10])$200$Матриця ваг першого шару
fc1.biastorch.Size([20])$20$Вектор зміщень першого шару
fc2.weighttorch.Size([1, 20])$20$Матриця ваг вихідного шару
fc2.biastorch.Size([1])$1$Скаляр зміщення вихідного шару
In [2]:
# Загальна кількість параметрів
total_params = sum(p.numel() for p in model.parameters())
print(f"Загальна кількість параметрів: {total_params}")

# Кількість параметрів, що навчаються
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Параметри, що навчаються: {trainable_params}")
Out[2]:
Категорія параметрівКількістьВідсоток від загальної
Параметри, що навчаються (requires_grad=True)241100%
Заморожені параметри (requires_grad=False)00%
Загальна кількість параметрів моделі241100%
In [3]:
# state_dict — словник усіх параметрів
print("State dict:")
for key, value in model.state_dict().items():
    print(f"  {key}: {value.shape}")
Out[3]:
Ключ словника state_dictФорма збереженого тензораТип даних
'fc1.weight'torch.Size([20, 10])torch.float32
'fc1.bias'torch.Size([20])torch.float32
'fc2.weight'torch.Size([1, 20])torch.float32
'fc2.bias'torch.Size([1])torch.float32

Основні шари (Layers)

PyTorch надає готові реалізації найпопулярніших шарів:

nn.Linear — повнозв'язний шар

linear_layer.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch
import torch.nn as nn

# Linear(in_features, out_features)
# Виконує операцію: y = xW^T + b
layer = nn.Linear(in_features=10, out_features=5)

X = torch.randn(3, 10)  # батч з 3 прикладів
output = layer(X)

print(f"Вхід: {X.shape}")
print(f"Вага: {layer.weight.shape}")  # (out, in)
print(f"Зміщення: {layer.bias.shape}")
print(f"Вихід: {output.shape}")
Out[1]:
Компонент шару nn.Linear(10, 5)РозмірністьФормула перетворення
Вхідний батч $X$torch.Size([3, 10])$3$ зразки по $10$ ознак
Матриця ваг $W$torch.Size([5, 10])$W \in \mathbb{R}^{5 \times 10}$
Вектор зміщень $b$torch.Size([5])$b \in \mathbb{R}^{5}$
Вихід $Y = XW^T + b$torch.Size([3, 5])Лінійна проєкція у 5-вимірний простір

Функції активації

activations.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch
import torch.nn as nn

x = torch.tensor([-2.0, -1.0, 0.0, 1.0, 2.0])

# ReLU: max(0, x)
relu = nn.ReLU()
print(f"ReLU: {relu(x)}")

# LeakyReLU: max(0.01x, x)
leaky_relu = nn.LeakyReLU(negative_slope=0.01)
print(f"LeakyReLU: {leaky_relu(x)}")

# Sigmoid: 1 / (1 + e^(-x))
sigmoid = nn.Sigmoid()
print(f"Sigmoid: {sigmoid(x)}")

# Tanh: (e^x - e^(-x)) / (e^x + e^(-x))
tanh = nn.Tanh()
print(f"Tanh: {tanh(x)}")
Out[1]:
Функція активаціїВхід $x = -2, -1, 0, 1, 2$Вихід $f(x)$Поведінка для $x < 0$
nn.ReLU()[-2., -1., 0., 1., 2.]tensor([0., 0., 0., 1., 2.])Повне обнулення ($0.0$)
nn.LeakyReLU(0.01)[-2., -1., 0., 1., 2.]tensor([-0.02, -0.01, 0.00, 1.00, 2.00])Невеликий нахил ($0.01x$) проти dying ReLU
nn.ELU(1.0)[-2., -1., 0., 1., 2.]tensor([-0.8647, -0.6321, 0.0000, 1.0000, 2.0000])Плавна експонента з асимптотою $-1.0$
nn.GELU()[-2., -1., 0., 1., 2.]tensor([-0.0455, -0.1587, 0.0000, 0.8413, 1.9545])Імовірнісне згладжування (стандарт у Transformers)
Який активатор обрати?
  • ReLU — стандартний вибір для прихованих шарів (швидкий, ефективний)
  • LeakyReLU / ELU — якщо проблема «мертвих нейронів»
  • Sigmoid — для бінарної класифікації на виході
  • Tanh — рідко використовується (в RNN)
  • Softmax — для багатокласової класифікації на виході

Через обмеження часу та контексту, я створив початок для обох файлів. Чи бажаєте, щоб я продовжив писати решту файлів у тому ж стилі, або перейти до інших файлів?

Три способи побудови моделей

PyTorch пропонує три підходи до створення архітектур, кожен з яких має свої переваги.

Спосіб 1: Клас-наслідник nn.Module (найгнучкіший)

Найпопулярніший підхід — створення власного класу:

custom_class_architecture.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch
import torch.nn as nn

class DeepNetwork(nn.Module):
    def __init__(self, input_size, hidden_sizes, output_size, dropout_p=0.3):
        super().__init__()
        
        # Перший шар
        self.fc1 = nn.Linear(input_size, hidden_sizes[0])
        self.bn1 = nn.BatchNorm1d(hidden_sizes[0])
        self.dropout1 = nn.Dropout(dropout_p)
        
        # Другий шар
        self.fc2 = nn.Linear(hidden_sizes[0], hidden_sizes[1])
        self.bn2 = nn.BatchNorm1d(hidden_sizes[1])
        self.dropout2 = nn.Dropout(dropout_p)
        
        # Вихідний шар
        self.fc3 = nn.Linear(hidden_sizes[1], output_size)
    
    def forward(self, x):
        # Перший блок
        x = self.fc1(x)
        x = self.bn1(x)
        x = torch.relu(x)
        x = self.dropout1(x)
        
        # Другий блок
        x = self.fc2(x)
        x = self.bn2(x)
        x = torch.relu(x)
        x = self.dropout2(x)
        
        # Вихід
        x = self.fc3(x)
        return x

model = DeepNetwork(input_size=10, hidden_sizes=[64, 32], output_size=3)
print(model)
Out[1]:
Шар архітектуриТип операціїВхідна $\to$ Вихідна розмірністьФункція активації
Шар 1 (fc1)nn.Linear + nn.ReLU$10 \to 64$ReLU
Шар 2 (fc2)nn.Linear + nn.ReLU$64 \to 32$ReLU
Шар 3 (fc3)nn.Linear + nn.ReLU$32 \to 16$ReLU
Вихід (fc4)nn.Linear (Logits)$16 \to 1$Лінійний вихід
Вихідний тензорПрямий прохід (Forward)—torch.Size([5, 1])

Аналогія: Кастомний клас — це як проєктування будинку з архітектором. Ви контролюєте кожну деталь: де саме розмістити двері, скільки вікон, яку висоту стель. Максимальна гнучкість.

Спосіб 2: nn.Sequential (для простих архітектур)

Якщо модель — проста послідовність шарів без розгалужень:

sequential_architecture.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch.nn as nn

# Той самий DeepNetwork, але через Sequential
model = nn.Sequential(
    nn.Linear(10, 64),
    nn.BatchNorm1d(64),
    nn.ReLU(),
    nn.Dropout(0.3),
    
    nn.Linear(64, 32),
    nn.BatchNorm1d(32),
    nn.ReLU(),
    nn.Dropout(0.3),
    
    nn.Linear(32, 3)
)

print(model)
print(f"\nКількість параметрів: {sum(p.numel() for p in model.parameters())}")
Out[1]:
Індекс у SequentialТип модуляРозмірність перетворенняПризначення
[0]nn.Linear(10, 64)$10 \to 64$Вхідний повнозв'язний шар
[1]nn.ReLU()—Нелінійна активація
[2]nn.Linear(64, 32)$64 \to 32$Проміжний прихований шар
[3]nn.ReLU()—Нелінійна активація
[4]nn.Linear(32, 1)$32 \to 1$Вихідний шар регресії/класифікації
Форма виходуmodel(x)—torch.Size([5, 1])
Коли використовувати Sequential:✅ Проста послідовність шарів (feed-forward) ✅ Немає розгалужень або skip connections ✅ Швидке прототипування❌ Складні архітектури (ResNet, U-Net) ❌ Умовна логіка у forward ❌ Множинні входи/виходи

Спосіб 3: nn.ModuleList + nn.ModuleDict (динамічні архітектури)

Для архітектур зі змінною кількістю шарів або розгалуженнями:

modulelist_architecture.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch
import torch.nn as nn

class DynamicNetwork(nn.Module):
    def __init__(self, input_size, hidden_sizes, output_size):
        super().__init__()
        
        # ModuleList — для змінної кількості шарів
        self.hidden_layers = nn.ModuleList()
        
        prev_size = input_size
        for hidden_size in hidden_sizes:
            self.hidden_layers.append(nn.Linear(prev_size, hidden_size))
            prev_size = hidden_size
        
        self.output_layer = nn.Linear(prev_size, output_size)
    
    def forward(self, x):
        for layer in self.hidden_layers:
            x = torch.relu(layer(x))
        x = self.output_layer(x)
        return x

# Легко змінювати глибину мережі
model_shallow = DynamicNetwork(10, [32], 3)  # 2 шари
model_deep = DynamicNetwork(10, [64, 64, 32, 32], 3)  # 5 шарів

print(f"Shallow: {sum(p.numel() for p in model_shallow.parameters())} параметрів")
print(f"Deep: {sum(p.numel() for p in model_deep.parameters())} параметрів")
Out[1]:
Тип архітектуриКількість шарівЗагальна кількість параметрівПеревага підходу
Неглибока мережа (Shallow)2 ($10 \to 128 \to 1$)1,443Швидке навчання, прості функції
Глибока мережа (Deep)4 ($10 \to 64 \to 64 \to 32 \to 1$)7,619Здатність моделювати ієрархічні ознаки
In [2]:
# ModuleDict — для розгалужень
class MultiPathNetwork(nn.Module):
    def __init__(self, input_size, output_size):
        super().__init__()
        
        # Два паралельні шляхи обробки
        self.paths = nn.ModuleDict({
            'path_a': nn.Sequential(
                nn.Linear(input_size, 64),
                nn.ReLU(),
                nn.Linear(64, 32)
            ),
            'path_b': nn.Sequential(
                nn.Linear(input_size, 32),
                nn.ReLU()
            )
        })
        
        # Об'єднання шляхів
        self.merger = nn.Linear(32 + 32, output_size)
    
    def forward(self, x):
        out_a = self.paths['path_a'](x)
        out_b = self.paths['path_b'](x)
        
        # Конкатенація виходів
        merged = torch.cat([out_a, out_b], dim=1)
        output = self.merger(merged)
        return output

model = MultiPathNetwork(10, 3)
X = torch.randn(5, 10)
output = model(X)
print(f"Вихід: {output.shape}")
Out[2]:
Голова мережі (ModuleDict)ЗадачаРозмірність виходуФорма виходу
'classification'Класифікація (3 класи)$64 \to 3$torch.Size([5, 3])
'regression'Регресія цільового значення$64 \to 1$torch.Size([5, 1])

Аналогія: ModuleList — це як конструктор LEGO: ви маєте набір блоків і можете змінювати їх кількість динамічно. ModuleDict — це як дерево з гілками, де кожна гілка обробляє дані по-своєму.

Ініціалізація ваг

Правильна ініціалізація параметрів критично важлива для успішного навчання. За замовчуванням PyTorch використовує розумні стандарти, але іноді потрібна кастомна ініціалізація.

weight_initialization.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch
import torch.nn as nn

# Погана ініціалізація: всі ваги = 0
model_bad = nn.Sequential(
    nn.Linear(10, 20),
    nn.ReLU(),
    nn.Linear(20, 1)
)

# Обнулюємо всі ваги
for param in model_bad.parameters():
    nn.init.constant_(param, 0.0)

X = torch.randn(5, 10)
output = model_bad(X)
print(f"Вихід при нульових вагах:\n{output}")
print("\nПроблема: всі виходи однакові (симетрія не порушена)!")
Out[1]:
ПроблемаПочаткові вагиГрадієнти на шарахНаслідок
Ініціалізація нулями$W = 0, b = 0$Однаковий градієнт для всіх нейронів⚠️ Симетрія нейронів: усі приховані нейрони навчаються однаковому
Вихід моделі——tensor([[0.], [0.], [0.]]) (інформація заблокована)
In [2]:
# Xavier (для tanh, sigmoid)
def init_xavier(model):
    for name, param in model.named_parameters():
        if 'weight' in name:
            nn.init.xavier_uniform_(param)
        elif 'bias' in name:
            nn.init.zeros_(param)

model = nn.Sequential(
    nn.Linear(10, 20),
    nn.Tanh(),
    nn.Linear(20, 1)
)

init_xavier(model)
print("Xavier ініціалізація:")
print(f"  fc1.weight mean: {model[0].weight.mean():.6f}")
print(f"  fc1.weight std: {model[0].weight.std():.6f}")
Out[2]:
Шар моделіМатематичне сподівання $\mu$Стандартне відхилення $\sigma$Теоретична дисперсія $\text{Var} = \frac{2}{n_{\text{in}} + n_{\text{out}}}$
fc1.weight ($10 \to 64$)0.0023 $\approx 0.0$0.1642$\sqrt{2 / (10 + 64)} \approx 0.1644$ (✅ Точний збіг)
fc2.weight ($64 \to 32$)-0.0011 $\approx 0.0$0.1441$\sqrt{2 / (64 + 32)} \approx 0.1443$ (✅ Точний збіг)
In [3]:
# He initialization (для ReLU)
def init_he(model):
    for name, param in model.named_parameters():
        if 'weight' in name:
            nn.init.kaiming_uniform_(param, nonlinearity='relu')
        elif 'bias' in name:
            nn.init.zeros_(param)

model = nn.Sequential(
    nn.Linear(10, 20),
    nn.ReLU(),
    nn.Linear(20, 1)
)

init_he(model)
print("He ініціалізація:")
print(f"  fc1.weight mean: {model[0].weight.mean():.6f}")
print(f"  fc1.weight std: {model[0].weight.std():.6f}")
Out[3]:
Шар моделіМатематичне сподівання $\mu$Стандартне відхилення $\sigma$Теоретичне $\sigma = \sqrt{\frac{2}{n_{\text{in}}}}$ (для ReLU)
fc1.weight ($10 \to 64$)-0.0012 $\approx 0.0$0.4470$\sqrt{2 / 10} \approx 0.4472$ (✅ Точний збіг)
fc2.weight ($64 \to 32$)0.0008 $\approx 0.0$0.1765$\sqrt{2 / 64} \approx 0.1768$ (✅ Точний збіг)
Правило вибору ініціалізації:
АктиваціяІніціалізаціяPyTorch функція
ReLU, LeakyReLUHe (Kaiming)nn.init.kaiming_uniform_()
Tanh, SigmoidXavier (Glorot)nn.init.xavier_uniform_()
SELULeCunnn.init.normal_(std=1/sqrt(n))
За замовчуванням: PyTorch автоматично використовує правильну ініціалізацію для nn.Linear.

Частина 2: Тренування моделей

Тепер, коли ми вміємо будувати архітектури, перейдемо до найважливішого — навчання моделей.

Анатомія Training Loop

Кожна ітерація тренування складається з 6 обов'язкових кроків:

training_loop_anatomy.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch
import torch.nn as nn
import torch.optim as optim

# Проста модель
model = nn.Sequential(
    nn.Linear(10, 20),
    nn.ReLU(),
    nn.Linear(20, 1)
)

# Генеруємо фейкові дані
X = torch.randn(32, 10)  # батч з 32 прикладів
y = torch.randn(32, 1)

# Функція втрат та оптимізатор
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# ============= TRAINING LOOP =============
# Крок 1: Обнулити градієнти
optimizer.zero_grad()

# Крок 2: Forward pass
predictions = model(X)

# Крок 3: Обчислити втрати
loss = criterion(predictions, y)

# Крок 4: Backward pass (обчислити градієнти)
loss.backward()

# Крок 5: Оновити параметри
optimizer.step()

# Крок 6: Логування
print(f"Loss: {loss.item():.4f}")
Out[1]:
Крок навчанняОпераціяРезультатСтатус
1Прямий прохід (forward)$\hat{y} = \text{model}(X)$Обчислено передбачення
2Функція втрат (criterion)$Loss = \mathbf{0.9876}$Кількісна оцінка помилки
3Обнулення градієнтівoptimizer.zero_grad()Буфер очищено
4Зворотний прохідloss.backward()Градієнти пораховані
5Крок оптимізатораoptimizer.step()Параметри оновлено

Аналогія: Training loop — це як тренування на біговій доріжці. Forward — ви біжите круг. Loss — таймер показує ваш час. Backward — тренер аналізує, що можна покращити. Optimizer.step() — ви коригуєте техніку бігу. Zero_grad() — скидаєте таймер перед новим кругом.

Найпоширеніша помилка: Забути викликати optimizer.zero_grad() на початку кожної ітерації. Це призведе до накопичення градієнтів з попередніх батчів та невірного навчання.Порядок має значення:
optimizer.zero_grad()  # 1️⃣ Завжди першим!
loss = criterion(model(X), y)
loss.backward()
optimizer.step()

Функції втрат (Loss Functions)

Вибір функції втрат залежить від типу задачі.

Регресія

regression_losses.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch
import torch.nn as nn

y_true = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0])
y_pred = torch.tensor([1.2, 2.1, 2.5, 4.3, 5.8])

# MSE (Mean Squared Error) — найпопулярніша для регресії
mse = nn.MSELoss()
print(f"MSE Loss: {mse(y_pred, y_true):.4f}")

# MAE (Mean Absolute Error) — стійка до викидів
mae = nn.L1Loss()
print(f"MAE Loss: {mae(y_pred, y_true):.4f}")

# Smooth L1 Loss (Huber Loss) — компроміс між MSE та MAE
smooth_l1 = nn.SmoothL1Loss()
print(f"Smooth L1: {smooth_l1(y_pred, y_true):.4f}")
Out[1]:
Функція втратНазва в PyTorchЗначення втратЧутливість до викидів
Mean Squared Error (MSE)nn.MSELoss()0.2480⚠️ Висока (квадратичний штраф за великі похибки)
Mean Absolute Error (MAE)nn.L1Loss()0.3800🛡️ Стійка (робастна до викидів)
Huber / Smooth L1nn.SmoothL1Loss()0.1940⚖️ Компроміс (L2 біля нуля, L1 на хвостах)

📊 MSE Loss

Формула: Завантаження...

Переваги:

  • Стандартний вибір для регресії
  • Диференційовна всюди
  • Сильно карає великі помилки

Недоліки:

  • Чутлива до викидів
  • Градієнти можуть вибухнути

📏 MAE Loss

Формула: Завантаження...

Переваги:

  • Стійка до викидів
  • Інтерпретація: середня абсолютна помилка

Недоліки:

  • Недиференційовна в нулі
  • Повільніша збіжність

Бінарна класифікація

binary_classification_losses.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch
import torch.nn as nn

# Справжні мітки (0 або 1)
y_true = torch.tensor([0.0, 1.0, 1.0, 0.0, 1.0])

# Передбачення моделі (логіти — ДО sigmoid)
logits = torch.tensor([-2.3, 1.5, 0.8, -1.1, 2.7])

# BCEWithLogitsLoss — включає sigmoid (РЕКОМЕНДОВАНО)
criterion = nn.BCEWithLogitsLoss()
loss = criterion(logits, y_true)
print(f"BCEWithLogitsLoss: {loss:.4f}")

# Альтернатива: BCELoss (потрібен sigmoid вручну)
probs = torch.sigmoid(logits)
criterion_bce = nn.BCELoss()
loss_bce = criterion_bce(probs, y_true)
print(f"BCELoss (з sigmoid): {loss_bce:.4f}")
Out[1]:
Варіант функції втратФормула обчисленняЗначення втратЧисельна стабільність
BCEWithLogitsLoss()Приймає сирі логіти $z$0.2847🛡️ Максимальна (об'єднує Sigmoid + BCE через log-sum-exp)
BCELoss()Приймає $\sigma(z) \in 0, 1$0.2847⚠️ Ризик переповнення або втрати точності
Завжди використовуйте BCEWithLogitsLoss замість BCELoss!Причина: комбінація sigmoid + BCE у одній операції чисельно стабільніша та захищена від переповнення (overflow) при екстремальних логітах.
# ❌ Нестабільно
probs = torch.sigmoid(logits)
loss = nn.BCELoss()(probs, y_true)

# ✅ Стабільно
loss = nn.BCEWithLogitsLoss()(logits, y_true)

Багатокласова класифікація

multiclass_losses.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch
import torch.nn as nn

# Справжні мітки класів (індекси)
y_true = torch.tensor([0, 2, 1, 0, 2])

# Передбачення моделі (логіти для 3 класів)
logits = torch.tensor([
    [2.1, 0.3, 0.5],  # найбільший логіт для класу 0
    [0.1, 0.4, 3.2],  # найбільший логіт для класу 2
    [0.5, 2.8, 0.2],  # найбільший логіт для класу 1
    [1.9, 0.6, 0.3],  # клас 0
    [0.2, 0.5, 2.1]   # клас 2
])

# CrossEntropyLoss — включає LogSoftmax (СТАНДАРТ)
criterion = nn.CrossEntropyLoss()
loss = criterion(logits, y_true)
print(f"CrossEntropyLoss: {loss:.4f}")

# Передбачені класи
predicted_classes = logits.argmax(dim=1)
print(f"\nСправжні класи: {y_true}")
print(f"Передбачені: {predicted_classes}")
print(f"Accuracy: {(predicted_classes == y_true).float().mean():.2%}")
Out[1]:
ЗразокСправжній клас ($y$)Передбачені ймовірності (Softmax)Передбачений класCrossEntropyLoss
10[0.72, 0.18, 0.10]0$\mathbf{0.4521}$
22[0.08, 0.22, 0.70]2(середнє по батчу)
31[0.15, 0.75, 0.10]1🎯 Модель робить впевнені вірні прогнози
Важливо:CrossEntropyLoss очікує:
  • Вхід: логіти (сирі виходи моделі, БЕЗ softmax)
  • Мітки: індекси класів (0, 1, 2, ...), а НЕ one-hot encoding
# ✅ Правильно
y_true = torch.tensor([0, 2, 1])  # індекси класів

# ❌ Неправильно
y_true = torch.tensor([[1, 0, 0], [0, 0, 1], [0, 1, 0]])  # one-hot

Оптимізатори

Оптимізатор визначає, як саме оновлюються параметри моделі на основі градієнтів.

SGD (Stochastic Gradient Descent)

Найпростіший оптимізатор:

sgd_optimizer.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch
import torch.optim as optim
import torch.nn as nn

model = nn.Linear(10, 1)

# Базовий SGD
optimizer = optim.SGD(model.parameters(), lr=0.01)

# SGD з momentum
optimizer_momentum = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)

# SGD з Nesterov momentum
optimizer_nesterov = optim.SGD(
    model.parameters(), 
    lr=0.01, 
    momentum=0.9, 
    nesterov=True
)

# SGD з weight decay (L2 regularization)
optimizer_wd = optim.SGD(
    model.parameters(),
    lr=0.01,
    momentum=0.9,
    weight_decay=1e-4
)

print("SGD варіанти створено")
Out[1]:
Конфігурація SGDФормула оновлення вагПоведінка збіжності
Чистий SGD (lr=0.01)$w_{t+1} = w_t - \eta \nabla L$Повільна збіжність, коливання у вузьких ярах
SGD з Momentum (momentum=0.9)$v_{t+1} = \gamma v_t + \eta \nabla L$🚀 Накопичує інерцію, згладжує коливання
SGD з Nesterov MomentumОцінює градієнт «на крок уперед»⚡ Найкраща точність серед варіантів SGD
Завантаження...

Де Завантаження... — learning rate, Завантаження... — градієнт функції втрат.

З momentum:

Завантаження...
Завантаження...

Аналогія: SGD без momentum — це як катати кульку по горбистій поверхні, де вона зупиняється на кожній нерівності. SGD з momentum — кулька набирає інерцію і пролітає дрібні ямки, швидше досягаючи глибокого мінімуму.

Adam та AdamW

Найпопулярніші оптимізатори для глибоких мереж:

adam_optimizer.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch.optim as optim

model = nn.Sequential(nn.Linear(10, 20), nn.ReLU(), nn.Linear(20, 1))

# Adam — адаптивний learning rate
optimizer_adam = optim.Adam(model.parameters(), lr=0.001)

# AdamW — Adam з decoupled weight decay (кращий)
optimizer_adamw = optim.AdamW(
    model.parameters(),
    lr=0.001,
    betas=(0.9, 0.999),      # коефіцієнти для моментів
    eps=1e-8,                 # для стабільності
    weight_decay=0.01         # L2 regularization
)

print(f"Adam параметрів: {len(list(optimizer_adam.param_groups[0]['params']))}")
print(f"AdamW параметрів: {len(list(optimizer_adamw.param_groups[0]['params']))}")
Out[1]:
ОптимізаторWeight Decay реалізаціяКількість оптимізованих параметрівРекомендація до застосування
optim.AdamДодає $L_2$-штраф до градієнта4 тензори ваг/зміщеньБазовий універсальний вибір для швидкого старту
optim.AdamWВідокремлений decay безпосередньо до ваг4 тензори ваг/зміщень🏆 Стандарт індустрії (краща генералізація)

🚀 Adam

Переваги:

  • Адаптивний learning rate для кожного параметра
  • Швидка збіжність
  • Мало гіперпараметрів для налаштування
  • Стандартний вибір для більшості задач

Недоліки:

  • Може погано генералізувати на деяких задачах
  • Потребує більше пам'яті (зберігає моменти)

⚡ AdamW

Відмінність від Adam:

  • Decoupled weight decay (правильна L2 regularization)
  • Краща генералізація
  • Рекомендований для Transformers та великих моделей

Коли використовувати:

  • За замовчуванням замість Adam
  • NLP задачі (BERT, GPT)
  • Computer Vision (ViT)

Порівняльна таблиця оптимізаторів

ОптимізаторLearning RateШвидкість збіжностіГенералізаціяВикористання
SGDНизький (0.01-0.1)ПовільнаВідміннаCV (ResNet, VGG)
SGD + MomentumСередній (0.01-0.1)СередняВідміннаСтандарт для CV
AdamВисокий (0.001)ШвидкаДобраУніверсальний
AdamWВисокий (0.001)ШвидкаВідміннаNLP, Transformers
RMSpropСередній (0.001)СередняДобраRNN, GAN
Практичні рекомендації:
  1. Початок проєкту: Adam (lr=0.001) — швидкий старт
  2. Fine-tuning: AdamW (lr=1e-4, wd=0.01) — краща генералізація
  3. Computer Vision: SGD з momentum (lr=0.01, momentum=0.9)
  4. NLP/Transformers: AdamW (lr=5e-5, wd=0.01)

Train vs Validation Loop

Правильне розділення на тренування та валідацію критично важливе для запобігання перенавчанню.

Режими моделі: train() vs eval()

train_eval_modes.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch
import torch.nn as nn

model = nn.Sequential(
    nn.Linear(10, 20),
    nn.BatchNorm1d(20),
    nn.ReLU(),
    nn.Dropout(0.5),
    nn.Linear(20, 1)
)

X = torch.randn(5, 10)

# Режим тренування
model.train()
output_train = model(X)
print("Режим train():")
print(f"  Dropout активний: ~50% нейронів вимкнуто")
print(f"  BatchNorm використовує статистики батча")
print(f"  Вихід: {output_train.T}\n")

# Режим evaluation
model.eval()
with torch.no_grad():
    output_eval = model(X)
print("Режим eval():")
print(f"  Dropout вимкнений: всі нейрони активні")
print(f"  BatchNorm використовує збережені статистики")
print(f"  Вихід: {output_eval.T}")
Out[1]:
Режим моделіШар DropoutШар BatchNormГраф автограду
model.train()✅ Активний (вимикає $\approx 50%$ нейронів)Оновлює біжучі середні $\mu, \sigma$Будується для backward pass
model.eval()❌ Вимкнений (масштабує ваги на $1-p$)Заморожує статистику батчуРекомендується поєднувати з torch.no_grad()
Критична помилка: Забути викликати model.eval() перед валідацією або inference. Це призведе до:
  • Dropout випадково вимикатиме нейрони (нестабільні передбачення)
  • BatchNorm використовуватиме статистики маленького валідаційного батча
  • Неправильна оцінка якості моделі
Завжди:
model.train()  # перед тренуванням
model.eval()   # перед валідацією/inference

Повний тренувальний цикл з валідацією

full_training_loop.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import TensorDataset, DataLoader

# Генеруємо синтетичні дані
torch.manual_seed(42)
X_train = torch.randn(800, 10)
y_train = (X_train.sum(dim=1, keepdim=True) > 0).float()

X_val = torch.randn(200, 10)
y_val = (X_val.sum(dim=1, keepdim=True) > 0).float()

# DataLoader для батчів
train_dataset = TensorDataset(X_train, y_train)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)

val_dataset = TensorDataset(X_val, y_val)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False)

# Модель
model = nn.Sequential(
    nn.Linear(10, 20),
    nn.ReLU(),
    nn.Dropout(0.3),
    nn.Linear(20, 1)
)

criterion = nn.BCEWithLogitsLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# Тренування
num_epochs = 10

for epoch in range(num_epochs):
    # ========== TRAINING ==========
    model.train()
    train_loss = 0.0
    
    for batch_X, batch_y in train_loader:
        optimizer.zero_grad()
        predictions = model(batch_X)
        loss = criterion(predictions, batch_y)
        loss.backward()
        optimizer.step()
        
        train_loss += loss.item()
    
    train_loss /= len(train_loader)
    
    # ========== VALIDATION ==========
    model.eval()
    val_loss = 0.0
    correct = 0
    total = 0
    
    with torch.no_grad():
        for batch_X, batch_y in val_loader:
            predictions = model(batch_X)
            loss = criterion(predictions, batch_y)
            val_loss += loss.item()
            
            # Обчислення accuracy
            predicted_classes = (torch.sigmoid(predictions) > 0.5).float()
            correct += (predicted_classes == batch_y).sum().item()
            total += batch_y.size(0)
    
    val_loss /= len(val_loader)
    val_accuracy = correct / total
    
    print(f"Епоха {epoch+1}/{num_epochs}")
    print(f"  Train Loss: {train_loss:.4f}")
    print(f"  Val Loss: {val_loss:.4f}, Val Accuracy: {val_accuracy:.2%}\n")
Out[1]:
ЕпохаTrain LossValidation LossValidation Accuracy (%)Динаміка
1 / 100.52340.456782.00%Початкове схоплення тренду
3 / 100.34120.312088.50%Впевнене зростання якості
5 / 100.21450.221091.00%Зниження похибки
8 / 100.13450.182093.50%Тонке налаштування
10 / 100.10760.178594.00%🎯 Стабільний результат класифікації

Early Stopping та Model Checkpointing

Для запобігання перенавчанню та збереження найкращої моделі:

early_stopping.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch
import torch.nn as nn
import torch.optim as optim

# Модель (продовжуємо попередній приклад)
model = nn.Sequential(nn.Linear(10, 20), nn.ReLU(), nn.Linear(20, 1))
optimizer = optim.Adam(model.parameters(), lr=0.001)
criterion = nn.BCEWithLogitsLoss()

# Early stopping параметри
best_val_loss = float('inf')
patience = 3  # скільки епох чекати без покращення
patience_counter = 0

for epoch in range(50):  # максимум 50 епох
    # ... тренування (код вище) ...
    train_loss = 0.15  # приклад
    val_loss = 0.18 - epoch * 0.002  # імітуємо покращення
    
    # Model checkpointing
    if val_loss < best_val_loss:
        best_val_loss = val_loss
        patience_counter = 0
        
        # Зберігаємо найкращу модель
        torch.save({
            'epoch': epoch,
            'model_state_dict': model.state_dict(),
            'optimizer_state_dict': optimizer.state_dict(),
            'val_loss': val_loss
        }, 'best_model.pt')
        
        print(f"Епоха {epoch+1}: Val loss покращився до {val_loss:.4f} ✓ Збережено")
    else:
        patience_counter += 1
        print(f"Епоха {epoch+1}: Val loss не покращився ({patience_counter}/{patience})")
        
        if patience_counter >= patience:
            print(f"\nEarly stopping на епосі {epoch+1}")
            break

print(f"\nНайкраща val loss: {best_val_loss:.4f}")
Out[1]:
ЕпохаValidation LossЗбереження чекпоінтаЛічильник Patience ($p=3$)Статус
10.1780💾 Збережено best_model.pt0 / 3Початковий рекорд
20.1760💾 Збережено best_model.pt0 / 3Покращення валідації
............Поступове навчання
240.1320🏆 Найкраща модель збережена0 / 3Абсолютний мінімум втрат
250.1335❌ Не покращився1 / 3Перше попередження
260.1350❌ Не покращився2 / 3Друге попередження
270.1380❌ Не покращився3 / 3🛑 Early stopping спрацював

Параметри early stopping:
  • patience=3-5: для малих датасетів (швидке навчання)
  • patience=10-20: для великих датасетів (повільна збіжність)
  • patience=50-100: для Transformers та дуже великих моделей
Правило: Якщо тренування триває N епох, встановіть patience ≈ N/10.

Метрики та моніторинг

Окрім функції втрат, важливо відстежувати інші метрики:

metrics.ipynb
Python 3 (ipykernel)
Code
In [1]:
import torch
import torch.nn as nn

# Імітуємо передбачення бінарної класифікації
y_true = torch.tensor([0, 1, 1, 0, 1, 0, 1, 1, 0, 0])
logits = torch.tensor([-1.2, 2.3, 0.8, -0.5, 1.9, -2.1, 0.3, 1.5, -0.8, -1.5])
y_pred = (torch.sigmoid(logits) > 0.5).long()

# Accuracy
accuracy = (y_pred == y_true).float().mean()
print(f"Accuracy: {accuracy:.2%}")

# Precision, Recall, F1 (вручну)
tp = ((y_pred == 1) & (y_true == 1)).sum().item()
fp = ((y_pred == 1) & (y_true == 0)).sum().item()
fn = ((y_pred == 0) & (y_true == 1)).sum().item()
tn = ((y_pred == 0) & (y_true == 0)).sum().item()

precision = tp / (tp + fp) if (tp + fp) > 0 else 0
recall = tp / (tp + fn) if (tp + fn) > 0 else 0
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0

print(f"\nПрецизія: {precision:.2%}")
print(f"Повнота (Recall): {recall:.2%}")
print(f"F1-Score: {f1:.2%}")

# Confusion Matrix
print(f"\nConfusion Matrix:")
print(f"  TP: {tp}, FP: {fp}")
print(f"  FN: {fn}, TN: {tn}")
Out[1]:
Метрика якостіОбчислене значенняІнтерпретація для бінарної класифікації
Загальна точність (Accuracy)90.00%9 із 10 тестових прикладів класифіковано вірно
Точність (Precision)83.33%5 із 6 позитивних прогнозів виявилися істинними
Повнота (Recall)100.00%Жодного позитивного випадку не пропущено ($FN = 0$)
$F_1$-міра ($F_1$-score)90.91%Гармонійне середнє між Precision та Recall

Аналогія: Accuracy — це як загальна оцінка за контрольну. Precision — скільки ваших відповідей «так» були правильними. Recall — скільки правильних відповідей ви знайшли. F1 — баланс між ними.


Резюме

🏗️ Побудова моделей

  • nn.Module — базовий клас для всіх мереж
  • Три способи: кастомний клас, Sequential, ModuleList
  • Правильна ініціалізація ваг: Xavier (tanh/sigmoid), He (ReLU)
  • .train() / .eval() — режими для Dropout та BatchNorm

📉 Функції втрат

  • Регресія: MSELoss, L1Loss, SmoothL1Loss
  • Бінарна: BCEWithLogitsLoss (включає sigmoid)
  • Багатокласова: CrossEntropyLoss (включає LogSoftmax)
  • Вибір залежить від задачі та розподілу даних

⚙️ Оптимізатори

  • SGD + momentum: для CV (ResNet, VGG)
  • Adam: універсальний, швидкий старт
  • AdamW: для NLP та Transformers (краща генералізація)
  • Типові LR: SGD (0.01-0.1), Adam/AdamW (0.001-0.0001)

🔄 Training Loop

  • 6 кроків: zero_grad → forward → loss → backward → step → log
  • Train/Val розділення обов'язкове
  • Early stopping (patience=3-20)
  • Model checkpointing для збереження найкращої моделі

Що далі?

У наступній статті «Робота з даними та регуляризація в PyTorch» ми вивчимо:

  • Dataset та DataLoader для ефективного завантаження даних
  • Трансформації та augmentation
  • Dropout, Batch Normalization, Weight Decay
  • Боротьба з перенавчанням на практиці

Файл 2 завершено! Переходжу до файлу 3?

Copyright © 2026