Quickstart¶
A nonlinear eigenvalue problem¶
The following problem is the standard smoke test for Stiefel solvers:
where \(\rho = \mathrm{Diag}(XX^\top)\) and \(L^{\dagger}\) is the pseudo-inverse of the positive definite \(L\). The cost function and its Euclidean gradient are
Taking \(L\) tridiagonal, so that \(L^{\dagger} = L^{-1}\):
import numpy as np
from scipy.sparse import diags
from scipy.sparse.linalg import spsolve
from smopt import slpg_smooth, Stiefel
n, p, alpha = 1000, 10, 1.0
M = Stiefel(n, p)
L = diags([-1, 2, -1], [1, 0, -1], shape=(n, n)).tocsc()
def obj_fun(X):
LX = L @ X
rho = np.sum(X * X, 1)
Lrho = spsolve(L, rho)
fval = 0.5 * np.sum(X * LX) + (alpha / 4) * np.sum(rho * Lrho)
grad = LX + alpha * Lrho[:, np.newaxis] * X
return fval, grad
X, out = slpg_smooth(obj_fun, M)
Note
SciPy is only used to build this example's data; smopt itself
depends on NumPy alone.
Step by step¶
1. Fix the manifold¶
The dimensions are carried by a Stiefel instance, which also
exposes the geometric maps the solvers use:
from smopt import Stiefel
M = Stiefel(1000, 10)
2. Define the objective¶
A solver expects one callable returning the value and the Euclidean gradient together:
def obj_fun(X):
...
return fval, grad
Returning both at once is usually far cheaper than computing them
separately, even with caching, so smopt asks for them in a single
call. X arrives as an (n, p) array and grad must have the same
shape.
3. Run a solver¶
X, out = slpg_smooth(obj_fun, M)
X is the solution and out is a dictionary of log information:
| Key | Meaning |
|---|---|
fvals, kkts, feas |
per-iteration histories |
fval, kkt, fea |
the final objective, stationarity and feasibility |
beta |
the penalty pencf actually used |
Nonsmooth problems¶
A regularizer of your own¶
Pass any prox(X, eta) that minimizes
\(\|Y - X\|_F^2 / (2\eta) + r(Y)\). Here is \(\ell_1\) regularization built
from the operator shipped with the package:
from smopt import prox_l1, slpg
gamma = 0.05
X, out = slpg(obj_fun, M, prox=lambda X, eta: prox_l1(X, eta, gamma=gamma))
Row sparsity¶
For \(r(X) = \gamma\|X\|_{2,1}\) use the dedicated driver, which knows the prox and the constraint multiplier in closed form:
from smopt import slpg_l21
X, out = slpg_l21(obj_fun, M, gamma=1.0)
Whole rows of X are driven to zero, which selects variables:
import numpy as np
live = np.linalg.norm(X, axis=1) > 1e-6
print(f"{live.sum()} of {len(live)} rows survive")
Choosing a solver¶
| Solver | Use when |
|---|---|
slpg_smooth |
the objective is smooth |
slpg |
there is a nonsmooth term with a known prox |
slpg_l21 |
the nonsmooth term is \(\gamma\|X\|_{2,1}\) |
pencf |
a constraint dissolving penalty method is preferred |
Common options¶
Every solver accepts:
X, out = slpg_smooth(
obj_fun,
M,
xinit=None, # starting point; random feasible point if omitted
maxit=100, # iteration budget
gtol=1e-5, # stationarity tolerance
post_process=True, # round the answer onto the manifold
verbosity=2, # 0 silent, 1 final lines, 2 periodic
)
pencf takes the starting point first and adds beta:
from smopt import pencf
X, out = pencf(xinit, obj_fun, M, beta=None)