diff --git a/analysis/charts.py b/analysis/charts.py new file mode 100644 index 0000000..19bd51f --- /dev/null +++ b/analysis/charts.py @@ -0,0 +1,128 @@ +"""Gráficos (SVG) a partir dos resumos coletados em reports// (tarefa 74). + +Paleta: slots 1 (azul, GPU) e 2 (laranja, CPU) da paleta categórica de referência, validados +(CVD ΔE 24,7; visão normal 33,6; contraste ≥ 3:1 sobre #fcfcfb). Uma série → sem legenda; +duas séries → legenda + rótulos diretos. Um único eixo y por gráfico. + +Uso: python -m analysis.charts reports/pilot +""" +import json +import os +import sys +from pathlib import Path + +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt # noqa: E402 + +SURFACE, INK, INK_2, GRID = "#fcfcfb", "#0b0b0b", "#52514e", "#e6e5e1" +GPU, CPU = "#2a78d6", "#eb6834" +NAMES = {"semif": "SemIf", "rizzo_flow": "Rizzo Flow", "laya": "Laya", "gliner": "GLiNER"} +P95_LIMIT_MS = 150 + +plt.rcParams.update({ + "svg.fonttype": "none", "font.family": "sans-serif", "font.size": 11, + "axes.edgecolor": GRID, "axes.labelcolor": INK_2, "xtick.color": INK_2, "ytick.color": INK_2, + "axes.facecolor": SURFACE, "figure.facecolor": SURFACE, "text.color": INK, +}) + + +def save(fig, path: Path): + fig.savefig(path) + preview = os.environ.get("DMB_CHART_PREVIEW") # PNG para inspeção visual, fora do repositório + if preview: + fig.savefig(Path(preview) / path.with_suffix(".png").name, dpi=110) + + +def load(dest: Path) -> dict: + out = {} + for f in dest.glob("summary-*.json"): + s = json.loads(f.read_text()) + out[(s["candidate"]["name"], s["device"])] = s + return out + + +def style(ax, grid_axis="x"): + for side in ("top", "right"): + ax.spines[side].set_visible(False) + ax.grid(axis=grid_axis, color=GRID, linewidth=0.8) + ax.set_axisbelow(True) + + +def accuracy_chart(data, dest): + rows = sorted(((NAMES[c], s["quality"]["accuracy"] * 100) for (c, d), s in data.items() if d == "cuda"), + key=lambda r: r[1]) + fig, ax = plt.subplots(figsize=(7, 2.8)) + bars = ax.barh([r[0] for r in rows], [r[1] for r in rows], height=0.55, color=GPU) + for bar, (_, v) in zip(bars, rows): + ax.text(v + 1, bar.get_y() + bar.get_height() / 2, f"{v:.0f}%", va="center", color=INK, fontsize=11) + ax.set_xlim(0, 105) + ax.set_xlabel("Acurácia (%)") + ax.set_title("Acurácia por candidato — piloto, 50 exemplos de dev", loc="left", fontsize=12, color=INK) + style(ax) + fig.tight_layout() + save(fig, dest / "chart-acuracia.svg") + plt.close(fig) + + +def latency_chart(data, dest): + cands = sorted({c for c, _ in data}, key=lambda c: data[(c, "cuda")]["latency_ms"]["p50"]) + fig, ax = plt.subplots(figsize=(7, 3.0)) + for i, c in enumerate(cands): + g = data[(c, "cuda")]["latency_ms"]["p50"] + cpu = data.get((c, "cpu"), {}).get("latency_ms", {}).get("p50") + if cpu: + ax.plot([g, cpu], [i, i], color=GRID, linewidth=2, zorder=1) + ax.scatter(cpu, i, s=70, color=CPU, edgecolor=SURFACE, linewidth=2, zorder=3, + label="CPU (8 OCPU)" if i == 0 else None) + ax.text(cpu * 1.18, i, f"{cpu / 1000:.1f} s" if cpu >= 1000 else f"{cpu:.0f} ms", + va="center", color=INK, fontsize=10) + ax.scatter(g, i, s=70, color=GPU, edgecolor=SURFACE, linewidth=2, zorder=3, + label="GPU (A10)" if i == 0 else None) + ax.text(g / 1.18, i, f"{g:.0f} ms", va="center", ha="right", color=INK, fontsize=10) + ax.set_xscale("log") + ax.set_xlim(5, 200_000) + ax.set_yticks(range(len(cands)), [NAMES[c] for c in cands]) + ax.set_ylim(-0.6, len(cands) - 0.4) + ax.set_xlabel("Latência mediana por decisão (ms, escala log)") + ax.set_title("Latência por decisão: GPU × CPU (lote 1)", loc="left", fontsize=12, color=INK) + ax.legend(loc="lower right", frameon=False, fontsize=10) + style(ax) + fig.tight_layout() + save(fig, dest / "chart-latencia.svg") + plt.close(fig) + + +def tradeoff_chart(data, dest): + fig, ax = plt.subplots(figsize=(7, 3.6)) + ax.axvline(P95_LIMIT_MS, color=INK_2, linewidth=1, linestyle=(0, (4, 3))) + ax.text(P95_LIMIT_MS + 3, 102, f"limite p95 = {P95_LIMIT_MS} ms", color=INK_2, fontsize=9, va="top") + for (c, d), s in data.items(): + if d != "cuda": + continue + x, y = s["latency_ms"]["p95"], s["quality"]["accuracy"] * 100 + ax.scatter(x, y, s=80, color=GPU, edgecolor=SURFACE, linewidth=2, zorder=3) + ax.annotate(NAMES[c], (x, y), xytext=(8, -4), textcoords="offset points", color=INK, fontsize=10) + ax.set_xlim(0, 220) + ax.set_ylim(30, 104) + ax.set_xlabel("Latência p95 por decisão em GPU A10 (ms)") + ax.set_ylabel("Acurácia (%)") + ax.set_title("Qualidade × latência em GPU — piloto", loc="left", fontsize=12, color=INK) + style(ax, grid_axis="both") + fig.tight_layout() + save(fig, dest / "chart-qualidade-latencia.svg") + plt.close(fig) + + +def main(): + dest = Path(sys.argv[1]) + data = load(dest) + accuracy_chart(data, dest) + latency_chart(data, dest) + tradeoff_chart(data, dest) + print(sorted(p.name for p in dest.glob("chart-*.svg"))) + + +if __name__ == "__main__": + main() diff --git a/infra/ds/collect.py b/infra/ds/collect.py index 83a0850..622f276 100644 --- a/infra/ds/collect.py +++ b/infra/ds/collect.py @@ -67,14 +67,20 @@ def main(): table = ("| Candidato | Disp. | Precisão | n | Acurácia | Macro F1 | ECE (bruto) | p50 ms | p95 ms | p99 ms | " "Carga s | RAM pico MiB | GPU pico MiB | Erros | Truncados |\n" "|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|\n" + "\n".join(rows)) - readme = (f"# Resultados — partição `{args.split}`" - + (f", {args.examples} exemplos" if args.examples else "") + "\n\n" - "Gerado por `infra/ds/collect.py` a partir dos buckets de resultados. CPU: us-chicago-1 " - "(`VM.Standard.E4.Flex`, 8 OCPU, sub-rede privada). GPU: sa-saopaulo-1 (`VM.GPU.A10.1`). " - "Lote 1; latências excluem os exemplos de aquecimento. Probabilidades ainda sem calibração.\n\n" - + table + "\n") - (dest / "README.md").write_text(readme) - print(readme) + start, end = "", "" + block = f"{start}\n{table}\n{end}" + readme_path = dest / "README.md" + if readme_path.exists() and start in readme_path.read_text(): + # README editado à mão: só a tabela entre os marcadores é regenerada. + text = readme_path.read_text() + readme = text[:text.index(start)] + block + text[text.index(end) + len(end):] + else: + readme = (f"# Resultados — partição `{args.split}`" + + (f", {args.examples} exemplos" if args.examples else "") + "\n\n" + "Gerado por `infra/ds/collect.py`. Lote 1; latências sem os exemplos de aquecimento; " + "probabilidades sem calibração.\n\n" + block + "\n") + readme_path.write_text(readme) + print(table) if __name__ == "__main__": diff --git a/reports/pilot/README.md b/reports/pilot/README.md index abe6e0f..b6a1e69 100644 --- a/reports/pilot/README.md +++ b/reports/pilot/README.md @@ -1,7 +1,37 @@ -# Resultados — partição `dev`, 50 exemplos +# Piloto — 50 exemplos de desenvolvimento por candidato -Gerado por `infra/ds/collect.py` a partir dos buckets de resultados. CPU: us-chicago-1 (`VM.Standard.E4.Flex`, 8 OCPU, sub-rede privada). GPU: sa-saopaulo-1 (`VM.GPU.A10.1`). Lote 1; latências excluem os exemplos de aquecimento. Probabilidades ainda sem calibração. +## Sumário executivo +O piloto validou, de ponta a ponta, a execução dos quatro candidatos sobre a mesma amostra de 50 enunciados de atendimento de telefonia móvel, em CPU e em GPU. Não houve erros de execução nem truncamento de entrada, e nenhum parâmetro operacional precisou de ajuste. + +- **Qualidade:** os dois modelos de 4B parâmetros lideram. O **SemIf** acertou **92%** e o **Rizzo Flow** **82–86%**. Os encoders compactos ficaram abaixo: **Laya** com 66% e **GLiNER** com 48%. +- **Abstenção:** o SemIf reconheceu 10 de 12 pedidos sem intenção correspondente; o Laya, 4 de 12. Esse é o comportamento que evita rotas erradas no orquestrador. +- **Latência em GPU A10:** Laya e GLiNER respondem em cerca de **20 ms**; SemIf e Rizzo, em cerca de **110 ms** de mediana. Mas o **p95 dos modelos de 4B (175–181 ms) fica acima do limite pré-registrado de 150 ms**. +- **CPU:** só Laya e GLiNER são viáveis (p95 de 0,2 a 0,5 s). SemIf e Rizzo levam de 13 a 31 s por decisão e ficam fora das rodadas em CPU. +- **Leitura preliminar:** há um *trade-off* claro entre qualidade e latência, que a rodada completa de desenvolvimento, a calibração e o teste final (1.744 exemplos) vão quantificar com intervalos de confiança. + +## Resultados gerais + +![Acurácia por candidato](chart-acuracia.svg) + +![Qualidade × latência em GPU](chart-qualidade-latencia.svg) + +![Latência por decisão: GPU × CPU](chart-latencia.svg) + +| Candidato | Acertos dentro do escopo (38) | `sem_correspondencia` reconhecido (12) | Escopo marcado como `sem_correspondencia` | +|---|---|---|---| +| SemIf | 36 | 10 | 2 | +| Rizzo Flow | 34 | 7 | 2 | +| Laya | 29 | 4 | 0 | +| GLiNER | 19 | 5 | 11 | + +*(GPU A10. Em CPU os números coincidem, exceto o Rizzo Flow, que acerta 2 casos a mais: os backends CPU e CUDA do llama.cpp não são numericamente idênticos.)* + +## Resultados detalhados + +Lote 1. As latências excluem os 5 primeiros exemplos (aquecimento). ECE calculado sobre as probabilidades brutas, ainda sem calibração. + + | Candidato | Disp. | Precisão | n | Acurácia | Macro F1 | ECE (bruto) | p50 ms | p95 ms | p99 ms | Carga s | RAM pico MiB | GPU pico MiB | Erros | Truncados | |---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| | gliner | cpu | float32 | 50 | 48.0% | 43.9% | 0.198 | 244.0 | 481.0 | 511.6 | 21.0 | 3257 | — | 0 | 0 | @@ -12,3 +42,33 @@ Gerado por `infra/ds/collect.py` a partir dos buckets de resultados. CPU: us-chi | rizzo_flow | cuda | gguf-q8_0 | 50 | 82.0% | 84.3% | 0.103 | 108.1 | 174.7 | 182.5 | 5.3 | 4527 | 5107 | 0 | 0 | | semif | cpu | bfloat16 | 50 | 92.0% | 93.9% | 0.079 | 30633.4 | 47585.7 | 49344.0 | 6.3 | 9137 | — | 0 | 0 | | semif | cuda | bfloat16 | 50 | 92.0% | 93.9% | 0.114 | 113.8 | 181.3 | 183.7 | 6.7 | 9020 | 8513 | 0 | 0 | + + +Arquivos: `summary--.json` (resumo da execução, com cenário, precisão e diagnósticos) e `predictions--.jsonl` (uma linha por exemplo, com a distribuição de probabilidades completa). + +## Metodologia + +**Amostra.** São 50 exemplos sorteados com seed fixa (20261007) da partição de desenvolvimento do conjunto congelado `d1-v1`. Os candidatos recebem os mesmos exemplos, na mesma ordem. + +| Característica | Composição da amostra | +|---|---| +| Intenções distintas | 29 das 42 da taxonomia | +| Rótulo `sem_correspondencia` | 12: 8 pedidos fora de escopo e 4 com a intenção correta fora das opções | +| Opções por pergunta | 4 (15 exemplos) · 8 (14) · 12 (9) · 16 (12) | +| Perfis de escrita | informal/abreviado (12), erros de digitação (11), curto/neutro (8), regionalismo (8), emocional (5), indireto/várias intenções (5), longo (1) | + +**Tarefa.** Classificação de intenção (D1): para cada enunciado, o candidato escolhe uma opção entre as intenções elegíveis do exemplo mais `sem_correspondencia`, pelo contrato `POST /v1/systemone` (`jev-compat-v1`). + +**Execução.** Lote 1 e sem paralelismo; cada candidato na sua precisão nativa (fp32 para Laya e GLiNER, bf16 para SemIf, GGUF Q8_0 para Rizzo Flow). Inferência isolada de fontes externas, com artefatos verificados por SHA-256. + +**Cenário.** CPU em us-chicago-1 (`VM.Standard.E4.Flex`, 8 OCPU, 64 GB). GPU em sa-saopaulo-1 (`VM.GPU.A10.1`). Mais detalhes em [docs/05](../../docs/05-cenario-de-execucao.md). + +**Limites desta etapa.** Com 50 exemplos, cada ponto percentual de acurácia corresponde a meio exemplo, e os intervalos de confiança são largos (cerca de ±13 p.p. a 95%). O piloto serve para validar a execução e dar a ordem de grandeza, não para ranquear. + +## Conclusão + +1. **Execução validada.** Os quatro candidatos rodam isolados, sem erros e sem truncamento, com a mesma entrada e o mesmo contrato, em CPU e em GPU. O piloto não exigiu ajuste operacional; o orçamento interno de opções do Laya foi verificado e mantido no padrão. +2. **CPU restrita aos encoders.** Pela regra pré-registrada (p95 ≤ 10 s), SemIf e Rizzo Flow ficam fora das rodadas em CPU. +3. **Sinal de qualidade.** Os modelos de 4B lideram em acurácia e em abstenção correta. Entre os compactos, o Laya acerta mais dentro do escopo, mas quase não abstém; o GLiNER marca muitos pedidos válidos como `sem_correspondencia`. +4. **Sinal de latência.** Em GPU, só Laya e GLiNER cumprem o p95 ≤ 150 ms. SemIf e Rizzo ficam perto do limite (cerca de 110 ms de mediana), mas acima dele no p95. +5. **Próximos passos.** Rodada completa de desenvolvimento (1.183 exemplos), calibração das probabilidades e escolha de limiares de abstenção (1.127 exemplos), congelamento da configuração e teste final (1.744 exemplos), com intervalos de confiança e cortes por perfil e número de opções. diff --git a/reports/pilot/chart-acuracia.svg b/reports/pilot/chart-acuracia.svg new file mode 100644 index 0000000..d27ce51 --- /dev/null +++ b/reports/pilot/chart-acuracia.svg @@ -0,0 +1,251 @@ + + + + + + + + 2026-10-07T21:19:36.948395 + image/svg+xml + + + Matplotlib v3.10.7, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + + + + + + + + 20 + + + + + + + + + + + + + 40 + + + + + + + + + + + + + 60 + + + + + + + + + + + + + 80 + + + + + + + + + + + + + 100 + + + + Acurácia (%) + + + + + + + + + + + + + + GLiNER + + + + + + + + + + Laya + + + + + + + + + + Rizzo Flow + + + + + + + + + + SemIf + + + + + + + + + + + + + + + + + + + + + + + 48% + + + 66% + + + 82% + + + 92% + + + Acurácia por candidato — piloto, 50 exemplos de dev + + + + + + + + + diff --git a/reports/pilot/chart-latencia.svg b/reports/pilot/chart-latencia.svg new file mode 100644 index 0000000..fc95ba4 --- /dev/null +++ b/reports/pilot/chart-latencia.svg @@ -0,0 +1,626 @@ + + + + + + + + 2026-10-07T21:19:37.064799 + image/svg+xml + + + Matplotlib v3.10.7, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 0 + 1 + + + + + + + + + + + + + + + + + + 1 + 0 + 2 + + + + + + + + + + + + + + + + + + 1 + 0 + 3 + + + + + + + + + + + + + + + + + + 1 + 0 + 4 + + + + + + + + + + + + + + + + + + 1 + 0 + 5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Latência mediana por decisão (ms, escala log) + + + + + + + + + + + + + + GLiNER + + + + + + + + + + Laya + + + + + + + + + + Rizzo Flow + + + + + + + + + + SemIf + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 244 ms + + + + + + + + + + + 18 ms + + + + + + + + 161 ms + + + + + + + + 22 ms + + + + + + + + 12.7 s + + + + + + + + 108 ms + + + + + + + + 30.6 s + + + + + + + + 114 ms + + + Latência por decisão: GPU × CPU (lote 1) + + + + + + + + + CPU (8 OCPU) + + + + + + + + GPU (A10) + + + + + + + + + + diff --git a/reports/pilot/chart-qualidade-latencia.svg b/reports/pilot/chart-qualidade-latencia.svg new file mode 100644 index 0000000..bffcd77 --- /dev/null +++ b/reports/pilot/chart-qualidade-latencia.svg @@ -0,0 +1,388 @@ + + + + + + + + 2026-10-07T21:19:37.147323 + image/svg+xml + + + Matplotlib v3.10.7, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + + + + + + + + 25 + + + + + + + + + + + + + 50 + + + + + + + + + + + + + 75 + + + + + + + + + + + + + 100 + + + + + + + + + + + + + 125 + + + + + + + + + + + + + 150 + + + + + + + + + + + + + 175 + + + + + + + + + + + + + 200 + + + + Latência p95 por decisão em GPU A10 (ms) + + + + + + + + + + + + + + + + + 30 + + + + + + + + + + + + + 40 + + + + + + + + + + + + + 50 + + + + + + + + + + + + + 60 + + + + + + + + + + + + + 70 + + + + + + + + + + + + + 80 + + + + + + + + + + + + + 90 + + + + + + + + + + + + + 100 + + + + Acurácia (%) + + + + + + + + + + + + + limite p95 = 150 ms + + + + + + + + + + + GLiNER + + + + + + + + Laya + + + + + + + + Rizzo Flow + + + + + + + + SemIf + + + Qualidade × latência em GPU — piloto + + + + + + + + +