use bench_harness::rng::Xorshift64Star;
use fandhe_ai::Tensor;
use fandhe_ai::compat::Sequential;
use fandhe_ai::optim::{
Adadelta, AdadeltaConfig, Adagrad, AdagradConfig, Adam, AdamConfig, AdamW, AdamWConfig, Adamax,
AdamaxConfig, ConstantLr, CosineAnnealingLr, ExponentialLr, Lamb, LambConfig, LinearWarmupLr,
LrScheduler, NAdam, NAdamConfig, OneCycleAnneal, OneCycleLr, OneCycleLrConfig, RAdam,
RAdamConfig, RmsProp, RmsPropConfig, Sgd, SgdConfig, StepLr, clip_grad_norm, clip_grad_value,
};
const BATCH: usize = 4;
const D_IN: usize = 8;
const D_HIDDEN: usize = 16;
const D_OUT: usize = 4;
const SEED_DATA: u64 = 0xC0FFEE;
const SEED_L1: u64 = 0x1111_1111;
const SEED_L2: u64 = 0x2222_2222;
fn gen_regression_data(seed: u64) -> (Tensor<f32>, Tensor<f32>) {
let mut rng = Xorshift64Star::new(seed);
let x = rng.fill_vec(BATCH * D_IN);
let y = rng.fill_vec(BATCH * D_OUT);
(
Tensor::new(x, &[BATCH, D_IN])
.unwrap_or_else(|e| panic!("test fixture: x の shape 構築に失敗: {e}")),
Tensor::new(y, &[BATCH, D_OUT])
.unwrap_or_else(|e| panic!("test fixture: y の shape 構築に失敗: {e}")),
)
}
fn build_model() -> Sequential {
Sequential::new()
.add_linear(D_IN, D_HIDDEN, SEED_L1)
.unwrap_or_else(|e| panic!("test fixture: 層 1 の構築に失敗: {e}"))
.add_relu()
.add_linear(D_HIDDEN, D_OUT, SEED_L2)
.unwrap_or_else(|e| panic!("test fixture: 層 2 の構築に失敗: {e}"))
}
fn scalar(t: &Tensor<f32>) -> f32 {
t.get(&[])
.unwrap_or_else(|| panic!("test fixture: スカラー shape [] のはず"))
}
fn train_with_sgd_and_clip(
model: &mut Sequential,
steps: usize,
lr: f32,
max_norm: f32,
) -> Vec<f32> {
let (x_data, y_data) = gen_regression_data(SEED_DATA);
let mut sgd = Sgd::new(SgdConfig::new(lr))
.unwrap_or_else(|e| panic!("test fixture: Sgd::new が失敗した: {e}"));
let mut log = Vec::with_capacity(steps);
for _ in 0..steps {
let updated = {
let tape = fandhe_ai::tape();
let bound = model.bind(&tape);
let x = tape.var(&x_data);
let y = tape.var(&y_data);
let pred = bound
.forward(&tape, &x)
.unwrap_or_else(|e| panic!("test fixture: forward が失敗した: {e}"));
let loss = pred
.mse_loss(&y)
.unwrap_or_else(|e| panic!("test fixture: mse_loss が失敗した: {e}"));
log.push(scalar(&loss.to_tensor()));
let grads = tape
.backward(&loss)
.unwrap_or_else(|e| panic!("test fixture: backward が失敗した: {e}"));
let grad_refs = bound
.trainable_grads(&grads)
.unwrap_or_else(|e| panic!("test fixture: trainable_grads が失敗した: {e}"));
let clip_result = clip_grad_norm(&grad_refs, max_norm)
.unwrap_or_else(|e| panic!("test fixture: clip_grad_norm が失敗した: {e}"));
let clipped_grad_refs: Vec<&Tensor<f32>> = clip_result.grads.iter().collect();
let param_refs = model.trainable_parameters();
sgd.step(¶m_refs, &clipped_grad_refs)
.unwrap_or_else(|e| panic!("test fixture: Sgd::step が失敗した: {e}"))
};
model
.apply_parameters(updated)
.unwrap_or_else(|e| panic!("test fixture: apply_parameters が失敗した: {e}"));
}
log
}
#[test]
fn sgd_with_clip_converges_via_facade_only() {
const STEPS: usize = 100;
const LR: f32 = 0.05;
const MAX_NORM: f32 = 10.0;
let mut model = build_model();
let log = train_with_sgd_and_clip(&mut model, STEPS, LR, MAX_NORM);
assert_eq!(log.len(), STEPS);
let initial = log[0];
let final_loss = *log.last().unwrap_or_else(|| unreachable!("log は空でない"));
assert!(final_loss.is_finite(), "final loss が非有限: {final_loss}");
assert!(
final_loss < 0.5 * initial,
"loss did not converge sufficiently: initial={initial} final={final_loss}"
);
}
fn train_with_adamw_and_clip(model: &mut Sequential, steps: usize, max_norm: f32) -> Vec<f32> {
let (x_data, y_data) = gen_regression_data(SEED_DATA);
let mut adamw = AdamW::new(AdamWConfig::default())
.unwrap_or_else(|e| panic!("test fixture: AdamW::new が失敗した: {e}"));
let mut log = Vec::with_capacity(steps);
for _ in 0..steps {
let updated = {
let tape = fandhe_ai::tape();
let bound = model.bind(&tape);
let x = tape.var(&x_data);
let y = tape.var(&y_data);
let pred = bound
.forward(&tape, &x)
.unwrap_or_else(|e| panic!("test fixture: forward が失敗した: {e}"));
let loss = pred
.mse_loss(&y)
.unwrap_or_else(|e| panic!("test fixture: mse_loss が失敗した: {e}"));
log.push(scalar(&loss.to_tensor()));
let grads = tape
.backward(&loss)
.unwrap_or_else(|e| panic!("test fixture: backward が失敗した: {e}"));
let grad_refs = bound
.trainable_grads(&grads)
.unwrap_or_else(|e| panic!("test fixture: trainable_grads が失敗した: {e}"));
let clip_result = clip_grad_norm(&grad_refs, max_norm)
.unwrap_or_else(|e| panic!("test fixture: clip_grad_norm が失敗した: {e}"));
let param_refs = model.trainable_parameters();
let params_and_grads: Vec<(&Tensor<f32>, &Tensor<f32>)> = param_refs
.into_iter()
.zip(clip_result.grads.iter())
.collect();
adamw
.step(¶ms_and_grads)
.unwrap_or_else(|e| panic!("test fixture: AdamW::step が失敗した: {e}"))
};
model
.apply_parameters(updated)
.unwrap_or_else(|e| panic!("test fixture: apply_parameters が失敗した: {e}"));
}
log
}
#[test]
fn adamw_with_clip_converges_via_facade_only() {
const STEPS: usize = 100;
const MAX_NORM: f32 = 10.0;
let mut model = build_model();
let log = train_with_adamw_and_clip(&mut model, STEPS, MAX_NORM);
assert_eq!(log.len(), STEPS);
let initial = log[0];
let final_loss = *log.last().unwrap_or_else(|| unreachable!("log は空でない"));
assert!(final_loss.is_finite(), "final loss が非有限: {final_loss}");
assert!(
final_loss < 0.5 * initial,
"loss did not converge sufficiently: initial={initial} final={final_loss}"
);
}
fn train_with_adam_and_clip(model: &mut Sequential, steps: usize, max_norm: f32) -> Vec<f32> {
let (x_data, y_data) = gen_regression_data(SEED_DATA);
let cfg = AdamConfig {
weight_decay: 1e-4,
..AdamConfig::default()
};
let mut adam =
Adam::new(cfg).unwrap_or_else(|e| panic!("test fixture: Adam::new が失敗した: {e}"));
let mut log = Vec::with_capacity(steps);
for _ in 0..steps {
let updated = {
let tape = fandhe_ai::tape();
let bound = model.bind(&tape);
let x = tape.var(&x_data);
let y = tape.var(&y_data);
let pred = bound
.forward(&tape, &x)
.unwrap_or_else(|e| panic!("test fixture: forward が失敗した: {e}"));
let loss = pred
.mse_loss(&y)
.unwrap_or_else(|e| panic!("test fixture: mse_loss が失敗した: {e}"));
log.push(scalar(&loss.to_tensor()));
let grads = tape
.backward(&loss)
.unwrap_or_else(|e| panic!("test fixture: backward が失敗した: {e}"));
let grad_refs = bound
.trainable_grads(&grads)
.unwrap_or_else(|e| panic!("test fixture: trainable_grads が失敗した: {e}"));
let clip_result = clip_grad_norm(&grad_refs, max_norm)
.unwrap_or_else(|e| panic!("test fixture: clip_grad_norm が失敗した: {e}"));
let param_refs = model.trainable_parameters();
let params_and_grads: Vec<(&Tensor<f32>, &Tensor<f32>)> = param_refs
.into_iter()
.zip(clip_result.grads.iter())
.collect();
adam.step(¶ms_and_grads)
.unwrap_or_else(|e| panic!("test fixture: Adam::step が失敗した: {e}"))
};
model
.apply_parameters(updated)
.unwrap_or_else(|e| panic!("test fixture: apply_parameters が失敗した: {e}"));
}
log
}
#[test]
fn adam_with_clip_converges_via_facade_only() {
const STEPS: usize = 100;
const MAX_NORM: f32 = 10.0;
let mut model = build_model();
let log = train_with_adam_and_clip(&mut model, STEPS, MAX_NORM);
assert_eq!(log.len(), STEPS);
let initial = log[0];
let final_loss = *log.last().unwrap_or_else(|| unreachable!("log は空でない"));
assert!(final_loss.is_finite(), "final loss が非有限: {final_loss}");
assert!(
final_loss < 0.5 * initial,
"loss did not converge sufficiently: initial={initial} final={final_loss}"
);
}
fn train_with_rmsprop_and_clip(
model: &mut Sequential,
steps: usize,
lr: f32,
max_norm: f32,
) -> Vec<f32> {
let (x_data, y_data) = gen_regression_data(SEED_DATA);
let cfg = RmsPropConfig {
lr,
..RmsPropConfig::default()
};
let mut rmsprop =
RmsProp::new(cfg).unwrap_or_else(|e| panic!("test fixture: RmsProp::new が失敗した: {e}"));
let mut log = Vec::with_capacity(steps);
for _ in 0..steps {
let updated = {
let tape = fandhe_ai::tape();
let bound = model.bind(&tape);
let x = tape.var(&x_data);
let y = tape.var(&y_data);
let pred = bound
.forward(&tape, &x)
.unwrap_or_else(|e| panic!("test fixture: forward が失敗した: {e}"));
let loss = pred
.mse_loss(&y)
.unwrap_or_else(|e| panic!("test fixture: mse_loss が失敗した: {e}"));
log.push(scalar(&loss.to_tensor()));
let grads = tape
.backward(&loss)
.unwrap_or_else(|e| panic!("test fixture: backward が失敗した: {e}"));
let grad_refs = bound
.trainable_grads(&grads)
.unwrap_or_else(|e| panic!("test fixture: trainable_grads が失敗した: {e}"));
let clip_result = clip_grad_norm(&grad_refs, max_norm)
.unwrap_or_else(|e| panic!("test fixture: clip_grad_norm が失敗した: {e}"));
let param_refs = model.trainable_parameters();
let params_and_grads: Vec<(&Tensor<f32>, &Tensor<f32>)> = param_refs
.into_iter()
.zip(clip_result.grads.iter())
.collect();
rmsprop
.step(¶ms_and_grads)
.unwrap_or_else(|e| panic!("test fixture: RmsProp::step が失敗した: {e}"))
};
model
.apply_parameters(updated)
.unwrap_or_else(|e| panic!("test fixture: apply_parameters が失敗した: {e}"));
}
log
}
#[test]
fn rmsprop_with_clip_converges_via_facade_only() {
const STEPS: usize = 100;
const LR: f32 = 1e-3;
const MAX_NORM: f32 = 10.0;
let mut model = build_model();
let log = train_with_rmsprop_and_clip(&mut model, STEPS, LR, MAX_NORM);
assert_eq!(log.len(), STEPS);
let initial = log[0];
let final_loss = *log.last().unwrap_or_else(|| unreachable!("log は空でない"));
assert!(final_loss.is_finite(), "final loss が非有限: {final_loss}");
assert!(
final_loss < 0.5 * initial,
"loss did not converge sufficiently: initial={initial} final={final_loss}"
);
}
fn train_with_adagrad_and_clip(
model: &mut Sequential,
steps: usize,
lr: f32,
max_norm: f32,
) -> Vec<f32> {
let (x_data, y_data) = gen_regression_data(SEED_DATA);
let cfg = AdagradConfig {
lr,
..AdagradConfig::default()
};
let mut adagrad =
Adagrad::new(cfg).unwrap_or_else(|e| panic!("test fixture: Adagrad::new が失敗した: {e}"));
let mut log = Vec::with_capacity(steps);
for _ in 0..steps {
let updated = {
let tape = fandhe_ai::tape();
let bound = model.bind(&tape);
let x = tape.var(&x_data);
let y = tape.var(&y_data);
let pred = bound
.forward(&tape, &x)
.unwrap_or_else(|e| panic!("test fixture: forward が失敗した: {e}"));
let loss = pred
.mse_loss(&y)
.unwrap_or_else(|e| panic!("test fixture: mse_loss が失敗した: {e}"));
log.push(scalar(&loss.to_tensor()));
let grads = tape
.backward(&loss)
.unwrap_or_else(|e| panic!("test fixture: backward が失敗した: {e}"));
let grad_refs = bound
.trainable_grads(&grads)
.unwrap_or_else(|e| panic!("test fixture: trainable_grads が失敗した: {e}"));
let clip_result = clip_grad_norm(&grad_refs, max_norm)
.unwrap_or_else(|e| panic!("test fixture: clip_grad_norm が失敗した: {e}"));
let param_refs = model.trainable_parameters();
let params_and_grads: Vec<(&Tensor<f32>, &Tensor<f32>)> = param_refs
.into_iter()
.zip(clip_result.grads.iter())
.collect();
adagrad
.step(¶ms_and_grads)
.unwrap_or_else(|e| panic!("test fixture: Adagrad::step が失敗した: {e}"))
};
model
.apply_parameters(updated)
.unwrap_or_else(|e| panic!("test fixture: apply_parameters が失敗した: {e}"));
}
log
}
#[test]
fn adagrad_with_clip_converges_via_facade_only() {
const STEPS: usize = 100;
const LR: f32 = 5e-2;
const MAX_NORM: f32 = 10.0;
let mut model = build_model();
let log = train_with_adagrad_and_clip(&mut model, STEPS, LR, MAX_NORM);
assert_eq!(log.len(), STEPS);
let initial = log[0];
let final_loss = *log.last().unwrap_or_else(|| unreachable!("log は空でない"));
assert!(final_loss.is_finite(), "final loss が非有限: {final_loss}");
assert!(
final_loss < 0.5 * initial,
"loss did not converge sufficiently: initial={initial} final={final_loss}"
);
}
fn train_with_step_and_clip<F>(
model: &mut Sequential,
steps: usize,
max_norm: f32,
mut step: F,
) -> Vec<f32>
where
F: FnMut(&[(&Tensor<f32>, &Tensor<f32>)]) -> Vec<Tensor<f32>>,
{
let (x_data, y_data) = gen_regression_data(SEED_DATA);
let mut log = Vec::with_capacity(steps);
for _ in 0..steps {
let updated = {
let tape = fandhe_ai::tape();
let bound = model.bind(&tape);
let x = tape.var(&x_data);
let y = tape.var(&y_data);
let pred = bound
.forward(&tape, &x)
.unwrap_or_else(|e| panic!("test fixture: forward が失敗した: {e}"));
let loss = pred
.mse_loss(&y)
.unwrap_or_else(|e| panic!("test fixture: mse_loss が失敗した: {e}"));
log.push(scalar(&loss.to_tensor()));
let grads = tape
.backward(&loss)
.unwrap_or_else(|e| panic!("test fixture: backward が失敗した: {e}"));
let grad_refs = bound
.trainable_grads(&grads)
.unwrap_or_else(|e| panic!("test fixture: trainable_grads が失敗した: {e}"));
let clip_result = clip_grad_norm(&grad_refs, max_norm)
.unwrap_or_else(|e| panic!("test fixture: clip_grad_norm が失敗した: {e}"));
let param_refs = model.trainable_parameters();
let params_and_grads: Vec<(&Tensor<f32>, &Tensor<f32>)> = param_refs
.into_iter()
.zip(clip_result.grads.iter())
.collect();
step(¶ms_and_grads)
};
model
.apply_parameters(updated)
.unwrap_or_else(|e| panic!("test fixture: apply_parameters が失敗した: {e}"));
}
log
}
fn assert_converged(log: &[f32], steps: usize) {
assert_eq!(log.len(), steps);
let initial = log[0];
let final_loss = *log.last().unwrap_or_else(|| unreachable!("log は空でない"));
assert!(final_loss.is_finite(), "final loss が非有限: {final_loss}");
assert!(
final_loss < 0.5 * initial,
"loss did not converge sufficiently: initial={initial} final={final_loss}"
);
}
#[test]
fn adadelta_with_clip_converges_via_facade_only() {
let mut opt = Adadelta::new(AdadeltaConfig::default())
.unwrap_or_else(|e| panic!("test fixture: Adadelta::new が失敗した: {e}"));
let mut model = build_model();
let log = train_with_step_and_clip(&mut model, 100, 10.0, |pg| {
opt.step(pg)
.unwrap_or_else(|e| panic!("test fixture: Adadelta::step が失敗した: {e}"))
});
assert_converged(&log, 100);
}
#[test]
fn adamax_with_clip_converges_via_facade_only() {
let mut opt = Adamax::new(AdamaxConfig::default())
.unwrap_or_else(|e| panic!("test fixture: Adamax::new が失敗した: {e}"));
let mut model = build_model();
let log = train_with_step_and_clip(&mut model, 100, 10.0, |pg| {
opt.step(pg)
.unwrap_or_else(|e| panic!("test fixture: Adamax::step が失敗した: {e}"))
});
assert_converged(&log, 100);
}
#[test]
fn nadam_with_clip_converges_via_facade_only() {
let mut opt = NAdam::new(NAdamConfig::default())
.unwrap_or_else(|e| panic!("test fixture: NAdam::new が失敗した: {e}"));
let mut model = build_model();
let log = train_with_step_and_clip(&mut model, 100, 10.0, |pg| {
opt.step(pg)
.unwrap_or_else(|e| panic!("test fixture: NAdam::step が失敗した: {e}"))
});
assert_converged(&log, 100);
}
#[test]
fn radam_with_clip_converges_via_facade_only() {
let cfg = RAdamConfig {
lr: 0.01,
..RAdamConfig::default()
};
let mut opt =
RAdam::new(cfg).unwrap_or_else(|e| panic!("test fixture: RAdam::new が失敗した: {e}"));
let mut model = build_model();
let log = train_with_step_and_clip(&mut model, 100, 10.0, |pg| {
opt.step(pg)
.unwrap_or_else(|e| panic!("test fixture: RAdam::step が失敗した: {e}"))
});
assert_converged(&log, 100);
}
fn train_with_lamb_and_clip(model: &mut Sequential, steps: usize, max_norm: f32) -> Vec<f32> {
let (x_data, y_data) = gen_regression_data(SEED_DATA);
let cfg = LambConfig {
lr: 0.02,
..LambConfig::default()
};
let mut lamb =
Lamb::new(cfg).unwrap_or_else(|e| panic!("test fixture: Lamb::new が失敗した: {e}"));
let mut log = Vec::with_capacity(steps);
for _ in 0..steps {
let updated = {
let tape = fandhe_ai::tape();
let bound = model.bind(&tape);
let x = tape.var(&x_data);
let y = tape.var(&y_data);
let pred = bound
.forward(&tape, &x)
.unwrap_or_else(|e| panic!("test fixture: forward が失敗した: {e}"));
let loss = pred
.mse_loss(&y)
.unwrap_or_else(|e| panic!("test fixture: mse_loss が失敗した: {e}"));
log.push(scalar(&loss.to_tensor()));
let grads = tape
.backward(&loss)
.unwrap_or_else(|e| panic!("test fixture: backward が失敗した: {e}"));
let grad_refs = bound
.trainable_grads(&grads)
.unwrap_or_else(|e| panic!("test fixture: trainable_grads が失敗した: {e}"));
let clip_result = clip_grad_norm(&grad_refs, max_norm)
.unwrap_or_else(|e| panic!("test fixture: clip_grad_norm が失敗した: {e}"));
let param_refs = model.trainable_parameters();
let params_and_grads: Vec<(&Tensor<f32>, &Tensor<f32>)> = param_refs
.into_iter()
.zip(clip_result.grads.iter())
.collect();
lamb.step(¶ms_and_grads)
.unwrap_or_else(|e| panic!("test fixture: Lamb::step が失敗した: {e}"))
};
model
.apply_parameters(updated)
.unwrap_or_else(|e| panic!("test fixture: apply_parameters が失敗した: {e}"));
}
log
}
#[test]
fn lamb_with_clip_converges_via_facade_only() {
const STEPS: usize = 100;
const MAX_NORM: f32 = 10.0;
let mut model = build_model();
let log = train_with_lamb_and_clip(&mut model, STEPS, MAX_NORM);
assert_eq!(log.len(), STEPS);
let initial = log[0];
let final_loss = *log.last().unwrap_or_else(|| unreachable!("log は空でない"));
assert!(final_loss.is_finite(), "final loss が非有限: {final_loss}");
assert!(
final_loss < 0.5 * initial,
"loss did not converge sufficiently: initial={initial} final={final_loss}"
);
}
#[test]
fn clip_before_step_order_contract_is_documented_and_effective() {
const LR: f32 = 0.05;
const TINY_MAX_NORM: f32 = 1e-3;
const LARGE_MAX_NORM: f32 = 1e6;
let (x_data, y_data) = gen_regression_data(SEED_DATA);
let model = build_model();
let tape = fandhe_ai::tape();
let bound = model.bind(&tape);
let x = tape.var(&x_data);
let y = tape.var(&y_data);
let pred = bound
.forward(&tape, &x)
.unwrap_or_else(|e| panic!("test fixture: forward が失敗した: {e}"));
let loss = pred
.mse_loss(&y)
.unwrap_or_else(|e| panic!("test fixture: mse_loss が失敗した: {e}"));
let grads = tape
.backward(&loss)
.unwrap_or_else(|e| panic!("test fixture: backward が失敗した: {e}"));
let grad_refs = bound
.trainable_grads(&grads)
.unwrap_or_else(|e| panic!("test fixture: trainable_grads が失敗した: {e}"));
let tiny = clip_grad_norm(&grad_refs, TINY_MAX_NORM)
.unwrap_or_else(|e| panic!("test fixture: clip_grad_norm(tiny) が失敗した: {e}"));
let large = clip_grad_norm(&grad_refs, LARGE_MAX_NORM)
.unwrap_or_else(|e| panic!("test fixture: clip_grad_norm(large) が失敗した: {e}"));
assert!(
tiny.scaled,
"max_norm={TINY_MAX_NORM} は total_norm={} より小さいはずで scaled=true になるべき",
tiny.total_norm
);
assert!(
!large.scaled,
"max_norm={LARGE_MAX_NORM} は total_norm={} より大きいはずで scaled=false になるべき",
large.total_norm
);
let tiny_norm_sq: f32 = tiny
.grads
.iter()
.map(|g| {
g.as_slice()
.unwrap_or_else(|| unreachable!("contiguous なテンソルのはず"))
.iter()
.map(|v| v * v)
.sum::<f32>()
})
.sum();
let large_norm_sq: f32 = large
.grads
.iter()
.map(|g| {
g.as_slice()
.unwrap_or_else(|| unreachable!("contiguous なテンソルのはず"))
.iter()
.map(|v| v * v)
.sum::<f32>()
})
.sum();
assert!(
tiny_norm_sq < large_norm_sq,
"tiny クリップ後の勾配ノルムが large クリップ後より小さくなっていない\
(clip → step の順序契約の効果が確認できない): tiny={tiny_norm_sq} large={large_norm_sq}"
);
let _ = LR; }
#[test]
fn lr_scheduler_drives_sgd_config_via_facade_only() {
const STEPS: usize = 6;
const BASE_LR: f32 = 0.1;
const STEP_SIZE: usize = 2;
const GAMMA: f32 = 0.5;
let scheduler = StepLr::new(BASE_LR, STEP_SIZE, GAMMA)
.unwrap_or_else(|e| panic!("test fixture: StepLr::new が失敗した: {e}"));
let expected_lrs = [0.1_f32, 0.1, 0.05, 0.05, 0.025, 0.025];
let observed_lrs: Vec<f32> = (0..STEPS).map(|step| scheduler.lr_at(step)).collect();
assert_eq!(
observed_lrs, expected_lrs,
"StepLr::lr_at の系列が期待値と一致しない"
);
let mut model = build_model();
let (x_data, y_data) = gen_regression_data(SEED_DATA);
let mut log = Vec::with_capacity(STEPS);
for step in 0..STEPS {
let lr = scheduler.lr_at(step);
let mut sgd = Sgd::new(SgdConfig::new(lr))
.unwrap_or_else(|e| panic!("test fixture: Sgd::new が失敗した: {e}"));
let updated = {
let tape = fandhe_ai::tape();
let bound = model.bind(&tape);
let x = tape.var(&x_data);
let y = tape.var(&y_data);
let pred = bound
.forward(&tape, &x)
.unwrap_or_else(|e| panic!("test fixture: forward が失敗した: {e}"));
let loss = pred
.mse_loss(&y)
.unwrap_or_else(|e| panic!("test fixture: mse_loss が失敗した: {e}"));
log.push(scalar(&loss.to_tensor()));
let grads = tape
.backward(&loss)
.unwrap_or_else(|e| panic!("test fixture: backward が失敗した: {e}"));
let grad_refs = bound
.trainable_grads(&grads)
.unwrap_or_else(|e| panic!("test fixture: trainable_grads が失敗した: {e}"));
let param_refs = model.trainable_parameters();
sgd.step(¶m_refs, &grad_refs)
.unwrap_or_else(|e| panic!("test fixture: Sgd::step が失敗した: {e}"))
};
model
.apply_parameters(updated)
.unwrap_or_else(|e| panic!("test fixture: apply_parameters が失敗した: {e}"));
}
assert_eq!(log.len(), STEPS);
let initial = log[0];
let final_loss = *log.last().unwrap_or_else(|| unreachable!("log は空でない"));
assert!(final_loss.is_finite(), "final loss が非有限: {final_loss}");
assert!(
final_loss < initial,
"loss did not decrease: initial={initial} final={final_loss}"
);
let constant = ConstantLr::new(BASE_LR)
.unwrap_or_else(|e| panic!("test fixture: ConstantLr::new が失敗した: {e}"));
assert_eq!(constant.lr_at(0), BASE_LR);
assert_eq!(constant.lr_at(100), BASE_LR);
}
fn train_with_sgd_and_clip_value(
model: &mut Sequential,
steps: usize,
lr: f32,
clip_value: f32,
) -> Vec<f32> {
let (x_data, y_data) = gen_regression_data(SEED_DATA);
let mut sgd = Sgd::new(SgdConfig::new(lr))
.unwrap_or_else(|e| panic!("test fixture: Sgd::new が失敗した: {e}"));
let mut log = Vec::with_capacity(steps);
for _ in 0..steps {
let updated = {
let tape = fandhe_ai::tape();
let bound = model.bind(&tape);
let x = tape.var(&x_data);
let y = tape.var(&y_data);
let pred = bound
.forward(&tape, &x)
.unwrap_or_else(|e| panic!("test fixture: forward が失敗した: {e}"));
let loss = pred
.mse_loss(&y)
.unwrap_or_else(|e| panic!("test fixture: mse_loss が失敗した: {e}"));
log.push(scalar(&loss.to_tensor()));
let grads = tape
.backward(&loss)
.unwrap_or_else(|e| panic!("test fixture: backward が失敗した: {e}"));
let grad_refs = bound
.trainable_grads(&grads)
.unwrap_or_else(|e| panic!("test fixture: trainable_grads が失敗した: {e}"));
let clipped = clip_grad_value(&grad_refs, clip_value)
.unwrap_or_else(|e| panic!("test fixture: clip_grad_value が失敗した: {e}"));
let clipped_grad_refs: Vec<&Tensor<f32>> = clipped.iter().collect();
let param_refs = model.trainable_parameters();
sgd.step(¶m_refs, &clipped_grad_refs)
.unwrap_or_else(|e| panic!("test fixture: Sgd::step が失敗した: {e}"))
};
model
.apply_parameters(updated)
.unwrap_or_else(|e| panic!("test fixture: apply_parameters が失敗した: {e}"));
}
log
}
#[test]
fn sgd_with_clip_value_converges_via_facade_only() {
const STEPS: usize = 100;
const LR: f32 = 0.05;
const CLIP_VALUE: f32 = 1.0;
let mut model = build_model();
let log = train_with_sgd_and_clip_value(&mut model, STEPS, LR, CLIP_VALUE);
assert_eq!(log.len(), STEPS);
let initial = log[0];
let final_loss = *log.last().unwrap_or_else(|| unreachable!("log は空でない"));
assert!(final_loss.is_finite(), "final loss が非有限: {final_loss}");
assert!(
final_loss < 0.5 * initial,
"loss did not converge sufficiently: initial={initial} final={final_loss}"
);
}
#[test]
fn cosine_annealing_lr_scheduler_drives_sgd_config_via_facade_only() {
const STEPS: usize = 6;
const BASE_LR: f32 = 0.1;
const T_MAX: usize = 4;
let scheduler = CosineAnnealingLr::new(BASE_LR, T_MAX, 0.0)
.unwrap_or_else(|e| panic!("test fixture: CosineAnnealingLr::new が失敗した: {e}"));
let mut model = build_model();
let (x_data, y_data) = gen_regression_data(SEED_DATA);
let mut log = Vec::with_capacity(STEPS);
for step in 0..STEPS {
let lr = scheduler.lr_at(step);
let mut sgd = Sgd::new(SgdConfig::new(lr))
.unwrap_or_else(|e| panic!("test fixture: Sgd::new が失敗した: {e}"));
let updated = {
let tape = fandhe_ai::tape();
let bound = model.bind(&tape);
let x = tape.var(&x_data);
let y = tape.var(&y_data);
let pred = bound
.forward(&tape, &x)
.unwrap_or_else(|e| panic!("test fixture: forward が失敗した: {e}"));
let loss = pred
.mse_loss(&y)
.unwrap_or_else(|e| panic!("test fixture: mse_loss が失敗した: {e}"));
log.push(scalar(&loss.to_tensor()));
let grads = tape
.backward(&loss)
.unwrap_or_else(|e| panic!("test fixture: backward が失敗した: {e}"));
let grad_refs = bound
.trainable_grads(&grads)
.unwrap_or_else(|e| panic!("test fixture: trainable_grads が失敗した: {e}"));
let param_refs = model.trainable_parameters();
sgd.step(¶m_refs, &grad_refs)
.unwrap_or_else(|e| panic!("test fixture: Sgd::step が失敗した: {e}"))
};
model
.apply_parameters(updated)
.unwrap_or_else(|e| panic!("test fixture: apply_parameters が失敗した: {e}"));
}
assert_eq!(log.len(), STEPS);
let initial = log[0];
let final_loss = *log.last().unwrap_or_else(|| unreachable!("log は空でない"));
assert!(final_loss.is_finite(), "final loss が非有限: {final_loss}");
assert!(
final_loss < initial,
"loss did not decrease: initial={initial} final={final_loss}"
);
let exponential = ExponentialLr::new(BASE_LR, 0.9)
.unwrap_or_else(|e| panic!("test fixture: ExponentialLr::new が失敗した: {e}"));
let warmup = LinearWarmupLr::new(BASE_LR, T_MAX, 0.25)
.unwrap_or_else(|e| panic!("test fixture: LinearWarmupLr::new が失敗した: {e}"));
assert!((exponential.lr_at(0) - BASE_LR).abs() < 1e-6);
assert!((warmup.lr_at(T_MAX) - BASE_LR).abs() < 1e-6);
}
#[test]
fn one_cycle_lr_scheduler_drives_sgd_config_via_facade_only() {
const STEPS: usize = 6;
const MAX_LR: f32 = 0.1;
const TOTAL_STEPS: usize = STEPS;
let scheduler = OneCycleLr::new(OneCycleLrConfig::new(MAX_LR, TOTAL_STEPS))
.unwrap_or_else(|e| panic!("test fixture: OneCycleLr::new が失敗した: {e}"));
let mut model = build_model();
let (x_data, y_data) = gen_regression_data(SEED_DATA);
let mut log = Vec::with_capacity(STEPS);
for step in 0..STEPS {
let lr = scheduler.lr_at(step);
let mut sgd = Sgd::new(SgdConfig::new(lr))
.unwrap_or_else(|e| panic!("test fixture: Sgd::new が失敗した: {e}"));
let updated = {
let tape = fandhe_ai::tape();
let bound = model.bind(&tape);
let x = tape.var(&x_data);
let y = tape.var(&y_data);
let pred = bound
.forward(&tape, &x)
.unwrap_or_else(|e| panic!("test fixture: forward が失敗した: {e}"));
let loss = pred
.mse_loss(&y)
.unwrap_or_else(|e| panic!("test fixture: mse_loss が失敗した: {e}"));
log.push(scalar(&loss.to_tensor()));
let grads = tape
.backward(&loss)
.unwrap_or_else(|e| panic!("test fixture: backward が失敗した: {e}"));
let grad_refs = bound
.trainable_grads(&grads)
.unwrap_or_else(|e| panic!("test fixture: trainable_grads が失敗した: {e}"));
let param_refs = model.trainable_parameters();
sgd.step(¶m_refs, &grad_refs)
.unwrap_or_else(|e| panic!("test fixture: Sgd::step が失敗した: {e}"))
};
model
.apply_parameters(updated)
.unwrap_or_else(|e| panic!("test fixture: apply_parameters が失敗した: {e}"));
}
assert_eq!(log.len(), STEPS);
let initial = log[0];
let final_loss = *log.last().unwrap_or_else(|| unreachable!("log は空でない"));
assert!(final_loss.is_finite(), "final loss が非有限: {final_loss}");
assert!(
final_loss < initial,
"loss did not decrease: initial={initial} final={final_loss}"
);
let linear_config = OneCycleLrConfig {
anneal_strategy: OneCycleAnneal::Linear,
..OneCycleLrConfig::new(MAX_LR, TOTAL_STEPS)
};
let linear_scheduler = OneCycleLr::new(linear_config)
.unwrap_or_else(|e| panic!("test fixture: OneCycleLr::new(Linear) が失敗した: {e}"));
assert!((linear_scheduler.lr_at(0) - MAX_LR / 25.0).abs() < 1e-6);
assert!((linear_scheduler.lr_at(TOTAL_STEPS - 1) - MAX_LR / 25.0 / 1e4).abs() < 1e-6);
}