Попередня тема закінчилась трьома рецептами. Щоб мережа впоралася із зображенням, їй потрібні: локальність — нейрон дивиться на маленьку ділянку, а не на всю картинку; спільні ваги — та сама ділянка обробляється тим самим набором чисел, де б вона не була; і зменшення розмірності, щоб стос таких шарів не розрісся до непристойних розмірів.
Третій рецепт бере на себе тема 08. Перші два — це рівно один обʼєкт, і ти вже вмієш його рахувати. Це згортка з теми 04: вікно ковзає по зображенню, у кожному положенні відбувається кілька множень і одна сума. Локальність — це вікно. Спільні ваги — це те, що ядро в усіх положеннях одне.
Тобто нової операції в цій темі не буде. Буде одна зміна, на вигляд дрібна, а за наслідками — та, що розділила історію компʼютерного зору навпіл.
ЗмінаЯдро перестали писати руками
У темі 04 ми складали ядра самі. Ядро розмиття — девʼять однакових коефіцієнтів
1/9. Ядро Собеля по горизонталі — три відʼємні числа зліва, три додатні
справа. Ядро різкості — центр більший за одиницю, сусіди відʼємні. Кожен раз
працювала та сама схема: людина розуміла, яку властивість хоче побачити, і вигадувала
табличку чисел, яка на цю властивість реагує.
Тема 05 показала, куди це впирається. Придумана ознака працює рівно доти, доки задача схожа на ту, під яку її придумували. Собель чудово знаходить межі — але «межа» і «кіт» це різні речі, і між ними лежить десяток кроків, кожен з яких треба вигадати окремо.
Згортковий шар (convolutional layer) робить один-єдиний крок убік. Він каже: нехай коефіцієнти ядра будуть параметрами моделі — такими самими, як ваги звичайного нейрона. Ми їх не вписуємо. Ми ставимо туди випадкові числа, показуємо мережі приклади, рахуємо помилку, і градієнтний спуск сам зсуває кожен коефіцієнт у бік, який помилку зменшує.
Наслідок номер один: ми більше не мусимо знати, яку саме ознаку шукати. Наслідок номер два, менш очевидний і значно приємніший: одне ядро — це дуже мало чисел. Ядро 3 × 3 на сірому зображенні має девʼять коефіцієнтів. Девʼять чисел обробляють картинку будь-якого розміру, бо ковзають по ній. Саме звідси береться та різниця в тринадцять разів, яку показала попередня тема: у повнозвʼязній мережі на тому самому завданні було 50 435 параметрів, у згортковій — 4 003, і згорткова при цьому виявилась кращою.
ФормаФорма даних: N, C, H, W
Перш ніж рахувати, треба домовитись, як лежать дані. У
темі 01 зображення було
масивом (висота, ширина, канали) — саме так його віддає OpenCV. У PyTorch
порядок інший, і на цьому спотикаються всі без винятку:
Чотири літери означають ось що. N (batch) — скільки зображень у пачці,
яку ми подаємо в мережу за один раз. C (channels) — скільки каналів у
кожного зображення: 1 для сірого, 3 для кольорового. H (height) — висота
в пікселях. W (width) — ширина. Тобто тензор
(64, 3, 28, 28) — це 64 кольорові картинки 28 × 28.
Слово «тензор» тут не означає нічого страшного: це просто багатовимірний масив чисел,
рідний брат np.ndarray. Чотиривимірний — бо в нас чотири незалежні осі,
по яких можна рухатись.
Чому канали окремою віссю
Могло ж бути інакше: скласти три канали в один довгий вектор, як робить повнозвʼязна мережа. Але тоді ми втратимо саме те, заради чого затіяли згортку. Ядро має ковзати по просторових осях — вправо й униз. Канал — не просторова вісь: «сусіднього» каналу поруч із зеленим не існує, червоний не лежить ліворуч від синього. Тому осі розділені: по H і W вікно рухається, по C — ні, воно накриває всі канали одразу. Розділ 03 покаже, що з цього виходить.
Порядок саме такий («канали спереду», channels-first) з міркувань швидкості: усі
числа одного каналу лежать у памʼяті поспіль. У TensorFlow і Keras прийнято протилежне —
(N, H, W, C), «канали ззаду», — тож код із туторіалу на іншому фреймворку
без перестановки осей не запрацює.
nn.Conv2d масив із OpenCV як є. Він має форму (28, 28, 3), а шар
чекає (N, 3, 28, 28): спершу треба переставити осі
(np.transpose або tensor.permute(2, 0, 1)), потім додати вісь
пачки ([None] або unsqueeze(0)). Помилка гучна — шар скаржиться
на кількість каналів, — але тільки якщо H і W не дорівнюють 3. Квадратне зображення 3 × 3
проїде без жодного слова й дасть безглуздий результат.ГлибинаЯдро має глибину
А тепер головне непорозуміння теми, і воно варте окремого розділу.
Коли кажуть «ядро 3 × 3», у голові малюється квадратик із девʼяти чисел. На сірому зображенні так і є. Але щойно на вході три канали, ядро 3 × 3 перестає бути плоским. Насправді воно має розмір 3 × 3 × 3: три числа вшир, три вгору-вниз і стільки шарів углиб, скільки каналів на вході.
Прочитаємо словами. C_in — кількість каналів входу, k —
сторона вікна. Ядро — це стос із C_in квадратних табличок k × k,
по одній на кожен вхідний канал.
І другий пункт, ще важливіший: усе це дає один-єдиний канал на виході. Не три. Один. Робиться так: перша табличка множиться на червоний канал, друга — на зелений, третя — на синій; усі добутки складаються разом, в одну купу; до суми додається зсув. Виходить одне число.
Ставимо вікно 3 × 3 у певне місце зображення. Воно вирізає три квадратики 3 × 3 — по одному з кожного каналу.
Кожен квадратик множимо поелементно на свою табличку ядра й складаємо девʼять добутків. Три канали — три проміжні суми.
Три проміжні суми складаємо між собою й додаємо зсув b — одне число
на все ядро.
Разом задіяно 3 × 9 = 27 множень і один додаток зсуву. Результат — одне число в одній вихідній карті.
Чому канали складаються, а не лишаються окремо? Бо ознака, яку шукає фільтр, може жити в комбінації кольорів: «різкий перехід від жовтого до синього» — це одночасно перепад у червоному, зеленому й синьому, кожен зі своїм знаком. Даючи ядру різні коефіцієнти для різних каналів і складаючи їхні внески, ми даємо йому шанс таку комбінацію помітити.
Ядро має глибину
Три канали входу, одне ядро 3 × 3 × 3, один вихід. Тисни «канал» — і дивись, як три окремі суми зливаються в одне число.
ФільтриБагато фільтрів — багато карт ознак
Одне ядро вміє шукати одну річ. Мережі цього мало: вертикальні межі, горизонтальні межі, плями, кути, перепади кольору — усе це різні ознаки, і кожній потрібне своє ядро. Тому згортковий шар тримає не одне ядро, а цілий набір, і всі вони працюють по тому самому входу паралельно, нічого одне про одного не знаючи.
Скільки їх — вирішуєш ти, аргументом out_channels. Кожне ядро дає свій
окремий канал на виході. Ці вихідні канали називають картами ознак
(feature map): карта, на якій у кожній точці записано, наскільки сильно саме цей фільтр
відгукнувся саме тут.
Ось як це виглядає в коді:
Читаємо: подали 8 кольорових картинок 28 × 28, отримали 8 стосів по 16 карт ознак,
кожна теж 28 × 28. Число 3 у in_channels описує вхід і мусить
збігатися з тим, що подаєш. Число 16 в out_channels — це наше рішення:
скільки різних ознак шар має право вивчити.
Зверни увагу на симетрію, яка робить згорткові мережі складаними в стос. Вихід шару —
це знову тензор (N, C, H, W), просто з іншим C. Отже, його можна
подати в наступний згортковий шар, у якого in_channels = 16. Другий шар
дивитиметься вже не на пікселі, а на карти ознак першого: його ядра 3 × 3 × 16 шукатимуть
комбінації на кшталт «тут одночасно є вертикальна межа й пляма». Так із простих ознак
збираються складні — і саме так виглядає будь-яка згорткова мережа зсередини.
C_in × k × k, який дає одну карту ознак;
ядро — одна його табличка k × k для одного вхідного каналу. Фільтрів
у шарі рівно out_channels, а ядер — out_channels × in_channels.Карти ознак
Ліворуч — вхідне зображення. Праворуч — карти ознак після згорткового шару. Перемикай кількість фільтрів і дивись, скільки картинок виходить із однієї.
РахунокСкільки в шарі чисел
Тепер порахуємо параметри — і зробимо це так, щоб більше ніколи не гадати.
У шарі є out_channels фільтрів. Кожен фільтр — це in_channels
табличок k × k. Плюс у кожного фільтра свій зсув — одне число:
Перший доданок — усі коефіцієнти ядер, другий — зсуви. Підставимо числа для
nn.Conv2d(3, 16, 3): множимо 16 × 3 × 3 × 3 = 432, додаємо 16 зсувів,
разом 448. Перевірено запуском: sum(p.numel() for p in
layer.parameters()) дає рівно 448.
| Шар | ядра | зсуви | разом |
|---|---|---|---|
Conv2d(1, 8, 3) | 72 | 8 | 80 |
Conv2d(3, 16, 3) | 432 | 16 | 448 |
Conv2d(3, 16, 5) | 1 200 | 16 | 1 216 |
Conv2d(3, 32, 7) | 4 704 | 32 | 4 736 |
Conv2d(64, 64, 3) | 36 864 | 64 | 36 928 |
Conv2d(256, 256, 1) | 65 536 | 256 | 65 792 |
Чого у формулі немає
Придивись до правої частини ще раз. Там є C_in, C_out і
k. Там немає H і W. Розмір зображення на кількість
параметрів не впливає взагалі.
Це не фокус, а прямий наслідок спільних ваг. Ядро одне на все зображення: воно
проїжджає по 28 × 28 і по 512 × 512 тими самими коефіцієнтами. Більша картинка означає
більше роботи — більше положень вікна, більше множень, довший час, — але не
більше чисел, які треба вивчити. Перевірено: той самий
nn.Conv2d(3, 16, 3, padding=1) на входах 28 × 28, 128 × 128 і 512 × 512 дає
виходи 16 × 28 × 28, 16 × 128 × 128 і 16 × 512 × 512 — і 448 параметрів у всіх трьох
випадках.
Порівняй із повнозвʼязним шаром, який на цьому й ламається. Щоб перетворити той самий вхід 3 × 28 × 28 на 16 × 28 × 28 «кожен з кожним», йому потрібно 2 352 × 12 544 ваг плюс 12 544 зсувів — це 29 516 032 числа проти наших 448. У 65 884 рази більше, і кожне з них залежить від розміру картинки: зміни вхід на 32 × 32 — і всю матрицю доведеться будувати заново.
Лічильник параметрів
Крути три ручки згорткового шару й дивись, як росте кількість чисел у ньому — і як поруч стоїть повнозвʼязний шар на тому самому вході.
Крайpadding і доля краю
Тепер повернімось до питання, яке вже виникало в темі 04: що робити з краєм. Вікно 3 × 3, поставлене в кутовий піксель, наполовину звисає за межі масиву. Варіантів два: не ставити його туди взагалі або домалювати навколо зображення рамку.
Перший варіант зветься padding=0, або valid: вікно
рахується тільки там, де воно повністю всередині. Ціна — зображення зменшується.
Для ядра k зникає по (k−1)/2 пікселя з кожного боку:
28 × 28 після ядра 3 × 3 стає 26 × 26, після 5 × 5 — 24 × 24.
Один раз це дрібниця. Але згорткові мережі складають десятки шарів, і кожен ядром 3 × 3 зʼїдає по два пікселі. Від зображення 28 × 28 на чотирнадцятому шарі не лишилось би нічого.
Другий варіант зветься same: домалювати рамку такої товщини, щоб вихід вийшов рівно такого самого розміру, як вхід. Для непарного ядра при кроці 1 товщина рамки рахується просто:
Для k = 3 це 1, для k = 5 — 2, для k = 7 — 3.
Легко перевірити на формулі розміру виходу, до якої ми дійдемо в наступному розділі:
підстав p = (k−1)/2 — і вихід збігається зі входом. Для парного ядра
симетричної цілої рамки не існує взагалі, і це одна з причин, чому в мережах майже
завжди беруть непарні ядра. У PyTorch можна писати
padding='same' словом — але тільки при stride=1.
Чим саме заповнити рамку
Тема 03 дала чотири режими межі OpenCV: константа, повторення краю, дзеркало,
дзеркало без повторення пікселя. У нейромережах з усього цього багатства майже завжди
використовують найпростіший — нулі. Замовчування
nn.Conv2d саме таке: padding_mode='zeros'. Інші режими є
('reflect', 'replicate', 'circular'), але
трапляються рідко.
І це не безневинно. Нуль — не «немає даних», нуль — це чорний піксель. Мережа не знає, що там нічого немає: вона бачить чорну рамку й вчиться на ній так само старанно, як на справжніх даних.
Наслідок можна порахувати руками. Візьмемо вхід 8 × 8 і ядро 3 × 3. Скільки разів кожен піксель потрапляє у вікно?
- Без рамки (
valid, вихід 6 × 6): центральний піксель потрапляє у вікно 9 разів, кутовий — 1 раз. Утричі менше по кожній осі. - З рамкою (
same, вихід 8 × 8): центральний піксель — ті самі 9 разів, кутовий — 4. Краще, але й досі менше половини.
Тобто кутовий піксель за будь-якого режиму впливає на результат слабше за центральний, а вікна, що його накривають, наполовину заповнені вигаданими нулями. Для класифікації, де предмет зазвичай у середині, це майже неважливо. Для сегментації, де відповідь потрібна в кожній точці включно з краєм, моделі часто мажуть уздовж рамки.
padding і доля краю
Кожна клітинка показує, скільки разів цей піксель потрапив у вікно згортки. Перемикай режим — і дивись, що стається з кутом.
Крокstride і розмір виходу
Досі вікно рухалось на один піксель за раз. Але воно може стрибати — через два, через
три. Цей крок і зветься stride.
stride=2 означає: порахували вікно, перескочили на дві клітинки, порахували
знову. Половина положень просто не рахується, і вихід виходить приблизно вдвічі меншим
по кожній осі. Це найпряміший спосіб зменшити розмір карти ознак — і саме тому
stride ще зʼявиться в темі 08,
де зменшення розмірності розбирають окремо. Нам тут stride потрібен
рівно як четвертий аргумент згортки, який впливає на форму виходу.
А форма виходу рахується однією формулою. Вона виглядає страшнувато, тому спершу словами: беремо довжину доріжки, по якій може проїхати вікно, ділимо на довжину кроку — і додаємо одиницю, бо перше положення вікна теж рахується.
Розберемо символи. H — висота входу. p — товщина рамки
(padding) з одного боку, тому в формулі стоїть 2p: рамка є
згори й знизу. k — сторона ядра. s — крок
(stride). Кутові дужки ⌊ ⌋ означають округлення вниз:
2.9 стає 2, 3.0 лишається 3; у коді те саме робить floor, і саме так
формула підписана на фігурі нижче. Для ширини формула та сама, з W замість
H.
Вікно шириною k їде по рядку довжиною H + 2p (зображення
разом із рамкою). Його ліва межа може стояти в позиціях від 0 до
H + 2p − k — далі вікно звисає. Це і є доріжка.
Зупинки — через кожні s клітинок: доріжка, поділена на s
і округлена вниз. Плюс сама стартова позиція.
Вхід 28, ядро 3, рамка 1, крок 2: доріжка = 28 + 2 − 3 = 27; 27 ÷ 2 = 13.5,
округлюємо вниз до 13; додаємо 1 — виходить 14. Перевірено запуском:
nn.Conv2d(1, 1, 3, stride=2, padding=1) на вході 28 × 28 дає
справді 14 × 14.
Округлення вниз — це викинуті дані
Округлення в формулі не косметичне. Воно означає, що останні пікселі рядка можуть не потрапити в жодне вікно взагалі. Дивись: вхід 28, ядро 3, без рамки, крок 2. Доріжка = 25, 25 ÷ 2 = 12.5 → 12, плюс 1 = 13. Але 12 кроків по 2 доводять ліву межу вікна до позиції 24, і вікно накриває пікселі 24, 25, 26. Піксель номер 27 — останній у рядку — не бачив жодного фільтра. Його просто немає в результаті.
Ознака така: якщо (H + 2p − k) не ділиться на s націло,
частина краю відрізана мовчки. Ніякої помилки, ніякого попередження. Тому пари
stride і padding у мережах підбирають так, щоб ділилось
рівно — найпоширеніша комбінація k=3, s=2, p=1 рівно вдвічі зменшує
парний розмір і нічого не викидає.
| Вхід H | k | s | p | вихід | обрізано |
|---|---|---|---|---|---|
| 28 | 3 | 1 | 0 | 26 | 0 |
| 28 | 3 | 1 | 1 | 28 | 0 |
| 28 | 5 | 1 | 2 | 28 | 0 |
| 28 | 3 | 2 | 1 | 14 | 0 |
| 28 | 3 | 2 | 0 | 13 | 1 |
| 28 | 7 | 2 | 3 | 14 | 0 |
| 28 | 4 | 3 | 0 | 9 | 0 |
| 10 | 3 | 3 | 0 | 3 | 1 |
Останній стовпчик рахує справжні пікселі, які не потрапили в жодне вікно.
Шостий рядок показує тонкість: там (28 + 6 − 7) = 27 теж не ділиться на 2,
і одна позиція справді пропадає — але пропадає вона в рамці з нулів, а не в
зображенні. Тому втрати нема. Коли рамка досить товста, залишок відрізається від неї,
а не від даних.
Форма наскрізь
Чотири ручки згортки й одна формула. Сірим показано рамку з нулів, рожевим — пікселі, які не потрапили в жодне вікно.
k=3, p=1,
k=5, p=2 і k=7, p=3 — прожени всі три й побачиш той самий
вихід 14 × 14 без жодної рожевої смужки. У мережах беруть найдешевшу з них,
k=3, s=2, p=1.ВагиЩо фільтр вивчає насправді
Дійшли до найцікавішого. Ми стільки разів сказали «коефіцієнти підбирає навчання» — але що там опиняється в кінці? Подивімось.
Наскрізний набір цієї теми — фігури 28 × 28 у відтінках сірого, і класів у ньому шість: коло, квадрат, ромб, кільце, хрест, трикутник. Шість, а не три, узято навмисно: тема показує, які саме ознаки виростають у першому шарі, а різні фігури тягнуть за собою різні ознаки — кільце просить кола, хрест просить кутів. Шість класів задають і нуль відліку: мережа, що тицяє навмання, влучає в одному випадку з шести, тобто дає точність 0.167.
Кладемо першим шаром nn.Conv2d(1, 8, 5, padding=2) — вісім фільтрів
5 × 5 на сірому вході, разом 8 × 25 + 8 = 208 параметрів, — і вчимо мережу дванадцять
епох на 1 200 прикладах. Точність на перевірці — 0.993 проти 0.167 у вгадування.
Ніде, жодного разу, ми не кажемо мережі, що таке межа: ми показуємо картинки й назви
фігур.
Після навчання дістаємо model.conv1.weight і дивимось на числа. Ось
перший із восьми фільтрів того запуску — двадцять пʼять чисел, які нікому не задавали:
Верхні рядки відʼємні, нижні — додатні. Це рівно та схема, яку ми в темі 04 вписували руками в ядро Собеля: одна половина зі знаком мінус, друга зі знаком плюс, і велика відповідь там, де між ними є перепад. Тільки Собель по вертикалі, тобто детектор горизонтальних меж. Мережа дійшла до цієї ідеї сама, з градієнтів — і жоден рядок коду їй цього не підказував.
Серед восьми фільтрів немає двох однакових, і вони діляться на впізнавані типи. Чотири мають описану асиметрію згори вниз — детектори горизонтальних меж; найсильніший із них дає різницю «верх мінус низ» −3.23 при різниці «ліво мінус право» лише 1.36. У пʼятого великі обидві асиметрії одразу (+2.67 і −2.43) — він реагує не на межу, а на кут: там, де перепад є і по горизонталі, і по вертикалі. Останні три мають майже всі коефіцієнти одного знака: із сумами ваг +3.89 і +3.26 це підсумовувачі яскравості, тобто детектори світлої плями, а з сумою −1.66 — той самий детектор навпаки, що гасить світле й підсвічує темне.
torch.manual_seed(0), щоб у тебе вийшли рівно ці числа. По-друге, фільтри
шумні: знаки правильні, а всередині кожної половини числа стрибають. Причина проста —
датасет із простих фігур мережа розвʼязує майже до 100 % за кілька епох, після чого
градієнт майже зникає, і ваги перестають рухатись, не встигнувши причесатись. Красиві
смугасті фільтри з відомих ілюстрацій — це перший шар великих мереж, навчених на мільйоні
різноманітних фотографій. На шести видах фігур такого не буде, і підставити натомість
чужу картинку було б обманом.Це і є відповідь на питання, з якого почалась уся тема. Ознаку не треба вигадувати. Достатньо дати мережі місце, куди ознаку можна записати, і причину її знайти.
Що вивчив фільтр
Вісім фільтрів 5 × 5 із першого шару навченої мережі. Ліворуч — самі числа, праворуч — відгук цього фільтра на тестову фігуру.
ПолеРецептивне поле
Одне число на виході згорткового шару порахували з вікна 3 × 3 — тобто на нього вплинуло девʼять вхідних пікселів. Ця ділянка входу, від якої залежить конкретне вихідне число, зветься рецептивним полем (receptive field).
Для одного шару це просто саме ядро. Цікаве починається, коли шари складають у стос. Візьмемо другий шар із ядром 3 × 3 і поставимо його на вихід першого. Одне число після другого шару зібране з девʼяти чисел першого шару. Але кожне з тих девʼяти саме зібране з вікна 3 × 3 у вхідному зображенні. Вікна сусідніх позицій перекриваються, і разом вони накривають квадрат 5 × 5.
Один шар — 3 × 3, два — 5 × 5, три — 7 × 7, пʼять — 11 × 11. Кожен новий шар додає по
одному пікселю з кожного боку, тобто k − 1 до сторони. Ось звідки береться
головна властивість глибоких мереж: перші шари бачать крихітні шматочки й можуть помітити
тільки межу чи пляму, а глибокі шари дивляться вже на пів зображення — і тільки тому
здатні відрізнити колесо від тарілки.
Чому два шари 3 × 3 краще за один 5 × 5
Обидва варіанти дають рецептивне поле 5 × 5, обидва перетворюють вхід 32 × 32 на 28 × 28. Порахуймо ціну для 16 каналів на вході й 16 на виході:
Стос із двох дешевший на 28 % — обидва числа перевірені через
sum(p.numel()). І це ще не вся вигода: між двома шарами стоїть
нелінійність (ReLU), тобто пара шарів уміє виражати складніші залежності, ніж один
лінійний фільтр більшого розміру. Саме тому в сучасних мережах ядра майже завжди 3 × 3,
а «широкі» 7 × 7 лишились хіба що в найпершому шарі.
Рецептивне поле
Додавай шари й дивись, яка ділянка входу впливає на одне вихідне число. Праворуч — ціна стосу проти ціни одного широкого ядра з тим самим полем.
ОдинЗгортка 1 × 1
Ядро розміром один піксель на перший погляд безглузде: уся ідея згортки була в
сусідах, а воно на них не дивиться. Але kernel_size=1 — один із
найуживаніших шарів у сучасних мережах.
Згадай, що ядро має глибину. Ядро 1 × 1 при 256 каналах на вході — це не одне число, а 256 чисел: по одному коефіцієнту на канал. Воно бере в кожній точці стовпчик значень усіх каналів, множить кожне на свій коефіцієнт і складає. Тобто це звичайний повнозвʼязний шар — але не по пікселях, а по каналах, і той самий у кожній точці зображення.
Звідси три застосування, які ти зустрінеш у будь-якій архітектурі:
- Змішати канали. Після звичайної згортки кожна карта ознак жила своїм життям. Шар 1 × 1 дозволяє скласти з них комбінації: «нова ознака = ця плюс та мінус третя».
- Змінити кількість каналів. Стиснути 256 карт до 64 або, навпаки, роздути 64 до 256. Просторовий розмір при цьому не змінюється взагалі.
- Зробити це дешево. Ось у чому головна принада.
Порахуємо ціну. Стиснути 256 каналів до 64 ядром 3 × 3 коштує
64 × 256 × 9 + 64 = 147 520 параметрів. Те саме ядром 1 × 1 —
64 × 256 × 1 + 64 = 16 448. Ядра при цьому дешевшають рівно
вдевʼятеро (147 456 проти 16 384), бо k × k у формулі перетворюється з
девʼятки на одиницю; зсувів же в обох випадках по 64, вони не діляться, тому весь шар
виходить дешевшим у 8.97 раза. Множень при цьому теж удевʼятеро менше.
Ціна зміни каналів
Ліворуч — що ядро 1 × 1 робить в одній точці: бере стовпчик усіх каналів і згортає його в новий стовпчик. Праворуч — скільки це коштує проти того самого перетворення ядром 3 × 3.
Звідси й типовий прийом: перед дорогою згорткою 3 × 3 стиснути канали шаром 1 × 1, порахувати 3 × 3 на вузькому місці й розтиснути назад. У темах про архітектури ця конструкція зветься пляшковим горлом (bottleneck): широкий вхід, вузьке горло посередині, широкий вихід.
НазваЦе кореляція, а не згортка
Коротка чесна примітка, обіцяна ще в
темі 04. Там ми зʼясували, що
cv2.filter2D рахує не згортку, а кореляцію: справжня
згортка перед роботою перевертає ядро на 180°, а filter2D цього не робить.
З nn.Conv2d рівно та сама історія. Перевірено запуском: подаємо на вхід
чорний квадрат з єдиною білою точкою й ядро 1 2 3 / 4 5 6 / 7 8 9. Відгук
виходить такий:
Це ядро, повернуте на пів оберта, — характерна ознака кореляції. Звірка зі
scipy.ndimage, де обидві операції існують окремими функціями, підтверджує
прямо: Conv2d == correlate дає True, а
Conv2d == convolve дає False.
Чому ж шар називають згортковим? Бо коли коефіцієнти навчаються,
різниці немає жодної. Якщо для задачі потрібне ядро A, а операція
фактично рахує кореляцію, градієнтний спуск просто знайде дзеркальне ядро — і результат
буде той самий. Мережі однаково, у якому порядку лежать її власні числа. Назва прижилася
історично, з математичної традиції, і міняти її пізно.
nn.Conv2d руками, результат вийде дзеркальним. Ліки
одні: перевернути ядро самому (np.flip по обох осях) перед тим, як
покласти у ваги.ДаліКуди далі
Підсумуймо, що тепер відомо про згортковий шар.
Це та сама операція з теми 04, у якої коефіцієнти стали параметрами. Вхід і вихід —
тензори (N, C, H, W). Ядро має глибину C_in і дає один
канал на виході; фільтрів у шарі стільки, скільки просиш у out_channels.
Параметрів — C_out × C_in × k × k + C_out, і розміру зображення в цій
формулі немає. padding вирішує долю краю, stride — крок вікна,
а розмір виходу дає ⌊(H + 2p − k)/s⌋ + 1 з округленням униз, яке мовчки
викидає залишок. Стос шарів нарощує рецептивне поле дешевше за одне широке ядро, а ядро
1 × 1 перемішує канали за девʼяту частину ціни. І так, усередині це кореляція.
Лишилось те, що ми весь час обходили. Подивись на форму виходу ще раз:
(8, 16, 28, 28). Картинка була 28 × 28 з одним каналом — 784 числа. Після
одного шару з шістнадцятьма фільтрами стало 12 544. Шар додав ознак, але просторовий
розмір не зменшив анітрохи: карти ознак лишились завбільшки з зображення. Другий такий
шар — і чисел стане ще більше, третій — ще. Стос виходить дорогим за памʼяттю й
повільним, а рецептивне поле росте всього по два пікселі за шар: щоб побачити половину
картинки, знадобилося б сім шарів.
Тому третій рецепт із теми 06 — зменшення розмірності — не розкіш, а необхідність. Як саме зменшувати, чим за це платять і чому це заодно дає ту саму терпимість до зсуву, якої повнозвʼязній мережі так бракувало, — про це тема 08, «Пулінг і зменшення розмірності».