Семантична сегментація

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

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

ПостановкаВід рамки до пікселя

Тема 02 вже називала цю задачу на карті завдань компʼютерного зору й дала їй імʼя: семантична сегментація. Формулювання коротке. Вхід — зображення висотою H і шириною W. Вихід — масив тієї самої висоти й ширини, у якому на місці кожного пікселя стоїть номер класу. Такий масив зветься маскою (mask).

«Семантична» означає: ми питаємо що це, а не котрий це. Три кола на одній картинці дадуть одну спільну область із міткою «коло» — маска не розрізняє, де перше коло, а де друге. Задача «розрізнити окремі предмети» зветься instance segmentation, і їй присвячена тема 31; задача «дати кожному пікселю і клас, і номер предмета» зветься panoptic — це тема 32. Сьогодні ми свідомо беремо найпростішу з трьох і розбираємо її до дна, бо всі решта стоять на ній.

Що змінюється порівняно з детекцією:

Форма виходуНе список рамок змінної довжини, а тензор фіксованої форми H×W×(класів). Ніякого NMS, ніяких якорів, ніякого зіставлення прогнозів з істинами — кожна клітинка виходу вже стоїть навпроти своєї клітинки істини.
ВтратаЗвичайна крос-ентропія, тільки застосована H×W разів замість одного. Задача формально повертається до класифікації — тієї самої, що в блоці 2.
РозміткаНе чотири числа на предмет, а обведений контур. Тема 02 рахувала: близько 10 годин на сотню масок — у десятки разів дорожче за рамки.
МетрикаНе mAP, бо немає впевненості й немає чого впорядковувати за нею. Натомість IoU по класах, усереднене — mIoU.

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

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

ІдеяКласифікація кожного пікселя

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

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

карта ознак 64 канали × 8 × 8 згортка 1×1 64 → 4 логіти 4 канали × 8 × 8 ← клас у кожній клітинці

Це і є головна ідея FCN — fully convolutional network, повністю згорткова мережа. Стаття 2015 року називалась саме так, і вся її новизна вміщається в один абзац: викиньте з класифікатора повнозвʼязні шари, і він почне видавати карту міток замість одного класу. Мережа стає повністю згортковою — у ній не лишається жодного шару, який залежить від розміру входу.

Друга, менш очевидна перевага: така мережа приймає вхід будь-якого розміру. Повнозвʼязний шар привʼязаний до довжини вектора, тому класифікатор вимагає рівно 224×224. Згортці 1×1 байдуже: подай 512×512 — дістанеш більшу карту.

Де в класифікаторі вмирає сітка

Два шляхи від тієї самої карти ознак. Ліворуч — класифікатор із блоку 2, праворуч — та сама мережа з іншою головою. Тіло однакове; уся різниця починається на четвертому кроці. Числа — з нашого прикладу: сцена 64×64, тіло з кроком 8, чотири класи.

Глобальне усереднення — це те місце, де 64 клітинки сітки зливаються в одне число на канал. Після нього відновлювати нема з чого: у векторі з 64 чисел не записано, у якому кутку картинки був предмет. Згортка 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-32s327×732×32 px входу
FCN-16s1614×1416×16 px
FCN-8s828×288×8 px
Що означає «s» у назві. Це 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 сценах одразу, не по одній намальованій.

як повертати розмір
карта8×8
піксель карти8×8 px
mIoU—
точність по пікселях—
Крок 8 лишає від істинної маски mIoU 0.4039, якщо повертати розмір сходинками, і 0.6528, якщо повертати плавно. Точність по пікселях при цьому тримається біля 0.92 і майже нічого не помічає — перший натяк на те, що вона непридатна як метрика. Зверни увагу й на друге: спосіб повернення важить не менше за сам крок. Та сама груба карта дає різницю приблизно в чверть mIoU залежно від того, розтягуєш ти її сходинками чи плавно. Числа цього абзацу заміряні на перевірній вибірці зі 120 сцен; інтерактив рахує на своїх 24, тому показує близькі, але не ті самі значення.

Ті самі числа, пораховані на всіх 120 перевірних сценах, варто виписати окремо — вони задають рамку всій темі. Це міра втрати на самій сітці, ще до будь-якого навчання:

