Phase 02 - Lesson 18

Selección de funciones

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

Más funciones no son mejores. Las características correctas son mejores.

Tipo: Construcción Idioma: Python Requisitos previos: Fase 2, Lecciones 01-09, 08 (ingeniería de características) Tiempo: ~75 minutos

Objetivos de aprendizaje

  • Implementar métodos de filtro (umbral de varianza, inentrenamiento mutua, chi-cuadrado) y métodos de envoltura (RFE, selección directa) desde cero.
  • Explicar por qué la inentrenamiento mutua captura relaciones característica-objetivo no lineales que la correlación pasa por alto
  • Comparar la regularización L1 (selección integrada) con RFE (selección de contenedor) y evaluar sus compensaciones computacionales
  • Cree un canal de selección de funciones que combine múltiples métodos y demuestre una generalización mejorada de los datos retenidos.

El problema

Tienes 500 funciones. Su modelo entrena lentamente, se sobreajusta constantemente y nadie puede explicar lo que aprendió. Agrega más funciones con la esperanza de mejorar el rendimiento. Se pone peor.

Esta es la maldición de la dimensionalidad en acción. A medida que crece el número de funciones, el volumen del espacio de funciones explota. Los puntos de datos se vuelven escasos. Las distancias entre puntos convergen. El modelo necesita exponencialmente más datos para encontrar patrones reales. Las características de ruido ahogan las características de la señal. El sobreajuste se convierte en el valor predeterminado.

La selección de funciones es el antídoto. Elimina el ruido. Eliminar la redundancia. Mantenga las características que contienen inentrenamiento real sobre el objetivo. El resultado: entrenamiento más rápido, mejor generalización y modelos que realmente puedes explicar.

El objetivo no es utilizar toda la inentrenamiento disponible. Es utilizar la inentrenamiento correcta.

El concepto

Tres categorías de selección de funciones

Cada método de selección de funciones se divide en una de tres categorías:

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 filtrado puntúa cada característica de forma independiente utilizando una medida estadística. No utilizan modelo. Rápidos, pero pierden interacciones entre funciones.

Métodos contenedor entrena un modelo para evaluar subconjuntos de características. Utilizan el desempeño del modelo como puntuación. Mejores resultados, pero caros porque reentrenan el modelo muchas veces.

Métodos integrados selecciona funciones como parte del entrenamiento del modelo. La regularización L1 lleva los pesos a cero. Los árboles de decisión se dividen según las características más útiles. La selección ocurre durante la adaptación, no como un paso separado.

Umbral de variación

El filtro más simple. Si una característica apenas varía entre las muestras, casi no contiene inentrenamiento.

Considere una característica que es 0,0 para 999 de 1000 muestras. Su varianza es cercana a cero. Ningún modelo puede usarlo para distinguir entre clases. Retírelo.

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

Establezca un umbral (por ejemplo, 0,01). Elimine todas las funciones con variación debajo. Esto elimina características constantes o casi constantes sin mirar la variable de destino en absoluto.

Cuándo usarlo: como paso de preprocesamiento antes de otros métodos. Capta características obviamente inútiles a un costo casi nulo.

Limitación: una característica puede tener una gran variación y seguir siendo puro ruido. El umbral de variación es necesario pero no suficiente.

Inentrenamiento mutua

La inentrenamiento mutua mide cuánto conocer el valor de la característica X reduce la incertidumbre sobre el objetivo Y.

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

Si X e Y son independientes, p(x, y) = p(x) * p(y), entonces el término logarítmico es cero e I(X; Y) = 0. Cuanto más le diga X sobre Y, mayor será la inentrenamiento mutua.

Ventaja clave sobre la correlación: la inentrenamiento mutua captura relaciones no lineales. Una característica puede tener una correlación cero con el objetivo pero una alta inentrenamiento mutua porque la relación es cuadrática o periódica.

Para características continuas, primero discretice en contenedores (estimación basada en histograma). El número de contenedores afecta la estimación: muy pocos contenedores pierden inentrenamiento, demasiados contenedores añaden ruido. Una elección común: sqrt(n) bins o regla 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]

Eliminación de funciones recursivas (RFE)

RFE es un método contenedor. Utiliza la importancia de las características propias de un modelo para podar iterativamente:

  1. Entrena el modelo con todas las funciones.
  2. Clasificar las características por importancia (coeficientes para modelos lineales, reducción de impurezas para árboles)
  3. Elimine las funciones menos importantes
  4. Repita hasta que quede la cantidad deseada de funciones.
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 las interacciones de funciones porque el modelo ve todas las funciones restantes juntas. Eliminar una característica cambia la importancia de otras. Esto lo hace más completo que los métodos de filtrado.

El costo: entrenas el modelo N - tiempos objetivo. Con 500 funciones y un objetivo de 10, son 490 carreras de entrenamiento. Para modelos caros, esto es lento. Puedes acelerarlo eliminando varias funciones por paso (por ejemplo, eliminando el 10% inferior en cada ronda).

L1 (Lasso) Regularización

La regularización L1 agrega el valor absoluto de los pesos a la función de pérdida:

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

El parámetro alfa controla la agresividad con la que se eliminan las características. Un alfa más alto significa que más pesos llegan exactamente a cero.

¿Por qué exactamente cero? La penalización L1 crea una región de restricción en forma de diamante en el espacio de peso. La solución óptima tiende a aterrizar en una esquina de este diamante, donde uno o más pesos son cero. La regularización L2 (cresta) crea una restricción circular donde los pesos se reducen pero rara vez llegan a cero.

Se trata de una selección de funciones integrada: el modelo aprende durante el entrenamiento qué funciones ignorar. Las características con peso cero se eliminan de manera efectiva.

Ventajas: ejecución de entrenamiento única, maneja características correlacionadas (selecciona una y pone a cero las demás), integrada en la mayoría de las implementaciones de modelos lineales.

Limitación: solo funciona para modelos lineales. No se puede capturar la importancia de la característica no lineal.

Importancia de las funciones basadas en árboles

Los árboles de decisión y sus conjuntos (bosques aleatorios, aumento de gradiente) clasifican las características de forma natural. Cada división reduce la impureza (Gini o entropía para clasificación, varianza para regresión). Las características que producen mayores reducciones de impurezas son más importantes.

Para un random forest con árboles T:

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

Esto proporciona una puntuación de importancia normalizada para cada característica. Maneja relaciones no lineales e interacciones de características automáticamente.

Precaución: la importancia basada en árboles está sesgada hacia características con muchos valores únicos (alta cardinalidad). Una columna de ID aleatoria parecerá importante porque divide perfectamente cada muestra. Utilice la importancia de la permutación como control de cordura.

Importancia de la permutación

Un método independiente del modelo:

  1. Entrene el modelo y registre el rendimiento de referencia en los datos de validación.
  2. Para cada característica: mezcle sus valores aleatoriamente, mida la caída en el rendimiento
  3. Cuanto mayor es la caída, más importante es la característica

Si mezclar una característica no afecta el rendimiento, el modelo no depende de ello. Si el rendimiento colapsa, esa característica es crítica.

La importancia de la permutación evita el sesgo de cardinalidad de la importancia basada en árboles. Pero es lento: una evaluación completa por característica, repetida varias veces para mayor estabilidad.

Tabla comparativa

Método Tipo Velocidad No lineal Interacciones de funciones
Umbral de variación Filtro Muy rápido No No
Inentrenamiento mutua Filtro Rápido No
Filtro de correlación Filtro Rápido No No
RFE Envoltorio Lento Depende del modelo
L1 / Lasso Integrado Rápido No (lineal) No
Importancia del árbol Integrado Medio
Importancia de la permutación Independiente del modelo Lento

Diagrama de flujo de decisión

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"]

Constrúyelo

Paso 1: Generar datos sintéticos con estructura de características conocida

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

Conocemos la verdad fundamental: las características 0-4 son informativas (más 3 y 4 son copias correlacionadas de 0 y 1), las características 5-9 están correlacionadas con características informativas, las características 10-19 son puro ruido. Un buen método de selección debería clasificar del 0 al 4 más alto y del 10 al 19 más bajo.

Paso 2: Umbral de varianza

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

