Consistent formatting pass: line wrapping, import sorting, trailing whitespace removal, let-chain indentation, merged derive attributes, and unsafe block reformatting. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
188 lines
5.7 KiB
Rust
188 lines
5.7 KiB
Rust
//! Logistic Regression implementation with GPU acceleration
|
|
|
|
use crate::error::Result;
|
|
use rtx_tensor::Tensor;
|
|
|
|
/// Logistic Regression for binary classification
|
|
#[derive(Debug, Clone)]
|
|
pub struct LogisticRegression {
|
|
/// Regularization strength (C = 1/alpha)
|
|
c: f32,
|
|
/// Maximum number of iterations
|
|
max_iter: usize,
|
|
/// Learning rate for gradient descent
|
|
learning_rate: f32,
|
|
/// Tolerance for convergence
|
|
tolerance: f32,
|
|
/// Model weights (coefficients)
|
|
weights: Option<Tensor>,
|
|
/// Model intercept
|
|
intercept: Option<Tensor>,
|
|
/// Whether to fit intercept
|
|
fit_intercept: bool,
|
|
}
|
|
|
|
impl LogisticRegression {
|
|
/// Create a new Logistic regression model
|
|
pub fn new() -> Self {
|
|
Self {
|
|
c: 1.0,
|
|
max_iter: 1000,
|
|
learning_rate: 0.01,
|
|
tolerance: 1e-6,
|
|
weights: None,
|
|
intercept: None,
|
|
fit_intercept: true,
|
|
}
|
|
}
|
|
|
|
/// Set regularization strength (C = 1/alpha)
|
|
pub fn c(mut self, c: f32) -> Self {
|
|
self.c = c;
|
|
self
|
|
}
|
|
|
|
/// Set maximum iterations
|
|
pub fn max_iter(mut self, max_iter: usize) -> Self {
|
|
self.max_iter = max_iter;
|
|
self
|
|
}
|
|
|
|
/// Set learning rate
|
|
pub fn learning_rate(mut self, lr: f32) -> Self {
|
|
self.learning_rate = lr;
|
|
self
|
|
}
|
|
|
|
/// Set whether to fit intercept
|
|
pub fn fit_intercept(mut self, fit_intercept: bool) -> Self {
|
|
self.fit_intercept = fit_intercept;
|
|
self
|
|
}
|
|
|
|
/// Sigmoid activation function
|
|
fn sigmoid(&self, z: &Tensor) -> Result<Tensor> {
|
|
// sigmoid(z) = 1 / (1 + exp(-z))
|
|
let neg_z = z.neg()?;
|
|
let exp_neg_z = neg_z.exp()?;
|
|
let one_plus_exp = exp_neg_z.add_scalar(1.0)?;
|
|
one_plus_exp.reciprocal().map_err(std::convert::Into::into)
|
|
}
|
|
|
|
/// Fit the Logistic regression model using gradient descent
|
|
pub fn fit(&mut self, x: &Tensor, y: &Tensor) -> Result<&mut Self> {
|
|
let (n_samples, n_features) = (x.shape().dims()[0], x.shape().dims()[1]);
|
|
|
|
// Add intercept column if needed
|
|
let x_design = if self.fit_intercept {
|
|
let ones = Tensor::ones(vec![n_samples, 1], x.device())?;
|
|
Tensor::cat(&[x.clone(), ones], 1)?
|
|
} else {
|
|
x.clone()
|
|
};
|
|
|
|
let n_params = x_design.shape().dims()[1];
|
|
|
|
// Initialize weights to small random values
|
|
let mut weights = Tensor::randn(&[n_params, 1], x.device())?.mul_scalar(0.01)?;
|
|
|
|
let alpha = 1.0 / self.c; // Convert C to alpha
|
|
let lr = self.learning_rate;
|
|
let n_samples_f = n_samples as f32;
|
|
|
|
// Gradient descent training
|
|
for _iter in 0..self.max_iter {
|
|
// Forward pass
|
|
let z = x_design.matmul(&weights)?;
|
|
let predictions = self.sigmoid(&z)?;
|
|
|
|
// Calculate loss (cross-entropy + L2 regularization)
|
|
let y_reshaped = y.view([n_samples, 1])?;
|
|
let diff = predictions.subtract(&y_reshaped)?;
|
|
|
|
// Gradient calculation
|
|
let gradient = x_design.transpose(0, 1)?.matmul(&diff)?;
|
|
let gradient = gradient.div_scalar(n_samples_f)?;
|
|
|
|
// Add L2 regularization to gradient (except bias term if fitting intercept)
|
|
if alpha > 0.0 {
|
|
let reg_weights = if self.fit_intercept {
|
|
// Don't regularize bias term
|
|
let weight_part = weights.slice(0, 0, n_features)?;
|
|
let reg_part = weight_part.mul_scalar(alpha)?;
|
|
let zero_bias = Tensor::zeros(vec![1, 1], x.device())?;
|
|
Tensor::cat(&[reg_part, zero_bias], 0)?
|
|
} else {
|
|
weights.mul_scalar(alpha)?
|
|
};
|
|
let _gradient = gradient.add(®_weights)?;
|
|
}
|
|
|
|
// Update weights
|
|
let update = gradient.mul_scalar(lr)?;
|
|
weights = weights.subtract(&update)?;
|
|
|
|
// Check convergence (simplified - using sum of absolute gradients)
|
|
let grad_sum = gradient.abs()?.sum(None)?;
|
|
if grad_sum.to_scalar::<f32>()? < self.tolerance {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if self.fit_intercept {
|
|
// Split weights and intercept
|
|
let weights_slice = weights.slice(0, 0, n_features)?;
|
|
let intercept_slice = weights.slice(0, n_features, n_features + 1)?;
|
|
|
|
self.weights = Some(weights_slice);
|
|
self.intercept = Some(intercept_slice);
|
|
} else {
|
|
self.weights = Some(weights);
|
|
self.intercept = None;
|
|
}
|
|
|
|
Ok(self)
|
|
}
|
|
|
|
/// Predict class probabilities
|
|
pub fn predict_proba(&self, x: &Tensor) -> Result<Tensor> {
|
|
let weights = self
|
|
.weights
|
|
.as_ref()
|
|
.ok_or_else(|| crate::error::MLError::ModelNotFitted)?;
|
|
|
|
let mut z = x.matmul(weights)?;
|
|
|
|
if let Some(intercept) = &self.intercept {
|
|
z = z.add(intercept)?;
|
|
}
|
|
|
|
self.sigmoid(&z)
|
|
}
|
|
|
|
/// Make binary predictions (0 or 1)
|
|
pub fn predict(&self, x: &Tensor) -> Result<Tensor> {
|
|
let probabilities = self.predict_proba(x)?;
|
|
// Convert probabilities to binary predictions (threshold = 0.5)
|
|
probabilities
|
|
.ge_scalar(0.5)
|
|
.map_err(std::convert::Into::into)
|
|
}
|
|
|
|
/// Get model coefficients
|
|
pub fn coef(&self) -> Option<&Tensor> {
|
|
self.weights.as_ref()
|
|
}
|
|
|
|
/// Get model intercept
|
|
pub fn intercept(&self) -> Option<&Tensor> {
|
|
self.intercept.as_ref()
|
|
}
|
|
}
|
|
|
|
impl Default for LogisticRegression {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|