HS Code Classifier (HS6, English)

Give it an English product description, get back the 6-digit Harmonized System (HS) code — the commodity code that drives customs tariff classification, duty rates, HTS lookups and trade compliance.

"men's cotton knitted t-shirt, short sleeve"           →  610910  (0.998)
"portable laptop computer, 14 inch display, 1.2 kg"    →  847130  (0.994)
"lithium-ion rechargeable battery, 3.7 V, 5000 mAh"    →  850760  (0.929)
"roasted arabica coffee beans, not decaffeinated"      →  090121  (0.811)

Fine-tuned end-to-end over the full 6,750-class HS6 space — not a toy subset of the most common chapters. HS4 (1,266 headings) and HS2 (97 chapters) come out of the same forward pass and are guaranteed consistent with the HS6 answer.

Task Product description → HS6 tariff code
Input language English
Label space 6,750 HS6 · 1,266 HS4 · 97 HS2
Accuracy on full product descriptions 71.94% top-1 · 91.08% top-5
Parameters 573.6M (fp32, 2.2 GB)
Context window 1,024 tokens at inference

What you get depends on what you give it: a full description runs at ~72% top-1, a two-word catalogue stub at ~38%. The complete breakdown — by input type, by length, by confidence — is right below.


Accuracy

Measured on a frozen held-out test set of 8,065 product descriptions that has zero overlap with training and was never used for tuning any hyperparameter or threshold.

Accuracy depends heavily on how much you tell it

This is the single most important table in this card:

Input Share of test Top-1 Top-5
Full product description 35% 71.94% 91.08%
Short subject line 22% 46.34% 73.17%
Two- or three-word phrase 43% 38.01% 61.70%

By raw character length:

Length Top-1
0–40 chars 39.00%
40–100 45.15%
100–200 46.98%
200–500 73.75%
500–1500 74.72%
1500+ 64.77%

Do not truncate your inputs. Material, construction, and intended use are exactly the features that separate one subheading from another; "two folding cots" does not contain them and no model can recover them. Feed the whole description — the spec sheet, the paragraph, the invoice line with attributes — and accuracy roughly doubles.

Averaged over the whole benchmark

The benchmark is deliberately hostile: 62% of its inputs are under 100 characters, so the average is dragged down by fragments that carry no classifiable features at all. Averaged over all 8,065 items:

Metric Score
HS6 top-1 (exact 6-digit match) 51.70%
HS4 top-1 (heading) 60.99%
HS2 top-1 (chapter) 74.28%
HS6 top-5 recall 74.48%

Which of the two numbers applies to you is decided by your input, not by the model: feed full descriptions and expect the 72% row, feed catalogue stubs and expect the 38% row. Nothing is hidden here — both are stated so you can predict your own result before you download 2.2 GB.

Confidence is usable as a threshold

Top-1 softmax probability is monotonically informative, which makes a straight-through / review split practical:

Confidence Share of traffic Accuracy
0.8 – 1.0 62.8% 63.7%
0.6 – 0.8 12.6% 28.4%
0.4 – 0.6 12.3% 21.6%
0.2 – 0.4 9.1% 12.0%
0.0 – 0.2 3.2% 3.1%

With temperature T ≈ 1.75 applied to the logits, calibration improves further: you can auto-accept 41.5% of the flow at 80.0% accuracy. Recommended pattern — auto-clear high-confidence items, route the rest to a human with the top-5 list attached (top-5 covers 74% of everything and 91% of full descriptions).

Where the errors go

Outcome Share
Correct at top-1 51.70%
Correct code present in top-5, but not ranked first 22.78%
Correct code absent from top-5 25.52%

Of the misses, 18.1% stay inside the correct HS4 heading and 26.6% inside the correct HS2 chapter — i.e. a large part of the error is near-miss, not nonsense.


Quick start

pip install transformers torch sentencepiece

Verified on transformers 5.5 / torch 2.11. The model code uses only long-stable transformers APIs, so 4.4x and later should work as well.

from transformers import AutoModel, AutoTokenizer

