Regresión lineal y descenso de gradiente
Cuaderno del tema. La teoría y los ejercicios con todo su código, listos para ejecutar en Colab sin instalar nada.
Los 13 bloques de código de la teoría, en el mismo orden. Si prefieres armar tu propio cuaderno, cópialos y córrelos en orden; en la ficha Teoría está la explicación de cada uno.
El modelo: una recta con dos perillas
import numpy as np
import matplotlib.pyplot as plt
# Semilla fija: cada vez que corras esto saldrán los mismos datos
rng = np.random.default_rng(42)
n = 30 # cantidad de estudiantes
# Horas de estudio en la semana del examen, entre 0 y 8
horas = rng.uniform(0, 8, size=n)
# Todo lo que las horas no explican (sueño, nervios, suerte)
ruido = rng.normal(0, 6, size=n)
# La regla «secreta»: 7 puntos por hora, partiendo de 30
nota = 30 + 7 * horas + ruido
print("Horas:", np.round(horas[:5], 1))
print("Notas:", np.round(nota[:5], 1))
plt.scatter(horas, nota)
plt.xlabel("Horas de estudio")
plt.ylabel("Nota del examen (sobre 100)")
plt.title("Treinta estudiantes (datos sintéticos)")
plt.show()Medir el error: residuos y función de pérdida
def predecir(x, w, b):
"""La predicción del modelo: una recta."""
return w * x + b
# Una primera recta elegida «a ojo»
w, b = 5.0, 40.0
prediccion = predecir(horas, w, b)
residuos = nota - prediccion # real menos predicho
for i in range(3):
print(f"Estudiante {i}: estudió {horas[i]:.1f} h, "
f"sacó {nota[i]:.1f}, la recta dice {prediccion[i]:.1f}, "
f"residuo {residuos[i]:+.1f}")
# Dos puntos bastan para dibujar una recta
x_recta = np.array([0, 8])
plt.scatter(horas, nota)
plt.plot(x_recta, predecir(x_recta, w, b), color="C0",
label="Recta a ojo: 5·x + 40")
plt.vlines(horas, prediccion, nota, color="C2", linewidth=1.2,
label="Residuos")
plt.xlabel("Horas de estudio")
plt.ylabel("Nota del examen (sobre 100)")
plt.title("Cada residuo es un error de la recta")
plt.legend()
plt.show()def mse(y, y_pred):
"""Error cuadrático medio."""
return np.mean((y - y_pred) ** 2)
def mae(y, y_pred):
"""Error absoluto medio."""
return np.mean(np.abs(y - y_pred))
for w, b in [(5, 40), (7, 30), (9, 20)]:
p = predecir(horas, w, b)
error = mse(nota, p)
print(f"w = {w}, b = {b}: MSE = {error:4.1f} "
f"RMSE = {np.sqrt(error):3.1f} MAE = {mae(nota, p):3.1f}")La pérdida es un paisaje
# 200 pendientes posibles entre 0 y 14
ws = np.linspace(0, 14, 200)
perdidas = [mse(nota, predecir(horas, w, 30)) for w in ws]
mejor = ws[np.argmin(perdidas)] # la pendiente con menor pérdida
plt.plot(ws, perdidas)
plt.scatter([mejor], [min(perdidas)], color="C1", s=140, zorder=3,
label="El punto más bajo")
plt.xlabel("Pendiente w (con b = 30 fijo)")
plt.ylabel("MSE")
plt.title("La pérdida según w")
plt.legend()
plt.show()
print(f"Con b = 30, la mejor pendiente está cerca de w = {mejor:.2f}")El gradiente: hacia dónde sube el valle
def gradiente(x, y, w, b):
"""Derivadas parciales del MSE respecto de w y de b."""
# Error = predicho menos real: el residuo con el signo cambiado
error = predecir(x, w, b) - y
dw = 2 * np.mean(error * x)
db = 2 * np.mean(error)
return dw, db
def perdida_en(w, b):
"""El MSE de la recta (w, b) con los datos de las notas."""
return mse(nota, predecir(horas, w, b))
# Comprobación con la derivada numérica (diferencia centrada),
# en la recta a ojo
h = 1e-6
w, b = 5.0, 40.0
dw_num = (perdida_en(w + h, b) - perdida_en(w - h, b)) / (2 * h)
db_num = (perdida_en(w, b + h) - perdida_en(w, b - h)) / (2 * h)
print("Con la fórmula: ", np.round(gradiente(horas, nota, w, b), 4))
print("Derivada numérica:", np.round([dw_num, db_num], 4))Descenso de gradiente: bajar paso a paso
def descenso_gradiente(x, y, tasa, pasos, w=0.0, b=0.0):
"""Ajusta y ≈ w·x + b bajando por el MSE.
Devuelve los w y b finales y el historial de (w, b, MSE).
"""
historial = [] # (w, b, MSE) antes de cada paso
for paso in range(pasos):
historial.append((w, b, mse(y, predecir(x, w, b))))
dw, db = gradiente(x, y, w, b) # hacia dónde sube el valle
w = w - tasa * dw # un paso en contra
b = b - tasa * db
historial.append((w, b, mse(y, predecir(x, w, b))))
return w, b, np.array(historial)
w_final, b_final, historial = descenso_gradiente(
horas, nota, tasa=0.02, pasos=2000)
for paso in [0, 1, 10, 100, 500, 2000]:
w_p, b_p, perdida = historial[paso]
print(f"Paso {paso:4d}: w = {w_p:5.2f} b = {b_p:5.2f}"
f" MSE = {perdida:7.1f}")fig, ejes = plt.subplots(2, 2, figsize=(4.8, 4.8),
sharex=True, sharey=True)
for eje, paso in zip(ejes.flat, [0, 10, 100, 2000]):
w_p, b_p, perdida = historial[paso]
# Solo la recta final va resaltada (color "C1")
color = "C1" if paso == 2000 else "C2"
eje.scatter(horas, nota, s=14)
eje.plot(x_recta, predecir(x_recta, w_p, b_p), color=color)
# Título en dos líneas para que quepa en cada panel
eje.set_title(f"Paso {paso}\nMSE = {perdida:.0f}")
for eje in ejes[1]:
eje.set_xlabel("Horas")
for eje in ejes[:, 0]:
eje.set_ylabel("Nota")
plt.show()# Una cuadrícula de 200 × 200 valores de (w, b)
W, B = np.meshgrid(np.linspace(-2, 14, 200), np.linspace(-10, 60, 200))
# Solo para dibujar: el MSE de las 40 000 rectas a la vez. [..., None]
# agrega un eje al final para combinarlo con los 30 estudiantes; no
# necesitas entender cada detalle
Z = np.mean((W[..., None] * horas + B[..., None] - nota) ** 2, axis=-1)
niveles = np.geomspace(Z.min() + 1, Z.max(), 14)
# Más alta que la de siempre: la leyenda va debajo
plt.figure(figsize=(4.8, 4.4))
plt.contour(W, B, Z, levels=niveles, colors="C2", linewidths=1,
alpha=0.5)
plt.plot(historial[:, 0], historial[:, 1], color="C0",
label="Camino del descenso")
algunos = [0, 1, 2, 3, 10, 100] # algunos pasos, marcados
plt.scatter(historial[algunos, 0], historial[algunos, 1],
color="C0", s=30, zorder=3)
plt.scatter([w_final], [b_final], color="C1", s=160, zorder=4,
label="Después de 2000 pasos")
plt.xlabel("w (pendiente)")
plt.ylabel("b (intercepto)")
plt.title("El valle del MSE visto desde arriba")
# La leyenda va debajo del gráfico, para no tapar las curvas
plt.legend(loc="upper center", bbox_to_anchor=(0.5, -0.24),
frameon=False)
plt.show()La tasa de aprendizaje
tasas = [(0.002, "C0", "lenta"),
(0.02, "C1", "adecuada"),
(0.04, "C2", "demasiado grande")]
plt.figure()
for tasa, color, nombre in tasas:
_, _, h = descenso_gradiente(horas, nota, tasa=tasa, pasos=600)
plt.plot(h[:, 2], color=color, label=f"tasa {tasa}: {nombre}")
print(f"tasa {tasa}: MSE después de 600 pasos = {h[-1, 2]:.4g}")
# Escala logarítmica: cada marca del eje es 10 veces la anterior
plt.yscale("log")
plt.ylim(10, 1e5)
plt.xlabel("Paso")
plt.ylabel("MSE (escala logarítmica)")
plt.title("La misma bajada con tres tasas")
plt.legend()
plt.show()La solución de scikit-learn
from sklearn.linear_model import LinearRegression
modelo = LinearRegression()
# X tiene que ser una tabla: 30 filas y 1 columna
modelo.fit(horas.reshape(-1, 1), nota)
print(f"scikit-learn: w = {modelo.coef_[0]:.4f}"
f" b = {modelo.intercept_:.4f}")
print(f"Nuestro descenso: w = {w_final:.4f} b = {b_final:.4f}")
print("Nota predicha para 6 horas:", modelo.predict([[6]]).round(1))Varias variables a la vez
from sklearn.datasets import load_diabetes
import pandas as pd
diabetes = load_diabetes(as_frame=True)
X = diabetes.data # tabla: 442 pacientes × 10 características
y = diabetes.target # avance de la enfermedad un año después
print("Forma de X:", X.shape)
print("Columnas:", list(X.columns))
print("Objetivo: de", y.min(), "a", y.max(),
"con media", round(y.mean(), 1))# Estandarizar: cada columna queda con media 0 y desviación estándar 1
X_est = ((X - X.mean()) / X.std(ddof=0)).to_numpy()
y_arr = y.to_numpy()
def descenso_gradiente_varias(X, y, tasa, pasos):
"""Descenso de gradiente para y ≈ X @ w + b (un peso por columna)."""
n, d = X.shape
w = np.zeros(d) # un peso por característica
b = 0.0
for paso in range(pasos):
error = X @ w + b - y # los n errores de una vez
w = w - tasa * 2 * X.T @ error / n
b = b - tasa * 2 * np.mean(error)
return w, b
w_gd, b_gd = descenso_gradiente_varias(X_est, y_arr, tasa=0.1,
pasos=20000)
modelo_diabetes = LinearRegression().fit(X_est, y_arr)
comparacion = pd.DataFrame(
{"descenso": w_gd, "scikit-learn": modelo_diabetes.coef_},
index=X.columns)
print(comparacion.round(2))
print(f"b: descenso = {b_gd:.2f}"
f" scikit-learn = {modelo_diabetes.intercept_:.2f}")¿Qué tan bueno es el modelo?
from sklearn.metrics import r2_score
pred = modelo_diabetes.predict(X_est)
residuos = y_arr - pred
r2 = 1 - np.sum(residuos ** 2) / np.sum((y_arr - y_arr.mean()) ** 2)
rmse = np.sqrt(np.mean(residuos ** 2))
error_abs = np.mean(np.abs(residuos))
print(f"RMSE = {rmse:.1f} MAE = {error_abs:.1f} R² = {r2:.3f}")
print(f"R² según scikit-learn: {r2_score(y_arr, pred):.3f}")
# Un modelo con una sola columna: bmi (la de índice 2)
solo_bmi = X_est[:, [2]]
modelo_bmi = LinearRegression().fit(solo_bmi, y_arr)
print(f"R² usando solo bmi: {modelo_bmi.score(solo_bmi, y_arr):.3f}")
plt.scatter(pred, residuos, s=16)
plt.axhline(0, color="C2")
plt.xlabel("Predicción del modelo")
plt.ylabel("Residuo (real menos predicho)")
plt.title("Residuos frente a predicciones")
plt.show()¿Terminaste este tema?
Márcalo y en la ruta verás cuánto llevas. Se guarda solo en este navegador.