hiplot-plus 0.1.0__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.
- hiplot/__init__.py +38 -0
- hiplot/__main__.py +10 -0
- hiplot/compress.py +70 -0
- hiplot/csvvalues.py +83 -0
- hiplot/experiment.py +637 -0
- hiplot/fetchers.py +322 -0
- hiplot/fetchers_demo.py +459 -0
- hiplot/ipython.py +217 -0
- hiplot/pkginfo.py +15 -0
- hiplot/py.typed +0 -0
- hiplot/render.py +141 -0
- hiplot/server.py +76 -0
- hiplot/static/built/hiplot.bundle.js +1224 -0
- hiplot/static/built/streamlit_component/hiplot_streamlit.bundle.js +1220 -0
- hiplot/static/built/streamlit_component/index.html +16 -0
- hiplot/static/icon.png +0 -0
- hiplot/streamlit_helpers.py +127 -0
- hiplot/templates/index.html +68 -0
- hiplot_plus-0.1.0.dist-info/METADATA +197 -0
- hiplot_plus-0.1.0.dist-info/RECORD +24 -0
- hiplot_plus-0.1.0.dist-info/WHEEL +4 -0
- hiplot_plus-0.1.0.dist-info/entry_points.txt +4 -0
- hiplot_plus-0.1.0.dist-info/licenses/LICENSE +21 -0
- hiplot_plus-0.1.0.dist-info/licenses/NOTICE +207 -0
hiplot/fetchers.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
|
2
|
+
# This source code is licensed under the MIT license found in the
|
|
3
|
+
# LICENSE file in the root directory of this source tree.
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import importlib
|
|
7
|
+
import importlib.util
|
|
8
|
+
import itertools
|
|
9
|
+
import json
|
|
10
|
+
import random
|
|
11
|
+
import re
|
|
12
|
+
import typing as tp
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from . import experiment as hip
|
|
16
|
+
from .fetchers_demo import README_DEMOS
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class NoFetcherFound(Exception):
|
|
20
|
+
def __init__(self, uri: str):
|
|
21
|
+
super().__init__(f"Unable to fetch an HiPlot experiment from '{uri}'")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_xp_with_fetchers_partial(fetchers: list[hip.ExperimentFetcher], uri: str) -> tuple[hip.Experiment, int]:
|
|
25
|
+
"""
|
|
26
|
+
Attempts to parse an uri.
|
|
27
|
+
Returns a corresponding experiment, and position in uri following the last used character.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
/path1
|
|
31
|
+
/path2
|
|
32
|
+
Returns the Experiment for `/path1` and position the substring `\n/path2`
|
|
33
|
+
"""
|
|
34
|
+
eol = uri.find("\n")
|
|
35
|
+
if eol == -1:
|
|
36
|
+
eol = len(uri)
|
|
37
|
+
for f in fetchers:
|
|
38
|
+
try:
|
|
39
|
+
endoffetcher = eol
|
|
40
|
+
get_uri_length: tp.Callable[[str], int] | None = getattr(f, "get_uri_length", None)
|
|
41
|
+
if get_uri_length is not None:
|
|
42
|
+
endoffetcher = get_uri_length(uri)
|
|
43
|
+
return f(uri[:endoffetcher]), endoffetcher
|
|
44
|
+
except hip.ExperimentFetcherDoesntApply:
|
|
45
|
+
continue
|
|
46
|
+
raise NoFetcherFound(uri)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def load_xps_with_fetchers(fetchers: list[hip.ExperimentFetcher], uri: str) -> list[hip.Experiment]:
|
|
50
|
+
uri = uri.lstrip()
|
|
51
|
+
xps: list[hip.Experiment] = []
|
|
52
|
+
while uri:
|
|
53
|
+
xp, endfetcher = load_xp_with_fetchers_partial(fetchers, uri)
|
|
54
|
+
uri = uri[endfetcher:].lstrip()
|
|
55
|
+
xps.append(xp)
|
|
56
|
+
assert xps, uri # If URI is empty, we should raise in `load_xp_with_fetchers_partial`
|
|
57
|
+
return xps
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def load_xp_with_fetchers(fetchers: list[hip.ExperimentFetcher], uri: str) -> hip.Experiment:
|
|
61
|
+
xps = load_xps_with_fetchers(fetchers, uri)
|
|
62
|
+
return hip.Experiment.merge({f"{i}": xp for i, xp in enumerate(xps)}) if len(xps) > 1 else xps[0]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class MultipleFetcher:
|
|
66
|
+
MULTI_PREFIX = "multi://"
|
|
67
|
+
|
|
68
|
+
def __init__(self, fetchers: list[hip.ExperimentFetcher]) -> None:
|
|
69
|
+
self.fetchers: list[hip.ExperimentFetcher] = [*fetchers, self]
|
|
70
|
+
|
|
71
|
+
def __call__(self, uri: str) -> hip.Experiment:
|
|
72
|
+
if not uri.startswith(self.MULTI_PREFIX):
|
|
73
|
+
raise hip.ExperimentFetcherDoesntApply()
|
|
74
|
+
defs = json.loads(uri[len(self.MULTI_PREFIX) :])
|
|
75
|
+
if isinstance(defs, list):
|
|
76
|
+
return hip.Experiment.merge({v: load_xp_with_fetchers(self.fetchers, v) for v in defs})
|
|
77
|
+
return hip.Experiment.merge({k: load_xp_with_fetchers(self.fetchers, v) for k, v in defs.items()})
|
|
78
|
+
|
|
79
|
+
def get_uri_length(self, uri: str) -> int:
|
|
80
|
+
"""
|
|
81
|
+
Returns the end position of the multi block
|
|
82
|
+
"""
|
|
83
|
+
if not uri.startswith(self.MULTI_PREFIX):
|
|
84
|
+
raise hip.ExperimentFetcherDoesntApply()
|
|
85
|
+
decoder = json.JSONDecoder()
|
|
86
|
+
_, current_read_offset = decoder.raw_decode(uri[len(self.MULTI_PREFIX) :])
|
|
87
|
+
return len(self.MULTI_PREFIX) + current_read_offset
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class InlineJsonFetcher:
|
|
91
|
+
URI_PREFIX = "json://"
|
|
92
|
+
|
|
93
|
+
def __call__(self, uri: str) -> hip.Experiment:
|
|
94
|
+
if not uri.startswith(self.URI_PREFIX):
|
|
95
|
+
raise hip.ExperimentFetcherDoesntApply()
|
|
96
|
+
exp_rows = json.loads(uri[len(self.URI_PREFIX) :])
|
|
97
|
+
assert isinstance(exp_rows, list)
|
|
98
|
+
return hip.Experiment.from_iterable(exp_rows)
|
|
99
|
+
|
|
100
|
+
def get_uri_length(self, uri: str) -> int:
|
|
101
|
+
"""
|
|
102
|
+
Returns the end position of the multi block
|
|
103
|
+
"""
|
|
104
|
+
if not uri.startswith(self.URI_PREFIX):
|
|
105
|
+
raise hip.ExperimentFetcherDoesntApply()
|
|
106
|
+
decoder = json.JSONDecoder()
|
|
107
|
+
_, current_read_offset = decoder.raw_decode(uri[len(self.URI_PREFIX) :])
|
|
108
|
+
return len(self.URI_PREFIX) + current_read_offset
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def load_demo(uri: str) -> hip.Experiment:
|
|
112
|
+
if uri in README_DEMOS:
|
|
113
|
+
# Keep demo data deterministic across page reloads/tests while preserving
|
|
114
|
+
# the caller's RNG state outside this function.
|
|
115
|
+
random_state = random.getstate()
|
|
116
|
+
try:
|
|
117
|
+
random.seed(f"hiplot-demo:{uri}")
|
|
118
|
+
return README_DEMOS[uri]()
|
|
119
|
+
finally:
|
|
120
|
+
random.setstate(random_state)
|
|
121
|
+
raise hip.ExperimentFetcherDoesntApply()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def load_csv(uri: str) -> hip.Experiment:
|
|
125
|
+
if not uri.endswith(".csv"):
|
|
126
|
+
raise hip.ExperimentFetcherDoesntApply(f"Not a CSV file: {uri}")
|
|
127
|
+
try:
|
|
128
|
+
return hip.Experiment.from_csv(uri)
|
|
129
|
+
except FileNotFoundError as error:
|
|
130
|
+
raise hip.ExperimentFetcherDoesntApply(f"No such file: {uri}") from error
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def load_json(uri: str) -> hip.Experiment:
|
|
134
|
+
if not uri.endswith(".json"):
|
|
135
|
+
raise hip.ExperimentFetcherDoesntApply(f"Not a JSON file: {uri}")
|
|
136
|
+
dat = json.loads(Path(uri).read_text(encoding="utf-8"))
|
|
137
|
+
if "job_id" in dat[0] and "kwargs" in dat[0] and "results" in dat[0]:
|
|
138
|
+
# Nevergrad JSON
|
|
139
|
+
return hip.Experiment.from_iterable(
|
|
140
|
+
[
|
|
141
|
+
{
|
|
142
|
+
"id": j["job_id"],
|
|
143
|
+
**{param_name: str(param_val) for param_name, param_val in j["kwargs"].items()},
|
|
144
|
+
**j["results"]["scores"],
|
|
145
|
+
}
|
|
146
|
+
for j in dat
|
|
147
|
+
]
|
|
148
|
+
)
|
|
149
|
+
return hip.Experiment.from_iterable(dat)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _load_fairseq_metrics_inline(line: str) -> dict[str, tp.Any]:
|
|
153
|
+
line = line.removeprefix("| epoch ")
|
|
154
|
+
epoch = int(line[:3])
|
|
155
|
+
values: dict[str, tp.Any] = {"epoch": epoch}
|
|
156
|
+
# | epoch 002 | loss 8.413 | ...
|
|
157
|
+
# | epoch 002 | valid on 'valid' subset | loss 7.599 | nll_loss 7.599 | ...
|
|
158
|
+
parts = line.split("|")[1:]
|
|
159
|
+
prefix = ""
|
|
160
|
+
for p in parts:
|
|
161
|
+
p = p.strip()
|
|
162
|
+
match_ds = re.match(r"valid on '([a-zA-Z0-9]*)' subset", p)
|
|
163
|
+
if match_ds is not None:
|
|
164
|
+
prefix = match_ds.group(1) + "_"
|
|
165
|
+
continue
|
|
166
|
+
key = prefix + p[::-1].split(" ", 1)[1][::-1].strip()
|
|
167
|
+
value = p[::-1].split(" ", 1)[0][::-1].strip()
|
|
168
|
+
try:
|
|
169
|
+
values[key] = float(value)
|
|
170
|
+
except ValueError:
|
|
171
|
+
values[key] = value
|
|
172
|
+
return values
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _parse_namespace_keywords(line: str) -> dict[str, tp.Any]:
|
|
176
|
+
call = ast.parse(line, mode="eval").body
|
|
177
|
+
if not isinstance(call, ast.Call):
|
|
178
|
+
raise ValueError(f"Expected a Namespace(...) call, got: {line[:80]}")
|
|
179
|
+
return {kw.arg: ast.literal_eval(kw.value) for kw in call.keywords if kw.arg is not None}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def load_fairseq(uri: str) -> hip.Experiment:
|
|
183
|
+
PREFIX = "fairseq://"
|
|
184
|
+
if not uri.startswith(PREFIX):
|
|
185
|
+
raise hip.ExperimentFetcherDoesntApply()
|
|
186
|
+
uri = uri[len(PREFIX) :]
|
|
187
|
+
train_log = Path(uri)
|
|
188
|
+
if train_log.is_dir():
|
|
189
|
+
found = False
|
|
190
|
+
try_files = [
|
|
191
|
+
*(train_log / f for f in ["train.log", "process.out", "process_0.out"]),
|
|
192
|
+
*train_log.glob("*.log"),
|
|
193
|
+
*(train_log / "slurm_logs").glob("*.log"),
|
|
194
|
+
]
|
|
195
|
+
for try_log_file in try_files:
|
|
196
|
+
if try_log_file.is_file():
|
|
197
|
+
found = True
|
|
198
|
+
train_log = try_log_file
|
|
199
|
+
break
|
|
200
|
+
if not found:
|
|
201
|
+
raise hip.ExperimentFetcherDoesntApply("No log file found")
|
|
202
|
+
lines = train_log.read_text(encoding="utf-8").split("\n")
|
|
203
|
+
|
|
204
|
+
datapoints: list[dict[str, tp.Any]] = []
|
|
205
|
+
params: dict[str, tp.Any] = {}
|
|
206
|
+
logs_prefix_re = r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \| [A-Z]* \| )"
|
|
207
|
+
for raw_line in lines:
|
|
208
|
+
# Strip log prefix, eg "2020-03-08 16:48:16 | INFO | "
|
|
209
|
+
m = re.match(logs_prefix_re, raw_line)
|
|
210
|
+
line = raw_line if m is None else raw_line[m.span()[1] :]
|
|
211
|
+
if line.startswith("Namespace("):
|
|
212
|
+
# Format: Namespace(activation_dropout=0.1, activation_fn='relu', ...). Evaluating it as
|
|
213
|
+
# `dict(...)` would run user input, so parse the keywords as literals instead (a too
|
|
214
|
+
# complex string can still exhaust the parser's stack depth).
|
|
215
|
+
params = _parse_namespace_keywords(line)
|
|
216
|
+
continue
|
|
217
|
+
# Results in JSON format
|
|
218
|
+
# valid | {"epoch": 33, "valid_loss": "0.723", "valid_ppl": "1.65", ...}
|
|
219
|
+
if line.startswith("valid | {"):
|
|
220
|
+
json_string = line.split("|", 1)[-1].lstrip()
|
|
221
|
+
valid_metrics = json.loads(json_string)
|
|
222
|
+
datapoints.append(valid_metrics)
|
|
223
|
+
# For older version of fairseq
|
|
224
|
+
if line.startswith("| epoch "):
|
|
225
|
+
values = _load_fairseq_metrics_inline(line)
|
|
226
|
+
if datapoints and datapoints[-1]["epoch"] == values["epoch"]:
|
|
227
|
+
datapoints[-1].update(values)
|
|
228
|
+
else:
|
|
229
|
+
datapoints.append(values)
|
|
230
|
+
datapoints = [
|
|
231
|
+
{
|
|
232
|
+
**params,
|
|
233
|
+
**values, # overrides 'learning rate' for instance
|
|
234
|
+
}
|
|
235
|
+
for values in datapoints
|
|
236
|
+
]
|
|
237
|
+
datapoints.sort(key=lambda d: float(d["epoch"]))
|
|
238
|
+
xp = hip.Experiment.from_iterable(datapoints)
|
|
239
|
+
for dp, next_dp in itertools.pairwise(xp.datapoints):
|
|
240
|
+
next_dp.from_uid = dp.uid
|
|
241
|
+
return xp
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class Wav2letterLoader:
|
|
245
|
+
def _parse_metrics(self, file: Path) -> list[dict[str, tp.Any]]:
|
|
246
|
+
# 001_perf:
|
|
247
|
+
"""
|
|
248
|
+
# date\tkey1\tkey2...
|
|
249
|
+
2019-09-30\tval1\tval2...
|
|
250
|
+
"""
|
|
251
|
+
PERF_PREFIX = "perf_"
|
|
252
|
+
lines = file.read_text(encoding="utf-8").split("\n")
|
|
253
|
+
metrics: list[dict[str, tp.Any]] = []
|
|
254
|
+
for line in lines[1:]:
|
|
255
|
+
if line == "":
|
|
256
|
+
continue
|
|
257
|
+
epoch_metrics: dict[str, tp.Any] = {}
|
|
258
|
+
for name, val in zip(lines[0].split()[1:], line.split(), strict=False):
|
|
259
|
+
try:
|
|
260
|
+
epoch_metrics[PERF_PREFIX + name] = float(val)
|
|
261
|
+
except ValueError:
|
|
262
|
+
epoch_metrics[PERF_PREFIX + name] = val
|
|
263
|
+
metrics.append(epoch_metrics)
|
|
264
|
+
return metrics
|
|
265
|
+
|
|
266
|
+
def __call__(self, uri: str) -> hip.Experiment:
|
|
267
|
+
PREFIX = "w2l://"
|
|
268
|
+
if not uri.startswith(PREFIX):
|
|
269
|
+
raise hip.ExperimentFetcherDoesntApply()
|
|
270
|
+
uri = uri[len(PREFIX) :]
|
|
271
|
+
perfs = sorted(Path(uri).glob("*_perf"))
|
|
272
|
+
|
|
273
|
+
prev_ckpt_name: str | None = None
|
|
274
|
+
xp = hip.Experiment()
|
|
275
|
+
for p in perfs:
|
|
276
|
+
mtrics = self._parse_metrics(p)
|
|
277
|
+
for m in mtrics:
|
|
278
|
+
ckpt_name = uri[-5:] + "_" + str(len(xp.datapoints))
|
|
279
|
+
xp.datapoints.append(hip.Datapoint(uid=ckpt_name, from_uid=prev_ckpt_name, values=m))
|
|
280
|
+
prev_ckpt_name = ckpt_name
|
|
281
|
+
return xp
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
load_wav2letter = Wav2letterLoader()
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _get_module_by_name_in_cwd(name: str) -> tp.Any:
|
|
288
|
+
spec = importlib.util.spec_from_file_location(name, str(Path.cwd() / f"{name}.py"))
|
|
289
|
+
if spec is None or spec.loader is None:
|
|
290
|
+
return None
|
|
291
|
+
module = importlib.util.module_from_spec(spec)
|
|
292
|
+
spec.loader.exec_module(module)
|
|
293
|
+
return module
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def get_fetcher(fetcher_spec: str) -> hip.ExperimentFetcher:
|
|
297
|
+
parts = fetcher_spec.split(".")
|
|
298
|
+
try:
|
|
299
|
+
module = importlib.import_module(".".join(parts[:-1]))
|
|
300
|
+
except ModuleNotFoundError:
|
|
301
|
+
if len(parts) != 2:
|
|
302
|
+
raise
|
|
303
|
+
module = _get_module_by_name_in_cwd(parts[0])
|
|
304
|
+
if module is None:
|
|
305
|
+
raise RuntimeError(f"Unable to create fetcher '{fetcher_spec}'")
|
|
306
|
+
|
|
307
|
+
fetcher: hip.ExperimentFetcher = getattr(module, parts[-1])
|
|
308
|
+
return fetcher
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def get_fetchers(add_fetchers: list[str]) -> list[hip.ExperimentFetcher]:
|
|
312
|
+
xp_fetchers: list[hip.ExperimentFetcher] = [
|
|
313
|
+
load_demo,
|
|
314
|
+
load_csv,
|
|
315
|
+
load_json,
|
|
316
|
+
load_fairseq,
|
|
317
|
+
load_wav2letter,
|
|
318
|
+
InlineJsonFetcher(),
|
|
319
|
+
]
|
|
320
|
+
xp_fetchers.extend(get_fetcher(fetcher_spec) for fetcher_spec in add_fetchers)
|
|
321
|
+
xp_fetchers.append(MultipleFetcher(xp_fetchers))
|
|
322
|
+
return xp_fetchers
|