pub mod fhe;
use crate::model::{Objective, WeirwoodTree};
pub trait Evaluator {
type Input;
type Output;
fn predict(&self, weirwood_tree: &WeirwoodTree, input: &Self::Input) -> Self::Output;
}
pub struct PlaintextEvaluator;
impl Evaluator for PlaintextEvaluator {
type Input = Vec<f32>;
type Output = f32;
fn predict(&self, weirwood_tree: &WeirwoodTree, features: &Vec<f32>) -> f32 {
let raw_score: f32 = weirwood_tree
.trees
.iter()
.map(|decision_tree| decision_tree.evaluate(features))
.sum();
raw_score + weirwood_tree.base_score
}
}
impl PlaintextEvaluator {
pub fn predict_proba(&self, weirwood_tree: &WeirwoodTree, features: &Vec<f32>) -> f32 {
let raw_score: f32 = self.predict(weirwood_tree, features);
match &weirwood_tree.objective {
Objective::BinaryLogistic => sigmoid(raw_score),
Objective::RegSquaredError => raw_score,
Objective::MultiSoftmax { .. } => sigmoid(raw_score), Objective::Other(_) => raw_score,
}
}
}
fn sigmoid(x: f32) -> f32 {
1.0 / (1.0 + (-x).exp())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{Node, Objective, Tree, WeirwoodTree};
fn tiny_tree() -> WeirwoodTree {
let nodes: Vec<Node> = vec![
Node {
split_feature: 0,
split_threshold: 1.0,
left_child: 1,
right_child: 2,
leaf_value: 0.0,
},
Node {
split_feature: 0,
split_threshold: 0.0,
left_child: -1,
right_child: -1,
leaf_value: -0.5,
},
Node {
split_feature: 0,
split_threshold: 0.0,
left_child: -1,
right_child: -1,
leaf_value: 0.5,
},
];
WeirwoodTree {
trees: vec![Tree { nodes }],
objective: Objective::BinaryLogistic,
base_score: 0.0,
num_features: 1,
}
}
fn deep_tree() -> WeirwoodTree {
let nodes: Vec<Node> = vec![
Node {
split_feature: 0,
split_threshold: 5.0,
left_child: 1,
right_child: 2,
leaf_value: 0.0,
},
Node {
split_feature: 1,
split_threshold: 2.0,
left_child: 3,
right_child: 4,
leaf_value: 0.0,
},
Node {
split_feature: 0,
split_threshold: 0.0,
left_child: -1,
right_child: -1,
leaf_value: 1.0,
},
Node {
split_feature: 0,
split_threshold: 0.0,
left_child: -1,
right_child: -1,
leaf_value: -1.0,
},
Node {
split_feature: 0,
split_threshold: 0.0,
left_child: -1,
right_child: -1,
leaf_value: 0.5,
},
];
WeirwoodTree {
trees: vec![Tree { nodes }],
objective: Objective::BinaryLogistic,
base_score: 0.0,
num_features: 2,
}
}
#[test]
fn plaintext_left_branch() {
let tree: WeirwoodTree = tiny_tree();
let score: f32 = PlaintextEvaluator.predict(&tree, &vec![0.5]);
approx::assert_abs_diff_eq!(score, -0.5, epsilon = 1e-6);
}
#[test]
fn plaintext_right_branch() {
let tree: WeirwoodTree = tiny_tree();
let score: f32 = PlaintextEvaluator.predict(&tree, &vec![2.0]);
approx::assert_abs_diff_eq!(score, 0.5, epsilon = 1e-6);
}
#[test]
fn boundary_at_threshold_goes_left() {
let tree: WeirwoodTree = tiny_tree();
let score: f32 = PlaintextEvaluator.predict(&tree, &vec![1.0]);
approx::assert_abs_diff_eq!(score, -0.5, epsilon = 1e-6);
}
#[test]
fn just_above_threshold_goes_right() {
let tree: WeirwoodTree = tiny_tree();
let score: f32 = PlaintextEvaluator.predict(&tree, &vec![1.0001]);
approx::assert_abs_diff_eq!(score, 0.5, epsilon = 1e-6);
}
#[test]
fn depth2_left_left() {
let tree: WeirwoodTree = deep_tree();
let score: f32 = PlaintextEvaluator.predict(&tree, &vec![1.0, 1.0]);
approx::assert_abs_diff_eq!(score, -1.0, epsilon = 1e-6);
}
#[test]
fn depth2_left_right() {
let tree: WeirwoodTree = deep_tree();
let score: f32 = PlaintextEvaluator.predict(&tree, &vec![1.0, 3.0]);
approx::assert_abs_diff_eq!(score, 0.5, epsilon = 1e-6);
}
#[test]
fn depth2_right() {
let tree: WeirwoodTree = deep_tree();
let score: f32 = PlaintextEvaluator.predict(&tree, &vec![6.0, 99.0]);
approx::assert_abs_diff_eq!(score, 1.0, epsilon = 1e-6);
}
#[test]
fn correct_feature_index_used() {
let nodes: Vec<Node> = vec![
Node {
split_feature: 1,
split_threshold: 0.5,
left_child: 1,
right_child: 2,
leaf_value: 0.0,
},
Node {
split_feature: 0,
split_threshold: 0.0,
left_child: -1,
right_child: -1,
leaf_value: -1.0,
},
Node {
split_feature: 0,
split_threshold: 0.0,
left_child: -1,
right_child: -1,
leaf_value: 1.0,
},
];
let tree: WeirwoodTree = WeirwoodTree {
trees: vec![Tree { nodes }],
objective: Objective::BinaryLogistic,
base_score: 0.0,
num_features: 2,
};
let left_score: f32 = PlaintextEvaluator.predict(&tree, &vec![999.0, 0.0]);
let right_score: f32 = PlaintextEvaluator.predict(&tree, &vec![0.0, 1.0]);
approx::assert_abs_diff_eq!(left_score, -1.0, epsilon = 1e-6);
approx::assert_abs_diff_eq!(right_score, 1.0, epsilon = 1e-6);
}
#[test]
fn two_trees_sum_correctly() {
let make_stump = |left_leaf: f32, right_leaf: f32| Tree {
nodes: vec![
Node {
split_feature: 0,
split_threshold: 1.0,
left_child: 1,
right_child: 2,
leaf_value: 0.0,
},
Node {
split_feature: 0,
split_threshold: 0.0,
left_child: -1,
right_child: -1,
leaf_value: left_leaf,
},
Node {
split_feature: 0,
split_threshold: 0.0,
left_child: -1,
right_child: -1,
leaf_value: right_leaf,
},
],
};
let tree: WeirwoodTree = WeirwoodTree {
trees: vec![make_stump(-0.3, 0.3), make_stump(-0.2, 0.2)],
objective: Objective::BinaryLogistic,
base_score: 0.0,
num_features: 1,
};
approx::assert_abs_diff_eq!(
PlaintextEvaluator.predict(&tree, &vec![0.0]),
-0.5,
epsilon = 1e-6
);
approx::assert_abs_diff_eq!(
PlaintextEvaluator.predict(&tree, &vec![2.0]),
0.5,
epsilon = 1e-6
);
}
#[test]
fn base_score_is_added_to_raw() {
let mut tree: WeirwoodTree = tiny_tree();
tree.base_score = 2.0;
approx::assert_abs_diff_eq!(
PlaintextEvaluator.predict(&tree, &vec![0.5]),
1.5,
epsilon = 1e-6
);
}
#[test]
fn zero_trees_returns_base_score() {
let tree: WeirwoodTree = WeirwoodTree {
trees: vec![],
objective: Objective::BinaryLogistic,
base_score: 0.5,
num_features: 1,
};
approx::assert_abs_diff_eq!(
PlaintextEvaluator.predict(&tree, &vec![1.0]),
0.5,
epsilon = 1e-6
);
}
#[test]
fn sigmoid_sanity() {
let tree: WeirwoodTree = tiny_tree();
let probability: f32 = PlaintextEvaluator.predict_proba(&tree, &vec![2.0]);
assert!(probability > 0.5 && probability < 1.0);
}
#[test]
fn sigmoid_of_zero_is_half() {
let nodes: Vec<Node> = vec![
Node {
split_feature: 0,
split_threshold: 1.0,
left_child: 1,
right_child: 2,
leaf_value: 0.0,
},
Node {
split_feature: 0,
split_threshold: 0.0,
left_child: -1,
right_child: -1,
leaf_value: 0.0,
},
Node {
split_feature: 0,
split_threshold: 0.0,
left_child: -1,
right_child: -1,
leaf_value: 0.0,
},
];
let tree: WeirwoodTree = WeirwoodTree {
trees: vec![Tree { nodes }],
objective: Objective::BinaryLogistic,
base_score: 0.0,
num_features: 1,
};
approx::assert_abs_diff_eq!(
PlaintextEvaluator.predict_proba(&tree, &vec![0.5]),
0.5,
epsilon = 1e-6
);
}
#[test]
fn sigmoid_known_values() {
let tree: WeirwoodTree = tiny_tree();
approx::assert_abs_diff_eq!(
PlaintextEvaluator.predict_proba(&tree, &vec![0.0]), 0.37754066_f32,
epsilon = 1e-5
);
approx::assert_abs_diff_eq!(
PlaintextEvaluator.predict_proba(&tree, &vec![2.0]), 0.62245934_f32,
epsilon = 1e-5
);
}
#[test]
fn regression_predict_proba_is_raw_score() {
let mut tree: WeirwoodTree = tiny_tree();
tree.objective = Objective::RegSquaredError;
tree.base_score = 1.0;
approx::assert_abs_diff_eq!(
PlaintextEvaluator.predict_proba(&tree, &vec![2.0]),
1.5,
epsilon = 1e-6
);
}
#[test]
fn other_objective_predict_proba_is_raw_score() {
let mut tree: WeirwoodTree = tiny_tree();
tree.objective = Objective::Other("custom:loss".into());
approx::assert_abs_diff_eq!(
PlaintextEvaluator.predict_proba(&tree, &vec![0.0]),
-0.5,
epsilon = 1e-6
);
}
}