Дешевше, а не точніше

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

Дванадцять тем поспіль ми ставили мережі одне питання: наскільки точно вона відповідає. AlexNet і VGG відповіли на нього глибиною й кількістю фільтрів, ResNet — скіп-зʼєднанням, яке зробило глибину керованою. А потім мережі довелося вийти з серверної: розблокувати телефон обличчям, знайти документ у кадрі камери, розмити тло під час відеодзвінка просто в браузері. І виявилось, що питання «наскільки точно» — не єдине, а іноді й не головне.

Ця тема про три архітектурні родини, які виникли з другого питання: Inception (2014), MobileNet (2017) і EfficientNet (2019). Але головне в ній не вони, а те, що зʼясувалося дорогою: «дешевше» — не одне число, а три, і вони посварені між собою.

ОбмеженняМережа має кудись поміститись

Коли модель живе на сервері, її обмеження — гроші й нічого більше. Мало памʼяті — докупи. Повільно — постав другу машину. На пристрої в кишені все інакше, і обмежень стає щонайменше чотири.

ОбмеженняЗвідки беретьсяЩо станеться, якщо порушити
Розмір завантаженняКористувач бачить вагу застосунку в магазині Не встановить. Ваги VGG16 — 553 МБ
Оперативна памʼятьТелефон дає застосунку сотні мегабайтів на все: і ваги, і проміжні карти ознакСистема вбʼє застосунок без пояснень
ЗатримкаКамера дає 30 кадрів на секунду — 33 мс на кадр Картинка смикається. Це дратує сильніше за кілька відсотків точності
Батарея й нагрівКожне множення — це заряд, а гарячий телефон сам знижує частотуМережа вклалась у бюджет на холодному телефоні й вилетіла з нього через хвилину

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

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

Три числаТри різні «дешевше»

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

Величина 1 · Параметри

Скільки чисел треба зберігати. Це вага файлу з моделлю: кожне число в звичайному форматі float32 займає 4 байти. Мільйон параметрів — 4 МБ.

розмір файлу ≈ параметрів × 4 байти

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

Величина 2 · Обчислення

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

множень = cout × Hout × Wout × cin × k × k

Словами: кожен вихідний піксель кожного вихідного каналу — це сума добутків по вікну k×k у всіх вхідних каналах. Тут cin і cout — канали на вході й на виході, H і W — розміри вихідної карти, k — сторона ядра. Це число залежить від розміру входу, росте разом із ним квадратично й приблизно відповідає за витрату батареї.

Величина 3 · Час

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

Легко припустити, що всі три впорядковують архітектури однаково: менше параметрів — менше множень — менше часу. Це припущення хибне. Ось девʼять справжніх архітектур, зібраних без ваг (нам потрібна форма, а не вміння), один потік процесора, без відеокарти; час — найкращий із тридцяти прогонів.

Архітектурапараметрівваги, МБмножень, млнчас, мс
AlexNet61 100 84024471437.9
VGG16138 357 54455315 470448.0
GoogLeNet13 004 888521 49889.0
ResNet1811 689 512471 81469.0
ResNet5025 557 0321024 089159.3
Inception-v327 161 2641095 713235.5
EfficientNet-B05 288 5482138642.5
MobileNet-v23 504 8721430132.4
MobileNet-v3-small2 542 856105611.0
Колонка часу — про цю машину, а не про всі. Параметри й множення однакові в кожного, хто повторить замір: це арифметика форми. Час — ні. Він залежить від процесора, від версії бібліотеки й навіть від того, що ще крутиться на машині в цю хвилину; наші заміри робились на завантаженому чотириядерному процесорі й від прогону до прогону гуляли на 20-30 %. Тому в цій темі важливі не самі мілісекунди, а порядок: він стійкий, і саме він суперечить порядку за параметрами. На іншому процесорі всі мілісекунди в цій таблиці будуть іншими — а те, хто кого обганяє, лишиться тим самим.

