Модель, якої немає

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

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

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

ПостановкаСхожі поводяться схоже

Це і є метод k найближчих сусідів (k-nearest neighbours, kNN), сформульований одним реченням: щоб оцінити новий обʼєкт, знайди k найсхожіших зі знайомих і подивись, чим вони виявились. Ніякої іншої ідеї всередині немає.

Наш архів — таблиця, у якій кожен рядок є одним оголошенням:

колонкащо цеприклад
modelмодель телефонаiPhone 12
yearрік випуску2021
conditionстан за пʼятибальною шкалою4
memory_gbобсяг памʼяті, ГБ128
account_age_daysвік акаунта продавця, днів30
priceзапитувана ціна, грн12 600
is_fraudмітка: чи виявилось оголошення приманкою1

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

Перша — відносна ціна: запитувана ціна, поділена на типову ціну тієї самої моделі з такою самою памʼяттю й станом. Сирі 12 000 грн нічого не кажуть: для флагмана це дешево, для бюджетника — дорого. А от «55% від типової ціни» — уже сигнал. Типова ціна в нашій вибірці — 12 000 грн, тож відносна ціна 0.75 означає 9 000 грн. Друга ознака — вік акаунта продавця в днях: приманки майже завжди виставляють зі свіжозареєстрованих акаунтів.

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

Пряма межа проти ламаної

Ті самі 300 навчальних оголошень, та сама пара ознак, дві різні моделі. Точність поміряна на 260 відкладених оголошеннях.

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

Різниця не косметична. Шахрайство тут двох ґатунків: масові дешеві приманки зі свіжих акаунтів (велика область ліворуч унизу) і вузький згусток праворуч унизу — нові акаунти, що продають майже за ринковою ціною, аби не викликати підозри дешевизною. Одна пряма не обведе обидві області одночасно, тому логістична регресія жертвує меншою. kNN не жертвує нічим: кожна ділянка площини вирішується незалежно від інших.

НепараметричністьНавчання, якого немає

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

У kNN підбирати нічого. Немає коефіцієнтів, немає рівняння, немає жодної величини, яку алгоритм обчислив би на навчальних даних. Навчання kNN — це просто запамʼятати таблицю. Буквально: fit() копіює дані в памʼять і на цьому закінчується. Тому такі методи називають непараметричними (non-parametric), а сам kNN — лінивим (lazy learner).

Що саме лишається після навчання

Ті самі 300 навчальних оголошень заходять у дві моделі. Ліворуч — те, що зберігає логістична регресія, праворуч — те, що зберігає kNN.

Ліворуч таблиця зникає, праворуч лишається. Логістична регресія вичавлює з 300 рядків три числа — і після навчання архів можна викинути. kNN не вичавлює нічого: його «вивчене» дорівнює вхідній таблиці, тому обсяг моделі росте разом із даними. Саме звідси беруться і ламана межа, і дорогі передбачення.
Непараметричний не означає «без налаштувань». Термін стосується іншого: у моделі немає фіксованого набору параметрів, який вчиться з даних. Число k у методі є, але це гіперпараметр — його не виводять з даних, а обирають зовні, як поріг у логістичній регресії. Обсяг «вивченого» у kNN росте разом із вибіркою: тисяча рядків — тисяча рядків моделі.

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

ВідстаньЩо означає «схожий»

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

d(a, b) = √( (a1 − b1)2 + (a2 − b2)2 )

Тут a і b — два оголошення, а нижній індекс нумерує ознаку: a₁ — відносна ціна першого, a₂ — вік його акаунта. Різниці показують, наскільки оголошення розходяться по кожній ознаці окремо; квадрат прибирає знак і підсилює великі розходження; корінь повертає результат до масштабу ознак. Це евклідова відстань — та сама, якою міряють відрізок лінійкою. Для p ознак під коренем буде p доданків.

Порахуємо руками на двох конкретних оголошеннях:

крок 1 · дві точки

Оголошення №1: відносна ціна 0.60 (це 7 200 грн), акаунту 30 днів.
Оголошення №2: відносна ціна 0.90 (10 800 грн), акаунту 210 днів.

крок 2 · різниці по кожній ознаці
Δціна = |0.60 − 0.90| = 0.30      Δвік = |30 − 210| = 180
крок 3 · підставляємо у формулу
d = √( 0.302 + 1802 ) = √( 0.09 + 32 400 ) = 180.0002

