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.
Files changed (148) hide show
  1. kalfa/__init__.py +19 -0
  2. kalfa/api.py +284 -0
  3. kalfa/board/__init__.py +1 -0
  4. kalfa/board/answers.py +419 -0
  5. kalfa/board/server.py +536 -0
  6. kalfa/board/static/api.js +176 -0
  7. kalfa/board/static/app.js +122 -0
  8. kalfa/board/static/board.css +228 -0
  9. kalfa/board/static/charts.css +46 -0
  10. kalfa/board/static/components/badge.js +14 -0
  11. kalfa/board/static/components/chart.js +204 -0
  12. kalfa/board/static/components/diagram.js +857 -0
  13. kalfa/board/static/components/error.js +17 -0
  14. kalfa/board/static/components/loading.js +7 -0
  15. kalfa/board/static/components/progress.js +25 -0
  16. kalfa/board/static/components/settings.js +418 -0
  17. kalfa/board/static/components/spark.js +43 -0
  18. kalfa/board/static/components/table.js +156 -0
  19. kalfa/board/static/diagram.css +76 -0
  20. kalfa/board/static/format.js +138 -0
  21. kalfa/board/static/index.html +34 -0
  22. kalfa/board/static/plot.js +594 -0
  23. kalfa/board/static/predictions.css +20 -0
  24. kalfa/board/static/store.js +215 -0
  25. kalfa/board/static/views/compare.js +185 -0
  26. kalfa/board/static/views/home.js +90 -0
  27. kalfa/board/static/views/record/config.js +81 -0
  28. kalfa/board/static/views/record/curves.js +137 -0
  29. kalfa/board/static/views/record/data.js +378 -0
  30. kalfa/board/static/views/record/files.js +128 -0
  31. kalfa/board/static/views/record/log.js +164 -0
  32. kalfa/board/static/views/record/model.js +286 -0
  33. kalfa/board/static/views/record/overview.js +216 -0
  34. kalfa/board/static/views/record/plots.js +103 -0
  35. kalfa/board/static/views/record/predictions.js +834 -0
  36. kalfa/board/static/views/record.js +179 -0
  37. kalfa/board/static/views/runs.js +124 -0
  38. kalfa/board/static/views/sweep.js +282 -0
  39. kalfa/board/watch.py +87 -0
  40. kalfa/cli.py +371 -0
  41. kalfa/commands/__init__.py +0 -0
  42. kalfa/commands/common.py +8 -0
  43. kalfa/commands/export.py +117 -0
  44. kalfa/commands/generate.py +31 -0
  45. kalfa/commands/guide/__init__.py +101 -0
  46. kalfa/commands/guide/about.py +93 -0
  47. kalfa/commands/guide/calls.py +102 -0
  48. kalfa/commands/guide/sections.py +186 -0
  49. kalfa/commands/guide/seen.py +307 -0
  50. kalfa/commands/guide/show.py +106 -0
  51. kalfa/commands/guide/training.py +333 -0
  52. kalfa/commands/guide/words.py +97 -0
  53. kalfa/commands/legos.py +508 -0
  54. kalfa/commands/plots.py +27 -0
  55. kalfa/commands/report/__init__.py +64 -0
  56. kalfa/commands/report/page.py +112 -0
  57. kalfa/commands/report/predictions.py +216 -0
  58. kalfa/commands/report/sections.py +356 -0
  59. kalfa/commands/stop.py +60 -0
  60. kalfa/commands/sweep/__init__.py +202 -0
  61. kalfa/commands/sweep/checks.py +98 -0
  62. kalfa/commands/sweep/plan.py +183 -0
  63. kalfa/commands/sweep/points.py +123 -0
  64. kalfa/commands/sweep/runner.py +100 -0
  65. kalfa/commands/sweep/space.py +67 -0
  66. kalfa/commands/sweep/strategies.py +228 -0
  67. kalfa/core/__init__.py +0 -0
  68. kalfa/core/aliases.py +229 -0
  69. kalfa/core/build.py +727 -0
  70. kalfa/core/check.py +550 -0
  71. kalfa/core/coerce.py +36 -0
  72. kalfa/core/context.py +149 -0
  73. kalfa/core/device.py +63 -0
  74. kalfa/core/errors.py +45 -0
  75. kalfa/core/graph.py +304 -0
  76. kalfa/core/history.py +70 -0
  77. kalfa/core/loop.py +710 -0
  78. kalfa/core/make.py +121 -0
  79. kalfa/core/plan.py +156 -0
  80. kalfa/core/predict.py +290 -0
  81. kalfa/core/protocols.py +122 -0
  82. kalfa/core/record.py +347 -0
  83. kalfa/core/refs.py +88 -0
  84. kalfa/core/rng.py +100 -0
  85. kalfa/core/rules.py +513 -0
  86. kalfa/core/run.py +568 -0
  87. kalfa/core/spec.py +413 -0
  88. kalfa/core/state.py +100 -0
  89. kalfa/core/wrap.py +671 -0
  90. kalfa/records/__init__.py +24 -0
  91. kalfa/records/compare.py +86 -0
  92. kalfa/records/reader.py +561 -0
  93. kalfa/records/stats/__init__.py +36 -0
  94. kalfa/records/stats/choices.py +282 -0
  95. kalfa/records/stats/classes.py +236 -0
  96. kalfa/records/stats/measures.py +144 -0
  97. kalfa/records/stats/pairs.py +225 -0
  98. kalfa/records/stats/tables.py +153 -0
  99. kalfa/records/sweep.py +244 -0
  100. kalfa/std/__init__.py +17 -0
  101. kalfa/std/actions.py +35 -0
  102. kalfa/std/common.py +56 -0
  103. kalfa/std/data/__init__.py +1 -0
  104. kalfa/std/data/base.py +45 -0
  105. kalfa/std/data/columns.py +190 -0
  106. kalfa/std/data/flow.py +317 -0
  107. kalfa/std/data/images.py +443 -0
  108. kalfa/std/data/options.py +147 -0
  109. kalfa/std/data/pre.py +84 -0
  110. kalfa/std/data/splits.py +140 -0
  111. kalfa/std/data/stream.py +294 -0
  112. kalfa/std/data/table.py +642 -0
  113. kalfa/std/data/text.py +436 -0
  114. kalfa/std/data/transforms.py +327 -0
  115. kalfa/std/data/weights.py +113 -0
  116. kalfa/std/data/window.py +135 -0
  117. kalfa/std/figure.py +37 -0
  118. kalfa/std/init.py +72 -0
  119. kalfa/std/loggers.py +105 -0
  120. kalfa/std/losses.py +161 -0
  121. kalfa/std/metrics.py +463 -0
  122. kalfa/std/models.py +122 -0
  123. kalfa/std/notes.py +1553 -0
  124. kalfa/std/objectives.py +360 -0
  125. kalfa/std/plots/__init__.py +75 -0
  126. kalfa/std/plots/classification.py +207 -0
  127. kalfa/std/plots/common.py +44 -0
  128. kalfa/std/plots/curves.py +56 -0
  129. kalfa/std/plots/images.py +188 -0
  130. kalfa/std/plots/pairs.py +66 -0
  131. kalfa/std/plots/predictions.py +154 -0
  132. kalfa/std/samplers.py +261 -0
  133. kalfa/std/samples.py +72 -0
  134. kalfa/std/schedules.py +74 -0
  135. kalfa/std/steps.py +133 -0
  136. kalfa/std/triggers.py +135 -0
  137. kalfa/summary.py +296 -0
  138. kalfa/tui/__init__.py +4 -0
  139. kalfa/tui/app.py +107 -0
  140. kalfa/tui/config.py +61 -0
  141. kalfa/tui/guide.py +100 -0
  142. kalfa/tui/jobs.py +97 -0
  143. kalfa/tui/legos.py +62 -0
  144. kalfa/tui/records.py +439 -0
  145. kalfa-3.0.0.dev0.dist-info/METADATA +261 -0
  146. kalfa-3.0.0.dev0.dist-info/RECORD +148 -0
  147. kalfa-3.0.0.dev0.dist-info/WHEEL +4 -0
  148. 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")