""" 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 @st.cache_resource 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 @st.cache_resource 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 @st.cache_data 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""" """, 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(""" """, 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(""" """, 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 """)