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 +443 -62
- src/bigru_t/model/kohonen_learning_system.py +689 -20
- src/bigru_t/model/som_metrics.py +117 -44
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-
|
| 146 |
-
#
|
| 147 |
-
#
|
| 148 |
-
#
|
| 149 |
-
#
|
| 150 |
-
#
|
| 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:
|
| 164 |
-
# Attention: 4 ×
|
| 165 |
-
# HypothesisEnsemble
|
| 166 |
-
# Adam state (ensemble): 2
|
| 167 |
-
# HypothesisClassifier: ~616K
|
| 168 |
-
# VQ-VAE-2: ~50K
|
| 169 |
-
# SOM weights: 864
|
| 170 |
# Buffer (256 samples): ~4KB
|
| 171 |
-
# Total model: ~
|
| 172 |
# + Python + PyTorch + HF datasets cache: ~1-2GB
|
| 173 |
-
# + Training intermediates: ~200MB
|
| 174 |
-
# Total: ~
|
| 175 |
-
HIDDEN_DIM =
|
| 176 |
-
VOCAB_SIZE =
|
| 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 |
-
|
| 182 |
-
|
|
|
|
|
|
|
|
|
|
| 183 |
DIM_CHOICE = "y"
|
| 184 |
|
| 185 |
-
# V2-dynamic — HypothesisEnsemble parameters (
|
| 186 |
-
N_HYPOTHESES =
|
| 187 |
-
MAX_N_HYPOTHESES =
|
| 188 |
-
MIN_N_HYPOTHESES = 4
|
| 189 |
-
N_TRIALS = 3
|
| 190 |
MIN_N_TRIALS = 1
|
| 191 |
MAX_N_TRIALS = 6
|
| 192 |
-
HYP_TRAIN_STEPS = 30
|
| 193 |
MIN_HYP_TRAIN_STEPS = 10
|
| 194 |
MAX_HYP_TRAIN_STEPS = 80
|
| 195 |
HYP_LR = 1e-4
|
| 196 |
-
HYP_HIDDEN_DIM =
|
| 197 |
LOSS_HISTORY_WINDOW = 8
|
| 198 |
PUNISHMENT_WINDOW = 12
|
| 199 |
|
| 200 |
# V2-dynamic-memory — Buffer sliding window (evita OOM em treino longo)
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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-
|
| 216 |
-
#
|
| 217 |
-
#
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 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(
|
| 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(
|
| 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(
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 1640 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 360 |
-
sigma0: float =
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 413 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 —
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1063 |
if self.enable_vqvae2 and self.vqvae2_compressor is not None:
|
| 1064 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
#
|
| 2776 |
-
|
| 2777 |
-
|
| 2778 |
-
|
| 2779 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2780 |
ewc_set = False
|
| 2781 |
-
sel_info["
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 110 |
-
self.te_history.append(
|
| 111 |
-
self.kl_history.append(
|
| 112 |
self.var_explained_history.append(
|
| 113 |
-
|
| 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 =
|
| 119 |
else:
|
| 120 |
-
dnr_val =
|
| 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-
|
| 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 |
-
#
|
| 234 |
-
if
|
| 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 |
-
#
|
| 325 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
# V6.5-V2-metrics-FIX-2: se data_norm é NaN ou zero, usa 1.0 como fallback
|
| 327 |
-
if
|
| 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
|
| 333 |
-
if
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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) <
|
| 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-
|
| 748 |
-
#
|
| 749 |
-
#
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
)
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|