Нейронные сети Нейронные сети: карта трека, интуиция и место среди ML
0%

Нейронные сети: карта трека, интуиция и место среди ML

Нейронные сети: карта трека, интуиция и место среди ML

Эта статья — вводная для всего трека. Её задача двойная: дать честную интуицию, что такое нейронная сеть на самом деле (не «модель мозга», а конкретный математический объект), и разложить по полкам маршрут — какая статья зачем нужна и в каком порядке читать. Если вы не проходили классический ML, стоит хотя бы пролистать трек «Машинное обучение» — понятия «обучающая выборка», «функция потерь», «переобучение», «валидация» дальше используются как данность.

1. Проблема, из которой всё выросло

Классическое программирование — это правила: вы описываете логику, машина исполняет. Классический ML — это правила, выведенные из данных, но над признаками, которые придумали вы. Логистическая регрессия отлично работает, если вы сами подали ей площадь, этаж, расстояние до метро. Она не умеет придумать признак «вид из окна» из массива пикселей.

Вот в этом и вся проблема. Возьмём задачу «на фото кот или собака». Признак «количество пикселей яркостью > 200» бесполезен. Признак «есть ли усы» — полезен, но чтобы его посчитать, нужно… решить задачу распознавания усов. Классический ML упирается в ручное конструирование признаков, а на изображениях, звуке и тексте руками осмысленные признаки не строятся. Нейронная сеть переворачивает постановку:

Вместо того чтобы придумывать признаки, давайте параметризуем само построение признаков и будем подбирать эти параметры градиентным спуском вместе с финальным классификатором.

Признаки перестают быть входом — они становятся обученным промежуточным представлением. Это и есть representation learning, вокруг него крутится весь трек, и предметно мы вернёмся к нему в статье об эмбеддингах. Плата за это: нужны данные (много), нужны вычисления (много), теряется прозрачность и появляются новые способы всё сломать.

2. Что такое нейронная сеть строго

Забудьте на минуту про нейроны и аналогию с мозгом — она историческая и скорее мешает. Нейронная сеть — это параметрическая функция, собранная композицией простых блоков:

$$f_\theta(x) = g_L \circ g_{L-1} \circ \dots \circ g_1(x)$$

Самый частый блок — аффинное преобразование плюс поэлементная нелинейность:

$$g_\ell(h) = \varphi(W_\ell h + b_\ell)$$

где $W_\ell$ — матрица весов, $b_\ell$ — вектор сдвигов, $\varphi$ — нелинейность (ReLU, GELU, tanh). Всё. Обучение — это подбор $\theta = {W_\ell, b_\ell}$ так, чтобы минимизировать функцию потерь на данных. Дальше — три вопроса, которые задаёт любой человек, впервые это увидевший.

Почему нужна нелинейность $\varphi$? Без неё композиция аффинных преобразований — снова аффинное: $W_2(W_1x + b_1) + b_2 = (W_2W_1)x + (W_2b_1 + b_2)$. Сеть из ста слоёв без активаций эквивалентна одному линейному слою, то есть линейной регрессии. Вся выразительность глубины держится на нелинейности между слоями.

Почему это вообще может выразить что угодно? Теорема об универсальной аппроксимации (Cybenko, 1989; Hornik, 1991) утверждает: сеть с одним скрытым слоем достаточной ширины приближает любую непрерывную функцию на компакте с любой точностью. Оригинал Cybenko — «Approximation by superpositions of a sigmoidal function».

Тогда зачем глубина, если хватает одного слоя? Потому что теорема говорит о существовании, а не о цене. «Достаточная ширина» может означать экспоненциальное число нейронов. Есть функции, которые глубокая сеть выражает полиномиальным числом параметров, а неглубокая — только экспоненциальным (см. Telgarsky, arxiv:1509.08101). Плюс сугубо практическое: глубина даёт иерархию признаков — края → текстуры → части объектов → объекты, — и такие представления переиспользуются между задачами.

Анатомия полносвязной сети: прямой и обратный проход

3. Три кита: параметризация, потеря, градиент

Любое обучение сети — это один и тот же цикл, вне зависимости от того, MLP это на двух признаках или LLM на 400 млрд параметров.

