scirs2_interpolate/random_features/
regressor.rs1use crate::error::InterpolateError;
7use crate::random_features::feature_map::{FourierFeatureMap, RffKernel};
8use crate::random_features::mod_internal::cholesky_solve_vec;
9use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
10
11#[derive(Debug, Clone)]
36pub struct RandomFeaturesRegressor {
37 pub feature_map: FourierFeatureMap,
39 pub weights: Array1<f64>,
41 pub lambda: f64,
43 fitted: bool,
45}
46
47impl RandomFeaturesRegressor {
48 pub fn new(kernel: RffKernel, d_features: usize, lambda: f64, seed: u64) -> Self {
60 Self {
64 feature_map: FourierFeatureMap::new(kernel, 1, d_features, seed),
65 weights: Array1::zeros(0),
66 lambda,
67 fitted: false,
68 }
69 }
70
71 pub fn fit(
79 &mut self,
80 x: &ArrayView2<f64>,
81 y: &ArrayView1<f64>,
82 ) -> Result<(), InterpolateError> {
83 let n = x.nrows();
84 let d_in = x.ncols();
85 if n == 0 {
86 return Err(InterpolateError::InsufficientData(
87 "Training data is empty".to_string(),
88 ));
89 }
90 if n != y.len() {
91 return Err(InterpolateError::DimensionMismatch(format!(
92 "x has {n} rows but y has {} elements",
93 y.len(),
94 )));
95 }
96 if d_in == 0 {
97 return Err(InterpolateError::InvalidInput {
98 message: "input dimension d_in must be > 0".to_string(),
99 });
100 }
101
102 let d_out = self.feature_map.d_out;
104 let kernel = self.feature_map.kernel.clone();
105 let seed = d_in as u64 * 0x9e37_79b9 ^ 42;
107 self.feature_map = FourierFeatureMap::new(kernel, d_in, d_out, seed);
108
109 let z = self.feature_map.transform(x)?;
110 let d = d_out;
111
112 let mut ztzt = vec![vec![0.0f64; d]; d];
114 for k in 0..n {
115 for i in 0..d {
116 for j in 0..=i {
117 let v = z[(k, i)] * z[(k, j)];
118 ztzt[i][j] += v;
119 if i != j {
120 ztzt[j][i] += v;
121 }
122 }
123 }
124 }
125 for i in 0..d {
126 ztzt[i][i] += self.lambda;
127 }
128
129 let mut zty = vec![0.0f64; d];
131 for j in 0..d {
132 for k in 0..n {
133 zty[j] += z[(k, j)] * y[k];
134 }
135 }
136
137 let w = cholesky_solve_vec(&ztzt, &zty)?;
138 self.weights = Array1::from_vec(w);
139 self.fitted = true;
140 Ok(())
141 }
142
143 pub fn predict(&self, x: &ArrayView2<f64>) -> Result<Array1<f64>, InterpolateError> {
148 if !self.fitted {
149 return Err(InterpolateError::InvalidState(
150 "Model not fitted; call fit() first".to_string(),
151 ));
152 }
153 let z = self.feature_map.transform(x)?;
154 let n = z.nrows();
155 let mut preds = Array1::<f64>::zeros(n);
156 for i in 0..n {
157 let zi = z.row(i);
158 preds[i] = zi.iter().zip(self.weights.iter()).map(|(a, b)| a * b).sum();
159 }
160 Ok(preds)
161 }
162
163 pub fn is_fitted(&self) -> bool {
165 self.fitted
166 }
167}
168
169#[cfg(test)]
172mod tests {
173 use super::*;
174 use scirs2_core::ndarray::{Array1, Array2};
175
176 #[test]
177 fn test_regressor_sin_1d() {
178 let n = 60;
179 let x = Array2::from_shape_fn((n, 1), |(i, _)| {
180 i as f64 * std::f64::consts::PI * 2.0 / n as f64
181 });
182 let y: Array1<f64> = x.column(0).mapv(f64::sin);
183
184 let mut reg =
185 RandomFeaturesRegressor::new(RffKernel::Gaussian { length_scale: 1.0 }, 200, 1e-4, 42);
186 reg.fit(&x.view(), &y.view()).expect("fit");
187
188 let x_test = Array2::from_shape_fn((20, 1), |(i, _)| {
189 i as f64 * std::f64::consts::PI * 2.0 / 20.0
190 });
191 let y_pred = reg.predict(&x_test.view()).expect("predict");
192 let y_true: Array1<f64> = x_test.column(0).mapv(f64::sin);
193
194 let rmse: f64 = {
195 let sum_sq: f64 = y_pred
196 .iter()
197 .zip(y_true.iter())
198 .map(|(p, t)| (p - t).powi(2))
199 .sum();
200 (sum_sq / 20.0).sqrt()
201 };
202 assert!(rmse < 0.5, "RMSE {rmse} should be < 0.5 for sin regression");
203 }
204
205 #[test]
206 fn test_predict_before_fit_errors() {
207 let reg =
208 RandomFeaturesRegressor::new(RffKernel::Gaussian { length_scale: 1.0 }, 50, 1e-3, 0);
209 let x = Array2::<f64>::zeros((3, 1));
210 assert!(reg.predict(&x.view()).is_err(), "should error before fit");
211 }
212
213 #[test]
214 fn test_regressor_output_length() {
215 let n_train = 20;
216 let n_test = 7;
217 let x_train = Array2::from_shape_fn((n_train, 2), |(i, j)| (i + j) as f64 * 0.1);
218 let y_train = Array1::from_iter((0..n_train).map(|i| i as f64 * 0.5));
219 let x_test = Array2::from_shape_fn((n_test, 2), |(i, j)| (i + j) as f64 * 0.15);
220
221 let mut reg =
222 RandomFeaturesRegressor::new(RffKernel::Laplacian { length_scale: 0.5 }, 30, 1e-3, 7);
223 reg.fit(&x_train.view(), &y_train.view()).expect("fit");
224 let preds = reg.predict(&x_test.view()).expect("predict");
225 assert_eq!(preds.len(), n_test);
226 }
227}