Усі три рівні незалежні — можна робити будь-який. Здавати одним зошитом, у якому кожна відповідь супроводжується числом або графіком, а не лише словами.
рівень 1Рівень 1 — База
Повтори головний сюжет лекції на своїх даних: покажи, що масштабування ознак для kNN — не косметика.
Візьми будь-який табличний датасет із sklearn.datasets — підійдуть
load_breast_cancer() або load_wine(). Обидва зручні тим, що ознаки в них навмисне
різного масштабу. Розбий на train/test через train_test_split(..., random_state=0)
і побудуй дві криві точності для k від 1 до 40 на одному графіку: без
масштабування й з StandardScaler.
Зроблено, якщо:
- на графіку дві криві, підписані осі й легенда;
- у тексті виписано чотири числа: точність при k = 1 і при найкращому k — для кожної
з двох кривих;
- названо, у скільки разів розмах найбільшої ознаки датасету перевищує розмах найменшої
(X.max(axis=0) - X.min(axis=0)), і одним реченням пояснено, чому саме ця ознака
визначала сусідів до масштабування.
рівень 2Рівень 2 — Плюс
Виміряй прокляття розмірності на живій задачі, а не на випадкових точках.
Візьми load_wine() (13 ознак), обовʼязково зі StandardScaler, і фіксуй k = 5.
Далі дописуй до матриці ознак колонки чистого шуму:
np.random.default_rng(0).normal(0, 1, (len(X), скільки)) — по 0, 5, 10, 20, 50, 100
зайвих колонок. Щоразу міряй точність на тесті.
Поруч зроби те саме для LogisticRegression(max_iter=5000) на тих самих даних.
Зроблено, якщо:
- побудовано графік «точність проти кількості шумових ознак» із двома лініями (kNN
і логістична регресія);
- у таблиці зведено точність обох моделей для 0 і для 100 шумових колонок;
- сформульовано висновок, чому шум б'є по kNN сильніше: у поясненні має бути згадано,
що шумова ознака входить у відстань нарівні з корисною, тоді як лінійна модель може
дати їй коефіцієнт, близький до нуля;
- додатково: заміряно %timeit на predict для kNN при 0 і при 100 ознаках і показано,
що час зростає.
рівень 3Рівень 3 — Виклик
Напиши kNN з нуля так, щоб він збігся з бібліотечним і працював за прийнятний час на всій тестовій вибірці одразу.
- Реалізуй
матриця_відстаней(A, B), яка повертає масив розміромlen(A) × len(B)без жодного циклу Python. Підказка: розкрий квадрат різниці —‖a − b‖² = ‖a‖² − 2·a·bᵀ + ‖b‖², і всі три доданки рахуються матричними операціями. - Реалізуй
передбачити(X_запитів, X_навч, y_навч, k, ваги), девагинабуває значень"uniform"або"distance". Для"distance"вага сусіда дорівнює1 / відстань, а прогноз — клас із найбільшою сумою ваг. - Окремо оброби випадок нульової відстані: якщо запит збігається з навчальним обʼєктом, бібліотека повертає мітку саме цього обʼєкта, ігноруючи решту сусідів.
Зроблено, якщо:
- проходять обидві перевірки на даних із практики:
assert np.array_equal(наші, KNeighborsClassifier(n_neighbors=7).fit(...).predict(...))
та те саме з weights="distance";
- окремою клітинкою виведено час роботи твоєї векторизованої версії й версії з циклом
for із практики на тих самих 280 тестових обʼєктах — із числами;
- у тексті названо, скільки чисел твоя реалізація тримає в памʼяті одночасно для
вибірки 520 × 280, і чому на мільйоні рядків цей підхід уже не спрацює.
Підказки
- Рівень 1. Якщо криві збігаються майже повністю — значить, у датасеті ознаки вже
однакового масштабу. Перевір розмахи; для
load_wine()різниця міжproline(сотні) іhue(одиниці) величезна, тому цей датасет підходить точно. - Рівень 2. Шумові колонки треба додавати до розбиття на train/test, інакше
масштабування й розбиття рахуватимуться на різних матрицях. Найзручніше зібрати повну
матрицю через
np.hstack([X, шум]). - Рівень 3. Перший доданок
‖a‖²— це(A ** 2).sum(axis=1), і його треба розгорнути в колонку через[:, None], щоб broadcasting склав його з рядком‖b‖². Після віднімання можуть виникнути крихітні відʼємні числа через похибку — притисни їх до нуля черезnp.maximum(..., 0)перед коренем. - Для порівняння порядку сусідів зручний
kneighbors(): він повертає і відстані, і номери — можна звірити свійargsortнапряму, не чекаючи розбіжності у прогнозах.