The base WangchanBERTa checkpoint was used (pretrained MLM only — not the existing fine-tuned variant) and a fresh token-classification head was trained from scratch.
Model details
Base model:airesearch/wangchanberta-base-att-spm-uncased (RoBERTa-base, ~105M params, SentencePiece tokenizer, vocab 25k)
Task: Token classification (NER) in IOB format
Language: Thai
Max sequence length: 416 subword tokens (matches pretraining)
License: CC-BY-SA 3.0 (inherits from the training corpus)
Intended use
Entity extraction from general-domain Thai text (news, PR content, social media). The model predicts 14 entity types plus O, in IOB scheme (29 labels total).
Entity types
Tag
Description
PERSON
Person name
ORGANIZATION
Company / organization
LOCATION
Location
DATE
Date
TIME
Time
MONEY
Monetary amount
PERCENT
Percentage
LAW
Legislation / law reference
LEN
Length measurement
TEMPERATURE
Temperature
PHONE
Phone number
EMAIL
Email address
URL
URL
ZIP
Zip code
Training data
pythainlp/thainer-corpus-v2.2 — 7,326 documents (news, PR from KKU, general Thai text), pre-split by the dataset authors:
Split
Docs
Train
3,938
Validation
1,313
Test
1,313
Tags in the corpus are stored as integers without a ClassLabel feature; the canonical id2label mapping was imported from pythainlp/thainer-corpus-v2-base-model which was trained on the same corpus.
Training procedure
Hyperparameters follow the WangchanBERTa paper's downstream recipe.
Setting
Value
Epochs
10 (with early stopping, patience 2)
Batch size
16
Learning rate
3e-5
LR scheduler
Linear with 10% warmup
Optimizer
AdamW (β1=0.9, β2=0.999, wd=0.01)
Precision
bf16
Max length
416
Seed
42
Best checkpoint criterion
Validation entity-level F1
Preprocessing notes
Two WangchanBERTa-specific steps matter for correct results:
Space handling. Spaces are semantically meaningful in Thai. Any whitespace-only token in the pre-tokenized input is replaced with the model's space token <_> before passing to the tokenizer.
Label alignment. After subword tokenization, the word's tag is assigned to the first subword only; continuation subwords and special tokens are masked with -100 so they are ignored by the loss.
Hardware
Single premium GPU on Google Colab Pro+ RTX PRO 6000 Blackwell Server Edition.
Evaluation
Entity-level metrics computed with seqeval on the held-out test split (1,313 docs).
Split
Precision
Recall
F1
Validation
0.817274
0.872834
0.844141
Test
0.813699
0.877883
0.844573
# Per-entity classification report on the test set
precision recall f1-score support
AGO 0.7292 0.9211 0.8140 38
DATE 0.8122 0.9276 0.8661 373
EMAIL 1.0000 1.0000 1.0000 1
FACILITY 0.4214 0.4558 0.4379 147
LAW 0.5000 0.5909 0.5417 44
LEN 1.0000 0.9412 0.9697 17
LOCATION 0.8159 0.8558 0.8354 839
MONEY 0.7800 0.8797 0.8269 133
ORGANIZATION 0.8453 0.9144 0.8785 1285
PERCENT 0.6800 0.7907 0.7312 43
PERSON 0.8873 0.9395 0.9127 645
PHONE 0.9459 1.0000 0.9722 35
TEMPERATURE 0.0000 0.0000 0.0000 2
TIME 0.7436 0.7838 0.7632 185
URL 0.9583 1.0000 0.9787 23
ZIP 1.0000 1.0000 1.0000 6
micro avg 0.8137 0.8779 0.8446 3816
macro avg 0.7574 0.8125 0.7830 3816
weighted avg 0.8143 0.8779 0.8447 3816
How to use
Because of how WangchanBERTa handles spaces, the standard pipeline("ner", ...) gives incorrect results on Thai. Use PyThaiNLP for word-level tokenization and run the model manually:
python
1import torch
2from transformers import AutoTokenizer, AutoModelForTokenClassification
3from pythainlp.tokenize import word_tokenize
45MODEL ="1amBanana/WangchanBERTa-NER-v0"6SPACE ="<_>"78tokenizer = AutoTokenizer.from_pretrained(MODEL)9model = AutoModelForTokenClassification.from_pretrained(MODEL).eval()10id2label = model.config.id2label
1112defpredict_ner(text:str):13 words = word_tokenize(text, engine="newmm", keep_whitespace=True)14 norm =[SPACE if(w isNoneornot w.strip())else w for w in words]15 enc = tokenizer(norm, is_split_into_words=True, return_tensors="pt",16 truncation=True, max_length=416)17with torch.no_grad():18 logits = model(**enc).logits[0]19 pred_ids = logits.argmax(-1).tolist()20 word_ids = enc.word_ids(batch_index=0)21 word_tags ={}22for p, w inzip(pred_ids, word_ids):23if w isNoneor w in word_tags:24continue25 word_tags[w]= id2label[int(p)]26return[(words[i], word_tags.get(i,"O"))for i inrange(len(words))]2728text ="นายมะลิวาโย บุญรักษา อาศัยอยู่ที่อำเภอนางรอง จังหวัดบุรีรัมย์ อายุ 99 ปี เพิ่งเรียนจบจากมหาวิทยาลัยเชียงใหม่"29result = predict_ner(sample)30pd.DataFrame(result, columns=["word","tag"])
Here is the result of the block code:
word
tag
0
นาย
B-PERSON
1
มะลิ
I-PERSON
2
วาโย
I-PERSON
3
I-PERSON
4
บุญ
I-PERSON
5
รักษา
I-PERSON
6
O
7
อาศัย
O
8
อยู่
O
9
ที่
O
10
อำเภอ
B-LOCATION
11
นางรอง
I-LOCATION
12
O
13
จังหวัด
B-LOCATION
14
บุรีรัมย์
I-LOCATION
15
O
16
อายุ
O
17
O
18
99
B-AGO
19
I-AGO
20
ปี
I-AGO
21
O
22
เพิ่ง
O
23
เรียนจบ
O
24
จาก
O
25
มหาวิทยาลัยเชียงใหม่
B-ORGANIZATION
Limitations
Domain. The corpus is dominated by news and PR text. Performance on informal / social / domain-specific Thai (medical, legal, financial) will be lower.
IOB scheme only. Non-nested entities. For overlapping entities, see Thai N-NER.
<_> preprocessing. Required for correct inference. The default transformers NER pipeline does not do this automatically and will produce wrong tags.
Case. The base model is uncased; casing is lost.
Max length. Inputs longer than 416 subword tokens are truncated.
Citation
Base model:
bibtex
1@misc{lowphansirikul2021wangchanberta,
2 title={WangchanBERTa: Pretraining transformer-based Thai Language Models},
3 author={Lalita Lowphansirikul and Charin Polpanumas and Nawat Jantrakulchai and Sarana Nutanong},
4 year={2021},
5 eprint={2101.09635},
6 archivePrefix={arXiv},
7 primaryClass={cs.CL}
8}
Dataset:
bibtex
1@dataset{wannaphong_phatthiyaphaibun_2024_10795907,
2 author = {Wannaphong Phatthiyaphaibun},
3 title = {Thai NER 2.2},
4 month = mar,
5 year = 2024,
6 publisher = {Zenodo},
7 version = {2.2},
8 doi = {10.5281/zenodo.10795907},
9 url = {https://doi.org/10.5281/zenodo.10795907}
10}