zsfm_tabfm/ensemble/
aggregate.rs1pub fn softmax_temperature(logits: &[f64], temperature: f64) -> Vec<f64> {
6 let scaled: Vec<f64> = logits.iter().map(|&v| v / temperature).collect();
7 let max = scaled.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
8 let exp: Vec<f64> = scaled.iter().map(|&v| (v - max).exp()).collect();
9 let sum: f64 = exp.iter().sum();
10 exp.into_iter().map(|v| v / sum).collect()
11}
12
13pub fn unshift_logits(logits: &[f64], shift: usize, n_classes: usize) -> Vec<f64> {
17 (0..n_classes)
18 .map(|c| logits[(c + shift) % n_classes])
19 .collect()
20}
21
22fn average_vecs(vecs: &[Vec<f64>]) -> Vec<f64> {
23 let n = vecs.len() as f64;
24 let dim = vecs[0].len();
25 let mut out = vec![0.0; dim];
26 for v in vecs {
27 for (o, x) in out.iter_mut().zip(v.iter()) {
28 *o += x / n;
29 }
30 }
31 out
32}
33
34fn weighted_average_vecs(vecs: &[Vec<f64>], weights: &[f64]) -> Vec<f64> {
35 let dim = vecs[0].len();
36 let mut out = vec![0.0; dim];
37 for (v, &w) in vecs.iter().zip(weights.iter()) {
38 for (o, x) in out.iter_mut().zip(v.iter()) {
39 *o += x * w;
40 }
41 }
42 out
43}
44
45pub enum ClassAggMode<'a> {
46 NnlsWeighted(&'a [f64]),
48 AverageLogits,
51 AverageProbs,
53}
54
55pub fn aggregate_classification(
58 logits_all: &[Vec<f64>],
59 temperature: f64,
60 mode: &ClassAggMode,
61) -> Vec<f64> {
62 match mode {
63 ClassAggMode::NnlsWeighted(weights) => {
64 let probs_all: Vec<Vec<f64>> = logits_all
65 .iter()
66 .map(|l| softmax_temperature(l, temperature))
67 .collect();
68 weighted_average_vecs(&probs_all, weights)
69 }
70 ClassAggMode::AverageLogits => {
71 let avg = average_vecs(logits_all);
72 softmax_temperature(&avg, temperature)
73 }
74 ClassAggMode::AverageProbs => {
75 let probs_all: Vec<Vec<f64>> = logits_all
76 .iter()
77 .map(|l| softmax_temperature(l, temperature))
78 .collect();
79 average_vecs(&probs_all)
80 }
81 }
82}
83
84pub fn average_scaled_predictions(scaled: &[f64]) -> f64 {
87 scaled.iter().sum::<f64>() / scaled.len() as f64
88}
89
90pub fn weighted_unscaled_predictions(unscaled: &[f64], weights: &[f64]) -> f64 {
93 unscaled
94 .iter()
95 .zip(weights.iter())
96 .map(|(p, w)| p * w)
97 .sum()
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn test_softmax_sums_to_one() {
106 let p = softmax_temperature(&[1.0, 2.0, 3.0], 0.9);
107 let sum: f64 = p.iter().sum();
108 assert!((sum - 1.0).abs() < 1e-9);
109 }
110
111 #[test]
112 fn test_unshift_roundtrip() {
113 let n_classes = 3;
115 let shift = 1;
116 let shifted_logits = vec![9.0, 0.1, 0.2];
119 let unshifted = unshift_logits(&shifted_logits, shift, n_classes);
120 assert_eq!(unshifted[2], 9.0);
122 }
123
124 #[test]
125 fn test_average_logits_vs_average_probs_differ() {
126 let logits_all = vec![vec![10.0, 0.0], vec![0.0, 10.0]];
127 let via_logits = aggregate_classification(&logits_all, 0.9, &ClassAggMode::AverageLogits);
128 let via_probs = aggregate_classification(&logits_all, 0.9, &ClassAggMode::AverageProbs);
129 assert!((via_logits[0] - 0.5).abs() < 1e-6);
131 let logits_all2 = vec![vec![10.0, 0.0], vec![1.0, 0.0]];
134 let l2 = aggregate_classification(&logits_all2, 0.9, &ClassAggMode::AverageLogits);
135 let p2 = aggregate_classification(&logits_all2, 0.9, &ClassAggMode::AverageProbs);
136 assert!(
137 (l2[0] - p2[0]).abs() > 1e-3,
138 "expected the two paths to diverge on asymmetric input"
139 );
140 let _ = via_probs;
141 }
142}