Python & Data Science

P02: Cómo funciona realmente el descenso de gradiente — y las variantes que lo hacen práctico

La última vez, Sam y el equipo explicaron el Arquero y el Blanco. Un modelo tiene que encontrar el “punto óptimo” entre lo demasiado simple (Sesgo) y lo demasiado complejo (Varianza). Pero, ¿cómo ajusta el arquero su puntería? No adivinando. Siente el viento, revisa dónde cayó la última flecha y luego ajusta el siguiente disparo.

Ese proceso de hacer pequeñas correcciones para reducir el error se llama Descenso de Gradiente. Está bajo el capó de casi todos los modelos de IA, desde regresiones lineales hasta las redes neuronales que impulsan a ChatGPT.

1. El excursionista vendado: cómo los modelos ‘palpan’ su camino hacia la verdad

Imagínate que eres un excursionista en una montaña. Una espesa niebla cae sobre ti. Estás prácticamente a ciegas. Sabes que hay una acogedora cabaña al fondo del valle, pero no puedes ver dónde está. ¿Cómo llegas allí?

Incluso sin poder ver, tus pies te ayudan. Puedes sentir la inclinación del terreno. Si se inclina hacia arriba a la izquierda y hacia abajo a la derecha, das un paso a la derecha. No sabes si la cabaña está a una milla o a diez pies de distancia, pero siempre y cuando sigas moviéndote cuesta abajo, llegarás al punto más bajo.

En la ciencia de datos, la ‘montaña’ es el error de tu modelo (o ‘Pérdida’). Estar más arriba significa cometer más errores. El ‘valle’ es el lugar donde tu modelo es lo más preciso posible. El Descenso de Gradiente consiste simplemente en ‘sentir’ la pendiente del error y dar pasos hacia el fondo.

2. ‘Gradiente’ es solo una forma elegante de decir pendiente

Cuando los matemáticos hablan de “gradiente”, en realidad solo se refieren a qué dirección es hacia arriba y qué tan empinada es.

Imagina que estás de pie en una colina. El gradiente es un vector que apunta directamente hacia la parte más empinada de la pendiente. Como queremos ir hacia abajo, nos movemos en la dirección opuesta.

Pero, ¿qué tan grande debería ser cada paso? Esa es la Tasa de aprendizaje.

  • Demasiado grande, y saltarás por encima del valle para aterrizar en el otro lado.
  • Demasiado pequeño, y estarás caminando durante años antes de llegar a la cabaña.

En la práctica, esto significa ajustar los pesos —la configuración— de tu modelo según la magnitud del error. Aquí se muestra cómo se ve una única actualización en Python puro:

# A tiny example of a single weight update
weight = 0.5
gradient = 0.8  # The 'slope' we calculated from our error
learning_rate = 0.1 

# We move the weight in the opposite direction of the gradient
weight = weight - (learning_rate * gradient)

print(f"New weight: {weight:.2f}")
# The weight decreased because the slope was positive (uphill).
  • weight = 0.5 inicializa el único parámetro del modelo a un valor de inicio — en un modelo real, los pesos suelen inicializarse de forma aleatoria.
  • gradient = 0.8 representa la pendiente de la pérdida con respecto al peso; un gradiente positivo significa que aumentar el peso aumentaría el error, por lo que necesitamos empujarlo en la dirección contraria.
  • weight = weight - (learning_rate * gradient) es la regla de actualización principal: mover el peso en dirección opuesta al gradiente, escalado por la tasa de aprendizaje. Con un gradiente positivo, el peso disminuye — dando un paso cuesta abajo.

La regla de actualización de pesos del descenso de gradiente ajusta cada parámetro dando un paso proporcional al gradiente negativo de la pérdida:

wwηL(w)w \leftarrow w - \eta \, \nabla L(w)

  • ww — el valor actual del peso (parámetro) que se está actualizando.
  • η\eta (eta) — la tasa de aprendizaje, un escalar que controla el tamaño del paso.
  • L(w)\nabla L(w) — el gradiente de la función de pérdida LL con respecto a ww; apunta en la dirección de mayor aumento del error, por lo que lo restamos.
