PowerMachine commited on
Commit
36d8c43
·
verified ·
1 Parent(s): fe2288d

V6.5-V2-parallel-reorganize: ParallelSOM4D + ReorganizingSOM4D + OOM-killer guards + unified state (75 files)

Browse files
model_states/v6_5_v2_model_states.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ca83456ac9ae4085ab33572768acf9b516ca5cc4b56b5cd63784d546a836735a
3
+ size 220157044
scripts/train_v6_5_v2.py CHANGED
@@ -1199,11 +1199,62 @@ def run_fase_conhecimento(
1199
  dataset_name=dataset_name,
1200
  enable_punishment=False,
1201
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1202
  except Exception as e:
1203
  logger.error(f"[V6.5-V2] process_batch_v2 error: {e}")
1204
  traceback.print_exc()
1205
  continue
1206
 
 
 
 
 
 
 
 
 
1207
  # V6.5-V2-kmeans-pp - Apos 1o chunk, inicializa pesos
1208
  # do SOM via k-means++ (deferred init).
1209
  # User requirement: "inicializacao com k-means++ sobre
@@ -1250,6 +1301,27 @@ def run_fase_conhecimento(
1250
 
1251
  step += 1
1252
  time.sleep(INTER_BATCH_PAUSE_S_FASE1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1253
  except Exception as e:
1254
  logger.error(f"[V6.5-V2] Batch error: {e}")
1255
  traceback.print_exc()
@@ -1897,11 +1969,54 @@ def run_fase_punicão(
1897
  dataset_name=PUNICAO_DATASET,
1898
  enable_punishment=True,
1899
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1900
  except Exception as e:
1901
  logger.error(f"[V6.5-V2] process_batch_v2 error: {e}")
1902
  traceback.print_exc()
1903
  continue
1904
 
 
 
 
 
 
1905
  # Log punishment events
1906
  action = result.get("action", "none")
1907
  if action != "none" and action != "success":
 
1199
  dataset_name=dataset_name,
1200
  enable_punishment=False,
1201
  )
1202
+ except (MemoryError, RuntimeError) as oom_err:
1203
+ # V6.5-V2-oom-guard — OOM-killer protection
1204
+ # User requirement: "o processo vem sendo morto OOM-kiler
1205
+ # (Out of memory) devido algum bug de lógica ou falta de
1206
+ # otimização que deve ser investigado (acrescentar
1207
+ # exceptions e melhor detecção de falhas de lógica e
1208
+ # erros de script)".
1209
+ #
1210
+ # Captura MemoryError e RuntimeError("out of memory")
1211
+ # explicitamente. Em vez de morrer, dispara limpeza
1212
+ # agressiva de memória e continua com o próximo batch.
1213
+ is_oom = (
1214
+ isinstance(oom_err, MemoryError)
1215
+ or "out of memory" in str(oom_err).lower()
1216
+ or "cuda" in str(oom_err).lower()
1217
+ )
1218
+ if is_oom:
1219
+ logger.error(
1220
+ f"[V6.5-V2-oom-guard] OOM detected in process_batch_v2: "
1221
+ f"{type(oom_err).__name__}: {str(oom_err)[:200]}"
1222
+ )
1223
+ logger.error("[V6.5-V2-oom-guard] Triggering aggressive memory cleanup...")
1224
+ cleanup_result = aggressive_memory_cleanup()
1225
+ logger.error(
1226
+ f"[V6.5-V2-oom-guard] Cleanup freed "
1227
+ f"{cleanup_result.get('freed_mb', 0):.1f}MB "
1228
+ f"(RSS: {cleanup_result.get('rss_before_mb', 0):.0f}MB → "
1229
+ f"{cleanup_result.get('rss_after_mb', 0):.0f}MB)"
1230
+ )
1231
+ # Trunca buffer para reduzir pressão
1232
+ if len(kls.buffer_4d) > 32:
1233
+ overflow = len(kls.buffer_4d) - 32
1234
+ del kls.buffer_4d[:overflow]
1235
+ del kls.buffer_labels[:overflow]
1236
+ logger.error(
1237
+ f"[V6.5-V2-oom-guard] Buffer truncated to 32 "
1238
+ f"(dropped {overflow} samples)"
1239
+ )
1240
+ continue
1241
+ else:
1242
+ logger.error(f"[V6.5-V2] process_batch_v2 RuntimeError: {oom_err}")
1243
+ traceback.print_exc()
1244
+ continue
1245
  except Exception as e:
1246
  logger.error(f"[V6.5-V2] process_batch_v2 error: {e}")
1247
  traceback.print_exc()
1248
  continue
1249
 
1250
+ # V6.5-V2-oom-guard — gc.collect() entre batches para
1251
+ # liberar tensores intermediários antes que se acumulem.
1252
+ # Em runs longos (8000+ samples), mesmo pequenos leaks
1253
+ # acumulam e causam OOM. O custo de gc.collect() é ~10ms,
1254
+ # insignificante vs. o risco de morte por OOM-killer.
1255
+ if step % 4 == 0:
1256
+ gc.collect()
1257
+
1258
  # V6.5-V2-kmeans-pp - Apos 1o chunk, inicializa pesos
1259
  # do SOM via k-means++ (deferred init).
1260
  # User requirement: "inicializacao com k-means++ sobre
 
1301
 
1302
  step += 1
1303
  time.sleep(INTER_BATCH_PAUSE_S_FASE1)
1304
+ except (MemoryError, RuntimeError) as oom_err_outer:
1305
+ # V6.5-V2-oom-guard — Outer OOM catch (metrics/attention eval)
1306
+ is_oom_outer = (
1307
+ isinstance(oom_err_outer, MemoryError)
1308
+ or "out of memory" in str(oom_err_outer).lower()
1309
+ )
1310
+ if is_oom_outer:
1311
+ logger.error(
1312
+ f"[V6.5-V2-oom-guard] Outer OOM in batch loop: "
1313
+ f"{type(oom_err_outer).__name__}: {str(oom_err_outer)[:200]}"
1314
+ )
1315
+ cleanup_result = aggressive_memory_cleanup()
1316
+ logger.error(
1317
+ f"[V6.5-V2-oom-guard] Cleanup freed "
1318
+ f"{cleanup_result.get('freed_mb', 0):.1f}MB"
1319
+ )
1320
+ continue
1321
+ else:
1322
+ logger.error(f"[V6.5-V2] Batch RuntimeError: {oom_err_outer}")
1323
+ traceback.print_exc()
1324
+ continue
1325
  except Exception as e:
1326
  logger.error(f"[V6.5-V2] Batch error: {e}")
1327
  traceback.print_exc()
 
1969
  dataset_name=PUNICAO_DATASET,
1970
  enable_punishment=True,
1971
  )
1972
+ except (MemoryError, RuntimeError) as oom_err_fase2:
1973
+ # V6.5-V2-oom-guard — OOM-killer protection FASE2
1974
+ # User requirement: "o processo vem sendo morto OOM-kiler".
1975
+ # FASE2 é mais pesada (hypothesis training + EWC) e mais
1976
+ # propensa a OOM. Captura MemoryError e RuntimeError("out
1977
+ # of memory") explicitamente para evitar morte do processo.
1978
+ is_oom_f2 = (
1979
+ isinstance(oom_err_fase2, MemoryError)
1980
+ or "out of memory" in str(oom_err_fase2).lower()
1981
+ or "cuda" in str(oom_err_fase2).lower()
1982
+ )
1983
+ if is_oom_f2:
1984
+ logger.error(
1985
+ f"[V6.5-V2-oom-guard] FASE2 OOM detected: "
1986
+ f"{type(oom_err_fase2).__name__}: {str(oom_err_fase2)[:200]}"
1987
+ )
1988
+ logger.error("[V6.5-V2-oom-guard] Triggering aggressive memory cleanup...")
1989
+ cleanup_result = aggressive_memory_cleanup()
1990
+ logger.error(
1991
+ f"[V6.5-V2-oom-guard] FASE2 cleanup freed "
1992
+ f"{cleanup_result.get('freed_mb', 0):.1f}MB "
1993
+ f"(RSS: {cleanup_result.get('rss_before_mb', 0):.0f}MB → "
1994
+ f"{cleanup_result.get('rss_after_mb', 0):.0f}MB)"
1995
+ )
1996
+ # Trunca buffer para reduzir pressão
1997
+ if len(kls.buffer_4d) > 32:
1998
+ overflow = len(kls.buffer_4d) - 32
1999
+ del kls.buffer_4d[:overflow]
2000
+ del kls.buffer_labels[:overflow]
2001
+ logger.error(
2002
+ f"[V6.5-V2-oom-guard] FASE2 buffer truncated to 32 "
2003
+ f"(dropped {overflow} samples)"
2004
+ )
2005
+ continue
2006
+ else:
2007
+ logger.error(f"[V6.5-V2] FASE2 process_batch_v2 RuntimeError: {oom_err_fase2}")
2008
+ traceback.print_exc()
2009
+ continue
2010
  except Exception as e:
2011
  logger.error(f"[V6.5-V2] process_batch_v2 error: {e}")
2012
  traceback.print_exc()
2013
  continue
2014
 
2015
+ # V6.5-V2-oom-guard — gc.collect() entre batches FASE2
2016
+ # (PUNITIVA é mais pesada — hypothesis training + EWC + revival)
2017
+ if step % 2 == 0:
2018
+ gc.collect()
2019
+
2020
  # Log punishment events
2021
  action = result.get("action", "none")
2022
  if action != "none" and action != "success":
src/bigru_t/model/kohonen_learning_system.py CHANGED
@@ -975,6 +975,314 @@ class KohonenSOM4D:
975
  "n_recent_bmus_tracked": int(len(self._recent_bmu_flat)),
976
  }
977
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
978
  def auto_adjust_conscience_gamma(self) -> Dict[str, Any]:
979
  """V6.5-V2-auto-conscience-v2 — Ajusta γ dinamicamente conforme ativação.
980
 
@@ -1804,22 +2112,60 @@ class KohonenLearningSystem:
1804
  V6.5-V2-memfix — Wrapped in torch.no_grad() to prevent graph building
1805
  during SOM Hebbian updates. The SOM uses competitive learning (BMU +
1806
  Gaussian neighborhood), not backprop — no gradient is needed.
 
 
 
 
 
 
 
 
 
 
1807
  """
1808
  if not self.buffer_4d:
1809
  return
1810
  # V6.5-V2-memfix — detach+stack to ensure no graph leaks from buffer
1811
  data = torch.stack(self.buffer_4d).detach()
1812
  with torch.no_grad():
 
 
 
 
 
 
1813
  for _ in range(5): # épocas de treino rápido
1814
  perm = torch.randperm(len(data))
1815
- for idx in perm:
1816
- x = data[idx]
1817
- bmu = self.som.find_bmu(x)
1818
- acc_fisher = (
1819
- self.punishment_count == 0
1820
- and self.som.old_weights_w is None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1821
  )
1822
- self.som.update_weights(x, bmu, accumulate_fisher=acc_fisher)
 
1823
 
1824
  # V6.5-V2-metrics-FIX-3 — VQ-VAE-2 e W8A8 compressão dentro de
1825
  # no_grad para evitar construção de grafo computacional (50-200MB
@@ -4104,6 +4450,128 @@ class KohonenLearningSystemV2(KohonenLearningSystem):
4104
  """
4105
  return self.som.parallel_neuron_activation_report()
4106
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4107
  def auto_revive_if_needed(
4108
  self,
4109
  dead_rate_threshold: float = 0.5,
 
975
  "n_recent_bmus_tracked": int(len(self._recent_bmu_flat)),
976
  }
977
 
978
+ # ==================================================================
979
+ # V6.5-V2-parallel — Vectorial parallel SOM (ParallelSOM4D pattern)
980
+ # User requirement: "Para elevar o desempenho ao nível máximo, a arquitetura
981
+ # com multiprocessing (baseada em CPU) que usamos anteriormente não é a
982
+ # ideal para cenários de alta dimensão. O custo de copiar os dados entre
983
+ # múltiplos processos do Python (overhead de IPC) anula o ganho da
984
+ # computação paralela. Para um ajuste paralelo verdadeiro e massivo de uma
985
+ # SOM de 4 dimensões, a abordagem ideal é o paralelismo vetorial em GPU
986
+ # utilizando PyTorch."
987
+ #
988
+ # Math: todas as operações são feitas via broadcasting PyTorch:
989
+ # 1. distances = sum((flat_weights - sample)^2, dim=-1) # (N_neurons,)
990
+ # 2. bmu_idx = argmin(distances)
991
+ # 3. grid_dists_sq = sum((grid_coords - bmu_coord)^2, dim=-1) # (N_neurons,)
992
+ # 4. influence = exp(-grid_dists_sq / (2 * sigma^2)) # (N_neurons,)
993
+ # 5. weights += alpha * influence * (sample - weights) # broadcast (N, 4)
994
+ # ==================================================================
995
+ def _precompute_grid_coords_flat(self) -> torch.Tensor:
996
+ """Pré-computa coordenadas 4D da grade planificadas (N_neurons, 4).
997
+
998
+ V6.5-V2-parallel — pré-computação evita recriar meshgrid a cada update.
999
+ Usado por train_batch_parallel e reorganize_map_by_density.
1000
+ """
1001
+ if not hasattr(self, "_grid_coords_flat_cache") or self._grid_coords_flat_cache is None:
1002
+ II, JJ, KK, LL = torch.meshgrid(
1003
+ torch.arange(self.I).float(),
1004
+ torch.arange(self.J).float(),
1005
+ torch.arange(self.K).float(),
1006
+ torch.arange(self.L).float(),
1007
+ indexing="ij",
1008
+ )
1009
+ grid_coords = torch.stack([II, JJ, KK, LL], dim=-1) # (I, J, K, L, 4)
1010
+ self._grid_coords_flat_cache = grid_coords.view(-1, 4) # (N, 4)
1011
+ return self._grid_coords_flat_cache
1012
+
1013
+ def train_batch_parallel(
1014
+ self,
1015
+ samples: torch.Tensor,
1016
+ use_conscience: bool = True,
1017
+ accumulate_fisher: bool = False,
1018
+ ) -> Dict[str, Any]:
1019
+ """V6.5-V2-parallel — Treina SOM com paralelismo vetorial PyTorch.
1020
+
1021
+ User requirement (ParallelSOM4D pattern): processa amostras usando
1022
+ operações matriciais PyTorch ao invés de loops Python. Cada amostra
1023
+ ainda é processada sequencialmente (online learning — Kohonen canônico),
1024
+ mas todas as operações internas (distância, BMU, vizinhança, update)
1025
+ são feitas via broadcasting sobre os 864 neurônios simultaneamente.
1026
+
1027
+ Vantagens:
1028
+ 1. Zero IPC overhead (sem multiprocessing)
1029
+ 2. Broadcast de tensores — uma instrução matricial atualiza todos os
1030
+ pesos da hipergrade 4D simultaneamente
1031
+ 3. Compatível com AVX512_VNNI / AMX_INT8 via xeon_runtime
1032
+ 4. Memória eficiente: apenas 2 tensores temporários (dist, influence)
1033
+
1034
+ Args:
1035
+ samples: tensor (B, 4) — batch de amostras 4D.
1036
+ use_conscience: se True, aplica bias de consciência (default True).
1037
+ accumulate_fisher: se True, acumula Fisher info em w.
1038
+
1039
+ Returns:
1040
+ Dict com: n_processed, n_bmu_unique, dead_rate, alpha_t, sigma_t.
1041
+ """
1042
+ if not isinstance(samples, torch.Tensor):
1043
+ samples = torch.tensor(samples, dtype=torch.float)
1044
+ samples = samples.float()
1045
+ if samples.dim() == 1:
1046
+ samples = samples.unsqueeze(0)
1047
+ # Sanitiza NaN/Inf
1048
+ samples = torch.nan_to_num(samples, nan=0.0, posinf=1e4, neginf=-1e4)
1049
+
1050
+ n_processed = 0
1051
+ bmu_indices: List[Tuple[int, int, int, int]] = []
1052
+ grid_coords_flat = self._precompute_grid_coords_flat()
1053
+
1054
+ with torch.no_grad():
1055
+ for sample_idx in range(samples.shape[0]):
1056
+ x = samples[sample_idx] # (4,)
1057
+ # 1. PARALELISMO MATRICIAL: distância de 1 amostra vs TODOS os neurônios
1058
+ flat_weights = self.weights.view(-1, 4) # (N, 4)
1059
+ dist = torch.sum((flat_weights - x.unsqueeze(0)) ** 2, dim=-1) # (N,)
1060
+
1061
+ # 2. BMU selection (com conscience opcional)
1062
+ if use_conscience:
1063
+ win_count_flat = self.bmu_win_count.flatten().float()
1064
+ max_win = win_count_flat.max().clamp(min=1.0)
1065
+ p_norm = win_count_flat / max_win
1066
+ bias = self.conscience_gamma * (0.5 - p_norm)
1067
+ dead_mask = (win_count_flat == 0).float()
1068
+ dead_boost = self.conscience_gamma * 0.5 * dead_mask
1069
+ bias_flat = bias + dead_boost
1070
+ dist_eff = dist - bias_flat
1071
+ flat_idx = int(torch.argmin(dist_eff).item())
1072
+ else:
1073
+ flat_idx = int(torch.argmin(dist).item())
1074
+
1075
+ # Recupera coordenada 4D da BMU
1076
+ i = flat_idx // (self.J * self.K * self.L)
1077
+ rest = flat_idx % (self.J * self.K * self.L)
1078
+ j = rest // (self.K * self.L)
1079
+ rest = rest % (self.K * self.L)
1080
+ k = rest // self.L
1081
+ l = rest % self.L
1082
+ bmu_idx = (i, j, k, l)
1083
+ bmu_indices.append(bmu_idx)
1084
+
1085
+ # 3. Vizinhança 4D paralela — distância na grade
1086
+ bmu_coord = grid_coords_flat[flat_idx] # (4,)
1087
+ grid_dists_sq = torch.sum((grid_coords_flat - bmu_coord.unsqueeze(0)) ** 2, dim=-1) # (N,)
1088
+
1089
+ # 4. Hiperparâmetros com floors
1090
+ sigma = max(self.sigma0 * math.exp(-self.t / 1000), 0.1)
1091
+ alpha = max(self.alpha0 * math.exp(-self.t / 2000), 0.001)
1092
+
1093
+ # 5. Influência Gaussiana
1094
+ influence = torch.exp(-grid_dists_sq / (2 * sigma ** 2)) # (N,)
1095
+ influence = influence.view(self.I, self.J, self.K, self.L, 1) # (I,J,K,L,1)
1096
+
1097
+ # 6. Update em massa — uma operação vetorial
1098
+ delta = x.view(1, 1, 1, 1, 4) - self.weights # (I,J,K,L,4)
1099
+ update = alpha * influence * delta
1100
+
1101
+ # EWC penalty na 4ª dimensão
1102
+ if self.old_weights_w is not None and self.fisher_w is not None:
1103
+ ewc_penalty = self.lambda_ewc * self.fisher_w * (
1104
+ self.weights[..., 3] - self.old_weights_w
1105
+ )
1106
+ update[..., 3] = update[..., 3] - ewc_penalty
1107
+
1108
+ self.weights = self.weights + update
1109
+ self.weights = torch.clamp(self.weights, -100.0, 100.0)
1110
+ if torch.isnan(self.weights).any() or torch.isinf(self.weights).any():
1111
+ self.weights = torch.nan_to_num(
1112
+ self.weights, nan=0.0, posinf=100.0, neginf=-100.0
1113
+ )
1114
+
1115
+ # 7. Fisher accumulation
1116
+ if accumulate_fisher:
1117
+ influence_flat = influence.view(-1)
1118
+ mask = influence_flat > 0.1
1119
+ if mask.any():
1120
+ diff_sq = (x[3] - self.weights.view(-1, 4)[mask, 3]) ** 2
1121
+ self.fisher_accum.view(-1)[mask] += diff_sq
1122
+ self.fisher_count.view(-1)[mask] += 1
1123
+
1124
+ self.t += 1
1125
+ n_processed += 1
1126
+
1127
+ # 8. Atualiza conscience (win frequency)
1128
+ try:
1129
+ self.update_win_frequency(bmu_idx)
1130
+ except Exception:
1131
+ pass
1132
+
1133
+ # 9. Auto-ajuste de γ (throttled)
1134
+ try:
1135
+ if self.t % self._auto_adjust_interval == 0:
1136
+ self.auto_adjust_conscience_gamma()
1137
+ except Exception:
1138
+ pass
1139
+
1140
+ # Estatísticas pós-batch
1141
+ n_unique_bmus = len(set(bmu_indices))
1142
+ n_active = int((self.bmu_win_count > 0).sum().item())
1143
+ dead_rate = float(1.0 - n_active / max(1, self.n_neurons))
1144
+ sigma_t = max(self.sigma0 * math.exp(-self.t / 1000), 0.1)
1145
+ alpha_t = max(self.alpha0 * math.exp(-self.t / 2000), 0.001)
1146
+
1147
+ return {
1148
+ "n_processed": n_processed,
1149
+ "n_bmu_unique": n_unique_bmus,
1150
+ "dead_rate": dead_rate,
1151
+ "n_active": n_active,
1152
+ "n_total": self.n_neurons,
1153
+ "alpha_t": float(alpha_t),
1154
+ "sigma_t": float(sigma_t),
1155
+ "som_t": int(self.t),
1156
+ "conscience_gamma": float(self.conscience_gamma),
1157
+ "method": "vectorial_parallel_pytorch",
1158
+ }
1159
+
1160
+ # ==================================================================
1161
+ # V6.5-V2-reorganize — Density-based map reorganization
1162
+ # (ReorganizingSOM4D pattern)
1163
+ # User requirement: "Para reorganizar o mapa SOM dinamicamente durante o
1164
+ # treinamento, a técnica matemática mais robusta e paralelizável é a
1165
+ # Filtragem de Ativação por Convolução Baseada em Densidade. Em vez de
1166
+ # alterar a topologia de forma sequencial (o que quebraria o paralelismo),
1167
+ # nós calculamos a frequência de ativação de cada neurônio (densidade de
1168
+ # uso) diretamente na GPU/CPU e aplicamos um filtro gaussiano
1169
+ # multidimensional. Os neurônios pouco ativados (isolados) sofrem uma força
1170
+ # de atração em direção aos agrupamentos de alta densidade, reorganizando
1171
+ # geometricamente o hipercubo 4D sem perder o alinhamento matricial."
1172
+ #
1173
+ # Math:
1174
+ # density_i = bmu_win_count[i] / sum(bmu_win_count) ∈ [0, 1]
1175
+ # density_center = sum(weights * density, dim=0) (centro de massa ponderado)
1176
+ # pull_force_i = (1 - density_i) * regularization_factor
1177
+ # new_weights_i = weights_i + pull_force_i * (density_center - weights_i)
1178
+ #
1179
+ # Comportamento:
1180
+ # - Neurônios com alta densidade (muito ativos): pull_force ≈ 0 → ficam parados
1181
+ # - Neurônios com baixa densidade (mortos): pull_force ≈ 1 → puxados para o centro
1182
+ # - Centro de massa é dominado pelos neurônios ativos (ponderação por densidade)
1183
+ # ==================================================================
1184
+ def reorganize_map_by_density(
1185
+ self,
1186
+ regularization_factor: float = 0.05,
1187
+ min_dead_rate_for_trigger: float = 0.3,
1188
+ ) -> Dict[str, Any]:
1189
+ """V6.5-V2-reorganize — Reorganização topológica paralela baseada em densidade.
1190
+
1191
+ Implementa o padrão ReorganizingSOM4D: identifica zonas frias (neurônios
1192
+ mortos) via hit_map e as desloca matematicamente em direção ao centro
1193
+ de massa ponderado pelos neurônios ativos.
1194
+
1195
+ Vantagens sobre revive_dead_neurons puro:
1196
+ 1. Não zera bmu_win_count (preserva histórico de ativação)
1197
+ 2. Pulso suave e contínuo (não reinicialização abrupta)
1198
+ 3. Compatível com paralelismo matricial — uma única operação vetorial
1199
+ 4. Decaimento temporal: regularization_factor *= decay (early epochs
1200
+ reorganizam drasticamente, late epochs estabilizam)
1201
+ 5. Eliminação efetiva de neurônios mortos sem quebrar a topologia
1202
+
1203
+ Args:
1204
+ regularization_factor: magnitude do pull (default 0.05 — 5% do
1205
+ deslocamento em direção ao centro de massa).
1206
+ min_dead_rate_for_trigger: só reorganiza se dead_rate > threshold
1207
+ (default 0.3 = 30% mortos). Evita reorganização desnecessária.
1208
+
1209
+ Returns:
1210
+ Dict com: reorganized, dead_rate_before, dead_rate_after (estimado),
1211
+ n_dead_pulled, density_center_norm, regularization_factor.
1212
+ """
1213
+ with torch.no_grad():
1214
+ # Computa dead rate atual
1215
+ n_total = self.n_neurons
1216
+ n_active = int((self.bmu_win_count > 0).sum().item())
1217
+ dead_rate = float(1.0 - n_active / max(1, n_total))
1218
+
1219
+ if dead_rate < min_dead_rate_for_trigger:
1220
+ return {
1221
+ "reorganized": False,
1222
+ "reason": f"dead_rate={dead_rate:.3f} < threshold={min_dead_rate_for_trigger}",
1223
+ "dead_rate": dead_rate,
1224
+ "n_active": n_active,
1225
+ "n_total": n_total,
1226
+ }
1227
+
1228
+ # Normaliza hit_map para distribuição de densidade
1229
+ total_hits = float(self.bmu_win_count.sum().item())
1230
+ if total_hits < 1e-8:
1231
+ # Sem hits — não há centro de massa para puxar
1232
+ return {
1233
+ "reorganized": False,
1234
+ "reason": "no_hits_in_buffer",
1235
+ "dead_rate": dead_rate,
1236
+ "n_active": n_active,
1237
+ "n_total": n_total,
1238
+ }
1239
+
1240
+ density = self.bmu_win_count.float() / total_hits # (I, J, K, L)
1241
+
1242
+ # Planifica para operações matriciais
1243
+ flat_weights = self.weights.view(-1, 4) # (N, 4)
1244
+ flat_density = density.view(-1, 1) # (N, 1)
1245
+
1246
+ # Centro de massa ponderado por densidade (âncora global)
1247
+ density_center = torch.sum(flat_weights * flat_density, dim=0, keepdim=True) # (1, 4)
1248
+ density_center_norm = float(torch.norm(density_center).item())
1249
+
1250
+ # Decaimento temporal: reorganização forte em épocas iniciais,
1251
+ # estabiliza em épocas finais (decay = exp(-t / 5000))
1252
+ decay = float(math.exp(-self.t / 5000.0))
1253
+ effective_factor = float(regularization_factor * decay)
1254
+
1255
+ # Força de atração: neurônios com baixa densidade sofrem maior deslocamento
1256
+ pull_force = (1.0 - flat_density) * effective_factor # (N, 1)
1257
+
1258
+ # Aplica reorganização em uma operação vetorial
1259
+ new_flat_weights = flat_weights + pull_force * (density_center - flat_weights)
1260
+
1261
+ # Restaura shape original
1262
+ self.weights = new_flat_weights.view(self.I, self.J, self.K, self.L, 4).clone()
1263
+ self.weights = torch.clamp(self.weights, -100.0, 100.0)
1264
+
1265
+ # Sanitiza NaN/Inf
1266
+ if torch.isnan(self.weights).any() or torch.isinf(self.weights).any():
1267
+ self.weights = torch.nan_to_num(
1268
+ self.weights, nan=0.0, posinf=100.0, neginf=-100.0
1269
+ )
1270
+
1271
+ # Conta quantos neurônios mortos foram efetivamente deslocados
1272
+ n_dead_pulled = int((pull_force.view(-1) > 1e-6).sum().item())
1273
+
1274
+ return {
1275
+ "reorganized": True,
1276
+ "dead_rate_before": dead_rate,
1277
+ "n_dead_pulled": n_dead_pulled,
1278
+ "density_center_norm": density_center_norm,
1279
+ "regularization_factor": effective_factor,
1280
+ "decay": decay,
1281
+ "n_active": n_active,
1282
+ "n_total": n_total,
1283
+ "method": "density_based_reorganization",
1284
+ }
1285
+
1286
  def auto_adjust_conscience_gamma(self) -> Dict[str, Any]:
1287
  """V6.5-V2-auto-conscience-v2 — Ajusta γ dinamicamente conforme ativação.
1288
 
 
2112
  V6.5-V2-memfix — Wrapped in torch.no_grad() to prevent graph building
2113
  during SOM Hebbian updates. The SOM uses competitive learning (BMU +
2114
  Gaussian neighborhood), not backprop — no gradient is needed.
2115
+
2116
+ V6.5-V2-parallel — Agora usa train_batch_parallel (ParallelSOM4D pattern)
2117
+ ao invés do loop find_bmu + update_weights manual. Isto elimina o
2118
+ overhead de Python e usa broadcasting PyTorch sobre os 864 neurônios.
2119
+ User requirement: "Para um ajuste paralelo verdadeiro e massivo de uma
2120
+ SOM de 4 dimensões, a abordagem ideal é o paralelismo vetorial em GPU
2121
+ utilizando PyTorch."
2122
+
2123
+ V6.5-V2-reorganize — Após treinar, dispara reorganização por densidade
2124
+ se dead_rate > 30% (ReorganizingSOM4D pattern).
2125
  """
2126
  if not self.buffer_4d:
2127
  return
2128
  # V6.5-V2-memfix — detach+stack to ensure no graph leaks from buffer
2129
  data = torch.stack(self.buffer_4d).detach()
2130
  with torch.no_grad():
2131
+ # V6.5-V2-parallel — Usa train_batch_parallel para todas as 5 épocas
2132
+ # User requirement: paralelismo vetorial PyTorch sobre os 864 neurônios
2133
+ acc_fisher = (
2134
+ self.punishment_count == 0
2135
+ and self.som.old_weights_w is None
2136
+ )
2137
  for _ in range(5): # épocas de treino rápido
2138
  perm = torch.randperm(len(data))
2139
+ # train_batch_parallel já faz sanitização NaN/Inf + conscience
2140
+ # + Fisher accumulation em uma única chamada vetorial
2141
+ self.som.train_batch_parallel(
2142
+ samples=data[perm],
2143
+ use_conscience=True,
2144
+ accumulate_fisher=acc_fisher,
2145
+ )
2146
+
2147
+ # V6.5-V2-reorganize — Reorganização topológica por densidade
2148
+ # User requirement (ReorganizingSOM4D pattern): "Os neurônios pouco
2149
+ # ativados (isolados) sofrem uma força de atração em direção aos
2150
+ # agrupamentos de alta densidade, reorganizando geometricamente o
2151
+ # hipercubo 4D sem perder o alinhamento matricial."
2152
+ try:
2153
+ reorg = self.reorganize_map_if_needed(
2154
+ regularization_factor=0.05,
2155
+ min_dead_rate_for_trigger=0.3,
2156
+ min_steps_between_reorganizations=300,
2157
+ )
2158
+ if reorg.get("action") == "auto_reorganized":
2159
+ import logging as _logging
2160
+ _log = _logging.getLogger("kohonen_learning_system")
2161
+ _log.info(
2162
+ f"[V6.5-V2-reorganize] Map reorganized: "
2163
+ f"n_dead_pulled={reorg.get('n_dead_pulled')}, "
2164
+ f"dead_rate={reorg.get('dead_rate_before', 0):.3f}, "
2165
+ f"factor={reorg.get('regularization_factor', 0):.4f}"
2166
  )
2167
+ except Exception:
2168
+ pass
2169
 
2170
  # V6.5-V2-metrics-FIX-3 — VQ-VAE-2 e W8A8 compressão dentro de
2171
  # no_grad para evitar construção de grafo computacional (50-200MB
 
4450
  """
4451
  return self.som.parallel_neuron_activation_report()
4452
 
4453
+ # ==================================================================
4454
+ # V6.5-V2-parallel — Wrappers para novos métodos parallel SOM
4455
+ # ==================================================================
4456
+ def train_som_batch_parallel(
4457
+ self,
4458
+ samples: torch.Tensor,
4459
+ use_conscience: bool = True,
4460
+ accumulate_fisher: bool = False,
4461
+ ) -> Dict[str, Any]:
4462
+ """V6.5-V2-parallel — Wrapper KLS para som.train_batch_parallel.
4463
+
4464
+ User requirement (ParallelSOM4D pattern): usa paralelismo vetorial
4465
+ PyTorch ao invés de loops Python + multiprocessing. Todas as operações
4466
+ internas (distância, BMU, vizinhança, update) são feitas via
4467
+ broadcasting sobre os 864 neurônios simultaneamente.
4468
+
4469
+ Args:
4470
+ samples: tensor (B, 4) — batch de amostras 4D.
4471
+ use_conscience: se True, aplica bias de consciência.
4472
+ accumulate_fisher: se True, acumula Fisher info em w.
4473
+
4474
+ Returns:
4475
+ Dict com: n_processed, n_bmu_unique, dead_rate, alpha_t, sigma_t.
4476
+ """
4477
+ return self.som.train_batch_parallel(
4478
+ samples=samples,
4479
+ use_conscience=use_conscience,
4480
+ accumulate_fisher=accumulate_fisher,
4481
+ )
4482
+
4483
+ def reorganize_som_map_by_density(
4484
+ self,
4485
+ regularization_factor: float = 0.05,
4486
+ min_dead_rate_for_trigger: float = 0.3,
4487
+ ) -> Dict[str, Any]:
4488
+ """V6.5-V2-reorganize — Wrapper KLS para som.reorganize_map_by_density.
4489
+
4490
+ User requirement (ReorganizingSOM4D pattern): reorganiza topologia do
4491
+ SOM dinamicamente durante o treinamento via filtragem de ativação por
4492
+ densidade. Neurônios pouco ativados (isolados) sofrem força de atração
4493
+ em direção aos agrupamentos de alta densidade, reorganizando
4494
+ geometricamente o hipercubo 4D sem perder o alinhamento matricial.
4495
+
4496
+ Args:
4497
+ regularization_factor: magnitude do pull (default 0.05).
4498
+ min_dead_rate_for_trigger: só reorganiza se dead_rate > threshold.
4499
+
4500
+ Returns:
4501
+ Dict com: reorganized, dead_rate_before, n_dead_pulled, etc.
4502
+ """
4503
+ return self.som.reorganize_map_by_density(
4504
+ regularization_factor=regularization_factor,
4505
+ min_dead_rate_for_trigger=min_dead_rate_for_trigger,
4506
+ )
4507
+
4508
+ def reorganize_map_if_needed(
4509
+ self,
4510
+ regularization_factor: float = 0.05,
4511
+ min_dead_rate_for_trigger: float = 0.3,
4512
+ min_steps_between_reorganizations: int = 300,
4513
+ ) -> Dict[str, Any]:
4514
+ """V6.5-V2-reorganize — Auto-dispara reorganização por densidade se necessário.
4515
+
4516
+ Verifica o dead_rate atual e reorganiza o mapa SOM se:
4517
+ - dead_rate > min_dead_rate_for_trigger (default 0.3 = 30% mortos)
4518
+ - pelo menos min_steps_between_reorganizations desde a última
4519
+ reorganização (cooldown)
4520
+
4521
+ Complementa auto_revive_if_needed: enquanto auto_revive faz
4522
+ reinicialização abrupta (k-means++ selection), reorganize_map faz
4523
+ pulso suave e contínuo (gravitational pull). Ambos podem coexistir.
4524
+
4525
+ Args:
4526
+ regularization_factor: magnitude do pull (default 0.05).
4527
+ min_dead_rate_for_trigger: limite para disparar (default 0.3).
4528
+ min_steps_between_reorganizations: cooldown em steps (default 300).
4529
+
4530
+ Returns:
4531
+ Dict com status da reorganização (ou action="skipped" se não disparou).
4532
+ """
4533
+ last_reorg_step = getattr(
4534
+ self, "_last_reorganize_step", -min_steps_between_reorganizations
4535
+ )
4536
+ current_step = self.som.t
4537
+ steps_since_last = current_step - last_reorg_step
4538
+
4539
+ # Computa dead rate atual
4540
+ n_total = self.som.n_neurons
4541
+ n_active = int((self.som.bmu_win_count > 0).sum().item())
4542
+ dead_rate = float(1.0 - n_active / max(1, n_total))
4543
+
4544
+ if dead_rate <= min_dead_rate_for_trigger:
4545
+ return {
4546
+ "action": "skipped",
4547
+ "reason": f"dead_rate={dead_rate:.3f} <= threshold={min_dead_rate_for_trigger}",
4548
+ "dead_rate": dead_rate,
4549
+ "n_active": n_active,
4550
+ "n_total": n_total,
4551
+ }
4552
+ if steps_since_last < min_steps_between_reorganizations:
4553
+ return {
4554
+ "action": "skipped",
4555
+ "reason": (
4556
+ f"cooldown: only {steps_since_last} steps since last reorg "
4557
+ f"(need {min_steps_between_reorganizations})"
4558
+ ),
4559
+ "dead_rate": dead_rate,
4560
+ "n_active": n_active,
4561
+ "n_total": n_total,
4562
+ }
4563
+
4564
+ # Dispara reorganização
4565
+ reorg = self.reorganize_som_map_by_density(
4566
+ regularization_factor=regularization_factor,
4567
+ min_dead_rate_for_trigger=min_dead_rate_for_trigger,
4568
+ )
4569
+ self._last_reorganize_step = current_step
4570
+ reorg["action"] = "auto_reorganized"
4571
+ reorg["trigger_dead_rate"] = dead_rate
4572
+ reorg["steps_since_last_reorg"] = steps_since_last
4573
+ return reorg
4574
+
4575
  def auto_revive_if_needed(
4576
  self,
4577
  dead_rate_threshold: float = 0.5,