Skip to content

ハイパラが増えたらConfigをPydanticで管理し、argparseでsweepまで済ませる

機械学習の実験コードを書いていると、ハイパーパラメータがどんどん増えていきます。 そして実験を回し始めると、今度はパラメータを少しずつ変えてsweepしたいという欲求も出てきます。

ここでは、"PydanticでConfigを定義して、argparse経由でCLIから渡せるようにする" という簡単なコードを紹介します。 これにより、パラメータをgridでsweepすることができ、実験の管理が楽になります。

Configをクラスにまとめる

まずはConfigをPydanticのBaseModelで定義します。 dataclassでもよいのですが、Pydanticにするとバリデーション(gtlegeなど)が使えて、早い段階でConfgのvalidationを効かせられるので便利です。

from enum import Enum
from pathlib import Path
from pydantic import BaseModel, Field

class OptimizerType(str, Enum):
    adam = "adam"
    adamw = "adamw"
    sgd = "sgd"

class Config(BaseModel):
    lr: float       = Field(default=3e-4, gt=0.0, le=1.0, description="Learning rate")
    epochs: int     = Field(default=10, ge=1, description="Number of training epochs")
    batch_size: int = Field(default=64, ge=1, description="Mini-batch size")
    seed: int       = Field(default=42, description="Random seed for reproducibility")
    hidden_dim: int = Field(default=128, ge=1, description="Hidden layer dimension")
    num_layers: int = Field(default=3, ge=1, le=100, description="Number of layers")
    dropout: float  = Field(default=0.1, ge=0.0, le=1.0, description="Dropout rate")
    optimizer: OptimizerType = Field(default=OptimizerType.adamw, description="Optimizer type")
    weight_decay: float = Field(default=1e-2, ge=0.0, description="Weight decay coefficient")
    dataset: str    = Field(default="mnist", description="Dataset name")
    output_dir: Path = Field(default=Path("outputs"), description="Output directory path")
    experiment_name: str = Field(default="default", description="Experiment name for logging")
    use_amp: bool   = Field(default=True, description="Enable automatic mixed precision")

普段の実験ではこのConfigをそのまま生成してtrain(cfg)に渡します。コードがスッキリし、新しいパラを追加するのもフィールドを一行足すだけです。

sweepしたくなったら

実験を本格的に回し始めると、「dropoutを0.1/0.2/0.3で比べたい」とか「lr1e-31e-4のどちらが効くか」といったパラメータスイープ(parameter sweep)をしたくなります。

直感的には「指定しなかったパラにはdefault値を使い、指定したパラには複数の候補値の全部の組み合わせを回す」というのが欲しい形でしょう。単一値ならsweepなし、複数値ならgrid sweepという動作にします。

Pydantic → argparseでブリッジする

Configのフィールドをそのままargparseの引数に移し替え、nargs="+"で「複数値を受け取れる」にします。

  • 単一値だけ渡す → スイープなしの1回実行
  • 複数値でスペース区切りに渡す → 各パラの候補の直積で複数回いっぺんに実行

それをitertools.productで展開して、list[Config]を作ります。

import itertools
import argparse
from typing import Any
from pydantic import BaseModel
from setuptools._distutils.util import strtobool

def _build_parser(model_cls: type[BaseModel]) -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="ML Training Config (pass multiple values per arg to sweep)",
        formatter_class=argparse.RawTextHelpFormatter,
    )
    for name, info in model_cls.model_fields.items():
        flag = f"--{name.replace('_', '-')}"
        ann = info.annotation
        default = info.default
        desc = info.description or name
        default_str = default.value if isinstance(default, Enum) else repr(default)
        help_text = f"{desc} (default: {default_str})"

        kwargs: dict[str, Any] = dict(nargs="+", default=None, help=help_text)
        if isinstance(ann, type) and issubclass(ann, Enum):
            kwargs["type"] = ann
            kwargs["choices"] = [e.value for e in ann]
        elif ann is bool:
            kwargs["type"] = strtobool
        else:
            kwargs["type"] = ann
        parser.add_argument(flag, **kwargs)

    return parser


def parse_configs(model_cls: type[BaseModel] = Config) -> list[Config]:
    parser = _build_parser(model_cls)
    args = parser.parse_args()

    grid: dict[str, list[Any]] = {}
    for name, info in model_cls.model_fields.items():
        raw: list | None = getattr(args, name, None)
        grid[name] = raw if raw is not None else [info.default]

    keys = list(grid.keys())
    return [model_cls(**dict(zip(keys, combo)))
            for combo in itertools.product(*(grid[k] for k in keys))]

メインではこう使います。

import itertools

def train(cfg: Config) -> float:
    return cfg.lr * cfg.epochs  # placeholder

if __name__ == "__main__":
    configs = parse_configs()
    print(f"Sweep: {len(configs)} run(s)\n")

    for i, cfg in enumerate(configs, 1):
        loss = train(cfg)
        print(f"[{i}/{len(configs)}] lr={cfg.lr} dropout={cfg.dropout} "
              f"optimizer={cfg.optimizer.value} -> loss={loss:.6f}")

使い方

# 単一値:スイープなし
uv run main.py --lr 0.001 --epochs 10

# 複数値を与えるとgridでスイープ
uv run main.py --lr 1e-3 1e-4 --dropout 0.1 0.2 0.3

# ヘルプ表示
uv run main.py --help

この例ではlrが2候補、dropoutが3候補なので直積 2×3=6 通りのConfigが自動生成されます。設定の何を変えたのか、どの組み合わせを回したのかがCLIで一目瞭然になります。

まとめ

この記事ではPydanticでConfigを定義し、argparseでCLIからパラメータをgrid sweepできるようにする方法を紹介しました。 これにより、ハイパーパラメータの管理が容易になり、実験の効率化が図れます。 また、このようなコマンドライン引数を準備する副次的な効果として、 AIエージェントが実験を自動で回す際にも便利なインターフェースになりうると考えています。

それでは。