Від нахилу до зворотного поширення: створюємо невеликий рушій автоматичного диференціювання

У нашій попередній статті про те, як нейронні мережі навчаються, ми пройшли через базовий навчальний цикл: модель робить прогноз, ми вимірюємо, наскільки вона неправильна, використовуючи функцію втрат, ми налаштовуємо ваги, а потім повторюємо. Щоб правильно відрегулювати кожну вагу, нам потрібно знати, чи зменшить її втрату збільшення чи зменшення. Один простий спосіб це з’ясувати – злегка підняти або опустити вагу, перерахувати втрату та побачити, чи покращилася вона. Проблема в тому, що реальні моделі можуть мати мільйони чи мільярди таких ваг. Тестування кожного окремо було б неймовірно повільним.

Ось де на допомогу приходять похідні. Вони дають нам швидкий шлях, не перевіряючи кожну можливу зміну. Для кожної ваги похідна говорить нам, наскільки втрата чутлива до невеликої зміни цієї ваги, припускаючи, що всі інші ваги залишаються незмінними. Якщо похідна додатна, ми знаємо, що нам слід зменшити цю вагу, щоб зменшити втрати. Якщо він негативний, ми повинні його збільшити.

Щоб побачити, як ми отримуємо ці похідні, ми почнемо з простого підходу під назвою кінцеві різниці, де ми оцінюємо нахил, порівнюючи втрати при двох сусідніх значеннях ваги. Потім ми побачимо, як похідні інструменти дозволяють нам знайти правильний напрямок для зменшення втрат без проб і помилок, і як ми можемо об’єднати їх разом через послідовність операцій. Нарешті, ми створимо невеликий механізм Python, який автоматично розраховує градієнти за допомогою техніки під назвою автоматичне диференціювання у зворотному режимі (або авторозрізнення), і ми використаємо його для навчання простої моделі.

Знайти напрямок шляхом тестування найближчих значень

Припустимо, наші втрати залежать від одного параметра aa:

L(a)=a2+3.L(a)=a^2+3.

При a=5a=5 втрата 2828. Нам слід збільшити або зменшити aa, щоб зменшити його?

Ми могли б спробувати обидва напрямки:

Параметр aaВтрата a2+3a^2+3
4.94.927.0127.01
5.05.028.0028.00
5.15.129.0129.01

Зменшення aa на 0.10.1, від 55 до 4.94.9, зменшило втрату від 2828 до 27.0127.01. Збільшення aa на 0.10.1, від 55 до 5.15.1, збільшило втрату з 2828 до 29.0129.01. Ці оцінки припускають, що невелике зниження від a=5a=5 зменшить втрати.

Важливо підкреслити, що напрямок, який зменшує втрати, залежить від поточного значення aa. Якби ми почали з a=−5a=-5, втрати також складали б 2828, але збільшення aa до −4.9-4.9 знизило б їх до 27.0127.01. Зменшення aa до −5.1-5.1 збільшить його до 29.0129.01. Для цієї функції рух до нуля зменшує втрати: від додатного значення ми маємо зменшити aa, а від від’ємного – збільшити.

Same loss, opposite directions
Loss L(a) = a² + 3 against Parameter aThe loss curve is U-shaped with a minimum at a = 0, where the loss is 3. At a = −5 and a = 5, the loss is 28. Arrows follow the curve downward toward zero: increase a from −5, but decrease a from 5.-60-310020330640Loss L(a) = a² + 3Parameter a(−5, 28)(5, 28)Increase a →← Decrease aMinimum (0, 3)
The green arrows show moves that lower the loss. From either starting point, moving toward zero helps; the direction depends on which side of zero you start.

Ось чому ми описуємо напрямок як локальний: він говорить нам, чи зменшить втрати невелике збільшення чи зменшення поточного значення параметра. Це не означає, що зменшення aa завжди допомагає, або що ми можемо продовжувати зменшувати його нескінченно. Починаючи з 55, ми можемо зменшити збитки, рухаючись до нуля, але як тільки ми пройдемо нуль, продовжуючи в тому ж напрямку, збитки знову зростають. Нам потрібно враховувати як те, де ми зараз, так і те, як далеко ми рухаємося.

Давайте тепер використаємо ті самі значення, щоб оцінити, як швидко змінюються втрати, коли ми змінюємо параметр. Наше перше порівняння підказало нам, у якому напрямку зменшилися втрати. Цього разу ми розділимо зміну втрат на зміну параметра, щоб оцінити нахил. Використовуючи значення a=5a=5 і a=5.1a=5.1:

L(5.1)−L(5)5.1−5=29.01−280.1=10.1.\frac{L(5.1)-L(5)}{5.1-5} =\frac{29.01-28}{0.1} =10.1.

Це співвідношення є середньою зміною втрати на одиницю зміни параметра протягом цього інтервалу. Це дає нам оцінку нахилу при a=5a=5, яка називається кінцево-різницевою оцінкою. Його позитивний знак передбачає зменшення aa, щоб зменшити втрати, тоді як його величина оцінює, наскільки втрати чутливі до параметра. Тут ми повторно використали значення втрат таблиці; обчислення коефіцієнта не вимагало додаткових оцінок збитків.

Записуючи зміну параметра як hh, це пряма різниця:

L(a+h)−L(a)h.\frac{L(a+h)-L(a)}{h}.

З мільйонами параметрів оцінка нахилу таким чином для кожного з них вимагала б мільйонів додаткових оцінок втрат. Щоб уникнути таких оцінок, нам потрібне правило, яке безпосередньо дає нам нахил.

Від кінцевих різниць до похідної

По-перше, розглянемо, що станеться з нашою оцінкою, коли ми зменшимо hh. У a=5a=5 ми використовували h=0.1h=0.1 і отримали 10.110.1. З h=0.001h=0.001 ми отримуємо:

L(5.001)−L(5)0.001=28.010001−280.001=10.001.\frac{L(5.001)-L(5)}{0.001} =\frac{28.010001-28}{0.001} =10.001.

Оцінка наближається до 1010. Похідна, написана L′(a)L'(a) або dL/dadL/da, є межею цього співвідношення, коли hh наближається до нуля:

L′(a)=lim⁡h→0L(a+h)−L(a)h.L'(a)=\lim_{h\to0}\frac{L(a+h)-L(a)}{h}.

Для L(a)=a2+3L(a)=a^2+3 ми можемо спростити це порівняння алгебраїчно. Для ненульового hh:

L(a+h)−L(a)h=(a+h)2+3−(a2+3)h=2ah+h2h=2a+h.\begin{aligned} \frac{L(a+h)-L(a)}{h} &=\frac{(a+h)^2+3-(a^2+3)}{h}\\ &=\frac{2ah+h^2}{h}\\ &=2a+h. \end{aligned}

Коли hh наближається до нуля, відношення наближається до 2a2a, що дає нам правило похідної:

L′(a)=2a,L′(5)=10.L'(a)=2a,\qquad L'(5)=10.

У a=5a=5 тепер ми можемо оцінити 2a=102a=10 безпосередньо, без обчислення L(5+h)L(5+h) для виявлення нахилу.

