use textprep::SubwordTokenizer;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("Dimension mismatch: expected {expected}, got {got}")]
DimensionMismatch { expected: usize, got: usize },
#[error("Token not found in codebook: {0}")]
TokenNotFound(u32),
#[error("Weight length mismatch: expected {expected}, got {got}")]
WeightLenMismatch { expected: usize, got: usize },
#[error("dimension cannot be zero")]
ZeroDimension,
#[error("matrix length {len} is not a multiple of dimension {dim}")]
InvalidMatrixShape { len: usize, dim: usize },
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone)]
pub struct Codebook {
matrix: Vec<f32>,
dim: usize,
}
impl Codebook {
pub fn new(matrix: Vec<f32>, dim: usize) -> Result<Self> {
if dim == 0 {
return Err(Error::ZeroDimension);
}
if !matrix.len().is_multiple_of(dim) {
return Err(Error::InvalidMatrixShape {
len: matrix.len(),
dim,
});
}
Ok(Self { matrix, dim })
}
pub fn get(&self, id: u32) -> Option<&[f32]> {
let start = (id as usize) * self.dim;
let end = start + self.dim;
if end <= self.matrix.len() {
Some(&self.matrix[start..end])
} else {
None
}
}
pub fn matrix(&self) -> &[f32] {
&self.matrix
}
pub fn dim(&self) -> usize {
self.dim
}
pub fn vocab_size(&self) -> usize {
self.matrix.len() / self.dim
}
}
impl Codebook {
pub fn encode_ids(&self, ids: &[u32]) -> Vec<f32> {
if ids.is_empty() {
return vec![0.0; self.dim];
}
let embeddings: Vec<&[f32]> = ids.iter().filter_map(|&id| self.get(id)).collect();
if embeddings.is_empty() {
return vec![0.0; self.dim];
}
let mut out = vec![0.0; self.dim];
let count = embeddings.len() as f32;
for emb in &embeddings {
for (o, &e) in out.iter_mut().zip(emb.iter()) {
*o += e;
}
}
for o in out.iter_mut() {
*o /= count;
}
out
}
pub fn encode_ids_strict(&self, ids: &[u32]) -> Result<Vec<f32>> {
if ids.is_empty() {
return Ok(vec![0.0; self.dim]);
}
let mut embeddings: Vec<&[f32]> = Vec::with_capacity(ids.len());
for &id in ids {
let emb = self.get(id).ok_or(Error::TokenNotFound(id))?;
embeddings.push(emb);
}
let mut out = vec![0.0; self.dim];
let count = embeddings.len() as f32;
for emb in &embeddings {
for (o, &e) in out.iter_mut().zip(emb.iter()) {
*o += e;
}
}
for o in out.iter_mut() {
*o /= count;
}
Ok(out)
}
pub fn encode_ids_weighted_strict(&self, ids: &[u32], weights: &[f32]) -> Result<Vec<f32>> {
if ids.len() != weights.len() {
return Err(Error::WeightLenMismatch {
expected: ids.len(),
got: weights.len(),
});
}
if ids.is_empty() {
return Ok(vec![0.0; self.dim]);
}
let dim = self.dim;
let mut out = vec![0.0f32; dim];
let mut sum_w = 0.0f32;
for (&id, &w) in ids.iter().zip(weights.iter()) {
let emb = self.get(id).ok_or(Error::TokenNotFound(id))?;
if w == 0.0 {
continue;
}
sum_w += w;
for (o, &e) in out.iter_mut().zip(emb.iter()) {
*o += w * e;
}
}
if sum_w <= 0.0 {
return Ok(vec![0.0; dim]);
}
for o in out.iter_mut() {
*o /= sum_w;
}
Ok(out)
}
pub fn encode_sequence_ids(&self, ids: &[u32]) -> Vec<Vec<f32>> {
let mut result = Vec::with_capacity(ids.len());
for &id in ids {
if let Some(emb) = self.get(id) {
result.push(emb.to_vec());
}
}
result
}
}
#[inline]
pub fn sif_weight(p: f32, a: f32) -> f32 {
debug_assert!(p >= 0.0, "sif_weight: p must be non-negative, got {p}");
if a <= 0.0 {
return 0.0;
}
if p < 0.0 {
return 0.0;
}
a / (a + p)
}
pub fn l2_normalize_in_place(v: &mut [f32]) {
let mut ss = 0.0f32;
for &x in v.iter() {
ss += x * x;
}
if ss <= 0.0 {
return;
}
let inv = 1.0f32 / ss.sqrt();
for x in v.iter_mut() {
*x *= inv;
}
}
pub fn remove_component_in_place(v: &mut [f32], u_unit: &[f32]) -> Result<()> {
if v.len() != u_unit.len() {
return Err(Error::DimensionMismatch {
expected: v.len(),
got: u_unit.len(),
});
}
debug_assert!(
(u_unit.iter().map(|x| x * x).sum::<f32>().sqrt() - 1.0).abs() < 0.01,
"u_unit should be approximately unit norm"
);
let mut dot = 0.0f32;
for i in 0..v.len() {
dot += u_unit[i] * v[i];
}
for i in 0..v.len() {
v[i] -= u_unit[i] * dot;
}
Ok(())
}
pub struct Projection<T: SubwordTokenizer> {
tokenizer: T,
codebook: Codebook,
}
impl<T: SubwordTokenizer> Projection<T> {
pub fn new(tokenizer: T, codebook: Codebook) -> Self {
Self {
tokenizer,
codebook,
}
}
pub fn encode(&self, text: &str) -> Vec<f32> {
let tokens = self.tokenizer.tokenize(text);
self.codebook.encode_ids(&tokens)
}
pub fn encode_sequence(&self, text: &str) -> Vec<Vec<f32>> {
let tokens = self.tokenizer.tokenize(text);
self.codebook.encode_sequence_ids(&tokens)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use textprep::BpeTokenizer;
#[test]
fn test_projection_basic() {
let mut vocab = HashMap::new();
vocab.insert("apple".to_string(), 0);
vocab.insert("pie".to_string(), 1);
let tokenizer = BpeTokenizer::from_vocab(vocab);
let matrix = vec![
1.0, 0.0, 0.0, 0.0, 1.0, 0.0, ];
let codebook = Codebook::new(matrix, 3).unwrap();
let proj = Projection::new(tokenizer, codebook);
let vec = proj.encode("apple pie");
assert!((vec[0] - 0.5).abs() < 1e-6);
assert!((vec[1] - 0.5).abs() < 1e-6);
assert!((vec[2] - 0.0).abs() < 1e-6);
}
#[test]
fn test_codebook_rejects_zero_dim() {
let err = Codebook::new(vec![1.0, 2.0, 3.0], 0).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("dimension cannot be zero"), "got: {msg}");
}
#[test]
fn test_codebook_rejects_non_multiple() {
let err = Codebook::new(vec![1.0, 2.0, 3.0], 2).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("not a multiple of dimension"), "got: {msg}");
}
#[test]
fn codebook_strict_errors_on_missing_token() {
let codebook = Codebook::new(vec![1.0, 2.0], 2).unwrap(); let err = codebook.encode_ids_strict(&[0, 9]).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("Token not found"), "got: {msg}");
}
#[test]
fn weighted_mean_matches_unweighted_mean_when_all_weights_equal() {
let matrix = vec![
1.0, 0.0, 0.0, 1.0, ];
let codebook = Codebook::new(matrix, 2).unwrap();
let ids = [0u32, 1u32];
let w = [1.0f32, 1.0f32];
let v = codebook.encode_ids_weighted_strict(&ids, &w).unwrap();
assert!((v[0] - 0.5).abs() < 1e-6);
assert!((v[1] - 0.5).abs() < 1e-6);
}
#[test]
fn l2_normalize_has_unit_norm_when_nonzero() {
let mut v = vec![3.0f32, 4.0];
l2_normalize_in_place(&mut v);
let norm = (v[0] * v[0] + v[1] * v[1]).sqrt();
assert!((norm - 1.0).abs() < 1e-6, "norm={norm}");
}
#[test]
fn single_token_equals_embedding() {
let codebook = Codebook::new(vec![1.0, 2.0, 3.0], 3).unwrap();
let v = codebook.encode_ids(&[0]);
assert_eq!(&v[..], &[1.0, 2.0, 3.0]);
}
#[test]
fn encode_ids_all_missing_returns_zero_vector() {
let codebook = Codebook::new(vec![1.0, 2.0, 3.0], 3).unwrap();
let v = codebook.encode_ids(&[999]);
assert_eq!(&v[..], &[0.0, 0.0, 0.0]);
}
#[test]
fn weighted_zero_weights_returns_zero_vector() {
let codebook = Codebook::new(vec![1.0, 2.0, 3.0], 3).unwrap();
let v = codebook.encode_ids_weighted_strict(&[0], &[0.0]).unwrap();
assert_eq!(&v[..], &[0.0, 0.0, 0.0]);
}
#[test]
fn l2_normalize_noop_on_zero_vector() {
let mut v = vec![0.0f32, 0.0, 0.0];
l2_normalize_in_place(&mut v);
assert_eq!(&v[..], &[0.0, 0.0, 0.0]);
}
#[test]
fn remove_component_dimension_mismatch_errors() {
let mut v = vec![1.0f32, 2.0];
let u = vec![0.0f32, 1.0, 0.0];
assert!(remove_component_in_place(&mut v, &u).is_err());
}
#[test]
fn multilingual_vocab_smoke() {
let mut vocab = HashMap::new();
vocab.insert("東京".to_string(), 0);
vocab.insert("Москва".to_string(), 1);
vocab.insert("التقى".to_string(), 2);
vocab.insert("राम".to_string(), 3);
vocab.insert("François".to_string(), 4);
let tokenizer = BpeTokenizer::from_vocab(vocab);
let matrix = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let codebook = Codebook::new(matrix, 1).unwrap();
let proj = Projection::new(tokenizer, codebook);
let v = proj.encode("東京 Москва التقى राम François");
assert!((v[0] - 3.0).abs() < 1e-6, "got={:?}", v);
}
}