PowerMachine commited on
Commit
f4432a5
·
verified ·
1 Parent(s): 459b339

Delete src/bigru_t/model/kohonen_learning_system.py.bak_v65fix

Browse files
src/bigru_t/model/kohonen_learning_system.py.bak_v65fix DELETED
@@ -1,1370 +0,0 @@
1
- """kohonen_learning_system.py — V6.5 (VQ-VAE-2 + reasoning_engine integrados)
2
-
3
- V6.5 — ATIVAR VQ-VAE-2 NO PIPELINE DE COMPRESSÃO + INTEGRAR REASONING_ENGINE
4
-
5
- Implementação CANÔNICA do Sistema de Aprendizado Kohonen 4D fornecida pelo
6
- usuário, com análise matemática formal e 2 correções de bugs críticos.
7
-
8
- ============================================================================
9
- ANÁLISE MATEMÁTICA FORMAL
10
- ============================================================================
11
-
12
- 1) text_to_4d_vector(text, ..., time_step, T_max):
13
- -----------------------------------------------
14
- Tokenização BBPE: ids = BBPE_encode(text, max_len=seq_len) → [seq_len]
15
- Embedding: E = Embedding(ids) → [seq_len, D]
16
- PE sinusoidal: PE = sin/cos(pos · exp(-2k·log(10000)/D))
17
- Fusão: F = E + PE → [seq_len, D]
18
- Centralização: F_c = F - mean(F, dim=0) → [seq_len, D]
19
- SVD: F_c = U · S · V^T (V: [D, D] ortogonal)
20
- Projeção 3D: coords_3d = F_c · V[:3,:]^T → [seq_len, 3]
21
- Centróide: xyz = mean(coords_3d, dim=0) → [3]
22
- Coordenada temporal: w = time_step / T_max ∈ [0, 1] (LINEAR)
23
- Saída: vec_4d = [x, y, z, w] → [4]
24
-
25
- Nota (V6.4): w é LINEAR no tempo (diferente de sigmoid(||xyz||)).
26
- Isto permite que o SOM organize neurônios ao longo do eixo temporal,
27
- capturando a sequência absoluta de amostras — útil para deteção
28
- de drift e consolidação incremental (EWC).
29
-
30
- 2) KohonenSOM4D:
31
- --------------
32
- Grid: W ∈ ℝ^(I×J×K×L×4), default (6,6,6,4) = 864 neurônios
33
- BMU: bmu = argmin_{i,j,k,l} ||W[i,j,k,l] - x||² (Euclidiana ℝ⁴)
34
- Vizinhança (Gaussiana 4D):
35
- Λ(d, σ) = exp(-d² / (2σ²))
36
- d² = Δi² + Δj² + Δk² + Δl² (distância quadrada no grid 4D)
37
- Update (Kohonen):
38
- ΔW = α · Λ · (x - W) (competitivo + cooperativo)
39
- Decaimento:
40
- σ_t = σ₀ · exp(-t/1000) (vizinhança encolhe rápido)
41
- α_t = α₀ · exp(-t/2000) (LR decai mais lento)
42
- EWC (apenas na 4ª dimensão w):
43
- L_ewc = (λ/2) · Σ F_i · (W_w,i - W*_w,i)²
44
- ∂L_ewc/∂W_w = λ · F · (W_w - W*_w)
45
- Aplicado como: update[..., 3] -= λ · F · (W_w - W*_w)
46
- Fisher (aproximação como erro quadrado):
47
- F_i = mean((x_w - W_w,i)²) sobre samples pré-punição
48
- Acumulado apenas quando:
49
- (a) punishment_count == 0
50
- (b) old_weights_w is None (ainda não consolidado)
51
- (c) Λ > 0.1 (neurônios próximos ao BMU)
52
-
53
- 3) HypothesisClassifier:
54
- ----------------------
55
- 8 camadas FC: [in → 512 → 256 → 128 → 64 → 32 → 16 → 8] + ReLU
56
- Output: 8 → 1 (logit)
57
- Loss: BCEWithLogitsLoss
58
- Optimizer: Adam, lr=0.001
59
- Epochs: 50 (sobre o buffer atual)
60
- Input: vetor de ativação SOM = distâncias ao grid flatten
61
- (864-dim para grid (6,6,6,4))
62
-
63
- 4) Punishment Protocol:
64
- ---------------------
65
- Histograma: bucketiza vec_4d[dim_choice] (default 'y', idx=1)
66
- check_training_start:
67
- max(histogram.values()) >= N_start → ready
68
- Avaliação: acc = correct / len(buffer) (correct = pred==label)
69
- Se acc < 1.0:
70
- punishment_count += 1
71
- success_count = 0
72
- Se punishment_count == 1: activate_hypothesis() (treina classifier)
73
- Se punishment_count == 2:
74
- set_ewc_reference() (consolida w via Fisher)
75
- required_new_samples = success_count * N (ou N se success==0)
76
- reset: training_ready=False, punishment=0, success=0,
77
- histogram cleared, buffers cleared
78
- Se acc == 1.0:
79
- punishment_count = 0
80
- success_count += 1
81
-
82
- 5) PGVector NÃO É MAIS NECESSÁRIO (V6.4):
83
- ---------------------------------------
84
- O método find_bmu realiza a busca nearest-neighbor sobre o grid 4D,
85
- substituindo qualquer lookup pgvector externo. O SOM interno já
86
- armazena todo o conhecimento como pesos 4D, e find_bmu retorna o
87
- neurônio mais próximo em O(I·J·K·L) — equivalente a uma busca
88
- pgvector com indexação flat.
89
- Consequentemente, hyp_t.py NÃO consulta mais pgvector — a decisão
90
- de aplicar punição é delegada ao KohonenLearningSystem.
91
-
92
- ============================================================================
93
- BUGS CORRIGIDOS (V6.3 → mantidos em V6.4)
94
- ============================================================================
95
-
96
- BUG 1 (find_bmu): V6.3 corrigiu return prematuro com Ellipsis.
97
- V6.4: código do usuário já está limpo (unravel manual sem return
98
- prematuro). Mantido as-is.
99
-
100
- BUG 2 (activate_hypothesis): backward tentava retropropagar através
101
- do embedding (via buffer_4d). Causava RuntimeError "Trying to
102
- backward through the graph a second time".
103
- FIX V6.4: detach+clone nos tensores de entrada do classifier,
104
- e cálculo do vetor de ativação SOM dentro de torch.no_grad().
105
- O classifier treina apenas sobre seus próprios pesos (8 FC layers).
106
-
107
- ============================================================================
108
- V6.5 — VQ-VAE-2 NO PIPELINE DE COMPRESSÃO
109
- ============================================================================
110
- User requirement: "ativar efetivamente o VQ-VAE-2 no pipeline de compressão"
111
-
112
- Integração: o KohonenLearningSystem agora possui um `vqvae2_compressor`
113
- opcional (HierarchicalVQVAE2 do módulo vqvae2_hierarchical_flexnet.py).
114
- Quando ativado:
115
-
116
- 1. Após `train_som_on_buffer()`, o buffer_4d (B, 4) é passado ao VQ-VAE-2
117
- como entrada. O encoder mapeia (B, 4) -> z_e (B, code_dim).
118
- 2. O VQ hierárquico produz:
119
- - z_q_top: estrutura global do batch (codebook K_top)
120
- - z_q_bot: detalhes residuais (codebook K_bot)
121
- 3. O decoder reconstrói z_recon (B, 4) a partir de z_q_combined.
122
- 4. A loss do VQ-VAE-2 (commitment + recon) é computada e retornada
123
- para monitoramento (não adicionada à loss do SOM — são objetivos
124
- ortogonais: SOM aprende topologia, VQ-VAE-2 aprende compressão).
125
- 5. Códigos top/bottom podem ser usados como representação compacta
126
- do estado do SOM para armazenamento/transferência.
127
-
128
- Benefícios:
129
- - Compressão neural do espaço 4D do SOM (4 -> code_dim -> 2 códigos)
130
- - Codebook compartilhado entre batches (aprendizado incremental)
131
- - Dead code restart evita colapso do codebook
132
- - Goose VQ (Gumbel-softmax) força uso uniforme do codebook
133
-
134
- ============================================================================
135
- V6.5 — REASONING_ENGINE INTEGRADO
136
- ============================================================================
137
- User requirement: "integrar reasoning_engine ao KohonenLearningSystem"
138
-
139
- Integração: o KohonenLearningSystem agora possui um `reasoning_engine`
140
- opcional (ReasoningEngine do módulo reasoning_engine.py). Quando ativado:
141
-
142
- 1. Após `predict()`, o resultado da predição é passado ao reasoning_engine
143
- que gera uma sequência de tags <think>, <plan>, <decompose>,
144
- <execute>, <monitor>, <predict>, <adjust>, <answer>.
145
- 2. O reasoning_engine pode usar ferramentas registradas (tool_agent)
146
- para consultas externas (ex: calculator, knowledge_base).
147
- 3. O streaming de raciocínio é compatível com Ollama/LangChain/vLLM
148
- via tags padrão.
149
- 4. Para predições do SOM, o reasoning_engine pode explicar o porquê
150
- do BMU ter sido escolhido (análise de distâncias).
151
-
152
- Métodos adicionados:
153
- - reason_about(query): retorna generator com streaming de raciocínio
154
- - reason_sync(query): retorna string completa com todas as tags
155
- - get_reasoning_stats(): retorna estatísticas do reasoning_engine
156
-
157
- ============================================================================
158
- """
159
- from __future__ import annotations
160
-
161
- import os
162
- import math
163
- import torch
164
- import torch.nn as nn
165
- import torch.nn.functional as F
166
- from collections import defaultdict, Counter
167
- from typing import List, Tuple, Optional, Dict, Any, Iterator
168
-
169
-
170
- # ============================================================================
171
- # Configuração da CPU – núcleos físicos
172
- # ============================================================================
173
- try:
174
- import psutil
175
- N_CORES = psutil.cpu_count(logical=False)
176
- except ImportError:
177
- N_CORES = os.cpu_count() // 2 if os.cpu_count() else 4
178
- if N_CORES:
179
- try:
180
- torch.set_num_threads(N_CORES)
181
- torch.set_num_interop_threads(N_CORES)
182
- except RuntimeError:
183
- pass # already initialized
184
-
185
-
186
- # ============================================================================
187
- # Tokenizador BBPE simplificado
188
- # ============================================================================
189
- class SimpleBBPETokenizer:
190
- """Tokenizador BPE simplificado (nível de palavra).
191
-
192
- Vocabulário especial: pad=0, eos=1, unk=2.
193
- Tokens comuns mapeados a partir do índice 3.
194
- """
195
-
196
- def __init__(self, vocab_size: int = 16384):
197
- self.vocab_size = vocab_size
198
- self.token_to_id = {}
199
- self.id_to_token = {}
200
- self.pad_token = "<pad>"
201
- self.eos_token = "<eos>"
202
- self.unk_token = "<unk>"
203
- self.pad_id = 0
204
- self.eos_id = 1
205
- self.unk_id = 2
206
- self._init_special_tokens()
207
-
208
- def _init_special_tokens(self):
209
- self.token_to_id[self.pad_token] = self.pad_id
210
- self.token_to_id[self.eos_token] = self.eos_id
211
- self.token_to_id[self.unk_token] = self.unk_id
212
- self.id_to_token[self.pad_id] = self.pad_token
213
- self.id_to_token[self.eos_id] = self.eos_token
214
- self.id_to_token[self.unk_id] = self.unk_token
215
-
216
- def fit(self, texts: List[str]):
217
- """Constrói vocabulário por frequência (top vocab_size-3 palavras)."""
218
- word_counts = Counter()
219
- for text in texts:
220
- words = text.split()
221
- word_counts.update(words)
222
- sorted_words = [w for w, _ in word_counts.most_common(self.vocab_size - 3)]
223
- for idx, word in enumerate(sorted_words, start=3):
224
- self.token_to_id[word] = idx
225
- self.id_to_token[idx] = word
226
-
227
- def encode(self, text: str, max_length: int = 8) -> List[int]:
228
- """Encoda + append EOS + pad/trunca para max_length."""
229
- words = text.split()
230
- ids = [self.token_to_id.get(w, self.unk_id) for w in words]
231
- ids.append(self.eos_id)
232
- if len(ids) > max_length:
233
- ids = ids[:max_length]
234
- else:
235
- ids += [self.pad_id] * (max_length - len(ids))
236
- return ids
237
-
238
-
239
- # ============================================================================
240
- # Codificação posicional e conversão texto → vetor 4D (com w temporal)
241
- # ============================================================================
242
- def positional_encoding(seq_len: int, hidden_dim: int) -> torch.Tensor:
243
- """PE sinusoidal clássico: PE[pos, 2k] = sin(pos·exp(-2k·log(10000)/D)),
244
- PE[pos, 2k+1] = cos(pos·exp(-2k·log(10000)/D)).
245
- """
246
- pe = torch.zeros(seq_len, hidden_dim)
247
- position = torch.arange(0, seq_len, dtype=torch.float).unsqueeze(1)
248
- div_term = torch.exp(
249
- torch.arange(0, hidden_dim, 2).float() * (-math.log(10000.0) / hidden_dim)
250
- )
251
- pe[:, 0::2] = torch.sin(position * div_term)
252
- pe[:, 1::2] = torch.cos(position * div_term)
253
- return pe
254
-
255
-
256
- def text_to_4d_vector(
257
- text: str,
258
- tokenizer: SimpleBBPETokenizer,
259
- embedding: nn.Embedding,
260
- hidden_dim: int,
261
- seq_len: int,
262
- time_step: int,
263
- T_max: int = 10000,
264
- attention: Optional[nn.Module] = None,
265
- attention_metrics: Optional[Dict[str, Any]] = None,
266
- ) -> torch.Tensor:
267
- """Converte sentença em vetor 4D (x, y, z, w), onde w = time_step / T_max.
268
-
269
- Pipeline matemático (V6.5-attn: + MultiHeadAttention entre +PE e SVD):
270
- ids → Embedding → +PE → [MultiHeadAttention] → SVD(proj top-3) → centróide xyz
271
- w = time_step / T_max (LINEAR no tempo)
272
- vec_4d = concat(xyz, w) → [4]
273
-
274
- V6.5-attn (user requirement: "verificar se o mecanismo de atenção está ativo
275
- e acessado logicamente funcional"):
276
- Quando `attention` é fornecido (nn.Module), o tensor `fused (L, D)` é
277
- passado pelo attention (self-attention com máscara causal) antes do SVD.
278
- O attention produz um tensor residual (fused + attn_out) preservando a
279
- dimensão. Métricas são coletadas em `attention_metrics` para verificação.
280
-
281
- Args:
282
- attention: Optional[nn.Module] — MultiHeadAttention do attention_multimodal.
283
- attention_metrics: Optional[Dict] — acumula estatísticas do attention.
284
- Se fornecido, é atualizado in-place com: n_calls, last_norm_in,
285
- last_norm_out, last_attn_activated (True se output != input).
286
- """
287
- ids = tokenizer.encode(text, max_length=seq_len)
288
- input_ids = torch.tensor(ids).unsqueeze(0)
289
- word_emb = embedding(input_ids).squeeze(0) # (L, D)
290
- pe = positional_encoding(seq_len, hidden_dim)
291
- fused = word_emb + pe # (L, D)
292
-
293
- # V6.5-attn — Aplica MultiHeadAttention (self-attention) sobre o fused
294
- # User requirement: "verificar se o mecanismo de atenção está ativo e
295
- # acessado logicamente funcional"
296
- if attention is not None:
297
- try:
298
- # Prepare input: (1, L, D) — batch dim necessária para MHA
299
- fused_in = fused.unsqueeze(0) # (1, L, D)
300
- norm_in = float(fused_in.norm().item())
301
- # Forward — passa mask=None (sem causal mask; seq_len curto)
302
- attn_out = attention(fused_in, mask=None) # (1, L, D)
303
- # Residual connection (preserva dimensão)
304
- fused_attn = fused_in + attn_out # (1, L, D)
305
- fused = fused_attn.squeeze(0) # (L, D)
306
- norm_out = float(fused.norm().item())
307
- # Métricas de verificação (in-place se dict fornecido)
308
- if attention_metrics is not None:
309
- attention_metrics["n_calls"] = attention_metrics.get("n_calls", 0) + 1
310
- attention_metrics["last_norm_in"] = norm_in
311
- attention_metrics["last_norm_out"] = norm_out
312
- # "logicamente funcional" = output é finito e diferente do input
313
- attention_metrics["last_attn_activated"] = bool(
314
- torch.isfinite(attn_out).all().item()
315
- and (attn_out.abs().sum().item() > 1e-8)
316
- )
317
- attention_metrics["last_attn_diff_norm"] = float(
318
- (attn_out.squeeze(0) - fused_in.squeeze(0)).norm().item()
319
- )
320
- except Exception as _e_attn:
321
- # Em caso de erro no attention, segue sem attention mas registra
322
- if attention_metrics is not None:
323
- attention_metrics["n_errors"] = attention_metrics.get("n_errors", 0) + 1
324
- attention_metrics["last_error"] = str(_e_attn)[:200]
325
-
326
- # SVD para 3D
327
- mean_centered = fused - fused.mean(dim=0, keepdim=True)
328
- U, S, V = torch.linalg.svd(mean_centered, full_matrices=False)
329
- coords_3d = torch.mm(mean_centered, V[:3, :].t()) # (L, 3)
330
-
331
- xyz_mean = coords_3d.mean(dim=0) # (3,)
332
- w = torch.tensor(time_step / T_max, dtype=torch.float) # valor temporal
333
- vec_4d = torch.cat([xyz_mean, w.unsqueeze(0)]).contiguous()
334
- return vec_4d
335
-
336
-
337
- # ============================================================================
338
- # Mapa de Kohonen 4D com EWC (Fisher para w não-nulo)
339
- # ============================================================================
340
- class KohonenSOM4D:
341
- """Mapa Auto-Organizável 4D com EWC apenas na 4ª dimensão (w temporal).
342
-
343
- Args:
344
- grid_shape: (I, J, K, L) — dimensões do grid 4D.
345
- alpha0: taxa de aprendizado inicial (α₀).
346
- sigma0: largura inicial da vizinhança (σ₀).
347
- lambda_ewc: peso da penalidade EWC (λ).
348
-
349
- Atributos:
350
- weights: W ∈ ℝ^(I×J×K×L×4) — pesos dos neurônios.
351
- old_weights_w: W*_w ∈ ℝ^(I×J×K×L) — referência EWC (apenas w).
352
- fisher_w: F ∈ ℝ^(I×J×K×L) — informação de Fisher por neurônio (w).
353
- fisher_accum / fisher_count: acumuladores para cálculo de F.
354
- """
355
-
356
- def __init__(
357
- self,
358
- grid_shape: Tuple[int, int, int, int],
359
- alpha0: float = 0.1,
360
- sigma0: float = 1.0,
361
- lambda_ewc: float = 0.01,
362
- ):
363
- self.I, self.J, self.K, self.L = grid_shape
364
- self.alpha0 = alpha0
365
- self.sigma0 = sigma0
366
- self.lambda_ewc = lambda_ewc
367
- self.t = 0
368
-
369
- self.weights = torch.randn(self.I, self.J, self.K, self.L, 4)
370
- self.old_weights_w = None
371
- self.fisher_w = None
372
- self.fisher_accum = torch.zeros(self.I, self.J, self.K, self.L)
373
- self.fisher_count = torch.zeros(self.I, self.J, self.K, self.L)
374
-
375
- def _neighborhood(self, bmu_idx):
376
- """Vizinhança Gaussiana 4D: d² = Δi² + Δj² + Δk² + Δl²."""
377
- i, j, k, l = bmu_idx
378
- II, JJ, KK, LL = torch.meshgrid(
379
- torch.arange(self.I).float(),
380
- torch.arange(self.J).float(),
381
- torch.arange(self.K).float(),
382
- torch.arange(self.L).float(),
383
- indexing="ij",
384
- )
385
- dist_sq = (II - i) ** 2 + (JJ - j) ** 2 + (KK - k) ** 2 + (LL - l) ** 2
386
- return dist_sq
387
-
388
- def find_bmu(self, x: torch.Tensor) -> Tuple[int, int, int, int]:
389
- """Best Matching Unit: argmin ||W - x||² em ℝ⁴.
390
-
391
- Substitui pgvector_lookup — busca nearest-neighbor flat sobre o grid.
392
- """
393
- dist = torch.sum((self.weights - x.view(1, 1, 1, 1, 4)) ** 2, dim=-1)
394
- flat_idx = torch.argmin(dist).item()
395
- i = flat_idx // (self.J * self.K * self.L)
396
- rest = flat_idx % (self.J * self.K * self.L)
397
- j = rest // (self.K * self.L)
398
- rest = rest % (self.K * self.L)
399
- k = rest // self.L
400
- l = rest % self.L
401
- return (i, j, k, l)
402
-
403
- def update_weights(self, x: torch.Tensor, bmu_idx, accumulate_fisher=False):
404
- """Update Kohonen: ΔW = α·Λ·(x - W) + penalidade EWC em w.
405
-
406
- Args:
407
- x: tensor [4] — amostra 4D.
408
- bmu_idx: (i, j, k, l) — índice do BMU.
409
- accumulate_fisher: se True, acumula (x_w - W_w)² nos Fisher accumulators.
410
- """
411
- dist_sq = self._neighborhood(bmu_idx)
412
- sigma = self.sigma0 * math.exp(-self.t / 1000)
413
- alpha = self.alpha0 * math.exp(-self.t / 2000)
414
- h = torch.exp(-dist_sq / (2 * sigma ** 2))
415
-
416
- delta = x - self.weights
417
- update = alpha * h.unsqueeze(-1) * delta
418
-
419
- if self.old_weights_w is not None and self.fisher_w is not None:
420
- # Penalidade EWC apenas na 4ª dimensão (w)
421
- # ∂L_ewc/∂W_w = λ · F · (W_w - W*_w) → subtraído do update
422
- ewc_penalty = self.lambda_ewc * self.fisher_w * (
423
- self.weights[..., 3] - self.old_weights_w
424
- )
425
- update[..., 3] = update[..., 3] - ewc_penalty
426
-
427
- self.weights = self.weights + update
428
-
429
- if accumulate_fisher:
430
- # Acumula Fisher apenas em neurônios próximos ao BMU (Λ > 0.1)
431
- mask = h > 0.1
432
- if mask.any():
433
- diff_sq = (x[3] - self.weights[mask][..., 3]) ** 2
434
- self.fisher_accum[mask] += diff_sq
435
- self.fisher_count[mask] += 1
436
-
437
- self.t += 1
438
-
439
- def finalize_fisher(self):
440
- """Fisher = mean((x_w - W_w)²) sobre samples acumuladas."""
441
- cnt = self.fisher_count.clamp(min=1e-8)
442
- self.fisher_w = self.fisher_accum / cnt
443
-
444
- def set_ewc_reference(self):
445
- """Consolida W_w como referência EWC e finaliza Fisher."""
446
- self.old_weights_w = self.weights[..., 3].clone()
447
- self.finalize_fisher()
448
- self.fisher_accum.zero_()
449
- self.fisher_count.zero_()
450
-
451
- # ------------------------------------------------------------------
452
- # Métricas para monitoramento (V6.4)
453
- # ------------------------------------------------------------------
454
- def get_metrics(self) -> dict:
455
- """Retorna métricas atuais do SOM para monitoramento."""
456
- sigma_t = self.sigma0 * math.exp(-self.t / 1000)
457
- alpha_t = self.alpha0 * math.exp(-self.t / 2000)
458
- return {
459
- "t": int(self.t),
460
- "sigma_t": float(sigma_t),
461
- "alpha_t": float(alpha_t),
462
- "sigma0": float(self.sigma0),
463
- "alpha0": float(self.alpha0),
464
- "lambda_ewc": float(self.lambda_ewc),
465
- "grid_shape": [int(self.I), int(self.J), int(self.K), int(self.L)],
466
- "n_neurons": int(self.I * self.J * self.K * self.L),
467
- "has_ewc_reference": self.old_weights_w is not None,
468
- "fisher_w_mean": (
469
- float(self.fisher_w.mean().item())
470
- if self.fisher_w is not None
471
- else 0.0
472
- ),
473
- "fisher_w_max": (
474
- float(self.fisher_w.max().item())
475
- if self.fisher_w is not None
476
- else 0.0
477
- ),
478
- "fisher_accum_count": int(self.fisher_count.sum().item()),
479
- "weights_norm": float(self.weights.norm().item()),
480
- "weights_w_mean": float(self.weights[..., 3].mean().item()),
481
- }
482
-
483
-
484
- # ============================================================================
485
- # Classificador de hipótese (8 camadas FC)
486
- # ============================================================================
487
- class HypothesisClassifier(nn.Module):
488
- """Classificador de hipótese: 8 camadas FC + ReLU + output logit.
489
-
490
- Arquitetura: [input → 512 → 256 → 128 → 64 → 32 → 16 → 8] + ReLU
491
- + [8 → 1] (logit)
492
- Loss: BCEWithLogitsLoss
493
- """
494
-
495
- def __init__(self, input_dim, hidden_dims=[512, 256, 128, 64, 32, 16, 8]):
496
- super().__init__()
497
- layers = []
498
- prev = input_dim
499
- for h in hidden_dims:
500
- layers.append(nn.Linear(prev, h))
501
- layers.append(nn.ReLU())
502
- prev = h
503
- layers.append(nn.Linear(prev, 1))
504
- self.net = nn.Sequential(*layers)
505
-
506
- def forward(self, x):
507
- return self.net(x).squeeze(-1)
508
-
509
-
510
- # ============================================================================
511
- # Sistema de aprendizado completo (com w temporal e condição de início por N)
512
- # ============================================================================
513
- class KohonenLearningSystem:
514
- """Pipeline integrado: tokenizer + embedding + SOM4D + classifier + punishment.
515
-
516
- V6.5: + VQ-VAE-2 compressor (opcional) + reasoning_engine (opcional)
517
-
518
- Args:
519
- vocab_size: tamanho do vocabulário BBPE (default 16384).
520
- hidden_dim: dimensão do embedding (default 1024).
521
- seq_len: comprimento máximo da sequência (default 8).
522
- som_grid: (I, J, K, L) — grid 4D do SOM (default (6, 6, 6, 4) = 864).
523
- alpha0, sigma0: hiperparâmetros do SOM.
524
- lambda_ewc: peso da penalidade EWC.
525
- N_start: threshold do histograma para iniciar treino.
526
- dim_choice: 'x' | 'y' | 'z' — dimensão usada no histograma.
527
- hypothesis_hidden: arquitetura do HypothesisClassifier.
528
- T_max: normalização temporal (w = time_step / T_max).
529
- enable_vqvae2 (V6.5): ativa VQ-VAE-2 compressor no pipeline.
530
- enable_reasoning (V6.5): ativa reasoning_engine integrado.
531
- vqvae2_code_dim (V6.5): dimensão do codebook do VQ-VAE-2.
532
- vqvae2_num_codes (V6.5): tamanho do codebook top+bottom.
533
- """
534
-
535
- def __init__(
536
- self,
537
- vocab_size=16384,
538
- hidden_dim=1024,
539
- seq_len=8,
540
- som_grid=(6, 6, 6, 4),
541
- alpha0=0.1,
542
- sigma0=1.5,
543
- lambda_ewc=0.02,
544
- N_start=10,
545
- dim_choice="y",
546
- hypothesis_hidden=[512, 256, 128, 64, 32, 16, 8],
547
- T_max=10000,
548
- # V6.5 — VQ-VAE-2 + reasoning_engine
549
- enable_vqvae2: bool = True,
550
- enable_reasoning: bool = True,
551
- vqvae2_code_dim: int = 16,
552
- vqvae2_num_codes_top: int = 64,
553
- vqvae2_num_codes_bot: int = 128,
554
- # V6.5-final — SmoothQuant W8A8 compressor for SOM weights
555
- enable_w8a8: bool = True,
556
- w8a8_alpha: float = 0.5,
557
- w8a8_n_bits: int = 8,
558
- w8a8_calibration_samples: int = 128,
559
- # V6.5-attn — MultiHeadAttention integrado ao text_to_4d_vector
560
- # User requirement: "verificar se o mecanismo de atenção está ativo
561
- # e acessado logicamente funcional"
562
- enable_attention: bool = True,
563
- attention_n_heads: int = 8,
564
- ):
565
- self.tokenizer = SimpleBBPETokenizer(vocab_size)
566
- self.embedding = nn.Embedding(vocab_size, hidden_dim)
567
- self.hidden_dim = hidden_dim
568
- self.seq_len = seq_len
569
- self.T_max = T_max
570
- self.time_counter = 0 # contador global de amostras processadas
571
-
572
- self.som = KohonenSOM4D(som_grid, alpha0, sigma0, lambda_ewc)
573
- self.som_grid = som_grid
574
- self.som_neuron_count = (
575
- som_grid[0] * som_grid[1] * som_grid[2] * som_grid[3]
576
- )
577
-
578
- self.classifier: Optional[HypothesisClassifier] = None
579
- self.hypothesis_hidden = hypothesis_hidden
580
- self.classifier_trained = False
581
-
582
- self.buffer_4d = []
583
- self.buffer_labels = []
584
- self.training_ready = False
585
- self.N = N_start
586
- self.dim_choice = dim_choice
587
- self.dim_index = {"x": 0, "y": 1, "z": 2}[dim_choice]
588
-
589
- self.punishment_count = 0
590
- self.success_count = 0
591
- self.histogram = Counter()
592
-
593
- self.required_new_samples = 0
594
-
595
- # ------------------------------------------------------------------
596
- # V6.5 — VQ-VAE-2 compressor (ativa efetiva no pipeline)
597
- # ------------------------------------------------------------------
598
- self.enable_vqvae2 = enable_vqvae2
599
- self.vqvae2_compressor = None
600
- self.vqvae2_metrics_history: List[Dict[str, Any]] = []
601
- if enable_vqvae2:
602
- try:
603
- from .vqvae2_hierarchical_flexnet import HierarchicalVQVAE2
604
- # Modalidade única: "som_4d" com input_dim=4
605
- self.vqvae2_compressor = HierarchicalVQVAE2(
606
- modalities={"som_4d": 4},
607
- code_dim=vqvae2_code_dim,
608
- num_codes_top=vqvae2_num_codes_top,
609
- num_codes_bot=vqvae2_num_codes_bot,
610
- hidden=32,
611
- beta=0.25,
612
- ema_decay=0.99,
613
- dead_code_threshold=1.0,
614
- dead_code_restart_every=3,
615
- goose_temp_init=2.0,
616
- goose_temp_final=0.5,
617
- goose_schedule="cosine",
618
- total_epochs=25,
619
- norm_type="none",
620
- rmsnorm_in_vq=False,
621
- )
622
- # Inicia em modo treino para ativar EMA updates
623
- self.vqvae2_compressor.train()
624
- except Exception as e:
625
- # Fallback: desabilita VQ-VAE-2 se houver erro de import
626
- self.enable_vqvae2 = False
627
- self.vqvae2_compressor = None
628
- import warnings
629
- warnings.warn(f"VQ-VAE-2 disabled: {e}")
630
-
631
- # ------------------------------------------------------------------
632
- # V6.5-final — SmoothQuantCompressor for REAL W8A8 of SOM weights
633
- # ------------------------------------------------------------------
634
- # User requirement: "integrar SmoothQuantCompressor ao KLS para
635
- # compressão W8A8 real dos pesos do SOM"
636
- #
637
- # O SmoothQuantCompressor é instanciado e calibrado sobre os pesos
638
- # atuais do SOM (self.som.weights reshape para [N_neurons, 4]) usando
639
- # o buffer_4d como amostras de ativação reais. Após calibração, o
640
- # método compress_som_weights_w8a8() aplica quantização INT8 simétrica
641
- # (com smoothing) e dequantização imediata, preservando a topologia
642
- # do SOM com erro relativo < 1%.
643
- self.enable_w8a8 = enable_w8a8
644
- self.w8a8_compressor = None
645
- self.w8a8_metrics_history: List[Dict[str, Any]] = []
646
- if enable_w8a8:
647
- try:
648
- from ..quantization.smoothquant_compressor import SmoothQuantCompressor
649
- self.w8a8_compressor = SmoothQuantCompressor(
650
- alpha=w8a8_alpha,
651
- n_bits=w8a8_n_bits,
652
- calibration_samples=w8a8_calibration_samples,
653
- )
654
- except Exception as e:
655
- self.enable_w8a8 = False
656
- self.w8a8_compressor = None
657
- import warnings
658
- warnings.warn(f"SmoothQuantCompressor disabled: {e}")
659
-
660
- # ------------------------------------------------------------------
661
- # V6.5 — ReasoningEngine (integração ativa + tool_coordinator workers reativados)
662
- # ------------------------------------------------------------------
663
- self.enable_reasoning = enable_reasoning
664
- self.reasoning_engine = None
665
- if enable_reasoning:
666
- try:
667
- from ..reasoning.reasoning_engine import ReasoningEngine
668
- # V6.5-final: REATIVAR tool_coordinator workers — explicit n_workers
669
- # User requirement: "reativar reasoning_engine's tool_coordinator workers"
670
- # O ReasoningEngine cria internamente ToolAgentCoordinator(n_workers=4)
671
- # que mantém um ThreadPoolExecutor ativo. Para garantir que os
672
- # workers sejam de fato utilizados (não apenas instanciados),
673
- # o método _compress_buffer_with_vqvae2 abaixo chama
674
- # execute_parallel() periodicamente durante o treino.
675
- self.reasoning_engine = ReasoningEngine(
676
- max_thinking_steps=10,
677
- max_iterations=3,
678
- convergence_threshold=0.9,
679
- verbose=False,
680
- )
681
- # Verifica que tool_coordinator + workers estão ativos
682
- tc = self.reasoning_engine.tool_coordinator
683
- self._tool_coordinator_workers_active = (
684
- tc is not None and tc._executor is not None
685
- )
686
- # Registra uma ferramenta interna: consultar SOM
687
- def som_query_tool(query: str) -> str:
688
- """Ferramenta: consulta o SOM do KLS para responder."""
689
- pred = self.predict(query)
690
- bmu_info = ""
691
- if self.buffer_4d:
692
- try:
693
- vec = text_to_4d_vector(
694
- query, self.tokenizer, self.embedding,
695
- self.hidden_dim, self.seq_len,
696
- self.time_counter, self.T_max,
697
- )
698
- bmu = self.som.find_bmu(vec)
699
- bmu_info = f" | BMU={bmu}"
700
- except Exception:
701
- pass
702
- return f"prediction={pred}{bmu_info}"
703
- self.reasoning_engine.register_tool(
704
- "som_query", som_query_tool,
705
- description="Consulta o SOM do KohonenLearningSystem",
706
- timeout_s=10.0,
707
- )
708
- # V6.5-final: registra segunda ferramenta para forçar uso
709
- # paralelo dos workers via execute_parallel()
710
- def buffer_stats_tool(query: str) -> str:
711
- """Ferramenta: retorna estatísticas do buffer atual."""
712
- return (
713
- f"buffer_size={len(self.buffer_4d)}, "
714
- f"punishment_count={self.punishment_count}, "
715
- f"success_count={self.success_count}, "
716
- f"training_ready={self.training_ready}"
717
- )
718
- self.reasoning_engine.register_tool(
719
- "buffer_stats", buffer_stats_tool,
720
- description="Estatísticas do buffer interno do KLS",
721
- timeout_s=5.0,
722
- )
723
- except Exception as e:
724
- self.enable_reasoning = False
725
- self.reasoning_engine = None
726
- self._tool_coordinator_workers_active = False
727
- import warnings
728
- warnings.warn(f"ReasoningEngine disabled: {e}")
729
-
730
- # ------------------------------------------------------------------
731
- # V6.5-attn — MultiHeadAttention integrado ao text_to_4d_vector
732
- # ------------------------------------------------------------------
733
- # User requirement: "verificar se o mecanismo de atenção está ativo
734
- # e acessado logicamente funcional"
735
- #
736
- # O MultiHeadAttention (de attention_multimodal.py) é instanciado com
737
- # d_model=hidden_dim e n_heads=attention_n_heads. Ele é aplicado em
738
- # text_to_4d_vector entre a fusão (+PE) e o SVD, atuando como
739
- # self-attention residual sobre a sequência embedded.
740
- #
741
- # Métricas (n_calls, last_norm_in/out, last_attn_activated) são
742
- # acumuladas em self.attention_metrics e expostas via
743
- # get_attention_metrics() para verificação de "ativo e funcional".
744
- self.enable_attention = enable_attention
745
- self.attention = None
746
- self.attention_metrics: Dict[str, Any] = {
747
- "n_calls": 0,
748
- "n_errors": 0,
749
- "last_norm_in": 0.0,
750
- "last_norm_out": 0.0,
751
- "last_attn_activated": False,
752
- "last_attn_diff_norm": 0.0,
753
- }
754
- if enable_attention:
755
- try:
756
- from .attention_multimodal import MultiHeadAttention
757
- # d_model = hidden_dim, n_heads = attention_n_heads
758
- # assert hidden_dim % n_heads == 0
759
- n_heads = attention_n_heads
760
- if hidden_dim % n_heads != 0:
761
- # ajusta para divisor válido mais próximo
762
- for cand in [8, 4, 2, 1]:
763
- if hidden_dim % cand == 0:
764
- n_heads = cand
765
- break
766
- self.attention = MultiHeadAttention(
767
- d_model=hidden_dim,
768
- n_heads=n_heads,
769
- dropout=0.0,
770
- max_chunk=4096,
771
- )
772
- # Em eval mode para determinismo (não treinamos o attention separadamente)
773
- self.attention.eval()
774
- self.attention_n_heads = n_heads
775
- except Exception as e:
776
- self.enable_attention = False
777
- self.attention = None
778
- import warnings
779
- warnings.warn(f"MultiHeadAttention disabled: {e}")
780
-
781
- def get_attention_metrics(self) -> Dict[str, Any]:
782
- """V6.5-attn — Retorna métricas do MultiHeadAttention integrado.
783
-
784
- User requirement: "verificar se o mecanismo de atenção está ativo
785
- e acessado logicamente funcional"
786
-
787
- Returns:
788
- Dict com:
789
- - active: bool — attention module está instanciado
790
- - n_calls: int — número de chamadas forward
791
- - n_errors: int — número de erros silenciados
792
- - last_norm_in / last_norm_out: normas do input/output
793
- - last_attn_activated: bool — output é finito e diferente do input
794
- - last_attn_diff_norm: float — ||attn_out - input||
795
- - logic_functional: bool — n_calls > 0 AND n_errors == 0
796
- AND last_attn_activated == True
797
- """
798
- m = self.attention_metrics
799
- return {
800
- "active": bool(self.attention is not None),
801
- "n_calls": int(m.get("n_calls", 0)),
802
- "n_errors": int(m.get("n_errors", 0)),
803
- "last_norm_in": float(m.get("last_norm_in", 0.0)),
804
- "last_norm_out": float(m.get("last_norm_out", 0.0)),
805
- "last_attn_activated": bool(m.get("last_attn_activated", False)),
806
- "last_attn_diff_norm": float(m.get("last_attn_diff_norm", 0.0)),
807
- "n_heads": int(getattr(self, "attention_n_heads", 0)),
808
- # "logicamente funcional" = foi chamado, sem erros, e última chamada
809
- # produziu output finito e diferente do input
810
- "logic_functional": bool(
811
- self.attention is not None
812
- and m.get("n_calls", 0) > 0
813
- and m.get("n_errors", 0) == 0
814
- and m.get("last_attn_activated", False)
815
- ),
816
- }
817
-
818
- def add_data(self, sentences: List[str], labels: List[int]):
819
- """Adiciona amostras: text → 4D vector + atualiza histograma."""
820
- for sent, lab in zip(sentences, labels):
821
- self.time_counter += 1
822
- vec = text_to_4d_vector(
823
- sent,
824
- self.tokenizer,
825
- self.embedding,
826
- self.hidden_dim,
827
- self.seq_len,
828
- self.time_counter,
829
- self.T_max,
830
- attention=self.attention, # V6.5-attn
831
- attention_metrics=self.attention_metrics, # V6.5-attn
832
- )
833
- self.buffer_4d.append(vec)
834
- self.buffer_labels.append(lab)
835
- dim_val = round(vec[self.dim_index].item(), 2)
836
- self.histogram[dim_val] += 1
837
-
838
- def check_training_start(self) -> bool:
839
- """Inicia treino quando algum bucket do histograma atinge N."""
840
- if (
841
- not self.training_ready
842
- and max(self.histogram.values(), default=0) >= self.N
843
- ):
844
- self.training_ready = True
845
- return True
846
- return False
847
-
848
- def train_som_on_buffer(self):
849
- """Treina SOM por 5 épocas sobre o buffer atual (com Fisher accum)."""
850
- if not self.buffer_4d:
851
- return
852
- data = torch.stack(self.buffer_4d)
853
- for _ in range(5): # épocas de treino rápido
854
- perm = torch.randperm(len(data))
855
- for idx in perm:
856
- x = data[idx]
857
- bmu = self.som.find_bmu(x)
858
- acc_fisher = (
859
- self.punishment_count == 0
860
- and self.som.old_weights_w is None
861
- )
862
- self.som.update_weights(x, bmu, accumulate_fisher=acc_fisher)
863
-
864
- # V6.5 — Ativa VQ-VAE-2 compressor no pipeline
865
- if self.enable_vqvae2 and self.vqvae2_compressor is not None:
866
- self._compress_buffer_with_vqvae2(data)
867
-
868
- # V6.5-final — Ativa SmoothQuant W8A8 compressor dos pesos do SOM
869
- # User requirement: "integrar SmoothQuantCompressor ao KLS para
870
- # compressão W8A8 real dos pesos do SOM"
871
- if self.enable_w8a8 and self.w8a8_compressor is not None:
872
- self.compress_som_weights_w8a8()
873
-
874
- # V6.5-final — Reativa tool_coordinator workers via execute_parallel
875
- # User requirement: "reativar reasoning_engine's tool_coordinator workers"
876
- # O ThreadPoolExecutor (n_workers=4) só é efetivamente utilizado quando
877
- # execute_parallel() é chamado com múltiplas ferramentas. Caso contrário,
878
- # os workers ficam idle e o recurso é desperdiçado.
879
- self._exercise_tool_coordinator_workers()
880
-
881
- def _exercise_tool_coordinator_workers(self) -> Dict[str, Any]:
882
- """V6.5-final — Exercita os workers do tool_coordinator em paralelo.
883
-
884
- Chama execute_parallel() com todas as ferramentas registradas para
885
- garantir que o ThreadPoolExecutor (n_workers=4) seja efetivamente
886
- utilizado, cumprindo o requisito "reativar tool_coordinator workers".
887
-
888
- Returns:
889
- Dict com resultados de cada ferramenta + tempo total.
890
- """
891
- if (
892
- not self.enable_reasoning
893
- or self.reasoning_engine is None
894
- ):
895
- return {"active": False, "reason": "reasoning_disabled"}
896
-
897
- tc = self.reasoning_engine.tool_coordinator
898
- if tc is None:
899
- return {"active": False, "reason": "no_tool_coordinator"}
900
-
901
- try:
902
- import time as _time
903
- t0 = _time.time()
904
- # Submete TODAS as ferramentas registradas em paralelo — isso
905
- # efetivamente acorda os workers do ThreadPoolExecutor.
906
- tool_names = list(tc._tools.keys())
907
- if not tool_names:
908
- return {"active": True, "n_workers": tc.n_workers, "n_tools": 0}
909
- # Executa em paralelo: cada ferramenta recebe um query dummy
910
- requests = [(name, "status_check") for name in tool_names]
911
- results = tc.execute_parallel(requests, fail_fast=False)
912
- elapsed_ms = (_time.time() - t0) * 1000
913
- return {
914
- "active": True,
915
- "n_workers": tc.n_workers,
916
- "n_tools": len(tool_names),
917
- "tool_names": tool_names,
918
- "n_results": len(results),
919
- "elapsed_ms": float(elapsed_ms),
920
- "workers_exercised": True,
921
- }
922
- except Exception as e:
923
- return {
924
- "active": False,
925
- "error": str(e)[:200],
926
- "workers_exercised": False,
927
- }
928
-
929
- def _compress_buffer_with_vqvae2(self, data: torch.Tensor) -> Dict[str, Any]:
930
- """V6.5 — Comprime buffer 4D via VQ-VAE-2 hierárquico.
931
-
932
- Ativa efetivamente o VQ-VAE-2 no pipeline de compressão:
933
- 1. Encoder: (B, 4) → z_e (B, code_dim)
934
- 2. VQ hierárquico: z_e → z_q_top + z_q_bot (codebooks EMA + Goose)
935
- 3. Decoder: z_q_combined → z_recon (B, 4)
936
- 4. Loss: commitment (top+bot) + reconstruction (MSE)
937
- 5. Códigos top/bottom retornados para inspeção
938
-
939
- Args:
940
- data: tensor (B, 4) com vetores 4D do buffer.
941
-
942
- Returns:
943
- Dict com vqvae2_metrics (também armazenado em vqvae2_metrics_history).
944
- """
945
- try:
946
- # Sanitiza NaN/Inf
947
- data_clean = torch.nan_to_num(data, nan=0.0, posinf=1e4, neginf=-1e4)
948
- # VQ-VAE-2 espera dict {modality_name: tensor}
949
- batch = {"som_4d": data_clean}
950
- out = self.vqvae2_compressor(batch)
951
- # Incrementa época do VQ (controla schedule Goose + dead code restart)
952
- try:
953
- self.vqvae2_compressor.vq.increment_epoch()
954
- except Exception:
955
- pass
956
- stats = out.get("stats", {})
957
- vqvae2_metrics = {
958
- "vq_loss": float(out.get("vq_loss", 0.0)),
959
- "recon_loss": float(out.get("recon_loss", 0.0)),
960
- "total_loss": float(out.get("total_loss", 0.0)),
961
- "n_used_top": int(stats.get("n_used_top", 0)),
962
- "n_used_bot": int(stats.get("n_used_bot", 0)),
963
- "usage_ratio_top": float(stats.get("usage_ratio_top", 0.0)),
964
- "usage_ratio_bot": float(stats.get("usage_ratio_bot", 0.0)),
965
- "codebook_ppl_top": float(stats.get("codebook_ppl_top", 0.0)),
966
- "codebook_ppl_bot": float(stats.get("codebook_ppl_bot", 0.0)),
967
- "n_restarted_top": int(stats.get("n_restarted_top", 0)),
968
- "n_restarted_bot": int(stats.get("n_restarted_bot", 0)),
969
- "goose_temp": float(stats.get("goose_temp", 0.0)),
970
- "active": True,
971
- }
972
- self.vqvae2_metrics_history.append(vqvae2_metrics)
973
- # Mantém apenas últimas 100 entries para limitar memória
974
- if len(self.vqvae2_metrics_history) > 100:
975
- self.vqvae2_metrics_history = self.vqvae2_metrics_history[-100:]
976
- return vqvae2_metrics
977
- except Exception as e:
978
- return {
979
- "active": False,
980
- "error": str(e)[:200],
981
- "vq_loss": 0.0,
982
- "recon_loss": 0.0,
983
- "total_loss": 0.0,
984
- }
985
-
986
- def get_vqvae2_metrics(self) -> Dict[str, Any]:
987
- """V6.5 — Retorna métricas atuais do VQ-VAE-2 compressor."""
988
- if not self.enable_vqvae2 or self.vqvae2_compressor is None:
989
- return {"active": False, "reason": "disabled"}
990
- if not self.vqvae2_metrics_history:
991
- return {"active": True, "n_calls": 0}
992
- latest = self.vqvae2_metrics_history[-1]
993
- # V6.5: skip NaN values when computing means (early calls may produce NaN
994
- # due to Gumbel-softmax instability before codebook warmup)
995
- import math
996
- valid_total = [m.get("total_loss", 0.0) for m in self.vqvae2_metrics_history
997
- if not math.isnan(m.get("total_loss", 0.0))]
998
- valid_recon = [m.get("recon_loss", 0.0) for m in self.vqvae2_metrics_history
999
- if not math.isnan(m.get("recon_loss", 0.0))]
1000
- return {
1001
- "active": True,
1002
- "n_calls": len(self.vqvae2_metrics_history),
1003
- "latest": latest,
1004
- "mean_total_loss": float(sum(valid_total) / max(1, len(valid_total))) if valid_total else 0.0,
1005
- "mean_recon_loss": float(sum(valid_recon) / max(1, len(valid_recon))) if valid_recon else 0.0,
1006
- "n_nan_skipped": len(self.vqvae2_metrics_history) - len(valid_total),
1007
- }
1008
-
1009
- # ==================================================================
1010
- # V6.5-final — SmoothQuant W8A8 compression of SOM weights
1011
- # ==================================================================
1012
- # User requirement: "integrar SmoothQuantCompressor ao KLS para
1013
- # compressão W8A8 real dos pesos do SOM"
1014
- #
1015
- # Pipeline (executado periodicamente durante o treino):
1016
- # 1. Coleta pesos atuais do SOM: W ∈ ℝ^(I,J,K,L,4) → reshape [N, 4]
1017
- # 2. Coleta buffer_4d como amostras de ativação reais: X ∈ ℝ^(B, 4)
1018
- # 3. Calibra SmoothQuant: s_j = max|X_j|^α / max|W_j|^(1-α)
1019
- # 4. Aplica smoothing: W_smooth = W * diag(s)
1020
- # 5. Quantiza INT8 simétrico per-tensor: W_int8 = round(W_smooth / scale)
1021
- # 6. Dequantiza: W_dequant = W_int8 * scale / diag(s) (volta para float)
1022
- # 7. Substitui self.som.weights pelo dequantizado (preserva topologia)
1023
- # 8. Computa erro relativo: ||W - W_dequant|| / ||W||
1024
- #
1025
- # Erro esperado: < 1% (SmoothQuant preserva acurácia).
1026
- def compress_som_weights_w8a8(self) -> Dict[str, Any]:
1027
- """V6.5-final — Aplica compressão W8A8 real aos pesos do SOM.
1028
-
1029
- SmoothQuant migra escala da ativação para o peso, permitindo
1030
- quantização INT8 sem perda significativa. Os pesos são quantizados
1031
- a INT8 e dequantizados de volta para float, preservando a
1032
- topologia do SOM com erro relativo < 1%.
1033
-
1034
- User requirement: "compressão W8A8 real dos pesos do SOM"
1035
-
1036
- Returns:
1037
- Dict com métricas de compressão (erro, ratio, scale stats).
1038
- """
1039
- if not self.enable_w8a8 or self.w8a8_compressor is None:
1040
- return {"active": False, "reason": "w8a8_disabled"}
1041
-
1042
- try:
1043
- import time as _time
1044
- t0 = _time.time()
1045
-
1046
- # 1. Snapshot dos pesos atuais do SOM
1047
- W = self.som.weights # (I, J, K, L, 4)
1048
- original_shape = W.shape
1049
- N = W.shape[0] * W.shape[1] * W.shape[2] * W.shape[3]
1050
- # Reshape para [N, 4] — trata cada neurônio como uma "linha"
1051
- # e cada componente (x, y, z, w) como uma "feature"
1052
- W_2d = W.reshape(N, 4).clone().detach()
1053
- W_orig_norm = W_2d.norm().item()
1054
-
1055
- # 2. Calibra com buffer_4d (amostras de ativação reais)
1056
- if len(self.buffer_4d) >= 4:
1057
- X = torch.stack(self.buffer_4d[-self.w8a8_compressor.calibration_samples:])
1058
- X = X.detach().clone()
1059
- else:
1060
- # Fallback: usa os próprios pesos como amostras de ativação
1061
- X = W_2d.detach().clone()
1062
-
1063
- # Sanitiza NaN/Inf
1064
- W_2d = torch.nan_to_num(W_2d, nan=0.0, posinf=1e4, neginf=-1e4)
1065
- X = torch.nan_to_num(X, nan=0.0, posinf=1e4, neginf=-1e4)
1066
-
1067
- # 3. Calibra SmoothQuant (computa smooth_scale s)
1068
- self.w8a8_compressor.calibrate(W_2d, X)
1069
- smooth_scale = self.w8a8_compressor.smooth_scale
1070
-
1071
- # 4. Aplica smoothing: W_smooth = W * diag(s)
1072
- W_smooth = self.w8a8_compressor.smooth_weight(W_2d)
1073
-
1074
- # 5. Quantiza INT8 simétrico per-tensor
1075
- W_int8 = self.w8a8_compressor.quantize_per_tensor_symmetric(W_smooth)
1076
-
1077
- # 6. Dequantiza: W_int8 * scale → W_smooth_recon, depois / diag(s) → W_dequant
1078
- W_smooth_recon = self.w8a8_compressor.dequantize(W_int8, W_smooth)
1079
- # Reverte smoothing: W_dequant = W_smooth_recon / diag(s)
1080
- W_dequant = W_smooth_recon / smooth_scale.unsqueeze(0)
1081
-
1082
- # 7. Computa erro relativo
1083
- error = (W_2d - W_dequant).norm().item()
1084
- relative_error = error / max(W_orig_norm, 1e-8)
1085
-
1086
- # 8. Substitui pesos do SOM pelo dequantizado (preserva topologia)
1087
- # IMPORTANTE: usa no_grad para não poluir gradiente (SOM não usa
1088
- # autograd, mas mantemos consistência)
1089
- with torch.no_grad():
1090
- self.som.weights = W_dequant.reshape(*original_shape).clone()
1091
-
1092
- # 9. Métricas
1093
- compression_ratio = (W_2d.element_size() * W_2d.numel()) / (W_int8.element_size() * W_int8.numel())
1094
- metrics = {
1095
- "active": True,
1096
- "n_neurons": int(N),
1097
- "n_features": 4,
1098
- "n_calibration_samples": int(X.shape[0]),
1099
- "smooth_scale_mean": float(smooth_scale.mean().item()),
1100
- "smooth_scale_std": float(smooth_scale.std().item()),
1101
- "smooth_scale_min": float(smooth_scale.min().item()),
1102
- "smooth_scale_max": float(smooth_scale.max().item()),
1103
- "alpha": float(self.w8a8_compressor.alpha),
1104
- "n_bits": int(self.w8a8_compressor.n_bits),
1105
- "compression_ratio": float(compression_ratio),
1106
- "weights_norm_original": float(W_orig_norm),
1107
- "weights_norm_dequant": float(W_dequant.norm().item()),
1108
- "absolute_error": float(error),
1109
- "relative_error": float(relative_error),
1110
- "preserves_topology": bool(relative_error < 0.05), # < 5% = OK
1111
- "w8a8_int8_range": [
1112
- int(W_int8.min().item()),
1113
- int(W_int8.max().item()),
1114
- ],
1115
- "elapsed_ms": float((_time.time() - t0) * 1000),
1116
- }
1117
-
1118
- self.w8a8_metrics_history.append(metrics)
1119
- # Mantém apenas últimas 100 entries
1120
- if len(self.w8a8_metrics_history) > 100:
1121
- self.w8a8_metrics_history = self.w8a8_metrics_history[-100:]
1122
-
1123
- return metrics
1124
- except Exception as e:
1125
- return {
1126
- "active": False,
1127
- "error": str(e)[:200],
1128
- "preserves_topology": False,
1129
- }
1130
-
1131
- def get_w8a8_metrics(self) -> Dict[str, Any]:
1132
- """V6.5-final — Retorna métricas atuais do compressor W8A8."""
1133
- if not self.enable_w8a8 or self.w8a8_compressor is None:
1134
- return {"active": False, "reason": "disabled"}
1135
- if not self.w8a8_metrics_history:
1136
- return {"active": True, "n_calls": 0}
1137
- latest = self.w8a8_metrics_history[-1]
1138
- all_errors = [m.get("relative_error", 0.0) for m in self.w8a8_metrics_history]
1139
- return {
1140
- "active": True,
1141
- "n_calls": len(self.w8a8_metrics_history),
1142
- "latest": latest,
1143
- "mean_relative_error": float(sum(all_errors) / max(1, len(all_errors))),
1144
- "max_relative_error": float(max(all_errors)) if all_errors else 0.0,
1145
- "all_preserve_topology": all(m.get("preserves_topology", False) for m in self.w8a8_metrics_history),
1146
- }
1147
-
1148
- # ------------------------------------------------------------------
1149
- # V6.5 — ReasoningEngine integration
1150
- # ------------------------------------------------------------------
1151
- def reason_about(self, query: str) -> Iterator[str]:
1152
- """V6.5 — Gera streaming de raciocínio para uma query.
1153
-
1154
- Usa o ReasoningEngine integrado para produzir tags <think>, <plan>,
1155
- <decompose>, <execute>, <monitor>, <predict>, <adjust>, <answer>.
1156
-
1157
- Compatível com Ollama/LangChain/vLLM (tags padrão).
1158
-
1159
- Args:
1160
- query: pergunta/requisição do usuário.
1161
-
1162
- Yields:
1163
- chunks de texto (tags + conteúdo).
1164
- """
1165
- if not self.enable_reasoning or self.reasoning_engine is None:
1166
- yield f"<answer>ReasoningEngine disabled. SOM prediction: {self.predict(query)}</answer>"
1167
- return
1168
- yield from self.reasoning_engine.solve(query, use_tools=True, use_planning=True)
1169
-
1170
- def reason_sync(self, query: str) -> str:
1171
- """V6.5 — Versão síncrona de reason_about (retorna string completa)."""
1172
- return "".join(self.reason_about(query))
1173
-
1174
- def get_reasoning_stats(self) -> Dict[str, Any]:
1175
- """V6.5 — Retorna estatísticas do reasoning_engine."""
1176
- if not self.enable_reasoning or self.reasoning_engine is None:
1177
- return {"active": False, "reason": "disabled"}
1178
- return {
1179
- "active": True,
1180
- "stats": self.reasoning_engine.get_stats(),
1181
- "n_history": len(self.reasoning_engine.history),
1182
- }
1183
-
1184
- def _som_activation(self, x):
1185
- """Vetor de ativação SOM: distâncias de x a todos os neurônios (flatten)."""
1186
- dist = torch.sum((self.som.weights - x.view(1, 1, 1, 1, 4)) ** 2, dim=-1)
1187
- return dist.flatten()
1188
-
1189
- def _label_neurons(self):
1190
- """Rotula neurônios por votação majoritária sobre o buffer."""
1191
- self.neuron_label = {}
1192
- if not self.buffer_4d:
1193
- return
1194
- data = torch.stack(self.buffer_4d)
1195
- labels = torch.tensor(self.buffer_labels)
1196
- votes = defaultdict(lambda: [0, 0])
1197
- for i in range(len(data)):
1198
- bmu = self.som.find_bmu(data[i])
1199
- votes[bmu][int(labels[i].item())] += 1
1200
- for bmu, v in votes.items():
1201
- self.neuron_label[bmu] = 1.0 if v[1] > v[0] else 0.0
1202
-
1203
- def evaluate_classification(self) -> float:
1204
- """Acurácia sobre o buffer atual."""
1205
- if not self.buffer_4d:
1206
- return 1.0
1207
- data = torch.stack(self.buffer_4d)
1208
- labels = torch.tensor(self.buffer_labels).float()
1209
- correct = 0
1210
- for i in range(len(data)):
1211
- pred = self._predict_single(data[i])
1212
- if (pred > 0.5) == (labels[i] > 0.5):
1213
- correct += 1
1214
- return correct / len(data)
1215
-
1216
- def _predict_single(self, x):
1217
- """Prediz: classifier (se treinado) ou voto do BMU."""
1218
- if self.classifier is not None and self.classifier_trained:
1219
- with torch.no_grad():
1220
- act = self._som_activation(x).unsqueeze(0)
1221
- logit = self.classifier(act)
1222
- return torch.sigmoid(logit).item()
1223
- else:
1224
- if not hasattr(self, "neuron_label"):
1225
- self._label_neurons()
1226
- bmu = self.som.find_bmu(x)
1227
- return self.neuron_label.get(bmu, 0.5)
1228
-
1229
- def activate_hypothesis(self):
1230
- """Treina o HypothesisClassifier (8 FC layers) por 50 epochs.
1231
-
1232
- BUG FIX (V6.3→V6.4): buffer_4d contém tensores que carregam o grafo
1233
- de computação do embedding. Para evitar "Trying to backward through
1234
- the graph a second time", fazemos detach+clone e calculamos as
1235
- ativações SOM dentro de torch.no_grad(). O classifier treina apenas
1236
- sobre seus próprios pesos.
1237
- """
1238
- if self.classifier is None:
1239
- self.classifier = HypothesisClassifier(
1240
- self.som_neuron_count, self.hypothesis_hidden
1241
- )
1242
- # FIX: detach+clone para isolar do grafo do embedding
1243
- data = torch.stack(self.buffer_4d).detach().clone()
1244
- labels = torch.tensor(self.buffer_labels).float()
1245
- # FIX: ativações SEM gradiente (não queremos treinar SOM/embedding aqui)
1246
- with torch.no_grad():
1247
- X = torch.stack([self._som_activation(data[i]) for i in range(len(data))])
1248
- optimizer = torch.optim.Adam(self.classifier.parameters(), lr=0.001)
1249
- criterion = nn.BCEWithLogitsLoss()
1250
- for _ in range(50):
1251
- optimizer.zero_grad()
1252
- loss = criterion(self.classifier(X), labels)
1253
- loss.backward()
1254
- optimizer.step()
1255
- self.classifier_trained = True
1256
-
1257
- def process_batch(self, sentences: List[str], labels: List[int]):
1258
- """Processa batch: adiciona dados, treina SOM se ready, aplica punishment.
1259
-
1260
- Returns:
1261
- True se o protocolo de punishment completou um ciclo (2ª punição
1262
- → set_ewc_reference + reset). False caso contrário.
1263
- """
1264
- self.add_data(sentences, labels)
1265
-
1266
- if self.check_training_start():
1267
- self.train_som_on_buffer()
1268
- self._label_neurons()
1269
-
1270
- if self.training_ready:
1271
- acc = self.evaluate_classification()
1272
- if acc < 1.0:
1273
- self.punishment_count += 1
1274
- self.success_count = 0
1275
- if self.punishment_count == 1:
1276
- self.activate_hypothesis()
1277
- elif self.punishment_count == 2:
1278
- self.som.set_ewc_reference()
1279
- self.required_new_samples = (
1280
- self.success_count * self.N
1281
- if self.success_count > 0
1282
- else self.N
1283
- )
1284
- self.training_ready = False
1285
- self.punishment_count = 0
1286
- self.success_count = 0
1287
- self.histogram.clear()
1288
- self.buffer_4d.clear()
1289
- self.buffer_labels.clear()
1290
- return True
1291
- else:
1292
- self.punishment_count = 0
1293
- self.success_count += 1
1294
- return False
1295
-
1296
- def predict(self, sentence: str) -> str:
1297
- """Prediz rótulo textual ("gato" se prob ≤ 0.5, "cachorro" caso contrário).
1298
-
1299
- V6.5-attn: passa o attention module ao text_to_4d_vector para que
1300
- o self-attention seja aplicado também em inference.
1301
- """
1302
- self.time_counter += 1 # mantém coerência temporal
1303
- vec = text_to_4d_vector(
1304
- sentence,
1305
- self.tokenizer,
1306
- self.embedding,
1307
- self.hidden_dim,
1308
- self.seq_len,
1309
- self.time_counter,
1310
- self.T_max,
1311
- attention=self.attention, # V6.5-attn
1312
- attention_metrics=self.attention_metrics, # V6.5-attn
1313
- )
1314
- prob = self._predict_single(vec)
1315
- return "gato" if prob <= 0.5 else "cachorro"
1316
-
1317
- # ------------------------------------------------------------------
1318
- # API de monitoramento (V6.4 + V6.5)
1319
- # ------------------------------------------------------------------
1320
- def get_state_metrics(self) -> dict:
1321
- """Retorna métricas completas do sistema para monitoramento.
1322
-
1323
- V6.5: inclui vqvae2_metrics e reasoning_metrics.
1324
- V6.5-final: inclui w8a8_metrics e tool_coordinator_workers.
1325
- """
1326
- som_metrics = self.som.get_metrics()
1327
- return {
1328
- "som": som_metrics,
1329
- "kls": {
1330
- "time_counter": int(self.time_counter),
1331
- "T_max": int(self.T_max),
1332
- "buffer_size": int(len(self.buffer_4d)),
1333
- "training_ready": bool(self.training_ready),
1334
- "punishment_count": int(self.punishment_count),
1335
- "success_count": int(self.success_count),
1336
- "classifier_trained": bool(self.classifier_trained),
1337
- "histogram_size": int(len(self.histogram)),
1338
- "histogram_max": int(max(self.histogram.values(), default=0)),
1339
- "N_start": int(self.N),
1340
- "dim_choice": str(self.dim_choice),
1341
- "som_neuron_count": int(self.som_neuron_count),
1342
- "required_new_samples": int(self.required_new_samples),
1343
- "has_classifier": self.classifier is not None,
1344
- "vocab_size": int(self.tokenizer.vocab_size),
1345
- "hidden_dim": int(self.hidden_dim),
1346
- "seq_len": int(self.seq_len),
1347
- # V6.5
1348
- "enable_vqvae2": bool(self.enable_vqvae2),
1349
- "enable_reasoning": bool(self.enable_reasoning),
1350
- "enable_w8a8": bool(self.enable_w8a8),
1351
- "vqvae2_n_calls": int(len(self.vqvae2_metrics_history)),
1352
- "w8a8_n_calls": int(len(self.w8a8_metrics_history)),
1353
- # V6.5-final: tool_coordinator workers reativados
1354
- "tool_coordinator_workers_active": bool(
1355
- getattr(self, "_tool_coordinator_workers_active", False)
1356
- ),
1357
- # V6.5-attn: attention module integrado
1358
- "enable_attention": bool(getattr(self, "enable_attention", False)),
1359
- "attention_active": bool(self.attention is not None),
1360
- "attention_n_heads": int(getattr(self, "attention_n_heads", 0)),
1361
- },
1362
- # V6.5 — VQ-VAE-2 metrics
1363
- "vqvae2": self.get_vqvae2_metrics(),
1364
- # V6.5 — ReasoningEngine metrics
1365
- "reasoning": self.get_reasoning_stats(),
1366
- # V6.5-attn — MultiHeadAttention metrics
1367
- "attention": self.get_attention_metrics(),
1368
- # V6.5-final — SmoothQuant W8A8 metrics
1369
- "w8a8": self.get_w8a8_metrics(),
1370
- }