Навчання CNN

Мережа зібрана, дані готові. Лишилось найважче: зрозуміти, що ввімкнути, за чим стежити — і як за двома кривими побачити, що саме пішло не так.

Мережу ми зібрали в темі 09, дані підготували в темі 10. Здавалось би, лишилось написати цикл і чекати. Але саме тут курс уперше стикається з тим, що той самий код на тих самих даних дає точність 1.000 або 0.167 залежно від дрібниць, які ніде не викликають помилки.

Ця тема — про ці дрібниці. Не про градієнтний спуск і не про те, як працює Adam, а про специфічне саме для згорткових мереж: у якому масштабі подавати пікселі, навіщо між згорткою й ReLU зʼявився ще один шар, чому одне й те саме число швидкості навчання буває і замалим, і завеликим — і як прочитати діагноз із двох кривих.

Що тут не повторюється. Градієнтний спуск, зворотне поширення, оптимізатори та регуляризацію нейромереж розібрано в курсі машинного навчання — тут ми на них спираємось.
Наскрізний приклад теми. Той самий, що в усьому блоці 2: фігури 28×28, шість класів (коло, квадрат, ромб, кільце, хрест, трикутник), намальовані формулами. Мережа теж та сама — три блоки Conv → ReLU → Pool і голова з двох повнозвʼязних шарів, 24 774 параметри. Усі числа нижче заміряні на ній.

ЗведенняДосі воно просто працювало

Скажімо чесно: у темах 06-09 навчання щоразу спрацьовувало з першого разу — і не тому, що ми все робили правильно, а тому, що задача крихітна: шість намальованих формулами фігур, майже без шуму, мережа на двадцять чотири тисячі параметрів. На справжніх даних так не буває. Ось приклад — та сама мережа, ті самі дані, змінене одне число:

швидкість навчанняточність на перевірціщо видно на кривій
0.00030.273втрата стоїть на 1.79 всі вісім епох
0.030.917втрата спадає до 0.04
1.00.167втрата злітає до 1032

0.167 — це рівно 1/6, рівень вгадування на шести класах: мережа не навчилась нічого. І — важливо — жоден із трьох прогонів не викинув помилки. Тому тема відповідає на два питання: що ввімкнути, щоб такі провали стали рідкістю, і як їх розпізнати, коли вони все одно трапляться.

ВхідНормалізація входу

Зображення з файлу — це цілі числа від 0 до 255. Ваги свіжої мережі — це числа порядку сотих. Помнож одне на друге, і побачиш, у чому проблема.

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

вхідсереднєвідхиленнявідгук згортки: розмах
0 … 25554.9793.44−364.5 … 282.2
0 … 10.2160.366−1.14 … 1.15
нормалізований0.0001.000−2.81 … 2.34

Ваги в усіх трьох рядках ті самі — мережа одна, зерно одне: стандартне відхилення 0.190, межі ±0.33. А розмах відгуку на сирому вході більший, ніж на нормалізованому, у 104 рази (66.6 проти 0.64 за стандартним відхиленням). Далі відгук іде в градієнт, градієнт множиться на швидкість навчання — і виходить, що масштаб даних тихо помножив твою швидкість навчання на невідоме число.

Що саме ламається

Найпоширеніше пояснення — «без нормалізації мережа не навчиться» — неправдиве, і легко спростовується заміром: три масштаби входу × чотири швидкості навчання, по шість епох на 360 прикладах, однакове зерно.

Масштаб входу зсуває вікно робочих швидкостей

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

масштаб входу
середнє входу—
розмах—
точність на перевірці—
Заміряно: SGD із моментом 0.9, 6 епох, 360 прикладів, шість класів. Рівень вгадування — 0.167. Сирий вхід 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.

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

Чому саме зображення. У табличних даних колонки мають різний масштаб, і ти нормалізуєш їх окремо — це вже було в курсі ML. У зображень масштаб однаковий у всіх пікселів, тому й здається, що нормалізувати нічого. Проблема тут не в різниці між пікселями, а в спільному зсуві всіх 784 чисел: вони всі невідʼємні й усі великі, тому градієнти всіх ваг одного нейрона мають однаковий знак і крокують разом.

Два числа й одне правило

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

x′ = ( x − μканал ) / σканал

Тут 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).

σ2 = 2 / nвх,   де nвх = Cвх × k × k

