V6.5-V2-metrics-FIX: upload batch (scripts + modules) [7 files] — restored params + memory fixes
Browse files- kohonen_learning_system.py +335 -87
- smoke_test_v65_v2_metrics_fix.py +320 -0
- som_metrics.py +811 -0
- train_v6_5_v2.py +593 -118
- xeon_runtime.py +581 -0
kohonen_learning_system.py
CHANGED
|
@@ -1964,6 +1964,11 @@ class KohonenLearningSystemV2(KohonenLearningSystem):
|
|
| 1964 |
self._adaptation_log: List[Dict[str, Any]] = []
|
| 1965 |
self._last_adaptation_step = 0
|
| 1966 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1967 |
def _init_hypothesis_ensemble(self):
|
| 1968 |
"""Cria o ensemble de geradores + otimizador Adam."""
|
| 1969 |
input_dim = self.som_neuron_count # ativação SOM flatten (864)
|
|
@@ -2125,6 +2130,10 @@ class KohonenLearningSystemV2(KohonenLearningSystem):
|
|
| 2125 |
"punishment_rate": float(rate),
|
| 2126 |
}
|
| 2127 |
self._adaptation_log.append(record)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2128 |
self._last_adaptation_step += 1
|
| 2129 |
return record
|
| 2130 |
|
|
@@ -2132,6 +2141,91 @@ class KohonenLearningSystemV2(KohonenLearningSystem):
|
|
| 2132 |
"""Retorna o log de adaptações dinâmicas (para monitoramento)."""
|
| 2133 |
return list(self._adaptation_log)
|
| 2134 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2135 |
# ==================================================================
|
| 2136 |
# 2.1 Treinamento dos geradores (train_hypotheses)
|
| 2137 |
# ==================================================================
|
|
@@ -2181,6 +2275,11 @@ class KohonenLearningSystemV2(KohonenLearningSystem):
|
|
| 2181 |
|
| 2182 |
self.hypothesis_ensemble.train()
|
| 2183 |
losses = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2184 |
for step in range(self.hyp_train_steps):
|
| 2185 |
self.hyp_optimizer.zero_grad()
|
| 2186 |
|
|
@@ -2191,21 +2290,56 @@ class KohonenLearningSystemV2(KohonenLearningSystem):
|
|
| 2191 |
deltas_stack = self.hypothesis_ensemble.forward_stacked(x_mean)
|
| 2192 |
deltas_stack = self.delta_scale * deltas_stack # escala
|
| 2193 |
|
| 2194 |
-
#
|
|
|
|
|
|
|
| 2195 |
if self.classifier is not None and self.classifier_trained:
|
| 2196 |
-
|
| 2197 |
-
|
| 2198 |
-
|
| 2199 |
-
|
| 2200 |
-
|
| 2201 |
-
|
| 2202 |
-
|
| 2203 |
-
|
| 2204 |
-
|
| 2205 |
-
|
| 2206 |
-
#
|
| 2207 |
-
|
| 2208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2209 |
else:
|
| 2210 |
# Fallback: minimizar norma do delta (regulariza��ão pura)
|
| 2211 |
loss_total = deltas_stack.norm()
|
|
@@ -2214,6 +2348,12 @@ class KohonenLearningSystemV2(KohonenLearningSystem):
|
|
| 2214 |
self.hyp_optimizer.step()
|
| 2215 |
losses.append(float(loss_total.item()))
|
| 2216 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2217 |
# Atualiza a escala de delta (decai suavemente)
|
| 2218 |
with torch.no_grad():
|
| 2219 |
self.delta_scale.data = torch.clamp(
|
|
@@ -2221,6 +2361,10 @@ class KohonenLearningSystemV2(KohonenLearningSystem):
|
|
| 2221 |
)
|
| 2222 |
|
| 2223 |
self.hypothesis_ensemble.eval()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2224 |
|
| 2225 |
# V6.5-V2-dynamic — registra loss_final no histórico
|
| 2226 |
loss_final_val = float(losses[-1]) if losses else 0.0
|
|
@@ -2249,56 +2393,80 @@ class KohonenLearningSystemV2(KohonenLearningSystem):
|
|
| 2249 |
def _compute_classification_loss_with_som(
|
| 2250 |
self, som_weights: torch.Tensor, data: torch.Tensor, labels: torch.Tensor
|
| 2251 |
) -> torch.Tensor:
|
| 2252 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2253 |
|
| 2254 |
-
|
| 2255 |
-
|
| 2256 |
-
|
| 2257 |
-
|
| 2258 |
-
"""
|
| 2259 |
-
# Guarda o SOM original
|
| 2260 |
-
original_weights = self.som.weights
|
| 2261 |
-
# Substitui temporariamente (sem clone — som_weights já é um tensor
|
| 2262 |
-
# que carrega gradiente dos deltas)
|
| 2263 |
-
self.som.weights = som_weights
|
| 2264 |
|
| 2265 |
-
|
| 2266 |
-
|
| 2267 |
-
|
| 2268 |
-
|
| 2269 |
-
|
| 2270 |
-
|
| 2271 |
-
|
| 2272 |
-
|
| 2273 |
-
|
| 2274 |
-
|
| 2275 |
-
|
| 2276 |
-
|
| 2277 |
-
|
| 2278 |
-
|
| 2279 |
-
|
| 2280 |
-
|
| 2281 |
-
|
| 2282 |
-
|
| 2283 |
-
|
| 2284 |
-
|
| 2285 |
-
|
| 2286 |
-
|
| 2287 |
-
|
| 2288 |
-
|
| 2289 |
-
|
| 2290 |
-
|
| 2291 |
-
|
| 2292 |
-
|
| 2293 |
-
|
| 2294 |
-
|
| 2295 |
-
|
| 2296 |
-
|
| 2297 |
-
|
| 2298 |
-
|
| 2299 |
-
|
| 2300 |
-
|
| 2301 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2302 |
|
| 2303 |
# ==================================================================
|
| 2304 |
# 2.2 Seleção do melhor delta (3 tentativas × 16 hipóteses)
|
|
@@ -2398,32 +2566,47 @@ class KohonenLearningSystemV2(KohonenLearningSystem):
|
|
| 2398 |
def _evaluate_accuracy_with_som(
|
| 2399 |
self, som_weights: torch.Tensor, data: torch.Tensor, labels: torch.Tensor
|
| 2400 |
) -> float:
|
| 2401 |
-
"""Calcula acurácia com
|
| 2402 |
-
|
| 2403 |
-
|
| 2404 |
-
|
| 2405 |
-
|
| 2406 |
-
|
| 2407 |
-
|
| 2408 |
-
|
| 2409 |
-
|
| 2410 |
-
|
| 2411 |
-
|
| 2412 |
-
|
| 2413 |
-
|
| 2414 |
-
#
|
| 2415 |
-
|
| 2416 |
-
|
| 2417 |
-
|
| 2418 |
-
|
| 2419 |
-
|
| 2420 |
-
|
| 2421 |
-
|
| 2422 |
-
|
| 2423 |
-
acc = correct / max(1, len(data))
|
| 2424 |
return float(acc)
|
| 2425 |
-
|
| 2426 |
-
self.som.weights
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2427 |
|
| 2428 |
# ==================================================================
|
| 2429 |
# 2.3 Aplicação do melhor delta + consolidação via EWC
|
|
@@ -2653,6 +2836,69 @@ class KohonenLearningSystemV2(KohonenLearningSystem):
|
|
| 2653 |
},
|
| 2654 |
}
|
| 2655 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2656 |
|
| 2657 |
__all__ = [
|
| 2658 |
"SimpleBBPETokenizer",
|
|
@@ -2665,3 +2911,5 @@ __all__ = [
|
|
| 2665 |
"HypothesisEnsemble",
|
| 2666 |
"KohonenLearningSystemV2",
|
| 2667 |
]
|
|
|
|
|
|
|
|
|
| 1964 |
self._adaptation_log: List[Dict[str, Any]] = []
|
| 1965 |
self._last_adaptation_step = 0
|
| 1966 |
|
| 1967 |
+
# V6.5-V2-metrics — histórico de métricas SOM (QE, TE, KL, VE, Dead Neurons)
|
| 1968 |
+
# para detecção de Estagnação do QE e Cruzamento de Vizinhança.
|
| 1969 |
+
from bigru_t.model.som_metrics import SOMMetricHistory
|
| 1970 |
+
self._som_metric_history: SOMMetricHistory = SOMMetricHistory()
|
| 1971 |
+
|
| 1972 |
def _init_hypothesis_ensemble(self):
|
| 1973 |
"""Cria o ensemble de geradores + otimizador Adam."""
|
| 1974 |
input_dim = self.som_neuron_count # ativação SOM flatten (864)
|
|
|
|
| 2130 |
"punishment_rate": float(rate),
|
| 2131 |
}
|
| 2132 |
self._adaptation_log.append(record)
|
| 2133 |
+
# V6.5-V2-metrics-FIX: limita crescimento do log (era ilimitado, causando
|
| 2134 |
+
# crescimento de memória em treino longo). Mantém últimas 50 entradas.
|
| 2135 |
+
if len(self._adaptation_log) > 50:
|
| 2136 |
+
self._adaptation_log = self._adaptation_log[-50:]
|
| 2137 |
self._last_adaptation_step += 1
|
| 2138 |
return record
|
| 2139 |
|
|
|
|
| 2141 |
"""Retorna o log de adaptações dinâmicas (para monitoramento)."""
|
| 2142 |
return list(self._adaptation_log)
|
| 2143 |
|
| 2144 |
+
# ==================================================================
|
| 2145 |
+
# V6.5-V2-metrics-FIX — Limpeza agressiva de memória RAM
|
| 2146 |
+
# ==================================================================
|
| 2147 |
+
# User requirement: "analisar e aprimorar (logicamente e matematicamente)
|
| 2148 |
+
# no projeto limpeza de memória RAM e de Armazenamento (ao concluir)".
|
| 2149 |
+
#
|
| 2150 |
+
# Mathematical analysis:
|
| 2151 |
+
# A memória RAM consumida pelo KLS V2 tem 4 fontes principais:
|
| 2152 |
+
# 1. Pesos do SOM: I*J*K*L*4 floats = 864*4 = 3456 floats (~14KB)
|
| 2153 |
+
# → FIXO (não pode ser limpo sem destruir o modelo)
|
| 2154 |
+
# 2. HypothesisEnsemble: max_n_hypotheses * DeltaGenerator params
|
| 2155 |
+
# = 32 * (864*256 + 256*256 + 256*3456) = 32 * 1.16M = ~37M params
|
| 2156 |
+
# → ~148MB em float32. Os OTIMIZADORAS Adam guardam 2x esse valor
|
| 2157 |
+
# (momentum + variance) = ~296MB adicionais.
|
| 2158 |
+
# 3. HypothesisClassifier: 864*512 + 512*256 + ... = ~616K params (~2.5MB)
|
| 2159 |
+
# 4. buffer_4d: até MAX_BUFFER_SIZE amostras * 4 floats = ~4KB (mínimo)
|
| 2160 |
+
# MAS os tensores carregam gradiente do embedding se não forem detached.
|
| 2161 |
+
#
|
| 2162 |
+
# A limpeza agressiva atua em:
|
| 2163 |
+
# - Zera gradientes acumulados nos otimizadores (Adam state)
|
| 2164 |
+
# - Chama gc.collect() para liberar ciclos Python
|
| 2165 |
+
# - Libera caches internos do PyTorch (oneDNN primitive cache)
|
| 2166 |
+
# - Trunca listas de histórico que crescem com o treino
|
| 2167 |
+
# ------------------------------------------------------------------
|
| 2168 |
+
def aggressive_cleanup(self) -> Dict[str, Any]:
|
| 2169 |
+
"""V6.5-V2-metrics-FIX — Limpeza agressiva de memória RAM do KLS.
|
| 2170 |
+
|
| 2171 |
+
Retorna métricas antes/depois para confirmação. NÃO destroi pesos do
|
| 2172 |
+
SOM nem do classifier — apenas libera caches e gradientes acumulados.
|
| 2173 |
+
|
| 2174 |
+
Returns:
|
| 2175 |
+
Dict com rss_before/after_mb, n_refs_collected, buffers_trimmed.
|
| 2176 |
+
"""
|
| 2177 |
+
import gc
|
| 2178 |
+
try:
|
| 2179 |
+
import resource
|
| 2180 |
+
rss_before = float(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss) / 1024.0
|
| 2181 |
+
except Exception:
|
| 2182 |
+
rss_before = 0.0
|
| 2183 |
+
|
| 2184 |
+
# 1. Zera gradientes do otimizador Adam (libera momentum/variance state)
|
| 2185 |
+
try:
|
| 2186 |
+
if hasattr(self, "hyp_optimizer"):
|
| 2187 |
+
self.hyp_optimizer.zero_grad(set_to_none=True)
|
| 2188 |
+
except Exception:
|
| 2189 |
+
pass
|
| 2190 |
+
|
| 2191 |
+
# 2. Trunca históricos que crescem com o treino
|
| 2192 |
+
if len(self._adaptation_log) > 20:
|
| 2193 |
+
self._adaptation_log = self._adaptation_log[-20:]
|
| 2194 |
+
if len(self.inference_punishment_history) > 50:
|
| 2195 |
+
self.inference_punishment_history = self.inference_punishment_history[-50:]
|
| 2196 |
+
if len(self.vqvae2_metrics_history) > 30:
|
| 2197 |
+
self.vqvae2_metrics_history = self.vqvae2_metrics_history[-30:]
|
| 2198 |
+
if len(self.w8a8_metrics_history) > 30:
|
| 2199 |
+
self.w8a8_metrics_history = self.w8a8_metrics_history[-30:]
|
| 2200 |
+
|
| 2201 |
+
# 3. gc.collect() em 2 passes (gerações 0+1 e 2)
|
| 2202 |
+
n_collected_0 = gc.collect(0)
|
| 2203 |
+
n_collected_1 = gc.collect(1)
|
| 2204 |
+
n_collected_2 = gc.collect(2)
|
| 2205 |
+
|
| 2206 |
+
# 4. Libera caches CPU do PyTorch (oneDNN/MKL)
|
| 2207 |
+
try:
|
| 2208 |
+
import torch
|
| 2209 |
+
if hasattr(torch, "cuda") and torch.cuda.is_available():
|
| 2210 |
+
torch.cuda.empty_cache()
|
| 2211 |
+
except Exception:
|
| 2212 |
+
pass
|
| 2213 |
+
|
| 2214 |
+
try:
|
| 2215 |
+
import resource
|
| 2216 |
+
rss_after = float(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss) / 1024.0
|
| 2217 |
+
except Exception:
|
| 2218 |
+
rss_after = 0.0
|
| 2219 |
+
|
| 2220 |
+
return {
|
| 2221 |
+
"rss_before_mb": float(rss_before),
|
| 2222 |
+
"rss_after_mb": float(rss_after),
|
| 2223 |
+
"freed_mb": max(0.0, rss_before - rss_after),
|
| 2224 |
+
"gc_collected": int(n_collected_0 + n_collected_1 + n_collected_2),
|
| 2225 |
+
"adaptation_log_trimmed": int(len(self._adaptation_log)),
|
| 2226 |
+
"timestamp": __import__("datetime").datetime.utcnow().isoformat() + "Z",
|
| 2227 |
+
}
|
| 2228 |
+
|
| 2229 |
# ==================================================================
|
| 2230 |
# 2.1 Treinamento dos geradores (train_hypotheses)
|
| 2231 |
# ==================================================================
|
|
|
|
| 2275 |
|
| 2276 |
self.hypothesis_ensemble.train()
|
| 2277 |
losses = []
|
| 2278 |
+
# V6.5-V2-metrics-FIX: pré-computa data_norm_sq uma única vez (N, 1)
|
| 2279 |
+
# para reuso em todos os steps — evita recomputação redundante.
|
| 2280 |
+
data_norm_sq = (data * data).sum(dim=-1, keepdim=True).t() # (1, N)
|
| 2281 |
+
labels_expanded = labels # (N,)
|
| 2282 |
+
|
| 2283 |
for step in range(self.hyp_train_steps):
|
| 2284 |
self.hyp_optimizer.zero_grad()
|
| 2285 |
|
|
|
|
| 2290 |
deltas_stack = self.hypothesis_ensemble.forward_stacked(x_mean)
|
| 2291 |
deltas_stack = self.delta_scale * deltas_stack # escala
|
| 2292 |
|
| 2293 |
+
# V6.5-V2-metrics-FIX: Avaliação VETORIZADA de todas as hipóteses
|
| 2294 |
+
# em paralelo (substitui loop que materializava 16 cópias do SOM).
|
| 2295 |
+
# Memória: (n_hyp, P, N) em vez de 16 * (P + 2*P*N).
|
| 2296 |
if self.classifier is not None and self.classifier_trained:
|
| 2297 |
+
# deltas_stack: (1, n_hyp, P) → (n_hyp, P, 4) reshape
|
| 2298 |
+
n_hyp = self.n_hypotheses
|
| 2299 |
+
P_neurons = self.som_neuron_count
|
| 2300 |
+
# som_weights_flat: (P, 4) detached
|
| 2301 |
+
W_base = som_weights_flat.reshape(P_neurons, 4) # (P, 4)
|
| 2302 |
+
# delta_h: (n_hyp, P) → reshape para (n_hyp, P, 4)
|
| 2303 |
+
deltas_3d = deltas_stack[0].reshape(n_hyp, P_neurons, 4) # (n_hyp, P, 4)
|
| 2304 |
+
# W_new[h, p, d] = W_base[p, d] + deltas_3d[h, p, d]
|
| 2305 |
+
W_new = W_base.unsqueeze(0) + deltas_3d # (n_hyp, P, 4) — broadcast
|
| 2306 |
+
# dist²[h, p, n] = ||W_new[h, p] - data[n]||²
|
| 2307 |
+
# = ||W_new[h, p]||² + ||data[n]||² - 2*W_new[h, p]·data[n]
|
| 2308 |
+
W_new_norm_sq = (W_new * W_new).sum(dim=-1) # (n_hyp, P)
|
| 2309 |
+
# cross[h, p, n] = W_new[h, p] · data[n]
|
| 2310 |
+
cross = torch.matmul(W_new, data.t()) # (n_hyp, P, N)
|
| 2311 |
+
dist_sq = (
|
| 2312 |
+
W_new_norm_sq.unsqueeze(-1) # (n_hyp, P, 1)
|
| 2313 |
+
+ data_norm_sq # (1, N) → broadcast (n_hyp, P, N)
|
| 2314 |
+
- 2.0 * cross
|
| 2315 |
+
) # (n_hyp, P, N)
|
| 2316 |
+
dist_sq = torch.clamp(dist_sq, min=0.0)
|
| 2317 |
+
# activations[h, n, p] = dist_sq[h, p, n]
|
| 2318 |
+
activations = dist_sq.transpose(1, 2) # (n_hyp, N, P)
|
| 2319 |
+
# Classifier forward (congelado)
|
| 2320 |
+
classifier_params_were_grad = [
|
| 2321 |
+
p.requires_grad for p in self.classifier.parameters()
|
| 2322 |
+
]
|
| 2323 |
+
for p in self.classifier.parameters():
|
| 2324 |
+
p.requires_grad_(False)
|
| 2325 |
+
try:
|
| 2326 |
+
logits = self.classifier(
|
| 2327 |
+
activations.reshape(n_hyp * len(data), P_neurons)
|
| 2328 |
+
).reshape(n_hyp, len(data)) # (n_hyp, N)
|
| 2329 |
+
# BCE por hipótese, depois média
|
| 2330 |
+
loss_per_hyp = F.binary_cross_entropy_with_logits(
|
| 2331 |
+
logits,
|
| 2332 |
+
labels_expanded.unsqueeze(0).expand(n_hyp, -1),
|
| 2333 |
+
reduction='none',
|
| 2334 |
+
).mean(dim=1) # (n_hyp,)
|
| 2335 |
+
loss_total = loss_per_hyp.mean()
|
| 2336 |
+
# Regularização L2 sobre os deltas
|
| 2337 |
+
reg_loss = 0.01 * deltas_stack.norm()
|
| 2338 |
+
loss_total = loss_total + reg_loss
|
| 2339 |
+
finally:
|
| 2340 |
+
for p, was_grad in zip(self.classifier.parameters(),
|
| 2341 |
+
classifier_params_were_grad):
|
| 2342 |
+
p.requires_grad_(was_grad)
|
| 2343 |
else:
|
| 2344 |
# Fallback: minimizar norma do delta (regulariza��ão pura)
|
| 2345 |
loss_total = deltas_stack.norm()
|
|
|
|
| 2348 |
self.hyp_optimizer.step()
|
| 2349 |
losses.append(float(loss_total.item()))
|
| 2350 |
|
| 2351 |
+
# V6.5-V2-metrics-FIX: libera tensores intermediários explicitamente
|
| 2352 |
+
# para reduzir pico de memória entre steps.
|
| 2353 |
+
del loss_total, deltas_stack
|
| 2354 |
+
if 'W_new' in dir():
|
| 2355 |
+
del W_new, cross, dist_sq, activations
|
| 2356 |
+
|
| 2357 |
# Atualiza a escala de delta (decai suavemente)
|
| 2358 |
with torch.no_grad():
|
| 2359 |
self.delta_scale.data = torch.clamp(
|
|
|
|
| 2361 |
)
|
| 2362 |
|
| 2363 |
self.hypothesis_ensemble.eval()
|
| 2364 |
+
# V6.5-V2-metrics-FIX: libera som_activations e som_weights_flat
|
| 2365 |
+
del som_activations, som_weights_flat, data, data_norm_sq
|
| 2366 |
+
import gc
|
| 2367 |
+
gc.collect()
|
| 2368 |
|
| 2369 |
# V6.5-V2-dynamic — registra loss_final no histórico
|
| 2370 |
loss_final_val = float(losses[-1]) if losses else 0.0
|
|
|
|
| 2393 |
def _compute_classification_loss_with_som(
|
| 2394 |
self, som_weights: torch.Tensor, data: torch.Tensor, labels: torch.Tensor
|
| 2395 |
) -> torch.Tensor:
|
| 2396 |
+
"""V6.5-V2-metrics-FIX — Computa perda de classificação com SOM temporário.
|
| 2397 |
+
|
| 2398 |
+
BUG CORRIGIDO (V6.5-V2-metrics-FIX):
|
| 2399 |
+
A versão anterior envolvia toda a computação em `torch.no_grad()`,
|
| 2400 |
+
o que matava o gradiente que deveria fluir dos deltas → SOM → loss.
|
| 2401 |
+
Resultado: train_hypotheses() não treinava os geradores de fato.
|
| 2402 |
+
|
| 2403 |
+
CORREÇÃO: A computação das ativações SOM (squared distances) é
|
| 2404 |
+
feita SEM no_grad, permitindo que o gradiente flua dos deltas
|
| 2405 |
+
(que pertencem ao HypothesisEnsemble) através do SOM temporário
|
| 2406 |
+
até a loss. Apenas o classifier é congelado (requires_grad=False)
|
| 2407 |
+
para que seu gradiente não seja atualizado por train_hypotheses.
|
| 2408 |
+
|
| 2409 |
+
Mathematical formulation (squared Euclidean distance, consistente com
|
| 2410 |
+
_som_activation que usa ||W - x||^2, não ||W - x||):
|
| 2411 |
+
Para cada neurônio p e amostra n:
|
| 2412 |
+
dist²[p, n] = ||W[p] - x[n]||²
|
| 2413 |
+
= ||W[p]||² + ||x[n]||² - 2·W[p]·x[n]
|
| 2414 |
+
activations = dist².t() → (N, P_neurons)
|
| 2415 |
+
logits = classifier(activations) → (N,)
|
| 2416 |
+
loss = BCEWithLogitsLoss(logits, labels)
|
| 2417 |
|
| 2418 |
+
Args:
|
| 2419 |
+
som_weights: tensor (I, J, K, L, 4) com gradiente (dos deltas).
|
| 2420 |
+
data: tensor (N, 4) — amostras 4D (detached).
|
| 2421 |
+
labels: tensor (N,) — rótulos binários.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2422 |
|
| 2423 |
+
Returns:
|
| 2424 |
+
Scalar loss tensor (com gradiente fluindo para som_weights).
|
| 2425 |
+
"""
|
| 2426 |
+
if self.classifier is not None and self.classifier_trained:
|
| 2427 |
+
# NÃO usa torch.no_grad() — gradiente deve fluir dos deltas
|
| 2428 |
+
# através do SOM temporário até a loss.
|
| 2429 |
+
W_flat = som_weights.reshape(-1, 4) # (P_neurons, 4)
|
| 2430 |
+
# Identidade algébrica: ||W - x||^2 = ||W||^2 + ||x||^2 - 2*W·x
|
| 2431 |
+
# Evita materializar tensor (P, N, 4) — usa (P, N) direto.
|
| 2432 |
+
W_norm_sq = (W_flat * W_flat).sum(dim=-1, keepdim=True) # (P, 1)
|
| 2433 |
+
data_norm_sq = (data * data).sum(dim=-1, keepdim=True).t() # (1, N)
|
| 2434 |
+
cross = W_flat @ data.t() # (P, N) — matmul eficiente
|
| 2435 |
+
dist_sq = W_norm_sq + data_norm_sq - 2.0 * cross # (P, N)
|
| 2436 |
+
dist_sq = torch.clamp(dist_sq, min=0.0) # estabilidade numérica
|
| 2437 |
+
activations = dist_sq.t() # (N, P_neurons) — gradiente fluindo!
|
| 2438 |
+
# Congela classifier durante train_hypotheses (não queremos
|
| 2439 |
+
# atualizar seus pesos — apenas os deltas dos geradores)
|
| 2440 |
+
classifier_params_were_grad = [
|
| 2441 |
+
p.requires_grad for p in self.classifier.parameters()
|
| 2442 |
+
]
|
| 2443 |
+
for p in self.classifier.parameters():
|
| 2444 |
+
p.requires_grad_(False)
|
| 2445 |
+
try:
|
| 2446 |
+
logits = self.classifier(activations) # (N,)
|
| 2447 |
+
loss = F.binary_cross_entropy_with_logits(logits, labels)
|
| 2448 |
+
finally:
|
| 2449 |
+
# Restaura requires_grad do classifier
|
| 2450 |
+
for p, was_grad in zip(self.classifier.parameters(),
|
| 2451 |
+
classifier_params_were_grad):
|
| 2452 |
+
p.requires_grad_(was_grad)
|
| 2453 |
+
return loss
|
| 2454 |
+
else:
|
| 2455 |
+
# Fallback: votação BMU (não-diferenciável)
|
| 2456 |
+
with torch.no_grad():
|
| 2457 |
+
if not hasattr(self, "neuron_label"):
|
| 2458 |
+
self._label_neurons()
|
| 2459 |
+
correct = 0
|
| 2460 |
+
for i in range(len(data)):
|
| 2461 |
+
bmu = self.som.find_bmu(data[i])
|
| 2462 |
+
pred = self.neuron_label.get(bmu, 0.5)
|
| 2463 |
+
if (pred > 0.5) == (labels[i].item() > 0.5):
|
| 2464 |
+
correct += 1
|
| 2465 |
+
return torch.tensor(
|
| 2466 |
+
1.0 - correct / max(1, len(data)),
|
| 2467 |
+
device=data.device,
|
| 2468 |
+
dtype=torch.float,
|
| 2469 |
+
)
|
| 2470 |
|
| 2471 |
# ==================================================================
|
| 2472 |
# 2.2 Seleção do melhor delta (3 tentativas × 16 hipóteses)
|
|
|
|
| 2566 |
def _evaluate_accuracy_with_som(
|
| 2567 |
self, som_weights: torch.Tensor, data: torch.Tensor, labels: torch.Tensor
|
| 2568 |
) -> float:
|
| 2569 |
+
"""V6.5-V2-metrics-FIX — Calcula acurácia com SOM temporário (vetorizado).
|
| 2570 |
+
|
| 2571 |
+
Substitui o loop que chamava _som_activation(data[i]) para cada amostra
|
| 2572 |
+
por uma única operação matmul. Reduz tempo de O(N*P) para O(P*N) mas
|
| 2573 |
+
com paralelismo SIMD (single matmul vs N python iterations).
|
| 2574 |
+
|
| 2575 |
+
Para o fallback (votação BMU), ainda precisa trocar self.som.weights
|
| 2576 |
+
temporariamente pois find_bmu lê self.som.weights diretamente.
|
| 2577 |
+
"""
|
| 2578 |
+
if self.classifier is not None and self.classifier_trained:
|
| 2579 |
+
with torch.no_grad():
|
| 2580 |
+
# Vetorizado: dist²[p, n] = ||W[p]||² + ||x[n]||² - 2*W[p]·x[n]
|
| 2581 |
+
W_flat = som_weights.reshape(-1, 4) # (P, 4)
|
| 2582 |
+
W_norm_sq = (W_flat * W_flat).sum(dim=-1, keepdim=True) # (P, 1)
|
| 2583 |
+
data_norm_sq = (data * data).sum(dim=-1, keepdim=True).t() # (1, N)
|
| 2584 |
+
cross = W_flat @ data.t() # (P, N)
|
| 2585 |
+
dist_sq = W_norm_sq + data_norm_sq - 2.0 * cross # (P, N)
|
| 2586 |
+
dist_sq = torch.clamp(dist_sq, min=0.0)
|
| 2587 |
+
activations = dist_sq.t() # (N, P)
|
| 2588 |
+
logits = self.classifier(activations) # (N,)
|
| 2589 |
+
preds = (logits > 0.5).float()
|
| 2590 |
+
acc = (preds == labels).float().mean().item()
|
|
|
|
| 2591 |
return float(acc)
|
| 2592 |
+
else:
|
| 2593 |
+
# Fallback: votação BMU — precisa trocar self.som.weights
|
| 2594 |
+
original_weights = self.som.weights
|
| 2595 |
+
self.som.weights = som_weights
|
| 2596 |
+
try:
|
| 2597 |
+
with torch.no_grad():
|
| 2598 |
+
if not hasattr(self, "neuron_label"):
|
| 2599 |
+
self._label_neurons()
|
| 2600 |
+
correct = 0
|
| 2601 |
+
for i in range(len(data)):
|
| 2602 |
+
bmu = self.som.find_bmu(data[i])
|
| 2603 |
+
pred = self.neuron_label.get(bmu, 0.5)
|
| 2604 |
+
if (pred > 0.5) == (labels[i].item() > 0.5):
|
| 2605 |
+
correct += 1
|
| 2606 |
+
acc = correct / max(1, len(data))
|
| 2607 |
+
return float(acc)
|
| 2608 |
+
finally:
|
| 2609 |
+
self.som.weights = original_weights
|
| 2610 |
|
| 2611 |
# ==================================================================
|
| 2612 |
# 2.3 Aplicação do melhor delta + consolidação via EWC
|
|
|
|
| 2836 |
},
|
| 2837 |
}
|
| 2838 |
|
| 2839 |
+
# ==================================================================
|
| 2840 |
+
# V6.5-V2-metrics — Integração das métricas SOM canônicas
|
| 2841 |
+
# (QE, TE, Kaski-Lagus, Variância Explicada, Dead Neurons,
|
| 2842 |
+
# Colapso Topológico, Estagnação QE, Cruzamento Vizinhança)
|
| 2843 |
+
# ==================================================================
|
| 2844 |
+
def compute_som_metrics(self) -> Dict[str, Any]:
|
| 2845 |
+
"""V6.5-V2-metrics — Computa todas as métricas SOM canônicas.
|
| 2846 |
+
|
| 2847 |
+
User requirement: "ANALISAR matematicamente e logicamente e inserir
|
| 2848 |
+
melhorias para os scripts das métricas de aprendizado e indicadores
|
| 2849 |
+
de falha em Mapas Auto-Organizáveis (SOM / Redes de Kohonen)
|
| 2850 |
+
avaliam a fidelidade de representação dos dados e a preservação da
|
| 2851 |
+
vizinhança topológica".
|
| 2852 |
+
|
| 2853 |
+
Returns:
|
| 2854 |
+
Dict com 8 métricas:
|
| 2855 |
+
1. quantization_error (QE)
|
| 2856 |
+
2. topological_error (TE)
|
| 2857 |
+
3. kaski_lagus_error (KL)
|
| 2858 |
+
4. explained_variance_share (VE)
|
| 2859 |
+
5. topological_collapse (dict)
|
| 2860 |
+
6. dead_neuron_rate (dict)
|
| 2861 |
+
7. qe_stagnation (dict) — requer histórico
|
| 2862 |
+
8. neighborhood_crossing (dict) — requer histórico
|
| 2863 |
+
Plus: overall_health, failure_indicators
|
| 2864 |
+
"""
|
| 2865 |
+
from bigru_t.model.som_metrics import compute_all_metrics
|
| 2866 |
+
if not self.buffer_4d:
|
| 2867 |
+
return {
|
| 2868 |
+
"quantization_error": 0.0,
|
| 2869 |
+
"topological_error": 0.0,
|
| 2870 |
+
"kaski_lagus_error": 0.0,
|
| 2871 |
+
"explained_variance_share": 0.0,
|
| 2872 |
+
"topological_collapse": {"severity": "none", "effective_rank": 0.0},
|
| 2873 |
+
"dead_neuron_rate": {"dead_neuron_rate": 1.0, "n_dead": self.som_neuron_count,
|
| 2874 |
+
"n_active": 0, "n_total": self.som_neuron_count},
|
| 2875 |
+
"qe_stagnation": {"is_stagnant": False, "severity": "none"},
|
| 2876 |
+
"neighborhood_crossing": {"detected": False, "severity": "none"},
|
| 2877 |
+
"overall_health": "unknown",
|
| 2878 |
+
"failure_indicators": ["no_buffer_data"],
|
| 2879 |
+
"n_failure_indicators": 1,
|
| 2880 |
+
"note": "buffer_4d vazio — métricas indisponíveis",
|
| 2881 |
+
}
|
| 2882 |
+
# Stack buffer em tensor (N, 4)
|
| 2883 |
+
data = torch.stack([
|
| 2884 |
+
v.detach().clone() if isinstance(v, torch.Tensor) else torch.tensor(v, dtype=torch.float)
|
| 2885 |
+
for v in self.buffer_4d
|
| 2886 |
+
]).float()
|
| 2887 |
+
weights = self.som.weights.detach()
|
| 2888 |
+
metrics = compute_all_metrics(data, weights, self._som_metric_history)
|
| 2889 |
+
# Registra no histórico
|
| 2890 |
+
self._som_metric_history.record(metrics)
|
| 2891 |
+
return metrics
|
| 2892 |
+
|
| 2893 |
+
def get_som_metric_history(self) -> Dict[str, Any]:
|
| 2894 |
+
"""Retorna histórico de métricas SOM (para análise temporal)."""
|
| 2895 |
+
return self._som_metric_history.to_dict()
|
| 2896 |
+
|
| 2897 |
+
def reset_som_metric_history(self) -> None:
|
| 2898 |
+
"""Reseta o histórico de métricas SOM (início de nova fase)."""
|
| 2899 |
+
from bigru_t.model.som_metrics import SOMMetricHistory
|
| 2900 |
+
self._som_metric_history = SOMMetricHistory()
|
| 2901 |
+
|
| 2902 |
|
| 2903 |
__all__ = [
|
| 2904 |
"SimpleBBPETokenizer",
|
|
|
|
| 2911 |
"HypothesisEnsemble",
|
| 2912 |
"KohonenLearningSystemV2",
|
| 2913 |
]
|
| 2914 |
+
# V6.5-V2-metrics: som_metrics é importado lazy (dentro de compute_som_metrics)
|
| 2915 |
+
# para evitar dependência circular e permitir uso standalone.
|
smoke_test_v65_v2_metrics_fix.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""smoke_test_v65_v2_metrics_fix.py — Validação rápida das correções V6.5-V2-metrics-FIX.
|
| 2 |
+
|
| 3 |
+
Verifica:
|
| 4 |
+
1. Importação do KohonenLearningSystemV2 com params restaurados (HIDDEN=1024, VOCAB=16384)
|
| 5 |
+
2. Bug fix do _compute_classification_loss_with_som (gradiente flui)
|
| 6 |
+
3. Vetorização do train_hypotheses (sem OOM)
|
| 7 |
+
4. aggressive_cleanup() funciona
|
| 8 |
+
5. SOM metrics (8 métricas) computam sem erro
|
| 9 |
+
6. predict() retorna labels dinâmicos (não hardcoded)
|
| 10 |
+
7. FASE1 + FASE2 logic (enable_punishment=False/True)
|
| 11 |
+
"""
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import os
|
| 15 |
+
import sys
|
| 16 |
+
import time
|
| 17 |
+
import traceback
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
|
| 20 |
+
# Setup paths
|
| 21 |
+
PROJECT_ROOT = Path("/home/z/my-project")
|
| 22 |
+
BIGRU_ROOT = PROJECT_ROOT / "BiGRU_T_version"
|
| 23 |
+
SRC_ROOT = BIGRU_ROOT / "src"
|
| 24 |
+
sys.path.insert(0, str(SRC_ROOT))
|
| 25 |
+
|
| 26 |
+
# Activate xeon_runtime
|
| 27 |
+
os.environ["V65_ENABLE_STREAMING"] = "1"
|
| 28 |
+
|
| 29 |
+
print("=" * 80)
|
| 30 |
+
print("V6.5-V2-metrics-FIX SMOKE TEST")
|
| 31 |
+
print("=" * 80)
|
| 32 |
+
print()
|
| 33 |
+
|
| 34 |
+
# Test 1: Import
|
| 35 |
+
print("[Test 1] Import KohonenLearningSystemV2...")
|
| 36 |
+
try:
|
| 37 |
+
from bigru_t.model.kohonen_learning_system import (
|
| 38 |
+
KohonenLearningSystemV2,
|
| 39 |
+
KohonenLearningSystem,
|
| 40 |
+
KohonenSOM4D,
|
| 41 |
+
HypothesisClassifier,
|
| 42 |
+
HypothesisEnsemble,
|
| 43 |
+
DeltaGenerator,
|
| 44 |
+
SimpleBBPETokenizer,
|
| 45 |
+
text_to_4d_vector,
|
| 46 |
+
)
|
| 47 |
+
from bigru_t.model.som_metrics import (
|
| 48 |
+
SOMMetricHistory,
|
| 49 |
+
compute_all_metrics,
|
| 50 |
+
quantization_error,
|
| 51 |
+
topological_error,
|
| 52 |
+
kaski_lagus_error,
|
| 53 |
+
explained_variance_share,
|
| 54 |
+
topological_collapse,
|
| 55 |
+
dead_neuron_rate,
|
| 56 |
+
)
|
| 57 |
+
print(" PASS: All imports successful")
|
| 58 |
+
except Exception as e:
|
| 59 |
+
print(f" FAIL: Import error: {e}")
|
| 60 |
+
traceback.print_exc()
|
| 61 |
+
sys.exit(1)
|
| 62 |
+
|
| 63 |
+
# Test 2: Instantiate with restored (larger) parameters
|
| 64 |
+
print("\n[Test 2] Instantiate KLS V2 with restored params (HIDDEN=1024, VOCAB=16384)...")
|
| 65 |
+
try:
|
| 66 |
+
# Use smaller grid for smoke test (faster), but full HIDDEN_DIM/VOCAB_SIZE
|
| 67 |
+
kls = KohonenLearningSystemV2(
|
| 68 |
+
vocab_size=16384, # restored
|
| 69 |
+
hidden_dim=1024, # restored
|
| 70 |
+
seq_len=8,
|
| 71 |
+
som_grid=(6, 6, 6, 4), # 864 neurons (Kohonen architecture preserved)
|
| 72 |
+
alpha0=0.1,
|
| 73 |
+
sigma0=1.5,
|
| 74 |
+
lambda_ewc=0.02,
|
| 75 |
+
N_start=10,
|
| 76 |
+
dim_choice="y",
|
| 77 |
+
hypothesis_hidden=[512, 256, 128, 64, 32, 16, 8],
|
| 78 |
+
T_max=10000,
|
| 79 |
+
enable_vqvae2=False, # disabled to save RAM
|
| 80 |
+
enable_reasoning=False, # disabled to save RAM
|
| 81 |
+
enable_w8a8=False, # disabled to save RAM
|
| 82 |
+
enable_attention=True,
|
| 83 |
+
attention_n_heads=8,
|
| 84 |
+
# V2-dynamic restored params
|
| 85 |
+
n_hypotheses=16, # restored
|
| 86 |
+
max_n_hypotheses=32, # restored
|
| 87 |
+
min_n_hypotheses=4,
|
| 88 |
+
n_trials=3,
|
| 89 |
+
min_n_trials=1,
|
| 90 |
+
max_n_trials=6,
|
| 91 |
+
hyp_train_steps=30, # restored
|
| 92 |
+
min_hyp_train_steps=10,
|
| 93 |
+
max_hyp_train_steps=80,
|
| 94 |
+
hyp_lr=1e-4,
|
| 95 |
+
hyp_hidden_dim=256, # restored
|
| 96 |
+
loss_history_window=8,
|
| 97 |
+
punishment_window=12,
|
| 98 |
+
)
|
| 99 |
+
assert kls.hidden_dim == 1024, f"hidden_dim={kls.hidden_dim}"
|
| 100 |
+
assert kls.tokenizer.vocab_size == 16384, f"vocab={kls.tokenizer.vocab_size}"
|
| 101 |
+
assert kls.n_hypotheses == 16, f"n_hyp={kls.n_hypotheses}"
|
| 102 |
+
assert kls.max_n_hypotheses == 32, f"max_n_hyp={kls.max_n_hypotheses}"
|
| 103 |
+
assert kls.hyp_train_steps == 30, f"hyp_steps={kls.hyp_train_steps}"
|
| 104 |
+
assert kls.hyp_hidden_dim == 256, f"hyp_hidden={kls.hyp_hidden_dim}"
|
| 105 |
+
assert hasattr(kls, "aggressive_cleanup"), "no aggressive_cleanup method"
|
| 106 |
+
print(f" PASS: KLS V2 instantiated")
|
| 107 |
+
print(f" hidden_dim={kls.hidden_dim}, vocab={kls.tokenizer.vocab_size}")
|
| 108 |
+
print(f" n_hypotheses={kls.n_hypotheses}/{kls.max_n_hypotheses}")
|
| 109 |
+
print(f" hyp_train_steps={kls.hyp_train_steps}, hyp_hidden_dim={kls.hyp_hidden_dim}")
|
| 110 |
+
print(f" som_grid={kls.som_grid} ({kls.som_neuron_count} neurons)")
|
| 111 |
+
print(f" has aggressive_cleanup: True")
|
| 112 |
+
except Exception as e:
|
| 113 |
+
print(f" FAIL: Instantiation error: {e}")
|
| 114 |
+
traceback.print_exc()
|
| 115 |
+
sys.exit(1)
|
| 116 |
+
|
| 117 |
+
# Test 3: Add data + train SOM (CONHECIMENTO phase)
|
| 118 |
+
print("\n[Test 3] Add data + train SOM (CONHECIMENTO phase)...")
|
| 119 |
+
try:
|
| 120 |
+
kls.tokenizer.fit([
|
| 121 |
+
"o gato dorme", "a casa eh azul", "ele corre rapido",
|
| 122 |
+
"ela canta uma musica", "o sol nasceu", "nos vamos viajar",
|
| 123 |
+
"luva de pedreiro tavila", "lula reserva valor",
|
| 124 |
+
"amazonas forca tarefa vitimas",
|
| 125 |
+
])
|
| 126 |
+
# Add 30 samples (more than N_start=10)
|
| 127 |
+
for i in range(3):
|
| 128 |
+
kls.add_data(
|
| 129 |
+
["o gato dorme", "o cachorro corre", "a menina brinca",
|
| 130 |
+
"o passaro voa", "o peixe nada", "a arvore cresce",
|
| 131 |
+
"o livro esta na mesa", "a musica toca", "o tempo passa",
|
| 132 |
+
"o mundo gira"],
|
| 133 |
+
[0, 1, 0, 1, 0, 1, 0, 1, 0, 1],
|
| 134 |
+
label_strings={0: "negative_class", 1: "positive_class"},
|
| 135 |
+
dataset_name="test_ds",
|
| 136 |
+
)
|
| 137 |
+
assert len(kls.buffer_4d) >= 10, f"buffer={len(kls.buffer_4d)}"
|
| 138 |
+
print(f" PASS: Added {len(kls.buffer_4d)} samples")
|
| 139 |
+
# Train SOM
|
| 140 |
+
started = kls.check_training_start()
|
| 141 |
+
print(f" training_ready: {kls.training_ready}")
|
| 142 |
+
if kls.training_ready:
|
| 143 |
+
kls.train_som_on_buffer()
|
| 144 |
+
kls._label_neurons()
|
| 145 |
+
print(f" SOM trained, weights norm={kls.som.weights.norm().item():.4f}")
|
| 146 |
+
except Exception as e:
|
| 147 |
+
print(f" FAIL: {e}")
|
| 148 |
+
traceback.print_exc()
|
| 149 |
+
sys.exit(1)
|
| 150 |
+
|
| 151 |
+
# Test 4: predict() returns dynamic labels (not gato/cachorro)
|
| 152 |
+
print("\n[Test 4] predict() returns dynamic labels...")
|
| 153 |
+
try:
|
| 154 |
+
pred = kls.predict("o gato dorme", dataset_name="test_ds")
|
| 155 |
+
assert pred in ["negative_class", "positive_class"], f"pred={pred}"
|
| 156 |
+
assert pred != "gato" and pred != "cachorro", f"hardcoded: {pred}"
|
| 157 |
+
print(f" PASS: predict returned '{pred}' (dynamic, not hardcoded)")
|
| 158 |
+
except Exception as e:
|
| 159 |
+
print(f" FAIL: {e}")
|
| 160 |
+
traceback.print_exc()
|
| 161 |
+
sys.exit(1)
|
| 162 |
+
|
| 163 |
+
# Test 5: activate_hypothesis (trains classifier)
|
| 164 |
+
print("\n[Test 5] activate_hypothesis (train classifier)...")
|
| 165 |
+
try:
|
| 166 |
+
kls.activate_hypothesis()
|
| 167 |
+
assert kls.classifier_trained, "classifier not trained"
|
| 168 |
+
print(f" PASS: Classifier trained ({kls.classifier is not None})")
|
| 169 |
+
except Exception as e:
|
| 170 |
+
print(f" FAIL: {e}")
|
| 171 |
+
traceback.print_exc()
|
| 172 |
+
sys.exit(1)
|
| 173 |
+
|
| 174 |
+
# Test 6: train_hypotheses (memory-efficient vectorized version)
|
| 175 |
+
print("\n[Test 6] train_hypotheses (memory-efficient vectorized)...")
|
| 176 |
+
try:
|
| 177 |
+
t0 = time.time()
|
| 178 |
+
result = kls.train_hypotheses()
|
| 179 |
+
elapsed_ms = (time.time() - t0) * 1000
|
| 180 |
+
assert result["active"], f"not active: {result}"
|
| 181 |
+
assert result["n_steps"] == 30, f"n_steps={result['n_steps']}"
|
| 182 |
+
print(f" PASS: train_hypotheses completed in {elapsed_ms:.0f}ms")
|
| 183 |
+
print(f" n_steps={result['n_steps']}, n_hypotheses={result['n_hypotheses']}")
|
| 184 |
+
print(f" loss_initial={result['loss_initial']:.4f}, loss_final={result['loss_final']:.4f}")
|
| 185 |
+
print(f" delta_scale_final={result['delta_scale_final']:.6f}")
|
| 186 |
+
if "adaptation" in result:
|
| 187 |
+
adapt = result["adaptation"]
|
| 188 |
+
print(f" adaptation: adapted={adapt['adapted']}, rules={adapt['rules_fired']}")
|
| 189 |
+
except Exception as e:
|
| 190 |
+
print(f" FAIL: {e}")
|
| 191 |
+
traceback.print_exc()
|
| 192 |
+
sys.exit(1)
|
| 193 |
+
|
| 194 |
+
# Test 7: SOM metrics (8 canonical metrics)
|
| 195 |
+
print("\n[Test 7] SOM metrics (8 canonical)...")
|
| 196 |
+
try:
|
| 197 |
+
metrics = kls.compute_som_metrics()
|
| 198 |
+
assert "quantization_error" in metrics
|
| 199 |
+
assert "topological_error" in metrics
|
| 200 |
+
assert "kaski_lagus_error" in metrics
|
| 201 |
+
assert "explained_variance_share" in metrics
|
| 202 |
+
assert "topological_collapse" in metrics
|
| 203 |
+
assert "dead_neuron_rate" in metrics
|
| 204 |
+
assert "qe_stagnation" in metrics
|
| 205 |
+
assert "neighborhood_crossing" in metrics
|
| 206 |
+
assert "overall_health" in metrics
|
| 207 |
+
print(f" PASS: All 8 SOM metrics computed")
|
| 208 |
+
print(f" QE={metrics['quantization_error']:.4f}")
|
| 209 |
+
print(f" TE={metrics['topological_error']:.4f}")
|
| 210 |
+
print(f" KL={metrics['kaski_lagus_error']:.4f}")
|
| 211 |
+
print(f" VE={metrics['explained_variance_share']:.4f}")
|
| 212 |
+
print(f" dead_neuron_rate={metrics['dead_neuron_rate'].get('dead_neuron_rate', 0):.3f}")
|
| 213 |
+
print(f" collapse_severity={metrics['topological_collapse'].get('severity', '?')}")
|
| 214 |
+
print(f" overall_health={metrics['overall_health']}")
|
| 215 |
+
print(f" n_failure_indicators={metrics['n_failure_indicators']}")
|
| 216 |
+
except Exception as e:
|
| 217 |
+
print(f" FAIL: {e}")
|
| 218 |
+
traceback.print_exc()
|
| 219 |
+
sys.exit(1)
|
| 220 |
+
|
| 221 |
+
# Test 8: select_best_delta (vectorized)
|
| 222 |
+
print("\n[Test 8] select_best_delta (3 trials × 16 hypotheses)...")
|
| 223 |
+
try:
|
| 224 |
+
t0 = time.time()
|
| 225 |
+
best_delta, best_acc, info = kls.select_best_delta()
|
| 226 |
+
elapsed_ms = (time.time() - t0) * 1000
|
| 227 |
+
assert info["active"], f"not active: {info}"
|
| 228 |
+
assert info["n_trials"] == 3, f"n_trials={info['n_trials']}"
|
| 229 |
+
assert info["n_hypotheses"] == 16, f"n_hyp={info['n_hypotheses']}"
|
| 230 |
+
print(f" PASS: select_best_delta completed in {elapsed_ms:.0f}ms")
|
| 231 |
+
print(f" best_acc={best_acc:.4f}")
|
| 232 |
+
print(f" delta_norm={info['delta_norm']:.4f}")
|
| 233 |
+
print(f" n_trials={info['n_trials']}, n_hypotheses={info['n_hypotheses']}")
|
| 234 |
+
except Exception as e:
|
| 235 |
+
print(f" FAIL: {e}")
|
| 236 |
+
traceback.print_exc()
|
| 237 |
+
sys.exit(1)
|
| 238 |
+
|
| 239 |
+
# Test 9: process_batch_v2 (CONHECIMENTO + PUNIÇÃO)
|
| 240 |
+
print("\n[Test 9] process_batch_v2 (CONHECIMENTO + PUNIÇÃO)...")
|
| 241 |
+
try:
|
| 242 |
+
# CONHECIMENTO
|
| 243 |
+
result_conh = kls.process_batch_v2(
|
| 244 |
+
["o gato dorme", "o cachorro corre"],
|
| 245 |
+
[0, 1],
|
| 246 |
+
label_strings={0: "negative_class", 1: "positive_class"},
|
| 247 |
+
dataset_name="test_ds",
|
| 248 |
+
enable_punishment=False,
|
| 249 |
+
)
|
| 250 |
+
assert result_conh["phase"] == "CONHECIMENTO"
|
| 251 |
+
print(f" PASS: CONHECIMENTO phase works: {result_conh['phase']}")
|
| 252 |
+
|
| 253 |
+
# PUNIÇÃO
|
| 254 |
+
result_pun = kls.process_batch_v2(
|
| 255 |
+
["o gato dorme", "o cachorro corre"],
|
| 256 |
+
[0, 1],
|
| 257 |
+
label_strings={0: "negative_class", 1: "positive_class"},
|
| 258 |
+
dataset_name="test_ds",
|
| 259 |
+
enable_punishment=True,
|
| 260 |
+
)
|
| 261 |
+
assert result_pun["phase"] == "PUNICAO"
|
| 262 |
+
print(f" PASS: PUNIÇÃO phase works: {result_pun['phase']}")
|
| 263 |
+
print(f" action: {result_pun.get('action', 'none')}")
|
| 264 |
+
except Exception as e:
|
| 265 |
+
print(f" FAIL: {e}")
|
| 266 |
+
traceback.print_exc()
|
| 267 |
+
sys.exit(1)
|
| 268 |
+
|
| 269 |
+
# Test 10: aggressive_cleanup
|
| 270 |
+
print("\n[Test 10] aggressive_cleanup()...")
|
| 271 |
+
try:
|
| 272 |
+
cleanup_result = kls.aggressive_cleanup()
|
| 273 |
+
assert "rss_before_mb" in cleanup_result
|
| 274 |
+
assert "rss_after_mb" in cleanup_result
|
| 275 |
+
assert "gc_collected" in cleanup_result
|
| 276 |
+
print(f" PASS: aggressive_cleanup() called")
|
| 277 |
+
print(f" rss_before={cleanup_result['rss_before_mb']:.1f}MB")
|
| 278 |
+
print(f" rss_after={cleanup_result['rss_after_mb']:.1f}MB")
|
| 279 |
+
print(f" freed={cleanup_result['freed_mb']:.1f}MB")
|
| 280 |
+
print(f" gc_collected={cleanup_result['gc_collected']}")
|
| 281 |
+
except Exception as e:
|
| 282 |
+
print(f" FAIL: {e}")
|
| 283 |
+
traceback.print_exc()
|
| 284 |
+
sys.exit(1)
|
| 285 |
+
|
| 286 |
+
# Test 11: get_v2_metrics
|
| 287 |
+
print("\n[Test 11] get_v2_metrics()...")
|
| 288 |
+
try:
|
| 289 |
+
v2_metrics = kls.get_v2_metrics()
|
| 290 |
+
assert v2_metrics["version"] == "V2-dynamic"
|
| 291 |
+
assert v2_metrics["n_hypotheses"] == 16
|
| 292 |
+
print(f" PASS: get_v2_metrics() works")
|
| 293 |
+
print(f" version={v2_metrics['version']}")
|
| 294 |
+
print(f" n_hypotheses={v2_metrics['n_hypotheses']}")
|
| 295 |
+
print(f" n_trials={v2_metrics['n_trials']}")
|
| 296 |
+
print(f" hyp_train_steps={v2_metrics['hyp_train_steps']}")
|
| 297 |
+
print(f" buffer_size={v2_metrics['buffer_size']}")
|
| 298 |
+
if "dynamic_adaptation" in v2_metrics:
|
| 299 |
+
da = v2_metrics["dynamic_adaptation"]
|
| 300 |
+
print(f" n_adaptations={da['n_adaptations']}")
|
| 301 |
+
print(f" loss_stats: slope={da['loss_stats']['slope']:.6f}, vol={da['loss_stats']['volatility']:.3f}")
|
| 302 |
+
except Exception as e:
|
| 303 |
+
print(f" FAIL: {e}")
|
| 304 |
+
traceback.print_exc()
|
| 305 |
+
sys.exit(1)
|
| 306 |
+
|
| 307 |
+
# Summary
|
| 308 |
+
print("\n" + "=" * 80)
|
| 309 |
+
print("SMOKE TEST SUMMARY")
|
| 310 |
+
print("=" * 80)
|
| 311 |
+
print(" ALL TESTS PASSED")
|
| 312 |
+
print(" - KLS V2 with restored params (HIDDEN=1024, VOCAB=16384) ✓")
|
| 313 |
+
print(" - Memory-efficient train_hypotheses (vectorized) ✓")
|
| 314 |
+
print(" - Bug fix: gradient flows through deltas → SOM → loss ✓")
|
| 315 |
+
print(" - 8 SOM canonical metrics computed ✓")
|
| 316 |
+
print(" - Dynamic labels (no hardcoded gato/cachorro) ✓")
|
| 317 |
+
print(" - CONHECIMENTO + PUNIÇÃO phases ✓")
|
| 318 |
+
print(" - aggressive_cleanup() works ✓")
|
| 319 |
+
print("=" * 80)
|
| 320 |
+
sys.exit(0)
|
som_metrics.py
ADDED
|
@@ -0,0 +1,811 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""som_metrics.py — V6.5-V2-metrics: Métricas de aprendizado e indicadores de
|
| 2 |
+
falha para Mapas Auto-Organizáveis (SOM / Redes de Kohonen).
|
| 3 |
+
|
| 4 |
+
============================================================================
|
| 5 |
+
MÉTRICAS PRINCIPAIS DE APRENDIZADO
|
| 6 |
+
============================================================================
|
| 7 |
+
|
| 8 |
+
1.1. Erro de Quantização (Quantization Error - QE)
|
| 9 |
+
Média das distâncias euclidianas entre cada vetor de entrada e o vetor
|
| 10 |
+
de pesos de seu neurônio vencedor (BMU). Mede a resolução ou ajuste fino
|
| 11 |
+
do mapa aos dados.
|
| 12 |
+
|
| 13 |
+
QE = (1/N) * Σ_i ||x_i - W_BMU(x_i)||₂
|
| 14 |
+
|
| 15 |
+
1.2. Erro Topológico (Topological Error - TE)
|
| 16 |
+
Percentual de vetores de entrada cujos primeiro e segundo neurônios
|
| 17 |
+
mais próximos (BMU e segunda BMU) NÃO são vizinhos diretos na grade
|
| 18 |
+
de saída. Avalia preservação das relações geométricas.
|
| 19 |
+
|
| 20 |
+
TE = (1/N) * Σ_i 𝟙[¬adjacent(BMU_1(x_i), BMU_2(x_i))]
|
| 21 |
+
|
| 22 |
+
1.3. Erro de Kaski-Lagus
|
| 23 |
+
Combinação ponderada que penaliza tanto a falta de resolução do vetor
|
| 24 |
+
de quantização quanto as quebras na continuidade topológica.
|
| 25 |
+
|
| 26 |
+
KL = α * QE_norm + β * TE
|
| 27 |
+
onde α=0.5, β=0.5 (pesos padrão)
|
| 28 |
+
|
| 29 |
+
Variante implementada: KL = QE * (1 + TE) (penaliza QE quando TE é alto)
|
| 30 |
+
|
| 31 |
+
1.4. Variância Explicada (Share of Explained Variance)
|
| 32 |
+
Fração da variabilidade total dos dados de entrada que é mapeada e
|
| 33 |
+
representada pelos vetores de referência (pesos) dos nós da rede.
|
| 34 |
+
|
| 35 |
+
VarExplained = 1 - Var(residual) / Var(total)
|
| 36 |
+
onde residual = x_i - W_BMU(x_i)
|
| 37 |
+
|
| 38 |
+
============================================================================
|
| 39 |
+
INDICADORES DE NÃO APRENDIZADO OU FALHA
|
| 40 |
+
============================================================================
|
| 41 |
+
|
| 42 |
+
2.1. Colapso Topológico (Topological Collapse)
|
| 43 |
+
Ocorre quando todos os vetores de peso convergem para o mesmo ponto
|
| 44 |
+
ou para uma linha reta. Indica perda total da dimensionalidade útil.
|
| 45 |
+
|
| 46 |
+
Detecção:
|
| 47 |
+
- std(weights) por dimensão < threshold (colapso total)
|
| 48 |
+
- rank(weights reshaped) < 2 (todos num ponto)
|
| 49 |
+
- rank < 3 (todos numa linha)
|
| 50 |
+
|
| 51 |
+
2.2. Taxa de Neurônios Mortos (Dead Neurons)
|
| 52 |
+
Percentual de neurônios que NUNCA se tornam BMU para nenhum vetor.
|
| 53 |
+
|
| 54 |
+
DeadRate = (n_neurons - n_active_neurons) / n_neurons
|
| 55 |
+
|
| 56 |
+
Threshold: DeadRate > 0.5 indica problema.
|
| 57 |
+
|
| 58 |
+
2.3. Estagnação do Erro de Quantização
|
| 59 |
+
QE estabiliza em patamar elevado ao longo das épocas. A taxa de
|
| 60 |
+
aprendizado decaiu rápido demais antes da fase de ordenação global.
|
| 61 |
+
|
| 62 |
+
Detecção: slope(QE) ≈ 0 E QE atual > 0.7 * max(QE histórico)
|
| 63 |
+
|
| 64 |
+
2.4. Cruzamento de Vizinhança (Neighborhood Crossing)
|
| 65 |
+
Crescimento repentino ou oscilação crônica do TE nas épocas finais.
|
| 66 |
+
|
| 67 |
+
Detecção:
|
| 68 |
+
- std(TE_final_5_epochs) > 2 * std(TE_initial_5_epochs) (oscilação)
|
| 69 |
+
- TE[-1] > TE[-5] (crescimento no final)
|
| 70 |
+
|
| 71 |
+
============================================================================
|
| 72 |
+
AUTOR: V6.5-V2-metrics
|
| 73 |
+
============================================================================
|
| 74 |
+
"""
|
| 75 |
+
from __future__ import annotations
|
| 76 |
+
|
| 77 |
+
import math
|
| 78 |
+
import logging
|
| 79 |
+
from collections import defaultdict, deque
|
| 80 |
+
from dataclasses import dataclass, field
|
| 81 |
+
from typing import Any, Dict, List, Optional, Tuple
|
| 82 |
+
|
| 83 |
+
import torch
|
| 84 |
+
import torch.nn.functional as F
|
| 85 |
+
|
| 86 |
+
logger = logging.getLogger(__name__)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
# ============================================================================
|
| 90 |
+
# Dataclass para armazenar histórico de métricas
|
| 91 |
+
# ============================================================================
|
| 92 |
+
@dataclass
|
| 93 |
+
class SOMMetricHistory:
|
| 94 |
+
"""Histórico deslizante de métricas SOM para detecção de estagnação e
|
| 95 |
+
cruzamento de vizinhança."""
|
| 96 |
+
qe_history: deque = field(default_factory=lambda: deque(maxlen=50))
|
| 97 |
+
te_history: deque = field(default_factory=lambda: deque(maxlen=50))
|
| 98 |
+
kl_history: deque = field(default_factory=lambda: deque(maxlen=50))
|
| 99 |
+
var_explained_history: deque = field(default_factory=lambda: deque(maxlen=50))
|
| 100 |
+
dead_neuron_rate_history: deque = field(default_factory=lambda: deque(maxlen=50))
|
| 101 |
+
epoch: int = 0
|
| 102 |
+
|
| 103 |
+
def record(self, metrics: Dict[str, Any]) -> None:
|
| 104 |
+
"""Registra métricas de uma época.
|
| 105 |
+
|
| 106 |
+
Nota: `dead_neuron_rate` pode ser float (valor direto) ou Dict
|
| 107 |
+
(saída de compute_all_metrics, com chave 'dead_neuron_rate').
|
| 108 |
+
"""
|
| 109 |
+
self.qe_history.append(float(metrics.get("quantization_error", 0.0)))
|
| 110 |
+
self.te_history.append(float(metrics.get("topological_error", 0.0)))
|
| 111 |
+
self.kl_history.append(float(metrics.get("kaski_lagus_error", 0.0)))
|
| 112 |
+
self.var_explained_history.append(
|
| 113 |
+
float(metrics.get("explained_variance_share", 0.0))
|
| 114 |
+
)
|
| 115 |
+
# dead_neuron_rate pode ser float ou Dict
|
| 116 |
+
dnr = metrics.get("dead_neuron_rate", 0.0)
|
| 117 |
+
if isinstance(dnr, dict):
|
| 118 |
+
dnr_val = float(dnr.get("dead_neuron_rate", 0.0))
|
| 119 |
+
else:
|
| 120 |
+
dnr_val = float(dnr)
|
| 121 |
+
self.dead_neuron_rate_history.append(dnr_val)
|
| 122 |
+
self.epoch += 1
|
| 123 |
+
|
| 124 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 125 |
+
return {
|
| 126 |
+
"epoch": int(self.epoch),
|
| 127 |
+
"qe_history": list(self.qe_history),
|
| 128 |
+
"te_history": list(self.te_history),
|
| 129 |
+
"kl_history": list(self.kl_history),
|
| 130 |
+
"var_explained_history": list(self.var_explained_history),
|
| 131 |
+
"dead_neuron_rate_history": list(self.dead_neuron_rate_history),
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
# ============================================================================
|
| 136 |
+
# Funções auxiliares
|
| 137 |
+
# ============================================================================
|
| 138 |
+
def _grid_neighbors_set(
|
| 139 |
+
idx: Tuple[int, int, int, int],
|
| 140 |
+
grid_shape: Tuple[int, int, int, int],
|
| 141 |
+
) -> set:
|
| 142 |
+
"""Retorna o conjunto de índices vizinhos diretos (6-connectivity em 4D)
|
| 143 |
+
de um neurônio na grade."""
|
| 144 |
+
i, j, k, l = idx
|
| 145 |
+
I, J, K, L = grid_shape
|
| 146 |
+
neighbors = set()
|
| 147 |
+
for di, dj, dk, dl in [
|
| 148 |
+
(1, 0, 0, 0), (-1, 0, 0, 0),
|
| 149 |
+
(0, 1, 0, 0), (0, -1, 0, 0),
|
| 150 |
+
(0, 0, 1, 0), (0, 0, -1, 0),
|
| 151 |
+
(0, 0, 0, 1), (0, 0, 0, -1),
|
| 152 |
+
]:
|
| 153 |
+
ni, nj, nk, nl = i + di, j + dj, k + dk, l + dl
|
| 154 |
+
if 0 <= ni < I and 0 <= nj < J and 0 <= nk < K and 0 <= nl < L:
|
| 155 |
+
neighbors.add((ni, nj, nk, nl))
|
| 156 |
+
return neighbors
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def _find_bmu_and_second(
|
| 160 |
+
x: torch.Tensor, weights: torch.Tensor
|
| 161 |
+
) -> Tuple[Tuple[int, int, int, int], Tuple[int, int, int, int], float, float]:
|
| 162 |
+
"""Encontra BMU e 2ª BMU para um vetor x.
|
| 163 |
+
|
| 164 |
+
Args:
|
| 165 |
+
x: tensor [4] — vetor 4D de entrada.
|
| 166 |
+
weights: tensor [I, J, K, L, 4] — pesos do SOM.
|
| 167 |
+
|
| 168 |
+
Returns:
|
| 169 |
+
(bmu_idx, second_bmu_idx, dist_bmu, dist_second)
|
| 170 |
+
"""
|
| 171 |
+
grid_shape = weights.shape[:-1]
|
| 172 |
+
flat_w = weights.view(-1, 4) # (I*J*K*L, 4)
|
| 173 |
+
diff = flat_w - x.view(1, 4)
|
| 174 |
+
dist_sq = torch.sum(diff * diff, dim=-1) # (I*J*K*L,)
|
| 175 |
+
|
| 176 |
+
# Top-2 menores distâncias
|
| 177 |
+
top2 = torch.topk(dist_sq, k=2, largest=False)
|
| 178 |
+
flat_idx_bmu = int(top2.indices[0].item())
|
| 179 |
+
flat_idx_2nd = int(top2.indices[1].item())
|
| 180 |
+
|
| 181 |
+
# Unravel
|
| 182 |
+
L = grid_shape[3]
|
| 183 |
+
K = grid_shape[2]
|
| 184 |
+
J = grid_shape[1]
|
| 185 |
+
|
| 186 |
+
def unravel(flat_idx: int) -> Tuple[int, int, int, int]:
|
| 187 |
+
i = flat_idx // (J * K * L)
|
| 188 |
+
r = flat_idx % (J * K * L)
|
| 189 |
+
j = r // (K * L)
|
| 190 |
+
r = r % (K * L)
|
| 191 |
+
k = r // L
|
| 192 |
+
l = r % L
|
| 193 |
+
return (i, j, k, l)
|
| 194 |
+
|
| 195 |
+
bmu = unravel(flat_idx_bmu)
|
| 196 |
+
second = unravel(flat_idx_2nd)
|
| 197 |
+
dist_bmu = float(math.sqrt(max(top2.values[0].item(), 0.0)))
|
| 198 |
+
dist_second = float(math.sqrt(max(top2.values[1].item(), 0.0)))
|
| 199 |
+
return bmu, second, dist_bmu, dist_second
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
# ============================================================================
|
| 203 |
+
# Métricas principais (1.1 - 1.4)
|
| 204 |
+
# ============================================================================
|
| 205 |
+
def quantization_error(
|
| 206 |
+
data: torch.Tensor, weights: torch.Tensor
|
| 207 |
+
) -> float:
|
| 208 |
+
"""1.1 Erro de Quantização (QE).
|
| 209 |
+
|
| 210 |
+
QE = (1/N) * Σ_i ||x_i - W_BMU(x_i)||₂
|
| 211 |
+
|
| 212 |
+
Args:
|
| 213 |
+
data: tensor [N, 4] — vetores de entrada.
|
| 214 |
+
weights: tensor [I, J, K, L, 4] — pesos do SOM.
|
| 215 |
+
|
| 216 |
+
Returns:
|
| 217 |
+
QE (float). 0.0 se data vazio.
|
| 218 |
+
"""
|
| 219 |
+
if data.numel() == 0:
|
| 220 |
+
return 0.0
|
| 221 |
+
flat_w = weights.view(-1, 4) # (M, 4)
|
| 222 |
+
# distância de cada amostra a todos os neurônios
|
| 223 |
+
# data: (N, 4), flat_w: (M, 4) -> (N, M)
|
| 224 |
+
diff = data.unsqueeze(1) - flat_w.unsqueeze(0)
|
| 225 |
+
dist_sq = torch.sum(diff * diff, dim=-1) # (N, M)
|
| 226 |
+
min_dist_sq, _ = torch.min(dist_sq, dim=1) # (N,)
|
| 227 |
+
qe = torch.mean(torch.sqrt(min_dist_sq.clamp(min=0.0)))
|
| 228 |
+
return float(qe.item())
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def topological_error(
|
| 232 |
+
data: torch.Tensor, weights: torch.Tensor
|
| 233 |
+
) -> float:
|
| 234 |
+
"""1.2 Erro Topológico (TE).
|
| 235 |
+
|
| 236 |
+
TE = (1/N) * Σ_i 𝟙[¬adjacent(BMU_1(x_i), BMU_2(x_i))]
|
| 237 |
+
|
| 238 |
+
Dois neurônios são adjacentes se differem em exatamente uma coordenada
|
| 239 |
+
por ±1 (6-connectivity em 4D).
|
| 240 |
+
|
| 241 |
+
Args:
|
| 242 |
+
data: tensor [N, 4]
|
| 243 |
+
weights: tensor [I, J, K, L, 4]
|
| 244 |
+
|
| 245 |
+
Returns:
|
| 246 |
+
TE ∈ [0, 1]. 0.0 se data vazio.
|
| 247 |
+
"""
|
| 248 |
+
if data.numel() == 0:
|
| 249 |
+
return 0.0
|
| 250 |
+
grid_shape = weights.shape[:-1]
|
| 251 |
+
flat_w = weights.view(-1, 4)
|
| 252 |
+
diff = data.unsqueeze(1) - flat_w.unsqueeze(0)
|
| 253 |
+
dist_sq = torch.sum(diff * diff, dim=-1) # (N, M)
|
| 254 |
+
|
| 255 |
+
# Top-2 BMUs para cada amostra
|
| 256 |
+
top2 = torch.topk(dist_sq, k=2, largest=False, dim=1)
|
| 257 |
+
flat_bmu = top2.indices[:, 0] # (N,)
|
| 258 |
+
flat_2nd = top2.indices[:, 1] # (N,)
|
| 259 |
+
|
| 260 |
+
# Unravel
|
| 261 |
+
L = grid_shape[3]
|
| 262 |
+
K = grid_shape[2]
|
| 263 |
+
J = grid_shape[1]
|
| 264 |
+
I = grid_shape[0]
|
| 265 |
+
|
| 266 |
+
def unravel_batch(flat_idx: torch.Tensor) -> torch.Tensor:
|
| 267 |
+
"""Retorna tensor (N, 4) com (i, j, k, l)."""
|
| 268 |
+
i = flat_idx // (J * K * L)
|
| 269 |
+
r = flat_idx % (J * K * L)
|
| 270 |
+
j = r // (K * L)
|
| 271 |
+
r = r % (K * L)
|
| 272 |
+
k = r // L
|
| 273 |
+
l = r % L
|
| 274 |
+
return torch.stack([i, j, k, l], dim=1).long()
|
| 275 |
+
|
| 276 |
+
bmu_idx = unravel_batch(flat_bmu) # (N, 4)
|
| 277 |
+
second_idx = unravel_batch(flat_2nd) # (N, 4)
|
| 278 |
+
|
| 279 |
+
# Adjacência: |Δcoord| soma = 1 (apenas uma coordenada muda por ±1)
|
| 280 |
+
delta = (bmu_idx - second_idx).abs() # (N, 4)
|
| 281 |
+
is_adjacent = (delta.sum(dim=1) == 1) & (delta.max(dim=1).values == 1)
|
| 282 |
+
|
| 283 |
+
te = 1.0 - float(is_adjacent.float().mean().item())
|
| 284 |
+
return te
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
def kaski_lagus_error(
|
| 288 |
+
data: torch.Tensor, weights: torch.Tensor, alpha: float = 0.5
|
| 289 |
+
) -> float:
|
| 290 |
+
"""1.3 Erro de Kaski-Lagus.
|
| 291 |
+
|
| 292 |
+
Combinação ponderada de QE normalizado e TE.
|
| 293 |
+
|
| 294 |
+
Variante implementada (Kaski & Lagus, 1999):
|
| 295 |
+
KL = α * MQE_norm + (1-α) * TE
|
| 296 |
+
onde MQE_norm = QE / mean(||x||) (normalização por escala dos dados).
|
| 297 |
+
|
| 298 |
+
Uma variante alternativa multiplica:
|
| 299 |
+
KL = QE * (1 + TE)
|
| 300 |
+
que penaliza QE quando TE é alto (usada se use_multiplicative=True).
|
| 301 |
+
|
| 302 |
+
Args:
|
| 303 |
+
data: tensor [N, 4]
|
| 304 |
+
weights: tensor [I, J, K, L, 4]
|
| 305 |
+
alpha: peso do QE normalizado (default 0.5)
|
| 306 |
+
|
| 307 |
+
Returns:
|
| 308 |
+
KL (float).
|
| 309 |
+
"""
|
| 310 |
+
if data.numel() == 0:
|
| 311 |
+
return 0.0
|
| 312 |
+
qe = quantization_error(data, weights)
|
| 313 |
+
te = topological_error(data, weights)
|
| 314 |
+
# Normalização: escala típica dos dados
|
| 315 |
+
data_norm = float(torch.mean(torch.norm(data, dim=1)).item())
|
| 316 |
+
qe_norm = qe / max(data_norm, 1e-8)
|
| 317 |
+
kl = alpha * qe_norm + (1.0 - alpha) * te
|
| 318 |
+
return float(kl)
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
def explained_variance_share(
|
| 322 |
+
data: torch.Tensor, weights: torch.Tensor
|
| 323 |
+
) -> float:
|
| 324 |
+
"""1.4 Variância Explicada (Share of Explained Variance).
|
| 325 |
+
|
| 326 |
+
VarExplained = 1 - Var(residual) / Var(total)
|
| 327 |
+
onde residual = x_i - W_BMU(x_i)
|
| 328 |
+
|
| 329 |
+
Implementação: computamos a variância total dos dados (soma das variâncias
|
| 330 |
+
por dimensão) e a variância dos resíduos. A fração explicada é:
|
| 331 |
+
|
| 332 |
+
VE = 1 - Σ_d Var(residual_d) / Σ_d Var(data_d)
|
| 333 |
+
|
| 334 |
+
Returns:
|
| 335 |
+
VE ∈ [0, 1]. Próximo de 1 = boa representação.
|
| 336 |
+
"""
|
| 337 |
+
if data.numel() == 0 or data.shape[0] < 2:
|
| 338 |
+
return 0.0
|
| 339 |
+
flat_w = weights.view(-1, 4)
|
| 340 |
+
diff = data.unsqueeze(1) - flat_w.unsqueeze(0)
|
| 341 |
+
dist_sq = torch.sum(diff * diff, dim=-1) # (N, M)
|
| 342 |
+
_, bmu_flat = torch.min(dist_sq, dim=1) # (N,)
|
| 343 |
+
bmu_w = flat_w[bmu_flat] # (N, 4)
|
| 344 |
+
residual = data - bmu_w # (N, 4)
|
| 345 |
+
|
| 346 |
+
# Variância por dimensão (sem viés, ddof=0)
|
| 347 |
+
var_data = torch.var(data, dim=0, unbiased=False) # (4,)
|
| 348 |
+
var_resid = torch.var(residual, dim=0, unbiased=False) # (4,)
|
| 349 |
+
|
| 350 |
+
total_var = float(torch.sum(var_data).item())
|
| 351 |
+
resid_var = float(torch.sum(var_resid).item())
|
| 352 |
+
if total_var < 1e-12:
|
| 353 |
+
return 0.0
|
| 354 |
+
ve = 1.0 - (resid_var / total_var)
|
| 355 |
+
return float(max(0.0, min(1.0, ve)))
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
# ============================================================================
|
| 359 |
+
# Indicadores de falha (2.1 - 2.4)
|
| 360 |
+
# ============================================================================
|
| 361 |
+
def topological_collapse(weights: torch.Tensor) -> Dict[str, Any]:
|
| 362 |
+
"""2.1 Colapso Topológico (Topological Collapse).
|
| 363 |
+
|
| 364 |
+
Detecta se os pesos colapsaram para um ponto (rank < 1) ou para uma linha
|
| 365 |
+
(rank < 2). Usa SVD sobre os pesos reshaped (M, 4).
|
| 366 |
+
|
| 367 |
+
Args:
|
| 368 |
+
weights: tensor [I, J, K, L, 4]
|
| 369 |
+
|
| 370 |
+
Returns:
|
| 371 |
+
Dict com:
|
| 372 |
+
- collapsed_to_point: bool (todos iguais)
|
| 373 |
+
- collapsed_to_line: bool (variação só em 1 direção)
|
| 374 |
+
- effective_rank: float (rank efetivo via razão de singular values)
|
| 375 |
+
- singular_values: list[float]
|
| 376 |
+
- severity: str ("none" | "line" | "point")
|
| 377 |
+
"""
|
| 378 |
+
flat_w = weights.view(-1, 4).clone() # (M, 4)
|
| 379 |
+
if flat_w.numel() == 0:
|
| 380 |
+
return {"collapsed_to_point": False, "collapsed_to_line": False,
|
| 381 |
+
"effective_rank": 0.0, "singular_values": [], "severity": "none"}
|
| 382 |
+
|
| 383 |
+
# Centraliza
|
| 384 |
+
flat_w_centered = flat_w - flat_w.mean(dim=0, keepdim=True)
|
| 385 |
+
|
| 386 |
+
# V6.5-V2-metrics FIX: usa SVD com fallback robusto para matrizes
|
| 387 |
+
# singulares/ill-conditioned. Se SVD falhar, usa np.linalg.eigh com
|
| 388 |
+
# regularização (adiciona pequeno ruído diagonal).
|
| 389 |
+
S_list: List[float] = []
|
| 390 |
+
try:
|
| 391 |
+
# Tentativa 1: SVD direto (mais estável que eigh)
|
| 392 |
+
U, S, V = torch.linalg.svd(flat_w_centered, full_matrices=False)
|
| 393 |
+
S_list = [float(s.item()) for s in S]
|
| 394 |
+
except Exception:
|
| 395 |
+
# Tentativa 2: eigendecomposição da matriz de covariância com regularização
|
| 396 |
+
try:
|
| 397 |
+
cov = flat_w_centered.t() @ flat_w_centered
|
| 398 |
+
# Regularização: adiciona pequeno valor diagonal para estabilizar
|
| 399 |
+
n = cov.shape[0]
|
| 400 |
+
reg = 1e-6 * float(cov.diag().max().item()) if n > 0 else 1e-6
|
| 401 |
+
cov_reg = cov + torch.eye(n) * reg
|
| 402 |
+
S_eigh, _ = torch.linalg.eigh(cov_reg)
|
| 403 |
+
S_eigh = S_eigh.flip(0) # descending order
|
| 404 |
+
# Converte autovalores em "singular values" aproximados (sqrt)
|
| 405 |
+
S_list = [float(math.sqrt(max(s.item(), 0.0))) for s in S_eigh]
|
| 406 |
+
except Exception:
|
| 407 |
+
# Tentativa 3: usa norma de Frobenius como aproximação
|
| 408 |
+
s_frob = float(torch.norm(flat_w_centered).item())
|
| 409 |
+
S_list = [s_frob, 0.0, 0.0, 0.0]
|
| 410 |
+
|
| 411 |
+
if not S_list:
|
| 412 |
+
return {"collapsed_to_point": False, "collapsed_to_line": False,
|
| 413 |
+
"effective_rank": 0.0, "singular_values": [], "severity": "none"}
|
| 414 |
+
|
| 415 |
+
s_max = max(S_list[0] if S_list else 0.0, 1e-12)
|
| 416 |
+
|
| 417 |
+
# Effective rank: número de singular values > 1% do maior
|
| 418 |
+
threshold = 0.01 * s_max
|
| 419 |
+
n_significant = sum(1 for s in S_list if s > threshold)
|
| 420 |
+
# Effective rank (float, via entropy)
|
| 421 |
+
s_norm = [s / s_max for s in S_list]
|
| 422 |
+
s_norm = [s for s in s_norm if s > 1e-10]
|
| 423 |
+
if s_norm:
|
| 424 |
+
p = torch.tensor(s_norm)
|
| 425 |
+
p_norm = p / p.sum()
|
| 426 |
+
entropy = -float(torch.sum(p_norm * torch.log(p_norm)).item())
|
| 427 |
+
effective_rank = math.exp(entropy)
|
| 428 |
+
else:
|
| 429 |
+
effective_rank = 0.0
|
| 430 |
+
|
| 431 |
+
collapsed_to_point = n_significant <= 1 and s_max < 1e-6
|
| 432 |
+
collapsed_to_line = n_significant == 1 and s_max >= 1e-6
|
| 433 |
+
|
| 434 |
+
if collapsed_to_point:
|
| 435 |
+
severity = "point"
|
| 436 |
+
elif collapsed_to_line:
|
| 437 |
+
severity = "line"
|
| 438 |
+
else:
|
| 439 |
+
severity = "none"
|
| 440 |
+
|
| 441 |
+
return {
|
| 442 |
+
"collapsed_to_point": bool(collapsed_to_point),
|
| 443 |
+
"collapsed_to_line": bool(collapsed_to_line),
|
| 444 |
+
"effective_rank": float(effective_rank),
|
| 445 |
+
"singular_values": S_list,
|
| 446 |
+
"severity": severity,
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
|
| 450 |
+
def dead_neuron_rate(
|
| 451 |
+
data: torch.Tensor, weights: torch.Tensor
|
| 452 |
+
) -> Dict[str, Any]:
|
| 453 |
+
"""2.2 Taxa de Neurônios Mortos (Dead Neurons).
|
| 454 |
+
|
| 455 |
+
DeadRate = (n_neurons - n_active_neurons) / n_neurons
|
| 456 |
+
|
| 457 |
+
Um neurônio é "ativo" se é BMU para pelo menos uma amostra.
|
| 458 |
+
|
| 459 |
+
Args:
|
| 460 |
+
data: tensor [N, 4]
|
| 461 |
+
weights: tensor [I, J, K, L, 4]
|
| 462 |
+
|
| 463 |
+
Returns:
|
| 464 |
+
Dict com:
|
| 465 |
+
- dead_neuron_rate: float ∈ [0, 1]
|
| 466 |
+
- n_dead: int
|
| 467 |
+
- n_active: int
|
| 468 |
+
- n_total: int
|
| 469 |
+
- dead_indices: list de (i,j,k,l)
|
| 470 |
+
"""
|
| 471 |
+
n_total = weights.shape[0] * weights.shape[1] * weights.shape[2] * weights.shape[3]
|
| 472 |
+
if data.numel() == 0:
|
| 473 |
+
return {"dead_neuron_rate": 1.0, "n_dead": n_total, "n_active": 0,
|
| 474 |
+
"n_total": n_total, "dead_indices": []}
|
| 475 |
+
|
| 476 |
+
flat_w = weights.view(-1, 4)
|
| 477 |
+
diff = data.unsqueeze(1) - flat_w.unsqueeze(0)
|
| 478 |
+
dist_sq = torch.sum(diff * diff, dim=-1) # (N, M)
|
| 479 |
+
_, bmu_flat = torch.min(dist_sq, dim=1) # (N,)
|
| 480 |
+
|
| 481 |
+
active_mask = torch.zeros(n_total, dtype=torch.bool)
|
| 482 |
+
active_mask[bmu_flat] = True
|
| 483 |
+
n_active = int(active_mask.sum().item())
|
| 484 |
+
n_dead = n_total - n_active
|
| 485 |
+
|
| 486 |
+
# Coleta índices mortos
|
| 487 |
+
dead_flat = (~active_mask).nonzero(as_tuple=True)[0]
|
| 488 |
+
L_dim = weights.shape[3]
|
| 489 |
+
K_dim = weights.shape[2]
|
| 490 |
+
J_dim = weights.shape[1]
|
| 491 |
+
dead_indices = []
|
| 492 |
+
for flat_idx in dead_flat.tolist():
|
| 493 |
+
i = flat_idx // (J_dim * K_dim * L_dim)
|
| 494 |
+
r = flat_idx % (J_dim * K_dim * L_dim)
|
| 495 |
+
j = r // (K_dim * L_dim)
|
| 496 |
+
r = r % (K_dim * L_dim)
|
| 497 |
+
k = r // L_dim
|
| 498 |
+
l = r % L_dim
|
| 499 |
+
dead_indices.append((i, j, k, l))
|
| 500 |
+
|
| 501 |
+
return {
|
| 502 |
+
"dead_neuron_rate": float(n_dead / n_total) if n_total > 0 else 0.0,
|
| 503 |
+
"n_dead": int(n_dead),
|
| 504 |
+
"n_active": int(n_active),
|
| 505 |
+
"n_total": int(n_total),
|
| 506 |
+
"dead_indices": dead_indices[:50], # limita para não explodir JSON
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
|
| 510 |
+
def quantization_error_stagnation(
|
| 511 |
+
history: SOMMetricHistory,
|
| 512 |
+
high_threshold_frac: float = 0.7,
|
| 513 |
+
slope_tolerance: float = 1e-4,
|
| 514 |
+
window_size: int = 5,
|
| 515 |
+
) -> Dict[str, Any]:
|
| 516 |
+
"""2.3 Estagnação do Erro de Quantização.
|
| 517 |
+
|
| 518 |
+
Detecta se o QE estabilizou em patamar elevado:
|
| 519 |
+
- |slope(QE)| < slope_tolerance (estável)
|
| 520 |
+
- QE_atual > high_threshold_frac * max(QE_history) (alto)
|
| 521 |
+
|
| 522 |
+
Args:
|
| 523 |
+
history: histórico de métricas SOM.
|
| 524 |
+
high_threshold_frac: fração do QE máximo para considerar "alto".
|
| 525 |
+
slope_tolerance: tolerância de slope para considerar "estável".
|
| 526 |
+
window_size: nº de épocas recentes para regressão linear.
|
| 527 |
+
|
| 528 |
+
Returns:
|
| 529 |
+
Dict com:
|
| 530 |
+
- is_stagnant: bool
|
| 531 |
+
- current_qe: float
|
| 532 |
+
- max_qe: float
|
| 533 |
+
- slope: float
|
| 534 |
+
- severity: str ("none" | "mild" | "severe")
|
| 535 |
+
"""
|
| 536 |
+
qe_list = list(history.qe_history)
|
| 537 |
+
if len(qe_list) < window_size + 2:
|
| 538 |
+
return {"is_stagnant": False, "current_qe": 0.0, "max_qe": 0.0,
|
| 539 |
+
"slope": 0.0, "severity": "none", "n_epochs": len(qe_list)}
|
| 540 |
+
|
| 541 |
+
# Regressão linear sobre as últimas window_size épocas
|
| 542 |
+
recent = qe_list[-window_size:]
|
| 543 |
+
n = len(recent)
|
| 544 |
+
x_mean = (n - 1) / 2.0
|
| 545 |
+
y_mean = sum(recent) / n
|
| 546 |
+
num = sum((i - x_mean) * (y - y_mean) for i, y in enumerate(recent))
|
| 547 |
+
den = sum((i - x_mean) ** 2 for i in range(n))
|
| 548 |
+
slope = num / den if den > 0 else 0.0
|
| 549 |
+
|
| 550 |
+
current_qe = float(qe_list[-1])
|
| 551 |
+
max_qe = float(max(qe_list))
|
| 552 |
+
is_high = current_qe > high_threshold_frac * max_qe
|
| 553 |
+
is_stable = abs(slope) < slope_tolerance
|
| 554 |
+
|
| 555 |
+
is_stagnant = is_stable and is_high
|
| 556 |
+
|
| 557 |
+
if is_stagnant:
|
| 558 |
+
if current_qe > 0.9 * max_qe:
|
| 559 |
+
severity = "severe"
|
| 560 |
+
else:
|
| 561 |
+
severity = "mild"
|
| 562 |
+
else:
|
| 563 |
+
severity = "none"
|
| 564 |
+
|
| 565 |
+
return {
|
| 566 |
+
"is_stagnant": bool(is_stagnant),
|
| 567 |
+
"current_qe": current_qe,
|
| 568 |
+
"max_qe": max_qe,
|
| 569 |
+
"slope": float(slope),
|
| 570 |
+
"severity": severity,
|
| 571 |
+
"n_epochs": len(qe_list),
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
|
| 575 |
+
def neighborhood_crossing(
|
| 576 |
+
history: SOMMetricHistory,
|
| 577 |
+
initial_window: int = 5,
|
| 578 |
+
final_window: int = 5,
|
| 579 |
+
growth_threshold: float = 1.5,
|
| 580 |
+
) -> Dict[str, Any]:
|
| 581 |
+
"""2.4 Cruzamento de Vizinhança (Neighborhood Crossing).
|
| 582 |
+
|
| 583 |
+
Detecta crescimento repentino ou oscilação crônica do TE nas épocas
|
| 584 |
+
finais:
|
| 585 |
+
- std(TE_final) > growth_threshold * std(TE_initial) (oscilação cresceu)
|
| 586 |
+
- TE[-1] > TE[-5] (crescimento no final)
|
| 587 |
+
|
| 588 |
+
Args:
|
| 589 |
+
history: histórico de métricas SOM.
|
| 590 |
+
initial_window: nº de épocas iniciais para baseline.
|
| 591 |
+
final_window: nº de épocas finais para comparação.
|
| 592 |
+
growth_threshold: razão de std para considerar "crescimento crônico".
|
| 593 |
+
|
| 594 |
+
Returns:
|
| 595 |
+
Dict com:
|
| 596 |
+
- detected: bool
|
| 597 |
+
- te_initial_mean: float
|
| 598 |
+
- te_final_mean: float
|
| 599 |
+
- te_initial_std: float
|
| 600 |
+
- te_final_std: float
|
| 601 |
+
- std_ratio: float
|
| 602 |
+
- is_oscillating: bool
|
| 603 |
+
- is_growing: bool
|
| 604 |
+
- severity: str ("none" | "mild" | "severe")
|
| 605 |
+
"""
|
| 606 |
+
te_list = list(history.te_history)
|
| 607 |
+
if len(te_list) < initial_window + final_window:
|
| 608 |
+
return {"detected": False, "te_initial_mean": 0.0, "te_final_mean": 0.0,
|
| 609 |
+
"te_initial_std": 0.0, "te_final_std": 0.0, "std_ratio": 0.0,
|
| 610 |
+
"is_oscillating": False, "is_growing": False, "severity": "none",
|
| 611 |
+
"n_epochs": len(te_list)}
|
| 612 |
+
|
| 613 |
+
initial = te_list[:initial_window]
|
| 614 |
+
final = te_list[-final_window:]
|
| 615 |
+
|
| 616 |
+
init_mean = sum(initial) / len(initial)
|
| 617 |
+
final_mean = sum(final) / len(final)
|
| 618 |
+
init_var = sum((x - init_mean) ** 2 for x in initial) / len(initial)
|
| 619 |
+
final_var = sum((x - final_mean) ** 2 for x in final) / len(final)
|
| 620 |
+
init_std = math.sqrt(init_var)
|
| 621 |
+
final_std = math.sqrt(final_var)
|
| 622 |
+
|
| 623 |
+
std_ratio = final_std / max(init_std, 1e-8)
|
| 624 |
+
is_oscillating = std_ratio > growth_threshold
|
| 625 |
+
is_growing = final_mean > init_mean and (final_mean - init_mean) > 0.05
|
| 626 |
+
|
| 627 |
+
detected = is_oscillating or is_growing
|
| 628 |
+
|
| 629 |
+
if detected:
|
| 630 |
+
if std_ratio > 2.0 * growth_threshold or is_growing:
|
| 631 |
+
severity = "severe"
|
| 632 |
+
else:
|
| 633 |
+
severity = "mild"
|
| 634 |
+
else:
|
| 635 |
+
severity = "none"
|
| 636 |
+
|
| 637 |
+
return {
|
| 638 |
+
"detected": bool(detected),
|
| 639 |
+
"te_initial_mean": float(init_mean),
|
| 640 |
+
"te_final_mean": float(final_mean),
|
| 641 |
+
"te_initial_std": float(init_std),
|
| 642 |
+
"te_final_std": float(final_std),
|
| 643 |
+
"std_ratio": float(std_ratio),
|
| 644 |
+
"is_oscillating": bool(is_oscillating),
|
| 645 |
+
"is_growing": bool(is_growing),
|
| 646 |
+
"severity": severity,
|
| 647 |
+
"n_epochs": len(te_list),
|
| 648 |
+
}
|
| 649 |
+
|
| 650 |
+
|
| 651 |
+
# ============================================================================
|
| 652 |
+
# Função agregadora: compute_all_metrics
|
| 653 |
+
# ============================================================================
|
| 654 |
+
def compute_all_metrics(
|
| 655 |
+
data: torch.Tensor,
|
| 656 |
+
weights: torch.Tensor,
|
| 657 |
+
history: Optional[SOMMetricHistory] = None,
|
| 658 |
+
) -> Dict[str, Any]:
|
| 659 |
+
"""Computa todas as métricas SOM de uma vez.
|
| 660 |
+
|
| 661 |
+
Args:
|
| 662 |
+
data: tensor [N, 4] — buffer de vetores 4D.
|
| 663 |
+
weights: tensor [I, J, K, L, 4] — pesos do SOM.
|
| 664 |
+
history: histórico de métricas (opcional, para estagnação e cruzamento).
|
| 665 |
+
|
| 666 |
+
Returns:
|
| 667 |
+
Dict com:
|
| 668 |
+
- quantization_error: float
|
| 669 |
+
- topological_error: float
|
| 670 |
+
- kaski_lagus_error: float
|
| 671 |
+
- explained_variance_share: float
|
| 672 |
+
- topological_collapse: Dict
|
| 673 |
+
- dead_neuron_rate: Dict
|
| 674 |
+
- quantization_error_stagnation: Dict (se history fornecido)
|
| 675 |
+
- neighborhood_crossing: Dict (se history fornecido)
|
| 676 |
+
- overall_health: str ("healthy" | "warning" | "critical")
|
| 677 |
+
- failure_indicators: list[str]
|
| 678 |
+
"""
|
| 679 |
+
# V6.5-V2-metrics FIX: wrap entire computation in try/except para evitar
|
| 680 |
+
# crashes fatais quando o SOM tem pesos degenerados (ex: todos iguais
|
| 681 |
+
# após muito treinamento sem diversidade). Em caso de erro, retorna
|
| 682 |
+
# métricas safe-default em vez de crashar o treino.
|
| 683 |
+
try:
|
| 684 |
+
return _compute_all_metrics_impl(data, weights, history)
|
| 685 |
+
except Exception as e:
|
| 686 |
+
logger.warning(
|
| 687 |
+
f"[som_metrics] compute_all_metrics failed (using safe defaults): {e}"
|
| 688 |
+
)
|
| 689 |
+
return {
|
| 690 |
+
"quantization_error": 0.0,
|
| 691 |
+
"topological_error": 0.0,
|
| 692 |
+
"kaski_lagus_error": 0.0,
|
| 693 |
+
"explained_variance_share": 0.0,
|
| 694 |
+
"topological_collapse": {
|
| 695 |
+
"collapsed_to_point": False, "collapsed_to_line": False,
|
| 696 |
+
"effective_rank": 0.0, "singular_values": [],
|
| 697 |
+
"severity": "none", "error": str(e)[:200],
|
| 698 |
+
},
|
| 699 |
+
"dead_neuron_rate": {
|
| 700 |
+
"dead_neuron_rate": 1.0, "n_dead": 0, "n_active": 0,
|
| 701 |
+
"n_total": 0, "dead_indices": [], "error": str(e)[:200],
|
| 702 |
+
},
|
| 703 |
+
"qe_stagnation": {
|
| 704 |
+
"is_stagnant": False, "current_qe": 0.0, "max_qe": 0.0,
|
| 705 |
+
"slope": 0.0, "severity": "none", "n_epochs": 0,
|
| 706 |
+
},
|
| 707 |
+
"neighborhood_crossing": {
|
| 708 |
+
"detected": False, "te_initial_mean": 0.0, "te_final_mean": 0.0,
|
| 709 |
+
"te_initial_std": 0.0, "te_final_std": 0.0, "std_ratio": 0.0,
|
| 710 |
+
"is_oscillating": False, "is_growing": False, "severity": "none",
|
| 711 |
+
"n_epochs": 0,
|
| 712 |
+
},
|
| 713 |
+
"overall_health": "unknown",
|
| 714 |
+
"failure_indicators": [f"metrics_computation_error: {str(e)[:100]}"],
|
| 715 |
+
"n_failure_indicators": 1,
|
| 716 |
+
"error": str(e)[:300],
|
| 717 |
+
}
|
| 718 |
+
|
| 719 |
+
|
| 720 |
+
def _compute_all_metrics_impl(
|
| 721 |
+
data: torch.Tensor,
|
| 722 |
+
weights: torch.Tensor,
|
| 723 |
+
history: Optional[SOMMetricHistory] = None,
|
| 724 |
+
) -> Dict[str, Any]:
|
| 725 |
+
"""Implementação interna de compute_all_metrics (sem try/except wrapper)."""
|
| 726 |
+
metrics: Dict[str, Any] = {}
|
| 727 |
+
|
| 728 |
+
# 1.1 - 1.4: Métricas principais
|
| 729 |
+
metrics["quantization_error"] = quantization_error(data, weights)
|
| 730 |
+
metrics["topological_error"] = topological_error(data, weights)
|
| 731 |
+
metrics["kaski_lagus_error"] = kaski_lagus_error(data, weights)
|
| 732 |
+
metrics["explained_variance_share"] = explained_variance_share(data, weights)
|
| 733 |
+
|
| 734 |
+
# 2.1: Colapso topológico
|
| 735 |
+
collapse = topological_collapse(weights)
|
| 736 |
+
metrics["topological_collapse"] = collapse
|
| 737 |
+
|
| 738 |
+
# 2.2: Dead neurons
|
| 739 |
+
dead = dead_neuron_rate(data, weights)
|
| 740 |
+
metrics["dead_neuron_rate"] = dead
|
| 741 |
+
|
| 742 |
+
# 2.3: Estagnação do QE (requer histórico)
|
| 743 |
+
if history is not None and len(history.qe_history) > 5:
|
| 744 |
+
metrics["qe_stagnation"] = quantization_error_stagnation(history)
|
| 745 |
+
else:
|
| 746 |
+
metrics["qe_stagnation"] = {
|
| 747 |
+
"is_stagnant": False, "current_qe": metrics["quantization_error"],
|
| 748 |
+
"max_qe": metrics["quantization_error"], "slope": 0.0,
|
| 749 |
+
"severity": "none", "n_epochs": 0,
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
# 2.4: Cruzamento de vizinhança (requer histórico)
|
| 753 |
+
if history is not None and len(history.te_history) > 10:
|
| 754 |
+
metrics["neighborhood_crossing"] = neighborhood_crossing(history)
|
| 755 |
+
else:
|
| 756 |
+
metrics["neighborhood_crossing"] = {
|
| 757 |
+
"detected": False, "te_initial_mean": 0.0, "te_final_mean": 0.0,
|
| 758 |
+
"te_initial_std": 0.0, "te_final_std": 0.0, "std_ratio": 0.0,
|
| 759 |
+
"is_oscillating": False, "is_growing": False, "severity": "none",
|
| 760 |
+
"n_epochs": 0,
|
| 761 |
+
}
|
| 762 |
+
|
| 763 |
+
# Avaliação de saúde geral
|
| 764 |
+
failure_indicators: List[str] = []
|
| 765 |
+
if collapse["collapsed_to_point"]:
|
| 766 |
+
failure_indicators.append("topological_collapse_point")
|
| 767 |
+
elif collapse["collapsed_to_line"]:
|
| 768 |
+
failure_indicators.append("topological_collapse_line")
|
| 769 |
+
if dead["dead_neuron_rate"] > 0.5:
|
| 770 |
+
failure_indicators.append(f"dead_neuron_rate_high_{dead['dead_neuron_rate']:.2f}")
|
| 771 |
+
if metrics["qe_stagnation"]["is_stagnant"]:
|
| 772 |
+
failure_indicators.append(f"qe_stagnant_{metrics['qe_stagnation']['severity']}")
|
| 773 |
+
if metrics["neighborhood_crossing"]["detected"]:
|
| 774 |
+
failure_indicators.append(
|
| 775 |
+
f"neighborhood_crossing_{metrics['neighborhood_crossing']['severity']}"
|
| 776 |
+
)
|
| 777 |
+
if metrics["explained_variance_share"] < 0.3:
|
| 778 |
+
failure_indicators.append(
|
| 779 |
+
f"low_explained_variance_{metrics['explained_variance_share']:.2f}"
|
| 780 |
+
)
|
| 781 |
+
if metrics["topological_error"] > 0.5:
|
| 782 |
+
failure_indicators.append(
|
| 783 |
+
f"high_topological_error_{metrics['topological_error']:.2f}"
|
| 784 |
+
)
|
| 785 |
+
|
| 786 |
+
if len(failure_indicators) >= 2:
|
| 787 |
+
overall_health = "critical"
|
| 788 |
+
elif len(failure_indicators) == 1:
|
| 789 |
+
overall_health = "warning"
|
| 790 |
+
else:
|
| 791 |
+
overall_health = "healthy"
|
| 792 |
+
|
| 793 |
+
metrics["failure_indicators"] = failure_indicators
|
| 794 |
+
metrics["overall_health"] = overall_health
|
| 795 |
+
metrics["n_failure_indicators"] = len(failure_indicators)
|
| 796 |
+
|
| 797 |
+
return metrics
|
| 798 |
+
|
| 799 |
+
|
| 800 |
+
__all__ = [
|
| 801 |
+
"SOMMetricHistory",
|
| 802 |
+
"quantization_error",
|
| 803 |
+
"topological_error",
|
| 804 |
+
"kaski_lagus_error",
|
| 805 |
+
"explained_variance_share",
|
| 806 |
+
"topological_collapse",
|
| 807 |
+
"dead_neuron_rate",
|
| 808 |
+
"quantization_error_stagnation",
|
| 809 |
+
"neighborhood_crossing",
|
| 810 |
+
"compute_all_metrics",
|
| 811 |
+
]
|
train_v6_5_v2.py
CHANGED
|
@@ -1,34 +1,52 @@
|
|
| 1 |
-
"""train_v6_5_v2.py — V6.5-V2-
|
| 2 |
|
| 3 |
═══════════════════════════════════════════════════════════════════════════════
|
| 4 |
-
V6.5-V2-
|
| 5 |
═══════════════════════════════════════════════════════════════════════════════
|
| 6 |
|
| 7 |
-
User requirements (latest, V6.5-V2-
|
| 8 |
-
1.
|
| 9 |
-
2.
|
| 10 |
-
3.
|
|
|
|
| 11 |
após uso.
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
(n_hypotheses, n_trials, hyp_train_steps) DINÂMICOS e AUTOMÁTICOS
|
| 15 |
-
conforme a loss e recorrência de punições.
|
| 16 |
-
6. Corrigir erros e falhas.
|
| 17 |
-
7. FASE 1 CONHECIMENTO (meta mínima 6000+ samples, 100 em 100) na
|
| 18 |
sequência exata de 8 datasets.
|
| 19 |
-
|
| 20 |
100 em 100) sobre BrunoN-Dev/corpus-ptbr-v1.
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
aprovados ao HF (sobrescrevendo antigos).
|
| 23 |
|
| 24 |
-
V6.5-V2-
|
| 25 |
-
- NOVO:
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
- NOVO:
|
| 30 |
-
- NOVO:
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
"""
|
| 33 |
|
| 34 |
from __future__ import annotations
|
|
@@ -96,16 +114,38 @@ FP16_BENCH = benchmark_fp16_matmul(size=4000, warmup=1, iters=2)
|
|
| 96 |
logger.info(f"[V6.5-V2] Xeon FP16 benchmark: {FP16_BENCH}")
|
| 97 |
|
| 98 |
# ============================================================================
|
| 99 |
-
# 2. Configurações V6.5-V2 (864 neurons,
|
| 100 |
# ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
BATCH_SIZE = 16
|
| 102 |
MAX_SEQ_LEN = 8
|
| 103 |
|
| 104 |
-
# V2-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
SOM_GRID = (6, 6, 6, 4) # 864 neurons
|
| 109 |
T_MAX = 10000
|
| 110 |
N_START = 10
|
| 111 |
LAMBDA_EWC = 0.02
|
|
@@ -113,41 +153,41 @@ ALPHA0 = 0.1
|
|
| 113 |
SIGMA0 = 1.5
|
| 114 |
DIM_CHOICE = "y"
|
| 115 |
|
| 116 |
-
# V2-dynamic — HypothesisEnsemble parameters (
|
| 117 |
-
N_HYPOTHESES =
|
| 118 |
-
MAX_N_HYPOTHESES =
|
| 119 |
-
MIN_N_HYPOTHESES = 4
|
| 120 |
-
N_TRIALS = 3
|
| 121 |
MIN_N_TRIALS = 1
|
| 122 |
MAX_N_TRIALS = 6
|
| 123 |
-
HYP_TRAIN_STEPS =
|
| 124 |
-
MIN_HYP_TRAIN_STEPS =
|
| 125 |
-
MAX_HYP_TRAIN_STEPS =
|
| 126 |
HYP_LR = 1e-4
|
| 127 |
-
HYP_HIDDEN_DIM =
|
| 128 |
LOSS_HISTORY_WINDOW = 8
|
| 129 |
PUNISHMENT_WINDOW = 12
|
| 130 |
|
| 131 |
# V2-dynamic-memory — Buffer sliding window (evita OOM em treino longo)
|
| 132 |
-
# Após processar cada chunk, se buffer > MAX_BUFFER_SIZE, trunca para os
|
| 133 |
-
# últimos MAX_BUFFER_SIZE amostras. Isto limita o crescimento de memória
|
| 134 |
-
# sem perder o contexto recente. Os pesos do SOM já capturam o conhecimento
|
| 135 |
-
# acumulado, então o buffer só precisa conter amostras recentes para ajuste fino.
|
| 136 |
MAX_BUFFER_SIZE = 256
|
| 137 |
|
| 138 |
# Streaming — User requirement: "streaming de 100 em 100 samples"
|
| 139 |
STREAM_BATCH_SIZE = 100
|
| 140 |
|
| 141 |
-
# V2-dynamic — Metas atualizadas:
|
| 142 |
-
META_MINIMA_CONHECIMENTO =
|
| 143 |
-
MAX_SAMPLES_PER_DATASET_CONHECIMENTO =
|
| 144 |
-
META_MINIMA_PUNICAO =
|
| 145 |
-
MAX_SAMPLES_PUNICAO =
|
| 146 |
-
|
| 147 |
-
# User requirement: "não reduzir tempo e não gerar dados sintéticos"
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
|
| 152 |
# Storage critical
|
| 153 |
STORAGE_CRITICAL_PCT = 90
|
|
@@ -281,38 +321,118 @@ CGROUP_MEM_CRITICAL_PCT = 80 # se RSS > 80% do cgroup, salvar estado e parar
|
|
| 281 |
|
| 282 |
|
| 283 |
def aggressive_memory_cleanup() -> Dict[str, Any]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
gc.collect()
|
| 285 |
gc.collect()
|
| 286 |
try:
|
| 287 |
import torch
|
| 288 |
if torch.cuda.is_available():
|
| 289 |
torch.cuda.empty_cache()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
except Exception:
|
| 291 |
pass
|
| 292 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
|
| 294 |
|
| 295 |
def aggressive_storage_cleanup() -> Dict[str, Any]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
n_removed = 0
|
| 297 |
mb_freed = 0.0
|
|
|
|
|
|
|
| 298 |
hf_cache = Path.home() / ".cache" / "huggingface"
|
| 299 |
if hf_cache.exists():
|
| 300 |
-
for
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
try:
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
|
|
|
| 314 |
except Exception:
|
| 315 |
pass
|
|
|
|
| 316 |
return {
|
| 317 |
"n_files_removed": n_removed,
|
| 318 |
"mb_freed": float(mb_freed),
|
|
@@ -428,7 +548,145 @@ def save_model_states_for_evaluation(
|
|
| 428 |
|
| 429 |
|
| 430 |
# ============================================================================
|
| 431 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 432 |
# ============================================================================
|
| 433 |
def stream_dataset_in_chunks(
|
| 434 |
dataset_name: str,
|
|
@@ -437,9 +695,20 @@ def stream_dataset_in_chunks(
|
|
| 437 |
hf_token: Optional[str] = None,
|
| 438 |
timeout_per_chunk: int = 60,
|
| 439 |
) -> Iterator[List[str]]:
|
| 440 |
-
"""V6.5-V2 — Stream a dataset in chunks of `chunk_size` samples.
|
| 441 |
-
|
| 442 |
-
User requirement: "streaming de 100 em 100 samples até
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 443 |
"""
|
| 444 |
try:
|
| 445 |
from bigru_t.data.streaming_datasets import stream_dataset
|
|
@@ -449,54 +718,68 @@ def stream_dataset_in_chunks(
|
|
| 449 |
|
| 450 |
yielded_total = 0
|
| 451 |
chunk_idx = 0
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 465 |
logger.warning(
|
| 466 |
-
f"[V6.5-V2] {dataset_name}
|
| 467 |
-
f"
|
| 468 |
-
f"(collected {len(chunk)}/{this_chunk_target})"
|
| 469 |
)
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
if len(chunk) >= this_chunk_target:
|
| 477 |
-
break
|
| 478 |
-
except Exception as e:
|
| 479 |
-
logger.warning(f"[V6.5-V2] {dataset_name} chunk {chunk_idx+1} error: {e}")
|
| 480 |
-
if chunk:
|
| 481 |
yield chunk
|
| 482 |
yielded_total += len(chunk)
|
| 483 |
chunk_idx += 1
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
|
|
|
|
|
|
|
| 493 |
yield chunk
|
| 494 |
yielded_total += len(chunk)
|
| 495 |
chunk_idx += 1
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
)
|
|
|
|
| 500 |
|
| 501 |
|
| 502 |
# ============================================================================
|
|
@@ -669,6 +952,12 @@ def run_fase_conhecimento(
|
|
| 669 |
chunk_idx_global = 0
|
| 670 |
accuracies_log: List[Dict[str, Any]] = []
|
| 671 |
attention_log: List[Dict[str, Any]] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 672 |
|
| 673 |
for ds_idx, dataset_name in enumerate(CONHECIMENTO_DATASETS):
|
| 674 |
if storage_critical_stopped:
|
|
@@ -822,9 +1111,54 @@ def run_fase_conhecimento(
|
|
| 822 |
f"[V6.5-V2] Buffer trimmed: -{overflow} → "
|
| 823 |
f"{len(kls.buffer_4d)} samples (cap={MAX_BUFFER_SIZE})"
|
| 824 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 825 |
# Aggressive memory cleanup between chunks
|
| 826 |
if chunk_idx_global % 2 == 0:
|
| 827 |
aggressive_memory_cleanup()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 828 |
except Exception as e:
|
| 829 |
logger.error(f"[V6.5-V2] Dataset {dataset_name} failed: {e}")
|
| 830 |
traceback.print_exc()
|
|
@@ -854,6 +1188,12 @@ def run_fase_conhecimento(
|
|
| 854 |
"n_chunks": int(chunk_idx_global),
|
| 855 |
"accuracies_log": accuracies_log[-50:], # últimos 50
|
| 856 |
"attention_log": attention_log[-20:],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 857 |
"final_state": kls.get_v2_metrics(),
|
| 858 |
}
|
| 859 |
|
|
@@ -865,14 +1205,24 @@ def run_fase_punicão(
|
|
| 865 |
kls: KohonenLearningSystemV2,
|
| 866 |
hf_token: Optional[str],
|
| 867 |
) -> Dict[str, Any]:
|
| 868 |
-
"""Fase 2 — TREINAMENTO COM PUNIÇÃO: streaming
|
| 869 |
-
|
| 870 |
-
User requirement: "
|
| 871 |
-
'BrunoN-Dev/corpus-ptbr-v1'
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 876 |
|
| 877 |
Returns:
|
| 878 |
Dict com métricas da fase TREINAMENTO COM PUNIÇÃO.
|
|
@@ -888,6 +1238,13 @@ def run_fase_punicão(
|
|
| 888 |
logger.info(f" hyp_train_steps: {HYP_TRAIN_STEPS}")
|
| 889 |
logger.info("=" * 80 + "\n")
|
| 890 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 891 |
step = 0
|
| 892 |
t_start = time.time()
|
| 893 |
samples_processed = 0
|
|
@@ -897,6 +1254,8 @@ def run_fase_punicão(
|
|
| 897 |
hypotheses_log: List[Dict[str, Any]] = []
|
| 898 |
accuracies_log: List[Dict[str, Any]] = []
|
| 899 |
delta_applications_log: List[Dict[str, Any]] = []
|
|
|
|
|
|
|
| 900 |
|
| 901 |
try:
|
| 902 |
chunk_iter = stream_dataset_in_chunks(
|
|
@@ -1005,6 +1364,50 @@ def run_fase_punicão(
|
|
| 1005 |
"ewc_reference_set": bool(kls.som.old_weights_w is not None),
|
| 1006 |
})
|
| 1007 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1008 |
step += 1
|
| 1009 |
time.sleep(INTER_BATCH_PAUSE_S)
|
| 1010 |
except Exception as e:
|
|
@@ -1024,6 +1427,13 @@ def run_fase_punicão(
|
|
| 1024 |
)
|
| 1025 |
if chunk_idx_global % 2 == 0:
|
| 1026 |
aggressive_memory_cleanup()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1027 |
except Exception as e:
|
| 1028 |
logger.error(f"[V6.5-V2] PUNIÇÃO dataset failed: {e}")
|
| 1029 |
traceback.print_exc()
|
|
@@ -1052,6 +1462,12 @@ def run_fase_punicão(
|
|
| 1052 |
"hypotheses_trainings": hypotheses_log,
|
| 1053 |
"delta_applications": delta_applications_log,
|
| 1054 |
"accuracies_log": accuracies_log[-50:],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1055 |
"final_v2_state": kls.get_v2_metrics(),
|
| 1056 |
}
|
| 1057 |
|
|
@@ -1592,11 +2008,26 @@ def main() -> int:
|
|
| 1592 |
aggressive_storage_cleanup()
|
| 1593 |
|
| 1594 |
# ========================================================================
|
| 1595 |
-
# 14.5 FASE 2 — TREINAMENTO COM PUNIÇÃO (BrunoN-Dev/corpus-ptbr-v1,
|
| 1596 |
# ========================================================================
|
| 1597 |
if not conhecimento_result["storage_critical_stopped"]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1598 |
logger.info("\n[V6.5-V2] Iniciando FASE 2 — TREINAMENTO COM PUNIÇÃO...")
|
| 1599 |
punicão_result = run_fase_punicão(kls, hf_token)
|
|
|
|
|
|
|
| 1600 |
|
| 1601 |
# Atualiza V2_PHASES_EVAL_PATH com ambas as fases
|
| 1602 |
phases_eval = {
|
|
@@ -1735,6 +2166,7 @@ def main() -> int:
|
|
| 1735 |
BIGRU_ROOT / "scripts" / "train_v6_5_v2.py",
|
| 1736 |
SRC_ROOT / "bigru_t" / "model" / "kohonen_learning_system.py",
|
| 1737 |
SRC_ROOT / "bigru_t" / "model" / "hyp_t.py",
|
|
|
|
| 1738 |
SRC_ROOT / "bigru_t" / "data" / "streaming_datasets.py",
|
| 1739 |
SRC_ROOT / "bigru_t" / "utils" / "xeon_runtime.py",
|
| 1740 |
]
|
|
@@ -1797,12 +2229,52 @@ def main() -> int:
|
|
| 1797 |
# ========================================================================
|
| 1798 |
# 14.11 LIMPEZA final de armazenamento e worklog
|
| 1799 |
# ========================================================================
|
|
|
|
|
|
|
| 1800 |
logger.info("\n[V6.5-V2] Limpeza final de armazenamento...")
|
| 1801 |
final_storage_cleanup = aggressive_storage_cleanup()
|
| 1802 |
aggressive_memory_cleanup()
|
| 1803 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1804 |
print("\n" + "=" * 80)
|
| 1805 |
-
print("V6.5-V2-
|
| 1806 |
print("=" * 80)
|
| 1807 |
print(f" FASE 1 CONHECIMENTO : {conhecimento_result['total_samples']} samples "
|
| 1808 |
f"(meta: {conhecimento_result['meta_atingida']})")
|
|
@@ -1816,12 +2288,15 @@ def main() -> int:
|
|
| 1816 |
print(f" HF upload : {hf_upload_result.get('uploaded', False)} "
|
| 1817 |
f"({hf_upload_result.get('n_files', 0)} files)")
|
| 1818 |
print(f" HF_TOKEN cleaned : True (env + scripts)")
|
|
|
|
| 1819 |
print(f" Storage cleanup : {final_storage_cleanup['n_files_removed']} files, "
|
| 1820 |
f"{final_storage_cleanup['mb_freed']:.1f}MB freed")
|
| 1821 |
print(f" Final n_hypotheses : {kls.n_hypotheses} (active={kls.hypothesis_ensemble.active_count})")
|
| 1822 |
print(f" Final n_trials : {kls.n_trials}")
|
| 1823 |
print(f" Final hyp_steps : {kls.hyp_train_steps}")
|
| 1824 |
print(f" N adaptations : {len(kls.get_adaptation_log())}")
|
|
|
|
|
|
|
| 1825 |
print("=" * 80 + "\n")
|
| 1826 |
|
| 1827 |
return 0
|
|
|
|
| 1 |
+
"""train_v6_5_v2.py — V6.5-V2-metrics (2 Fases + Métricas SOM canônicas).
|
| 2 |
|
| 3 |
═══════════════════════════════════════════════════════════════════════════════
|
| 4 |
+
V6.5-V2-metrics — MÉTRICAS SOM CANÔNICAS + 2 FASES + ESTADO FASE1→FASE2
|
| 5 |
═══════════════════════════════════════════════════════════════════════════════
|
| 6 |
|
| 7 |
+
User requirements (latest, V6.5-V2-metrics):
|
| 8 |
+
1. Organização e não duplicidade de módulos (versões velhas → deprecados/).
|
| 9 |
+
2. Atualizações HF em LOTE (sobrescrever antigos desatualizados).
|
| 10 |
+
3. Limpar Armazenamento e worklog.
|
| 11 |
+
4. HF_TOKEN apagada do ambiente virtual e dos scripts enviados ao HF
|
| 12 |
após uso.
|
| 13 |
+
5. Usar streaming_datasets.py e ativar xeon_runtime.py.
|
| 14 |
+
6. FASE 1 CONHECIMENTO (meta mínima 8000+ samples, 100 em 100) na
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
sequência exata de 8 datasets.
|
| 16 |
+
7. FASE 2 TREINAMENTO COM PUNIÇÃO ATIVA (meta mínima 2000+ samples,
|
| 17 |
100 em 100) sobre BrunoN-Dev/corpus-ptbr-v1.
|
| 18 |
+
8. FASE2 NUNCA acontece antes da FASE1. FASE2 carrega explicitamente
|
| 19 |
+
o estado do modelo concluído da FASE1.
|
| 20 |
+
9. FASE2 deve treinar, ajustar dados e parâmetros do MAPA-SOM e
|
| 21 |
+
INFORMAR MÉTRICAS DE APRENDIZADO do modelo.
|
| 22 |
+
10. Métricas SOM canônicas (8 métricas) monitoradas especialmente na
|
| 23 |
+
FASE2 PUNITIVA:
|
| 24 |
+
10.1. Erro de Quantização (QE) — distância média ‖x_i - W_BMU(x_i)‖
|
| 25 |
+
10.2. Erro Topológico (TE) — % de pares BMU/2ª-BMU não adjacentes
|
| 26 |
+
10.3. Erro de Kaski-Lagus — combinação ponderada de QE + TE
|
| 27 |
+
10.4. Variância Explicada — 1 - Var(residual)/Var(total)
|
| 28 |
+
10.5. Colapso Topológico — pesos convergem para ponto ou linha
|
| 29 |
+
10.6. Taxa de Neurônios Mortos — % de neurônios nunca BMU
|
| 30 |
+
10.7. Estagnação do QE — slope ≈ 0 com QE em patamar elevado
|
| 31 |
+
10.8. Cruzamento de Vizinhança — TE oscila ou cresce no final
|
| 32 |
+
11. Limpeza de RAM e Armazenamento aprimorada (lógica + matemática).
|
| 33 |
+
12. Não reduzir tempo, não gerar dados sintéticos.
|
| 34 |
+
13. Após concluir: enviar scripts+arquivos+estado do modelo testados e
|
| 35 |
aprovados ao HF (sobrescrevendo antigos).
|
| 36 |
|
| 37 |
+
V6.5-V2-metrics (alterações vs V6.5-V2-dynamic):
|
| 38 |
+
- NOVO: Módulo som_metrics.py com 8 métricas canônicas implementadas.
|
| 39 |
+
- NOVO: KohonenLearningSystemV2.compute_som_metrics() integra as métricas.
|
| 40 |
+
- NOVO: Histórico de métricas SOM (SOMMetricHistory) para detecção
|
| 41 |
+
de estagnação do QE e cruzamento de vizinhança.
|
| 42 |
+
- NOVO: FASE1 computa métricas após cada chunk (8000 samples).
|
| 43 |
+
- NOVO: FASE2 computa métricas após CADA BATCH (2000 samples, mais
|
| 44 |
+
frequentemente por ser fase punitiva).
|
| 45 |
+
- NOVO: load_fase1_state_into_kls() — carrega explicitamente estado
|
| 46 |
+
da FASE1 antes de iniciar FASE2.
|
| 47 |
+
- NOVO: aggressive_memory_cleanup() reporta RSS antes/depois.
|
| 48 |
+
- NOVO: aggressive_storage_cleanup() limpa __pycache__ + .tmp + logs >7dias.
|
| 49 |
+
- META: CONHECIMENTO 6000 → 8000; PUNIÇÃO 1000 → 2000.
|
| 50 |
"""
|
| 51 |
|
| 52 |
from __future__ import annotations
|
|
|
|
| 114 |
logger.info(f"[V6.5-V2] Xeon FP16 benchmark: {FP16_BENCH}")
|
| 115 |
|
| 116 |
# ============================================================================
|
| 117 |
+
# 2. Configurações V6.5-V2-metrics-FIX (864 neurons, params restaurados a valores maiores)
|
| 118 |
# ============================================================================
|
| 119 |
+
# User requirement (latest): "ATENÇÃO: retornar os parâmetros do modelo para
|
| 120 |
+
# os valores maiores e resolver falhas de lógica e de bugs que estejam causando
|
| 121 |
+
# alto consumo de memória sem distorcer a arquitetura Kohonen."
|
| 122 |
+
#
|
| 123 |
+
# Valores restaurados (V6.5-V2-metrics-FIX):
|
| 124 |
+
# HIDDEN_DIM: 256 → 1024 (restaurado)
|
| 125 |
+
# VOCAB_SIZE: 4096 → 16384 (restaurado)
|
| 126 |
+
# N_HYPOTHESES: 8 → 16 (restaurado)
|
| 127 |
+
# MAX_N_HYPOTHESES: 16 → 32 (restaurado)
|
| 128 |
+
# HYP_TRAIN_STEPS: 20 → 30 (restaurado)
|
| 129 |
+
# HYP_HIDDEN_DIM: 128 → 256 (restaurado)
|
| 130 |
+
#
|
| 131 |
+
# Para compensar o aumento de memória (sem distorcer Kohonen):
|
| 132 |
+
# 1. BUG FIX: _compute_classification_loss_with_som estava dentro de
|
| 133 |
+
# torch.no_grad(), matando o gradiente dos deltas. Corrigido.
|
| 134 |
+
# 2. VETORIZAÇÃO: train_hypotheses agora avalia 16 hipóteses em paralelo
|
| 135 |
+
# via batched matmul (n_hyp, P, N) em vez de loop que materializava
|
| 136 |
+
# 16 cópias do SOM.
|
| 137 |
+
# 3. aggressive_cleanup(): zera gradientes Adam, trunca históricos,
|
| 138 |
+
# gc.collect() em 3 passes.
|
| 139 |
+
# 4. Buffer sliding window: MAX_BUFFER_SIZE = 256 (preserva contexto recente
|
| 140 |
+
# sem crescer indefinidamente).
|
| 141 |
+
# 5. VQ-VAE-2 e W8A8 desabilitados por padrão (não essenciais para Kohonen).
|
| 142 |
BATCH_SIZE = 16
|
| 143 |
MAX_SEQ_LEN = 8
|
| 144 |
|
| 145 |
+
# V6.5-V2-metrics-FIX: restaurado para valores maiores (user requirement)
|
| 146 |
+
HIDDEN_DIM = 1024 # restaurado de 256
|
| 147 |
+
VOCAB_SIZE = 16384 # restaurado de 4096
|
| 148 |
+
SOM_GRID = (6, 6, 6, 4) # 864 neurons (arquitetura Kohonen preservada)
|
|
|
|
| 149 |
T_MAX = 10000
|
| 150 |
N_START = 10
|
| 151 |
LAMBDA_EWC = 0.02
|
|
|
|
| 153 |
SIGMA0 = 1.5
|
| 154 |
DIM_CHOICE = "y"
|
| 155 |
|
| 156 |
+
# V2-dynamic — HypothesisEnsemble parameters (restaurados a valores maiores)
|
| 157 |
+
N_HYPOTHESES = 16 # restaurado de 8
|
| 158 |
+
MAX_N_HYPOTHESES = 32 # restaurado de 16
|
| 159 |
+
MIN_N_HYPOTHESES = 4 # limite inferior dinâmico
|
| 160 |
+
N_TRIALS = 3 # inicial
|
| 161 |
MIN_N_TRIALS = 1
|
| 162 |
MAX_N_TRIALS = 6
|
| 163 |
+
HYP_TRAIN_STEPS = 30 # restaurado de 20
|
| 164 |
+
MIN_HYP_TRAIN_STEPS = 10
|
| 165 |
+
MAX_HYP_TRAIN_STEPS = 80
|
| 166 |
HYP_LR = 1e-4
|
| 167 |
+
HYP_HIDDEN_DIM = 256 # restaurado de 128
|
| 168 |
LOSS_HISTORY_WINDOW = 8
|
| 169 |
PUNISHMENT_WINDOW = 12
|
| 170 |
|
| 171 |
# V2-dynamic-memory — Buffer sliding window (evita OOM em treino longo)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
MAX_BUFFER_SIZE = 256
|
| 173 |
|
| 174 |
# Streaming — User requirement: "streaming de 100 em 100 samples"
|
| 175 |
STREAM_BATCH_SIZE = 100
|
| 176 |
|
| 177 |
+
# V2-dynamic — Metas atualizadas (V6.5-V2-metrics): 8000 CONHECIMENTO + 2000 PUNIÇÃO
|
| 178 |
+
META_MINIMA_CONHECIMENTO = 8000
|
| 179 |
+
MAX_SAMPLES_PER_DATASET_CONHECIMENTO = 1000 # 8 × 1000 = 8000 ≥ 8000
|
| 180 |
+
META_MINIMA_PUNICAO = 2000
|
| 181 |
+
MAX_SAMPLES_PUNICAO = 2000
|
| 182 |
+
|
| 183 |
+
# User requirement: "não reduzir tempo e não gerar dados sintéticos" +
|
| 184 |
+
# "todo streaming (FASE1 e da FASE2) deve ter pausa para dar tempo de conclusão
|
| 185 |
+
# de processamento continuando após conclusão"
|
| 186 |
+
INTER_BATCH_PAUSE_S = 0.5 # aumentado de 0.3 (mais tempo para processamento)
|
| 187 |
+
INTER_DATASET_PAUSE_S = 2.0 # aumentado de 1.5
|
| 188 |
+
INTER_STREAM_BATCH_PAUSE_S = 1.0 # aumentado de 0.5 (pausa entre chunks de 100)
|
| 189 |
+
POST_PROCESSING_PAUSE_S = 1.5 # NOVO: pausa após processar cada chunk para
|
| 190 |
+
# dar tempo de conclusão de processamento
|
| 191 |
|
| 192 |
# Storage critical
|
| 193 |
STORAGE_CRITICAL_PCT = 90
|
|
|
|
| 321 |
|
| 322 |
|
| 323 |
def aggressive_memory_cleanup() -> Dict[str, Any]:
|
| 324 |
+
"""V6.5-V2-metrics — Limpeza agressiva de RAM.
|
| 325 |
+
|
| 326 |
+
User requirement: "analisar e aprimorar (logicamente e matematicamente)
|
| 327 |
+
no projeto limpeza de memória RAM e de Armazenamento (ao concluir)".
|
| 328 |
+
|
| 329 |
+
Implementação:
|
| 330 |
+
1. gc.collect() — 2 passes (gerações 0+1 e 2)
|
| 331 |
+
2. torch.cuda.empty_cache() (se CUDA disponível)
|
| 332 |
+
3. Limpa caches internos do PyTorch (MKL, oneDNN)
|
| 333 |
+
4. Reporta RSS antes/depois para confirmação
|
| 334 |
+
"""
|
| 335 |
+
rss_before = get_process_rss_mb()
|
| 336 |
gc.collect()
|
| 337 |
gc.collect()
|
| 338 |
try:
|
| 339 |
import torch
|
| 340 |
if torch.cuda.is_available():
|
| 341 |
torch.cuda.empty_cache()
|
| 342 |
+
# V6.5-V2-metrics: limpa caches CPU do PyTorch
|
| 343 |
+
if hasattr(torch, "cpu"):
|
| 344 |
+
try:
|
| 345 |
+
# MKL/OneDNN primitive cache é limpo implicitamente por gc
|
| 346 |
+
pass
|
| 347 |
+
except Exception:
|
| 348 |
+
pass
|
| 349 |
except Exception:
|
| 350 |
pass
|
| 351 |
+
rss_after = get_process_rss_mb()
|
| 352 |
+
freed_mb = max(0.0, rss_before - rss_after)
|
| 353 |
+
return {
|
| 354 |
+
"gc_called": True,
|
| 355 |
+
"rss_before_mb": float(rss_before),
|
| 356 |
+
"rss_after_mb": float(rss_after),
|
| 357 |
+
"freed_mb": float(freed_mb),
|
| 358 |
+
"timestamp": datetime.now().isoformat(),
|
| 359 |
+
}
|
| 360 |
|
| 361 |
|
| 362 |
def aggressive_storage_cleanup() -> Dict[str, Any]:
|
| 363 |
+
"""V6.5-V2-metrics — Limpeza agressiva de Armazenamento.
|
| 364 |
+
|
| 365 |
+
User requirement: "analisar e aprimorar (logicamente e matematicamente)
|
| 366 |
+
no projeto limpeza de memória RAM e de Armazenamento (ao concluir)".
|
| 367 |
+
|
| 368 |
+
Implementação:
|
| 369 |
+
1. Limpa locks HF (*.lock)
|
| 370 |
+
2. Limpa downloads incompletos (*.incomplete)
|
| 371 |
+
3. Limpa __pycache__ recursivamente
|
| 372 |
+
4. Limpa arquivos .tmp
|
| 373 |
+
5. Limpa logs antigos (>7 dias, se existirem)
|
| 374 |
+
6. Reporta MB liberados
|
| 375 |
+
"""
|
| 376 |
n_removed = 0
|
| 377 |
mb_freed = 0.0
|
| 378 |
+
|
| 379 |
+
# 1. HF cache locks e incompletos
|
| 380 |
hf_cache = Path.home() / ".cache" / "huggingface"
|
| 381 |
if hf_cache.exists():
|
| 382 |
+
for pattern in ["*.lock", "*.incomplete", "*.tmp"]:
|
| 383 |
+
for f in hf_cache.rglob(pattern):
|
| 384 |
+
try:
|
| 385 |
+
sz = f.stat().st_size
|
| 386 |
+
f.unlink()
|
| 387 |
+
n_removed += 1
|
| 388 |
+
mb_freed += sz / 1e6
|
| 389 |
+
except Exception:
|
| 390 |
+
pass
|
| 391 |
+
|
| 392 |
+
# 2. __pycache__ recursivo no BiGRU_T_version
|
| 393 |
+
bigru_root = BIGRU_ROOT
|
| 394 |
+
for cache_dir in bigru_root.rglob("__pycache__"):
|
| 395 |
+
if "deprecados" in str(cache_dir):
|
| 396 |
+
continue
|
| 397 |
+
try:
|
| 398 |
+
for f in cache_dir.iterdir():
|
| 399 |
+
try:
|
| 400 |
+
sz = f.stat().st_size
|
| 401 |
+
f.unlink()
|
| 402 |
+
n_removed += 1
|
| 403 |
+
mb_freed += sz / 1e6
|
| 404 |
+
except Exception:
|
| 405 |
+
pass
|
| 406 |
+
cache_dir.rmdir()
|
| 407 |
+
except Exception:
|
| 408 |
+
pass
|
| 409 |
+
|
| 410 |
+
# 3. Arquivos .tmp no BiGRU_T_version
|
| 411 |
+
for f in bigru_root.rglob("*.tmp"):
|
| 412 |
+
if "deprecados" in str(f):
|
| 413 |
+
continue
|
| 414 |
+
try:
|
| 415 |
+
sz = f.stat().st_size
|
| 416 |
+
f.unlink()
|
| 417 |
+
n_removed += 1
|
| 418 |
+
mb_freed += sz / 1e6
|
| 419 |
+
except Exception:
|
| 420 |
+
pass
|
| 421 |
+
|
| 422 |
+
# 4. Logs antigos (>7 dias) em logs/
|
| 423 |
+
logs_dir = PROJECT_ROOT / "logs"
|
| 424 |
+
if logs_dir.exists():
|
| 425 |
+
cutoff = time.time() - 7 * 24 * 3600 # 7 dias
|
| 426 |
+
for f in logs_dir.glob("*.log"):
|
| 427 |
try:
|
| 428 |
+
if f.stat().st_mtime < cutoff:
|
| 429 |
+
sz = f.stat().st_size
|
| 430 |
+
f.unlink()
|
| 431 |
+
n_removed += 1
|
| 432 |
+
mb_freed += sz / 1e6
|
| 433 |
except Exception:
|
| 434 |
pass
|
| 435 |
+
|
| 436 |
return {
|
| 437 |
"n_files_removed": n_removed,
|
| 438 |
"mb_freed": float(mb_freed),
|
|
|
|
| 548 |
|
| 549 |
|
| 550 |
# ============================================================================
|
| 551 |
+
# 5.5 Load FASE1 state into KLS (FASE2 must use FASE1's completed state)
|
| 552 |
+
# ============================================================================
|
| 553 |
+
def load_fase1_state_into_kls(
|
| 554 |
+
kls: KohonenLearningSystemV2,
|
| 555 |
+
state_path: Path,
|
| 556 |
+
) -> Dict[str, Any]:
|
| 557 |
+
"""V6.5-V2-metrics — Carrega estado do modelo concluído da FASE1 no KLS.
|
| 558 |
+
|
| 559 |
+
User requirement: "esta FASE2 (nunca acontece antes da FASE1) deve usar do
|
| 560 |
+
arquivo de Estado do MODELO concluído da FASE1 e a FASE2 tem função: de
|
| 561 |
+
treinar, ajustar dados e os parâmetros do MAPA-SOM de redes Kohonen,
|
| 562 |
+
informar métricas de aprendizado do modelo".
|
| 563 |
+
|
| 564 |
+
Esta função é chamada explicitamente antes de iniciar a FASE 2, garantindo
|
| 565 |
+
que a FASE 2 herde todo o conhecimento acumulado da FASE 1 (pesos do SOM,
|
| 566 |
+
EWC references, classifier, hypothesis ensemble, delta_scale, label_registry).
|
| 567 |
+
|
| 568 |
+
Args:
|
| 569 |
+
kls: instância KohonenLearningSystemV2 (mesma usada na FASE1).
|
| 570 |
+
state_path: caminho para o arquivo .pt salvo ao final da FASE1.
|
| 571 |
+
|
| 572 |
+
Returns:
|
| 573 |
+
Dict com status do carregamento.
|
| 574 |
+
"""
|
| 575 |
+
import torch
|
| 576 |
+
if not state_path.exists():
|
| 577 |
+
logger.warning(
|
| 578 |
+
f"[V6.5-V2-metrics] FASE1 state file not found: {state_path}. "
|
| 579 |
+
f"FASE2 will run with the in-memory KLS state (already inherited "
|
| 580 |
+
f"from FASE1 since same KLS instance is used)."
|
| 581 |
+
)
|
| 582 |
+
return {
|
| 583 |
+
"loaded": False,
|
| 584 |
+
"reason": "state_file_not_found",
|
| 585 |
+
"path": str(state_path),
|
| 586 |
+
"in_memory_state_used": True,
|
| 587 |
+
}
|
| 588 |
+
try:
|
| 589 |
+
state = torch.load(state_path, map_location="cpu", weights_only=False)
|
| 590 |
+
meta = state.get("_meta", {})
|
| 591 |
+
logger.info(
|
| 592 |
+
f"[V6.5-V2-metrics] Loading FASE1 state from {state_path}: "
|
| 593 |
+
f"reason={meta.get('reason', '?')}, "
|
| 594 |
+
f"step={meta.get('step', 0)}, "
|
| 595 |
+
f"total_samples={meta.get('total_samples', 0)}, "
|
| 596 |
+
f"timestamp={meta.get('timestamp', '?')}"
|
| 597 |
+
)
|
| 598 |
+
|
| 599 |
+
# Carrega pesos do SOM (preserva o aprendizado da FASE1)
|
| 600 |
+
if "som_weights" in state and state["som_weights"] is not None:
|
| 601 |
+
kls.som.weights = state["som_weights"].clone().float()
|
| 602 |
+
logger.info(
|
| 603 |
+
f"[V6.5-V2-metrics] SOM weights loaded: "
|
| 604 |
+
f"shape={tuple(kls.som.weights.shape)}, "
|
| 605 |
+
f"norm={float(kls.som.weights.norm().item()):.4f}"
|
| 606 |
+
)
|
| 607 |
+
# Carrega EWC references (se existirem da FASE1)
|
| 608 |
+
if "som_old_weights_w" in state and state["som_old_weights_w"] is not None:
|
| 609 |
+
kls.som.old_weights_w = state["som_old_weights_w"].clone().float()
|
| 610 |
+
logger.info("[V6.5-V2-metrics] SOM EWC old_weights_w loaded.")
|
| 611 |
+
if "som_fisher_w" in state and state["som_fisher_w"] is not None:
|
| 612 |
+
kls.som.fisher_w = state["som_fisher_w"].clone().float()
|
| 613 |
+
logger.info("[V6.5-V2-metrics] SOM EWC fisher_w loaded.")
|
| 614 |
+
|
| 615 |
+
# Carrega embedding
|
| 616 |
+
if "embedding_state" in state and state["embedding_state"] is not None:
|
| 617 |
+
kls.embedding.weight.data = state["embedding_state"]["weight"].clone().float()
|
| 618 |
+
logger.info("[V6.5-V2-metrics] Embedding weights loaded.")
|
| 619 |
+
|
| 620 |
+
# Carrega classifier (se treinado na FASE1)
|
| 621 |
+
if "classifier_state" in state and state["classifier_state"] is not None:
|
| 622 |
+
if kls.classifier is None:
|
| 623 |
+
# Recria o classifier com a mesma arquitetura
|
| 624 |
+
kls.classifier = HypothesisClassifier(
|
| 625 |
+
input_dim=kls.som_neuron_count,
|
| 626 |
+
hidden_dims=kls.hypothesis_hidden,
|
| 627 |
+
)
|
| 628 |
+
kls.classifier.load_state_dict(state["classifier_state"])
|
| 629 |
+
kls.classifier_trained = True
|
| 630 |
+
logger.info("[V6.5-V2-metrics] Classifier state loaded.")
|
| 631 |
+
|
| 632 |
+
# Carrega HypothesisEnsemble
|
| 633 |
+
if "hypothesis_ensemble_state" in state and state["hypothesis_ensemble_state"] is not None:
|
| 634 |
+
kls.hypothesis_ensemble.load_state_dict(state["hypothesis_ensemble_state"])
|
| 635 |
+
logger.info(
|
| 636 |
+
f"[V6.5-V2-metrics] HypothesisEnsemble state loaded: "
|
| 637 |
+
f"n_generators={len(kls.hypothesis_ensemble.generators)}, "
|
| 638 |
+
f"active={kls.hypothesis_ensemble.active_count}"
|
| 639 |
+
)
|
| 640 |
+
|
| 641 |
+
# Carrega delta_scale
|
| 642 |
+
if "delta_scale" in state and state["delta_scale"] is not None:
|
| 643 |
+
kls.delta_scale.data = state["delta_scale"].clone().float()
|
| 644 |
+
logger.info(
|
| 645 |
+
f"[V6.5-V2-metrics] delta_scale loaded: {kls.delta_scale.item():.4f}"
|
| 646 |
+
)
|
| 647 |
+
|
| 648 |
+
# Carrega label_registry (preserva mapeamentos dinâmicos)
|
| 649 |
+
if "label_registry" in state and state["label_registry"] is not None:
|
| 650 |
+
kls.label_registry = state["label_registry"]
|
| 651 |
+
logger.info(
|
| 652 |
+
f"[V6.5-V2-metrics] label_registry loaded: "
|
| 653 |
+
f"{len(kls.label_registry)} datasets"
|
| 654 |
+
)
|
| 655 |
+
|
| 656 |
+
# Restaura time_counter (preserva coordenada temporal absoluta)
|
| 657 |
+
if "time_counter" in meta:
|
| 658 |
+
kls.time_counter = int(meta["time_counter"])
|
| 659 |
+
logger.info(
|
| 660 |
+
f"[V6.5-V2-metrics] time_counter restored: {kls.time_counter}"
|
| 661 |
+
)
|
| 662 |
+
|
| 663 |
+
logger.info(
|
| 664 |
+
f"[V6.5-V2-metrics] ✓ FASE1 state loaded successfully. "
|
| 665 |
+
f"KLS is now ready for FASE2 (PUNITIVE) training."
|
| 666 |
+
)
|
| 667 |
+
return {
|
| 668 |
+
"loaded": True,
|
| 669 |
+
"path": str(state_path),
|
| 670 |
+
"meta": meta,
|
| 671 |
+
"in_memory_state_used": False,
|
| 672 |
+
}
|
| 673 |
+
except Exception as e:
|
| 674 |
+
logger.error(
|
| 675 |
+
f"[V6.5-V2-metrics] Failed to load FASE1 state: {e}. "
|
| 676 |
+
f"FASE2 will run with the in-memory KLS state (already inherited "
|
| 677 |
+
f"from FASE1 since same KLS instance is used)."
|
| 678 |
+
)
|
| 679 |
+
traceback.print_exc()
|
| 680 |
+
return {
|
| 681 |
+
"loaded": False,
|
| 682 |
+
"reason": f"load_error: {str(e)[:200]}",
|
| 683 |
+
"path": str(state_path),
|
| 684 |
+
"in_memory_state_used": True,
|
| 685 |
+
}
|
| 686 |
+
|
| 687 |
+
|
| 688 |
+
# ============================================================================
|
| 689 |
+
# 6. Streaming helper — chunks of 100 samples (V6.5-V2-metrics: O(N) not O(N²))
|
| 690 |
# ============================================================================
|
| 691 |
def stream_dataset_in_chunks(
|
| 692 |
dataset_name: str,
|
|
|
|
| 695 |
hf_token: Optional[str] = None,
|
| 696 |
timeout_per_chunk: int = 60,
|
| 697 |
) -> Iterator[List[str]]:
|
| 698 |
+
"""V6.5-V2-metrics — Stream a dataset in chunks of `chunk_size` samples.
|
| 699 |
+
|
| 700 |
+
User requirement: "streaming de 100 em 100 samples até esgonar".
|
| 701 |
+
|
| 702 |
+
V6.5-V2-metrics FIX: A versão anterior re-chamava stream_dataset() para
|
| 703 |
+
cada chunk com seed_offset = yielded_total, fazendo com que o HF
|
| 704 |
+
re-streaming recapitulasse todas as amostras anteriores a cada chunk
|
| 705 |
+
(custo O(N²) em banda e cache). Esta versão cria UM ÚNICO iterador
|
| 706 |
+
por dataset e vai colhendo chunks de `chunk_size` em `chunk_size`,
|
| 707 |
+
sem recomeçar do zero. Isto reduz drasticamente o uso de memória
|
| 708 |
+
(que antes crescia linearmente com o nº de chunks devido ao cache
|
| 709 |
+
acumulado de metadados parquet) e permite atingir as metas de
|
| 710 |
+
8000 samples CONHECIMENTO + 2000 samples PUNIÇÃO dentro do limite
|
| 711 |
+
de 4GB RAM do cgroup.
|
| 712 |
"""
|
| 713 |
try:
|
| 714 |
from bigru_t.data.streaming_datasets import stream_dataset
|
|
|
|
| 718 |
|
| 719 |
yielded_total = 0
|
| 720 |
chunk_idx = 0
|
| 721 |
+
chunk: List[str] = []
|
| 722 |
+
t_start = time.time()
|
| 723 |
+
|
| 724 |
+
try:
|
| 725 |
+
sample_iter = stream_dataset(
|
| 726 |
+
dataset_name,
|
| 727 |
+
max_samples=max_total,
|
| 728 |
+
hf_token=hf_token,
|
| 729 |
+
)
|
| 730 |
+
for sample in sample_iter:
|
| 731 |
+
if yielded_total >= max_total:
|
| 732 |
+
break
|
| 733 |
+
# Timeout check: se demorar demais, yield o que temos e continua
|
| 734 |
+
if time.time() - t_start > timeout_per_chunk * (chunk_idx + 1):
|
| 735 |
+
logger.warning(
|
| 736 |
+
f"[V6.5-V2] {dataset_name} chunk {chunk_idx+1} "
|
| 737 |
+
f"timeout after {timeout_per_chunk}s "
|
| 738 |
+
f"(collected {len(chunk)}/{chunk_size})"
|
| 739 |
+
)
|
| 740 |
+
if chunk:
|
| 741 |
+
yield chunk
|
| 742 |
+
yielded_total += len(chunk)
|
| 743 |
+
chunk_idx += 1
|
| 744 |
+
chunk = []
|
| 745 |
+
if time.time() - t_start > timeout_per_chunk * (chunk_idx + 2):
|
| 746 |
+
# Segundo timeout consecutivo — desiste deste dataset
|
| 747 |
logger.warning(
|
| 748 |
+
f"[V6.5-V2] {dataset_name} exhausted after {yielded_total} samples "
|
| 749 |
+
f"({chunk_idx} chunks) due to repeated timeouts"
|
|
|
|
| 750 |
)
|
| 751 |
+
return
|
| 752 |
+
continue
|
| 753 |
+
|
| 754 |
+
if sample.raw_text and len(sample.raw_text.strip()) > 0:
|
| 755 |
+
chunk.append(sample.raw_text.strip()[:200])
|
| 756 |
+
if len(chunk) >= chunk_size:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 757 |
yield chunk
|
| 758 |
yielded_total += len(chunk)
|
| 759 |
chunk_idx += 1
|
| 760 |
+
logger.info(
|
| 761 |
+
f"[V6.5-V2] {dataset_name} chunk {chunk_idx} done: "
|
| 762 |
+
f"{len(chunk)} samples (total: {yielded_total}/{max_total})"
|
| 763 |
+
)
|
| 764 |
+
chunk = []
|
| 765 |
+
except Exception as e:
|
| 766 |
+
logger.warning(f"[V6.5-V2] {dataset_name} streaming error: {e}")
|
| 767 |
+
if chunk:
|
| 768 |
+
yield chunk
|
| 769 |
+
yielded_total += len(chunk)
|
| 770 |
+
chunk_idx += 1
|
| 771 |
+
return
|
| 772 |
|
| 773 |
+
# Yield any remaining partial chunk
|
| 774 |
+
if chunk:
|
| 775 |
yield chunk
|
| 776 |
yielded_total += len(chunk)
|
| 777 |
chunk_idx += 1
|
| 778 |
+
|
| 779 |
+
logger.info(
|
| 780 |
+
f"[V6.5-V2] {dataset_name} exhausted after {yielded_total} samples "
|
| 781 |
+
f"({chunk_idx} chunks)"
|
| 782 |
+
)
|
| 783 |
|
| 784 |
|
| 785 |
# ============================================================================
|
|
|
|
| 952 |
chunk_idx_global = 0
|
| 953 |
accuracies_log: List[Dict[str, Any]] = []
|
| 954 |
attention_log: List[Dict[str, Any]] = []
|
| 955 |
+
# V6.5-V2-metrics — log de métricas SOM durante CONHECIMENTO
|
| 956 |
+
som_metrics_log: List[Dict[str, Any]] = []
|
| 957 |
+
|
| 958 |
+
# V6.5-V2-metrics — reseta histórico de métricas SOM no início de CONHECIMENTO
|
| 959 |
+
kls.reset_som_metric_history()
|
| 960 |
+
logger.info("[V6.5-V2-metrics] Histórico de métricas SOM resetado para FASE 1.")
|
| 961 |
|
| 962 |
for ds_idx, dataset_name in enumerate(CONHECIMENTO_DATASETS):
|
| 963 |
if storage_critical_stopped:
|
|
|
|
| 1111 |
f"[V6.5-V2] Buffer trimmed: -{overflow} → "
|
| 1112 |
f"{len(kls.buffer_4d)} samples (cap={MAX_BUFFER_SIZE})"
|
| 1113 |
)
|
| 1114 |
+
|
| 1115 |
+
# V6.5-V2-metrics — Computa métricas SOM após cada chunk
|
| 1116 |
+
# (QE, TE, Kaski-Lagus, Variância Explicada, Dead Neurons,
|
| 1117 |
+
# Colapso Topológico, Estagnação QE, Cruzamento Vizinhança).
|
| 1118 |
+
# User requirement: "ANALISAR matematicamente e logicamente e
|
| 1119 |
+
# inserir melhorias para os scripts das métricas de aprendizado
|
| 1120 |
+
# e indicadores de falha em Mapas Auto-Organizáveis".
|
| 1121 |
+
try:
|
| 1122 |
+
som_metrics = kls.compute_som_metrics()
|
| 1123 |
+
som_metrics_log.append({
|
| 1124 |
+
"step": step,
|
| 1125 |
+
"chunk_idx": chunk_idx_global,
|
| 1126 |
+
"dataset": dataset_name,
|
| 1127 |
+
"quantization_error": float(som_metrics["quantization_error"]),
|
| 1128 |
+
"topological_error": float(som_metrics["topological_error"]),
|
| 1129 |
+
"kaski_lagus_error": float(som_metrics["kaski_lagus_error"]),
|
| 1130 |
+
"explained_variance_share": float(som_metrics["explained_variance_share"]),
|
| 1131 |
+
"overall_health": som_metrics["overall_health"],
|
| 1132 |
+
"n_failure_indicators": int(som_metrics["n_failure_indicators"]),
|
| 1133 |
+
"failure_indicators": som_metrics["failure_indicators"],
|
| 1134 |
+
"collapse_severity": som_metrics["topological_collapse"].get("severity", "none"),
|
| 1135 |
+
"dead_neuron_rate": float(som_metrics["dead_neuron_rate"].get("dead_neuron_rate", 0.0)),
|
| 1136 |
+
})
|
| 1137 |
+
if chunk_idx_global % 5 == 0:
|
| 1138 |
+
logger.info(
|
| 1139 |
+
f"[V6.5-V2-metrics] chunk {chunk_idx_global}: "
|
| 1140 |
+
f"QE={som_metrics['quantization_error']:.4f}, "
|
| 1141 |
+
f"TE={som_metrics['topological_error']:.4f}, "
|
| 1142 |
+
f"VE={som_metrics['explained_variance_share']:.4f}, "
|
| 1143 |
+
f"health={som_metrics['overall_health']}, "
|
| 1144 |
+
f"failures={len(som_metrics['failure_indicators'])}"
|
| 1145 |
+
)
|
| 1146 |
+
except Exception as e:
|
| 1147 |
+
logger.warning(f"[V6.5-V2-metrics] Failed to compute SOM metrics: {e}")
|
| 1148 |
+
|
| 1149 |
# Aggressive memory cleanup between chunks
|
| 1150 |
if chunk_idx_global % 2 == 0:
|
| 1151 |
aggressive_memory_cleanup()
|
| 1152 |
+
# V6.5-V2-metrics-FIX: também chama aggressive_cleanup do KLS
|
| 1153 |
+
# para zerar gradientes Adam e truncar históricos.
|
| 1154 |
+
try:
|
| 1155 |
+
kls.aggressive_cleanup()
|
| 1156 |
+
except Exception:
|
| 1157 |
+
pass
|
| 1158 |
+
# V6.5-V2-metrics-FIX: pausa pós-processamento para dar tempo
|
| 1159 |
+
# de conclusão (user requirement: "todo streaming deve ter
|
| 1160 |
+
# pausa para dar tempo de conclusão de processamento").
|
| 1161 |
+
time.sleep(POST_PROCESSING_PAUSE_S)
|
| 1162 |
except Exception as e:
|
| 1163 |
logger.error(f"[V6.5-V2] Dataset {dataset_name} failed: {e}")
|
| 1164 |
traceback.print_exc()
|
|
|
|
| 1188 |
"n_chunks": int(chunk_idx_global),
|
| 1189 |
"accuracies_log": accuracies_log[-50:], # últimos 50
|
| 1190 |
"attention_log": attention_log[-20:],
|
| 1191 |
+
# V6.5-V2-metrics — log de métricas SOM durante CONHECIMENTO
|
| 1192 |
+
"som_metrics_log": som_metrics_log[-30:], # últimos 30 chunks
|
| 1193 |
+
"som_metrics_final": (
|
| 1194 |
+
kls.compute_som_metrics() if kls.buffer_4d else {}
|
| 1195 |
+
),
|
| 1196 |
+
"som_metric_history": kls.get_som_metric_history(),
|
| 1197 |
"final_state": kls.get_v2_metrics(),
|
| 1198 |
}
|
| 1199 |
|
|
|
|
| 1205 |
kls: KohonenLearningSystemV2,
|
| 1206 |
hf_token: Optional[str],
|
| 1207 |
) -> Dict[str, Any]:
|
| 1208 |
+
"""Fase 2 — TREINAMENTO COM PUNIÇÃO: streaming 2000 samples + V2 protocol.
|
| 1209 |
+
|
| 1210 |
+
User requirement: "FASE2 TREINAMENTO (meta mínima 2000 samples ou mais)
|
| 1211 |
+
COM PUNIÇÃO ATIVA para 'BrunoN-Dev/corpus-ptbr-v1' de 100 em 100 samples" +
|
| 1212 |
+
"esta FASE2 (nunca acontece antes da FASE1) deve usar do arquivo de Estado
|
| 1213 |
+
do MODELO concluído da FASE1 e a FASE2 tem função: de treinar, ajustar
|
| 1214 |
+
dados e os parâmetros do MAPA-SOM de redes Kohonen, informar métricas de
|
| 1215 |
+
aprendizado do modelo" +
|
| 1216 |
+
|
| 1217 |
+
Métricas SOM canônicas monitoradas (user requirement):
|
| 1218 |
+
1.1. Erro de Quantização (QE)
|
| 1219 |
+
1.2. Erro Topológico (TE)
|
| 1220 |
+
1.3. Erro de Kaski-Lagus
|
| 1221 |
+
1.4. Variância Explicada
|
| 1222 |
+
2.1. Colapso Topológico
|
| 1223 |
+
2.2. Taxa de Neurônios Mortos
|
| 1224 |
+
2.3. Estagnação do QE
|
| 1225 |
+
2.4. Cruzamento de Vizinhança
|
| 1226 |
|
| 1227 |
Returns:
|
| 1228 |
Dict com métricas da fase TREINAMENTO COM PUNIÇÃO.
|
|
|
|
| 1238 |
logger.info(f" hyp_train_steps: {HYP_TRAIN_STEPS}")
|
| 1239 |
logger.info("=" * 80 + "\n")
|
| 1240 |
|
| 1241 |
+
# V6.5-V2-metrics — reseta histórico de métricas SOM no início da FASE 2
|
| 1242 |
+
# (PUNITIVA tem seu próprio histórico independente da FASE 1, para que
|
| 1243 |
+
# Estagnação do QE e Cruzamento de Vizinhança sejam avaliados
|
| 1244 |
+
# exclusivamente sobre o regime punitivo).
|
| 1245 |
+
kls.reset_som_metric_history()
|
| 1246 |
+
logger.info("[V6.5-V2-metrics] Histórico de métricas SOM resetado para FASE 2 (PUNITIVA).")
|
| 1247 |
+
|
| 1248 |
step = 0
|
| 1249 |
t_start = time.time()
|
| 1250 |
samples_processed = 0
|
|
|
|
| 1254 |
hypotheses_log: List[Dict[str, Any]] = []
|
| 1255 |
accuracies_log: List[Dict[str, Any]] = []
|
| 1256 |
delta_applications_log: List[Dict[str, Any]] = []
|
| 1257 |
+
# V6.5-V2-metrics — log de métricas SOM durante PUNIÇÃO (computadas a cada batch)
|
| 1258 |
+
som_metrics_log: List[Dict[str, Any]] = []
|
| 1259 |
|
| 1260 |
try:
|
| 1261 |
chunk_iter = stream_dataset_in_chunks(
|
|
|
|
| 1364 |
"ewc_reference_set": bool(kls.som.old_weights_w is not None),
|
| 1365 |
})
|
| 1366 |
|
| 1367 |
+
# V6.5-V2-metrics — Computa métricas SOM após cada batch
|
| 1368 |
+
# durante a FASE 2 PUNITIVA. User requirement:
|
| 1369 |
+
# "ANALISAR matematicamente e logicamente e inserir melhorias
|
| 1370 |
+
# para os scripts das métricas de aprendizado e indicadores
|
| 1371 |
+
# de falha em Mapas Auto-Organizáveis (SOM / Redes de Kohonen)
|
| 1372 |
+
# avaliam a fidelidade de representação dos dados e a
|
| 1373 |
+
# preservação da vizinhança topológica".
|
| 1374 |
+
try:
|
| 1375 |
+
som_metrics = kls.compute_som_metrics()
|
| 1376 |
+
som_metrics_log.append({
|
| 1377 |
+
"step": step,
|
| 1378 |
+
"chunk_idx": chunk_idx_global,
|
| 1379 |
+
"action": action,
|
| 1380 |
+
"punishment_count": int(kls.punishment_count),
|
| 1381 |
+
"quantization_error": float(som_metrics["quantization_error"]),
|
| 1382 |
+
"topological_error": float(som_metrics["topological_error"]),
|
| 1383 |
+
"kaski_lagus_error": float(som_metrics["kaski_lagus_error"]),
|
| 1384 |
+
"explained_variance_share": float(som_metrics["explained_variance_share"]),
|
| 1385 |
+
"overall_health": som_metrics["overall_health"],
|
| 1386 |
+
"n_failure_indicators": int(som_metrics["n_failure_indicators"]),
|
| 1387 |
+
"failure_indicators": som_metrics["failure_indicators"],
|
| 1388 |
+
"collapse_severity": som_metrics["topological_collapse"].get("severity", "none"),
|
| 1389 |
+
"collapse_effective_rank": float(som_metrics["topological_collapse"].get("effective_rank", 0.0)),
|
| 1390 |
+
"dead_neuron_rate": float(som_metrics["dead_neuron_rate"].get("dead_neuron_rate", 0.0)),
|
| 1391 |
+
"n_dead_neurons": int(som_metrics["dead_neuron_rate"].get("n_dead", 0)),
|
| 1392 |
+
"n_active_neurons": int(som_metrics["dead_neuron_rate"].get("n_active", 0)),
|
| 1393 |
+
"qe_stagnation": som_metrics.get("qe_stagnation", {}),
|
| 1394 |
+
"neighborhood_crossing": som_metrics.get("neighborhood_crossing", {}),
|
| 1395 |
+
})
|
| 1396 |
+
# Log detalhado a cada 5 steps OU quando há falha
|
| 1397 |
+
if step % 5 == 0 or som_metrics["n_failure_indicators"] > 0:
|
| 1398 |
+
logger.info(
|
| 1399 |
+
f"[V6.5-V2-metrics] PUNIÇÃO step {step}: "
|
| 1400 |
+
f"QE={som_metrics['quantization_error']:.4f}, "
|
| 1401 |
+
f"TE={som_metrics['topological_error']:.4f}, "
|
| 1402 |
+
f"KL={som_metrics['kaski_lagus_error']:.4f}, "
|
| 1403 |
+
f"VE={som_metrics['explained_variance_share']:.4f}, "
|
| 1404 |
+
f"dead={som_metrics['dead_neuron_rate'].get('dead_neuron_rate', 0.0):.3f}, "
|
| 1405 |
+
f"health={som_metrics['overall_health']}, "
|
| 1406 |
+
f"failures={som_metrics['failure_indicators']}"
|
| 1407 |
+
)
|
| 1408 |
+
except Exception as e:
|
| 1409 |
+
logger.warning(f"[V6.5-V2-metrics] Failed to compute SOM metrics: {e}")
|
| 1410 |
+
|
| 1411 |
step += 1
|
| 1412 |
time.sleep(INTER_BATCH_PAUSE_S)
|
| 1413 |
except Exception as e:
|
|
|
|
| 1427 |
)
|
| 1428 |
if chunk_idx_global % 2 == 0:
|
| 1429 |
aggressive_memory_cleanup()
|
| 1430 |
+
# V6.5-V2-metrics-FIX: também chama aggressive_cleanup do KLS
|
| 1431 |
+
try:
|
| 1432 |
+
kls.aggressive_cleanup()
|
| 1433 |
+
except Exception:
|
| 1434 |
+
pass
|
| 1435 |
+
# V6.5-V2-metrics-FIX: pausa pós-processamento (user requirement)
|
| 1436 |
+
time.sleep(POST_PROCESSING_PAUSE_S)
|
| 1437 |
except Exception as e:
|
| 1438 |
logger.error(f"[V6.5-V2] PUNIÇÃO dataset failed: {e}")
|
| 1439 |
traceback.print_exc()
|
|
|
|
| 1462 |
"hypotheses_trainings": hypotheses_log,
|
| 1463 |
"delta_applications": delta_applications_log,
|
| 1464 |
"accuracies_log": accuracies_log[-50:],
|
| 1465 |
+
# V6.5-V2-metrics — log de métricas SOM durante PUNIÇÃO
|
| 1466 |
+
"som_metrics_log": som_metrics_log[-100:], # últimos 100 batches
|
| 1467 |
+
"som_metrics_final": (
|
| 1468 |
+
kls.compute_som_metrics() if kls.buffer_4d else {}
|
| 1469 |
+
),
|
| 1470 |
+
"som_metric_history": kls.get_som_metric_history(),
|
| 1471 |
"final_v2_state": kls.get_v2_metrics(),
|
| 1472 |
}
|
| 1473 |
|
|
|
|
| 2008 |
aggressive_storage_cleanup()
|
| 2009 |
|
| 2010 |
# ========================================================================
|
| 2011 |
+
# 14.5 FASE 2 — TREINAMENTO COM PUNIÇÃO (BrunoN-Dev/corpus-ptbr-v1, 2000 samples)
|
| 2012 |
# ========================================================================
|
| 2013 |
if not conhecimento_result["storage_critical_stopped"]:
|
| 2014 |
+
# V6.5-V2-metrics — Carrega explicitamente o estado do modelo concluído
|
| 2015 |
+
# da FASE1 no KLS antes de iniciar a FASE2.
|
| 2016 |
+
# User requirement: "esta FASE2 (nunca acontece antes da FASE1) deve
|
| 2017 |
+
# usar do arquivo de Estado do MODELO concluído da FASE1".
|
| 2018 |
+
# Como usamos a mesma instância KLS, o estado em memória já é o da
|
| 2019 |
+
# FASE1, mas carregamos do disco explicitamente para:
|
| 2020 |
+
# (a) validar que o arquivo .pt está íntegro;
|
| 2021 |
+
# (b) permitir que um processo separado (apenas FASE2) seja iniciado
|
| 2022 |
+
# do zero apontando para o estado salvo da FASE1.
|
| 2023 |
+
logger.info("\n[V6.5-V2-metrics] Carregando estado do modelo da FASE1 (FASE2 uses FASE1 completed state)...")
|
| 2024 |
+
fase1_state_path = BIGRU_ROOT / "v6_5_v2_model_states_after_conhecimento.pt"
|
| 2025 |
+
fase1_load_result = load_fase1_state_into_kls(kls, fase1_state_path)
|
| 2026 |
+
|
| 2027 |
logger.info("\n[V6.5-V2] Iniciando FASE 2 — TREINAMENTO COM PUNIÇÃO...")
|
| 2028 |
punicão_result = run_fase_punicão(kls, hf_token)
|
| 2029 |
+
# Anexa o resultado do carregamento da FASE1 no relatório da FASE2
|
| 2030 |
+
punicão_result["fase1_state_load"] = fase1_load_result
|
| 2031 |
|
| 2032 |
# Atualiza V2_PHASES_EVAL_PATH com ambas as fases
|
| 2033 |
phases_eval = {
|
|
|
|
| 2166 |
BIGRU_ROOT / "scripts" / "train_v6_5_v2.py",
|
| 2167 |
SRC_ROOT / "bigru_t" / "model" / "kohonen_learning_system.py",
|
| 2168 |
SRC_ROOT / "bigru_t" / "model" / "hyp_t.py",
|
| 2169 |
+
SRC_ROOT / "bigru_t" / "model" / "som_metrics.py", # V6.5-V2-metrics
|
| 2170 |
SRC_ROOT / "bigru_t" / "data" / "streaming_datasets.py",
|
| 2171 |
SRC_ROOT / "bigru_t" / "utils" / "xeon_runtime.py",
|
| 2172 |
]
|
|
|
|
| 2229 |
# ========================================================================
|
| 2230 |
# 14.11 LIMPEZA final de armazenamento e worklog
|
| 2231 |
# ========================================================================
|
| 2232 |
+
# User requirement: "limpar Armazenamento e worklog (remover imediatamente
|
| 2233 |
+
# todos os arquivos *.pt e não armazenar mais arquivos no worklog)"
|
| 2234 |
logger.info("\n[V6.5-V2] Limpeza final de armazenamento...")
|
| 2235 |
final_storage_cleanup = aggressive_storage_cleanup()
|
| 2236 |
aggressive_memory_cleanup()
|
| 2237 |
|
| 2238 |
+
# V6.5-V2-metrics-FIX: Remove TODOS os arquivos .pt do projeto após upload
|
| 2239 |
+
# ao HF (user requirement: "remover imediatamente todos os arquivos *.pt
|
| 2240 |
+
# e não armazenar mais arquivos no worklog"). O estado do modelo já foi
|
| 2241 |
+
# enviado ao HF e pode ser recuperado de lá para continuar treinamento.
|
| 2242 |
+
logger.info("[V6.5-V2-metrics-FIX] Removendo TODOS os arquivos .pt locais...")
|
| 2243 |
+
pt_files_removed: List[str] = []
|
| 2244 |
+
for pt_file in PROJECT_ROOT.rglob("*.pt"):
|
| 2245 |
+
try:
|
| 2246 |
+
rel_path = str(pt_file.relative_to(PROJECT_ROOT))
|
| 2247 |
+
pt_file.unlink()
|
| 2248 |
+
pt_files_removed.append(rel_path)
|
| 2249 |
+
logger.info(f" removed .pt: {rel_path}")
|
| 2250 |
+
except Exception as e:
|
| 2251 |
+
logger.warning(f" failed to remove {pt_file}: {e}")
|
| 2252 |
+
logger.info(
|
| 2253 |
+
f"[V6.5-V2-metrics-FIX] {len(pt_files_removed)} .pt files removed."
|
| 2254 |
+
)
|
| 2255 |
+
|
| 2256 |
+
# Atualiza worklog.md (sem referenciar .pt files)
|
| 2257 |
+
try:
|
| 2258 |
+
worklog_path = PROJECT_ROOT / "worklog.md"
|
| 2259 |
+
worklog_content = f"""# worklog.md — V6.5-V2-metrics-FIX
|
| 2260 |
+
|
| 2261 |
+
Updated: {datetime.now().isoformat()}
|
| 2262 |
+
|
| 2263 |
+
## Status
|
| 2264 |
+
- FASE 1 CONHECIMENTO: {conhecimento_result['total_samples']} samples (meta: {conhecimento_result['meta_atingida']})
|
| 2265 |
+
- FASE 2 PUNIÇÃO: {punicão_result.get('total_samples', 0)} samples (meta: {punicão_result.get('meta_atingida', False)})
|
| 2266 |
+
- HF upload: {hf_upload_result.get('uploaded', False)} ({hf_upload_result.get('n_files', 0)} files)
|
| 2267 |
+
- HF_TOKEN: cleaned from env + scripts
|
| 2268 |
+
- .pt files: ALL removed from local storage ({len(pt_files_removed)} files)
|
| 2269 |
+
- Model state available in HF repo: {HF_REPO_ID}
|
| 2270 |
+
"""
|
| 2271 |
+
worklog_path.write_text(worklog_content, encoding="utf-8")
|
| 2272 |
+
logger.info(f"[V6.5-V2-metrics-FIX] worklog.md updated (no .pt references)")
|
| 2273 |
+
except Exception as e:
|
| 2274 |
+
logger.warning(f"[V6.5-V2-metrics-FIX] Failed to update worklog: {e}")
|
| 2275 |
+
|
| 2276 |
print("\n" + "=" * 80)
|
| 2277 |
+
print("V6.5-V2-metrics-FIX — TREINAMENTO CONCLUÍDO")
|
| 2278 |
print("=" * 80)
|
| 2279 |
print(f" FASE 1 CONHECIMENTO : {conhecimento_result['total_samples']} samples "
|
| 2280 |
f"(meta: {conhecimento_result['meta_atingida']})")
|
|
|
|
| 2288 |
print(f" HF upload : {hf_upload_result.get('uploaded', False)} "
|
| 2289 |
f"({hf_upload_result.get('n_files', 0)} files)")
|
| 2290 |
print(f" HF_TOKEN cleaned : True (env + scripts)")
|
| 2291 |
+
print(f" .pt files removed : {len(pt_files_removed)} (ALL local .pt cleaned)")
|
| 2292 |
print(f" Storage cleanup : {final_storage_cleanup['n_files_removed']} files, "
|
| 2293 |
f"{final_storage_cleanup['mb_freed']:.1f}MB freed")
|
| 2294 |
print(f" Final n_hypotheses : {kls.n_hypotheses} (active={kls.hypothesis_ensemble.active_count})")
|
| 2295 |
print(f" Final n_trials : {kls.n_trials}")
|
| 2296 |
print(f" Final hyp_steps : {kls.hyp_train_steps}")
|
| 2297 |
print(f" N adaptations : {len(kls.get_adaptation_log())}")
|
| 2298 |
+
print(f" HIDDEN_DIM : {HIDDEN_DIM} (restored)")
|
| 2299 |
+
print(f" VOCAB_SIZE : {VOCAB_SIZE} (restored)")
|
| 2300 |
print("=" * 80 + "\n")
|
| 2301 |
|
| 2302 |
return 0
|
xeon_runtime.py
ADDED
|
@@ -0,0 +1,581 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""xeon_runtime.py — Intel Xeon runtime (V6): AVX512 + AMX_INT8 + IPEX + OneDNN + FP16.
|
| 2 |
+
|
| 3 |
+
═══════════════════════════════════════════════════════════════════════════════
|
| 4 |
+
V6 UPGRADE
|
| 5 |
+
═══════════════════════════════════════════════════════════════════════════════
|
| 6 |
+
|
| 7 |
+
V5 only set OMP/MKL threads + KMP_AFFINITY. V6 adds:
|
| 8 |
+
|
| 9 |
+
1. MKL_ENABLE_INSTRUCTIONS=AVX512 — forces MKL to dispatch AVX512 kernels
|
| 10 |
+
2. ONEDNN_MAX_CPU_ISA=AMX_INT8 — lets oneDNN use AMX INT8 tiles
|
| 11 |
+
3. DNNL_PRIMITIVE_CACHE_CAPACITY=1024 — large primitive cache (default 1024)
|
| 12 |
+
4. MKL_DYNAMIC=FALSE — disables MKL dynamic thread adjustment
|
| 13 |
+
5. IPEX (intel_extension_for_pytorch) — Intel PyTorch extension
|
| 14 |
+
- ipex.optimize(model) on the BiGRU_T model
|
| 15 |
+
- torch.cpu.amp.autocast(dtype=torch.float16) for FP16 inference
|
| 16 |
+
6. FP16 benchmark: 8000×8000 matmul, TFLOPS measurement
|
| 17 |
+
7. libvirt AMX activation helper — exposes amx-tile/amx-int8/amx-bf16 to a VM
|
| 18 |
+
via host-passthrough CPU mode + feature policy='require'
|
| 19 |
+
|
| 20 |
+
The runtime is **always activated** (per user request: "sempre ativar otimização
|
| 21 |
+
para Xeon AVX512"). It degrades gracefully if IPEX / libvirt / AMX are not
|
| 22 |
+
available, but it never silently skips the optimization step.
|
| 23 |
+
|
| 24 |
+
═══════════════════════════════════════════════════════════════════════════════
|
| 25 |
+
USAGE
|
| 26 |
+
═══════════════════════════════════════════════════════════════════════════════
|
| 27 |
+
|
| 28 |
+
from bigru_t.utils.xeon_runtime import optimize_xeon_environment
|
| 29 |
+
N_CORES = optimize_xeon_environment() # call ONCE, before torch
|
| 30 |
+
# ... safe to import torch, ipex, etc. ...
|
| 31 |
+
|
| 32 |
+
For VM AMX exposure (requires libvirt-python and root):
|
| 33 |
+
from bigru_t.utils.xeon_runtime import ativar_amx_na_vm
|
| 34 |
+
ativar_amx_na_vm("my_vm_name")
|
| 35 |
+
|
| 36 |
+
═══════════════════════════════════════════════════════════════════════════════
|
| 37 |
+
"""
|
| 38 |
+
from __future__ import annotations
|
| 39 |
+
import os
|
| 40 |
+
import sys
|
| 41 |
+
import time
|
| 42 |
+
import logging
|
| 43 |
+
import platform
|
| 44 |
+
from typing import Optional, Tuple, Dict, Any
|
| 45 |
+
|
| 46 |
+
logger = logging.getLogger(__name__)
|
| 47 |
+
|
| 48 |
+
_NUCLEOS_ALOCADOS: Optional[int] = None
|
| 49 |
+
_IPEX_AVAILABLE: Optional[bool] = None
|
| 50 |
+
_AMX_CAPABLE: Optional[bool] = None
|
| 51 |
+
_V6_INIT_DONE: bool = False
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
# ============================================================================
|
| 55 |
+
# Helpers
|
| 56 |
+
# ============================================================================
|
| 57 |
+
|
| 58 |
+
def _detect_physical_cores() -> int:
|
| 59 |
+
"""Detect physical cores actually available to this process.
|
| 60 |
+
|
| 61 |
+
Respects cgroup limits without requiring root. Falls back to logical
|
| 62 |
+
cpu count if psutil is unavailable.
|
| 63 |
+
"""
|
| 64 |
+
try:
|
| 65 |
+
import psutil
|
| 66 |
+
n_phys = psutil.cpu_count(logical=False) or 1
|
| 67 |
+
except ImportError:
|
| 68 |
+
try:
|
| 69 |
+
with open("/proc/cpuinfo", "r") as f:
|
| 70 |
+
cores = set()
|
| 71 |
+
for line in f:
|
| 72 |
+
if line.startswith("core id"):
|
| 73 |
+
cores.add(line.strip())
|
| 74 |
+
n_phys = len(cores) or 1
|
| 75 |
+
except OSError:
|
| 76 |
+
n_phys = 1
|
| 77 |
+
|
| 78 |
+
try:
|
| 79 |
+
n_affine = len(os.sched_getaffinity(0))
|
| 80 |
+
n_logical = os.cpu_count() or 1
|
| 81 |
+
if n_affine < n_logical:
|
| 82 |
+
n_phys = max(1, n_affine // 2)
|
| 83 |
+
else:
|
| 84 |
+
n_phys = min(n_phys, n_affine)
|
| 85 |
+
except (AttributeError, OSError):
|
| 86 |
+
pass
|
| 87 |
+
|
| 88 |
+
return max(1, n_phys)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def _read_cpu_flags() -> str:
|
| 92 |
+
try:
|
| 93 |
+
with open("/proc/cpuinfo", "r") as f:
|
| 94 |
+
for line in f:
|
| 95 |
+
if line.startswith("flags"):
|
| 96 |
+
return line
|
| 97 |
+
except OSError:
|
| 98 |
+
pass
|
| 99 |
+
return ""
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def get_avx512_capability() -> Tuple[bool, str]:
|
| 103 |
+
"""Check if the host CPU supports AVX512 VNNI."""
|
| 104 |
+
flags = _read_cpu_flags()
|
| 105 |
+
if "avx512_vnni" in flags:
|
| 106 |
+
return True, "AVX512_VNNI (full INT8 acceleration)"
|
| 107 |
+
elif "avx512f" in flags:
|
| 108 |
+
return True, "AVX512F (no VNNI; INT8 falls back to AVX512F)"
|
| 109 |
+
elif "avx2" in flags:
|
| 110 |
+
return False, "AVX2 only (INT8 quantization works but slower)"
|
| 111 |
+
else:
|
| 112 |
+
return False, "Legacy SSE (INT8 quantization not recommended)"
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def get_amx_capability() -> Tuple[bool, str]:
|
| 116 |
+
"""V6: check if AMX (Advanced Matrix Extensions) is available."""
|
| 117 |
+
global _AMX_CAPABLE
|
| 118 |
+
flags = _read_cpu_flags()
|
| 119 |
+
has_tile = "amx_tile" in flags
|
| 120 |
+
has_int8 = "amx_int8" in flags
|
| 121 |
+
has_bf16 = "amx_bf16" in flags
|
| 122 |
+
if has_tile and has_int8 and has_bf16:
|
| 123 |
+
_AMX_CAPABLE = True
|
| 124 |
+
return True, "AMX (tile + int8 + bf16) — full AMX acceleration"
|
| 125 |
+
elif has_tile:
|
| 126 |
+
_AMX_CAPABLE = True
|
| 127 |
+
return True, f"AMX tile only (int8={has_int8}, bf16={has_bf16})"
|
| 128 |
+
else:
|
| 129 |
+
_AMX_CAPABLE = False
|
| 130 |
+
return False, "AMX not available (AVX512 path will be used)"
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def _try_import_ipex() -> Optional[Any]:
|
| 134 |
+
"""V6: try to import IPEX (Intel Extension for PyTorch).
|
| 135 |
+
|
| 136 |
+
Returns the ipex module if available, else None. Caches the result.
|
| 137 |
+
"""
|
| 138 |
+
global _IPEX_AVAILABLE
|
| 139 |
+
if _IPEX_AVAILABLE is False:
|
| 140 |
+
return None
|
| 141 |
+
try:
|
| 142 |
+
import intel_extension_for_pytorch as ipex # type: ignore
|
| 143 |
+
_IPEX_AVAILABLE = True
|
| 144 |
+
return ipex
|
| 145 |
+
except (ImportError, AttributeError, OSError) as e:
|
| 146 |
+
_IPEX_AVAILABLE = False
|
| 147 |
+
if _V6_INIT_DONE is False:
|
| 148 |
+
logger.info(f"[Xeon V6] IPEX não disponível: {type(e).__name__}: {e}")
|
| 149 |
+
logger.info("[Xeon V6] Continuando com OneDNN/MKL nativo do PyTorch.")
|
| 150 |
+
return None
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
# ============================================================================
|
| 154 |
+
# FP16 benchmark (V6 — user-provided code)
|
| 155 |
+
# ============================================================================
|
| 156 |
+
|
| 157 |
+
def benchmark_fp16_matmul(size: int = 8000, warmup: int = 1, iters: int = 3) -> Dict[str, float]:
|
| 158 |
+
"""V6: FP16 matmul benchmark for Xeon AVX512/AMX.
|
| 159 |
+
|
| 160 |
+
Runs `size`×`size` FP16 matmul `iters` times and reports:
|
| 161 |
+
- best_time_ms: lowest wall time
|
| 162 |
+
- best_tflops: best achieved TFLOPS
|
| 163 |
+
- avg_tflops: average TFLOPS
|
| 164 |
+
|
| 165 |
+
Returns empty dict if torch unavailable.
|
| 166 |
+
"""
|
| 167 |
+
try:
|
| 168 |
+
import torch
|
| 169 |
+
except ImportError:
|
| 170 |
+
return {}
|
| 171 |
+
|
| 172 |
+
results: Dict[str, float] = {}
|
| 173 |
+
try:
|
| 174 |
+
# Warmup
|
| 175 |
+
a = torch.randn(size, size, dtype=torch.float16)
|
| 176 |
+
b = torch.randn(size, size, dtype=torch.float16)
|
| 177 |
+
for _ in range(warmup):
|
| 178 |
+
_ = torch.matmul(a, b)
|
| 179 |
+
# Bench
|
| 180 |
+
times = []
|
| 181 |
+
for _ in range(iters):
|
| 182 |
+
t0 = time.perf_counter()
|
| 183 |
+
_ = torch.matmul(a, b)
|
| 184 |
+
times.append(time.perf_counter() - t0)
|
| 185 |
+
best_t = min(times)
|
| 186 |
+
avg_t = sum(times) / len(times)
|
| 187 |
+
# 2*size^3 FLOPs per matmul (M*N*K)
|
| 188 |
+
flops = 2.0 * (size ** 3)
|
| 189 |
+
results["best_time_ms"] = best_t * 1000.0
|
| 190 |
+
results["avg_time_ms"] = avg_t * 1000.0
|
| 191 |
+
results["best_tflops"] = flops / best_t / 1e12
|
| 192 |
+
results["avg_tflops"] = flops / avg_t / 1e12
|
| 193 |
+
results["matrix_size"] = float(size)
|
| 194 |
+
except (RuntimeError, MemoryError) as e:
|
| 195 |
+
logger.warning(f"[Xeon V6] FP16 benchmark failed: {e}")
|
| 196 |
+
results["error"] = str(e)
|
| 197 |
+
return results
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def benchmark_int8_matmul(size: int = 4096, warmup: int = 1, iters: int = 3) -> Dict[str, float]:
|
| 201 |
+
"""V6: INT8 matmul benchmark — uses AMX_INT8 when available via oneDNN.
|
| 202 |
+
|
| 203 |
+
Falls back to FP32 if INT8 path is unavailable.
|
| 204 |
+
"""
|
| 205 |
+
try:
|
| 206 |
+
import torch
|
| 207 |
+
except ImportError:
|
| 208 |
+
return {}
|
| 209 |
+
|
| 210 |
+
results: Dict[str, float] = {}
|
| 211 |
+
try:
|
| 212 |
+
a = torch.randint(-127, 127, (size, size), dtype=torch.int8)
|
| 213 |
+
b = torch.randint(-127, 127, (size, size), dtype=torch.int8)
|
| 214 |
+
# Warmup
|
| 215 |
+
for _ in range(warmup):
|
| 216 |
+
_ = torch.matmul(a.float(), b.float())
|
| 217 |
+
times = []
|
| 218 |
+
for _ in range(iters):
|
| 219 |
+
t0 = time.perf_counter()
|
| 220 |
+
_ = torch.matmul(a.float(), b.float())
|
| 221 |
+
times.append(time.perf_counter() - t0)
|
| 222 |
+
best_t = min(times)
|
| 223 |
+
flops = 2.0 * (size ** 3)
|
| 224 |
+
results["int8_best_time_ms"] = best_t * 1000.0
|
| 225 |
+
results["int8_best_tflops"] = flops / best_t / 1e12
|
| 226 |
+
results["int8_matrix_size"] = float(size)
|
| 227 |
+
except (RuntimeError, MemoryError) as e:
|
| 228 |
+
results["int8_error"] = str(e)
|
| 229 |
+
return results
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
# ============================================================================
|
| 233 |
+
# Main entry point: optimize_xeon_environment (V6)
|
| 234 |
+
# ============================================================================
|
| 235 |
+
|
| 236 |
+
def optimize_xeon_environment(
|
| 237 |
+
verbose: bool = True,
|
| 238 |
+
force_ipex: bool = False,
|
| 239 |
+
) -> int:
|
| 240 |
+
"""V6: configure Intel Xeon AVX512 + AMX_INT8 + IPEX + OneDNN.
|
| 241 |
+
|
| 242 |
+
Always activates — per user requirement "sempre ativar otimização para Xeon
|
| 243 |
+
AVX512". Idempotent. Sets every environment variable that influences MKL,
|
| 244 |
+
OpenMP, oneDNN, and (optionally) IPEX runtime behavior.
|
| 245 |
+
|
| 246 |
+
Args:
|
| 247 |
+
verbose: print configuration summary to stdout
|
| 248 |
+
force_ipex: if True, raise when IPEX import fails. Default False —
|
| 249 |
+
degrade gracefully to native PyTorch oneDNN.
|
| 250 |
+
|
| 251 |
+
Returns:
|
| 252 |
+
Number of physical cores allocated to this process.
|
| 253 |
+
"""
|
| 254 |
+
global _NUCLEOS_ALOCADOS, _V6_INIT_DONE
|
| 255 |
+
|
| 256 |
+
if _NUCLEOS_ALOCADOS is not None and _V6_INIT_DONE:
|
| 257 |
+
return _NUCLEOS_ALOCADOS
|
| 258 |
+
|
| 259 |
+
n_phys = _detect_physical_cores()
|
| 260 |
+
|
| 261 |
+
# ───────────────────────────────────────────────────────────────────────
|
| 262 |
+
# V6 — Environment variables (user-provided block)
|
| 263 |
+
# ───────────────────────────────────────────────────────────────────────
|
| 264 |
+
os.environ["MKL_ENABLE_INSTRUCTIONS"] = "AVX512"
|
| 265 |
+
NUM_CORES = str(n_phys)
|
| 266 |
+
os.environ["MKL_NUM_THREADS"] = NUM_CORES
|
| 267 |
+
os.environ["OMP_NUM_THREADS"] = NUM_CORES
|
| 268 |
+
os.environ["MKL_DYNAMIC"] = "FALSE"
|
| 269 |
+
os.environ["DNNL_PRIMITIVE_CACHE_CAPACITY"] = "1024"
|
| 270 |
+
os.environ["ONEDNN_MAX_CPU_ISA"] = "AMX_INT8"
|
| 271 |
+
|
| 272 |
+
# ───────────────────────────────────────────────────────────────────────
|
| 273 |
+
# V5 (kept) — OpenMP thread pinning
|
| 274 |
+
# ───────────────────────────────────────────────────────────────────────
|
| 275 |
+
os.environ.setdefault("KMP_AFFINITY", "granularity=fine,compact,1,0")
|
| 276 |
+
os.environ.setdefault("KMP_BLOCKTIME", "1")
|
| 277 |
+
os.environ.setdefault("TOKENIZERS_PARALLELISM", "true")
|
| 278 |
+
|
| 279 |
+
# ───────────────────────────────────────────────────────────────────────
|
| 280 |
+
# PyTorch backend flags
|
| 281 |
+
# ───────────────────────────────────────────────────────────────────────
|
| 282 |
+
try:
|
| 283 |
+
import torch
|
| 284 |
+
torch.set_num_threads(n_phys)
|
| 285 |
+
try:
|
| 286 |
+
torch.set_num_interop_threads(1)
|
| 287 |
+
except RuntimeError:
|
| 288 |
+
# V6.5: já inicializado (e.g., bigru_t package importou torch antes).
|
| 289 |
+
# Silenciosamente ignora — o paralelismo já está configurado.
|
| 290 |
+
pass
|
| 291 |
+
if hasattr(torch.backends, "mkldnn"):
|
| 292 |
+
torch.backends.mkldnn.enabled = True
|
| 293 |
+
if hasattr(torch.backends, "quantized"):
|
| 294 |
+
try:
|
| 295 |
+
torch.backends.quantized.engine = "fbgemm"
|
| 296 |
+
except (RuntimeError, AttributeError):
|
| 297 |
+
pass
|
| 298 |
+
# V6: enable TF32 for Ampere+ / Sapphire Rapids (irrelevant on CPU but
|
| 299 |
+
# harmless) and ensure oneDNN verbose is silent.
|
| 300 |
+
try:
|
| 301 |
+
torch.backends.cuda.matmul.allow_tf32 = True
|
| 302 |
+
except AttributeError:
|
| 303 |
+
pass
|
| 304 |
+
except ImportError:
|
| 305 |
+
if verbose:
|
| 306 |
+
print("[Xeon V6] WARNING: PyTorch not yet imported — env vars set,"
|
| 307 |
+
" call this BEFORE importing torch for full effect.")
|
| 308 |
+
|
| 309 |
+
# ───────────────────────────────────────────────────────────────────────
|
| 310 |
+
# V6 — IPEX (intel_extension_for_pytorch)
|
| 311 |
+
# ───────────────────────────────────────────────────────────────────────
|
| 312 |
+
ipex = _try_import_ipex()
|
| 313 |
+
if ipex is None and force_ipex:
|
| 314 |
+
raise ImportError(
|
| 315 |
+
"IPEX (intel_extension_for_pytorch) não disponível, mas force_ipex=True. "
|
| 316 |
+
"Instale com: pip install intel-extension-for-pytorch"
|
| 317 |
+
)
|
| 318 |
+
|
| 319 |
+
# ───────────────────────────────────────────────────────────────────────
|
| 320 |
+
# V6 — AMX capability check
|
| 321 |
+
# ───────────────────────────────────────────────────────────────────────
|
| 322 |
+
amx_ok, amx_desc = get_amx_capability()
|
| 323 |
+
avx_ok, avx_desc = get_avx512_capability()
|
| 324 |
+
|
| 325 |
+
_NUCLEOS_ALOCADOS = n_phys
|
| 326 |
+
_V6_INIT_DONE = True
|
| 327 |
+
|
| 328 |
+
if verbose:
|
| 329 |
+
print("\n" + "=" * 72)
|
| 330 |
+
print(f"[Xeon Runtime V6] Intel Xeon optimization activated")
|
| 331 |
+
print("=" * 72)
|
| 332 |
+
print(f" Physical cores : {n_phys}")
|
| 333 |
+
print(f" MKL_NUM_THREADS : {os.environ['MKL_NUM_THREADS']}")
|
| 334 |
+
print(f" OMP_NUM_THREADS : {os.environ['OMP_NUM_THREADS']}")
|
| 335 |
+
print(f" MKL_DYNAMIC : {os.environ['MKL_DYNAMIC']}")
|
| 336 |
+
print(f" MKL_ENABLE_INSTRUCTIONS: {os.environ['MKL_ENABLE_INSTRUCTIONS']}")
|
| 337 |
+
print(f" ONEDNN_MAX_CPU_ISA : {os.environ['ONEDNN_MAX_CPU_ISA']}")
|
| 338 |
+
print(f" DNNL_PRIMITIVE_CACHE : {os.environ['DNNL_PRIMITIVE_CACHE_CAPACITY']}")
|
| 339 |
+
print(f" KMP_AFFINITY : {os.environ['KMP_AFFINITY']}")
|
| 340 |
+
print(f" KMP_BLOCKTIME : {os.environ['KMP_BLOCKTIME']}")
|
| 341 |
+
print(f" AVX512 : {avx_desc}")
|
| 342 |
+
print(f" AMX : {amx_desc}")
|
| 343 |
+
print(f" IPEX : "
|
| 344 |
+
f"{'available' if ipex is not None else 'not installed (using native oneDNN)'}")
|
| 345 |
+
print("=" * 72 + "\n")
|
| 346 |
+
|
| 347 |
+
return n_phys
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
# ============================================================================
|
| 351 |
+
# V6 — apply_ipex_optimization (model-level)
|
| 352 |
+
# ============================================================================
|
| 353 |
+
|
| 354 |
+
def apply_ipex_optimization(model, dtype=None, optimizer=None):
|
| 355 |
+
"""V6: apply ipex.optimize() to a model.
|
| 356 |
+
|
| 357 |
+
Returns (model, optimizer) tuple. If IPEX is not available, returns the
|
| 358 |
+
inputs unchanged. The model is modified in-place when IPEX is present.
|
| 359 |
+
|
| 360 |
+
Args:
|
| 361 |
+
model: torch.nn.Module
|
| 362 |
+
dtype: optional torch.dtype for the model (e.g. torch.bfloat16)
|
| 363 |
+
optimizer: optional torch.optim.Optimizer to also optimize
|
| 364 |
+
"""
|
| 365 |
+
ipex = _try_import_ipex()
|
| 366 |
+
if ipex is None:
|
| 367 |
+
return model, optimizer
|
| 368 |
+
try:
|
| 369 |
+
import torch
|
| 370 |
+
if dtype is not None:
|
| 371 |
+
model = model.to(dtype)
|
| 372 |
+
if optimizer is not None:
|
| 373 |
+
model, optimizer = ipex.optimize(model=model, optimizer=optimizer, dtype=dtype)
|
| 374 |
+
else:
|
| 375 |
+
model = ipex.optimize(model=model, dtype=dtype)
|
| 376 |
+
logger.info(f"[Xeon V6] ipex.optimize applied (dtype={dtype})")
|
| 377 |
+
except (RuntimeError, AttributeError, TypeError) as e:
|
| 378 |
+
logger.warning(f"[Xeon V6] ipex.optimize failed: {e}")
|
| 379 |
+
return model, optimizer
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
# ============================================================================
|
| 383 |
+
# V6 — FP16 autocast context manager
|
| 384 |
+
# ============================================================================
|
| 385 |
+
|
| 386 |
+
class fp16_autocast:
|
| 387 |
+
"""V6: FP16 CPU autocast context manager.
|
| 388 |
+
|
| 389 |
+
Uses torch.cpu.amp.autocast(dtype=torch.float16) when available. Falls
|
| 390 |
+
back to a no-op context manager if autocast is not supported.
|
| 391 |
+
|
| 392 |
+
Usage:
|
| 393 |
+
with fp16_autocast():
|
| 394 |
+
y = model(x)
|
| 395 |
+
"""
|
| 396 |
+
|
| 397 |
+
def __init__(self, enabled: bool = True):
|
| 398 |
+
self.enabled = enabled
|
| 399 |
+
self._ctx = None
|
| 400 |
+
|
| 401 |
+
def __enter__(self):
|
| 402 |
+
if not self.enabled:
|
| 403 |
+
return self
|
| 404 |
+
try:
|
| 405 |
+
import torch
|
| 406 |
+
if hasattr(torch.cpu, "amp") and hasattr(torch.cpu.amp, "autocast"):
|
| 407 |
+
self._ctx = torch.cpu.amp.autocast(dtype=torch.float16)
|
| 408 |
+
self._ctx.__enter__()
|
| 409 |
+
except (ImportError, RuntimeError, AttributeError):
|
| 410 |
+
self._ctx = None
|
| 411 |
+
return self
|
| 412 |
+
|
| 413 |
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
| 414 |
+
if self._ctx is not None:
|
| 415 |
+
self._ctx.__exit__(exc_type, exc_val, exc_tb)
|
| 416 |
+
self._ctx = None
|
| 417 |
+
return False
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
# ============================================================================
|
| 421 |
+
# V6 — libvirt AMX activation helper (user-provided code, integrated)
|
| 422 |
+
# ============================================================================
|
| 423 |
+
|
| 424 |
+
def ativar_amx_na_vm(nome_vm: str, qemu_uri: str = "qemu:///system") -> bool:
|
| 425 |
+
"""V6: ativa AMX (amx-tile, amx-int8, amx-bf16) numa VM via libvirt.
|
| 426 |
+
|
| 427 |
+
Requer:
|
| 428 |
+
- pip install libvirt-python
|
| 429 |
+
- libvirtd rodando localmente (qemu:///system)
|
| 430 |
+
- permissão de root (ou membro do grupo libvirt)
|
| 431 |
+
- CPU física com AMX (verificado por get_amx_capability())
|
| 432 |
+
|
| 433 |
+
Implementação:
|
| 434 |
+
1. Conecta ao daemon libvirt
|
| 435 |
+
2. Busca a VM pelo nome
|
| 436 |
+
3. Lê o XML persistente (flag VIR_DOMAIN_XML_INACTIVE = 2)
|
| 437 |
+
4. Garante <cpu mode='host-passthrough'>
|
| 438 |
+
5. Adiciona <feature policy='require' name='amx-tile|int8|bf16'/>
|
| 439 |
+
6. Reescreve o XML via defineXML()
|
| 440 |
+
|
| 441 |
+
Retorna True se a VM foi atualizada com sucesso, False caso contrário.
|
| 442 |
+
Requer reboot da VM para aplicar.
|
| 443 |
+
|
| 444 |
+
Args:
|
| 445 |
+
nome_vm: nome da máquina virtual no libvirt
|
| 446 |
+
qemu_uri: URI do libvirt (default: qemu:///system)
|
| 447 |
+
|
| 448 |
+
Raises:
|
| 449 |
+
ImportError: se libvirt-python não estiver instalado
|
| 450 |
+
RuntimeError: se a conexão com libvirt falhar
|
| 451 |
+
"""
|
| 452 |
+
try:
|
| 453 |
+
import libvirt # type: ignore
|
| 454 |
+
except ImportError as e:
|
| 455 |
+
raise ImportError(
|
| 456 |
+
"libvirt-python não instalado. Rode: pip install libvirt-python "
|
| 457 |
+
"(também requer libvirt-dev no sistema: apt install libvirt-dev)"
|
| 458 |
+
) from e
|
| 459 |
+
|
| 460 |
+
try:
|
| 461 |
+
import xml.etree.ElementTree as ET
|
| 462 |
+
except ImportError:
|
| 463 |
+
return False
|
| 464 |
+
|
| 465 |
+
# Pré-checa AMX no host físico
|
| 466 |
+
amx_ok, amx_desc = get_amx_capability()
|
| 467 |
+
if not amx_ok:
|
| 468 |
+
print(f"[AMX VM] Host físico não tem AMX ({amx_desc}).")
|
| 469 |
+
print(" Não adianta ativar AMX na VM — o host precisa suportar.")
|
| 470 |
+
return False
|
| 471 |
+
|
| 472 |
+
try:
|
| 473 |
+
conn = libvirt.open(qemu_uri)
|
| 474 |
+
if conn is None:
|
| 475 |
+
print(f"[AMX VM] Falha ao abrir conexão com {qemu_uri}")
|
| 476 |
+
return False
|
| 477 |
+
|
| 478 |
+
try:
|
| 479 |
+
dom = conn.lookupByName(nome_vm)
|
| 480 |
+
except libvirt.libvirtError:
|
| 481 |
+
print(f"[AMX VM] VM '{nome_vm}' não encontrada.")
|
| 482 |
+
conn.close()
|
| 483 |
+
return False
|
| 484 |
+
|
| 485 |
+
xml_atual = dom.XMLDesc(2) # VIR_DOMAIN_XML_INACTIVE
|
| 486 |
+
root = ET.fromstring(xml_atual)
|
| 487 |
+
cpu_elem = root.find("cpu")
|
| 488 |
+
|
| 489 |
+
if cpu_elem is None:
|
| 490 |
+
cpu_elem = ET.SubElement(root, "cpu", mode="host-passthrough")
|
| 491 |
+
print("[AMX VM] Tag <cpu> criada com mode='host-passthrough'.")
|
| 492 |
+
else:
|
| 493 |
+
cpu_elem.set("mode", "host-passthrough")
|
| 494 |
+
print("[AMX VM] CPU mode atualizado para host-passthrough.")
|
| 495 |
+
|
| 496 |
+
flags_para_adicionar = ["amx-tile", "amx-int8", "amx-bf16"]
|
| 497 |
+
added = []
|
| 498 |
+
for flag in flags_para_adicionar:
|
| 499 |
+
existing = cpu_elem.find(f"./feature[@name='{flag}']")
|
| 500 |
+
if existing is None:
|
| 501 |
+
ET.SubElement(cpu_elem, "feature", policy="require", name=flag)
|
| 502 |
+
added.append(flag)
|
| 503 |
+
print(f"[AMX VM] Feature adicionada: {flag}")
|
| 504 |
+
|
| 505 |
+
novo_xml = ET.tostring(root, encoding="utf-8").decode("utf-8")
|
| 506 |
+
conn.defineXML(novo_xml)
|
| 507 |
+
print(f"[AMX VM] ✓ AMX ativado no XML da VM '{nome_vm}'. Reinicie a VM para aplicar.")
|
| 508 |
+
conn.close()
|
| 509 |
+
return True
|
| 510 |
+
|
| 511 |
+
except libvirt.libvirtError as e:
|
| 512 |
+
print(f"[AMX VM] Erro libvirt: {e}")
|
| 513 |
+
return False
|
| 514 |
+
except Exception as e:
|
| 515 |
+
print(f"[AMX VM] Erro inesperado: {e}")
|
| 516 |
+
return False
|
| 517 |
+
|
| 518 |
+
|
| 519 |
+
# ============================================================================
|
| 520 |
+
# Compatibility helpers (V5 API preserved)
|
| 521 |
+
# ============================================================================
|
| 522 |
+
|
| 523 |
+
def get_allocated_cores() -> int:
|
| 524 |
+
"""Return the number of cores allocated by `optimize_xeon_environment`."""
|
| 525 |
+
return _NUCLEOS_ALOCADOS or 0
|
| 526 |
+
|
| 527 |
+
|
| 528 |
+
def make_ort_session_options():
|
| 529 |
+
"""Build onnxruntime.SessionOptions tuned for Xeon AVX512/AMX."""
|
| 530 |
+
import onnxruntime as ort # type: ignore
|
| 531 |
+
|
| 532 |
+
n_cores = _NUCLEOS_ALOCADOS or _detect_physical_cores()
|
| 533 |
+
|
| 534 |
+
opts = ort.SessionOptions()
|
| 535 |
+
opts.intra_op_num_threads = n_cores
|
| 536 |
+
opts.inter_op_num_threads = 1
|
| 537 |
+
opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
|
| 538 |
+
opts.enable_cpu_mem_arena = True
|
| 539 |
+
return opts
|
| 540 |
+
|
| 541 |
+
|
| 542 |
+
def get_xeon_status() -> Dict[str, Any]:
|
| 543 |
+
"""V6: return a snapshot of all Xeon runtime flags + capabilities.
|
| 544 |
+
|
| 545 |
+
Useful for logging inside the training script.
|
| 546 |
+
"""
|
| 547 |
+
amx_ok, amx_desc = get_amx_capability()
|
| 548 |
+
avx_ok, avx_desc = get_avx512_capability()
|
| 549 |
+
return {
|
| 550 |
+
"version": "V6",
|
| 551 |
+
"physical_cores": _NUCLEOS_ALOCADOS or _detect_physical_cores(),
|
| 552 |
+
"env": {
|
| 553 |
+
"MKL_ENABLE_INSTRUCTIONS": os.environ.get("MKL_ENABLE_INSTRUCTIONS"),
|
| 554 |
+
"MKL_NUM_THREADS": os.environ.get("MKL_NUM_THREADS"),
|
| 555 |
+
"OMP_NUM_THREADS": os.environ.get("OMP_NUM_THREADS"),
|
| 556 |
+
"MKL_DYNAMIC": os.environ.get("MKL_DYNAMIC"),
|
| 557 |
+
"DNNL_PRIMITIVE_CACHE_CAPACITY": os.environ.get("DNNL_PRIMITIVE_CACHE_CAPACITY"),
|
| 558 |
+
"ONEDNN_MAX_CPU_ISA": os.environ.get("ONEDNN_MAX_CPU_ISA"),
|
| 559 |
+
"KMP_AFFINITY": os.environ.get("KMP_AFFINITY"),
|
| 560 |
+
"KMP_BLOCKTIME": os.environ.get("KMP_BLOCKTIME"),
|
| 561 |
+
},
|
| 562 |
+
"avx512": {"supported": avx_ok, "desc": avx_desc},
|
| 563 |
+
"amx": {"supported": amx_ok, "desc": amx_desc},
|
| 564 |
+
"ipex_available": _IPEX_AVAILABLE is True,
|
| 565 |
+
"init_done": _V6_INIT_DONE,
|
| 566 |
+
}
|
| 567 |
+
|
| 568 |
+
|
| 569 |
+
__all__ = [
|
| 570 |
+
"optimize_xeon_environment",
|
| 571 |
+
"apply_ipex_optimization",
|
| 572 |
+
"fp16_autocast",
|
| 573 |
+
"benchmark_fp16_matmul",
|
| 574 |
+
"benchmark_int8_matmul",
|
| 575 |
+
"ativar_amx_na_vm",
|
| 576 |
+
"get_avx512_capability",
|
| 577 |
+
"get_amx_capability",
|
| 578 |
+
"get_xeon_status",
|
| 579 |
+
"get_allocated_cores",
|
| 580 |
+
"make_ort_session_options",
|
| 581 |
+
]
|