ResNet і скіп-зʼєднання

Глибина працювала, працювала — і раптом перестала. Мережа з двадцяти пʼяти шарів вивчила навчальні дані гірше, ніж мережа з пʼяти. Полагодив це один знак «плюс».

У попередній темі ми порахували AlexNet і VGG по шарах і побачили дві речі. Перша відома всім: глибина працює, і VGG16 із тринадцятьма згортками краща за AlexNet із пʼятьма. Друга відома менше: 123 642 856 із 138 мільйонів ваг VGG16 сидять у трьох повнозвʼязних шарах голови, тобто мережа, яку памʼятають як глибоку згорткову, на 89.4 % складається зі звичайних повнозвʼязних ваг.

Обидві ці речі виправила одна архітектура, і виправила одним прийомом. Її звати ResNet — від residual network, «залишкова мережа». У цій темі ми розберемо, що саме зламалось, коли мережі спробували зробити ще глибшими, і чому рядок out = out + x це полагодив.

Що ми зміряємо самі. Усі числа цієї теми отримані на нашому власному досліді, а не переказані зі статті. Ми навчимо десять мереж на тих самих фігурах 28×28, що й у блоці 2, зміряємо норму градієнта по шарах, перевіримо, чи справді залишковий блок на старті нічого не робить, і розберемо справжню resnet18 через torchvision. Кожне число тут — з нашого власного заміру, жодне не взяте на віру.

ОбіцянкаОбіцянка глибини

До 2015 року історія згорткових мереж читалася як одна пряма лінія. AlexNet, 2012 — пʼять згорткових шарів. VGG16, 2014 — тринадцять. Точність росла разом із глибиною, і пояснення було зрозумілим: кожен шар додає рівень абстракції. Перші шари бачать межі, середні — кутки й текстури, глибші — частини предметів. Більше шарів — довший ланцюжок абстракцій.

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

Мережа з більшою кількістю шарів давала гіршу точність. Причому гіршою вона була не тільки на перевірочних даних, яких не бачила, а й на самих навчальних, які бачила тисячі разів. Це вже не схоже на жодну з відомих проблем, і саме через це явище дістало власне імʼя — деградація (degradation).

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

ВимірЩо показує вимір

Задача — та сама, що в блоці 2: шість класів фігур 28×28 (коло, квадрат, ромб, кільце, хрест, трикутник), згенерованих формулами, зі зсувом центра до чотирьох пікселів і гаусовим шумом. Шістсот прикладів на навчання, триста на перевірку. Шість класів означає, що вгадування навмання дає 0.167 — це наша підлога, нижче не буває.

Мережу будуємо з однакових блоків. Кожен блок — це дві згортки 3×3 на шістнадцяти каналах, після кожної батчнорм (тема 11), після першої ReLU, після другої теж ReLU. Перед блоками стоїть один згортковий шар, який зменшує картинку з 28×28 до 7×7; такий вхідний шар прийнято звати стемом (stem) — він приймає сирий кадр і віддає блокам уже стиснуту карту ознак. Після блоків — глобальне усереднення й один лінійний шар на шість класів. Глибину міряємо в згорткових шарах: один у стемі плюс дві на блок. Один блок — три шари, дванадцять блоків — двадцять пʼять.

Тепер найважливіше в цьому досліді. Ми порівнюємо дві мережі, які відрізняються рівно одним знаком «плюс»:

def forward(self, x):
    out = self.relu(self.bn1(self.conv1(x)))
    out = self.bn2(self.conv2(out))
    out = out + x          # цього рядка немає в простій мережі
    return self.relu(out)

Однакова кількість згорток, однакова кількість батчнормів, однакова кількість ReLU, однакова кількість ваг до останнього числа. Ми зміряли: на глибині 25 обидві мережі мають рівно 56 342 параметри. Різниця лише в тому, що залишкова мережа додає до виходу блоку його ж вхід. Це важливо: якби мережі відрізнялись ще чимось, висновок нічого не вартував би.

Навчання: SGD із моментом 0.9, швидкість навчання 0.005, двадцять епох, батч 64. Кожен рядок — середнє з трьох прогонів із різними зернами випадковості, бо на такій маленькій задачі один прогін занадто шумний, щоб на нього спиратись.

