ML/AIMLE·AIMachine Learning Engineering
Machine Learning · Ensembles · Artículo completo

Gradient Boosting y XGBoost

Árboles pequeños que se corrigen unos a otros siguiendo el gradiente de la pérdida: el algoritmo detrás de XGBoost y LightGBM, implementado desde cero y puesto a prueba con resultados reales.

Resultados obtenidos con scikit-learn 1.8.0 y XGBoost 3.2.0 · random_state = 42 · Laboratorio relacionado

01Qué es

El gradient boosting construye un modelo sumando muchos árboles de decisión pequeños, uno detrás de otro. Cada árbol nuevo se entrena para corregir los errores que todavía comete la suma de los anteriores. Jerome Friedman lo formuló en 2001 como un descenso de gradiente en el espacio de funciones.

XGBoost, LightGBM, CatBoost y el HistGradientBoosting de scikit-learn son implementaciones de esta idea con optimizaciones de velocidad y regularización. Con datos tabulares, es la familia de modelos que más suele ganar.

02Intuición

Piensa en un golfista: el primer golpe deja la bola lejos del hoyo, y cada golpe siguiente se da desde donde quedó, apuntando a lo que falta. El primer «golpe» es predecir la media; cada árbol siguiente apunta al residuo que queda.

La diferencia con Random Forest es de fondo. El bosque entrena árboles grandes e independientes y los promedia para reducir la varianza. El boosting entrena árboles pequeños (poco profundos, con mucho sesgo) en serie para reducir el sesgo. Por eso cada árbol aporta solo una fracción de su predicción, la tasa de aprendizaje: pasos pequeños para no pasarse.

03Fundamento matemático

Se busca una función F que minimice una pérdida L. En vez de mover pesos, se mueve la propia función, sumándole en cada paso algo que apunte en contra del gradiente:

F0(x)=arg⁡min⁡c∑iL(yi,c),rim=−[∂L(yi,F(xi))∂F(xi)]F=Fm−1F_0(x) = \arg\min_c \sum_i L(y_i, c), \qquad r_{im} = -\left[\frac{\partial L(y_i, F(x_i))}{\partial F(x_i)}\right]_{F = F_{m-1}}

Esos rimr_{im} son los pseudo-residuos. Se ajusta un árbol hmh_m a ellos y se avanza un paso de tamaño η:

Fm(x)=Fm−1(x)+η hm(x)F_m(x) = F_{m-1}(x) + \eta\, h_m(x)

Con la pérdida cuadrática L=12(y−F)2L = \tfrac{1}{2}(y - F)^2, el gradiente negativo es exactamente y−Fy - F: el residuo de toda la vida. Con otras pérdidas (log loss para clasificación, Huber para robustez) cambia solo esta fórmula.

XGBoost añade un término de regularización a cada árbol y usa también la segunda derivada (aproximación de Newton):

Ω(h)=γT+12λ∑j=1Twj2,wj∗=−∑i∈Ijgi∑i∈Ijhi+λ\Omega(h) = \gamma T + \tfrac{1}{2}\lambda \sum_{j=1}^{T} w_j^2, \qquad w_j^* = -\frac{\sum_{i \in I_j} g_i}{\sum_{i \in I_j} h_i + \lambda}

donde T es el número de hojas, wjw_j el valor de cada hoja, y gi,hig_i, h_i la primera y segunda derivada de la pérdida.

04Algoritmo paso a paso

  1. Empieza prediciendo una constante: la media (regresión) o el log-odds de la clase positiva (clasificación).
  2. Calcula, para cada ejemplo, el gradiente negativo de la pérdida respecto a la predicción actual.
  3. Entrena un árbol poco profundo (2 a 6 niveles) para predecir esos pseudo-residuos.
  4. Suma a la predicción la salida de ese árbol multiplicada por la tasa de aprendizaje.
  5. Repite hasta el número de árboles fijado o hasta que el error de validación deje de bajar.

05Pseudocódigo

función GRADIENT_BOOSTING(X, y, M, η, profundidad):
    F ← media(y)
    árboles ← []
    para m ← 1 hasta M:
        r ← −∂L(y, F)/∂F            # con pérdida cuadrática: r = y − F
        h ← ÁRBOL_REGRESIÓN(X, r, profundidad)
        F ← F + η · h(X)
        añadir h a árboles
    devolver media(y), árboles

función PREDECIR(x):
    devolver F₀ + η · Σ h(x) para h en árboles

06Implementación desde cero en Python

Python
import numpy as np
from sklearn.tree import DecisionTreeRegressor

