File size: 18,774 Bytes
d5c5af5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
#!/usr/bin/env python3
"""
som_auto_adjust_runner.py — V6.7 Standalone executable script for SOM
auto-adjustment with metrics evaluation and failure indicator detection.

User requirement:
    "AVALIAR as métricas de aprendizado e indicadores de falha em Mapas
    Auto-Organizáveis (SOM / Redes de Kohonen) avaliam a fidelidade de
    representação dos dados e a preservação da vizinhança topológica:

    1. Métricas Principais de Aprendizado (gerar scripts capazes de
       autoajustes):
       1.1. Erro de Quantização (QE)
       1.2. Erro Topológico (TE)
       1.3. Erro de Kaski-Lagus
       1.4. Variância Explicada

    2. Indicadores de Não Aprendizado ou Falha (gerar scripts capazes de
       autoajustes):
       2.1. Colapso Topológico
       2.2. Neurônios Mortos (Dead Neurons)
       2.3. Estagnação do QE
       2.4. Cruzamento de Vizinhança

    PORTANTO: observar se os valores demonstram que o modelo esteja
    aprendendo e parar caso não esteja e aprimorar matematicamente a lógica
    de autoajustes analisando Kohonen."

USAGE:
    python3 som_auto_adjust_runner.py --kls-state <path.pt> [--auto-adjust]
    python3 som_auto_adjust_runner.py --live --kls-object <pickle>
    python3 som_auto_adjust_runner.py --demo

OUTPUT:
    /home/z/my-project/download/som_metrics_<timestamp>.json
    /home/z/my-project/download/som_auto_adjust_<timestamp>.json
"""
from __future__ import annotations

import sys
import os
import json
import time
import argparse
import logging
from pathlib import Path
from datetime import datetime
from typing import Any, Dict, List, Optional

# Add project src to path
PROJECT_ROOT = Path("/home/z/my-project/BiGRU_T_version")
sys.path.insert(0, str(PROJECT_ROOT / "src"))

logging.basicConfig(
    level=logging.INFO,
    format="[%(asctime)s] [%(levelname)s] %(message)s",
    datefmt="%H:%M:%S",
)
logger = logging.getLogger("som_auto_adjust_runner")


# ============================================================================
# SECTION 1: SOM METRICS EVALUATION
# ============================================================================

