Classificador de NCM pela descrição do produto — BERTimbau + ONNX

Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação é do contribuinte/contador.

Este modelo é um auxiliar de triagem. Quando não tem confiança suficiente, ele se abstém — isso é um recurso de segurança, não uma falha. Nenhuma saída deste modelo substitui o parecer de um contador habilitado (CFC Res. 560/83).


O que este modelo faz

Ele recebe a descrição de um item de nota fiscal do jeito que ela realmente sai do ERP — telegráfica, abreviada, sem pontuação, tipo "PARAF SEXT ZINC M8X40 DIN933" — e sugere os 3 códigos NCM de 8 dígitos mais prováveis, cada um com uma confiança calibrada. Quando a confiança não basta para uma decisão automática, o modelo sinaliza abstenção: o top-3 continua visível, mas marcado como "isto precisa de revisão humana".

O fluxo completo, na prática:

descrição do item na NF-e  →  código NCM sugerido  →  texto oficial da tabela NCM
"PARAF SEXT ZINC M8X40..."    73181500 (98,3%)        "Parafusos, pinos ou pernos,
                                                       roscados... de ferro ou aço"

Por trás desse resultado: neuralmind/bert-large-portuguese-cased (BERTimbau Large) com uma cabeça de classificação linear sobre 9.748 códigos NCM de 8 dígitos — 344,4M de parâmetros no total (24 camadas, hidden 1024; contagem feita no checkpoint). O ajuste fino foi feito exclusivamente com dados públicos e processamento local (detalhes na seção de dados).

Este modelo não roda em llama.cpp nem tem versão GGUF. Ele é um encoder com cabeça de classificação, não um decoder que gera texto — o caminho de produção é o ONNX Runtime (veja "Como usar"). Essa escolha de arquitetura foi medida em uma comparação direta entre as duas famílias (um bake-off), não assumida: para classificar itens em lote na CPU de um escritório contábil, o encoder acertou ~4x mais e custou de 5 a 40x menos por item do que o decoder equivalente.

Exemplos reais: da descrição na nota ao código na tabela

Todos os exemplos abaixo são saídas reais de execução (nada foi editado), cruzadas com a tabela NCM vigente (Resolução Gecex nº 812/2025). Foram capturados com uma versão anterior deste pacote (corrida d); o fluxo e o formato da saída são idênticos na versão atual, e os números de confiança individuais podem oscilar alguns décimos entre versões do modelo — o texto ao redor de cada exemplo continua valendo. A terceira coluna é o texto oficial da tabela para o código sugerido — posição e subitem combinados, para leitura:

Descrição do item (como vem na NF-e) NCM sugerido Confiança O que diz a tabela NCM oficial
PARAF SEXT ZINC M8X40 DIN933 7318.15.00 98,3% Parafusos, pinos ou pernos, roscados […] de ferro fundido, ferro ou aço → Outros parafusos e pinos ou pernos, mesmo com as porcas e arruelas
TONER COMPAT HP CF283A PRETO 8443.99.33 99,7% Impressoras — partes e acessórios → Cartuchos de revelador (toners)
CANETA ESF AZUL CX 50 9608.10.00 99,2% Canetas esferográficas
OLEO SOJA REFINADO PET 900ML 1507.90.11 97,3% Óleo de soja, refinado → Em recipientes com capacidade inferior ou igual a 5 l
MORTADELA FATIADA KG 1601.00.00 95,7% Enchidos e produtos semelhantes, de carne […]; preparações alimentícias à base desses produtos
PNEU 175/70 R13 82T 4011.10.00 93,6% Pneumáticos novos, de borracha → Do tipo utilizado em automóveis de passageiros
CIMENTO PORTLAND CP II Z 32 SC 50KG 2523.29.10 67,0% Cimentos Portland → Cimento comum

Repare no cimento: 67% em vez de 98%. A confiança é calibrada — ela cai quando a descrição deixa margem real de dúvida (o segundo candidato, 2523.29.90 "Outros", leva 12,2%). Esse número não é decorativo: ele foi ajustado para que "67%" signifique, na prática, acertar cerca de 67% das vezes.

E quando o modelo não sabe? Abstenção em ação