class GradientBoosting:
    def __init__(self, n_estimators=100, learning_rate=0.1, max_depth=2):
        self.n, self.lr, self.depth = n_estimators, learning_rate, max_depth

    def fit(self, X, y):
        self.f0 = y.mean()                          # modelo inicial: la media
        pred = np.full(len(y), self.f0)
        self.arboles = []
        for _ in range(self.n):
            residuo = y - pred                      # = −gradiente de ½(y − f)²
            arbol = DecisionTreeRegressor(max_depth=self.depth).fit(X, residuo)
            pred += self.lr * arbol.predict(X)      # un paso pequeño en esa dirección
            self.arboles.append(arbol)
        return self

    def predict(self, X):
        return self.f0 + self.lr * sum(a.predict(X) for a in self.arboles)

Usa los árboles de regresión de scikit-learn como pieza base, pero el boosting en sí son esas 15 líneas. En el problema no lineal de la sección de evaluación obtiene un R² de 0,929, idéntico al de GradientBoostingRegressor con los mismos parámetros.

07Implementación con Scikit-learn y XGBoost

Python
from sklearn.ensemble import GradientBoostingRegressor, HistGradientBoostingRegressor
import xgboost as xgb

gbr = GradientBoostingRegressor(n_estimators=300, learning_rate=0.05, max_depth=2, random_state=42)
hgb = HistGradientBoostingRegressor(max_iter=300, learning_rate=0.05, max_depth=3)   # rápido con datos grandes
xgbm = xgb.XGBRegressor(n_estimators=300, learning_rate=0.05, max_depth=2, subsample=0.8, reg_lambda=1.0)

for m in (gbr, hgb, xgbm):
    m.fit(X_tr, y_tr)

HistGradientBoosting* agrupa cada variable en hasta 255 intervalos antes de buscar cortes, lo que lo hace muchísimo más rápido con datos grandes; admite valores faltantes y variables categóricas de forma nativa, como LightGBM. XGBoost ofrece además entrenamiento en GPU y una regularización más fina.

08Datasets de ejemplo

Se usan dos problemas de regresión para ver cuándo compensa el boosting y cuándo no:

  • Diabetes (442 pacientes, 10 variables), el mismo del artículo de regresión lineal: pocos datos y una relación casi lineal.
  • Friedman #1, un problema sintético clásico (Friedman, 1991) con interacciones y curvas, 2.000 ejemplos y 10 variables de las que solo 5 influyen:
Python
from sklearn.datasets import make_friedman1

# y = 10·sin(π·x₁·x₂) + 20·(x₃ − 0,5)² + 10·x₄ + 5·x₅ + ruido  (x₆…x₁₀ no influyen)
X, y = make_friedman1(n_samples=2000, noise=1.0, random_state=42)

09Entrenamiento

Más árboles siempre bajan el error de entrenamiento, pero no el de test. Con 300 árboles y η = 0,05 en Diabetes:

30405060708011050100200300número de árboles (escala por posición)mínimo en test
RMSE entrenamientoRMSE test
Implementación desde cero. El error de test toca fondo hacia los 100 árboles (52,4) y luego sube despacio mientras el de entrenamiento sigue bajando: sobreajuste.

La solución estándar es el early stopping: separar una parte de validación y parar cuando deja de mejorar.

Python
m = GradientBoostingRegressor(n_estimators=2000, learning_rate=0.05, max_depth=2,
                              validation_fraction=0.2, n_iter_no_change=20, random_state=42)
m.fit(X_tr, y_tr)
print(m.n_estimators_)     # 170: se detiene solo cuando la validación deja de mejorar

10Evaluación

