North Sami NLP Grammar

Finite state and Constraint Grammar based analysers, proofing tools and other resources

North Sámi speller accuracy review — path to 95% top‑1 / 99% top‑5

Date: 2026‑08‑20 (outcomes addendum 2026‑08‑21; round‑3 addendum 2026‑08‑22) Scope: lang-sme desktop speller (se.zhfst) suggestion quality Author: Divvun tooling review (automated analysis + build-system survey)

Round 3 (addendum, 2026‑08‑22): per-category analysis and cost recalibration

The per-category programme recommended below was executed. First finding: the round‑2 engine fixes (out-of-alphabet input handling, unknown-output enumeration) were worth far more than previously reported — re-measuring with the current engine put the true starting point at 83.33% top‑1 / 96.02% top‑5 / 98.50% offered-at-all under the analysis config, and cut the never-offered pool from 494 to 160. All round‑2 reports had been produced with a stale binary; the numbers in the 2026‑08‑21 addendum understate what had already shipped.

Round‑3 changes, each validated by a full A/B run over the gold set:

  1. Edit-cost recalibration (editdist.default-new.regex), driven by a re-ranking simulator over the per-word reports that predicts the net effect of any single-edit cost change before a rebuild. Landed: same-letter single↔double consonant (gradation slips, the canonical Sámi writing error) 15→10 for all consonants — previously only 8 letters had a dedicated class, priced the same as cross-consonant changes; o↔u and e↔i 15→8; word-final s↔š at 8; word-final -ii-iid (illative vs genitive-plural) at 10; initial-uppercase 25→12. Net +39 top‑1 in isolation. The delete direction of the -iid rule was tried and reverted — it promoted spurious short forms (net negative), and the simulator’s per-lever attribution caught it in one iteration.
  2. 19 new chunk rules (strings.default.regex), mined from the 425 residual failures with per-rule collateral measurement: every rule’s left side was checked against all 9,076 attested correct forms; chunks impossible in correct Sámi (ä, járvv, unaccented aigge…) ride at weight 1, chunks whose left side occurs in real words ride at 25 so they never undercut a letter edit. Net +28 top‑1, +40 top‑5. The Norwegian-definite family (-hallenis, -festivalenis) was evaluated and rejected: any covering rule fires on hundreds of correct locative/diminutive forms.
  3. divvunspell case-handling fixes (benefit all Giella languages): all-caps detection is no longer defeated by a single stray lower-case character (RÁðI now yields RÁĐI; previously junk like R6-I), and corrections for irregular-cased inputs are re-capitalized (EOvddidatOvddidat, ŦMuitalusatMuitalusat; previously emitted lower-case and never matched).
  4. Corpus hygiene: 7 rows — 2 still expected the substandard ollusat (contradicting its +Err/Orth reclassification), 5 expected a casing that contradicted the input’s case context.
Metric (n‑best 100) True round‑2 (re-measured) Round 3
Top‑1 8,896 (83.33%) 8,968 (84.02%)
Top‑5 10,250 (96.02%) 10,305 (96.53%)
Correct offered at all 10,515 (98.50%) 10,543 (98.76%)
Never offered 160 132
False accepts / false negatives 2 / 0 0 / 0

Ceiling analysis (the honest part). With the current candidate generator, a perfect re-ranker — one that always picks the intended word whenever it sits within a given weight window of the current winner — reaches: 88.2% top‑1 (window +5), 90.9% (+10), 93.0% (+15), 94.4% (+20), 96.0% (+30). 95% top‑1 therefore demands near-flawless discrimination across a ±30-unit window — two full edit operations — which isolated-word unigram frequency cannot deliver: the median winner-vs-intended gap is a single weight unit, and per-case attribution shows the edit model prefers the wrong winner in 56% of near-misses, lexicon frequency in 23%, with the rest mixed. The credible remaining levers, in order: a properly trained frequency model (the current LM compresses the whole corpus into ≤50 weight units; its estimates alone account for 312 measured near-miss losses), context-aware re-ranking (the grammar-checker pipeline is the natural home — a speller that sees only one word at a time cannot choose between boahtte/bohte when both are one cheap edit away), and continued chunk mining against fresh error corpora. 99% top‑5 likewise first requires pushing offered-at-all above 99%: of the 132 still-missing, ~50 are proper-noun rewrites (NäkkeljogaNeahčiljoga) and ~65 are distance‑4+ one-offs no generalizable rule covers — the realistic route is more curated Err/Orth-style knowledge, not larger edit budgets.

Sign‑off: make check green under the Divvun HFST toolchain (this round touches only error-model weights, chunk rules, and the test corpus — no analyser sources); zero false accepts and zero false negatives for the first time.