ARROZ BRANCO TIPO 1 5KG
→ ABSTEVE-SE (nenhum candidato passou dos pisos de decisão)
   top-3 exibido mesmo assim:
   1006.30.19 (19,8%) — arroz semibranqueado ou branqueado, parboilizado, outros
   1006.10.92 (15,4%) — arroz com casca, não parboilizado
   1006.20.20 (13,8%) — arroz descascado, não parboilizado

E ele tem razão em hesitar: a tabela NCM separa o arroz por beneficiamento (com casca / descascado / branqueado, parboilizado ou não) — e "ARROZ BRANCO TIPO 1 5KG" não diz se é parboilizado. Nenhum classificador honesto consegue cravar a 8ª casa com essa informação; a resposta certa é escalar para quem tem o produto na mão. É exatamente isso que a abstenção faz.

Outro caso real, mais sutil:

PARACETAMOL 750MG C/20 COMP
→ ABSTEVE-SE (confiança do 1º candidato abaixo do piso de 55%)
   3004.90.45 (52,7%) — medicamentos em doses, p/ venda a retalho: paracetamol
   3003.90.55 (27,4%) — medicamentos NÃO acondicionados p/ venda a retalho: paracetamol

O primeiro candidato é o código certo para uma caixa de 20 comprimidos — mas a disputa entre 30.04 (acondicionado para venda) e 30.03 (a granel) depende de um detalhe que a descrição não crava, e o modelo prefere entregar a dúvida ao contador em vez de escondê-la atrás de um chute.

Para quem é

Contadores e escritórios contábeis, integradores de ERP (Omie, Bling, Tiny) e sellers/marketplaces que precisam classificar catálogos inteiros em NCM — seja como triagem antes da revisão humana, seja para conferir divergências entre o NCM informado e a descrição do produto.

Por que isso importa agora (reforma tributária e a NF-e de 2026)

A partir de 03/08/2026, a NF-e de Regime Normal passa a ser rejeitada quando não trouxer IBS/CBS/cClassTrib válidos (NT 2025.002 v1.40). Em 04/01/2027, a mesma exigência chega ao Simples Nacional e ao MEI. O cClassTrib depende do NCM correto — um NCM errado contamina toda a tributação da reforma dali para frente.

Este modelo ataca a parte estável do problema: descrição → NCM. O mapeamento NCM → cClassTrib/CST fica em tabelas oficiais versionadas, fora do modelo, de propósito: essas tabelas mudam com frequência (a IT 2025.002 já está na v1.50) e não faz sentido retreinar um modelo a cada portaria.


Como usar

1. pip install ncm-classificador (recomendado — o caminho testado de ponta a ponta)

O pacote pip (ncm-classificador, wheel de 10,5KB, sem dependência de torch) já traz a tokenização, a aplicação de temperatura e a regra de abstenção — exatamente as mesmas usadas para medir os números desta página. É o caminho oficial do produto, não uma reimplementação.

pip install ncm-classificador
# baixe o pacote de inferência deste repositório (modelo.onnx + tokenizer/ +
# labels.json + inferencia.json + manifest.json) e aponte para ele:
export NCM_PACOTE=/caminho/para/pacote-baixado-do-hf
ncm classificar "PARAF SEXT ZINC M8X40 DIN933"

Saída real (medida em 2026-07-17, com uma versão anterior deste pacote — corrida d; a versão atual segue o mesmo formato):

                    NCM para: PARAF SEXT ZINC M8X40 DIN933
┌───────────────┬─────────────────────┐
│ candidato NCM │ confiança calibrada │
├───────────────┼─────────────────────┤
│ 73181500      │               98.3% │
│ 73181100      │                0.8% │
│ 86079900      │                0.1% │
└───────────────┴─────────────────────┘
Ferramenta de apoio · caráter orientativo · a responsabilidade pela
classificação é do contribuinte/contador.

O mesmo pacote traz ncm lote arquivo.csv (classificação em lote) e ncm servir (API REST auto-hospedada em FastAPI, com API key e rate limit). A documentação completa está na página do pacote no PyPI: https://pypi.org/project/ncm-classificador/ (código sob licença MIT).

2. ONNX Runtime direto (para quem já tem pipeline próprio)

O grafo devolve logits crus. A calibração (temperatura e pisos de abstenção) é política de produto e mora em inferencia.json, não dentro do grafo — assim a calibração pode ser reajustada sem reexportar o modelo.

