# ==============================================================================
# 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")