DEV Community

Jesus Oviedo Riquelme
Jesus Oviedo Riquelme

Posted on

MLZC25-14. Regularización y Tuning del Modelo: La Batalla contra el Sobreajuste

🎯 Objetivo del Post: Aprenderás a combatir el sobreajuste usando técnicas de regularización profesionales y a optimizar hiperparámetros para obtener el mejor rendimiento posible.

📚 ¿Qué es el Sobreajuste y por qué es un Problema?

Imagina que estás estudiando para un examen memorizando las respuestas exactas de los ejercicios de práctica. Cuando llegue el examen real con preguntas similares pero diferentes, probablemente no te vaya bien. Esto es exactamente lo que pasa con el sobreajuste en Machine Learning.

⚠️ Definición Crítica: El sobreajuste (overfitting) ocurre cuando un modelo aprende demasiado bien los datos de entrenamiento, incluyendo el ruido y las particularidades específicas, pero no puede generalizar a datos nuevos.

🚨 Señales de Sobreajuste

  1. 📈 Rendimiento en entrenamiento muy alto, rendimiento en validación bajo
  2. 📊 Gap grande entre RMSE de entrenamiento y validación
  3. 🔢 Coeficientes muy grandes o inestables
  4. 💥 Modelo que funciona perfectamente en datos conocidos pero falla en datos nuevos

💡 Regla de Oro: Si tu modelo es perfecto en entrenamiento pero terrible en validación, tienes sobreajuste.

Ejemplo Visual del Problema

import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.metrics import mean_squared_error

# Crear datos sintéticos
np.random.seed(42)
X = np.linspace(0, 10, 50).reshape(-1, 1)
y = 2 * X.flatten() + 1 + np.random.normal(0, 2, 50)

# Modelo simple (grado 1)
modelo_simple = LinearRegression()
modelo_simple.fit(X, y)
y_pred_simple = modelo_simple.predict(X)

# Modelo complejo (grado 15 - sobreajustado)
poly = PolynomialFeatures(degree=15)
X_poly = poly.fit_transform(X)
modelo_complejo = LinearRegression()
modelo_complejo.fit(X_poly, y)
y_pred_complejo = modelo_complejo.predict(X_poly)

# Visualizar
plt.figure(figsize=(15, 5))

plt.subplot(1, 3, 1)
plt.scatter(X, y, alpha=0.6, label='Datos reales')
plt.plot(X, y_pred_simple, 'r-', label='Modelo simple')
plt.title('Modelo Simple (Grado 1)')
plt.legend()

plt.subplot(1, 3, 2)
plt.scatter(X, y, alpha=0.6, label='Datos reales')
plt.plot(X, y_pred_complejo, 'g-', label='Modelo complejo')
plt.title('Modelo Complejo (Grado 15)')
plt.legend()

# Calcular errores
mse_simple = mean_squared_error(y, y_pred_simple)
mse_complejo = mean_squared_error(y, y_pred_complejo)

plt.subplot(1, 3, 3)
modelos = ['Simple', 'Complejo']
mse_values = [mse_simple, mse_complejo]
plt.bar(modelos, mse_values, color=['blue', 'red'])
plt.title('Error en Datos de Entrenamiento')
plt.ylabel('MSE')
plt.show()

print(f"MSE Modelo Simple: {mse_simple:.2f}")
print(f"MSE Modelo Complejo: {mse_complejo:.2f}")
Enter fullscreen mode Exit fullscreen mode

¿Qué es la Regularización?

La regularización es una técnica que penaliza la complejidad del modelo para evitar el sobreajuste. Es como poner límites de velocidad en una carretera para evitar accidentes.

Principios de la Regularización

  1. Penalizar coeficientes grandes: Coeficientes muy grandes pueden indicar sobreajuste
  2. Reducir la varianza: Hacer el modelo más estable
  3. Mejorar la generalización: Que funcione mejor en datos nuevos

Tipos de Regularización

1. Ridge Regression (Regularización L2)

Ridge penaliza la suma de los cuadrados de los coeficientes.