Від VGG16 до MobileNet-v3-small — 54 рази за параметрами. Але подивись на AlexNet. За параметрами він другий із девʼяти: 61 мільйон, більше за ResNet50 і Inception-v3 разом узяті. За часом він — сьомий, тобто третій із кінця. Ті самі девʼять мереж, два різні порядки.

Три «дешевше» не збігаються

Кожен рядок — архітектура, кожна колонка — своя міра вартості. Кнопки впорядковують рядки за обраною мірою. Цифра в кружечку — місце в цій колонці.

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

AlexNet: багато ваг, мало роботи. Повнозвʼязний шар на 4096 нейронів має 4096 × 9216 ≈ 37 мільйонів ваг — і робить рівно 37 мільйонів множень. Одна вага — одне множення, і все. Згортковий шар поводиться протилежно: ядро 3×3 на 256 каналів має 590 тисяч ваг, але прикладається до кожного з 28 × 28 положень, тобто робить 462 мільйони множень. Одна вага — сотні множень. Тому мережа з головою на мільйони параметрів важить багато й рахує мало, а мережа з тілом на ті самі мільйони важить мало й рахує довго. Це той самий переїзд ваг з голови в тіло, який тема 14 показала на ResNet, — тільки тепер ми дивимось на нього з боку вартості.

EfficientNet-B0 проти AlexNet: менше всього — а часу стільки ж. У EfficientNet-B0 в 11.6 раза менше параметрів і в 1.85 раза менше множень, ніж в AlexNet. За часом вони практично рівні: 42.5 мс проти 37.9. Тобто зменшили обидва числа, які вміємо рахувати, — і не виграли нічого в тому єдиному, що відчуває користувач.

Це не помилка заміру, а головна незручна правда теми. Повернемось до неї в розділі 06.

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

InceptionНе обирати розмір ядра

Повернімось у 2014 рік. Проєктувальник мережі мусить для кожного шару відповісти на питання, на яке нема відповіді: яке брати ядро — 1×1, 3×3 чи 5×5?

Питання не дозвільне. Ядро 3×3 бачить дрібну деталь: край, кут, штрих. Ядро 5×5 — більший фрагмент: вигин, кільце, фактуру. А предмети бувають різного розміру навіть в одному кадрі: кіт на передньому плані й кіт удалині займають різну кількість пікселів, і те, що для першого «дрібна деталь», для другого вже цілий кіт.

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

Автори GoogLeNet відповіли інакше: не обирати. Пропустити вхід через усі варіанти одразу й скласти результати поруч.

вхід: 192 канали, карта 28×28
   │
   ├── 1×1 ────────────────→  64 канали
   ├── 3×3, padding 1 ─────→ 128 каналів
   ├── 5×5, padding 2 ─────→  32 канали
   └── maxpool 3×3 → 1×1 ──→  32 канали
   │
   torch.cat по каналах
   ↓
вихід: 256 каналів, карта 28×28

Це і є модуль Inception (Inception module). Чотири гілки рахуються незалежно від одного й того самого входу, а потім їхні виходи склеюються по каналах — операцією torch.cat(..., dim=1).

«Склеюються по каналах» варто розкрити. Кожна гілка дає карту тієї самої висоти й ширини — 28×28, це забезпечено підбором padding; різняться вони лише кількістю каналів. Склеювання ставить ці стоси один на одного: 64 + 128 + 32 + 32 = 256 каналів тієї самої карти. Нічого не додається й не усереднюється — результати гілок лежать поруч, і наступний шар сам вирішить, на які з них дивитись.

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

Гілка з пулінгом стоїть там із тієї самої логіки: пулінг із теми 08 — теж спосіб подивитись на околицю, тож нехай і він буде одним із варіантів.

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

У щойно намальованому модулі є проблема, і вона арифметична. Порахуймо гілку 5×5 чесно, за формулою параметрів згорткового шару з теми 07:

параметрів = cin × cout × k × k + cout

Підставляємо: вхід 192 канали, вихід 32 канали, ядро 5×5.

192 × 32 × 25 + 32 = 153 600 + 32 = 153 632

