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/samplers.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
|
|
3
|
+
import torch
|
|
4
|
+
from torch import nn
|
|
5
|
+
|
|
6
|
+
from kalfa.core.aliases import register
|
|
7
|
+
from kalfa.core.errors import RunError
|
|
8
|
+
from kalfa.core.rng import Rng
|
|
9
|
+
from kalfa.std.common import evaluating
|
|
10
|
+
from kalfa.std.data.text import size_of
|
|
11
|
+
from kalfa.std.objectives import NoiseTables
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
STREAM = "samples"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def module_of(model):
|
|
18
|
+
if isinstance(model, nn.Module):
|
|
19
|
+
return model
|
|
20
|
+
inner = getattr(model, "module", None)
|
|
21
|
+
if isinstance(inner, nn.Module):
|
|
22
|
+
return inner
|
|
23
|
+
if callable(model):
|
|
24
|
+
return model
|
|
25
|
+
raise RunError(f"a sampler needs a model to call, got {type(model).__name__}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def device_of(ctx, model):
|
|
29
|
+
device = getattr(ctx, "device", None)
|
|
30
|
+
if device is not None:
|
|
31
|
+
return Rng.place(device)
|
|
32
|
+
if isinstance(model, nn.Module):
|
|
33
|
+
for tensor in itertools.chain(model.parameters(), model.buffers()):
|
|
34
|
+
return tensor.device
|
|
35
|
+
return torch.device("cpu")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def count_of(sampler, n):
|
|
39
|
+
count = int(sampler.n if n is None else n)
|
|
40
|
+
if count < 1:
|
|
41
|
+
raise RunError(f"{type(sampler).__name__} draws n samples; n must be positive, got {count}")
|
|
42
|
+
return count
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def rng_of(ctx):
|
|
46
|
+
return getattr(ctx, "rng", None)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def normal(ctx, shape, device):
|
|
50
|
+
rng = rng_of(ctx)
|
|
51
|
+
if rng is None:
|
|
52
|
+
return torch.randn(shape, device=device)
|
|
53
|
+
return rng.normal(STREAM, shape, device=device)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def first(result):
|
|
57
|
+
if isinstance(result, dict):
|
|
58
|
+
result = next(iter(result.values()))
|
|
59
|
+
while isinstance(result, (tuple, list)):
|
|
60
|
+
result = result[0]
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def finished(result):
|
|
65
|
+
result = first(result)
|
|
66
|
+
if isinstance(result, torch.Tensor):
|
|
67
|
+
return result.detach().cpu()
|
|
68
|
+
return result
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class GanSampler:
|
|
72
|
+
def __init__(self, model, latent, n_classes=None, n=64):
|
|
73
|
+
self.model = model
|
|
74
|
+
self.latent = int(latent)
|
|
75
|
+
self.n_classes = None if n_classes is None else int(n_classes)
|
|
76
|
+
self.n = int(n)
|
|
77
|
+
|
|
78
|
+
def sample(self, ctx, n=None):
|
|
79
|
+
count = count_of(self, n)
|
|
80
|
+
model = module_of(self.model)
|
|
81
|
+
device = device_of(ctx, model)
|
|
82
|
+
with evaluating(model):
|
|
83
|
+
z = normal(ctx, (count, self.latent), device)
|
|
84
|
+
if self.n_classes:
|
|
85
|
+
labels = torch.arange(count, device=device) % self.n_classes
|
|
86
|
+
return finished(model(z, labels))
|
|
87
|
+
return finished(model(z))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class LatentSampler:
|
|
91
|
+
def __init__(self, model, latent, n=64):
|
|
92
|
+
self.model = model
|
|
93
|
+
self.latent = int(latent)
|
|
94
|
+
self.n = int(n)
|
|
95
|
+
|
|
96
|
+
def sample(self, ctx, n=None):
|
|
97
|
+
count = count_of(self, n)
|
|
98
|
+
model = module_of(self.model)
|
|
99
|
+
device = device_of(ctx, model)
|
|
100
|
+
with evaluating(model):
|
|
101
|
+
return finished(model(normal(ctx, (count, self.latent), device)))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class DdpmSampler:
|
|
105
|
+
def __init__(self, model, schedule, shape, n=64, steps=None):
|
|
106
|
+
self.model = model
|
|
107
|
+
self.schedule = schedule
|
|
108
|
+
self.shape = tuple(int(part) for part in shape)
|
|
109
|
+
self.n = int(n)
|
|
110
|
+
self.steps = None if steps is None else int(steps)
|
|
111
|
+
self.noise = None
|
|
112
|
+
|
|
113
|
+
def tables(self, device):
|
|
114
|
+
source = self.schedule
|
|
115
|
+
if not isinstance(source, NoiseTables):
|
|
116
|
+
if self.noise is None or self.noise.schedule is not source:
|
|
117
|
+
self.noise = NoiseTables(source)
|
|
118
|
+
source = self.noise
|
|
119
|
+
placed = source.on(device)
|
|
120
|
+
return int(placed.steps), placed.betas, placed.alphas, placed.cumulative
|
|
121
|
+
|
|
122
|
+
def timesteps(self, total):
|
|
123
|
+
if self.steps is None:
|
|
124
|
+
return list(range(total - 1, -1, -1))
|
|
125
|
+
if self.steps < 1:
|
|
126
|
+
raise RunError(f"ddpm_sampler steps must be positive, got {self.steps}")
|
|
127
|
+
count = min(self.steps, total)
|
|
128
|
+
if count == 1:
|
|
129
|
+
return [total - 1]
|
|
130
|
+
spaced = torch.linspace(total - 1, 0, count, dtype=torch.float64).round().long().tolist()
|
|
131
|
+
return list(dict.fromkeys(int(step) for step in spaced))
|
|
132
|
+
|
|
133
|
+
def sample(self, ctx, n=None):
|
|
134
|
+
count = count_of(self, n)
|
|
135
|
+
model = module_of(self.model)
|
|
136
|
+
device = device_of(ctx, model)
|
|
137
|
+
total, betas, alphas, cumulative = self.tables(device)
|
|
138
|
+
size = (count, *self.shape)
|
|
139
|
+
schedule = self.timesteps(total)
|
|
140
|
+
with evaluating(model):
|
|
141
|
+
x = normal(ctx, size, device)
|
|
142
|
+
for position, step in enumerate(schedule):
|
|
143
|
+
t = torch.full((count,), step, dtype=torch.long, device=device)
|
|
144
|
+
predicted = first(model(x, t)).to(x.dtype)
|
|
145
|
+
if self.steps is None:
|
|
146
|
+
x = self.ancestral(ctx, x, predicted, step, betas, alphas, cumulative, size, device)
|
|
147
|
+
else:
|
|
148
|
+
following = schedule[position + 1] if position + 1 < len(schedule) else None
|
|
149
|
+
x = self.implicit(x, predicted, step, following, cumulative)
|
|
150
|
+
return x.clamp(-1.0, 1.0).detach().cpu()
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def ancestral(ctx, x, predicted, step, betas, alphas, cumulative, size, device):
|
|
154
|
+
coefficient = betas[step] / (1.0 - cumulative[step]).sqrt()
|
|
155
|
+
x = (x - coefficient * predicted) / alphas[step].sqrt()
|
|
156
|
+
if step > 0:
|
|
157
|
+
x = x + betas[step].sqrt() * normal(ctx, size, device)
|
|
158
|
+
return x
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def implicit(x, predicted, step, following, cumulative):
|
|
162
|
+
current = cumulative[step]
|
|
163
|
+
previous = cumulative[following] if following is not None else torch.ones_like(current)
|
|
164
|
+
start = ((x - (1.0 - current).sqrt() * predicted) / current.sqrt()).clamp(-1.0, 1.0)
|
|
165
|
+
return previous.sqrt() * start + (1.0 - previous).sqrt() * predicted
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class LmSampler:
|
|
169
|
+
def __init__(self, model, tokenizer, prompt="", max_new_tokens=100, temperature=1.0, top_k=None, n=1):
|
|
170
|
+
self.model = model
|
|
171
|
+
self.tokenizer = tokenizer
|
|
172
|
+
self.prompt = str(prompt)
|
|
173
|
+
self.max_new_tokens = int(max_new_tokens)
|
|
174
|
+
self.temperature = float(temperature)
|
|
175
|
+
self.top_k = None if top_k is None else int(top_k)
|
|
176
|
+
self.n = int(n)
|
|
177
|
+
|
|
178
|
+
def tokenizer_of(self, ctx):
|
|
179
|
+
tokenizer = self.tokenizer
|
|
180
|
+
if tokenizer is None or isinstance(tokenizer, str):
|
|
181
|
+
tokenizer = getattr(getattr(ctx, "data", None), "tokenizer", None)
|
|
182
|
+
if tokenizer is None or isinstance(tokenizer, str) or not callable(getattr(tokenizer, "decode", None)):
|
|
183
|
+
raise RunError("lm_sampler needs a tokenizer with encode and decode; write tokenizer: \"@data.tokenizer\"")
|
|
184
|
+
return tokenizer
|
|
185
|
+
|
|
186
|
+
@staticmethod
|
|
187
|
+
def window_of(ctx):
|
|
188
|
+
limit = getattr(getattr(ctx, "data", None), "seq_len", None)
|
|
189
|
+
if isinstance(limit, int) and not isinstance(limit, bool) and limit > 0:
|
|
190
|
+
return limit
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def logits_of(result, size=None, length=None):
|
|
195
|
+
if isinstance(result, dict) and "logits" in result:
|
|
196
|
+
result = result["logits"]
|
|
197
|
+
result = first(result)
|
|
198
|
+
if not isinstance(result, torch.Tensor) or result.ndim not in (2, 3):
|
|
199
|
+
shape = tuple(result.shape) if isinstance(result, torch.Tensor) else type(result).__name__
|
|
200
|
+
raise RunError(f"lm_sampler needs logits of shape (batch, tokens, vocabulary) from the model, got {shape}")
|
|
201
|
+
if result.ndim == 3:
|
|
202
|
+
result = result[:, -1, :] if LmSampler.classes_last(result.shape, size, length) else result[:, :, -1]
|
|
203
|
+
return result.float()
|
|
204
|
+
|
|
205
|
+
@staticmethod
|
|
206
|
+
def classes_last(shape, size, length):
|
|
207
|
+
tokens, classes = shape[1], shape[2]
|
|
208
|
+
if tokens == length:
|
|
209
|
+
return True
|
|
210
|
+
if classes == length:
|
|
211
|
+
return False
|
|
212
|
+
if size is None or classes == size:
|
|
213
|
+
return True
|
|
214
|
+
if tokens == size:
|
|
215
|
+
return False
|
|
216
|
+
raise RunError(f"lm_sampler cannot find the vocabulary axis of logits of shape {tuple(shape)}: the model "
|
|
217
|
+
f"read {length} tokens and the tokenizer has {size}; give (batch, tokens, vocabulary) or "
|
|
218
|
+
"(batch, vocabulary, tokens)")
|
|
219
|
+
|
|
220
|
+
def pick(self, logits, generator):
|
|
221
|
+
if self.temperature <= 0:
|
|
222
|
+
return logits.argmax(dim=-1, keepdim=True)
|
|
223
|
+
logits = logits / self.temperature
|
|
224
|
+
if self.top_k:
|
|
225
|
+
keep = min(self.top_k, logits.shape[-1])
|
|
226
|
+
least = torch.topk(logits, keep, dim=-1).values[:, -1:]
|
|
227
|
+
logits = logits.masked_fill(logits < least, float("-inf"))
|
|
228
|
+
return torch.multinomial(torch.softmax(logits, dim=-1), 1, generator=generator)
|
|
229
|
+
|
|
230
|
+
def sample(self, ctx, n=None):
|
|
231
|
+
count = count_of(self, n)
|
|
232
|
+
tokenizer = self.tokenizer_of(ctx)
|
|
233
|
+
size = size_of(tokenizer)
|
|
234
|
+
model = module_of(self.model)
|
|
235
|
+
device = device_of(ctx, model)
|
|
236
|
+
prompt = [int(token) for token in tokenizer.encode(self.prompt)] if self.prompt else []
|
|
237
|
+
skip = 0 if prompt else 1
|
|
238
|
+
ids = torch.tensor([prompt or [0]] * count, dtype=torch.long, device=device)
|
|
239
|
+
limit = self.window_of(ctx)
|
|
240
|
+
rng = rng_of(ctx)
|
|
241
|
+
generator = None if rng is None else rng.stream(STREAM, device)
|
|
242
|
+
with evaluating(model):
|
|
243
|
+
for _ in range(self.max_new_tokens):
|
|
244
|
+
window = ids[:, -limit:] if limit else ids
|
|
245
|
+
logits = self.logits_of(model(window), size, window.shape[1])
|
|
246
|
+
ids = torch.cat([ids, self.pick(logits, generator)], dim=1)
|
|
247
|
+
return [tokenizer.decode(row) for row in ids[:, skip:].cpu().tolist()]
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
REGISTRATIONS = (
|
|
251
|
+
("gan_sampler", "kalfa.std.samplers.GanSampler",
|
|
252
|
+
"n samples of model(z) or model(z, label) with z from the samples stream and labels cycling over n_classes"),
|
|
253
|
+
("latent_sampler", "kalfa.std.samplers.LatentSampler", "n samples of a decoder fed standard normal latents"),
|
|
254
|
+
("ddpm_sampler", "kalfa.std.samplers.DdpmSampler",
|
|
255
|
+
"reverse diffusion from noise of the given shape with the beta schedule; steps limits the number of steps"),
|
|
256
|
+
("lm_sampler", "kalfa.std.samplers.LmSampler",
|
|
257
|
+
"autoregressive text from the prompt: max_new_tokens draws at temperature, top_k limits the choices"),
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
for name, target, doc in REGISTRATIONS:
|
|
261
|
+
register(name, target, slot="sampler", doc=doc)
|
kalfa/std/samples.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import numpy
|
|
5
|
+
import torch
|
|
6
|
+
from PIL import Image
|
|
7
|
+
|
|
8
|
+
from kalfa.core.record import atomic
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def image_grid(images, columns=8, pad=2):
|
|
12
|
+
images = torch.nan_to_num(torch.as_tensor(images).detach().float().cpu())
|
|
13
|
+
if images.ndim == 3:
|
|
14
|
+
images = images.unsqueeze(1)
|
|
15
|
+
count, channels, height, width = images.shape
|
|
16
|
+
columns = max(1, min(columns, count))
|
|
17
|
+
rows = math.ceil(count / columns)
|
|
18
|
+
low, high = float(images.min()), float(images.max())
|
|
19
|
+
if high > low:
|
|
20
|
+
images = (images - low) / (high - low)
|
|
21
|
+
else:
|
|
22
|
+
images = images.clamp(0.0, 1.0)
|
|
23
|
+
canvas = torch.ones((channels, rows * (height + pad) + pad, columns * (width + pad) + pad))
|
|
24
|
+
for index in range(count):
|
|
25
|
+
row, column = divmod(index, columns)
|
|
26
|
+
top = pad + row * (height + pad)
|
|
27
|
+
left = pad + column * (width + pad)
|
|
28
|
+
canvas[:, top:top + height, left:left + width] = images[index]
|
|
29
|
+
if channels == 1:
|
|
30
|
+
canvas = canvas.expand(3, -1, -1)
|
|
31
|
+
return canvas.permute(1, 2, 0).numpy()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def write_png(grid, path, least=256, most=8):
|
|
35
|
+
pixels = numpy.clip(numpy.rint(numpy.asarray(grid) * 255.0), 0, 255).astype(numpy.uint8)
|
|
36
|
+
height, width = pixels.shape[:2]
|
|
37
|
+
scale = max(1, min(most, math.ceil(least / max(height, width, 1))))
|
|
38
|
+
image = Image.fromarray(numpy.ascontiguousarray(pixels))
|
|
39
|
+
if scale > 1:
|
|
40
|
+
image = image.resize((width * scale, height * scale), Image.Resampling.NEAREST)
|
|
41
|
+
with atomic(path) as temporary:
|
|
42
|
+
image.save(temporary, format="PNG")
|
|
43
|
+
return Path(path)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def is_text(samples):
|
|
47
|
+
return isinstance(samples, (list, tuple)) and bool(samples) and all(isinstance(item, str) for item in samples)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def is_images(samples):
|
|
51
|
+
return isinstance(samples, torch.Tensor) and samples.ndim == 4 and samples.shape[1] in (1, 3)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def save_samples(samples, directory, stem, figures=None):
|
|
55
|
+
directory = Path(directory)
|
|
56
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
if isinstance(samples, str):
|
|
58
|
+
samples = [samples]
|
|
59
|
+
if isinstance(samples, torch.Tensor):
|
|
60
|
+
samples = samples.detach().cpu()
|
|
61
|
+
path = directory / f"{stem}.pt"
|
|
62
|
+
with atomic(path) as temporary:
|
|
63
|
+
torch.save(samples, temporary)
|
|
64
|
+
written = [path]
|
|
65
|
+
if is_text(samples):
|
|
66
|
+
text = directory / f"{stem}.txt"
|
|
67
|
+
with atomic(text) as temporary:
|
|
68
|
+
temporary.write_text("\n\n".join(samples) + "\n", encoding="utf-8")
|
|
69
|
+
written.append(text)
|
|
70
|
+
elif is_images(samples):
|
|
71
|
+
written.append(write_png(image_grid(samples), directory / f"{stem}.png"))
|
|
72
|
+
return written
|
kalfa/std/schedules.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
from kalfa.core.aliases import register
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def fraction(update, steps):
|
|
7
|
+
return min(max(float(update) / float(steps), 0.0), 1.0)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Warmup:
|
|
11
|
+
def __init__(self, steps):
|
|
12
|
+
self.steps = int(steps)
|
|
13
|
+
|
|
14
|
+
def __call__(self, update):
|
|
15
|
+
if self.steps <= 0:
|
|
16
|
+
return 1.0
|
|
17
|
+
return fraction(update, self.steps)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Cosine:
|
|
21
|
+
def __init__(self, steps, warmup=0, floor=0.0):
|
|
22
|
+
self.steps = int(steps)
|
|
23
|
+
self.warmup = int(warmup)
|
|
24
|
+
self.floor = float(floor)
|
|
25
|
+
|
|
26
|
+
def __call__(self, update):
|
|
27
|
+
update = float(update)
|
|
28
|
+
if self.warmup > 0 and update < self.warmup:
|
|
29
|
+
return fraction(update, self.warmup)
|
|
30
|
+
if self.steps <= self.warmup:
|
|
31
|
+
return self.floor
|
|
32
|
+
progress = fraction(update - self.warmup, self.steps - self.warmup)
|
|
33
|
+
return self.floor + (1.0 - self.floor) * 0.5 * (1.0 + math.cos(math.pi * progress))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class LinearWarmup:
|
|
37
|
+
def __init__(self, start, end, steps):
|
|
38
|
+
self.start = float(start)
|
|
39
|
+
self.end = float(end)
|
|
40
|
+
self.steps = int(steps)
|
|
41
|
+
|
|
42
|
+
def __call__(self, update):
|
|
43
|
+
if self.steps <= 0:
|
|
44
|
+
return self.end
|
|
45
|
+
return self.start + (self.end - self.start) * fraction(update, self.steps)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class StepDecay:
|
|
49
|
+
def __init__(self, every, gamma=0.1):
|
|
50
|
+
self.every = int(every)
|
|
51
|
+
self.gamma = float(gamma)
|
|
52
|
+
|
|
53
|
+
def __call__(self, update):
|
|
54
|
+
if self.every <= 0:
|
|
55
|
+
return 1.0
|
|
56
|
+
return self.gamma ** (int(update) // self.every)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
SCHEDULES = (
|
|
60
|
+
("warmup", "kalfa.std.schedules.Warmup", "a factor rising linearly from 0 to 1 over steps updates, then 1"),
|
|
61
|
+
("cosine", "kalfa.std.schedules.Cosine", "linear warmup over warmup updates, then cosine decay from 1 to "
|
|
62
|
+
"floor at steps updates"),
|
|
63
|
+
("linear_warmup", "kalfa.std.schedules.LinearWarmup", "a value moving linearly from start to end over steps "
|
|
64
|
+
"updates, then end; a loss weight or an lr factor"),
|
|
65
|
+
("step_decay", "kalfa.std.schedules.StepDecay", "a factor multiplied by gamma every every updates"),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def register_names():
|
|
70
|
+
for name, target, doc in SCHEDULES:
|
|
71
|
+
register(name, target, slot="schedule", doc=doc)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
register_names()
|
kalfa/std/steps.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
|
|
3
|
+
from kalfa.core.aliases import register
|
|
4
|
+
from kalfa.core.errors import RunError
|
|
5
|
+
from kalfa.core.loop import TrainTurn, autocast, batch_size, detached
|
|
6
|
+
from kalfa.core.protocols import Step
|
|
7
|
+
from kalfa.core.wrap import loss_scalar
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Supervised(Step):
|
|
11
|
+
def __init__(self, order=None, amp=False, grad_clip=None, accumulate=1):
|
|
12
|
+
self.order = None if order is None else list(order)
|
|
13
|
+
self.amp = bool(amp)
|
|
14
|
+
self.grad_clip = None if grad_clip is None else float(grad_clip)
|
|
15
|
+
self.accumulate = int(accumulate or 1)
|
|
16
|
+
self.cached = (None, {})
|
|
17
|
+
self.pending = {}
|
|
18
|
+
self.stepped = True
|
|
19
|
+
|
|
20
|
+
def arrangement(self, ctx):
|
|
21
|
+
order = list(self.order or ctx.optimizers)
|
|
22
|
+
unknown = [name for name in order if name not in ctx.optimizers]
|
|
23
|
+
if unknown:
|
|
24
|
+
raise RunError(f"{self.path}.order names optimizers {unknown} that are not defined; the optimizers are "
|
|
25
|
+
f"{sorted(ctx.optimizers)}")
|
|
26
|
+
return order
|
|
27
|
+
|
|
28
|
+
def outputs_for(self, batch, ctx):
|
|
29
|
+
entry = ctx.predicts()
|
|
30
|
+
if entry is None:
|
|
31
|
+
return {}
|
|
32
|
+
return entry.call(batch)
|
|
33
|
+
|
|
34
|
+
def minimize(self, entry, scalar):
|
|
35
|
+
if self.accumulate <= 1:
|
|
36
|
+
self.stepped = True
|
|
37
|
+
return entry.minimize(scalar, clip=self.grad_clip)
|
|
38
|
+
count = self.pending.get(entry.name, 0) + 1
|
|
39
|
+
if count == 1:
|
|
40
|
+
entry.zero_grad()
|
|
41
|
+
entry.backward(scalar / self.accumulate)
|
|
42
|
+
if count < self.accumulate:
|
|
43
|
+
self.pending[entry.name] = count
|
|
44
|
+
return None
|
|
45
|
+
self.pending[entry.name] = 0
|
|
46
|
+
norm = entry.clip(self.grad_clip)
|
|
47
|
+
entry.step()
|
|
48
|
+
self.stepped = True
|
|
49
|
+
return norm
|
|
50
|
+
|
|
51
|
+
def update(self, entry, outputs, batch, ctx):
|
|
52
|
+
if entry.minimizes is None:
|
|
53
|
+
raise RunError(f"optimizer {entry.name!r} minimizes nothing; write minimizes: \"@losses.<name>\"")
|
|
54
|
+
value = entry.minimizes(outputs, batch, ctx)
|
|
55
|
+
scalar = loss_scalar(value, entry.minimizes.name)
|
|
56
|
+
if not isinstance(scalar, torch.Tensor) or not scalar.requires_grad:
|
|
57
|
+
raise RunError(f"the loss {entry.minimizes.name!r} of optimizer {entry.name!r} carries no gradient; its "
|
|
58
|
+
f"models are frozen or the loss does not depend on their parameters")
|
|
59
|
+
ctx.last_norms[entry.name] = self.minimize(entry, scalar)
|
|
60
|
+
return value
|
|
61
|
+
|
|
62
|
+
def train_batch(self, batch, ctx):
|
|
63
|
+
result = {}
|
|
64
|
+
self.stepped = False
|
|
65
|
+
for name in self.arrangement(ctx):
|
|
66
|
+
outputs = self.outputs_for(batch, ctx)
|
|
67
|
+
self.cached = (ctx.batches, detached(outputs))
|
|
68
|
+
result[name] = self.update(ctx.optimizers[name], outputs, batch, ctx)
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
def eval_batch(self, batch, ctx):
|
|
72
|
+
stamp, outputs = self.cached
|
|
73
|
+
if stamp is not None and stamp == ctx.batches:
|
|
74
|
+
return outputs
|
|
75
|
+
return self.outputs_for(batch, ctx)
|
|
76
|
+
|
|
77
|
+
def predict(self, batch, ctx):
|
|
78
|
+
return self.outputs_for(batch, ctx)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Alternating(Supervised):
|
|
82
|
+
def __init__(self, order=None, repeat=None, fresh_batch=False, amp=False, grad_clip=None, accumulate=1):
|
|
83
|
+
super().__init__(order, amp, grad_clip, accumulate)
|
|
84
|
+
self.repeat = dict(repeat or {})
|
|
85
|
+
self.fresh_batch = bool(fresh_batch)
|
|
86
|
+
|
|
87
|
+
def step_once(self, entry, batch, ctx):
|
|
88
|
+
with autocast(ctx, self.amp):
|
|
89
|
+
outputs = self.outputs_for(batch, ctx)
|
|
90
|
+
value = self.update(entry, outputs, batch, ctx)
|
|
91
|
+
self.cached = (ctx.batches, detached(outputs))
|
|
92
|
+
return value
|
|
93
|
+
|
|
94
|
+
def train_turn(self, ctx):
|
|
95
|
+
turn = TrainTurn(ctx, self)
|
|
96
|
+
order = self.arrangement(ctx)
|
|
97
|
+
repeat = {name: int(self.repeat.get(name, 1)) for name in order}
|
|
98
|
+
stepped_by = {name: 0 for name in order}
|
|
99
|
+
while turn.more():
|
|
100
|
+
shared = None if self.fresh_batch else turn.next_batch()
|
|
101
|
+
if shared is None and not self.fresh_batch:
|
|
102
|
+
break
|
|
103
|
+
losses = {}
|
|
104
|
+
last = None
|
|
105
|
+
self.stepped = False
|
|
106
|
+
for name in order:
|
|
107
|
+
entry = ctx.optimizers[name]
|
|
108
|
+
for _ in range(repeat[name]):
|
|
109
|
+
batch = turn.next_batch() if self.fresh_batch else shared
|
|
110
|
+
if batch is None:
|
|
111
|
+
break
|
|
112
|
+
value = self.step_once(entry, batch, ctx)
|
|
113
|
+
turn.means.record(entry.minimizes.name, value, batch_size(batch))
|
|
114
|
+
losses[name] = value
|
|
115
|
+
stepped_by[name] += 1
|
|
116
|
+
last = batch
|
|
117
|
+
if last is None:
|
|
118
|
+
break
|
|
119
|
+
if self.stepped:
|
|
120
|
+
turn.record_update(losses)
|
|
121
|
+
turn.observe(last)
|
|
122
|
+
idle = [name for name in order if stepped_by[name] == 0]
|
|
123
|
+
if idle and turn.taken:
|
|
124
|
+
ctx.logger.warning(f"turn {ctx.turn}: optimizers {idle} took no step; the train loader ran out of "
|
|
125
|
+
f"batches before their place in order {order}")
|
|
126
|
+
return turn.close()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
register("supervised", "kalfa.std.steps.Supervised", slot="step",
|
|
130
|
+
doc="One forward pass of the predicts model per batch, then every optimizer in order minimizes its loss")
|
|
131
|
+
register("alternating", "kalfa.std.steps.Alternating", slot="step",
|
|
132
|
+
doc="Per update round each optimizer in order takes repeat[name] steps, on the shared batch or on a fresh "
|
|
133
|
+
"batch per step")
|
kalfa/std/triggers.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
from kalfa.core.aliases import register
|
|
5
|
+
from kalfa.core.protocols import Trigger
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def latest(ctx, key):
|
|
9
|
+
value = (getattr(ctx, "latest", None) or {}).get(key)
|
|
10
|
+
if value is None:
|
|
11
|
+
return None
|
|
12
|
+
number = float(value)
|
|
13
|
+
return None if math.isnan(number) else number
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AfterEpoch(Trigger):
|
|
17
|
+
def __init__(self, at):
|
|
18
|
+
self.at = int(at)
|
|
19
|
+
|
|
20
|
+
def fires(self, ctx):
|
|
21
|
+
return ctx.turn >= self.at
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Every(Trigger):
|
|
25
|
+
def __init__(self, epochs):
|
|
26
|
+
self.epochs = int(epochs)
|
|
27
|
+
|
|
28
|
+
def fires(self, ctx):
|
|
29
|
+
return self.epochs > 0 and ctx.turn % self.epochs == 0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Plateau(Trigger):
|
|
33
|
+
persist = ("best", "wait")
|
|
34
|
+
|
|
35
|
+
def __init__(self, watch, patience, mode="min", min_delta=0.0):
|
|
36
|
+
if mode not in ("min", "max"):
|
|
37
|
+
raise ValueError(f"mode must be min or max, got {mode!r}")
|
|
38
|
+
self.watch = str(watch)
|
|
39
|
+
self.patience = int(patience)
|
|
40
|
+
self.mode = mode
|
|
41
|
+
self.min_delta = float(min_delta)
|
|
42
|
+
self.best = None
|
|
43
|
+
self.wait = 0
|
|
44
|
+
|
|
45
|
+
def improved(self, value):
|
|
46
|
+
if self.best is None:
|
|
47
|
+
return True
|
|
48
|
+
if self.mode == "max":
|
|
49
|
+
return value > self.best + self.min_delta
|
|
50
|
+
return value < self.best - self.min_delta
|
|
51
|
+
|
|
52
|
+
def reset(self):
|
|
53
|
+
self.best = None
|
|
54
|
+
self.wait = 0
|
|
55
|
+
|
|
56
|
+
def fires(self, ctx):
|
|
57
|
+
value = latest(ctx, self.watch)
|
|
58
|
+
if value is None:
|
|
59
|
+
return False
|
|
60
|
+
if self.improved(value):
|
|
61
|
+
self.best = value
|
|
62
|
+
self.wait = 0
|
|
63
|
+
else:
|
|
64
|
+
self.wait += 1
|
|
65
|
+
return self.wait >= self.patience
|
|
66
|
+
|
|
67
|
+
def describe(self):
|
|
68
|
+
return f"{self.watch} plateau {self.patience}"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Above(Trigger):
|
|
72
|
+
def __init__(self, watch, value):
|
|
73
|
+
self.watch = str(watch)
|
|
74
|
+
self.value = float(value)
|
|
75
|
+
|
|
76
|
+
def fires(self, ctx):
|
|
77
|
+
current = latest(ctx, self.watch)
|
|
78
|
+
return current is not None and current > self.value
|
|
79
|
+
|
|
80
|
+
def describe(self):
|
|
81
|
+
return f"{self.watch} > {self.value}"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class Below(Trigger):
|
|
85
|
+
def __init__(self, watch, value):
|
|
86
|
+
self.watch = str(watch)
|
|
87
|
+
self.value = float(value)
|
|
88
|
+
|
|
89
|
+
def fires(self, ctx):
|
|
90
|
+
current = latest(ctx, self.watch)
|
|
91
|
+
return current is not None and current < self.value
|
|
92
|
+
|
|
93
|
+
def describe(self):
|
|
94
|
+
return f"{self.watch} < {self.value}"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class TimeBudget(Trigger):
|
|
98
|
+
persist = ("started",)
|
|
99
|
+
|
|
100
|
+
def __init__(self, minutes):
|
|
101
|
+
self.minutes = float(minutes)
|
|
102
|
+
self.started = None
|
|
103
|
+
|
|
104
|
+
def reset(self):
|
|
105
|
+
self.started = None
|
|
106
|
+
|
|
107
|
+
def fires(self, ctx):
|
|
108
|
+
now = time.time()
|
|
109
|
+
if self.started is None:
|
|
110
|
+
self.started = now
|
|
111
|
+
return (now - self.started) >= self.minutes * 60.0
|
|
112
|
+
|
|
113
|
+
def describe(self):
|
|
114
|
+
return f"after {self.minutes} minutes"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
TRIGGERS = (
|
|
118
|
+
("after_epoch", "kalfa.std.triggers.AfterEpoch", "fires from the turn whose count reaches at (absolute, so a "
|
|
119
|
+
"resumed run counts on)"),
|
|
120
|
+
("every", "kalfa.std.triggers.Every", "fires every epochs turns"),
|
|
121
|
+
("plateau", "kalfa.std.triggers.Plateau", "fires after patience turns without the watched series improving "
|
|
122
|
+
"by min_delta; turns without a value are not counted"),
|
|
123
|
+
("above", "kalfa.std.triggers.Above", "fires when the watched series is above value"),
|
|
124
|
+
("below", "kalfa.std.triggers.Below", "fires when the watched series is below value"),
|
|
125
|
+
("time_budget", "kalfa.std.triggers.TimeBudget", "fires once minutes have passed since the first turn that "
|
|
126
|
+
"asked it"),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def register_names():
|
|
131
|
+
for name, target, doc in TRIGGERS:
|
|
132
|
+
register(name, target, slot="trigger", doc=doc)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
register_names()
|