use crate::compat::Sequential;
use crate::optim::Adam;
use crate::{AutodiffError, Tape, Tensor, Var};
use super::train_support::{check_model_size, cross_entropy_mean, scalar_of, take_updated};
const BN_EPS: f32 = 1e-5;
const BN_MOMENTUM: f32 = 0.1;
pub struct ResNetBlock {
main: Sequential,
shortcut: Option<Sequential>,
}
impl ResNetBlock {
fn new(
in_channels: usize,
out_channels: usize,
stride: usize,
seed: u64,
) -> Result<Self, AutodiffError> {
if in_channels == 0 || out_channels == 0 || stride == 0 {
return Err(AutodiffError::InvalidArgument(
"ResNetBlock::new: in_channels・out_channels・stride はいずれも 0 より \
大きい必要がある"
.to_string(),
));
}
let main = Sequential::new()
.add_conv2d(
in_channels,
out_channels,
[3, 3],
[stride, stride],
[1, 1],
[1, 1],
1,
seed,
)?
.add_batch_norm2d(out_channels, BN_EPS, BN_MOMENTUM)?
.add_relu()
.add_conv2d(
out_channels,
out_channels,
[3, 3],
[1, 1],
[1, 1],
[1, 1],
1,
seed.wrapping_add(1),
)?
.add_batch_norm2d(out_channels, BN_EPS, BN_MOMENTUM)?;
let shortcut = if stride != 1 || in_channels != out_channels {
Some(
Sequential::new()
.add_conv2d(
in_channels,
out_channels,
[1, 1],
[stride, stride],
[0, 0],
[1, 1],
1,
seed.wrapping_add(2),
)?
.add_batch_norm2d(out_channels, BN_EPS, BN_MOMENTUM)?,
)
} else {
None
};
Ok(ResNetBlock { main, shortcut })
}
pub fn has_projection_shortcut(&self) -> bool {
self.shortcut.is_some()
}
fn forward<'t>(&self, tape: &'t Tape, x: &Var<'t>) -> Result<Var<'t>, AutodiffError> {
let main_out = self.main.forward(tape, x)?;
let shortcut_out = match &self.shortcut {
Some(s) => s.forward(tape, x)?,
None => *x,
};
let summed = main_out.add(&shortcut_out)?;
Ok(summed.relu())
}
fn named_parameters(&self) -> Vec<(String, &Tensor<f32>)> {
let mut out: Vec<(String, &Tensor<f32>)> = self
.main
.named_parameters()
.into_iter()
.map(|(k, v)| (format!("main.{k}"), v))
.collect();
if let Some(shortcut) = &self.shortcut {
out.extend(
shortcut
.named_parameters()
.into_iter()
.map(|(k, v)| (format!("shortcut.{k}"), v)),
);
}
out
}
fn set_training(&mut self, training: bool) {
if training {
self.main.train();
if let Some(shortcut) = &mut self.shortcut {
shortcut.train();
}
} else {
self.main.eval();
if let Some(shortcut) = &mut self.shortcut {
shortcut.eval();
}
}
}
}
pub struct ResNet {
stem: Sequential,
blocks: Vec<ResNetBlock>,
head: Sequential,
depth: usize,
width: usize,
num_classes: usize,
}
impl ResNet {
pub fn new(
depth: usize,
width: usize,
num_classes: usize,
seed: u64,
) -> Result<Self, AutodiffError> {
if width == 0 || num_classes == 0 {
return Err(AutodiffError::InvalidArgument(
"ResNet::new: width・num_classes はいずれも 0 より大きい必要がある".to_string(),
));
}
if depth < 8 || !(depth - 2).is_multiple_of(6) {
return Err(AutodiffError::InvalidArgument(format!(
"ResNet::new: depth は 6n+2(n >= 1。8, 14, 20, ...)である必要がある \
(実際: {depth})"
)));
}
let n = (depth - 2) / 6;
let width_x2 = width.checked_mul(2).ok_or_else(|| {
AutodiffError::InvalidArgument(format!(
"ResNet::new: width({width})* 2 が usize の範囲を超える"
))
})?;
let width_x4 = width.checked_mul(4).ok_or_else(|| {
AutodiffError::InvalidArgument(format!(
"ResNet::new: width({width})* 4 が usize の範囲を超える"
))
})?;
let total_blocks = 3usize.checked_mul(n).ok_or_else(|| {
AutodiffError::InvalidArgument(format!(
"ResNet::new: block 総数(3 * n。n={n})が usize の範囲を超える"
))
})?;
let per_block = width_x4
.saturating_mul(width_x4)
.saturating_mul(18)
.saturating_add(width_x4.saturating_mul(8));
let estimated = total_blocks
.saturating_mul(per_block)
.saturating_add(width_x4.saturating_mul(num_classes))
.saturating_add(width.saturating_mul(27));
check_model_size("ResNet::new", estimated)?;
let stem = Sequential::new()
.add_conv2d(3, width, [3, 3], [1, 1], [1, 1], [1, 1], 1, seed)?
.add_batch_norm2d(width, BN_EPS, BN_MOMENTUM)?
.add_relu();
let mut blocks = Vec::with_capacity(total_blocks);
let mut in_channels = width;
let mut seed_ctr = seed.wrapping_add(10);
for (stage_idx, &out_channels) in [width, width_x2, width_x4].iter().enumerate() {
for block_idx in 0..n {
let stride = if stage_idx > 0 && block_idx == 0 {
2
} else {
1
};
blocks.push(ResNetBlock::new(
in_channels,
out_channels,
stride,
seed_ctr,
)?);
seed_ctr = seed_ctr.wrapping_add(10);
in_channels = out_channels;
}
}
let head = Sequential::new()
.add_adaptive_avg_pool2d([1, 1])?
.add_flatten(1, 3)
.add_linear(width_x4, num_classes, seed_ctr)?;
Ok(ResNet {
stem,
blocks,
head,
depth,
width,
num_classes,
})
}
pub fn forward<'t>(&self, tape: &'t Tape, x: &Var<'t>) -> Result<Var<'t>, AutodiffError> {
let input_shape = x.to_tensor().shape().to_vec();
if input_shape.len() != 4 || input_shape[1] != 3 {
return Err(AutodiffError::InvalidArgument(format!(
"ResNet::forward: 入力 shape は [N, 3, H, W] である必要がある(実際: \
{input_shape:?})"
)));
}
let mut current = self.stem.forward(tape, x)?;
for block in &self.blocks {
current = block.forward(tape, ¤t)?;
}
self.head.forward(tape, ¤t)
}
pub fn predict(&self, x: &Tensor<f32>) -> Result<Tensor<f32>, AutodiffError> {
let tape = crate::tape();
let xv = tape.var(x);
let y = self.forward(&tape, &xv)?;
Ok(y.to_tensor())
}
pub fn named_parameters(&self) -> Vec<(String, &Tensor<f32>)> {
let mut out: Vec<(String, &Tensor<f32>)> = self
.stem
.named_parameters()
.into_iter()
.map(|(k, v)| (format!("stem.{k}"), v))
.collect();
for (i, block) in self.blocks.iter().enumerate() {
out.extend(
block
.named_parameters()
.into_iter()
.map(|(k, v)| (format!("layer.{i}.{k}"), v)),
);
}
out.extend(
self.head
.named_parameters()
.into_iter()
.map(|(k, v)| (format!("head.{k}"), v)),
);
out
}
pub fn set_training(&mut self, training: bool) {
if training {
self.stem.train();
self.head.train();
} else {
self.stem.eval();
self.head.eval();
}
for block in &mut self.blocks {
block.set_training(training);
}
}
pub fn training(&self) -> bool {
self.stem.training()
}
pub fn train_step(
&mut self,
x: &Tensor<f32>,
y: &Tensor<i32>,
opt: &mut Adam,
) -> Result<f32, AutodiffError> {
let input_shape = x.shape();
if input_shape.len() != 4 || input_shape[1] != 3 {
return Err(AutodiffError::InvalidArgument(format!(
"ResNet::train_step: 入力 shape は [N, 3, H, W] である必要がある(実際: \
{input_shape:?})"
)));
}
self.set_training(true);
let (loss_value, updated) = {
let tape = crate::tape();
let xv = tape.var(x);
let stem_bound = self.stem.bind(&tape);
let mut block_bounds = Vec::with_capacity(self.blocks.len());
for block in &self.blocks {
let main_bound = block.main.bind(&tape);
let shortcut_bound = block.shortcut.as_ref().map(|s| s.bind(&tape));
block_bounds.push((main_bound, shortcut_bound));
}
let head_bound = self.head.bind(&tape);
let mut current = stem_bound.forward(&tape, &xv)?;
for (main_bound, shortcut_bound) in &block_bounds {
let main_out = main_bound.forward(&tape, ¤t)?;
let shortcut_out = match shortcut_bound {
Some(s) => s.forward(&tape, ¤t)?,
None => current,
};
current = main_out.add(&shortcut_out)?.relu();
}
let logits = head_bound.forward(&tape, ¤t)?;
let loss = cross_entropy_mean(&tape, &logits, y, self.num_classes)?;
let loss_value = scalar_of(&loss.to_tensor())?;
let grads = tape.backward(&loss)?;
let mut param_refs: Vec<&Tensor<f32>> = self.stem.trainable_parameters();
for block in &self.blocks {
param_refs.extend(block.main.trainable_parameters());
if let Some(shortcut) = &block.shortcut {
param_refs.extend(shortcut.trainable_parameters());
}
}
param_refs.extend(self.head.trainable_parameters());
let mut grad_refs: Vec<&Tensor<f32>> = stem_bound.trainable_grads(&grads)?;
for (main_bound, shortcut_bound) in &block_bounds {
grad_refs.extend(main_bound.trainable_grads(&grads)?);
if let Some(s) = shortcut_bound {
grad_refs.extend(s.trainable_grads(&grads)?);
}
}
grad_refs.extend(head_bound.trainable_grads(&grads)?);
if param_refs.len() != grad_refs.len() {
return Err(AutodiffError::InvalidArgument(format!(
"ResNet::train_step: パラメータ数({})と勾配数({})が不一致",
param_refs.len(),
grad_refs.len()
)));
}
let pairs: Vec<(&Tensor<f32>, &Tensor<f32>)> =
param_refs.into_iter().zip(grad_refs).collect();
let updated = opt.step(&pairs)?;
(loss_value, updated)
};
let mut idx = 0usize;
let stem_n = self.stem.trainable_parameters().len();
self.stem
.apply_parameters(take_updated(&updated, &mut idx, stem_n)?)?;
for block in &mut self.blocks {
let main_n = block.main.trainable_parameters().len();
block
.main
.apply_parameters(take_updated(&updated, &mut idx, main_n)?)?;
if let Some(shortcut) = &mut block.shortcut {
let shortcut_n = shortcut.trainable_parameters().len();
shortcut.apply_parameters(take_updated(&updated, &mut idx, shortcut_n)?)?;
}
}
let head_n = self.head.trainable_parameters().len();
self.head
.apply_parameters(take_updated(&updated, &mut idx, head_n)?)?;
if idx != updated.len() {
return Err(AutodiffError::InvalidArgument(format!(
"ResNet::train_step: 更新後パラメータ数({})が消費数({idx})と不一致",
updated.len()
)));
}
Ok(loss_value)
}
pub fn depth(&self) -> usize {
self.depth
}
pub fn width(&self) -> usize {
self.width
}
pub fn num_classes(&self) -> usize {
self.num_classes
}
pub fn num_blocks(&self) -> usize {
self.blocks.len()
}
pub fn blocks(&self) -> &[ResNetBlock] {
&self.blocks
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_rejects_huge_arguments_without_panic() {
assert!(matches!(
ResNet::new(usize::MAX - 1, 1, 1, 0),
Err(AutodiffError::InvalidArgument(_))
));
assert!(matches!(
ResNet::new(8, 1 << 20, 10, 0),
Err(AutodiffError::InvalidArgument(_))
));
assert!(matches!(
ResNet::new(8, 4, 1 << 40, 0),
Err(AutodiffError::InvalidArgument(_))
));
assert!(ResNet::new(8, 4, 10, 0).is_ok());
}
#[test]
fn block_rejects_zero_arguments() {
assert!(matches!(
ResNetBlock::new(0, 4, 1, 1),
Err(AutodiffError::InvalidArgument(_))
));
assert!(matches!(
ResNetBlock::new(4, 4, 0, 1),
Err(AutodiffError::InvalidArgument(_))
));
}
#[test]
fn projection_block_has_twelve_parameters_with_main_and_shortcut_prefixes() {
let block = ResNetBlock::new(4, 8, 2, 0x5555_6666).unwrap();
assert!(block.has_projection_shortcut());
let params = block.named_parameters();
assert_eq!(params.len(), 12);
assert!(params.iter().any(|(n, _)| n.starts_with("main.")));
assert!(params.iter().any(|(n, _)| n.starts_with("shortcut.")));
}
#[test]
fn identity_block_has_no_shortcut_parameters() {
let block = ResNetBlock::new(4, 4, 1, 1).unwrap();
assert!(!block.has_projection_shortcut());
assert!(
block
.named_parameters()
.iter()
.all(|(n, _)| n.starts_with("main."))
);
}
}