28. Configuration et CLI

argparse avancé, click, typer, pydantic-settings, .env, configuration hiérarchique, patterns de config pour le ML.


28.1 argparse avancé

Rappel des bases (leçon 13). Compléments :

Sous-commandes

import argparse
 
parser = argparse.ArgumentParser(description="Outil ML")
subparsers = parser.add_subparsers(dest="commande", required=True)
 
# Sous-commande "train"
train = subparsers.add_parser("train", help="Entraîner un modèle")
train.add_argument("--data", required=True)
train.add_argument("--epochs", type=int, default=10)
train.add_argument("--lr", type=float, default=0.001)
 
# Sous-commande "eval"
eval = subparsers.add_parser("eval", help="Évaluer un modèle")
eval.add_argument("--checkpoint", required=True)
eval.add_argument("--batch-size", type=int, default=32)
 
args = parser.parse_args()
if args.commande == "train":
    entraîner(args.data, args.epochs, args.lr)
elif args.commande == "eval":
    évaluer(args.checkpoint, args.batch_size)
python script.py train --data dataset/ --epochs 50
python script.py eval --checkpoint model.pt

Types personnalisés

def intervalle(valeur: str) -> tuple[float, float]:
    parts = [float(x) for x in valeur.split(",")]
    if len(parts) != 2:
        raise argparse.ArgumentTypeError("Format attendu: min,max")
    return (parts[0], parts[1])
 
parser.add_argument("--range", type=intervalle, default=(0.0, 1.0))

Actions

parser.add_argument("--verbose", action="store_true")      # booléen
parser.add_argument("--mode", action="store", default="a") # valeur
parser.add_argument("--debug", action="store_const", const=True)  # constante
parser.add_argument("--level", action="count", default=0)  # -vvv → 3

Groupes

# Groupe mutuellement exclusif
group = parser.add_mutually_exclusive_group()
group.add_argument("--train", action="store_true")
group.add_argument("--eval", action="store_true")

28.2 click — décorateurs CLI

pip install click
import click
 
@click.command()
@click.option("--data", required=True, help="Chemin du dataset")
@click.option("--epochs", default=10, type=int, help="Nombre d'epochs")
@click.option("--lr", default=0.001, type=float, help="Learning rate")
@click.option("--verbose", is_flag=True, help="Mode verbeux")
@click.argument("nom_experience")
def main(data, epochs, lr, verbose, nom_experience):
    """Entraîne un modèle sur DATA."""
    if verbose:
        click.echo(f"Configuration: epochs={epochs}, lr={lr}")
    click.echo(f"Lancement de {nom_experience}...")
 
if __name__ == "__main__":
    main()
python train.py --data dataset/ --epochs 50 --lr 0.01 mon_exp

Sous-commandes avec click.Group

@click.group()
def cli():
    """Outil de gestion d'expériences."""
 
@cli.command()
@click.option("--config", required=True)
def train(config):
    """Lancer l'entraînement."""
    click.echo(f"Entraînement avec {config}")
 
@cli.command()
@click.option("--checkpoint", required=True)
def eval(checkpoint):
    """Évaluer un modèle."""
    click.echo(f"Évaluation de {checkpoint}")
 
@cli.command()
@click.option("--name", required=True)
def init(name):
    """Initialiser une nouvelle expérience."""
    click.echo(f"Expérience {name} créée")
 
if __name__ == "__main__":
    cli()
python exp.py train --config config.yaml
python exp.py eval --checkpoint model.pt
python exp.py init --name "exp-001"

Prompts interactifs

@click.command()
@click.option("--nom", prompt="Nom de l'expérience")
@click.option("--epochs", prompt="Nombre d'epochs", type=int)
@click.password_option()
def config(nom, epochs):
    """Configuration interactive."""
    click.echo(f"OK: {nom}, {epochs} epochs")

28.3 typer — moderne, basé sur les types

pip install typer
import typer
 
app = typer.Typer()
 
@app.command()
def train(
    data: str = typer.Option(..., "--data", "-d", help="Chemin du dataset"),
    epochs: int = typer.Option(10, "--epochs", "-e"),
    lr: float = typer.Option(0.001, "--lr"),
    verbose: bool = typer.Option(False, "--verbose", "-v"),
):
    """Entraîner un modèle."""
    if verbose:
        typer.echo(f"Configuration: epochs={epochs}, lr={lr}")
    typer.echo("Entraînement...")
 
