Оголошення на дошці: фото телефона, а на фото — цінник. Щоб дошка могла шукати за ціною, комусь треба перетворити ділянку пікселів на рядок «4500 грн». Задача звучить дрібно поруч із сегментацією, з якої ми щойно вийшли: там модель відповідала за кожен піксель окремо, тут вона має видати десяток символів. Але саме ця дрібна задача ламає всі звички, набуті в перших шести блоках, — і ламає їх у першому ж реченні постановки.
ПостановкаОдне слово, дві задачі
Слово OCR (optical character recognition, розпізнавання тексту) означає не одну задачу, а дві, поставлені одна за одною:
| Крок | Питання | Вихід | На що схоже з курсу |
|---|---|---|---|
| детекція тексту | де на знімку рядки? | список рамок | детекція з блоку 5 |
| розпізнавання рядка | що написано в цій рамці? | рядок символів | нічого з попередніх блоків |
Перший крок ми фактично вже вміємо. Рамка навколо рядка тексту — це та сама рамка,
що навколо машини в темі 23, тільки
предмет інший: довгий, тонкий і без сталого співвідношення сторін. Метрики ті самі —
IoU, mAP, NMS.
Другий крок — новий цілком. Досі кожна наша модель віддавала або одне число (клас), або масив сталого розміру (карта ознак, маска). Тут вихід — послідовність невідомої довжини: «4500 грн» це вісім символів, «Продам телефон терміново» — двадцять чотири, і мережа мусить сама вирішити, скільки їх.
Чому ці два кроки не зливають в один? Спокуса є: одна мережа, яка дивиться на фото й видає весь текст. Такі моделі існують, але майже всі промислові системи досі розділені, і на це є проста причина, яку ми зараз заміряємо: розпізнавач уміє читати рівно один рядок, і вся його точність тримається на тому, що детектор дав йому саме один рядок і саме цілком.
СегментаціяЧому не можна різати рядок на літери
Найочевидніший план такий: розріж рядок на символи, а далі класифікуй кожен символ окремо. Класифікація дрібних картинок — задача з блоку 2, вона розвʼязана.
План помирає на першому кроці, і помирає красиво. Щоб розрізати рядок на символи, треба вже знати, де вони — а це і є розпізнавання, тільки складніше: тепер модель має не просто назвати літери, а ще й знайти межі між ними.
Здається, що межі видно й так: між літерами є проміжок. Перевіримо це правило — «порожній стовпець пікселів розділяє символи» — на трьохстах рядках із трьох-пʼяти цифр.
| проміжок між цифрами | рядків поділено правильно | шматків на символ |
|---|---|---|
| завжди 1 px | 1.0000 | 1.0000 |
| 1 або 2 px | 1.0000 | 1.0000 |
| 0, 1 або 2 px | 0.3933 | 0.7935 |
| 0 або 1 px | 0.2500 | 0.6990 |
Доки між цифрами лишається хоч один порожній стовпець, правило працює ідеально — і саме тому воно так довго здавалося робочим. Досить проміжкам іноді зникати, і правильно поділених рядків лишається 39.33 %.
Заміряймо це точніше. Візьмімо всі сто пар цифр і поставмо їх упритул, без жодного проміжку: 81 пара зі 100 злипається в один звʼязний шматок. Вижили рівно девʼятнадцять — усі ті, де є одиниця. У нашому шрифті одиниця вузька, її крайні стовпці порожні, тож вона сама створює проміжок там, де його не заклали.
Порожній стовпець — не межа символу
Згори — рядок цифр растровим шрифтом 5×7, бірюзовим обведено шматки, які знайшло наївне правило. Під ним смуга «чорнило в стовпці»: рожеве — стовпець не порожній, отже різати тут не можна.
Друга поломка того самого правила зовсім інша й тому цінна. Досить кільком випадковим пікселям упасти на папір між цифрами — і порожніх стовпців не лишається взагалі, рядок стає одним шматком. Тобто наївна сегментація ламається двома різними способами: від злиття символів і від зникнення проміжків.
Висновок, який визначає всю решту теми: сегментацію на символи треба не покращувати, а прибирати. Модель має читати рядок цілком і сама вирішувати, де закінчується один символ і починається інший — не окремим кроком, а всередині відповіді.
РамкаРамка вирішує, що прочитається
Перш ніж будувати модель, варто побачити, що з нею робить перший крок. Зберемо найпростіший шаблонний читач — рівно те, що робив класичний рушій до появи мереж:
- узяти рамку;
- знайти в ній смужку з чорнилом і звести її до сталої висоти сім пікселів;
- порізати по порожніх стовпцях;
- кожен шматок звести до 5×7 і порівняти з десятьма шаблонами.
Крок другий тут головний, і він є в кожному рушії, зокрема й у нейромережевому: модель має сталий вхід, тож смужку хоч-не-хоч зводять до однієї висоти. Саме через нього рамка перестає бути «дрібницею розмітки».
| рамка навколо рядка «4500» | прочитано | CER |
|---|---|---|
| точно по рядку | 4500 | 0.0000 |
| із запасом 2 px | 4500 | 0.0000 |
| із запасом 4 px | 4500 | 0.0000 |
| зачепила другий рядок | 7142991 | 1.5000 |
| обидва рядки разом | 70400 | 0.7500 |
| зрізано верх 2 px | 4000 | 0.2500 |
| зрізано низ 2 px | 4000 | 0.2500 |
| зрізано правий край | 4508 | 0.2500 |
Три речі варто прочитати з цієї таблиці окремо.
Зайві порожні пікселі навколо рядка не коштують нічого: читач сам знаходить смужку з чорнилом. Тому вимога «рамка точно по буквах» — міф, і детектор, який трохи завеликий, нікому не заважає.
Половина рамки — половина рядка. Це лінійна, передбачувана, майже нецікава поломка: скільки символів випало з рамки, стільки й не прочиталось.
А ось це поломка іншого роду. Смужку зводять до сталої висоти, і два рядки, стиснуті в сім пікселів, не схожі ні на що. Помилка не пропорційна: рамка більша на кілька пікселів — і читається сміття. CER тут 1.5000, тобто більший за одиницю: модель не просто помилилась, вона дописала зайвого.
Рамка й те, що з неї виходить
Ліворуч — знімок із двома рядками цін і рамка, яку ти рухаєш. Праворуч — смужка, зведена до сталої висоти сім пікселів, і те, що з неї прочиталося.
Те саме, тільки різкіше, дає промисловий рушій. Якщо покласти в одну рамку два
рядки й сказати йому --psm 7, тобто «тут рівно один рядок», він поверне
одну літеру «я» і CER рівно 1.0000. Той самий знімок із
--psm 6, тобто «тут блок тексту», читається без жодної помилки.
Різниця не в моделі й не в картинці — різниця в тому, що йому сказали про рамку.
І ще різкіше — якщо розрізати рядок навпіл горизонтально: верхня половина дає CER 1.0870, нижня 0.9130. Порівняй це з вертикальним розрізом, який відрізає рівно стільки символів, скільки випало з рамки. Розпізнавач рядка читає вздовж рядка; поперечний розріз він переживає, поздовжній — ні.
Отже відповідь на питання «навіщо два кроки, а не один»: тому що ці два кроки
ламаються по-різному й вимірюються по-різному. Детекцію міряють через
IoU — і рамка з IoU 0.85 вважається доброю. Але
IoU 0.85 у високої тонкої рамки означає кілька зайвих пікселів по
висоті, а це рівно та поломка, що дає CER 1.5000. Метрика першого кроку не
бачить того, що вбиває другий.
МетрикаЧим міряти: відстань Левенштейна
Досі всі наші метрики порівнювали речі однакового розміру: маску з маскою, рамку з рамкою. Тут треба порівняти два рядки різної довжини, і звична «частка збігів по позиціях» одразу бреше: досить моделі пропустити першу літеру, і всі решта позиції зсунуться, хоч текст майже правильний.
Правильна відповідь — відстань Левенштейна (Levenshtein distance): найменша кількість дій, потрібних, щоб перетворити один рядок на інший. Дій рівно три, і всі вони коштують одиницю:
Порахуймо руками. Істина «телефон», модель прочитала «тепефон». Перші дві літери збігаються, третя різна: «л» проти «п». Одна заміна — і рядки однакові. Відстань 1.
Складніше: істина «телефон», модель прочитала «телефзон». Тут нічого замінювати не треба — досить викинути зайву «з». Знову відстань 1, хоча по позиціях збігається лише пʼять символів із семи: усе після «ф» зсунулось на одну клітинку. Саме це й робить відстань Левенштейна правильною метрикою: вона розуміє, що вставка зсуває все решта, і не карає за зсув двічі.
Рахують її таблицею. У клітинці (i, j) стоїть відстань між першими i символами істини й першими j символами прогнозу. Кожна клітинка — мінімум із трьох сусідів плюс одиниця, і лише по діагоналі одиниця не додається, якщо символи збіглися. Заповнивши таблицю, у правому нижньому куті дістаємо відповідь.
З відстані роблять дві різні метрики, і плутати їх дорого.
CER (character error rate, посимвольна похибка) каже, яка частка символів зіпсована. CER 0.0435 на рядку з двадцяти трьох символів означає рівно одну помилку. Метрика не обмежена зверху одиницею: якщо модель дописала зайвого, відстань може перевищити довжину істини, і CER вийде більшим за 1.
Друга метрика жорсткіша й ближча до життя: у пошуку за ціною рядок «4500» з однією помилкою так само марний, як рядок із пʼятьма.
Ці дві метрики розходяться з довжиною рядка, і це не тонкість, а головна пастка звітів про OCR.
| помилка на символ | довжина рядка | CER | рядок цілком |
|---|---|---|---|
| 0.02 | 4 | 0.0203 | 0.9210 |
| 0.02 | 12 | 0.0198 | 0.7880 |
| 0.02 | 25 | 0.0188 | 0.6230 |
| 0.05 | 4 | 0.0465 | 0.8280 |
| 0.05 | 12 | 0.0456 | 0.5650 |
| 0.05 | 25 | 0.0474 | 0.2880 |
Одна й та сама модель. CER не зрушив із місця — він і не мусить, бо міряє символи. А «рядок цілком» упав із 0.9210 до 0.6230 просто тому, що рядки стали довшими. Формула тут проста: якщо помилки на символах незалежні, то ймовірність чистого рядка дорівнює (1 − p)L, і вона спадає з довжиною експоненційно.
Відстань Левенштейна й дві метрики
Ліворуч — таблиця відстані між істиною «телефон» і прогнозом, із підсвіченим найдешевшим шляхом. Праворуч — як «рядок цілком» падає з довжиною при тій самій посимвольній похибці.
РушійПромисловий рушій під псуванням
Досі курс усе будував сам: свій CLIP у темі
22, свій YOLO в темі 25, свій SAM у
темі 32. Тут уперше поруч стоїть
готова навчена модель, яку можна запустити офлайн, — рушій tesseract,
і він уміє українську.
Це дає рідкісну нагоду: замість здогадів «що заважає розпізнаванню» можна просто поламати картинку шістьма способами й подивитись на числа. Рядок один і той самий — «Продам телефон 4500 грн» та ще два подібні, щоб один щасливий випадок не збрехав.
tesseract 5.3.2 і шрифтом Liberation Sans. На іншій версії рушія чи
з іншим шрифтом третя цифра буде іншою. Важлива не вона, а порядок причин —
він на диво стійкий і на диво несподіваний.| псування | CER | чи заважає |
|---|---|---|
| мова eng замість ukr | 0.7045 | вбиває |
| висота літери 8 px | 0.6187 | вбиває |
| нахил 15° | 0.4211 | сильно |
| нахил 7° | 0.1855 | помітно |
| нахил 5° | 0.0101 | майже ні |
| розмиття σ 2 при висоті 32 | 0.0000 | ні |
| контраст 10 % від початкового | 0.0000 | ні |
| контраст 2 % (розмах 6 рівнів сірого) | 0.0000 | ні |
Останній рядок варто прочитати двічі. Картинка, на якій усе зображення вміщається в шість рівнів сірого — око бачить майже чистий білий аркуш, — читається без жодної помилки. Бінаризація всередині рушія адаптивна: вона дивиться на локальний перепад, а не на абсолютну яскравість, і їй байдуже, наскільки блідий текст, доки він відрізняється від паперу.
Між цими двома межами ми не міряли: 2 % — найнижчий рівень, який стоїть у таблиці, а не найнижчий, який працює. І ще одне обмеження, важливіше за перше: контраст тут псували наодинці, на чистому рядку висотою 32 px. Контраст стискає різницю «чорнило проти паперу», і будь-яке інше псування — шум, розмиття, дрібний шрифт — відкушує від того самого запасу. Тому «2 % не заважають» справджується для контрасту як єдиної вади й не переноситься на бліду й шумну картинку.
Звідси перший практичний висновок, протилежний тому, що написано в половині посібників: «почистити картинку» перед OCR — здебільшого марна робота. Перевіримо це прямо: візьмімо розмитий рядок і прогонимо через порогування Оцу, класичний спосіб «покращити» вхід.
| розмитий рядок (σ 3) | CER |
|---|---|
| як є | 0.0670 |
| після порогування Оцу | 0.1673 |
Стало вдвічі гірше. Оцу вибирає один поріг на все зображення й викидає півтони — саме ту інформацію, якою внутрішня адаптивна бінаризація рушія скористалася б. Чужий крок обробки не додав нічого, зате зʼїв дані.
Нахил — найдешевша поломка
А ось нахил заважає, і заважає різко. Між 5° і 7° стоїть обрив: 0.0101 → 0.1855, тобто у вісімнадцять разів. Причина видна, щойно згадати попередній розділ: рушій ріже сторінку на рядки горизонтальними смугами. Нахилений рядок у горизонтальну смугу не влазить — його верх на одному кінці й низ на іншому опиняються в різних смугах, і ми знову маємо ту саму поломку, що в інтерактиві 2.
Найважливіше тут — що це лікується повністю й задарма. Повернімо рядок назад на той самий кут:
| нахил | CER як є | CER після вирівнювання |
|---|---|---|
| 7° | 0.1855 | 0.0000 |
| 10° | 0.1919 | 0.0000 |
| 15° | 0.4211 | 0.0000 |
Рівно нуль. Не «майже нуль», не «краще» — точнісінько той самий результат, що на непошкодженому рядку. Вирівнювання нахилу (deskew) — єдина передобробка з усіх, які ми перевірили, що справді щось дає, і вона коштує один поворот.
Справжня межа — висота літери
Найцікавіше — остання поломка. Заміряймо CER як функцію висоти шрифту в пікселях.
| висота | 32 | 16 | 14 | 12 | 11 | 10 | 9 | 8 | 7 |
|---|---|---|---|---|---|---|---|---|---|
| CER | 0.0000 | 0.0000 | 0.0145 | 0.0108 | 0.0562 | 0.1736 | 0.2053 | 0.6187 | 0.7910 |
До одинадцяти пікселів усе гаразд, з десяти починається сповзання, з восьми — вже сміття. Ось звідки береться правило «скануй у 300 DPI», яке всі повторюють, не пояснюючи: при 300 точках на дюйм звичайний кегль 10 pt дає літеру приблизно двадцять пікселів заввишки — рівно з дворазовим запасом над межею. При 150 DPI було б десять пікселів, тобто рівно на краю прірви.
І тут же розгадка розмиття. У таблиці вище розмиття з σ 2 не заважало при висоті 32. Заміряймо ту саму σ при різних висотах:
| висота | σ 0 | σ 1 | σ 2 | σ 3 | σ 4 | σ 6 |
|---|---|---|---|---|---|---|
| 20 px | 0.0000 | 0.0000 | 0.0973 | 0.7778 | 0.8813 | 0.9596 |
| 32 px | 0.0000 | 0.0000 | 0.0000 | 0.0670 | 0.6457 | 1.0000 |
| 48 px | 0.0000 | 0.0000 | 0.0000 | 0.0000 | 0.0670 | 0.6520 |
Поріг їде разом із висотою: 2 px при висоті 20, 3-4 px при 32, 6 px при 48. Тобто розмиття — не окрема причина, а та сама причина, записана інакше. Значення має не σ у пікселях, а σ у частках товщини штриха. Обидві поломки — і дрібний шрифт, і розмиття — це один і той самий дефіцит: у літери лишилось замало пікселів, щоб її форма ще була формою.
Що справді заважає читанню
Обери спосіб псування й силу. Ліворуч — як після цього виглядає цифра, праворуч — заміряний CER на всіх рівнях цього псування, поточний рівень підсвічено.
tesseract 5.3.2 на трьох різних
рядках; картинка ліворуч намальована власним растровим шрифтом, тому вона
ілюструє псування, а не є його джерелом.АлфавітАлфавіт — частина моделі
Найсильніший результат розділу отримується без жодного псування картинки.
Візьмімо той самий бездоганний знімок і змінімо одну літеру в налаштуванні:
замість lang="ukr" напишімо lang="eng".
Українською CER рівно 0.0000, англійською — 0.7045 у середньому по трьох рядках, тобто зіпсовано сім символів із десяти. Це найбільше псування з усіх, які ми міряли, і воно не зачепило жодного пікселя.
Подивись, що саме сталося. Рушій не «не зміг» — він чесно віддав найкращу відповідь із тих, що йому дозволено вимовити. Кирилиці в англійському алфавіті немає, тож кожен гліф розклався на схожі латинські:
| було | стало | що сталося |
|---|---|---|
| П | N | найсхожіша латинська літера |
| д | g | хвостик униз збігся |
| ф | cb | одна літера стала двома |
| и | u | перевернута форма |
Третій рядок найважливіший. «ф» — це коло з вертикальною рискою; латиниця такого гліфа не має, зате має пару c плюс b, яка разом виглядає схоже. Модель змінила довжину рядка, щоб краще пояснити пікселі. Це не збій — це саме те, чого від неї вимагали.
Практичний наслідок буденніший, ніж здається. Найчастіша причина, з якої OCR «не працює», — не архітектура й не якість знімка, а неправильно виставлений алфавіт: у документі є українська, а рушій налаштований на англійську; у полі самі цифри, а рушій дозволяє собі літери й читає «0» як «O».
ШумШум немонотонний, і один замір бреше
Лишилося останнє псування — гаусів шум. Тут легко зробити висновок, протилежний до правди, і зробити його чесно: досить заміряти один рівень.
| σ шуму | середній CER | найкращий прогін | найгірший |
|---|---|---|---|
| 0.10 | 0.0000 | 0.0000 | 0.0000 |
| 0.20 | 0.0070 | 0.0000 | 0.0323 |
| 0.25 | 0.0353 | 0.0000 | 0.0870 |
| 0.30 | 0.6860 | 0.0435 | 1.0000 |
| 0.35 | 0.6871 | 0.0435 | 1.0000 |
| 0.40 | 0.5961 | 0.0435 | 1.0000 |
| 0.50 | 0.4128 | 0.0645 | 1.0000 |
| 0.60 | 0.1481 | 0.0968 | 0.2609 |
Крива не монотонна. Найгірше рушієві не при найбільшому шумі, а посередині: 0.6871 при σ 0.35 і лише 0.1481 при σ 0.60. Якби ми заміряли один рівень — скажімо, σ 0.60 — то написали б «шум майже не заважає» і мали б рацію рівно в тій одній точці.
Пояснення просте, щойно подивитись на самі відповіді. При середньому шумі рушій вигадує зайві сегменти: шумні плями на папері він приймає за розділові знаки й повертає довгий рядок сміття, у якому справжні літери потонули. При великому шумі він здається раніше й повертає короткий рядок або порожнечу — а короткий помилковий рядок коштує в CER менше, ніж довгий.
МодельCRNN: смужка ознак замість літер
Тепер збудуємо власну модель — і побудуємо її так, щоб кроку «поділ на символи» в ній не було взагалі. Класична відповідь зветься CRNN (convolutional recurrent neural network) і складається з трьох частин.
Звичайні згортки з пулінгом, як у блоці 2. Але пулінг несиметричний: по висоті стискаємо агресивно, по ширині майже ні. З картинки 16×40 виходить карта висотою 4 і шириною 20. Далі висоту згортаємо в канали, і лишається смужка з 20 колонок, у кожній — вектор ознак.
Двонапрямлений GRU проходить смужку зліва направо й справа
наліво. Навіщо: колонка сама по собі бачить лише вертикальну скибку літери, а
сусідні колонки кажуть, що це за літера цілком. Двонапрямленість потрібна тому,
що початок літери пояснюється її кінцем не менше, ніж навпаки.
У кожній із T колонок — розподіл над алфавітом плюс один зайвий символ. У нас це 10 цифр + 1 = 11 класів. Про зайвий символ — увесь наступний розділ.
Уся мережа — 57 131 вага, і на картинці 16×40 вона дає смужку з T = 20 колонок. Головне в ній не розмір, а те, чого в ній немає: ніде не сказано, де на картинці кожна цифра. Розмітка складається з пари «картинка → рядок», і все.
Але тоді виникає питання, яке й тримає всю тему. Мережа віддає 20 колонок, а правильна відповідь має, скажімо, чотири символи. Як порівняти 20 з 4, не знаючи, яка колонка якому символу відповідає?
CTCПорожній символ і склеювання повторів
Відповідь зветься CTC (connectionist temporal classification). Ідея в тому, щоб не вибирати одне вирівнювання, а підсумувати по всіх.
Спершу правила перетворення. Мережа віддає шлях — по одному символу на кожну з T колонок. Шлях перетворюють на рядок двома діями, строго в такому порядку:
- склеїти сусідні однакові символи в один;
- викинути порожні символи.
Тире тут — порожній символ (blank). Тепер видно, навіщо він потрібен, і це не «символ пробілу».
Склеювання повторів потрібне тому, що одна цифра займає кілька колонок смужки. Мережа природно скаже «пʼятірка, пʼятірка, пʼятірка» на трьох сусідніх колонках, і без склеювання ми прочитали б «555» замість «5».
Порожній символ потрібен тому, що після склеювання не можна записати справжній повтор. «55» вимагає порожнього між двома пʼятірками — інакше склеювання зʼїсть другу. Порожній символ означає не «пробіл», а «тут щойно нічого не закінчилось»: він розділяє дві сусідні однакові літери й заповнює колонки, у яких немає нічого цікавого.
Скільки колонок треба
Звідси випливає обмеження на довжину смужки. Щоб записати рядок довжини L, потрібно щонайменше L колонок, а на кожну пару сусідніх однакових символів — ще по одній під порожній:
У найгіршому випадку, коли всі символи однакові, це дає T ≥ 2L − 1. Порахуймо на прикладах:
| рядок | L | повторів | найменше T |
|---|---|---|---|
| 45 | 2 | 0 | 2 |
| 450 | 3 | 0 | 3 |
| 455 | 3 | 1 | 4 |
| 555 | 3 | 2 | 5 |
| 55555 | 5 | 4 | 9 |
Скільки шляхів ведуть до одного рядка
Сітка станів: рядки — розширена послідовність (порожній, символ, порожній, символ…), стовпці — колонки смужки. Число в кружку — скільки шляхів приводять сюди. Праворуч унизу — відповідь.
Тепер найголовніше. Імовірність рядка за CTC — це сума ймовірностей усіх шляхів, що в нього склеюються:
Символ Σ (велика грецька «сигма») означає «додай усе, що стоїть праворуч». Втрата CTC — це мінус логарифм цієї суми, і навчання просто збільшує сумарну ймовірність усіх правильних вирівнювань одразу, не обираючи між ними.
Шляхів багато — для рядка «45009» при T = 20 їх
1 961 256, — але рахувати їх поодинці не треба: та сама сітка з інтерактиву дозволяє порахувати суму
за T кроків динамічним програмуванням. Ми перевірили це
прямо: перебрали всі 4096 шляхів на маленькому прикладі й дістали
0.0078335876 проти 0.0078335870 у nn.CTCLoss — збіг до сьомого знака.
НавчанняВікно за швидкістю навчання
А тепер поламане місце, через яке CTC має репутацію примхливої втрати. Навчімо мережу тричі — з різною швидкістю навчання, по три зерна на кожну.
| швидкість навчання | рядок цілком | CER | частка порожніх відповідей |
|---|---|---|---|
| 3e-2 | 0.0333 ±0.0471 [0.0000..0.1000] | 0.7287 ±0.2319 [0.4334..1.0000] | 0.3367 |
| 3e-3 | 1.0000 ±0.0000 | 0.0000 ±0.0000 | 0.0000 |
| 3e-4 | 0.0000 ±0.0000 | 1.0000 ±0.0000 | 1.0000 |
По обидва боки від робочого вікна стоїть той самий результат — порожній рядок. Замалий крок не дає мережі вибратись із порожнього символу взагалі: усі три зерна віддають порожнечу на всіх двохстах перевірних картинках, і CER виходить рівно 1.0000. Завеликий збиває навчання так, що частина зерен теж падає в порожнечу.
Це не «підбери швидкість навчання» — це властивість самої втрати, і видно її на кривій.
Дивись на робочу криву (3e-3) між четвертою й тринадцятою епохою: за десять епох втрата впала на 0.0963. У таблиці ці дві епохи не надруковані, зате поруч видно те саме на круглих числах — з 2.7261 на пʼятій до 2.6392 на пʼятнадцятій, і це полиця, яка не випадкова.
Наслідок практичний і неприємний: читач, який вимкне навчання на пʼятнадцятій епосі, побачить втрату 2.6392 і рівну лінію позаду — і вирішить, що CTC не працює. А при замалому кроці полиця й не закінчується — остання втрата 2.4828 проти 0.0097 у робочого режиму.
Полиця на кривій втрати
Три криві — три швидкості навчання. Повзунок ставить епоху, на якій ти вирішив зупинитись і зробити висновок.
ДекодуванняЖадібний шлях проти найімовірнішого рядка
Мережа навчена. Лишилось перетворити двадцять колонок на рядок — і тут ховається остання пастка теми.
Жадібне декодування (greedy decoding) бере в кожній колонці найімовірніший символ і склеює те, що вийшло. Це один рядок коду й майже завжди правильна відповідь.
Але воно неправильне за побудовою, і зрозуміти чому важливіше, ніж уміти його замінити. Жадібний вибір знаходить найімовірніший шлях. А нас цікавить найімовірніший рядок, і ймовірність рядка — це сума по всіх його шляхах. Рядок може складатися з мільйона посередніх шляхів і перемогти один блискучий.
Ось найменший приклад, де вони розходяться. Три класи: порожній, «0» і «1». У кожній колонці порожній трохи ймовірніший за кожну окрему цифру — але цифра «0» може прийти багатьма шляхами, а порожній рядок лише одним.
Найімовірніший шлях проти найімовірнішого рядка
Таблиця ймовірностей: рядки — класи, стовпці — колонки смужки. Жовтим — жадібний вибір. Праворуч — усі рядки, які взагалі можна отримати, з їхньою повною ймовірністю.
Правильна відповідь зветься beam search — і в курсі ми лишаємо цю
назву латиницею, бо звичний переклад «променевий пошук» вводить в оману: нічого
променевого там немає. Ідея проста: вести не один шлях, а кілька десятків найкращих
рядків одразу, додаючи ймовірності шляхів, що зійшлися в один рядок. Ми
перевірили його на навченій мережі при дедалі більшому шумі.
| σ шуму на перевірці | жадібне CER | CER із beam search | розійшлись рядків |
|---|---|---|---|
| 0.08 | 0.0000 | 0.0000 | 0 зі 100 |
| 0.25 | 0.3342 | 0.3317 | 3 зі 100 |
| 0.40 | 0.7805 | 0.7756 | 6 зі 100 |
| 0.55 | 0.9127 | 0.9102 | 6 зі 100 |
Результат чесний і не дуже мальовничий: beam search майже нічого
не дає. Він міняє відповідь щонайбільше на шести рядках зі ста, а CER рухається в
третьому знаку.
beam
search виграє не сам по собі, а разом зі словником. У справжньому OCR до ймовірності шляху
додають імовірність слова з мовної моделі, і тоді він може обрати менш
імовірний шлях заради більш імовірного слова: «телефон» замість «телефом». Без
словника він просто акуратніше рахує те саме — і чесна різниця в третьому знаку.
Тому «додати beam search» без мовної моделі — це та сама марна робота, що
«почистити картинку» з розділу 5.ПідсумокЩо з цього забрати
Тема почалась із задачі, яка виглядала дрібною, і закінчилась чотирма твердженнями, жодне з яких не про архітектуру.
- Сегментація на символи — не крок до розвʼязку, а та сама задача. Найлегший можливий випадок — моноширинний растровий шрифт із явними проміжками — ламається від одного зниклого пікселя: правильно поділених рядків 0.3933. Тому CRNN і не ділить нічого: вона віддає смужку, а вирівнювання шукає втрата.
- Найбільше шкодить не якість знімка, а постановка. Найгірше псування з усіх, що ми міряли, — 0.7045 — не зачепило жодного пікселя: змінили алфавіт. Друге за силою — дрібний шрифт. А розмиття й контраст, на які витрачають передобробку, не заважають узагалі, і порогування Оцу робить вдвічі гірше.
- Метрика мусить називатись повністю. CER і «рядок цілком» розходяться з довжиною рядка: при незмінному CER 0.02 частка чистих рядків падає з 0.9210 на чотирьох символах до 0.6230 на двадцяти пʼяти. Звіт з однією з цих метрик можна прочитати як завгодно.
- У CTC є вузьке вікно, і по обидва боки від нього — мовчання. Полиця на кривій втрати — не «мережа не вчиться», а мінімум, у якому найдешевше нічого не казати. Хто зупинить навчання на полиці, зробить хибний висновок; хто візьме замалий крок, ніколи з неї не зійде.
І окремо — те, що ця тема дала курсу вперше. Досі ми будували все самі й порівнювати не було з чим. Тут поруч стояв промисловий рушій, і виявилось, що питання до нього ставляться ті самі, що до власної моделі: який у тебе алфавіт, скільки пікселів на літеру, як ти ділиш рядок на смуги. Готова модель не скасовує цих питань — вона лише відповідає на них за тебе, і мовчки.
Куди йдемо далі
Блок 7 додає до знімка два виміри, яких у курсі ще не було. Ця тема була про те, що робити, коли відповідь не картинка. Наступні три — про те, що робити, коли вхід не одна картинка.
Тема 34 додає час: той самий предмет на сусідніх кадрах має лишитись тим самим предметом, і зʼявиться нова метрика — підміна номера. Ти вже бачив у цій темі, як помилка детектора руйнує все далі по конвеєру; у трекінгу це стане головним сюжетом. Тема 35 питає, що з часом робити далі: як класифікувати не кадр, а рух. А тема 36 додає глибину — і разом із нею похибку, яку вперше в курсі міряють у метрах, а не в безрозмірних частках.