use crate::{Error, Result};
use super::super::{
LrScheduler, TrainingCallback, TrainingCallbackContext, TrainingControl, TrainingSnapshot,
};
use super::validation::ValidationMetric;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum EarlyStopMode {
#[default]
Min,
Max,
}
pub struct EarlyStopping {
patience: u64,
min_delta: f64,
mode: EarlyStopMode,
metric_name: String,
monitor: Box<dyn Fn(TrainingSnapshot) -> f64>,
best: f64,
bad_epochs: u64,
stop: bool,
}
impl EarlyStopping {
pub fn new(patience: u64, min_delta: f64, mode: EarlyStopMode) -> Result<Self> {
Self::with_monitor(patience, min_delta, mode, "loss", |snapshot| {
snapshot.epoch_mean_loss()
})
}
pub fn with_monitor(
patience: u64,
min_delta: f64,
mode: EarlyStopMode,
metric_name: impl Into<String>,
monitor: impl Fn(TrainingSnapshot) -> f64 + 'static,
) -> Result<Self> {
if !min_delta.is_finite() || min_delta < 0.0 {
return Err(Error::invalid_argument(
"early-stop minimum delta must be finite and non-negative",
));
}
let metric_name = metric_name.into();
if metric_name.is_empty() {
return Err(Error::invalid_argument(
"early-stop metric name must not be empty",
));
}
Ok(Self {
patience,
min_delta,
mode,
metric_name,
monitor: Box::new(monitor),
best: match mode {
EarlyStopMode::Min => f64::INFINITY,
EarlyStopMode::Max => f64::NEG_INFINITY,
},
bad_epochs: 0,
stop: false,
})
}
pub fn with_validation_metric(
patience: u64,
min_delta: f64,
mode: EarlyStopMode,
metric: ValidationMetric,
) -> Result<Self> {
let name = metric.name().to_owned();
Self::with_monitor(patience, min_delta, mode, name, move |_| metric.result())
}
pub const fn should_stop(&self) -> bool {
self.stop
}
pub const fn best(&self) -> f64 {
self.best
}
pub const fn bad_epochs(&self) -> u64 {
self.bad_epochs
}
}
impl Default for EarlyStopping {
fn default() -> Self {
Self::new(5, 1.0e-4, EarlyStopMode::Min).expect("the constant OA early-stop defaults are valid")
}
}
impl TrainingCallback for EarlyStopping {
fn on_epoch_end(&mut self, context: &mut TrainingCallbackContext<'_>) -> Result<TrainingControl> {
let snapshot = context.snapshot();
let metric = (self.monitor)(snapshot);
if !metric.is_finite() {
return Err(Error::callback(format!(
"early-stop metric {} is not finite",
self.metric_name
)));
}
let improved = match self.mode {
EarlyStopMode::Min => metric < self.best - self.min_delta,
EarlyStopMode::Max => metric > self.best + self.min_delta,
};
if improved {
self.best = metric;
self.bad_epochs = 0;
return Ok(TrainingControl::Continue);
}
self.bad_epochs = self.bad_epochs.saturating_add(1);
if self.bad_epochs >= self.patience {
self.stop = true;
crate::log_info!(
crate::LogComponent::ML,
"epoch {}: early stopping; {} did not improve for {} epochs (best {:.6})",
snapshot.epoch(),
self.metric_name,
self.bad_epochs,
self.best
);
Ok(TrainingControl::Stop)
} else {
Ok(TrainingControl::Continue)
}
}
}
pub struct LearningRateScheduler<'schedule> {
schedule: &'schedule dyn LrScheduler,
}
impl<'schedule> LearningRateScheduler<'schedule> {
pub const fn new(schedule: &'schedule dyn LrScheduler) -> Self {
Self { schedule }
}
}
impl TrainingCallback for LearningRateScheduler<'_> {
fn on_step_end(&mut self, context: &mut TrainingCallbackContext<'_>) -> Result<TrainingControl> {
let next_step = context.snapshot().step_count().saturating_add(1);
let learning_rate = self.schedule.learning_rate(next_step);
context.optimizer().set_learning_rate(learning_rate)?;
Ok(TrainingControl::Continue)
}
}
pub type CbEarlyStop = EarlyStopping;
pub type CbLrScheduler<'schedule> = LearningRateScheduler<'schedule>;