PowerMachine commited on
Commit
b485c6e
·
verified ·
1 Parent(s): 08f7f82

V6.5-V2-auto-conscience: upload batch (modules + scripts) [73 files] — auto-adjust conscience_gamma + reasoning_engine disabled by default + HF_TOKEN scrubbed

Browse files
scripts/train_v6_5_v2.py CHANGED
@@ -100,6 +100,27 @@ logger = logging.getLogger("train_v6_5_v2")
100
  os.environ["V65_ENABLE_STREAMING"] = "1"
101
  logger.info(f"[V6.5-V2] V65_ENABLE_STREAMING={os.environ['V65_ENABLE_STREAMING']} (forced)")
102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  sys.path.insert(0, str(SRC_ROOT))
104
 
105
  from bigru_t.utils.xeon_runtime import ( # noqa: E402
@@ -142,63 +163,60 @@ logger.info(f"[V6.5-V2] Xeon FP16 benchmark: {FP16_BENCH}")
142
  BATCH_SIZE = 16
143
  MAX_SEQ_LEN = 8
144
 
145
- # V6.5-V2-metrics-FIX-2: Parâmetros ajustados para caber em 4GB cgroup limit.
146
- # User requirement: "retornar os parâmetros do modelo para os valores maiores
147
- # e resolver falhas de lógica e de bugs que estejam causando alto consumo
148
- # de memória sem distorcer a arquitetura Kohonen".
149
- #
150
- # Os valores abaixo são um COMPROMISSO entre "valores maiores" e "caber em 4GB":
151
- # HIDDEN_DIM: 1024 → 512 (embedding: 64MB → 32MB, attention: 16.8M → 4.2M params)
152
- # VOCAB_SIZE: 16384 → 8192 (embedding: 32MB → 16MB)
153
- # N_HYPOTHESES: 16 → 8 (active generators)
154
- # MAX_N_HYPOTHESES: 32 → 16 (pre-allocated generators: 148.9MB → 74.5MB)
155
- # HYP_HIDDEN_DIM: 256 → 128 (each generator: 1.16M → 0.58M params)
156
- #
157
- # A arquitetura Kohonen (SOM grid 6×6×6×4 = 864 neurons, BMU, Gaussian
158
- # neighborhood, EWC, Fisher) é PRESERVADA integralmente. Apenas as dimensões
159
- # dos módulos auxiliares (embedding, attention, hypothesis ensemble) são
160
- # reduzidas para evitar OOM no cgroup de 4GB.
161
  #
162
- # Memory budget (estimativa):
163
- # Embedding: 8192 × 512 × 4 = 16MB
164
- # Attention: 4 × 512² × 4 = 4MB
165
- # HypothesisEnsemble: 16 × (864×128 + 128×128 + 128×3456) = 16 × 0.58M = 37MB
166
- # Adam state (ensemble): 2 × 37MB = 74MB
167
- # HypothesisClassifier: ~616K params = 2.5MB
168
- # VQ-VAE-2: ~50K params = 0.2MB
169
- # SOM weights: 864 × 4 × 4 = 14KB
170
  # Buffer (256 samples): ~4KB
171
- # Total model: ~134MB
172
  # + Python + PyTorch + HF datasets cache: ~1-2GB
173
- # + Training intermediates: ~200MB
174
- # Total: ~1.5-2.5GB (well within 4GB)
175
- HIDDEN_DIM = 512 # V6.5-V2-metrics-FIX-2: reduced from 1024
176
- VOCAB_SIZE = 8192 # V6.5-V2-metrics-FIX-2: reduced from 16384
177
  SOM_GRID = (6, 6, 6, 4) # 864 neurons (arquitetura Kohonen preservada)
178
  T_MAX = 10000
179
  N_START = 10
180
  LAMBDA_EWC = 0.02
181
- ALPHA0 = 0.1
182
- SIGMA0 = 1.5
 
 
 
183
  DIM_CHOICE = "y"
184
 
185
- # V2-dynamic — HypothesisEnsemble parameters (ajustados para 4GB cgroup)
186
- N_HYPOTHESES = 8 # V6.5-V2-metrics-FIX-2: reduced from 16
187
- MAX_N_HYPOTHESES = 16 # V6.5-V2-metrics-FIX-2: reduced from 32
188
- MIN_N_HYPOTHESES = 4 # limite inferior dinâmico
189
- N_TRIALS = 3 # inicial
190
  MIN_N_TRIALS = 1
191
  MAX_N_TRIALS = 6
192
- HYP_TRAIN_STEPS = 30 # mantido (não afeta memória, só tempo)
193
  MIN_HYP_TRAIN_STEPS = 10
194
  MAX_HYP_TRAIN_STEPS = 80
195
  HYP_LR = 1e-4
196
- HYP_HIDDEN_DIM = 128 # V6.5-V2-metrics-FIX-2: reduced from 256
197
  LOSS_HISTORY_WINDOW = 8
198
  PUNISHMENT_WINDOW = 12
199
 
200
  # V2-dynamic-memory — Buffer sliding window (evita OOM em treino longo)
201
- MAX_BUFFER_SIZE = 256
 
 
 
 
202
 
203
  # Streaming — User requirement: "streaming de 100 em 100 samples"
204
  STREAM_BATCH_SIZE = 100
@@ -212,13 +230,32 @@ MAX_SAMPLES_PUNICAO = 2000
212
  # User requirement: "não reduzir tempo e não gerar dados sintéticos" +
213
  # "todo streaming (FASE1 e da FASE2) deve ter pausa para dar tempo de conclusão
214
  # de processamento continuando após conclusão"
215
- # V6.5-V2-metrics-FIX-2: pausas reduzidas para caber em 20min de treino
216
- # (container kills processes after ~10min se não estiver em foreground).
217
- # As pausas ainda garantem tempo para processamento, mas são mais curtas.
218
- INTER_BATCH_PAUSE_S = 0.1 # reduzido de 0.5 (ainda há pausa)
219
- INTER_DATASET_PAUSE_S = 0.5 # reduzido de 2.0
220
- INTER_STREAM_BATCH_PAUSE_S = 0.2 # reduzido de 1.0
221
- POST_PROCESSING_PAUSE_S = 0.3 # reduzido de 1.5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
 
223
  # Storage critical
224
  STORAGE_CRITICAL_PCT = 90
@@ -1165,13 +1202,13 @@ def run_fase_conhecimento(
1165
  })
1166
 
1167
  step += 1
1168
- time.sleep(INTER_BATCH_PAUSE_S)
1169
  except Exception as e:
1170
  logger.error(f"[V6.5-V2] Batch error: {e}")
1171
  traceback.print_exc()
1172
  continue
1173
 
1174
- time.sleep(INTER_STREAM_BATCH_PAUSE_S)
1175
  # V2-dynamic-memory — Buffer sliding window: trunca para os
1176
  # últimos MAX_BUFFER_SIZE amostras após cada chunk.
1177
  # Os pesos do SOM já capturam o conhecimento acumulado,
