- SGD permite entrenar regresiones cuadráticas en JavaScript de forma eficiente, evitando el coste prohibitivo de la ecuación normal en problemas con muchas características.
- La combinación de funciones de pérdida adecuadas y regularización L1/L2 controla el sobreajuste y hace que el modelo cuadrático sea robusto incluso en presencia de ruido y outliers.
- Optimizadores avanzados como Momentum, Nesterov, AdaGrad, AdaDelta o Adam mejoran la convergencia de SGD, adaptando el tamaño de paso y suavizando la trayectoria de los parámetros.

Si estás metido en Machine Learning y te gusta programar en JavaScript, tarde o temprano te vas a topar con la idea de ajustar modelos de regresión mediante descenso de gradiente, y en particular con su versión estocástica (SGD). Cuando pasamos de regresión lineal simple a regresión cuadrática, el modelo sigue siendo lineal en los parámetros, pero la implementación práctica (especialmente en el navegador o con Node.js) obliga a entender bien cómo funciona el optimizer, qué hiperparámetros tocar y qué trampas evitar.
En este artículo vamos a desgranar, paso a paso, cómo funciona el descenso de gradiente estocástico aplicado a regresión cuadrática en JavaScript, enlazándolo con la teoría clásica (ecuación normal, variantes de gradiente, regularización, funciones de pérdida, etc.) y con implementaciones reales inspiradas en librerías como scikit-learn, pero adaptadas al ecosistema JS. Verás tanto la base matemática como las intuiciones prácticas para datasets grandes, datos con ruido o presencia de outliers.
De la ecuación normal al descenso de gradiente estocástico
En regresión lineal clásica, el punto de partida suele ser la ecuación normal, que nos dice que los parámetros óptimos se pueden obtener como θ = (XᵀX)⁻¹ Xᵀy, donde X es la matriz de características, y es el vector objetivo y θ el vector de parámetros. En teoría es muy cómodo porque obtienes la solución óptima de una tacada, sin iterar.
El problema llega cuando la matriz de características X es muy grande (por ejemplo, cuando el número de características n ronda o supera las 10.000). Calcular explícitamente la inversa de XᵀX deja de ser viable, tanto por coste computacional como por estabilidad numérica. En análisis numérico, de hecho, se evita a toda costa invertir matrices de forma directa y se prefieren descomposiciones tipo LU, QR o SVD, que son más robustas.
En el mundo del Machine Learning práctico, especialmente a gran escala, esta limitación hace que la ecuación normal solo sea razonable para problemas relativamente pequeños. Para datasets enormes o con muchas features, los algoritmos iterativos como el descenso de gradiente (en sus variantes batch, mini‑batch y estocástico) se vuelven la opción natural, porque permiten aproximar el mínimo de la función de coste sin tener que resolver sistemas lineales gigantescos.
Regresión cuadrática con descenso de gradiente significa, al final, que seguimos usando una función de pérdida (por ejemplo el error cuadrático medio) y un modelo lineal en parámetros, pero las características incluyen términos no lineales, como x y x², o en general polinomios. En JavaScript esto se traduce en construir manualmente un vector de entrada extendido, por ejemplo [1, x, x²], y aplicar actualización de parámetros vía gradiente de la misma forma que en una regresión lineal simple.
Qué es el descenso de gradiente y por qué es tan importante
El descenso de gradiente es, probablemente, el algoritmo de optimización más usado en aprendizaje automático. Su misión es sencilla de describir: minimizar una función de coste ajustando iterativamente los parámetros de un modelo en la dirección de máximo descenso de dicha función.
La idea geométrica es muy visual: imagina que caminas por una superficie montañosa buscando el punto más bajo. El gradiente te indica la dirección de máxima subida, así que para bajar solo tienes que moverte en la dirección opuesta. En cada iteración el algoritmo calcula el gradiente de la función de coste con respecto a los parámetros actuales y los actualiza restando un múltiplo de ese gradiente.
La regla general de actualización para un parámetro θ es θ := θ − η · ∇θ L(θ), donde η es la tasa de aprendizaje (learning rate) y ∇θ L(θ) es el gradiente de la pérdida respecto a θ. Para un modelo de regresión (lineal o cuadrática), los parámetros son típicamente los pesos w y el sesgo b, y la función objetivo suele ser el error cuadrático medio sobre todas las muestras de entrenamiento.
En regresión lineal con n muestras (xᵢ, yᵢ), el modelo más simple tiene la forma ŷᵢ = w·xᵢ + b, y la pérdida global L(w, b) se define como la media del cuadrado de los errores: L(w, b) = (1/n) Σ (yᵢ − ŷᵢ)². Al pasar a regresión cuadrática, el modelo cambia a ŷᵢ = a·xᵢ² + b·xᵢ + c, pero el esquema es el mismo: derivamos la pérdida respecto a cada parámetro (a, b, c) y aplicamos la regla de actualización iterativa para acercarnos al mínimo.
Descenso de gradiente estocástico (SGD): concepto y motivación
El descenso de gradiente “clásico” calcula el gradiente usando todas las muestras en cada iteración. Esto se conoce como batch gradient descent. Su principal inconveniente es que para datasets grandes cada actualización es muy costosa, porque hay que recorrer el conjunto completo de entrenamiento para computar la media de los gradientes.
El descenso de gradiente estocástico (Stochastic Gradient Descent, SGD) reduce ese coste usando solo una muestra (o un mini‑lote pequeño) por actualización. En vez de derivar L(θ), que es el promedio de todas las pérdidas, se deriva ℓ(xᵢ, yᵢ; θ), la pérdida individual de una sola muestra (o media sobre un subconjunto), usando esa derivada como aproximación ruidosa del gradiente verdadero.
Formalmente, si L(θ) = (1/n) Σ ℓ(xᵢ, yᵢ; θ), en SGD aproximamos ∇θ L(θ) por ∇θ ℓ(xᵢ, yᵢ; θ) seleccionando (xᵢ, yᵢ) de manera aleatoria (o recorriendo el dataset barajado). Esta aproximación introduce ruido en la trayectoria de los parámetros, pero permite hacer muchas más actualizaciones por unidad de tiempo, lo que a la larga suele compensar, sobre todo con datos masivos.
En términos prácticos, SGD se ha convertido en el caballo de batalla para problemas de Machine Learning a gran escala y dispersos, como clasificación de texto, procesamiento de lenguaje natural o entrenamiento de redes neuronales profundas. Su complejidad por iteración es básicamente lineal en el número de muestras procesadas, y al trabajar con datos dispersos (por ejemplo, bolsas de palabras) se aprovecha muy bien la estructura para reducir cálculos.
Cuando aplicamos SGD a regresión cuadrática en JavaScript, la lógica es idéntica: tomamos una muestra (o mini‑lote), calculamos la predicción del modelo cuadrático, computamos el error, derivamos la pérdida respecto a los parámetros a, b y c, y actualizamos esos parámetros con un learning rate adecuado. El ruido que introduce el muestreo aleatorio ayuda a escapar de mínimos locales pequeños cuando trabajamos con funciones de coste algo más complicadas, como ocurre en modelos más complejos o con regularizaciones no triviales.
Funciones de pérdida y regularización en modelos lineales y de regresión
SGD en sí mismo no define un modelo, sino una técnica de optimización. Lo que determina el tipo de modelo (clasificador o regresor) es la combinación de función de pérdida L y término de regularización R(w) que queremos minimizar.
En clasificación supervisada suelen usarse pérdidas convexas como la bisagra (hinge) de las SVM lineales, la pérdida de perceptrón o la pérdida logística. Cada una de estas pérdidas castiga de forma distinta los errores de clasificación y conduce a distintos comportamientos del clasificador. Por ejemplo, la pérdida logística está asociada a la regresión logística y permite interpretar la salida como probabilidades P(y|x).
Para regresión, las pérdidas habituales son la pérdida cuadrática (mínimos cuadrados), la pérdida de Huber y la pérdida epsilon-insensitive de las SVR. La pérdida cuadrática es la más estándar y adecuada cuando los errores se distribuyen aproximadamente de forma gaussiana; la de Huber y la epsilon-insensitive son más robustas frente a outliers, al limitar la influencia de errores muy grandes.
El término de regularización R(w) controla la complejidad del modelo, penalizando pesos grandes para evitar sobreajuste. Las opciones más comunes son la norma L2 (Ridge), la norma L1 (Lasso) y la combinación de ambas conocida como Elastic Net. L2 favorece soluciones con pesos pequeños pero densos, L1 tiende a producir soluciones dispersas (con muchos coeficientes cero), y Elastic Net combina ambas ventajas, especialmente útil cuando hay características muy correlacionadas.
En regresión cuadrática con SGD en JavaScript, podemos implementar fácilmente estas ideas añadiendo a la pérdida un término como α·R(w), donde w incluye los coeficientes del polinomio (por ejemplo, [a, b, c]). La derivada del término de regularización se suma a la derivada de la pérdida original, y la regla de actualización queda w ← w − η (∂L/∂w + α ∂R/∂w). Este esquema es perfectamente trasladable a cualquier entorno JS, tanto en Node como en el navegador.
Detalles matemáticos del procedimiento SGD
Desde el punto de vista formal, partimos de un conjunto de entrenamiento {(x₁, y₁), …, (xₙ, yₙ)}, con xᵢ ∈ ℝᵐ y yᵢ real (o ±1 en clasificación binaria). El modelo es una función lineal f(x) = wᵀx + b, aunque x puede contener transformaciones no lineales (como x², x³, etc.), de manera que f sigue siendo lineal en los parámetros.
El objetivo es minimizar el error de entrenamiento regularizado E(w, b) = (1/n) Σ L(yᵢ, f(xᵢ)) + α R(w), donde L mide el desacuerdo entre predicción y etiqueta, R penaliza la complejidad de w y α > 0 controla la fuerza de esa penalización. Distintas elecciones de L y R generan distintos modelos concretos: SVM, regresión logística, Ridge, Lasso, Elastic Net, SVR, etc.
En descenso de gradiente estocástico, en lugar de usar el gradiente exacto de E(w, b), calculamos la derivada parcial basándonos en una sola muestra (o mini‑lote). La regla de actualización para w queda: w ← w − η [α ∂R(w)/∂w + ∂L(yᵢ, wᵀxᵢ + b)/∂w], mientras que el intercepto b se actualiza normalmente sin regularización.
La tasa de aprendizaje η puede ser constante, decrecer de forma inversa o adaptativa. En muchos frameworks se usan programas de aprendizaje del tipo ηᵗ = η₀ / tᵖ (con p > 0) o estrategias “óptimas” basadas en la norma esperada de las muestras. En una implementación en JavaScript, puedes replicar estas políticas ajustando el valor de η en cada época o cada cierto número de iteraciones.
SGD para datos dispersos y complejidad computacional
Una de las grandes ventajas de SGD es su eficiencia cuando trabajamos con matrices de características dispersas, como las generadas por modelos de bolsa de palabras o one-hot encodings con vocabularios enormes. Si representamos X con estructuras tipo CSR (Compressed Sparse Row) en entornos científicos, solo se realizan operaciones sobre los elementos no nulos.
La complejidad de entrenamiento de un modelo lineal con SGD es O(k · n · p̄), donde k es el número de épocas, n el número de muestras y p̄ el número medio de atributos no nulos por muestra. En la práctica, para un cierto nivel de precisión deseado, el tiempo de entrenamiento no escala linealmente con el tamaño del dataset completo una vez que hemos visto un número suficiente de muestras (del orden de 10⁶).
En un entorno JavaScript, aunque no tengamos de serie estructuras dispersas tan optimizadas como en NumPy o SciPy, sí podemos representar vectores como listas de índices no nulos y valores, y escribir rutinas de producto punto eficientes. Esto se vuelve especialmente interesante si queremos entrenar modelos de regresión (lineal, cuadrática o de mayor grado) sobre representaciones de texto en Node, por ejemplo, sin depender de bindings a librerías nativas pesadas.
La regla general a nivel práctico es que SGD escala mucho mejor que la ecuación normal cuando el número de muestras o características crece. A cambio, renunciamos a la solución exacta y dependemos de hiperparámetros como el número de épocas, el tamaño de mini‑lote y, sobre todo, la tasa de aprendizaje y la regularización.
Criterios de parada y estrategias de early stopping
Como el descenso de gradiente estocástico es un método iterativo, necesitamos una forma de decidir cuándo parar. En la práctica se utilizan dos grandes enfoques: basarse en la función de coste sobre los datos de entrenamiento, o usar un conjunto de validación para monitorizar el rendimiento generalizado.
En el enfoque de early stopping con conjunto de validación, se reserva una fracción de los datos para validar (por ejemplo el 10%). El modelo se entrena sobre el resto y, tras cada época, se mide una métrica de rendimiento (p.ej. R² o error cuadrático medio) sobre la validación. Si esa métrica deja de mejorar durante un número determinado de épocas consecutivas (n_iter_no_change) por encima de una cierta tolerancia, detenemos el entrenamiento.
En el enfoque basado solo en entrenamiento, todo el dataset se usa para ajustar los parámetros, y la función objetivo se monitoriza cada cierto número de iteraciones. Si la mejora entre épocas es menor que una tolerancia determinada durante varias iteraciones, o si simplemente se alcanza un número máximo de pasos (max_iter), se detiene el proceso.
En una implementación de regresión cuadrática con SGD en JavaScript, podemos replicar estas estrategias sin problema: mantener un historial del coste, comparar diferencias sucesivas y cortar el bucle de entrenamiento cuando la mejora sea marginal. Si trabajas en el navegador, también es útil limitar el tiempo máximo de entrenamiento para no bloquear la interfaz, usando Web Workers o dividiendo el entrenamiento en pequeños bloques con setTimeout.
Descenso de gradiente simple, estocástico y por mini‑lotes
El descenso de gradiente “puro” (GD) calcula el gradiente completo de la función de coste, usando todas las muestras, y después da un paso en la dirección de descenso: x^{t+1} = x^t − α ∇f(x^t). Este método garantiza que, con un tamaño de paso suficientemente pequeño, la función disminuye en cada iteración, siempre que el gradiente no sea cero.
El descenso de gradiente estocástico (SGD) es una adaptación para funciones de coste que son suma de muchos términos individuales: f(x) = (1/|Ω|) Σ_{i ∈ Ω} fᵢ(x). En lugar de promediar todos los gradientes, en cada iteración muestreamos un subconjunto Sᵗ ⊂ Ω y calculamos un pseudo-gradiente como media solo sobre Sᵗ. Esto se interpreta como una estimación del valor esperado del gradiente verdadero.
En la práctica, la mayoría de implementaciones modernas usan mini‑batch gradient descent, una especie de término medio entre GD y SGD puro. En cada actualización se usa un pequeño lote de muestras (por ejemplo, 32, 64 o 128) para computar un gradiente menos ruidoso que el de una sola muestra, pero mucho más barato que el de todo el dataset. Este enfoque suele ser el punto dulce para entrenar redes neuronales y modelos lineales a gran escala.
Los beneficios de SGD y mini‑batch incluyen menos cálculos por iteración, cierta robustez frente a outliers y la posibilidad de aprovechar mejor la aleatoriedad para evitar mínimos locales pobres. La contrapartida es que el gradiente se vuelve ruidoso y la trayectoria de los parámetros puede oscilar, obligándonos a jugar con tasas de aprendizaje más cuidadas y, a menudo, con algoritmos de optimización avanzados como Momentum, Nesterov, RMSProp, AdaGrad, AdaDelta o Adam.
Variantes avanzadas: Momentum, Nesterov, AdaGrad, AdaDelta y Adam
El descenso de gradiente “ingenuo” puede ser lento o inestable en funciones con valles estrechos, muchos mínimos locales o escalas muy distintas entre parámetros. Por eso han surgido múltiples variantes que tratan de mejorar la convergencia, suavizar la trayectoria y adaptar el tamaño de paso a cada componente del gradiente.
El descenso de gradiente con momento (Momentum) introduce una especie de inercia: en lugar de usar solo el gradiente actual, se combina con una fracción del gradiente (o dirección) acumulado en iteraciones anteriores. La nueva dirección pᵗ se calcula como gᵗ + η p^{t−1}, con η cercano a 1 (p.ej. 0,9). Esto hace que los gradientes persistentes en una misma dirección se refuercen, acelerando la bajada, mientras que las oscilaciones de alta frecuencia se suavizan.
El descenso acelerado de Nesterov (NAG) afina todavía más el uso del momento. En vez de evaluar el gradiente en la posición actual, se hace una predicción adelantada x̃ᵗ = xᵗ − α p^{t−1}, se calcula el gradiente en x̃ᵗ y luego se corrige. Este esquema tipo predictor-corrector tiende a anticipar mejor los cambios en la superficie de la función de coste y reduce los sobrepasos en los valles.
AdaGrad introduce la idea de adaptar de forma independiente el paso en cada parámetro acumulando el cuadrado de las derivadas parciales. Para el parámetro i, se mantiene Gᵢᵗ = Σ (gᵢᵏ)² y se escala el paso como α / (√(Gᵢᵗ) + ε). De este modo, las coordenadas con gradientes grandes reciben pasos cada vez más pequeños, mientras que las que tienen gradientes sistemáticamente pequeños pueden seguir dando pasos razonables. Es muy útil cuando las características tienen escalas muy distintas.
AdaDelta mejora sobre AdaGrad usando una media móvil de los cuadrados de los gradientes en lugar de una suma acumulativa, lo que evita que el paso decrezca demasiado rápido en cuanto llevamos muchas iteraciones. Se actualiza Gᵢᵗ = η (gᵢᵗ)² + (1−η) Gᵢ^{t−1} con η grande (típicamente 0,9) y se usa esa Gᵢᵗ para escalar adaptativamente cada componente del paso.
Adam (Adaptive Moment Estimation) combina ideas de Momentum y AdaGrad/AdaDelta. Calcula un promedio exponencial de los gradientes (primer momento) y de sus cuadrados (segundo momento), corrige el sesgo inicial dividendo por factores (1−η₁ᵗ) y (1−η₂ᵗ), y ajusta el paso usando ambas estimaciones. Es especialmente efectivo con SGD sobre mini‑lotes y se ha convertido en el optimizador por defecto en muchos frameworks de Deep Learning debido a su estabilidad y rapidez de convergencia.
Implementar descenso de gradiente y SGD en JavaScript
Llevar toda esta teoría a JavaScript es más directo de lo que puede parecer. Podemos representar vectores y matrices con arrays nativos, usar bucles para calcular productos punto y derivadas, y encapsular el modelo de regresión en una clase que mantenga los parámetros (por ejemplo, coeficientes del polinomio y sesgo) y métodos de entrenamiento y predicción.
Un ejemplo clásico en Python implementa regresión lineal entrenada con descenso de gradiente mediante una clase que mantiene X, Y, los parámetros b, una función predict(), una función compute_cost() y un método update_coeffs() que aplica la regla de actualización usando el gradiente de la pérdida. Este patrón se puede trasladar casi tal cual a JavaScript, sustituyendo NumPy por arrays y operaciones manuales.
Para regresión cuadrática, bastaría con extender la característica de entrada añadiendo el término cuadrático. Es decir, para cada valor x generamos un vector [x, x²] (y opcionalmente 1 para el sesgo), y definimos el modelo como ŷ = w₀ + w₁x + w₂x². Las derivadas de la pérdida respecto a w₀, w₁ y w₂ son directas de obtener y el bucle de actualización de parámetros se mantiene idéntico al linéal.
En una implementación de SGD en JavaScript podríamos, por ejemplo, barajar los índices de las muestras antes de cada época, recorrerlos uno a uno o en bloques de tamaño batch_size, calcular la predicción y el error, actualizar los parámetros y, cada cierto número de iteraciones, evaluar el coste sobre el conjunto completo o una muestra grande para monitorizar la convergencia. Añadir Momentum, Nesterov o Adam implica mantener vectores adicionales para los promedios de gradientes, pero la estructura general del código no cambia demasiado.
Consejos prácticos al usar SGD (y regresión cuadrática) en la práctica
El descenso de gradiente estocástico es muy sensible al escalado de las características. Si quieres que converja de forma razonable, es muy recomendable escalar o estandarizar tus features: llevarlas a un rango [0, 1], [−1, 1] o tener media 0 y varianza 1. En un pipeline JS puedes normalizar los datos antes del entrenamiento y asegurarte de aplicar el mismo escalado a los datos de test.
Elegir la tasa de aprendizaje adecuada es crítico. Si η es demasiado grande, el algoritmo puede divergir o oscilar violentamente alrededor del mínimo; si es demasiado pequeña, convergerá, pero a paso de tortuga. Una buena práctica es probar varios órdenes de magnitud (por ejemplo 10⁻¹, 10⁻², 10⁻³…) y observar la curva de coste. También puedes usar una tasa de aprendizaje decreciente o recurrir a optimizadores adaptativos como Adam para reducir la sensibilidad a este hiperparámetro.
El número de iteraciones o épocas puede aproximarse considerando que SGD suele necesitar del orden de 10⁶ muestras observadas para converger razonablemente en muchos problemas estándar. Una estimación inicial de max_iter puede ser ceil(10⁶ / n), donde n es el número de muestras de entrenamiento. A partir de ahí, conviene monitorizar y cortar con early stopping cuando el coste deje de mejorar.
Cuando hay outliers en los datos, usar funciones de pérdida robustas (como Huber o epsilon-insensitive) puede marcar una gran diferencia. En lugar de minimizar solo el error cuadrático clásico, estas pérdidas limitan el impacto de errores muy grandes, evitando que unos pocos puntos extremos dominen el ajuste del modelo. Integrarlas en una implementación de SGD en JavaScript solo requiere cambiar la expresión de la pérdida y de su derivada.
Por último, si te mueves en el terreno de redes neuronales o problemas muy no lineales, aprovechar algoritmos como Momentum, Nesterov, AdaDelta o Adam sobre un esquema de SGD con mini‑lotes es prácticamente obligado. En regresión cuadrática simple se puede funcionar razonablemente bien con SGD básico, pero en cuanto el modelo se complica, estos optimizadores avanzados te van a ahorrar muchos quebraderos de cabeza.
Integrando todo lo anterior, la regresión cuadrática con descenso de gradiente estocástico en JavaScript se convierte en una combinación muy potente: por un lado, un modelo suficientemente flexible como para capturar relaciones no lineales sencillas (vía términos x², x³, etc.), y por otro, un mecanismo de entrenamiento escalable y versátil como SGD, enriquecido con regularización, estrategias de parada y optimizadores adaptativos. Con un buen preprocesado de datos, una tasa de aprendizaje razonable y algo de mimo en la implementación, es perfectamente viable entrenar este tipo de modelos de forma eficiente directamente en el navegador o en un backend Node.js.