Параметризация — какую форму имеет $f_\theta$. Это выбор архитектуры, и он не косметический: архитектура кодирует то, что вы знаете о данных заранее (индуктивное смещение). Свёртка кодирует «сдвиг картинки не меняет смысл». Рекуррентность — «есть порядок во времени». Attention — «важность связи зависит от содержимого, а не от расстояния». Правильное смещение экономит данные на порядок.

Структура данных, симметрия и подходящая архитектура

Функция потерь — как измеряется ошибка. Cross-entropy для классификации, MSE для регрессии, contrastive для представлений, KL для дистилляции. Потеря должна быть дифференцируемой по $\theta$: метрика бизнеса (accuracy, F1, выручка) обычно не дифференцируема, поэтому вы всегда оптимизируете прокси. Разрыв между прокси и настоящей целью — источник половины проблем в проде, подробнее в статье про оценку моделей.

Градиент — как узнать, куда двигать параметры. Обратное распространение ошибки — это просто правило цепочки, применённое к вычислительному графу и организованное так, чтобы не пересчитывать общие подвыражения. Ключевой факт: обратный проход стоит примерно столько же, сколько прямой (константа 2–3×), независимо от числа параметров. Именно поэтому обучение миллиардов параметров вообще возможно. Детали — в статье о перцептроне и backprop.

4. Минимальная сеть с нуля: numpy, 60 строк

Ничто не заменяет собственную реализацию. Ниже — двухслойный MLP, решающий классическую нелинейную задачу «два полумесяца», которую линейная модель не берёт в принципе. Никаких фреймворков, только numpy.

import numpy as np

rng = np.random.default_rng(0)


def make_moons(n=1000, noise=0.20):
    """Два переплетённых полумесяца — линейно неразделимая задача."""
    k = n // 2
    t = np.linspace(0, np.pi, k)
    outer = np.c_[np.cos(t), np.sin(t)]            # верхний полумесяц
    inner = np.c_[1 - np.cos(t), 0.5 - np.sin(t)]  # нижний, сдвинутый
    X = np.vstack([outer, inner]) + rng.normal(0, noise, (2 * k, 2))
    y = np.r_[np.zeros(k), np.ones(k)][:, None]    # форма [N, 1]
    return X, y


def bce_with_logits(z, y):
    """Численно устойчивая бинарная кросс-энтропия от логитов.
    Наивный вариант log(sigmoid(z)) даёт inf при |z| > ~35."""
    return np.mean(np.maximum(z, 0) - z * y + np.log1p(np.exp(-np.abs(z))))


X, y = make_moons()
n_in, n_hid = 2, 32

# He-инициализация: дисперсия 2/fan_in компенсирует, что ReLU обнуляет половину сигнала.
W1 = rng.normal(0, np.sqrt(2 / n_in), (n_in, n_hid))
b1 = np.zeros(n_hid)
W2 = rng.normal(0, np.sqrt(2 / n_hid), (n_hid, 1))
b2 = np.zeros(1)

lr, N = 0.5, len(X)
for step in range(3000):
    # ---------- прямой проход ----------
    z1 = X @ W1 + b1            # [N, 32]
    a1 = np.maximum(z1, 0)      # ReLU
    z2 = a1 @ W2 + b2           # [N, 1] — логит, без сигмоиды

    # ---------- обратный проход ----------
    # Ключевое упрощение: производная BCE по логиту = sigmoid(z) - y.
    # Сигмоида и кросс-энтропия «схлопываются» аналитически.
    p = 1 / (1 + np.exp(-z2))
    dz2 = (p - y) / N           # [N, 1]
    dW2 = a1.T @ dz2            # [32, 1]
    db2 = dz2.sum(axis=0)
    da1 = dz2 @ W2.T            # [N, 32]
    dz1 = da1 * (z1 > 0)        # производная ReLU — маска
    dW1 = X.T @ dz1             # [2, 32]
    db1 = dz1.sum(axis=0)

    # ---------- шаг градиентного спуска ----------
    W1 -= lr * dW1; b1 -= lr * db1
    W2 -= lr * dW2; b2 -= lr * db2

    if step % 500 == 0:
        acc = ((z2 > 0) == y).mean()
        print(f"шаг {step:4d}  loss {bce_with_logits(z2, y):.4f}  acc {acc:.3f}")

