Мережу ми зібрали в темі 09, дані підготували в темі 10. Здавалось би, лишилось написати цикл і чекати. Але саме тут курс уперше стикається з тим, що той самий код на тих самих даних дає точність 1.000 або 0.167 залежно від дрібниць, які ніде не викликають помилки.
Ця тема — про ці дрібниці. Не про градієнтний спуск і не про те, як працює
Adam, а про специфічне саме для згорткових мереж: у якому масштабі подавати
пікселі, навіщо між згорткою й ReLU зʼявився ще один шар, чому одне й те
саме число швидкості навчання буває і замалим, і завеликим — і як прочитати діагноз із
двох кривих.
Conv → ReLU → Pool і голова з двох
повнозвʼязних шарів, 24 774 параметри. Усі числа нижче заміряні на ній.ЗведенняДосі воно просто працювало
Скажімо чесно: у темах 06-09 навчання щоразу спрацьовувало з першого разу — і не тому, що ми все робили правильно, а тому, що задача крихітна: шість намальованих формулами фігур, майже без шуму, мережа на двадцять чотири тисячі параметрів. На справжніх даних так не буває. Ось приклад — та сама мережа, ті самі дані, змінене одне число:
| швидкість навчання | точність на перевірці | що видно на кривій |
|---|---|---|
| 0.0003 | 0.273 | втрата стоїть на 1.79 всі вісім епох |
| 0.03 | 0.917 | втрата спадає до 0.04 |
| 1.0 | 0.167 | втрата злітає до 1032 |
0.167 — це рівно 1/6, рівень вгадування на шести класах: мережа не навчилась нічого. І — важливо — жоден із трьох прогонів не викинув помилки. Тому тема відповідає на два питання: що ввімкнути, щоб такі провали стали рідкістю, і як їх розпізнати, коли вони все одно трапляться.
ВхідНормалізація входу
Зображення з файлу — це цілі числа від 0 до 255. Ваги свіжої мережі — це числа порядку сотих. Помнож одне на друге, і побачиш, у чому проблема.
Візьмемо нашу мережу одразу після створення й подивимось на два числа: типовий розмір ваги першої згортки й розмах того, що ця згортка видає.
| вхід | середнє | відхилення | відгук згортки: розмах |
|---|---|---|---|
| 0 … 255 | 54.97 | 93.44 | −364.5 … 282.2 |
| 0 … 1 | 0.216 | 0.366 | −1.14 … 1.15 |
| нормалізований | 0.000 | 1.000 | −2.81 … 2.34 |
Ваги в усіх трьох рядках ті самі — мережа одна, зерно одне: стандартне відхилення
0.190, межі ±0.33. А розмах відгуку на сирому вході більший, ніж на
нормалізованому, у 104 рази (66.6 проти 0.64 за стандартним
відхиленням). Далі відгук іде в градієнт, градієнт множиться на швидкість навчання — і
виходить, що масштаб даних тихо помножив твою швидкість навчання на
невідоме число.
Що саме ламається
Найпоширеніше пояснення — «без нормалізації мережа не навчиться» — неправдиве, і легко спростовується заміром: три масштаби входу × чотири швидкості навчання, по шість епох на 360 прикладах, однакове зерно.
Масштаб входу зсуває вікно робочих швидкостей
Ліворуч — розподіл значень, які приходять у мережу: форма та сама, змінюється лише шкала осі й місце нуля. Праворуч — крива втрат. Перемкни масштаб і пройди повзунком усі чотири швидкості: робоче значення щоразу інше.
0…255 найкраще працює при швидкості
0.003, нормалізований — при 0.03, а 0…1 не дійшов до пристойної точності
в жодному з чотирьох значень. Одне й те саме число буває і замалим, і завеликим —
залежно лише від того, як ти поділив пікселі.Результат несподіваний двічі. По-перше, сирий вхід 0…255 чудово
навчився — 1.000 при швидкості 0.003; твердження «без нормалізації нічого не
вийде» замір не підтверджує. По-друге, найгіршим виявився нібито безпечний
0…1: жодне з чотирьох значень не дало навіть 0.6.
Читай таблицю не по рядках, а по вікнах: у кожного масштабу свій проміжок робочих швидкостей, і проміжки майже не перекриваються. Візьми звичну для нормалізованих даних 0.03 і подай сирі пікселі — точність упаде до 0.167. Візьми 0.003, ідеальну для сирих, і подай нормалізовані — мережа доповзе лише до 0.343.
Ось точне формулювання, заради якого й потрібен цей розділ: нормалізація не робить навчання можливим — вона робить швидкість навчання переносною. Число з чужого репозиторію спрацює й у тебе, якщо ви обидва подаєте дані з нульовим середнім і одиничним відхиленням. Інакше ви говорите різними мовами.
Два числа й одне правило
Нормалізація зображення — це два кроки на канал: відняти середнє, поділити на стандартне відхилення.
Тут x — значення пікселя, μканал — середнє всіх пікселів цього каналу по всіх зображеннях, σканал — їхнє типове відхилення від цього середнього. Після дії середнє стає нулем, а відхилення — одиницею.
Для кольорового зображення чисел буде по три — середнє червоного, зеленого й синього окремо. Усереднення йде по прикладах, висоті й ширині, але не по каналах:
channel_mean = train_images.mean(dim=(0, 2, 3), keepdim=True) # приклади, висота, ширина channel_std = train_images.std(dim=(0, 2, 3), keepdim=True)
На нашому датасеті вийшло μ = 54.97 і σ = 93.44. Числа
конкретні, і саме вони мусять поїхати разом із моделлю.
Наслідок, який кусає вже після навчання: ці два числа — частина
моделі. Зберіг ваги, а μ і σ лишились у коді, яким ти їх
рахував, — модель
у продакшені працюватиме погано й без жодного повідомлення про помилку. Зберігай їх
поруч із вагами.
СтартІніціалізація
Ваги теж не беруться абиякими — це вже розібрано в темі про активації й ініціалізацію. Тут — два факти, які стосуються згорток.
Перший: усі ваги нулями — це не «нейтральний старт», а поломка. Якщо всі ядра однієї згортки однакові, то й відгуки однакові, і градієнти однакові — і вони лишаться однаковими назавжди. Вісім каналів першої згортки будуть вісьмома копіями одного. Випадковість початкових ваг тут не прикраса, а єдине, що розрізняє нейрони.
Другий: для ReLU беруть ініціалізацію Хе (He). Розмах
початкових ваг обирають так, щоб сигнал не згасав і не вибухав, проходячи крізь десяток
шарів. ReLU обнуляє половину значень, тому дисперсію ваг беруть удвічі
більшою, ніж дала б звична ініціалізація Ксавʼє (Xavier).
Для згортки nвх — не кількість вхідних каналів, а кількість чисел, які ядро перемножує на одну вихідну клітинку: канали × площа ядра. Для другої згортки нашої мережі це 8 × 3 × 3 = 72, тобто σ ≈ 0.167.
nn.Conv2d у PyTorch уже
ініціалізується за Хе (варіант kaiming_uniform) — робити це окремо не треба.
Знати треба: зустрівши чужий код із nn.init.normal_(w, std=0.01) для глибокої
мережі з ReLU, ти зрозумієш, чому вона не вчиться.БатчнормБатчнорм
Нормалізація входу привела до ладу числа на вході. Але після першої ж згортки вони знову розповзаються: розподіл усередині мережі залежить від ваг, а ваги міняються на кожному кроці. Батч-нормалізація (batch normalization, батчнорм) — шар, який робить із проміжними числами те саме, що ми зробили з входом, і робить це на кожному кроці. Формула та сама, з двома додатковими вагами:
Спершу число приводять до нульового середнього й одиничного відхилення — це μбатч і σбатч по поточному батчу. Потім результат множать на γ і додають β — дві вивчувані ваги на канал. Вони дають мережі змогу при потребі скасувати нормалізацію, якщо шару вигідніший інший масштаб.
Чому в CNN нормалізують по каналу, а не по елементу
Це головна відмінність батчнорму в згортковій мережі. Візьмемо карту ознак після нашої першої згортки: 8 каналів по 28×28, батч із 32 зображень — разом 200 704 числа. Скільки пар (μ, σ) рахувати?
Своя пара чисел на кожну клітинку карти: для позиції (0, 0), для (0, 1) і так далі — вийде 8 × 28 × 28 = 6 272 пари. І це неправильно. Канал згортки — це одне ядро, застосоване в кожній точці, тому відгук у (0, 0) і відгук у (14, 14) — те саме вимірювання тієї самої прикмети, просто в іншому місці.
Пара чисел на канал: усереднюємо по батчу, по висоті й по ширині разом. Вийде 8 пар — по одній на вивчену прикмету.
Кожне середнє порахуване по 25 088 значеннях, тому воно стійке. У варіанті «по елементу» кожне спиралось би на 32 числа — і шуміло б.
Саме тому шар зветься nn.BatchNorm2d(8): єдиний його аргумент — кількість
каналів, а параметрів у ньому рівно 2 × 8 = 16. У всій нашій мережі батчнорм
додає 112 ваг до 24 774 — менш ніж пів відсотка.
Що це дає: замір
Ставимо nn.BatchNorm2d між згорткою й ReLU — повний сучасний
блок виглядає як Conv → BatchNorm → ReLU → Pool — і порівнюємо з тією самою
мережею без нього. Однакове зерно, 8 епох, 360 прикладів, SGD із моментом 0.9.
| швидкість 0.02 | епох до 0.90 | точність наприкінці |
|---|---|---|
| без батчнорму | 7 | 0.983 |
| з батчнормом | 2 | 1.000 |
Це і є перший результат: з батчнормом мережа дійшла до 0.90 за дві епохи замість семи. Не «трохи швидше» — у три з половиною рази менше роботи до того самого результату.
Головне практичне: наскільки більший крок можна взяти
Але не заради прискорення батчнорм ставлять у кожну сучасну мережу, а заради ось цього: він піднімає стелю швидкості навчання. Ту саму пару мереж проганяємо по пʼяти значеннях.
| швидкість | без батчнорму | з батчнормом |
|---|---|---|
| 0.05 | 0.960 | 0.980 |
| 0.10 | 0.913 | 0.993 |
| 0.15 | 0.167 | 0.997 |
| 0.20 | 0.167 | 0.937 |
| 0.30 | 0.167 | 0.167 |
Без батчнорму навчання розвалюється між 0.10 і 0.15; з батчнормом — між 0.20 і 0.30. Стеля піднялась приблизно вдвічі. Найчіткіше це видно в рядку 0.15: те саме число, той самий датасет, те саме зерно — і 0.167 проти 0.997.
Є й другий, менш помітний ефект. При швидкості 0.1 мережа без батчнорму формально теж
дійшла до 0.90 — але її точність по епохах
0.50 0.63 0.51 0.91 0.69 0.74 0.74 0.91 не вчиться, а стрибає. З батчнормом
на тій самій швидкості після четвертої епохи крива виходить нагору й тримається біля
одиниці.
Батчнорм і стеля швидкості навчання
Дві криві точності на одній сітці: та сама мережа з батчнормом і без. Пересунь повзунок швидкості й подивись, де кожна з них ламається.
Пастка: у режимі перевірки батчнорм працює інакше
Коли мережа рахує відповідь для нового зображення, батча немає — є одна картинка. По чому тоді рахувати μбатч і σбатч? Ні по чому. Тому під час навчання батчнорм веде окремий бухгалтерський запис — біжучі середнє й дисперсію по всіх батчах, які бачив, — а в режимі перевірки бере їх замість статистик поточного батча. Це принципова різниця: один шар рахує дві різні формули залежно від режиму.
| Режим | Що бере батчнорм | Наслідок |
|---|---|---|
навчання (model.train()) | середнє й відхилення поточного батча | відповідь на одне зображення залежить від того, які зображення лежать поруч у батчі |
перевірка (model.eval()) | накопичені біжучі середнє й відхилення | відповідь залежить лише від самого зображення |
А тепер найважливіше: забути model.eval() — не помилка часу виконання.
Мережа не впаде, не попередить і чесно надрукує точність. Просто не ту.
Забутий model.eval()
Одна навчена мережа з батчнормом, один і той самий перевірочний набір із 300 зображень. Міняється лише розмір батча, яким його прогнали — і те, чи згадали про режим.
Спершу зверни увагу на бірюзові стовпчики: з model.eval() точність
однакова при будь-якому батчі — рівно 0.980. Це і є сенс режиму
перевірки: відповідь на зображення не залежить від того, які картинки лежать поруч.
А тепер рожеві. При батчі 300 забутий eval() дає навіть трохи більше —
0.990, різниця в одне відсоткове очко: помилки не видно взагалі. При батчі 1 та сама
помилка коштує 13 відсоткових пунктів — бо батчнорм у режимі навчання
нормалізує кожне зображення саме собою, і після цього від нього мало що лишається.
Висновок жорсткий: помилка, якої не видно на великому батчі, зʼявляється саме в продакшені, де зображення приходять по одному. Локально все було добре, на сервері точність упала — і жоден лог нічого не показав.
eval()
у мережі з дропаутом, ти отримаєш точність, яка міняється від виміру до виміру:
пʼять прогонів на тих самих даних дали 0.973, 0.977, 0.983, 0.983, 0.973 замість
стабільної 1.000. Різниця маленька, зате відтворюваність зникає зовсім.КрокШвидкість навчання
Швидкість навчання (learning rate) — множник, на який домножується градієнт перед зміною ваги. У згорткових мережах вона важить більше за все інше разом узяте: жодне інше число не перетворює 1.000 на 0.167 такою легкою рукою. Замір: та сама мережа без батчнорму, 8 епох, SGD із моментом 0.9, міняється лише швидкість.
| швидкість | втрата, епоха 1 | втрата, епоха 8 | точність | режим |
|---|---|---|---|---|
| 0.0003 | 1.79 | 1.79 | 0.273 | повзе |
| 0.003 | 1.79 | 1.76 | 0.320 | повзе |
| 0.03 | 1.79 | 0.14 | 0.917 | збігається |
| 0.3 | 1.80 | 1.81 | 0.167 | розходиться |
| 1.0 | 1.86 | 1.85 | 0.167 | вибухає |
Три режими видно з першої колонки чисел, і кожен має свій характерний слід.
Втрата спадає так повільно, що на графіку виглядає прямою: при 0.0003 вона за вісім епох не зрушила з 1.79 у третьому знаку. Що робити: збільшити швидкість у 3-10 разів. Не в 1.5 — на цій шкалі півтора рази не видно.
Наскільки саме — видно з тієї самої кривої. Якщо втрата не зрушила навіть у третьому знаку (наш рядок 0.0003: 1.79 на першій епосі й 1.79 на восьмій), ти далеко, і множити доведеться кілька разів поспіль — робочою тут виявилась 0.03, тобто у сто разів більша. Якщо втрата повзе, але таки спадає (рядок 0.003: 1.79 → 1.76), ти вже поруч, і одного множення на десять вистачає. Ознака проста: чи міняється втрата взагалі.
Втрата спадає впевнено: 1.79 → 1.77 → 1.68 → 1.11 → 0.89 → 0.22 → 0.04. Дрібні коливання наприкінці (0.04 → 0.14) — норма, це шум батчів.
Найпідступніший режим, бо виглядає майже як «повзе». При 0.3 втрата теж стоїть
близько 1.8 — але вона там не ще, а вже: перші кроки викинули ваги
так далеко, що мережа видає всім класам однакову відповідь. Число
ln 6 = 1.792 — це рівно те, що дає рівномірний розподіл на шести класах.
Як відрізнити від «повзе»: при замалій швидкості втрата спадає нехай і на соті, але монотонно. При завеликій вона встигає смикнутись: при 0.3 за другу епоху падає до 1.62, а на третій підстрибує до 2.03 — вище за початкові 1.80, — і аж потім осідає на плато.
Крайній випадок того самого: при 1.0 середня втрата за другу епоху дорівнює 1.1 × 1032. Далі числа переповнюються — тут уже нема чого діагностувати.
Три режими на одній кривій
Крива втрат по епохах для обраної швидкості навчання. Шкала втрат логарифмічна — інакше вибух при 1.0 розчавив би решту кривих у пряму лінію.
ln 6 = 1.792: стільки коштує
відповідь «не знаю, всі класи однаково ймовірні». Крива, що лежить на ній, означає
мертву мережу незалежно від причини.РозкладРозклад швидкості
Одне число на все навчання — не обовʼязково: швидкість можна змінювати по ходу. На початку ваги випадкові, і треба великими кроками дійти до потрібної області; наприкінці мережа вже майже там, і великий крок лише перестрибує мінімум. Спершу швидко, потім повільно.
| Розклад | Як міняється швидкість | Коли брати |
|---|---|---|
| Стала | ніяк | коли треба просто перевірити, що мережа вчиться |
| Сходинковий (step) | ділиться на 10 у заздалегідь названі моменти | класика; вимагає вгадати, коли саме ділити |
| Косинусний (cosine) | плавно спадає до нуля за чвертю косинусоїди | замовчування для більшості сучасних робіт: нічого не треба вгадувати, крім довжини навчання |
| З розігрівом (warmup) | перші кілька відсотків кроків швидкість росте від нуля | коли початкова швидкість велика — розігрів не дає першим крокам зруйнувати ваги |
Замір: 10 епох, початкова швидкість 0.1 у всіх чотирьох випадках. Число обране навмисно на межі — саме там розклад і має значення.
| розклад | точність наприкінці | найкраща за навчання |
|---|---|---|
| стала | 0.167 | 0.913 |
| сходинки | 0.993 | 0.993 |
| косинус | 1.000 | 1.000 |
| розігрів + косинус | 1.000 | 1.000 |
Перший рядок — найцікавіший. Зі сталою швидкістю мережа дійшла до 0.913 на девʼятій епосі, а на десятій розвалилась: середня втрата злетіла з 0.17 до 77.25. Це типова смерть наприкінці навчання — ваги вже великі, градієнти теж, і крок, стерпний на початку, стає завеликим. Будь-який спадний розклад цю смерть скасовує.
Зверни увагу й на те, чого не сталося: розігрів нічого не додав до косинуса, обидва дали 1.000. Розігрів потрібен не завжди, а тоді, коли початкова швидкість дуже велика або мережа глибока.
Чотири розклади швидкості
Угорі — як міняється швидкість по кроках навчання, унизу — крива втрат, яка з цього вийшла. Початкова швидкість у всіх однакова: 0.1.
БатчРозмір батча
Батч — скільки зображень мережа дивиться перед тим, як один раз змінити ваги. Число впливає одразу на три речі, і вони тягнуть у різні боки.
| Що | При більшому батчі |
|---|---|
| Памʼять | росте прямо пропорційно: активації всіх шарів множаться на розмір батча |
| Швидкість | епоха коштує дешевше: менше кроків, а кожен краще використовує процесор |
| Шум градієнта | падає: градієнт усереднений по більшій кількості прикладів, тому точніший — і тому крок стає обережнішим, ніж міг би бути |
Перші два пункти очевидні, третій — ні, і саме він найчастіше псує результат. Спершу час; замір на 1200 прикладах, дві епохи:
| батч | кроків за епоху | час епохи |
|---|---|---|
| 8 | 150 | 4.76 с |
| 32 | 38 | 2.97 с |
| 128 | 10 | 2.58 с |
| 512 | 3 | 2.17 с |
Від 8 до 512 епоха подешевшала у 2.2 раза, і це на процесорі; на відеокарті різниця буває десятикратною. Здавалось би, бери найбільший, що влазить у памʼять. Але ось що при цьому стається з якістю — 360 прикладів, 8 епох, швидкість навчання однакова, 0.03:
| батч | точність наприкінці | найкраща за навчання |
|---|---|---|
| 8 | 0.167 | 0.883 |
| 32 | 0.917 | 0.983 |
| 128 | 0.373 | 0.447 |
Провалилися обидва краї, і з різних причин. Батч 8 дає вчетверо більше змін ваг при тій самій швидкості — навчання розхитується: точність доходить до 0.883 на четвертій епосі, а далі падає до вгадування. Батч 128 навпаки: за ті самі вісім епох мережа зробила вчетверо менше кроків, ніж при батчі 32, і просто не дійшла.
0.0075 дав 1.000 замість 0.167. Правило
спрацювало.І одразу чесна межа: у зворотний бік воно не спрацювало — батч 128 при швидкості 0.12 дав 0.167, тобто ще гірше, ніж без виправлення. Правило емпіричне, добре працює у вузькому діапазоні й ламається на краях. Тут ми край і побачили.
Розмір батча: три речі одночасно
Три смуги: скільки коштує епоха, наскільки шумний градієнт і яка вийшла точність. Праворуч — перемикач, чи підправляти швидкість навчання під батч.
РегуляризаціяРегуляризація в зорі
Перенавчання й регуляризацію ти вже проходив у курсі машинного навчання (тема 37). Тут — лише те, що в зорі поводиться інакше. Три інструменти, у порядку корисності для зображень.
| Інструмент | Що робить | Місце в зорі |
|---|---|---|
| Аугментація | показує ту саму картинку в новому вигляді: повернуту, зсунуту, освітлену інакше | Головний засіб. Це єдиний спосіб додати мережі інформації, а не лише обмежити її. Розібрано в темі 10 |
| Weight decay | додає до втрати штраф за великі ваги — по суті L2-регуляризація | Ставлять майже завжди, значення 1e-4 … 1e-2. Дешевий і нешкідливий |
| Дропаут | випадково вимикає частину нейронів на кожному кроці навчання | Тільки в голові, і навіть там не завжди. Чому — нижче |
Чому дропаут у згортках працює гірше
Причина геометрична. Дропаут вимикає окремі числа. У повнозвʼязному шарі кожне число — окрема ознака, і вимкнути її означає справді позбавити мережу інформації: доведеться шукати запасну. У згортковій карті сусідні клітинки — це відгуки того самого ядра на майже той самий шматок зображення, тобто сильно корельовані.
Вимкнувши клітинку (5, 5), ти майже нічого не приховав: сусідки (5, 4) і (5, 6) несуть ту саму інформацію, і мережа просто читає сусіда. Регуляризації не вийшло, а шуму в градієнт додалося сповна.
DropBlock і SpatialDropout: вони вимикають не окрему клітинку, а
цілий квадрат або цілий канал — тобто щось, що справді не можна прочитати в сусіда. Але
на практиці найчастіше просто не ставлять дропаут у тілі взагалі, поклавшись на
аугментацію й батчнорм.Замір: чи допоміг дропаут на нашому датасеті
Щоб було що регуляризувати, потрібне справжнє перенавчання: беремо
60 прикладів на шість класів і піднімаємо шум до 0.45. Тепер мережа
справді запамʼятовує навчальну вибірку — 0.933 на ній проти 0.713 на перевірочній.
25 епох, Adam зі швидкістю 0.003, однакове зерно.
| дропаут | на навчальній | на перевірочній | розрив |
|---|---|---|---|
| немає | 0.933 | 0.713 | 0.220 |
| у згортках 0.25 | 0.683 | 0.567 | 0.117 |
| у голові 0.5 | 0.917 | 0.697 | 0.220 |
| у голові 0.3 | 0.933 | 0.717 | 0.217 |
| скрізь | 0.633 | 0.520 | 0.113 |
Скажу прямо: дропаут тут не допоміг ніяк. Найкращий результат — 0.717 з дропаутом 0.3 у голові — відрізняється від 0.713 без дропауту на чотири тисячні, тобто на одне зображення з трьохсот. Це не покращення, це шум.
А дропаут у згортках зашкодив: 0.567 замість 0.713. Зверни увагу, як саме він «зменшив розрив» — із 0.220 до 0.117. Формально розрив скоротився, фактично мережа стала гіршою в обидві сторони. Це важливий урок про сам показник «розрив»: його можна покращити, зіпсувавши модель. Weight decay теж нічого не дав: 0.001 — ті самі 0.713, а 0.01 — уже 0.663.
Розрив звузився, модель погіршала
Два рядки з таблиці вище на одній шкалі точності. Довжина смуги — це і є «розрив»: відстань від точності на перевірочній вибірці до точності на навчальній.
Правило, яке з цього випливає: спершу постав діагноз, потім став ліки. Дропаут і weight decay лікують перенавчання від надлишку параметрів; від браку даних вони не лікують і, як показує таблиця, можуть погіршити.
ДіагностикаКриві як діагностика
Це головний практичний розділ теми: усе попереднє було про те, що ввімкнути, а цей — про те, як зрозуміти, що саме зламалось. Інструмент один: дві криві на одному графіку — точність на навчальній вибірці й на перевірочній, по епохах, плюс крива втрати. Пʼять типових картинок, які з них складаються, покривають майже все, що трапляється насправді.
Пʼять діагнозів за кривими
Ліворуч — втрата на навчанні, праворуч — точність на навчальній і перевірочній вибірках. Усі пʼять картинок заміряні на нашому датасеті, а не намальовані.
Розберемо кожен стан і, головне, що з ним робити.
Норма
Втрата спадає монотонно майже до нуля, дві криві точності ростуть разом і лишаються поруч: 1.000 і 1.000. Головна ознака здорового навчання — не висока точність, а те, що обидві криві рухаються разом. Що робити: нічого; якщо точності мало — брати більшу мережу або більше даних.
Недонавчання
Втрата стоїть: 1.795 на першій епосі, 1.790 на десятій. Обидві криві точності низькі — 0.286 і 0.287 — і теж поруч, але внизу. Ознака: мережа погана навіть на тих даних, які бачила.
Що робити: збільшити швидкість (у нас саме це й було), навчати довше, взяти більшу мережу, перевірити нормалізацію входу. Регуляризацію тут, навпаки, треба прибрати: вона робить те саме, тільки навмисно.
Перенавчання
Втрата спадає нормально — 1.81 → 0.27. Але криві точності розходяться: навчальна йде до 0.933, перевірочна зупиняється на 0.713. Ознака — не сам розрив (невеликий розрив нормальний), а те, що він росте з епохами.
Що робити: перш ніж хапатись за дропаут, спитай, звідки перенавчання. Якщо від браку даних (у нас — 10 прикладів на клас) — потрібні дані: аугментація або перенос навчання. Якщо від надлишку параметрів — weight decay, дропаут у голові, менша мережа, рання зупинка. Розділ 8 показав, що переплутати ці два випадки означає зробити модель гіршою.
Завелика швидкість
Найхарактерніша картинка з усіх. Втрата на першій епосі 1.80, на другій
падає до 1.62 — і на третій підстрибує до 2.03. Далі
лягає на 1.81 до кінця, а точність — на 0.167. Ознака подвійна: початок був
непоганим, а потім усе провалилось у рівне плато на рівні
ln(кількість класів) — для шести класів це 1.792.
Що робити: зменшити швидкість у 3-10 разів; або додати батчнорм, спадний розклад
і розігрів. Ознака, що вже досить ділити: перші дві-три епохи перестали смикатись —
втрата спадає й більше не підстрибує вище за початкову. У нашому замірі між 0.3, яка
розвалилась, і 0.03, яка збіглася, рівно десять разів.
Витік даних
Найтихіший стан. Втрата спадає, обидві криві ростуть, розрив помірний — усе виглядає як звичайне перенавчання. Різниця в одному числі: перевірочна точність 0.767 замість 0.713. Це і є вся видима ціна витоку — 5.4 відсоткових пункти, при тому що пʼята частина перевірочної вибірки насправді навчальна. Він не ламає графік, він його трохи прикрашає — тому й живе в проєктах роками.
Що робити: рахувати перетин вибірок явно, а не дивитись на око. Ділити до будь-якої обробки. І памʼятати, що з реальними зображеннями буває гірше: два кадри з одного відео — формально різні файли, фактично той самий приклад.
| Що видно | Діагноз | Перше, що спробувати |
|---|---|---|
| Обидві криві високо й поруч | норма | нічого; більша мережа або більше даних, якщо треба вище |
| Обидві криві низько й поруч, втрата стоїть | недонавчання | більша швидкість, довше навчання, більша мережа |
| Криві розходяться, розрив росте | перенавчання | дані й аугментація; регуляризація — лише якщо мережа завелика |
Втрата смикнулась і лягла на ln K | завелика швидкість | поділити швидкість на 3-10; батчнорм; розклад |
| Усе підозріло добре | витік | порахувати перетин вибірок; перевірити, коли саме ділили |
ТишаПомилки без повідомлення
Окремий список — для помилок, які не дають traceback: кожна дає програмі відпрацювати до кінця й надрукувати число. Просто не те.
1. Забутий model.eval()
Розібрано в розділі 4. Ціна — до 13 відсоткових пунктів, і залежить від розміру батча:
на великому помилки не видно взагалі (там режим без eval() дає навіть на
одне очко більше), а в продакшені вона проявляється сповна. Як ловити: зроби функцію оцінювання, яка сама викликає
model.eval() першим рядком, і не пиши цикл перевірки вручну.
2. Перевірку нормалізували іншими числами
Середнє й відхилення порахували двічі — окремо для навчальної вибірки й окремо для
перевірочної. Виглядає логічно («кожен набір нормалізуємо сам по собі»), а насправді це
два різні перетворення: модель училась на одному розподілі, а перевіряється на іншому.
Як ловити: тримати μ і σ в одній змінній; два виклики
.mean() для одного й того самого — уже підозра.
3. Перемішали дані після поділу на вибірки
Точніше — поділили те, що вже перетинається. Ціна на нашому датасеті — 5.4 пункти
(розділ 9); на реальних даних більше: кадри з одного відео, кілька фотографій того самого
товару, аугментовані копії. Як ловити: порахувати перетин явно — два зрізи
[:600] і [300:] виглядають як поділ рівно доти, доки не
порахуєш, скільки в них спільного.
4. Аугментували перевірочну вибірку
Аугментація (тема 10) навмисне псує
зображення, щоб мережа побачила більше варіантів. Застосована до перевірки, вона одразу
занижує оцінку (модель міряють на спотвореннях, яких у житті не буде) і
робить її невідтворюваною. Як ловити: тримати два набори
перетворень окремо — train_transform з аугментацією й
eval_transform без неї.
ПовторВідтворюваність
Усі числа в цій темі супроводжувались словами «те саме зерно». Чому цього все одно мало.
Випадковості в навчанні щонайменше три: початкові ваги, порядок перемішування прикладів по батчах і випадковість самих шарів (дропаут, аугментація). Усі три фіксуються двома рядками:
torch.manual_seed(0) # ваги, дропаут, перемішування в PyTorch rng = np.random.default_rng(42) # усе, що генерує numpy — у нас це сам датасет
Із фіксованими зернами повторний запуск дає побітово ті самі числа — це перевірено, і саме тому числа в лекції збігаються з тими, які отримаєш ти.
Чому результат усе одно плаває
Бо зерно фіксує одну траєкторію з мільйона можливих, а не робить її типовою. Змінюєш зерно — змінюються початкові ваги — навчання йде іншим шляхом і приходить в іншу точку.
Замір робимо на важкій версії задачі — тій самій, що в розділі 8. На легкій міряти немає сенсу: там усі зерна впираються в стелю, і розкид виходить 0.003. Різницю між зернами видно рівно там, де задача складна.
| зерно | точність на перевірці |
|---|---|
| 0 | 0.713 |
| 1 | 0.727 |
| 2 | 0.783 |
| 3 | 0.723 |
| 4 | 0.783 |
| розкид | 0.070 |
Сім відсоткових пунктів — від однієї лише зміни початкових ваг. Тепер згадай таблицю дропауту з розділу 8: найкращий варіант дав 0.717 проти 0.713 без дропауту. Різниця — чотири тисячні при розкиді сімдесят тисячних.
Смуга зерна й виграш, який у неї вміщується
Угорі — пʼять запусків того самого коду, різниця лише в зерні. Унизу — найкращий варіант дропауту з розділу 08 проти варіанта без дропауту, на тій самій шкалі.
Ось практичний висновок, заради якого цей розділ узагалі є: різниця між двома моделями, менша за розкид від зерна, — це не різниця. Якщо архітектура A дала 0.760, а архітектура B — 0.735, а сам лише вибір зерна гуляє на 0.070, то ти нічого не довів.
Що роблять на практиці: запускають кожен варіант три-пʼять разів із різними зернами й порівнюють середні, а не окремі числа. Інакше половина твоїх «покращень» виявиться перевибором зерна.
a + b + c у різному порядку дає різний останній
знак.ПідсумокПідсумок
Коротко — що вмикати й за чим стежити, коли навчаєш згорткову мережу.
| Крок | Що зробити | Чому |
|---|---|---|
| Вхід | відняти середнє, поділити на відхилення; обидва числа — по навчальній вибірці, по каналах | щоб швидкість навчання означала те саме, що в чужому коді |
| Блок | Conv → BatchNorm → ReLU → Pool | у три з половиною рази менше епох до тієї самої точності й удвічі вища стеля швидкості |
| Швидкість | шукати множенням на десять; узяти втричі менше за ту, що ще працює | це єдине число, яке саме по собі перетворює 1.000 на 0.167 |
| Розклад | косинусний спад до нуля | рятує від смерті наприкінці навчання й нічого не коштує |
| Батч | найбільший, що влазить у памʼять; при зміні — міняти й швидкість | більший батч дешевший за епоху, але робить менше кроків |
| Регуляризація | аугментація завжди; weight decay майже завжди; дропаут — тільки в голові й тільки якщо допомагає | у згортках дропаут ламає більше, ніж лікує |
| Перевірка | окрема функція, яка сама кличе model.eval() | інакше помилка проявиться лише в продакшені |
| Кожен раз | дивитись на дві криві точності, а не на одне фінальне число | діагноз читається з форми кривих, а не з результату |
Головна думка теми, якщо лишати одну: навчання ламається тихо — жодна з розібраних поломок не викидає винятку. Тому єдиний робочий інструмент — дивитись на криві. Не на фінальну точність, не на втрату останнього батча, а на пару кривих по епохах.
Місток до наступної теми
Усе сказане мовчки передбачає одну річ: ти вчиш мережу з нуля, від випадкових ваг, на своїх даних. Саме тому нас так хвилювали ініціалізація, розігрів і стеля швидкості.
Але подивись іще раз на замір із розділу 8: шістдесят прикладів на шість класів — і жодна регуляризація не допомогла, бо лікувати треба було не мережу, а брак даних. А що, як хтось уже навчив схожу мережу на мільйоні зображень і готовий поділитися вагами? Тоді починати з випадкових чисел — марнотратство. Про це — наступна тема, і там на нас чекає розворот: перенос навчання виграє, поки цільових даних мало, і програє, коли їх стає досить.