use crate::{Result, TextError};
use std::collections::{HashMap, HashSet};
use std::f64::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ModelType {
BertBase,
BertLarge,
RobertaBase,
RobertaLarge,
DistilBert,
Custom(String),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LayerAggregation {
LastLayer,
Average,
WeightedAverage,
Concatenate,
Maximum,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AlignmentAlgorithm {
Greedy,
Optimal,
Hierarchical,
AttentionBased,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum EmbeddingStrategy {
CharacterBased,
WordLevel,
Subword,
Contextual,
}
#[derive(Debug, Clone)]
pub struct BertScoreConfig {
pub model_type: ModelType,
pub layer_aggregation: LayerAggregation,
pub alignment_algorithm: AlignmentAlgorithm,
pub embedding_strategy: EmbeddingStrategy,
pub normalize_embeddings: bool,
pub use_fast_tokenizer: bool,
pub embedding_dim: usize,
pub layers_to_use: Vec<usize>,
pub max_sequence_length: usize,
pub batch_size: usize,
pub ignore_case: bool,
pub use_importance_weighting: bool,
}
impl Default for BertScoreConfig {
fn default() -> Self {
Self {
model_type: ModelType::BertBase,
layer_aggregation: LayerAggregation::Average,
alignment_algorithm: AlignmentAlgorithm::Greedy,
embedding_strategy: EmbeddingStrategy::WordLevel,
normalize_embeddings: true,
use_fast_tokenizer: true,
embedding_dim: 768, layers_to_use: vec![9, 10, 11], max_sequence_length: 512,
batch_size: 32,
ignore_case: false,
use_importance_weighting: false,
}
}
}
impl BertScoreConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_model(mut self, model_type: ModelType) -> Self {
self.model_type = model_type;
self.embedding_dim = match model_type {
ModelType::BertBase | ModelType::RobertaBase | ModelType::DistilBert => 768,
ModelType::BertLarge | ModelType::RobertaLarge => 1024,
ModelType::Custom(_) => self.embedding_dim, };
self
}
pub fn with_layer_aggregation(mut self, aggregation: LayerAggregation) -> Self {
self.layer_aggregation = aggregation;
self
}
pub fn with_alignment_algorithm(mut self, algorithm: AlignmentAlgorithm) -> Self {
self.alignment_algorithm = algorithm;
self
}
pub fn with_embedding_strategy(mut self, strategy: EmbeddingStrategy) -> Self {
self.embedding_strategy = strategy;
self
}
pub fn with_normalize_embeddings(mut self, normalize: bool) -> Self {
self.normalize_embeddings = normalize;
self
}
pub fn with_layers(mut self, layers: Vec<usize>) -> Self {
self.layers_to_use = layers;
self
}
pub fn with_max_sequence_length(mut self, max_length: usize) -> Self {
self.max_sequence_length = max_length;
self
}
pub fn with_batch_size(mut self, batch_size: usize) -> Self {
self.batch_size = batch_size.max(1);
self
}
pub fn with_importance_weighting(mut self, use_weighting: bool) -> Self {
self.use_importance_weighting = use_weighting;
self
}
}
#[derive(Debug, Clone)]
pub struct BertScoreResult {
pub precision: f64,
pub recall: f64,
pub f1_score: f64,
pub alignment_details: Option<AlignmentDetails>,
}
#[derive(Debug, Clone)]
pub struct AlignmentDetails {
pub reference_tokens: Vec<String>,
pub candidate_tokens: Vec<String>,
pub token_similarities: Vec<Vec<f64>>,
pub alignments: Vec<(usize, usize, f64)>,
}
#[derive(Debug, Clone)]
pub struct BertScore {
config: BertScoreConfig,
embedding_cache: HashMap<String, Vec<f64>>,
vocabulary: Option<HashSet<String>>,
}
impl Default for BertScore {
fn default() -> Self {
Self::new()
}
}
impl BertScore {
pub fn new() -> Self {
Self {
config: BertScoreConfig::default(),
embedding_cache: HashMap::new(),
vocabulary: None,
}
}
pub fn with_config(config: BertScoreConfig) -> Self {
Self {
config,
embedding_cache: HashMap::new(),
vocabulary: None,
}
}
pub fn compute(
&self,
references: &[&str],
candidates: &[&str],
) -> Result<Vec<BertScoreResult>> {
if references.len() != candidates.len() {
return Err(TextError::Other(anyhow::anyhow!(
"Number of references ({}) must match number of candidates ({})",
references.len(),
candidates.len()
)));
}
if references.is_empty() {
return Err(TextError::Other(anyhow::anyhow!(
"No input texts provided for BERTScore computation"
)));
}
let mut results = Vec::new();
for chunk in references
.chunks(self.config.batch_size)
.zip(candidates.chunks(self.config.batch_size))
{
let (ref_chunk, cand_chunk) = chunk;
let batch_results = self.compute_batch(ref_chunk, cand_chunk)?;
results.extend(batch_results);
}
Ok(results)
}
pub fn compute_detailed(&self, reference: &str, candidate: &str) -> Result<BertScoreResult> {
let mut result = self.compute_single(reference, candidate)?;
if let Ok(details) = self.compute_alignment_details(reference, candidate) {
result.alignment_details = Some(details);
}
Ok(result)
}
pub fn compute_corpus_statistics(
&self,
references: &[&str],
candidates: &[&str],
) -> Result<CorpusStatistics> {
let results = self.compute(references, candidates)?;
let precision_scores: Vec<f64> = results.iter().map(|r| r.precision).collect();
let recall_scores: Vec<f64> = results.iter().map(|r| r.recall).collect();
let f1_scores: Vec<f64> = results.iter().map(|r| r.f1_score).collect();
Ok(CorpusStatistics {
mean_precision: Self::mean(&precision_scores),
mean_recall: Self::mean(&recall_scores),
mean_f1: Self::mean(&f1_scores),
std_precision: Self::std_dev(&precision_scores),
std_recall: Self::std_dev(&recall_scores),
std_f1: Self::std_dev(&f1_scores),
median_precision: Self::median(&mut precision_scores.clone()),
median_recall: Self::median(&mut recall_scores.clone()),
median_f1: Self::median(&mut f1_scores.clone()),
num_pairs: results.len(),
})
}
pub fn compare_models(
&self,
other: &BertScore,
references: &[&str],
candidates: &[&str],
) -> Result<ModelComparison> {
let results1 = self.compute(references, candidates)?;
let results2 = other.compute(references, candidates)?;
let stats1 = self.compute_corpus_statistics(references, candidates)?;
let stats2 = other.compute_corpus_statistics(references, candidates)?;
let f1_improvement = stats2.mean_f1 - stats1.mean_f1;
let precision_improvement = stats2.mean_precision - stats1.mean_precision;
let recall_improvement = stats2.mean_recall - stats1.mean_recall;
let mut model1_wins = 0;
let mut model2_wins = 0;
let mut ties = 0;
for (r1, r2) in results1.iter().zip(results2.iter()) {
if (r1.f1_score - r2.f1_score).abs() < 1e-6 {
ties += 1;
} else if r1.f1_score > r2.f1_score {
model1_wins += 1;
} else {
model2_wins += 1;
}
}
Ok(ModelComparison {
model1_config: self.config.clone(),
model2_config: other.config.clone(),
model1_stats: stats1,
model2_stats: stats2,
f1_improvement,
precision_improvement,
recall_improvement,
model1_wins,
model2_wins,
ties,
})
}
fn compute_batch(
&self,
references: &[&str],
candidates: &[&str],
) -> Result<Vec<BertScoreResult>> {
let mut results = Vec::new();
for (reference, candidate) in references.iter().zip(candidates.iter()) {
let result = self.compute_single(reference, candidate)?;
results.push(result);
}
Ok(results)
}
fn compute_single(&self, reference: &str, candidate: &str) -> Result<BertScoreResult> {
let reference = if self.config.ignore_case {
reference.to_lowercase()
} else {
reference.to_string()
};
let candidate = if self.config.ignore_case {
candidate.to_lowercase()
} else {
candidate.to_string()
};
let ref_tokens = self.tokenize(&reference)?;
let cand_tokens = self.tokenize(&candidate)?;
if ref_tokens.is_empty() || cand_tokens.is_empty() {
return Ok(BertScoreResult {
precision: 0.0,
recall: 0.0,
f1_score: 0.0,
alignment_details: None,
});
}
let ref_embeddings = self.get_embeddings(&ref_tokens)?;
let cand_embeddings = self.get_embeddings(&cand_tokens)?;
let similarity_matrix =
self.compute_similarity_matrix(&ref_embeddings, &cand_embeddings)?;
let (precision, recall) = match self.config.alignment_algorithm {
AlignmentAlgorithm::Greedy => self.greedy_alignment(&similarity_matrix),
AlignmentAlgorithm::Optimal => self.optimal_alignment(&similarity_matrix)?,
AlignmentAlgorithm::Hierarchical => self.hierarchical_alignment(&similarity_matrix)?,
AlignmentAlgorithm::AttentionBased => {
self.attention_based_alignment(&similarity_matrix)?
}
};
let f1_score = if precision + recall > 0.0 {
2.0 * precision * recall / (precision + recall)
} else {
0.0
};
Ok(BertScoreResult {
precision,
recall,
f1_score,
alignment_details: None,
})
}
fn tokenize(&self, text: &str) -> Result<Vec<String>> {
match self.config.embedding_strategy {
EmbeddingStrategy::CharacterBased => Ok(text.chars().map(|c| c.to_string()).collect()),
EmbeddingStrategy::WordLevel => {
Ok(text.split_whitespace().map(|s| s.to_string()).collect())
}
EmbeddingStrategy::Subword => {
self.subword_tokenize(text)
}
EmbeddingStrategy::Contextual => {
Ok(text.split_whitespace().map(|s| s.to_string()).collect())
}
}
}
fn subword_tokenize(&self, text: &str) -> Result<Vec<String>> {
let words: Vec<&str> = text.split_whitespace().collect();
let mut tokens = Vec::new();
for word in words {
if word.len() <= 4 {
tokens.push(word.to_string());
} else {
let mid = word.len() / 2;
tokens.push(word[..mid].to_string());
tokens.push(word[mid..].to_string());
}
}
Ok(tokens)
}
fn get_embeddings(&self, tokens: &[String]) -> Result<Vec<Vec<f64>>> {
let mut embeddings = Vec::new();
for token in tokens {
let embedding = match self.config.embedding_strategy {
EmbeddingStrategy::CharacterBased => self.character_based_embedding(token),
EmbeddingStrategy::WordLevel => self.word_level_embedding(token),
EmbeddingStrategy::Subword => self.subword_embedding(token),
EmbeddingStrategy::Contextual => self.contextual_embedding(token),
};
embeddings.push(embedding);
}
Ok(embeddings)
}
fn character_based_embedding(&self, token: &str) -> Vec<f64> {
let mut embedding = vec![0.0; self.config.embedding_dim];
if token.is_empty() {
return embedding;
}
let chars: Vec<char> = token.chars().collect();
for (i, &ch) in chars.iter().enumerate() {
let char_code = ch as u32;
let pos_weight = (i as f64) / (chars.len() as f64);
let dim1 = (char_code % self.config.embedding_dim as u32) as usize;
let dim2 = ((char_code / 26) % self.config.embedding_dim as u32) as usize;
embedding[dim1] += pos_weight;
if dim2 != dim1 {
embedding[dim2] += 1.0 - pos_weight;
}
}
embedding[0] += chars.len() as f64 / 50.0; if chars.len() > 1 {
embedding[1] += if token.chars().any(|c| c.is_uppercase()) {
1.0
} else {
0.0
};
embedding[2] += if token.chars().any(|c| c.is_numeric()) {
1.0
} else {
0.0
};
}
self.normalize_if_configured(embedding)
}
fn word_level_embedding(&self, token: &str) -> Vec<f64> {
let mut embedding = vec![0.0; self.config.embedding_dim];
if token.is_empty() {
return embedding;
}
let chars: Vec<char> = token.chars().collect();
let len = chars.len();
for (i, &ch) in chars.iter().enumerate() {
let char_code = ch as u32;
let pos_weight = (i as f64) / (len as f64);
let dim1 = ((char_code * 31) % self.config.embedding_dim as u32) as usize;
let dim2 = ((char_code * 37 + i as u32) % self.config.embedding_dim as u32) as usize;
let dim3 = ((char_code * 41 + len as u32) % self.config.embedding_dim as u32) as usize;
embedding[dim1] += pos_weight * 0.5;
embedding[dim2] += (1.0 - pos_weight) * 0.3;
embedding[dim3] += 0.2;
}
let features = self.extract_linguistic_features(token);
for (i, feature) in features.iter().enumerate() {
if i < self.config.embedding_dim {
embedding[i] += feature * 0.1;
}
}
self.normalize_if_configured(embedding)
}
fn subword_embedding(&self, token: &str) -> Vec<f64> {
let char_emb = self.character_based_embedding(token);
let word_emb = self.word_level_embedding(token);
let mut embedding = vec![0.0; self.config.embedding_dim];
for i in 0..self.config.embedding_dim {
embedding[i] = 0.6 * word_emb[i] + 0.4 * char_emb[i];
}
self.normalize_if_configured(embedding)
}
fn contextual_embedding(&self, token: &str) -> Vec<f64> {
let mut embedding = self.word_level_embedding(token);
for i in 0..embedding.len() {
let context_factor = (token.len() as f64 * PI * i as f64).sin() * 0.1;
embedding[i] += context_factor;
}
self.normalize_if_configured(embedding)
}
fn extract_linguistic_features(&self, token: &str) -> Vec<f64> {
let mut features = vec![0.0; 16];
if token.is_empty() {
return features;
}
let chars: Vec<char> = token.chars().collect();
let len = chars.len();
features[0] = len as f64 / 20.0; features[1] = if token.chars().any(|c| c.is_uppercase()) {
1.0
} else {
0.0
};
features[2] = if token.chars().any(|c| c.is_numeric()) {
1.0
} else {
0.0
};
features[3] = if token.chars().any(|c| c.is_alphabetic()) {
1.0
} else {
0.0
};
features[4] = if token.contains('-') { 1.0 } else { 0.0 };
features[5] = if token.contains('_') { 1.0 } else { 0.0 };
features[6] = token.chars().filter(|c| c.is_vowel_like()).count() as f64 / len as f64;
features[7] = token.chars().filter(|c| c.is_consonant_like()).count() as f64 / len as f64;
let bigrams = self.get_char_ngrams(token, 2);
let trigrams = self.get_char_ngrams(token, 3);
features[8] = bigrams.len() as f64 / 10.0;
features[9] = trigrams.len() as f64 / 10.0;
features[10] = self.estimate_word_frequency(token);
features[11] = if token.len() > 3 && Self::is_common_prefix(&token[..3]) {
1.0
} else {
0.0
};
features[12] = if token.len() > 3 && Self::is_common_suffix(&token[token.len() - 3..]) {
1.0
} else {
0.0
};
features[13] = if token.ends_with("ing") { 1.0 } else { 0.0 };
features[14] = if token.ends_with("ed") { 1.0 } else { 0.0 };
features[15] = if token.ends_with("ly") { 1.0 } else { 0.0 };
features
}
fn get_char_ngrams(&self, text: &str, n: usize) -> HashSet<String> {
let chars: Vec<char> = text.chars().collect();
let mut ngrams = HashSet::new();
if chars.len() < n {
return ngrams;
}
for i in 0..=chars.len() - n {
let ngram: String = chars[i..i + n].iter().collect();
ngrams.insert(ngram);
}
ngrams
}
fn estimate_word_frequency(&self, word: &str) -> f64 {
match word.len() {
1..=2 => 0.9, 3..=4 => 0.7,
5..=6 => 0.5,
7..=8 => 0.3,
_ => 0.1,
}
}
fn is_common_prefix(s: &str) -> bool {
matches!(
s,
"pre" | "un" | "re" | "in" | "dis" | "en" | "non" | "over" | "mis" | "sub"
)
}
fn is_common_suffix(s: &str) -> bool {
matches!(
s,
"ing" | "ed" | "er" | "est" | "ly" | "ion" | "tion" | "ness" | "ment" | "ful"
)
}
fn normalize_if_configured(&self, mut embedding: Vec<f64>) -> Vec<f64> {
if self.config.normalize_embeddings {
let norm: f64 = embedding.iter().map(|x| x * x).sum::<f64>().sqrt();
if norm > 0.0 {
for val in &mut embedding {
*val /= norm;
}
}
}
embedding
}
fn compute_similarity_matrix(
&self,
ref_embeddings: &[Vec<f64>],
cand_embeddings: &[Vec<f64>],
) -> Result<Vec<Vec<f64>>> {
let mut matrix = vec![vec![0.0; cand_embeddings.len()]; ref_embeddings.len()];
for (i, ref_emb) in ref_embeddings.iter().enumerate() {
for (j, cand_emb) in cand_embeddings.iter().enumerate() {
matrix[i][j] = self.cosine_similarity(ref_emb, cand_emb);
}
}
Ok(matrix)
}
fn cosine_similarity(&self, vec1: &[f64], vec2: &[f64]) -> f64 {
if vec1.len() != vec2.len() {
return 0.0;
}
let dot_product: f64 = vec1.iter().zip(vec2.iter()).map(|(a, b)| a * b).sum();
let norm1: f64 = vec1.iter().map(|x| x * x).sum::<f64>().sqrt();
let norm2: f64 = vec2.iter().map(|x| x * x).sum::<f64>().sqrt();
if norm1 == 0.0 || norm2 == 0.0 {
0.0
} else {
dot_product / (norm1 * norm2)
}
}
fn greedy_alignment(&self, similarity_matrix: &[Vec<f64>]) -> (f64, f64) {
if similarity_matrix.is_empty() || similarity_matrix[0].is_empty() {
return (0.0, 0.0);
}
let num_ref = similarity_matrix.len();
let num_cand = similarity_matrix[0].len();
let mut precision_sum = 0.0;
for j in 0..num_cand {
let mut max_sim = 0.0;
for i in 0..num_ref {
max_sim = max_sim.max(similarity_matrix[i][j]);
}
precision_sum += max_sim;
}
let precision = precision_sum / num_cand as f64;
let mut recall_sum = 0.0;
for i in 0..num_ref {
let mut max_sim = 0.0;
for j in 0..num_cand {
max_sim = max_sim.max(similarity_matrix[i][j]);
}
recall_sum += max_sim;
}
let recall = recall_sum / num_ref as f64;
(precision, recall)
}
fn optimal_alignment(&self, similarity_matrix: &[Vec<f64>]) -> Result<(f64, f64)> {
Ok(self.greedy_alignment(similarity_matrix))
}
fn hierarchical_alignment(&self, similarity_matrix: &[Vec<f64>]) -> Result<(f64, f64)> {
let chunk_size = 5;
let num_ref = similarity_matrix.len();
let num_cand = if similarity_matrix.is_empty() {
0
} else {
similarity_matrix[0].len()
};
if num_ref <= chunk_size || num_cand <= chunk_size {
return Ok(self.greedy_alignment(similarity_matrix));
}
let mut total_precision = 0.0;
let mut total_recall = 0.0;
let mut chunks_processed = 0;
for ref_start in (0..num_ref).step_by(chunk_size) {
for cand_start in (0..num_cand).step_by(chunk_size) {
let ref_end = (ref_start + chunk_size).min(num_ref);
let cand_end = (cand_start + chunk_size).min(num_cand);
let chunk: Vec<Vec<f64>> = similarity_matrix[ref_start..ref_end]
.iter()
.map(|row| row[cand_start..cand_end].to_vec())
.collect();
let (chunk_precision, chunk_recall) = self.greedy_alignment(&chunk);
total_precision += chunk_precision;
total_recall += chunk_recall;
chunks_processed += 1;
}
}
let avg_precision = if chunks_processed > 0 {
total_precision / chunks_processed as f64
} else {
0.0
};
let avg_recall = if chunks_processed > 0 {
total_recall / chunks_processed as f64
} else {
0.0
};
Ok((avg_precision, avg_recall))
}
fn attention_based_alignment(&self, similarity_matrix: &[Vec<f64>]) -> Result<(f64, f64)> {
let num_ref = similarity_matrix.len();
let num_cand = if similarity_matrix.is_empty() {
0
} else {
similarity_matrix[0].len()
};
if num_ref == 0 || num_cand == 0 {
return Ok((0.0, 0.0));
}
let mut precision_sum = 0.0;
let mut recall_sum = 0.0;
for j in 0..num_cand {
let mut attention_weights = Vec::new();
let mut total_weight = 0.0;
for i in 0..num_ref {
let weight = similarity_matrix[i][j].exp(); attention_weights.push(weight);
total_weight += weight;
}
if total_weight > 0.0 {
let mut weighted_sim = 0.0;
for (i, &weight) in attention_weights.iter().enumerate() {
weighted_sim += (weight / total_weight) * similarity_matrix[i][j];
}
precision_sum += weighted_sim;
}
}
for i in 0..num_ref {
let mut attention_weights = Vec::new();
let mut total_weight = 0.0;
for j in 0..num_cand {
let weight = similarity_matrix[i][j].exp();
attention_weights.push(weight);
total_weight += weight;
}
if total_weight > 0.0 {
let mut weighted_sim = 0.0;
for (j, &weight) in attention_weights.iter().enumerate() {
weighted_sim += (weight / total_weight) * similarity_matrix[i][j];
}
recall_sum += weighted_sim;
}
}
let precision = precision_sum / num_cand as f64;
let recall = recall_sum / num_ref as f64;
Ok((precision, recall))
}
fn compute_alignment_details(
&self,
reference: &str,
candidate: &str,
) -> Result<AlignmentDetails> {
let ref_tokens = self.tokenize(reference)?;
let cand_tokens = self.tokenize(candidate)?;
let ref_embeddings = self.get_embeddings(&ref_tokens)?;
let cand_embeddings = self.get_embeddings(&cand_tokens)?;
let similarity_matrix =
self.compute_similarity_matrix(&ref_embeddings, &cand_embeddings)?;
let mut alignments = Vec::new();
for i in 0..ref_tokens.len() {
let mut best_j = 0;
let mut best_sim = 0.0;
for j in 0..cand_tokens.len() {
if similarity_matrix[i][j] > best_sim {
best_sim = similarity_matrix[i][j];
best_j = j;
}
}
if best_sim > 0.1 {
alignments.push((i, best_j, best_sim));
}
}
Ok(AlignmentDetails {
reference_tokens: ref_tokens,
candidate_tokens: cand_tokens,
token_similarities: similarity_matrix,
alignments,
})
}
fn mean(values: &[f64]) -> f64 {
if values.is_empty() {
0.0
} else {
values.iter().sum::<f64>() / values.len() as f64
}
}
fn std_dev(values: &[f64]) -> f64 {
if values.len() < 2 {
0.0
} else {
let mean = Self::mean(values);
let variance =
values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (values.len() - 1) as f64;
variance.sqrt()
}
}
fn median(values: &mut [f64]) -> f64 {
if values.is_empty() {
return 0.0;
}
values.sort_by(|a, b| {
a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
});
let mid = values.len() / 2;
if values.len() % 2 == 0 {
(values[mid - 1] + values[mid]) / 2.0
} else {
values[mid]
}
}
}
#[derive(Debug, Clone)]
pub struct CorpusStatistics {
pub mean_precision: f64,
pub mean_recall: f64,
pub mean_f1: f64,
pub std_precision: f64,
pub std_recall: f64,
pub std_f1: f64,
pub median_precision: f64,
pub median_recall: f64,
pub median_f1: f64,
pub num_pairs: usize,
}
#[derive(Debug, Clone)]
pub struct ModelComparison {
pub model1_config: BertScoreConfig,
pub model2_config: BertScoreConfig,
pub model1_stats: CorpusStatistics,
pub model2_stats: CorpusStatistics,
pub f1_improvement: f64,
pub precision_improvement: f64,
pub recall_improvement: f64,
pub model1_wins: usize,
pub model2_wins: usize,
pub ties: usize,
}
trait CharExt {
fn is_vowel_like(&self) -> bool;
fn is_consonant_like(&self) -> bool;
}
impl CharExt for char {
fn is_vowel_like(&self) -> bool {
matches!(
self.to_lowercase().next(),
Some('a' | 'e' | 'i' | 'o' | 'u' | 'y')
)
}
fn is_consonant_like(&self) -> bool {
self.is_alphabetic() && !self.is_vowel_like()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_bert_score() {
let scorer = BertScore::new();
let references = &["The cat sat on the mat"];
let candidates = &["A cat was sitting on the mat"];
let results = scorer.compute(references, candidates).expect("computation should succeed");
assert_eq!(results.len(), 1);
let result = &results[0];
assert!(result.precision > 0.0 && result.precision <= 1.0);
assert!(result.recall > 0.0 && result.recall <= 1.0);
assert!(result.f1_score > 0.0 && result.f1_score <= 1.0);
}
#[test]
fn test_perfect_match() {
let scorer = BertScore::new();
let references = &["hello world"];
let candidates = &["hello world"];
let results = scorer.compute(references, candidates).expect("computation should succeed");
let result = &results[0];
assert!(result.f1_score > 0.9);
}
#[test]
fn test_empty_inputs() {
let scorer = BertScore::new();
let references = &[""];
let candidates = &["hello"];
let results = scorer.compute(references, candidates).expect("computation should succeed");
let result = &results[0];
assert_eq!(result.precision, 0.0);
assert_eq!(result.recall, 0.0);
assert_eq!(result.f1_score, 0.0);
}
#[test]
fn test_different_configurations() {
let config1 =
BertScoreConfig::new().with_embedding_strategy(EmbeddingStrategy::CharacterBased);
let config2 = BertScoreConfig::new().with_embedding_strategy(EmbeddingStrategy::WordLevel);
let scorer1 = BertScore::with_config(config1);
let scorer2 = BertScore::with_config(config2);
let references = &["The quick brown fox"];
let candidates = &["A fast brown fox"];
let results1 = scorer1.compute(references, candidates).expect("computation should succeed");
let results2 = scorer2.compute(references, candidates).expect("computation should succeed");
assert_ne!(results1[0].f1_score, results2[0].f1_score);
}
#[test]
fn test_alignment_algorithms() {
let config_greedy =
BertScoreConfig::new().with_alignment_algorithm(AlignmentAlgorithm::Greedy);
let config_attention =
BertScoreConfig::new().with_alignment_algorithm(AlignmentAlgorithm::AttentionBased);
let scorer_greedy = BertScore::with_config(config_greedy);
let scorer_attention = BertScore::with_config(config_attention);
let references = &["The cat sat"];
let candidates = &["Cat was sitting"];
let results_greedy = scorer_greedy.compute(references, candidates).expect("computation should succeed");
let results_attention = scorer_attention.compute(references, candidates).expect("computation should succeed");
assert!(results_greedy[0].f1_score > 0.0);
assert!(results_attention[0].f1_score > 0.0);
}
#[test]
fn test_detailed_computation() {
let scorer = BertScore::new();
let result = scorer.compute_detailed("hello world", "hi world").expect("detailed computation should succeed");
assert!(result.precision > 0.0);
assert!(result.recall > 0.0);
assert!(result.alignment_details.is_some());
let details = result.alignment_details.expect("operation should succeed");
assert!(!details.reference_tokens.is_empty());
assert!(!details.candidate_tokens.is_empty());
assert!(!details.token_similarities.is_empty());
}
#[test]
fn test_corpus_statistics() {
let scorer = BertScore::new();
let references = &["hello world", "the cat sat", "quick brown fox"];
let candidates = &["hi world", "a cat sits", "fast brown fox"];
let stats = scorer
.compute_corpus_statistics(references, candidates)
.expect("operation should succeed");
assert_eq!(stats.num_pairs, 3);
assert!(stats.mean_f1 > 0.0);
assert!(stats.std_f1 >= 0.0);
assert!(stats.median_f1 > 0.0);
}
#[test]
fn test_model_comparison() {
let scorer1 = BertScore::new();
let config2 = BertScoreConfig::new().with_embedding_strategy(EmbeddingStrategy::Subword);
let scorer2 = BertScore::with_config(config2);
let references = &["hello world", "the cat"];
let candidates = &["hi world", "a cat"];
let comparison = scorer1
.compare_models(&scorer2, references, candidates)
.expect("operation should succeed");
assert_eq!(
comparison.model1_wins + comparison.model2_wins + comparison.ties,
2
);
assert!(comparison.f1_improvement.abs() >= 0.0); }
#[test]
fn test_tokenization_strategies() {
let scorer = BertScore::new();
let char_tokens = scorer.tokenize("hello").expect("tokenization should succeed");
assert_eq!(char_tokens.len(), 1);
let config =
BertScoreConfig::new().with_embedding_strategy(EmbeddingStrategy::CharacterBased);
let char_scorer = BertScore::with_config(config);
let char_tokens = char_scorer.tokenize("hello").expect("tokenization should succeed");
assert_eq!(char_tokens.len(), 5); }
#[test]
fn test_linguistic_features() {
let scorer = BertScore::new();
let features = scorer.extract_linguistic_features("running");
assert_eq!(features.len(), 16);
assert!(features[0] > 0.0); assert_eq!(features[13], 1.0); }
#[test]
fn test_normalization() {
let config = BertScoreConfig::new().with_normalize_embeddings(true);
let scorer = BertScore::with_config(config);
let embedding = scorer.word_level_embedding("test");
let norm: f64 = embedding.iter().map(|x| x * x).sum::<f64>().sqrt();
assert!((norm - 1.0).abs() < 1e-6); }
#[test]
fn test_case_sensitivity() {
let config = BertScoreConfig::new().with_ignore_case(true);
let scorer = BertScore::with_config(config);
let references = &["Hello World"];
let candidates = &["hello world"];
let results = scorer.compute(references, candidates).expect("computation should succeed");
assert!(results[0].f1_score > 0.95); }
#[test]
fn test_similarity_matrix() {
let scorer = BertScore::new();
let ref_embeddings = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
let cand_embeddings = vec![vec![1.0, 0.0], vec![0.5, 0.5]];
let matrix = scorer
.compute_similarity_matrix(&ref_embeddings, &cand_embeddings)
.expect("operation should succeed");
assert_eq!(matrix.len(), 2);
assert_eq!(matrix[0].len(), 2);
assert!((matrix[0][0] - 1.0).abs() < 1e-6); assert!(matrix[0][1] > 0.0 && matrix[0][1] < 1.0); }
#[test]
fn test_error_handling() {
let scorer = BertScore::new();
let result = scorer.compute(&["ref1", "ref2"], &["cand1"]);
assert!(result.is_err());
let result = scorer.compute(&[], &[]);
assert!(result.is_err());
}
}