Сто пʼятдесят три тисячі параметрів на одну гілку, яка дає всього 32 канали з 256. Гілка 3×3 із її 128 каналами коштує ще більше: 192 × 128 × 9 + 128 = 221 312. Разом модуль важить 393 472 параметри. А таких модулів у GoogLeNet девʼять, і кожен наступний працює з більшою кількістю каналів.

Джерело витрати видно з формули: перший множник — cin, тобто 192. Дороге не саме ядро 5×5, а те, що воно мусить дивитись на всі 192 вхідні канали одночасно.

Рішення — те, що ми вже вміємо. Згортка 1×1 з теми 07 не дивиться на сусідів у просторі взагалі: вона бере один піксель і змішує його канали. Тому вона дешева. Поставимо її перед дорогим ядром — щоб стиснути 192 канали до 16, а вже потім прикласти 5×5.

Крок 1 · стиснути
192 → 16 через 1×1: 192 × 16 × 1 × 1 + 16 = 3 088

Згортка 1×1 з 16 фільтрами. Кожен фільтр — це 192 числа: скільки взяти від кожного вхідного каналу. Ядро 1×1 означає «дивись лише на цей піксель», тож множник k×k дорівнює одиниці й зникає.

Крок 2 · тепер дороге ядро дешеве
16 → 32 через 5×5: 16 × 32 × 25 + 32 = 12 832

Те саме ядро 5×5, той самий вихід у 32 канали. Але вхід тепер не 192 канали, а 16, і перший множник у формулі впав у дванадцять разів.

Крок 3 · разом
3 088 + 12 832 = 15 920  проти  153 632

Гілка подешевшала в 9.6 раза. Обидві частини разом коштують майже вдесятеро менше, ніж прямий шлях.

Ця стисла проміжна ділянка й зветься пляшковим горлом (bottleneck): широкий вхід, вузьке місце посередині, широкий вихід. Те саме зробимо з гілкою 3×3 — стиснемо 192 канали до 96, — і порахуємо модуль повністю.

Гілкабез горлаз горлом 1×1різниця
1×1 → 6412 35212 352—
3×3 → 128221 312129 2481.7×
5×5 → 32153 63215 9209.6×
pool → 326 1766 176—
модуль цілком393 472163 6962.4×

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

Модуль Inception і його горло

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

гілки 3×3 і 5×5
параметрів модуля—
інший варіант—
економія горла—
Горло виправдане не завжди. Прокрути повзунок до лівого краю, 64 канали: економія не просто зникає — модуль із горлом стає важчим за модуль без нього, 137 072 параметри проти 131 328. Стискати нема чого, а зайвий шар 1×1 усе одно треба оплатити; беззбитковість настає близько 67 каналів. Виграш росте разом із шириною входу, і саме тому горло стоїть у глибоких шарах, де каналів багато, а не на початку мережі.

Тепер зрозуміло, чому модуль Inception узагалі можна собі дозволити. Горло — не прикраса до ідеї паралельних гілок, а умова її існування.

Дрібниця, яку варто знати. Якщо зібрати GoogLeNet у torchvision, вийде 13 004 888 параметрів. Але майже половина з них — допоміжні класифікатори: дві додаткові голови, приліплені до середини мережі, щоб градієнт доходив до перших шарів. Це той самий біль, який тема 14 розвʼязала скіп-зʼєднанням, тільки розвʼязаний милицею. Працюють вони лише під час навчання, а в готовій моделі просто лежать без діла: зібрана без них — googlenet(aux_logits=False) — та сама мережа має 6 624 904 параметри. Коли цитуєш розмір моделі, дивись, який саме з двох чисел ти цитуєш.

РозділитиРоздільна згортка по глибині

Горло 1×1 — напівзахід: воно зменшує кількість каналів перед дорогим ядром, але саме ядро й далі робить дві справи одночасно. MobileNet запитав: а навіщо взагалі дві справи в одній операції?

Придивімось до звичайної згортки 3×3 із 64 каналів у 128. Кожен із 128 вихідних каналів — це сума по всіх 64 вхідних каналах, і в кожному з них ще й сума по вікну 3×3. Тобто одна операція відповідає на два різні питання:

Ідея MobileNet: розділити ці два питання на дві окремі операції. Спершу тільки «де», окремо в кожному каналі. Потім тільки «що», без огляду на сусідів. Результат зветься роздільною згорткою по глибині (depthwise separable convolution).

Крок 1 · тільки «де»

Згортка по глибині (depthwise convolution). Кожен вхідний канал отримує своє власне ядро 3×3 і опрацьовується окремо від решти. Канали між собою не перемішуються взагалі: було 64 канали — стало 64 канали.

параметрів = cin × k × k + cin = 64 × 9 + 64 = 640

У PyTorch це не окремий шар, а звичайний nn.Conv2d з аргументом groups: nn.Conv2d(64, 64, 3, padding=1, groups=64). Аргумент groups ділить канали на групи, які не бачать одна одну; коли груп рівно стільки, скільки каналів, кожен канал лишається наодинці.

Крок 2 · тільки «що»

Точкова згортка (pointwise convolution) — це вже знайома нам згортка 1×1. Вона бере кожен піксель окремо й змішує його 64 канали у 128 нових. Просторових сусідів вона не бачить зовсім — цю роботу вже зробив крок 1.

параметрів = cin × cout + cout = 64 × 128 + 128 = 8 320
Крок 3 · рахунок
звичайна: 64 × 128 × 9 + 128 = 73 856 роздільна: 640 + 8 320 = 8 960 73 856 ÷ 8 960 = 8.24

Той самий вхід, той самий вихід, те саме вікно 3×3 — і в 8.24 раза менше параметрів.

Ми нічого не відкинули: обидві операції разом і далі дивляться на вікно 3×3 в усіх каналах. Змінилось інше — замість 64 × 128 = 8192 окремих ядер 3×3 (по одному на кожну пару «вхідний канал → вихідний канал») тепер є 64 ядра 3×3 плюс таблиця змішування 64 × 128. Добуток розпався на суму.

Ось як економія росте з кількістю каналів:

вхід → вихідзвичайнароздільнарізниця
32 → 6418 4962 4327.61×
64 → 12873 8568 9608.24×
128 → 256295 16834 3048.60×
256 → 5121 180 160134 1448.80×

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

cin·cout·k² ÷ (cin·k² + cin·cout) = 1 ÷ (1/k² + 1/cout)

Прочитаймо це словами. Скоротилось cin — кількість вхідних каналів на відношення не впливає взагалі. Лишились дві величини: розмір ядра й кількість вихідних каналів. Коли вихідних каналів багато, доданок 1/cout стає мізерним, і відношення прямує до k² = 9. Для ядра 5×5 межа була б 25.

Перевіримо на числах: при k=3 і cout=128 маємо 1 ÷ (0.1111 + 0.0078) = 8.41; у таблиці стоїть 8.24, і різниця — це саме ті зсуви, які ми відкинули.

Звичайна проти роздільної

Дві схеми однієї операції. Покрути повзунки й подивись, який із них насправді керує економією, а який ні.

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

РозривЧому економія в 9 разів не дає прискорення в 9 разів

Ми щойно скоротили параметри у 8.24 раза; множення скорочуються приблизно так само — формула та сама, лише помножена на розмір карти. Здається, що й час має впасти у вісім разів. Заміряймо.

Вхід 64 канали, карта 56×56, один шар, один потік процесора:

операція 64→128параметрівчас, мс
звичайна 3×373 8566.05
роздільна8 9601.36
різниця8.24×4.45×

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

мережа на фігурахпараметрівмноженьпрохід, мсточність
звичайні згортки23 7981 919 61613.020.973
роздільні3 846356 32012.030.972
різниця6.19×5.39×1.08×−0.001

Обіцяно проти отриманого

Ті самі два заміри, поставлені поруч. Ліворуч від кожної пари — у скільки разів менше стало того, що ми рахуємо; праворуч — у скільки разів менше стало того, що користувач відчуває.