Fórmula: Costo = MSE + α × Σ(coeficiente²)

Donde α (alpha) es el parámetro de regularización.

from sklearn.linear_model import Ridge
from sklearn.preprocessing import StandardScaler

# Cargar datos preparados
df = pd.read_csv('car_data_final.csv')
X = df.drop('precio', axis=1)
y = df['precio']

# Dividir datos
from sklearn.model_selection import train_test_split
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)

# Normalizar datos (importante para regularización)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_val_scaled = scaler.transform(X_val)

# Comparar modelos con diferentes valores de alpha
alphas = [0, 0.1, 1, 10, 100, 1000]
results_ridge = []

for alpha in alphas:
    # Modelo Ridge
    modelo_ridge = Ridge(alpha=alpha)
    modelo_ridge.fit(X_train_scaled, y_train)

    # Predicciones
    y_pred_train = modelo_ridge.predict(X_train_scaled)
    y_pred_val = modelo_ridge.predict(X_val_scaled)

    # Métricas
    rmse_train = np.sqrt(mean_squared_error(y_train, y_pred_train))
    rmse_val = np.sqrt(mean_squared_error(y_val, y_pred_val))

    results_ridge.append({
        'alpha': alpha,
        'rmse_train': rmse_train,
        'rmse_val': rmse_val,
        'gap': rmse_val - rmse_train
    })

# Crear DataFrame con resultados
results_df = pd.DataFrame(results_ridge)
print("Resultados Ridge Regression:")
print(results_df)
Enter fullscreen mode Exit fullscreen mode

2. Lasso Regression (Regularización L1)

Lasso penaliza la suma de los valores absolutos de los coeficientes.

Fórmula: Costo = MSE + α × Σ|coeficiente|

from sklearn.linear_model import Lasso

# Probar diferentes valores de alpha para Lasso
alphas_lasso = [0.001, 0.01, 0.1, 1, 10]
results_lasso = []

for alpha in alphas_lasso:
    # Modelo Lasso
    modelo_lasso = Lasso(alpha=alpha, max_iter=10000)
    modelo_lasso.fit(X_train_scaled, y_train)

    # Predicciones
    y_pred_train = modelo_lasso.predict(X_train_scaled)
    y_pred_val = modelo_lasso.predict(X_val_scaled)

    # Métricas
    rmse_train = np.sqrt(mean_squared_error(y_train, y_pred_train))
    rmse_val = np.sqrt(mean_squared_error(y_val, y_pred_val))

    # Contar características seleccionadas (coeficientes no cero)
    n_features = np.sum(modelo_lasso.coef_ != 0)

    results_lasso.append({
        'alpha': alpha,
        'rmse_train': rmse_train,
        'rmse_val': rmse_val,
        'n_features': n_features,
        'gap': rmse_val - rmse_train
    })

# Crear DataFrame con resultados
results_lasso_df = pd.DataFrame(results_lasso)
print("Resultados Lasso Regression:")
print(results_lasso_df)
Enter fullscreen mode Exit fullscreen mode

3. Elastic Net (Combinación de L1 y L2)

Elastic Net combina Ridge y Lasso, dando lo mejor de ambos mundos.

from sklearn.linear_model import ElasticNet

# Elastic Net con diferentes parámetros
modelo_elastic = ElasticNet(alpha=0.1, l1_ratio=0.5, max_iter=10000)
modelo_elastic.fit(X_train_scaled, y_train)

y_pred_elastic = modelo_elastic.predict(X_val_scaled)
rmse_elastic = np.sqrt(mean_squared_error(y_val, y_pred_elastic))

print(f"RMSE Elastic Net: ${rmse_elastic:,.2f}")
Enter fullscreen mode Exit fullscreen mode

Visualización de Regularización

Gráfico de Coeficientes vs Alpha

# Visualizar cómo cambian los coeficientes con la regularización
plt.figure(figsize=(15, 10))