import json
import numpy as np
import onnxruntime as ort
from tokenizers import Tokenizer

# arquivos deste repositório: modelo.onnx, tokenizer/tokenizer.json,
# labels.json (lista de 9.748 códigos NCM, na ORDEM dos logits),
# inferencia.json (temperatura + pisos de abstenção)
sessao = ort.InferenceSession("modelo.onnx", providers=["CPUExecutionProvider"])
tok = Tokenizer.from_file("tokenizer/tokenizer.json")
labels = json.load(open("labels.json", encoding="utf-8"))
config = json.load(open("inferencia.json", encoding="utf-8"))

enc = tok.encode("PARAF SEXT ZINC M8X40 DIN933")
# trunque/pad para config["max_len"] (128) — o pacote pip faz isso por você
(logits,) = sessao.run(
    ["logits"],
    {
        "input_ids": np.array([enc.ids], dtype=np.int64),
        "attention_mask": np.array([enc.attention_mask], dtype=np.int64),
        "token_type_ids": np.array([enc.type_ids], dtype=np.int64),
    },
)
temperatura = config["temperatura"]  # 0,788
probs = np.exp(logits / temperatura) / np.exp(logits / temperatura).sum()
top3 = np.argsort(-probs[0])[:3]
for i in top3:
    print(labels[i], f"{probs[0][i]:.1%}")

3. transformers (acesso direto aos pesos — para pesquisa, não para produto)

O checkpoint também está disponível em safetensors/transformers, para quem quer inspecionar ou retreinar os pesos. Atenção: o id2label do config.json usa placeholders genéricos (LABEL_0, LABEL_1...) — o mapeamento real para códigos NCM está em labels.json (na mesma ordem dos logits), e a confiança só é calibrada depois de dividir os logits pela temperatura de inferencia.json. Para uso em produto, prefira o caminho ONNX/pip acima, que já faz tudo isso.

import json
import torch
from transformers import AutoModelForSequenceClassification, AutoTokenizer

tok = AutoTokenizer.from_pretrained("DominuZ/ncm-classificador-bertimbau")
model = AutoModelForSequenceClassification.from_pretrained("DominuZ/ncm-classificador-bertimbau")
labels = json.load(open("labels.json", encoding="utf-8"))

inputs = tok("PARAF SEXT ZINC M8X40 DIN933", return_tensors="pt", truncation=True, max_length=128)
with torch.no_grad():
    logits = model(**inputs).logits
probs = torch.softmax(logits / 0.788, dim=-1)  # temperatura de inferencia.json
top3 = torch.topk(probs, k=3)
for score, idx in zip(top3.values[0], top3.indices[0]):
    print(labels[idx], f"{score:.1%}")

Métricas

Três recortes diferentes, porque um número só esconderia mais do que revela neste problema:

Recorte acc@2 acc@4 acc@6 acc@8 ECE
Validação agregada (44.257 itens, ~74% sintético) 89,4% 72,3% 62,4% 57,3% 0,027
Validação — só dados reais (11.446 itens, 100% consenso multi-emissor, nada sintético) 76,3%
RFB-bench (4.944 decisões vinculantes reais da Receita, prosa jurídica formal) 60,1% 38,8% 22,2% 16,7%

acc@N = fração de itens em que os N primeiros dígitos do código previsto coincidem com os do código correto (acc@2 = capítulo certo, acc@4 = posição, acc@6 = subposição, acc@8 = código completo). É a métrica adequada porque a hierarquia NCM é aninhada: acertar capítulo e posição já reduz o trabalho de revisão mesmo quando a 8ª casa erra. (Não confundir com top-N: o top-3 que o produto exibe é outra coisa — os 3 códigos completos mais prováveis.) ECE = Expected Calibration Error após ajuste de temperatura (T=0,788) — quanto mais perto de zero, mais a confiança informada corresponde à taxa de acerto real.

