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/images.py
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import numpy
|
|
7
|
+
import pandas
|
|
8
|
+
import torch
|
|
9
|
+
from PIL import Image
|
|
10
|
+
from torch.utils.data import Dataset
|
|
11
|
+
|
|
12
|
+
from kalfa.core.aliases import register
|
|
13
|
+
from kalfa.core.errors import BuildError, RunError, error
|
|
14
|
+
from kalfa.std.data import flow, transforms
|
|
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
|
+
EXTENSIONS = (".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff", ".webp")
|
|
23
|
+
GRAY_MODES = ("1", "L", "LA", "I", "I;16", "F")
|
|
24
|
+
BATCH_SIZE = 64
|
|
25
|
+
SPLIT_HINT = "a list of ratios such as [0.8, 0.1, 0.1] or a call such as {use: kfold, folds: 5, fold: 0}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def labels_of(labels):
|
|
29
|
+
if not isinstance(labels, bool):
|
|
30
|
+
raise BuildError(f"labels is true or false, got {labels!r}")
|
|
31
|
+
return labels
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def extensions_of(extensions):
|
|
35
|
+
if isinstance(extensions, str):
|
|
36
|
+
extensions = [extensions]
|
|
37
|
+
if not isinstance(extensions, (list, tuple)) or not all(isinstance(item, str) for item in extensions):
|
|
38
|
+
raise BuildError(f"extensions is a list of file suffixes such as ['.png', '.jpg'], got {extensions!r}")
|
|
39
|
+
out = tuple(item.lower() if item.startswith(".") else f".{item.lower()}" for item in extensions if item)
|
|
40
|
+
if not out:
|
|
41
|
+
raise BuildError("extensions names at least one file suffix such as .png")
|
|
42
|
+
return out
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def transforms_check(spec):
|
|
46
|
+
items = spec if isinstance(spec, (list, tuple)) else [spec]
|
|
47
|
+
for position, item in enumerate(items):
|
|
48
|
+
if isinstance(item, str):
|
|
49
|
+
target_of(item, f"[{position}]")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
VALIDATORS = (
|
|
53
|
+
("batch", lambda value: batch_of(value, BATCH_SIZE)),
|
|
54
|
+
("labels", labels_of),
|
|
55
|
+
("extensions", extensions_of),
|
|
56
|
+
("split", split_check),
|
|
57
|
+
("transform", transforms_check),
|
|
58
|
+
("train_transform", transforms_check),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def absolute(path):
|
|
63
|
+
return Path(os.path.abspath(Path(str(path)).expanduser()))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def folder_of(folder, directories=()):
|
|
67
|
+
if not isinstance(folder, (str, Path)) or not str(folder):
|
|
68
|
+
raise BuildError(f"folder is the path of an image folder, got {folder!r}")
|
|
69
|
+
try:
|
|
70
|
+
path = locate(folder, directories)
|
|
71
|
+
except BuildError:
|
|
72
|
+
raise BuildError(f"image folder {str(folder)!r} not found") from None
|
|
73
|
+
if not path.is_dir():
|
|
74
|
+
raise BuildError(f"image folder {str(folder)!r} is a file, not a directory")
|
|
75
|
+
return absolute(path)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def visible(path):
|
|
79
|
+
return not path.name.startswith(".")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def images_in(directory, extensions):
|
|
83
|
+
return sorted(path for path in directory.iterdir()
|
|
84
|
+
if path.is_file() and visible(path) and path.suffix.lower() in extensions)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def scan(root, extensions):
|
|
88
|
+
found = {}
|
|
89
|
+
for entry in sorted(root.iterdir()):
|
|
90
|
+
if entry.is_dir() and visible(entry):
|
|
91
|
+
files = images_in(entry, extensions)
|
|
92
|
+
if files:
|
|
93
|
+
found[entry.name] = files
|
|
94
|
+
if found:
|
|
95
|
+
loose = images_in(root, extensions)
|
|
96
|
+
if loose:
|
|
97
|
+
logger.warning(f"{root}: {len(loose)} images lie next to the class folders and are left out")
|
|
98
|
+
paths = [str(path) for files in found.values() for path in files]
|
|
99
|
+
names = [name for name, files in found.items() for _ in files]
|
|
100
|
+
return pandas.DataFrame({"path": paths, "label": names}), list(found)
|
|
101
|
+
paths = [str(path) for path in images_in(root, extensions)]
|
|
102
|
+
if not paths:
|
|
103
|
+
raise BuildError(f"image folder {str(root)!r} holds no images with the extensions {list(extensions)}")
|
|
104
|
+
return pandas.DataFrame({"path": paths}), []
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def mode_of(path):
|
|
108
|
+
try:
|
|
109
|
+
with Image.open(path) as image:
|
|
110
|
+
return "L" if image.mode in GRAY_MODES else "RGB"
|
|
111
|
+
except OSError as failure:
|
|
112
|
+
raise BuildError(f"{path}: not a readable image ({failure})") from failure
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def open_image(path, mode):
|
|
116
|
+
try:
|
|
117
|
+
with Image.open(path) as image:
|
|
118
|
+
return image.convert(mode)
|
|
119
|
+
except OSError as failure:
|
|
120
|
+
raise RunError(f"{path}: not a readable image ({failure})") from failure
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class ImageDataset(Dataset):
|
|
124
|
+
def __init__(self, paths, codes, pipeline, mode, rows, frame=None):
|
|
125
|
+
self.paths = list(paths)
|
|
126
|
+
self.codes = None if codes is None else torch.from_numpy(numpy.array(codes, dtype="int64"))
|
|
127
|
+
self.pipeline = pipeline
|
|
128
|
+
self.mode = mode
|
|
129
|
+
self.rows = numpy.array(rows)
|
|
130
|
+
self.frame = frame
|
|
131
|
+
|
|
132
|
+
def __len__(self):
|
|
133
|
+
return len(self.paths)
|
|
134
|
+
|
|
135
|
+
def __getitem__(self, position):
|
|
136
|
+
image = open_image(self.paths[position], self.mode)
|
|
137
|
+
item = {"image": transforms.image_tensor(self.pipeline(image))}
|
|
138
|
+
if self.codes is not None:
|
|
139
|
+
item["label"] = self.codes[position]
|
|
140
|
+
return item
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class ImageFolder(Loaders):
|
|
144
|
+
inputs = ("image",)
|
|
145
|
+
exposes = ("n_classes", "classes", "class_weights", "image_shape")
|
|
146
|
+
predictions = True
|
|
147
|
+
spectator_patterns = ("path", "label")
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def check(cls, params, problems, directories=()):
|
|
151
|
+
if params.get("split") is None:
|
|
152
|
+
problems.append(error("split_required", f"data.split is required: {SPLIT_HINT}", "data.split"))
|
|
153
|
+
folder = params.get("folder")
|
|
154
|
+
if isinstance(folder, (str, Path)) and str(folder):
|
|
155
|
+
try:
|
|
156
|
+
folder_of(folder, directories)
|
|
157
|
+
except BuildError as failure:
|
|
158
|
+
problems.append(error("data_value", str(failure), "data.folder",
|
|
159
|
+
hint="the run looks in the working directory, then next to the config"))
|
|
160
|
+
for name, validator in VALIDATORS:
|
|
161
|
+
if params.get(name) is None:
|
|
162
|
+
continue
|
|
163
|
+
try:
|
|
164
|
+
validator(params[name])
|
|
165
|
+
except BuildError as failure:
|
|
166
|
+
problems.append(error("data_value", str(failure), f"data.{name}"))
|
|
167
|
+
name_problems(params.get("split"), "data.split", problems)
|
|
168
|
+
|
|
169
|
+
def __init__(self, folder, split=None, transform=None, train_transform=None, batch=64, labels=True,
|
|
170
|
+
extensions=EXTENSIONS):
|
|
171
|
+
self.folder = folder
|
|
172
|
+
self.split = split_of(split)
|
|
173
|
+
self.transform = transforms.pipeline_of(transform, "data.transform")
|
|
174
|
+
self.train_transform = (self.transform if train_transform is None
|
|
175
|
+
else transforms.pipeline_of(train_transform, "data.train_transform"))
|
|
176
|
+
self.batch = batch_of(batch, BATCH_SIZE)
|
|
177
|
+
self.wants_labels = labels_of(labels)
|
|
178
|
+
self.labels = self.wants_labels
|
|
179
|
+
self.extensions = extensions_of(extensions)
|
|
180
|
+
self.classes = None
|
|
181
|
+
self.n_classes = None
|
|
182
|
+
self.class_weights = None
|
|
183
|
+
self.image_shape = None
|
|
184
|
+
self.mode = None
|
|
185
|
+
self.root = None
|
|
186
|
+
self.found = None
|
|
187
|
+
self.frames = {}
|
|
188
|
+
self.datasets = {}
|
|
189
|
+
self.counts = {}
|
|
190
|
+
self.rng = None
|
|
191
|
+
self.loaders_by_set = None
|
|
192
|
+
self.fitted = False
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def target_names(self):
|
|
196
|
+
return ["label"] if self.labels else []
|
|
197
|
+
|
|
198
|
+
def scanned(self, ctx):
|
|
199
|
+
root = folder_of(self.folder, config_directories(ctx))
|
|
200
|
+
frame, classes = scan(root, self.extensions)
|
|
201
|
+
self.root = root
|
|
202
|
+
self.found = len(frame)
|
|
203
|
+
return frame, classes
|
|
204
|
+
|
|
205
|
+
def lookup(self):
|
|
206
|
+
return {name: code for code, name in enumerate(self.classes)}
|
|
207
|
+
|
|
208
|
+
def coded(self, frame):
|
|
209
|
+
if not self.labels:
|
|
210
|
+
return frame
|
|
211
|
+
lookup = self.lookup()
|
|
212
|
+
unknown = sorted(set(frame["label"]) - set(lookup))
|
|
213
|
+
if unknown:
|
|
214
|
+
raise BuildError(f"{self.root}: the class folders {unknown} are not among the fitted classes "
|
|
215
|
+
f"{self.classes}")
|
|
216
|
+
return frame.assign(code=frame["label"].map(lookup).astype("int64"))
|
|
217
|
+
|
|
218
|
+
def sets_of(self, frame, ctx):
|
|
219
|
+
sets = {name: part for name, part in seeded_split(self.split, ctx).split(frame).items() if len(part)}
|
|
220
|
+
if "train" not in sets:
|
|
221
|
+
raise BuildError("the split leaves no train images")
|
|
222
|
+
return sets
|
|
223
|
+
|
|
224
|
+
def weights_of(self, train):
|
|
225
|
+
counts = numpy.bincount(train["code"].to_numpy(dtype="int64"), minlength=len(self.classes))
|
|
226
|
+
weights = 1.0 / numpy.maximum(counts.astype("float64"), 1.0)
|
|
227
|
+
return torch.tensor(weights / weights.mean(), dtype=torch.float32)
|
|
228
|
+
|
|
229
|
+
def shape_of(self, train):
|
|
230
|
+
image = open_image(train["path"].iloc[0], self.mode)
|
|
231
|
+
return tuple(int(size) for size in transforms.image_tensor(self.transform(image)).shape)
|
|
232
|
+
|
|
233
|
+
def fit(self, ctx):
|
|
234
|
+
frame, classes = self.scanned(ctx)
|
|
235
|
+
self.labels = self.wants_labels and bool(classes)
|
|
236
|
+
if self.wants_labels and not classes:
|
|
237
|
+
logger.info(f"{self.root} has no class folders; the images carry no label")
|
|
238
|
+
self.classes = list(classes) if self.labels else None
|
|
239
|
+
self.n_classes = None if self.classes is None else len(self.classes)
|
|
240
|
+
self.mode = mode_of(frame["path"].iloc[0])
|
|
241
|
+
sets = self.sets_of(self.coded(frame), ctx)
|
|
242
|
+
self.class_weights = self.weights_of(sets["train"]) if self.labels else None
|
|
243
|
+
self.image_shape = self.shape_of(sets["train"])
|
|
244
|
+
self.fitted = True
|
|
245
|
+
self.materialize(sets, ctx)
|
|
246
|
+
|
|
247
|
+
def rebuild(self, ctx):
|
|
248
|
+
if not self.fitted:
|
|
249
|
+
raise BuildError("the data is not fitted; load(directory) first")
|
|
250
|
+
frame, classes = self.scanned(ctx)
|
|
251
|
+
if self.labels and not classes:
|
|
252
|
+
raise BuildError(f"{self.root} has no class folders, but the run was fitted on the classes {self.classes}")
|
|
253
|
+
if self.mode is None:
|
|
254
|
+
self.mode = mode_of(frame["path"].iloc[0])
|
|
255
|
+
self.materialize(self.sets_of(self.coded(frame), ctx), ctx)
|
|
256
|
+
|
|
257
|
+
def dataset_of(self, frame, pipeline):
|
|
258
|
+
codes = frame["code"].to_numpy() if self.labels and "code" in frame.columns else None
|
|
259
|
+
return ImageDataset(frame["path"].tolist(), codes, pipeline, self.mode, frame.index)
|
|
260
|
+
|
|
261
|
+
def materialize(self, sets, ctx):
|
|
262
|
+
self.rng = getattr(ctx, "rng", None)
|
|
263
|
+
self.frames = dict(sets)
|
|
264
|
+
self.datasets = {name: self.dataset_of(part, self.train_transform if name == "train" else self.transform)
|
|
265
|
+
for name, part in sets.items()}
|
|
266
|
+
self.counts = {name: len(part) for name, part in sets.items()}
|
|
267
|
+
self.loaders_by_set = None
|
|
268
|
+
rows = ", ".join(f"{name} {count}" for name, count in self.counts.items())
|
|
269
|
+
classes = "no labels" if self.classes is None else f"{len(self.classes)} classes"
|
|
270
|
+
logger.info(f"data: {rows} images; {classes}; image shape {self.image_shape}")
|
|
271
|
+
|
|
272
|
+
def listing_of(self, frame):
|
|
273
|
+
if isinstance(frame, pandas.DataFrame):
|
|
274
|
+
if "path" not in frame.columns:
|
|
275
|
+
raise BuildError(f"loader_for takes a frame with a path column; the columns are {list(frame.columns)}")
|
|
276
|
+
listing = frame
|
|
277
|
+
elif isinstance(frame, (str, Path)):
|
|
278
|
+
path = absolute(frame)
|
|
279
|
+
if path.is_dir():
|
|
280
|
+
listing, _ = scan(path, self.extensions)
|
|
281
|
+
elif path.is_file():
|
|
282
|
+
listing = pandas.DataFrame({"path": [str(path)]})
|
|
283
|
+
else:
|
|
284
|
+
raise BuildError(f"loader_for: {str(frame)!r} is neither an image folder nor an image file")
|
|
285
|
+
elif isinstance(frame, (list, tuple)) and all(isinstance(item, (str, Path)) for item in frame):
|
|
286
|
+
listing = pandas.DataFrame({"path": [str(absolute(item)) for item in frame]})
|
|
287
|
+
else:
|
|
288
|
+
raise BuildError(f"loader_for takes a frame with a path column, a list of image paths or a folder, got "
|
|
289
|
+
f"{type(frame).__name__}")
|
|
290
|
+
if not len(listing):
|
|
291
|
+
raise BuildError("loader_for: there are no images to load")
|
|
292
|
+
missing = [str(path) for path in listing["path"] if not Path(str(path)).expanduser().is_file()]
|
|
293
|
+
if missing:
|
|
294
|
+
raise BuildError(f"loader_for: {len(missing)} images are missing, such as {missing[:3]}")
|
|
295
|
+
return listing
|
|
296
|
+
|
|
297
|
+
def codes_for(self, listing):
|
|
298
|
+
if not self.labels or "label" not in listing.columns:
|
|
299
|
+
return None
|
|
300
|
+
lookup = self.lookup()
|
|
301
|
+
names = listing["label"].tolist()
|
|
302
|
+
unknown = sorted({str(name) for name in names if name not in lookup})
|
|
303
|
+
if unknown:
|
|
304
|
+
logger.warning(f"loader_for: the labels {unknown[:5]} are not among the classes {self.classes}; the "
|
|
305
|
+
f"images are loaded without labels")
|
|
306
|
+
return None
|
|
307
|
+
return numpy.array([lookup[name] for name in names], dtype="int64")
|
|
308
|
+
|
|
309
|
+
def loader_for(self, frame, set_name="test"):
|
|
310
|
+
if not self.fitted:
|
|
311
|
+
raise BuildError("the data is not fitted; call fit(ctx) or load(directory) first")
|
|
312
|
+
listing = self.listing_of(frame)
|
|
313
|
+
paths = [str(Path(str(path)).expanduser()) for path in listing["path"]]
|
|
314
|
+
mode = self.mode or mode_of(paths[0])
|
|
315
|
+
dataset = ImageDataset(paths, self.codes_for(listing), self.transform, mode, listing.index, frame=listing)
|
|
316
|
+
return self.make_loader(dataset, train=False)
|
|
317
|
+
|
|
318
|
+
def spectators(self, set_name):
|
|
319
|
+
part = self.frames.get(set_name)
|
|
320
|
+
if part is None:
|
|
321
|
+
return None
|
|
322
|
+
return part[[column for column in ("path", "label") if column in part.columns]]
|
|
323
|
+
|
|
324
|
+
def decode(self, name, values):
|
|
325
|
+
if not self.is_label(name):
|
|
326
|
+
reason = "the images carry no label" if not self.labels else "the only target is label"
|
|
327
|
+
raise BuildError(f"decode({name!r}): {reason}")
|
|
328
|
+
array = values.detach().cpu().numpy() if isinstance(values, torch.Tensor) else numpy.asarray(values)
|
|
329
|
+
count = len(self.classes)
|
|
330
|
+
if array.ndim >= 2 and array.shape[-1] != 1:
|
|
331
|
+
if array.shape[-1] != count:
|
|
332
|
+
raise BuildError(f"decode({name!r}): takes class indices or scores of width {count}, got the shape "
|
|
333
|
+
f"{array.shape}")
|
|
334
|
+
array = array.argmax(axis=-1)
|
|
335
|
+
codes = numpy.asarray(array).astype("int64").reshape(-1)
|
|
336
|
+
if codes.size and (codes.min() < 0 or codes.max() >= count):
|
|
337
|
+
raise BuildError(f"decode({name!r}): class indices lie in [0, {count}), got {codes.min()} to "
|
|
338
|
+
f"{codes.max()}")
|
|
339
|
+
return numpy.asarray(self.classes, dtype=object)[codes]
|
|
340
|
+
|
|
341
|
+
def is_label(self, name):
|
|
342
|
+
return bool(self.labels) and name == "label"
|
|
343
|
+
|
|
344
|
+
def rescales(self, name):
|
|
345
|
+
return False
|
|
346
|
+
|
|
347
|
+
def inverse(self, name, values):
|
|
348
|
+
return values
|
|
349
|
+
|
|
350
|
+
def save(self, directory):
|
|
351
|
+
if not self.fitted:
|
|
352
|
+
raise BuildError("the data is not fitted; there is nothing to save")
|
|
353
|
+
root = Path(directory) / "fitted"
|
|
354
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
355
|
+
fields = {
|
|
356
|
+
"kind": "image_folder",
|
|
357
|
+
"classes": None if self.classes is None else list(self.classes),
|
|
358
|
+
"labels": bool(self.labels),
|
|
359
|
+
"image_shape": None if self.image_shape is None else list(self.image_shape),
|
|
360
|
+
"transform": list(self.transform.names),
|
|
361
|
+
"train_transform": list(self.train_transform.names),
|
|
362
|
+
"mode": self.mode,
|
|
363
|
+
"class_weights": None if self.class_weights is None else self.class_weights.tolist(),
|
|
364
|
+
"targets": list(self.target_names),
|
|
365
|
+
"target_fields": self.target_fields(),
|
|
366
|
+
}
|
|
367
|
+
(root / "fields.json").write_text(json.dumps(fields, indent=2))
|
|
368
|
+
|
|
369
|
+
def target_fields(self):
|
|
370
|
+
if not self.labels:
|
|
371
|
+
return []
|
|
372
|
+
weights = None if self.class_weights is None else self.class_weights.tolist()
|
|
373
|
+
return [{"name": "label", "width": 1, "chain": [], "classes": list(self.classes), "class_weights": weights}]
|
|
374
|
+
|
|
375
|
+
def load(self, directory):
|
|
376
|
+
file = Path(directory) / "fitted" / "fields.json"
|
|
377
|
+
try:
|
|
378
|
+
fields = json.loads(file.read_text())
|
|
379
|
+
except FileNotFoundError:
|
|
380
|
+
raise BuildError(f"{file.parent} lacks fields.json; save(directory) writes the fitted files") from None
|
|
381
|
+
except json.JSONDecodeError as failure:
|
|
382
|
+
raise BuildError(f"{file} is not valid JSON: {failure}") from failure
|
|
383
|
+
if not isinstance(fields, dict) or fields.get("kind") != "image_folder":
|
|
384
|
+
raise BuildError(f"{file} was not written by image_folder")
|
|
385
|
+
self.labels = bool(fields.get("labels"))
|
|
386
|
+
classes = fields.get("classes")
|
|
387
|
+
self.classes = list(classes) if self.labels and classes is not None else None
|
|
388
|
+
if self.labels and self.classes is None:
|
|
389
|
+
raise BuildError(f"{file} says the images carry labels but lists no classes")
|
|
390
|
+
self.n_classes = None if self.classes is None else len(self.classes)
|
|
391
|
+
weights = fields.get("class_weights")
|
|
392
|
+
self.class_weights = None if weights is None else torch.tensor(weights, dtype=torch.float32)
|
|
393
|
+
shape = fields.get("image_shape")
|
|
394
|
+
self.image_shape = None if shape is None else tuple(int(size) for size in shape)
|
|
395
|
+
self.mode = fields.get("mode") or None
|
|
396
|
+
self.found = None
|
|
397
|
+
self.frames = {}
|
|
398
|
+
self.datasets = {}
|
|
399
|
+
self.counts = {}
|
|
400
|
+
self.loaders_by_set = None
|
|
401
|
+
self.fitted = True
|
|
402
|
+
return self
|
|
403
|
+
|
|
404
|
+
def report(self):
|
|
405
|
+
return {"rows": dict(self.counts), "targets": list(self.target_names),
|
|
406
|
+
"classes": None if self.classes is None else list(self.classes), "labels": bool(self.labels),
|
|
407
|
+
"image_shape": None if self.image_shape is None else list(self.image_shape), "mode": self.mode,
|
|
408
|
+
"transform": list(self.transform.names), "train_transform": list(self.train_transform.names)}
|
|
409
|
+
|
|
410
|
+
def pipeline(self):
|
|
411
|
+
graph = flow.Flow()
|
|
412
|
+
root = self.root if self.root is not None else self.folder
|
|
413
|
+
images = None if self.found is None else f"{flow.amount(self.found)} images"
|
|
414
|
+
source = flow.source_node(graph, root, self.found, [images])
|
|
415
|
+
rows = source
|
|
416
|
+
classes = None
|
|
417
|
+
if self.labels and self.classes:
|
|
418
|
+
classes = rows = graph.step(source, "classes", "classes", "classes", flow.listed(self.classes),
|
|
419
|
+
len(self.classes))
|
|
420
|
+
split = flow.split_node(graph, rows, self.split)
|
|
421
|
+
made = flow.set_nodes(graph, split, self.counts, edges=flow.shares_of(self.split))
|
|
422
|
+
same = self.train_transform is self.transform
|
|
423
|
+
trains = [key for name, key in made.items() if same or name == "train"] or [split]
|
|
424
|
+
evals = [key for name, key in made.items() if name != "train"] or [split]
|
|
425
|
+
ends = graph.chain(trains if same else evals, "transform", flow.pipeline_steps(self.transform), "transform",
|
|
426
|
+
"transform", "every set" if same else "valid, test and predict")
|
|
427
|
+
if not same:
|
|
428
|
+
ends += graph.chain(trains, "train_transform", flow.pipeline_steps(self.train_transform), "transform",
|
|
429
|
+
"train_transform", "train set")
|
|
430
|
+
shape = " × ".join(str(size) for size in self.image_shape) if self.image_shape else "C × H × W"
|
|
431
|
+
image = graph.node("tensor:image", "tensor", "image", f"float {shape}")
|
|
432
|
+
graph.join(ends, image)
|
|
433
|
+
tensors = [image]
|
|
434
|
+
if classes is not None:
|
|
435
|
+
about = f"int64, {len(self.classes)} classes"
|
|
436
|
+
tensors.append(graph.step(classes, "tensor:label", "tensor", "label", about))
|
|
437
|
+
flow.batch_node(graph, tensors, self.batch)
|
|
438
|
+
return graph.result()
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
register("image_folder", "kalfa.std.data.images.ImageFolder", slot="data",
|
|
442
|
+
doc="a folder of class folders of images: the batch carries image (float (C, H, W) after the transforms) and "
|
|
443
|
+
"label (int64); transform for every set, train_transform for the train set")
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pandas
|
|
4
|
+
|
|
5
|
+
from kalfa.core import aliases
|
|
6
|
+
from kalfa.core.errors import BuildError
|
|
7
|
+
from kalfa.std.data import splits
|
|
8
|
+
from kalfa.std.data.columns import target_of
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
SET_NAMES = ("train", "valid", "test")
|
|
12
|
+
FILTER_KEYS = ("all",) + SET_NAMES
|
|
13
|
+
BATCH_DEFAULTS = {"size": 128, "eval_size": None, "workers": 0, "drop_last": False, "shuffle": True}
|
|
14
|
+
STREAM_SPLIT = "a stream needs a sequential or given split; stream cuts the file in order"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def batch_spec(batch):
|
|
18
|
+
if isinstance(batch, bool):
|
|
19
|
+
raise BuildError(f"batch is an integer size or a mapping of {list(BATCH_DEFAULTS)}, got {batch!r}")
|
|
20
|
+
if isinstance(batch, int):
|
|
21
|
+
spec = {**BATCH_DEFAULTS, "size": batch}
|
|
22
|
+
elif isinstance(batch, dict):
|
|
23
|
+
unknown = sorted(set(batch) - set(BATCH_DEFAULTS))
|
|
24
|
+
if unknown:
|
|
25
|
+
raise BuildError(f"batch: unknown keys {unknown}; the keys are {list(BATCH_DEFAULTS)}")
|
|
26
|
+
spec = {**BATCH_DEFAULTS, **batch}
|
|
27
|
+
else:
|
|
28
|
+
raise BuildError(f"batch is an integer size or a mapping of {list(BATCH_DEFAULTS)}, got "
|
|
29
|
+
f"{type(batch).__name__}")
|
|
30
|
+
for key in ("size", "eval_size"):
|
|
31
|
+
value = spec[key]
|
|
32
|
+
if key == "eval_size" and value is None:
|
|
33
|
+
continue
|
|
34
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 1:
|
|
35
|
+
raise BuildError(f"batch.{key} must be a positive integer, got {value!r}")
|
|
36
|
+
spec["workers"] = int(spec["workers"] or 0)
|
|
37
|
+
spec["drop_last"] = bool(spec["drop_last"])
|
|
38
|
+
spec["shuffle"] = bool(spec["shuffle"])
|
|
39
|
+
return spec
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def batch_of(batch, size=BATCH_DEFAULTS["size"]):
|
|
43
|
+
if isinstance(batch, dict) and "size" not in batch:
|
|
44
|
+
batch = {**batch, "size": size}
|
|
45
|
+
return batch_spec(batch)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def filters_of(spec):
|
|
49
|
+
if spec is None:
|
|
50
|
+
return None, {}
|
|
51
|
+
if isinstance(spec, str):
|
|
52
|
+
return spec, {}
|
|
53
|
+
if isinstance(spec, dict):
|
|
54
|
+
unknown = sorted(set(spec) - set(FILTER_KEYS))
|
|
55
|
+
if unknown:
|
|
56
|
+
raise BuildError(f"filter: unknown keys {unknown}; the keys are {list(FILTER_KEYS)}")
|
|
57
|
+
wrong = [key for key, text in spec.items() if not isinstance(text, str)]
|
|
58
|
+
if wrong:
|
|
59
|
+
raise BuildError(f"filter: {wrong} must be pandas query strings")
|
|
60
|
+
return spec.get("all"), {name: text for name, text in spec.items() if name != "all"}
|
|
61
|
+
raise BuildError("filter is a pandas query string or a mapping of all, train, valid and test to query strings")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def query(frame, text, where):
|
|
65
|
+
try:
|
|
66
|
+
return frame.query(text)
|
|
67
|
+
except Exception as error:
|
|
68
|
+
raise BuildError(f"{where}: the query {text!r} failed: {error}") from error
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def split_of(spec):
|
|
72
|
+
if spec is None:
|
|
73
|
+
raise BuildError("data.split is required: a list of ratios such as [0.7, 0.15, 0.15] or a call such as "
|
|
74
|
+
"{use: kfold, folds: 5, fold: 0}")
|
|
75
|
+
if isinstance(spec, (list, tuple)):
|
|
76
|
+
return splits.RandomSplit(spec)
|
|
77
|
+
if isinstance(spec, str):
|
|
78
|
+
return target_of(spec, "split")()
|
|
79
|
+
if isinstance(spec, dict):
|
|
80
|
+
if "use" not in spec:
|
|
81
|
+
raise BuildError(f"split: a mapping needs use, got the keys {sorted(spec)}")
|
|
82
|
+
params = {key: value for key, value in spec.items() if key != "use"}
|
|
83
|
+
return target_of(spec["use"], "split")(**params)
|
|
84
|
+
if hasattr(spec, "split"):
|
|
85
|
+
return spec
|
|
86
|
+
raise BuildError(f"split is a list of ratios, a call or an object with split(frame), got {type(spec).__name__}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def split_check(spec):
|
|
90
|
+
if isinstance(spec, (list, tuple)):
|
|
91
|
+
splits.RandomSplit(spec)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def split_target(spec):
|
|
95
|
+
if isinstance(spec, (list, tuple)):
|
|
96
|
+
return splits.RandomSplit
|
|
97
|
+
if isinstance(spec, str):
|
|
98
|
+
return target_of(spec, "split")
|
|
99
|
+
if isinstance(spec, dict):
|
|
100
|
+
return target_of(spec["use"], "split") if "use" in spec else None
|
|
101
|
+
if hasattr(spec, "split"):
|
|
102
|
+
return type(spec)
|
|
103
|
+
return getattr(spec, "target", None)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def stream_split_check(stream, split):
|
|
107
|
+
if not stream or split is None:
|
|
108
|
+
return
|
|
109
|
+
try:
|
|
110
|
+
target = split_target(split)
|
|
111
|
+
except BuildError:
|
|
112
|
+
return
|
|
113
|
+
if isinstance(target, type) and not issubclass(target, (splits.SequentialSplit, splits.GivenSplit)):
|
|
114
|
+
raise BuildError(STREAM_SPLIT)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def stream_check(value):
|
|
118
|
+
if value is None or value is False:
|
|
119
|
+
return value
|
|
120
|
+
from kalfa.std.data.stream import options_of
|
|
121
|
+
|
|
122
|
+
return options_of(value)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def locate(file, directories=()):
|
|
126
|
+
path = Path(str(file)).expanduser()
|
|
127
|
+
if path.exists():
|
|
128
|
+
return path
|
|
129
|
+
searched = list(dict.fromkeys(str(item) for item in [*(directories or ()), *reversed(aliases.SEARCH_DIRS)]))
|
|
130
|
+
if not path.is_absolute():
|
|
131
|
+
for directory in searched:
|
|
132
|
+
candidate = Path(directory) / path
|
|
133
|
+
if candidate.exists():
|
|
134
|
+
return candidate
|
|
135
|
+
looked = "" if path.is_absolute() or not searched else f"; also looked under {[str(item) for item in searched]}"
|
|
136
|
+
raise BuildError(f"data file {str(file)!r} not found{looked}")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def read_table(path):
|
|
140
|
+
suffix = path.suffix.lower()
|
|
141
|
+
if suffix in (".parquet", ".pq"):
|
|
142
|
+
return pandas.read_parquet(path)
|
|
143
|
+
if suffix == ".csv":
|
|
144
|
+
return pandas.read_csv(path)
|
|
145
|
+
if suffix == ".tsv":
|
|
146
|
+
return pandas.read_csv(path, sep="\t")
|
|
147
|
+
raise BuildError(f"{path}: the table format {suffix!r} is unknown; use .parquet or .csv")
|
kalfa/std/data/pre.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import numpy
|
|
2
|
+
from sklearn.preprocessing import LabelEncoder, OneHotEncoder
|
|
3
|
+
|
|
4
|
+
from kalfa.core.errors import BuildError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def one_hot():
|
|
8
|
+
return OneHotEncoder(handle_unknown="ignore", sparse_output=False)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def flat(values):
|
|
12
|
+
return numpy.asarray(values).reshape(-1)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Abs:
|
|
16
|
+
rescales = False
|
|
17
|
+
|
|
18
|
+
def fit(self, values):
|
|
19
|
+
return self
|
|
20
|
+
|
|
21
|
+
def transform(self, values):
|
|
22
|
+
return numpy.abs(numpy.asarray(values))
|
|
23
|
+
|
|
24
|
+
def inverse_transform(self, values):
|
|
25
|
+
return numpy.asarray(values)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Log:
|
|
29
|
+
def __init__(self, base=10.0, norm=1.0):
|
|
30
|
+
self.base = float(base)
|
|
31
|
+
self.norm = float(norm)
|
|
32
|
+
if self.base <= 0.0 or self.base == 1.0:
|
|
33
|
+
raise BuildError(f"log: base must be positive and not 1, got {base!r}")
|
|
34
|
+
if self.norm <= 0.0:
|
|
35
|
+
raise BuildError(f"log: norm must be positive, got {norm!r}")
|
|
36
|
+
|
|
37
|
+
def fit(self, values):
|
|
38
|
+
return self
|
|
39
|
+
|
|
40
|
+
def transform(self, values):
|
|
41
|
+
return numpy.log1p(numpy.asarray(values, dtype="float64") / self.norm) / numpy.log(self.base)
|
|
42
|
+
|
|
43
|
+
def inverse_transform(self, values):
|
|
44
|
+
return numpy.expm1(numpy.asarray(values, dtype="float64") * numpy.log(self.base)) * self.norm
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Cast:
|
|
48
|
+
rescales = False
|
|
49
|
+
|
|
50
|
+
def __init__(self, dtype="float32"):
|
|
51
|
+
try:
|
|
52
|
+
self.dtype = numpy.dtype(dtype)
|
|
53
|
+
except TypeError:
|
|
54
|
+
raise BuildError(f"cast: unknown dtype {dtype!r}") from None
|
|
55
|
+
|
|
56
|
+
def fit(self, values):
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
def transform(self, values):
|
|
60
|
+
return numpy.asarray(values).astype(self.dtype)
|
|
61
|
+
|
|
62
|
+
def inverse_transform(self, values):
|
|
63
|
+
return numpy.asarray(values)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Label:
|
|
67
|
+
rescales = False
|
|
68
|
+
|
|
69
|
+
def __init__(self):
|
|
70
|
+
self.encoder = LabelEncoder()
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def classes_(self):
|
|
74
|
+
return self.encoder.classes_
|
|
75
|
+
|
|
76
|
+
def fit(self, values):
|
|
77
|
+
self.encoder.fit(flat(values))
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
def transform(self, values):
|
|
81
|
+
return numpy.asarray(self.encoder.transform(flat(values)), dtype="int64")
|
|
82
|
+
|
|
83
|
+
def inverse_transform(self, values):
|
|
84
|
+
return self.encoder.inverse_transform(numpy.asarray(values, dtype="int64").reshape(-1))
|