Heads and fine-tuning¶
The search fits every neuron on its own and freezes it once its layer is done; nothing in it trains two neurons together. This page covers what reads the prediction off the last layer, the three optional passes that do train neurons together, and pruning:
| Step | Call or key | Trains |
|---|---|---|
| head fit | Trainer.train_out_proj |
a linear head over the last layer's survivors |
| per-layer fine-tune | train.layer_finetune |
each layer's survivors together, as the search goes |
| end-to-end pass | Trainer.train_finetune |
every parameter of the network at once |
| pruning | Trainer.prune |
nothing: it deletes what the prediction does not use |
Without a head¶
By default the model predicts with one neuron: the survivor of the last
layer with the lowest criterion value. For regression infer returns that
neuron's output, for binary its output is the logit, and a multi-class
model passes it through its class map (see
Classification). The result is the classical GMDH
model, a single polynomial of polynomials of the inputs. The one exception
is a multi-class model with a projection per candidate, which combines
every survivor of the last layer.
The output head¶
model.use_output_projection: true adds out_proj, a linear layer over
the last layer's survivors: Linear(num_out_neurons, 1) for regression,
and Linear(num_out_neurons, C) followed by the log-softmax for
multi-class. Binary models have no head and ignore the key.
The head reads the model.num_out_neurons survivors with the lowest
criterion values. Left at null, it reads model.nbest_neurons of them,
every survivor of the last layer.
Fitting the head¶
The search does not train the head. Until Trainer.train_out_proj fits it,
the head has random weights and the model's predictions are meaningless:
train_out_proj freezes the rest of the network, computes its outputs once
per split, fits the head on the train split and early-stops on dev. It
ends on its last step (see
Last step or best evaluation) and saves
the model to model_last.ckpt. train.out_proj_train sets the
optimizer (adam by default, sgd or lbfgs), the learning rate, the step
limit, the evaluation interval and the early stop.
Fitting the head is least squares for regression and logistic regression
for multi-class: small convex problems that lbfgs with its line search
solves in a few dozen steps. The default adam at a learning rate of 0.001
can stop well short of what lbfgs reaches, so an lbfgs block is usually
the better choice for the head:
train:
out_proj_train:
optimizer: lbfgs
lr: 0.1
max_steps: 100
eval_interval: 5
early_stop_patience: 6
A head on its own does not always help: fitting it can lower the dev error yet raise the test error, since it only optimizes the readout on the splits it sees. The head pays off mainly as the starting point of the end-to-end pass, below.
The per-layer fine-tune¶
train.layer_finetune: true adds a step to every layer, after selection:
the survivors train together through a temporary linear head, against the
model's training loss, on the train split, with an early stop on dev. Every
parameter the survivors own trains, except a per-candidate class map: their
coefficients and, for the RBF family, their centres and widths. The temporary head is then discarded.
The pass takes its settings from train.out_proj_train, like the head
fit.
The pass turns the survivors from individual predictors into a basis for a linear combination: together they fit better, each alone fits worse. That has two consequences.
- It needs an output head. A model without one predicts with a single
survivor, which the pass has made worse, so that readout collapses. Set
model.use_output_projectionand let the head read every survivor (num_out_neuronsnull); reading only some of them tends to cost accuracy and widen the spread across folds. - It changes where the search stops. The growth rule compares the
survivors' own criterion values by default, and those get worse, so the
search stops early.
train.layer_err_source: readoutscores each layer by the temporary head's dev loss instead (see Criteria).
The per-layer fine-tune does not always pay off. On some data it stops the
search earlier and ends up a little worse than leaving it off; on other data
it is part of the stack that helps most (see
the end-to-end pass). It is worth measuring both
ways on your own data. The pass does not work on binary models: it fails
with ValueError at the first layer.
The end-to-end pass¶
Trainer.train_finetune(model, train_dl, dev_dl, cfg=None, val_dl=None)
trains every parameter of the network at once: all layers' coefficients,
RBF centres and widths, a learned class map (the shared projection or one
per candidate; the soft binner's points stay fixed) and the head. Its loss
is the model's training loss on exactly what infer returns, so it
optimizes the readout the model is scored on.
trainer.train(model, train_dl, dev_dl, test_dl)
trainer.train_out_proj(model, train_dl, dev_dl)
trainer.train_finetune(model, train_dl, dev_dl)
preds, targets = trainer.infer(model, test_dl)
train.finetune_train (or a block of the same shape passed as cfg) sets
the optimizer (adam, adamw or sgd; there is no LBFGS here), the
learning rate, the plateau schedule, the step limit and the early stop.
The early stop reads the dev split, or the validation split when
train.stop_source is val.
With a head and a block like the one below, the pass typically stops on its own within a couple of thousand steps, a few minutes on a CPU:
train:
finetune_train:
optimizer: adamw
lr: 1.0e-4
weight_decay: 1.0e-4
max_steps: 20000
eval_interval: 25
early_stop_patience: 20
early_stop_min_delta: 1.0e-6
lr_min: 1.0e-8
The usual progression: the best neuron alone gives one test error, a fitted head on its own can be slightly worse, and the head followed by the end-to-end pass improves on both.
Things to know before using it:
- Use a small learning rate. Every weight moves at once, so the pass
wants a far gentler step than the head fit: AdamW at around
1e-4to1e-5is a good starting range. - Check that it converged. The log prints
finetune early stop at step ...when the pass stops on its own. Without that line, it ran tomax_stepsand its result is cut short, which is worse and more variable; raisemax_stepsuntil the early-stop line appears on every run. - Keep the head. Removing the head and fine-tuning the bare network against its best neuron tends to do worse than no fine-tuning at all, because a single readout neuron is a poor target. With the head, fine-tuning reliably helps.
- It repeats only with the same calls. Seeded, the pass gives the same
result on a CPU run after run. Every pass over a loader draws from the
random generator, so extra loops over a loader, to score the head along
the way for instance, change the pass's shuffling and its result. On a
GPU, set
train.use_deterministic_algorithms(see Training). - It widens the spread. Fine-tuned models vary more from run to run than the plain search.
- It saves the model to the run folder's
model_last.ckptwhen it ends, on its last step unlessfinetune_train.keep_best_weightsis on (see Last step or best evaluation). - Run it before
Trainer.infer. On a model with a head, a call toTrainer.inferbefore the pass makes the pass fail (see Troubleshooting).
On a CUDA device with adam or adamw, the pass runs as a captured CUDA
graph, which is much faster (see Performance).
The three passes together, the per-layer fine-tune, the head and the
end-to-end pass, are what the tutorials' *_finetune configs add. On some
tasks the gain from the full stack is larger than the difference between the
cheapest and the most expensive search.
In the tutorial scripts¶
The tutorial scripts read three top-level flags: finetune_end_to_end
runs the pass after the head fit, finetune_drop_head removes the head
first, and finetune_prune_first prunes the network first. Pruning first
leaves the accuracy unchanged, because the neurons it removes get no
gradient anyway.
Last step or best evaluation¶
The head fit, the per-layer fine-tune and the end-to-end pass evaluate
every eval_interval steps, and a last step that falls between two
evaluations is evaluated too. By default each pass ends on its last step.
keep_best_weights: true in train.out_proj_train (the head fit and the
per-layer fine-tune) or train.finetune_train (the end-to-end pass) makes
it end on the weights of its lowest evaluated loss instead: the pass copies
the weights it trains on every new best and copies them back when it
stops. best_weights_copy says where the copy is kept: device, next to
the parameters, in GPU memory on CUDA; cpu, in host memory; or disk, in
a file in the run folder that is deleted when the pass ends. The copy is as
large as what the pass trains: the head, a layer's survivors, or the whole
model.
Neither setting is better everywhere. The last step's weights sometimes win on the test split even when the best evaluation's are lower on dev, and for the per-layer fine-tune the best weights can leave lower layer errors and let the search train more layers. Measure both on your data.
With keep_best_weights on, the log says which step each pass kept:
finetune: kept the weights of step 875 (dev loss 0.2704); the last evaluation, step 1375, gave 0.2715
Pruning¶
Trainer.prune(model) deletes every neuron the prediction does not reach.
It keeps the neurons the readout reads in the last layer: the
out_proj.in_features the head reads, every survivor of a multi-class
model with a projection per candidate and no head, and one neuron
otherwise. Then it works down the layers, keeping whatever those neurons
read. It follows any input layout, including inputs from older layers
under model.shortcut.prev_layers, and deletes a layer that nothing reads
any more. The predictions stay the same.
A model built with model.use_layer_norm cannot be pruned: each of its
LayerNorms normalizes a row over every column of a layer's input, so
removing columns would change the predictions. prune raises ValueError
and leaves such a model as it is.
Without a head, pruning typically leaves only a handful of neurons: the model collapses toward the single chain the best neuron depends on, and unused features drop out. With a head that reads every survivor, far more neurons survive, since the prediction reaches back into every layer.
The knobs¶
model.use_output_projection, model.num_out_neurons,
train.layer_finetune, train.layer_err_source, train.out_proj_train,
train.finetune_train, train.stop_source. See
Head fit and per-layer fine-tune
and End-to-end pass.
Checked against TorchSONN 0.1.5.