Пʼять тем блоку про детекцію закінчились одним реченням, яке варто перечитати: рамка каже, де предмет, але не каже, які саме пікселі йому належать. Прямокутник по осях описує людину, що йде, з величезним запасом порожнечі всередині; описує листок на гілці разом із половиною гілки; описує пухлину на знімку разом зі здоровою тканиною навколо. Якщо тобі треба порахувати площу, вирізати предмет із фону або сказати, де рівно закінчується дорога — рамка не відповідає в принципі. Сьогодні ми міняємо форму відповіді востаннє: замість списку прямокутників мережа видає карту, у якій підписаний кожен піксель.
ПостановкаВід рамки до пікселя
Тема 02 вже називала цю задачу на карті завдань компʼютерного зору й дала їй імʼя: семантична сегментація. Формулювання коротке. Вхід — зображення висотою H і шириною W. Вихід — масив тієї самої висоти й ширини, у якому на місці кожного пікселя стоїть номер класу. Такий масив зветься маскою (mask).
«Семантична» означає: ми питаємо що це, а не котрий це. Три кола на одній картинці дадуть одну спільну область із міткою «коло» — маска не розрізняє, де перше коло, а де друге. Задача «розрізнити окремі предмети» зветься instance segmentation, і їй присвячена тема 31; задача «дати кожному пікселю і клас, і номер предмета» зветься panoptic — це тема 32. Сьогодні ми свідомо беремо найпростішу з трьох і розбираємо її до дна, бо всі решта стоять на ній.
Що змінюється порівняно з детекцією:
Останній рядок другого пункту — не жарт і не спрощення. Семантична сегментація справді є класифікацією: тією самою, яку ми робили в темі 09, тільки замість одного числа на картинку модель дає одне число на піксель. Уся складність теми в тому, як зробити так, щоб цих чисел було стільки ж, скільки пікселів на вході.
ІдеяКласифікація кожного пікселя
Візьмімо звичайну класифікаційну мережу з блоку 2 і подивимось, що в ній заважає. Вона влаштована так: кілька згорткових блоків стискають картинку в карту ознак, далі глобальне усереднення перетворює карту на вектор, далі повнозвʼязний шар дає логіти класів. Останні два кроки і є перешкода — вони навмисно викидають просторову інформацію. Глобальне усереднення каже «десь на картинці є кіт», і саме цього ми колись хотіли.
Прибираємо їх. Замість глобального усереднення й повнозвʼязного шару ставимо
згортку 1×1 з числом виходів, рівним числу класів. Що вона робить:
бере вектор ознак у кожній клітинці карти окремо й перетворює його на вектор логітів
класів у тій самій клітинці. Просторова сітка при цьому не рухається взагалі.
Це і є головна ідея FCN — fully convolutional network, повністю згорткова мережа. Стаття 2015 року називалась саме так, і вся її новизна вміщається в один абзац: викиньте з класифікатора повнозвʼязні шари, і він почне видавати карту міток замість одного класу. Мережа стає повністю згортковою — у ній не лишається жодного шару, який залежить від розміру входу.
Друга, менш очевидна перевага: така мережа приймає вхід будь-якого розміру.
Повнозвʼязний шар привʼязаний до довжини вектора, тому класифікатор вимагає рівно
224×224. Згортці 1×1 байдуже: подай 512×512 — дістанеш більшу карту.
Де в класифікаторі вмирає сітка
Два шляхи від тієї самої карти ознак. Ліворуч — класифікатор із блоку 2, праворуч — та сама мережа з іншою головою. Тіло однакове; уся різниця починається на четвертому кроці. Числа — з нашого прикладу: сцена 64×64, тіло з кроком 8, чотири класи.
1×1 робить рівно ту саму роботу — перекодовує 64 ознаки в 4 логіти, —
але робить її окремо в кожній клітинці, тому сітка 8×8 доживає до виходу.
Порахуй числа в третьому й пʼятому рядку: 4 096 → 64 ліворуч і 4 096 → 256
праворуч.А тепер погана новина, з якої й починається справжня тема.
ПроблемаГоловна проблема — роздільність
Тіло класифікаційної мережі стискає. У ResNet-50 пʼять етапів зменшення, разом
вони дають крок (stride) 32: карта ознак у 32 рази менша за вхід по кожній
стороні. При вході 224×224 остання карта має розмір 7×7. Тобто наш чудовий
шар 1×1 видасть маску 7×7 — сорок девʼять чисел на все зображення.
Порахуймо, що це означає для одного пікселя карти:
| мережа | крок | карта при вході 224×224 | один піксель карти = |
|---|---|---|---|
| FCN-32s | 32 | 7×7 | 32×32 px входу |
| FCN-16s | 16 | 14×14 | 16×16 px |
| FCN-8s | 8 | 28×28 | 8×8 px |
stride — крок, тобто
у скільки разів карта дрібніша за вхід. FCN-32s бере логіти просто з останньої карти
тіла. А от FCN-16s і FCN-8s дістають дрібнішу сітку не тим, що менше
стискають: тіло в них те саме, з кроком 32. Вони додають до розтягнутих логітів
останньої карти ще одні логіти, пораховані на ранішій, ще не так сильно
стиснутій карті того самого тіла — і тільки суму розтягують до розміру входу. Тобто
вже у статті 2015 року роздільність повертали, беручи її з ранніх шарів. Ця думка
проросте у скіп-зʼєднання теми 30; поки що
запамʼятай саме назву, бо в статтях цифра після дефіса завжди означає крок.Один піксель карти при кроці 32 відповідає квадрату 32×32 вхідних пікселів — це 1024 пікселі, яким мережа дає одну спільну відповідь. Межі предмета всередині цього квадрата не існує. Саме тому маски FCN-32s зі статей виглядають як розмиті плями.
На наших сценах 64×64 картина та сама, тільки числа менші: крок 16 дає карту 4×4, і фігура радіусом 6-10 пікселів уміщається в одну її клітинку цілком.
Наскільки це дорого — можна поміряти без жодного навчання. Візьмемо істинну маску, загрубимо її до кроку S і повернемо назад до 64×64. Ніякого навчання: ми просто дивимось, скільки від маски лишається після проходу крізь сітку такого розміру.
Крок карти вбиває межу
Ліворуч — істинна маска 64×64. Посередині — та сама маска, стиснута до карти кроку S. Праворуч — те, що вийде, якщо повернути її назад до розміру входу. Перемикач задає, як повертати: сходинками (кожна клітинка — суцільний квадрат) чи плавно (лінійна інтерполяція оцінок класів). Число mIoU рахується по 24 сценах одразу, не по одній намальованій.
Ті самі числа, пораховані на всіх 120 перевірних сценах, варто виписати окремо — вони задають рамку всій темі. Це міра втрати на самій сітці, ще до будь-якого навчання:
| крок | карта | mIoU сходинками | mIoU плавно |
|---|---|---|---|
| 2 | 32×32 | 0.8577 | 0.9738 |
| 4 | 16×16 | 0.6631 | 0.9081 |
| 8 | 8×8 | 0.4039 | 0.6528 |
| 16 | 4×4 | 0.1503 | 0.1300 |
| 32 | 2×2 | 0.0295 | 0.0013 |
Тепер поміряймо те саме на справжній мережі. Будуємо просту FCN-подібну модель:
тіло з кількох згорткових блоків зі стисканням, голова 1×1, білінійне
збільшення логітів до 64×64. Міняємо лише крок тіла:
| крок тіла | карта | параметрів | mIoU (3 зерна) | час навчання |
|---|---|---|---|---|
| 4 | 16×16 | 4 916 | 0.6815 ± 0.0606 | 12.9 с |
| 8 | 8×8 | 7 252 | 0.6151 ± 0.0279 | 11.8 с |
| 16 | 4×4 | 9 588 | 0.2268 ± 0.0224 | 10.0 с |
Перше, що впадає в око, — параметрів більше в гіршої мережі: крок 16 має 9 588 ваг проти 4 916 у кроку 4, бо кожен рівень стискання додає ще один згортковий блок. Стискання купує не якість, а дешевизну обчислень.
Друге: між кроком 4 і кроком 8 різниця лише 0.0664 при розкиді 0.0606 — тобто її фактично немає. А от між кроком 8 і кроком 16 різниця 0.3884, у 14 разів більша за розкид. Це і є момент, коли роздільності перестає вистачати: ціла фігура вміщається в одну клітинку карти 4×4, і мережі нема де сказати, де в цій клітинці межа.
МетрикиМетрики: IoU, mIoU, Dice
Перш ніж лагодити роздільність, треба домовитись, чим міряти. І тут на нас чекає пастка, у яку легко впасти й важко помітити.
Чому точність по пікселях безглузда
Найочевидніша метрика — частка правильно підписаних пікселів. Порахуймо, з чого складається наша перевірна вибірка:
| клас | пікселів | частка |
|---|---|---|
| фон | 442 403 | 90.01 % |
| квадрат | 22 475 | 4.57 % |
| коло | 14 949 | 3.04 % |
| трикутник | 11 693 | 2.38 % |
Тепер уяви найгіршу можливу модель: вона фарбує все зображення фоном і більше не вміє нічого. Її точність по пікселях дорівнює 0.9001 — і це не збіг, а тотожність: частка правильних пікселів у такої моделі дорівнює частці фону, хоч би що ти робив із даними. Девʼяносто відсотків звучать як добра модель. Користі — нуль.
Але порожня модель — надто легка мішень: її відкинеш і без метрики. Справжня біда в іншому, і побачити її можна лише на моделі, яка майже все робить правильно. Порахуймо на пальцях.
Хай мережа ідеально виділяє фон, квадрат і коло, а трикутник зафарбовує фоном — просто не бачить його. Один клас із трьох утрачено повністю. Неправильних пікселів у неї рівно стільки, скільки їх у трикутника:
Тобто модель, яка не бачить цілий клас, просіла всього на 0.0238 — і це знову тотожність, а не збіг: утрата дорівнює частці загубленого класу. І ось де ламається метрика: точність усереднює по пікселях, тому клас важить у ній рівно стільки, скільки займає площі. У трикутника 2.38 % голосів, у фону — 90.01 %. Загубити найдрібніший клас цілком дешевше, ніж помилитись на трьох відсотках фону, і оптимізатор це чудово розуміє.
Точність по пікселях міряє розмір фону, а не модель
Повзунок міняє середній радіус фігур — а разом із ним і частку фону. Перемикач обирає порожню модель: «усе фон», «усе квадрат» або випадковий підпис. Обидві метрики рахуються по 24 сценах.
IoU по класах і mIoU
Отже потрібна метрика, у якій маленький клас важить стільки ж, скільки великий. Правильна метрика будується так само, як у детекції, тільки не для рамок, а для множин пікселів. Для кожного класу окремо:
Словами: скільки пікселів ми і мережа назвали цим класом разом, поділити на скільки пікселів назвав цим класом хоч хтось із двох. Символ ∩ — перетин, спільна частина двох множин; ∪ — обʼєднання, усе разом без повторів. Число завжди між 0 і 1: одиниця означає повний збіг, нуль — жодного спільного пікселя.
mIoU (mean IoU) — середнє цих чисел по класах. І тут є питання, від якого залежить сама цифра: чи входить у середнє фон. У нашому курсі — ні, ми усереднюємо по трьох класах предметів. Причина видно на прикладі вище: модель «усе фон» має IoU фону 0.9001 і IoU предметів 0, тож середнє з фоном дало б 0.2250, а без фону — рівно 0.000. Друге число чесніше описує, що модель нікому не потрібна. Обидві угоди трапляються в статтях, тому, читаючи чуже mIoU, завжди перевіряй, які класи в ньому усереднені.
Хай істина — квадрат 10×10 пікселів, тобто 100 пікселів класу «квадрат». Мережа підписала квадратом область 12×12 = 144 пікселі, зсунуту на 2 пікселі вправо й на 2 вниз.
Спільна частина — прямокутник 8×8, бо зсув на 2 зʼїдає по два стовпці й два рядки з боку істини.
Сума площ мінус перетин, щоб спільну частину не порахувати двічі.
Здається несправедливо суворо: мережа накрила предмет цілком, а дістала трохи більше за третину. Але IoU карає одразу за дві речі — за пропущене й за зайве, — і саме тому його важко обдурити.
Чому mIoU лікує те, що зламало точність
Повернімось до моделі, яка загубила трикутник, і поміряймо її новою метрикою. IoU квадрата й кола в неї по 1 — вони виділені ідеально. IoU трикутника — рівно 0: жодного його пікселя не названо трикутником, тож перетин порожній.
Порівняй два числа на ту саму помилку: точність просіла на 0.0238, mIoU — на 0.3333. Друга метрика чутливіша до неї в чотирнадцять разів, і причина в одному слові — що саме усереднюють. Точність усереднює по пікселях, тому голос класу дорівнює його площі. mIoU усереднює по класах: кожен із трьох предметів дає свою третину, хай яка його площа. Трикутник важить у ньому рівно стільки ж, скільки квадрат, майже вдвічі за нього більший, — і рівно стільки ж, скільки важив би фон, якби ми його рахували.
Друга половина ліків — у самому дробі IoU, і без неї перша не працювала б. Рівні голоси класів самі по собі можна було б обдурити з протилежного боку: малюй рідкісний клас щедро, з великим запасом, — і пропущених пікселів не лишиться зовсім. Не вийде, бо в знаменнику IoU стоїть обʼєднання, куди входить і все зайве, що намалювала мережа: перетин виросте трохи, обʼєднання роздується — і дріб упаде. Саме ця властивість поверне нам у розділі 10 неприємний результат: зважування класів у втраті метрику не покращило, бо воно лікує тільки пропущене й тим самим додає зайвого.
Чим Dice відрізняється від IoU
Друга метрика, яку постійно цитують поруч, — коефіцієнт Дайса (Dice coefficient), у медичних статтях його ще звуть F1 по пікселях:
Різниця з IoU в знаменнику: там було обʼєднання, тут — сума площ, у якій перетин порахований двічі. Через це Dice завжди більший або рівний IoU. Звʼязок між ними точний і не залежить від форми масок:
Підставимо наш приклад: IoU = 0.3556, отже Dice = 2·0.3556/1.3556 = 0.5246. Перевіримо прямо за визначенням: 2·64/(100+144) = 128/244 = 0.5246. Збіглося.
Тепер найважливіше — наскільки Dice оптимістичніший. Візьмімо дві маски однакової площі й будемо міняти частку перекриття:
| перекриття | IoU | Dice | у скільки разів більший |
|---|---|---|---|
| 90 % | 0.8182 | 0.9000 | 1.10 |
| 50 % | 0.3333 | 0.5000 | 1.50 |
| 10 % | 0.0526 | 0.1000 | 1.90 |
На добрих масках дві метрики майже збігаються — різниця 10 %. На поганих Dice майже вдвічі оптимістичніший: 0.0526 проти 0.1000. Практичний наслідок простий: дві статті на одному датасеті можуть звітувати 0.85 і 0.75, і краща з них — та, що цитує IoU. Дивись, яку метрику назвали, перш ніж порівнювати числа.
IoU і Dice на двох масках, які можна зсувати
Ліворуч — дві однакові маски; повзунок розводить їх. Перетин підсвічено. Праворуч — крива Dice = 2·IoU/(1+IoU) і поточна точка на ній. Обидва числа рахуються прямим підрахунком пікселів, а не за формулою — формула лише звіряється з ними.
Спосіб 1Спосіб перший: просто збільшити
Отже, у нас є карта логітів 8×8, а потрібна маска 64×64. Найпростіше рішення — розтягнути карту інтерполяцією. Ніяких ваг, ніякого навчання: чиста арифметика.
Найгрубіший варіант — найближчий сусід (nearest): кожна клітинка карти стає суцільним квадратом 8×8. Саме його ми міряли в інтерактиві 1 як «сходинками», і саме він дав 0.4039. Межа виходить пилчастою, з кроком у розмір клітинки.
Білінійна інтерполяція робить те саме плавно. Для кожного вихідного пікселя вона знаходить чотири найближчі клітинки карти й бере їхнє зважене середнє: що ближче клітинка, то більша вага. «Бі-» означає, що це робиться двічі — спершу вздовж рядка, потім уздовж стовпця.
Хай вихідний піксель лежить між клітинками карти так, що по горизонталі він відстоїть на 0.25 від лівої, а по вертикалі — на 0.75 від верхньої. Позначимо ці частки u = 0.25 і v = 0.75.
Вага кожного сусіда — добуток двох «протилежних» часток. Ближчий по горизонталі дістає (1−u), дальший — u; так само по вертикалі.
Сума ваг дорівнює одиниці: 0.1875+0.0625+0.5625+0.1875 = 1.0000. Це важливо — інакше інтерполяція міняла б яскравість.
Значення пікселя — сума значень чотирьох клітинок, помножених на їхні ваги. Ніяких навчених чисел тут немає взагалі.
Чому це дає розмиту межу. Між двома сусідніми клітинками інтерполяція будує лінійний перехід завширшки в одну клітинку — вісім пікселів при кроці 8. Різкої межі там нема з чого взяти: карта не містить інформації, де саме в цьому проміжку предмет закінчився. Ми не відновлюємо деталь, ми згладжуємо сходинки — і все ж це не дрібниця: 0.4039 проти 0.6528 на тій самій карті кроку 8. Плавне збільшення дає межу з точністю до половини клітинки замість цілої.
deeplabv3_resnet50 рахує логіти на карті кроку 8 і в останньому рядку
розтягує їх білінійно до розміру входу. Уся робота зроблена до цього кроку,
а не в ньому.Спосіб 2Спосіб другий: навчити збільшення
Другий підхід: хай ваги збільшення теж вчаться. Операція зветься транспонована
згортка (transposed convolution), у коді — nn.ConvTranspose2d.
Трапляється назва «деконволюція», і вона невдала: жодного оберненого перетворення
тут немає.
Що вона робить: звичайна згортка бере багато вхідних пікселів і дає
один вихідний; транспонована — навпаки. Вона бере один вхідний
піксель, множить на нього все ядро й додає цей відбиток у кілька сусідніх
вихідних клітинок, потім зсувається на stride і додає наступний.
Вихід — сума всіх накладених відбитків.
І тут ховається пастка, через яку транспонована згортка стала відома більше своїми дефектами, ніж перевагами.
Артефакт шахівниці
Порахуймо, скільки разів кожна вихідна клітинка отримує внесок. Хай ядро має розмір k, а крок дорівнює 2. Відбитки ядра лягають через два, тому клітинка з парним номером і клітинка з непарним номером можуть накритись різну кількість разів.
Найпростіша перевірка: покладімо всі ваги ядра рівними одиниці й подамо на вхід одиниці. Тоді значення кожної вихідної клітинки дорівнює просто кількості відбитків, які на неї впали. Ось що виходить у центрі карти для клітинки 2×2:
| ядро · крок | внесків у 4 позиціях | дисперсія | найбільше / найменше |
|---|---|---|---|
| 2 · 2 | 1 1 1 1 | 0.0000 | 1.00 |
| 3 · 2 | 1 2 2 4 | 1.1875 | 4.00 |
| 4 · 2 | 4 4 4 4 | 0.0000 | 1.00 |
| 5 · 2 | 4 6 6 9 | 3.1875 | 2.25 |
| 6 · 2 | 9 9 9 9 | 0.0000 | 1.00 |
Правило видно одразу: якщо розмір ядра ділиться на крок без остачі, усі позиції покриті однаково. Якщо ні — на виході зʼявляється регулярний візерунок із періодом у крок, схожий на шахівницю. При ядрі 3 і кроці 2 одна позиція з чотирьох отримує вчетверо більший внесок за іншу, і жодне навчання цього не прибирає: мережа може підправити ваги, але не може змінити, скільки разів клітинка накрита.
Звідки береться шахівниця
Ліворуч — куди лягають відбитки ядра на вихід, коли вхідна карта 8×8 збільшується вдвічі з кроком 2. Праворуч — скільки внесків отримала кожна вихідна клітинка: що темніше, то більше. Перемикай розмір ядра й дивись, коли візерунок зникає.
Обидва ядра ми поставили в одну мережу — тіло кроку 16, чотири сходинки збільшення по два — і навчили кожну на трьох зернах:
| збільшення | параметрів | mIoU (3 зерна) | час, с |
|---|---|---|---|
| ядро 4, крок 2 | 26 100 | 0.1673 ± 0.0250 | 19.3 |
| ядро 3, крок 2 | 18 932 | 0.0602 ± 0.0694 | 19.5 |
Перший висновок: ядро 3 гірше за ядро 4 — 0.0602 проти 0.1673. Але головне тут не середнє, а окремі зерна: у ядра 3 вони дали 0.1575, 0.0230, 0.0001. Одне навчання з трьох розвалилось повністю, а розкид (0.0694) більший за сам результат — тоді як у ядра 4 він 0.0250, у 2.8 раза менший. Саме ця нестабільність, а не різниця середніх, і є найпереконливіший наслідок нерівного покриття.
Другий висновок неприємніший для самої ідеї: обидві мережі гірші за просте білінійне збільшення з того самого тіла, яке дало 0.2268. Тобто 16 512 додаткових ваг не купили нічого — саме тому сучасні декодери майже не беруть ConvTranspose2d.
ConvTranspose2d — став
kernel_size, кратний stride: 4 при кроці 2, 6 при кроці 3.
Ще безпечніше — не брати її взагалі: nn.Upsample плюс звичайна згортка
3×3 дають те саме вміння з навченими вагами й без жодного візерунка.
Саме так і роблять сучасні декодери, і це той шлях, яким піде
тема 30.Спосіб 3Спосіб третій: не стискати взагалі
Обидва попередні способи лікують наслідок. Карта вже стала маленькою, і ми намагаємось відновити те, чого в ній немає. DeepLab поставив питання інакше: а навіщо ми взагалі стискаємо?
Відповідь відома з теми 08: стискання розширює рецептивне поле (receptive field). Згортка 3×3 бачить три пікселі. Дві згортки поспіль — пʼять. Щоб один нейрон побачив ділянку 33×33, потрібно шістнадцять згорток 3×3 підряд, а це шістнадцять шарів ваг. Пулінг дає те саме дешевше: після зменшення вдвічі та сама згортка 3×3 накриває вже 6 пікселів оригіналу.
Атрозна згортка (atrous convolution, вона ж dilated) дає третій вихід. Ідея
дитяча: лишити в ядрі ті самі девʼять ваг, але розсунути їх. Замість дивитись
на сусідні пікселі, ядро дивиться через один, через три, через сім. Аргумент, що цим
керує, зветься dilation.
Тут k — скільки ваг у ядрі по стороні (у нас 3), d — dilation. При
d = 1 виходить звичайна згортка 3×3. При d = 16 ядро охоплює 16·2 + 1 = 33
пікселі по стороні. І ось головне число теми:
| dilation | ефективне ядро | поле, пікселів | параметрів | множень на піксель |
|---|---|---|---|---|
| 1 | 3×3 | 9 | 36 928 | 36 864 |
| 2 | 5×5 | 25 | 36 928 | 36 864 |
| 4 | 9×9 | 81 | 36 928 | 36 864 |
| 8 | 17×17 | 289 | 36 928 | 36 864 |
| 16 | 33×33 | 1 089 | 36 928 | 36 864 |
Числа взяті з nn.Conv2d(64, 64, 3, padding=d, dilation=d). Поле зору
виросло зі 9 пікселів до 1089 — у 121 раз. Параметрів як було 36 928, так і
лишилось: 64·64·9 ваг плюс 64 зсуви. Множень теж стільки само. Це і є вся ідея
DeepLab в одному рядку: рецептивне поле можна купити задарма.
Наслідок для сегментації прямий. Якщо рецептивне поле більше не потребує стискання, то можна прибрати останні пулінги з тіла й лишити карту великою. Мережа бачить контекст і водночас зберігає роздільність. Саме так улаштований DeepLab: у ResNet-50 останні два етапи зменшення замінені на dilation 2 і 4, і крок падає з 32 до 8 без утрати рецептивного поля.
Але задарма нічого не буває, і в атрозної згортки є своя ціна — навіть дві.
Поле росте, параметри не рухаються
Ліворуч — сітка вхідних пікселів; підсвічені девʼять, які читає
ядро при заданому dilation, і окреслене поле, яке воно накриває.
Праворуч — два лічильники в часі: рецептивне поле росте квадратично, параметри стоять.
dilation ядро бачить широко, але рідко.Це перша ціна, і вона має назву — ефект решітки (gridding). Сусідні пікселі починають читати різні, ніде не перетинні набори відліків: піксель із парною координатою при d = 2 бачить лише парні, з непарною — лише непарні. Дрібна деталь завширшки в один-два пікселі може провалитись між зубцями решітки. Через це в DeepLab dilation усередині блоку не роблять однаковим — беруть кілька різних значень, щоб дірки одного ядра затулялись відліками іншого.
Друга ціна — час. Атрозна згортка не додає множень на піксель, але вона працює на карті, яку ми не стиснули. Карта 64×64 містить у 64 рази більше клітинок, ніж карта 8×8, і кожна з них коштує повного набору множень. У наших замірах це видно прямо в секундах — про це наступні розділи.
ASPPASPP: кілька масштабів одразу
Тепер зʼєднаймо дві думки. Перша: різні предмети на одному кадрі мають різний
розмір, і потрібне їм рецептивне поле теж різне. Друга: одне атрозне ядро з великим
dilation бачить широко, але рідко.
Рішення DeepLab зветься ASPP — atrous spatial pyramid pooling. Замість
однієї згортки ставимо кілька паралельних гілок з різними
dilation, пропускаємо ту саму карту крізь усі, склеюємо результати по
каналах і зводимо їх назад згорткою 1×1. У справжньому
deeplabv3_resnet50 гілок пʼять:
| 1×1 | без контексту взагалі — чисте перекодування ознак у точці |
| 3×3, dilation 12 | поле 25×25 клітинок карти |
| 3×3, dilation 24 | поле 49×49 |
| 3×3, dilation 36 | поле 73×73 |
| глобальне усереднення | уся карта одним числом на канал |
Остання гілка заслуговує окремого слова. Вона стискає всю карту до одного пікселя
усередненням, пропускає його крізь 1×1 і розтягує назад. Навіщо: жодне
скінченне dilation не бачить кадр цілком, а глобальний контекст іноді
вирішує все — синє над горизонтом це небо, синє під ним вода.
Що покриває кожна гілка ASPP
Сітка навколо одного пікселя. Кожна гілка малює свої девʼять
відліків своїм кольором. Перемикай набір dilation і дивись, як
змінюється сумарне поле й скільки пікселів у ньому взагалі прочитано.
Замір: те саме тіло кроку 16, та сама голова 1×1, різниця лише
в модулі між ними.
| модуль контексту | параметрів | mIoU (3 зерна) | час, с |
|---|---|---|---|
| одна гілка 3×3, dilation 2 | 12 212 | 0.0746 ± 0.0559 | 10.5 |
| ASPP 1 · 2 · 4 + усереднення | 15 860 | 0.1920 ± 0.0059 | 13.2 |
ASPP кращий за одну гілку в 2.6 раза: 0.1920 проти 0.0746. Різниця 0.1174 — удвічі більша за розкид однієї гілки (0.0559) і у двадцять разів за розкид ASPP (0.0059).
Друге — саме ці розкиди. Одна гілка на одному зерні дала 0.0123, а на іншому 0.1478: це не модель, а лотерея. ASPP на трьох зернах тримається в межах 0.0135. Кілька масштабів роблять навчання не лише кращим, а й передбачуваним.
1×1 без жодного модуля — та
сама мережа без нього дала 0.2268. Причина не в ASPP, а в розмірі задачі: 160
навчальних сцен замало, щоб окупити три зайві шари. Порівняння «одна гілка проти
ASPP» від цього не псується — обидві мережі отримали однакову надбавку в глибині, і
серед них ASPP виграє впевнено. Але цитувати наше число як «ASPP покращує
сегментацію» не можна: воно каже лише «серед модулів контексту кілька масштабів
кращі за один».ПорівнянняТри способи поруч
Тепер поставимо всі три підходи в одну таблицю. Беремо найгірший випадок із розділу 3 — крок 16, карта 4×4 — і лагодимо його трьома способами. Умови однакові для всіх: 16 каналів, ті самі 160 навчальних сцен, 14 епох, AdamW, три зерна.
Третій рядок підібраний так, щоб порівняння було чесним до кінця. Атрозне тіло має стільки ж шарів, стільки ж параметрів і те саме рецептивне поле (63 пікселі), що й тіло кроку 16. Різниця рівно одна: воно не стискає.
| спосіб | карта перед головою | параметрів | час, с | mIoU (3 зерна) |
|---|---|---|---|---|
| білінійне збільшення | 4×4 | 9 588 | 10.0 | 0.2268 ± 0.0224 |
| ConvTranspose2d, ядро 4 | 4×4 → 64×64 | 26 100 | 19.3 | 0.1673 ± 0.0250 |
| атрозні згортки | 64×64 | 9 588 | 60.1 | 0.4322 ± 0.0389 |
Головне число теми — у третьому рядку. Атрозне тіло дало 0.4322 проти 0.2268 у білінійного при тих самих 9 588 параметрах, тому самому числі шарів і тому самому рецептивному полі 63 пікселі. Виграш 0.2055 — у 9 разів більший за розкид. Єдина різниця між двома мережами в тому, стискаємо ми карту чи ні, і саме вона коштує майже половини метрики. Оце і є DeepLab у мініатюрі.
Тепер ціна. Атрозне тіло навчалося 60.1 с проти 10.0 с — у 6.0 раза довше. Кожна зайва секунда пішла на те, що всі пʼять згорток працюють на карті 64×64 замість 32×32, 16×16, 8×8 і 4×4. Ось компроміс DeepLab у двох числах: маску вдвічі кращу купують проходом у шість разів довшим.
І середній рядок, який теж треба назвати вголос: навчене збільшення програло навіть білінійному, маючи майже втричі більше ваг (26 100 проти 9 588) і вдвічі довший прохід. На цій задачі воно не окупилось жодним чином.
Три способи на одній масці
Перемикач обирає спосіб, повзунок — крок тіла. Ліворуч видно, що вийде з істинної маски після проходу крізь карту такого кроку й повернення назад обраним способом. Праворуч — скільки множень коштує тіло на кадр 64×64.
ConvTranspose2d — її ми міряли навчанням у розділі 6.ВтратаВтрата на дисбалансі
Девʼяносто відсотків фону тиснуть не лише на метрику, а й на саму втрату. Звичайна крос-ентропія рахує середнє по пікселях, тож девʼять із десяти доданків у ній — це фон. Модель отримує найбільшу винагороду за те, щоб добре фарбувати фон, і найменшу — за межі предметів, яких мало.
Два звичні ліки:
- Зважена крос-ентропія. Кожному класу дається вага, обернено пропорційна його частоті. У нас це дає фону вагу 0.280, а трикутникові — 10.507: помилка на трикутнику коштує в тридцять сім разів більше за помилку на фоні.
- Втрата Дайса. Береться формула Dice, зроблена гладкою (замість жорстких міток — імовірності softmax), і мінімізується 1 − Dice. Головна її властивість: вона нормована на площу класу, тому маленький клас важить у ній стільки ж, скільки великий, без жодних ваг.
Замір, три зерна, одна й та сама мережа кроку 8 — міняється тільки втрата:
| втрата | mIoU (3 зерна) | розкид |
|---|---|---|
| крос-ентропія | 0.6151 | ± 0.0279 |
| зважена крос-ентропія | 0.4181 | ± 0.0176 |
| втрата Дайса | 0.6647 | ± 0.0505 |
Результат не той, якого чекаєш від підручника.
Зважування зашкодило. 0.4181 проти 0.6151 — падіння на 0.1970, у сім разів більше за розкид. Причина видно на масках: з вагою 10.507 модель щедро малює трикутники там, де їх немає. Пропущених пікселів меншає, зайвих більшає, а IoU карає і за те, і за те. Зважування зсуває поріг рішення, а не вчить мережу бачити краще.
Втрата Дайса не відрізняється від крос-ентропії. 0.6647 проти 0.6151 — різниця 0.0496 при розкиді 0.0505, тобто її немає. Висновок: на цій задачі вибір втрати не вирішує нічого, а вибір кроку тіла вирішив половину метрики.
МоделіСправжні моделі
Подивимось, як усе це виглядає в бібліотеці. Три готові сегментатори з
torchvision, побудовані без ваг
(weights=None, weights_backbone=None), щоб порахувати саму конструкцію:
| модель | тіло | голова | усього |
|---|---|---|---|
| fcn_resnet50 | 23 508 032 | 9 448 981 | 32 957 013 |
| deeplabv3_resnet50 | 23 508 032 | 16 130 837 | 39 638 869 |
| lraspp_mobilenet_v3_large | 2 971 952 | 249 586 | 3 221 538 |
Перше, що варто побачити: у перших двох моделей однакове тіло — той самий ResNet-50 на 23 508 032 ваги. Уся різниця між FCN і DeepLab сидить у голові: 9 448 981 проти 16 130 837. DeepLab дорожчий рівно на ASPP, і це 6.7 мільйона ваг — більше, ніж уся мережа LR-ASPP разом із тілом.
Друге — розрив у 12.3 раза між DeepLab на ResNet-50 і LR-ASPP на MobileNetV3. Звідки він береться, видно з двох стовпців окремо:
MobileNetV3 замість ResNet-50: 2 971 952 проти 23 508 032, тобто в 7.9 раза легше. Це заслуга не сегментації, а теми 15 — роздільних згорток.
249 586 проти 16 130 837 — у 64 рази легше. LR-ASPP (lite reduced ASPP)
викидає з ASPP усі атрозні гілки й лишає дві: 1×1 і глобальне
усереднення, яке працює як «ворота» — множить перші ознаки на своє значення,
замість склеювати по каналах.
Що важить більше? Перевіряється однією моделлю:
deeplabv3_mobilenet_v3_large має те саме легке тіло (2 971 952),
але важку голову ASPP (8 053 525) — разом 11 025 477. Тобто на легкому
тілі вибір голови міняє модель у 3.4 раза.
Практичний висновок: якщо треба сегментація на телефоні, дивись не лише на тіло. Голова ASPP, яка на ResNet-50 губилась серед двадцяти трьох мільйонів, на MobileNet стає більшою за саму мережу.
ПідсумокПідсумок і місток далі
Семантична сегментація ставить перед мережею просту вимогу: вихід тієї самої форми,
що вхід. Прибрати повнозвʼязні шари й поставити 1×1 легко — це FCN. Далі
починається справжня задача, і вона одна: повернути роздільність, яку тіло
стерло стисканням.
kernel_size, не кратний
stride, дає шахівницю: покриття 1 · 2 · 2 · 4 замість рівного.І ще дві речі, які легко забути, бо вони не про архітектуру. Перша: точність по пікселях не є метрикою — вона дорівнює частці найбільшого класу. Друга: Dice і IoU — різні числа, і що гірша маска, то більший між ними розрив; на перекритті 10 % він дворазовий.
Тепер про межу сьогоднішнього підходу. Атрозна згортка зберігає роздільність чесно — але дорого, бо кожен шар тіла працює на повній карті. У нашому крихітному прикладі це коштувало у 6.0 раза довшим навчанням: 60.1 с проти 10.0 с. На кадрі 512×512 із сотнями каналів той самий множник робить модель непридатною для реального часу.
Тому є другий шлях, протилежний за духом. Не «не стискати», а стиснути, як завжди, і потім повернути деталі з ранніх шарів — тих, які ще памʼятають, де проходила межа, бо вони працювали на повній роздільності. Ідея називається скіп-зʼєднання, а мережа, яка на ній побудована, — U-Net. Це наступна тема, і там на нас чекає несподіванка: скіп-зʼєднання допомагають не завжди, а рівно тоді, коли є що втрачати.