Antes de empezar
Varios ejercicios usan las variables y funciones de la teoría: horas, nota, predecir, mse, mae, gradiente, descenso_gradiente, modelo (la recta de las horas y las notas), X_est, y_arr, descenso_gradiente_varias y modelo_diabetes. Abre un cuaderno nuevo en Colab, pega y ejecuta en orden los bloques de la ficha Código y después trabaja debajo. Intenta cada ejercicio antes de abrir la respuesta: equivocarse primero y corregir después es como se aprende de verdad.
Ejercicios
1. Las piezas del aprendizaje
En el ejemplo de las horas de estudio y las notas, di qué es cada cosa:
- La recta ŷ = w·x + b.
- Los números w y b.
- El MSE.
- Repetir pasos de descenso de gradiente hasta que el error deje de bajar.
- Las horas de estudio, y la nota.
Respuesta
- El modelo: la forma de la regla que usamos para predecir.
- Los parámetros: los números del modelo que se ajustan con los datos.
- La función de pérdida: un solo número que dice qué tan mal predice el modelo con esos parámetros.
- El entrenamiento: buscar los parámetros que hacen mínima la pérdida.
- Las horas son la característica (la entrada, x) y la nota es el objetivo (lo que queremos predecir, y). Como cada ejemplo trae su respuesta, es aprendizaje supervisado.
2. MSE y MAE a mano
Cuatro puntos: (1, 3), (2, 5), (3, 4) y (4, 8). Para la recta ŷ = x + 2, calcula sin computadora los residuos, el MSE, el RMSE y el MAE. Después compruébalo con las funciones mse y mae de la teoría.
Respuesta
Las predicciones son 3, 4, 5 y 6. Los residuos (real menos predicho) son 0, 1, −1 y 2.
- MSE = (0 + 1 + 1 + 4) / 4 = 1,5
- RMSE = √1,5 ≈ 1,22
- MAE = (0 + 1 + 1 + 2) / 4 = 1
x = np.array([1, 2, 3, 4])
y = np.array([3, 5, 4, 8])
p = x + 2
print("Residuos:", y - p)
print(f"MSE = {mse(y, p)} RMSE = {np.sqrt(mse(y, p)):.2f}"
f" MAE = {mae(y, p)}")
Residuos: [ 0 1 -1 2]
MSE = 1.5 RMSE = 1.22 MAE = 1.0
3. El gradiente a mano
a) Partiendo de MSE = (1/n) · Σ (w·xᵢ + b − yᵢ)², deriva tú mismo la derivada parcial respecto de b. Escribe cada paso y di qué regla usas.
b) Con los cuatro puntos del ejercicio 2 y la recta w = 1, b = 2, calcula las dos derivadas parciales.
c) Da un paso de descenso de gradiente con tasa α = 0,1. ¿Cuáles son los nuevos w y b? ¿Bajó el MSE?
Respuesta
a) La derivada de una suma es la suma de las derivadas, y la constante 1/n sale afuera. Para cada término usamos la regla de la cadena: la derivada de u² es 2u por la derivada de u. Aquí u = w·xᵢ + b − yᵢ, y su derivada respecto de b es 1 (w, xᵢ e yᵢ no dependen de b). Entonces
∂MSE/∂b = (1/n) · Σ 2 · (w·xᵢ + b − yᵢ) · 1 = (2/n) · Σ (ŷᵢ − yᵢ),
es decir, el doble del error promedio (predicho menos real).
b) Los errores ŷ − y son 0, −1, 1 y −2.
- ∂MSE/∂b = 2 · (0 − 1 + 1 − 2) / 4 = −1
- ∂MSE/∂w = 2 · (0·1 − 1·2 + 1·3 − 2·4) / 4 = 2 · (−7/4) = −3,5
c) w = 1 − 0,1 · (−3,5) = 1,35 y b = 2 − 0,1 · (−1) = 2,1. El MSE baja de 1,5 a 1,279. Las dos derivadas eran negativas, así que el paso aumentó w y b: la recta se inclinó más y subió, porque se quedaba corta con el último punto.
dw, db = gradiente(x, y, 1, 2)
print("dw =", dw, " db =", db)
w, b = 1 - 0.1 * dw, 2 - 0.1 * db
print(f"Después del paso: w = {w:.2f} b = {b:.2f}")
print(f"MSE antes = {mse(y, predecir(x, 1, 2)):.3f}"
f" después = {mse(y, predecir(x, w, b)):.3f}")
dw = -3.5 db = -1.0
Después del paso: w = 1.35 b = 2.10
MSE antes = 1.500 después = 1.279
4. ¿Qué tasa es cuál?
Se entrenó la recta de la teoría (las 30 notas, partiendo de w = 0, b = 0) tres veces, cada una con una tasa de aprendizaje distinta, A, B y C. La tabla de abajo muestra el MSE en algunos pasos. Para cada entrenamiento, di si la tasa es lenta, adecuada o demasiado grande, y qué harías.
El código que hizo la tabla (muestra las tasas: ábrelo al final)
pasos_tabla = [0, 1, 2, 5, 10, 50, 100, 300]
curvas = {}
for nombre, tasa in [("A", 0.03), ("B", 0.039), ("C", 0.005)]:
_, _, h = descenso_gradiente(horas, nota, tasa=tasa, pasos=300)
curvas[nombre] = h[pasos_tabla, 2]
tabla = pd.DataFrame({"Paso": pasos_tabla, **curvas})
print(tabla.to_string(index=False, float_format=lambda v: f"{v:.4g}"))
Paso A B C
0 4123 4123 4123
1 1486 4479 2336
2 616.5 4868 1363
5 196.1 6269 385.9
10 162.6 9623 203.9
50 74.1 3.225e+05 168.6
100 37.3 2.652e+07 141.3
300 22.57 1.213e+15 74.43
Respuesta
- A, adecuada: baja rápido y en el paso 300 ya está en 22,57, casi el mínimo (22,47, el de la teoría). Se puede parar.
- B, demasiado grande: el error sube desde el primer paso y cada vez más rápido. El descenso diverge: hay que bajar la tasa. Fíjate en que al principio sube despacio: con una tasa apenas por encima del límite, la divergencia tarda en notarse.
- C, lenta: baja todo el tiempo, pero en 300 pasos todavía está en 74,43. Funciona, pero conviene subir la tasa o dar muchos más pasos.
Las tasas eran A = 0,03, B = 0,039 y C = 0,005.
5. Un atípico: MSE frente a MAE
El estudiante de índice 5 es el que más estudió. Imagina que al pasar las notas alguien se equivocó y le anotó un 0.
nota_mala = nota.copy()
nota_mala[5] = 0 # el error al pasar las notas
print(f"Horas: {horas[5]:.1f} nota real: {nota[5]:.1f}")
Horas: 7.8 nota real: 91.4
a) Sin volver a entrenar, calcula el MSE y el MAE de la recta de scikit-learn de la teoría (modelo, entrenado con las notas correctas) antes y después del error. ¿Cuántas veces crece cada uno?
b) Vuelve a entrenar LinearRegression con nota_mala. ¿Cuánto cambian w y b?
Respuesta
pred = modelo.predict(horas.reshape(-1, 1))
antes = mse(nota, pred), mae(nota, pred)
despues = mse(nota_mala, pred), mae(nota_mala, pred)
print(f"MSE: {antes[0]:.1f} -> {despues[0]:.1f}"
f" ({despues[0] / antes[0]:.1f} veces)")
print(f"MAE: {antes[1]:.2f} -> {despues[1]:.2f}"
f" ({despues[1] / antes[1]:.1f} veces)")
print(f"Residuo del dato malo: {nota_mala[5] - pred[5]:.1f}")
modelo_malo = LinearRegression().fit(horas.reshape(-1, 1), nota_mala)
print(f"Antes: w = {modelo.coef_[0]:.2f} b = {modelo.intercept_:.2f}")
print(f"Después: w = {modelo_malo.coef_[0]:.2f}"
f" b = {modelo_malo.intercept_:.2f}")
MSE: 22.5 -> 261.6 (11.6 veces)
MAE: 3.86 -> 6.48 (1.7 veces)
Residuo del dato malo: -84.9
Antes: w = 6.89 b = 31.18
Después: w = 5.02 b = 36.51
a) Con la misma recta, el MSE crece casi 12 veces y el MAE apenas 1,7 veces. Un solo residuo de unos 85 puntos, elevado al cuadrado, pesa más que los otros 29 juntos.
b) Un solo dato malo entre treinta le baja la pendiente en casi dos puntos por hora y le sube el intercepto en más de cinco. Como LinearRegression minimiza el MSE, hereda su sensibilidad a los atípicos. Por eso siempre conviene mirar los datos antes de entrenar (tema 4) y, si hay atípicos que no se pueden corregir, considerar el MAE.
6. ¿Hasta dónde se puede subir la tasa?
Con los datos de la teoría, prueba tasas entre 0,036 y 0,039 con 2000 pasos y busca la más grande con la que el descenso todavía converge. Escribe un bucle; no lo hagas a mano.
Respuesta
for tasa in [0.036, 0.037, 0.038, 0.0385, 0.039]:
_, _, h = descenso_gradiente(horas, nota, tasa=tasa, pasos=2000)
print(f"tasa {tasa}: MSE final = {h[-1, 2]:.4g}")
tasa 0.036: MSE final = 22.47
tasa 0.037: MSE final = 22.47
tasa 0.038: MSE final = 22.47
tasa 0.0385: MSE final = 1.146e+36
tasa 0.039: MSE final = 1.576e+80
Con 0,038 todavía converge al mínimo (22,47) y con 0,0385 ya diverge: el límite está entre 0,038 y 0,0385. Ese límite depende de los datos: cuanto más grandes son los valores de x, más empinadas son las paredes del valle y más chico tiene que ser el paso. Es otra razón para estandarizar.
7. Pocos pasos con muchas variables
En la teoría entrenamos el modelo de diabetes con 20 000 pasos. Repite el entrenamiento con solo 300 pasos y compara con scikit-learn (modelo_diabetes): el MSE y los pesos de bmi, s1 y s2. ¿Qué observas? ¿Cómo lo explicas?
Respuesta
w_300, b_300 = descenso_gradiente_varias(X_est, y_arr, tasa=0.1, pasos=300)
error_300 = np.mean((X_est @ w_300 + b_300 - y_arr) ** 2)
error_min = np.mean((modelo_diabetes.predict(X_est) - y_arr) ** 2)
print(f"MSE con 300 pasos: {error_300:.1f} mínimo: {error_min:.1f}"
f" ({100 * (error_300 / error_min - 1):.1f} % más)")
pesos = pd.DataFrame({"300 pasos": w_300,
"scikit-learn": modelo_diabetes.coef_},
index=X.columns)
print(pesos.loc[["bmi", "s1", "s2"]].round(2))
MSE con 300 pasos: 2867.7 mínimo: 2859.7 (0.3 % más)
300 pasos scikit-learn
bmi 24.98 24.73
s1 -15.98 -37.68
s2 5.44 22.68
El MSE ya está a menos de medio punto porcentual del mínimo y el peso de bmi está casi bien, pero los pesos de s1 y s2 siguen lejos de los de scikit-learn.
La explicación es que s1 (colesterol total) y s2 (LDL) están muy correlacionadas: subir una y bajar la otra casi no cambia las predicciones. En el valle eso es un fondo largo y casi plano, y el descenso avanza por ahí muy despacio. Moraleja: que la pérdida ya casi no baje no garantiza que cada peso haya llegado a su valor final, y los pesos de variables muy relacionadas entre sí hay que interpretarlos con cuidado.
8. Predecir fuera de los datos
Con el modelo de las horas y las notas, predice la nota de alguien que estudió 0 horas y la de alguien que estudió 12 horas. ¿Tienen sentido las dos respuestas?
Respuesta
print("Predicciones para 0 y 12 horas:",
modelo.predict([[0], [12]]).round(1))
print(f"Horas en los datos: de {horas.min():.2f} a {horas.max():.2f}")
Predicciones para 0 y 12 horas: [ 31.2 113.9]
Horas en los datos: de 0.35 a 7.80
La de 12 horas no tiene sentido: el examen es sobre 100. El modelo solo vio estudiantes de 0,35 a 7,80 horas; fuera de ese rango no sabe nada, y una recta sigue subiendo para siempre. Predecir fuera del rango de los datos se llama extrapolar. Las 0 horas también están fuera del rango, pero apenas: 0,35 horas por debajo del que menos estudió. Es una extrapolación pequeña y la predicción es razonable. Las 12 horas, en cambio, están más de 4 horas por encima del que más estudió, y ahí la recta ya inventa.
9. Leer el R²
Tres modelos de regresión, medidos con los mismos datos con que se entrenaron, dan R² = 0,95, R² = 0 y R² = −0,3. ¿Qué significa cada uno?
Respuesta
- 0,95: el modelo explica el 95 % de la variación del objetivo; sus errores son chicos comparados con cuánto varían los datos.
- 0: predecir siempre el promedio daría el mismo error. El modelo no aporta nada.
- −0,3: es peor que predecir el promedio. Con una regresión lineal bien entrenada y medida en sus propios datos de entrenamiento eso no puede pasar, así que algo falló: por ejemplo, un descenso que divergió o que paró demasiado pronto.
Un R² alto en los datos de entrenamiento todavía no dice cómo le irá al modelo con datos nuevos. Eso lo ves en el tema 7.
Mini-proyecto: el precio de las viviendas
Vas a predecir el valor de las viviendas de una zona con una sola variable y después con todas, y a comparar.
Los datos. El conjunto California Housing viene en scikit-learn y se descarga de internet la primera vez que lo pides (pesa menos de 1 MB); después queda guardado. Tiene 20 640 zonas de California según el censo de 1990 de Estados Unidos. El objetivo, MedHouseVal, es el valor mediano de las viviendas de la zona en cientos de miles de dólares (2,5 significa 250 000 dólares).
| Columna | Qué es |
|---|---|
MedInc |
Ingreso mediano de los hogares de la zona (en decenas de miles de dólares) |
HouseAge |
Antigüedad mediana de las casas, en años |
AveRooms, AveBedrms |
Promedio de habitaciones y de dormitorios por vivienda |
Population, AveOccup |
Habitantes de la zona y personas por vivienda |
Latitude, Longitude |
Ubicación de la zona |
from sklearn.datasets import fetch_california_housing
# La primera vez necesita internet; después queda guardado
casas = fetch_california_housing(as_frame=True)
X = casas.data
y = casas.target.to_numpy()
print(X.shape)
(20640, 8)
Pasos.
- Mira los datos. Grafica
MedHouseValfrente aMedInc(conplt.scatter(..., s=2, alpha=0.3), porque son muchos puntos). ¿Ves una tendencia? ¿Qué pasa arriba, en el valor 5? Cuenta cuántas zonas tienen exactamente ese valor máximo. - Una variable, desde cero. Estandariza
MedIncy entrena con tu funcióndescenso_gradientede la teoría. Elige la tasa y el número de pasos mirando la curva de pérdida. Guarda la curva como figura. - Una variable, con scikit-learn. Entrena
LinearRegressionconMedIncsin estandarizar. Para comparar con tu resultado, vuelve tus w y b a las unidades originales: si estandarizaste con media m y desviación s, la pendiente original es w/s y el intercepto es b − w·m/s. ¿Coinciden? - Todas las variables. Estandariza las 8 columnas y entrena
LinearRegression. Si te animas, entrena tambiéndescenso_gradiente_variasy compara los pesos. - Compara. Haz una tabla con RMSE, MAE y R² para el modelo de una variable y el de ocho. Dibuja los residuos frente a las predicciones del modelo de ocho variables.
- Concluye. Escribe tres conclusiones de una o dos frases cada una. Por ejemplo: ¿cuánto mejora usar todas las variables? ¿Qué variable tiene el peso más grande (en valor absoluto) en el modelo estandarizado y qué podría significar? ¿Qué forma rara ves en el gráfico de residuos y de dónde sale?
Resultados de referencia, para comprobar los tuyos
Este código hace los pasos 1 a 5 sin gráficos; tus números deberían coincidir.
from sklearn.metrics import r2_score
ingreso = X["MedInc"].to_numpy()
print("Zonas con el valor máximo:", (y == y.max()).sum(),
" valor máximo:", y.max())
# Paso 2: una variable, desde cero, con MedInc estandarizado
m, s = ingreso.mean(), ingreso.std()
w_e, b_e, _ = descenso_gradiente((ingreso - m) / s, y, tasa=0.1, pasos=500)
print(f"Descenso, estandarizado: w = {w_e:.3f} b = {b_e:.3f}")
print(f"Descenso, original: w = {w_e / s:.3f}"
f" b = {b_e - w_e * m / s:.3f}")
# Paso 3: una variable, con scikit-learn
una = LinearRegression().fit(ingreso.reshape(-1, 1), y)
print(f"scikit-learn: w = {una.coef_[0]:.3f}"
f" b = {una.intercept_:.3f}")
# Paso 4: las ocho variables, estandarizadas
X8 = ((X - X.mean()) / X.std(ddof=0)).to_numpy()
todas = LinearRegression().fit(X8, y)
print(pd.Series(todas.coef_, index=X.columns).round(2).to_string())
# Paso 5: comparar
for nombre, p in [("1 variable ", una.predict(ingreso.reshape(-1, 1))),
("8 variables", todas.predict(X8))]:
print(f"{nombre}: RMSE = {np.sqrt(mse(y, p)):.3f}"
f" MAE = {mae(y, p):.3f} R² = {r2_score(y, p):.3f}")
Zonas con el valor máximo: 965 valor máximo: 5.00001
Descenso, estandarizado: w = 0.794 b = 2.069
Descenso, original: w = 0.418 b = 0.451
scikit-learn: w = 0.418 b = 0.451
MedInc 0.83
HouseAge 0.12
AveRooms -0.27
AveBedrms 0.31
Population -0.00
AveOccup -0.04
Latitude -0.90
Longitude -0.87
1 variable : RMSE = 0.837 MAE = 0.626 R² = 0.473
8 variables: RMSE = 0.724 MAE = 0.531 R² = 0.606
- Muchas zonas tienen exactamente el valor máximo: los valores del censo se recortaron arriba («500 000 dólares o más»). Eso deja una línea recta de residuos en el gráfico, porque el modelo predice valores por encima de 5 que nunca aparecen en los datos.
- Con solo
MedInc, cada 10 000 dólares de ingreso mediano suman unos 41 800 dólares al valor de la vivienda. El descenso conMedIncestandarizado llega a lo mismo que scikit-learn, una vez que se vuelve a las unidades originales. - Con las 8 variables, el R² sube de 0,47 a 0,61. Los pesos más grandes del modelo estandarizado son los de
Latitude,LongitudeyMedInc: la ubicación importa casi tanto como el ingreso. - Todo esto está medido con los mismos datos del entrenamiento. En el tema 7 vas a repetir el mini-proyecto midiendo con datos que el modelo no vio.
Errores comunes
- Una tasa de aprendizaje demasiado grande. Si la pérdida sube, oscila o aparece
nanoinf, baja la tasa (dividiéndola por 3 o por 10) antes de buscar otro error. - Actualizar w y después usar el w nuevo para calcular el paso de b. Las dos derivadas se calculan primero, con los mismos w y b, y recién después se actualizan los dos.
- Olvidar el promedio en el gradiente. Si sumas en lugar de promediar, el gradiente crece con la cantidad de datos y una tasa que funcionaba con 30 ejemplos diverge con 3000.
- Confundir el signo. El gradiente apunta cuesta arriba: el paso es
w - tasa * dw, now + tasa * dw. Si el error sube con cualquier tasa, revisa el signo. - No estandarizar con varias variables. Si una característica va de 0 a 1 y otra de 0 a 10 000, el valle es tan alargado que ninguna tasa sirve para las dos.
- Dar a scikit-learn una lista en lugar de una tabla.
fitesperaXcon forma (ejemplos, características). Con una sola variable usax.reshape(-1, 1); si no, verás un error que habla de «2D array». - Comparar MSE de problemas distintos. El MSE está en unidades al cuadrado y depende de la escala del objetivo. Para interpretarlo, usa el RMSE (mismas unidades que y) o el R².
- Creer que un R² alto en el entrenamiento garantiza buenas predicciones. Un modelo puede ir muy bien con los datos que ya vio y mal con datos nuevos (tema 7).
- Extrapolar. Una recta no sabe nada fuera del rango de los datos con que se entrenó.
- Leer un peso como causa. Un peso grande dice que la variable ayuda a predecir en estos datos, no que la cause.
¿Lo lograste?
Al terminar este tema deberías poder hacer lo siguiente. Al lado, dónde lo practicaste.
- Definir modelo, parámetros, función de pérdida y entrenamiento (ejercicio 1).
- Calcular el MSE y el MAE, y explicar cuándo conviene cada uno (ejercicios 2 y 5).
- Programar desde cero el descenso de gradiente para una regresión lineal (ejercicio 3 y pasos 2 y 4 del mini-proyecto).
- Diagnosticar con la curva de pérdida una tasa de aprendizaje baja, adecuada o demasiado alta (ejercicios 4 y 6).
- Usar
LinearRegressionde scikit-learn y comparar sus coeficientes con los tuyos (pasos 3 y 4 del mini-proyecto).