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/std/common.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import numbers
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
|
|
5
|
+
import torch
|
|
6
|
+
from torch import nn
|
|
7
|
+
|
|
8
|
+
from kalfa.core.errors import BuildError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("kalfa.std")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def positive_int(value, what):
|
|
15
|
+
if isinstance(value, bool) or not isinstance(value, numbers.Integral) or value < 1:
|
|
16
|
+
raise BuildError(f"{what} must be a positive integer, got {value!r}")
|
|
17
|
+
return int(value)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def numeric(value):
|
|
21
|
+
return isinstance(value, numbers.Real) and not isinstance(value, bool)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@contextmanager
|
|
25
|
+
def evaluating(model):
|
|
26
|
+
modules = list(model.modules()) if isinstance(model, nn.Module) else []
|
|
27
|
+
modes = [module.training for module in modules]
|
|
28
|
+
if modules:
|
|
29
|
+
model.eval()
|
|
30
|
+
try:
|
|
31
|
+
with torch.no_grad():
|
|
32
|
+
yield
|
|
33
|
+
finally:
|
|
34
|
+
for module, mode in zip(modules, modes):
|
|
35
|
+
module.training = mode
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def classes_last_of(pred, target, choice, owner, label):
|
|
39
|
+
if choice is not None:
|
|
40
|
+
return bool(choice)
|
|
41
|
+
if pred.ndim < 3 or target.ndim != pred.ndim - 1 or pred.shape[0] != target.shape[0]:
|
|
42
|
+
return None
|
|
43
|
+
last = tuple(pred.shape[1:-1]) == tuple(target.shape[1:])
|
|
44
|
+
first = tuple(pred.shape[2:]) == tuple(target.shape[1:])
|
|
45
|
+
if last and first:
|
|
46
|
+
if not getattr(owner, "ambiguous_warned", False):
|
|
47
|
+
owner.ambiguous_warned = True
|
|
48
|
+
logger.warning(f"{label}: logits of shape {tuple(pred.shape)} fit both (batch, tokens, vocabulary) and "
|
|
49
|
+
f"(batch, vocabulary, tokens); the vocabulary is taken in dim 1 as torch does; write "
|
|
50
|
+
f"classes_last: true for (batch, tokens, vocabulary)")
|
|
51
|
+
return False
|
|
52
|
+
if last:
|
|
53
|
+
return True
|
|
54
|
+
if first:
|
|
55
|
+
return False
|
|
56
|
+
return None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from . import images, stream, table, text, transforms, window # noqa: F401
|
kalfa/std/data/base.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from torch.utils.data import DataLoader, IterableDataset
|
|
2
|
+
|
|
3
|
+
from kalfa.core.errors import BuildError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def config_directories(ctx):
|
|
7
|
+
return tuple(getattr(getattr(ctx, "config", None), "directories", None) or ())
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def seeded_split(split, ctx):
|
|
11
|
+
seed = getattr(ctx, "seed", None)
|
|
12
|
+
if getattr(split, "seed", 0) is None and seed is not None:
|
|
13
|
+
split.seed = seed
|
|
14
|
+
return split
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Loaders:
|
|
18
|
+
def make_loader(self, dataset, train):
|
|
19
|
+
spec = self.batch
|
|
20
|
+
if isinstance(dataset, IterableDataset):
|
|
21
|
+
return DataLoader(dataset, batch_size=spec["size"] if train else (spec["eval_size"] or spec["size"]),
|
|
22
|
+
shuffle=False, drop_last=train and spec["drop_last"], num_workers=spec["workers"])
|
|
23
|
+
if train:
|
|
24
|
+
shuffle = spec["shuffle"]
|
|
25
|
+
generator = self.rng.stream("loader") if shuffle and self.rng is not None else None
|
|
26
|
+
return DataLoader(dataset, batch_size=spec["size"], shuffle=shuffle, drop_last=spec["drop_last"],
|
|
27
|
+
num_workers=spec["workers"], generator=generator)
|
|
28
|
+
return DataLoader(dataset, batch_size=spec["eval_size"] or spec["size"], shuffle=False,
|
|
29
|
+
num_workers=spec["workers"])
|
|
30
|
+
|
|
31
|
+
def loaders(self):
|
|
32
|
+
if not self.datasets:
|
|
33
|
+
raise BuildError("the data is not fitted; call fit(ctx) before loaders()")
|
|
34
|
+
if self.loaders_by_set is None:
|
|
35
|
+
self.loaders_by_set = {name: self.make_loader(dataset, name == "train")
|
|
36
|
+
for name, dataset in self.datasets.items()}
|
|
37
|
+
return dict(self.loaders_by_set)
|
|
38
|
+
|
|
39
|
+
def rows(self, set_name):
|
|
40
|
+
if set_name not in self.datasets:
|
|
41
|
+
raise BuildError(f"there is no {set_name!r} set; the sets are {list(self.datasets)}")
|
|
42
|
+
rows = getattr(self.datasets[set_name], "rows", None)
|
|
43
|
+
if rows is None:
|
|
44
|
+
raise BuildError(f"the {set_name} set is streamed and has no row table")
|
|
45
|
+
return rows
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import fnmatch
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
|
|
4
|
+
import numpy
|
|
5
|
+
import torch
|
|
6
|
+
|
|
7
|
+
from kalfa.core import aliases
|
|
8
|
+
from kalfa.core.coerce import coerced
|
|
9
|
+
from kalfa.core.errors import BuildError, error
|
|
10
|
+
from kalfa.std.data import pre
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def is_pattern(text):
|
|
14
|
+
return any(character in text for character in "*?[")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def matches(column, pattern):
|
|
18
|
+
return fnmatch.fnmatchcase(column, pattern) if is_pattern(pattern) else column == pattern
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def matches_any(column, patterns):
|
|
22
|
+
return any(matches(column, pattern) for pattern in patterns)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def specificity(pattern):
|
|
26
|
+
return sum(1 for character in pattern if character not in "*?["), pattern.count("?")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def owners_of(columns, patterns):
|
|
30
|
+
owners = {}
|
|
31
|
+
for column in columns:
|
|
32
|
+
claims = [pattern for pattern in patterns if matches(column, pattern)]
|
|
33
|
+
if not claims:
|
|
34
|
+
continue
|
|
35
|
+
literal = [pattern for pattern in claims if not is_pattern(pattern)]
|
|
36
|
+
if literal:
|
|
37
|
+
owners[column] = literal[0]
|
|
38
|
+
continue
|
|
39
|
+
ranked = sorted(claims, key=specificity, reverse=True)
|
|
40
|
+
if len(ranked) > 1 and specificity(ranked[0]) == specificity(ranked[1]):
|
|
41
|
+
raise BuildError(f"column {column!r} matches the features patterns {ranked[0]!r} and {ranked[1]!r} "
|
|
42
|
+
f"with equal specificity; make one of them more specific")
|
|
43
|
+
owners[column] = ranked[0]
|
|
44
|
+
return owners
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def hint_of(name):
|
|
48
|
+
return aliases.suggestion(name) if isinstance(name, str) and not aliases.is_import_path(name) else None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def target_of(name, where=None):
|
|
52
|
+
try:
|
|
53
|
+
return aliases.import_target(aliases.resolve(name))
|
|
54
|
+
except BuildError as failure:
|
|
55
|
+
hint = hint_of(name)
|
|
56
|
+
place = f"{where}: " if where else ""
|
|
57
|
+
raise BuildError(f"{place}{failure}" + (f" ({hint})" if hint else "")) from None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def name_problems(spec, where, problems):
|
|
61
|
+
for item in spec if isinstance(spec, (list, tuple)) else [spec]:
|
|
62
|
+
if not isinstance(item, str):
|
|
63
|
+
continue
|
|
64
|
+
try:
|
|
65
|
+
found = aliases.resolve(item)
|
|
66
|
+
except BuildError as failure:
|
|
67
|
+
problems.append(error("unknown_use", str(failure), where, hint_of(item)))
|
|
68
|
+
continue
|
|
69
|
+
try:
|
|
70
|
+
aliases.import_target(found)
|
|
71
|
+
except BuildError as failure:
|
|
72
|
+
problems.append(error("import_failed", str(failure), where))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def step_of(spec, where):
|
|
76
|
+
if isinstance(spec, str):
|
|
77
|
+
return target_of(spec, where)()
|
|
78
|
+
if isinstance(spec, dict):
|
|
79
|
+
if "use" not in spec:
|
|
80
|
+
raise BuildError(f"{where}: a preprocessor call needs use, got the keys {sorted(spec)}")
|
|
81
|
+
params = {key: value for key, value in spec.items() if key != "use"}
|
|
82
|
+
target = target_of(spec["use"], where)
|
|
83
|
+
return target(**coerced(target, params))
|
|
84
|
+
if hasattr(spec, "transform"):
|
|
85
|
+
return spec
|
|
86
|
+
raise BuildError(f"{where}: a preprocessor is a short name, an import path, a call or an object with "
|
|
87
|
+
f"transform, got {type(spec).__name__}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def chain_of(spec, where):
|
|
91
|
+
if spec is None:
|
|
92
|
+
return []
|
|
93
|
+
items = list(spec) if isinstance(spec, (list, tuple)) else [spec]
|
|
94
|
+
return [step_of(item, where) for item in items]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def as_matrix(values):
|
|
98
|
+
array = numpy.asarray(values)
|
|
99
|
+
if array.ndim == 1:
|
|
100
|
+
return array.reshape(-1, 1)
|
|
101
|
+
if array.ndim > 2:
|
|
102
|
+
return array.reshape(len(array), -1)
|
|
103
|
+
return array
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def apply_step(step, values, where, fit):
|
|
107
|
+
try:
|
|
108
|
+
if fit and hasattr(step, "fit"):
|
|
109
|
+
step.fit(values)
|
|
110
|
+
return as_matrix(step.transform(values))
|
|
111
|
+
except (ValueError, TypeError) as error:
|
|
112
|
+
raise BuildError(f"{where}: {type(step).__name__} failed: {error}") from error
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def names_after(step, names, width):
|
|
116
|
+
if hasattr(step, "get_feature_names_out"):
|
|
117
|
+
try:
|
|
118
|
+
return [str(name) for name in step.get_feature_names_out(list(names))]
|
|
119
|
+
except (ValueError, TypeError, AttributeError):
|
|
120
|
+
pass
|
|
121
|
+
if width == len(names):
|
|
122
|
+
return list(names)
|
|
123
|
+
return [f"{'_'.join(names)}_{position}" for position in range(width)]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def floats_of(values, where):
|
|
127
|
+
try:
|
|
128
|
+
return numpy.asarray(values, dtype="float32")
|
|
129
|
+
except (ValueError, TypeError) as error:
|
|
130
|
+
raise BuildError(f"{where}: the values are not numeric after the chain ({error}); add one_hot, label or "
|
|
131
|
+
f"cast") from error
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def tensor_of(array, dtype):
|
|
135
|
+
out = numpy.ascontiguousarray(array, dtype=dtype)
|
|
136
|
+
if not out.flags.writeable:
|
|
137
|
+
out = out.copy()
|
|
138
|
+
return torch.from_numpy(out)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def step_names(chain):
|
|
142
|
+
return [type(step).__name__ for step in chain]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def inverts(step):
|
|
146
|
+
return hasattr(step, "inverse_transform") and bool(getattr(step, "rescales", True))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def label_step(chain):
|
|
150
|
+
for step in chain:
|
|
151
|
+
if isinstance(step, pre.Label):
|
|
152
|
+
return step
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def mapping_of(spec, what):
|
|
157
|
+
if spec is None:
|
|
158
|
+
return {}
|
|
159
|
+
if not isinstance(spec, dict):
|
|
160
|
+
raise BuildError(f"{what} is a mapping of column names or patterns to preprocessors, got "
|
|
161
|
+
f"{type(spec).__name__}")
|
|
162
|
+
return dict(spec)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def names_of(spec, what):
|
|
166
|
+
if spec is None:
|
|
167
|
+
return []
|
|
168
|
+
if isinstance(spec, str):
|
|
169
|
+
return [spec]
|
|
170
|
+
if isinstance(spec, (list, tuple)) and all(isinstance(item, str) for item in spec):
|
|
171
|
+
return list(spec)
|
|
172
|
+
raise BuildError(f"{what} is a column name or a list of column names, got {spec!r}")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@dataclass
|
|
176
|
+
class Block:
|
|
177
|
+
pattern: str
|
|
178
|
+
columns: list
|
|
179
|
+
chain: list
|
|
180
|
+
names: list = field(default_factory=list)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass
|
|
184
|
+
class Target:
|
|
185
|
+
name: str
|
|
186
|
+
chain: list
|
|
187
|
+
label: object = None
|
|
188
|
+
width: int = 1
|
|
189
|
+
classes: list | None = None
|
|
190
|
+
class_weights: object = None
|
kalfa/std/data/flow.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from kalfa.core import aliases
|
|
5
|
+
from kalfa.std.data import splits
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
LISTED = 6
|
|
9
|
+
CLIP = 90
|
|
10
|
+
SCALARS = (bool, int, float, str)
|
|
11
|
+
LABELS = ("name", "path")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def alias_of(target, slot=None):
|
|
15
|
+
kind = target if isinstance(target, type) else type(target)
|
|
16
|
+
path = aliases.target_path(kind)
|
|
17
|
+
found = [name for name, entry in aliases.ALIASES.items() if aliases.target_path(entry.target) == path]
|
|
18
|
+
preferred = [name for name in found if slot is None or aliases.ALIASES[name].slot == slot]
|
|
19
|
+
chosen = preferred or found
|
|
20
|
+
return chosen[0] if chosen else None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def kind_of(data):
|
|
24
|
+
return alias_of(data, "data") or aliases.target_path(type(data))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def jsonable(value):
|
|
28
|
+
if isinstance(value, dict):
|
|
29
|
+
return {str(key): jsonable(item) for key, item in value.items()}
|
|
30
|
+
if isinstance(value, (list, tuple, set, frozenset)):
|
|
31
|
+
return [jsonable(item) for item in value]
|
|
32
|
+
if value is None or isinstance(value, (bool, str)):
|
|
33
|
+
return value
|
|
34
|
+
if isinstance(value, int):
|
|
35
|
+
return int(value)
|
|
36
|
+
if isinstance(value, float):
|
|
37
|
+
return float(value) if math.isfinite(value) else None
|
|
38
|
+
if isinstance(value, Path):
|
|
39
|
+
return str(value)
|
|
40
|
+
if callable(getattr(value, "tolist", None)):
|
|
41
|
+
return jsonable(value.tolist())
|
|
42
|
+
return str(value)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def number(value):
|
|
46
|
+
if value is None or isinstance(value, bool):
|
|
47
|
+
return None
|
|
48
|
+
try:
|
|
49
|
+
return int(value)
|
|
50
|
+
except (TypeError, ValueError):
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def amount(value):
|
|
55
|
+
value = number(value)
|
|
56
|
+
if value is None:
|
|
57
|
+
return "?"
|
|
58
|
+
if abs(value) < 10000:
|
|
59
|
+
return str(value)
|
|
60
|
+
if abs(value) < 1000000:
|
|
61
|
+
return f"{value / 1000:.1f}k".replace(".0k", "k")
|
|
62
|
+
return f"{value / 1000000:.1f}M".replace(".0M", "M")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def clip(text, size=CLIP):
|
|
66
|
+
text = "" if text is None else str(text)
|
|
67
|
+
return text if len(text) <= size else text[:size - 1].rstrip() + "…"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def tail(text, size=60):
|
|
71
|
+
text = str(text)
|
|
72
|
+
return text if len(text) <= size else "…" + text[-(size - 1):]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def listed(names, limit=LISTED):
|
|
76
|
+
names = [str(name) for name in names or ()]
|
|
77
|
+
if len(names) <= limit:
|
|
78
|
+
return ", ".join(names)
|
|
79
|
+
return ", ".join(names[:limit]) + f" and {len(names) - limit} more"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def share(value):
|
|
83
|
+
return f"{round(float(value) * 100, 1):g}%"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def simple(value):
|
|
87
|
+
if isinstance(value, SCALARS):
|
|
88
|
+
return True
|
|
89
|
+
return isinstance(value, (list, tuple)) and len(value) <= 8 and all(isinstance(item, SCALARS) for item in value)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def value_text(value):
|
|
93
|
+
if isinstance(value, bool):
|
|
94
|
+
return str(value).lower()
|
|
95
|
+
if isinstance(value, float):
|
|
96
|
+
return f"{value:.4g}"
|
|
97
|
+
if isinstance(value, (list, tuple)):
|
|
98
|
+
return "[" + ", ".join(value_text(item) for item in value) + "]"
|
|
99
|
+
return str(value)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def params_text(params):
|
|
103
|
+
return ", ".join(f"{key} {value_text(value)}" for key, value in params.items() if simple(value))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def params_of(thing):
|
|
107
|
+
fields = getattr(thing, "__dict__", None)
|
|
108
|
+
if not isinstance(fields, dict):
|
|
109
|
+
return ""
|
|
110
|
+
return params_text({key: value for key, value in fields.items()
|
|
111
|
+
if key not in LABELS and not key.startswith("_") and not key.endswith("_")})
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def named(text):
|
|
115
|
+
text = str(text)
|
|
116
|
+
if aliases.is_import_path(text):
|
|
117
|
+
return text.rsplit(".", 1)[-1], text
|
|
118
|
+
return text, ""
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def step_label(item):
|
|
122
|
+
if isinstance(item, str):
|
|
123
|
+
return named(item)
|
|
124
|
+
if isinstance(item, dict):
|
|
125
|
+
label, path = named(item.get("use"))
|
|
126
|
+
return label, params_text({key: value for key, value in item.items() if key != "use"}) or path
|
|
127
|
+
return alias_of(item, "value") or type(item).__name__, params_of(item)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def steps_of(spec, chain=()):
|
|
131
|
+
items = [] if spec is None else list(spec) if isinstance(spec, (list, tuple)) else [spec]
|
|
132
|
+
if chain and len(items) != len(chain):
|
|
133
|
+
items = list(chain)
|
|
134
|
+
return [step_label(item) for item in items]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def pipeline_steps(pipeline):
|
|
138
|
+
names = list(getattr(pipeline, "names", None) or ())
|
|
139
|
+
steps = list(getattr(pipeline, "steps", None) or ())
|
|
140
|
+
if len(names) != len(steps):
|
|
141
|
+
return [step_label(step) for step in steps]
|
|
142
|
+
found = []
|
|
143
|
+
for name, step in zip(names, steps):
|
|
144
|
+
label, path = named(name)
|
|
145
|
+
found.append((label, params_of(step) or path))
|
|
146
|
+
return found
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class Flow:
|
|
150
|
+
def __init__(self):
|
|
151
|
+
self.nodes = []
|
|
152
|
+
self.edges = []
|
|
153
|
+
self.groups = []
|
|
154
|
+
self.ids = set()
|
|
155
|
+
|
|
156
|
+
def unique(self, key):
|
|
157
|
+
found, position = str(key), 1
|
|
158
|
+
while found in self.ids:
|
|
159
|
+
position += 1
|
|
160
|
+
found = f"{key}#{position}"
|
|
161
|
+
self.ids.add(found)
|
|
162
|
+
return found
|
|
163
|
+
|
|
164
|
+
def node(self, key, kind, label, detail="", count=None, group=None):
|
|
165
|
+
key = self.unique(key)
|
|
166
|
+
self.nodes.append({"id": key, "kind": kind, "label": str(label), "detail": clip(detail), "count": number(count),
|
|
167
|
+
"group": group})
|
|
168
|
+
return key
|
|
169
|
+
|
|
170
|
+
def edge(self, start, end, label=""):
|
|
171
|
+
if start is not None and end is not None and start != end:
|
|
172
|
+
self.edges.append({"from": start, "to": end, "label": str(label or "")})
|
|
173
|
+
|
|
174
|
+
def join(self, starts, end, label=""):
|
|
175
|
+
for start in starts:
|
|
176
|
+
self.edge(start, end, label)
|
|
177
|
+
|
|
178
|
+
def group(self, key, label, detail="", parent=None):
|
|
179
|
+
key = self.unique(key)
|
|
180
|
+
self.groups.append({"id": key, "label": str(label), "detail": clip(detail), "parent": parent})
|
|
181
|
+
return key
|
|
182
|
+
|
|
183
|
+
def step(self, start, key, kind, label, detail="", count=None, group=None, edge=""):
|
|
184
|
+
made = self.node(key, kind, label, detail, count, group)
|
|
185
|
+
self.edge(start, made, edge)
|
|
186
|
+
return made
|
|
187
|
+
|
|
188
|
+
def chain(self, starts, key, steps, kind="chain", label=None, detail=""):
|
|
189
|
+
steps = list(steps)
|
|
190
|
+
if not steps:
|
|
191
|
+
return list(starts)
|
|
192
|
+
group = self.group(f"group:{key}", label or key, detail) if label is not None else None
|
|
193
|
+
ends = list(starts)
|
|
194
|
+
for position, (name, about) in enumerate(steps):
|
|
195
|
+
made = self.node(f"{key}:{position}", kind, name, about, group=group)
|
|
196
|
+
self.join(ends, made)
|
|
197
|
+
ends = [made]
|
|
198
|
+
return ends
|
|
199
|
+
|
|
200
|
+
def result(self):
|
|
201
|
+
return {"nodes": self.nodes, "edges": self.edges, "groups": self.groups}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def source_node(graph, path, count=None, extra=()):
|
|
205
|
+
text = str(path)
|
|
206
|
+
name = Path(text).name or text
|
|
207
|
+
about = ([tail(text)] if text != name else []) + [part for part in extra if part]
|
|
208
|
+
return graph.node("source", "source", name, ", ".join(about), count)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def split_label(split):
|
|
212
|
+
name = alias_of(split, "value")
|
|
213
|
+
return f"{name} split" if name else type(split).__name__
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def split_detail(split):
|
|
217
|
+
if isinstance(split, splits.KFoldSplit):
|
|
218
|
+
parts = [f"fold {split.fold} of {split.folds}"]
|
|
219
|
+
if split.valid_ratio:
|
|
220
|
+
parts.append(f"valid {share(split.valid_ratio)} of the rest")
|
|
221
|
+
if split.seed is not None:
|
|
222
|
+
parts.append(f"seed {split.seed}")
|
|
223
|
+
return ", ".join(parts)
|
|
224
|
+
if isinstance(split, splits.GivenSplit):
|
|
225
|
+
return f"by the column {split.column}"
|
|
226
|
+
shares = ", ".join(share(value) for value in getattr(split, "ratios", None) or () if value)
|
|
227
|
+
if isinstance(split, splits.SequentialSplit):
|
|
228
|
+
about = "in file order" + (f", within each {split.group}" if split.group is not None else "")
|
|
229
|
+
elif isinstance(split, splits.RandomSplit):
|
|
230
|
+
about = "shuffled" + (f" with seed {split.seed}" if split.seed is not None else "")
|
|
231
|
+
else:
|
|
232
|
+
return params_of(split)
|
|
233
|
+
return f"{shares}, {about}" if shares else about
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def shares_of(split):
|
|
237
|
+
if isinstance(split, splits.KFoldSplit):
|
|
238
|
+
found = {"train": "other folds", "test": f"fold {split.fold}"}
|
|
239
|
+
if split.valid_ratio:
|
|
240
|
+
found["valid"] = f"{share(split.valid_ratio)} of the rest"
|
|
241
|
+
return found
|
|
242
|
+
if isinstance(split, splits.GivenSplit):
|
|
243
|
+
labels = {"train": split.train, "valid": split.valid, "test": split.test}
|
|
244
|
+
return {name: f"{split.column} = {value}" for name, value in labels.items() if value is not None}
|
|
245
|
+
ratios = getattr(split, "ratios", None)
|
|
246
|
+
if isinstance(ratios, (list, tuple)) and len(ratios) == len(splits.SET_NAMES):
|
|
247
|
+
try:
|
|
248
|
+
return {name: share(value) for name, value in zip(splits.SET_NAMES, ratios) if float(value) > 0}
|
|
249
|
+
except (TypeError, ValueError):
|
|
250
|
+
return {}
|
|
251
|
+
return {}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def split_node(graph, start, split):
|
|
255
|
+
return graph.step(start, "split", "split", split_label(split), split_detail(split))
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def set_nodes(graph, start, counts, details=None, edges=None):
|
|
259
|
+
details = details or {}
|
|
260
|
+
edges = edges or {}
|
|
261
|
+
return {name: graph.step(start, f"set:{name}", "set", name, details.get(name, ""), count, edge=edges.get(name, ""))
|
|
262
|
+
for name, count in (counts or {}).items()}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def batch_text(batch):
|
|
266
|
+
if not isinstance(batch, dict):
|
|
267
|
+
return ""
|
|
268
|
+
parts = [f"size {batch['size']}"] if batch.get("size") else []
|
|
269
|
+
if batch.get("eval_size"):
|
|
270
|
+
parts.append(f"eval {batch['eval_size']}")
|
|
271
|
+
if batch.get("drop_last"):
|
|
272
|
+
parts.append("drop last")
|
|
273
|
+
return ", ".join(parts)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def batch_node(graph, starts, batch):
|
|
277
|
+
made = graph.node("batch", "batch", "batch", batch_text(batch))
|
|
278
|
+
graph.join(starts, made)
|
|
279
|
+
return made
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def feed_node(graph, starts, feed, windows=None):
|
|
283
|
+
label = alias_of(feed, "value") or type(feed).__name__
|
|
284
|
+
count = sum(windows.values()) if windows else None
|
|
285
|
+
made = graph.node("feed", "feed", label, params_of(feed), count)
|
|
286
|
+
graph.join(starts, made)
|
|
287
|
+
return made
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def tensor_names(data):
|
|
291
|
+
found = {}
|
|
292
|
+
for name in list(getattr(data, "inputs", None) or ()):
|
|
293
|
+
found.setdefault(str(name), "input")
|
|
294
|
+
for name in list(getattr(data, "target_names", None) or ()):
|
|
295
|
+
found.setdefault(str(name), "target")
|
|
296
|
+
return found
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def generic(data):
|
|
300
|
+
reporter = getattr(data, "report", None)
|
|
301
|
+
report = reporter() if callable(reporter) else None
|
|
302
|
+
rows = report.get("rows") if isinstance(report, dict) else None
|
|
303
|
+
graph = Flow()
|
|
304
|
+
source = graph.node("source", "source", type(data).__name__, kind_of(data))
|
|
305
|
+
made = set_nodes(graph, source, rows if isinstance(rows, dict) else {})
|
|
306
|
+
tensors = [graph.step(source, f"tensor:{name}", "tensor", name, role) for name, role in tensor_names(data).items()]
|
|
307
|
+
batch_node(graph, (list(made.values()) or [source]) + tensors, getattr(data, "batch", None))
|
|
308
|
+
return graph.result()
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def pipeline_of(data):
|
|
312
|
+
method = getattr(data, "pipeline", None)
|
|
313
|
+
found = method() if callable(method) else None
|
|
314
|
+
if not isinstance(found, dict) or not isinstance(found.get("nodes"), list):
|
|
315
|
+
found = generic(data)
|
|
316
|
+
return jsonable({"nodes": found.get("nodes") or [], "edges": found.get("edges") or [],
|
|
317
|
+
"groups": found.get("groups") or []})
|