initium-deeplearning 0.1.0__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.
- initium/LNN_controller/cfc_backbone_parallel.py +265 -0
- initium/LNN_controller/cfc_controller.py +325 -0
- initium/LNN_controller/chained_controller.py +83 -0
- initium/LNN_controller/hybrid_controller.py +90 -0
- initium/MoE/README.md +1 -0
- initium/MoE/moe_layer.py +552 -0
- initium/__init__.py +1 -0
- initium/config/controller_config.py +86 -0
- initium/config/train_config.py +64 -0
- initium/core_training.py +2182 -0
- initium/data/__init__.py +0 -0
- initium/data/audio/__init__.py +0 -0
- initium/data/audio/audio_dataset.py +96 -0
- initium/data/base_dataset.py +75 -0
- initium/data/common/__init__.py +0 -0
- initium/data/common/chain_task.py +542 -0
- initium/data/common/digit_codec.py +85 -0
- initium/data/common/graph_io.py +123 -0
- initium/data/common/real_data.py +747 -0
- initium/data/dataset_registry.py +59 -0
- initium/data/graph_traversal/__init__.py +0 -0
- initium/data/graph_traversal/graph_traversal.py +746 -0
- initium/data/multimodal/__init__.py +0 -0
- initium/data/multimodal/multimodal_dataset.py +401 -0
- initium/data/text/__init__.py +0 -0
- initium/data/text/text_dataset.py +79 -0
- initium/data/video/__init__.py +0 -0
- initium/data/video/video_dataset.py +85 -0
- initium/inference/__init__.py +1 -0
- initium/inference/cache/__init__.py +1 -0
- initium/inference/cache/base_cache.py +112 -0
- initium/inference/cache/cache_config.py +21 -0
- initium/inference/cache/cache_registry.py +99 -0
- initium/inference/cache/cached_engine.py +264 -0
- initium/inference/cache/keys.py +77 -0
- initium/inference/cache/prefix_cache.py +59 -0
- initium/inference/cache/result_cache.py +57 -0
- initium/inference/cache/state_utils.py +74 -0
- initium/inference/cache/stores.py +167 -0
- initium/inference/capabilities.py +86 -0
- initium/inference/checkpoint_io.py +63 -0
- initium/inference/engine.py +88 -0
- initium/inference/inference_config.py +22 -0
- initium/inference/metrics.py +103 -0
- initium/inference/model_loader.py +270 -0
- initium/inference/run_inference.py +526 -0
- initium/inference/run_logging.py +173 -0
- initium/inference/tasks/__init__.py +1 -0
- initium/inference/tasks/audio_task.py +114 -0
- initium/inference/tasks/base_task.py +60 -0
- initium/inference/tasks/graph_traversal_task.py +235 -0
- initium/inference/tasks/multimodal_task.py +74 -0
- initium/inference/tasks/task_registry.py +68 -0
- initium/inference/tasks/text_task.py +114 -0
- initium/inference/tasks/video_task.py +114 -0
- initium/mamba_controller/mamba2_controller.py +433 -0
- initium/mamba_controller/mamba3_controller.py +242 -0
- initium/mamba_controller/mamba_backbone_parallel.py +291 -0
- initium/mamba_controller/mamba_controller.py +983 -0
- initium/mamba_controller/split_graph_dnc.py +623 -0
- initium/memory_manipulation/dynamic_memory_resize.py +157 -0
- initium/memory_manipulation/dynamic_n_controller.py +152 -0
- initium/memory_manipulation/link_matrix_ablation.py +130 -0
- initium/memory_manipulation/nvrtc_compat.py +143 -0
- initium/memory_manipulation/stochastic_write_head_v2.py +545 -0
- initium/setup_wheels.py +175 -0
- initium_deeplearning-0.1.0.dist-info/METADATA +253 -0
- initium_deeplearning-0.1.0.dist-info/RECORD +71 -0
- initium_deeplearning-0.1.0.dist-info/WHEEL +4 -0
- initium_deeplearning-0.1.0.dist-info/entry_points.txt +3 -0
- initium_deeplearning-0.1.0.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: data/common/chain_task.py
|
|
3
|
+
|
|
4
|
+
Generic "K facts, then M chained queries" recall+aggregation task -- shared
|
|
5
|
+
scaffolding for the text / audio / video dataset modules. Mirrors
|
|
6
|
+
data/graph_traversal/graph_traversal.py's overall shape (digit-encoded
|
|
7
|
+
fields, curriculum, evaluate_*, depth breakdown, OOD eval against a fixed
|
|
8
|
+
held-out table, memory-ablation eval, robustness eval) but factored so the
|
|
9
|
+
mechanical plumbing isn't re-derived three times.
|
|
10
|
+
|
|
11
|
+
What stays per-modality (the "treat each modality individually" seam, NOT
|
|
12
|
+
shared): the semantic meaning of a "fact"/"query" (see each of
|
|
13
|
+
data/text/text_dataset.py, data/audio/audio_dataset.py,
|
|
14
|
+
data/video/video_dataset.py), the OOD generalization axis
|
|
15
|
+
(_synthetic_ood_facts), and the robustness perturbation (perturb_fact).
|
|
16
|
+
|
|
17
|
+
Real-data hook (v2): `_fact_pool` / `_test_fact_pool` (None by default --
|
|
18
|
+
unchanged synthetic behavior). A subclass whose __init__ was given a real
|
|
19
|
+
dataset_link/test_dataset_link (see data/common/real_data.py) sets these to
|
|
20
|
+
a real (key,value) fact pool built from that source; build_episode() then
|
|
21
|
+
samples FROM that pool instead of drawing fresh random labels, and
|
|
22
|
+
build_ood_facts() uses the disjoint real test pool instead of the seeded
|
|
23
|
+
synthetic table -- the modality analogue of "train on synthetic graphs,
|
|
24
|
+
test on the held-out London Underground graph."
|
|
25
|
+
|
|
26
|
+
Task shape: present `num_facts` (key, value) pairs (digit-coded labels in
|
|
27
|
+
[0, label_range)), shuffled, first item phase-tagged (mirrors graph's own
|
|
28
|
+
convention exactly). Then `num_queries` query keys in sequence; each answer
|
|
29
|
+
step must predict:
|
|
30
|
+
- `value` : the fact value for that key (single-hop recall -- analogue of
|
|
31
|
+
graph's dst|src+edge lookup check)
|
|
32
|
+
- `cumsum` : (running sum of every value answered so far) mod label_range
|
|
33
|
+
(forces genuine sequential state -- analogue of graph's
|
|
34
|
+
src|prev_dst_ok chain check)
|
|
35
|
+
Depth axis = num_queries (this family's analogue of "hop count").
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import random
|
|
41
|
+
|
|
42
|
+
import torch
|
|
43
|
+
|
|
44
|
+
from initium.data.base_dataset import BaseDataset
|
|
45
|
+
from initium.data.common.digit_codec import DigitCodec, digit_field_diversity, digit_field_loss
|
|
46
|
+
from initium.memory_manipulation.dynamic_memory_resize import resize_memory
|
|
47
|
+
|
|
48
|
+
NUM_FIELDS = 2 # [value, cumsum]
|
|
49
|
+
NUM_PHASE_CHANNELS = 2
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def score_answer_steps(output, target_digits, answer_mask, codec, verbose=False, field_log=None):
|
|
53
|
+
"""Shared per-episode answer scoring: decode each answer step's (value,
|
|
54
|
+
cumsum) fields, compare to target_digits, optionally print, optionally
|
|
55
|
+
accumulate a per-(episode_depth, hop_position) breakdown. Used by both
|
|
56
|
+
KVChainDataset.evaluate_chain (text/audio/video) and
|
|
57
|
+
MultimodalDataset.evaluate_chain (data/multimodal/multimodal_dataset.py)
|
|
58
|
+
so the two families can never silently drift in what counts as
|
|
59
|
+
'correct', how depth/perfect-episode bookkeeping is derived, or how the
|
|
60
|
+
field_log is keyed.
|
|
61
|
+
-> (ep_total, ep_correct, episode_perfect, correct_value, correct_cumsum, n_steps)
|
|
62
|
+
ep_total/ep_correct: this episode's step-count / correct-field-count
|
|
63
|
+
(2 fields per step), for the caller's by_depth breakdown.
|
|
64
|
+
correct_value/correct_cumsum/n_steps: per-field correct counts and step
|
|
65
|
+
count, for the caller's running totals (separate value vs cumsum
|
|
66
|
+
accuracy in the final summary print).
|
|
67
|
+
field_log: optional dict, keyed by (depth, hop_position) -> [n, value_ok,
|
|
68
|
+
cumsum_ok], filled in place. depth = this episode's total number of
|
|
69
|
+
answer steps (its 'num_queries' -- the family's hop-count analogue),
|
|
70
|
+
so this groups hop position WITHIN episodes of the same length,
|
|
71
|
+
mirroring graph_traversal.py's (path_length, hop_position) keying.
|
|
72
|
+
Lets a caller ask "does cumsum specifically degrade at later hop
|
|
73
|
+
positions within an episode (a capacity/forgetting signal), or is it
|
|
74
|
+
uniformly low across positions (a training-progress signal)?"
|
|
75
|
+
"""
|
|
76
|
+
D = codec.label_dim
|
|
77
|
+
nd = codec.num_digits
|
|
78
|
+
answer_idx = (answer_mask == 1).nonzero(as_tuple=True)[0]
|
|
79
|
+
depth = int(answer_idx.shape[0])
|
|
80
|
+
ep_total = ep_correct = 0
|
|
81
|
+
episode_perfect = True
|
|
82
|
+
correct_value = correct_cumsum = n_steps = 0
|
|
83
|
+
for hop_pos, idx in enumerate(answer_idx, start=1):
|
|
84
|
+
v_pred = codec.decode_field(output[idx][0:D])
|
|
85
|
+
c_pred = codec.decode_field(output[idx][D : 2 * D])
|
|
86
|
+
td = target_digits[idx].tolist()
|
|
87
|
+
v_tgt = int("".join(map(str, td[0:nd])))
|
|
88
|
+
c_tgt = int("".join(map(str, td[nd : 2 * nd])))
|
|
89
|
+
v_ok, c_ok = v_pred == v_tgt, c_pred == c_tgt
|
|
90
|
+
correct_value += int(v_ok)
|
|
91
|
+
correct_cumsum += int(c_ok)
|
|
92
|
+
ep_correct += int(v_ok) + int(c_ok)
|
|
93
|
+
n_steps += 1
|
|
94
|
+
ep_total += 1
|
|
95
|
+
if not (v_ok and c_ok):
|
|
96
|
+
episode_perfect = False
|
|
97
|
+
if verbose:
|
|
98
|
+
print(
|
|
99
|
+
f" step {hop_pos}: value {v_pred}/{v_tgt} ok={v_ok} | cumsum {c_pred}/{c_tgt} ok={c_ok}"
|
|
100
|
+
)
|
|
101
|
+
if field_log is not None:
|
|
102
|
+
fl = field_log.setdefault((depth, hop_pos), [0, 0, 0])
|
|
103
|
+
fl[0] += 1
|
|
104
|
+
fl[1] += int(v_ok)
|
|
105
|
+
fl[2] += int(c_ok)
|
|
106
|
+
return ep_total, ep_correct, episode_perfect, correct_value, correct_cumsum, n_steps
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def write_value_cumsum_field_log(writer, file, step, lesson, eval_type, field_log):
|
|
110
|
+
"""Shared field_log writer for the value/cumsum family (KVChainDataset
|
|
111
|
+
and MultimodalDataset both fill field_log via score_answer_steps above,
|
|
112
|
+
so they share this same (depth, hop_position) -> [n, value_ok,
|
|
113
|
+
cumsum_ok] schema and can share how it's dumped/printed)."""
|
|
114
|
+
for (depth, hop_pos), (n, v_ok, c_ok) in sorted(field_log.items()):
|
|
115
|
+
writer.writerow(
|
|
116
|
+
[step, lesson, eval_type, depth, hop_pos, n, v_ok / n * 100, c_ok / n * 100]
|
|
117
|
+
)
|
|
118
|
+
file.flush()
|
|
119
|
+
agg = {}
|
|
120
|
+
for (_depth, hop_pos), v in field_log.items():
|
|
121
|
+
a = agg.setdefault(hop_pos, [0, 0, 0])
|
|
122
|
+
for i in range(3):
|
|
123
|
+
a[i] += v[i]
|
|
124
|
+
parts = []
|
|
125
|
+
for hop_pos in sorted(agg)[:4]:
|
|
126
|
+
n, v_ok, c_ok = agg[hop_pos]
|
|
127
|
+
parts.append(
|
|
128
|
+
f"hop{hop_pos}: value {v_ok / n * 100:.0f} cumsum {c_ok / n * 100:.0f} (n={n})"
|
|
129
|
+
)
|
|
130
|
+
print(f" [field acc {eval_type} step {step}] " + " | ".join(parts))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def value_cumsum_field_log_header():
|
|
134
|
+
return ["depth", "hop_position", "n_steps", "value_acc", "cumsum_acc"]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class ChainCurriculum:
|
|
138
|
+
def __init__(self, dataset, table, lesson_nr_cells):
|
|
139
|
+
assert len(table) == len(lesson_nr_cells)
|
|
140
|
+
self.dataset = dataset
|
|
141
|
+
self.table = table
|
|
142
|
+
self.lesson = 0
|
|
143
|
+
self.lesson_nr_cells = list(lesson_nr_cells)
|
|
144
|
+
|
|
145
|
+
def maybe_advance(self, model, device, step=None, optimizer=None):
|
|
146
|
+
nf, nq = self.table[self.lesson]
|
|
147
|
+
combined_acc, perfect_frac, breakdown = self.dataset.evaluate_chain(
|
|
148
|
+
model,
|
|
149
|
+
device,
|
|
150
|
+
num_episodes=self.dataset.eval_batch_size,
|
|
151
|
+
num_facts_range=nf,
|
|
152
|
+
num_queries_range=nq,
|
|
153
|
+
step_breakdown=True,
|
|
154
|
+
field_log=(field_log := {}),
|
|
155
|
+
)
|
|
156
|
+
if breakdown:
|
|
157
|
+
print(
|
|
158
|
+
f" [lesson {self.lesson + 1} eval by depth] "
|
|
159
|
+
+ ", ".join(
|
|
160
|
+
f"{d}-step: acc {a:.1f}% perfect {p:.1f}% (n={n})"
|
|
161
|
+
for d, (a, p, n) in breakdown.items()
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
hop_writer = getattr(self, "hop_log_writer", None)
|
|
165
|
+
if hop_writer is not None:
|
|
166
|
+
for d, (a, p, n) in sorted(breakdown.items()):
|
|
167
|
+
hop_writer.writerow([step, self.lesson + 1, "id", d, a, p, n])
|
|
168
|
+
self.hop_log_file.flush()
|
|
169
|
+
|
|
170
|
+
field_writer = getattr(self, "field_log_writer", None)
|
|
171
|
+
if field_writer is not None and field_log:
|
|
172
|
+
self.dataset.write_field_log(
|
|
173
|
+
field_writer, self.field_log_file, step, self.lesson + 1, "id", field_log
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
if (
|
|
177
|
+
combined_acc / 100.0 >= self.dataset.advance_threshold
|
|
178
|
+
and self.lesson < len(self.table) - 1
|
|
179
|
+
):
|
|
180
|
+
self.lesson += 1
|
|
181
|
+
new_n = self.lesson_nr_cells[self.lesson]
|
|
182
|
+
if new_n != self.lesson_nr_cells[self.lesson - 1]:
|
|
183
|
+
resize_memory(model, new_n, device=device, optimizer=optimizer)
|
|
184
|
+
print(f">>> Memory resized to nr_cells={new_n} for lesson {self.lesson + 1}")
|
|
185
|
+
print(f">>> Curriculum advanced to lesson {self.lesson + 1}/{len(self.table)}")
|
|
186
|
+
writer = getattr(self, "advance_log_writer", None)
|
|
187
|
+
if writer is not None:
|
|
188
|
+
writer.writerow([step, self.lesson + 1, len(self.table)])
|
|
189
|
+
self.advance_log_file.flush()
|
|
190
|
+
return self.lesson, combined_acc, perfect_frac
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _as_range(v):
|
|
194
|
+
"""Accept either a fixed int or an (lo, hi) range; normalize to a range."""
|
|
195
|
+
return tuple(v) if isinstance(v, tuple | list) else (v, v)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class KVChainDataset(BaseDataset):
|
|
199
|
+
"""Parameterized by a few modality-specific hooks (see below). Concrete
|
|
200
|
+
text/audio/video dataset classes only override `name`, the curriculum
|
|
201
|
+
table/lesson_nr_cells, `_synthetic_ood_facts()`, and optionally
|
|
202
|
+
`perturb_fact()` -- plus, when a real dataset_link is given, they set
|
|
203
|
+
`_fact_pool`/`_test_fact_pool` in their own __init__ (see module
|
|
204
|
+
docstring's "Real-data hook")."""
|
|
205
|
+
|
|
206
|
+
advance_threshold = 0.85
|
|
207
|
+
label_range = 1000
|
|
208
|
+
codec = DigitCodec(num_digits=3, digit_base=10)
|
|
209
|
+
input_dim = 2 * codec.label_dim + NUM_PHASE_CHANNELS # 62
|
|
210
|
+
output_dim = NUM_FIELDS * codec.label_dim # 60
|
|
211
|
+
eval_batch_size = 100
|
|
212
|
+
old_lesson_mix_rate = 0.10
|
|
213
|
+
ood_query_range = (3, 8)
|
|
214
|
+
|
|
215
|
+
# Real-data hook (v2): None -> unchanged synthetic behavior everywhere
|
|
216
|
+
# below. Set by a subclass's __init__ when a real dataset_link/
|
|
217
|
+
# test_dataset_link was given.
|
|
218
|
+
_fact_pool: list[tuple[int, int]] | None = None
|
|
219
|
+
_test_fact_pool: list[tuple[int, int]] | None = None
|
|
220
|
+
_table: list
|
|
221
|
+
_lesson_nr_cells: list[int]
|
|
222
|
+
|
|
223
|
+
# ---- modality-specific hooks -------------------------------------------
|
|
224
|
+
def build_ood_facts(self, n_facts: int = 20):
|
|
225
|
+
"""Fixed held-out fact table -- the analogue of graph's
|
|
226
|
+
build_london_underground_eval(): unseen at training time,
|
|
227
|
+
reproducible across runs. Uses the real, disjoint-key test pool
|
|
228
|
+
when one was loaded (see module docstring); otherwise falls back to
|
|
229
|
+
each subclass's own seeded synthetic table via
|
|
230
|
+
_synthetic_ood_facts()."""
|
|
231
|
+
if self._test_fact_pool is not None:
|
|
232
|
+
rng = random.Random(999)
|
|
233
|
+
pool = self._test_fact_pool
|
|
234
|
+
return rng.sample(pool, min(n_facts, len(pool)))
|
|
235
|
+
return self._synthetic_ood_facts(n_facts)
|
|
236
|
+
|
|
237
|
+
def _synthetic_ood_facts(self, n_facts: int = 20):
|
|
238
|
+
"""Default synthetic seeded held-out table. Override per modality
|
|
239
|
+
for a more domain-meaningful OOD story (see each of
|
|
240
|
+
data/text/text_dataset.py etc.)."""
|
|
241
|
+
rng = random.Random(1234)
|
|
242
|
+
keys = rng.sample(range(self.label_range), n_facts)
|
|
243
|
+
vals = [rng.randrange(self.label_range) for _ in range(n_facts)]
|
|
244
|
+
return list(zip(keys, vals))
|
|
245
|
+
|
|
246
|
+
def perturb_fact(self, key_vec: torch.Tensor, value_vec: torch.Tensor, rng, severity: float):
|
|
247
|
+
"""Robustness hook (Layer D). Default: flip a random one-hot
|
|
248
|
+
digit-slot with prob=severity ("token corruption"). Override for a
|
|
249
|
+
more domain-meaningful perturbation."""
|
|
250
|
+
|
|
251
|
+
def _flip(vec):
|
|
252
|
+
vec = vec.clone()
|
|
253
|
+
if rng.random() < severity:
|
|
254
|
+
nd, db = self.codec.num_digits, self.codec.digit_base
|
|
255
|
+
pos = rng.randrange(nd)
|
|
256
|
+
vec[pos * db : (pos + 1) * db] = 0.0
|
|
257
|
+
vec[pos * db + rng.randrange(db)] = 1.0
|
|
258
|
+
return vec
|
|
259
|
+
|
|
260
|
+
return _flip(key_vec), _flip(value_vec)
|
|
261
|
+
|
|
262
|
+
# ---- curriculum ---------------------------------------------------------
|
|
263
|
+
def make_curriculum(self):
|
|
264
|
+
return ChainCurriculum(self, self._table, self._lesson_nr_cells)
|
|
265
|
+
|
|
266
|
+
# ---- episode construction ------------------------------------------------
|
|
267
|
+
def _encode_fact(self, key, value, phase_transition, perturb=None, rng=None):
|
|
268
|
+
key_vec = self.codec.encode_label(key)
|
|
269
|
+
val_vec = self.codec.encode_label(value)
|
|
270
|
+
if perturb:
|
|
271
|
+
key_vec, val_vec = self.perturb_fact(key_vec, val_vec, rng or random, perturb)
|
|
272
|
+
return torch.cat([key_vec, val_vec, torch.tensor([phase_transition, 0.0])])
|
|
273
|
+
|
|
274
|
+
def _encode_query(self, key, phase_transition):
|
|
275
|
+
return torch.cat(
|
|
276
|
+
[
|
|
277
|
+
self.codec.encode_label(key),
|
|
278
|
+
torch.zeros(self.codec.label_dim),
|
|
279
|
+
torch.tensor([phase_transition, 0.0]),
|
|
280
|
+
]
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
def _encode_answer(self, phase_transition):
|
|
284
|
+
return torch.cat(
|
|
285
|
+
[torch.zeros(2 * self.codec.label_dim), torch.tensor([phase_transition, 1.0])]
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
def build_episode(self, num_facts_range, num_queries_range, rng=None, perturb=None):
|
|
289
|
+
rng = rng if rng is not None else random
|
|
290
|
+
num_facts_range = _as_range(num_facts_range)
|
|
291
|
+
num_queries_range = _as_range(num_queries_range)
|
|
292
|
+
num_facts = rng.randint(*num_facts_range)
|
|
293
|
+
if self._fact_pool is not None:
|
|
294
|
+
# Real-data hook: sample real (key,value) facts from the loaded
|
|
295
|
+
# pool instead of drawing fresh random labels every episode.
|
|
296
|
+
num_facts = min(num_facts, len(self._fact_pool))
|
|
297
|
+
facts = rng.sample(self._fact_pool, num_facts)
|
|
298
|
+
else:
|
|
299
|
+
keys = rng.sample(range(self.label_range), num_facts)
|
|
300
|
+
vals = [rng.randrange(self.label_range) for _ in range(num_facts)]
|
|
301
|
+
facts = list(zip(keys, vals))
|
|
302
|
+
rng.shuffle(facts)
|
|
303
|
+
num_queries = rng.randint(*num_queries_range)
|
|
304
|
+
fact_keys = [k for k, _ in facts]
|
|
305
|
+
query_keys = [rng.choice(fact_keys) for _ in range(num_queries)]
|
|
306
|
+
|
|
307
|
+
NF = self.codec.num_digits
|
|
308
|
+
inputs, target_digits, answer_mask = [], [], []
|
|
309
|
+
for i, (k, v) in enumerate(facts):
|
|
310
|
+
inputs.append(self._encode_fact(k, v, 1.0 if i == 0 else 0.0, perturb=perturb, rng=rng))
|
|
311
|
+
target_digits.append([0] * (2 * NF))
|
|
312
|
+
answer_mask.append(0)
|
|
313
|
+
for i, k in enumerate(query_keys):
|
|
314
|
+
inputs.append(self._encode_query(k, 1.0 if i == 0 else 0.0))
|
|
315
|
+
target_digits.append([0] * (2 * NF))
|
|
316
|
+
answer_mask.append(0)
|
|
317
|
+
value_of, cumsum = dict(facts), 0
|
|
318
|
+
for i, k in enumerate(query_keys):
|
|
319
|
+
cumsum = (cumsum + value_of[k]) % self.label_range
|
|
320
|
+
inputs.append(self._encode_answer(1.0 if i == 0 else 0.0))
|
|
321
|
+
target_digits.append(
|
|
322
|
+
self.codec.label_to_digit_targets(value_of[k])
|
|
323
|
+
+ self.codec.label_to_digit_targets(cumsum)
|
|
324
|
+
)
|
|
325
|
+
answer_mask.append(1)
|
|
326
|
+
return (
|
|
327
|
+
torch.stack(inputs),
|
|
328
|
+
torch.tensor(target_digits, dtype=torch.long),
|
|
329
|
+
torch.tensor(answer_mask, dtype=torch.float32),
|
|
330
|
+
num_queries,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
def collate(self, batch):
|
|
334
|
+
max_len = max(len(x[0]) for x in batch)
|
|
335
|
+
B = len(batch)
|
|
336
|
+
padded_input = torch.zeros(B, max_len, self.input_dim)
|
|
337
|
+
padded_targets = torch.zeros(
|
|
338
|
+
B, max_len, NUM_FIELDS * self.codec.num_digits, dtype=torch.long
|
|
339
|
+
)
|
|
340
|
+
padded_mask = torch.zeros(B, max_len)
|
|
341
|
+
for i, (inp, tgt, mask, _) in enumerate(batch):
|
|
342
|
+
T = inp.size(0)
|
|
343
|
+
padded_input[i, :T] = inp
|
|
344
|
+
padded_targets[i, :T] = tgt
|
|
345
|
+
padded_mask[i, :T] = mask
|
|
346
|
+
return padded_input, padded_targets, padded_mask
|
|
347
|
+
|
|
348
|
+
def sample_batch(self, curriculum, batch_size):
|
|
349
|
+
episodes = []
|
|
350
|
+
for _ in range(batch_size):
|
|
351
|
+
idx = (
|
|
352
|
+
random.randint(0, curriculum.lesson - 1)
|
|
353
|
+
if curriculum.lesson > 0 and random.random() < self.old_lesson_mix_rate
|
|
354
|
+
else curriculum.lesson
|
|
355
|
+
)
|
|
356
|
+
nf, nq = curriculum.table[idx]
|
|
357
|
+
episodes.append(self.build_episode(nf, nq))
|
|
358
|
+
return self.collate(episodes)
|
|
359
|
+
|
|
360
|
+
# ---- loss / diversity -----------------------------------------------------
|
|
361
|
+
def loss(self, output, target, mask):
|
|
362
|
+
return digit_field_loss(output, target, mask, NUM_FIELDS, self.codec.digit_base)
|
|
363
|
+
|
|
364
|
+
def diversity(self, output, target, mask):
|
|
365
|
+
return digit_field_diversity(
|
|
366
|
+
output, mask, NUM_FIELDS * self.codec.num_digits, self.codec.digit_base
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
def set_output_proj(self, proj):
|
|
370
|
+
self._output_proj = proj
|
|
371
|
+
|
|
372
|
+
# ---- evaluation -------------------------------------------------------------
|
|
373
|
+
def _decode_answer(self, output_step):
|
|
374
|
+
D = self.codec.label_dim
|
|
375
|
+
return self.codec.decode_field(output_step[0:D]), self.codec.decode_field(
|
|
376
|
+
output_step[D : 2 * D]
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
def evaluate_chain(
|
|
380
|
+
self,
|
|
381
|
+
model,
|
|
382
|
+
device,
|
|
383
|
+
num_episodes,
|
|
384
|
+
num_facts_range=None,
|
|
385
|
+
num_queries_range=None,
|
|
386
|
+
fixed_facts=None,
|
|
387
|
+
rng=None,
|
|
388
|
+
ablate_memory=False,
|
|
389
|
+
step_breakdown=False,
|
|
390
|
+
perturb=None,
|
|
391
|
+
verbose_n=0,
|
|
392
|
+
skip_stages=None,
|
|
393
|
+
field_log=None,
|
|
394
|
+
):
|
|
395
|
+
model.eval()
|
|
396
|
+
total = correct_value = correct_cumsum = perfect_episodes = tested = 0
|
|
397
|
+
by_depth = {}
|
|
398
|
+
_rng = rng or random
|
|
399
|
+
with torch.no_grad():
|
|
400
|
+
while tested < num_episodes:
|
|
401
|
+
if fixed_facts is not None:
|
|
402
|
+
keys = [k for k, _ in fixed_facts]
|
|
403
|
+
vals_map = dict(fixed_facts)
|
|
404
|
+
nq = _rng.randint(*_as_range(num_queries_range or self.ood_query_range))
|
|
405
|
+
query_keys = [_rng.choice(keys) for _ in range(nq)]
|
|
406
|
+
facts = list(fixed_facts)
|
|
407
|
+
_rng.shuffle(facts)
|
|
408
|
+
NF = self.codec.num_digits
|
|
409
|
+
inputs, target_digits, answer_mask = [], [], []
|
|
410
|
+
for i, (k, v) in enumerate(facts):
|
|
411
|
+
inputs.append(
|
|
412
|
+
self._encode_fact(
|
|
413
|
+
k, v, 1.0 if i == 0 else 0.0, perturb=perturb, rng=_rng
|
|
414
|
+
)
|
|
415
|
+
)
|
|
416
|
+
target_digits.append([0] * (2 * NF))
|
|
417
|
+
answer_mask.append(0)
|
|
418
|
+
for i, k in enumerate(query_keys):
|
|
419
|
+
inputs.append(self._encode_query(k, 1.0 if i == 0 else 0.0))
|
|
420
|
+
target_digits.append([0] * (2 * NF))
|
|
421
|
+
answer_mask.append(0)
|
|
422
|
+
cumsum = 0
|
|
423
|
+
for i, k in enumerate(query_keys):
|
|
424
|
+
cumsum = (cumsum + vals_map[k]) % self.label_range
|
|
425
|
+
inputs.append(self._encode_answer(1.0 if i == 0 else 0.0))
|
|
426
|
+
target_digits.append(
|
|
427
|
+
self.codec.label_to_digit_targets(vals_map[k])
|
|
428
|
+
+ self.codec.label_to_digit_targets(cumsum)
|
|
429
|
+
)
|
|
430
|
+
answer_mask.append(1)
|
|
431
|
+
ep = (
|
|
432
|
+
torch.stack(inputs),
|
|
433
|
+
torch.tensor(target_digits, dtype=torch.long),
|
|
434
|
+
torch.tensor(answer_mask, dtype=torch.float32),
|
|
435
|
+
nq,
|
|
436
|
+
)
|
|
437
|
+
else:
|
|
438
|
+
ep = self.build_episode(
|
|
439
|
+
num_facts_range, num_queries_range, rng=_rng, perturb=perturb
|
|
440
|
+
)
|
|
441
|
+
input_seq, target_digits, answer_mask, _ = ep
|
|
442
|
+
input_seq = input_seq.unsqueeze(0).to(device)
|
|
443
|
+
output, _ = model(
|
|
444
|
+
input_seq,
|
|
445
|
+
(None, None, None),
|
|
446
|
+
reset_experience=True,
|
|
447
|
+
pass_through_memory=not ablate_memory,
|
|
448
|
+
**({"combiner_skip_stages": skip_stages} if skip_stages else {}),
|
|
449
|
+
)
|
|
450
|
+
output = self._output_proj(output.transpose(0, 1).contiguous().squeeze(0))
|
|
451
|
+
|
|
452
|
+
ep_total, ep_correct, episode_perfect, ep_cv, ep_cc, ep_n = score_answer_steps(
|
|
453
|
+
output,
|
|
454
|
+
target_digits,
|
|
455
|
+
answer_mask,
|
|
456
|
+
self.codec,
|
|
457
|
+
verbose=(tested < verbose_n),
|
|
458
|
+
field_log=field_log,
|
|
459
|
+
)
|
|
460
|
+
correct_value += ep_cv
|
|
461
|
+
correct_cumsum += ep_cc
|
|
462
|
+
total += ep_n
|
|
463
|
+
perfect_episodes += int(episode_perfect)
|
|
464
|
+
tested += 1
|
|
465
|
+
if step_breakdown:
|
|
466
|
+
acc = by_depth.setdefault(ep_total, [0, 0, 0, 0])
|
|
467
|
+
acc[0] += 2 * ep_total
|
|
468
|
+
acc[1] += ep_correct
|
|
469
|
+
acc[2] += 1
|
|
470
|
+
acc[3] += int(episode_perfect)
|
|
471
|
+
|
|
472
|
+
combined_acc = 100.0 * (correct_value + correct_cumsum) / max(2 * total, 1)
|
|
473
|
+
perfect_frac = 100.0 * perfect_episodes / max(tested, 1)
|
|
474
|
+
print(
|
|
475
|
+
f"Eval [{self.name}]: value {100.0 * correct_value / max(total, 1):.2f}% | "
|
|
476
|
+
f"cumsum {100.0 * correct_cumsum / max(total, 1):.2f}% | combined {combined_acc:.2f}% | "
|
|
477
|
+
f"perfect {perfect_frac:.2f}% ({tested} episodes)"
|
|
478
|
+
)
|
|
479
|
+
model.train()
|
|
480
|
+
if step_breakdown:
|
|
481
|
+
breakdown = {
|
|
482
|
+
d: (100.0 * c / max(t, 1), 100.0 * p / max(e, 1), e)
|
|
483
|
+
for d, (t, c, e, p) in sorted(by_depth.items())
|
|
484
|
+
}
|
|
485
|
+
return combined_acc, perfect_frac, breakdown
|
|
486
|
+
return combined_acc, perfect_frac
|
|
487
|
+
|
|
488
|
+
def evaluate_ood(self, model, device, num_episodes, rng, verbose_n=0, field_log=None):
|
|
489
|
+
return self.evaluate_chain(
|
|
490
|
+
model,
|
|
491
|
+
device,
|
|
492
|
+
num_episodes,
|
|
493
|
+
fixed_facts=self.build_ood_facts(),
|
|
494
|
+
num_queries_range=self.ood_query_range,
|
|
495
|
+
rng=rng,
|
|
496
|
+
step_breakdown=True,
|
|
497
|
+
verbose_n=verbose_n,
|
|
498
|
+
field_log=field_log,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
def evaluate_id_ablated(self, model, device, curriculum, lesson_idx):
|
|
502
|
+
nf, nq = curriculum.table[lesson_idx]
|
|
503
|
+
return self.evaluate_chain(
|
|
504
|
+
model,
|
|
505
|
+
device,
|
|
506
|
+
self.eval_batch_size,
|
|
507
|
+
num_facts_range=nf,
|
|
508
|
+
num_queries_range=nq,
|
|
509
|
+
ablate_memory=True,
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
def evaluate_id_combiner_stage_ablated(
|
|
513
|
+
self, model, device, curriculum, lesson_idx, skip_stages
|
|
514
|
+
):
|
|
515
|
+
nf, nq = curriculum.table[lesson_idx]
|
|
516
|
+
return self.evaluate_chain(
|
|
517
|
+
model,
|
|
518
|
+
device,
|
|
519
|
+
self.eval_batch_size,
|
|
520
|
+
num_facts_range=nf,
|
|
521
|
+
num_queries_range=nq,
|
|
522
|
+
skip_stages=skip_stages,
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
def evaluate_robustness(self, model, device, curriculum, lesson_idx, perturbation, rng):
|
|
526
|
+
nf, nq = curriculum.table[lesson_idx]
|
|
527
|
+
severity = perturbation.get("severity", 0.2) if isinstance(perturbation, dict) else 0.2
|
|
528
|
+
return self.evaluate_chain(
|
|
529
|
+
model,
|
|
530
|
+
device,
|
|
531
|
+
self.eval_batch_size,
|
|
532
|
+
num_facts_range=nf,
|
|
533
|
+
num_queries_range=nq,
|
|
534
|
+
rng=rng,
|
|
535
|
+
perturb=severity,
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
def field_log_header(self):
|
|
539
|
+
return value_cumsum_field_log_header()
|
|
540
|
+
|
|
541
|
+
def write_field_log(self, writer, file, step, lesson, eval_type, field_log):
|
|
542
|
+
write_value_cumsum_field_log(writer, file, step, lesson, eval_type, field_log)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: data/common/digit_codec.py
|
|
3
|
+
|
|
4
|
+
Shared digit-based label encoding/decoding + loss/diversity, factored out of
|
|
5
|
+
data/graph_traversal/graph_traversal.py's LABEL_DIGITS/DIGIT_BASE/encode_label/
|
|
6
|
+
label_to_digits/digit_loss/prediction_diversity so every new modality dataset
|
|
7
|
+
(text/audio/video/multimodal) can reuse the exact same "predict N digits per
|
|
8
|
+
field, one-hot over DIGIT_BASE" convention instead of re-deriving it.
|
|
9
|
+
graph_traversal.py itself is left untouched (it predates this module and is a
|
|
10
|
+
validated Phase-2+ dataset not worth touching) - this is purely for NEW
|
|
11
|
+
dataset modules to import from.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import torch
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DigitCodec:
|
|
20
|
+
def __init__(self, num_digits: int = 3, digit_base: int = 10):
|
|
21
|
+
self.num_digits = num_digits
|
|
22
|
+
self.digit_base = digit_base
|
|
23
|
+
self.label_dim = num_digits * digit_base
|
|
24
|
+
self.label_range = digit_base**num_digits
|
|
25
|
+
|
|
26
|
+
def encode_label(self, label) -> torch.Tensor:
|
|
27
|
+
vec = torch.zeros(self.label_dim)
|
|
28
|
+
if label is None:
|
|
29
|
+
return vec
|
|
30
|
+
for pos, d in enumerate(self._digits(label)):
|
|
31
|
+
vec[pos * self.digit_base + d] = 1.0
|
|
32
|
+
return vec
|
|
33
|
+
|
|
34
|
+
def _digits(self, label: int):
|
|
35
|
+
s, out = label, []
|
|
36
|
+
for _ in range(self.num_digits):
|
|
37
|
+
out.append(s % self.digit_base)
|
|
38
|
+
s //= self.digit_base
|
|
39
|
+
return list(reversed(out))
|
|
40
|
+
|
|
41
|
+
def label_to_digit_targets(self, label: int):
|
|
42
|
+
return self._digits(label)
|
|
43
|
+
|
|
44
|
+
def decode_field(self, logits_flat: torch.Tensor) -> int:
|
|
45
|
+
"""logits_flat: (num_digits * digit_base,) -> int label."""
|
|
46
|
+
logits = logits_flat.view(self.num_digits, self.digit_base)
|
|
47
|
+
val = 0
|
|
48
|
+
for d in logits.argmax(dim=-1).tolist():
|
|
49
|
+
val = val * self.digit_base + d
|
|
50
|
+
return val
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def digit_field_loss(
|
|
54
|
+
output: torch.Tensor,
|
|
55
|
+
target_digits: torch.Tensor,
|
|
56
|
+
answer_mask: torch.Tensor,
|
|
57
|
+
num_fields: int,
|
|
58
|
+
digit_base: int,
|
|
59
|
+
) -> torch.Tensor:
|
|
60
|
+
"""Generalizes graph_traversal.digit_loss to an arbitrary field count
|
|
61
|
+
(graph uses num_fields=3 [src,edge,dst]; the chain-task family uses 2
|
|
62
|
+
[value,cumsum])."""
|
|
63
|
+
B, T, _ = output.shape
|
|
64
|
+
num_digits_total = target_digits.shape[-1]
|
|
65
|
+
logits = output.view(B, T, num_digits_total, digit_base)
|
|
66
|
+
log_probs = torch.log_softmax(logits, dim=-1)
|
|
67
|
+
gathered = torch.gather(log_probs, -1, target_digits.unsqueeze(-1)).squeeze(-1)
|
|
68
|
+
per_step_loss = -gathered.sum(dim=-1)
|
|
69
|
+
mask = answer_mask.float()
|
|
70
|
+
total = (per_step_loss * mask).sum()
|
|
71
|
+
denom = mask.sum().clamp(min=1.0)
|
|
72
|
+
return total / denom
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def digit_field_diversity(
|
|
76
|
+
output: torch.Tensor, answer_mask: torch.Tensor, num_digits_total: int, digit_base: int
|
|
77
|
+
) -> float:
|
|
78
|
+
mask = answer_mask.bool()
|
|
79
|
+
if mask.sum() == 0:
|
|
80
|
+
return 0.0
|
|
81
|
+
logits = output.view(*output.shape[:2], num_digits_total, digit_base)
|
|
82
|
+
preds = logits.argmax(dim=-1)[mask]
|
|
83
|
+
if preds.numel() == 0:
|
|
84
|
+
return 0.0
|
|
85
|
+
return sum(preds[:, d].unique().numel() for d in range(num_digits_total)) / num_digits_total
|