| """
|
| ===================================================================
|
| 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
|
|
|
|
|
| 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')
|
|
|
|
|
| self.activations = {}
|
| self.hooks = []
|
| self._register_hook()
|
|
|
|
|
| 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
|
|
|
|
|
| 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}")
|
|
|
|
|
|
|
|
|
|
|
| img_tensor = torch.randn(
|
| 1, 3, IMAGE_SIZE[0], IMAGE_SIZE[1],
|
| device=self.device,
|
| requires_grad=True
|
| )
|
| img_tensor.data *= 0.1
|
|
|
|
|
| optimizer = torch.optim.Adam([img_tensor], lr=lr)
|
|
|
|
|
| history = {
|
| 'activation': [],
|
| 'l2_loss': [],
|
| 'tv_loss': [],
|
| 'total_loss': []
|
| }
|
|
|
|
|
|
|
|
|
|
|
| iterator = range(iterations)
|
| if verbose:
|
| iterator = tqdm(iterator, desc="Optimizando")
|
|
|
| for iteration in iterator:
|
|
|
| optimizer.zero_grad()
|
|
|
|
|
| img_transformed = self._apply_transforms(img_tensor)
|
|
|
|
|
| _ = self.model(img_transformed)
|
|
|
|
|
| acts = self.activations['target']
|
| neuron_activation = acts[0, neuron_idx].mean()
|
|
|
|
|
|
|
|
|
|
|
|
|
| activation_loss = -neuron_activation
|
|
|
|
|
| l2_loss = l2_decay * (img_tensor ** 2).mean()
|
|
|
|
|
| tv_loss = tv_weight * self._total_variation(img_tensor)
|
|
|
|
|
| total_loss = activation_loss + l2_loss + tv_loss
|
|
|
|
|
|
|
|
|
| total_loss.backward()
|
| optimizer.step()
|
|
|
|
|
|
|
|
|
| if (iteration + 1) % BLUR_FREQUENCY == 0:
|
| with torch.no_grad():
|
| img_tensor.data = self._apply_blur(img_tensor.data)
|
|
|
|
|
|
|
|
|
| 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())
|
|
|
|
|
|
|
|
|
| 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}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| """
|
|
|
| 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))
|
|
|
|
|
| if ROTATION_RANGE > 0:
|
| angle = np.random.uniform(-ROTATION_RANGE, ROTATION_RANGE)
|
| img = transforms.functional.rotate(img, angle)
|
|
|
|
|
| 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
|
| )
|
|
|
|
|
| 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
|
| """
|
|
|
| tv_h = torch.abs(img[:, :, 1:, :] - img[:, :, :-1, :]).mean()
|
|
|
|
|
| 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 = torch.ones(
|
| 1, 1, kernel_size, kernel_size,
|
| device=self.device
|
| )
|
| kernel = kernel / kernel.sum()
|
|
|
|
|
| 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)
|
|
|
|
|
| mean = torch.tensor(IMAGENET_MEAN).view(3, 1, 1)
|
| std = torch.tensor(IMAGENET_STD).view(3, 1, 1)
|
| img = img * std + mean
|
|
|
|
|
| img = torch.clamp(img, 0, 1)
|
|
|
|
|
| img = img.permute(1, 2, 0).numpy()
|
|
|
|
|
| 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")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| """
|
|
|
| from modules.image_processor import ImageProcessor
|
| processor = ImageProcessor(device=device)
|
|
|
|
|
| synthetic_pil = processor.image_to_pil(synthetic_image)
|
| synthetic_tensor, _ = processor.load_and_preprocess(synthetic_pil)
|
|
|
|
|
| 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
|
|
|
|
|
| with torch.no_grad():
|
| _ = model(real_image_tensor)
|
| real_act = activations['target'][0, neuron_idx].mean().item()
|
|
|
|
|
| activations.clear()
|
| with torch.no_grad():
|
| _ = model(synthetic_tensor)
|
| synthetic_act = activations['target'][0, neuron_idx].mean().item()
|
|
|
|
|
| handle.remove()
|
|
|
| return {
|
| 'real_activation': real_act,
|
| 'synthetic_activation': synthetic_act,
|
| 'improvement': (synthetic_act / max(real_act, 1e-8)) * 100
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
| if __name__ == "__main__":
|
| print("🧪 Testing FeatureGenerator...\n")
|
|
|
|
|
| print("1️⃣ Cargando modelo AlexNet...")
|
| from torchvision import models
|
| model = models.alexnet(pretrained=False)
|
| model.eval()
|
|
|
|
|
| print("\n2️⃣ Creando FeatureGenerator...")
|
| generator = FeatureGenerator(model, 'features.0')
|
|
|
|
|
| print("\n3️⃣ Generando patrón sintético...")
|
| img, history = generator.generate_pattern(
|
| neuron_idx=0,
|
| iterations=50,
|
| 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}")
|
|
|
|
|
| print("\n5️⃣ Limpiando...")
|
| generator.cleanup()
|
|
|
| print("\n✅ Testing completado!")
|
|
|