def evaluate_som_metrics(weights: Any, data: Any = None,
                          history: Optional[List[Dict]] = None) -> Dict[str, Any]:
    """Avalia todas as 4 métricas principais + 4 indicadores de falha.

    Métricas Principais (User requirement §1):
        - Quantization Error (QE)
        - Topological Error (TE)
        - Kaski-Lagus Error
        - Explained Variance Share

    Indicadores de Falha (User requirement §2):
        - Topological Collapse
        - Dead Neurons Rate
        - QE Stagnation
        - Neighborhood Crossing

    Args:
        weights: tensor de pesos do SOM (shape: grid_dims + input_dim)
        data: tensor de dados de entrada (opcional)
        history: histórico de métricas anteriores (para detecção de estagnação)

    Returns:
        Dicionário completo com todas as métricas e indicadores.
    """
    import torch
    from bigru_t.model.som_metrics import (
        compute_all_metrics, SOMMetricHistory
    )

    if not isinstance(weights, torch.Tensor):
        weights = torch.tensor(weights, dtype=torch.float32)

    logger.info(f"[METRICS] Avaliando SOM com pesos shape={tuple(weights.shape)}")
    logger.info(f"[METRICS] data={'fornecido' if data is not None else 'None'}")
    logger.info(f"[METRICS] history={len(history) if history else 0} entradas")

    # Se data não for fornecida, usa os próprios pesos como proxy
    if data is None:
        # Flatten weights to use as data samples
        if weights.dim() > 2:
            data = weights.reshape(-1, weights.shape[-1])
        else:
            data = weights
        logger.info(f"[METRICS] data proxy criado: shape={tuple(data.shape)}")

    # Converte history (list of dicts) em SOMMetricHistory (objeto esperado)
    som_history = SOMMetricHistory()
    if history:
        for entry in history:
            som_history.record(entry)
        logger.info(f"[METRICS] SOMMetricHistory populado com {len(history)} entradas")

    try:
        # compute_all_metrics já chama _nan_safe_metrics internamente quando
        # necessário. NÃO devemos chamar _nan_safe_metrics aqui.
        metrics = compute_all_metrics(
            data=data,
            weights=weights,
            history=som_history,
        )

        # Log das métricas principais
        qe = float(metrics.get("quantization_error", 0))
        te = float(metrics.get("topological_error", 0))
        kl = float(metrics.get("kaski_lagus_error", 0))
        ev = float(metrics.get("explained_variance_share", 0))
        logger.info(f"[METRICS] QE (Quantization Error) = {qe:.6f}")
        logger.info(f"[METRICS] TE (Topological Error)  = {te:.6f}")
        logger.info(f"[METRICS] Kaski-Lagus Error       = {kl:.6f}")
        logger.info(f"[METRICS] Explained Variance       = {ev:.6f}")

        # Log dos indicadores de falha
        n_failures = int(metrics.get("n_failure_indicators", 0))
        failures = metrics.get("failure_indicators", [])
        health = metrics.get("overall_health", "unknown")
        logger.info(f"[METRICS] Overall Health: {health}")
        logger.info(f"[METRICS] Failure Indicators: {n_failures} ativos")
        for f in failures:
            logger.warning(f"[METRICS]   ⚠ {f}")

        # Detalhes dos 4 indicadores
        collapse = metrics.get("topological_collapse", {})
        dead = metrics.get("dead_neuron_rate", {})
        stag = metrics.get("qe_stagnation", {})
        cross = metrics.get("neighborhood_crossing", {})
        logger.info(f"[METRICS] Topological Collapse: severity={collapse.get('severity', 'none')}")
        logger.info(f"[METRICS] Dead Neurons: rate={dead.get('dead_neuron_rate', 0):.2%}")
        logger.info(f"[METRICS] QE Stagnation: detected={stag.get('stagnation_detected', False)}")
        logger.info(f"[METRICS] Neighborhood Crossing: severity={cross.get('severity', 'none')}")

        return metrics
    except Exception as e:
        import traceback
        logger.error(f"[METRICS] Erro ao computar métricas: {e}")
        traceback.print_exc()
        return {"error": str(e), "traceback": traceback.format_exc()}


# ============================================================================
# SECTION 2: SOM AUTO-ADJUSTMENT LOGIC
# ============================================================================