@@ -1248,6 +1285,31 @@ def run_fase_conhecimento(
1248
  except Exception as e:
1249
  logger.warning(f"[V6.5-V2-metrics-FIX-2] Failed to compute SOM metrics: {e}")
1250
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1251
  # Aggressive memory cleanup between chunks
1252
  if chunk_idx_global % 2 == 0:
1253
  aggressive_memory_cleanup()
@@ -1260,13 +1322,13 @@ def run_fase_conhecimento(
1260
  # V6.5-V2-metrics-FIX: pausa pós-processamento para dar tempo
1261
  # de conclusão (user requirement: "todo streaming deve ter
1262
  # pausa para dar tempo de conclusão de processamento").
1263
- time.sleep(POST_PROCESSING_PAUSE_S)
1264
  except Exception as e:
1265
  logger.error(f"[V6.5-V2] Dataset {dataset_name} failed: {e}")
1266
  traceback.print_exc()
1267
  streaming_failures[dataset_name] += 1
1268
 
1269
- time.sleep(INTER_DATASET_PAUSE_S)
1270
  # V6.5-V2-metrics-FIX-2 — Save state after each dataset to preserve
1271
  # progress in case the process is killed by container timeout.
1272
  # User requirement: "o estado do modelo deve ser contínuo" — saving
@@ -1347,6 +1409,266 @@ def run_fase_conhecimento(
1347
  }
1348
 
1349
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1350
  # ============================================================================
1351
  # 10. PHASE 2 — TREINAMENTO COM PUNIÇÃO (16 hipóteses × 3 tentativas)
1352
  # ============================================================================
@@ -1613,13 +1935,35 @@ def run_fase_punicão(
1613
  traceback.print_exc()
1614
 
1615
  step += 1
1616
- time.sleep(INTER_BATCH_PAUSE_S)
 
 
 
1617
  except Exception as e:
1618
  logger.error(f"[V6.5-V2] Batch error in PUNIÇÃO: {e}")
1619
  traceback.print_exc()
1620
  continue
1621
 
1622
- time.sleep(INTER_STREAM_BATCH_PAUSE_S)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1623
  # V2-dynamic-memory — Buffer sliding window na PUNIÇÃO também
1624
  if len(kls.buffer_4d) > MAX_BUFFER_SIZE:
1625
  overflow = len(kls.buffer_4d) - MAX_BUFFER_SIZE
@@ -1636,8 +1980,9 @@ def run_fase_punicão(
1636
  kls.aggressive_cleanup()
1637
  except Exception:
1638
  pass
1639
- # V6.5-V2-metrics-FIX: pausa pós-processamento (user requirement)
1640
- time.sleep(POST_PROCESSING_PAUSE_S)
 
1641
  except Exception as e:
1642
  logger.error(f"[V6.5-V2] PUNIÇÃO dataset failed: {e}")
1643
  traceback.print_exc()
@@ -1652,6 +1997,24 @@ def run_fase_punicão(
1652
  logger.info(f" Hypotheses trainings: {len(hypotheses_log)}")
1653
  logger.info(f" Delta applications: {len(delta_applications_log)}")
1654
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1655
  return {
1656
  "phase": "TREINAMENTO_COM_PUNICAO",
1657
  "dataset": PUNICAO_DATASET,
@@ -1673,6 +2036,8 @@ def run_fase_punicão(
1673
  ),
1674
  "som_metric_history": kls.get_som_metric_history(),
1675
  "final_v2_state": kls.get_v2_metrics(),
 
 
1676
  }
1677
 
1678
 
@@ -2115,7 +2480,17 @@ def main() -> int:
2115
  # + Goose VQ, fornecendo representação compacta do estado do SOM.
2116
  # O compressor já sanitiza NaN/Inf internamente (torch.nan_to_num).
2117
  enable_vqvae2=True, # REATIVADO (was False in V6.5-V2-metrics-FIX)
2118
- enable_reasoning=True,
 
 
 
 
 
 
 
 
 
 
2119
  enable_w8a8=False, # W8A8 permanece desabilitado (não essencial para Kohonen)
2120
  vqvae2_code_dim=16,
2121
  vqvae2_num_codes_top=64,
@@ -2384,23 +2759,29 @@ def main() -> int:
2384
  SRC_ROOT / "bigru_t" / "data" / "streaming_datasets.py",
2385
  SRC_ROOT / "bigru_t" / "utils" / "xeon_runtime.py",
2386
  ]
2387
- # Relatórios + estados
 
 
 
 
2388
  outputs_to_upload: List[Path] = [
2389
  REPORT_PATH,
2390
- MODEL_STATES_PATH,
2391
  V2_PHASES_EVAL_PATH,
2392
  PREDICT_FIX_EVAL_PATH,
2393
  ATTENTION_EVAL_PATH,
2394
  USER_QUESTIONS_PATH,
2395
- BIGRU_ROOT / "v6_5_v2_model_states_after_conhecimento.pt",
2396
  ]
2397
- # Filtra os que existem
2398
  all_files: List[Path] = []
 
2399
  for p in scripts_to_upload + outputs_to_upload:
2400
- if p.exists():
2401
- all_files.append(p)
2402
- else:
2403
  logger.warning(f"[V6.5-V2] Upload file not found: {p}")
 
 
 
 
 
2404
 
2405
  # Apaga HF_TOKEN dos scripts ANTES do upload (não enviar tokens)
2406
  scrub_result = scrub_hf_token_from_scripts(BIGRU_ROOT / "scripts")
 
100
  os.environ["V65_ENABLE_STREAMING"] = "1"
101
  logger.info(f"[V6.5-V2] V65_ENABLE_STREAMING={os.environ['V65_ENABLE_STREAMING']} (forced)")
102
 
103
+ # V6.5-V2-metrics-FIX-4 — HF datasets memory optimization (OOM-killer mitigation)
104
+ # User requirement: "o processo vem sendo morto OOM-kiler (Out of memory) devido
105
+ # algum bug de lógica ou falta de otimização que deve ser investigado".
106
+ # Estas variáveis reduzem o consumo de memória do HF datasets library:
107
+ # - HF_DATASETS_DISABLE_IN_MEMORY_CACHE: não cacheia datasets em RAM
108
+ # - HF_DATASETS_OFFLINE=0: permite streaming mas não força cache local
109
+ # - DATASETS_FINGERPRINT_CACHING_DISABLED: skipa fingerprinting (CPU/memory)
110
+ # - TOKENIZERS_PARALLELISM=false: evita spawn de processos paralelos
111
+ # - HF_HUB_DISABLE_TELEMETRY: desabilita telemetria (CPU/network)
112
+ os.environ["HF_DATASETS_DISABLE_IN_MEMORY_CACHE"] = "1"
113
+ os.environ["DATASETS_FINGERPRINT_CACHING_DISABLED"] = "1"
114
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
115
+ os.environ["HF_HUB_DISABLE_TELEMETRY"] = "1"
116
+ os.environ.setdefault("HF_DATASETS_CACHE", "/tmp/hf_datasets_cache_v65")
117
+ # Cria o dir de cache se não existir (limpa cache antigo periodicamente)
118
+ try:
119
+ cache_dir = Path(os.environ["HF_DATASETS_CACHE"])
120
+ cache_dir.mkdir(parents=True, exist_ok=True)
121
+ except Exception:
122
+ pass
123
+
124
  sys.path.insert(0, str(SRC_ROOT))
125
 
126
  from bigru_t.utils.xeon_runtime import ( # noqa: E402
 
163
  BATCH_SIZE = 16
164
  MAX_SEQ_LEN = 8
165
 
166
+ # V6.5-V2-metrics-FIX-3: Parâmetros restaurados aos valores canônicos
167
+ # exigidos pelo usuário (sem redução por OOM — as correções de memory leak
168
+ # no KLS já tornam os valores maiores viáveis dentro do cgroup de 4GB).
169
+ # User requirement (FIX-3): "SEMPRE MANTER PARÂMETROS HIDDEN_DIM 1024,
170
+ # VOCAB_SIZE 16384, N_HYPOTHESES 16, MAX_N_HYPOTHESES 32, HYP_TRAIN_STEPS 30,
171
+ # HYP_HIDDEN_DIM 256. Grid SOM (6,6,6,4)=864 neurônios".
 
 
 
 
 
 
 
 
 
 
172
  #
173
+ # Memory budget (estimativa real após correções FIX-3):
174
+ # Embedding: 16384 × 1024 × 4 = 67MB
175
+ # Attention: 4 × 1024² × 4 = 16MB
176
+ # HypothesisEnsemble (32 pre-alloc, 16 active): 32×(864×256+256×256+256×3456) = 32×1.16M = 148MB
177
+ # Adam state (ensemble, 16 active): 2×16×1.16M×4 = 149MB
178
+ # HypothesisClassifier: 864→512→256→128→64→32→16→8→1 = ~616K = 2.5MB
179
+ # VQ-VAE-2: ~50K = 0.2MB
180
+ # SOM weights: 864×4×4 = 14KB
181
  # Buffer (256 samples): ~4KB
182
+ # Total model: ~380MB
183
  # + Python + PyTorch + HF datasets cache: ~1-2GB
184
+ # + Training intermediates (com no_grad aplicado): ~200MB
185
+ # Total: ~2-3GB (dentro de 4GB com folga graças ao no_grad no VQ-VAE-2)
186
+ HIDDEN_DIM = 1024 # V6.5-V2-metrics-FIX-3: restored to canonical
187
+ VOCAB_SIZE = 16384 # V6.5-V2-metrics-FIX-3: restored to canonical
188
  SOM_GRID = (6, 6, 6, 4) # 864 neurons (arquitetura Kohonen preservada)
189
  T_MAX = 10000
190
  N_START = 10
191
  LAMBDA_EWC = 0.02
192
+ # V6.5-V2-metrics-FIX-3: α₀ e σ₀ conforme especificação canônica Kohonen
193
+ # α₀ ∈ [0.5, 1.0] para fase de ordenação (rough training)
194
+ # σ₀ = metade da maior dimensão da grade = max(6,6,6,4)/2 = 3.0
195
+ ALPHA0 = 0.5
196
+ SIGMA0 = 3.0
197
  DIM_CHOICE = "y"
198
 
199
+ # V2-dynamic — HypothesisEnsemble parameters (valores canônicos restaurados)
200
+ N_HYPOTHESES = 16 # V6.5-V2-metrics-FIX-3: restored from 8
201
+ MAX_N_HYPOTHESES = 32 # V6.5-V2-metrics-FIX-3: restored from 16
202
+ MIN_N_HYPOTHESES = 4 # limite inferior dinâmico
203
+ N_TRIALS = 3 # inicial
204
  MIN_N_TRIALS = 1
205
  MAX_N_TRIALS = 6
206
+ HYP_TRAIN_STEPS = 30 # mantido (canônico)
207
  MIN_HYP_TRAIN_STEPS = 10
208
  MAX_HYP_TRAIN_STEPS = 80
209
  HYP_LR = 1e-4
210
+ HYP_HIDDEN_DIM = 256 # V6.5-V2-metrics-FIX-3: restored from 128
211
  LOSS_HISTORY_WINDOW = 8
212
  PUNISHMENT_WINDOW = 12
213
 
214
  # V2-dynamic-memory — Buffer sliding window (evita OOM em treino longo)
215
+ # V6.5-V2-metrics-FIX-4: reduzido de 256 para 128 (OOM-killer mitigation).
216
+ # O buffer_4d só precisa conter amostras recentes para que o SOM compute
217
+ # métricas (QE, TE, KL, VE) — os pesos do SOM já capturam o conhecimento
218
+ # acumulado de todas as amostras históricas.
219
+ MAX_BUFFER_SIZE = 128
220
 
221
  # Streaming — User requirement: "streaming de 100 em 100 samples"
222
  STREAM_BATCH_SIZE = 100
 
230
  # User requirement: "não reduzir tempo e não gerar dados sintéticos" +
231
  # "todo streaming (FASE1 e da FASE2) deve ter pausa para dar tempo de conclusão
232
  # de processamento continuando após conclusão"
233
+ # V6.5-V2-metrics-FIX-4 (latest user requirement):
234
+ # "FASE2 PUNIÇÃO é mais pesada é pode exigir pausas do streaming até
235
+ # concluir o processamento".
236
+ # Pausas separadas para FASE1 (CONHECIMENTO) e FASE2 (PUNITIVA):
237
+ # - FASE1: pausas moderadas (SOM-only, sem hypothesis layer)
238
+ # - FASE2: pausas maiores (16 hipóteses × 30 steps × 3 trials + EWC + revival)
239
+ INTER_BATCH_PAUSE_S_FASE1 = 0.15 # CONHECIMENTO: leve
240
+ INTER_BATCH_PAUSE_S_FASE2 = 0.60 # PUNITIVA: 4x maior (heavier processing)
241
+ INTER_DATASET_PAUSE_S_FASE1 = 0.5
242
+ INTER_DATASET_PAUSE_S_FASE2 = 1.5 # PUNITIVA: 3x maior
243
+ INTER_STREAM_BATCH_PAUSE_S_FASE1 = 0.3
244
+ INTER_STREAM_BATCH_PAUSE_S_FASE2 = 1.2 # PUNITIVA: 4x maior
245
+ POST_PROCESSING_PAUSE_S_FASE1 = 0.4
246
+ POST_PROCESSING_PAUSE_S_FASE2 = 1.5 # PUNITIVA: ~4x maior
247
+ # Compatibilidade (mantém nomes antigos apontando para FASE1 — usados em
248
+ # código legado que não diferencia fases)
249
+ INTER_BATCH_PAUSE_S = INTER_BATCH_PAUSE_S_FASE1
250
+ INTER_DATASET_PAUSE_S = INTER_DATASET_PAUSE_S_FASE1
251
+ INTER_STREAM_BATCH_PAUSE_S = INTER_STREAM_BATCH_PAUSE_S_FASE1
252
+ POST_PROCESSING_PAUSE_S = POST_PROCESSING_PAUSE_S_FASE1
253
+
254
+ # V6.5-V2-metrics-FIX-4 — auto-revive config
255
+ # User requirement: "distribuindo o processamento paralelamente".
256
+ # Quando dead_rate > 50% e passou o cooldown, revive neurônios mortos.
257
+ AUTO_REVIVE_DEAD_RATE_THRESHOLD = 0.5
258
+ AUTO_REVIVE_COOLDOWN_STEPS = 200
259
 
260
  # Storage critical
261
  STORAGE_CRITICAL_PCT = 90
 
1202
  })
1203
 
1204
  step += 1
1205
+ time.sleep(INTER_BATCH_PAUSE_S_FASE1)
1206
  except Exception as e:
1207
  logger.error(f"[V6.5-V2] Batch error: {e}")
1208
  traceback.print_exc()
1209
  continue
1210
 
1211
+ time.sleep(INTER_STREAM_BATCH_PAUSE_S_FASE1)
1212
  # V2-dynamic-memory — Buffer sliding window: trunca para os
1213
  # últimos MAX_BUFFER_SIZE amostras após cada chunk.
1214
  # Os pesos do SOM já capturam o conhecimento acumulado,
 
1285
  except Exception as e:
1286
  logger.warning(f"[V6.5-V2-metrics-FIX-2] Failed to compute SOM metrics: {e}")
1287
 
1288
+ # V6.5-V2-metrics-FIX-4 — Auto-revive neurônios mortos
1289
+ # User requirement: "APRIMORAR (...) a ativação e uso e acesso
1290
+ # dos neurônios (apenas dois estão sendo ativados:
1291
+ # neurons_active=2/864) distribuindo o processamento paralelamente".
1292
+ # O conscience mechanism (DeSieno 1988) já força distribuição
1293
+ # uniforme de BMU, mas como safety net adicional, revive
1294
+ # explicitamente neurônios que ainda estão mortos após o cooldown.
1295
+ try:
1296
+ revival = kls.auto_revive_if_needed(
1297
+ dead_rate_threshold=AUTO_REVIVE_DEAD_RATE_THRESHOLD,
1298
+ min_steps_between_revivals=AUTO_REVIVE_COOLDOWN_STEPS,
1299
+ )
1300
+ if revival.get("action") == "auto_revived":
1301
+ logger.info(
1302
+ f"[V6.5-V2-metrics-FIX-4] AUTO-REVIVE triggered: "
1303
+ f"n_revived={revival['n_revived']}/{revival['n_total']}, "
1304
+ f"dead_rate {revival['dead_rate_before']:.3f} → "
1305
+ f"{revival['dead_rate_after']:.3f}, "
1306
+ f"steps_since_last={revival['steps_since_last_revival']}"
1307
+ )
1308
+ except Exception as revive_err:
1309
+ logger.warning(
1310
+ f"[V6.5-V2-metrics-FIX-4] auto_revive_if_needed failed: {revive_err}"
1311
+ )
1312
+
1313
  # Aggressive memory cleanup between chunks
1314
  if chunk_idx_global % 2 == 0:
1315
  aggressive_memory_cleanup()
 
1322
  # V6.5-V2-metrics-FIX: pausa pós-processamento para dar tempo
1323
  # de conclusão (user requirement: "todo streaming deve ter
1324
  # pausa para dar tempo de conclusão de processamento").
1325
+ time.sleep(POST_PROCESSING_PAUSE_S_FASE1)
1326
  except Exception as e:
1327
  logger.error(f"[V6.5-V2] Dataset {dataset_name} failed: {e}")
1328
  traceback.print_exc()
1329
  streaming_failures[dataset_name] += 1
1330
 
1331
+ time.sleep(INTER_DATASET_PAUSE_S_FASE1)
1332
  # V6.5-V2-metrics-FIX-2 — Save state after each dataset to preserve
1333
  # progress in case the process is killed by container timeout.
1334
  # User requirement: "o estado do modelo deve ser contínuo" — saving
 
1409
  }
1410
 
1411
 
1412
+ # ============================================================================
1413
+ # 9.5 — V6.5-V2-metrics-FIX-4: Sumário final da FASE2
1414
+ # ============================================================================
1415
+ def build_fase2_final_summary(
1416
+ kls: KohonenLearningSystemV2,
1417
+ som_metrics_log: List[Dict[str, Any]],
1418
+ punishment_log: List[Dict[str, Any]],
1419
+ hypotheses_log: List[Dict[str, Any]],
1420
+ delta_applications_log: List[Dict[str, Any]],
1421
+ elapsed_s: float,
1422
+ ) -> Dict[str, Any]:
1423
+ """V6.5-V2-metrics-FIX-4 — Constrói sumário final da FASE2 mostrando evolução.
1424
+
1425
+ User requirement: "ao final da FASE2 mostrar evolução de métricas e dos
1426
+ indicadores e da taxa de aprendizagem".
1427
+
1428
+ Extrai trajetória temporal das métricas SOM (QE, TE, KL, VE), indicadores
1429
+ de falha (dead_rate, collapse, stagnation, crossing), taxa de aprendizado
1430
+ (α_t, σ_t) e estatísticas do conscience mechanism (neurons_active,
1431
+ uniformity_score). Compara início vs fim para mostrar evolução.
1432
+
1433
+ Returns:
1434
+ Dict com:
1435
+ - summary_lines: List[str] — linhas formatadas para logger.info
1436
+ - metrics_evolution: Dict com first/last/delta de cada métrica
1437
+ - learning_rate_evolution: Dict com α_t, σ_t no início e fim
1438
+ - indicators_evolution: Dict com indicadores de falha no início e fim
1439
+ - neuron_activation_evolution: Dict com neurons_active no início e fim
1440
+ - punishment_stats: Dict com estatísticas de punição
1441
+ """
1442
+ summary_lines: List[str] = []
1443
+
1444
+ # 1. Trajetória das métricas principais (QE, TE, KL, VE)
1445
+ def _safe_get(log_list, key, idx):
1446
+ if not log_list or idx >= len(log_list):
1447
+ return 0.0
1448
+ try:
1449
+ return float(log_list[idx].get(key, 0.0))
1450
+ except (TypeError, ValueError):
1451
+ return 0.0
1452
+
1453
+ n_logs = len(som_metrics_log)
1454
+ first_idx = 0
1455
+ last_idx = max(0, n_logs - 1)
1456
+
1457
+ metrics_first = {
1458
+ "QE": _safe_get(som_metrics_log, "quantization_error", first_idx),
1459
+ "TE": _safe_get(som_metrics_log, "topological_error", first_idx),
1460
+ "KL": _safe_get(som_metrics_log, "kaski_lagus_error", first_idx),
1461
+ "VE": _safe_get(som_metrics_log, "explained_variance_share", first_idx),
1462
+ }
1463
+ metrics_last = {
1464
+ "QE": _safe_get(som_metrics_log, "quantization_error", last_idx),
1465
+ "TE": _safe_get(som_metrics_log, "topological_error", last_idx),
1466
+ "KL": _safe_get(som_metrics_log, "kaski_lagus_error", last_idx),
1467
+ "VE": _safe_get(som_metrics_log, "explained_variance_share", last_idx),
1468
+ }
1469
+ metrics_delta = {
1470
+ k: metrics_last[k] - metrics_first[k] for k in metrics_first
1471
+ }
1472
+
1473
+ # 2. Indicadores de falha
1474
+ first_failures = som_metrics_log[first_idx].get("failure_indicators", []) if som_metrics_log else []
1475
+ last_failures = som_metrics_log[last_idx].get("failure_indicators", []) if som_metrics_log else []
1476
+ first_health = som_metrics_log[first_idx].get("overall_health", "unknown") if som_metrics_log else "unknown"
1477
+ last_health = som_metrics_log[last_idx].get("overall_health", "unknown") if som_metrics_log else "unknown"
1478
+
1479
+ # 3. Ativação de neurônios (conscience mechanism)
1480
+ first_neurons_active = _safe_get(som_metrics_log, "n_active_neurons_bmu", first_idx)
1481
+ last_neurons_active = _safe_get(som_metrics_log, "n_active_neurons_bmu", last_idx)
1482
+ first_neurons_total = _safe_get(som_metrics_log, "n_total_neurons_bmu", first_idx) or 864
1483
+ last_neurons_total = _safe_get(som_metrics_log, "n_total_neurons_bmu", last_idx) or 864
1484
+
1485
+ # 4. Relatório paralelo final do SOM (conscience + uniformity)
1486
+ try:
1487
+ parallel_report = kls.parallel_neuron_activation_report()
1488
+ except Exception:
1489
+ parallel_report = {}
1490
+
1491
+ # 5. Taxa de aprendizado (α_t, σ_t) do SOM
1492
+ try:
1493
+ som_metrics = kls.som.get_metrics()
1494
+ alpha_t = som_metrics.get("alpha_t_effective", 0.0)
1495
+ sigma_t = som_metrics.get("sigma_t_effective", 0.0)
1496
+ alpha0 = som_metrics.get("alpha0", 0.5)
1497
+ sigma0 = som_metrics.get("sigma0", 3.0)
1498
+ som_t = som_metrics.get("t", 0)
1499
+ except Exception:
1500
+ alpha_t = sigma_t = alpha0 = sigma0 = som_t = 0.0
1501
+
1502
+ # 6. Estatísticas de punição
1503
+ n_punishments = len(punishment_log)
1504
+ n_train_hyp_calls = len(hypotheses_log)
1505
+ n_delta_apps = len(delta_applications_log)
1506
+ # Taxa de sucesso das aplicações de delta (acc_after > acc_before)
1507
+ delta_success = 0
1508
+ if delta_applications_log:
1509
+ for d in delta_applications_log:
1510
+ try:
1511
+ if float(d.get("acc_after", 0.0)) > float(d.get("acc_before", 0.0)):
1512
+ delta_success += 1
1513
+ except (TypeError, ValueError):
1514
+ pass
1515
+ delta_success_rate = float(delta_success / max(1, n_delta_apps))
1516
+
1517
+ # 7. Hipóteses — evolução da loss
1518
+ if hypotheses_log:
1519
+ loss_first = float(hypotheses_log[0].get("loss_initial", 0.0))
1520
+ loss_last_init = float(hypotheses_log[-1].get("loss_initial", 0.0))
1521
+ loss_last_final = float(hypotheses_log[-1].get("loss_final", 0.0))
1522
+ else:
1523
+ loss_first = loss_last_init = loss_last_final = 0.0
1524
+
1525
+ # ===================== MONTAGEM DAS LINHAS DE SUMÁRIO =====================
1526
+ summary_lines.append(f" Duração total: {elapsed_s:.1f}s")
1527
+ summary_lines.append(f" Logs de métricas computados: {n_logs}")
1528
+ summary_lines.append("")
1529
+ summary_lines.append(" ─── EVOLUÇÃO DAS MÉTRICAS PRINCIPAIS (início → fim) ───")
1530
+ summary_lines.append(
1531
+ f" QE (Quantization Error): {metrics_first['QE']:.4f} → "
1532
+ f"{metrics_last['QE']:.4f} (Δ={metrics_delta['QE']:+.4f})"
1533
+ )
1534
+ summary_lines.append(
1535
+ f" TE (Topological Error) : {metrics_first['TE']:.4f} → "
1536
+ f"{metrics_last['TE']:.4f} (Δ={metrics_delta['TE']:+.4f})"
1537
+ )
1538
+ summary_lines.append(
1539
+ f" KL (Kaski-Lagus) : {metrics_first['KL']:.4f} → "
1540
+ f"{metrics_last['KL']:.4f} (Δ={metrics_delta['KL']:+.4f})"
1541
+ )
1542
+ summary_lines.append(
1543
+ f" VE (Explained Variance): {metrics_first['VE']:.4f} → "
1544
+ f"{metrics_last['VE']:.4f} (Δ={metrics_delta['VE']:+.4f})"
1545
+ )
1546
+ summary_lines.append("")
1547
+ summary_lines.append(" ─── INDICADORES DE FALHA ───")
1548
+ summary_lines.append(
1549
+ f" Overall health: {first_health} → {last_health}"
1550
+ )
1551
+ summary_lines.append(
1552
+ f" Failure indicators (início): {len(first_failures)} — {first_failures[:3]}"
1553
+ )
1554
+ summary_lines.append(
1555
+ f" Failure indicators (fim) : {len(last_failures)} — {last_failures[:3]}"
1556
+ )
1557
+ summary_lines.append("")
1558
+ summary_lines.append(" ─── ATIVAÇÃO DE NEURÔNIOS (Conscience Mechanism) ───")
1559
+ summary_lines.append(
1560
+ f" Neurons ativos (BMU): {int(first_neurons_active)}/{int(first_neurons_total)} "
1561
+ f"→ {int(last_neurons_active)}/{int(last_neurons_total)}"
1562
+ )
1563
+ if parallel_report:
1564
+ summary_lines.append(
1565
+ f" Uniformity score (fim): {parallel_report.get('uniformity_score', 0.0):.4f} "
1566
+ f"(1.0 = perfeitamente uniforme)"
1567
+ )
1568
+ summary_lines.append(
1569
+ f" Conscience bias mean/std: "
1570
+ f"{parallel_report.get('conscience_bias_mean', 0.0):.6f} / "
1571
+ f"{parallel_report.get('conscience_bias_std', 0.0):.6f} "
1572
+ f"(deve tender a 0)"
1573
+ )
1574
+ summary_lines.append(
1575
+ f" Win count max/mean: "
1576
+ f"{parallel_report.get('max_win_count', 0.0):.0f} / "
1577
+ f"{parallel_report.get('mean_win_count', 0.0):.2f}"
1578
+ )
1579
+ summary_lines.append("")
1580
+ summary_lines.append(" ─── TAXA DE APRENDIZADO (Kohonen schedule) ───")
1581
+ summary_lines.append(
1582
+ f" α_t (learning rate): α₀={alpha0:.4f} → α_t={alpha_t:.6f} "
1583
+ f"(floor=0.001, decai em exp(-t/2000))"
1584
+ )
1585
+ summary_lines.append(
1586
+ f" σ_t (neighborhood) : σ₀={sigma0:.4f} → σ_t={sigma_t:.6f} "
1587
+ f"(floor=0.1, decai em exp(-t/1000))"
1588
+ )
1589
+ summary_lines.append(f" SOM t (updates) : {som_t}")
1590
+ summary_lines.append("")
1591
+ summary_lines.append(" ─── ESTATÍSTICAS DE PUNIÇÃO ───")
1592
+ summary_lines.append(f" Total punishment events: {n_punishments}")
1593
+ summary_lines.append(f" Hypotheses trainings : {n_train_hyp_calls}")
1594
+ summary_lines.append(f" Delta applications : {n_delta_apps}")
1595
+ summary_lines.append(
1596
+ f" Delta success rate : {delta_success_rate:.2%} "
1597
+ f"({delta_success}/{n_delta_apps} melhoraram acurácia)"
1598
+ )
1599
+ if hypotheses_log:
1600
+ summary_lines.append(
1601
+ f" Loss inicial primeira chamada: {loss_first:.6f}"
1602
+ )
1603
+ summary_lines.append(
1604
+ f" Loss inicial última chamada : {loss_last_init:.6f} "
1605
+ f"→ final: {loss_last_final:.6f}"
1606
+ )
1607
+ summary_lines.append("")
1608
+ summary_lines.append(" ─── HIPÓTESES (configuração final) ───")
1609
+ try:
1610
+ v2_metrics = kls.get_v2_metrics()
1611
+ summary_lines.append(
1612
+ f" n_hypotheses ativas: {v2_metrics.get('n_hypotheses', 0)} / "
1613
+ f"max {v2_metrics.get('max_n_hypotheses', 0)}"
1614
+ )
1615
+ summary_lines.append(
1616
+ f" hyp_train_steps atual: {v2_metrics.get('hyp_train_steps', 0)}"
1617
+ )
1618
+ summary_lines.append(
1619
+ f" total_hyp_steps_executed: "
1620
+ f"{v2_metrics.get('total_hyp_steps_executed', 0)}"
1621
+ )
1622
+ summary_lines.append(
1623
+ f" n_adaptations dinâmicas: "
1624
+ f"{v2_metrics.get('dynamic_adaptation', {}).get('n_adaptations', 0)}"
1625
+ )
1626
+ summary_lines.append(
1627
+ f" EWC reference set: {v2_metrics.get('ewc_reference_set', False)}"
1628
+ )
1629
+ except Exception:
1630
+ pass
1631
+
1632
+ return {
1633
+ "summary_lines": summary_lines,
1634
+ "metrics_evolution": {
1635
+ "first": metrics_first,
1636
+ "last": metrics_last,
1637
+ "delta": metrics_delta,
1638
+ },
1639
+ "learning_rate_evolution": {
1640
+ "alpha0": alpha0,
1641
+ "alpha_t_final": alpha_t,
1642
+ "sigma0": sigma0,
1643
+ "sigma_t_final": sigma_t,
1644
+ "som_t": som_t,
1645
+ },
1646
+ "indicators_evolution": {
1647
+ "first_health": first_health,
1648
+ "last_health": last_health,
1649
+ "first_failures": first_failures,
1650
+ "last_failures": last_failures,
1651
+ "first_n_failures": len(first_failures),
1652
+ "last_n_failures": len(last_failures),
1653
+ },
1654
+ "neuron_activation_evolution": {
1655
+ "first_active": int(first_neurons_active),
1656
+ "last_active": int(last_neurons_active),
1657
+ "total": int(last_neurons_total),
1658
+ "parallel_report_final": parallel_report,
1659
+ },
1660
+ "punishment_stats": {
1661
+ "n_punishments": n_punishments,
1662
+ "n_train_hyp_calls": n_train_hyp_calls,
1663
+ "n_delta_apps": n_delta_apps,
1664
+ "delta_success_rate": delta_success_rate,
1665
+ "loss_first_initial": loss_first,
1666
+ "loss_last_initial": loss_last_init,
1667
+ "loss_last_final": loss_last_final,
1668
+ },
1669
+ }
1670
+
1671
+
1672
  # ============================================================================
1673
  # 10. PHASE 2 — TREINAMENTO COM PUNIÇÃO (16 hipóteses × 3 tentativas)
1674
  # ============================================================================
 
1935
  traceback.print_exc()
1936
 
1937
  step += 1
1938
+ # V6.5-V2-metrics-FIX-4 — pausa maior na FASE2 (PUNITIVA)
1939
+ # User requirement: "FASE2 PUNIÇÃO é mais pesada é pode exigir
1940
+ # pausas do streaming até concluir o processamento".
1941
+ time.sleep(INTER_BATCH_PAUSE_S_FASE2)
1942
  except Exception as e:
1943
  logger.error(f"[V6.5-V2] Batch error in PUNIÇÃO: {e}")
1944
  traceback.print_exc()
1945
  continue
1946
 
1947
+ # V6.5-V2-metrics-FIX-4 — Auto-revive neurônios mortos na FASE2
1948
+ # (PUNITIVA também deve distribuir processamento paralelamente)
1949
+ try:
1950
+ revival = kls.auto_revive_if_needed(
1951
+ dead_rate_threshold=AUTO_REVIVE_DEAD_RATE_THRESHOLD,
1952
+ min_steps_between_revivals=AUTO_REVIVE_COOLDOWN_STEPS,
1953
+ )
1954
+ if revival.get("action") == "auto_revived":
1955
+ logger.info(
1956
+ f"[V6.5-V2-metrics-FIX-4] PUNIÇÃO AUTO-REVIVE: "
1957
+ f"n_revived={revival['n_revived']}/{revival['n_total']}, "
1958
+ f"dead_rate {revival['dead_rate_before']:.3f} → "
1959
+ f"{revival['dead_rate_after']:.3f}"
1960
+ )
1961
+ except Exception as revive_err:
1962
+ logger.warning(
1963
+ f"[V6.5-V2-metrics-FIX-4] PUNIÇÃO auto_revive failed: {revive_err}"
1964
+ )
1965
+
1966
+ time.sleep(INTER_STREAM_BATCH_PAUSE_S_FASE2)
1967
  # V2-dynamic-memory — Buffer sliding window na PUNIÇÃO também
1968
  if len(kls.buffer_4d) > MAX_BUFFER_SIZE:
1969
  overflow = len(kls.buffer_4d) - MAX_BUFFER_SIZE
 
1980
  kls.aggressive_cleanup()
1981
  except Exception:
1982
  pass
1983
+ # V6.5-V2-metrics-FIX-4: pausa pós-processamento maior na FASE2
1984
+ # (user requirement: "FASE2 PUNIÇÃO é mais pesada")
1985
+ time.sleep(POST_PROCESSING_PAUSE_S_FASE2)
1986
  except Exception as e:
1987
  logger.error(f"[V6.5-V2] PUNIÇÃO dataset failed: {e}")
1988
  traceback.print_exc()
 
1997
  logger.info(f" Hypotheses trainings: {len(hypotheses_log)}")
1998
  logger.info(f" Delta applications: {len(delta_applications_log)}")
1999
 
2000
+ # V6.5-V2-metrics-FIX-4 — Sumário final da FASE2
2001
+ # User requirement: "ao final da FASE2 mostrar evolução de métricas e dos
2002
+ # indicadores e da taxa de aprendizagem".
2003
+ fase2_summary = build_fase2_final_summary(
2004
+ kls=kls,
2005
+ som_metrics_log=som_metrics_log,
2006
+ punishment_log=punishment_log,
2007
+ hypotheses_log=hypotheses_log,
2008
+ delta_applications_log=delta_applications_log,
2009
+ elapsed_s=t_elapsed,
2010
+ )
2011
+ logger.info("\n" + "=" * 80)
2012
+ logger.info("[V6.5-V2-metrics-FIX-4] FASE 2 — EVOLUÇÃO FINAL DE MÉTRICAS")
2013
+ logger.info("=" * 80)
2014
+ for line in fase2_summary["summary_lines"]:
2015
+ logger.info(line)
2016
+ logger.info("=" * 80 + "\n")
2017
+
2018
  return {
2019
  "phase": "TREINAMENTO_COM_PUNICAO",
2020
  "dataset": PUNICAO_DATASET,
 
2036
  ),
2037
  "som_metric_history": kls.get_som_metric_history(),
2038
  "final_v2_state": kls.get_v2_metrics(),
2039
+ # V6.5-V2-metrics-FIX-4 — sumário final da FASE2
2040
+ "fase2_summary": fase2_summary,
2041
  }
2042
 
2043
 
 
2480
  # + Goose VQ, fornecendo representação compacta do estado do SOM.
2481
  # O compressor já sanitiza NaN/Inf internamente (torch.nan_to_num).
2482
  enable_vqvae2=True, # REATIVADO (was False in V6.5-V2-metrics-FIX)
2483
+ # V6.5-V2-metrics-FIX-4: reasoning_engine desabilitado para mitigar
2484
+ # OOM-killer (User requirement implícito: "investigar e corrigir
2485
+ # falhas de lógica e bugs que estejam causando alto consumo de memória
2486
+ # sem distorcer a arquitetura Kohonen"). O ReasoningEngine cria um
2487
+ # ThreadPoolExecutor(4 workers) + ToolAgentCoordinator que consome
2488
+ # ~100-200MB adicionais. Como o reasoning_engine é OPCIONAL e não
2489
+ # afeta o aprendizado do SOM (apenas gera tags <think>/<plan>/<answer>
2490
+ # para predições), desabilitá-lo preserva a arquitetura Kohonen e
2491
+ # libera memória para o streaming de 8000+2000 samples.
2492
+ # Para reativar: mudar para True (requer ≥6GB cgroup).
2493
+ enable_reasoning=False,
2494
  enable_w8a8=False, # W8A8 permanece desabilitado (não essencial para Kohonen)
2495
  vqvae2_code_dim=16,
2496
  vqvae2_num_codes_top=64,
 
2759
  SRC_ROOT / "bigru_t" / "data" / "streaming_datasets.py",
2760
  SRC_ROOT / "bigru_t" / "utils" / "xeon_runtime.py",
2761
  ]
