nbml
A minimal machine learning library built on ndarray for low-level ML algorithm development in Rust.
Unlike high-level frameworks, nbml provides bare primitives and a lightweight optimizer API for building custom neural networks from scratch. If you want comfortable abstractions, see Burn. If you want to understand what's happening under the hood and have full control, nbml gives you the building blocks.
Features
- Core primitives: Transformers, Attention, LSTM, Conv2D, Feedforward layers, etc
- Activation functions: ReLU, Sigmoid, Tanh, etc
- Layers: Softmax, LayerNorm, Sequence Pooling
- Optimizers: AdamW, SGD
- Utilities: Variable Sequence Batching, Gradient Clipping, Gumbel Softmax, Plots, etc
- Minimal abstractions: Direct ndarray integration for custom algorithms
Quick Start
use FFN;
use Activation;
use ;
// Build a simple feedforward network
let mut model = FFNnew;
// Create optimizer
let mut optimizer = default.with;
// Training loop (simplified)
for batch in training_data
Architecture
NN Layers (nbml::nn)
Layer: Single nonlinear projection layerFFN: Feedforward network with configurable layersRNN: Vanilla recurrent neural networkLSTM: Long Short-Term Memory NetworkGRU: Gated Recurrent UnitESN: Echo-state network, fixed recurrence + readoutLSM: Liquid state machineRNNReservoir: RNN reservoir (used by ESN)SNNReservoir: Spiking neural network reservoir (used by LSM)Conv2D: Explicit Im2Col Conv2D layer (CPU efficient, memory hungry)PatchwiseConv2D: Patchwise Conv2D layer (CPU hungry, memory efficient)LinearSSM: Discrete Linear SSMAttention: Core softmax attention primitiveSelfAttention: Multi-head self attentionCrossAttention: Multi-head cross attentionLinearAttention: Linear self attention with recurrent matrix-valued state. Subquadratic alternative to softmax attention (Katharopoulos et al., 2020)GatedLinearAttention: Gated linear attention with matrix-valued state and outer-product gating (Yang et al., 2024)DeltaNet: Linear attention with delta rule for error-corrected state updates (Yang et al., 2024)GatedDeltaNet: Gated delta network combining data-dependent gating with delta rule updates (Yang et al., 2025)Transformer: Transformer encoder/decoder blockLinearTransformer: Transformer block using linear self attention instead of softmax attentionGlaTransformer: Transformer block using gated linear attention.DeltaNetTransformer: Transformer block using DeltaNet linear attention.GdnTransformer: Transformer block using Gated DeltaNet linear attention.
Layers (nbml::layers)
Layers that are only useful as components of other modules:
Dropout: Stateful dropout layerEmbedding: Learnable token embeddingsL2Norm: L2 normalizationLinear: Affine transformationSoftmax: Row-wise softmaxLayerNorm: Layer normalizationSequencePooling: Sequence mean-pooling
Optimizers (nbml::optim)
ToParams
ToParams connects your model's weights and gradients to the optimizer. Implement params() to return a list of Param entries, each pairing a weight array with its gradient. The optimizer reads these pointers on each step to update weights in-place — no ownership transfer, no framework magic.
Param::new and with_grad accept any ndarray dimension (Array1, Array2, Array3, etc.), so you don't need separate methods per shape:
Params compose — bubble them up from sub-modules to build arbitrary architectures:
ToParams also provides zero_grads() to reset all gradient arrays after an optimizer step:
let mut aa = new;
aa.forward // <- implement this yourself
aa.backward // <- implement this yourself
optimizer.step;
aa.zero_grads;
Available optimizers:
AdamW: Adaptive moment estimation with weight decaySGD: Stochastic gradient descent
Use .with(&mut impl ToParams) to initialize a stateful optimizer (like AdamW) for your network:
let mut model = new;
let mut optim = default.with; // creates momentum/variance state for all parameters
ToIntermediates
ToIntermediates lets you snapshot and restore a module's cached activations (the values stored during forward() that backward() needs for gradient computation). This enables training loops that aren't possible in standard frameworks:
- Recursive / weight-tied depth: Forward the same module N times, stashing intermediates between each call. During backward, restore each stash in reverse to compute correct weight gradients for every application.
- Online learning with rollback: Checkpoint recurrent state mid-sequence, run an optimizer step, then restore and continue from the checkpoint.
Implement intermediates() to return mutable references to your cached values:
Then stash_intermediates() and apply_intermediates() work automatically:
let mut model = new;
// Forward pass A
model.forward;
let stash_a = model.stash_intermediates;
// Forward pass B (overwrites cache)
model.forward;
model.backward; // correct grads for B
// Restore A's cache, compute A's grads
model.apply_intermediates;
model.backward; // correct grads for A
Intermediates are returned from stash_intermediates as Vec<ArrayD<T>> - aliased as IntermediateCache.
Activation Functions (nbml::f)
use f;
let x = from_vec;
let activated = relu;
Includes derivatives for backpropagation: d_relu, d_tanh, d_sigmoid, etc.
Design Philosophy
nbml is designed for:
- Experimentation / Research: Prototyping of novel architectures, through full control of forward and backward passes
- Nonstandard Architectures: A lot more freedom without autograd running the show
- Transparency: No hidden magic, every operation is explicit
- Compute-Constrained Deployment: Lightweight + no C deps. Very quick for small models
nbml is not designed for:
- Large Scale Production deployment (use PyTorch, TensorFlow, or Burn)
- Automatic differentiation (you wire up the backward pass for custom modules)
- GPU acceleration (CPU-only via ndarray)
The included nn primitives are technically plug and play, but when composing them you will have to wire backward() yourself.
Examples
Custom LSTM Training
use LSTM;
use ;
let mut lstm = LSTMnew;
let mut optimizer = default.with;
// where batch.dim() is (batch_size, seq_len, features)
// and features == lstm.d_model == (128 in this case)
for batch in data
Multi-Head Attention
use SelfAttention;
let mut attention = new;
// where input.dim() is (batch_size, seq_len, features)
// features == d_in == (512 in this case)
// and mask == (batch_size, seq_len, seq_len)
// with each element as 1. or 0. depending on whether or not the token
// is padding
let output = attention.forward;
Transformer Decoder
use Transformer;
use Array3;
let mut transformer = new_decoder;
let y_pred = transformer.forward;
// some bs.
let d_y_pred = ones;
transformer.backward;
transformer.zero_grads;