def apply_auto_adjustments(metrics: Dict, kls: Optional[Any] = None) -> Dict[str, Any]:
    """Aplica auto-ajustes SOM baseado nas métricas e indicadores de falha.

    User requirement §1.3.1: α₀: 0.5 a 1.0 (decai para ~0.01-0.001)
    User requirement §1.3.2: σ₀: metade da maior dimensão da grade

    Estratégia matemática (Kohonen 2001):
        - Se QE alto (> mediana histórica + 2σ) → aumenta α₀
        - Se TE alto (> 0.2) → aumenta σ₀ (preserva topologia)
        - Se Dead Neurons > 30% → aumenta γ (conscience) e reset σ
        - Se Colapso Topológico → RESET completa dos pesos (severe)
        - Se Estagnação QE → boost de α₀ para escapar de mínimo local
        - Se Cruzamento Vizinhança → reduz α₀ e aumenta σ₀

    Args:
        metrics: dicionário de métricas (saída de evaluate_som_metrics)
        kls: instância de KohonenLearningSystem (opcional — para aplicar mudanças)

    Returns:
        Dicionário com ações tomadas e novos hiperparâmetros.
    """
    from bigru_t.model.som_auto_adjust import create_auto_adjuster

    logger.info("[AUTO-ADJUST] Iniciando análise de auto-ajuste...")
    adjuster = create_auto_adjuster()

    # State before
    state_before = adjuster.state.to_dict() if hasattr(adjuster.state, "to_dict") else {}

    # Aplica análise via auto_adjuster (lógica canônica do projeto)
    actions_taken: List[str] = []
    stop_training = False
    stop_reason: Optional[str] = None

    try:
        # O auto_adjuster consome um dicionário de métricas
        # Signature: adjust(self, kls, som_metrics=None, batch_idx=0)
        # kls pode ser None (apenas analisa, não aplica mudanças)
        result = adjuster.adjust(kls=kls, som_metrics=metrics, batch_idx=0)
        if isinstance(result, dict):
            stop_training = bool(result.get("stop_training", False))
            stop_reason = result.get("stop_reason")
            actions = result.get("actions", result.get("actions_taken", []))
            actions_taken.extend(actions)
            logger.info(f"[AUTO-ADJUST] Ações: {actions}")
            if stop_training:
                logger.warning(f"[AUTO-ADJUST] STOP TRAINING: {stop_reason}")
    except Exception as e:
        logger.error(f"[AUTO-ADJUST] Erro ao aplicar ajuste: {e}")
        actions_taken.append(f"error: {e}")

    # Lógica adicional de auto-ajuste matemático (Kohonen)
    # Baseada nos indicadores de falha específicos
    qe = float(metrics.get("quantization_error", 0))
    te = float(metrics.get("topological_error", 0))
    dead_rate = float(metrics.get("dead_neuron_rate", {}).get("dead_neuron_rate", 0))
    collapse_severity = metrics.get("topological_collapse", {}).get("severity", "none")
    stag_detected = metrics.get("qe_stagnation", {}).get("stagnation_detected", False)
    cross_severity = metrics.get("neighborhood_crossing", {}).get("severity", "none")

    recommendations: Dict[str, Any] = {}

    # Recomendação 1: α₀ baseado em QE
    # Kohonen: α₀ ∈ [0.5, 1.0] para fase de ordenação, decay para ~0.01
    if qe > 0.5:
        recommendations["alpha_0"] = 1.0  # máximo para escapar de mínimos
        recommendations["alpha_decay"] = "exponential"  # decai rápido
        actions_taken.append("alpha_0=1.0 (QE alto, escape de mínimo)")
    elif qe > 0.1:
        recommendations["alpha_0"] = 0.7
        recommendations["alpha_decay"] = "exponential"
        actions_taken.append("alpha_0=0.7 (QE moderado)")
    else:
        recommendations["alpha_0"] = 0.5
        recommendations["alpha_decay"] = "linear"  # decai suave
        actions_taken.append("alpha_0=0.5 (QE baixo, convergência)")

    # Recomendação 2: σ₀ baseado em TE
    # Kohonen: σ₀ = metade da maior dimensão da grade
    # Grid (4,4,4,4) → maior dimensão = 4 → σ₀ = 2
    grid_dims = (4, 4, 4, 4)
    largest_dim = max(grid_dims)
    sigma_0_baseline = largest_dim / 2.0  # = 2.0
    if te > 0.3:
        recommendations["sigma_0"] = sigma_0_baseline * 1.5  # 3.0 — mais vizinhança
        actions_taken.append("sigma_0=3.0 (TE alto, preserva topologia)")
    elif te > 0.1:
        recommendations["sigma_0"] = sigma_0_baseline * 1.25  # 2.5
        actions_taken.append("sigma_0=2.5 (TE moderado)")
    else:
        recommendations["sigma_0"] = sigma_0_baseline  # 2.0
        actions_taken.append("sigma_0=2.0 (TE baixo, canonical)")

    # Recomendação 3: Dead Neurons → conscience mechanism
    if dead_rate > 0.30:
        recommendations["conscience_gamma"] = 0.05  # ativa conscience
        actions_taken.append(f"conscience_gamma=0.05 (dead_rate={dead_rate:.2%})")
    elif dead_rate > 0.10:
        recommendations["conscience_gamma"] = 0.02
        actions_taken.append(f"conscience_gamma=0.02 (dead_rate={dead_rate:.2%})")
    else:
        recommendations["conscience_gamma"] = 0.0  # sem conscience

    # Recomendação 4: Colapso Topológico → RESET
    if collapse_severity == "severe":
        recommendations["reset_weights"] = True
        recommendations["reset_reason"] = "topological_collapse_severe"
        actions_taken.append("RESET pesos (colapso topológico severo)")
        stop_training = True
        stop_reason = "Topological collapse severe — needs full reset"
    elif collapse_severity == "moderate":
        recommendations["boost_alpha"] = 1.5  # multiplica α₀
        actions_taken.append("boost_alpha x1.5 (colapso moderado)")

    # Recomendação 5: Estagnação QE → boost
    if stag_detected:
        recommendations["boost_alpha"] = recommendations.get("boost_alpha", 1.0) * 1.3
        recommendations["sigma_boost"] = 1.2
        actions_taken.append("boost_alpha x1.3 + sigma_boost x1.2 (estagnação QE)")

    # Recomendação 6: Cruzamento de Vizinhança → reduz α, aumenta σ
    if cross_severity in ("severe", "moderate"):
        recommendations["alpha_reduction"] = 0.7
        recommendations["sigma_boost"] = recommendations.get("sigma_boost", 1.0) * 1.4
        actions_taken.append(f"alpha_reduction x0.7 + sigma_boost x1.4 (crossing={cross_severity})")

    # State after
    state_after = adjuster.state.to_dict() if hasattr(adjuster.state, "to_dict") else {}

    result = {
        "timestamp": datetime.utcnow().isoformat() + "Z",
        "metrics_summary": {
            "qe": qe,
            "te": te,
            "dead_rate": dead_rate,
            "collapse_severity": collapse_severity,
            "qe_stagnation": stag_detected,
            "crossing_severity": cross_severity,
        },
        "actions_taken": actions_taken,
        "recommendations": recommendations,
        "stop_training": stop_training,
        "stop_reason": stop_reason,
        "state_before": state_before,
        "state_after": state_after,
    }
    logger.info(f"[AUTO-ADJUST] {len(actions_taken)} ações tomadas")
    if stop_training:
        logger.warning(f"[AUTO-ADJUST] STOP: {stop_reason}")
    return result


