Ejemplo redes

# ==============================================================================
# CONVERSIÓN FAHRENHEIT → CELSIUS CON REDES NEURONALES
#
# FÓRMULA REAL:   C = (F - 32) × 5/9   ≈   C = F × 0.5556 - 17.778
#
# OBJETIVO: que la red aprenda sola esa fórmula, SIN que nosotros se la digamos.
# Solo le damos pares de datos (F, C) y dejamos que ajuste sus pesos.
#
# MODELOS:
#   Modelo 1 — UNA sola neurona           (1 peso + 1 bias = 2 parámetros)
#   Modelo 2 — Dos capas de 4 neuronas    (más potente, más parámetros)
#
# INSTALACIÓN (una sola vez):
#   pip install tensorflow numpy matplotlib
# ==============================================================================


# ==============================================================================
# PASO 1: Importar librerías
# ==============================================================================

import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers

tf.random.set_seed(42)
np.random.seed(42)

print("=" * 60)
print("  REDES NEURONALES: Fahrenheit → Celsius")
print("=" * 60)
print(f"  TensorFlow: {tf.__version__}")


# ==============================================================================
# PASO 2: Crear el dataset — 20 pares (Fahrenheit, Celsius)
# ==============================================================================
#
# Generamos 20 temperaturas en Fahrenheit distribuidas entre -40 °F y 212 °F
# y calculamos su equivalente exacto en Celsius con la fórmula real.
#
# -40 °F es un punto especial: coincide exactamente con -40 °C.
# 212 °F = punto de ebullición del agua = 100 °C.

fahrenheit = np.array([
    -40,  -22,   -4,   14,   32,   50,   68,   77,   86,   95,
    104,  113,  122,  140,  158,  176,  185,  194,  203,  212
], dtype=float)

# Fórmula exacta: C = (F - 32) × 5 / 9
celsius = (fahrenheit - 32) * 5 / 9

print("\n" + "=" * 60)
print("PASO 2: Dataset — 20 pares Fahrenheit / Celsius")
print("=" * 60)
print(f"{'Fahrenheit':>12} │ {'Celsius':>10} │ {'Nota'}")
print("-" * 45)
notas = {-40: "F=C", 32: "Congela", 98.6: "Cuerpo", 212: "Hierve"}
for f, c in zip(fahrenheit, celsius):
    nota = notas.get(f, "")
    print(f"{f:>12.1f} │ {c:>10.4f} │ {nota}")


# ==============================================================================
# PASO 3: Normalizar los datos
# ==============================================================================
#
# Las redes neuronales aprenden mejor cuando los datos están en rangos pequeños
# (cerca de 0). Guardamos la media y desviación para desnormalizar al final.

f_media  = fahrenheit.mean()
f_std    = fahrenheit.std()
c_media  = celsius.mean()
c_std    = celsius.std()

F_norm = (fahrenheit - f_media) / f_std    # Fahrenheit normalizado
C_norm = (celsius    - c_media) / c_std    # Celsius    normalizado

print("\n" + "=" * 60)
print("PASO 3: Normalización (media=0, desv=1)")
print("=" * 60)
print(f"  Fahrenheit → media: {f_media:.2f}, desv: {f_std:.2f}")
print(f"  Celsius    → media: {c_media:.2f}, desv: {c_std:.2f}")


# ==============================================================================
# PASO 4A: MODELO 1 — Una sola neurona
# ==============================================================================
#
# Arquitectura:   [1 entrada F]  →  [1 neurona]  →  [1 salida C]
#
# Esta neurona tiene exactamente DOS parámetros:
#   w (peso / weight): multiplica a F
#   b (bias / sesgo):  suma un valor fijo
#
# La neurona calcula:   salida = w × F + b
#
# ¿Se puede aprender C = F × 0.5556 - 17.778 con una sola neurona?
# SÍ, porque la conversión es una función LINEAL (línea recta).
# Una neurona sin activación no lineal es exactamente eso: una línea.
#
# Sin activation= → la neurona es lineal (el valor por defecto es 'linear')

modelo1 = keras.Sequential([
    layers.Dense(units=1, input_shape=(1,))   # 1 neurona, sin activación no lineal
], name="Una_Neurona")

modelo1.compile(
    optimizer=keras.optimizers.Adam(learning_rate=0.05),
    loss="mean_squared_error"   # MSE: error cuadrático medio entre C_pred y C_real
)

print("\n" + "=" * 60)
print("PASO 4A: Modelo 1 — Una sola neurona")
print("=" * 60)
modelo1.summary()

