use fandhe_ai::optim::Adam;
use fandhe_ai::{AutodiffError, Tape, Tensor, Var};
pub trait ReferenceModule {
fn forward<'t>(&self, tape: &'t Tape, x: &Var<'t>) -> Result<Var<'t>, AutodiffError>;
fn named_parameters(&self) -> Vec<(String, &Tensor<f32>)>;
fn set_training(&mut self, training: bool);
fn is_training(&self) -> bool;
}
pub trait Trainable: ReferenceModule {
fn train_step(
&mut self,
x: &Tensor<f32>,
y: &Tensor<i32>,
opt: &mut Adam,
) -> Result<f32, AutodiffError>;
}
pub fn cross_entropy_mean<'t>(
tape: &'t Tape,
logits: &Var<'t>,
targets: &Tensor<i32>,
num_classes: usize,
) -> Result<Var<'t>, AutodiffError> {
if num_classes == 0 {
return Err(AutodiffError::InvalidArgument(
"cross_entropy_mean: num_classes は 0 より大きい必要がある".to_string(),
));
}
let shape = targets.shape();
if shape.len() != 1 {
return Err(AutodiffError::InvalidArgument(format!(
"cross_entropy_mean: targets の rank は 1 である必要がある(実際: {shape:?})"
)));
}
let n = shape[0];
if n == 0 {
return Err(AutodiffError::InvalidArgument(
"cross_entropy_mean: targets は空であってはならない".to_string(),
));
}
let mut target_idx = vec![0i32; n];
for (i, slot) in target_idx.iter_mut().enumerate() {
let t = targets.get(&[i]).ok_or_else(|| {
AutodiffError::InvalidArgument(format!(
"cross_entropy_mean: targets[{i}] の読み出しに失敗した"
))
})?;
if t < 0 || (t as usize) >= num_classes {
return Err(AutodiffError::InvalidArgument(format!(
"cross_entropy_mean: targets[{i}]={t} が [0, {num_classes}) の範囲外"
)));
}
*slot = t;
}
let target_idx_tensor = Tensor::new(target_idx, &[n, 1]).map_err(|e| {
AutodiffError::InvalidArgument(format!(
"cross_entropy_mean: target_idx テンソル構築に失敗: {e}"
))
})?;
let weight = vec![1.0f32 / n as f32; n];
let weight_tensor = Tensor::new(weight, &[n, 1]).map_err(|e| {
AutodiffError::InvalidArgument(format!(
"cross_entropy_mean: weight テンソル構築に失敗: {e}"
))
})?;
let weight_var = tape.var(&weight_tensor);
let logits_shape = logits.to_tensor().shape().to_vec();
if logits_shape != [n, num_classes] {
return Err(AutodiffError::InvalidArgument(format!(
"cross_entropy_mean: logits の shape は [{n}, {num_classes}] である必要がある \
(実際: {logits_shape:?})"
)));
}
let log_probs = logits.log_softmax(1)?;
let selected = log_probs.gather(1, &target_idx_tensor)?;
let weighted = selected.mul(&weight_var)?;
let summed = weighted.sum(None)?;
summed.neg()
}
pub fn scalar_of(t: &Tensor<f32>) -> Result<f32, AutodiffError> {
t.get(&[]).ok_or_else(|| {
AutodiffError::InvalidArgument(
"scalar_of: loss テンソルの shape が [] ではない".to_string(),
)
})
}
pub fn heldout_loss<M: ReferenceModule>(
model: &mut M,
x: &Tensor<f32>,
y: &Tensor<i32>,
batch_size: usize,
num_classes: usize,
) -> Result<f32, AutodiffError> {
if batch_size == 0 {
return Err(AutodiffError::InvalidArgument(
"heldout_loss: batch_size は 0 より大きい必要がある".to_string(),
));
}
let x_shape = x.shape();
if x_shape.is_empty() || x_shape[0] == 0 {
return Err(AutodiffError::InvalidArgument(
"heldout_loss: x は rank 1 以上かつ空でない必要がある".to_string(),
));
}
let n = x_shape[0];
if y.shape() != [n] {
return Err(AutodiffError::InvalidArgument(format!(
"heldout_loss: x の先頭軸長 {n} と y の shape {:?} が一致しない",
y.shape()
)));
}
let original_training = model.is_training();
model.set_training(false);
let result = heldout_loss_in_eval(model, x, y, n, batch_size, num_classes);
model.set_training(original_training);
result
}
fn heldout_loss_in_eval<M: ReferenceModule>(
model: &M,
x: &Tensor<f32>,
y: &Tensor<i32>,
n: usize,
batch_size: usize,
num_classes: usize,
) -> Result<f32, AutodiffError> {
let mut total = 0.0f32;
let mut start = 0;
while start < n {
let len = batch_size.min(n - start);
let xb = sub_tensor_f32(x, start, len)?;
let yb = sub_tensor_i32(y, start, len)?;
let tape = fandhe_ai::tape();
let xv = tape.var(&xb);
let logits = model.forward(&tape, &xv)?;
let loss = cross_entropy_mean(&tape, &logits, &yb, num_classes)?;
total += scalar_of(&loss.to_tensor())? * len as f32;
start += len;
}
Ok(total / n as f32)
}
pub fn predict_in_eval<M: ReferenceModule>(
model: &mut M,
x: &Tensor<f32>,
) -> Result<Tensor<f32>, AutodiffError> {
let original_training = model.is_training();
model.set_training(false);
let tape = fandhe_ai::tape();
let xv = tape.var(x);
let result = model.forward(&tape, &xv).map(|y| y.to_tensor());
model.set_training(original_training);
result
}
pub fn sub_tensor_f32(
x: &Tensor<f32>,
start: usize,
len: usize,
) -> Result<Tensor<f32>, AutodiffError> {
let full_shape = x.shape();
if full_shape.is_empty() {
return Err(AutodiffError::InvalidArgument(
"sub_tensor_f32: x の rank は 1 以上である必要がある(スカラーは不可)".to_string(),
));
}
let n = full_shape[0];
let end_idx = start.checked_add(len).ok_or_else(|| {
AutodiffError::InvalidArgument(format!(
"sub_tensor_f32: start({start})と len({len})の和が usize の範囲を超える"
))
})?;
if end_idx > n {
return Err(AutodiffError::InvalidArgument(format!(
"sub_tensor_f32: [{start}, {end_idx}) が先頭軸の長さ {n} を超える"
)));
}
let item_len: usize = full_shape[1..].iter().product();
let contiguous = x.contiguous();
let flat = contiguous.as_slice().ok_or_else(|| {
AutodiffError::InvalidArgument("sub_tensor_f32: contiguous() 直後は必ず Some".to_string())
})?;
let begin = start * item_len;
let end = begin + len * item_len;
let mut shape = full_shape.to_vec();
shape[0] = len;
Tensor::new(flat[begin..end].to_vec(), &shape).map_err(|e| {
AutodiffError::InvalidArgument(format!("sub_tensor_f32: テンソル再構築に失敗: {e}"))
})
}
pub fn sub_tensor_i32(
y: &Tensor<i32>,
start: usize,
len: usize,
) -> Result<Tensor<i32>, AutodiffError> {
let y_shape = y.shape();
if y_shape.len() != 1 {
return Err(AutodiffError::InvalidArgument(format!(
"sub_tensor_i32: y の rank は 1 である必要がある(実際: {y_shape:?})"
)));
}
let n = y_shape[0];
let end_idx = start.checked_add(len).ok_or_else(|| {
AutodiffError::InvalidArgument(format!(
"sub_tensor_i32: start({start})と len({len})の和が usize の範囲を超える"
))
})?;
if end_idx > n {
return Err(AutodiffError::InvalidArgument(format!(
"sub_tensor_i32: [{start}, {end_idx}) が先頭軸の長さ {n} を超える"
)));
}
let contiguous = y.contiguous();
let flat = contiguous.as_slice().ok_or_else(|| {
AutodiffError::InvalidArgument("sub_tensor_i32: contiguous() 直後は必ず Some".to_string())
})?;
Tensor::new(flat[start..end_idx].to_vec(), &[len]).map_err(|e| {
AutodiffError::InvalidArgument(format!("sub_tensor_i32: テンソル再構築に失敗: {e}"))
})
}
pub fn fit_epochs<M: Trainable>(
model: &mut M,
x: &Tensor<f32>,
y: &Tensor<i32>,
opt: &mut Adam,
epochs: usize,
batch_size: usize,
) -> Result<Vec<f32>, AutodiffError> {
if epochs == 0 || batch_size == 0 {
return Err(AutodiffError::InvalidArgument(
"fit_epochs: epochs・batch_size はいずれも 0 より大きい必要がある".to_string(),
));
}
let x_shape = x.shape();
if x_shape.is_empty() {
return Err(AutodiffError::InvalidArgument(
"fit_epochs: x の rank は 1 以上である必要がある(スカラーは不可)".to_string(),
));
}
let n = x_shape[0];
if n == 0 {
return Err(AutodiffError::InvalidArgument(
"fit_epochs: x は空であってはならない".to_string(),
));
}
if y.shape() != [n] {
return Err(AutodiffError::InvalidArgument(format!(
"fit_epochs: x の先頭軸長 {n} と y の shape {:?} が一致しない",
y.shape()
)));
}
model.set_training(true);
let mut history = Vec::with_capacity(epochs);
for _ in 0..epochs {
let mut total = 0.0f32;
let mut start = 0;
while start < n {
let len = batch_size.min(n - start);
let xb = sub_tensor_f32(x, start, len)?;
let yb = sub_tensor_i32(y, start, len)?;
let loss = model.train_step(&xb, &yb, opt)?;
total += loss * len as f32;
start += len;
}
history.push(total / n as f32);
}
Ok(history)
}
pub fn accuracy<M: ReferenceModule>(
model: &mut M,
x: &Tensor<f32>,
y: &Tensor<i32>,
batch_size: usize,
num_classes: usize,
) -> Result<f32, AutodiffError> {
if batch_size == 0 {
return Err(AutodiffError::InvalidArgument(
"accuracy: batch_size は 0 より大きい必要がある".to_string(),
));
}
if num_classes == 0 {
return Err(AutodiffError::InvalidArgument(
"accuracy: num_classes は 0 より大きい必要がある".to_string(),
));
}
let x_shape = x.shape();
if x_shape.is_empty() {
return Err(AutodiffError::InvalidArgument(
"accuracy: x の rank は 1 以上である必要がある(スカラーは不可)".to_string(),
));
}
let n = x_shape[0];
if n == 0 {
return Err(AutodiffError::InvalidArgument(
"accuracy: x は空であってはならない".to_string(),
));
}
if y.shape() != [n] {
return Err(AutodiffError::InvalidArgument(format!(
"accuracy: x の先頭軸長 {n} と y の shape {:?} が一致しない",
y.shape()
)));
}
for i in 0..n {
let label = y.get(&[i]).ok_or_else(|| {
AutodiffError::InvalidArgument(format!("accuracy: y[{i}] の読み出しに失敗した"))
})?;
if label < 0 || (label as usize) >= num_classes {
return Err(AutodiffError::InvalidArgument(format!(
"accuracy: y[{i}]={label} が [0, {num_classes}) の範囲外"
)));
}
}
let original_training = model.is_training();
model.set_training(false);
let result = accuracy_in_eval(model, x, y, n, batch_size, num_classes);
model.set_training(original_training);
result
}
fn accuracy_in_eval<M: ReferenceModule>(
model: &mut M,
x: &Tensor<f32>,
y: &Tensor<i32>,
n: usize,
batch_size: usize,
num_classes: usize,
) -> Result<f32, AutodiffError> {
let mut correct = 0usize;
let mut start = 0;
while start < n {
let len = batch_size.min(n - start);
let xb = sub_tensor_f32(x, start, len)?;
let yb = sub_tensor_i32(y, start, len)?;
let tape = fandhe_ai::tape();
let xv = tape.var(&xb);
let logits = model.forward(&tape, &xv)?;
let out = logits.to_tensor();
let out_shape = out.shape().to_vec();
if out_shape != [len, num_classes] {
return Err(AutodiffError::InvalidArgument(format!(
"accuracy: logits の shape は [{len}, {num_classes}] である必要がある \
(実際: {out_shape:?})"
)));
}
let contiguous = out.contiguous();
let flat = contiguous.as_slice().ok_or_else(|| {
AutodiffError::InvalidArgument("accuracy: contiguous() 直後は必ず Some".to_string())
})?;
for i in 0..len {
let row = &flat[i * num_classes..(i + 1) * num_classes];
if let Some((idx, &v)) = row.iter().enumerate().find(|&(_, &v)| !v.is_finite()) {
return Err(AutodiffError::InvalidArgument(format!(
"accuracy: logits[{}][{idx}] が非有限({v})",
start + i
)));
}
let mut best_idx = 0usize;
let mut best_val = row[0];
for (idx, &v) in row.iter().enumerate().skip(1) {
if v > best_val {
best_val = v;
best_idx = idx;
}
}
let label = yb.get(&[i]).ok_or_else(|| {
AutodiffError::InvalidArgument(format!("accuracy: targets[{i}] の読み出しに失敗"))
})?;
if best_idx as i32 == label {
correct += 1;
}
}
start += len;
}
Ok(correct as f32 / n as f32)
}