use std::collections::VecDeque;
use super::error::TextError;
use super::limits::{TextLimits, check_exact_count};
use super::ngram::ngram_count;
use super::split::{Split, split};
use super::standardize::{Standardize, standardize};
use super::vocab::{PADDING_ID, Vocabulary};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct BagOptions {
pub(crate) standardize: Standardize,
pub(crate) split: Split,
pub(crate) ngrams: Option<usize>,
pub(crate) width: usize,
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum BagKind<'a> {
Presence,
Count,
Weighted(&'a [f32]),
}
fn row_token_count(
input: &str,
opts: &BagOptions,
limits: &TextLimits,
index: usize,
) -> Result<usize, TextError> {
let std = standardize(input, opts.standardize, limits, index)?;
let tokens = split(&std, opts.split).count();
match opts.ngrams {
Some(n) => ngram_count(tokens, n, limits),
None => Ok(tokens),
}
}
fn bump(row: &mut [f32], vocab: &Vocabulary, token: &str) {
let id = vocab.lookup(token);
if id == PADDING_ID {
return;
}
if let Some(slot) = usize::try_from(id).ok().and_then(|i| row.get_mut(i)) {
*slot += 1.0;
}
}
fn count_row(
input: &str,
vocab: &Vocabulary,
opts: &BagOptions,
limits: &TextLimits,
index: usize,
row: &mut [f32],
) -> Result<(), TextError> {
let std = standardize(input, opts.standardize, limits, index)?;
match opts.ngrams {
None => {
for t in split(&std, opts.split) {
bump(row, vocab, t);
}
}
Some(n) => {
let mut window: VecDeque<&str> = VecDeque::with_capacity(n);
let mut buf = String::new();
for t in split(&std, opts.split) {
if window.len() == n {
window.pop_front();
}
window.push_back(t);
for k in 1..=window.len() {
buf.clear();
for (i, w) in window.range(window.len() - k..).enumerate() {
if i > 0 {
buf.push(' ');
}
buf.push_str(w);
}
bump(row, vocab, &buf);
}
}
}
}
Ok(())
}
pub(crate) fn bag<S: AsRef<str>>(
vocab: &Vocabulary,
opts: &BagOptions,
limits: &TextLimits,
inputs: &[S],
kind: BagKind<'_>,
) -> Result<crate::Tensor<f32>, TextError> {
if let Some(n) = opts.ngrams {
limits.check_ngrams(n)?;
}
limits.check_inputs(inputs)?;
for (index, s) in inputs.iter().enumerate() {
check_exact_count(index, row_token_count(s.as_ref(), opts, limits, index)?)?;
}
let batch = inputs.len();
let width = opts.width;
let elements = limits.check_output_elements(batch, width)?;
let mut data = vec![0.0f32; elements];
if width > 0 {
for (index, (row, s)) in data.chunks_exact_mut(width).zip(inputs.iter()).enumerate() {
count_row(s.as_ref(), vocab, opts, limits, index, row)?;
match kind {
BagKind::Presence => {
for v in row.iter_mut().filter(|v| **v > 0.0) {
*v = 1.0;
}
}
BagKind::Count => {}
BagKind::Weighted(weights) => {
for (v, w) in row.iter_mut().zip(weights.iter()) {
if *v != 0.0 {
*v *= *w;
}
}
}
}
}
}
crate::Tensor::new(data, &[batch, width]).map_err(TextError::Shape)
}
#[cfg(test)]
mod tests {
use super::*;
fn vocab(words: &[&str]) -> Vocabulary {
let Ok(v) = Vocabulary::from_tokens(&TextLimits::default(), words) else {
panic!("vocab must build");
};
v
}
fn opts(v: &Vocabulary) -> BagOptions {
BagOptions {
standardize: Standardize::None,
split: Split::Whitespace,
ngrams: None,
width: v.vocabulary_size(),
}
}
fn run(
v: &Vocabulary,
o: &BagOptions,
inputs: &[&str],
kind: BagKind<'_>,
) -> Result<(Vec<usize>, Vec<f32>), TextError> {
bag(v, o, &TextLimits::default(), inputs, kind)
.map(|t| (t.shape().to_vec(), t.host_slice().to_vec()))
}
#[test]
fn count_and_presence_use_id_as_column() {
let v = vocab(&["cat", "dog"]); let o = opts(&v);
let (s, d) = run(&v, &o, &["cat cat dog bird", "bird"], BagKind::Count).unwrap();
assert_eq!(s, vec![2, 4]);
assert_eq!(d, vec![0.0, 1.0, 2.0, 1.0, 0.0, 1.0, 0.0, 0.0]);
let (_, d) = run(&v, &o, &["cat cat dog bird", "bird"], BagKind::Presence).unwrap();
assert_eq!(d, vec![0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0]);
}
#[test]
fn id_zero_tokens_are_dropped_from_column_zero() {
let v = vocab(&["cat"]);
let mut o = opts(&v);
o.split = Split::None;
let (_, d) = run(&v, &o, &["", "cat"], BagKind::Count).unwrap();
assert_eq!(d, vec![0.0, 0.0, 0.0, 0.0, 0.0, 1.0]);
let (_, d) = run(&v, &opts(&v), &[" "], BagKind::Count).unwrap();
assert_eq!(d, vec![0.0, 0.0, 0.0]);
}
#[test]
fn ngrams_are_counted_with_space_join() {
let v = vocab(&["a", "a b"]); let mut o = opts(&v);
o.ngrams = Some(2);
let (_, d) = run(&v, &o, &["a b a b"], BagKind::Count).unwrap();
assert_eq!(d, vec![0.0, 3.0, 2.0, 2.0]);
}
#[test]
fn ngrams_window_handles_short_rows_and_n_above_length() {
let v = vocab(&["a", "a b", "a b c", "b c"]); let mut o = opts(&v);
o.ngrams = Some(3);
let (_, d) = run(&v, &o, &["a b c", "a", ""], BagKind::Count).unwrap();
assert_eq!(&d[0..6], &[0.0, 2.0, 1.0, 1.0, 1.0, 1.0]);
assert_eq!(&d[6..12], &[0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
assert_eq!(&d[12..18], &[0.0; 6]);
}
#[test]
fn weighted_multiplies_count_by_weight_for_nonzero_columns_only() {
let v = vocab(&["x", "y"]);
let o = opts(&v);
let w = [0.0f32, 0.5, 2.0, 4.0];
let (_, d) = run(&v, &o, &["x x y z"], BagKind::Weighted(&w)).unwrap();
assert_eq!(d, vec![0.0, 0.5, 4.0, 4.0]);
}
#[test]
fn empty_batch_keeps_width_and_pad_columns_stay_zero() {
let v = vocab(&["x"]);
let mut o = opts(&v);
let (s, d) = run(&v, &o, &[], BagKind::Count).unwrap();
assert_eq!((s, d.len()), (vec![0, 3], 0));
o.width = 6;
let (s, d) = run(&v, &o, &["x q"], BagKind::Count).unwrap();
assert_eq!(s, vec![1, 6]);
assert_eq!(d, vec![0.0, 1.0, 1.0, 0.0, 0.0, 0.0]);
}
#[test]
fn output_size_is_rejected_before_allocation() {
let v = vocab(&["x"]);
let mut o = opts(&v);
o.width = 1 << 30;
let limits = TextLimits::default();
let inputs = ["x"; 4];
assert!(matches!(
bag(&v, &o, &limits, &inputs, BagKind::Count),
Err(TextError::OutputTooLarge { .. })
));
let small = TextLimits::default().with_max_output_elements(6).unwrap();
o.width = 3;
assert!(bag(&v, &o, &small, &["x", "x"], BagKind::Count).is_ok());
assert!(matches!(
bag(&v, &o, &small, &["x", "x", "x"], BagKind::Count),
Err(TextError::OutputTooLarge { .. })
));
}
}