Учебное пособие о том, как использовать наиболее распространенные методы выбора признаков для задач классификации.

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

TL; DR - Сводная таблица

Основные методы кратко изложены в таблице ниже и обсуждаются в следующих разделах.

Что такое выбор функций и почему он полезен?

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

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

Неконтролируемые методы

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

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

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

from sklearn.datasets import load_iris
from sklearn.decomposition import PCA
from sklearn.svm import SVC
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
import numpy as np
h = .01
x_min, x_max = -4,4
y_min, y_max = -1.5,1.5
# loading dataset
data = load_iris()
X, y = data.data, data.target
# selecting first 2 components of PCA
X_pca = PCA().fit_transform(X)
X_selected = X_pca[:,:2]
# training classifier and evaluating on the whole plane
clf = SVC(kernel='linear')
clf.fit(X_selected,y)
xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
                     np.arange(y_min, y_max, h))
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
# Plotting
cmap_light = ListedColormap(['#FFAAAA', '#AAFFAA', '#AAAAFF'])
cmap_bold = ListedColormap(['#FF0000', '#00FF00', '#0000FF'])
plt.figure(figsize=(10,5))
plt.pcolormesh(xx, yy, Z, alpha=.6,cmap=cmap_light)
plt.title('PCA - Iris dataset')
plt.xlabel('Dimension 1')
plt.ylabel('Dimension 2')
plt.scatter(X_pca[:,0],X_pca[:,1],c=data.target,cmap=cmap_bold)
plt.show()

Еще одно использование уменьшения размерности в контексте оценки функций - для визуализации: в пространстве с меньшей размерностью легче визуально проверить, являются ли данные потенциально разделимыми, что помогает установить ожидания в отношении точности классификации. На практике мы выполняем уменьшение размерности (например, PCA) над подмножеством функций и проверяем, как метки распределяются в уменьшенном пространстве. Если они кажутся отдельными, это явный признак того, что при использовании этого набора функций ожидается высокая эффективность классификации.

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

from sklearn.datasets import load_iris
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
from mlxtend.plotting import plot_pca_correlation_graph
data = load_iris()
X, y = data.data, data.target
plt.figure(figsize=(10,5))
X_pca = PCA().fit_transform(X)
plt.title('PCA - Iris dataset')
plt.xlabel('Dimension 1')
plt.ylabel('Dimension 2')
plt.scatter(X_pca[:,0],X_pca[:,1],c=data.target)
_ = plot_pca_correlation_graph(X,data.feature_names)

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

Одномерные методы фильтрации

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

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

Баллы обычно измеряют либо зависимость между зависимой переменной и характеристиками (например, Chi2 и, для регрессии, коэффициент корреляции Перлса), либо разницу между распределениями функций с учетом метки класса (F-тест и T-тест).

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

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

Общие тесты включают:

В пакете sklearn реализованы некоторые методы фильтрации. Однако, поскольку большинство из них основано на статистических тестах, также можно использовать статистические пакеты (такие какstatsmodels).

Один пример показан ниже:

from sklearn.feature_selection import f_classif, chi2, mutual_info_classif
from statsmodels.stats.multicomp import pairwise_tukeyhsd
from sklearn.datasets import load_iris
data = load_iris()
X,y = data.data, data.target
chi2_score, chi_2_p_value = chi2(X,y)
f_score, f_p_value = f_classif(X,y)
mut_info_score = mutual_info_classif(X,y)
pairwise_tukeyhsd = [list(pairwise_tukeyhsd(X[:,i],y).reject) for i in range(4)]
print('chi2 score        ', chi2_score)
print('chi2 p-value      ', chi_2_p_value)
print('F - score score   ', f_score)
print('F - score p-value ', f_p_value)
print('mutual info       ', mut_info_score)
print('pairwise_tukeyhsd',pairwise_tukeyhsd)
Out:
chi2 score         [ 10.82   3.71 116.31  67.05]
chi2 p-value       [0.   0.16 0.   0.  ]
F - score score    [ 119.26   49.16 1180.16  960.01]
F - score p-value  [0. 0. 0. 0.]
mutual info        [0.51 0.27 0.98 0.98]
pairwise_tukeyhsd [[True, True, True], [True, True, True], [True, True, True], [True, True, True]]

Визуальные способы ранжирования функций

Коробчатые сюжеты и сюжеты для скрипки

Графики Boxplots / Violin могут помочь визуализировать распределение функции с учетом класса. Для набора данных Iris ниже показан пример.

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

