use fandhe_ai_tensor_core::Element;
use fandhe_ai_tensor_core::data::{
DataError, DataLoader, DataLoaderConfig, Dataset, TensorDataset,
};
use super::{FunctionalModel, FunctionalVars, NodeDef, invalid};
use crate::compat::training::{
Compiled, FitConfig, FitTarget, History, Loss, Optimizer, OptimizerState,
};
use crate::{AutodiffError, Gradients, Tape, Tensor, Var};
fn not_compiled(method: &str) -> AutodiffError {
invalid(format!(
"FunctionalModel::{method}: compile() が呼ばれていない(optimizer/loss が未設定)"
))
}
struct GraphDataset<T: Element> {
xs: Vec<TensorDataset<f32>>,
ys: Vec<TensorDataset<T>>,
}
impl<T: Element> Dataset for GraphDataset<T> {
type Batch = (Vec<Tensor<f32>>, Vec<Tensor<T>>);
fn len(&self) -> usize {
self.xs.first().map_or(0, Dataset::len)
}
fn validate(&self) -> Result<(), DataError> {
for d in &self.xs {
d.validate()?;
}
for d in &self.ys {
d.validate()?;
}
let expected = self.len();
let lens = self
.xs
.iter()
.map(Dataset::len)
.chain(self.ys.iter().map(Dataset::len));
for (component, found) in lens.enumerate() {
if found != expected {
return Err(DataError::LengthMismatch {
expected,
found,
component,
});
}
}
Ok(())
}
fn batch(&self, indices: &[usize]) -> Result<Self::Batch, DataError> {
let mut xs = Vec::new();
xs.try_reserve_exact(self.xs.len()).map_err(|_| {
DataError::Shape(fandhe_ai_tensor_core::ShapeError::ElementCountOverflow)
})?;
for d in &self.xs {
xs.push(d.batch(indices)?);
}
let mut ys = Vec::new();
ys.try_reserve_exact(self.ys.len()).map_err(|_| {
DataError::Shape(fandhe_ai_tensor_core::ShapeError::ElementCountOverflow)
})?;
for d in &self.ys {
ys.push(d.batch(indices)?);
}
Ok((xs, ys))
}
}
fn build_loader<T: FitTarget>(
method: &str,
xs: &[&Tensor<f32>],
ys: &[&Tensor<T>],
config: DataLoaderConfig,
) -> Result<DataLoader<GraphDataset<T>>, AutodiffError> {
let to_invalid = |e: DataError| invalid(format!("FunctionalModel::{method}: {e}"));
let mut x_sets = Vec::new();
x_sets
.try_reserve_exact(xs.len())
.map_err(|_| super::super::alloc_failed())?;
for x in xs {
x_sets.push(TensorDataset::new((*x).clone()).map_err(to_invalid)?);
}
let mut y_sets = Vec::new();
y_sets
.try_reserve_exact(ys.len())
.map_err(|_| super::super::alloc_failed())?;
for y in ys {
y_sets.push(TensorDataset::new((*y).clone()).map_err(to_invalid)?);
}
DataLoader::new(
GraphDataset {
xs: x_sets,
ys: y_sets,
},
config,
)
.map_err(to_invalid)
}
fn combined_loss<'t, T: FitTarget>(
loss: Loss,
tape: &'t Tape,
preds: &[Var<'t>],
targets: &[Tensor<T>],
) -> Result<Var<'t>, AutodiffError> {
let mut total: Option<Var<'t>> = None;
for (pred, target) in preds.iter().zip(targets) {
let l = T::loss_for(loss, tape, pred, target)?;
total = Some(match total {
None => l,
Some(acc) => acc.add(&l)?,
});
}
total.ok_or_else(|| invalid("FunctionalModel: 出力が 0 件で損失を作れない".to_string()))
}
impl FunctionalModel {
pub(crate) fn bind<'m, 't>(&'m self, tape: &'t Tape) -> FunctionalVars<'m, 't> {
FunctionalVars {
model: self,
blocks: self.blocks().map(|(i, b)| (i, b.bind(tape))).collect(),
}
}
pub fn trainable_parameters(&self) -> Vec<&Tensor<f32>> {
self.blocks()
.flat_map(|(_, b)| b.trainable_parameters())
.collect()
}
pub fn apply_parameters(&mut self, updated: Vec<Tensor<f32>>) -> Result<(), AutodiffError> {
let mut counts: Vec<(usize, usize)> = Vec::new();
counts
.try_reserve_exact(self.nodes.len())
.map_err(|_| super::super::alloc_failed())?;
for (index, block) in self.blocks() {
counts.push((index, block.trainable_parameters().len()));
}
let total: usize = counts.iter().map(|(_, n)| *n).sum();
if updated.len() != total {
return Err(invalid(format!(
"FunctionalModel::apply_parameters: 更新後パラメータ数 {} が訓練対象パラメータ数 {total} と一致しない",
updated.len()
)));
}
let current = self.trainable_parameters();
for (slot, (cur, new)) in current.iter().zip(&updated).enumerate() {
if cur.shape() != new.shape() {
return Err(invalid(format!(
"FunctionalModel::apply_parameters: パラメータ {slot} の shape {:?} が現在の {:?} と一致しない",
new.shape(),
cur.shape()
)));
}
}
let mut previous: Vec<Vec<Tensor<f32>>> = Vec::new();
previous
.try_reserve_exact(counts.len())
.map_err(|_| super::super::alloc_failed())?;
for (_, block) in self.blocks() {
previous.push(block.trainable_parameters().into_iter().cloned().collect());
}
let mut rest = updated.into_iter();
for (applied, (index, n)) in counts.iter().enumerate() {
let chunk: Vec<Tensor<f32>> = rest.by_ref().take(*n).collect();
let result = match self.nodes.get_mut(*index) {
Some(NodeDef::Block { block, .. }) => block.apply_parameters(chunk),
_ => Err(invalid(format!(
"内部不整合: ノード {index} がブロックでない"
))),
};
if let Err(err) = result {
let mut failures: Vec<String> = Vec::new();
for ((done, _), snapshot) in counts.iter().zip(previous).take(applied + 1) {
let restored = match self.nodes.get_mut(*done) {
Some(NodeDef::Block { block, .. }) => block.apply_parameters(snapshot),
_ => Err(invalid("ブロックでないため復元不能".to_string())),
};
if let Err(rb) = restored {
failures.push(format!("ノード {done}: {rb}"));
}
}
if !failures.is_empty() {
return Err(invalid(format!(
"FunctionalModel::apply_parameters: ノード {index} の適用に失敗({err})し、巻き戻しにも失敗した({})。モデルが部分適用のまま残っている可能性がある",
failures.join("; ")
)));
}
return Err(err);
}
}
Ok(())
}
pub fn compile(&mut self, optimizer: Optimizer, loss: Loss) -> Result<(), AutodiffError> {
if matches!(optimizer, Optimizer::Lbfgs(_)) {
return Err(invalid(
"FunctionalModel::compile: Optimizer::Lbfgs は第 1 段で未対応(closure 駆動の更新経路が要るため)"
.to_string(),
));
}
let state = OptimizerState::new(optimizer)?;
self.compiled = Some(Compiled {
optimizer: state,
loss,
amp: None,
param_groups: Vec::new(),
});
Ok(())
}
#[cfg(test)]
pub(crate) fn is_compiled(&self) -> bool {
self.compiled.is_some()
}
fn check_io_counts(
&self,
method: &str,
x_count: usize,
y_count: usize,
) -> Result<(), AutodiffError> {
if x_count != self.inputs.len() {
return Err(invalid(format!(
"FunctionalModel::{method}: 入力テンソル数 {x_count} がモデルの入力数 {} と一致しない",
self.inputs.len()
)));
}
if y_count != self.outputs.len() {
return Err(invalid(format!(
"FunctionalModel::{method}: 目標テンソル数 {y_count} がモデルの出力数 {} と一致しない",
self.outputs.len()
)));
}
for (_, block) in self.blocks() {
block.reject_seq_first_layer_for_fit(&format!("FunctionalModel::{method}"))?;
}
Ok(())
}
pub fn fit<T: FitTarget>(
&mut self,
xs: &[&Tensor<f32>],
ys: &[&Tensor<T>],
config: FitConfig,
) -> Result<History, AutodiffError> {
let mut compiled = self.compiled.take().ok_or_else(|| not_compiled("fit"))?;
let prepared = self.prepare_fit(xs, ys, config);
let loader = match prepared {
Ok(loader) => loader,
Err(e) => {
self.compiled = Some(compiled);
return Err(e);
}
};
let prev_training = self.training();
self.set_training(true);
let result = self.run_fit(&mut compiled, &loader, config);
self.set_training(prev_training);
self.compiled = Some(compiled);
result
}
fn prepare_fit<T: FitTarget>(
&self,
xs: &[&Tensor<f32>],
ys: &[&Tensor<T>],
config: FitConfig,
) -> Result<DataLoader<GraphDataset<T>>, AutodiffError> {
if config.epochs == 0 {
return Err(invalid("FunctionalModel::fit: epochs == 0".to_string()));
}
if config.accumulate_steps != 1 {
return Err(invalid(format!(
"FunctionalModel::fit: accumulate_steps は 1 のみ対応(指定 {}。第 1 段の対象外)",
config.accumulate_steps
)));
}
if config
.validation_split_bits
.is_some_and(|b| f32::from_bits(b) != 0.0)
{
return Err(invalid(
"FunctionalModel::fit: validation_split は未対応(検証データを持たない)"
.to_string(),
));
}
self.check_io_counts("fit", xs.len(), ys.len())?;
for (_, block) in self.blocks() {
block.reject_untracked_parametric_layer("FunctionalModel::fit")?;
}
build_loader(
"fit",
xs,
ys,
DataLoaderConfig::new(config.batch_size)
.shuffle(config.shuffle)
.drop_last(config.drop_last),
)
}
fn run_fit<T: FitTarget>(
&mut self,
compiled: &mut Compiled,
loader: &DataLoader<GraphDataset<T>>,
config: FitConfig,
) -> Result<History, AutodiffError> {
let mut loss = Vec::new();
loss.try_reserve_exact(config.epochs)
.map_err(|_| super::super::alloc_failed())?;
let mut lr = Vec::new();
lr.try_reserve_exact(config.epochs)
.map_err(|_| super::super::alloc_failed())?;
for _ in 0..config.epochs {
lr.push(compiled.optimizer.lr());
let mut weighted_sum = 0.0f64;
let mut count = 0usize;
for batch in loader {
let (x_batch, y_batch) =
batch.map_err(|e| invalid(format!("FunctionalModel::fit: {e}")))?;
let n_batch = x_batch
.first()
.and_then(|t| t.shape().first().copied())
.unwrap_or(0);
let stepped = {
let tape = crate::tape();
let bound = self.bind(&tape);
let x_vars: Vec<Var<'_>> = x_batch.iter().map(|b| tape.var(b)).collect();
let preds = bound.forward(&tape, &x_vars)?;
let loss_var = combined_loss::<T>(compiled.loss, &tape, &preds, &y_batch)?;
let loss_scalar = loss_var.to_tensor().get(&[]).ok_or_else(|| {
invalid(
"FunctionalModel::fit: loss の shape が [] ではない(loss 演算の契約違反)"
.to_string(),
)
})?;
weighted_sum += loss_scalar as f64 * n_batch as f64;
count += n_batch;
let grads = tape.backward(&loss_var)?;
let grad_refs = bound.trainable_grads(&grads)?;
let param_refs = self.trainable_parameters();
compiled.optimizer.step_dispatch(
¶m_refs,
&grad_refs,
&compiled.param_groups,
)?
};
self.apply_parameters(stepped)?;
}
if count == 0 {
return Err(invalid(
"FunctionalModel::fit: この epoch で処理されたサンプルが 0 件(drop_last により全バッチが切り捨てられた可能性がある)"
.to_string(),
));
}
loss.push((weighted_sum / count as f64) as f32);
}
Ok(History {
loss,
val_loss: Vec::new(),
lr,
val_metrics: Vec::new(),
})
}
pub fn evaluate<T: FitTarget>(
&mut self,
xs: &[&Tensor<f32>],
ys: &[&Tensor<T>],
batch_size: usize,
) -> Result<f32, AutodiffError> {
let loss = self
.compiled
.as_ref()
.ok_or_else(|| not_compiled("evaluate"))?
.loss;
self.check_io_counts("evaluate", xs.len(), ys.len())?;
let loader = build_loader("evaluate", xs, ys, DataLoaderConfig::new(batch_size))?;
let prev_training = self.training();
self.set_training(false);
let result = self.run_evaluate(&loader, loss);
self.set_training(prev_training);
result
}
fn run_evaluate<T: FitTarget>(
&self,
loader: &DataLoader<GraphDataset<T>>,
loss: Loss,
) -> Result<f32, AutodiffError> {
let mut weighted_sum = 0.0f64;
let mut count = 0usize;
for batch in loader {
let (x_batch, y_batch) =
batch.map_err(|e| invalid(format!("FunctionalModel::evaluate: {e}")))?;
let n_batch = x_batch
.first()
.and_then(|t| t.shape().first().copied())
.unwrap_or(0);
let tape = crate::tape();
let x_vars: Vec<Var<'_>> = x_batch.iter().map(|b| tape.var(b)).collect();
let preds = self.forward(&tape, &x_vars)?;
let loss_var = combined_loss::<T>(loss, &tape, &preds, &y_batch)?;
let loss_scalar = loss_var.to_tensor().get(&[]).ok_or_else(|| {
invalid(
"FunctionalModel::evaluate: loss の shape が [] ではない(loss 演算の契約違反)"
.to_string(),
)
})?;
weighted_sum += loss_scalar as f64 * n_batch as f64;
count += n_batch;
}
if count == 0 {
return Err(invalid(
"FunctionalModel::evaluate: 処理されたサンプルが 0 件".to_string(),
));
}
Ok((weighted_sum / count as f64) as f32)
}
}
impl<'m, 't> FunctionalVars<'m, 't> {
pub(crate) fn forward(
&self,
tape: &'t Tape,
inputs: &[Var<'t>],
) -> Result<Vec<Var<'t>>, AutodiffError> {
let mut cursor = self.blocks.iter();
self.model
.eval_graph(tape, inputs, &mut |index, _block, x| match cursor.next() {
Some((bound_index, vars)) if *bound_index == index => vars.forward(tape, x),
_ => Err(invalid(format!(
"内部不整合: ブロックノード {index} の bind 結果が見つからない"
))),
})
}
#[cfg(test)]
pub(crate) fn trainable_vars(&self) -> Vec<&Var<'t>> {
self.blocks
.iter()
.flat_map(|(_, vars)| vars.trainable_vars())
.collect()
}
pub(crate) fn trainable_grads<'g>(
&self,
grads: &'g Gradients,
) -> Result<Vec<&'g Tensor<f32>>, AutodiffError> {
let mut out = Vec::new();
for (_, vars) in &self.blocks {
let part = vars.trainable_grads(grads)?;
out.try_reserve(part.len())
.map_err(|_| super::super::alloc_failed())?;
out.extend(part);
}
Ok(out)
}
}