Extrator de citações jurídicas em português do Brasil. Lê um documento judicial e marca onde está cada citação de jurisprudência e de lei, com o tipo de cada uma. É o extrator da nossa solução para o Desafio Caça-Alucinações (BRACIS 2026 × Jusbrasil), que verifica se as citações de um texto existem de verdade num acervo de decisões.
O nome homenageia Luiz Gama (1830–1882), advogado abolicionista que libertou centenas de pessoas nos tribunais citando a lei com precisão.
Esta é a versão v1.3: as 12 primeiras das 22 camadas do v1.2, treinadas para imitar as
probabilidades do v1.2 (destilação). Dentro da solução, a saída no estilo dos documentos do
desafio é a mesma do v1.2; em texto real fica no mesmo nível, e roda 1,5 vez mais rápido.
O v1.2 continua disponível na tag v1.2.
Código, pipeline completo e experimentos: github.com/vinimlo/gama.
Classificação de tokens no esquema BIO, com três tipos:
| Rótulo | O que marca | Exemplo |
|---|---|---|
JURIS | precedente identificado por número, súmula ou tema | REsp nº 1.234.567/SP, Súmula 7 do STJ |
LEI | dispositivo de lei, com o nome do diploma | art. 927 do Código de Processo Civil |
VAGA | julgado citado sem número: tribunal, ano e relator | julgado do STJ proferido em 2021 pela relatoria de Nancy Andrighi |
As bordas seguem as convenções do gabarito do desafio: artigo e preposição antes da citação ficam fora, pontuação depois fica fora, a UF no fim do número entra.
O modelo só diz onde a citação está. Se ela existe ou foi inventada, quem decide é uma consulta exata ao acervo, feita pelo resto do pipeline. Um modelo que decidisse isso sozinho estaria chutando com fluência, que é justamente a alucinação que se quer pegar.
O repositório traz o bio.py, o mesmo módulo que rotulou o treino e decodifica a saída:
import importlib.util
import torch
from huggingface_hub import hf_hub_download
from transformers import AutoModelForTokenClassification, AutoTokenizer
REPO, REV = "vinimlo/gama", "5f924ca2fa77c2aae6afe6d770c78ca4438f52f3"
tok = AutoTokenizer.from_pretrained(REPO, revision=REV)
modelo = AutoModelForTokenClassification.from_pretrained(REPO, revision=REV).eval()
spec = importlib.util.spec_from_file_location("bio", hf_hub_download(REPO, "bio.py", revision=REV))
bio = importlib.util.module_from_spec(spec)
spec.loader.exec_module(bio)
texto = ("Nesse sentido, invoca-se o REsp nº 1.234.567/SP e o art. 927 do Código de Processo Civil, "
"bem como o julgado do STJ proferido em 2021 pela relatoria de Nancy Andrighi.")
enc = tok(texto, return_offsets_mapping=True, return_tensors="pt")
with torch.no_grad():
rotulos = modelo(input_ids=enc["input_ids"],
attention_mask=enc["attention_mask"]).logits[0].argmax(-1).tolist()
offsets = [tuple(o) for o in enc["offset_mapping"][0].tolist()]
for ini, fim, tipo in bio.decodificar(offsets, rotulos, texto):
print(tipo, texto[ini:fim])
# JURIS REsp nº 1.234.567/SP
# LEI art. 927 do Código de Processo Civil
# VAGA julgado do STJ proferido em 2021 pela relatoria de Nancy Andrighi
O contexto é de 8.192 tokens, o bastante para a maioria dos documentos numa passada só.
Para documentos maiores, para a confiança por citação e para a versão mais recente do
bio.py, use o extrator do repositório no GitHub (src/gama/extratores/neural.py).
Os pesos sozinhos só acham as citações. A solução inteira, com a guarda, a consulta ao acervo e a confiança calibrada, roda em Docker a partir do repositório no GitHub:
git clone https://github.com/vinimlo/gama && cd gama
bash run.sh --preparar # com rede, uma vez
bash run.sh <caminho_db> <pasta_txt> <arquivo_saida.csv> # sem rede
O primeiro comando constrói a imagem e baixa estes pesos na revisão fixa. O segundo lê a
base SQLite do acervo e uma pasta de .txt, roda sem rede e grava o CSV no formato da
submissão, com um JSON por documento ao lado. Usa a GPU se o Docker tiver o runtime NVIDIA;
sem ela roda em CPU, a cerca de 1 s por documento.
Na solução, o modelo não trabalha sozinho. Entre ele e o resto do pipeline há uma guarda, e os números "com a guarda" desta página são dela. Quem usar só os pesos obtém o "Gama sozinho" das tabelas.
A guarda existe porque o modelo foi treinado no estilo dos documentos do desafio e, fora dele, erra de um jeito previsível. Em ementa real ele pode tomar o "Rel. Min. Fulano, julgado em ..." que vem logo depois de um precedente numerado por uma referência vaga, ou marcar pedaços soltos como "Rel", "2011" e "DJe". Mas ele sabe quando hesita. O que separa esses erros dos acertos é a confiança do span: a média, sobre os tokens rotulados como citação, da probabilidade que o modelo deu ao rótulo escolhido. No estilo do desafio, o menor dos 4.447 acertos do v1.3 no estresse difícil tem 0,9993 (com o v1.2, nenhum de 34.171 acertos ficou abaixo de 0,98). Os falsos positivos do v1.3 em ementa têm mediana de 0,83.
São duas regras, nesta ordem:
VAGA a até 2 caracteres de outro span sai. No molde do desafio, a referência vaga é uma
frase própria, e a mais próxima de outra citação fica a 4 caracteres.No estilo do desafio nenhuma das duas dispara. Em 4.626 documentos, a saída é a mesma com e
sem guarda, e a nota oficial não se mexe. Fora dele, sim. Nas 305 ementas do benchmark, o
F1 de extração vai de 0,620 a 0,818. Num teste de 172 ementas sorteadas depois, fora de tudo
o que foi usado para escolher as regras, o ganho foi +0,205 (IC95 +0,154 a +0,246). A
implementação está em src/gama/extratores/guarda.py no GitHub; o extrator da solução,
--extrator neural, já a inclui.
Solução completa (extrator, guarda, resolver, confiança calibrada) com cada versão do modelo.
| v1.2 | v1.3 | |
|---|---|---|
| Dev, estresse difícil e 4.000 sintéticos (4.626 documentos), métrica oficial | 1,10000 / 1,09999 / 1,09999 | iguais, com o JSON final idêntico documento a documento |
| Texto real, F1 de extração, 172 ementas separadas antes da escolha da guarda | 0,820 | 0,839 (+0,019; IC95 +0,003 a +0,033) |
| Tempo por documento, NVIDIA L4 / CPU, medidos lado a lado | 0,071 s / 1,62 s | 0,046 s / 0,93 s |
| Parâmetros / pesos em FP32 | 307,5M / 1,23 GB | 257,4M / 1,03 GB |
As 172 ementas foram separadas antes da escolha da guarda e mediram a guarda uma vez só. Depois elas também serviram para comparar os alunos e escolher o v1.3, então para a diferença entre v1.2 e v1.3 são dado de desenvolvimento, não um teste independente. E retreinado com outras sementes o v1.2 varia mais do que essa diferença (0,785 a 0,842 nas 172, com a guarda).
Métrica oficial do desafio, macroF1 · (1 − 0,5·τ) · (1 + 0,10·(1 − Brier)), com o mesmo
resolver para todos os extratores. O teto prático é 1,10.
| Conjunto | Gama v1.3 | Gama v1.3 com a guarda da solução | Régua (regex) | Qwen3-8B zero-shot | GLiNER 2.5 zero-shot |
|---|---|---|---|---|---|
| Dev da organização, 26 documentos | 1,10000 | 1,10000 | 1,09848 | não medido¹ | não medido¹ |
| Estresse difícil, 600 documentos, métrica oficial | 1,09999 | 1,09999 | 0,84880 | 0,81065 | 0,44445 |
| Texto real, 305 ementas, F1 de extração | 0,620 | 0,818 | 0,663 | 0,707 | 0,608 |
| Tempo por documento, NVIDIA L4 | 0,035 s | 0,037 s | 0,001 s | 11,7 s | 0,110 s |
¹ Os documentos do dev são da organização e não saem da nossa máquina.
O estresse difícil segue o estilo dos documentos do desafio, com frases escritas por um
LLM que nenhum modelo viu no treino e ruído de OCR forte. Nele o fine-tune é a diferença:
o melhor extrator sem treino fica abaixo até da régua. O texto real são ementas do STF,
STJ e TJRJ (celsowm/jurisprudencias_br,
CC-BY-4.0), com citações anotadas por dois LLMs e adjudicadas por critério escrito. Ali a
ordem se inverte: um LLM genérico de 8B extrai melhor que o modelo sozinho, a um custo
cerca de 330 vezes maior por documento. A guarda da solução troca os trechos em que o
modelo hesita (confiança < 0,95) pelos da régua e põe o Gama de volta na frente.
Na fase de treino do Kaggle, a solução completa com o v1.2 marcou 1,09999.
O gráfico acima compara o Gama com extratores sem treino. Para separar o que vem do fine-tune do que vem da escolha do modelo, treinamos concorrentes nos mesmos documentos sintéticos do v1.2 e medimos todos pelo mesmo harness, sozinhos e com a guarda da solução. F1 de extração em texto real; as 305 ementas do benchmark escolheram os limiares, e as 172 separadas depois só confirmam.
| Texto real sozinho, 305 / 172 | Texto real com a guarda, 305 / 172 | Estresse difícil sozinho, métrica oficial | |
|---|---|---|---|
| Gama v1.3 | 0,620 / 0,635 | 0,818 / 0,839 | 1,09999 |
| BERTimbau, mesma receita do v1.2 | 0,697 / 0,705 | 0,813 / 0,826 | 1,09999 |
| GLiNER 2.5 multi treinado | 0,808 / 0,824 | 0,816 / 0,837 | 1,09615 |
| mmBERT-base com o encoder congelado, só a saída treinada | 0,243 / 0,247 | 0,350 / 0,353 | 0,90182 |
Sozinhos, BERTimbau e GLiNER treinados extraem melhor que o Gama em texto real. Com a guarda, nenhum passa o v1.3. O GLiNER 2.5 treinado chega ao mesmo nível sem guarda nenhuma (−0,010 nas 305 e −0,015 nas 172, IC95 cruzando zero), porque já generaliza fora do molde; a guarda quase não o ajuda, já que a maior parte dos erros dele sai com score acima de 0,99. O Gama especializa no estilo do desafio e compensa sabendo quando hesita, e no estilo do desafio segue na frente (1,09999 contra 1,09615). Uma semente por modelo: não é equivalência. Até 29/09 estas tabelas usavam o GLiNER multi v2.1, que era pior nos dois papéis.
O encoder congelado vai longe no estilo do desafio e desaba em texto real. O fine-tune rende pela adaptação do encoder. O mmBERT-base original, com uma cabeça de rótulos sem treino, dá F1 de 0,0005: ruído, como se esperava.
Retreinamos também o v1.2 e o v1.3 com as sementes 7 e 21. Com a guarda, o v1.2 vai de 0,760 a 0,830 nas 305 ementas; o v1.3, destilado do mesmo professor, fica entre 0,812 e 0,818. No estilo do desafio as seis sementes dão o mesmo JSON final no dev e 1,09999 no estresse com a guarda.
Todo número de texto real aqui usa um gabarito de LLM adjudicado, e cada concorrente teve uma semente. Um teste com gabarito humano, num conjunto que não participou de nenhuma escolha, ainda não foi feito. Desenho e intervalos: controles.
v1.3, destilado do v1.2:
0,5·CE(ouro) + T²·KL(professor/T ‖ aluno/T) + 0,5·KL(professor ‖ aluno), T = 2,
com o v1.2 rodando no próprio treino. O aluno copia as probabilidades, não só o rótulo,
porque a solução usa a confiança do modelo.celsowm/jurisprudencias_br,
que o Celso F. publica sob CC-BY-4.0 com decisões do STF, do STJ e do TJRJ coletadas pelo
Juriscraper; nenhuma delas está nos testes. Foi nesse texto que o limite do v1.2 fora dos
moldes apareceu, e é nele que o v1.3 aprende a reproduzir a confiança do professor fora do
estilo do desafio, que é de onde a guarda lê.max_len 2048, lote 8, taxa de aprendizado 5e-5, semente 13.treino/destilar.py no GitHub.v1.2, o professor:
jhu-clsp/mmBERT-base (MIT),
revisão c5955035435e2bf121cde7f3c8863ef52ff35d82.max_len 1024, lote 8, taxa de aprendizado 5e-5, semente 13.treino/treinar.py no GitHub.O comando exato de cada passo, dos bancos de frases aos pesos, está no guia de reprodução.
A revisão acima é fixa e é a que a solução usa. A inferência é determinística (eval(),
sem amostragem, algoritmos determinísticos do torch) e dá a mesma saída em GPU e em CPU.
Os dados de treino estão em vinimlo/gama-goldenset,
nas revisões 31474b1 (v1.2) e ec430c0 (v1.3).
A lista comentada está em literatura.
@misc{gama2026,
title = {Gama: extrator de citações jurídicas do Desafio Caça-Alucinações (BRACIS 2026 × Jusbrasil)},
author = {Melo, Vinícius and Siron, Gabriel},
year = {2026},
howpublished = {\url{https://huggingface.co/vinimlo/gama}},
note = {v1.3, revisão 5f924ca}
}
Vinícius Melo e Gabriel Siron, para o Desafio Caça-Alucinações (BRACIS 2026 × Jusbrasil).
Gama is a token-classification model (BIO: JURIS case-law citations, LEI statute
provisions, VAGA unnumbered references by court, year and rapporteur) for Brazilian
Portuguese legal documents. Version v1.3 keeps the first 12 of the 22 layers of v1.2
(fine-tuned from mmBERT-base on 6,000 synthetic documents) and is distilled from it: inside
the full pipeline it gives the same outputs on challenge-style documents, the same level on
real court summaries (within seed variance), and runs 1.5 times faster. It only locates
citations; whether a citation exists is decided by an exact lookup in a frozen collection of
decisions. On challenge-style documents it clearly beats zero-shot open models (Qwen3-8B,
GLiNER 2.5); on real court summaries outside that style, a zero-shot Qwen3-8B extracts
better than the model alone at about 330 times the cost, and the pipeline's guard (falling
back to rules where the model is unsure) puts Gama back ahead. Trained on the same synthetic
data, BERTimbau and a fine-tuned GLiNER 2.5 extract better than the model alone on real court
summaries, but neither beats v1.3 once the guard is on (GLiNER 2.5 reaches the same level
with no guard at all, within one seed); with a frozen encoder and only the output layer
trained, mmBERT-base collapses on real text (F1 0.243), so the gain comes from adapting the
encoder. Real-text gold labels are LLM-adjudicated, not human. The full pipeline runs in
Docker from the GitHub repository with bash run.sh --preparar (network, once) and
bash run.sh <db_path> <txt_folder> <output.csv> (offline). Built for the BRACIS 2026 ×
Jusbrasil hallucination detection challenge.
Extrator de citações jurídicas em português do Brasil. Lê um documento judicial e marca onde está cada citação de jurisprudência e de lei, com o tipo de cada uma. É o extrator da nossa solução para o Desafio Caça-Alucinações (BRACIS 2026 × Jusbrasil), que verifica se as citações de um texto existem de verdade num acervo de decisões.
O nome homenageia Luiz Gama (1830–1882), advogado abolicionista que libertou centenas de pessoas nos tribunais citando a lei com precisão.
Esta é a versão v1.3: as 12 primeiras das 22 camadas do v1.2, treinadas para imitar as
probabilidades do v1.2 (destilação). Dentro da solução, a saída no estilo dos documentos do
desafio é a mesma do v1.2; em texto real fica no mesmo nível, e roda 1,5 vez mais rápido.
O v1.2 continua disponível na tag v1.2.
Código, pipeline completo e experimentos: github.com/vinimlo/gama.
Classificação de tokens no esquema BIO, com três tipos:
| Rótulo | O que marca | Exemplo |
|---|---|---|
JURIS | precedente identificado por número, súmula ou tema | REsp nº 1.234.567/SP, Súmula 7 do STJ |
LEI | dispositivo de lei, com o nome do diploma | art. 927 do Código de Processo Civil |
VAGA | julgado citado sem número: tribunal, ano e relator | julgado do STJ proferido em 2021 pela relatoria de Nancy Andrighi |
As bordas seguem as convenções do gabarito do desafio: artigo e preposição antes da citação ficam fora, pontuação depois fica fora, a UF no fim do número entra.
O modelo só diz onde a citação está. Se ela existe ou foi inventada, quem decide é uma consulta exata ao acervo, feita pelo resto do pipeline. Um modelo que decidisse isso sozinho estaria chutando com fluência, que é justamente a alucinação que se quer pegar.
O repositório traz o bio.py, o mesmo módulo que rotulou o treino e decodifica a saída:
import importlib.util
import torch
from huggingface_hub import hf_hub_download
from transformers import AutoModelForTokenClassification, AutoTokenizer
REPO, REV = "vinimlo/gama", "5f924ca2fa77c2aae6afe6d770c78ca4438f52f3"
tok = AutoTokenizer.from_pretrained(REPO, revision=REV)
modelo = AutoModelForTokenClassification.from_pretrained(REPO, revision=REV).eval()
spec = importlib.util.spec_from_file_location("bio", hf_hub_download(REPO, "bio.py", revision=REV))
bio = importlib.util.module_from_spec(spec)
spec.loader.exec_module(bio)
texto = ("Nesse sentido, invoca-se o REsp nº 1.234.567/SP e o art. 927 do Código de Processo Civil, "
"bem como o julgado do STJ proferido em 2021 pela relatoria de Nancy Andrighi.")
enc = tok(texto, return_offsets_mapping=True, return_tensors="pt")
with torch.no_grad():
rotulos = modelo(input_ids=enc["input_ids"],
attention_mask=enc["attention_mask"]).logits[0].argmax(-1).tolist()
offsets = [tuple(o) for o in enc["offset_mapping"][0].tolist()]
for ini, fim, tipo in bio.decodificar(offsets, rotulos, texto):
print(tipo, texto[ini:fim])
# JURIS REsp nº 1.234.567/SP
# LEI art. 927 do Código de Processo Civil
# VAGA julgado do STJ proferido em 2021 pela relatoria de Nancy Andrighi
O contexto é de 8.192 tokens, o bastante para a maioria dos documentos numa passada só.
Para documentos maiores, para a confiança por citação e para a versão mais recente do
bio.py, use o extrator do repositório no GitHub (src/gama/extratores/neural.py).
Os pesos sozinhos só acham as citações. A solução inteira, com a guarda, a consulta ao acervo e a confiança calibrada, roda em Docker a partir do repositório no GitHub:
git clone https://github.com/vinimlo/gama && cd gama
bash run.sh --preparar # com rede, uma vez
bash run.sh <caminho_db> <pasta_txt> <arquivo_saida.csv> # sem rede
O primeiro comando constrói a imagem e baixa estes pesos na revisão fixa. O segundo lê a
base SQLite do acervo e uma pasta de .txt, roda sem rede e grava o CSV no formato da
submissão, com um JSON por documento ao lado. Usa a GPU se o Docker tiver o runtime NVIDIA;
sem ela roda em CPU, a cerca de 1 s por documento.
Na solução, o modelo não trabalha sozinho. Entre ele e o resto do pipeline há uma guarda, e os números "com a guarda" desta página são dela. Quem usar só os pesos obtém o "Gama sozinho" das tabelas.
A guarda existe porque o modelo foi treinado no estilo dos documentos do desafio e, fora dele, erra de um jeito previsível. Em ementa real ele pode tomar o "Rel. Min. Fulano, julgado em ..." que vem logo depois de um precedente numerado por uma referência vaga, ou marcar pedaços soltos como "Rel", "2011" e "DJe". Mas ele sabe quando hesita. O que separa esses erros dos acertos é a confiança do span: a média, sobre os tokens rotulados como citação, da probabilidade que o modelo deu ao rótulo escolhido. No estilo do desafio, o menor dos 4.447 acertos do v1.3 no estresse difícil tem 0,9993 (com o v1.2, nenhum de 34.171 acertos ficou abaixo de 0,98). Os falsos positivos do v1.3 em ementa têm mediana de 0,83.
São duas regras, nesta ordem:
VAGA a até 2 caracteres de outro span sai. No molde do desafio, a referência vaga é uma
frase própria, e a mais próxima de outra citação fica a 4 caracteres.No estilo do desafio nenhuma das duas dispara. Em 4.626 documentos, a saída é a mesma com e
sem guarda, e a nota oficial não se mexe. Fora dele, sim. Nas 305 ementas do benchmark, o
F1 de extração vai de 0,620 a 0,818. Num teste de 172 ementas sorteadas depois, fora de tudo
o que foi usado para escolher as regras, o ganho foi +0,205 (IC95 +0,154 a +0,246). A
implementação está em src/gama/extratores/guarda.py no GitHub; o extrator da solução,
--extrator neural, já a inclui.
Solução completa (extrator, guarda, resolver, confiança calibrada) com cada versão do modelo.
| v1.2 | v1.3 | |
|---|---|---|
| Dev, estresse difícil e 4.000 sintéticos (4.626 documentos), métrica oficial | 1,10000 / 1,09999 / 1,09999 | iguais, com o JSON final idêntico documento a documento |
| Texto real, F1 de extração, 172 ementas separadas antes da escolha da guarda | 0,820 | 0,839 (+0,019; IC95 +0,003 a +0,033) |
| Tempo por documento, NVIDIA L4 / CPU, medidos lado a lado | 0,071 s / 1,62 s | 0,046 s / 0,93 s |
| Parâmetros / pesos em FP32 | 307,5M / 1,23 GB | 257,4M / 1,03 GB |
As 172 ementas foram separadas antes da escolha da guarda e mediram a guarda uma vez só. Depois elas também serviram para comparar os alunos e escolher o v1.3, então para a diferença entre v1.2 e v1.3 são dado de desenvolvimento, não um teste independente. E retreinado com outras sementes o v1.2 varia mais do que essa diferença (0,785 a 0,842 nas 172, com a guarda).
Métrica oficial do desafio, macroF1 · (1 − 0,5·τ) · (1 + 0,10·(1 − Brier)), com o mesmo
resolver para todos os extratores. O teto prático é 1,10.
| Conjunto | Gama v1.3 | Gama v1.3 com a guarda da solução | Régua (regex) | Qwen3-8B zero-shot | GLiNER 2.5 zero-shot |
|---|---|---|---|---|---|
| Dev da organização, 26 documentos | 1,10000 | 1,10000 | 1,09848 | não medido¹ | não medido¹ |
| Estresse difícil, 600 documentos, métrica oficial | 1,09999 | 1,09999 | 0,84880 | 0,81065 | 0,44445 |
| Texto real, 305 ementas, F1 de extração | 0,620 | 0,818 | 0,663 | 0,707 | 0,608 |
| Tempo por documento, NVIDIA L4 | 0,035 s | 0,037 s | 0,001 s | 11,7 s | 0,110 s |
¹ Os documentos do dev são da organização e não saem da nossa máquina.
O estresse difícil segue o estilo dos documentos do desafio, com frases escritas por um
LLM que nenhum modelo viu no treino e ruído de OCR forte. Nele o fine-tune é a diferença:
o melhor extrator sem treino fica abaixo até da régua. O texto real são ementas do STF,
STJ e TJRJ (celsowm/jurisprudencias_br,
CC-BY-4.0), com citações anotadas por dois LLMs e adjudicadas por critério escrito. Ali a
ordem se inverte: um LLM genérico de 8B extrai melhor que o modelo sozinho, a um custo
cerca de 330 vezes maior por documento. A guarda da solução troca os trechos em que o
modelo hesita (confiança < 0,95) pelos da régua e põe o Gama de volta na frente.
Na fase de treino do Kaggle, a solução completa com o v1.2 marcou 1,09999.
O gráfico acima compara o Gama com extratores sem treino. Para separar o que vem do fine-tune do que vem da escolha do modelo, treinamos concorrentes nos mesmos documentos sintéticos do v1.2 e medimos todos pelo mesmo harness, sozinhos e com a guarda da solução. F1 de extração em texto real; as 305 ementas do benchmark escolheram os limiares, e as 172 separadas depois só confirmam.
| Texto real sozinho, 305 / 172 | Texto real com a guarda, 305 / 172 | Estresse difícil sozinho, métrica oficial | |
|---|---|---|---|
| Gama v1.3 | 0,620 / 0,635 | 0,818 / 0,839 | 1,09999 |
| BERTimbau, mesma receita do v1.2 | 0,697 / 0,705 | 0,813 / 0,826 | 1,09999 |
| GLiNER 2.5 multi treinado | 0,808 / 0,824 | 0,816 / 0,837 | 1,09615 |
| mmBERT-base com o encoder congelado, só a saída treinada | 0,243 / 0,247 | 0,350 / 0,353 | 0,90182 |
Sozinhos, BERTimbau e GLiNER treinados extraem melhor que o Gama em texto real. Com a guarda, nenhum passa o v1.3. O GLiNER 2.5 treinado chega ao mesmo nível sem guarda nenhuma (−0,010 nas 305 e −0,015 nas 172, IC95 cruzando zero), porque já generaliza fora do molde; a guarda quase não o ajuda, já que a maior parte dos erros dele sai com score acima de 0,99. O Gama especializa no estilo do desafio e compensa sabendo quando hesita, e no estilo do desafio segue na frente (1,09999 contra 1,09615). Uma semente por modelo: não é equivalência. Até 29/09 estas tabelas usavam o GLiNER multi v2.1, que era pior nos dois papéis.
O encoder congelado vai longe no estilo do desafio e desaba em texto real. O fine-tune rende pela adaptação do encoder. O mmBERT-base original, com uma cabeça de rótulos sem treino, dá F1 de 0,0005: ruído, como se esperava.
Retreinamos também o v1.2 e o v1.3 com as sementes 7 e 21. Com a guarda, o v1.2 vai de 0,760 a 0,830 nas 305 ementas; o v1.3, destilado do mesmo professor, fica entre 0,812 e 0,818. No estilo do desafio as seis sementes dão o mesmo JSON final no dev e 1,09999 no estresse com a guarda.
Todo número de texto real aqui usa um gabarito de LLM adjudicado, e cada concorrente teve uma semente. Um teste com gabarito humano, num conjunto que não participou de nenhuma escolha, ainda não foi feito. Desenho e intervalos: controles.
v1.3, destilado do v1.2:
0,5·CE(ouro) + T²·KL(professor/T ‖ aluno/T) + 0,5·KL(professor ‖ aluno), T = 2,
com o v1.2 rodando no próprio treino. O aluno copia as probabilidades, não só o rótulo,
porque a solução usa a confiança do modelo.celsowm/jurisprudencias_br,
que o Celso F. publica sob CC-BY-4.0 com decisões do STF, do STJ e do TJRJ coletadas pelo
Juriscraper; nenhuma delas está nos testes. Foi nesse texto que o limite do v1.2 fora dos
moldes apareceu, e é nele que o v1.3 aprende a reproduzir a confiança do professor fora do
estilo do desafio, que é de onde a guarda lê.max_len 2048, lote 8, taxa de aprendizado 5e-5, semente 13.treino/destilar.py no GitHub.v1.2, o professor:
jhu-clsp/mmBERT-base (MIT),
revisão c5955035435e2bf121cde7f3c8863ef52ff35d82.max_len 1024, lote 8, taxa de aprendizado 5e-5, semente 13.treino/treinar.py no GitHub.O comando exato de cada passo, dos bancos de frases aos pesos, está no guia de reprodução.
A revisão acima é fixa e é a que a solução usa. A inferência é determinística (eval(),
sem amostragem, algoritmos determinísticos do torch) e dá a mesma saída em GPU e em CPU.
Os dados de treino estão em vinimlo/gama-goldenset,
nas revisões 31474b1 (v1.2) e ec430c0 (v1.3).
A lista comentada está em literatura.
@misc{gama2026,
title = {Gama: extrator de citações jurídicas do Desafio Caça-Alucinações (BRACIS 2026 × Jusbrasil)},
author = {Melo, Vinícius and Siron, Gabriel},
year = {2026},
howpublished = {\url{https://huggingface.co/vinimlo/gama}},
note = {v1.3, revisão 5f924ca}
}
Vinícius Melo e Gabriel Siron, para o Desafio Caça-Alucinações (BRACIS 2026 × Jusbrasil).
Gama is a token-classification model (BIO: JURIS case-law citations, LEI statute
provisions, VAGA unnumbered references by court, year and rapporteur) for Brazilian
Portuguese legal documents. Version v1.3 keeps the first 12 of the 22 layers of v1.2
(fine-tuned from mmBERT-base on 6,000 synthetic documents) and is distilled from it: inside
the full pipeline it gives the same outputs on challenge-style documents, the same level on
real court summaries (within seed variance), and runs 1.5 times faster. It only locates
citations; whether a citation exists is decided by an exact lookup in a frozen collection of
decisions. On challenge-style documents it clearly beats zero-shot open models (Qwen3-8B,
GLiNER 2.5); on real court summaries outside that style, a zero-shot Qwen3-8B extracts
better than the model alone at about 330 times the cost, and the pipeline's guard (falling
back to rules where the model is unsure) puts Gama back ahead. Trained on the same synthetic
data, BERTimbau and a fine-tuned GLiNER 2.5 extract better than the model alone on real court
summaries, but neither beats v1.3 once the guard is on (GLiNER 2.5 reaches the same level
with no guard at all, within one seed); with a frozen encoder and only the output layer
trained, mmBERT-base collapses on real text (F1 0.243), so the gain comes from adapting the
encoder. Real-text gold labels are LLM-adjudicated, not human. The full pipeline runs in
Docker from the GitHub repository with bash run.sh --preparar (network, once) and
bash run.sh <db_path> <txt_folder> <output.csv> (offline). Built for the BRACIS 2026 ×
Jusbrasil hallucination detection challenge.