кроккартаmIoU сходинкамиmIoU плавно
232×320.85770.9738
416×160.66310.9081
88×80.40390.6528
164×40.15030.1300
322×20.02950.0013
Це показник, а не доведена стеля. Спокусливо сказати «мережа з кроком 8 не дасть більше за 0.6528». Неправда: навчена мережа з кроком 16 дала 0.2268 (розділ 9), тобто більше за 0.1300 із таблиці. Причина в тому, що таблиця пропускає крізь карту саме істинну маску, а навчена мережа не зобовʼязана видавати в клітинці усереднену істину — вона може видати такі оцінки класів, з яких після розтягування вийде краща межа. Тобто число показує, скільки губить сітка при чесному усередненні, і нічого не забороняє мережі хитрувати.

Тепер поміряймо те саме на справжній мережі. Будуємо просту FCN-подібну модель: тіло з кількох згорткових блоків зі стисканням, голова 1×1, білінійне збільшення логітів до 64×64. Міняємо лише крок тіла:

крок тілакартапараметрівmIoU (3 зерна)час навчання
416×164 9160.6815 ± 0.060612.9 с
88×87 2520.6151 ± 0.027911.8 с
164×49 5880.2268 ± 0.022410.0 с

Перше, що впадає в око, — параметрів більше в гіршої мережі: крок 16 має 9 588 ваг проти 4 916 у кроку 4, бо кожен рівень стискання додає ще один згортковий блок. Стискання купує не якість, а дешевизну обчислень.

Друге: між кроком 4 і кроком 8 різниця лише 0.0664 при розкиді 0.0606 — тобто її фактично немає. А от між кроком 8 і кроком 16 різниця 0.3884, у 14 разів більша за розкид. Це і є момент, коли роздільності перестає вистачати: ціла фігура вміщається в одну клітинку карти 4×4, і мережі нема де сказати, де в цій клітинці межа.

МетрикиМетрики: IoU, mIoU, Dice

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

Чому точність по пікселях безглузда

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

класпікселівчастка
фон442 40390.01 %
квадрат22 4754.57 %
коло14 9493.04 %
трикутник11 6932.38 %

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

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

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

(491 520 − 11 693) / 491 520 = 479 827 / 491 520 = 0.9762

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

Той самий сюжет ми вже бачили. У темі 23 фон займав 85.4 % якорів, і саме звідти виріс focal loss у темі 26. У сегментації дисбаланс ще різкіший, бо він рахується не по якорях, а по пікселях: у нас 90 %, а на вуличних сценах небо й дорога легко дають 95 % і більше.

Точність по пікселях міряє розмір фону, а не модель

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

порожня модель
частка фону—
точність по пікселях—
mIoU—
найбільший клас—
Хоч би як ти крутив повзунок, точність по пікселях моделі «усе фон» дорівнює частці фону — до останнього знака. Метрика не міряє модель, вона міряє датасет. mIoU при цьому стоїть на 0.000 і не рухається: жодного пікселя жодного предмета не вгадано, отже перетин порожній.

IoU по класах і mIoU

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

IoUклас = |прогноз ∩ істина| / |прогноз ∪ істина|

Словами: скільки пікселів ми і мережа назвали цим класом разом, поділити на скільки пікселів назвав цим класом хоч хтось із двох. Символ ∩ — перетин, спільна частина двох множин; ∪ — обʼєднання, усе разом без повторів. Число завжди між 0 і 1: одиниця означає повний збіг, нуль — жодного спільного пікселя.

mIoU (mean IoU) — середнє цих чисел по класах. І тут є питання, від якого залежить сама цифра: чи входить у середнє фон. У нашому курсі — ні, ми усереднюємо по трьох класах предметів. Причина видно на прикладі вище: модель «усе фон» має IoU фону 0.9001 і IoU предметів 0, тож середнє з фоном дало б 0.2250, а без фону — рівно 0.000. Друге число чесніше описує, що модель нікому не потрібна. Обидві угоди трапляються в статтях, тому, читаючи чуже mIoU, завжди перевіряй, які класи в ньому усереднені.

крок 1 · рахуємо руками

Хай істина — квадрат 10×10 пікселів, тобто 100 пікселів класу «квадрат». Мережа підписала квадратом область 12×12 = 144 пікселі, зсунуту на 2 пікселі вправо й на 2 вниз.

