Фаза 02 · урок 18

Отбор признаков

Цель урока: У вас 500 признаков. Модель обучается медленно, постоянно переобучается, и никто не может объяснить, чему она научилась. Вы добавляете ещё признаки в надежде улучшить качество. Становится хуже.

Текущий релиз AlexBred.com: первые 100 уроков русскоязычной программы.

Курс
AI Engineering from Scratch
Фаза
Основы машинного обучения
Чтение
18 мин.
Проверено
Содержание урока
  1. Цели обучения
  2. Проблема
  3. Концепция
  4. Три категории отбора признаков
  5. Порог дисперсии
  6. Взаимная информация
  7. Рекурсивное исключение признаков (RFE)
  8. L1-регуляризация (Lasso)
  9. Важность признаков на основе деревьев
  10. Перестановочная важность
  11. Сравнительная таблица
  12. Блок-схема выбора
  13. Соберите это
  14. Шаг 1: сгенерируйте синтетические данные с известной структурой признаков
  15. Шаг 2: порог дисперсии
  16. Шаг 3: взаимная информация (дискретная)
  17. Шаг 4: рекурсивное исключение признаков
  18. Шаг 5: L1-отбор признаков
  19. Шаг 6: важность на основе деревьев (простое дерево решений)
  20. Шаг 7: запустите все методы и сравните
  21. Используйте это
  22. Выпустите это
  23. Упражнения
  24. Ключевые термины
  25. Дополнительное чтение

Больше признаков — не значит лучше. Лучше — правильные признаки.

Тип: Сборка Язык: 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 — метод-обёртка. Он итеративно использует собственную важность признаков модели для отсечения:

  1. Обучите модель на всех признаках.
  2. Ранжируйте признаки по важности (коэффициенты для линейных моделей, уменьшение неоднородности для деревьев).
  3. Удалите наименее важный признак или признаки.
  4. Повторяйте, пока не останется нужное число признаков.

Диаграмма к уроку «Отбор признаков»

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 как проверку здравого смысла.

Перестановочная важность

Метод, не зависящий от модели:

  1. Обучите модель и запишите базовое качество на валидационных данных.
  2. Для каждого признака случайно перемешайте его значения и измерьте падение качества.
  3. Чем сильнее падение, тем важнее признак.

Если перемешивание признака не ухудшает качество, модель от него не зависит. Если качество рушится, этот признак критичен.

Перестановочная важность устраняет смещение важности деревьев из-за кардинальности. Но она медленна: требуется одна полная оценка на каждый признак, повторённая несколько раз для устойчивости.

Сравнительная таблица

Метод Тип Скорость Нелинейность Взаимодействия признаков
Порог дисперсии Фильтр Очень быстро Нет Нет
Взаимная информация Фильтр Быстро Да Нет
Корреляционный фильтр Фильтр Быстро Нет Нет
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 — краткое справочное дерево решений для выбора подходящего метода отбора признаков

Упражнения

  1. Прямой отбор: реализуйте противоположность RFE. Начните с нуля признаков. На каждом шаге добавляйте признак, который сильнее всего улучшает качество модели. Остановитесь, когда добавление признаков перестанет помогать. Сравните выбранные признаки с результатами RFE. Что быстрее? Что даёт лучшие результаты?

  2. Устойчивый отбор: запустите L1-отбор признаков 50 раз, каждый раз на случайной подвыборке 80 % данных, со слегка различающимися значениями alpha. Подсчитайте, как часто выбирается каждый признак. Признаки, выбранные более чем в 80 % запусков, «устойчивы». Сравните устойчивые признаки с L1-отбором за один запуск. Что надёжнее?

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

  4. Конвейер отбора признаков: объедините порог дисперсии, фильтр взаимной информации и RFE в один конвейер. Сначала удалите признаки с почти нулевой дисперсией, затем оставьте верхние 50 % по взаимной информации, после чего запустите RFE на выживших. Сравните этот конвейер с запуском одного RFE на всех признаках. Быстрее ли конвейер? Точен ли он в той же мере?

  5. Перестановочная важность с нуля: реализуйте permutation importance. Для каждого признака перемешайте его значения 10 раз и измерьте среднее падение F1-оценки. Сравните ранжирование с важностью на основе деревьев. Найдите случаи, где они расходятся, и объясните почему (подсказка: коррелированные признаки).

Ключевые термины

Термин Как обычно говорят Что это действительно означает
Фильтрующий метод «Оценивать признаки независимо» Подход к отбору признаков, который ранжирует признаки статистической мерой без обучения модели и рассматривает каждый признак изолированно
Метод-обёртка «Использовать модель для выбора признаков» Подход к отбору, оценивающий подмножества признаков обучением модели и использующий её качество как критерий выбора
Встроенный метод «Модель выбирает признаки во время обучения» Отбор признаков, происходящий во время подгонки модели, например L1-регуляризация, обнуляющая веса
Взаимная информация «Сколько одна переменная сообщает о другой» Мера уменьшения неопределённости о Y при знании X, улавливающая как линейные, так и нелинейные зависимости
Рекурсивное исключение признаков «Обучить, ранжировать, отсечь, повторить» Итеративный метод-обёртка: обучает модель, удаляет наименее важные признаки и повторяет до достижения целевого числа
L1 / Lasso-регуляризация «Штраф, убивающий признаки» Добавление суммы абсолютных значений весов к функции потерь, заставляющее неважные веса стать в точности нулевыми
Порог дисперсии «Удалить константные признаки» Исключение признаков, дисперсия которых между образцами ниже заданного порога, для удаления не несущих информации признаков
Важность признака «Какие признаки важнее всего» Оценка того, насколько признак влияет на предсказания: по приростам разбиений в деревьях или модулям коэффициентов в линейных моделях
Перестановочная важность «Перемешать и измерить ущерб» Оценка важности признака случайным перемешиванием его значений и измерением последующего падения качества модели
Проклятие размерности «Слишком много признаков, слишком мало данных» Явление, при котором добавление признаков экспоненциально увеличивает объём пространства, разрежает данные и лишает расстояния смысла

Дополнительное чтение


Источник: Feature Selection 02.17 — Несбалансированные данные · Фаза 2 — Основы машинного обучения · Полный каталог · 03.01 — Перцептрон