use std::{
collections::HashMap,
fmt::Display,
sync::atomic::{AtomicUsize, Ordering},
};
use nove_tensor::{DType, Device, Shape, Tensor};
use crate::{Model, ModelError};
static ID: AtomicUsize = AtomicUsize::new(0);
#[derive(Debug, Clone)]
pub struct Conv2d {
weight: Tensor,
bias: Option<Tensor>,
in_channels: usize,
out_channels: usize,
kernel_size: (usize, usize),
padding: usize,
stride: usize,
dilation: usize,
groups: usize,
id: usize,
}
impl Conv2d {
pub fn weight(&self) -> Tensor {
self.weight.clone()
}
pub fn bias(&self) -> Option<Tensor> {
self.bias.clone()
}
}
impl Model for Conv2d {
type Input = Tensor;
type Output = Tensor;
fn forward(&mut self, input: Self::Input) -> Result<Self::Output, ModelError> {
let y = input.conv2d(
&self.weight,
self.padding,
self.stride,
self.dilation,
self.groups,
)?;
let y = if let Some(bias) = &self.bias {
y.add(bias)?
} else {
y
};
Ok(y)
}
fn require_grad(&mut self, grad_enabled: bool) -> Result<(), ModelError> {
self.weight = self.weight.require_grad(grad_enabled)?;
if let Some(bias) = &mut self.bias {
self.bias = Some(bias.require_grad(grad_enabled)?);
}
Ok(())
}
fn to_device(&mut self, device: &Device) -> Result<(), ModelError> {
self.weight = self.weight.to_device(device)?;
if let Some(bias) = &mut self.bias {
self.bias = Some(bias.to_device(device)?);
}
Ok(())
}
fn to_dtype(&mut self, dtype: &DType) -> Result<(), ModelError> {
self.weight = self.weight.to_dtype(dtype)?;
if let Some(bias) = &mut self.bias {
self.bias = Some(bias.to_dtype(dtype)?);
}
Ok(())
}
fn parameters(&self) -> Result<Vec<Tensor>, ModelError> {
match &self.bias {
Some(bias) => Ok(vec![self.weight.clone(), bias.clone()]),
None => Ok(vec![self.weight.clone()]),
}
}
fn named_parameters(&self) -> Result<HashMap<String, Tensor>, ModelError> {
Ok(self
.parameters()?
.into_iter()
.map(|t| match t.name()? {
Some(name) => Ok((name, t)),
None => Err(ModelError::ParameterMissingName),
})
.collect::<Result<HashMap<_, _>, ModelError>>()?)
}
}
impl Display for Conv2d {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"conv2d.{}(in_channels={}, out_channels={}, kernel_size={:?}, stride={}, padding={}, dilation={}, groups={}, bias_enabled={})",
self.id,
self.in_channels,
self.out_channels,
self.kernel_size,
self.stride,
self.padding,
self.dilation,
self.groups,
self.bias.is_some(),
)
}
}
pub struct Conv2dBuilder {
in_channels: Option<usize>,
out_channels: Option<usize>,
kernel_size: Option<(usize, usize)>,
padding: usize,
stride: usize,
dilation: usize,
groups: usize,
bias_enabled: bool,
device: Device,
dtype: DType,
grad_enabled: bool,
}
impl Default for Conv2dBuilder {
fn default() -> Self {
Self {
in_channels: None,
out_channels: None,
kernel_size: None,
padding: 0,
stride: 1,
dilation: 1,
groups: 1,
bias_enabled: true,
device: Device::cpu(),
dtype: DType::F32,
grad_enabled: true,
}
}
}
impl Conv2dBuilder {
pub fn in_channels(&mut self, in_channels: usize) -> &mut Self {
self.in_channels = Some(in_channels);
self
}
pub fn out_channels(&mut self, out_channels: usize) -> &mut Self {
self.out_channels = Some(out_channels);
self
}
pub fn kernel_size(&mut self, kernel_size: (usize, usize)) -> &mut Self {
self.kernel_size = Some(kernel_size);
self
}
pub fn padding(&mut self, padding: usize) -> &mut Self {
self.padding = padding;
self
}
pub fn stride(&mut self, stride: usize) -> &mut Self {
self.stride = stride;
self
}
pub fn dilation(&mut self, dilation: usize) -> &mut Self {
self.dilation = dilation;
self
}
pub fn groups(&mut self, groups: usize) -> &mut Self {
self.groups = groups;
self
}
pub fn bias_enabled(&mut self, bias_enabled: bool) -> &mut Self {
self.bias_enabled = bias_enabled;
self
}
pub fn device(&mut self, device: Device) -> &mut Self {
self.device = device;
self
}
pub fn dtype(&mut self, dtype: DType) -> &mut Self {
self.dtype = dtype;
self
}
pub fn grad_enabled(&mut self, grad_enabled: bool) -> &mut Self {
self.grad_enabled = grad_enabled;
self
}
pub fn build(&self) -> Result<Conv2d, ModelError> {
let in_channels = self.in_channels.ok_or(ModelError::MissingArgument(
"in_channels in Conv2dBuilder".to_string(),
))?;
let out_channels = self.out_channels.ok_or(ModelError::MissingArgument(
"out_channels in Conv2dBuilder".to_string(),
))?;
let kernel_size = self.kernel_size.ok_or(ModelError::MissingArgument(
"kernel_size in Conv2dBuilder".to_string(),
))?;
if kernel_size.0 == 0 || kernel_size.1 == 0 {
return Err(ModelError::InvalidArgument(
"kernel_size in Conv2dBuilder must be greater than 0".to_string(),
));
}
if in_channels % self.groups != 0 {
return Err(ModelError::InvalidArgument(
"in_channels must be divisible by groups".to_string(),
));
}
if out_channels % self.groups != 0 {
return Err(ModelError::InvalidArgument(
"out_channels must be divisible by groups".to_string(),
));
}
if self.stride == 0 {
return Err(ModelError::InvalidArgument(
"stride in Conv2dBuilder must be greater than 0".to_string(),
));
}
if self.dilation == 0 {
return Err(ModelError::InvalidArgument(
"dilation in Conv2dBuilder must be greater than 0".to_string(),
));
}
if self.groups == 0 {
return Err(ModelError::InvalidArgument(
"groups in Conv2dBuilder must be greater than 0".to_string(),
));
}
let id = ID.fetch_add(1, Ordering::Relaxed);
let fan_in = (in_channels / self.groups) * kernel_size.0 * kernel_size.1;
let std = (2.0 / fan_in as f32).sqrt();
let weight = Tensor::randn(
0.0,
std,
&Shape::from_dims(&[
out_channels,
in_channels / self.groups,
kernel_size.0,
kernel_size.1,
]),
&self.device,
self.grad_enabled,
)?
.to_dtype(&self.dtype)?
.require_name(&format!("conv2d.{}.weight", id))?;
let bias = if self.bias_enabled {
let bias = Tensor::zeros(
&Shape::from_dims(&[out_channels, 1, 1]),
&self.dtype,
&self.device,
self.grad_enabled,
)?
.to_dtype(&self.dtype)?
.require_name(&format!("conv2d.{}.bias", id))?;
Some(bias)
} else {
None
};
Ok(Conv2d {
weight,
bias,
in_channels,
out_channels,
kernel_size,
padding: self.padding,
stride: self.stride,
dilation: self.dilation,
groups: self.groups,
id,
})
}
}