use std::collections::HashMap;
use std::fmt;
use super::error::TextError;
use super::limits::TextLimits;
pub(crate) const PADDING_TOKEN: &str = "";
pub(crate) const OOV_TOKEN: &str = "[UNK]";
pub(crate) const PADDING_ID: i32 = 0;
pub(crate) const OOV_ID: i32 = 1;
pub(crate) const RESERVED_COUNT: usize = 2;
pub(crate) fn check_max_tokens(max_tokens: Option<usize>) -> Result<(), TextError> {
match max_tokens {
Some(m) if m <= RESERVED_COUNT => Err(TextError::InvalidMaxTokens { max_tokens: m }),
_ => Ok(()),
}
}
pub(crate) fn check_vocabulary_within_max_tokens(
max_tokens: Option<usize>,
vocabulary_len: usize,
) -> Result<(), TextError> {
if let Some(m) = max_tokens {
let total = vocabulary_len.saturating_add(RESERVED_COUNT);
if total > m {
return Err(TextError::VocabularyTooLarge { len: total, max: m });
}
}
Ok(())
}
#[derive(Clone)]
pub(crate) struct Vocabulary {
tokens: Vec<String>,
ids: HashMap<String, i32>,
}
impl Vocabulary {
pub(crate) fn from_tokens<S: AsRef<str>>(
limits: &TextLimits,
tokens: &[S],
) -> Result<Self, TextError> {
let total = tokens.len().saturating_add(RESERVED_COUNT);
limits.check_vocabulary_size(total)?;
let mut seen: HashMap<&str, usize> = HashMap::new();
for (i, t) in tokens.iter().enumerate() {
let t = t.as_ref();
limits.check_vocabulary_token_len(i, t.len())?;
if t.is_empty() {
return Err(TextError::EmptyVocabularyToken { index: i });
}
if t == OOV_TOKEN {
return Err(TextError::ReservedVocabularyToken { index: i });
}
if let Some(&first) = seen.get(t) {
return Err(TextError::DuplicateVocabularyToken { first, second: i });
}
seen.insert(t, i);
}
let mut out_tokens: Vec<String> = Vec::with_capacity(total);
let mut ids: HashMap<String, i32> = HashMap::with_capacity(total);
out_tokens.push(PADDING_TOKEN.to_owned());
out_tokens.push(OOV_TOKEN.to_owned());
ids.insert(PADDING_TOKEN.to_owned(), PADDING_ID);
ids.insert(OOV_TOKEN.to_owned(), OOV_ID);
for (i, t) in tokens.iter().enumerate() {
let id = i
.checked_add(RESERVED_COUNT)
.and_then(|v| i32::try_from(v).ok())
.ok_or(TextError::VocabularyTooLarge {
len: total,
max: i32::MAX as usize,
})?;
let t = t.as_ref();
out_tokens.push(t.to_owned());
ids.insert(t.to_owned(), id);
}
Ok(Self {
tokens: out_tokens,
ids,
})
}
pub(crate) fn lookup(&self, token: &str) -> i32 {
self.ids.get(token).copied().unwrap_or(OOV_ID)
}
pub(crate) fn tokens(&self) -> &[String] {
&self.tokens
}
pub(crate) fn vocabulary_size(&self) -> usize {
self.tokens.len()
}
}
impl fmt::Debug for Vocabulary {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Vocabulary")
.field("len", &self.tokens.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn small_limits() -> TextLimits {
TextLimits::default()
.with_max_vocabulary_size(4)
.and_then(|l| l.with_max_vocabulary_token_bytes(6))
.unwrap_or_default()
}
fn build(tokens: &[&str]) -> Result<Vocabulary, TextError> {
Vocabulary::from_tokens(&TextLimits::default(), tokens)
}
#[test]
fn known_unknown_and_reserved_lookup() {
let Ok(v) = build(&["cat", "dog"]) else {
panic!("構築に失敗");
};
assert_eq!(v.lookup("cat"), 2);
assert_eq!(v.lookup("dog"), 3);
assert_eq!(v.lookup("bird"), OOV_ID);
assert_eq!(v.lookup(PADDING_TOKEN), PADDING_ID);
assert_eq!(v.lookup(OOV_TOKEN), OOV_ID);
assert_eq!(v.tokens()[0], PADDING_TOKEN);
assert_eq!(v.tokens()[1], OOV_TOKEN);
}
#[test]
fn round_trip_and_size() {
let Ok(v) = build(&["cat", "dog", "emu"]) else {
panic!("構築に失敗");
};
assert_eq!(v.vocabulary_size(), 3 + RESERVED_COUNT);
assert_eq!(v.vocabulary_size(), v.tokens().len());
for t in v.tokens() {
let Ok(id) = usize::try_from(v.lookup(t)) else {
panic!("id が負");
};
assert_eq!(&v.tokens()[id], t);
}
}
#[test]
fn empty_vocabulary_maps_everything_to_oov() {
let Ok(v) = build(&[]) else {
panic!("構築に失敗");
};
assert_eq!(v.vocabulary_size(), 2);
assert_eq!(v.lookup("x"), OOV_ID);
}
#[test]
fn case_sensitive_and_multibyte() {
let Ok(v) = build(&["a", "日本"]) else {
panic!("構築に失敗");
};
assert_eq!(v.lookup("A"), OOV_ID);
assert_eq!(v.lookup("日本"), 3);
}
#[test]
fn empty_token_is_empty_error() {
assert!(matches!(
build(&["a", ""]),
Err(TextError::EmptyVocabularyToken { index: 1 })
));
}
#[test]
fn oov_token_is_reserved_error_but_lowercase_is_accepted() {
assert!(matches!(
build(&["a", "b", "[UNK]"]),
Err(TextError::ReservedVocabularyToken { index: 2 })
));
assert!(build(&["[unk]"]).is_ok());
}
#[test]
fn duplicate_reports_first_two_occurrences() {
assert!(matches!(
build(&["x", "y", "x", "x"]),
Err(TextError::DuplicateVocabularyToken {
first: 0,
second: 2
})
));
}
#[test]
fn vocabulary_size_boundary() {
let l = small_limits();
assert!(Vocabulary::from_tokens(&l, &["a", "b"]).is_ok());
assert!(matches!(
Vocabulary::from_tokens(&l, &["a", "b", "c"]),
Err(TextError::VocabularyTooLarge { len: 5, max: 4 })
));
}
#[test]
fn token_bytes_boundary() {
let l = small_limits();
assert!(Vocabulary::from_tokens(&l, &["日本"]).is_ok());
assert!(matches!(
Vocabulary::from_tokens(&l, &["a", "日本a"]),
Err(TextError::VocabularyTokenTooLong {
index: 1,
len: 7,
max: 6
})
));
}
#[test]
fn invalid_first_token_fails_before_scanning_rest() {
let mut toks: Vec<String> = vec![String::new()];
toks.extend((0..50_000).map(|i| format!("t{i}")));
assert!(matches!(
Vocabulary::from_tokens(&TextLimits::default(), &toks),
Err(TextError::EmptyVocabularyToken { index: 0 })
));
}
#[test]
fn check_order() {
let l = small_limits();
assert!(matches!(
Vocabulary::from_tokens(&l, &["日本a", "b", "c"]),
Err(TextError::VocabularyTooLarge { .. })
));
let Ok(l4) = l.with_max_vocabulary_token_bytes(4) else {
panic!("上限の設定に失敗");
};
assert!(matches!(
Vocabulary::from_tokens(&l4, &["[UNK]"]),
Err(TextError::VocabularyTokenTooLong { index: 0, .. })
));
assert!(matches!(
build(&["a", "a", "[UNK]"]),
Err(TextError::DuplicateVocabularyToken {
first: 0,
second: 1
})
));
}
#[test]
fn debug_and_errors_do_not_leak_tokens() {
let Ok(v) = build(&["SECRET_MARKER_TOKEN"]) else {
panic!("構築に失敗");
};
let dbg = format!("{v:?}");
assert!(!dbg.contains("SECRET_MARKER_TOKEN"));
assert!(dbg.contains('3'));
let l = small_limits();
let errs = [
Vocabulary::from_tokens(&l, &["SECRET_LONG_TOKEN"]).err(),
build(&["SECRET_A", "SECRET_A"]).err(),
build(&["SECRET_A", ""]).err(),
build(&["SECRET_A", "[UNK]"]).err(),
];
for e in errs {
let Some(e) = e else {
panic!("Err になるはず");
};
assert!(!format!("{e:?}").contains("SECRET"));
assert!(!format!("{e}").contains("SECRET"));
}
}
#[test]
fn clone_keeps_lookup() {
let Ok(v) = build(&["cat"]) else {
panic!("構築に失敗");
};
let c = v.clone();
assert_eq!(c.lookup("cat"), v.lookup("cat"));
assert_eq!(c.lookup("zzz"), OOV_ID);
}
}