IADev - Senior por Suscripción

Inicio

Regresión lineal en Go: 20 líneas de código o harness de agentes

Antes de empezar, ¿qué haremos aquí? exploraremos creo yo que el algoritmo más elemental y simple que podemos hacer para que algo sea “inteligente” o más bien, que una máquina sea capaz de aprender.

Haremos un pequeño script en Go para implementar un algoritmo de regresión lineal simple, sin dependencias externas, utilizando únicamente la biblioteca estándar, con esto podemos de forma práctica endender, cómo hacer que un software sea capaz de aprender patrones y producir respuestas gracias ello.

Por qué en Go y no en Python? porque me gusta más y ya está, igualmente, esto lo puedes implementar hasta en un Protoboard con compuertas lógicas o quizá menos, si te lo ingenias bien.

Cómo funciona el algoritmo?

La regresión lineal simple busca una recta que se ajuste lo mejor posible a un conjunto de observaciones, vale que hay que ver algo de matemática pero muy simple la verdad, con sabernos la formula basta:

y = b0 + b1*x

Esa simple ecuación, dónde, si esto lo usamos para calcular la predicción del precio de una vivienda, cada variable podria ser

  • x: variable independiente, por ejemplo, metros cuadrados
  • y: variable dependiente, por ejemplo, el precio de la vivienda, lo que queremos obtener
  • b0: intercepto, el valor estimado cuando x = 0
  • b1: pendiente, cuánto cambia y por cada unidad adicional de la x

El algoritmo utiliza el método de mínimos cuadrados ordinarios (OLS), minimizando la suma de los errores al cuadrado entre los valores que se observan y los que se estiman.

Implementación en Go

Crearemos un paquete reutilizable llamado regression


package regression

import (
	"errors"
	"math"
)

type LinearRegression struct {
	Intercept float64
	Slope     float64
}

// Calcula los coeficientes mediante mínimos cuadrados
func Fit(x, y []float64) (*LinearRegression, error) {
	if len(x) != len(y) {
		return nil, errors.New("x e y deben tener la misma longitud")
	}

	if len(x) < 2 {
		return nil, errors.New("se necesitan al menos dos observaciones")
	}

	n := float64(len(x))

	var sumX, sumY float64

	for i := range x {
		sumX += x[i]
		sumY += y[i]
	}

	meanX := sumX / n
	meanY := sumY / n

	var ssXY, ssXX float64

	for i := range x {
		dx := x[i] - meanX
		dy := y[i] - meanY

		ssXY += dx * dy
		ssXX += dx * dx
	}

	if ssXX == 0 {
		return nil, errors.New("x no tiene variación")
	}

	slope := ssXY / ssXX
	intercept := meanY - slope*meanX

	return &LinearRegression{
		Intercept: intercept,
		Slope:     slope,
	}, nil
}

// Estima el valor de y para un valor x.
func (lr *LinearRegression) Predict(x float64) float64 {
	return lr.Intercept + lr.Slope*x
}

// Mide la calidad del ajuste.
// Un valor próximo a 1 indica mejor ajuste.
func (lr *LinearRegression) RSquared(x, y []float64) float64 {
	if len(x) != len(y) || len(y) == 0 {
		return math.NaN()
	}

	var meanY float64
	for _, value := range y {
		meanY += value
	}
	meanY /= float64(len(y))

	var ssTotal, ssResidual float64

	for i := range y {
		prediction := lr.Predict(x[i])

		ssTotal += math.Pow(y[i]-meanY, 2)
		ssResidual += math.Pow(y[i]-prediction, 2)
	}

	if ssTotal == 0 {
		if ssResidual == 0 {
			return 1
		}
		return 0
	}

	return 1 - ssResidual/ssTotal
}