# Ridge
plt.subplot(2, 2, 1)
alphas_plot = np.logspace(-3, 3, 50)
coefs_ridge = []
for alpha in alphas_plot:
    ridge = Ridge(alpha=alpha)
    ridge.fit(X_train_scaled, y_train)
    coefs_ridge.append(ridge.coef_)

coefs_ridge = np.array(coefs_ridge)
for i in range(min(10, coefs_ridge.shape[1])):
    plt.plot(alphas_plot, coefs_ridge[:, i], label=f'Feature {i}')
plt.xscale('log')
plt.xlabel('Alpha')
plt.ylabel('Coeficiente')
plt.title('Ridge: Coeficientes vs Alpha')
plt.legend()

# Lasso
plt.subplot(2, 2, 2)
coefs_lasso = []
for alpha in alphas_plot:
    lasso = Lasso(alpha=alpha, max_iter=10000)
    lasso.fit(X_train_scaled, y_train)
    coefs_lasso.append(lasso.coef_)

coefs_lasso = np.array(coefs_lasso)
for i in range(min(10, coefs_lasso.shape[1])):
    plt.plot(alphas_plot, coefs_lasso[:, i], label=f'Feature {i}')
plt.xscale('log')
plt.xlabel('Alpha')
plt.ylabel('Coeficiente')
plt.title('Lasso: Coeficientes vs Alpha')
plt.legend()

# Comparación de RMSE
plt.subplot(2, 2, 3)
plt.plot(results_df['alpha'], results_df['rmse_train'], 'o-', label='Entrenamiento')
plt.plot(results_df['alpha'], results_df['rmse_val'], 's-', label='Validación')
plt.xscale('log')
plt.xlabel('Alpha')
plt.ylabel('RMSE')
plt.title('Ridge: RMSE vs Alpha')
plt.legend()

plt.subplot(2, 2, 4)
plt.plot(results_lasso_df['alpha'], results_lasso_df['rmse_train'], 'o-', label='Entrenamiento')
plt.plot(results_lasso_df['alpha'], results_lasso_df['rmse_val'], 's-', label='Validación')
plt.xscale('log')
plt.xlabel('Alpha')
plt.ylabel('RMSE')
plt.title('Lasso: RMSE vs Alpha')
plt.legend()

plt.tight_layout()
plt.show()
Enter fullscreen mode Exit fullscreen mode

Tuning de Hiperparámetros

¿Qué son los Hiperparámetros?

Los hiperparámetros son parámetros que no se aprenden durante el entrenamiento, sino que se establecen antes. Ejemplos:

  • Alpha en Ridge/Lasso (fuerza de regularización)
  • Grado del polinomio en regresión polinomial
  • Número de características a usar

Grid Search: Búsqueda Exhaustiva

from sklearn.model_selection import GridSearchCV

# Grid Search para Ridge
param_grid_ridge = {
    'alpha': [0.001, 0.01, 0.1, 1, 10, 100, 1000]
}

grid_ridge = GridSearchCV(
    Ridge(),
    param_grid_ridge,
    cv=5,
    scoring='neg_mean_squared_error',
    return_train_score=True
)

grid_ridge.fit(X_train_scaled, y_train)

print("Mejores parámetros Ridge:")
print(grid_ridge.best_params_)
print(f"Mejor RMSE: ${np.sqrt(-grid_ridge.best_score_):,.2f}")

# Grid Search para Lasso
param_grid_lasso = {
    'alpha': [0.001, 0.01, 0.1, 1, 10],
    'max_iter': [1000, 5000, 10000]
}

grid_lasso = GridSearchCV(
    Lasso(),
    param_grid_lasso,
    cv=5,
    scoring='neg_mean_squared_error',
    return_train_score=True
)

grid_lasso.fit(X_train_scaled, y_train)

print("\nMejores parámetros Lasso:")
print(grid_lasso.best_params_)
print(f"Mejor RMSE: ${np.sqrt(-grid_lasso.best_score_):,.2f}")
Enter fullscreen mode Exit fullscreen mode

Random Search: Búsqueda Aleatoria

from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import uniform, randint