En términos simplesSímbolo estadísticoEquivalente en Python
Valor actual del peso (parámetro)wwweight
Tamaño del paso — cuánto moverse en cada actualizaciónη\eta (tasa de aprendizaje)learning_rate / lr
Pendiente de la pérdida respecto al pesoL=Lw\nabla L = \frac{\partial L}{\partial w}gradient / dw
Regla de actualización de pesoswwηLw \leftarrow w - \eta \, \nabla Lweight -= lr * dw

3. Descenso de gradiente por lotes: el enfoque del perfeccionista

Entonces, ¿cómo usamos esto realmente con los datos? La versión más básica es Descenso de gradiente por lotes.

Imagina de nuevo a ese excursionista con los ojos vendados. Antes de dar un solo paso, te comunicas por radio con 1,000 otros excursionistas. Le pides a cada uno que sienta la pendiente bajo sus pies y te lo reporte. Luego promedias todas sus respuestas y das un solo paso.

En términos de datos, “lote” (batch) significa que el modelo analiza cada fila de tu conjunto de datos antes de actualizar sus pesos.

El inconveniente: Es estable y se mueve en línea recta hacia el objetivo. Pero es lento. Un millón de filas de datos significa un millón de cálculos solo para dar un pequeño paso. Piensa en un comité que no se mueve hasta que todos hayan hablado.

import numpy as np

# Let's create some simple data: y = 2x + noise
np.random.seed(42)
X = 2 * np.random.rand(100, 1)
y = 4 + 3 * X + np.random.randn(100, 1)

# Batch Gradient Descent
weight = np.random.randn(1)
bias = np.random.randn(1)
lr = 0.1

for epoch in range(10):
    # 1. Predict for ALL 100 points
    predictions = X * weight + bias
    
    # 2. Calculate the average error (gradient)
    dw = (2/100) * X.T.dot(predictions - y)
    db = (2/100) * np.sum(predictions - y)
    
    # 3. Update weights
    weight -= lr * dw
    bias -= lr * db
    
    print(f"Epoch {epoch+1}: Weight is {weight[0][0]:.2f}")
  • X = 2 * np.random.rand(100, 1) genera 100 valores x aleatorios uniformemente en [0, 2) con forma (100, 1) para que sea un vector columna.
  • y = 4 + 3 * X + np.random.randn(100, 1) crea el objetivo como una función lineal (pendiente 3, intercepto 4) más ruido gaussiano — el trabajo del modelo es recuperar esos coeficientes.
  • dw = (2/100) * X.T.dot(predictions - y) calcula el gradiente de la pérdida MSE con respecto al peso: el 2/100 proviene de derivar el error cuadrático medio, y X.T.dot(...) aplica la regla de la cadena multiplicando los residuos por las entradas.
  • db = (2/100) * np.sum(predictions - y) es el gradiente del sesgo — como el sesgo no tiene multiplicación por la entrada, solo sumas los residuos.
  • weight -= lr * dw es la versión in-place de la regla de actualización — equivalente a weight = weight - lr * dw.

Después de 10 pasos, el peso ya se acerca a 3.0. Progreso constante, pero cada paso fue ‘costoso’ ya que utilizamos todo el conjunto de datos.

4. Descenso de gradiente estocástico (SGD): el excursionista impaciente

‘Estocástico’ es simplemente una palabra matemática que significa ‘aleatorio’. Si el Descenso de Gradiente por Lotes es un comité, el SGD es un excursionista solitario que tiene muchísima prisa.

En lugar de observar todos los datos, el SGD observa solo una fila aleatoria, calcula el error y da un paso. Luego, selecciona otra fila aleatoria y da otro paso.

El problema: Solo observa un punto a la vez, por lo que el gradiente es muy ‘ruidoso’. Una fila podría indicarle que vaya a la izquierda, mientras que la siguiente le dice que vaya a la derecha. El camino hacia el valle zigzaguea. Pero cada paso es mucho más rápido de calcular, y ese ruido en realidad puede ayudar al modelo a salir de pequeños ‘baches’ (mínimos locales) en la montaña.

# SGD: Updating after every single point
weight = np.random.randn(1)
bias = np.random.randn(1)