Для згортки nвх — не кількість вхідних каналів, а кількість чисел, які ядро перемножує на одну вихідну клітинку: канали × площа ядра. Для другої згортки нашої мережі це 8 × 3 × 3 = 72, тобто σ ≈ 0.167.

Хороша новина. nn.Conv2d у PyTorch уже ініціалізується за Хе (варіант kaiming_uniform) — робити це окремо не треба. Знати треба: зустрівши чужий код із nn.init.normal_(w, std=0.01) для глибокої мережі з ReLU, ти зрозумієш, чому вона не вчиться.

БатчнормБатчнорм

Нормалізація входу привела до ладу числа на вході. Але після першої ж згортки вони знову розповзаються: розподіл усередині мережі залежить від ваг, а ваги міняються на кожному кроці. Батч-нормалізація (batch normalization, батчнорм) — шар, який робить із проміжними числами те саме, що ми зробили з входом, і робить це на кожному кроці. Формула та сама, з двома додатковими вагами:

y = γ · ( x − μбатч ) / σбатч + β

Спершу число приводять до нульового середнього й одиничного відхилення — це μбатч і σбатч по поточному батчу. Потім результат множать на γ і додають β — дві вивчувані ваги на канал. Вони дають мережі змогу при потребі скасувати нормалізацію, якщо шару вигідніший інший масштаб.

Чому в CNN нормалізують по каналу, а не по елементу

Це головна відмінність батчнорму в згортковій мережі. Візьмемо карту ознак після нашої першої згортки: 8 каналів по 28×28, батч із 32 зображень — разом 200 704 числа. Скільки пар (μ, σ) рахувати?

Варіант, який здається природним

Своя пара чисел на кожну клітинку карти: для позиції (0, 0), для (0, 1) і так далі — вийде 8 × 28 × 28 = 6 272 пари. І це неправильно. Канал згортки — це одне ядро, застосоване в кожній точці, тому відгук у (0, 0) і відгук у (14, 14) — те саме вимірювання тієї самої прикмети, просто в іншому місці.

Як роблять насправді

Пара чисел на канал: усереднюємо по батчу, по висоті й по ширині разом. Вийде 8 пар — по одній на вивчену прикмету.

mean(dim=(0, 2, 3)) → 8 чисел замість 6 272

Кожне середнє порахуване по 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точність наприкінці
без батчнорму70.983
з батчнормом21.000

Це і є перший результат: з батчнормом мережа дійшла до 0.90 за дві епохи замість семи. Не «трохи швидше» — у три з половиною рази менше роботи до того самого результату.

Головне практичне: наскільки більший крок можна взяти

Але не заради прискорення батчнорм ставлять у кожну сучасну мережу, а заради ось цього: він піднімає стелю швидкості навчання. Ту саму пару мереж проганяємо по пʼяти значеннях.

швидкістьбез батчнормуз батчнормом
0.050.9600.980
0.100.9130.993
0.150.1670.997
0.200.1670.937
0.300.1670.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 не вчиться, а стрибає. З батчнормом на тій самій швидкості після четвертої епохи крива виходить нагору й тримається біля одиниці.

А чому він узагалі допомагає — питання відкрите. Оригінальна стаття 2015 року пояснювала ефект «внутрішнім зсувом розподілів» (internal covariate shift), і це пояснення розійшлося по всіх підручниках. У 2018-му його перевірили експериментом і не підтвердили: зсув розподілів можна навмисно відновити, і батчнорм усе одно допомагає. Чинне пояснення інше — батчнорм робить поверхню втрат гладшою, тому більший крок перестає бути небезпечним. Саме це ми й поміряли таблицею вище: не «швидше», а «вища стеля».
Чесно про стелю. Стеля піднялась, але не зникла: при 0.30 батчнорм не врятував — 0.167, той самий рівень вгадування, що й без нього. Твердження «з батчнормом можна брати будь-яку швидкість» неправдиве, і спростовується одним прогоном. Правильне формулювання — «можна брати приблизно вдвічі більшу, і це варто перевірити на своїх даних».

Батчнорм і стеля швидкості навчання

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

без батчнорму—
з батчнормом—
епох до 0.90 без—
епох до 0.90 з—
Заміряно: 8 епох, 360 прикладів, SGD із моментом 0.9, нормалізований вхід, зерно 0. Пунктир — рівень вгадування 0.167. При 0.02 виграш батчнорму — у швидкості (2 епохи проти 7). При 0.15 — уже не у швидкості, а в самому факті: без нього мережа не вчиться зовсім.