# Random Search para Elastic Net
param_dist_elastic = {
    'alpha': uniform(0.001, 10),
    'l1_ratio': uniform(0, 1),
    'max_iter': randint(1000, 10000)
}

random_elastic = RandomizedSearchCV(
    ElasticNet(),
    param_dist_elastic,
    n_iter=100,
    cv=5,
    scoring='neg_mean_squared_error',
    random_state=42
)

random_elastic.fit(X_train_scaled, y_train)

print("Mejores parámetros Elastic Net:")
print(random_elastic.best_params_)
print(f"Mejor RMSE: ${np.sqrt(-random_elastic.best_score_):,.2f}")
Enter fullscreen mode Exit fullscreen mode

Validación Cruzada para Selección de Modelo

Comparación Completa de Modelos

from sklearn.linear_model import LinearRegression
from sklearn.model_selection import cross_val_score

# Lista de modelos a comparar
modelos = {
    'Linear Regression': LinearRegression(),
    'Ridge (α=1)': Ridge(alpha=1),
    'Ridge (α=10)': Ridge(alpha=10),
    'Lasso (α=0.1)': Lasso(alpha=0.1, max_iter=10000),
    'Lasso (α=1)': Lasso(alpha=1, max_iter=10000),
    'Elastic Net': ElasticNet(alpha=0.1, l1_ratio=0.5, max_iter=10000)
}

# Evaluar cada modelo con validación cruzada
resultados_cv = {}
for nombre, modelo in modelos.items():
    scores = cross_val_score(
        modelo, X_train_scaled, y_train, 
        cv=5, scoring='neg_mean_squared_error'
    )
    rmse_cv = np.sqrt(-scores)
    resultados_cv[nombre] = {
        'rmse_mean': rmse_cv.mean(),
        'rmse_std': rmse_cv.std()
    }

# Crear DataFrame con resultados
cv_df = pd.DataFrame(resultados_cv).T
cv_df = cv_df.sort_values('rmse_mean')

print("Resultados de Validación Cruzada:")
print(cv_df.round(2))

# Visualizar resultados
plt.figure(figsize=(12, 6))
modelos_nombres = cv_df.index
rmse_means = cv_df['rmse_mean']
rmse_stds = cv_df['rmse_std']

plt.errorbar(range(len(modelos_nombres)), rmse_means, yerr=rmse_stds, 
             fmt='o', capsize=5, capthick=2)
plt.xticks(range(len(modelos_nombres)), modelos_nombres, rotation=45)
plt.ylabel('RMSE ($)')
plt.title('Comparación de Modelos (Validación Cruzada)')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Enter fullscreen mode Exit fullscreen mode

Evaluación Final en Conjunto de Prueba

Seleccionar el Mejor Modelo

# Entrenar el mejor modelo (Ridge con alpha=10)
mejor_modelo = Ridge(alpha=10)
mejor_modelo.fit(X_train_scaled, y_train)

# Predicciones en conjunto de prueba
y_pred_test = mejor_modelo.predict(X_val_scaled)

# Métricas finales
rmse_final = np.sqrt(mean_squared_error(y_val, y_pred_test))
mae_final = mean_absolute_error(y_val, y_pred_test)
r2_final = r2_score(y_val, y_pred_test)

print("Rendimiento Final del Modelo:")
print(f"RMSE: ${rmse_final:,.2f}")
print(f"MAE:  ${mae_final:,.2f}")
print(f"R²:   {r2_final:.3f}")

# Comparar con modelo sin regularización
modelo_sin_reg = LinearRegression()
modelo_sin_reg.fit(X_train_scaled, y_train)
y_pred_sin_reg = modelo_sin_reg.predict(X_val_scaled)
rmse_sin_reg = np.sqrt(mean_squared_error(y_val, y_pred_sin_reg))

mejora = ((rmse_sin_reg - rmse_final) / rmse_sin_reg) * 100
print(f"\nMejora con regularización: {mejora:.1f}%")
Enter fullscreen mode Exit fullscreen mode

Análisis de Coeficientes Finales

