Одна хорошая альтернатива горячему кодированию ваших категорий

Категориальные переменные являются проблемой для алгоритмов машинного обучения. Поскольку большинство (если не все) из них принимают в качестве входных данных только числовые значения, нам необходимо преобразовать категории в числа, чтобы использовать их в модели.

Закодировав их сразу, мы создаем действительно разреженную матрицу и завышаем количество измерений, с которыми должна работать модель, и мы можем стать жертвой ужасного проклятия размерности. Это усиливается, когда функция имеет слишком много категорий, большинство из которых бесполезны для прогноза.

Одним из умных способов решения этой проблемы является Target Encoder.

Код и примеры, использованные в этой статье, также доступны в моем репозитории GitHub:



Целевой кодировщик

Основная идея целевого кодировщика состоит в том, чтобы кодировать категории, заменяя их для измерения эффекта, который они могут оказать на цель.

В бинарном классификаторе самый простой способ сделать это — вычислить вероятность p(t = 1 | x = ci), при которой t обозначает цель, x – ввод, а ci – i-й категория. В байесовской статистике это считается апостериорной вероятностью t=1 при условии, что входными данными была категория ci.

Это означает, что мы заменим категорию ci на значение апостериорной вероятности того, что цель равна 1 при наличии этой категории.

Рассмотрим набор данных ниже:

Для каждой отдельной возможной категории (документальная литература, романтика, драма, научная фантастика и фэнтези) нам нужно подсчитать, сколько раз встречается цель 0 и цель 1. Затем мы вычисляем:

Это можно сделать с помощью кода ниже:

categories = df['genre'].unique()
targets = df['target'].unique()
cat_list = []
for cat in categories:
    aux_dict = {}
    aux_dict['category'] = cat
    aux_df = df[df['genre'] == cat]
    counts = aux_df['target'].value_counts()
    aux_dict['count'] = sum(counts)
    for t in targets:
        aux_dict['target_' + str(t)] = counts[t]
    cat_list.append(aux_dict)
cat_list = pd.DataFrame(cat_list)
cat_list['genre_encoded_dumb'] = cat_list['target_1'] / cat_list['count']

Итак, в конце кадр данных cat_list будет выглядеть так:

Поскольку интересующей целью является значение «1», эта вероятность фактически представляет собой среднее значение цели с учетом категории. По этой причине этот метод целевого кодирования также называют «средним» кодированием.

Мы можем вычислить это среднее с помощью простой агрегации, тогда:

stats = df['target'].groupby(df['genre']).agg(['count', 'mean'])

Ну вот! Переменные и их соответствующее кодирование с помощью одной строки кода.

Мы можем заменить категории их закодированными значениями, как показано ниже:

Поскольку мы используем среднее целевое значение для каждой категории, этот подход действительно легко адаптировать и для регрессионных моделей.

Проблемы с этим подходом

Этот метод кодирования действительно прост и эффективен. Тем не менее, есть важные моменты, которые вы должны иметь в виду при его использовании.

Одним из действительно важных эффектов является Target Leakage. Используя вероятность цели для кодирования признаков, мы снабжаем их информацией о той самой переменной, которую пытаемся смоделировать. Это похоже на «обман», поскольку модель будет учиться на переменной, которая сама содержит цель.

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

Кроме того, использование среднего значения в качестве предиктора для всего распределения хорошо, но не идеально. Если бы все различные распределения и комбинации данных могли быть представлены одним средним значением, наша жизнь была бы намного проще.

Даже если среднее значение является хорошей сводкой, мы обучаем модели на части данных. Среднее значение этой дроби может не совпадать со средним значением всей совокупности (помните центральную предельную теорему?), поэтому кодирование может быть неверным. Если выборка достаточно отличается от генеральной совокупности, модель может даже превосходить обучающие данные.

Целевой кодировщик с предварительным сглаживанием

Мы можем использовать предварительное сглаживание, чтобы уменьшить эти нежелательные эффекты.

Идея проста. Предположим, у нас есть модель для прогнозирования качества книги в интернет-магазине. У нас может быть книга с 5 оценками, дающими оценку 9,8 из 10, но другие книги имеют средний балл 7. Этот эффект возникает из-за того, что мы используем среднее значение небольшой выборки, и он аналогичен проблеме, которую я изложил. выше.

Мы можем «сгладить» оценку этой книги с меньшим количеством оценок, учитывая также среднее значение всей совокупности книг.