Обидві пари розходяться, і друга — до нерозрізненності. На одному шарі 8.24 раза за параметрами перетворюються на 4.45 за часом — майже вдвічі менше. На цілій мережі 6.19 раза за параметрами й 5.39 за множеннями дають 1.08 за часом, тобто вісім відсотків — менше за 20-30 % розкиду, з яким гуляють наші заміри часу на завантаженому процесорі. Чесно це читається так: часу не зекономлено нічого, і саме цей нуль треба пояснити.

Параметрів у шість разів менше, множень у пʼять разів менше, точність та сама — а часу заощаджено вісім відсотків, і це менше за розкид самого заміру. Чому?

Причина в тому, що процесор не витрачає час на множення. Він витрачає його на очікування чисел із памʼяті.

Причина 1 · арифметика дешевша за памʼять

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

Причина 2 · згортка по глибині читає багато, а рахує мало

Звичайна згортка 64→128 читає вхідну карту й на кожен її піксель робить 128 × 9 = 1152 множення. Згортка по глибині читає рівно ту саму карту, а робить на піксель 9 множень. Читання те саме, роботи в 128 разів менше — і час впирається не в роботу, а в читання.

Причина 3 · роздільна згортка погано векторизується

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

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

Архітектурамножень, млнчас, мсмлн множень / мс
VGG1615 470448.034.5
ResNet181 81469.026.3
ResNet504 089159.325.7
Inception-v35 713235.524.3
AlexNet71437.918.8
GoogLeNet1 49889.016.8
MobileNet-v230132.49.3
EfficientNet-B038642.59.1
MobileNet-v3-small5611.05.1

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

Практичний висновок, який коштує людям тижнів. Кількість множень (у статтях її звуть FLOPs — floating point operations, операції з дробовими числами, або MACs — multiply-accumulate, «помножити й додати»; на одну MAC припадає дві FLOP) — не міра швидкості. Це міра роботи. Дві мережі з однаковою кількістю множень легко відрізняються часом удвічі. Тому будь-яка заява «наша модель у 5 разів ефективніша» без слова «заміряно на такому-то залізі» нічого не означає. Міряй час, а не FLOPs.

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

Чи означає це, що роздільна згортка марна? Ні. По-перше, вона все ж зменшує вагу файлу — а це окреме обмеження, яке нікуди не поділося. По-друге, на мобільних процесорах і спеціалізованих прискорювачах, під які MobileNet і проєктувався, співвідношення інше: там памʼять і арифметика збалансовані не так, як на настільному процесорі. По-третє, зекономлені параметри можна витратити на глибину — що MobileNet і робить. Але обіцянки «в девʼять разів швидше» ця операція не дає ніде.

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

Вхід росте — що росте разом із ним

Одна й та сама мережа на 23 798 параметрів, вісім розмірів входу. Повзунок обирає розмір; усе показано відносно 28×28.

параметрів23 798
множень—
множень, разів—
часу, разів—
Дві криві розходяться. Від 28×28 до 56×56 множень стає рівно вчетверо більше — сторона подвоїлась, площа зросла в чотири рази. Часу ж стає більше лише в 2.25 раза. Причина та сама: на маленькому вході процесор простоює в очікуванні даних, і, отримавши більше роботи, він виконує її «безкоштовно», доки не впреться в справжню межу.

РучкиДві ручки MobileNet

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

Вони визнали, що не знають, який бюджет буде в користувача. Тому замість однієї моделі випустили сімейство й дали дві ручки, якими користувач сам рухається по кривій «точність проти вартості».

Ручка перша: множник ширини

Множник ширини (width multiplier), позначається α — це просто число, на яке множиться кількість каналів у кожному шарі. Було 16, 32, 64 канали — при α = 0.5 стане 8, 16, 32.

Параметри згорткового шару — це cin × cout × k², і множник зменшує обидва множники одразу. Отже:

параметрів ≈ α² × від початкових

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

