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
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import numpy
|
|
2
|
+
import pandas
|
|
3
|
+
|
|
4
|
+
from .tables import finite, label_text, labels, numbers_of, numeric, ordered_labels, texts
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
CODES = (3, 20)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def plan_nodes(plan):
|
|
11
|
+
nodes = (plan or {}).get("nodes")
|
|
12
|
+
return nodes if isinstance(nodes, dict) else {}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def plan_targets(plan):
|
|
16
|
+
node = plan_nodes(plan).get("data")
|
|
17
|
+
params = node.get("params") if isinstance(node, dict) else None
|
|
18
|
+
targets = params.get("targets") if isinstance(params, dict) else None
|
|
19
|
+
if isinstance(targets, dict) and targets:
|
|
20
|
+
return [str(name) for name in targets]
|
|
21
|
+
if isinstance(targets, (list, tuple)) and targets:
|
|
22
|
+
return [str(name) for name in targets]
|
|
23
|
+
if isinstance(targets, str):
|
|
24
|
+
return [targets]
|
|
25
|
+
fields = ((plan or {}).get("data") or {}).get("fields")
|
|
26
|
+
if isinstance(fields, dict):
|
|
27
|
+
return [str(name) for name, field in fields.items() if isinstance(field, dict) and field.get("target")]
|
|
28
|
+
return []
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def plan_classes(plan):
|
|
32
|
+
return labels(((plan or {}).get("data") or {}).get("classes"))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def fitted_classes(fields, target=None):
|
|
36
|
+
if not isinstance(fields, dict):
|
|
37
|
+
return None
|
|
38
|
+
if target is not None:
|
|
39
|
+
for item in fields.get("target_fields") or ():
|
|
40
|
+
if isinstance(item, dict) and item.get("name") == target:
|
|
41
|
+
return labels(item.get("classes"))
|
|
42
|
+
return labels(fields.get("classes"))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def fitted_entries(fields):
|
|
46
|
+
if not isinstance(fields, dict):
|
|
47
|
+
return {}
|
|
48
|
+
return {str(item["name"]): labels(item.get("classes")) for item in fields.get("target_fields") or ()
|
|
49
|
+
if isinstance(item, dict) and item.get("name") is not None}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def classes_for(plan, fitted, target=None):
|
|
53
|
+
entries = fitted_entries(fitted)
|
|
54
|
+
if target in entries and (entries[target] is None or sum(1 for found in entries.values() if found) > 1):
|
|
55
|
+
return entries[target]
|
|
56
|
+
return plan_classes(plan) or fitted_classes(fitted, target)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def plan_pairings(plan):
|
|
60
|
+
pairs = {}
|
|
61
|
+
for path, node in plan_nodes(plan).items():
|
|
62
|
+
if not isinstance(node, dict) or node.get("slot") not in ("loss", "metric"):
|
|
63
|
+
continue
|
|
64
|
+
keys = node.get("keys") if isinstance(node.get("keys"), dict) else {}
|
|
65
|
+
pred, target = keys.get("pred"), keys.get("target")
|
|
66
|
+
if isinstance(pred, str) and isinstance(target, str):
|
|
67
|
+
pairs.setdefault(pred, target)
|
|
68
|
+
return pairs
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def frame_targets(frame):
|
|
72
|
+
return [column for column in frame.columns
|
|
73
|
+
if column != "row" and not str(column).startswith(("pred_", "raw_", "prob_"))]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def kind_of(frame, target, classes, scores):
|
|
77
|
+
if classes or scores:
|
|
78
|
+
return "classification"
|
|
79
|
+
return "regression" if pandas.api.types.is_numeric_dtype(frame[target]) else "classification"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def spread_member(column, columns):
|
|
83
|
+
stem, _, position = str(column).rpartition("_")
|
|
84
|
+
if stem and position.isdigit() and f"{stem}_0" in columns and f"{stem}_1" in columns:
|
|
85
|
+
return stem, int(position)
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def declared_targets(plan, fitted, frame):
|
|
90
|
+
targets = plan_targets(plan)
|
|
91
|
+
if targets:
|
|
92
|
+
return targets
|
|
93
|
+
if isinstance(fitted, dict) and isinstance(fitted.get("targets"), list):
|
|
94
|
+
return [str(name) for name in fitted["targets"]]
|
|
95
|
+
found = frame_targets(frame)
|
|
96
|
+
return found if len(found) == 1 else []
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def matches(frame, plan, fitted=None):
|
|
100
|
+
columns = [str(column) for column in frame.columns]
|
|
101
|
+
pairings = plan_pairings(plan)
|
|
102
|
+
targets = declared_targets(plan, fitted, frame)
|
|
103
|
+
fallback = targets[0] if len(targets) == 1 else None
|
|
104
|
+
found = []
|
|
105
|
+
for column in columns:
|
|
106
|
+
if not column.startswith("pred_"):
|
|
107
|
+
continue
|
|
108
|
+
output = column[len("pred_"):]
|
|
109
|
+
target, base = pairings.get(output), None
|
|
110
|
+
member = None if target is not None else spread_member(column, columns)
|
|
111
|
+
if member is not None:
|
|
112
|
+
base = pairings.get(member[0][len("pred_"):], fallback)
|
|
113
|
+
target = None if base is None else f"{base}_{member[1]}"
|
|
114
|
+
elif target is None:
|
|
115
|
+
target = fallback
|
|
116
|
+
if target is None or target not in columns or target == column:
|
|
117
|
+
target = None
|
|
118
|
+
found.append((column, output, target, base))
|
|
119
|
+
return found
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def raw_of(columns, output):
|
|
123
|
+
return f"raw_{output}" if f"raw_{output}" in columns else None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def scores_of(columns, output):
|
|
127
|
+
return [name for name in columns if name.startswith(f"prob_{output}_")]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def suffix_label(suffix, known):
|
|
131
|
+
if suffix in known:
|
|
132
|
+
return suffix
|
|
133
|
+
try:
|
|
134
|
+
number = float(suffix)
|
|
135
|
+
except ValueError:
|
|
136
|
+
return suffix
|
|
137
|
+
return label_text(number) or suffix
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def score_labels(scores, output, known=()):
|
|
141
|
+
prefix = f"prob_{output}_"
|
|
142
|
+
known = set(known or ())
|
|
143
|
+
return {column: suffix_label(column[len(prefix):], known) for column in scores if column.startswith(prefix)}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def label_columns(scores, output, known=()):
|
|
147
|
+
found = {}
|
|
148
|
+
for column, label in score_labels(scores, output, known).items():
|
|
149
|
+
found.setdefault(label, column)
|
|
150
|
+
return found
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def prediction_pairs(frame, plan, fitted=None):
|
|
154
|
+
if frame is None:
|
|
155
|
+
return []
|
|
156
|
+
columns = [str(column) for column in frame.columns]
|
|
157
|
+
pairs = []
|
|
158
|
+
for column, output, target, base in matches(frame, plan, fitted):
|
|
159
|
+
if target is None:
|
|
160
|
+
continue
|
|
161
|
+
raw, scores = raw_of(columns, output), scores_of(columns, output)
|
|
162
|
+
kind = kind_of(frame, target, classes_for(plan, fitted, base or target), scores)
|
|
163
|
+
pairs.append({"output": output, "pred": column, "target": target, "kind": kind, "raw": raw, "scores": scores})
|
|
164
|
+
return pairs
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def observed(frame, pred, target):
|
|
168
|
+
return ordered_labels(set(texts(frame[target]).tolist()) | set(texts(frame[pred]).tolist()))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def detected(frame, pred, target, classes, scores):
|
|
172
|
+
truth = frame[target]
|
|
173
|
+
if classes or scores or not numeric(truth):
|
|
174
|
+
found = classes or observed(frame, pred, target)
|
|
175
|
+
if len(found) == 2:
|
|
176
|
+
return "binary", found, ["binary", "multiclass"]
|
|
177
|
+
return "multiclass", found, ["multiclass"]
|
|
178
|
+
distinct = numpy.unique(finite(numbers_of(truth)))
|
|
179
|
+
if len(distinct) == 2:
|
|
180
|
+
return "binary", [label_text(value) for value in distinct], ["binary", "regression"]
|
|
181
|
+
low, high = CODES
|
|
182
|
+
if numeric(frame[pred]) and low <= len(distinct) <= high and bool(numpy.all(distinct == numpy.round(distinct))):
|
|
183
|
+
guessed = numpy.unique(finite(numbers_of(frame[pred])))
|
|
184
|
+
if bool(numpy.isin(guessed, distinct).all()):
|
|
185
|
+
return "multiclass", [label_text(value) for value in distinct], ["multiclass", "regression"]
|
|
186
|
+
return "regression", None, ["regression"]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def target_pair(frame, column, output, target, classes):
|
|
190
|
+
columns = [str(name) for name in frame.columns]
|
|
191
|
+
scores = scores_of(columns, output)
|
|
192
|
+
auto, found, tasks = detected(frame, column, target, classes, scores)
|
|
193
|
+
return {"output": output, "pred": column, "columns": [column], "target": target, "auto": auto, "tasks": tasks,
|
|
194
|
+
"raw": raw_of(columns, output), "scores": scores, "classes": found}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def loose_pair(output, pred, task, columns, raw=None):
|
|
198
|
+
return {"output": output, "pred": pred, "columns": columns, "target": None, "auto": task, "tasks": [task],
|
|
199
|
+
"raw": raw, "scores": [], "classes": None}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def task_pairs(frame, plan, fitted=None):
|
|
203
|
+
columns = [str(column) for column in frame.columns]
|
|
204
|
+
found, spreads = [], {}
|
|
205
|
+
for column, output, target, base in matches(frame, plan, fitted):
|
|
206
|
+
if target is not None:
|
|
207
|
+
found.append(target_pair(frame, column, output, target, classes_for(plan, fitted, base or target)))
|
|
208
|
+
continue
|
|
209
|
+
member = spread_member(column, columns)
|
|
210
|
+
if member is None:
|
|
211
|
+
found.append(loose_pair(output, column, "values", [column], raw_of(columns, output)))
|
|
212
|
+
continue
|
|
213
|
+
stem = member[0]
|
|
214
|
+
if stem not in spreads:
|
|
215
|
+
spreads[stem] = loose_pair(stem[len("pred_"):], stem, "embedding", [])
|
|
216
|
+
found.append(spreads[stem])
|
|
217
|
+
spreads[stem]["columns"].append(column)
|
|
218
|
+
return [single(pair, columns) if pair["auto"] == "embedding" and len(pair["columns"]) < 2 else pair
|
|
219
|
+
for pair in found]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def single(pair, columns):
|
|
223
|
+
column = pair["columns"][0]
|
|
224
|
+
output = column[len("pred_"):]
|
|
225
|
+
return loose_pair(output, column, "values", [column], raw_of(columns, output))
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import numbers
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import numpy
|
|
6
|
+
import pandas
|
|
7
|
+
|
|
8
|
+
from ...core.errors import KalfaError
|
|
9
|
+
from ..reader import fields_of
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
PREDICTIONS = "predictions.parquet"
|
|
13
|
+
IMAGE_TYPES = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif",
|
|
14
|
+
".bmp": "image/bmp", ".webp": "image/webp"}
|
|
15
|
+
IMAGE_SUFFIXES = tuple(IMAGE_TYPES)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_table(directory, name=PREDICTIONS):
|
|
19
|
+
path = Path(directory) / name
|
|
20
|
+
if not path.is_file():
|
|
21
|
+
return None
|
|
22
|
+
try:
|
|
23
|
+
return pandas.read_parquet(path)
|
|
24
|
+
except (OSError, ValueError):
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def filtered(frame, where, name=PREDICTIONS):
|
|
29
|
+
if not where:
|
|
30
|
+
return frame
|
|
31
|
+
try:
|
|
32
|
+
return frame.query(where)
|
|
33
|
+
except Exception as exc:
|
|
34
|
+
raise KalfaError(f"bad where for {name}: {exc}") from exc
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def read_predictions(directory, name=PREDICTIONS, where=None):
|
|
38
|
+
frame = load_table(directory, name)
|
|
39
|
+
return None if frame is None else filtered(frame, where, name)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def prediction_tables(directory):
|
|
43
|
+
root = Path(directory)
|
|
44
|
+
if not root.is_dir():
|
|
45
|
+
return []
|
|
46
|
+
return sorted(path.name for path in root.glob("predictions*.parquet") if path.is_file())
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def read_fitted_fields(directory):
|
|
50
|
+
return fields_of(directory)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def image_column(frame):
|
|
54
|
+
if "path" not in [str(column) for column in frame.columns]:
|
|
55
|
+
return None
|
|
56
|
+
values = [value for value in frame["path"].tolist() if not missing(value)]
|
|
57
|
+
if values and all(isinstance(value, str) and value.lower().endswith(IMAGE_SUFFIXES) for value in values):
|
|
58
|
+
return "path"
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def missing(value):
|
|
63
|
+
if value is None:
|
|
64
|
+
return True
|
|
65
|
+
try:
|
|
66
|
+
return bool(pandas.isna(value))
|
|
67
|
+
except (TypeError, ValueError):
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def label_text(value):
|
|
72
|
+
if isinstance(value, numpy.generic):
|
|
73
|
+
value = value.item()
|
|
74
|
+
if missing(value):
|
|
75
|
+
return None
|
|
76
|
+
if isinstance(value, bool):
|
|
77
|
+
return str(value)
|
|
78
|
+
if isinstance(value, numbers.Integral):
|
|
79
|
+
return str(int(value))
|
|
80
|
+
if isinstance(value, numbers.Real):
|
|
81
|
+
number = float(value)
|
|
82
|
+
if math.isfinite(number) and number.is_integer():
|
|
83
|
+
return str(int(number))
|
|
84
|
+
return format(number, ".12g")
|
|
85
|
+
return str(value)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def labels(classes):
|
|
89
|
+
if not isinstance(classes, (list, tuple)) or not classes:
|
|
90
|
+
return None
|
|
91
|
+
return [label_text(label) or str(label) for label in classes]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def label_key(text):
|
|
95
|
+
try:
|
|
96
|
+
number = float(text)
|
|
97
|
+
except (TypeError, ValueError):
|
|
98
|
+
return 1, 0.0, str(text)
|
|
99
|
+
return (0, number, "") if math.isfinite(number) else (1, 0.0, str(text))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def ordered_labels(values):
|
|
103
|
+
return sorted({value for value in values if value is not None}, key=label_key)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def cached_text(known, value):
|
|
107
|
+
try:
|
|
108
|
+
return known[(type(value), value)]
|
|
109
|
+
except KeyError:
|
|
110
|
+
text = known[(type(value), value)] = label_text(value)
|
|
111
|
+
return text
|
|
112
|
+
except TypeError:
|
|
113
|
+
return label_text(value)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def texts(series):
|
|
117
|
+
known = {}
|
|
118
|
+
return numpy.asarray([cached_text(known, value) for value in series.tolist()], dtype=object)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def present(values):
|
|
122
|
+
return numpy.fromiter((value is not None for value in values), dtype=bool, count=len(values))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def numeric(series):
|
|
126
|
+
return pandas.api.types.is_numeric_dtype(series) and not pandas.api.types.is_bool_dtype(series)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def numbers_of(series):
|
|
130
|
+
return pandas.to_numeric(series, errors="coerce").to_numpy(dtype=numpy.float64)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def finite(values):
|
|
134
|
+
array = numpy.asarray(values, dtype=numpy.float64).reshape(-1)
|
|
135
|
+
return array[numpy.isfinite(array)]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def plain(values):
|
|
139
|
+
return [None if isinstance(value, float) and not math.isfinite(value) else value for value in values.tolist()]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def plain_value(value):
|
|
143
|
+
if isinstance(value, numpy.generic):
|
|
144
|
+
value = value.item()
|
|
145
|
+
if isinstance(value, float) and not math.isfinite(value):
|
|
146
|
+
return None
|
|
147
|
+
return None if missing(value) else value
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def rows_in(frame):
|
|
151
|
+
if "row" in frame.columns:
|
|
152
|
+
return frame["row"].to_numpy()
|
|
153
|
+
return frame.index.to_numpy()
|
kalfa/records/sweep.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import math
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import numpy
|
|
7
|
+
|
|
8
|
+
from .reader import (
|
|
9
|
+
MANIFEST,
|
|
10
|
+
SWEEP,
|
|
11
|
+
best_in,
|
|
12
|
+
is_finite,
|
|
13
|
+
is_number,
|
|
14
|
+
is_sweep_root,
|
|
15
|
+
normal_mode,
|
|
16
|
+
read_history,
|
|
17
|
+
read_json,
|
|
18
|
+
record_stamp,
|
|
19
|
+
safe_json,
|
|
20
|
+
summary_of,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
POINT_NAME = re.compile(r"^\d{4}$")
|
|
25
|
+
FAILED = "failed"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
is_sweep = is_sweep_root
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def read_sweep(root):
|
|
32
|
+
return read_json(root, SWEEP)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def point_dirs(root):
|
|
36
|
+
root = Path(root)
|
|
37
|
+
try:
|
|
38
|
+
found = [entry for entry in root.iterdir() if entry.is_dir() and POINT_NAME.match(entry.name)]
|
|
39
|
+
except OSError:
|
|
40
|
+
return []
|
|
41
|
+
return sorted(found, key=lambda path: int(path.name))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def objective_of(summary, objective):
|
|
45
|
+
if not isinstance(summary, dict) or not isinstance(objective, dict):
|
|
46
|
+
return None
|
|
47
|
+
watch = objective.get("watch")
|
|
48
|
+
if watch is None:
|
|
49
|
+
return None
|
|
50
|
+
if str(objective.get("at") or "best") == "last":
|
|
51
|
+
last = summary.get("last")
|
|
52
|
+
value = last.get(watch) if isinstance(last, dict) else None
|
|
53
|
+
else:
|
|
54
|
+
value = best_value(summary, watch, mode_of(objective))
|
|
55
|
+
return float(value) if is_finite(value) else None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def best_value(summary, watch, mode):
|
|
59
|
+
best = summary.get("best")
|
|
60
|
+
if isinstance(best, dict) and best.get("series") == watch and normal_mode(best.get("mode")) == mode:
|
|
61
|
+
return best.get("value")
|
|
62
|
+
path = summary.get("path")
|
|
63
|
+
if not isinstance(path, (str, Path)):
|
|
64
|
+
return None
|
|
65
|
+
return best_in(read_history(path), watch, mode)[0]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def point_id(summary, directory):
|
|
69
|
+
sweep = summary.get("sweep")
|
|
70
|
+
if isinstance(sweep, dict) and is_number(sweep.get("id")):
|
|
71
|
+
return int(sweep["id"])
|
|
72
|
+
return int(directory.name)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def point_values(summary):
|
|
76
|
+
sweep = summary.get("sweep")
|
|
77
|
+
if isinstance(sweep, dict) and isinstance(sweep.get("values"), dict):
|
|
78
|
+
return dict(sweep["values"])
|
|
79
|
+
params = summary.get("params")
|
|
80
|
+
return dict(params) if isinstance(params, dict) else {}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def point_of(directory, root, objective):
|
|
84
|
+
summary = summary_of(directory)
|
|
85
|
+
if not isinstance(summary, dict):
|
|
86
|
+
return None
|
|
87
|
+
status = summary.get("status")
|
|
88
|
+
return {
|
|
89
|
+
"id": point_id(summary, directory),
|
|
90
|
+
"path": directory.relative_to(root).as_posix(),
|
|
91
|
+
"values": point_values(summary),
|
|
92
|
+
"status": status,
|
|
93
|
+
"turn": summary.get("turn"),
|
|
94
|
+
"total": summary.get("total"),
|
|
95
|
+
"objective": None if status == FAILED else objective_of(summary, objective),
|
|
96
|
+
"summary": summary,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def sweep_points(root, cache=None):
|
|
101
|
+
root = Path(root)
|
|
102
|
+
sweep = read_sweep(root)
|
|
103
|
+
objective = sweep.get("objective") if isinstance(sweep, dict) else None
|
|
104
|
+
asked = json.dumps(objective, sort_keys=True, default=str)
|
|
105
|
+
points = []
|
|
106
|
+
for directory in point_dirs(root):
|
|
107
|
+
if cache is None:
|
|
108
|
+
point = point_of(directory, root, objective)
|
|
109
|
+
else:
|
|
110
|
+
stamp = record_stamp(directory, safe_json(directory, MANIFEST))
|
|
111
|
+
point = cache.get(("point", str(directory), str(root), asked), stamp,
|
|
112
|
+
lambda directory=directory: point_of(directory, root, objective))
|
|
113
|
+
if point is not None:
|
|
114
|
+
points.append(dict(point))
|
|
115
|
+
return points
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def is_valued(point):
|
|
119
|
+
return is_finite(point.get("objective")) and point.get("status") != FAILED
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def id_key(point):
|
|
123
|
+
identity = point.get("id")
|
|
124
|
+
return (0, identity, "") if is_number(identity) else (1, 0, str(identity))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def mode_of(objective):
|
|
128
|
+
return normal_mode(objective.get("mode") if isinstance(objective, dict) else objective)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def rank_points(points, objective):
|
|
132
|
+
maximize = mode_of(objective) == "max"
|
|
133
|
+
valued = [point for point in points if is_valued(point)]
|
|
134
|
+
unvalued = [point for point in points if not is_valued(point)]
|
|
135
|
+
valued.sort(key=lambda point: ((-point["objective"] if maximize else point["objective"]), id_key(point)))
|
|
136
|
+
unvalued.sort(key=id_key)
|
|
137
|
+
return valued + unvalued
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def best_point(points, objective):
|
|
141
|
+
ranked = rank_points(points, objective)
|
|
142
|
+
if ranked and is_valued(ranked[0]):
|
|
143
|
+
return ranked[0]
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def running_best(points, objective):
|
|
148
|
+
maximize = mode_of(objective) == "max"
|
|
149
|
+
ordered = sorted(points, key=id_key)
|
|
150
|
+
best = None
|
|
151
|
+
found = {"ids": [], "objective": [], "best": []}
|
|
152
|
+
for point in ordered:
|
|
153
|
+
value = point["objective"] if is_valued(point) else None
|
|
154
|
+
if value is not None and (best is None or (value > best if maximize else value < best)):
|
|
155
|
+
best = value
|
|
156
|
+
found["ids"].append(point.get("id"))
|
|
157
|
+
found["objective"].append(value)
|
|
158
|
+
found["best"].append(best)
|
|
159
|
+
return found
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def is_log_space(spec):
|
|
163
|
+
return isinstance(spec, dict) and isinstance(spec.get("log"), (list, tuple))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def label_of(value):
|
|
167
|
+
if isinstance(value, float):
|
|
168
|
+
return f"{value:.4g}"
|
|
169
|
+
return value if isinstance(value, str) else json.dumps(value, default=str)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def axis_of(key, values, spec):
|
|
173
|
+
numeric = all(is_finite(value) for value in values)
|
|
174
|
+
if numeric and is_log_space(spec) and all(value > 0 for value in values):
|
|
175
|
+
shown = sorted(set(values))
|
|
176
|
+
return {"key": key, "label": f"{key} (log)", "values": [math.log10(value) for value in values],
|
|
177
|
+
"ticks": [math.log10(value) for value in shown] if len(shown) <= 12 else None,
|
|
178
|
+
"text": [label_of(float(value)) for value in shown] if len(shown) <= 12 else None}
|
|
179
|
+
if numeric:
|
|
180
|
+
return {"key": key, "label": key, "values": [float(value) for value in values], "ticks": None, "text": None}
|
|
181
|
+
names = []
|
|
182
|
+
for value in values:
|
|
183
|
+
name = label_of(value)
|
|
184
|
+
if name not in names:
|
|
185
|
+
names.append(name)
|
|
186
|
+
return {"key": key, "label": key, "values": [float(names.index(label_of(value))) for value in values],
|
|
187
|
+
"ticks": [float(position) for position in range(len(names))], "text": names}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def axes_of(points, sweep):
|
|
191
|
+
sweep = sweep if isinstance(sweep, dict) else {}
|
|
192
|
+
objective = sweep.get("objective") if isinstance(sweep.get("objective"), dict) else {}
|
|
193
|
+
space = sweep.get("space") if isinstance(sweep.get("space"), dict) else {}
|
|
194
|
+
valued = sorted((point for point in points if is_valued(point)), key=id_key)
|
|
195
|
+
keys = []
|
|
196
|
+
for point in valued:
|
|
197
|
+
keys.extend(key for key in point["values"] if key not in keys)
|
|
198
|
+
keys = [key for key in keys if all(key in point["values"] for point in valued)]
|
|
199
|
+
axes = [axis_of(key, [point["values"][key] for point in valued], space.get(key)) for key in keys]
|
|
200
|
+
watch = str(objective.get("watch") or "objective")
|
|
201
|
+
axes.append({"key": "objective", "label": watch, "values": [float(point["objective"]) for point in valued],
|
|
202
|
+
"ticks": None, "text": None})
|
|
203
|
+
return {"ids": [point.get("id") for point in valued], "axes": axes if valued else [],
|
|
204
|
+
"left_out": len(points) - len(valued), "mode": mode_of(objective)}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def frozen(value):
|
|
208
|
+
if isinstance(value, dict):
|
|
209
|
+
return tuple(sorted((str(key), frozen(item)) for key, item in value.items()))
|
|
210
|
+
if isinstance(value, (list, tuple, set)):
|
|
211
|
+
return tuple(frozen(item) for item in value)
|
|
212
|
+
return value
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def numeric_last(summary):
|
|
216
|
+
last = summary.get("last")
|
|
217
|
+
if not isinstance(last, dict):
|
|
218
|
+
return {}
|
|
219
|
+
return {key: float(value) for key, value in last.items() if is_finite(value)}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def group_folds(summaries, key="fold"):
|
|
223
|
+
groups = {}
|
|
224
|
+
for summary in summaries:
|
|
225
|
+
if not isinstance(summary, dict):
|
|
226
|
+
continue
|
|
227
|
+
params = summary.get("params")
|
|
228
|
+
if not isinstance(params, dict) or key not in params:
|
|
229
|
+
continue
|
|
230
|
+
rest = {name: value for name, value in params.items() if name != key}
|
|
231
|
+
group_key = frozen(rest)
|
|
232
|
+
if group_key not in groups:
|
|
233
|
+
groups[group_key] = {"group": rest, "members": [], "mean": {}, "std": {}}
|
|
234
|
+
groups[group_key]["members"].append(summary)
|
|
235
|
+
for group in groups.values():
|
|
236
|
+
lasts = [numeric_last(member) for member in group["members"]]
|
|
237
|
+
common = set(lasts[0])
|
|
238
|
+
for last in lasts[1:]:
|
|
239
|
+
common &= set(last)
|
|
240
|
+
for series in sorted(common):
|
|
241
|
+
values = numpy.asarray([last[series] for last in lasts], dtype=float)
|
|
242
|
+
group["mean"][series] = float(values.mean())
|
|
243
|
+
group["std"][series] = float(numpy.std(values))
|
|
244
|
+
return list(groups.values())
|
kalfa/std/__init__.py
ADDED
kalfa/std/actions.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
|
|
3
|
+
from kalfa.core.aliases import register
|
|
4
|
+
from kalfa.core.errors import BuildError, RunError
|
|
5
|
+
from kalfa.core.protocols import Action
|
|
6
|
+
from kalfa.std.samples import save_samples
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WriteSamples(Action):
|
|
10
|
+
requires_generate = True
|
|
11
|
+
|
|
12
|
+
def __init__(self, sampler=None, n=16):
|
|
13
|
+
self.sampler = sampler
|
|
14
|
+
self.n = int(n)
|
|
15
|
+
|
|
16
|
+
def bind(self, ctx):
|
|
17
|
+
if self.sampler is None and getattr(ctx, "generate", None) is None:
|
|
18
|
+
raise BuildError(f"{self.path or 'write_samples'}: write_samples needs a sampler: write sampler: "
|
|
19
|
+
"{use: gan_sampler, ...} or a generate section")
|
|
20
|
+
|
|
21
|
+
def __call__(self, ctx):
|
|
22
|
+
sampler = self.sampler if self.sampler is not None else getattr(ctx, "generate", None)
|
|
23
|
+
if sampler is None:
|
|
24
|
+
raise RunError(f"{self.path or 'write_samples'} needs a sampler: write sampler: <a sampler call> or a "
|
|
25
|
+
"generate section")
|
|
26
|
+
with torch.no_grad():
|
|
27
|
+
samples = sampler.sample(ctx, self.n)
|
|
28
|
+
if getattr(ctx, "record", None) is None:
|
|
29
|
+
return []
|
|
30
|
+
return save_samples(samples, ctx.record.path("samples"), f"turn_{ctx.turn}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
register("write_samples", "kalfa.std.actions.WriteSamples", slot="action",
|
|
34
|
+
doc="draws n samples from its sampler (or the run's generate) and writes samples/turn_<k>.pt, a png grid "
|
|
35
|
+
"for images or a txt for text")
|