Понимание проектирования функций (часть 2)

Категориальные данные

Стратегии работы с дискретными категориальными данными

Вступление

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

Мотивация

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

Понимание категориальных данных

Давайте разберемся с категориальным представлением данных, прежде чем углубляться в стратегии разработки функций. Как правило, любой атрибут данных, который является категориальным по своей природе, представляет дискретные значения, которые принадлежат определенному конечному набору категорий или классов. Они также часто известны как классы или метки в контексте атрибутов или переменных, которые должны быть предсказаны моделью (широко известные как переменные ответа). Эти дискретные значения могут быть текстовыми или числовыми (или даже неструктурированными данными, такими как изображения!). Существует два основных класса категориальных данных: номинальные и порядковые.

В любом номинальном категориальном атрибуте данных отсутствует концепция упорядочивания значений этого атрибута. Рассмотрим простой пример категорий погоды, как показано на следующем рисунке. Мы видим, что у нас есть шесть основных классов или категорий в этом конкретном сценарии без какой-либо концепции или понятия порядка (ветреный не всегда бывает до солнечного, не меньше или больше, чем солнечно).

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

Порядковые категориальные атрибуты имеют некоторый смысл или понятие порядка среди своих значений. Например, посмотрите на следующий рисунок, на котором указаны размеры рубашек. Совершенно очевидно, что порядок или в данном случае размер имеет значение, когда речь идет о рубашках (S меньше, чем M, который меньше L и т. Д.).

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

Разработка функций на категориальных данных

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

import pandas as pd
import numpy as np

Преобразование номинальных атрибутов

Номинальные атрибуты состоят из дискретных категориальных значений без понятия или чувства порядка между ними. Идея здесь состоит в том, чтобы преобразовать эти атрибуты в более представительный числовой формат, который может быть легко понят нижестоящим кодом и конвейерами. Давайте посмотрим на новый набор данных о продажах видеоигр. Этот набор данных также доступен в Kaggle, а также в моем репозитории GitHub.

vg_df = pd.read_csv('datasets/vgsales.csv', encoding='utf-8')
vg_df[['Name', 'Platform', 'Year', 'Genre', 'Publisher']].iloc[1:7]

Давайте сосредоточимся на атрибуте видеоигры Genre, показанном в приведенном выше фрейме данных. Совершенно очевидно, что это номинальный категориальный атрибут, как и Publisher и Platform. Мы можем легко получить список уникальных жанров видеоигр следующим образом.

genres = np.unique(vg_df['Genre'])
genres
Output
------
array(['Action', 'Adventure', 'Fighting', 'Misc', 'Platform',  
       'Puzzle', 'Racing', 'Role-Playing', 'Shooter', 'Simulation',  
       'Sports', 'Strategy'], dtype=object)

Это говорит нам о том, что у нас есть 12 различных жанров видеоигр. Теперь мы можем сгенерировать схему кодирования меток для сопоставления каждой категории числовому значению, используя scikit-learn.

from sklearn.preprocessing import LabelEncoder
gle = LabelEncoder()
genre_labels = gle.fit_transform(vg_df['Genre'])
genre_mappings = {index: label for index, label in 
                  enumerate(gle.classes_)}
genre_mappings

Output
------
{0: 'Action', 1: 'Adventure', 2: 'Fighting', 3: 'Misc',
 4: 'Platform', 5: 'Puzzle', 6: 'Racing', 7: 'Role-Playing',
 8: 'Shooter', 9: 'Simulation', 10: 'Sports', 11: 'Strategy'}

Таким образом, была сгенерирована схема сопоставления, в которой каждое значение жанра сопоставляется с числом с помощью объекта LabelEncoder gle. Преобразованные метки сохраняются в значении genre_labels, которое мы можем записать обратно в наш фрейм данных.

vg_df['GenreLabel'] = genre_labels
vg_df[['Name', 'Platform', 'Year', 'Genre', 'GenreLabel']].iloc[1:7]

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

Преобразование порядковых атрибутов

Порядковые атрибуты - это категориальные атрибуты с определенным порядком значений. Давайте рассмотрим наш набор данных о покемонах, который мы использовали в части 1 этой серии. Давайте более подробно остановимся на атрибуте Generation.

poke_df = pd.read_csv('datasets/Pokemon.csv', encoding='utf-8')
poke_df = poke_df.sample(random_state=1, 
                         frac=1).reset_index(drop=True)
np.unique(poke_df['Generation'])
Output
------
array(['Gen 1', 'Gen 2', 'Gen 3', 'Gen 4', 'Gen 5', 'Gen 6'], 
         dtype=object)

