6. Justificación de decisiones técnicas
Cada decisión importante del proyecto, con su alternativa, el porqué de la elección y los trade-offs. Es el material directo para responder "¿por qué hicieron X y no Y?" en una defensa.
6.1. ¿Por qué estas arquitecturas (ligeras) y no una ResNet-50 o un ViT grande?
Decisión: EfficientNet-B0/Lite0, MobileNetV3, ShuffleNetV2, GhostNetV2, FastViT-T8.
Razón: la restricción de despliegue manda. El objetivo es correr offline en un teléfono de gama media (≤20 MB, ≤300 ms, Snapdragon 6xx). Una ResNet-50 (~98 MB) o un ViT-Base (~330 MB) no caben ni cumplen la latencia. Todas las arquitecturas elegidas fueron diseñadas para edge/móvil.
| Alternativa | Por qué se descartó |
|---|---|
| ResNet-50 / ResNet-101 | Demasiado grande y lenta para móvil |
| ViT-Base / Large | Requiere muchos más datos y cómputo; pesado |
| Modelo custom desde cero | Sin transfer learning, sobreajustaría con 25k imágenes |
Trade-off aceptado: algo menos de capacidad máxima a cambio de viabilidad de despliegue. Los resultados (macro-F1 >0.91) muestran que la capacidad es suficiente para la tarea.
6.2. ¿Por qué transfer learning y no entrenar desde cero?
Decisión: cargar pesos de ImageNet y reemplazar la cabeza.
Razón: con ~25k imágenes (y clases minoritarias de cientos), entrenar desde cero sobreajustaría severamente. ImageNet aporta features visuales genéricas ya aprendidas. Ver fundamentos: transfer learning y el experimento hipotético en ¿qué pasaría si...?.
6.3. ¿Por qué resolución 224×224?
Decisión: target_size: [224, 224] (con excepciones: FastViT 256, EfficientNet-B4 380).
Razón: 224 es el estándar de ImageNet con el que se preentrenaron los modelos. Usar la resolución de preentrenamiento aprovecha mejor los pesos. Además, menor resolución = menos cómputo y memoria = más viable en móvil.
Trade-off: más resolución podría capturar lesiones finas mejor, pero cuadruplicaría el cómputo (área) y alejaría de los pesos preentrenados. El sistema respeta la resolución nativa de cada modelo vía input_sizes.py, que es el equilibrio correcto.
6.4. ¿Por qué Resize directo (con distorsión de aspecto) y no Resize+CenterCrop?
Decisión: T.Resize(target_size) que distorsiona la relación de aspecto, en vez de recortar al centro.
Razón: un CenterCrop descartaría los bordes de la hoja, y justo en los bordes/márgenes aparecen síntomas clave (necrosis marginal por potasio, puntas moradas por fósforo). Distorsionar el aspecto preserva toda la hoja. Además, es consistente entre train/eval/inferencia (el mismo Resize en los tres), evitando skew. Es una decisión de dominio deliberada, marcada explícitamente en el código para que nadie la "corrija".
6.5. ¿Por qué AdamW con LR 1e-4?
Decisión: AdamW, lr=1e-4, weight_decay=1e-4, LR constante.
Razón:
- AdamW converge rápido y robusto sin schedule complejo → ideal para comparar muchas arquitecturas rápido y barato (el propósito de los baselines). El weight decay desacoplado da regularización correcta.
- LR 1e-4 es el rango típico de fine-tuning (no de entrenar desde cero, que usaría LR mayor): partimos de pesos buenos, así que pasos pequeños evitan destruirlos.
- LR constante por comparabilidad: todos los baselines bajo las mismas condiciones. Un scheduler es la primera mejora para el pipeline principal.
Alternativa: SGD+momentum con cosine schedule podría exprimir algo más de precisión final, a costa de más tuning y épocas. Se prefirió la eficiencia comparativa. Ver fundamentos: optimizadores.
6.6. ¿Por qué macro-F1 como métrica de selección y no accuracy?
Decisión: seleccionar best.pth por val macro-F1; reportar macro-F1 como métrica principal.
Razón: el dataset está fuertemente desbalanceado. Accuracy premiaría acertar healthy (mayoritaria) e ignorar las deficiencias (minoritarias) — justo las que más importan al agricultor. Macro-F1 pesa todas las clases igual. Ver fundamentos: métricas.
6.7. ¿Por qué balancear con WeightedRandomSampler y no ponderar la loss?
Decisión: WeightedRandomSampler (sobremuestreo de minoritarias) + augmentation asimétrico, con CrossEntropyLoss sin pesos.
Razón: hay dos formas clásicas de atacar el desbalance: (a) muestrear más las clases raras (sampler), (b) penalizar más sus errores (loss ponderada). Hacer ambas sería doble compensación: sobre-corregiría hacia las minoritarias, degradando las mayoritarias. El proyecto elige el sampler porque, combinado con el augmentation agresivo de minoritarias, aporta también variedad (no solo repetición), reduciendo el sobreajuste a las pocas imágenes raras.
Detalle elegante: el sampler se desactiva solo si el split ya está balanceado (build_weighted_sampler devuelve None), evitando reducir la cobertura por época innecesariamente. Ver datos: sampler.
| Estrategia | Ventaja | Riesgo |
|---|---|---|
| Sampler (elegida) | Balance + variedad vía augmentation | Repite imágenes raras (mitigado por augmentation) |
| Loss ponderada | Simple, sin cambiar el muestreo | Puede desestabilizar; no aporta variedad |
| Ambas | — | Doble compensación (evitado) |
6.8. ¿Por qué deduplicar por SHA-256 y estratificar por clase+entorno?
Decisión: dedup exacta por hash de contenido; split estratificado por label+environment.
Razón:
- Dedup por SHA-256: el dataset consolida 8 fuentes que se solapan (la misma imagen de PlantVillage puede aparecer en varios). Un duplicado en train y test infla las métricas (el modelo "recuerda" la respuesta). El hash de contenido detecta duplicados exactos independientemente del nombre de archivo.
- Estratificar por clase+entorno: garantiza que la mezcla lab/real se preserve en los tres splits, evaluando en el mismo dominio (campo real) que el de despliegue. Ver splitter.
Limitación honesta: la dedup es exacta (bit-a-bit), no perceptual. Dos imágenes casi idénticas (recorte, recompresión JPEG) pasarían como distintas. Es un riesgo conocido. Ver riesgos.
6.9. ¿Por qué dos pipelines (baselines + principal) en vez de uno?
Decisión: train_baselines.py (funcional) y train.py (esqueleto), compartiendo infraestructura.
Razón: separan dos objetivos distintos. Baselines = comparar muchas arquitecturas rápido y barato para elegir candidatas (LR constante, config simple, 30 épocas). Principal = entrenar el modelo final elegido con todo el rigor (schedulers, fine-tuning por fases, más épocas), que aún está por implementar. Compartir la capa de datos/modelos evita divergencia. Ver entrenamiento.
6.10. ¿Por qué explicabilidad post-hoc y desacoplada del entrenamiento?
Decisión: LIME y Grad-CAM se corren después, como paso separado (antes corrían automáticamente al entrenar).
Razón: desacoplar permite (a) entrenar rápido sin el costo de LIME cada vez; (b) re-explicar cualquier checkpoint viejo sin reentrenar; (c) iterar en la explicabilidad independientemente. Persistir .json/.npy permite reanálisis sin re-ejecutar LIME. Que sea post-hoc (no un modelo intrínsecamente interpretable) es lo estándar para CNNs: la interpretabilidad intrínseca sacrificaría precisión.
6.11. ¿Por qué usar dos técnicas de XAI y no una?
Para triangular: LIME (perturbación, model-agnostic) y Grad-CAM (gradientes, usa la estructura real). Si coinciden, confianza alta; si divergen, la explicación es dudosa. Frente a un jurado, ofrecer dos métodos complementarios es más robusto que uno solo. Ver explicabilidad.
6.12. ¿Por qué versionar runs por timestamp y no sobrescribir?
Decisión: cada run en outputs/<pipeline>/<modelo>/<timestamp>/, con latest.json apuntando al último.
Razón: reproducibilidad y trazabilidad de experimentos. Nunca pierdes un run previo; puedes comparar históricos. summary.json por run guarda hiperparámetros + mapeo de clases exacto, evitando el bug de rótulos permutados. Ver common.py.
6.13. Resumen: matriz de decisiones
| Decisión | Elegido | Alternativa principal | Driver |
|---|---|---|---|
| Arquitecturas | Ligeras (edge) | ResNet/ViT grande | Despliegue móvil |
| Pesos iniciales | Transfer learning | Desde cero | Dataset pequeño |
| Resolución | 224 (nativa por modelo) | 384+ | Preentrenamiento + móvil |
| Redimensionado | Resize directo | Resize+Crop | Preservar bordes de la hoja |
| Optimizador | AdamW, LR 1e-4 const. | SGD+cosine | Comparar rápido |
| Métrica | Macro-F1 | Accuracy | Desbalance |
| Balanceo | Sampler + aug | Loss ponderada | Evitar doble compensación |
| Dedup | SHA-256 exacto | Perceptual | Fuga de datos + simplicidad |
| XAI | LIME + Grad-CAM post-hoc | SHAP / intrínseca | Confianza + costo |
Siguiente
Los valores concretos de cada perilla y qué pasa al moverla: Hiperparámetros.