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
@@ -0,0 +1,327 @@
1
+ import inspect
2
+
3
+ import numpy
4
+ import torch
5
+ import torch.nn.functional as functional
6
+ from PIL import Image
7
+
8
+ from kalfa.core import aliases
9
+ from kalfa.core.aliases import register
10
+ from kalfa.core.errors import BuildError, RunError
11
+ from kalfa.std.data.columns import target_of
12
+
13
+
14
+ PRESETS = {
15
+ "imagenet": ([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
16
+ "cifar10": ([0.4914, 0.4822, 0.4465], [0.2470, 0.2435, 0.2616]),
17
+ "mnist": ([0.1307], [0.3081]),
18
+ }
19
+
20
+ GRAY_WEIGHTS = (0.299, 0.587, 0.114)
21
+ PIL_MODES = ("1", "L", "LA", "RGB", "RGBA", "I", "I;16", "F")
22
+
23
+
24
+ def built(target, params, where):
25
+ try:
26
+ made = target(**params)
27
+ except BuildError:
28
+ raise
29
+ except Exception as failure:
30
+ raise BuildError(f"{where}: {aliases.target_path(target)} could not be built: {type(failure).__name__}: "
31
+ f"{failure}") from failure
32
+ if not callable(made):
33
+ raise BuildError(f"{where}: {aliases.target_path(target)} gave a {type(made).__name__}, which is not a "
34
+ f"callable image -> image")
35
+ return made
36
+
37
+
38
+ def transform_of(spec, where):
39
+ if isinstance(spec, str):
40
+ target = target_of(spec, where)
41
+ if inspect.isclass(target):
42
+ return built(target, {}, where)
43
+ if callable(target):
44
+ return target
45
+ raise BuildError(f"{where}: {spec!r} is a {type(target).__name__}, not a transform")
46
+ if isinstance(spec, dict):
47
+ if "use" not in spec:
48
+ raise BuildError(f"{where}: a transform call needs use, got the keys {sorted(spec)}")
49
+ params = {key: value for key, value in spec.items() if key != "use"}
50
+ return built(target_of(spec["use"], where), params, where)
51
+ if inspect.isclass(spec):
52
+ return built(spec, {}, where)
53
+ if callable(spec):
54
+ return spec
55
+ raise BuildError(f"{where}: a transform is a short name, an import path, a call or a callable object, got "
56
+ f"{type(spec).__name__}")
57
+
58
+
59
+ def name_of(spec):
60
+ if isinstance(spec, str):
61
+ return spec
62
+ if isinstance(spec, dict):
63
+ return str(spec.get("use"))
64
+ path = aliases.target_path(spec if inspect.isclass(spec) or inspect.isroutine(spec) else type(spec))
65
+ for name, entry in aliases.ALIASES.items():
66
+ if entry.slot == "value" and aliases.target_path(entry.target) == path:
67
+ return name
68
+ return path
69
+
70
+
71
+ class Pipeline:
72
+ def __init__(self, steps=(), names=()):
73
+ self.steps = list(steps)
74
+ self.names = list(names)
75
+
76
+ def __call__(self, image):
77
+ for step in self.steps:
78
+ image = step(image)
79
+ return image
80
+
81
+
82
+ def pipeline_of(specs, where):
83
+ if specs is None:
84
+ return Pipeline()
85
+ if isinstance(specs, Pipeline):
86
+ return specs
87
+ items = list(specs) if isinstance(specs, (list, tuple)) else [specs]
88
+ steps = [transform_of(item, f"{where}[{position}]") for position, item in enumerate(items)]
89
+ return Pipeline(steps, [name_of(item) for item in items])
90
+
91
+
92
+ def pil_array(image):
93
+ if image.mode not in PIL_MODES:
94
+ image = image.convert("RGB")
95
+ return numpy.array(image)
96
+
97
+
98
+ def array_tensor(array):
99
+ if array.dtype == numpy.uint8:
100
+ values = array.astype(numpy.float32) / 255.0
101
+ elif array.dtype == numpy.uint16:
102
+ values = array.astype(numpy.float32) / 65535.0
103
+ else:
104
+ values = array.astype(numpy.float32)
105
+ if values.ndim == 2:
106
+ values = values[None]
107
+ elif values.ndim == 3:
108
+ values = values.transpose(2, 0, 1)
109
+ else:
110
+ raise RunError(f"to_tensor takes an array of shape (H, W) or (H, W, C), got {array.shape}")
111
+ return torch.from_numpy(numpy.ascontiguousarray(values, dtype=numpy.float32))
112
+
113
+
114
+ class ToTensor:
115
+ def __call__(self, image):
116
+ if isinstance(image, torch.Tensor):
117
+ return image
118
+ if isinstance(image, Image.Image):
119
+ return array_tensor(pil_array(image))
120
+ if isinstance(image, numpy.ndarray):
121
+ return array_tensor(image)
122
+ raise RunError(f"to_tensor takes a PIL image, an array or a tensor, got {type(image).__name__}")
123
+
124
+
125
+ def image_tensor(value):
126
+ tensor = value if isinstance(value, torch.Tensor) else ToTensor()(value)
127
+ if tensor.dtype == torch.uint8:
128
+ tensor = tensor.to(torch.float32) / 255.0
129
+ elif tensor.dtype != torch.float32:
130
+ tensor = tensor.to(torch.float32)
131
+ if tensor.ndim == 2:
132
+ tensor = tensor.unsqueeze(0)
133
+ if tensor.ndim < 3:
134
+ raise RunError(f"an image tensor is (C, H, W), got the shape {tuple(tensor.shape)}")
135
+ return tensor
136
+
137
+
138
+ def preset_of(name):
139
+ key = str(name).lower()
140
+ if key not in PRESETS:
141
+ raise BuildError(f"normalize: unknown preset {name!r}; the presets are {sorted(PRESETS)}")
142
+ return PRESETS[key]
143
+
144
+
145
+ def values_of(value, what):
146
+ if isinstance(value, torch.Tensor):
147
+ value = value.flatten().tolist()
148
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
149
+ return [float(value)]
150
+ if (isinstance(value, (list, tuple)) and value
151
+ and all(isinstance(item, (int, float)) and not isinstance(item, bool) for item in value)):
152
+ return [float(item) for item in value]
153
+ raise BuildError(f"normalize: {what} is a number, a list of numbers or a preset name ({', '.join(PRESETS)}), "
154
+ f"got {value!r}")
155
+
156
+
157
+ def per_channel(values, channels, what):
158
+ if len(values) == channels:
159
+ return values
160
+ if len(values) == 1:
161
+ return values * channels
162
+ raise RunError(f"normalize: {len(values)} {what} values for an image of {channels} channels")
163
+
164
+
165
+ class Normalize:
166
+ def __init__(self, mean=0.5, std=None):
167
+ if isinstance(mean, str):
168
+ mean, preset_std = preset_of(mean)
169
+ if std is None:
170
+ std = preset_std
171
+ if std is None:
172
+ std = 0.5
173
+ if isinstance(std, str):
174
+ std = preset_of(std)[1]
175
+ self.mean = values_of(mean, "mean")
176
+ self.std = values_of(std, "std")
177
+ if any(value <= 0.0 for value in self.std):
178
+ raise BuildError(f"normalize: std must be positive, got {self.std}")
179
+ if len(self.mean) != len(self.std) and 1 not in (len(self.mean), len(self.std)):
180
+ raise BuildError(f"normalize: mean has {len(self.mean)} values and std {len(self.std)}")
181
+
182
+ def __call__(self, image):
183
+ floating = isinstance(image, torch.Tensor) and image.is_floating_point() and image.ndim >= 3
184
+ tensor = image if floating else image_tensor(image)
185
+ channels = int(tensor.shape[-3])
186
+ shape = (channels, 1, 1)
187
+ mean = torch.tensor(per_channel(self.mean, channels, "mean"), dtype=tensor.dtype, device=tensor.device)
188
+ std = torch.tensor(per_channel(self.std, channels, "std"), dtype=tensor.dtype, device=tensor.device)
189
+ return (tensor - mean.view(shape)) / std.view(shape)
190
+
191
+
192
+ class TwoViews:
193
+ def __init__(self, each):
194
+ self.each = pipeline_of(each, "two_views.each")
195
+
196
+ def __call__(self, image):
197
+ return torch.stack([image_tensor(self.each(image)), image_tensor(self.each(image))])
198
+
199
+
200
+ def size_of(size, what):
201
+ if isinstance(size, int) and not isinstance(size, bool) and size > 0:
202
+ return size, size
203
+ if (isinstance(size, (list, tuple)) and len(size) == 2
204
+ and all(isinstance(item, int) and not isinstance(item, bool) and item > 0 for item in size)):
205
+ return int(size[0]), int(size[1])
206
+ raise BuildError(f"{what}: size is a positive integer or [height, width], got {size!r}")
207
+
208
+
209
+ def extent(image):
210
+ if isinstance(image, Image.Image):
211
+ return image.height, image.width
212
+ return int(image.shape[-2]), int(image.shape[-1])
213
+
214
+
215
+ def crop(image, top, left, height, width):
216
+ if isinstance(image, Image.Image):
217
+ return image.crop((left, top, left + width, top + height))
218
+ out = image.new_zeros((*image.shape[:-2], height, width))
219
+ full_height, full_width = extent(image)
220
+ first_row, first_column = max(top, 0), max(left, 0)
221
+ last_row, last_column = min(top + height, full_height), min(left + width, full_width)
222
+ if last_row > first_row and last_column > first_column:
223
+ piece = image[..., first_row:last_row, first_column:last_column]
224
+ out[..., first_row - top:last_row - top, first_column - left:last_column - left] = piece
225
+ return out
226
+
227
+
228
+ def planar(image):
229
+ return image if isinstance(image, Image.Image) else image_tensor(image)
230
+
231
+
232
+ class Resize:
233
+ def __init__(self, size):
234
+ self.size = size_of(size, "resize")
235
+
236
+ def __call__(self, image):
237
+ height, width = self.size
238
+ image = planar(image)
239
+ if isinstance(image, Image.Image):
240
+ return image.resize((width, height), Image.Resampling.BILINEAR)
241
+ batched = image if image.ndim == 4 else image.unsqueeze(0)
242
+ out = functional.interpolate(batched, size=(height, width), mode="bilinear", align_corners=False,
243
+ antialias=True)
244
+ return out if image.ndim == 4 else out.squeeze(0)
245
+
246
+
247
+ class CenterCrop:
248
+ def __init__(self, size):
249
+ self.size = size_of(size, "center_crop")
250
+
251
+ def __call__(self, image):
252
+ height, width = self.size
253
+ image = planar(image)
254
+ full_height, full_width = extent(image)
255
+ top = int(round((full_height - height) / 2.0))
256
+ left = int(round((full_width - width) / 2.0))
257
+ return crop(image, top, left, height, width)
258
+
259
+
260
+ def offset(span):
261
+ if span <= 0:
262
+ return int(round(span / 2.0))
263
+ return int(torch.randint(0, span + 1, (1,)).item())
264
+
265
+
266
+ class RandomCrop:
267
+ def __init__(self, size, padding=0):
268
+ self.size = size_of(size, "random_crop")
269
+ if isinstance(padding, bool) or not isinstance(padding, int) or padding < 0:
270
+ raise BuildError(f"random_crop: padding is a non negative integer, got {padding!r}")
271
+ self.padding = padding
272
+
273
+ def __call__(self, image):
274
+ height, width = self.size
275
+ image = planar(image)
276
+ full_height, full_width = extent(image)
277
+ top = offset(full_height + 2 * self.padding - height) - self.padding
278
+ left = offset(full_width + 2 * self.padding - width) - self.padding
279
+ return crop(image, top, left, height, width)
280
+
281
+
282
+ class RandomFlip:
283
+ def __init__(self, p=0.5):
284
+ if isinstance(p, bool) or not isinstance(p, (int, float)) or not 0.0 <= float(p) <= 1.0:
285
+ raise BuildError(f"random_flip: p is a probability in [0, 1], got {p!r}")
286
+ self.p = float(p)
287
+
288
+ def __call__(self, image):
289
+ image = planar(image)
290
+ if float(torch.rand(())) >= self.p:
291
+ return image
292
+ if isinstance(image, Image.Image):
293
+ return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
294
+ return torch.flip(image, dims=(-1,))
295
+
296
+
297
+ class Grayscale:
298
+ def __call__(self, image):
299
+ image = planar(image)
300
+ if isinstance(image, Image.Image):
301
+ return image if image.mode == "L" else image.convert("L")
302
+ channels = int(image.shape[-3])
303
+ if channels == 1:
304
+ return image
305
+ if channels in (3, 4):
306
+ weights = image.new_tensor(GRAY_WEIGHTS).view(3, 1, 1)
307
+ return (image[..., :3, :, :] * weights).sum(dim=-3, keepdim=True)
308
+ raise RunError(f"grayscale takes an image of 1, 3 or 4 channels, got {channels}")
309
+
310
+
311
+ REGISTRATIONS = (
312
+ ("to_tensor", "kalfa.std.data.transforms.ToTensor",
313
+ "a PIL image or an array becomes a float tensor (C, H, W) in [0, 1]; a tensor passes through"),
314
+ ("normalize", "kalfa.std.data.transforms.Normalize",
315
+ "(x - mean) / std per channel; mean and std are numbers, lists or a preset name: imagenet, cifar10, mnist"),
316
+ ("two_views", "kalfa.std.data.transforms.TwoViews",
317
+ "applies the each pipeline twice with fresh randomness and stacks the two results into (2, C, H, W)"),
318
+ ("resize", "kalfa.std.data.transforms.Resize",
319
+ "resize a PIL image or a tensor to size (an int or [height, width])"),
320
+ ("center_crop", "kalfa.std.data.transforms.CenterCrop", "crop the center of a PIL image or a tensor to size"),
321
+ ("random_crop", "kalfa.std.data.transforms.RandomCrop", "crop a random window of size after padding"),
322
+ ("random_flip", "kalfa.std.data.transforms.RandomFlip", "flip an image horizontally with probability p"),
323
+ ("grayscale", "kalfa.std.data.transforms.Grayscale", "an image in one channel"),
324
+ )
325
+
326
+ for name, target, doc in REGISTRATIONS:
327
+ register(name, target, slot="value", doc=doc)
@@ -0,0 +1,113 @@
1
+ import math
2
+ from dataclasses import dataclass
3
+
4
+ import numpy
5
+
6
+ from kalfa.core.errors import BuildError
7
+
8
+
9
+ SHAPE = ("weights maps a batch field name to a weight column or to a mapping of pandas conditions to weights, such as "
10
+ "{w: {\"price < 5\": 5}}")
11
+
12
+
13
+ @dataclass
14
+ class Weight:
15
+ name: str
16
+ column: str | None = None
17
+ rules: tuple = ()
18
+
19
+ def source(self):
20
+ if self.column is not None:
21
+ return self.column
22
+ return {text: value for text, value in self.rules}
23
+
24
+ def detail(self):
25
+ if self.column is not None:
26
+ return f"column {self.column}"
27
+ return "; ".join([f"{text}: {value:g}" for text, value in self.rules] + ["else 1"])
28
+
29
+ def values(self, frame):
30
+ where = f"weights.{self.name}"
31
+ if self.column is not None:
32
+ found = column_values(frame, self.column, where)
33
+ else:
34
+ found = numpy.ones(len(frame), dtype="float64")
35
+ free = numpy.ones(len(frame), dtype=bool)
36
+ for text, value in self.rules:
37
+ hit = mask_of(frame, text, where) & free
38
+ found[hit] = value
39
+ free &= ~hit
40
+ bad = int((~numpy.isfinite(found)).sum())
41
+ if bad:
42
+ raise BuildError(f"{where}: {bad} rows have a weight that is not a finite number")
43
+ return found
44
+
45
+
46
+ def column_values(frame, column, where):
47
+ if column not in frame.columns:
48
+ raise BuildError(f"{where}: the table has no column {column!r}; the columns are {list(frame.columns)}")
49
+ try:
50
+ return numpy.asarray(frame[column].to_numpy(), dtype="float64")
51
+ except (TypeError, ValueError) as error:
52
+ raise BuildError(f"{where}: the column {column!r} is not numeric") from error
53
+
54
+
55
+ def mask_of(frame, text, where):
56
+ try:
57
+ found = frame.eval(text)
58
+ except Exception as error:
59
+ raise BuildError(f"{where}: the condition {text!r} failed: {error}") from error
60
+ mask = numpy.asarray(found)
61
+ if mask.dtype != bool or mask.shape != (len(frame),):
62
+ raise BuildError(f"{where}: the condition {text!r} must give true or false for every row, like 'price < 5'")
63
+ return mask
64
+
65
+
66
+ def number(value):
67
+ return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value)
68
+
69
+
70
+ def rules_of(name, item):
71
+ rules = []
72
+ for text, value in item.items():
73
+ if not isinstance(text, str) or not text.strip():
74
+ raise BuildError(f"weights.{name}: a condition is a pandas expression in quotes such as \"price < 5\", "
75
+ f"got {text!r}")
76
+ if not number(value):
77
+ raise BuildError(f"weights.{name}: the weight of {text!r} must be a finite number, got {value!r}")
78
+ rules.append((text, float(value)))
79
+ return tuple(rules)
80
+
81
+
82
+ def weights_of(spec, targets=None, drop=None):
83
+ if spec is None:
84
+ return []
85
+ if not isinstance(spec, dict):
86
+ raise BuildError(f"{SHAPE}; got {type(spec).__name__}")
87
+ taken = {str(name) for name in targets or ()}
88
+ dropped = {str(name) for name in drop or ()} if isinstance(drop, (list, tuple, set)) else set()
89
+ found = []
90
+ for name, item in spec.items():
91
+ if not isinstance(name, str) or not name:
92
+ raise BuildError(f"weights: a batch field name is text, got {name!r}")
93
+ if name == "x":
94
+ raise BuildError("weights: a weight cannot be named x; x is the feature tensor")
95
+ if name in taken:
96
+ raise BuildError(f"weights: {name!r} is a target; give the weight field its own name")
97
+ if name in dropped:
98
+ raise BuildError(f"weights: {name!r} is in drop; a weight field cannot be dropped")
99
+ if isinstance(item, str) and item:
100
+ found.append(Weight(name, column=item))
101
+ elif isinstance(item, dict) and item:
102
+ found.append(Weight(name, rules=rules_of(name, item)))
103
+ else:
104
+ raise BuildError(f"weights.{name}: {SHAPE}; got {item!r}")
105
+ return found
106
+
107
+
108
+ def scale_of(weight, values):
109
+ mean = float(numpy.mean(values)) if len(values) else math.nan
110
+ if not mean > 0:
111
+ raise BuildError(f"weights.{weight.name}: the train weights have the mean {mean:g}; it must be positive to "
112
+ "scale them to mean one")
113
+ return 1.0 / mean
@@ -0,0 +1,135 @@
1
+ import numpy
2
+ import pandas
3
+ import torch
4
+ from torch.utils.data import Dataset
5
+
6
+ from kalfa.core.aliases import register
7
+ from kalfa.core.errors import BuildError
8
+ from kalfa.std.common import positive_int
9
+
10
+
11
+ EMPTY = numpy.zeros(0, dtype="int64")
12
+
13
+
14
+ def positive(value, what):
15
+ return positive_int(value, f"window: {what}")
16
+
17
+
18
+ def flat(values):
19
+ values = torch.as_tensor(values)
20
+ if values.ndim == 2 and values.shape[1] == 1:
21
+ return values[:, 0]
22
+ return values
23
+
24
+
25
+ def codes_of(groups, earlier, count, before):
26
+ if groups is None:
27
+ return numpy.zeros(count, dtype="int64"), numpy.zeros(before, dtype="int64")
28
+ current = numpy.asarray(groups)
29
+ if len(current) != count:
30
+ raise BuildError(f"window: {len(current)} group values for {count} rows")
31
+ if earlier is None or not before:
32
+ codes, _ = pandas.factorize(current, sort=False, use_na_sentinel=False)
33
+ return codes.astype("int64"), numpy.full(before, -1, dtype="int64")
34
+ earlier = numpy.asarray(earlier)
35
+ if len(earlier) != before:
36
+ raise BuildError(f"window: {len(earlier)} group values for {before} context rows")
37
+ codes, _ = pandas.factorize(numpy.concatenate([earlier, current]), sort=False, use_na_sentinel=False)
38
+ codes = codes.astype("int64")
39
+ return codes[before:], codes[:before]
40
+
41
+
42
+ def members_of(codes):
43
+ if not len(codes):
44
+ return {}
45
+ order = numpy.argsort(codes, kind="stable")
46
+ ordered = codes[order]
47
+ cuts = numpy.flatnonzero(ordered[1:] != ordered[:-1]) + 1
48
+ keys = ordered[numpy.concatenate([[0], cuts]).astype("int64")]
49
+ return dict(zip(keys.tolist(), numpy.split(order.astype("int64"), cuts)))
50
+
51
+
52
+ def gather(values, earlier, layout, before):
53
+ index = torch.from_numpy(layout)
54
+ out = values.new_zeros((len(layout), *values.shape[1:]))
55
+ late = index >= before
56
+ out[late] = values[index[late] - before]
57
+ if earlier is not None and tuple(earlier.shape[1:]) == tuple(values.shape[1:]):
58
+ out[~late] = earlier[index[~late]].to(values.dtype)
59
+ return out
60
+
61
+
62
+ class WindowDataset(Dataset):
63
+ def __init__(self, x, targets, rows, starts, size, horizon):
64
+ self.x = x
65
+ self.targets = targets
66
+ self.rows = rows
67
+ self.starts = starts
68
+ self.size = size
69
+ self.horizon = horizon
70
+
71
+ def __len__(self):
72
+ return len(self.starts)
73
+
74
+ def __getitem__(self, position):
75
+ start = int(self.starts[position])
76
+ end = start + self.size
77
+ item = {"x": self.x[start:end]}
78
+ for name, values in self.targets.items():
79
+ item[name] = values[end:end + self.horizon]
80
+ return item
81
+
82
+
83
+ class Window:
84
+ def __init__(self, size, horizon=1, stride=1, context=True, group=None):
85
+ self.size = positive(size, "size")
86
+ self.horizon = positive(horizon, "horizon")
87
+ self.stride = positive(stride, "stride")
88
+ self.context = bool(context)
89
+ self.group = group
90
+
91
+ def dataset(self, x, targets, rows, groups=None, context=None):
92
+ x = torch.as_tensor(x, dtype=torch.float32)
93
+ count = int(x.shape[0])
94
+ rows = numpy.asarray(rows)
95
+ if len(rows) != count:
96
+ raise BuildError(f"window: {len(rows)} row ids for {count} rows")
97
+ targets = {name: flat(values) for name, values in (targets or {}).items()}
98
+ earlier_x, earlier_targets, earlier_groups = x[:0], {}, None
99
+ if self.context and context is not None:
100
+ earlier_x, earlier_targets, _, earlier_groups = context
101
+ earlier_x = torch.as_tensor(earlier_x, dtype=torch.float32)
102
+ earlier_targets = {name: flat(values) for name, values in (earlier_targets or {}).items()}
103
+ if tuple(earlier_x.shape[1:]) != tuple(x.shape[1:]):
104
+ raise BuildError(f"window: the context rows have the shape {tuple(earlier_x.shape[1:])}, the rows "
105
+ f"{tuple(x.shape[1:])}")
106
+ before = int(earlier_x.shape[0])
107
+ current, previous = codes_of(groups, earlier_groups, count, before)
108
+ tails = members_of(previous)
109
+ pieces, starts, firsts = [], [], []
110
+ length = 0
111
+ for code, members in members_of(current).items():
112
+ tail = tails.get(code, EMPTY)[-self.size:]
113
+ offset = len(tail)
114
+ total = offset + len(members)
115
+ ends = numpy.arange(max(self.size, offset), total - self.horizon + 1, self.stride, dtype="int64")
116
+ if not len(ends):
117
+ continue
118
+ pieces.append(numpy.concatenate([tail, members + before]))
119
+ starts.append(ends - self.size + length)
120
+ firsts.append(members[ends - offset])
121
+ length += total
122
+ layout = numpy.concatenate(pieces) if pieces else EMPTY
123
+ starts = numpy.concatenate(starts) if starts else EMPTY
124
+ firsts = numpy.concatenate(firsts) if firsts else EMPTY
125
+ order = numpy.argsort(firsts, kind="stable")
126
+ windows = gather(x, earlier_x, layout, before)
127
+ horizons = {name: gather(values, earlier_targets.get(name), layout, before)
128
+ for name, values in targets.items()}
129
+ return WindowDataset(windows, horizons, rows[firsts[order]], starts[order], self.size, self.horizon)
130
+
131
+
132
+ register("window", "kalfa.std.data.window.Window", slot="value",
133
+ doc="turns the rows of a table into windows: x of the last size rows, each target over the next horizon "
134
+ "rows; every window keeps to one value of the group column; with context the first windows of a set "
135
+ "reach back into the previous set")
kalfa/std/figure.py ADDED
@@ -0,0 +1,37 @@
1
+ from pathlib import Path
2
+
3
+ import matplotlib
4
+
5
+ from kalfa.core.record import atomic
6
+
7
+
8
+ matplotlib.use("Agg")
9
+
10
+ from matplotlib import pyplot # noqa: E402
11
+
12
+
13
+ PALETTE = ("#2a78d6", "#eb6834", "#1baf7a", "#eda100", "#e87ba4", "#008300", "#4a3aa7", "#e34948")
14
+
15
+
16
+ class Figures:
17
+ colors = PALETTE
18
+ width = 6.4
19
+ height = 4.4
20
+
21
+ def __init__(self, directory, format="png", dpi=150):
22
+ self.directory = directory
23
+ self.format = format
24
+ self.dpi = dpi
25
+
26
+ def new(self, width=None, height=None):
27
+ size = (float(width or self.width), float(height or self.height))
28
+ return pyplot.figure(figsize=size, layout="constrained")
29
+
30
+ def save(self, figure, name):
31
+ path = Path(self.directory) / f"{name}.{self.format}"
32
+ try:
33
+ with atomic(path) as temporary:
34
+ figure.savefig(temporary, format=self.format, dpi=self.dpi)
35
+ finally:
36
+ pyplot.close(figure)
37
+ return path
kalfa/std/init.py ADDED
@@ -0,0 +1,72 @@
1
+ from torch import nn
2
+ from torch.nn.parameter import is_lazy
3
+
4
+ from kalfa.core.aliases import register
5
+
6
+
7
+ LAYERS = (nn.Linear, nn.Conv1d, nn.Conv2d, nn.Conv3d, nn.ConvTranspose1d, nn.ConvTranspose2d, nn.ConvTranspose3d,
8
+ nn.Embedding)
9
+
10
+
11
+ def zero_bias(layer):
12
+ bias = getattr(layer, "bias", None)
13
+ if bias is not None:
14
+ nn.init.zeros_(bias)
15
+
16
+
17
+ class Pending:
18
+ def __init__(self, fill):
19
+ self.fill = fill
20
+ self.done = False
21
+
22
+ def __call__(self, module, inputs):
23
+ if not self.done:
24
+ self.done = True
25
+ self.fill(module)
26
+
27
+
28
+ def initializer(fill):
29
+ def apply(module):
30
+ for layer in module.modules():
31
+ if not isinstance(layer, LAYERS):
32
+ continue
33
+ if any(is_lazy(parameter) for parameter in layer.parameters(recurse=False)):
34
+ layer.register_forward_pre_hook(Pending(fill))
35
+ else:
36
+ fill(layer)
37
+
38
+ return apply
39
+
40
+
41
+ def normal_init(std=0.02, mean=0.0):
42
+ def fill(layer):
43
+ nn.init.normal_(layer.weight, mean=float(mean), std=float(std))
44
+ zero_bias(layer)
45
+
46
+ return initializer(fill)
47
+
48
+
49
+ def xavier_init(gain=1.0):
50
+ def fill(layer):
51
+ nn.init.xavier_uniform_(layer.weight, gain=float(gain))
52
+ zero_bias(layer)
53
+
54
+ return initializer(fill)
55
+
56
+
57
+ INITS = (
58
+ ("normal_init", "kalfa.std.init.normal_init", "draws linear, convolution and embedding weights from "
59
+ "normal(mean, std) and zeroes biases; lazy layers are filled on "
60
+ "their first forward"),
61
+ ("xavier_init", "kalfa.std.init.xavier_init", "xavier uniform weights for linear, convolution and embedding "
62
+ "layers, zero biases; lazy layers are filled on their first "
63
+ "forward"),
64
+ )
65
+
66
+
67
+ def register_names():
68
+ for name, target, doc in INITS:
69
+ register(name, target, slot="value", doc=doc)
70
+
71
+
72
+ register_names()