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 family | Input | Output |
|---|---|---|
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 pooling | Convolution axis order | Reduced 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:
- Valid convolution produces
[2, 6, 6, 4]. - Pooling produces
[2, 3, 3, 4]. - Flatten produces
[2, 36]. - 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.