Outcomes (addendum, 2026‑08‑21)

The programme below was executed over two days. Results on the corrected corpus (10,679 pairs; three wrong expected forms fixed, one real-word pair and one non-typo pair removed, five mojibake lines repaired):

Metric (n‑best 100) Before After Production config
Top‑1 80.98% 83.3% 83.2%
Top‑5 93.94% 96.0% 94.8%
Correct offered at all 97.04% 98.5% 95.3%
No suggestions 18 0
Typo accepted as correct 11 0

Landed: the LM maxweight raise (10→50, recommendation A); the missing confusion pairs ŋ↔n, i↔y, s↔ŧ and chunk rewrites incl. the nj↔ŋ digraph family (B, C); lexicon curation of the false accepts and missing forms (E); the +Cmp penalty confirmed already optimal (G). In divvunspell (affects all Giella languages): the n‑best recall bug fixed, the search converted to best‑first with exact state deduplication — 29% faster mean, 7× better worst case, identical results. Recommendation F was implemented not as pair enumeration but as extract-errorth-strings.py: 5,238 stem-level alternation rules derived from the lexicon’s +Err/Orth* entries into the strings component — beating the enumerated-pairs approach on every metric and correcting misspelled stems inside dynamic compounds no pair list could contain (make errorth-regen reproduces the rules byte-for-byte). Known cost: se.zhfst grows to ~131 MB (from 56 MB); reduction options (rule-support thresholds, non-determinised error models at a search-time cost) are understood if size matters downstream.

Sign‑off: full make check green under both the C++ 3.17.1 toolchain and the Divvun HFST (Rust) toolchain, identical test sets. Remaining distance to 95/99 is ranking-dominated; the per-category tracking recommended below is the tool for the next iteration.

Summary

Measured against the full 10,682‑pair tools/spellcheckers/test/typos.tsv gold set, the current speller places the correct suggestion first for 80.98% of typos and in the top five for 93.94%. The two dominant, fixable causes are:

  1. Lexical frequency barely participates in ranking. The corpus language model compresses the entire frequency spectrum of the language into 8 weight units, while a single edit in the error model costs 5–59. Rare wrong-lemma neighbours routinely outrank frequent correct words, and exact weight ties are broken alphabetically.
  2. The error model cannot reach 316 of the corrections at all, 255 of which are ≥3 character edits away — beyond its 2‑edit budget. Most of these are single contiguous stem or compound alternations (e.g. kultuvra‑kultur‑) that targeted string patterns can cover without enlarging the edit budget.

With the recommended changes, ~90% top‑1 and ~97% top‑5 are high-confidence outcomes; 95% / 99% is reachable but requires the full programme, including pattern mining for stem alternations and exploiting the lexicon’s +Err/Orth knowledge. The corpus itself is clean (ceiling 99.94%), so the targets are not data-limited.

Methodology

Current results

Metric Count Rate
Correct suggestion first (top‑1) 8,650 / 10,682 80.98%
Correct in top 5 10,035 93.94%
Correct anywhere in top 100 10,366 97.04%
Correct never offered 316 2.96%
Typo accepted as correct (false negative) 11 0.10%
Correct word rejected (false accept) 0 0%

Gap to targets: 95% top‑1 needs +1,498 promotions to first place; 99% top‑5 needs +541 into the top five.

Anatomy of the 2,032 top‑1 misses

Bucket Count Diagnosis
Correct at position 1–4 1,385 Ranking failure: a wrong one-edit neighbour outranks the right correction
— of which exact weight ties 139 Tie-break is alphabetical; the winner sorts before the expected word in all 139
— weight gap ≤ 5 units 507 Flippable with modest cost tuning
— weight gap > 10 units 588 Need category-level repricing or frequency separation
Correct at position 5–99 331 87% are overpriced by >20 units
Correct absent at n=100 316 310 are accepted by the lexicon but never generated; only 4 words are genuinely missing
— of the absent: edit distance ≥3 255 Beyond the 2‑edit model. 180 are one contiguous ≤6‑char mid-word chunk (stem/compound alternation)

Confusion analysis across all misses (top pairs, input→expected): a→á 216, á→a 138, consonant gemination/degemination (the del/ins t, k, l mass, several hundred combined), o→u 79, e→i 73, u→o 67, d→đ 49, s→š 43, j→ŋ 43, i→e 48.

The wrong corrections that win are dominated by wrong-lemma a↔á flips (140), spurious vowel insertions/deletions, and ii↔i — i.e. the cheap edits applied to the wrong word.

Root causes

1. The frequency model is crushed (ranking)

