Введение
Большинство исследователей данных знакомо с алгоритмом k ближайших соседей и легко могут применить его, импортировав соответствующий класс из scikit-learn:
from sklearn.neighbors import KNeighborsClassifier, KNeighborsRegressor
Читавшие документацию/курсы/книги немного дальше примера использования знают, что ещё хорошо бы свести признаки к одному масштабу:
from sklearn.preprocessing import StandardScaler, MinMaxScaler
ss = StandardScaler()
X_scale = ss.fit_transform(X)
Понявшие смысл этого действия могут даже попытаться понастраивать веса признаков, опираясь на собственное представление об их важности. Вроде всё строго, обыденно и даже слегка скучно. Может модель вытащит на малом числе фичей, а может и нет…
Капитан Очевидность – персонаж, олицетворяющий человека, который говорит банальные вещи, которые все кроме новичков давно знают и без него
Категориальные признаки
Что там было из простого? А, масштабирование признаков! Оно предполагает сведение векторов к «схожим» экстремальным показателям. Минимум в нуле, максимум в единице, что-то там про математическое ожидание, дисперсию и т.д. Работает на целых и не целых числах, но вот с категориальными не прокатит, даже если очень хочется. Исключение, когда они имеют упорядоченную структуру, но и то с опаской и долгими предварительными размышлениями.
Что же тогда делать с категориальными признаками?
1) Перевод в дамми переменные
Из длинного формата их представление переходит в широкий и появляется столько столбцов, сколько было категорий в признаке (или на один меньше, если в выборке предполагается наличие всех возможных в тесте/проде категорий). На выходе получаем много ноликов и мало единиц.
from sklearn.preprocessing import OneHotEncoder
one_hot = OneHotEncoder()
X_dummy = one_hot.fit_transform(X_cat)
И вроде вот, всё решено, можно прыгать использовать. Но есть одна маленькая проблемка: размерность, а с ней и затраты памяти, скачут дай боже (а мы сидим в гараже со стареньким ноутом). Частичным решением является использование разреженных матриц scipy:
OneHotEncoder(sparse=True)
Такой вариант не всегда спасёт и поможет, но обычно выбора и нет.
2) На первый взгляд глупо, на второй тоже, на третий score взлетает
Специфичная ситуация может возникнуть когда вы предполагаете (основываясь хотя бы на статистических тестах или бизнес-логике), что категориальные признаки играют первичную роль в формировании целевой переменной. Они явно выделяют кластеры (которые по размеру больше, чем ваше любимое число соседей) и уже внутри них по остальным переменным уточняют прогноз. Да, такое бывает. В финансах точно бывает.
а) Переводим категории в натуральные числа (реализация scikit-learn предполагает сделать это по отдельности для всех признаков);
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
X_cat_int = le.fit_transform(X_cat)
б) Масштабируем остальные признаки ($inline$min=0, max=\frac{1}{n_{all} — n_{cat}}$inline$); (1)
с) Обучаем модель.
Почему может сработать? Как вы помните категориальные фичи играют у нас первую скрипку и когда мы выдаём им дельты между категориями начиная от 1, в то время как сумма дельт по всем признакам не может превышать единицы (1), то для модели kNN мы делаем их основой для вычисления ближайших соседей. В первую очередь, они ищутся среди друзей по кластеру, а уже потом из них выделяются самые близкие по остальным переменным.
В scikit-learn также реализована возможность кастомизации метрики расстояния, как на уровне степени для метрики Минковского, так и на уровне полностью своей функции. Можно поэкспериментировать со специальным классом, а потом вставить в модель:
from sklearn.neighbors import DistanceMetric
Звучит хорошо, выглядит гладко, но без подводных камней не обошлось. Сам scikit-learn под капотом имеет низкоуровневый C, а вот кастомная функция подвержена всем недостаткам Python, о чём честно сообщают разработчики. Сначала можно подумать, что это не критично и, поставив на ночь, вы утром получите готовый прогноз. Но все надежды ломаются при хоть сколько-нибудь большом числе соседей и размерах обучающей выборки.
Отношения с соседями
Что ещё было из простого? А, точно, веса признаков! Но сейчас не о них. Благодаря авторам библиотеки scikit-learn позволяет настроить веса соседей, которые изначально имеют равный вклад в прогноз. За это отвечает парметр weights. По дефолту имеет значение 'uniform', который прямо как унисекс подходит всем. Но если есть желание выработать собственный стиль, то стоит обратить внимание на 'distance' и callable. Первое задаёт вклад соседей пропорционально расстоянию до них, а второе открывает все возможности для проявления
Гиперпараметры
Для хорошего качества модели нужно не только обучить её на хороших данных, но и настроить её параметры, которые не будут меняться во время самого обучения, т.е. гиперпараметры.
В модели kNN гиперпараметрами обычно выступают:
а) Число соседей (n_neighbors) — с параметром стоит экспериментировать и не бояться ставить большим;
б) Степень для метрики Минковского (p) — 1 или 2 крайне часто будут лучшими.
Дополнительно можно настроить алгоритм под капотом модели (algorithm), рекомендуется делать при понимании его матчасти.
Заключение
Теперь вы знаете об алгоритме kNN чуть больше и можете дать свой фидбэк публикации.