Skip to content

Instantly share code, notes, and snippets.

@robintux
Last active March 27, 2026 16:24
Show Gist options
  • Select an option

  • Save robintux/8277f255f4897e3e90efd79376501f73 to your computer and use it in GitHub Desktop.

Select an option

Save robintux/8277f255f4897e3e90efd79376501f73 to your computer and use it in GitHub Desktop.
# ============================================================================
# DEMOSTRACIÓN: Aproximación Universal con Keras
# ============================================================================
import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
from tensorflow.keras.callbacks import EarlyStopping
import matplotlib.pyplot as plt
np.random.seed(42)
tf.random.set_seed(42)
def universal_approximator_demo_keras():
"""
Visualiza cómo una MLP de una capa oculta aproxima funciones target
Implementación usando Keras/TensorFlow con mejoras para enseñanza
"""
print("=" * 70)
print("Iniciando demostración de aproximación universal (Keras)")
print("=" * 70)
# ------------------------------------------------------------------------
# 1. FUNCIÓN TARGET Y GENERACIÓN DE DATOS
# ------------------------------------------------------------------------
def target_function(x):
"""
Función objetivo: combinación de sinusoides de diferente frecuencia + ruido
Diseñada para requerir diferentes niveles de capacidad de aproximación
"""
return np.sin(5*x) + 0.5*np.sin(15*x) + 0.1*np.random.randn(*x.shape)
# Generar datos de entrenamiento
x_train = np.linspace(-2, 2, 200)
y_train = target_function(x_train).astype(np.float32)
print(f"Datos generados: {len(x_train)} muestras en [-2, 2]")
print(f"Rango de y: [{y_train.min():.3f}, {y_train.max():.3f}]")
# ------------------------------------------------------------------------
# 2. CONFIGURACIÓN DE EXPERIMENTO
# ------------------------------------------------------------------------
capacities = [5, 20, 100] # Neuronas en capa oculta
epochs = 2000
learning_rate = 0.01
batch_size = 200
# Preparar datos para Keras: reshape a (n_samples, n_features)
X_train = x_train.reshape(-1, 1)
y_train_reshaped = y_train.reshape(-1, 1)
# ------------------------------------------------------------------------
# 3. FUNCIÓN AUXILIAR: CREAR Y ENTRENAR MODELO
# ------------------------------------------------------------------------
def create_and_train_model(hidden_units, X, y, epochs, lr, batch_size):
"""
Crea, compila y entrena un modelo MLP de una capa oculta
Returns:
model: Modelo Keras entrenado
history: Historial de entrenamiento para diagnóstico
"""
model = keras.Sequential([
layers.Dense(
hidden_units,
activation='tanh',
input_shape=(1,),
kernel_initializer='glorot_uniform',
name='hidden_layer'
),
layers.Dense(
1,
kernel_initializer='glorot_uniform',
name='output_layer'
)
], name=f'MLP_{hidden_units}units')
# Compilar con optimizador y pérdida
model.compile(
optimizer=keras.optimizers.Adam(learning_rate=lr),
loss='mean_squared_error',
metrics=['mae']
)
callbacks = [
EarlyStopping(
monitor='loss',
patience=100,
restore_best_weights=True,
verbose=0
)
]
# Entrenar
history = model.fit(
X, y,
epochs=epochs,
batch_size=batch_size,
callbacks=callbacks,
verbose=0
)
return model, history
# ------------------------------------------------------------------------
# 4. ENTRENAR MODELOS CON DIFERENTES CAPACIDADES
# ------------------------------------------------------------------------
results = {}
print(f"\nEntrenando {len(capacities)} modelos con diferentes capacidades...")
for units in capacities:
print(f" * Capacidad: {units:3d} neuronas ", end="")
model, history = create_and_train_model(
hidden_units=units,
X=X_train,
y=y_train_reshaped,
epochs=epochs,
lr=learning_rate,
batch_size=batch_size
)
# Métricas finales
final_loss = history.history['loss'][-1]
final_mae = history.history['mae'][-1]
results[units] = {
'model': model,
'history': history,
'final_loss': final_loss,
'final_mae': final_mae,
'epochs_trained': len(history.history['loss'])
}
print(f"-> Loss: {final_loss:.6f}, MAE: {final_mae:.4f}, Épocas: {results[units]['epochs_trained']}")
# ------------------------------------------------------------------------
# 5. EVALUACIÓN Y PREDICCIÓN EN GRID FINO
# ------------------------------------------------------------------------
# Grid para visualización suave (incluye extrapolación)
x_test = np.linspace(-2.5, 2.5, 500).astype(np.float32)
x_test_reshaped = x_test.reshape(-1, 1)
# Función verdadera sin ruido para referencia visual
y_true_smooth = np.sin(5*x_test) + 0.5*np.sin(15*x_test)
# ------------------------------------------------------------------------
# 6. VISUALIZACIÓN PRINCIPAL: APROXIMACIÓN POR CAPACIDAD
# ------------------------------------------------------------------------
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
fig.suptitle('Teorema de Aproximación Universal: Efecto de la Capacidad',
fontsize=16, fontweight='bold', y=1.02)
colors = ['#2E86AB', '#A23B72', '#F18F01']
for ax, units, color in zip(axes, capacities, colors):
model = results[units]['model']
# Predicciones en grid de evaluación
y_pred = model.predict(x_test_reshaped, verbose=0).flatten()
# Panel A: Ajuste a datos
ax.scatter(x_train, y_train, s=15, alpha=0.4, color='gray',
label='Datos (con ruido)', zorder=1)
ax.plot(x_test, y_true_smooth, 'k--', linewidth=1.5,
label='Función verdadera (sin ruido)', zorder=2)
ax.plot(x_test, y_pred, '-', color=color, linewidth=2.5,
label=f'Predicción ({units} neuronas)', zorder=3)
# Región de entrenamiento vs. extrapolación
ax.axvspan(-2, 2, alpha=0.1, color='green', label='Dominio de entrenamiento')
ax.axvline(x=-2, color='green', linestyle=':', alpha=0.5)
ax.axvline(x=2, color='green', linestyle=':', alpha=0.5)
# Métricas en el título secundario
metrics = results[units]
ax.text(0.02, 0.98, f'MSE: {metrics["final_loss"]:.4f}\nMAE: {metrics["final_mae"]:.4f}',
transform=ax.transAxes, fontsize=9, verticalalignment='top',
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.3))
ax.set_xlabel('x', fontsize=11)
ax.set_ylabel('y', fontsize=11)
ax.set_title(f'Capacidad: {units} neuronas\n({metrics["epochs_trained"]} épocas)',
fontsize=12, fontweight='bold')
ax.legend(fontsize=8, loc='lower right')
ax.grid(alpha=0.3, linestyle='--')
ax.set_xlim(-2.6, 2.6)
plt.tight_layout()
plt.show()
# ------------------------------------------------------------------------
# 7. PANEL ADICIONAL: CURVAS DE APRENDIZAJE (DIAGNÓSTICO)
# ------------------------------------------------------------------------
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
fig.suptitle('Diagnóstico de Entrenamiento: Curvas de Pérdida',
fontsize=14, fontweight='bold', y=1.02)
# Panel A: Pérdida por época (escala log para mejor visualización)
ax1 = axes[0]
for units, color in zip(capacities, colors):
loss_history = results[units]['history'].history['loss']
epochs_plot = range(len(loss_history))
ax1.semilogy(epochs_plot, loss_history, '-', color=color,
label=f'{units} neuronas', linewidth=1.5, alpha=0.9)
ax1.set_xlabel('Época', fontsize=11)
ax1.set_ylabel('Pérdida MSE (escala log)', fontsize=11)
ax1.set_title('Convergencia del Error de Entrenamiento', fontsize=12)
ax1.legend(fontsize=9)
ax1.grid(alpha=0.3, which='both')
# Panel B: Pérdida final vs. capacidad (análisis de sobreajuste potencial)
ax2 = axes[1]
final_losses = [results[u]['final_loss'] for u in capacities]
bars = ax2.bar([f'{u} neuronas' for u in capacities], final_losses,
color=colors, edgecolor='black', alpha=0.8)
# Añadir valores en las barras
for bar, units in zip(bars, capacities):
height = bar.get_height()
ax2.text(bar.get_x() + bar.get_width()/2., height,
f'{height:.4f}', ha='center', va='bottom', fontsize=10)
ax2.set_xlabel('Capacidad del Modelo', fontsize=11)
ax2.set_ylabel('Pérdida MSE Final', fontsize=11)
ax2.set_title('Error de Entrenamiento vs. Número de Parámetros', fontsize=12)
ax2.grid(alpha=0.3, axis='y')
# Nota sobre sobreajuste
ax2.text(0.5, -0.2, 'Nota: Menor error de entrenamiento ≠ mejor generalización',
transform=ax2.transAxes, fontsize=9, style='italic',
ha='center', bbox=dict(boxstyle='round', facecolor='lightyellow', alpha=0.5))
plt.tight_layout()
plt.show()
# ------------------------------------------------------------------------
# 8. RESUMEN CUANTITATIVO Y CONCLUSIONES
# ------------------------------------------------------------------------
print("\n" + "=" * 70)
print(" RESUMEN CUANTITATIVO")
print("=" * 70)
print(f"{'Capacidad':<15} {'Parámetros':<12} {'MSE Final':<15} {'MAE Final':<12} {'Épocas'}")
print("-" * 70)
for units in capacities:
# Calcular número aproximado de parámetros
n_params = 1*units + units + units*1 + 1 # weights + biases + weights + bias
metrics = results[units]
print(f"{units:3d} neuronas {n_params:4d} "
f"{metrics['final_loss']:<15.6f} {metrics['final_mae']:<12.4f} {metrics['epochs_trained']}")
print("\n🔍 OBSERVACIONES CLAVE:")
print(" 1. Mayor capacidad → menor error de entrenamiento (como predice el teorema)")
print(" 2. Con 5 neuronas: la red no captura la frecuencia alta (15*x)")
print(" 3. Con 100 neuronas: ajuste casi perfecto, pero riesgo de sobreajuste al ruido")
print(" 4. Extrapolación fuera de [-2,2]: comportamiento impredecible (limitación de MLPs)")
print("\n \t PREGUNTA PARA REFLEXIÓN:")
print(" ¿Cómo distinguirías entre 'aproximación buena' y 'memorización del ruido'?")
print(" → Pista: necesitarías un conjunto de validación SIN ruido o con ruido independiente")
return results
# ============================================================================
# EJECUCIÓN
# ============================================================================
if __name__ == "__main__":
results = universal_approximator_demo_keras()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment