Одна стара правило, застосоване системно
Зворотне поширення (backpropagation) – це алгоритм, який робить навчання глибоких мереж здійсненним, і в його основі немає нічого більш екзотичного, ніж ланцюгове правило з першого курсу математичного аналізу, застосоване до кожного параметра мережі одночасно. Якщо f(x) = g(h(x)), то ланцюгове правило стверджує, що df/dx = dg/dh · dh/dx – похідна складної функції є добутком похідних її частин. Румелтарт, Хінтон та Вільямс популяризували цей метод навчання у 1986 році, і з того часу галузь базується на деякій його версії: для ланцюга з n обчислених операцій, похідна є добутком n локальних часткових похідних, а зворотне поширення – це саме цей добуток, обчислюваний шар за шаром, починаючи від функції втрат.
Перетворення виразу на граф
Будь-який математичний вираз можна представити як обчислювальний граф — спрямований ациклічний граф, у якому вузли є операціями (додавання, множення, експоненціювання, застосування активації), а ребра несуть значення вперед. Розглянемо, наприклад, функцію втрат L = (a·w + b − y)²: її можна розкласти на множення, додавання, віднімання та піднесення до квадрату, які розташовані послідовно. Прямий прохід оцінює цей граф зліва направо, обчислюючи та зберігаючи кожне проміжне значення. Зворотний прохід проходить по тому ж графу справа наліво, множачи локальні похідні для накопичення внеску кожного вузла у загальний градієнт функції втрат відносно кожного входу — це множення справа наліво є буквально застосуванням ланцюгового правила, яке виконується один раз на ребро.
прямий прохід: m = a*w s = m+b e = s-y L = e^2 зворотний прохід (з початковим dL/dL = 1): dL/de = 2e dL/ds = dL/de * de/ds = 2e dL/dm = dL/ds * ds/dm = 2e dL/dw = dL/dm * dm/dw = 2e * a dL/da = dL/dm * dm/da = 2e * w Коли значення подається в більше ніж один вузол, що знаходиться вниз по потоку — вага, яка використовується в кількох шарах, або вхід, який повторно використовується у багатьох місцях — градієнт цієї ваги є сумою внесків, які надходять з усіх вузлів, які її використовували. Це багатовимірне ланцюгове правило проявляється безпосередньо в обліку, і це деталі, яка найчастіше викликає проблеми при реалізації з нуля, якщо її не врахувати.
forward: m = a*w s = m+b e = s-y L = e^2 backward (seed dL/dL = 1): dL/de = 2e dL/ds = dL/de * de/ds = 2e dL/dm = dL/ds * ds/dm = 2e dL/dw = dL/dm * dm/dw = 2e * a dL/da = dL/dm * dm/da = 2e * w
Від скалярів до матриць: Якобіан
Реальні мережі працюють з векторами та матрицями, а не з окремими числами, тому "похідна" шару стає як Якобіан матриця J, де Jij = ∂yi/∂xj. Для повнозв’язного шару y = Wx + b, корисні градієнти виявляються зовнішнім добутком та матрично-векторним добутком, а не чимось, що потребує явного матеріалізування повного Якобіана:
dL/dW = delta . x^T (зовнішній добуток; delta = dL/dy, upstream gradient) dL/dx = W^T . delta (передається далі назад до попереднього шару) dL/db = delta (підсумовується по виміру батчу) Елементні функції активації зберігають свій Якобіан на діагоналі, зводячись до простого множення з upstream градієнтом. Softmax у комбінації з втратою перехресної ентропії є приємним винятком, де дійсно щільний Якобіан алгебраїчно спрощується на всі шляхи вниз до ŷ − y, тому що ця комбінація майже універсально використовується для вихідних шарів класифікації замість того, щоб обчислювати її довго.
dL/dW = delta . x^T (outer product; delta = dL/dy, upstream gradient) dL/dx = W^T . delta (passed further back to the previous layer) dL/db = delta (summed over the batch dimension)
Чому градієнти зникають — і як це зупинити
У мережі з 20 шарами найраніший шар отримує градієнт, який є результатом приблизно 20 матриць ваг та 20 похідних активацій, що ланцюжаться за допомогою механізму, описаного вище. Якщо величина кожного фактора нижче 1 — що відбувається з насиченим активованим функцією сигмоїдою, похідна якої ніколи не перевищує 0,25 — цей добуток геометрично зменшується: 0,25^20 приблизно дорівнює 10^-12, що призводить до зникнення градієнту та робить найраніші шари практично неспроможними навчатися. Якщо ж величину факторів підвищити вище 1, то той самий добуток розростається замість того, щоб зникати. Сучасні архітектури протидіють цьому кількома способами одночасно: активовані функції ReLU, похідна яких дорівнює точно 1 для будь-якого позитивного вхідного значення, усуваючи насичення; залишкове з’єднання, яке надає градієнту коротший шлях навколо цілих блоків шарів; нормалізація батчу, яка підтримує преактивації в добре поводячомуся діапазоні; та схеми ініціалізації, такі як He або Xavier, які спеціально обираються так, щоб дисперсія активацій не зростала і не зменшувалася з шаром до шару.
Мінімальний двигун автоградієнту
Уся описана вище механіка – побудова графа під час прямого проходження та його обхід назад, множення локальних похідних – вміщується в відносно невелику кількість коду. Кожен елемент відстежує власний градієнт, свої входи та невеликий закритий блок (closure), який знає, як передавати свій власний градіент цим входам; виклик backward() топологічно сортує граф і викликає кожен closure у зворотному порядку.
class Value { constructor(data, children = []) { this.data = data; this.grad = 0; this._prev = children; this._backward = () => {}; } mul(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; } backward() { const topo = [], seen = new Set(); (function build(v){ if(!seen.has(v)){ seen.add(v); v._prev.forEach(build); topo.push(v); } })(this); this.grad = 1; topo.reverse().forEach(v => v._backward()); } } Це зворотне автоматичне диференціювання, і це саме те, що роблять PyTorch's autograd та JAX's grad transform під капотом, просто в набагато більшому масштабі з тензорами замість скалярів. Зворотній режим є правильним вибором для нейронних мереж, особливо тому, що функція втрат має один скалярний вихід і мільйони параметрів вхідних даних – один зворотний прохід одночасно відновлює градієнт кожного параметра, тоді як прямий прохід потребував би одного проходу за кожною вимірною ознакою та виправдовувався б лише в протилежному випадку: багато виходів і мало входів.
class Value {
constructor(data, children = []) {
this.data = data; this.grad = 0;
this._prev = children; this._backward = () => {};
}
mul(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;
}
backward() {
const topo = [], seen = new Set();
(function build(v){ if(!seen.has(v)){ seen.add(v);
v._prev.forEach(build); topo.push(v); } })(this);
this.grad = 1;
topo.reverse().forEach(v => v._backward());
}
}
Як ця симуляція використовує її
Симуляція зворотного поширення на цьому сайті навчає невелику багатошарову перцептронну мережу в режимі реального часу на канві, демонструючи прямий прохід обчислення активацій зліва направо та зворотний прохід передачі градієнтів праворуч від відповідних зв’язків, розфарбований за знаком і величиною. Оскільки мережа достатньо мала, щоб відобразити кожен вагу та кожен градієнт на екрані, ви можете спостерігати, як відбувається зникнення градієнта в режимі реального часу, додаючи додаткові шари або перемикаючись на функцію активації типу «сигмоїда», а також спостерігати, як це вирішується, коли ви переходите на ReLU або додаєте зв’язок із залишком – ті ж самі компроміси, що описані вище, але видимі, а не абстрактні.
Frequently asked questions
Чому зворотне поширення завжди виконується у зворотному напрямку, а не прямо через мережу?
Це тому, що нейронна мережа має мільйони параметрів, які впливають на єдине скалярне значення втрат – небагато виходів, багато входів. Зворотне поширення обчислює градієнт відносно кожного параметра за один зворотний прохід, незалежно від кількості параметрів. Пряме поширення потребувало б одного проходу для кожної вимірювальної осі, що є неймовірно дорогим у такому масштабі. Якщо мережа мала б багато виходів і мало входів, то пряме поширення було б дешевшим – це просто не форма втратної функції.
Що саме викликає зникаючі градієнти в глибоких мережах?
Правило ланцюга множить локальний похідний для кожного шару, коли градієнт поширюється назад. Якщо кожен із цих факторів надійно нижчий за 1 – що відбувається з насиченими активаціями, такими як сигмоїд, чий похідний ніколи не перевищує 0,25 – добуток з 20 шарів може зменшитися в 10 разів або більше перед досягненням першого шару, роблячи ваги практично нездатними до навчання. ReLU активації, зв’язки залишків, пакетна нормалізація та обережна ініціалізація ваг існують спеціально для того, щоб цей добуток залишався близьким до 1.
Як перевірити, чи дійсно правильний зворотний прохід, який ви написали?
Перевірка градієнтів: порівняйте аналітичний градієнт з backprop із чисельним оцінкою за допомогою різниці кінцівок, [L(w+ε) − L(w−ε)] / (2ε), для невеликого ε. Якщо обидва значення збігаються приблизно на шість значущих цифр для кожного параметра, то зворотний прохід, швидше за все, правильний. Це повільно – один додатковий прямий прохід на параметр – тому воно використовується лише для валідації нової реалізації, ніколи під час фактичного навчання.
Спробуйте наживо
Усе, що вище, працює прямо у вашому браузері — відкрийте the simulation і змінюйте параметри під час роботи. Нічого не встановлюється, нічого не завантажується на сервер, уся модель живе в одній вкладці.
▶ Відкрити симуляцію the simulation