Последовательный отбор признаков для модели машинного обучения
Как известно, некоторые признаки, характеризующие объекты в моделях машинного обучения могут оказаться фиктивными или избыточными. Соответственно, их включение в пайплайн может привести к снижению качества прогнозов. Рассмотрим, один из популярных способов оптимизации набора признаков, который заключается в формировании их множества путем последовательного добавления по одному наиболее эффективному.
То есть сначала работа модели тестируется на каждом отдельном признаке и выбирается максимизирующий выбранную оценку, затем добавляется еще один, такой что на паре модель дает лучшую оценку и так далее до достижения заданного количества признаков. Существует схожий алгоритм, но действующий в обратном порядке - когда из множества всех признаков убирается по одному, от потери которого качество меньше всего "пострадает".
Оба алгоритма реализованы в классе SequentialFeatureSelector модуля sklearn.feature_selection. Рассмотрим его работу на примере следующего датафрейма:
from sklearn.datasets import load_diabetes from sklearn.metrics import mean_absolute_error, mean_absolute_percentage_error import pandas as pd import numpy as np np.random.seed(0) df, y = load_diabetes(return_X_y=True, as_frame=True) df['age'] = pd.cut(df['age'], bins=5, labels=False) df['target'] = y df = df.sample(frac=1).reset_index(drop=True) display(df.head()) display(df.shape)
from sklearn.model_selection import train_test_split X_tr, X_val, y_tr, y_val = train_test_split(df.drop(columns='target').copy(), df['target'], test_size=0.2) y_tr.shape[0], y_val.shape[0]
А теперь сформируем пайплайн нашей модели (подробнее читай здесь):
from sklearn.pipeline import Pipeline from sklearn.compose import ColumnTransformer from sklearn.preprocessing import RobustScaler from sklearn.preprocessing import OneHotEncoder from sklearn.linear_model import ElasticNet ct = ColumnTransformer([('age_enc', OneHotEncoder(sparse=False, handle_unknown='ignore'), ['age'])], remainder='passthrough') model = Pipeline([('enc', ct), ('sc', RobustScaler()), ('reg', ElasticNet())]) model.fit(X_tr, y_tr) y_p = model.predict(X_val) print(f'{mean_absolute_error(y_val, y_p):.2f}', f'{mean_absolute_percentage_error(y_val, y_p):.3f}')
Попытка запустить SequentialFeatureSelector с нашим пайплайном приведет к ошибке. Обнаружилось, что класс SequentialFeatureSelector не корректно работает с ColumnTransformer, поэтому первый шаг заменим на собственную обработку и пересоздадим объект model:
ohe = OneHotEncoder(sparse=False, handle_unknown='ignore') ohe.fit(X_tr[['age']]) ages_tr = ohe.transform(X_tr[['age']]) ages_val = ohe.transform(X_val[['age']]) X_tr_mod = np.concatenate([X_tr.drop(columns='age'), ages_tr], axis=1) X_val_mod = np.concatenate([X_val.drop(columns='age'), ages_val], axis=1) model = Pipeline([('sc', RobustScaler()), ('reg', ElasticNet())])
Теперь обучим SequentialFeatureSelector:
from sklearn.feature_selection import SequentialFeatureSelector sfs = SequentialFeatureSelector(model, n_features_to_select=5, cv=5, scoring='neg_mean_absolute_percentage_error', direction='forward', n_jobs=-1) sfs.fit(X_tr_mod, y_tr)
В конструктор класса передаем объект модели, количество оптимальных признаков (n_features_to_select) и итераций в кросс-валидации (cv), метрику (scoring), направление (direction, определяет будет происходить добавление или удаление признаков), количество потоков обработки.
Метод get_support возвращает маску выбранных признаков, где True соответствует попавшим в итоговое множество::
sfs.get_support()
Теперь можно запустить пайплайн на отобранных признаках:
model.fit(X_tr_mod[:, sfs.get_support()], y_tr) y_p = model.predict(X_val_mod[:, sfs.get_support()]) print(f'{mean_absolute_error(y_val, y_p):.2f}', f'{mean_absolute_percentage_error(y_val, y_p):.3f}')
Качество снизилось, так как мы не знаем, какое оптимальное количество признаков нужно модели. Соответственно, найти его можно путем перебора в цикле, но следует иметь в виду, что количество рассматриваемых моделей велико, и это может привести к большим временным затратам (на каждой итерации строится k*num моделей, где k - число итераций перекрестной проверки и num - число оставшихся признаков):
best_score = 0.5 best_num = 0 for num in range(1, X_tr_mod.shape[1]-1): sfs = SequentialFeatureSelector(model, n_features_to_select=num, cv=5, scoring='neg_mean_absolute_percentage_error', direction='forward', n_jobs=-1) sfs.fit(X_tr_mod, y_tr) model.fit(X_tr_mod[:, sfs.get_support()], y_tr) y_p = model.predict(X_val_mod[:, sfs.get_support()]) score_temp = mean_absolute_percentage_error(y_val, y_p) print(f'Признаков {num}, mape={score_temp}') if score_temp<best_score: best_score = score_temp best_num = num best_support = sfs.get_support() print(f"best mape = {best_score}") print(f"best num = {best_num}") best_support
Теперь оставим оптимальное количество признаков и оценим модель:
from sklearn.feature_selection import SequentialFeatureSelector sfs = SequentialFeatureSelector(model, n_features_to_select=9, cv=5, scoring='neg_mean_absolute_percentage_error', direction='forward', n_jobs=-1) sfs.fit(X_tr_mod, y_tr) model.fit(X_tr_mod[:, sfs.get_support()], y_tr) y_p = model.predict(X_val_mod[:, sfs.get_support()]) print(f'{mean_absolute_error(y_val, y_p):.2f}', f'{mean_absolute_percentage_error(y_val, y_p):.3f}')