use std::fmt;
use super::adapt::adapt;
use super::error::TextError;
use super::limits::TextLimits;
use super::split::Split;
use super::standardize::Standardize;
use super::vocab::{Vocabulary, check_max_tokens, check_vocabulary_within_max_tokens};
use crate::Tensor;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct StringLookupConfig {
pub max_tokens: Option<usize>,
pub limits: TextLimits,
}
impl StringLookupConfig {
pub fn with_max_tokens(mut self, max_tokens: usize) -> Self {
self.max_tokens = Some(max_tokens);
self
}
pub fn with_limits(mut self, limits: TextLimits) -> Self {
self.limits = limits;
self
}
}
#[derive(Clone)]
pub struct StringLookup {
config: StringLookupConfig,
vocabulary: Vocabulary,
}
impl StringLookup {
pub fn from_vocabulary<S: AsRef<str>>(
config: StringLookupConfig,
vocabulary: &[S],
) -> Result<Self, TextError> {
check_max_tokens(config.max_tokens)?;
check_vocabulary_within_max_tokens(config.max_tokens, vocabulary.len())?;
let vocabulary = Vocabulary::from_tokens(&config.limits, vocabulary)?;
Ok(Self { config, vocabulary })
}
pub fn adapt<S: AsRef<str>>(
config: StringLookupConfig,
tokens: &[S],
) -> Result<Self, TextError> {
check_max_tokens(config.max_tokens)?;
let vocabulary = adapt(
tokens,
Standardize::None,
Split::None,
None,
config.max_tokens,
&config.limits,
)?;
Ok(Self { config, vocabulary })
}
pub fn lookup<S: AsRef<str>>(&self, tokens: &[S]) -> Result<Tensor<i32>, TextError> {
let limits = &self.config.limits;
limits.check_inputs(tokens)?;
let n = limits.check_output_elements(tokens.len(), 1)?;
let mut ids: Vec<i32> = Vec::with_capacity(n);
for t in tokens {
ids.push(self.vocabulary.lookup(t.as_ref()));
}
Tensor::new(ids, &[n]).map_err(TextError::Shape)
}
pub fn token(&self, id: i32) -> Result<&str, TextError> {
usize::try_from(id)
.ok()
.and_then(|i| self.vocabulary.tokens().get(i))
.map(String::as_str)
.ok_or(TextError::IdOutOfRange { index: 0 })
}
pub fn invert(&self, ids: &Tensor<i32>) -> Result<Vec<String>, TextError> {
let limits = &self.config.limits;
let numel = limits.check_output_elements(ids.numel(), 1)?;
match numel.checked_mul(std::mem::size_of::<String>()) {
Some(bytes) if bytes <= isize::MAX as usize => {}
_ => {
return Err(TextError::OutputTooLarge {
elements: numel,
max: isize::MAX as usize / std::mem::size_of::<String>(),
});
}
}
let host = ids.host_slice();
let tokens = self.vocabulary.tokens();
let lookup = |id: i32| usize::try_from(id).ok().and_then(|i| tokens.get(i));
if let Some(index) = host.iter().position(|&id| lookup(id).is_none()) {
return Err(TextError::IdOutOfRange { index });
}
limits
.check_corpus_total_bytes(host.iter().map(|&id| lookup(id).map_or(0, |t| t.len())))?;
Ok(host.iter().filter_map(|&id| lookup(id).cloned()).collect())
}
pub fn vocabulary(&self) -> &[String] {
self.vocabulary.tokens()
}
pub fn vocabulary_size(&self) -> usize {
self.vocabulary.vocabulary_size()
}
pub fn config(&self) -> &StringLookupConfig {
&self.config
}
}
impl fmt::Debug for StringLookup {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("StringLookup")
.field("config", &self.config)
.field("vocabulary_size", &self.vocabulary.vocabulary_size())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn with_limits(limits: TextLimits, vocab: &[&str]) -> StringLookup {
let Ok(sl) =
StringLookup::from_vocabulary(StringLookupConfig::default().with_limits(limits), vocab)
else {
panic!("構築に失敗");
};
sl
}
fn build(tokens: &[&str]) -> StringLookup {
with_limits(TextLimits::default(), tokens)
}
fn ids(v: Vec<i32>) -> Tensor<i32> {
let n = v.len();
let Ok(t) = Tensor::new(v, &[n]) else {
panic!("テンソル作成に失敗");
};
t
}
#[test]
fn ids_follow_input_order_and_reserved() {
let sl = build(&["b", "a", "日本"]);
assert_eq!(sl.vocabulary(), ["", "[UNK]", "b", "a", "日本"]);
assert_eq!(sl.vocabulary_size(), 5);
let Ok(t) = sl.lookup(&["a", "b", "日本", "A", "", "[UNK]"]) else {
panic!("lookup 失敗");
};
assert_eq!(t.shape(), &[6]);
assert_eq!(t.host_slice().to_vec(), [3, 2, 4, 1, 0, 1]);
}
#[test]
fn invalid_vocabulary_is_rejected() {
let c = StringLookupConfig::default;
assert!(matches!(
StringLookup::from_vocabulary(c(), &["a", "a"]),
Err(TextError::DuplicateVocabularyToken {
first: 0,
second: 1
})
));
assert!(matches!(
StringLookup::from_vocabulary(c(), &["[UNK]"]),
Err(TextError::ReservedVocabularyToken { index: 0 })
));
assert!(matches!(
StringLookup::from_vocabulary(c(), &["a", ""]),
Err(TextError::EmptyVocabularyToken { index: 1 })
));
}
#[test]
fn max_tokens_is_fail_closed() {
let cfg = |m| StringLookupConfig::default().with_max_tokens(m);
assert!(matches!(
StringLookup::from_vocabulary(cfg(2), &["a"]),
Err(TextError::InvalidMaxTokens { max_tokens: 2 })
));
assert!(matches!(
StringLookup::from_vocabulary(cfg(3), &["a", "b"]),
Err(TextError::VocabularyTooLarge { len: 4, max: 3 })
));
assert!(StringLookup::from_vocabulary(cfg(4), &["a", "b"]).is_ok());
assert!(matches!(
StringLookup::adapt(cfg(1), &["a"]),
Err(TextError::InvalidMaxTokens { max_tokens: 1 })
));
}
#[test]
fn lookup_empty_batch_and_limits() {
let sl = build(&["a"]);
let Ok(t) = sl.lookup::<&str>(&[]) else {
panic!("空バッチ失敗");
};
assert_eq!(t.shape(), &[0]);
let Ok(l) = TextLimits::default().with_max_batch(1) else {
panic!("limits");
};
assert!(matches!(
with_limits(l, &["a"]).lookup(&["a", "a"]),
Err(TextError::BatchTooLarge { len: 2, max: 1 })
));
let Ok(l) = TextLimits::default().with_max_input_bytes(2) else {
panic!("limits");
};
assert!(matches!(
with_limits(l, &["a"]).lookup(&["a", "abc"]),
Err(TextError::InputTooLong { index: 1, .. })
));
let Ok(l) = TextLimits::default().with_max_output_elements(1) else {
panic!("limits");
};
assert!(matches!(
with_limits(l, &["a"]).lookup(&["a", "a"]),
Err(TextError::OutputTooLarge {
elements: 2,
max: 1
})
));
}
#[test]
fn token_bounds() {
let sl = build(&["a", "b"]);
assert_eq!(sl.token(0).ok(), Some(""));
assert_eq!(sl.token(1).ok(), Some("[UNK]"));
assert_eq!(sl.token(3).ok(), Some("b"));
for bad in [-1, 4, i32::MAX, i32::MIN] {
assert!(
matches!(sl.token(bad), Err(TextError::IdOutOfRange { index: 0 })),
"{bad}"
);
}
}
#[test]
fn invert_round_trip_flatten_and_empty() {
let sl = build(&["a", "b"]);
let Ok(t) = Tensor::new(vec![2, 3, 1, 0], &[2, 2]) else {
panic!("テンソル作成に失敗");
};
let expected: Vec<String> = ["a", "b", "[UNK]", ""].map(String::from).to_vec();
assert_eq!(sl.invert(&t).ok(), Some(expected));
let Ok(e) = Tensor::<i32>::new(vec![], &[0]) else {
panic!("空テンソル");
};
assert_eq!(sl.invert(&e).ok(), Some(Vec::<String>::new()));
}
#[test]
fn invert_reports_first_out_of_range_position() {
let sl = build(&["a"]);
assert!(matches!(
sl.invert(&ids(vec![2, 0, 9, -1])),
Err(TextError::IdOutOfRange { index: 2 })
));
}
#[test]
fn invert_limits() {
let Ok(l) = TextLimits::default().with_max_output_elements(2) else {
panic!("limits");
};
assert!(matches!(
with_limits(l, &["a"]).invert(&ids(vec![99, 99, 99])),
Err(TextError::OutputTooLarge {
elements: 3,
max: 2
})
));
let Ok(l) = TextLimits::default().with_max_corpus_bytes(3) else {
panic!("limits");
};
let sl = with_limits(l, &["ab"]);
assert!(sl.invert(&ids(vec![2])).is_ok());
assert!(matches!(
sl.invert(&ids(vec![2, 2])),
Err(TextError::CorpusTooLarge {
total_bytes: 4,
max: 3
})
));
}
#[test]
fn adapt_orders_by_frequency_and_truncates() {
let Ok(sl) = StringLookup::adapt(
StringLookupConfig::default(),
&["b", "a b", "a b", "", "[UNK]", "c", "c"],
) else {
panic!("adapt 失敗");
};
assert_eq!(sl.vocabulary(), ["", "[UNK]", "a b", "c", "b"]);
let Ok(sl) = StringLookup::adapt(
StringLookupConfig::default().with_max_tokens(4),
&["b", "a b", "a b", "c", "c"],
) else {
panic!("adapt 失敗");
};
assert_eq!(sl.vocabulary(), ["", "[UNK]", "a b", "c"]);
}
#[test]
fn debug_and_errors_hide_input_text() {
let marker = "SECRET_MARKER";
let sl = build(&[marker]);
assert!(!format!("{sl:?}").contains(marker));
let errs = [
StringLookup::from_vocabulary(StringLookupConfig::default(), &[marker, marker]).err(),
StringLookup::from_vocabulary(StringLookupConfig::default(), &["[UNK]", marker]).err(),
StringLookup::from_vocabulary(
StringLookupConfig::default().with_max_tokens(3),
&[marker, "x"],
)
.err(),
];
for e in errs {
let Some(e) = e else { panic!("Err のはず") };
assert!(!e.to_string().contains(marker));
assert!(!format!("{e:?}").contains(marker));
}
}
}