deepbox/optim
Optimizers
SGD, Adam and its variants, RMSprop, Adagrad, LBFGS, LARS, LAMB, Lion and other optimizers, with parameter groups and state dicts.
- Optimizers
Overview
- Build an optimizer with
new Adam(model.parameters(), { lr }), or pass groups,[{ params, lr }], for per-group options. Options are camelCase and flat.Adamtakesbeta1andbeta2rather than abetastuple, plusweightDecay,amsgradandmaximize. - Defaults:
SGDuseslr: 0.01with no momentum,AdamandAdamWuselr: 0.001, andAdamWusesweightDecay: 0.01. step()reads the gradients thatbackward()left behind, and gradients accumulate, so calloptimizer.zeroGrad()before eachbackward(). Frozen parameters and parameters without a gradient are skipped atstep(), as in PyTorch. In 1.0.0 the second case threwNotFittedError. A failed step, for example one with a non-finite gradient, changes neither the parameters nor the optimizer state.stateDict()returns a copy that does not alias live state, andloadStateDict()applies it atomically.LBFGStakes a closure,step(closure), that re-evaluates the model and returns the loss. It supportslineSearchFn: "strong_wolfe".- When the parameters live on a device, the optimizers keep their state there.
LBFGSandSparseAdamare the exceptions and throwDeviceErrorfor device parameters.
Optimizer
extends Record<string, unknown>, State extends Record<string, unknown>>Abstract base class for all optimizers.
Concrete optimizers (SGD, Adam, ...) extend this class and implement step(). The base class owns the parameter groups, the per-parameter state map, gradient zeroing and checkpointing through stateDict() / loadStateDict().
Parameters are split into groups, and every group carries its own options (for example a different learning rate per layer). Options a group does not set are taken from the optimizer defaults, and every group is range checked when it is added and again at each step().
A parameter with requiresGrad = false (for example one frozen with Module.freezeParameters()) stays in its group but is skipped by step(). A trainable parameter whose gradient is null (it took no part in the loss, or no backward() has run yet) is skipped as well, as in PyTorch, and gets no optimizer state. A failed step() (a non-finite gradient, an invalid option) changes neither the parameters nor the optimizer state.
import { SGD } from 'deepbox/optim'; const optimizer = new SGD(model.parameters(), { lr: 0.01 }); // Training loopfor (let epoch = 0; epoch < 100; epoch++) { optimizer.zeroGrad(); const loss = computeLoss(); loss.backward(); optimizer.step(); console.log(`epoch ${epoch}: loss ${loss.item()}`);}// Different learning rates per groupconst optimizer = new SGD([ { params: model.layer1.parameters(), lr: 0.01 }, { params: model.layer2.parameters(), lr: 0.001 }], { lr: 0.01 });Members (11)
constructor(params: Iterable<GradTensor> | ReadonlyArray<ParamGroup<Options>>, defaults: Readonly<Options>)Create a new optimizer.
paramGroups: Array<{ params: GradTensor[]; options: Options; }>Groups of parameters with their associated hyperparameters.
lr: numberLearning rate of the first parameter group, or 0 when the optimizer has no groups or the first group has no numeric
lroption.stepCount: numberTotal number of optimization steps performed, including the steps recorded in a state dictionary that was loaded.
getLearningRate(groupIdx?: number): numberGet the learning rate of a parameter group.
setLearningRate(lr: number): voidSet the learning rate of every parameter group.
abstract step(closure?: () => number): number | undefinedPerform a single optimization step (parameter update).
zeroGrad(): voidReset the gradients of all optimized parameters to zero.
addParamGroup(paramGroup: ParamGroup<Options>): voidAdd a parameter group to the optimizer.
stateDict(): { stepCount: number; state: { paramId: number; param: GradTensor; state: State; }[]; paramGroups: { params: GradTensor[]; paramIds: number[]; options: Options; }[]; }Get a snapshot of the optimizer state for checkpointing.
loadStateDict(stateDict: Record<string, unknown>): voidLoad optimizer state from a state dictionary.
AdaDelta
extends Optimizer<AdaDeltaOptions, AdaDeltaState>AdaDelta optimizer (Zeiler, 2012).
AdaDelta is an extension of Adagrad that avoids its monotonically shrinking learning rate. It keeps exponential moving averages of the squared gradients and of the squared parameter updates, and scales each step by the ratio of their root-mean-squares. The update follows torch.optim.Adadelta:
v = rho * v + (1 - rho) * g^2delta = sqrt(acc + eps) / sqrt(v + eps) * gacc = rho * acc + (1 - rho) * delta^2theta -= lr * deltaWhen weightDecay is non-zero, weightDecay * theta is added to the gradient (L2 penalty) before the update.
import { AdaDelta } from 'deepbox/optim'; const optimizer = new AdaDelta(model.parameters(), { lr: 1.0, rho: 0.9, eps: 1e-6}); // Training loopfor (let epoch = 0; epoch < numEpochs; epoch++) { optimizer.zeroGrad(); // ... optimizer.step();}Members (2)
constructor(params: Iterable<GradTensor> | ReadonlyArray<ParamGroup<AdaDeltaOptions>>, options?: { readonly lr?: number; readonly rho?: number; readonly eps?: number; readonly weightDecay?: number; readonly maximize?: boolean; })Create a new AdaDelta optimizer.
step(closure?: () => number): number | undefinedPerform a single optimization step.
Adagrad
extends Optimizer<AdagradOptions, AdagradState>Adagrad (Adaptive Gradient Algorithm) optimizer.
Adagrad adapts the learning rate for each parameter based on the historical sum of squared gradients. Parameters with larger gradients receive smaller effective learning rates, while parameters with smaller gradients receive larger effective learning rates. The update follows torch.optim.Adagrad:
clr = lr / (1 + (t - 1) * lrDecay)sum += g^2theta -= clr * g / (sqrt(sum) + eps)When weightDecay is non-zero, weightDecay * theta is added to the gradient (L2 penalty) before the update.
import { Adagrad } from 'deepbox/optim'; const optimizer = new Adagrad(model.parameters(), { lr: 0.01, eps: 1e-10}); // Training loopfor (let epoch = 0; epoch < numEpochs; epoch++) { optimizer.zeroGrad(); // ... optimizer.step();}Members (2)
constructor(params: Iterable<GradTensor> | ReadonlyArray<ParamGroup<AdagradOptions>>, options?: { readonly lr?: number; readonly eps?: number; readonly weightDecay?: number; readonly lrDecay?: number; readonly initialAccumulatorValue?: number; readonly maximize?: boolean; })Create a new Adagrad optimizer.
step(closure?: () => number): number | undefinedPerform a single optimization step.
Adam
extends Optimizer<AdamOptions, AdamState>Adam (Adaptive Moment Estimation) optimizer (Kingma and Ba, 2015).
Computes adaptive learning rates for each parameter by maintaining running averages of both the gradients and their squared values. The update follows torch.optim.Adam:
m = beta1 * m + (1 - beta1) * gv = beta2 * v + (1 - beta2) * g^2theta -= (lr / (1 - beta1^t)) * m / (sqrt(v / (1 - beta2^t)) + eps)When weightDecay is non-zero, weightDecay * theta is added to the gradient (classic L2 penalty); use AdamW for decoupled weight decay. With amsgrad, v is replaced by its running maximum in the denominator.
import { Adam } from 'deepbox/optim'; const optimizer = new Adam(model.parameters(), { lr: 0.001, beta1: 0.9, beta2: 0.999});Members (2)
constructor(params: Iterable<GradTensor> | ReadonlyArray<ParamGroup<AdamOptions>>, options?: { readonly lr?: number; readonly beta1?: number; readonly beta2?: number; readonly eps?: number; readonly weightDecay?: number; readonly amsgrad?: boolean; readonly maximize?: boolean; })Create a new Adam optimizer.
step(closure?: () => number): number | undefinedPerform a single optimization step.
Adamax
extends Optimizer<AdamaxOptions, AdamaxState>Adamax optimizer, a variant of Adam based on the infinity norm (Kingma and Ba, 2015).
Often used for embeddings and sparse gradients. The update follows torch.optim.Adamax:
m = beta1 * m + (1 - beta1) * gu = max(beta2 * u, |g| + eps)theta -= (lr / (1 - beta1^t)) * m / uWhen weightDecay is non-zero, weightDecay * theta is added to the gradient (L2 penalty) before the update.
import { Adamax } from 'deepbox/optim'; const optimizer = new Adamax(model.parameters(), { lr: 0.002 });Members (2)
constructor(params: Iterable<GradTensor> | ReadonlyArray<ParamGroup<AdamaxOptions>>, options?: { readonly lr?: number; readonly beta1?: number; readonly beta2?: number; readonly eps?: number; readonly weightDecay?: number; readonly maximize?: boolean; })Create a new Adamax optimizer.
step(closure?: () => number): number | undefinedPerform a single optimization step.
AdamW
extends Optimizer<AdamWOptions, AdamWState>AdamW (Adam with decoupled weight decay) optimizer (Loshchilov and Hutter, 2019).
AdamW applies weight decay directly to the parameters instead of adding it to the gradient, so the decay is not rescaled by the adaptive denominator. The update follows torch.optim.AdamW:
theta *= 1 - lr * weightDecaym = beta1 * m + (1 - beta1) * gv = beta2 * v + (1 - beta2) * g^2theta -= (lr / (1 - beta1^t)) * m / (sqrt(v / (1 - beta2^t)) + eps)import { AdamW } from 'deepbox/optim'; const optimizer = new AdamW(model.parameters(), { lr: 0.001, weightDecay: 0.01, // Typical value for AdamW beta1: 0.9, beta2: 0.999}); // Training loopfor (let epoch = 0; epoch < numEpochs; epoch++) { optimizer.zeroGrad(); // ... optimizer.step();}Members (2)
constructor(params: Iterable<GradTensor> | ReadonlyArray<ParamGroup<AdamWOptions>>, options?: { readonly lr?: number; readonly beta1?: number; readonly beta2?: number; readonly eps?: number; readonly weightDecay?: number; readonly amsgrad?: boolean; readonly maximize?: boolean; })Create a new AdamW optimizer.
step(closure?: () => number): number | undefinedPerform a single optimization step.
ASGD
extends Optimizer<ASGDOptions, ASGDState>Averaged Stochastic Gradient Descent (ASGD) optimizer (Polyak and Juditsky, 1992).
Runs SGD with a decaying step size and keeps a running (Polyak-Ruppert) average of the iterates, which often generalizes better than the last iterate. The average starts after t0 steps and is available through ASGD.averagedParameters. The update follows torch.optim.ASGD:
theta = theta * (1 - lambda * eta) - eta * gax += mu * (theta - ax)eta = lr / (1 + lambda * lr * t)^alphamu = 1 / max(1, t - t0)When weightDecay is non-zero, weightDecay * theta is added to the gradient (L2 penalty) before the update. With maximize, the gradient is negated first.
import { ASGD } from 'deepbox/optim'; const optimizer = new ASGD(model.parameters(), { lr: 0.01, t0: 1e6,});Members (3)
constructor(params: Iterable<GradTensor> | ReadonlyArray<ParamGroup<ASGDOptions>>, options?: { readonly lr?: number; readonly lambda?: number; readonly alpha?: number; readonly t0?: number; readonly weightDecay?: number; readonly maximize?: boolean; })Create a new ASGD optimizer.
step(closure?: () => number): number | undefinedPerform a single optimization step.
averagedParameters(): Tensor[]Get the running average of each parameter (Polyak-Ruppert averaging).
LAMB
extends Optimizer<LAMBOptions, LAMBState>LAMB (Layer-wise Adaptive Moments optimizer for Batch training) optimizer.
LAMB extends Adam with layer-wise adaptive learning rates, enabling training with very large batch sizes (up to 64K for BERT). It combines the benefits of Adam's per-element adaptivity with LARS-style layer-wise trust ratios. Each parameter tensor is treated as one layer:
r = m_hat / (sqrt(v_hat) + eps) + weightDecay * thetatrust = ||theta|| / ||r|| (1 when either norm is zero)theta -= lr * trust * rwhere m_hat and v_hat are the bias-corrected Adam moment estimates.
Reference: "Large Batch Optimization for Deep Learning: Training BERT in 76 Minutes" (You et al., 2019)
import { LAMB } from 'deepbox/optim'; const optimizer = new LAMB(model.parameters(), { lr: 0.001, beta1: 0.9, beta2: 0.999, weightDecay: 0.01,}); for (let epoch = 0; epoch < numEpochs; epoch++) { optimizer.zeroGrad(); // ... optimizer.step();}Members (2)
constructor(params: Iterable<GradTensor> | ReadonlyArray<ParamGroup<LAMBOptions>>, options?: { readonly lr?: number; readonly beta1?: number; readonly beta2?: number; readonly eps?: number; readonly weightDecay?: number; })Create a new LAMB optimizer.
step(closure?: () => number): number | undefinedPerform a single optimization step.
LARS
extends Optimizer<LARSOptions, LARSState>LARS (Layer-wise Adaptive Rate Scaling) optimizer.
LARS scales the learning rate per layer based on the ratio of the parameter norm to the gradient norm. This allows training with very large batch sizes (up to 32K) without divergence. Each parameter tensor is treated as one layer:
localLr = eta * ||theta|| / (||g|| + weightDecay * ||theta|| + eps) (1 when either norm is zero)m = momentum * m + lr * localLr * (g + weightDecay * theta)theta -= mReference: "Large Batch Training of Convolutional Networks" (You et al., 2017)
import { LARS } from 'deepbox/optim'; const optimizer = new LARS(model.parameters(), { lr: 0.1, momentum: 0.9, weightDecay: 1e-4, eta: 0.001,}); for (let epoch = 0; epoch < numEpochs; epoch++) { optimizer.zeroGrad(); // ... optimizer.step();}Members (2)
constructor(params: Iterable<GradTensor> | ReadonlyArray<ParamGroup<LARSOptions>>, options?: { readonly lr?: number; readonly momentum?: number; readonly weightDecay?: number; readonly eta?: number; readonly eps?: number; })Create a new LARS optimizer.
step(closure?: () => number): number | undefinedPerform a single optimization step.
LBFGS
extends Optimizer<LBFGSOptions, LBFGSState>L-BFGS (Limited-memory Broyden–Fletcher–Goldfarb–Shanno) optimizer.
A quasi-Newton method that approximates the inverse Hessian using a limited history of past updates. Unlike first-order methods (Adam, SGD), LBFGS can converge much faster for smooth, deterministic objectives.
Important: LBFGS requires a closure that reevaluates the model and returns the loss. This is because LBFGS may evaluate the function multiple times per step. All parameters are treated as one flat vector, so it uses a single set of hyperparameters: parameter groups with differing options are rejected.
Each step(closure) runs up to maxIter L-BFGS iterations. The step length is chosen by one of two line searches: - lineSearchFn: null (default): the initial step lr is halved (at most 10 times) until the loss decreases. The first step is scaled by min(1, 1 / ||g||_1). maxEval is checked between iterations, so one line search can exceed it by up to nine evaluations. - lineSearchFn: "strong_wolfe": a line search satisfying the strong Wolfe conditions, as in torch.optim.LBFGS. This is the better choice for non-quadratic objectives.
LBFGS only supports float32/float64 parameters on the host; device tensors are rejected with a DeviceError.
import { LBFGS } from 'deepbox/optim'; const optimizer = new LBFGS(model.parameters(), { lr: 1 }); for (let epoch = 0; epoch < numEpochs; epoch++) { const closure = () => { optimizer.zeroGrad(); const output = model.forward(input); const loss = criterion(output, target); loss.backward(); return loss.item(); }; optimizer.step(closure);}Members (4)
constructor(params: Iterable<GradTensor> | ReadonlyArray<ParamGroup<LBFGSOptions>>, options?: { readonly lr?: number; readonly maxIter?: number; readonly maxEval?: number; readonly toleranceGrad?: number; readonly toleranceChange?: number; readonly historySize?: number; readonly lineSearchFn?: "strong_wolfe" | null; })Create a new LBFGS optimizer.
stateDict(): { lbfgs: { stepCount: number; sHistory: Float64Array<ArrayBuffer>[]; yHistory: Float64Array<ArrayBuffer>[]; rhoHistory: number[]; prevFlatGrad: Float64Array<ArrayBuffer> | null; prevFlatParams: Float64Array<ArrayBuffer> | null; }; stepCount: number; state: { paramId: number; param: GradTensor; state: LBFG…Snapshot of the optimizer, including the L-BFGS curvature history so that a resumed run continues with the same Hessian approximation.
loadStateDict(stateDict: Record<string, unknown>): voidRestore optimizer state saved by LBFGS.stateDict.
step(closure?: () => number): number | undefinedRun up to
maxIterL-BFGS iterations.
Lion
extends Optimizer<LionOptions, LionState>Lion (EvoLved Sign Momentum) optimizer.
Introduced by Chen et al. (2023) in "Symbolic Discovery of Optimization Algorithms". Lion is a simpler, more memory-efficient alternative to Adam that keeps a single momentum buffer and applies sign-based updates, so every element moves by the same magnitude lr regardless of its gradient scale.
Update rule (per parameter):
u = sign(beta1 * m + (1 - beta1) * g)theta -= lr * (u + weightDecay * theta)m = beta2 * m + (1 - beta2) * gWeight decay is decoupled, as in AdamW. Lion typically needs a 3-10x smaller learning rate than Adam (suggested: 1e-4 to 3e-4, versus Adam's 1e-3) and a correspondingly larger weight decay.
import { Lion } from 'deepbox/optim'; const optimizer = new Lion(model.parameters(), { lr: 3e-4, beta1: 0.9, beta2: 0.99});Members (2)
constructor(params: Iterable<GradTensor> | ReadonlyArray<ParamGroup<LionOptions>>, options?: { readonly lr?: number; readonly beta1?: number; readonly beta2?: number; readonly weightDecay?: number; readonly maximize?: boolean; })Create a new Lion optimizer.
step(closure?: () => number): number | undefinedPerform a single optimization step.
Nadam
extends Optimizer<NadamOptions, NadamState>Nadam (Nesterov-accelerated Adam) optimizer (Dozat, 2016).
Combines Adam's adaptive learning rates with Nesterov momentum: the first moment estimate is replaced by a look-ahead blend of the next momentum estimate and the current gradient. The momentum coefficient follows the schedule mu_t = beta1 * (1 - 0.5 * 0.96^(t * momentumDecay)) used by torch.optim.NAdam.
By default weightDecay is an L2 penalty added to the gradient. With decoupledWeightDecay: true the parameters are instead multiplied by 1 - lr * weightDecay before the update (NAdamW).
import { Nadam } from 'deepbox/optim'; const optimizer = new Nadam(model.parameters(), { lr: 0.002, beta1: 0.9, beta2: 0.999}); // Training loopfor (let epoch = 0; epoch < numEpochs; epoch++) { optimizer.zeroGrad(); // ... optimizer.step();}Members (2)
constructor(params: Iterable<GradTensor> | ReadonlyArray<ParamGroup<NadamOptions>>, options?: { readonly lr?: number; readonly beta1?: number; readonly beta2?: number; readonly eps?: number; readonly weightDecay?: number; readonly momentumDecay?: number; readonly decoupledWeightDecay?: boolean; readonly maximize?: boo…Create a new Nadam optimizer.
step(closure?: () => number): number | undefinedPerform a single optimization step.
RAdam
extends Optimizer<RAdamOptions, RAdamState>RAdam (Rectified Adam) optimizer.
Fixes Adam's early training variance issue by computing a variance rectification term. When the variance is tractable (high enough SMA), uses the adaptive learning rate; otherwise falls back to SGD with momentum. The update follows PyTorch's torch.optim.RAdam:
rhoInf = 2 / (1 - beta2) - 1rhoT = rhoInf - 2 t beta2^t / (1 - beta2^t)rhoT > 5: param -= lr * rect * sqrt(1 - beta2^t) * mHat / (sqrt(v) + eps)otherwise: param -= lr * mHat (mHat = m / (1 - beta1^t))With the default beta2 = 0.999 the first five steps use the SGD-with-momentum branch.
import { RAdam } from 'deepbox/optim'; const optimizer = new RAdam(model.parameters(), { lr: 0.001 });Members (2)
constructor(params: Iterable<GradTensor> | ReadonlyArray<ParamGroup<RAdamOptions>>, options?: { readonly lr?: number; readonly beta1?: number; readonly beta2?: number; readonly eps?: number; readonly weightDecay?: number; readonly decoupledWeightDecay?: boolean; readonly maximize?: boolean; })Create a new RAdam optimizer.
step(closure?: () => number): number | undefinedPerform a single optimization step.
RMSprop
extends Optimizer<RMSpropOptions, RMSpropState>RMSprop (Root Mean Square Propagation) optimizer.
RMSprop adapts the learning rate for each parameter by dividing by a running average of recent gradient magnitudes. This helps with non-stationary objectives and is particularly effective for RNNs.
The update follows PyTorch's torch.optim.RMSprop:
v = alpha * v + (1 - alpha) * g^2avg = centered ? sqrt(v - gAvg^2) + eps : sqrt(v) + eps (gAvg = EMA of g)buf = momentum * buf + g / avg (momentum > 0)param = param - lr * (momentum > 0 ? buf : g / avg)import { RMSprop } from 'deepbox/optim'; const optimizer = new RMSprop(model.parameters(), { lr: 0.01, alpha: 0.99, momentum: 0.9, centered: true}); // Training loopfor (let epoch = 0; epoch < numEpochs; epoch++) { optimizer.zeroGrad(); // ... optimizer.step();}Members (2)
constructor(params: Iterable<GradTensor> | ReadonlyArray<ParamGroup<RMSpropOptions>>, options?: { readonly lr?: number; readonly alpha?: number; readonly eps?: number; readonly weightDecay?: number; readonly momentum?: number; readonly centered?: boolean; readonly maximize?: boolean; })Create a new RMSprop optimizer.
step(closure?: () => number): number | undefinedPerform a single optimization step.
Rprop
extends Optimizer<RpropOptions, RpropState>Rprop (Resilient Backpropagation) optimizer.
Only uses the sign of the gradient, not its magnitude. Step sizes adapt individually per parameter based on sign changes. The update follows PyTorch's torch.optim.Rprop (Rprop- with weight reset on a sign change): for every element, with prev the previous gradient,
g * prev > 0: step = min(step * etaPlus, stepMax); param -= sign(g) * stepg * prev < 0: step = max(step * etaMinus, stepMin); param unchanged, prev = 0otherwise: param -= sign(g) * stepThe step sizes start at lr (clamped to [stepMin, stepMax]). Because the learning rate only seeds the step sizes, changing it later (for example with an LR scheduler) does not affect parameters that already have state.
import { Rprop } from 'deepbox/optim'; const optimizer = new Rprop(model.parameters(), { lr: 0.01, etaMinus: 0.5, etaPlus: 1.2,});Members (2)
constructor(params: Iterable<GradTensor> | ReadonlyArray<ParamGroup<RpropOptions>>, options?: { readonly lr?: number; readonly etaMinus?: number; readonly etaPlus?: number; readonly stepMin?: number; readonly stepMax?: number; readonly maximize?: boolean; })Create a new Rprop optimizer.
step(closure?: () => number): number | undefinedPerform a single optimization step.
SGD
extends Optimizer<SGDOptions, SGDState>Stochastic Gradient Descent (SGD) optimizer.
Implements vanilla SGD with optional momentum, weight decay, and Nesterov acceleration. The update follows PyTorch's torch.optim.SGD:
d = grad + weightDecay * parambuf = momentum * buf + (1 - dampening) * d (buf = d on the first step)d = nesterov ? d + momentum * buf : bufparam = param - lr * dWith maximize: true the gradient sign is flipped and the objective is maximized.
import { SGD } from 'deepbox/optim';import { Module } from 'deepbox/nn'; const model: Module = ...;const optimizer = new SGD(model.parameters(), { lr: 0.01, momentum: 0.9, weightDecay: 5e-4, nesterov: true}); // Training loopfor (let epoch = 0; epoch < numEpochs; epoch++) { for (const [inputs, targets] of dataLoader) { optimizer.zeroGrad(); const outputs = model.forward(inputs); const loss = criterion(outputs, targets); loss.backward(); optimizer.step(); console.log(`epoch ${epoch}: loss ${loss.item()}`); }}Members (2)
constructor(params: Iterable<GradTensor> | ReadonlyArray<ParamGroup<SGDOptions>>, options?: { readonly lr?: number; readonly momentum?: number; readonly dampening?: number; readonly weightDecay?: number; readonly nesterov?: boolean; readonly maximize?: boolean; })Create a new SGD optimizer.
step(closure?: () => number): number | undefinedPerform a single optimization step.
SparseAdam
extends Optimizer<SparseAdamOptions, SparseAdamState>SparseAdam optimizer, a variant of Adam designed for sparse gradients.
Only updates the moment estimates for gradient indices that are non-zero, making it suited to parameters with sparse gradient updates such as embedding layers. Gradients are dense tensors here, and an exact zero marks an index that was not touched. The update follows PyTorch's torch.optim.SparseAdam. Weight decay is not supported.
Algorithm: For each parameter element where gradient ≠ 0:
m_t = β₁ * m_{t-1} + (1 - β₁) * g_tv_t = β₂ * v_{t-1} + (1 - β₂) * g_t²θ_t = θ_{t-1} - lr * √(1 - β₂^t) / (1 - β₁^t) * m_t / (√v_t + ε)For indices where gradient = 0, moment estimates and parameters are unchanged. The step counter t advances on every call to step().
import { SparseAdam } from 'deepbox/optim';import { Embedding } from 'deepbox/nn'; const embedding = new Embedding(10000, 128);const optimizer = new SparseAdam(embedding.parameters(), { lr: 0.001 });Members (2)
constructor(params: Iterable<GradTensor> | ReadonlyArray<ParamGroup<SparseAdamOptions>>, options?: { readonly lr?: number; readonly beta1?: number; readonly beta2?: number; readonly eps?: number; readonly maximize?: boolean; })Create a new SparseAdam optimizer.
step(closure?: () => number): number | undefinedPerform a single optimization step.
import { tensor } from "deepbox/ndarray";import { Linear, mseLoss } from "deepbox/nn";import { AdamW, SGD } from "deepbox/optim"; const layer = new Linear(2, 1);const x = tensor([[1, 2], [3, 4], [5, 6]]);const y = tensor([[5], [11], [17]]); const optimizer = new AdamW(layer.parameters(), { lr: 0.05, weightDecay: 0.01 });for (let step = 0; step < 100; step++) { optimizer.zeroGrad(); // gradients accumulate otherwise const loss = mseLoss(layer.forward(x), y); loss.backward(); optimizer.step(); if (step % 25 === 0) console.log(step, loss.item());} // Parameter groups with their own learning rates.const [weight, bias] = [...layer.parameters()];if (weight && bias) { const grouped = new SGD([{ params: [weight], lr: 0.1 }, { params: [bias], lr: 0.01 }], { lr: 0.05 }); console.log(grouped.getLearningRate(0), grouped.getLearningRate(1));}Found a mistake? Open an issue.