Диференціювання дає нам правило для обчислення локальної швидкості зміни. Правило походить від аналізу того, як змінюється функція; коли ми його маємо, ми можемо застосувати його до поточного значення параметра.

Ми все ще розраховуємо втрати на кожному кроці тренування. Похідна дозволяє вибрати локальний напрямок оновлення без додаткової оцінки втрат для кожного збурення параметра. Після оновлення параметрів наступний крок обчислює нові втрати та градієнт.

Використання похідної для вибору напрямку

Похідна показує, наскільки втрати чутливі до невеликої зміни параметра. У a=5a=5 значення L′(5)=10L'(5)=10 означає:

ΔL≈10 Δa.\Delta L\approx10\,\Delta a.

Збільшення aa на 0.010.01 збільшить втрати приблизно на 0.10.1. Зменшення його на 0.010.01 зменшить втрати приблизно на 0.10.1. Ми можемо вибрати локальний напрямок за знаком похідної, не перевіряючи обидві можливості.

При a=−5a=-5 похідною є −10-10, тому невелике збільшення aa зменшує втрати. При a=0a=0 похідна дорівнює нулю; для цієї конкретної функції, це її мінімум. Нульова похідна не встановлює мінімум для кожної функції.

Візуалізація схилу

Похідна — це нахил дотичної до кривої втрат. Скінченно-різницевою оцінкою є нахил січної, що проходить через дві точки. Коли ці точки зближуються, нахил січної наближається до похідної.

Зменште Дрібна зміна h до 0.0010.001 нижче та спостерігайте, як помаранчевий секанс наближається до фіолетового тангенса. При a=5a=5 оцінка нахилу стає 10.00110.001, близькою до похідної 1010. Перемістіть Параметр а від 55 до −5-5, щоб побачити, як напрям покращення зміниться.

Experiment 1 · finite-difference intuitionThe slope gives the local direction
L(a) = a² + 3 against Parameter aAt a=5, the loss is 28 and the tangent slope is 10. With h=0.8, the secant slope is 10.8.-60-310.75021.5332.25643L(a) = a² + 3Parameter a
CurveTangent: exact derivativeSecant: finite difference
Current loss L(a) = 28
(L(a + h) − L(a)) / h = 10.8
L′(a) = 2a = 10

Positive slope: decrease a slightly to reduce the loss. The finite-difference error is h = 0.8.

Purpose: visualize the meaning of a derivative. The training experiments compute gradients with automatic differentiation.

Похідна описує локальні зміни. На a=5a=5 зменшення аж до a=−6a=-6 призведе до збільшення втрат від 2828 до 3939, незважаючи на початок у запропонованому напрямку. Розмір оновлення також має значення.

Для цієї функції точна зміна втрат становить 2aΔa+(Δa)22a\Delta a+(\Delta a)^2. Похідна фіксує частину першого порядку; він не передбачає точно довільних кінцевих змін. Пізніше ми знову використаємо кінцеві різниці, щоб незалежно перевірити наш механізм диференціювання.

Три способи обчислення або оцінки похідної

Константа 33 зсунула криву втрат вгору, не змінивши її нахилу. Щоб порівняти методи диференціювання, використовуйте простішу функцію f(x)=x2f(x)=x^2 у x=5x=5. Його похідною є 1010:

методЩо він використовуєРозрахунок на x=5x=5Результат
Символічна диференціаціяПравила, застосовані до виразуОтримайте 2x2x, потім замініть 551010
Кінцеві різниціВиходи на сусідніх входах(f(5.001)−f(5))/0.001(f(5.001)-f(5))/0.001Приблизно 10.00110.001
Автоматична диференціаціяВиконувані операції та їх локальні похідні правилаДля x * x додайте внески 5+55+51010

Символічна диференціація створює похідний вираз, який ми можемо оцінити пізніше. Autodiff оцінює правила похідних, використовуючи значення, записані під час обчислення. Для цієї згладженої функції обидві обчислюють похідну у вибраній точці з урахуванням арифметики з плаваючою комою в коді. Кінцеві різниці оцінюють його шляхом порівняння результатів у різних точках.

Зворотне поширення нейронної мережі використовує третій підхід. Ми надаємо похідні правила для базових операцій, а система об’єднує їх за допомогою правила ланцюга. Він може обчислити градієнт при поточних значеннях параметрів без створення одного великого символічного виразу для всієї мережі.

Від локальних похідних до ланцюгового правила

Додавання і множення мають власні правила похідних. Спочатку розглянемо незалежні входи x=3x=3 і y=4y=4. Як зміна xx впливає на вихід zz, залишаючи yy фіксованим?

ОпераціяПохідна відносно xxУ x=3,y=4x=3,y=4Скінченно-різницева перевірка
z=x+yz=x+y∂z/∂x=1\partial z/\partial x=111((3.001+4)−(3+4))/0.001=1((3.001+4)-(3+4))/0.001=1
z=xyz=xy∂z/∂x=y\partial z/\partial x=y44((3.001×4)−(3×4))/0.001=4((3.001\times4)-(3\times4))/0.001=4

Крім того, зміна xx змінює вихід на ту саму величину. Для множення зміна масштабується на yy. Ми використовуємо локальне правило похідної кожної операції безпосередньо; ще немає проміжних операцій. Символ ∂\partial означає, що ми змінюємо один вхід, утримуючи інші незалежні входи фіксованими.

З’єднайте операції

Тепер дозвольте одній операції переходити в іншу:

a=x+y,z=ax.a=x+y,\qquad z=ax.

При x=3,y=4x=3,y=4 пряме обчислення дає a=7a=7 і z=21z=21. Ми могли б розширити весь вираз і відрізнити його:

z=(x+y)x=x2+xy⟹∂z∂x=2x+y=10.z=(x+y)x=x^2+xy \quad\Longrightarrow\quad \frac{\partial z}{\partial x}=2x+y=10.

Правило ланцюга дозволяє отримати ту саму похідну, дотримуючись проміжних співвідношень. Є два шляхи від xx до zz:

  • Через додавання x→a→zx\to a\to z: локальна похідна від aa відносно xx дорівнює 11, а локальна похідна від множення щодо aa дорівнює x=3x=3. Їх продукт - 3×1=33\times1=3.
  • Безпосередньо в множенні, x→zx\to z: якщо інший його операнд aa фіксований, локальна похідна a=7a=7.

Зміна xx впливає на обидва шляхи, тому ми додаємо їхні внески:

∂z∂x=3×1⏟through a+7⏟direct path=10.\frac{\partial z}{\partial x} =\underbrace{3\times1}_{\text{through }a} +\underbrace{7}_{\text{direct path}} =10.

Утримання aa фіксованим описує лише локальне правило множення. У повному обчисленні a=x+ya=x+y також змінюється, коли змінюється xx; перший шлях пояснює цей ефект.

Для порівняння, кінцева різниця оцінює повну функцію на найближчому вході:

z(3.001,4)−z(3,4)0.001=21.010001−210.001=10.001.\frac{z(3.001,4)-z(3,4)}{0.001} =\frac{21.010001-21}{0.001} =10.001.

