use crate::optim::{
Adagrad, AdagradConfig, Adam, AdamConfig, AdamW, AdamWConfig, GradScaler, GradScalerConfig,
Lamb, LambConfig, LbfgsConfig, RmsProp, RmsPropConfig, Sgd, SgdConfig,
};
use crate::{AutodiffError, Tensor};
use fandhe_ai_autodiff::Reduction;
use fandhe_ai_autodiff::loss_ops::l1_loss;
use fandhe_ai_autodiff::nn::optim::Lbfgs;
use fandhe_ai_autodiff::nn::optim::{ParamGroup, ParamGroupStep as _};
use fandhe_ai_autodiff::nn::optim::OptimizerStateDict;
use fandhe_ai_autodiff::nn::optim::amp::grad_scaler_from_state;
use fandhe_ai_tensor_core::Element;
use fandhe_ai_tensor_core::ScalarDType;
use fandhe_ai_tensor_core::data::{
DataLoader, DataLoaderConfig, Dataset, PrefetchConfig, PrefetchDataLoader, RandomSampler,
SequentialSampler, TensorDataset,
};
use std::collections::HashMap;
use super::callbacks::Callback;
use super::metrics::{ConfusionAccumulator, Metrics, MetricsResult};
use super::sequential::Sequential;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AmpDType {
F16,
Bf16,
}
impl AmpDType {
fn to_scalar_dtype(self) -> ScalarDType {
match self {
AmpDType::F16 => ScalarDType::F16,
AmpDType::Bf16 => ScalarDType::Bf16,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AmpConfig {
compute_dtype: AmpDType,
grad_scaler: GradScalerConfig,
}
impl AmpConfig {
pub fn new(compute_dtype: AmpDType) -> Self {
AmpConfig {
compute_dtype,
grad_scaler: GradScalerConfig::default(),
}
}
pub fn grad_scaler(mut self, config: GradScalerConfig) -> Self {
self.grad_scaler = config;
self
}
}
pub(super) struct AmpState {
dtype: ScalarDType,
amp_dtype: AmpDType,
grad_scaler_config: GradScalerConfig,
scaler: GradScaler,
}
impl std::fmt::Debug for AmpState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AmpState")
.field("dtype", &self.dtype)
.field("scale", &self.scaler.scale())
.finish()
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Loss {
Mse,
CrossEntropy,
L1,
Bce,
BceWithLogits,
Nll,
KlDiv,
Huber,
SmoothL1,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Optimizer {
Sgd(SgdConfig),
AdamW(AdamWConfig),
Adam(AdamConfig),
RmsProp(RmsPropConfig),
Adagrad(AdagradConfig),
Lamb(LambConfig),
Lbfgs(LbfgsConfig),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FitConfig {
pub(super) epochs: usize,
pub(super) batch_size: usize,
pub(super) shuffle: bool,
pub(super) drop_last: bool,
pub(super) accumulate_steps: u32,
pub(super) validation_split_bits: Option<u32>,
}
impl FitConfig {
pub fn new(epochs: usize, batch_size: usize) -> Self {
FitConfig {
epochs,
batch_size,
shuffle: false,
drop_last: false,
accumulate_steps: 1,
validation_split_bits: None,
}
}
pub fn shuffle(mut self, on: bool) -> Self {
self.shuffle = on;
self
}
pub fn drop_last(mut self, on: bool) -> Self {
self.drop_last = on;
self
}
pub fn accumulate_steps(mut self, n: u32) -> Self {
self.accumulate_steps = n;
self
}
pub fn validation_split(mut self, fraction: f32) -> Self {
self.validation_split_bits = Some(fraction.to_bits());
self
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct FitWeights<'a> {
class_weight: Option<HashMap<u32, f32>>,
sample_weight: Option<&'a [f32]>,
}
impl<'a> FitWeights<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn class_weight(mut self, weights: HashMap<u32, f32>) -> Self {
self.class_weight = Some(weights);
self
}
pub fn sample_weight(mut self, weights: &'a [f32]) -> Self {
self.sample_weight = Some(weights);
self
}
fn is_non_default(&self) -> bool {
self.class_weight.is_some() || self.sample_weight.is_some()
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub struct History {
pub loss: Vec<f32>,
pub val_loss: Vec<f32>,
pub lr: Vec<f32>,
pub val_metrics: Vec<MetricsResult>,
}
pub trait FitTarget: Element + private::Sealed {
#[doc(hidden)]
fn loss_for<'t>(
loss: Loss,
tape: &'t crate::Tape,
pred: &crate::Var<'t>,
target_batch: &Tensor<Self>,
) -> Result<crate::Var<'t>, AutodiffError>;
#[doc(hidden)]
fn require_metrics_support() -> Result<(), AutodiffError> {
Ok(())
}
#[doc(hidden)]
fn as_class_targets(target_batch: &Tensor<Self>) -> Option<&Tensor<i32>> {
let _ = target_batch;
None
}
#[doc(hidden)]
fn check_class_range(y: &Tensor<Self>, c: usize) -> Result<(), AutodiffError> {
let _ = (y, c);
Ok(())
}
#[doc(hidden)]
fn weighted_loss_for<'t>(
loss: Loss,
tape: &'t crate::Tape,
pred: &crate::Var<'t>,
target_batch: &Tensor<Self>,
sample: Option<&Tensor<f32>>,
class: Option<&HashMap<u32, f32>>,
) -> Result<crate::Var<'t>, AutodiffError> {
let _ = (tape, pred, target_batch, sample, class);
Err(AutodiffError::InvalidArgument(format!(
"Sequential::fit_with_weights: Loss::{loss:?} と target 型の組には重み付き損失が定義されていない"
)))
}
}
mod private {
pub trait Sealed {}
impl Sealed for f32 {}
impl Sealed for i32 {}
}
impl FitTarget for f32 {
fn loss_for<'t>(
loss: Loss,
tape: &'t crate::Tape,
pred: &crate::Var<'t>,
target_batch: &Tensor<f32>,
) -> Result<crate::Var<'t>, AutodiffError> {
let t = || tape.var_no_grad(target_batch);
match loss {
Loss::Mse => pred.mse_loss_with(&t(), Reduction::Mean),
Loss::L1 => l1_loss(pred, &t(), Reduction::Mean),
Loss::Bce => pred.bce_loss(&t(), Reduction::Mean),
Loss::BceWithLogits => pred.bce_with_logits_loss(&t(), Reduction::Mean),
Loss::KlDiv => pred.kl_div_loss(&t(), Reduction::Mean),
Loss::Huber => pred.huber_loss(&t(), 1.0, Reduction::Mean),
Loss::SmoothL1 => pred.smooth_l1_loss(&t(), 1.0, Reduction::Mean),
Loss::CrossEntropy | Loss::Nll => Err(AutodiffError::InvalidArgument(format!(
"Sequential::fit/evaluate: Loss::{loss:?} には Tensor<i32> の \
target(クラス添字)が必要(Tensor<f32> が渡された)"
))),
}
}
fn weighted_loss_for<'t>(
loss: Loss,
tape: &'t crate::Tape,
pred: &crate::Var<'t>,
target_batch: &Tensor<f32>,
sample: Option<&Tensor<f32>>,
class: Option<&HashMap<u32, f32>>,
) -> Result<crate::Var<'t>, AutodiffError> {
match loss {
Loss::Mse if class.is_none() => weighted_mse_loss(tape, pred, target_batch, sample),
_ => Err(AutodiffError::InvalidArgument(format!(
"Sequential::fit_with_weights: Loss::{loss:?}(Tensor<f32> target)には \
重み付き損失が定義されていない(sample_weight は Mse のみ・class_weight は \
CrossEntropy のみ)"
))),
}
}
fn require_metrics_support() -> Result<(), AutodiffError> {
Err(AutodiffError::InvalidArgument(
"Sequential::fit_with_metrics: metrics はクラス添字 target(Loss::CrossEntropy/\
Loss::Nll・Tensor<i32>)でのみ計算できる(Tensor<f32> target が渡された)"
.to_string(),
))
}
}
impl FitTarget for i32 {
fn loss_for<'t>(
loss: Loss,
_tape: &'t crate::Tape,
pred: &crate::Var<'t>,
target_batch: &Tensor<i32>,
) -> Result<crate::Var<'t>, AutodiffError> {
match loss {
Loss::CrossEntropy => pred.cross_entropy_loss(target_batch, 1, Reduction::Mean),
Loss::Nll => pred.nll_loss(target_batch, 1, Reduction::Mean),
Loss::Mse
| Loss::L1
| Loss::Bce
| Loss::BceWithLogits
| Loss::KlDiv
| Loss::Huber
| Loss::SmoothL1 => Err(AutodiffError::InvalidArgument(format!(
"Sequential::fit/evaluate: Loss::{loss:?} には Tensor<f32> の target が必要\
(Tensor<i32> が渡された)"
))),
}
}
fn as_class_targets(target_batch: &Tensor<i32>) -> Option<&Tensor<i32>> {
Some(target_batch)
}
fn check_class_range(y: &Tensor<i32>, c: usize) -> Result<(), AutodiffError> {
for (i, &yi) in y.host_slice().iter().enumerate() {
if usize::try_from(yi).ok().filter(|&k| k < c).is_none() {
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::fit_with_weights: target[{i}] = {yi} がクラス範囲 [0, {c}) の外"
)));
}
}
Ok(())
}
fn weighted_loss_for<'t>(
loss: Loss,
tape: &'t crate::Tape,
pred: &crate::Var<'t>,
target_batch: &Tensor<i32>,
sample: Option<&Tensor<f32>>,
class: Option<&HashMap<u32, f32>>,
) -> Result<crate::Var<'t>, AutodiffError> {
match loss {
Loss::CrossEntropy => {
weighted_cross_entropy_loss(tape, pred, target_batch, sample, class)
}
_ => Err(AutodiffError::InvalidArgument(format!(
"Sequential::fit_with_weights: Loss::{loss:?}(Tensor<i32> target)には \
重み付き損失が定義されていない(CrossEntropy のみ)"
))),
}
}
}
type FitBatch<T> =
Result<(Tensor<f32>, Tensor<T>, Option<Tensor<f32>>), fandhe_ai_tensor_core::data::DataError>;
enum FitLoader<T: Element> {
Plain(DataLoader<(TensorDataset<f32>, TensorDataset<T>)>),
Weighted(DataLoader<(TensorDataset<f32>, TensorDataset<T>, TensorDataset<f32>)>),
Prefetch(PrefetchDataLoader<(TensorDataset<f32>, TensorDataset<T>)>),
}
impl<T: Element> FitLoader<T> {
fn iter(&mut self) -> Box<dyn Iterator<Item = FitBatch<T>> + '_> {
match self {
FitLoader::Plain(l) => Box::new(l.iter().map(|b| b.map(|(x, y)| (x, y, None)))),
FitLoader::Weighted(l) => {
Box::new(l.iter().map(|b| b.map(|(x, y, w)| (x, y, Some(w)))))
}
FitLoader::Prefetch(l) => Box::new(l.iter().map(|b| b.map(|(x, y)| (x, y, None)))),
}
}
}
fn resolve_validation_split(
method: &str,
bits: Option<u32>,
x_len: Option<usize>,
y_len: Option<usize>,
has_explicit_validation: bool,
) -> Result<Option<usize>, AutodiffError> {
let Some(bits) = bits else {
return Ok(None);
};
let s = f32::from_bits(bits);
if s == 0.0 {
return Ok(None);
}
if !s.is_finite() || !(0.0..1.0).contains(&s) {
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: validation_split は 0 以上 1 未満の有限値が必要({s} が渡された)"
)));
}
if has_explicit_validation {
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: validation_split と明示の validation は併用できない"
)));
}
let (Some(n), Some(ny)) = (x_len, y_len) else {
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: validation_split には rank 1 以上の x/y が必要"
)));
};
if n != ny {
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: x のサンプル数 {n} と y のサンプル数 {ny} が一致しない"
)));
}
let split_at = (n as f64 * (1.0 - s as f64)).floor() as usize;
if split_at == 0 || split_at >= n {
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: validation_split = {s} は N = {n} では学習側({split_at} 件)か\
検証側が 0 件になる"
)));
}
Ok(Some(split_at))
}
#[allow(clippy::type_complexity)]
fn split_for_validation<T: FitTarget>(
method: &str,
x: &Tensor<f32>,
y: &Tensor<T>,
at: usize,
) -> Result<(Tensor<f32>, Tensor<T>, Tensor<f32>, Tensor<T>), AutodiffError> {
let to_err = |e: crate::ShapeError| {
AutodiffError::InvalidArgument(format!("Sequential::{method}: validation_split: {e}"))
};
let n = x.shape().first().copied().unwrap_or(0);
let rest = n.saturating_sub(at);
Ok((
x.narrow(0, 0, at).map_err(to_err)?,
y.narrow(0, 0, at).map_err(to_err)?,
x.narrow(0, at, rest).map_err(to_err)?,
y.narrow(0, at, rest).map_err(to_err)?,
))
}
fn validate_fit_weights(
method: &str,
weights: &FitWeights<'_>,
n: usize,
loss: Loss,
is_lbfgs: bool,
has_custom_step: bool,
) -> Result<(), AutodiffError> {
if !weights.is_non_default() {
return Ok(());
}
let bad = |msg: String| {
Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: {msg}"
)))
};
if let Some(sw) = weights.sample_weight {
if sw.len() != n {
return bad(format!(
"sample_weight の長さ {} が x のサンプル数 {n} と一致しない",
sw.len()
));
}
if let Some((i, v)) = sw
.iter()
.enumerate()
.find(|(_, v)| !v.is_finite() || **v < 0.0)
{
return bad(format!(
"sample_weight[{i}] = {v} は有限かつ非負でなければならない"
));
}
if !matches!(loss, Loss::CrossEntropy | Loss::Mse) {
return bad(format!(
"sample_weight は Loss::CrossEntropy/Loss::Mse でのみ有効(Loss::{loss:?} には重み付き式が定義されていない)"
));
}
}
if let Some(cw) = &weights.class_weight {
if let Some((k, v)) = cw.iter().find(|(_, v)| !v.is_finite() || **v < 0.0) {
return bad(format!(
"class_weight[{k}] = {v} は有限かつ非負でなければならない"
));
}
if !matches!(loss, Loss::CrossEntropy) {
return bad(format!(
"class_weight は Loss::CrossEntropy でのみ有効(Loss::{loss:?} が compile されている)"
));
}
}
if is_lbfgs {
return bad("非既定の重みは Optimizer::Lbfgs と併用できない".to_string());
}
if has_custom_step {
return bad("非既定の重みはカスタム学習 step フックと併用できない".to_string());
}
Ok(())
}
fn alloc_coef(len: usize) -> Result<Vec<f32>, AutodiffError> {
let mut v: Vec<f32> = Vec::new();
v.try_reserve_exact(len)
.map_err(|_| super::alloc_failed())?;
v.resize(len, 0.0);
Ok(v)
}
fn checked_f32_coef(v: f64, what: &str) -> Result<f32, AutodiffError> {
let c = v as f32;
if v.is_finite() && c.is_finite() && !(v != 0.0 && c == 0.0) {
Ok(c)
} else {
Err(AutodiffError::InvalidArgument(format!(
"Sequential::fit_with_weights: {what} から導出した損失係数 {v} が f32 で表現できない\
(overflow またはゼロへのアンダーフロー)"
)))
}
}
fn check_ce_coefs_for_all_batches(
classes: &[i32],
batch_size: usize,
drop_last: bool,
shuffle: bool,
weights: &FitWeights<'_>,
) -> Result<(), AutodiffError> {
let total = classes.len();
if total == 0 || batch_size == 0 {
return Ok(());
}
let rem = total % batch_size;
let max_nb = batch_size.min(total);
let min_nb = if rem > 0 && !drop_last { rem } else { max_nb };
for (i, &yi) in classes.iter().enumerate() {
let nb = if shuffle {
if max_nb != min_nb {
let cw = class_weight_of(weights, yi);
let sw = sample_weight_at(weights.sample_weight, i) as f64;
checked_f32_coef(
-(sw * cw as f64) / max_nb as f64,
"sample_weight × class_weight",
)?;
}
min_nb
} else {
let start = (i / batch_size) * batch_size;
let actual = batch_size.min(total - start);
if drop_last && actual < batch_size {
continue;
}
actual
};
let cw = class_weight_of(weights, yi);
let sw = sample_weight_at(weights.sample_weight, i) as f64;
checked_f32_coef(
-(sw * cw as f64) / nb as f64,
"sample_weight × class_weight",
)?;
}
Ok(())
}
fn class_weight_of(weights: &FitWeights<'_>, yi: i32) -> f32 {
weights
.class_weight
.as_ref()
.and_then(|m| u32::try_from(yi).ok().and_then(|k| m.get(&k).copied()))
.unwrap_or(1.0)
}
fn sample_weight_at(sample: Option<&[f32]>, i: usize) -> f32 {
sample.and_then(|t| t.get(i).copied()).unwrap_or(1.0)
}
fn weighted_mse_loss<'t>(
tape: &'t crate::Tape,
pred: &crate::Var<'t>,
target: &Tensor<f32>,
sample: Option<&Tensor<f32>>,
) -> Result<crate::Var<'t>, AutodiffError> {
let shape = pred.to_tensor().shape().to_vec();
if shape.as_slice() != target.shape() || shape.is_empty() {
return Err(AutodiffError::InvalidArgument(
"Sequential::fit_with_weights: 重み付き MSE は pred と target の shape が一致し \
rank 1 以上であることが必要"
.to_string(),
));
}
let n = shape[0];
let numel = shape
.iter()
.try_fold(1usize, |a, &d| a.checked_mul(d))
.ok_or_else(|| {
AutodiffError::InvalidArgument(
"Sequential::fit_with_weights: 要素数が usize を超える".to_string(),
)
})?;
if n == 0 || numel == 0 {
return Err(AutodiffError::InvalidArgument(
"Sequential::fit_with_weights: 空のバッチには重み付き損失を定義できない".to_string(),
));
}
let m = numel / n;
let sw = sample.map(|t| t.host_slice());
let mut coef = alloc_coef(numel)?;
let mut zero_mask = vec![false; numel];
for i in 0..n {
let w = sample_weight_at(sw.as_deref(), i);
let s = checked_f32_coef((w as f64 / (n as f64 * m as f64)).sqrt(), "sample_weight")?;
coef[i * m..(i + 1) * m].fill(s);
if s == 0.0 {
zero_mask[i * m..(i + 1) * m].fill(true);
}
}
let coef_t = Tensor::new(coef, &shape).map_err(AutodiffError::Shape)?;
let coef_v = tape.var_no_grad(&coef_t);
let target_v = tape.var_no_grad(target);
let pred_t = pred.to_tensor();
let overflow_mask: Vec<bool> = pred_t
.host_slice()
.iter()
.zip(target.host_slice().iter())
.zip(zero_mask.iter())
.map(|((&p, &t), &z)| !z && !(p - t).is_finite())
.collect();
let mut scaled = if overflow_mask.iter().any(|&b| b) {
let keep: Vec<bool> = overflow_mask.iter().map(|&b| !b).collect();
let ov_t = Tensor::new(overflow_mask, &shape).map_err(AutodiffError::Shape)?;
let keep_t = Tensor::new(keep, &shape).map_err(AutodiffError::Shape)?;
let a = pred.sub(&target_v)?.mul(&coef_v)?.masked_fill(&ov_t, 0.0)?;
let b = pred
.mul(&coef_v)?
.sub(&target_v.mul(&coef_v)?)?
.masked_fill(&keep_t, 0.0)?;
a.add(&b)?
} else {
pred.sub(&target_v)?.mul(&coef_v)?
};
if zero_mask.iter().any(|&b| b) {
let mask_t = Tensor::new(zero_mask, &shape).map_err(AutodiffError::Shape)?;
scaled = scaled.masked_fill(&mask_t, 0.0)?;
}
scaled.mul(&scaled)?.sum(None)
}
fn weighted_cross_entropy_loss<'t>(
tape: &'t crate::Tape,
pred: &crate::Var<'t>,
target: &Tensor<i32>,
sample: Option<&Tensor<f32>>,
class: Option<&HashMap<u32, f32>>,
) -> Result<crate::Var<'t>, AutodiffError> {
let shape = pred.to_tensor().shape().to_vec();
let (n, c) = match shape.as_slice() {
[n, c] if *n > 0 && *c > 0 => (*n, *c),
_ => {
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::fit_with_weights: 重み付き CrossEntropy の logits は shape [N, C]\
(N, C > 0)が必要({shape:?} が渡された)"
)));
}
};
if target.numel() != n {
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::fit_with_weights: target の要素数 {} が logits の N = {n} と一致しない",
target.numel()
)));
}
if let Some(class) = class
&& let Some(bad) = class.keys().find(|&&k| k as usize >= c)
{
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::fit_with_weights: class_weight のキー {bad} がクラス数 C = {c} 以上"
)));
}
let total = n.checked_mul(c).ok_or_else(|| {
AutodiffError::InvalidArgument(
"Sequential::fit_with_weights: N×C が usize を超える".to_string(),
)
})?;
let _ = total;
let sw = sample.map(|t| t.host_slice());
let mut coef = alloc_coef(n)?;
let mut zero_mask = vec![false; n];
let mut idx: Vec<i32> = Vec::new();
idx.try_reserve_exact(n)
.map_err(|_| super::alloc_failed())?;
let ys = target.host_slice();
for (i, &yi) in ys.iter().enumerate() {
let cls = usize::try_from(yi).ok().filter(|&k| k < c).ok_or_else(|| {
AutodiffError::InvalidArgument(format!(
"Sequential::fit_with_weights: target[{i}] = {yi} がクラス範囲 [0, {c}) の外"
))
})?;
let cw = class
.and_then(|m| m.get(&(cls as u32)).copied())
.unwrap_or(1.0);
let sw_i = sample_weight_at(sw.as_deref(), i) as f64;
let c = checked_f32_coef(
-(sw_i * cw as f64) / n as f64,
"sample_weight × class_weight",
)?;
coef[i] = c;
zero_mask[i] = c == 0.0;
idx.push(yi);
}
let coef_t = Tensor::new(coef, &[n, 1]).map_err(AutodiffError::Shape)?;
let idx_t = Tensor::new(idx, &[n, 1]).map_err(AutodiffError::Shape)?;
let coef_v = tape.var_no_grad(&coef_t);
let any_zero = zero_mask.iter().any(|&b| b);
let mask_t = Tensor::new(zero_mask, &[n, 1]).map_err(AutodiffError::Shape)?;
let logits = if any_zero {
pred.masked_fill(&mask_t, 0.0)?
} else {
*pred
};
let mut picked = logits.log_softmax(1)?.gather(1, &idx_t)?;
if any_zero {
picked = picked.masked_fill(&mask_t, 0.0)?;
}
picked.mul(&coef_v)?.sum(None)
}
pub(super) enum OptimizerState {
Sgd(Sgd),
AdamW(AdamW),
Adam(Adam),
RmsProp(RmsProp),
Adagrad(Adagrad),
Lamb(Lamb),
Lbfgs(Lbfgs),
}
impl std::fmt::Debug for OptimizerState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OptimizerState::Sgd(sgd) => f.debug_tuple("Sgd").field(sgd).finish(),
OptimizerState::AdamW(_) => f.debug_tuple("AdamW").finish(),
OptimizerState::Adam(_) => f.debug_tuple("Adam").finish(),
OptimizerState::RmsProp(_) => f.debug_tuple("RmsProp").finish(),
OptimizerState::Adagrad(_) => f.debug_tuple("Adagrad").finish(),
OptimizerState::Lamb(_) => f.debug_tuple("Lamb").finish(),
OptimizerState::Lbfgs(_) => f.debug_tuple("Lbfgs").finish(),
}
}
}
impl OptimizerState {
pub(super) fn new(optimizer: Optimizer) -> Result<Self, AutodiffError> {
match optimizer {
Optimizer::Sgd(config) => Ok(OptimizerState::Sgd(Sgd::new(config)?)),
Optimizer::AdamW(config) => Ok(OptimizerState::AdamW(AdamW::new(config)?)),
Optimizer::Adam(config) => Ok(OptimizerState::Adam(Adam::new(config)?)),
Optimizer::RmsProp(config) => Ok(OptimizerState::RmsProp(RmsProp::new(config)?)),
Optimizer::Adagrad(config) => Ok(OptimizerState::Adagrad(Adagrad::new(config)?)),
Optimizer::Lamb(config) => Ok(OptimizerState::Lamb(Lamb::new(config)?)),
Optimizer::Lbfgs(config) => Ok(OptimizerState::Lbfgs(Lbfgs::new(config)?)),
}
}
pub(super) fn lr(&self) -> f32 {
match self {
OptimizerState::Sgd(sgd) => sgd.config().lr,
OptimizerState::AdamW(adamw) => adamw.config().lr,
OptimizerState::Adam(adam) => adam.config().lr,
OptimizerState::RmsProp(rmsprop) => rmsprop.config().lr,
OptimizerState::Adagrad(adagrad) => adagrad.config().lr,
OptimizerState::Lamb(lamb) => lamb.config().lr,
OptimizerState::Lbfgs(lbfgs) => lbfgs.config().lr,
}
}
fn set_lr(&mut self, new_lr: f32) -> Result<(), AutodiffError> {
match self {
OptimizerState::Sgd(sgd) => sgd.set_lr(new_lr),
OptimizerState::AdamW(adamw) => adamw.set_lr(new_lr),
OptimizerState::Adam(adam) => adam.set_lr(new_lr),
OptimizerState::Lbfgs(lbfgs) => lbfgs.set_lr(new_lr),
OptimizerState::RmsProp(_) | OptimizerState::Adagrad(_) | OptimizerState::Lamb(_) => {
Err(AutodiffError::InvalidArgument(
"OptimizerState::set_lr: RmsProp/Adagrad/Lamb は set_lr を \
提供しないため LrSchedule callback と併用できない \
(Sequential::fit_with_callbacks_named の引数検査で \
通常は事前に拒否される)"
.to_string(),
))
}
}
}
pub(super) fn step(
&mut self,
params: &[&Tensor<f32>],
grads: &[&Tensor<f32>],
) -> Result<Vec<Tensor<f32>>, AutodiffError> {
if params.len() != grads.len() {
return Err(AutodiffError::InvalidArgument(format!(
"optimizer step: params.len() ({}) != \
grads.len() ({})",
params.len(),
grads.len()
)));
}
match self {
OptimizerState::Sgd(sgd) => sgd.step(params, grads),
OptimizerState::AdamW(adamw) => {
let pairs: Vec<(&Tensor<f32>, &Tensor<f32>)> =
params.iter().copied().zip(grads.iter().copied()).collect();
adamw.step(&pairs)
}
OptimizerState::Adam(adam) => {
let pairs: Vec<(&Tensor<f32>, &Tensor<f32>)> =
params.iter().copied().zip(grads.iter().copied()).collect();
adam.step(&pairs)
}
OptimizerState::RmsProp(rmsprop) => {
let pairs: Vec<(&Tensor<f32>, &Tensor<f32>)> =
params.iter().copied().zip(grads.iter().copied()).collect();
rmsprop.step(&pairs)
}
OptimizerState::Adagrad(adagrad) => {
let pairs: Vec<(&Tensor<f32>, &Tensor<f32>)> =
params.iter().copied().zip(grads.iter().copied()).collect();
adagrad.step(&pairs)
}
OptimizerState::Lamb(lamb) => {
let pairs: Vec<(&Tensor<f32>, &Tensor<f32>)> =
params.iter().copied().zip(grads.iter().copied()).collect();
lamb.step(&pairs)
}
OptimizerState::Lbfgs(_) => Err(AutodiffError::InvalidArgument(
"OptimizerState::step: Lbfgs は closure 駆動の \
try_step_closure のみで更新するため、この汎用 step 経由の \
呼び出しには非対応(Sequential::run_fit は Lbfgs 専用の \
lbfgs_batch_step 経路へ分岐する)"
.to_string(),
)),
}
}
fn step_grouped(
&mut self,
params: &[&Tensor<f32>],
grads: &[&Tensor<f32>],
groups: &[ParamGroup],
) -> Result<Vec<Tensor<f32>>, AutodiffError> {
match self {
OptimizerState::Sgd(o) => o.step_with_groups(params, grads, groups),
OptimizerState::AdamW(o) => o.step_with_groups(params, grads, groups),
OptimizerState::Adam(o) => o.step_with_groups(params, grads, groups),
OptimizerState::RmsProp(o) => o.step_with_groups(params, grads, groups),
OptimizerState::Adagrad(o) => o.step_with_groups(params, grads, groups),
OptimizerState::Lamb(o) => o.step_with_groups(params, grads, groups),
OptimizerState::Lbfgs(_) => Err(AutodiffError::InvalidArgument(
"OptimizerState::step_grouped: Lbfgs は param groups と併用できない \
(compile_with_param_groups が compile 時に拒否する)"
.to_string(),
)),
}
}
pub(super) fn step_dispatch(
&mut self,
params: &[&Tensor<f32>],
grads: &[&Tensor<f32>],
groups: &[ParamGroup],
) -> Result<Vec<Tensor<f32>>, AutodiffError> {
if groups.is_empty() {
self.step(params, grads)
} else {
self.step_grouped(params, grads, groups)
}
}
fn supports_lr_schedule(&self) -> bool {
!matches!(
self,
OptimizerState::RmsProp(_) | OptimizerState::Adagrad(_) | OptimizerState::Lamb(_)
)
}
}
#[derive(Debug)]
pub(super) struct Compiled {
pub(super) optimizer: OptimizerState,
pub(super) loss: Loss,
pub(super) amp: Option<AmpState>,
pub(super) param_groups: Vec<ParamGroup>,
}
pub(super) struct CompiledSnapshot {
pub(super) loss: Loss,
pub(super) optimizer: Optimizer,
pub(super) optimizer_state: HashMap<String, Tensor<f32>>,
pub(super) lbfgs_history_len: Option<usize>,
pub(super) amp: Option<AmpSnapshot>,
}
pub(super) struct AmpSnapshot {
pub(super) dtype: AmpDType,
pub(super) grad_scaler_config: GradScalerConfig,
pub(super) scale: f32,
pub(super) growth_tracker: u64,
}
impl Sequential {
pub(super) fn snapshot_compiled(&self) -> Result<Option<CompiledSnapshot>, AutodiffError> {
self.compiled.as_ref().map(snapshot_of_compiled).transpose()
}
fn lbfgs_slot_shapes(&self) -> Vec<Vec<usize>> {
self.trainable_parameters()
.iter()
.map(|p| p.shape().to_vec())
.collect()
}
pub(super) fn check_lbfgs_restorable(
&self,
snap: &CompiledSnapshot,
) -> Result<(), AutodiffError> {
let Optimizer::Lbfgs(config) = snap.optimizer else {
return Ok(());
};
let Some(n) = snap.lbfgs_history_len else {
return Err(AutodiffError::InvalidArgument(
"Lbfgs の snapshot に履歴件数がありません".to_string(),
));
};
Lbfgs::new(config)?.load_state_dict(
snap.optimizer_state.clone(),
&self.lbfgs_slot_shapes(),
n,
)
}
pub(super) fn restore_compiled(&mut self, snap: CompiledSnapshot) -> Result<(), AutodiffError> {
let slot_shapes = self.lbfgs_slot_shapes();
self.compiled = Some(compiled_from_snapshot(snap, &slot_shapes)?);
Ok(())
}
}
pub(super) fn snapshot_of_compiled(compiled: &Compiled) -> Result<CompiledSnapshot, AutodiffError> {
if !compiled.param_groups.is_empty() {
return Err(AutodiffError::InvalidArgument(
"save_model: param groups を持つ compile 状態(compile_with_param_groups)は \
保存に未対応。重みだけなら state_dict 系を使う"
.to_string(),
));
}
let (optimizer, optimizer_state, lbfgs_history_len) = match &compiled.optimizer {
OptimizerState::Sgd(o) => (Optimizer::Sgd(*o.config()), o.state_dict()?, None),
OptimizerState::AdamW(o) => (Optimizer::AdamW(*o.config()), o.state_dict()?, None),
OptimizerState::Adam(o) => (Optimizer::Adam(*o.config()), o.state_dict()?, None),
OptimizerState::RmsProp(o) => (Optimizer::RmsProp(*o.config()), o.state_dict()?, None),
OptimizerState::Adagrad(o) => (Optimizer::Adagrad(*o.config()), o.state_dict()?, None),
OptimizerState::Lamb(o) => (Optimizer::Lamb(*o.config()), o.state_dict()?, None),
OptimizerState::Lbfgs(o) => (
Optimizer::Lbfgs(*o.config()),
o.state_dict()?,
Some(o.history_len()),
),
};
let amp = compiled.amp.as_ref().map(|a| AmpSnapshot {
dtype: a.amp_dtype,
grad_scaler_config: a.grad_scaler_config,
scale: a.scaler.scale(),
growth_tracker: a.scaler.growth_tracker(),
});
Ok(CompiledSnapshot {
loss: compiled.loss,
optimizer,
optimizer_state,
lbfgs_history_len,
amp,
})
}
pub(super) fn compiled_from_snapshot(
snap: CompiledSnapshot,
slot_shapes: &[Vec<usize>],
) -> Result<Compiled, AutodiffError> {
let CompiledSnapshot {
loss,
optimizer,
optimizer_state,
lbfgs_history_len,
amp,
} = snap;
let is_lbfgs = matches!(optimizer, Optimizer::Lbfgs(_));
if is_lbfgs != lbfgs_history_len.is_some() || (is_lbfgs && amp.is_some()) {
return Err(AutodiffError::InvalidArgument(
"Sequential の compile 状態の復元: Lbfgs の履歴件数・AMP の組合せが不正です"
.to_string(),
));
}
let mut state = OptimizerState::new(optimizer)?;
match &mut state {
OptimizerState::Sgd(o) => o.load_state_dict(optimizer_state)?,
OptimizerState::AdamW(o) => o.load_state_dict(optimizer_state)?,
OptimizerState::Adam(o) => o.load_state_dict(optimizer_state)?,
OptimizerState::RmsProp(o) => o.load_state_dict(optimizer_state)?,
OptimizerState::Adagrad(o) => o.load_state_dict(optimizer_state)?,
OptimizerState::Lamb(o) => o.load_state_dict(optimizer_state)?,
OptimizerState::Lbfgs(o) => {
o.load_state_dict(optimizer_state, slot_shapes, lbfgs_history_len.unwrap_or(0))?
}
}
let amp = match amp {
None => None,
Some(a) => Some(AmpState {
dtype: a.dtype.to_scalar_dtype(),
amp_dtype: a.dtype,
grad_scaler_config: a.grad_scaler_config,
scaler: grad_scaler_from_state(a.grad_scaler_config, a.scale, a.growth_tracker)?,
}),
};
Ok(Compiled {
optimizer: state,
loss,
amp,
param_groups: Vec::new(),
})
}
fn not_compiled(method: &str) -> AutodiffError {
AutodiffError::InvalidArgument(format!(
"Sequential::{method}: compile() が呼ばれていない(optimizer/loss が未設定)"
))
}
pub type TrainStepFn<'h, T> = dyn FnMut(
&Sequential,
&Tensor<f32>,
&Tensor<T>,
&mut TrainStepOptimizer<'_>,
) -> Result<TrainStepOutput, AutodiffError>
+ 'h;
pub struct TrainStepOptimizer<'a> {
state: &'a mut OptimizerState,
groups: &'a [ParamGroup],
}
impl TrainStepOptimizer<'_> {
pub fn step(
&mut self,
params: &[&Tensor<f32>],
grads: &[&Tensor<f32>],
) -> Result<Vec<Tensor<f32>>, AutodiffError> {
self.state.step_dispatch(params, grads, self.groups)
}
#[must_use]
pub fn lr(&self) -> f32 {
self.state.lr()
}
}
#[non_exhaustive]
pub struct TrainStepOutput {
loss: f32,
updated: Option<Vec<Tensor<f32>>>,
}
impl TrainStepOutput {
#[must_use]
pub fn new(loss: f32) -> Self {
Self {
loss,
updated: None,
}
}
#[must_use]
pub fn with_updated(mut self, params: Vec<Tensor<f32>>) -> Self {
self.updated = Some(params);
self
}
}
type CustomStepOutcome = Result<(f32, Option<Vec<Tensor<f32>>>), AutodiffError>;
type CustomStepHook<'h, T> = dyn FnMut(
&Sequential,
&Tensor<f32>,
&Tensor<T>,
&mut OptimizerState,
&[ParamGroup],
) -> CustomStepOutcome
+ 'h;
fn accumulate_grads_into(
acc: &mut Vec<Tensor<f32>>,
grads: &[&Tensor<f32>],
method: &str,
) -> Result<(), AutodiffError> {
if acc.len() != grads.len() {
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: 勾配累積バッファの長さ ({}) が今回の \
勾配数 ({}) と一致しない(内部不変条件違反)",
acc.len(),
grads.len()
)));
}
let mut next: Vec<Tensor<f32>> = Vec::new();
next.try_reserve_exact(acc.len())
.map_err(|_| super::alloc_failed())?;
for (a, g) in acc.iter().zip(grads.iter()) {
if a.shape() != g.shape() {
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: 勾配累積の shape が一致しない \
(累積側: {:?}, 今回: {:?})",
a.shape(),
g.shape()
)));
}
let a_slice = a.host_slice();
let g_slice = g.host_slice();
let mut summed: Vec<f32> = Vec::new();
summed
.try_reserve_exact(a_slice.len())
.map_err(|_| super::alloc_failed())?;
summed.extend(a_slice.iter().zip(g_slice.iter()).map(|(x, y)| x + y));
next.push(Tensor::new(summed, a.shape())?);
}
*acc = next;
Ok(())
}
fn ensure_finite_grads<'a>(
grads: impl IntoIterator<Item = &'a Tensor<f32>>,
method: &str,
stage: &str,
weighted: bool,
) -> Result<(), AutodiffError> {
if grads
.into_iter()
.any(|g| g.host_slice().iter().any(|v| !v.is_finite()))
{
return Err(AutodiffError::InvalidArgument(if weighted {
format!(
"Sequential::{method}: 重み付き損失の勾配({stage})に非有限値(NaN/inf)が\
含まれるためパラメータ更新を拒否した(重み×勾配の overflow の疑い)"
)
} else {
format!(
"Sequential::{method}: 累積勾配({stage})に非有限値(NaN/inf)が\
含まれるためパラメータ更新を拒否した(勾配累積の f32 加算 overflow の疑い)"
)
}));
}
Ok(())
}
fn lbfgs_batch_step<T: FitTarget>(
model: &mut Sequential,
lbfgs: &mut Lbfgs,
loss: Loss,
x_batch: &Tensor<f32>,
y_batch: &Tensor<T>,
method: &str,
) -> Result<f32, AutodiffError> {
let snapshot: Vec<Tensor<f32>> = model.trainable_parameters().into_iter().cloned().collect();
let result = lbfgs.try_step_closure(&snapshot, |trial| {
model.apply_parameters(trial.to_vec())?;
let tape = crate::tape();
let bound = model.bind(&tape);
let x_var = tape.var(x_batch);
let pred = bound.forward_with_precision(&tape, &x_var, None)?;
let loss_var = T::loss_for(loss, &tape, &pred, y_batch)?;
let loss_scalar = loss_var.to_tensor().get(&[]).ok_or_else(|| {
AutodiffError::InvalidArgument(format!(
"Sequential::{method}: loss の shape が [] ではない\
(loss 演算の契約違反)"
))
})?;
let grads = tape.backward(&loss_var)?;
let grad_refs = bound.trainable_grads(&grads)?;
let grads_owned: Vec<Tensor<f32>> = grad_refs.into_iter().cloned().collect();
Ok((loss_scalar, grads_owned))
});
match result {
Ok(updated) => {
model.apply_parameters(updated)?;
lbfgs.last_loss().ok_or_else(|| {
AutodiffError::InvalidArgument(format!(
"Sequential::{method}: Lbfgs::try_step_closure 成功後に \
last_loss が None(内部不変条件違反)"
))
})
}
Err(err) => {
model.apply_parameters(snapshot)?;
Err(err)
}
}
}
impl Sequential {
pub fn compile(&mut self, optimizer: Optimizer, loss: Loss) -> Result<(), AutodiffError> {
self.compiled = Some(Compiled {
optimizer: OptimizerState::new(optimizer)?,
loss,
amp: None,
param_groups: Vec::new(),
});
Ok(())
}
pub fn compile_with_param_groups(
&mut self,
optimizer: Optimizer,
loss: Loss,
param_groups: &[ParamGroup],
) -> Result<(), AutodiffError> {
if !param_groups.is_empty() && matches!(optimizer, Optimizer::Lbfgs(_)) {
return Err(AutodiffError::InvalidArgument(
"Sequential::compile_with_param_groups: Optimizer::Lbfgs は空でない \
param_groups と併用できない(closure 駆動のため step_with_groups を持たない)"
.to_string(),
));
}
let optimizer_state = OptimizerState::new(optimizer)?;
self.compiled = Some(Compiled {
optimizer: optimizer_state,
loss,
amp: None,
param_groups: param_groups.to_vec(),
});
Ok(())
}
pub fn compile_with_amp(
&mut self,
optimizer: Optimizer,
loss: Loss,
amp: AmpConfig,
) -> Result<(), AutodiffError> {
if matches!(optimizer, Optimizer::Lbfgs(_)) {
return Err(AutodiffError::InvalidArgument(
"Sequential::compile_with_amp: Optimizer::Lbfgs は AMP \
(損失スケーリング)と併用できない(イシュー #2172。\
closure 駆動の outer step 内で forward/backward を \
複数回評価するため scale/unscale の適用箇所が定義でき \
ない)"
.to_string(),
));
}
let optimizer_state = OptimizerState::new(optimizer)?;
let scaler = GradScaler::new(amp.grad_scaler)?;
self.compiled = Some(Compiled {
optimizer: optimizer_state,
loss,
amp: Some(AmpState {
dtype: amp.compute_dtype.to_scalar_dtype(),
amp_dtype: amp.compute_dtype,
grad_scaler_config: amp.grad_scaler,
scaler,
}),
param_groups: Vec::new(),
});
Ok(())
}
pub fn is_compiled(&self) -> bool {
self.compiled.is_some()
}
pub fn amp_loss_scale(&self) -> Option<f32> {
self.compiled
.as_ref()?
.amp
.as_ref()
.map(|amp| amp.scaler.scale())
}
pub fn fit<T: FitTarget>(
&mut self,
x: &Tensor<f32>,
y: &Tensor<T>,
config: FitConfig,
) -> Result<History, AutodiffError> {
self.fit_with_callbacks_named(
"fit",
x,
y,
config,
&FitWeights::default(),
None,
&mut [],
&[],
None,
None,
)
}
pub fn fit_with_callbacks<T: FitTarget>(
&mut self,
x: &Tensor<f32>,
y: &Tensor<T>,
config: FitConfig,
validation: Option<(&Tensor<f32>, &Tensor<T>)>,
callbacks: &mut [Callback],
) -> Result<History, AutodiffError> {
self.fit_with_callbacks_named(
"fit_with_callbacks",
x,
y,
config,
&FitWeights::default(),
validation,
callbacks,
&[],
None,
None,
)
}
#[allow(clippy::too_many_arguments)]
pub fn fit_with_metrics<T: FitTarget>(
&mut self,
x: &Tensor<f32>,
y: &Tensor<T>,
config: FitConfig,
validation: Option<(&Tensor<f32>, &Tensor<T>)>,
callbacks: &mut [Callback],
metrics: &[Metrics],
) -> Result<History, AutodiffError> {
self.fit_with_callbacks_named(
"fit_with_metrics",
x,
y,
config,
&FitWeights::default(),
validation,
callbacks,
metrics,
None,
None,
)
}
#[allow(clippy::too_many_arguments)]
pub fn fit_with_prefetch<T: FitTarget>(
&mut self,
x: &Tensor<f32>,
y: &Tensor<T>,
config: FitConfig,
prefetch: PrefetchConfig,
validation: Option<(&Tensor<f32>, &Tensor<T>)>,
callbacks: &mut [Callback],
metrics: &[Metrics],
) -> Result<History, AutodiffError> {
self.fit_with_callbacks_named(
"fit_with_prefetch",
x,
y,
config,
&FitWeights::default(),
validation,
callbacks,
metrics,
None,
Some(prefetch),
)
}
#[allow(clippy::too_many_arguments)]
pub fn fit_with_train_step<T: FitTarget>(
&mut self,
x: &Tensor<f32>,
y: &Tensor<T>,
config: FitConfig,
validation: Option<(&Tensor<f32>, &Tensor<T>)>,
callbacks: &mut [Callback],
metrics: &[Metrics],
train_step: &mut TrainStepFn<'_, T>,
) -> Result<History, AutodiffError> {
let mut shim = |m: &Sequential,
xb: &Tensor<f32>,
yb: &Tensor<T>,
opt: &mut OptimizerState,
groups: &[ParamGroup]|
-> CustomStepOutcome {
let mut handle = TrainStepOptimizer { state: opt, groups };
let out = train_step(m, xb, yb, &mut handle)?;
Ok((out.loss, out.updated))
};
let hook: &mut CustomStepHook<'_, T> = &mut shim;
self.fit_with_callbacks_named(
"fit_with_train_step",
x,
y,
config,
&FitWeights::default(),
validation,
callbacks,
metrics,
Some(hook),
None,
)
}
#[allow(clippy::too_many_arguments)]
pub fn fit_with_weights<T: FitTarget>(
&mut self,
x: &Tensor<f32>,
y: &Tensor<T>,
config: FitConfig,
weights: &FitWeights<'_>,
validation: Option<(&Tensor<f32>, &Tensor<T>)>,
callbacks: &mut [Callback],
metrics: &[Metrics],
) -> Result<History, AutodiffError> {
self.fit_with_callbacks_named(
"fit_with_weights",
x,
y,
config,
weights,
validation,
callbacks,
metrics,
None,
None,
)
}
#[allow(clippy::too_many_arguments)]
fn fit_with_callbacks_named<T: FitTarget>(
&mut self,
method: &'static str,
x: &Tensor<f32>,
y: &Tensor<T>,
config: FitConfig,
weights: &FitWeights<'_>,
validation: Option<(&Tensor<f32>, &Tensor<T>)>,
callbacks: &mut [Callback],
metrics: &[Metrics],
custom_step: Option<&mut CustomStepHook<'_, T>>,
prefetch: Option<PrefetchConfig>,
) -> Result<History, AutodiffError> {
let mut compiled = self.compiled.take().ok_or_else(|| not_compiled(method))?;
if config.epochs == 0 {
self.compiled = Some(compiled);
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: epochs == 0"
)));
}
if let Err(e) = self.reject_untracked_parametric_layer(&format!("Sequential::{method}")) {
self.compiled = Some(compiled);
return Err(e);
}
if let Err(e) = self.reject_seq_first_layer_for_fit(&format!("Sequential::{method}")) {
self.compiled = Some(compiled);
return Err(e);
}
if config.accumulate_steps == 0 {
self.compiled = Some(compiled);
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: accumulate_steps == 0"
)));
}
if config.accumulate_steps > 1 && compiled.amp.is_some() {
self.compiled = Some(compiled);
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: accumulate_steps > 1 は \
compile_with_amp(AMP)と併用できない(イシュー #2180 \
スコープ外)"
)));
}
if custom_step.is_some() && compiled.amp.is_some() {
self.compiled = Some(compiled);
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: カスタム学習 step フックは \
compile_with_amp(AMP)と併用できない(イシュー #2184)"
)));
}
if custom_step.is_some() && config.accumulate_steps > 1 {
self.compiled = Some(compiled);
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: カスタム学習 step フックは \
accumulate_steps > 1(勾配累積)と併用できない \
(イシュー #2184)"
)));
}
if config.accumulate_steps > 1 && matches!(compiled.optimizer, OptimizerState::Lbfgs(_)) {
self.compiled = Some(compiled);
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: accumulate_steps > 1 は \
Optimizer::Lbfgs と併用できない(イシュー #2172)"
)));
}
if custom_step.is_some() && matches!(compiled.optimizer, OptimizerState::Lbfgs(_)) {
self.compiled = Some(compiled);
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: カスタム学習 step フックは \
Optimizer::Lbfgs と併用できない(イシュー #2172。フックは \
&Sequential〈不変参照〉しか受け取らず closure 駆動の \
trial パラメータ書き込みを実行できない)"
)));
}
let ema_count = callbacks
.iter()
.filter(|cb| matches!(cb, Callback::Ema(_)))
.count();
if ema_count > 0 {
let reason = if ema_count > 1 {
Some("Callback::Ema は複数指定できない")
} else if matches!(compiled.optimizer, OptimizerState::Lbfgs(_)) {
Some("Callback::Ema は Optimizer::Lbfgs と併用できない")
} else if custom_step.is_some() {
Some("Callback::Ema はカスタム学習 step フックと併用できない")
} else if compiled.amp.is_some() {
Some(
"Callback::Ema は compile_with_amp(AMP)と併用できない(AMP の skip step の扱いが未決のため。docs/autodiff-ema-decision.md §10.2 (g))",
)
} else {
None
};
if let Some(reason) = reason {
self.compiled = Some(compiled);
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: {reason}(イシュー #2560)"
)));
}
}
let swa_count = callbacks
.iter()
.filter(|cb| matches!(cb, Callback::Swa(_)))
.count();
if swa_count > 0 {
let has_ema = callbacks.iter().any(|cb| matches!(cb, Callback::Ema(_)));
let es_restores = callbacks
.iter()
.any(|cb| matches!(cb, Callback::EarlyStopping(es) if es.restores_best_weights()));
let reason = if swa_count > 1 {
Some("Callback::Swa は複数指定できない")
} else if matches!(compiled.optimizer, OptimizerState::Lbfgs(_)) {
Some("Callback::Swa は Optimizer::Lbfgs と併用できない")
} else if custom_step.is_some() {
Some("Callback::Swa はカスタム学習 step フックと併用できない")
} else if compiled.amp.is_some() {
Some(
"Callback::Swa は compile_with_amp(AMP)と併用できない(skip step と平均の関係が未決のため)",
)
} else if has_ema {
Some(
"Callback::Swa は Callback::Ema と併用できない(fit 終了時の重みの意味が衝突するため)",
)
} else if es_restores {
Some("Callback::Swa は EarlyStopping::restore_best_weights(true) と併用できない")
} else if !compiled.optimizer.supports_lr_schedule() {
Some(
"Callback::Swa は set_lr を提供しない optimizer(RmsProp/Adagrad/Lamb)と併用できない",
)
} else if !compiled.param_groups.is_empty() {
Some(
"Callback::Swa は compile_with_param_groups で param groups を設定した状態では併用できない",
)
} else {
None
};
if let Some(reason) = reason {
self.compiled = Some(compiled);
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: {reason}(イシュー #3059)"
)));
}
}
let split_at = match resolve_validation_split(
method,
config.validation_split_bits,
x.shape().first().copied(),
y.shape().first().copied(),
validation.is_some(),
) {
Ok(v) => v,
Err(e) => {
self.compiled = Some(compiled);
return Err(e);
}
};
let split_owned = match split_at {
Some(at) => match split_for_validation(method, x, y, at) {
Ok(v) => Some(v),
Err(e) => {
self.compiled = Some(compiled);
return Err(e);
}
},
None => None,
};
let (x, y, validation) = match &split_owned {
Some((x_tr, y_tr, x_val, y_val)) => (x_tr, y_tr, Some((x_val, y_val))),
None => (x, y, validation),
};
let n_total = split_owned
.as_ref()
.map(|(x_tr, _, x_val, _)| {
x_tr.shape().first().copied().unwrap_or(0)
+ x_val.shape().first().copied().unwrap_or(0)
})
.or_else(|| x.shape().first().copied())
.unwrap_or(0);
if let Err(e) = validate_fit_weights(
method,
weights,
n_total,
compiled.loss,
matches!(compiled.optimizer, OptimizerState::Lbfgs(_)),
custom_step.is_some(),
) {
self.compiled = Some(compiled);
return Err(e);
}
let sliced_weights;
let weights: &FitWeights<'_> = match (split_at, weights.sample_weight) {
(Some(at), Some(sw)) => {
sliced_weights = FitWeights {
class_weight: weights.class_weight.clone(),
sample_weight: sw.get(..at),
};
&sliced_weights
}
_ => weights,
};
if validation.is_none()
&& let Some(offending) = callbacks.iter().find(|cb| cb.requires_validation())
{
self.compiled = Some(compiled);
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: callback {offending:?} は \
Monitor::ValLoss または Monitor::ValMetric を監視するが \
validation が None"
)));
}
if !compiled.optimizer.supports_lr_schedule()
&& callbacks
.iter()
.any(|cb| matches!(cb, Callback::LrSchedule(_)))
{
self.compiled = Some(compiled);
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: compile() した optimizer(RmsProp/\
Adagrad/Lamb のいずれか)は set_lr を提供しないため \
Callback::LrSchedule と併用できない"
)));
}
if !compiled.param_groups.is_empty() {
if callbacks
.iter()
.any(|cb| matches!(cb, Callback::LrSchedule(_)))
{
self.compiled = Some(compiled);
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: compile_with_param_groups で param groups を \
設定した状態では Callback::LrSchedule と併用できない"
)));
}
let n_slots = self.trainable_parameters().len();
if let Some(slot) = compiled
.param_groups
.iter()
.flat_map(|g| g.params.iter().copied())
.find(|&slot| slot >= n_slots)
{
self.compiled = Some(compiled);
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: param_groups がスロット添字 {slot} を参照して \
いるが、学習対象パラメータは {n_slots} 件"
)));
}
}
if !metrics.is_empty() && validation.is_none() {
self.compiled = Some(compiled);
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: metrics が非空だが validation が None\
(metrics は validation set 上で定義される指標のため)"
)));
}
for cb in callbacks.iter() {
if let Some(super::callbacks::Monitor::ValMetric(m)) = cb.monitor() {
if m == Metrics::ConfusionMatrix {
self.compiled = Some(compiled);
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: callback {cb:?} は \
Monitor::ValMetric(Metrics::ConfusionMatrix)(非スカラー)を \
監視できない"
)));
}
if !metrics.contains(&m) {
self.compiled = Some(compiled);
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: callback {cb:?} は Monitor::ValMetric({m:?}) を \
監視するが、{m:?} が metrics 引数に含まれない"
)));
}
}
}
if !metrics.is_empty()
&& let Err(e) = T::require_metrics_support()
{
self.compiled = Some(compiled);
return Err(e);
}
for cb in callbacks.iter_mut() {
if let Callback::EarlyStopping(es) = cb {
es.reset_for_fit();
}
}
for cb in callbacks.iter_mut() {
let r = match cb {
Callback::CsvLogger(l) => l.begin_fit(method, validation.is_some(), metrics),
Callback::JsonLogger(l) => l.begin_fit(method, validation.is_some(), metrics),
_ => Ok(()),
};
if let Err(e) = r {
for cb in callbacks.iter_mut() {
cb.end_fit();
}
self.compiled = Some(compiled);
return Err(e);
}
}
for i in 0..callbacks.len() {
let r = match &mut callbacks[i] {
Callback::Ema(e) => e.begin_fit(&*self),
_ => Ok(()),
};
if let Err(e) = r {
for cb in callbacks.iter_mut() {
cb.end_fit();
}
self.compiled = Some(compiled);
return Err(e);
}
}
for i in 0..callbacks.len() {
let r = match &mut callbacks[i] {
Callback::Swa(s) => s.begin_fit(self),
_ => Ok(()),
};
if let Err(e) = r {
for cb in callbacks.iter_mut() {
cb.end_fit();
}
self.compiled = Some(compiled);
return Err(e);
}
}
let prev_training = self.training();
self.set_training(true);
let result = self.run_fit(
method,
&mut compiled,
x,
y,
config,
weights,
validation,
callbacks,
metrics,
custom_step,
prefetch,
);
if result.is_err() {
for cb in callbacks.iter_mut() {
if let Callback::Swa(s) = cb {
s.abort_fit(self);
}
}
}
for cb in callbacks.iter_mut() {
cb.end_fit();
}
self.set_training(prev_training);
self.compiled = Some(compiled);
result
}
#[allow(clippy::too_many_arguments)]
fn run_fit<T: FitTarget>(
&mut self,
method: &'static str,
compiled: &mut Compiled,
x: &Tensor<f32>,
y: &Tensor<T>,
config: FitConfig,
weights: &FitWeights<'_>,
validation: Option<(&Tensor<f32>, &Tensor<T>)>,
callbacks: &mut [Callback],
metrics: &[Metrics],
mut custom_step: Option<&mut CustomStepHook<'_, T>>,
prefetch: Option<PrefetchConfig>,
) -> Result<History, AutodiffError> {
let to_invalid_arg = |e: fandhe_ai_tensor_core::data::DataError| {
AutodiffError::InvalidArgument(format!("Sequential::{method}: {e}"))
};
let x_dataset = TensorDataset::new(x.clone()).map_err(to_invalid_arg)?;
let y_dataset = TensorDataset::new(y.clone()).map_err(to_invalid_arg)?;
let loader_config = DataLoaderConfig::new(config.batch_size)
.shuffle(config.shuffle)
.drop_last(config.drop_last);
let mut loader = match (weights.sample_weight, prefetch) {
(None, Some(cfg)) => {
let len = Dataset::len(&x_dataset);
let dataset = (x_dataset, y_dataset);
let built = if config.shuffle {
let sampler = RandomSampler::new(len, config.batch_size, config.drop_last)
.map_err(to_invalid_arg)?;
PrefetchDataLoader::new(dataset, sampler, cfg)
} else {
let sampler = SequentialSampler::new(len, config.batch_size, config.drop_last)
.map_err(to_invalid_arg)?;
PrefetchDataLoader::new(dataset, sampler, cfg)
};
FitLoader::Prefetch(built.map_err(to_invalid_arg)?)
}
(Some(sw), _) => {
let mut buf: Vec<f32> = Vec::new();
buf.try_reserve_exact(sw.len())
.map_err(|_| super::alloc_failed())?;
buf.extend_from_slice(sw);
let w_tensor = Tensor::new(buf, &[sw.len()]).map_err(AutodiffError::Shape)?;
let w_dataset = TensorDataset::new(w_tensor).map_err(to_invalid_arg)?;
FitLoader::Weighted(
DataLoader::new((x_dataset, y_dataset, w_dataset), loader_config)
.map_err(to_invalid_arg)?,
)
}
(None, None) => FitLoader::Plain(
DataLoader::new((x_dataset, y_dataset), loader_config).map_err(to_invalid_arg)?,
),
};
let weighted = weights.is_non_default();
let check_accumulated = weighted || config.accumulate_steps > 1;
let mut loss = Vec::new();
loss.try_reserve_exact(config.epochs)
.map_err(|_| super::alloc_failed())?;
let mut val_loss = Vec::new();
if validation.is_some() {
val_loss
.try_reserve_exact(config.epochs)
.map_err(|_| super::alloc_failed())?;
}
let mut lr = Vec::new();
lr.try_reserve_exact(config.epochs)
.map_err(|_| super::alloc_failed())?;
let mut val_metrics = Vec::new();
if validation.is_some() && !metrics.is_empty() {
val_metrics
.try_reserve_exact(config.epochs)
.map_err(|_| super::alloc_failed())?;
}
let mut history = History {
loss,
val_loss,
lr,
val_metrics,
};
let mut targets_checked = false;
let epoch_result: Result<(), AutodiffError> = 'epochs_block: {
'epochs: for epoch_local in 0..config.epochs {
for cb in callbacks.iter() {
if let Callback::LrSchedule(ls) = cb
&& let Err(e) = compiled.optimizer.set_lr(ls.lr_for_epoch_begin())
{
break 'epochs_block Err(e);
}
}
for cb in callbacks.iter_mut() {
if let Callback::Swa(s) = cb {
match s.lr_for_epoch(compiled.optimizer.lr()) {
Ok(Some(lr)) => {
if let Err(e) = compiled.optimizer.set_lr(lr) {
break 'epochs_block Err(e);
}
}
Ok(None) => {}
Err(e) => break 'epochs_block Err(e),
}
}
}
history.lr.push(compiled.optimizer.lr());
let mut weighted_sum = 0.0f64;
let mut count = 0usize;
let mut acc: Option<Vec<Tensor<f32>>> = None;
let mut micro: u32 = 0;
for batch in loader.iter() {
let (x_batch, y_batch, w_batch) = match batch {
Ok(v) => v,
Err(e) => {
break 'epochs_block Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: {e}"
)));
}
};
let n_batch = x_batch.shape().first().copied().unwrap_or(0);
let updated: Option<Vec<Tensor<f32>>> = if let Some(hook) = custom_step.as_mut()
{
let (loss_scalar, updated_from_hook) = match hook(
&*self,
&x_batch,
&y_batch,
&mut compiled.optimizer,
&compiled.param_groups,
) {
Ok(v) => v,
Err(e) => break 'epochs_block Err(e),
};
weighted_sum += loss_scalar as f64 * n_batch as f64;
count += n_batch;
updated_from_hook
} else if let OptimizerState::Lbfgs(lbfgs) = &mut compiled.optimizer {
let loss_scalar = match lbfgs_batch_step(
self,
lbfgs,
compiled.loss,
&x_batch,
&y_batch,
method,
) {
Ok(v) => v,
Err(e) => break 'epochs_block Err(e),
};
weighted_sum += loss_scalar as f64 * n_batch as f64;
count += n_batch;
None
} else {
let tape = crate::tape();
let bound = self.bind(&tape);
let x_var = tape.var(&x_batch);
let low_precision_dtype = compiled.amp.as_ref().map(|amp| amp.dtype);
let pred = match bound.forward_with_precision(
&tape,
&x_var,
low_precision_dtype,
) {
Ok(v) => v,
Err(e) => break 'epochs_block Err(e),
};
if weighted
&& !targets_checked
&& let [_, c] = pred.to_tensor().shape()
{
if let Err(e) = T::check_class_range(y, *c) {
break 'epochs_block Err(e);
}
if compiled.loss == Loss::CrossEntropy
&& let Some(cls) = T::as_class_targets(y)
&& let Err(e) = check_ce_coefs_for_all_batches(
&cls.host_slice(),
config.batch_size,
config.drop_last,
config.shuffle,
weights,
)
{
break 'epochs_block Err(e);
}
targets_checked = true;
}
let loss_result = if weighted {
T::weighted_loss_for(
compiled.loss,
&tape,
&pred,
&y_batch,
w_batch.as_ref(),
weights.class_weight.as_ref(),
)
} else {
T::loss_for(compiled.loss, &tape, &pred, &y_batch)
};
let loss_var = match loss_result {
Ok(v) => v,
Err(e) => break 'epochs_block Err(e),
};
let loss_scalar = match loss_var.to_tensor().get(&[]) {
Some(v) => v,
None => {
break 'epochs_block Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: loss の shape が [] ではない\
(loss 演算の契約違反)"
)));
}
};
if weighted && !loss_scalar.is_finite() {
break 'epochs_block Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: 重み付き損失が非有限({loss_scalar})の\
ためパラメータ更新を拒否した(重み×損失の overflow の疑い)"
)));
}
weighted_sum += loss_scalar as f64 * n_batch as f64;
count += n_batch;
if let Some(amp) = compiled.amp.as_mut() {
let scaled_loss = match amp.scaler.scale_loss(&loss_var) {
Ok(v) => v,
Err(e) => break 'epochs_block Err(e),
};
let grads = match tape.backward(&scaled_loss) {
Ok(v) => v,
Err(e) => break 'epochs_block Err(e),
};
let grad_refs = match bound.trainable_grads(&grads) {
Ok(v) => v,
Err(e) => break 'epochs_block Err(e),
};
let unscale_result = match amp.scaler.unscale(&grad_refs) {
Ok(v) => v,
Err(e) => break 'epochs_block Err(e),
};
if unscale_result.should_skip_step() {
if let Err(e) = amp.scaler.update(true) {
break 'epochs_block Err(e);
}
None
} else {
let unscaled_refs: Vec<&Tensor<f32>> =
unscale_result.grads.iter().collect();
let param_refs = self.trainable_parameters();
let stepped = match compiled.optimizer.step_dispatch(
¶m_refs,
&unscaled_refs,
&compiled.param_groups,
) {
Ok(v) => v,
Err(e) => break 'epochs_block Err(e),
};
if let Err(e) = amp.scaler.update(false) {
break 'epochs_block Err(e);
}
Some(stepped)
}
} else {
let grads = match tape.backward(&loss_var) {
Ok(v) => v,
Err(e) => break 'epochs_block Err(e),
};
let grad_refs = match bound.trainable_grads(&grads) {
Ok(v) => v,
Err(e) => break 'epochs_block Err(e),
};
if weighted
&& let Err(e) = ensure_finite_grads(
grad_refs.iter().copied(),
method,
"マイクロバッチ",
weighted,
)
{
break 'epochs_block Err(e);
}
micro += 1;
if micro == 1 {
let mut cloned: Vec<Tensor<f32>> = Vec::new();
if cloned.try_reserve_exact(grad_refs.len()).is_err() {
break 'epochs_block Err(super::alloc_failed());
}
cloned.extend(grad_refs.iter().map(|g| (*g).clone()));
acc = Some(cloned);
} else {
let acc_buf = match acc.as_mut() {
Some(buf) => buf,
None => {
break 'epochs_block Err(AutodiffError::InvalidArgument(
format!(
"Sequential::{method}: 勾配累積バッファが\
初期化されていない(内部不変条件違反)"
),
));
}
};
if let Err(e) = accumulate_grads_into(acc_buf, &grad_refs, method) {
break 'epochs_block Err(e);
}
}
if micro == config.accumulate_steps {
let param_refs = self.trainable_parameters();
let acc_buf = match acc.as_ref() {
Some(buf) => buf,
None => {
break 'epochs_block Err(AutodiffError::InvalidArgument(
format!(
"Sequential::{method}: 勾配累積バッファが\
初期化されていない(内部不変条件違反)"
),
));
}
};
if check_accumulated
&& let Err(e) = ensure_finite_grads(
acc_buf.iter(),
method,
"累積境界",
weighted,
)
{
break 'epochs_block Err(e);
}
let acc_refs: Vec<&Tensor<f32>> = acc_buf.iter().collect();
let stepped = match compiled.optimizer.step_dispatch(
¶m_refs,
&acc_refs,
&compiled.param_groups,
) {
Ok(v) => v,
Err(e) => break 'epochs_block Err(e),
};
acc = None;
micro = 0;
Some(stepped)
} else {
None
}
}
};
if let Some(updated) = updated {
if let Err(e) = self.apply_parameters(updated) {
break 'epochs_block Err(e);
}
if let Err(e) = self.ema_update_after_step(callbacks) {
break 'epochs_block Err(e);
}
}
}
if micro > 0 {
let param_refs = self.trainable_parameters();
let acc_buf = match acc.as_ref() {
Some(buf) => buf,
None => {
break 'epochs_block Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: 勾配累積バッファが初期化されていない\
(内部不変条件違反)"
)));
}
};
if check_accumulated
&& let Err(e) = ensure_finite_grads(
acc_buf.iter(),
method,
"epoch 末の端数 flush",
weighted,
)
{
break 'epochs_block Err(e);
}
let acc_refs: Vec<&Tensor<f32>> = acc_buf.iter().collect();
let stepped = match compiled.optimizer.step_dispatch(
¶m_refs,
&acc_refs,
&compiled.param_groups,
) {
Ok(v) => v,
Err(e) => break 'epochs_block Err(e),
};
if let Err(e) = self.apply_parameters(stepped) {
break 'epochs_block Err(e);
}
if let Err(e) = self.ema_update_after_step(callbacks) {
break 'epochs_block Err(e);
}
}
if count == 0 {
break 'epochs_block Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: この epoch で処理されたサンプルが 0 件\
(drop_last により全バッチが切り捨てられた可能性がある)"
)));
}
history.loss.push((weighted_sum / count as f64) as f32);
if let Err(e) = self.swa_update_after_epoch(callbacks) {
break 'epochs_block Err(e);
}
let ema_backup = match self.ema_swap_in(callbacks) {
Ok(b) => b,
Err(e) => break 'epochs_block Err(e),
};
let eval_outcome: Result<bool, AutodiffError> = 'eval: {
if let Some((x_val, y_val)) = validation {
self.set_training(false);
let v = self.run_evaluate_with_metrics::<T>(
x_val,
y_val,
config.batch_size,
compiled.loss,
metrics,
method,
);
self.set_training(true);
match v {
Ok((loss_v, metrics_v)) => {
history.val_loss.push(loss_v);
if let Some(m) = metrics_v {
history.val_metrics.push(m);
}
}
Err(e) => break 'eval Err(e),
}
}
let mut stop = false;
for cb in callbacks.iter_mut() {
match cb {
Callback::ModelCheckpoint(mc) => {
if let Some(value) = mc.monitor_value_at(&history, epoch_local)
&& let Err(e) = mc.observe(value, self)
{
break 'eval Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: ModelCheckpoint::to_file の保存に失敗した: {e}"
)));
}
}
Callback::LrSchedule(ls) => {
let value = ls.monitor_value_at(&history, epoch_local);
if let Err(e) = ls.advance(value) {
break 'eval Err(e);
}
}
Callback::CsvLogger(l) => {
if let Err(e) = l.write_epoch(method, &history, epoch_local) {
break 'eval Err(e);
}
}
Callback::JsonLogger(l) => {
if let Err(e) = l.write_epoch(method, &history, epoch_local) {
break 'eval Err(e);
}
}
Callback::Ema(_) => {}
Callback::Swa(_) => {}
Callback::Lambda(l) => {
if let Err(e) = l.invoke(epoch_local, &history) {
break 'eval Err(e);
}
}
Callback::EarlyStopping(es) => {
if let Some(value) = es.monitor_value_at(&history, epoch_local)
&& es.observe(value, epoch_local, || self.state_dict())
{
stop = true;
}
}
}
}
Ok(stop)
};
if let Some(backup) = ema_backup
&& let Err(e) = self.load_state_dict(backup)
{
break 'epochs_block Err(e);
}
match eval_outcome {
Ok(true) => break 'epochs,
Ok(false) => {}
Err(e) => break 'epochs_block Err(e),
}
}
Ok(())
};
let mut restore_err: Option<AutodiffError> = None;
for cb in callbacks.iter_mut() {
if let Callback::EarlyStopping(es) = cb
&& let Some(state) = es.take_restore_state()
&& let Err(e) = self.load_state_dict(state)
{
restore_err = Some(e);
}
}
epoch_result?;
if let Some(e) = restore_err {
return Err(e);
}
self.swa_finish_fit(callbacks)?;
Ok(history)
}
fn swa_update_after_epoch(&self, callbacks: &mut [Callback]) -> Result<(), AutodiffError> {
for cb in callbacks.iter_mut() {
if let Callback::Swa(s) = cb {
s.update_after_epoch(self)?;
}
}
Ok(())
}
fn swa_finish_fit(&mut self, callbacks: &mut [Callback]) -> Result<(), AutodiffError> {
for cb in callbacks.iter_mut() {
if let Callback::Swa(s) = cb {
s.finish_fit(self)?;
}
}
Ok(())
}
fn ema_update_after_step(&self, callbacks: &mut [Callback]) -> Result<(), AutodiffError> {
for cb in callbacks.iter_mut() {
if let Callback::Ema(e) = cb {
e.update(self)?;
}
}
Ok(())
}
fn ema_swap_in(
&mut self,
callbacks: &[Callback],
) -> Result<Option<std::collections::HashMap<String, Tensor<f32>>>, AutodiffError> {
for cb in callbacks {
if let Callback::Ema(e) = cb {
let shadow = e.shadow_for_eval()?;
let backup = self.state_dict();
self.load_state_dict(shadow)?;
return Ok(Some(backup));
}
}
Ok(None)
}
pub fn evaluate<T: FitTarget>(
&mut self,
x: &Tensor<f32>,
y: &Tensor<T>,
batch_size: usize,
) -> Result<f32, AutodiffError> {
let loss = self
.compiled
.as_ref()
.ok_or_else(|| not_compiled("evaluate"))?
.loss;
let prev_training = self.training();
self.set_training(false);
let result = self
.run_evaluate_with_metrics::<T>(x, y, batch_size, loss, &[], "evaluate")
.map(|(loss_v, _metrics_v)| loss_v);
self.set_training(prev_training);
result
}
fn run_evaluate_with_metrics<T: FitTarget>(
&self,
x: &Tensor<f32>,
y: &Tensor<T>,
batch_size: usize,
loss: Loss,
metrics: &[Metrics],
method: &str,
) -> Result<(f32, Option<MetricsResult>), AutodiffError> {
self.reject_seq_first_layer_for_fit(&format!("Sequential::{method}"))?;
let to_invalid_arg = |e: fandhe_ai_tensor_core::data::DataError| {
AutodiffError::InvalidArgument(format!("Sequential::{method}: {e}"))
};
let x_dataset = TensorDataset::new(x.clone()).map_err(to_invalid_arg)?;
let y_dataset = TensorDataset::new(y.clone()).map_err(to_invalid_arg)?;
let loader = DataLoader::new((x_dataset, y_dataset), DataLoaderConfig::new(batch_size))
.map_err(to_invalid_arg)?;
let mut weighted_sum = 0.0f64;
let mut count = 0usize;
let mut accumulator: Option<ConfusionAccumulator> = None;
for batch in &loader {
let (x_batch, y_batch) = batch.map_err(|e| {
AutodiffError::InvalidArgument(format!("Sequential::{method}: {e}"))
})?;
let n_batch = x_batch.shape().first().copied().unwrap_or(0);
let tape = crate::tape();
let x_var = tape.var(&x_batch);
let pred = self.forward(&tape, &x_var)?;
let loss_var = T::loss_for(loss, &tape, &pred, &y_batch)?;
let loss_scalar = loss_var.to_tensor().get(&[]).ok_or_else(|| {
AutodiffError::InvalidArgument(format!(
"Sequential::{method}: loss の shape が [] ではない\
(loss 演算の契約違反)"
))
})?;
weighted_sum += loss_scalar as f64 * n_batch as f64;
count += n_batch;
if !metrics.is_empty() {
let target_batch = T::as_class_targets(&y_batch).ok_or_else(|| {
AutodiffError::InvalidArgument(format!(
"Sequential::{method}: metrics はクラス添字 target(Loss::CrossEntropy/\
Loss::Nll・Tensor<i32>)でのみ計算できる"
))
})?;
let pred_tensor = pred.to_tensor();
let pred_shape = pred_tensor.shape();
if pred_shape.len() != 2 {
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: metrics には pred(logits)が rank 2 [N, C] \
である必要がある (実際は {pred_shape:?})"
)));
}
let num_classes = pred_shape[1];
let pred_classes = pred.argmax(Some(1))?;
match accumulator.as_mut() {
Some(acc) => acc.observe(&pred_classes, target_batch)?,
None => {
let mut acc = ConfusionAccumulator::new(num_classes)?;
acc.observe(&pred_classes, target_batch)?;
accumulator = Some(acc);
}
}
}
}
if count == 0 {
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::{method}: 処理されたサンプルが 0 件"
)));
}
let avg_loss = (weighted_sum / count as f64) as f32;
let metrics_result = match accumulator {
Some(acc) => Some(acc.finish(metrics)?),
None => None,
};
Ok((avg_loss, metrics_result))
}
}
#[cfg(test)]
mod lbfgs_fit_failure_tests {
use super::*;
const D_IN: usize = 3;
const D_HIDDEN: usize = 4;
const D_OUT: usize = 2;
const N: usize = 4;
const SEED_L1: u64 = 0x1BF6_1111;
const SEED_L2: u64 = 0x1BF6_2222;
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 deterministic_fill(seed: u64, n: usize) -> Vec<f32> {
let mut state = seed;
(0..n)
.map(|_| {
state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^= z >> 31;
((z >> 11) as f64 / (1u64 << 53) as f64) as f32 - 0.5
})
.collect()
}
fn gen_regression_data(seed: u64) -> (Tensor<f32>, Tensor<f32>) {
let x = deterministic_fill(seed, N * D_IN);
let y = deterministic_fill(seed ^ 0x5555_5555_5555_5555, N * D_OUT);
(
Tensor::new(x, &[N, D_IN])
.unwrap_or_else(|e| panic!("test fixture: x の shape 構築に失敗: {e}")),
Tensor::new(y, &[N, D_OUT])
.unwrap_or_else(|e| panic!("test fixture: y の shape 構築に失敗: {e}")),
)
}
fn params_bit_exact(a: &[&Tensor<f32>], b: &[&Tensor<f32>]) -> bool {
if a.len() != b.len() {
return false;
}
a.iter().zip(b.iter()).all(|(x, y)| {
let xd = x.contiguous();
let yd = y.contiguous();
let xs = xd.as_slice().expect("test fixture: contiguous 化済み");
let ys = yd.as_slice().expect("test fixture: contiguous 化済み");
xs.len() == ys.len()
&& xs
.iter()
.zip(ys.iter())
.all(|(a, b)| a.to_bits() == b.to_bits())
})
}
#[test]
fn lbfgs_fit_restores_params_and_keeps_compiled_after_multi_eval_failure() {
let (x, y) = gen_regression_data(0x1BF6_D474);
let mut model = build_model();
model
.compile(
Optimizer::Lbfgs(LbfgsConfig {
lr: 1e30,
max_iter: 2,
..LbfgsConfig::default()
}),
Loss::Mse,
)
.unwrap_or_else(|e| panic!("test fixture: compile に失敗: {e}"));
let snapshot_before: Vec<Tensor<f32>> =
model.trainable_parameters().into_iter().cloned().collect();
let eval_before = model
.evaluate(&x, &y, N)
.unwrap_or_else(|e| panic!("test fixture: 事前 evaluate に失敗: {e}"));
let result = model.fit(&x, &y, FitConfig::new(1, N));
let err = result.expect_err(
"極端に大きい lr による非有限 loss は Lbfgs::try_step_closure を \
Err にするはず(テスト前提が崩れている場合はここで失敗する)",
);
assert!(
matches!(err, AutodiffError::InvalidArgument(_)),
"非有限 loss の検出は InvalidArgument のはず: {err:?}"
);
let msg = err.to_string();
assert!(
msg.contains("non-finite loss"),
"想定した失敗経路(2 回目以降の closure 評価での非有限 loss \
検出)ではない可能性がある: {msg}"
);
let snapshot_after = model.trainable_parameters();
assert_eq!(snapshot_before.len(), snapshot_after.len());
let before_refs: Vec<&Tensor<f32>> = snapshot_before.iter().collect();
assert!(
params_bit_exact(&before_refs, &snapshot_after),
"L-BFGS closure 失敗後にパラメータが fit 呼び出し前の snapshot \
から変化している"
);
assert!(
model.is_compiled(),
"L-BFGS closure 失敗後も is_compiled() が true のまま維持される \
はず"
);
let eval_after = model
.evaluate(&x, &y, N)
.unwrap_or_else(|e| panic!("L-BFGS closure 失敗後の evaluate に失敗: {e}"));
assert_eq!(
eval_before, eval_after,
"パラメータが復元されているなら fit 前後で evaluate の損失は \
完全一致するはず"
);
model
.compile(Optimizer::Lbfgs(LbfgsConfig::default()), Loss::Mse)
.unwrap_or_else(|e| panic!("test fixture: 再 compile に失敗: {e}"));
let history = model
.fit(&x, &y, FitConfig::new(1, N))
.unwrap_or_else(|e| panic!("再 compile 後の fit に失敗: {e}"));
assert_eq!(history.loss.len(), 1);
assert!(history.loss[0].is_finite());
}
}
#[cfg(test)]
mod seq_first_mha_fit_rejection_tests {
use super::super::MultiheadAttentionConfig;
use super::*;
fn data() -> (Tensor<f32>, Tensor<f32>) {
(
Tensor::new(vec![0.1; 4 * 2 * 8], &[4, 2, 8]).unwrap(),
Tensor::new(vec![0.2; 4 * 2 * 8], &[4, 2, 8]).unwrap(),
)
}
fn compiled(batch_first: bool) -> Sequential {
let cfg = MultiheadAttentionConfig::new(8, 2).with_batch_first(batch_first);
let mut m = Sequential::new()
.add_multihead_attention_with_config(cfg, 1)
.unwrap();
m.compile(Optimizer::Sgd(SgdConfig::new(0.01)), Loss::Mse)
.unwrap();
m
}
#[test]
fn fit_and_evaluate_reject_batch_first_false() {
let (x, y) = data();
let mut m = compiled(false);
let err = m.fit(&x, &y, FitConfig::new(1, 2)).unwrap_err();
assert!(matches!(err, AutodiffError::InvalidArgument(_)));
assert!(m.is_compiled(), "拒否後も compiled を保持する");
let err = m.evaluate(&x, &y, 2).unwrap_err();
assert!(matches!(err, AutodiffError::InvalidArgument(_)));
}
#[test]
fn batch_first_true_still_accepted() {
let (x, y) = data();
let mut m = compiled(true);
m.fit(&x, &y, FitConfig::new(1, 2)).unwrap();
m.evaluate(&x, &y, 2).unwrap();
}
}