1use std::collections::{HashMap, HashSet};
7
8use anyhow::{Context, Result};
9use rayon::prelude::*;
10use serde_json::Value;
11
12use crate::infer::TabFMModel;
13
14use super::aggregate::{self, ClassAggMode};
15use super::calibration::{PlattParams, VectorScalingParams};
16use super::cat_encoder::{CategoricalOrdinalEncoder, LabelEncoder};
17use super::config_gen::{self, EnsembleConfigParams, MemberConfig, NormMethod};
18use super::nnls;
19use super::oof;
20use super::scalers::{self, StandardScaler};
21
22const DEFAULT_BATCH_CHUNK_SIZE: usize = 8;
28
29pub struct EnsembleParams {
43 pub(crate) n_estimators: usize,
44 pub(crate) norm_methods: Vec<NormMethod>,
45 pub(crate) class_shift: bool,
46 pub(crate) outlier_threshold: f64,
47 pub(crate) softmax_temperature: f64,
48 pub(crate) average_logits: bool,
49 pub(crate) random_state: u64,
50 pub(crate) binary_calibration: bool, pub(crate) multiclass_calibration: bool, pub(crate) num_folds_for_cv: usize,
53 pub(crate) enable_nnls: bool,
54 pub(crate) nnls_beta: f64,
55 pub(crate) calibration_lambda: f64,
56 pub(crate) batch_size: Option<usize>,
63}
64
65impl Default for EnsembleParams {
66 fn default() -> Self {
67 EnsembleParams {
68 n_estimators: 32,
69 norm_methods: vec![NormMethod::None, NormMethod::Power],
70 class_shift: true,
71 outlier_threshold: 4.0,
72 softmax_temperature: 0.9,
73 average_logits: true,
74 random_state: 42,
75 binary_calibration: false,
76 multiclass_calibration: false,
77 num_folds_for_cv: 5,
78 enable_nnls: false,
79 nnls_beta: 0.75,
80 calibration_lambda: 1e-2,
81 batch_size: None,
82 }
83 }
84}
85
86impl EnsembleParams {
87 pub fn with_n_estimators(mut self, v: usize) -> Self { self.n_estimators = v; self }
88 pub fn with_norm_methods(mut self, v: Vec<NormMethod>) -> Self { self.norm_methods = v; self }
89 pub fn with_class_shift(mut self, v: bool) -> Self { self.class_shift = v; self }
90 pub fn with_outlier_threshold(mut self, v: f64) -> Self { self.outlier_threshold = v; self }
91 pub fn with_softmax_temperature(mut self, v: f64) -> Self { self.softmax_temperature = v; self }
92 pub fn with_average_logits(mut self, v: bool) -> Self { self.average_logits = v; self }
93 pub fn with_random_state(mut self, v: u64) -> Self { self.random_state = v; self }
94 pub fn with_binary_calibration(mut self, v: bool) -> Self { self.binary_calibration = v; self }
96 pub fn with_multiclass_calibration(mut self, v: bool) -> Self { self.multiclass_calibration = v; self }
98 pub fn with_num_folds_for_cv(mut self, v: usize) -> Self { self.num_folds_for_cv = v; self }
99 pub fn with_enable_nnls(mut self, v: bool) -> Self { self.enable_nnls = v; self }
100 pub fn with_nnls_beta(mut self, v: f64) -> Self { self.nnls_beta = v; self }
101 pub fn with_calibration_lambda(mut self, v: f64) -> Self { self.calibration_lambda = v; self }
102 pub fn with_batch_size(mut self, v: Option<usize>) -> Self { self.batch_size = v; self }
103}
104
105pub struct ClassificationOutput {
106 pub probabilities: Vec<Vec<f64>>, pub predicted_labels: Vec<String>,
108 pub classes: Vec<String>,
109}
110
111pub struct RegressionOutput {
112 pub predictions: Vec<f64>, }
114
115struct MemberTable {
117 x: Vec<Vec<f32>>, cat_mask: Vec<bool>,
119}
120
121struct ColumnCache {
130 scaled: HashMap<(usize, NormMethod), (Vec<f64>, Vec<f64>)>,
133}
134
135impl ColumnCache {
136 fn build(
137 x_train_raw: &[Vec<Value>],
138 x_query_raw: &[Vec<Value>],
139 cat_mask: &[bool],
140 configs: &[MemberConfig],
141 outlier_threshold: f64,
142 ) -> Self {
143 let n_features = x_train_raw.first().map(|r| r.len()).unwrap_or(0);
144
145 let mut raw_train: Vec<Vec<f64>> = Vec::with_capacity(n_features);
146 let mut raw_query: Vec<Vec<f64>> = Vec::with_capacity(n_features);
147 for col in 0..n_features {
148 let train_raw: Vec<Value> = x_train_raw.iter().map(|r| r[col].clone()).collect();
149 let query_raw: Vec<Value> = x_query_raw.iter().map(|r| r[col].clone()).collect();
150 let (t, q) = if cat_mask[col] {
151 let enc = CategoricalOrdinalEncoder::fit(&train_raw);
152 (enc.transform(&train_raw), enc.transform(&query_raw))
153 } else {
154 let parse = |v: &Value| v.as_f64().unwrap_or(f64::NAN);
155 (train_raw.iter().map(parse).collect(), query_raw.iter().map(parse).collect())
156 };
157 raw_train.push(t);
158 raw_query.push(q);
159 }
160
161 let distinct_norm_methods: HashSet<NormMethod> = configs.iter().map(|c| c.norm_method).collect();
162
163 let mut scaled = HashMap::with_capacity(n_features * distinct_norm_methods.len());
164 for col in 0..n_features {
165 for &nm in &distinct_norm_methods {
166 let transformed = scalers::apply_pipeline(&raw_train[col], &raw_query[col], nm, outlier_threshold);
167 scaled.insert((col, nm), transformed);
168 }
169 }
170
171 ColumnCache { scaled }
172 }
173
174 fn scaled(&self, col: usize, norm_method: NormMethod) -> (&[f64], &[f64]) {
175 let (t, q) = self.scaled.get(&(col, norm_method)).expect("precomputed for every config's norm_method");
176 (t, q)
177 }
178}
179
180fn build_member_table(
183 cache: &ColumnCache,
184 n_train: usize,
185 n_query: usize,
186 cat_mask: &[bool],
187 member: &MemberConfig,
188) -> MemberTable {
189 let h = member.feature_permutation.len();
190 let mut x = vec![vec![0f32; h]; n_train + n_query];
191 let mut permuted_cat_mask: Vec<bool> = Vec::with_capacity(h);
192
193 for (pos, &src_col) in member.feature_permutation.iter().enumerate() {
194 permuted_cat_mask.push(cat_mask[src_col]);
195 let (train_scaled, query_scaled) = cache.scaled(src_col, member.norm_method);
196 for (row_idx, &v) in train_scaled.iter().enumerate() {
197 x[row_idx][pos] = v as f32;
198 }
199 for (row_idx, &v) in query_scaled.iter().enumerate() {
200 x[n_train + row_idx][pos] = v as f32;
201 }
202 }
203
204 MemberTable { x, cat_mask: permuted_cat_mask }
205}
206
207#[allow(clippy::too_many_arguments)]
215pub fn run_members_classification(
216 model: &TabFMModel,
217 x_train_raw: &[Vec<Value>],
218 y_train_codes: &[f64],
219 x_query_raw: &[Vec<Value>],
220 cat_mask: &[bool],
221 n_classes: usize,
222 configs: &[MemberConfig],
223 outlier_threshold: f64,
224 batch_size: Option<usize>,
225) -> Result<Vec<Vec<Vec<f64>>>> {
226 let n_train = x_train_raw.len();
227 let n_query = x_query_raw.len();
228 let n_features = x_train_raw.first().map(|r| r.len()).unwrap_or(0);
229
230 let cache = ColumnCache::build(x_train_raw, x_query_raw, cat_mask, configs, outlier_threshold);
231 let tables: Vec<MemberTable> =
232 configs.iter().map(|member| build_member_table(&cache, n_train, n_query, cat_mask, member)).collect();
233
234 let chunk_size = batch_size.unwrap_or(DEFAULT_BATCH_CHUNK_SIZE).max(1);
235 let num_chunks = configs.len().div_ceil(chunk_size);
236
237 let chunks: Result<Vec<Vec<Vec<Vec<f64>>>>> = (0..num_chunks)
238 .into_par_iter()
239 .map(|chunk_idx| -> Result<Vec<Vec<Vec<f64>>>> {
240 let start = chunk_idx * chunk_size;
241 let end = (start + chunk_size).min(configs.len());
242 let member_chunk = &configs[start..end];
243 let table_chunk = &tables[start..end];
244
245 let x_batch: Vec<Vec<Vec<f32>>> = table_chunk.iter().map(|t| t.x.clone()).collect();
246 let cat_mask_batch: Vec<Vec<bool>> = table_chunk.iter().map(|t| t.cat_mask.clone()).collect();
247 let y_batch: Vec<Vec<f32>> = member_chunk
248 .iter()
249 .map(|member| {
250 y_train_codes
251 .iter()
252 .map(|&c| ((c as i64 + member.class_shift as i64).rem_euclid(n_classes as i64)) as f32)
253 .chain(std::iter::repeat(0f32).take(n_query))
254 .collect()
255 })
256 .collect();
257
258 let out_batch = model
259 .predict_batch(&x_batch, &y_batch, n_train, &cat_mask_batch, Some(n_features))
260 .context("member batch forward pass")?;
261
262 Ok(member_chunk
263 .iter()
264 .zip(out_batch.iter())
265 .map(|(member, out)| {
266 out[n_train..]
267 .iter()
268 .map(|row| {
269 let row64: Vec<f64> = row.iter().map(|&v| v as f64).collect();
270 aggregate::unshift_logits(&row64, member.class_shift, n_classes)
271 })
272 .collect()
273 })
274 .collect())
275 })
276 .collect();
277
278 Ok(chunks?.into_iter().flatten().collect())
279}
280
281pub fn run_members_regression(
284 model: &TabFMModel,
285 x_train_raw: &[Vec<Value>],
286 y_train_scaled: &[f64],
287 x_query_raw: &[Vec<Value>],
288 cat_mask: &[bool],
289 configs: &[MemberConfig],
290 outlier_threshold: f64,
291 batch_size: Option<usize>,
292) -> Result<Vec<Vec<f64>>> {
293 let n_train = x_train_raw.len();
294 let n_query = x_query_raw.len();
295 let n_features = x_train_raw.first().map(|r| r.len()).unwrap_or(0);
296
297 let cache = ColumnCache::build(x_train_raw, x_query_raw, cat_mask, configs, outlier_threshold);
298 let tables: Vec<MemberTable> =
299 configs.iter().map(|member| build_member_table(&cache, n_train, n_query, cat_mask, member)).collect();
300
301 let chunk_size = batch_size.unwrap_or(DEFAULT_BATCH_CHUNK_SIZE).max(1);
302 let num_chunks = configs.len().div_ceil(chunk_size);
303
304 let chunks: Result<Vec<Vec<Vec<f64>>>> = (0..num_chunks)
305 .into_par_iter()
306 .map(|chunk_idx| -> Result<Vec<Vec<f64>>> {
307 let start = chunk_idx * chunk_size;
308 let end = (start + chunk_size).min(configs.len());
309 let table_chunk = &tables[start..end];
310
311 let x_batch: Vec<Vec<Vec<f32>>> = table_chunk.iter().map(|t| t.x.clone()).collect();
312 let cat_mask_batch: Vec<Vec<bool>> = table_chunk.iter().map(|t| t.cat_mask.clone()).collect();
313 let y_batch: Vec<Vec<f32>> = (0..table_chunk.len())
314 .map(|_| y_train_scaled.iter().map(|&v| v as f32).chain(std::iter::repeat(0f32).take(n_query)).collect())
315 .collect();
316
317 let out_batch = model
318 .predict_batch(&x_batch, &y_batch, n_train, &cat_mask_batch, Some(n_features))
319 .context("member batch forward pass")?;
320
321 Ok(out_batch.iter().map(|out| out[n_train..].iter().map(|row| row[0] as f64).collect()).collect())
322 })
323 .collect();
324
325 Ok(chunks?.into_iter().flatten().collect())
326}
327
328fn class_agg_mode<'a>(p: &EnsembleParams, nnls_weights: &'a Option<Vec<f64>>) -> ClassAggMode<'a> {
329 match nnls_weights {
330 Some(w) => ClassAggMode::NnlsWeighted(w),
331 None if p.average_logits => ClassAggMode::AverageLogits,
332 None => ClassAggMode::AverageProbs,
333 }
334}
335
336pub fn run_classification(
337 model: &TabFMModel,
338 x_train_raw: &[Vec<Value>],
339 y_train_raw: &[Value],
340 x_test_raw: &[Vec<Value>],
341 cat_mask: &[bool],
342 p: &EnsembleParams,
343) -> Result<ClassificationOutput> {
344 let n_train = x_train_raw.len();
345 let n_test = x_test_raw.len();
346 let n_features = x_train_raw.first().map(|r| r.len()).unwrap_or(0);
347
348 let label_enc = LabelEncoder::fit(y_train_raw);
349 let n_classes = label_enc.n_classes();
350 let y_codes = label_enc.transform(y_train_raw);
351
352 let configs = config_gen::generate_ensemble(&EnsembleConfigParams {
353 n_estimators: p.n_estimators,
354 n_features,
355 n_train_rows: n_train,
356 is_classification: true,
357 n_classes,
358 class_shift: p.class_shift,
359 permute_categorical: false,
360 cat_value_counts: vec![],
361 max_num_rows: None,
362 norm_methods: p.norm_methods.clone(),
363 random_state: p.random_state,
364 });
365
366 let per_member_logits = run_members_classification(
367 model, x_train_raw, &y_codes, x_test_raw, cat_mask, n_classes, &configs, p.outlier_threshold, p.batch_size,
368 )?;
369
370 let binary_or_multiclass_calibration = if n_classes == 2 { p.binary_calibration } else { p.multiclass_calibration };
371
372 let oof_logits = if p.enable_nnls || binary_or_multiclass_calibration {
376 Some(oof::run_oof_classification(
377 model, x_train_raw, &y_codes, cat_mask, n_classes, &configs, p, p.num_folds_for_cv,
378 )?)
379 } else {
380 None
381 };
382
383 let nnls_weights = if p.enable_nnls {
384 let oof_logits = oof_logits.as_ref().expect("computed above when enable_nnls");
385 let oof_probs: Vec<Vec<Vec<f64>>> = oof_logits
387 .iter()
388 .map(|m| m.iter().map(|l| aggregate::softmax_temperature(l, p.softmax_temperature)).collect())
389 .collect();
390 let n_est = oof_probs.len();
391 let n_tr = oof_probs[0].len();
392 let mut design: Vec<Vec<f64>> = vec![vec![0.0; n_tr * n_classes]; n_est];
393 let mut target = vec![0.0; n_tr * n_classes];
394 for (r, &yc) in y_codes.iter().enumerate() {
395 target[r * n_classes + yc as usize] = 1.0;
396 }
397 for (m, member_probs) in oof_probs.iter().enumerate() {
398 for (r, probs) in member_probs.iter().enumerate() {
399 for c in 0..n_classes {
400 design[m][r * n_classes + c] = probs[c];
401 }
402 }
403 }
404 let raw_weights = nnls::nnls(&design, &target);
405 Some(nnls::finalize_weights(&raw_weights, p.nnls_beta))
406 } else {
407 None
408 };
409
410 let mode = class_agg_mode(p, &nnls_weights);
411 let mut probabilities = Vec::with_capacity(n_test);
412 for row_idx in 0..n_test {
413 let logits_all: Vec<Vec<f64>> = per_member_logits.iter().map(|m| m[row_idx].clone()).collect();
414 probabilities.push(aggregate::aggregate_classification(&logits_all, p.softmax_temperature, &mode));
415 }
416
417 if binary_or_multiclass_calibration {
418 let oof_logits = oof_logits.as_ref().expect("computed above when calibration enabled");
422 let n_tr = oof_logits[0].len();
423 let mut oof_final_probs = Vec::with_capacity(n_tr);
424 for row_idx in 0..n_tr {
425 let logits_all: Vec<Vec<f64>> = oof_logits.iter().map(|m| m[row_idx].clone()).collect();
426 oof_final_probs.push(aggregate::aggregate_classification(&logits_all, p.softmax_temperature, &mode));
427 }
428 let y_codes_usize: Vec<usize> = y_codes.iter().map(|&c| c as usize).collect();
429
430 if n_classes == 2 {
431 let params = PlattParams::fit(&oof_final_probs, &y_codes_usize, p.calibration_lambda);
432 probabilities = probabilities.iter().map(|p| params.apply(p)).collect();
433 } else {
434 let params = VectorScalingParams::fit(&oof_final_probs, &y_codes_usize, p.calibration_lambda);
435 probabilities = probabilities.iter().map(|p| params.apply(p)).collect();
436 }
437 }
438
439 let mut predicted_labels = Vec::with_capacity(n_test);
440 for probs in &probabilities {
441 let (best_idx, _) =
442 probs.iter().enumerate().fold((0, f64::NEG_INFINITY), |acc, (i, &v)| if v > acc.1 { (i, v) } else { acc });
443 predicted_labels.push(label_enc.decode(best_idx).to_string());
444 }
445
446 let classes: Vec<String> = (0..n_classes).map(|c| label_enc.decode(c).to_string()).collect();
447 Ok(ClassificationOutput { probabilities, predicted_labels, classes })
448}
449
450pub fn run_regression(
451 model: &TabFMModel,
452 x_train_raw: &[Vec<Value>],
453 y_train_raw: &[f64],
454 x_test_raw: &[Vec<Value>],
455 cat_mask: &[bool],
456 p: &EnsembleParams,
457) -> Result<RegressionOutput> {
458 let n_train = x_train_raw.len();
459 let n_features = x_train_raw.first().map(|r| r.len()).unwrap_or(0);
460
461 let y_scaler = StandardScaler::fit(y_train_raw);
462 let y_scaled = y_scaler.transform(y_train_raw);
463
464 let configs = config_gen::generate_ensemble(&EnsembleConfigParams {
465 n_estimators: p.n_estimators,
466 n_features,
467 n_train_rows: n_train,
468 is_classification: false,
469 n_classes: 1,
470 class_shift: false,
471 permute_categorical: false,
472 cat_value_counts: vec![],
473 max_num_rows: None,
474 norm_methods: p.norm_methods.clone(),
475 random_state: p.random_state,
476 });
477
478 let per_member_scaled_preds = run_members_regression(
479 model, x_train_raw, &y_scaled, x_test_raw, cat_mask, &configs, p.outlier_threshold, p.batch_size,
480 )?;
481
482 let nnls_weights = if p.enable_nnls {
483 let oof_scaled = oof::run_oof_regression(model, x_train_raw, &y_scaled, cat_mask, &configs, p, p.num_folds_for_cv)?;
484 let n_est = oof_scaled.len();
485 let n_tr = oof_scaled[0].len();
486 let mut design: Vec<Vec<f64>> = vec![vec![0.0; n_tr]; n_est];
488 for m in 0..n_est {
489 for r in 0..n_tr {
490 design[m][r] = y_scaler.inverse_transform_scalar(oof_scaled[m][r]);
491 }
492 }
493 let raw_weights = nnls::nnls(&design, y_train_raw);
494 Some(nnls::finalize_weights(&raw_weights, p.nnls_beta))
495 } else {
496 None
497 };
498
499 let n_test = x_test_raw.len();
500 let mut predictions = Vec::with_capacity(n_test);
501 for row_idx in 0..n_test {
502 let pred = match &nnls_weights {
503 Some(weights) => {
504 let unscaled: Vec<f64> = per_member_scaled_preds
505 .iter()
506 .map(|m| y_scaler.inverse_transform_scalar(m[row_idx]))
507 .collect();
508 aggregate::weighted_unscaled_predictions(&unscaled, weights)
509 }
510 None => {
511 let scaled_across_members: Vec<f64> = per_member_scaled_preds.iter().map(|m| m[row_idx]).collect();
512 let avg_scaled = aggregate::average_scaled_predictions(&scaled_across_members);
513 y_scaler.inverse_transform_scalar(avg_scaled)
514 }
515 };
516 predictions.push(pred);
517 }
518
519 Ok(RegressionOutput { predictions })
520}