Por que o RFB-bench ainda é mais baixo que o val — e por que publicamos o número assim mesmo: o modelo foi treinado majoritariamente com descrições de item de NF-e — telegráficas, abreviadas, sem sujeito nem verbo. O RFB-bench vem de Soluções de Consulta da Receita Federal: prosa jurídica longa e formal, outro universo de texto. É desvio de distribuição clássico, não defeito de arquitetura. A boa notícia é que essa distância diminuiu bastante depois que passamos a treinar com uma fatia de dado real nesse mesmo registro formal — a lista consolidada de bens da Camex/LESSIN (pública, descrições técnicas de ex-tarifários escritas pelo próprio governo). O acc@8 no RFB-bench quase dobrou (8,9% → 16,7%) e o acc@2 subiu 4,9pp (55,2% → 60,1%), sem custar acurácia na validação com dados reais de NF-e (76,3%, dentro do ruído dos 76,5% anteriores). Antes disso, paráfrases formais sintéticas tinham melhorado só o capítulo (+10pp no acc@2) sem tocar no código completo — foi dado real, não paráfrase, que moveu a régua no acc@8. Ainda assim o a distância persiste: o RFB-bench continua bem abaixo do desempenho em texto de NF-e, porque a maior parte do treino segue sendo o registro telegráfico da nota fiscal. Preferimos publicar o número como ele é a escondê-lo: a proposta deste projeto é se destacar pela calibração e pela abstenção corretas, e isso começa por divulgar as métricas como elas são. (Nota: os rótulos do RFB-bench vêm de atos oficiais da Receita, mas a revisão humana par a par do conjunto ainda está em andamento — o dataset publicado indica isso item a item.)


Calibração e abstenção — o diferencial

A confiança que o modelo informa é calibrada: quando ele diz "98,3%", isso fica próximo da taxa de acerto observada para previsões naquela faixa (ECE 0,027 depois do ajuste de temperatura, contra 0,155 antes). E quando a confiança não basta, ele se abstém em vez de chutar:

  • Pisos de abstenção (provisórios): confiança do 1º candidato abaixo de 55% ou margem entre o 1º e o 2º candidato abaixo de 10 pontos percentuais.
  • Taxa de abstenção observada na validação: 48,8%.
  • Predição conforme (RAPS, α=0,05): conjunto de candidatos com 95,1% de cobertura garantida e tamanho médio de 312 códigos — um número que documenta a fronteira desta geração (garantir cobertura alta num espaço de 9.748 códigos ainda exige um conjunto grande; um modelo mais forte encolhe esse conjunto sem mexer no α).
  • Verificação manual (20 itens): quando o modelo respondeu, o top-1 estava correto em todos os casos verificados; os erros observados caíram todos do lado da abstenção — o contrato "na dúvida, escale a um humano" se sustentou na amostra.

A abstenção não esconde o top-3 — ele continua visível, apenas marcado como insuficiente para decisão automática. O produto aponta o risco; quem decide é o contador.


Resposta parcial de posição — quando a folha não fecha, mas a posição fecha

Nem toda abstenção é igual. Às vezes o modelo não tem confiança suficiente para cravar os 8 dígitos, mas está bastante seguro sobre os 4 primeiros (a posição da NCM — o nível que já reduz o universo de folhas candidatas para o contador escolher). Medimos essa situação separadamente, com um piso de confiança próprio calibrado só para a posição: em dados reais, quando a soma de probabilidade da posição vencedora passa desse piso, ela acerta a posição correta em 93,8% dos casos (321 itens, barra pré-registrada era 89,9% — ver ADR-026/ADR-027 em docs/DECISIONS.md). Isso acontece em 5,6% das consultas que, de outra forma, seriam abstenções mudas — nesses casos, a resposta ganha um campo extra: resposta_parcial.

Saída real de execução (pacote f-1, ncm classificar ... --json):

{"descricao": "ARROZ(CLASSIFICAÇÂO SEM CARACTERÍSTICAS)", "top3": [{"ncm8": "10062020", "confianca": 0.301446}, {"ncm8": "10061092", "confianca": 0.141093}, {"ncm8": "10061091", "confianca": 0.107488}], "abstem": true, "resposta_parcial": {"nivel": "posicao", "codigo": "1006", "confianca": 0.902203, "candidatas_folha": ["10062020", "10061092", "10061091"], "texto": "Faltam os últimos 4 dígitos — refine entre as folhas candidatas com seu contador."}, "disclaimer": "Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação é do contribuinte/contador.", "versao_pacote": "f-1"}

Repare que abstem continua true. A parcial não é uma segunda forma de decisão automática — é o modelo dizendo "não sei o produto exato, mas sei a família, e isso já corta o trabalho do contador de 9.748 folhas possíveis para um punhado". A decisão final sobre qual das candidatas_folha é a correta continua sendo humana.