множникканалипараметрівмноженьпрохід, мсточність
0.254 / 8 / 161 634141 2163.740.853
0.508 / 16 / 326 142508 2246.600.962
0.7512 / 24 / 4813 5301 101 02410.090.959
1.0016 / 32 / 6423 7981 919 61613.020.973

Від 1.00 до 0.50 параметрів стало вчетверо менше, а точність упала на 1.1 пункту: з 0.973 до 0.962. Виглядає як безкоштовна економія — але наскільки стійкі ці числа?

Ми навчили ту саму мережу при α = 0.5 ще двічі, змінивши лише випадкове зерно: 0.962, 0.935, 0.978. Розкид — 4.3 пункту, тобто вчетверо більший за різницю між α = 0.5 і α = 1.0.

Що це означає. На нашій задачі різницю між половинною й повною шириною не видно: вона тоне у випадковості навчання. Твердження «0.5 гірше за 1.0 на один пункт» ми зробити не можемо — можемо лише сказати, що різниця, якщо вона є, менша за розкид. А от падіння при α = 0.25 до 0.853 — це вже 12 пунктів, утричі більше за розкид, і воно справжнє.

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

Множник ширини

Чотири заміряні положення. Смуга похибки на точності — це розкид між трьома зернами при α = 0.5.

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

Ручка друга: множник роздільності

Множник роздільності (resolution multiplier), позначається ρ — це просто менше вхідне зображення. Замість 224×224 подати 192×192, або 160×160, або 128×128.

Діє вона зовсім інакше за першу:

множник ширини αмножник роздільності ρ
Параметрипадають як α²не змінюються зовсім
Множенняпадають як α²падають як ρ²
Вага файлупадаєне змінюється
Що втрачаємоздатність розрізняти багато різних ознакдрібні деталі, які просто зникли з пікселів

Отже ручки не взаємозамінні: вагу завантаження зменшує тільки α, а час і батарею — обидві. На нашій задачі та сама мережа на 23 798 параметрів при вході 20×20 замість 28×28 робить 979 584 множення замість 1 919 616 (удвічі менше), витрачає 7.13 мс замість 13.02, а точність дає 0.957 проти 0.973. Параметрів, як і обіцяно, рівно стільки ж.

РазомEfficientNet: масштабувати треба все разом

До 2019 року в інженера було три способи зробити мережу більшою, і кожен використовували поодинці:

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

Звідси складене масштабування (compound scaling): рости треба за всіма трьома осями одночасно й у сталій пропорції. Пропорція задається одним числом — коефіцієнтом φ, який каже, у скільки разів більший бюджет ти готовий витратити:

глибина d = αφ ширина w = βφ роздільність r = γφ

Тут α, β, γ — три сталі, свої для родини архітектур, а φ — єдина ручка, яку крутить користувач. Запис αφ означає «α, помножене саме на себе φ разів». Самі сталі не довільні — вони повʼязані бюджетом, і ось як кожна вісь впливає на кількість множень:

Глибина — лінійно

Удвічі більше шарів — удвічі більше роботи: вартість × d.

Ширина — у квадраті

У формулі множень канали стоять двічі: cin і cout. Подвоїти ширину — це вчетверо більше роботи: вартість × w².

Роздільність — теж у квадраті

Подвоїти сторону — це вчетверо більше пікселів, і кожен опрацьовується окремо: вартість × r².

Разом
вартість ≈ d × w² × r²

Тому й обирають сталі так, щоб α × β² × γ² ≈ 2: тоді кожен крок φ на одиницю подвоює вартість, і бюджет рахується в голові.

Значення, які автори EfficientNet опублікували: α = 1.2, β = 1.1, γ = 1.15. Перевіримо множення: 1.2 × 1.1² × 1.15² = 1.2 × 1.21 × 1.3225 = 1.92 — справді близько до двійки.

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

Один бюджет, три осі

Три повзунки й спільний бюджет обчислень. Кнопка ставить пропорцію EfficientNet при поточному бюджеті.