глибинапараметрів проста, навчпроста, тест залишкова, навчзалишкова, тест
3 шари4 9500.8070.7590.8760.821
5 шарів9 6220.9260.8680.9340.869
9 шарів18 9660.8720.7981.0000.943
17 шарів37 6540.7980.7171.0000.960
25 шарів56 3420.5670.5301.0000.954

Прочитай ліву половину таблиці згори вниз. Проста мережа виходить на найкращу навчальну точність на пʼяти шарах — 0.926. Далі кожні додані шари роблять гірше: 0.872 на девʼяти, 0.798 на сімнадцяти, 0.567 на двадцяти пʼяти. Мережа з двадцяти пʼяти шарів і 56 тисяч ваг вивчила навчальні дані гірше, ніж мережа з пʼяти шарів і 9.6 тисячі ваг — маючи вшестеро більше ваг і ту саму задачу.

Тепер праву половину. Залишкова мережа з тих самих блоків росте монотонно й з девʼяти шарів упирається в стелю: 1.000 на навчальних даних на глибинах 9, 17 і 25. Перевірочна точність — 0.943, 0.960, 0.954. Мережа не ламається.

Деградація глибини

Веди повзунок по глибині. Суцільні лінії — навчальна точність, пунктирні — перевірочна. Рожеві — проста мережа, бірюзові — залишкова.

проста, навч—
проста, тест—
залишк., навч—
залишк., тест—
параметрів—
розрив навч—
Вісь ліворуч — частка правильних відповідей, від 0.167 (вгадування на шести класах) до 1.000. Вісь унизу — глибина в згорткових шарах, нерівномірна: 3, 5, 9, 17, 25. Кожна точка — середнє з трьох прогонів. Обидві мережі на кожній глибині мають однакову кількість ваг, згорток, батчнормів і ReLU.

Не перенавчанняЦе не перенавчання

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

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

Подивись на рядок «25 шарів» у нашій таблиці. Навчальна точність простої мережі — 0.567. Перевірочна — 0.530. Розрив між ними 0.037, тобто його практично немає. Мережа не завчила навчальні дані — вона їх не вивчила. На тих самих шістсот прикладах, які бачила двадцять разів поспіль, вона помиляється майже в половині випадків.

Порівняй із мережею на пʼять шарів: 0.926 навчальної проти 0.868 перевірочної. Ось там розрив є, і це нормальний невеликий надлишок памʼяті над узагальненням.

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

Друга думка, яка приходить наступною, — зникомий градієнт (vanishing gradient). Це відома проблема глибоких мереж: коли сигнал помилки йде назад крізь багато шарів, він множиться на похідні й може згасати до нуля, так що перші шари нічого не вчаться. Але в нашій мережі після кожної згортки стоїть батчнорм, а він саме для того й придуманий, щоб тримати масштаб сигналу в межах. Ми зміряємо градієнт у розділі 6 і побачимо: з ним справді щось не так, але зовсім не те, чого чекаєш.

ПарадоксПарадокс тотожності

Тепер найважливіший аргумент теми, і він не потребує ні коду, ні математики — лише уважності.

Візьмімо нашу мережу на пʼять шарів, яка дала 0.926 на навчальних даних. Тепер збудуймо мережу на двадцять пʼять шарів так: перші пʼять шарів такі самі, а решта двадцять — додаткові.

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

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

Тотожне перетворення (identity mapping) — найпростіша функція, яка існує: f(x) = x. Що подали на вхід, те й вийшло. Для шару мережі це означає «нічого не змінюй, просто пропусти далі».

Із цього випливає висновок, який виглядає залізобетонним:

точність(25 шарів) ≥ точність(5 шарів)

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

А тепер наш вимір: 0.567 проти 0.926. Розрив — 0.359, і він не на користь глибшої.

Парадокс тотожності

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

дописано шарів—
теорія, не менше—
вимір, навч—
борг—
Сірі коробки — базова мережа на пʼять шарів (навчальна точність 0.926). Рожеві — дописані шари: якщо кожен вивчить тотожність, мережа поводитиметься як базова, тож нижче 0.926 впасти не мала б. «Борг» — на скільки фактичний вимір нижчий за цю межу.

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