hist1 = modelo1.fit(
    F_norm, C_norm,
    epochs=500,
    verbose=0          # silencioso para no llenar la pantalla
)

loss_final_m1 = hist1.history["loss"][-1]
print(f"\n  ✅ Entrenamiento completado — Loss final: {loss_final_m1:.8f}")


# ==============================================================================
# PASO 4B: MODELO 2 — Dos capas de 4 neuronas
# ==============================================================================
#
# Arquitectura:
#   [1 entrada]  →  [4 neuronas ReLU]  →  [4 neuronas ReLU]  →  [1 salida]
#
# Parámetros:
#   Capa oculta 1: (1 entrada × 4 neuronas) + 4 bias = 8
#   Capa oculta 2: (4 entradas × 4 neuronas) + 4 bias = 20
#   Capa salida:   (4 entradas × 1 neurona)  + 1 bias = 5
#   TOTAL: 33 parámetros
#
# Para un problema LINEAL como este, esta arquitectura es "excesiva",
# pero sirve perfectamente para comparar comportamiento y mostrar los pesos.
#
# ReLU: si el valor es negativo → 0; si es positivo → lo deja pasar.
# La combinación de varias ReLU puede aproximar cualquier función.

modelo2 = keras.Sequential([
    layers.Dense(units=4, activation="relu", input_shape=(1,)),  # capa oculta 1
    layers.Dense(units=4, activation="relu"),                     # capa oculta 2
    layers.Dense(units=1)                                         # capa de salida (lineal)
], name="Dos_Capas_4_Neuronas")

modelo2.compile(
    optimizer=keras.optimizers.Adam(learning_rate=0.05),
    loss="mean_squared_error"
)

print("\n" + "=" * 60)
print("PASO 4B: Modelo 2 — Dos capas de 4 neuronas")
print("=" * 60)
modelo2.summary()

hist2 = modelo2.fit(
    F_norm, C_norm,
    epochs=500,
    verbose=0
)

loss_final_m2 = hist2.history["loss"][-1]
print(f"\n  ✅ Entrenamiento completado — Loss final: {loss_final_m2:.8f}")


# ==============================================================================
# PASO 5: Evaluar el rendimiento de ambos modelos
# ==============================================================================

def evaluar_modelo(modelo, F_norm, celsius_real, f_media, f_std, c_media, c_std):
    """
    Evalúa un modelo comparando sus predicciones con los valores reales.
    Desnormaliza tanto la predicción como el valor real para mostrar °C verdaderos.
    """
    C_pred_norm = modelo.predict(F_norm, verbose=0).flatten()

    # Desnormalizamos para obtener grados Celsius reales
    C_pred_real = C_pred_norm * c_std + c_media

    errores_abs = np.abs(C_pred_real - celsius_real)
    mae  = errores_abs.mean()     # Error Absoluto Medio (MAE) en °C
    rmse = np.sqrt(((C_pred_real - celsius_real) ** 2).mean())  # Raíz del Error Cuadrático Medio

    return C_pred_real, mae, rmse

C_pred1, mae1, rmse1 = evaluar_modelo(modelo1, F_norm, celsius, f_media, f_std, c_media, c_std)
C_pred2, mae2, rmse2 = evaluar_modelo(modelo2, F_norm, celsius, f_media, f_std, c_media, c_std)

print("\n" + "=" * 60)
print("PASO 5: Rendimiento comparado")
print("=" * 60)
print(f"\n  {'Modelo':<28} {'MAE (°C)':>10} {'RMSE (°C)':>12}")
print("  " + "-" * 52)
print(f"  {'Modelo 1: Una neurona':<28} {mae1:>10.6f} {rmse1:>12.6f}")
print(f"  {'Modelo 2: Dos capas × 4':<28} {mae2:>10.6f} {rmse2:>12.6f}")
print("\n  MAE  = error promedio en grados Celsius")
print("  RMSE = error promedio (penaliza más los errores grandes)")
print("\n  Un MAE < 0.5 °C es excelente para uso práctico.")

# Tabla predicciones vs real
print("\n  Tabla de predicciones (primeras 10 muestras):")
print(f"  {'F (°F)':>8} │ {'C real':>8} │ {'M1 pred':>8} │ {'M1 err':>8} │ "
      f"{'M2 pred':>8} │ {'M2 err':>8}")
