Skip to content

Training Models

The Sequential workflow

Stack layers, then CompileFit (or FitStep) → Predict:

net := model.NewSequential()
net.Add(layer.NewDense(8))
net.Add(&layer.Tanh{})
net.Add(layer.NewDense(1))
net.Add(&layer.Sigmoid{})

net.Compile(2, loss.MeanSquaredError{}, optim.NewAdam(0.05))
net.Fit(inputs, targets, 5000)   // 5000 epochs over the full batch

pred, _ := net.Predict(inputs)

Compile(inputCols, loss, optimizer) initializes every layer, threading the column count through the stack. Fit runs full-batch epochs; FitStep(input, target) runs exactly one forward/backward/update step and returns the loss, which is the building block for mini-batch training.

Datasets

Dataset pairs inputs with targets and provides shuffling, splitting, standardization, and buffer-reusing mini-batch iteration:

ds, _ := dataset.New(inputs, targets)
ds.Shuffle(rng)
train, test, _ := ds.Split(0.2)          // views, no copying
mean, std := train.Standardize()         // fit on train...
test.StandardizeWith(mean, std)          // ...apply to test

for epoch := 0; epoch < epochs; epoch++ {
    train.Batches(32, rng, func(in, tgt *tensai.Matrix) error {
        _, err := net.FitStep(in, tgt)
        return err
    })
}

Split returns copy-free views; Batches reuses its batch buffers across iterations, keeping allocation out of the inner loop.

A convolutional model

net := model.NewSequential()
net.Add(layer.NewConv2D(8, 3, 1, 1)) // outC, kernel, stride, pad
net.Add(&layer.ReLU{})
net.Add(layer.NewMaxPool2D(2))
net.Add(layer.NewDense(64))
net.Add(layer.NewBatchNorm())
net.Add(layer.NewLeakyReLU(0.01))
net.Add(layer.NewDropout(0.3))
net.Add(layer.NewDense(10))

// The input geometry is stated once; the spatial shape threads through
// the stack, so the conv and pool layers pick their dimensions up from it.
net.CompileImage(layer.Image{H: 28, W: 28, C: 1}, loss.SoftmaxCrossEntropy{}, optim.NewAdamW(0.001, 0.01))
net.Fit(inputs, targets, 10)

Each input row is a flattened channel-major image (index = (channel*height + y)*width + x). Dropout and BatchNorm are automatically in training mode inside Fit/FitStep and in inference mode inside Predict.

Saving and loading

Save/Load (and the SaveFile/LoadFile convenience wrappers) round-trip trained Sequential parameters as JSON, including BatchNorm running statistics:

net.SaveFile("model.json")

// Later: build + Compile the same architecture, then
net.LoadFile("model.json")

The architecture itself is not serialized — reconstruct the same layer stack and Compile before loading. Autograd parameters (RNN/LSTM/attention cells) are saved positionally with SaveParams/LoadParams — see Automatic Differentiation.

For deployment beyond Go, trained models export to TFLite and ONNX — see Model Formats.

Training a Sequential on the autograd engine

Fit runs the layers' own Forward and Backward. net.Graph() builds the same model as an autograd graph instead:

g, err := net.Graph()
trainer := autograd.NewTrainer(optim.NewAdam(0.01), g.Params()...)
tape := autograd.NewTape()
tape.UseDevice(dev) // optional; see the GPU guide
tape.Bind(g.Params()...)

for step := 0; step < steps; step++ {
    loss, _ := g.Loss(g.Forward(autograd.Input(x)), y)
    trainer.Step(loss)
    tape.Reset()
}
g.Sync() // brings a device's weights back into the layers

The parameters are the layers' own matrices, so training through the graph trains the model: Predict, Save and the exports keep working, and the graph's forward pass matches Predict to float32 rounding. What it adds is the GPU -- a graph runs wherever a tape sends it, which the hand-written stack cannot.

On a CPU the two are within a few percent of each other (go test -bench Step ./model), which is not where they started: comparing them turned up two things the layer path was doing that the graph was not, and fixing both made a step of a 1024-wide model 1.5x faster. The layer path allocates less -- it reuses a scratch buffer per layer, where the graph allocates its intermediates from the tape -- so it stays the default.

Every layer has a graph form; SetTraining switches Dropout and BatchNorm between their training and inference behaviour, as Fit and Predict do for the layer stack.

BatchNorm is worth a note: its graph form is the definition -- take the batch's mean and variance, normalize, scale and shift -- and the gradient the engine derives from that matches the hand-written one the layer carries, element for element. The running estimates it keeps update the same way too, since they are a side effect of the forward pass in both.

Low-allocation training

Layers reuse their forward/backward scratch buffers across training steps, so a full MLP step runs in ~29 allocations and GC stays out of the training loop. Predict always returns freshly allocated results, so predictions are safe to keep.