Не знайде. Спробуй уявити, що потрібно, щоб згортка 3×3 із батчнормом вивчила тотожність. Ядро має стати одиничним імпульсом: одиниця рівно в центрі свого каналу й нулі у всіх інших 143 позиціях (16 каналів × 9 позицій, мінус одна). Батчнорм має підібрати γ і β так, щоб не змінити масштаб. Це не «просто нічого не робити» — це дуже конкретна, дуже вузька точка у просторі ваг, і градієнтний спуск не має жодної причини цілитись саме в неї.

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

ІдеяТотожність задарма

Коли задача звучить як «потрібне перетворення існує, але його важко знайти», є два шляхи: шукати краще або зробити так, щоб шукати не довелось. ResNet обрала друге.

Ідея в одному рядку. Нехай F(x) — те, що рахує блок із двох згорток. Замість того щоб брати вихід блоку як є, додамо до нього вхід:

y = F(x) + x

Розберемо кожен символ. x — те, що прийшло в блок: карта ознак, наприклад 16 каналів по 7×7. F(x) — те, що порахували дві згортки з батчнормами всередині блоку; форма така сама, 16 на 7×7. y — вихід блоку. Знак «плюс» — звичайне поелементне додавання двох тензорів однакової форми: перше число до першого, друге до другого.

Тепер подивись, що змінилося. Раніше блок мав вивчити яким має бути вихід. Тепер він вчить що додати до входу. Різницю між бажаним виходом і входом називають залишком (residual) — звідси й назва архітектури.

крок 1 — чого ми хочемо від зайвого шару

Щоб глибша мережа не була гіршою за коротшу, зайвий блок має вміти нічого не робити. Позначмо бажаний вихід блоку H(x). «Нічого не робити» означає H(x) = x.

крок 2 — як це виглядає у звичайному блоці
звичайний блок: y = F(x) щоб y = x, треба: F(x) = x

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

крок 3 — як це виглядає в залишковому блоці
залишковий блок: y = F(x) + x щоб y = x, треба: F(x) = 0

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

крок 4 — що з цього виходить

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

Порахуймо це руками на чотирьох числах, щоб не лишалось абстракцією. Хай на вхід блоку прийшов маленький шматок карти ознак: x = [2, 0, 1, 3]. Хай згортки всередині видали F(x) = [0.1, −0.2, 0, 0.05] — маленькі числа, бо ваги ще майже не навчились.

Це і є зміст скіп-зʼєднання: сигнал за замовчуванням проходить, а шар лише вносить поправку.

ГрадієнтЩо це робить із градієнтом

Досі ми говорили про прямий прохід — про те, як дані йдуть від входу до виходу. Але навчання відбувається у зворотному, і саме там скіп-зʼєднання видно найкраще.

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

Тепер формально, і це єдина похідна в темі. Похідна — це відповідь на питання «наскільки зміниться вихід, якщо трохи змінити вхід». Позначається значком ∂ (кругле «д»), і запис ∂y/∂x читається «як швидко y змінюється зі зміною x». Похідна суми дорівнює сумі похідних — це шкільне правило, і воно тут вирішує все:

y = F(x) + x   ⟹   ∂y/∂x = ∂F/∂x + 1

Одиниця в кінці — і є скіп-зʼєднання. Що б не сталося з градієнтом усередині блоку, до неї додається чиста одиниця: частина сигналу проходить крізь блок незміненою. У звичайному блоці цієї одиниці немає, там просто ∂y/∂x = ∂F/∂x, і якщо цей множник систематично більший або менший за одиницю, за двадцять блоків він підноситься до двадцятого степеня.

Зміряймо. Беремо мережу з дванадцяти блоків (25 шарів), подаємо один батч із 64 зображень, рахуємо помилку, робимо зворотний прохід — і дивимось на норму градієнта першої згортки в кожному блоці. Норма — це просто довжина вектора: корінь із суми квадратів усіх чисел градієнта. Одне число, що каже «наскільки сильно цей шар зараз штовхають».

