Skip to content

4.1. Configuración y rutas

Cubre config/dataset.yaml (el contrato de configuración) y src/config.py (resolución de rutas + semilla global). Es la base de todo el proyecto: casi todos los módulos leen de aquí.

config/dataset.yaml — la fuente única de verdad

Propósito

Centralizar todas las constantes de dominio del proyecto en un solo archivo declarativo, para que ningún valor de negocio (nombres de clases, tamaño de imagen, semilla, umbrales) esté hardcodeado en el código Python. Cambiar el comportamiento del pipeline debería ser, siempre que sea posible, editar este YAML.

Contenido completo comentado

yaml
paths:
  raw_dir: "clean"                    # subdirectorio de DATASET_ROOT donde vive el dataset limpio
  split_output_dir: "splits/seed_42"  # relativo a outputs/

dataset:
  target_size: [224, 224]   # (alto, ancho). 224 es el estándar de ImageNet.
  seed: 42                  # semilla global de reproducibilidad
  classes:                  # ⭐ ORDEN CANÓNICO: fija los índices de clase (class_to_idx)
    - common_rust           # 0
    - fall_armyworm         # 1
    - gray_leaf_spot        # 2
    - healthy               # 3
    - lethal_necrosis       # 4
    - nitrogen_deficiency   # 5
    - northern_corn_leaf_blight  # 6
    - phosphorus_deficiency # 7
    - potassium_deficiency  # 8

augmentation:
  minority_ratio_threshold: 4.0   # una clase es "minoritaria" si max_count/count > 4.0

baseline:                 # perfil para comparar arquitecturas rápido y barato
  classes: [ ... las 9 ... ]
  max_images_per_class: 1500   # tope por clase (recorta las mayoritarias)
  seed: 42

lime:
  images_per_class: 2       # muestreo pequeño para reportes visuales
  num_samples: 1000         # perturbaciones LIME por imagen
  num_features: 5           # superpíxeles a resaltar
  seed: 42
  baseline: true            # por defecto explica sobre el perfil baseline
  report_sample_size: 30    # muestreo amplio para la fidelidad agregada

gradcam:
  enabled: true             # activa el 4º panel Grad-CAM en los reportes

El detalle más importante: el orden de classes

El orden de la lista dataset.classes no es cosmético: determina el class_to_idx (mapeo clase→índice entero) que la cabeza del modelo aprende. common_rust es la clase 0, healthy la 3, etc.

Por qué importa tanto: los índices de clase no se guardan dentro del state_dict del modelo (los pesos). Se guardan aparte, en el summary.json del run. Si alguna vez reconstruyes el mapeo desde una lista con orden distinto (p.ej. baseline.classes vs dataset.classes), obtienes etiquetas permutadas: el modelo predice "clase 5" y tú la traduces a la clase equivocada. Este fue un bug real de rótulos en los reportes LIME, documentado en el código de load_run_metadata. La lección: el orden canónico es dataset.classes, y la fuente de verdad al cargar un run es su summary.json.

Riesgos de modificar este archivo

CambioConsecuencia
Reordenar classesRompe todos los checkpoints existentes (índices desalineados). Habría que reentrenar.
Cambiar target_sizeCambia la resolución de entrada de todos los modelos sin excepción declarada; afecta memoria, velocidad y precisión.
Cambiar seedLos splits y el muestreo dejan de coincidir con los runs previos; los resultados no serán comparables.
Bajar minority_ratio_thresholdMás clases se consideran minoritarias → más augmentation agresivo + sampler activo.
Subir max_images_per_classSplits baseline más grandes → entrenamiento más lento pero potencialmente mejor cobertura.

src/config.py — resolución de rutas y semilla

Propósito

Resolver de forma segura dónde están los datos de entrada y dónde escribir los artefactos, y fijar la semilla global de aleatoriedad. Es un módulo pequeño (45 líneas) pero de criticidad máxima: es el guardián de las rutas.

Código anotado

python
PROJECT_ROOT = Path(__file__).resolve().parents[1]
load_dotenv(PROJECT_ROOT / ".env")

# None cuando falta la variable: Path("") resolvería al cwd, que "existe" y burla los guards.
_raw_dataset_root = os.getenv("DATASET_ROOT", "").strip()
DATASET_ROOT: Path | None = Path(_raw_dataset_root) if _raw_dataset_root else None

Bloque crítico, línea por línea:

  • PROJECT_ROOT se calcula relativo a la ubicación del propio archivo (__file__), no al directorio de trabajo. Así el proyecto funciona desde cualquier cwd.
  • load_dotenv carga las variables del archivo .env (que está gitignored y contiene DATASET_ROOT).
  • La sutileza clave está en el comentario: si DATASET_ROOT no está definido y usáramos Path(""), eso resolvería al directorio actual, que existe y por tanto burlaría cualquier chequeo if path.exists(). Al forzar None, cualquier uso indebido falla de inmediato con un mensaje claro en lugar de leer basura silenciosamente. Es un ejemplo de "fallar ruidoso en vez de silencioso".

Funciones

get_dataset_root() -> Path

  • Objetivo: devolver la ruta del dataset fuente, validada.
  • Entrada: ninguna (lee la constante del módulo). Salida: Path.
  • Caso borde: si DATASET_ROOT is None, lanza SystemExit con instrucciones (copiar .env.example.env). No devuelve un Path inválido.
  • Uso: todo lo que consume datos (splits, entrenamiento, explicabilidad, predicción).

get_output_root() -> Path

  • Objetivo: devolver dónde escribir artefactos.
  • Lógica: si OUTPUT_ROOT está definido (entornos remotos como Modal, con volumen persistente), lo usa; si no, PROJECT_ROOT/outputs.
  • Uso: todo lo que produce artefactos (splits, checkpoints, métricas, reportes LIME).

La dicotomía consume/produce

get_dataset_root() = lo que el pipeline lee (inmutable). get_output_root() = lo que el pipeline escribe (regenerable, gitignored). Esta separación permite, por ejemplo, montar el dataset como solo-lectura y redirigir toda la escritura a un volumen persistente distinto — exactamente lo que hace la configuración de Modal (DATASET_ROOT=/data, OUTPUT_ROOT=/outputs).

set_global_seed(seed: int) -> None

  • Objetivo: hacer reproducible una ejecución.
  • Qué hace: siembra random, numpy, torch (CPU y CUDA) y fuerza cudnn.deterministic=True, cudnn.benchmark=False.
python
def set_global_seed(seed: int) -> None:
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    torch.backends.cudnn.deterministic = True   # algoritmos deterministas
    torch.backends.cudnn.benchmark = False       # desactiva autotuning no determinista
  • Trade-off importante: cudnn.benchmark=False sacrifica algo de velocidad en GPU a cambio de reproducibilidad. cuDNN, con benchmark=True, prueba varios algoritmos de convolución y elige el más rápido para el tamaño de entrada dado — pero esa elección puede variar entre corridas, rompiendo el determinismo. Aquí se prioriza reproducibilidad, coherente con el carácter académico del proyecto.
  • Caso borde: ni siquiera esto garantiza determinismo bit-a-bit al 100 % entre GPUs distintas o versiones de CUDA; garantiza reproducibilidad dentro del mismo entorno.

Complejidad y riesgos

Todo el módulo es O(1). El único riesgo real es de configuración de entorno: un .env ausente o mal configurado. El diseño lo mitiga fallando con un mensaje accionable.

Siguiente

Con las rutas y la config claras, sigue con el corazón del proyecto: la Capa de datos.