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#
variantslists the alternatives to the default. The default is part of the sweep too, which is why two values invariantsgive 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=256still 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.