Ми також можемо розрахувати чутливість до yy, використовуючи ті самі збережені значення. Є один шлях, y→a→zy\to a\to z, який дає ∂z/∂y=3×1=3\partial z/\partial y=3\times1=3. Жодна похідна не вимагала іншої оцінки на збуреному вході.

Правило ланцюга дозволяє обчислити, як зміна в одній змінній впливає на кінцевий результат, дотримуючись зв’язків між проміжними змінними. Якщо цей результат є втратою, той самий процес повідомляє нам, як кожен параметр впливає на втрату. Помножте локальні похідні вздовж шляху та додайте вклади з різних шляхів.

Зворотне розповсюдження організовує це обчислення у зворотному напрямку від кінцевого результату, повторно використовуючи проміжні значення та накопичені градієнти. Він все ще обчислює похідну для кожного параметра; це дозволяє уникнути повторення всього прямого обчислення для кожного збурення параметра.

Нелінійні зв’язки роблять поточне значення важливим

Лінійна залежність має постійний нахил: однакова зміна вхідних даних завжди призводить до однакових змін вихідних даних. У нелінійному відношенні локальний ефект може залежати від поточного входу. Обчислення дає нам спосіб обчислити зміну чутливості.

Розглянемо спрощений сферичний газовий контейнер з регульованим радіусом. Тримайте кількість газу та його температуру постійними, і припустімо, що тиск і об’єм задовольняють PV=100PV=100 у вибраних нами одиницях. Це моделює розширення того самого газу; додавання газу під час надування справжньої повітряної кулі змінило б припущення.

Радіус визначає об’єм, а об’єм – тиск:

r ⟶ V=43πr3 ⟶ P=100V.r\ \longrightarrow\ V=\frac43\pi r^3 \ \longrightarrow\ P=\frac{100}{V}.

Якщо ми трохи збільшимо радіус, на скільки зміниться тиск? Обидва зв’язки відомі, але жоден не має постійної швидкості змін. Їх місцевими похідними є:

dVdr=4πr2,dPdV=−100V2.\frac{dV}{dr}=4\pi r^2, \qquad \frac{dP}{dV}=-\frac{100}{V^2}.

Перший говорить нам, скільки змінюється обсяг на одиницю радіуса; другий говорить нам, скільки змінюється тиск на одиницю об’єму. Ланцюгове правило об’єднує їх:

dPdr=dPdVdVdr=−100V2 4πr2.\frac{dP}{dr} =\frac{dP}{dV}\frac{dV}{dr} =-\frac{100}{V^2}\,4\pi r^2.

При r=2r=2 об’єм становить приблизно 33.510333.5103, тиск – 2.98422.9842, а dP/dr≈−4.4762dP/dr\approx-4.4762. Збільшення радіуса на 0.010.01, отже, передбачає зміну тиску приблизно на −0.0448-0.0448. Оцінка нового тиску дає фактичну зміну близько −0.0443-0.0443; похідна дає локальне наближення.

Зберігайте Збільшення радіуса Δr на 0.010.01 і перемістіть Поточний радіус r з 11 на 22. Таке ж збільшення радіуса призводить до значно меншої зміни тиску при більшому радіусі. Потім збільште крок, щоб побачити, як передбачення дотичної стане менш точним.

Experiment 2 · nonlinear relationshipsSame radius increase, different pressure change
r = 2 → V = 4πr³/3 = 33.5103 → P = 100/V = 2.9842
dV/dr = 50.2655 · dP/dV = -0.0891
dP/dr = (dP/dV) × (dV/dr) = -4.4762
Pressure P against Radius rAt radius 2, pressure is 2.9842 and the local slope is -4.4762. Increasing radius by 0.01 predicts a pressure change of -0.0448; the actual change is -0.0443. The plot keeps the same axes as the radius changes.101.8756.52.75133.62519.54.526Pressure PRadius r
Pressure curve and current valueTangent and predicted value (ring)Actual new pressure (dot)
Predicted ΔP = (dP/dr) × Δr = -0.0448
Actual ΔP = P(r + Δr) − P(r) = -0.0443
New pressure: predicted 2.9394 · actual 2.9398

The negative slope means a small radius increase reduces pressure. The tangent estimates that change at the current radius; larger steps can differ substantially from its prediction.

Model: fixed amount of gas at constant temperature, with PV = 100 in chosen units. The extra pressure evaluation checks the tangent prediction. The derivative comes from the two local rules.

Зв’язок із нейронними мережами – це зміна чутливості. Вплив ваги на остаточну втрату залежить від поточних значень уздовж шляхів обчислення. Нелінійні активації та функція втрати можуть змінити цю чутливість, тому ми знову оцінюємо локальні похідні правила після кожного оновлення. Навіть афінна модель передбачення, така як y^=wx+b\hat y=wx+b, може мати нелінійні втрати як функцію своїх ваг, коли ми зводимо її помилку передбачення в квадрат.

Авторізниця через одне множення

Поверніться до f(x)=x2f(x)=x^2 і напишіть це як програму. Тепер ми простежимо, як механізм записує та застосовує місцеві правила:

def f(x):
    return x * x

Правило множення застосовується до обох операндів. Якщо z=abz=ab, його локальні похідні:

∂z∂a=b,∂z∂b=a.\frac{\partial z}{\partial a}=b, \qquad \frac{\partial z}{\partial b}=a.

Для x * x обидва операнди посилаються на той самий вхід. У x=5x=5 кожен операнд є 55, тому кожна локальна похідна також є 55. Зміна xx впливає на результат через шляхи обох операндів. Їхні внески додають:

d(x⋅x)dx=5+5=10at x=5.\frac{d(x\cdot x)}{dx}=5+5=10 \quad\text{at }x=5.

Виберіть Оцініть x × x, потім Градієнт вихідного матеріалу. Слідкуйте за кожним операндом по черзі, щоб побачити, як накопичений градієнт досягає 1010.

Experiment 3 · automatic differentiationOne input, two operand paths
Differentiate x times x through its two operand pathsBoth paths connect the same input x=5 to one multiplication. Stage 0 of 4. Choose the evaluation point x = 5. Both operand paths refer to this same x.Left operand: xRight operand: xShared input xvalue = 5grad so far = —q = x × xvalue = —∂q/∂q = —
Forward: values →Backward: gradient contributions ←
Stage 0 of 4. Choose the evaluation point x = 5. Both operand paths refer to this same x.
Input used for this trace: x = 5.

The trace uses the multiplication edges from the article’s scalar engine. Changing x starts a new trace at that value.

Віджет починає зворотний перехід із похідною виводу відносно самого себе: dq/dq=1dq/dq=1. Кожен шлях множить цей вхідний градієнт на його локальну похідну. У звичайному Python арифметика така:

x = 5.0
left, right = x, x
q = left * right             # forward: 25.0

output_grad = 1.0            # dq/dq
x_grad = 0.0
x_grad += output_grad * right  # left operand contributes 5.0
x_grad += output_grad * left   # right operand contributes 5.0
print(x_grad)                 # 10.0

