- Classificador de NCM pela descrição do produto — BERTimbau + ONNX
- O que este modelo faz
- Exemplos reais: da descrição na nota ao código na tabela
- Para quem é
- Por que isso importa agora (reforma tributária e a NF-e de 2026)
- Como usar
- Métricas
- Calibração e abstenção — o diferencial
- Resposta parcial de posição — quando a folha não fecha, mas a posição fecha
- Dados de treino
- Limitações
- Latência (CPU, medição real)
- Perguntas frequentes
- Licença
- Disclaimer
- English summary
- O que este modelo faz
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 SSDrecebeu 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
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-reported0.894
- acc@4 (posição — 4 dígitos) on Validação interna (44.257 itens, ~74% sintético)self-reported0.723
- acc@6 (subposição — 6 dígitos) on Validação interna (44.257 itens, ~74% sintético)self-reported0.624
- acc@8 (código NCM completo de 8 dígitos exato) on Validação interna (44.257 itens, ~74% sintético)self-reported0.573
- ECE pós-temperatura (T=0,788) on Validação interna (44.257 itens, ~74% sintético)self-reported0.027
- acc@8 on Validação — subconjunto 100% real (consenso multi-emissor, 11.446 itens)self-reported0.763
- acc@2 on Soluções de Consulta COANA/COSIT (4.944 pares, pendente de revisão humana)self-reported0.601
- acc@4 on Soluções de Consulta COANA/COSIT (4.944 pares, pendente de revisão humana)self-reported0.388