Gaston
up hugging
b2293d5
Raw
History Blame Contribute Delete
13.5 kB
"""
===================================================================
HELPERS.PY - Funciones Auxiliares
===================================================================
Este módulo contiene funciones de utilidad general que se usan
en múltiples partes de la aplicación.
Categorías:
1. Conversiones de formato
2. Validaciones
3. Formateo de texto
4. Funciones de IO
===================================================================
"""
import numpy as np
import torch
from typing import Union, Tuple, Optional, List
from pathlib import Path
import json
from datetime import datetime
# ===================================================================
# 1. CONVERSIONES DE FORMATO
# ===================================================================
def ensure_numpy(data: Union[np.ndarray, torch.Tensor]) -> np.ndarray:
"""
Asegura que los datos estén en formato numpy.
Args:
data: Array numpy o tensor PyTorch
Returns:
Array numpy
"""
if isinstance(data, torch.Tensor):
return data.detach().cpu().numpy()
return data
def ensure_tensor(
data: Union[np.ndarray, torch.Tensor],
device: Optional[torch.device] = None
) -> torch.Tensor:
"""
Asegura que los datos estén en formato tensor.
Args:
data: Array numpy o tensor PyTorch
device: Device objetivo (opcional)
Returns:
Tensor de PyTorch
"""
if isinstance(data, np.ndarray):
tensor = torch.from_numpy(data).float()
else:
tensor = data
if device is not None:
tensor = tensor.to(device)
return tensor
def normalize_range(
data: np.ndarray,
target_range: Tuple[float, float] = (0, 1)
) -> np.ndarray:
"""
Normaliza array a un rango específico.
Args:
data: Array a normalizar
target_range: Rango objetivo (min, max)
Returns:
Array normalizado
"""
min_val, max_val = target_range
# Normalizar a [0, 1] primero
data_min = data.min()
data_max = data.max()
if data_max - data_min > 1e-8:
normalized = (data - data_min) / (data_max - data_min)
else:
normalized = np.zeros_like(data)
# Escalar al rango objetivo
scaled = normalized * (max_val - min_val) + min_val
return scaled
def to_uint8(data: np.ndarray) -> np.ndarray:
"""
Convierte array a uint8 en rango [0, 255].
Args:
data: Array en cualquier rango
Returns:
Array uint8 en [0, 255]
"""
# Normalizar a [0, 1]
if data.max() > 1.0:
data = data / 255.0
# Escalar a [0, 255]
data = np.clip(data * 255, 0, 255).astype(np.uint8)
return data
# ===================================================================
# 2. VALIDACIONES
# ===================================================================
def validate_image_shape(
image: np.ndarray,
expected_channels: int = 3
) -> bool:
"""
Valida que una imagen tenga el shape correcto.
Args:
image: Array de imagen
expected_channels: Número esperado de canales
Returns:
True si es válida
"""
if image.ndim not in [2, 3]:
return False
if image.ndim == 3 and image.shape[2] != expected_channels:
return False
return True
def validate_activation_shape(
activations: torch.Tensor,
expected_dims: int = 4
) -> bool:
"""
Valida que las activaciones tengan el shape correcto.
Args:
activations: Tensor de activaciones
expected_dims: Número esperado de dimensiones
Returns:
True si es válida
"""
return activations.dim() == expected_dims
def is_valid_neuron_idx(
neuron_idx: int,
num_channels: int
) -> bool:
"""
Valida que un índice de neurona sea válido.
Args:
neuron_idx: Índice de la neurona
num_channels: Número total de canales
Returns:
True si es válido
"""
return 0 <= neuron_idx < num_channels
# ===================================================================
# 3. FORMATEO DE TEXTO
# ===================================================================
def format_number(num: float, decimals: int = 3) -> str:
"""
Formatea un número con decimales específicos.
Args:
num: Número a formatear
decimals: Número de decimales
Returns:
String formateado
"""
return f"{num:.{decimals}f}"
def format_percentage(value: float, decimals: int = 1) -> str:
"""
Formatea un valor como porcentaje.
Args:
value: Valor en rango [0, 1]
decimals: Número de decimales
Returns:
String con porcentaje (ej: "75.3%")
"""
return f"{value * 100:.{decimals}f}%"
def format_layer_name(layer_name: str) -> str:
"""
Formatea nombre de capa para display.
Args:
layer_name: Nombre técnico de la capa (ej: 'features.0')
Returns:
Nombre formateado (ej: 'Features Layer 0')
"""
parts = layer_name.split('.')
if len(parts) == 2:
return f"{parts[0].capitalize()} Layer {parts[1]}"
return layer_name
def format_model_name(model_name: str) -> str:
"""
Formatea nombre de modelo para display.
Args:
model_name: Nombre técnico (ej: 'alexnet')
Returns:
Nombre formateado (ej: 'AlexNet')
"""
# Casos especiales
special_cases = {
'alexnet': 'AlexNet',
'resnet18': 'ResNet-18',
'resnet50': 'ResNet-50',
'vgg16': 'VGG-16',
'vgg19': 'VGG-19'
}
return special_cases.get(model_name.lower(), model_name.capitalize())
# ===================================================================
# 4. FUNCIONES DE IO
# ===================================================================
def save_json(data: dict, filepath: Union[str, Path]):
"""
Guarda diccionario como JSON.
Args:
data: Diccionario a guardar
filepath: Ruta del archivo
"""
filepath = Path(filepath)
filepath.parent.mkdir(parents=True, exist_ok=True)
with open(filepath, 'w') as f:
json.dump(data, f, indent=2)
def load_json(filepath: Union[str, Path]) -> dict:
"""
Carga diccionario desde JSON.
Args:
filepath: Ruta del archivo
Returns:
Diccionario cargado
"""
with open(filepath, 'r') as f:
return json.load(f)
def get_timestamp() -> str:
"""
Obtiene timestamp actual formateado.
Returns:
String con timestamp (ej: '2025-01-15_14-30-45')
"""
return datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
def create_filename(
prefix: str,
suffix: str = '',
extension: str = 'png',
include_timestamp: bool = True
) -> str:
"""
Crea nombre de archivo con formato consistente.
Args:
prefix: Prefijo del nombre
suffix: Sufijo del nombre (opcional)
extension: Extensión del archivo
include_timestamp: Si incluir timestamp
Returns:
Nombre de archivo (ej: 'heatmap_neuron38_2025-01-15_14-30-45.png')
"""
parts = [prefix]
if suffix:
parts.append(suffix)
if include_timestamp:
parts.append(get_timestamp())
filename = '_'.join(parts) + f'.{extension}'
return filename
# ===================================================================
# 5. CÁLCULOS MATEMÁTICOS
# ===================================================================
def compute_improvement_percentage(
baseline: float,
improved: float
) -> float:
"""
Calcula porcentaje de mejora.
Args:
baseline: Valor base
improved: Valor mejorado
Returns:
Porcentaje de mejora
"""
if baseline < 1e-8:
return 0.0
return ((improved - baseline) / baseline) * 100
def safe_divide(
numerator: float,
denominator: float,
default: float = 0.0
) -> float:
"""
División segura que evita división por cero.
Args:
numerator: Numerador
denominator: Denominador
default: Valor por defecto si denominator es 0
Returns:
Resultado de la división o default
"""
if abs(denominator) < 1e-8:
return default
return numerator / denominator
def moving_average(
data: List[float],
window_size: int = 5
) -> List[float]:
"""
Calcula promedio móvil de una serie.
Args:
data: Lista de valores
window_size: Tamaño de la ventana
Returns:
Lista con promedios móviles
"""
if len(data) < window_size:
return data
result = []
for i in range(len(data)):
start = max(0, i - window_size + 1)
end = i + 1
window = data[start:end]
result.append(sum(window) / len(window))
return result
# ===================================================================
# 6. FUNCIONES DE DISPLAY
# ===================================================================
def print_section_header(title: str, width: int = 70):
"""
Imprime un header de sección formateado.
Args:
title: Título de la sección
width: Ancho del header
"""
print("\n" + "=" * width)
print(title.center(width))
print("=" * width + "\n")
def print_progress(
current: int,
total: int,
prefix: str = '',
suffix: str = '',
decimals: int = 1,
length: int = 50
):
"""
Imprime barra de progreso en consola.
Args:
current: Valor actual
total: Valor total
prefix: Texto antes de la barra
suffix: Texto después de la barra
decimals: Decimales en porcentaje
length: Longitud de la barra
"""
percent = ("{0:." + str(decimals) + "f}").format(100 *
(current / float(total)))
filled = int(length * current // total)
bar = '█' * filled + '-' * (length - filled)
print(f'\r{prefix} |{bar}| {percent}% {suffix}', end='')
# Nueva línea al completar
if current == total:
print()
# ===================================================================
# 7. FUNCIONES DE GEOMETRÍA
# ===================================================================
def compute_iou(
box1: Tuple[int, int, int, int],
box2: Tuple[int, int, int, int]
) -> float:
"""
Calcula Intersection over Union de dos cajas.
Args:
box1: (x1, y1, x2, y2)
box2: (x1, y1, x2, y2)
Returns:
IoU en [0, 1]
"""
x1_inter = max(box1[0], box2[0])
y1_inter = max(box1[1], box2[1])
x2_inter = min(box1[2], box2[2])
y2_inter = min(box1[3], box2[3])
# Área de intersección
inter_area = max(0, x2_inter - x1_inter) * max(0, y2_inter - y1_inter)
# Áreas de cada caja
box1_area = (box1[2] - box1[0]) * (box1[3] - box1[1])
box2_area = (box2[2] - box2[0]) * (box2[3] - box2[1])
# Union
union_area = box1_area + box2_area - inter_area
# IoU
iou = safe_divide(inter_area, union_area)
return iou
def clip_to_bounds(
coords: Tuple[int, int],
max_coords: Tuple[int, int]
) -> Tuple[int, int]:
"""
Clip coordenadas a límites de imagen.
Args:
coords: (y, x) coordenadas
max_coords: (max_y, max_x) límites
Returns:
Coordenadas clipeadas
"""
y = max(0, min(coords[0], max_coords[0] - 1))
x = max(0, min(coords[1], max_coords[1] - 1))
return (y, x)
# ===================================================================
# TESTING
# ===================================================================
if __name__ == "__main__":
print("🧪 Testing helpers...\n")
# Test conversiones
print("1️⃣ Testing conversiones...")
arr = np.random.rand(3, 224, 224)
tensor = ensure_tensor(arr)
print(f" Array → Tensor: {tensor.shape}")
arr_back = ensure_numpy(tensor)
print(f" Tensor → Array: {arr_back.shape}")
# Test normalización
print("\n2️⃣ Testing normalización...")
data = np.array([1, 2, 3, 4, 5])
normalized = normalize_range(data, (0, 1))
print(f" Original: {data}")
print(f" Normalizado: {normalized}")
# Test formateo
print("\n3️⃣ Testing formateo...")
print(f" Número: {format_number(3.14159265, decimals=2)}")
print(f" Porcentaje: {format_percentage(0.753)}")
print(f" Layer: {format_layer_name('features.0')}")
print(f" Model: {format_model_name('alexnet')}")
# Test cálculos
print("\n4️⃣ Testing cálculos...")
improvement = compute_improvement_percentage(10.0, 15.0)
print(f" Mejora: {improvement:.1f}%")
# Test filename
print("\n5️⃣ Testing filename...")
filename = create_filename('heatmap', 'neuron38', include_timestamp=False)
print(f" Filename: {filename}")
print("\n✅ Testing completado!")