Skip to content

Unicode and character tokenizer profiles (0.2.0) - #4

Merged
kholdrex merged 3 commits into
mainfrom
unicode-tokenizers
Oct 11, 2026
Merged

kholdrex merged 3 commits into
mainfrom
unicode-tokenizers

Conversation

@kholdrex

Copy link
Copy Markdown
Member

rouge-score's tokenizer keeps only [a-z0-9], so ROUGE is zero for Russian, Arabic, Hindi, Chinese and most other languages. This adds two opt-in, versioned tokenization profiles that run natively, including in parallel score_batch:

  • UnicodeTokenizer (unicode-v1): NFKC, full Unicode case folding, NFKC again, then the words found by the Unicode word boundary rules (UAX #29) that contain an alphabetic character or a number. For languages that separate words with spaces.
  • CharacterTokenizer (character-v1): the same normalization, then one token per grapheme cluster that contains an alphabetic character or a number. For character-level ROUGE, a common convention for Chinese.

The default tokenizer and its scores are unchanged.

A profile name must always mean the same tokens, so all Unicode data comes from exactly pinned crates (unicode-normalization 0.1.25, unicode-segmentation 1.13.3, ICU4X 2.3.0 and its data crates, all Unicode 17.0) rather than from the standard library, whose tables follow the compiler version. ICU4X raises the minimum Rust version to 1.88.

Tests: token fixtures for several scripts, normalization idempotence, Unicode 17 characters, and native scoring checked against rouge-score fed the same tokens through its custom-tokenizer path (1,000 random multilingual pairs per profile, plus score_batch, score_multi and pickling).

On 5,000 pairs of 60-word texts, compared with rouge-score plus a Python tokenizer: Russian with unicode-v1 is about 11× faster per call and 50× with score_batch on 8 cores; Chinese with character-v1 about 50× and 200×.

The rouge-score tokenizer keeps only [a-z0-9], so ROUGE is zero for most
languages. Users work around it with Python tokenizers, which also turns
off the parallel path. Two opt-in, versioned profiles now run natively:

- unicode-v1: lowercase, NFKC, UAX #29 words that contain a letter or a
  number, for languages that separate words with spaces.
- character-v1: lowercase, NFKC, one token per grapheme cluster, for
  character-level ROUGE such as in Chinese.

The unicode-segmentation and unicode-normalization versions are pinned so
a profile keeps its behaviour; both use Unicode 17.0, which needs Rust
1.85. The default tokenizer and its scores are unchanged.
str::to_lowercase and char::is_alphanumeric follow the Unicode version
of the compiler, so the same profile could tokenize differently depending
on the Rust release a wheel was built with. Case folding and the
alphabetic/number test now come from ICU4X 2.3 (Unicode 17.0), pinned
like the normalization and segmentation crates.

Normalization is NFKC, full case folding, then NFKC again. Folding before
NFKC left compatibility capitals such as U+1D400 uppercase, and folding
is context-free, unlike lowercasing with its final-sigma rule. ICU4X
needs Rust 1.88.
icu_casemap and icu_properties accept any 2.3.x data release, so a
downstream lockfile could pick newer tables and change tokens.
@kholdrex
kholdrex merged commit d67b42d into main Oct 11, 2026
7 checks passed
@kholdrex
kholdrex deleted the unicode-tokenizers branch October 11, 2026 03:11
@kholdrex kholdrex mentioned this pull request Oct 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant