use burn::{
Tensor,
config::Config,
module::Module,
nn::{
activation::ActivationConfig,
norm::NormalizationConfig,
},
prelude::Backend,
};
use crate::{
blocks::conv::{
ConvBlock2d,
ConvBlock2dConfig,
ConvBlock2dMeta,
},
burner::module::ModuleInit,
errors::{
BunsenError,
BunsenResult,
},
};
pub trait ConvSeq2dMeta {
fn block_metas(&self) -> Vec<&dyn ConvBlock2dMeta>;
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("ConvSeq2d must have at least one block")
.in_channels()
}
fn out_channels(&self) -> usize {
self.block_metas()
.last()
.expect("ConvSeq2d must have at least one block")
.out_channels()
}
fn stride(&self) -> [usize; 2] {
self.block_metas().iter().fold([1, 1], |acc, meta| {
let stride = meta.stride();
[acc[0] * stride[0], acc[1] * stride[1]]
})
}
fn validate(&self) -> BunsenResult<()> {
let metas = self.block_metas();
if metas.is_empty() {
return Err(BunsenError::Invalid(
"ConvSeq2d 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!(
"ConvSeq2d block {idx} out_channels ({}) != block {} in_channels ({})",
prev.out_channels(),
idx + 1,
next.in_channels(),
)));
}
}
Ok(())
}
fn try_output_resolution(
&self,
input_resolution: [usize; 2],
) -> BunsenResult<[usize; 2]> {
let mut resolution = input_resolution;
for (idx, meta) in self.block_metas().iter().enumerate() {
resolution = meta
.try_output_resolution(resolution)
.map_err(|err| BunsenError::Invalid(format!("ConvSeq2d block {idx}: {err}")))?;
}
Ok(resolution)
}
fn try_output_shape(
&self,
input_shape: [usize; 4],
) -> BunsenResult<[usize; 4]> {
let [batch_size, in_channels, in_height, in_width] = input_shape;
if in_channels != self.in_channels() {
return Err(BunsenError::Invalid(format!(
"ConvSeq2d expected in_channels ({}), got ({in_channels})",
self.in_channels(),
)));
}
let [out_height, out_width] = self.try_output_resolution([in_height, in_width])?;
Ok([batch_size, self.out_channels(), out_height, out_width])
}
}
#[derive(Config, Debug)]
pub struct ConvSeq2dConfig {
pub blocks: Vec<ConvBlock2dConfig>,
}
impl ConvSeq2dMeta for ConvSeq2dConfig {
fn block_metas(&self) -> Vec<&dyn ConvBlock2dMeta> {
self.blocks
.iter()
.map(|config| config as &dyn ConvBlock2dMeta)
.collect()
}
}
impl ConvSeq2dConfig {
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, ConvSeq2d<B>> for ConvSeq2dConfig {
fn try_init(
&self,
device: &B::Device,
) -> BunsenResult<ConvSeq2d<B>> {
let blocks = self
.blocks
.iter()
.map(|config| config.try_init(device))
.collect::<BunsenResult<Vec<_>>>()?;
ConvSeq2d::try_new(blocks)
}
}
#[derive(Module, Debug)]
pub struct ConvSeq2d<B: Backend> {
pub blocks: Vec<ConvBlock2d<B>>,
}
impl<B: Backend> ConvSeq2dMeta for ConvSeq2d<B> {
fn block_metas(&self) -> Vec<&dyn ConvBlock2dMeta> {
self.blocks
.iter()
.map(|block| block as &dyn ConvBlock2dMeta)
.collect()
}
}
impl<B: Backend> ConvSeq2d<B> {
pub fn try_new(blocks: Vec<ConvBlock2d<B>>) -> BunsenResult<Self> {
let seq = Self { blocks };
seq.validate()?;
Ok(seq)
}
pub fn forward(
&self,
input: Tensor<B, 4>,
) -> Tensor<B, 4> {
let mut output = input;
for block in &self.blocks {
output = block.forward(output);
}
output
}
}
#[cfg(test)]
mod tests {
use burn::{
backend::Autodiff,
nn::{
PaddingConfig2d,
activation::ActivationConfig,
conv::Conv2dConfig,
},
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,
) -> ConvBlock2dConfig {
ConvBlock2dConfig::new(
Conv2dConfig::new([in_channels, out_channels], [3, 3])
.with_stride([stride, stride])
.with_padding(PaddingConfig2d::Explicit(1, 1, 1, 1))
.with_bias(false),
)
.with_norm(None)
.with_act(Some(ActivationConfig::Relu))
}
fn block(
in_channels: usize,
out_channels: usize,
stride: usize,
) -> ConvBlock2d<B> {
block_config(in_channels, out_channels, stride).init(&Default::default())
}
#[test]
fn test_validate_empty() {
let err = ConvSeq2d::<B>::try_new(vec![]).unwrap_err();
assert!(matches!(err, BunsenError::Invalid(_)));
let err = ConvSeq2dConfig::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 = ConvSeq2d::try_new(blocks).unwrap_err();
assert!(matches!(err, BunsenError::Invalid(_)));
let result: BunsenResult<ConvSeq2d<B>> =
ConvSeq2dConfig::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 = ConvSeq2d::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, 4]);
}
#[test]
fn test_config_meta_matches_module() {
let config = ConvSeq2dConfig::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, 4]);
assert_eq!(
config.try_output_shape([1, 2, 16, 16]).unwrap(),
[1, 8, 4, 4]
);
let seq: ConvSeq2d<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, 16]).unwrap(),
config.try_output_shape([1, 2, 16, 16]).unwrap()
);
}
#[test]
fn test_output_resolution() {
let seq = ConvSeq2d::try_new(vec![block(2, 4, 2), block(4, 8, 2)]).unwrap();
assert_eq!(seq.try_output_resolution([16, 16]).unwrap(), [4, 4]);
assert_eq!(seq.try_output_resolution([12, 12]).unwrap(), [3, 3]);
assert_eq!(seq.try_output_resolution([6, 6]).unwrap(), [2, 2]);
}
#[test]
fn test_output_resolution_dilated() {
let device = Default::default();
let dilated = ConvBlock2dConfig::new(
Conv2dConfig::new([2, 4], [3, 3])
.with_stride([1, 1])
.with_dilation([2, 2])
.with_padding(PaddingConfig2d::Valid)
.with_bias(false),
)
.with_norm(None)
.with_act(None)
.init(&device);
let seq = ConvSeq2d::try_new(vec![dilated]).unwrap();
assert_eq!(seq.try_output_resolution([10, 12]).unwrap(), [6, 8]);
assert_eq!(seq.try_output_shape([1, 2, 10, 12]).unwrap(), [1, 4, 6, 8]);
let input = Tensor::<B, 4>::random([1, 2, 10, 12], Distribution::Default, &device);
assert_eq!(seq.forward(input).dims(), [1, 4, 6, 8]);
}
#[test]
fn test_output_shape_matches_forward() {
let device = Default::default();
let seq = ConvSeq2d::try_new(vec![block(2, 4, 2), block(4, 8, 2)]).unwrap();
let batch_size = 3;
let input = Tensor::<B, 4>::random([batch_size, 2, 16, 16], Distribution::Default, &device);
let predicted = seq.try_output_shape([batch_size, 2, 16, 16]).unwrap();
let actual = seq.forward(input).dims();
assert_eq!(predicted, [batch_size, 8, 4, 4]);
assert_eq!(predicted, actual);
}
#[test]
fn test_output_shape_channel_mismatch() {
let seq = ConvSeq2d::try_new(vec![block(2, 4, 1)]).unwrap();
assert!(seq.try_output_shape([1, 3, 8, 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 = ConvSeq2d::try_new(blocks).unwrap();
let input = Tensor::<B, 4>::random([2, 2, 8, 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);
}
}