use ndarray::Array2;
#[derive(Debug, Clone)]
pub struct ConfidenceCalibrator {
pub temperature: f32,
}
impl Default for ConfidenceCalibrator {
fn default() -> Self {
Self { temperature: 1.0 }
}
}
impl ConfidenceCalibrator {
pub fn new(temperature: f32) -> Self {
Self {
temperature: temperature.max(1e-6),
}
}
pub fn softmax_row(&self, logits: &[f32]) -> Vec<f32> {
let t = self.temperature;
let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let exps: Vec<f32> = logits.iter().map(|&x| ((x - max) / t).exp()).collect();
let sum: f32 = exps.iter().sum();
if sum <= 0.0 || !sum.is_finite() {
let n = logits.len().max(1) as f32;
return vec![1.0 / n; logits.len()];
}
exps.into_iter().map(|e| e / sum).collect()
}
pub fn calibrate_prob(&self, raw: f32) -> f32 {
let p = raw.clamp(0.0, 1.0);
if self.temperature <= 1e-6 {
return p;
}
p.powf(1.0 / self.temperature).clamp(0.0, 1.0)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CharConfidence {
pub character: char,
pub confidence: f32,
pub raw_prob: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DecodeConfidence {
pub chars: Vec<CharConfidence>,
pub overall: f32,
}
impl DecodeConfidence {
pub fn empty() -> Self {
Self {
chars: Vec::new(),
overall: 0.0,
}
}
pub fn word_confidences(&self) -> Vec<(String, f32)> {
let mut words = Vec::new();
let mut current: Vec<&CharConfidence> = Vec::new();
for ch in &self.chars {
if ch.character.is_alphanumeric() {
current.push(ch);
} else {
if !current.is_empty() {
words.push(aggregate_word(¤t));
current.clear();
}
}
}
if !current.is_empty() {
words.push(aggregate_word(¤t));
}
words
}
}
fn aggregate_word(chars: &[&CharConfidence]) -> (String, f32) {
let text: String = chars.iter().map(|c| c.character).collect();
let mean = chars.iter().map(|c| c.confidence).sum::<f32>() / chars.len() as f32;
(text, mean)
}
pub fn greedy_path_confidence(
logits: &Array2<f32>,
vocab: &[char],
calibrator: &ConfidenceCalibrator,
) -> DecodeConfidence {
let (seq_len, num_classes) = logits.dim();
if seq_len == 0 || num_classes == 0 {
return DecodeConfidence::empty();
}
let mut chars = Vec::new();
let mut prev_label = 0usize;
for t in 0..seq_len {
let row: Vec<f32> = (0..num_classes).map(|c| logits[[t, c]]).collect();
let probs = calibrator.softmax_row(&row);
let best_label = probs
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
.unwrap_or(0);
if best_label != 0 && best_label != prev_label {
if best_label > 0 && best_label - 1 < vocab.len() {
let raw = probs[best_label];
chars.push(CharConfidence {
character: vocab[best_label - 1],
confidence: calibrator.calibrate_prob(raw),
raw_prob: raw,
});
}
}
prev_label = best_label;
}
let overall = if chars.is_empty() {
0.0
} else {
chars.iter().map(|c| c.confidence).sum::<f32>() / chars.len() as f32
};
DecodeConfidence { chars, overall }
}
pub fn hypothesis_confidence(
logits: &Array2<f32>,
text: &str,
calibrator: &ConfidenceCalibrator,
) -> DecodeConfidence {
let (seq_len, num_classes) = logits.dim();
if seq_len == 0 || text.is_empty() || num_classes == 0 {
return DecodeConfidence::empty();
}
let mut frame_peaks = Vec::with_capacity(seq_len);
for t in 0..seq_len {
let row: Vec<f32> = (0..num_classes).map(|c| logits[[t, c]]).collect();
let probs = calibrator.softmax_row(&row);
let peak = probs
.iter()
.enumerate()
.filter(|(i, _)| *i != 0)
.map(|(_, &p)| p)
.fold(0.0f32, f32::max);
if peak > 0.0 {
frame_peaks.push(peak);
}
}
let text_chars: Vec<char> = text.chars().collect();
if text_chars.is_empty() {
return DecodeConfidence::empty();
}
let chars: Vec<CharConfidence> = if frame_peaks.is_empty() {
text_chars
.iter()
.map(|&c| CharConfidence {
character: c,
confidence: 0.0,
raw_prob: 0.0,
})
.collect()
} else {
let n = text_chars.len();
text_chars
.iter()
.enumerate()
.map(|(i, &c)| {
let start = i * frame_peaks.len() / n;
let end = ((i + 1) * frame_peaks.len() / n).max(start + 1);
let slice = &frame_peaks[start.min(frame_peaks.len())..end.min(frame_peaks.len())];
let raw = if slice.is_empty() {
0.0
} else {
slice.iter().sum::<f32>() / slice.len() as f32
};
CharConfidence {
character: c,
confidence: calibrator.calibrate_prob(raw),
raw_prob: raw,
}
})
.collect()
};
let overall = chars.iter().map(|c| c.confidence).sum::<f32>() / chars.len() as f32;
DecodeConfidence { chars, overall }
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::Array2;
#[test]
fn test_softmax_sums_to_one() {
let cal = ConfidenceCalibrator::default();
let probs = cal.softmax_row(&[1.0, 2.0, 3.0]);
let sum: f32 = probs.iter().sum();
assert!((sum - 1.0).abs() < 1e-5);
assert!(probs[2] > probs[1] && probs[1] > probs[0]);
}
#[test]
fn test_temperature_softens() {
let sharp = ConfidenceCalibrator::new(0.5);
let soft = ConfidenceCalibrator::new(2.0);
let logits = [0.0, 5.0, 0.0];
let p_sharp = sharp.softmax_row(&logits);
let p_soft = soft.softmax_row(&logits);
assert!(p_soft[1] < p_sharp[1]);
}
#[test]
fn test_calibrate_prob_in_range() {
let cal = ConfidenceCalibrator::new(1.5);
assert!((0.0..=1.0).contains(&cal.calibrate_prob(0.9)));
assert!((0.0..=1.0).contains(&cal.calibrate_prob(-1.0)));
assert!((0.0..=1.0).contains(&cal.calibrate_prob(2.0)));
}
#[test]
fn test_greedy_path_confidence_high_on_clear_signal() {
let vocab = vec!['a', 'b'];
let cal = ConfidenceCalibrator::default();
let logits = Array2::from_shape_vec(
(3, 3),
vec![
-10.0, 10.0, -10.0, 10.0, -10.0, -10.0, -10.0, -10.0, 10.0, ],
)
.unwrap();
let conf = greedy_path_confidence(&logits, &vocab, &cal);
assert_eq!(conf.chars.len(), 2);
assert_eq!(conf.chars[0].character, 'a');
assert_eq!(conf.chars[1].character, 'b');
assert!(conf.chars[0].confidence > 0.9);
assert!(conf.chars[1].confidence > 0.9);
assert!(conf.overall > 0.9);
}
#[test]
fn test_word_confidences_split() {
let conf = DecodeConfidence {
chars: vec![
CharConfidence {
character: 'h',
confidence: 0.9,
raw_prob: 0.9,
},
CharConfidence {
character: 'i',
confidence: 0.7,
raw_prob: 0.7,
},
CharConfidence {
character: ' ',
confidence: 0.5,
raw_prob: 0.5,
},
CharConfidence {
character: 'a',
confidence: 0.8,
raw_prob: 0.8,
},
],
overall: 0.75,
};
let words = conf.word_confidences();
assert_eq!(words.len(), 2);
assert_eq!(words[0].0, "hi");
assert!((words[0].1 - 0.8).abs() < 1e-5);
assert_eq!(words[1].0, "a");
assert!((words[1].1 - 0.8).abs() < 1e-5);
}
#[test]
fn test_hypothesis_confidence_assigns_all_chars() {
let cal = ConfidenceCalibrator::default();
let logits = Array2::from_shape_vec(
(4, 3),
vec![
-5.0, 5.0, -5.0, -5.0, 4.0, -5.0, -5.0, -5.0, 5.0, 5.0, -5.0, -5.0,
],
)
.unwrap();
let conf = hypothesis_confidence(&logits, "ab", &cal);
assert_eq!(conf.chars.len(), 2);
assert!(conf.overall > 0.5);
}
}