1use crate::error::{StatsError, StatsResult};
13use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2, ScalarOperand};
14use scirs2_core::numeric::{Float, NumAssign, NumCast, One, Zero};
15use scirs2_core::{simd_ops::SimdUnifiedOps, validation::*};
16use std::collections::HashMap;
17use std::marker::PhantomData;
18
19mod bnn_train;
20mod diagnostics;
21mod glm;
22mod model_fit;
23
24pub use bnn_train::BnnTrainingConfig;
25
26pub trait AdvancedBayesianFloat:
37 Float
38 + NumCast
39 + NumAssign
40 + SimdUnifiedOps
41 + Zero
42 + One
43 + PartialOrd
44 + Copy
45 + Send
46 + Sync
47 + std::fmt::Display
48 + std::iter::Sum<Self>
49 + ScalarOperand
50 + 'static
51{
52}
53
54impl<T> AdvancedBayesianFloat for T where
55 T: Float
56 + NumCast
57 + NumAssign
58 + SimdUnifiedOps
59 + Zero
60 + One
61 + PartialOrd
62 + Copy
63 + Send
64 + Sync
65 + std::fmt::Display
66 + std::iter::Sum<T>
67 + ScalarOperand
68 + 'static
69{
70}
71
72#[derive(Debug, Clone)]
74pub struct BayesianModelComparison<F> {
75 pub models: Vec<BayesianModel<F>>,
77 pub criteria: Vec<ModelSelectionCriterion>,
79 pub cv_config: CrossValidationConfig,
81 pub parallel_config: ParallelConfig,
83}
84
85#[derive(Debug, Clone)]
87pub struct BayesianModel<F> {
88 pub id: String,
90 pub model_type: ModelType,
92 pub prior: AdvancedPrior<F>,
94 pub likelihood: LikelihoodType,
96 pub complexity: f64,
98}
99
100#[derive(Debug, Clone)]
102pub enum AdvancedPrior<F> {
103 Conjugate { parameters: HashMap<String, F> },
105 Hierarchical { levels: Vec<PriorLevel<F>> },
107 Mixture {
109 components: Vec<PriorComponent<F>>,
110 weights: Array1<F>,
111 },
112 Sparse {
114 sparsity_type: SparsityType,
115 sparsity_params: HashMap<String, F>,
116 },
117 NonParametric {
119 process_type: NonParametricProcess,
120 concentration: F,
121 },
122}
123
124#[derive(Debug, Clone)]
126pub struct PriorLevel<F> {
127 pub level_id: String,
129 pub distribution: DistributionType<F>,
131 pub dependencies: Vec<String>,
133}
134
135#[derive(Debug, Clone)]
137pub struct PriorComponent<F> {
138 pub weight: F,
140 pub distribution: DistributionType<F>,
142}
143
144pub enum DistributionType<F> {
146 Normal {
147 mean: F,
148 precision: F,
149 },
150 Gamma {
151 shape: F,
152 rate: F,
153 },
154 Beta {
155 alpha: F,
156 beta: F,
157 },
158 InverseGamma {
159 shape: F,
160 scale: F,
161 },
162 Exponential {
163 rate: F,
164 },
165 Uniform {
166 lower: F,
167 upper: F,
168 },
169 StudentT {
170 degrees_freedom: F,
171 location: F,
172 scale: F,
173 },
174 Laplace {
175 location: F,
176 scale: F,
177 },
178 Horseshoe {
179 tau: F,
180 },
181 Custom {
182 log_density: Box<dyn Fn(F) -> F + Send + Sync>,
183 parameters: HashMap<String, F>,
184 },
185}
186
187impl<F: std::fmt::Debug> std::fmt::Debug for DistributionType<F> {
188 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 match self {
190 DistributionType::Normal { mean, precision } => f
191 .debug_struct("Normal")
192 .field("mean", mean)
193 .field("precision", precision)
194 .finish(),
195 DistributionType::Gamma { shape, rate } => f
196 .debug_struct("Gamma")
197 .field("shape", shape)
198 .field("rate", rate)
199 .finish(),
200 DistributionType::Beta { alpha, beta } => f
201 .debug_struct("Beta")
202 .field("alpha", alpha)
203 .field("beta", beta)
204 .finish(),
205 DistributionType::Uniform { lower, upper } => f
206 .debug_struct("Uniform")
207 .field("lower", lower)
208 .field("upper", upper)
209 .finish(),
210 DistributionType::InverseGamma { shape, scale } => f
211 .debug_struct("InverseGamma")
212 .field("shape", shape)
213 .field("scale", scale)
214 .finish(),
215 DistributionType::StudentT {
216 degrees_freedom,
217 location,
218 scale,
219 } => f
220 .debug_struct("StudentT")
221 .field("degrees_freedom", degrees_freedom)
222 .field("location", location)
223 .field("scale", scale)
224 .finish(),
225 DistributionType::Exponential { rate } => {
226 f.debug_struct("Exponential").field("rate", rate).finish()
227 }
228 DistributionType::Laplace { location, scale } => f
229 .debug_struct("Laplace")
230 .field("location", location)
231 .field("scale", scale)
232 .finish(),
233 DistributionType::Horseshoe { tau } => {
234 f.debug_struct("Horseshoe").field("tau", tau).finish()
235 }
236 DistributionType::Custom { parameters, .. } => f
237 .debug_struct("Custom")
238 .field("parameters", parameters)
239 .field("log_density", &"<function>")
240 .finish(),
241 }
242 }
243}
244
245impl<F: Clone> Clone for DistributionType<F> {
246 fn clone(&self) -> Self {
247 match self {
248 DistributionType::Normal { mean, precision } => DistributionType::Normal {
249 mean: mean.clone(),
250 precision: precision.clone(),
251 },
252 DistributionType::Gamma { shape, rate } => DistributionType::Gamma {
253 shape: shape.clone(),
254 rate: rate.clone(),
255 },
256 DistributionType::Beta { alpha, beta } => DistributionType::Beta {
257 alpha: alpha.clone(),
258 beta: beta.clone(),
259 },
260 DistributionType::Uniform { lower, upper } => DistributionType::Uniform {
261 lower: lower.clone(),
262 upper: upper.clone(),
263 },
264 DistributionType::InverseGamma { shape, scale } => DistributionType::InverseGamma {
265 shape: shape.clone(),
266 scale: scale.clone(),
267 },
268 DistributionType::StudentT {
269 degrees_freedom,
270 location,
271 scale,
272 } => DistributionType::StudentT {
273 degrees_freedom: degrees_freedom.clone(),
274 location: location.clone(),
275 scale: scale.clone(),
276 },
277 DistributionType::Exponential { rate } => {
278 DistributionType::Exponential { rate: rate.clone() }
279 }
280 DistributionType::Horseshoe { tau } => DistributionType::Horseshoe { tau: tau.clone() },
281 DistributionType::Laplace { location, scale } => DistributionType::Laplace {
282 location: location.clone(),
283 scale: scale.clone(),
284 },
285 DistributionType::Custom { parameters: _, .. } => {
286 panic!("Cannot clone DistributionType::Custom with function pointer")
289 }
290 }
291 }
292}
293
294#[derive(Debug, Clone, Copy)]
296pub enum SparsityType {
297 Horseshoe,
299 SpikeAndSlab,
301 Lasso,
303 ElasticNet,
305 FinnishHorseshoe,
307}
308
309#[derive(Debug, Clone, Copy)]
311pub enum NonParametricProcess {
312 DirichletProcess,
314 PitmanYor,
316 ChineseRestaurant,
318 IndianBuffet,
320}
321
322#[derive(Debug, Clone)]
324pub enum ModelType {
325 LinearRegression,
327 LogisticRegression,
329 GeneralizedLinear { family: GLMFamily },
331 HierarchicalLinear { levels: usize },
333 GaussianProcess { kernel: KernelType },
335 BayesianNeuralNetwork {
337 layers: Vec<usize>,
338 activation: ActivationType,
339 },
340 StateSpace {
342 state_dim: usize,
343 observation_dim: usize,
344 },
345 Mixture {
347 components: usize,
348 component_type: ComponentType,
349 },
350}
351
352#[derive(Debug, Clone, Copy)]
354pub enum GLMFamily {
355 Gaussian,
356 Binomial,
357 Poisson,
358 Gamma,
359 InverseGaussian,
360 NegativeBinomial,
361}
362
363#[derive(Debug, Clone)]
365pub enum KernelType {
366 RBF { length_scale: f64 },
367 Matern { nu: f64, length_scale: f64 },
368 Periodic { period: f64, length_scale: f64 },
369 Linear { variance: f64 },
370 Polynomial { degree: usize, variance: f64 },
371 WhiteNoise { variance: f64 },
372 Sum { kernels: Vec<KernelType> },
373 Product { kernels: Vec<KernelType> },
374}
375
376#[derive(Debug, Clone, Copy)]
378pub enum ActivationType {
379 ReLU,
380 Sigmoid,
381 Tanh,
382 Swish,
383 GELU,
384}
385
386#[derive(Debug, Clone, Copy)]
388pub enum ComponentType {
389 Gaussian,
390 StudentT,
391 Laplace,
392 Skewed,
393}
394
395#[derive(Debug, Clone, Copy)]
397pub enum LikelihoodType {
398 Gaussian,
399 Binomial,
400 Poisson,
401 Gamma,
402 Beta,
403 Exponential,
404 StudentT,
405 Laplace,
406 Robust,
407}
408
409#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
411pub enum ModelSelectionCriterion {
412 DIC,
414 WAIC,
416 LooCv,
418 MarginalLikelihood,
420 PPL,
422 CVIC,
424}
425
426#[derive(Debug, Clone)]
428pub struct CrossValidationConfig {
429 pub k_folds: usize,
431 pub mc_samples: usize,
433 pub seed: Option<u64>,
435 pub stratify: bool,
437}
438
439#[derive(Debug, Clone)]
441pub struct ParallelConfig {
442 pub num_chains: usize,
444 pub parallel_models: bool,
446 pub parallel_cv: bool,
448}
449
450#[derive(Debug, Clone)]
452pub struct AdvancedBayesianRegression<F> {
453 pub model: BayesianModel<F>,
455 pub mcmc_config: MCMCConfig,
457 pub vi_config: VIConfig,
459 _phantom: PhantomData<F>,
460}
461
462#[derive(Debug, Clone)]
464pub struct MCMCConfig {
465 pub n_samples_: usize,
467 pub n_burnin: usize,
469 pub thin: usize,
471 pub n_chains: usize,
473 pub adaptation_period: usize,
475 pub target_acceptance: f64,
477 pub use_nuts: bool,
479 pub use_hmc: bool,
481}
482
483#[derive(Debug, Clone)]
485pub struct VIConfig {
486 pub max_iter: usize,
488 pub tolerance: f64,
490 pub learning_rate: f64,
492 pub family: VariationalFamily,
494 pub n_mc_samples: usize,
496}
497
498#[derive(Debug, Clone, Copy)]
500pub enum VariationalFamily {
501 MeanFieldGaussian,
503 FullRankGaussian,
505 NormalizingFlow,
507 MixtureGaussian,
509}
510
511#[derive(Debug, Clone)]
513pub struct BayesianGaussianProcess<F> {
514 pub x_train: Array2<F>,
516 pub y_train: Array1<F>,
518 pub kernel: KernelType,
520 pub noise_level: F,
522 pub hyperpriors: HashMap<String, DistributionType<F>>,
524 pub hyperparameter_samples: Option<Array2<F>>,
526}
527
528#[derive(Debug, Clone)]
530pub struct BayesianNeuralNetwork<F> {
531 pub architecture: Vec<usize>,
533 pub activations: Vec<ActivationType>,
535 pub weight_priors: Vec<DistributionType<F>>,
537 pub bias_priors: Vec<DistributionType<F>>,
539 pub weight_samples: Option<Vec<Vec<Array2<F>>>>,
543 pub bias_samples: Option<Vec<Vec<Array1<F>>>>,
547}
548
549#[derive(Debug, Clone)]
551pub struct ModelComparisonResult<F> {
552 pub rankings: HashMap<ModelSelectionCriterion, Vec<String>>,
554 pub ic_values: HashMap<String, HashMap<ModelSelectionCriterion, F>>,
556 pub bayes_factors: Array2<F>,
558 pub model_weights: HashMap<String, F>,
560 pub cv_results: HashMap<String, CrossValidationResult<F>>,
562 pub best_models: HashMap<ModelSelectionCriterion, String>,
564}
565
566#[derive(Debug, Clone)]
568pub struct CrossValidationResult<F> {
569 pub mean_score: F,
571 pub std_error: F,
573 pub fold_scores: Array1<F>,
575 pub effective_n_params: F,
577}
578
579#[derive(Debug, Clone)]
581pub struct AdvancedBayesianResult<F> {
582 pub posterior_samples: Array2<F>,
584 pub posterior_summary: PosteriorSummary<F>,
586 pub diagnostics: MCMCDiagnostics<F>,
588 pub model_fit: ModelFitMetrics<F>,
590 pub predictions: PredictiveDistribution<F>,
592}
593
594#[derive(Debug, Clone)]
596pub struct PosteriorSummary<F> {
597 pub means: Array1<F>,
599 pub stds: Array1<F>,
601 pub credible_intervals: Array2<F>,
603 pub ess: Array1<F>,
605 pub rhat: Array1<F>,
607}
608
609#[derive(Debug, Clone)]
611pub struct MCMCDiagnostics<F> {
612 pub acceptance_rates: Array1<F>,
614 pub autocorrelations: Array2<F>,
616 pub geweke_diagnostic: Array1<F>,
618 pub heidelberger_welch: Array1<bool>,
620 pub mc_errors: Array1<F>,
622}
623
624#[derive(Debug, Clone)]
626pub struct ModelFitMetrics<F> {
627 pub dic: F,
629 pub waic: F,
631 pub lppd: F,
633 pub p_eff: F,
635 pub posterior_p_value: F,
638 pub log_marginal_likelihood: F,
642 pub ppl: F,
646 pub loo_cv: F,
649 pub cvic: F,
652}
653
654#[derive(Debug, Clone)]
656pub struct PredictiveDistribution<F> {
657 pub means: Array1<F>,
659 pub variances: Array1<F>,
661 pub quantiles: Array2<F>,
663 pub samples: Array2<F>,
665}
666
667impl<F: AdvancedBayesianFloat> BayesianModelComparison<F> {
668 pub fn new() -> Self {
670 Self {
671 models: Vec::new(),
672 criteria: vec![
673 ModelSelectionCriterion::DIC,
674 ModelSelectionCriterion::WAIC,
675 ModelSelectionCriterion::LooCv,
676 ],
677 cv_config: CrossValidationConfig::default(),
678 parallel_config: ParallelConfig::default(),
679 }
680 }
681
682 pub fn add_model(&mut self, model: BayesianModel<F>) {
684 self.models.push(model);
685 }
686
687 pub fn compare_models(
697 &self,
698 x: &ArrayView2<F>,
699 y: &ArrayView1<F>,
700 ) -> StatsResult<ModelComparisonResult<F>> {
701 checkarray_finite(x, "x")?;
702 checkarray_finite(y, "y")?;
703
704 if x.nrows() != y.len() {
705 return Err(StatsError::DimensionMismatch(
706 "X and y must have same number of observations".to_string(),
707 ));
708 }
709 if self.models.is_empty() {
710 return Err(StatsError::InvalidArgument(
711 "At least one model must be registered via add_model before compare_models"
712 .to_string(),
713 ));
714 }
715
716 let mut rankings = HashMap::new();
717 let mut ic_values = HashMap::new();
718 let mut cv_results = HashMap::new();
719 let mut log_marginal_likelihoods: HashMap<String, F> = HashMap::new();
720
721 for model in &self.models {
723 let model_result = self.fit_single_model(model, x, y)?;
724 log_marginal_likelihoods.insert(
725 model.id.clone(),
726 model_result.model_fit.log_marginal_likelihood,
727 );
728
729 let mut model_ic_values = HashMap::new();
730
731 for criterion in &self.criteria {
732 let ic_value = self.compute_criterion(&model_result, criterion)?;
733 model_ic_values.insert(*criterion, ic_value);
734 }
735
736 ic_values.insert(model.id.clone(), model_ic_values);
737
738 let cv_result = self.cross_validate_model(model, x, y)?;
740 cv_results.insert(model.id.clone(), cv_result);
741 }
742
743 for criterion in &self.criteria {
748 let mut model_scores: Vec<(String, F)> = ic_values
749 .iter()
750 .map(|(id, scores)| (id.clone(), scores[criterion]))
751 .collect();
752
753 model_scores.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
754
755 let ranking: Vec<String> = model_scores.into_iter().map(|(id_, _)| id_).collect();
756 rankings.insert(*criterion, ranking);
757 }
758
759 let n_models = self.models.len();
762 let mut bayes_factors = Array2::<F>::ones((n_models, n_models));
763 for (i, model_i) in self.models.iter().enumerate() {
764 let log_ml_i = log_marginal_likelihoods[&model_i.id];
765 for (j, model_j) in self.models.iter().enumerate() {
766 let log_ml_j = log_marginal_likelihoods[&model_j.id];
767 bayes_factors[[i, j]] = (log_ml_i - log_ml_j).exp();
768 }
769 }
770
771 let model_weights = self.compute_model_weights(&ic_values)?;
773
774 let mut best_models = HashMap::new();
776 for criterion in &self.criteria {
777 if let Some(ranking) = rankings.get(criterion) {
778 if let Some(best_model) = ranking.first() {
779 best_models.insert(*criterion, best_model.clone());
780 }
781 }
782 }
783
784 Ok(ModelComparisonResult {
785 rankings,
786 ic_values,
787 bayes_factors,
788 model_weights,
789 cv_results,
790 best_models,
791 })
792 }
793
794 fn fit_single_model(
800 &self,
801 model: &BayesianModel<F>,
802 x: &ArrayView2<F>,
803 y: &ArrayView1<F>,
804 ) -> StatsResult<AdvancedBayesianResult<F>> {
805 let mut result = model_fit::fit_dispatch(model, x, y, &model_fit::primary_bnn_config())?;
806
807 let n = x.nrows();
808 let n_f = F::from(n).expect("sample count fits in any Float");
809 let two = F::from(-2.0).expect("-2.0 fits in any Float");
810
811 let loo_k = n.min(15);
817 let (loo_mean_ll, _, _) =
818 model_fit::k_fold_mean_loglik(model, x, y, loo_k, &model_fit::cv_bnn_config())?;
819 result.model_fit.loo_cv = two * loo_mean_ll * n_f;
820
821 let cvic_k = self.cv_config.k_folds.min(n.max(2));
822 let (cvic_mean_ll, _, _) =
823 model_fit::k_fold_mean_loglik(model, x, y, cvic_k, &model_fit::cv_bnn_config())?;
824 result.model_fit.cvic = two * cvic_mean_ll * n_f;
825
826 Ok(result)
827 }
828
829 fn compute_criterion(
835 &self,
836 result: &AdvancedBayesianResult<F>,
837 criterion: &ModelSelectionCriterion,
838 ) -> StatsResult<F> {
839 match criterion {
840 ModelSelectionCriterion::DIC => Ok(result.model_fit.dic),
841 ModelSelectionCriterion::WAIC => Ok(result.model_fit.waic),
842 ModelSelectionCriterion::LooCv => Ok(result.model_fit.loo_cv),
843 ModelSelectionCriterion::MarginalLikelihood => {
844 Ok(-result.model_fit.log_marginal_likelihood)
845 }
846 ModelSelectionCriterion::PPL => Ok(result.model_fit.ppl),
847 ModelSelectionCriterion::CVIC => Ok(result.model_fit.cvic),
848 }
849 }
850
851 fn cross_validate_model(
855 &self,
856 model: &BayesianModel<F>,
857 x: &ArrayView2<F>,
858 y: &ArrayView1<F>,
859 ) -> StatsResult<CrossValidationResult<F>> {
860 let k = self.cv_config.k_folds.min(x.nrows().max(2));
861 let (mean_score, std_error, fold_scores) =
862 model_fit::k_fold_mean_loglik(model, x, y, k, &model_fit::cv_bnn_config())?;
863 let effective_n_params = F::from(x.ncols()).expect("column count fits in any Float");
864
865 Ok(CrossValidationResult {
866 mean_score,
867 std_error,
868 fold_scores,
869 effective_n_params,
870 })
871 }
872
873 fn compute_model_weights(
875 &self,
876 ic_values: &HashMap<String, HashMap<ModelSelectionCriterion, F>>,
877 ) -> StatsResult<HashMap<String, F>> {
878 let mut weights = HashMap::new();
879
880 let waic_values: Vec<_> = ic_values
882 .iter()
883 .map(|(id, scores)| (id.clone(), scores[&ModelSelectionCriterion::WAIC]))
884 .collect();
885
886 let min_waic = waic_values
887 .iter()
888 .map(|(_, waic)| *waic)
889 .fold(F::infinity(), |a, b| if a < b { a } else { b });
890
891 let weight_sum: F = waic_values
892 .iter()
893 .map(|(_, waic)| {
894 (-((*waic - min_waic) / F::from(2.0).expect("Failed to convert constant to float")))
895 .exp()
896 })
897 .sum();
898
899 for (id, waic) in waic_values {
900 let weight = (-(waic - min_waic)
901 / F::from(2.0).expect("Failed to convert constant to float"))
902 .exp()
903 / weight_sum;
904 weights.insert(id, weight);
905 }
906
907 Ok(weights)
908 }
909}
910
911impl Default for CrossValidationConfig {
912 fn default() -> Self {
913 Self {
914 k_folds: 5,
915 mc_samples: 1000,
916 seed: None,
917 stratify: false,
918 }
919 }
920}
921
922impl Default for ParallelConfig {
923 fn default() -> Self {
924 Self {
925 num_chains: 4,
926 parallel_models: true,
927 parallel_cv: true,
928 }
929 }
930}
931
932impl Default for MCMCConfig {
933 fn default() -> Self {
934 Self {
935 n_samples_: 2000,
936 n_burnin: 1000,
937 thin: 1,
938 n_chains: 4,
939 adaptation_period: 500,
940 target_acceptance: 0.65,
941 use_nuts: true,
942 use_hmc: false,
943 }
944 }
945}
946
947impl Default for VIConfig {
948 fn default() -> Self {
949 Self {
950 max_iter: 10000,
951 tolerance: 1e-6,
952 learning_rate: 0.01,
953 family: VariationalFamily::MeanFieldGaussian,
954 n_mc_samples: 100,
955 }
956 }
957}
958
959impl<F: AdvancedBayesianFloat> Default for BayesianModelComparison<F> {
960 fn default() -> Self {
961 Self::new()
962 }
963}
964
965impl<F: AdvancedBayesianFloat> BayesianGaussianProcess<F> {
966 pub fn new(
968 x_train: Array2<F>,
969 y_train: Array1<F>,
970 kernel: KernelType,
971 noise_level: F,
972 ) -> StatsResult<Self> {
973 checkarray_finite(&x_train.view(), "x_train")?;
974 checkarray_finite(&y_train.view(), "y_train")?;
975
976 if x_train.nrows() != y_train.len() {
977 return Err(StatsError::DimensionMismatch(
978 "X and y must have same number of observations".to_string(),
979 ));
980 }
981
982 if noise_level <= F::zero() {
983 return Err(StatsError::InvalidArgument(
984 "Noise _level must be positive".to_string(),
985 ));
986 }
987
988 Ok(Self {
989 x_train,
990 y_train,
991 kernel,
992 noise_level,
993 hyperpriors: HashMap::new(),
994 hyperparameter_samples: None,
995 })
996 }
997
998 pub fn compute_kernel_matrix(
1000 &self,
1001 x1: &ArrayView2<F>,
1002 x2: &ArrayView2<F>,
1003 ) -> StatsResult<Array2<F>> {
1004 let n1 = x1.nrows();
1005 let n2 = x2.nrows();
1006 let mut k = Array2::zeros((n1, n2));
1007
1008 for i in 0..n1 {
1009 for j in 0..n2 {
1010 let x1_row = x1.row(i);
1011 let x2_row = x2.row(j);
1012 k[[i, j]] = self.kernel_function(&x1_row, &x2_row)?;
1013 }
1014 }
1015
1016 Ok(k)
1017 }
1018
1019 fn kernel_function(&self, x1: &ArrayView1<F>, x2: &ArrayView1<F>) -> StatsResult<F> {
1021 match &self.kernel {
1022 KernelType::RBF { length_scale } => {
1023 let length_scale = F::from(*length_scale).expect("Failed to convert to float");
1024 let mut squared_dist = F::zero();
1025
1026 for (a, b) in x1.iter().zip(x2.iter()) {
1027 let diff = *a - *b;
1028 squared_dist = squared_dist + diff * diff;
1029 }
1030
1031 Ok((-squared_dist
1032 / (F::from(2.0).expect("Failed to convert constant to float")
1033 * length_scale
1034 * length_scale))
1035 .exp())
1036 }
1037 KernelType::Matern { nu, length_scale } => {
1038 let nu = F::from(*nu).expect("Failed to convert to float");
1039 let length_scale = F::from(*length_scale).expect("Failed to convert to float");
1040 let mut dist = F::zero();
1041
1042 for (a, b) in x1.iter().zip(x2.iter()) {
1043 let diff = *a - *b;
1044 dist = dist + diff * diff;
1045 }
1046 dist = dist.sqrt();
1047
1048 if nu == F::from(1.5).expect("Failed to convert constant to float") {
1050 let sqrt3_r_l = F::from(3.0)
1051 .expect("Failed to convert constant to float")
1052 .sqrt()
1053 * dist
1054 / length_scale;
1055 Ok((F::one() + sqrt3_r_l) * (-sqrt3_r_l).exp())
1056 } else {
1057 Ok((-dist * dist
1059 / (F::from(2.0).expect("Failed to convert constant to float")
1060 * length_scale
1061 * length_scale))
1062 .exp())
1063 }
1064 }
1065 KernelType::Linear { variance } => {
1066 let variance = F::from(*variance).expect("Failed to convert to float");
1067 let dot_product = F::simd_dot(x1, x2);
1068 Ok(variance * dot_product)
1069 }
1070 KernelType::WhiteNoise { variance } => {
1071 let variance = F::from(*variance).expect("Failed to convert to float");
1072 let mut is_equal = true;
1074 for (a, b) in x1.iter().zip(x2.iter()) {
1075 if (*a - *b).abs()
1076 > F::from(1e-10).expect("Failed to convert constant to float")
1077 {
1078 is_equal = false;
1079 break;
1080 }
1081 }
1082 Ok(if is_equal { variance } else { F::zero() })
1083 }
1084 _ => {
1085 let mut squared_dist = F::zero();
1087 for (a, b) in x1.iter().zip(x2.iter()) {
1088 let diff = *a - *b;
1089 squared_dist = squared_dist + diff * diff;
1090 }
1091 Ok(
1092 (-squared_dist / F::from(2.0).expect("Failed to convert constant to float"))
1093 .exp(),
1094 )
1095 }
1096 }
1097 }
1098
1099 fn training_cholesky(&self) -> StatsResult<Array2<F>> {
1102 let n_train = self.x_train.nrows();
1103 let mut k_train = self.compute_kernel_matrix(&self.x_train.view(), &self.x_train.view())?;
1104 for i in 0..n_train {
1105 k_train[[i, i]] = k_train[[i, i]] + self.noise_level;
1106 }
1107 scirs2_linalg::cholesky(&k_train.view(), None).map_err(|e| {
1108 StatsError::ComputationError(format!(
1109 "Gaussian process kernel matrix is not positive definite (Cholesky decomposition failed): {e}"
1110 ))
1111 })
1112 }
1113
1114 fn solve_alpha(&self, l: &Array2<F>) -> StatsResult<Array1<F>> {
1117 let z = scirs2_linalg::solve_triangular(&l.view(), &self.y_train.view(), true, false)
1118 .map_err(|e| {
1119 StatsError::ComputationError(format!("GP forward substitution failed: {e}"))
1120 })?;
1121 scirs2_linalg::solve_triangular(&l.t(), &z.view(), false, false)
1122 .map_err(|e| StatsError::ComputationError(format!("GP back substitution failed: {e}")))
1123 }
1124
1125 pub fn predict(&self, xtest: &ArrayView2<F>) -> StatsResult<(Array1<F>, Array1<F>)> {
1130 checkarray_finite(xtest, "x_test")?;
1131 if xtest.ncols() != self.x_train.ncols() {
1132 return Err(StatsError::DimensionMismatch(format!(
1133 "x_test has {} columns, expected {} to match the training data",
1134 xtest.ncols(),
1135 self.x_train.ncols()
1136 )));
1137 }
1138
1139 let n_test = xtest.nrows();
1140 let l = self.training_cholesky()?;
1141 let alpha = self.solve_alpha(&l)?;
1142
1143 let k_star = self.compute_kernel_matrix(xtest, &self.x_train.view())?;
1145 let mean_pred = k_star.dot(&alpha);
1146
1147 let mut var_pred = Array1::<F>::zeros(n_test);
1148 for i in 0..n_test {
1149 let k_star_i = k_star.row(i).to_owned();
1150 let v = scirs2_linalg::solve_triangular(&l.view(), &k_star_i.view(), true, false)
1151 .map_err(|e| {
1152 StatsError::ComputationError(format!(
1153 "GP predictive variance solve failed: {e}"
1154 ))
1155 })?;
1156 let quad = v.dot(&v);
1157 let test_row = xtest.row(i);
1158 let k_ii = self.kernel_function(&test_row, &test_row)?;
1159 var_pred[i] = (k_ii - quad).max(F::zero());
1160 }
1161
1162 Ok((mean_pred, var_pred))
1163 }
1164
1165 pub fn log_marginal_likelihood(&self) -> StatsResult<F> {
1168 let n = self.x_train.nrows();
1169 let l = self.training_cholesky()?;
1170 let alpha = self.solve_alpha(&l)?;
1171 let data_fit = self.y_train.dot(&alpha);
1172
1173 let mut log_det_half = F::zero();
1174 for i in 0..n {
1175 let diag = l[[i, i]]
1176 .abs()
1177 .max(F::from(1e-300).expect("1e-300 fits in any Float"));
1178 log_det_half = log_det_half + diag.ln();
1179 }
1180
1181 let two_pi = F::from(2.0 * std::f64::consts::PI).expect("2*pi fits in any Float");
1182 let half = F::from(0.5).expect("0.5 fits in any Float");
1183 Ok(-half * data_fit
1184 - log_det_half
1185 - half * F::from(n).expect("n fits in any Float") * two_pi.ln())
1186 }
1187}
1188
1189impl<F: AdvancedBayesianFloat> BayesianNeuralNetwork<F> {
1190 pub fn new(architecture: Vec<usize>, activations: Vec<ActivationType>) -> StatsResult<Self> {
1192 if architecture.len() < 2 {
1193 return Err(StatsError::InvalidArgument(
1194 "Architecture must have at least input and output layers".to_string(),
1195 ));
1196 }
1197
1198 if activations.len() != architecture.len() - 1 {
1199 return Err(StatsError::InvalidArgument(
1200 "Number of activations must equal number of layers - 1".to_string(),
1201 ));
1202 }
1203
1204 let n_layers = architecture.len() - 1;
1205
1206 let weight_priors = (0..n_layers)
1208 .map(|i| {
1209 let fan_in = F::from(architecture[i]).expect("Failed to convert to float");
1210 let precision = fan_in; DistributionType::Normal {
1212 mean: F::zero(),
1213 precision,
1214 }
1215 })
1216 .collect();
1217
1218 let bias_priors = (0..n_layers)
1219 .map(|_| DistributionType::Normal {
1220 mean: F::zero(),
1221 precision: F::from(0.1).expect("Failed to convert constant to float"),
1222 })
1223 .collect();
1224
1225 Ok(Self {
1226 architecture,
1227 activations,
1228 weight_priors,
1229 bias_priors,
1230 weight_samples: None,
1231 bias_samples: None,
1232 })
1233 }
1234
1235 fn apply_activation(&self, x: F, activation: ActivationType) -> F {
1237 match activation {
1238 ActivationType::ReLU => {
1239 if x > F::zero() {
1240 x
1241 } else {
1242 F::zero()
1243 }
1244 }
1245 ActivationType::Sigmoid => F::one() / (F::one() + (-x).exp()),
1246 ActivationType::Tanh => x.tanh(),
1247 ActivationType::Swish => x / (F::one() + (-x).exp()),
1248 ActivationType::GELU => {
1249 let sqrt_2_pi = F::from(0.7978845608).expect("Failed to convert constant to float"); let coeff = F::from(0.044715).expect("Failed to convert constant to float");
1252 let inner = sqrt_2_pi * (x + coeff * x * x * x);
1253 F::from(0.5).expect("Failed to convert constant to float")
1254 * x
1255 * (F::one() + inner.tanh())
1256 }
1257 }
1258 }
1259
1260 pub fn forward(
1262 &self,
1263 x: &ArrayView2<F>,
1264 weights: &[Array2<F>],
1265 biases: &[Array1<F>],
1266 ) -> StatsResult<Array2<F>> {
1267 checkarray_finite(x, "x")?;
1268
1269 if weights.len() != self.architecture.len() - 1 {
1270 return Err(StatsError::InvalidArgument(
1271 "Number of weight matrices must match network layers".to_string(),
1272 ));
1273 }
1274
1275 if biases.len() != self.architecture.len() - 1 {
1276 return Err(StatsError::InvalidArgument(
1277 "Number of bias vectors must match network layers".to_string(),
1278 ));
1279 }
1280
1281 let mut activations = x.to_owned();
1282
1283 for (layer_idx, &activation_type) in self.activations.iter().enumerate() {
1284 let z = self.linear_transform(
1286 &activations.view(),
1287 &weights[layer_idx],
1288 &biases[layer_idx],
1289 )?;
1290
1291 activations = z.mapv(|val| self.apply_activation(val, activation_type));
1293 }
1294
1295 Ok(activations)
1296 }
1297
1298 fn linear_transform(
1300 &self,
1301 x: &ArrayView2<F>,
1302 weights: &Array2<F>,
1303 bias: &Array1<F>,
1304 ) -> StatsResult<Array2<F>> {
1305 let (batchsize, input_dim) = x.dim();
1306 let (weight_input_dim, output_dim) = weights.dim();
1307
1308 if input_dim != weight_input_dim {
1309 return Err(StatsError::DimensionMismatch(
1310 "Input dimension must match weight matrix input dimension".to_string(),
1311 ));
1312 }
1313
1314 if bias.len() != output_dim {
1315 return Err(StatsError::DimensionMismatch(
1316 "Bias length must match weight matrix output dimension".to_string(),
1317 ));
1318 }
1319
1320 let mut result = Array2::zeros((batchsize, output_dim));
1322
1323 for i in 0..batchsize {
1324 for j in 0..output_dim {
1325 let mut sum = F::zero();
1326 for k in 0..input_dim {
1327 sum = sum + x[[i, k]] * weights[[k, j]];
1328 }
1329 result[[i, j]] = sum + bias[j];
1330 }
1331 }
1332
1333 Ok(result)
1334 }
1335
1336 }
1341
1342#[cfg(test)]
1343mod tests {
1344 use super::*;
1345 use scirs2_core::ndarray::array;
1346
1347 #[test]
1348 fn test_model_comparison() {
1349 let mut comparison = BayesianModelComparison::<f64>::new();
1350
1351 let model = BayesianModel {
1352 id: "linear_model".to_string(),
1353 model_type: ModelType::LinearRegression,
1354 prior: AdvancedPrior::Conjugate {
1355 parameters: HashMap::new(),
1356 },
1357 likelihood: LikelihoodType::Gaussian,
1358 complexity: 3.0,
1359 };
1360
1361 comparison.add_model(model);
1362
1363 let x = array![[1.0, 0.5], [3.0, -1.0], [5.0, 2.0], [7.0, -0.5]];
1364 let y = array![1.2, 2.1, 3.4, 3.8];
1365
1366 let result = comparison
1367 .compare_models(&x.view(), &y.view())
1368 .expect("compare_models should succeed for a well-specified single model");
1369
1370 let fit = &result.ic_values["linear_model"];
1376 assert!(fit[&ModelSelectionCriterion::WAIC].is_finite());
1377 assert!(fit[&ModelSelectionCriterion::DIC].is_finite());
1378 assert!(result.model_weights["linear_model"] > 0.0);
1379 }
1380
1381 #[test]
1382 fn test_model_comparison_prefers_true_generating_model() {
1383 let xs_base: Vec<f64> = vec![
1396 -4.0, -3.0, -2.0, -1.5, -1.0, -0.5, -0.2, 0.2, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0,
1397 ];
1398 let ys_base: Vec<f64> = vec![
1399 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0,
1400 ];
1401 let reps = 4;
1411 let xs: Vec<f64> = xs_base
1412 .iter()
1413 .cloned()
1414 .cycle()
1415 .take(xs_base.len() * reps)
1416 .collect();
1417 let ys: Vec<f64> = ys_base
1418 .iter()
1419 .cloned()
1420 .cycle()
1421 .take(ys_base.len() * reps)
1422 .collect();
1423 let x = Array2::from_shape_fn((xs.len(), 2), |(i, j)| if j == 0 { 1.0 } else { xs[i] });
1428 let y = Array1::from_vec(ys);
1429
1430 let mut comparison = BayesianModelComparison::<f64>::new();
1431 comparison.add_model(BayesianModel {
1432 id: "true_logit_link".to_string(),
1433 model_type: ModelType::GeneralizedLinear {
1434 family: GLMFamily::Binomial,
1435 },
1436 prior: AdvancedPrior::Conjugate {
1437 parameters: HashMap::new(),
1438 },
1439 likelihood: LikelihoodType::Binomial,
1440 complexity: 2.0,
1441 });
1442 comparison.add_model(BayesianModel {
1443 id: "wrong_gaussian_link".to_string(),
1444 model_type: ModelType::LinearRegression,
1445 prior: AdvancedPrior::Conjugate {
1446 parameters: HashMap::new(),
1447 },
1448 likelihood: LikelihoodType::Gaussian,
1449 complexity: 2.0,
1450 });
1451
1452 let result = comparison
1453 .compare_models(&x.view(), &y.view())
1454 .expect("compare_models should succeed for two well-specified GLM models");
1455
1456 for criterion in [ModelSelectionCriterion::WAIC, ModelSelectionCriterion::DIC] {
1457 let ranking = &result.rankings[&criterion];
1458 assert_eq!(
1459 ranking.first().map(|s| s.as_str()),
1460 Some("true_logit_link"),
1461 "{criterion:?} should rank the correctly-specified model first, got {ranking:?}"
1462 );
1463 }
1464
1465 let bf_true_vs_wrong = result.bayes_factors[[0, 1]];
1469 assert!(
1470 bf_true_vs_wrong > 1.0,
1471 "Bayes factor should favor the true generating model, got {bf_true_vs_wrong}"
1472 );
1473 }
1474
1475 #[test]
1476 fn test_generalized_linear_family_likelihood_mismatch_is_rejected() {
1477 let xs: Vec<f64> = (0..10).map(|i| i as f64 * 0.3).collect();
1488 let ys: Vec<f64> = xs.iter().map(|&xv| (0.4 + 0.6 * xv).exp()).collect();
1489 let x = Array2::from_shape_fn((xs.len(), 2), |(i, j)| if j == 0 { 1.0 } else { xs[i] });
1490 let y = Array1::from_vec(ys);
1491
1492 let mut comparison = BayesianModelComparison::<f64>::new();
1493 comparison.add_model(BayesianModel {
1494 id: "mismatched_model".to_string(),
1495 model_type: ModelType::GeneralizedLinear {
1496 family: GLMFamily::Poisson,
1497 },
1498 prior: AdvancedPrior::Conjugate {
1499 parameters: HashMap::new(),
1500 },
1501 likelihood: LikelihoodType::Gaussian,
1503 complexity: 2.0,
1504 });
1505
1506 let err = comparison.compare_models(&x.view(), &y.view()).expect_err(
1507 "a GeneralizedLinear model whose declared family disagrees with its \
1508 likelihood must be rejected, not silently fit as `likelihood` alone",
1509 );
1510 let message = err.to_string();
1511 assert!(
1512 message.contains("family") && message.contains("likelihood"),
1513 "error should explain the family/likelihood mismatch, got: {message}"
1514 );
1515 }
1516
1517 #[test]
1518 fn test_gaussian_process_noiseless_interpolation() {
1519 let x_train = array![[0.0], [1.0], [2.0]];
1522 let y_train = array![0.0, 1.0, 4.0];
1523 let noise = 1e-6; let gp = BayesianGaussianProcess::new(
1526 x_train.clone(),
1527 y_train.clone(),
1528 KernelType::RBF { length_scale: 1.0 },
1529 noise,
1530 )
1531 .expect("GP construction should succeed");
1532
1533 assert_eq!(gp.x_train.nrows(), 3);
1534 assert_eq!(gp.y_train.len(), 3);
1535
1536 let (mean_train, var_train) = gp
1540 .predict(&x_train.view())
1541 .expect("prediction at training points should succeed");
1542 for i in 0..3 {
1543 assert!(
1544 (mean_train[i] - y_train[i]).abs() < 1e-3,
1545 "GP should nearly interpolate noiseless training data at point {i}: got {}, expected {}",
1546 mean_train[i],
1547 y_train[i]
1548 );
1549 assert!(
1550 var_train[i] < 1e-2,
1551 "GP posterior variance at a training point should be tiny, got {}",
1552 var_train[i]
1553 );
1554 }
1555
1556 let x_mid = array![[0.5]];
1561 let (mean_mid, _) = gp
1562 .predict(&x_mid.view())
1563 .expect("midpoint prediction should succeed");
1564 assert!(
1565 (mean_mid[0] - 0.0).abs() > 1e-3 && (mean_mid[0] - 1.0).abs() > 1e-3,
1566 "GP posterior mean at the midpoint should be a genuine blend of neighboring \
1567 training values, not equal to either one exactly: got {}",
1568 mean_mid[0]
1569 );
1570
1571 let x_far = array![[50.0]];
1574 let (_, var_far) = gp
1575 .predict(&x_far.view())
1576 .expect("far-point prediction should succeed");
1577 assert!(
1578 var_far[0] > var_train[0] + 1e-3,
1579 "GP posterior variance should grow away from training data: far={}, near={}",
1580 var_far[0],
1581 var_train[0]
1582 );
1583
1584 let log_ml = gp
1585 .log_marginal_likelihood()
1586 .expect("log marginal likelihood should compute");
1587 assert!(log_ml.is_finite());
1588 }
1589
1590 #[test]
1591 fn test_bayesian_neural_network_prior_predictive_is_input_dependent() {
1592 let bnn = BayesianNeuralNetwork::<f64>::new(
1593 vec![2, 5, 1],
1594 vec![ActivationType::ReLU, ActivationType::Sigmoid],
1595 )
1596 .expect("network construction should succeed");
1597
1598 let x_test = array![[0.0, 0.0], [5.0, -5.0], [-5.0, 5.0], [10.0, 10.0]];
1602 let (means, vars) = bnn
1603 .predict_with_uncertainty(&x_test.view(), 200)
1604 .expect("prior-predictive prediction should succeed");
1605
1606 let first_mean = means[[0, 0]];
1607 let all_means_equal =
1608 (0..x_test.nrows()).all(|i| (means[[i, 0]] - first_mean).abs() < 1e-9);
1609 assert!(
1610 !all_means_equal,
1611 "predictive means should genuinely depend on very different input rows, got {means:?}"
1612 );
1613 for v in vars.iter() {
1614 assert!(*v >= 0.0, "variance must be non-negative, got {v}");
1615 }
1616 assert!(
1617 means.iter().any(|&m| m.abs() > 1e-9),
1618 "means should not all be the fabricated placeholder 0.0, got {means:?}"
1619 );
1620 }
1621
1622 #[test]
1623 fn test_bayesian_neural_network_fit_improves_predictions() {
1624 let xs: Vec<[f64; 2]> = vec![
1627 [-2.0, -2.0],
1628 [-2.0, 0.0],
1629 [-2.0, 2.0],
1630 [0.0, -2.0],
1631 [0.0, 0.0],
1632 [0.0, 2.0],
1633 [2.0, -2.0],
1634 [2.0, 0.0],
1635 [2.0, 2.0],
1636 ];
1637 let sigmoid = |z: f64| 1.0 / (1.0 + (-z).exp());
1638 let ys: Vec<f64> = xs
1639 .iter()
1640 .map(|p| sigmoid(0.5 * p[0] - 0.3 * p[1]))
1641 .collect();
1642
1643 let x = Array2::from_shape_fn((xs.len(), 2), |(i, j)| xs[i][j]);
1644 let y_col = Array2::from_shape_fn((ys.len(), 1), |(i, _)| ys[i]);
1645 let y_flat = Array1::from_vec(ys);
1646
1647 let mut bnn = BayesianNeuralNetwork::<f64>::new(
1648 vec![2, 6, 1],
1649 vec![ActivationType::ReLU, ActivationType::Sigmoid],
1650 )
1651 .expect("network construction should succeed");
1652
1653 let config = BnnTrainingConfig {
1654 n_ensemble: 6,
1655 epochs: 400,
1656 learning_rate: 0.2,
1657 bootstrap: true,
1658 seed: Some(20_260_729),
1659 };
1660 bnn.fit(&x.view(), &y_col.view(), &config)
1661 .expect("BNN ensemble training should succeed");
1662
1663 let (means_after, vars_after) = bnn
1664 .predict_with_uncertainty(&x.view(), 40)
1665 .expect("post-fit prediction should succeed");
1666
1667 let mse_after: f64 = (0..xs.len())
1668 .map(|i| {
1669 let d = means_after[[i, 0]] - y_flat[i];
1670 d * d
1671 })
1672 .sum::<f64>()
1673 / xs.len() as f64;
1674
1675 let mean_y = y_flat.iter().sum::<f64>() / y_flat.len() as f64;
1676 let baseline_mse: f64 =
1677 y_flat.iter().map(|&yv| (yv - mean_y).powi(2)).sum::<f64>() / y_flat.len() as f64;
1678
1679 assert!(
1680 mse_after < baseline_mse * 0.5,
1681 "fitted BNN should fit learnable training data substantially better than a \
1682 mean-only baseline: mse_after={mse_after}, baseline_mse={baseline_mse}"
1683 );
1684
1685 let first_var = vars_after[[0, 0]];
1686 let any_different = (0..xs.len()).any(|i| (vars_after[[i, 0]] - first_var).abs() > 1e-9);
1687 assert!(
1688 any_different,
1689 "post-fit predictive variance should vary across inputs, got {vars_after:?}"
1690 );
1691 }
1692}