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/build.py
ADDED
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import inspect
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from . import aliases, wrap
|
|
6
|
+
from .coerce import coerced
|
|
7
|
+
from .context import train_of
|
|
8
|
+
from .errors import BuildError, KalfaError, ProblemsError, error
|
|
9
|
+
from .make import build_model, build_optimizer
|
|
10
|
+
from .rules import parse_rules
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
SLOT_KEYS = {
|
|
14
|
+
"data": (),
|
|
15
|
+
"model": ("inputs", "outputs", "ema", "freeze", "weights", "init"),
|
|
16
|
+
"loss": ("pred", "target", "on", "every", "rescale", "sample_weight"),
|
|
17
|
+
"metric": ("pred", "target", "on", "every", "rescale"),
|
|
18
|
+
"optimizer": ("over", "minimizes", "schedule"),
|
|
19
|
+
"step": ("order", "amp", "grad_clip", "accumulate"),
|
|
20
|
+
"trigger": (),
|
|
21
|
+
"action": (),
|
|
22
|
+
"logger": (),
|
|
23
|
+
"schedule": (),
|
|
24
|
+
"plot": (),
|
|
25
|
+
"sampler": (),
|
|
26
|
+
"strategy": (),
|
|
27
|
+
"value": (),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
SECTION_SLOTS = {
|
|
31
|
+
"data": "data",
|
|
32
|
+
"models": "model",
|
|
33
|
+
"losses": "loss",
|
|
34
|
+
"metrics": "metric",
|
|
35
|
+
"optimizers": "optimizer",
|
|
36
|
+
"plots": "plot",
|
|
37
|
+
"generate": "sampler",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
ORDER = ("data", "models", "losses", "metrics", "optimizers", "train.step", "plots", "generate")
|
|
41
|
+
|
|
42
|
+
MODEL_FIXED = ("inputs", "outputs", "ema", "weights", "init")
|
|
43
|
+
OPTIMIZER_FIXED = ("over", "schedule")
|
|
44
|
+
CALL_KEYS = {"model": ("init",), "optimizer": ("schedule",)}
|
|
45
|
+
|
|
46
|
+
TOP_LEVEL = ("include", "params", "plugins", "seed", "device", "record", "data", "models", "losses", "metrics",
|
|
47
|
+
"optimizers", "train", "rules", "plots", "generate", "sweep")
|
|
48
|
+
LOADER_KEYS = ("include", "params", "plugins")
|
|
49
|
+
SINGLE_NODES = ("data", "generate")
|
|
50
|
+
DEFAULT_STEP = "supervised"
|
|
51
|
+
STEP_ATTRIBUTES = SLOT_KEYS["step"]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class Ref:
|
|
56
|
+
text: str
|
|
57
|
+
path: str
|
|
58
|
+
attr: str | None
|
|
59
|
+
at: str
|
|
60
|
+
target: object = None
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def name(self):
|
|
64
|
+
return self.path.rsplit(".", 1)[-1]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class Node:
|
|
69
|
+
path: str
|
|
70
|
+
slot: str
|
|
71
|
+
use: str | None
|
|
72
|
+
target: object
|
|
73
|
+
params: dict
|
|
74
|
+
keys: dict = field(default_factory=dict)
|
|
75
|
+
refs: list = field(default_factory=list)
|
|
76
|
+
children: list = field(default_factory=list)
|
|
77
|
+
obj: object = None
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def name(self):
|
|
81
|
+
return self.path.rsplit(".", 1)[-1]
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def section(self):
|
|
85
|
+
return self.path.split(".", 1)[0]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class Signature:
|
|
90
|
+
params: dict
|
|
91
|
+
accepts_extra: bool
|
|
92
|
+
required: tuple
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class Tree:
|
|
97
|
+
config: object
|
|
98
|
+
nodes: dict
|
|
99
|
+
order: list
|
|
100
|
+
settings: dict
|
|
101
|
+
rules: list
|
|
102
|
+
problems: list = field(default_factory=list)
|
|
103
|
+
imports: list = field(default_factory=list)
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def errors(self):
|
|
107
|
+
return [problem for problem in self.problems if problem.severity == "error"]
|
|
108
|
+
|
|
109
|
+
def node(self, path):
|
|
110
|
+
return self.nodes[path]
|
|
111
|
+
|
|
112
|
+
def section(self, name):
|
|
113
|
+
return {node.name: node for node in self.nodes.values()
|
|
114
|
+
if node.path.startswith(f"{name}.") and "." not in node.path[len(name) + 1:]
|
|
115
|
+
and "[" not in node.path[len(name) + 1:]}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def signature_of(target):
|
|
119
|
+
if inspect.isclass(target):
|
|
120
|
+
function = target.__init__
|
|
121
|
+
skip = 1
|
|
122
|
+
else:
|
|
123
|
+
function = target
|
|
124
|
+
skip = 0
|
|
125
|
+
try:
|
|
126
|
+
raw = inspect.signature(function)
|
|
127
|
+
except (TypeError, ValueError):
|
|
128
|
+
return Signature({}, True, ())
|
|
129
|
+
params = {}
|
|
130
|
+
extra = False
|
|
131
|
+
required = []
|
|
132
|
+
for name, parameter in list(raw.parameters.items())[skip:]:
|
|
133
|
+
if parameter.kind is inspect.Parameter.VAR_KEYWORD:
|
|
134
|
+
extra = True
|
|
135
|
+
continue
|
|
136
|
+
if parameter.kind is inspect.Parameter.VAR_POSITIONAL:
|
|
137
|
+
continue
|
|
138
|
+
params[name] = parameter
|
|
139
|
+
if parameter.default is inspect.Parameter.empty:
|
|
140
|
+
required.append(name)
|
|
141
|
+
return Signature(params, extra, tuple(required))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def leading_required(signature):
|
|
145
|
+
count = 0
|
|
146
|
+
positional = (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD)
|
|
147
|
+
for parameter in signature.params.values():
|
|
148
|
+
if parameter.kind not in positional or parameter.default is not inspect.Parameter.empty:
|
|
149
|
+
break
|
|
150
|
+
count += 1
|
|
151
|
+
return count
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def criterion_function(target, signature=None):
|
|
155
|
+
if target is None or inspect.isclass(target) or not callable(target):
|
|
156
|
+
return False
|
|
157
|
+
signature = signature_of(target) if signature is None else signature
|
|
158
|
+
return leading_required(signature) >= 2
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def node_signature(target, slot):
|
|
162
|
+
signature = signature_of(target)
|
|
163
|
+
drop = ()
|
|
164
|
+
names = list(signature.params)
|
|
165
|
+
if slot == "optimizer" and names:
|
|
166
|
+
drop = ("params",) if "params" in names else (names[0],)
|
|
167
|
+
elif slot == "plot" and not inspect.isclass(target):
|
|
168
|
+
drop = tuple(names[:2])
|
|
169
|
+
elif slot == "loss" and criterion_function(target, signature):
|
|
170
|
+
drop = tuple(names[:2])
|
|
171
|
+
if not drop:
|
|
172
|
+
return signature
|
|
173
|
+
params = {name: parameter for name, parameter in signature.params.items() if name not in drop}
|
|
174
|
+
required = tuple(name for name in signature.required if name not in drop)
|
|
175
|
+
return Signature(params, signature.accepts_extra, required)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def fixed_of(node):
|
|
179
|
+
target = node.target
|
|
180
|
+
if node.slot == "model":
|
|
181
|
+
names = list(MODEL_FIXED)
|
|
182
|
+
if target is not None:
|
|
183
|
+
names.extend(node_signature(target, node.slot).params)
|
|
184
|
+
names.extend(name for name in node.params if name not in names)
|
|
185
|
+
return tuple(dict.fromkeys(names))
|
|
186
|
+
if node.slot in ("loss", "metric"):
|
|
187
|
+
return tuple(dict.fromkeys(("pred", "target", *declared_fixed(target))))
|
|
188
|
+
if node.slot == "optimizer":
|
|
189
|
+
return OPTIMIZER_FIXED
|
|
190
|
+
if node.slot in ("step", "trigger"):
|
|
191
|
+
return tuple(declared_fixed(target))
|
|
192
|
+
return ()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def declared_fixed(target):
|
|
196
|
+
found = getattr(target, "fixed", ()) if target is not None else ()
|
|
197
|
+
if isinstance(found, str):
|
|
198
|
+
return (found,)
|
|
199
|
+
try:
|
|
200
|
+
return tuple(str(name) for name in found)
|
|
201
|
+
except TypeError:
|
|
202
|
+
return ()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def join(path, key):
|
|
206
|
+
return f"{path}[{key}]" if isinstance(key, int) else f"{path}.{key}"
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class Builder:
|
|
210
|
+
def __init__(self, config, data):
|
|
211
|
+
self.config = config
|
|
212
|
+
self.data = data
|
|
213
|
+
self.problems = []
|
|
214
|
+
self.declared = []
|
|
215
|
+
self.known = set()
|
|
216
|
+
self.nodes = {}
|
|
217
|
+
self.settings = {}
|
|
218
|
+
self.rules = {}
|
|
219
|
+
self.imports = []
|
|
220
|
+
|
|
221
|
+
def fail(self, code, message, path=None, hint=None):
|
|
222
|
+
self.problems.append(error(code, message, path, hint))
|
|
223
|
+
|
|
224
|
+
def run(self):
|
|
225
|
+
self.scan_top_level()
|
|
226
|
+
self.declare_all()
|
|
227
|
+
self.known = {path for path, _, _ in self.declared}
|
|
228
|
+
for path, slot, raw in self.declared:
|
|
229
|
+
self.add(self.make_node(path, slot, raw))
|
|
230
|
+
specs = parse_rules(self.rules or None, self.problems)
|
|
231
|
+
return Tree(self.config, self.nodes, list(self.nodes), self.settings, specs, self.problems, self.imports)
|
|
232
|
+
|
|
233
|
+
def scan_top_level(self):
|
|
234
|
+
for key, value in self.data.items():
|
|
235
|
+
name = str(key)
|
|
236
|
+
if name not in TOP_LEVEL:
|
|
237
|
+
self.fail("unknown_section", f"{name!r} is not a top level key; the keys are {list(TOP_LEVEL)}", name)
|
|
238
|
+
self.settings[name] = value
|
|
239
|
+
continue
|
|
240
|
+
if name in LOADER_KEYS or name in SECTION_SLOTS:
|
|
241
|
+
continue
|
|
242
|
+
if name == "rules":
|
|
243
|
+
if value is None:
|
|
244
|
+
continue
|
|
245
|
+
if not isinstance(value, dict):
|
|
246
|
+
self.fail("bad_section", "rules must be a mapping of rule names to rules", "rules")
|
|
247
|
+
continue
|
|
248
|
+
self.rules = value
|
|
249
|
+
elif name == "train":
|
|
250
|
+
train = self.train_mapping()
|
|
251
|
+
self.settings["train"] = {k: v for k, v in train.items() if k != "step"}
|
|
252
|
+
else:
|
|
253
|
+
self.settings[name] = value
|
|
254
|
+
|
|
255
|
+
def train_mapping(self):
|
|
256
|
+
train = self.data.get("train")
|
|
257
|
+
if train is None:
|
|
258
|
+
return {}
|
|
259
|
+
if not isinstance(train, dict):
|
|
260
|
+
self.fail("bad_section", "train must be a mapping with epochs or updates", "train")
|
|
261
|
+
return {}
|
|
262
|
+
return train
|
|
263
|
+
|
|
264
|
+
def declare(self, path, slot, raw):
|
|
265
|
+
self.declared.append((path, slot, raw))
|
|
266
|
+
|
|
267
|
+
def declare_all(self):
|
|
268
|
+
for section in ORDER:
|
|
269
|
+
if section == "train.step":
|
|
270
|
+
train = self.data.get("train") if isinstance(self.data.get("train"), dict) else {}
|
|
271
|
+
step = train.get("step")
|
|
272
|
+
if step is None:
|
|
273
|
+
step = {"use": DEFAULT_STEP}
|
|
274
|
+
self.declare("train.step", "step", step)
|
|
275
|
+
log = train.get("log")
|
|
276
|
+
for index, entry in enumerate([log] if isinstance(log, (str, dict)) else list(log or [])):
|
|
277
|
+
self.declare(f"train.log[{index}]", "logger", entry)
|
|
278
|
+
elif section in SINGLE_NODES:
|
|
279
|
+
if section in self.data and self.data[section] is not None:
|
|
280
|
+
self.declare(section, SECTION_SLOTS[section], self.data[section])
|
|
281
|
+
else:
|
|
282
|
+
self.declare_section(section)
|
|
283
|
+
rules = self.rules if isinstance(self.rules, dict) else {}
|
|
284
|
+
for name, rule in rules.items():
|
|
285
|
+
if not isinstance(rule, dict):
|
|
286
|
+
continue
|
|
287
|
+
if rule.get("when") is not None:
|
|
288
|
+
self.declare(f"rules.{name}.when", "trigger", rule["when"])
|
|
289
|
+
if rule.get("run") is not None:
|
|
290
|
+
self.declare(f"rules.{name}.run", "action", rule["run"])
|
|
291
|
+
|
|
292
|
+
def declare_section(self, section):
|
|
293
|
+
raw = self.data.get(section)
|
|
294
|
+
if raw is None:
|
|
295
|
+
return
|
|
296
|
+
if not isinstance(raw, dict):
|
|
297
|
+
self.fail("bad_section", f"{section} must be a mapping of names to calls", section)
|
|
298
|
+
return
|
|
299
|
+
for name, value in raw.items():
|
|
300
|
+
self.declare(f"{section}.{name}", SECTION_SLOTS[section], value)
|
|
301
|
+
|
|
302
|
+
def add(self, node):
|
|
303
|
+
self.nodes[node.path] = node
|
|
304
|
+
for child in node.children:
|
|
305
|
+
self.add(child)
|
|
306
|
+
|
|
307
|
+
def make_node(self, path, slot, raw):
|
|
308
|
+
if isinstance(raw, str) and not raw.startswith("@"):
|
|
309
|
+
raw = {"use": raw}
|
|
310
|
+
node = Node(path, slot, None, None, {})
|
|
311
|
+
if not isinstance(raw, dict) or "use" not in raw:
|
|
312
|
+
self.fail("not_a_call", f"expected a call such as {{use: name, ...}} or a name, got {raw!r}", path,
|
|
313
|
+
"a mapping is a call only when it carries use")
|
|
314
|
+
return node
|
|
315
|
+
text = raw["use"]
|
|
316
|
+
if not isinstance(text, str) or not text:
|
|
317
|
+
self.fail("not_a_call", f"use must be a short name or an import path, got {text!r}", path)
|
|
318
|
+
return node
|
|
319
|
+
node.use = text
|
|
320
|
+
resolved = None
|
|
321
|
+
try:
|
|
322
|
+
resolved = aliases.resolve(text)
|
|
323
|
+
except BuildError as exc:
|
|
324
|
+
self.fail("unknown_use", str(exc), path, aliases.suggestion(text))
|
|
325
|
+
if resolved is not None:
|
|
326
|
+
node.use = aliases.target_path(resolved)
|
|
327
|
+
try:
|
|
328
|
+
node.target = aliases.import_target(resolved)
|
|
329
|
+
except BuildError as exc:
|
|
330
|
+
self.fail("import_failed", str(exc), path)
|
|
331
|
+
if node.target is not None and aliases.is_import_path(node.use) and node.use not in self.imports:
|
|
332
|
+
self.imports.append(node.use)
|
|
333
|
+
slot_keys = SLOT_KEYS.get(slot, ())
|
|
334
|
+
for key, value in raw.items():
|
|
335
|
+
if key == "use":
|
|
336
|
+
continue
|
|
337
|
+
if not isinstance(key, str):
|
|
338
|
+
self.fail("bad_key", f"the key {key!r} is not a name; quote keys such as yes, no, on and off", path)
|
|
339
|
+
continue
|
|
340
|
+
if key in CALL_KEYS.get(slot, ()) and isinstance(value, str) and not value.startswith("@"):
|
|
341
|
+
value = {"use": value}
|
|
342
|
+
holder = node.keys if key in slot_keys else node.params
|
|
343
|
+
holder[key] = self.value(value, join(path, key), node)
|
|
344
|
+
if slot == "optimizer":
|
|
345
|
+
self.check_optimizer(node)
|
|
346
|
+
if node.target is not None:
|
|
347
|
+
self.check_signature(node)
|
|
348
|
+
return node
|
|
349
|
+
|
|
350
|
+
def value(self, raw, at, node):
|
|
351
|
+
if isinstance(raw, dict):
|
|
352
|
+
if "use" in raw:
|
|
353
|
+
child = self.make_node(at, "value", raw)
|
|
354
|
+
node.children.append(child)
|
|
355
|
+
return child
|
|
356
|
+
return {key: self.value(item, join(at, key), node) for key, item in raw.items()}
|
|
357
|
+
if isinstance(raw, (list, tuple)):
|
|
358
|
+
return [self.value(item, join(at, index), node) for index, item in enumerate(raw)]
|
|
359
|
+
if isinstance(raw, str) and raw.startswith("@"):
|
|
360
|
+
ref = self.reference(raw, at, node)
|
|
361
|
+
node.refs.append(ref)
|
|
362
|
+
return ref
|
|
363
|
+
return raw
|
|
364
|
+
|
|
365
|
+
def reference(self, text, at, node):
|
|
366
|
+
body = text[1:]
|
|
367
|
+
parts = [part for part in body.split(".") if part]
|
|
368
|
+
if parts and len(parts) == body.count(".") + 1:
|
|
369
|
+
for cut in range(len(parts), 0, -1):
|
|
370
|
+
path = ".".join(parts[:cut])
|
|
371
|
+
if path in self.known:
|
|
372
|
+
attr = ".".join(parts[cut:]) or None
|
|
373
|
+
return Ref(text, path, attr, at)
|
|
374
|
+
root = parts[0] if parts else ""
|
|
375
|
+
if root in SECTION_SLOTS or root == "train":
|
|
376
|
+
known = sorted(path for path in self.known if path == root or path.startswith(f"{root}."))
|
|
377
|
+
message = f"{at}: {text} names nothing; {root} has {known}"
|
|
378
|
+
else:
|
|
379
|
+
roots = ", ".join([*SECTION_SLOTS, "train.step"])
|
|
380
|
+
message = f"{at}: {text} names nothing; a reference starts with one of {roots}"
|
|
381
|
+
self.fail("reference_unknown", message, node.path)
|
|
382
|
+
return Ref(text, body, None, at)
|
|
383
|
+
|
|
384
|
+
def check_optimizer(self, node):
|
|
385
|
+
over = node.keys.get("over")
|
|
386
|
+
if "over" not in node.keys:
|
|
387
|
+
self.fail("missing_key", "an optimizer needs over, the modules it optimizes, such as over: '@models.net'",
|
|
388
|
+
node.path)
|
|
389
|
+
else:
|
|
390
|
+
items = over if isinstance(over, list) else [over]
|
|
391
|
+
for index, item in enumerate(items):
|
|
392
|
+
where = join(join(node.path, "over"), index) if isinstance(over, list) else join(node.path, "over")
|
|
393
|
+
if isinstance(item, dict):
|
|
394
|
+
if not isinstance(item.get("of"), Ref):
|
|
395
|
+
self.fail("reference_expected", f"{where}: a group needs of: '@models.<name>'", node.path)
|
|
396
|
+
elif not isinstance(item, Ref):
|
|
397
|
+
self.fail("reference_expected", f"{where}: expected '@models.<name>', got {item!r}", node.path)
|
|
398
|
+
minimizes = node.keys.get("minimizes")
|
|
399
|
+
if minimizes is not None and not isinstance(minimizes, Ref):
|
|
400
|
+
self.fail("reference_expected", f"{join(node.path, 'minimizes')}: expected '@losses.<name>', got "
|
|
401
|
+
f"{minimizes!r}", node.path)
|
|
402
|
+
|
|
403
|
+
def check_signature(self, node):
|
|
404
|
+
signature = node_signature(node.target, node.slot)
|
|
405
|
+
known = signature.params
|
|
406
|
+
label = node.use
|
|
407
|
+
if not signature.accepts_extra:
|
|
408
|
+
for key in node.params:
|
|
409
|
+
if key not in known:
|
|
410
|
+
self.fail("unknown_key", f"{label} has no param {key!r}; its params are {sorted(known)}",
|
|
411
|
+
node.path)
|
|
412
|
+
provided = set(node.params)
|
|
413
|
+
if node.slot == "step":
|
|
414
|
+
provided.update(node.keys)
|
|
415
|
+
missing = [name for name in signature.required if name not in provided]
|
|
416
|
+
if missing:
|
|
417
|
+
self.fail("missing_key", f"{label} needs {missing}", node.path)
|
|
418
|
+
if node.slot != "step":
|
|
419
|
+
for key in node.keys:
|
|
420
|
+
if key in known:
|
|
421
|
+
self.fail("slot_conflict", f"{key!r} is a {node.slot} slot key and also a param of {label}; "
|
|
422
|
+
f"the class cannot take it from the config", node.path,
|
|
423
|
+
"rename the constructor param")
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def build(config):
|
|
427
|
+
data = config if isinstance(config, dict) else getattr(config, "data", None)
|
|
428
|
+
if not isinstance(data, dict):
|
|
429
|
+
data = {}
|
|
430
|
+
return Builder(config, data).run()
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def materialize(value):
|
|
434
|
+
if isinstance(value, Node):
|
|
435
|
+
return value.obj
|
|
436
|
+
if isinstance(value, Ref):
|
|
437
|
+
return value.target
|
|
438
|
+
if isinstance(value, dict):
|
|
439
|
+
return {key: materialize(item) for key, item in value.items()}
|
|
440
|
+
if isinstance(value, list):
|
|
441
|
+
return [materialize(item) for item in value]
|
|
442
|
+
return value
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def instantiate(tree, ctx):
|
|
446
|
+
if tree.errors:
|
|
447
|
+
raise ProblemsError(tree.errors)
|
|
448
|
+
built = set()
|
|
449
|
+
for path in tree.order:
|
|
450
|
+
if path not in built:
|
|
451
|
+
build_node(tree, ctx, tree.nodes[path], built)
|
|
452
|
+
for section in ("models", "losses", "metrics", "optimizers"):
|
|
453
|
+
table = getattr(ctx, section, None)
|
|
454
|
+
if isinstance(table, dict):
|
|
455
|
+
for name in tree.section(section):
|
|
456
|
+
if name in table:
|
|
457
|
+
table[name] = table.pop(name)
|
|
458
|
+
return ctx
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def build_node(tree, ctx, node, built=None):
|
|
462
|
+
built = set() if built is None else built
|
|
463
|
+
built.add(node.path)
|
|
464
|
+
for child in node.children:
|
|
465
|
+
if child.path not in built:
|
|
466
|
+
build_node(tree, ctx, child, built)
|
|
467
|
+
resolve_refs(tree, ctx, [node.path], built)
|
|
468
|
+
if node.target is None:
|
|
469
|
+
raise BuildError(f"{node.path}: nothing to build, {node.use!r} did not import")
|
|
470
|
+
kwargs = materialize(node.params)
|
|
471
|
+
keys = materialize(node.keys)
|
|
472
|
+
signature = node_signature(node.target, node.slot)
|
|
473
|
+
slot = node.slot
|
|
474
|
+
if slot == "optimizer":
|
|
475
|
+
obj = build_optimizer(node, kwargs, keys, signature, ctx)
|
|
476
|
+
ctx.optimizers[node.name] = obj
|
|
477
|
+
elif slot == "plot":
|
|
478
|
+
if inspect.isclass(node.target):
|
|
479
|
+
obj = construct(node, kwargs)
|
|
480
|
+
label(obj, node, signature)
|
|
481
|
+
bind(obj, ctx)
|
|
482
|
+
ctx.plots[node.name] = (obj, {})
|
|
483
|
+
else:
|
|
484
|
+
obj = node.target
|
|
485
|
+
ctx.plots[node.name] = (obj, kwargs)
|
|
486
|
+
else:
|
|
487
|
+
if slot == "step":
|
|
488
|
+
kwargs = {**kwargs, **{key: value for key, value in keys.items() if key in signature.params}}
|
|
489
|
+
if slot == "loss" and criterion_function(node.target):
|
|
490
|
+
obj = functools.partial(node.target, **kwargs) if kwargs else node.target
|
|
491
|
+
else:
|
|
492
|
+
obj = construct(node, kwargs)
|
|
493
|
+
label(obj, node, signature)
|
|
494
|
+
if slot == "data":
|
|
495
|
+
ctx.data = obj
|
|
496
|
+
bind(obj, ctx)
|
|
497
|
+
prepare_data(obj, ctx, node)
|
|
498
|
+
elif slot == "model":
|
|
499
|
+
bind(obj, ctx)
|
|
500
|
+
ctx.models[node.name] = build_model(node, obj, keys, ctx)
|
|
501
|
+
elif slot == "loss":
|
|
502
|
+
bind(obj, ctx)
|
|
503
|
+
wrapped = wrap.wrap_loss(obj, keys, node.name, node.path)
|
|
504
|
+
if wrapped is not obj:
|
|
505
|
+
bind(wrapped, ctx)
|
|
506
|
+
ctx.losses[node.name] = wrapped
|
|
507
|
+
obj = wrapped
|
|
508
|
+
elif slot == "metric":
|
|
509
|
+
bind(obj, ctx)
|
|
510
|
+
wrapped = wrap.wrap_metric(obj, keys, node.name, node.path)
|
|
511
|
+
if wrapped is not obj:
|
|
512
|
+
bind(wrapped, ctx)
|
|
513
|
+
ctx.metrics[node.name] = wrapped
|
|
514
|
+
obj = wrapped
|
|
515
|
+
elif slot == "step":
|
|
516
|
+
for key in STEP_ATTRIBUTES:
|
|
517
|
+
if key in keys:
|
|
518
|
+
setattr(obj, key, keys[key])
|
|
519
|
+
bind(obj, ctx)
|
|
520
|
+
ctx.step = obj
|
|
521
|
+
elif slot == "sampler":
|
|
522
|
+
bind(obj, ctx)
|
|
523
|
+
ctx.generate = obj
|
|
524
|
+
elif slot == "logger":
|
|
525
|
+
if ctx.fitted_from is None:
|
|
526
|
+
bind(obj, ctx)
|
|
527
|
+
ctx.loggers.append(obj)
|
|
528
|
+
else:
|
|
529
|
+
bind(obj, ctx)
|
|
530
|
+
node.obj = obj
|
|
531
|
+
return obj
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def construct(node, kwargs):
|
|
535
|
+
try:
|
|
536
|
+
obj = node.target(**coerced(node.target, kwargs))
|
|
537
|
+
except ProblemsError:
|
|
538
|
+
raise
|
|
539
|
+
except KalfaError as exc:
|
|
540
|
+
message = str(exc)
|
|
541
|
+
if message.startswith((f"{node.path}:", f"{node.path}.", f"{node.path}[")):
|
|
542
|
+
raise
|
|
543
|
+
raise type(exc)(f"{node.path}: {message}") from exc
|
|
544
|
+
except Exception as exc:
|
|
545
|
+
raise BuildError(f"{node.path}: {node.use} could not be built: {type(exc).__name__}: {exc}") from exc
|
|
546
|
+
if obj is None:
|
|
547
|
+
raise BuildError(f"{node.path}: {node.use} returned None; a factory returns the object to use")
|
|
548
|
+
return obj
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def label(obj, node, signature):
|
|
552
|
+
for attribute, value in (("name", node.name), ("path", node.path)):
|
|
553
|
+
if attribute in signature.params:
|
|
554
|
+
continue
|
|
555
|
+
try:
|
|
556
|
+
setattr(obj, attribute, value)
|
|
557
|
+
except (AttributeError, TypeError):
|
|
558
|
+
pass
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def bind(obj, ctx):
|
|
562
|
+
method = getattr(obj, "bind", None)
|
|
563
|
+
if callable(method):
|
|
564
|
+
method(ctx)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def prepare_data(obj, ctx, node):
|
|
568
|
+
source = getattr(ctx, "fitted_from", None)
|
|
569
|
+
if source is None:
|
|
570
|
+
fit(obj, ctx, node)
|
|
571
|
+
return
|
|
572
|
+
if callable(getattr(obj, "load", None)):
|
|
573
|
+
obj.load(str(source))
|
|
574
|
+
if getattr(ctx, "want_sets", True) and callable(getattr(obj, "rebuild", None)):
|
|
575
|
+
try:
|
|
576
|
+
obj.rebuild(ctx)
|
|
577
|
+
except KalfaError as exc:
|
|
578
|
+
ctx.rebuild_failure = str(exc)
|
|
579
|
+
ctx.logger.warning(f"the sets of {source} could not be rebuilt ({exc}); only the fitted preprocessors "
|
|
580
|
+
f"are loaded")
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def fit(obj, ctx, node):
|
|
584
|
+
method = getattr(obj, "fit", None)
|
|
585
|
+
if not callable(method):
|
|
586
|
+
return
|
|
587
|
+
try:
|
|
588
|
+
method(ctx)
|
|
589
|
+
except KalfaError:
|
|
590
|
+
raise
|
|
591
|
+
except Exception as exc:
|
|
592
|
+
raise BuildError(f"{node.path}: {node.use} could not fit: {type(exc).__name__}: {exc}") from exc
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def resolve_refs(tree, ctx, paths=None, built=None):
|
|
596
|
+
nodes = [tree.nodes[path] for path in paths] if paths is not None else list(tree.nodes.values())
|
|
597
|
+
resolved = {}
|
|
598
|
+
for node in nodes:
|
|
599
|
+
for ref in node.refs:
|
|
600
|
+
ref.target = locate(tree, ctx, ref, built)
|
|
601
|
+
resolved[ref.at] = ref.target
|
|
602
|
+
return resolved
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def demand(tree, ctx, ref, built):
|
|
606
|
+
node = tree.nodes.get(ref.path)
|
|
607
|
+
if node is None or node.obj is not None or built is None:
|
|
608
|
+
return
|
|
609
|
+
if node.section != ref.at.split(".", 1)[0].split("[", 1)[0]:
|
|
610
|
+
return
|
|
611
|
+
if node.path in built:
|
|
612
|
+
raise BuildError(f"{ref.at}: {ref.text} is being built and refers back to itself; the references form a "
|
|
613
|
+
f"cycle")
|
|
614
|
+
build_node(tree, ctx, node, built)
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def exposed(ref, base):
|
|
618
|
+
first = ref.attr.split(".")[0]
|
|
619
|
+
exposes = getattr(base, "exposes", None)
|
|
620
|
+
if exposes is None:
|
|
621
|
+
if first.startswith("_") or not hasattr(base, first):
|
|
622
|
+
raise BuildError(f"{ref.at}: {ref.text}: the data class has no attribute {first!r}")
|
|
623
|
+
return
|
|
624
|
+
if first not in tuple(exposes):
|
|
625
|
+
raise BuildError(f"{ref.at}: {ref.text} is not exposed; the data class exposes {list(exposes)}")
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def locate(tree, ctx, ref, built=None):
|
|
629
|
+
root, _, rest = ref.path.partition(".")
|
|
630
|
+
if ref.path == "data":
|
|
631
|
+
base = ctx.data
|
|
632
|
+
if base is None:
|
|
633
|
+
raise BuildError(f"{ref.at}: {ref.text} is asked before data is built")
|
|
634
|
+
if ref.attr is None:
|
|
635
|
+
return base
|
|
636
|
+
exposed(ref, base)
|
|
637
|
+
return walk(tree, ref, base, ref.attr)
|
|
638
|
+
if root == "models" and rest:
|
|
639
|
+
entry = ctx.models.get(rest)
|
|
640
|
+
if entry is None:
|
|
641
|
+
demand(tree, ctx, ref, built)
|
|
642
|
+
entry = ctx.models.get(rest)
|
|
643
|
+
if entry is None:
|
|
644
|
+
raise BuildError(f"{ref.at}: {ref.text} is not built yet")
|
|
645
|
+
if ref.attr is None:
|
|
646
|
+
return entry.module
|
|
647
|
+
parts = ref.attr.split(".")
|
|
648
|
+
if parts[0] == "ema":
|
|
649
|
+
if entry.ema is None:
|
|
650
|
+
raise BuildError(f"{ref.at}: {ref.text} asks for an EMA copy but models.{rest} has no ema key")
|
|
651
|
+
return walk(tree, ref, entry.ema, ".".join(parts[1:])) if len(parts) > 1 else entry.ema
|
|
652
|
+
return walk(tree, ref, entry.module, ref.attr)
|
|
653
|
+
tables = {"losses": ctx.losses, "metrics": ctx.metrics, "optimizers": ctx.optimizers}
|
|
654
|
+
base = None
|
|
655
|
+
if root in tables and rest:
|
|
656
|
+
base = tables[root].get(rest)
|
|
657
|
+
elif ref.path == "generate":
|
|
658
|
+
base = ctx.generate
|
|
659
|
+
elif ref.path == "train.step":
|
|
660
|
+
base = ctx.step
|
|
661
|
+
if base is None:
|
|
662
|
+
demand(tree, ctx, ref, built)
|
|
663
|
+
node = tree.nodes.get(ref.path)
|
|
664
|
+
base = node.obj if node is not None else None
|
|
665
|
+
if base is None:
|
|
666
|
+
raise BuildError(f"{ref.at}: {ref.text} is not built yet")
|
|
667
|
+
return walk(tree, ref, base, ref.attr) if ref.attr else base
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def walk(tree, ref, base, attr):
|
|
671
|
+
current = base
|
|
672
|
+
done = []
|
|
673
|
+
for part in attr.split("."):
|
|
674
|
+
done.append(part)
|
|
675
|
+
try:
|
|
676
|
+
current = getattr(current, part)
|
|
677
|
+
except AttributeError:
|
|
678
|
+
child = tree.nodes.get(f"{ref.path}.{'.'.join(done)}")
|
|
679
|
+
if child is not None and child.obj is not None:
|
|
680
|
+
current = child.obj
|
|
681
|
+
else:
|
|
682
|
+
raise BuildError(f"{ref.at}: {ref.text}: {type(current).__name__} has no attribute {part!r}") from None
|
|
683
|
+
return current
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def param_names(node):
|
|
687
|
+
names = list(node.params)
|
|
688
|
+
if node.target is not None:
|
|
689
|
+
signature = node_signature(node.target, node.slot)
|
|
690
|
+
names.extend(name for name in signature.params if name not in names)
|
|
691
|
+
return names
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def leaves(path, value):
|
|
695
|
+
yield path
|
|
696
|
+
if isinstance(value, dict):
|
|
697
|
+
for key, item in value.items():
|
|
698
|
+
yield from leaves(join(path, key), item)
|
|
699
|
+
elif isinstance(value, list):
|
|
700
|
+
for index, item in enumerate(value):
|
|
701
|
+
yield from leaves(join(path, index), item)
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def mutable_paths(tree):
|
|
705
|
+
found = set()
|
|
706
|
+
if train_of(tree).get("updates"):
|
|
707
|
+
found.add("train.updates.total")
|
|
708
|
+
else:
|
|
709
|
+
found.add("train.epochs")
|
|
710
|
+
for node in tree.nodes.values():
|
|
711
|
+
fixed = fixed_of(node)
|
|
712
|
+
if node.slot == "optimizer":
|
|
713
|
+
found.update(f"{node.path}.{name}" for name in param_names(node))
|
|
714
|
+
found.add(f"{node.path}.minimizes")
|
|
715
|
+
elif node.slot == "model":
|
|
716
|
+
found.add(f"{node.path}.freeze")
|
|
717
|
+
elif node.slot in ("loss", "metric", "step", "trigger"):
|
|
718
|
+
names = param_names(node)
|
|
719
|
+
if node.slot == "step":
|
|
720
|
+
names.extend(name for name in STEP_ATTRIBUTES if name not in names)
|
|
721
|
+
for name in names:
|
|
722
|
+
if name in fixed:
|
|
723
|
+
continue
|
|
724
|
+
found.update(leaves(join(node.path, name), node.params.get(name)))
|
|
725
|
+
if node.slot in ("loss", "metric"):
|
|
726
|
+
found.update((f"{node.path}.on", f"{node.path}.every"))
|
|
727
|
+
return sorted(found)
|