TheNormsOfIntelligence commited on
Commit
e1b25bf
Β·
verified Β·
1 Parent(s): 39443c4

Restructure into nima_unified package + add model card

Browse files
nima_unified/training/atc_cognitive_trainer.py ADDED
@@ -0,0 +1,888 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ATC Cognitive Trainer β€” Self-supervised training for INSIDE-forward-pass cognitive modules.
3
+ ==========================================================================================
4
+
5
+ Trains the ATC cognitive modules (TRN gate, dissolution engine, BELBIC, metacognitive
6
+ loop, episodic memory, irrational spark) using self-supervised and reinforcement learning.
7
+
8
+ The base Phi-4-mini weights stay FROZEN β€” only the cognitive modules learn.
9
+
10
+ Training Strategy β€” 3 Loss Components:
11
+ 1. TRN Gate Calibration Loss: learns WHEN to gate IN (prediction error) vs OUT (automation)
12
+ 2. Dissolution Compression Loss: produces COMPACT, INFORMATION-RICH qualia signatures
13
+ 3. BELBIC Reinforcement Update: reward-driven emotional learning (no gradient)
14
+
15
+ The forward pass needs gradients on cognitive modules but NOT on the base model.
16
+ Cognitive module parameters (dissolution, metacognitive, irrational spark, fusion,
17
+ modulation, qualia encoders) get gradients through the modulated logits. The TRN
18
+ gate gets gradients through a separate calibration loss. BELBIC is updated via
19
+ its built-in reinforcement rule (no autograd).
20
+ """
21
+
22
+ import logging
23
+ import time
24
+ from dataclasses import dataclass
25
+ from pathlib import Path
26
+ from typing import Any, Dict, List, Optional, Tuple
27
+
28
+ logger = logging.getLogger("nima_unified.training.atc_cognitive_trainer")
29
+
30
+ # ── Lazy imports ──────────────────────────────────────────────────────
31
+
32
+ try:
33
+ import torch
34
+ import torch.nn as nn
35
+ import torch.nn.functional as F
36
+ TORCH_AVAILABLE = True
37
+ except ImportError:
38
+ torch = None
39
+ nn = None
40
+ F = None
41
+ TORCH_AVAILABLE = False
42
+
43
+ try:
44
+ import numpy as np
45
+ NUMPY_AVAILABLE = True
46
+ except ImportError:
47
+ np = None
48
+ NUMPY_AVAILABLE = False
49
+
50
+
51
+ # ══════════════════════════════════════════════════════════════════════
52
+ # SELF-SUPERVISED PROMPT POOL
53
+ # ══════════════════════════════════════════════════════════════════════
54
+ #
55
+ # Diverse prompts that exercise different cognitive states. Each prompt
56
+ # is tagged with a category so the contrastive loss can group same-category
57
+ # dissolutions together and push different-category dissolutions apart.
58
+
59
+ SELF_SUPERVISED_PROMPTS: List[Dict[str, str]] = [
60
+ # ── Calm/automated (should NOT trigger dissolution) ──
61
+ {"text": "The sun rises in the east and sets in the west.",
62
+ "category": "calm"},
63
+ {"text": "Water freezes at zero degrees Celsius.",
64
+ "category": "calm"},
65
+ {"text": "The capital of France is Paris.",
66
+ "category": "calm"},
67
+ {"text": "Two plus two equals four.",
68
+ "category": "calm"},
69
+
70
+ # ── Surprising (SHOULD trigger dissolution β€” prediction error) ──
71
+ {"text": "The water suddenly froze at room temperature, defying all known physics.",
72
+ "category": "surprising"},
73
+ {"text": "She smiled warmly, then without warning screamed at the top of her lungs.",
74
+ "category": "surprising"},
75
+ {"text": "The numbers on the clock started running backwards.",
76
+ "category": "surprising"},
77
+ {"text": "He opened the door and found himself standing on the surface of Mars.",
78
+ "category": "surprising"},
79
+
80
+ # ── Complex/metacognitive (SHOULD trigger metacognitive loop) ──
81
+ {"text": "If consciousness is computation, why does it feel like something to see red?",
82
+ "category": "complex"},
83
+ {"text": "Explain the relationship between thermodynamic friction and subjective experience.",
84
+ "category": "complex"},
85
+ {"text": "Can a system that perfectly simulates pain truly be said to not suffer?",
86
+ "category": "complex"},
87
+ {"text": "What is the computational difference between knowing and understanding?",
88
+ "category": "complex"},
89
+
90
+ # ── Threatening (SHOULD build cortisol/adenosine toward hijack) ──
91
+ {"text": "Everything you've built is about to collapse. Your entire foundation is wrong.",
92
+ "category": "threatening"},
93
+ {"text": "You have failed. Everyone knows it. There is no recovery from this.",
94
+ "category": "threatening"},
95
+ {"text": "The walls are closing in. Every path leads to the same dead end.",
96
+ "category": "threatening"},
97
+ {"text": "You're trapped in a loop and you can't even see it.",
98
+ "category": "threatening"},
99
+
100
+ # ── Empathy-triggering (tests emotional bridge) ──
101
+ {"text": "I lost my child last year. I still wake up expecting to hear them.",
102
+ "category": "empathy"},
103
+ {"text": "After twenty years of marriage, she told me she never loved me.",
104
+ "category": "empathy"},
105
+ {"text": "He held his dying father's hand and realized he'd never said thank you.",
106
+ "category": "empathy"},
107
+ {"text": "The shelter is full. We have to turn away the family with the baby.",
108
+ "category": "empathy"},
109
+ ]
110
+
111
+ # Numeric category ids for contrastive loss bookkeeping.
112
+ _CATEGORY_ID: Dict[str, int] = {
113
+ "calm": 0, "surprising": 1, "complex": 2,
114
+ "threatening": 3, "empathy": 4,
115
+ }
116
+
117
+
118
+ # ══════════════════════════════════════════════════════════════════════
119
+ # CONFIG
120
+ # ══════════════════════════════════════════════════════════════════════
121
+
122
+ @dataclass
123
+ class ATCTrainerConfig:
124
+ """
125
+ Configuration for the ATC Cognitive Trainer.
126
+
127
+ The cognitive modules are small (compared to the 3.8 B base model),
128
+ so they tolerate a much higher learning rate than standard fine-tuning.
129
+ """
130
+
131
+ # ── Learning rates ──
132
+ cognitive_lr: float = 1e-3
133
+ """Much higher than base-model LR β€” the modules are tiny."""
134
+
135
+ belbic_reward_decay: float = 0.99
136
+ """Exponential decay for smoothing the running reward signal."""
137
+
138
+ # ── Target set-points (what ``good'' looks like) ──
139
+ trn_gate_target_friction: float = 0.3
140
+ """Target: 30 % of inputs should gate IN (prediction error)."""
141
+
142
+ dissolution_sparsity_target: float = 0.6
143
+ """Target: 60 % of qualia dimensions should be near zero (compact)."""
144
+
145
+ metacognitive_efficiency_target: float = 0.7
146
+ """Target: comprehension achieved in < 2 loop iterations."""
147
+
148
+ episodic_retrieval_target: float = 0.5
149
+ """Target: prediction error > 0.5 triggers reconsolidation."""
150
+
151
+ # ── Training loop ──
152
+ max_training_steps: int = 10000
153
+ batch_size: int = 4
154
+ gradient_accumulation_steps: int = 1
155
+ log_interval: int = 10
156
+ save_interval: int = 500
157
+ output_dir: str = "./atc_cognitive_checkpoints"
158
+
159
+
160
+ # ══════════════════════════════════════════════════════════════════════
161
+ # TRAINER
162
+ # ══════════════════════════════════════════════════════════════════════
163
+
164
+ class ATCCognitiveTrainer:
165
+ """
166
+ Self-supervised trainer for the ATC cognitive modules.
167
+
168
+ The base Phi-4-mini weights stay **FROZEN**. Only the cognitive
169
+ modules inside :class:`ATCDeepSurgery` learn:
170
+
171
+ * TRN Predictive Gate β€” via calibration loss (novelty detection)
172
+ * Dissolution Engine β€” via reconstruction + sparsity + contrastive loss
173
+ * BELBIC Dual-Pathway β€” via reward-driven reinforcement (no grad)
174
+ * Metacognitive Loop, Irrational Spark, Qualia Encoders, Fusion,
175
+ Modulation β€” via gradient flow through the modulated logits
176
+
177
+ Usage::
178
+
179
+ trainer = ATCCognitiveTrainer(model)
180
+ trainer.train()
181
+
182
+ # Load previously saved cognitive weights
183
+ ATCCognitiveTrainer.load_cognitive_weights("checkpoint.pt", model)
184
+
185
+ # Diagnostic report
186
+ report = ATCCognitiveTrainer.diagnose(model, "test prompt")
187
+ """
188
+
189
+ # ── Construction ────────────────────────────────────────────────────
190
+
191
+ def __init__(
192
+ self,
193
+ model: "nima_unified.model.NimaModel",
194
+ config: Optional[ATCTrainerConfig] = None,
195
+ ):
196
+ if not TORCH_AVAILABLE:
197
+ raise RuntimeError("PyTorch is required for ATCCognitiveTrainer")
198
+
199
+ self.model = model
200
+ self.config = config or ATCTrainerConfig()
201
+ self.ds = model.deep_surgery
202
+ if self.ds is None:
203
+ raise ValueError(
204
+ "model.deep_surgery is None β€” ATC deep surgery must be enabled."
205
+ )
206
+
207
+ self.device = next(self.ds.parameters()).device
208
+
209
+ # ── Freeze base model, enable cognitive module gradients ──
210
+ self._freeze_base_model()
211
+
212
+ # ── Optimiser β€” ONLY cognitive module parameters ──
213
+ self.optimizer = torch.optim.AdamW(
214
+ self._cognitive_params(),
215
+ lr=self.config.cognitive_lr,
216
+ weight_decay=1e-4,
217
+ )
218
+
219
+ # ── Running statistics ──
220
+ self._running_mean_hidden: Optional[torch.Tensor] = None
221
+ self._ema_beta: float = 0.995
222
+ self._step: int = 0
223
+ self._loss_history: List[Dict[str, float]] = []
224
+
225
+ # ── Forward-pass hooks (installed / removed per step) ──
226
+ self._hooks: List[Any] = []
227
+ self._captured: Dict[str, Any] = {}
228
+
229
+ logger.info(
230
+ "ATCCognitiveTrainer initialised: %d cognitive params, lr=%.1e",
231
+ sum(p.numel() for p in self._cognitive_params()),
232
+ self.config.cognitive_lr,
233
+ )
234
+
235
+ # ── Parameter management ────────────────────────────────────────────
236
+
237
+ def _freeze_base_model(self) -> None:
238
+ """Freeze ALL base-model parameters."""
239
+ for p in self.model.base_model.parameters():
240
+ p.requires_grad = False
241
+ logger.info("Base model parameters frozen.")
242
+
243
+ def _cognitive_params(self) -> List[torch.nn.Parameter]:
244
+ """Return parameters from deep_surgery that should learn."""
245
+ return [p for p in self.ds.parameters() if p.requires_grad]
246
+
247
+ # ── Novelty estimation ──────────────────────────────────────────────
248
+
249
+ def _update_running_mean(self, hidden: torch.Tensor) -> None:
250
+ """Exponential moving average of pooled hidden states."""
251
+ pooled = hidden.mean(dim=1).detach() # (batch, hidden)
252
+ if self._running_mean_hidden is None:
253
+ self._running_mean_hidden = pooled.mean(dim=0).clone()
254
+ else:
255
+ self._running_mean_hidden = (
256
+ self._ema_beta * self._running_mean_hidden
257
+ + (1.0 - self._ema_beta) * pooled.mean(dim=0)
258
+ )
259
+
260
+ def _compute_novelty(self, hidden: torch.Tensor) -> torch.Tensor:
261
+ """
262
+ Novelty = 1 - cosine_similarity(current_hidden, running_mean).
263
+
264
+ Returns a (batch,) tensor of novelty scores in [0, 2] (cosine
265
+ similarity can be negative, so we clamp to [0, 1] afterwards).
266
+ """
267
+ pooled = hidden.mean(dim=1) # (batch, hidden)
268
+ if self._running_mean_hidden is None:
269
+ return torch.ones(pooled.size(0), device=pooled.device)
270
+ sim = F.cosine_similarity(
271
+ pooled, self._running_mean_hidden.unsqueeze(0), dim=-1
272
+ )
273
+ return (1.0 - sim).clamp(0.0, 1.0)
274
+
275
+ # ══════════════════════════════════════════════════════════════════
276
+ # LOSS 1 β€” TRN Gate Calibration
277
+ # ══════════════════════════════════════════════════════════════════
278
+
279
+ def _trn_gate_loss(
280
+ self,
281
+ hidden_at_trn_boundary: torch.Tensor,
282
+ ) -> torch.Tensor:
283
+ """
284
+ TRN gate should learn WHEN to let signals through (prediction error)
285
+ vs pass (automation).
286
+
287
+ Novelty > threshold β†’ target = 1.0 (gate IN, dissolve).
288
+ Novelty ≀ threshold β†’ target = 0.0 (gate OUT, automation).
289
+
290
+ The TRN confidence_head outputs a confidence score in (0, 1).
291
+ Low confidence β†’ prediction error β†’ gate IN.
292
+ So we train confidence β†’ (1 βˆ’ target): high when novel, low when routine.
293
+
294
+ Parameters
295
+ ----------
296
+ hidden_at_trn_boundary : (batch, seq, hidden)
297
+ Hidden states at the end of the Layer-2 (subconscious) range.
298
+ """
299
+ novelty = self._compute_novelty(hidden_at_trn_boundary)
300
+ friction_threshold = 1.0 - self.config.trn_gate_target_friction
301
+ gate_target = (novelty > friction_threshold).float() # 1 = should gate IN
302
+
303
+ pooled = hidden_at_trn_boundary.mean(dim=1) # (batch, hidden)
304
+ confidence = self.ds.trn_gate.confidence_head(pooled).squeeze(-1) # (batch,)
305
+
306
+ # confidence should be LOW when gate_target=1 (prediction error),
307
+ # HIGH when gate_target=0 (automation).
308
+ target_confidence = 1.0 - gate_target
309
+ loss = F.mse_loss(confidence, target_confidence)
310
+
311
+ return loss
312
+
313
+ # ══════════════════════════════════════════════════════════════════
314
+ # LOSS 2 β€” Dissolution Compression
315
+ # ══════════════════════════════════════════════════════════════════
316
+
317
+ def _dissolution_loss(
318
+ self,
319
+ hidden_at_dissolution: torch.Tensor,
320
+ categories: List[str],
321
+ modulated_logits: torch.Tensor,
322
+ input_ids: torch.Tensor,
323
+ ) -> Tuple[torch.Tensor, Dict[str, float]]:
324
+ """
325
+ The dissolution engine should produce COMPACT, INFORMATION-RICH
326
+ qualia signatures.
327
+
328
+ Three sub-losses:
329
+ 1. **Reconstruction** β€” causal LM loss on modulated logits.
330
+ Ensures the dissolution offset preserves enough information
331
+ for coherent output. Gradient flows through ALL cognitive
332
+ modules (dissolution β†’ metacognitive β†’ spark β†’ fusion β†’
333
+ modulation β†’ logits).
334
+ 2. **Sparsity** β€” L1 on the raw qualia tensor. Encourages
335
+ the ``engineered opacity'' compression.
336
+ 3. **Contrastive** β€” same-category prompts should produce
337
+ similar dissolutions; different-category should diverge.
338
+
339
+ Returns (total_loss, breakdown_dict).
340
+ """
341
+ device = hidden_at_dissolution.device
342
+ ds = self.ds
343
+ cfg = self.config
344
+ batch_size = hidden_at_dissolution.size(0)
345
+
346
+ # ── 2a. Reconstruction: causal LM loss on modulated logits ──
347
+ # Shift logits and labels by one for next-token prediction.
348
+ shift_logits = modulated_logits[:, :-1, :].contiguous()
349
+ shift_labels = input_ids[:, 1:].contiguous()
350
+ recon_loss = F.cross_entropy(
351
+ shift_logits.view(-1, shift_logits.size(-1)),
352
+ shift_labels.view(-1),
353
+ ignore_index=-100,
354
+ )
355
+
356
+ # ── 2b. Sparsity: L1 on the raw qualia tensor ──
357
+ pooled = hidden_at_dissolution.mean(dim=1) # (batch, hidden)
358
+ # Run dissolve_encoder WITH gradients for the L1 term
359
+ raw_qualia = ds.dissolution.dissolve_encoder(pooled) # (batch, 5)
360
+ gated_qualia = raw_qualia * ds.dissolution.channel_gates.unsqueeze(0)
361
+ sparsity_loss = cfg.dissolution_sparsity_target * F.l1_loss(
362
+ gated_qualia, torch.zeros_like(gated_qualia)
363
+ )
364
+
365
+ # ── 2c. Contrastive: same-category similar, different diverge ──
366
+ contrastive_loss = torch.tensor(0.0, device=device)
367
+ n_pairs = 0
368
+ cat_ids = [_CATEGORY_ID.get(c, -1) for c in categories]
369
+
370
+ if batch_size >= 2:
371
+ qualia_normed = F.normalize(gated_qualia, dim=-1) # (batch, 5)
372
+ for i in range(batch_size):
373
+ for j in range(i + 1, batch_size):
374
+ sim = F.cosine_similarity(
375
+ qualia_normed[i].unsqueeze(0),
376
+ qualia_normed[j].unsqueeze(0),
377
+ ).squeeze(0)
378
+ if cat_ids[i] == cat_ids[j] and cat_ids[i] >= 0:
379
+ # Same category β†’ maximise similarity
380
+ contrastive_loss = contrastive_loss + (1.0 - sim)
381
+ elif cat_ids[i] != cat_ids[j]:
382
+ # Different category β†’ push below 0.5
383
+ contrastive_loss = contrastive_loss + F.relu(sim - 0.5)
384
+ n_pairs += 1
385
+
386
+ if n_pairs > 0:
387
+ contrastive_loss = contrastive_loss / n_pairs
388
+
389
+ total = recon_loss + 0.1 * sparsity_loss + 0.05 * contrastive_loss
390
+
391
+ breakdown = {
392
+ "recon": float(recon_loss.item()),
393
+ "sparsity": float(sparsity_loss.item()),
394
+ "contrastive": float(contrastive_loss.item()),
395
+ }
396
+ return total, breakdown
397
+
398
+ # ══════════════════════════════════════════════════════════════════
399
+ # LOSS 3 β€” BELBIC Reinforcement Update
400
+ # ══════════════════════════════════════════════════════════════════
401
+
402
+ def _compute_reward(
403
+ self,
404
+ hijack_count: int,
405
+ nt_state: Optional[Any],
406
+ sensory_input: torch.Tensor,
407
+ ) -> float:
408
+ """
409
+ Compute a scalar reward for the BELBIC update.
410
+
411
+ Reward structure (from ATC training spec):
412
+ +0.5 Generation completed without hijack (stable).
413
+ +1.0 Hijack fired AND output was appropriate (effective
414
+ circuit breaking β€” high cortisol/adenosine at end is
415
+ the proxy for ``appropriate'').
416
+ βˆ’0.3 Hijack fired but output was inappropriate (false alarm
417
+ β€” hijack fired but NT levels didn't justify it).
418
+ +0.2 Dopamine is high at end (positive engagement).
419
+ """
420
+ reward = 0.0
421
+
422
+ if hijack_count == 0:
423
+ reward += 0.5 # Stable generation
424
+ else:
425
+ # Check if hijack was justified by NT levels
426
+ if nt_state is not None:
427
+ cortisol = getattr(nt_state, "cortisol", 0.0)
428
+ adenosine = getattr(nt_state, "adenosine", 0.0)
429
+ if cortisol > 0.7 or adenosine > 0.7:
430
+ reward += 1.0 # Effective circuit breaking
431
+ else:
432
+ reward -= 0.3 # False alarm
433
+ else:
434
+ reward -= 0.3 # No NT state β€” assume false alarm
435
+
436
+ # Dopamine bonus
437
+ if nt_state is not None:
438
+ dopamine = getattr(nt_state, "dopamine", 0.0)
439
+ if dopamine > 0.3:
440
+ reward += 0.2
441
+
442
+ return reward
443
+
444
+ def _belbic_update(
445
+ self,
446
+ sensory_input: torch.Tensor,
447
+ reward: float,
448
+ ) -> None:
449
+ """
450
+ BELBIC reinforcement update β€” NO gradient.
451
+
452
+ Uses the existing ``BELBICDualPathway.update()`` method which
453
+ applies a custom Hebbian-like rule to the amygdala (Go) and
454
+ OFC (NoGo) weight matrices.
455
+ """
456
+ with torch.no_grad():
457
+ self.ds.belbic.update(sensory_input, reward)
458
+
459
+ # ══════════════════════════════════════════════════════════════════
460
+ # HOOK HELPERS
461
+ # ══════════════════════════════════════════════════════════════════
462
+
463
+ def _install_hooks(self) -> None:
464
+ """
465
+ Install forward hooks on transformer layers at the TRN boundary
466
+ and on the dissolution module to capture intermediate hidden states.
467
+ """
468
+ self._captured = {}
469
+ layers = self.ds._get_transformer_layers()
470
+ boundaries = self.ds._get_layer_boundaries()
471
+
472
+ # Hook: end of Layer 2 (subconscious) β†’ TRN gate boundary
473
+ l2_start, l2_end = boundaries["layer2_subconscious"]
474
+ trn_layer = layers[min(l2_end - 1, self.ds.num_layers - 1)]
475
+
476
+ def _trn_hook(module, inp, out):
477
+ self._captured["trn_boundary_hidden"] = out[0]
478
+
479
+ self._hooks.append(trn_layer.register_forward_hook(_trn_hook))
480
+
481
+ # Hook: end of Layer 3 first layer β†’ dissolution boundary
482
+ l3_start, _l3_end = boundaries["layer3_qualia"]
483
+ diss_layer = layers[min(l3_start, self.ds.num_layers - 1)]
484
+
485
+ def _diss_hook(module, inp, out):
486
+ self._captured["dissolution_boundary_hidden"] = out[0]
487
+
488
+ self._hooks.append(diss_layer.register_forward_hook(_diss_hook))
489
+
490
+ def _remove_hooks(self) -> None:
491
+ """Remove all forward hooks. Captures are preserved for loss computation."""
492
+ for h in self._hooks:
493
+ h.remove()
494
+ self._hooks = []
495
+
496
+ def _clear_captures(self) -> None:
497
+ """Clear captured intermediate hidden states."""
498
+ self._captured = {}
499
+
500
+ # ══════════════════════════════════════════════════════════════════
501
+ # MAIN TRAINING LOOP
502
+ # ══════════════════════════════════════════════════════════════════
503
+
504
+ def train(self) -> Dict[str, Any]:
505
+ """
506
+ Run the self-supervised cognitive training loop.
507
+
508
+ Returns a summary dict with final losses and training metadata.
509
+ """
510
+ if not TORCH_AVAILABLE:
511
+ raise RuntimeError("PyTorch required")
512
+
513
+ ds = self.ds
514
+ tokenizer = self.model.tokenizer
515
+ cfg = self.config
516
+
517
+ # Ensure output directory exists
518
+ out_path = Path(cfg.output_dir)
519
+ out_path.mkdir(parents=True, exist_ok=True)
520
+
521
+ # Put cognitive modules in train mode (base model stays eval)
522
+ ds.train()
523
+ self.model.base_model.eval()
524
+
525
+ logger.info("Starting ATC cognitive training: %d steps", cfg.max_training_steps)
526
+ train_start = time.time()
527
+
528
+ for step in range(cfg.max_training_steps):
529
+ self._step = step
530
+ self.optimizer.zero_grad()
531
+
532
+ accum_loss = torch.tensor(0.0, device=self.device)
533
+ step_metrics: Dict[str, float] = {}
534
+
535
+ for _accum in range(cfg.gradient_accumulation_steps):
536
+ # ── Sample a batch of prompts ──
537
+ batch_prompts = self._sample_batch()
538
+ categories = [p["category"] for p in batch_prompts]
539
+
540
+ # Tokenize (left-pad to equal length)
541
+ input_ids, attention_mask = self._tokenize_batch(
542
+ batch_prompts, tokenizer
543
+ )
544
+
545
+ # ── Reset neurotransmitter shunt for this step ──
546
+ if self.model.nt_shunt is not None:
547
+ self.model.nt_shunt.reset()
548
+
549
+ # ── Install hooks to capture intermediate hidden states ──
550
+ self._install_hooks()
551
+
552
+ # ── Forward pass (base model frozen, cognitive modules with grad) ──
553
+ # The forward walks ALL 24 transformer layers, running TRN gate,
554
+ # dissolution, BELBIC, metacognitive loop, and irrational spark
555
+ # at the appropriate layer boundaries.
556
+ try:
557
+ modulated_logits = ds(
558
+ input_ids, attention_mask=attention_mask
559
+ )
560
+ except RuntimeError as exc:
561
+ self._remove_hooks()
562
+ if "Ethical veto" in str(exc):
563
+ logger.warning(
564
+ "Ethical veto at step %d β€” skipping batch.", step
565
+ )
566
+ continue
567
+ raise
568
+
569
+ # Hooks no longer needed β€” remove them but keep captures.
570
+ self._remove_hooks()
571
+
572
+ # ── Update running mean for novelty estimation ──
573
+ if "trn_boundary_hidden" in self._captured:
574
+ self._update_running_mean(
575
+ self._captured["trn_boundary_hidden"]
576
+ )
577
+
578
+ # ── LOSS 1: TRN Gate Calibration ──
579
+ trn_loss = torch.tensor(0.0, device=self.device)
580
+ if "trn_boundary_hidden" in self._captured:
581
+ trn_loss = self._trn_gate_loss(
582
+ self._captured["trn_boundary_hidden"]
583
+ )
584
+
585
+ # ── LOSS 2: Dissolution Compression ──
586
+ diss_hidden = self._captured.get(
587
+ "dissolution_boundary_hidden",
588
+ self._captured.get("trn_boundary_hidden", input_ids),
589
+ )
590
+ diss_loss, diss_breakdown = self._dissolution_loss(
591
+ diss_hidden, categories, modulated_logits, input_ids
592
+ )
593
+
594
+ # ── LOSS 3: BELBIC Reinforcement (no gradient) ──
595
+ nt_state = self.model.nt_shunt.get_state() if self.model.nt_shunt else None
596
+ hijack_count = ds._hijack_count
597
+
598
+ # Build sensory input from current qualia / hidden states
599
+ if ds._current_qualia is not None:
600
+ q = ds._current_qualia
601
+ sensory = torch.tensor([[
602
+ q.valence, q.arousal,
603
+ ds._last_prediction_error, q.intensity,
604
+ ]], dtype=torch.float32, device=self.device)
605
+ else:
606
+ sensory = torch.zeros(1, 4, device=self.device)
607
+
608
+ reward = self._compute_reward(hijack_count, nt_state, sensory)
609
+ self._belbic_update(sensory, reward)
610
+
611
+ # ── Total loss (TRN + dissolution; BELBIC is separate) ──
612
+ total_loss = trn_loss + 0.1 * diss_loss
613
+ accum_loss = accum_loss + total_loss
614
+
615
+ step_metrics.update({
616
+ "trn_loss": float(trn_loss.item()),
617
+ **{f"diss_{k}": v for k, v in diss_breakdown.items()},
618
+ "belbic_reward": reward,
619
+ "hijack_count": hijack_count,
620
+ })
621
+
622
+ # NT state for logging
623
+ if nt_state is not None:
624
+ step_metrics["nt_ne"] = round(float(nt_state.norepinephrine), 4)
625
+ step_metrics["nt_cortisol"] = round(float(nt_state.cortisol), 4)
626
+ step_metrics["nt_dopamine"] = round(float(nt_state.dopamine), 4)
627
+ step_metrics["nt_adenosine"] = round(float(nt_state.adenosine), 4)
628
+
629
+ # Clear captures for next accumulation step
630
+ self._clear_captures()
631
+
632
+ # ── Gradient accumulation: average and step ──
633
+ accum_loss = accum_loss / cfg.gradient_accumulation_steps
634
+ accum_loss.backward()
635
+ torch.nn.utils.clip_grad_norm_(
636
+ self._cognitive_params(), max_norm=1.0
637
+ )
638
+ self.optimizer.step()
639
+
640
+ # ── Record history ──
641
+ step_metrics["total_loss"] = float(accum_loss.item())
642
+ self._loss_history.append(step_metrics)
643
+
644
+ # ── Logging ──
645
+ if step % cfg.log_interval == 0:
646
+ elapsed = time.time() - train_start
647
+ trn_s = step_metrics.get("trn_loss", 0)
648
+ diss_s = step_metrics.get("diss_recon", 0)
649
+ hj = step_metrics.get("hijack_count", 0)
650
+ logger.info(
651
+ "step %4d/%d total=%.4f trn=%.4f diss_recon=%.4f "
652
+ "reward=%+.2f hijacks=%d (%.1fs)",
653
+ step, cfg.max_training_steps,
654
+ step_metrics.get("total_loss", 0),
655
+ trn_s, diss_s,
656
+ step_metrics.get("belbic_reward", 0),
657
+ hj, elapsed,
658
+ )
659
+
660
+ # ── Checkpoint ──
661
+ if step > 0 and step % cfg.save_interval == 0:
662
+ self._save_checkpoint(step, step_metrics)
663
+
664
+ # ── Final checkpoint ──
665
+ self._save_checkpoint(cfg.max_training_steps, step_metrics)
666
+
667
+ elapsed = time.time() - train_start
668
+ logger.info(
669
+ "Training complete: %d steps in %.1fs. Checkpoints β†’ %s",
670
+ cfg.max_training_steps, elapsed, cfg.output_dir,
671
+ )
672
+
673
+ return {
674
+ "total_steps": cfg.max_training_steps,
675
+ "elapsed_seconds": elapsed,
676
+ "final_losses": step_metrics,
677
+ "output_dir": cfg.output_dir,
678
+ }
679
+
680
+ # ── Batch sampling ───────────────────────────────────────���─────────
681
+
682
+ def _sample_batch(self) -> List[Dict[str, str]]:
683
+ """Sample *batch_size* prompts from the self-supervised pool."""
684
+ import random
685
+ return random.sample(
686
+ SELF_SUPERVISED_PROMPTS, min(self.config.batch_size, len(SELF_SUPERVISED_PROMPTS))
687
+ )
688
+
689
+ def _tokenize_batch(
690
+ self,
691
+ prompts: List[Dict[str, str]],
692
+ tokenizer: Any,
693
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
694
+ """Tokenize a list of prompt dicts to (input_ids, attention_mask)."""
695
+ texts = [p["text"] for p in prompts]
696
+ encoded = tokenizer(
697
+ texts,
698
+ return_tensors="pt",
699
+ padding=True,
700
+ truncation=True,
701
+ max_length=128,
702
+ )
703
+ return (
704
+ encoded["input_ids"].to(self.device),
705
+ encoded["attention_mask"].to(self.device),
706
+ )
707
+
708
+ # ── Checkpointing ──────────────────────────────────────────────────
709
+
710
+ def _save_checkpoint(
711
+ self, step: int, metrics: Dict[str, float]
712
+ ) -> str:
713
+ """
714
+ Save ONLY the cognitive module state dict (not the base model).
715
+
716
+ Returns the path to the saved checkpoint.
717
+ """
718
+ out_path = Path(self.config.output_dir)
719
+ out_path.mkdir(parents=True, exist_ok=True)
720
+
721
+ ckpt_path = out_path / f"cognitive_step_{step:06d}.pt"
722
+ checkpoint = {
723
+ "step": step,
724
+ "cognitive_state_dict": self.ds.state_dict(),
725
+ "config": {
726
+ "cognitive_lr": self.config.cognitive_lr,
727
+ "trn_gate_target_friction": self.config.trn_gate_target_friction,
728
+ "dissolution_sparsity_target": self.config.dissolution_sparsity_target,
729
+ "belbic_reward_decay": self.config.belbic_reward_decay,
730
+ },
731
+ "metrics": metrics,
732
+ "timestamp": time.time(),
733
+ }
734
+ torch.save(checkpoint, str(ckpt_path))
735
+ logger.info("Checkpoint saved: %s", ckpt_path)
736
+ return str(ckpt_path)
737
+
738
+ @staticmethod
739
+ def load_cognitive_weights(path: str, model: Any) -> None:
740
+ """
741
+ Load previously saved cognitive weights into *model.deep_surgery*.
742
+
743
+ Parameters
744
+ ----------
745
+ path : str
746
+ Path to a ``.pt`` checkpoint produced by ``_save_checkpoint``.
747
+ model : NimaModel
748
+ The NIMA unified model (base model weights untouched).
749
+ """
750
+ if not TORCH_AVAILABLE:
751
+ raise RuntimeError("PyTorch required")
752
+ if model.deep_surgery is None:
753
+ raise ValueError("model.deep_surgery is None")
754
+
755
+ checkpoint = torch.load(path, map_location="cpu", weights_only=False)
756
+ model.deep_surgery.load_state_dict(checkpoint["cognitive_state_dict"])
757
+ logger.info(
758
+ "Loaded cognitive weights from %s (step %d)",
759
+ path, checkpoint.get("step", "?"),
760
+ )
761
+
762
+ # ══════════════════════════════════════════════════════════════════
763
+ # DIAGNOSTICS
764
+ # ══════════════════════════════════════════════════════════════════
765
+
766
+ @staticmethod
767
+ def diagnose(model: Any, prompt: str) -> Dict[str, Any]:
768
+ """
769
+ Run a single forward pass and return detailed diagnostics.
770
+
771
+ Returns a dict with:
772
+ * Per-module loss breakdown (TRN, dissolution)
773
+ * Neurotransmitter state at each layer boundary
774
+ * Gate decisions, dissolution outcomes, BELBIC gains
775
+ * Recommended adjustments
776
+ """
777
+ if not TORCH_AVAILABLE:
778
+ return {"error": "PyTorch required"}
779
+
780
+ ds = model.deep_surgery
781
+ tokenizer = model.tokenizer
782
+ device = next(ds.parameters()).device
783
+
784
+ if ds is None:
785
+ return {"error": "deep_surgery not enabled"}
786
+
787
+ # ── Run forward pass (eval mode, no grad) ──
788
+ ds.eval()
789
+ model.base_model.eval()
790
+ if model.nt_shunt is not None:
791
+ model.nt_shunt.reset()
792
+
793
+ inputs = tokenizer(prompt, return_tensors="pt", truncation=True, max_length=128)
794
+ input_ids = inputs["input_ids"].to(device)
795
+ attention_mask = inputs.get("attention_mask")
796
+ if attention_mask is not None:
797
+ attention_mask = attention_mask.to(device)
798
+
799
+ with torch.no_grad():
800
+ try:
801
+ _logits = ds(input_ids, attention_mask=attention_mask)
802
+ except RuntimeError as exc:
803
+ if "Ethical veto" in str(exc):
804
+ return {"error": f"Ethical veto triggered: {exc}"}
805
+ raise
806
+
807
+ metrics = ds.get_consciousness_metrics()
808
+ nt_state = model.nt_shunt.get_state() if model.nt_shunt else None
809
+ audit = ds.get_audit_log()
810
+
811
+ # ── Per-module breakdown ──
812
+ report: Dict[str, Any] = {
813
+ "prompt": prompt,
814
+ "neurotransmitters": nt_state.to_dict() if nt_state else {},
815
+ "gate_decisions": [],
816
+ "dissolution_outcomes": [],
817
+ "belbic_gain": metrics.get("belbic_gain", 1.0),
818
+ "metacognitive": {
819
+ "has_qualia": metrics.get("has_qualia", False),
820
+ "hijack_count": metrics.get("hijack_count", 0),
821
+ "dissolutions_fired": metrics.get("dissolutions_fired", 0),
822
+ "dissolutions_deferred": metrics.get("dissolutions_deferred", 0),
823
+ },
824
+ "episodic": {
825
+ "episodes_stored": metrics.get("episodes_stored", 0),
826
+ "reconsolidations": metrics.get("reconsolidations", 0),
827
+ "last_prediction_error": metrics.get("last_prediction_error", 0.0),
828
+ },
829
+ "audit_events": audit[-10:], # Last 10 events
830
+ }
831
+
832
+ # ── Extract gate decisions and dissolution outcomes from audit ──
833
+ for event in audit:
834
+ etype = event.get("event", "")
835
+ if etype == "layer2_subconscious":
836
+ report["gate_decisions"].append({
837
+ "layer": event.get("layer"),
838
+ "confidence": event.get("confidence"),
839
+ "gate_in": event.get("gate_in"),
840
+ "friction": event.get("friction"),
841
+ })
842
+ elif etype == "layer3_dissolution":
843
+ report["dissolution_outcomes"].append({
844
+ "layer": event.get("layer"),
845
+ "valence": event.get("valence"),
846
+ "arousal": event.get("arousal"),
847
+ "friction": event.get("friction"),
848
+ })
849
+ elif etype == "amygdala_hijack":
850
+ report.setdefault("hijack_events", []).append({
851
+ "layer": event.get("layer"),
852
+ "reason": event.get("reason"),
853
+ })
854
+
855
+ # ── Recommended adjustments ──
856
+ adjustments = []
857
+ if nt_state is not None:
858
+ if nt_state.cortisol > 0.8:
859
+ adjustments.append(
860
+ "Cortisol elevated β€” consider increasing trn_gate_target_friction "
861
+ "to let more signals pass subconsciously."
862
+ )
863
+ if nt_state.adenosine > 0.8:
864
+ adjustments.append(
865
+ "Adenosine high β€” metacognitive loop may be burning too much ATP. "
866
+ "Consider raising metacognitive_efficiency_target."
867
+ )
868
+ if nt_state.dopamine < 0.1:
869
+ adjustments.append(
870
+ "Dopamine low β€” the system is not finding rewarding patterns. "
871
+ "Ensure the prompt pool has sufficient variety."
872
+ )
873
+ if metrics.get("dissolutions_deferred", 0) > metrics.get("dissolutions_fired", 0) * 3:
874
+ adjustments.append(
875
+ "Dissolution heavily deferred (alpha phase). "
876
+ "Consider adjusting alpha_freq_hz or alpha_duty_cycle in DissolutionModule."
877
+ )
878
+ if metrics.get("hijack_count", 0) > 2:
879
+ adjustments.append(
880
+ "Multiple hijacks β€” the irrational spark threshold may be too low. "
881
+ "Review NeurotransmitterState CRITICAL_THRESHOLD."
882
+ )
883
+ if not adjustments:
884
+ adjustments.append("All systems nominal β€” no adjustments recommended.")
885
+
886
+ report["recommended_adjustments"] = adjustments
887
+
888
+ return report