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/make.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from . import state, wrap
|
|
2
|
+
from .errors import BuildError, KalfaError
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def as_list(value):
|
|
6
|
+
if value is None:
|
|
7
|
+
return None
|
|
8
|
+
if isinstance(value, str):
|
|
9
|
+
return [value]
|
|
10
|
+
return list(value)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_model(node, module, keys, ctx):
|
|
14
|
+
name = node.name
|
|
15
|
+
data_inputs = getattr(ctx.data, "inputs", None) if ctx.data is not None else None
|
|
16
|
+
inputs = as_list(keys.get("inputs")) or list(data_inputs or ("x",))
|
|
17
|
+
outputs = as_list(keys.get("outputs")) or [name]
|
|
18
|
+
init = keys.get("init")
|
|
19
|
+
if init is not None:
|
|
20
|
+
if not callable(init):
|
|
21
|
+
raise BuildError(f"{node.path}: init must be a call such as {{use: normal_init}}, got {init!r}")
|
|
22
|
+
init(module)
|
|
23
|
+
weights = keys.get("weights")
|
|
24
|
+
if weights is not None:
|
|
25
|
+
load_weights(node, module, weights)
|
|
26
|
+
freeze = keys.get("freeze", False)
|
|
27
|
+
entry = wrap.ModelEntry(name, module, inputs, outputs, freeze=freeze, weights=weights, init=init)
|
|
28
|
+
if "freeze" in keys:
|
|
29
|
+
entry.set_freeze(freeze)
|
|
30
|
+
ema = keys.get("ema")
|
|
31
|
+
if ema is not None and ema is not False:
|
|
32
|
+
entry.ema = ema_copy(node, module, ema)
|
|
33
|
+
entry.path = node.path
|
|
34
|
+
return entry
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def ema_copy(node, module, ema):
|
|
38
|
+
if isinstance(ema, dict):
|
|
39
|
+
decay = ema.get("decay", 0.999)
|
|
40
|
+
every = ema.get("every", 1)
|
|
41
|
+
elif ema is True:
|
|
42
|
+
decay, every = 0.999, 1
|
|
43
|
+
else:
|
|
44
|
+
decay, every = ema, 1
|
|
45
|
+
try:
|
|
46
|
+
return wrap.EmaCopy(module, float(decay), int(every))
|
|
47
|
+
except (TypeError, ValueError) as exc:
|
|
48
|
+
raise BuildError(f"{node.path}: ema takes a decay such as 0.999 or {{decay, every}}, got {ema!r}") from exc
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def load_weights(node, module, weights):
|
|
52
|
+
spec = {"run": weights} if isinstance(weights, str) else weights
|
|
53
|
+
if not isinstance(spec, dict) or not spec.get("run"):
|
|
54
|
+
raise BuildError(f"{node.path}: weights takes {{run, model, which}}, got {weights!r}")
|
|
55
|
+
model = spec.get("model") or node.name
|
|
56
|
+
try:
|
|
57
|
+
path = state.checkpoint_path(spec["run"], spec.get("which"))
|
|
58
|
+
checkpoint = state.read_checkpoint(path)
|
|
59
|
+
except KalfaError:
|
|
60
|
+
raise
|
|
61
|
+
except Exception as exc:
|
|
62
|
+
raise BuildError(f"{node.path}: weights could not be read from {spec['run']}: {exc}") from exc
|
|
63
|
+
nodes = checkpoint.get("nodes", {}) if isinstance(checkpoint, dict) else {}
|
|
64
|
+
key = f"models.{model}"
|
|
65
|
+
if key not in nodes:
|
|
66
|
+
raise BuildError(f"{node.path}: {path} holds no {key}; it holds {sorted(nodes)}")
|
|
67
|
+
try:
|
|
68
|
+
module.load_state_dict(nodes[key])
|
|
69
|
+
except Exception as exc:
|
|
70
|
+
raise BuildError(f"{node.path}: the weights of {key} in {path} do not fit {node.use}: {exc}") from exc
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def build_optimizer(node, kwargs, keys, signature, ctx):
|
|
74
|
+
over = groups_of(node, keys.get("over"))
|
|
75
|
+
entry = wrap.OptimizerEntry(node.name, node.target, kwargs, over, minimizes=keys.get("minimizes"),
|
|
76
|
+
schedule=keys.get("schedule"))
|
|
77
|
+
entry.path = node.path
|
|
78
|
+
link_emas(entry, over, ctx)
|
|
79
|
+
try:
|
|
80
|
+
if callable(getattr(entry, "bind", None)):
|
|
81
|
+
entry.bind(ctx)
|
|
82
|
+
else:
|
|
83
|
+
entry.build()
|
|
84
|
+
except KalfaError:
|
|
85
|
+
raise
|
|
86
|
+
except Exception as exc:
|
|
87
|
+
raise BuildError(f"{node.path}: {node.use} could not be built: {type(exc).__name__}: {exc}") from exc
|
|
88
|
+
return entry
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def covers(model, module):
|
|
92
|
+
return any(module is part for part in model.module.modules())
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def link_emas(entry, over, ctx):
|
|
96
|
+
emas = getattr(entry, "emas", None)
|
|
97
|
+
if not isinstance(emas, list):
|
|
98
|
+
return
|
|
99
|
+
modules = [item.of if isinstance(item, wrap.Group) else item for item in over]
|
|
100
|
+
for model in ctx.models.values():
|
|
101
|
+
if model.ema is None or any(model.ema is known for known in emas):
|
|
102
|
+
continue
|
|
103
|
+
if any(covers(model, module) for module in modules):
|
|
104
|
+
emas.append(model.ema)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def groups_of(node, over):
|
|
108
|
+
if over is None:
|
|
109
|
+
raise BuildError(f"{node.path}: over names the modules to optimize")
|
|
110
|
+
items = over if isinstance(over, list) else [over]
|
|
111
|
+
groups = []
|
|
112
|
+
for item in items:
|
|
113
|
+
if isinstance(item, dict):
|
|
114
|
+
rest = dict(item)
|
|
115
|
+
of = rest.pop("of", None)
|
|
116
|
+
if of is None:
|
|
117
|
+
raise BuildError(f"{node.path}: a group in over needs of: '@models.<name>'")
|
|
118
|
+
groups.append(wrap.Group(of, rest.pop("match", None), **rest))
|
|
119
|
+
else:
|
|
120
|
+
groups.append(item)
|
|
121
|
+
return groups
|
kalfa/core/plan.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from .build import fixed_of, mutable_paths, node_signature
|
|
4
|
+
from .context import train_of
|
|
5
|
+
from .errors import RunError
|
|
6
|
+
from .record import version as kalfa_version
|
|
7
|
+
from .refs import jsonable, ref_json, ref_parts
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
FORMAT = 1
|
|
11
|
+
SET_PREFIX = {"train": "train", "valid": "val", "test": "test"}
|
|
12
|
+
DEFAULT_ON = ("train", "valid", "test")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def plan_of(tree, version=None):
|
|
16
|
+
settings = tree.settings or {}
|
|
17
|
+
nodes = {}
|
|
18
|
+
for path, node in tree.nodes.items():
|
|
19
|
+
signature = node_signature(node.target, node.slot) if node.target is not None else None
|
|
20
|
+
checked = signature is not None and not signature.accepts_extra
|
|
21
|
+
nodes[path] = {
|
|
22
|
+
"slot": node.slot,
|
|
23
|
+
"use": node.use,
|
|
24
|
+
"params": jsonable(node.params),
|
|
25
|
+
"keys": jsonable(node.keys),
|
|
26
|
+
"fixed": list(fixed_of(node)),
|
|
27
|
+
"signature": "checked" if checked else "open",
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
"format": FORMAT,
|
|
31
|
+
"kalfa": version if version is not None else kalfa_version(),
|
|
32
|
+
"seed": jsonable(settings.get("seed")),
|
|
33
|
+
"device": jsonable(settings.get("device")),
|
|
34
|
+
"record": jsonable(settings.get("record")),
|
|
35
|
+
"nodes": nodes,
|
|
36
|
+
"data": data_of(tree),
|
|
37
|
+
"train": jsonable(settings.get("train") or {}),
|
|
38
|
+
"rules": rules_of(tree),
|
|
39
|
+
"series": series_of(tree),
|
|
40
|
+
"mutable": mutable_paths(tree),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def data_of(tree):
|
|
45
|
+
node = tree.nodes.get("data")
|
|
46
|
+
if node is None:
|
|
47
|
+
return {"inputs": [], "exposes": []}
|
|
48
|
+
target = node.target
|
|
49
|
+
inputs = getattr(target, "inputs", None) if target is not None else None
|
|
50
|
+
exposes = getattr(target, "exposes", None) if target is not None else None
|
|
51
|
+
out = {"inputs": list(inputs) if inputs else ["x"], "exposes": list(exposes or ())}
|
|
52
|
+
fields = fields_of(tree, node)
|
|
53
|
+
if fields is not None:
|
|
54
|
+
out["fields"] = fields
|
|
55
|
+
return out
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def fields_of(tree, node):
|
|
59
|
+
file = node.params.get("file")
|
|
60
|
+
if not isinstance(file, str) or not file:
|
|
61
|
+
return None
|
|
62
|
+
candidates = [Path(file)]
|
|
63
|
+
for directory in getattr(tree.config, "directories", None) or ():
|
|
64
|
+
candidates.append(Path(directory) / file)
|
|
65
|
+
for candidate in candidates:
|
|
66
|
+
if candidate.is_file():
|
|
67
|
+
try:
|
|
68
|
+
return read_header(candidate)
|
|
69
|
+
except Exception:
|
|
70
|
+
return None
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def read_header(path):
|
|
75
|
+
suffix = path.suffix.lower()
|
|
76
|
+
if suffix in (".parquet", ".pq"):
|
|
77
|
+
import pyarrow.parquet as parquet
|
|
78
|
+
|
|
79
|
+
schema = parquet.read_schema(str(path))
|
|
80
|
+
elif suffix in (".csv", ".tsv"):
|
|
81
|
+
import pyarrow.csv as csv
|
|
82
|
+
|
|
83
|
+
options = csv.ParseOptions(delimiter="\t") if suffix == ".tsv" else None
|
|
84
|
+
reader = csv.open_csv(str(path), parse_options=options)
|
|
85
|
+
try:
|
|
86
|
+
schema = reader.schema
|
|
87
|
+
finally:
|
|
88
|
+
close = getattr(reader, "close", None)
|
|
89
|
+
if callable(close):
|
|
90
|
+
close()
|
|
91
|
+
else:
|
|
92
|
+
return None
|
|
93
|
+
return {name: str(schema.field(name).type) for name in schema.names}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def call_of(tree, path, value):
|
|
97
|
+
if isinstance(value, str):
|
|
98
|
+
return {"use": value}
|
|
99
|
+
node = tree.nodes.get(path)
|
|
100
|
+
if node is None or not isinstance(value, dict) or "use" not in value:
|
|
101
|
+
return jsonable(value)
|
|
102
|
+
return {"use": value["use"], **jsonable(node.keys), **jsonable(node.params)}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def patch_value(value):
|
|
106
|
+
if ref_parts(value) is None:
|
|
107
|
+
return jsonable(value)
|
|
108
|
+
try:
|
|
109
|
+
return ref_json(value)
|
|
110
|
+
except RunError:
|
|
111
|
+
return jsonable(value)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def rules_of(tree):
|
|
115
|
+
out = []
|
|
116
|
+
for spec in tree.rules:
|
|
117
|
+
out.append({
|
|
118
|
+
"id": spec.name,
|
|
119
|
+
"when": call_of(tree, f"rules.{spec.name}.when", spec.when),
|
|
120
|
+
"after": spec.after,
|
|
121
|
+
"once": bool(spec.once),
|
|
122
|
+
"patch": [{"path": str(path), "value": patch_value(value)} for path, value in spec.set.items()],
|
|
123
|
+
"stop": bool(spec.stop),
|
|
124
|
+
"run": call_of(tree, f"rules.{spec.name}.run", spec.run),
|
|
125
|
+
})
|
|
126
|
+
return out
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def sets_of(node):
|
|
130
|
+
on = node.keys.get("on")
|
|
131
|
+
if on is None:
|
|
132
|
+
on = getattr(node.target, "on", None) if node.target is not None else None
|
|
133
|
+
if on is None:
|
|
134
|
+
on = DEFAULT_ON
|
|
135
|
+
if isinstance(on, str):
|
|
136
|
+
on = (on,)
|
|
137
|
+
return [SET_PREFIX.get(str(name), str(name)) for name in on]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def series_of(tree):
|
|
141
|
+
series = []
|
|
142
|
+
for node in tree.nodes.values():
|
|
143
|
+
if node.slot == "loss":
|
|
144
|
+
series.append(f"train/{node.name}")
|
|
145
|
+
series.extend(f"{prefix}/{node.name}" for prefix in sets_of(node))
|
|
146
|
+
elif node.slot == "metric":
|
|
147
|
+
series.extend(f"{prefix}/{node.name}" for prefix in sets_of(node))
|
|
148
|
+
for node in tree.nodes.values():
|
|
149
|
+
if node.slot == "optimizer":
|
|
150
|
+
series.extend((f"lr/{node.name}", f"minimizes/{node.name}"))
|
|
151
|
+
track = train_of(tree).get("track")
|
|
152
|
+
if isinstance(track, str):
|
|
153
|
+
track = [track]
|
|
154
|
+
for path in track or ():
|
|
155
|
+
series.append(f"track/{path}")
|
|
156
|
+
return list(dict.fromkeys(series))
|
kalfa/core/predict.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import fnmatch
|
|
2
|
+
|
|
3
|
+
import numpy
|
|
4
|
+
import pandas
|
|
5
|
+
import torch
|
|
6
|
+
|
|
7
|
+
from .errors import KalfaError, RunError
|
|
8
|
+
from .record import atomic
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
WIDE = 32
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def batch_size(batch):
|
|
15
|
+
if isinstance(batch, dict):
|
|
16
|
+
values = batch.values()
|
|
17
|
+
elif isinstance(batch, (list, tuple)):
|
|
18
|
+
values = batch
|
|
19
|
+
else:
|
|
20
|
+
values = [batch]
|
|
21
|
+
for value in values:
|
|
22
|
+
shape = getattr(value, "shape", None)
|
|
23
|
+
if shape is not None and len(shape):
|
|
24
|
+
return int(shape[0])
|
|
25
|
+
return 1
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def enter(ctx, batch):
|
|
29
|
+
ctx.batches += 1
|
|
30
|
+
return batch if ctx.device is None else ctx.device.move(batch)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def empty(loader):
|
|
34
|
+
try:
|
|
35
|
+
return len(loader) == 0
|
|
36
|
+
except TypeError:
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def as_matrix(value):
|
|
41
|
+
if isinstance(value, torch.Tensor):
|
|
42
|
+
value = value.detach().cpu().numpy()
|
|
43
|
+
value = numpy.asarray(value)
|
|
44
|
+
if value.ndim == 0:
|
|
45
|
+
value = value.reshape(1)
|
|
46
|
+
return value.reshape(len(value), -1)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def spread(label, values):
|
|
50
|
+
values = numpy.asarray(values)
|
|
51
|
+
if values.ndim <= 1:
|
|
52
|
+
return {label: values}
|
|
53
|
+
if values.shape[1] == 1:
|
|
54
|
+
return {label: values[:, 0]}
|
|
55
|
+
return {f"{label}_{position}": values[:, position] for position in range(values.shape[1])}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def target_names(data, batch=None):
|
|
59
|
+
names = [str(name) for name in getattr(data, "target_names", None) or ()]
|
|
60
|
+
return names if batch is None else [name for name in names if name in batch]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def is_label(matrix):
|
|
64
|
+
return bool(numpy.issubdtype(matrix.dtype, numpy.integer) or matrix.dtype == bool)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def labelled(data, name, truths):
|
|
68
|
+
truth = truths.get(name)
|
|
69
|
+
if truth is not None:
|
|
70
|
+
return is_label(truth)
|
|
71
|
+
asks = getattr(data, "is_label", None)
|
|
72
|
+
return bool(asks(name)) if callable(asks) else False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def rescales(data, name):
|
|
76
|
+
asks = getattr(data, "rescales", None)
|
|
77
|
+
return bool(asks(name)) if callable(asks) else False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def inverse(data, name, matrix):
|
|
81
|
+
transform = getattr(data, "inverse", None)
|
|
82
|
+
if not callable(transform):
|
|
83
|
+
return matrix
|
|
84
|
+
values = matrix[:, 0] if matrix.ndim == 2 and matrix.shape[1] == 1 else matrix
|
|
85
|
+
return numpy.asarray(transform(name, values))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def decoded(data, name, indexes):
|
|
89
|
+
decode = getattr(data, "decode", None)
|
|
90
|
+
if not callable(decode):
|
|
91
|
+
return None
|
|
92
|
+
return numpy.asarray(decode(name, indexes))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def pairings(ctx, outputs, targets):
|
|
96
|
+
pairs = {}
|
|
97
|
+
for entry in [*ctx.losses.values(), *ctx.metrics.values()]:
|
|
98
|
+
pred, target = getattr(entry, "pred", None), getattr(entry, "target", None)
|
|
99
|
+
if pred in outputs and isinstance(target, str):
|
|
100
|
+
pairs.setdefault(pred, target)
|
|
101
|
+
for name in outputs:
|
|
102
|
+
if name in targets:
|
|
103
|
+
pairs.setdefault(name, name)
|
|
104
|
+
if len(targets) == 1:
|
|
105
|
+
for name in outputs:
|
|
106
|
+
pairs.setdefault(name, targets[0])
|
|
107
|
+
return pairs
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def rows_of(data, set_name, count, loader=None, own=True):
|
|
111
|
+
rows = getattr(getattr(loader, "dataset", None), "rows", None)
|
|
112
|
+
if rows is not None and len(rows) == count:
|
|
113
|
+
return numpy.asarray(rows)
|
|
114
|
+
rows = getattr(data, "rows", None)
|
|
115
|
+
if own and callable(rows):
|
|
116
|
+
try:
|
|
117
|
+
found = numpy.asarray(rows(set_name))
|
|
118
|
+
except KalfaError:
|
|
119
|
+
found = None
|
|
120
|
+
if found is not None and len(found) == count:
|
|
121
|
+
return found
|
|
122
|
+
return numpy.arange(count)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def aligned(frame, rows, count):
|
|
126
|
+
if frame is None or len(frame) == count:
|
|
127
|
+
return frame
|
|
128
|
+
try:
|
|
129
|
+
picked = frame.loc[list(rows)]
|
|
130
|
+
except KeyError:
|
|
131
|
+
picked = None
|
|
132
|
+
if picked is None or len(picked) != count:
|
|
133
|
+
raise RunError(f"{count} predicted rows cannot be aligned with the {len(frame)} rows of the frame; the "
|
|
134
|
+
f"frame needs a unique index")
|
|
135
|
+
return picked
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def spectator_columns(data, set_name, count, frame=None):
|
|
139
|
+
if frame is not None:
|
|
140
|
+
patterns = [str(pattern) for pattern in getattr(data, "spectator_patterns", None) or ()]
|
|
141
|
+
return {column: frame[column].to_numpy() for column in frame.columns
|
|
142
|
+
if any(fnmatch.fnmatchcase(str(column), pattern) for pattern in patterns)}
|
|
143
|
+
extra = data.spectators(set_name) if callable(getattr(data, "spectators", None)) else None
|
|
144
|
+
if extra is None:
|
|
145
|
+
return {}
|
|
146
|
+
if isinstance(extra, pandas.DataFrame):
|
|
147
|
+
extra = {column: extra[column].to_numpy() for column in extra.columns}
|
|
148
|
+
return {name: numpy.asarray(values) for name, values in dict(extra).items() if len(values) == count}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def collect_outputs(ctx, loader, entry=None):
|
|
152
|
+
data = ctx.data
|
|
153
|
+
names = None
|
|
154
|
+
guesses, truths = {}, {}
|
|
155
|
+
with torch.no_grad():
|
|
156
|
+
for batch in loader:
|
|
157
|
+
batch = enter(ctx, batch)
|
|
158
|
+
if names is None:
|
|
159
|
+
names = target_names(data, batch)
|
|
160
|
+
outputs = entry.call(batch) if entry is not None else ctx.step.predict(batch, ctx)
|
|
161
|
+
for name, value in outputs.items():
|
|
162
|
+
guesses.setdefault(name, []).append(as_matrix(value))
|
|
163
|
+
for name in names:
|
|
164
|
+
truths.setdefault(name, []).append(as_matrix(batch[name]))
|
|
165
|
+
guesses = {name: numpy.concatenate(pieces) for name, pieces in guesses.items()}
|
|
166
|
+
truths = {name: numpy.concatenate(pieces) for name, pieces in truths.items()}
|
|
167
|
+
return guesses, truths
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def spread_truth(columns, data, name, matrix):
|
|
171
|
+
columns.update(spread(name, inverse(data, name, matrix) if rescales(data, name) else matrix))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def truth_columns(columns, data, truths, frame):
|
|
175
|
+
if frame is not None:
|
|
176
|
+
for name in target_names(data):
|
|
177
|
+
matrix = truths.get(name)
|
|
178
|
+
if matrix is not None and matrix.shape[1] > 1 and not is_label(matrix):
|
|
179
|
+
spread_truth(columns, data, name, matrix)
|
|
180
|
+
elif name in frame.columns:
|
|
181
|
+
columns[name] = frame[name].to_numpy()
|
|
182
|
+
return
|
|
183
|
+
for name, matrix in truths.items():
|
|
184
|
+
if is_label(matrix):
|
|
185
|
+
indexes = matrix[:, 0] if matrix.shape[1] == 1 else matrix.argmax(axis=1)
|
|
186
|
+
labels = decoded(data, name, indexes)
|
|
187
|
+
columns[name] = indexes if labels is None else labels
|
|
188
|
+
else:
|
|
189
|
+
spread_truth(columns, data, name, matrix)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def softmax(matrix):
|
|
193
|
+
shifted = matrix.astype(numpy.float64) - matrix.max(axis=1, keepdims=True)
|
|
194
|
+
weights = numpy.exp(shifted)
|
|
195
|
+
return weights / weights.sum(axis=1, keepdims=True)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def sigmoid(values):
|
|
199
|
+
return 0.5 * (1.0 + numpy.tanh(0.5 * values.astype(numpy.float64)))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def class_names(data, target, count):
|
|
203
|
+
indexes = numpy.arange(count)
|
|
204
|
+
try:
|
|
205
|
+
labels = decoded(data, target, indexes)
|
|
206
|
+
except (KalfaError, IndexError, KeyError, ValueError):
|
|
207
|
+
labels = None
|
|
208
|
+
if labels is None or len(labels) != count:
|
|
209
|
+
return [str(index) for index in indexes]
|
|
210
|
+
return [str(label) for label in labels]
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def class_scores(data, target, width):
|
|
214
|
+
if width == 1:
|
|
215
|
+
return True
|
|
216
|
+
try:
|
|
217
|
+
labels = decoded(data, target, numpy.arange(width))
|
|
218
|
+
except (KalfaError, IndexError, KeyError, ValueError):
|
|
219
|
+
return False
|
|
220
|
+
return labels is None or len(labels) == width
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def probability_columns(columns, data, name, target, matrix):
|
|
224
|
+
if matrix.shape[1] > 1:
|
|
225
|
+
probabilities = softmax(matrix)
|
|
226
|
+
for position, label in enumerate(class_names(data, target, matrix.shape[1])):
|
|
227
|
+
columns[f"prob_{name}_{label}"] = probabilities[:, position]
|
|
228
|
+
else:
|
|
229
|
+
positive = class_names(data, target, 2)[1]
|
|
230
|
+
columns[f"prob_{name}_{positive}"] = sigmoid(matrix[:, 0])
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def guess_columns(columns, ctx, guesses, truths):
|
|
234
|
+
data = ctx.data
|
|
235
|
+
targets = target_names(data) or list(truths)
|
|
236
|
+
pairs = pairings(ctx, list(guesses), targets)
|
|
237
|
+
for name, matrix in guesses.items():
|
|
238
|
+
target = pairs.get(name)
|
|
239
|
+
if target is not None and target not in targets:
|
|
240
|
+
target = None
|
|
241
|
+
if target is not None and labelled(data, target, truths) and not class_scores(data, target, matrix.shape[1]):
|
|
242
|
+
target = None
|
|
243
|
+
if target is None:
|
|
244
|
+
if matrix.shape[1] > WIDE:
|
|
245
|
+
continue
|
|
246
|
+
columns.update(spread(f"pred_{name}", matrix))
|
|
247
|
+
elif labelled(data, target, truths):
|
|
248
|
+
indexes = matrix.argmax(axis=1) if matrix.shape[1] > 1 else (matrix[:, 0] > 0).astype(numpy.int64)
|
|
249
|
+
labels = decoded(data, target, indexes)
|
|
250
|
+
columns[f"pred_{name}"] = indexes if labels is None else labels
|
|
251
|
+
if labels is not None:
|
|
252
|
+
columns[f"raw_{name}"] = indexes
|
|
253
|
+
probability_columns(columns, data, name, target, matrix)
|
|
254
|
+
elif rescales(data, target):
|
|
255
|
+
columns.update(spread(f"pred_{name}", inverse(data, target, matrix)))
|
|
256
|
+
columns.update(spread(f"raw_{name}", matrix))
|
|
257
|
+
else:
|
|
258
|
+
columns.update(spread(f"pred_{name}", matrix))
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def predictions_table(ctx, set_name="test", loader=None, frame=None, entry=None):
|
|
262
|
+
own = loader is None
|
|
263
|
+
if own:
|
|
264
|
+
loader = ctx.loaders().get(set_name)
|
|
265
|
+
if loader is None or empty(loader):
|
|
266
|
+
return pandas.DataFrame()
|
|
267
|
+
ctx.set_modes(False)
|
|
268
|
+
ctx.set_name = set_name
|
|
269
|
+
data = ctx.data
|
|
270
|
+
guesses, truths = collect_outputs(ctx, loader, entry)
|
|
271
|
+
if not guesses:
|
|
272
|
+
return pandas.DataFrame()
|
|
273
|
+
count = len(next(iter(guesses.values())))
|
|
274
|
+
rows = rows_of(data, set_name, count, loader, own)
|
|
275
|
+
frame = aligned(frame, rows, count)
|
|
276
|
+
columns = {"row": rows}
|
|
277
|
+
truth_columns(columns, data, truths, frame)
|
|
278
|
+
guess_columns(columns, ctx, guesses, truths)
|
|
279
|
+
for name, values in spectator_columns(data, set_name, count, frame).items():
|
|
280
|
+
columns.setdefault(name, values)
|
|
281
|
+
return pandas.DataFrame(columns)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def write_predictions(ctx, set_name):
|
|
285
|
+
table = predictions_table(ctx, set_name)
|
|
286
|
+
if ctx.record is not None and len(table):
|
|
287
|
+
name = "predictions.parquet" if set_name == "test" else f"predictions_{set_name}.parquet"
|
|
288
|
+
with atomic(ctx.record.path(name)) as temporary:
|
|
289
|
+
table.to_parquet(temporary, index=False)
|
|
290
|
+
return table
|
kalfa/core/protocols.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from torch import nn
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Objective(nn.Module):
|
|
5
|
+
on = ("train", "valid", "test")
|
|
6
|
+
every = 1
|
|
7
|
+
fixed = ()
|
|
8
|
+
name = ""
|
|
9
|
+
path = ""
|
|
10
|
+
|
|
11
|
+
def bind(self, ctx):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
def forward(self, outputs, batch, ctx):
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ContextMetric:
|
|
19
|
+
on = ("train", "valid", "test")
|
|
20
|
+
every = 1
|
|
21
|
+
name = ""
|
|
22
|
+
path = ""
|
|
23
|
+
|
|
24
|
+
def bind(self, ctx):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
def reset(self):
|
|
28
|
+
raise NotImplementedError
|
|
29
|
+
|
|
30
|
+
def update(self, outputs, batch, ctx):
|
|
31
|
+
raise NotImplementedError
|
|
32
|
+
|
|
33
|
+
def compute(self):
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Step:
|
|
38
|
+
order = None
|
|
39
|
+
amp = False
|
|
40
|
+
grad_clip = None
|
|
41
|
+
accumulate = 1
|
|
42
|
+
stepped = True
|
|
43
|
+
fixed = ()
|
|
44
|
+
persist = ()
|
|
45
|
+
name = "step"
|
|
46
|
+
path = "train.step"
|
|
47
|
+
|
|
48
|
+
def bind(self, ctx):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
def train_turn(self, ctx):
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
|
|
54
|
+
def train_batch(self, batch, ctx):
|
|
55
|
+
raise NotImplementedError
|
|
56
|
+
|
|
57
|
+
def eval_batch(self, batch, ctx):
|
|
58
|
+
raise NotImplementedError
|
|
59
|
+
|
|
60
|
+
def predict(self, batch, ctx):
|
|
61
|
+
raise NotImplementedError
|
|
62
|
+
|
|
63
|
+
def state_dict(self):
|
|
64
|
+
return {name: getattr(self, name) for name in self.persist}
|
|
65
|
+
|
|
66
|
+
def load_state_dict(self, state):
|
|
67
|
+
for name, value in (state or {}).items():
|
|
68
|
+
setattr(self, name, value)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Trigger:
|
|
72
|
+
persist = ()
|
|
73
|
+
fixed = ()
|
|
74
|
+
name = ""
|
|
75
|
+
path = ""
|
|
76
|
+
|
|
77
|
+
def bind(self, ctx):
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
def fires(self, ctx):
|
|
81
|
+
raise NotImplementedError
|
|
82
|
+
|
|
83
|
+
def reset(self):
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
def describe(self):
|
|
87
|
+
return type(self).__name__
|
|
88
|
+
|
|
89
|
+
def state_dict(self):
|
|
90
|
+
return {name: getattr(self, name) for name in self.persist}
|
|
91
|
+
|
|
92
|
+
def load_state_dict(self, state):
|
|
93
|
+
for name, value in (state or {}).items():
|
|
94
|
+
setattr(self, name, value)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Action:
|
|
98
|
+
name = ""
|
|
99
|
+
path = ""
|
|
100
|
+
|
|
101
|
+
def bind(self, ctx):
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
def __call__(self, ctx):
|
|
105
|
+
raise NotImplementedError
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class Logger:
|
|
109
|
+
name = ""
|
|
110
|
+
path = ""
|
|
111
|
+
|
|
112
|
+
def bind(self, ctx):
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
def turn(self, ctx, line):
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
def update(self, ctx, line):
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
def close(self, ctx):
|
|
122
|
+
pass
|