print("  " + "-" * 65)
for i in range(10):
    print(f"  {fahrenheit[i]:>8.1f} │ {celsius[i]:>8.4f} │ {C_pred1[i]:>8.4f} │ "
          f"{abs(C_pred1[i]-celsius[i]):>8.4f} │ {C_pred2[i]:>8.4f} │ "
          f"{abs(C_pred2[i]-celsius[i]):>8.4f}")


# ==============================================================================
# PASO 6: Mostrar los pesos de cada neurona
# ==============================================================================
#
# Los pesos son los NÚMEROS que la red aprendió ajustando sus parámetros.
# Para Modelo 1 (una neurona lineal), deberían aproximarse a:
#   w ≈ 1.0  (en escala normalizada)
#   b ≈ 0.0  (la fórmula C = F × 0.5556 - 17.78 se transforma en la escala norm.)
#
# Los pesos reales en escala original se pueden calcular así:
#   pendiente = w × (c_std / f_std)
#   intercepto = b × c_std + c_media - w × (c_std / f_std) × f_media

print("\n" + "=" * 60)
print("PASO 6: Pesos aprendidos por cada neurona")
print("=" * 60)

# ---- MODELO 1: UNA NEURONA ----
print("\n  ── MODELO 1: Una sola neurona ──")
pesos_m1, bias_m1 = modelo1.layers[0].get_weights()

# get_weights() devuelve una lista: [matriz_de_pesos, vector_de_bias]
# Para 1 neurona con 1 entrada: pesos shape=(1,1), bias shape=(1,)
w = pesos_m1[0][0]     # el único peso
b = bias_m1[0]         # el único bias

# Convertir de escala normalizada a escala real (°C / °F)
pendiente    = w * (c_std / f_std)
intercepto   = b * c_std + c_media - w * (c_std / f_std) * f_media

print(f"\n  Neurona 1:")
print(f"    Peso  (w) en escala norm.:  {w:>10.6f}")
print(f"    Bias  (b) en escala norm.:  {b:>10.6f}")
print(f"\n  Traducido a escala real °C = w×°F + b:")
print(f"    Pendiente aprendida:  {pendiente:>10.6f}  (real: 0.555556)")
print(f"    Intercepto aprendido: {intercepto:>10.6f}  (real: -17.777778)")
print(f"\n  Error de la pendiente:  {abs(pendiente - 0.555556):>10.8f} °C/°F")
print(f"  Error del intercepto:   {abs(intercepto - (-17.777778)):>10.8f} °C")

# ---- MODELO 2: DOS CAPAS DE 4 NEURONAS ----
print("\n\n  ── MODELO 2: Dos capas de 4 neuronas ──")

nombres_capas = ["Capa oculta 1 (ReLU)", "Capa oculta 2 (ReLU)", "Capa de salida (lineal)"]

for idx, (capa, nombre) in enumerate(zip(modelo2.layers, nombres_capas)):
    pesos, bias = capa.get_weights()
    # pesos shape: (n_entradas, n_neuronas)
    # bias  shape: (n_neuronas,)
    n_entradas, n_neuronas = pesos.shape

    print(f"\n  ┌─ {nombre}")
    print(f"  │  Entradas: {n_entradas}   Neuronas: {n_neuronas}")
    print(f"  │")

    for j in range(n_neuronas):
        print(f"  │  Neurona {j+1}:")
        for i in range(n_entradas):
            print(f"  │    w[entrada_{i+1}→neurona_{j+1}] = {pesos[i][j]:>10.6f}")
        print(f"  │    bias[neurona_{j+1}]           = {bias[j]:>10.6f}")
        if j < n_neuronas - 1:
            print(f"  │")
    print(f"  └{'─' * 50}")


# ==============================================================================
# PASO 7: Gráficos
# ==============================================================================

fig, axes = plt.subplots(2, 2, figsize=(15, 11))
fig.suptitle("Redes Neuronales: Aprender Fahrenheit → Celsius", fontsize=15, fontweight="bold")

VERDE   = "#27AE60"
NARANJA = "#E67E22"
AZUL    = "#2980B9"
ROJO    = "#E74C3C"

# ---- Gráfico 1: Curvas de pérdida ----
ax = axes[0, 0]
ax.plot(hist1.history["loss"], color=NARANJA, linewidth=2, label="Modelo 1: 1 neurona")
ax.plot(hist2.history["loss"], color=AZUL,    linewidth=2, label="Modelo 2: 2 capas × 4")
ax.set_title("Curva de pérdida (MSE) durante el entrenamiento")
ax.set_xlabel("Época")
ax.set_ylabel("MSE (escala normalizada)")
ax.legend()
ax.grid(True, alpha=0.3)
ax.set_yscale("log")   # escala logarítmica para ver mejor la convergencia

