В прошлой статье обучение свелось к схеме «посчитай градиент и шагни против него». Мы аккуратно обошли главный вопрос: как именно посчитать градиент — вектор из 13 002 частных производных функции потерь? Наивный способ — шевелить каждый вес по отдельности и смотреть, что стало с потерями, — требует 13 002 прогонов сети на каждый шаг. Для LLM с миллиардами параметров это смешно.
Алгоритм, который решает эту задачу за один проход вперёд и один назад, называется обратным распространением ошибки — backpropagation. Это, без преувеличения, самый важный алгоритм современного ИИ: без него не было бы ни GPT, ни диффузионных моделей, ни всего, что вы запускаете дома через Ollama.
1. Градиент как «степень виновности»
Начнём с интерпретации. Градиент — это 13 002 числа, и каждое отвечает на вопрос: «если сдвинуть этот вес на капельку, насколько изменится функция потерь?»
Конкретные числа для наглядности: допустим, производная по одному из весов равна 3.2, а по другому — 0.1. Это значит, что потери в 32 раза чувствительнее к первому весу. Первый — рычаг, второй — мёртвая ручка. Шаг градиентного спуска сдвинет оба, но первый — в 32 раза сильнее. Градиент — это распределение важности: кого винить в ошибке и насколько сильно.
Вопрос в том, как вычислить все эти «виновности» быстро. Backpropagation делает это за два прохода по сети — и сейчас мы поймём, почему это вообще возможно.
2. Настройка на одном примере: чего мы хотим
Упростим задачу до предела. Одна картинка — скажем, двойка. Сеть выдала на выходном нейроне «2» активацию 0.2 вместо желаемой 1.0. Хочется что-то подкрутить, чтобы эта активация выросла. Смотрим на последний нейрон и обнаруживаем, что влиять на его активацию можно тремя способами:
- Смещение (bias) — сдвинуть порог срабатывания.
- Веса входящих связей — изменить, кого этот нейрон слушает и насколько.
- Активации предыдущего слоя — изменить сами входные данные нейрона.
Разберём каждый и найдём в них неожиданно симметричную логику.
3. Три рычага: смещение, веса, активации
Смещение. Самый тупой рычаг: прибавил к смещению +0.1 — активация выросла (через сигмоиду). Работает всегда, но без разбора: нейрон станет срабатывать чаще на всех картинках, не только на двойках. Грубая настройка.
Веса. Вот здесь красиво. Взвешенная сумма — это $\sum w_i a_i$, и производная суммы по конкретному весу $w_i$ равна… активности входного нейрона $a_i$. Что это значит практически: увеличивать вес имеет смысл в первую очередь у тех связей, чей вход и так горит. Если слева от детектора петли какой-то нейрон сильно активен на этой картинке — усиливайте связь с ним; если вход молчит (активность около нуля), крутить этот вес почти бесполезно: ноль на что ни умножай — ноль.
Получается правило: кто активен, тот и учится. Связь усиливается тогда, когда оба её конца — и вход, и выход — участвуют в игре. Нейробиологи узнают в этом хеббовское правило: «нейроны, которые срабатывают вместе, связываются вместе». Красивый случай, когда искусственная и биологическая нейросети пришли к одному принципу с разных сторон.
Активации предыдущего слоя. Третий рычаг выглядит странно: мы же не можем «поменять входные данные»? Для входного слоя с пикселями — нет. Но активации скрытых слоёв — это такие же вычисляемые числа, как и всё остальное. Желание «хочу, чтобы этот скрытый нейрон был активнее» превращается в инструкцию: «увеличь вот эти веса и смещения, от которых он зависит». И так рекурсивно — слой за слоем — до самого низа.
4. Волна, бегущая назад
Соберём механизм. Пусть один выходной нейрон «хочет» измениться на какую-то величину (для картинки-двойки: вверх у нейрона «2», вниз у остальных девяти — каждый тянет одеяло на себя). Каждый скрытый нейрон получает от выходного слоя сумму желаемых изменений от всех связей, где он участвует (с учётом весов этих связей): «в целом тебя просят стать чуть активнее». Это желание он превращает в запросы к своим весам, смещению — и к слою ниже. Волна желаний бежит от выхода ко входу, и по дороге каждый нейрон понимает, как ему подкрутить свои ручки.
Важный финальный штрих: как желания разных выходных нейронов объединяются? Просто суммируются. Нейрон «2» просит скрытый слой измениться в одну сторону, нейрон «5» — в другую, и скрытый слой слышит равнодействующую.
Что получается в итоге? Если аккуратно проделать всю эту процедуру и взять результат с минусом, он окажется… в точности отрицательным градиентом функции потерь — тем самым вектором «кого и насколько винить», который нужен градиентному спуску. Интуиция и математика сходятся: backpropagation — это вычисление градиента через распространение «желаний» назад по сети. Почему сходится — строго покажем в следующей статье: это просто цепное правило дифференцирования.
5. Мини-батчи: обучение без ожидания
Осталась практическая проблема. Всё описанное — про одну картинку. А функция потерь определена как среднее по десяткам тысяч примеров, и честный градиент требует прогнать волну назад для каждого. Дорого.
Трюк, которым живёт весь современный ML: стохастический градиентный спуск (SGD). Перемешиваем обучающую выборку, режем её на мини-батчи — скажем, по 100 картинок — и делаем шаг градиентного спуска по градиенту, посчитанному на одном батче. Это не «честный» градиент всей выборки, а его случайная оценка — но направление почти всегда годное, а вычислений в сотни раз меньше. Обменяли точность шага на количество шагов — выгодная сделка.
Названия, которые вы встретите в реальности (Adam, AdamW, SGD в конфигах PyTorch и скриптах fine-tuning), — это всё вариации одного и того же: «шагать против градиента, посчитанного по мини-батчу», с разными ухищрениями в выборе шага.
6. Что сеть выучивает на самом деле
В первой статье была надежда: первый слой выучит края, второй — петли, выходной — цифры. Разбирая backprop, стоит честно посмотреть, что происходит в реальности. Если визуализировать веса обученной сети, красивых «детекторов края» в чистом виде там обычно нет: распределённые, зашумлённые паттерны, которые сложно описать словами. Идея «слои = уровни абстракций» — полезная интуиция, а не гарантия; в полной мере она раскрывается в специальных архитектурах — например, в свёрточных сетях, где иерархия «края → текстуры → части» наблюдается явно.
Но вот что важно: backpropagation всё равно находит работающие веса. Ему не нужно, чтобы наши красивые метафоры сбывались буквально, — только чтобы функция потерь падала.
7. Почему это важно знать инженеру
Autograd — это backpropagation. PyTorch, JAX, TensorFlow — все современные фреймворки строят граф вычислений и автоматически дифференцируют его ровно этим алгоритмом. Кнопка .backward() в PyTorch — буквальная реализация волны из этой статьи. Понимать, что за ней происходит, — значит понимать, почему обучение иногда ломается.
Fine-tuning LLM. Дообучение модели на своём датасете — это та же волна, бегущая через миллиарды весов. LoRA экономит память именно тем, что разрешает менять лишь маленькую часть «ручек», оставляя остальные замороженными. Параметр «batch size» в конфиге — это размер мини-батча из раздела 5.
Диагностика обучения. Затухающие градиенты (волна назад угасает — глубокие слои перестают учиться), взрывающиеся градиенты (волна усиливается до бессмысленных чисел) — типовые поломки, и теперь у вас есть правильная картинка для их понимания. Остаточные связи в ResNet и трансформерах, кстати, во многом и придуманы, чтобы волне было легче бежать назад.
Заключение
Backpropagation — это распределение вины: ошибка на выходе превращается в градиент, который говорит каждому из 13 002 весов, насколько он виноват и в какую сторону ему сдвинуться. Три рычага (смещение, веса, активации), правило «кто активен, тот и учится», волна желаний от выхода ко входу, усреднение по мини-батчам — вот и весь секрет самого рабочего алгоритма в ИИ.
Остался последний шаг: показать, почему «волна желаний» математически совпадает с градиентом. Это чистая красота — цепное правило дифференцирования, разложенное по слоям. Финальная статья серии: Математика backpropagation: разбираем цепное правило по шагам.