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/metrics.py
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
import numpy
|
|
4
|
+
import torch
|
|
5
|
+
from torch.nn import functional
|
|
6
|
+
|
|
7
|
+
from kalfa.core.aliases import import_target, register
|
|
8
|
+
from kalfa.core.errors import BuildError, RunError
|
|
9
|
+
from kalfa.core.protocols import ContextMetric
|
|
10
|
+
from kalfa.std.common import classes_last_of, evaluating
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def flat(value):
|
|
14
|
+
return value.detach().cpu().reshape(-1).to(torch.float64)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def pair(pred, target):
|
|
18
|
+
guess, truth = flat(pred), flat(target)
|
|
19
|
+
if guess.numel() != truth.numel():
|
|
20
|
+
raise ValueError(f"pred has {guess.numel()} values and target {truth.numel()}; the metric needs one per row")
|
|
21
|
+
return guess, truth
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def labels_of(pred):
|
|
25
|
+
scores = pred.detach().cpu()
|
|
26
|
+
if scores.ndim >= 2 and scores.shape[-1] > 1:
|
|
27
|
+
return scores.reshape(len(scores), -1).argmax(dim=-1)
|
|
28
|
+
scores = scores.reshape(-1)
|
|
29
|
+
if scores.is_floating_point():
|
|
30
|
+
return (scores > 0).long()
|
|
31
|
+
return scores.long()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def scores_of(pred):
|
|
35
|
+
scores = pred.detach().cpu().to(torch.float32)
|
|
36
|
+
if scores.ndim <= 1 or (scores.ndim == 2 and scores.shape[1] == 1):
|
|
37
|
+
return scores.reshape(-1)
|
|
38
|
+
scores = torch.softmax(scores.reshape(len(scores), -1), dim=1)
|
|
39
|
+
return scores[:, 1] if scores.shape[1] == 2 else scores
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def targets_of(target, pred):
|
|
43
|
+
truth = target.detach().cpu()
|
|
44
|
+
if truth.ndim >= 2 and truth.shape[-1] > 1 and tuple(truth.shape) == tuple(pred.shape):
|
|
45
|
+
return truth.reshape(len(truth), -1).argmax(dim=-1)
|
|
46
|
+
return truth.reshape(-1).long()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def check_rows(rows, truth):
|
|
50
|
+
if rows != truth.numel():
|
|
51
|
+
raise ValueError(f"pred has {rows} rows and target {truth.numel()}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SumMetric:
|
|
55
|
+
def __init__(self):
|
|
56
|
+
self.total = 0.0
|
|
57
|
+
self.count = 0
|
|
58
|
+
|
|
59
|
+
def reset(self):
|
|
60
|
+
self.total = 0.0
|
|
61
|
+
self.count = 0
|
|
62
|
+
|
|
63
|
+
def to(self, device):
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
def finish(self, mean):
|
|
67
|
+
return mean
|
|
68
|
+
|
|
69
|
+
def compute(self):
|
|
70
|
+
if not self.count:
|
|
71
|
+
return math.nan
|
|
72
|
+
return float(self.finish(self.total / self.count))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Rmse(SumMetric):
|
|
76
|
+
def update(self, pred, target):
|
|
77
|
+
guess, truth = pair(pred, target)
|
|
78
|
+
self.total += float(((guess - truth) ** 2).sum())
|
|
79
|
+
self.count += int(truth.numel())
|
|
80
|
+
|
|
81
|
+
def finish(self, mean):
|
|
82
|
+
return math.sqrt(mean)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Mae(SumMetric):
|
|
86
|
+
def update(self, pred, target):
|
|
87
|
+
guess, truth = pair(pred, target)
|
|
88
|
+
self.total += float((guess - truth).abs().sum())
|
|
89
|
+
self.count += int(truth.numel())
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class Confusion:
|
|
93
|
+
averages = ()
|
|
94
|
+
|
|
95
|
+
def __init__(self, num_classes=None, average=None):
|
|
96
|
+
if average not in self.averages:
|
|
97
|
+
raise ValueError(f"average must be one of {list(self.averages)}, got {average!r}")
|
|
98
|
+
self.num_classes = None if num_classes is None else int(num_classes)
|
|
99
|
+
self.average = average
|
|
100
|
+
self.counts = {}
|
|
101
|
+
|
|
102
|
+
def reset(self):
|
|
103
|
+
self.counts = {}
|
|
104
|
+
|
|
105
|
+
def to(self, device):
|
|
106
|
+
return self
|
|
107
|
+
|
|
108
|
+
def update(self, pred, target):
|
|
109
|
+
picked = labels_of(pred)
|
|
110
|
+
truth = targets_of(target, pred)
|
|
111
|
+
check_rows(picked.numel(), truth)
|
|
112
|
+
if picked.numel() == 0:
|
|
113
|
+
return
|
|
114
|
+
pairs, counts = torch.unique(torch.stack([truth, picked], dim=1), dim=0, return_counts=True)
|
|
115
|
+
for (seen, guessed), count in zip(pairs.tolist(), counts.tolist()):
|
|
116
|
+
self.counts[(seen, guessed)] = self.counts.get((seen, guessed), 0) + count
|
|
117
|
+
|
|
118
|
+
def classes(self):
|
|
119
|
+
if self.num_classes is not None:
|
|
120
|
+
return list(range(self.num_classes))
|
|
121
|
+
return sorted({label for entry in self.counts for label in entry})
|
|
122
|
+
|
|
123
|
+
def total(self):
|
|
124
|
+
return sum(self.counts.values())
|
|
125
|
+
|
|
126
|
+
def support(self, label):
|
|
127
|
+
return sum(count for (seen, _), count in self.counts.items() if seen == label)
|
|
128
|
+
|
|
129
|
+
def predicted(self, label):
|
|
130
|
+
return sum(count for (_, guessed), count in self.counts.items() if guessed == label)
|
|
131
|
+
|
|
132
|
+
def hits(self, label):
|
|
133
|
+
return self.counts.get((label, label), 0)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class Accuracy(Confusion):
|
|
137
|
+
averages = ("micro", "macro")
|
|
138
|
+
|
|
139
|
+
def __init__(self, num_classes=None, average="micro"):
|
|
140
|
+
super().__init__(num_classes, average)
|
|
141
|
+
|
|
142
|
+
def compute(self):
|
|
143
|
+
total = self.total()
|
|
144
|
+
if not total:
|
|
145
|
+
return math.nan
|
|
146
|
+
if self.average == "micro":
|
|
147
|
+
return sum(self.hits(label) for label in self.classes()) / total
|
|
148
|
+
recalls = [self.hits(label) / self.support(label) for label in self.classes() if self.support(label)]
|
|
149
|
+
return sum(recalls) / len(recalls)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class F1(Confusion):
|
|
153
|
+
averages = ("macro", "micro", "weighted", "binary")
|
|
154
|
+
|
|
155
|
+
def __init__(self, num_classes=None, average="macro"):
|
|
156
|
+
super().__init__(num_classes, average)
|
|
157
|
+
|
|
158
|
+
def score(self, label):
|
|
159
|
+
denominator = self.predicted(label) + self.support(label)
|
|
160
|
+
return 2.0 * self.hits(label) / denominator if denominator else 0.0
|
|
161
|
+
|
|
162
|
+
def compute(self):
|
|
163
|
+
total = self.total()
|
|
164
|
+
if not total:
|
|
165
|
+
return math.nan
|
|
166
|
+
if self.average == "binary":
|
|
167
|
+
return self.score(1)
|
|
168
|
+
if self.average == "micro":
|
|
169
|
+
return sum(self.hits(label) for label in self.classes()) / total
|
|
170
|
+
labels = self.classes()
|
|
171
|
+
if self.average == "weighted":
|
|
172
|
+
return sum(self.score(label) * self.support(label) for label in labels) / total
|
|
173
|
+
return sum(self.score(label) for label in labels) / len(labels)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class Scored:
|
|
177
|
+
def __init__(self, num_classes=None):
|
|
178
|
+
self.num_classes = None if num_classes is None else int(num_classes)
|
|
179
|
+
self.scores = []
|
|
180
|
+
self.targets = []
|
|
181
|
+
|
|
182
|
+
def reset(self):
|
|
183
|
+
self.scores = []
|
|
184
|
+
self.targets = []
|
|
185
|
+
|
|
186
|
+
def to(self, device):
|
|
187
|
+
return self
|
|
188
|
+
|
|
189
|
+
def update(self, pred, target):
|
|
190
|
+
scores = scores_of(pred)
|
|
191
|
+
truth = targets_of(target, pred)
|
|
192
|
+
check_rows(len(scores), truth)
|
|
193
|
+
if truth.numel() == 0:
|
|
194
|
+
return
|
|
195
|
+
self.scores.append(scores)
|
|
196
|
+
self.targets.append(truth)
|
|
197
|
+
|
|
198
|
+
def gathered(self):
|
|
199
|
+
if not self.scores:
|
|
200
|
+
return None, None
|
|
201
|
+
return torch.cat(self.scores).numpy(), torch.cat(self.targets).numpy()
|
|
202
|
+
|
|
203
|
+
@staticmethod
|
|
204
|
+
def scorable(truth):
|
|
205
|
+
return len(truth) > 0 and truth.min() != truth.max()
|
|
206
|
+
|
|
207
|
+
def compute(self):
|
|
208
|
+
scores, truth = self.gathered()
|
|
209
|
+
if scores is None:
|
|
210
|
+
return math.nan
|
|
211
|
+
if scores.ndim == 1:
|
|
212
|
+
return float(self.binary(scores, truth)) if self.scorable(truth) else math.nan
|
|
213
|
+
values = []
|
|
214
|
+
for label in range(scores.shape[1]):
|
|
215
|
+
positives = (truth == label).astype(int)
|
|
216
|
+
if self.scorable(positives):
|
|
217
|
+
values.append(float(self.binary(scores[:, label], positives)))
|
|
218
|
+
return sum(values) / len(values) if values else math.nan
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class Auroc(Scored):
|
|
222
|
+
def binary(self, scores, truth):
|
|
223
|
+
from sklearn.metrics import roc_auc_score
|
|
224
|
+
|
|
225
|
+
return roc_auc_score(truth, scores)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class AveragePrecision(Scored):
|
|
229
|
+
def binary(self, scores, truth):
|
|
230
|
+
from sklearn.metrics import average_precision_score
|
|
231
|
+
|
|
232
|
+
return average_precision_score(truth, scores)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def rmse():
|
|
236
|
+
return Rmse()
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def mae():
|
|
240
|
+
return Mae()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def accuracy(num_classes=None, average="micro"):
|
|
244
|
+
return Accuracy(num_classes, average)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def f1(num_classes=None, average="macro"):
|
|
248
|
+
return F1(num_classes, average)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def auroc(num_classes=None):
|
|
252
|
+
return Auroc(num_classes)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def average_precision(num_classes=None):
|
|
256
|
+
return AveragePrecision(num_classes)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
METRICS = (
|
|
260
|
+
("rmse", "kalfa.std.metrics.rmse", "root mean squared error over all predicted values"),
|
|
261
|
+
("mae", "kalfa.std.metrics.mae", "mean absolute error over all predicted values"),
|
|
262
|
+
("accuracy", "kalfa.std.metrics.accuracy", "fraction of rows whose predicted class (argmax of logits, or "
|
|
263
|
+
"logit > 0) matches the target; average micro or macro"),
|
|
264
|
+
("f1", "kalfa.std.metrics.f1", "f1 over the classes seen (or num_classes); average macro, micro, weighted or "
|
|
265
|
+
"binary"),
|
|
266
|
+
("auroc", "kalfa.std.metrics.auroc", "area under the roc curve from scores, (N, 2) logits or one versus rest "
|
|
267
|
+
"macro over (N, C) logits; nan when the set holds one class"),
|
|
268
|
+
("average_precision", "kalfa.std.metrics.average_precision", "average precision from scores, (N, 2) logits "
|
|
269
|
+
"or one versus rest macro over (N, C) logits"),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def register_names():
|
|
274
|
+
for name, target, doc in METRICS:
|
|
275
|
+
register(name, target, slot="metric", doc=doc)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
register_names()
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class Perplexity:
|
|
282
|
+
def __init__(self, classes_last=None):
|
|
283
|
+
self.classes_last = classes_last
|
|
284
|
+
self.reset()
|
|
285
|
+
|
|
286
|
+
def reset(self):
|
|
287
|
+
self.total = 0.0
|
|
288
|
+
self.tokens = 0
|
|
289
|
+
|
|
290
|
+
def to(self, device):
|
|
291
|
+
return self
|
|
292
|
+
|
|
293
|
+
def update(self, pred, target):
|
|
294
|
+
logits = pred.detach().cpu().to(torch.float32)
|
|
295
|
+
truth = target.detach().cpu().long()
|
|
296
|
+
if logits.ndim < 2:
|
|
297
|
+
raise ValueError(f"perplexity needs logits (N, T, V) or (N, V), got shape {tuple(logits.shape)}")
|
|
298
|
+
if classes_last_of(logits, truth, self.classes_last, self, "perplexity") is False and logits.ndim >= 3:
|
|
299
|
+
logits = logits.movedim(1, -1)
|
|
300
|
+
logits = logits.reshape(-1, logits.shape[-1])
|
|
301
|
+
truth = truth.reshape(-1)
|
|
302
|
+
if truth.numel() != len(logits):
|
|
303
|
+
raise ValueError(f"perplexity has logits of shape {tuple(pred.shape)} for targets of shape "
|
|
304
|
+
f"{tuple(target.shape)}; it needs (N, T, V) or (N, V, T) logits for (N, T) targets")
|
|
305
|
+
counted = int((truth != -100).sum())
|
|
306
|
+
if not counted:
|
|
307
|
+
return
|
|
308
|
+
self.total += float(functional.cross_entropy(logits, truth, reduction="sum", ignore_index=-100))
|
|
309
|
+
self.tokens += counted
|
|
310
|
+
|
|
311
|
+
def compute(self):
|
|
312
|
+
if not self.tokens:
|
|
313
|
+
return math.nan
|
|
314
|
+
try:
|
|
315
|
+
return math.exp(self.total / self.tokens)
|
|
316
|
+
except OverflowError:
|
|
317
|
+
return math.inf
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def pixels(images):
|
|
321
|
+
values = images.detach().cpu().to(torch.float64)
|
|
322
|
+
if values.ndim == 3:
|
|
323
|
+
values = values.unsqueeze(1)
|
|
324
|
+
if values.ndim != 4:
|
|
325
|
+
raise RunError(f"the pixel extractor needs images (N, C, H, W), got shape {tuple(values.shape)}")
|
|
326
|
+
return functional.adaptive_avg_pool2d(values, 16).flatten(1)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def extractor_of(spec):
|
|
330
|
+
if spec is None:
|
|
331
|
+
return pixels
|
|
332
|
+
found = spec
|
|
333
|
+
if isinstance(spec, str):
|
|
334
|
+
try:
|
|
335
|
+
found = import_target(spec)
|
|
336
|
+
except BuildError as failure:
|
|
337
|
+
raise BuildError(f"fid: extractor {spec!r} is not an importable function images -> features "
|
|
338
|
+
f"({failure})") from None
|
|
339
|
+
if not callable(found):
|
|
340
|
+
raise BuildError(f"fid: extractor {spec!r} is not a function images -> features")
|
|
341
|
+
return found
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def feature_rows(values):
|
|
345
|
+
if isinstance(values, torch.Tensor):
|
|
346
|
+
values = values.detach().cpu().to(torch.float64).numpy()
|
|
347
|
+
values = numpy.asarray(values, dtype=numpy.float64)
|
|
348
|
+
return values.reshape(len(values), -1)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def first_tensor(value):
|
|
352
|
+
if isinstance(value, dict):
|
|
353
|
+
value = next(iter(value.values()))
|
|
354
|
+
if isinstance(value, (tuple, list)):
|
|
355
|
+
value = value[0]
|
|
356
|
+
return value
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def symmetric(matrix):
|
|
360
|
+
return (matrix + matrix.T) / 2.0
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def moments(rows):
|
|
364
|
+
width = rows.shape[1]
|
|
365
|
+
return rows.mean(axis=0), numpy.cov(rows, rowvar=False).reshape(width, width)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def root_of(matrix):
|
|
369
|
+
values, vectors = numpy.linalg.eigh(symmetric(matrix))
|
|
370
|
+
return (vectors * numpy.sqrt(numpy.clip(values, 0.0, None))) @ vectors.T
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def frechet_distance(real, fake):
|
|
374
|
+
real = numpy.asarray(real, dtype=numpy.float64)
|
|
375
|
+
fake = numpy.asarray(fake, dtype=numpy.float64)
|
|
376
|
+
if len(real) < 2 or len(fake) < 2:
|
|
377
|
+
return math.nan
|
|
378
|
+
real = real.reshape(len(real), -1)
|
|
379
|
+
fake = fake.reshape(len(fake), -1)
|
|
380
|
+
if real.shape[1] != fake.shape[1]:
|
|
381
|
+
raise RunError(f"fid: the real features have {real.shape[1]} columns and the fakes {fake.shape[1]}")
|
|
382
|
+
if not (numpy.isfinite(real).all() and numpy.isfinite(fake).all()):
|
|
383
|
+
return math.nan
|
|
384
|
+
mean_real, cov_real = moments(real)
|
|
385
|
+
mean_fake, cov_fake = moments(fake)
|
|
386
|
+
root = root_of(cov_real)
|
|
387
|
+
values = numpy.linalg.eigvalsh(symmetric(root @ cov_fake @ root))
|
|
388
|
+
shared = numpy.sqrt(numpy.clip(values, 0.0, None)).sum()
|
|
389
|
+
difference = mean_real - mean_fake
|
|
390
|
+
distance = difference @ difference + numpy.trace(cov_real) + numpy.trace(cov_fake) - 2.0 * shared
|
|
391
|
+
return max(float(distance), 0.0)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class Fid(ContextMetric):
|
|
395
|
+
on = ("valid",)
|
|
396
|
+
|
|
397
|
+
def __init__(self, model, latent, real="image", label=None, n=2000, extractor=None, batch=128):
|
|
398
|
+
self.model = model
|
|
399
|
+
self.latent = int(latent)
|
|
400
|
+
self.real = real
|
|
401
|
+
self.label = label
|
|
402
|
+
self.n = int(n)
|
|
403
|
+
self.extractor = extractor
|
|
404
|
+
self.batch = int(batch)
|
|
405
|
+
if self.batch < 1:
|
|
406
|
+
raise BuildError(f"fid: batch must be a positive int, got {batch!r}")
|
|
407
|
+
self.extract = extractor_of(extractor)
|
|
408
|
+
self.reset()
|
|
409
|
+
|
|
410
|
+
def reset(self):
|
|
411
|
+
self.real_features = []
|
|
412
|
+
self.fake_features = []
|
|
413
|
+
self.seen = 0
|
|
414
|
+
|
|
415
|
+
def field(self, batch, name):
|
|
416
|
+
if name not in batch:
|
|
417
|
+
raise RunError(f"{self.path or 'fid'}: the batch has no field {name!r}; it has {sorted(batch)}")
|
|
418
|
+
return batch[name]
|
|
419
|
+
|
|
420
|
+
def features_of(self, images):
|
|
421
|
+
return feature_rows(self.extract(images))
|
|
422
|
+
|
|
423
|
+
def fakes(self, noise, labels):
|
|
424
|
+
found = []
|
|
425
|
+
for start in range(0, len(noise), self.batch):
|
|
426
|
+
parts = [noise[start:start + self.batch]]
|
|
427
|
+
if labels is not None:
|
|
428
|
+
parts.append(labels[start:start + self.batch])
|
|
429
|
+
found.append(self.features_of(first_tensor(self.model(*parts))))
|
|
430
|
+
return found
|
|
431
|
+
|
|
432
|
+
def update(self, outputs, batch, ctx):
|
|
433
|
+
need = self.n - self.seen
|
|
434
|
+
if need <= 0:
|
|
435
|
+
return
|
|
436
|
+
real = self.field(batch, self.real)[:need]
|
|
437
|
+
count = len(real)
|
|
438
|
+
if not count:
|
|
439
|
+
return
|
|
440
|
+
labels = None if self.label is None else self.field(batch, self.label)[:count]
|
|
441
|
+
with evaluating(self.model):
|
|
442
|
+
self.real_features.append(self.features_of(real))
|
|
443
|
+
noise = ctx.rng.normal("fid", (count, self.latent), like=real)
|
|
444
|
+
self.fake_features.extend(self.fakes(noise, labels))
|
|
445
|
+
self.seen += count
|
|
446
|
+
|
|
447
|
+
def compute(self):
|
|
448
|
+
if not self.real_features or not self.fake_features:
|
|
449
|
+
return math.nan
|
|
450
|
+
return frechet_distance(numpy.concatenate(self.real_features), numpy.concatenate(self.fake_features))
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def perplexity(classes_last=None):
|
|
454
|
+
return Perplexity(classes_last)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
register("perplexity", "kalfa.std.metrics.perplexity", slot="metric",
|
|
458
|
+
doc="exp of the mean token cross entropy of logits (N, T, V), (N, V, T) or (N, V) against int targets; "
|
|
459
|
+
"classes_last: true or false states the layout when the tokens and the vocabulary have the same size")
|
|
460
|
+
register("fid", "kalfa.std.metrics.Fid", slot="metric",
|
|
461
|
+
doc="Frechet distance between features of n real images and n images of model(z) (or model(z, label)); "
|
|
462
|
+
"extractor is an import path of a function images -> features, the default pools pixels; valid only, "
|
|
463
|
+
"pair it with every to keep it cheap")
|
kalfa/std/models.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
|
|
3
|
+
from torch import nn
|
|
4
|
+
|
|
5
|
+
from kalfa.core import aliases
|
|
6
|
+
from kalfa.core.aliases import register
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
ACTIVATIONS = {
|
|
10
|
+
"relu": nn.ReLU,
|
|
11
|
+
"gelu": nn.GELU,
|
|
12
|
+
"tanh": nn.Tanh,
|
|
13
|
+
"silu": nn.SiLU,
|
|
14
|
+
"sigmoid": nn.Sigmoid,
|
|
15
|
+
"elu": nn.ELU,
|
|
16
|
+
"leaky_relu": nn.LeakyReLU,
|
|
17
|
+
"softplus": nn.Softplus,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def activation_of(activation):
|
|
22
|
+
if isinstance(activation, nn.Module):
|
|
23
|
+
return copy.deepcopy(activation)
|
|
24
|
+
if isinstance(activation, str):
|
|
25
|
+
found = ACTIVATIONS.get(activation)
|
|
26
|
+
if found is None:
|
|
27
|
+
raise ValueError(f"unknown activation {activation!r}; one of {sorted(ACTIVATIONS)}")
|
|
28
|
+
return found()
|
|
29
|
+
if callable(activation):
|
|
30
|
+
return activation()
|
|
31
|
+
raise TypeError(f"activation must be a name, a module or a module class, got {type(activation).__name__}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def linear_of(width, size):
|
|
35
|
+
return nn.LazyLinear(size) if width is None else nn.Linear(width, size)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Mlp(nn.Module):
|
|
39
|
+
def __init__(self, hidden, out, activation="relu", dropout=0.0, in_features=None):
|
|
40
|
+
super().__init__()
|
|
41
|
+
sizes = [int(hidden)] if isinstance(hidden, int) else [int(size) for size in hidden]
|
|
42
|
+
width = None if in_features is None else int(in_features)
|
|
43
|
+
layers = []
|
|
44
|
+
for size in sizes:
|
|
45
|
+
layers.append(linear_of(width, size))
|
|
46
|
+
layers.append(activation_of(activation))
|
|
47
|
+
if dropout and float(dropout) > 0.0:
|
|
48
|
+
layers.append(nn.Dropout(float(dropout)))
|
|
49
|
+
width = size
|
|
50
|
+
layers.append(linear_of(width, int(out)))
|
|
51
|
+
self.layers = nn.Sequential(*layers)
|
|
52
|
+
|
|
53
|
+
def forward(self, x):
|
|
54
|
+
return self.layers(x)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def layer_of(item):
|
|
58
|
+
if isinstance(item, nn.Module):
|
|
59
|
+
return item
|
|
60
|
+
if isinstance(item, str):
|
|
61
|
+
target = aliases.resolve(item)
|
|
62
|
+
if isinstance(target, str):
|
|
63
|
+
target = aliases.import_target(target)
|
|
64
|
+
return target()
|
|
65
|
+
if callable(item):
|
|
66
|
+
return item()
|
|
67
|
+
raise TypeError(f"a sequential layer is a module, a short name or a module class, got {type(item).__name__}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Sequential(nn.Module):
|
|
71
|
+
def __init__(self, layers):
|
|
72
|
+
super().__init__()
|
|
73
|
+
if isinstance(layers, (str, nn.Module)) or not hasattr(layers, "__iter__"):
|
|
74
|
+
raise TypeError("sequential takes layers: a list of modules or short names")
|
|
75
|
+
self.layers = nn.Sequential(*[layer_of(item) for item in layers])
|
|
76
|
+
|
|
77
|
+
def forward(self, *inputs):
|
|
78
|
+
value = inputs[0] if len(inputs) == 1 else inputs
|
|
79
|
+
return self.layers(value)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class LastStep(nn.Module):
|
|
83
|
+
def forward(self, value):
|
|
84
|
+
if isinstance(value, (tuple, list)):
|
|
85
|
+
value = value[0]
|
|
86
|
+
return value[:, -1]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
MODELS = (
|
|
90
|
+
("mlp", "kalfa.std.models.Mlp", "linear layers over hidden sizes with an activation, dropout and a final "
|
|
91
|
+
"linear to out; the first layer infers its input width when in_features is "
|
|
92
|
+
"not given"),
|
|
93
|
+
("stack", "kalfa.std.models.Sequential", "a stack of layers applied in order: call mappings such as "
|
|
94
|
+
"{use: linear, out_features: 8} or short names of argument free layers "
|
|
95
|
+
"such as relu, flatten or last_step"),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
LAYERS = (
|
|
99
|
+
("linear", "torch.nn.LazyLinear", "a linear layer that infers its input width: {use: linear, out_features: n}"),
|
|
100
|
+
("relu", "torch.nn.ReLU", "rectified linear activation"),
|
|
101
|
+
("gelu", "torch.nn.GELU", "gaussian error linear activation"),
|
|
102
|
+
("tanh", "torch.nn.Tanh", "hyperbolic tangent activation"),
|
|
103
|
+
("sigmoid", "torch.nn.Sigmoid", "sigmoid activation"),
|
|
104
|
+
("silu", "torch.nn.SiLU", "sigmoid weighted linear activation"),
|
|
105
|
+
("dropout", "torch.nn.Dropout", "dropout with probability p"),
|
|
106
|
+
("flatten", "torch.nn.Flatten", "flattens every dimension after the batch dimension"),
|
|
107
|
+
("layer_norm", "torch.nn.LayerNorm", "layer normalisation over normalized_shape"),
|
|
108
|
+
("embedding", "torch.nn.Embedding", "a lookup table: {use: embedding, num_embeddings: n, embedding_dim: d}"),
|
|
109
|
+
("batch_norm", "torch.nn.LazyBatchNorm1d", "batch normalisation over features, width inferred"),
|
|
110
|
+
("conv2d", "torch.nn.LazyConv2d", "a 2d convolution that infers its input channels"),
|
|
111
|
+
("last_step", "kalfa.std.models.LastStep", "the last time step of a batch first sequence output, also from the "
|
|
112
|
+
"(output, state) tuple a GRU or LSTM returns"),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def register_names():
|
|
116
|
+
for name, target, doc in MODELS:
|
|
117
|
+
register(name, target, slot="model", doc=doc)
|
|
118
|
+
for name, target, doc in LAYERS:
|
|
119
|
+
register(name, target, slot="value", doc=doc)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
register_names()
|