Решая ту или иную задачу машинного обучения не стоит забывать одно простое правило: "Garbage in - garbage out" , означающее, что чем "зашумленнее" были поданы данные на входе, тем хуже будет решение задачи.
Корректный отбор признаков - один из важнейших этапов предобработки данных. Особенно критичен отбор признаков при решении задач в индустрии, когда датасаентист встречается с большим набором фичей, многие из которых не имеют отношения к решаемой проблеме.
Отбор признаков обеспечаивает следующие преимущества:
Существуют три основных класса алгоритмов отбора признаков:
Схематичное изображение оберточного метода.
Одним из таких оберточных методов, который будет рассмотрен в данном туториале, будет алгоритм Boruta , названный в честь лесного славянского демона, реализующий адаптационный алгоритм для модели случайного леса (Kursa, Rudnicki, 2010).
Основная идея алгоритма заключается в сравнении исходных признаков с их теневыми копиями (англ. - shadow features) - признаками, полученными с помощью случайного перемешивания значений исходных признаков между строками. Соответственно, признаки, которые мало чем отличаются от теневых будут совершенно не важны для модели.
Рассмотрим алгоритм пошагово:
Изначально алгоритм был реализован на R, а уже затем перенесен на Python с некоторыми изменениями:
Параметры алгоритма: estimator : object n_estimators : int or string, default = 1000 perc : int, default = 100 alpha : float, default = 0.05 two_step : Boolean, default = True max_iter : int, default = 100 verbose : int, default=0
Более подробно о параметрах и атрибутах можно прочитать тут.
Применять алгоритм Boruta будем на публичном датасете Breast Cancer Wisconsin (Diagnostic) Data Set представляющий собой проблему классификации, где по характеристикам опухоли в женской груди необходимо определить, является ли опухоль доброкачественной (benign) либо злокачественной (malignant).
# импортируем необходимые библиотеки
import warnings
warnings.filterwarnings('ignore')
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from boruta import BorutaPy
import numpy as np
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import roc_auc_score
#загружаем сам датасет
from sklearn.datasets import load_breast_cancer
data = load_breast_cancer()
df = pd.DataFrame(np.c_[data['data'], data['target']],
columns= np.append(data['feature_names'], ['target']))
df.head()
mean radius | mean texture | mean perimeter | mean area | mean smoothness | mean compactness | mean concavity | mean concave points | mean symmetry | mean fractal dimension | ... | worst texture | worst perimeter | worst area | worst smoothness | worst compactness | worst concavity | worst concave points | worst symmetry | worst fractal dimension | target | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 17.99 | 10.38 | 122.80 | 1001.0 | 0.11840 | 0.27760 | 0.3001 | 0.14710 | 0.2419 | 0.07871 | ... | 17.33 | 184.60 | 2019.0 | 0.1622 | 0.6656 | 0.7119 | 0.2654 | 0.4601 | 0.11890 | 0.0 |
1 | 20.57 | 17.77 | 132.90 | 1326.0 | 0.08474 | 0.07864 | 0.0869 | 0.07017 | 0.1812 | 0.05667 | ... | 23.41 | 158.80 | 1956.0 | 0.1238 | 0.1866 | 0.2416 | 0.1860 | 0.2750 | 0.08902 | 0.0 |
2 | 19.69 | 21.25 | 130.00 | 1203.0 | 0.10960 | 0.15990 | 0.1974 | 0.12790 | 0.2069 | 0.05999 | ... | 25.53 | 152.50 | 1709.0 | 0.1444 | 0.4245 | 0.4504 | 0.2430 | 0.3613 | 0.08758 | 0.0 |
3 | 11.42 | 20.38 | 77.58 | 386.1 | 0.14250 | 0.28390 | 0.2414 | 0.10520 | 0.2597 | 0.09744 | ... | 26.50 | 98.87 | 567.7 | 0.2098 | 0.8663 | 0.6869 | 0.2575 | 0.6638 | 0.17300 | 0.0 |
4 | 20.29 | 14.34 | 135.10 | 1297.0 | 0.10030 | 0.13280 | 0.1980 | 0.10430 | 0.1809 | 0.05883 | ... | 16.67 | 152.20 | 1575.0 | 0.1374 | 0.2050 | 0.4000 | 0.1625 | 0.2364 | 0.07678 | 0.0 |
5 rows × 31 columns
Датасет состоит из 30 признаков и одного целевого признака. О значении большинства признаков остается лишь догадываться, тем более определить их значимость, датасаентисту, не разбирающемуся в медицине, не представляется возможным.
print('Доля меток с злокачественной опухолью - {}.'.format(np.round(df[df.target == 1].shape[0]/df.shape[0],2)))
Доля меток с злокачественной опухолью - 0.63.
Валидировать алгоритм будем на 3 фолдах, в качестве метрики будем использовать ROC AUC score.
X = df.iloc[:,:-1]
y = df['target']
def validation(ensemble, X, y, n_folds = 3):
rocs = []
skf = StratifiedKFold(n_splits=n_folds)
for train, test in skf.split(X, y):
X_train, X_test = X.loc[train,:], X.loc[test,:]
y_train, y_test = y[train],y[test]
ensemble.fit(X_train,y_train)
rocs.append(roc_auc_score(y_test,ensemble.predict_proba(X_test)[:,1]))
return np.mean(rocs)
Воспользуемся коробочным решением алгоритма и посмотрим на результат.
clf = RandomForestClassifier(n_estimators=500,max_depth=3, random_state=10)
print('ROC AUC SCORE - {}.'.format(validation(clf, X, y)))
ROC AUC SCORE - 0.987678451662355.
Коробочное решение алгоритма справляется уже неплохо, попробуем призвать нашего лесного демона, может, он сможет улучшить наш алгоритм. При этом, попробуем добавить в данные немного шума, посмотрим, сможет ли Boruta найти их.
X_with_noise = X.copy()
X_with_noise['random_noise_0'] = np.random.random( X_with_noise.shape[0])
X_with_noise['random_noise_1'] = np.random.random( X_with_noise.shape[0])
X_with_noise['random_noise_2'] = np.random.random( X_with_noise.shape[0])
X_with_noise['random_noise_3'] = np.random.random( X_with_noise.shape[0])
feat_selector = BorutaPy(clf, n_estimators='auto', verbose=2, random_state=17, max_iter=50)
feat_selector.fit(X_with_noise.values, y)
Iteration: 1 / 50 Confirmed: 0 Tentative: 34 Rejected: 0 Iteration: 2 / 50 Confirmed: 0 Tentative: 34 Rejected: 0 Iteration: 3 / 50 Confirmed: 0 Tentative: 34 Rejected: 0 Iteration: 4 / 50 Confirmed: 0 Tentative: 34 Rejected: 0 Iteration: 5 / 50 Confirmed: 0 Tentative: 34 Rejected: 0 Iteration: 6 / 50 Confirmed: 0 Tentative: 34 Rejected: 0 Iteration: 7 / 50 Confirmed: 0 Tentative: 34 Rejected: 0 Iteration: 8 / 50 Confirmed: 23 Tentative: 7 Rejected: 4 Iteration: 9 / 50 Confirmed: 23 Tentative: 7 Rejected: 4 Iteration: 10 / 50 Confirmed: 23 Tentative: 7 Rejected: 4 Iteration: 11 / 50 Confirmed: 23 Tentative: 7 Rejected: 4 Iteration: 12 / 50 Confirmed: 25 Tentative: 5 Rejected: 4 Iteration: 13 / 50 Confirmed: 25 Tentative: 5 Rejected: 4 Iteration: 14 / 50 Confirmed: 25 Tentative: 4 Rejected: 5 Iteration: 15 / 50 Confirmed: 25 Tentative: 4 Rejected: 5 Iteration: 16 / 50 Confirmed: 26 Tentative: 3 Rejected: 5 Iteration: 17 / 50 Confirmed: 26 Tentative: 3 Rejected: 5 Iteration: 18 / 50 Confirmed: 26 Tentative: 3 Rejected: 5 Iteration: 19 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 20 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 21 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 22 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 23 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 24 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 25 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 26 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 27 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 28 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 29 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 30 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 31 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 32 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 33 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 34 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 35 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 36 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 37 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 38 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 39 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 40 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 41 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 42 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 43 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 44 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 45 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 46 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 47 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 48 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 Iteration: 49 / 50 Confirmed: 27 Tentative: 2 Rejected: 5 BorutaPy finished running. Iteration: 50 / 50 Confirmed: 27 Tentative: 2 Rejected: 5
BorutaPy(alpha=0.05, estimator=RandomForestClassifier(bootstrap=True, class_weight=None, criterion='gini', max_depth=3, max_features='auto', max_leaf_nodes=None, min_impurity_decrease=0.0, min_impurity_split=None, min_samples_leaf=1, min_samples_split=2, min_weight_fraction_leaf=0.0, n_estimators=253, n_jobs=1, oob_score=False, random_state=<mtrand.RandomState object at 0x11174c510>, verbose=0, warm_start=False), max_iter=50, n_estimators='auto', perc=100, random_state=<mtrand.RandomState object at 0x11174c510>, two_step=True, verbose=2)
Уже на 8 итерации Boruta определил 4 добавленных шумовых признака. В том числе, для некоторых признаков исходного датасета была поставлена метка Tentative, то есть алгоритм не совсем уверен, являются ли данные метки важными для предсказания или нет.
Определим, что это за признаки и попробуем обучить алгоритм без них.
# Неважные для обучения признаки
X_with_noise.columns[np.where(feat_selector.ranking_ !=1)]
Index(['texture error', 'smoothness error', 'symmetry error', 'random_noise_0', 'random_noise_1', 'random_noise_2', 'random_noise_3'], dtype='object')
X_important = X.iloc[:,np.where(feat_selector.ranking_ ==1)[0]]
clf = RandomForestClassifier(n_estimators=500,max_depth=3, random_state=10)
print('ROC AUC SCORE - {}.'.format(validation(clf, X_important, y)))
ROC AUC SCORE - 0.9881580801334618.
Качество алгоритма улучшилось! А значит, наш лесной демон выполнил свою работу и может отправляться обратно в лес.
Хоть и в данном примере Boruta отработал на отлично, и с его помощью удалось улучшить качество алгоритма, но это не всегда так. В любом случае, попробовать использовать алгоритм стоит.
Стоит отметить, что у алгоритма есть небольшой баг, который автор еще не устранил: в том случае, если по завершению всех итераций алгоритма Boruta не нашёл ни одного неважного признака (Rejected = 0) вылетает ошибка "iteration over a 0-d array". До выхода нового апдейта модуля для корректной работы алгоритма можно добавлять колонку констант, которая будет наверняка принята алгоритмом как rejected и не спровоцирует вышеобознаечнную ошибку.
По личному опыту скажу, что Boruta хорошо заходит в задачах с генерацией большого количества искусственных признаков: в задачах кредитного скоринга, но не стоит забывать, что работа алгоритма может занять много времени в зависимости от объема данных.
Также в сети появилась новая улучшенная версия Boruta, реализованная на всеми любимом XGBoost. Прочитать про нее можно тут.
На последок, желаю всем успехов в тренировки своих демонов, надеюсь данный туториал был вам полезен!