import pandas as pd
import seaborn as sns
sns.set()
df = pd.DataFrame(data.data,columns=data.feature_names)
df['target'] = data.target
df_temp = pd.melt(df,id_vars='target',value_vars=list(df.columns)[:-1], 
                  var_name="Feature", value_name="Value")
g = sns.FacetGrid(data = df_temp, col="Feature", col_wrap=4, size=4.5,sharey = False)
g.map(sns.boxplot,"target", "Value");
g = sns.FacetGrid(data = df_temp, col="Feature", col_wrap=4, size=4.5,sharey = False)
g.map(sns.violinplot,"target", "Value");

Рейтинг характеристик с кривой ROC

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

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

В приведенном ниже примере показана кривая ROC для различных функций.

from sklearn.datasets import load_iris
import matplotlib.pyplot as plt
from sklearn.metrics import auc
import numpy as np
# loading dataset
data = load_iris()
X, y = data.data, data.target
y_ = y == 2
plt.figure(figsize=(13,7))
for col in range(X.shape[1]):
    tpr,fpr = [],[]
    for threshold in np.linspace(min(X[:,col]),max(X[:,col]),100):
        detP = X[:,col] < threshold
        tpr.append(sum(detP & y_)/sum(y_))# TP/P, aka recall
        fpr.append(sum(detP & (~y_))/sum((~y_)))# FP/N
        
    if auc(fpr,tpr) < .5:
        aux = tpr
        tpr = fpr
        fpr = aux
    plt.plot(fpr,tpr,label=data.feature_names[col] + ', auc = '\
                           + str(np.round(auc(fpr,tpr),decimals=3)))
plt.title('ROC curve - Iris features')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.legend()
plt.show()

Методы многомерного фильтра

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

mRMR

mRMR (минимальная избыточность и максимальная релевантность) - это эвристический алгоритм для поиска близкого к оптимальному подмножества функций с учетом как важности функций, так и корреляции между ними.

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

В наборе S из N функций релевантность функций (D) вычисляется следующим образом:

где I - оператор взаимной информации.

Избыточность функций обозначается следующим образом:

Оценка mRMR для набора S определяется как (D - R). Цель состоит в том, чтобы найти подмножество функций с максимальным значением (D-R). Однако на практике мы выполняем инкрементный поиск (также известный как прямой выбор), в котором на каждом этапе мы добавляем функцию, которая дает наибольшую mRMR.

Алгоритм реализован на языке C самими авторами алгоритма. Вы можете найти исходный код пакета, а также оригинал статьи здесь.

Оболочка Python (не поддерживаемая) была создана с именем pymrmr. В случае проблем с pymrmr, я советую вызывать функцию уровня C.

Приведенный ниже код иллюстрирует использование pymrmr. Обратите внимание, что столбцы pandas фрейма данных должны быть отформатированы, как описано в пакете уровня C (здесь).

import pandas as pd
import pymrmr
df = pd.read_csv('some_df.csv')
# Pass a dataframe with a predetermined configuration. 
# Check http://home.penglab.com/proj/mRMR/ for the dataset requirements
pymrmr.mRMR(df, 'MIQ', 10)

Выход:

*** This program and the respective minimum Redundancy Maximum Relevance (mRMR)
     algorithm were developed by Hanchuan Peng <[email protected]>for
     the paper
     "Feature selection based on mutual information: criteria of
      max-dependency, max-relevance, and min-redundancy,"
      Hanchuan Peng, Fuhui Long, and Chris Ding,
      IEEE Transactions on Pattern Analysis and Machine Intelligence,
      Vol. 27, No. 8, pp.1226-1238, 2005.
*** MaxRel features ***
 Order    Fea     Name    Score
 1        765     v765    0.375
 2        1423    v1423   0.337
 3        513     v513    0.321
 4        249     v249    0.309
 5        267     v267    0.304
 6        245     v245    0.304
 7        1582    v1582   0.280
 8        897     v897    0.269
 9        1771    v1771   0.269
 10       1772    v1772   0.269
*** mRMR features ***
 Order    Fea     Name    Score
 1        765     v765    0.375
 2        1123    v1123   24.913
 3        1772    v1772   3.984
 4        286     v286    2.280
 5        467     v467    1.979
 6        377     v377    1.768
 7        513     v513    1.803
 8        1325    v1325   1.634
 9        1972    v1972   1.741
 10       1412    v1412   1.689
Out[1]:
 ['v765',
  'v1123',
  'v1772',
  'v286',
  'v467',
  'v377',
  'v513',
  'v1325',
  'v1972',
  'v1412']