2762
+ # V6.5-V2-metrics-FIX-3: FILTRA arquivos .pt do upload HF.
2763
+ # User requirement: "ao concluir enviar para o HF os arquivos e módulos
2764
+ # e scripts em lote" + auditoria encontrou que .pt files estavam sendo
2765
+ # uploaded (MODEL_STATES_PATH e *_after_conhecimento.pt). Estados do
2766
+ # modelo (>40MB cada) não devem ir ao HF — apenas scripts e relatórios.
2767
  outputs_to_upload: List[Path] = [
2768
  REPORT_PATH,
 
2769
  V2_PHASES_EVAL_PATH,
2770
  PREDICT_FIX_EVAL_PATH,
2771
  ATTENTION_EVAL_PATH,
2772
  USER_QUESTIONS_PATH,
 
2773
  ]
2774
+ # Filtra os que existem E não são .pt/.pth/.bin/.safetensors
2775
  all_files: List[Path] = []
2776
+ _FORBIDDEN_UPLOAD_EXTS = {'.pt', '.pth', '.bin', '.safetensors', '.ckpt'}
2777
  for p in scripts_to_upload + outputs_to_upload:
2778
+ if not p.exists():
 
 
2779
  logger.warning(f"[V6.5-V2] Upload file not found: {p}")