Вернемся к нашему примеру: у нас есть 5 категорий для кодирования: документальная литература, романтика, драма, научная фантастика и фэнтези, и мы уже знаем, как использовать среднее значение каждой категории для ее кодирования. Теперь мы можем использовать среднее целевого значения по всем категориям, чтобы сгладить кодирование каждой категории.

Мы называем среднее значение цели априорной вероятностью p(t = 1), а кодирование может использовать параметр α изменяется от 0 до 1, чтобы сбалансировать это сглаживание.

Обычно мы получаем эту альфу из выражения:

Код для выполнения кодирования с предварительным сглаживанием:

smoothing_factor = 1.0 # The f of the smoothing factor equation 
min_samples_leaf = 1 # The k of the smoothing factor equation
prior = df['target'].mean()
smoove = 1 / (1 + np.exp(-(stats['count'] - min_samples_leaf) / smoothing_factor))
smoothing = prior * (1 - smoove) + stats['mean'] * smoove
encoded = pd.Series(smoothing, name = 'genre_encoded_complete')

Это было адаптировано из библиотеки category_encoders на основе sklearn. Мы также можем использовать библиотеку для кодирования без необходимости делать это вручную:

from category_encoders import TargetEncoder
encoder = TargetEncoder()
df['genre_encoded_sklearn'] = encoder.fit_transform(df['genre'], df['target'])

Результат всех методов ниже:

Мы видим, что хотя между методами со сглаживанием и без него есть некоторая разница, они все же очень близки.

Мультиклассовый подход

До сих пор я объяснял Target Encoder для двоичного классификатора, и легко понять, как мы можем адаптировать его и к регрессии. А как насчет многоклассовой классификации?

В приведенном ниже наборе данных, если мы просто возьмем среднее значение, будет учтено, что цель 2 в два раза больше цели 1. Кроме того, как бы мы взяли среднее значение, если цель также была категорией?

Результаты кодировок странные: категория Nonfiction была закодирована с условной вероятностью 1,00, что явно неверно, так как у нее могут быть все три цели.

Чтобы заставить Target Encoder работать с многоклассовой классификацией, нам нужно кодировать функции для каждой цели независимо. Итак, давайте рассчитаем апостериорные вероятности каждой цели для каждой категории.

categories = df['genre'].unique()
targets = df['target'].unique()
cat_list = []
for cat in categories:
    aux_dict = {}
    aux_dict['category'] = cat
    aux_df = df[df['genre'] == cat]
    counts = aux_df['target'].value_counts()
    aux_dict['count'] = sum(counts)
    for t in targets:
        aux_dict['target_' + str(t)] = counts[t] if t in counts.keys() else 0
    cat_list.append(aux_dict)
cat_list = pd.DataFrame(cat_list)
for t in targets:
    cat_list['genre_encoded_target_' + str(t)] = cat_list['target_' + str(t)] / cat_list['count']

Результат:

Все кодировки теперь правильно отражают апостериорные значения, как и ожидалось. Даже «Романтика» будет закодирована как «0» для цели «1», поскольку она никогда не появлялась для этой категории.

Приведенный выше код также будет работать с категориальной целью. Если мы изменим цель, используя строку:

df['target'] = df['target'].replace({0: 'apple', 1: "banana", 2: 'orange'})

Результат не будет отличаться:

Теперь, когда мы понимаем, что нужно сделать, чтобы использовать целевое кодирование в мультиклассовых задачах, легко создать простой код для использования объекта category_encoders.TargetEncoder в этом сценарии:

from category_encoders import TargetEncoder
targets = df['target'].unique()
for t in targets:
    target_aux = df['target'].apply(lambda x: 1 if x == t else 0)
    encoder = TargetEncoder()
    df['genre_encoded_sklearn_target_' + str(t)] = encoder.fit_transform(df['genre'], target_aux)

Легко сделать! И результат обоими методами достаточно хорош:

Если вы знакомы с One-Hot Encoding, то знаете, что теперь вы можете удалить любой из закодированных столбцов, чтобы избежать мультиколлинеарности.

Заключение

Категориальные переменные целевого кодирования решают проблему размерности, с которой мы сталкиваемся при использовании One-Hot Encoding, но этот подход необходимо использовать с осторожностью, чтобы избежать целевой утечки.

Вы должны использовать его на своих моделях и сравнить с другими кодировками, чтобы выбрать ту, которая лучше подходит для вашего случая.

Если вам нравится этот пост…

Поддержите меня чашечкой кофе!

И прочитайте этот замечательный пост



Рекомендации

https://contrib.scikit-learn.org/category_encoders/targetencoder.html

https://maxhalford.github.io/blog/target-encoding/

https://dl.acm.org/doi/10.1145/507533.507538