блокпроста мережазалишкова
1 (біля входу)9.0771.480
42.5840.609
80.6880.435
12 (біля виходу)0.17970.350
перший / останній50.5×4.2×
Несподіванка, яку варто назвати прямо. Підручники обіцяють згасання градієнта до входу. У нас вийшло навпаки: у простій мережі градієнт до входу розростається, і в пʼятдесят разів. Причина — батчнорм. Він ділить сигнал на його ж стандартне відхилення, і на зворотному проході це ділення перетворюється на множення. Двадцять чотири таких множення поспіль дають накопичений розгін. Тобто батчнорм урятував нас від згасання й натомість подарував протилежну біду. Для нашого висновку це нічого не міняє: в обох випадках сигнал по шарах нерівний, а один крок оптимізатора має бути доречним одразу для всіх шарів. У залишковій мережі розкид 4.2 раза замість 50.5 — сигнал по шарах майже однаковий.

Норма градієнта по шарах

Перемкни архітектуру. Стовпчики — норма градієнта в кожному з дванадцяти блоків, зліва вхід, справа вихід. Шкала логарифмічна.

архітектура
блок 1—
блок 12—
розкид—
тест, 25 шарів—
Заміряно на старті навчання, до першого кроку оптимізатора, на батчі з 64 зображень. Висота стовпчика — норма градієнта ваг першої згортки блоку в логарифмічній шкалі: кожна горизонтальна лінія — десятикратна зміна. Третій режим (нульова ініціалізація останнього батчнорму) розібраний у розділі 8.

БлокБлок ResNet по деталях

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

class Block(nn.Module):
    def __init__(self, channels):
        self.conv1 = nn.Conv2d(channels, channels, 3, padding=1, bias=False)
        self.bn1   = nn.BatchNorm2d(channels)
        self.conv2 = nn.Conv2d(channels, channels, 3, padding=1, bias=False)
        self.bn2   = nn.BatchNorm2d(channels)

    def forward(self, x):
        out = relu(self.bn1(self.conv1(x)))   # перша половина: згортка, BN, ReLU
        out = self.bn2(self.conv2(out))       # друга: згортка і BN, ReLU поки нема
        out = out + x                         # скіп-зʼєднання
        return relu(out)                      # і аж тепер ReLU

Три деталі, які варто прокоментувати окремо.

Згортки без зсуву (bias=False). Батчнорм одразу після згортки віднімає середнє по батчу, тож будь-який доданий зсув він тут-таки й прибирає. Тримати параметр, дію якого наступний шар скасовує, — марна витрата. Для нашого блоку на 16 каналів це економія 32 чисел із 4 672, дрібниця; для ResNet-50 — десятки тисяч.

ReLU стоїть після додавання, а не до нього. Це та деталь, повз яку легко пройти, а вона визначає, що блок узагалі здатен зробити. ReLU обнуляє все відʼємне. Якщо поставити її перед додаванням, гілка F(x) віддаватиме лише невідʼємні числа, і блок зможе тільки додавати — ніколи не віднімати. Сигнал тоді росте від блоку до блоку й ніколи не спадає. Ми зміряли середнє значення активацій на старті: у канонічному варіанті воно росте від 0.91 у першому блоці до 2.36 у дванадцятому, а якщо перенести ReLU перед додаванням (в інтерактиві 4 це режим «скіп після ReLU») — від 1.12 до 5.42. За вдвічі швидшого розгону на дванадцяти блоках на пʼятдесяти буде вже погано.

Чесно про наш вимір. На нашій задачі варіант «ReLU перед додаванням» дав перевірочну точність 0.960 проти 0.954 в канонічного — тобто різниці в точності не видно. Дванадцяти блоків замало, щоб розгін активацій устиг зашкодити. Ми не робитимемо вигляд, що вимір показав більше, ніж показав: доказом тут є зростання масштабу активацій (2.36 проти 5.42), а не точність. Саме тому канонічний блок ставить ReLU останньою — це запобіжник на глибину, якої наша задача не потребує.

