Regresión Lineal con un Único Perceptrón
Ejemplo de regresión lineal con un único perceptrón: predicción de ventas de helados a partir de la temperatura, con descenso del gradiente manual y comparación con sklearn.
🍦 Regresión Lineal con un Único Perceptrón
Submódulo: El Perceptrón · Fundamentos de Deep Learning
En este ejemplo veremos cómo un único perceptrón con activación lineal puede resolver un problema de regresión simple. Esto es exactamente lo que hace una regresión lineal, pero implementada como una neurona artificial que aprende mediante descenso del gradiente.
El problema: predecir las ventas de helados a partir de la temperatura máxima diaria. Un único input, un único output — la unidad más simple posible de una red neuronal.
Modelo: $\hat{y} = w \cdot x + b$ — un perceptrón con activación lineal (identidad).
1. Los datos
Disponemos de 13 observaciones reales que relacionan temperatura (°C) con helados vendidos. Es un dataset pequeño e ideal para visualizar el proceso de aprendizaje.
| Temperatura (°C) | Helados vendidos |
|---|---|
| 23.4 | 44 |
| 25.0 | 94 |
| 27.2 | 54 |
| ... | ... |
| 38.0 | 284 |
import numpy as np
import matplotlib.pyplot as plt
# ── Datos: temperatura → helados vendidos ────────────────
temperaturas = np.array([23.4, 25.0, 27.2, 29.0, 30.5, 31.0,
32.0, 33.1, 34.0, 35.2, 36.0, 37.5, 38.0])
helados = np.array([44, 94, 54, 106, 93, 183,
147, 205, 187, 158, 243, 232, 284])
plt.figure(figsize=(8, 5))
plt.scatter(temperaturas, helados, color="#6C5CE7", s=60, zorder=3)
plt.xlabel("Temperatura (°C)")
plt.ylabel("Helados vendidos")
plt.title("Datos originales")
plt.grid(alpha=0.3)
plt.show()
Text(0, 0.5, 'Helados vendidos')
2. Normalización de los datos
Antes de entrenar, normalizamos los datos al rango $[0, 1]$ con min-max scaling:
$$x_{norm} = \frac{x - x_{min}}{x_{max} - x_{min}}$$
Esto es importante porque el descenso del gradiente converge mejor cuando las variables tienen escalas similares.
# ── Normalización min-max ─────────────────────────────────
def normalize(data):
return (data - data.min()) / (data.max() - data.min())
x = normalize(temperaturas)
y = normalize(helados)
plt.figure(figsize=(8, 5))
plt.scatter(x, y, color="#6C5CE7", s=60, zorder=3)
plt.xlabel("Temperatura (normalizada)")
plt.ylabel("Helados (normalizado)")
plt.title("Datos normalizados al rango [0, 1]")
plt.grid(alpha=0.3)
plt.show()
Text(0, 0.5, 'Helados vendidos')
3. El perceptrón como modelo de regresión
Nuestro perceptrón tiene:
- 1 peso ($w$): la pendiente de la recta
- 1 bias ($b$): la ordenada en el origen
- Activación lineal (identidad): $f(z) = z$
La predicción es simplemente: $\hat{y} = w \cdot x + b$
La función de pérdida es el Error Cuadrático Medio (MSE): $$L = \frac{1}{n}\sum_{i=1}^{n}(y_i - \hat{y}_i)^2$$
# ── Parámetros del perceptrón ──────────────────────────────
np.random.seed(42)
w = np.random.rand() # Peso (pendiente)
b = np.random.rand() # Bias (ordenada)
learning_rate = 0.1
epochs = 1000
def mse_loss(y_true, y_pred):
"""Error cuadrático medio."""
return np.mean((y_true - y_pred) ** 2)
print(f"Parámetros iniciales: w = {w:.4f}, b = {b:.4f}")
print(f"Learning rate: {learning_rate}, Epochs: {epochs}")
4. Entrenamiento con descenso del gradiente
En cada epoch:
- Forward pass: calculamos $\hat{y} = w \cdot x + b$
- Calculamos la pérdida: $L = \frac{1}{n}\sum(y - \hat{y})^2$
- Calculamos los gradientes:
- $\frac{\partial L}{\partial w} = -\frac{2}{n}\sum(y - \hat{y}) \cdot x$
- $\frac{\partial L}{\partial b} = -\frac{2}{n}\sum(y - \hat{y})$
- Actualizamos: $w \leftarrow w - \alpha \cdot \frac{\partial L}{\partial w}$, $b \leftarrow b - \alpha \cdot \frac{\partial L}{\partial b}$
# ── Descenso del gradiente ────────────────────────────────
losses = []
for epoch in range(epochs):
# Forward pass
y_pred = w * x + b
# Calcular pérdida
loss = mse_loss(y, y_pred)
losses.append(loss)
# Calcular gradientes (derivadas parciales)
error = y - y_pred
dw = -2 * np.mean(error * x) # ∂L/∂w
db = -2 * np.mean(error) # ∂L/∂b
# Actualizar parámetros
w -= learning_rate * dw
b -= learning_rate * db
print(f"Entrenamiento completado")
print(f"w = {w:.4f}, b = {b:.4f}")
print(f"Loss final: {losses[-1]:.6f}")
5. Resultados
5.1 Ajuste sobre datos normalizados
# ── Recta ajustada sobre datos normalizados ──────────────
plt.figure(figsize=(8, 5))
plt.scatter(x, y, color="#6C5CE7", s=60, zorder=3, label="Datos")
x_line = np.linspace(0, 1, 100)
plt.plot(x_line, w * x_line + b, color="#E17055", lw=2, label=f"ŷ = {w:.3f}·x + {b:.3f}")
plt.xlabel("Temperatura (normalizada)")
plt.ylabel("Helados (normalizado)")
plt.title("Perceptrón ajustado — datos normalizados")
plt.legend()
plt.grid(alpha=0.3)
plt.show()
[<matplotlib.lines.Line2D at 0x196c55024d0>]
5.2 Evolución de la pérdida
# ── Curva de pérdida ──────────────────────────────────────
plt.figure(figsize=(10, 4))
plt.plot(losses, color="#6C5CE7", lw=2)
plt.xlabel("Epoch")
plt.ylabel("MSE Loss")
plt.title("Evolución de la pérdida durante el entrenamiento")
plt.grid(alpha=0.3)
plt.show()
5.3 Modelo en escala original
Para interpretar el modelo en unidades reales, debemos desnormalizar los parámetros:
# ── Desnormalización de parámetros ────────────────────────
w_real = w * (helados.max() - helados.min()) / (temperaturas.max() - temperaturas.min())
b_real = helados.min() + (helados.max() - helados.min()) * (b - w * temperaturas.min() / (temperaturas.max() - temperaturas.min()))
print(f"Modelo en escala original:")
print(f" Helados = {w_real:.2f} × Temperatura + {b_real:.2f}")
print(f" → Por cada °C más, se venden ~{w_real:.1f} helados más")
# ── Visualización en escala original ──────────────────────
plt.figure(figsize=(10, 6))
plt.scatter(temperaturas, helados, color="#6C5CE7", s=60, zorder=3, label="Datos reales")
t_line = np.linspace(temperaturas.min() - 1, temperaturas.max() + 1, 100)
plt.plot(t_line, w_real * t_line + b_real, color="#E17055", lw=2.5, label="Perceptrón (gradiente)")
plt.xlabel("Temperatura máxima (°C)")
plt.ylabel("Helados vendidos")
plt.title("Predicción de ventas de helados — Perceptrón con activación lineal")
plt.legend(fontsize=11)
plt.grid(alpha=0.3)
plt.show()
6. Comparación con la solución analítica (sklearn)
La regresión lineal también se puede resolver de forma analítica (ecuaciones normales). Comparemos el resultado de nuestro perceptrón con el de sklearn.linear_model.LinearRegression:
from sklearn.linear_model import LinearRegression
# Solución analítica
reg = LinearRegression()
reg.fit(temperaturas.reshape(-1, 1), helados)
print(f"Solución analítica (sklearn): w = {reg.coef_[0]:.4f}, b = {reg.intercept_:.4f}")
print(f"Solución por gradiente: w = {w_real:.4f}, b = {b_real:.4f}")
print(f"\nDiferencia en w: {abs(reg.coef_[0] - w_real):.4f}")
# ── Comparación visual ─────────────────────────────────────
plt.figure(figsize=(10, 6))
plt.scatter(temperaturas, helados, color="#6C5CE7", s=60, zorder=3, label="Datos reales")
plt.plot(t_line, w_real * t_line + b_real, "--", color="#E17055", lw=2.5, label="Perceptrón (descenso gradiente)")
plt.plot(t_line, reg.predict(t_line.reshape(-1, 1)), color="#00B894", lw=2.5, label="Regresión lineal (sklearn)")
plt.xlabel("Temperatura máxima (°C)")
plt.ylabel("Helados vendidos")
plt.title("Comparación: Perceptrón vs. Solución Analítica")
plt.legend(fontsize=11)
plt.grid(alpha=0.3)
plt.show()
Pendiente (w) - scikit-learn: 16.841026481921787 Intersección (b) - scikit-learn: -353.9702827303057
7. Conclusiones
- Un perceptrón con activación lineal es equivalente a una regresión lineal.
- El descenso del gradiente converge a la misma solución que las ecuaciones normales (solución analítica).
- La ventaja del gradiente: escala a problemas más complejos (múltiples capas, activaciones no lineales) donde no existe solución analítica.
- Este perceptrón es la unidad fundamental que, apilada en capas con activaciones no lineales, forma las redes neuronales profundas.
💡 Concepto clave: cuando añadimos una activación no lineal (ReLU, sigmoid...) al perceptrón, dejamos de hacer regresión lineal y podemos capturar patrones complejos. Esa es la motivación del MLP.