Глава 5. Обработка категориальных данных
5.0 Введение
Часто полезно измерять объекты не с точки зрения их количества, а с точки зрения некоторого качества. Мы часто представляем качественную информацию в виде категорий, таких как пол, цвета или марка автомобиля. Однако не все категориальные данные одинаковы. Наборы категорий без внутренней упорядоченности называются номинальными. Примеры номинальных категорий включают:
Синий, Красный, Зеленый
Мужчина, Женщина
Банан, Клубника, Яблоко
Напротив, когда набор категорий имеет некоторую естественную упорядоченность, мы называем его ординальным. Например:
Низкий, Средний, Высокий
Молодой, Старый
Согласен, Нейтрален, Не согласен
Кроме того, категориальная информация часто представлена в данных в виде вектора или столбца строк (например, "Maine", "Texas", "Delaware"). Проблема в том, что большинство алгоритмов машинного обучения требуют числовых значений в качестве входных данных.
Алгоритм k-ближайших соседей является примером алгоритма, который требует числовых данных. Один из шагов в алгоритме — вычисление расстояний между наблюдениями — часто с использованием Евклидова расстояния:
Рисунок: Формула Евклидова расстояния
где x и y — два наблюдения, а индекс i обозначает значение для i-го признака наблюдений. Однако вычисление расстояния, очевидно, невозможно, если значение xi является строкой (например, "Texas"). Вместо этого нам нужно преобразовать строку в некоторый числовой формат, чтобы ее можно было ввести в уравнение Евклидова расстояния. Наша цель — преобразовать данные таким образом, чтобы правильно отразить информацию в категориях (ординальность, относительные интервалы между категориями и т. д.). В этой главе мы рассмотрим методы выполнения этого преобразования, а также преодоления других проблем, часто возникающих при обработке категориальных данных.
5.1 Кодирование номинальных категориальных признаков
Проблема
У вас есть признак с номинальными классами, который не имеет внутренней упорядоченности (например, яблоко, груша, банан), и вы хотите закодировать этот признак в числовые значения.
Решение
Однократное кодирование признака с использованием LabelBinarizer scikit-learn:
# Import librariesimport numpy as npfrom sklearn.preprocessing import LabelBinarizer, MultiLabelBinarizerCreate featurefeature = np.array([["Texas"],["California"],["Texas"],["Delaware"],["Texas"]])Create one-hot encoderone_hot = LabelBinarizer()One-hot encode featureone_hot.fit_transform(feature)array([[0, 0, 1],[1, 0, 0],[0, 0, 1],[0, 1, 0],[0, 0, 1]])Мы можем использовать атрибут classes_ для вывода классов:
# View feature classesone_hot.classes_array(['California', 'Delaware', 'Texas'],dtype='Если мы хотим отменить однократное кодирование, мы можем использовать inverse_transform:
# Reverse one-hot encodingone_hot.inverse_transform(one_hot.transform(feature))array(['Texas', 'California', 'Texas', 'Delaware', 'Texas'],dtype='Мы даже можем использовать pandas для однократного кодирования признака:
# Import libraryimport pandas as pdCreate dummy variables from featurepd.get_dummies(feature[:,0])| California | Delaware | Texas | |
|---|---|---|---|
| 0 | 0 | 0 | 1 |
| 1 | 1 | 0 | 0 |
| 2 | 0 | 0 | 1 |
| 3 | 0 | 1 | 0 |
| 4 | 0 | 0 | 1 |
Одной из полезных возможностей scikit-learn является обработка ситуации, когда каждое наблюдение перечисляет несколько классов:
# Create multiclass featuremulticlass_feature = [("Texas", "Florida"),("California", "Alabama"),("Texas", "Florida"),("Delware", "Florida"),("Texas", "Alabama")]Create multiclass one-hot encoderone_hot_multiclass = MultiLabelBinarizer()One-hot encode multiclass featureone_hot_multiclass.fit_transform(multiclass_feature)array([[0, 0, 0, 1, 1],[1, 1, 0, 0, 0],[0, 0, 0, 1, 1],[0, 0, 1, 1, 0],[1, 0, 0, 0, 1]])Еще раз, мы можем увидеть классы с помощью метода classes_:
# View classesone_hot_multiclass.classes_array(['Alabama', 'California', 'Delware', 'Florida', 'Texas'], dtype=object)Обсуждение
ОбсуждениеМы можем подумать, что правильная стратегия — присвоить каждому классу числовое значение (например, Texas = 1, California = 2). Однако, когда наши классы не имеют внутренней упорядоченности (например, Texas не «меньше», чем California), наши числовые значения ошибочно создают упорядоченность, которой нет.
Правильная стратегия — создать бинарный признак для каждого класса в исходном признаке. Это часто называется однократным кодированием (one-hot encoding) в литературе по машинному обучению или даммированием (dummying) в статистической и исследовательской литературе. Признак в нашем решении был вектором, содержащим три класса (т.е. Texas, California и Delaware). При однократном кодировании каждый класс становится своим собственным признаком с 1, когда класс появляется, и 0 в противном случае. Поскольку наш признак имел три класса, однократное кодирование вернуло три бинарных признака (по одному для каждого класса). Используя однократное кодирование, мы можем зафиксировать принадлежность наблюдения к классу, сохраняя при этом представление о том, что класс не имеет какой-либо иерархии.
Наконец, часто рекомендуется после однократного кодирования признака удалить один из признаков, полученных при однократном кодировании, в результирующей матрице, чтобы избежать линейной зависимости.
См. также
См. такжеDummy Variable Trap, AlgosomeDropping one of the columns when using one-hot encoding, CrossValidated
5.2 Кодирование ординальных категориальных признаков
5.2 Кодирование ординальных категориальных признаковПроблема
ПроблемаУ вас есть ординальный категориальный признак (например, высокий, средний, низкий), и вы хотите преобразовать его в числовые значения.
Решение
РешениеИспользуйте метод replace pandas DataFrame для преобразования строковых меток в числовые эквиваленты:
# Load libraryimport pandas as pdCreate featuresdataframe = pd.DataFrame({"Score": ["Low", "Low", "Medium", "Medium", "High"]})Create mapperscale_mapper = {"Low":1,"Medium":2,"High":3}Replace feature values with scaledataframe["Score"].replace(scale_mapper)0 11 12 23 24 3Name: Score, dtype: int64Обсуждение
ОбсуждениеЧасто у нас есть признак с классами, которые имеют некоторую естественную упорядоченность. Известный пример — шкала Лайкерта:
Полностью согласенСогласенНейтраленНе согласенПолностью не согласен
При кодировании признака для использования в машинном обучении нам необходимо преобразовать ординальные классы в числовые значения, сохраняющие представление об упорядоченности. Самый распространенный подход — создать словарь, который сопоставляет строковую метку класса с числом, а затем применить эту карту к признаку.
Важно, чтобы наш выбор числовых значений основывался на нашей предварительной информации об ординальных классах. В нашем решении «высокий» буквально в три раза больше, чем «низкий». Это хорошо во многих случаях, но может сломаться, если предполагаемые интервалы между классами не равны:
dataframe = pd.DataFrame({"Score": ["Low","Low","Medium","Medium","High","Barely More Than Medium"]})scale_mapper = {"Low":1,"Medium":2,"Barely More Than Medium": 3,"High":4}dataframe["Score"].replace(scale_mapper)0 11 12 23 24 45 3Name: Score, dtype: int64В этом примере расстояние между «Низким» и «Средним» такое же, как и расстояние между «Средним» и «Едва больше, чем средний», что почти наверняка не точно. Лучший подход — осознанно подходить к числовым значениям, сопоставленным с классами:
scale_mapper = {"Low":1,"Medium":2,"Barely More Than Medium": 2.1,"High":3}dataframe["Score"].replace(scale_mapper)0 1.01 1.02 2.03 2.04 3.05 2.1Name: Score, dtype: float645.3 Кодирование словарей признаков
5.3 Кодирование словарей признаковПроблема
ПроблемаУ вас есть словарь, и вы хотите преобразовать его в матрицу признаков.
Решение
РешениеИспользуйте DictVectorizer:
# Import libraryfrom sklearn.feature_extraction import DictVectorizerCreate dictionarydata_dict = [{"Red": 2, "Blue": 4},{"Red": 4, "Blue": 3},{"Red": 1, "Yellow": 2},{"Red": 2, "Yellow": 2}]Create dictionary vectorizerdictvectorizer = DictVectorizer(sparse=False)Convert dictionary to feature matrixfeatures = dictvectorizer.fit_transform(data_dict)View feature matrixfeaturesarray([[ 4., 2., 0.],[ 3., 4., 0.],[ 0., 1., 2.],[ 0., 2., 2.]])По умолчанию DictVectorizer выводит разреженную матрицу, которая хранит только элементы со значением, отличным от 0. Это может быть очень полезно, когда у нас есть массивные матрицы (часто встречающиеся в обработке естественного языка), и мы хотим минимизировать требования к памяти. Мы можем заставить DictVectorizer выводить плотную матрицу, используя sparse=False.
Мы можем получить имена каждого сгенерированного признака с помощью метода get_feature_names:
# Get feature namesfeature_names = dictvectorizer.get_feature_names()View feature namesfeature_names['Blue', 'Red', 'Yellow']Хотя это не обязательно, для иллюстрации мы можем создать pandas DataFrame для лучшего просмотра вывода:
# Import libraryimport pandas as pdCreate dataframe from featurespd.DataFrame(features, columns=feature_names)| Blue | Red | Yellow | |
|---|---|---|---|
| 0 | 4.0 | 2.0 | 0.0 |
| 1 | 3.0 | 4.0 | 0.0 |
| 2 | 0.0 | 1.0 | 2.0 |
| 3 | 0.0 | 2.0 | 2.0 |
Обсуждение
ОбсуждениеСловарь является популярной структурой данных, используемой многими языками программирования; однако алгоритмы машинного обучения ожидают данные в виде матрицы. Мы можем добиться этого с помощью DictVectorizer scikit-learn.
Это распространенная ситуация при работе с обработкой естественного языка. Например, у нас может быть коллекция документов, и для каждого документа у нас есть словарь, содержащий количество раз, когда каждое слово появляется в документе. Используя DictVectorizer, мы можем легко создать матрицу признаков, где каждый признак — это количество раз, когда слово появляется в каждом документе:
# Create word counts dictionaries for four documentsdoc_1_word_count = {"Red": 2, "Blue": 4}doc_2_word_count = {"Red": 4, "Blue": 3}doc_3_word_count = {"Red": 1, "Yellow": 2}doc_4_word_count = {"Red": 2, "Yellow": 2}Create listdoc_word_counts = [doc_1_word_count,doc_2_word_count,doc_3_word_count,doc_4_word_count]Convert list of word count dictionaries into feature matrixdictvectorizer.fit_transform(doc_word_counts)array([[ 4., 2., 0.],[ 3., 4., 0.],[ 0., 1., 2.],[ 0., 2., 2.]])В нашем игрушечном примере есть только три уникальных слова (Red, Yellow, Blue), поэтому в нашей матрице только три признака; однако вы можете представить, что если каждый документ на самом деле был бы книгой в университетской библиотеке, наша матрица признаков была бы очень большой (и тогда мы захотели бы установить sparse в True).
См. также
См. такжеHow to use dictionaries in PythonSciPy Sparse Matrices
5.4 Импутация пропущенных значений класса
5.4 Импутация пропущенных значений классаПроблема
ПроблемаУ вас есть категориальный признак, содержащий пропущенные значения, которые вы хотите заменить предсказанными значениями.
Решение
РешениеИдеальное решение — обучить алгоритм классификатора машинного обучения для предсказания пропущенных значений, обычно классификатор k-ближайших соседей (KNN):
# Load librariesimport numpy as npfrom sklearn.neighbors import KNeighborsClassifierfrom sklearn.impute import SimpleImputerfrom sklearn.preprocessing import StandardScalerfrom sklearn.datasets import make_blobsCreate feature matrix with categorical featureX = np.array([[0, 2.10, 1.45],[1, 1.18, 1.33],[0, 1.22, 1.27],[1, -0.21, -1.19]])Create feature matrix with missing values in the categorical featureX_with_nan = np.array([[np.nan, 0.87, 1.31],[np.nan, -0.67, -0.22]])Train KNN learnerclf = KNeighborsClassifier(3, weights='distance')trained_model = clf.fit(X[:,1:], X[:,0])Predict missing values' classimputed_values = trained_model.predict(X_with_nan[:,1:])Join column of predicted class with their other featuresX_with_imputed = np.hstack((imputed_values.reshape(-1,1), X_with_nan[:,1:]))Join two feature matricesnp.vstack((X_with_imputed, X))array([[ 0. , 0.87, 1.31],[ 1. , -0.67, -0.22],[ 0. , 2.1 , 1.45],[ 1. , 1.18, 1.33],[ 0. , 1.22, 1.27],[ 1. , -0.21, -1.19]])Альтернативное решение — заполнить пропущенные значения наиболее частым значением признака:
from sklearn.impute import SimpleImputerJoin the two feature matricesX_complete = np.vstack((X_with_nan, X))imputer = SimpleImputer(strategy='most_frequent')imputer.fit_transform(X_complete)array([[ 0. , 0.87, 1.31],[ 0. , -0.67, -0.22],[ 0. , 2.1 , 1.45],[ 1. , 1.18, 1.33],[ 0. , 1.22, 1.27],[ 1. , -0.21, -1.19]])Обсуждение
ОбсуждениеКогда у нас есть пропущенные значения в категориальном признаке, наше лучшее решение — открыть наш набор инструментов алгоритмов машинного обучения для предсказания значений пропущенных наблюдений. Мы можем добиться этого, рассматривая признак с пропущенными значениями как целевой вектор, а остальные признаки — как матрицу признаков. Часто используемый алгоритм — KNN (подробно рассмотренный далее в этой книге), который присваивает пропущенному значению наиболее частый класс из k ближайших наблюдений.
Альтернативно, мы можем заполнить пропущенные значения наиболее частым классом признака. Хотя это менее изощренный метод, чем KNN, он гораздо более масштабируем для больших данных. В любом случае, рекомендуется включить бинарный признак, указывающий, содержат ли наблюдения импутированные значения.
См. также
См. такжеImputation of missing values with scikit-learnOvercoming Missing Values in a Random Forest ClassifierA Study of K-Nearest Neighbour as an Imputation Method
5.5 Обработка несбалансированных классов
5.5 Обработка несбалансированных классовПроблема
ПроблемаУ вас есть целевой вектор с сильно несбалансированными классами, и вы хотите внести коррективы, чтобы справиться с несбалансированностью классов.
Решение
РешениеСоберите больше данных. Если это невозможно, измените метрики, используемые для оценки вашей модели. Если это не работает, рассмотрите возможность использования встроенных параметров веса класса модели (если доступны), даунсэмплинга или апсэмплинга. Мы рассмотрим метрики оценки в более поздней главе, поэтому пока сосредоточимся на параметрах веса класса, даунсэмплинге и апсэмплинге.
Чтобы продемонстрировать наши решения, нам нужно создать некоторые данные с несбалансированными классами. Набор данных Ириса Фишера содержит три сбалансированных класса по 50 наблюдений каждый, указывающих вид цветка (Iris setosa, Iris virginica и Iris versicolor). Чтобы сделать набор данных несбалансированным, мы удаляем 40 из 50 наблюдений Iris setosa, а затем объединяем классы Iris virginica и Iris versicolor. Конечным результатом является бинарный целевой вектор, указывающий, является ли наблюдение цветком Iris setosa или нет. Результат — 10 наблюдений Iris setosa (класс 0) и 100 наблюдений не Iris setosa (класс 1):
# Load librariesimport numpy as npfrom sklearn.ensemble import RandomForestClassifierfrom sklearn.datasets import load_irisLoad iris datairis = load_iris()Create feature matrixfeatures = iris.dataCreate target vectortarget = iris.targetRemove first 40 observationsfeatures = features[40:,:]target = target[40:]Create binary target vector indicating if class 0target = np.where((target == 0), 0, 1)Look at the imbalanced target vectortargetarray([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1])Многие алгоритмы в scikit-learn предлагают параметр для взвешивания классов во время обучения, чтобы противодействовать эффекту их несбалансированности. Хотя мы еще не рассмотрели его, RandomForestClassifier — популярный алгоритм классификации, который включает параметр class_weight. Вы можете передать аргумент, явно указывающий желаемые веса классов:
# Create weightsweights = {0: .9, 1: 0.1}Create random forest classifier with weightsRandomForestClassifier(class_weight=weights)RandomForestClassifier(class_weight={0: 0.9, 1: 0.1})Или вы можете передать balanced, который автоматически создает веса, обратно пропорциональные частотам классов:
# Train a random forest with balanced class weightsRandomForestClassifier(class_weight="balanced")RandomForestClassifier(class_weight='balanced')Альтернативно, мы можем снизить выборку (downsample) мажоритарного класса или увеличить выборку (upsample) миноритарного класса. При снижении выборки мы случайно выбираем без замены из мажоритарного класса (т.е. класса с большим количеством наблюдений), чтобы создать новое подмножество наблюдений, равное по размеру миноритарному классу. Например, если миноритарный класс имеет 10 наблюдений, мы случайно выберем 10 наблюдений из мажоритарного класса и используем эти 20 наблюдений в качестве наших данных. Здесь мы делаем именно это, используя наши несбалансированные данные Ириса:
# Indicies of each class' observationsi_class0 = np.where(target == 0)[0]i_class1 = np.where(target == 1)[0]Number of observations in each classn_class0 = len(i_class0)n_class1 = len(i_class1)For every observation of class 0, randomly samplefrom class 1 without replacementi_class1_downsampled = np.random.choice(i_class1, size=n_class0, replace=False)Join together class 0's target vector with thedownsampled class 1's target vectornp.hstack((target[i_class0], target[i_class1_downsampled]))array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])# Join together class 0's upsampled feature matrix with class 1's feature matrixnp.vstack((features[i_class0_upsampled,:], features[i_class1,:]))[0:5]array([[ 5. , 3.5, 1.6, 0.6],[ 5. , 3.5, 1.6, 0.6],[ 5. , 3.3, 1.4, 0.2],[ 4.5, 2.3, 1.3, 0.3],[ 4.8, 3. , 1.4, 0.3]])Другой вариант — увеличить выборку миноритарного класса. При увеличении выборки для каждого наблюдения в мажоритарном классе мы случайно выбираем наблюдение из миноритарного класса с заменой. Конечным результатом является одинаковое количество наблюдений из миноритарного и мажоритарного классов. Увеличение выборки реализуется очень похоже на снижение выборки, только наоборот:
# For every observation in class 1, randomly sample from class 0 with replacementi_class0_upsampled = np.random.choice(i_class0, size=n_class1, replace=True)Join together class 0's upsampled target vector with class 1's target vectornp.concatenate((target[i_class0_upsampled], target[i_class1]))array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])# Join together class 0's upsampled feature matrix with class 1's feature matrixnp.vstack((features[i_class0_upsampled,:], features[i_class1,:]))[0:5]array([[ 5. , 3.5, 1.6, 0.6],[ 5. , 3.5, 1.6, 0.6],[ 5. , 3.3, 1.4, 0.2],[ 4.5, 2.3, 1.3, 0.3],[ 4.8, 3. , 1.4, 0.3]])Обсуждение
ОбсуждениеВ реальном мире несбалансированные классы встречаются повсеместно — большинство посетителей не нажимают кнопку покупки, и многие типы рака, к счастью, редки. По этой причине обработка несбалансированных классов является распространенной задачей в машинном обучении.
Наша лучшая стратегия — просто собрать больше наблюдений — особенно наблюдений миноритарного класса. Однако это часто невозможно, поэтому нам приходится прибегать к другим вариантам.
Вторая стратегия — использовать метрику оценки модели, лучше подходящую для несбалансированных классов. Точность часто используется в качестве метрики для оценки производительности модели, но при наличии несбалансированных классов точность может быть плохо подходящей. Например, если только у 0.5% наблюдений есть редкий рак, то даже наивная модель, которая предсказывает, что ни у кого нет рака, будет иметь точность 99.5%. Ясно, что это не идеальный вариант. Некоторые лучшие метрики, которые мы обсудим в более поздних главах, — это матрицы ошибок, точность (precision), полнота (recall), F1-метрика и ROC-кривые.
Третья стратегия — использовать параметры взвешивания классов, включенные в реализации некоторых моделей. Это позволяет алгоритму корректировать несбалансированные классы. К счастью, многие классификаторы scikit-learn имеют параметр class_weight, что делает его хорошим вариантом.
Четвертая и пятая стратегии связаны: снижение выборки (downsampling) и увеличение выборки (upsampling). При снижении выборки мы создаем случайное подмножество мажоритарного класса, равное по размеру миноритарному классу. При увеличении выборки мы многократно выбираем с заменой из миноритарного класса, чтобы сделать его равным по размеру мажоритарному классу. Решение между использованием снижения выборки и увеличения выборки зависит от контекста, и в целом мы должны попробовать оба варианта, чтобы увидеть, какой дает лучшие результаты.
Другие статьи по этой теме:
- Маленькая книга о глубоком обучении
- 100 Страниц о машинном обучении
- Математика для нейронного обучения