Скіп нічого не коштує. У ньому немає жодної ваги: це операція додавання двох тензорів. Наш блок на 16 каналів має 4 672 параметри — дві згортки 2 × 16 × 16 × 3 × 3 = 4 608 плюс два батчнорми 2 × 2 × 16 = 64, — і рівно стільки ж їх у простому блоці. Скіп додає трохи памʼяті (вхід блоку треба тримати до самого додавання) і кілька відсотків часу, але жодного параметра.

Куди йде додавання

Три варіанти проводки одного й того самого блоку. Дивись, де стоїть додавання відносно останньої ReLU і що з цього виходить.

проводка
блок може—
активація, блок 12—
розгін від блоку 1—
тест, 25 шарів—
Числа під схемою заміряні на нашій мережі з дванадцяти блоків. «Активація, блок 12» — середнє значення виходу останнього блоку на старті навчання; «розгін» — у скільки разів воно більше за вихід першого блоку. Точність — середнє з трьох прогонів на глибині 25.

НульНульова ініціалізація

У розділі 5 ми сказали: тотожність досягається, коли F(x) = 0. Це відкриває дешевий прийом, який радять усі практичні посібники з ResNet: зробити так, щоб на самому старті кожен блок уже видавав нуль.

Робиться це одним рядком: γ (гамма) останнього батчнорму в блоці ставимо в нуль.

nn.init.zeros_(block.bn2.weight)

Нагадаю, що робить батчнорм: він нормалізує число, а потім множить на γ і додає β. Ці два параметри мережа вчить сама, і за замовчуванням вони починають з γ = 1, β = 0. Якщо поставити γ = 0, вихід батчнорму стає нулем незалежно від входу. Отже, F(x) = 0, і блок віддає relu(0 + x), що дорівнює просто x — бо x прийшов із попереднього ReLU й уже невідʼємний.

Ми це перевірили прямо, а не на віру: перевірка коштує один рядок, torch.allclose(block(x), x). Він відповідає True для блоку з нульовим γ і False для звичайного, де максимальне відхилення виходу від входу становить 0.65.

Що це дає. Глибока залишкова мережа на старті поводиться як неглибока: усі блоки прозорі, працює тільки стем і голова. Масштаб сигналу не пливе: ми зміряли середнє активацій по дванадцяти блоках, і воно тримається на 0.73 від першого до останнього, тоді як у звичайної залишкової мережі росте з 0.91 до 2.36. Глибина «вмикається» поступово, у міру того як γ відходить від нуля.

А ось наш вимір, і він проти прийому. На нашій задачі нульова ініціалізація виявилась помітно гіршою: 0.358 перевірочної точності на глибині 25 проти 0.954 у звичайної залишкової мережі. Причина видно з інтерактиву 3: при γ = 0 градієнт усередині блоків дорівнює рівно нулю — не малий, а точно нуль. Блоки не вчаться взагалі, поки сама γ не зрушить з місця, а вона рухається повільно: за двадцять епох дійшла в середньому лише до 0.028. Ми перевірили здогад — дали шістдесят епох замість двадцяти, і картина розвернулась: 0.867 у мережі з нульовим γ проти 0.807 у звичайної. Отже, нульова ініціалізація — це прийом для довгого розкладу. Уведено її не в самій статті ResNet, а в Goyal та ін. (2017), де ImageNet вчили девʼяносто епох; на двадцяти вона не встигає нічого дати, а ціну бере одразу.

Нульова ініціалізація останнього BN

Перемкни старт. Стовпчики — середня активація на виході кожного з дванадцяти блоків до першого кроку навчання.

старт мережі
блок тотожний?—
розгін по стосу—
тест, 20 епох—
тест, 60 епох—
Заміряно на батчі з 64 зображень до першого кроку оптимізатора. При γ = 0 лінія рівна, бо кожен блок віддає рівно те, що отримав. Точність за 20 епох — середнє з трьох прогонів; за 60 епох — один прогін на глибині 25.

ФормиКоли форми не збігаються

Досі всі наші блоки мали однаковий вхід і вихід: 16 каналів по 7×7 усередині, 16 каналів по 7×7 назовні. Тому F(x) + x просто працює: два тензори однакової форми додаються поелементно.