Com a parcial somada à abstenção de folha normal, a taxa efetiva de consultas que ficam sem nenhum apoio de código cai de 25,6% para 19,9% em dados reais — o restante da abstenção ganha pelo menos a posição. Testamos também uma camada equivalente no nível de capítulo (2 dígitos); ela não bateu a barra de precisão pré-registrada e ficou de fora desta versão (arquivada em definitivo pelo ADR-026) — por isso só o nível posição aparece no produto.

O campo resposta_parcial só existe em pacotes de modelo que trazem piso_posicao no inferencia.json (o f-1 publicado aqui traz); pacotes mais antigos continuam funcionando normalmente, só que sempre com resposta_parcial: null.


Dados de treino

100% dados públicos e processamento local — nenhuma API paga, nenhum dado raspado de terceiros. Composição real do conjunto de treino do modelo publicado aqui (treino_f.parquet, 678.585 exemplos, 9.748 códigos — números lidos do arquivo, não estimados; as mesmas linhas do conjunto E, com as descrições da Camex a peso 0,5 no gradiente):

origem exemplos descrição
consenso 398.230 descrições reais de item de NF-e, rotuladas por consenso multi-emissor: um grupo de descrições equivalentes só recebe um NCM quando ≥3 emissores distintos concordam com ≥70% dos votos — sobre um corpus de 28.035.593 itens do Portal da Transparência (2021-10 a 2026-06)
quarentena_v1 154.801 descrições reais adicionais, mineradas dos grupos que não atingiram o quórum do consenso, aceitas apenas com unanimidade total de votos como compensação pelo quórum menor (reforço para códigos com <21 exemplos)
sintese_v1 74.523 descrições sintéticas geradas e verificadas por dois modelos locais independentes (gerador de 4B / verificador de 9B), para códigos sem exemplos reais suficientes
sintese_nesh_v1 25.794 descrições sintéticas guiadas pelo texto oficial da NESH (Notas Explicativas do Sistema Harmonizado, IN RFB 2.169/2023), com contraste explícito contra posições NCM vizinhas — reforço direcionado aos códigos que continuavam com poucos exemplos após a síntese v1
camex_v1 25.237 descrições reais de produto vindas da lista consolidada de bens da Camex/LESSIN (pública, atualizada semanalmente) — texto técnico em prosa formal-jurídica, o mesmo registro das Soluções de Consulta da Receita (RFB-bench). É a fonte que fechou boa parte da distância do modelo para texto formal; entra com peso 0,5 no gradiente desta corrida, para não deslocar capacidade demais do registro telegráfico da NF-e (que ainda é o alvo principal do produto)

Todo o pipeline (consenso, mineração, síntese, ingestão da Camex) rodou em uma única GPU de 16GB, sem nenhuma API paga — os modelos de síntese são locais, servidos via llama.cpp. A validação (44.257 itens) usa a mesma mistura sem Camex; o RFB-bench (4.944 pares) ficou totalmente fora do treino, com guarda de anticontaminação verificada em código (inclusive contra sobreposição de texto entre a Camex e o RFB-bench).

LGPD: dos itens de NF-e processados, usamos exclusivamente (descrição, NCM). Identificadores de emitente (CNPJ/CPF) são descartados na fronteira do ponto de entrada do pipeline — pseudonimizados (hash truncado) apenas o suficiente para o voto de consenso contar emissores distintos, nunca guardados de forma reversível, e removidos de vez antes da rotulagem. Nenhum dado de destinatário/comprador entra no pipeline.