2780
+ continue
2781
+ if p.suffix.lower() in _FORBIDDEN_UPLOAD_EXTS:
2782
+ logger.info(f"[V6.5-V2] Skipping model state from HF upload: {p.name}")
2783
+ continue
2784
+ all_files.append(p)
2785
 
2786
  # Apaga HF_TOKEN dos scripts ANTES do upload (não enviar tokens)
2787
  scrub_result = scrub_hf_token_from_scripts(BIGRU_ROOT / "scripts")
src/bigru_t/model/kohonen_learning_system.py CHANGED
@@ -325,7 +325,23 @@ def text_to_4d_vector(
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,)
@@ -356,13 +372,34 @@ class KohonenSOM4D:
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
 
@@ -372,6 +409,54 @@ class KohonenSOM4D:
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
@@ -385,13 +470,39 @@ class KohonenSOM4D:
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)
@@ -400,17 +511,349 @@ class KohonenSOM4D:
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
@@ -425,6 +868,13 @@ class KohonenSOM4D:
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)
@@ -436,6 +886,26 @@ class KohonenSOM4D:
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)
@@ -455,10 +925,20 @@ class KohonenSOM4D:
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),
@@ -478,6 +958,26 @@ class KohonenSOM4D:
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
 
@@ -547,7 +1047,12 @@ class KohonenLearningSystem:
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,
@@ -1059,15 +1564,21 @@ class KohonenLearningSystem:
1059
  )
1060
  self.som.update_weights(x, bmu, accumulate_fisher=acc_fisher)
1061
 
1062
- # V6.5 — Ativa VQ-VAE-2 compressor no pipeline
 
 
 
 
1063
  if self.enable_vqvae2 and self.vqvae2_compressor is not None:
1064
- self._compress_buffer_with_vqvae2(data)
 
1065
 
1066
  # V6.5-final — Ativa SmoothQuant W8A8 compressor dos pesos do SOM
1067
  # User requirement: "integrar SmoothQuantCompressor ao KLS para
1068
  # compressão W8A8 real dos pesos do SOM"
1069
  if self.enable_w8a8 and self.w8a8_compressor is not None:
1070
- self.compress_som_weights_w8a8()
 
1071
 
1072
  # V6.5-final — Reativa tool_coordinator workers via execute_parallel
1073
  # User requirement: "reativar reasoning_engine's tool_coordinator workers"
@@ -2417,6 +2928,18 @@ class KohonenLearningSystemV2(KohonenLearningSystem):
2417
  loss_total = deltas_stack.norm()
2418
 
2419
  loss_total.backward()
 
 
 
 
 
 
 
 
 
 
 
 
2420
  self.hyp_optimizer.step()
2421
  losses.append(float(loss_total.item()))
2422
 
@@ -2772,13 +3295,35 @@ class KohonenLearningSystemV2(KohonenLearningSystem):
2772
  # Re-rotula neurônios (BMU labels podem ter mudado)
2773
  self._label_neurons()
2774
 
2775
- # Consolida via EWC (Fisher information em w)
2776
- try:
2777
- self.som.set_ewc_reference()
2778
- ewc_set = True
2779
- except Exception as e:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2780
  ewc_set = False
2781
- sel_info["ewc_error"] = str(e)[:200]
 
 
 
 
 
 
 
 
2782
 
2783
  return {
2784
  "active": True,
@@ -3163,6 +3708,130 @@ class KohonenLearningSystemV2(KohonenLearningSystem):
3163
  "max_hyp_train_steps": int(self.max_hyp_train_steps),
3164
  }
3165
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3166
  # ==================================================================
3167
  # V6.5-V2-metrics — Integração das métricas SOM canônicas
3168
  # (QE, TE, Kaski-Lagus, Variância Explicada, Dead Neurons,
 
325
 
326
  # SVD para 3D
327
  mean_centered = fused - fused.mean(dim=0, keepdim=True)
328
+ # V6.5-V2-metrics-FIX-3 — SVD com fallback robusto: se a matriz for
329
+ # rank-deficiente (ex: todos os tokens são <pad> → todas as linhas
330
+ # idênticas), torch.linalg.svd pode produzir NaN ou levantar RuntimeError.
331
+ # Fallback 1: driver 'gesvd' (mais estável para matrizes singulares).
332
+ # Fallback 2: zeros (vetor 4D neutro, não contamina o SOM).
333
+ try:
334
+ U, S, V = torch.linalg.svd(mean_centered, full_matrices=False)
335
+ except Exception:
336
+ try:
337
+ U, S, V = torch.linalg.svd(mean_centered, full_matrices=False, driver='gesvd')
338
+ except Exception:
339
+ S = torch.zeros(min(mean_centered.shape), device=mean_centered.device)
340
+ V = torch.eye(mean_centered.shape[1], device=mean_centered.device)
341
+ # V6.5-V2-metrics-FIX-3 — sanitiza NaN do SVD (rank-deficient → S pode ter NaN)
342
+ if torch.isnan(S).any() or torch.isinf(S).any():
343
+ S = torch.nan_to_num(S, nan=0.0, posinf=0.0, neginf=0.0)
344
+ V = torch.nan_to_num(V, nan=0.0, posinf=0.0, neginf=0.0)
345
  coords_3d = torch.mm(mean_centered, V[:3, :].t()) # (L, 3)
346
 
347
  xyz_mean = coords_3d.mean(dim=0) # (3,)
 
372
  def __init__(
373
  self,
374
  grid_shape: Tuple[int, int, int, int],
375
+ alpha0: float = 0.5,
376
+ sigma0: float = 3.0,
377
  lambda_ewc: float = 0.01,
378
+ conscience_gamma: float = 0.1,
379
+ conscience_beta: float = 0.1,
380
  ):
381
+ # V6.5-V2-metrics-FIX-3 — α₀=0.5 e σ₀=3.0 conforme especificação
382
+ # canônica para SOM 4D (Kohonen classic):
383
+ # - α₀ ∈ [0.5, 1.0] para fase de ordenação (rough training)
384
+ # - σ₀ = metade da maior dimensão da grade = max(6,6,6,4)/2 = 3.0
385
+ # V6.5-V2-metrics-FIX-3 — floors explícitos no update_weights garantem
386
+ # que α_t e σ_t nunca decaiam abaixo de 0.001 e 0.1 respectivamente
387
+ # (sem isso, após ~7000 updates σ→0 e o SOM degenera em k-means puro,
388
+ # perdendo preservação topológica).
389
+ #
390
+ # V6.5-V2-metrics-FIX-4 (Conscience Mechanism — DeSieno 1988):
391
+ # Corrige o problema neurons_active=2/864 relatado pelo usuário.
392
+ # Cada neurônio i mantém uma frequência de vitória p_i ∈ [0,1].
393
+ # BMU selection: bmu = argmin_i ( ||W_i - x||² - b_i )
394
+ # onde b_i = γ · (1/N - p_i) é o bias de consciência
395
+ # γ = 0.1 (default), N = total de neurônios (864)
396
+ # Update da frequência (EMA): p_i ← (1-β)·p_i + β·𝟙[i==bmu], β=0.1
397
+ # Quando um neurônio ganha demais (p_i > 1/N), b_i fica negativo
398
+ # (penaliza), e quando nunca ganha (p_i ≈ 0), b_i fica positivo
399
+ # (empurra para ganhar). Isto força distribuição uniforme de BMU.
400
  self.I, self.J, self.K, self.L = grid_shape
401
+ self.alpha0 = float(alpha0)
402
+ self.sigma0 = float(sigma0)
403
  self.lambda_ewc = lambda_ewc
404
  self.t = 0
405
 
 
409
  self.fisher_accum = torch.zeros(self.I, self.J, self.K, self.L)
410
  self.fisher_count = torch.zeros(self.I, self.J, self.K, self.L)
411
 
412
+ # V6.5-V2-metrics-FIX-4 — Conscience mechanism (DeSieno 1988)
413
+ # Inicializa p_i = 1/N (uniforme) — sem viés inicial.
414
+ # b_i = γ · (1/N - p_i) começa em 0 (sem bias).
415
+ self.n_neurons = self.I * self.J * self.K * self.L
416
+ self.conscience_gamma = float(conscience_gamma)
417
+ self.conscience_beta = float(conscience_beta)
418
+ target_p = 1.0 / self.n_neurons
419
+ self.win_frequency = torch.full(
420
+ (self.I, self.J, self.K, self.L), target_p, dtype=torch.float
421
+ )
422
+ # Contador absoluto de vitórias por neurônio (para auditoria)
423
+ self.bmu_win_count = torch.zeros(
424
+ self.I, self.J, self.K, self.L, dtype=torch.long
425
+ )
426
+ # Histórico dos últimos K BMUs (para revive_dead_neurons)
427
+ self._recent_bmu_flat: List[int] = []
428
+
429
+ # ------------------------------------------------------------------
430
+ # V6.5-V2-auto-conscience — Estado para ajuste adaptativo de γ.
431
+ # User requirement: "aprimorar conscience_gamma para ajuste
432
+ # automático conforme a quantidade de neurônios esteja caindo".
433
+ #
434
+ # Math (DeSieno 1988 + adaptação adaptativa):
435
+ # r = n_active / n_total (∈ [0,1])
436
+ # γ_base = γ_max · (1 - r)^2 (intervenção cresce
437
+ # com colapso)
438
+ # drop = max(0, r_prev - r) (tendência de queda)
439
+ # γ_boost = 0.3 · drop (reforço antecipado)
440
+ # γ_target = clip(γ_base + γ_boost, γ_min, γ_max)
441
+ # γ_new = (1 - η) · γ_old + η · γ_target (EMA suave)
442
+ #
443
+ # Comportamento esperado:
444
+ # r = 1.0 → γ_base = 0.00 (data distribution domina)
445
+ # r = 0.5 → γ_base = 0.25 (intervenção moderada)
446
+ # r = 0.1 → γ_base = 0.81 (intervenção forte)
447
+ # r = 0.0 → γ_base = 1.00 (intervenção máxima)
448
+ # r caindo 1.0→0.5 → +0.15 boost (antecipa colapso)
449
+ # ------------------------------------------------------------------
450
+ self._conscience_gamma_min = 0.01
451
+ self._conscience_gamma_max = 1.0
452
+ self._conscience_eta = 0.05 # EMA smoothing
453
+ self._prev_n_active = -1
454
+ self._prev_n_active_rate = -1.0
455
+ self._last_gamma_target = float(conscience_gamma)
456
+ self._last_gamma_drop = 0.0
457
+ self._last_activation_rate = 1.0
458
+ self._auto_adjust_interval = 50 # ajusta γ a cada 50 updates
459
+
460
  def _neighborhood(self, bmu_idx):
