use faer::MatRef;
use rand::Rng;
use rand::rngs::SmallRng;
use crate::prelude::*;
pub struct QuantisedStore {
pub data: Vec<u8>,
pub n_samples: usize,
pub n_features: usize,
pub feature_min: Vec<f32>,
pub feature_range: Vec<f32>,
}
impl QuantisedStore {
pub fn from_csc(
mat: &CompressedSparseData2<u16, f32>,
n_samples: usize,
) -> Result<Self, ScErrors> {
let n_features = mat.indptr.len() - 1;
let mut data = vec![0u8; n_features * n_samples];
let mut mins = Vec::with_capacity(n_features);
let mut ranges = Vec::with_capacity(n_features);
let vals = mat.data_2.as_ref().ok_or(CoreErrors::Data2NotAvailable)?;
for j in 0..n_features {
let s = mat.indptr[j] as usize;
let e = mat.indptr[j + 1] as usize;
let col_indices = &mat.indices[s..e];
let col_vals = &vals[s..e];
let mut min_v = 0_f32;
let mut max_v = 0_f32;
for &v in col_vals {
if v < min_v {
min_v = v;
}
if v > max_v {
max_v = v;
}
}
let range = max_v - min_v;
mins.push(min_v);
ranges.push(range);
let offset = j * n_samples;
if range > 1e-10 {
let scale = 255.0 / range;
for i in 0..col_indices.len() {
let cell_idx = col_indices[i] as usize;
let val = col_vals[i];
data[offset + cell_idx] = ((val - min_v) * scale).round() as u8;
}
}
}
Ok(Self {
data,
n_samples,
n_features,
feature_min: mins,
feature_range: ranges,
})
}
pub fn from_columns(columns: &[Vec<f32>]) -> Self {
let n_features = columns.len();
assert!(!columns.is_empty(), "need at least one feature");
let n_samples = columns[0].len();
let mut data = vec![0u8; n_features * n_samples];
let mut mins = Vec::with_capacity(n_features);
let mut ranges = Vec::with_capacity(n_features);
for (j, col) in columns.iter().enumerate() {
assert_eq!(col.len(), n_samples);
let mut min_v = f32::INFINITY;
let mut max_v = f32::NEG_INFINITY;
for &v in col {
if v < min_v {
min_v = v;
}
if v > max_v {
max_v = v;
}
}
let range = max_v - min_v;
mins.push(min_v);
ranges.push(range);
let offset = j * n_samples;
if range > 1e-10 {
let scale = 255.0 / range;
for (i, &v) in col.iter().enumerate() {
data[offset + i] = ((v - min_v) * scale).round().min(255.0) as u8;
}
}
}
Self {
data,
n_samples,
n_features,
feature_min: mins,
feature_range: ranges,
}
}
pub fn from_row_major(flat: &[f32], n_samples: usize, n_features: usize) -> Self {
let mut columns: Vec<Vec<f32>> = vec![Vec::with_capacity(n_samples); n_features];
for i in 0..n_samples {
for j in 0..n_features {
columns[j].push(flat[i * n_features + j]);
}
}
Self::from_columns(&columns)
}
pub fn from_mat(features: MatRef<f32>) -> Self {
let n_samples = features.nrows();
let n_features = features.ncols();
let mut data = vec![0u8; n_features * n_samples];
let mut mins = Vec::with_capacity(n_features);
let mut ranges = Vec::with_capacity(n_features);
for j in 0..n_features {
let mut min_v = f32::MAX;
let mut max_v = f32::MIN;
for i in 0..n_samples {
let v = *features.get(i, j);
if v < min_v {
min_v = v;
}
if v > max_v {
max_v = v;
}
}
let range = max_v - min_v;
mins.push(min_v);
ranges.push(range);
let offset = j * n_samples;
if range > 1e-10 {
let scale = 255.0 / range;
for i in 0..n_samples {
let v = *features.get(i, j);
data[offset + i] = ((v - min_v) * scale).round() as u8;
}
}
}
Self {
data,
n_samples,
n_features,
feature_min: mins,
feature_range: ranges,
}
}
pub fn from_raw(data: Vec<u8>, n_samples: usize, n_features: usize) -> Self {
assert_eq!(data.len(), n_samples * n_features);
Self {
data,
n_samples,
n_features,
feature_min: vec![0.0; n_features],
feature_range: vec![0.0; n_features],
}
}
#[inline(always)]
pub fn get_col(&self, feature: usize) -> &[u8] {
let start = feature * self.n_samples;
&self.data[start..start + self.n_samples]
}
}
#[inline]
pub fn node_variance(sum: f32, sum_sq: f32, n: usize) -> f32 {
if n < 2 {
return 0.0;
}
let nf = n as f32;
f32::max(0.0, sum_sq / nf - (sum / nf) * (sum / nf))
}
pub fn train_oob_split(buf: &mut [u32], n_train: usize, rng: &mut SmallRng) -> usize {
let n = buf.len();
let n_train = n_train.min(n);
for i in 0..n_train {
let j = rng.random_range(i..n);
buf.swap(i, j);
}
n_train
}
pub fn init_and_split(buf: &mut [u32], n: usize, n_train: usize, rng: &mut SmallRng) -> usize {
for i in 0..n {
buf[i] = i as u32;
}
train_oob_split(&mut buf[..n], n_train, rng)
}
pub struct PartitionBuffers {
pub train_left: Vec<u32>,
pub train_right: Vec<u32>,
pub oob_left: Vec<u32>,
pub oob_right: Vec<u32>,
}
impl PartitionBuffers {
pub fn new(n_samples: usize) -> Self {
Self {
train_left: vec![0u32; n_samples],
train_right: vec![0u32; n_samples],
oob_left: vec![0u32; n_samples],
oob_right: vec![0u32; n_samples],
}
}
pub fn partition_train(
&mut self,
train_samples: &mut [u32],
tf_col: &[u8],
threshold: u8,
) -> usize {
let mut tl = 0usize;
let mut tr = 0usize;
for &s in train_samples.iter() {
if tf_col[s as usize] <= threshold {
self.train_left[tl] = s;
tl += 1;
} else {
self.train_right[tr] = s;
tr += 1;
}
}
train_samples[..tl].copy_from_slice(&self.train_left[..tl]);
train_samples[tl..tl + tr].copy_from_slice(&self.train_right[..tr]);
tl
}
pub fn partition_oob(
&mut self,
oob_samples: &mut [u32],
tf_col: &[u8],
threshold: u8,
) -> usize {
let mut ol = 0usize;
let mut or_ = 0usize;
for &s in oob_samples.iter() {
if tf_col[s as usize] <= threshold {
self.oob_left[ol] = s;
ol += 1;
} else {
self.oob_right[or_] = s;
or_ += 1;
}
}
oob_samples[..ol].copy_from_slice(&self.oob_left[..ol]);
oob_samples[ol..ol + or_].copy_from_slice(&self.oob_right[..or_]);
ol
}
}
pub struct EarlyStopMonitor {
ring: Vec<f32>,
window: usize,
}
impl EarlyStopMonitor {
pub fn new(window: usize) -> Self {
Self {
ring: Vec::with_capacity(window),
window,
}
}
pub fn push(&mut self, improvement: f32) -> bool {
if self.ring.len() >= self.window {
self.ring.remove(0);
}
self.ring.push(improvement);
if self.ring.len() >= self.window {
let avg: f32 = self.ring.iter().sum::<f32>() / self.ring.len() as f32;
avg <= 0.0
} else {
false
}
}
pub fn reset(&mut self) {
self.ring.clear();
}
}
pub fn normalise_importances(importances: &mut [f32]) {
let total: f32 = importances.iter().sum();
if total > 0.0 {
let inv = 1.0 / total;
importances.iter_mut().for_each(|v| *v *= inv);
}
}
pub fn sample_features(
feat_buf: &mut [usize],
n_features: usize,
k: usize,
rng: &mut SmallRng,
) -> usize {
let k = k.min(n_features);
for i in 0..k {
let j = rng.random_range(i..n_features);
feat_buf.swap(i, j);
}
k
}
pub fn resolve_n_features_split(configured: usize, n_features: usize) -> usize {
if configured == 0 {
((n_features as f32).sqrt() as usize).max(1)
} else {
configured
}
}
#[derive(Clone, Debug)]
pub struct GbmSplitInfo {
pub feature: usize,
pub threshold: u8,
pub score: f32,
pub n_left: u32,
pub y_sum_left: f32,
pub y_sum_sq_left: f32,
}
pub struct NodeHistograms {
pub counts: Vec<u32>,
pub y_sums: Vec<f32>,
pub y_sum_sqs: Vec<f32>,
pub n_features: usize,
}
impl NodeHistograms {
pub fn new(n_features: usize) -> Self {
let n = n_features * 256;
Self {
counts: vec![0u32; n],
y_sums: vec![0.0f32; n],
y_sum_sqs: vec![0.0f32; n],
n_features,
}
}
pub fn reset(&mut self) {
let n = self.n_features * 256;
self.counts[..n].fill(0);
self.y_sums[..n].fill(0.0);
self.y_sum_sqs[..n].fill(0.0);
}
pub fn build_from_samples(&mut self, x: &QuantisedStore, samples: &[u32], residuals: &[f32]) {
let n = self.n_features * 256;
self.counts[..n].fill(0);
self.y_sums[..n].fill(0.0);
self.y_sum_sqs[..n].fill(0.0);
for f in 0..self.n_features {
let col = x.get_col(f);
let base = f * 256;
for &s in samples {
let bin = col[s as usize] as usize;
let idx = base + bin;
self.counts[idx] += 1;
let r = residuals[s as usize];
self.y_sums[idx] += r;
self.y_sum_sqs[idx] += r * r;
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn find_best_split(
&self,
total_sum: f32,
total_sum_sq: f32,
n_samples: u32,
min_samples_leaf: u32,
n_features_split: usize,
feat_buf: &mut [usize],
rng: &mut SmallRng,
) -> Option<GbmSplitInfo> {
let n_features = self.n_features;
let k_feats = if n_features_split == 0 || n_features_split >= n_features {
n_features
} else {
n_features_split
};
sample_features(feat_buf, n_features, k_feats, rng);
let parent_var = node_variance(total_sum, total_sum_sq, n_samples as usize);
if parent_var < 1e-10 {
return None;
}
let nf = n_samples as f32;
let mut best = GbmSplitInfo {
feature: usize::MAX,
threshold: 0,
score: 0.0,
n_left: 0,
y_sum_left: 0.0,
y_sum_sq_left: 0.0,
};
for fi in 0..k_feats {
let feat = feat_buf[fi];
let base = feat * 256;
let mut cum_count = 0u32;
let mut cum_sum = 0.0f32;
let mut cum_sum_sq = 0.0f32;
for bin in 0..255usize {
cum_count += self.counts[base + bin];
cum_sum += self.y_sums[base + bin];
cum_sum_sq += self.y_sum_sqs[base + bin];
let nl = cum_count;
let nr = n_samples - nl;
if nl < min_samples_leaf || nr < min_samples_leaf {
continue;
}
let nlf = nl as f32;
let nrf = nr as f32;
let mean_l = cum_sum / nlf;
let var_l = f32::max(0.0, cum_sum_sq / nlf - mean_l * mean_l);
let sum_r = total_sum - cum_sum;
let sum_sq_r = total_sum_sq - cum_sum_sq;
let mean_r = sum_r / nrf;
let var_r = f32::max(0.0, sum_sq_r / nrf - mean_r * mean_r);
let score = parent_var - (nlf / nf) * var_l - (nrf / nf) * var_r;
if score > best.score {
best.score = score;
best.feature = feat;
best.threshold = bin as u8;
best.n_left = nl;
best.y_sum_left = cum_sum;
best.y_sum_sq_left = cum_sum_sq;
}
}
}
if best.feature == usize::MAX {
None
} else {
Some(best)
}
}
}
pub struct HistogramPool {
pub histograms: Vec<NodeHistograms>,
free: Vec<usize>,
}
impl HistogramPool {
pub fn new(capacity: usize, n_features: usize) -> Self {
let histograms = (0..capacity)
.map(|_| NodeHistograms::new(n_features))
.collect();
let free = (0..capacity).rev().collect();
Self { histograms, free }
}
pub fn acquire(&mut self) -> usize {
self.free.pop().expect("histogram pool exhausted")
}
pub fn release(&mut self, idx: usize) {
self.free.push(idx);
}
pub fn subtract(&mut self, parent: usize, child: usize, out: usize) {
debug_assert_ne!(parent, out);
debug_assert_ne!(child, out);
let n = self.histograms[0].n_features * 256;
let mut out_h = std::mem::replace(&mut self.histograms[out], NodeHistograms::new(0));
let (p, c) = (&self.histograms[parent], &self.histograms[child]);
for ((o, a), b) in out_h.counts[..n]
.iter_mut()
.zip(&p.counts[..n])
.zip(&c.counts[..n])
{
*o = a - b;
}
for ((o, a), b) in out_h.y_sums[..n]
.iter_mut()
.zip(&p.y_sums[..n])
.zip(&c.y_sums[..n])
{
*o = a - b;
}
for ((o, a), b) in out_h.y_sum_sqs[..n]
.iter_mut()
.zip(&p.y_sum_sqs[..n])
.zip(&c.y_sum_sqs[..n])
{
*o = a - b;
}
self.histograms[out] = out_h;
}
}
pub struct GbmScratch {
pub feat_buf: Vec<usize>,
pub partitions: PartitionBuffers,
}
impl GbmScratch {
pub fn new(n_features: usize, n_samples: usize) -> Self {
Self {
feat_buf: (0..n_features).collect(),
partitions: PartitionBuffers::new(n_samples),
}
}
}
pub fn apply_regression_leaf(
residuals: &mut [f32],
train_samples: &[u32],
oob_samples: &[u32],
y_sum_train: f32,
n_train: usize,
learning_rate: f32,
oob_improvement: &mut f32,
) {
if n_train == 0 {
return;
}
let pred = y_sum_train / n_train as f32;
let lr_pred = learning_rate * pred;
let lr_pred_sq = lr_pred * lr_pred;
for &s in oob_samples {
let r = residuals[s as usize];
*oob_improvement += 2.0 * lr_pred * r - lr_pred_sq;
}
for &s in train_samples {
residuals[s as usize] -= lr_pred;
}
for &s in oob_samples {
residuals[s as usize] -= lr_pred;
}
}
#[inline]
pub fn tree_seed(base_seed: usize, tree_idx: usize) -> u64 {
base_seed.wrapping_add(tree_idx.wrapping_mul(6_364_136_223_846_793_005)) as u64
}