for i in range(10): # Just look at the first 10 points
    xi = X[i:i+1]
    yi = y[i:i+1]
    
    prediction = xi * weight + bias
    dw = 2 * xi.T.dot(prediction - yi)
    db = 2 * (prediction - yi)
    
    weight -= lr * dw
    bias -= lr * db
    print(f"Step {i+1}: Weight is {weight[0][0]:.2f}")
  • xi = X[i:i+1] extrae una sola fila de X manteniendo la forma 2-D (1, 1) — usar i:i+1 en lugar de X[i] preserva la estructura de vector columna que .dot() espera.
  • dw = 2 * xi.T.dot(prediction - yi) omite el factor de promediado 1/N porque solo hay un punto de datos — el gradiente se calcula a partir de un solo residuo.
  • El bucle se ejecuta solo 10 veces (no sobre el conjunto de datos completo), lo que demuestra que el SGD da un paso después de cada fila, no después de ver todas las filas — de ahí la etiqueta de “impaciente”.

¿Notas cómo el peso da saltos mucho más erráticos que en la versión por Lotes? Ese es el ruido del SGD.

5. Mini-lotes: la solución de Ricitos de Oro

En la práctica, casi nadie usa Batch puro ni SGD puro. Usamos descenso de gradiente por mini-lotes.

Tomamos un pequeño grupo de filas —típicamente 32, 64 o 128— y actualizamos el modelo con ese grupo. Se obtienen las ventajas de ambos extremos:

  1. Más rápido que Batch, ya que se evita revisar todo el conjunto de datos.
  2. Más estable que SGD, porque promediar 32 puntos da una señal más confiable que uno solo.

Entonces, ¿por qué 32 o 64? Esto suele confundir a los principiantes, pero la respuesta generalmente se reduce a cómo funciona la memoria de la computadora. Los procesadores manejan pequeños bloques de datos de manera eficiente cuando esos bloques caben perfectamente en la memoria caché.

batch_size = 20
for i in range(0, 100, batch_size):
    Xi = X[i:i+batch_size]
    yi = y[i:i+batch_size]
    
    # Update based on this 'mini-batch'
    predictions = Xi * weight + bias
    dw = (2/batch_size) * Xi.T.dot(predictions - yi)
    weight -= lr * dw
    print(f"Batch starting at {i}: Weight updated")
  • for i in range(0, 100, batch_size) recorre el conjunto de datos en bloques de 20 — range(0, 100, 20) produce [0, 20, 40, 60, 80], generando 5 mini-lotes.
  • Xi = X[i:i+batch_size] toma 20 filas a la vez — lo suficientemente pequeño para caber en la caché de la CPU, lo suficientemente grande para promediar el ruido individual.
  • dw = (2/batch_size) * Xi.T.dot(predictions - yi) restablece el factor de promediado pero divide por batch_size (20) en lugar del tamaño completo del conjunto de datos (100) — manteniendo la magnitud del gradiente consistente independientemente del tamaño del lote.

6. Adam: el excursionista inteligente con memoria

El descenso de gradiente estándar trata cada paso como si fuera el primer día de trabajo. Olvida de dónde acaba de venir.

Adam (Estimación Adaptativa del Momento) es el optimizador más popular hoy en día porque tiene una “memoria”. Imagina una pelota pesada rodando colina abajo. Adquiere momentum. Hazla rodar hacia el sur por un rato y seguirá avanzando hacia el sur incluso al pasar por un pequeño bache.

Adam también utiliza “tasas de aprendizaje adaptativas”. Si un peso específico cambia mucho, Adam reduce el tamaño de su paso. Si un peso apenas se mueve, Adam le da un empujón.

Acelera en zonas planas y se frena cuando las cosas se complican. Por eso Adam es la opción “predeterminada” para casi todos. Yo me inclinaría por él como punto de partida, ya que hace el trabajo de elegir la tasa de aprendizaje por ti.

7. En resumen: ¿cuál deberías usar?

