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/data/text.py
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import joblib
|
|
6
|
+
import numpy
|
|
7
|
+
import pandas
|
|
8
|
+
import torch
|
|
9
|
+
from torch.utils.data import Dataset
|
|
10
|
+
|
|
11
|
+
from kalfa.core.aliases import register, target_path
|
|
12
|
+
from kalfa.core.errors import BuildError, error, warning
|
|
13
|
+
from kalfa.std.common import positive_int
|
|
14
|
+
from kalfa.std.data import flow
|
|
15
|
+
from kalfa.std.data.base import Loaders, config_directories, seeded_split
|
|
16
|
+
from kalfa.std.data.columns import name_problems, target_of
|
|
17
|
+
from kalfa.std.data.options import batch_of, locate, split_check, split_of
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger("kalfa.data")
|
|
21
|
+
|
|
22
|
+
TEXT_COLUMNS = ("text", "line")
|
|
23
|
+
TARGETS = ("targets", "input_ids")
|
|
24
|
+
BATCH_SIZE = 64
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def stride_of(value, seq_len):
|
|
28
|
+
return seq_len if value is None else positive_int(value, "stride")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def id_list(values):
|
|
32
|
+
if isinstance(values, torch.Tensor):
|
|
33
|
+
values = values.detach().cpu().numpy()
|
|
34
|
+
return [int(value) for value in numpy.asarray(values).reshape(-1).tolist()]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def id_tensor(encoded):
|
|
38
|
+
if isinstance(encoded, torch.Tensor):
|
|
39
|
+
return encoded.detach().to(device="cpu", dtype=torch.int64).reshape(-1)
|
|
40
|
+
return torch.from_numpy(numpy.array(encoded, dtype="int64").reshape(-1))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def window_count(tokens, seq_len, stride):
|
|
44
|
+
return 0 if tokens < seq_len + 1 else (tokens - seq_len - 1) // stride + 1
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def size_of(tokenizer):
|
|
48
|
+
for name in ("size", "vocab_size"):
|
|
49
|
+
value = getattr(tokenizer, name, None)
|
|
50
|
+
if callable(value):
|
|
51
|
+
value = value()
|
|
52
|
+
if value is not None:
|
|
53
|
+
return int(value)
|
|
54
|
+
if hasattr(tokenizer, "__len__"):
|
|
55
|
+
return len(tokenizer)
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def is_tokenizer(value):
|
|
60
|
+
return callable(getattr(value, "encode", None)) and callable(getattr(value, "decode", None))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def tokenizer_of(spec, lowercase=False):
|
|
64
|
+
if spec is None or (isinstance(spec, str) and spec == "char"):
|
|
65
|
+
return CharTokenizer(lowercase)
|
|
66
|
+
if isinstance(spec, dict):
|
|
67
|
+
if "use" not in spec:
|
|
68
|
+
raise BuildError(f"tokenizer: a mapping needs use, got the keys {sorted(spec)}")
|
|
69
|
+
params = {key: value for key, value in spec.items() if key != "use"}
|
|
70
|
+
made = target_of(spec["use"], "tokenizer")(**params)
|
|
71
|
+
elif isinstance(spec, (str, type)):
|
|
72
|
+
target = target_of(spec, "tokenizer") if isinstance(spec, str) else spec
|
|
73
|
+
chars = isinstance(target, type) and issubclass(target, CharTokenizer)
|
|
74
|
+
made = target(lowercase) if chars else target()
|
|
75
|
+
else:
|
|
76
|
+
made = spec
|
|
77
|
+
if not is_tokenizer(made):
|
|
78
|
+
raise BuildError(f"tokenizer is char, a short name, an import path, a call or an object with encode and "
|
|
79
|
+
f"decode, got {type(made).__name__}")
|
|
80
|
+
return made
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def tokenizer_check(value):
|
|
84
|
+
if isinstance(value, str) and value != "char":
|
|
85
|
+
target_of(value)
|
|
86
|
+
return value
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class CharTokenizer:
|
|
90
|
+
unknown = "<unk>"
|
|
91
|
+
|
|
92
|
+
def __init__(self, lowercase=False):
|
|
93
|
+
self.lowercase = bool(lowercase)
|
|
94
|
+
self.vocab = []
|
|
95
|
+
self.index = {}
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def size(self):
|
|
99
|
+
return len(self.vocab)
|
|
100
|
+
|
|
101
|
+
def prepared(self, text):
|
|
102
|
+
text = str(text)
|
|
103
|
+
return text.lower() if self.lowercase else text
|
|
104
|
+
|
|
105
|
+
def adopt(self, vocab):
|
|
106
|
+
self.vocab = [str(item) for item in vocab]
|
|
107
|
+
self.index = {character: position for position, character in enumerate(self.vocab) if position}
|
|
108
|
+
return self
|
|
109
|
+
|
|
110
|
+
def fit(self, texts):
|
|
111
|
+
if isinstance(texts, str):
|
|
112
|
+
texts = [texts]
|
|
113
|
+
characters = {"\n"}
|
|
114
|
+
for text in texts:
|
|
115
|
+
characters.update(self.prepared(text))
|
|
116
|
+
return self.adopt([self.unknown, *sorted(characters)])
|
|
117
|
+
|
|
118
|
+
def encode(self, text):
|
|
119
|
+
if not self.vocab:
|
|
120
|
+
raise BuildError("the char tokenizer is not fitted; call fit(texts) or from_json(data) first")
|
|
121
|
+
return [self.index.get(character, 0) for character in self.prepared(text)]
|
|
122
|
+
|
|
123
|
+
def decode(self, ids):
|
|
124
|
+
size = len(self.vocab)
|
|
125
|
+
return "".join(self.vocab[position] for position in id_list(ids) if 0 < position < size)
|
|
126
|
+
|
|
127
|
+
def to_json(self):
|
|
128
|
+
return {"kind": "char", "vocab": list(self.vocab), "lowercase": self.lowercase}
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def from_json(cls, data):
|
|
132
|
+
if isinstance(data, (str, bytes)):
|
|
133
|
+
data = json.loads(data)
|
|
134
|
+
if not isinstance(data, dict):
|
|
135
|
+
raise BuildError(f"a char tokenizer is read from a mapping with vocab, got {type(data).__name__}")
|
|
136
|
+
kind = data.get("kind", "char")
|
|
137
|
+
if kind != "char":
|
|
138
|
+
raise BuildError(f"the tokenizer kind {kind!r} is not char")
|
|
139
|
+
vocab = data.get("vocab")
|
|
140
|
+
if not isinstance(vocab, list) or not vocab or vocab[0] != cls.unknown:
|
|
141
|
+
raise BuildError(f"a char tokenizer's vocab is a list that starts with {cls.unknown!r}")
|
|
142
|
+
return cls(lowercase=data.get("lowercase", False)).adopt(vocab)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class TextDataset(Dataset):
|
|
146
|
+
def __init__(self, ids, seq_len, stride, short=False):
|
|
147
|
+
self.ids = ids
|
|
148
|
+
self.length = int(seq_len)
|
|
149
|
+
count = window_count(len(ids), self.length, stride)
|
|
150
|
+
if not count and short and len(ids) >= 2:
|
|
151
|
+
self.length = len(ids) - 1
|
|
152
|
+
count = 1
|
|
153
|
+
self.starts = numpy.arange(count, dtype="int64") * int(stride)
|
|
154
|
+
self.rows = numpy.arange(count, dtype="int64")
|
|
155
|
+
|
|
156
|
+
def __len__(self):
|
|
157
|
+
return len(self.starts)
|
|
158
|
+
|
|
159
|
+
def __getitem__(self, position):
|
|
160
|
+
start = int(self.starts[position])
|
|
161
|
+
chunk = self.ids[start:start + self.length + 1]
|
|
162
|
+
return {"input_ids": chunk[:-1], "targets": chunk[1:]}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
VALIDATORS = (
|
|
166
|
+
("seq_len", lambda value: positive_int(value, "seq_len")),
|
|
167
|
+
("stride", lambda value: stride_of(value, 1)),
|
|
168
|
+
("batch", lambda value: batch_of(value, BATCH_SIZE)),
|
|
169
|
+
("tokenizer", tokenizer_check),
|
|
170
|
+
("split", split_check),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class TextLines(Loaders):
|
|
175
|
+
inputs = ("input_ids",)
|
|
176
|
+
exposes = ("vocab_size", "tokenizer", "seq_len")
|
|
177
|
+
predictions = False
|
|
178
|
+
|
|
179
|
+
@classmethod
|
|
180
|
+
def check(cls, params, problems, directories=()):
|
|
181
|
+
if params.get("split") is None:
|
|
182
|
+
problems.append(error("split_required", "data.split is required: a list of ratios such as "
|
|
183
|
+
"[0.9, 0.05, 0.05] or a call such as {use: sequential, ratios: [0.9, 0.1]}",
|
|
184
|
+
"data.split"))
|
|
185
|
+
file = params.get("file")
|
|
186
|
+
if isinstance(file, (str, Path)) and str(file):
|
|
187
|
+
try:
|
|
188
|
+
locate(file, directories)
|
|
189
|
+
except BuildError as failure:
|
|
190
|
+
problems.append(warning("data_file_missing", str(failure), "data.file",
|
|
191
|
+
hint="the run looks in the working directory, then next to the config"))
|
|
192
|
+
elif "file" in params:
|
|
193
|
+
problems.append(error("data_value", f"file is the path of a utf-8 text file, got {file!r}", "data.file"))
|
|
194
|
+
for name, validator in VALIDATORS:
|
|
195
|
+
if name not in params:
|
|
196
|
+
continue
|
|
197
|
+
try:
|
|
198
|
+
validator(params[name])
|
|
199
|
+
except BuildError as failure:
|
|
200
|
+
problems.append(error("data_value", str(failure), f"data.{name}"))
|
|
201
|
+
name_problems(params.get("split"), "data.split", problems)
|
|
202
|
+
|
|
203
|
+
def __init__(self, file, tokenizer="char", seq_len=128, split=None, batch=BATCH_SIZE, stride=None,
|
|
204
|
+
lowercase=False):
|
|
205
|
+
self.file = file
|
|
206
|
+
self.seq_len = positive_int(seq_len, "seq_len")
|
|
207
|
+
self.stride = stride_of(stride, self.seq_len)
|
|
208
|
+
self.lowercase = bool(lowercase)
|
|
209
|
+
self.tokenizer = tokenizer_of(tokenizer, self.lowercase)
|
|
210
|
+
self.split = split_of(split)
|
|
211
|
+
self.batch = batch_of(batch, BATCH_SIZE)
|
|
212
|
+
self.target_names = ["targets"]
|
|
213
|
+
self.spectator_patterns = []
|
|
214
|
+
self.datasets = {}
|
|
215
|
+
self.counts = {}
|
|
216
|
+
self.lines = {}
|
|
217
|
+
self.tokens = {}
|
|
218
|
+
self.read_lines = None
|
|
219
|
+
self.rng = None
|
|
220
|
+
self.directories = ()
|
|
221
|
+
self.loaders_by_set = None
|
|
222
|
+
self.fitted = False
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def vocab_size(self):
|
|
226
|
+
return size_of(self.tokenizer)
|
|
227
|
+
|
|
228
|
+
def cleaned(self, lines):
|
|
229
|
+
kept = [str(line) for line in lines if str(line).strip()]
|
|
230
|
+
return [line.lower() for line in kept] if self.lowercase else kept
|
|
231
|
+
|
|
232
|
+
def read(self, path):
|
|
233
|
+
try:
|
|
234
|
+
text = Path(path).read_text(encoding="utf-8")
|
|
235
|
+
except UnicodeDecodeError as failure:
|
|
236
|
+
raise BuildError(f"{path} is not utf-8 text: {failure}") from failure
|
|
237
|
+
return self.cleaned(text.replace("\r\n", "\n").replace("\r", "\n").split("\n"))
|
|
238
|
+
|
|
239
|
+
def sets_of(self, ctx):
|
|
240
|
+
self.directories = config_directories(ctx)
|
|
241
|
+
lines = self.read(locate(self.file, self.directories))
|
|
242
|
+
if not lines:
|
|
243
|
+
raise BuildError(f"{self.file}: the file has no non empty lines")
|
|
244
|
+
self.read_lines = len(lines)
|
|
245
|
+
frame = pandas.DataFrame({"line": lines, "index": numpy.arange(len(lines))})
|
|
246
|
+
parts = {name: part for name, part in seeded_split(self.split, ctx).split(frame).items() if len(part)}
|
|
247
|
+
if "train" not in parts:
|
|
248
|
+
raise BuildError("the split leaves no train lines")
|
|
249
|
+
return {name: (part.sort_values("index") if "index" in part.columns else part)["line"].tolist()
|
|
250
|
+
for name, part in parts.items()}
|
|
251
|
+
|
|
252
|
+
def encoded(self, lines):
|
|
253
|
+
return id_tensor(self.tokenizer.encode("\n".join(lines)))
|
|
254
|
+
|
|
255
|
+
def materialize(self, sets, ctx):
|
|
256
|
+
self.rng = getattr(ctx, "rng", None)
|
|
257
|
+
self.datasets = {}
|
|
258
|
+
self.counts = {}
|
|
259
|
+
self.lines = {}
|
|
260
|
+
self.tokens = {}
|
|
261
|
+
for name, lines in sets.items():
|
|
262
|
+
ids = self.encoded(lines)
|
|
263
|
+
dataset = TextDataset(ids, self.seq_len, self.stride)
|
|
264
|
+
self.lines[name] = len(lines)
|
|
265
|
+
self.tokens[name] = int(len(ids))
|
|
266
|
+
if not len(dataset):
|
|
267
|
+
message = f"the {name} text has {len(ids)} tokens, fewer than seq_len + 1 = {self.seq_len + 1}"
|
|
268
|
+
if name == "train":
|
|
269
|
+
raise BuildError(f"{message}; lower seq_len or give more text")
|
|
270
|
+
logger.warning(f"{message}; the {name} set is left out")
|
|
271
|
+
continue
|
|
272
|
+
self.datasets[name] = dataset
|
|
273
|
+
self.counts[name] = len(dataset)
|
|
274
|
+
self.loaders_by_set = None
|
|
275
|
+
windows = ", ".join(f"{name} {count}" for name, count in self.counts.items())
|
|
276
|
+
logger.info(f"data: {windows} windows of {self.seq_len} tokens; vocabulary {self.vocab_size}")
|
|
277
|
+
|
|
278
|
+
def fit(self, ctx):
|
|
279
|
+
sets = self.sets_of(ctx)
|
|
280
|
+
fitter = getattr(self.tokenizer, "fit", None)
|
|
281
|
+
if callable(fitter):
|
|
282
|
+
fitter(list(sets["train"]))
|
|
283
|
+
self.fitted = True
|
|
284
|
+
self.materialize(sets, ctx)
|
|
285
|
+
|
|
286
|
+
def rebuild(self, ctx):
|
|
287
|
+
if not self.fitted:
|
|
288
|
+
raise BuildError("the data is not fitted; load(directory) first")
|
|
289
|
+
self.materialize(self.sets_of(ctx), ctx)
|
|
290
|
+
|
|
291
|
+
def lines_of(self, source):
|
|
292
|
+
if isinstance(source, pandas.DataFrame):
|
|
293
|
+
column = next((name for name in TEXT_COLUMNS if name in source.columns), None)
|
|
294
|
+
if column is None:
|
|
295
|
+
raise BuildError(f"loader_for takes a frame with a text column, got the columns "
|
|
296
|
+
f"{list(source.columns)}")
|
|
297
|
+
return self.cleaned(source[column].dropna().astype(str).tolist())
|
|
298
|
+
if isinstance(source, (str, Path)):
|
|
299
|
+
return self.read(locate(source, self.directories))
|
|
300
|
+
if isinstance(source, (list, tuple)) and all(isinstance(item, str) for item in source):
|
|
301
|
+
return self.cleaned(source)
|
|
302
|
+
raise BuildError(f"loader_for takes a list of strings, a frame with a text column or a file path, got "
|
|
303
|
+
f"{type(source).__name__}")
|
|
304
|
+
|
|
305
|
+
def loader_for(self, frame, set_name="test"):
|
|
306
|
+
if not self.fitted:
|
|
307
|
+
raise BuildError("the data is not fitted; call fit(ctx) or load(directory) first")
|
|
308
|
+
dataset = TextDataset(self.encoded(self.lines_of(frame)), self.seq_len, self.stride, short=True)
|
|
309
|
+
if not len(dataset):
|
|
310
|
+
raise BuildError(f"loader_for: the {set_name} text has fewer than two tokens")
|
|
311
|
+
return self.make_loader(dataset, train=False)
|
|
312
|
+
|
|
313
|
+
def spectators(self, set_name):
|
|
314
|
+
return None
|
|
315
|
+
|
|
316
|
+
def decode(self, name, values):
|
|
317
|
+
if name not in TARGETS:
|
|
318
|
+
raise BuildError(f"unknown target {name!r}; the targets are {self.target_names}")
|
|
319
|
+
if isinstance(values, torch.Tensor):
|
|
320
|
+
values = values.detach().cpu().numpy()
|
|
321
|
+
array = numpy.asarray(values)
|
|
322
|
+
if array.dtype.kind == "f" and array.ndim >= 2 and array.shape[-1] == self.vocab_size:
|
|
323
|
+
array = array.argmax(axis=-1)
|
|
324
|
+
if array.ndim <= 1:
|
|
325
|
+
return self.tokenizer.decode(id_list(array))
|
|
326
|
+
rows = array.reshape(int(numpy.prod(array.shape[:-1])), array.shape[-1])
|
|
327
|
+
return [self.tokenizer.decode(id_list(row)) for row in rows]
|
|
328
|
+
|
|
329
|
+
def is_label(self, name):
|
|
330
|
+
return name == "targets"
|
|
331
|
+
|
|
332
|
+
def rescales(self, name):
|
|
333
|
+
return False
|
|
334
|
+
|
|
335
|
+
def inverse(self, name, values):
|
|
336
|
+
return values
|
|
337
|
+
|
|
338
|
+
def state(self):
|
|
339
|
+
lowercase = self.lowercase or bool(getattr(self.tokenizer, "lowercase", False))
|
|
340
|
+
common = {"lowercase": lowercase, "seq_len": self.seq_len, "stride": self.stride}
|
|
341
|
+
if isinstance(self.tokenizer, CharTokenizer):
|
|
342
|
+
return {**self.tokenizer.to_json(), **common}
|
|
343
|
+
writer = getattr(self.tokenizer, "to_json", None)
|
|
344
|
+
return {"kind": target_path(type(self.tokenizer)), "state": writer() if callable(writer) else None, **common}
|
|
345
|
+
|
|
346
|
+
def save(self, directory):
|
|
347
|
+
if not self.fitted:
|
|
348
|
+
raise BuildError("the data is not fitted; there is nothing to save")
|
|
349
|
+
root = Path(directory) / "fitted"
|
|
350
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
351
|
+
state = self.state()
|
|
352
|
+
if state["kind"] != "char" and state["state"] is None:
|
|
353
|
+
joblib.dump(self.tokenizer, root / "tokenizer.joblib")
|
|
354
|
+
(root / "tokenizer.json").write_text(json.dumps(state, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
355
|
+
|
|
356
|
+
def restored(self, data, root):
|
|
357
|
+
kind = data.get("kind", "char")
|
|
358
|
+
if kind == "char":
|
|
359
|
+
return CharTokenizer.from_json(data)
|
|
360
|
+
current = self.tokenizer
|
|
361
|
+
if data.get("state") is not None:
|
|
362
|
+
source = type(current) if target_path(type(current)) == kind else target_of(kind)
|
|
363
|
+
reader = getattr(source, "from_json", None)
|
|
364
|
+
if not callable(reader):
|
|
365
|
+
raise BuildError(f"{root}: the tokenizer {kind} has no from_json to read its saved state")
|
|
366
|
+
return reader(data["state"])
|
|
367
|
+
if (root / "tokenizer.joblib").exists():
|
|
368
|
+
return joblib.load(root / "tokenizer.joblib")
|
|
369
|
+
if target_path(type(current)) == kind:
|
|
370
|
+
return current
|
|
371
|
+
raise BuildError(f"{root}: the tokenizer {kind} cannot be restored")
|
|
372
|
+
|
|
373
|
+
def load(self, directory):
|
|
374
|
+
root = Path(directory) / "fitted"
|
|
375
|
+
try:
|
|
376
|
+
data = json.loads((root / "tokenizer.json").read_text(encoding="utf-8"))
|
|
377
|
+
except FileNotFoundError as failure:
|
|
378
|
+
raise BuildError(f"{root} lacks tokenizer.json; save(directory) writes the fitted files") from failure
|
|
379
|
+
self.tokenizer = self.restored(data, root)
|
|
380
|
+
self.seq_len = positive_int(data.get("seq_len", self.seq_len), "seq_len")
|
|
381
|
+
self.stride = stride_of(data.get("stride"), self.seq_len)
|
|
382
|
+
self.lowercase = bool(data.get("lowercase", self.lowercase))
|
|
383
|
+
self.datasets = {}
|
|
384
|
+
self.counts = {}
|
|
385
|
+
self.lines = {}
|
|
386
|
+
self.tokens = {}
|
|
387
|
+
self.read_lines = None
|
|
388
|
+
self.loaders_by_set = None
|
|
389
|
+
self.fitted = True
|
|
390
|
+
return self
|
|
391
|
+
|
|
392
|
+
def report(self):
|
|
393
|
+
kind = "char" if isinstance(self.tokenizer, CharTokenizer) else target_path(type(self.tokenizer))
|
|
394
|
+
return {"rows": dict(self.counts), "lines": dict(self.lines), "tokens": dict(self.tokens),
|
|
395
|
+
"vocab_size": self.vocab_size, "seq_len": self.seq_len, "stride": self.stride, "tokenizer": kind}
|
|
396
|
+
|
|
397
|
+
def set_text(self, name):
|
|
398
|
+
if name not in self.lines:
|
|
399
|
+
return ""
|
|
400
|
+
return f"{flow.amount(self.lines[name])} lines, {flow.amount(self.tokens.get(name))} tokens"
|
|
401
|
+
|
|
402
|
+
def pipeline(self):
|
|
403
|
+
graph = flow.Flow()
|
|
404
|
+
lines = None if self.read_lines is None else f"{flow.amount(self.read_lines)} lines"
|
|
405
|
+
source = flow.source_node(graph, self.file, self.read_lines, [lines])
|
|
406
|
+
split = flow.split_node(graph, source, self.split)
|
|
407
|
+
details = {name: ", ".join(part for part in (self.set_text(name), f"{flow.amount(count)} windows") if part)
|
|
408
|
+
for name, count in self.counts.items()}
|
|
409
|
+
made = flow.set_nodes(graph, split, self.counts, details, flow.shares_of(self.split))
|
|
410
|
+
for name in self.lines:
|
|
411
|
+
if name not in self.counts:
|
|
412
|
+
graph.step(split, f"drop:{name}", "drop", f"{name} left out",
|
|
413
|
+
f"{self.set_text(name)}, fewer than seq_len + 1 = {self.seq_len + 1}")
|
|
414
|
+
kind = self.report()["tokenizer"]
|
|
415
|
+
lowercase = self.lowercase or bool(getattr(self.tokenizer, "lowercase", False))
|
|
416
|
+
tokenizer = graph.node("tokenizer", "tokenizer", "tokenizer", kind + (", lowercase" if lowercase else ""),
|
|
417
|
+
self.vocab_size)
|
|
418
|
+
for name, key in made.items():
|
|
419
|
+
graph.edge(key, tokenizer, "fit" if name == "train" else "")
|
|
420
|
+
if not made:
|
|
421
|
+
graph.edge(split, tokenizer)
|
|
422
|
+
windows = graph.step(tokenizer, "windows", "windows", "windows",
|
|
423
|
+
f"seq_len {self.seq_len}, stride {self.stride}",
|
|
424
|
+
sum(self.counts.values()) if self.counts else None)
|
|
425
|
+
tensors = [graph.step(windows, "tensor:input_ids", "tensor", "input_ids", f"int64, {self.seq_len} token ids"),
|
|
426
|
+
graph.step(windows, "tensor:targets", "tensor", "targets",
|
|
427
|
+
f"int64, the next {self.seq_len} token ids")]
|
|
428
|
+
flow.batch_node(graph, tensors, self.batch)
|
|
429
|
+
return graph.result()
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
register("text_lines", "kalfa.std.data.text.TextLines", slot="data",
|
|
433
|
+
doc="a text file read line by line: a tokenizer fitted on the train lines, sequences of seq_len tokens; the "
|
|
434
|
+
"batch carries input_ids and targets (the next tokens)")
|
|
435
|
+
register("char_tokenizer", "kalfa.std.data.text.CharTokenizer", slot="value",
|
|
436
|
+
doc="one token per character, vocabulary from the train lines")
|