Машинне навчання · Блок 4 · Тема 18

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

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

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

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

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

Це і є метод 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 грн. Друга ознака — вік акаунта продавця в днях: приманки майже завжди виставляють зі свіжозареєстрованих акаунтів.

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

Схема 1 · Пряма межа проти ламаної

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

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

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

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

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

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

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

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

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

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

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 присвячений саме цій пастці; поки що зрівняємо масштаби найпростішим способом — поділимо кожну ознаку на її розмах:

ціна: 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

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

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

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

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

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

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

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

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

Схема 3 · Голосування дев'яти сусідів

Один запит, дев'ять найближчих сусідів з архіву, дві різні відповіді залежно від 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 — ціну найдорожчого з відомих телефонів і не зрушить далі. Екстраполювати він не вміє принципово, і це чесно.

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

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

Інтерактив 1 · Повзунок k і межа рішень

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

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

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

Це той самий компроміс, якому присвячена тема Bias-Variance, просто в інших позначеннях. Роль складності моделі грає величина 1/k: маленьке k — мале зміщення й велика дисперсія, велике k — навпаки. Криву праворуч на інтерактиві ти вже бачив у тій темі, тільки по горизонталі там був степінь многочлена.

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

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

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

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

Інтерактив 2 · Вплив масштабу ознак

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

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

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

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

Способів зрівняти масштаби два, обидва є в scikit-learn:

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

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

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

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

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

Інтерактив 3 · Прокляття розмірності

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

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

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

Пояснити це можна й без обчислень. Щоб у кубі розмірності d охопити околицю з 1% усього обʼєму, її ребро має дорівнювати 0.01^(1/d). При d = 2 це 0.1 — маленький квадратик у кутку. При d = 50 уже 0.912: щоб набрати той самий відсоток сусідів, «околиця» має розтягнутись майже на весь куб по кожній осі. Локальність зникає, а kNN — метод суто локальний.

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

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

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

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

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

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

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

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

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

Далі в практиці. У practice.ipynb ти порахуєш матрицю відстаней на NumPy й знайдеш k сусідів вручну, звіриш свої прогнози з KNeighborsClassifier до останнього обʼєкта, проженеш k від 1 до 50 й побудуєш криву точності, побачиш стрибок від StandardScaler і наприкінці оціниш ціну телефона через KNeighborsRegressor.

Далі в темі

Теорію прочитано. Тепер закріпи її на практиці.