The source files for all examples can be found in /examples.

Lasso, Three Ways

This example shows how to use the MLSolver, QPSolver, and the GenericSolver interfaces to solve a lasso regression problem.

The lasso regression problem is

\[\begin{array}{ll} \text{minimize} & (1/2)\|Ax - b\|_2^2 + \gamma \|x\|_1 \end{array}\]

using GeNIOS
using Random, LinearAlgebra, SparseArrays

Generating the problem data

Random.seed!(1)
m, n = 200, 400
A = randn(m, n)
A .-= sum(A, dims=1) ./ m
normalize!.(eachcol(A))
xstar = sprandn(n, 0.1)
b = A*xstar + 1e-3*randn(m)
λ = 0.05*norm(A'*b, Inf)
0.14048198582260793

MLSolver interface

The MLSolver interface requires the per-sample loss and (optionally) its conjugate, if we want to use the dual gap as a stopping criterion (discussed in our paper). The conjugate function of $f$, defined as

\[f^*(y) = \sup_x \{yx - f(x)\},\]

Specifying the conjugate function is optional, and the solver will fall back to using the primal and dual residuals if it is not specified.

f(x) = 0.5*x^2
fconj(x) = 0.5*x^2
λ1 = λ
λ2 = 0.0
solver = GeNIOS.MLSolver(f, λ1, λ2, A, b; fconj=fconj)
res = solve!(solver; options=GeNIOS.SolverOptions(relax=true, use_dual_gap=true, dual_gap_tol=1e-3, verbose=true))
rmse = sqrt(1/m*norm(A*solver.zk - b, 2)^2)
println("Final RMSE: $(round(rmse, digits=8))")
Starting setup...
Setup in  0.140s

──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
    Iteration      Objective           RMSE       Dual Gap       r_primal         r_dual              ρ           Time
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
            0      1.995e+01      3.158e-01      9.256e+00            Inf            Inf      1.000e+00         0.000
            1      1.995e+01      3.158e-01      9.256e+00      0.000e+00      1.058e+01      1.000e+00         0.187
           20      4.260e+00      5.491e-02      5.403e-02      2.000e-02      6.603e-02      1.000e+00         0.192
           36      4.260e+00      5.408e-02      8.625e-04      4.108e-04      1.712e-03      1.000e+00         0.197

SOLVED in  0.197s, 36 iterations
Total time:  0.337s
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Final RMSE: 0.076475

Note that the solver uses forward-mode automatic differentiation to compute the first and second derivatives of $f$. We can supply these arguments for additional speedup, if desired.

df(x) = x
d2f(x) = 1.0
solver = GeNIOS.MLSolver(f, df, d2f, λ1, λ2, A, b; fconj=fconj)
res = solve!(solver; options=GeNIOS.SolverOptions(relax=true, use_dual_gap=true, dual_gap_tol=1e-3, verbose=true))
rmse = sqrt(1/m*norm(A*solver.zk - b, 2)^2)
println("Final RMSE: $(round(rmse, digits=8))")
Starting setup...
Setup in  0.038s

──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
    Iteration      Objective           RMSE       Dual Gap       r_primal         r_dual              ρ           Time
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
            0      1.995e+01      3.158e-01      9.256e+00            Inf            Inf      1.000e+00         0.000
            1      1.995e+01      3.158e-01      9.256e+00      0.000e+00      1.058e+01      1.000e+00         0.076
           20      4.260e+00      5.491e-02      5.403e-02      2.000e-02      6.603e-02      1.000e+00         0.081
           36      4.260e+00      5.408e-02      8.625e-04      4.108e-04      1.712e-03      1.000e+00         0.087

SOLVED in  0.087s, 36 iterations
Total time:  0.125s
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Final RMSE: 0.076475

QPSolver interface

The QPSolver interface requires us to specify the problem in the form

\[\begin{array}{ll} \text{minimize} & (1/2)x^TPx + q^Tx \\ \text{subject to} & l \leq Mx \leq u \end{array}\]

We introduce the new variable $t$ and inntroduce the constraint $-t \leq x \leq t$ to enforce the $\ell_1$ norm. The problem then becomes

\[\begin{array}{ll} \text{minimize} & (1/2)x^TA^TAx + b^TAx + \gamma \mathbf{1}^Tt \\ \text{subject to} & \begin{bmatrix}0 \\ -\infty\end{bmatrix} \leq \begin{bmatrix} I & I \\ I & -I \end{bmatrix} \begin{bmatrix}x \\ t\end{bmatrix} \leq \begin{bmatrix}\infty \\ 0\end{bmatrix} \end{array}\]

P = blockdiag(sparse(A'*A), spzeros(n, n))
q = vcat(-A'*b, λ*ones(n))
M = [
    sparse(I, n, n)     sparse(I, n, n);
    sparse(I, n, n)     -sparse(I, n, n)
]
l = [zeros(n); -Inf*ones(n)]
u = [Inf*ones(n); zeros(n)]
solver = GeNIOS.QPSolver(P, q, M, l, u)
res = solve!(
    solver; options=GeNIOS.SolverOptions(
        relax=true,
        max_iters=1000,
        eps_abs=1e-4,
        eps_rel=1e-4,
        verbose=true)
);
rmse = sqrt(1/m*norm(A*solver.xk[1:n] - b, 2)^2)
println("Final RMSE: $(round(rmse, digits=8))")
Starting setup...
Setup in  0.000s

──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
    Iteration        Obj Val       r_primal         r_dual              ρ           Time
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
            0      0.000e+00            Inf            Inf      1.000e+00         0.000
            1      0.000e+00      0.000e+00      1.095e+01      1.000e+00         0.048
           20     -1.530e+01      4.039e-02      2.905e-01      1.000e+00         0.073
           40     -1.565e+01      1.575e-02      9.517e-02      1.000e+00         0.091
           60     -1.568e+01      5.419e-03      3.517e-02      1.000e+00         0.112
           80     -1.569e+01      2.035e-03      1.340e-02      1.000e+00         0.136
          100     -1.569e+01      7.494e-04      4.885e-03      1.000e+00         0.161
          109     -1.569e+01      4.726e-04      3.031e-03      1.000e+00         0.173

SOLVED in  0.173s, 109 iterations
Total time:  0.173s
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Final RMSE: 0.07684455

Generic interface

Finally, we use the generic interface, which provides a large amount of control but is more complicated to use than the specialized interfaces demonstrated above.

First, we define the custom HessianOperator, which is used to solve the linear system in the $x$-subproblem. Since the Hessian of the objective is simply $A^TA$, this operator is simple for the Lasso problem. However, note that, since this is a custom type, we can speed up the multiplication to be more efficient than if we were to use $A^TA$ directly.

The update! function is called before the solution of the $x$-subproblem to update the Hessian, if necessary.

struct HessianLasso{T, S <: AbstractMatrix{T}} <: HessianOperator
    A::S
    vm::Vector{T}
end
function LinearAlgebra.mul!(y, H::HessianLasso, x)
    mul!(H.vm, H.A, x)
    mul!(y, H.A', H.vm)
    return nothing
end

function update!(::HessianLasso, ::Solver)
    return nothing
end
update! (generic function with 1 method)

Now, we define $f$, its gradient, $g$, and its proximal operator.

params = (; A=A, b=b, tmp=zeros(m), λ=λ)
function f(x, p)
    A, b, tmp = p.A, p.b, p.tmp
    mul!(tmp, A, x)
    @. tmp -= b
    return 0.5 * sum(w->w^2, tmp)
end

function grad_f!(g, x, p)
    A, b, tmp = p.A, p.b, p.tmp
    mul!(tmp, A, x)
    @. tmp -= b
    mul!(g, A', tmp)
    return nothing
end

Hf = HessianLasso(A, zeros(m))
g(z, p) = p.λ*sum(x->abs(x), z)

function prox_g!(v, z, ρ, p)
    λ = p.λ
    @inline soft_threshold(x::T, κ::T) where {T <: Real} = sign(x) * max(zero(T), abs(x) - κ)
    v .= soft_threshold.(z, λ/ρ)
end
prox_g! (generic function with 1 method)

Finally, we can solve the problem.

solver = GeNIOS.GenericSolver(
    f, grad_f!, Hf,         # f(x)
    g, prox_g!,             # g(z)
    I, zeros(n);            # M, c: Mx + z = c
    params=params
)
res = solve!(solver; options=GeNIOS.SolverOptions(relax=true, verbose=true))
rmse = sqrt(1/m*norm(A*solver.zk - b, 2)^2)
println("Final RMSE: $(round(rmse, digits=8))")
Starting setup...
Setup in  0.110s

──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
    Iteration        Obj Val       r_primal         r_dual              ρ           Time
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
            0      1.995e+01            Inf            Inf      1.000e+00         0.000
            1      1.995e+01      0.000e+00      1.058e+01      1.000e+00         0.108
           20      4.268e+00      1.994e-02      2.279e-02      1.000e+00         0.113
           30      4.260e+00      2.118e-03      8.626e-04      1.000e+00         0.116

SOLVED in  0.116s, 30 iterations
Total time:  0.226s
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Final RMSE: 0.07648496

GPU support

Check out the page GPU Support for an example showing how to use GPUs with GeNIOS.

ProximalOperators.jl

We could alternatively use ProximalOperators.jl to define the proximal operator for g:

using ProximalOperators
prox_func = NormL1(λ)
gp(x, p) = prox_func(x)
prox_gp!(v, z, ρ, p) = prox!(v, prox_func, z, ρ)

# We see that this give the same result
solver = GeNIOS.GenericSolver(
    f, grad_f!, Hf,         # f(x)
    gp, prox_gp!,           # g(z)
    I, zeros(n);            # M, c: Mx + z = c
    params=params
)
res = solve!(solver; options=GeNIOS.SolverOptions(relax=true, verbose=true))
rmse = sqrt(1/m*norm(A*solver.zk - b, 2)^2)
println("Final RMSE: $(round(rmse, digits=8))")
Starting setup...
Setup in  0.008s

──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
    Iteration        Obj Val       r_primal         r_dual              ρ           Time
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
            0      1.995e+01            Inf            Inf      1.000e+00         0.000
            1      1.995e+01      0.000e+00      1.058e+01      1.000e+00         0.028
           20      4.268e+00      1.994e-02      2.279e-02      1.000e+00         0.033
           30      4.260e+00      2.118e-03      8.626e-04      1.000e+00         0.037

SOLVED in  0.037s, 30 iterations
Total time:  0.045s
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Final RMSE: 0.07648496

This page was generated using Literate.jl.