Пастка: у режимі перевірки батчнорм працює інакше

Коли мережа рахує відповідь для нового зображення, батча немає — є одна картинка. По чому тоді рахувати μбатч і σбатч? Ні по чому. Тому під час навчання батчнорм веде окремий бухгалтерський запис — біжучі середнє й дисперсію по всіх батчах, які бачив, — а в режимі перевірки бере їх замість статистик поточного батча. Це принципова різниця: один шар рахує дві різні формули залежно від режиму.

РежимЩо бере батчнормНаслідок
навчання (model.train())середнє й відхилення поточного батчавідповідь на одне зображення залежить від того, які зображення лежать поруч у батчі
перевірка (model.eval())накопичені біжучі середнє й відхиленнявідповідь залежить лише від самого зображення

А тепер найважливіше: забути model.eval() — не помилка часу виконання. Мережа не впаде, не попередить і чесно надрукує точність. Просто не ту.

Забутий model.eval()

Одна навчена мережа з батчнормом, один і той самий перевірочний набір із 300 зображень. Міняється лише розмір батча, яким його прогнали — і те, чи згадали про режим.

батч на перевірці
з eval()—
без eval()—
ціна помилки—
Заміряно на мережі з батчнормом, навченій 8 епох при швидкості 0.05. Та сама мережа без батчнорму й без дропауту дає 0.960 в обох режимах — рівно нуль різниці. Помилка існує тільки там, де є шар, який поводиться в режимах по-різному.

Спершу зверни увагу на бірюзові стовпчики: з 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.00031.791.790.273повзе
0.0031.791.760.320повзе
0.031.790.140.917збігається
0.31.801.810.167розходиться
1.01.861.850.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 розчавив би решту кривих у пряму лінію.

втрата на кінець—
точність—
режим—
Заміряно: 8 епох, 360 прикладів, SGD із моментом 0.9, нормалізований вхід. Сіра пунктирна лінія — рівень ln 6 = 1.792: стільки коштує відповідь «не знаю, всі класи однаково ймовірні». Крива, що лежить на ній, означає мертву мережу незалежно від причини.
Практичне правило пошуку. Шукай швидкість не додаванням, а множенням: 0.0001, 0.001, 0.01, 0.1. Знайшовши те значення, при якому мережа ще вчиться, а наступне вже ламається, візьми утричі менше за нього — і матимеш запас на те, що на повних даних поведінка буде трохи іншою.

РозкладРозклад швидкості

Одне число на все навчання — не обовʼязково: швидкість можна змінювати по ходу. На початку ваги випадкові, і треба великими кроками дійти до потрібної області; наприкінці мережа вже майже там, і великий крок лише перестрибує мінімум. Спершу швидко, потім повільно.

РозкладЯк міняється швидкістьКоли брати
Сталаніякколи треба просто перевірити, що мережа вчиться
Сходинковий (step)ділиться на 10 у заздалегідь названі моментикласика; вимагає вгадати, коли саме ділити
Косинусний (cosine)плавно спадає до нуля за чвертю косинусоїдизамовчування для більшості сучасних робіт: нічого не треба вгадувати, крім довжини навчання
З розігрівом (warmup)перші кілька відсотків кроків швидкість росте від нуляколи початкова швидкість велика — розігрів не дає першим крокам зруйнувати ваги

Замір: 10 епох, початкова швидкість 0.1 у всіх чотирьох випадках. Число обране навмисно на межі — саме там розклад і має значення.

розкладточність наприкінцінайкраща за навчання
стала0.1670.913
сходинки0.9930.993
косинус1.0001.000
розігрів + косинус1.0001.000

Перший рядок — найцікавіший. Зі сталою швидкістю мережа дійшла до 0.913 на девʼятій епосі, а на десятій розвалилась: середня втрата злетіла з 0.17 до 77.25. Це типова смерть наприкінці навчання — ваги вже великі, градієнти теж, і крок, стерпний на початку, стає завеликим. Будь-який спадний розклад цю смерть скасовує.

Зверни увагу й на те, чого не сталося: розігрів нічого не додав до косинуса, обидва дали 1.000. Розігрів потрібен не завжди, а тоді, коли початкова швидкість дуже велика або мережа глибока.

Чотири розклади швидкості

Угорі — як міняється швидкість по кроках навчання, унизу — крива втрат, яка з цього вийшла. Початкова швидкість у всіх однакова: 0.1.

