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/core/wrap.py
ADDED
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import fnmatch
|
|
3
|
+
import functools
|
|
4
|
+
import inspect
|
|
5
|
+
import math
|
|
6
|
+
|
|
7
|
+
import torch
|
|
8
|
+
from torch import nn
|
|
9
|
+
|
|
10
|
+
from .errors import BuildError, RunError
|
|
11
|
+
from .protocols import ContextMetric, Objective
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DEFAULT_ON = ("train", "valid", "test")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def as_tuple(on):
|
|
18
|
+
return (on,) if isinstance(on, str) else tuple(on)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def fields(mapping):
|
|
22
|
+
return sorted(str(key) for key in mapping)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def name_of(obj):
|
|
26
|
+
name = getattr(obj, "name", None)
|
|
27
|
+
return name if isinstance(name, str) and name else type(obj).__name__
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def unwrap(obj):
|
|
31
|
+
if obj is None:
|
|
32
|
+
return None
|
|
33
|
+
if callable(getattr(obj, "named_parameters", None)):
|
|
34
|
+
return obj
|
|
35
|
+
inner = getattr(obj, "module", None)
|
|
36
|
+
if inner is not None and callable(getattr(inner, "named_parameters", None)):
|
|
37
|
+
return inner
|
|
38
|
+
raise BuildError(f"{type(obj).__name__} {name_of(obj)!r} has no parameters")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def construct(cls, path):
|
|
42
|
+
try:
|
|
43
|
+
return cls()
|
|
44
|
+
except TypeError as problem:
|
|
45
|
+
raise BuildError(f"{path}: {cls.__name__} needs arguments; write it as a call with use") from problem
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def grad_norm(parameters):
|
|
49
|
+
grads = [parameter.grad.detach() for parameter in parameters if parameter.grad is not None]
|
|
50
|
+
if not grads:
|
|
51
|
+
return 0.0
|
|
52
|
+
norms = torch.stack([torch.linalg.vector_norm(grad.float()) for grad in grads])
|
|
53
|
+
return float(torch.linalg.vector_norm(norms))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def frozen_parts(module):
|
|
57
|
+
found = []
|
|
58
|
+
for part in module.modules():
|
|
59
|
+
parameters = list(part.parameters())
|
|
60
|
+
if parameters and not any(parameter.requires_grad for parameter in parameters):
|
|
61
|
+
found.append(part)
|
|
62
|
+
return found
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class EmaCopy(nn.Module):
|
|
66
|
+
def __init__(self, module, decay=0.999, every=1):
|
|
67
|
+
super().__init__()
|
|
68
|
+
self.decay = float(decay)
|
|
69
|
+
self.every = int(every)
|
|
70
|
+
self.module = copy.deepcopy(module)
|
|
71
|
+
for parameter in self.module.parameters():
|
|
72
|
+
parameter.requires_grad_(False)
|
|
73
|
+
nn.Module.train(self, False)
|
|
74
|
+
self.updates = 0
|
|
75
|
+
object.__setattr__(self, "source", module)
|
|
76
|
+
|
|
77
|
+
def shift(self, module):
|
|
78
|
+
self.updates += 1
|
|
79
|
+
if self.every > 1 and self.updates % self.every:
|
|
80
|
+
return
|
|
81
|
+
module = unwrap(module)
|
|
82
|
+
with torch.no_grad():
|
|
83
|
+
own = dict(self.module.named_parameters())
|
|
84
|
+
for name, parameter in module.named_parameters():
|
|
85
|
+
kept = own.get(name)
|
|
86
|
+
if kept is None:
|
|
87
|
+
raise RunError(f"the EMA copy has no parameter {name!r}; the module changed after it was copied")
|
|
88
|
+
kept.mul_(self.decay).add_(parameter.detach().to(kept.device), alpha=1.0 - self.decay)
|
|
89
|
+
buffers = dict(self.module.named_buffers())
|
|
90
|
+
for name, buffer in module.named_buffers():
|
|
91
|
+
kept = buffers.get(name)
|
|
92
|
+
if kept is not None:
|
|
93
|
+
kept.copy_(buffer)
|
|
94
|
+
|
|
95
|
+
def forward(self, *inputs):
|
|
96
|
+
return self.module(*inputs)
|
|
97
|
+
|
|
98
|
+
def train(self, mode=True):
|
|
99
|
+
return self
|
|
100
|
+
|
|
101
|
+
def get_extra_state(self):
|
|
102
|
+
return {"updates": self.updates}
|
|
103
|
+
|
|
104
|
+
def set_extra_state(self, state):
|
|
105
|
+
self.updates = int((state or {}).get("updates", 0))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ModelEntry:
|
|
109
|
+
def __init__(self, name, module, inputs, outputs, ema=None, freeze=False, weights=None, init=None):
|
|
110
|
+
self.name = name
|
|
111
|
+
self.module = module
|
|
112
|
+
self.inputs = list(inputs)
|
|
113
|
+
self.outputs = list(outputs)
|
|
114
|
+
self.ema = ema
|
|
115
|
+
self.freeze = freeze
|
|
116
|
+
self.weights = weights
|
|
117
|
+
self.init = init
|
|
118
|
+
self.summary = None
|
|
119
|
+
self.path = f"models.{name}"
|
|
120
|
+
self.locked = False
|
|
121
|
+
self.frozen_parts = []
|
|
122
|
+
if freeze:
|
|
123
|
+
self.set_freeze(freeze)
|
|
124
|
+
|
|
125
|
+
def call(self, batch):
|
|
126
|
+
inputs = []
|
|
127
|
+
for name in self.inputs:
|
|
128
|
+
if name not in batch:
|
|
129
|
+
raise RunError(f"model {self.name!r} needs the batch field {name!r}; the batch has {fields(batch)}")
|
|
130
|
+
inputs.append(batch[name])
|
|
131
|
+
return self.name_outputs(self.module(*inputs))
|
|
132
|
+
|
|
133
|
+
def name_outputs(self, result):
|
|
134
|
+
names = self.outputs or [self.name]
|
|
135
|
+
if isinstance(result, torch.Tensor):
|
|
136
|
+
return {names[0]: result}
|
|
137
|
+
if isinstance(result, dict):
|
|
138
|
+
missing = [name for name in names if name not in result]
|
|
139
|
+
if missing:
|
|
140
|
+
raise RunError(f"model {self.name!r} returned a dict without {missing}; its keys are {fields(result)}")
|
|
141
|
+
return result
|
|
142
|
+
if isinstance(result, (tuple, list)):
|
|
143
|
+
if len(result) != len(names):
|
|
144
|
+
raise RunError(f"model {self.name!r} returned {len(result)} values for the outputs {names}")
|
|
145
|
+
return dict(zip(names, result))
|
|
146
|
+
raise RunError(f"model {self.name!r} returned {type(result).__name__}, not a tensor, a tuple or a dict")
|
|
147
|
+
|
|
148
|
+
def set_freeze(self, value):
|
|
149
|
+
freeze_parameters(self.module, value)
|
|
150
|
+
self.freeze = value
|
|
151
|
+
self.locked = value is True
|
|
152
|
+
self.frozen_parts = [] if self.locked or not value else frozen_parts(self.module)
|
|
153
|
+
if self.locked:
|
|
154
|
+
self.module.eval()
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def frozen(self):
|
|
158
|
+
return self.freeze
|
|
159
|
+
|
|
160
|
+
def set_mode(self, train):
|
|
161
|
+
self.module.train(bool(train) and not self.locked)
|
|
162
|
+
for part in self.frozen_parts:
|
|
163
|
+
part.eval()
|
|
164
|
+
return self
|
|
165
|
+
|
|
166
|
+
def parameter_count(self):
|
|
167
|
+
return sum(parameter.numel() for parameter in self.module.parameters())
|
|
168
|
+
|
|
169
|
+
def state_dict(self):
|
|
170
|
+
return self.module.state_dict()
|
|
171
|
+
|
|
172
|
+
def load_state_dict(self, state):
|
|
173
|
+
self.module.load_state_dict(state)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def align(guess, truth):
|
|
177
|
+
if isinstance(guess, torch.Tensor) and isinstance(truth, torch.Tensor):
|
|
178
|
+
if truth.ndim == guess.ndim - 1 and guess.ndim > 0 and guess.shape[-1] == 1:
|
|
179
|
+
guess = guess.squeeze(-1)
|
|
180
|
+
elif guess.ndim == truth.ndim - 1 and truth.ndim > 0 and truth.shape[-1] == 1:
|
|
181
|
+
truth = truth.squeeze(-1)
|
|
182
|
+
return guess, truth
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def pair_values(owner, outputs, batch, ctx, kind):
|
|
186
|
+
label = f"{kind} {owner.name!r}" if getattr(owner, "name", "") else kind
|
|
187
|
+
if owner.pred is None:
|
|
188
|
+
raise RunError(f"{label}: pred is not set")
|
|
189
|
+
if owner.target is None:
|
|
190
|
+
raise RunError(f"{label}: target is not set")
|
|
191
|
+
outputs = {} if outputs is None else outputs
|
|
192
|
+
if owner.pred not in outputs:
|
|
193
|
+
raise RunError(f"{label}: output {owner.pred!r} is not among the step outputs {fields(outputs)}")
|
|
194
|
+
if owner.target not in batch:
|
|
195
|
+
raise RunError(f"{label}: field {owner.target!r} is not in the batch; its fields are {fields(batch)}")
|
|
196
|
+
guess, truth = align(outputs[owner.pred], batch[owner.target])
|
|
197
|
+
if owner.rescale:
|
|
198
|
+
inverse = getattr(getattr(ctx, "data", None), "inverse", None)
|
|
199
|
+
if callable(inverse):
|
|
200
|
+
guess = inverse(owner.target, guess)
|
|
201
|
+
truth = inverse(owner.target, truth)
|
|
202
|
+
return guess, truth
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def apply_keys(obj, keys, path):
|
|
206
|
+
if keys.get("sample_weight") is not None:
|
|
207
|
+
raise BuildError(f"{path}: sample_weight belongs to a criterion, a loss of (pred, target); "
|
|
208
|
+
f"{type(obj).__name__} is an objective and reads the batch field itself")
|
|
209
|
+
for key in ("pred", "target", "rescale"):
|
|
210
|
+
if key not in keys:
|
|
211
|
+
continue
|
|
212
|
+
if not hasattr(obj, key):
|
|
213
|
+
raise BuildError(f"{path}: {key} belongs to a criterion or a metric, not to {type(obj).__name__}")
|
|
214
|
+
value = keys[key]
|
|
215
|
+
setattr(obj, key, bool(value) if key == "rescale" and value is not None else value)
|
|
216
|
+
if "on" in keys:
|
|
217
|
+
obj.on = as_tuple(keys["on"])
|
|
218
|
+
if "every" in keys:
|
|
219
|
+
obj.every = int(keys["every"])
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def takes_reduction(function):
|
|
223
|
+
try:
|
|
224
|
+
return "reduction" in inspect.signature(function).parameters
|
|
225
|
+
except (TypeError, ValueError):
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def function_name(function):
|
|
230
|
+
inner = function.func if isinstance(function, functools.partial) else function
|
|
231
|
+
return getattr(inner, "__name__", None) or type(inner).__name__
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def reduction_of(function):
|
|
235
|
+
reduction = getattr(function, "reduction", None)
|
|
236
|
+
if isinstance(reduction, str):
|
|
237
|
+
return reduction
|
|
238
|
+
if not takes_reduction(function):
|
|
239
|
+
return None
|
|
240
|
+
bound = function.keywords.get("reduction") if isinstance(function, functools.partial) else None
|
|
241
|
+
return "mean" if bound is None else bound
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def element_losses(function, guess, truth):
|
|
245
|
+
kept = getattr(function, "reduction", None)
|
|
246
|
+
if not isinstance(kept, str):
|
|
247
|
+
return function(guess, truth, reduction="none")
|
|
248
|
+
function.reduction = "none"
|
|
249
|
+
try:
|
|
250
|
+
return function(guess, truth)
|
|
251
|
+
finally:
|
|
252
|
+
function.reduction = kept
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def weighted(values, weight, reduction, label):
|
|
256
|
+
if reduction not in ("mean", "sum"):
|
|
257
|
+
raise RunError(f"{label}: sample_weight takes the reduction mean or sum, not {reduction!r}")
|
|
258
|
+
if not isinstance(values, torch.Tensor) or values.ndim == 0:
|
|
259
|
+
raise RunError(f"{label}: sample_weight needs one loss value per element, but the loss returned one value")
|
|
260
|
+
weight = weight.to(values.dtype)
|
|
261
|
+
while weight.ndim > 1 and weight.shape[-1] == 1:
|
|
262
|
+
weight = weight.squeeze(-1)
|
|
263
|
+
if weight.ndim > values.ndim or tuple(weight.shape) != tuple(values.shape[:weight.ndim]):
|
|
264
|
+
raise RunError(f"{label}: the weights of shape {tuple(weight.shape)} do not match the element losses of shape "
|
|
265
|
+
f"{tuple(values.shape)}; a weight has one value per row")
|
|
266
|
+
scaled = values * weight.reshape(weight.shape + (1,) * (values.ndim - weight.ndim))
|
|
267
|
+
return scaled.sum() if reduction == "sum" else scaled.mean()
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class Criterion(Objective):
|
|
271
|
+
def __init__(self, function, pred=None, target=None, rescale=False, on=("train", "valid", "test"), every=1,
|
|
272
|
+
sample_weight=None, path=""):
|
|
273
|
+
super().__init__()
|
|
274
|
+
self.pred = pred
|
|
275
|
+
self.target = target
|
|
276
|
+
self.rescale = bool(rescale)
|
|
277
|
+
self.on = (on,) if isinstance(on, str) else tuple(on)
|
|
278
|
+
self.every = int(every)
|
|
279
|
+
self.sample_weight = sample_weight
|
|
280
|
+
self.function = function
|
|
281
|
+
if sample_weight is not None:
|
|
282
|
+
self.check_weight(path or "loss")
|
|
283
|
+
|
|
284
|
+
def check_weight(self, path):
|
|
285
|
+
if not isinstance(self.sample_weight, str) or not self.sample_weight:
|
|
286
|
+
raise BuildError(f"{path}: sample_weight names a batch field such as w, got {self.sample_weight!r}")
|
|
287
|
+
reduction = reduction_of(self.function)
|
|
288
|
+
if reduction is None:
|
|
289
|
+
raise BuildError(f"{path}: sample_weight needs a loss that can return one value per element, a loss "
|
|
290
|
+
"with a reduction param such as mse, l1, huber, bce_logits or cross_entropy; "
|
|
291
|
+
f"{function_name(self.function)} has none")
|
|
292
|
+
if reduction not in ("mean", "sum"):
|
|
293
|
+
raise BuildError(f"{path}: sample_weight takes the reduction mean or sum, not {reduction!r}")
|
|
294
|
+
|
|
295
|
+
def pair(self, outputs, batch, ctx):
|
|
296
|
+
return pair_values(self, outputs, batch, ctx, "loss")
|
|
297
|
+
|
|
298
|
+
def forward(self, outputs, batch, ctx):
|
|
299
|
+
guess, truth = self.pair(outputs, batch, ctx)
|
|
300
|
+
if self.sample_weight is None:
|
|
301
|
+
return self.function(guess, truth)
|
|
302
|
+
label = f"loss {self.name!r}" if getattr(self, "name", "") else "loss"
|
|
303
|
+
if self.sample_weight not in batch:
|
|
304
|
+
raise RunError(f"{label}: sample_weight names the field {self.sample_weight!r}, which is not in the "
|
|
305
|
+
f"batch; its fields are {fields(batch)}")
|
|
306
|
+
values = element_losses(self.function, guess, truth)
|
|
307
|
+
return weighted(values, batch[self.sample_weight], reduction_of(self.function), label)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def wrap_loss(obj, keys, name, path):
|
|
311
|
+
keys = dict(keys or {})
|
|
312
|
+
if isinstance(obj, type):
|
|
313
|
+
obj = construct(obj, path)
|
|
314
|
+
if isinstance(obj, Objective):
|
|
315
|
+
apply_keys(obj, keys, path)
|
|
316
|
+
result = obj
|
|
317
|
+
elif callable(obj):
|
|
318
|
+
result = Criterion(obj, pred=keys.get("pred"), target=keys.get("target"), rescale=bool(keys.get("rescale")),
|
|
319
|
+
on=keys.get("on", DEFAULT_ON), every=keys.get("every", 1),
|
|
320
|
+
sample_weight=keys.get("sample_weight"), path=path)
|
|
321
|
+
else:
|
|
322
|
+
raise BuildError(f"{path}: {type(obj).__name__} is not a loss; a loss is an Objective or a callable "
|
|
323
|
+
"taking (pred, target)")
|
|
324
|
+
result.name = name
|
|
325
|
+
result.path = path
|
|
326
|
+
return result
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def as_float(value):
|
|
330
|
+
if isinstance(value, dict):
|
|
331
|
+
return {str(key): as_float(inner) for key, inner in value.items()}
|
|
332
|
+
if isinstance(value, torch.Tensor):
|
|
333
|
+
if value.numel() == 1:
|
|
334
|
+
return float(value.item())
|
|
335
|
+
return {str(index): float(item) for index, item in enumerate(value.detach().flatten().tolist())}
|
|
336
|
+
if isinstance(value, (list, tuple)):
|
|
337
|
+
return {str(index): as_float(item) for index, item in enumerate(value)}
|
|
338
|
+
return float(value)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class MetricEntry:
|
|
342
|
+
def __init__(self, name, metric, pred=None, target=None, rescale=None, on=("train", "valid", "test"), every=1):
|
|
343
|
+
self.name = name
|
|
344
|
+
self.metric = metric
|
|
345
|
+
self.pred = pred
|
|
346
|
+
self.target = target
|
|
347
|
+
self.rescale = rescale
|
|
348
|
+
self.on = (on,) if isinstance(on, str) else tuple(on)
|
|
349
|
+
self.every = int(every)
|
|
350
|
+
self.path = f"metrics.{name}"
|
|
351
|
+
self.placed = False
|
|
352
|
+
self.seen = 0
|
|
353
|
+
|
|
354
|
+
def reset(self):
|
|
355
|
+
self.metric.reset()
|
|
356
|
+
self.seen = 0
|
|
357
|
+
|
|
358
|
+
def update(self, outputs, batch, ctx):
|
|
359
|
+
if isinstance(self.metric, ContextMetric):
|
|
360
|
+
self.metric.update(outputs, batch, ctx)
|
|
361
|
+
self.seen += 1
|
|
362
|
+
return
|
|
363
|
+
guess, truth = pair_values(self, outputs, batch, ctx, "metric")
|
|
364
|
+
if isinstance(guess, torch.Tensor):
|
|
365
|
+
guess = guess.detach()
|
|
366
|
+
if not self.placed:
|
|
367
|
+
self.to(guess.device)
|
|
368
|
+
if isinstance(truth, torch.Tensor):
|
|
369
|
+
truth = truth.detach()
|
|
370
|
+
if not truth.is_floating_point() and not truth.is_complex():
|
|
371
|
+
truth = truth.long()
|
|
372
|
+
self.metric.update(guess, truth)
|
|
373
|
+
self.seen += 1
|
|
374
|
+
|
|
375
|
+
def compute(self):
|
|
376
|
+
if not self.seen:
|
|
377
|
+
return math.nan
|
|
378
|
+
return as_float(self.metric.compute())
|
|
379
|
+
|
|
380
|
+
def to(self, device):
|
|
381
|
+
mover = getattr(self.metric, "to", None)
|
|
382
|
+
if callable(mover):
|
|
383
|
+
mover(device)
|
|
384
|
+
self.placed = True
|
|
385
|
+
return self
|
|
386
|
+
|
|
387
|
+
def state_dict(self):
|
|
388
|
+
return {}
|
|
389
|
+
|
|
390
|
+
def load_state_dict(self, state):
|
|
391
|
+
pass
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def wrap_metric(obj, keys, name, path):
|
|
395
|
+
keys = dict(keys or {})
|
|
396
|
+
if isinstance(obj, type):
|
|
397
|
+
obj = construct(obj, path)
|
|
398
|
+
if isinstance(obj, ContextMetric):
|
|
399
|
+
apply_keys(obj, keys, path)
|
|
400
|
+
obj.name = name
|
|
401
|
+
obj.path = path
|
|
402
|
+
return obj
|
|
403
|
+
if not all(callable(getattr(obj, method, None)) for method in ("update", "compute", "reset")):
|
|
404
|
+
raise BuildError(f"{path}: {type(obj).__name__} is not a metric; a metric has update(pred, target), "
|
|
405
|
+
"compute() and reset(), or it is a ContextMetric")
|
|
406
|
+
entry = MetricEntry(name, obj, pred=keys.get("pred"), target=keys.get("target"), rescale=keys.get("rescale"),
|
|
407
|
+
on=keys.get("on", DEFAULT_ON), every=keys.get("every", 1))
|
|
408
|
+
entry.path = path
|
|
409
|
+
return entry
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
class Group:
|
|
413
|
+
def __init__(self, of, match=None, **params):
|
|
414
|
+
self.of = of
|
|
415
|
+
self.match = match
|
|
416
|
+
self.params = dict(params)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
class OptimizerEntry:
|
|
420
|
+
def __init__(self, name, target, params, over, minimizes=None, schedule=None):
|
|
421
|
+
self.name = name
|
|
422
|
+
self.target = target
|
|
423
|
+
self.params = dict(params)
|
|
424
|
+
self.over = list(over)
|
|
425
|
+
self.minimizes = minimizes
|
|
426
|
+
self.schedule = schedule
|
|
427
|
+
self.real = None
|
|
428
|
+
self.base = None
|
|
429
|
+
self.scaler = None
|
|
430
|
+
self.updates = 0
|
|
431
|
+
self.pending = None
|
|
432
|
+
self.emas = []
|
|
433
|
+
self.origins = []
|
|
434
|
+
self.path = f"optimizers.{name}"
|
|
435
|
+
|
|
436
|
+
def bind(self, ctx):
|
|
437
|
+
self.build()
|
|
438
|
+
|
|
439
|
+
def build(self):
|
|
440
|
+
if self.real is not None:
|
|
441
|
+
return self.real
|
|
442
|
+
claimed = set()
|
|
443
|
+
groups = []
|
|
444
|
+
origins = []
|
|
445
|
+
for position, item in enumerate(self.over):
|
|
446
|
+
if not isinstance(item, Group):
|
|
447
|
+
continue
|
|
448
|
+
module = unwrap(item.of)
|
|
449
|
+
if module is None:
|
|
450
|
+
continue
|
|
451
|
+
picked = [parameter for name, parameter in module.named_parameters()
|
|
452
|
+
if (item.match is None or fnmatch.fnmatchcase(name, item.match)) and id(parameter) not in claimed]
|
|
453
|
+
if not picked:
|
|
454
|
+
wanted = f" matching {item.match!r}" if item.match else ""
|
|
455
|
+
raise BuildError(f"{self.path}.over[{position}]: no parameter of {name_of(module)} is left for the "
|
|
456
|
+
f"group{wanted}")
|
|
457
|
+
claimed.update(id(parameter) for parameter in picked)
|
|
458
|
+
groups.append({"params": picked, **item.params})
|
|
459
|
+
origins.append(item)
|
|
460
|
+
rest = []
|
|
461
|
+
for item in self.over:
|
|
462
|
+
if isinstance(item, Group):
|
|
463
|
+
continue
|
|
464
|
+
module = unwrap(item)
|
|
465
|
+
if module is None:
|
|
466
|
+
continue
|
|
467
|
+
for parameter in module.parameters():
|
|
468
|
+
if id(parameter) not in claimed:
|
|
469
|
+
claimed.add(id(parameter))
|
|
470
|
+
rest.append(parameter)
|
|
471
|
+
if rest:
|
|
472
|
+
groups.insert(0, {"params": rest})
|
|
473
|
+
origins.insert(0, None)
|
|
474
|
+
if not groups:
|
|
475
|
+
raise BuildError(f"{self.path} has no parameters to optimize")
|
|
476
|
+
self.real = self.target(groups, **self.params)
|
|
477
|
+
self.origins = origins
|
|
478
|
+
self.base = [float(group.get("lr", math.nan)) for group in self.real.param_groups]
|
|
479
|
+
if self.pending is not None:
|
|
480
|
+
self.real.load_state_dict(self.pending)
|
|
481
|
+
self.pending = None
|
|
482
|
+
self.restore_params()
|
|
483
|
+
self.reschedule()
|
|
484
|
+
return self.real
|
|
485
|
+
|
|
486
|
+
def restore_params(self):
|
|
487
|
+
for index, (group, origin) in enumerate(zip(self.real.param_groups, self.origins)):
|
|
488
|
+
for name, value in self.params.items():
|
|
489
|
+
if origin is None or name not in origin.params:
|
|
490
|
+
group[name] = value
|
|
491
|
+
if origin is not None:
|
|
492
|
+
group.update(origin.params)
|
|
493
|
+
self.base[index] = float(group.get("lr", math.nan))
|
|
494
|
+
|
|
495
|
+
def owned(self):
|
|
496
|
+
self.build()
|
|
497
|
+
found = []
|
|
498
|
+
seen = set()
|
|
499
|
+
for group in self.real.param_groups:
|
|
500
|
+
for parameter in group["params"]:
|
|
501
|
+
if id(parameter) not in seen:
|
|
502
|
+
seen.add(id(parameter))
|
|
503
|
+
found.append(parameter)
|
|
504
|
+
return found
|
|
505
|
+
|
|
506
|
+
def modules(self):
|
|
507
|
+
found = []
|
|
508
|
+
for item in self.over:
|
|
509
|
+
module = unwrap(item.of if isinstance(item, Group) else item)
|
|
510
|
+
if module is not None and all(module is not other for other in found):
|
|
511
|
+
found.append(module)
|
|
512
|
+
return found
|
|
513
|
+
|
|
514
|
+
def parameters(self):
|
|
515
|
+
found = []
|
|
516
|
+
seen = set()
|
|
517
|
+
for module in self.modules():
|
|
518
|
+
for parameter in module.parameters():
|
|
519
|
+
if id(parameter) not in seen:
|
|
520
|
+
seen.add(id(parameter))
|
|
521
|
+
found.append(parameter)
|
|
522
|
+
return found
|
|
523
|
+
|
|
524
|
+
def scaling(self):
|
|
525
|
+
if self.scaler is None:
|
|
526
|
+
return False
|
|
527
|
+
enabled = getattr(self.scaler, "is_enabled", None)
|
|
528
|
+
return bool(enabled()) if callable(enabled) else True
|
|
529
|
+
|
|
530
|
+
def reschedule(self):
|
|
531
|
+
if self.schedule is None or self.real is None:
|
|
532
|
+
return
|
|
533
|
+
factor = float(self.schedule(self.updates))
|
|
534
|
+
for group, base in zip(self.real.param_groups, self.base):
|
|
535
|
+
group["lr"] = base * factor
|
|
536
|
+
|
|
537
|
+
def zero_grad(self):
|
|
538
|
+
for parameter in self.owned():
|
|
539
|
+
parameter.grad = None
|
|
540
|
+
|
|
541
|
+
def backward(self, loss):
|
|
542
|
+
if self.scaling():
|
|
543
|
+
self.scaler.scale(loss).backward()
|
|
544
|
+
else:
|
|
545
|
+
loss.backward()
|
|
546
|
+
|
|
547
|
+
def clip(self, limit):
|
|
548
|
+
parameters = self.owned()
|
|
549
|
+
if self.scaling():
|
|
550
|
+
self.scaler.unscale_(self.real)
|
|
551
|
+
if limit is None:
|
|
552
|
+
return grad_norm(parameters)
|
|
553
|
+
return float(torch.nn.utils.clip_grad_norm_(parameters, float(limit)))
|
|
554
|
+
|
|
555
|
+
def step(self):
|
|
556
|
+
self.build()
|
|
557
|
+
if self.scaling():
|
|
558
|
+
self.scaler.step(self.real)
|
|
559
|
+
self.scaler.update()
|
|
560
|
+
else:
|
|
561
|
+
self.real.step()
|
|
562
|
+
self.updates += 1
|
|
563
|
+
self.reschedule()
|
|
564
|
+
self.shift_emas()
|
|
565
|
+
|
|
566
|
+
def shift_emas(self):
|
|
567
|
+
for ema in self.emas:
|
|
568
|
+
ema.shift(ema.source)
|
|
569
|
+
|
|
570
|
+
def minimize(self, loss, clip=None):
|
|
571
|
+
self.zero_grad()
|
|
572
|
+
self.backward(loss)
|
|
573
|
+
norm = self.clip(clip)
|
|
574
|
+
self.step()
|
|
575
|
+
return norm
|
|
576
|
+
|
|
577
|
+
def rate(self):
|
|
578
|
+
if self.real is None:
|
|
579
|
+
return float(self.params.get("lr", math.nan))
|
|
580
|
+
return float(self.real.param_groups[0]["lr"])
|
|
581
|
+
|
|
582
|
+
def group_label(self, group):
|
|
583
|
+
label = getattr(group, "label", None) or name_of(unwrap(group.of) or group.of)
|
|
584
|
+
return f"{label}[{group.match}]" if group.match else label
|
|
585
|
+
|
|
586
|
+
def rates(self):
|
|
587
|
+
found = {}
|
|
588
|
+
if self.real is None:
|
|
589
|
+
for item in self.over:
|
|
590
|
+
if isinstance(item, Group):
|
|
591
|
+
found[self.group_label(item)] = float(item.params.get("lr", self.params.get("lr", math.nan)))
|
|
592
|
+
return found
|
|
593
|
+
for index, origin in enumerate(self.origins):
|
|
594
|
+
if origin is not None:
|
|
595
|
+
found[self.group_label(origin)] = float(self.real.param_groups[index]["lr"])
|
|
596
|
+
return found
|
|
597
|
+
|
|
598
|
+
def set_param(self, name, value):
|
|
599
|
+
if name in ("minimizes", "schedule", "over"):
|
|
600
|
+
setattr(self, name, value)
|
|
601
|
+
if name == "schedule":
|
|
602
|
+
self.reschedule()
|
|
603
|
+
return
|
|
604
|
+
self.params[name] = value
|
|
605
|
+
if self.real is None:
|
|
606
|
+
return
|
|
607
|
+
for index, (group, origin) in enumerate(zip(self.real.param_groups, self.origins)):
|
|
608
|
+
if origin is not None and name in origin.params:
|
|
609
|
+
continue
|
|
610
|
+
group[name] = value
|
|
611
|
+
if name == "lr":
|
|
612
|
+
self.base[index] = float(value)
|
|
613
|
+
self.reschedule()
|
|
614
|
+
|
|
615
|
+
def state_dict(self):
|
|
616
|
+
inner = self.pending if self.real is None else self.real.state_dict()
|
|
617
|
+
state = {"torch": inner, "updates": self.updates, "params": dict(self.params),
|
|
618
|
+
"base": None if self.base is None else list(self.base)}
|
|
619
|
+
saver = getattr(self.scaler, "state_dict", None)
|
|
620
|
+
if self.scaling() and callable(saver):
|
|
621
|
+
state["scaler"] = saver()
|
|
622
|
+
return state
|
|
623
|
+
|
|
624
|
+
def load_state_dict(self, state):
|
|
625
|
+
if not state:
|
|
626
|
+
return
|
|
627
|
+
self.updates = int(state.get("updates", 0))
|
|
628
|
+
loader = getattr(self.scaler, "load_state_dict", None)
|
|
629
|
+
if state.get("scaler") is not None and callable(loader):
|
|
630
|
+
loader(state["scaler"])
|
|
631
|
+
inner = state.get("torch")
|
|
632
|
+
if inner is None:
|
|
633
|
+
return
|
|
634
|
+
if self.real is None:
|
|
635
|
+
self.pending = inner
|
|
636
|
+
return
|
|
637
|
+
self.real.load_state_dict(inner)
|
|
638
|
+
self.restore_params()
|
|
639
|
+
self.reschedule()
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def freeze_parameters(module, spec):
|
|
643
|
+
module = unwrap(module)
|
|
644
|
+
if spec is True:
|
|
645
|
+
for parameter in module.parameters():
|
|
646
|
+
parameter.requires_grad_(False)
|
|
647
|
+
return
|
|
648
|
+
patterns = [] if not spec else ([spec] if isinstance(spec, str) else [str(pattern) for pattern in spec])
|
|
649
|
+
for name, parameter in module.named_parameters():
|
|
650
|
+
if any(fnmatch.fnmatchcase(name, pattern) for pattern in patterns):
|
|
651
|
+
parameter.requires_grad_(False)
|
|
652
|
+
elif parameter.is_floating_point() or parameter.is_complex():
|
|
653
|
+
parameter.requires_grad_(True)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def active(entry, set_name, turn):
|
|
657
|
+
on = getattr(entry, "on", None)
|
|
658
|
+
if on is not None and set_name not in on:
|
|
659
|
+
return False
|
|
660
|
+
every = int(getattr(entry, "every", 1) or 1)
|
|
661
|
+
return every <= 1 or turn % every == 0
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def loss_scalar(value, name):
|
|
665
|
+
if isinstance(value, dict):
|
|
666
|
+
if "loss" not in value:
|
|
667
|
+
raise ValueError(f"loss {name!r} returned a mapping without a 'loss' key; its keys are {sorted(value)}")
|
|
668
|
+
return value["loss"]
|
|
669
|
+
if not isinstance(value, torch.Tensor):
|
|
670
|
+
raise ValueError(f"loss {name!r} returned {type(value).__name__}, not a tensor")
|
|
671
|
+
return value
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from .compare import aligned_series, config_diff, config_of, differing_params # noqa: F401
|
|
2
|
+
from .reader import ( # noqa: F401
|
|
3
|
+
best_of,
|
|
4
|
+
data_report_of,
|
|
5
|
+
events_of,
|
|
6
|
+
fields_of,
|
|
7
|
+
files_of,
|
|
8
|
+
is_record,
|
|
9
|
+
model_summary_of,
|
|
10
|
+
plots_of,
|
|
11
|
+
progress_of,
|
|
12
|
+
read_events,
|
|
13
|
+
read_history,
|
|
14
|
+
read_manifest,
|
|
15
|
+
read_plan,
|
|
16
|
+
read_updates,
|
|
17
|
+
records_under,
|
|
18
|
+
series_of,
|
|
19
|
+
state_of,
|
|
20
|
+
summary_of,
|
|
21
|
+
updates_of,
|
|
22
|
+
)
|
|
23
|
+
from .stats import prediction_pairs, prediction_stats, prediction_tables, read_predictions # noqa: F401
|
|
24
|
+
from .sweep import best_point, group_folds, is_sweep, rank_points, read_sweep, sweep_points # noqa: F401
|