tools/spellcheckers/weights/config.json sets maxweight: 10. The resulting unigram LM (built by giella-core/scripts/corpus2unigramlm.py over weights/spellercorpus.raw.txt, 3.1M tokens / 166k types) maps the most frequent word in the language (ja, weight 1.98) and a hapax (9.54) into a spread of 8 units, with out-of-corpus words at 10.0. Suggestion ranking is frequency + tag penalties + error-model cost, and error-model costs run 5–59 with +Cmp at 45 — so frequency is nearly irrelevant. Symptoms:

2. Edit costs contradict the observed error distribution (ranking)

tools/spellcheckers/editdist.default-new.regex (the hand-written model; it shadows the older generated path) prices:

The result: for a typo whose real fix is gemination or a vowel-pair substitution, some other word one cheap á-flip away wins. This is the single largest miss category.

3. The 2‑edit budget cannot reach 316 corrections (coverage)

Of the 316 never-offered corrections, 310 are in the lexicon. 255 are ≥3 raw edits from the typo — but 180 of those 255 differ by one contiguous mid-word chunk of ≤6 characters: compound-form alternations (kultuvradoalut → kulturdoalut), stem alternations, colon-inflection forms (Ab:a → Ab). These are systematic morphology, not random noise, and are addressable as single weighted string-rewrite patterns. Raising EDIT_DISTANCE to 3 is not recommended: the error model is already 245 MB with 1.27 s worst-case lookups.

4. divvunspell n‑best pruning leaks recall (client)

For 9 of the 43 words that hit the n=100 suggestion cap, the correct word does surface at -n 2000 — one at true post-reweight rank 12. The n‑best cut is applied against pre-reweight raw weights, so genuinely good candidates are pruned before reordering. This also explains previously observed n‑best-sensitivity between equivalent archives.

5. Small lexicon issues

# Change Where Effort Expected effect
A Raise LM maxweight from 10 to ~30–50 (sweep empirically) tools/spellcheckers/weights/config.json 1 line + acceptor rebuild (~5 min) per trial Largest single lever: restores frequency separation across all ranking buckets; fixes alphabetical ties as a side effect
B Recalibrate edit costs against the confusion matrix: gemination classes cheaper; add o↔u, e↔i, a↔e/i (~10–15), ŋ↔n, nj↔ŋ (~5–10), i↔y; keep generic fallbacks as-is tools/spellcheckers/editdist.default-new.regex Iterative; errmodel-only rebuilds are minutes Attacks the 507 near-miss ranking failures and much of the deep bucket
C Add chunk-rewrite patterns for the systematic stem/compound alternations and colon-inflection paradigms strings.default.regex, final_strings.default.txt Pattern mining, iterative The only viable route to 99% top‑5: covers the 255 distance‑3+ absents at one edit each
D Fix n‑best pruning to cut on post-reweight weights divvunspell Client-side fix Recovers up to ~43 absents, some at true rank ≤31; benefits all Giella languages
E Lexicon curation: add the 4 missing forms; restrict kultuvra‑ in compound-left position; review the 11 false accepts src/fst/ lexc sources Small Removes the false negatives; ~15 words
F Compile the lexicon’s +Err/Orth* pairs (incl. the dedicated Err/Orth-a-á class) into an error-model component instead of hard-deleting them via remove-error-strings.regex build system + src/fst/ Medium project Injects curated normative-error knowledge; closes part of the final gap
G Tune +Cmp +45 (per compound boundary) once A lands weights/tags.reweight 1 line per trial Currently suppresses legitimate compound suggestions; must be re-balanced against the new LM range

Housekeeping found along the way: accents.default.regex and its composition rule (Makefile.mod-desktop-hfst.am:105‑122) are dead code — the target is never consumed; strings.default.txt, initial_letters.*, DEFAULT_WEIGHT, USE_SWAPS, CORPUS_SIZE are all inert in the desktop configuration; clean_weight_corpus.sh output is built but never read (the LM tokenises the raw corpus itself). These should be deleted or deliberately revived to prevent future tuning-by-dead-knob.

Explicit non-recommendation: do not seed words.default.txt from typos.tsv pairs. That is training on the evaluation set; it would inflate the reported metric without improving real-world accuracy. Whole-word replacement entries should come from independent corpora (e.g. SIKOR error annotations).

Feasibility

Reproduction

# Build (Divvun HFST toolchain first in PATH):
./configure --without-forrest --disable-syntax --enable-spellers \
            --with-backend-format=openfst-tropical
make -j4

# Evaluate:
divvunspell accuracy -c cfg-n100.json -o report.json \
    tools/spellcheckers/test/typos.tsv tools/spellcheckers/se.zhfst
# cfg-n100.json = production config with "n-best": 100, "beam": null
Last updated: Edit on GitHub

Sitemap

On this page