Evaluation (burmesenlp bench)¶
Boundary-level precision/recall/F1 for word segmentation, against gold corpora fetched at runtime — never vendored, since every corpus wired in is CC BY-NC-SA or stricter.
burmesenlp bench --scheme nopipe # word_tokenize() alone (default)
burmesenlp bench --scheme pipe # word_tokenize() + BMWE, compound-preserving gold
burmesenlp bench --scheme both # run both, report separately
burmesenlp bench --corpus alt # independent corpus, independent of the lexicon
Read this before quoting a number from here¶
The myPOS nopipe aggregate (F1 0.9507) is train-on-test and must never
be quoted alone. The bundled lexicon's word-form inventory is derived
from myPOS — verified empirically, not assumed: 100% of the lexicon's
~24k word-forms appear in myPOS v3.0's gold vocabulary, which does not
happen between independently-compiled resources by chance. word_tokenize()
is greedy longest-match over that lexicon, so scoring it against myPOS
measures how well it recovers the corpus its own dictionary came from,
not general segmentation ability.
Three numbers, not one:
| P | R | F1 | n | |
|---|---|---|---|---|
| myPOS nopipe, in-lexicon boundaries | 0.9900 | 0.9161 | 0.9516 | 43196 sentences (97.72% of gold vocabulary is in-lexicon) |
| myPOS nopipe, OOV boundaries (honest generalization estimate) | 0.6078 | 0.9465 | 0.7403 | same corpus, 2.28% of vocabulary |
| ALT (independent corpus, independent word scheme, not implicated in the lexicon contamination) | 0.8821 | 0.9274 | 0.9042 | 20106 sentences |
The ~21-point F1 gap between in-lexicon and OOV strata on myPOS is how
much of the aggregate 0.9507 is memorized vocabulary rather than method.
--scheme nopipe always prints this stratified breakdown, with the
caution, alongside the aggregate — never emit the aggregate alone.
Why boundary-level, not token accuracy¶
Each segmentation is a set of boundary character offsets; scoring is set overlap. One wrong boundary costs exactly one boundary — token accuracy would double-count it (a wrong split corrupts two adjacent tokens).
Two schemes are not the same gold standard¶
myPOS v3.0 ships two word-boundary conventions in the same corpus:
nopipe— compound words flattened; matchesword_tokenize()'s own granularity (compound/idiom merging is the separate, later BMWE stage).pipe— myPOS's own\|compound marker preserved and merged into one gold token, for scoringword_tokenize()+ BMWE together.
Never compare a nopipe score to a pipe score, or to any published
number that doesn't state its scheme — myPOS and ALT disagree with each
other about what a Burmese word is, and published Burmese segmentation
figures (90-94%, ~80%) are unusable as targets for exactly this reason.
The output header always states corpus, scheme, n, and which pipeline
stages were active.
Before trusting a pipe-scheme F1¶
BMWE's compound trie and myPOS's \| convention were built independently.
--scheme pipe always prints a sample of BMWE-vs-gold disagreement
positions (spread across distinct sentences, not just the first N) before
the score — read it. A large gap between nopipe and pipe F1 is not
automatically "BMWE is broken"; it can just as easily mean the gold
scheme is measuring something broader than compound/idiom merging (e.g.
myPOS marks productive grammatical derivations and proper-noun phrases
with \| too, which are arguably other pipeline stages' jobs, not
BMWE's).
--category¶
--scheme pipe --category buckets the disagreement audit into
date_number / productive_derivation / proper_noun / genuine_compound
(the actual work queue for growing BMWE's trie) instead of a flat sample.
On the full corpus: 1609 date/number, 5084 productive grammatical
derivation (-မှု/-ရေး nominalizers — a regular grammar pattern, not
idiom material), 2397 proper nouns the gazetteer already catches, and
17583 in the genuine_compound fallback bucket. That fallback bucket is
a ceiling, not a confirmed count — it still contains definitional cases
the three heuristics don't catch (e.g. a span crossing a grammatical
particle like ကို/၏/သို့/နှင့်), so treat it as a work queue to skim, not
a ready-made bug list.
The proper_noun heuristic checks the toolkit's own gazetteer — so a
known gazetteer gap falls through to genuine_compound too. Two
confirmed examples sitting there right now: ရန်ကုန်မြို့ ("Yangon City")
and ဗိုလ်ချုပ်အောင်ဆန်း ("General Aung San") are not gazetteer hits.
That's unclaimed work between BMWE and the gazetteer, not a bug in this
categorizer — noted here, not fixed.
--freeze-strata — measuring a lexicon/gazetteer expansion honestly¶
OOV is defined relative to the lexicon. Expanding the lexicon moves tokens from OOV to in-vocabulary, so a naive before/after comparison of the OOV stratum scores a different token set each time — that measures set composition, not accuracy.
burmesenlp bench --scheme nopipe --freeze-strata baseline.json # run BEFORE any expansion — creates the snapshot
# ... expand the lexicon ...
burmesenlp bench --scheme nopipe --freeze-strata baseline.json # run AFTER — reuses the same snapshot
The first run for a given path creates it from the current lexicon —
run this before changing a single entry. Every later run with the same
path reuses that frozen vocabulary to decide stratum membership, while
word_tokenize() itself still uses the live (possibly-expanded) lexicon
to produce hypotheses — so a name that moves from OOV to correctly
segmented after expansion shows up as an OOV-stratum improvement, not as
a token quietly relabeled into the IV stratum. Works for both --corpus
mypos and --corpus alt. Always report the overall (unstratified) F1
alongside — it's stable under vocabulary changes and can't be gamed this
way.
ALT is held out — --final is required to score it¶
Decision, enforced in the tool, not just documented: iterate against
--corpus mypos during development (already known-contaminated, so
iterating on it costs nothing additional); score --corpus alt only for
a measurement you intend to report. Repeatedly checking ALT during a
tuning loop ("add names → score ALT → repeat") is the myPOS contamination
story again, arriving more slowly.
--corpus alt refuses to run without --final. Every --final run is
appended to a persistent log (alt_holdout_log.jsonl in the corpus cache
dir) with a timestamp and --reason; a second --final run prints a
warning listing every prior run, so repeat use is visible in any report
rather than silently possible. Not a hard block past the first use —
enforcement is the deliberate flag plus the audit trail, not a counter.
Gazetteer changes are invisible to bench — know this before measuring one¶
word_tokenize() (what --scheme nopipe scores) is pure lexicon
longest-match; +BMWE (--scheme pipe) adds idiom merging. Neither
consults the gazetteer — GazetteerManager runs strictly after both,
in BurmeseNLP._analyze(), only labeling spans of already-final tokens.
It never merges tokens or feeds back into segmentation. Adding entries to
corpus/gazetteers/*.json alone therefore cannot move any bench score,
regardless of the data's quality — checked directly against
WordSegmenter, BMWEEngine, and tokenize/: zero references to
gazetteer anywhere in that path.
For a gazetteer expansion to be bench-measurable, confirmed entries
need to also go into the lexicon (as NOUN — the lexicon's
POS_TAGS has no PROPN; that tagging is applied downstream, once the
gazetteer also recognizes the span, via the entity-lock mechanism in
pipeline/__init__.py). Gazetteer-only additions are real work (entity
typing, attributes) but will read as a flat 0.0000 change here — that's
wiring, not evidence about whether named entities matter.
--diff¶
Compares against a pre-computed external segmenter's output (one sentence per line, space-separated words, line-aligned to the gold corpus) — e.g. myWord or mmCRFseg output you generated yourself:
bench cannot run third-party segmenters itself.
--export-text: getting a --diff-compatible file for an external tool¶
--diff requires the external file to be exactly line-aligned to the
gold corpus — same sentence count, same order, same underlying text. Producing
that alignment by hand is easy to get wrong (the reference text used
internally is the canonicalized, reconstructed, unsegmented string per
sentence, not the raw corpus line). --export-text writes exactly that:
burmesenlp bench --corpus mypos --export-text sentences.txt
# run your external segmenter against sentences.txt yourself, e.g.:
python myword.py word -ub dict_ver1/unigram-word.bin -bb dict_ver1/bigram-word.bin sentences.txt myword_output.txt
burmesenlp bench --corpus mypos --diff myword=myword_output.txt
--corpus alt --export-text requires --final too, same as scoring ALT
directly — handing out ALT's sentences for an external tool to segment is
still spending the held-out measurement, not a scoring-free side channel.
Measured: burmesenlp vs. myWord (Viterbi word segmenter)¶
Run once, reported honestly (myWord: MIT-licensed, ye-kyaw-thu/myWord,
cloned locally for this one-time comparison, not vendored or run in CI).
500 myPOS v3.0 sentences, nopipe scheme, same sample scored both ways:
| precision | recall | F1 | |
|---|---|---|---|
burmesenlp (word_tokenize()) |
0.9850 | 0.9159 | 0.9492 |
| myWord (Viterbi, unigram+bigram) | 0.9111 | 0.9952 | 0.9513 |
Near-tie, opposite error profiles: burmesenlp's greedy dictionary longest-match under-splits (misses real boundaries — lower recall) but rarely over-splits (higher precision); myWord's statistical decoder does the reverse. Worth stating plainly: myWord's dictionaries are trained on its own separate "myWord Corpus Ver. 1.0" (12M+ words, per its README), not myPOS — unlike burmesenlp's bundled lexicon, which is derived from myPOS (100% word-form overlap, see the train-on-test caution elsewhere in this doc). So this comparison is not neutral ground: it has whatever advantage myPOS-vocabulary familiarity gives burmesenlp, and myWord is still essentially tied on F1 without that advantage on this specific corpus.
Licensing¶
Every gold corpus here is CC BY-NC-SA (NonCommercial) — cached locally on
first use, never shipped in the burmesenlp wheel. This also means: do not
persist data derived from these corpora (n-gram counts, frequency
tables) into Apache-2.0 code — see the burmesenlp.bench module
docstring.