Skip to content

Commit 4ec0e22

Browse files
committed
Add theta tuning interface for SGP
1 parent 54bb51c commit 4ec0e22

File tree

10 files changed

+319
-146
lines changed

10 files changed

+319
-146
lines changed

gp/benches/gp.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ fn criterion_gp(c: &mut Criterion) {
5858
SquaredExponentialCorr::default(),
5959
)
6060
.kpls_dim(Some(1))
61-
.initial_theta(Some(vec![1.0]))
61+
.theta_guess(vec![1.0])
6262
.fit(&Dataset::new(xt.to_owned(), yt.to_owned()))
6363
.expect("GP fit error"),
6464
)

gp/src/algorithm.rs

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@ use ndarray::{arr1, s, Array, Array1, Array2, ArrayBase, Axis, Data, Ix1, Ix2, Z
1515
use ndarray_einsum_beta::*;
1616
#[cfg(feature = "blas")]
1717
use ndarray_linalg::{cholesky::*, eigh::*, qr::*, svd::*, triangular::*};
18-
use ndarray_rand::rand::SeedableRng;
18+
use ndarray_rand::rand::{Rng, SeedableRng};
1919
use ndarray_stats::QuantileExt;
2020

2121
use rand_xoshiro::Xoshiro256Plus;
2222
#[cfg(feature = "serializable")]
2323
use serde::{Deserialize, Serialize};
2424
use std::fmt;
2525

26-
use ndarray_rand::rand_distr::{Normal, Uniform};
26+
use ndarray_rand::rand_distr::Normal;
2727
use ndarray_rand::RandomExt;
2828

2929
// const LOG10_20: f64 = 1.301_029_995_663_981_3; //f64::log10(20.);
@@ -751,7 +751,7 @@ impl<F: Float, Mean: RegressionModel<F>, Corr: CorrelationModel<F>, D: Data<Elem
751751
let y = dataset.targets();
752752
if let Some(d) = self.kpls_dim() {
753753
if *d > x.ncols() {
754-
return Err(GpError::InvalidValue(format!(
754+
return Err(GpError::InvalidValueError(format!(
755755
"Dimension reduction {} should be smaller than actual \
756756
training input dimensions {}",
757757
d,
@@ -786,12 +786,17 @@ impl<F: Float, Mean: RegressionModel<F>, Corr: CorrelationModel<F>, D: Data<Elem
786786
"Warning: multiple x input features have the same value (at least same row twice)."
787787
);
788788
}
789-
let theta0 = self
790-
.initial_theta()
791-
.clone()
792-
.map_or(Array1::from_elem(w_star.ncols(), F::cast(1e-2)), |v| {
793-
Array::from_vec(v)
794-
});
789+
790+
// Initial guess for theta
791+
let theta0_dim = self.theta_tuning().theta0().len();
792+
let theta0 = if theta0_dim == 1 {
793+
Array1::from_elem(w_star.ncols(), self.theta_tuning().theta0()[0])
794+
} else if theta0_dim == w_star.ncols() {
795+
Array::from_vec(self.theta_tuning().theta0().to_vec())
796+
} else {
797+
panic!("Initial guess for theta should be either 1-dim or dim of xtrain (w_star.ncols()), got {}", theta0_dim)
798+
};
799+
795800
let fx = self.mean().value(&xtrain.data);
796801
let base: f64 = 10.;
797802
let objfn = |x: &[f64], _gradient: Option<&mut [f64]>, _params: &mut ()| -> f64 {
@@ -815,7 +820,20 @@ impl<F: Float, Mean: RegressionModel<F>, Corr: CorrelationModel<F>, D: Data<Elem
815820

816821
// Multistart: user theta0 + 1e-5, 1e-4, 1e-3, 1e-2, 0.1, 1., 10.
817822
// let bounds = vec![(F::cast(-6.), F::cast(2.)); theta0.len()];
818-
let (theta0s, bounds) = prepare_multistart(self.n_start(), &theta0);
823+
let bounds_dim = self.theta_tuning().bounds().len();
824+
let bounds = if bounds_dim == 1 {
825+
vec![self.theta_tuning().bounds()[0]; w_star.ncols()]
826+
} else if theta0_dim == w_star.ncols() {
827+
self.theta_tuning().bounds().to_vec()
828+
} else {
829+
panic!(
830+
"Bounds for theta should be either 1-dim or dim of xtrain ({}), got {}",
831+
w_star.ncols(),
832+
theta0_dim
833+
)
834+
};
835+
836+
let (theta0s, bounds) = prepare_multistart(self.n_start(), &theta0, &bounds);
819837

820838
let opt_thetas = theta0s.map_axis(Axis(1), |theta| {
821839
optimize_params(objfn, &theta.to_owned(), &bounds)
@@ -840,11 +858,13 @@ impl<F: Float, Mean: RegressionModel<F>, Corr: CorrelationModel<F>, D: Data<Elem
840858
pub(crate) fn prepare_multistart<F: Float>(
841859
n_start: usize,
842860
theta0: &Array1<F>,
861+
bounds: &[(F, F)],
843862
) -> (Array2<F>, Vec<(F, F)>) {
844-
let limits = (F::cast(-6.), F::cast(2.));
845-
// let mut bounds = vec![(F::cast(1e-16).log10(), F::cast(1.).log10()); params.ncols()];
846-
//let limits = (F::cast(-16), F::cast(0.));
847-
let bounds = vec![limits; theta0.len()];
863+
// Use log10 theta as optimization parameter
864+
let bounds: Vec<(F, F)> = bounds
865+
.iter()
866+
.map(|(lo, up)| (lo.log10(), up.log10()))
867+
.collect();
848868

849869
// Multistart: user theta0 + 1e-5, 1e-4, 1e-3, 1e-2, 0.1, 1., 10.
850870
let mut theta0s = Array2::zeros((n_start + 1, theta0.len()));
@@ -854,17 +874,17 @@ pub(crate) fn prepare_multistart<F: Float>(
854874
std::cmp::Ordering::Equal => {
855875
//let mut rng = Xoshiro256Plus::seed_from_u64(42);
856876
let mut rng = Xoshiro256Plus::from_entropy();
857-
theta0s.row_mut(1).assign(&Array::random_using(
858-
theta0.len(),
859-
Uniform::new(limits.0, limits.1),
860-
&mut rng,
861-
))
877+
let vals = bounds.iter().map(|(a, b)| rng.gen_range(*a..*b)).collect();
878+
theta0s.row_mut(1).assign(&Array::from_vec(vals))
862879
}
863880
std::cmp::Ordering::Greater => {
864-
let mut xlimits: Array2<F> = Array2::zeros((theta0.len(), 2));
865-
for mut row in xlimits.rows_mut() {
866-
row.assign(&arr1(&[limits.0, limits.1]));
867-
}
881+
let mut xlimits: Array2<F> = Array2::zeros((bounds.len(), 2));
882+
// for mut row in xlimits.rows_mut() {
883+
// row.assign(&arr1(&[limits.0, limits.1]));
884+
// }
885+
Zip::from(xlimits.rows_mut())
886+
.and(&bounds)
887+
.for_each(|mut row, limits| row.assign(&arr1(&[limits.0, limits.1])));
868888
// Use a seed here for reproducibility. Do we need to make it truly random
869889
// Probably no, as it is just to get init values spread over
870890
// [1e-6, 20] for multistart thanks to LHS method.
@@ -1187,7 +1207,7 @@ mod tests {
11871207
ConstantMean::default(),
11881208
SquaredExponentialCorr::default(),
11891209
)
1190-
.initial_theta(Some(vec![0.1]))
1210+
.theta_guess(vec![0.1])
11911211
.kpls_dim(Some(1))
11921212
.fit(&Dataset::new(xt, yt))
11931213
.expect("GP fit error");
@@ -1210,7 +1230,7 @@ mod tests {
12101230
[<$regr Mean>]::default(),
12111231
[<$corr Corr>]::default(),
12121232
)
1213-
.initial_theta(Some(vec![0.1]))
1233+
.theta_guess(vec![0.1])
12141234
.fit(&Dataset::new(xt, yt))
12151235
.expect("GP fit error");
12161236
let yvals = gp

gp/src/errors.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@ pub enum GpError {
2121
/// When PLS fails
2222
#[error("PLS error: {0}")]
2323
PlsError(#[from] linfa_pls::PlsError),
24-
/// When a value is invalid
25-
#[error("PLS error: {0}")]
26-
InvalidValue(String),
2724
/// When a linfa error occurs
2825
#[error(transparent)]
2926
LinfaError(#[from] linfa::error::Error),
@@ -36,7 +33,7 @@ pub enum GpError {
3633
/// When error during loading
3734
#[error("Load error: {0}")]
3835
LoadError(String),
39-
/// When error during loading
36+
/// When error dur to a bad value
4037
#[error("InvalidValue error: {0}")]
4138
InvalidValueError(String),
4239
}

gp/src/parameters.rs

Lines changed: 107 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,71 @@ use crate::correlation_models::{CorrelationModel, SquaredExponentialCorr};
22
use crate::errors::{GpError, Result};
33
use crate::mean_models::{ConstantMean, RegressionModel};
44
use linfa::{Float, ParamGuard};
5+
use std::convert::TryFrom;
6+
7+
#[cfg(feature = "serializable")]
8+
use serde::{Deserialize, Serialize};
9+
10+
/// A structure to represent a n-dim parameter estimation
11+
#[derive(Clone, Debug, PartialEq, Eq)]
12+
#[cfg_attr(feature = "serializable", derive(Serialize, Deserialize))]
13+
pub struct ParamTuning<F: Float> {
14+
pub guess: Vec<F>,
15+
pub bounds: Vec<(F, F)>,
16+
}
17+
18+
impl<F: Float> TryFrom<ParamTuning<F>> for ThetaTuning<F> {
19+
type Error = GpError;
20+
fn try_from(pt: ParamTuning<F>) -> Result<ThetaTuning<F>> {
21+
if pt.guess.len() != pt.bounds.len() && (pt.guess.len() != 1 || pt.bounds.len() != 1) {
22+
return Err(GpError::InvalidValueError(
23+
"Bad theta tuning specification".to_string(),
24+
));
25+
}
26+
// TODO: check if guess in bounds
27+
Ok(ThetaTuning(pt))
28+
}
29+
}
30+
31+
/// As structure for theta hyperparameters guess
32+
#[derive(Clone, Debug, PartialEq, Eq)]
33+
#[cfg_attr(feature = "serializable", derive(Serialize, Deserialize))]
34+
35+
pub struct ThetaTuning<F: Float>(ParamTuning<F>);
36+
impl<F: Float> Default for ThetaTuning<F> {
37+
fn default() -> ThetaTuning<F> {
38+
ThetaTuning(ParamTuning {
39+
guess: vec![F::cast(0.01)],
40+
bounds: vec![(F::cast(1e-6), F::cast(1e2))],
41+
})
42+
}
43+
}
44+
45+
impl<F: Float> From<ThetaTuning<F>> for ParamTuning<F> {
46+
fn from(tt: ThetaTuning<F>) -> ParamTuning<F> {
47+
ParamTuning {
48+
guess: tt.0.guess,
49+
bounds: tt.0.bounds,
50+
}
51+
}
52+
}
53+
54+
impl<F: Float> ThetaTuning<F> {
55+
pub fn theta0(&self) -> &[F] {
56+
&self.0.guess
57+
}
58+
pub fn bounds(&self) -> &[(F, F)] {
59+
&self.0.bounds
60+
}
61+
}
562

663
/// A set of validated GP parameters.
764
#[derive(Clone, Debug, PartialEq, Eq)]
865
pub struct GpValidParams<F: Float, Mean: RegressionModel<F>, Corr: CorrelationModel<F>> {
966
/// Parameter of the autocorrelation model
1067
pub(crate) theta: Option<Vec<F>>,
68+
/// Parameter guess of the autocorrelation model
69+
pub(crate) theta_tuning: ThetaTuning<F>,
1170
/// Regression model representing the mean(x)
1271
pub(crate) mean: Mean,
1372
/// Correlation model representing the spatial correlation between errors at e(x) and e(x')
@@ -24,6 +83,7 @@ impl<F: Float> Default for GpValidParams<F, ConstantMean, SquaredExponentialCorr
2483
fn default() -> GpValidParams<F, ConstantMean, SquaredExponentialCorr> {
2584
GpValidParams {
2685
theta: None,
86+
theta_tuning: ThetaTuning::default(),
2787
mean: ConstantMean(),
2888
corr: SquaredExponentialCorr(),
2989
kpls_dim: None,
@@ -34,11 +94,6 @@ impl<F: Float> Default for GpValidParams<F, ConstantMean, SquaredExponentialCorr
3494
}
3595

3696
impl<F: Float, Mean: RegressionModel<F>, Corr: CorrelationModel<F>> GpValidParams<F, Mean, Corr> {
37-
/// Get starting theta value for optimization
38-
pub fn initial_theta(&self) -> &Option<Vec<F>> {
39-
&self.theta
40-
}
41-
4297
/// Get mean model
4398
pub fn mean(&self) -> &Mean {
4499
&self.mean
@@ -49,6 +104,11 @@ impl<F: Float, Mean: RegressionModel<F>, Corr: CorrelationModel<F>> GpValidParam
49104
&self.corr
50105
}
51106

107+
/// Get starting theta value for optimization
108+
pub fn theta_tuning(&self) -> &ThetaTuning<F> {
109+
&self.theta_tuning
110+
}
111+
52112
/// Get number of components used by PLS
53113
pub fn kpls_dim(&self) -> &Option<usize> {
54114
&self.kpls_dim
@@ -77,6 +137,7 @@ impl<F: Float, Mean: RegressionModel<F>, Corr: CorrelationModel<F>> GpParams<F,
77137
pub fn new(mean: Mean, corr: Corr) -> GpParams<F, Mean, Corr> {
78138
Self(GpValidParams {
79139
theta: None,
140+
theta_tuning: ThetaTuning::default(),
80141
mean,
81142
corr,
82143
kpls_dim: None,
@@ -85,14 +146,6 @@ impl<F: Float, Mean: RegressionModel<F>, Corr: CorrelationModel<F>> GpParams<F,
85146
})
86147
}
87148

88-
/// Set initial value for theta hyper parameter.
89-
///
90-
/// During training process, the internal optimization is started from `initial_theta`.
91-
pub fn initial_theta(mut self, theta: Option<Vec<F>>) -> Self {
92-
self.0.theta = theta;
93-
self
94-
}
95-
96149
/// Set mean model.
97150
pub fn mean(mut self, mean: Mean) -> Self {
98151
self.0.mean = mean;
@@ -112,6 +165,36 @@ impl<F: Float, Mean: RegressionModel<F>, Corr: CorrelationModel<F>> GpParams<F,
112165
self
113166
}
114167

168+
/// Set initial value for theta hyper parameter.
169+
///
170+
/// During training process, the internal optimization is started from `theta_guess`.
171+
pub fn theta_guess(mut self, theta_guess: Vec<F>) -> Self {
172+
self.0.theta_tuning = ParamTuning {
173+
guess: theta_guess,
174+
..ThetaTuning::default().into()
175+
}
176+
.try_into()
177+
.unwrap();
178+
self
179+
}
180+
181+
/// Set theta hyper parameter search space.
182+
pub fn theta_bounds(mut self, theta_bounds: Vec<(F, F)>) -> Self {
183+
self.0.theta_tuning = ParamTuning {
184+
bounds: theta_bounds,
185+
..ThetaTuning::default().into()
186+
}
187+
.try_into()
188+
.unwrap();
189+
self
190+
}
191+
192+
/// Set theta hyper parameter tuning
193+
pub fn theta_tuning(mut self, theta_tuning: ThetaTuning<F>) -> Self {
194+
self.0.theta_tuning = theta_tuning;
195+
self
196+
}
197+
115198
/// Set the number of internal GP hyperparameter theta optimization restarts
116199
pub fn n_start(mut self, n_start: usize) -> Self {
117200
self.0.n_start = n_start;
@@ -136,18 +219,19 @@ impl<F: Float, Mean: RegressionModel<F>, Corr: CorrelationModel<F>> ParamGuard
136219
fn check_ref(&self) -> Result<&Self::Checked> {
137220
if let Some(d) = self.0.kpls_dim {
138221
if d == 0 {
139-
return Err(GpError::InvalidValue("`kpls_dim` canot be 0!".to_string()));
222+
return Err(GpError::InvalidValueError(
223+
"`kpls_dim` canot be 0!".to_string(),
224+
));
140225
}
141-
if let Some(theta) = self.0.initial_theta() {
142-
if theta.len() > 1 && d > theta.len() {
143-
return Err(GpError::InvalidValue(format!(
144-
"Dimension reduction ({}) should be smaller than expected
226+
let theta = self.0.theta_tuning().theta0();
227+
if theta.len() > 1 && d > theta.len() {
228+
return Err(GpError::InvalidValueError(format!(
229+
"Dimension reduction ({}) should be smaller than expected
145230
training input size infered from given initial theta length ({})",
146-
d,
147-
theta.len()
148-
)));
149-
};
150-
}
231+
d,
232+
theta.len()
233+
)));
234+
};
151235
}
152236
Ok(&self.0)
153237
}

0 commit comments

Comments
 (0)