крок 2 · перетин

Спільна частина — прямокутник 8×8, бо зсув на 2 зʼїдає по два стовпці й два рядки з боку істини.

|перетин| = 8 × 8 = 64
крок 3 · обʼєднання

Сума площ мінус перетин, щоб спільну частину не порахувати двічі.

|обʼєднання| = 100 + 144 − 64 = 180
крок 4 · відповідь
IoU = 64 / 180 = 0.3556

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

Чому mIoU лікує те, що зламало точність

Повернімось до моделі, яка загубила трикутник, і поміряймо її новою метрикою. IoU квадрата й кола в неї по 1 — вони виділені ідеально. IoU трикутника — рівно 0: жодного його пікселя не названо трикутником, тож перетин порожній.

mIoU = (1 + 1 + 0) / 3 = 0.6667

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

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

Чим Dice відрізняється від IoU

Друга метрика, яку постійно цитують поруч, — коефіцієнт Дайса (Dice coefficient), у медичних статтях його ще звуть F1 по пікселях:

Dice = 2·|прогноз ∩ істина| / (|прогноз| + |істина|)

Різниця з IoU в знаменнику: там було обʼєднання, тут — сума площ, у якій перетин порахований двічі. Через це Dice завжди більший або рівний IoU. Звʼязок між ними точний і не залежить від форми масок:

Dice = 2·IoU / (1 + IoU)

Підставимо наш приклад: IoU = 0.3556, отже Dice = 2·0.3556/1.3556 = 0.5246. Перевіримо прямо за визначенням: 2·64/(100+144) = 128/244 = 0.5246. Збіглося.

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

перекриттяIoUDiceу скільки разів більший
90 %0.81820.90001.10
50 %0.33330.50001.50
10 %0.05260.10001.90

На добрих масках дві метрики майже збігаються — різниця 10 %. На поганих Dice майже вдвічі оптимістичніший: 0.0526 проти 0.1000. Практичний наслідок простий: дві статті на одному датасеті можуть звітувати 0.85 і 0.75, і краща з них — та, що цитує IoU. Дивись, яку метрику назвали, перш ніж порівнювати числа.

IoU і Dice на двох масках, які можна зсувати

Ліворуч — дві однакові маски; повзунок розводить їх. Перетин підсвічено. Праворуч — крива Dice = 2·IoU/(1+IoU) і поточна точка на ній. Обидва числа рахуються прямим підрахунком пікселів, а не за формулою — формула лише звіряється з ними.

форма масок
перекриття—
IoU—
Dice—
Dice / IoU—
Що гірший збіг, то більший розрив. Криву Dice = 2·IoU/(1+IoU) видно праворуч: біля одиниці вона притискається до діагоналі, біля нуля відривається від неї вдвічі. Тобто Dice прикрашає саме там, де тобі найважливіше знати правду — на поганих масках.

Спосіб 1Спосіб перший: просто збільшити

Отже, у нас є карта логітів 8×8, а потрібна маска 64×64. Найпростіше рішення — розтягнути карту інтерполяцією. Ніяких ваг, ніякого навчання: чиста арифметика.

Найгрубіший варіант — найближчий сусід (nearest): кожна клітинка карти стає суцільним квадратом 8×8. Саме його ми міряли в інтерактиві 1 як «сходинками», і саме він дав 0.4039. Межа виходить пилчастою, з кроком у розмір клітинки.

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

крок 1 · де ми між клітинками

Хай вихідний піксель лежить між клітинками карти так, що по горизонталі він відстоїть на 0.25 від лівої, а по вертикалі — на 0.75 від верхньої. Позначимо ці частки u = 0.25 і v = 0.75.

крок 2 · ваги чотирьох сусідів

Вага кожного сусіда — добуток двох «протилежних» часток. Ближчий по горизонталі дістає (1−u), дальший — u; так само по вертикалі.

верх-ліво (1−u)(1−v) = 0.75·0.25 = 0.1875 верх-право u(1−v) = 0.25·0.25 = 0.0625 низ-ліво (1−u)v = 0.75·0.75 = 0.5625 низ-право u·v = 0.25·0.75 = 0.1875

Сума ваг дорівнює одиниці: 0.1875+0.0625+0.5625+0.1875 = 1.0000. Це важливо — інакше інтерполяція міняла б яскравість.