Кожне значення, необхідне для цієї похідної, отримано з обчислення на x=5x=5. Ми використали правило множення та збережені операнди. Оцінка f(5.001)f(5.001) належить до скінченно-різницевого методу в таблиці; Трасування авторозрізнення вище завершується єдиною точкою оцінки.

Автоматизація відбувається завдяки запису цих шляхів операндів і застосуванню їхніх похідних правил для нас. Пізніше наш клас Value зробить те саме обчислення, коли ми напишемо q = x * x і викличемо q.backward().

Знання операцій і розуміння форми

Припустимо, у нас є тільки стіл:

xxСпостережений вихід
11
24
39
…\ldots…\ldots
10100

Ці кінцеві спостереження не визначають однозначно функцію між переліченими точками або її точну похідну в перерахованій точці. Кілька функцій можуть узгоджуватися з кожним рядком і мати різні нахили. Одним із варіантів є обчислення кінцево-різницевої оцінки за доступними сусідніми зразками.

З програмою return x * x ми маємо більше інформації: ми знаємо операції, які генерують вихідні дані, тому ми можемо застосувати їхні похідні правила. Нейронна мережа також має відомі обчислення для своїх прогнозів і втрат. Навчальні приклади можуть надходити у вигляді таблиці, але модель і втрати є виконуваними математичними функціями.

Знання цих операцій також не дає нам автоматичного розуміння всієї кривої. Розглянемо:

g(x)=sin⁡(x2)+0.2x2,g′(x)=2xcos⁡(x2)+0.4x.g(x)=\sin(x^2)+0.2x^2, \qquad g'(x)=2x\cos(x^2)+0.4x.

Ми можемо оцінити значення та похідну в будь-якій вибраній точці за допомогою цих формул. Чи можете ви уявити кожен підйом, спад і поворотний момент, просто читаючи їх? Перемістіть Відома точка оцінки x нижче, а потім виберіть Розкрийте повну криву. Місцеве значення та нахил доступні до того, як буде видно всю ділянку.

Experiment 4 · local slope and global shapeCompute a slope before revealing the curve
g(x) = sin(x²) + 0.2x²
g(x) against xAt x=1, g(x)=1.0415 and its derivative is 1.4806. Only the selected point and its tangent are visible.-4-1-20.50223.545g(x)x
Known operations: x² = 1 → sin(x²) = 0.8415 → add 0.2x² = 0.2.
g(x) = 1.0415 · g′(x) = 2x cos(x²) + 0.4x = 1.4806

The value and derivative are computed from known rules at the selected point. Revealing the curve additionally samples the function across the displayed interval.

Формула повністю визначає функцію. Розуміння його глобальної геометрії потребує додаткової оцінки чи аналізу. Діаграма дає нам більше цієї інформації за вибраний інтервал, тоді як похідна описує локальну чутливість в одній точці.

Для нейронної мережі втрати можуть залежати від мільйонів ваг і зміщень. Ми знаємо програму, яка обчислює L(w1,w2,…)L(w_1,w_2,\ldots) при вибраному налаштуванні параметра. Його градієнт дає місцеву чутливість. Ці обчислення не визначають місцезнаходження кожного мінімуму в іншому місці цього високовимірного простору. Навчання використовує локальну інформацію для вибору оновлення, а потім обчислює новий градієнт за оновленими параметрами.

Прогноз дає нам щось оцінити

Тепер припустимо, що модель отримує вхідні дані x=2x=2 і має передбачити цільовий y=5y=5. Дайте йому одну вагу ww і одне зміщення bb:

y^=wx+b.\hat y=wx+b.

Капелюх у y^\hat y означає передбачення. На w=1w=1 і b=0b=0 передбачено 22, що 33 нижче цілі.

Що слід звести до мінімуму у навчанні? Мінімізація самого прогнозу призведе до його зниження, навіть якщо нам потрібно, щоб він збільшився. Нам потрібна функція, яка вимірює розбіжність між прогнозом і метою.

Виберіть квадрат помилки:

L=(y^−y)2.L=(\hat y-y)^2.

Поточні втрати (2−5)2=9(2-5)^2=9. Нуль означатиме, що передбачення відповідає цільовому показнику для цього прикладу.

Підставте обчислення моделі у втрати:

L(w,b)=(wx+b−y)2.\boxed{L(w,b)=(wx+b-y)^2.}

Це зв’язок, який нам потрібен. Тепер втрати є функцією параметрів, які ми можемо змінити. Для фіксованого прикладу навчання (x,y)=(2,5)(x,y)=(2,5):

L(w,b)=(2w+b−5)2.L(w,b)=(2w+b-5)^2.

Одне лише число 99 не говорить нам, у який бік рухатися. З b=0b=0 і w=1w=1, і w=4w=4 створюють втрати 99, але вони повинні рухатися в протилежних напрямках. Ми розрізняємо функцію, яка породила втрату, оцінену за поточними параметрами.

Те саме обчислення, із вагою як змінна

Тримайте тренувальний вхід x=2x=2, ціль y=5y=5 і зміщення b=0b=0 фіксованими. Втрата стає звичайною функцією однієї змінної:

L(w)=(2w−5)2=4w2−20w+25.L(w)=(2w-5)^2=4w^2-20w+25.

Ми можемо диференціювати цей маленький вираз вручну та замінити поточну вагу, точно так само, як ми зробили для x2x^2:

КрокЗнайома функціяВтрата моделі з фіксованими навчальними даними
Напишіть функціюf(t)=t2f(t)=t^2L(w)=(2w−5)2L(w)=(2w-5)^2
Диференціюватиf′(t)=2tf'(t)=2tL′(w)=8w−20L'(w)=8w-20
Вставте поточне значенняt=5t=5w=1w=1
Отримайте похіднуf′(5)=10f'(5)=10L′(1)=−12L'(1)=-12

Буква, що позначає змінну, змінилася з tt на ww. Навчальний вхід і ціль є фіксованими числами всередині L(w)L(w); ми запитуємо, як втрата реагує на вагу. Така ж відмінність застосовується, коли ми використовуємо цілу партію: вхідні дані та цілі партії залишаються фіксованими, поки ми диференціюємо параметри.

Ця модель досить мала, щоб розкласти її в поліном. Для більшої мережі autodiff записує прямі операції з поточним налаштуванням параметра та оцінює їх локальні похідні правила назад. Він обчислює числові значення похідних параметрів без попередньої побудови однієї розширеної символічної формули для градієнта мережі.

Яку похідну взяти?

Для ваги ми хочемо:

∂L∂w.\frac{\partial L}{\partial w}.

Прочитайте це так: як швидко зміниться втрата, якщо ми змінимо вагу, утримуючи інші незалежні величини незмінними? Символ ∂\partial використовується, оскільки LL залежить від кількох змінних. Для цього розрахунку вхід, ціль і зміщення залишаються фіксованими.

Модель і втрата утворюють композицію. Спочатку обчисліть прогноз, потім відніміть ціль, а потім підведіть помилку в квадрат:

w ⟶ y^=wx+b ⟶ e=y^−y ⟶ L=e2.w\ \longrightarrow\ \hat y=wx+b \ \longrightarrow\ e=\hat y-y \ \longrightarrow\ L=e^2.

