use crate::error::IncrementalError;
use crate::learning_rate::LearningRateSchedule;
use crate::IncrementalSupervisedEstimator;
use ndarray::{Array1, Array2};
use std::collections::BTreeSet;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MulticlassStrategy {
OneVsRest,
}
#[derive(Debug)]
pub struct IncrementalLogisticRegression {
weights: Option<Array2<f64>>, bias: Option<Array1<f64>>, schedule: LearningRateSchedule,
step_count: usize,
l2_penalty: f64,
strategy: MulticlassStrategy,
seen_classes: BTreeSet<usize>,
}
impl IncrementalLogisticRegression {
pub fn new(
schedule: LearningRateSchedule,
l2_penalty: f64,
strategy: MulticlassStrategy,
) -> Self {
Self {
weights: None,
bias: None,
schedule,
step_count: 0,
l2_penalty,
strategy,
seen_classes: BTreeSet::new(),
}
}
pub fn strategy(&self) -> &MulticlassStrategy {
&self.strategy
}
fn sigmoid(x: f64) -> f64 {
1.0 / (1.0 + (-x).exp())
}
fn ensure_class_capacity(&mut self, label: usize, n_features: usize) {
if !self.seen_classes.contains(&label) {
self.seen_classes.insert(label);
let target_rows = label + 1;
match (&mut self.weights, &mut self.bias) {
(Some(w), Some(b)) => {
if w.nrows() < target_rows {
let mut new_w = Array2::zeros((target_rows, n_features));
new_w.slice_mut(ndarray::s![..w.nrows(), ..]).assign(w);
*w = new_w;
let mut new_b = Array1::zeros(target_rows);
new_b.slice_mut(ndarray::s![..b.len()]).assign(b);
*b = new_b;
}
}
_ => {
self.weights = Some(Array2::zeros((target_rows, n_features)));
self.bias = Some(Array1::zeros(target_rows));
}
}
}
}
fn validate_batch(
&self,
batch_x: &Array2<f64>,
batch_y: &Array1<usize>,
) -> Result<(), IncrementalError> {
if batch_x.nrows() == 0 {
return Err(IncrementalError::EmptyBatch);
}
if batch_x.nrows() != batch_y.len() {
return Err(IncrementalError::TargetDimensionMismatch {
target_len: batch_y.len(),
feature_rows: batch_x.nrows(),
});
}
if batch_x.iter().any(|v| !v.is_finite()) {
return Err(IncrementalError::NonFiniteInput);
}
if let Some(ref w) = self.weights {
if batch_x.ncols() != w.ncols() {
return Err(IncrementalError::DimensionMismatch {
expected: w.ncols(),
actual: batch_x.ncols(),
});
}
}
Ok(())
}
pub fn partial_fit_labels(
&mut self,
batch_x: &Array2<f64>,
batch_y: &Array1<usize>,
) -> Result<(), IncrementalError> {
self.validate_batch(batch_x, batch_y)?;
let n_samples = batch_x.nrows() as f64;
let n_features = batch_x.ncols();
for &y in batch_y {
self.ensure_class_capacity(y, n_features);
}
let weights = self.weights.as_mut().unwrap();
let bias = self.bias.as_mut().unwrap();
let eta = self.schedule.calculate(self.step_count);
for &class_idx in &self.seen_classes {
let y_binary: Array1<f64> = batch_y
.mapv(|y| if y == class_idx { 1.0 } else { 0.0 });
let w_c = weights.row(class_idx);
let b_c = bias[class_idx];
let z = batch_x.dot(&w_c) + b_c;
let probs = z.mapv(Self::sigmoid);
let errors = &probs - &y_binary;
let mut w_grad = batch_x.t().dot(&errors) / n_samples;
if self.l2_penalty > 0.0 {
w_grad += &(&w_c * self.l2_penalty);
}
let b_grad = errors.sum() / n_samples;
let mut w_c_mut = weights.row_mut(class_idx);
w_c_mut -= &(w_grad * eta);
bias[class_idx] -= b_grad * eta;
}
self.step_count += 1;
Ok(())
}
pub fn predict_proba(&self, x: &Array2<f64>) -> Result<Array2<f64>, IncrementalError> {
if x.nrows() == 0 {
return Err(IncrementalError::EmptyBatch);
}
if x.iter().any(|v| !v.is_finite()) {
return Err(IncrementalError::NonFiniteInput);
}
let (weights, bias) = match (&self.weights, &self.bias) {
(Some(w), Some(b)) => (w, b),
_ => return Err(IncrementalError::EmptyBatch),
};
if x.ncols() != weights.ncols() {
return Err(IncrementalError::DimensionMismatch {
expected: weights.ncols(),
actual: x.ncols(),
});
}
let mut raw_logits = x.dot(&weights.t()); for (i, &b) in bias.iter().enumerate() {
raw_logits.column_mut(i).mapv_inplace(|v| Self::sigmoid(v + b));
}
Ok(raw_logits)
}
pub fn predict_labels(&self, x: &Array2<f64>) -> Result<Array1<usize>, IncrementalError> {
let probs = self.predict_proba(x)?;
let mut preds = Vec::with_capacity(x.nrows());
for row in probs.rows() {
let mut max_idx = 0;
let mut max_prob = f64::NEG_INFINITY;
for (idx, &p) in row.iter().enumerate() {
if p > max_prob {
max_prob = p;
max_idx = idx;
}
}
preds.push(max_idx);
}
Ok(Array1::from(preds))
}
}
impl IncrementalSupervisedEstimator for IncrementalLogisticRegression {
fn partial_fit(
&mut self,
batch_x: &Array2<f64>,
batch_y: &Array1<f64>,
) -> Result<(), IncrementalError> {
let labels = batch_y.mapv(|val| val.round() as usize);
self.partial_fit_labels(batch_x, &labels)
}
fn predict(&self, x: &Array2<f64>) -> Result<Array1<f64>, IncrementalError> {
let probs = self.predict_proba(x)?;
if probs.ncols() > 1 {
Ok(probs.column(1).to_owned())
} else {
Ok(probs.column(0).to_owned())
}
}
}