Порівняй результат із доданками. Відстань дорівнює 180.0002, а різниця у віці — рівно 180. Ціна не вплинула ні на що: її внесок у підкореневу суму становить 0.00028%.

Це не арифметична помилка й не властивість цієї пари. Це наслідок одиниць вимірювання: вік акаунта живе в діапазоні 0…365, а відносна ціна — у 0.30…1.45. Дні просто більші за частки й у сумі квадратів розчавлюють усе інше. Розділ 6 присвячений саме цій пастці; поки що зрівняємо масштаби найпростішим способом — поділимо кожну ознаку на її розмах:

Розмах — це відстань від найменшого значення ознаки до найбільшого. У ціни це 1.45 − 0.30 = 1.15, у віку акаунта — 365 − 0 = 365. Ділення на розмах перекладає обидві різниці в одну спільну одиницю: «яку частку всього можливого діапазону ознаки займає це розходження».

ціна: 0.30 / 1.15 = 0.2609      вік: 180 / 365 = 0.4932
d = √( 0.26092 + 0.49322 ) = √0.3113 = 0.558

Тепер ціна дає 21.9% підкореневої суми замість 0.00028% — вона нарешті бере участь у рішенні. Усі відстані далі в лекції рахуються саме так, у зрівняних одиницях.

Евклідова відстань — не єдина можлива. Друга за поширеністю — манхеттенська (Manhattan, вона ж L1): замість кореня із суми квадратів беруть просто суму модулів різниць.

d1(a, b) = |a1 − b1| + |a2 − b2| = 0.2609 + 0.4932 = 0.754

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

Дві відстані між тими самими оголошеннями

Ліворуч — геометрія в зрівняних одиницях. Праворуч — внесок кожної ознаки у квадрат відстані до і після зрівнювання масштабів.

Головне праворуч: у сирих одиницях смужка ціни настільки тонка, що її не видно — 0.00028% від квадрата відстані. Після ділення на розмах ознака займає 21.9%. Той самий алгоритм, ті самі дані, а «схожість» означає геть інше.
Для нечислових ознак евклідова відстань не визначена: колонку model не відняти від іншої моделі. Стандартний вихід — one-hot: кожна модель стає окремою ознакою 0/1, і різниця між різними моделями дорівнює √2, а між однаковими — нулю. Для суто категорійних даних беруть відстань Геммінга — частку неспівпадінь.

ПрогнозГолосування і середнє

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

Класифікація: сусіди голосують

Порахуй відстань від нового оголошення до всіх оголошень архіву, відсортуй за зростанням, візьми перші k і подивись, яких міток серед них більше. Ця мітка й буде відповіддю. Ось запит: оголошення за 9 000 грн (0.75 від типової ціни) від акаунта віком 160 днів.

Голосування девʼяти сусідів

Один запит, девʼять найближчих сусідів з архіву, дві різні відповіді залежно від k.

Той самий запит, дві відповіді. Троє найближчих: двоє з них виявились приманками — 2 проти 1, вирок «шахрайство». Розширюємо коло до девʼяти — додаються переважно чесні оголошення, стає 3 проти 6, і вирок перевертається на «чесне». Ніхто не помилився: питання просто ставили різним колам сусідів.

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

Замість «одна точка — один голос» сусідів можна зважувати за близькістю: у scikit-learn це weights='distance', вага дорівнює 1/d. Тоді сусід упритул важить більше за того, що ледве потрапив у коло, а модель менш чутлива до вибору k.

Регресія: сусіди усереднюються

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

ŷ = ( y(1) + y(2) + … + y(k) ) / k

Нехай треба оцінити телефон 2021 року, 128 ГБ, стан 4. Пʼять найсхожіших оголошень з архіву:

сусідрікпамʼятьстанвідстаньціна, грн
1202112840.0011 400
2202112850.2512 200
3202012840.3310 900
4202212840.3312 600
5202125640.5011 900

При k = 3 відповідь дорівнює (11 400 + 12 200 + 10 900) / 3 = 11 500 грн. При k = 5 — (11 400 + 12 200 + 10 900 + 12 600 + 11 900) / 5 = 11 800 грн. Знову ж таки, k змінює відповідь.

Чого kNN-регресія не вміє. Прогноз — це середнє реальних цін із таблиці, тому він ніколи не вийде за межі найдешевшого й найдорожчого оголошення архіву. На запиті «скільки коштуватиме модель 2027 року» лінійна регресія видасть якесь число (можливо, безглузде), а kNN — ціну найдорожчого з відомих телефонів і не зрушить далі. Екстраполювати він не вміє принципово, і це чесно.