Кожна операція має локальну похідну:

ОпераціяЯкі зміни?Місцева похідна
y^=wx+b\hat y=wx+bЗмініть ww, утримуйте x,bx,b фіксованим∂y^/∂w=x\partial\hat y/\partial w=x
e=y^−ye=\hat y-yЗмініть y^\hat y, утримуйте yy фіксованим∂e/∂y^=1\partial e/\partial\hat y=1
L=e2L=e^2Змінити eedL/de=2edL/de=2e

Правило ланцюга множить ці чутливості на шляху:

∂L∂w=dLde∂e∂y^∂y^∂w=2e⋅1⋅x=2(y^−y)x.\frac{\partial L}{\partial w} =\frac{dL}{de} \frac{\partial e}{\partial\hat y} \frac{\partial\hat y}{\partial w} =2e\cdot1\cdot x =2(\hat y-y)x.

За нашими початковими значеннями e=−3e=-3 і x=2x=2:

∂L∂w=2(−3)(2)=−12.\frac{\partial L}{\partial w}=2(-3)(2)=-12.

Тому збільшення ваги на 0.010.01 має змінити втрату приблизно на −12×0.01=−0.12-12\times0.01=-0.12. Точна нова втрата - (2.02−5)2=8.8804(2.02-5)^2=8.8804, зміна −0.1196-0.1196. Згода покращується, коли крок стає меншим.

Для зміщення змінюється останній фактор: ∂y^/∂b=1\partial\hat y/\partial b=1. Отже:

∂L∂b=2(y^−y)=−6.\frac{\partial L}{\partial b}=2(\hat y-y)=-6.

Похідні за всіма параметрами, які можна навчити, разом утворюють градієнт. Ось цей вектор ∇L=(−12,−6)\nabla L=(-12,-6), у порядку (w,b)(w,b).

Вибір втрати змінює сигнал навчання

Модель повідомляє нам, як її прогноз реагує на вагу. Втрата говорить нам, наскільки ця зміна прогнозу має значення для мети.

Для того самого прогнозу порівняйте дві втрати:

ВтратаПохідна щодо передбаченняПохідна по вазі
Квадрат помилки: (y^−y)2(\hat y-y)^22(y^−y)2(\hat y-y)2(y^−y)x2(\hat y-y)x
Абсолютна помилка: ∣y^−y∣\lvert\hat y-y\rvertsign⁡(y^−y)\operatorname{sign}(\hat y-y), далеко від нуляsign⁡(y^−y)x\operatorname{sign}(\hat y-y)x, далеко від нуля

У нашій початковій точці градієнт передбачення становить −6-6 для квадратичної помилки та −1-1 для абсолютної помилки. Множення на ту саму похідну моделі, x=2x=2, дає вагові градієнти −12-12 і −2-2.

Перемикайтеся між Квадрат помилки і Абсолютна помилка нижче. Перемістіть Вага w, спостерігаючи як за градієнтом передбачення, так і за градієнтом ваги. Вхідні дані залишаються фіксованими, тому змінюється лише перший фактор.

Experiment 5The loss changes the slope
Loss L(w) against weight wInput x=2, target y=5, bias b=0. Prediction 2, loss 9, weight gradient -12.-100.7513.752.527.54.2541.25655Loss L(w)weight w
Current weightLocal slopeAfter the proposed step
ŷ = 2 × 1 = 2 · target = 5 · L = 9
∂L/∂ŷ = -6 · ∂ŷ/∂w = 2 · ∂L/∂w = -12
w next = 1 − 0.1 × (-12) = 2.2 · next loss = 0.36

x = 2, y = 5, and b = 0 stay fixed. This experiment updates only w.

Квадрат помилки створює більший градієнт передбачення, коли величина помилки більша. Абсолютна похибка має постійну величину градієнта по відношенню до прогнозу, коли похибка відмінна від нуля. Його похідна не визначена в нулі; віджет вибирає там нульовий субградієнт.

Жодна втрата не містить правил диференціації. Ці правила походять від числення. Вибір втрати визначає функцію, на яку діють ці правила, і, отже, початковий сигнал, що поширюється через модель.

Похідне стає оновленням

Градієнтний спуск віднімає масштабований градієнт:

wnew=w−η∂L∂w.w_{\text{new}}=w-\eta\frac{\partial L}{\partial w}.

Швидкість навчання η\eta встановлює розмір кроку. З квадратичною помилкою, w=1w=1 і η=0.1\eta=0.1:

wnew=1−0.1(−12)=2.2.w_{\text{new}}=1-0.1(-12)=2.2.

Утримуючи упередження на нульовому рівні, новий прогноз буде 4.44.4, а нова втрата – 0.360.36. Використовуйте Скинути і Зробіть один крок в експерименті з втратами, щоб відтворити цей розрахунок.

Тут ми оновили лише вагу. Якщо ми також оновимо зміщення з того самого градієнта, він стане 0.60.6, даючи прогноз 2.2×2+0.6=52.2\times2+0.6=5 і нульові втрати для цього окремого прикладу. Це різні оновлення, тому вони дають різні результати.

Від’ємна похідна передбачає збільшення параметра; додатна похідна передбачає її зменшення. Це місцева заява. Великий крок може пройти мінімум і збільшити збиток. Зворотне поширення обчислює градієнт; оптимізатор і швидкість його навчання визначають, що з ним робити.

Зворотне поширення - це правило ланцюга через граф

Ми можемо записати обчислення у вигляді графіка. Кожен блок містить число, і кожна операція пов’язує свій результат зі значеннями, які використовуються для його отримання.

Почніть із крайнього правого поля. Похідна втрати відносно себе дорівнює одиниці:

∂L∂L=1.\frac{\partial L}{\partial L}=1.

Потім працюйте назад. Коробка отримує градієнт, який повідомляє нам, як кінцевий збиток реагує на значення цієї коробки. Щоб передати внесок до одного з його входів, помножте на локальну похідну операції:

input gradient contribution=output gradient×local derivative.\boxed{\text{input gradient contribution} =\text{output gradient}\times\text{local derivative}.}

Виберіть Градієнт насіння, потім Зворотне поширення одна операція, доки не досягнете ww і xx. Кожен відображений градієнт є похідною від фінальної втрати, навіть якщо квадрат представляє проміжний результат.

Experiment 6Follow the loss backward
wvalue 1grad —xvalue 2grad —p = w × xvalue 2grad —bvalue 0grad —ŷ = p + bvalue 2grad —y (target)value 5grad —e = ŷ − yvalue -3grad —L = e²value 9grad —Values flow → · apply local derivative rules ←

Scroll the graph sideways to follow all the operations.

Stage 0 of 5. Forward pass: evaluate the operations from left to right. Each box stores its value. Gradients have not been seeded yet.
Gradients revealed so far. “grad” means ∂L / ∂(box value).
ValueForward valueGradient
w1—
x2—
p = w × x2—
b0—
ŷ = p + b2—
y (target)5—
e = ŷ − y-3—
L = e²9—

