use crate::{AutodiffError, Gradients, LinearVars, Tape, Tensor, Var};
use fandhe_ai_autodiff::nn::activation::{Relu, Sigmoid, Tanh};
use fandhe_ai_autodiff::nn::{Linear, Module};
pub struct Sequential {
layers: Vec<Box<dyn Module>>,
}
impl Default for Sequential {
fn default() -> Self {
Self::new()
}
}
impl Sequential {
pub fn new() -> Self {
Sequential { layers: Vec::new() }
}
pub fn add_linear(
mut self,
in_features: usize,
out_features: usize,
seed: u64,
) -> Result<Self, AutodiffError> {
let linear = Linear::new(in_features, out_features, true, seed)?;
self.layers.push(Box::new(linear));
Ok(self)
}
pub fn add_relu(mut self) -> Self {
self.layers.push(Box::new(Relu));
self
}
pub fn add_sigmoid(mut self) -> Self {
self.layers.push(Box::new(Sigmoid));
self
}
pub fn add_tanh(mut self) -> Self {
self.layers.push(Box::new(Tanh));
self
}
pub fn forward<'t>(&self, tape: &'t Tape, input: &Var<'t>) -> Result<Var<'t>, AutodiffError> {
let mut current = *input;
for layer in &self.layers {
current = layer.forward(&tape.0, ¤t)?;
}
Ok(current)
}
pub fn predict(&self, input: &Tensor<f32>) -> Result<Tensor<f32>, AutodiffError> {
let tape = crate::tape();
let input_var = tape.var(input);
let output = self.forward(&tape, &input_var)?;
Ok(output.to_tensor())
}
pub fn bind<'m, 't>(&'m self, tape: &'t Tape) -> SequentialVars<'m, 't> {
let linears = self
.layers
.iter()
.filter_map(|layer| layer.as_linear())
.map(|linear| linear.bind(&tape.0))
.collect();
SequentialVars {
model: self,
linears,
}
}
pub fn trainable_parameters(&self) -> Vec<&Tensor<f32>> {
let mut out = Vec::new();
for layer in &self.layers {
if let Some(linear) = layer.as_linear() {
out.push(linear.weight());
if let Some(bias) = linear.bias() {
out.push(bias);
}
}
}
out
}
pub fn apply_parameters(&mut self, updated: Vec<Tensor<f32>>) -> Result<(), AutodiffError> {
let linears: Vec<&mut Linear> = self
.layers
.iter_mut()
.filter_map(|layer| layer.as_linear_mut())
.collect();
let mut updated = updated.into_iter();
let mut rebuilt = Vec::with_capacity(linears.len());
for (layer_index, linear) in linears.iter().enumerate() {
let has_bias = linear.bias().is_some();
let new_weight = updated.next().ok_or_else(|| {
AutodiffError::InvalidArgument(
"Sequential::apply_parameters: updated has fewer elements than \
trainable_parameters() (weight missing)"
.to_string(),
)
})?;
let expected_shape = linear.weight().shape();
if new_weight.shape() != expected_shape {
return Err(AutodiffError::InvalidArgument(format!(
"Sequential::apply_parameters: layer {layer_index} weight shape changed \
from {expected_shape:?} to {:?} (apply_parameters only supports \
shape-preserving updates; #426)",
new_weight.shape()
)));
}
let new_bias = if has_bias {
Some(updated.next().ok_or_else(|| {
AutodiffError::InvalidArgument(
"Sequential::apply_parameters: updated has fewer elements than \
trainable_parameters() (bias missing)"
.to_string(),
)
})?)
} else {
None
};
rebuilt.push(Linear::from_parameters(new_weight, new_bias)?);
}
if updated.next().is_some() {
return Err(AutodiffError::InvalidArgument(
"Sequential::apply_parameters: updated has more elements than \
trainable_parameters()"
.to_string(),
));
}
for (linear, new_linear) in linears.into_iter().zip(rebuilt) {
*linear = new_linear;
}
Ok(())
}
}
pub struct SequentialVars<'m, 't> {
model: &'m Sequential,
linears: Vec<LinearVars<'t>>,
}
impl<'m, 't> SequentialVars<'m, 't> {
pub fn forward(&self, tape: &'t Tape, input: &Var<'t>) -> Result<Var<'t>, AutodiffError> {
let mut current = *input;
let mut linears = self.linears.iter();
for layer in &self.model.layers {
if layer.as_linear().is_some() {
let vars = linears.next().ok_or_else(|| {
AutodiffError::InvalidArgument(
"SequentialVars::forward: bind 済み LinearVars が model.layers の \
Linear 層数より少ない(bind/forward 間の Linear 層数対応が崩れた)"
.to_string(),
)
})?;
current = vars.forward(¤t)?;
} else {
current = layer.forward(&tape.0, ¤t)?;
}
}
Ok(current)
}
pub fn linears(&self) -> &[LinearVars<'t>] {
&self.linears
}
pub fn trainable_vars(&self) -> Vec<&Var<'t>> {
let mut out = Vec::new();
for vars in &self.linears {
out.push(&vars.weight);
if let Some(bias) = &vars.bias {
out.push(bias);
}
}
out
}
pub fn trainable_grads<'g>(
&self,
grads: &'g Gradients,
) -> Result<Vec<&'g Tensor<f32>>, AutodiffError> {
let mut out = Vec::new();
for vars in &self.linears {
let weight_grad = grads.get(&vars.weight)?.ok_or_else(|| {
AutodiffError::InvalidArgument(
"SequentialVars::trainable_grads: weight に到達する勾配がない \
(loss へ未到達)"
.to_string(),
)
})?;
out.push(weight_grad);
if let Some(bias) = &vars.bias {
let bias_grad = grads.get(bias)?.ok_or_else(|| {
AutodiffError::InvalidArgument(
"SequentialVars::trainable_grads: bias に到達する勾配がない \
(loss へ未到達)"
.to_string(),
)
})?;
out.push(bias_grad);
}
}
Ok(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
const SEED1: u64 = 1001;
const SEED2: u64 = 2002;
fn dense_vec(t: &Tensor<f32>) -> Vec<f32> {
t.contiguous()
.as_slice()
.expect("contiguous() 直後は必ず as_slice() が Some を返す")
.to_vec()
}
#[test]
fn sequential_builds_and_predicts_two_layer_mlp() {
let model = Sequential::new()
.add_linear(8, 16, SEED1)
.unwrap()
.add_relu()
.add_linear(16, 4, SEED2)
.unwrap();
let batch = 3;
let input = Tensor::new(vec![0.1_f32; batch * 8], &[batch, 8]).unwrap();
let output = model.predict(&input).unwrap();
assert_eq!(output.shape(), &[batch, 4]);
}
#[test]
fn sequential_forward_matches_manual_forward_bit_exact() {
let linear1 = Linear::new(8, 16, true, SEED1).unwrap();
let linear2 = Linear::new(16, 4, true, SEED2).unwrap();
let batch = 2;
let input_data: Vec<f32> = (0..batch * 8).map(|i| i as f32 * 0.05).collect();
let input_tensor = Tensor::new(input_data, &[batch, 8]).unwrap();
let manual_tape = crate::tape();
let manual_input = manual_tape.var(&input_tensor);
let h = linear1.bind(&manual_tape.0).forward(&manual_input).unwrap();
let h = h.relu();
let manual_output = linear2.bind(&manual_tape.0).forward(&h).unwrap();
let model = Sequential {
layers: vec![Box::new(linear1), Box::new(Relu), Box::new(linear2)],
};
let seq_tape = crate::tape();
let seq_input = seq_tape.var(&input_tensor);
let seq_output = model.forward(&seq_tape, &seq_input).unwrap();
assert_eq!(
dense_vec(&manual_output.to_tensor()),
dense_vec(&seq_output.to_tensor())
);
}
#[test]
fn sequential_forward_on_external_tape_supports_backward() {
let model = Sequential::new()
.add_linear(4, 3, SEED1)
.unwrap()
.add_sigmoid();
let tape = crate::tape();
let input = tape.var(&Tensor::new(vec![0.1_f32, 0.2, 0.3, 0.4], &[1, 4]).unwrap());
let output = model.forward(&tape, &input).unwrap();
let loss = output.sum(None).unwrap();
let grads = tape.backward(&loss).unwrap();
let input_grad = grads
.get(&input)
.unwrap()
.expect("入力は loss に寄与している");
assert_eq!(input_grad.shape(), input.to_tensor().shape());
}
#[test]
fn add_linear_propagates_invalid_argument() {
let result = Sequential::new().add_linear(0, 4, SEED1);
match result {
Err(err) => assert!(matches!(err, AutodiffError::InvalidArgument(_))),
Ok(_) => panic!("in_features == 0 は拒否されるはず"),
}
}
#[test]
fn bind_returns_linear_vars_in_layer_order_with_mixed_bias() {
let l1 = Linear::new(4, 3, true, SEED1).unwrap();
let l2 = Linear::new(3, 2, false, SEED2).unwrap();
let model = Sequential {
layers: vec![Box::new(l1), Box::new(Relu), Box::new(l2)],
};
let tape = crate::tape();
let bound = model.bind(&tape);
assert_eq!(bound.linears().len(), 2);
assert!(bound.linears()[0].bias.is_some());
assert!(bound.linears()[1].bias.is_none());
}
#[test]
fn sequential_vars_forward_matches_manual_forward_bit_exact() {
let batch = 2;
let input_data: Vec<f32> = (0..batch * 8).map(|i| i as f32 * 0.05).collect();
let input_tensor = Tensor::new(input_data, &[batch, 8]).unwrap();
let module_model = Sequential {
layers: vec![
Box::new(Linear::new(8, 16, true, SEED1).unwrap()),
Box::new(Relu),
Box::new(Linear::new(16, 4, true, SEED2).unwrap()),
],
};
let module_tape = crate::tape();
let module_input = module_tape.var(&input_tensor);
let module_output = module_model.forward(&module_tape, &module_input).unwrap();
let train_model = Sequential {
layers: vec![
Box::new(Linear::new(8, 16, true, SEED1).unwrap()),
Box::new(Relu),
Box::new(Linear::new(16, 4, true, SEED2).unwrap()),
],
};
let train_tape = crate::tape();
let bound = train_model.bind(&train_tape);
let train_input = train_tape.var(&input_tensor);
let train_output = bound.forward(&train_tape, &train_input).unwrap();
assert_eq!(
dense_vec(&module_output.to_tensor()),
dense_vec(&train_output.to_tensor())
);
}
#[test]
fn trainable_parameters_order_contract_weight_then_bias_per_layer() {
let model = Sequential::new()
.add_linear(4, 3, SEED1) .unwrap()
.add_relu()
.add_linear(3, 2, SEED2) .unwrap();
let params = model.trainable_parameters();
assert_eq!(params.len(), 4);
assert_eq!(params[0].shape(), &[4, 3]); assert_eq!(params[1].shape(), &[3]); assert_eq!(params[2].shape(), &[3, 2]); assert_eq!(params[3].shape(), &[2]); }
#[test]
fn apply_parameters_rejects_element_count_mismatch() {
let mut model = Sequential::new().add_linear(4, 3, SEED1).unwrap();
let updated = vec![Tensor::new(vec![0.0f32; 12], &[4, 3]).unwrap()];
let err = model.apply_parameters(updated).unwrap_err();
assert!(matches!(err, AutodiffError::InvalidArgument(_)));
}
#[test]
fn apply_parameters_propagates_bias_shape_mismatch() {
let mut model = Sequential::new().add_linear(4, 3, SEED1).unwrap();
let weight = Tensor::new(vec![0.0f32; 12], &[4, 3]).unwrap();
let bad_bias = Tensor::new(vec![0.0f32; 5], &[5]).unwrap();
let err = model.apply_parameters(vec![weight, bad_bias]).unwrap_err();
assert!(matches!(err, AutodiffError::Shape(_)));
}
#[test]
fn apply_parameters_leaves_model_unchanged_on_excess_element_count() {
let mut model = Sequential::new().add_linear(2, 1, SEED1).unwrap();
let original_params: Vec<Tensor<f32>> =
model.trainable_parameters().into_iter().cloned().collect();
let weight = Tensor::new(vec![9.0f32, 9.0], &[2, 1]).unwrap();
let bias = Tensor::new(vec![9.0f32], &[1]).unwrap();
let extra = Tensor::new(vec![9.0f32], &[1]).unwrap();
let err = model
.apply_parameters(vec![weight, bias, extra])
.unwrap_err();
assert!(matches!(err, AutodiffError::InvalidArgument(_)));
let after_params = model.trainable_parameters();
assert_eq!(original_params.len(), after_params.len());
for (before, after) in original_params.iter().zip(after_params.iter()) {
assert_eq!(dense_vec(before), dense_vec(after));
}
}
#[test]
fn apply_parameters_updates_weights_used_by_subsequent_predict() {
let mut model = Sequential::new().add_linear(2, 1, SEED1).unwrap();
let zero_weight = Tensor::new(vec![0.0f32, 0.0], &[2, 1]).unwrap();
let zero_bias = Tensor::new(vec![7.0f32], &[1]).unwrap();
model
.apply_parameters(vec![zero_weight, zero_bias])
.unwrap();
let input = Tensor::new(vec![1.0f32, 2.0], &[1, 2]).unwrap();
let output = model.predict(&input).unwrap();
assert!((output.get(&[0, 0]).unwrap() - 7.0).abs() < 1e-6);
}
#[test]
fn apply_parameters_updates_bias_less_layer() {
let linear = Linear::new(2, 1, false, SEED1).unwrap();
let mut model = Sequential {
layers: vec![Box::new(linear)],
};
assert_eq!(model.trainable_parameters().len(), 1);
let zero_weight = Tensor::new(vec![0.0f32, 0.0], &[2, 1]).unwrap();
model.apply_parameters(vec![zero_weight]).unwrap();
let input = Tensor::new(vec![1.0f32, 2.0], &[1, 2]).unwrap();
let output = model.predict(&input).unwrap();
assert!((output.get(&[0, 0]).unwrap()).abs() < 1e-6);
}
#[test]
fn apply_parameters_leaves_model_unchanged_on_mid_sequence_shape_error() {
let mut model = Sequential::new()
.add_linear(2, 3, SEED1)
.unwrap()
.add_relu()
.add_linear(3, 1, SEED2)
.unwrap();
let original_params: Vec<Tensor<f32>> =
model.trainable_parameters().into_iter().cloned().collect();
let l1_weight = Tensor::new(vec![9.0f32; 6], &[2, 3]).unwrap();
let l1_bias = Tensor::new(vec![9.0f32; 3], &[3]).unwrap();
let l2_weight = Tensor::new(vec![9.0f32; 3], &[3, 1]).unwrap();
let l2_bad_bias = Tensor::new(vec![9.0f32; 5], &[5]).unwrap();
let err = model
.apply_parameters(vec![l1_weight, l1_bias, l2_weight, l2_bad_bias])
.unwrap_err();
assert!(matches!(err, AutodiffError::Shape(_)));
let after_params = model.trainable_parameters();
assert_eq!(original_params.len(), after_params.len());
for (before, after) in original_params.iter().zip(after_params.iter()) {
assert_eq!(dense_vec(before), dense_vec(after));
}
}
#[test]
fn apply_parameters_rejects_weight_shape_change() {
let mut model = Sequential::new().add_linear(4, 3, SEED1).unwrap();
let original_params: Vec<Tensor<f32>> =
model.trainable_parameters().into_iter().cloned().collect();
let new_weight = Tensor::new(vec![0.0f32; 9], &[3, 3]).unwrap();
let new_bias = Tensor::new(vec![0.0f32; 3], &[3]).unwrap();
let err = model
.apply_parameters(vec![new_weight, new_bias])
.unwrap_err();
assert!(matches!(err, AutodiffError::InvalidArgument(_)));
let after_params = model.trainable_parameters();
assert_eq!(original_params.len(), after_params.len());
for (before, after) in original_params.iter().zip(after_params.iter()) {
assert_eq!(dense_vec(before), dense_vec(after));
}
}
#[test]
fn apply_parameters_rejects_interlayer_consistent_resize() {
let mut model = Sequential::new()
.add_linear(2, 3, SEED1)
.unwrap()
.add_relu()
.add_linear(3, 1, SEED2)
.unwrap();
let l1_weight = Tensor::new(vec![0.0f32; 8], &[2, 4]).unwrap();
let l1_bias = Tensor::new(vec![0.0f32; 4], &[4]).unwrap();
let l2_weight = Tensor::new(vec![0.0f32; 4], &[4, 1]).unwrap();
let l2_bias = Tensor::new(vec![0.0f32; 1], &[1]).unwrap();
let err = model
.apply_parameters(vec![l1_weight, l1_bias, l2_weight, l2_bias])
.unwrap_err();
assert!(matches!(err, AutodiffError::InvalidArgument(_)));
}
#[test]
fn apply_parameters_leaves_model_unchanged_on_mid_sequence_weight_shape_error() {
let mut model = Sequential::new()
.add_linear(2, 3, SEED1)
.unwrap()
.add_relu()
.add_linear(3, 1, SEED2)
.unwrap();
let original_params: Vec<Tensor<f32>> =
model.trainable_parameters().into_iter().cloned().collect();
let l1_weight = Tensor::new(vec![9.0f32; 6], &[2, 3]).unwrap();
let l1_bias = Tensor::new(vec![9.0f32; 3], &[3]).unwrap();
let l2_weight = Tensor::new(vec![9.0f32; 6], &[3, 2]).unwrap();
let l2_bias = Tensor::new(vec![9.0f32; 2], &[2]).unwrap();
let err = model
.apply_parameters(vec![l1_weight, l1_bias, l2_weight, l2_bias])
.unwrap_err();
assert!(matches!(err, AutodiffError::InvalidArgument(_)));
let after_params = model.trainable_parameters();
assert_eq!(original_params.len(), after_params.len());
for (before, after) in original_params.iter().zip(after_params.iter()) {
assert_eq!(dense_vec(before), dense_vec(after));
}
}
}