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
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 reportesEl 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
| Cambio | Consecuencia |
|---|---|
Reordenar classes | Rompe todos los checkpoints existentes (índices desalineados). Habría que reentrenar. |
Cambiar target_size | Cambia la resolución de entrada de todos los modelos sin excepción declarada; afecta memoria, velocidad y precisión. |
Cambiar seed | Los splits y el muestreo dejan de coincidir con los runs previos; los resultados no serán comparables. |
Bajar minority_ratio_threshold | Más clases se consideran minoritarias → más augmentation agresivo + sampler activo. |
Subir max_images_per_class | Splits 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
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 NoneBloque crítico, línea por línea:
PROJECT_ROOTse calcula relativo a la ubicación del propio archivo (__file__), no al directorio de trabajo. Así el proyecto funciona desde cualquiercwd.load_dotenvcarga las variables del archivo.env(que está gitignored y contieneDATASET_ROOT).- La sutileza clave está en el comentario: si
DATASET_ROOTno está definido y usáramosPath(""), eso resolvería al directorio actual, que existe y por tanto burlaría cualquier chequeoif path.exists(). Al forzarNone, 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, lanzaSystemExitcon instrucciones (copiar.env.example→.env). No devuelve unPathinvá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_ROOTestá 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 fuerzacudnn.deterministic=True,cudnn.benchmark=False.
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=Falsesacrifica algo de velocidad en GPU a cambio de reproducibilidad. cuDNN, conbenchmark=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.