Основываясь на приведенных выше результатах, мы видим, что всего существует 6 поколений, и каждый покемон обычно принадлежит к определенному поколению на основе видеоигр (когда они были выпущены), а также телесериал следует за аналогичный график. Этот атрибут обычно порядковый (здесь необходимы знания предметной области), потому что большинство покемонов, принадлежащих к поколению 1, были представлены в видеоиграх и телешоу раньше, чем поколение 2 и т. Д. . Поклонники могут посмотреть на следующий рисунок, чтобы запомнить некоторых популярных покемонов каждого поколения (мнения фанатов могут отличаться!).

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

gen_ord_map = {'Gen 1': 1, 'Gen 2': 2, 'Gen 3': 3, 
               'Gen 4': 4, 'Gen 5': 5, 'Gen 6': 6}
poke_df['GenerationLabel'] = poke_df['Generation'].map(gen_ord_map)
poke_df[['Name', 'Generation', 'GenerationLabel']].iloc[4:10]

Из приведенного выше кода совершенно очевидно, что функция map(…) из pandas весьма полезна для преобразования этой порядковой характеристики.

Кодирование категориальных атрибутов

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

Вы можете спросить, мы только что преобразовали категории в числовые метки в предыдущем разделе, зачем нам это сейчас? Причина довольно проста. Что касается жанров видеоигр, если мы напрямую введем атрибут GenreLabel в качестве функции в модели машинного обучения, он будет рассматривать его как непрерывное числовое значение функции мышления 10 (Спорт ) больше, чем 6 (Racing), но это бессмысленно, потому что жанр Sports определенно не больше и не меньше, чем Racing , это принципиально разные ценности или категории, которые нельзя сравнивать напрямую. Следовательно, нам нужен дополнительный уровень схем кодирования, где фиктивные функции создаются для каждого уникального значения или категории из всех отдельных категорий для каждого атрибута.

Самая горячая схема кодирования

Учитывая, что у нас есть числовое представление любого категориального атрибута с метками m (после преобразования), схема однократного кодирования кодирует или преобразует атрибут в m двоичных объектов, которые могут содержать только значение 1 или 0. Таким образом, каждое наблюдение в категориальном объекте преобразуется в вектор размера m только с одним из значений 1 (что указывает на то, что он активен). Давайте возьмем подмножество нашего набора данных Pokémon, отображающее два интересующих нас атрибута.

poke_df[['Name', 'Generation', 'Legendary']].iloc[4:10]

Интересующие нас атрибуты - это покемоны Generation и их Legendary статус. Первый шаг - преобразовать эти атрибуты в числовые представления на основе того, что мы узнали ранее.

from sklearn.preprocessing import OneHotEncoder, LabelEncoder
# transform and map pokemon generations
gen_le = LabelEncoder()
gen_labels = gen_le.fit_transform(poke_df['Generation'])
poke_df['Gen_Label'] = gen_labels
# transform and map pokemon legendary status
leg_le = LabelEncoder()
leg_labels = leg_le.fit_transform(poke_df['Legendary'])
poke_df['Lgnd_Label'] = leg_labels
poke_df_sub = poke_df[['Name', 'Generation', 'Gen_Label',  
                       'Legendary', 'Lgnd_Label']]
poke_df_sub.iloc[4:10]

Характеристики Gen_Label и Lgnd_Label теперь отображают числовые представления наших категориальных функций. Давайте теперь применим схему быстрого кодирования к этим функциям.

# encode generation labels using one-hot encoding scheme
gen_ohe = OneHotEncoder()
gen_feature_arr = gen_ohe.fit_transform(
                              poke_df[['Gen_Label']]).toarray()
gen_feature_labels = list(gen_le.classes_)
gen_features = pd.DataFrame(gen_feature_arr, 
                            columns=gen_feature_labels)
# encode legendary status labels using one-hot encoding scheme
leg_ohe = OneHotEncoder()
leg_feature_arr = leg_ohe.fit_transform(
                                poke_df[['Lgnd_Label']]).toarray()
leg_feature_labels = ['Legendary_'+str(cls_label) 
                           for cls_label in leg_le.classes_]
leg_features = pd.DataFrame(leg_feature_arr, 
                            columns=leg_feature_labels)

В общем, вы всегда можете кодировать обе функции вместе, используя функцию fit_transform(…), передав ей двухмерный массив двух функций вместе (ознакомьтесь с документацией!). Но мы кодируем каждую функцию отдельно, чтобы упростить понимание. Помимо этого, мы также можем создавать отдельные фреймы данных и соответствующим образом маркировать их. Давайте теперь объединим эти функциональные рамки и посмотрим на окончательный результат.

poke_df_ohe = pd.concat([poke_df_sub, gen_features, leg_features], axis=1)
columns = sum([['Name', 'Generation', 'Gen_Label'],   
               gen_feature_labels, ['Legendary', 'Lgnd_Label'], 
               leg_feature_labels], [])