крок 3 · відповідь

Значення пікселя — сума значень чотирьох клітинок, помножених на їхні ваги. Ніяких навчених чисел тут немає взагалі.

Чому це дає розмиту межу. Між двома сусідніми клітинками інтерполяція будує лінійний перехід завширшки в одну клітинку — вісім пікселів при кроці 8. Різкої межі там нема з чого взяти: карта не містить інформації, де саме в цьому проміжку предмет закінчився. Ми не відновлюємо деталь, ми згладжуємо сходинки — і все ж це не дрібниця: 0.4039 проти 0.6528 на тій самій карті кроку 8. Плавне збільшення дає межу з точністю до половини клітинки замість цілої.

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

Спосіб 2Спосіб другий: навчити збільшення

Другий підхід: хай ваги збільшення теж вчаться. Операція зветься транспонована згортка (transposed convolution), у коді — nn.ConvTranspose2d. Трапляється назва «деконволюція», і вона невдала: жодного оберненого перетворення тут немає.

Що вона робить: звичайна згортка бере багато вхідних пікселів і дає один вихідний; транспонована — навпаки. Вона бере один вхідний піксель, множить на нього все ядро й додає цей відбиток у кілька сусідніх вихідних клітинок, потім зсувається на stride і додає наступний. Вихід — сума всіх накладених відбитків.

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

Артефакт шахівниці

Порахуймо, скільки разів кожна вихідна клітинка отримує внесок. Хай ядро має розмір k, а крок дорівнює 2. Відбитки ядра лягають через два, тому клітинка з парним номером і клітинка з непарним номером можуть накритись різну кількість разів.

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

ядро · кроквнесків у 4 позиціяхдисперсіянайбільше / найменше
2 · 21 1 1 10.00001.00
3 · 21 2 2 41.18754.00
4 · 24 4 4 40.00001.00
5 · 24 6 6 93.18752.25
6 · 29 9 9 90.00001.00

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

Звідки береться шахівниця

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

kernel_size при кроці 2
внесків у клітинці 2×2—
дисперсія—
найбільше / найменше—
ділиться на крок—
Ядро 3 при кроці 2 дає покриття 1 · 2 · 2 · 4 — різниця вчетверо між сусідніми пікселями виходу. Ядра 2, 4 і 6 покривають вихід рівно, бо діляться на крок. Це не питання навчання: покриття задане геометрією, і мережа може лише компенсувати його вагами, а не скасувати.

Обидва ядра ми поставили в одну мережу — тіло кроку 16, чотири сходинки збільшення по два — і навчили кожну на трьох зернах:

збільшенняпараметрівmIoU (3 зерна)час, с
ядро 4, крок 226 1000.1673 ± 0.025019.3
ядро 3, крок 218 9320.0602 ± 0.069419.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.

ефективний розмір ядра = d·(k − 1) + 1

Тут k — скільки ваг у ядрі по стороні (у нас 3), d — dilation. При d = 1 виходить звичайна згортка 3×3. При d = 16 ядро охоплює 16·2 + 1 = 33 пікселі по стороні. І ось головне число теми:

dilationефективне ядрополе, пікселівпараметрівмножень на піксель
13×3936 92836 864
25×52536 92836 864
49×98136 92836 864
817×1728936 92836 864
1633×331 08936 92836 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, і окреслене поле, яке воно накриває. Праворуч — два лічильники в часі: рецептивне поле росте квадратично, параметри стоять.

ефективне ядро3×3
поле, пікселів9
параметрів36 928
прочитано з поля100 %
Параметрів рівно 36 928 у всіх положеннях повзунка — від dilation 1 до dilation 16. Але подивись на четверте табло: частка поля, яку ядро насправді читає, падає з 100 % до 0.83 %. Дірки ростуть швидше за поле, і при великому 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 і дивись, як змінюється сумарне поле й скільки пікселів у ньому взагалі прочитано.

набір dilation
гілка глобального усереднення
сумарне поле—
прочитано пікселів—
гілок—
щільність—
Одна гілка з dilation 2 читає 9 пікселів із поля 25. Набір 1 · 2 · 4 читає 25 пікселів із поля 81 — і, головне, дрібні деталі вже не провалюються, бо гілка з dilation 1 дивиться суцільно. Набір 1 · 6 · 12 · 18 — той, що стоїть у справжньому DeepLab, — бачить дуже далеко, але щільність падає до одиниць відсотків: він розрахований на карту кроку 8, де один піксель це вісім.

