Capítulo 22 de 37 · intermedio
AdaBoost
Qué cubre este capítulo
Allá en la semana 1 construimos un decision stump: un feature, un umbral, un solo corte recto. Es el clasificador útil más débil que existe, y con datos difíciles apenas le gana a un volado. Este capítulo toma ese mismo stump y hace algo que no debería funcionar — apila un montón de ellos hasta formar uno de los clasificadores más fuertes de la era previa al deep learning.
El truco es AdaBoost. Entrena los stumps en secuencia, y a cada stump nuevo se le indica que le importe más los puntos que los anteriores fallaron. Eso se logra cargando un peso en cada punto de entrenamiento: ajustas un stump sobre los datos ponderados, le das un voto proporcional a qué tan bien le fue, y luego le subes el peso a todo lo que falló para que el siguiente stump se incline hacia allá. Repite cincuenta veces y de una pila de líneas emerge una frontera que ninguna línea sola podría dibujar jamás.
Construimos todo desde cero en NumPy — el stump ponderado, el voto, el repesado, el conteo final — y luego dejamos que scikit-learn construya el mismo ensamble y verificamos que los números caigan en el mismo lugar. Y te toca verlo suceder: una ronda de boosting por cuadro, con los puntos difíciles hinchándose a la vista y jalando al siguiente stump hacia ellos mientras la frontera del ensamble se afila.
Un poco de historia
AdaBoost salió de una pregunta teórica que suena a acertijo. En 1990 Robert Schapire preguntó si un aprendiz "débil" — uno que solo tiene que hacerlo ligeramente mejor que adivinar al azar — podía potenciarse hasta ser uno "fuerte" que se acerque arbitrariamente a lo perfecto. La respuesta sorprendente fue que sí, y no era solo una demostración; era una receta.
Yoav Freund y Schapire convirtieron esa receta en un algoritmo práctico en un paper de conferencia de 1995, publicado completo como "A decision-theoretic generalization of on-line learning and an application to boosting" en 1997. Lo llamaron AdaBoost, abreviatura de adaptive boosting — adaptativo porque cada ronda se adapta a los errores de la anterior. Funcionó vergonzosamente bien, era casi trivial de implementar, y durante una década fue lo que agarrabas cuando querías accuracy sin complicarte. En 2003 los dos ganaron el Premio Gödel por él, que es más o menos la forma en que el mundo de la teoría dice que este sí importó.
El aprendiz débil de los demos originales, y el que usamos aquí, es el decision stump de la semana 1. Ese es el hilo conductor que vale la pena no soltar: el modelo más humilde del curso es el átomo con el que se construye AdaBoost.
La intuición
Estos son los datos con los que vamos a trabajar. Dos clases, y están acomodadas de la única manera que un stump no puede manejar: concéntricas. Una clase se sienta en una bolita al centro, la otra la rodea. No hay línea vertical u horizontal que puedas dibujar que deje a la mayoría de una clase de un lado y a la mayoría de la otra del otro — un solo stump se topa con un techo cerca del 67% aquí, apenas mejor que adivinar.
Ahora imagina entrenar stumps en fila. El primero dibuja su mejor línea única y acierta un pedazo del plano y falla otro. En lugar de tirarlo, lo conservamos, y tomamos nota: estos puntos de acá, los que falló, ahora importan más. El segundo stump entrena sobre esa nota — se le califica sobre los datos ponderados, así que acertar los puntos marcados vale más que acertar los fáciles, y dibuja una línea distinta para perseguirlos. Sus fallos se marcan a su vez. Sigue así y acumulas un comité de stumps, cada uno experto en una rebanada distinta de los casos difíciles, y cada uno con un voto proporcional a qué tan confiable fue. La mayoría ponderada del comité talla el anillo que ningún miembro podría tallar solo.
Esa es toda la idea, y la razón por la que funciona es que cada ronda está obligada a trabajar en la parte del problema que las rondas anteriores encontraron más difícil. Nada puede vivir de los puntos fáciles para siempre.
La matemática
Las etiquetas están en — no en — porque esa elección hace que toda la actualización colapse en aritmética de signos. Cada stump débil es una función que produce , y cada punto de entrenamiento carga un peso en la ronda , empezando uniforme en .
El stump de la ronda es el que minimiza el error ponderado — el peso total que descansa sobre los puntos que falla:
Como los pesos suman uno, es un número entre 0 y 1, y con pesos uniformes es simplemente la tasa de fallos ordinaria. De ahí calculamos el voto del stump, :
Mira lo que eso hace. Un stump con es un volado y recibe — voz cero. Un stump mejor que el azar recibe un voto positivo que crece conforme su error se encoge; un stump perfecto recibiría uno infinito. Luego repesamos, empujando peso hacia los puntos que este stump falló:
El producto es cuando el stump acierta y cuando falla, así que los puntos correctos se multiplican por y se encogen, y los incorrectos por y crecen. es solo el normalizador que mantiene los pesos sumando uno. Al final, el ensamble predice con un voto ponderado de los stumps, y toma el signo:
Cinco fórmulas, y las últimas cuatro son una línea de código cada una. Ese es todo el algoritmo.
En qué es bueno, en qué no
La fortaleza de AdaBoost es que convierte un modelo que jamás pondrías en producción por sí solo en uno que sí podrías. Dale un aprendiz débil que sea apenas un poco mejor que el azar y bajará el error de entrenamiento rápido, muchas veces hasta cero, y — esta es la parte que sorprendió a la gente — el error de test tiende a seguir bajando incluso después de que el error de entrenamiento toca fondo, porque los márgenes siguen ensanchándose. Casi no tiene perillas. Están el número de rondas y la elección del aprendiz débil, y en esencia eso es todo. Sin escalado de features, sin métrica de distancia, sin kernel que elegir.
La debilidad es la otra cara del mismo mecanismo. AdaBoost persigue sus errores, y no puede distinguir un punto genuinamente difícil de uno mal etiquetado o de un outlier. Un punto que solo es ruido se falla ronda tras ronda, así que su peso sube y sube, y al final el ensamble se está contorsionando para ajustarse a basura. Con datos limpios esa insistencia es una virtud; con datos ruidosos es la forma en que AdaBoost cae en overfitting. Verás un asomo de eso en este mismo capítulo — nuestra accuracy de test alcanza su pico a medio camino y luego se desinfla conforme las rondas tardías empiezan a ajustarse a las rarezas del split en lugar de a su señal.
Los datos
El dataset es un snapshot de make_gaussian_quantiles de scikit-learn: 200
puntos en dos dimensiones, divididos en dos clases de 100 según qué tan lejos
caen del centro de una gaussiana. La clase A es la bolita interior, la clase B
el cascarón que la rodea. Es el demo canónico de AdaBoost por una razón — es el
layout 2-D más simple donde un solo corte alineado a los ejes no tiene
esperanza pero una pila boosted sí. El scatter de arriba es todo el dataset; el
CSV está commiteado bajo data/ y el README anota exactamente cómo se generó.
Lo dividimos en 140 para entrenamiento y 60 para test, con semilla fija, y cada
número de este capítulo sale de ese split vía data/results.json.
Constrúyelo, una función a la vez
Siete funciones pequeñas, sin librería. En el mismo orden en que las escribiría frente a una terminal: primero el aprendiz débil, luego las piezas que convierten una secuencia de aprendices débiles en uno fuerte.
Empecemos con el stump. Es el clasificador de umbral de la semana 1 con dos cambios — las etiquetas ahora son , y hay un bit de polaridad para que el corte pueda mirar en cualquiera de las dos direcciones:
def stump_predict(X, stump):
"""A decision stump: one feature, one threshold, one direction.
`polarity` +1 means predict +1 on the high side of the threshold, -1 on the
low side; polarity -1 flips it. This is the week-1 threshold classifier with
its labels moved to {-1, +1} and a direction bit so the split can point
either way. Returns an (N,) array of ±1 predictions.
"""
col = X[:, stump["feature"]]
pred = np.ones(len(X))
if stump["polarity"] == 1:
pred[col < stump["threshold"]] = -1.0
else:
pred[col >= stump["threshold"]] = -1.0
return pred
AdaBoost nunca califica un stump por accuracy simple; lo califica por error ponderado, el peso total sobre los puntos que falla. Con pesos uniformes eso es la tasa de fallos ordinaria, pero una vez que los pesos se dispersan es lo que hace que un stump le importe los puntos marcados:
def weighted_error(y, pred, w):
"""The fraction of weight sitting on misclassified points.
Not the raw miss rate — each mistake is counted by its sample weight w, so a
point AdaBoost has flagged as hard costs more to get wrong. With uniform
weights this is exactly the ordinary error rate.
"""
return float(w[pred != y].sum())
Ajustar un stump es la misma búsqueda por fuerza bruta de la semana 1, ahora minimizando el error ponderado sobre cada feature, cada umbral entre valores adyacentes, y ambas polaridades. Devuelve el stump y sus predicciones para que el loop no tenga que recalcularlas:
def fit_stump(X, y, w):
"""Fit the stump with the smallest weighted error on weighted data.
Brute force, the honest way: for every feature, try a threshold between each
pair of adjacent sorted values and both polarities, and keep the combination
that misclassifies the least weight. Returns the stump plus its weighted
error and its prediction vector, which the boosting loop reuses.
"""
best = {"error": np.inf, "stump": None, "pred": None}
n_features = X.shape[1]
for f in range(n_features):
values = np.unique(X[:, f])
thresholds = (values[:-1] + values[1:]) / 2.0 # midpoints
for thr in thresholds:
for polarity in (1, -1):
stump = {"feature": f, "threshold": float(thr), "polarity": polarity}
pred = stump_predict(X, stump)
err = weighted_error(y, pred, w)
if err < best["error"]:
best = {"error": err, "stump": stump, "pred": pred}
return best["stump"], best["error"], best["pred"]
Una vez que tenemos el error del stump, su voto cae directo de la fórmula. El clamp es la única línea defensiva — evita que un stump perfecto produzca una infinita:
def stump_vote(error, eps=1e-10):
"""How much say this stump gets in the final vote: alpha.
A stump with low error earns a big positive alpha; a coin-flip stump
(error 1/2) earns alpha 0 and is ignored. The eps clamp keeps a perfect
stump (error 0) from blowing up to an infinite vote.
"""
error = min(max(error, eps), 1 - eps)
return 0.5 * np.log((1 - error) / error)
Luego el repesado, el corazón de todo el asunto. Los puntos correctos se encogen, los fallados crecen, y renormalizamos para que los pesos sigan siendo una distribución:
def reweight(w, alpha, y, pred):
"""Raise the weight on the points this stump missed, lower it on the hits.
w <- w * exp(-alpha * y * pred): the product y*pred is +1 on a correct call
and -1 on a wrong one, so correct points get multiplied by exp(-alpha) and
wrong points by exp(+alpha). Renormalize so the weights stay a distribution
that sums to one — that is what makes the next stump's weighted error a clean
fraction.
"""
w = w * np.exp(-alpha * y * pred)
return w / w.sum()
Ahora arma el loop. Pesos uniformes para empezar; cada ronda ajusta un stump, le asigna su voto, registra lo que pasó, y repesa para la siguiente ronda. Guardo un historial completo por ronda porque la animación lo reproduce cuadro por cuadro — los pesos que jalaron a cada stump, el stump mismo, su error y su voto, y la accuracy acumulada:
def adaboost_fit(X, y, n_rounds, X_test=None, y_test=None):
"""The AdaBoost loop: fit a stump, score it, vote it, reweight, repeat.
Start with uniform weights. Each round: fit the best weighted stump, read
off its weighted error and its vote alpha, then reweight the points so the
next round leans on the current mistakes. Returns the ensemble (a list of
(alpha, stump) pairs) and a per-round history the chapter replays frame by
frame — the weights going into the round, the stump it produced, its error
and alpha, and running train/test accuracy.
"""
n = len(X)
w = np.full(n, 1.0 / n) # uniform to start
ensemble, history = [], []
for _ in range(n_rounds):
stump, error, pred = fit_stump(X, y, w)
alpha = stump_vote(error)
history.append({
"weights": w.copy(), # the weights that pulled THIS stump
"stump": stump,
"error": error,
"alpha": float(alpha),
})
ensemble.append((alpha, stump))
history[-1]["train_acc"] = accuracy(ensemble, X, y)
if X_test is not None:
history[-1]["test_acc"] = accuracy(ensemble, X_test, y_test)
w = reweight(w, alpha, y, pred) # hard points swell for next round
return ensemble, history
Y la predicción: un voto ponderado de cada stump, luego el signo. Un stump confiable jala fuerte, uno débil apenas mueve el total:
def adaboost_predict(ensemble, X):
"""The final call: a weighted vote of every stump, then take the sign.
Each stump votes ±1; its vote is scaled by its alpha. Sum the scaled votes
and the sign is the prediction. A stump that was reliable pulls hard; a weak
one barely nudges the total. Ties (a sum of exactly 0) fall to +1.
"""
total = np.zeros(len(X))
for alpha, stump in ensemble:
total += alpha * stump_predict(X, stump)
return np.where(total >= 0, 1.0, -1.0)
Míralo trabajar
Este es el algoritmo corriendo de verdad, una ronda de boosting por cuadro. Léelo en tres lugares a la vez.
Los puntos son los datos de entrenamiento, y su TAMAÑO es su peso actual — el peso que jaló al stump de esta ronda. En la ronda uno todos son del mismo tamaño, porque los pesos empiezan uniformes. Observa lo que pasa después: los puntos cerca de la frontera del anillo, los que los stumps siguen fallando, se hinchan ronda tras ronda. Esa hinchazón es la atención del algoritmo, hecha visible. La línea naranja es el stump más nuevo, y puedes verlo atraído hacia los puntos gordos. La cuadrícula sombreada detrás de todo es la frontera de decisión del ensamble usando todos los stumps hasta el momento — empieza como un solo corte burdo y se afila hasta ser el anillo conforme los votos se acumulan. El panel inferior traza el error de entrenamiento y de test cayendo conforme las rondas se apilan.
Dale play. Cada cuadro es una ronda real; el caption la nombra, junto con el error de su stump, su voto , y la accuracy del ensamble. Resetea y córrelo tantas veces como quieras.
Vale la pena detenerse en dos cosas. Primero, nota que los stumps individuales siguen siendo terribles — sus errores rondan el 0.4 todo el camino, y sus votos se mantienen pequeños. La línea de ningún cuadro individual se ve más lista que la de la semana 1. La inteligencia no está en ningún stump; está en la acumulación. Segundo, observa la frontera en la cuadrícula sombreada pasar de un corte plano a algo que genuinamente envuelve la bolita interior. Ese anillo está construido enteramente con líneas horizontales y verticales, apiladas y votadas — que es exactamente cómo un comité de opiniones débiles y alineadas a los ejes se convierte en una curva.
Ahora la misma corrida llevada hasta las cincuenta rondas, solo las curvas de error. Aquí es donde aparece la debilidad honesta de AdaBoost:
El error de entrenamiento se desliza sostenidamente hacia cero — eso es boosting cumpliendo lo que promete. El error de test cae rápido al principio, toca fondo cerca de 0.10 alrededor de la ronda 17, y luego vuelve a subir a la deriva. Esas rondas tardías ya no están encontrando señal; están gastando sus votos en el puñado de puntos incrustados en el traslape de clases, los que ninguna línea puede arreglar, y pagándolo en los datos apartados. Este es el overfitting que el ruido y los outliers causan en AdaBoost, y es por lo que "más rondas" no sale gratis.
La implementación completa
El archivo completo sobre el que realmente corrió la animación, de arriba a abajo — siete funciones, sin librería:
"""AdaBoost (discrete AdaBoost / SAMME for two classes), built from scratch.
One decision stump is a weak learner: it barely beats a coin flip. AdaBoost
stacks many of them into a strong classifier by making each new stump focus on
the points the previous ones got wrong. It does that by carrying a weight on
every training point, fitting a stump that minimizes the WEIGHTED error, giving
that stump a vote proportional to how good it was, then raising the weight on
the points it missed so the next stump is pulled toward them.
Labels are in {-1, +1} throughout — that is what makes the weight update and the
final vote come out as clean sign arithmetic. Pure NumPy, no ML library here.
Every function below appears in the chapter one step at a time (the
`# region:` markers are what the book's include directives pull in).
"""
import numpy as np
import pandas as pd
# region: stump_predict
def stump_predict(X, stump):
"""A decision stump: one feature, one threshold, one direction.
`polarity` +1 means predict +1 on the high side of the threshold, -1 on the
low side; polarity -1 flips it. This is the week-1 threshold classifier with
its labels moved to {-1, +1} and a direction bit so the split can point
either way. Returns an (N,) array of ±1 predictions.
"""
col = X[:, stump["feature"]]
pred = np.ones(len(X))
if stump["polarity"] == 1:
pred[col < stump["threshold"]] = -1.0
else:
pred[col >= stump["threshold"]] = -1.0
return pred
# endregion
# region: weighted_error
def weighted_error(y, pred, w):
"""The fraction of weight sitting on misclassified points.
Not the raw miss rate — each mistake is counted by its sample weight w, so a
point AdaBoost has flagged as hard costs more to get wrong. With uniform
weights this is exactly the ordinary error rate.
"""
return float(w[pred != y].sum())
# endregion
# region: fit_stump
def fit_stump(X, y, w):
"""Fit the stump with the smallest weighted error on weighted data.
Brute force, the honest way: for every feature, try a threshold between each
pair of adjacent sorted values and both polarities, and keep the combination
that misclassifies the least weight. Returns the stump plus its weighted
error and its prediction vector, which the boosting loop reuses.
"""
best = {"error": np.inf, "stump": None, "pred": None}
n_features = X.shape[1]
for f in range(n_features):
values = np.unique(X[:, f])
thresholds = (values[:-1] + values[1:]) / 2.0 # midpoints
for thr in thresholds:
for polarity in (1, -1):
stump = {"feature": f, "threshold": float(thr), "polarity": polarity}
pred = stump_predict(X, stump)
err = weighted_error(y, pred, w)
if err < best["error"]:
best = {"error": err, "stump": stump, "pred": pred}
return best["stump"], best["error"], best["pred"]
# endregion
# region: stump_vote
def stump_vote(error, eps=1e-10):
"""How much say this stump gets in the final vote: alpha.
A stump with low error earns a big positive alpha; a coin-flip stump
(error 1/2) earns alpha 0 and is ignored. The eps clamp keeps a perfect
stump (error 0) from blowing up to an infinite vote.
"""
error = min(max(error, eps), 1 - eps)
return 0.5 * np.log((1 - error) / error)
# endregion
# region: reweight
def reweight(w, alpha, y, pred):
"""Raise the weight on the points this stump missed, lower it on the hits.
w <- w * exp(-alpha * y * pred): the product y*pred is +1 on a correct call
and -1 on a wrong one, so correct points get multiplied by exp(-alpha) and
wrong points by exp(+alpha). Renormalize so the weights stay a distribution
that sums to one — that is what makes the next stump's weighted error a clean
fraction.
"""
w = w * np.exp(-alpha * y * pred)
return w / w.sum()
# endregion
# region: adaboost_fit
def adaboost_fit(X, y, n_rounds, X_test=None, y_test=None):
"""The AdaBoost loop: fit a stump, score it, vote it, reweight, repeat.
Start with uniform weights. Each round: fit the best weighted stump, read
off its weighted error and its vote alpha, then reweight the points so the
next round leans on the current mistakes. Returns the ensemble (a list of
(alpha, stump) pairs) and a per-round history the chapter replays frame by
frame — the weights going into the round, the stump it produced, its error
and alpha, and running train/test accuracy.
"""
n = len(X)
w = np.full(n, 1.0 / n) # uniform to start
ensemble, history = [], []
for _ in range(n_rounds):
stump, error, pred = fit_stump(X, y, w)
alpha = stump_vote(error)
history.append({
"weights": w.copy(), # the weights that pulled THIS stump
"stump": stump,
"error": error,
"alpha": float(alpha),
})
ensemble.append((alpha, stump))
history[-1]["train_acc"] = accuracy(ensemble, X, y)
if X_test is not None:
history[-1]["test_acc"] = accuracy(ensemble, X_test, y_test)
w = reweight(w, alpha, y, pred) # hard points swell for next round
return ensemble, history
# endregion
# region: adaboost_predict
def adaboost_predict(ensemble, X):
"""The final call: a weighted vote of every stump, then take the sign.
Each stump votes ±1; its vote is scaled by its alpha. Sum the scaled votes
and the sign is the prediction. A stump that was reliable pulls hard; a weak
one barely nudges the total. Ties (a sum of exactly 0) fall to +1.
"""
total = np.zeros(len(X))
for alpha, stump in ensemble:
total += alpha * stump_predict(X, stump)
return np.where(total >= 0, 1.0, -1.0)
# endregion
def accuracy(ensemble, X, y):
"""Fraction the current ensemble gets right — used all through the loop."""
return float((adaboost_predict(ensemble, X) == y).mean())
def load_data(path="../data/quantiles.csv"):
"""make_gaussian_quantiles snapshot: 200 2-D points, two concentric classes.
Returns X as an (N, 2) float array and y in {-1, +1}. The raw CSV stores the
label as {0, 1}; we remap 0 -> -1 so the boosting math stays sign-clean.
"""
df = pd.read_csv(path)
X = df[["x1", "x2"]].to_numpy(float)
y = np.where(df["label"].to_numpy(int) == 1, 1.0, -1.0)
return X, y
La versión de librería
Nadie escribe esto a mano en producción. AdaBoost con decision stumps es un
constructor en scikit-learn — el estimador base es un DecisionTreeClassifier
limitado a max_depth=1, que es exactamente nuestro stump:
def sk_adaboost(X_train, y_train, X_test, y_test, n_rounds):
"""Fit sklearn AdaBoost with depth-1 trees; return train/test accuracy.
learning_rate=1.0 leaves the vote alpha unshrunk, so it lines up with our
from-scratch alpha = 1/2 ln((1-eps)/eps). Also returns the fitted model so
the traces can read its per-round staged accuracy.
"""
clf = AdaBoostClassifier(
estimator=DecisionTreeClassifier(max_depth=1),
n_estimators=n_rounds,
learning_rate=1.0,
random_state=0,
)
clf.fit(X_train, y_train)
train_acc = float(clf.score(X_train, y_train))
test_acc = float(clf.score(X_test, y_test))
return train_acc, test_acc, clf
Dos diferencias con el nuestro, ambas pequeñas. La librería ajusta cada stump por impureza de Gini en lugar de buscar la accuracy directamente, así que en cualquier ronda dada puede elegir un corte ligeramente distinto — pero está optimizando para lo mismo, un buen corte ponderado, y aterriza en el mismo vecindario. Y desde scikit-learn 1.6 el único algoritmo es SAMME, la generalización multiclase del AdaBoost discreto; para nuestras dos clases se reduce precisamente al voto que derivamos. Para comparar qué tan rápido cae el error de cada versión, la librería nos entrega su accuracy después de cada ronda sucesiva:
def sk_staged(clf, X, y):
"""sklearn's accuracy after each successive round, for the error curve.
staged_score walks the ensemble one estimator at a time, so we get the same
"accuracy vs number of rounds" trace we build from our own history — the
honest way to compare how fast each version's error falls.
"""
return [float(s) for s in clf.staged_score(X, y)]
Desde cero contra librería
El mismo split 140/60, ambos entrenados por 50 rondas, evaluados sobre los 60 apartados. Aquí está el cara a cara, con el stump solitario incluido para que veas lo que el boosting compró:
El stump solo saca 0.63 en el conjunto de test — apenas arriba de adivinar,
exactamente el piso que esperábamos de clases concéntricas. Nuestro AdaBoost
desde cero lo sube a 0.88, y el de scikit-learn llega a 0.92. Ambos modelos
boosted alcanzan el mismo 0.98 en los datos de entrenamiento; la brecha entre
ellos en test es la diferencia de un split entre Gini y accuracy en cuál umbral
eligió cada ronda, no una diferencia en el algoritmo. El titular es el salto de
0.63 a los altos 0.80 y 0.90: un aprendiz débil apilado cincuenta veces, sobre
datos donde uno solo de ellos es inútil. Cada cifra de aquí se lee de
data/results.json, regenerado cada vez que el código cambia, para que la prosa
no pueda desviarse de lo que corrió.
Conclusiones
AdaBoost es la demostración más limpia del curso de una idea que atraviesa todo el ML moderno: un modelo fuerte no tiene que ser un modelo complicado. Puede ser un enjambre de modelos tontos, bien apuntados. El stump que jamás pondrías en producción se convierte, cincuenta copias después y cada una apuntada a los errores de la anterior, en un clasificador que dibuja fronteras que ninguno de ellos podría. Entiende esto y entiendes el motor dentro de cada modelo de gradient boosting que vayas a correr en tu vida.
Úsalo cuando tu aprendiz débil sea genuinamente débil y tus datos sean genuinamente limpios. Esa segunda condición no es opcional. Como AdaBoost define "difícil" como "se sigue fallando", no puede distinguir un caso sutil de uno corrupto, y con gusto verterá sus rondas tardías en ajustarse a tus outliers — viste el error de test volver a subir mientras el de entrenamiento seguía cayendo. Si tus etiquetas son ruidosas, el boosting encontrará el ruido. En ese escenario un ensamble con bagging como el random forest del capítulo pasado, que promedia en lugar de perseguir, suele ser la apuesta más segura.
Y este es el puente hacia lo que sigue. AdaBoost repesa puntos; el repesado es en realidad una forma burda de decirle a cada aprendiz nuevo que arregle los errores actuales. El gradient boosting hace eso explícito — en lugar de empujar pesos, cada aprendiz nuevo se ajusta directamente al residual, el gradiente de una pérdida que tú eliges. El mismo movimiento central, perseguir lo que sigue mal, pero generalizado del error de clasificación a cualquier objetivo diferenciable. Esa generalización es lo que llevó al boosting de ser un clasificador ingenioso a ser el caballo de batalla detrás de XGBoost y de cada leaderboard de Kaggle, y es exactamente a donde va el siguiente capítulo.