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.
- kalfa/__init__.py +19 -0
- kalfa/api.py +284 -0
- kalfa/board/__init__.py +1 -0
- kalfa/board/answers.py +419 -0
- kalfa/board/server.py +536 -0
- kalfa/board/static/api.js +176 -0
- kalfa/board/static/app.js +122 -0
- kalfa/board/static/board.css +228 -0
- kalfa/board/static/charts.css +46 -0
- kalfa/board/static/components/badge.js +14 -0
- kalfa/board/static/components/chart.js +204 -0
- kalfa/board/static/components/diagram.js +857 -0
- kalfa/board/static/components/error.js +17 -0
- kalfa/board/static/components/loading.js +7 -0
- kalfa/board/static/components/progress.js +25 -0
- kalfa/board/static/components/settings.js +418 -0
- kalfa/board/static/components/spark.js +43 -0
- kalfa/board/static/components/table.js +156 -0
- kalfa/board/static/diagram.css +76 -0
- kalfa/board/static/format.js +138 -0
- kalfa/board/static/index.html +34 -0
- kalfa/board/static/plot.js +594 -0
- kalfa/board/static/predictions.css +20 -0
- kalfa/board/static/store.js +215 -0
- kalfa/board/static/views/compare.js +185 -0
- kalfa/board/static/views/home.js +90 -0
- kalfa/board/static/views/record/config.js +81 -0
- kalfa/board/static/views/record/curves.js +137 -0
- kalfa/board/static/views/record/data.js +378 -0
- kalfa/board/static/views/record/files.js +128 -0
- kalfa/board/static/views/record/log.js +164 -0
- kalfa/board/static/views/record/model.js +286 -0
- kalfa/board/static/views/record/overview.js +216 -0
- kalfa/board/static/views/record/plots.js +103 -0
- kalfa/board/static/views/record/predictions.js +834 -0
- kalfa/board/static/views/record.js +179 -0
- kalfa/board/static/views/runs.js +124 -0
- kalfa/board/static/views/sweep.js +282 -0
- kalfa/board/watch.py +87 -0
- kalfa/cli.py +371 -0
- kalfa/commands/__init__.py +0 -0
- kalfa/commands/common.py +8 -0
- kalfa/commands/export.py +117 -0
- kalfa/commands/generate.py +31 -0
- kalfa/commands/guide/__init__.py +101 -0
- kalfa/commands/guide/about.py +93 -0
- kalfa/commands/guide/calls.py +102 -0
- kalfa/commands/guide/sections.py +186 -0
- kalfa/commands/guide/seen.py +307 -0
- kalfa/commands/guide/show.py +106 -0
- kalfa/commands/guide/training.py +333 -0
- kalfa/commands/guide/words.py +97 -0
- kalfa/commands/legos.py +508 -0
- kalfa/commands/plots.py +27 -0
- kalfa/commands/report/__init__.py +64 -0
- kalfa/commands/report/page.py +112 -0
- kalfa/commands/report/predictions.py +216 -0
- kalfa/commands/report/sections.py +356 -0
- kalfa/commands/stop.py +60 -0
- kalfa/commands/sweep/__init__.py +202 -0
- kalfa/commands/sweep/checks.py +98 -0
- kalfa/commands/sweep/plan.py +183 -0
- kalfa/commands/sweep/points.py +123 -0
- kalfa/commands/sweep/runner.py +100 -0
- kalfa/commands/sweep/space.py +67 -0
- kalfa/commands/sweep/strategies.py +228 -0
- kalfa/core/__init__.py +0 -0
- kalfa/core/aliases.py +229 -0
- kalfa/core/build.py +727 -0
- kalfa/core/check.py +550 -0
- kalfa/core/coerce.py +36 -0
- kalfa/core/context.py +149 -0
- kalfa/core/device.py +63 -0
- kalfa/core/errors.py +45 -0
- kalfa/core/graph.py +304 -0
- kalfa/core/history.py +70 -0
- kalfa/core/loop.py +710 -0
- kalfa/core/make.py +121 -0
- kalfa/core/plan.py +156 -0
- kalfa/core/predict.py +290 -0
- kalfa/core/protocols.py +122 -0
- kalfa/core/record.py +347 -0
- kalfa/core/refs.py +88 -0
- kalfa/core/rng.py +100 -0
- kalfa/core/rules.py +513 -0
- kalfa/core/run.py +568 -0
- kalfa/core/spec.py +413 -0
- kalfa/core/state.py +100 -0
- kalfa/core/wrap.py +671 -0
- kalfa/records/__init__.py +24 -0
- kalfa/records/compare.py +86 -0
- kalfa/records/reader.py +561 -0
- kalfa/records/stats/__init__.py +36 -0
- kalfa/records/stats/choices.py +282 -0
- kalfa/records/stats/classes.py +236 -0
- kalfa/records/stats/measures.py +144 -0
- kalfa/records/stats/pairs.py +225 -0
- kalfa/records/stats/tables.py +153 -0
- kalfa/records/sweep.py +244 -0
- kalfa/std/__init__.py +17 -0
- kalfa/std/actions.py +35 -0
- kalfa/std/common.py +56 -0
- kalfa/std/data/__init__.py +1 -0
- kalfa/std/data/base.py +45 -0
- kalfa/std/data/columns.py +190 -0
- kalfa/std/data/flow.py +317 -0
- kalfa/std/data/images.py +443 -0
- kalfa/std/data/options.py +147 -0
- kalfa/std/data/pre.py +84 -0
- kalfa/std/data/splits.py +140 -0
- kalfa/std/data/stream.py +294 -0
- kalfa/std/data/table.py +642 -0
- kalfa/std/data/text.py +436 -0
- kalfa/std/data/transforms.py +327 -0
- kalfa/std/data/weights.py +113 -0
- kalfa/std/data/window.py +135 -0
- kalfa/std/figure.py +37 -0
- kalfa/std/init.py +72 -0
- kalfa/std/loggers.py +105 -0
- kalfa/std/losses.py +161 -0
- kalfa/std/metrics.py +463 -0
- kalfa/std/models.py +122 -0
- kalfa/std/notes.py +1553 -0
- kalfa/std/objectives.py +360 -0
- kalfa/std/plots/__init__.py +75 -0
- kalfa/std/plots/classification.py +207 -0
- kalfa/std/plots/common.py +44 -0
- kalfa/std/plots/curves.py +56 -0
- kalfa/std/plots/images.py +188 -0
- kalfa/std/plots/pairs.py +66 -0
- kalfa/std/plots/predictions.py +154 -0
- kalfa/std/samplers.py +261 -0
- kalfa/std/samples.py +72 -0
- kalfa/std/schedules.py +74 -0
- kalfa/std/steps.py +133 -0
- kalfa/std/triggers.py +135 -0
- kalfa/summary.py +296 -0
- kalfa/tui/__init__.py +4 -0
- kalfa/tui/app.py +107 -0
- kalfa/tui/config.py +61 -0
- kalfa/tui/guide.py +100 -0
- kalfa/tui/jobs.py +97 -0
- kalfa/tui/legos.py +62 -0
- kalfa/tui/records.py +439 -0
- kalfa-3.0.0.dev0.dist-info/METADATA +261 -0
- kalfa-3.0.0.dev0.dist-info/RECORD +148 -0
- kalfa-3.0.0.dev0.dist-info/WHEEL +4 -0
- kalfa-3.0.0.dev0.dist-info/entry_points.txt +2 -0
kalfa/core/check.py
ADDED
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import os
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
import torch
|
|
6
|
+
|
|
7
|
+
from . import aliases, build, plan, spec
|
|
8
|
+
from .context import train_of
|
|
9
|
+
from .errors import ProblemsError, RunError, error, warning
|
|
10
|
+
from .protocols import ContextMetric, Objective
|
|
11
|
+
from .refs import SINGULAR, join_tokens, ref_parts, tokenize
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
TRAIN_KEYS = ("epochs", "updates", "step", "predicts", "evaluate", "checkpoint", "report", "track", "log")
|
|
15
|
+
CHECKPOINT_KEYS = ("watch", "mode", "last", "every")
|
|
16
|
+
SECTIONS = ("models", "losses", "metrics", "optimizers")
|
|
17
|
+
SET_NAMES = ("train", "valid", "test")
|
|
18
|
+
SERIES_PREFIXES = ("train", "val", "test", "track", "lr", "minimizes")
|
|
19
|
+
STD_STEPS = ("supervised", "alternating")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Checked:
|
|
24
|
+
config: object
|
|
25
|
+
tree: object
|
|
26
|
+
plan: dict | None
|
|
27
|
+
problems: list = field(default_factory=list)
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def errors(self):
|
|
31
|
+
return [problem for problem in self.problems if problem.severity == "error"]
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def warnings(self):
|
|
35
|
+
return [problem for problem in self.problems if problem.severity == "warning"]
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def ok(self):
|
|
39
|
+
return not self.errors
|
|
40
|
+
|
|
41
|
+
def text(self):
|
|
42
|
+
return "\n".join(str(problem) for problem in self.problems) or "ok"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def merge(into, more):
|
|
46
|
+
for problem in more or ():
|
|
47
|
+
if problem not in into:
|
|
48
|
+
into.append(problem)
|
|
49
|
+
return into
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def has_errors(problems):
|
|
53
|
+
return any(problem.severity == "error" for problem in problems)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def positive_int(value):
|
|
57
|
+
return isinstance(value, int) and not isinstance(value, bool) and value > 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def ref_target(value):
|
|
61
|
+
if isinstance(value, build.Ref):
|
|
62
|
+
return value.path, value.attr
|
|
63
|
+
text = ref_parts(value)
|
|
64
|
+
if text is None:
|
|
65
|
+
return None, None
|
|
66
|
+
try:
|
|
67
|
+
tokens = tokenize(text)
|
|
68
|
+
except RunError:
|
|
69
|
+
return None, None
|
|
70
|
+
split = 2 if tokens[0] in SECTIONS else 1
|
|
71
|
+
return join_tokens(tokens[:split]), join_tokens(tokens[split:]) or None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def ref_text(value):
|
|
75
|
+
return value.text if isinstance(value, build.Ref) else value
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def mutable_set(tree):
|
|
79
|
+
found = build.mutable_paths(tree)
|
|
80
|
+
if isinstance(found, dict):
|
|
81
|
+
found = found.keys()
|
|
82
|
+
return {str(path) for path in found or ()}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def nested_paths(prefix, value):
|
|
86
|
+
paths = set()
|
|
87
|
+
if isinstance(value, dict) and "use" not in value:
|
|
88
|
+
for key, inner in value.items():
|
|
89
|
+
leaf = f"{prefix}.{key}"
|
|
90
|
+
paths.add(leaf)
|
|
91
|
+
paths |= nested_paths(leaf, inner)
|
|
92
|
+
elif isinstance(value, (list, tuple)):
|
|
93
|
+
for index, inner in enumerate(value):
|
|
94
|
+
leaf = f"{prefix}[{index}]"
|
|
95
|
+
paths.add(leaf)
|
|
96
|
+
paths |= nested_paths(leaf, inner)
|
|
97
|
+
return paths
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def known_paths(tree):
|
|
101
|
+
paths = set()
|
|
102
|
+
for node in tree.nodes.values():
|
|
103
|
+
paths.add(node.path)
|
|
104
|
+
paths.update(f"{node.path}.{key}" for key in build.SLOT_KEYS.get(node.slot, ()))
|
|
105
|
+
for bucket in (node.params, node.keys):
|
|
106
|
+
for key, value in (bucket or {}).items():
|
|
107
|
+
leaf = f"{node.path}.{key}"
|
|
108
|
+
paths.add(leaf)
|
|
109
|
+
paths |= nested_paths(leaf, value)
|
|
110
|
+
for key, value in train_of(tree).items():
|
|
111
|
+
paths.add(f"train.{key}")
|
|
112
|
+
paths |= nested_paths(f"train.{key}", value)
|
|
113
|
+
return paths
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def node_for(tree, path):
|
|
117
|
+
try:
|
|
118
|
+
tokens = tokenize(path)
|
|
119
|
+
except RunError:
|
|
120
|
+
return None, None
|
|
121
|
+
for count in range(len(tokens), 0, -1):
|
|
122
|
+
text = join_tokens(tokens[:count])
|
|
123
|
+
if text in tree.nodes:
|
|
124
|
+
return tree.nodes[text], tokens[count:]
|
|
125
|
+
return None, tokens
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def nearest(tree, node, mutable, path):
|
|
129
|
+
if node is not None:
|
|
130
|
+
under = sorted(item for item in mutable if item.startswith((f"{node.path}.", f"{node.path}[")))
|
|
131
|
+
if under:
|
|
132
|
+
return f"mutable under {node.path}: " + ", ".join(under)
|
|
133
|
+
return f"nothing under {node.path} can be set by a rule"
|
|
134
|
+
section = path.split(".", 1)[0].split("[", 1)[0]
|
|
135
|
+
if section == "train":
|
|
136
|
+
under = sorted(item for item in mutable if item.startswith("train."))
|
|
137
|
+
return "mutable under train: " + ", ".join(under) if under else "nothing under train can be set by a rule"
|
|
138
|
+
names = sorted(tree.section(section)) if section in SECTIONS else []
|
|
139
|
+
if names:
|
|
140
|
+
return f"the {section} are " + ", ".join(names)
|
|
141
|
+
return "a path starts with " + ", ".join(SECTIONS + ("train",))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def check_set_path(tree, rule, path, mutable, known, problems):
|
|
145
|
+
if path in mutable:
|
|
146
|
+
return
|
|
147
|
+
where = f"rules.{rule.name}.set"
|
|
148
|
+
node, _ = node_for(tree, path)
|
|
149
|
+
hint = nearest(tree, node, mutable, path)
|
|
150
|
+
if path in known:
|
|
151
|
+
problems.append(error("not_mutable", f"{path} is fixed and cannot be set by a rule", where, hint=hint))
|
|
152
|
+
else:
|
|
153
|
+
problems.append(error("unknown_path", f"{path} is not a path of the plan", where, hint=hint))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def check_set_value(tree, rule, path, value, problems):
|
|
157
|
+
if ref_parts(value) is None:
|
|
158
|
+
return
|
|
159
|
+
where = f"rules.{rule.name}.set"
|
|
160
|
+
target, _ = ref_target(value)
|
|
161
|
+
if target is None:
|
|
162
|
+
problems.append(error("rule_ref", f"{path} is set to {value!r}, which is not a reference", where))
|
|
163
|
+
return
|
|
164
|
+
if target not in tree.nodes:
|
|
165
|
+
section = target.split(".", 1)[0]
|
|
166
|
+
names = sorted(tree.section(section)) if section in SECTIONS else []
|
|
167
|
+
hint = f"the {section} are " + ", ".join(names) if names else None
|
|
168
|
+
problems.append(error("rule_ref", f"{path} is set to {value!r}, which names no node", where, hint=hint))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def check_cycles(specs, problems):
|
|
172
|
+
after = {rule.name: rule.after for rule in specs}
|
|
173
|
+
reported = set()
|
|
174
|
+
for name in after:
|
|
175
|
+
seen = []
|
|
176
|
+
current = name
|
|
177
|
+
while current is not None and current in after and current not in seen:
|
|
178
|
+
seen.append(current)
|
|
179
|
+
current = after[current]
|
|
180
|
+
if current == name and tuple(sorted(seen)) not in reported:
|
|
181
|
+
reported.add(tuple(sorted(seen)))
|
|
182
|
+
problems.append(error("rule_after", "rules " + ", ".join(seen) + " wait for each other and can never fire",
|
|
183
|
+
f"rules.{name}.after"))
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def data_weights(tree):
|
|
187
|
+
node = tree.nodes.get("data")
|
|
188
|
+
weights = (node.params or {}).get("weights") if node is not None else None
|
|
189
|
+
return [str(name) for name in weights] if isinstance(weights, dict) else []
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def data_targets(tree):
|
|
193
|
+
node = tree.nodes.get("data")
|
|
194
|
+
if node is None:
|
|
195
|
+
return False, []
|
|
196
|
+
params = node.params or {}
|
|
197
|
+
table = node.use == "table" or "targets" in params or getattr(node.target, "__name__", "") == "Table"
|
|
198
|
+
targets = params.get("targets")
|
|
199
|
+
names = [str(name) for name in targets] if isinstance(targets, dict) else []
|
|
200
|
+
return table, names
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def check_train(tree, problems):
|
|
204
|
+
settings = train_of(tree)
|
|
205
|
+
keys_hint = "the train keys are " + ", ".join(TRAIN_KEYS)
|
|
206
|
+
for key in settings:
|
|
207
|
+
if key not in TRAIN_KEYS:
|
|
208
|
+
problems.append(error("train_key", f"unknown train key {key!r}", f"train.{key}", hint=keys_hint))
|
|
209
|
+
epochs = settings.get("epochs")
|
|
210
|
+
updates = settings.get("updates")
|
|
211
|
+
if epochs is not None and updates is not None:
|
|
212
|
+
problems.append(error("train_unit", "train takes epochs or updates, not both", "train"))
|
|
213
|
+
elif epochs is None and updates is None:
|
|
214
|
+
problems.append(error("train_unit", "train needs epochs or updates: {total, turn}", "train"))
|
|
215
|
+
if epochs is not None and not positive_int(epochs):
|
|
216
|
+
problems.append(error("train_unit", f"epochs is a positive integer, got {epochs!r}", "train.epochs"))
|
|
217
|
+
if updates is not None:
|
|
218
|
+
if not isinstance(updates, dict) or not positive_int(updates.get("total")):
|
|
219
|
+
problems.append(error("train_unit", "updates is a mapping {total, turn} with a positive integer total",
|
|
220
|
+
"train.updates"))
|
|
221
|
+
else:
|
|
222
|
+
for key in updates:
|
|
223
|
+
if key not in ("total", "turn"):
|
|
224
|
+
problems.append(error("train_key", f"unknown updates key {key!r}", f"train.updates.{key}",
|
|
225
|
+
hint="the updates keys are total and turn"))
|
|
226
|
+
if "turn" in updates and not positive_int(updates["turn"]):
|
|
227
|
+
problems.append(error("train_unit", f"updates.turn is a positive integer, got {updates['turn']!r}",
|
|
228
|
+
"train.updates.turn"))
|
|
229
|
+
report = settings.get("report", "last")
|
|
230
|
+
checkpoint = settings.get("checkpoint")
|
|
231
|
+
if report not in ("best", "last"):
|
|
232
|
+
problems.append(error("train_report", f"report is best or last, got {report!r}", "train.report"))
|
|
233
|
+
elif report == "best" and not (isinstance(checkpoint, dict) and checkpoint.get("watch")):
|
|
234
|
+
problems.append(error("report_best", "report: best needs a checkpoint with a watch series", "train.report",
|
|
235
|
+
hint="add checkpoint: {watch: val/<metric>}"))
|
|
236
|
+
if checkpoint is not None:
|
|
237
|
+
check_checkpoint(checkpoint, problems)
|
|
238
|
+
evaluate = settings.get("evaluate")
|
|
239
|
+
if evaluate is not None:
|
|
240
|
+
check_evaluate(tree, evaluate, problems)
|
|
241
|
+
predicts = settings.get("predicts")
|
|
242
|
+
if predicts is not None and predicts is not False:
|
|
243
|
+
target, attr = ref_target(predicts)
|
|
244
|
+
if target is None or attr or not target.startswith("models.") or target not in tree.nodes:
|
|
245
|
+
names = sorted(tree.section("models"))
|
|
246
|
+
problems.append(error("predicts_ref", f"predicts names a model like @models.<name>, got {predicts!r}",
|
|
247
|
+
"train.predicts", hint="the models are " + ", ".join(names) if names else None))
|
|
248
|
+
track = settings.get("track")
|
|
249
|
+
if track is not None and (not isinstance(track, list) or not all(isinstance(item, str) for item in track)):
|
|
250
|
+
problems.append(error("train_track", f"track is a list of strings, got {track!r}", "train.track"))
|
|
251
|
+
elif track:
|
|
252
|
+
for path in track:
|
|
253
|
+
check_track_path(tree, path, problems)
|
|
254
|
+
log = settings.get("log")
|
|
255
|
+
entries = [log] if isinstance(log, (str, dict)) else log
|
|
256
|
+
if log is not None and (not isinstance(entries, list)
|
|
257
|
+
or not all(isinstance(item, str) or (isinstance(item, dict) and "use" in item)
|
|
258
|
+
for item in entries)):
|
|
259
|
+
problems.append(error("train_log", f"log is a list of logger names or calls, got {log!r}", "train.log"))
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def check_track_path(tree, path, problems):
|
|
263
|
+
where = "train.track"
|
|
264
|
+
try:
|
|
265
|
+
tokens = tokenize(path)
|
|
266
|
+
except RunError as failure:
|
|
267
|
+
problems.append(error("track_path", str(failure), where))
|
|
268
|
+
return
|
|
269
|
+
root = tokens[0]
|
|
270
|
+
if root == "train":
|
|
271
|
+
return
|
|
272
|
+
if root not in SECTIONS:
|
|
273
|
+
problems.append(error("unknown_path", f"{path} is not a path of the plan", where,
|
|
274
|
+
hint="a track path starts with " + ", ".join(SECTIONS + ("train",))))
|
|
275
|
+
return
|
|
276
|
+
if len(tokens) < 3 or not isinstance(tokens[1], str) or f"{root}.{tokens[1]}" not in tree.nodes:
|
|
277
|
+
names = sorted(tree.section(root))
|
|
278
|
+
hint = f"the {root} are " + ", ".join(names) if names else f"there are no {root}"
|
|
279
|
+
problems.append(error("unknown_path", f"{path} names no {SINGULAR[root]} of the plan and one of its "
|
|
280
|
+
f"values, like {root}.<name>.<param>", where, hint=hint))
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def check_checkpoint(checkpoint, problems):
|
|
284
|
+
if not isinstance(checkpoint, dict):
|
|
285
|
+
problems.append(error("checkpoint_value", "checkpoint is a mapping {watch, mode, last, every}",
|
|
286
|
+
"train.checkpoint"))
|
|
287
|
+
return
|
|
288
|
+
for key in checkpoint:
|
|
289
|
+
if key not in CHECKPOINT_KEYS:
|
|
290
|
+
problems.append(error("checkpoint_key", f"unknown checkpoint key {key!r}", f"train.checkpoint.{key}",
|
|
291
|
+
hint="the checkpoint keys are " + ", ".join(CHECKPOINT_KEYS)))
|
|
292
|
+
watch = checkpoint.get("watch")
|
|
293
|
+
if watch is not None and (not isinstance(watch, str) or not watch):
|
|
294
|
+
problems.append(error("checkpoint_watch", f"watch is a series name like val/rmse, got {watch!r}",
|
|
295
|
+
"train.checkpoint.watch"))
|
|
296
|
+
mode = checkpoint.get("mode", "min")
|
|
297
|
+
if mode not in ("min", "max"):
|
|
298
|
+
problems.append(error("checkpoint_value", f"mode is min or max, got {mode!r}", "train.checkpoint.mode"))
|
|
299
|
+
every = checkpoint.get("every")
|
|
300
|
+
if every is not None and not positive_int(every):
|
|
301
|
+
problems.append(error("checkpoint_value", f"every is a positive integer, got {every!r}",
|
|
302
|
+
"train.checkpoint.every"))
|
|
303
|
+
last = checkpoint.get("last", True)
|
|
304
|
+
if not isinstance(last, bool):
|
|
305
|
+
problems.append(error("checkpoint_value", f"last is true or false, got {last!r}", "train.checkpoint.last"))
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def check_evaluate(tree, evaluate, problems):
|
|
309
|
+
if not isinstance(evaluate, list) or not all(isinstance(name, str) and name for name in evaluate):
|
|
310
|
+
problems.append(error("evaluate_set", f"evaluate is a list of set names, got {evaluate!r}", "train.evaluate"))
|
|
311
|
+
return
|
|
312
|
+
if len(set(evaluate)) != len(evaluate):
|
|
313
|
+
problems.append(error("evaluate_set", "evaluate names a set twice", "train.evaluate"))
|
|
314
|
+
table, _ = data_targets(tree)
|
|
315
|
+
unknown = [name for name in evaluate if name not in SET_NAMES]
|
|
316
|
+
if table and unknown:
|
|
317
|
+
problems.append(error("evaluate_set", "evaluate names " + ", ".join(unknown) + ", which a table does not make",
|
|
318
|
+
"train.evaluate", hint="the sets of a table are " + ", ".join(SET_NAMES)))
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def step_is_std(tree):
|
|
322
|
+
node = tree.nodes.get("train.step")
|
|
323
|
+
if node is None or node.use in STD_STEPS:
|
|
324
|
+
return True
|
|
325
|
+
return getattr(node.target, "__module__", "").startswith("kalfa.std")
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def check_optimizers(tree, problems):
|
|
329
|
+
make = error if step_is_std(tree) else warning
|
|
330
|
+
losses = sorted(tree.section("losses"))
|
|
331
|
+
hint = "the losses are " + ", ".join(losses) if losses else "there are no losses"
|
|
332
|
+
for node in tree.section("optimizers").values():
|
|
333
|
+
minimizes = (node.keys or {}).get("minimizes", (node.params or {}).get("minimizes"))
|
|
334
|
+
if minimizes is None:
|
|
335
|
+
problems.append(make("minimizes_required", f"optimizer {node.name!r} names no loss to minimize", node.path,
|
|
336
|
+
hint='add minimizes: "@losses.<name>"'))
|
|
337
|
+
continue
|
|
338
|
+
target, attr = ref_target(minimizes)
|
|
339
|
+
if target is None or attr or not target.startswith("losses.") or target not in tree.nodes:
|
|
340
|
+
problems.append(error("minimizes_ref", f"minimizes references a loss like @losses.<name>, got "
|
|
341
|
+
f"{ref_text(minimizes)!r}", f"{node.path}.minimizes", hint=hint))
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def is_criterion(node):
|
|
345
|
+
target = node.target
|
|
346
|
+
if target is None:
|
|
347
|
+
return False
|
|
348
|
+
if inspect.isclass(target) and issubclass(target, (Objective, ContextMetric)):
|
|
349
|
+
return False
|
|
350
|
+
return True
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def output_names(tree):
|
|
354
|
+
outputs = []
|
|
355
|
+
for node in tree.section("models").values():
|
|
356
|
+
names = (node.keys or {}).get("outputs")
|
|
357
|
+
if isinstance(names, str):
|
|
358
|
+
names = [names]
|
|
359
|
+
if isinstance(names, (list, tuple)) and names:
|
|
360
|
+
outputs.extend(str(name) for name in names)
|
|
361
|
+
else:
|
|
362
|
+
outputs.append(node.name)
|
|
363
|
+
return outputs
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def check_criteria(tree, problems):
|
|
367
|
+
models = tree.section("models")
|
|
368
|
+
predicts = train_of(tree).get("predicts")
|
|
369
|
+
target, attr = ref_target(predicts) if predicts is not None else (None, None)
|
|
370
|
+
predicts_known = (len(models) == 1 and predicts is not False) or (target in tree.nodes and not attr)
|
|
371
|
+
outputs = output_names(tree)
|
|
372
|
+
table, targets = data_targets(tree)
|
|
373
|
+
weights = data_weights(tree)
|
|
374
|
+
outputs_hint = "the outputs are " + ", ".join(outputs) if outputs else None
|
|
375
|
+
for section in ("losses", "metrics"):
|
|
376
|
+
kind = SINGULAR[section]
|
|
377
|
+
for node in tree.section(section).values():
|
|
378
|
+
if not is_criterion(node):
|
|
379
|
+
if (node.keys or {}).get("sample_weight") is not None:
|
|
380
|
+
problems.append(error("weight_objective", f"{kind} {node.name!r} is an objective; sample_weight "
|
|
381
|
+
"belongs to a criterion, a loss of (pred, target)",
|
|
382
|
+
f"{node.path}.sample_weight",
|
|
383
|
+
hint="an objective reads the weight field from the batch itself"))
|
|
384
|
+
continue
|
|
385
|
+
keys = node.keys or {}
|
|
386
|
+
pred = keys.get("pred")
|
|
387
|
+
target = keys.get("target")
|
|
388
|
+
if pred is None and not predicts_known:
|
|
389
|
+
problems.append(error("pred_required", f"{kind} {node.name!r} needs pred: <output> because no single "
|
|
390
|
+
"model predicts", node.path,
|
|
391
|
+
hint=outputs_hint or "set train.predicts to the model whose output it compares"))
|
|
392
|
+
elif pred is not None and outputs and pred not in outputs:
|
|
393
|
+
problems.append(warning("unknown_output", f"pred {pred!r} is no output of a model", f"{node.path}.pred",
|
|
394
|
+
hint=outputs_hint))
|
|
395
|
+
if table and target is None and len(targets) != 1:
|
|
396
|
+
hint = "the targets are " + ", ".join(targets) if targets else "the data declares no targets"
|
|
397
|
+
problems.append(error("target_required", f"{kind} {node.name!r} needs target: <name> because the data "
|
|
398
|
+
f"has {len(targets)} targets", node.path, hint=hint))
|
|
399
|
+
weight = keys.get("sample_weight")
|
|
400
|
+
if table and weight is not None and weight not in weights:
|
|
401
|
+
hint = ("the weights are " + ", ".join(weights) if weights
|
|
402
|
+
else 'declare it under data.weights, such as weights: {w: {"price < 5": 5}}')
|
|
403
|
+
problems.append(error("unknown_weight", f"sample_weight {weight!r} is no weight of the data",
|
|
404
|
+
f"{node.path}.sample_weight", hint=hint))
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def check_device(tree, problems):
|
|
408
|
+
data = getattr(getattr(tree, "config", None), "data", None)
|
|
409
|
+
device = data.get("device") if isinstance(data, dict) else None
|
|
410
|
+
if device is None or device == "auto":
|
|
411
|
+
return
|
|
412
|
+
if not isinstance(device, str):
|
|
413
|
+
problems.append(error("device_value", f"device is a string like cpu, cuda:0 or auto, got {device!r}", "device"))
|
|
414
|
+
return
|
|
415
|
+
try:
|
|
416
|
+
torch.device(device)
|
|
417
|
+
except (RuntimeError, TypeError, ValueError):
|
|
418
|
+
problems.append(error("device_value", f"{device!r} is not a device torch knows", "device",
|
|
419
|
+
hint="use auto, cpu, mps, cuda or cuda:<index>"))
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def check_series(text, names, prefixes, where, problems, strict=False):
|
|
423
|
+
make = error if strict else warning
|
|
424
|
+
code = "watch_unknown" if strict else "unknown_series"
|
|
425
|
+
prefix, _, rest = text.partition("/")
|
|
426
|
+
if not rest or prefix not in prefixes:
|
|
427
|
+
problems.append(make(code, f"{text!r} does not look like a series such as val/<metric>", where))
|
|
428
|
+
return
|
|
429
|
+
if prefix in ("train", "val", "test") and rest.split("/")[0] not in names:
|
|
430
|
+
problems.append(make(code, f"{text!r} names no loss or metric", where,
|
|
431
|
+
hint="the names are " + ", ".join(sorted(names)) if names else None))
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def check_watches(tree, problems):
|
|
435
|
+
names = set(tree.section("losses")) | set(tree.section("metrics"))
|
|
436
|
+
settings = train_of(tree)
|
|
437
|
+
prefixes = set(SERIES_PREFIXES)
|
|
438
|
+
evaluate = settings.get("evaluate")
|
|
439
|
+
if isinstance(evaluate, list):
|
|
440
|
+
prefixes.update(str(name) for name in evaluate)
|
|
441
|
+
checkpoint = settings.get("checkpoint")
|
|
442
|
+
if isinstance(checkpoint, dict) and isinstance(checkpoint.get("watch"), str) and checkpoint["watch"]:
|
|
443
|
+
check_series(checkpoint["watch"], names, prefixes, "train.checkpoint.watch", problems,
|
|
444
|
+
strict=settings.get("report") == "best")
|
|
445
|
+
for node in tree.nodes.values():
|
|
446
|
+
watch = (node.params or {}).get("watch")
|
|
447
|
+
if node.path.endswith(".when") and isinstance(watch, str) and watch:
|
|
448
|
+
check_series(watch, names, prefixes, f"{node.path}.watch", problems)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def check_names(tree, problems):
|
|
452
|
+
shared = set(tree.section("losses")) & set(tree.section("metrics"))
|
|
453
|
+
for name in sorted(shared):
|
|
454
|
+
problems.append(warning("series_clash", f"loss {name!r} and metric {name!r} write the same series "
|
|
455
|
+
f"train/{name}, val/{name} and test/{name}; the metric value is kept",
|
|
456
|
+
f"metrics.{name}", hint="rename one of them to record both"))
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def check_data(tree, problems):
|
|
460
|
+
node = tree.nodes.get("data")
|
|
461
|
+
if node is None or node.target is None:
|
|
462
|
+
return
|
|
463
|
+
checker = inspect.getattr_static(node.target, "check", None)
|
|
464
|
+
if not isinstance(checker, (classmethod, staticmethod)):
|
|
465
|
+
return
|
|
466
|
+
directories = getattr(getattr(tree, "config", None), "directories", None) or ()
|
|
467
|
+
node.target.check(dict(node.params or {}), problems, directories)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def check_data_refs(tree, problems):
|
|
471
|
+
node = tree.nodes.get("data")
|
|
472
|
+
exposes = getattr(node.target, "exposes", None) if node is not None and node.target is not None else None
|
|
473
|
+
if exposes is None:
|
|
474
|
+
return
|
|
475
|
+
exposes = tuple(str(name) for name in exposes)
|
|
476
|
+
for owner in tree.nodes.values():
|
|
477
|
+
for ref in owner.refs:
|
|
478
|
+
if ref.path != "data" or not ref.attr:
|
|
479
|
+
continue
|
|
480
|
+
first = ref.attr.split(".")[0]
|
|
481
|
+
if first not in exposes:
|
|
482
|
+
problems.append(error("reference_unknown", f"{ref.at}: {ref.text} is not exposed", owner.path,
|
|
483
|
+
hint="the data class exposes " + ", ".join(exposes)))
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def check_actions(tree, problems):
|
|
487
|
+
has_generate = "generate" in tree.nodes
|
|
488
|
+
for node in tree.nodes.values():
|
|
489
|
+
if node.slot != "action" or not getattr(node.target, "requires_generate", False):
|
|
490
|
+
continue
|
|
491
|
+
if node.params.get("sampler") is None and not has_generate:
|
|
492
|
+
problems.append(error("generate_required", f"{node.use} needs a sampler or a generate section", node.path,
|
|
493
|
+
hint="write sampler: {use: ...} on the action or a generate section"))
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def check_sweep(tree, problems):
|
|
497
|
+
raw = tree.settings.get("sweep") if isinstance(tree.settings, dict) else None
|
|
498
|
+
if raw is None:
|
|
499
|
+
return
|
|
500
|
+
from kalfa.commands.sweep import check_sweep as checker
|
|
501
|
+
|
|
502
|
+
checker(tree, raw, problems)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def validate(tree):
|
|
506
|
+
problems = []
|
|
507
|
+
specs = tree.rules
|
|
508
|
+
mutable = mutable_set(tree)
|
|
509
|
+
known = known_paths(tree)
|
|
510
|
+
for rule in specs:
|
|
511
|
+
for path, value in rule.set.items():
|
|
512
|
+
check_set_path(tree, rule, path, mutable, known, problems)
|
|
513
|
+
check_set_value(tree, rule, path, value, problems)
|
|
514
|
+
check_cycles(specs, problems)
|
|
515
|
+
check_data(tree, problems)
|
|
516
|
+
check_data_refs(tree, problems)
|
|
517
|
+
check_train(tree, problems)
|
|
518
|
+
check_optimizers(tree, problems)
|
|
519
|
+
check_criteria(tree, problems)
|
|
520
|
+
check_device(tree, problems)
|
|
521
|
+
check_watches(tree, problems)
|
|
522
|
+
check_names(tree, problems)
|
|
523
|
+
check_actions(tree, problems)
|
|
524
|
+
check_sweep(tree, problems)
|
|
525
|
+
return problems
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def check(paths=(), sets=(), params=(), mappings=()):
|
|
529
|
+
config = spec.load(paths=paths, sets=sets, params=params, mappings=mappings)
|
|
530
|
+
problems = merge([], config.problems)
|
|
531
|
+
for directory in config.directories:
|
|
532
|
+
aliases.add_search_dir(directory)
|
|
533
|
+
aliases.add_search_dir(os.getcwd())
|
|
534
|
+
if has_errors(problems):
|
|
535
|
+
return Checked(config, None, None, problems)
|
|
536
|
+
aliases.import_plugins(config.plugins, problems)
|
|
537
|
+
if has_errors(problems):
|
|
538
|
+
return Checked(config, None, None, problems)
|
|
539
|
+
try:
|
|
540
|
+
tree = build.build(config)
|
|
541
|
+
except ProblemsError as failure:
|
|
542
|
+
return Checked(config, None, None, merge(problems, failure.problems))
|
|
543
|
+
merge(problems, tree.problems)
|
|
544
|
+
merge(problems, validate(tree))
|
|
545
|
+
made = None
|
|
546
|
+
if not has_errors(problems):
|
|
547
|
+
from kalfa import __version__
|
|
548
|
+
|
|
549
|
+
made = plan.plan_of(tree, __version__)
|
|
550
|
+
return Checked(config, tree, made, problems)
|
kalfa/core/coerce.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import numbers
|
|
3
|
+
|
|
4
|
+
import torch
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
TENSORS = ("weight", "pos_weight")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_loss(target):
|
|
11
|
+
return inspect.isclass(target) and issubclass(target, torch.nn.modules.loss._Loss)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def numeric(value):
|
|
15
|
+
if isinstance(value, bool):
|
|
16
|
+
return False
|
|
17
|
+
if isinstance(value, numbers.Number):
|
|
18
|
+
return True
|
|
19
|
+
return isinstance(value, list) and bool(value) and all(numeric(item) for item in value)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def coerced(target, kwargs):
|
|
23
|
+
try:
|
|
24
|
+
parameters = inspect.signature(target).parameters
|
|
25
|
+
except (TypeError, ValueError):
|
|
26
|
+
return kwargs
|
|
27
|
+
found = dict(kwargs)
|
|
28
|
+
for name, value in kwargs.items():
|
|
29
|
+
parameter = parameters.get(name)
|
|
30
|
+
if parameter is None:
|
|
31
|
+
continue
|
|
32
|
+
if isinstance(value, list) and isinstance(parameter.default, tuple):
|
|
33
|
+
found[name] = tuple(value)
|
|
34
|
+
elif name in TENSORS and is_loss(target) and numeric(value):
|
|
35
|
+
found[name] = torch.as_tensor(value, dtype=torch.float32)
|
|
36
|
+
return found
|