Problema no lineal (Friedman #1, 500 test)

ModeloRMSER²Tiempo de entrenamiento
Regresión lineal2,4750,752< 0,01 s
Random Forest (300 árboles)1,8550,8611,8 s
Boosting desde cero (500, η = 0,05, prof. 3)1,3230,9292,6 s
GradientBoostingRegressor (mismos parámetros)1,3240,9292,5 s
XGBoost (+ subsample 0,8)1,2590,9360,27 s

Problema casi lineal y con pocos datos (Diabetes, validación cruzada de 5)

ModeloR² medioDesviación
Regresión lineal0,4780,085
XGBoost0,4410,084
GradientBoostingRegressor0,4360,096
Random Forest0,4270,095

Las dos tablas cuentan la historia completa. Cuando hay interacciones y curvas, el boosting reduce el error a la mitad respecto al modelo lineal y supera al bosque. Cuando los datos son pocos y la relación es casi lineal, la regresión lineal gana. No existe un algoritmo mejor en todos los problemas; por eso se valida siempre contra una línea base.

11Métricas

Las mismas que en cualquier regresión (RMSE, MAE, R², ver el artículo de regresión lineal) o clasificación (log loss, ROC-AUC). La diferencia práctica es que el boosting optimiza directamente la pérdida que elijas, así que conviene que la pérdida de entrenamiento coincida con la métrica que te importa: loss="absolute_error" si mides MAE, loss="huber" si hay valores atípicos.

12Visualización

0204060800,010,050,10,31,0tasa de aprendizaje η con 300 árboles fijos
RMSE entrenamientoRMSE test
Diabetes. Con η = 1 el modelo memoriza el entrenamiento (RMSE 2,3) y se desploma en test (74,4). Tasa pequeña + más árboles + early stopping es la receta estable.
bmi · masa corporal0.368
s5 · log triglicéridos0.292
bp · tensión media0.108
s2 · LDL0.045
s3 · HDL0.043
Importancia de las variables (reducción de impureza) en Diabetes. A diferencia de los coeficientes lineales, aquí colesterol total y LDL no aparecen inflados por la multicolinealidad.

13Ventajas

  • Suele ser el modelo más preciso en datos tabulares.
  • Captura no linealidades e interacciones sin ingeniería de variables.
  • Flexible: cualquier pérdida diferenciable.
  • Las implementaciones modernas manejan valores faltantes y categóricas, y escalan a millones de filas.
  • No necesita escalar las variables.

14Limitaciones

  • Más hiperparámetros que un Random Forest y más sensibles; sin early stopping sobreajusta.
  • Entrenamiento secuencial: cada árbol depende del anterior (se paraleliza dentro de cada árbol, no entre árboles).
  • Como todos los árboles, no extrapola fuera del rango de entrenamiento.
  • Con pocos datos o relaciones lineales puede ser peor que un modelo lineal, como se ha visto.

15Hiperparámetros

Parámetroscikit-learn (GradientBoosting)XGBoost 3.2Efecto
n_estimators100100Número de árboles. Fíjalo alto y usa early stopping.
learning_rate0.10.3Tamaño de cada paso. Más bajo necesita más árboles pero generaliza mejor.
max_depth36Profundidad de cada árbol; controla qué interacciones puede capturar.
subsample1.01.0Fracción de filas por árbol (stochastic boosting). 0,5–0,8 reduce varianza.
reg_lambda / l2_regularization— / 0.0 (Hist)1.0Penalización L2 de los valores de las hojas.
min_samples_leaf / min_child_weight11Tamaño mínimo de hoja; subirlo suaviza.
n_iter_no_change / early_stopping_roundsNoneNoneActiva el early stopping.

16Casos de uso

  • Competiciones de Kaggle con datos tabulares.
  • Riesgo de crédito, fraude y scoring.
  • Ranking en buscadores y recomendadores (LambdaMART).
  • Previsión de demanda con variables de calendario y retardos.

17Aplicaciones industriales

  • Mantenimiento predictivo: probabilidad de fallo a partir de sensores. Ver el proyecto.
  • Energía: previsión de producción renovable y demanda.
  • Telecomunicaciones: predicción de bajas y de incidencias en red.
  • Logística: tiempos de entrega y roturas de stock.

18Ejercicio práctico

Comprueba cómo el early stopping compensa una tasa de aprendizaje demasiado alta: ¿cuántos árboles elige para cada η y cuál es el error final?

Plantilla
# Ejercicio: sobreajuste con learning_rate alto y cómo lo arregla el early stopping.
for lr in (0.05, 0.3, 1.0):
    m = GradientBoostingRegressor(n_estimators=2000, learning_rate=lr, max_depth=2,
                                  validation_fraction=0.2, n_iter_no_change=20, random_state=42)
    # ... entrena, imprime m.n_estimators_ y el RMSE en test. ¿Cuántos árboles usa cada uno?

19Código completo

Todos los experimentos, incluidos los dos datasets y las comparaciones:

20Referencias y papers

  1. J. H. Friedman (2001). «Greedy Function Approximation: A Gradient Boosting Machine». The Annals of Statistics, 29(5), 1189–1232.
  2. Y. Freund, R. E. Schapire (1997). «A Decision-Theoretic Generalization of On-Line Learning and an Application to Boosting». Journal of Computer and System Sciences, 55(1), 119–139.
  3. J. H. Friedman (1991). «Multivariate Adaptive Regression Splines». The Annals of Statistics, 19(1), 1–67. (Origen del problema Friedman #1).
  4. T. Chen, C. Guestrin (2016). «XGBoost: A Scalable Tree Boosting System». Proceedings of KDD 2016. Ficha en la biblioteca.
  5. G. Ke et al. (2017). «LightGBM: A Highly Efficient Gradient Boosting Decision Tree». NeurIPS 2017.