Limitações

  • Ambiguidade que a descrição não resolve: boa parte das abstenções não é fraqueza do modelo, é informação que falta na nota — o exemplo do arroz acima (a tabela exige saber se é parboilizado; a descrição não diz). Nesses casos, o caminho certo é o fluxo com revisão humana — e é para isso que a abstenção existe.
  • Erros confiantes existem. Exemplo real: NOTEBOOK 15,6 I5 8GB 256GB SSD recebeu 8443.32.31 (impressora de jato de tinta) com 56,6% — logo acima do piso de abstenção de 55%, então o modelo respondeu, e respondeu errado (os candidatos 2 e 3, ambos da posição 84.71 de notebooks, somavam 35%). É o tipo de caso que os pisos provisórios ainda deixam passar; por isso o top-3 completo é sempre exibido e a decisão final é humana.
  • Códigos raros aprendem menos: 1.654 dos 9.748 códigos NCM seguem com poucos exemplos de treino mesmo após duas rodadas de geração sintética — é o teto do que a síntese consegue produzir com o material atual. Para esses códigos raros, espere confiança menor e mais abstenção. O próximo degrau é dado real novo (mais meses do Portal da Transparência, outras fontes públicas), não mais síntese sobre o mesmo material.
  • Texto formal (RFB-bench) segue sendo uma fronteira, mesmo com o avanço recente — veja a seção de métricas. Dado real do mesmo registro formal (Camex/LESSIN) quase dobrou o acc@8 nesse benchmark (8,9% → 16,7%); antes disso, paráfrases sintéticas só tinham melhorado capítulo/posição (+10pp no acc@2) sem mexer no código completo. A distância para o desempenho em texto de NF-e diminuiu, mas continua grande — a maior parte do treino ainda é o registro telegráfico da nota fiscal.
  • A validação agregada é ~74% sintética (32.811 de 44.257 itens) — o 57,3% de acc@8 carrega esse viés; o recorte 100% real (76,3%) é a referência mais honesta para desempenho em produção sobre texto de NF-e.
  • A tabela NCM oficial muda (novos códigos, extinções, renumerações). O modelo aprende o problema estável (descrição → NCM); a tabela vigente e o mapeamento para cClassTrib/CST vivem em tabelas versionadas fora do modelo. Uma mudança de nomenclatura publicada depois do corte de treino só chega ao modelo com retreino — ou pela atualização da tabela no produto, que é trocável sem mexer no modelo.
  • Os pisos de abstenção (confiança 0,55 / margem 0,10) são provisórios — calibrados na validação, ainda sem validação contra volume de produção real.
  • Formato de exportação: só a variante fp32 (~1,3GB) é distribuída. A quantização INT8 dinâmica foi medida e reprovada (na corrida atual, -5,6pp de acc@8 e ECE 3,7x pior — a calibração quebra) e não está neste repositório; quantização estática poderá ser avaliada no futuro, com o mesmo critério de paridade.
  • Não emite parecer fiscal. É ferramenta de triagem e apoio — nunca a decisão final.

Latência (CPU, medição real)

51,3ms por item em lote de 1 (incluindo tokenização; onnxruntime com CPUExecutionProvider — medição real do gate de paridade sobre o val inteiro, 44.257 itens). Lotes maiores amortizam melhor o custo fixo de carregar o modelo na memória. Não precisa de GPU.


Perguntas frequentes

Como encontrar o código NCM correto a partir da descrição do produto? O caminho oficial é a tabela NCM (Siscomex) com as regras gerais de interpretação do Sistema Harmonizado e as notas da NESH — e, em caso de dúvida real, uma Solução de Consulta à Receita Federal. Este modelo automatiza a primeira etapa desse funil: dado o texto livre do item, ele aponta os 3 códigos mais prováveis com confiança calibrada, para o contador revisar apenas o que merece revisão.

O que acontece se o NCM da nota fiscal estiver errado? Tributação errada (a NCM determina IPI, II e, com a reforma, o cClassTrib de IBS/CBS), risco de autuação e multa, e — a partir de 03/08/2026 — rejeição da NF-e quando o cClassTrib derivado for inválido. Classificar bem deixou de ser detalhe de compliance para virar pré-requisito de emissão.

Este modelo substitui o contador? Não — e foi desenhado para não substituir. Ele se abstém quando não tem confiança, exibe o top-3 mesmo quando se abstém e carrega o disclaimer em toda saída. A responsabilidade pela classificação fiscal é do contribuinte e de seu contador (CFC Res. 560/83); o modelo faz a triagem, o humano decide.

Roda offline? Preciso de GPU? Roda 100% offline e em CPU (~51ms por item). O pacote de inferência é autocontido: modelo.onnx + tokenizer + labels.json + inferencia.json, com integridade conferida por SHA-256 antes de responder.

Tem versão GGUF para llama.cpp / LM Studio / Ollama? Não, e não vai ter: a arquitetura é um encoder com cabeça de classificação (não um modelo generativo). O caminho de produção é ONNX Runtime — mais leve e mais barato para este problema.


