File size: 7,883 Bytes
e26f4cc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python3
"""51_treino_v050.py — Treino com dados reais (Wikipédia PT-BR) e telemetria v0.5.0.

Recoleta os shards (foram apagados na limpeza de estados — regeneráveis por
design), treina as fases A (tokenizador GSQ+RCO) e B (gerador NCM) com tetos
de tempo honestos, e adiciona o painel de telemetria v0.5.0 (detecção de
aprendizado Theil–Sen sobre as séries do JSONL).

Saída: resultados/treino_v050.json + telemetria_fase_a/b.jsonl
"""
import json, math, os, sys, time

def coletar(pasta, max_artigos=400, max_bytes=4_000_000):
    """Streaming controlado (tetos duros)."""
    from datasets import load_dataset
    ds = load_dataset("wikimedia/wikipedia", "20231101.pt", split="train", streaming=True)
    textos, total_bytes = [], 0
    for ex in ds:
        t = (ex.get("text") or "").strip()
        if len(t) < 200:
            continue
        textos.append(t)
        total_bytes += len(t.encode("utf-8"))
        if len(textos) % 100 == 0:
            print(f"  ... {len(textos)} artigos, {total_bytes/1e6:.2f} MB", flush=True)
        if len(textos) >= max_artigos or total_bytes >= max_bytes:
            break
    def limpa(t):
        partes = [p.strip() for p in t.split("\n") if len(p.strip()) > 80]
        return " ".join(partes[:6])[:1800]
    textos = [limpa(t) for t in textos]
    n_val = max(1, len(textos) // 10)
    with open(os.path.join(pasta, "dados", "shard_validacao.jsonl"), "w") as f:
        for t in textos[:n_val]:
            f.write(json.dumps({"texto": t}, ensure_ascii=False) + "\n")
    with open(os.path.join(pasta, "dados", "shard_treino.jsonl"), "w") as f:
        for t in textos[n_val:]:
            f.write(json.dumps({"texto": t}, ensure_ascii=False) + "\n")
    print(f"coletado: {len(textos)} artigos, {total_bytes/1e6:.2f} MB")

def theil_sen(ys):
    if len(ys) < 4:
        return 0.0, "amostra_insuficiente"
    incl = []
    for i in range(len(ys)):
        for j in range(i + 1, len(ys)):
            if j > i:
                incl.append((ys[j] - ys[i]) / (j - i))
    incl.sort()
    med = incl[len(incl) // 2]
    media = sum(ys) / len(ys)
    rel = med / max(abs(media), 1e-9)
    vered = "aprendendo" if rel < -1e-4 else ("divergindo" if rel > 1e-4 else "estagnado")
    return med, vered

def painel_do_jsonl(caminho):
    """Painel v0.5.0: séries do JSONL → detecção Theil–Sen por métrica."""
    series = {}
    if not os.path.exists(caminho):
        return series
    with open(caminho) as f:
        for linha in f:
            try:
                ev = json.loads(linha)
            except json.JSONDecodeError:
                continue
            dados = ev.get("dados", {})
            if ev.get("tipo") == "dump":
                # dump final: {"nome_serie": [[passo, valor], ...]}
                for k, v in dados.items():
                    if isinstance(v, list):
                        ys = [par[1] for par in v if isinstance(par, list) and len(par) == 2]
                        if len(ys) >= 2:
                            series[k] = ys
            else:
                for k, v in dados.items():
                    if isinstance(v, (int, float)) and not isinstance(v, bool):
                        series.setdefault(k, []).append(float(v))
    painel = {}
    for k, ys in series.items():
        if len(ys) >= 8:
            med, vered = theil_sen(ys)
            painel[k] = {"inclinacao": med, "veredito": vered, "n": len(ys),
                         "primeiro": ys[0], "ultimo": ys[-1]}
    return painel

def main():
    pasta = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    sys.path.insert(0, pasta)
    os.makedirs(os.path.join(pasta, "resultados"), exist_ok=True)
    import khtst_rust as K

    cfg = K.Config(os.path.join(pasta, "configs", "base_rust.json"))

    # 1) dados (se ausentes — regeneráveis)
    if not os.path.exists(os.path.join(pasta, "dados", "shard_treino.jsonl")):
        print("=== Coleta de dados reais (Wikipédia PT-BR, streaming com tetos) ===", flush=True)
        coletar(pasta)
    else:
        print("shards presentes — reutilizando")

    bpe = K.Bpe.carregar(os.path.join(pasta, "dados", "bpe.json"))
    seqs = []
    with open(os.path.join(pasta, "dados", "shard_treino.jsonl")) as f:
        for linha in f:
            ids = bpe.codificar(json.loads(linha)["texto"])
            for i in range(0, max(1, len(ids) - 256), 128):
                seqs.append(ids[i:i + 256])
    print(f"sequências de treino: {len(seqs)} (ctx=256)", flush=True)
    # limita para teto de tempo honesto (~6 min de fase A + ~5 min de B)
    seqs = seqs[:1400]

    rel = {"versao": "0.5.0", "dados": {"artigos": "ver shard", "sequencias": len(seqs)}, "fases": {}}

    # 2) fase A
    print("=== FASE A — tokenizador de conceitos (GSQ+RCO Stiefel) ===", flush=True)
    t0 = time.time()
    tel_a = os.path.join(pasta, "resultados", "telemetria_fase_a.jsonl")
    saida = K.treinar_fase_a(
        cfg, seqs, caminho_telemetria=tel_a,
        caminho_checkpoint=os.path.join(pasta, "resultados", "tokenizador_conceitos.json"),
    )
    dt = time.time() - t0
    r = saida["resumo"]
    rel["fases"]["a"] = {**r, "tempo_s": dt}
    print(f"  perda {r['perda_inicial']:.3f} -> {r['perda_final']:.3f} | acc_recon {r['acc_recon']:.3f} "
          f"| δ_ort {r['delta_ort_max']:.1e} | perp_codebook {r['perplexidade_codebook']:.1f} "
          f"| escore {r['escore_aprendizado']:.3f} | {dt:.0f}s", flush=True)

    # 3) fase B
    print("=== FASE B — gerador NCM (MLE exato, hierarquias congeladas) ===", flush=True)
    t0 = time.time()
    tel_b = os.path.join(pasta, "resultados", "telemetria_fase_b.jsonl")
    saida_b = K.treinar_fase_b(
        cfg, seqs,
        caminho_tokenizador=os.path.join(pasta, "resultados", "tokenizador_conceitos.json"),
        caminho_telemetria=tel_b,
        # checkpoint TEMPORÁRIO para a suíte V10 (apagado na limpeza final;
        # regenerável rodando este script)
        caminho_checkpoint=os.path.join(pasta, "resultados", "gerador_conceitos.json"),
        teto_seg=300.0,           # teto honesto de 5 min
    )
    dt = time.time() - t0
    rb = saida_b["resumo"]
    rel["fases"]["b"] = {**rb, "tempo_s": dt}
    print(f"  CE {rb['perda_inicial']:.3f} -> {rb['perda_final']:.3f} | acc_conceitos {rb.get('acuracia_media', 0):.3f} "
          f"| {dt:.0f}s", flush=True)

    # 4) painel de telemetria v0.5.0 (Theil–Sen por série)
    print("=== Painel de telemetria v0.5.0 (detecção Theil–Sen) ===", flush=True)
    painel_a = painel_do_jsonl(tel_a)
    painel_b = painel_do_jsonl(tel_b)
    for nome, p in (("fase A", painel_a), ("fase B", painel_b)):
        for k, v in sorted(p.items()):
            print(f"  [{nome}] {k}: {v['primeiro']:.4f} -> {v['ultimo']:.4f} ({v['veredito']}, n={v['n']})")
    rel["telemetria_v050"] = {"fase_a": painel_a, "fase_b": painel_b}

    # gates de aprendizado honestos
    gates = {
        "perda_a_cai": r["perda_final"] < r["perda_inicial"],
        "ortogonalidade_a": r["delta_ort_max"] < 1e-5,
        "codebook_sem_colapso": r["perplexidade_codebook"] > 8.0,
        "ce_b_cai": rb["perda_final"] < rb["perda_inicial"],
        "deteccao_a_aprendendo": painel_a.get("perda", {}).get("veredito") == "aprendendo",
    }
    if painel_b:
        gates["deteccao_b_aprendendo"] = painel_b.get("perda", {}).get("veredito") == "aprendendo"
    rel["gates"] = gates
    ok = sum(1 for v in gates.values() if v)
    print(f"\n=== GATES: {ok}/{len(gates)} verdes ===")
    for g, v in gates.items():
        print(f"  {'OK ' if v else 'FALHOU'} {g}")

    with open(os.path.join(pasta, "resultados", "treino_v050.json"), "w") as f:
        json.dump(rel, f, indent=1, ensure_ascii=False)
    print("salvo: resultados/treino_v050.json")
    if ok < len(gates):
        sys.exit(1)

if __name__ == "__main__":
    main()