kalfa 3.0.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. kalfa/__init__.py +19 -0
  2. kalfa/api.py +284 -0
  3. kalfa/board/__init__.py +1 -0
  4. kalfa/board/answers.py +419 -0
  5. kalfa/board/server.py +536 -0
  6. kalfa/board/static/api.js +176 -0
  7. kalfa/board/static/app.js +122 -0
  8. kalfa/board/static/board.css +228 -0
  9. kalfa/board/static/charts.css +46 -0
  10. kalfa/board/static/components/badge.js +14 -0
  11. kalfa/board/static/components/chart.js +204 -0
  12. kalfa/board/static/components/diagram.js +857 -0
  13. kalfa/board/static/components/error.js +17 -0
  14. kalfa/board/static/components/loading.js +7 -0
  15. kalfa/board/static/components/progress.js +25 -0
  16. kalfa/board/static/components/settings.js +418 -0
  17. kalfa/board/static/components/spark.js +43 -0
  18. kalfa/board/static/components/table.js +156 -0
  19. kalfa/board/static/diagram.css +76 -0
  20. kalfa/board/static/format.js +138 -0
  21. kalfa/board/static/index.html +34 -0
  22. kalfa/board/static/plot.js +594 -0
  23. kalfa/board/static/predictions.css +20 -0
  24. kalfa/board/static/store.js +215 -0
  25. kalfa/board/static/views/compare.js +185 -0
  26. kalfa/board/static/views/home.js +90 -0
  27. kalfa/board/static/views/record/config.js +81 -0
  28. kalfa/board/static/views/record/curves.js +137 -0
  29. kalfa/board/static/views/record/data.js +378 -0
  30. kalfa/board/static/views/record/files.js +128 -0
  31. kalfa/board/static/views/record/log.js +164 -0
  32. kalfa/board/static/views/record/model.js +286 -0
  33. kalfa/board/static/views/record/overview.js +216 -0
  34. kalfa/board/static/views/record/plots.js +103 -0
  35. kalfa/board/static/views/record/predictions.js +834 -0
  36. kalfa/board/static/views/record.js +179 -0
  37. kalfa/board/static/views/runs.js +124 -0
  38. kalfa/board/static/views/sweep.js +282 -0
  39. kalfa/board/watch.py +87 -0
  40. kalfa/cli.py +371 -0
  41. kalfa/commands/__init__.py +0 -0
  42. kalfa/commands/common.py +8 -0
  43. kalfa/commands/export.py +117 -0
  44. kalfa/commands/generate.py +31 -0
  45. kalfa/commands/guide/__init__.py +101 -0
  46. kalfa/commands/guide/about.py +93 -0
  47. kalfa/commands/guide/calls.py +102 -0
  48. kalfa/commands/guide/sections.py +186 -0
  49. kalfa/commands/guide/seen.py +307 -0
  50. kalfa/commands/guide/show.py +106 -0
  51. kalfa/commands/guide/training.py +333 -0
  52. kalfa/commands/guide/words.py +97 -0
  53. kalfa/commands/legos.py +508 -0
  54. kalfa/commands/plots.py +27 -0
  55. kalfa/commands/report/__init__.py +64 -0
  56. kalfa/commands/report/page.py +112 -0
  57. kalfa/commands/report/predictions.py +216 -0
  58. kalfa/commands/report/sections.py +356 -0
  59. kalfa/commands/stop.py +60 -0
  60. kalfa/commands/sweep/__init__.py +202 -0
  61. kalfa/commands/sweep/checks.py +98 -0
  62. kalfa/commands/sweep/plan.py +183 -0
  63. kalfa/commands/sweep/points.py +123 -0
  64. kalfa/commands/sweep/runner.py +100 -0
  65. kalfa/commands/sweep/space.py +67 -0
  66. kalfa/commands/sweep/strategies.py +228 -0
  67. kalfa/core/__init__.py +0 -0
  68. kalfa/core/aliases.py +229 -0
  69. kalfa/core/build.py +727 -0
  70. kalfa/core/check.py +550 -0
  71. kalfa/core/coerce.py +36 -0
  72. kalfa/core/context.py +149 -0
  73. kalfa/core/device.py +63 -0
  74. kalfa/core/errors.py +45 -0
  75. kalfa/core/graph.py +304 -0
  76. kalfa/core/history.py +70 -0
  77. kalfa/core/loop.py +710 -0
  78. kalfa/core/make.py +121 -0
  79. kalfa/core/plan.py +156 -0
  80. kalfa/core/predict.py +290 -0
  81. kalfa/core/protocols.py +122 -0
  82. kalfa/core/record.py +347 -0
  83. kalfa/core/refs.py +88 -0
  84. kalfa/core/rng.py +100 -0
  85. kalfa/core/rules.py +513 -0
  86. kalfa/core/run.py +568 -0
  87. kalfa/core/spec.py +413 -0
  88. kalfa/core/state.py +100 -0
  89. kalfa/core/wrap.py +671 -0
  90. kalfa/records/__init__.py +24 -0
  91. kalfa/records/compare.py +86 -0
  92. kalfa/records/reader.py +561 -0
  93. kalfa/records/stats/__init__.py +36 -0
  94. kalfa/records/stats/choices.py +282 -0
  95. kalfa/records/stats/classes.py +236 -0
  96. kalfa/records/stats/measures.py +144 -0
  97. kalfa/records/stats/pairs.py +225 -0
  98. kalfa/records/stats/tables.py +153 -0
  99. kalfa/records/sweep.py +244 -0
  100. kalfa/std/__init__.py +17 -0
  101. kalfa/std/actions.py +35 -0
  102. kalfa/std/common.py +56 -0
  103. kalfa/std/data/__init__.py +1 -0
  104. kalfa/std/data/base.py +45 -0
  105. kalfa/std/data/columns.py +190 -0
  106. kalfa/std/data/flow.py +317 -0
  107. kalfa/std/data/images.py +443 -0
  108. kalfa/std/data/options.py +147 -0
  109. kalfa/std/data/pre.py +84 -0
  110. kalfa/std/data/splits.py +140 -0
  111. kalfa/std/data/stream.py +294 -0
  112. kalfa/std/data/table.py +642 -0
  113. kalfa/std/data/text.py +436 -0
  114. kalfa/std/data/transforms.py +327 -0
  115. kalfa/std/data/weights.py +113 -0
  116. kalfa/std/data/window.py +135 -0
  117. kalfa/std/figure.py +37 -0
  118. kalfa/std/init.py +72 -0
  119. kalfa/std/loggers.py +105 -0
  120. kalfa/std/losses.py +161 -0
  121. kalfa/std/metrics.py +463 -0
  122. kalfa/std/models.py +122 -0
  123. kalfa/std/notes.py +1553 -0
  124. kalfa/std/objectives.py +360 -0
  125. kalfa/std/plots/__init__.py +75 -0
  126. kalfa/std/plots/classification.py +207 -0
  127. kalfa/std/plots/common.py +44 -0
  128. kalfa/std/plots/curves.py +56 -0
  129. kalfa/std/plots/images.py +188 -0
  130. kalfa/std/plots/pairs.py +66 -0
  131. kalfa/std/plots/predictions.py +154 -0
  132. kalfa/std/samplers.py +261 -0
  133. kalfa/std/samples.py +72 -0
  134. kalfa/std/schedules.py +74 -0
  135. kalfa/std/steps.py +133 -0
  136. kalfa/std/triggers.py +135 -0
  137. kalfa/summary.py +296 -0
  138. kalfa/tui/__init__.py +4 -0
  139. kalfa/tui/app.py +107 -0
  140. kalfa/tui/config.py +61 -0
  141. kalfa/tui/guide.py +100 -0
  142. kalfa/tui/jobs.py +97 -0
  143. kalfa/tui/legos.py +62 -0
  144. kalfa/tui/records.py +439 -0
  145. kalfa-3.0.0.dev0.dist-info/METADATA +261 -0
  146. kalfa-3.0.0.dev0.dist-info/RECORD +148 -0
  147. kalfa-3.0.0.dev0.dist-info/WHEEL +4 -0
  148. kalfa-3.0.0.dev0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,93 @@
