- Introducción
- Arquitectura del modelo
- Configuración del experimento
- Entrenamiento
- Resultados
- Análisis
- Conclusiones y trabajo futuro
ResNet-50 es la variante de 50 capas de la familia ResNet propuesta por He, Zhang, Ren y Sun (2016) en "Deep Residual Learning for Image Recognition" (CVPR 2016). Su contribución central es la conexión residual (skip connection): en lugar de aprender la transformación completa
Este mecanismo resuelve el problema de la degradación del gradiente en redes muy profundas y permitió entrenar redes de cientos de capas por primera vez de forma efectiva. ResNet-50 sigue siendo, una década después de su publicación, un punto de referencia canónico en visión por computadora: ofrece una relación capacidad/costo favorable (~25 M parámetros) y pesos preentrenados de alta calidad (IMAGENET1K_V2).
En este proyecto se usa como modelo de referencia CNN clásica, en contraste con las arquitecturas más modernas (EfficientNet-B3, Swin-T, MaxViT-T) del mismo pipeline.
| Índice | Clase | Descripción |
|---|---|---|
| 0 | Elliptical | Elípticas suaves, sin estructura interna visible |
| 1 | Lenticular | Discos con abultamiento central, sin brazos espirales |
| 2 | Spiral | Espirales con brazos bien definidos |
| 3 | Barred_Spiral | Espirales con barra central |
| 4 | Edge_on | Galaxias vistas de canto (disco fino visible) |
| 5 | Irregular | Morfología perturbada o asimétrica |
La motivación de ResNet parte de una observación empírica: agregar más capas a una red profunda sin skip connections degrada la precisión en entrenamiento (no solo en validación), lo que indica que el problema no es sobreajuste sino dificultad de optimización. La hipótesis es que aprender la función identidad
La formulación residual:
donde
con
ResNet-50 usa bloques Bottleneck en lugar de los bloques básicos de ResNet-18/34. Cada bloque Bottleneck apila tres convoluciones:
Entrada: (B, C_in, H, W)
│
├─────────────────────────────────────────────── [Shortcut]
│ │
│ Conv 1×1 (C_in → C_mid) │ Conv 1×1 (C_in → C_out, stride s)
│ BatchNorm + ReLU │ BatchNorm
│ │ [solo si C_in ≠ C_out o stride > 1]
│ Conv 3×3 (C_mid → C_mid, stride s, pad=1) │
│ BatchNorm + ReLU │
│ │
│ Conv 1×1 (C_mid → C_out) │
│ BatchNorm │
│ │
└──────────────────── (+) ────────────────────────
│
ReLU
│
Salida: (B, C_out, H', W')
Donde:
- C_mid = C_out / 4: la conv 1×1 inicial comprime los canales a un cuarto (el "bottleneck"), reduciendo el costo de la conv 3×3 central.
- stride s: se aplica en la conv 3×3 del primer bloque de cada stage (s=2), reduciendo la resolución espacial.
- Expansión ×4: cada stage expande los canales por un factor de 4 respecto al ancho interno.
- ReLU: se aplica después de la suma residual, no antes.
Comparación de parámetros frente al bloque básico (dos conv 3×3 en series):
| Bloque | Conv 1 | Conv 2 | Conv 3 | Params (C=256) |
|---|---|---|---|---|
| Básico (2 capas) | 3×3 C→C | 3×3 C→C | — | 2 × 9 × C² ≈ 589K |
| Bottleneck (3 capas) | 1×1 C→C/4 | 3×3 C/4→C/4 | 1×1 C/4→C | C²/4 + 9C²/16 + C²/4 ≈ 147K |
El Bottleneck usa ~4× menos parámetros por bloque para la misma anchura de canal, permitiendo redes más profundas con costo similar.
La red completa tiene un stem inicial, cuatro stages de bloques Bottleneck y una cabeza de clasificación.
| Componente | Tipo | Canales salida | Resolución (224px entrada) | Bloques |
|---|---|---|---|---|
| Stem | Conv 7×7, stride 2, pad 3 | 64 | 112×112 | 1 |
| MaxPool | 3×3, stride 2, pad 1 | 64 | 56×56 | — |
| Layer 1 (S1) | Bottleneck ×3 (64 → 256) | 256 | 56×56 | 3 |
| Layer 2 (S2) | Bottleneck ×4 (128 → 512) | 512 | 28×28 | 4 |
| Layer 3 (S3) | Bottleneck ×6 (256 → 1024) | 1024 | 14×14 | 6 |
| Layer 4 (S4) | Bottleneck ×3 (512 → 2048) | 2048 | 7×7 | 3 |
| Head | AdaptiveAvgPool2d(1,1) + FC | 6 | 1×1 → 6 | — |
| Total | 16 |
Notas:
- El primer bloque de cada stage (S2–S4) usa stride 2 en la conv 3×3 para reducir la resolución.
- El primer bloque de cada stage también usa un shortcut de proyección para adaptar los canales.
- Todos los bloques usan BN + ReLU (no SiLU como EfficientNet).
- El stage S4 produce feature maps de 7×7 con 2048 canales — el vector de características más rico de la red.
La cabeza original de ResNet-50 (para ImageNet, 1000 clases) es simplemente:
AdaptiveAvgPool2d(1, 1)
Flatten
Linear(2048, 1000)
Para este proyecto se reemplaza la capa fc final por una nueva para 6 clases:
# torchvision expone la cabeza directamente como base_model.fc
in_features = base_model.fc.in_features # 2048
base_model.fc = nn.Linear(in_features, NUM_CLASSES) # 2048 → 6La cabeza final en producción:
AdaptiveAvgPool2d(output_size=(1, 1))
Flatten(start_dim=1)
Linear(in_features=2048, out_features=6, bias=True)
A diferencia de EfficientNet-B3, ResNet-50 no tiene Dropout en la cabeza — es uno de los diseños más austeros posibles: un único vector global promediado seguido de una proyección lineal.
| Grupo | Parámetros |
|---|---|
| Backbone (layers 1–4 + stem) | ~23,508,032 |
| Cabeza personalizada (Linear 2048→6) | ~12,294 |
| Total | ~23,520,326 |
El modelo original de ImageNet tenía ~25.56 M parámetros con la cabeza Linear(2048, 1000). Al reemplazarla por Linear(2048, 6) se eliminan ~2.05 M parámetros de la cabeza, resultando en ~23.52 M totales.
El entrenamiento usa learning rate diferencial: la cabeza se entrena con LR 10× mayor que el backbone, ya que sus pesos son aleatorios al inicio mientras que el backbone está preentrenado en ImageNet.
La red devuelve logits crudos — un vector de 6 valores reales sin ninguna función de activación final. No se aplica softmax ni sigmoid dentro del modelo.
Entrada : tensor (B, 3, 224, 224) float32
Salida : tensor (B, 6) float32 ← logits crudos
Ejemplo con B=1 (una imagen de galaxia espiral con barra):
logits = [ 0.18, -0.91, 1.34, 2.61, -1.82, -0.74 ]
índice : 0 1 2 3 4 5
clase : Ell. Len. Spiral Bar.Sp Edge_on Irreg.
Del logit a la probabilidad (softmax):
# Inferencia — pipeline completo
model.eval()
with torch.no_grad():
logits = model(x) # (B, 6) — salida directa
probs = torch.softmax(logits, dim=1) # (B, 6) — probabilidades [0, 1], suma = 1
pred = logits.argmax(dim=1) # (B,) — índice de clase ganadora
label = IDX_TO_CLASS[pred.item()] # str — nombre legible
# Para el ejemplo anterior:
# probs ≈ [0.079, 0.026, 0.252, 0.886, 0.011, 0.031] (suma ≈ 1)
# pred = 3 → 'Barred_Spiral' ✓Durante el entrenamiento — CrossEntropyLoss toma logits directamente:
criterion = nn.CrossEntropyLoss(weight=class_weights)
loss = criterion(logits, labels) # logits: (B, 6) labels: (B,) con índices 0-5
# CrossEntropyLoss = LogSoftmax + NLLLoss internamente
# No pasar softmax antes — causaría doble-softmax y loss incorrectoFlujo completo de datos a través del modelo:
(B, 3, 224, 224)
│
├─ Stem: Conv 7×7 (stride 2) + BN + ReLU + MaxPool (stride 2)
│ (B, 64, 56, 56)
├─ Layer1: Bottleneck ×3 (64 → 256)
│ (B, 256, 56, 56)
├─ Layer2: Bottleneck ×4 (128 → 512, stride 2 en bloque 1)
│ (B, 512, 28, 28)
├─ Layer3: Bottleneck ×6 (256 → 1024, stride 2 en bloque 1)
│ (B, 1024, 14, 14)
├─ Layer4: Bottleneck ×3 (512 → 2048, stride 2 en bloque 1)
│ (B, 2048, 7, 7) ← feature map final
├─ AdaptiveAvgPool2d(1, 1)
│ (B, 2048, 1, 1)
├─ Flatten
│ (B, 2048) ← vector de características
└─ Linear(2048 → 6) ← cabeza personalizada
(B, 6) ← LOGITS (salida final)
| Componente | Valor |
|---|---|
| GPU | NVIDIA RTX 5060 Ti (Blackwell GB206, 8 GB VRAM) |
| CUDA | 12.8 |
| PyTorch | ≥ 2.7 (--index-url https://download.pytorch.org/whl/cu128) |
| OS | Windows 11 |
| Entorno | Jupyter Notebook local |
sm_ target |
sm_120 (Blackwell) |
Tamaño del dataset:
| Split | Imágenes |
|---|---|
| Train | 77,789 |
| Val | 16,670 |
| Test | 16,670 |
| Total | 111,129 |
Parámetros de entrada:
| Parámetro | Valor |
|---|---|
CROP_SIZE |
280 px (center crop desde 424×424, margen 1.25× sobre 224) |
IMAGE_SIZE |
224 px (resize después del crop) |
BATCH_SIZE |
128 |
NUM_WORKERS |
0 (Windows — evita deadlock en Jupyter) |
pin_memory |
True (CUDA) — transfiere en DMA, sin bloquear CPU |
Comparación con EfficientNet-B3: B3 usó
CROP_SIZE=320(resolución original 300px, margen ×1.07) yBATCH_SIZE=64. ResNet-50 usa un crop más ajustado porque su resolución nativa es 224px, y un batch mayor porque su backbone es más eficiente en VRAM (~23.5M params vs ~10.7M pero con cabeza más ligera).
Ambos splits (train y val/test) comparten el preprocesamiento base determinista. La aumentación se aplica solo en train (sección 3.4).
| Paso | Operación | Motivo |
|---|---|---|
| 1 | CenterCrop(280) |
Elimina el borde negro de ~50px por lado (imágenes GZ2 son 424px pero la galaxia ocupa ~324px centrales) |
| 2 | Resize(224) |
Ajusta al tamaño nativo de preentrenamiento de ResNet-50 |
| 3 | ToTensor() |
Convierte PIL.Image [0, 255] a torch.Tensor [0.0, 1.0] |
| 4 | Normalize(mean, std) |
Normaliza por canal con estadísticas ImageNet |
Estadísticas de normalización ImageNet:
| Canal | Media | Desviación típica |
|---|---|---|
| R | 0.485 | 0.229 |
| G | 0.456 | 0.224 |
| B | 0.406 | 0.225 |
El tensor de entrada al modelo tiene valores aproximados en
Rationale del CenterCrop para GZ2:
Las imágenes del catálogo GZ2 (424×424 px) incluyen un borde negro de ~50px por lado: la información morfológica se concentra en los ~324px centrales. El crop de 280px captura el 86% del contenido útil (280/324 ≈ 0.86), descartando solo parte del halo exterior difuso. Esto es más conservador que el crop de B3 (320px), adecuado dado que ResNet-50 trabaja a 224px de resolución.
Aplicada únicamente al split de entrenamiento, después del preprocesamiento base:
| Aumentación | Parámetros | Motivo morfológico |
|---|---|---|
RandomHorizontalFlip |
p=0.5 | Las galaxias son simétricas especularmente |
RandomVerticalFlip |
p=0.5 | Sin orientación preferida en el espacio |
RandomRotation |
degrees=180 | Invarianza rotacional completa (galaxias no tienen "arriba") |
ColorJitter |
brightness=0.15, contrast=0.15 | Variación de exposición entre imágenes del survey |
No se usa RandomResizedCrop para no distorsionar la escala angular de las estructuras morfológicas (brazos espirales, barras).
El dataset GZ2 está fuertemente desbalanceado. Se calculan pesos inversamente proporcionales a la frecuencia de cada clase en el split de entrenamiento:
donde
| Clase |
|
|
|---|---|---|
| Elliptical | ~17,510 | 0.74 |
| Lenticular | ~11,900 | 1.09 |
| Spiral | ~17,510 | 0.74 |
| Barred_Spiral | ~17,510 | 0.74 |
| Edge_on | ~9,230 | 1.40 |
| Irregular | ~4,130 | 3.12 |
Los pesos se pasan a nn.CrossEntropyLoss(weight=class_weights), penalizando más los errores en clases minoritarias como Irregular y Edge_on.
Optimizador: AdamW con dos grupos de parámetros:
| Grupo | LR inicial | Weight Decay |
|---|---|---|
| Backbone (layers 1–4, stem) | 1e-4 |
1e-4 |
| Cabeza (fc) | 1e-3 |
1e-4 |
La relación LR_head / LR_backbone = 10 permite que la cabeza (inicializada aleatoriamente) converja rápido mientras el backbone (preentrenado en ImageNet) se ajusta suavemente.
Planificador: CosineAnnealingLR con decaimiento suave:
| Parámetro | Valor |
|---|---|
T_max |
30 (aunque entrenamiento paró en época 19) |
eta_min |
1e-6 |
| LR backbone en época 14 (mejor) | 5.57e-5 |
| LR head en época 14 (mejor) | 5.53e-4 |
El schedule se planificó para 30 épocas, pero el early stopping disparó en la época 19. En ese punto los LR habían decaído al 59% del máximo (coseno en
| Técnica | Configuración | Efecto |
|---|---|---|
| AMP (float16) | GradScaler + autocast |
~1.8× velocidad; VRAM reducida de ~8GB a ~4.5GB |
| Weight Decay | 1e-4 (AdamW) |
Penaliza pesos grandes, reduce sobreajuste |
| Early Stopping | patience=5 épocas sin mejora en val F1 |
Detuvo en época 19 (mejor en 14), evitó 11 épocas inútiles |
| BatchNorm | En cada bloque Bottleneck | Estabiliza activaciones, funciona como regularizador implícito |
| torch.compile | False (Windows — sin Triton) |
Desactivado automáticamente (os.name == 'nt') |
Nota sobre torch.compile en Windows:
torch.compile()requiere Triton como backend de compilación JIT, que no está disponible en Windows. En el entrenamiento de EfficientNet-B3 esto causó unTritonMissingerror. La solución implementada esUSE_COMPILE = os.name != 'nt', que desactiva la optimización automáticamente en Windows. El impacto en velocidad es ~10-15% por época.
Observaciones principales:
- La pérdida de validación sube consistentemente a partir de la época 8, mientras que la pérdida de entrenamiento sigue descendiendo — señal temprana de sobreajuste.
- El F1 de validación alcanza su máximo en la época 14 (0.6914) y no vuelve a ese nivel; el early stopping disparó correctamente 5 épocas después.
- El LR schedule está planificado para 30 épocas, pero el entrenamiento se detuvo en la 19, dejando el LR en ~59% de su valor inicial — el modelo paró antes de llegar al régimen de LR muy bajo, lo que confirma que el aprendizaje útil ya se había agotado.
| Época | Train Loss | Train F1 | Val Loss | Val F1 | Δ F1 (T–V) | Mejor | Tiempo |
|---|---|---|---|---|---|---|---|
| 1 | 0.9890 | 0.5817 | 0.8466 | 0.6345 | −0.053 | ✓ | 763s |
| 2 | 0.8352 | 0.6358 | 0.8418 | 0.6277 | +0.008 | — | 1219s |
| 3 | 0.7915 | 0.6502 | 0.7678 | 0.6570 | −0.007 | ✓ | 1105s |
| 4 | 0.7700 | 0.6593 | 0.7503 | 0.6641 | −0.005 | ✓ | 1082s |
| 5 | 0.7527 | 0.6637 | 0.7555 | 0.6590 | +0.005 | — | 935s |
| 6 | 0.7346 | 0.6693 | 0.7484 | 0.6628 | +0.006 | — | 527s |
| 7 | 0.7195 | 0.6763 | 0.7756 | 0.6680 | +0.008 | ✓ | 537s |
| 8 | 0.7051 | 0.6830 | 0.7446 | 0.6753 | +0.008 | ✓ | 579s |
| 9 | 0.6906 | 0.6883 | 0.7556 | 0.6689 | +0.019 | — | 579s |
| 10 | 0.6761 | 0.6918 | 0.7430 | 0.6734 | +0.018 | — | 579s |
| 11 | 0.6580 | 0.7002 | 0.7371 | 0.6789 | +0.021 | ✓ | 579s |
| 12 | 0.6416 | 0.7076 | 0.7681 | 0.6839 | +0.024 | ✓ | 578s |
| 13 | 0.6262 | 0.7137 | 0.7616 | 0.6738 | +0.040 | — | 566s |
| 14 | 0.6063 | 0.7242 | 0.7814 | 0.6914 | +0.033 | ✓ BEST | 563s |
| 15 | 0.5883 | 0.7302 | 0.7881 | 0.6812 | +0.049 | — | 609s |
| 16 | 0.5664 | 0.7403 | 0.7884 | 0.6798 | +0.061 | — | 600s |
| 17 | 0.5444 | 0.7503 | 0.8215 | 0.6822 | +0.068 | — | 506s |
| 18 | 0.5232 | 0.7614 | 0.8381 | 0.6836 | +0.078 | — | 499s |
| 19 | 0.5028 | 0.7723 | 0.8367 | 0.6816 | +0.091 | — | 502s |
Early stopping disparó tras la época 19 (5 épocas consecutivas sin superar val F1 = 0.6914).
Épocas 1–5: significativamente más lentas por calentamiento del caché de disco (ver §6.3).
Evaluación sobre el test set (16,670 imágenes no vistas durante entrenamiento ni validación), usando el checkpoint best.pth (época 14).
| Métrica | Valor |
|---|---|
| Macro F1 | 0.678 |
| Accuracy | 70.5% |
| Weighted F1 | 0.710 |
| Val F1 (mejor) | 0.6914 (época 14) |
Nota: La diferencia entre val F1 (0.6914) y test F1 (0.678) es esperable: son splits distintos con varianza estadística natural. La separación no indica data leakage — el test set nunca fue visto durante el desarrollo.
Comparación con EfficientNet-B3:
| Métrica | EfficientNet-B3 | ResNet-50 | Δ |
|---|---|---|---|
| Val F1 mejor | 0.6894 | 0.6914 | +0.002 ✓ |
| Época mejor / total | 16 / 30 | 14 / 19 | −11 épocas |
| Parámetros | ~10.7 M | ~23.5 M | +12.8 M |
| BATCH_SIZE | 64 | 128 | ×2 |
| Early stopping | No aplicado | ✓ épocas 15–19 | — |
| Tiempo total | ~17,500s | ~12,904s | −4,600s |
ResNet-50 supera marginalmente a B3 en val F1 (+0.002) y entrena más rápido en total gracias al early stopping, a pesar de tener más del doble de parámetros.
Los patrones más relevantes en la matrix normalizada (derecha):
- Elliptical → Lenticular (15%): la mayor fuente de confusión de Elliptical. Las galaxias elípticas de baja excentricidad y las lenticulares de poca inclinación son morfológicamente muy similares incluso para expertos humanos.
- Lenticular → Elliptical (20%): la segunda confusión más importante en la dirección opuesta.
- Lenticular → Irregular (13%): las lenticulares perturbadas o de baja calidad fotométrica son asignadas a Irregular.
- Spiral → Lenticular (11%): espirales de brazos tenues (tipo Sa/S0a) son confundidas con lenticulares.
- Irregular → Lenticular (14%) e Irregular → Barred_Spiral (11%): la forma irregular de muchas galaxias perurbadas puede asemejarse a morfologías más regulares cuando la imagen es ruidosa.
| Clase | Precisión | Recall | F1 | Support |
|---|---|---|---|---|
| Elliptical | 0.759 | 0.760 | 0.759 | 3,750 |
| Lenticular | 0.475 | 0.487 | 0.481 | 2,539 |
| Spiral | 0.754 | 0.643 | 0.694 | 3,750 |
| Barred_Spiral | 0.812 | 0.780 | 0.795 | 3,750 |
| Edge_on | 0.889 | 0.905 | 0.897 | 1,992 |
| Irregular | 0.354 | 0.589 | 0.442 | 889 |
| Macro avg | 0.674 | 0.694 | 0.678 | 16,670 |
| Weighted avg | 0.712 | 0.705 | 0.710 | 16,670 |
El modelo muestra un patrón de overfitting progresivo típico de fine-tuning sin dropout en la cabeza:
| Época | Train F1 | Val F1 | Brecha (T − V) | Observación |
|---|---|---|---|---|
| 1 | 0.582 | 0.635 | −0.053 | Val > Train (augmentaciones penalizan train F1) |
| 7 | 0.676 | 0.668 | +0.008 | Las curvas se cruzan |
| 14 | 0.724 | 0.691 | +0.033 | Mejor modelo |
| 19 | 0.772 | 0.682 | +0.091 | Early stopping — gap crítico |
En la época 1, val F1 > train F1. Esto es esperado y no indica un problema: el F1 de entrenamiento se mide sobre imágenes aumentadas (rotadas, flipped, color jitter), que son más difíciles. El F1 de validación usa solo center crop, por lo que el modelo "luce mejor" en val durante el calentamiento inicial.
El gap se estabilizó en torno a +0.033 en la época 14 y luego creció rápidamente. Si el entrenamiento hubiera continuado hasta la época 30 (como en B3), el gap habría alcanzado probablemente ~0.10–0.12, con una caída adicional del val F1 de ~0.005–0.010. Early stopping evitó ese escenario.
Lenticular — el caso más problemático (F1 = 0.481):
La clase Lenticular presenta la mayor confusión bidireccional con Elliptical:
| Confusión | Porcentaje | Causa morfológica |
|---|---|---|
| Lenticular → Elliptical | 20% (510/2539) | Galaxias S0 vistas de frente muestran perfil lumínico elíptico suave |
| Elliptical → Lenticular | 15% (546/3750) | Elípticas elongadas con abultamiento central parecen tener "disco" |
| Lenticular → Irregular | 13% (326/2539) | Lenticulares perturbadas o con mala relación señal/ruido |
Este patrón de confusión (recall Lenticular ≈ 0.49) es idéntico al de EfficientNet-B3 (0.49), lo que sugiere que la ambigüedad Lenticular/Elliptical es una limitación inherente a las características que ambas redes aprenden — no un problema de capacidad del modelo. La distinción es fundamentalmente difícil incluso para humanos sin espectroscopía.
Irregular — segunda clase más difícil (F1 = 0.442):
| Confusión | Porcentaje | Causa |
|---|---|---|
| Irregular → Lenticular | 14% (124/889) | Galaxias en interacción con morfología asimétrica |
| Irregular → Spiral | 11% (96/889) | Espirales perturbadas etiquetadas como irregulares |
| Irregular → Barred_Spiral | 11% (97/889) | Estructuras irregulares interpretadas como barras |
El alto recall de Irregular (0.589) pero baja precisión (0.354) indica que la red sobreestima irregulares — asigna muchas galaxias a esta clase cuando no sabe qué son.
Edge_on — la clase más sencilla (F1 = 0.897):
La morfología de canto (disco fino, bulbo central) es morfológicamente distintiva. El recall de 0.905 es el más alto del modelo. Sin embargo, cae respecto a EfficientNet-B3 (0.94 vs 0.90), posiblemente porque B3 captura mejor la elongación fina gracias a sus feature maps con mayor resolución efectiva (compound scaling vs resolución fija ResNet).
Las primeras 5 épocas muestran tiempos muy superiores al resto:
| Épocas | Tiempo promedio | Explicación |
|---|---|---|
| 1–5 | ~1,020s (~17 min) | Imágenes leídas por primera vez desde SSD/HDD |
| 6–19 | ~550s (~9 min) | Imágenes en caché RAM del SO (page cache) |
Con NUM_WORKERS=0 (Windows), el proceso principal lee cada imagen secuencialmente. En la primera pasada, el SO carga los ~77,789 JPEGs (~2.3 GB en disco) desde el almacenamiento físico a la RAM. A partir de la época 6, todas las imágenes ya están en la página de caché del sistema operativo y los accesos son casi instantáneos (latencia ~10× menor).
Este efecto fue más pronunciado que en EfficientNet-B3 porque:
- ResNet-50 fue el segundo modelo entrenado — la ventana de caché podría haberse parcialmente eviccionado entre sesiones.
BATCH_SIZE=128implica más accesos a disco por época queBATCH_SIZE=64.
Impacto en el tiempo total: las 5 épocas lentas añadieron ~2,350s extra al tiempo total. Sin ellas, el entrenamiento habría durado ~10,550s (~2h 56min) en lugar de ~12,904s (~3h 35min).
- Val F1 = 0.6914 — el mejor resultado hasta el momento, superando EfficientNet-B3 por +0.002 con casi el doble de parámetros y la misma resolución de entrada.
- Early stopping funcionó correctamente: el entrenamiento se detuvo en la época 19 (5 épocas sin mejora), ahorrando 11 épocas de cómputo. ResNet-50 converge más rápido que B3 en términos de épocas, aunque cada época es similar en tiempo.
- BATCH_SIZE=128 fue viable en 8GB VRAM con AMP — el modelo es eficiente en memoria a pesar de sus 23.5M parámetros.
- Comparación limpia con B3: misma resolución (224px), mismas aumentaciones, mismos splits — la comparación es justa.
- Lenticular sigue siendo el cuello de botella: F1 = 0.481, idéntico a B3. La ambigüedad morfológica entre S0 y E parece requerir arquitecturas capaces de capturar estructura global (e.g., mecanismos de atención).
- Sin Dropout en la cabeza: a diferencia de B3, ResNet-50 no tiene Dropout antes del clasificador. Añadirlo podría reducir el overfitting en la cabeza.
- torch.compile desactivado en Windows (sin Triton): se pierde ~10-15% de velocidad por época.
- ResNet es una arquitectura completamente local: cada neurona en los feature maps solo "ve" un receptive field local. Las galaxias con estructuras globales dispersas (brazos espirales, barras largas) podrían beneficiarse de mecanismos de atención global.
| Experimento | Motivación |
|---|---|
| Swin-T @ 308px | Atención windowed jerárquica — captura estructura global y local; posible mejora en Spiral/Lenticular |
| MaxViT-T @ 416px | Atención dual local+global; mayor resolución para detectar estructuras sutiles |
| Ensemble B3 + ResNet-50 | Ambos modelos tienen fortalezas complementarias (B3 mejor en Edge_on, ResNet en Barred_Spiral) |
| Dropout en cabeza ResNet | Añadir Dropout(0.3) antes de fc para reducir el gap de overfitting |
| Archivo | Contenido |
|---|---|
models/checkpoints/resnet50/best.pth |
Época 14 — val F1 = 0.6914 ← usar para inferencia |
models/checkpoints/resnet50/latest.pth |
Época 19 — val F1 = 0.6816 |
models/checkpoints/resnet50/epoch_005.pth … epoch_015.pth |
Hitos cada 5 épocas |
logs/resnet50_log.csv |
Historial completo de 19 épocas |
logs/resnet50_training_curves.png |
Curvas de loss, F1 y LR |
logs/resnet50_confusion_matrix.png |
Matriz de confusión (test set) |
Descarga del modelo: El checkpoint
best.pthestá disponible públicamente en Kaggle: https://www.kaggle.com/models/jeancdevx/galaxy-morph-resnet50

