Як навчаються нейронні мережі: зворотне поширення та градієнтний спуск
Нейронна мережа використовує параметри, включаючи ваги та зміщення, щоб перетворити вхідні дані на прогнози. Його архітектура визначає обчислення, а значення його параметрів визначають, як ці обчислення реагують на вхідні дані. Навчання коригує ці значення за допомогою прикладів, щоб мережа могла вивчати такі завдання, як розпізнавання рукописних цифр або переклад тексту.
Навчання – це процес пошуку корисних значень для цих параметрів. Мережа робить прогнози, вимірює їхню похибку за допомогою функції втрат і обчислює, як змінюються втрати з кожним параметром. Зворотне поширення обчислює ці похідні, які називаються градієнтами. Оптимізатор, наприклад gradient descent, використовує їх для оновлення параметрів.
Ось як це виглядає на найпростішому можливому прикладі — модель вчиться укладати лінію. Натисніть Крок кілька разів і подивіться:
Кожне клацання запускає одну навчальну ітерацію: робити прогнози, вимірювати їх похибки (червоні пунктирні лінії), обчислювати градієнти та оновлювати параметри. Завдяки використаній тут швидкості навчання лінія наближається до даних з кожним кроком.
Ми пропрацюємо кожен із цих кроків, починаючи з одного нейрона та поширюючи обчислення на кілька рівнів. Віджети показують, як змінюються числа, а фрагменти Python пов’язують ці зміни з кодом.
Почніть із прикладів нейрона та втрат, потім попрацюйте над похідними та правилом ланцюга. Якщо ви вже знаєте обчислення, ви можете перейти до правила ланцюга між багатьма шарами.
Нейронна мережа - це лише параметри
Ми будемо використовувати щільну мережу прямого зв’язку: нейрони розташовані шарами, і кожен нейрон одного шару отримує вихідні дані попереднього шару. Кожен нейрон поєднує свої входи в один вихід. Щоб зрозуміти цю мережу, ми почнемо з одного нейрона.
Віджет нижче показує нейрон із 3 входами. Зліва ви керуєте вхідними значеннями; праворуч ви встановлюєте його ваги, зміщення та функцію активації. Ваги та зміщення є вивченими параметрами; функція активації - це вибір, який ми робимо під час проектування мережі.
На віджеті ви можете побачити нейрон, який множить кожен вхідний сигнал на відповідну вагу, додаючи їх із зміщенням, і передача результату через функцію активації.
Записується у вигляді рівняння:
Тут — вхід, — його вага, — зміщення, а — функція активації.
Спробуйте кілька конфігурацій, щоб створити інтуїцію:
- Вага контролює важливість вхідних даних. Встановіть x₁ = 1,0, а решту – 0. Тепер перетягніть w₁ — вихід відповідає безпосередньо. Встановіть w₁ = 0, і цей вхід повністю ігнорується, незалежно від його значення.
- Знак ваги змінює свій внесок. Встановіть x₁ = 1,0, w₁ = -1,5, а все інше дорівнює 0. Зважена сума від’ємна, тому активація персептрона видає 0. Тепер встановіть x₁ = -1,0: добуток стає додатним. Від’ємна вага зменшує суму для позитивних вхідних даних і збільшує її для негативних вхідних даних.
- Зміщення зміщує межу прийняття рішень. Якщо всі вхідні дані дорівнюють 0, лише зміщення визначає суму. При активації перцептрона позитивне зміщення дає 1; негативне зміщення означає, що зважені входи повинні подолати його, перш ніж вихід стане 1.
- Функція активації формує результат. Переключіться з Perceptron (жорсткий 0/1) на Sigmoid — тепер вихід є плавним значенням між 0 і 1. Спробуйте ReLU — він пропускає додатні значення через незмінні та обрізає негативні до нуля.
Чому функції активації важливі? Без нелінійних активацій кожен рівень обчислює афінну функцію: лінійне перетворення плюс зсув. Складання цих шарів все ще дає одну афінну функцію, незалежно від глибини. Нелінійні активації дозволяють мережі представляти більш складні відносини. Наприклад, вони можуть трансформувати вхідні дані так, що класи, які перекриваються вздовж кожної прямолінійної межі, стають роздільними. Кріс Олах ілюструє цю геометрію в Neural Networks, Manifolds, and Topology.
Ваги та зміщення — це параметри нейрона — значення, які йому потрібно засвоїти. Нейрон зберігає одну вагу на вхід — у нашому віджеті вище це вектор із 3 значень. Вагові коефіцієнти визначають, наскільки важливий кожен вхід, а зміщення зміщує результат вгору або вниз. Разом ці параметри визначають, на що реагує нейрон. Різні ваги та зміщення змушують той самий нейрон виявляти абсолютно різні шаблони у своїх вхідних даних.
Оскільки множення та сума є просто скалярним добутком, це зазвичай записується у векторній формі:
Тут — вектор (список) усіх вхідних даних — напр. — а — вектор усіх ваг — наприклад, . Скалярний добуток множить кожну пару та підсумовує результати: .
У Python це може виглядати так:
import numpy as np
class Neuron:
def __init__(self, n_inputs):
self.w = np.random.randn(n_inputs) # w — weight vector, e.g. [w₁, w₂, w₃]
self.b = 0.0 # b — bias
def forward(self, x): # x — input vector, e.g. [x₁, x₂, x₃]
z = np.dot(self.w, x) + self.b # w · x + b — dot product + bias
return max(0, z) # f(z) — activation function (ReLU)
# Create a neuron with 3 inputs and run it
neuron = Neuron(3)
output = neuron.forward(np.array([1.0, 0.5, 0.7]))Шар - це група нейронів
Складіть багато з цих нейронів разом, і ви отримаєте шар. З’єднайте кілька шарів, і ви отримаєте нейронну мережу. Ось маленький — 2 входи, два прихованих шару по 3 нейрони в кожному та 1 вихід. Їх називають «прихованими», тому що ви бачите лише вхідні дані та вихідні дані — шари між ними є внутрішніми для мережі, невидимими ззовні:
Кожен нейрон зберігає одну вагу на вхід і зміщення, а потім застосовує активацію:
Зазвичай шар зберігає всі разом у матриці ваг () — один рядок ваг на нейрон.
Для мережі, зображеної вище, layer1.W — це матриця 3×2 — 3 нейрони, кожен з яких має 2 ваги, оскільки кожен нейрон отримує 2 входи:
layer1.W = [[ 0.4, -0.2], ← neuron 0: weights for x₁, x₂
[ 0.1, 0.7], ← neuron 1: weights for x₁, x₂
[-0.3, 0.5]] ← neuron 2: weights for x₁, x₂Для одного нейрона ми мали скалярний добуток між одним ваговим вектором і вхідними даними: . Для повного шару ми складаємо всі вагові вектори в матрицю , а всі зміщення — у вектор , тому та сама операція застосовується до кожного нейрона одночасно:
Коли ми обчислюємо , кожен рядок утворюється з вхідними даними — це зважена сума одного нейрона. Матричне множення виконує все це за одну операцію.
Щоб отримати геометричний вигляд цього обчислення, перегляньте що насправді робить матриця ваг. Він слідує за чотирма вхідними точками через прихований шар, щоб показати, як вагова матриця, зміщення та нелінійна активація змінюють своє представлення та роблять класи роздільними.
У Python це виглядає так:
import numpy as np
class Layer:
def __init__(self, n_inputs, n_neurons):
# W is a matrix where each ROW is one neuron's weights.
# Shape: (n_neurons, n_inputs) — so W[0] is neuron 0's weights,
# W[1] is neuron 1's weights, etc.
self.W = np.random.randn(n_neurons, n_inputs)
self.b = np.zeros(n_neurons) # b — bias vector, one per neuron
def forward(self, x):
# W @ x multiplies every neuron's weight row by the input,
# computing all dot products at once
return np.maximum(0, self.W @ x + self.b) # ReLU activation
# Build the network from the diagram above
layer1 = Layer(2, 3) # 2 inputs → 3 neurons (6 weights + 3 biases = 9)
layer2 = Layer(3, 3) # 3 inputs → 3 neurons (9 weights + 3 biases = 12)
output = Layer(3, 1) # 3 inputs → 1 neuron (3 weights + 1 bias = 4)
# Forward pass — each layer's output feeds into the next
x = np.array([0.5, 0.8])
h1 = layer1.forward(x) # input → hidden layer 1
h2 = layer2.forward(h1) # hidden layer 1 → hidden layer 2
y = output.forward(h2) # hidden layer 2 → outputМатричні операції дозволяють бібліотекам обчислювати разом багато нейронів і багато прикладів, що робить їх добре придатними для паралельного виконання на GPU.
Кожна вага і кожне упередження є параметром. Рівень 1 має 9, шар 2 має 12, а вихідний рівень має 4: загалом 25 параметрів. Навчання має обчислити оновлення для кожного з них.
Як мережа навчається?
Навчання нейронної мережі означає коригування кожного ваги та зміщення. Ваги зазвичай ініціалізуються випадковим чином, щоб порушити симетрію між нейронами; зміщення можуть починатися з нуля, як це відбувається в нашому коді. Потім навчання оновлює їх за допомогою градієнтів втрати.
Кожна ітерація навчання має чотири етапи:
| етап | Що воно робить |
|---|---|
| 1. Пас вперед | Проведіть вхідні дані через кожен рівень мережі, помноживши на ваги та застосовуючи активації, щоб створити прогноз |
| 2. Розрахунок збитків | Порівняйте прогноз із фактичним цільовим значенням за допомогою функції втрат (наприклад, MSE), яка зводить усі помилки до одного числа — наскільки неправильна модель? |
| 3. Зворотне поширення | Попрацюйте мережею у зворотному напрямку, використовуючи правило ланцюга, щоб обчислити, як змінюються втрати з кожним параметром |
| 4. Градієнтний спуск | Відніміть від кожного параметра швидкість навчання, помножену на градієнт, щоб зробити крок до менших втрат |
Тренування включає всі чотири етапи, починаючи з пасу вперед. Під час виведення мережа використовує свої поточні параметри для прогнозування; він не обчислює градієнти навчання та не оновлює свої ваги.
Решта цієї статті присвячена тому, як ми вимірюємо помилки, обчислюємо градієнти та використовуємо їх для оновлення параметрів.
Процес має два напрямки: дані переходять вперед для створення прогнозу, потім градієнти переходять назад, щоб оптимізатор міг оновити ваги:
Forward pass: each layer receives activations, passes output →
activations activations activations
Input ─────────▶ Layer 1 ─────────▶ Layer 2 ─────────▶ Output ──▶ Loss
Backward pass: each layer receives gradient signal, passes it ←
gradients gradients gradients
Input ◀───────── Layer 1 ◀───────── Layer 2 ◀───────── Output ◀── ∂L
↓ ∂L/∂W₁ ↓ ∂L/∂W₂ ↓ ∂L/∂W₃
(own weight (own weight (own weight
gradients) gradients) gradients)Зверніть увагу на симетрію: у прямому проході кожен шар отримує активації від попереднього шару та передає свій вихід вперед. У зворотному проході кожен шар отримує сигнал градієнта від наступного шару та передає його назад. В обох напрямках кожному шару для виконання своєї роботи потрібен вхід від сусіда.
Простий приклад: підгонка лінії
Ми почнемо з одного нейрона, одного входу, однієї ваги та одного зміщення. Це дає нам втрату лише з двома параметрами, які потрібно дослідити, перш ніж ми поширимо обчислення градієнта на кілька шарів.
З одним входом і активацією ідентичності (яка повертає свій вхід незмінним) нейрон обчислює . Тут і є скалярами. Навчання цього нейрона відповідати лінії дозволяє нам вивчати алгоритм навчання без похідної активації, яка ускладнює обчислення.
Припустімо, хтось дає вам п’ять балів і каже, що вони походять від лінійної функції , і просить вас знайти w і b:
| x (введення) | -2 | -1 | 0 | 1 | 2 |
| y (вихід) | -3 | -1 | 1 | 3 | 5 |
Дані дають нам вхідні та вихідні дані. Нам потрібно знайти параметри, які їх з’єднують: , нахил лінії, і , її перетинання на осі y.
Вивчення функції дозволяє передбачити виходи для вхідних даних, яких немає в навчальних даних. Якщо співвідношення , ми можемо передбачити , навіть якщо 1,5 немає в таблиці. Створення точних прогнозів на нових вхідних даних називається узагальненням. Наші дані дотримуються точної лінії; на реальних даних, точне тренування саме по собі не встановлює, що модель буде узагальнюватися.
Отже, давайте розмістимо ці точки на графіку (зелені крапки) і спробуємо знайти w і b вручну, регулюючи повзунки.
Використовуйте значення втрат, щоб керувати собою — перетягніть їх і подивіться, чи зможете ви звести втрату до нуля. Ви побачите, що і зводять втрати до нуля — це точні параметри, які згенерували дані, що дає нам :
Повзунки повідомляють про втрату: єдине число, яке підсумовує помилки передбачення в наборі даних.
Коли ви перетягуєте повзунки, зверніть увагу на червоні пунктирні лінії — це окремі помилки в кожній точці даних, що показує, наскільки прогноз відрізняється від фактичного значення. Ми обчислюємо похибку для кожної точки як .
Різні функції втрат узагальнюють ці помилки різними способами:
- Середня квадратична помилка (MSE) — для регресії (прогнозування чисел). Зводить у квадрат кожну помилку та усереднює їх.
- Cross-Entropy — для класифікації (прогнозування категорій). Вимірює, наскільки далекі прогнозовані ймовірності від справжніх міток.
- Середня абсолютна похибка (MAE) — як MSE, але використовує абсолютні значення замість квадратів, менш чутливі до викидів.
Оскільки ми підбираємо лінію — завдання, відоме як регресія (передбачення безперервного числа), — ми використали середню квадратичну помилку (MSE): візьміть кожну помилку, зведіть її в квадрат, а потім усередніть усі. Зведення в квадрат робить помилки невід’ємними, тому додатні та від’ємні помилки не можна скасувати, а більшим помилкам надається більша вага:
Давайте застосуємо цю формулу до наших даних. Скажімо, ваша поточна припущення , , отже, . Для кожної з наших 5 точок даних ми обчислюємо прогноз, помилку (наскільки далеко) і квадрат помилки:
| помилка | помилка² | |||
|---|---|---|---|---|
| -2 | -3 | -5 | -2 | 4 |
| -1 | -1 | -2 | -1 | 1 |
| 0 | 1 | 1 | 0 | 0 |
| 1 | 3 | 4 | 1 | 1 |
| 2 | 5 | 7 | 2 | 4 |
| означає → | збиток = 2,0 |
MSE становить 2,0. Виймання його квадратного кореня дає середньоквадратичну помилку (RMSE), , у тих самих одиницях, що й передбачення. Це відрізняється від середньої абсолютної похибки, яка тут . У і усі помилки та обидва вимірювання дорівнюють нулю.
Нам вдалося знайти параметри вручну для нашої простої функції з 2 параметрами, але уявіть, що ми робимо це з 25 параметрами, не кажучи вже про мільйони. Пізніше ми застосуємо все з цієї статті до реального завдання — навчання мережі класифікації рукописних цифр із понад 100 000 параметрами. Ручне налаштування неможливе в такому масштабі — жодна людина не зможе досліджувати простір з мільйонами вимірів. Нам потрібен систематичний спосіб розглядати втрати та математично з’ясувати в якому напрямку рухати кожен параметр, щоб зменшити його. Це саме те, що зворотне розповсюдження та градієнтний спуск роблять разом: зворотне розповсюдження обчислює, у який спосіб налаштувати кожен параметр, і градієнтний спуск робить невеликий крок у цьому напрямку. Ми повторюємо цей процес, щоб зменшити втрати.
Лінійні найменші квадрати також можна розв’язувати безпосередньо за допомогою нормальних рівнянь або матричної факторизації. Це дає мінімум цільової квадратичної помилки; це не гарантує ідеальної відповідності довільним даним. Загальні нейронні мережі не мають аналогічного прямого рішення, тому ми використовуємо ітераційні оптимізатори, такі як градієнтний спуск.
Навчальний цикл, крок за кроком
Давайте спочатку подивимося, як працює цей автоматичний алгоритм. Наведений нижче віджет дає змогу виконувати зворотне поширення та градієнтний спуск для нашого завдання крок за кроком і спостерігати за всім, що відбувається:
- Лівий графік: точки даних (зелені крапки) і лінія прогнозу моделі (синя) на основі поточних значень і . Червоні пунктирні лінії показують похибку в кожній точці — різницю між прогнозом і фактичним значенням. Ці помилки зводяться в квадрат і усереднюються, щоб отримати втрату MSE.
- Права діаграма: втрати після кожного оновлення. За стандартної швидкості навчання вона зменшується до нуля. Плато може означати конвергенцію; зростання втрат може означати, що рівень навчання занадто великий.
Натисніть Крок, щоб запустити одне оновлення градієнтного спуску, або Крок x10, щоб запустити десять одночасно.
Backpropagation
Gradient Descent
▶ Computation (step 0)
Зберігайте швидкість навчання на рівні за замовчуванням 0,1 і кілька разів натисніть Крок. Втрати зменшуються, коли лінія наближається до даних. Розгорніть Обчислення, щоб перевірити передбачення, помилки та градієнти.
У цьому прикладі градієнти зменшуються, коли параметри наближаються до мінімуму, тому оновлення lr * dw і lr * db зменшуються, навіть якщо lr залишається фіксованим. Параметри сходяться до w = 2, b = 1. Швидкість навчання має значення: більше значення може завадити конвергенції, як показує наступний віджет.
Кожне натискання кнопки «Крок» запускає одну повну ітерацію навчання — чотири кроки, що відповідають циклу навчання, який ми описали раніше і виглядає так у Python:
# 1. forward pass
y_pred = w * x + b
# 2. loss computation
error = y_pred - y
loss = np.mean(error ** 2)
# 3. backpropagation
dw = 2 * np.mean(error * x)
db = 2 * np.mean(error)
# 4. gradient descent
w = w - lr * dw
b = b - lr * dbКроки 1 і 2 створюють прогнози та обчислюють їх MSE. Крок 3 обчислює градієнти втрат. Крок 4 оновлює кожен параметр шляхом віднімання його градієнта, помноженого на швидкість навчання, lr. Спочатку ми перевіримо швидкість навчання, а потім виведемо dw і db.
Вибір швидкості навчання
У розділі обчислень вище ви можете побачити, що градієнтний спуск оновлює такі параметри:
w = w - lr * dw
b = b - lr * dbГрадієнт (dw, db) повідомляє нам, в якому напрямку рухати кожен параметр і на скільки відносно інших. Але як далеко ми повинні зробити крок? Це те, що контролює швидкість навчання (lr) — вона масштабує кожен градієнт перед його застосуванням.
Для диференційованих втрат з ненульовим градієнтом від’ємний градієнт є місцевим напрямком вниз. Кінцевий крок все ще може перевищити і збільшити втрати. У цьому конкретному прикладі:
- Повільна конвергенція (спробуйте 0,01): невеликі оновлення потребують багатьох кроків, щоб наблизитися до мінімуму.
- Стійка конвергенція (спробуйте 0,1): втрати зменшуються на кожному кроці.
- Зменшення коливань (спробуйте 0,4):
wперетинає оптимум на кожному кроці, але відстань зменшується. - Стійкі коливання (спробуйте 0,5):
wчергується між -3 і 7. Зміщення досягає 1 одразу, але втрати залишаються на рівні 50 після першого кроку. - Розбіжність (спробуйте 1,0 або 1,5):
wперевищує величину, що збільшується, і втрата зростає.
Спробуйте самі — змініть швидкість навчання та натисніть Крок x10, щоб побачити ефект:
Стабільний діапазон швидкості навчання залежить від втрати. Для цього набору даних конвергенція з довільної початкової точки вимагає додатного рівня навчання нижче 0,5; ми побачимо чому, коли спростимо формулу втрат.
Більші мережі потребують налаштування швидкості навчання для моделі, даних і оптимізатора. Розклад темпів навчання змінює його в порівнянні з навчанням. Адаптивні оптимізатори, такі як Adam, також використовують історію градієнтів для масштабування оновлень параметрів. Ці варіанти змінюють правило оновлення, зберігаючи етапи прямого проходу, обчислення втрат і зворотного поширення.
Обчислення зворотного поширення
У розділі обчислень вище ви можете побачити, що зворотне поширення обчислює градієнти таким чином:
dw = 2 * np.mean(error * x)
db = 2 * np.mean(error)У цих двох рядках зібрано багато чого. Чому ми множимо error на x для dw, але не для db? Звідки береться 2? Яке відношення має mean? Розпаковуємо його крок за кроком.
Пам’ятайте, що втрати обчислюються на основі прогнозів, а прогнози залежать від w і b.
Зрештою, втрата є функцією параметрів — змініть w або b, і втрата зміниться.
Перетягніть w або b у віджет нижче та спостерігайте за зміною втрати — біла крапка рухається вздовж кривої, показуючи, де саме ви знаходитесь на ландшафті втрати:
Перетягніть w: точка рухається вздовж лівої кривої, тоді як права крива зміщується вертикально. Зміна b робить зворотне. Для цього центрованого набору даних мінімум уздовж осі w залишається 2, а мінімум уздовж осі b залишається 1. Тут параметри можна оптимізувати незалежно; ми виведемо причину нижче.
Формули градієнта виходять із похідної цих кривих — вимірювання того, наскільки зміниться втрата, коли ви трохи змінюєте кожен параметр.
Отже, 2 * mean(error * x) є просто похідною функції втрат відносно w.
Щоб зрозуміти, як ми переходимо від функції втрат до 2 * mean(error * x), нам потрібні три концепції, які будуються одна на одній:
- Похідні — що означає вимірювати, як змінюється функція
- Правило ланцюжка — як обчислити похідні, коли функції з’єднані разом
- Часткові похідні та градієнти — як обробляти кілька параметрів одночасно
Наприкінці ми точно простежимо, звідки походить кожна частина цієї формули. Почнемо з того, що насправді таке похідна.
Формула, яку ми щойно бачили —
2 * mean(error * x)— специфічна для втрат MSE із лінійною моделлю. Різні функції втрат і архітектури створюють різні формули градієнта, але математичні принципи, що лежать в основі, завжди однакові. Для нашої простої моделі ми можемо вивести формулу вручну; для глибоких мереж із мільйонами параметрів такі фреймворки, як PyTorch і TensorFlow, автоматично обчислюють похідні за допомогою autograd (автоматична диференціація).
Похідна: нахил у точці
Похідна відповідає на одне запитання: якщо я трохи підштовхну цей вхід, наскільки зміниться вихід? Подумайте про це як про нахил кривої в одній точці. Якщо ви стоїте на пагорбі, похідна говорить вам, наскільки крута земля під вашими ногами — і в якому напрямку вона йде вниз.
Візьмемо таку просту функцію, як f(x) = x². Перетягніть точку x уздовж кривої та спостерігайте, як змінюються нахил і похідна:
▼ Computation
Установіть x = 2 і dx = 0.5 у віджеті. Жовта лінія () — це підштовх до вхідних даних, зелена лінія () — це те, наскільки змінюється вихід у відповідь. У розділі «Обчислення» під діаграмою показано, як вони поєднуються в похідну.
Спочатку ми оцінюємо функцію в нашій точці: . Потім ми підштовхуємо введення dx і знову оцінюємо: . Різниця говорить нам, наскільки змінився результат: . Поділ на поштовх дає нам швидкість зміни: .
Це співвідношення (4,5) є приблизно похідною при x = 2 — воно говорить вам про швидкість: на цьому етапі вихід змінюється приблизно в 4 рази швидше, ніж вхід. Це не зовсім 4, оскільки dx = 0.5 все ще є великим поштовхом. Тепер давайте зменшимо dx — спробуйте перетягнути його до 0.1:
- — ближче до
4
Похідна є межею цього відношення, коли вхідна зміна зменшується до нуля:
– це зміна виходу (). Поділіть на , щоб отримати співвідношення. Частина просто означає «зменшити до нуля» — саме те, що ви робили за допомогою повзунка, спостерігаючи, як співвідношення збігається до точного значення.
Для ми можемо вирішити це:
Як , це просто . Отже, .
Отримавши формулу похідної, ми можемо оцінити безпосередньо замість того, щоб оцінювати нахил із дедалі меншими змінами вхідних даних.
Для іншого візуального вступу див. Сутність числення 3Blue1Brown.
Правило ланцюга: обчислення похідних комбінованих функцій
Ми знаємо, як отримати похідну простої функції, наприклад . Але що відбувається, коли одна функція переходить в іншу? Це називається композицією функції — і це саме те, що робить наше обчислення:
y_pred = w * x + b # prediction
error = y_pred - y # how far off
loss = np.mean(error ** 2) # squared error, averagedЩоб отримати градієнт, спочатку розглянемо одну точку даних. Його квадрат помилки залежить від w через три операції: передбачення, віднімання цілі та зведення у квадрат. Після диференціації цього ланцюга ми будемо усереднювати набір даних.
Отже, ми бачимо, що обчислення втрат від w не є однією функцією — це ланцюжок із трьох функцій, кожна з яких передає свій вихід у наступну:
Прописано:
- — прогноз моделі
- — як далеко ми
- — квадрат помилки (те, що ми хочемо мінімізувати)
Втрата — три функції, вкладені одна в одну.
Ми можемо знайти похідну кожної окремої функції, але як поєднати їх, щоб отримати похідну всього ланцюга? Відповідь – правило ланцюга: помножте локальні похідні разом.
Запам’ятайте формулу похідної:
.
Коефіцієнт скінченної зміни наближається до похідної в межі. Ми записуємо цю похідну як , з виходом зверху і входом знизу. Для трьох функцій у нашій мережі:
- : вихід , вхід →
- : вихід , вхід →
- : вихід , вхід →
Використовуючи цю нотацію, правило ланцюга розширюється до:
Чому множення? Оскільки кожна функція вкладена в наступну, вихід однієї стає входом іншої. Подумайте про це як про ланцюжок підштовхувань: якщо ви трохи підштовхнете w, y_pred зміниться на x разів більше. Тоді error змінюється в 1 разів, незалежно від зміни y_pred. Для досить малої зміни error² змінюється приблизно на 2·error, помножену на зміну error. Кожна ланка в ланцюжку масштабує поштовх — і масштабування поєднується шляхом множення.
Три способи поєднання функцій
Існує три основні способи поєднання двох функцій і , і кожен має своє правило похідні комбінують:
-
Додавання: — похідні додавання. Якщо змінюється на 3, а змінюється на 5, сума змінюється на 8. Це правило суми: .
-
Множення: — це складніше, оскільки обидва фактори можуть змінюватися. Це правило продукту: . Ви повинні враховувати кожну зміну функції в той час як інший залишається постійним.
-
Композиція (вкладання): — вихід надходить у . Похідні множаться. Це є правило ланцюжка: . Підштовхування до масштабується , а потім ця масштабована зміна отримує знову масштабується .
Наше обчислення втрат є композицією — — тому ми множимо похідні. Якщо натомість функції були додані або помножені разом, ми використаємо відповідне правило. На практиці нейронні мережі використовуйте всі три: додавання (терми зміщення), множення (ваги, помножені на вхідні дані), і композиція (шари, що подаються в один одного). Зворотне поширення застосовує те правило, яке відповідає кожній операції.
Гаразд, давайте обчислимо комбіновану похідну нашого ланцюжка втрат. Ми показали, як знайти похідну від , що дало нам . Той самий підхід працює для простіших функцій: похідна від – це просто (постійний множник), а похідна від – (віднімання константи не змінює швидкість). Це робить обчислення кожної окремої похідної простою:
- Щоб обчислити , ми використовуємо той факт, що похідною від є . Оскільки , похідна
x. - Щоб обчислити , ми використовуємо той факт, що похідною від є . Оскільки , похідна
1. - Щоб обчислити , ми використовуємо той факт, що похідною від є . Оскільки функція , похідна
2 · error.
Що дає нам:
Це для однієї точки даних.
Простежимо це за допомогою дійсних чисел. З w = 3, b = 1 візьміть точку даних x = 2, y = 5:
w = 3
↓ × x = ×2
y_pred = 3·2 + 1 = 7
↓ × 1
error = 7 - 5 = 2
↓ × 2·error = ×4
error² = 4Правило ланцюга: . За невеликої зміни квадрат помилки змінюється приблизно на . Це локальне наближення: збільшення w з 3 до 4 змінює квадрат помилки з 4 до 16, збільшення на 12, а не на 8.
Але ми маємо 5 точок даних, а не одну. Оскільки MSE усереднює квадрати помилок для всіх точок даних, нам також потрібно усереднити похідні. Для кожної точки ми обчислюємо :
| -2 | -3 | -5 | -2 | |
| -1 | -1 | -2 | -1 | |
| 0 | 1 | 1 | 0 | |
| 1 | 3 | 4 | 1 | |
| 2 | 5 | 7 | 2 |
Усередніть їх: . Отже, dw = 4 — градієнт говорить нам, що втрати збільшуються, коли ми збільшуємо w, тому ми повинні зменшити їх. (І справді, справжнім значенням є w = 2, що менше, ніж наше припущення 3.)
У математичній нотації це:
І в Python:
dw = 2 * np.mean(error * x)Наведений нижче віджет дозволяє відстежувати цей ланцюжок для кожної точки даних. Натисніть різні кнопки x=, щоб побачити, як змінюються локальні похідні — зверніть увагу, як правило ланцюга дає різне значення для кожної точки, оскільки x і error різні:
Для db це той самий ланцюжок, але інший:
оскільки , похідна відносно b є просто 1 (замість x). Отже:
А в нашому коді Python це виглядає так:
db = 2 * np.mean(error)Ось чому правило ланцюжка має значення: щоразу, коли втрата обчислюється за допомогою послідовності операцій (а це завжди), вам потрібно відстежити, як кожен параметр вплинув на кінцевий результат. Для нашої 2-параметричної моделі ланцюг має 3 кроки. Глибока нейронна мережа може мати сотні — по одній на кожен шар, — але принцип ідентичний: кожен шар — це ще одна функція в композиції, ще одна локальна похідна для множення.
Застосування похідних до нашої функції втрат
Тепер, коли ми знаємо, як обчислювати окремі похідні та комбінувати їх за допомогою правила ланцюга, давайте застосуємо ці знання до нашої проблеми. «Крива», яку ми хочемо мінімізувати, є нашою функцією втрат — формулою, яку ми обрали для вимірювання помилки. Ми точно знаємо цю функцію:
loss = np.mean((w * x + b - y) ** 2)Ми не знаємо, які значення w і b роблять його найменшим. Похідна допомагає нам дізнатися:
він повідомляє нам, якщо я збільшую w на крихітну суму, збиток зростає чи зменшується? І як швидко?
Дані (x і y) фіксовані — це дані нашого навчання. Якщо ми також залишимо b постійним на даний момент (скажімо, b = 3), тоді втрати стають функцією лише w, і ми можемо побудувати це як просту криву. Наприклад, у w = 0:
y_pred = w * x + b # 0 * [-2,-1,0,1,2] + 3 = [3, 3, 3, 3, 3]
error = y_pred - y # [3,3,3,3,3] - [-3,-1,1,3,5] = [6, 4, 2, 0, -2]
loss = np.mean(error ** 2) # mean([36, 16, 4, 0, 4]) = 12.0Це дає нам одну точку на кривій: (w=0, loss=12). Зробіть це для кожного w від -5 до 5 (зберігаючи b = 3 фіксованим), і ми отримаємо повну картину — втрати як функцію лише w:
Вісь x – w, а вісь y – втрати. Мінімум виникає на w = 2, де втрата становить 4, оскільки b все ще фіксується на 3. Кожне передбачення на 2 перевищує цільове значення. Перетягніть повзунок, щоб перевірити прогнози, помилки та квадратичні помилки, які утворюють кожну точку на кривій.
Ми можемо зробити те саме для b — цього разу виправивши w = 2 і змінивши b від -5 до 5:
Та сама форма параболи, але тепер вісь x b. Мінімум знаходиться на b = 1, де втрата падає до нуля. Разом w = 2 і b = 1 є точними параметрами, які згенерували наші дані — y = 2x + 1.
Ці графіки оцінюють втрати при багатьох значеннях параметрів, щоб ми могли бачити їх форму. Дослідження всіх комбінацій стає недоцільним, оскільки кількість параметрів зростає. Натомість градієнтний спуск обчислює похідні за поточними параметрами та використовує їх для вибору наступного кроку.
Часткові похідні
Зверніть увагу на те, що ми щойно зробили: щоб зрозуміти, як втрата залежить від w, ми заморозили b і змінили тільки w. Щоб зрозуміти, як це залежить від b, ми заморозили w і змінили лише b. Це саме те, що таке часткова похідна — похідна втрати за одним параметром, а всі інші залишаються фіксованими:
- ∂loss/∂w — як змінюється втрата, коли ви підштовхуєте
w(із замороженимb) - ∂loss/∂b — як змінюється втрата, коли ви підштовхуєте
b(із замороженимw)
Кожна з двох наведених вище кривих є зрізом ландшафту втрат уздовж одного параметра. Нахил цієї кривої в будь-якій точці є частковою похідною:
dw = 2 * np.mean(error * x) # ∂loss/∂w — how loss changes with w
db = 2 * np.mean(error) # ∂loss/∂b — how loss changes with bНаведений нижче віджет є комбінованим видом двох кривих, які ми бачили вище, тепер показуючи часткові похідні в дії. Лівий графік змінюється w (тримає b фіксованим) — правий графік змінюється b (тримає w фіксованим). На кожній діаграмі біла крапка вказує на те, де ви зараз знаходитесь, блакитна пунктирна лінія є дотичною (її нахил є частковою похідною), а зелена стрілка показує, у якому напрямку рухатися, щоб зменшити втрати.
Перетягніть повзунки та спостерігайте, що відбувається:
- Далеко від мінімуму — крива крута, дотична різко нахилена, а похідна — велике число. Градієнтний спуск тут робить великі кроки.
- Поблизу мінімуму — крива вирівнюється, дотична майже горизонтальна, а похідна близька до нуля. Кроки стають крихітними — модель налаштовується.
- Мінімум — дотична ідеально плоска. Похідна дорівнює нулю. Діватися нікуди — приїхали.
Для наших п’яти точок даних, , середнє значення дорівнює нулю, а середнє значення дорівнює 2. Розширення втрати дає:
Перехресний член зникає, оскільки сума вхідних даних дорівнює нулю. Це пояснює, чому зміна w зсуває криву b вертикально без переміщення її мінімуму, і навпаки. Похідні спрощуються до:
Після одного оновлення градієнтного спуску відстані від оптимальних стають:
Обидві відстані зменшуються, коли . У lr = 0.5 перший множник дорівнює -1, тому w коливається без наближення. У lr = 1 це -3, так що відстань потроюється за кожен крок.
Ця незалежність є властивістю нашого центрованого лінійного прикладу. З іншими наборами даних і в багатошарових мережах градієнт одного параметра зазвичай залежить від інших. Щоб зробити крок градієнтного спуску, обчисліть усі градієнти за поточних параметрів, а потім застосуйте оновлення.
Від похідної до градієнта
Градієнт — це просто вектор усіх частинних похідних, об’єднаних разом: [dw, db]. Він вказує в напрямку найкрутішого зростання втрат. Тож ми рухаємось у протилежному напрямку — тому правило оновлення віднімає: w = w - lr * dw.
За допомогою двох параметрів ми можемо побудувати втрату як тривимірну поверхню: w на одній осі, b на іншій і втрату як висоту. Наша втрата утворює опуклу чашу з унікальним мінімумом на w = 2, b = 1. Кожна попередня крива є зрізом цієї поверхні. Спробуйте Крок (обидва) з різних вихідних точок; з фіксованою швидкістю навчання віджета 0,1, параметри наближаються до того самого мінімуму.
Спробуйте клацнути Крок (w) і Крок (b) окремо — ви побачите, як точка переміщається вздовж однієї осі за раз, створюючи сходи вниз по чаші. Потім спробуйте Step (both) — це те, що робить справжній градієнтний спуск, оновлюючи обидва параметри одночасно. Ви можете обертати поверхню, перетягуючи, щоб побачити її з різних кутів.
Жовта стрілка показує напрямок оновлення: його компоненти вздовж осей параметрів пропорційні [-dw, -db], негативному градієнту. Його висота відповідає зміні втрат. Крок (w) і Крок (b) переміщуються вздовж однієї осі параметрів; Крок (обидва) поєднує обидва оновлення.
У початковій точці w = -3, b = 3 градієнти dw = -20 і db = 4, тому оновлення просувається далі вздовж осі w. Відносні розміри залежать як від кривизни втрат, так і від поточних значень параметрів. На w = 2, b = 3, наприклад, dw = 0 і змінюється лише зміщення.
Градієнт вимірює локальну чутливість, а не те, наскільки параметр заслуговує на помилку. Невеликий градієнт може виникнути поблизу мінімуму, але він також може виникнути в плоскій області з високими втратами.
За межами чаші: чому глибокі мережі неопуклі
Наша квадратична помилка є опуклою, тому що це сума квадратів виразів, афінних за параметрами. Для цього набору даних має унікальний мінімум. Квадратичні в цілому не обов’язково повинні бути опуклими: , наприклад, вигинається вниз.
У багаторівневій мережі параметри послідовних рівнів взаємодіють через продукти та активації, тому втрати, як правило, є невипуклими. Це можуть показати навіть два скалярних лінійних шари. Нехай їхні ваги будуть і , їхні вхідні дані дорівнюють 1, а цільовий показник дорівнює 1:
І , і мають нульові втрати. Їх середина має втрату 1. Опукла функція не може мати значення середини більше, ніж середнє значення кінцевої точки, тому ця втрата не є опуклою. Нелінійні активації додають додаткову структуру, але вони не потрібні для невипуклості в параметрах.
Такі ландшафти можуть містити локальні мінімуми, сідлові точки та плоскі області. Сам по собі нульовий градієнт не означає, що ми знайшли найкраще рішення, а спуск градієнта не гарантує глобального мінімуму. Розділ про оптимізацію підручника Deep Learning описує ці проблеми більш детально.
Навчання спрямоване на пошук параметрів, які роблять корисні прогнози на основі нових даних. Ми контролюємо продуктивність перевірки, а також втрату навчання. Рання зупинка та регулярізація можуть допомогти обмежити переобладнання; вони не перетворюють неопуклу втрату на опуклу.
Стохастичний градієнтний спуск
Поки що ми обчислювали градієнти, використовуючи всі наші навчальні дані одночасно. Щоразу, коли ви натискаєте «Крок» у віджеті вище, dw = 2 * mean(error * x) обчислює 2 * error * x для кожної з наших 5 точок даних окремо, а потім усереднює їх у єдиний градієнт:
dw = 2 * mean(error * x)
= 2 * mean([-24.00, -7.00, 0.00, -3.00, -16.00])
= -20.00Використання всіх 5 балів дає точний градієнт наших тренувальних втрат. Це не робить оновлення захищеним від викидів: велика помилка все одно може домінувати в градієнті MSE. У великому наборі даних обробка кожного прикладу перед кожним оновленням коштує дорого. Міні-партії дозволяють оновлювати параметри після обробки меншої підмножини.
Перемішайте навчальні приклади, а потім розділіть їх на міні-пакети. Запустіть 4-етапний навчальний цикл для кожної партії: прохід вперед, обчислення втрат, зворотне поширення та оновлення параметрів. Кожне оновлення використовує лише приклади з цієї групи.
Це стохастичний градієнтний спуск (SGD). «Стохастик» просто означає випадковий — маючи на увазі випадкове перемішування. Правило оновлення те саме, усереднення за міні-пакетом замість повного набору даних:
Тут — поточна міні-партія. Його градієнт є оцінкою градієнта повного набору даних за поточних параметрів. Окреме оновлення може збільшити втрату повного набору даних. Оскільки параметри змінюються між пакетами, одна епоха міні-пакетного оновлення не еквівалентна одному повному пакетному оновленню.
Після того, як ви пройшли через кожен приклад, це одна епоха. Перемішайте ще раз і почніть наступну епоху. Ось чому ви бачите «епоху» в журналах навчання — кожна епоха означає, що модель бачила кожен приклад у наборі даних рівно один раз.
Для наших 5 точок даних із розміром партії 2 це виглядає так:
| Епоха | Перетасовані дані | Партія 1 | Партія 2 | Партія 3 |
|---|---|---|---|---|
| 1 | [0, 2, -1, -2, 1] | (0, 2) | (-1, -2) | (1) |
| 2 | [2, -2, 1, 0, -1] | (2, -2) | (1, 0) | (-1) |
| 3 | [-1, 1, -2, 2, 0] | (-1, 1) | (-2, 2) | (0) |
Кожна партія запускає повний 4-кроковий цикл (прямий перехід → втрати → зворотне поширення → градієнтний спад), тому кожна епоха виконує 3 оновлення параметрів замість 1. До кінця кожної епохи кожна точка даних була використана рівно один раз — перемішування змінює, які приклади групуються разом, і зменшує залежність від їх початкового порядку.
Наведений нижче віджет запускає обидва методи поруч із нашим 5-точковим набором даних, щоб ви могли порівнювати їх безпосередньо. Обидва починаються з однакових параметрів (w = -3, b = 3) і використовують однакову швидкість навчання. Кожне клацання «Кроку» оновлює один параметр для кожного методу.
Синя лінія (повна партія) використовує всі 5 точок на кожному кроці.
Помаранчева лінія (міні-партія) використовує лише точки batch_size
— помаранчеві кружечки показують, які з них, а смужка епохи відстежує прогрес у наборі даних.
Кілька разів натисніть «Крок» і спостерігайте за кривими втрат праворуч. Синя крива (повна партія) плавно спадає зі швидкістю навчання за замовчуванням. Помаранчева крива (міні-партія) зигзагоподібна.
За замовчуванням увімкнено фіксований порядок, тому партії є однаковими під час кожного запуску, що робить цей зигзагоподібний візерунок відтворюваним. Зніміть цей прапорець, щоб випадково перемішувати кожну епоху — помаранчева крива щоразу виглядатиме по-різному, але загальна поведінка однакова.
Клацніть кілька перших кроків, щоб зрозуміти, чому: на кроці 1→2 втрати падають (випадково партія дала хороший градієнт), але на кроці 2→3 втрати збільшуються — цей пакет змінив параметри в напрямку, який допоміг його власним очкам, але зашкодив іншим. Потім крок 3→4 знову падає. Це нормально: кожен пакет бачить лише частину даних, тому деякі кроки перевищують або навіть йдуть неправильно. З налаштуваннями за замовчуванням у цьому прикладі загальна тенденція спрямована на зниження втрат, незважаючи на ці індивідуальні збільшення.
Стандартний повний пакетний запуск наближається до рішення за меншої кількості оновлень, але кожне оновлення обробляє більше прикладів. Порівняйте вартість оновлення 3 параметрів із нашими 5 точками даних:
- Повна партія (3 кроки): кожен крок використовує всі 5 балів. Це 3 × 5 = 15 обчислень точок даних. Кожна точка обробляється 3 рази.
- SGD із розміром партії 2 (3 кроки = 1 епоха): кожен крок використовує лише 2 точки (або 1 для останньої партії). Це 2 + 2 + 1 = 5 обчислень точок даних. Кожна точка обробляється один раз.
Обидва методи виконують 3 оновлення параметрів, але міні-пакетний запуск обробляє на третину менше прикладів. Це зараховує роботу, а не прогрес до фіксованого збитку: два методи не обов’язково досягають однакових збитків після цих оновлень. Для більших наборів даних корисним порівнянням є час для досягнення цільової продуктивності перевірки, яка також залежить від апаратного забезпечення та розміру пакета.
Ви також можете спробувати різні розміри партії, щоб побачити різницю в поведінці:
- розмір партії = 1 — кожен крок використовує одну точку, тому оновлення можуть суттєво відрізнятися між прикладами. 5 кроків = 1 епоха (кожна точка побачена один раз). Це оригінальний “стохастичний” градієнтний спуск.
- розмір партії = 2 — менше шуму, кожна епоха займає 3 кроки (2 + 2 + 1 залишок). Це ближче до того, що використовується на практиці.
- розмір пакету = 5 — це всі наші дані в одному пакеті, тому це ідентично повному пакетному градієнтному спуску. Обидві лінії ідеально перекриваються. 1 крок = 1 епоха.
Розмір пакету враховує градієнтний шум, використання пам’яті та ефективність апаратної обробки прикладів. Менші партії дозволяють оновлювати більше даних за один прохід; більші партії усереднюють більше прикладів на оновлення. Шум іноді може допомогти оптимізувати, але він не гарантує виходу з локального мінімуму або конвергенції до глобального.
Правило ланцюга на багатьох рівнях
Пам’ятаєте, як зворотне поширення використовує правило ланцюга для множення локальних похідних?
У нашому прикладі був найпростіший випадок: один нейрон з однією вагою w, одним зміщенням b і одним входом:
Ланцюжок від кожного параметра до втрати мав 3 ланки , які виглядали так у Python:
y_pred = w * x + b # f1: prediction
error = y_pred - y # f2: how far off
loss = error ** 2 # f3: squared error
dw = x * 1 * (2 * error) # chain rule for w: f'1 · f'2 · f'3
db = 1 * 1 * (2 * error) # chain rule for b: same chain, different f'1Але реальні мережі мають кілька нейронів на шар, кожен зі своєю вагою. Обчислення градієнта не змінюється — ми все ще обчислюємо часткову похідну для кожної окремої ваги за допомогою правила ланцюга, як і раніше. Різниця в масштабах. Ланцюжок стає довшим для ваг, які розташовані далі від втрати — більше шарів для проходження, більше похідних для множення. І він стає ширшим — вихід нейрона може передаватися багатьом нейронам наступного шару, тому градієнт повинен підсумовувати внески всіх цих шляхів.
Наступна діаграма має два прихованих шари по 3 нейрони кожен і скалярний вихід. Перемикайте кнопки, щоб порівняти вагу в другому прихованому шарі з вагою в першому:
Ми будемо використовувати ReLU як у прихованих шарах, так і в активації ідентичності на виході. Для прикладу навчання визначте передачу вперед як:
Тут має форму , має форму , а вихідний вектор ваги має 3 записи. Вектори і містять зважені суми до активації; і утримують виходи після активації. на схемі з’єднує з . Його з’єднує з . Ми також будемо називати ваги від до і і : ці три ваги утворюють перший стовпець .
Градієнт для v₁ (шар 2) починає діяти. Ця вага впливає на втрату через , і . Записуючи , його похідна є:
Інші вхідні дані множаться на різні ваги, тому вони не відображаються в . Вони все ще впливають на і прогноз, який використовується для оцінки похідної. Зауважте, що : вихідний зв’язок сприяє своїй вазі, навіть якщо ми розрізняємо параметр на попередньому рівні.
Тепер виберіть Градієнт для w₁ (шар 1). Зміна змінює , який живить усі три нейрони у другому прихованому шарі. Ми повинні додати внески всіх трьох шляхів:
Кожен шлях вносить продукт місцевих похідних. Там, де шляхи розгалужуються, зворотне поширення сумує їхні внески. Це дає наступний розрахунок для двох виділених ваг:
# All values come from the same forward pass; indices start at 0.
# ReLU's derivative is 1 for positive inputs, 0 for negative inputs.
# At exactly zero, we use 0 as the implementation convention.
d_output = 2 * error
# Gradients with respect to the second layer's pre-activations q.
d_q = (q > 0) * u * d_output # shape: (3,)
# v₁ is W2[0, 0]: h₁ -> g₁.
dv1 = h[0] * d_q[0]
# Sum the three paths from h₁ through the second layer.
d_h1 = np.dot(W2[:, 0], d_q)
# w₁ is W1[0, 0]: x₁ -> h₁.
dw1 = x[0] * (z[0] > 0) * d_h1Ми можемо обчислити градієнти для кожної ваги та зміщення за допомогою тих самих операцій. Цей код завершує зворотне поширення для одного прикладу, а потім оновлює всі параметри:
# Output layer: y_pred = u @ g + c
du = g * d_output # 3 weights
dc = d_output # scalar bias
# Second hidden layer: g = ReLU(W2 @ h + b2)
dW2 = np.outer(d_q, h) # shape: (3, 3)
db2 = d_q
# First hidden layer: h = ReLU(W1 @ x + b1)
d_z = (z > 0) * (W2.T @ d_q)
dW1 = np.outer(d_z, x) # shape: (3, 2)
db1 = d_z
# Apply updates only after every gradient has been computed.
u = u - lr * du
c = c - lr * dc
W2 = W2 - lr * dW2
b2 = b2 - lr * db2
W1 = W1 - lr * dW1
b1 = b1 - lr * db1Для міні-серії з втратою MSE усередніть ці градієнти для кожного прикладу перед оновленням. Зворотне поширення повторно використовує проміжні градієнти, такі як d_q і d_z, тому йому не потрібно відстежувати кожен шлях окремо для кожного параметра.
Додавання шарів вводить більше ваги та факторів активації на кожному шляху. Повторне множення може зробити градієнт набагато меншим або більшим до моменту, коли він досягне раннього шару.
Проблема зникаючого градієнта
У скалярному ланцюжку, якщо кожен шар має похідну 0,5, проходження через 100 шарів множить вхідний градієнт на . Тоді ранній рівень отримує дуже слабкий сигнал для оновлення своїх параметрів. Це проблема зникаючого градієнта. Більш широкі мережі використовують матричні добутки та суми за шляхами, але повторне скорочення може мати той самий ефект.
Натомість повторне посилення може спричинити вибухові градієнти, що призведе до нестабільних оновлень.
Наведений нижче віджет дає змогу побачити це в дії. Перетягніть локальну похідну нижче 1 і спостерігайте, як градієнт зникає, коли він тече назад через шари. Потім спробуйте значення вище 1 і подивіться, як воно вибухне:
Похідна сигмоїди становить щонайбільше 0,25, тому повторювані похідні сигмоїди можуть суттєво зменшити градієнт. Вага матриці також впливає на його розмір; самі по собі похідні активації не визначають, чи зникає повний градієнт або розривається.
Кілька варіантів дизайну допомагають навчити глибші мережі:
- Активація ReLU має похідну 1 для позитивних вхідних даних, тому активація не зменшує градієнт на її активній гілці. Для негативних вхідних даних його похідна дорівнює 0, що блокує градієнт через цей нейрон. Вагові матриці все ще можуть посилювати або зменшувати сигнал.
- Залишкові з’єднання додають ідентифікаційний шлях навколо блоку шарів. Для блоку похідна включає ідентичний термін, а також похідну від . Це забезпечує додатковий маршрут для градієнтів. Див. статтю ResNet.
- Пакетна нормалізація нормалізує проміжні значення за допомогою статистики міні-серії та вивчає масштаб і зсув. Це може полегшити оптимізацію, але воно не змушує похідні дорівнювати 1 і не гарантує стабільні градієнти. Див. документ нормалізації пакетів.
Налаштування ініціалізації, архітектури та оптимізатора впливають на поширення градієнта. Ці методи стосуються різних частин проблеми; ніхто не гарантує, що кожен градієнт залишається в корисному масштабі.
Готові застосувати все це на практиці? У наступна стаття ми створюємо та навчаємо справжню нейронну мережу на MNIST — записуємо пряму передачу, зворотне поширення та градієнтний спуск з нуля в NumPy, а потім порівнюємо це з реалізацією Keras.