Частные производные и градиент
Производные функций многих переменных, градиент как направление наискорейшего подъёма, якобиан на пальцах и главная формула обучения нейросетей — градиент матричного умножения; в конце — как проверить любой градиент численно.
Зачем это нужно
Заголовок раздела «Зачем это нужно»Loss языковой модели зависит не от одного числа, а от миллиардов. Значит, нужна производная по каждому из них. Все эти производные вместе — градиентградиентEN gradientВектор всех частных производных ∇f. Указывает направление наискорейшего роста функции; его длина — крутизна склона.. Это вектор, а для матрицы весов — матрица той же формы, что и сами веса. Каждый шаг оптимизатора — от простого градиентного спуска до AdamW и Muon — начинается с градиента.
В прошлом уроке мы прогнали обратный проход для скаляров. Теперь поднимемся на уровень векторов и матриц — именно в таком виде backprop работает в настоящих сетях. Главный результат урока — формула градиента линейного слоя, через который проходит почти вся «тяжёлая» работа модели. А в конце научимся проверять любой градиент численно: это главный инструмент отладки всего, что вы будете писать руками на ступенях 3–5.
Функции многих переменных
Заголовок раздела «Функции многих переменных»Функция принимает две переменные и возвращает одно число. Её удобно представлять как рельеф: точка — место на карте, — высота. Как на топографической карте, рельеф рисуют линиями уровня — кривыми, вдоль которых высота одинакова. У нашей функции это вытянутые эллипсы вокруг минимума в точке .
Loss нейросети — такой же рельеф, только «карта» имеет не два измерения, а миллиарды. Обучение — это спуск в низину по этому рельефу. Нарисовать его нельзя, но вся математика работает так же, как на плоскости.
Частные производные
Заголовок раздела «Частные производные»Как изменится высота, если шагнуть только вдоль оси , не трогая ? Это частная производнаячастная производнаяEN partial derivative∂f/∂x — производная функции многих переменных по одной из них, когда остальные заморожены. по : все остальные переменные замораживаем и дифференцируем как обычно.
Значок («дэ круглое») напоминает, что переменных несколько. Пример: в точке .
- По (считаем числом): . Почему : при замороженном слагаемое — это просто , прямая с наклоном 6, то есть .
- По (считаем числом): . Слагаемое от не зависит — для это константа, её производная 0 (урок 1.2.1).
Численная проверка — та же центральная разность, только шевелим одну переменную:
f = lambda x, y: x ** 2 + 3 * x * yh = 1e-5print((f(1 + h, 2) - f(1 - h, 2)) / (2 * h)) # ≈ 8.000000000008 — двигаем только xprint((f(1, 2 + h) - f(1, 2 - h)) / (2 * h)) # ≈ 3.000000000064 — двигаем только yГрадиент: куда круче всего вверх
Заголовок раздела «Градиент: куда круче всего вверх»Соберём все частные производные в вектор — это и есть градиент:
Значок называют «набла». Для градиент , в точке это .
У градиента та же форма, что у входа: если вход — матрица , градиент — тоже матрица .
В PyTorch w.grad всегда имеет форму w.
Почему именно «наискорейший подъём»
Заголовок раздела «Почему именно «наискорейший подъём»»Шагнём из точки на маленькое расстояние в направлении единичного вектора . Такой шаг — это сдвиг на по оси и на по оси . Сдвиг по меняет высоту примерно на (производная — «курс обмена» из урока 1.2.1), сдвиг по — на , а при маленьком шаге эти вклады просто складываются. Итого высота изменится примерно на — скалярное произведение градиента на направление. Величину называют производной по направлениюпроизводная по направлениюEN directional derivativeСкорость изменения функции при движении в направлении единичного вектора u — равна скалярному произведению ∇f · u.. В точке с сравним несколько направлений. Единичный вектор вдоль градиента получаем нормировкой из урока 1.1.1: длина равна , и .
| Направление | Что происходит | |
|---|---|---|
| вдоль оси : | 2 | подъём, но пологий |
| вдоль градиента: | 6.32 | самый крутой подъём |
| перпендикулярно: | 0 | идём по линии уровня — высота не меняется |
| против градиента: | −6.32 | самый крутой спуск |
Объяснение — из урока 1.1.1: . Косинус максимален, когда угол , то есть когда мы идём вдоль градиента. Отсюда три свойства:
- Градиент указывает направление наискорейшего роста функции.
- Его длина — крутизна склона в этом направлении (здесь ).
- Градиент перпендикулярен линиям уровня: вдоль линии уровня высота не меняется, производная по направлению равна 0.
Градиентный спуск на плоскости
Заголовок раздела «Градиентный спуск на плоскости»Чтобы спуститься, шагаем против градиента: . Для нашей функции с шаг даёт и . Крутая координата за шаг сжимается в 2.5 раза, а пологая — всего в 1.25 раза, и путь на рисунке сначала «падает» на дно оврага, а потом медленно ползёт вдоль него. На вытянутых «оврагах» простой спуск неэффективен — это мотивация для Momentum и Adam (урок 1.4.2).
Якобиан — интуитивно
Заголовок раздела «Якобиан — интуитивно»До сих пор функция выдавала одно число. А слой нейросети превращает вектор в вектор: , , . Теперь у каждого выхода есть производная по каждому входу — получается таблица . Это матрица Якобиматрица Якоби (якобиан)EN Jacobian matrixТаблица всех частных производных векторной функции — ∂yᵢ/∂xⱼ. Для y = Wx якобиан равен самой матрице W., или якобиан:
Пример с числами: в точке . Первая строка якобиана — частные производные первого выхода: и . Вторая строка — производные второго выхода: 1 и 3. Итого : строка — выход, столбец — вход.
Два важных примера:
- Линейный слой . Поскольку , производная . Якобиан линейного преобразования — сама матрица . Логично: линейная функция везде имеет один и тот же «наклон».
- Поэлементная функция, например ReLU. Выход зависит только от , поэтому якобиан диагональный: на диагонали там, где , и в остальных местах.
Цепное правило для векторов звучит знакомо: якобиан композиции — произведение якобианов. Композиция функций ↔ умножение матриц — та же связь, что в уроке 1.1.2.
Почему якобиан никогда не строят целиком
Заголовок раздела «Почему якобиан никогда не строят целиком»Возьмём батч из 1000 токенов с векторами размерности 4096 — всего 4.1 млн чисел на входе и столько же на выходе слоя. Якобиан такого слоя — таблица чисел. Это десятки терабайт на один слой.
Спасает то, что loss — одно число. На обратном проходе к слою приходит не якобиан, а вектор той же формы, что . Слою нужно лишь вычислить — произведение вектора на якобиан. Для ReLU это поэлементное умножение на маску из нулей и единиц, для линейного слоя — умножение на . Ни одна огромная матрица не появляется. Именно эту операцию — «вектор на якобиан» — реализует каждый узел autograd.
Градиент матричного умножения
Заголовок раздела «Градиент матричного умножения»Теперь главная формула урока. Линейный слой на батче (урок 1.1.2):
Читаем: батч — примеров по признаков, матрица весов превращает входов в выходов, получается
— строк по чисел. Обратите внимание: здесь для простоты веса хранятся как , то есть
(in, out), и транспонирования в формуле нет. В nn.Linear из урока 1.1.2 веса лежат наоборот, (out, in), — как это
меняет формулы, покажем в конце раздела.
Сверху, из следующих слоёв, пришёл градиент — матрица формы . Нужно найти градиенты по весам, по входу (чтобы передать дальше вниз) и по сдвигу:
| Что | Форма | Градиент | Как считается | Форма градиента |
|---|---|---|---|---|
| вход | : | — уходит в предыдущий слой | ||
| веса | : | — идёт в оптимизатор | ||
| сдвиг | сумма строк | — идёт в оптимизатор |
Обратите внимание на последний столбец: форма каждого градиента совпадает с формой того, по чему его берут.
Вывод по элементам
Заголовок раздела «Вывод по элементам»Здесь много индексов. Если при первом чтении они пугают, перейдите к «Мнемонике» и «Примеру с числами» ниже, а сюда вернитесь потом: вывод нужен, чтобы понять, откуда формула, а пользоваться ей можно и без него.
Запишем один элемент выхода: . Индексы: — номер примера в батче, — номер входа, — номер выхода. (Буква здесь работает дважды: в индексе это номер примера, а — сдвиг -го выхода. Так пишут во многих статьях — смысл подсказывает место буквы.) Вес участвует в элементах всех примеров батча (с тем же ), и каждый раз с множителем . Это ветвление из прошлого урока: вклады всех путей складываются:
Так же для входа: участвует во всех своей строки с множителями , поэтому . А сдвиг прибавляется к каждой строке — его градиент равен сумме столбца матрицы по всему батчу.
Мнемоника «по формам»
Заголовок раздела «Мнемоника «по формам»»Формулу легко восстановить, даже если вы её забыли. Градиент по обязан иметь форму , то есть . Под рукой есть () и (). Перемножить их так, чтобы получилось , можно единственным способом: . Для () из () и () получается только .
Пример с числами
Заголовок раздела «Пример с числами»Пусть (2 примера по 2 признака), , а — просто сумма всех элементов . Тогда каждый элемент входит в с коэффициентом 1, и — матрица из единиц формы :
Смысл каждого числа можно проверить «на пальцах». Вес умножается на первый признак обоих примеров, . Признак умножается на первую строку , сумма которой . Сдвиг прибавлен к двум строкам — отсюда 2.
Градиент одного примера — матрица ранга 1
Заголовок раздела «Градиент одного примера — матрица ранга 1»Если в батче один пример (), то — это одна строка (), а — строка (). Тогда — столбец на строку, внешнее произведение. По уроку 1.1.3 это матрица ранга 1. Градиент по батчу из примеров — сумма таких слоёв, его ранг не больше .
В настоящем обучении в батче миллионы токенов, и граница «ранг не больше » уже ничего не ограничивает: формально градиент может иметь полный ранг. Но на практике в градиенте матрицы весов обычно доминируют немногие направления: несколько первых сингулярных чисел (урок 1.1.3) намного больше остальных. Поэтому к градиенту полезно относиться именно как к матрице — так делает, например, оптимизатор Muon (о нём — в «Китайском угле» ниже).
В PyTorch: веса хранятся транспонированными
Заголовок раздела «В PyTorch: веса хранятся транспонированными»nn.Linear хранит веса в форме (out, in) и считает (урок 1.1.2). Тогда та же логика даёт
weight.grad формы (out, in). Проверим:
import torch
torch.manual_seed(0)layer = torch.nn.Linear(3, 2) # веса (out, in) = (2, 3)X = torch.randn(4, 3) # батч из 4 примеровY = layer(X) # Y = X @ W.T + b, форма (4, 2)G = torch.randn(4, 2) # пусть dL/dY = G (как будто пришло сверху)L = (Y * G).sum() # такая L даёт ровно dL/dY = GL.backward()
print(torch.allclose(layer.weight.grad, G.T @ X)) # True: для весов (out, in) это Gᵀ Xprint(torch.allclose(layer.bias.grad, G.sum(0))) # True: сумма по батчуТрюк L = (Y * G).sum() полезно запомнить: он позволяет «подать сверху» любой градиент и проверить обратный проход
одного слоя отдельно от всей сети.
Проверка градиента
Заголовок раздела «Проверка градиента»Ручной backward ошибается тихо: код работает, loss даже немного падает, а градиент неверен. Самый надёжный способ поймать такую ошибку — проверка градиентапроверка градиентаEN gradient checkСравнение аналитического градиента с численным (центральные разности) по относительной ошибке. Главный способ найти баг в backward. (gradient check): посчитать градиент численно и сравнить с аналитическим.
- Для каждого элемента параметра сдвиньте его на и на , посчитайте loss и возьмите центральную разность (урок 1.2.1). Верните элемент на место.
- Сравните с аналитическим градиентом по относительной ошибке (формула ниже) — абсолютная ничего не скажет, если градиенты порядка или .
- Прочитайте результат по таблице.
Относительная ошибка для аналитического градиента и численного считается по каждому элементу, и берётся наибольшая:
Числитель — обычная разница, знаменатель приводит её к масштабу самих чисел, а спасает от деления на ноль, когда оба градиента нулевые. Например, и дают .
| Относительная ошибка (float64) | Вердикт |
|---|---|
| меньше | отлично, градиент верен |
| от до | подозрительно; допустимо, если в функции есть изломы (ReLU, max) |
| от до | скорее всего, ошибка |
| больше | ошибка почти наверняка |
Проверим на . Аналитически , и по формуле урока . Добавим и вариант с типичным багом — «забыли» производную tanh:
import numpy as np
rng = np.random.default_rng(0)X = rng.standard_normal((4, 3))W = rng.standard_normal((3, 2))
def loss(W): return np.tanh(X @ W).sum() # L = сумма tanh(XW)
# аналитический градиент: dL/dY = 1 − tanh²(Y), затем dL/dW = Xᵀ · dL/dYY = X @ WdW = X.T @ (1 - np.tanh(Y) ** 2)dW_bug = X.T @ np.ones_like(Y) # «забыли» производную tanh
# численный градиент: шевелим каждый элемент W по очередиnum = np.zeros_like(W) # массив нулей той же формы, что Wh = 1e-5for idx in np.ndindex(W.shape): # перебирает все индексы: (0, 0), (0, 1), (1, 0), … old = W[idx] W[idx] = old + h # сдвинули элемент вверх up = loss(W) W[idx] = old - h # и вниз down = loss(W) W[idx] = old # обязательно вернуть на место! num[idx] = (up - down) / (2 * h) # центральная разность из урока 1.2.1
def rel_error(a, b): return np.max(np.abs(a - b) / np.maximum(1e-8, np.abs(a) + np.abs(b)))
print(f"верный градиент: {rel_error(dW, num):.1e}")print(f"градиент с багом: {rel_error(dW_bug, num):.1e}")верный градиент: 1.0e-11градиент с багом: 4.3e-01Практические правила.
- Проверяйте во float64: во float32 численная производная сама ошибается на и маскирует баги.
- Численный градиент стоит два вычисления loss на каждый элемент. Проверяйте на крошечных размерах (батч 3–5, слой 4×3) или на нескольких случайных элементах большого тензора.
- В точке излома (ReLU ровно в нуле) численная и аналитическая производные законно расходятся — не пугайтесь одиночных выбросов, если вход оказался рядом с нулём.
В PyTorch для этого есть готовая функция torch.autograd.gradcheck — она делает то же самое и требует float64:
import torchfrom torch.autograd import gradcheck
def f(X, W): return torch.tanh(X @ W).sum()
X = torch.randn(4, 3, dtype=torch.float64, requires_grad=True) # gradcheck требует float64W = torch.randn(3, 2, dtype=torch.float64, requires_grad=True)print(gradcheck(f, (X, W), eps=1e-6, atol=1e-4)) # TrueПрактика
Заголовок раздела «Практика»Реализуйте численный градиент для массива любой формы, обратный проход линейного слоя и относительную ошибку. Последний тест специально использует квадратные матрицы — там, где мнемоника «по формам» бессильна.
import numpy as np
def numerical_gradient(f, X, h=1e-5):
"""Численный градиент скалярной функции f по массиву X (любой формы).
Для каждого элемента X[idx]: сдвиньте его на +h и на −h, посчитайте f,
возьмите центральную разность и ОБЯЗАТЕЛЬНО верните элемент на место.
Перебрать все индексы удобно так: for idx in np.ndindex(X.shape).
"""
# TODO: ваш код
...
def linear_backward(X, W, G):
"""Обратный проход линейного слоя Y = X @ W + b.
X — (B, n), W — (n, m), G = dL/dY — (B, m).
Верните кортеж (dX, dW, db) формы (B, n), (n, m), (m,).
"""
# TODO: ваш код
...
def rel_error(a, b):
"""Относительная ошибка: max |a − b| / max(1e-8, |a| + |b|) по всем элементам."""
# TODO: ваш код
...
# Проверьте себя кнопкой «Запустить»:
rng = np.random.default_rng(0)
X = rng.standard_normal((4, 3))
W = rng.standard_normal((3, 5))
b = rng.standard_normal(5)
R = rng.standard_normal((4, 5)) # L = sum(Y * R), поэтому dL/dY = R
def loss_of_W(W_):
return np.sum((X @ W_ + b) * R)
grads = linear_backward(X, W, R)
if grads is not None: # пока функция не написана, она возвращает None
dX, dW, db = grads
print(rel_error(dW, numerical_gradient(loss_of_W, W))) # меньше 1e-7 — градиент верен
Задание для Jupyter (по желанию). Возьмите двухслойную сеть на батче 5×4
со скрытым слоем 6 и выходом 3. Напишите обратный проход руками по формулам урока (не забудьте маску ReLU) и
проверьте все градиенты через numerical_gradient. Это ровно та задача, которую вы будете решать в уроке 3.1.2.
Проверь себя
Заголовок раздела «Проверь себя»- Частная производная — производная по одной переменной при замороженных остальных. Градиент — вектор всех частных производных той же формы, что и вход.
- Градиент указывает направление наискорейшего роста, его длина — крутизна, он перпендикулярен линиям уровня. Спуск: .
- Якобиан — таблица производных вектор-функции; для это . Backprop не строит якобианы, а умножает на них вектор градиента.
- Линейный слой: , , . Градиент одного примера — ранг 1.
- Любой ручной backward проверяйте численно во float64: относительная ошибка меньше — отлично, больше — баг.
Дальше: модуль 1.3 — вероятность и теория информации: случайные величины, распределения и то, как языковая модель выдаёт вероятности следующего токена.