use crate::{optimizer::BaseOptimizer, Optimizer, OptimizerResult, OptimizerState, ParamGroup};
use parking_lot::RwLock;
use std::collections::HashMap;
use std::ops::Add;
use std::sync::Arc;
use torsh_core::error::Result;
use torsh_tensor::{
creation::{ones, zeros_like},
Tensor,
};
pub struct AdaHessian {
base: BaseOptimizer,
lr: f32,
betas: (f32, f32),
eps: f32,
weight_decay: f32,
#[allow(dead_code)]
hessian_power: f32,
update_each: usize,
#[allow(dead_code)]
n_samples: usize,
#[allow(dead_code)]
avg_conv_kernel: bool,
step_count: usize,
}
impl AdaHessian {
#[allow(clippy::too_many_arguments)]
pub fn new(
params: Vec<Arc<RwLock<Tensor>>>,
lr: Option<f32>,
betas: Option<(f32, f32)>,
eps: Option<f32>,
weight_decay: Option<f32>,
hessian_power: Option<f32>,
update_each: Option<usize>,
n_samples: Option<usize>,
avg_conv_kernel: Option<bool>,
) -> Self {
let lr = lr.unwrap_or(0.15);
let betas = betas.unwrap_or((0.9, 0.999));
let eps = eps.unwrap_or(1e-4);
let weight_decay = weight_decay.unwrap_or(0.0);
let hessian_power = hessian_power.unwrap_or(1.0);
let update_each = update_each.unwrap_or(1);
let n_samples = n_samples.unwrap_or(1);
let avg_conv_kernel = avg_conv_kernel.unwrap_or(true);
let mut defaults = HashMap::new();
defaults.insert("lr".to_string(), lr);
defaults.insert("beta1".to_string(), betas.0);
defaults.insert("beta2".to_string(), betas.1);
defaults.insert("eps".to_string(), eps);
defaults.insert("weight_decay".to_string(), weight_decay);
let param_group = ParamGroup::new(params, lr);
let base = BaseOptimizer {
param_groups: vec![param_group],
state: HashMap::new(),
optimizer_type: "AdaHessian".to_string(),
defaults,
};
Self {
base,
lr,
betas,
eps,
weight_decay,
hessian_power,
update_each,
n_samples,
avg_conv_kernel,
step_count: 0,
}
}
pub fn builder() -> AdaHessianBuilder {
AdaHessianBuilder::default()
}
#[allow(dead_code)]
fn compute_hessian_diagonal(&self, _param: &Tensor, grad: &Tensor) -> Result<Tensor> {
let hessian_diag = grad.pow(2.0)?;
if self.hessian_power != 1.0 {
Ok(hessian_diag.pow(self.hessian_power)?)
} else {
Ok(hessian_diag)
}
}
#[allow(dead_code)]
fn apply_spatial_averaging(&self, hessian: &Tensor) -> Result<Tensor> {
if !self.avg_conv_kernel {
return Ok(hessian.clone());
}
let shape = hessian.shape();
if shape.ndim() == 4 {
let dims = shape.dims();
let batch_size = dims[0];
let channels = dims[1];
let height = dims[2];
let width = dims[3];
Ok(hessian.clone())
} else if shape.ndim() == 2 {
let dims = shape.dims();
let rows = dims[0];
let cols = dims[1];
Ok(hessian.clone())
} else {
Ok(hessian.clone())
}
}
}
impl Optimizer for AdaHessian {
fn step(&mut self) -> OptimizerResult<()> {
self.step_count += 1;
let should_update_hessian = self.step_count % self.update_each == 0;
for group in &mut self.base.param_groups {
for param_arc in &group.params {
let param = param_arc.write();
if !param.has_grad() {
continue;
}
let grad = param
.grad()
.expect("gradient should exist after has_grad check");
let param_id = format!("{:p}", param_arc.as_ref());
let state = self.base.state.entry(param_id.clone()).or_insert_with(|| {
let mut state = HashMap::new();
state.insert(
"step".to_string(),
zeros_like(¶m).expect("tensor creation should succeed"),
);
state.insert(
"exp_avg".to_string(),
zeros_like(¶m).expect("tensor creation should succeed"),
);
state.insert(
"exp_hessian_diag_sq".to_string(),
zeros_like(¶m).expect("tensor creation should succeed"),
);
state
});
let mut step_tensor = state.get("step").expect("step state should exist").clone();
let mut exp_avg = state
.get("exp_avg")
.expect("exp_avg state should exist")
.clone();
let mut exp_hessian_diag_sq = state
.get("exp_hessian_diag_sq")
.expect("exp_hessian_diag_sq state should exist")
.clone();
step_tensor.add_scalar_(1.0)?;
let step = step_tensor.to_vec()?[0] as i32;
let effective_grad = if self.weight_decay != 0.0 {
let weight_decay_term = param.mul_scalar(self.weight_decay)?;
grad.add(&weight_decay_term)?
} else {
grad.clone()
};
exp_avg = exp_avg
.mul_scalar(self.betas.0)?
.add(&effective_grad.mul_scalar(1.0 - self.betas.0)?)?;
let _hessian_was_computed = should_update_hessian;
if should_update_hessian {
let _param_data = param.clone();
let grad_data = effective_grad.clone();
drop(param);
let hessian_diag = {
grad_data.pow(2.0)?
};
let hessian_diag_avg = {
hessian_diag.clone()
};
exp_hessian_diag_sq = exp_hessian_diag_sq
.mul_scalar(self.betas.1)?
.add(&hessian_diag_avg.mul_scalar(1.0 - self.betas.1)?)?;
}
let bias_correction1 = 1.0 - self.betas.0.powi(step);
let bias_correction2 = 1.0 - self.betas.1.powi(step);
let corrected_exp_avg = exp_avg.div_scalar(bias_correction1)?;
let corrected_exp_hessian_diag_sq =
exp_hessian_diag_sq.div_scalar(bias_correction2)?;
let denom = corrected_exp_hessian_diag_sq.sqrt()?.add_scalar(self.eps)?;
let update = corrected_exp_avg.div(&denom)?.mul_scalar(self.lr)?;
let mut param = param_arc.write();
*param = param.sub(&update)?;
state.insert("step".to_string(), step_tensor);
state.insert("exp_avg".to_string(), exp_avg);
state.insert("exp_hessian_diag_sq".to_string(), exp_hessian_diag_sq);
}
}
Ok(())
}
fn zero_grad(&mut self) {
self.base.zero_grad();
}
fn get_lr(&self) -> Vec<f32> {
self.base.get_lr()
}
fn set_lr(&mut self, lr: f32) {
self.lr = lr;
self.base.set_lr(lr);
}
fn add_param_group(&mut self, params: Vec<Arc<RwLock<Tensor>>>, options: HashMap<String, f32>) {
self.base.add_param_group(params, options);
}
fn parameters(&self) -> Vec<Arc<RwLock<Tensor>>> {
self.base.parameters()
}
fn state_dict(&self) -> OptimizerResult<OptimizerState> {
self.base.state_dict()
}
fn load_state_dict(&mut self, state: OptimizerState) -> OptimizerResult<()> {
self.base.load_state_dict(state)
}
}
pub struct AdaHessianBuilder {
params: Vec<Arc<RwLock<Tensor>>>,
lr: f32,
betas: (f32, f32),
eps: f32,
weight_decay: f32,
hessian_power: f32,
update_each: usize,
n_samples: usize,
avg_conv_kernel: bool,
}
impl Default for AdaHessianBuilder {
fn default() -> Self {
Self {
params: Vec::new(),
lr: 0.15,
betas: (0.9, 0.999),
eps: 1e-4,
weight_decay: 0.0,
hessian_power: 1.0,
update_each: 1,
n_samples: 1,
avg_conv_kernel: true,
}
}
}
impl AdaHessianBuilder {
pub fn params(mut self, params: Vec<Arc<RwLock<Tensor>>>) -> Self {
self.params = params;
self
}
pub fn lr(mut self, lr: f32) -> Self {
self.lr = lr;
self
}
pub fn betas(mut self, betas: (f32, f32)) -> Self {
self.betas = betas;
self
}
pub fn eps(mut self, eps: f32) -> Self {
self.eps = eps;
self
}
pub fn weight_decay(mut self, weight_decay: f32) -> Self {
self.weight_decay = weight_decay;
self
}
pub fn hessian_power(mut self, power: f32) -> Self {
self.hessian_power = power;
self
}
pub fn update_each(mut self, update_each: usize) -> Self {
self.update_each = update_each;
self
}
pub fn n_samples(mut self, n_samples: usize) -> Self {
self.n_samples = n_samples;
self
}
pub fn avg_conv_kernel(mut self, avg_conv_kernel: bool) -> Self {
self.avg_conv_kernel = avg_conv_kernel;
self
}
pub fn build(self) -> AdaHessian {
AdaHessian::new(
self.params,
Some(self.lr),
Some(self.betas),
Some(self.eps),
Some(self.weight_decay),
Some(self.hessian_power),
Some(self.update_each),
Some(self.n_samples),
Some(self.avg_conv_kernel),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use torsh_tensor::creation::randn;
#[test]
fn test_adahessian_creation() -> OptimizerResult<()> {
let param = Arc::new(RwLock::new(randn::<f32>(&[10, 10])?));
let params = vec![param];
let optimizer = AdaHessian::new(
params,
Some(0.15),
Some((0.9, 0.999)),
Some(1e-4),
Some(0.0),
Some(1.0),
Some(1),
Some(1),
Some(true),
);
assert_eq!(optimizer.lr, 0.15);
assert_eq!(optimizer.betas, (0.9, 0.999));
assert_eq!(optimizer.eps, 1e-4);
assert_eq!(optimizer.hessian_power, 1.0);
Ok(())
}
#[test]
fn test_adahessian_builder() -> OptimizerResult<()> {
let param = Arc::new(RwLock::new(randn::<f32>(&[5, 5])?));
let params = vec![param];
let optimizer = AdaHessian::builder()
.params(params)
.lr(0.1)
.betas((0.95, 0.99))
.eps(1e-6)
.weight_decay(0.01)
.hessian_power(0.5)
.update_each(2)
.n_samples(2)
.avg_conv_kernel(false)
.build();
assert_eq!(optimizer.lr, 0.1);
assert_eq!(optimizer.betas, (0.95, 0.99));
assert_eq!(optimizer.eps, 1e-6);
assert_eq!(optimizer.weight_decay, 0.01);
assert_eq!(optimizer.hessian_power, 0.5);
assert_eq!(optimizer.update_each, 2);
assert!(!optimizer.avg_conv_kernel);
Ok(())
}
#[test]
fn test_adahessian_step() -> OptimizerResult<()> {
let param = Arc::new(RwLock::new(ones(&[3, 3])?.requires_grad_(true)));
let initial_values = param.read().to_vec()?;
{
let mut p = param.write();
let grad = ones(&[3, 3])?;
p.set_grad(Some(grad));
}
let params = vec![param.clone()];
let mut optimizer =
AdaHessian::new(params, Some(0.1), None, None, None, None, None, None, None);
optimizer.step()?;
let final_values = param.read().to_vec()?;
let has_changed = initial_values
.iter()
.zip(&final_values)
.any(|(&initial, &final_val)| (initial - final_val).abs() > 1e-6);
assert!(
has_changed,
"Parameter should change after optimization step"
);
Ok(())
}
#[test]
#[allow(dead_code)]
fn test_hessian_diagonal_computation() -> OptimizerResult<()> {
let param = randn::<f32>(&[2, 2])?;
let grad = randn::<f32>(&[2, 2])?;
let optimizer = AdaHessian::new(
vec![],
Some(0.15),
None,
None,
None,
Some(1.0),
None,
None,
None,
);
let hessian_diag = optimizer.compute_hessian_diagonal(¶m, &grad)?;
let hessian_vec = hessian_diag.to_vec()?;
for &val in &hessian_vec {
assert!(
val >= 0.0,
"Hessian diagonal elements should be non-negative"
);
}
Ok(())
}
#[test]
#[allow(dead_code)]
fn test_spatial_averaging() -> OptimizerResult<()> {
let hessian_4d = randn::<f32>(&[2, 3, 4, 4])?;
let optimizer = AdaHessian::new(
vec![],
Some(0.15),
None,
None,
None,
None,
None,
None,
Some(true),
);
let averaged = optimizer.apply_spatial_averaging(&hessian_4d)?;
assert_eq!(averaged.shape(), hessian_4d.shape());
let hessian_2d = randn::<f32>(&[3, 4])?;
let unchanged = optimizer.apply_spatial_averaging(&hessian_2d)?;
assert_eq!(unchanged.shape(), hessian_2d.shape());
Ok(())
}
}