Paso 3: Inentrenamiento mutua (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

Paso 4: Eliminación de funciones recursivas

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

Paso 5: selección de funciones 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

Paso 6: Importancia basada en árbol (árbol de decisión simple)

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

Paso 7: ejecutar todos los métodos y comparar

El archivo de código ejecuta los cinco métodos en el mismo conjunto de datos sintéticos e imprime una tabla de comparación que muestra qué características selecciona cada método.

Úsalo

Con scikit-learn, la selección de funciones está integrada en el proceso:

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_

Las implementaciones desde cero muestran exactamente lo que sucede dentro de cada método. El umbral de variación es simplemente calcular var(X, axis=0) y aplicar una máscara. La inentrenamiento mutua es contar frecuencias conjuntas y marginales en una tabla de contingencia. RFE es un bucle que entrena, clasifica y poda. L1 es un descenso en gradiente con un paso de umbral suave. La importancia del árbol acumula reducciones de impurezas en las divisiones. No hay magia, sólo estadísticas y bucles.

Las versiones de sklearn agregan solidez (por ejemplo, mutual_info_classif usa estimación de densidad k-NN en lugar de agrupación), velocidad (implementaciones de C) e integración de canalizaciones.

Envíalo

Esta lección produce:

  • outputs/skill-feature-selector.md: un árbol de decisión de referencia rápida para elegir el método de selección de características correcto

Ejercicios

  1. Selección directa: implemente lo contrario de RFE. Comience sin funciones. En cada paso, agregue la característica que mejore más el rendimiento del modelo. Deténgase cuando agregar funciones ya no ayude. Compare las funciones seleccionadas con los resultados de RFE. ¿Cuál es más rápido? ¿Cuál da mejores resultados?

  2. Selección de estabilidad: ejecute la selección de características L1 50 veces, cada vez en una submuestra aleatoria del 80% de los datos, con valores alfa ligeramente diferentes. Cuente con qué frecuencia se selecciona cada característica. Las características seleccionadas en >80% de las ejecuciones son "estables". Compare características estables con la selección L1 de ejecución única. ¿Cuál es más confiable?

  3. Detección de multicolinealidad: calcule la matriz de correlación para todas las características. Implemente una función que, dado un umbral de correlación (por ejemplo, 0,9), elimine una característica de cada par altamente correlacionado (manteniendo la que tiene mayor inentrenamiento mutua con el objetivo). Pruebe el conjunto de datos sintéticos y verifique que elimine las características correlacionadas redundantes.

  4. Canalización de selección de funciones: umbral de variación de la cadena, filtro de inentrenamiento mutua y RFE en una sola canalización. Primero elimine las características de variación casi nula, luego mantenga el 50% superior según la inentrenamiento mutua y luego ejecute RFE con los supervivientes. Compare esta canalización con la ejecución de RFE sola en todas las funciones. ¿Es el oleoducto más rápido? ¿Es igualmente exacto?

  5. Importancia de la permutación desde cero: implementar la importancia de la permutación. Para cada característica, mezcle sus valores 10 veces y mida la caída promedio en la score F1. Compare la clasificación con la importancia basada en árboles. Encuentre casos en los que no estén de acuerdo y explique por qué (pista: características correlacionadas).

Términos clave

Término Lo que dice la gente Lo que realmente significa
Método de filtrado "Puntuar funciones de forma independiente" Un enfoque de selección de características que clasifica las características utilizando una medida estadística sin entrenar un modelo, evaluando cada característica de forma aislada
Método de envoltura "Utilice el modelo para seleccionar funciones" Un enfoque de selección de características que evalúa subconjuntos de características entrenando un modelo y utilizando su desempeño como criterio de selección
Método integrado "El modelo selecciona características durante el entrenamiento" Selección de características que ocurre como parte del ajuste del modelo, como la regularización L1 que lleva los pesos a cero
Inentrenamiento mutua "Cuánto te dice una variable sobre otra" Una medida de la reducción de la incertidumbre sobre Y dado el conocimiento de X, que captura dependencias tanto lineales como no lineales
Eliminación de funciones recursivas "Entrena, clasifica, poda, repite" Un método contenedor iterativo que entrena un modelo, elimina las características menos importantes y se repite hasta alcanzar un recuento objetivo.
L1 / Lasso regularización "Penalti que mata características" Agregar la suma de los valores de peso absoluto a la función de pérdida, lo que lleva los pesos de características sin importancia a exactamente cero
Umbral de variación "Eliminar funciones constantes" Eliminar características cuya variación entre muestras cae por debajo de un umbral específico, filtrar características que no contienen inentrenamiento
Importancia de la característica "Qué características son más importantes" Una puntuación que indica cuánto contribuye cada característica a las predicciones del modelo, calculada a partir de ganancias divididas (árboles) o magnitudes de coeficientes (lineales)
Importancia de la permutación "Baraja y mide el daño" Evaluar la importancia de las características mezclando aleatoriamente los valores de cada característica y midiendo la caída resultante en el rendimiento del modelo
Maldición de la dimensionalidad "Demasiadas funciones, pocos datos" El fenómeno por el que agregar características aumenta exponencialmente el volumen del espacio de características, lo que hace que los datos sean escasos y las distancias carezcan de sentido

Lectura adicional

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