use ndarray::{Array1, Array2};
use serde::Serialize;
use serde_json::Value;
use crate::canonical::{
AffineModel, GaussianNaiveBayes, GeneralizedLinearModel, LinearScoreClassifier, LinkFunction,
SvmClassifier, SvmKernel, SvmRegressor,
};
use crate::{Error, Result};
fn invalid(message: impl Into<String>) -> Error {
Error::InvalidModel(format!(
"incompatible Linfa 0.8.1 layout: {}",
message.into()
))
}
fn value<T: Serialize>(model: &T) -> Result<Value> {
serde_json::to_value(model).map_err(|error| invalid(error.to_string()))
}
fn field<'a>(value: &'a Value, name: &str) -> Result<&'a Value> {
value
.get(name)
.ok_or_else(|| invalid(format!("missing `{name}`")))
}
fn vector(value: &Value) -> Result<Vec<f64>> {
value
.as_array()
.or_else(|| value.get("data").and_then(Value::as_array))
.ok_or_else(|| invalid("expected numeric vector"))?
.iter()
.map(|item| item.as_f64().ok_or_else(|| invalid("non-numeric value")))
.collect()
}
fn matrix(value: &Value) -> Result<(usize, usize, Vec<f64>)> {
if let Some(dimensions) = value.get("dim").and_then(Value::as_array) {
let rows = dimensions
.first()
.and_then(Value::as_u64)
.ok_or_else(|| invalid("invalid matrix rows"))? as usize;
let columns = dimensions
.get(1)
.and_then(Value::as_u64)
.ok_or_else(|| invalid("invalid matrix columns"))? as usize;
return Ok((rows, columns, vector(value)?));
}
let rows = value
.as_array()
.ok_or_else(|| invalid("expected numeric matrix"))?;
let parsed = rows.iter().map(vector).collect::<Result<Vec<_>>>()?;
let columns = parsed.first().map_or(0, Vec::len);
Ok((
parsed.len(),
columns,
parsed.into_iter().flatten().collect(),
))
}
fn class_entries(value: &Value) -> Result<Vec<(i64, &Value)>> {
let mut entries = value
.as_object()
.ok_or_else(|| invalid("class_info is not an object"))?
.iter()
.map(|(label, info)| {
label
.parse::<i64>()
.map(|label| (label, info))
.map_err(|_| invalid("class label is not an integer"))
})
.collect::<Result<Vec<_>>>()?;
entries.sort_by_key(|entry| entry.0);
Ok(entries)
}
pub fn gaussian_naive_bayes<T: Serialize>(model: &T) -> Result<GaussianNaiveBayes> {
let model = value(model)?;
let entries = class_entries(field(&model, "class_info")?)?;
let means = entries
.iter()
.map(|(_, info)| vector(field(info, "theta")?))
.collect::<Result<Vec<_>>>()?;
let variances = entries
.iter()
.map(|(_, info)| vector(field(info, "sigma")?))
.collect::<Result<Vec<_>>>()?;
let features = means.first().map_or(0, Vec::len);
GaussianNaiveBayes::new(
Array2::from_shape_vec(
(entries.len(), features),
means.into_iter().flatten().collect(),
)
.map_err(|error| invalid(error.to_string()))?,
Array2::from_shape_vec(
(entries.len(), features),
variances.into_iter().flatten().collect(),
)
.map_err(|error| invalid(error.to_string()))?,
Array1::from_iter(
entries
.iter()
.map(|(_, info)| {
field(info, "prior").and_then(|value| {
value.as_f64().ok_or_else(|| invalid("invalid class prior"))
})
})
.collect::<Result<Vec<_>>>()?,
),
entries.iter().map(|entry| entry.0).collect(),
)
}
pub fn multinomial_naive_bayes<T: Serialize>(model: &T) -> Result<LinearScoreClassifier> {
let model = value(model)?;
let entries = class_entries(field(&model, "class_info")?)?;
let rows = entries
.iter()
.map(|(_, info)| vector(field(info, "feature_log_prob")?))
.collect::<Result<Vec<_>>>()?;
let features = rows.first().map_or(0, Vec::len);
let coefficients = Array2::from_shape_fn((features, entries.len()), |(feature, class)| {
rows[class][feature]
});
LinearScoreClassifier::new(
coefficients,
Array1::from_iter(
entries
.iter()
.map(|(_, info)| {
field(info, "prior").and_then(|value| {
value
.as_f64()
.map(f64::ln)
.ok_or_else(|| invalid("invalid class prior"))
})
})
.collect::<Result<Vec<_>>>()?,
),
entries.iter().map(|entry| entry.0).collect(),
None,
)
}
pub fn tweedie_regressor<T: Serialize>(model: &T) -> Result<GeneralizedLinearModel> {
let model = value(model)?;
let coefficients = Array1::from(vector(field(&model, "coef")?)?);
let intercept = field(&model, "intercept")?
.as_f64()
.ok_or_else(|| invalid("invalid intercept"))?;
let link = match field(&model, "link")?.as_str() {
Some("Identity") => LinkFunction::Identity,
Some("Log") => LinkFunction::Log,
Some("Logit") => LinkFunction::Logit,
other => return Err(invalid(format!("unknown link function {other:?}"))),
};
Ok(GeneralizedLinearModel::new(coefficients, intercept, link))
}
fn svm_kernel(kernel: &Value) -> Result<SvmKernel> {
if kernel.as_str() == Some("Linear") {
return Err(invalid(
"linear-kernel SVM stores no support vectors; use linear_svm_score",
));
}
if let Some(eps) = kernel.get("Gaussian") {
let eps = eps.as_f64().ok_or_else(|| invalid("invalid Gaussian width"))?;
if eps <= 0.0 {
return Err(invalid("non-positive Gaussian kernel width"));
}
return Ok(SvmKernel::Rbf { gamma: 1.0 / eps });
}
if let Some(parameters) = kernel.get("Polynomial").and_then(Value::as_array) {
let coef0 = parameters
.first()
.and_then(Value::as_f64)
.ok_or_else(|| invalid("invalid polynomial constant"))?;
let degree = parameters
.get(1)
.and_then(Value::as_f64)
.ok_or_else(|| invalid("invalid polynomial degree"))?;
if !degree.is_finite() || degree < 0.0 || degree.fract() != 0.0 || degree > f64::from(u32::MAX)
{
return Err(invalid("non-integer polynomial degree"));
}
return Ok(SvmKernel::Polynomial {
gamma: 1.0,
coef0,
degree: degree as u32,
});
}
Err(invalid("unsupported SVM kernel"))
}
fn support_coefficients(alpha: &Value) -> Result<Vec<f64>> {
Ok(vector(alpha)?
.into_iter()
.filter(|coefficient| coefficient.abs() > 100.0 * f64::EPSILON)
.collect())
}
pub fn svm_regressor<T: Serialize>(model: &T) -> Result<SvmRegressor> {
let model = value(model)?;
let SvmSupport {
kernel,
rho,
coefficients,
rows,
columns,
data,
} = svm_support(&model)?;
let support_vectors =
Array2::from_shape_vec((rows, columns), data).map_err(|error| invalid(error.to_string()))?;
SvmRegressor::new(support_vectors, Array1::from(coefficients), -rho, kernel, false)
}
struct SvmSupport {
kernel: SvmKernel,
rho: f64,
coefficients: Vec<f64>,
rows: usize,
columns: usize,
data: Vec<f64>,
}
fn svm_support(model: &Value) -> Result<SvmSupport> {
let kernel = svm_kernel(field(model, "kernel_method")?)?;
let rho = field(model, "rho")?
.as_f64()
.ok_or_else(|| invalid("invalid rho"))?;
let coefficients = support_coefficients(field(model, "alpha")?)?;
let support = field(model, "sep_hyperplane")?
.get("WeightedCombination")
.ok_or_else(|| invalid("nonlinear SVM is missing its support vectors"))?;
let (rows, columns, data) = matrix(support)?;
if rows != coefficients.len() {
return Err(invalid("support-vector and coefficient counts disagree"));
}
Ok(SvmSupport {
kernel,
rho,
coefficients,
rows,
columns,
data,
})
}
pub fn svm_classifier<T: Serialize>(
model: &T,
positive_label: i64,
negative_label: i64,
) -> Result<SvmClassifier> {
if positive_label == negative_label {
return Err(invalid("SVM classifier needs two distinct labels"));
}
let model = value(model)?;
let SvmSupport {
kernel,
rho,
coefficients,
rows,
columns,
data,
} = svm_support(&model)?;
let mut ordered_vectors = Vec::with_capacity(data.len());
let mut ordered_coefficients = Vec::with_capacity(coefficients.len());
let mut counts = [0usize; 2];
for (class, positive) in [true, false].into_iter().enumerate() {
for (row, &coefficient) in coefficients.iter().enumerate() {
if (coefficient > 0.0) == positive {
ordered_vectors.extend_from_slice(&data[row * columns..(row + 1) * columns]);
ordered_coefficients.push(coefficient);
counts[class] += 1;
}
}
}
let support_vectors = Array2::from_shape_vec((rows, columns), ordered_vectors)
.map_err(|error| invalid(error.to_string()))?;
let classifier = SvmClassifier {
support_vectors,
coefficients: Array1::from(ordered_coefficients),
rho: Array1::from(vec![-rho]),
vectors_per_class: counts.to_vec(),
class_labels: vec![positive_label, negative_label],
prob_a: Vec::new(),
prob_b: Vec::new(),
kernel,
};
classifier.validate()?;
Ok(classifier)
}
pub fn pls<T: Serialize>(model: &T) -> Result<AffineModel> {
let model = value(model)?;
let inner = model.get(0).unwrap_or(&model);
let (inputs, outputs, coefficient_values) = matrix(field(inner, "coefficients")?)?;
let coefficients = Array2::from_shape_vec((inputs, outputs), coefficient_values)
.map_err(|error| invalid(error.to_string()))?;
let x_mean = vector(field(inner, "x_mean")?)?;
let x_std = vector(field(inner, "x_std")?)?;
let y_mean = vector(field(inner, "y_mean")?)?;
let matrix = Array2::from_shape_fn((inputs, outputs), |(input, output)| {
coefficients[(input, output)] / x_std[input]
});
let bias = Array1::from_iter((0..outputs).map(|output| {
y_mean[output]
- (0..inputs)
.map(|input| x_mean[input] * matrix[(input, output)])
.sum::<f64>()
}));
AffineModel::new(matrix, bias)
}