На графіку також наведено похідні щодо вхідних даних і цілі. Це не робить їх придатними для навчання. Ми вибираємо, які значення оновлюватиме оптимізатор: тут лише ww та bb. У глибшій мережі «вхід» шару може бути виходом попереднього шару, тому його градієнт стає маршрутом назад до попередніх параметрів.

Важлива відмінність виникає, коли вартість має кілька шляхів до втрати. Внески з цих шляхів додати. Наприклад:

q=a⋅a+a⟹dqda=a+a+1=2a+1.q=a\cdot a+a \quad\Longrightarrow\quad \frac{dq}{da}=a+a+1=2a+1.

У a=3a=3 ця похідна є 77. Зворотний перехід має зберігати всі три внески, включаючи обидва використання aa у множенні. Ось чому наш двигун використовуватиме += під час поширення градієнтів.

Побудуйте число, яке запам’ятовує його обчислення

Звичайний float Python зберігає значення. Наш Value також зберігатиме:

  • grad: похідна вибраного кінцевого результату відносно цього значення.
  • edges: послідовність пар (parent, local_derivative), що записує, як було обчислено це значення.

Додавання і множення стають операціями побудови графіків. Для z=a+bz=a+b обидві крайові похідні є 11. Для z=abz=ab це bb і aa.

Ось перша частина двигуна, включаючи цілі ступені та активацію, яку ми використаємо пізніше:

import math


class Value:
    def __init__(self, data, edges=()):
        self.data = float(data)
        self.grad = 0.0
        self.edges = tuple(edges)  # (parent, local derivative) pairs

    @staticmethod
    def wrap(value):
        return value if isinstance(value, Value) else Value(value)

    def __add__(self, other):
        other = Value.wrap(other)
        return Value(self.data + other.data, ((self, 1.0), (other, 1.0)))

    def __mul__(self, other):
        other = Value.wrap(other)
        return Value(self.data * other.data,
                     ((self, other.data), (other, self.data)))

    def __pow__(self, exponent):
        if not isinstance(exponent, int) or exponent < 1:
            raise ValueError("This engine supports positive integer powers only")
        return Value(self.data ** exponent,
                     ((self, exponent * self.data ** (exponent - 1)),))

    def tanh(self):
        output = math.tanh(self.data)
        return Value(output, ((self, 1.0 - output * output),))

Коли ми множимо два об’єкти Value, __mul__ негайно обчислює результат і зберігає два ребра з їх локальними похідними. Ці похідні значення обчислюються з використанням поточних даних операндів. Ще нічого не вивчено; ми записуємо форвардне обчислення.

Ми безпосередньо реалізуємо степені, тому (prediction - target) ** 2 записує операцію квадрата. Механізм навмисно підтримує лише додатні цілі ступені. Це охоплює наші приклади без введення доменів, таких як дробові ступені від’ємних чисел.

Гіперболічний тангенс, tanh, є гладкою функцією активації з похідною 1−tanh⁡2(z)1-\tanh^2(z). Це дозволяє мережі виражати нелінійні зв’язки.

Зручні оператори Python роблять движок приємним у використанні. Зворотні оператори дозволяють такі вирази, як 2 * w і sum(...):

    def __neg__(self):
        return self * -1.0

    def __sub__(self, other):
        return self + -Value.wrap(other)

    def __rsub__(self, other):
        return Value.wrap(other) + -self

    __radd__ = __add__
    __rmul__ = __mul__

Усі ці методи належать до Value. повний файл Python містить двигун, моделі та приклади, які можна запускати. Ви також можете завантажити tinygrad.py.

Перегляньте графік у правильному порядку

Щоб обчислити градієнти, нам потрібно обробити кожен результат перед його батьками, після того, як надійдуть усі внески цього результату.

По-перше, обхід у глибину відвідує батьківські вузли перед додаванням самого вузла. Це створює топологічний порядок. Набір seen гарантує, що спільний вузол буде додано лише один раз. Змінюючи порядок, кінцевий результат стає першим.

Додайте цей метод до Value:

    def backward(self):
        order, seen = [], set()

        def visit(node):
            if node in seen:
                return
            seen.add(node)
            for parent, _ in node.edges:
                visit(parent)
            order.append(node)

        visit(self)
        for node in order:
            node.grad = 0.0
        self.grad = 1.0
        for node in reversed(order):
            for parent, local_derivative in node.edges:
                parent.grad += node.grad * local_derivative

Є три окремі кроки: очистити старі градієнти на доступному графіку, додати градієнт остаточного виходу до одного та повернутися назад, щоб накопичити внески.

Набір seen дедуплікує вузли, тоді як edges зберігає кожне входження операнда. У a * a два ребра вказують на одного батька. Обидва виконують своє оновлення +=. Заміна edges на набір батьків без збереження відповідних внесків втратить цю відмінність.

Наш backward() навмисно очищає доступні градієнти під час кожного виклику. Це робить повторні виклики на тому самому незмінному графіку відтворюваним. Він відрізняється від API, які накопичують листові градієнти в зворотних викликах і вимагають окремого кроку обнулення. Протягом одного зворотного проходу внески все ще накопичуються з усіх шляхів графа.

Ця реалізація зберігає числові локальні похідні під час прямого проходу. Змініть дані параметрів після назад, а потім побудуйте новий графік для наступної ітерації. Повторний запуск старого графіка після зміни даних призведе до використання застарілих локальних похідних.

Завдяки рекурсивному обходу графа цей навчальний механізм є коротким. Довгі ланцюжки обчислень, такі як втрати, підсумовані за тисячами прикладів, можуть перевищувати обмеження рекурсії Python; ітераційний обхід уникає цього обмеження.

Перевірте двигун на арифметику

Збережіть повний файл як tinygrad.py. В іншому файлі Python у тому ж каталозі:

from tinygrad import Value

w, b = Value(1), Value(0)
prediction = w * 2 + b
loss = (prediction - 5) ** 2
loss.backward()

print(loss.data)  # 9.0
print(w.grad)     # -12.0
print(b.grad)     # -6.0

a = Value(3)
q = a * a + a
q.backward()
print(a.grad)    # 7.0: all three paths contribute

Двигун так і не отримав спеціальної формули для градієнта ваги цієї моделі. Він записував множення, додавання, віднімання та квадрат, а потім комбінував їх місцеві похідні.

Використовуйте кінцеві різниці, щоб перевірити результат

Це друга мета, оголошена на початку: незалежна перевірка. Двигун уже отримав w.grad із записаних операцій на w=1w=1. Щоб перевірити його виконання, оцініть втрати окремо з обох сторін цієї ваги:

def numeric_loss(weight):
    return (weight * 2 - 5) ** 2

h = 1e-5
finite_difference = (numeric_loss(1 + h) - numeric_loss(1 - h)) / (2 * h)
assert abs(w.grad - finite_difference) < 1e-6

Ця центральна відмінність є чисельною перевіркою, а не алгоритмом диференціювання двигуна. Перевірка кожного параметра таким чином вимагає двох оцінок для кожного параметра. Натомість автоматичне диференціювання у зворотному режимі повторно використовує окремий записаний графік для обчислення всіх градієнтів параметрів для скалярного результату, з роботою, пропорційною розміру графіка.

