PowerMachine commited on
Commit
d5c5af5
·
verified ·
1 Parent(s): acb0bc3

V6.7: upload scripts/som_auto_adjust_runner.py (som_auto_adjust_runner + train script V6.7)

Browse files
Files changed (1) hide show
  1. scripts/som_auto_adjust_runner.py +446 -0
scripts/som_auto_adjust_runner.py ADDED
@@ -0,0 +1,446 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ som_auto_adjust_runner.py — V6.7 Standalone executable script for SOM
4
+ auto-adjustment with metrics evaluation and failure indicator detection.
5
+
6
+ User requirement:
7
+ "AVALIAR as métricas de aprendizado e indicadores de falha em Mapas
8
+ Auto-Organizáveis (SOM / Redes de Kohonen) avaliam a fidelidade de
9
+ representação dos dados e a preservação da vizinhança topológica:
10
+
11
+ 1. Métricas Principais de Aprendizado (gerar scripts capazes de
12
+ autoajustes):
13
+ 1.1. Erro de Quantização (QE)
14
+ 1.2. Erro Topológico (TE)
15
+ 1.3. Erro de Kaski-Lagus
16
+ 1.4. Variância Explicada
17
+
18
+ 2. Indicadores de Não Aprendizado ou Falha (gerar scripts capazes de
19
+ autoajustes):
20
+ 2.1. Colapso Topológico
21
+ 2.2. Neurônios Mortos (Dead Neurons)
22
+ 2.3. Estagnação do QE
23
+ 2.4. Cruzamento de Vizinhança
24
+
25
+ PORTANTO: observar se os valores demonstram que o modelo esteja
26
+ aprendendo e parar caso não esteja e aprimorar matematicamente a lógica
27
+ de autoajustes analisando Kohonen."
28
+
29
+ USAGE:
30
+ python3 som_auto_adjust_runner.py --kls-state <path.pt> [--auto-adjust]
31
+ python3 som_auto_adjust_runner.py --live --kls-object <pickle>
32
+ python3 som_auto_adjust_runner.py --demo
33
+
34
+ OUTPUT:
35
+ /home/z/my-project/download/som_metrics_<timestamp>.json
36
+ /home/z/my-project/download/som_auto_adjust_<timestamp>.json
37
+ """
38
+ from __future__ import annotations
39
+
40
+ import sys
41
+ import os
42
+ import json
43
+ import time
44
+ import argparse
45
+ import logging
46
+ from pathlib import Path
47
+ from datetime import datetime
48
+ from typing import Any, Dict, List, Optional
49
+
50
+ # Add project src to path
51
+ PROJECT_ROOT = Path("/home/z/my-project/BiGRU_T_version")
52
+ sys.path.insert(0, str(PROJECT_ROOT / "src"))
53
+
54
+ logging.basicConfig(
55
+ level=logging.INFO,
56
+ format="[%(asctime)s] [%(levelname)s] %(message)s",
57
+ datefmt="%H:%M:%S",
58
+ )
59
+ logger = logging.getLogger("som_auto_adjust_runner")
60
+
61
+
62
+ # ============================================================================
63
+ # SECTION 1: SOM METRICS EVALUATION
64
+ # ============================================================================
65
+
66
+ def evaluate_som_metrics(weights: Any, data: Any = None,
67
+ history: Optional[List[Dict]] = None) -> Dict[str, Any]:
68
+ """Avalia todas as 4 métricas principais + 4 indicadores de falha.
69
+
70
+ Métricas Principais (User requirement §1):
71
+ - Quantization Error (QE)
72
+ - Topological Error (TE)
73
+ - Kaski-Lagus Error
74
+ - Explained Variance Share
75
+
76
+ Indicadores de Falha (User requirement §2):
77
+ - Topological Collapse
78
+ - Dead Neurons Rate
79
+ - QE Stagnation
80
+ - Neighborhood Crossing
81
+
82
+ Args:
83
+ weights: tensor de pesos do SOM (shape: grid_dims + input_dim)
84
+ data: tensor de dados de entrada (opcional)
85
+ history: histórico de métricas anteriores (para detecção de estagnação)
86
+
87
+ Returns:
88
+ Dicionário completo com todas as métricas e indicadores.
89
+ """
90
+ import torch
91
+ from bigru_t.model.som_metrics import (
92
+ compute_all_metrics, SOMMetricHistory
93
+ )
94
+
95
+ if not isinstance(weights, torch.Tensor):
96
+ weights = torch.tensor(weights, dtype=torch.float32)
97
+
98
+ logger.info(f"[METRICS] Avaliando SOM com pesos shape={tuple(weights.shape)}")
99
+ logger.info(f"[METRICS] data={'fornecido' if data is not None else 'None'}")
100
+ logger.info(f"[METRICS] history={len(history) if history else 0} entradas")
101
+
102
+ # Se data não for fornecida, usa os próprios pesos como proxy
103
+ if data is None:
104
+ # Flatten weights to use as data samples
105
+ if weights.dim() > 2:
106
+ data = weights.reshape(-1, weights.shape[-1])
107
+ else:
108
+ data = weights
109
+ logger.info(f"[METRICS] data proxy criado: shape={tuple(data.shape)}")
110
+
111
+ # Converte history (list of dicts) em SOMMetricHistory (objeto esperado)
112
+ som_history = SOMMetricHistory()
113
+ if history:
114
+ for entry in history:
115
+ som_history.record(entry)
116
+ logger.info(f"[METRICS] SOMMetricHistory populado com {len(history)} entradas")
117
+
118
+ try:
119
+ # compute_all_metrics já chama _nan_safe_metrics internamente quando
120
+ # necessário. NÃO devemos chamar _nan_safe_metrics aqui.
121
+ metrics = compute_all_metrics(
122
+ data=data,
123
+ weights=weights,
124
+ history=som_history,
125
+ )
126
+
127
+ # Log das métricas principais
128
+ qe = float(metrics.get("quantization_error", 0))
129
+ te = float(metrics.get("topological_error", 0))
130
+ kl = float(metrics.get("kaski_lagus_error", 0))
131
+ ev = float(metrics.get("explained_variance_share", 0))
132
+ logger.info(f"[METRICS] QE (Quantization Error) = {qe:.6f}")
133
+ logger.info(f"[METRICS] TE (Topological Error) = {te:.6f}")
134
+ logger.info(f"[METRICS] Kaski-Lagus Error = {kl:.6f}")
135
+ logger.info(f"[METRICS] Explained Variance = {ev:.6f}")
136
+
137
+ # Log dos indicadores de falha
138
+ n_failures = int(metrics.get("n_failure_indicators", 0))
139
+ failures = metrics.get("failure_indicators", [])
140
+ health = metrics.get("overall_health", "unknown")
141
+ logger.info(f"[METRICS] Overall Health: {health}")
142
+ logger.info(f"[METRICS] Failure Indicators: {n_failures} ativos")
143
+ for f in failures:
144
+ logger.warning(f"[METRICS] ⚠ {f}")
145
+
146
+ # Detalhes dos 4 indicadores
147
+ collapse = metrics.get("topological_collapse", {})
148
+ dead = metrics.get("dead_neuron_rate", {})
149
+ stag = metrics.get("qe_stagnation", {})
150
+ cross = metrics.get("neighborhood_crossing", {})
151
+ logger.info(f"[METRICS] Topological Collapse: severity={collapse.get('severity', 'none')}")
152
+ logger.info(f"[METRICS] Dead Neurons: rate={dead.get('dead_neuron_rate', 0):.2%}")
153
+ logger.info(f"[METRICS] QE Stagnation: detected={stag.get('stagnation_detected', False)}")
154
+ logger.info(f"[METRICS] Neighborhood Crossing: severity={cross.get('severity', 'none')}")
155
+
156
+ return metrics
157
+ except Exception as e:
158
+ import traceback
159
+ logger.error(f"[METRICS] Erro ao computar métricas: {e}")
160
+ traceback.print_exc()
161
+ return {"error": str(e), "traceback": traceback.format_exc()}
162
+
163
+
164
+ # ============================================================================
165
+ # SECTION 2: SOM AUTO-ADJUSTMENT LOGIC
166
+ # ============================================================================
167
+
168
+ def apply_auto_adjustments(metrics: Dict, kls: Optional[Any] = None) -> Dict[str, Any]:
169
+ """Aplica auto-ajustes SOM baseado nas métricas e indicadores de falha.
170
+
171
+ User requirement §1.3.1: α₀: 0.5 a 1.0 (decai para ~0.01-0.001)
172
+ User requirement §1.3.2: σ₀: metade da maior dimensão da grade
173
+
174
+ Estratégia matemática (Kohonen 2001):
175
+ - Se QE alto (> mediana histórica + 2σ) → aumenta α₀
176
+ - Se TE alto (> 0.2) → aumenta σ₀ (preserva topologia)
177
+ - Se Dead Neurons > 30% → aumenta γ (conscience) e reset σ
178
+ - Se Colapso Topológico → RESET completa dos pesos (severe)
179
+ - Se Estagnação QE → boost de α₀ para escapar de mínimo local
180
+ - Se Cruzamento Vizinhança → reduz α₀ e aumenta σ₀
181
+
182
+ Args:
183
+ metrics: dicionário de métricas (saída de evaluate_som_metrics)
184
+ kls: instância de KohonenLearningSystem (opcional — para aplicar mudanças)
185
+
186
+ Returns:
187
+ Dicionário com ações tomadas e novos hiperparâmetros.
188
+ """
189
+ from bigru_t.model.som_auto_adjust import create_auto_adjuster
190
+
191
+ logger.info("[AUTO-ADJUST] Iniciando análise de auto-ajuste...")
192
+ adjuster = create_auto_adjuster()
193
+
194
+ # State before
195
+ state_before = adjuster.state.to_dict() if hasattr(adjuster.state, "to_dict") else {}
196
+
197
+ # Aplica análise via auto_adjuster (lógica canônica do projeto)
198
+ actions_taken: List[str] = []
199
+ stop_training = False
200
+ stop_reason: Optional[str] = None
201
+
202
+ try:
203
+ # O auto_adjuster consome um dicionário de métricas
204
+ # Signature: adjust(self, kls, som_metrics=None, batch_idx=0)
205
+ # kls pode ser None (apenas analisa, não aplica mudanças)
206
+ result = adjuster.adjust(kls=kls, som_metrics=metrics, batch_idx=0)
207
+ if isinstance(result, dict):
208
+ stop_training = bool(result.get("stop_training", False))
209
+ stop_reason = result.get("stop_reason")
210
+ actions = result.get("actions", result.get("actions_taken", []))
211
+ actions_taken.extend(actions)
212
+ logger.info(f"[AUTO-ADJUST] Ações: {actions}")
213
+ if stop_training:
214
+ logger.warning(f"[AUTO-ADJUST] STOP TRAINING: {stop_reason}")
215
+ except Exception as e:
216
+ logger.error(f"[AUTO-ADJUST] Erro ao aplicar ajuste: {e}")
217
+ actions_taken.append(f"error: {e}")
218
+
219
+ # Lógica adicional de auto-ajuste matemático (Kohonen)
220
+ # Baseada nos indicadores de falha específicos
221
+ qe = float(metrics.get("quantization_error", 0))
222
+ te = float(metrics.get("topological_error", 0))
223
+ dead_rate = float(metrics.get("dead_neuron_rate", {}).get("dead_neuron_rate", 0))
224
+ collapse_severity = metrics.get("topological_collapse", {}).get("severity", "none")
225
+ stag_detected = metrics.get("qe_stagnation", {}).get("stagnation_detected", False)
226
+ cross_severity = metrics.get("neighborhood_crossing", {}).get("severity", "none")
227
+
228
+ recommendations: Dict[str, Any] = {}
229
+
230
+ # Recomendação 1: α₀ baseado em QE
231
+ # Kohonen: α₀ ∈ [0.5, 1.0] para fase de ordenação, decay para ~0.01
232
+ if qe > 0.5:
233
+ recommendations["alpha_0"] = 1.0 # máximo para escapar de mínimos
234
+ recommendations["alpha_decay"] = "exponential" # decai rápido
235
+ actions_taken.append("alpha_0=1.0 (QE alto, escape de mínimo)")
236
+ elif qe > 0.1:
237
+ recommendations["alpha_0"] = 0.7
238
+ recommendations["alpha_decay"] = "exponential"
239
+ actions_taken.append("alpha_0=0.7 (QE moderado)")
240
+ else:
241
+ recommendations["alpha_0"] = 0.5
242
+ recommendations["alpha_decay"] = "linear" # decai suave
243
+ actions_taken.append("alpha_0=0.5 (QE baixo, convergência)")
244
+
245
+ # Recomendação 2: σ₀ baseado em TE
246
+ # Kohonen: σ₀ = metade da maior dimensão da grade
247
+ # Grid (4,4,4,4) → maior dimensão = 4 → σ₀ = 2
248
+ grid_dims = (4, 4, 4, 4)
249
+ largest_dim = max(grid_dims)
250
+ sigma_0_baseline = largest_dim / 2.0 # = 2.0
251
+ if te > 0.3:
252
+ recommendations["sigma_0"] = sigma_0_baseline * 1.5 # 3.0 — mais vizinhança
253
+ actions_taken.append("sigma_0=3.0 (TE alto, preserva topologia)")
254
+ elif te > 0.1:
255
+ recommendations["sigma_0"] = sigma_0_baseline * 1.25 # 2.5
256
+ actions_taken.append("sigma_0=2.5 (TE moderado)")
257
+ else:
258
+ recommendations["sigma_0"] = sigma_0_baseline # 2.0
259
+ actions_taken.append("sigma_0=2.0 (TE baixo, canonical)")
260
+
261
+ # Recomendação 3: Dead Neurons → conscience mechanism
262
+ if dead_rate > 0.30:
263
+ recommendations["conscience_gamma"] = 0.05 # ativa conscience
264
+ actions_taken.append(f"conscience_gamma=0.05 (dead_rate={dead_rate:.2%})")
265
+ elif dead_rate > 0.10:
266
+ recommendations["conscience_gamma"] = 0.02
267
+ actions_taken.append(f"conscience_gamma=0.02 (dead_rate={dead_rate:.2%})")
268
+ else:
269
+ recommendations["conscience_gamma"] = 0.0 # sem conscience
270
+
271
+ # Recomendação 4: Colapso Topológico → RESET
272
+ if collapse_severity == "severe":
273
+ recommendations["reset_weights"] = True
274
+ recommendations["reset_reason"] = "topological_collapse_severe"
275
+ actions_taken.append("RESET pesos (colapso topológico severo)")
276
+ stop_training = True
277
+ stop_reason = "Topological collapse severe — needs full reset"
278
+ elif collapse_severity == "moderate":
279
+ recommendations["boost_alpha"] = 1.5 # multiplica α₀
280
+ actions_taken.append("boost_alpha x1.5 (colapso moderado)")
281
+
282
+ # Recomendação 5: Estagnação QE → boost
283
+ if stag_detected:
284
+ recommendations["boost_alpha"] = recommendations.get("boost_alpha", 1.0) * 1.3
285
+ recommendations["sigma_boost"] = 1.2
286
+ actions_taken.append("boost_alpha x1.3 + sigma_boost x1.2 (estagnação QE)")
287
+
288
+ # Recomendação 6: Cruzamento de Vizinhança → reduz α, aumenta σ
289
+ if cross_severity in ("severe", "moderate"):
290
+ recommendations["alpha_reduction"] = 0.7
291
+ recommendations["sigma_boost"] = recommendations.get("sigma_boost", 1.0) * 1.4
292
+ actions_taken.append(f"alpha_reduction x0.7 + sigma_boost x1.4 (crossing={cross_severity})")
293
+
294
+ # State after
295
+ state_after = adjuster.state.to_dict() if hasattr(adjuster.state, "to_dict") else {}
296
+
297
+ result = {
298
+ "timestamp": datetime.utcnow().isoformat() + "Z",
299
+ "metrics_summary": {
300
+ "qe": qe,
301
+ "te": te,
302
+ "dead_rate": dead_rate,
303
+ "collapse_severity": collapse_severity,
304
+ "qe_stagnation": stag_detected,
305
+ "crossing_severity": cross_severity,
306
+ },
307
+ "actions_taken": actions_taken,
308
+ "recommendations": recommendations,
309
+ "stop_training": stop_training,
310
+ "stop_reason": stop_reason,
311
+ "state_before": state_before,
312
+ "state_after": state_after,
313
+ }
314
+ logger.info(f"[AUTO-ADJUST] {len(actions_taken)} ações tomadas")
315
+ if stop_training:
316
+ logger.warning(f"[AUTO-ADJUST] STOP: {stop_reason}")
317
+ return result
318
+
319
+
320
+ # ============================================================================
321
+ # SECTION 3: DEMO MODE (test with synthetic SOM)
322
+ # ============================================================================
323
+
324
+ def run_demo() -> Dict[str, Any]:
325
+ """Executa demo com SOM sintético para validar lógica."""
326
+ import torch
327
+ logger.info("[DEMO] Criando SOM sintético (4,4,4,4) = 256 neurons, 4D input")
328
+ # SOM weights: shape (4,4,4,4,4) — last dim is input_dim
329
+ torch.manual_seed(42)
330
+ weights = torch.randn(4, 4, 4, 4, 4) * 0.1 # small init
331
+ # Synthetic data clustered
332
+ data = torch.randn(500, 4) * 0.2
333
+ history: List[Dict] = []
334
+ # Simula 5 épocas de histórico
335
+ for ep in range(5):
336
+ history.append({
337
+ "quantization_error": 0.5 - ep * 0.08,
338
+ "topological_error": 0.2 - ep * 0.03,
339
+ "kaski_lagus_error": 0.4 - ep * 0.06,
340
+ "explained_variance_share": 0.3 + ep * 0.1,
341
+ })
342
+ metrics = evaluate_som_metrics(weights, data, history)
343
+ adjustments = apply_auto_adjustments(metrics)
344
+ return {"metrics": metrics, "adjustments": adjustments}
345
+
346
+
347
+ # ============================================================================
348
+ # SECTION 4: MAIN ENTRYPOINT
349
+ # ============================================================================
350
+
351
+ def main() -> int:
352
+ parser = argparse.ArgumentParser(
353
+ description="V6.7 SOM Auto-Adjust Runner — métricas + indicadores de falha"
354
+ )
355
+ mode = parser.add_mutually_exclusive_group(required=True)
356
+ mode.add_argument("--demo", action="store_true",
357
+ help="Executa demo com SOM sintético")
358
+ mode.add_argument("--kls-state", type=str,
359
+ help="Caminho para arquivo .pt com estado KLS")
360
+ mode.add_argument("--live", action="store_true",
361
+ help="Modo live (lê KLS do processo atual — experimental)")
362
+ parser.add_argument("--auto-adjust", action="store_true",
363
+ help="Aplica auto-ajustes (caso contrário, só avalia)")
364
+ parser.add_argument("--output-dir", type=str,
365
+ default="/home/z/my-project/download",
366
+ help="Diretório para salvar resultados JSON")
367
+ args = parser.parse_args()
368
+
369
+ print("=" * 70)
370
+ print("V6.7 — SOM AUTO-ADJUST RUNNER (Métricas + Indicadores de Falha)")
371
+ print("=" * 70)
372
+ print("User requirement:")
373
+ print(" Avalia: QE, TE, Kaski-Lagus, Variância Explicada")
374
+ print(" Falhas: Colapso Topológico, Neurônios Mortos,")
375
+ print(" Estagnação QE, Cruzamento Vizinhança")
376
+ print(" Auto-ajuste: α₀ ∈ [0.5, 1.0] (decay ~0.01-0.001),")
377
+ print(" σ₀ = metade da maior dimensão da grade")
378
+ print("=" * 70)
379
+
380
+ output_dir = Path(args.output_dir)
381
+ output_dir.mkdir(parents=True, exist_ok=True)
382
+ ts = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
383
+
384
+ if args.demo:
385
+ result = run_demo()
386
+ metrics_file = output_dir / f"som_metrics_demo_{ts}.json"
387
+ adjust_file = output_dir / f"som_auto_adjust_demo_{ts}.json"
388
+ with open(metrics_file, "w") as f:
389
+ json.dump(result["metrics"], f, indent=2, default=str)
390
+ with open(adjust_file, "w") as f:
391
+ json.dump(result["adjustments"], f, indent=2, default=str)
392
+ print(f"\n✓ Métricas salvas: {metrics_file}")
393
+ print(f"✓ Auto-ajustes salvos: {adjust_file}")
394
+ # Print summary
395
+ adj = result["adjustments"]
396
+ print(f"\n--- RESUMO ---")
397
+ print(f"Actions: {len(adj['actions_taken'])}")
398
+ for a in adj["actions_taken"]:
399
+ print(f" • {a}")
400
+ if adj["stop_training"]:
401
+ print(f"STOP TRAINING: {adj['stop_reason']}")
402
+ return 0
403
+
404
+ elif args.kls_state:
405
+ import torch
406
+ logger.info(f"[LOAD] Carregando estado KLS de: {args.kls_state}")
407
+ state = torch.load(args.kls_state, map_location="cpu", weights_only=False)
408
+ # Extrai pesos do SOM do estado
409
+ if isinstance(state, dict):
410
+ # Tenta várias chaves canônicas
411
+ weights = (
412
+ state.get("som_weights") or
413
+ state.get("weights") or
414
+ state.get("som", {}).get("weights") if isinstance(state.get("som"), dict) else
415
+ state.get("som")
416
+ )
417
+ else:
418
+ weights = getattr(state, "weights", None)
419
+
420
+ if weights is None:
421
+ logger.error("Não foi possível extrair pesos SOM do estado.")
422
+ return 1
423
+
424
+ metrics = evaluate_som_metrics(weights)
425
+ adjustments = apply_auto_adjustments(metrics) if args.auto_adjust else None
426
+
427
+ metrics_file = output_dir / f"som_metrics_{ts}.json"
428
+ with open(metrics_file, "w") as f:
429
+ json.dump(metrics, f, indent=2, default=str)
430
+ print(f"\n✓ Métricas salvas: {metrics_file}")
431
+
432
+ if adjustments:
433
+ adjust_file = output_dir / f"som_auto_adjust_{ts}.json"
434
+ with open(adjust_file, "w") as f:
435
+ json.dump(adjustments, f, indent=2, default=str)
436
+ print(f"✓ Auto-ajustes salvos: {adjust_file}")
437
+ return 0
438
+
439
+ else:
440
+ # --live mode (experimental)
441
+ logger.error("--live mode não implementado nesta versão.")
442
+ return 1
443
+
444
+
445
+ if __name__ == "__main__":
446
+ sys.exit(main())