← AI Курс
Фаза 3. Практик·Урок 34

Урок 34. Свой framework для нейросетей на NumPy

Цель: построить мини-PyTorch с нуля и понять backprop изнутри Время: ~90 минут Источник: AI-For-Beginners/06 + Karpathy «Neural Networks Zero to Hero»

Зачем тебе этот урок

Можно всю жизнь пользоваться PyTorch и не понимать что внутри. Но один раз написать свой mini-framework на NumPy = понять, как работает backprop, и почему модели иногда «не учатся».

К концу урока ты:

1. Идея обучения за 30 секунд

У нас есть нейросеть с параметрами (веса w и biases b). У нас есть данные (X, y). Мы хотим, чтобы сеть на входе X выдавала y.

  1. Forward pass: прогоняем X через сеть, получаем prediction.
  2. Loss: считаем ошибку (насколько prediction отличается от y).
  3. Backward pass: считаем градиент loss по каждому параметру (как loss изменится, если чуть поменять параметр).
  4. Update: сдвигаем параметры в сторону уменьшения loss.
  5. Повтор пока loss не станет маленьким.

2. Loss function

Самая простая для регрессии — MSE (Mean Squared Error):

L = (1/N) · Σ (y_pred - y_true)²

Чем больше ошибка — тем больше loss. Цель — минимизировать.

Для классификации — Cross-Entropy:

L = -Σ y_true · log(y_pred)

3. Gradient descent на пальцах

Loss — это функция от параметров: L(w₁, w₂, ..., wₙ, b₁, ...). Мы в точке этой функции (текущие значения параметров) и хотим попасть в минимум.

Градиент ∂L/∂wᵢ = в какую сторону loss растёт быстрее всего по wᵢ. Идём в противоположную сторону:

w_i ← w_i - learning_rate · ∂L/∂w_i

Это весь gradient descent в одной формуле.

4. Chain rule — сердце backprop

Сеть из нескольких слоёв = композиция функций. Чтобы посчитать градиент loss по весам первого слоя, нужно «протащить» производную через все слои.

Chain rule из матанализа:

Если y = f(g(x)), то  dy/dx = (df/dg) · (dg/dx)

Для сети из L слоёв: градиент loss по весу w в слое 1 = произведение производных всех слоёв 2..L и самого слоя 1.

Backpropagation = алгоритм эффективного вычисления этих градиентов, идя от выхода ко входу.

5. Минимальный framework

Реализуем 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

6. Собираем сеть и обучаем XOR

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]]. Ты обучил нейросеть.

7. Что мы только что сделали

~80 строк кода. И это база любого современного фреймворка.

8. Что добавляют настоящие фреймворки

9. Распространённые ошибки при обучении

  • Loss не падает. Слишком большой learning rate (взрывается) или слишком маленький (не двигается). Попробуй 0.01, 0.001.
  • Loss = NaN. Произошло overflow — обычно из-за высокого LR или sigmoid с очень большими/маленькими z. Проверь масштаб данных.
  • Loss падает, точность не растёт. Метрика неправильно меряет (или есть утечка в данных).
  • Веса начинаются с нулей. Все нейроны учатся одному. Используй случайную инициализацию.
  • Vanishing gradients. Sigmoid в глубоких сетях: градиент стремится к нулю. Лечение — ReLU.

10. Глоссарий

Forward pass

Прогон входа через сеть — получение предсказания.

Loss function

Функция, которая измеряет ошибку. MSE, Cross-Entropy.

Backward pass / Backprop

Вычисление градиентов loss по всем параметрам с применением chain rule.

Gradient descent

Метод оптимизации: идёшь в сторону, противоположную градиенту.

Learning rate

Размер шага обновления весов. Один из главных гиперпараметров.

Epoch

Один полный проход по всем данным.

11. Практика (90 минут)

  1. Скопируй framework из урока в файл и запусти на XOR.
  2. Добавь свой слой: Tanh (формула в Уроке 33).
  3. Попробуй разные learning rate: 0.001, 0.01, 0.1, 1.0 — посмотри что происходит.
  4. Замени XOR на MNIST (можно через sklearn.datasets) — настоящий датасет.
  5. Сравни время обучения с PyTorch на той же задаче.
  6. В progress.md зафиксируй: какие гипер-параметры лучше всего сработали.

12. Проверь себя

1. Цикл обучения нейросети в 4 шагах?
Forward → Loss → Backward → Update. Повтор.

2. Что такое gradient descent?
Метод оптимизации: считаем градиент loss по параметру, идём в противоположную сторону.

3. Зачем chain rule в backprop?
Чтобы посчитать градиент loss по весам внутренних слоёв через производные внешних слоёв.

4. Почему нельзя инициализировать веса нулями?
Все нейроны будут учиться одному и тому же — нет смысла в нескольких нейронах.

5. Что значит «loss = NaN» в логах?
Численное переполнение. Обычно: слишком большой LR, или sigmoid с экстремальными значениями. Уменьши LR или нормализуй данные.

13. Что должно остаться в голове

  1. Обучение = Forward + Loss + Backward + Update в цикле.
  2. Backprop = chain rule, идущий от loss к входам слой за слоем.
  3. Loss функция = MSE для регрессии, Cross-Entropy для классификации.
  4. SGD: w ← w − lr · gradient.
  5. He initialization для ReLU, Xavier для tanh/sigmoid.
  6. Современные фреймворки = тот же базовый цикл + auto-grad + GPU + optimizers.
📌 Закрепление: если запустил свой framework и обучил XOR — поздравляю, ты понимаешь больше, чем 90% людей, работающих с ML. Дальше — PyTorch (Урок 35), но идеи те же.