Spaces:
Sleeping
Sleeping
| """ | |
| Dashboard de Análisis de Activaciones de CNN | |
| Aplicación Streamlit para visualizar activaciones de ResNet18 y AlexNet | |
| """ | |
| import os | |
| import streamlit as st | |
| import torch | |
| import torch.nn as nn | |
| from torchvision import models | |
| from PIL import Image | |
| import numpy as np | |
| import matplotlib.pyplot as plt | |
| from utils_streamlit import ( | |
| StreamlitImageAnalyzer, | |
| create_activation_heatmap, | |
| create_filter_grid, | |
| get_available_conv_layers, | |
| fig_to_image | |
| ) | |
| # Configuración de la página | |
| st.set_page_config( | |
| page_title="CNN Activation Analyzer", | |
| page_icon="🔬", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| os.environ['STREAMLIT_SERVER_HEADLESS'] = 'true' | |
| # Título principal | |
| st.title("🔬 Analizador de Activaciones de CNN") | |
| st.markdown(""" | |
| Esta aplicación permite analizar las activaciones internas de redes neuronales convolucionales | |
| (ResNet18 y AlexNet) para entender qué patrones detecta cada capa. | |
| """) | |
| # Configuración del dispositivo | |
| def get_device(): | |
| """Determina el dispositivo disponible (GPU o CPU).""" | |
| return torch.device('cuda' if torch.cuda.is_available() else 'cpu') | |
| device = get_device() | |
| # Cargar modelo | |
| def load_model(model_name: str): | |
| """ | |
| Carga el modelo pre-entrenado. | |
| Args: | |
| model_name: 'resnet18' o 'alexnet' | |
| Returns: | |
| Modelo PyTorch | |
| """ | |
| if model_name == 'alexnet': | |
| model = models.alexnet(pretrained=True) | |
| elif model_name == 'resnet18': | |
| model = models.resnet18(pretrained=True) | |
| else: | |
| raise ValueError(f"Modelo no soportado: {model_name}") | |
| model.eval() | |
| return model | |
| # Sidebar - Configuración | |
| st.sidebar.header("⚙️ Configuración") | |
| # Selección de modelo | |
| model_name = st.sidebar.selectbox( | |
| "Selecciona el modelo", | |
| options=['alexnet', 'resnet18'], | |
| index=0, | |
| help="Elige entre ResNet18 (más moderno) o AlexNet (clásico)" | |
| ) | |
| # Cargar modelo seleccionado | |
| with st.spinner(f"Cargando modelo {model_name}..."): | |
| model = load_model(model_name) | |
| st.sidebar.success(f"✅ Modelo {model_name} cargado") | |
| st.sidebar.caption(f"🖥️ Dispositivo: {device}") | |
| # Obtener capas disponibles | |
| conv_layers = get_available_conv_layers(model, model_name) | |
| # Selección de capa | |
| st.sidebar.subheader("🎯 Capa a Analizar") | |
| selected_layer = st.sidebar.selectbox( | |
| "Selecciona una capa convolucional", | |
| options=conv_layers, | |
| index=0, | |
| help="Capas más tempranas detectan patrones simples, capas profundas detectan conceptos complejos" | |
| ) | |
| # Parámetros de visualización | |
| # Parámetros de visualización | |
| st.sidebar.subheader("📊 Parámetros de Visualización") | |
| # Detectar número de filtros en la capa seleccionada | |
| def get_layer_num_filters(_model, layer_name, _device): | |
| """Obtiene el número de filtros en una capa específica.""" | |
| dummy_input = torch.randn(1, 3, 224, 224).to(_device) | |
| num_filters_found = [0] | |
| def hook_fn(module, input, output): | |
| num_filters_found[0] = output.shape[1] | |
| # Registrar hook temporal | |
| hook_handle = None | |
| for name, module in _model.named_modules(): | |
| if name == layer_name: | |
| hook_handle = module.register_forward_hook(hook_fn) | |
| break | |
| # Forward pass para obtener dimensiones | |
| with torch.no_grad(): | |
| _ = _model(dummy_input) | |
| # Limpiar hook | |
| if hook_handle: | |
| hook_handle.remove() | |
| return num_filters_found[0] | |
| # Obtener número de filtros | |
| num_filters_in_layer = get_layer_num_filters(model, selected_layer, device) | |
| # Limitar slider al número real de filtros (máximo 24 para no saturar la UI) | |
| max_selectable = min(num_filters_in_layer, 24) | |
| top_k = st.sidebar.slider( | |
| f"Número de neuronas más activas", | |
| min_value=4, | |
| max_value=max_selectable, | |
| value=min(12, max_selectable), | |
| step=4, | |
| help=f"Capa '{selected_layer}' tiene {num_filters_in_layer} filtros totales" | |
| ) | |
| criterion = st.sidebar.selectbox( | |
| "Criterio de selección", | |
| options=['balanced', 'mean', 'max', 'std'], | |
| index=0, | |
| help=( | |
| "balanced: Neuronas activas Y selectivas (RECOMENDADO) - " | |
| "evita filtros que se activan en todo (como fondos)\n" | |
| "mean: Solo por activación promedio\n" | |
| "max: Solo por activación máxima\n" | |
| "std: Solo por variabilidad" | |
| ) | |
| ) | |
| # Parámetros adicionales para balanced | |
| if criterion == 'balanced': | |
| st.sidebar.markdown("**Ajustes de Balance:**") | |
| activation_weight = st.sidebar.slider( | |
| "Peso Activación vs Selectividad", | |
| min_value=0.0, | |
| max_value=1.0, | |
| value=0.5, | |
| step=0.1, | |
| help=( | |
| "1.0 = Solo activación (puede incluir fondos)\n" | |
| "0.5 = Balance 50/50 (RECOMENDADO)\n" | |
| "0.0 = Solo selectividad (filtros muy específicos)" | |
| ) | |
| ) | |
| min_sparsity = st.sidebar.slider( | |
| "Sparsity Mínima Requerida", | |
| min_value=0.0, | |
| max_value=0.8, # ← Aumentar rango | |
| value=0.5, # ← Empezar en 0 por defecto | |
| step=0.05, | |
| help=( | |
| "Filtra neuronas con sparsity menor a este valor.\n\n" | |
| "**Recomendaciones por capa:**\n" | |
| "• Capas tempranas (conv1, layer1): 0.05-0.10\n" | |
| "• Capas medias (layer2, layer3): 0.10-0.20\n" | |
| "• Capas profundas (layer4): 0.20-0.40\n\n" | |
| "**Valores comunes:**\n" | |
| "• 0.0 = Sin filtro (incluye fondos)\n" | |
| "• 0.15 = Filtrar fondos uniformes (RECOMENDADO)\n" | |
| "• 0.30 = Solo neuronas muy selectivas" | |
| ) | |
| ) | |
| if min_sparsity == 0.0: | |
| st.sidebar.warning( | |
| "⚠️ Sin filtro de sparsity: puede incluir neuronas de fondo") | |
| else: | |
| activation_weight = 0.6 | |
| min_sparsity = 0.0 | |
| alpha = st.sidebar.slider( | |
| "Transparencia del heatmap", | |
| min_value=0.0, | |
| max_value=1.0, | |
| value=0.5, | |
| step=0.1 | |
| ) | |
| cmap = st.sidebar.selectbox( | |
| "Colormap", | |
| options=['jet', 'viridis', 'hot', 'cool', 'plasma'], | |
| index=0 | |
| ) | |
| # ======================================================================== | |
| # DETECCIÓN DE CAMBIOS EN SIDEBAR | |
| # ======================================================================== | |
| # Detectar si hubo cambios en los parámetros del sidebar | |
| # Inicializar valores previos si no existen | |
| if 'prev_model' not in st.session_state: | |
| st.session_state['prev_model'] = model_name | |
| st.session_state['prev_layer'] = selected_layer | |
| st.session_state['prev_top_k'] = top_k | |
| st.session_state['prev_criterion'] = criterion | |
| st.session_state['prev_activation_weight'] = activation_weight | |
| st.session_state['prev_min_sparsity'] = min_sparsity | |
| st.session_state['prev_alpha'] = alpha | |
| st.session_state['prev_cmap'] = cmap | |
| # Verificar si hubo cambios | |
| params_changed = ( | |
| st.session_state['prev_model'] != model_name or | |
| st.session_state['prev_layer'] != selected_layer or | |
| st.session_state['prev_top_k'] != top_k or | |
| st.session_state['prev_criterion'] != criterion or | |
| st.session_state['prev_activation_weight'] != activation_weight or | |
| st.session_state['prev_min_sparsity'] != min_sparsity or | |
| st.session_state['prev_alpha'] != alpha or | |
| st.session_state['prev_cmap'] != cmap | |
| ) | |
| # Si hubo cambios, hacer scroll al inicio | |
| if params_changed: | |
| # LIMPIAR RESULTADOS VIEJOS | |
| if 'results' in st.session_state: | |
| del st.session_state['results'] | |
| # Agregar timestamp único para forzar ejecución del script | |
| import time | |
| timestamp = int(time.time() * 1000) | |
| st.markdown(f""" | |
| <script id="scroll-script-{timestamp}"> | |
| window.parent.document.querySelector('section.main').scrollTo(0, 0); | |
| </script> | |
| """, unsafe_allow_html=True) | |
| # Actualizar valores previos | |
| st.session_state['prev_model'] = model_name | |
| st.session_state['prev_layer'] = selected_layer | |
| st.session_state['prev_top_k'] = top_k | |
| st.session_state['prev_criterion'] = criterion | |
| st.session_state['prev_activation_weight'] = activation_weight | |
| st.session_state['prev_min_sparsity'] = min_sparsity | |
| st.session_state['prev_alpha'] = alpha | |
| st.session_state['prev_cmap'] = cmap | |
| # Sección principal - Carga de imagen | |
| st.header("📸 Carga de Imagen") | |
| # Si ya hay imagen, mostrarla con botón para cambiar | |
| if 'current_image' in st.session_state: | |
| col_img, col_btn = st.columns([3, 1]) | |
| with col_img: | |
| st.image(st.session_state['current_image'], | |
| caption="Imagen a analizar", | |
| width=400) | |
| with col_btn: | |
| if st.button("🔄 Cambiar imagen", use_container_width=True): | |
| st.session_state['show_uploader'] = True | |
| st.rerun() | |
| # Mostrar uploader si no hay imagen o si se presionó "Cambiar" | |
| if 'current_image' not in st.session_state or st.session_state.get('show_uploader', False): | |
| col1, col2 = st.columns([1, 1]) | |
| with col1: | |
| image_option = st.radio( | |
| "Selecciona una opción:", | |
| options=["Usar imagen de ejemplo", "Subir mi propia imagen"], | |
| index=0 | |
| ) | |
| with col2: | |
| if image_option == "Usar imagen de ejemplo": | |
| example_url = st.text_input( | |
| "URL de imagen de ejemplo", | |
| value="https://images.unsplash.com/photo-1574158622682-e40e69881006?w=400", | |
| help="Ingresa una URL de imagen o usa la predeterminada" | |
| ) | |
| if st.button("🔄 Cargar imagen de ejemplo"): | |
| try: | |
| from urllib.request import urlopen | |
| pil_image = Image.open(urlopen(example_url)) | |
| st.session_state['current_image'] = pil_image | |
| st.session_state['show_uploader'] = False | |
| st.success("✅ Imagen cargada correctamente") | |
| st.rerun() | |
| except Exception as e: | |
| st.error(f"❌ Error al cargar imagen: {str(e)}") | |
| else: | |
| uploaded_file = st.file_uploader( | |
| "Sube una imagen", | |
| type=['jpg', 'jpeg', 'png'], | |
| help="Formatos soportados: JPG, JPEG, PNG" | |
| ) | |
| if uploaded_file is not None: | |
| pil_image = Image.open(uploaded_file) | |
| st.session_state['current_image'] = pil_image | |
| st.session_state['show_uploader'] = False | |
| st.success("✅ Imagen subida correctamente") | |
| st.rerun() | |
| # Mostrar imagen actual | |
| if 'current_image' in st.session_state: | |
| # st.subheader("🖼️ Imagen Original") | |
| # st.image(st.session_state['current_image'], | |
| # caption="Imagen a analizar", width=400) | |
| # Botón de análisis | |
| if st.button("🚀 Analizar Activaciones", type="primary"): | |
| # ═══════════════════════════════════════════════════════════════ | |
| # LIMPIAR RESULTADOS ANTERIORES DE TAB 6 | |
| # ═══════════════════════════════════════════════════════════════ | |
| if 'ablation_results' in st.session_state: | |
| del st.session_state['ablation_results'] | |
| if 'ablation_neurons' in st.session_state: | |
| del st.session_state['ablation_neurons'] | |
| if 'ablation_amp_factor' in st.session_state: | |
| del st.session_state['ablation_amp_factor'] | |
| if 'ablation_noise_level' in st.session_state: | |
| del st.session_state['ablation_noise_level'] | |
| # Crear columnas para imagen + progreso | |
| col_img_prog, col_status_prog = st.columns([1, 1]) | |
| with st.spinner("Analizando imagen... Esto puede tomar unos segundos..."): | |
| try: | |
| # Crear analizador | |
| analyzer = StreamlitImageAnalyzer( | |
| model=model, | |
| target_layer=selected_layer, | |
| device=device | |
| ) | |
| # Procesar imagen | |
| img_tensor, img_vis = analyzer.load_image_from_pil( | |
| st.session_state['current_image'] | |
| ) | |
| # Analizar | |
| results = analyzer.analyze_image(img_tensor) | |
| activations = results['activations'] | |
| # Calcular estadísticas | |
| stats = analyzer.get_neuron_statistics(activations) | |
| # Debug: mostrar distribución de sparsity | |
| if criterion == 'balanced' and min_sparsity > 0: | |
| sparsity_values = [s['sparsity'] for s in stats] | |
| num_above_threshold = sum( | |
| 1 for s in sparsity_values if s >= min_sparsity) | |
| max_sparsity = max(sparsity_values) | |
| st.info(f"🔍 **Filtrado de Neuronas**: De {len(stats)} totales, " | |
| f"**{num_above_threshold}** tienen sparsity ≥ {min_sparsity:.0%}. " | |
| f"| Sparsity promedio: {np.mean(sparsity_values):.1%} " | |
| f"| Máxima: {max_sparsity:.1%}") | |
| top_neurons = analyzer.get_top_neurons( | |
| stats, | |
| top_k=top_k, | |
| criterion=criterion, | |
| activation_weight=activation_weight, | |
| min_sparsity=min_sparsity | |
| ) | |
| # Guardar resultados en session_state | |
| st.session_state['results'] = { | |
| 'activations': activations, | |
| 'prediction': results['prediction'], | |
| 'confidence': results['confidence'], | |
| 'stats': stats, | |
| 'top_neurons': top_neurons, | |
| 'img_vis': img_vis, | |
| 'layer_name': selected_layer, | |
| 'image_tensor': img_tensor | |
| } | |
| # Cleanup | |
| analyzer.cleanup() | |
| st.success("✅ Análisis completado!") | |
| # Scroll al inicio de la página | |
| st.markdown(""" | |
| <script> | |
| window.parent.document.querySelector('section.main').scrollTo(0, 0); | |
| </script> | |
| """, unsafe_allow_html=True) | |
| st.rerun() | |
| except Exception as e: | |
| st.error(f"❌ Error durante el análisis: {str(e)}") | |
| st.exception(e) | |
| # Mostrar resultados si existen | |
| if 'results' in st.session_state: | |
| results = st.session_state['results'] | |
| # Definir activations para usarlo en todos los tabs | |
| activations = results['activations'] | |
| st.markdown("---") | |
| st.header("📊 Resultados del Análisis") | |
| # Crear tabs para organizar resultados | |
| tab0, tab1, tab2, tab3, tab4, tab5, tab6 = st.tabs([ | |
| "📊 Resultados Generales", | |
| "🔥 Heatmaps Superpuestos", | |
| "🎨 Grid de Filtros", | |
| "🔬 Análisis Detallado", | |
| "🎯 Visualización de Filtros", | |
| "🤖 Predicción del Modelo", | |
| "🧪 Experimentos de Ablación" | |
| ]) | |
| # =================================================================== | |
| # TAB 0: RESULTADOS GENERALES | |
| # =================================================================== | |
| with tab0: | |
| st.info("💡 Vista general del análisis: información de la capa, predicción del modelo, y top neuronas más activas.") | |
| # Sección 1: Información General | |
| st.subheader("1️⃣ Información General") | |
| col1, col2, col3, col4 = st.columns(4) | |
| with col1: | |
| st.metric("Capa Analizada", results['layer_name']) | |
| with col2: | |
| st.metric("Clase Predicha", f"#{results['prediction']}") | |
| with col3: | |
| st.metric("Confianza", f"{results['confidence']:.2%}") | |
| with col4: | |
| shape = activations.shape | |
| st.metric("Neuronas Totales", shape[1]) | |
| st.markdown("---") | |
| # Sección 2: Estadísticas Globales | |
| st.subheader("2️⃣ Estadísticas Globales de Activaciones") | |
| col1, col2, col3, col4 = st.columns(4) | |
| with col1: | |
| st.metric("Media", f"{activations.mean().item():.4f}") | |
| with col2: | |
| st.metric("Máximo", f"{activations.max().item():.4f}") | |
| with col3: | |
| st.metric("Desv. Estándar", f"{activations.std().item():.4f}") | |
| with col4: | |
| sparsity = (activations == 0).float().mean().item() * 100 | |
| st.metric("Sparsity", f"{sparsity:.1f}%") | |
| # Información adicional de la capa | |
| with st.expander("📈 Ver información detallada de la capa"): | |
| st.write(f"**Shape completo:** {activations.shape}") | |
| st.write(f"**Batch size:** {activations.shape[0]}") | |
| st.write(f"**Número de neuronas:** {activations.shape[1]}") | |
| st.write(f"**Alto del mapa:** {activations.shape[2]} píxeles") | |
| st.write(f"**Ancho del mapa:** {activations.shape[3]} píxeles") | |
| total_values = activations.numel() | |
| st.write(f"**Total de valores:** {total_values:,}") | |
| st.write( | |
| f"**Memoria aprox:** {total_values * 4 / 1024 / 1024:.2f} MB") | |
| # Distribución de valores | |
| num_positive = (activations > 0).sum().item() | |
| num_zero = (activations == 0).sum().item() | |
| num_negative = (activations < 0).sum().item() | |
| st.write("**Distribución de valores:**") | |
| st.write( | |
| f"- Positivos: {num_positive:,} ({num_positive/total_values*100:.1f}%)") | |
| st.write( | |
| f"- Ceros: {num_zero:,} ({num_zero/total_values*100:.1f}%)") | |
| st.write( | |
| f"- Negativos: {num_negative:,} ({num_negative/total_values*100:.1f}%)") | |
| st.markdown("---") | |
| # Sección 3: Top Neuronas | |
| st.subheader( | |
| f"3️⃣ Top {len(results['top_neurons'])} Neuronas Más Activas") | |
| st.caption(f"Criterio de selección: {criterion}") | |
| # Crear tabla de top neuronas | |
| import pandas as pd | |
| top_data = [] | |
| for rank, neuron_idx in enumerate(results['top_neurons'], 1): | |
| s = results['stats'][neuron_idx] | |
| top_data.append({ | |
| 'Rank': rank, | |
| 'Neurona': neuron_idx, | |
| 'Media': f"{s['mean']:.4f}", | |
| 'Máxima': f"{s['max']:.4f}", | |
| 'Std': f"{s['std']:.4f}", | |
| 'Sparsity': f"{s['sparsity']*100:.1f}%" | |
| }) | |
| df = pd.DataFrame(top_data) | |
| st.dataframe(df, use_container_width=True, hide_index=True) | |
| st.info( | |
| "💡 Usa los otros tabs para explorar visualizaciones detalladas de estas neuronas.") | |
| with tab1: | |
| st.write("**Visualización de las neuronas más interesantes**") | |
| # Toggle para mostrar/ocultar imagen de fondo | |
| col_toggle1, col_toggle2 = st.columns([3, 1]) | |
| with col_toggle1: | |
| st.info( | |
| f"💡 Las activaciones fuertes (rojo/amarillo) indican dónde el filtro detectó patrones relevantes.") | |
| with col_toggle2: | |
| show_background = st.checkbox( | |
| "🖼️ Mostrar imagen", | |
| value=True, | |
| help="Activa/desactiva la imagen de fondo en los heatmaps" | |
| ) | |
| # Mostrar los top 5 más interesantes por defecto | |
| num_heatmaps_to_show = min(top_k, len(results['top_neurons'])) | |
| # Crear grid de heatmaps (2 columnas) | |
| cols_per_row = 3 | |
| num_rows = (num_heatmaps_to_show + cols_per_row - 1) // cols_per_row | |
| for row_idx in range(num_rows): | |
| cols = st.columns(cols_per_row) | |
| for col_idx in range(cols_per_row): | |
| neuron_list_idx = row_idx * cols_per_row + col_idx | |
| if neuron_list_idx < num_heatmaps_to_show: | |
| neuron_idx = results['top_neurons'][neuron_list_idx] | |
| neuron_stats = results['stats'][neuron_idx] | |
| with cols[col_idx]: | |
| # Crear heatmap | |
| act_map = activations[0, | |
| neuron_idx, :, :].cpu().numpy() | |
| if show_background: | |
| # Con imagen de fondo | |
| fig = create_activation_heatmap( | |
| image_vis=results['img_vis'], | |
| activation_map=act_map, | |
| title=f"#{neuron_list_idx + 1}: Filtro {neuron_idx}", | |
| alpha=alpha, | |
| cmap=cmap, | |
| figsize=(3, 3) | |
| ) | |
| else: | |
| # Solo mapa de calor (sin imagen de fondo) | |
| fig, ax = plt.subplots(figsize=(3, 3)) | |
| # Redimensionar mapa de activación | |
| from scipy.ndimage import zoom | |
| h, w = results['img_vis'].shape[:2] | |
| h_act, w_act = act_map.shape | |
| if (h_act, w_act) != (h, w): | |
| zoom_factors = (h / h_act, w / w_act) | |
| act_resized = zoom( | |
| act_map, zoom_factors, order=1) | |
| else: | |
| act_resized = act_map | |
| # Mostrar solo el mapa de calor | |
| im = ax.imshow(act_resized, cmap=cmap) | |
| ax.set_title(f"#{neuron_list_idx + 1}: Filtro {neuron_idx}", | |
| fontsize=14, fontweight='bold') | |
| ax.axis('off') | |
| plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04) | |
| plt.tight_layout() | |
| st.pyplot(fig) | |
| plt.close(fig) | |
| # Mostrar estadísticas debajo | |
| st.caption(f"**Media:** {neuron_stats['mean']:.3f} | " | |
| f"**Máx:** {neuron_stats['max']:.3f} | " | |
| f"**Std:** {neuron_stats['std']:.3f}") | |
| # Separador | |
| st.markdown("---") | |
| # Opción para ver una neurona específica con más detalle | |
| with st.expander("🔍 Ver neurona específica en detalle"): | |
| selected_neuron_idx = st.selectbox( | |
| "Selecciona una neurona", | |
| options=results['top_neurons'], | |
| index=0, | |
| format_func=lambda x: f"Neurona {x} (Rank {results['top_neurons'].index(x) + 1})" | |
| ) | |
| # Toggle individual para el detalle | |
| show_bg_detail = st.checkbox( | |
| "🖼️ Mostrar imagen de fondo", | |
| value=True, | |
| key="detail_bg" | |
| ) | |
| # Crear heatmap grande | |
| act_map = activations[0, selected_neuron_idx, :, :].cpu().numpy() | |
| if show_bg_detail: | |
| # Con imagen de fondo | |
| fig = create_activation_heatmap( | |
| image_vis=results['img_vis'], | |
| activation_map=act_map, | |
| title=f"Mapa de Activación - Neurona {selected_neuron_idx}", | |
| alpha=alpha, | |
| cmap=cmap, | |
| figsize=(4, 4) | |
| ) | |
| else: | |
| # Solo mapa de calor | |
| fig, ax = plt.subplots(figsize=(4, 4)) | |
| from scipy.ndimage import zoom | |
| h, w = results['img_vis'].shape[:2] | |
| h_act, w_act = act_map.shape | |
| if (h_act, w_act) != (h, w): | |
| zoom_factors = (h / h_act, w / w_act) | |
| act_resized = zoom(act_map, zoom_factors, order=1) | |
| else: | |
| act_resized = act_map | |
| im = ax.imshow(act_resized, cmap=cmap) | |
| ax.set_title(f"Mapa de Activación - Neurona {selected_neuron_idx}", | |
| fontsize=14, fontweight='bold') | |
| ax.axis('off') | |
| plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04) | |
| plt.tight_layout() | |
| st.pyplot(fig) | |
| plt.close(fig) | |
| # Información detallada de la neurona seleccionada | |
| neuron_stats = results['stats'][selected_neuron_idx] | |
| col1, col2, col3, col4 = st.columns(4) | |
| with col1: | |
| st.metric("Media", f"{neuron_stats['mean']:.4f}") | |
| with col2: | |
| st.metric("Máxima", f"{neuron_stats['max']:.4f}") | |
| with col3: | |
| st.metric("Std", f"{neuron_stats['std']:.4f}") | |
| with col4: | |
| st.metric("Sparsity", f"{neuron_stats['sparsity']*100:.1f}%") | |
| with tab2: | |
| st.write("**Grid de todos los filtros más activos numerados**") | |
| # Crear grid | |
| fig_grid = create_filter_grid( | |
| activations=activations, | |
| neuron_indices=results['top_neurons'], | |
| image_vis=results['img_vis'], | |
| max_cols=4, | |
| cmap=cmap | |
| ) | |
| st.pyplot(fig_grid) | |
| plt.close(fig_grid) | |
| st.caption(""" | |
| Cada panel muestra el mapa de activación de un filtro específico. Los números en las esquinas | |
| indican el ranking (1 = más activo). El número en el título corresponde al índice real de la | |
| neurona en la capa. | |
| """) | |
| with tab3: | |
| st.write("**Análisis detallado: ¿Por qué se activó cada filtro?**") | |
| st.info( | |
| "💡 Analiza cada filtro individualmente para entender qué detectó y dónde.") | |
| # CSS para permitir wrap en tabs | |
| st.markdown(""" | |
| <style> | |
| /* Permitir que los tabs hagan wrap en múltiples líneas */ | |
| div[data-baseweb="tab-list"] { | |
| flex-wrap: wrap !important; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # Importar funciones necesarias | |
| from filter_visualization import ( | |
| compute_activation_regions, | |
| create_image_with_filter_patches, | |
| explain_filter_activation | |
| ) | |
| # Pre-calcular todas las regiones de activación | |
| num_filtros_detalle = min(top_k, len(results['top_neurons'])) | |
| all_regions = {} | |
| for neuron_idx in results['top_neurons'][:num_filtros_detalle]: | |
| act_map = activations[0, neuron_idx, :, :].cpu().numpy() | |
| all_regions[neuron_idx] = compute_activation_regions( | |
| act_map, | |
| results['img_vis'].shape[:2], | |
| threshold_percentile=75, | |
| min_regions=3 | |
| ) | |
| # Crear pestañas para cada uno de los 5 filtros | |
| filter_tabs = st.tabs([ | |
| f"Filtro {results['top_neurons'][i]} (#{i+1})" | |
| for i in range(num_filtros_detalle) | |
| ]) | |
| # Generar contenido para cada pestaña | |
| for tab_idx, filter_tab in enumerate(filter_tabs): | |
| neuron_idx = results['top_neurons'][tab_idx] | |
| with filter_tab: | |
| # Obtener mapa de activación | |
| act_map = activations[0, neuron_idx, :, :].cpu().numpy() | |
| regions = all_regions[neuron_idx] | |
| # Layout principal: Columna izquierda (visualizaciones) | Columna derecha (análisis) | |
| col_left, col_right = st.columns([1.3, 1]) | |
| # =================================================================== | |
| # COLUMNA IZQUIERDA: Visualizaciones (Regiones + Heatmap) | |
| # =================================================================== | |
| with col_left: | |
| # Sección 1: Regiones + Patrón del Filtro | |
| st.markdown("### 🖼️ Regiones + Patrón del Filtro") | |
| fig_combined = create_image_with_filter_patches( | |
| image_vis=results['img_vis'], | |
| activation_map=act_map, | |
| model=model, | |
| layer_name=results['layer_name'], | |
| filter_idx=neuron_idx, | |
| max_boxes=3 | |
| ) | |
| st.pyplot(fig_combined) | |
| plt.close(fig_combined) | |
| st.caption( | |
| "🔴 Roja: Mayor | 🟡 Amarilla: Media | 🟢 Verde: Menor") | |
| # Separador | |
| st.markdown("---") | |
| # Mostrar descomposición RGB del filtro | |
| st.markdown( | |
| "### 🎨 Patrón del Filtro") | |
| # Importar función para extraer pesos del filtro | |
| from filter_visualization import extract_filter_weights_rgb, create_rgb_channel_visualization | |
| try: | |
| # Intentar extraer pesos RGB | |
| filter_weights = extract_filter_weights_rgb( | |
| model=model, | |
| layer_name=results['layer_name'], | |
| filter_idx=neuron_idx | |
| ) | |
| if filter_weights is not None: | |
| # Capa RGB real - Mostrar descomposición completa | |
| st.markdown("#### Descomposición por Canales RGB") | |
| fig_channels = create_rgb_channel_visualization( | |
| filter_weights=filter_weights, | |
| filter_idx=neuron_idx | |
| ) | |
| st.pyplot(fig_channels) | |
| plt.close(fig_channels) | |
| st.caption( | |
| "💡 **Interpretación**: Esta es una capa temprana que opera directamente sobre píxeles RGB. " | |
| "Cada canal muestra qué intensidad de ese color busca el filtro." | |
| ) | |
| else: | |
| # Capa profunda - Mostrar explicación | |
| st.info( | |
| f"ℹ️ **Capa profunda detectada**: `{results['layer_name']}`\n\n" | |
| "Esta capa no opera sobre píxeles RGB directamente, sino sobre " | |
| "representaciones abstractas aprendidas por capas anteriores.\n\n" | |
| "**¿Qué significa esto?**\n" | |
| "- Los filtros tienen cientos de canales de entrada (no solo 3 RGB)\n" | |
| "- No se pueden visualizar como 'patrones de color'\n" | |
| "- Las **regiones de activación** arriba son una forma de entender qué detecta este filtro" | |
| ) | |
| except Exception as e: | |
| st.error(f"❌ Error al visualizar filtro: {str(e)}") | |
| # Sección 2: Heatmap completo (justo debajo) | |
| # st.markdown("---") | |
| # st.markdown("### 🌡️ Mapa de Calor Completo") | |
| # fig_heat = create_activation_heatmap( | |
| # image_vis=results['img_vis'], | |
| # activation_map=act_map, | |
| # title=f"Filtro {neuron_idx}", | |
| # alpha=alpha, | |
| # cmap=cmap, | |
| # figsize=(3, 3) | |
| # ) | |
| # st.pyplot(fig_heat) | |
| # plt.close(fig_heat) | |
| # =================================================================== | |
| # COLUMNA DERECHA: Análisis (se mantiene a la derecha todo el tiempo) | |
| # =================================================================== | |
| with col_right: | |
| st.markdown(f"### 🔍 Análisis") | |
| # Estadísticas | |
| neuron_stats = results['stats'][neuron_idx] | |
| metric_col1, metric_col2 = st.columns(2) | |
| with metric_col1: | |
| st.metric("Media", f"{neuron_stats['mean']:.4f}") | |
| st.metric("Desv. Std", f"{neuron_stats['std']:.4f}") | |
| with metric_col2: | |
| st.metric("Máxima", f"{neuron_stats['max']:.4f}") | |
| # Indicador visual de sparsity | |
| sparsity_pct = neuron_stats['sparsity']*100 | |
| if sparsity_pct < 10: | |
| st.metric("Sparsity", f"{sparsity_pct:.1f}%", | |
| delta="Baja", delta_color="inverse") | |
| elif sparsity_pct < 30: | |
| st.metric("Sparsity", f"{sparsity_pct:.1f}%", | |
| delta="Media", delta_color="off") | |
| else: | |
| st.metric("Sparsity", f"{sparsity_pct:.1f}%", | |
| delta="Alta", delta_color="normal") | |
| st.markdown("---") | |
| # Explicación textual | |
| explanation = explain_filter_activation( | |
| filter_idx=neuron_idx, | |
| regions=regions, | |
| neuron_stats=neuron_stats | |
| ) | |
| st.markdown(explanation) | |
| # Interpretación simple | |
| st.markdown("---") | |
| st.markdown("#### 💡 Interpretación") | |
| if len(regions) > 0: | |
| if neuron_stats['sparsity'] > 0.5: | |
| st.success( | |
| f"✅ Filtro selectivo: {len(regions)} región(es) específica(s)") | |
| else: | |
| st.info( | |
| f"ℹ️ Filtro general: {len(regions)} región(es) detectada(s)") | |
| avg_intensity = np.mean( | |
| [r['intensity'] for r in regions[:3]]) | |
| if avg_intensity > 0.7: | |
| st.write("🔥 Coincidencia fuerte del patrón") | |
| elif avg_intensity > 0.4: | |
| st.write("⚡ Coincidencia moderada") | |
| else: | |
| st.write("💫 Coincidencia débil") | |
| else: | |
| st.warning("⚠️ Patrón no detectado en esta imagen") | |
| with tab4: | |
| st.write("**Visualización de Filtros: Patches detectados y Patrones RGB**") | |
| st.info( | |
| "💡 Explora qué partes de la imagen activaron cada filtro y qué patrón busca.") | |
| # Importar funciones | |
| from filter_visualization import ( | |
| create_filter_grid_rgb, | |
| create_activation_patches_visualization | |
| ) | |
| # =================================================================== | |
| # SECCIÓN 1: PESOS RGB DE LOS FILTROS | |
| # =================================================================== | |
| st.subheader("🎨 Patrones RGB de los Filtros") | |
| st.caption( | |
| "Estos son los pesos aprendidos (kernel de 7×7) que busca cada filtro") | |
| with st.spinner("Generando visualización de filtros RGB..."): | |
| try: | |
| fig_filters = create_filter_grid_rgb( | |
| model=model, | |
| layer_name=results['layer_name'], | |
| filter_indices=results['top_neurons'][:top_k], | |
| num_cols=6 | |
| ) | |
| st.pyplot(fig_filters) | |
| plt.close(fig_filters) | |
| st.info( | |
| "💡 **Interpretación**: En capas tempranas (conv1, layer1), estos patrones representan " | |
| "colores y texturas. En capas profundas son más abstractos." | |
| ) | |
| except Exception as e: | |
| st.warning( | |
| f"⚠️ No se pueden visualizar filtros RGB para esta capa: {str(e)}") | |
| st.caption( | |
| "**Limitación**: Solo capas convolucionales tempranas (que reciben entrada RGB) " | |
| "pueden visualizarse como patrones de color." | |
| ) | |
| st.markdown("---") | |
| # =================================================================== | |
| # SECCIÓN 2: PATCHES DE IMAGEN QUE ACTIVARON CADA FILTRO | |
| # =================================================================== | |
| st.subheader("📸 Patches de Imagen que Activaron cada Filtro") | |
| st.caption("Fragmentos reales de la imagen que más activaron cada filtro") | |
| # Mostrar patches para los top 5 filtros | |
| for idx, neuron_idx in enumerate(results['top_neurons'][:top_k], 1): | |
| act_map = activations[0, neuron_idx, :, :].cpu().numpy() | |
| # Encabezado del filtro | |
| col_title, col_stats = st.columns([2, 1]) | |
| with col_title: | |
| st.markdown(f"### Filtro {neuron_idx} (Rank #{idx})") | |
| with col_stats: | |
| neuron_stats = results['stats'][neuron_idx] | |
| st.caption( | |
| f"Media: {neuron_stats['mean']:.3f} | " | |
| f"Sparsity: {neuron_stats['sparsity']*100:.0f}%" | |
| ) | |
| # Generar visualización de patches | |
| fig_patches = create_activation_patches_visualization( | |
| image_vis=results['img_vis'], | |
| activation_map=act_map, | |
| filter_idx=neuron_idx, | |
| num_patches=3 | |
| ) | |
| st.pyplot(fig_patches) | |
| plt.close(fig_patches) | |
| st.markdown("---") | |
| # Nota final | |
| st.info( | |
| "💡 **Tip**: Compara los patrones RGB (arriba) con los patches detectados (aquí) " | |
| "para entender qué está buscando y encontrando cada filtro." | |
| ) | |
| # =================================================================== | |
| # TAB 5: PREDICCIÓN DEL MODELO | |
| # =================================================================== | |
| with tab5: | |
| st.write("**Información sobre la predicción del modelo**") | |
| st.info("💡 Esta pestaña muestra qué clase detectó el modelo en la imagen.") | |
| # Predicción principal | |
| pred = results['prediction'] | |
| conf = results['confidence'] | |
| # Diccionario completo de clases ImageNet (las más comunes) | |
| IMAGENET_CLASSES = { | |
| # Gatos | |
| 281: "gato atigrado (tabby cat)", | |
| 282: "gato tigre (tiger cat)", | |
| 283: "gato persa (Persian cat)", | |
| 284: "gato siamés (Siamese cat)", | |
| 285: "gato egipcio (Egyptian cat)", | |
| # Perros | |
| 207: "golden retriever", | |
| 208: "labrador retriever", | |
| 209: "pastor alemán (German shepherd)", | |
| 235: "pastor belga (Belgian sheepdog)", | |
| 236: "cocker spaniel", | |
| # Otros animales | |
| 151: "chimpancé", | |
| 388: "ballena jorobada (humpback whale)", | |
| 33: "lobo gris (grey wolf)", | |
| 334: "mono aullador (howler monkey)", | |
| # Objetos | |
| 404: "avión de pasajeros (airliner)", | |
| 436: "guitarra acústica (acoustic guitar)", | |
| 511: "piscina (swimming pool)", | |
| 779: "computadora portátil (laptop)", | |
| # Vehículos | |
| 609: "jeep", | |
| 656: "minivan", | |
| 751: "racer (coche de carreras)", | |
| } | |
| # Obtener nombre de la clase | |
| class_name = IMAGENET_CLASSES.get(pred, f"Clase ImageNet #{pred}") | |
| # Banner principal | |
| st.markdown("### 🔍 Predicción Principal") | |
| col1, col2, col3 = st.columns([2, 1, 1]) | |
| with col1: | |
| if conf > 0.7: | |
| st.success(f"### {class_name}") | |
| elif conf > 0.4: | |
| st.warning(f"### {class_name}") | |
| else: | |
| st.error(f"### {class_name}") | |
| with col2: | |
| st.metric("Confianza", f"{conf:.1%}") | |
| with col3: | |
| st.metric("Clase ID", f"#{pred}") | |
| st.markdown("---") | |
| # Interpretación de la confianza | |
| st.markdown("### 📈 Interpretación de la Confianza") | |
| if conf > 0.9: | |
| st.success( | |
| "✅ **Muy alta confianza** - El modelo está muy seguro de esta predicción.") | |
| elif conf > 0.7: | |
| st.info("👍 **Alta confianza** - El modelo está razonablemente seguro.") | |
| elif conf > 0.4: | |
| st.warning( | |
| "⚠️ **Confianza moderada** - El modelo tiene algunas dudas.") | |
| else: | |
| st.error( | |
| "❌ **Baja confianza** - El modelo no está seguro de la predicción.") | |
| # Barra de progreso visual | |
| st.progress(conf) | |
| st.markdown("---") | |
| # Información sobre ImageNet | |
| st.markdown("### ℹ️ Sobre las Clases de ImageNet") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown(""" | |
| **¿Qué es ImageNet?** | |
| ImageNet es un conjunto de datos con más de 14 millones de imágenes | |
| organizadas en **1,000 categorías** diferentes. | |
| Los modelos ResNet18 y AlexNet fueron entrenados con este dataset | |
| para reconocer objetos, animales, vehículos, y más. | |
| """) | |
| with col2: | |
| st.markdown(""" | |
| **Categorías principales:** | |
| - 🐱 Animales (perros, gatos, aves, etc.) | |
| - 🚗 Vehículos (coches, aviones, barcos) | |
| - 🏠 Objetos cotidianos (muebles, instrumentos) | |
| - 🌿 Naturaleza (plantas, paisajes) | |
| - 🍎 Alimentos | |
| """) | |
| # Información adicional del modelo | |
| st.markdown("---") | |
| st.markdown("### 🤖 Información del Modelo") | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.metric("Modelo Usado", model_name.upper()) | |
| with col2: | |
| if model_name == 'resnet18': | |
| st.metric("Parámetros", "~11M") | |
| else: # alexnet | |
| st.metric("Parámetros", "~61M") | |
| with col3: | |
| st.metric("Dataset", "ImageNet") | |
| # Nota sobre limitaciones | |
| with st.expander("⚠️ Limitaciones del modelo"): | |
| st.markdown(""" | |
| **Ten en cuenta:** | |
| 1. **Sesgos del dataset**: El modelo puede tener mejor desempeño | |
| en categorías más representadas en ImageNet. | |
| 2. **Clases específicas**: Si tu imagen no pertenece a ninguna | |
| de las 1,000 clases, la predicción será la más cercana. | |
| 3. **Confianza baja**: Una confianza <50% puede indicar que la | |
| imagen no encaja bien en ninguna categoría. | |
| 4. **Contexto importa**: El fondo y otros elementos pueden | |
| influir en la predicción. | |
| """) | |
| # Enlace a documentación | |
| st.markdown("---") | |
| st.info( | |
| "📚 **Más información**: Para ver la lista completa de las 1,000 clases de ImageNet, " | |
| "visita [ImageNet Classes](https://deeplearning.cms.waikato.ac.nz/user-guide/class-maps/IMAGENET/)" | |
| ) | |
| # =================================================================== | |
| # TAB 6: EXPERIMENTOS DE ABLACIÓN | |
| # =================================================================== | |
| with tab6: | |
| st.markdown("## 🧪 Experimentos de Ablación") | |
| st.markdown( | |
| "Descubre qué tan importantes son las neuronas seleccionadas para la predicción del modelo. " | |
| "Ejecuta experimentos de **knockout**, **aislamiento**, **amplificación** y **ruido** para entender su rol." | |
| ) | |
| # Importar funciones necesarias | |
| from utils_streamlit import run_ablation_experiment, get_imagenet_class_name | |
| from filter_visualization import ( | |
| extract_filter_weights_rgb, | |
| ) | |
| # =================================================================== | |
| # SECCIÓN 1: CONFIGURACIÓN Y BASELINE | |
| # =================================================================== | |
| st.markdown("---") | |
| st.markdown("### 🎯 Configuración del Experimento") | |
| # Baseline en columnas compactas | |
| col_base1, col_base2, col_base3 = st.columns(3) | |
| baseline_class = get_imagenet_class_name(results['prediction']) | |
| with col_base1: | |
| st.metric("📊 Predicción Original", baseline_class) | |
| with col_base2: | |
| st.metric("🎯 Confianza", f"{results['confidence']:.1%}") | |
| with col_base3: | |
| st.metric("🔢 Clase ID", f"#{results['prediction']}") | |
| st.markdown("") | |
| # ═══════════════════════════════════════════════════════════════ | |
| # FORMULARIO: Evita recargas al cambiar configuración | |
| # ═══════════════════════════════════════════════════════════════ | |
| with st.form("ablation_config_form"): | |
| st.markdown("#### ⚙️ Configuración de Experimentos") | |
| # Selector de neuronas y parámetros | |
| col_selector, col_params = st.columns([2, 1]) | |
| with col_selector: | |
| # Crear opciones con información útil | |
| neuron_options = { | |
| neuron_idx: f"Filtro {neuron_idx} (#{results['top_neurons'].index(neuron_idx) + 1}) - " | |
| f"Media: {results['stats'][neuron_idx]['mean']:.2f}, " | |
| f"Sparsity: {results['stats'][neuron_idx]['sparsity']*100:.0f}%" | |
| for neuron_idx in results['top_neurons'] | |
| } | |
| selected_neurons = st.multiselect( | |
| "🎯 Selecciona neurona(s) para experimentar:", | |
| options=list(neuron_options.keys()), | |
| format_func=lambda x: neuron_options[x], | |
| default=[results['top_neurons'][0]], | |
| help="Selecciona una o múltiples neuronas para analizar su importancia" | |
| ) | |
| with col_params: | |
| amp_factor = st.slider( | |
| "⚡ Factor de amplificación:", | |
| min_value=2.0, | |
| max_value=10.0, | |
| value=5.0, | |
| step=1.0, | |
| help="Factor de multiplicación para el experimento de amplificación" | |
| ) | |
| noise_level = st.slider( | |
| "🌫️ Nivel de ruido:", | |
| min_value=0.1, | |
| max_value=2.0, | |
| value=0.5, | |
| step=0.1, | |
| help="Intensidad del ruido gaussiano" | |
| ) | |
| # Botón único: aplica Y ejecuta | |
| st.markdown("") | |
| # Calcular total de experimentos | |
| if selected_neurons: | |
| total_exp = len(selected_neurons) * 4 + \ | |
| (4 if len(selected_neurons) > 1 else 0) | |
| button_text = f"🧪 Ejecutar los Experimentos" | |
| button_help = f"Ejecutará {len(selected_neurons) * 4} individuales" | |
| if len(selected_neurons) > 1: | |
| button_help += f" + 4 grupales" | |
| else: | |
| button_text = "🧪 Ejecutar Experimentos" | |
| button_help = "Selecciona al menos una neurona primero" | |
| execute_button = st.form_submit_button( | |
| button_text, | |
| type="primary", | |
| use_container_width=True, | |
| help=button_help | |
| ) | |
| # ═══════════════════════════════════════════════════════════════ | |
| # VALIDACIÓN FUERA DEL FORM | |
| # ═══════════════════════════════════════════════════════════════ | |
| if not selected_neurons: | |
| st.warning("⚠️ Selecciona al menos una neurona en el formulario") | |
| st.stop() | |
| # Mostrar info de tipos de experimentos | |
| with st.expander("ℹ️ ¿Qué hace cada experimento?", expanded=False): | |
| col_info1, col_info2, col_info3, col_info4 = st.columns(4) | |
| with col_info1: | |
| st.markdown("**🔴 Knockout**") | |
| st.caption("Apaga las neuronas para ver si son críticas") | |
| with col_info2: | |
| st.markdown("**🟢 Aislamiento**") | |
| st.caption("Activa SOLO estas neuronas") | |
| with col_info3: | |
| st.markdown("**⚡ Amplificación**") | |
| st.caption("Multiplica activaciones") | |
| with col_info4: | |
| st.markdown("**🌫️ Ruido**") | |
| st.caption("Agrega ruido para probar robustez") | |
| st.markdown("---") | |
| # =================================================================== | |
| # EJECUCIÓN DE EXPERIMENTOS | |
| # =================================================================== | |
| if execute_button: | |
| # Estructura para almacenar resultados | |
| experiments_results = { | |
| 'individual': {}, | |
| 'group': {} | |
| } | |
| # Barra de progreso | |
| progress_bar = st.progress(0) | |
| status_text = st.empty() | |
| # Calcular total de experimentos | |
| individual_experiments = len(selected_neurons) * 4 | |
| group_experiments = 4 if len(selected_neurons) > 1 else 0 | |
| total_experiments = individual_experiments + group_experiments | |
| current_step = 0 | |
| # --------------------------------------------------------------- | |
| # EXPERIMENTOS INDIVIDUALES | |
| # --------------------------------------------------------------- | |
| for neuron_idx in selected_neurons: | |
| experiments_results['individual'][neuron_idx] = {} | |
| # Knockout individual | |
| status_text.text(f"🔴 Knockout - Filtro {neuron_idx}...") | |
| result_ko = run_ablation_experiment( | |
| model=model, | |
| image=results['image_tensor'], | |
| target_layer=results['layer_name'], | |
| neuron_idx=neuron_idx, | |
| experiment_type='knockout', | |
| device=device | |
| ) | |
| experiments_results['individual'][neuron_idx]['knockout'] = result_ko | |
| current_step += 1 | |
| progress_bar.progress(current_step / total_experiments) | |
| # Isolation individual | |
| status_text.text(f"🟢 Aislamiento - Filtro {neuron_idx}...") | |
| result_iso = run_ablation_experiment( | |
| model=model, | |
| image=results['image_tensor'], | |
| target_layer=results['layer_name'], | |
| neuron_idx=neuron_idx, | |
| experiment_type='isolation', | |
| device=device | |
| ) | |
| experiments_results['individual'][neuron_idx]['isolation'] = result_iso | |
| current_step += 1 | |
| progress_bar.progress(current_step / total_experiments) | |
| # Amplification individual | |
| status_text.text(f"⚡ Amplificación - Filtro {neuron_idx}...") | |
| result_amp = run_ablation_experiment( | |
| model=model, | |
| image=results['image_tensor'], | |
| target_layer=results['layer_name'], | |
| neuron_idx=neuron_idx, | |
| experiment_type='amplify', | |
| amplification_factor=amp_factor, | |
| device=device | |
| ) | |
| experiments_results['individual'][neuron_idx]['amplify'] = result_amp | |
| current_step += 1 | |
| progress_bar.progress(current_step / total_experiments) | |
| # Experimento 4: Noise individual | |
| status_text.text(f"🌫️ Ruido - Filtro {neuron_idx}...") | |
| result_noise = run_ablation_experiment( | |
| model=model, | |
| image=results['image_tensor'], | |
| target_layer=results['layer_name'], | |
| neuron_idx=neuron_idx, | |
| experiment_type='add_noise', | |
| noise_level=noise_level, | |
| device=device | |
| ) | |
| experiments_results['individual'][neuron_idx]['noise'] = result_noise | |
| current_step += 1 | |
| progress_bar.progress(current_step / total_experiments) | |
| # --------------------------------------------------------------- | |
| # EXPERIMENTOS GRUPALES (si hay múltiples neuronas) | |
| # --------------------------------------------------------------- | |
| if len(selected_neurons) > 1: | |
| # Group Knockout | |
| status_text.text( | |
| f"🔴 Knockout grupal - {len(selected_neurons)} neuronas...") | |
| result_group_ko = run_ablation_experiment( | |
| model=model, | |
| image=results['image_tensor'], | |
| target_layer=results['layer_name'], | |
| neuron_idx=selected_neurons, | |
| experiment_type='group_knockout', | |
| device=device | |
| ) | |
| experiments_results['group']['knockout'] = result_group_ko | |
| current_step += 1 | |
| progress_bar.progress(current_step / total_experiments) | |
| # Group Isolation | |
| status_text.text( | |
| f"🟢 Aislamiento grupal - {len(selected_neurons)} neuronas...") | |
| result_group_iso = run_ablation_experiment( | |
| model=model, | |
| image=results['image_tensor'], | |
| target_layer=results['layer_name'], | |
| neuron_idx=selected_neurons, | |
| experiment_type='group_isolation', | |
| device=device | |
| ) | |
| experiments_results['group']['isolation'] = result_group_iso | |
| current_step += 1 | |
| progress_bar.progress(current_step / total_experiments) | |
| # Group Amplification | |
| status_text.text( | |
| f"⚡ Amplificación grupal - {len(selected_neurons)} neuronas...") | |
| result_group_amp = run_ablation_experiment( | |
| model=model, | |
| image=results['image_tensor'], | |
| target_layer=results['layer_name'], | |
| neuron_idx=selected_neurons, | |
| experiment_type='group_amplify', | |
| amplification_factor=amp_factor, | |
| device=device | |
| ) | |
| experiments_results['group']['amplify'] = result_group_amp | |
| current_step += 1 | |
| progress_bar.progress(current_step / total_experiments) | |
| # Experimento grupal 4: Ruido del grupo completo | |
| status_text.text( | |
| f"🌫️ Ruido grupal - {len(selected_neurons)} neuronas...") | |
| result_group_noise = run_ablation_experiment( | |
| model=model, | |
| image=results['image_tensor'], | |
| target_layer=results['layer_name'], | |
| neuron_idx=selected_neurons, # Lista completa | |
| experiment_type='group_noise', | |
| noise_level=noise_level, | |
| device=device | |
| ) | |
| experiments_results['group']['noise'] = result_group_noise | |
| current_step += 1 | |
| progress_bar.progress(current_step / total_experiments) | |
| # Limpiar y guardar | |
| progress_bar.empty() | |
| status_text.empty() | |
| st.session_state['ablation_results'] = experiments_results | |
| st.session_state['ablation_neurons'] = selected_neurons | |
| st.session_state['ablation_amp_factor'] = amp_factor | |
| st.session_state['ablation_noise_level'] = noise_level | |
| st.success( | |
| f"✅ Completado: {len(selected_neurons)*4} individuales" + | |
| (f" + 4 grupales" if len(selected_neurons) > 1 else "") | |
| ) | |
| # =================================================================== | |
| # MOSTRAR RESULTADOS (si existen) | |
| # =================================================================== | |
| if 'ablation_results' in st.session_state: | |
| results_data = st.session_state['ablation_results'] | |
| tested_neurons = st.session_state['ablation_neurons'] | |
| amp_factor_used = st.session_state['ablation_amp_factor'] | |
| noise_level_used = st.session_state['ablation_noise_level'] | |
| st.markdown("---") | |
| st.markdown("## 📊 Resultados de los Experimentos") | |
| # =============================================================== | |
| # SECCIÓN 2: COMPARACIÓN VISUAL | |
| # =============================================================== | |
| st.markdown("### 🔬 Comparación Visual: Original vs Ruido") | |
| st.info( | |
| f"Comparación visual de activaciones originales vs con ruido gaussiano. " | |
| f"Nivel de ruido: ±{noise_level_used}" | |
| ) | |
| # Crear figura con 2 filas: Original (arriba) y Con Ruido (abajo) | |
| num_neurons = len(tested_neurons) | |
| max_cols = min(4, num_neurons) | |
| fig, axes = plt.subplots( | |
| 2, max_cols, | |
| figsize=(3 * max_cols, 6) # ← Reducido de 4x8 a 3x6 | |
| ) | |
| # Asegurar que axes sea 2D | |
| if max_cols == 1: | |
| axes = axes.reshape(2, 1) | |
| # FILA 1: ACTIVACIONES ORIGINALES | |
| for idx, neuron_idx in enumerate(tested_neurons[:max_cols]): | |
| ax = axes[0, idx] | |
| # Obtener activación original | |
| act_map = results['activations'][0, | |
| neuron_idx, :, :].cpu().numpy() | |
| # Normalizar activación | |
| if act_map.max() > act_map.min(): | |
| act_norm = (act_map - act_map.min()) / \ | |
| (act_map.max() - act_map.min()) | |
| else: | |
| act_norm = act_map | |
| # Mostrar heatmap | |
| im = ax.imshow( | |
| act_norm, | |
| cmap='hot', # ← Sin variable, valor fijo | |
| alpha=1.0 | |
| ) | |
| ax.set_title( | |
| f"📊 Original - Filtro {neuron_idx}", | |
| fontsize=12, | |
| fontweight='bold' | |
| ) | |
| ax.axis('off') | |
| plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04) | |
| ax.text( | |
| 0.5, -0.1, | |
| f"Max: {act_map.max():.2f} | Mean: {act_map.mean():.2f}", | |
| transform=ax.transAxes, | |
| ha='center', | |
| fontsize=9, | |
| bbox=dict(boxstyle='round', facecolor='white', alpha=0.8) | |
| ) | |
| # FILA 2: ACTIVACIONES CON RUIDO | |
| for idx, neuron_idx in enumerate(tested_neurons[:max_cols]): | |
| ax = axes[1, idx] | |
| # Obtener activación original | |
| act_map = results['activations'][0, | |
| neuron_idx, :, :].cpu().numpy() | |
| # Agregar ruido gaussiano | |
| act_std = act_map.std() | |
| noise = np.random.randn( | |
| *act_map.shape) * act_std * noise_level_used | |
| act_map_noisy = act_map + noise | |
| # Normalizar activación con ruido | |
| if act_map_noisy.max() > act_map_noisy.min(): | |
| act_norm = (act_map_noisy - act_map_noisy.min()) / \ | |
| (act_map_noisy.max() - act_map_noisy.min()) | |
| else: | |
| act_norm = act_map_noisy | |
| # Mostrar heatmap | |
| im = ax.imshow( | |
| act_norm, | |
| cmap='hot', # ← Sin variable, valor fijo | |
| alpha=1.0 | |
| ) | |
| ax.set_title( | |
| f"🌫️ Con Ruido (±{noise_level_used}) - Filtro {neuron_idx}", | |
| fontsize=12, | |
| fontweight='bold' | |
| ) | |
| ax.axis('off') | |
| plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04) | |
| ax.text( | |
| 0.5, -0.1, | |
| f"Max: {act_map_noisy.max():.2f} | Ruido: ±{act_std * noise_level_used:.2f}", | |
| transform=ax.transAxes, | |
| ha='center', | |
| fontsize=9, | |
| bbox=dict(boxstyle='round', facecolor='white', alpha=0.8) | |
| ) | |
| plt.tight_layout() | |
| st.pyplot(fig) | |
| plt.close(fig) | |
| # Mostrar resultado del experimento de ruido | |
| st.markdown("---") | |
| st.markdown("#### 🎯 Resultado del Experimento de Ruido") | |
| # Obtener resultado de ruido | |
| if len(tested_neurons) > 1: | |
| if 'group' in results_data and 'noise' in results_data['group']: | |
| noise_result = results_data['group']['noise'] | |
| result_type = "Resultado grupal" | |
| else: | |
| noise_result = None | |
| else: | |
| noise_result = results_data['individual'][tested_neurons[0]]['noise'] | |
| result_type = "Resultado individual" | |
| if noise_result: | |
| noise_class = get_imagenet_class_name( | |
| noise_result['prediction']) | |
| noise_change = noise_result['confidence'] - \ | |
| results['confidence'] | |
| col_noise1, col_noise2, col_noise3 = st.columns(3) | |
| with col_noise1: | |
| st.metric( | |
| "Predicción Original", | |
| baseline_class, | |
| help="Predicción sin ruido" | |
| ) | |
| st.caption(f"Confianza: {results['confidence']:.1%}") | |
| with col_noise2: | |
| st.metric( | |
| "Predicción con Ruido", | |
| noise_class, | |
| help="Predicción con ruido en activaciones" | |
| ) | |
| st.caption(f"Confianza: {noise_result['confidence']:.1%}") | |
| with col_noise3: | |
| st.metric( | |
| "Cambio en Confianza", | |
| f"{noise_result['confidence']:.1%}", | |
| delta=f"{noise_change:+.1%}" | |
| ) | |
| # Clasificación de robustez | |
| if abs(noise_change) < 0.05: | |
| st.success("🟢 **Muy Robusta**") | |
| elif abs(noise_change) < 0.10: | |
| st.info("🟡 **Robusta**") | |
| elif abs(noise_change) < 0.15: | |
| st.warning("🟠 **Sensible**") | |
| else: | |
| st.error("🔴 **Muy Sensible**") | |
| # Interpretación detallada | |
| st.markdown("") | |
| if noise_result['prediction'] == results['prediction']: | |
| st.success( | |
| f"✅ **Mantiene la clase:** Aunque se agregó ruido (±{noise_level_used}), " | |
| f"el modelo sigue prediciendo '{noise_class}'. " | |
| f"{'Las neuronas son robustas.' if abs(noise_change) < 0.10 else 'Hay cierta sensibilidad al ruido.'}" | |
| ) | |
| else: | |
| st.error( | |
| f"❌ **Cambió de clase:** El ruido (±{noise_level_used}) causó que la predicción " | |
| f"cambiara de '{baseline_class}' a '{noise_class}'. " | |
| "Las neuronas son muy sensibles a perturbaciones." | |
| ) | |
| # --------------------------------------------------------------- | |
| # VIZ TAB 2: Mapas Individuales (Grid) | |
| # --------------------------------------------------------------- | |
| # with viz_tab2: | |
| # st.info( | |
| # "Vista completa de los mapas de activación de todas las neuronas seleccionadas" | |
| # ) | |
| # # Colormap selector | |
| # im = ax.imshow( | |
| # act_norm, | |
| # cmap=viz_cmap_grid, # ← Usa el selector | |
| # alpha=1.0 # ← Sin transparencia | |
| # ) | |
| # # Crear grid de activaciones | |
| # from utils_streamlit import create_filter_grid | |
| # fig_grid = create_filter_grid( | |
| # activations=results['activations'], | |
| # neuron_indices=tested_neurons, | |
| # image_vis=results['img_vis'], | |
| # max_cols=min(4, len(tested_neurons)), | |
| # cmap=viz_cmap_grid | |
| # ) | |
| # st.pyplot(fig_grid) | |
| # plt.close(fig_grid) | |
| # =============================================================== | |
| # SECCIÓN 3: RESULTADOS DE EXPERIMENTOS | |
| # =============================================================== | |
| st.markdown("---") | |
| st.markdown("### 📈 Resultados de Experimentos") | |
| # Tabs internos para resultados | |
| if len(tested_neurons) > 1: | |
| results_tab1, results_tab2, results_tab3 = st.tabs([ | |
| "👥 Resultados Grupales", | |
| "🔢 Resultados Individuales", | |
| "📊 Comparación Detallada" | |
| ]) | |
| else: | |
| # Si solo hay 1 neurona, no mostrar tab grupal | |
| results_tab2, results_tab3 = st.tabs([ | |
| "🔢 Resultados Individuales", | |
| "📊 Comparación Detallada" | |
| ]) | |
| results_tab1 = None | |
| # --------------------------------------------------------------- | |
| # RESULTS TAB 1: Resultados Grupales | |
| # --------------------------------------------------------------- | |
| if results_tab1 is not None: | |
| with results_tab1: | |
| st.info( | |
| f"📌 Analizando {len(tested_neurons)} neuronas como grupo: {tested_neurons}") | |
| # Obtener resultados grupales | |
| group_ko = results_data['group']['knockout'] | |
| group_iso = results_data['group']['isolation'] | |
| group_amp = results_data['group']['amplify'] | |
| group_noise = results_data['group']['noise'] | |
| # Tabla resumen de 4 experimentos | |
| import pandas as pd | |
| summary_data = { | |
| 'Experimento': ['🔵 Original', '🟢 Aislamiento', '⚡ Amplificación', '🌫️ Ruido', '🔴 Knockout'], | |
| 'Predicción': [ | |
| baseline_class, | |
| get_imagenet_class_name(group_iso['prediction']), | |
| get_imagenet_class_name(group_amp['prediction']), | |
| get_imagenet_class_name(group_noise['prediction']), | |
| get_imagenet_class_name(group_ko['prediction']) | |
| ], | |
| 'Confianza': [ | |
| f"{results['confidence']:.1%}", | |
| f"{group_iso['confidence']:.1%}", | |
| f"{group_amp['confidence']:.1%}", | |
| f"{group_noise['confidence']:.1%}", | |
| f"{group_ko['confidence']:.1%}" | |
| ], | |
| 'Δ Cambio': [ | |
| '-', | |
| f"{(group_iso['confidence'] - results['confidence']):+.1%}", | |
| f"{(group_amp['confidence'] - results['confidence']):+.1%}", | |
| f"{(group_noise['confidence'] - results['confidence']):+.1%}", | |
| f"{(group_ko['confidence'] - results['confidence']):+.1%}" | |
| ], | |
| 'Estado': [ | |
| '🔵 Base', | |
| '✅' if group_iso['prediction'] == results['prediction'] else '❌ Cambió', | |
| '✅' if group_amp['prediction'] == results['prediction'] else '❌ Cambió', | |
| '✅' if group_noise['prediction'] == results['prediction'] else '❌ Cambió', | |
| '✅' if group_ko['prediction'] == results['prediction'] else '❌ Cambió' | |
| ] | |
| } | |
| df_summary = pd.DataFrame(summary_data) | |
| st.dataframe( | |
| df_summary, use_container_width=True, hide_index=True) | |
| st.markdown("---") | |
| # Análisis de sinergia | |
| st.markdown("#### 🔬 Análisis de Sinergia") | |
| # Calcular suma de efectos individuales | |
| sum_individual_ko_changes = sum([ | |
| abs(results_data['individual'][n]['knockout'] | |
| ['confidence'] - results['confidence']) | |
| for n in tested_neurons | |
| ]) | |
| ko_change = group_ko['confidence'] - results['confidence'] | |
| group_ko_change = abs(ko_change) | |
| col_syn1, col_syn2 = st.columns(2) | |
| with col_syn1: | |
| st.metric("Efecto Grupal (Knockout)", | |
| f"{group_ko_change:.1%}") | |
| with col_syn2: | |
| st.metric("Suma Efectos Individuales", | |
| f"{sum_individual_ko_changes:.1%}") | |
| # Interpretación | |
| if group_ko_change > sum_individual_ko_changes * 1.2: | |
| st.success( | |
| "🔗 **Sinergia positiva**: El grupo tiene más impacto que la suma de individuales" | |
| ) | |
| elif group_ko_change < sum_individual_ko_changes * 0.5: | |
| st.warning( | |
| "🔀 **Redundancia**: El grupo tiene menos impacto (neuronas redundantes)" | |
| ) | |
| else: | |
| st.info( | |
| "➡️ **Efecto aditivo**: El grupo suma aproximadamente los efectos individuales" | |
| ) | |
| # Interpretación de importancia | |
| st.markdown("---") | |
| st.markdown("#### 💡 Importancia del Grupo") | |
| if abs(ko_change) > 0.2: | |
| st.error( | |
| "🔥 **Grupo crítico**: Eliminarlo cambia drásticamente la predicción") | |
| elif abs(ko_change) > 0.1: | |
| st.warning( | |
| "⚡ **Grupo importante**: Tiene impacto significativo") | |
| else: | |
| st.info( | |
| "💤 **Grupo redundante**: El modelo compensa fácilmente") | |
| if group_iso['confidence'] > 0.5: | |
| st.success( | |
| "🎯 **Suficientes por sí solas**: Este grupo es altamente determinante") | |
| elif group_iso['confidence'] > 0.3: | |
| st.info( | |
| "⚖️ **Contribuyentes fuertes**: Aportan información significativa") | |
| # --------------------------------------------------------------- | |
| # RESULTS TAB 2: Resultados Individuales | |
| # --------------------------------------------------------------- | |
| with results_tab2: | |
| st.markdown("#### 🔍 Tabla Comparativa por Neurona") | |
| # Tabla comparativa | |
| import pandas as pd | |
| comparison_data = [] | |
| for neuron_idx in tested_neurons: | |
| ko_result = results_data['individual'][neuron_idx]['knockout'] | |
| iso_result = results_data['individual'][neuron_idx]['isolation'] | |
| amp_result = results_data['individual'][neuron_idx]['amplify'] | |
| ko_change = ko_result['confidence'] - results['confidence'] | |
| amp_change = amp_result['confidence'] - \ | |
| results['confidence'] | |
| noise_result = results_data['individual'][neuron_idx]['noise'] | |
| noise_change = noise_result['confidence'] - \ | |
| results['confidence'] | |
| comparison_data.append({ | |
| 'Filtro': neuron_idx, | |
| 'Rank': f"#{tested_neurons.index(neuron_idx) + 1}", | |
| 'Sparsity': f"{results['stats'][neuron_idx]['sparsity']*100:.0f}%", | |
| 'KO': f"{ko_result['confidence']:.1%}", | |
| 'Δ KO': f"{ko_change:+.1%}", | |
| 'ISO': f"{iso_result['confidence']:.1%}", | |
| 'AMP': f"{amp_result['confidence']:.1%}", | |
| 'Δ AMP': f"{amp_change:+.1%}", | |
| 'NOISE': f"{noise_result['confidence']:.1%}", | |
| 'Δ NOISE': f"{noise_change:+.1%}" | |
| }) | |
| df_comparison = pd.DataFrame(comparison_data) | |
| st.dataframe( | |
| df_comparison, use_container_width=True, hide_index=True) | |
| st.markdown("---") | |
| st.markdown("#### 📋 Detalles por Neurona") | |
| # Expanders con detalles | |
| for neuron_idx in tested_neurons: | |
| with st.expander(f"🔍 Filtro {neuron_idx} - Análisis Detallado", expanded=False): | |
| ko_result = results_data['individual'][neuron_idx]['knockout'] | |
| iso_result = results_data['individual'][neuron_idx]['isolation'] | |
| amp_result = results_data['individual'][neuron_idx]['amplify'] | |
| noise_result = results_data['individual'][neuron_idx]['noise'] | |
| col_det1, col_det2, col_det3, col_det4 = st.columns(4) | |
| # Knockout | |
| with col_det1: | |
| st.markdown("**🔴 Knockout**") | |
| ko_change = ko_result['confidence'] - \ | |
| results['confidence'] | |
| st.metric( | |
| "Confianza", f"{ko_result['confidence']:.1%}", delta=f"{ko_change:.1%}") | |
| if abs(ko_change) > 0.1: | |
| st.caption("⚡ Importante") | |
| else: | |
| st.caption("💤 Redundante") | |
| # Isolation | |
| with col_det2: | |
| st.markdown("**🟢 Aislamiento**") | |
| st.metric( | |
| "Confianza", f"{iso_result['confidence']:.1%}") | |
| if iso_result['confidence'] > 0.3: | |
| st.caption("🎯 Determinante") | |
| else: | |
| st.caption("⚖️ Contribuyente") | |
| # Amplification | |
| with col_det3: | |
| st.markdown("**⚡ Amplificación**") | |
| amp_change = amp_result['confidence'] - \ | |
| results['confidence'] | |
| st.metric( | |
| "Confianza", f"{amp_result['confidence']:.1%}", delta=f"{amp_change:.1%}") | |
| if amp_change > 0.05: | |
| st.caption("📈 Efectiva") | |
| else: | |
| st.caption("➡️ Sin efecto") | |
| # Noise | |
| with col_det4: | |
| st.markdown("**🌫️ Ruido**") | |
| noise_change = noise_result['confidence'] - \ | |
| results['confidence'] | |
| st.metric( | |
| "Confianza", f"{noise_result['confidence']:.1%}", delta=f"{noise_change:.1%}") | |
| if abs(noise_change) > 0.1: | |
| st.caption("🔴 Sensible") | |
| else: | |
| st.caption("🟢 Robusta") | |
| # --------------------------------------------------------------- | |
| # RESULTS TAB 3: Comparación Detallada | |
| # --------------------------------------------------------------- | |
| with results_tab3: | |
| st.markdown( | |
| "#### 📊 Resumen Completo de Todos los Experimentos") | |
| import pandas as pd | |
| # Crear tabla completa con todos los datos | |
| detailed_data = [] | |
| # Agregar datos individuales | |
| for neuron_idx in tested_neurons: | |
| ko = results_data['individual'][neuron_idx]['knockout'] | |
| iso = results_data['individual'][neuron_idx]['isolation'] | |
| amp = results_data['individual'][neuron_idx]['amplify'] | |
| detailed_data.append({ | |
| 'Tipo': 'Individual', | |
| 'Neurona(s)': f"Filtro {neuron_idx}", | |
| 'Experimento': 'Knockout', | |
| 'Predicción': get_imagenet_class_name(ko['prediction']), | |
| 'Confianza': f"{ko['confidence']:.1%}", | |
| 'Δ': f"{(ko['confidence'] - results['confidence']):+.1%}", | |
| 'Estado': '✅ Mantiene' if ko['prediction'] == results['prediction'] else '❌ Cambió' | |
| }) | |
| detailed_data.append({ | |
| 'Tipo': 'Individual', | |
| 'Neurona(s)': f"Filtro {neuron_idx}", | |
| 'Experimento': 'Aislamiento', | |
| 'Predicción': get_imagenet_class_name(iso['prediction']), | |
| 'Confianza': f"{iso['confidence']:.1%}", | |
| 'Δ': f"{(iso['confidence'] - results['confidence']):+.1%}", | |
| 'Estado': '✅ Mantiene' if ko['prediction'] == results['prediction'] else '❌ Cambió' | |
| }) | |
| detailed_data.append({ | |
| 'Tipo': 'Individual', | |
| 'Neurona(s)': f"Filtro {neuron_idx}", | |
| 'Experimento': 'Amplificación', | |
| 'Predicción': get_imagenet_class_name(amp['prediction']), | |
| 'Confianza': f"{amp['confidence']:.1%}", | |
| 'Δ': f"{(amp['confidence'] - results['confidence']):+.1%}", | |
| 'Estado': '✅ Mantiene' if ko['prediction'] == results['prediction'] else '❌ Cambió' | |
| }) | |
| # Agregar datos grupales si existen | |
| if len(tested_neurons) > 1 and 'group' in results_data: | |
| group_ko = results_data['group']['knockout'] | |
| group_iso = results_data['group']['isolation'] | |
| group_amp = results_data['group']['amplify'] | |
| neurons_str = f"{len(tested_neurons)} neuronas" | |
| detailed_data.append({ | |
| 'Tipo': 'Grupal', | |
| 'Neurona(s)': neurons_str, | |
| 'Experimento': 'Knockout', | |
| 'Predicción': get_imagenet_class_name(group_ko['prediction']), | |
| 'Confianza': f"{group_ko['confidence']:.1%}", | |
| 'Δ': f"{(group_ko['confidence'] - results['confidence']):+.1%}", | |
| 'Estado': '✅ Mantiene' if ko['prediction'] == results['prediction'] else '❌ Cambió' | |
| }) | |
| detailed_data.append({ | |
| 'Tipo': 'Grupal', | |
| 'Neurona(s)': neurons_str, | |
| 'Experimento': 'Aislamiento', | |
| 'Predicción': get_imagenet_class_name(group_iso['prediction']), | |
| 'Confianza': f"{group_iso['confidence']:.1%}", | |
| 'Δ': f"{(group_iso['confidence'] - results['confidence']):+.1%}", | |
| 'Estado': '✅ Mantiene' if ko['prediction'] == results['prediction'] else '❌ Cambió' | |
| }) | |
| detailed_data.append({ | |
| 'Tipo': 'Grupal', | |
| 'Neurona(s)': neurons_str, | |
| 'Experimento': 'Amplificación', | |
| 'Predicción': get_imagenet_class_name(group_amp['prediction']), | |
| 'Confianza': f"{group_amp['confidence']:.1%}", | |
| 'Δ': f"{(group_amp['confidence'] - results['confidence']):+.1%}", | |
| 'Estado': '✅ Mantiene' if ko['prediction'] == results['prediction'] else '❌ Cambió' | |
| }) | |
| df_detailed = pd.DataFrame(detailed_data) | |
| st.dataframe( | |
| df_detailed, use_container_width=True, hide_index=True) | |
| # Botón de descarga | |
| csv = df_detailed.to_csv(index=False).encode('utf-8') | |
| st.download_button( | |
| label="📥 Descargar resultados (CSV)", | |
| data=csv, | |
| file_name="ablation_results.csv", | |
| mime="text/csv" | |
| ) | |
| # =============================================================== | |
| # SECCIÓN 4: INTERPRETACIÓN Y CONCLUSIONES | |
| # =============================================================== | |
| st.markdown("---") | |
| st.markdown("### 💡 Interpretación y Conclusiones") | |
| # Análisis automático | |
| col_concl1, col_concl2 = st.columns(2) | |
| with col_concl1: | |
| st.markdown("#### 🏆 Neuronas Más Importantes") | |
| # Encontrar neurona con mayor impacto en knockout | |
| max_ko_impact = 0 | |
| most_important = None | |
| for neuron_idx in tested_neurons: | |
| ko_change = abs( | |
| results_data['individual'][neuron_idx]['knockout']['confidence'] - results['confidence']) | |
| if ko_change > max_ko_impact: | |
| max_ko_impact = ko_change | |
| most_important = neuron_idx | |
| if most_important: | |
| st.success(f"🥇 **Filtro {most_important}**") | |
| st.caption(f"Impacto en knockout: {max_ko_impact:.1%}") | |
| # Lista de neuronas críticas | |
| critical_neurons = [ | |
| n for n in tested_neurons | |
| if abs(results_data['individual'][n]['knockout']['confidence'] - results['confidence']) > 0.1 | |
| ] | |
| if critical_neurons: | |
| st.info(f"⚡ **Neuronas críticas**: {critical_neurons}") | |
| else: | |
| st.warning("💤 Ninguna neurona individual es crítica") | |
| with col_concl2: | |
| st.markdown("#### 📋 Resumen Ejecutivo") | |
| # Contar tipos de neuronas | |
| important = sum([ | |
| 1 for n in tested_neurons | |
| if abs(results_data['individual'][n]['knockout']['confidence'] - results['confidence']) > 0.1 | |
| ]) | |
| determinant = sum([ | |
| 1 for n in tested_neurons | |
| if results_data['individual'][n]['isolation']['confidence'] > 0.3 | |
| ]) | |
| effective_amp = sum([ | |
| 1 for n in tested_neurons | |
| if (results_data['individual'][n]['amplify']['confidence'] - results['confidence']) > 0.05 | |
| ]) | |
| st.metric("Neuronas Importantes", | |
| f"{important}/{len(tested_neurons)}") | |
| st.metric("Neuronas Determinantes", | |
| f"{determinant}/{len(tested_neurons)}") | |
| st.metric("Amplificación Efectiva", | |
| f"{effective_amp}/{len(tested_neurons)}") | |
| # Recomendación final | |
| st.markdown("---") | |
| if len(tested_neurons) > 1 and 'group' in results_data: | |
| group_ko_change = abs( | |
| results_data['group']['knockout']['confidence'] - results['confidence']) | |
| if group_ko_change > 0.2: | |
| st.error( | |
| "🔥 **Conclusión**: Este grupo de neuronas es **CRÍTICO** para la predicción. " | |
| "Eliminarlas cambia drásticamente el resultado del modelo." | |
| ) | |
| elif group_ko_change > 0.1: | |
| st.warning( | |
| "⚡ **Conclusión**: Este grupo de neuronas es **IMPORTANTE**. " | |
| "Contribuyen significativamente a la predicción final." | |
| ) | |
| else: | |
| st.success( | |
| "✅ **Conclusión**: Este grupo de neuronas es **REDUNDANTE**. " | |
| "El modelo puede compensar su ausencia fácilmente." | |
| ) | |
| else: | |
| # Conclusión individual | |
| if most_important and max_ko_impact > 0.1: | |
| st.warning( | |
| f"⚡ **Conclusión**: El Filtro {most_important} es importante " | |
| f"(impacto: {max_ko_impact:.1%}), pero no crítico para la predicción." | |
| ) | |
| else: | |
| st.success( | |
| "✅ **Conclusión**: Las neuronas seleccionadas son redundantes. " | |
| "El modelo distribuye la información en múltiples neuronas." | |
| ) | |
| else: | |
| st.info( | |
| "👆 Por favor, carga una imagen y presiona 'Analizar Activaciones' para comenzar.") | |
| # Footer | |
| st.markdown("---") | |
| st.caption(""" | |
| **Interpretability III** - Dashboard de análisis de activaciones CNN | |
| Modelos disponibles: ResNet18, AlexNet | Desarrollado con Streamlit y PyTorch | |
| """) | |