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/objectives.py
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import math
|
|
3
|
+
|
|
4
|
+
import torch
|
|
5
|
+
from torch import nn
|
|
6
|
+
from torch.nn import functional
|
|
7
|
+
|
|
8
|
+
from kalfa.core.aliases import register
|
|
9
|
+
from kalfa.core.errors import RunError
|
|
10
|
+
from kalfa.core.predict import batch_size, target_names
|
|
11
|
+
from kalfa.core.protocols import Objective
|
|
12
|
+
from kalfa.core.rng import Rng
|
|
13
|
+
from kalfa.core.wrap import loss_scalar
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
NOISE = "noise"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def title(owner):
|
|
20
|
+
name = getattr(owner, "name", "")
|
|
21
|
+
return f"loss {name!r}" if name else type(owner).__name__
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def named(obj):
|
|
25
|
+
return getattr(obj, "name", "") or getattr(obj, "__name__", "") or type(obj).__name__
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def field(batch, name, owner):
|
|
29
|
+
if name not in batch:
|
|
30
|
+
raise RunError(f"{title(owner)}: the batch has no field {name!r}; its fields are {sorted(map(str, batch))}")
|
|
31
|
+
return batch[name]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def first(result):
|
|
35
|
+
if isinstance(result, (tuple, list)):
|
|
36
|
+
return result[0]
|
|
37
|
+
if isinstance(result, dict):
|
|
38
|
+
return next(iter(result.values()))
|
|
39
|
+
return result
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def call(module, value, label):
|
|
43
|
+
return module(value) if label is None else module(value, label)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def device_of(module, batch):
|
|
47
|
+
for parameter in module.parameters():
|
|
48
|
+
return parameter.device
|
|
49
|
+
for value in batch.values() if isinstance(batch, dict) else ():
|
|
50
|
+
if isinstance(value, torch.Tensor):
|
|
51
|
+
return value.device
|
|
52
|
+
return torch.device("cpu")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def normal(ctx, shape, like=None, device=None):
|
|
56
|
+
rng = getattr(ctx, "rng", None)
|
|
57
|
+
if rng is not None:
|
|
58
|
+
return rng.normal(NOISE, shape, like=like, device=device)
|
|
59
|
+
if like is None:
|
|
60
|
+
return torch.randn(shape, device=device)
|
|
61
|
+
return torch.randn(shape, device=like.device, dtype=like.dtype if like.dtype.is_floating_point else None)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def uniform(ctx, shape, like):
|
|
65
|
+
rng = getattr(ctx, "rng", None)
|
|
66
|
+
if rng is not None:
|
|
67
|
+
return rng.uniform(NOISE, shape, like=like)
|
|
68
|
+
return torch.rand(shape, device=like.device, dtype=like.dtype if like.dtype.is_floating_point else None)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def integers(ctx, high, count, device):
|
|
72
|
+
rng = getattr(ctx, "rng", None)
|
|
73
|
+
if rng is not None:
|
|
74
|
+
return rng.integers(NOISE, 0, high, (count,), device=device)
|
|
75
|
+
return torch.randint(0, int(high), (count,), device=device)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def owned_modules(ctx):
|
|
79
|
+
found = []
|
|
80
|
+
for entry in (getattr(ctx, "models", None) or {}).values():
|
|
81
|
+
for module in (getattr(entry, "module", None), getattr(entry, "ema", None)):
|
|
82
|
+
if isinstance(module, nn.Module):
|
|
83
|
+
found.extend(module.modules())
|
|
84
|
+
return found
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def release(objective, ctx):
|
|
88
|
+
owned = owned_modules(ctx)
|
|
89
|
+
for name, module in list(objective.named_children()):
|
|
90
|
+
if any(module is part for part in owned):
|
|
91
|
+
delattr(objective, name)
|
|
92
|
+
object.__setattr__(objective, name, module)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def gaussian_kl(mu, logvar):
|
|
96
|
+
return -0.5 * torch.mean(torch.sum(1.0 + logvar - mu.pow(2) - logvar.exp(), dim=-1))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class BetaSchedule:
|
|
100
|
+
def __init__(self, steps):
|
|
101
|
+
self.steps = int(steps)
|
|
102
|
+
if self.steps < 1:
|
|
103
|
+
raise ValueError(f"a beta schedule needs at least one step, got {steps}")
|
|
104
|
+
self.betas = self.compute()
|
|
105
|
+
|
|
106
|
+
def compute(self):
|
|
107
|
+
raise NotImplementedError
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class LinearBeta(BetaSchedule):
|
|
111
|
+
def __init__(self, steps, start=1.0e-4, end=0.02):
|
|
112
|
+
self.start = float(start)
|
|
113
|
+
self.end = float(end)
|
|
114
|
+
if not (0.0 < self.start < 1.0 and 0.0 < self.end < 1.0):
|
|
115
|
+
raise ValueError(f"linear_beta needs start and end inside (0, 1), got {start} and {end}")
|
|
116
|
+
super().__init__(steps)
|
|
117
|
+
|
|
118
|
+
def compute(self):
|
|
119
|
+
return torch.linspace(self.start, self.end, self.steps, dtype=torch.float64).to(torch.float32)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class CosineBeta(BetaSchedule):
|
|
123
|
+
def __init__(self, steps, s=0.008):
|
|
124
|
+
self.s = float(s)
|
|
125
|
+
super().__init__(steps)
|
|
126
|
+
|
|
127
|
+
def compute(self):
|
|
128
|
+
grid = torch.arange(self.steps + 1, dtype=torch.float64) / self.steps
|
|
129
|
+
curve = torch.cos((grid + self.s) / (1.0 + self.s) * math.pi / 2.0) ** 2
|
|
130
|
+
cumulative = curve / curve[0]
|
|
131
|
+
betas = 1.0 - cumulative[1:] / cumulative[:-1]
|
|
132
|
+
return betas.clamp(1.0e-8, 0.999).to(torch.float32)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class NoiseTables:
|
|
136
|
+
def __init__(self, schedule):
|
|
137
|
+
self.schedule = schedule
|
|
138
|
+
self.steps = schedule.steps
|
|
139
|
+
self.betas = schedule.betas
|
|
140
|
+
self.alphas = 1.0 - self.betas
|
|
141
|
+
self.cumulative = torch.cumprod(self.alphas, dim=0)
|
|
142
|
+
self.placed = {}
|
|
143
|
+
|
|
144
|
+
def on(self, device, dtype=torch.float32):
|
|
145
|
+
where = Rng.place(device)
|
|
146
|
+
key = (str(where), dtype)
|
|
147
|
+
found = self.placed.get(key)
|
|
148
|
+
if found is None:
|
|
149
|
+
found = copy.copy(self)
|
|
150
|
+
found.betas = self.betas.to(where, dtype)
|
|
151
|
+
found.alphas = self.alphas.to(where, dtype)
|
|
152
|
+
found.cumulative = self.cumulative.to(where, dtype)
|
|
153
|
+
self.placed[key] = found
|
|
154
|
+
return found
|
|
155
|
+
|
|
156
|
+
def __iter__(self):
|
|
157
|
+
return iter((self.betas, self.alphas, self.cumulative))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class VaeElbo(Objective):
|
|
161
|
+
def __init__(self, recon, w_rec=1.0, w_kl=1.0, mu="mu", logvar="logvar"):
|
|
162
|
+
super().__init__()
|
|
163
|
+
self.recon = recon
|
|
164
|
+
self.w_rec = float(w_rec)
|
|
165
|
+
self.w_kl = w_kl
|
|
166
|
+
self.mu = mu
|
|
167
|
+
self.logvar = logvar
|
|
168
|
+
|
|
169
|
+
def weight(self, ctx):
|
|
170
|
+
if callable(self.w_kl):
|
|
171
|
+
return float(self.w_kl(getattr(ctx, "updates", 0)))
|
|
172
|
+
return float(self.w_kl)
|
|
173
|
+
|
|
174
|
+
def forward(self, outputs, batch, ctx):
|
|
175
|
+
outputs = {} if outputs is None else outputs
|
|
176
|
+
missing = [name for name in (self.mu, self.logvar) if name not in outputs]
|
|
177
|
+
if missing:
|
|
178
|
+
raise RunError(f"{title(self)}: the step outputs have no {missing}; they are {sorted(map(str, outputs))}; "
|
|
179
|
+
"name the encoder outputs with mu and logvar")
|
|
180
|
+
kl = gaussian_kl(outputs[self.mu].float(), outputs[self.logvar].float())
|
|
181
|
+
recon = loss_scalar(self.recon(outputs, batch, ctx), named(self.recon))
|
|
182
|
+
w_kl = self.weight(ctx)
|
|
183
|
+
return {"loss": self.w_rec * recon + w_kl * kl, "recon": recon, "kl": kl, "w_kl": w_kl}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class WganGpD(Objective):
|
|
187
|
+
on = ("train",)
|
|
188
|
+
|
|
189
|
+
def __init__(self, generator, critic, latent, real="image", label=None, gp_weight=10.0):
|
|
190
|
+
super().__init__()
|
|
191
|
+
self.generator = generator
|
|
192
|
+
self.critic = critic
|
|
193
|
+
self.latent = int(latent)
|
|
194
|
+
self.real = real
|
|
195
|
+
self.label = label
|
|
196
|
+
self.gp_weight = float(gp_weight)
|
|
197
|
+
|
|
198
|
+
def bind(self, ctx):
|
|
199
|
+
release(self, ctx)
|
|
200
|
+
|
|
201
|
+
def forward(self, outputs, batch, ctx):
|
|
202
|
+
real = field(batch, self.real, self).detach()
|
|
203
|
+
label = None if self.label is None else field(batch, self.label, self)
|
|
204
|
+
count = real.shape[0]
|
|
205
|
+
z = normal(ctx, (count, self.latent), device=real.device)
|
|
206
|
+
with torch.no_grad():
|
|
207
|
+
fake = first(call(self.generator, z, label)).detach()
|
|
208
|
+
if fake.shape != real.shape:
|
|
209
|
+
raise RunError(f"{title(self)}: the generator makes {tuple(fake.shape)} but the real field "
|
|
210
|
+
f"{self.real!r} is {tuple(real.shape)}")
|
|
211
|
+
score_real = first(call(self.critic, real, label))
|
|
212
|
+
score_fake = first(call(self.critic, fake, label))
|
|
213
|
+
with torch.enable_grad():
|
|
214
|
+
mix = uniform(ctx, (count,) + (1,) * (real.ndim - 1), like=real)
|
|
215
|
+
between = (mix * real + (1.0 - mix) * fake).requires_grad_(True)
|
|
216
|
+
score = first(call(self.critic, between, label))
|
|
217
|
+
gradients = torch.autograd.grad(score.sum(), between, create_graph=True)[0]
|
|
218
|
+
penalty = ((gradients.flatten(1).norm(dim=1) - 1.0) ** 2).mean()
|
|
219
|
+
wasserstein = score_real.mean() - score_fake.mean()
|
|
220
|
+
return {"loss": -wasserstein + self.gp_weight * penalty, "wasserstein": wasserstein, "gp": penalty}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class WganG(Objective):
|
|
224
|
+
def __init__(self, generator, critic, latent, label=None):
|
|
225
|
+
super().__init__()
|
|
226
|
+
self.generator = generator
|
|
227
|
+
self.critic = critic
|
|
228
|
+
self.latent = int(latent)
|
|
229
|
+
self.label = label
|
|
230
|
+
|
|
231
|
+
def bind(self, ctx):
|
|
232
|
+
release(self, ctx)
|
|
233
|
+
|
|
234
|
+
def forward(self, outputs, batch, ctx):
|
|
235
|
+
label = None if self.label is None else field(batch, self.label, self)
|
|
236
|
+
z = normal(ctx, (batch_size(batch), self.latent), device=device_of(self.generator, batch))
|
|
237
|
+
fake = first(call(self.generator, z, label))
|
|
238
|
+
return -first(call(self.critic, fake, label)).mean()
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class Ddpm(Objective):
|
|
242
|
+
def __init__(self, model, schedule, x="image"):
|
|
243
|
+
super().__init__()
|
|
244
|
+
self.model = model
|
|
245
|
+
self.schedule = schedule
|
|
246
|
+
self.x = x
|
|
247
|
+
self.tables = NoiseTables(schedule)
|
|
248
|
+
|
|
249
|
+
def bind(self, ctx):
|
|
250
|
+
release(self, ctx)
|
|
251
|
+
|
|
252
|
+
def forward(self, outputs, batch, ctx):
|
|
253
|
+
if self.tables.schedule is not self.schedule:
|
|
254
|
+
self.tables = NoiseTables(self.schedule)
|
|
255
|
+
x = field(batch, self.x, self)
|
|
256
|
+
tables = self.tables.on(x.device)
|
|
257
|
+
count = x.shape[0]
|
|
258
|
+
t = integers(ctx, tables.steps, count, x.device)
|
|
259
|
+
noise = normal(ctx, x.shape, like=x)
|
|
260
|
+
weight = tables.cumulative[t].to(x.dtype).reshape((count,) + (1,) * (x.ndim - 1))
|
|
261
|
+
noised = weight.sqrt() * x + (1.0 - weight).sqrt() * noise
|
|
262
|
+
predicted = first(self.model(noised, t))
|
|
263
|
+
return functional.mse_loss(predicted.float(), noise.float())
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class Distill(Objective):
|
|
267
|
+
def __init__(self, student, teacher, x=None, label=None, temperature=1.0, alpha=0.5):
|
|
268
|
+
super().__init__()
|
|
269
|
+
self.student = student
|
|
270
|
+
self.teacher = teacher
|
|
271
|
+
self.x = x
|
|
272
|
+
self.label = label
|
|
273
|
+
self.temperature = float(temperature)
|
|
274
|
+
self.alpha = float(alpha)
|
|
275
|
+
|
|
276
|
+
def bind(self, ctx):
|
|
277
|
+
release(self, ctx)
|
|
278
|
+
|
|
279
|
+
def input_name(self, ctx):
|
|
280
|
+
if self.x is not None:
|
|
281
|
+
return self.x
|
|
282
|
+
inputs = list(getattr(getattr(ctx, "data", None), "inputs", None) or ())
|
|
283
|
+
if not inputs:
|
|
284
|
+
raise RunError(f"{title(self)}: x is not written and the data declares no inputs; write x: <field>")
|
|
285
|
+
return inputs[0]
|
|
286
|
+
|
|
287
|
+
def label_name(self, ctx):
|
|
288
|
+
if self.label is not None:
|
|
289
|
+
return self.label
|
|
290
|
+
names = target_names(getattr(ctx, "data", None))
|
|
291
|
+
if len(names) != 1:
|
|
292
|
+
raise RunError(f"{title(self)}: label is not written and the data has {len(names)} targets {names}; "
|
|
293
|
+
"write label: <field>")
|
|
294
|
+
return names[0]
|
|
295
|
+
|
|
296
|
+
def forward(self, outputs, batch, ctx):
|
|
297
|
+
x = field(batch, self.input_name(ctx), self)
|
|
298
|
+
labels = field(batch, self.label_name(ctx), self).reshape(-1).long()
|
|
299
|
+
logits = first(self.student(x)).float()
|
|
300
|
+
with torch.no_grad():
|
|
301
|
+
guide = first(self.teacher(x)).float()
|
|
302
|
+
scale = self.temperature
|
|
303
|
+
soft = functional.softmax(guide / scale, dim=-1)
|
|
304
|
+
guess = functional.log_softmax(logits / scale, dim=-1)
|
|
305
|
+
kl = functional.kl_div(guess, soft, reduction="batchmean") * scale * scale
|
|
306
|
+
ce = functional.cross_entropy(logits, labels)
|
|
307
|
+
return {"loss": self.alpha * kl + (1.0 - self.alpha) * ce, "ce": ce, "kl": kl}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class Ntxent(Objective):
|
|
311
|
+
def __init__(self, encoder, projector=None, views="image", temperature=0.5):
|
|
312
|
+
super().__init__()
|
|
313
|
+
self.encoder = encoder
|
|
314
|
+
self.projector = projector
|
|
315
|
+
self.views = views
|
|
316
|
+
self.temperature = float(temperature)
|
|
317
|
+
|
|
318
|
+
def bind(self, ctx):
|
|
319
|
+
release(self, ctx)
|
|
320
|
+
|
|
321
|
+
def forward(self, outputs, batch, ctx):
|
|
322
|
+
views = field(batch, self.views, self)
|
|
323
|
+
if views.ndim < 3 or views.shape[1] != 2:
|
|
324
|
+
raise RunError(f"{title(self)}: ntxent needs two views per item, a (B, 2, ...) field, but {self.views!r} "
|
|
325
|
+
f"is {tuple(views.shape)}; put two_views in the train transform")
|
|
326
|
+
count = views.shape[0]
|
|
327
|
+
embedded = first(self.encoder(views.flatten(0, 1)))
|
|
328
|
+
if self.projector is not None:
|
|
329
|
+
embedded = first(self.projector(embedded))
|
|
330
|
+
z = functional.normalize(embedded.flatten(1).float(), dim=1)
|
|
331
|
+
similarity = z @ z.t() / self.temperature
|
|
332
|
+
diagonal = torch.eye(2 * count, device=z.device, dtype=torch.bool)
|
|
333
|
+
similarity = similarity.masked_fill(diagonal, float("-inf"))
|
|
334
|
+
partner = torch.arange(2 * count, device=z.device) ^ 1
|
|
335
|
+
return functional.cross_entropy(similarity, partner)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
REGISTRATIONS = (
|
|
339
|
+
("vae_elbo", "kalfa.std.objectives.VaeElbo", "loss",
|
|
340
|
+
"w_rec times the recon criterion on the outputs plus w_kl (a number or a schedule of the update count) times the "
|
|
341
|
+
"gaussian KL of the mu and logvar outputs; terms recon, kl, w_kl"),
|
|
342
|
+
("wgan_gp_d", "kalfa.std.objectives.WganGpD", "loss",
|
|
343
|
+
"the critic loss of WGAN GP: fake minus real scores plus gp_weight times the gradient penalty on interpolates; "
|
|
344
|
+
"the latent noise comes from the noise stream; terms wasserstein, gp; train only by default"),
|
|
345
|
+
("wgan_g", "kalfa.std.objectives.WganG", "loss",
|
|
346
|
+
"the generator loss of WGAN: minus the critic score of fresh fakes"),
|
|
347
|
+
("ddpm", "kalfa.std.objectives.Ddpm", "loss",
|
|
348
|
+
"the denoising loss: noise the x field at a random step of the beta schedule and regress the noise with "
|
|
349
|
+
"model(x_t, t)"),
|
|
350
|
+
("distill", "kalfa.std.objectives.Distill", "loss",
|
|
351
|
+
"alpha times the KL between student and teacher logits at temperature plus (1 - alpha) times cross entropy with "
|
|
352
|
+
"the label field (default the only target of the data); terms ce, kl"),
|
|
353
|
+
("ntxent", "kalfa.std.objectives.Ntxent", "loss",
|
|
354
|
+
"NT-Xent over two views per image: the encoder (and projector) embeds both views, partners are positives"),
|
|
355
|
+
("linear_beta", "kalfa.std.objectives.LinearBeta", "value", "betas rising linearly from start to end over steps"),
|
|
356
|
+
("cosine_beta", "kalfa.std.objectives.CosineBeta", "value", "the cosine beta schedule of Nichol and Dhariwal"),
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
for name, target, slot, doc in REGISTRATIONS:
|
|
360
|
+
register(name, target, slot=slot, doc=doc)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from kalfa.core.aliases import register
|
|
2
|
+
from kalfa.std.plots.classification import (
|
|
3
|
+
MAX_CLASSES,
|
|
4
|
+
SKEW,
|
|
5
|
+
binary_curve,
|
|
6
|
+
binary_precision_recall_curve,
|
|
7
|
+
binary_roc,
|
|
8
|
+
class_histogram,
|
|
9
|
+
classified,
|
|
10
|
+
confusion_matrix,
|
|
11
|
+
floats,
|
|
12
|
+
label_set,
|
|
13
|
+
score_column,
|
|
14
|
+
)
|
|
15
|
+
from kalfa.std.plots.common import (
|
|
16
|
+
GRID,
|
|
17
|
+
INK,
|
|
18
|
+
LEGEND_LIMIT,
|
|
19
|
+
MUTED,
|
|
20
|
+
color_of,
|
|
21
|
+
directory_of,
|
|
22
|
+
dress,
|
|
23
|
+
label_text,
|
|
24
|
+
quiet,
|
|
25
|
+
spans_decades,
|
|
26
|
+
)
|
|
27
|
+
from kalfa.std.plots.curves import DEFAULT_PREFIXES, history_series, loss_curve, turn_word
|
|
28
|
+
from kalfa.std.plots.images import (
|
|
29
|
+
TEXT_LINES,
|
|
30
|
+
TEXT_WIDTH,
|
|
31
|
+
first_output,
|
|
32
|
+
image_field,
|
|
33
|
+
image_pairs,
|
|
34
|
+
pixels,
|
|
35
|
+
sample_images,
|
|
36
|
+
samples_grid,
|
|
37
|
+
shaped,
|
|
38
|
+
show,
|
|
39
|
+
text_sheet,
|
|
40
|
+
)
|
|
41
|
+
from kalfa.std.plots.pairs import chosen_pair, pick_pair, pred_column, prediction_pairs, read_predictions
|
|
42
|
+
from kalfa.std.plots.predictions import (
|
|
43
|
+
HORIZON,
|
|
44
|
+
forecast_samples,
|
|
45
|
+
forecast_series,
|
|
46
|
+
horizon_pair,
|
|
47
|
+
indexed,
|
|
48
|
+
numeric,
|
|
49
|
+
pred_vs_true,
|
|
50
|
+
r_squared,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
__all__ = ["DEFAULT_PREFIXES", "GRID", "HORIZON", "INK", "LEGEND_LIMIT", "MAX_CLASSES", "MUTED", "SKEW", "TEXT_LINES",
|
|
55
|
+
"TEXT_WIDTH", "binary_curve", "binary_precision_recall_curve", "binary_roc", "chosen_pair",
|
|
56
|
+
"class_histogram", "classified", "color_of", "confusion_matrix", "directory_of", "dress", "first_output",
|
|
57
|
+
"floats", "forecast_samples", "forecast_series", "history_series", "horizon_pair", "image_field",
|
|
58
|
+
"image_pairs", "indexed", "label_set", "label_text", "loss_curve", "numeric", "pick_pair", "pixels",
|
|
59
|
+
"pred_column", "pred_vs_true", "prediction_pairs", "quiet", "r_squared", "read_predictions",
|
|
60
|
+
"sample_images", "samples_grid", "score_column", "shaped", "show", "spans_decades", "text_sheet",
|
|
61
|
+
"turn_word"]
|
|
62
|
+
|
|
63
|
+
for _name, _doc in (
|
|
64
|
+
("loss_curve", "history.jsonl series against the turn; series names the keys, default every train/ and val/ key"),
|
|
65
|
+
("pred_vs_true", "scatter of each pred_<output> against its target with a y = x line; pairs maps output to target"),
|
|
66
|
+
("confusion_matrix", "confusion matrix of a label output against its target with counts; pred and target name the "
|
|
67
|
+
"pair"),
|
|
68
|
+
("image_pairs", "n test images above their first model output (reconstructions); needs the run"),
|
|
69
|
+
("samples_grid", "a grid of the first n images in samples/<file>.pt, or the text of samples/<file>.txt"),
|
|
70
|
+
("forecast_samples", "n windows of a horizon target: the truth and the prediction over the horizon steps"),
|
|
71
|
+
("class_histogram", "the histogram of the positive class score per true class"),
|
|
72
|
+
("binary_roc", "the ROC curve of a binary classification with its AUC"),
|
|
73
|
+
("binary_precision_recall_curve", "the precision recall curve of a binary classification with its AP"),
|
|
74
|
+
):
|
|
75
|
+
register(_name, f"kalfa.std.plots.{_name}", slot="plot", doc=_doc)
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import numpy
|
|
2
|
+
|
|
3
|
+
from kalfa.core.errors import KalfaError
|
|
4
|
+
from kalfa.records import reader, stats
|
|
5
|
+
from kalfa.std.plots.common import INK, LEGEND_LIMIT, MUTED, color_of, directory_of, dress, label_text, quiet
|
|
6
|
+
from kalfa.std.plots.pairs import chosen_pair, pick_pair, read_predictions
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
MAX_CLASSES = 64
|
|
10
|
+
SKEW = 20.0
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def label_set(truth, guess):
|
|
14
|
+
values = set(truth.tolist()) | set(guess.tolist())
|
|
15
|
+
try:
|
|
16
|
+
return sorted(values)
|
|
17
|
+
except TypeError:
|
|
18
|
+
return sorted(values, key=str)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def confusion_matrix(record, figures, pred=None, target=None):
|
|
22
|
+
directory = directory_of(record)
|
|
23
|
+
frame = read_predictions(directory)
|
|
24
|
+
if frame is None:
|
|
25
|
+
return None
|
|
26
|
+
pair = pick_pair(frame, quiet(reader.read_plan, directory), pred, target)
|
|
27
|
+
if pair is None:
|
|
28
|
+
return None
|
|
29
|
+
column, truth_column = pair
|
|
30
|
+
keep = frame[column].notna() & frame[truth_column].notna()
|
|
31
|
+
guess = frame.loc[keep, column].to_numpy()
|
|
32
|
+
truth = frame.loc[keep, truth_column].to_numpy()
|
|
33
|
+
labels = label_set(truth, guess)
|
|
34
|
+
if not labels or len(labels) > MAX_CLASSES:
|
|
35
|
+
return None
|
|
36
|
+
index = {label: position for position, label in enumerate(labels)}
|
|
37
|
+
matrix = numpy.zeros((len(labels), len(labels)), dtype="int64")
|
|
38
|
+
for true_value, guess_value in zip(truth.tolist(), guess.tolist()):
|
|
39
|
+
matrix[index[true_value], index[guess_value]] += 1
|
|
40
|
+
shares = matrix / numpy.maximum(matrix.sum(axis=1, keepdims=True), 1)
|
|
41
|
+
side = min(16.0, 2.0 + 0.7 * len(labels))
|
|
42
|
+
figure = figures.new(side + 1.2, side)
|
|
43
|
+
axis = figure.subplots()
|
|
44
|
+
drawn = axis.imshow(shares, cmap="Blues", vmin=0.0, vmax=1.0)
|
|
45
|
+
for row in range(len(labels)):
|
|
46
|
+
for col in range(len(labels)):
|
|
47
|
+
axis.text(col, row, f"{matrix[row, col]:,}\n{shares[row, col] * 100:.0f}%", ha="center", va="center",
|
|
48
|
+
fontsize=8, color="#ffffff" if shares[row, col] > 0.55 else INK)
|
|
49
|
+
names = [label_text(label) for label in labels]
|
|
50
|
+
tilt = len(names) > 6 or max(len(name) for name in names) > 6
|
|
51
|
+
axis.set_xticks(range(len(names)), names, rotation=45 if tilt else 0, ha="right" if tilt else "center")
|
|
52
|
+
axis.set_yticks(range(len(names)), names)
|
|
53
|
+
axis.grid(False)
|
|
54
|
+
figure.colorbar(drawn, ax=axis, fraction=0.046, pad=0.03, label="row share")
|
|
55
|
+
axis.set_title(f"Confusion matrix ({truth_column}, n = {int(matrix.sum()):,})", loc="left")
|
|
56
|
+
axis.set_xlabel(f"predicted ({column[len('pred_'):]})")
|
|
57
|
+
axis.set_ylabel(f"true {truth_column}")
|
|
58
|
+
figures.save(figure, "confusion_matrix")
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def classified(directory, plot, pred, target):
|
|
63
|
+
frame = stats.read_predictions(directory)
|
|
64
|
+
if frame is None or not len(frame):
|
|
65
|
+
return None
|
|
66
|
+
plan = quiet(reader.read_plan, directory)
|
|
67
|
+
fitted = quiet(reader.fields_of, directory)
|
|
68
|
+
pair = chosen_pair(frame, plan, fitted, pred, target, prefer="classification")
|
|
69
|
+
if pair is None:
|
|
70
|
+
return None
|
|
71
|
+
if pair["kind"] != "classification":
|
|
72
|
+
raise KalfaError(f"{plot}: the predictions carry no probabilities for {pair['pred']}; its target "
|
|
73
|
+
f"{pair['target']} is a regression target, not a label")
|
|
74
|
+
return frame, plan, fitted, pair
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def score_column(pair, classes):
|
|
78
|
+
scores = pair["scores"]
|
|
79
|
+
if scores:
|
|
80
|
+
named = stats.positive_column(scores, pair["output"], classes) if classes and len(classes) == 2 else None
|
|
81
|
+
return named or scores[-1]
|
|
82
|
+
return pair["raw"]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def class_histogram(record, figures, pred=None, target=None, bins=40):
|
|
86
|
+
import pandas
|
|
87
|
+
from matplotlib.colors import to_rgba
|
|
88
|
+
|
|
89
|
+
directory = directory_of(record)
|
|
90
|
+
found = classified(directory, "class_histogram", pred, target)
|
|
91
|
+
if found is None:
|
|
92
|
+
return None
|
|
93
|
+
frame, plan, fitted, pair = found
|
|
94
|
+
output, truth_column = pair["output"], pair["target"]
|
|
95
|
+
classes = stats.classes_for(plan, fitted, truth_column)
|
|
96
|
+
column = score_column(pair, classes)
|
|
97
|
+
if column is None:
|
|
98
|
+
raise KalfaError(f"class_histogram: the predictions carry no probabilities (prob_{output}_<class>) and no "
|
|
99
|
+
f"raw_{output} scores for {pair['pred']}")
|
|
100
|
+
scores = pandas.to_numeric(frame[column], errors="coerce").to_numpy(dtype="float64")
|
|
101
|
+
known = frame[truth_column].notna().to_numpy()
|
|
102
|
+
keep = numpy.isfinite(scores) & known
|
|
103
|
+
if not keep.any():
|
|
104
|
+
return None
|
|
105
|
+
scores = scores[keep]
|
|
106
|
+
truth = numpy.asarray([label_text(value) for value in frame.loc[keep, truth_column].tolist()], dtype=object)
|
|
107
|
+
seen = sorted({str(value) for value in truth.tolist()})
|
|
108
|
+
order = list(classes or [])
|
|
109
|
+
labels = [label for label in order if label in seen] + [label for label in seen if label not in order]
|
|
110
|
+
if len(labels) > MAX_CLASSES:
|
|
111
|
+
return None
|
|
112
|
+
if column.startswith("prob_") and scores.min() >= 0.0 and scores.max() <= 1.0:
|
|
113
|
+
edges = numpy.linspace(0.0, 1.0, int(bins) + 1)
|
|
114
|
+
else:
|
|
115
|
+
edges = numpy.histogram_bin_edges(scores, bins=int(bins))
|
|
116
|
+
counts = [numpy.histogram(scores[truth == label], bins=edges)[0] for label in labels]
|
|
117
|
+
peaks = [int(count.max()) for count in counts if count.max() > 0]
|
|
118
|
+
log = len(peaks) > 1 and max(peaks) / min(peaks) > SKEW
|
|
119
|
+
figure = figures.new(8.0, 5.0)
|
|
120
|
+
axis = figure.subplots()
|
|
121
|
+
for position, (label, count) in enumerate(zip(labels, counts)):
|
|
122
|
+
color = color_of(figures, position)
|
|
123
|
+
axis.stairs(count, edges, fill=True, baseline=0.5 if log else 0.0, facecolor=to_rgba(color, 0.3),
|
|
124
|
+
edgecolor=color, linewidth=1.4, label=f"{label} (n = {int(count.sum()):,})")
|
|
125
|
+
if log:
|
|
126
|
+
axis.set_yscale("log")
|
|
127
|
+
axis.set_ylim(bottom=0.5)
|
|
128
|
+
if len(labels) <= LEGEND_LIMIT:
|
|
129
|
+
axis.legend(loc="upper center", frameon=False, title=f"true {truth_column}")
|
|
130
|
+
prefix = f"prob_{output}_"
|
|
131
|
+
axis.set_xlabel(f"P({column[len(prefix):]})" if column.startswith(prefix) else column)
|
|
132
|
+
axis.set_ylabel("rows")
|
|
133
|
+
axis.set_title(f"Score by true class ({truth_column}, n = {len(scores):,})", loc="left")
|
|
134
|
+
dress(axis)
|
|
135
|
+
figures.save(figure, "class_histogram")
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def floats(values):
|
|
140
|
+
return numpy.asarray([numpy.nan if value is None else value for value in values], dtype="float64")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def binary_curve(record, plot, pred, target):
|
|
144
|
+
found = classified(directory_of(record), plot, pred, target)
|
|
145
|
+
if found is None:
|
|
146
|
+
return None
|
|
147
|
+
frame, plan, fitted, pair = found
|
|
148
|
+
output, truth_column = pair["output"], pair["target"]
|
|
149
|
+
if not pair["scores"]:
|
|
150
|
+
raise KalfaError(f"{plot}: the predictions carry no probabilities for {pair['pred']}; a binary curve reads "
|
|
151
|
+
f"the prob_{output}_<class> columns that a label target writes")
|
|
152
|
+
classes = stats.classes_for(plan, fitted, truth_column)
|
|
153
|
+
result = stats.classification_stats(frame, pair["pred"], truth_column, pair["raw"], pair["scores"], classes)
|
|
154
|
+
if len(result["classes"]) != 2:
|
|
155
|
+
raise KalfaError(f"{plot} needs two classes; {truth_column} has {len(result['classes'])}: "
|
|
156
|
+
f"{', '.join(result['classes'])}")
|
|
157
|
+
if result["roc"] is None:
|
|
158
|
+
return None
|
|
159
|
+
return result, pair
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def binary_roc(record, figures, pred=None, target=None):
|
|
163
|
+
found = binary_curve(record, "binary_roc", pred, target)
|
|
164
|
+
if found is None:
|
|
165
|
+
return None
|
|
166
|
+
result, pair = found
|
|
167
|
+
roc = result["roc"]
|
|
168
|
+
figure = figures.new(5.6, 5.0)
|
|
169
|
+
axis = figure.subplots()
|
|
170
|
+
axis.plot(floats(roc["fpr"]), floats(roc["tpr"]), color=color_of(figures, 0), linewidth=2.0,
|
|
171
|
+
label=f"{pair['output']} (AUC {roc['auc']:.3f})")
|
|
172
|
+
axis.plot([0.0, 1.0], [0.0, 1.0], color=MUTED, linewidth=1.0, linestyle="--", label="chance")
|
|
173
|
+
axis.set_xlim(0.0, 1.0)
|
|
174
|
+
axis.set_ylim(0.0, 1.02)
|
|
175
|
+
axis.legend(loc="lower right", frameon=False)
|
|
176
|
+
positive = result["classes"][1]
|
|
177
|
+
axis.set_title(f"ROC ({pair['target']} = {positive}, AUC {roc['auc']:.3f}, n = {result['count']:,})", loc="left")
|
|
178
|
+
axis.set_xlabel("false positive rate")
|
|
179
|
+
axis.set_ylabel("true positive rate")
|
|
180
|
+
dress(axis)
|
|
181
|
+
figures.save(figure, "binary_roc")
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def binary_precision_recall_curve(record, figures, pred=None, target=None):
|
|
186
|
+
found = binary_curve(record, "binary_precision_recall_curve", pred, target)
|
|
187
|
+
if found is None:
|
|
188
|
+
return None
|
|
189
|
+
result, pair = found
|
|
190
|
+
pr = result["pr"]
|
|
191
|
+
positive = result["classes"][1]
|
|
192
|
+
rate = result["per_class"][positive]["support"] / result["count"]
|
|
193
|
+
figure = figures.new(5.6, 5.0)
|
|
194
|
+
axis = figure.subplots()
|
|
195
|
+
axis.step(floats(pr["recall"]), floats(pr["precision"]), where="post", color=color_of(figures, 0), linewidth=2.0,
|
|
196
|
+
label=f"{pair['output']} (AP {pr['ap']:.3f})")
|
|
197
|
+
axis.axhline(rate, color=MUTED, linewidth=1.0, linestyle="--", label=f"base rate {rate:.3f}")
|
|
198
|
+
axis.set_xlim(0.0, 1.0)
|
|
199
|
+
axis.set_ylim(0.0, 1.02)
|
|
200
|
+
axis.legend(loc="lower left", frameon=False)
|
|
201
|
+
axis.set_title(f"Precision and recall ({pair['target']} = {positive}, AP {pr['ap']:.3f}, "
|
|
202
|
+
f"n = {result['count']:,})", loc="left")
|
|
203
|
+
axis.set_xlabel("recall")
|
|
204
|
+
axis.set_ylabel("precision")
|
|
205
|
+
dress(axis)
|
|
206
|
+
figures.save(figure, "binary_precision_recall_curve")
|
|
207
|
+
return None
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
LEGEND_LIMIT = 12
|
|
6
|
+
GRID = "#e3e2dd"
|
|
7
|
+
INK = "#0b0b0b"
|
|
8
|
+
MUTED = "#8c8b86"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def directory_of(record):
|
|
12
|
+
return Path(getattr(record, "directory", record))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def quiet(read, directory):
|
|
16
|
+
try:
|
|
17
|
+
return read(directory)
|
|
18
|
+
except (OSError, ValueError):
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def color_of(figures, position):
|
|
23
|
+
colors = getattr(figures, "colors", None) or tuple(f"C{index}" for index in range(10))
|
|
24
|
+
return colors[position % len(colors)]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def dress(axis):
|
|
28
|
+
axis.grid(True, color=GRID, linewidth=0.8)
|
|
29
|
+
axis.set_axisbelow(True)
|
|
30
|
+
for side in ("top", "right"):
|
|
31
|
+
axis.spines[side].set_visible(False)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def spans_decades(values):
|
|
35
|
+
finite = [value for value in values if math.isfinite(value)]
|
|
36
|
+
if not finite or any(value <= 0 for value in finite):
|
|
37
|
+
return False
|
|
38
|
+
return max(finite) / min(finite) > 100.0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def label_text(value):
|
|
42
|
+
if isinstance(value, float) and value.is_integer():
|
|
43
|
+
return str(int(value))
|
|
44
|
+
return str(value)
|