use super::tokenize::{Tokenize, TokenizeError};
use std::collections::HashMap;
pub struct WordPieceTokenizer {
token_to_id: HashMap<String, u32>,
id_to_token: Vec<String>,
unk_token_id: u32,
max_word_len: usize,
do_lower_case: bool,
continuation_prefix: String,
handle_chinese_chars: bool,
clean_text: bool,
cls_token_id: Option<u32>,
sep_token_id: Option<u32>,
pad_token_id: Option<u32>,
added: Option<super::added::AddedTokens>,
special_decode: rustc_hash::FxHashSet<u32>,
}
impl WordPieceTokenizer {
pub fn new(
vocab: Vec<String>,
unk_token_id: u32,
max_word_len: usize,
do_lower_case: bool,
) -> Self {
let prefix = if vocab.iter().any(|k| k.starts_with("##")) {
"##".to_string()
} else {
String::new()
};
Self::with_options(
vocab,
unk_token_id,
max_word_len,
do_lower_case,
true,
true,
prefix,
)
}
#[allow(clippy::too_many_arguments)]
pub fn with_options(
vocab: Vec<String>,
unk_token_id: u32,
max_word_len: usize,
do_lower_case: bool,
handle_chinese_chars: bool,
clean_text: bool,
continuation_prefix: String,
) -> Self {
let mut token_to_id = HashMap::with_capacity(vocab.len());
for (id, token) in vocab.iter().enumerate() {
token_to_id.insert(token.clone(), id as u32);
}
let cls_token_id = token_to_id.get("[CLS]").copied();
let sep_token_id = token_to_id.get("[SEP]").copied();
let pad_token_id = token_to_id.get("[PAD]").copied();
Self {
token_to_id,
id_to_token: vocab,
unk_token_id,
max_word_len,
do_lower_case,
continuation_prefix,
handle_chinese_chars,
clean_text,
cls_token_id,
sep_token_id,
pad_token_id,
added: None,
special_decode: rustc_hash::FxHashSet::default(),
}
}
pub fn with_added_tokens(mut self, map: &rustc_hash::FxHashMap<String, u32>) -> Self {
self.added = super::added::AddedTokens::new(map);
self
}
pub fn with_special_decode_ids(mut self, ids: rustc_hash::FxHashSet<u32>) -> Self {
self.special_decode = ids;
self
}
pub fn cls_token_id(&self) -> Option<u32> {
self.cls_token_id
}
pub fn sep_token_id(&self) -> Option<u32> {
self.sep_token_id
}
pub fn pad_token_id(&self) -> Option<u32> {
self.pad_token_id
}
pub fn unk_token_id(&self) -> u32 {
self.unk_token_id
}
fn basic_tokenize(&self, text: &str) -> Vec<String> {
let cleaned;
let text = if self.clean_text {
cleaned = clean_text(text);
cleaned.as_str()
} else {
text
};
let text = if self.handle_chinese_chars && text.chars().any(is_chinese_char) {
let mut s = String::with_capacity(text.len() + 8);
for c in text.chars() {
if is_chinese_char(c) {
s.push(' ');
s.push(c);
s.push(' ');
} else {
s.push(c);
}
}
s
} else {
text.to_string()
};
let text = if self.do_lower_case {
let lowered = text.to_lowercase();
strip_accents(&lowered)
} else {
text
};
let mut tokens = Vec::new();
for word in text.split_whitespace() {
split_on_punctuation(word, &mut tokens);
}
tokens
}
fn wordpiece_tokenize(&self, word: &str) -> Vec<u32> {
let chars: Vec<char> = word.chars().collect();
if chars.len() > self.max_word_len {
return vec![self.unk_token_id];
}
let mut ids = Vec::new();
let mut start = 0;
while start < chars.len() {
let mut end = chars.len();
let mut matched = None;
while start < end {
let raw: String = chars[start..end].iter().collect();
let lookup = if start == 0 || self.continuation_prefix.is_empty() {
raw
} else {
format!("{}{}", self.continuation_prefix, raw)
};
if let Some(&id) = self.token_to_id.get(&lookup) {
matched = Some(id);
break;
}
end -= 1;
}
match matched {
Some(id) => {
ids.push(id);
start = end;
}
None => return vec![self.unk_token_id],
}
}
ids
}
}
impl WordPieceTokenizer {
fn encode_ordinary(&self, text: &str) -> Vec<u32> {
let words = self.basic_tokenize(text);
let mut ids = Vec::new();
for word in &words {
ids.extend(self.wordpiece_tokenize(word));
}
ids
}
}
impl Tokenize for WordPieceTokenizer {
fn encode(&self, text: &str) -> Vec<u32> {
match &self.added {
Some(added) => added.encode_with(text, |gap| self.encode_ordinary(gap)),
None => self.encode_ordinary(text),
}
}
fn decode(&self, ids: &[u32]) -> Result<String, TokenizeError> {
if self.continuation_prefix.is_empty() {
self.decode_without_prefix(ids)
} else {
self.decode_with_prefix(ids)
}
}
fn vocab_size(&self) -> usize {
self.id_to_token.len()
}
}
impl WordPieceTokenizer {
pub fn token_surface(&self, id: u32) -> Option<String> {
self.id_to_token.get(id as usize).cloned()
}
fn decode_with_prefix(&self, ids: &[u32]) -> Result<String, TokenizeError> {
let mut pieces = Vec::with_capacity(ids.len());
for &id in ids {
let token = self
.id_to_token
.get(id as usize)
.ok_or(TokenizeError::InvalidTokenId(id))?;
if is_special_token(token) || self.special_decode.contains(&id) {
continue;
}
if let Some(stripped) = token.strip_prefix(self.continuation_prefix.as_str()) {
pieces.push(stripped.to_string());
} else {
if !pieces.is_empty() {
pieces.push(" ".to_string());
}
pieces.push(token.to_string());
}
}
Ok(cleanup_tokenization(&pieces.join("")))
}
fn decode_without_prefix(&self, ids: &[u32]) -> Result<String, TokenizeError> {
let mut parts = Vec::with_capacity(ids.len());
for &id in ids {
let token = self
.id_to_token
.get(id as usize)
.ok_or(TokenizeError::InvalidTokenId(id))?;
if is_special_token(token) || self.special_decode.contains(&id) {
continue;
}
parts.push(token.as_str());
}
Ok(cleanup_tokenization(&parts.join(" ")))
}
}
fn cleanup_tokenization(s: &str) -> String {
s.replace(" .", ".")
.replace(" ?", "?")
.replace(" !", "!")
.replace(" ,", ",")
}
fn is_special_token(token: &str) -> bool {
matches!(token, "[CLS]" | "[SEP]" | "[PAD]" | "[UNK]" | "[MASK]")
|| (token.starts_with("[unused") && token.ends_with(']'))
}
fn strip_accents(text: &str) -> String {
use unicode_general_category::{get_general_category, GeneralCategory};
use unicode_normalization::UnicodeNormalization;
text.nfd()
.filter(|c| get_general_category(*c) != GeneralCategory::NonspacingMark)
.collect()
}
fn split_on_punctuation(word: &str, out: &mut Vec<String>) {
let mut current = String::new();
for c in word.chars() {
if is_punctuation(c) {
if !current.is_empty() {
out.push(std::mem::take(&mut current));
}
out.push(c.to_string());
} else {
current.push(c);
}
}
if !current.is_empty() {
out.push(current);
}
}
fn clean_text(text: &str) -> String {
use unicode_general_category::{get_general_category, GeneralCategory};
let mut out = String::with_capacity(text.len());
for c in text.chars() {
if c == '\0' || c == '\u{fffd}' {
continue;
}
let is_keepable_ws = matches!(c, '\t' | '\n' | '\r');
if !is_keepable_ws {
match get_general_category(c) {
GeneralCategory::Control
| GeneralCategory::Format
| GeneralCategory::Surrogate
| GeneralCategory::PrivateUse
| GeneralCategory::Unassigned => continue,
_ => {}
}
}
if c == ' ' || is_keepable_ws || get_general_category(c) == GeneralCategory::SpaceSeparator
{
out.push(' ');
} else {
out.push(c);
}
}
out
}
fn is_chinese_char(c: char) -> bool {
let cp = c as u32;
(0x4E00..=0x9FFF).contains(&cp)
|| (0x3400..=0x4DBF).contains(&cp)
|| (0x20000..=0x2A6DF).contains(&cp)
|| (0x2A700..=0x2B73F).contains(&cp)
|| (0x2B740..=0x2B81F).contains(&cp)
|| (0x2B820..=0x2CEAF).contains(&cp)
|| (0xF900..=0xFAFF).contains(&cp)
|| (0x2F800..=0x2FA1F).contains(&cp)
}
fn is_punctuation(c: char) -> bool {
matches!(c, '\x21'..='\x2F' | '\x3A'..='\x40' | '\x5B'..='\x60' | '\x7B'..='\x7E')
|| c.is_ascii_punctuation()
|| {
let cat = unicode_general_category::get_general_category(c);
matches!(
cat,
unicode_general_category::GeneralCategory::ConnectorPunctuation
| unicode_general_category::GeneralCategory::DashPunctuation
| unicode_general_category::GeneralCategory::ClosePunctuation
| unicode_general_category::GeneralCategory::FinalPunctuation
| unicode_general_category::GeneralCategory::InitialPunctuation
| unicode_general_category::GeneralCategory::OtherPunctuation
| unicode_general_category::GeneralCategory::OpenPunctuation
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_tokenizer() -> WordPieceTokenizer {
let vocab = vec![
"[PAD]".to_string(), "[UNK]".to_string(), "[CLS]".to_string(), "[SEP]".to_string(), "hello".to_string(), "world".to_string(), "##ing".to_string(), "##s".to_string(), "un".to_string(), "##know".to_string(), "##n".to_string(), ",".to_string(), "the".to_string(), "a".to_string(), ];
WordPieceTokenizer::new(vocab, 1, 200, true)
}
#[test]
fn test_encode_basic() {
let tok = make_tokenizer();
let ids = tok.encode("hello world");
assert_eq!(ids, vec![4, 5]);
}
#[test]
fn test_encode_subwords() {
let tok = make_tokenizer();
let ids = tok.encode("unknown");
assert_eq!(ids, vec![8, 9, 10]);
}
#[test]
fn test_encode_punctuation() {
let tok = make_tokenizer();
let ids = tok.encode("hello, world");
assert_eq!(ids, vec![4, 11, 5]);
}
#[test]
fn test_decode_basic() {
let tok = make_tokenizer();
let text = tok.decode(&[4, 5]).unwrap();
assert_eq!(text, "hello world");
}
#[test]
fn test_decode_subwords() {
let tok = make_tokenizer();
let text = tok.decode(&[8, 9, 10]).unwrap();
assert_eq!(text, "unknown");
}
#[test]
fn test_decode_skips_special() {
let tok = make_tokenizer();
let text = tok.decode(&[2, 4, 5, 3]).unwrap();
assert_eq!(text, "hello world");
}
#[test]
fn test_vocab_size() {
let tok = make_tokenizer();
assert_eq!(tok.vocab_size(), 14);
}
#[test]
fn test_special_token_ids() {
let tok = make_tokenizer();
assert_eq!(tok.cls_token_id(), Some(2));
assert_eq!(tok.sep_token_id(), Some(3));
assert_eq!(tok.pad_token_id(), Some(0));
assert_eq!(tok.unk_token_id(), 1);
}
#[test]
fn clean_text_strips_control_and_format_chars() {
assert_eq!(
clean_text("a\u{200b}b\u{200c}\u{feff}c\0\u{fffd}d\te"),
"abcd e"
);
assert_eq!(clean_text("plain text"), "plain text");
}
#[test]
fn test_unknown_word() {
let tok = make_tokenizer();
assert_eq!(tok.encode("xyz"), vec![1]);
}
#[test]
fn test_handle_chinese_chars() {
let tok = make_tokenizer();
assert_eq!(tok.encode("hello世界world"), vec![4, 1, 1, 5]);
}
#[test]
fn test_lowercase() {
let tok = make_tokenizer();
let ids = tok.encode("Hello WORLD");
assert_eq!(ids, vec![4, 5]);
}
#[test]
fn test_case_sensitive() {
let vocab = vec![
"[UNK]".to_string(), "Hello".to_string(), "hello".to_string(), ];
let tok = WordPieceTokenizer::new(vocab, 0, 200, false);
let ids = tok.encode("Hello");
assert_eq!(ids, vec![1]);
let ids = tok.encode("hello");
assert_eq!(ids, vec![2]);
}
#[test]
fn test_decode_invalid_id() {
let tok = make_tokenizer();
let result = tok.decode(&[999]);
assert!(result.is_err());
}
}