neural_feature_visualization / modules /feature_generator.py
Gaston
up hugging
b2293d5
Raw
History Blame Contribute Delete
15.9 kB
"""
===================================================================
FEATURE_GENERATOR.PY - Generación de Patrones Sintéticos
===================================================================
Este módulo genera imágenes sintéticas que maximizan la activación
de neuronas específicas mediante técnicas de optimización (gradient ascent).
Técnicas implementadas:
1. Gradient Ascent en el espacio de píxeles
2. Regularizaciones (L2, Total Variation)
3. Transformaciones aleatorias (jitter, rotation, scale)
4. Blur periódico para suavizado
Uso:
generator = FeatureGenerator(model, 'features.0')
synthetic_img, history = generator.generate_pattern(neuron_idx=38)
===================================================================
"""
from config import (
IMAGE_SIZE,
FEATURE_ITERATIONS,
FEATURE_LR,
L2_DECAY,
TV_WEIGHT,
JITTER,
ROTATION_RANGE,
SCALE_RANGE,
BLUR_FREQUENCY,
BLUR_KERNEL_SIZE,
VERBOSE_FREQUENCY,
IMAGENET_MEAN,
IMAGENET_STD
)
import torch
import torch.nn as nn
import torch.nn.functional as F
from torchvision import transforms
import numpy as np
from typing import Dict, Tuple, Optional
from tqdm import tqdm
# Importar configuración
import sys
sys.path.append('..')
class FeatureGenerator:
"""
Generador de patrones sintéticos que maximizan activaciones neuronales.
Usa gradient ascent para optimizar una imagen desde ruido aleatorio
hasta una imagen que activa fuertemente una neurona objetivo.
"""
def __init__(
self,
model: nn.Module,
target_layer: str,
device: Optional[torch.device] = None
):
"""
Inicializa el generador.
Args:
model: Modelo de PyTorch en modo eval
target_layer: Nombre de la capa objetivo
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.activations = {}
self.hooks = []
self._register_hook()
# Normalización ImageNet
self.mean = torch.tensor(IMAGENET_MEAN).view(3, 1, 1).to(self.device)
self.std = torch.tensor(IMAGENET_STD).view(3, 1, 1).to(self.device)
print(f"✅ FeatureGenerator inicializado")
print(f" Capa objetivo: {target_layer}")
print(f" Device: {self.device}")
def _register_hook(self):
"""Registra hook para capturar activaciones."""
def hook_fn(module, input, output):
self.activations['target'] = output
# Buscar y registrar hook
for name, module in self.model.named_modules():
if name == self.target_layer:
handle = module.register_forward_hook(hook_fn)
self.hooks.append(handle)
print(f" ✓ Hook registrado en: {name}")
return
raise ValueError(f"❌ Capa '{self.target_layer}' no encontrada")
def generate_pattern(
self,
neuron_idx: int,
iterations: int = FEATURE_ITERATIONS,
lr: float = FEATURE_LR,
l2_decay: float = L2_DECAY,
tv_weight: float = TV_WEIGHT,
verbose: bool = False
) -> Tuple[np.ndarray, Dict]:
"""
Genera patrón sintético que maximiza activación de neurona.
Args:
neuron_idx: Índice de la neurona objetivo
iterations: Número de iteraciones de optimización
lr: Learning rate
l2_decay: Peso de regularización L2
tv_weight: Peso de Total Variation
verbose: Mostrar barra de progreso
Returns:
Tupla (imagen, historial):
- imagen: Array [H, W, 3] uint8 en [0, 255]
- historial: Dict con métricas por iteración
"""
if verbose:
print(f"\n{'='*70}")
print(f"🎨 Generando patrón para Neurona {neuron_idx}")
print(f"{'='*70}")
print(f"Iteraciones: {iterations}")
print(f"Learning rate: {lr}")
print(f"L2 decay: {l2_decay}")
print(f"TV weight: {tv_weight}")
# ===============================================================
# PASO 1: Inicializar imagen con ruido
# ===============================================================
img_tensor = torch.randn(
1, 3, IMAGE_SIZE[0], IMAGE_SIZE[1],
device=self.device,
requires_grad=True
)
img_tensor.data *= 0.1 # Escalar ruido inicial
# Optimizador
optimizer = torch.optim.Adam([img_tensor], lr=lr)
# Historial de métricas
history = {
'activation': [],
'l2_loss': [],
'tv_loss': [],
'total_loss': []
}
# ===============================================================
# PASO 2: Loop de optimización (Gradient Ascent)
# ===============================================================
iterator = range(iterations)
if verbose:
iterator = tqdm(iterator, desc="Optimizando")
for iteration in iterator:
# Reiniciar gradientes
optimizer.zero_grad()
# Aplicar transformaciones aleatorias
img_transformed = self._apply_transforms(img_tensor)
# Forward pass
_ = self.model(img_transformed)
# Obtener activación de neurona objetivo
acts = self.activations['target'] # [1, C, H, W]
neuron_activation = acts[0, neuron_idx].mean()
# -------------------------------------------------------
# Calcular pérdidas
# -------------------------------------------------------
# Loss principal: MAXIMIZAR activación (negativo para minimizar)
activation_loss = -neuron_activation
# Regularización L2: penaliza valores extremos
l2_loss = l2_decay * (img_tensor ** 2).mean()
# Total Variation: suaviza la imagen
tv_loss = tv_weight * self._total_variation(img_tensor)
# Loss total
total_loss = activation_loss + l2_loss + tv_loss
# -------------------------------------------------------
# Backward y actualizar
# -------------------------------------------------------
total_loss.backward()
optimizer.step()
# -------------------------------------------------------
# Aplicar blur periódicamente
# -------------------------------------------------------
if (iteration + 1) % BLUR_FREQUENCY == 0:
with torch.no_grad():
img_tensor.data = self._apply_blur(img_tensor.data)
# -------------------------------------------------------
# Registrar historial
# -------------------------------------------------------
history['activation'].append(neuron_activation.item())
history['l2_loss'].append(l2_loss.item())
history['tv_loss'].append(tv_loss.item())
history['total_loss'].append(total_loss.item())
# -------------------------------------------------------
# Log progreso (solo en modo verbose sin tqdm)
# -------------------------------------------------------
if not verbose and (iteration + 1) % VERBOSE_FREQUENCY == 0:
print(f"Iter {iteration+1:4d} | "
f"Act: {neuron_activation.item():7.4f} | "
f"L2: {l2_loss.item():7.4f} | "
f"TV: {tv_loss.item():7.4f}")
# ===============================================================
# PASO 3: Post-procesamiento
# ===============================================================
# Convertir a imagen visualizable
img_final = self._tensor_to_image(img_tensor)
if verbose:
print(f"\n✅ Generación completada")
print(f" Activación final: {history['activation'][-1]:.4f}")
print(f"{'='*70}\n")
return img_final, history
def _apply_transforms(
self,
img: torch.Tensor
) -> torch.Tensor:
"""
Aplica transformaciones aleatorias para robustez.
Estas transformaciones ayudan a:
- Evitar overfitting a patrones específicos
- Generar imágenes más naturales
- Mejorar generalización
Args:
img: Tensor [1, 3, H, W]
Returns:
Tensor transformado
"""
# Jitter: traslación aleatoria
if JITTER > 0:
ox = np.random.randint(-JITTER, JITTER + 1)
oy = np.random.randint(-JITTER, JITTER + 1)
img = torch.roll(img, shifts=(ox, oy), dims=(2, 3))
# Rotación aleatoria
if ROTATION_RANGE > 0:
angle = np.random.uniform(-ROTATION_RANGE, ROTATION_RANGE)
img = transforms.functional.rotate(img, angle)
# Escala aleatoria
if SCALE_RANGE[0] < SCALE_RANGE[1]:
scale = np.random.uniform(*SCALE_RANGE)
new_size = int(IMAGE_SIZE[0] * scale)
img = F.interpolate(
img,
size=(new_size, new_size),
mode='bilinear',
align_corners=False
)
# Center crop o pad
if new_size > IMAGE_SIZE[0]:
img = transforms.functional.center_crop(img, IMAGE_SIZE)
else:
pad = (IMAGE_SIZE[0] - new_size) // 2
img = F.pad(img, (pad, pad, pad, pad), mode='reflect')
return img
def _total_variation(self, img: torch.Tensor) -> torch.Tensor:
"""
Calcula Total Variation Loss para suavizar la imagen.
TV mide la variación entre píxeles vecinos:
- TV alto = imagen ruidosa
- TV bajo = imagen suave
Args:
img: Tensor [1, 3, H, W]
Returns:
Scalar con pérdida TV
"""
# Diferencias horizontales
tv_h = torch.abs(img[:, :, 1:, :] - img[:, :, :-1, :]).mean()
# Diferencias verticales
tv_v = torch.abs(img[:, :, :, 1:] - img[:, :, :, :-1]).mean()
return tv_h + tv_v
def _apply_blur(
self,
img: torch.Tensor,
kernel_size: int = BLUR_KERNEL_SIZE
) -> torch.Tensor:
"""
Aplica Gaussian blur para suavizar.
Args:
img: Tensor [1, 3, H, W]
kernel_size: Tamaño del kernel
Returns:
Tensor suavizado
"""
# Kernel gaussiano simple (promedio uniforme)
kernel = torch.ones(
1, 1, kernel_size, kernel_size,
device=self.device
)
kernel = kernel / kernel.sum()
# Aplicar a cada canal
blurred = torch.zeros_like(img)
for c in range(3):
channel = img[:, c:c+1, :, :]
blurred[:, c:c+1, :, :] = F.conv2d(
channel,
kernel,
padding=kernel_size // 2
)
return blurred
def _tensor_to_image(self, tensor: torch.Tensor) -> np.ndarray:
"""
Convierte tensor a imagen visualizable [0, 255].
Args:
tensor: Tensor [1, 3, H, W] normalizado
Returns:
Array numpy [H, W, 3] uint8 en [0, 255]
"""
img = tensor.detach().cpu().squeeze(0) # [3, H, W]
# Desnormalizar (ImageNet)
mean = torch.tensor(IMAGENET_MEAN).view(3, 1, 1)
std = torch.tensor(IMAGENET_STD).view(3, 1, 1)
img = img * std + mean
# Clip a [0, 1]
img = torch.clamp(img, 0, 1)
# A numpy [H, W, 3]
img = img.permute(1, 2, 0).numpy()
# A rango [0, 255]
img = (img * 255).astype(np.uint8)
return img
def cleanup(self):
"""Limpia hooks y libera recursos."""
for hook in self.hooks:
hook.remove()
self.hooks.clear()
self.activations.clear()
print("🧹 FeatureGenerator limpiado")
# ===================================================================
# FUNCIONES DE UTILIDAD
# ===================================================================
def compare_activations(
model: nn.Module,
target_layer: str,
real_image_tensor: torch.Tensor,
synthetic_image: np.ndarray,
neuron_idx: int,
device: torch.device
) -> Dict:
"""
Compara activaciones de imagen real vs sintética.
Args:
model: Modelo de PyTorch
target_layer: Capa objetivo
real_image_tensor: Tensor [1, 3, H, W] de imagen real
synthetic_image: Array [H, W, 3] de imagen sintética
neuron_idx: Índice de neurona
device: Device
Returns:
Dict con activaciones y mejora
"""
# Procesar imagen sintética
from modules.image_processor import ImageProcessor
processor = ImageProcessor(device=device)
# Convertir sintética a tensor
synthetic_pil = processor.image_to_pil(synthetic_image)
synthetic_tensor, _ = processor.load_and_preprocess(synthetic_pil)
# Crear hook temporal
activations = {}
def hook_fn(module, input, output):
activations['target'] = output
for name, module in model.named_modules():
if name == target_layer:
handle = module.register_forward_hook(hook_fn)
break
# Obtener activación de imagen real
with torch.no_grad():
_ = model(real_image_tensor)
real_act = activations['target'][0, neuron_idx].mean().item()
# Obtener activación de imagen sintética
activations.clear()
with torch.no_grad():
_ = model(synthetic_tensor)
synthetic_act = activations['target'][0, neuron_idx].mean().item()
# Limpiar hook
handle.remove()
return {
'real_activation': real_act,
'synthetic_activation': synthetic_act,
'improvement': (synthetic_act / max(real_act, 1e-8)) * 100
}
# ===================================================================
# TESTING
# ===================================================================
if __name__ == "__main__":
print("🧪 Testing FeatureGenerator...\n")
# Cargar modelo
print("1️⃣ Cargando modelo AlexNet...")
from torchvision import models
model = models.alexnet(pretrained=False)
model.eval()
# Crear generator
print("\n2️⃣ Creando FeatureGenerator...")
generator = FeatureGenerator(model, 'features.0')
# Generar patrón (pocas iteraciones para test)
print("\n3️⃣ Generando patrón sintético...")
img, history = generator.generate_pattern(
neuron_idx=0,
iterations=50, # Pocas iteraciones para test
verbose=True
)
print(f"\n4️⃣ Resultados:")
print(f" Imagen shape: {img.shape}")
print(f" Imagen dtype: {img.dtype}")
print(f" Imagen range: [{img.min()}, {img.max()}]")
print(f" Activación final: {history['activation'][-1]:.4f}")
# Cleanup
print("\n5️⃣ Limpiando...")
generator.cleanup()
print("\n✅ Testing completado!")