neural_feature_visualization / modules /neuron_analyzer.py
Gaston
up hugging
b2293d5
Raw
History Blame Contribute Delete
17.6 kB
"""
===================================================================
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
# Importar configuración
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):
# Guardar con la clave 'target' para acceso consistente
self.activations['target'] = output.detach()
# También con el nombre original
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:
# Mostrar capas disponibles para debug
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')
# Hook para capturar activaciones
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}'")
# Verificar dimensiones con un forward pass dummy
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
"""
# Asegurar que está en el device correcto
image_tensor = image_tensor.to(self.device)
# Asegurar dimensión de batch
if image_tensor.dim() == 3:
image_tensor = image_tensor.unsqueeze(0)
# Limpiar SOLO las activaciones, NO los hooks
self.hook.activations.clear()
# Forward pass
with torch.no_grad():
_ = self.model(image_tensor)
# Obtener activaciones usando la clave 'target'
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 = []
# Remover batch dimension
acts = activations[0] # [C, H, W]
for neuron_idx in range(acts.shape[0]):
neuron_act = acts[neuron_idx] # [H, W]
# Encontrar posición de máxima activación
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 # (y, x)
})
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)
"""
# Ordenar por criterio
sorted_stats = sorted(stats, key=lambda x: x[criterion], reverse=True)
# Extraer índices
ranked_indices = [s['neuron_idx'] for s in sorted_stats]
# Retornar top-k si se especifica
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]
"""
# Extraer mapa de la neurona
act_map = activations[0, neuron_idx].cpu().numpy()
# Normalizar a [0, 1]
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() # [C, H, W]
if method == 'max':
heatmap = acts.max(axis=0)
elif method == 'mean':
heatmap = acts.mean(axis=0)
elif method == 'weighted':
# Ponderar por activación máxima de cada canal
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")
# Normalizar
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, # Bicubic
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
"""
# Umbral: considerar píxeles con activación > 80% del máximo
threshold = activation_map.max() * 0.8
hot_mask = activation_map >= threshold
# Seguridad: si no hay píxeles calientes, usar máximo simple
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)
# Calcular centro de masa de la región caliente
# Esto pondera cada píxel por su valor de activación
cy, cx = center_of_mass(activation_map * hot_mask)
# Redondear a coordenadas enteras
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
# Calcular límites
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)
# Extraer ROI
if image.ndim == 3:
roi = image[y1:y2, x1:x2, :]
else:
roi = image[y1:y2, x1:x2]
# Redimensionar si no es del tamaño exacto
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")
# ===================================================================
# FUNCIONES DE UTILIDAD
# ===================================================================
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
# Aplicar colormap al heatmap
cmap = plt.get_cmap(colormap)
heatmap_colored = cmap(heatmap)[:, :, :3] # [H, W, 3]
# Combinar
overlay = image * (1 - alpha) + heatmap_colored * alpha
return np.clip(overlay, 0, 1)
# ===================================================================
# TESTING
# ===================================================================
if __name__ == "__main__":
print("🧪 Testing NeuronAnalyzer...\n")
# Cargar modelo de prueba
print("1️⃣ Cargando modelo AlexNet...")
from torchvision import models
model = models.alexnet(pretrained=False)
model.eval()
# Crear analyzer
print("\n2️⃣ Creando NeuronAnalyzer...")
analyzer = NeuronAnalyzer(model, 'features.0')
# Crear imagen de prueba
print("\n3️⃣ Creando imagen de prueba...")
test_img = torch.randn(1, 3, 224, 224)
# Extraer activaciones
print("\n4️⃣ Extrayendo activaciones...")
activations = analyzer.extract_activations(test_img)
print(f" Shape: {activations.shape}")
# Estadísticas
print("\n5️⃣ Calculando estadísticas...")
stats = analyzer.compute_neuron_statistics(activations)
print(f" Neuronas analizadas: {len(stats)}")
print(f" Ejemplo neurona 0: {stats[0]}")
# Ranking
print("\n6️⃣ Rankeando neuronas...")
top_neurons = analyzer.rank_neurons(stats, top_k=5)
print(f" Top 5 neuronas: {top_neurons}")
# Heatmap
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}]")
# Cleanup
print("\n8️⃣ Limpiando...")
analyzer.cleanup()
print("\n✅ Testing completado!")