# ============================================================================
# SECTION 3: DEMO MODE (test with synthetic SOM)
# ============================================================================

def run_demo() -> Dict[str, Any]:
    """Executa demo com SOM sintético para validar lógica."""
    import torch
    logger.info("[DEMO] Criando SOM sintético (4,4,4,4) = 256 neurons, 4D input")
    # SOM weights: shape (4,4,4,4,4) — last dim is input_dim
    torch.manual_seed(42)
    weights = torch.randn(4, 4, 4, 4, 4) * 0.1  # small init
    # Synthetic data clustered
    data = torch.randn(500, 4) * 0.2
    history: List[Dict] = []
    # Simula 5 épocas de histórico
    for ep in range(5):
        history.append({
            "quantization_error": 0.5 - ep * 0.08,
            "topological_error": 0.2 - ep * 0.03,
            "kaski_lagus_error": 0.4 - ep * 0.06,
            "explained_variance_share": 0.3 + ep * 0.1,
        })
    metrics = evaluate_som_metrics(weights, data, history)
    adjustments = apply_auto_adjustments(metrics)
    return {"metrics": metrics, "adjustments": adjustments}


# ============================================================================
# SECTION 4: MAIN ENTRYPOINT
# ============================================================================

def main() -> int:
    parser = argparse.ArgumentParser(
        description="V6.7 SOM Auto-Adjust Runner — métricas + indicadores de falha"
    )
    mode = parser.add_mutually_exclusive_group(required=True)
    mode.add_argument("--demo", action="store_true",
                      help="Executa demo com SOM sintético")
    mode.add_argument("--kls-state", type=str,
                      help="Caminho para arquivo .pt com estado KLS")
    mode.add_argument("--live", action="store_true",
                      help="Modo live (lê KLS do processo atual — experimental)")
    parser.add_argument("--auto-adjust", action="store_true",
                        help="Aplica auto-ajustes (caso contrário, só avalia)")
    parser.add_argument("--output-dir", type=str,
                        default="/home/z/my-project/download",
                        help="Diretório para salvar resultados JSON")
    args = parser.parse_args()

    print("=" * 70)
    print("V6.7 — SOM AUTO-ADJUST RUNNER (Métricas + Indicadores de Falha)")
    print("=" * 70)
    print("User requirement:")
    print("  Avalia: QE, TE, Kaski-Lagus, Variância Explicada")
    print("  Falhas: Colapso Topológico, Neurônios Mortos,")
    print("          Estagnação QE, Cruzamento Vizinhança")
    print("  Auto-ajuste: α₀ ∈ [0.5, 1.0] (decay ~0.01-0.001),")
    print("               σ₀ = metade da maior dimensão da grade")
    print("=" * 70)

    output_dir = Path(args.output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)
    ts = datetime.utcnow().strftime("%Y%m%d_%H%M%S")

    if args.demo:
        result = run_demo()
        metrics_file = output_dir / f"som_metrics_demo_{ts}.json"
        adjust_file = output_dir / f"som_auto_adjust_demo_{ts}.json"
        with open(metrics_file, "w") as f:
            json.dump(result["metrics"], f, indent=2, default=str)
        with open(adjust_file, "w") as f:
            json.dump(result["adjustments"], f, indent=2, default=str)
        print(f"\n✓ Métricas salvas: {metrics_file}")
        print(f"✓ Auto-ajustes salvos: {adjust_file}")
        # Print summary
        adj = result["adjustments"]
        print(f"\n--- RESUMO ---")
        print(f"Actions: {len(adj['actions_taken'])}")
        for a in adj["actions_taken"]:
            print(f"  • {a}")
        if adj["stop_training"]:
            print(f"STOP TRAINING: {adj['stop_reason']}")
        return 0

    elif args.kls_state:
        import torch
        logger.info(f"[LOAD] Carregando estado KLS de: {args.kls_state}")
        state = torch.load(args.kls_state, map_location="cpu", weights_only=False)
        # Extrai pesos do SOM do estado
        if isinstance(state, dict):
            # Tenta várias chaves canônicas
            weights = (
                state.get("som_weights") or
                state.get("weights") or
                state.get("som", {}).get("weights") if isinstance(state.get("som"), dict) else
                state.get("som")
            )
        else:
            weights = getattr(state, "weights", None)

        if weights is None:
            logger.error("Não foi possível extrair pesos SOM do estado.")
            return 1

        metrics = evaluate_som_metrics(weights)
        adjustments = apply_auto_adjustments(metrics) if args.auto_adjust else None

        metrics_file = output_dir / f"som_metrics_{ts}.json"
        with open(metrics_file, "w") as f:
            json.dump(metrics, f, indent=2, default=str)
        print(f"\n✓ Métricas salvas: {metrics_file}")

        if adjustments:
            adjust_file = output_dir / f"som_auto_adjust_{ts}.json"
            with open(adjust_file, "w") as f:
                json.dump(adjustments, f, indent=2, default=str)
            print(f"✓ Auto-ajustes salvos: {adjust_file}")
        return 0

    else:
        # --live mode (experimental)
        logger.error("--live mode não implementado nesta versão.")
        return 1


if __name__ == "__main__":
    sys.exit(main())