use crate::backend::{BinSplit, DeviceNode, DeviceTree};
use crate::data::{BinMapper, BinnedDataset, FeatureMeta};
use crate::error::{Result, SilvicError};
use crate::tree::{in_bitset, MissingType, Tree, CATEGORICAL_MASK, DEFAULT_LEFT_MASK};
pub(crate) fn device_tree(tree: &Tree, data: &BinnedDataset, bias: f64) -> Result<DeviceTree> {
let nodes = (0..tree.num_leaves.saturating_sub(1))
.map(|node| {
Ok(DeviceNode {
split: node_split(tree, node, data)?,
left: tree.left_child[node],
right: tree.right_child[node],
})
})
.collect::<Result<Vec<_>>>()?;
Ok(DeviceTree {
nodes,
leaf_values: leaf_values(tree, bias),
})
}
pub(crate) fn leaf_values(tree: &Tree, bias: f64) -> Vec<f32> {
tree.leaf_value.iter().map(|&v| (v - bias) as f32).collect()
}
fn node_split(tree: &Tree, node: usize, data: &BinnedDataset) -> Result<BinSplit> {
let feature = usize::try_from(tree.split_feature[node])
.ok()
.filter(|&f| f < data.features.len())
.ok_or_else(|| {
SilvicError::config(format!(
"init_model splits on feature {}, the training data has {}",
tree.split_feature[node],
data.features.len()
))
})?;
let meta = &data.features[feature];
let decision_type = tree.decision_type[node];
let left_bins = match &meta.bins {
BinMapper::Numerical {
upper_bounds, nan_bin, ..
} if decision_type & CATEGORICAL_MASK == 0 => {
numerical_bins(meta, upper_bounds, *nan_bin, tree.threshold[node], decision_type)
}
BinMapper::Categorical { categories } if decision_type & CATEGORICAL_MASK != 0 => {
categorical_bins(tree, node, categories)?
}
BinMapper::Trivial => {
if data.groups.is_empty() {
return Err(SilvicError::config(
"init_model splits on features the training data has none of",
));
}
return Ok(BinSplit {
group: 0,
column: 0,
left_bins: [0; 8],
});
}
_ => {
return Err(SilvicError::config(format!(
"init_model splits feature {feature} as {}, the training data bins it as {}",
if decision_type & CATEGORICAL_MASK != 0 {
"categorical"
} else {
"numerical"
},
if meta.is_categorical() {
"categorical"
} else {
"numerical"
}
)))
}
};
Ok(BinSplit {
group: meta.group,
column: meta.column,
left_bins,
})
}
fn set(bits: &mut [u32; 8], bin: usize, left: bool) {
let mask = 1 << (bin % 32);
if left {
bits[bin / 32] |= mask;
} else {
bits[bin / 32] &= !mask;
}
}
fn numerical_bins(
meta: &FeatureMeta,
upper_bounds: &[f64],
nan_bin: bool,
threshold: f64,
decision_type: i8,
) -> [u32; 8] {
let default_left = decision_type & DEFAULT_LEFT_MASK != 0;
let mut bits = [0u32; 8];
for (bin, &bound) in upper_bounds.iter().enumerate() {
set(&mut bits, bin, bound.min(f64::MAX) <= threshold);
}
let nan = upper_bounds.len();
match MissingType::from_decision_type(decision_type) {
MissingType::NaN => {
if nan_bin {
set(&mut bits, nan, default_left);
}
}
MissingType::Zero => {
set(&mut bits, usize::from(meta.bins.value_to_bin(0.0)), default_left);
if nan_bin {
set(&mut bits, nan, default_left);
}
}
MissingType::None => {
if nan_bin {
set(&mut bits, nan, 0.0 <= threshold);
}
}
}
bits
}
fn categorical_bins(tree: &Tree, node: usize, categories: &[i32]) -> Result<[u32; 8]> {
let index = tree.threshold[node] as usize;
let malformed = || SilvicError::config(format!("init_model node {node} has no category bitset"));
let begin = *tree.cat_boundaries.get(index).ok_or_else(malformed)? as usize;
let end = *tree.cat_boundaries.get(index + 1).ok_or_else(malformed)? as usize;
let words = tree.cat_threshold.get(begin..end).ok_or_else(malformed)?;
let mut bits = [0u32; 8];
for (b, &category) in categories.iter().enumerate() {
set(&mut bits, b + 1, in_bitset(words, category));
}
Ok(bits)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data::FeatureGroup;
use crate::tree::SplitChildren;
fn dataset() -> BinnedDataset {
let meta = |name: &str, bins: BinMapper, num_bins: u32, column: u32, hist_offset: u32| FeatureMeta {
name: name.into(),
bins,
num_bins,
group: 0,
column,
hist_offset,
};
let numerical = BinMapper::Numerical {
upper_bounds: vec![1.5, 2.5, 3.5, f64::INFINITY],
nan_bin: true,
min: 0.0,
max: 5.0,
};
let categorical = BinMapper::Categorical {
categories: vec![7, 3, 9],
};
BinnedDataset {
num_rows: 0,
row_offset: 0,
features: vec![meta("n", numerical, 5, 0, 0), meta("c", categorical, 4, 1, 5)],
groups: vec![FeatureGroup {
features: vec![0, 1],
row_stride: 4,
hist_offset: 0,
total_bins: 9,
data: Vec::new(),
}],
total_bins: 9,
label: Vec::new(),
weight: None,
query_boundaries: None,
}
}
fn left(split: &BinSplit, bins: u8) -> Vec<u8> {
(0..bins).filter(|&b| split.goes_left(b)).collect()
}
const CHILDREN: SplitChildren = SplitChildren {
left_value: 1.0,
right_value: 2.0,
left_count: 1,
right_count: 1,
left_weight: 1.0,
right_weight: 1.0,
gain: 1.0,
};
#[test]
fn numerical_thresholds_map_to_the_bins_below_them() {
let data = dataset();
let cases = [
(2.5, true, MissingType::NaN, vec![0, 1, 4]),
(2.5, false, MissingType::NaN, vec![0, 1]),
(f64::MAX, false, MissingType::NaN, vec![0, 1, 2, 3]),
(1.5, false, MissingType::None, vec![0, 4]),
(-1.0, false, MissingType::None, vec![]),
(2.0, false, MissingType::NaN, vec![0]),
];
for (threshold, default_left, missing, expected) in cases {
let mut tree = Tree::new();
tree.split_numerical(0, 0, threshold, default_left, missing, &CHILDREN);
let device = device_tree(&tree, &data, 0.0).expect("device tree");
assert_eq!(
left(&device.nodes[0].split, 5),
expected,
"threshold {threshold}, default_left {default_left}, {missing:?}"
);
}
}
#[test]
fn categorical_bitsets_map_to_category_bins() {
let data = dataset();
let mut tree = Tree::new();
tree.split_categorical(0, 1, &[9, 7, 4], MissingType::NaN, &CHILDREN);
let device = device_tree(&tree, &data, 0.5).expect("device tree");
assert_eq!(left(&device.nodes[0].split, 4), vec![1, 3]);
assert_eq!(device.leaf_values, vec![0.5, 1.5]);
let mut wrong = Tree::new();
wrong.split_numerical(0, 1, 1.0, false, MissingType::None, &CHILDREN);
assert_eq!(device_tree(&wrong, &data, 0.0).expect_err("kind").kind(), "config");
}
}