poke_df_ohe[columns].iloc[4:10]

Таким образом, вы можете видеть, что были созданы 6 фиктивных переменных или двоичных функций для Generation и 2 для Legendary, поскольку это общее количество различных категорий в каждом из этих атрибутов соответственно. Активное состояние категории обозначается значением 1 в одной из этих фиктивных переменных, что хорошо видно из приведенного выше фрейма данных.

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

new_poke_df = pd.DataFrame([['PikaZoom', 'Gen 3', True], 
                           ['CharMyToast', 'Gen 4', False]],
                       columns=['Name', 'Generation', 'Legendary'])
new_poke_df

Здесь вы можете использовать scikit-learn’s отличный API, вызвав transform(…) функцию ранее построенных LabeLEncoder и OneHotEncoder объектов на новых данных. Помните наш рабочий процесс: сначала мы выполняем преобразование.

new_gen_labels = gen_le.transform(new_poke_df['Generation'])
new_poke_df['Gen_Label'] = new_gen_labels
new_leg_labels = leg_le.transform(new_poke_df['Legendary'])
new_poke_df['Lgnd_Label'] = new_leg_labels
new_poke_df[['Name', 'Generation', 'Gen_Label', 'Legendary', 
             'Lgnd_Label']]

Когда у нас есть числовые метки, давайте применим схему кодирования сейчас!

new_gen_feature_arr = gen_ohe.transform(new_poke_df[['Gen_Label']]).toarray()
new_gen_features = pd.DataFrame(new_gen_feature_arr, 
                                columns=gen_feature_labels)
new_leg_feature_arr = leg_ohe.transform(new_poke_df[['Lgnd_Label']]).toarray()
new_leg_features = pd.DataFrame(new_leg_feature_arr, 
                                columns=leg_feature_labels)
new_poke_ohe = pd.concat([new_poke_df, new_gen_features, new_leg_features], axis=1)
columns = sum([['Name', 'Generation', 'Gen_Label'], 
               gen_feature_labels,
               ['Legendary', 'Lgnd_Label'], leg_feature_labels], [])
new_poke_ohe[columns]

Таким образом, вы можете видеть, что эту схему довольно легко применить к новым данным, используя scikit-learn’s мощный API.

Вы также можете легко применить схему однократного кодирования, используя функцию to_dummies(…) из pandas.

gen_onehot_features = pd.get_dummies(poke_df['Generation'])
pd.concat([poke_df[['Name', 'Generation']], gen_onehot_features], 
           axis=1).iloc[4:10]

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

Схема фиктивного кодирования

Фиктивная схема кодирования аналогична схеме "горячего" кодирования, за исключением случая фиктивной схемы кодирования, когда она применяется к категориальному признаку с m различными метками, мы получаем m - 1 двоичная функция. Таким образом, каждое значение категориальной переменной преобразуется в вектор размером m - 1. Дополнительная функция полностью игнорируется, и поэтому, если значения категории находятся в диапазоне от {0, 1,…, m-1}, 0th или столбец характеристик m - 1, и соответствующие значения категорий обычно представлены вектором из всех нулей (0) . Давайте попробуем применить фиктивную схему кодирования к Pokémon Generation , отказавшись от функции двоичного кодирования первого уровня (Gen 1).

gen_dummy_features = pd.get_dummies(poke_df['Generation'], 
                                    drop_first=True)
pd.concat([poke_df[['Name', 'Generation']], gen_dummy_features], 
          axis=1).iloc[4:10]

Если вы хотите, вы также можете удалить функцию двоичного кодирования последнего уровня (Gen 6) следующим образом.

gen_onehot_features = pd.get_dummies(poke_df['Generation'])
gen_dummy_features = gen_onehot_features.iloc[:,:-1]
pd.concat([poke_df[['Name', 'Generation']], gen_dummy_features],  
          axis=1).iloc[4:10]

Основываясь на приведенных выше изображениях, совершенно ясно, что категории, принадлежащие удаленному объекту, представлены в виде вектора нулей (0), как мы обсуждали ранее.

Схема кодирования эффектов

Схема кодирования эффекта на самом деле очень похожа на фиктивную схему кодирования, за исключением того, что во время процесса кодирования кодированные признаки или вектор признаков для значений категорий, которые представляют все 0 в фиктивной схеме кодирования, заменяются на -1 в схеме кодирования эффекта. Это станет яснее на следующем примере.

gen_onehot_features = pd.get_dummies(poke_df['Generation'])
gen_effect_features = gen_onehot_features.iloc[:,:-1]
gen_effect_features.loc[np.all(gen_effect_features == 0, 
                               axis=1)] = -1.
pd.concat([poke_df[['Name', 'Generation']], gen_effect_features], 
          axis=1).iloc[4:10]