Але справжня мережа так не будується. За правилом із теми 09 сторона карти час від часу зменшується вдвічі, а кількість каналів подвоюється. У той момент вхід блоку має форму, скажімо, 64 канали по 56×56, а вихід — 128 каналів по 28×28. Додати їх не можна: різні розміри.

Розвʼязок ResNet — пропустити сам скіп крізь проєкцію: згортку 1×1 з кроком 2. Згортка 1×1 (ми розбирали її в темі 07) не дивиться на сусідів — вона лише перемішує канали в кожній точці окремо. Крок 2 пропускає кожен другий піксель по обох осях, зменшуючи сторону вдвічі. Разом це рівно та операція, яка потрібна: змінити кількість каналів і сторону, не роблячи нічого змістовного.

y = F(x) + Ws·x

Ws — ваги цієї згортки 1×1. Скіп перестав бути безкоштовним, тож порахуймо, скільки він тепер коштує. Згортка 1×1 без зсуву з cin у cout каналів має cin × cout ваг, плюс батчнорм після неї додає ще 2 × cout.

У справжній resnet18 проєкція потрібна рівно тричі — на трьох переходах між стадіями. Ми порахували всі три:

перехідканалипроєкція, вагувесь блок, вагчастка
layer2[0]64 → 1288 448230 1443.67 %
layer3[0]128 → 25633 280919 0403.62 %
layer4[0]256 → 512132 0963 673 0883.60 %
разом—173 82411 689 5121.49 %

Тобто вся тотожність, яку ResNet купує проєкціями, коштує півтора відсотка мережі. Решта скіпів — а їх у resnet18 пʼять із восьми — не коштує нічого взагалі.

Коли скіпу потрібна проєкція

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

вхід—
вихід—
проєкція, ваг—
частка блоку—
Числа з torchvision.models.resnet18(weights=None), вхід 3×224×224. Верхня гілка — дві згортки 3×3, нижня — скіп. Коли форми збігаються, скіп намальовано лінією без коробки: він не має жодної ваги.
Чому не нулями. У статті ResNet розглядали й дешевший варіант: не проєкцію, а доповнення нулями — узяти 64 канали, дописати 64 нульових і викидати кожен другий піксель. Це справді безкоштовно. Але працює трохи гірше, і оскільки проєкції коштують півтора відсотка, вибір очевидний. Дешевий варіант досі трапляється в маленьких мережах для CIFAR, де важить кожна вага.

ГорлоПляшкове горло

Наш блок із двох згорток 3×3 звуть звичайним (basic block), і саме на ньому побудовані ResNet-18 і ResNet-34. Але ResNet-50 і глибші влаштовані інакше, і причина суто арифметична.

Згортка 3×3 із c каналів у c каналів має c × c × 9 ваг — тобто росте як квадрат кількості каналів. На 64 каналах це 36 864, на 512 — 2 359 296, у шістдесят чотири рази більше. Стос глибиною в пʼятдесят шарів на широких каналах просто не влізе нікуди.

Прийом називається пляшкове горло (bottleneck) і складається з трьох згорток замість двох:

1×1 : c → c/4      стиснути канали вчетверо — дешево, бо ядро одиничне
3×3 : c/4 → c/4    уся просторова робота — на вчетверо вужчому місці
1×1 : c/4 → c      розтиснути назад до потрібної ширини

Уся просторова робота — та сама згортка 3×3, яка й бачить сусідів, — робиться на вчетверо меншій кількості каналів. Дорога згортка отримує в шістнадцять разів менше ваг (бо c² стало (c/4)² ), а дешеві 1×1 по краях коштують мало.

каналівзвичайний блокпляшкове горлоу скільки менше
6473 9844 54416.3×
2561 180 67270 40016.8×
5124 720 640280 06416.9×
204875 505 6644 462 59216.9×

Останній рядок і є відповіддю на питання «навіщо». ResNet-50 закінчується стадією на 2048 каналів. На звичайних блоках вона важила б 226 516 992 ваги — більше, ніж уся VGG16 разом із її головою. На пляшкових горлах та сама стадія важить 14 964 736, у пʼятнадцять разів менше. Уся ResNet-50 разом виходить на 25 557 032 параметри — при пʼятдесяти трьох згорткових шарах це впʼятеро легше за VGG16 із тринадцятьма.

