PowerMachine commited on
Commit
6ab683e
·
verified ·
1 Parent(s): 7e0516f

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

Browse files
src/bigru_t/model/kohonen_refactored/kohonen_learning_system.py DELETED
@@ -1,671 +0,0 @@
1
- """kohonen_learning_system.py — V6.4 (refatoração matemática completa)
2
-
3
- Implementação CANÔNICA do Sistema de Aprendizado Kohonen 4D fornecida pelo
4
- usuário, com análise matemática formal e 2 correções de bugs críticos.
5
-
6
- ============================================================================
7
- ANÁLISE MATEMÁTICA FORMAL
8
- ============================================================================
9
-
10
- 1) text_to_4d_vector(text, ..., time_step, T_max):
11
- -----------------------------------------------
12
- Tokenização BBPE: ids = BBPE_encode(text, max_len=seq_len) → [seq_len]
13
- Embedding: E = Embedding(ids) → [seq_len, D]
14
- PE sinusoidal: PE = sin/cos(pos · exp(-2k·log(10000)/D))
15
- Fusão: F = E + PE → [seq_len, D]
16
- Centralização: F_c = F - mean(F, dim=0) → [seq_len, D]
17
- SVD: F_c = U · S · V^T (V: [D, D] ortogonal)
18
- Projeção 3D: coords_3d = F_c · V[:3,:]^T → [seq_len, 3]
19
- Centróide: xyz = mean(coords_3d, dim=0) → [3]
20
- Coordenada temporal: w = time_step / T_max ∈ [0, 1] (LINEAR)
21
- Saída: vec_4d = [x, y, z, w] → [4]
22
-
23
- Nota (V6.4): w é LINEAR no tempo (diferente de sigmoid(||xyz||)).
24
- Isto permite que o SOM organize neurônios ao longo do eixo temporal,
25
- capturando a sequência absoluta de amostras — útil para deteção
26
- de drift e consolidação incremental (EWC).
27
-
28
- 2) KohonenSOM4D:
29
- --------------
30
- Grid: W ∈ ℝ^(I×J×K×L×4), default (6,6,6,4) = 864 neurônios
31
- BMU: bmu = argmin_{i,j,k,l} ||W[i,j,k,l] - x||² (Euclidiana ℝ⁴)
32
- Vizinhança (Gaussiana 4D):
33
- Λ(d, σ) = exp(-d² / (2σ²))
34
- d² = Δi² + Δj² + Δk² + Δl² (distância quadrada no grid 4D)
35
- Update (Kohonen):
36
- ΔW = α · Λ · (x - W) (competitivo + cooperativo)
37
- Decaimento:
38
- σ_t = σ₀ · exp(-t/1000) (vizinhança encolhe rápido)
39
- α_t = α₀ · exp(-t/2000) (LR decai mais lento)
40
- EWC (apenas na 4ª dimensão w):
41
- L_ewc = (λ/2) · Σ F_i · (W_w,i - W*_w,i)²
42
- ∂L_ewc/∂W_w = λ · F · (W_w - W*_w)
43
- Aplicado como: update[..., 3] -= λ · F · (W_w - W*_w)
44
- Fisher (aproximação como erro quadrado):
45
- F_i = mean((x_w - W_w,i)²) sobre samples pré-punição
46
- Acumulado apenas quando:
47
- (a) punishment_count == 0
48
- (b) old_weights_w is None (ainda não consolidado)
49
- (c) Λ > 0.1 (neurônios próximos ao BMU)
50
-
51
- 3) HypothesisClassifier:
52
- ----------------------
53
- 8 camadas FC: [in → 512 → 256 → 128 → 64 → 32 → 16 → 8] + ReLU
54
- Output: 8 → 1 (logit)
55
- Loss: BCEWithLogitsLoss
56
- Optimizer: Adam, lr=0.001
57
- Epochs: 50 (sobre o buffer atual)
58
- Input: vetor de ativação SOM = distâncias ao grid flatten
59
- (864-dim para grid (6,6,6,4))
60
-
61
- 4) Punishment Protocol:
62
- ---------------------
63
- Histograma: bucketiza vec_4d[dim_choice] (default 'y', idx=1)
64
- check_training_start:
65
- max(histogram.values()) >= N_start → ready
66
- Avaliação: acc = correct / len(buffer) (correct = pred==label)
67
- Se acc < 1.0:
68
- punishment_count += 1
69
- success_count = 0
70
- Se punishment_count == 1: activate_hypothesis() (treina classifier)
71
- Se punishment_count == 2:
72
- set_ewc_reference() (consolida w via Fisher)
73
- required_new_samples = success_count * N (ou N se success==0)
74
- reset: training_ready=False, punishment=0, success=0,
75
- histogram cleared, buffers cleared
76
- Se acc == 1.0:
77
- punishment_count = 0
78
- success_count += 1
79
-
80
- 5) PGVector NÃO É MAIS NECESSÁRIO (V6.4):
81
- ---------------------------------------
82
- O método find_bmu realiza a busca nearest-neighbor sobre o grid 4D,
83
- substituindo qualquer lookup pgvector externo. O SOM interno já
84
- armazena todo o conhecimento como pesos 4D, e find_bmu retorna o
85
- neurônio mais próximo em O(I·J·K·L) — equivalente a uma busca
86
- pgvector com indexação flat.
87
- Consequentemente, hyp_t.py NÃO consulta mais pgvector — a decisão
88
- de aplicar punição é delegada ao KohonenLearningSystem.
89
-
90
- ============================================================================
91
- BUGS CORRIGIDOS (V6.3 → mantidos em V6.4)
92
- ============================================================================
93
-
94
- BUG 1 (find_bmu): V6.3 corrigiu return prematuro com Ellipsis.
95
- V6.4: código do usuário já está limpo (unravel manual sem return
96
- prematuro). Mantido as-is.
97
-
98
- BUG 2 (activate_hypothesis): backward tentava retropropagar através
99
- do embedding (via buffer_4d). Causava RuntimeError "Trying to
100
- backward through the graph a second time".
101
- FIX V6.4: detach+clone nos tensores de entrada do classifier,
102
- e cálculo do vetor de ativação SOM dentro de torch.no_grad().
103
- O classifier treina apenas sobre seus próprios pesos (8 FC layers).
104
-
105
- ============================================================================
106
- """
107
- from __future__ import annotations
108
-
109
- import os
110
- import math
111
- import torch
112
- import torch.nn as nn
113
- import torch.nn.functional as F
114
- from collections import defaultdict, Counter
115
- from typing import List, Tuple, Optional
116
-
117
-
118
- # ============================================================================
119
- # Configuração da CPU – núcleos físicos
120
- # ============================================================================
121
- try:
122
- import psutil
123
- N_CORES = psutil.cpu_count(logical=False)
124
- except ImportError:
125
- N_CORES = os.cpu_count() // 2 if os.cpu_count() else 4
126
- if N_CORES:
127
- try:
128
- torch.set_num_threads(N_CORES)
129
- torch.set_num_interop_threads(N_CORES)
130
- except RuntimeError:
131
- pass # already initialized
132
-
133
-
134
- # ============================================================================
135
- # Tokenizador BBPE simplificado
136
- # ============================================================================
137
- class SimpleBBPETokenizer:
138
- """Tokenizador BPE simplificado (nível de palavra).
139
-
140
- Vocabulário especial: pad=0, eos=1, unk=2.
141
- Tokens comuns mapeados a partir do índice 3.
142
- """
143
-
144
- def __init__(self, vocab_size: int = 16384):
145
- self.vocab_size = vocab_size
146
- self.token_to_id = {}
147
- self.id_to_token = {}
148
- self.pad_token = "<pad>"
149
- self.eos_token = "<eos>"
150
- self.unk_token = "<unk>"
151
- self.pad_id = 0
152
- self.eos_id = 1
153
- self.unk_id = 2
154
- self._init_special_tokens()
155
-
156
- def _init_special_tokens(self):
157
- self.token_to_id[self.pad_token] = self.pad_id
158
- self.token_to_id[self.eos_token] = self.eos_id
159
- self.token_to_id[self.unk_token] = self.unk_id
160
- self.id_to_token[self.pad_id] = self.pad_token
161
- self.id_to_token[self.eos_id] = self.eos_token
162
- self.id_to_token[self.unk_id] = self.unk_token
163
-
164
- def fit(self, texts: List[str]):
165
- """Constrói vocabulário por frequência (top vocab_size-3 palavras)."""
166
- word_counts = Counter()
167
- for text in texts:
168
- words = text.split()
169
- word_counts.update(words)
170
- sorted_words = [w for w, _ in word_counts.most_common(self.vocab_size - 3)]
171
- for idx, word in enumerate(sorted_words, start=3):
172
- self.token_to_id[word] = idx
173
- self.id_to_token[idx] = word
174
-
175
- def encode(self, text: str, max_length: int = 8) -> List[int]:
176
- """Encoda + append EOS + pad/trunca para max_length."""
177
- words = text.split()
178
- ids = [self.token_to_id.get(w, self.unk_id) for w in words]
179
- ids.append(self.eos_id)
180
- if len(ids) > max_length:
181
- ids = ids[:max_length]
182
- else:
183
- ids += [self.pad_id] * (max_length - len(ids))
184
- return ids
185
-
186
-
187
- # ============================================================================
188
- # Codificação posicional e conversão texto → vetor 4D (com w temporal)
189
- # ============================================================================
190
- def positional_encoding(seq_len: int, hidden_dim: int) -> torch.Tensor:
191
- """PE sinusoidal clássico: PE[pos, 2k] = sin(pos·exp(-2k·log(10000)/D)),
192
- PE[pos, 2k+1] = cos(pos·exp(-2k·log(10000)/D)).
193
- """
194
- pe = torch.zeros(seq_len, hidden_dim)
195
- position = torch.arange(0, seq_len, dtype=torch.float).unsqueeze(1)
196
- div_term = torch.exp(
197
- torch.arange(0, hidden_dim, 2).float() * (-math.log(10000.0) / hidden_dim)
198
- )
199
- pe[:, 0::2] = torch.sin(position * div_term)
200
- pe[:, 1::2] = torch.cos(position * div_term)
201
- return pe
202
-
203
-
204
- def text_to_4d_vector(
205
- text: str,
206
- tokenizer: SimpleBBPETokenizer,
207
- embedding: nn.Embedding,
208
- hidden_dim: int,
209
- seq_len: int,
210
- time_step: int,
211
- T_max: int = 10000,
212
- ) -> torch.Tensor:
213
- """Converte sentença em vetor 4D (x, y, z, w), onde w = time_step / T_max.
214
-
215
- Pipeline matemático:
216
- ids → Embedding → +PE → SVD(proj top-3) → centróide xyz
217
- w = time_step / T_max (LINEAR no tempo)
218
- vec_4d = concat(xyz, w) → [4]
219
- """
220
- ids = tokenizer.encode(text, max_length=seq_len)
221
- input_ids = torch.tensor(ids).unsqueeze(0)
222
- word_emb = embedding(input_ids).squeeze(0) # (L, D)
223
- pe = positional_encoding(seq_len, hidden_dim)
224
- fused = word_emb + pe # (L, D)
225
-
226
- # SVD para 3D
227
- mean_centered = fused - fused.mean(dim=0, keepdim=True)
228
- U, S, V = torch.linalg.svd(mean_centered, full_matrices=False)
229
- coords_3d = torch.mm(mean_centered, V[:3, :].t()) # (L, 3)
230
-
231
- xyz_mean = coords_3d.mean(dim=0) # (3,)
232
- w = torch.tensor(time_step / T_max, dtype=torch.float) # valor temporal
233
- vec_4d = torch.cat([xyz_mean, w.unsqueeze(0)]).contiguous()
234
- return vec_4d
235
-
236
-
237
- # ============================================================================
238
- # Mapa de Kohonen 4D com EWC (Fisher para w não-nulo)
239
- # ============================================================================
240
- class KohonenSOM4D:
241
- """Mapa Auto-Organizável 4D com EWC apenas na 4ª dimensão (w temporal).
242
-
243
- Args:
244
- grid_shape: (I, J, K, L) — dimensões do grid 4D.
245
- alpha0: taxa de aprendizado inicial (α₀).
246
- sigma0: largura inicial da vizinhança (σ₀).
247
- lambda_ewc: peso da penalidade EWC (λ).
248
-
249
- Atributos:
250
- weights: W ∈ ℝ^(I×J×K×L×4) — pesos dos neurônios.
251
- old_weights_w: W*_w ∈ ℝ^(I×J×K×L) — referência EWC (apenas w).
252
- fisher_w: F ∈ ℝ^(I×J×K×L) — informação de Fisher por neurônio (w).
253
- fisher_accum / fisher_count: acumuladores para cálculo de F.
254
- """
255
-
256
- def __init__(
257
- self,
258
- grid_shape: Tuple[int, int, int, int],
259
- alpha0: float = 0.1,
260
- sigma0: float = 1.0,
261
- lambda_ewc: float = 0.01,
262
- ):
263
- self.I, self.J, self.K, self.L = grid_shape
264
- self.alpha0 = alpha0
265
- self.sigma0 = sigma0
266
- self.lambda_ewc = lambda_ewc
267
- self.t = 0
268
-
269
- self.weights = torch.randn(self.I, self.J, self.K, self.L, 4)
270
- self.old_weights_w = None
271
- self.fisher_w = None
272
- self.fisher_accum = torch.zeros(self.I, self.J, self.K, self.L)
273
- self.fisher_count = torch.zeros(self.I, self.J, self.K, self.L)
274
-
275
- def _neighborhood(self, bmu_idx):
276
- """Vizinhança Gaussiana 4D: d² = Δi² + Δj² + Δk² + Δl²."""
277
- i, j, k, l = bmu_idx
278
- II, JJ, KK, LL = torch.meshgrid(
279
- torch.arange(self.I).float(),
280
- torch.arange(self.J).float(),
281
- torch.arange(self.K).float(),
282
- torch.arange(self.L).float(),
283
- indexing="ij",
284
- )
285
- dist_sq = (II - i) ** 2 + (JJ - j) ** 2 + (KK - k) ** 2 + (LL - l) ** 2
286
- return dist_sq
287
-
288
- def find_bmu(self, x: torch.Tensor) -> Tuple[int, int, int, int]:
289
- """Best Matching Unit: argmin ||W - x||² em ℝ⁴.
290
-
291
- Substitui pgvector_lookup — busca nearest-neighbor flat sobre o grid.
292
- """
293
- dist = torch.sum((self.weights - x.view(1, 1, 1, 1, 4)) ** 2, dim=-1)
294
- flat_idx = torch.argmin(dist).item()
295
- i = flat_idx // (self.J * self.K * self.L)
296
- rest = flat_idx % (self.J * self.K * self.L)
297
- j = rest // (self.K * self.L)
298
- rest = rest % (self.K * self.L)
299
- k = rest // self.L
300
- l = rest % self.L
301
- return (i, j, k, l)
302
-
303
- def update_weights(self, x: torch.Tensor, bmu_idx, accumulate_fisher=False):
304
- """Update Kohonen: ΔW = α·Λ·(x - W) + penalidade EWC em w.
305
-
306
- Args:
307
- x: tensor [4] — amostra 4D.
308
- bmu_idx: (i, j, k, l) — índice do BMU.
309
- accumulate_fisher: se True, acumula (x_w - W_w)² nos Fisher accumulators.
310
- """
311
- dist_sq = self._neighborhood(bmu_idx)
312
- sigma = self.sigma0 * math.exp(-self.t / 1000)
313
- alpha = self.alpha0 * math.exp(-self.t / 2000)
314
- h = torch.exp(-dist_sq / (2 * sigma ** 2))
315
-
316
- delta = x - self.weights
317
- update = alpha * h.unsqueeze(-1) * delta
318
-
319
- if self.old_weights_w is not None and self.fisher_w is not None:
320
- # Penalidade EWC apenas na 4ª dimensão (w)
321
- # ∂L_ewc/∂W_w = λ · F · (W_w - W*_w) → subtraído do update
322
- ewc_penalty = self.lambda_ewc * self.fisher_w * (
323
- self.weights[..., 3] - self.old_weights_w
324
- )
325
- update[..., 3] = update[..., 3] - ewc_penalty
326
-
327
- self.weights = self.weights + update
328
-
329
- if accumulate_fisher:
330
- # Acumula Fisher apenas em neurônios próximos ao BMU (Λ > 0.1)
331
- mask = h > 0.1
332
- if mask.any():
333
- diff_sq = (x[3] - self.weights[mask][..., 3]) ** 2
334
- self.fisher_accum[mask] += diff_sq
335
- self.fisher_count[mask] += 1
336
-
337
- self.t += 1
338
-
339
- def finalize_fisher(self):
340
- """Fisher = mean((x_w - W_w)²) sobre samples acumuladas."""
341
- cnt = self.fisher_count.clamp(min=1e-8)
342
- self.fisher_w = self.fisher_accum / cnt
343
-
344
- def set_ewc_reference(self):
345
- """Consolida W_w como referência EWC e finaliza Fisher."""
346
- self.old_weights_w = self.weights[..., 3].clone()
347
- self.finalize_fisher()
348
- self.fisher_accum.zero_()
349
- self.fisher_count.zero_()
350
-
351
- # ------------------------------------------------------------------
352
- # Métricas para monitoramento (V6.4)
353
- # ------------------------------------------------------------------
354
- def get_metrics(self) -> dict:
355
- """Retorna métricas atuais do SOM para monitoramento."""
356
- sigma_t = self.sigma0 * math.exp(-self.t / 1000)
357
- alpha_t = self.alpha0 * math.exp(-self.t / 2000)
358
- return {
359
- "t": int(self.t),
360
- "sigma_t": float(sigma_t),
361
- "alpha_t": float(alpha_t),
362
- "sigma0": float(self.sigma0),
363
- "alpha0": float(self.alpha0),
364
- "lambda_ewc": float(self.lambda_ewc),
365
- "grid_shape": [int(self.I), int(self.J), int(self.K), int(self.L)],
366
- "n_neurons": int(self.I * self.J * self.K * self.L),
367
- "has_ewc_reference": self.old_weights_w is not None,
368
- "fisher_w_mean": (
369
- float(self.fisher_w.mean().item())
370
- if self.fisher_w is not None
371
- else 0.0
372
- ),
373
- "fisher_w_max": (
374
- float(self.fisher_w.max().item())
375
- if self.fisher_w is not None
376
- else 0.0
377
- ),
378
- "fisher_accum_count": int(self.fisher_count.sum().item()),
379
- "weights_norm": float(self.weights.norm().item()),
380
- "weights_w_mean": float(self.weights[..., 3].mean().item()),
381
- }
382
-
383
-
384
- # ============================================================================
385
- # Classificador de hipótese (8 camadas FC)
386
- # ============================================================================
387
- class HypothesisClassifier(nn.Module):
388
- """Classificador de hipótese: 8 camadas FC + ReLU + output logit.
389
-
390
- Arquitetura: [input → 512 → 256 → 128 → 64 → 32 → 16 → 8] + ReLU
391
- + [8 → 1] (logit)
392
- Loss: BCEWithLogitsLoss
393
- """
394
-
395
- def __init__(self, input_dim, hidden_dims=[512, 256, 128, 64, 32, 16, 8]):
396
- super().__init__()
397
- layers = []
398
- prev = input_dim
399
- for h in hidden_dims:
400
- layers.append(nn.Linear(prev, h))
401
- layers.append(nn.ReLU())
402
- prev = h
403
- layers.append(nn.Linear(prev, 1))
404
- self.net = nn.Sequential(*layers)
405
-
406
- def forward(self, x):
407
- return self.net(x).squeeze(-1)
408
-
409
-
410
- # ============================================================================
411
- # Sistema de aprendizado completo (com w temporal e condição de início por N)
412
- # ============================================================================
413
- class KohonenLearningSystem:
414
- """Pipeline integrado: tokenizer + embedding + SOM4D + classifier + punishment.
415
-
416
- Args:
417
- vocab_size: tamanho do vocabulário BBPE (default 16384).
418
- hidden_dim: dimensão do embedding (default 1024).
419
- seq_len: comprimento máximo da sequência (default 8).
420
- som_grid: (I, J, K, L) — grid 4D do SOM (default (6, 6, 6, 4) = 864).
421
- alpha0, sigma0: hiperparâmetros do SOM.
422
- lambda_ewc: peso da penalidade EWC.
423
- N_start: threshold do histograma para iniciar treino.
424
- dim_choice: 'x' | 'y' | 'z' — dimensão usada no histograma.
425
- hypothesis_hidden: arquitetura do HypothesisClassifier.
426
- T_max: normalização temporal (w = time_step / T_max).
427
- """
428
-
429
- def __init__(
430
- self,
431
- vocab_size=16384,
432
- hidden_dim=1024,
433
- seq_len=8,
434
- som_grid=(6, 6, 6, 4),
435
- alpha0=0.1,
436
- sigma0=1.5,
437
- lambda_ewc=0.02,
438
- N_start=10,
439
- dim_choice="y",
440
- hypothesis_hidden=[512, 256, 128, 64, 32, 16, 8],
441
- T_max=10000,
442
- ):
443
- self.tokenizer = SimpleBBPETokenizer(vocab_size)
444
- self.embedding = nn.Embedding(vocab_size, hidden_dim)
445
- self.hidden_dim = hidden_dim
446
- self.seq_len = seq_len
447
- self.T_max = T_max
448
- self.time_counter = 0 # contador global de amostras processadas
449
-
450
- self.som = KohonenSOM4D(som_grid, alpha0, sigma0, lambda_ewc)
451
- self.som_grid = som_grid
452
- self.som_neuron_count = (
453
- som_grid[0] * som_grid[1] * som_grid[2] * som_grid[3]
454
- )
455
-
456
- self.classifier: Optional[HypothesisClassifier] = None
457
- self.hypothesis_hidden = hypothesis_hidden
458
- self.classifier_trained = False
459
-
460
- self.buffer_4d = []
461
- self.buffer_labels = []
462
- self.training_ready = False
463
- self.N = N_start
464
- self.dim_choice = dim_choice
465
- self.dim_index = {"x": 0, "y": 1, "z": 2}[dim_choice]
466
-
467
- self.punishment_count = 0
468
- self.success_count = 0
469
- self.histogram = Counter()
470
-
471
- self.required_new_samples = 0
472
-
473
- def add_data(self, sentences: List[str], labels: List[int]):
474
- """Adiciona amostras: text → 4D vector + atualiza histograma."""
475
- for sent, lab in zip(sentences, labels):
476
- self.time_counter += 1
477
- vec = text_to_4d_vector(
478
- sent,
479
- self.tokenizer,
480
- self.embedding,
481
- self.hidden_dim,
482
- self.seq_len,
483
- self.time_counter,
484
- self.T_max,
485
- )
486
- self.buffer_4d.append(vec)
487
- self.buffer_labels.append(lab)
488
- dim_val = round(vec[self.dim_index].item(), 2)
489
- self.histogram[dim_val] += 1
490
-
491
- def check_training_start(self) -> bool:
492
- """Inicia treino quando algum bucket do histograma atinge N."""
493
- if (
494
- not self.training_ready
495
- and max(self.histogram.values(), default=0) >= self.N
496
- ):
497
- self.training_ready = True
498
- return True
499
- return False
500
-
501
- def train_som_on_buffer(self):
502
- """Treina SOM por 5 épocas sobre o buffer atual (com Fisher accum)."""
503
- if not self.buffer_4d:
504
- return
505
- data = torch.stack(self.buffer_4d)
506
- for _ in range(5): # épocas de treino rápido
507
- perm = torch.randperm(len(data))
508
- for idx in perm:
509
- x = data[idx]
510
- bmu = self.som.find_bmu(x)
511
- acc_fisher = (
512
- self.punishment_count == 0
513
- and self.som.old_weights_w is None
514
- )
515
- self.som.update_weights(x, bmu, accumulate_fisher=acc_fisher)
516
-
517
- def _som_activation(self, x):
518
- """Vetor de ativação SOM: distâncias de x a todos os neurônios (flatten)."""
519
- dist = torch.sum((self.som.weights - x.view(1, 1, 1, 1, 4)) ** 2, dim=-1)
520
- return dist.flatten()
521
-
522
- def _label_neurons(self):
523
- """Rotula neurônios por votação majoritária sobre o buffer."""
524
- self.neuron_label = {}
525
- if not self.buffer_4d:
526
- return
527
- data = torch.stack(self.buffer_4d)
528
- labels = torch.tensor(self.buffer_labels)
529
- votes = defaultdict(lambda: [0, 0])
530
- for i in range(len(data)):
531
- bmu = self.som.find_bmu(data[i])
532
- votes[bmu][int(labels[i].item())] += 1
533
- for bmu, v in votes.items():
534
- self.neuron_label[bmu] = 1.0 if v[1] > v[0] else 0.0
535
-
536
- def evaluate_classification(self) -> float:
537
- """Acurácia sobre o buffer atual."""
538
- if not self.buffer_4d:
539
- return 1.0
540
- data = torch.stack(self.buffer_4d)
541
- labels = torch.tensor(self.buffer_labels).float()
542
- correct = 0
543
- for i in range(len(data)):
544
- pred = self._predict_single(data[i])
545
- if (pred > 0.5) == (labels[i] > 0.5):
546
- correct += 1
547
- return correct / len(data)
548
-
549
- def _predict_single(self, x):
550
- """Prediz: classifier (se treinado) ou voto do BMU."""
551
- if self.classifier is not None and self.classifier_trained:
552
- with torch.no_grad():
553
- act = self._som_activation(x).unsqueeze(0)
554
- logit = self.classifier(act)
555
- return torch.sigmoid(logit).item()
556
- else:
557
- if not hasattr(self, "neuron_label"):
558
- self._label_neurons()
559
- bmu = self.som.find_bmu(x)
560
- return self.neuron_label.get(bmu, 0.5)
561
-
562
- def activate_hypothesis(self):
563
- """Treina o HypothesisClassifier (8 FC layers) por 50 epochs.
564
-
565
- BUG FIX (V6.3→V6.4): buffer_4d contém tensores que carregam o grafo
566
- de computação do embedding. Para evitar "Trying to backward through
567
- the graph a second time", fazemos detach+clone e calculamos as
568
- ativações SOM dentro de torch.no_grad(). O classifier treina apenas
569
- sobre seus próprios pesos.
570
- """
571
- if self.classifier is None:
572
- self.classifier = HypothesisClassifier(
573
- self.som_neuron_count, self.hypothesis_hidden
574
- )
575
- # FIX: detach+clone para isolar do grafo do embedding
576
- data = torch.stack(self.buffer_4d).detach().clone()
577
- labels = torch.tensor(self.buffer_labels).float()
578
- # FIX: ativações SEM gradiente (não queremos treinar SOM/embedding aqui)
579
- with torch.no_grad():
580
- X = torch.stack([self._som_activation(data[i]) for i in range(len(data))])
581
- optimizer = torch.optim.Adam(self.classifier.parameters(), lr=0.001)
582
- criterion = nn.BCEWithLogitsLoss()
583
- for _ in range(50):
584
- optimizer.zero_grad()
585
- loss = criterion(self.classifier(X), labels)
586
- loss.backward()
587
- optimizer.step()
588
- self.classifier_trained = True
589
-
590
- def process_batch(self, sentences: List[str], labels: List[int]):
591
- """Processa batch: adiciona dados, treina SOM se ready, aplica punishment.
592
-
593
- Returns:
594
- True se o protocolo de punishment completou um ciclo (2ª punição
595
- → set_ewc_reference + reset). False caso contrário.
596
- """
597
- self.add_data(sentences, labels)
598
-
599
- if self.check_training_start():
600
- self.train_som_on_buffer()
601
- self._label_neurons()
602
-
603
- if self.training_ready:
604
- acc = self.evaluate_classification()
605
- if acc < 1.0:
606
- self.punishment_count += 1
607
- self.success_count = 0
608
- if self.punishment_count == 1:
609
- self.activate_hypothesis()
610
- elif self.punishment_count == 2:
611
- self.som.set_ewc_reference()
612
- self.required_new_samples = (
613
- self.success_count * self.N
614
- if self.success_count > 0
615
- else self.N
616
- )
617
- self.training_ready = False
618
- self.punishment_count = 0
619
- self.success_count = 0
620
- self.histogram.clear()
621
- self.buffer_4d.clear()
622
- self.buffer_labels.clear()
623
- return True
624
- else:
625
- self.punishment_count = 0
626
- self.success_count += 1
627
- return False
628
-
629
- def predict(self, sentence: str) -> str:
630
- """Prediz rótulo textual ("gato" se prob ≤ 0.5, "cachorro" caso contrário)."""
631
- self.time_counter += 1 # mantém coerência temporal
632
- vec = text_to_4d_vector(
633
- sentence,
634
- self.tokenizer,
635
- self.embedding,
636
- self.hidden_dim,
637
- self.seq_len,
638
- self.time_counter,
639
- self.T_max,
640
- )
641
- prob = self._predict_single(vec)
642
- return "gato" if prob <= 0.5 else "cachorro"
643
-
644
- # ------------------------------------------------------------------
645
- # API de monitoramento (V6.4)
646
- # ------------------------------------------------------------------
647
- def get_state_metrics(self) -> dict:
648
- """Retorna métricas completas do sistema para monitoramento."""
649
- som_metrics = self.som.get_metrics()
650
- return {
651
- "som": som_metrics,
652
- "kls": {
653
- "time_counter": int(self.time_counter),
654
- "T_max": int(self.T_max),
655
- "buffer_size": int(len(self.buffer_4d)),
656
- "training_ready": bool(self.training_ready),
657
- "punishment_count": int(self.punishment_count),
658
- "success_count": int(self.success_count),
659
- "classifier_trained": bool(self.classifier_trained),
660
- "histogram_size": int(len(self.histogram)),
661
- "histogram_max": int(max(self.histogram.values(), default=0)),
662
- "N_start": int(self.N),
663
- "dim_choice": str(self.dim_choice),
664
- "som_neuron_count": int(self.som_neuron_count),
665
- "required_new_samples": int(self.required_new_samples),
666
- "has_classifier": self.classifier is not None,
667
- "vocab_size": int(self.tokenizer.vocab_size),
668
- "hidden_dim": int(self.hidden_dim),
669
- "seq_len": int(self.seq_len),
670
- },
671
- }