Con esta implementación ofrecemos tres operaciones muy básicas:

  • Fit(x, y) esto entrena al modelo y calcula sus coeficientes
  • Predict(x) esto predice un resultado utilizando el modelo ya entrenado
  • RSquared(x, y) calcula el coeficiente de determinación R^2`

PD: si te fijas, solo como dato, para que lo interiorices un poco más, este “aprendizaje” se guarda en memoria ram, por ello Predict y RSquared son métodos del “objeto” que será el modelo ya entrenado, el entrenamiento con Fit nos devuelve ese modelo, no lo sé, pero este tipo de detalles tontos siempre me parecen arte cuando los concibes xd

Cómo lo usamos?

Veamos algunos ejemplos de caso de uso

Estimar el precio de una vivienda

Imagina que tenemos los precios históricos de viviendas de una zona y queremos estimar cuánto costaría una vivienda de X metros cuadrados, pues vamos a implementar en Go


package main

import (
	"fmt"

	"github.com/project/regression"
)

func main() {
	// Superficies en metros cuadrados.
	x := []float64{
		50, 65, 80,
		95, 120, 150,
	}

	// Precioss históricos en euros por cada m2
	y := []float64{
		150000, 185000, 220000,
		255000, 310000, 380000,
	}

	// Entrenamos al modelo (vamos a gastar CPU, RAM y Agua)
	model, err := regression.Fit(x, y)
	if err != nil {
		panic(err)
	}

	// Ahora, le pasamos unos m2 que nunca antes ha visto
	price := model.Predict(110)

	fmt.Printf("Precio estimado: %.2f €\n", price)
	fmt.Printf("R2: %.4f\n", model.RSquared(x, y))
}

Ejecutando podriamos tener algo como

Precio estimado: 288.000 €
R2 ≈ 0,9998.

Otra cosita, el R2, es el coeficiente de determinación, básicamente es una métrica que indica qué proporción de la variabilidad de los datos consigue explicar nuestro modelo de regresión lineal, mientras más tienda a 1 es mejor.

Si por ejemplo esto nos da 0.8, explica el 80% de la variabilidad, si nos da 0,0 no mejora la predicción constante basada en la media y si nos da un negativo significa que es peor que predecir siempre la media.


Y ya está, hemos enseñado a la máquina a hacer algo, qué esperabas? verdad que no es nada del otro mundo? verdad que has programado cosas más complejas? es simple, esto es una forma muy básica de cómo entrenar modelos.

En el post de la ia bajo el capó hablamos de los “pesos”, pues en este ejemplo, nuestra estructura de datos

type LinearRegression struct {
	Intercept float64
	Slope     float64
}

sería el equivalente a esos famosos pesos de los que tanto se habla cuando se entrenan modelos como GPT, porque esos valores son los parámetros aprendidos durante el entrenamiento.

Ahora veamos otro ejemplo simple:

Cuánto tardaría un server en explotar por una fuga de memoria?

Imagina que tenemos un proceso en Go con una fuga de memoria, cada minuto registras el consumo de RAM con alguna herramienta y eso te da datos

Minuto RAM utilizada
0 120 MB
5 175 MB
10 230 MB
15 290 MB
20 345 MB
25 405 MB

Con esa info podríamos simplemente reemplazar y predecir cuándo el proceso alcanzrá 1GB de memoria.


package main

import (
	"fmt"

	"example.com/project/regression"
)

func main() {
	// Los datos de la tabla
	minutes := []float64{0, 5, 10, 15, 20, 25}
	memory := []float64{120, 175, 230, 290, 345, 405}

	// Entrenamos el modelo otra vez
	model, err := regression.Fit(minutes, memory)
	if err != nil {
		panic(err)

	// Predecimos ahora el consumo de memoria para minutos que el modelo aun no tiene en su memoria, si notas empezamos desde +30
	futureMinutes = []float64{30, 40, 50, 60, 70, 80, 90}
	for _, minute := range futureMinutes {
		prediction := model.Predict(minute)

		fmt.Printf(
			"Minuto %.0f: %.2f MB\n",
			minute,
			prediction,
		)
	}

	// Calculamos cuándo se alcanzará el límite.
	const memoryLimit = 1024.0 // MB

	// Esto podría pasar
	if model.Slope <= 0 {
		fmt.Println("No se detecta crecimiento de memoria.")
		return
	}

	timeToLimit := (memoryLimit - model.Intercept) / model.Slope

	fmt.Printf("Límite de memoria: %.0f MB\n", memoryLimit)
	fmt.Printf("Tiempo estimado hasta el límite: %.2f minutos\n", timeToLimit)

	// Tiempo restante desde la última observación.
	lastMinute := minutes[len(minutes)-1]
	remainingTime := timeToLimit - lastMinute

	if remainingTime <= 0 {
		fmt.Println("El modelo estima que el límite ya se ha alcanzado.")
	} else {
		fmt.Printf(
			"Tiempo restante desde la última medición: %.2f minutos\n",
			remainingTime,
		)
	}
}

El resultado debería ser algo como

Minuto 30: 459.86 MB
Minuto 40: 573.86 MB
Minuto 50: 687.86 MB
Minuto 60: 801.86 MB
Minuto 70: 915.86 MB
Minuto 80: 1029.86 MB
Minuto 90: 1143.86 MB

Límite de memoria: 1024 MB
Tiempo estimado hasta el límite: 79.49 minutos
Tiempo restante desde la última medición: 54.49 minutos

Y así de simple. Hemos implementado un algoritmo de regresión lineal en unas pocas líneas de Go, sin frameworks de machine learning, sin dependencias de medio internet y lo mejor de todo, sin necesitar una suscripción de OpenAI que nos explique cómo sumar dos números.

regresion-linear-en-go

No hemos tenido que resolver integrales, derivadas, ecuaciones diferenciales ni invocar ninguna entidad matemática de otro universo, solo hemos utilizado una fórmula que algún matemático, hace bastante tiempo, tuvo la amabilidad de desarrollar y nosotros como buenos ingenieros, hemos hecho lo que mejor sabemos hacer: entender el problema, aprovechar el conocimiento existente e implementarlo.

El resultado es un algoritmo sencillo, comprensible, prácticamente gratuito de ejecutar y con un potencial enorme para resolver problemas reales.

Así que la próxima vez que necesites hacer una predicción sencilla, quizá no sea necesario montar un harness con 999 agentes de IA, cada uno con su correspondiente archivo .md de 800 líneas explicándole que es un ingeniero sénior con 30 años de experiencia, experto en estadística bayesiana, arquitectura hexagonal y liderazgo de equipos multidisciplinares.

Tampoco hace falta que un agente planificador convoque a tres subagentes investigadores, que estos deleguen en otros cinco agentes especializados y que, después de consumir 200.000 tokens y unos cuantos dólares por petición, un agente revisor concluya que la mejor solución es instalar una dependencia de Python que, por debajo, hace exactamente lo que acabamos de implementar.

A veces, la solución no necesita más inteligencia artificial, necesita un poco más de inteligencia real y junto con algo de ingeniería.

Pero bueno, siempre puedes preguntarle a tu agente si está de acuerdo, seguramente te responda que es una reflexión excelente, que tienes toda la razón y que le encantaría ayudarte a diseñar una arquitectura multiagente para evitar que vuelva a suceder.

VIBECODER, aquí!

¿Leer tú? Eso no escala. Copia este prompt y que tu IA de confianza haga el esfuerzo.

Lee el artículo completo en https://iadev.blog/posts/regresion-lineal-go-o-harness-de-agentes/ y resúmelo.

Explica la idea principal, los argumentos clave y la conclusión. Distingue la sátira de las afirmaciones literales por mi y no añadas datos que no aparezcan en el texto. Si no puedes acceder a la URL, dilo y pídeme que pegue el artículo.

Compártelo

El algoritmo ya ha leído suficiente.
Pásaselo a una persona.

Redes sociales

Mensajería

¿Quieres escribir aquí? Pues escribe.

Este blog también es tuyo. Ven a despotricar a gusto: aquí no hay censura ni necesitas un cargo inventado en LinkedIn, solo un post en Markdown y una PR.

Crea otra categoría si te da la gana, firma con tu cuenta de Github, crea una nueva feature si la necesitas para el post, total, hoy día todos podemos hacer apps millonarias con un prompt, ¿qué es hacer una feature en un blog estático frente a eso?

Escribir mi post Guía para publicar

~ Antes de escribir necesitas ser colaborador del repositorio, puedes solicitar acceso abriendo este issue.
~ El proceso está automatizado, recibirás una invitación que debes aceptar.

Puedes crear el post como anónimo, pero tu cuenta y la PR mergeada seguirán siendo públicas en GitHub ya que entrarás en los commits.

Historial

  1. chore: post descenso-de-gradiente-go-machine-learning (#5)Brayan Narváez29b5dfb
  2. chore: post regresion-lineal-go-o-harness-de-agentes (#4)Brayan Narváezb736065
  3. feat: contribuidores aceptados mediante actions por abrir issues (#3)Brayan Narváez8c6bcf1
  4. chore: post la-ia-arreglo-un-bug-y-me-regalo-200k-lineas-de-deuda-tecnica (#2)Brayan Narváez3faad2f
  5. chore: post la-ia-arreglo-un-bug-y-me-regalo-200000-lineas-de-deuda-tecnicaBrayan Narváez2fafa99
  6. chore: post la-ia-bajo-el-capo (#1)Brayan Narváez4b54226
  7. chore: post la-ia-bajo-el-capoBrayan Narváez25bc714
  8. release: primera versión del blog, desde este punto partimosBrayan Narváez0468f2a
  9. feat: plan de contingenciaBrayan Narváez8113fd7
  10. feat: pagina 404 y componente botónBrayan Narváez5ea7de2
  11. feat: crear nuevo post y fallback del fallback de avatarBrayan Narváez1468a37
  12. chore: consideracion para desplegar en cloudflare, token de authBrayan Narváez4f63a72
  13. ia: generacion de imagen para seo (pero revisado y corregido, conBrayan Narváezfc0046c
  14. feat: compartir articulosBrayan Narváezb7c8b33
  15. feat: articulos aptos para "vibras"Brayan Narváez8dc7d26
  16. fix: tooltip sobre el boton back to topBrayan Narváez2a47b01
  17. feat: back to top botonBrayan Narváez8131e93
  18. feat: múltiples autores, cualquiera podrá escribir en este blogBrayan Narváez2d30665
  19. fix: borde reservado para el scroll eliminadoBrayan Narváez8210e02
  20. feat: historial de commits del repo en barra lateralBrayan Narváez3273be9
  21. feat: footer como barra baja con iconosBrayan Narváezb92bac1
  22. chore: actualizamos el sloganBrayan Narváez3b81fc7
  23. feat: retraso de 200ms al tooltipBrayan Narváez55badff
  24. feat: cursor blinkBrayan Narváezd8dad1f
  25. ia: ajustes visuales responsiveBrayan Narváez173d5e9
  26. feat: calculo de gasto de agua con inflacion xdBrayan Narváez4671788
  27. feat: calculo de tokens en buildtime, basado en conteo, solo tokens deBrayan Narváezcbb05c6
  28. feat: tiempo de lectura, base para conteo de tokensBrayan Narváez06db851
  29. feat: animacion de thumbnail con view-transitionBrayan Narváez359846d
  30. feat: categorias para los posts, y algunas refactorizaciones pequeñasBrayan Narváez2c40065
  31. feat: thumbnail, prompt para generar mas, faviconbrayan-bemobile4e1baaf
  32. ia: variables de font size en cssBrayan Narváezeda8702
  33. chore: check formatea los ficheros con biome correctamenteBrayan Narváez85b4d76
  34. feat: paginador en la homeBrayan Narváez78de189
  35. chore: centrando un poquito, aquí falta diseñoBrayan Narváez621889c
  36. fix: mi 404, no el de astroBrayan Narváezba35d49
  37. feat: diseño base y arquitectura aplicada, le pregunté a claudioBrayan Narváez50fdadd
  38. feat: husky como ci local, por si la IA enloqueceBrayan Narváez82f5114
  39. feat: astro base setup y arquitectura definidaBrayan Narváez167f91d