Entonces, ¿cómo elegir? Aquí tienes la regla general:

  • Usa Batch si tu conjunto de datos es minúsculo (unos pocos cientos de filas) y deseas la respuesta más precisa posible. Cada paso utiliza todos los datos, por lo que la señal del gradiente es perfectamente estable, pero en conjuntos de datos grandes cada paso es prohibitivamente lento y la trayectoria suave puede quedarse atascada en mínimos locales.
  • Usa SGD si tienes un conjunto de datos masivo y necesitas actualizaciones rápidas y aproximadas; el ruido en cada paso puede ayudar al modelo a escapar de mínimos locales poco profundos en los que Batch se estancaría. El compromiso es que la convergencia es inestable y el peso rebota alrededor del mínimo en lugar de asentarse limpiamente.
  • Usa Mini-Batch para casi todo lo demás. Es el estándar de la industria para el aprendizaje profundo: equilibra la estabilidad de Batch con la velocidad de SGD, y los tamaños de lote (32, 64, 128) se eligen para encajar perfectamente en la caché de la GPU/CPU y obtener la máxima eficiencia de hardware.
  • Usa Adam como tu optimizador dentro de Mini-Batch si no quieres pasar todo el día ajustando tu tasa de aprendizaje. Es lo suficientemente ‘inteligente’ para manejar la mayoría de los problemas adaptando el tamaño del paso por parámetro sobre la marcha.

Lo que cubrimos:

  • Descenso de gradiente percibe la pendiente del error y se mueve cuesta abajo.
  • Tasa de aprendizaje es el tamaño de tu paso: demasiado grande es peligroso, demasiado pequeño es lento.
  • Batch es estable pero lento; SGD es rápido pero caótico; Mini-Batch se encuentra entre ambos.
  • Adam es el favorito moderno porque utiliza momento y se adapta a los datos.

El modelo de Sam ahora actualiza sus pesos mediante descenso de gradiente, pero en un pequeño conjunto de entrenamiento de HomeMatch ya está empezando a sobreajustarse. Eso es lo que aborda la próxima parte sobre Regularización: evitar que el modelo se desvíe demasiado y memorice el ruido en lugar del patrón real.

Comprueba tu comprensión

Las preguntas a continuación progresan desde la simple recordación hasta el diseño abierto, siguiendo aproximadamente la Taxonomía de Bloom.

Recordar ¿Qué controla la “tasa de aprendizaje” (learning rate) y qué falla si se ajusta a un valor demasiado grande frente a uno demasiado pequeño?

Comprender En sus propias palabras, explique por qué la trayectoria del SGD hacia el mínimo es “en zigzag”, mientras que la del Batch Gradient Descent es suave, utilizando las analogías de los senderistas del artículo.

Aplicar Utilizando la fórmula de actualización de pesos del artículo (weight = weight - learning_rate * gradient), calcule el nuevo peso si el peso actual es 1.2, el gradiente es 0.5 y la tasa de aprendizaje es 0.2.

Analizar El artículo menciona que Mini-Batch es “más rápido que Batch” y “más suave que SGD”. Analice paso a paso por qué promediar el gradiente sobre 32 puntos aleatorios (Mini-Batch) reduce el ruido que se obtendría de un solo punto (SGD), sin requerir el costo computacional total de promediar sobre todos los 100+ puntos (Batch).

Evaluar El artículo recomienda Adam como el valor predeterminado “si no quieres pasar todo el día ajustando tu tasa de aprendizaje”. Critique este consejo: ¿existe alguna buena razón para usar el Mini-Batch Gradient Descent simple (no adaptativo) en lugar de Adam, a pesar de que requiere más ajuste manual?

Crear Diseñe un pequeño experimento (no necesariamente código) que le permita comparar Batch, SGD y Mini-Batch en el mismo conjunto de datos, y que le muestre a un compañero de equipo el compromiso de “ruido vs. velocidad” que describe el artículo. ¿Qué graficaría y qué esperaría ver para cada método?


Artículos relacionados

Referencias y lecturas adicionales

  • Rumelhart, D. E., Hinton, G. E., & Williams, R. J. (1986). “Learning representations by back-propagating errors.” Nature, 323, 533–536. — el artículo fundamental que introdujo la retropropagación, haciendo que el descenso de gradiente sea práctico para modelos multicapa.
  • 3Blue1Brown — What is Gradient Descent? — un recorrido visual animado de cómo el descenso de gradiente navega por un paisaje de pérdida.
  • Scikit-learn documentation: SGD classifiers & regressors — descenso de gradiente estocástico de nivel de producción para tareas clásicas de ML.

Esta traducción fue generada automáticamente y puede contener errores. Si el idioma inglés es tu preferencia, puedes leer el artículo original en inglés .

«Aplica lo que aprendiste» es para suscriptores Supporter e Insider.

Suscríbete para desbloquear los ejercicios de este artículo.

Ver planes

¿Buscas otra cosa?

Busca en todos los artículos por título, resumen o tema.