Можно всю жизнь пользоваться PyTorch и не понимать что внутри. Но один раз написать свой mini-framework на NumPy = понять, как работает backprop, и почему модели иногда «не учатся».
К концу урока ты:
У нас есть нейросеть с параметрами (веса w и biases b). У нас есть данные (X, y). Мы хотим, чтобы сеть на входе X выдавала y.
Самая простая для регрессии — MSE (Mean Squared Error):
L = (1/N) · Σ (y_pred - y_true)²
Чем больше ошибка — тем больше loss. Цель — минимизировать.
Для классификации — Cross-Entropy:
L = -Σ y_true · log(y_pred)
Loss — это функция от параметров: L(w₁, w₂, ..., wₙ, b₁, ...). Мы в точке этой функции (текущие значения параметров) и хотим попасть в минимум.
Градиент ∂L/∂wᵢ = в какую сторону loss растёт быстрее всего по wᵢ. Идём в противоположную сторону:
w_i ← w_i - learning_rate · ∂L/∂w_i
Это весь gradient descent в одной формуле.
Сеть из нескольких слоёв = композиция функций. Чтобы посчитать градиент loss по весам первого слоя, нужно «протащить» производную через все слои.
Chain rule из матанализа:
Если y = f(g(x)), то dy/dx = (df/dg) · (dg/dx)
Для сети из L слоёв: градиент loss по весу w в слое 1 = произведение производных всех слоёв 2..L и самого слоя 1.
Backpropagation = алгоритм эффективного вычисления этих градиентов, идя от выхода ко входу.
Реализуем 4 базовых модуля: Linear, ReLU, Sigmoid, MSE.
import numpy as np
class Linear:
def __init__(self, in_features, out_features):
# He init для ReLU
self.W = np.random.randn(in_features, out_features) * np.sqrt(2.0 / in_features)
self.b = np.zeros(out_features)
self.input = None
self.grad_W = None
self.grad_b = None
def forward(self, x):
self.input = x # запоминаем для backward
return x @ self.W + self.b
def backward(self, grad_output):
# grad по W: x.T @ grad_output
self.grad_W = self.input.T @ grad_output
self.grad_b = grad_output.sum(axis=0)
# grad для предыдущего слоя
return grad_output @ self.W.T
class ReLU:
def __init__(self):
self.mask = None
def forward(self, x):
self.mask = (x > 0).astype(float)
return x * self.mask
def backward(self, grad_output):
return grad_output * self.mask
class Sigmoid:
def __init__(self):
self.output = None
def forward(self, x):
self.output = 1 / (1 + np.exp(-x))
return self.output
def backward(self, grad_output):
return grad_output * self.output * (1 - self.output)
class MSE:
def forward(self, y_pred, y_true):
self.y_pred = y_pred
self.y_true = y_true
return ((y_pred - y_true) ** 2).mean()
def backward(self):
return 2 * (self.y_pred - self.y_true) / self.y_true.size
class Network:
def __init__(self, layers):
self.layers = layers
def forward(self, x):
for layer in self.layers:
x = layer.forward(x)
return x
def backward(self, grad):
for layer in reversed(self.layers):
grad = layer.backward(grad)
def step(self, lr):
for layer in self.layers:
if isinstance(layer, Linear):
layer.W -= lr * layer.grad_W
layer.b -= lr * layer.grad_b
# XOR data
X = np.array([[0,0],[0,1],[1,0],[1,1]], dtype=float)
y = np.array([[0],[1],[1],[0]], dtype=float)
net = Network([
Linear(2, 4),
ReLU(),
Linear(4, 1),
Sigmoid(),
])
loss_fn = MSE()
for epoch in range(2000):
y_pred = net.forward(X)
loss = loss_fn.forward(y_pred, y)
grad = loss_fn.backward()
net.backward(grad)
net.step(lr=0.1)
if epoch % 200 == 0:
print(f"Epoch {epoch}, loss: {loss:.4f}")
print("Predictions:", net.forward(X).round(2))
Запусти. Через 1000 эпох loss упадёт до ~0.001, predictions станут [[0],[1],[1],[0]]. Ты обучил нейросеть.
~80 строк кода. И это база любого современного фреймворка.
Прогон входа через сеть — получение предсказания.
Функция, которая измеряет ошибку. MSE, Cross-Entropy.
Вычисление градиентов loss по всем параметрам с применением chain rule.
Метод оптимизации: идёшь в сторону, противоположную градиенту.
Размер шага обновления весов. Один из главных гиперпараметров.
Один полный проход по всем данным.
Tanh (формула в Уроке 33).progress.md зафиксируй: какие гипер-параметры лучше всего сработали.1. Цикл обучения нейросети в 4 шагах?
Forward → Loss → Backward → Update. Повтор.
2. Что такое gradient descent?
Метод оптимизации: считаем градиент loss по параметру, идём в противоположную сторону.
3. Зачем chain rule в backprop?
Чтобы посчитать градиент loss по весам внутренних слоёв через производные внешних слоёв.
4. Почему нельзя инициализировать веса нулями?
Все нейроны будут учиться одному и тому же — нет смысла в нескольких нейронах.
5. Что значит «loss = NaN» в логах?
Численное переполнение. Обычно: слишком большой LR, или sigmoid с экстремальными значениями. Уменьши LR или нормализуй данные.