| """
|
| ===================================================================
|
| NEURON_ANALYZER.PY - Análisis de Activaciones Neuronales
|
| ===================================================================
|
|
|
| Este módulo analiza las activaciones de neuronas en una capa específica
|
| cuando el modelo procesa una imagen.
|
|
|
| Funcionalidades principales:
|
| 1. Extracción de activaciones mediante hooks
|
| 2. Cálculo de estadísticas por neurona
|
| 3. Generación de mapas de calor (heatmaps)
|
| 4. Identificación de ROI (Region of Interest) de máxima activación
|
| 5. Ranking de neuronas por nivel de activación
|
|
|
| Uso:
|
| analyzer = NeuronAnalyzer(model, 'features.0')
|
| activations = analyzer.extract_activations(image_tensor)
|
| heatmap = analyzer.compute_heatmap(activations)
|
| ===================================================================
|
| """
|
|
|
| from config import ROI_SIZE
|
| import torch
|
| import torch.nn as nn
|
| import numpy as np
|
| from typing import Dict, List, Tuple, Optional
|
| from scipy.ndimage import zoom, center_of_mass
|
| from skimage.transform import resize
|
|
|
|
|
| import sys
|
| sys.path.append('..')
|
|
|
|
|
| class ActivationHook:
|
| """
|
| Clase auxiliar para registrar hooks y capturar activaciones.
|
| """
|
|
|
| def __init__(self, model: nn.Module, target_layer: str):
|
| """
|
| Args:
|
| model: Modelo de PyTorch
|
| target_layer: Nombre de la capa a capturar
|
| """
|
| self.model = model
|
| self.target_layer = target_layer
|
| self.activations = {}
|
| self.hooks = []
|
| self.hook_registered = False
|
|
|
| def _hook_fn(self, name: str):
|
| """Crea función hook para una capa específica."""
|
| def hook(module, input, output):
|
|
|
| self.activations['target'] = output.detach()
|
|
|
| self.activations[name] = output.detach()
|
| return hook
|
|
|
| def register_hook(self):
|
| """Registra el hook en la capa objetivo."""
|
| found = False
|
| for name, module in self.model.named_modules():
|
| if name == self.target_layer:
|
| handle = module.register_forward_hook(self._hook_fn(name))
|
| self.hooks.append(handle)
|
| self.hook_registered = True
|
| found = True
|
| print(f" ✓ Hook registrado exitosamente en: {name}")
|
| break
|
|
|
| if not found:
|
|
|
| print(f" ❌ Capa '{self.target_layer}' no encontrada")
|
| print(f" Capas disponibles:")
|
| for name, _ in self.model.named_modules():
|
| if len(name) > 0 and 'Conv2d' in str(type(_)):
|
| print(f" • {name}")
|
|
|
| return found
|
|
|
| def get_activations(self) -> Dict[str, torch.Tensor]:
|
| """Retorna las activaciones capturadas."""
|
| return self.activations
|
|
|
| def clear(self):
|
| """Limpia activaciones y hooks."""
|
| self.activations.clear()
|
| for hook in self.hooks:
|
| hook.remove()
|
| self.hooks.clear()
|
| self.hook_registered = False
|
|
|
| def clear_activations_only(self):
|
| """Limpia solo las activaciones, mantiene los hooks activos."""
|
| self.activations.clear()
|
|
|
|
|
| class NeuronAnalyzer:
|
| """
|
| Analizador de activaciones neuronales.
|
|
|
| Extrae y analiza las activaciones de una capa específica del modelo
|
| cuando procesa una imagen.
|
| """
|
|
|
| def __init__(
|
| self,
|
| model: nn.Module,
|
| target_layer: str,
|
| device: Optional[torch.device] = None
|
| ):
|
| """
|
| Inicializa el analizador.
|
|
|
| Args:
|
| model: Modelo de PyTorch en modo eval
|
| target_layer: Nombre de la capa a analizar (ej: 'features.0')
|
| device: Device para computación
|
| """
|
| self.model = model.eval()
|
| self.target_layer = target_layer
|
| self.device = device if device else torch.device('cpu')
|
|
|
|
|
| self.hook = ActivationHook(model, target_layer)
|
| success = self.hook.register_hook()
|
|
|
| if not success:
|
| raise ValueError(
|
| f"No se pudo registrar hook en capa '{target_layer}'")
|
|
|
|
|
| self._verify_layer()
|
|
|
| print(f"✅ NeuronAnalyzer inicializado")
|
| print(f" Capa objetivo: {target_layer}")
|
| print(f" Device: {self.device}")
|
|
|
| def _verify_layer(self):
|
| """Verifica que la capa capture activaciones correctamente."""
|
| dummy = torch.randn(1, 3, 224, 224).to(self.device)
|
| with torch.no_grad():
|
| _ = self.model(dummy)
|
|
|
| acts = self.hook.get_activations()
|
| if 'target' in acts:
|
| shape = acts['target'].shape
|
| print(f" Shape de activaciones: {shape}")
|
| self.num_channels = shape[1]
|
| else:
|
| available_keys = list(acts.keys())
|
| raise ValueError(
|
| f"Capa '{self.target_layer}' no capturó activaciones correctamente. "
|
| f"Claves disponibles: {available_keys}"
|
| )
|
|
|
| self.hook.clear_activations_only()
|
|
|
| def extract_activations(
|
| self,
|
| image_tensor: torch.Tensor
|
| ) -> torch.Tensor:
|
| """
|
| Extrae activaciones de la capa objetivo para una imagen.
|
|
|
| Args:
|
| image_tensor: Tensor [1, 3, H, W] normalizado
|
|
|
| Returns:
|
| Tensor [1, C, H', W'] con activaciones
|
| """
|
|
|
| image_tensor = image_tensor.to(self.device)
|
|
|
|
|
| if image_tensor.dim() == 3:
|
| image_tensor = image_tensor.unsqueeze(0)
|
|
|
|
|
| self.hook.activations.clear()
|
|
|
|
|
| with torch.no_grad():
|
| _ = self.model(image_tensor)
|
|
|
|
|
| acts = self.hook.get_activations()
|
|
|
| if 'target' not in acts:
|
| raise RuntimeError(
|
| f"No se capturaron activaciones para la capa '{self.target_layer}'. "
|
| f"Claves disponibles: {list(acts.keys())}"
|
| )
|
|
|
| activations = acts['target']
|
|
|
| return activations
|
|
|
| def compute_neuron_statistics(
|
| self,
|
| activations: torch.Tensor
|
| ) -> List[Dict]:
|
| """
|
| Calcula estadísticas para cada neurona (canal).
|
|
|
| Args:
|
| activations: Tensor [1, C, H, W] de activaciones
|
|
|
| Returns:
|
| Lista de diccionarios con estadísticas por neurona:
|
| - neuron_idx: Índice de la neurona
|
| - mean: Activación promedio
|
| - max: Activación máxima
|
| - std: Desviación estándar
|
| - max_position: (y, x) de activación máxima
|
| """
|
| stats = []
|
|
|
|
|
| acts = activations[0]
|
|
|
| for neuron_idx in range(acts.shape[0]):
|
| neuron_act = acts[neuron_idx]
|
|
|
|
|
| max_val, max_idx = neuron_act.flatten().max(0)
|
| max_pos = np.unravel_index(max_idx.cpu().item(), neuron_act.shape)
|
|
|
| stats.append({
|
| 'neuron_idx': neuron_idx,
|
| 'mean': neuron_act.mean().item(),
|
| 'max': max_val.item(),
|
| 'std': neuron_act.std().item(),
|
| 'max_position': max_pos
|
| })
|
|
|
| return stats
|
|
|
| def rank_neurons(
|
| self,
|
| stats: List[Dict],
|
| criterion: str = 'mean',
|
| top_k: Optional[int] = None
|
| ) -> List[int]:
|
| """
|
| Rankea neuronas por criterio de activación.
|
|
|
| Args:
|
| stats: Lista de estadísticas por neurona
|
| criterion: Criterio de ranking ('mean', 'max', 'std')
|
| top_k: Si especificado, retorna solo top-k neuronas
|
|
|
| Returns:
|
| Lista de índices de neuronas ordenados (mayor a menor)
|
| """
|
|
|
| sorted_stats = sorted(stats, key=lambda x: x[criterion], reverse=True)
|
|
|
|
|
| ranked_indices = [s['neuron_idx'] for s in sorted_stats]
|
|
|
|
|
| if top_k is not None:
|
| ranked_indices = ranked_indices[:top_k]
|
|
|
| return ranked_indices
|
|
|
| def get_neuron_activation_map(
|
| self,
|
| activations: torch.Tensor,
|
| neuron_idx: int
|
| ) -> np.ndarray:
|
| """
|
| Obtiene el mapa de activación de una neurona específica.
|
|
|
| Args:
|
| activations: Tensor [1, C, H, W]
|
| neuron_idx: Índice de la neurona
|
|
|
| Returns:
|
| Array numpy [H, W] con mapa de activación normalizado [0, 1]
|
| """
|
|
|
| act_map = activations[0, neuron_idx].cpu().numpy()
|
|
|
|
|
| if act_map.max() > act_map.min():
|
| act_map = (act_map - act_map.min()) / \
|
| (act_map.max() - act_map.min())
|
| else:
|
| act_map = np.zeros_like(act_map)
|
|
|
| return act_map
|
|
|
| def compute_heatmap(
|
| self,
|
| activations: torch.Tensor,
|
| method: str = 'max'
|
| ) -> np.ndarray:
|
| """
|
| Computa mapa de calor global combinando todas las neuronas.
|
|
|
| Args:
|
| activations: Tensor [1, C, H, W]
|
| method: Método de agregación:
|
| - 'max': Máximo por posición espacial
|
| - 'mean': Promedio por posición
|
| - 'weighted': Promedio ponderado por activación máxima
|
|
|
| Returns:
|
| Array numpy [H, W] normalizado [0, 1]
|
| """
|
| acts = activations[0].cpu().numpy()
|
|
|
| if method == 'max':
|
| heatmap = acts.max(axis=0)
|
| elif method == 'mean':
|
| heatmap = acts.mean(axis=0)
|
| elif method == 'weighted':
|
|
|
| max_per_channel = acts.max(axis=(1, 2), keepdims=True)
|
| weights = max_per_channel / (max_per_channel.sum() + 1e-8)
|
| heatmap = (acts * weights).sum(axis=0)
|
| else:
|
| raise ValueError(f"Método '{method}' no soportado")
|
|
|
|
|
| if heatmap.max() > heatmap.min():
|
| heatmap = (heatmap - heatmap.min()) / \
|
| (heatmap.max() - heatmap.min())
|
| else:
|
| heatmap = np.zeros_like(heatmap)
|
|
|
| return heatmap
|
|
|
| def resize_heatmap(
|
| self,
|
| heatmap: np.ndarray,
|
| target_size: Tuple[int, int]
|
| ) -> np.ndarray:
|
| """
|
| Redimensiona mapa de calor a tamaño objetivo.
|
|
|
| Args:
|
| heatmap: Array [H, W]
|
| target_size: (height, width) objetivo
|
|
|
| Returns:
|
| Heatmap redimensionado
|
| """
|
| return resize(
|
| heatmap,
|
| target_size,
|
| order=3,
|
| mode='reflect',
|
| anti_aliasing=True
|
| )
|
|
|
| def find_max_activation_region(
|
| self,
|
| activation_map: np.ndarray,
|
| roi_size: Tuple[int, int] = ROI_SIZE
|
| ) -> Tuple[int, int]:
|
| """
|
| Encuentra la región de máxima activación en el mapa.
|
|
|
| Usa el método 'weighted_area' que encuentra el centro de masa
|
| de la región con alta activación (más robusto que buscar un
|
| solo píxel máximo).
|
|
|
| Args:
|
| activation_map: Array [H, W] con mapa de activación
|
| roi_size: Tamaño del ROI (height, width)
|
|
|
| Returns:
|
| Tupla (y, x) con coordenadas del centro del ROI
|
| """
|
|
|
| threshold = activation_map.max() * 0.8
|
| hot_mask = activation_map >= threshold
|
|
|
|
|
| if hot_mask.sum() == 0:
|
| max_idx = activation_map.argmax()
|
| max_y, max_x = np.unravel_index(max_idx, activation_map.shape)
|
| return (max_y, max_x)
|
|
|
|
|
|
|
| cy, cx = center_of_mass(activation_map * hot_mask)
|
|
|
|
|
| return (int(round(cy)), int(round(cx)))
|
|
|
| def extract_roi(
|
| self,
|
| image: np.ndarray,
|
| center: Tuple[int, int],
|
| roi_size: Tuple[int, int] = ROI_SIZE
|
| ) -> np.ndarray:
|
| """
|
| Extrae ROI de la imagen centrado en una posición.
|
|
|
| Args:
|
| image: Array [H, W, 3] o [H, W]
|
| center: (y, x) centro del ROI
|
| roi_size: (height, width) del ROI
|
|
|
| Returns:
|
| ROI extraído [roi_h, roi_w, 3] o [roi_h, roi_w]
|
| """
|
| cy, cx = center
|
| h, w = roi_size
|
|
|
|
|
| y1 = max(0, cy - h // 2)
|
| y2 = min(image.shape[0], cy + h // 2)
|
| x1 = max(0, cx - w // 2)
|
| x2 = min(image.shape[1], cx + w // 2)
|
|
|
|
|
| if image.ndim == 3:
|
| roi = image[y1:y2, x1:x2, :]
|
| else:
|
| roi = image[y1:y2, x1:x2]
|
|
|
|
|
| if roi.shape[:2] != roi_size:
|
| if image.ndim == 3:
|
| roi = resize(roi, (roi_size[0], roi_size[1], image.shape[2]))
|
| else:
|
| roi = resize(roi, roi_size)
|
|
|
| return roi
|
|
|
| def get_roi_bounds(
|
| self,
|
| center: Tuple[int, int],
|
| roi_size: Tuple[int, int],
|
| image_size: Tuple[int, int]
|
| ) -> Tuple[int, int, int, int]:
|
| """
|
| Calcula los límites del ROI respetando bordes de imagen.
|
|
|
| Args:
|
| center: (y, x) centro del ROI
|
| roi_size: (height, width) del ROI
|
| image_size: (height, width) de la imagen
|
|
|
| Returns:
|
| Tupla (y1, x1, y2, x2) con límites del ROI
|
| """
|
| cy, cx = center
|
| h, w = roi_size
|
| img_h, img_w = image_size
|
|
|
| y1 = max(0, cy - h // 2)
|
| y2 = min(img_h, cy + h // 2)
|
| x1 = max(0, cx - w // 2)
|
| x2 = min(img_w, cx + w // 2)
|
|
|
| return (y1, x1, y2, x2)
|
|
|
| def cleanup(self):
|
| """Limpia hooks y libera recursos."""
|
| self.hook.clear()
|
| print("🧹 NeuronAnalyzer limpiado")
|
|
|
|
|
|
|
|
|
|
|
|
|
| def create_overlay_heatmap(
|
| image: np.ndarray,
|
| heatmap: np.ndarray,
|
| alpha: float = 0.5,
|
| colormap: str = 'jet'
|
| ) -> np.ndarray:
|
| """
|
| Crea un overlay de heatmap sobre imagen.
|
|
|
| Args:
|
| image: Array [H, W, 3] en [0, 1]
|
| heatmap: Array [H, W] en [0, 1]
|
| alpha: Transparencia del heatmap
|
| colormap: Nombre del colormap de matplotlib
|
|
|
| Returns:
|
| Imagen con overlay [H, W, 3] en [0, 1]
|
| """
|
| import matplotlib.pyplot as plt
|
|
|
|
|
| cmap = plt.get_cmap(colormap)
|
| heatmap_colored = cmap(heatmap)[:, :, :3]
|
|
|
|
|
| overlay = image * (1 - alpha) + heatmap_colored * alpha
|
|
|
| return np.clip(overlay, 0, 1)
|
|
|
|
|
|
|
|
|
|
|
|
|
| if __name__ == "__main__":
|
| print("🧪 Testing NeuronAnalyzer...\n")
|
|
|
|
|
| print("1️⃣ Cargando modelo AlexNet...")
|
| from torchvision import models
|
| model = models.alexnet(pretrained=False)
|
| model.eval()
|
|
|
|
|
| print("\n2️⃣ Creando NeuronAnalyzer...")
|
| analyzer = NeuronAnalyzer(model, 'features.0')
|
|
|
|
|
| print("\n3️⃣ Creando imagen de prueba...")
|
| test_img = torch.randn(1, 3, 224, 224)
|
|
|
|
|
| print("\n4️⃣ Extrayendo activaciones...")
|
| activations = analyzer.extract_activations(test_img)
|
| print(f" Shape: {activations.shape}")
|
|
|
|
|
| print("\n5️⃣ Calculando estadísticas...")
|
| stats = analyzer.compute_neuron_statistics(activations)
|
| print(f" Neuronas analizadas: {len(stats)}")
|
| print(f" Ejemplo neurona 0: {stats[0]}")
|
|
|
|
|
| print("\n6️⃣ Rankeando neuronas...")
|
| top_neurons = analyzer.rank_neurons(stats, top_k=5)
|
| print(f" Top 5 neuronas: {top_neurons}")
|
|
|
|
|
| print("\n7️⃣ Generando heatmap...")
|
| heatmap = analyzer.compute_heatmap(activations)
|
| print(f" Heatmap shape: {heatmap.shape}")
|
| print(f" Heatmap range: [{heatmap.min():.3f}, {heatmap.max():.3f}]")
|
|
|
|
|
| print("\n8️⃣ Limpiando...")
|
| analyzer.cleanup()
|
|
|
| print("\n✅ Testing completado!")
|
|
|