розклад
швидкість на кінець—
втрата на кінець—
точність наприкінці—
Заміряно: 10 епох, 360 прикладів, 12 кроків на епоху, SGD із моментом 0.9, початкова швидкість 0.1. Верхній графік малює точну формулу розкладу по кроках; нижній — заміряну середню втрату за епоху.

БатчРозмір батча

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

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

Перші два пункти очевидні, третій — ні, і саме він найчастіше псує результат. Спершу час; замір на 1200 прикладах, дві епохи:

батчкроків за епохучас епохи
81504.76 с
32382.97 с
128102.58 с
51232.17 с

Від 8 до 512 епоха подешевшала у 2.2 раза, і це на процесорі; на відеокарті різниця буває десятикратною. Здавалось би, бери найбільший, що влазить у памʼять. Але ось що при цьому стається з якістю — 360 прикладів, 8 епох, швидкість навчання однакова, 0.03:

батчточність наприкінцінайкраща за навчання
80.1670.883
320.9170.983
1280.3730.447

Провалилися обидва краї, і з різних причин. Батч 8 дає вчетверо більше змін ваг при тій самій швидкості — навчання розхитується: точність доходить до 0.883 на четвертій епосі, а далі падає до вгадування. Батч 128 навпаки: за ті самі вісім епох мережа зробила вчетверо менше кроків, ніж при батчі 32, і просто не дійшла.

Правило лінійного масштабування. Змінюєш батч у k разів — зміни й швидкість навчання в k разів. Перевіримо на нашому провалі: батч 8 при швидкості 0.03/4 = 0.0075 дав 1.000 замість 0.167. Правило спрацювало.

І одразу чесна межа: у зворотний бік воно не спрацювало — батч 128 при швидкості 0.12 дав 0.167, тобто ще гірше, ніж без виправлення. Правило емпіричне, добре працює у вузькому діапазоні й ламається на краях. Тут ми край і побачили.

Розмір батча: три речі одночасно

Три смуги: скільки коштує епоха, наскільки шумний градієнт і яка вийшла точність. Праворуч — перемикач, чи підправляти швидкість навчання під батч.

швидкість навчання
кроків за епоху · на 1200—
час епохи—
точність—
Час епохи заміряно на 1200 прикладах, точність — на 360 за 8 епох. Батч 512 у колонці точності позначено прочерком: на 360 прикладах він дав би один крок за епоху, і порівняння втратило б сенс. Шум градієнта показано як 1/√B — саме так спадає похибка середнього при зростанні вибірки.

РегуляризаціяРегуляризація в зорі

Перенавчання й регуляризацію ти вже проходив у курсі машинного навчання (тема 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.9330.7130.220
у згортках 0.250.6830.5670.117
у голові 0.50.9170.6970.220
у голові 0.30.9330.7170.217
скрізь0.6330.5200.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.

Розрив звузився, модель погіршала

Два рядки з таблиці вище на одній шкалі точності. Довжина смуги — це і є «розрив»: відстань від точності на перевірочній вибірці до точності на навчальній.

Коротша смуга — гірша модель. Дропаут у згортках скоротив розрив з 0.220 до 0.117, і за самим лише цим показником виглядає як ліки. Але вся смуга поїхала ліворуч: на перевірочній вибірці 0.567 замість 0.713. Розрив вимірює відстань між двома числами, а не якість жодного з них, — і зменшити його можна, просто зіпсувавши навчання.
Чому так. Перенавчання тут викликане не надлишком параметрів (24 774 на шість простих класів — це небагато), а браком даних: по десять картинок на клас. Такий брак лікується не регуляризацією, а даними — тому головний засіб у зорі це аугментація (тема 10), а на малій вибірці ще й перенос навчання (тема 12).

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

ДіагностикаКриві як діагностика

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

Пʼять діагнозів за кривими

Ліворуч — втрата на навчанні, праворуч — точність на навчальній і перевірочній вибірках. Усі пʼять картинок заміряні на нашому датасеті, а не намальовані.

що показати
навчальна—
перевірочна—
розрив—
втрата на кінець—
Усі пʼять станів заміряні на фігурах 28×28. «Норма» — повне навчання на 1200 прикладах із батчнормом і косинусним розкладом. «Недонавчання» — швидкість 0.0003. «Перенавчання» — 60 прикладів із шумом 0.45. «Завелика швидкість» — 0.3. «Витік» — те саме перенавчання, але 60 навчальних прикладів потрапили в перевірочну вибірку з 300.

Розберемо кожен стан і, головне, що з ним робити.

Норма

Втрата спадає монотонно майже до нуля, дві криві точності ростуть разом і лишаються поруч: 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 без неї.

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

ПовторВідтворюваність

Усі числа в цій темі супроводжувались словами «те саме зерно». Чому цього все одно мало.

Випадковості в навчанні щонайменше три: початкові ваги, порядок перемішування прикладів по батчах і випадковість самих шарів (дропаут, аугментація). Усі три фіксуються двома рядками:

torch.manual_seed(0)                 # ваги, дропаут, перемішування в PyTorch
rng = np.random.default_rng(42)      # усе, що генерує numpy — у нас це сам датасет

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

Чому результат усе одно плаває

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

Замір робимо на важкій версії задачі — тій самій, що в розділі 8. На легкій міряти немає сенсу: там усі зерна впираються в стелю, і розкид виходить 0.003. Різницю між зернами видно рівно там, де задача складна.

зерноточність на перевірці
00.713
10.727
20.783
30.723
40.783
розкид0.070

Сім відсоткових пунктів — від однієї лише зміни початкових ваг. Тепер згадай таблицю дропауту з розділу 8: найкращий варіант дав 0.717 проти 0.713 без дропауту. Різниця — чотири тисячні при розкиді сімдесят тисячних.

Смуга зерна й виграш, який у неї вміщується

Угорі — пʼять запусків того самого коду, різниця лише в зерні. Унизу — найкращий варіант дропауту з розділу 08 проти варіанта без дропауту, на тій самій шкалі.

Виграш, який не видно на тлі зерна, — не виграш. Смуга зерна завширшки 0.070, найкращий дропаут додає 0.004 — у 17.5 раза менше й цілком усередині смуги. Тому 0.717 не «краще» за 0.713: обидва числа — дві точки одного розкиду, і щоб їх розрізнити, потрібні кілька зерен на кожен варіант.

Ось практичний висновок, заради якого цей розділ узагалі є: різниця між двома моделями, менша за розкид від зерна, — це не різниця. Якщо архітектура A дала 0.760, а архітектура B — 0.735, а сам лише вибір зерна гуляє на 0.070, то ти нічого не довів.

Що роблять на практиці: запускають кожен варіант три-пʼять разів із різними зернами й порівнюють середні, а не окремі числа. Інакше половина твоїх «покращень» виявиться перевибором зерна.

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

ПідсумокПідсумок

Коротко — що вмикати й за чим стежити, коли навчаєш згорткову мережу.

КрокЩо зробитиЧому
Вхідвідняти середнє, поділити на відхилення; обидва числа — по навчальній вибірці, по каналахщоб швидкість навчання означала те саме, що в чужому коді
БлокConv → BatchNorm → ReLU → Poolу три з половиною рази менше епох до тієї самої точності й удвічі вища стеля швидкості
Швидкістьшукати множенням на десять; узяти втричі менше за ту, що ще працюєце єдине число, яке саме по собі перетворює 1.000 на 0.167
Розкладкосинусний спад до нулярятує від смерті наприкінці навчання й нічого не коштує
Батчнайбільший, що влазить у памʼять; при зміні — міняти й швидкістьбільший батч дешевший за епоху, але робить менше кроків
Регуляризаціяаугментація завжди; weight decay майже завжди; дропаут — тільки в голові й тільки якщо допомагаєу згортках дропаут ламає більше, ніж лікує
Перевіркаокрема функція, яка сама кличе model.eval()інакше помилка проявиться лише в продакшені
Кожен раздивитись на дві криві точності, а не на одне фінальне числодіагноз читається з форми кривих, а не з результату

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

Місток до наступної теми

Усе сказане мовчки передбачає одну річ: ти вчиш мережу з нуля, від випадкових ваг, на своїх даних. Саме тому нас так хвилювали ініціалізація, розігрів і стеля швидкості.

Але подивись іще раз на замір із розділу 8: шістдесят прикладів на шість класів — і жодна регуляризація не допомогла, бо лікувати треба було не мережу, а брак даних. А що, як хтось уже навчив схожу мережу на мільйоні зображень і готовий поділитися вагами? Тоді починати з випадкових чисел — марнотратство. Про це — наступна тема, і там на нас чекає розворот: перенос навчання виграє, поки цільових даних мало, і програє, коли їх стає досить.