Надзвичайно малі значення hh зрештою призводять до скасування числа з плаваючою комою, тому менші значення не завжди є кращими для цієї числової перевірки. Навчальні цикли нижче отримують градієнти, викликаючи backward() на поточному графіку.

Вчіться на кількох прикладах

Один навчальний приклад не може визначити унікальну лінію: багато пар (w,b)(w,b) задовольняють 2w+b=52w+b=5. Додайте більше прикладів:

Введіть xxЦіль yy
01
13
25
37

Ці точки лежать на y=2x+1y=2x+1, тому ми можемо перевірити, чи знаходить навчання w≈2w\approx2 і b≈1b\approx1.

Визначте втрату як середнє чотирьох квадратів помилок:

L(w,b)=14∑i=14(wxi+b−yi)2.L(w,b)=\frac14\sum_{i=1}^{4}(wx_i+b-y_i)^2.

Спільна вага відображається в усіх чотирьох прогнозах. Під час зворотного руху кожен шлях сприяє тому самому w.grad. Множення на 1/41/4 масштабує всі внески відповідно до середнього збитку. Підсумовування замість усереднення помножить обидва градієнти на чотири.

Використовуйте Крок, щоб перевірити одне оновлення, а потім Пробігти 100 кроків. Спробуйте дізнатися швидкість 0.30.3 зі стану скидання, щоб побачити зростання втрат, навіть якщо градієнти розраховані правильно.

Experiment 7Use the gradients to learn a line
prediction ŷ against input xAfter 0 updates, w=-1, b=0, mean squared error=41.5. The four targets lie on y=2x+1.-0.3-40.6-0.751.52.52.45.753.39prediction ŷinput x
Model: ŷ = wx + bTargetsPrediction errors
Step 0 · w = -1 · b = 0 · MSE = 41.5
log₁₀(MSE) against training stepLoss history on a logarithmic scale. A value of −3 means MSE 0.001. Losses below 10⁻¹² are plotted at −12.0-32.5-1.755-0.57.50.75102log₁₀(MSE)training step

Each click builds a fresh graph, averages four squared errors, runs backward(), and updates both w and b. The line plot rescales if the parameters grow; the loss plot uses log₁₀, with a floor of 10⁻¹².

Віджет використовує аналог JavaScript скалярного двигуна вище. Його градієнти обчислюються через новий графік на кожному кроці. Ось той самий навчальний цикл у Python:

def train_line(steps=1000, learning_rate=0.05):
    data = [(0, 1), (1, 3), (2, 5), (3, 7)]
    w, b = Value(-1), Value(0)
    for _ in range(steps):
        loss = sum((w * x + b - y) ** 2 for x, y in data) * (1 / len(data))
        loss.backward()  # also clears this graph's old gradients
        for parameter in (w, b):
            parameter.data -= learning_rate * parameter.grad
    final_loss = sum((w.data * x + b.data - y) ** 2 for x, y in data) / len(data)
    return w.data, b.data, final_loss

Обидва градієнти походять від того самого прямого проходу до зміни будь-якого параметра. Наступна ітерація повторно обчислює прогнози, втрати та похідні за оновленими параметрами. Повернений збиток оцінюється після останнього оновлення, тому він описує повернуті ваги.

Використовуйте той самий механізм для нейронної мережі

Поки що модель була спорідненою. Невелика нейронна мережа не потребує нових механізмів диференціювання: її прямий перехід — це більша композиція операцій, які ми вже підтримуємо.

Один нейрон: зважена сума, потім активація

Нейрон спочатку об’єднує свої вхідні дані в одне число, а потім застосовує функцію активації:

z=∑i=1nwixi+b=w⊤x+b,y^=f(z)=f ⁣(∑i=1nwixi+b).z=\sum_{i=1}^{n}w_ix_i+b=\mathbf w^\top\mathbf x+b, \qquad \boxed{\hat y=f(z)=f\!\left(\sum_{i=1}^{n}w_ix_i+b\right).}

Тут xix_i — це один вхідний сигнал, wiw_i — його вивчена вага, bb — це вивчене зміщення, а ff — функція активації. Вихід y^\hat y є одним скаляром; ми зберігаємо yy для цілі. У цьому розділі ii індексує вхідні дані для одного нейрона, а не навчальні приклади в попередньому розділі.

Скалярний добуток w⊤x\mathbf w^\top\mathbf x множить відповідні записи та підсумовує їх. Додавання зміщення робить zz афінним перетворенням від Rn\mathbb R^n до R\mathbb R. Застосування нелінійної активації, такої як ReLU, sigmoid або tanh, дозволяє шарам створювати нелінійні функції. Наша попередня модель y^=wx+b\hat y=wx+b — це випадок з одним входом і ідентифікаційною активацією f(z)=zf(z)=z.

Для конкретного прикладу візьмемо три входи:

x=[231],w=[0.5−12],b=1.\mathbf x=\begin{bmatrix}2\\3\\1\end{bmatrix}, \qquad \mathbf w=\begin{bmatrix}0.5\\-1\\2\end{bmatrix}, \qquad b=1.

Спочатку обчисліть зважену суму плюс зсув:

z=(0.5)(2)+(−1)(3)+(2)(1)+1=1.z=(0.5)(2)+(-1)(3)+(2)(1)+1=1.

З ReLU результат буде max⁡(0,1)=1\max(0,1)=1. Наш механізм використовує tanh, тому його нелінійний нейрон замість цього повертає:

y^=tanh⁡(1)≈0.7616.\hat y=\tanh(1)\approx0.7616.

Вхідні дані, ваги та зміщення визначають zz; активація визначає, як це значення передається вперед.

Дотримуйтеся правила ланцюга через нейрон

Додайте втрату до виходу цього нейрона. Вага впливає на цю втрату через зважену суму та активацію:

wi ⟶ z ⟶ y^=f(z) ⟶ L.w_i\ \longrightarrow\ z\ \longrightarrow\ \hat y=f(z)\ \longrightarrow\ L.

Локальна похідна зваженої суми відносно wiw_i дорівнює xix_i. Активація сприяє f′(z)f'(z). Помноживши їх на чутливість до втрат на виході, ви отримаєте:

∂L∂wi=∂L∂y^dy^dz∂z∂wi=∂L∂y^⏟loss sensitivityf′(z)⏟activation slopexi⏟input.\boxed{ \frac{\partial L}{\partial w_i} =\frac{\partial L}{\partial\hat y} \frac{d\hat y}{dz} \frac{\partial z}{\partial w_i} =\underbrace{\frac{\partial L}{\partial\hat y}}_{\text{loss sensitivity}} \underbrace{f'(z)}_{\text{activation slope}} \underbrace{x_i}_{\text{input}}. }

Для упередженості ∂z/∂b=1\partial z/\partial b=1, отже:

∂L∂b=∂L∂y^ f′(z).\frac{\partial L}{\partial b} =\frac{\partial L}{\partial\hat y}\,f'(z).

Для tanh, f′(z)=1−tanh⁡2(z)=1−y^2f'(z)=1-\tanh^2(z)=1-\hat y^2. Якщо цей єдиний нейрон передбачає ціль yy і ми використовуємо квадрат помилки, L=(y^−y)2L=(\hat y-y)^2, тоді:

∂L∂wi=2(y^−y)(1−y^2)xi.\frac{\partial L}{\partial w_i} =2(\hat y-y)(1-\hat y^2)x_i.

Поверніться до прикладу з трьома входами та виберіть ціль y=1y=1. При z=1z=1 нахил активації становить приблизно 0.42000.4200. Для першої ваги:

∂L∂w1=2(tanh⁡(1)−1)(1−tanh⁡2(1))⋅2≈−0.4005.\frac{\partial L}{\partial w_1} =2(\tanh(1)-1)\bigl(1-\tanh^2(1)\bigr)\cdot2 \approx-0.4005.

Таким чином, невелике збільшення w1w_1 зменшує локальні втрати. Порівняно з нашою попередньою афінною моделлю, ланцюг має один додатковий фактор: нахил активації. З активацією ідентичності цей фактор становить 11, що відновлює попередню формулу.

Для прихованого нейрона ∂L/∂y^\partial L/\partial\hat y надходить із пізніх шарів. Щоб продовжити назад, цей нейрон вносить (∂L/∂y^)f′(z)wi(\partial L/\partial\hat y)f'(z)w_i у градієнт вхідного xix_i. Якщо вхід живить кілька нейронів, їхні внески додаються, як це було в наших попередніх прикладах графіків.

Від одного нейрона до шару

Шар із нейронами mm має один ваговий вектор і одне зміщення на нейрон. Складіть вектори ваги як рядки W∈Rm×nW\in\mathbb R^{m\times n}, а зміщення – у b∈Rm\mathbf b\in\mathbb R^m:

a=f(Wx+b),a∈Rm.\mathbf a=f(W\mathbf x+\mathbf b), \qquad \mathbf a\in\mathbb R^m.

Тут a\mathbf a збирає вихідні дані рівня, а ff діє на кожен запис окремо. MLP передає ці результати на наступний рівень.

Існує також геометрична інтерпретація. Для ненульових векторів w⊤x=∥w∥∥x∥cos⁡θ\mathbf w^\top\mathbf x=\lVert\mathbf w\rVert\lVert\mathbf x\rVert\cos\theta: скалярний добуток вимірює вирівнювання, масштабоване за обома величинами. Кожен нейрон реагує у своєму власному напрямку ваги, причому зміщення зміщує його вхідну інформацію для активації. Це корисна відправна точка для інтерпретації відповіді нейрона, хоча сам по собі напрямок ваги не визначає, яку функцію представляє нейрон.

Наведений нижче код реалізує ті самі два етапи: total є zz, а total.tanh() застосовує активацію. Ми зберігаємо кінцевий шар лінійним для цього прикладу регресії:

import random


class Neuron:
    def __init__(self, n_inputs, rng, nonlinear=True):
        self.weights = [Value(rng.uniform(-1, 1)) for _ in range(n_inputs)]
        self.bias = Value(0)
        self.nonlinear = nonlinear

    def __call__(self, inputs):
        total = sum((w * x for w, x in zip(self.weights, inputs)), self.bias)
        return total.tanh() if self.nonlinear else total

    def parameters(self):
        return self.weights + [self.bias]


class MLP:
    def __init__(self, sizes, seed=42):
        rng = random.Random(seed)
        self.layers = [
            [Neuron(n_in, rng, nonlinear=i < len(sizes) - 2)
             for _ in range(n_out)]
            for i, (n_in, n_out) in enumerate(zip(sizes, sizes[1:]))
        ]

    def __call__(self, inputs):
        for layer in self.layers:
            inputs = [neuron(inputs) for neuron in layer]
        return inputs

    def parameters(self):
        return [p for layer in self.layers for neuron in layer
                for p in neuron.parameters()]

MLP([2, 4, 1]) створює два входи, чотири приховані нейрони з tanh і один лінійний вихід. Його параметрами є 8 прихованих ваг, 4 прихованих зміщення, 4 вихідні зміщення та 1 вихідне зміщення: 17 скалярів, які можна навчити.

Навчіть його на XOR, кодуючи однакові входи як −1-1 і різні входи як +1+1. Жодна афінна функція двох входів не може точно відповідати всім чотирьом цілям; приховані нелінійні активації дозволяють моделі підганяти їх.

def train_xor(steps=1000, learning_rate=0.1):
    # Same inputs twice equal -1; different inputs equal +1.
    examples = [([0, 0], -1), ([0, 1], 1), ([1, 0], 1), ([1, 1], -1)]
    model = MLP([2, 4, 1], seed=42)
    parameters = model.parameters()
    for _ in range(steps):
        loss = sum((model(x)[0] - y) ** 2 for x, y in examples) * 0.25
        loss.backward()
        for parameter in parameters:
            parameter.data -= learning_rate * parameter.grad
    predictions = [model(x)[0].data for x, _ in examples]
    final_loss = sum((p - y) ** 2 for p, (_, y) in zip(predictions, examples)) / 4
    return predictions, final_loss

Структура навчання не змінилася: побудова прогнозів і скалярних втрат, виклик backward() і оновлення параметрів. Двигун бачить операції та краї. Йому не потрібно знати, які операції належать нейрону чи шару.

Запустіть повний файл:

python3 tinygrad.py

З наданим початковим кодом, 1000 кроками та швидкістю навчання 0.10.1 передбачення XOR округляються до [-1.0, 1.0, 1.0, -1.0] з четвертим знаком після коми. Це невелика демонстрація з фіксованими даними та ініціалізацією, а не гарантія для довільних мереж або швидкості навчання.

До чого призвела втрата

Ми почали з спроби довколишніх значень параметрів у втраті іграшки, а потім вивели правило, яке безпосередньо визначає його локальну швидкість зміни. Прослідкувавши похідні за допомогою простих операцій, ми вибрали функцію прогнозування та приєднали збиток. Заміна зробила цю втрату функцією параметрів моделі, даючи нам те ж саме точне запитання: як зміниться ця мета, якщо зміниться один параметр?

Втрата визначає мету та її чутливість до прогнозу. Модель надає шляхи від параметрів до цього прогнозу. Зворотне поширення поєднує локальні похідні вздовж цих шляхів, додаючи внески там, де шляхи зустрічаються. Градієнтний спуск використовує отримані градієнти параметрів для вибору оновлення.

Для моделі, побудованої на основі операцій із придатними для використання правилами похідних, скалярні втрати дають нашому механізму диференціювання вихідні дані для початку та мету, яку оптимізатор повинен зменшити.

Щоб дізнатися більше про нейронні мережі, див. Як нейронні мережі навчаються: зворотне поширення та градієнтний спуск. Щоб отримати додаткову реалізацію, яка приєднує зворотні функції до операцій, перегляньте Micrograd Engine Карпаті і його Нейронні мережі: матеріали від нуля до героя.

Перегляд зображення

Змінюйте масштаб жестом щипка або клавішами + / −. Переміщуйте зображення перетягуванням або стрілками. Esc — закрити.