Фаза 02 · урок 18
Отбор признаков
Цель урока: У вас 500 признаков. Модель обучается медленно, постоянно переобучается, и никто не может объяснить, чему она научилась. Вы добавляете ещё признаки в надежде улучшить качество. Становится хуже.
Текущий релиз AlexBred.com: первые 100 уроков русскоязычной программы.
Содержание урока
- Цели обучения
- Проблема
- Концепция
- Три категории отбора признаков
- Порог дисперсии
- Взаимная информация
- Рекурсивное исключение признаков (RFE)
- L1-регуляризация (Lasso)
- Важность признаков на основе деревьев
- Перестановочная важность
- Сравнительная таблица
- Блок-схема выбора
- Соберите это
- Шаг 1: сгенерируйте синтетические данные с известной структурой признаков
- Шаг 2: порог дисперсии
- Шаг 3: взаимная информация (дискретная)
- Шаг 4: рекурсивное исключение признаков
- Шаг 5: L1-отбор признаков
- Шаг 6: важность на основе деревьев (простое дерево решений)
- Шаг 7: запустите все методы и сравните
- Используйте это
- Выпустите это
- Упражнения
- Ключевые термины
- Дополнительное чтение
Больше признаков — не значит лучше. Лучше — правильные признаки.
Тип: Сборка Язык: Python Предварительные требования: Фаза 2, уроки 01–09, 08 (проектирование признаков) Время: ~75 минут
Цели обучения
- Реализовать с нуля фильтрующие методы (порог дисперсии, взаимную информацию, критерий хи-квадрат) и методы-обёртки (RFE, прямой отбор)
- Объяснить, почему взаимная информация улавливает нелинейные связи признака и цели, которые пропускает корреляция
- Сравнить L1-регуляризацию (встроенный отбор) с RFE (методом-обёрткой) и оценить их вычислительные компромиссы
- Построить конвейер отбора признаков, объединяющий несколько методов, и показать улучшение обобщающей способности на отложенных данных
Проблема
У вас 500 признаков. Модель обучается медленно, постоянно переобучается, и никто не может объяснить, чему она научилась. Вы добавляете ещё признаки в надежде улучшить качество. Становится хуже.
Так проявляется проклятие размерности. С ростом числа признаков объём пространства признаков взрывообразно увеличивается. Точки данных становятся разреженными. Расстояния между точками сближаются. Чтобы найти реальные закономерности, модели нужно экспоненциально больше данных. Шумовые признаки заглушают полезные сигналы. Переобучение становится нормой.
Отбор признаков — противоядие. Уберите шум. Удалите избыточность. Оставьте признаки, которые действительно несут информацию о целевой переменной. Результат: более быстрое обучение, лучшее обобщение и модели, которые можно объяснить.
Цель не в том, чтобы использовать всю доступную информацию. Цель — использовать правильную информацию.
Концепция
Три категории отбора признаков
Каждый метод отбора признаков относится к одной из трёх категорий:
Фильтрующие методы (filter methods) независимо оценивают каждый признак статистической мерой. Они не используют модель. Это быстро, но взаимодействия признаков остаются незамеченными.
Методы-обёртки (wrapper methods) обучают модель для оценки подмножеств признаков. В качестве оценки используется качество модели. Результаты лучше, но это дорого, потому что модель переобучается много раз.
Встроенные методы (embedded methods) выбирают признаки во время обучения модели. L1-регуляризация обнуляет веса. Деревья решений делят данные по наиболее полезным признакам. Отбор происходит при подгонке, а не отдельным шагом.
Порог дисперсии
Самый простой фильтр. Если признак почти не меняется между образцами, он почти не несёт информации.
Представьте признак, равный 0.0 для 999 из 1000 образцов. Его дисперсия близка к нулю. Ни одна модель не сможет использовать его для различения классов. Удалите его.
variance(x) = mean((x - mean(x))^2)
Установите порог (например, 0.01). Отбросьте каждый признак с дисперсией ниже этого порога. Так удаляются константные или почти константные признаки вообще без обращения к целевой переменной.
Когда применять: как шаг предобработки перед другими методами. Он почти бесплатно обнаруживает очевидно бесполезные признаки.
Ограничение: признак может иметь высокую дисперсию и при этом быть чистым шумом. Порог дисперсии необходим, но недостаточен.
Взаимная информация
Взаимная информация измеряет, насколько знание значения признака X уменьшает неопределённость в отношении цели Y.
I(X; Y) = sum_x sum_y p(x, y) * log(p(x, y) / (p(x) * p(y)))
Если X и Y независимы, то p(x, y) = p(x) * p(y), поэтому логарифмический член равен нулю и I(X; Y) = 0. Чем больше X сообщает о Y, тем выше взаимная информация.
Главное преимущество перед корреляцией: взаимная информация улавливает нелинейные зависимости. Признак может иметь нулевую корреляцию с целью, но высокую взаимную информацию, поскольку связь квадратичная или периодическая.
Для непрерывных признаков сначала выполните дискретизацию по корзинам (оценка на основе гистограммы). Число корзин влияет на оценку: слишком мало корзин теряет информацию, слишком много добавляет шум. Обычный выбор: sqrt(n) корзин или правило Стерджесса (1 + log2(n)).
Рекурсивное исключение признаков (RFE)
RFE — метод-обёртка. Он итеративно использует собственную важность признаков модели для отсечения:
- Обучите модель на всех признаках.
- Ранжируйте признаки по важности (коэффициенты для линейных моделей, уменьшение неоднородности для деревьев).
- Удалите наименее важный признак или признаки.
- Повторяйте, пока не останется нужное число признаков.
RFE учитывает взаимодействия признаков, поскольку модель одновременно видит все оставшиеся признаки. Удаление одного признака меняет важность других. Поэтому метод основательнее фильтрующих методов.
Цена: модель обучается N - target раз. При 500 признаках и цели в 10 признаков это 490 запусков обучения. Для дорогих моделей это медленно. Ускорить процесс можно, удаляя на каждом шаге несколько признаков (например, нижние 10 % за раунд).
L1-регуляризация (Lasso)
L1-регуляризация добавляет к функции потерь абсолютные значения весов:
loss = prediction_error + alpha * sum(|w_i|)
Параметр alpha управляет агрессивностью отсечения признаков. Чем выше alpha, тем больше весов становится в точности равными нулю.
Почему именно нулю? L1-штраф создаёт ромбовидную область ограничений в пространстве весов. Оптимальное решение стремится попасть в угол этого ромба, где один или несколько весов равны нулю. L2-регуляризация (ridge) создаёт круговую область ограничений: веса уменьшаются, но редко достигают нуля.
Это встроенный отбор признаков: модель во время обучения определяет, какие признаки игнорировать. Признаки с нулевым весом фактически удалены.
Преимущества: один запуск обучения, обработка коррелированных признаков (выбирает один и обнуляет другие), поддерживается большинством реализаций линейных моделей.
Ограничение: работает только для линейных моделей. Не может отражать нелинейную важность признаков.
Важность признаков на основе деревьев
Деревья решений и их ансамбли (случайные леса, градиентный бустинг) естественным образом ранжируют признаки. Каждое разбиение уменьшает неоднородность (Gini или энтропию для классификации, дисперсию для регрессии). Признаки с большим уменьшением неоднородности важнее.
Для случайного леса из T деревьев:
importance(feature_j) = (1/T) * sum over all trees of
sum over all nodes splitting on feature_j of
(n_samples * impurity_decrease)
Это даёт нормализованную оценку важности каждого признака. Она автоматически учитывает нелинейные связи и взаимодействия признаков.
Предостережение: важность на основе деревьев смещена в пользу признаков с большим числом уникальных значений (высокой кардинальностью). Случайный столбец ID будет выглядеть важным, поскольку идеально разделяет каждый образец. Используйте permutation importance как проверку здравого смысла.
Перестановочная важность
Метод, не зависящий от модели:
- Обучите модель и запишите базовое качество на валидационных данных.
- Для каждого признака случайно перемешайте его значения и измерьте падение качества.
- Чем сильнее падение, тем важнее признак.
Если перемешивание признака не ухудшает качество, модель от него не зависит. Если качество рушится, этот признак критичен.
Перестановочная важность устраняет смещение важности деревьев из-за кардинальности. Но она медленна: требуется одна полная оценка на каждый признак, повторённая несколько раз для устойчивости.
Сравнительная таблица
| Метод | Тип | Скорость | Нелинейность | Взаимодействия признаков |
|---|---|---|---|---|
| Порог дисперсии | Фильтр | Очень быстро | Нет | Нет |
| Взаимная информация | Фильтр | Быстро | Да | Нет |
| Корреляционный фильтр | Фильтр | Быстро | Нет | Нет |
| RFE | Обёртка | Медленно | Зависит от модели | Да |
| L1 / Lasso | Встроенный | Быстро | Нет (линейный) | Нет |
| Важность деревьев | Встроенный | Средне | Да | Да |
| Перестановочная важность | Независимый от модели | Медленно | Да | Да |
Блок-схема выбора
Соберите это
Шаг 1: сгенерируйте синтетические данные с известной структурой признаков
import numpy as np
def make_feature_selection_data(n_samples=500, seed=42):
rng = np.random.RandomState(seed)
x1 = rng.randn(n_samples)
x2 = rng.randn(n_samples)
x3 = rng.randn(n_samples)
x4 = x1 + 0.1 * rng.randn(n_samples)
x5 = x2 + 0.1 * rng.randn(n_samples)
informative = np.column_stack([x1, x2, x3, x4, x5])
correlated = np.column_stack([
x1 * 0.9 + 0.1 * rng.randn(n_samples),
x2 * 0.8 + 0.2 * rng.randn(n_samples),
x3 * 0.7 + 0.3 * rng.randn(n_samples),
x1 * 0.5 + x2 * 0.5 + 0.1 * rng.randn(n_samples),
x2 * 0.6 + x3 * 0.4 + 0.1 * rng.randn(n_samples),
])
noise = rng.randn(n_samples, 10) * 0.5
X = np.hstack([informative, correlated, noise])
y = (2 * x1 - 1.5 * x2 + x3 + 0.5 * rng.randn(n_samples) > 0).astype(int)
feature_names = (
[f"info_{i}" for i in range(5)]
+ [f"corr_{i}" for i in range(5)]
+ [f"noise_{i}" for i in range(10)]
)
return X, y, feature_names
Мы знаем истинную структуру: признаки 0–4 информативны (при этом 3 и 4 — коррелированные копии 0 и 1), признаки 5–9 коррелированы с информативными, признаки 10–19 — чистый шум. Хороший метод отбора должен ранжировать 0–4 выше всего, а 10–19 — ниже всего.
Шаг 2: порог дисперсии
def variance_threshold(X, threshold=0.01):
variances = np.var(X, axis=0)
mask = variances > threshold
return mask, variances
Шаг 3: взаимная информация (дискретная)
def discretize(x, n_bins=10):
min_val, max_val = x.min(), x.max()
if max_val == min_val:
return np.zeros_like(x, dtype=int)
bin_edges = np.linspace(min_val, max_val, n_bins + 1)
binned = np.digitize(x, bin_edges[1:-1])
return binned
def mutual_information(X, y, n_bins=10):
n_samples, n_features = X.shape
mi_scores = np.zeros(n_features)
y_vals, y_counts = np.unique(y, return_counts=True)
p_y = y_counts / n_samples
for f in range(n_features):
x_binned = discretize(X[:, f], n_bins)
x_vals, x_counts = np.unique(x_binned, return_counts=True)
p_x = dict(zip(x_vals, x_counts / n_samples))
mi = 0.0
for xv in x_vals:
for yi, yv in enumerate(y_vals):
joint_mask = (x_binned == xv) & (y == yv)
p_xy = np.sum(joint_mask) / n_samples
if p_xy > 0:
mi += p_xy * np.log(p_xy / (p_x[xv] * p_y[yi]))
mi_scores[f] = mi
return mi_scores
Шаг 4: рекурсивное исключение признаков
def simple_logistic_importance(X, y, lr=0.1, epochs=100):
n_samples, n_features = X.shape
w = np.zeros(n_features)
b = 0.0
for _ in range(epochs):
z = X @ w + b
pred = 1.0 / (1.0 + np.exp(-np.clip(z, -500, 500)))
error = pred - y
w -= lr * (X.T @ error) / n_samples
b -= lr * np.mean(error)
return w, b
def rfe(X, y, n_features_to_select=5, lr=0.1, epochs=100):
n_total = X.shape[1]
remaining = list(range(n_total))
rankings = np.ones(n_total, dtype=int)
rank = n_total
while len(remaining) > n_features_to_select:
X_subset = X[:, remaining]
w, _ = simple_logistic_importance(X_subset, y, lr, epochs)
importances = np.abs(w)
least_idx = np.argmin(importances)
original_idx = remaining[least_idx]
rankings[original_idx] = rank
rank -= 1
remaining.pop(least_idx)
for idx in remaining:
rankings[idx] = 1
selected_mask = rankings == 1
return selected_mask, rankings
Шаг 5: L1-отбор признаков
def soft_threshold(w, alpha):
return np.sign(w) * np.maximum(np.abs(w) - alpha, 0)
def l1_feature_selection(X, y, alpha=0.1, lr=0.01, epochs=500):
n_samples, n_features = X.shape
w = np.zeros(n_features)
b = 0.0
for _ in range(epochs):
z = X @ w + b
pred = 1.0 / (1.0 + np.exp(-np.clip(z, -500, 500)))
error = pred - y
gradient_w = (X.T @ error) / n_samples
gradient_b = np.mean(error)
w -= lr * gradient_w
w = soft_threshold(w, lr * alpha)
b -= lr * gradient_b
selected_mask = np.abs(w) > 1e-6
return selected_mask, w
Шаг 6: важность на основе деревьев (простое дерево решений)
def gini_impurity(y):
if len(y) == 0:
return 0.0
classes, counts = np.unique(y, return_counts=True)
probs = counts / len(y)
return 1.0 - np.sum(probs ** 2)
def best_split(X, y, feature_idx):
values = np.unique(X[:, feature_idx])
if len(values) <= 1:
return None, -1.0
best_threshold = None
best_gain = -1.0
parent_gini = gini_impurity(y)
n = len(y)
for i in range(len(values) - 1):
threshold = (values[i] + values[i + 1]) / 2.0
left_mask = X[:, feature_idx] <= threshold
right_mask = ~left_mask
n_left = np.sum(left_mask)
n_right = np.sum(right_mask)
if n_left == 0 or n_right == 0:
continue
gain = parent_gini - (n_left / n) * gini_impurity(y[left_mask]) - (n_right / n) * gini_impurity(y[right_mask])
if gain > best_gain:
best_gain = gain
best_threshold = threshold
return best_threshold, best_gain
def tree_importance(X, y, n_trees=50, max_depth=5, seed=42):
rng = np.random.RandomState(seed)
n_samples, n_features = X.shape
importances = np.zeros(n_features)
for _ in range(n_trees):
sample_idx = rng.choice(n_samples, size=n_samples, replace=True)
feature_subset = rng.choice(n_features, size=max(1, int(np.sqrt(n_features))), replace=False)
X_boot = X[sample_idx]
y_boot = y[sample_idx]
tree_imp = _build_tree_importance(X_boot, y_boot, feature_subset, max_depth)
importances += tree_imp
total = importances.sum()
if total > 0:
importances /= total
return importances
def _build_tree_importance(X, y, feature_subset, max_depth, depth=0):
n_features = X.shape[1]
importances = np.zeros(n_features)
if depth >= max_depth or len(np.unique(y)) <= 1 or len(y) < 4:
return importances
best_feature = None
best_threshold = None
best_gain = -1.0
for f in feature_subset:
threshold, gain = best_split(X, y, f)
if gain > best_gain:
best_gain = gain
best_feature = f
best_threshold = threshold
if best_feature is None or best_gain <= 0:
return importances
importances[best_feature] += best_gain * len(y)
left_mask = X[:, best_feature] <= best_threshold
right_mask = ~left_mask
importances += _build_tree_importance(X[left_mask], y[left_mask], feature_subset, max_depth, depth + 1)
importances += _build_tree_importance(X[right_mask], y[right_mask], feature_subset, max_depth, depth + 1)
return importances
Шаг 7: запустите все методы и сравните
Файл с кодом запускает все пять методов на одном синтетическом наборе данных и выводит сравнительную таблицу, показывающую, какие признаки выбирает каждый метод.
Используйте это
В scikit-learn отбор признаков встроен в конвейер:
from sklearn.feature_selection import (
VarianceThreshold,
mutual_info_classif,
RFE,
SelectFromModel,
)
from sklearn.linear_model import Lasso, LogisticRegression
from sklearn.ensemble import RandomForestClassifier
vt = VarianceThreshold(threshold=0.01)
X_filtered = vt.fit_transform(X)
mi_scores = mutual_info_classif(X, y)
top_k = np.argsort(mi_scores)[-10:]
rfe_selector = RFE(LogisticRegression(), n_features_to_select=10)
rfe_selector.fit(X, y)
X_rfe = rfe_selector.transform(X)
lasso_selector = SelectFromModel(Lasso(alpha=0.01))
lasso_selector.fit(X, y)
X_lasso = lasso_selector.transform(X)
rf = RandomForestClassifier(n_estimators=100)
rf.fit(X, y)
importances = rf.feature_importances_
Реализации с нуля показывают, что именно происходит внутри каждого метода. Порог дисперсии — это лишь вычисление var(X, axis=0) и применение маски. Взаимная информация — подсчёт совместных и маргинальных частот в таблице сопряжённости. RFE — цикл обучения, ранжирования и отсечения. L1 — градиентный спуск с шагом мягкого порогового отсечения. Важность деревьев накапливает уменьшения неоднородности по разбиениям. Никакой магии — только статистика и циклы.
Версии sklearn добавляют устойчивость (например, mutual_info_classif использует оценку плотности k-NN вместо разбиения по корзинам), скорость (реализации на C) и интеграцию с конвейерами.
Выпустите это
Этот урок создаёт:
outputs/skill-feature-selector.md— краткое справочное дерево решений для выбора подходящего метода отбора признаков
Упражнения
-
Прямой отбор: реализуйте противоположность RFE. Начните с нуля признаков. На каждом шаге добавляйте признак, который сильнее всего улучшает качество модели. Остановитесь, когда добавление признаков перестанет помогать. Сравните выбранные признаки с результатами RFE. Что быстрее? Что даёт лучшие результаты?
-
Устойчивый отбор: запустите L1-отбор признаков 50 раз, каждый раз на случайной подвыборке 80 % данных, со слегка различающимися значениями alpha. Подсчитайте, как часто выбирается каждый признак. Признаки, выбранные более чем в 80 % запусков, «устойчивы». Сравните устойчивые признаки с L1-отбором за один запуск. Что надёжнее?
-
Обнаружение мультиколлинеарности: вычислите корреляционную матрицу всех признаков. Реализуйте функцию, которая по порогу корреляции (например, 0.9) удаляет один признак из каждой сильно коррелированной пары, оставляя тот, у которого выше взаимная информация с целью. Проверьте на синтетическом наборе данных, что она удаляет избыточные коррелированные признаки.
-
Конвейер отбора признаков: объедините порог дисперсии, фильтр взаимной информации и RFE в один конвейер. Сначала удалите признаки с почти нулевой дисперсией, затем оставьте верхние 50 % по взаимной информации, после чего запустите RFE на выживших. Сравните этот конвейер с запуском одного RFE на всех признаках. Быстрее ли конвейер? Точен ли он в той же мере?
-
Перестановочная важность с нуля: реализуйте permutation importance. Для каждого признака перемешайте его значения 10 раз и измерьте среднее падение F1-оценки. Сравните ранжирование с важностью на основе деревьев. Найдите случаи, где они расходятся, и объясните почему (подсказка: коррелированные признаки).
Ключевые термины
| Термин | Как обычно говорят | Что это действительно означает |
|---|---|---|
| Фильтрующий метод | «Оценивать признаки независимо» | Подход к отбору признаков, который ранжирует признаки статистической мерой без обучения модели и рассматривает каждый признак изолированно |
| Метод-обёртка | «Использовать модель для выбора признаков» | Подход к отбору, оценивающий подмножества признаков обучением модели и использующий её качество как критерий выбора |
| Встроенный метод | «Модель выбирает признаки во время обучения» | Отбор признаков, происходящий во время подгонки модели, например L1-регуляризация, обнуляющая веса |
| Взаимная информация | «Сколько одна переменная сообщает о другой» | Мера уменьшения неопределённости о Y при знании X, улавливающая как линейные, так и нелинейные зависимости |
| Рекурсивное исключение признаков | «Обучить, ранжировать, отсечь, повторить» | Итеративный метод-обёртка: обучает модель, удаляет наименее важные признаки и повторяет до достижения целевого числа |
| L1 / Lasso-регуляризация | «Штраф, убивающий признаки» | Добавление суммы абсолютных значений весов к функции потерь, заставляющее неважные веса стать в точности нулевыми |
| Порог дисперсии | «Удалить константные признаки» | Исключение признаков, дисперсия которых между образцами ниже заданного порога, для удаления не несущих информации признаков |
| Важность признака | «Какие признаки важнее всего» | Оценка того, насколько признак влияет на предсказания: по приростам разбиений в деревьях или модулям коэффициентов в линейных моделях |
| Перестановочная важность | «Перемешать и измерить ущерб» | Оценка важности признака случайным перемешиванием его значений и измерением последующего падения качества модели |
| Проклятие размерности | «Слишком много признаков, слишком мало данных» | Явление, при котором добавление признаков экспоненциально увеличивает объём пространства, разрежает данные и лишает расстояния смысла |
Дополнительное чтение
- An Introduction to Variable and Feature Selection (Guyon & Elisseeff, 2003) — основополагающий обзор методов отбора признаков, на который до сих пор широко ссылаются
- Руководство scikit-learn по отбору признаков — практическая справка по фильтрующим, обёрточным и встроенным методам с кодом
- Stability Selection (Meinshausen & Buhlmann, 2010) — объединяет подвыборки с отбором признаков для устойчивых и воспроизводимых результатов
- Beware Default Random Forest Importances (Strobl et al., 2007) — демонстрирует смещение важности деревьев из-за кардинальности и предлагает условную важность
Источник: Feature Selection 02.17 — Несбалансированные данные · Фаза 2 — Основы машинного обучения · Полный каталог · 03.01 — Перцептрон