# Analizar coeficientes del mejor modelo
coef_df = pd.DataFrame({
    'caracteristica': X.columns,
    'coeficiente': mejor_modelo.coef_,
    'abs_coeficiente': np.abs(mejor_modelo.coef_)
})

coef_df = coef_df.sort_values('abs_coeficiente', ascending=False)

print("Top 10 características más importantes:")
print(coef_df.head(10))

# Visualizar coeficientes
plt.figure(figsize=(12, 8))
top_features = coef_df.head(15)
plt.barh(range(len(top_features)), top_features['coeficiente'])
plt.yticks(range(len(top_features)), top_features['caracteristica'])
plt.xlabel('Coeficiente')
plt.title('Coeficientes del Mejor Modelo (Ridge α=10)')
plt.axvline(x=0, color='black', linestyle='--', alpha=0.5)
plt.tight_layout()
plt.show()
Enter fullscreen mode Exit fullscreen mode

Mejores Prácticas

1. Siempre Normalizar para Regularización

# CRÍTICO: Normalizar antes de regularización
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
Enter fullscreen mode Exit fullscreen mode

2. Usar Validación Cruzada

# Para selección de hiperparámetros
from sklearn.model_selection import cross_val_score
scores = cross_val_score(modelo, X, y, cv=5)
Enter fullscreen mode Exit fullscreen mode

3. Separar Conjuntos de Datos

# Nunca uses datos de prueba para tuning
X_train, X_temp, y_train, y_temp = train_test_split(X, y, test_size=0.4)
X_val, X_test, y_val, y_test = train_test_split(X_temp, y_temp, test_size=0.5)
Enter fullscreen mode Exit fullscreen mode

4. Documentar Proceso de Tuning

# Guardar configuración final
configuracion_final = {
    'modelo': 'Ridge',
    'alpha': 10,
    'normalizacion': 'StandardScaler',
    'cv_score': cv_df.loc['Ridge (α=10)', 'rmse_mean'],
    'test_rmse': rmse_final
}
Enter fullscreen mode Exit fullscreen mode

Errores Comunes

1. No Normalizar Datos

  • La regularización asume que todas las características están en la misma escala
  • Solución: Siempre usar StandardScaler o MinMaxScaler

2. Usar Datos de Prueba para Tuning

  • Esto causa overfitting en la selección de hiperparámetros
  • Solución: Usar validación cruzada o conjunto de validación separado

3. Alpha Muy Alto o Muy Bajo

  • Alpha muy alto: underfitting (modelo demasiado simple)
  • Alpha muy bajo: overfitting (modelo demasiado complejo)
  • Solución: Usar grid search o random search

4. Ignorar la Interpretabilidad

  • La regularización puede hacer los coeficientes menos interpretables
  • Solución: Balancear rendimiento e interpretabilidad

Conclusión

La regularización y el tuning de hiperparámetros son técnicas esenciales para crear modelos robustos y generalizables. Nos ayudan a:

  • Evitar el sobreajuste: Mantener el modelo simple y estable
  • Mejorar la generalización: Que funcione bien en datos nuevos
  • Optimizar el rendimiento: Encontrar la configuración óptima
  • Seleccionar características: Lasso puede eliminar características irrelevantes

Puntos clave:

  • Ridge (L2) reduce coeficientes pero no los elimina
  • Lasso (L1) puede eliminar características completamente
  • Elastic Net combina lo mejor de ambos
  • Siempre normalizar antes de regularizar
  • Usar validación cruzada para selección de hiperparámetros

Próximos pasos:

  • Experimentar con otros algoritmos (Random Forest, XGBoost)
  • Aplicar regularización a modelos más complejos
  • Implementar pipelines de ML automatizados
  • Desplegar el modelo en producción

¡Has completado un proyecto completo de Machine Learning! Desde la preparación de datos hasta la optimización del modelo, has aprendido las técnicas fundamentales que se usan en la industria.


¿Has usado regularización en tus proyectos? ¿Qué técnicas de tuning de hiperparámetros prefieres usar?

Top comments (0)