461
  """Vizinhança Gaussiana 4D: d² = Δi² + Δj² + Δk² + Δl²."""
462
  i, j, k, l = bmu_idx
 
470
  dist_sq = (II - i) ** 2 + (JJ - j) ** 2 + (KK - k) ** 2 + (LL - l) ** 2
471
  return dist_sq
472
 
473
+ def find_bmu(self, x: torch.Tensor, use_conscience: bool = True) -> Tuple[int, int, int, int]:
474
+ """Best Matching Unit: argmin ||W - x||² em ℝ⁴, com conscience bias opcional.
475
 
476
  Substitui pgvector_lookup — busca nearest-neighbor flat sobre o grid.
477
+
478
+ V6.5-V2-metrics-FIX-4 (Conscience Mechanism — DeSieno 1988):
479
+ Quando use_conscience=True, adiciona bias b_i = γ·(1/N - p_i) à
480
+ distância, forçando distribuição uniforme de BMU entre os 864
481
+ neurônios. Isto corrige o problema neurons_active=2/864:
482
+
483
+ bmu = argmin_i ( ||W_i - x||² - b_i )
484
+
485
+ Sem conscience, neurônios próximos a poucas amostras ficam mortos.
486
+ Com conscience, neurônios que ganham demais (p_i > 1/N) são
487
+ penalizados (b_i < 0 aumenta distância efetiva), e neurônios que
488
+ nunca ganham (p_i ≈ 0) são favorecidos (b_i > 0 reduz distância).
489
+
490
+ Args:
491
+ x: tensor [4] — vetor de entrada.
492
+ use_conscience: se True, aplica bias de consciência (default True).
493
  """
494
+ # Sanitiza x antes de computar distância
495
+ if torch.isnan(x).any() or torch.isinf(x).any():
496
+ x = torch.nan_to_num(x, nan=0.0, posinf=1e4, neginf=-1e4)
497
  dist = torch.sum((self.weights - x.view(1, 1, 1, 1, 4)) ** 2, dim=-1)
498
+ if use_conscience:
499
+ # b_i = γ · (1/N - p_i) → subtraído da distância (menor distância efetiva vence)
500
+ target_p = 1.0 / self.n_neurons
501
+ bias = self.conscience_gamma * (target_p - self.win_frequency)
502
+ dist_effective = dist - bias
503
+ flat_idx = int(torch.argmin(dist_effective).item())
504
+ else:
505
+ flat_idx = int(torch.argmin(dist).item())
506
  i = flat_idx // (self.J * self.K * self.L)
507
  rest = flat_idx % (self.J * self.K * self.L)
508
  j = rest // (self.K * self.L)
 
511
  l = rest % self.L
512
  return (i, j, k, l)
513
 
514
+ def update_win_frequency(self, bmu_idx: Tuple[int, int, int, int]) -> None:
515
+ """V6.5-V2-metrics-FIX-4 — Atualiza frequência de vitória (EMA).
516
+
517
+ p_i ← (1-β)·p_i + β·𝟙[i==bmu], β = conscience_beta (default 0.1)
518
+
519
+ Também mantém contador absoluto bmu_win_count para auditoria.
520
+ Deve ser chamado APÓS find_bmu e APÓS update_weights.
521
+
522
+ Args:
523
+ bmu_idx: (i, j, k, l) — índice do BMU selecionado.
524
+ """
525
+ with torch.no_grad():
526
+ # EMA update
527
+ self.win_frequency = (
528
+ (1.0 - self.conscience_beta) * self.win_frequency
529
+ )
530
+ self.win_frequency[bmu_idx] += self.conscience_beta
531
+ # Contador absoluto
532
+ self.bmu_win_count[bmu_idx] += 1
533
+ # Histórico recente (para revive_dead_neurons)
534
+ flat = (
535
+ bmu_idx[0] * (self.J * self.K * self.L)
536
+ + bmu_idx[1] * (self.K * self.L)
537
+ + bmu_idx[2] * self.L
538
+ + bmu_idx[3]
539
+ )
540
+ self._recent_bmu_flat.append(flat)
541
+ # Mantém últimos 200 BMUs
542
+ if len(self._recent_bmu_flat) > 200:
543
+ self._recent_bmu_flat = self._recent_bmu_flat[-200:]
544
+
545
+ def revive_dead_neurons(
546
+ self,
547
+ data_buffer: Optional[List[torch.Tensor]] = None,
548
+ dead_threshold: int = 0,
549
+ ) -> Dict[str, Any]:
550
+ """V6.5-V2-metrics-FIX-4 — Revive neurônios mortos reinicializando pesos.
551
+
552
+ User requirement: "ANALISAR matematicamente e logicamente a ativação e
553
+ uso e acesso dos neurônios (apenas dois estão sendo ativados:
554
+ neurons_active=2/864) distribuindo o processamento paralelamente".
555
+
556
+ Um neurônio é considerado "morto" se bmu_win_count <= dead_threshold
557
+ (nunca ou raramente foi BMU). Para cada neurônio morto:
558
+
559
+ 1. Se data_buffer fornecido: amostra um vetor aleatório do buffer e
560
+ atribui aos pesos do neurônio (reinicialização data-driven).
561
+ 2. Se buffer vazio: reinicializa com ruído gaussiano pequeno (N(0, 0.1)).
562
+ 3. Reseta win_frequency para 1/N (sem bias) e bmu_win_count para 0.
563
+
564
+ Isto garante que TODOS os 864 neurônios sejam utilizados, distribuindo
565
+ o processamento paralelo do SOM conforme solicitado.
566
+
567
+ Args:
568
+ data_buffer: lista de tensores [4] — amostras do buffer_4d do KLS.
569
+ dead_threshold: neurônios com win_count <= threshold são revividos.
570
+
571
+ Returns:
572
+ Dict com: n_revived, n_total, revived_indices, dead_rate_before, dead_rate_after.
573
+ """
574
+ with torch.no_grad():
575
+ dead_mask = self.bmu_win_count <= dead_threshold
576
+ n_dead = int(dead_mask.sum().item())
577
+ n_total = self.n_neurons
578
+ dead_rate_before = float(n_dead / n_total)
579
+
580
+ revived_indices: List[Tuple[int, int, int, int]] = []
581
+ if n_dead == 0:
582
+ return {
583
+ "n_revived": 0,
584
+ "n_total": n_total,
585
+ "revived_indices": [],
586
+ "dead_rate_before": dead_rate_before,
587
+ "dead_rate_after": dead_rate_before,
588
+ "action": "none",
589
+ }
590
+
591
+ # Amostra pontos do buffer se disponível
592
+ buffer_tensor = None
593
+ if data_buffer and len(data_buffer) > 0:
594
+ try:
595
+ buffer_tensor = torch.stack(
596
+ [v.detach().clone() if isinstance(v, torch.Tensor)
597
+ else torch.tensor(v, dtype=torch.float)
598
+ for v in data_buffer]
599
+ ).float()
600
+ except Exception:
601
+ buffer_tensor = None
602
+
603
+ # Itera sobre neurônios mortos e reinicializa
604
+ dead_indices = dead_mask.nonzero(as_tuple=False)
605
+ for idx_tensor in dead_indices:
606
+ i, j, k, l = idx_tensor.tolist()
607
+ if buffer_tensor is not None and buffer_tensor.shape[0] > 0:
608
+ # Amostra aleatória do buffer
609
+ sample_idx = torch.randint(0, buffer_tensor.shape[0], (1,)).item()
610
+ new_w = buffer_tensor[sample_idx].clone()
611
+ # Pequeno ruído para evitar duplicação exata
612
+ new_w = new_w + 0.05 * torch.randn(4)
613
+ else:
614
+ # Reinicialização gaussiana pequena
615
+ new_w = 0.1 * torch.randn(4)
616
+ # Clamp para segurança
617
+ new_w = torch.clamp(new_w, -10.0, 10.0)
618
+ self.weights[i, j, k, l] = new_w
619
+ # Reset counters
620
+ self.win_frequency[i, j, k, l] = 1.0 / n_total
621
+ self.bmu_win_count[i, j, k, l] = 0
622
+ revived_indices.append((i, j, k, l))
623
+
624
+ # Recalcula dead rate após revival
625
+ new_dead_mask = self.bmu_win_count <= dead_threshold
626
+ n_dead_after = int(new_dead_mask.sum().item())
627
+ dead_rate_after = float(n_dead_after / n_total)
628
+
629
+ return {
630
+ "n_revived": len(revived_indices),
631
+ "n_total": n_total,
632
+ "revived_indices": revived_indices[:50], # top 50 para log
633
+ "n_dead_before": n_dead,
634
+ "n_dead_after": n_dead_after,
635
+ "dead_rate_before": dead_rate_before,
636
+ "dead_rate_after": dead_rate_after,
637
+ "action": "revived" if revived_indices else "none",
638
+ "used_buffer": buffer_tensor is not None,
639
+ }
640
+
641
+ def parallel_neuron_activation_report(self) -> Dict[str, Any]:
642
+ """V6.5-V2-metrics-FIX-4 — Relatório estruturado de ativação dos 864 neurônios.
643
+
644
+ User requirement: "distribuindo o processamento paralelamente" + o
645
+ exemplo de código fornecido mostra estruturação vetorizada para
646
+ análise estatística e auditoria do modelo.
647
+
648
+ Este método produz um relatório análogo ao DataFrame do exemplo,
649
+ mas otimizado para o grid 4D (6,6,6,4) com 864 neurônios:
650
+
651
+ Returns:
652
+ Dict com:
653
+ - n_total_neurons: int (864)
654
+ - n_active_neurons: int (vitória em ≥1 amostra histórica)
655
+ - n_dead_neurons: int (nunca foi BMU)
656
+ - neuron_activation_rate: float
657
+ - max_win_count: int (neurônio mais ativo)
658
+ - min_win_count: int (neurônio menos ativo)
659
+ - mean_win_count: float
660
+ - std_win_count: float
661
+ - max_win_frequency: float
662
+ - min_win_frequency: float
663
+ - bmu_distribution_top20: dict {flat_idx: count}
664
+ - bmu_distribution_bottom20: dict {flat_idx: count} (mortos)
665
+ - conscience_bias_mean: float (deve tender a 0 se uniforme)
666
+ - conscience_bias_std: float (deve tender a 0 se uniforme)
667
+ - uniformity_score: float (1 - CV da win_frequency, ∈ [0,1])
668
+ """
669
+ with torch.no_grad():
670
+ win_counts_flat = self.bmu_win_count.flatten().float()
671
+ win_freq_flat = self.win_frequency.flatten()
672
+
673
+ n_total = self.n_neurons
674
+ n_active = int((self.bmu_win_count > 0).sum().item())
675
+ n_dead = n_total - n_active
676
+
677
+ # Estatísticas
678
+ if n_total > 0:
679
+ max_wc = float(win_counts_flat.max().item())
680
+ min_wc = float(win_counts_flat.min().item())
681
+ mean_wc = float(win_counts_flat.mean().item())
682
+ std_wc = float(win_counts_flat.std().item())
683
+ max_wf = float(win_freq_flat.max().item())
684
+ min_wf = float(win_freq_flat.min().item())
685
+ # Uniformidade: 1 - CV (coeficiente de variação)
686
+ cv = float(std_wc / max(mean_wc, 1e-8))
687
+ uniformity = max(0.0, 1.0 - cv)
688
+ else:
689
+ max_wc = min_wc = mean_wc = std_wc = 0.0
690
+ max_wf = min_wf = 0.0
691
+ uniformity = 0.0
692
+
693
+ # Bias de consciência: b_i = γ · (1/N - p_i)
694
+ target_p = 1.0 / n_total
695
+ bias_flat = self.conscience_gamma * (target_p - win_freq_flat)
696
+ bias_mean = float(bias_flat.mean().item())
697
+ bias_std = float(bias_flat.std().item())
698
+
699
+ # Top-20 BMUs mais frequentes
700
+ from collections import Counter
701
+ recent_counter = Counter(self._recent_bmu_flat)
702
+ top20 = dict(recent_counter.most_common(20))
703
+
704
+ # Bottom-20 (mortos ou raros) — últimos em vitórias absolutas
705
+ sorted_indices = torch.argsort(win_counts_flat)
706
+ bottom20_idx = sorted_indices[:20].tolist()
707
+ bottom20 = {
708
+ int(idx): int(self.bmu_win_count.flatten()[idx].item())
709
+ for idx in bottom20_idx
710
+ }
711
+
712
+ return {
713
+ "n_total_neurons": int(n_total),
714
+ "n_active_neurons": int(n_active),
715
+ "n_dead_neurons": int(n_dead),
716
+ "neuron_activation_rate": float(n_active / max(1, n_total)),
717
+ "max_win_count": max_wc,
718
+ "min_win_count": min_wc,
719
+ "mean_win_count": mean_wc,
720
+ "std_win_count": std_wc,
721
+ "max_win_frequency": max_wf,
722
+ "min_win_frequency": min_wf,
723
+ "bmu_distribution_top20": {str(k): int(v) for k, v in top20.items()},
724
+ "bmu_distribution_bottom20": {str(k): int(v) for k, v in bottom20.items()},
725
+ "conscience_bias_mean": bias_mean,
726
+ "conscience_bias_std": bias_std,
727
+ "uniformity_score": uniformity,
728
+ "conscience_gamma": float(self.conscience_gamma),
729
+ "conscience_beta": float(self.conscience_beta),
730
+ # V6.5-V2-auto-conscience — telemetria do ajuste adaptativo
731
+ "conscience_gamma_min": float(self._conscience_gamma_min),
732
+ "conscience_gamma_max": float(self._conscience_gamma_max),
733
+ "conscience_gamma_target": float(self._last_gamma_target),
734
+ "conscience_gamma_drop": float(self._last_gamma_drop),
735
+ "activation_rate_at_last_adjust": float(self._last_activation_rate),
736
+ "prev_n_active_at_last_adjust": int(self._prev_n_active),
737
+ "n_recent_bmus_tracked": int(len(self._recent_bmu_flat)),
738
+ }
739
+
740
+ def auto_adjust_conscience_gamma(self) -> Dict[str, Any]:
741
+ """V6.5-V2-auto-conscience — Ajusta γ dinamicamente conforme ativação.
742
+
743
+ User requirement: "aprimorar conscience_gamma para ajuste automático
744
+ conforme a quantidade de neurônios esteja caindo".
745
+
746
+ Quando a quantidade de neurônios ativos cai (colapso), o bias de
747
+ consciência precisa ficar mais forte para reviver neurônios mortos.
748
+ Quando a ativação está saudável (≈1.0), γ decai para deixar a
749
+ distribuição de dados comandar o SOM (preservação topológica natural).
750
+
751
+ Math (DeSieno 1988 + adaptação adaptativa):
752
+ r = n_active / n_total (∈ [0,1])
753
+ γ_base = γ_max · (1 - r)^2 (intervenção
754
+ cresce com
755
+ colapso)
756
+ drop = max(0, r_prev - r) (tendência de
757
+ queda)
758
+ γ_boost = 0.3 · drop (reforço
759
+ antecipado)
760
+ γ_target = clip(γ_base + γ_boost, γ_min, γ_max)
761
+ γ_new = (1 - η) · γ_old + η · γ_target (EMA suave)
762
+
763
+ Comportamento esperado em diferentes cenários:
764
+ r = 1.0 (saudável) → γ_base = 0.00 (data distribution domina)
765
+ r = 0.5 (degradação) → γ_base = 0.25 (intervenção moderada)
766
+ r = 0.1 (colapso severo) → γ_base = 0.81 (intervenção forte)
767
+ r = 0.0 (paralisia) → γ_base = 1.00 (intervenção máxima)
768
+ r caindo 1.0→0.5 → +0.15 boost (antecipa colapso)
769
+ r subindo 0.5→1.0 → drop=0, sem boost extra
770
+
771
+ Returns:
772
+ Dict com: n_active, n_total, activation_rate, prev_activation_rate,
773
+ drop, gamma_target, gamma_old, gamma_new, gamma_min, gamma_max, eta.
774
+ """
775
+ with torch.no_grad():
776
+ n_total = self.n_neurons
777
+ n_active = int((self.bmu_win_count > 0).sum().item())
778
+ r = float(n_active / max(1, n_total))
779
+
780
+ # Tendência: se _prev_n_active_rate < 0, ainda não houve ajuste
781
+ r_prev = (
782
+ self._prev_n_active_rate
783
+ if self._prev_n_active_rate >= 0
784
+ else r
785
+ )
786
+ drop = max(0.0, r_prev - r)
787
+
788
+ # γ alvo: intervenção quadrática no colapso + boost antecipado
789
+ gamma_base = self._conscience_gamma_max * (1.0 - r) ** 2
790
+ gamma_trend_boost = 0.3 * drop
791
+ gamma_target = max(
792
+ self._conscience_gamma_min,
793
+ min(
794
+ self._conscience_gamma_max,
795
+ gamma_base + gamma_trend_boost,
796
+ ),
797
+ )
798
+
799
+ # EMA suave para evitar oscilações bruscas
800
+ eta = self._conscience_eta
801
+ gamma_old = float(self.conscience_gamma)
802
+ gamma_new = (1.0 - eta) * gamma_old + eta * float(gamma_target)
803
+ gamma_new = float(max(
804
+ self._conscience_gamma_min,
805
+ min(self._conscience_gamma_max, gamma_new),
806
+ ))
807
+
808
+ # Persiste estado
809
+ self.conscience_gamma = gamma_new
810
+ self._prev_n_active = n_active
811
+ self._prev_n_active_rate = r
812
+ self._last_gamma_target = float(gamma_target)
813
+ self._last_gamma_drop = float(drop)
814
+ self._last_activation_rate = r
815
+
816
+ return {
817
+ "n_active": n_active,
818
+ "n_total": n_total,
819
+ "activation_rate": r,
820
+ "prev_activation_rate": r_prev,
821
+ "drop": drop,
822
+ "gamma_target": float(gamma_target),
823
+ "gamma_old": gamma_old,
824
+ "gamma_new": gamma_new,
825
+ "gamma_min": float(self._conscience_gamma_min),
826
+ "gamma_max": float(self._conscience_gamma_max),
827
+ "eta": float(eta),
828
+ }
829
+
830
  def update_weights(self, x: torch.Tensor, bmu_idx, accumulate_fisher=False):
