use burn::{
Tensor,
config::Config,
module::Module,
nn::{
activation::ActivationConfig,
norm::NormalizationConfig,
},
prelude::Backend,
};
use crate::{
blocks::conv::{
ConvBlock1d,
ConvBlock1dConfig,
ConvBlock1dMeta,
},
burner::module::ModuleInit,
errors::{
BunsenError,
BunsenResult,
},
};
pub trait ConvSeq1dMeta {
fn block_metas(&self) -> Vec<&dyn ConvBlock1dMeta>;
fn len(&self) -> usize {
self.block_metas().len()
}
fn is_empty(&self) -> bool {
self.block_metas().is_empty()
}
fn in_channels(&self) -> usize {
self.block_metas()
.first()
.expect("ConvSeq1d must have at least one block")
.in_channels()
}
fn out_channels(&self) -> usize {
self.block_metas()
.last()
.expect("ConvSeq1d must have at least one block")
.out_channels()
}
fn stride(&self) -> usize {
self.block_metas()
.iter()
.map(|meta| meta.stride()[0])
.product()
}
fn validate(&self) -> BunsenResult<()> {
let metas = self.block_metas();
if metas.is_empty() {
return Err(BunsenError::Invalid(
"ConvSeq1d must have at least one block".to_string(),
));
}
for (idx, pair) in metas.windows(2).enumerate() {
let prev = pair[0];
let next = pair[1];
if prev.out_channels() != next.in_channels() {
return Err(BunsenError::Invalid(format!(
"ConvSeq1d block {idx} out_channels ({}) != block {} in_channels ({})",
prev.out_channels(),
idx + 1,
next.in_channels(),
)));
}
}
Ok(())
}
fn try_output_length(
&self,
in_length: usize,
) -> BunsenResult<usize> {
let mut length = in_length;
for (idx, meta) in self.block_metas().iter().enumerate() {
length = meta
.try_output_length(length)
.map_err(|err| BunsenError::Invalid(format!("ConvSeq1d block {idx}: {err}")))?;
}
Ok(length)
}
fn try_output_shape(
&self,
input_shape: [usize; 3],
) -> BunsenResult<[usize; 3]> {
let [batch_size, in_channels, in_length] = input_shape;
if in_channels != self.in_channels() {
return Err(BunsenError::Invalid(format!(
"ConvSeq1d expected in_channels ({}), got ({in_channels})",
self.in_channels(),
)));
}
let out_length = self.try_output_length(in_length)?;
Ok([batch_size, self.out_channels(), out_length])
}
}
#[derive(Config, Debug)]
pub struct ConvSeq1dConfig {
pub blocks: Vec<ConvBlock1dConfig>,
}
impl ConvSeq1dMeta for ConvSeq1dConfig {
fn block_metas(&self) -> Vec<&dyn ConvBlock1dMeta> {
self.blocks
.iter()
.map(|config| config as &dyn ConvBlock1dMeta)
.collect()
}
}
impl ConvSeq1dConfig {
pub fn with_act<A: Into<Option<ActivationConfig>>>(
self,
act: A,
) -> Self {
let act = act.into();
Self {
blocks: self
.blocks
.into_iter()
.map(|block| block.with_act(act.clone()))
.collect(),
}
}
pub fn with_norm<N: Into<Option<NormalizationConfig>>>(
self,
norm: N,
) -> Self {
let norm = norm.into();
Self {
blocks: self
.blocks
.into_iter()
.map(|block| block.with_norm(norm.clone()))
.collect(),
}
}
}
impl<B: Backend> ModuleInit<B, ConvSeq1d<B>> for ConvSeq1dConfig {
fn try_init(
&self,
device: &B::Device,
) -> BunsenResult<ConvSeq1d<B>> {
let blocks = self
.blocks
.iter()
.map(|config| config.try_init(device))
.collect::<BunsenResult<Vec<_>>>()?;
ConvSeq1d::try_new(blocks)
}
}
#[derive(Module, Debug)]
pub struct ConvSeq1d<B: Backend> {
pub blocks: Vec<ConvBlock1d<B>>,
}
impl<B: Backend> ConvSeq1dMeta for ConvSeq1d<B> {
fn block_metas(&self) -> Vec<&dyn ConvBlock1dMeta> {
self.blocks
.iter()
.map(|block| block as &dyn ConvBlock1dMeta)
.collect()
}
}
impl<B: Backend> ConvSeq1d<B> {
pub fn try_new(blocks: Vec<ConvBlock1d<B>>) -> BunsenResult<Self> {
let seq = Self { blocks };
seq.validate()?;
Ok(seq)
}
pub fn forward(
&self,
input: Tensor<B, 3>,
) -> Tensor<B, 3> {
let mut output = input;
for block in &self.blocks {
output = block.forward(output);
}
output
}
}
#[cfg(test)]
mod tests {
use burn::{
backend::Autodiff,
nn::{
PaddingConfig1d,
activation::ActivationConfig,
conv::Conv1dConfig,
},
tensor::Distribution,
};
use super::*;
use crate::support::testing::CpuBackend;
type I = CpuBackend;
type B = Autodiff<I>;
fn block_config(
in_channels: usize,
out_channels: usize,
stride: usize,
) -> ConvBlock1dConfig {
ConvBlock1dConfig::new(
Conv1dConfig::new(in_channels, out_channels, 3)
.with_stride(stride)
.with_padding(PaddingConfig1d::Explicit(1, 1))
.with_bias(false),
)
.with_norm(None)
.with_act(Some(ActivationConfig::Relu))
}
fn block(
in_channels: usize,
out_channels: usize,
stride: usize,
) -> ConvBlock1d<B> {
block_config(in_channels, out_channels, stride).init(&Default::default())
}
#[test]
fn test_validate_empty() {
let err = ConvSeq1d::<B>::try_new(vec![]).unwrap_err();
assert!(matches!(err, BunsenError::Invalid(_)));
let err = ConvSeq1dConfig::new(vec![]).validate().unwrap_err();
assert!(matches!(err, BunsenError::Invalid(_)));
}
#[test]
fn test_validate_channel_mismatch() {
let blocks = vec![block(2, 4, 1), block(8, 16, 1)];
let err = ConvSeq1d::try_new(blocks).unwrap_err();
assert!(matches!(err, BunsenError::Invalid(_)));
let result: BunsenResult<ConvSeq1d<B>> =
ConvSeq1dConfig::new(vec![block_config(2, 4, 1), block_config(8, 16, 1)])
.try_init(&Default::default());
assert!(matches!(result.unwrap_err(), BunsenError::Invalid(_)));
}
#[test]
fn test_meta() {
let seq = ConvSeq1d::try_new(vec![block(2, 4, 2), block(4, 8, 2), block(8, 8, 1)]).unwrap();
assert_eq!(seq.len(), 3);
assert!(!seq.is_empty());
assert_eq!(seq.in_channels(), 2);
assert_eq!(seq.out_channels(), 8);
assert_eq!(seq.stride(), 4);
}
#[test]
fn test_config_meta_matches_module() {
let config = ConvSeq1dConfig::new(vec![block_config(2, 4, 2), block_config(4, 8, 2)]);
assert_eq!(config.len(), 2);
assert_eq!(config.in_channels(), 2);
assert_eq!(config.out_channels(), 8);
assert_eq!(config.stride(), 4);
assert_eq!(config.try_output_shape([1, 2, 16]).unwrap(), [1, 8, 4]);
let seq: ConvSeq1d<B> = config.init(&Default::default());
assert_eq!(seq.in_channels(), config.in_channels());
assert_eq!(seq.out_channels(), config.out_channels());
assert_eq!(seq.stride(), config.stride());
assert_eq!(
seq.try_output_shape([1, 2, 16]).unwrap(),
config.try_output_shape([1, 2, 16]).unwrap()
);
}
#[test]
fn test_output_length() {
let seq = ConvSeq1d::try_new(vec![block(2, 4, 2), block(4, 8, 2)]).unwrap();
assert_eq!(seq.try_output_length(16).unwrap(), 4);
assert_eq!(seq.try_output_length(12).unwrap(), 3);
assert_eq!(seq.try_output_length(6).unwrap(), 2);
}
#[test]
fn test_output_length_dilated() {
let device = Default::default();
let dilated = ConvBlock1dConfig::new(
Conv1dConfig::new(2, 4, 3)
.with_stride(1)
.with_dilation(2)
.with_padding(PaddingConfig1d::Valid)
.with_bias(false),
)
.with_norm(None)
.with_act(None)
.init(&device);
let seq = ConvSeq1d::try_new(vec![dilated]).unwrap();
assert_eq!(seq.try_output_length(10).unwrap(), 6);
assert_eq!(seq.try_output_shape([1, 2, 10]).unwrap(), [1, 4, 6]);
let input = Tensor::<B, 3>::random([1, 2, 10], Distribution::Default, &device);
assert_eq!(seq.forward(input).dims(), [1, 4, 6]);
}
#[test]
fn test_output_shape_matches_forward() {
let device = Default::default();
let seq = ConvSeq1d::try_new(vec![block(2, 4, 2), block(4, 8, 2)]).unwrap();
let batch_size = 3;
let in_length = 16;
let input =
Tensor::<B, 3>::random([batch_size, 2, in_length], Distribution::Default, &device);
let predicted = seq.try_output_shape([batch_size, 2, in_length]).unwrap();
let actual = seq.forward(input).dims();
assert_eq!(predicted, [batch_size, 8, 4]);
assert_eq!(predicted, actual);
}
#[test]
fn test_output_shape_channel_mismatch() {
let seq = ConvSeq1d::try_new(vec![block(2, 4, 1)]).unwrap();
assert!(seq.try_output_shape([1, 3, 8]).is_err());
}
#[test]
fn test_forward_matches_sequential() {
let device = Default::default();
let blocks = vec![block(2, 4, 2), block(4, 8, 1)];
let seq = ConvSeq1d::try_new(blocks).unwrap();
let input = Tensor::<B, 3>::random([2, 2, 8], Distribution::Default, &device);
let output = seq.forward(input.clone());
let expected = {
let mut x = input;
for b in &seq.blocks {
x = b.forward(x);
}
x
};
output.to_data().assert_eq(&expected.to_data(), true);
}
}