У темі про градієнтний спуск ми домовились: щоб покращити модель, треба знати похідну втрати по кожному параметру. Тоді параметрів було два, і питання «як його порахувати» навіть не поставало. Тепер параметрів тисячі, а в справжніх моделях — мільярди, і питання стає головним. Уся ця лекція відповідає рівно на нього.
Попередня тема, нейронні мережі, показала мережу як конструкцію й оголосила, що градієнт по всіх вагах рахують зворотним поширенням. Ми взяли це на віру й пішли далі. Час повернутись і розібрати механізм до останнього множення: звідки береться кожна похідна, чому їх рахують у зворотному порядку і чому саме цей порядок робить навчання можливим узагалі.
01 / ЗадачаМільйон питань до однієї помилки
Наскрізний приклад цієї лекції — дошка оголошень про вживані телефони з теми про розвідку даних. Задача та сама: відрізнити шахрайське оголошення від чесного. Ознак візьмемо дві, щоб усе вміщалось на одному малюнку:
- вік акаунта продавця — шахрай частіше працює зі свіжого;
- відхилення ціни від типової для цієї моделі телефона — приманка буває або підозріло дешевою, або підозріло дорогою.
Обидві ознаки стандартизовані: нуль означає «як у середнього оголошення», одиниця —
«на одне стандартне відхилення вбік». Мережу візьмемо крихітну: два входи, два нейрони
прихованого шару з tanh, один вихід із сигмоїдою. Девʼять чисел усього —
чотири ваги першого шару, два зсуви, дві ваги другого й один зсув.
Мережа порахувала відповідь, ми порівняли її з правдою і отримали одне число —
втрату L. Тепер треба відповісти на девʼять окремих питань: «якщо цю вагу
збільшити на крихітну величину, наскільки зміниться L?». Девʼять відповідей —
це і є градієнт, і саме з нього градієнтний спуск робить крок.
Девʼять питань — не проблема. Проблема починається, коли їх мільйон. Найочевидніший спосіб отримати відповідь ми вже знаємо з теми про градієнтний спуск: ворухнути вагу й подивитись, що станеться. Це чисельна похідна. Вона проста, чесна і працює завжди. Але кожне питання коштує повного прогону мережі, а то й двох. Мільйон ваг — два мільйони прогонів на один крок навчання. А кроків треба десятки тисяч.
Зворотне поширення помилки (backpropagation) відповідає на всі мільйон питань одразу, за час одного додаткового прогону. Не швидше в рази — швидше в мільйон разів. І весь фокус тримається на одному правилу з першого курсу матаналізу, яке ми зараз виведемо з нуля, без матаналізу.
02 / ЛанцюгЛанцюгове правило
Забудьмо на кілька хвилин про мережі. Ти продаєш телефон, який купуєш за кордоном за 250 доларів. Ціна в гривнях залежить від курсу долара, а курс останнім часом росте приблизно на 30 копійок за день. Питання: на скільки гривень подорожчає телефон за день?
Відповідь очевидна, і в ній уже вся ідея. Один день додає до курсу 0.30 гривні. Одна гривня курсу додає до ціни 250 гривень, бо телефон коштує 250 доларів. Отже, один день додає до ціни 0.30 × 250 = 75 гривень. Ми перемножили два впливи.
Схема 1 · Ланцюгове правило на побутовому прикладі
День впливає на курс, курс — на ціну. Щоб дізнатись вплив дня на ціну, впливи перемножують.
Це і є ланцюгове правило (chain rule). Формально: якщо величина
c залежить від b, а b залежить від a, то
Читається зліва направо так: «вплив a на c дорівнює впливу
b на c, помноженому на вплив a на b».
Символ ∂ («частинна похідна») означає рівно те, що ми щойно робили словами:
наскільки зміниться верхня величина, якщо нижню зрушити на одиницю, а все інше лишити
на місці.
Ланцюг може бути будь-якої довжини. Якщо між днем і ціною стоять ще три посередники — множників стане пʼять. Правило не змінюється: вплив уздовж шляху — це добуток впливів на кожній ланці. А якщо шляхів кілька (курс впливає і на ціну закупівлі, і на вартість доставки), то впливи вздовж різних шляхів додаються. Два цих речення — весь математичний апарат зворотного поширення. Більше нічого не буде.
Правило про кілька шляхів варто побачити окремо, бо в мережі воно працює постійно. Уяви, що курс долара впливає не лише на ціну закупівлі, а ще й на вартість доставки, а кінцева ціна складається з обох. Тоді вплив дня на кінцеву ціну — це сума двох добутків: один уздовж першого шляху, другий уздовж другого. У мережі так поводиться кожен прихований нейрон: він годує всі нейрони наступного шару, тому його провина — це сума провин, що прийшли з усіх цих напрямків. У нашій крихітній мережі шлях від прихованого нейрона рівно один, бо вихідний нейрон теж один, — але формула в розділі 07 записана вже із сумою.
03 / ГрафГраф і локальна похідна
Тепер повернімось до мережі й подивимось на неї не як на «шари нейронів», а як на
обчислювальний граф — послідовність простих операцій, де результат кожної
йде на вхід наступним. Наша мережа розкладається на такі операції: помножити вхід на вагу,
додати результати й зсув, застосувати tanh, знову помножити й додати,
застосувати сигмоїду, порахувати втрату. Усе. Жодної операції складнішої за множення,
додавання й одну функцію одного аргументу.
Ключова властивість такого розкладу: кожен вузол знає лише свою локальну
похідну — як його власний вихід залежить від його власних входів. Вузол множення
u = w·x знає, що ∂u/∂w = x і ∂u/∂x = w. Вузол
tanh знає, що ∂a/∂z = 1 − a². Ці знання не залежать ні від решти
мережі, ні від функції втрат, ні від даних — вони випливають із самої операції.
Тепер поєднаймо це з ланцюговим правилом. Уяви, що вузлу хтось повідомив ззовні одне число: наскільки його вихід впливає на втрату. Цього досить, щоб вузол самотужки порахував, наскільки на втрату впливає кожен його вхід: треба помножити отримане число на свою локальну похідну. І передати результат далі, тим, хто стоїть перед ним.
Схема 2 · Що відбувається в одному вузлі
Уперед течуть значення, назад — похідні. Вузол множить те, що прийшло справа, на свою локальну похідну.
tanh дорівнює 0.5591. Добуток 0.4630
іде далі ліворуч.Ось чому обчислення організують саме як прохід назад. Щоб вузол міг щось порахувати, він мусить спершу отримати число від того, хто стоїть після нього. Значить, рахувати треба з кінця: спочатку вузол втрати, потім вихідний нейрон, потім прихований шар, потім ваги першого шару. Порядок обчислень протилежний до порядку, у якому текли дані. Звідси й назва — зворотне поширення.
Число, яке вузол отримує справа, має власне імʼя. Для нейрона це похідна втрати по його
передактиваційному значенню z, і її позначають грецькою
буквою дельта:
Дельту зручно читати як «наскільки цей нейрон винен у помилці». Чим більша дельта за модулем, тим сильніше зміна цього нейрона зрушила б втрату. Знак каже, у який бік. Усі градієнти по вагах виводяться з дельт одним рухом — це ми зробимо в розділі 07.
04 / УпередПрямий прохід із числами
Далі — повний розрахунок на реальних числах. Візьмімо одне оголошення з дошки:
акаунт продавця молодший за середній на одне стандартне відхилення, а ціна нижча за
типову для цієї моделі на півтора. Після стандартизації ознаки дорівнюють рівно
x₁ = −1.00 і x₂ = −1.50. Оголошення справді шахрайське, тому
правильна відповідь y = 1.
Ваги мережі поки що такі (у справжньому навчанні вони прийшли б із випадкової ініціалізації, тут ми їх просто зафіксували, щоб числа можна було перевірити):
Прямий прохід — це просто підстановка. Перший нейрон прихованого шару рахує зважену
суму z₁ = (−0.40)·(−1.00) + (−0.20)·(−1.50) + 0.10 = 0.80, другий отримує
z₂ = −1.20. Далі кожен пропускає своє число крізь tanh і дає
0.6640 та −0.8337. Вихідний нейрон зважує ці два числа
й отримує z = −1.1140, сигмоїда перетворює його на ймовірність
ŷ = 0.2471.
Мережа сказала: «шанс шахрайства 25 %». Правильна відповідь — 1. Модель упевнено помиляється, і крос-ентропія це фіксує:
Для орієнтира: якби мережа відповіла «не знаю» (ŷ = 0.5), втрата була б 0.69. Тобто модель зараз удвічі гірша за монетку. Прямий прохід закінчено — ми маємо число, яке треба зменшити, і девʼять ваг, які можна для цього крутити. Далі найцікавіше.
05 / НазадЗворотний прохід із числами
Починаємо з кінця. Останній вузол — сигмоїда з крос-ентропією. У попередній темі ми бачили, що ця пара дає найкоротшу з можливих похідних: множники в чисельнику й знаменнику скорочуються повністю, і лишається чиста різниця:
Знак відʼємний, і це осмислено: щоб зменшити втрату, z вихідного нейрона
треба збільшити (тоді сигмоїда підніме ŷ ближче до одиниці). Модуль 0.75 — це
майже максимум, який тут узагалі можливий: різниця «прогноз мінус правда» не перевищує
одиниці.
Тепер це число розповзається по мережі. Вихідний нейрон роздає його своїм двом ребрам,
множачи на те, що по цьому ребру прийшло вперед. По першому ребру прийшло
a₁ = 0.6640, по другому a₂ = −0.8337:
Зверни увагу на знаки. Перша вага має відʼємний градієнт — її треба збільшувати. Друга має додатний — її треба зменшувати. Причина в тому, що другий нейрон видав відʼємне число, і множення на нього перевернуло знак провини. Так і має бути: щоб підняти вихід, вагу при відʼємному вході треба опустити.
Далі провина йде крізь вихідний нейрон до прихованого шару. Тут
працюють обидві частини ланцюгового правила. Спершу вплив множиться на вагу ребра, бо
саме через неї нейрон впливає на вихід: −0.7529 × (−1.10) = 0.8282 для
першого і −0.7529 × 0.70 = −0.5270 для другого. Потім кожне число множиться
на локальну похідну tanh у своїй точці:
Ось перше місце, де видно майбутню біду. Множник 1 − a² для першого
нейрона дорівнює 0.56, для другого — 0.31. Провина, що пройшла крізь другий нейрон,
ослабла втричі просто тому, що він працює далі від нуля. Це ще один шар — а якби їх
було двадцять, множники перемножились би двадцять разів. Про це в розділі 09.
Лишилось роздати дельти прихованого шару по вагах першого. Правило те саме, що для
другого шару: помножити провину нейрона на те, що по цьому ребру прийшло. А приходять
сюди самі ознаки:
∂L/∂W₁₁ = 0.4630 × (−1.00) = −0.4630,
∂L/∂W₁₂ = 0.4630 × (−1.50) = −0.6945, і так само для другого нейрона.
Зсуви отримують дельту без множення взагалі — бо зсув входить у суму з коефіцієнтом
одиниця.
Пройди цей маршрут сам. Кнопки ведуть спершу вперед, від ознак до втрати, потім назад, від втрати до кожної ваги. Кожне число стоїть на своєму ребрі: бірюзове — значення, що йде вперед, рожеве — похідна, що повертається назад.
Інтерактив 1 · Граф із числами: уперед і назад
Мережа 2 → 2 → 1 на одному оголошенні. Одинадцять кроків: пʼять уперед, пʼять назад і крок спуску.
Останній крок кнопки — це вже не backprop, а градієнтний спуск: усі девʼять ваг
одночасно зсуваються проти градієнта з кроком η = 0.5. Результат варто
запамʼятати: прогноз піднявся з 0.2471 до 0.6136,
втрата впала з 1.3979 до 0.4884. Один крок, і модель
уже на правильному боці.
Таблиця 1 · Повний ручний розрахунок
Усе, що ми щойно порахували, в одному місці. Візьми олівець і пройди зверху вниз.
| величина | як рахується | результат |
|---|---|---|
| z₁⁽¹⁾ | (−0.40)·(−1.00) + (−0.20)·(−1.50) + 0.10 | 0.8000 |
| z₂⁽¹⁾ | 0.50·(−1.00) + 0.40·(−1.50) + (−0.10) | −1.2000 |
| a₁⁽¹⁾ | tanh(0.8000) | 0.6640 |
| a₂⁽¹⁾ | tanh(−1.2000) | −0.8337 |
| z⁽²⁾ | (−1.10)·0.6640 + 0.70·(−0.8337) + 0.20 | −1.1140 |
| ŷ | σ(−1.1140) | 0.2471 |
| L | −log(0.2471) | 1.3979 |
| δ⁽²⁾ | ŷ − y = 0.2471 − 1 | −0.7529 |
| ∂L/∂w₁⁽²⁾ | δ⁽²⁾ · a₁⁽¹⁾ = (−0.7529)·0.6640 | −0.4999 |
| ∂L/∂w₂⁽²⁾ | δ⁽²⁾ · a₂⁽¹⁾ = (−0.7529)·(−0.8337) | 0.6276 |
| ∂L/∂b⁽²⁾ | δ⁽²⁾ · 1 | −0.7529 |
| 1 − a₁² | 1 − 0.6640² | 0.5591 |
| 1 − a₂² | 1 − 0.8337² | 0.3050 |
| δ₁⁽¹⁾ | (−0.7529)·(−1.10)·0.5591 | 0.4630 |
| δ₂⁽¹⁾ | (−0.7529)·0.70·0.3050 | −0.1607 |
| ∂L/∂W₁₁⁽¹⁾ | δ₁⁽¹⁾ · x₁ = 0.4630·(−1.00) | −0.4630 |
| ∂L/∂W₁₂⁽¹⁾ | δ₁⁽¹⁾ · x₂ = 0.4630·(−1.50) | −0.6945 |
| ∂L/∂W₂₁⁽¹⁾ | δ₂⁽¹⁾ · x₁ = (−0.1607)·(−1.00) | 0.1607 |
| ∂L/∂W₂₂⁽¹⁾ | δ₂⁽¹⁾ · x₂ = (−0.1607)·(−1.50) | 0.2411 |
| ∂L/∂b₁⁽¹⁾ | δ₁⁽¹⁾ · 1 | 0.4630 |
| ∂L/∂b₂⁽¹⁾ | δ₂⁽¹⁾ · 1 | −0.1607 |
06 / ПорядокЧому назад, а не вперед
Похідні можна рахувати й у прямому порядку — це називають прямим режимом
диференціювання. Виглядає він природніше: беремо одну вагу, питаємо «як від неї залежить
z₁», потім «як від z₁ залежить a₁», і так до самої
втрати. Один прохід графом, усе чесно.
Проблема в тому, що такий прохід дає похідну однієї величини по одному входу. Хочеш другу вагу — прохід заново. Девʼять ваг — девʼять проходів. Мільйон ваг — мільйон проходів.
Зворотний режим влаштований дзеркально. Він рахує похідні однієї вихідної величини по всіх входах одразу. Один прохід — і на кожному ребрі графа вже лежить своя похідна. І тут вирішальний факт про машинне навчання: вихідна величина в нас одна. Функція втрат — це завжди одне число. Скільки б параметрів не було, на виході графа стоїть скаляр, і зворотний режим підходить ідеально.
Схема 3 · Два напрямки диференціювання
Ліворуч — по одному входу за прохід. Праворуч — усі входи за один прохід.
Порахуймо ціну обох підходів у прогонах мережі. Чисельна похідна за центральною різницею потребує двох прогонів на кожен параметр — один зі зсувом уперед, один назад. Backprop потребує одного прямого проходу й одного зворотного, а зворотний коштує приблизно стільки ж, скільки прямий. Разом — два прогони на всі параметри, скільки б їх не було.
| мережа | ваг | чисельно, прогонів | backprop, прогонів | виграш |
|---|---|---|---|---|
| наша 2 → 2 → 1 | 9 | 18 | 2 | 9× |
| 2 → 32 → 16 → 1 | 641 | 1 282 | 2 | 641× |
| 784 → 128 → 10 | 101 770 | 203 540 | 2 | 101 770× |
| мільйон ваг | 1 000 000 | 2 000 000 | 2 | 1 000 000× |
Останній рядок варто перевести в час, бо саме там ховається відповідь на питання «чому глибокого навчання не було раніше». Припустімо, один прогін мережі з мільйоном ваг триває одну мілісекунду. Тоді чисельний градієнт для одного кроку спуску займе 2000 секунд — більш ніж пів години. Кроків для пристойного навчання треба хоча б десять тисяч. Отже, 20 мільйонів секунд, або сім місяців безперервного рахунку. Backprop зробить ту саму роботу за 20 секунд.
Інтерактив 2 · Ціна двох підходів
Скільки прогонів мережі коштує один повний градієнт. Вертикальна вісь логарифмічна.
a з прямого проходу —
тому навчання зʼїдає значно більше памʼяті, ніж прогноз. Ця памʼять і є справжня ціна
backprop, а не час.07 / ФормулиГрадієнти по вагах і зсувах
Тепер запишемо правила загально — так, щоб вони працювали для будь-якого шару будь-якої ширини. Виведення коротке: усе, що для нього потрібно, ми вже порахували руками.
Вага w_jk входить у граф рівно в одному місці — у суму нейрона
j. Значить, увесь її вплив на втрату проходить через z_j
і більше ніде.
Друга частина рахується в один рядок: у сумі вага множиться на a_k,
тому похідна суми по вазі дорівнює a_k. Перша частина — це дельта, яку
приніс зворотний прохід. Ось і все правило: градієнт ваги — це провина
нейрона, помножена на те, що прийшло по ребру.
Зсув — це та сама вага, тільки вхід у неї завжди дорівнює одиниці. Тому градієнт зсуву дорівнює самій дельті. Ніякого окремого правила для зсувів не існує.
Нейрон k впливає на втрату не одним шляхом, а через усі
нейрони наступного шару, до яких від нього йде ребро. Тому впливи вздовж різних
шляхів додаються — це та сама сума, про яку ми говорили в розділі 02. А множник
f′ — локальна похідна власної активації.
Сума по j з кроку 4 — це і є множення на транспоновану матрицю ваг.
Знак ⊙ означає поелементне множення. У такому вигляді формули й живуть
у коді: три рядки на весь зворотний прохід.
Перевірмо крок 2 на наших числах ще раз, бо саме він найчастіше плутається. Градієнт
першої ваги другого шару має дорівнювати δ⁽²⁾ · a₁ = (−0.7529)·(0.6640) = −0.4999.
У таблиці стоїть рівно це. А градієнт першої ваги першого шару —
δ₁⁽¹⁾ · x₁ = 0.4630·(−1.00) = −0.4630. Правило те саме, змінилось лише те,
що прийшло по ребру: там був вихід прихованого нейрона, тут — сама ознака.
a поточного шару замість попереднього, або забути
транспонування в кроці 5. Код при цьому не падає — розміри матриць часто збігаються
випадково, особливо коли шари однакової ширини. Мережа навіть навчиться, просто гірше.
Саме проти цього існує наступний розділ.08 / КонтрольПеревірка градієнта
Ми маємо два способи дізнатись похідну. Аналітичний — формули backprop, швидкий, але його легко написати з помилкою. Чисельний — визначення похідної в лоб, повільний, зате помилитись у ньому майже неможливо: там нема чого плутати. Очевидна ідея: звірити один з одним.
Чисельну похідну беруть центральною різницею — тим самим прийомом, що в темі про градієнтний спуск:
Зсунули вагу трохи вправо, порахували втрату; зсунули трохи вліво, порахували втрату;
поділили різницю на пройдений шлях. Центральна різниця точніша за односторонню
[L(w+h) − L(w)] / h: у неї похибка спадає як h², а не як
h. Коштує вона два прогони замість одного — але ми й не збираємось нею
навчати.
Нижче — обидва способи поруч на нашій мережі. Повзунок крутить одну-єдину вагу
(W₁₁, ребро від віку акаунта до першого прихованого нейрона), а на екрані
одночасно видно: що стало з прогнозом, що стало з втратою, яку похідну дає backprop
і яку — чисельний спосіб. Кнопками перемикається крок h.
Інтерактив 3 · Ланцюг впливів і перевірка градієнта
Угорі — втрата як функція однієї ваги й дотична, нахил якої і є градієнт. Унизу — модуль градієнта.
Із цього інтерактиву варто винести дві речі, і вони різні.
Перша — про перевірку. Крок h не можна брати «якомога
меншим». Завеликий h дає похибку відсікання: січна проходить не там, де
дотична. Замалий дає похибку округлення: L(w+h) і L(w−h)
відрізняються в пʼятнадцятому знаку, а числа з рухомою комою стільки знаків просто не
тримають. Різниця виходить майже випадковою, ділення на крихітне 2h роздуває
цю випадковість до величезних значень. Практичний орієнтир — h близько
1e−5, і порівнювати не абсолютну різницю, а відносну:
|аналітичний − чисельний| / (|аналітичний| + |чисельний|). Менше
1e−7 — реалізація правильна; більше 1e−4 — майже напевно баг.
Друга — про сам ланцюг. Поведи повзунок до правого чи лівого краю.
Втрата на лівому краї величезна, майже 1.7, а градієнт при цьому — чотири тисячних. Модель
помиляється жахливо і не знає, куди рухатись. Причина одна: tanh
пішов у насичення, множник 1 − a² упав майже до нуля, і ланцюг впливу
обірвався на першій же ланці. Найсильніший сигнал — посередині, де активація ще не
насичена.
09 / МежаДе ланцюг рветься
Ми бачили це двічі: у розділі 05, де провина ослабла втричі на одному нейроні, і в інтерактиві 3, де на краю повзунка градієнт упав до чотирьох тисячних. Тепер узагальнимо одним реченням. Градієнт на вході в мережу — це добуток усіх локальних похідних уздовж шляху. Якщо кожен множник менший за одиницю, добуток на глибині двадцяти шарів стає нулем: це затухання градієнта. Якщо кожен більший за одиницю — добуток вибухає: це вибух градієнта. Backprop тут ні в чому не винен, він просто чесно перемножує те, що йому дали.
Це не дрібна деталь, а головне обмеження глибоких мереж, і в темі 33 це розібрано з таблицею: похідна сигмоїди тримається орієнтовно коло 0.25, тому на десяти шарах градієнт слабшає приблизно в мільйон разів. Що з цим роблять — вибір активації, ініціалізація ваг, нормалізація, залишкові зʼєднання — розбирає наступна тема, про функції активації. Тут досить запамʼятати, звідки береться сам ефект: із того, що ланцюгове правило множить, а не додає.
Із вибухом борються найпростішим із можливих способів — обрізанням норми (gradient clipping): якщо довжина вектора градієнта перевищила поріг, вектор скорочують до цього порога, лишивши напрямок незмінним. Прийом грубий, зате рятує від єдиного кроку, що відкидає всі ваги в нескінченність. Із затуханням так не вийде: помножити маленький градієнт назад на велике число можна, але це підсилить і шум разом із ним. Тому затухання лікують не після факту, а на етапі проєктування мережі.
10 / ДаліКуди далі
Підсумок теми вміщається в пʼять рядків:
- ланцюгове правило — вплив уздовж шляху це добуток впливів на ланках, а впливи різних шляхів додаються;
- граф — мережа розкладається на прості операції, кожна знає лише свою локальну похідну;
- прямий прохід рахує значення, зворотний — похідні, тим самим графом по тих самих ребрах;
- ефективність — усі градієнти за два прогони замість двох на кожну вагу; на мільйоні ваг це виграш у мільйон разів;
- перевірка градієнта — єдиний надійний спосіб довести, що backprop написаний без помилки.
Далі курс іде двома природними продовженнями. Тема про функції активації підхоплює розділ 09: які саме множники ставити в ланцюг, щоб він не рвався, і чому ReLU змінила все. Тема про оптимізатори бере вже готовий градієнт і питає, що з ним робити: чистий крок, момент, адаптивна швидкість, Adam. Backprop у них не змінюється взагалі — він лишається тим самим проходом назад по тому самому графу.
А ще раніше, в темі про перцептрон, ми бачили модель зі сходинкою замість активації. Тепер зрозуміло, чому її не можна навчати градієнтом: похідна сходинки дорівнює нулю всюди, ланцюг множиться на нуль на першій же ланці, і назад не приходить нічого. Backprop працює не тому, що мережа «схожа на мозок», а тому, що всі її операції диференційовні. І з тієї ж причини він працює далеко за межами нейромереж: будь-яке обчислення, розкладене на диференційовні кроки, можна продиференціювати зворотним режимом. Саме на цьому стоять сучасні бібліотеки автоматичного диференціювання.
practice.ipynb ти збереш цю саму
мережу 2 → 2 → 1 на чистому NumPy, відтвориш ручний розрахунок до останнього знака,
перевіриш усі девʼять градієнтів чисельно, навчиш мережу на дошці оголошень і порівняєш
її з MLPClassifier. А наприкінці навмисно зламаєш одну формулу й побачиш, як
чисельна перевірка ловить помилку, якої не видно ні з коду, ні з кривої втрат.Далі в темі
Теорію прочитано. Тепер закріпи її на практиці.