Типичный вывод: loss падает с 0.74 до ~0.068, accuracy — с 0.50 до 0.974. Логистическая регрессия, обученная на тех же данных тем же градиентным спуском, упирается в 0.87 — ровно потому, что ей недоступна нелинейная граница. Разрыв в 10 п.п. на задаче из двух признаков даёт один скрытый слой из 32 нейронов.

Четыре момента, каждый из которых развернётся в отдельную тему:

  1. np.maximum(z, 0) - z*y + log1p(exp(-|z|)) — устойчивая формула вместо наивной. Численная устойчивость в нейросетях не мелочь, а систематическая тема.
  2. He-инициализация. Замените на rng.normal(0, 0.01, ...) — сеть будет учиться в разы медленнее, а на 10 слоях просто не сойдётся (подробнее).
  3. dz1 = da1 * (z1 > 0) — вся суть backprop: градиент течёт назад, поэлементно умножаясь на локальные производные.
  4. Полный батч на каждом шаге. На реальных данных так нельзя — нужен SGD по мини-батчам.

Проверка градиента

Аналитический градиент легко реализовать с ошибкой, а сеть при этом всё равно как-то учится — баг маскируется. Всегда сверяйтесь с численной производной:

def numeric_grad(param, idx, eps=1e-5):
    """Центральная разность: (L(θ+ε) − L(θ−ε)) / 2ε. Ошибка O(ε²)."""
    orig = param[idx]
    param[idx] = orig + eps
    lp = bce_with_logits(np.maximum(X @ W1 + b1, 0) @ W2 + b2, y)
    param[idx] = orig - eps
    lm = bce_with_logits(np.maximum(X @ W1 + b1, 0) @ W2 + b2, y)
    param[idx] = orig
    return (lp - lm) / (2 * eps)
# Относительная погрешность должна быть < 1e-6; проверяйте ДО обучения
# и на нескольких случайных индексах каждой матрицы.

То же самое на PyTorch

import torch
import torch.nn as nn

Xt = torch.tensor(X, dtype=torch.float32)
yt = torch.tensor(y, dtype=torch.float32)

model = nn.Sequential(
    nn.Linear(2, 32),
    nn.ReLU(),
    nn.Linear(32, 1),          # выдаём логит
)
# BCEWithLogitsLoss внутри делает ту же устойчивую формулу, что мы писали руками
loss_fn = nn.BCEWithLogitsLoss()
opt = torch.optim.Adam(model.parameters(), lr=1e-2)

for step in range(1500):
    opt.zero_grad()            # градиенты в PyTorch аккумулируются — обнуляем явно
    z = model(Xt)
    loss = loss_fn(z, yt)
    loss.backward()            # тот самый обратный проход, но автоматический
    opt.step()

Пятьдесят строк ручной математики свернулись в loss.backward(). Это и есть главное, что дают фреймворки: автоматическое дифференцирование произвольного вычислительного графа — см. официальный туториал по autograd.

5. Сложность и цена: считаем на салфетке

Инженерная интуиция «сколько это будет стоить» отличает специалиста от человека, который просто запускает чужие ноутбуки. Один полносвязный слой $d_{in} \to d_{out}$ при батче $B$:

Ресурс Оценка
Время, прямой проход $O(B \cdot d_{in} \cdot d_{out})$, примерно $2 B d_{in} d_{out}$ FLOPs
Время, обратный проход ≈ 2× от прямого (градиент по входу + градиент по весам)
Память под параметры $d_{in} d_{out} + d_{out}$ чисел
Память под состояние Adam ×3 от параметров (сами веса + два момента)
Память под активации $O(B \cdot d_{out})$ на слой, живёт до backward

Отсюда правило, которым пользуются при планировании обучения LLM: полное обучение стоит примерно 6 $ND$ FLOPs, где $N$ — число параметров, $D$ — число токенов (2 на прямой + 4 на обратный проход на параметр на токен). Формула и её применение — в работе Kaplan et al., «Scaling Laws for Neural Language Models», и уточнение в Chinchilla, arxiv:2203.15556.

