3. Variants#

This is what cinnamon is for. A researcher rarely wants a configuration; they want the twelve configurations that differ along two axes, each one addressable and reproducible.

python examples/tutorial/03_variants.py

Declare the axes and resolution enumerates the combinations, giving each a key derived from its values.

class ClassifierConfig(Configuration):
    # `variants` lists the *alternatives* to the default. The default itself is
    # index 0 and always stays in the sweep.
    learning_rate: float = Param(1e-3, variants=[1e-2, 1e-4])
    hidden_size: int = Param(128, variants=[256])
    dropout: float = Param(0.1, description="Not varied: stays fixed everywhere")

Three learning rates times two hidden sizes is six configurations, out of six lines. dropout has no variants, so it stays fixed everywhere and never appears in a tag.

What to notice#

  • variants lists the alternatives to the default. The default is part of the sweep too, which is why two values in variants give three configurations.

  • The tags are derived from the values, so a key is stable across runs and across machines. Rerun the file and learning_rate=0.01--hidden_size=256 still names the same experiment. That is what makes a result addressable six months later.

  • Nobody wrote those keys down. They are a consequence of the declaration, which is also why they cannot fall out of step with it.

The whole file#

 1"""
 23. Variants -- one component, many configurations.
 3
 4    python examples/tutorial/03_variants.py
 5
 6This is what cinnamon is for. A researcher rarely wants *a* configuration; they
 7want the twelve configurations that differ along two axes, each addressable and
 8reproducible. Declare the axes, and resolution enumerates the combinations for
 9you, giving each one a stable key.
10"""
11
12from cinnamon.configuration import Configuration, Param
13from cinnamon.registry import Registry
14
15
16class Classifier:
17    def __init__(self, learning_rate: float, hidden_size: int, dropout: float):
18        self.learning_rate = learning_rate
19        self.hidden_size = hidden_size
20        self.dropout = dropout
21
22    def describe(self) -> str:
23        return (
24            f"lr={self.learning_rate} hidden={self.hidden_size} dropout={self.dropout}"
25        )
26
27
28class ClassifierConfig(Configuration):
29    # `variants` lists the *alternatives* to the default. The default itself is
30    # index 0 and always stays in the sweep.
31    learning_rate: float = Param(1e-3, variants=[1e-2, 1e-4])
32    hidden_size: int = Param(128, variants=[256])
33    dropout: float = Param(0.1, description="Not varied: stays fixed everywhere")
34
35
36def main() -> None:
37    Registry.initialize()
38    Registry.register_configuration(
39        config=ClassifierConfig(),
40        name="classifier",
41        namespace="tutorial",
42        component=f"{__name__}.Classifier",
43    )
44
45    valid_keys, _ = Registry.dag_resolution()
46
47    # 3 learning rates x 2 hidden sizes = 6 configurations, from six lines.
48    print(f"{len(valid_keys)} configurations generated:\n")
49    for key in sorted(valid_keys, key=str):
50        component = Registry.from_key(key)
51        tags = ", ".join(sorted(key.tags)) or "(defaults)"
52        print(f"  {tags:36s} -> {component.describe()}")
53
54    print(
55        "\nThe tags are derived from the values, so a key is stable across runs:"
56        "\nrerun this file and 'learning_rate=0.01--hidden_size=256' still names"
57        "\nthe same experiment. That is what makes results addressable."
58    )
59
60
61if __name__ == "__main__":
62    main()

Next: 4. Dependencies — configurations that reference other registrations.