zsfm_tabfm/ensemble/
calibration.rs1const EPS: f64 = 1e-12;
9
10fn golden_section_bounded(f: impl Fn(f64) -> f64, lo: f64, hi: f64) -> f64 {
13 const GR: f64 = 0.618_033_988_749_895; let (mut a, mut b) = (lo, hi);
15 let mut c = b - GR * (b - a);
16 let mut d = a + GR * (b - a);
17 let mut fc = f(c);
18 let mut fd = f(d);
19 for _ in 0..100 {
20 if (b - a).abs() < 1e-10 {
21 break;
22 }
23 if fc < fd {
24 b = d;
25 d = c;
26 fd = fc;
27 c = b - GR * (b - a);
28 fc = f(c);
29 } else {
30 a = c;
31 c = d;
32 fc = fd;
33 d = a + GR * (b - a);
34 fd = f(d);
35 }
36 }
37 0.5 * (a + b)
38}
39
40pub struct PlattParams {
41 pub a: f64,
42 pub b: f64,
43}
44
45impl PlattParams {
46 pub fn fit(p_all: &[Vec<f64>], y: &[usize], lambda: f64) -> Self {
48 let z: Vec<f64> = p_all
49 .iter()
50 .map(|p| ((p[1] + EPS) / (p[0] + EPS)).ln())
51 .collect();
52 let loss = |a: f64, b: f64| -> f64 {
53 let n = z.len() as f64;
54 let mut nll = 0.0;
55 for i in 0..z.len() {
56 let p1 = sigmoid(a * z[i] + b);
57 let p_correct = if y[i] == 1 { p1 } else { 1.0 - p1 };
58 nll -= (p_correct + EPS).ln();
59 }
60 nll / n + lambda * ((a - 1.0).powi(2) + b.powi(2))
61 };
62
63 let mut a = 1.0f64;
64 let mut b = 0.0f64;
65 for _ in 0..20 {
66 a = golden_section_bounded(|av| loss(av, b), 0.8, 1.2);
67 b = golden_section_bounded(|bv| loss(a, bv), -1.0, 1.0);
68 }
69 PlattParams { a, b }
70 }
71
72 pub fn apply(&self, p: &[f64]) -> Vec<f64> {
73 let z = ((p[1] + EPS) / (p[0] + EPS)).ln();
74 let p1 = sigmoid(self.a * z + self.b);
75 vec![1.0 - p1, p1]
76 }
77}
78
79pub struct VectorScalingParams {
80 pub w: Vec<f64>,
81 pub b: Vec<f64>,
82}
83
84impl VectorScalingParams {
85 pub fn fit(p_all: &[Vec<f64>], y: &[usize], lambda: f64) -> Self {
87 let k = p_all[0].len();
88 let z: Vec<Vec<f64>> = p_all
89 .iter()
90 .map(|p| p.iter().map(|&v| (v + EPS).ln()).collect())
91 .collect();
92
93 let loss = |w: &[f64], b: &[f64]| -> f64 {
94 let n = z.len() as f64;
95 let mut nll = 0.0;
96 for (i, zi) in z.iter().enumerate() {
97 let logits: Vec<f64> = (0..k).map(|c| w[c] * zi[c] + b[c]).collect();
98 let probs = softmax(&logits);
99 nll -= (probs[y[i]] + EPS).ln();
100 }
101 let reg: f64 = w.iter().map(|&wv| (wv - 1.0).powi(2)).sum::<f64>()
102 + b.iter().map(|&bv| bv.powi(2)).sum::<f64>();
103 nll / n + lambda * reg
104 };
105
106 let mut w = vec![1.0f64; k];
107 let mut b = vec![0.0f64; k];
108 for _ in 0..20 {
109 for c in 0..k {
110 let (w2, b2) = (w.clone(), b.clone());
111 w[c] = golden_section_bounded(
112 |wc| {
113 let mut wt = w2.clone();
114 wt[c] = wc;
115 loss(&wt, &b2)
116 },
117 0.8,
118 1.2,
119 );
120 }
121 for c in 0..k {
122 let (w2, b2) = (w.clone(), b.clone());
123 b[c] = golden_section_bounded(
124 |bc| {
125 let mut bt = b2.clone();
126 bt[c] = bc;
127 loss(&w2, &bt)
128 },
129 -1.0,
130 1.0,
131 );
132 }
133 }
134 VectorScalingParams { w, b }
135 }
136
137 pub fn apply(&self, p: &[f64]) -> Vec<f64> {
138 let k = p.len();
139 let logits: Vec<f64> = (0..k)
140 .map(|c| self.w[c] * (p[c] + EPS).ln() + self.b[c])
141 .collect();
142 softmax(&logits)
143 }
144}
145
146fn sigmoid(x: f64) -> f64 {
147 1.0 / (1.0 + (-x).exp())
148}
149
150fn softmax(logits: &[f64]) -> Vec<f64> {
151 let max = logits.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
152 let exp: Vec<f64> = logits.iter().map(|&v| (v - max).exp()).collect();
153 let sum: f64 = exp.iter().sum();
154 exp.into_iter().map(|v| v / sum).collect()
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn test_platt_improves_calibration_of_overconfident_probs() {
163 let p_all = vec![
167 vec![0.99, 0.01],
168 vec![0.02, 0.98],
169 vec![0.6, 0.4],
170 vec![0.4, 0.6],
171 ];
172 let y = vec![0, 1, 0, 1];
173 let params = PlattParams::fit(&p_all, &y, 1e-2);
174 for p in &p_all {
175 let out = params.apply(p);
176 assert!((out[0] + out[1] - 1.0).abs() < 1e-6);
177 assert!(out[0] >= 0.0 && out[1] >= 0.0);
178 }
179 }
180
181 #[test]
182 fn test_vector_scaling_valid_probabilities() {
183 let p_all = vec![
184 vec![0.7, 0.2, 0.1],
185 vec![0.1, 0.8, 0.1],
186 vec![0.2, 0.2, 0.6],
187 ];
188 let y = vec![0, 1, 2];
189 let params = VectorScalingParams::fit(&p_all, &y, 1e-2);
190 for p in &p_all {
191 let out = params.apply(p);
192 let sum: f64 = out.iter().sum();
193 assert!((sum - 1.0).abs() < 1e-6);
194 }
195 }
196}