Два следствия, которые ломают интуицию новичков:

  • Память под активации часто больше, чем под веса. Модель на 1 ГБ весов при батче 32 может требовать 20 ГБ на активации. Отсюда gradient checkpointing — пересчитывать активации вместо хранения, меняя память на время.
  • Батч почти не влияет на время на пример (пока GPU загружен): матричное умножение [B×d_in] @ [d_in×d_out] при малых B упирается в пропускную способность памяти, а не в вычисления. Батч 1 и батч 32 могут занимать почти одинаковое время.

6. Когда нейросеть — правильный выбор, а когда нет

Это самый недооценённый раздел. Половина провальных ML-проектов — это нейросеть там, где хватало градиентного бустинга.

Нейросеть уместна, когда: данные имеют структуру, которую можно закодировать в архитектуре (пиксели, аудио, текст, графы, последовательности событий); данных много — от десятков тысяч примеров, а лучше миллионы (либо есть предобученная модель для дообучения — тогда порог падает до сотен примеров); признаки сырые и низкоуровневые; нужен перенос обучения или генерация.

Нейросеть плохой выбор, когда:

  • Табличные данные среднего размера. Здесь градиентный бустинг стабильно выигрывает — это не мнение, а результат систематических сравнений: Grinsztajn et al., «Why do tree-based models still outperform deep learning on tabular data?». Начинайте с бустинга.
  • Примеров сотни, а не тысячи, и нет подходящей предобученной модели.
  • Требуется объяснимость по регуляторным причинам (скоринг, медицина). Частично лечится методами из статьи об интерпретируемости, но линейная модель или дерево объяснимы «из коробки».
  • Жёсткий бюджет по латентности и нет ресурса на оптимизацию инференса.
  • Задача решается правилами: если бизнес-логика выражается двадцатью if, напишите двадцать if.

Разумная стратегия: всегда начинайте с простого бейзлайна — константа, логистическая регрессия, бустинг. Нейросеть должна обгонять бейзлайн заметно, иначе её сложность не окупается. Об этом ровно так же пишет Андрей Карпати в «A Recipe for Training Neural Networks» — обязательное чтение перед первым реальным проектом.

7. Как нейросети сюда пришли: короткая история

Хронология помогает не изобретать заново то, что уже пробовали и отбросили.

Здесь важна закономерность: прорывы почти всегда упирались не в новые идеи, а в вычисления и данные. Свёртки существовали с 1989, но взлетели в 2012, когда появились GPU и ImageNet. Attention описан в 2014, но стал доминирующим в 2017, когда его сделали единственным механизмом и научились параллелить. Урок сформулировал Ричард Саттон в «The Bitter Lesson»: методы, масштабирующиеся с вычислениями, в долгую побеждают методы, в которые вложены человеческие знания о предметной области.

8. Карта трека

Двенадцать статей делятся на четыре смысловых блока.

Рекомендуемые маршруты:

  • Полный, с нуля: 01 → 02 → 03 → 04 → 05 → 06 → 07 → 08 → 09 → 10 → 11. Порядок исторический и он же логический: каждая архитектура решает проблему предыдущей.
  • «Хочу к LLM быстрее»: 01 → 02 → 05 → 06 → 09 → 10. Свёртки можно отложить, но 04 стоит прочитать ради понимания, почему attention вытеснил RNN — иначе трансформер выглядит магией.
  • «Я инженер, модель уже дали»: 10 → 11 → 02 → 05. Начните с деплоя.
  • Компьютерное зрение: 01 → 02 → 03 → 07 → 10.

Связь с соседними треками: математический аппарат (градиенты, матрицы, вероятности) — «Математические основы»; подготовка данных — «Данные и признаки»; переобучение и регуляризация в общем виде — соответствующая статья ML-трека.

9. Жизненный цикл модели в проде

Обучение — это меньшая часть работы. Реальная система выглядит так:

Обратите внимание на циклы «Оценка → Обучение» и «Мониторинг → Данные»: в зрелых командах львиная доля времени уходит именно на них, а не на выбор архитектуры.

10. Типичные ошибки, которые совершают все

Собрано из практики; каждая стоит кому-то недели.

Утечка данных (leakage). Нормализация посчитана по всему датасету до разбиения на train/test. Или в признаках есть информация из будущего. Симптом: подозрительно хорошие метрики на валидации и провал в проде. Правило: любое преобразование, которое «учится» на данных, обучается только на train.

Не перепроверили, что сеть способна переобучиться. Первый диагностический шаг — взять 20 примеров и добиться нулевой ошибки на них. Если сеть не может запомнить 20 примеров — у вас баг в коде, а не проблема с данными.

Забыли model.eval() / opt.zero_grad(). Dropout и BatchNorm ведут себя по-разному в обучении и инференсе; забытый eval() даёт плавающие предсказания, забытый zero_grad() — накопление градиентов и странную расходимость.

Learning rate «на глазок». LR — самый важный гиперпараметр, важнее архитектуры. Слишком большой — loss уходит в NaN; слишком маленький — сеть «учится», но неделю. Ищите его логарифмической сеткой или LR range test.

Сравнение моделей на разных сплитах. Разница в 0.3% при разных random seed — это шум. Фиксируйте seed, усредняйте несколько запусков, показывайте разброс. И не смотрите на accuracy при дисбалансе: 99% при 1% положительного класса — это константный предсказатель.

Оптимизация того, что легко измерить. Сеть отлично выучит прокси-метрику и при этом не решит задачу: учите рекомендации по кликам — получите кликбейт.

Игнорирование стоимости инференса до конца проекта. Модель, которая учится три дня, но не влезает в 100 мс на CPU, — это выброшенные три дня. Ограничения по латентности и памяти закладывайте в постановку.

11. Что нужно из инструментов

Минимальный набор, чтобы идти по треку с руками, а не только глазами:

# CPU-версия достаточна для всех примеров трека, кроме глав про LLM
pip install torch numpy matplotlib scikit-learn
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"

Из бесплатных вычислений — Google Colab и Kaggle Notebooks дают GPU на несколько часов, этого хватает на всё до дообучения небольших языковых моделей включительно.

Источники, к которым стоит возвращаться на протяжении всего трека:

  • Goodfellow, Bengio, Courville, Deep Learning — фундаментальный учебник, бесплатно: deeplearningbook.org
  • Dive into Deep Learning — учебник с исполняемым кодом на PyTorch/TF/JAX: d2l.ai
  • Michael Nielsen, Neural Networks and Deep Learning — лучшее интуитивное введение в backprop: neuralnetworksanddeeplearning.com
  • Andrej Karpathy, Neural Networks: Zero to Hero — построение autograd и GPT с нуля: karpathy.ai/zero-to-hero.html
  • Stanford CS231n — классический курс по CV и нейросетям: cs231n.github.io
  • PyTorch Tutorials — официальная документация: pytorch.org/tutorials

12. Мини-итог

  • Нейронная сеть — композиция аффинных преобразований и нелинейностей, параметры которой подбираются градиентным спуском. Не модель мозга, а функция с миллионами чисел.
  • Главное отличие от классического ML — признаки не задаются, а обучаются: это снимает потолок feature engineering и открывает работу с сырыми данными.
  • Нелинейность обязательна: без неё любая глубина схлопывается в линейную модель. Глубина даёт не выразительность, а экономию параметров и иерархию представлений.
  • Обучение — один цикл: батч → прямой проход → потеря → backprop → шаг оптимизатора. Обратный проход стоит ≈2× прямого, и это делает масштаб возможным.
  • Архитектура кодирует априорное знание о структуре данных: сетка → CNN, последовательность → Transformer, граф → GNN, таблица → скорее бустинг.
  • Нейросеть — не универсальный ответ: бейзлайн, стоимость инференса и объяснимость решают, стоит ли она того.

Что дальше

Мы разобрали, чем является нейронная сеть и зачем она нужна. Теперь — как она работает изнутри: один нейрон, почему выбор активации меняет всё, и как правило цепочки превращается в алгоритм, вычисляющий градиент по миллиардам параметров за один проход.

Перцептрон, функции активации и обратное распространение ошибки

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов