Phase 02 - Lesson 18

Seleção de recursos

This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.

Mais recursos não são melhores. Os recursos certos são melhores.

Tipo: Construir Idioma: Python Pré-requisitos: Fase 2, Lições 01-09, 08 (engenharia de características) Tempo: ~75 minutos

Objetivos de aprendizagem

  • Implementar métodos de filtro (limite de variância, informações mútuas, qui-quadrado) e métodos de wrapper (RFE, seleção direta) do zero
  • Explicar por que informações mútuas capturam relacionamentos não-lineares de recurso-alvo que a correlação não percebe
  • Compare a regularização L1 (seleção incorporada) com RFE (seleção de wrapper) e avalie suas compensações computacionais
  • Construir um pipeline de seleção de recursos que combine vários métodos e demonstre generalização aprimorada em dados retidos

O problema

Você tem 500 recursos. Seu modelo treina lentamente, se adapta constantemente e ninguém consegue explicar o que aprendeu. Você adiciona mais recursos na esperança de melhorar o desempenho. Fica pior.

Esta é a maldição da dimensionalidade em ação. À medida que o número de recursos aumenta, o volume do espaço de recursos explode. Os pontos de dados tornam-se esparsos. As distâncias entre os pontos convergem. O modelo precisa de exponencialmente mais dados para encontrar padrões reais. Os recursos de ruído abafam os recursos de sinal. Overfitting se torna o padrão.

A seleção de recursos é o antídoto. Tire o barulho. Remova a redundância. Mantenha os recursos que contêm informações reais sobre o alvo. O resultado: treinamento mais rápido, melhor generalização e modelos que você pode realmente explicar.

O objetivo não é usar todas as informações disponíveis. É usar a informação certa.

O Conceito

Três categorias de seleção de recursos

Cada método de seleção de recursos se enquadra em uma das três categorias:

flowchart TD
    A[Feature Selection Methods] --> B[Filter Methods]
    A --> C[Wrapper Methods]
    A --> D[Embedded Methods]

    B --> B1["Variance Threshold"]
    B --> B2["Mutual Information"]
    B --> B3["Chi-squared Test"]
    B --> B4["Correlation Filtering"]

    C --> C1["Recursive Feature Elimination"]
    C --> C2["Forward Selection"]
    C --> C3["Backward Elimination"]

    D --> D1["L1 / Lasso Regularization"]
    D --> D2["Tree-based Importance"]
    D --> D3["Elastic Net"]

Métodos de filtro pontuam cada recurso de forma independente usando uma medida estatística. Eles não usam um modelo. Rápido, mas eles perdem as interações de recursos.

Métodos wrapper treinam um modelo para avaliar subconjuntos de recursos. Eles usam o desempenho do modelo como pontuação. Melhores resultados, mas caros porque retreinam o modelo muitas vezes.

Métodos incorporados selecionam recursos como parte do treinamento do modelo. A regularização L1 leva os pesos a zero. As árvores de decisão são divididas nos recursos mais úteis. A seleção acontece durante a adaptação e não como uma etapa separada.

Limite de Variância

O filtro mais simples. Se um recurso varia pouco entre as amostras, ele quase não contém informações.

Considere um recurso que é 0,0 para 999 de 1.000 amostras. Sua variância é próxima de zero. Nenhum modelo pode usá-lo para distinguir entre classes. Remova-o.

variance(x) = mean((x - mean(x))^2)

Defina um limite (por exemplo, 0,01). Elimine todos os recursos com variação abaixo deles. Isso remove recursos constantes ou quase constantes sem olhar para a variável de destino.

Quando usar: como uma etapa de pré-processamento antes de outros métodos. Ele captura recursos obviamente inúteis a um custo quase zero.

Limitação: um recurso pode ter alta variação e ainda assim ser puro ruído. O limite de variação é necessário, mas não suficiente.

Informação Mútua

A informação mútua mede o quanto o conhecimento do valor do recurso X reduz a incerteza sobre o alvo Y.

I(X; Y) = sum_x sum_y p(x, y) * log(p(x, y) / (p(x) * p(y)))

Se X e Y são independentes, p(x, y) = p(x) * p(y), então o termo logarítmico é zero e I(X; Y) = 0. Quanto mais X falar sobre Y, maior será a informação mútua.

Vantagem principal sobre a correlação: a informação mútua captura relações não lineares. Um recurso pode ter correlação zero com o alvo, mas alta informação mútua porque o relacionamento é quadrático ou periódico.

Para recursos contínuos, primeiro discretize em compartimentos (estimativa baseada em histograma). O número de caixas afeta a estimativa – poucas caixas perdem informações, muitas caixas adicionam ruído. Uma escolha comum: sqrt(n) bins ou regra de Sturges (1 + log2(n)).

flowchart LR
    A[Feature X] --> B[Discretize into Bins]
    B --> C["Compute Joint Distribution p(x,y)"]
    C --> D["Compute MI = sum p(x,y) * log(p(x,y) / p(x)p(y))"]
    D --> E["Rank Features by MI Score"]
    E --> F[Select Top K]

Eliminação de recursos recursivos (RFE)

RFE é um método wrapper. Ele usa a importância do recurso do próprio modelo para podar iterativamente:

  1. Treine o modelo com todos os recursos
  2. Classifique as características por importância (coeficientes para modelos lineares, redução de impurezas para árvores)
  3. Remova os recursos menos importantes
  4. Repita até que o número desejado de recursos permaneça
flowchart TD
    A["Start: All N Features"] --> B["Train Model"]
    B --> C["Rank Feature Importances"]
    C --> D["Remove Least Important"]
    D --> E{"Features == Target Count?"}
    E -->|No| B
    E -->|Yes| F["Return Selected Features"]

RFE considera as interações de recursos porque o modelo vê todos os recursos restantes juntos. A remoção de um recurso altera a importância de outros. Isso o torna mais completo do que os métodos de filtro.

O custo: você treina o modelo N - tempos alvo. Com 500 recursos e uma meta de 10, são 490 execuções de treinamento. Para modelos caros, isso é lento. Você pode acelerar removendo vários recursos por etapa (por exemplo, remover os 10% inferiores a cada rodada).

L1 (Lasso) Regularização

A regularização L1 adiciona o valor absoluto dos pesos à função de perda:

loss = prediction_error + alpha * sum(|w_i|)

O parâmetro alfa controla a agressividade com que os recursos são removidos. Alfa mais alto significa que mais pesos vão exatamente para zero.

Por que exatamente zero? A penalidade L1 cria uma região de restrição em forma de diamante no espaço de pesos. A solução ótima tende a cair em um canto deste diamante, onde um ou mais pesos são zero. A regularização L2 (crista) cria uma restrição circular onde os pesos diminuem, mas raramente chegam a zero.

Esta é a seleção de recursos incorporada: o modelo aprende durante o treinamento quais recursos ignorar. Recursos com peso zero são removidos de forma eficaz.

Vantagens: execução única de treinamento, lida com recursos correlacionados (escolhe um e zera os outros), integrado à maioria das implementações de modelo linear.

Limitação: funciona apenas para modelos lineares. Não é possível capturar a importância do recurso não linear.

Importância do recurso baseado em árvore

As árvores de decisão e seus conjuntos (florestas aleatórias, aumento de gradiente) classificam naturalmente os recursos. Cada divisão reduz a impureza (Gini ou entropia para classificação, variância para regressão). As características que produzem maiores reduções de impurezas são mais importantes.

Para uma random forest com árvores T:

importance(feature_j) = (1/T) * sum over all trees of
    sum over all nodes splitting on feature_j of
        (n_samples * impurity_decrease)

Isso fornece uma pontuação de importância normalizada para cada recurso. Ele lida com relacionamentos não lineares e interações de recursos automaticamente.

Cuidado: a importância baseada em árvore é direcionada para recursos com muitos valores exclusivos (alta cardinalidade). Uma coluna de ID aleatória parecerá importante porque divide perfeitamente cada amostra. Use a importância da permutação como uma verificação de sanidade.

Importância da Permutação

Um método independente de modelo:

  1. Treine o modelo e registre o desempenho da linha de base nos dados de validação
  2. Para cada recurso: embaralhe seus valores aleatoriamente, meça a queda no desempenho
  3. Quanto maior a queda, mais importante é o recurso

Se embaralhar um recurso não prejudicar o desempenho, o modelo não depende dele. Se o desempenho falhar, esse recurso será crítico.

A importância da permutação evita o viés de cardinalidade da importância baseada em árvore. Mas é lento: uma avaliação completa por recurso, repetida várias vezes para estabilidade.

Tabela de comparação

Método Tipo Velocidade Não linear Interações de recursos
Limite de variação Filtro Muito rápido Não Não
Informação mútua Filtro Rápido Sim Não
Filtro de correlação Filtro Rápido Não Não
RFE Invólucro Lento Depende do modelo Sim
L1 / Lasso Incorporado Rápido Não (linear) Não
Importância da árvore Incorporado Médio Sim Sim
Importância da permutação Agnóstico de modelo Lento Sim Sim

Fluxograma de decisão

flowchart TD
    A[Start: Feature Selection] --> B{How many features?}
    B -->|"< 50"| C["Start with variance threshold + mutual information"]
    B -->|"50-500"| D["Variance threshold, then L1 or tree importance"]
    B -->|"> 500"| E["Variance threshold, then mutual info filter, then RFE on survivors"]

    C --> F{Using linear model?}
    D --> F
    E --> F

    F -->|Yes| G["L1 regularization for final selection"]
    F -->|No - trees| H["Tree importance + permutation importance"]
    F -->|No - other| I["RFE with your model"]

    G --> J[Validate: compare selected vs all features]
    H --> J
    I --> J

    J --> K{Performance improved?}
    K -->|Yes| L["Ship with selected features"]
    K -->|No| M["Try different method or keep all features"]

Construa

Etapa 1: gerar dados sintéticos com estrutura de recursos conhecida

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

Sabemos a verdade: os recursos 0 a 4 são informativos (mais 3 e 4 são cópias correlacionadas de 0 e 1), os recursos 5 a 9 são correlacionados com recursos informativos, os recursos 10 a 19 são puro ruído. Um bom método de seleção deve ser classificado de 0 a 4 no nível mais alto e de 10 a 19 no nível mais baixo.

Etapa 2: limite de variação

def variance_threshold(X, threshold=0.01):
    variances = np.var(X, axis=0)
    mask = variances > threshold
    return mask, variances

Etapa 3: Informação mútua (discreta)

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

Etapa 4: Eliminação de recursos recursivos

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

Etapa 5: seleção de recurso 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

Etapa 6: Importância baseada em árvore (árvore de decisão simples)

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

Etapa 7: execute todos os métodos e compare

O arquivo de código executa todos os cinco métodos no mesmo conjunto de dados sintético e imprime uma tabela de comparação mostrando quais recursos cada método seleciona.

Use-o

Com scikit-learn, a seleção de recursos é incorporada ao pipeline:

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_

As implementações do zero mostram exatamente o que acontece dentro de cada método. O limite de variação é apenas calcular var(X, axis=0) e aplicar uma máscara. A informação mútua é a contagem de frequências conjuntas e marginais em uma tabela de contingência. RFE é um loop que treina, classifica e poda. L1 é uma descida do gradiente com um passo de limiar suave. A importância da árvore acumula reduções de impurezas entre as divisões. Sem mágica – apenas estatísticas e loops.

As versões do sklearn adicionam robustez (por exemplo, mutual_info_classif usa estimativa de densidade k-NN em vez de binning), velocidade (implementações C) e integração de pipeline.

Envie

Esta lição produz:

  • outputs/skill-feature-selector.md -- uma árvore de decisão de referência rápida para escolher o método correto de seleção de recursos

Exercícios

  1. Seleção direta: implemente o oposto de RFE. Comece com zero recursos. Em cada etapa, adicione o recurso que mais melhora o desempenho do modelo. Pare quando adicionar recursos não ajudar mais. Compare os recursos selecionados com os resultados RFE. Qual é mais rápido? Qual dá melhores resultados?

  2. Seleção de estabilidade: execute a seleção de recursos L1 50 vezes, cada vez em uma subamostra aleatória de 80% dos dados, com valores alfa ligeiramente diferentes. Conte com que frequência cada recurso é selecionado. Os recursos selecionados em > 80% das execuções são "estáveis". Compare recursos estáveis ​​com a seleção L1 de execução única. Qual é mais confiável?

  3. Detecção de multicolinearidade: calcule a matriz de correlação para todos os recursos. Implemente uma função que, dado um limite de correlação (por exemplo, 0,9), remova um recurso de cada par altamente correlacionado (mantendo aquele com maior informação mútua com o alvo). Teste no conjunto de dados sintético e verifique se ele remove os recursos correlacionados redundantes.

  4. Pipeline de seleção de recursos: limite de variação da cadeia, filtro de informações mútuas e RFE em um único pipeline. Primeiro remova os recursos de variação quase zero, depois mantenha os 50% principais por informações mútuas e, em seguida, execute RFE nos sobreviventes. Compare esse pipeline com a execução de RFE sozinho em todos os recursos. O pipeline é mais rápido? É igualmente preciso?

  5. Importância da permutação do zero: implemente a importância da permutação. Para cada recurso, embaralhe seus valores 10 vezes e meça a queda média na score F1. Compare a classificação com a importância baseada em árvore. Encontre casos em que eles discordam e explique o porquê (dica: características correlacionadas).

Termos-chave

Prazo O que as pessoas dizem O que isso realmente significa
Método de filtro "Pontuação de recursos de forma independente" Uma abordagem de seleção de recursos que classifica recursos usando uma medida estatística sem treinar um modelo, avaliando cada recurso isoladamente
Método wrapper "Use o modelo para escolher recursos" Uma abordagem de seleção de recursos que avalia subconjuntos de recursos treinando um modelo e usando seu desempenho como critério de seleção
Método incorporado “O modelo seleciona recursos durante o treinamento” Seleção de recursos que acontece como parte do ajuste do modelo, como a regularização L1 levando os pesos a zero
Informação mútua "Quanto uma variável diz sobre outra" Uma medida da redução da incerteza sobre Y dado o conhecimento de X, captando dependências lineares e não lineares
Eliminação de recursos recursivos “Treinar, classificar, podar, repetir” Um método wrapper iterativo que treina um modelo, remove os recursos menos importantes e se repete até que uma contagem alvo seja atingida
Regularização L1 / Lasso “Penalidade que mata recursos” Adicionando a soma dos valores de peso absolutos à função de perda, o que leva os pesos de recursos sem importância a exatamente zero
Limite de variação "Remover recursos constantes" Eliminando recursos cuja variação entre amostras cai abaixo de um limite especificado, filtrando recursos que não carregam informações
Importância do recurso "Quais recursos são mais importantes" Uma pontuação que indica o quanto cada característica contribui para as previsões do modelo, calculada a partir de ganhos divididos (árvores) ou magnitudes de coeficientes (lineares)
Importância da permutação "Embaralhe e meça o dano" Avaliando a importância do recurso embaralhando aleatoriamente os valores de cada recurso e medindo a queda resultante no desempenho do modelo
Maldição da dimensionalidade “Muitos recursos, dados insuficientes” O fenômeno em que adicionar recursos aumenta exponencialmente o volume do espaço de recursos, tornando os dados esparsos e as distâncias sem sentido

Leitura Adicional

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).