# ---- Gráfico 2: Predicciones vs real ----
ax = axes[0, 1]
ax.plot(fahrenheit, celsius,  color=ROJO,    linewidth=2.5, linestyle="--",
        label="Fórmula real", zorder=5)
ax.scatter(fahrenheit, C_pred1, color=NARANJA, s=80, marker="o",
           label="Modelo 1: 1 neurona", zorder=4)
ax.scatter(fahrenheit, C_pred2, color=AZUL,    s=60, marker="^",
           label="Modelo 2: 2 capas × 4", zorder=3)
ax.set_title("Predicciones vs valores reales")
ax.set_xlabel("Temperatura (°F)")
ax.set_ylabel("Temperatura (°C)")
ax.legend()
ax.grid(True, alpha=0.3)

# ---- Gráfico 3: Error absoluto por muestra ----
ax = axes[1, 0]
x_idx = np.arange(len(fahrenheit))
ancho = 0.35
bars1 = ax.bar(x_idx - ancho/2, np.abs(C_pred1 - celsius), ancho,
               color=NARANJA, alpha=0.85, label="Modelo 1: 1 neurona")
bars2 = ax.bar(x_idx + ancho/2, np.abs(C_pred2 - celsius), ancho,
               color=AZUL, alpha=0.85, label="Modelo 2: 2 capas × 4")
ax.set_title("Error absoluto por muestra (°C)")
ax.set_xlabel("Índice de muestra")
ax.set_ylabel("|Predicción − Real|  (°C)")
ax.set_xticks(x_idx)
ax.set_xticklabels([f"{f:.0f}" for f in fahrenheit], rotation=45, ha="right", fontsize=8)
ax.legend()
ax.grid(True, alpha=0.3, axis="y")

# ---- Gráfico 4: Visualización de pesos del Modelo 1 ----
ax = axes[1, 1]
categorias  = ["Peso (w)\n(escala norm.)", "Bias (b)\n(escala norm.)",
               "Pendiente real\n(°C/°F)", "Intercepto real\n(°C)"]
valores_aprendidos = [w,          b,          pendiente,       intercepto]
valores_reales     = [None,       None,        0.555556,       -17.777778]
colores = [NARANJA, NARANJA, VERDE, VERDE]

bars = ax.bar(categorias, valores_aprendidos, color=colores, alpha=0.8, edgecolor="white")

# Líneas horizontales con los valores teóricos (solo donde aplica)
ax.axhline(0.555556,    xmin=0.49, xmax=0.74, color=ROJO, linewidth=2.5,
           linestyle="--", label="Valor teórico")
ax.axhline(-17.777778,  xmin=0.74, xmax=1.0,  color=ROJO, linewidth=2.5,
           linestyle="--")

for bar, val in zip(bars, valores_aprendidos):
    ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.15 * np.sign(val + 0.001),
            f"{val:.4f}", ha="center", va="bottom" if val >= 0 else "top", fontsize=9, fontweight="bold")

ax.set_title("Modelo 1: Pesos aprendidos vs teóricos")
ax.set_ylabel("Valor del parámetro")
ax.axhline(0, color="black", linewidth=0.8)
ax.legend()
ax.grid(True, alpha=0.3, axis="y")

plt.tight_layout()
plt.savefig("fahrenheit_celsius_redes.png", dpi=150, bbox_inches="tight")
plt.show()


# ==============================================================================
# RESUMEN FINAL
# ==============================================================================

print("\n" + "=" * 60)
print("RESUMEN FINAL")
print("=" * 60)
print(f"\n  Fórmula real:       C = F × 0.555556 − 17.777778")
print(f"  Fórmula aprendida:  C = F × {pendiente:.6f} − {abs(intercepto):.6f}")
print(f"\n  {'Modelo':<28} {'Parámetros':>12} {'MAE (°C)':>10}")
print("  " + "-" * 52)
print(f"  {'Modelo 1: Una neurona':<28} {'2':>12} {mae1:>10.6f}")
print(f"  {'Modelo 2: Dos capas × 4':<28} {'33':>12} {mae2:>10.6f}")
print(f"\n  ✅ Código completado. Gráfico guardado: fahrenheit_celsius_redes.png")

Publicado por

Juan Pablo Fuentes

Formador de programación y bases de datos