REPO = "Kenpache/hs-code-classifier-en"
model = AutoModel.from_pretrained(REPO, trust_remote_code=True).eval()
tokenizer = AutoTokenizer.from_pretrained(REPO, trust_remote_code=True)

model.classify(["men's cotton knitted t-shirt, short sleeve"], tokenizer, top_k=5)
# [[{'hs6': '610910', 'score': 0.9983},
#   {'hs6': '610990', 'score': 0.0010},
#   {'hs6': '611020', 'score': 0.0005}, ...]]

classify() batches for you and accepts a list of any length:

codes = model.classify(descriptions, tokenizer, top_k=5, batch_size=32)

On GPU

model = AutoModel.from_pretrained(REPO, trust_remote_code=True).to("cuda").eval()

CUDA, Apple Silicon (mps) and CPU all work. Roughly 3 GB of VRAM at batch 16 / length 1024; about 4 GB of RAM on CPU.

Leave the context window at 1,024 tokens. It is already the default in config.json; lowering recommended_max_length costs about 2.9 points on texts longer than 1,500 characters, and raising it to 2,048 adds nothing.

All three HS levels at once

import torch

enc = tokenizer(["woven cotton fabric, dyed, 200 g/m2"], truncation=True,
                max_length=1024, return_tensors="pt")
with torch.no_grad():
    out = model(**enc)

hs6 = model.config.id2label[out.logits.argmax(-1).item()]     # '520839'
hs4 = model.config.id2hs4[out.logits_hs4.argmax(-1).item()]   # '5208'
hs2 = model.config.id2hs2[out.logits_hs2.argmax(-1).item()]   # '52'

hs4 and hs2 are marginals of the same distribution (logsumexp over the children of each parent), so the levels can never contradict each other: the model cannot return heading 6109 and a subheading that lives under 6110.


Limitations

  1. English only. The encoder is multilingual, but the head was trained on English product descriptions. Other languages are untested and expected to be much weaker.
  2. HS6 only. The first six digits are internationally harmonized; national 8–10 digit tariff lines are out of scope and this model does not predict them.
  3. Short inputs are hard, as the tables above show. Under ~40 characters, expect ~39% top-1.
  4. Coverage is uneven across the 6,750 classes. Rarely-seen codes are much weaker than the headline number suggests; roughly 3,800 codes carry the bulk of the model's competence.
  5. The ceiling is domain-imposed, not model-imposed. Identical descriptions legitimately receive different codes depending on context (end use, material composition, degree of processing), which caps any text-only classifier.
  6. Not legal or customs advice. Output is a ranked suggestion. Binding classification is a decision of the competent authority. Use this to triage, to pre-fill, and to route to a human — not to file unattended.

Intended use

Good fits:

  • pre-filling HS6 on customs declarations, then human review of low-confidence rows
  • catalogue / marketplace enrichment at scale
  • landed-cost and duty estimation tooling
  • deduplicating and sanity-checking existing classifications (flag rows where the model is confident and disagrees)

Poor fits: unattended filing, national tariff lines beyond 6 digits, non-English input.


Files

File What it is
model.safetensors weights, fp32, 2.2 GB
config.json encoder config + head config + id2label for all 6,750 HS6 codes
modeling_hs6.py, configuration_hs6.py model definition (loaded via trust_remote_code=True)
tokenizer.json, sentencepiece.bpe.model, … XLM-R tokenizer, model_max_length 1024

License

MIT, following the BAAI/bge-m3 base model.

Citation

@misc{hs6_classifier_en,
  title  = {HS Code Classifier (HS6, English)},
  year   = {2026},
  note   = {XLM-RoBERTa-large (bge-m3) with a flat 6,750-class HS6 head
            and marginalized HS4/HS2 levels},
  url    = {https://huggingface.co/Kenpache/hs-code-classifier-en}
}
Downloads last month
18
Safetensors
Model size
0.6B params
Tensor type
I64
·
F32
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for Kenpache/hs-code-classifier-en

Base model

BAAI/bge-m3
Finetuned
(528)
this model