kalfa 3.0.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kalfa/__init__.py +19 -0
- kalfa/api.py +284 -0
- kalfa/board/__init__.py +1 -0
- kalfa/board/answers.py +419 -0
- kalfa/board/server.py +536 -0
- kalfa/board/static/api.js +176 -0
- kalfa/board/static/app.js +122 -0
- kalfa/board/static/board.css +228 -0
- kalfa/board/static/charts.css +46 -0
- kalfa/board/static/components/badge.js +14 -0
- kalfa/board/static/components/chart.js +204 -0
- kalfa/board/static/components/diagram.js +857 -0
- kalfa/board/static/components/error.js +17 -0
- kalfa/board/static/components/loading.js +7 -0
- kalfa/board/static/components/progress.js +25 -0
- kalfa/board/static/components/settings.js +418 -0
- kalfa/board/static/components/spark.js +43 -0
- kalfa/board/static/components/table.js +156 -0
- kalfa/board/static/diagram.css +76 -0
- kalfa/board/static/format.js +138 -0
- kalfa/board/static/index.html +34 -0
- kalfa/board/static/plot.js +594 -0
- kalfa/board/static/predictions.css +20 -0
- kalfa/board/static/store.js +215 -0
- kalfa/board/static/views/compare.js +185 -0
- kalfa/board/static/views/home.js +90 -0
- kalfa/board/static/views/record/config.js +81 -0
- kalfa/board/static/views/record/curves.js +137 -0
- kalfa/board/static/views/record/data.js +378 -0
- kalfa/board/static/views/record/files.js +128 -0
- kalfa/board/static/views/record/log.js +164 -0
- kalfa/board/static/views/record/model.js +286 -0
- kalfa/board/static/views/record/overview.js +216 -0
- kalfa/board/static/views/record/plots.js +103 -0
- kalfa/board/static/views/record/predictions.js +834 -0
- kalfa/board/static/views/record.js +179 -0
- kalfa/board/static/views/runs.js +124 -0
- kalfa/board/static/views/sweep.js +282 -0
- kalfa/board/watch.py +87 -0
- kalfa/cli.py +371 -0
- kalfa/commands/__init__.py +0 -0
- kalfa/commands/common.py +8 -0
- kalfa/commands/export.py +117 -0
- kalfa/commands/generate.py +31 -0
- kalfa/commands/guide/__init__.py +101 -0
- kalfa/commands/guide/about.py +93 -0
- kalfa/commands/guide/calls.py +102 -0
- kalfa/commands/guide/sections.py +186 -0
- kalfa/commands/guide/seen.py +307 -0
- kalfa/commands/guide/show.py +106 -0
- kalfa/commands/guide/training.py +333 -0
- kalfa/commands/guide/words.py +97 -0
- kalfa/commands/legos.py +508 -0
- kalfa/commands/plots.py +27 -0
- kalfa/commands/report/__init__.py +64 -0
- kalfa/commands/report/page.py +112 -0
- kalfa/commands/report/predictions.py +216 -0
- kalfa/commands/report/sections.py +356 -0
- kalfa/commands/stop.py +60 -0
- kalfa/commands/sweep/__init__.py +202 -0
- kalfa/commands/sweep/checks.py +98 -0
- kalfa/commands/sweep/plan.py +183 -0
- kalfa/commands/sweep/points.py +123 -0
- kalfa/commands/sweep/runner.py +100 -0
- kalfa/commands/sweep/space.py +67 -0
- kalfa/commands/sweep/strategies.py +228 -0
- kalfa/core/__init__.py +0 -0
- kalfa/core/aliases.py +229 -0
- kalfa/core/build.py +727 -0
- kalfa/core/check.py +550 -0
- kalfa/core/coerce.py +36 -0
- kalfa/core/context.py +149 -0
- kalfa/core/device.py +63 -0
- kalfa/core/errors.py +45 -0
- kalfa/core/graph.py +304 -0
- kalfa/core/history.py +70 -0
- kalfa/core/loop.py +710 -0
- kalfa/core/make.py +121 -0
- kalfa/core/plan.py +156 -0
- kalfa/core/predict.py +290 -0
- kalfa/core/protocols.py +122 -0
- kalfa/core/record.py +347 -0
- kalfa/core/refs.py +88 -0
- kalfa/core/rng.py +100 -0
- kalfa/core/rules.py +513 -0
- kalfa/core/run.py +568 -0
- kalfa/core/spec.py +413 -0
- kalfa/core/state.py +100 -0
- kalfa/core/wrap.py +671 -0
- kalfa/records/__init__.py +24 -0
- kalfa/records/compare.py +86 -0
- kalfa/records/reader.py +561 -0
- kalfa/records/stats/__init__.py +36 -0
- kalfa/records/stats/choices.py +282 -0
- kalfa/records/stats/classes.py +236 -0
- kalfa/records/stats/measures.py +144 -0
- kalfa/records/stats/pairs.py +225 -0
- kalfa/records/stats/tables.py +153 -0
- kalfa/records/sweep.py +244 -0
- kalfa/std/__init__.py +17 -0
- kalfa/std/actions.py +35 -0
- kalfa/std/common.py +56 -0
- kalfa/std/data/__init__.py +1 -0
- kalfa/std/data/base.py +45 -0
- kalfa/std/data/columns.py +190 -0
- kalfa/std/data/flow.py +317 -0
- kalfa/std/data/images.py +443 -0
- kalfa/std/data/options.py +147 -0
- kalfa/std/data/pre.py +84 -0
- kalfa/std/data/splits.py +140 -0
- kalfa/std/data/stream.py +294 -0
- kalfa/std/data/table.py +642 -0
- kalfa/std/data/text.py +436 -0
- kalfa/std/data/transforms.py +327 -0
- kalfa/std/data/weights.py +113 -0
- kalfa/std/data/window.py +135 -0
- kalfa/std/figure.py +37 -0
- kalfa/std/init.py +72 -0
- kalfa/std/loggers.py +105 -0
- kalfa/std/losses.py +161 -0
- kalfa/std/metrics.py +463 -0
- kalfa/std/models.py +122 -0
- kalfa/std/notes.py +1553 -0
- kalfa/std/objectives.py +360 -0
- kalfa/std/plots/__init__.py +75 -0
- kalfa/std/plots/classification.py +207 -0
- kalfa/std/plots/common.py +44 -0
- kalfa/std/plots/curves.py +56 -0
- kalfa/std/plots/images.py +188 -0
- kalfa/std/plots/pairs.py +66 -0
- kalfa/std/plots/predictions.py +154 -0
- kalfa/std/samplers.py +261 -0
- kalfa/std/samples.py +72 -0
- kalfa/std/schedules.py +74 -0
- kalfa/std/steps.py +133 -0
- kalfa/std/triggers.py +135 -0
- kalfa/summary.py +296 -0
- kalfa/tui/__init__.py +4 -0
- kalfa/tui/app.py +107 -0
- kalfa/tui/config.py +61 -0
- kalfa/tui/guide.py +100 -0
- kalfa/tui/jobs.py +97 -0
- kalfa/tui/legos.py +62 -0
- kalfa/tui/records.py +439 -0
- kalfa-3.0.0.dev0.dist-info/METADATA +261 -0
- kalfa-3.0.0.dev0.dist-info/RECORD +148 -0
- kalfa-3.0.0.dev0.dist-info/WHEEL +4 -0
- kalfa-3.0.0.dev0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,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
|
kalfa/std/data/window.py
ADDED
|
@@ -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()
|