вартість, разів—
бюджет—
Бюджет один, розподілів багато. Восьмикратний бюджет можна витратити на вісім разів глибшу мережу, або на 2.83 раза ширшу, або на 2.83 раза більший вхід — або потроху на все. Арифметика вартості тут наша й точна; а от те, що збалансований розподіл дає вищу точність за однакової вартості, — це результат авторів EfficientNet, і перевірити його на нашій задачі ми не можемо: вона надто легка, щоб побачити різницю.

ПошукАрхітектуру можна не вигадувати, а шукати

Складене масштабування каже, як збільшувати мережу. Але від чого відштовхуватись? У EfficientNet є базова модель B0, з якої розгортаються B1…B7. Звідки взялась сама B0?

Її не вигадала людина. Її знайшла програма — методом, що зветься пошук архітектур (neural architecture search, NAS): описати простір можливих мереж (скільки блоків, які ядра, де роздільні згортки), а далі перебирати варіанти, навчати кожен і залишати кращі.

Перше, чого не варто недооцінювати: це дорого. Не «треба потужний компʼютер», а сотні й тисячі відеокарто-днів на один пошук. Беручи готову EfficientNet, ти користуєшся результатом обчислення, яке не повториш.

Друге: критерій пошуку вирішує все. MobileNet-v3 шукали не за кількістю множень, а за заміряним часом на реальному телефоні — і саме тому вона вийшла іншою, ніж мережі, підібрані за FLOPs. Це той самий висновок розділу 06, тільки зроблений авторами архітектури: оптимізуєш не ту величину — отримаєш не ту мережу.

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

ВибірЩо обирати під задачу

Питання «яка архітектура найкраща» відповіді не має. Питання «яка вкладається в мій бюджет» — має завжди.

Куди ставимоЩо тисне насправдіЗ чого починати
Сервер, пакетна обробка Практично нічого: памʼять є, час є Найточніше, що витримає бюджет грошей: ResNet50, Inception-v3
Сервер, відповідь на запит Затримка на один запит; вага файлу не тисне ResNet18 або EfficientNet-B0, і обовʼязково з заміром часу
Телефон, кадр із камери Усе одразу: вага, памʼять, 33 мс, нагрів MobileNet-v3 із множником ширини під свій бюджет
Браузер Розмір завантаження понад усе MobileNet-v3-small — 10 МБ проти 553 МБ у VGG16

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

Хто вкладається в бюджет

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

сценарій
проходять—
найбільша з них—
Бюджет відсікає, а не ранжує. Точки — це наші заміри ваги файлу й часу; зафарбований прямокутник — дозволена область. Зверни увагу, що при жорсткому бюджеті часу відсіюються не найважчі мережі, а найглибші: Inception-v3 вилітає раніше за AlexNet, хоч важить у 2.2 раза менше (108.6 МБ проти 244.4).

ПідсумокЩо лишається в голові

Ця тема не додала жодного нового способу зробити мережу точнішою. Вона додала інше — уміння називати ціну.

ІдеяЩо вона насправді дає
Три різні «дешевше»Параметри, множення й час не збігаються навіть у порядку: AlexNet другий за параметрами й сьомий за часом
Модуль InceptionНе обирати розмір ядра, а взяти всі й склеїти по каналах
Пляшкове горло 1×1Стиснути канали перед дорогим ядром: 2.4 раза — але лише коли каналів багато; на 64 каналах горло шкодить
Роздільна згорткаРозділити «де» і «що». Параметрів менше в 8-9 разів, межа економії дорівнює k²
Множники ширини й роздільностіДві ручки на різні обмеження; одна одну не замінюють
Складене масштабуванняРости за трьома осями разом. Пропорція знайдена пошуком, а не виведена

І один висновок, важливіший за всі шість. Економія параметрів — не економія часу. Ми бачили це тричі: роздільна згортка дала 8.24× за параметрами й 4.45× за часом на одному шарі; 6.19× за параметрами й 1.08× за часом на нашій мережі; а мережі на роздільних згортках використовують процесор у три-сім разів гірше за грубу VGG16. Кількість множень — міра роботи, а не швидкості, і між ними стоїть памʼять.

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

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

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

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