Приведенный выше вывод ясно показывает, что покемоны, принадлежащие Generation 6, теперь представлены вектором значений -1 по сравнению с нулями в фиктивном кодировании.

Схема подсчета корзин

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

Следовательно, нам нужно обратить внимание на другие схемы проектирования функций категориальных данных для функций, имеющих большое количество возможных категорий (например, IP-адреса). Схема подсчета ячеек является полезной схемой для работы с категориальными переменными, имеющими много категорий. В этой схеме вместо использования фактических значений меток для кодирования мы используем статистическую информацию, основанную на вероятности, о значении и фактическом целевом значении или значении ответа, которое мы стремимся предсказать в наших усилиях по моделированию. Простой пример будет основан на прошлых исторических данных для IP-адресов и тех, которые использовались в DDOS-атаках; мы можем построить значения вероятности для DDOS-атаки, вызванной любым из IP-адресов. Используя эту информацию, мы можем закодировать входную функцию, которая показывает, что, если тот же IP-адрес появится в будущем, какова вероятность возникновения DDOS-атаки. Эта схема требует исторических данных в качестве предварительного условия и является сложной. Изобразить это с полным примером здесь сейчас сложно, но в Интернете есть несколько ресурсов, на которые вы можете обратиться за тем же.

Схема хеширования функций

Схема хеширования функций - еще одна полезная схема разработки функций для работы с крупномасштабными категориальными функциями. В этой схеме обычно используется хеш-функция с заранее установленным количеством закодированных функций (как вектор заранее определенной длины), так что хешированные значения функций используются в качестве индексов в этом заранее определенном векторе, а значения обновлен соответственно. Поскольку хеш-функция отображает большое количество значений в небольшой конечный набор значений, несколько разных значений могут создавать один и тот же хэш, что называется коллизиями. Обычно используется хеш-функция со знаком, так что знак значения, полученного из хеш-функции, используется как знак значения, которое хранится в конечном векторе признаков по соответствующему индексу. Это должно гарантировать меньшее количество коллизий и меньшее накопление ошибок из-за коллизий.

Схемы хеширования работают со строками, числами и другими структурами, такими как векторы. Вы можете думать о хешированных выходных данных как о конечном наборе бункеров b, так что, когда хеш-функция применяется к одним и тем же значениям \ категориям, они назначаются одному и тому же бину (или подмножество ячеек) из ячеек b на основе хэш-значения. Мы можем предварительно определить значение b, которое становится окончательным размером закодированного вектора признаков для каждого категориального атрибута, который мы кодируем с помощью схемы хеширования признаков.

Таким образом, даже если у нас есть более 1000 различных категорий в объекте и мы устанавливаем b = 10 в качестве окончательного размера вектора объекта, выходной набор признаков по-прежнему будет иметь только 10 функций по сравнению с 1000 двоичных функций, если мы будем использовать схему однократного кодирования. Давайте рассмотрим атрибут Genre в нашем наборе данных видеоигр.

unique_genres = np.unique(vg_df[['Genre']])
print("Total game genres:", len(unique_genres))
print(unique_genres)
Output
------
Total game genres: 12
['Action' 'Adventure' 'Fighting' 'Misc' 'Platform' 'Puzzle' 'Racing'
 'Role-Playing' 'Shooter' 'Simulation' 'Sports' 'Strategy']

Мы видим, что всего существует 12 жанров видеоигр. Если бы мы использовали схему однократного кодирования для Genre feature, у нас было бы 12 двоичных функций. Вместо этого мы теперь будем использовать схему хеширования функций, используя класс scikit-learn’s FeatureHasher, который использует подписанную 32-битную версию хеш-функции Murmurhash3. В этом случае мы предварительно определим окончательный размер вектора объекта, равный 6.

from sklearn.feature_extraction import FeatureHasher
fh = FeatureHasher(n_features=6, input_type='string')
hashed_features = fh.fit_transform(vg_df['Genre'])
hashed_features = hashed_features.toarray()
pd.concat([vg_df[['Name', 'Genre']], pd.DataFrame(hashed_features)], 
          axis=1).iloc[1:7]

На основании вышеприведенных выходных данных атрибут Genre categorical был закодирован с использованием схемы хеширования в 6 функций вместо 12. Мы также видим, что строки 1 и 6 обозначают один и тот же жанр игр, Платформа, которые были правильно закодированы. в тот же вектор признаков.

Заключение

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

Следующим шагом будут стратегии разработки функций для неструктурированных текстовых данных. Будьте на связи!

Чтобы узнать о стратегиях разработки функций для непрерывных числовых данных, ознакомьтесь с частью 1 этой серии!

Весь код и наборы данных, используемые в этой статье, доступны на моем GitHub

Код также доступен как Блокнот Jupyter.