Вибір kСкільки сусідів питати

Настав час покрутити головне налаштування. Візьми повзунок і проведи його від краю до краю — тут видно все, що варто знати про k.

Повзунок k і межа рішень

Ліворуч — область рішення на площині ознак і навчальні оголошення. Праворуч — точність на 260 відкладених оголошеннях для кожного k.

точність, тест—
точність, навчання—
найкраще k—
його точність—
Прожени обидва краї. При k = 1 точність на навчальних даних дорівнює 1.000 — і це не досягнення: найближчий сусід навчального оголошення завжди він сам. При k = 51 межа розповзається, вузький згусток праворуч унизу зникає повністю, і модель наближається до відповіді «чесне» для всіх.

Три режими, які ти щойно побачив, варто назвати словами.

Під цим компромісом лежить пара, яку варто назвати вже тут. Зміщення (bias) — це помилка від того, що модель занадто груба, щоб уловити справжню форму закономірності: велике k усереднює півархіву й стирає вузький згусток «преміум»-шахрайства. Дисперсія (variance) — помилка від того, що модель занадто чутлива й ганяється за випадковостями: k = 1 обростає острівцями навколо кожного помилково поміченого оголошення. Роль складності моделі тут грає величина 1/k: маленьке k — мале зміщення й велика дисперсія, велике k — навпаки, а десь між ними лежить мінімум сумарної помилки, і крива праворуч на інтерактиві його показує. Розкладати помилку на ці дві частини системно вчить тема 17 «Перенавчання, недонавчання і розклад помилки»; там та сама крива побудована по степені многочлена, а не по k.

І не обирай k за цією кривою. Ми щойно перебрали всі k і взяли найкраще за тестовою вибіркою — тобто підглянули у відповідь. Так робити не можна: обране так k буде оптимістично зміщеним. Чесний спосіб — крос-валідація на навчальній частині, а тестова чіпається один раз наприкінці. Про цей розподіл ролей — тема Train / Validation / Test. Крива тут показана лише тому, що ми вивчаємо поведінку методу, а не будуємо продукт.

МасштабПастка, на якій ламається більшість

Повертаємось до розбіжності, помітної ще в розділі 3. Візьмемо не відносну ціну, а дві сирі колонки таблиці як вони є: price у гривнях і account_age_days у днях. Ціни в архіві коливаються від 3 600 до 17 300 грн, вік акаунта — від 0 до 365 днів. Тобто найбільша можлива різниця у ціні дорівнює приблизно 13 700, а у віці — 365, і у квадраті це розрив приблизно в 1 400 разів.

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

Вплив масштабу ознак

Ті самі дві ознаки в тих самих осях. Перемикач змінює тільки одне: у яких одиницях рахується відстань.

точність, тест—
«завжди чесне»—
голоси за шахрайство—
вирок для ★—
Зірка — конкретне нове оголошення: 12 600 грн від акаунта віком 30 днів, і воно справді виявилось шахрайським. Це «преміум»-приманка: ціна майже ринкова, і впіймати її можна лише за віком акаунта. Саме тому в режимі «як є» модель її пропускає.

Числа за перемикачем такі. При k = 7 у сирих одиницях точність дорівнює 0.750, після зрівнювання — 0.931. Для порівняння: тривіальна відповідь «усі оголошення чесні» дає 0.715. Тобто незмасштабований kNN майже нічого не додає до відповіді «нічого не роби».

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

Способів зрівняти масштаби два, обидва є в scikit-learn, і обидва вже розібрані в темі 09 «Передобробка даних» — тут вони потрібні тому, що для kNN це не гігієна, а питання життя й смерті:

Де тут витік даних. Мінімум, максимум, середнє й відхилення рахують лише на навчальній частині, а до тестової застосовують уже готові числа. Якщо порахувати їх на всій таблиці одразу, у навчання просочиться інформація про тестові обʼєкти, і оцінка якості завищиться. Технічно це робиться конвеєром: make_pipeline(StandardScaler(), KNeighborsClassifier(k)) — тоді fit усередині крос-валідації масштабує кожну частину окремо й помилитись просто ніде.

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

РозмірністьКоли «найближчий» втрачає сенс

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

