Skip to main content

ipfrs_tensorlogic/ensemble_learner/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use 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/// Draw a uniform f64 in [0, 1) from the PRNG state.
17#[inline]
18pub(super) fn xorshift_f64(state: &mut u64) -> f64 {
19    (xorshift64(state) >> 11) as f64 / (1u64 << 53) as f64
20}
21/// Draw a usize in [0, n) from the PRNG state.
22#[inline]
23pub(super) fn xorshift_usize(state: &mut u64, n: usize) -> usize {
24    (xorshift64(state) as usize).wrapping_rem(n)
25}
26/// Bootstrap sample of `n` indices from `[0, pool_size)` with replacement.
27pub(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/// Weighted bootstrap: draw `n` indices according to `weights`.
31#[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}
48/// Find the best decision stump for a set of (weighted) samples, restricted to
49/// a candidate feature subset.
50///
51/// Returns `(feature_index, threshold, direction, weighted_error)`.
52pub(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}
142/// Fit a decision stump to continuous residuals (for gradient boosting).
143/// Returns `(feature_index, threshold, direction, leaf_pos, leaf_neg)`.
144///
145/// `leaf_pos` is the mean residual for samples where the stump predicts +1,
146/// `leaf_neg` is the mean residual for samples where the stump predicts -1.
147pub(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}
249/// Fit a simple perceptron (one gradient descent pass per sample).
250pub(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}