Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Neural network layers

Choose layers by the shape of each sample and the output your task needs. All tensors use f32 values and place the batch axis first. The neural network prelude exports the layers below.

Common shape rules

Layer familyInputOutput
Dense(units)[batch, ..., features], rank at least two[batch, ..., units]
Conv1D[batch, length, channels][batch, output_length, filters]
Conv2D[batch, height, width, channels][batch, output_height, output_width, filters]
Conv3D[batch, depth, height, width, channels]Same axis order with new spatial sizes and filters
Local poolingConvolution axis orderReduced spatial axes, unchanged channels
Flatten[batch, ...][batch, product_of_remaining_axes]
Recurrent, default[batch, time, features][batch, units]
Recurrent, return sequences[batch, time, features][batch, time, units]

These rows describe shape behavior, not constructor syntax. Image and convolution tensors place channels last. For grayscale images, keep the final channel dimension equal to one. A single image still needs a batch axis.

Dense layers

Create a dense layer with Dense::new(units, activation)?. Use Activation::ReLU for a simple hidden layer. Choose the final activation to match your loss. See output and loss choices.

Dense replaces only the last axis and preserves all leading axes. For example, [8, 12, 6] becomes [8, 12, 4] with four units. Add Flatten::new() first when you need one vector per image. Without flattening, Dense processes each leading position separately.

Convolution and pooling

Conv2D::new(filters, kernel_size, strides, activation)? configures a two-dimensional convolution. Its default padding is PaddingType::Valid. Call .with_padding(PaddingType::Same) to request padding that preserves spatial size when the stride equals one. Use .with_dilation_rate(...) when you need a larger effective kernel.

For each spatial axis, let effective_kernel = dilation * (kernel - 1) + 1. With valid padding, the output size is floor((input - effective_kernel) / stride) + 1. The effective kernel must fit the input for valid padding. With same padding, the output size is ceil(input / stride). Apply these rules separately to each spatial axis.

MaxPooling2D::new((2, 2)) keeps the largest value in each window. AveragePooling2D uses window averages. Local pooling preserves the channel count. Pool strides default to the pool size, with valid padding. Override them using with_strides(...)? and with_padding(...). Global pooling reduces all spatial positions to one value per channel.

The image example below transforms [2, 8, 8, 1] through these shapes:

  1. Valid convolution produces [2, 6, 6, 4].
  2. Pooling produces [2, 3, 3, 4].
  3. Flatten produces [2, 36].
  4. Dense produces [2, 3].

Recurrent layers

Use SimpleRNN, LSTM, or GRU for ordered sequences. Each constructor takes units and an activation, such as LSTM::new(4, Activation::Tanh)?. Each expects [batch, time, features] input. The default output contains the final hidden state, shaped [batch, units]. Call .with_return_sequences(true) to return one hidden state per timestep.

Enable sequence output before stacking another recurrent layer. A following Dense layer then maps every timestep separately. For one prediction per sequence, keep the final recurrent layer default output. Call .with_go_backwards(true) to process timesteps in reverse order.

Run image and sequence stacks

Prediction does not require an optimizer or loss. This example uses zero inputs to verify both stack shapes. The source also lives in guide/examples/nn_shapes.rs.

use ndarray::Array;
use rustyml::prelude::neural_network::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let images = Array::<f32, _>::zeros((2, 8, 8, 1)).into_dyn();
    let cnn = SequentialBuilder::new()
        .add(Conv2D::new(4, (3, 3), (1, 1), Activation::ReLU)?.with_random_state(11))
        .add(MaxPooling2D::new((2, 2)))
        .add(Flatten::new())
        .add(Dense::new(3, Activation::Linear)?.with_random_state(12))
        .build(&Shape::known(images.shape()))?;
    println!("image predictions: {:?}", cnn.predict(&images)?.shape());

    let sequence = Array::<f32, _>::zeros((2, 5, 3)).into_dyn();
    let rnn = SequentialBuilder::new()
        .add(
            LSTM::new(4, Activation::Tanh)?
                .with_random_state(13)
                .with_return_sequences(true),
        )
        .add(Dense::new(2, Activation::Linear)?.with_random_state(14))
        .build(&Shape::known(sequence.shape()))?;
    println!(
        "sequence predictions: {:?}",
        rnn.predict(&sequence)?.shape()
    );
    Ok(())
}

Expected output:

image predictions: [2, 3]
sequence predictions: [2, 5, 2]

Fix shape errors

Compare every layer output with the next layer input requirements. Use model.summary() to inspect the complete stack. Build with the actual input tensor shape before training. Check channel order when convolution rejects image input. Check with_return_sequences when recurrent layers reject a preceding layer output. Check target dimensions against the selected loss before fitting.