En este tutorial, implementamos una canalización experimental de PyTorch controlada por Gin Config en la que el código de entrenamiento ejecutable permanece estable. Al mismo tiempo, los grados de libertad experimentales se trasladan a archivos de configuración declarativos. Construimos una tarea de clasificación binaria en espiral no lineal, definimos un MLP configurable con variantes arquitectónicas de alcance y exponemos parámetros para el optimizador, el programador, la pérdida, el procesamiento por lotes, la siembra y el bucle de entrenamiento a través de enlaces @gin.configurable. Usamos las referencias de alcance de Gin para crear instancias de configuraciones de modelos separadas, enlaces de tiempo de ejecución para anular los parámetros seleccionados sin editar el código fuente y exportación de configuraciones operativas para capturar la configuración resuelta exacta que produce cada ejecución de entrenamiento.
Instalación de Gin Config y creación del conjunto de datos en espiral
!pip -q instalar gin-config importar os importar json importar matemáticas importar aleatorio importar textwrap desde pathlib importar ruta importar gin importar numpy como np importar antorcha importar torch.nn como nn importar torch.nn.funcional como F de torch.utils.data importar TensorDataset, DataLoader importar matplotlib.pyplot como plt ROOT = Path("/content/gin_config_sharp_tutorial") CONFIG_DIR = ROOT / "configs" RUN_DIR = ROOT / "ejecuta" CONFIG_DIR.mkdir(padres=True, exist_ok=True) RUN_DIR.mkdir(parents=True, exist_ok=True) gin.clear_config() @gin.configurable def seed_everything(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) return semilla @gin.configurable def make_spiral_dataset( n_per_class=gin.REQUIRED, ruido=0.18, rotaciones=1.75, train_fraction=0.8, semilla=0,): rng = np.random.default_rng(seed) radio_0 = np.linspace(0.05, 1.0, n_per_clase) theta_0 = rotaciones * 2 * np.pi * radio_0 theta_0 += rng.normal(0.0, ruido, tamaño=n_por_clase) x0 = np.stack( [ radio_0 * np.cos(theta_0), radio_0 * np.sin(theta_0), ], eje=1, ) radio_1 = np.linspace(0.05, 1.0, n_per_clase) theta_1 = rotaciones * 2 * np.pi * radio_1 + np.pi theta_1 += rng.normal(0.0, ruido, tamaño=n_per_clase) x1 = np.stack( [ radio_1 * np.cos(theta_1), radio_1 * np.sin(theta_1), ], eje=1, ) x = np.concatenate([x0, x1], eje=0).astype(np.float32) y = np.concatenate( [ np.zeros((n_per_class, 1)), np.ones((n_per_class, 1)), ], eje=0, ).astype(np.float32) orden = rng.permutation(len(x)) x = x[orden] y = y[orden] dividir = int(train_fraction * len(x)) x_train, y_train = x[:split], y[:split] x_val, y_val = x[split:], y[split:] media = x_train.mean(axis=0, keepdims=True) std = x_train.std(axis=0, keepdims=True) + 1e-8 x_train = (x_train – media) / std x_val = (x_val – media) / std return { "tren": ( torch.tensor(x_train), torch.tensor(y_train), ), "val": ( torch.tensor(x_val), torch.tensor(y_val), ), "metadatos": { "n_train": int(len(x_train)), "n_val": int(len(x_val)), "n_features": int(x_train.shape[1]), "ruido": float(ruido), "rotations": float(rotaciones), "seed": int(seed), }, } @gin.configurable(denylist=["x", "y"]) def make_loader( x, y, lote_size=128, shuffle=True, seed=0, ): generador = torch.Generator() generador.manual_seed(seed) conjunto de datos = TensorDataset(x, y) devuelve DataLoader(conjunto de datos, tamaño_lote=tamaño_lote, shuffle=shuffle, generador=generador, drop_last=False,)
Comenzamos instalando Gin Config e importando las bibliotecas principales de Python, PyTorch, NumPy y las bibliotecas de trazado necesarias para el experimento. Creamos una estructura de directorio de proyecto limpia y restablecemos el estado de configuración global de Gin para que el cuaderno se ejecute de manera reproducible. Luego definimos la función semilla, generamos un conjunto de datos en espiral no lineal y construimos un DataLoader configurable que Gin puede controlar a través de enlaces externos.
Definición de un MLP, un optimizador y un programador configurables por Gin
def capa_activación(nombre): nombre = nombre.lower() si nombre == "relu": regresa nn.ReLU() si nombre == "gelu": regresa nn.GELU() si nombre == "tanh": regresa nn.Tanh() si nombre == "silu": regresa nn.SiLU() levanta ValueError(f"Activación desconocida: {nombre}") @gin.configurable clase MLP(nn.Module): def __init__( self, input_dim=gin.REQUIRED, oculto_dims=(64, 64), salida_dim=1, activación="gelu", abandono=0.0, use_layernorm=False,): super().__init__() capas =[]current_dim = input_dim para oculta_dim en oculta_dims: capas.append(nn.Linear(current_dim, oculta_dim)) si use_layernorm: capas.append(nn.LayerNorm(hidden_dim)) capas.append(activación_capa(activación)) si abandono > 0: capas.append(nn.Dropout(abandono)) current_dim = oculta_dim capas.append(nn.Linear(current_dim, salida_dim)) self.network = nn.Sequential(*layers) def forward(self, x): return self.network(x) @gin.configurable(denylist=["params"]) def make_optimizer( params, name="adamw", lr=3e-3, Weight_decay=1e-3, impulse=0.9,): nombre = nombre.lower() si nombre == "adamw": retorno antorcha.optim.AdamW( params, lr=lr, peso_decay=peso_decay, ) si nombre == "sgd": retorno antorcha.optim.SGD( params, lr=lr, impulso=momentum, peso_decay=peso_decay, ) elevar ValueError(f"Optimizador desconocido: {nombre}") @gin.configurable(denylist=["optimizer"]) def make_cosine_scheduler( optimizador, total_epochs=60, warmup_epochs=5, min_lr_factor=0.05,): def lr_lambda(época): si época < época_calentamiento: return float(época + 1) / float(max(1, épocas_calentamiento)) progreso = (época – épocas de calentamiento) / float( max(1, épocas_total – épocas de calentamiento) ) coseno = 0.5 * (1.0 + math.cos(math.pi * progreso)) return min_lr_factor + (1.0 – min_lr_factor) * coseno return torch.optim.lr_scheduler.LambdaLR( optimizador, lr_lambda=lr_lambda, ) @gin.configurable def bce_with_logits_loss( logits, objetivos, label_smoothing=0.0, ): if label_smoothing > 0: objetivos = objetivos * (1.0 – label_smoothing) + 0.5 * label_smoothing return F.binary_cross_entropy_with_logits(logits, objetivos) @torch.no_grad() def evaluar(modelo, cargador, loss_fn, dispositivo): model.eval() total_loss = 0.0 total_correct = 0 total_count = 0 para x, y en el cargador: x = x.to(dispositivo) y = y.to(dispositivo) logits = model(x) loss = loss_fn(logits, y) probs = torch.sigmoid(logits) preds = (probs >= 0.5).float() pérdida_total += pérdida.item() * len(x) total_correct += (preds == y).sum().item() cuenta_total += len(x) return { "pérdida": pérdida_total / cuenta_total, "precisión": corrección_total / cuenta_total, }
Definimos los bloques de construcción de la red neuronal que forman el modelo configurable y las utilidades de entrenamiento. Creamos una clase MLP cuya arquitectura, función de activación, abandono y comportamiento de normalización de capas se controlan a través de Gin en lugar de valores codificados. También implementamos funciones configurables de optimizador, programador, pérdida y evaluación para que el proceso de capacitación siga siendo modular y listo para experimentar.
Implementación del bucle de entrenamiento y del corredor de experimentos
@gin.configurable( denylist=[ "modelo", "optimizador", "programador", "train_loader", "val_loader", "device", ] ) def fit( modelo, optimizador, programador, train_loader, val_loader, dispositivo, épocas=60, grad_clip_norm=1.0, log_every=10, loss_fn=bce_with_logits_loss, ): historia =[]para época en rango (1, épocas + 1): model.train() para x, y en train_loader: x = x.to(dispositivo) y = y.to(dispositivo) optimizador.zero_grad(set_to_none=True) logits = model(x) loss = loss_fn(logits, y) loss.backward() si grad_clip_norm no es Ninguno: nn.utils.clip_grad_norm_( model.parameters(), grad_clip_norm, ) optimizador.step() si el programador no es Ninguno: planificador.step() train_metrics = evaluar( modelo, train_loader, loss_fn, dispositivo, ) val_metrics = evaluar( modelo, val_loader, loss_fn, dispositivo, ) lr = optimizador.param_groups[0]["lr"] fila = { "epoch": epoch, "lr": lr, "train_loss": train_metrics["loss"], "train_accuracy": train_metrics["accuracy"], "val_loss": val_metrics["loss"], "val_accuracy": val_metrics["accuracy"], } historial.append(fila) si época == 1 o época % log_every == 0 o época == épocas: print( f"epoch={epoch:03d} | " f"lr={lr:.6f} | " f"train_loss={row['train_loss']:.4f} | " f"train_acc={row['train_accuracy']:.3f} | " f"val_loss={row['val_loss']:.4f} | " f"val_acc={row['val_accuracy']:.3f}" ) historial de retorno @gin.configurable def run_experiment( tag=gin.REQUIRED, model=gin.REQUIRED, dataset_fn=make_spiral_dataset, optimizador_factory=make_optimizer, Scheduler_factory=make_cosine_scheduler, prefer_gpu=True, ): seed_everything() dispositivo = "cuda" si prefiere_gpu y torch.cuda.is_available() de lo contrario "cpu" datos = dataset_fn() x_train, y_train = datos["tren"] x_val, y_val = datos["val"] train_loader = make_loader( x_train, y_train, shuffle=True, ) val_loader = make_loader( x_val, y_val, shuffle=False, ) model = model.to(device) optimizador = optimizador_factory(model.parameters()) planificador = Ninguno si planificador_factory no es Ninguno: planificador = planificador_factory(optimizador) print("n" + "=" * 80) print(f"Experimento: {etiqueta}") print("=" * 80) print(f"Dispositivo: {dispositivo}") print(f"Conjunto de datos: {datos['metadata']}") print(f"Parámetros: {suma(p.numel() for p in model.parameters()):,}") historial = ajuste( modelo=modelo, optimizador=optimizador, planificador=programador, train_loader=train_loader, val_loader=val_loader, dispositivo=dispositivo, ) resultado = { "etiqueta": etiqueta, "dispositivo": dispositivo, "metadatos": datos["metadatos"], "parámetros": suma(p.numel() para p en model.parameters()), "final": historial[-1], "historio": historial, } devolver resultado
Implementamos el bucle de entrenamiento principal, en el que el modelo realiza pases hacia adelante, calcula la pérdida de entropía cruzada binaria, propaga gradientes hacia atrás, aplica recorte de gradiente y actualiza parámetros. Evaluamos el modelo después de cada época tanto en el conjunto de entrenamiento como en el de validación, mientras almacenamos el historial de pérdida, precisión y tasa de aprendizaje. Luego definimos el ejecutor de experimentos de nivel superior que conecta el conjunto de datos, el modelo, el optimizador, el programador y el bucle de entrenamiento a través de dependencias administradas por Gin.
Escribir archivos de configuración de Gin con enlaces con alcance y anulaciones de tiempo de ejecución
BASE_CONFIG = CONFIG_DIR / "base.gin" COMPACT_CONFIG = CONFIG_DIR / "compact_adamw.gin" WIDE_CONFIG = CONFIG_DIR / "wide_sgd.gin" BASE_CONFIG.write_text( textwrap.dedent( """ SEED = 123 N_PER_CLASS = 900 EPOCHS = 50 BATCH = 128 seed_everything.seed = %SEED make_spiral_dataset.n_per_class = %N_PER_CLASS make_spiral_dataset.noise = 0.20 make_spiral_dataset.rotations = 1.85 make_spiral_dataset.train_fraction = 0.80 make_spiral_dataset.seed = %SEED make_loader.batch_size = %BATCH make_loader.seed = %SEED MLP.input_dim = 2 MLP.output_dim = 1 MLP.activation = 'gelu' MLP.dropout = 0.05 MLP.use_layernorm = True make_optimizer.name="adamw" make_optimizer.lr = 0.003 make_optimizer.weight_decay = 0.001 make_optimizer.momentum = 0,9 make_cosine_scheduler.total_epochs = %EPOCHS make_cosine_scheduler.warmup_epochs = 5 make_cosine_scheduler.min_lr_factor = 0,05 bce_with_logits_loss.label_smoothing = 0,02 fit.epochs = %EPOCHS fit.grad_clip_norm = 1,0 fit.log_every = 10 fit.loss_fn = @bce_with_logits_loss run_experiment.dataset_fn = @make_spiral_dataset run_experiment.optimizer_factory = @make_optimizer run_experiment.scheduler_factory = @make_cosine_scheduler run_experiment.prefer_gpu = True """ ).strip() ) COMPACT_CONFIG.write_text( textwrap.dedent( f""" include '{BASE_CONFIG.as_posix()}' run_experiment.tag = 'compact_gelu_adamw' run_experiment.model = @compact/MLP() compact/MLP.hidden_dims = (64, 64, 64) compact/MLP.dropout = 0.05 compact/MLP.use_layernorm = True make_optimizer.name="adamw" make_optimizer.lr = 0.003 make_optimizer.weight_decay = 0.001 """ ).strip() ) WIDE_CONFIG.write_text( textwrap.dedent( f""" include '{BASE_CONFIG.as_posix()}' run_experiment.tag = 'wide_relu_sgd' run_experiment.model = @wide/MLP() ancho/MLP.hidden_dims = (128, 128, 128, 64) ancho/MLP.activation = 'relu' ancho/MLP.dropout = 0.02 ancho/MLP.use_layernorm = True make_optimizer.name="sgd" make_optimizer.lr = 0.035 make_optimizer.momentum = 0.92 make_optimizer.weight_decay = 0.0005 bce_with_logits_loss.label_smoothing = 0.0 """ ).strip() ) def run_from_gin_file(config_path, runtime_bindings=None): runtime_bindings = runtime_bindings o[]gin.clear_config() gin.parse_config_files_and_bindings( config_files=[str(config_path)], vinculantes=runtime_bindings, skip_unknown=False, finalize_config=True, ) print("nArchivo de configuración cargado:") print(config_path) print("nParámetros consultados seleccionados:") print("fit.epochs =", gin.query_parameter("fit.epochs")) print("make_loader.batch_size =", gin.query_parameter("make_loader.batch_size")) print("make_spiral_dataset.noise =", gin.query_parameter("make_spiral_dataset.noise")) prueba: gin.bind_parameter("fit.epochs", 999) excepto RuntimeError como error: print("nComprobación de bloqueo de configuración:") print(str(error).splitlines()[0]) resultado = run_experiment() etiqueta = resultado["etiqueta"] out_dir = RUN_DIR / etiqueta out_dir.mkdir(parents=True, exist_ok=True) result_path = out_dir / "result.json" operative_path = out_dir / "operative_config.gin" result_path.write_text(json.dumps(resultado, sangría=2)) operative_path.write_text(gin.operative_config_str()) print("nSaved:") print(result_path) print(operative_path) devuelve resultado, operative_path compact_result, compact_operative = run_from_gin_file( COMPACT_CONFIG, runtime_bindings=[ "fit.epochs = 45", "make_spiral_dataset.noise = 0.18", "run_experiment.tag = 'compact_gelu_adamw_runtime_override'", ], ) ancho_resultado, ancho_operativo = run_from_gin_file( WIDE_CONFIG, runtime_bindings=[ "fit.epochs = 45", "make_spiral_dataset.noise = 0.18", "run_experiment.tag = 'wide_relu_sgd_runtime_override'", ], )
Creamos los archivos de configuración de Gin reales que controlan el experimento sin modificar el código fuente de Python. Definimos una configuración base compartida y luego componemos dos experimentos de alcance: un modelo AdamW compacto basado en GELU y un modelo SGD más amplio basado en ReLU. También demostramos anulaciones de tiempo de ejecución, consultas de parámetros, bloqueo de configuraciones, serialización de resultados y exportación de configuraciones operativas para un seguimiento de experimentos reproducible.
Comparar resultados y exportar la configuración operativa
def plot_metric(resultados, métrica, título): plt.figure(figsize=(9, 4)) para obtener resultados: épocas = [fila["época"] para fila en resultado["historia"]] valores = [fila[métrica] para fila en resultado["historia"]] plt.plot(épocas, valores, etiqueta=resultado["etiqueta"]) plt.xlabel("Epoch") plt.ylabel(metric) plt.title(title) plt.grid(True, alpha=0.3) plt.legend() plt.tight_layout() plt.show() plot_metric( [compact_result, wide_result], "val_loss", "Pérdida de validación controlada por Gin Config", ) plot_metric( [compact_result, wide_result], "val_accuracy", "Precisión de validación controlada por Gin Config", ) resumen = [ { "tag": compact_result["tag"], "params": compact_result["parameters"], "val_loss": compact_result["final"]["val_loss"], "val_accuracy": compact_result["final"]["val_accuracy"], }, { "tag": wide_result["tag"], "params":wide_result["parameters"], "val_loss":wide_result["final"]["val_loss"], "val_accuracy":wide_result["final"]["val_accuracy"], }, ] print("n" + "=" * 80) print("Comparación final") print("=" * 80) para la fila en resumen: print( f"{row['tag']} | " f"params={row['params']:,} | " f"val_loss={row['val_loss']:.4f} | " f"val_acc={row['val_accuracy']:.3f}" ) print("n" + "=" * 80) print("Vista previa de la configuración operativa del experimento compacto") print("=" * 80) print(compact_operative.read_text()[:2500]) print("n" + "=" * 80) print("Archivos generados") print("=" * 80) para la ruta en sorted(ROOT.rglob("*")): if path.is_file(): print(path)
Visualizamos las curvas de pérdida de validación y precisión de validación para ambos experimentos controlados por Gin. Resumimos los recuentos de parámetros finales, las pérdidas de validación y las precisiones de validación para comparar claramente las dos configuraciones. También imprimimos la configuración operativa y los archivos generados, que proporcionan un registro completo de las configuraciones exactas utilizadas durante la ejecución.
Conclusión
En conclusión, tenemos un flujo de trabajo de gestión de experimentos reproducible que demuestra cómo Gin Config mejora el control, la trazabilidad y la modularidad en proyectos de PyTorch. Ejecutamos múltiples experimentos con alcance a partir de archivos .gin compuestos, comparamos el comportamiento de entrenamiento de AdamW y SGD bajo conjuntos de datos controlados y configuraciones de época, verificamos el bloqueo de la configuración después del análisis y guardamos tanto las métricas como las configuraciones operativas para su posterior inspección. Nos brinda un patrón para escalar los experimentos de Colab a canales de nivel de investigación, en los que la arquitectura del modelo, la estrategia de optimización, la generación de datos y los programas de capacitación deben ajustarse sistemáticamente sin romper la implementación central.
Consulte los códigos completos con Notebook aquí. Además, no dude en seguirnos en Twitter y no olvide unirse a nuestro SubReddit de más de 150.000 ml y suscribirse a nuestro boletín. ¡Esperar! estas en telegrama? Ahora también puedes unirte a nosotros en Telegram.
¿Necesita asociarse con nosotros para promocionar su repositorio de GitHub O su página principal de Hugging O su lanzamiento de producto O seminario web, etc.? Conéctate con nosotros
A Sana Hassan, pasante de consultoría en Marktechpost y estudiante de doble titulación en IIT Madras, le apasiona aplicar la tecnología y la inteligencia artificial para abordar los desafíos del mundo real. Con un gran interés en resolver problemas prácticos, aporta una nueva perspectiva a la intersección de la IA y las soluciones de la vida real.