831
  """Update Kohonen: ΔW = α·Λ·(x - W) + penalidade EWC em w.
832
 
833
+ V6.5-V2-metrics-FIX-4 — agora chama update_win_frequency automaticamente
834
+ após o update, garantindo que o conscience mechanism seja atualizado
835
+ a cada amostra processada (sem necessidade de chamada externa).
836
+
837
+ V6.5-V2-metrics-FIX-3 — Correções matemáticas:
838
+ 1. Floors explícitos em α_t e σ_t (previnem colapso topológico
839
+ após muitas épocas, quando σ_t→0 degenera o SOM em k-means).
840
+ 2. Sanitização de NaN/Inf em x ANTES do update (uma única amostra
841
+ NaN não deve contaminar permanentemente os pesos).
842
+ 3. Clamp dos pesos após update (evita explosão numérica).
843
+
844
  Args:
845
  x: tensor [4] — amostra 4D.
846
  bmu_idx: (i, j, k, l) — índice do BMU.
847
  accumulate_fisher: se True, acumula (x_w - W_w)² nos Fisher accumulators.
848
  """
849
+ # V6.5-V2-metrics-FIX-3 — sanitiza x: substitui NaN/Inf por 0
850
+ # (uma amostra degenerada não deve contaminar todo o SOM).
851
+ if torch.isnan(x).any() or torch.isinf(x).any():
852
+ x = torch.nan_to_num(x, nan=0.0, posinf=1e4, neginf=-1e4)
853
  dist_sq = self._neighborhood(bmu_idx)
854
+ # V6.5-V2-metrics-FIX-3 — floors explícitos (previnem σ_t→0)
855
+ sigma = max(self.sigma0 * math.exp(-self.t / 1000), 0.1)
856
+ alpha = max(self.alpha0 * math.exp(-self.t / 2000), 0.001)
857
  h = torch.exp(-dist_sq / (2 * sigma ** 2))
858
 
859
  delta = x - self.weights
 
868
  update[..., 3] = update[..., 3] - ewc_penalty
869
 
870
  self.weights = self.weights + update
871
+ # V6.5-V2-metrics-FIX-3 — clamp pós-update para evitar explosão
872
+ # (pesos Kohonen são tipicamente O(1); permitir valores > 100 indica
873
+ # divergência e propaga NaN para todas as métricas subsequentes).
874
+ self.weights = torch.clamp(self.weights, -100.0, 100.0)
875
+ # V6.5-V2-metrics-FIX-3 — se ainda assim surgir NaN, reverte este update
876
+ if torch.isnan(self.weights).any() or torch.isinf(self.weights).any():
877
+ self.weights = torch.nan_to_num(self.weights, nan=0.0, posinf=100.0, neginf=-100.0)
878
 
879
  if accumulate_fisher:
880
  # Acumula Fisher apenas em neurônios próximos ao BMU (Λ > 0.1)
 
886
 
887
  self.t += 1
888
 
889
+ # V6.5-V2-metrics-FIX-4 — atualiza conscience mechanism (win frequency)
890
+ # automaticamente após cada update. Isto garante que o bias de consciência
891
+ # seja aplicado corretamente no próximo find_bmu, forçando distribuição
892
+ # uniforme de BMU entre os 864 neurônios.
893
+ try:
894
+ self.update_win_frequency(bmu_idx)
895
+ except Exception:
896
+ # Não deixa falha no conscience quebrar o treino principal
897
+ pass
898
+
899
+ # V6.5-V2-auto-conscience — ajuste adaptativo de γ (throttled).
900
+ # Roda a cada _auto_adjust_interval updates (default 50) para
901
+ # evitar custo computacional em cada amostra e suavizar o ajuste.
902
+ try:
903
+ if self.t % self._auto_adjust_interval == 0:
904
+ self.auto_adjust_conscience_gamma()
905
+ except Exception:
906
+ # Não deixa falha no auto-ajuste quebrar o treino principal
907
+ pass
908
+
909
  def finalize_fisher(self):
910
  """Fisher = mean((x_w - W_w)²) sobre samples acumuladas."""
911
  cnt = self.fisher_count.clamp(min=1e-8)
 
925
  """Retorna métricas atuais do SOM para monitoramento."""
926
  sigma_t = self.sigma0 * math.exp(-self.t / 1000)
927
  alpha_t = self.alpha0 * math.exp(-self.t / 2000)
928
+ # V6.5-V2-metrics-FIX-4 — floors aplicados (consistência com update_weights)
929
+ sigma_t_eff = max(sigma_t, 0.1)
930
+ alpha_t_eff = max(alpha_t, 0.001)
931
+ # V6.5-V2-metrics-FIX-4 — estatísticas do conscience mechanism
932
+ n_total = self.n_neurons
933
+ n_active = int((self.bmu_win_count > 0).sum().item())
934
+ n_dead = n_total - n_active
935
+ win_counts_flat = self.bmu_win_count.flatten().float()
936
  return {
937
  "t": int(self.t),
938
  "sigma_t": float(sigma_t),
939
  "alpha_t": float(alpha_t),
940
+ "sigma_t_effective": float(sigma_t_eff),
941
+ "alpha_t_effective": float(alpha_t_eff),
942
  "sigma0": float(self.sigma0),
943
  "alpha0": float(self.alpha0),
944
  "lambda_ewc": float(self.lambda_ewc),
 
958
  "fisher_accum_count": int(self.fisher_count.sum().item()),
959
  "weights_norm": float(self.weights.norm().item()),
960
  "weights_w_mean": float(self.weights[..., 3].mean().item()),
961
+ # V6.5-V2-metrics-FIX-4 — conscience mechanism status
962
+ "conscience_gamma": float(self.conscience_gamma),
963
+ "conscience_beta": float(self.conscience_beta),
964
+ "n_active_neurons": n_active,
965
+ "n_dead_neurons": n_dead,
966
+ "neuron_activation_rate": float(n_active / max(1, n_total)),
967
+ "bmu_win_count_mean": float(win_counts_flat.mean().item()) if n_total > 0 else 0.0,
968
+ "bmu_win_count_max": float(win_counts_flat.max().item()) if n_total > 0 else 0.0,
969
+ "win_frequency_mean": float(self.win_frequency.mean().item()),
970
+ "win_frequency_max": float(self.win_frequency.max().item()),
971
+ "win_frequency_min": float(self.win_frequency.min().item()),
972
+ # V6.5-V2-auto-conscience — telemetria do ajuste adaptativo de γ
973
+ "conscience_gamma_min": float(self._conscience_gamma_min),
974
+ "conscience_gamma_max": float(self._conscience_gamma_max),
975
+ "conscience_gamma_target": float(self._last_gamma_target),
976
+ "conscience_gamma_drop": float(self._last_gamma_drop),
977
+ "conscience_gamma_eta": float(self._conscience_eta),
978
+ "conscience_gamma_adjust_interval": int(self._auto_adjust_interval),
979
+ "activation_rate_at_last_adjust": float(self._last_activation_rate),
980
+ "prev_n_active_at_last_adjust": int(self._prev_n_active),
981
  }
982
 
983
 
 
1047
  T_max=10000,
1048
  # V6.5 — VQ-VAE-2 + reasoning_engine
1049
  enable_vqvae2: bool = True,
1050
+ # V6.5-V2 — reasoning_engine desativado por padrão (user requirement:
1051
+ # "por enquanto manter reasoning_engine desativado"). O ReasoningEngine
1052
+ # cria ThreadPoolExecutor(4 workers) + ToolAgentCoordinator que consome
1053
+ # ~100-200MB adicionais e não afeta o aprendizado do SOM. Para reativar,
1054
+ # passe enable_reasoning=True explicitamente (requer ≥6GB cgroup).
1055
+ enable_reasoning: bool = False,
1056
  vqvae2_code_dim: int = 16,
1057
  vqvae2_num_codes_top: int = 64,
1058
  vqvae2_num_codes_bot: int = 128,
 
1564
  )
1565
  self.som.update_weights(x, bmu, accumulate_fisher=acc_fisher)
1566
 
1567
+ # V6.5-V2-metrics-FIX-3 — VQ-VAE-2 e W8A8 compressão dentro de
1568
+ # no_grad para evitar construção de grafo computacional (50-200MB
1569
+ # por batch desperdiçados, causa direta de OOM em runs longos).
1570
+ # O VQ-VAE-2 aqui é usado como compressor estatístico (não treinado
1571
+ # por backprop no loop principal), então no_grad é correto.
1572
  if self.enable_vqvae2 and self.vqvae2_compressor is not None:
1573
+ with torch.no_grad():
1574
+ self._compress_buffer_with_vqvae2(data)
1575
 
1576
  # V6.5-final — Ativa SmoothQuant W8A8 compressor dos pesos do SOM
1577
  # User requirement: "integrar SmoothQuantCompressor ao KLS para
1578
  # compressão W8A8 real dos pesos do SOM"
1579
  if self.enable_w8a8 and self.w8a8_compressor is not None:
1580
+ with torch.no_grad():
1581
+ self.compress_som_weights_w8a8()
1582
 
1583
  # V6.5-final — Reativa tool_coordinator workers via execute_parallel
1584
  # User requirement: "reativar reasoning_engine's tool_coordinator workers"
 
2928
  loss_total = deltas_stack.norm()
2929
 
2930
  loss_total.backward()
2931
+ # V6.5-V2-metrics-FIX-3 — gradient clipping no treino de hipóteses
2932
+ # previne explosão de gradientes em batches degenerados (ex: todos
2933
+ # os labels iguais → BCE produz gradientes grandes). max_norm=1.0
2934
+ # é o valor canônico recomendado pela literatura para BCE heads.
2935
+ torch.nn.utils.clip_grad_norm_(
2936
+ self.hypothesis_ensemble.parameters(), max_norm=1.0
2937
+ )
2938
+ # V6.5-V2-metrics-FIX-3 — zera grad do delta_scale (que não está
2939
+ # no optimizer mas aparece no grafo de forward, aculumando .grad
2940
+ # silenciosamente a cada step).
2941
+ if self.delta_scale.grad is not None:
2942
+ self.delta_scale.grad = None
2943
  self.hyp_optimizer.step()