1
+ from kalfa.commands.guide.words import and_list
2
+ from kalfa.core.build import TOP_LEVEL
3
+
4
+
5
+ RUN = "run"
6
+ RUN_KEYS = ("seed", "device", "record", "include", "plugins")
7
+ UNKNOWN_COLOR = "grey35"
8
+
9
+ COLORS = {"run": "blue_violet", "params": "dark_magenta", "data": "dark_cyan", "models": "dodger_blue3",
10
+ "losses": "red3", "metrics": "dark_orange3", "optimizers": "green4", "train": "dark_goldenrod",
11
+ "rules": "deep_pink4", "plots": "turquoise4", "generate": "medium_purple4", "sweep": "chartreuse4"}
12
+
13
+ ABOUT = {
14
+ "run": ("Top level values that set up the run as a whole. seed seeds every random stream and the splits (0 when "
15
+ "it is not written). device is auto (cuda, then mps, then cpu), cpu, mps, cuda or cuda:<index>. record "
16
+ "is the directory the run writes, new or empty; without it a run writes runs/<config name>_<datetime>. "
17
+ "include merges other files below this one, and plugins are modules imported before the build so that "
18
+ "the short names they register can be used."),
19
+ "params": ("Values the rest of the config reads as $name$. They are substituted after every layer is merged: a "
20
+ "string that is only $name$ takes the value with its type, and inside a longer string the value is "
21
+ "written as text. The flag -p name=value overrides a param from the command line, a sweep varies them "
22
+ "point by point, and a param that nothing reads is a warning."),
23
+ "data": ("One call: the data class. It reads the files, cuts them into the train, valid and test sets, fits what "
24
+ "it learns (preprocessors, a vocabulary) on the train set only and yields the batches. Its batch fields "
25
+ "are what a model takes as inputs and what a loss or a metric names as target, and the values it "
26
+ "exposes once fitted are read elsewhere as @data.<name>. A relative path is looked for in the working "
27
+ "directory, then next to the config files."),
28
+ "models": ("Each name is a call of an nn.Module. kalfa calls it with the batch fields of inputs (default the "
29
+ "inputs of the data class, x for a table) and names what it returns after outputs (default the model "
30
+ "name); these names are what pred refers to. The slot keys inputs, outputs, init, weights, freeze and "
31
+ "ema are read by kalfa and never reach the class; init, weights, freeze and ema apply in that order. "
32
+ "With one model, that model is the predicts model whose outputs fill the predictions table."),
33
+ "losses": ("Each name is a call of a loss. A criterion compares one prediction with one target, (pred, target), "
34
+ "and kalfa picks the two tensors; an objective reads the outputs, the batch and the run context "
35
+ "itself. Every loss writes the series <set>/<name> on the sets it runs on, such as train/mse and "
36
+ "val/mse. A loss that an optimizer minimizes gives the gradients; the others are computed without "
37
+ "gradients and only observe. sample_weight names a row weight of the data (data.weights) that "
38
+ "multiplies the loss of every row of a criterion."),
39
+ "metrics": ("Each name is a call of a metric, accumulated over a whole set and written as <set>/<name>, such as "
40
+ "val/rmse. A metric takes the same pred, target, on, every and rescale keys as a loss. rescale is on "
41
+ "by default when the target has an invertible preprocessor, so the metric speaks the original units. "
42
+ "A loss and a metric of the same name write the same series."),
43
+ "optimizers": ("Each name is a call of a torch optimizer (adam, adamw, sgd or an import path). over names the "
44
+ "parameters it steps, minimizes names the loss it steps on, and schedule multiplies the base lr "
45
+ "after every update. The history writes lr/<name> and minimizes/<name>. A rule may patch the "
46
+ "torch params and minimizes while the run goes on; over and schedule stay fixed."),
47
+ "train": ("The budget and the shape of the training. epochs (turns that each pass over the train set) or updates "
48
+ "({total, turn}) is required, never both. step is the algorithm that trains a turn (supervised when it "
49
+ "is not written), predicts the model whose outputs fill the predictions table, evaluate the sets "
50
+ "evaluated after every turn (the existing valid and test sets when it is not written), checkpoint and "
51
+ "report the weights kept and used at the end, track the values written as series and log the loggers."),
52
+ "rules": ("Each name is a rule, asked after every turn in the order of the config, after the evaluation and "
53
+ "before the checkpoints. A rule fires when its trigger (when) fires, and with after only from the turn "
54
+ "after the named rule fired; without when it fires as soon as it is ready. It fires once unless once "
55
+ "is false. Its effects are set (patches of plan paths, applied to the live objects), stop (the run "
56
+ "ends after the turn) and run (an action)."),
57
+ "plots": ("Each name is a call of a plot. Plots are drawn at the end of a run into <record>/plots from the "
58
+ "record files, and again at any time with kalfa plots RECORD. A std plot saves its image under its own "
59
+ "name, such as loss_curve.png; a plot of your own that returns its figure is saved under its key here."),
60
+ "generate": ("One call: the sampler. At the end of a run it draws n samples into <record>/samples/samples.pt, "
61
+ "with a png grid for images and a txt for text; kalfa generate RECORD draws more later, and the "
62
+ "write_samples action uses it from a rule when it names no sampler."),
63
+ "sweep": ("A sweep runs this config once per point, each point with other values of some params. strategy picks "
64
+ "the points, space gives the values of every swept param, objective ranks the points and record is the "
65
+ "sweep root whose numbered directories 0000, 0001 and so on hold the point records. kalfa sweep reads "
66
+ "this section."),
67
+ }
68
+
69
+ UNKNOWN_ABOUT = ("kalfa does not know the top level key {name}, so check reports it as unknown_section. The top "
70
+ "level keys are {keys}.")
71
+
72
+ DATA_KEYS = {
73
+ "file": ("the file to read; a relative path is looked for in the working directory, then next to the config "
74
+ "files"),
75
+ "folder": "the folder to read; every subdirectory is a class",
76
+ "split": "how the rows are cut into the train, valid and test sets: a list of ratios or a split call",
77
+ "batch": "the batch size, or a mapping of size, eval_size, workers, drop_last and shuffle",
78
+ "features": "a mapping of column patterns to preprocessors",
79
+ "targets": "a mapping of target columns to preprocessors",
80
+ "transform": "a list of image transforms applied to every set",
81
+ "train_transform": "a list of image transforms that replaces transform for the train set",
82
+ }
83
+
84
+ RUN_NOTES = {
85
+ "seed": "seeds every random stream (noise, samples, loader) and the splits",
86
+ "record": "the record directory the run writes; it must be new or empty, and every tool reads it afterwards",
87
+ }
88
+
89
+
90
+ def about_of(name):
91
+ if name in ABOUT:
92
+ return ABOUT[name]
93
+ return UNKNOWN_ABOUT.format(name=name, keys=and_list(TOP_LEVEL))
@@ -0,0 +1,102 @@
1
+ import inspect
2
+
3
+ from kalfa.commands.guide.words import flat, lower_first, short, unique
4
+ from kalfa.core import aliases
5
+
6
+
7
+ LEGO_KEYS = ("activation", "init", "schedule", "step", "strategy", "tokenizer", "when", "feed", "run", "log", "layers",
8
+ "each", "split", "transform", "train_transform", "sampler")
9
+ COLUMNS = ("features", "targets")
10
+
11
+
12
+ def use_of(value):
13
+ if isinstance(value, str) and value and not value.startswith(("@", "$")):
14
+ return value
15
+ if isinstance(value, dict) and isinstance(value.get("use"), str):
16
+ return value["use"]
17
+ return None
18
+
19
+
20
+ def lego_name(text):
21
+ return aliases.entry_of(text) is not None or aliases.is_import_path(text)
22
+
23
+
24
+ def legos_in(value, lego=False):
25
+ found = []
26
+ if isinstance(value, dict):
27
+ use = value.get("use")
28
+ if isinstance(use, str):
29
+ found.append(use)
30
+ for key, item in value.items():
31
+ if key != "use":
32
+ found += legos_in(item, key in LEGO_KEYS or (lego and not isinstance(use, str)))
33
+ elif isinstance(value, list):
34
+ for item in value:
35
+ found += legos_in(item, lego)
36
+ elif lego and isinstance(value, str) and lego_name(value):
37
+ found.append(value)
38
+ return found
39
+
40
+
41
+ def first_line(target):
42
+ own = target.__dict__.get("__doc__") if inspect.isclass(target) else getattr(target, "__doc__", None)
43
+ sentence = flat(own if isinstance(own, str) else "").split(". ")[0].rstrip(".")
44
+ if not sentence or len(sentence) > 140 or "`" in sentence or ":math:" in sentence:
45
+ return None
46
+ return lower_first(sentence)
47
+
48
+
49
+ def import_note(use, target):
50
+ if target is None:
51
+ return f"{use}: an import path"
52
+ kind = "class" if inspect.isclass(target) else "function" if inspect.isroutine(target) else "object"
53
+ name = getattr(target, "__name__", None) or type(target).__name__
54
+ line = first_line(target) if kind != "object" else None
55
+ return f"{use}: an import path to the {kind} {name}" + (f"; {line}" if line else "")
56
+
57
+
58
+ def named_for(use):
59
+ if not aliases.is_import_path(use):
60
+ return None
61
+ found = [entry for entry in aliases.ALIASES.values() if aliases.target_path(entry.target) == use]
62
+ found.sort(key=lambda entry: entry.plugin)
63
+ return found[0] if found else None
64
+
65
+
66
+ def known(use):
67
+ return aliases.entry_of(use) or named_for(use)
68
+
69
+
70
+ def lego_note(use, target=None):
71
+ entry = aliases.entry_of(use)
72
+ if entry is not None:
73
+ doc = lower_first(short(entry.doc))
74
+ if doc and doc.lower() != use.lower():
75
+ return f"{use}: {doc}"
76
+ return f"{use} is {aliases.target_path(entry.target)}"
77
+ twin = named_for(use)
78
+ if twin is not None:
79
+ doc = lower_first(short(twin.doc))
80
+ return f"{use}: the target of the short name {twin.name}" + (f", {doc}" if doc else "")
81
+ if aliases.is_import_path(use):
82
+ return import_note(use, target)
83
+ return f"{use} is not a short name kalfa knows"
84
+
85
+
86
+ def param_lines(use, value, skip=(), lead=""):
87
+ entry = known(use) if use else None
88
+ if entry is None or not isinstance(value, dict):
89
+ return []
90
+ return [f"{lead}{key}: {short(entry.params[key])}" for key in value
91
+ if key != "use" and key not in skip and key in entry.params]
92
+
93
+
94
+ def call_notes(seen, path, value, skip=()):
95
+ use = use_of(value)
96
+ if use is None:
97
+ return []
98
+ notes = [lego_note(use, seen.target(path))] + param_lines(use, value, skip)
99
+ inner = value.items() if isinstance(value, dict) else ()
100
+ nested = [name for key, item in inner if key != "use" and key not in skip
101
+ for name in legos_in(item, key in LEGO_KEYS)]
102
+ return notes + [lego_note(name, seen.found(name)) for name in unique(nested) if name != use]
@@ -0,0 +1,186 @@
1
+ from kalfa.commands.guide.about import DATA_KEYS, RUN_NOTES
2
+ from kalfa.commands.guide.calls import COLUMNS, LEGO_KEYS, call_notes, lego_note, legos_in, param_lines, use_of
3
+ from kalfa.commands.guide.seen import (layer_notes, override_notes, param_value_notes, patch_notes, ref_meaning,
4
+ ref_name, refs_of, swept)
5
+ from kalfa.commands.guide.words import and_list, short, unique, value_text
6
+ from kalfa.core import aliases
7
+ from kalfa.core.build import SLOT_KEYS
8
+ from kalfa.core.plan import sets_of
9
+
10
+
11
+ QUIET_KEYS = {"optimizers": ("over", "minimizes"), "rules": ("set",)}
12
+ PREDICTS = "its outputs fill the predictions table"
13
+ UNTABLED = "the step computes its outputs for every batch, and this data class writes no predictions table"
14
+
15
+
16
+ def series_note(seen, section, path):
17
+ node = seen.node(path) if section in ("losses", "metrics", "optimizers") else None
18
+ if node is None:
19
+ return []
20
+ if section == "optimizers":
21
+ return [f"series lr/{node.name} and minimizes/{node.name}"]
22
+ series = [f"{prefix}/{node.name}" for prefix in sets_of(node)]
23
+ series = ([f"train/{node.name}"] if section == "losses" else []) + series
24
+ return [f"series {and_list(unique(series))}"]
25
+
26
+
27
+ def common_notes(seen, section, value, path, keys):
28
+ hidden = QUIET_KEYS.get(section, ())
29
+ shown = {key: item for key, item in value.items() if key not in hidden} if isinstance(value, dict) else value
30
+ shown = None if path == "train.predicts" else shown
31
+ notes = [f"{text} is {ref_meaning(text)}" for text in unique(text for _, text in refs_of(shown))]
32
+ notes += param_value_notes(seen, value)
33
+ readers = seen.readers(path) if section in ("models", "losses", "metrics") else []
34
+ notes += [f"read as @{path} by {and_list(readers)}"] if readers else []
35
+ notes += patch_notes(seen, path) if section not in ("optimizers", "rules") else []
36
+ notes += series_note(seen, section, path)
37
+ return notes + layer_notes(seen, keys, value) + override_notes(seen, path)
38
+
39
+
40
+ def run_notes(seen, key, value, keys, path):
41
+ if key == "device":
42
+ device = seen.value("device")
43
+ if device in (None, "auto"):
44
+ return ["auto takes cuda when it is there, then mps, then cpu"]
45
+ return [f"the run trains on {value_text(device)}"]
46
+ if key == "include":
47
+ return ["merges these files below this one, so a key written here wins over theirs"]
48
+ if key == "plugins":
49
+ modules = getattr(seen.config, "plugins", None) or []
50
+ return [f"imported before the build: {and_list(modules)}; the names they register with kalfa.lego become "
51
+ "short names"]
52
+ return [RUN_NOTES[key]] if key in RUN_NOTES else []
53
+
54
+
55
+ def param_notes(seen, key, value, keys, path):
56
+ paths = seen.uses.get(str(key), [])
57
+ if paths:
58
+ shown = paths[:5] + ([f"{len(paths) - 5} more"] if len(paths) > 5 else [])
59
+ notes = [f"read as ${key}$ by {and_list(shown)}"]
60
+ else:
61
+ notes = [f"nothing reads ${key}$"]
62
+ sweep = swept(seen, str(key))
63
+ return notes + ([sweep] if sweep else [])
64
+
65
+
66
+ def split_text(ratios):
67
+ if not ratios or not all(isinstance(ratio, (int, float)) and not isinstance(ratio, bool) for ratio in ratios):
68
+ return None
69
+ names = ("train", "valid", "test")
70
+ shares = [f"{ratio * 100:g}% {name}" for name, ratio in zip(names, ratios) if ratio]
71
+ missing = [name for index, name in enumerate(names) if index >= len(ratios) or not ratios[index]]
72
+ tail = f", with no {and_list(missing)} set" if missing else ""
73
+ return f"a shuffled split: {and_list(shares)}{tail}, seeded by the top level seed"
74
+
75
+
76
+ def target_notes(targets):
77
+ notes = []
78
+ for name, chain in (targets or {}).items():
79
+ chain = chain if isinstance(chain, list) else [chain]
80
+ names = [use_of(item) for item in chain if item is not None]
81
+ if not names:
82
+ notes.append(f"{name} is kept raw")
83
+ elif "label" in names:
84
+ notes.append(f"{name} is a classification target: label gives int64 class codes")
85
+ else:
86
+ notes.append(f"{name} is preprocessed; predictions and rescaled metrics come back in its original units")
87
+ return notes
88
+
89
+
90
+ def data_notes(seen, key, value, keys, path):
91
+ if path == "data":
92
+ return call_notes(seen, "data", value)
93
+ target = seen.target("data")
94
+ if key == "use":
95
+ notes = [lego_note(value, target)]
96
+ inputs = getattr(target, "inputs", None)
97
+ exposes = getattr(target, "exposes", None)
98
+ if inputs:
99
+ notes.append(f"a model takes {and_list(inputs)} as inputs when it names no inputs")
100
+ if exposes:
101
+ notes.append(f"@data reads {and_list(exposes)} once it is fitted")
102
+ return notes
103
+ use = use_of(seen.mapping("data"))
104
+ entry = aliases.entry_of(use) if use else None
105
+ note = (entry.params.get(key) if entry is not None else None) or DATA_KEYS.get(key)
106
+ notes = [f"{key}: {short(note)}"] if note else []
107
+ resolved = seen.value(*keys)
108
+ if key == "split" and isinstance(resolved, list):
109
+ notes.append(split_text(resolved))
110
+ if key == "targets" and isinstance(resolved, dict):
111
+ notes += target_notes(resolved)
112
+ names = unique(legos_in(value, key in LEGO_KEYS or key in COLUMNS))
113
+ inner = use_of(value) if key in LEGO_KEYS and isinstance(value, dict) else None
114
+ if inner:
115
+ notes += [lego_note(inner)] + param_lines(inner, value)
116
+ return notes + [lego_note(name, seen.found(name)) for name in names if name != inner]
117
+
118
+
119
+ def model_key_notes(name, keys):
120
+ notes = []
121
+ inputs, outputs = keys.get("inputs"), keys.get("outputs")
122
+ if inputs is not None:
123
+ listed = inputs if isinstance(inputs, list) else [inputs]
124
+ notes.append(f"inputs: called with the batch fields {and_list(listed)} in this order")
125
+ if outputs is not None:
126
+ listed = outputs if isinstance(outputs, list) else [outputs]
127
+ notes.append(f"outputs: names what it returns {and_list(listed)}; a tensor takes the first name and a tuple "
128
+ "matches them in order")
129
+ else:
130
+ notes.append(f"without outputs, a tensor it returns is named {name}")
131
+ ema = keys.get("ema")
132
+ if ema is not None and ema is not False:
133
+ decay = ema.get("decay") if isinstance(ema, dict) else ema
134
+ every = ema.get("every") if isinstance(ema, dict) else None
135
+ text = "ema: keeps an exponential moving average copy"
136
+ text += f" with decay {value_text(decay)}" if decay is not None and decay is not True else ""
137
+ text += f", updated every {value_text(every)} steps" if every else ""
138
+ notes.append(text + f", read as @models.{name}.ema")
139
+ freeze = keys.get("freeze")
140
+ if freeze is True:
141
+ notes.append("freeze: every parameter is frozen and the module stays in eval mode")
142
+ elif freeze is False:
143
+ notes.append("freeze: false, nothing is frozen; a rule may patch it")
144
+ elif isinstance(freeze, (list, str)):
145
+ listed = freeze if isinstance(freeze, list) else [freeze]
146
+ notes.append(f"freeze: the parameters matching {and_list(listed)} are frozen")
147
+ weights = keys.get("weights")
148
+ if isinstance(weights, dict):
149
+ source = f" from the record {weights['run']}" if weights.get("run") else ""
150
+ notes.append(f"weights: loads the {weights.get('which') or 'last'} checkpoint of the model "
151
+ f"{weights.get('model') or name}{source}")
152
+ elif isinstance(weights, str):
153
+ notes.append(f"weights: loads the last checkpoint of the model {name} from the record {weights}")
154
+ init = use_of(keys.get("init"))
155
+ if init:
156
+ notes += [f"init: applies {init} to the module once it is built", lego_note(init)]
157
+ notes += param_lines(init, keys.get("init"), lead="init.")
158
+ return notes
159
+
160
+
161
+ def model_notes(seen, key, value, keys, path):
162
+ name = str(key)
163
+ notes = call_notes(seen, path, value, SLOT_KEYS["model"])
164
+ notes += model_key_notes(name, seen.mapping(*keys))
165
+ if seen.predicts() == name:
166
+ written = seen.mapping("train").get("predicts") is not None
167
+ how = "train.predicts names it" if written else "the only model, so it is the predicts model"
168
+ notes.append(f"{how}: {PREDICTS if seen.tabled() else UNTABLED}")
169
+ steppers = [opt for opt, item in seen.mapping("optimizers").items()
170
+ if isinstance(item, dict) and name in over_models(item.get("over"))]
171
+ if steppers:
172
+ notes.append(f"{and_list(steppers)} {'steps' if len(steppers) == 1 else 'step'} its parameters")
173
+ elif seen.mapping("optimizers"):
174
+ notes.append("no optimizer names it in over")
175
+ return notes
176
+
177
+
178
+ def over_models(over):
179
+ items = over if isinstance(over, list) else [over]
180
+ found = []
181
+ for item in items:
182
+ ref = item.get("of") if isinstance(item, dict) else item
183
+ name = ref_name(ref, "models")
184
+ if name:
185
+ found.append(name)
186
+ return found