Зворотне поширення — розгорнуте ланцюгове правило
Зворотне поширення — алгоритм, що уможливлює глибоке навчання. Це ланцюгове правило математичного аналізу, систематично застосоване до обчислювального графа — і воно перетворило галузь після 1986 року. Простежуємо математику від скалярних правил до якобіанів і будуємо мінімальний рушій autograd, який диференціює будь-який вираз.
1. Ланцюгове правило
Припустимо, f(x) = g(h(x)). Ланцюгове правило стверджує, що похідна f за x дорівнює:
Для ланцюга з n складених функцій це узагальнюється до добутку n частинних похідних. Зворотне поширення — це саме цей добуток, обчислений шар за шаром, починаючи з виходу функції втрат.
2. Обчислювальні графи
Обчислювальний граф — це спрямований ациклічний граф (DAG), де вузли — це операції (+, ·, exp, …), а ребра несуть значення. Будь-який математичний вираз можна подати таким графом:
Під час прямого проходу ми обчислюємо значення зліва направо. Під час зворотного проходу ми перемножуємо локальні частинні похідні справа наліво, щоб накопичити внесок кожного вузла в сумарний градієнт.
3. Прямий і зворотний проходи
Для графа вище зворотний прохід (із початковим значенням dL/dL = 1) поширює:
Кожен вузол накопичує градієнт від усіх вузлів, що від нього залежать (багатовимірне ланцюгове правило). Там, де значення використовується в кількох наступних вузлах, його внески в градієнт підсумовуються.
4. Якобіани та матричні градієнти
Коли входи й виходи є векторами, «похідною» є матриця Якобі J, де Jᵢⱼ = ∂yᵢ/∂xⱼ. Для лінійного шару y = Wx + b:
Поелементні операції
Для поелементного y = σ(z) якобіан діагональний: ∂L/∂z = ∂L/∂y ⊙ σ'(z). Просто поточковий добуток.
Якобіан Softmax
Для y = softmax(z), Jᵢⱼ = yᵢ(δᵢⱼ − yⱼ) — повна щільна матриця. У поєднанні з функцією втрат перехресної ентропії спрощується до ŷ − y.
Вимір пакета
За міні-пакета з m зразків градієнти ваг усереднюються по пакету: dW = (δ · Aᵀ) / m, щоб градієнти не залежали від масштабу розміру пакета.
Перевірка градієнта
Перевірте аналітичні градієнти чисельно: порівняйте dL/dw із [L(w+ε)−L(w−ε)]/(2ε). Якщо вони збігаються до ~6 знаків після коми, ваше зворотне поширення правильне.
5. Згасання та вибух градієнтів
У 20-шаровій мережі градієнт першого шару містить добуток 20 матриць ваг і 20 похідних активацій. Якщо кожен множник має величину < 1 (сигмоїда насичується до σ' ≈ 0), добуток експоненційно зменшується → згасання градієнта. Якщо кожен множник > 1, він експоненційно зростає → вибух градієнта.
Рішення:
- Активації ReLU: σ'(z) = 1 для z > 0 — без насичення у додатній півплощині.
- Залишкові з'єднання (ResNet): «магістраль» градієнта обходить шари через додавання-перемички.
- Пакетна нормалізація: нормалізує переактивації по міні-пакету, утримуючи їх у лінійному режимі.
- Ретельна ініціалізація ваг: ініціалізація He для ReLU (var = 2/fan_in), Xavier/Glorot для tanh.
- Обрізання градієнта: обмеження норми градієнта порогом, що запобігає вибуху градієнтів у RNN.
6. Поза межами SGD — Adam і компанія
SGD оновлює кожну вагу однаково, незалежно від історії її градієнта. Адаптивні оптимізатори відстежують статистику градієнтів для кожного параметра:
Adam збігається швидше за SGD у більшості випадків і стійкий до вибору швидкості навчання. Для доналаштування мовних моделей AdamW додає належний спад ваг L2, відокремлений від кроку градієнта.
7. Мінімальний рушій autograd
// Скалярний autograd — автоматичне диференціювання у зворотному режимі
class Value {
constructor(data, _children = [], _op = '') {
this.data = data;
this.grad = 0;
this._backward = () => {};
this._prev = new Set(_children);
this._op = _op;
}
add(other) {
other = other instanceof Value ? other : new Value(other);
const out = new Value(this.data + other.data, [this, other], '+');
out._backward = () => { this.grad += out.grad; other.grad += out.grad; };
return out;
}
mul(other) {
other = other instanceof Value ? other : new Value(other);
const out = new Value(this.data * other.data, [this, other], '*');
out._backward = () => {
this.grad += other.data * out.grad;
other.grad += this.data * out.grad;
};
return out;
}
pow(n) {
const out = new Value(this.data ** n, [this], `**${n}`);
out._backward = () => { this.grad += n * (this.data ** (n - 1)) * out.grad; };
return out;
}
relu() {
const out = new Value(this.data > 0 ? this.data : 0, [this], 'relu');
out._backward = () => { this.grad += (out.data > 0 ? 1 : 0) * out.grad; };
return out;
}
backward() {
// Топологічне сортування, потім виклик _backward у зворотному порядку
const topo = []; const visited = new Set();
const build = v => {
if (!visited.has(v)) {
visited.add(v);
for (const child of v._prev) build(child);
topo.push(v);
}
};
build(this);
this.grad = 1;
for (const v of topo.reverse()) v._backward();
}
}
// Приклад: L = (x·w + b - y)²
const x = new Value(2.0);
const w = new Value(-3.0);
const b = new Value(6.88);
const y = new Value(1.0);
const L = x.mul(w).add(b).add(y.mul(-1)).pow(2);
L.backward();
console.log(w.grad); // ∂L/∂w ≈ 2*(x*w+b-y)*x
8. Режими автоматичного диференціювання
Зворотний режим (backprop)
Один зворотний прохід обчислює ∂L/∂θ для УСІХ параметрів одночасно. Ідеально, коли виходів ≪ входів — саме випадок нейронної мережі.
Прямий режим
Обчислює добуток якобіана на вектор Jv — один прямий прохід на кожен вимір входу. Ефективний, коли входів ≪ виходів (рідко в ML).
Символьне диференціювання
Виводить вирази у замкненій формі (Mathematica, SymPy). Точне, але може породжувати експоненційно великі вирази («розбухання виразу»).
Чисельне диференціювання
Скінченні різниці [f(x+h)−f(x)]/h. Просте, але повільне (один прохід на параметр) і піддається похибкам скорочення чисел з рухомою комою.
PyTorch і JAX використовують AD у зворотному режимі з динамічним обчислювальним графом (define-by-run). JAX додатково підтримує AD у прямому режимі та оператори композиції функцій (jit, vmap, grad), які чисто компонуються завдяки його функціональному дизайну.