Замір: те саме тіло кроку 16, та сама голова 1×1, різниця лише в модулі між ними.

модуль контекступараметрівmIoU (3 зерна)час, с
одна гілка 3×3, dilation 212 2120.0746 ± 0.055910.5
ASPP 1 · 2 · 4 + усереднення15 8600.1920 ± 0.005913.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×49 58810.00.2268 ± 0.0224
ConvTranspose2d, ядро 44×4 → 64×6426 10019.30.1673 ± 0.0250
атрозні згортки64×649 58860.10.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.

спосіб
карта перед головою—
mIoU відновлення—
множень у тілі—
дорожче за найдешевше—
Повзунок кроку рухає перші два способи й нічого не робить із третім — у цьому вся суть. Атрозне тіло не має кроку: карта завжди 64×64, тому маска не грубішає ніколи. Плату видно в останніх двох табло: на краю повзунка, при кроці 16, атрозне тіло коштує вдесятеро більше множень за те саме тіло зі стисканням, а при кроці 2 — лише втричі. Що глибше стискання, то дорожча відмова від нього. Одна чесна засторога: навчених ваг у браузері взятися нема звідки, тож режим «навчити збільшення» канва відтворює ланцюжком дворазових кроків без ваг. Він показує геометрію збільшення по два, а не якість справжньої ConvTranspose2d — її ми міряли навчанням у розділі 6.

ВтратаВтрата на дисбалансі

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

Два звичні ліки:

  1. Зважена крос-ентропія. Кожному класу дається вага, обернено пропорційна його частоті. У нас це дає фону вагу 0.280, а трикутникові — 10.507: помилка на трикутнику коштує в тридцять сім разів більше за помилку на фоні.
  2. Втрата Дайса. Береться формула 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_resnet5023 508 0329 448 98132 957 013
deeplabv3_resnet5023 508 03216 130 83739 638 869
lraspp_mobilenet_v3_large2 971 952249 5863 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. Звідки він береться, видно з двох стовпців окремо:

джерело 1 · інше тіло

MobileNetV3 замість ResNet-50: 2 971 952 проти 23 508 032, тобто в 7.9 раза легше. Це заслуга не сегментації, а теми 15 — роздільних згорток.

джерело 2 · інша голова

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

Просто збільшитиНуль ваг, нуль часу. Плавна інтерполяція набагато краща за сходинки (0.6528 проти 0.4039 на кроці 8), але межу вона не відновлює, а згладжує.
Навчити збільшенняВаги вчаться, але kernel_size, не кратний stride, дає шахівницю: покриття 1 · 2 · 2 · 4 замість рівного.
Не стискатиАтрозна згортка купує рецептивне поле задарма: 33×33 при тих самих 36 928 параметрах. Плата — час на нестиснутій карті й ефект решітки.
ASPPКілька dilation паралельно плюс глобальне усереднення. Латає решітку й дає кілька масштабів одразу — ціною найважчої голови серед усіх.

І ще дві речі, які легко забути, бо вони не про архітектуру. Перша: точність по пікселях не є метрикою — вона дорівнює частці найбільшого класу. Друга: Dice і IoU — різні числа, і що гірша маска, то більший між ними розрив; на перекритті 10 % він дворазовий.

Тепер про межу сьогоднішнього підходу. Атрозна згортка зберігає роздільність чесно — але дорого, бо кожен шар тіла працює на повній карті. У нашому крихітному прикладі це коштувало у 6.0 раза довшим навчанням: 60.1 с проти 10.0 с. На кадрі 512×512 із сотнями каналів той самий множник робить модель непридатною для реального часу.

Тому є другий шлях, протилежний за духом. Не «не стискати», а стиснути, як завжди, і потім повернути деталі з ранніх шарів — тих, які ще памʼятають, де проходила межа, бо вони працювали на повній роздільності. Ідея називається скіп-зʼєднання, а мережа, яка на ній побудована, — U-Net. Це наступна тема, і там на нас чекає несподіванка: скіп-зʼєднання допомагають не завжди, а рівно тоді, коли є що втрачати.