use scirs2_core::ndarray::{s, Array1, Array2, ArrayView1, ArrayView2, Axis};
use sklears_core::{
error::{Result as SklResult, SklearsError},
traits::{Estimator, Fit, Predict, Untrained},
types::Float,
};
#[derive(Debug, Clone)]
pub struct MLTSVM<S = Untrained> {
state: S,
c1: Float, c2: Float, epsilon: Float, max_iter: usize, }
#[derive(Debug, Clone)]
pub struct MLTSVMTrained {
models: Vec<TwinSVMModel>, n_labels: usize,
feature_means: Array1<Float>,
feature_stds: Array1<Float>,
}
#[derive(Debug, Clone)]
pub struct TwinSVMModel {
w1: Array1<Float>, b1: Float, w2: Array1<Float>, b2: Float, }
impl MLTSVM<Untrained> {
pub fn new() -> Self {
Self {
state: Untrained,
c1: 1.0,
c2: 1.0,
epsilon: 1e-3,
max_iter: 1000,
}
}
pub fn c1(mut self, c1: Float) -> Self {
self.c1 = c1;
self
}
pub fn c2(mut self, c2: Float) -> Self {
self.c2 = c2;
self
}
pub fn epsilon(mut self, epsilon: Float) -> Self {
self.epsilon = epsilon;
self
}
pub fn max_iter(mut self, max_iter: usize) -> Self {
self.max_iter = max_iter;
self
}
}
impl Default for MLTSVM<Untrained> {
fn default() -> Self {
Self::new()
}
}
impl Estimator for MLTSVM<Untrained> {
type Config = ();
type Error = SklearsError;
type Float = Float;
fn config(&self) -> &Self::Config {
&()
}
}
impl Fit<ArrayView2<'_, Float>, Array2<i32>> for MLTSVM<Untrained> {
type Fitted = MLTSVM<MLTSVMTrained>;
fn fit(self, x: &ArrayView2<'_, Float>, y: &Array2<i32>) -> SklResult<Self::Fitted> {
let (n_samples, _n_features) = x.dim();
let (y_samples, n_labels) = y.dim();
if n_samples != y_samples {
return Err(SklearsError::InvalidInput(
"Number of samples in X and y must match".to_string(),
));
}
if n_samples < 2 {
return Err(SklearsError::InvalidInput(
"Need at least 2 samples for SVM training".to_string(),
));
}
for sample_idx in 0..y_samples {
for label_idx in 0..n_labels {
let value = y[[sample_idx, label_idx]];
if value != 0 && value != 1 {
return Err(SklearsError::InvalidInput(format!(
"All label values must be 0 or 1, found: {}",
value
)));
}
}
}
let feature_means = x
.mean_axis(Axis(0))
.expect("array should have elements for mean computation");
let feature_stds = x
.mapv(|val| val * val)
.mean_axis(Axis(0))
.expect("array should have elements for mean computation")
- &feature_means.mapv(|mean| mean * mean);
let feature_stds = feature_stds.mapv(|var| (var.max(1e-10)).sqrt());
let mut models = Vec::new();
for label_idx in 0..n_labels {
let y_label = y.column(label_idx);
let model = self.train_twin_svm(x, &y_label, &feature_means, &feature_stds)?;
models.push(model);
}
Ok(MLTSVM {
state: MLTSVMTrained {
models,
n_labels,
feature_means,
feature_stds,
},
c1: self.c1,
c2: self.c2,
epsilon: self.epsilon,
max_iter: self.max_iter,
})
}
}
impl MLTSVM<Untrained> {
fn train_twin_svm(
&self,
x: &ArrayView2<'_, Float>,
y: &ArrayView1<'_, i32>,
feature_means: &Array1<Float>,
feature_stds: &Array1<Float>,
) -> SklResult<TwinSVMModel> {
let (n_samples, n_features) = x.dim();
let mut x_normalized = x.to_owned();
for mut row in x_normalized.rows_mut().into_iter() {
row -= feature_means;
row /= feature_stds;
}
let mut pos_samples = Vec::new();
let mut neg_samples = Vec::new();
for i in 0..n_samples {
if y[i] == 1 {
pos_samples.push(x_normalized.row(i).to_owned());
} else {
neg_samples.push(x_normalized.row(i).to_owned());
}
}
if pos_samples.is_empty() || neg_samples.is_empty() {
return Err(SklearsError::InvalidInput(
"Need both positive and negative samples for Twin SVM".to_string(),
));
}
let pos_matrix = Array2::from_shape_vec(
(pos_samples.len(), n_features),
pos_samples.into_iter().flatten().collect(),
)
.map_err(|_| SklearsError::InvalidInput("Failed to create positive matrix".to_string()))?;
let neg_matrix = Array2::from_shape_vec(
(neg_samples.len(), n_features),
neg_samples.into_iter().flatten().collect(),
)
.map_err(|_| SklearsError::InvalidInput("Failed to create negative matrix".to_string()))?;
let (w1, b1) = self.solve_twin_svm_problem(&pos_matrix, &neg_matrix, self.c1)?;
let (w2, b2) = self.solve_twin_svm_problem(&neg_matrix, &pos_matrix, self.c2)?;
Ok(TwinSVMModel { w1, b1, w2, b2 })
}
fn solve_twin_svm_problem(
&self,
target_matrix: &Array2<Float>,
other_matrix: &Array2<Float>,
c: Float,
) -> SklResult<(Array1<Float>, Float)> {
let n_target = target_matrix.nrows();
let n_other = other_matrix.nrows();
let n_features = target_matrix.ncols();
let mut w = Array1::<Float>::zeros(n_features + 1);
let learning_rate = 0.01;
for _iter in 0..self.max_iter {
let mut gradient = Array1::<Float>::zeros(n_features + 1);
for i in 0..n_target {
let x_aug = {
let mut x = Array1::ones(n_features + 1);
x.slice_mut(s![..n_features]).assign(&target_matrix.row(i));
x
};
let loss = x_aug.dot(&w);
gradient += &(x_aug * loss);
}
for i in 0..n_other {
let x_aug = {
let mut x = Array1::ones(n_features + 1);
x.slice_mut(s![..n_features]).assign(&other_matrix.row(i));
x
};
let margin = 1.0 - x_aug.dot(&w);
if margin > 0.0 {
gradient -= &(x_aug * c);
}
}
let gradient_norm = gradient.mapv(|x| x.abs()).sum();
w -= &(gradient * learning_rate);
if gradient_norm < self.epsilon {
break;
}
}
let weights = w.slice(s![..n_features]).to_owned();
let bias = w[n_features];
Ok((weights, bias))
}
}
impl Predict<ArrayView2<'_, Float>, Array2<i32>> for MLTSVM<MLTSVMTrained> {
fn predict(&self, x: &ArrayView2<'_, Float>) -> SklResult<Array2<i32>> {
let (n_samples, n_features) = x.dim();
let expected_features = self.state.feature_means.len();
if n_features != expected_features {
return Err(SklearsError::InvalidInput(format!(
"Number of features in X ({}) does not match training data ({})",
n_features, expected_features
)));
}
let mut predictions = Array2::<i32>::zeros((n_samples, self.state.n_labels));
let mut x_normalized = x.to_owned();
for mut row in x_normalized.rows_mut().into_iter() {
row -= &self.state.feature_means;
row /= &self.state.feature_stds;
}
for label_idx in 0..self.state.n_labels {
let model = &self.state.models[label_idx];
for sample_idx in 0..n_samples {
let x_sample = x_normalized.row(sample_idx);
let dist1 = (x_sample.dot(&model.w1) + model.b1).abs();
let dist2 = (x_sample.dot(&model.w2) + model.b2).abs();
predictions[[sample_idx, label_idx]] = if dist1 < dist2 { 1 } else { 0 };
}
}
Ok(predictions)
}
}
impl MLTSVM<MLTSVMTrained> {
pub fn n_labels(&self) -> usize {
self.state.n_labels
}
pub fn decision_function(&self, x: &ArrayView2<'_, Float>) -> SklResult<Array2<Float>> {
let (n_samples, _n_features) = x.dim();
let mut decision_values = Array2::<Float>::zeros((n_samples, self.state.n_labels));
let mut x_normalized = x.to_owned();
for mut row in x_normalized.rows_mut().into_iter() {
row -= &self.state.feature_means;
row /= &self.state.feature_stds;
}
for label_idx in 0..self.state.n_labels {
let model = &self.state.models[label_idx];
for sample_idx in 0..n_samples {
let x_sample = x_normalized.row(sample_idx);
let dist1 = x_sample.dot(&model.w1) + model.b1;
let dist2 = x_sample.dot(&model.w2) + model.b2;
decision_values[[sample_idx, label_idx]] = dist1 - dist2;
}
}
Ok(decision_values)
}
}
#[derive(Debug, Clone)]
pub struct RankSVM<S = Untrained> {
state: S,
c: Float, epsilon: Float, max_iter: usize, threshold_strategy: ThresholdStrategy, }
#[derive(Debug, Clone)]
pub enum ThresholdStrategy {
Fixed(Float),
OptimizeF1,
TopK(usize),
}
#[derive(Debug, Clone)]
pub struct RankSVMTrained {
models: Vec<RankingSVMModel>, thresholds: Vec<Float>, n_labels: usize,
feature_means: Array1<Float>,
feature_stds: Array1<Float>,
}
#[derive(Debug, Clone)]
pub struct RankingSVMModel {
weights: Array1<Float>,
bias: Float,
}
impl RankSVM<Untrained> {
pub fn new() -> Self {
Self {
state: Untrained,
c: 1.0,
epsilon: 1e-3,
max_iter: 1000,
threshold_strategy: ThresholdStrategy::Fixed(0.0),
}
}
pub fn c(mut self, c: Float) -> Self {
self.c = c;
self
}
pub fn epsilon(mut self, epsilon: Float) -> Self {
self.epsilon = epsilon;
self
}
pub fn max_iter(mut self, max_iter: usize) -> Self {
self.max_iter = max_iter;
self
}
pub fn threshold_strategy(mut self, strategy: ThresholdStrategy) -> Self {
self.threshold_strategy = strategy;
self
}
}
impl Default for RankSVM<Untrained> {
fn default() -> Self {
Self::new()
}
}
impl Estimator for RankSVM<Untrained> {
type Config = ();
type Error = SklearsError;
type Float = Float;
fn config(&self) -> &Self::Config {
&()
}
}
impl Fit<ArrayView2<'_, Float>, Array2<i32>> for RankSVM<Untrained> {
type Fitted = RankSVM<RankSVMTrained>;
fn fit(self, x: &ArrayView2<'_, Float>, y: &Array2<i32>) -> SklResult<Self::Fitted> {
let (n_samples, _n_features) = x.dim();
let (y_samples, n_labels) = y.dim();
if n_samples != y_samples {
return Err(SklearsError::InvalidInput(
"Number of samples in X and y must match".to_string(),
));
}
for sample_idx in 0..y_samples {
for label_idx in 0..n_labels {
let value = y[[sample_idx, label_idx]];
if value != 0 && value != 1 {
return Err(SklearsError::InvalidInput(format!(
"All label values must be 0 or 1, found: {}",
value
)));
}
}
}
let feature_means = x.mean_axis(Axis(0)).ok_or_else(|| {
SklearsError::InvalidInput("Cannot compute feature means from input data".to_string())
})?;
let squared_means = x.mapv(|val| val * val).mean_axis(Axis(0)).ok_or_else(|| {
SklearsError::InvalidInput("Cannot compute squared means from input data".to_string())
})?;
let feature_stds = squared_means - &feature_means.mapv(|mean| mean * mean);
let feature_stds = feature_stds.mapv(|var| (var.max(1e-10)).sqrt());
let mut models = Vec::new();
for label_idx in 0..n_labels {
let y_label = y.column(label_idx);
let model = self.train_ranking_svm(x, &y_label, &feature_means, &feature_stds)?;
models.push(model);
}
let thresholds = match &self.threshold_strategy {
ThresholdStrategy::Fixed(threshold) => vec![*threshold; n_labels],
ThresholdStrategy::OptimizeF1 => {
self.optimize_f1_thresholds(x, y, &models, &feature_means, &feature_stds)?
}
ThresholdStrategy::TopK(_) => vec![0.0; n_labels], };
Ok(RankSVM {
state: RankSVMTrained {
models,
thresholds,
n_labels,
feature_means,
feature_stds,
},
c: self.c,
epsilon: self.epsilon,
max_iter: self.max_iter,
threshold_strategy: self.threshold_strategy,
})
}
}
impl RankSVM<Untrained> {
fn train_ranking_svm(
&self,
x: &ArrayView2<'_, Float>,
y: &ArrayView1<'_, i32>,
feature_means: &Array1<Float>,
feature_stds: &Array1<Float>,
) -> SklResult<RankingSVMModel> {
let (n_samples, n_features) = x.dim();
let mut x_normalized = x.to_owned();
for mut row in x_normalized.rows_mut().into_iter() {
row -= feature_means;
row /= feature_stds;
}
let mut weights = Array1::<Float>::zeros(n_features);
let mut bias = 0.0;
let learning_rate = 0.01;
for _iter in 0..self.max_iter {
let mut weight_gradient = Array1::<Float>::zeros(n_features);
let mut bias_gradient = 0.0;
for i in 0..n_samples {
for j in 0..n_samples {
if y[i] > y[j] {
let x_i = x_normalized.row(i);
let x_j = x_normalized.row(j);
let x_diff = &x_i.to_owned() - &x_j.to_owned();
let score_diff = x_diff.dot(&weights) + bias;
let margin = 1.0 - score_diff;
if margin > 0.0 {
weight_gradient -= &(x_diff * self.c);
bias_gradient -= self.c;
}
}
}
}
weight_gradient += &(&weights * 2.0);
let gradient_norm = weight_gradient.mapv(|x| x.abs()).sum();
weights -= &(weight_gradient * learning_rate);
bias -= bias_gradient * learning_rate;
if gradient_norm < self.epsilon {
break;
}
}
Ok(RankingSVMModel { weights, bias })
}
fn optimize_f1_thresholds(
&self,
x: &ArrayView2<'_, Float>,
y: &Array2<i32>,
models: &[RankingSVMModel],
feature_means: &Array1<Float>,
feature_stds: &Array1<Float>,
) -> SklResult<Vec<Float>> {
let mut thresholds = Vec::new();
for (label_idx, model) in models.iter().enumerate().take(y.ncols()) {
let y_true = y.column(label_idx);
let scores = self.predict_scores_single_label(x, model, feature_means, feature_stds)?;
let threshold = self.find_optimal_f1_threshold(&y_true, &scores)?;
thresholds.push(threshold);
}
Ok(thresholds)
}
fn predict_scores_single_label(
&self,
x: &ArrayView2<'_, Float>,
model: &RankingSVMModel,
feature_means: &Array1<Float>,
feature_stds: &Array1<Float>,
) -> SklResult<Array1<Float>> {
let (n_samples, _) = x.dim();
let mut scores = Array1::<Float>::zeros(n_samples);
for i in 0..n_samples {
let x_sample = x.row(i);
let x_normalized = (&x_sample.to_owned() - feature_means) / feature_stds;
scores[i] = x_normalized.dot(&model.weights) + model.bias;
}
Ok(scores)
}
fn find_optimal_f1_threshold(
&self,
y_true: &ArrayView1<'_, i32>,
scores: &Array1<Float>,
) -> SklResult<Float> {
let mut score_threshold_pairs: Vec<(Float, i32)> = scores
.iter()
.zip(y_true.iter())
.map(|(&score, &label)| (score, label))
.collect();
score_threshold_pairs
.sort_by(|a, b| a.0.partial_cmp(&b.0).expect("operation should succeed"));
let mut best_f1 = 0.0;
let mut best_threshold = 0.0;
for &(threshold, _) in &score_threshold_pairs {
let mut tp = 0;
let mut fp = 0;
let mut fn_count = 0;
for (&score, &true_label) in scores.iter().zip(y_true.iter()) {
let predicted = if score >= threshold { 1 } else { 0 };
match (true_label, predicted) {
(1, 1) => tp += 1,
(0, 1) => fp += 1,
(1, 0) => fn_count += 1,
_ => {}
}
}
let precision = if tp + fp > 0 {
tp as Float / (tp + fp) as Float
} else {
0.0
};
let recall = if tp + fn_count > 0 {
tp as Float / (tp + fn_count) as Float
} else {
0.0
};
let f1 = if precision + recall > 0.0 {
2.0 * precision * recall / (precision + recall)
} else {
0.0
};
if f1 > best_f1 {
best_f1 = f1;
best_threshold = threshold;
}
}
Ok(best_threshold)
}
}
impl Predict<ArrayView2<'_, Float>, Array2<i32>> for RankSVM<RankSVMTrained> {
fn predict(&self, x: &ArrayView2<'_, Float>) -> SklResult<Array2<i32>> {
let (n_samples, n_features) = x.dim();
let expected_features = self.state.feature_means.len();
if n_features != expected_features {
return Err(SklearsError::InvalidInput(format!(
"Number of features in X ({}) does not match training data ({})",
n_features, expected_features
)));
}
let mut predictions = Array2::<i32>::zeros((n_samples, self.state.n_labels));
match &self.threshold_strategy {
ThresholdStrategy::TopK(k) => {
for sample_idx in 0..n_samples {
let mut scores = Vec::new();
for label_idx in 0..self.state.n_labels {
let x_sample = x.row(sample_idx);
let x_normalized = (&x_sample.to_owned() - &self.state.feature_means)
/ &self.state.feature_stds;
let score = x_normalized.dot(&self.state.models[label_idx].weights)
+ self.state.models[label_idx].bias;
scores.push((score, label_idx));
}
scores.sort_by(|a, b| b.0.partial_cmp(&a.0).expect("operation should succeed"));
for &(_, label_idx) in scores.iter().take(*k) {
predictions[[sample_idx, label_idx]] = 1;
}
}
}
_ => {
for label_idx in 0..self.state.n_labels {
let threshold = self.state.thresholds[label_idx];
for sample_idx in 0..n_samples {
let x_sample = x.row(sample_idx);
let x_normalized = (&x_sample.to_owned() - &self.state.feature_means)
/ &self.state.feature_stds;
let score = x_normalized.dot(&self.state.models[label_idx].weights)
+ self.state.models[label_idx].bias;
predictions[[sample_idx, label_idx]] =
if score >= threshold { 1 } else { 0 };
}
}
}
}
Ok(predictions)
}
}
impl RankSVM<RankSVMTrained> {
pub fn n_labels(&self) -> usize {
self.state.n_labels
}
pub fn decision_function(&self, x: &ArrayView2<'_, Float>) -> SklResult<Array2<Float>> {
let (n_samples, n_features) = x.dim();
let expected_features = self.state.feature_means.len();
if n_features != expected_features {
return Err(SklearsError::InvalidInput(format!(
"Number of features in X ({}) does not match training data ({})",
n_features, expected_features
)));
}
let mut decision_values = Array2::<Float>::zeros((n_samples, self.state.n_labels));
for sample_idx in 0..n_samples {
for label_idx in 0..self.state.n_labels {
let x_sample = x.row(sample_idx);
let x_normalized =
(&x_sample.to_owned() - &self.state.feature_means) / &self.state.feature_stds;
let score = x_normalized.dot(&self.state.models[label_idx].weights)
+ self.state.models[label_idx].bias;
decision_values[[sample_idx, label_idx]] = score;
}
}
Ok(decision_values)
}
pub fn predict_ranking(&self, x: &ArrayView2<'_, Float>) -> SklResult<Array2<usize>> {
let (n_samples, n_features) = x.dim();
let expected_features = self.state.feature_means.len();
if n_features != expected_features {
return Err(SklearsError::InvalidInput(format!(
"Number of features in X ({}) does not match training data ({})",
n_features, expected_features
)));
}
let mut rankings = Array2::<usize>::zeros((n_samples, self.state.n_labels));
for sample_idx in 0..n_samples {
let mut scores = Vec::new();
for label_idx in 0..self.state.n_labels {
let x_sample = x.row(sample_idx);
let x_normalized =
(&x_sample.to_owned() - &self.state.feature_means) / &self.state.feature_stds;
let score = x_normalized.dot(&self.state.models[label_idx].weights)
+ self.state.models[label_idx].bias;
scores.push((score, label_idx));
}
scores.sort_by(|a, b| b.0.partial_cmp(&a.0).expect("operation should succeed"));
for (rank, &(_score, label_idx)) in scores.iter().enumerate() {
rankings[[sample_idx, rank]] = label_idx;
}
}
Ok(rankings)
}
pub fn thresholds(&self) -> &Vec<Float> {
&self.state.thresholds
}
}
#[derive(Debug, Clone)]
pub struct MultiOutputSVM<S = Untrained> {
state: S,
kernel: SVMKernel,
c: Float,
epsilon: Float,
gamma: Option<Float>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SVMKernel {
Linear,
Polynomial {
degree: i32,
gamma: Float,
coef0: Float,
},
Rbf { gamma: Float },
Sigmoid { gamma: Float, coef0: Float },
}
#[derive(Debug, Clone)]
pub struct MultiOutputSVMTrained {
models: Vec<SVMModel>,
n_outputs: usize,
feature_means: Array1<Float>,
feature_stds: Array1<Float>,
}
#[derive(Debug, Clone)]
pub struct SVMModel {
support_vectors: Array2<Float>,
support_coefficients: Array1<Float>,
bias: Float,
kernel: SVMKernel,
}
impl MultiOutputSVM<Untrained> {
pub fn new() -> Self {
Self {
state: Untrained,
kernel: SVMKernel::Rbf { gamma: 1.0 },
c: 1.0,
epsilon: 1e-3,
gamma: None,
}
}
pub fn kernel(mut self, kernel: SVMKernel) -> Self {
self.kernel = kernel;
self
}
pub fn c(mut self, c: Float) -> Self {
self.c = c;
self
}
pub fn epsilon(mut self, epsilon: Float) -> Self {
self.epsilon = epsilon;
self
}
pub fn gamma(mut self, gamma: Float) -> Self {
self.gamma = Some(gamma);
self
}
}
impl Default for MultiOutputSVM<Untrained> {
fn default() -> Self {
Self::new()
}
}
impl Estimator for MultiOutputSVM<Untrained> {
type Config = ();
type Error = SklearsError;
type Float = Float;
fn config(&self) -> &Self::Config {
&()
}
}
impl Fit<ArrayView2<'_, Float>, ArrayView2<'_, Float>> for MultiOutputSVM<Untrained> {
type Fitted = MultiOutputSVM<MultiOutputSVMTrained>;
fn fit(self, x: &ArrayView2<'_, Float>, y: &ArrayView2<'_, Float>) -> SklResult<Self::Fitted> {
let (n_samples, _n_features) = x.dim();
let (y_samples, n_outputs) = y.dim();
if n_samples != y_samples {
return Err(SklearsError::InvalidInput(
"Number of samples in X and y must match".to_string(),
));
}
let feature_means = x
.mean_axis(Axis(0))
.expect("array should have elements for mean computation");
let feature_stds = x
.mapv(|val| val * val)
.mean_axis(Axis(0))
.expect("array should have elements for mean computation")
- &feature_means.mapv(|mean| mean * mean);
let feature_stds = feature_stds.mapv(|var| (var.max(1e-10)).sqrt());
let kernel = if let Some(gamma) = self.gamma {
match self.kernel {
SVMKernel::Rbf { .. } => SVMKernel::Rbf { gamma },
SVMKernel::Polynomial { degree, coef0, .. } => SVMKernel::Polynomial {
degree,
gamma,
coef0,
},
SVMKernel::Sigmoid { coef0, .. } => SVMKernel::Sigmoid { gamma, coef0 },
other => other,
}
} else {
self.kernel
};
let mut models = Vec::new();
for output_idx in 0..n_outputs {
let y_output = y.column(output_idx);
let model =
self.train_single_svm(x, &y_output, &feature_means, &feature_stds, kernel)?;
models.push(model);
}
Ok(MultiOutputSVM {
state: MultiOutputSVMTrained {
models,
n_outputs,
feature_means,
feature_stds,
},
kernel,
c: self.c,
epsilon: self.epsilon,
gamma: self.gamma,
})
}
}
impl MultiOutputSVM<Untrained> {
fn train_single_svm(
&self,
x: &ArrayView2<'_, Float>,
y: &ArrayView1<'_, Float>,
feature_means: &Array1<Float>,
feature_stds: &Array1<Float>,
kernel: SVMKernel,
) -> SklResult<SVMModel> {
let (n_samples, _n_features) = x.dim();
let mut x_normalized = x.to_owned();
for mut row in x_normalized.rows_mut().into_iter() {
row -= feature_means;
row /= feature_stds;
}
let support_vectors = x_normalized.clone();
let mut support_coefficients = Array1::<Float>::zeros(n_samples);
let y_mean = y
.mean()
.expect("array should have elements for mean computation");
for i in 0..n_samples {
support_coefficients[i] = (y[i] - y_mean) / self.c;
}
let bias = y_mean;
Ok(SVMModel {
support_vectors,
support_coefficients,
bias,
kernel,
})
}
}
impl Predict<ArrayView2<'_, Float>, Array2<Float>> for MultiOutputSVM<MultiOutputSVMTrained> {
fn predict(&self, x: &ArrayView2<'_, Float>) -> SklResult<Array2<Float>> {
let (n_samples, _) = x.dim();
let mut predictions = Array2::<Float>::zeros((n_samples, self.state.n_outputs));
let mut x_normalized = x.to_owned();
for mut row in x_normalized.rows_mut().into_iter() {
row -= &self.state.feature_means;
row /= &self.state.feature_stds;
}
for output_idx in 0..self.state.n_outputs {
let model = &self.state.models[output_idx];
for sample_idx in 0..n_samples {
let x_sample = x_normalized.row(sample_idx);
let mut prediction = model.bias;
for (sv_idx, support_vector) in model.support_vectors.rows().into_iter().enumerate()
{
let kernel_value =
compute_kernel_value(&x_sample, &support_vector, model.kernel);
prediction += model.support_coefficients[sv_idx] * kernel_value;
}
predictions[[sample_idx, output_idx]] = prediction;
}
}
Ok(predictions)
}
}
fn compute_kernel_value(
x1: &ArrayView1<Float>,
x2: &ArrayView1<Float>,
kernel: SVMKernel,
) -> Float {
match kernel {
SVMKernel::Linear => x1.dot(x2),
SVMKernel::Polynomial {
degree,
gamma,
coef0,
} => (gamma * x1.dot(x2) + coef0).powi(degree),
SVMKernel::Rbf { gamma } => {
let dist_sq = x1
.iter()
.zip(x2.iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<Float>();
(-gamma * dist_sq).exp()
}
SVMKernel::Sigmoid { gamma, coef0 } => (gamma * x1.dot(x2) + coef0).tanh(),
}
}