@app.command()
def eval(
    checkpoint: str = typer.Argument(..., help="Chemin du checkpoint"),
    batch_size: int = typer.Option(32),
):
    """Évaluer un modèle."""
    typer.echo(f"Évaluation de {checkpoint}")
 
if __name__ == "__main__":
    app()

Typer avec dataclasses

from dataclasses import dataclass
 
@dataclass
class ConfigEntraînement:
    data: str
    epochs: int = 10
    lr: float = 0.001
    batch_size: int = 32
 
app = typer.Typer()
 
@app.command()
def train(config: ConfigEntraînement = typer.Option(...)):
    typer.echo(f"Entraînement: epochs={config.epochs}")

Validation automatique

from typing import Annotated
 
def train(
    lr: Annotated[float, typer.Option(min=1e-6, max=1.0)] = 0.001,
    epochs: Annotated[int, typer.Option(min=1, max=1000)] = 10,
):
    ...

28.4 pydantic-settings — configuration depuis l’environnement

pip install pydantic-settings
from pydantic_settings import BaseSettings
from typing import Optional
 
class Settings(BaseSettings):
    # Valeurs depuis variables d'environnement ou .env
    project_name: str = "ArtNotes-ML"
    debug: bool = False
    database_url: str = "sqlite:///data.db"
    api_key: Optional[str] = None
    max_workers: int = 4
    experiment_dir: str = "./experiments"
 
    class Config:
        env_file = ".env"
        env_prefix = "MYAPP_"  # cherche MYAPP_DEBUG, etc.
 
settings = Settings()
 
# Utilisation
print(settings.database_url)

.env :

MYAPP_DEBUG=true
MYAPP_DATABASE_URL=postgres://localhost/db
MYAPP_API_KEY=secret123

Héritage et profils

class DevSettings(Settings):
    debug: bool = True
    database_url: str = "sqlite:///dev.db"
 
class ProdSettings(Settings):
    debug: bool = False
    database_url: str = "postgres://prod/db"

28.5 hydra — configuration hiérarchique (ML)

pip install hydra-core
# config.yaml
defaults:
  - model: resnet20
  - training: default
  - _self_
# model/resnet20.yaml
name: resnet20
hidden_size: 256
dropout: 0.1
# training/default.yaml
batch_size: 64
learning_rate: 0.001
epochs: 100
optimizer: adam
import hydra
from omegaconf import DictConfig
 
@hydra.main(version_base=None, config_path=".", config_name="config")
def main(cfg: DictConfig):
    print(cfg.model.name)           # resnet20
    print(cfg.training.epochs)      # 100
    print(cfg.training.learning_rate)  # 0.001
 
if __name__ == "__main__":
    main()
# Surcharge depuis la ligne de commande
python train.py model=resnet56 training.epochs=200
 
# Composer avec plusieurs configs
python train.py model=vit training=debug

28.6 Tableau comparatif

OutilApprocheTypéValidationSous-commandesConfig hiérarchique
argparseImpératifNonManuelOuiNon
clickDécorateurNonVia typesOuiNon
typerDécorateur + typesOuiAutomatiqueOuiNon
pydantic-settingsClasseOuiAutomatiqueNonHéritage
hydraYAML + compositionOmegaConfNonNonOui

28.7 Recommandations pour la thèse

  • Petits scripts : argparse ou typer (rapide, pas de dépendance)
  • CLI complète : typer (typage, auto-doc, validation)
  • Configuration ML : pydantic-settings + .env (secrets) + YAML (config)
  • Grid search / multi-expérience : hydra (composition, overrides)
# Exemple typique thèse ML
from pydantic_settings import BaseSettings
from pathlib import Path
 
class ExpConfig(BaseSettings):
    # Dataset
    data_path: Path = Path("./data")
    dataset: str = "cifar10"
    num_classes: int = 10
 
    # Modèle
    model: str = "resnet20"
    hidden_size: int = 256
    dropout: float = 0.1
 
    # Entraînement
    epochs: int = 100
    batch_size: int = 64
    learning_rate: float = 0.001
    weight_decay: float = 5e-4
    optimizer: str = "adam"
 
    # Byzantine
    byzantine_ratio: float = 0.0
    attack_type: str = "none"
    aggregation_rule: str = "mean"
 
    # Expérience
    experiment_name: str = "default"
    seed: int = 42
    log_dir: Path = Path("./logs")
 
    class Config:
        env_file = ".env"
        env_prefix = "EXP_"

🔗 ← Retour au cours · ← précédent · Suivant →