Звичайний блок проти пляшкового горла

Веди повзунок по ширині каналів. Обидва блоки мають однакові вхід і вихід — різна лише внутрішня будова.

звичайний, ваг—
горло, ваг—
звичайний, множень—
горло, множень—
виграш—
вузьке місце—
Ваги пораховані формулою, множення — для карти 14×14. Товщина смуги всередині схеми пропорційна кількості каналів у цьому місці — видно, як згортка 3×3 працює на вчетверо вужчому. Висота стовпчиків праворуч — кількість ваг у логарифмічній шкалі, інакше 4 544 і 75 мільйонів не вміщаються поруч.

ResNet18ResNet18 по шарах

Зберімо все докупи на справжній мережі. torchvision будує її локально, нічого не завантажуючи:

import torchvision.models as models
model = models.resnet18(weights=None)   # архітектура є, ваг з ImageNet немає

Будова: один згортковий шар 7×7 на початку, потім чотири стадії по два блоки, потім глобальне усереднення й один лінійний шар. Разом 20 згорткових шарів і 11 689 512 параметрів.

стадіяблоківканалівкрок першого блоку
conv1—3 → 642 (плюс пулінг 2)
layer1264 → 641
layer2264 → 1282
layer32128 → 2562
layer42256 → 5122
голова—512 → 1000—

А тепер головне число, заради якого ми сюди йшли ще з теми 13. Порахуймо, де ці 11.7 мільйона лежать:

мережаусього вагзгорткових шаріву згорткаху голові
AlexNet61 100 84054.0 %96.0 %
VGG16138 357 5441310.6 %89.4 %
ResNet1811 689 5122095.5 %4.4 %
ResNet3421 797 6723697.6 %2.4 %
ResNet5025 557 0325391.8 %8.0 %

Пропорція перевернулась. У VGG16 голова тримає 123 642 856 ваг, у ResNet18 — 513 000, у 241 раз менше. Мережа стала вчетверо глибшою за AlexNet — і водночас у дванадцять разів легшою за VGG16. Глибшою за саму VGG16 вона лише в півтора раза: уся економія тут у вазі, не в довжині.

І зробили це не скіп-зʼєднання. Зробило глобальне усереднення з теми 08: замість Flatten, що перетворює 512×7×7 на рядок із 25 088 чисел і віддає його повнозвʼязному шару на 4096 виходів, ResNet усереднює кожну карту в одне число й віддає лінійному шару рядок із 512. Один шар замість трьох, 512 входів замість 25 088.

Дві незалежні революції в одній мережі, і їх часто плутають. Скіп-зʼєднання зробило мережу навчальною на великій глибині — без них 20 шарів не зійшлись би. Глобальне усереднення зробило її легкою — воно прибрало 96 % ваг, які раніше сиділи в голові. Це різні прийоми, вони розвʼязують різні задачі, і кожен працює без іншого. У ResNet вони просто приїхали разом.

Ще одне число варте уваги. Батчнорми в ResNet18 займають 0.08 % параметрів — 9 600 ваг з одинадцяти з половиною мільйонів. Шар, без якого глибока мережа не навчається взагалі, важить менше однієї тисячної.

ПідсумокПідсумок і що далі

Що ми зміряли в цій темі:

Скіп-зʼєднання пішло далі за ResNet

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

Що далі

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

Але «легша за VGG16» — це слабка похвала. ResNet-50 — це 25 557 032 ваги, і сама лише її остання стадія, яку ми щойно порахували, важить 14 964 736 — більше, ніж уся ResNet18 разом. І це вже після того, як пляшкове горло зрізало з неї пʼятнадцять шістнадцятих. На сервері з відеокартою це дрібниця. На телефоні, який має вкластися в кадр відео й не розрядити батарею, це неможливо.

Отже, наступне питання не «як зробити глибше», а «як зробити дешевше» — і відповідь виявиться такою ж несподіваною, як і скіп-зʼєднання. Про це — тема 15, Inception, MobileNet, EfficientNet.