Методы обертки

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

  1. Выберите показатель производительности (вероятность, AIC, BIC, оценка F1, точность, MSE, MAE…), обозначенный как M.
  2. Выберите классификатор / регрессор /…, обозначенный здесь как C.
  3. Поиск различных подмножеств функций с помощью заданного метода поиска. Для каждого подмножества S выполните следующие действия:
  • Обучите и протестируйте C по схеме перекрестной проверки, используя S в качестве функций классификатора;
  • Получите средний балл от процедуры перекрестной проверки (для показателя M) и присвойте этот балл подмножеству S;
  • Выберите новое подмножество и повторите шаг a.

Детализация Шаг 3

На третьем шаге не указывается, какой метод поиска будет использоваться. Тестирование всех возможных подмножеств функций недопустимо (выбор грубой силы) практически в любой ситуации, поскольку для этого потребуется выполнить шаг 3 экспоненциальное количество раз (2 в степени количества функций). Помимо временной сложности, при таком большом количестве возможностей вполне вероятно, что определенная комбинация функций работает лучше всего просто случайно, что делает решение методом грубой силы более склонным к переобучению.

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

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

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

Выбор вперед / назад по-прежнему подвержен переобучению, поскольку обычно оценки имеют тенденцию улучшаться за счет добавления дополнительных функций. Один из способов избежать такой ситуации - использовать оценки, снижающие сложность модели, например AIC или BIC.

Иллюстрация структуры метода оболочки показана ниже. Важно отметить, что набор функций (1) обнаруживается с помощью метода поиска и (2) перекрестно проверяется на том же классификаторе, для которого он предназначен.

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

Пакет Python для методов оболочки

mlxtend (http://rasbt.github.io/mlxtend/) - полезный пакет для различных задач, связанных с наукой о данных. Методы оболочки в этом пакете можно найти в SequentialFeatureSelector. Он обеспечивает выбор функций вперед и назад с некоторыми вариациями.

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

Пример ниже был извлечен с главной страницы пакета.

from mlxtend.feature_selection import SequentialFeatureSelector as SFS
from mlxtend.plotting import plot_sequential_feature_selection as plot_sfs
from sklearn.linear_model import LinearRegression
from sklearn.datasets import load_boston
boston = load_boston()
X, y = boston.data, boston.target
lr = LinearRegression()
sfs = SFS(lr, 
          k_features=13, 
          forward=True, 
          floating=False, 
          scoring='neg_mean_squared_error',
          cv=10)
sfs = sfs.fit(X, y)
fig = plot_sfs(sfs.get_metric_dict(), kind='std_err')
plt.title('Sequential Forward Selection (w. StdErr)')
plt.grid()
plt.show()

Встроенные методы

Обучение классификатора сводится к задаче оптимизации, в которой мы пытаемся минимизировать функцию его параметров (отмеченных здесь как). Эта функция известна как функция потерь (обозначена как 𝐿 (𝜃)).

В более общем контексте мы обычно хотим минимизировать целевую функцию, которая учитывает как функцию потерь, так и штраф (или регуляризация) (Ω (𝜃)) сложности модели:

obj (𝜃) = 𝐿 (𝜃) + Ω (𝜃)

Встроенные методы для линейных классификаторов

Для линейных классификаторов (например, линейная SVM, логистическая регрессия) функция потерь обозначается как:

Где каждый соответствует одной выборке данных, а Wᵀxʲ обозначает внутреннее произведение вектора коэффициентов (w₁, w₂,… w_n) с характеристиками в каждом образце.

Для линейной SVM и логистической регрессии шарнирные и логистические потери соответственно:

Двумя наиболее распространенными штрафами для линейных классификаторов являются штрафы L-1 и L-2:

Чем выше значение λ, тем сильнее штраф, и оптимальная целевая функция будет иметь тенденцию к сужению все больше и больше коэффициентов w_i. .

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

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

Для некоторых линейных классификаторов (линейная SVM, логистическая регрессия) можно эффективно использовать штраф L-1, что означает, что существуют эффективные численные методы для оптимизации конечной целевой функции. То же самое не относится к нескольким другим классификаторам (различные методы SVM ядра, деревья решений и т. Д.). Поэтому для разных классификаторов следует использовать разные методы регуляризации.

Пример логистической регрессии с регуляризацией показан ниже, и мы видим, что алгоритмы исключают некоторые из функций по мере уменьшения C (подумайте, если C как 1 / λ) .

import numpy as np
import matplotlib.pyplot as plt
from sklearn.svm import LinearSVC
from sklearn.model_selection import ShuffleSplit
from sklearn.model_selection import GridSearchCV
from sklearn.utils import check_random_state
from sklearn import datasets
from sklearn.linear_model import LogisticRegression
rnd = check_random_state(1)
# set up dataset
n_samples = 3000
n_features = 15
# l1 data (only 5 informative features)
X, y = datasets.make_classification(n_samples=n_samples,
                                        n_features=n_features, n_informative=5,
                                        random_state=1)
cs = np.logspace(-2.3, 0, 50)
coefs = []
for c in cs:
    clf = LogisticRegression(solver='liblinear',C=c,penalty='l1')
    # clf = LinearSVC(C=c,penalty='l1', loss='squared_hinge', dual=False, tol=1e-3)
    
    clf.fit(X,y)
    coefs.append(list(clf.coef_[0]))
    
coefs = np.array(coefs)
plt.figure(figsize=(10,5))
for i,col in enumerate(range(n_features)):
    plt.plot(cs,coefs[:,col])
plt.xscale('log')
plt.title('L1 penalty - Logistic regression')
plt.xlabel('C')
plt.ylabel('Coefficient value')
plt.show()

Важность функций из древовидных моделей

Другой распространенный метод выбора признаков состоит в извлечении ранга важности признаков из базовых моделей дерева.

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

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

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

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

from sklearn.datasets import load_boston
from sklearn.ensemble import RandomForestRegressor
import numpy as np
boston = load_boston()
X = boston.data
Y = boston.target
feat_names = boston.feature_names 
rf = RandomForestRegressor()
rf.fit(X, Y)
print("Features sorted by their score:")
print(sorted(zip(map(lambda x: round(x, 4), rf.feature_importances_), feat_names), 
             reverse=True))
Out:
Features sorted by their score:
[(0.4334, 'LSTAT'), (0.3709, 'RM'), (0.0805, 'DIS'), (0.0314, 'CRIM'), (0.0225, 'NOX'), (0.0154, 'TAX'), (0.0133, 'PTRATIO'), (0.0115, 'AGE'), (0.011, 'B'), (0.0043, 'INDUS'), (0.0032, 'RAD'), (0.0016, 'CHAS'), (0.0009, 'ZN')]

Дополнительно: основные оценки примесей для моделей деревьев

Как объяснялось выше, «примесь» - это оценка, используемая алгоритмом дерева решений при принятии решения о разделении узла. Существует множество алгоритмов дерева решений (IDR3, C4.5, CART,…), но общее правило состоит в том, что переменная, с помощью которой мы разбиваем узел в дереве, является той, которая дает наибольшее улучшение примеси.

Наиболее распространенные примеси - примеси Джини и энтропия. Улучшение примеси Джини известно как «важность Джини», в то время как улучшение энтропии - это получение информации.

SHAP: надежные значения функций из древовидных моделей

(Спасибо Энрике Гаспарини Фиуза ду Насименто за предложение!)

SHAP - это нечто большее, чем просто это. Это алгоритм, обеспечивающий объяснение модели на основе любой прогнозной модели. Однако для древовидных моделей это особенно полезно: авторы разработали высокоскоростное и точное (не только локальное) объяснение таких моделей, совместимое с X GBoost, LightGBM , модели деревьев CatBoost и scikit-learn.

Я рекомендую проверить возможности объяснения, предоставляемые SHAP (например, зависимость функций, эффекты взаимодействия, мониторинг модели…). Ниже я изображаю (только) значения функций, выводимые SHAP, которые более надежны, чем вывод исходной древовидной модели, при ранжировании их для выбора функции. Этот пример был извлечен с их страницы github.

import xgboost
import shap
# load JS visualization code to notebook
shap.initjs()
# train XGBoost model
X,y = shap.datasets.boston()
model = xgboost.train({"learning_rate": 0.01}, xgboost.DMatrix(X, label=y), 100)
# explain the model's predictions using SHAP values
# (same syntax works for LightGBM, CatBoost, and scikit-learn models)
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X)
shap.summary_plot(shap_values, X, plot_type="bar")

Заключение - когда использовать каждый метод?

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

Методы обертки, как правило, очень хорошо работают на практике. Однако они требуют больших вычислительных ресурсов, особенно при работе с сотнями функций. Но если у вас есть вычислительные ресурсы, они - отличный вариант.

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

использованная литература

Введение в выбор переменных и функций

Смещение в мерах важности случайных лесных переменных: иллюстрации, источники и решение

Выбор характеристик для классификации: обзор