У просторі багатьох вимірів відстані між випадковими точками стають майже однаковими: найближча точка до запиту виявляється майже такою самою далекою, як найдальша. А якщо найближчий сусід не ближчий за інших, слово «найближчий» перестає щось означати — і разом із ним перестає працювати весь метод. Це і є прокляття розмірності (curse of dimensionality). Перевіримо прямо: розкидаємо 400 випадкових точок у кубі зі стороною 1, поміряємо для кожного запиту відстань до найближчої й до найдальшої, а тоді почнемо додавати виміри до тих самих точок.

Прокляття розмірності

400 точок, рівномірно розкиданих у кубі. Повзунок додає виміри; відношення «найближча / найдальша» повзе до одиниці.

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

Числа з інтерактиву: при 2 вимірах відношення дорівнює 0.027 — найближча точка у 37 разів ближча за найдальшу. При 10 вимірах воно вже 0.308, при 50 — 0.620. Пучок стискається, і кожен наступний вимір робить «сусідство» дедалі умовнішим.

Пояснити це можна й без обчислень, самим обʼємом. Обʼєм кубика з ребром r у просторі з d вимірами дорівнює r, помноженому само на себе d разів: на площині це квадрат r·r, у трьох вимірах — r·r·r, і так далі. Отже, щоб кубик займав 1% усього обʼєму, його ребро має бути таким числом, яке після d перемножень само на себе дає 0.01. Це число називають коренем d-го степеня з 0.01 і записують 0.01^(1/d): дріб у показнику означає корінь, а не звичайний степінь.

Перевір на двох вимірах: 0.1 × 0.1 = 0.01, тобто ребро 0.1 — маленький квадратик у кутку, як і має бути. А при d = 50 потрібне ребро вже 0.912: щоб набрати той самий один відсоток сусідів, «околиця» має розтягнутись майже на весь куб по кожній осі. Локальність зникає, а kNN — метод суто локальний.

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

ЦінаНавчання безкоштовне, передбачення дороге

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

kNN проти лінійних моделей

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

kNNлінійна / логістична регресія
навчаннямиттєве: скопіювати таблицюдороге: ітерації по всій вибірці
передбаченнядороге: n·p дій на кожен запитмиттєве: p множень
памʼятьуся навчальна таблицяp + 1 чисел
форма межібудь-яка, задається данимитільки пласка
масштаб ознаккритичнийбажаний
зайві ознакишкодять завждигасяться регуляризацією
поясненняприкладами: ось схожі випадкикоефіцієнтами: ось внесок ознаки
оновлення данихдописати рядок — і всеперенавчити модель
Як цим користуватись: n — розмір навчальної таблиці, p — кількість ознак. Дві верхні клітинки міняються місцями, і саме вони найчастіше вирішують вибір: якщо дані оновлюються щохвилини, а запитів мало, kNN виграє; якщо навпаки — програє повністю.

Порахуємо на нашій дошці. Архів у 300 оголошень і два запити на секунду — 600 порівнянь на секунду, тобто нічого. Архів у 5 мільйонів і 1 000 запитів на секунду — 5 мільярдів обчислень відстані щосекунди, і у відповідь за 50 мілісекунд це не вкладеться ніяк.

Повний перебір, утім, не єдиний варіант. scikit-learn уміє будувати просторові індекси — KD-дерево й ball tree, — які відсікають цілі області, куди найближчий сусід точно не потрапить (algorithm='auto' обирає сам). Але тут знову вспливає розмірність: приблизно після 20 ознак дерева перестають відсікати хоч щось і вироджуються у той самий перебір.

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

ПідсумокЩо варто винести

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

ідеясутьчому важливо
сусідствоk найближчих із архівууся модель — це сам архів
відстаньевклідова або манхеттенськаозначення слова «схожий»
класифікаціяголосування сусідівнепарне k рятує від нічиї
регресіясереднє їхніх значеньпрогноз не виходить за межі архіву
вибір k1/k — це складність моделімаленьке k повторює шум, велике стирає деталі
масштабзрівняти обовʼязковобез цього одна ознака зʼїдає всі інші
розмірністьвідстані сходяться«найближчий» втрачає сенс

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

І памʼятай, для чого ми взагалі рахували точність. На дошці шахрайських оголошень 28%, і ціна двох помилок різна: пропущена приманка коштує грошей покупцю, хибна тривога — репутації чесному продавцю. Точність такого не розрізняє, тож у справжній задачі метрику беруть із теми Precision і Recall. Тут вона слугує лише спільною лінійкою для порівняння k.