Restauração de pontuação e capitalização em português com BERT
Textos de ASR, legendagem e OCR frequentemente chegam sem pontuação ou capitalização. Para restaurar essas marcas em português, publiquei o checkpoint dominguesm/bert-restore-punctuation-ptbr e o pacote respunct. No repositório mantenho também a documentação pública do projeto, com dashboard de métricas em W&B e página no Hub, além da interface de uso [2]. A principal contribuição é combinar classificação de tokens com uma interface que segmenta textos longos, executa inferência e reconstrói o resultado [1, 2].
Publiquei dois artefatos complementares. O modelo é um BertForTokenClassification com pesos, tokenizer, configuração e 15 classes no Hugging Face Hub; ele pode ser carregado por ferramentas compatíveis com Transformers. O pacote Python, na versão 1.0.0, usa simpletransformers==0.63.7 e torch==1.12.0, normaliza a entrada e interpreta as classes para devolver uma string [2]. Essa separação importa: resultados, licença, compatibilidade, pré-processamento e riscos operacionais não são intercambiáveis.
O problema como classificação de tokens
Considere uma entrada normalizada com palavras . Para cada posição , o objetivo é escolher em um vocabulário de 15 rótulos. O encoder BERT produz uma representação contextual ; uma camada linear gera 15 logits, e uma softmax os transforma em probabilidades:
Durante o ajuste fino, a perda usual é a entropia cruzada por token válido. Para um lote com máscara mi, que vale 0 em padding, tokens especiais e subtokens ignorados, e 1 nos tokens supervisionados:
Não se trata de geração de texto autoregressiva. Todas as posições de uma janela são codificadas bidirecionalmente e classificadas; o texto final é montado por regras. Isso aproveita o contexto à esquerda e à direita, útil para decidir, por exemplo, se “mais tarde” inicia uma sentença ou apenas continua uma oração. Para cada decisão final, usei o argmax local de uma classe; não implementei CRF, decodificação global ou restrições gramaticais entre rótulos [1].
Renderizando diagrama...
Taxonomia: pontuação e caixa em uma classe
No checkpoint, defini o primeiro caractere para codificar o sinal posterior e o segundo para codificar a caixa. O significa ausência; U, inicial maiúscula. Assim, OO mantém uma palavra minúscula sem sinal, OU capitaliza sem pontuar, ,O acrescenta vírgula sem capitalizar e .U combina inicial maiúscula com ponto posterior. Usei os seguintes rótulos [1]:
| ID | Rótulo | Operação |
|---|---|---|
| 0 | OU | maiúscula, sem sinal |
| 1 | OO | minúscula, sem sinal |
| 2–4 | .O, !O, ,O | ponto, exclamação ou vírgula |
| 5–7 | .U, !U, ,U | maiúscula mais o sinal |
| 8–10 | :O, ;O, :U | dois-pontos, ponto e vírgula, ou maiúscula mais dois-pontos |
| 11–14 | 'O, -O, ?O, ?U | apóstrofo, hífen, interrogação, ou maiúscula mais interrogação |
Não há todas as combinações cartesianas possíveis: não registrei, por exemplo, ;U, 'U e -U. Também não existe uma classe para preservar caixa mista, siglas ou a grafia original; o wrapper primeiro converte tudo para minúsculas.
Quando publiquei o dataset tabular, usei nomes como B-UO, B-OO, B-O., B-U, e B-O? [3]. O prefixo B- é uma convenção do campo ner_tags, não uma entidade linguística. Além disso, no dataset escrevi caixa antes do sinal, enquanto no checkpoint escrevi sinal antes da caixa: B-U, corresponde semanticamente a ,U, e B-O. a .O. Tratar os nomes como idênticos sem esse mapeamento permutaria classes.
De texto pontuado a exemplos alinhados
Usei o WikiLingua no ajuste fino [1]. O corpus foi criado originalmente para sumarização abstrativa multilíngue: cerca de 770 mil pares artigo–resumo de guias WikiHow em 18 idiomas, com 81.695 pares em português paralelos ao inglês; os alinhamentos entre línguas usam as imagens dos passos dos artigos [4, 5]. Para restauração de pontuação, aproveitei o texto humano que já contém caixa e sinais, não a tarefa de sumarização em si.
Preparei os exemplos supervisionados em quatro operações: separei o texto original em palavras; registrei se cada palavra começava em maiúscula; desloquei o sinal seguinte para o rótulo da própria palavra; e forneci ao modelo a versão sem sinal e em minúsculas. Por exemplo:
Original: Henrique voltou, Pedro saiu.
Entrada: henrique voltou pedro saiu
Rótulos: OU ,O OU .O
Reconstrução: Henrique voltou, Pedro saiu.Há dois alinhamentos diferentes. Primeiro, o alinhamento entre palavra e anotação deve sobreviver à remoção da pontuação: aspas, abreviações, números, apóstrofos e hífens tornam uma simples divisão por espaços insuficiente. Segundo, o tokenizer WordPiece pode decompor uma palavra em vários subtokens. A prática documentada para classificação de tokens é mapear subtokens à palavra de origem, supervisionar apenas o primeiro e marcar tokens especiais e continuações com -100, para que não entrem na entropia cruzada [6]. Replicar um rótulo de pontuação em todos os subtokens altera a frequência das classes e pode inserir o mesmo sinal várias vezes na reconstrução.
Em 2023, publiquei também o restore-punctuation-ptbr-dataset, com text, tokens e ner_tags: 4.767.824 linhas de treino, 595.978 de validação e 595.978 de teste [3]. Naquela publicação, registrei apenas o esquema e as contagens; não documentei fonte, licença, deduplicação, algoritmo de tokenização, semente nem a relação exata com os splits usados em 2022. Por isso, não apresento os Parquets de 2023 como uma reprodução exata do corpus do primeiro treino.
Renderizando diagrama...
BERTimbau e ajuste fino
BERT pré-treina um encoder Transformer profundamente bidirecional com linguagem mascarada e, na formulação original, previsão da próxima sentença. O resultado pode ser adaptado a tarefas por token com uma camada de saída adicional e ajuste fino conjunto [7, 8]. Eu parti do BERTimbau Base cased, BERT pré-treinado para português brasileiro sobre o brWaC por um milhão de passos com whole-word masking [9, 10]. “Cased” significa que seu vocabulário e pré-treino preservam caixa e acentos; isso não impede que a entrada desta tarefa seja deliberadamente convertida para minúsculas, pois a caixa original é justamente parte do alvo.
Configurei o restaurador com 12 camadas, 12 cabeças de atenção, dimensão oculta 768, camada intermediária 3.072, vocabulário de 29.794 itens, dropout 0,1 e posições absolutas até 512 tokens. Sobre o encoder, usei BertForTokenClassification com 15 saídas. Publiquei 108.344.591 parâmetros em Safetensors, valor muito próximo da designação arredondada de 110 milhões do BERTimbau Base [1, 10].
Treinei por três épocas com AdamW, taxa de aprendizado 4e-5, scheduler linear com warmup, 570 passos de aquecimento (warmup_ratio=0.06), lote 8 no treino, 16 na avaliação, sequências de até 512, FP16 e uma GPU. Avaliei durante o treino e não usei early stopping, semente manual nem ponderação de classes registrada. O último ponto que preservei no CSV está no passo 9.498 [1]. Para uma reprodução bit a bit, ainda faltam o script de construção, a versão exata do corpus, a divisão usada e a semente, que não registrei.
Como o pacote reconstrói o texto
Em RestorePuncts.restore_puncts, implementei a seguinte sequência [2]:
prepare_textremove espaços externos, troca quebras de linha por espaços, converte para minúsculas, apaga os caracteres destring.punctuatione colapsa espaços repetidos.split_textcria janelas de 250 palavras por padrão, com sobreposição de 20, para reduzir o risco de ultrapassar 512 WordPieces.NERModel.predictclassifica as janelas e devolve um dicionário por palavra.make_resultsdescarta as últimas 20 predições de todas as janelas exceto a final, capitaliza quando o segundo caractere éU, anexa o primeiro caractere quando ele não éOe junta tudo com espaços.
Esse descarte escolhe uma única previsão nas regiões sobrepostas e evita duplicação, mas não combina probabilidades nem reconcilia divergências entre janelas. O modelo vê contexto limitado; “texto arbitrariamente grande” significa processamento em partes, não atenção global.
API do pacote
A instalação e o uso documentados são:
python -m pip install respunct==1.0.0from respunct import RestorePuncts
restaurador = RestorePuncts(
words_per_pred=250,
overlap=20,
use_cuda=False,
)
texto = (
"henrique foi no lago pescar com o pedro mais tarde "
"foram para a casa do pedro fritar os peixes"
)
print(restaurador.restore_puncts(texto))
# Henrique foi no lago pescar com o Pedro. Mais tarde, foram para a casa do Pedro fritar os peixes.Embora eu tenha mencionado GPU quando disponível na docstring, defini use_cuda=False como padrão no construtor; é preciso passar True explicitamente e ter uma instalação CUDA compatível. words_per_pred deve ser maior que overlap. Como contei o corte em palavras e o limite do BERT usa WordPieces, 250 não garante matematicamente ficar abaixo de 512 em texto com muitos termos raros. As dependências fixadas são antigas e podem não instalar em versões recentes de Python ou em plataformas sem wheel para PyTorch 1.12; isolar esse runtime em container ou ambiente virtual é mais seguro do que relaxar versões sem teste.
O dataset que publiquei depois é outro produto e outra API. Como são aproximadamente 4,27 GB para download e 11,8 GB após preparação segundo o Hub, streaming=True é útil para inspeção reprodutível:
from datasets import load_dataset
dados = load_dataset(
"dominguesm/restore-punctuation-ptbr-dataset",
split="train",
streaming=True,
)
exemplo = next(iter(dados))
assert len(exemplo["tokens"]) == len(exemplo["ner_tags"])Avaliação: 96% não resume o modelo
No README, publiquei um relatório por classe com 1.047.818 tokens [1, 2]:
| Classe | Precisão | Recall | F1 | Suporte |
|---|---|---|---|---|
OU | 0,89 | 0,91 | 0,90 | 69.376 |
OO | 0,99 | 0,98 | 0,98 | 857.659 |
.O | 0,86 | 0,93 | 0,89 | 60.410 |
,O | 0,85 | 0,83 | 0,84 | 48.608 |
,U | 0,73 | 0,76 | 0,75 | 3.521 |
?O | 0,68 | 0,78 | 0,73 | 1.168 |
.U | 0,66 | 0,72 | 0,69 | 1.884 |
:U | 0,59 | 0,63 | 0,61 | 352 |
:O | 0,70 | 0,53 | 0,60 | 2.420 |
?U | 0,50 | 0,56 | 0,53 | 36 |
!U | 0,38 | 0,32 | 0,34 | 38 |
!O | 0,30 | 0,05 | 0,08 | 783 |
;O | 0,35 | 0,04 | 0,08 | 1.557 |
'O | 0,00 | 0,00 | 0,00 | 3 |
-O | 0,00 | 0,00 | 0,00 | 3 |
| macro avg | 0,57 | 0,54 | 0,54 | 1.047.818 |
| weighted avg | 0,96 | 0,96 | 0,96 | 1.047.818 |
Precisão, recall e F1 para uma classe são:
Para .O, os valores arredondados dão
coerente com 0,89. Com suporte 60.410 e recall 0,93, isso representa aproximadamente 56.181 verdadeiros positivos e 4.229 falsos negativos; dividindo por precisão 0,86, há cerca de 65.327 previsões de ponto, portanto 9.146 falsos positivos. São aproximações porque a tabela arredonda as taxas.
O desequilíbrio é decisivo. OO sozinho corresponde a
dos tokens. A média weighted pondera cada F1 pelo suporte e, por isso, fica próxima de 0,96, assim como a acurácia. A média macro dá o mesmo peso às 15 classes e cai para 0,54: ela expõe que um sistema excelente em “não inserir nada” pode ser fraco nos sinais raros. Exclamação e ponto e vírgula têm F1 0,08; apóstrofo e hífen têm somente três casos cada e nenhum acerto. Não há base estatística para prometer restauração confiável desses sinais.
Publiquei três grupos de resultados que não devem ser misturados: a tabela arredondada acima; o eval_results.txt, com precisão 0,5885, recall 0,5451 e F1 0,5660; e o model-index, com 57,72%, 53,83% e 55,70%. Não registrei por que eles diferem, qual forma de média cada exportação usa ou qual etapa do treino gerou cada número. Para comparação científica, fixe a versão do modelo, o split, o pré-processamento e a implementação da métrica, e reporte ao menos macro-F1, F1 por sinal, matriz de confusão e intervalos de confiança.
Reprodutibilidade e implantação
Uma reprodução confiável deve registrar as versões do modelo, do tokenizer, dos dados e das bibliotecas, além do mapeamento das 15 classes, regra de alinhamento de subtokens, semente, hardware e política de janelas. Deve ainda testar invariantes: uma saída por palavra, nenhum sinal duplicado na sobreposição e comportamento conhecido para palavras que viram vários WordPieces. O artefato contém cerca de 108,3 milhões de parâmetros; em FP32, só os pesos ocupam aproximadamente 413 MiB, além de ativações e runtime. Meça memória, latência e throughput no hardware real, com comprimentos representativos, em vez de inferi-los do tamanho do arquivo.
Em serviço, carregue uma única instância por worker, limite tamanho e concorrência, aplique timeout e faça batching apenas após medir a latência. Monitore distribuição das classes, textos truncados, tamanho em WordPieces e mudança de domínio. Para ASR em tempo real, a bidirecionalidade exige contexto futuro; uma estratégia de janela móvel introduz atraso e pode revisar pontuação perto da borda. O wrapper de 2022 é simples, não um servidor pronto com fila, observabilidade ou controle de recursos.
As licenças também pertencem a camadas diferentes: publiquei o respunct sob Apache-2.0 e o checkpoint sob CC BY 4.0; o BERTimbau Base usa MIT; e o conteúdo WikiHow descrito pelo WikiLingua usa CC BY-NC-SA 3.0 com requisitos de atribuição [1, 2, 5, 10]. Não defini uma licença no dataset de 2023. Uso comercial ou redistribuição exige revisar cada artefato e a proveniência efetivamente adotada, sem assumir que a licença do código cobre pesos e dados.
Comportamentos conhecidos, privacidade e uso responsável
O pré-processamento que implementei é destrutivo: ele apaga toda pontuação ASCII e caixa antes da inferência. Logo, não “corrige somente o necessário”; pode substituir pontuação já correta, perder siglas, URLs, e-mails, código, emoticons, decimais e convenções como COVID-19. Na reconstrução, junto tokens com espaços e anexo apóstrofo e hífen à palavra anterior; construções que exigem ligação sem espaço ao token seguinte merecem testes específicos. A função capitalize() preserva acentos, mas reduz o restante da palavra a minúsculas, inadequado para caixa interna.
Outras limitações que observei vêm do domínio. WikiHow é texto instrucional revisado, diferente de fala espontânea, chats, prontuários, petições ou transcrições com hesitações. Pontuação muda significado e intenção: transformar uma afirmação em pergunta, ou deslocar uma vírgula, pode alterar uma decisão. Números altos nas classes frequentes não autorizam uso autônomo em medicina, justiça, acessibilidade ou publicação. Nesses cenários, mantenha o original, mostre diferenças e confiança, estabeleça revisão humana e avalie um conjunto local que inclua grupos, sotaques, nomes e gêneros textuais relevantes.
Implementei a inferência para baixar o modelo do Hub e processar o texto localmente, sem enviá-lo a uma API externa [2]. Ainda assim, cache, logs, traces, notebooks e filas podem expor dados pessoais. Minimize retenção, mascare identificadores antes de enviá-los aos sistemas de observabilidade, controle o acesso ao cache e evite registrar entrada e saída completas. Se houver ajuste fino com dados privados, documente base legal, consentimento, período de retenção e risco de memorização. Restauração de pontuação melhora legibilidade; não verifica fatos, corrige transcrição nem remove conteúdo sensível.
Conclusão
Com este trabalho, publiquei um checkpoint e um pacote que transformam a restauração de pontuação em uma etapa reutilizável de classificação de tokens. Ao combinar o modelo com segmentação e reconstrução de textos longos, aprendi que métricas agregadas precisam ser acompanhadas por avaliação por classe e pelo contexto real de uso.
Referências
- Domingues, M. bert-restore-punctuation-ptbr: model card, config, model args e resultados. Hugging Face, 2022. Modelo e arquivos.
- Domingues, M. respunct. GitHub e PyPI, versão 1.0.0, 2022. Código-fonte e pacote.
- Domingues, M. restore-punctuation-ptbr-dataset. Hugging Face, 2023. Cartão e Parquets.
- Ladhak, F.; Durmus, E.; Cardie, C.; McKeown, K. WikiLingua: A New Benchmark Dataset for Cross-Lingual Abstractive Summarization. Findings of EMNLP, 2020. DOI.
- Durmus, E. et al. WikiLingua repository and dataset description. GitHub.
- Hugging Face. Token classification: preprocessing and label alignment. Documentação Transformers.
- Devlin, J.; Chang, M.-W.; Lee, K.; Toutanova, K. BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding. NAACL-HLT, 2019. arXiv.
- Google Research. BERT source code and documentation. GitHub.
- Souza, F.; Nogueira, R.; Lotufo, R. BERTimbau: Pretrained BERT Models for Brazilian Portuguese. BRACIS, 2020, p. 403–417. DOI.
- NeuralMind. BERTimbau Base model card and Portuguese BERT repository. Hugging Face e GitHub.
- Domingues, M. respunct README. GitHub, 2022. Dashboard e página oficial do projeto.
BibTeX
@inproceedings{devlin2019bert,
title = {BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding},
author = {Devlin, Jacob and Chang, Ming-Wei and Lee, Kenton and Toutanova, Kristina},
booktitle = {Proceedings of NAACL-HLT 2019},
pages = {4171--4186},
year = {2019},
doi = {10.18653/v1/N19-1423}
}
@inproceedings{ladhak2020wikilingua,
title = {WikiLingua: A New Benchmark Dataset for Cross-Lingual Abstractive Summarization},
author = {Ladhak, Faisal and Durmus, Esin and Cardie, Claire and McKeown, Kathleen},
booktitle = {Findings of the Association for Computational Linguistics: EMNLP 2020},
pages = {4034--4048},
year = {2020},
doi = {10.18653/v1/2020.findings-emnlp.360}
}
@inproceedings{souza2020bertimbau,
title = {BERTimbau: Pretrained BERT Models for Brazilian Portuguese},
author = {Souza, F{\'a}bio and Nogueira, Rodrigo and Lotufo, Roberto},
booktitle = {Intelligent Systems: BRACIS 2020},
series = {Lecture Notes in Computer Science},
volume = {12319},
pages = {403--417},
publisher = {Springer},
year = {2020},
doi = {10.1007/978-3-030-61377-8_28}
}
@software{domingues2022respunct,
author = {Domingues, Maicon},
title = {respunct: Restore punctuation of Portuguese texts},
version = {1.0.0},
year = {2022},
url = {https://github.com/DominguesM/respunct}
}