Licença

Os pesos ajustados publicados neste repositório (checkpoint + cabeça de 9.748 classes) são distribuídos sob CC BY 4.0 — use, redistribua e derive à vontade, com atribuição a este repositório. O modelo base (neuralmind/bert-large-portuguese-cased, BERTimbau) é distribuído sob MIT pelos autores originais (NeuralMind), e a atribuição a eles permanece.


Disclaimer

Ferramenta de apoio · caráter orientativo · a responsabilidade pela classificação é do contribuinte/contador.

Este modelo assiste o trabalho do contador — nunca o substitui (CFC Res. 560/83). Ele se abstém quando não tem confiança suficiente; abstenção não é defeito, é o modelo dizendo "isto precisa de revisão humana".


English summary

What: A Portuguese-language classifier (BERTimbau Large + a 9,748-class head, 344.4M parameters) that maps free-text Brazilian tax-invoice (NF-e) item descriptions to 8-digit NCM codes (Brazil's harmonized-system-derived tax classification), returning a top-3 with calibrated confidence and explicit abstention when uncertain.

Why now: starting 2026-08-03, Brazilian NF-e invoices under the standard tax regime get rejected without valid IBS/CBS tax-classification codes (cClassTrib), which in turn depend on a correct NCM code — this model targets that upstream, stable sub-problem.

Numbers: aggregate validation (44,257 items, ~74% synthetic) acc@8 57.3%, acc@2 89.4%, ECE 0.027 post-temperature; on the real-data-only validation cut (11,446 items) acc@8 76.3%; on an out-of-distribution benchmark of 4,944 real binding tax rulings from Brazil's federal tax authority (formal legal prose vs. the telegraphic invoice-item style seen in training) acc@8 is 16.7% (acc@2 60.1%) — still well below in-distribution performance, but nearly double what earlier checkpoints scored (8.9%), after adding a slice of real training data written in that same formal register (Camex/LESSIN, a public government list of product descriptions) at half gradient weight. Reported openly either way — the proposal here is to stand out on honest calibration and abstention, not on cherry-picked numbers. Abstention rate 48.8% on aggregate validation, 25.6% on real-data queries. When the model can't commit to the full 8-digit code but is confident about the 4-digit position, it now returns a partial position answer alongside the abstention (measured at 93.8% position accuracy on 321 real items, cutting real "no answer at all" queries from 25.6% to 19.9%) — the item still counts as an abstention; the partial answer narrows the candidates for the accountant, it never decides on its own. Trained 100% on local/public data (public government feed + locally-verified synthetic data, cross-model generator/verifier), on a single 16GB consumer GPU, with no paid third-party APIs. Not GGUF-compatible (it's an encoder + classification head, not a decoder) — the deployment rail is ONNX Runtime (fp32; dynamic INT8 quantization was measured and rejected for distribution — it broke calibration). This is a support tool, not a substitute for professional accounting judgment.

Downloads last month
31
Safetensors
Model size
0.3B params
Tensor type
F32
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Dataset used to train DominuZ/ncm-classificador-bertimbau

Evaluation results

  • acc@2 (capítulo — 2 primeiros dígitos do NCM corretos) on Validação interna (44.257 itens, ~74% sintético)
    self-reported
    0.894
  • acc@4 (posição — 4 dígitos) on Validação interna (44.257 itens, ~74% sintético)
    self-reported
    0.723
  • acc@6 (subposição — 6 dígitos) on Validação interna (44.257 itens, ~74% sintético)
    self-reported
    0.624
  • acc@8 (código NCM completo de 8 dígitos exato) on Validação interna (44.257 itens, ~74% sintético)
    self-reported
    0.573
  • ECE pós-temperatura (T=0,788) on Validação interna (44.257 itens, ~74% sintético)
    self-reported
    0.027
  • acc@8 on Validação — subconjunto 100% real (consenso multi-emissor, 11.446 itens)
    self-reported
    0.763
  • acc@2 on Soluções de Consulta COANA/COSIT (4.944 pares, pendente de revisão humana)
    self-reported
    0.601
  • acc@4 on Soluções de Consulta COANA/COSIT (4.944 pares, pendente de revisão humana)
    self-reported
    0.388