ipfrs_tensorlogic/ensemble_learner/
functions.rs1use super::types::{ElBaseModel, ElError, ElSample};
6
7#[inline]
8pub(super) fn xorshift64(state: &mut u64) -> u64 {
9 let mut x = *state;
10 x ^= x << 13;
11 x ^= x >> 7;
12 x ^= x << 17;
13 *state = x;
14 x
15}
16#[inline]
18pub(super) fn xorshift_f64(state: &mut u64) -> f64 {
19 (xorshift64(state) >> 11) as f64 / (1u64 << 53) as f64
20}
21#[inline]
23pub(super) fn xorshift_usize(state: &mut u64, n: usize) -> usize {
24 (xorshift64(state) as usize).wrapping_rem(n)
25}
26pub(super) fn bootstrap_indices(rng: &mut u64, pool_size: usize, n: usize) -> Vec<usize> {
28 (0..n).map(|_| xorshift_usize(rng, pool_size)).collect()
29}
30#[allow(dead_code)]
32pub(super) fn weighted_bootstrap(rng: &mut u64, weights: &[f64], n: usize) -> Vec<usize> {
33 let total: f64 = weights.iter().sum();
34 let cdf: Vec<f64> = weights
35 .iter()
36 .scan(0.0f64, |acc, w| {
37 *acc += w / total;
38 Some(*acc)
39 })
40 .collect();
41 (0..n)
42 .map(|_| {
43 let u = xorshift_f64(rng);
44 cdf.partition_point(|&v| v < u).min(weights.len() - 1)
45 })
46 .collect()
47}
48pub(super) fn best_stump(
53 samples: &[ElSample],
54 sample_weights: &[f64],
55 feature_subset: &[usize],
56) -> Result<(usize, f64, bool, f64), ElError> {
57 let n = samples.len();
58 if n == 0 {
59 return Err(ElError::EmptyTrainingSet);
60 }
61 let n_feat = samples
62 .first()
63 .ok_or(ElError::EmptyTrainingSet)?
64 .features
65 .len();
66 if n_feat == 0 {
67 return Err(ElError::InvalidConfig(
68 "samples must have at least one feature".to_string(),
69 ));
70 }
71 let total_weight: f64 = sample_weights.iter().sum();
72 if total_weight <= 0.0 {
73 return Err(ElError::Arithmetic(
74 "sample weights sum to zero".to_string(),
75 ));
76 }
77 let mut best_err = f64::MAX;
78 let mut best_feat = 0usize;
79 let mut best_thresh = 0.0f64;
80 let mut best_dir = true;
81 for &feat_idx in feature_subset {
82 let mut vals: Vec<(f64, f64, f64)> = samples
83 .iter()
84 .zip(sample_weights.iter())
85 .map(|(s, &w)| {
86 let fv = s.features.get(feat_idx).copied().unwrap_or(0.0);
87 (fv, s.label, w)
88 })
89 .collect();
90 vals.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
91 for i in 0..vals.len().saturating_sub(1) {
92 let thresh = (vals[i].0 + vals[i + 1].0) / 2.0;
93 for &dir in &[true, false] {
94 let err: f64 = vals
95 .iter()
96 .map(|(fv, label, w)| {
97 let pred = if dir { *fv <= thresh } else { *fv > thresh };
98 let pred_val: f64 = if pred { 1.0 } else { -1.0 };
99 let label_sign: f64 = if *label >= 0.0 { 1.0 } else { -1.0 };
100 if (pred_val - label_sign).abs() > 1e-9 {
101 *w
102 } else {
103 0.0
104 }
105 })
106 .sum::<f64>()
107 / total_weight;
108 if err < best_err {
109 best_err = err;
110 best_feat = feat_idx;
111 best_thresh = thresh;
112 best_dir = dir;
113 }
114 }
115 }
116 for &dir in &[true, false] {
117 let thresh = vals.first().map(|v| v.0 - 1.0).unwrap_or(-1.0);
118 let err: f64 = vals
119 .iter()
120 .map(|(fv, label, w)| {
121 let pred = if dir { *fv <= thresh } else { *fv > thresh };
122 let pred_val: f64 = if pred { 1.0 } else { -1.0 };
123 let label_sign: f64 = if *label >= 0.0 { 1.0 } else { -1.0 };
124 if (pred_val - label_sign).abs() > 1e-9 {
125 *w
126 } else {
127 0.0
128 }
129 })
130 .sum::<f64>()
131 / total_weight;
132 if err < best_err {
133 best_err = err;
134 best_feat = feat_idx;
135 best_thresh = thresh;
136 best_dir = dir;
137 }
138 }
139 }
140 Ok((best_feat, best_thresh, best_dir, best_err))
141}
142pub(super) fn best_regression_stump(
148 samples: &[ElSample],
149 residuals: &[f64],
150 feature_subset: &[usize],
151) -> Result<(usize, f64, bool, f64, f64), ElError> {
152 let n = samples.len();
153 if n == 0 {
154 return Err(ElError::EmptyTrainingSet);
155 }
156 let n_feat = samples
157 .first()
158 .ok_or(ElError::EmptyTrainingSet)?
159 .features
160 .len();
161 if n_feat == 0 {
162 return Err(ElError::InvalidConfig(
163 "samples must have at least one feature".to_string(),
164 ));
165 }
166 let mut best_mse = f64::MAX;
167 let mut best_feat = 0usize;
168 let mut best_thresh = 0.0f64;
169 let mut best_dir = true;
170 for &feat_idx in feature_subset {
171 let mut vals: Vec<(f64, f64)> = samples
172 .iter()
173 .zip(residuals.iter())
174 .map(|(s, &r)| {
175 let fv = s.features.get(feat_idx).copied().unwrap_or(0.0);
176 (fv, r)
177 })
178 .collect();
179 vals.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
180 for i in 0..vals.len().saturating_sub(1) {
181 let thresh = (vals[i].0 + vals[i + 1].0) / 2.0;
182 for &dir in &[true, false] {
183 let (mut sum_pos, mut cnt_pos) = (0.0f64, 0usize);
184 let (mut sum_neg, mut cnt_neg) = (0.0f64, 0usize);
185 for (fv, r) in &vals {
186 if (dir && *fv <= thresh) || (!dir && *fv > thresh) {
187 sum_pos += r;
188 cnt_pos += 1;
189 } else {
190 sum_neg += r;
191 cnt_neg += 1;
192 }
193 }
194 let mean_pos = if cnt_pos > 0 {
195 sum_pos / cnt_pos as f64
196 } else {
197 0.0
198 };
199 let mean_neg = if cnt_neg > 0 {
200 sum_neg / cnt_neg as f64
201 } else {
202 0.0
203 };
204 let mse: f64 = vals
205 .iter()
206 .map(|(fv, r)| {
207 let pred = if (dir && *fv <= thresh) || (!dir && *fv > thresh) {
208 mean_pos
209 } else {
210 mean_neg
211 };
212 let d = r - pred;
213 d * d
214 })
215 .sum::<f64>();
216 if mse < best_mse {
217 best_mse = mse;
218 best_feat = feat_idx;
219 best_thresh = thresh;
220 best_dir = dir;
221 }
222 }
223 }
224 }
225 let (mut sum_pos, mut cnt_pos) = (0.0f64, 0usize);
226 let (mut sum_neg, mut cnt_neg) = (0.0f64, 0usize);
227 for (s, &r) in samples.iter().zip(residuals.iter()) {
228 let fv = s.features.get(best_feat).copied().unwrap_or(0.0);
229 if (best_dir && fv <= best_thresh) || (!best_dir && fv > best_thresh) {
230 sum_pos += r;
231 cnt_pos += 1;
232 } else {
233 sum_neg += r;
234 cnt_neg += 1;
235 }
236 }
237 let leaf_pos = if cnt_pos > 0 {
238 sum_pos / cnt_pos as f64
239 } else {
240 0.0
241 };
242 let leaf_neg = if cnt_neg > 0 {
243 sum_neg / cnt_neg as f64
244 } else {
245 0.0
246 };
247 Ok((best_feat, best_thresh, best_dir, leaf_pos, leaf_neg))
248}
249pub(super) fn fit_perceptron(
251 samples: &[ElSample],
252 n_features: usize,
253 rng: &mut u64,
254 lr: f64,
255) -> ElBaseModel {
256 let mut weights: Vec<f64> = (0..n_features)
257 .map(|_| (xorshift_f64(rng) - 0.5) * 0.01)
258 .collect();
259 let mut bias = 0.0f64;
260 for s in samples {
261 let score: f64 = s
262 .features
263 .iter()
264 .zip(weights.iter())
265 .map(|(x, w)| x * w)
266 .sum::<f64>()
267 + bias;
268 let label_sign: f64 = if s.label >= 0.0 { 1.0 } else { -1.0 };
269 let pred_sign: f64 = if score >= 0.0 { 1.0 } else { -1.0 };
270 if (pred_sign - label_sign).abs() > 1e-9 {
271 for (w, x) in weights.iter_mut().zip(s.features.iter()) {
272 *w += lr * label_sign * x;
273 }
274 bias += lr * label_sign;
275 }
276 }
277 ElBaseModel::Perceptron {
278 weights,
279 bias,
280 weight: 1.0,
281 }
282}