Last active
March 27, 2026 16:24
-
-
Save robintux/8277f255f4897e3e90efd79376501f73 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # ============================================================================ | |
| # 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