use super::adapt::adapt;
use super::error::TextError;
use super::limits::TextLimits;
use super::split::Split;
use super::standardize::Standardize;
use super::transform::{TransformOptions, transform};
use super::vocab::Vocabulary;
const SECRET: &str = "SECRET_MARKER_TOKEN";
fn limits() -> TextLimits {
TextLimits::default()
}
fn tokens_of(v: &Vocabulary) -> Vec<&str> {
v.tokens().iter().map(String::as_str).collect()
}
fn build(
corpus: &[&str],
st: Standardize,
sp: Split,
ngrams: Option<usize>,
max_tokens: Option<usize>,
) -> Vocabulary {
match adapt(corpus, st, sp, ngrams, max_tokens, &limits()) {
Ok(v) => v,
Err(e) => panic!("adapt failed: {e}"),
}
}
fn run(v: &Vocabulary, opts: &TransformOptions, inputs: &[&str]) -> (Vec<usize>, Vec<i32>) {
match transform(v, opts, &limits(), inputs) {
Ok(t) => (t.shape().to_vec(), t.host_slice().to_vec()),
Err(e) => panic!("transform failed: {e}"),
}
}
fn corpus3() -> [&'static str; 3] {
["The cat sat.", "the dog sat", "A cat!"]
}
#[test]
fn direct_vocabulary_standardizes_before_lookup() {
let Ok(v) = Vocabulary::from_tokens(&limits(), &["cat", "dog", "the"]) else {
panic!("vocab must build");
};
let (s, d) = run(
&v,
&TransformOptions::default(),
&["The CAT!", "dog, bird?"],
);
assert_eq!(s, vec![2, 2]);
assert_eq!(d, vec![4, 2, 3, 1]);
}
#[test]
fn adapt_order_decides_ids() {
let v = build(
&corpus3(),
Standardize::default(),
Split::default(),
None,
None,
);
assert_eq!(
tokens_of(&v),
vec!["", "[UNK]", "cat", "sat", "the", "a", "dog"]
);
let inputs = ["THE dog!", "a bird sat on the cat"];
let (s, d) = run(&v, &TransformOptions::default(), &inputs);
assert_eq!(s, vec![2, 6]);
assert_eq!(d, vec![4, 6, 0, 0, 0, 0, 5, 1, 3, 1, 4, 2]);
let o = TransformOptions {
output_sequence_length: Some(3),
..Default::default()
};
let (s, d) = run(&v, &o, &inputs);
assert_eq!(s, vec![2, 3]);
assert_eq!(d, vec![4, 6, 0, 5, 1, 3]);
}
#[test]
fn max_tokens_truncation_turns_rare_words_into_oov() {
let v = build(
&corpus3(),
Standardize::default(),
Split::default(),
None,
Some(5),
);
assert_eq!(tokens_of(&v), vec!["", "[UNK]", "cat", "sat", "the"]);
let (s, d) = run(
&v,
&TransformOptions::default(),
&["THE dog!", "a bird sat on the cat"],
);
assert_eq!(s, vec![2, 6]);
assert_eq!(d, vec![4, 1, 0, 0, 0, 0, 1, 1, 3, 1, 4, 2]);
}
#[test]
fn bigram_pipeline_through_adapt_and_transform() {
let v = build(
&["a b", "a b c"],
Standardize::None,
Split::Whitespace,
Some(2),
None,
);
assert_eq!(
tokens_of(&v),
vec!["", "[UNK]", "a", "a b", "b", "b c", "c"]
);
let o = TransformOptions {
standardize: Standardize::None,
ngrams: Some(2),
..Default::default()
};
let (s, d) = run(&v, &o, &["a b c", "c a"]);
assert_eq!(s, vec![2, 5]);
assert_eq!(d, vec![2, 4, 6, 3, 5, 6, 2, 1, 0, 0]);
}
#[test]
fn split_modes_through_adapt_and_transform() {
let v = build(&["aab"], Standardize::None, Split::Character, None, None);
assert_eq!(tokens_of(&v), vec!["", "[UNK]", "a", "b"]);
let o = TransformOptions {
standardize: Standardize::None,
split: Split::Character,
..Default::default()
};
let (s, d) = run(&v, &o, &["abc"]);
assert_eq!(s, vec![1, 3]);
assert_eq!(d, vec![2, 3, 1]);
let v = build(
&["Hello World", "x"],
Standardize::None,
Split::None,
None,
None,
);
assert_eq!(tokens_of(&v), vec!["", "[UNK]", "Hello World", "x"]);
let o = TransformOptions {
standardize: Standardize::None,
split: Split::None,
..Default::default()
};
let (s, d) = run(&v, &o, &["Hello World", "hello world"]);
assert_eq!(s, vec![2, 1]);
assert_eq!(d, vec![2, 1]);
}
#[test]
fn corpus_order_does_not_change_output() {
let inputs = ["THE dog!", "a bird sat on the cat"];
let want = vec![4, 6, 0, 0, 0, 0, 5, 1, 3, 1, 4, 2];
for corpus in [
["The cat sat.", "the dog sat", "A cat!"],
["A cat!", "the dog sat", "The cat sat."],
["the dog sat", "A cat!", "The cat sat."],
] {
let v = build(
&corpus,
Standardize::default(),
Split::default(),
None,
None,
);
let (_, d) = run(&v, &TransformOptions::default(), &inputs);
assert_eq!(d, want);
}
}
#[test]
fn pipeline_failures_are_typed_and_do_not_leak_input() {
let Ok(tight) = limits().with_max_distinct_tokens(1) else {
panic!("limits");
};
let r = adapt(
&[SECRET, "other"],
Standardize::None,
Split::Whitespace,
None,
None,
&tight,
);
let Err(e) = r else {
panic!("adapt must fail on distinct-token limit");
};
assert!(matches!(e, TextError::TooManyDistinctTokens { .. }));
assert!(!format!("{e}").contains(SECRET));
assert!(!format!("{e:?}").contains(SECRET));
let Ok(small) = limits().with_max_corpus_bytes(4) else {
panic!("limits");
};
let r = adapt(
&[SECRET],
Standardize::None,
Split::Whitespace,
None,
None,
&small,
);
let Err(e) = r else {
panic!("adapt must fail on corpus bytes");
};
assert!(!format!("{e}").contains(SECRET));
assert!(!format!("{e:?}").contains(SECRET));
let Ok(v) = Vocabulary::from_tokens(&limits(), &[SECRET, "b"]) else {
panic!("vocab must build");
};
assert!(!format!("{v:?}").contains(SECRET));
let Ok(out_small) = limits().with_max_output_elements(3) else {
panic!("limits");
};
let r = transform(
&v,
&TransformOptions::default(),
&out_small,
&[SECRET, "b b"],
);
let Err(e) = r else {
panic!("transform must fail on output elements");
};
assert!(!format!("{e}").contains(SECRET));
assert!(!format!("{e:?}").contains(SECRET));
}
const DIM: usize = 3;
fn wired_ids(len: Option<usize>) -> (Vocabulary, crate::Tensor<i32>) {
let v = build(
&corpus3(),
Standardize::default(),
Split::default(),
None,
None,
);
let o = TransformOptions {
output_sequence_length: len,
..Default::default()
};
match transform(&v, &o, &limits(), &["THE dog!", "a bird sat on the cat"]) {
Ok(t) => (v, t),
Err(e) => panic!("transform failed: {e}"),
}
}
fn weight(rows: usize) -> crate::Tensor<f32> {
match crate::Tensor::<f32>::new((0..rows * DIM).map(|i| i as f32).collect(), &[rows, DIM]) {
Ok(t) => t,
Err(e) => panic!("weight: {e}"),
}
}
fn embed_grad(
ids: &crate::Tensor<i32>,
rows: usize,
padding_idx: Option<usize>,
) -> (Vec<usize>, Vec<f32>, Vec<f32>) {
let tape = crate::tape();
let w = tape.var(&weight(rows));
let Ok(out) = w.embedding(ids, padding_idx) else {
panic!("embedding must accept transform output");
};
let value = out.to_tensor();
let shape = value.shape().to_vec();
let fwd = value.host_slice().to_vec();
let Ok(loss) = out.sum(None) else {
panic!("sum");
};
let Ok(grads) = tape.backward(&loss) else {
panic!("backward");
};
let Ok(Some(dw)) = grads.get(&w) else {
panic!("weight grad must exist");
};
(shape, fwd, dw.host_slice().to_vec())
}
#[test]
fn transform_output_feeds_embedding_and_zeroes_padding_grad() {
let (v, ids) = wired_ids(None);
assert_eq!(ids.shape(), &[2, 6]);
let rows = v.vocabulary_size();
assert_eq!(rows, 7);
let (shape, fwd, dw) = embed_grad(&ids, rows, Some(0));
assert_eq!(shape, vec![2, 6, DIM]);
let want_ids = [4usize, 6, 0, 0, 0, 0, 5, 1, 3, 1, 4, 2];
let mut want_fwd = Vec::new();
for id in want_ids {
for c in 0..DIM {
want_fwd.push((id * DIM + c) as f32);
}
}
assert_eq!(fwd, want_fwd);
let counts = [0.0f32, 2.0, 1.0, 1.0, 2.0, 1.0, 1.0];
let want_dw: Vec<f32> = counts.iter().flat_map(|&c| [c; DIM]).collect();
assert_eq!(dw, want_dw);
}
#[test]
fn without_padding_idx_row_zero_receives_gradient() {
let (v, ids) = wired_ids(None);
let (_, _, dw) = embed_grad(&ids, v.vocabulary_size(), None);
assert_eq!(&dw[..DIM], &[4.0, 4.0, 4.0]);
}
#[test]
fn fixed_length_output_also_wires_into_embedding() {
let (v, ids) = wired_ids(Some(3));
assert_eq!(ids.shape(), &[2, 3]);
let (shape, _, dw) = embed_grad(&ids, v.vocabulary_size(), Some(0));
assert_eq!(shape, vec![2, 3, DIM]);
let counts = [0.0f32, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0];
let want_dw: Vec<f32> = counts.iter().flat_map(|&c| [c; DIM]).collect();
assert_eq!(dw, want_dw);
}