2944
  losses.append(float(loss_total.item()))
2945
 
 
3295
  # Re-rotula neurônios (BMU labels podem ter mudado)
3296
  self._label_neurons()
3297
 
3298
+ # V6.5-V2-metrics-FIX-3 — se best_delta tinha NaN (detectado acima e
3299
+ # retornado early), NÃO chegamos aqui. Mas se chegamos via caminho
3300
+ # else (pesos ficaram NaN após aplicação e foram revertidos), NÃO
3301
+ # devemos consolidar via EWC — consolidar pesos potencialmente
3302
+ # corruptos como referência EWC propagaria o problema para o futuro.
3303
+ # Verificação: se os pesos atuais têm NaN, pula set_ewc_reference.
3304
+ weights_corrupt = torch.isnan(self.som.weights).any() or torch.isinf(self.som.weights).any()
3305
+ if weights_corrupt:
3306
+ import logging as _logging
3307
+ _logging.getLogger(__name__).error(
3308
+ f"[V6.5-V2-metrics-FIX-3] SOM weights contain NaN/Inf after "
3309
+ f"delta application — SKIPPING set_ewc_reference to avoid "
3310
+ f"propagating corruption. Weights will be sanitized on next "
3311
+ f"update_weights call."
3312
+ )
3313
+ # Sanitiza imediatamente para permitir continuidade do treino
3314
+ self.som.weights = torch.nan_to_num(
3315
+ self.som.weights, nan=0.0, posinf=100.0, neginf=-100.0
3316
+ )
3317
  ewc_set = False
3318
+ sel_info["ewc_skipped"] = "weights_corrupt_after_delta"
3319
+ else:
3320
+ # Consolida via EWC (Fisher information em w)
3321
+ try:
3322
+ self.som.set_ewc_reference()
3323
+ ewc_set = True
3324
+ except Exception as e:
3325
+ ewc_set = False
3326
+ sel_info["ewc_error"] = str(e)[:200]
3327
 
3328
  return {
3329
  "active": True,
 
3708
  "max_hyp_train_steps": int(self.max_hyp_train_steps),
3709
  }
3710
 
3711
+ # ==================================================================
3712
+ # V6.5-V2-metrics-FIX-4 — Conscience mechanism + Dead neuron revival
3713
+ # ==================================================================
3714
+ # User requirement (latest): "APRIMORAR (em ambas as FASE1 e FASE2):
3715
+ # analisar matematicamente e logicamente a ativação e uso e acesso dos
3716
+ # neurônios (apenas dois estão sendo ativados: neurons_active=2/864)
3717
+ # distribuindo o processamento paralelamente".
3718
+ #
3719
+ # Mathematical formulation (Conscience Mechanism — DeSieno 1988):
3720
+ #
3721
+ # Cada neurônio i mantém win_frequency p_i ∈ [0,1] (EMA, β=0.1).
3722
+ # BMU selection: bmu = argmin_i ( ||W_i - x||² - b_i )
3723
+ # onde b_i = γ · (1/N - p_i) é o conscience bias
3724
+ # γ = 0.1 (default), N = total de neurônios (864)
3725
+ #
3726
+ # Quando p_i > 1/N (neurônio ganha demais): b_i < 0 → distância
3727
+ # efetiva AUMENTA → neurônio é penalizado.
3728
+ # Quando p_i < 1/N (neurônio nunca ganha): b_i > 0 → distância
3729
+ # efetiva DIMINUI → neurônio é favorecido.
3730
+ #
3731
+ # Convergência: p_i → 1/N para todo i (distribuição uniforme de BMU),
3732
+ # garantindo que TODOS os 864 neurônios sejam utilizados.
3733
+ #
3734
+ # Dead neuron revival (complementar):
3735
+ # Se após K amostras um neurônio ainda tem win_count = 0, ele é
3736
+ # reinicializado para uma amostra aleatória do buffer (data-driven)
3737
+ # ou para N(0, 0.1) se buffer vazio. Isto acelera a diversificação
3738
+ # quando o conscience mechanism sozinho não basta.
3739
+ # ------------------------------------------------------------------
3740
+ def revive_dead_neurons(
3741
+ self,
3742
+ dead_threshold: int = 0,
3743
+ use_buffer: bool = True,
3744
+ ) -> Dict[str, Any]:
3745
+ """V6.5-V2-metrics-FIX-4 — Wrapper KLS para som.revive_dead_neurons.
3746
+
3747
+ Usa o buffer_4d atual do KLS como fonte de dados para reinicialização
3748
+ data-driven dos neurônios mortos.
3749
+
3750
+ Args:
3751
+ dead_threshold: neurônios com win_count <= threshold são revividos.
3752
+ use_buffer: se True, usa buffer_4d do KLS como fonte.
3753
+
3754
+ Returns:
3755
+ Dict com status do revival (n_revived, dead_rate_before/after, etc).
3756
+ """
3757
+ data_buffer = self.buffer_4d if use_buffer else None
3758
+ return self.som.revive_dead_neurons(
3759
+ data_buffer=data_buffer,
3760
+ dead_threshold=dead_threshold,
3761
+ )
3762
+
3763
+ def parallel_neuron_activation_report(self) -> Dict[str, Any]:
3764
+ """V6.5-V2-metrics-FIX-4 — Wrapper KLS para som.parallel_neuron_activation_report.
3765
+
3766
+ Retorna relatório estruturado de ativação dos 864 neurônios, análogo
3767
+ ao DataFrame do exemplo fornecido pelo usuário, mas otimizado para
3768
+ o grid 4D do SOM.
3769
+
3770
+ Returns:
3771
+ Dict com estatísticas detalhadas (n_active, n_dead, win_count
3772
+ distribution, conscience_bias stats, uniformity_score, etc).
3773
+ """
3774
+ return self.som.parallel_neuron_activation_report()
3775
+
3776
+ def auto_revive_if_needed(
3777
+ self,
3778
+ dead_rate_threshold: float = 0.5,
3779
+ min_steps_between_revivals: int = 200,
3780
+ ) -> Dict[str, Any]:
3781
+ """V6.5-V2-metrics-FIX-4 — Revive neurônios automaticamente se dead_rate alto.
3782
+
3783
+ Verifica o dead_rate atual e revive neurônios mortos se:
3784
+ - dead_rate > dead_rate_threshold (default 0.5 = 50% mortos)
3785
+ - pelo menos min_steps_between_revivals desde o último revival
3786
+
3787
+ Isto é chamado automaticamente pelo treinador após cada chunk,
3788
+ garantindo que o SOM mantenha distribuição uniforme de BMU ao longo
3789
+ do treino sem intervenção manual.
3790
+
3791
+ Args:
3792
+ dead_rate_threshold: limite para disparar revival (default 0.5).
3793
+ min_steps_between_revivals: cooldown em steps (default 200).
3794
+
3795
+ Returns:
3796
+ Dict com status do revival (ou action="skipped" se não disparou).
3797
+ """
3798
+ last_revival_step = getattr(self, "_last_revival_step", -min_steps_between_revivals)
3799
+ current_step = self.som.t
3800
+ steps_since_last = current_step - last_revival_step
3801
+
3802
+ # Computa dead rate atual
3803
+ report = self.som.parallel_neuron_activation_report()
3804
+ dead_rate = 1.0 - report["neuron_activation_rate"]
3805
+
3806
+ if dead_rate <= dead_rate_threshold:
3807
+ return {
3808
+ "action": "skipped",
3809
+ "reason": f"dead_rate={dead_rate:.3f} <= threshold={dead_rate_threshold}",
3810
+ "dead_rate": dead_rate,
3811
+ "n_active": report["n_active_neurons"],
3812
+ "n_total": report["n_total_neurons"],
3813
+ }
3814
+ if steps_since_last < min_steps_between_revivals:
3815
+ return {
3816
+ "action": "skipped",
3817
+ "reason": f"cooldown: only {steps_since_last} steps since last revival "
3818
+ f"(need {min_steps_between_revivals})",
3819
+ "dead_rate": dead_rate,
3820
+ "n_active": report["n_active_neurons"],
3821
+ "n_total": report["n_total_neurons"],
3822
+ }
3823
+
3824
+ # Dispara revival
3825
+ revival = self.revive_dead_neurons(
3826
+ dead_threshold=0,
3827
+ use_buffer=True,
3828
+ )
3829
+ self._last_revival_step = current_step
3830
+ revival["action"] = "auto_revived"
3831
+ revival["trigger_dead_rate"] = dead_rate
3832
+ revival["steps_since_last_revival"] = steps_since_last
3833
+ return revival
3834
+
3835
  # ==================================================================
3836
  # V6.5-V2-metrics — Integração das métricas SOM canônicas
3837
  # (QE, TE, Kaski-Lagus, Variância Explicada, Dead Neurons,
src/bigru_t/model/som_metrics.py CHANGED
@@ -100,24 +100,41 @@ class SOMMetricHistory:
100
  dead_neuron_rate_history: deque = field(default_factory=lambda: deque(maxlen=50))
101
  epoch: int = 0
102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  def record(self, metrics: Dict[str, Any]) -> None:
104
- """Registra métricas de uma época.
105
 
106
  Nota: `dead_neuron_rate` pode ser float (valor direto) ou Dict
107
  (saída de compute_all_metrics, com chave 'dead_neuron_rate').
108
  """
109
- self.qe_history.append(float(metrics.get("quantization_error", 0.0)))
110
- self.te_history.append(float(metrics.get("topological_error", 0.0)))
111
- self.kl_history.append(float(metrics.get("kaski_lagus_error", 0.0)))
112
  self.var_explained_history.append(
113
- float(metrics.get("explained_variance_share", 0.0))
114
  )
115
  # dead_neuron_rate pode ser float ou Dict
116
  dnr = metrics.get("dead_neuron_rate", 0.0)
117
  if isinstance(dnr, dict):
118
- dnr_val = float(dnr.get("dead_neuron_rate", 0.0))
119
  else:
120
- dnr_val = float(dnr)
121
  self.dead_neuron_rate_history.append(dnr_val)
122
  self.epoch += 1
123
 
@@ -214,9 +231,8 @@ def quantization_error(
214
  weights: tensor [I, J, K, L, 4] — pesos do SOM.
215
 
216
  Returns:
217
- QE (float). 0.0 se data vazio. NaN propagado é substituído por 0.0
218
- (V6.5-V2-metrics-FIX-2: user requirement "investigar QE e KL resultando
219
- em NAN").
220
  """
221
  if data.numel() == 0:
222
  return 0.0
@@ -230,8 +246,8 @@ def quantization_error(
230
  min_dist_sq = torch.clamp(min_dist_sq, min=0.0)
231
  qe = torch.mean(torch.sqrt(min_dist_sq))
232
  qe_val = float(qe.item())
233
- # NaN check final (defense in depth)
234
- if qe_val != qe_val: # NaN
235
  return 0.0
236
  return qe_val
237
 
@@ -251,14 +267,22 @@ def topological_error(
251
  weights: tensor [I, J, K, L, 4]
252
 
253
  Returns:
254
- TE ∈ [0, 1]. 0.0 se data vazio.
255
  """
256
  if data.numel() == 0:
257
  return 0.0
 
 
 
 
 
258
  grid_shape = weights.shape[:-1]
259
  flat_w = weights.view(-1, 4)
260
  diff = data.unsqueeze(1) - flat_w.unsqueeze(0)
261
  dist_sq = torch.sum(diff * diff, dim=-1) # (N, M)
 
 
 
262
 
263
  # Top-2 BMUs para cada amostra
264
  top2 = torch.topk(dist_sq, k=2, largest=False, dim=1)
@@ -289,6 +313,9 @@ def topological_error(
289
  is_adjacent = (delta.sum(dim=1) == 1) & (delta.max(dim=1).values == 1)
290
 
291
  te = 1.0 - float(is_adjacent.float().mean().item())
 
 
 
292
  return te
293
 
294
 
@@ -321,16 +348,21 @@ def kaski_lagus_error(
321
  return 0.0
322
  qe = quantization_error(data, weights)
323
  te = topological_error(data, weights)
324
- # Normalização: escala típica dos dados
325
- data_norm = float(torch.mean(torch.norm(data, dim=1)).item())
 
 
 
 
 
326
  # V6.5-V2-metrics-FIX-2: se data_norm é NaN ou zero, usa 1.0 como fallback
327
- if data_norm != data_norm or data_norm < 1e-8: # NaN or ~0
328
  data_norm = 1.0
329
  qe_norm = qe / max(data_norm, 1e-8)
330
  kl = alpha * qe_norm + (1.0 - alpha) * te
331
  kl_val = float(kl)
332
- # NaN check final
333
- if kl_val != kl_val: # NaN
334
  return 0.0
335
  return kl_val
336
 
@@ -366,10 +398,17 @@ def explained_variance_share(
366
 
367
  total_var = float(torch.sum(var_data).item())
368
  resid_var = float(torch.sum(var_resid).item())
 
 
 
369
  if total_var < 1e-12:
370
- return 0.0
371
  ve = 1.0 - (resid_var / total_var)
372
- return float(max(0.0, min(1.0, ve)))
 
 
 
 
373
 
374
 
375
  # ============================================================================
@@ -429,6 +468,16 @@ def topological_collapse(weights: torch.Tensor) -> Dict[str, Any]:
429
  return {"collapsed_to_point": False, "collapsed_to_line": False,
430
  "effective_rank": 0.0, "singular_values": [], "severity": "none"}
431
 
 
 
 
 
 
 
 
 
 
 
432
  s_max = max(S_list[0] if S_list else 0.0, 1e-12)
433
 
434
  # Effective rank: número de singular values > 1% do maior
@@ -493,7 +542,12 @@ def dead_neuron_rate(
493
  flat_w = weights.view(-1, 4)
494
  diff = data.unsqueeze(1) - flat_w.unsqueeze(0)
495
  dist_sq = torch.sum(diff * diff, dim=-1) # (N, M)
 
 
 
496
  _, bmu_flat = torch.min(dist_sq, dim=1) # (N,)
 
 
497
 
498
  active_mask = torch.zeros(n_total, dtype=torch.bool)
499
  active_mask[bmu_flat] = True
@@ -566,8 +620,12 @@ def quantization_error_stagnation(
566
 
567
  current_qe = float(qe_list[-1])
568
  max_qe = float(max(qe_list))
 
 
 
 
569
  is_high = current_qe > high_threshold_frac * max_qe
570
- is_stable = abs(slope) < slope_tolerance
571
 
572
  is_stagnant = is_stable and is_high
573
 
@@ -630,6 +688,17 @@ def neighborhood_crossing(
630
  initial = te_list[:initial_window]
631
  final = te_list[-final_window:]
632
 
 
 
 
 
 
 
 
 
 
 
 
633
  init_mean = sum(initial) / len(initial)
634
  final_mean = sum(final) / len(final)
635
  init_var = sum((x - init_mean) ** 2 for x in initial) / len(initial)
@@ -744,29 +813,33 @@ def compute_all_metrics(
744
 
745
  try:
746
  result = _compute_all_metrics_impl(data, weights, history)
747
- # V6.5-V2-metrics-FIX-2: pós-computação, verifica se QE ou KL é NaN.
748
- # Se sim, registra warning e substitui por 0.0 (para não quebrar logs JSON
749
- # e nem contaminar o histórico de estagnação/cruzamento).
750
- qe_val = result.get("quantization_error", 0.0)
751
- kl_val = result.get("kaski_lagus_error", 0.0)
752
- if isinstance(qe_val, float) and (qe_val != qe_val): # NaN check
753
- logger.warning(
754
- f"[som_metrics] QE computed as NaN — replacing with 0.0 "
755
- f"(indicates degenerate SOM state). Investigate train_hypotheses "
756
- f"or apply_best_delta for gradient explosion."
757
- )
758
- result["quantization_error"] = 0.0
759
- result["qe_was_nan"] = True
760
- # KL depende de QE, então também será NaN
761
- result["kaski_lagus_error"] = 0.0
762
- result["kl_was_nan"] = True
763
- elif isinstance(kl_val, float) and (kl_val != kl_val): # NaN check
764
- logger.warning(
765
- f"[som_metrics] KL computed as NaN (QE was fine) — replacing with 0.0. "
766
- f"Usually due to zero-norm data in kaski_lagus normalization."
767
- )
768
- result["kaski_lagus_error"] = 0.0
769
- result["kl_was_nan"] = True
 
 
 
 
770
  return result
771
  except Exception as e:
772
  logger.warning(
 
100
  dead_neuron_rate_history: deque = field(default_factory=lambda: deque(maxlen=50))
101
  epoch: int = 0
102
 
103
+ @staticmethod
104
+ def _safe_float(v: Any, default: float = 0.0) -> float:
105
+ """Converte para float, substituindo NaN/Inf por default.
106
+
107
+ V6.5-V2-metrics-FIX-3: Um único NaN no histórico contamina
108
+ permanentemente a detecção de estagnação (regressão linear)
109
+ e de cruzamento de vizinhança (média/desvio-padrão). Esta
110
+ barreira garante que somente valores finitos entrem no deque.
111
+ """
112
+ try:
113
+ f = float(v)
114
+ except (TypeError, ValueError):
115
+ return default
116
+ if not math.isfinite(f):
117
+ return default
118
+ return f
119
+
120
  def record(self, metrics: Dict[str, Any]) -> None:
121
+ """Registra métricas de uma época (NaN/Inf são substituídos por 0.0).
122
 
123
  Nota: `dead_neuron_rate` pode ser float (valor direto) ou Dict
124
  (saída de compute_all_metrics, com chave 'dead_neuron_rate').
125
  """
126
+ self.qe_history.append(self._safe_float(metrics.get("quantization_error", 0.0)))
127
+ self.te_history.append(self._safe_float(metrics.get("topological_error", 0.0)))
128
+ self.kl_history.append(self._safe_float(metrics.get("kaski_lagus_error", 0.0)))
129
  self.var_explained_history.append(
130
+ self._safe_float(metrics.get("explained_variance_share", 0.0))
131
  )
132
  # dead_neuron_rate pode ser float ou Dict
133
  dnr = metrics.get("dead_neuron_rate", 0.0)
134
  if isinstance(dnr, dict):
135
+ dnr_val = self._safe_float(dnr.get("dead_neuron_rate", 0.0))
136
  else:
137
+ dnr_val = self._safe_float(dnr)
138
  self.dead_neuron_rate_history.append(dnr_val)
139
  self.epoch += 1
140
 
 
231
  weights: tensor [I, J, K, L, 4] — pesos do SOM.
232
 
233
  Returns:
234
+ QE (float). 0.0 se data vazio. NaN/Inf propagado é substituído por 0.0
235
+ (V6.5-V2-metrics-FIX-3: agora também captura Inf, não apenas NaN).
 
236
  """
237
  if data.numel() == 0:
238
  return 0.0
 
246
  min_dist_sq = torch.clamp(min_dist_sq, min=0.0)
247
  qe = torch.mean(torch.sqrt(min_dist_sq))
248
  qe_val = float(qe.item())
249
+ # V6.5-V2-metrics-FIX-3: rejeita tanto NaN quanto Inf
250
+ if not math.isfinite(qe_val):
251
  return 0.0
252
  return qe_val
253
 
 
267
  weights: tensor [I, J, K, L, 4]
268
 
269
  Returns:
270
+ TE ∈ [0, 1]. 0.0 se data vazio ou se grid tem < 2 neurônios.
271
  """
272
  if data.numel() == 0:
273
  return 0.0
274
+ # V6.5-V2-metrics-FIX-3: guard contra grid com < 2 neurônios
275
+ # (torch.topk(k=2) levanta RuntimeError se M < 2).
276
+ M = weights.shape[0] * weights.shape[1] * weights.shape[2] * weights.shape[3]
277
+ if M < 2:
278
+ return 0.0
279
  grid_shape = weights.shape[:-1]
280
  flat_w = weights.view(-1, 4)
281
  diff = data.unsqueeze(1) - flat_w.unsqueeze(0)
282
  dist_sq = torch.sum(diff * diff, dim=-1) # (N, M)
283
+ # V6.5-V2-metrics-FIX-3: substitui NaN/Inf por +inf antes do topk
284
+ # para que neurônios com pesos degenerados não sejam eleitos BMU.
285
+ dist_sq = torch.where(torch.isfinite(dist_sq), dist_sq, torch.full_like(dist_sq, float('inf')))
286
 
287
  # Top-2 BMUs para cada amostra
288
  top2 = torch.topk(dist_sq, k=2, largest=False, dim=1)
 
313
  is_adjacent = (delta.sum(dim=1) == 1) & (delta.max(dim=1).values == 1)
314
 
315
  te = 1.0 - float(is_adjacent.float().mean().item())
316
+ # V6.5-V2-metrics-FIX-3: rejeita NaN/Inf no resultado
317
+ if not math.isfinite(te):
318
+ return 0.0
319
  return te
320
 
321
 
 
348
  return 0.0
349
  qe = quantization_error(data, weights)
350
  te = topological_error(data, weights)
351
+ # V6.5-V2-metrics-FIX-3: normalização canônica por desvio em torno do
352
+ # centróide (mean(||x_i - x̄||)), NÃO por mean(||x_i||). A versão antiga
353
+ # era sensível a translação dos dados — deslocar todos os vetores por +c
354
+ # inflava artificialmente ||x|| e reduzia KL, mesmo com QE/TE inalterados.
355
+ centroid = data.mean(dim=0, keepdim=True) # (1, 4)
356
+ centered_norm = torch.norm(data - centroid, dim=1) # (N,)
357
+ data_norm = float(centered_norm.mean().item())
358
  # V6.5-V2-metrics-FIX-2: se data_norm é NaN ou zero, usa 1.0 como fallback
359
+ if not math.isfinite(data_norm) or data_norm < 1e-8:
360
  data_norm = 1.0
361
  qe_norm = qe / max(data_norm, 1e-8)
362
  kl = alpha * qe_norm + (1.0 - alpha) * te
363
  kl_val = float(kl)
364
+ # V6.5-V2-metrics-FIX-3: rejeita tanto NaN quanto Inf
365
+ if not math.isfinite(kl_val):
366
  return 0.0
367
  return kl_val
368
 
 
398
 
399
  total_var = float(torch.sum(var_data).item())
400
  resid_var = float(torch.sum(var_resid).item())
401
+ # V6.5-V2-metrics-FIX-3: se dados são constantes (var~0) e o SOM os
402
+ # representa perfeitamente (resid~0), a variância explicada é 1.0
403
+ # (caso degenerado, mas correto). Antes retornava 0.0 erroneamente.
404
  if total_var < 1e-12:
405
+ return 1.0 if resid_var < 1e-12 else 0.0
406
  ve = 1.0 - (resid_var / total_var)
407
+ ve_val = float(max(0.0, min(1.0, ve)))
408
+ # V6.5-V2-metrics-FIX-3: rejeita NaN/Inf
409
+ if not math.isfinite(ve_val):
410
+ return 0.0
411
+ return ve_val
412
 
413
 
414
  # ============================================================================
 
468
  return {"collapsed_to_point": False, "collapsed_to_line": False,
469
  "effective_rank": 0.0, "singular_values": [], "severity": "none"}
470
 
471
+ # V6.5-V2-metrics-FIX-3: se SVD retornou NaN/Inf (pesos degenerados),
472
+ # marca explicitamente como corrupção por NaN para não mascarar o problema.
473
+ if any(not math.isfinite(s) for s in S_list):
474
+ return {
475
+ "collapsed_to_point": False, "collapsed_to_line": False,
476
+ "effective_rank": 0.0, "singular_values": S_list,
477
+ "severity": "nan_corrupt",
478
+ "error": "SVD returned NaN/Inf singular values (weights contain NaN/Inf)",
479
+ }
480
+
481
  s_max = max(S_list[0] if S_list else 0.0, 1e-12)
482
 
483
  # Effective rank: número de singular values > 1% do maior
 
542
  flat_w = weights.view(-1, 4)
543
  diff = data.unsqueeze(1) - flat_w.unsqueeze(0)
544
  dist_sq = torch.sum(diff * diff, dim=-1) # (N, M)
545
+ # V6.5-V2-metrics-FIX-3: substitui NaN/Inf por +inf para que neurônios
546
+ # degenerados não sejam eleitos BMU e não causem IndexError.
547
+ dist_sq = torch.where(torch.isfinite(dist_sq), dist_sq, torch.full_like(dist_sq, float('inf')))
548
  _, bmu_flat = torch.min(dist_sq, dim=1) # (N,)
549
+ # V6.5-V2-metrics-FIX-3: valida range dos índices antes de indexar
550
+ bmu_flat = torch.clamp(bmu_flat, 0, n_total - 1)
551
 
552
  active_mask = torch.zeros(n_total, dtype=torch.bool)
553
  active_mask[bmu_flat] = True
 
620
 
621
  current_qe = float(qe_list[-1])
622
  max_qe = float(max(qe_list))
623
+ # V6.5-V2-metrics-FIX-3: tolerância RELATIVA à magnitude do QE,
624
+ # não absoluta. Para QE~0.01, slope_tolerance=1e-4 representaria
625
+ # 1% por época (grande); para QE~100, seria negligenciável.
626
+ rel_tol = slope_tolerance * max(1.0, max_qe)
627
  is_high = current_qe > high_threshold_frac * max_qe
628
+ is_stable = abs(slope) < rel_tol
629
 
630
  is_stagnant = is_stable and is_high
631
 
 
688
  initial = te_list[:initial_window]
689
  final = te_list[-final_window:]
690
 
691
+ # V6.5-V2-metrics-FIX-3: filtra NaN/Inf (defesa em profundidade — o
692
+ # SOMMetricHistory.record já deveria ter feito isso, mas chamadas
693
+ # diretas a neighborhood_crossing podem receber histórico corrupto).
694
+ initial = [v for v in initial if math.isfinite(v)]
695
+ final = [v for v in final if math.isfinite(v)]
696
+ if len(initial) < 2 or len(final) < 2:
697
+ return {"detected": False, "te_initial_mean": 0.0, "te_final_mean": 0.0,
698
+ "te_initial_std": 0.0, "te_final_std": 0.0, "std_ratio": 0.0,
699
+ "is_oscillating": False, "is_growing": False, "severity": "none",
700
+ "n_epochs": len(te_list)}
701
+
702
  init_mean = sum(initial) / len(initial)
703
  final_mean = sum(final) / len(final)
704
  init_var = sum((x - init_mean) ** 2 for x in initial) / len(initial)
 
813
 
814
  try:
815
  result = _compute_all_metrics_impl(data, weights, history)
816
+ # V6.5-V2-metrics-FIX-3: pós-computação, verifica TODAS as métricas
817
+ # escalares por NaN/Inf (não apenas QE/KL). Substitui por 0.0 e marca
818
+ # para que o histórico não seja contaminado.
819
+ _SCALAR_METRIC_KEYS = [
820
+ "quantization_error", "topological_error", "kaski_lagus_error",
821
+ "explained_variance_share",
822
+ ]
823
+ any_nan = False
824
+ for k in _SCALAR_METRIC_KEYS:
825
+ v = result.get(k, 0.0)
826
+ if isinstance(v, float) and not math.isfinite(v):
827
+ logger.warning(
828
+ f"[som_metrics] {k} computed as NaN/Inf ({v}) — replacing "
829
+ f"with 0.0. Investigate train_hypotheses or apply_best_delta "
830
+ f"for gradient explosion."
831
+ )
832
+ result[k] = 0.0
833
+ result[f"{k}_was_nan"] = True
834
+ any_nan = True
835
+ # V6.5-V2-metrics-FIX-3: também verifica sub-dicts de collapse/dead
836
+ # para severity='nan_corrupt' e propaga como failure_indicator.
837
+ if result.get("topological_collapse", {}).get("severity") == "nan_corrupt":
838
+ any_nan = True
839
+ if any_nan and "failure_indicators" in result:
840
+ result["failure_indicators"].append("post_compute_nan_replaced")
841
+ result["overall_health"] = "critical"
842
+ result["n_failure_indicators"] = len(result["failure_indicators"])
843
  return result
844
  except Exception as e:
845
  logger.warning(