Quickstart#

This guide will walk you through the basic usage of torch_numopt. You will learn how to:

  • Set up an objective function for a supervised learning problem.

  • Choose and configure an optimizer.

  • Run a training loop.

  • Understand the closure-based workflow.

For a complete list of available optimizers and advanced customization, see the API reference.

Installation#

Install the package via pip:

pip install torch-numopt

Or install from source:

git clone https://github.com/GheodeAI/torch_numopt.git
cd torch_numopt
pip install -e .

1. Define your model and loss#

Start with a standard PyTorch model and loss function:

import torch
import torch.nn as nn

model = nn.Sequential(
    nn.Linear(10, 20),
    nn.ReLU(),
    nn.Linear(20, 1)
)
loss_fn = nn.MSELoss()

2. Prepare your data#

Generate some dummy data (or load your own):

X = torch.randn(100, 10)
y = torch.randn(100, 1)

3. Create the objective#

The SupervisedLearningObjective wraps your model, loss function, and data. It also holds a reference to the optimizer.

from torch_numopt import SupervisedLearningObjective, GaussNewtonLS

optimizer = GaussNewtonLS(model.parameters(), lr_init=1.0)
objective = SupervisedLearningObjective(model, loss_fn, optimizer)

# Important: set the data before the first step
objective.set_data(X, y)

Why do we call `set_data`? The objective is stateless with respect to the data – this lets you switch between full-batch and mini-batches at each iteration by calling set_data again before optimizer.step().

4. Run the training loop#

The optimizer expects a closure that computes the loss and performs backpropagation. The objective itself is callable and does exactly that, so you simply pass it to optimizer.step().

for epoch in range(100):
    # If you want to use a different batch, call objective.set_data(batch_X, batch_y) here
    optimizer.step(objective)   # objective.closure() is called internally

    # Monitor loss (optional)
    with torch.no_grad():
        loss = objective.loss(*objective.params)
        print(f"Epoch {epoch:3d} | Loss: {loss.item():.6f}")

Important: This is not the typical PyTorch pattern of calling loss.backward() and then optimizer.step(). The optimizer takes full control of evaluation and backpropagation, which is necessary for line-search and trust-region methods that re-evaluate the objective multiple times per iteration.

5. Try a different optimizer#

Switching to another optimizer is trivial. For example, to use Newton with line search:

from torch_numopt import NewtonLS

optimizer = NewtonLS(
    model.parameters(),
    lr_init=1.0,
    damping="identity",      # improves stability
    mu=1e-4,
    block_hessian=True       # saves memory
)

objective = SupervisedLearningObjective(model, loss_fn, optimizer)
objective.set_data(X, y)

# Training loop remains the same
for epoch in range(100):
    optimizer.step(objective)
    # ...

6. Using mini-batches#

If you want to use mini-batch training, simply call set_data with a new batch before each step():

batch_size = 32
for epoch in range(100):
    for i in range(0, len(X), batch_size):
        batch_X = X[i:i+batch_size]
        batch_y = y[i:i+batch_size]
        objective.set_data(batch_X, batch_y)
        optimizer.step(objective)

Caveat: The library is designed for deterministic (full-batch) problems. Mini-batch updates introduce noise that can destabilize second-order methods. If you experience issues, consider using a larger batch size or switching to a first-order optimizer (e.g., GradientDescentLS).

7. Where to go next#