The lasso three ways
The lasso three ways: proximal gradient descent (ISTA), its accelerated version (FISTA), and ADMM, all built from the same two functions.
minimize f(β) + g(β), f(β) = ‖y - Xβ‖² / 2, g(β) = λ ‖β‖₁LeastSquaresLoss is smooth and proximable, so one object gives the gradient that (F)ISTA needs and the proximal operator that ADMM needs; its proximal operator comes from a thin SVD of X computed once.
using ProximalBase, LinearAlgebra, Random
rng = Xoshiro(1)
n, p = 200, 50
X = randn(rng, n, p)
β_true = [3.0; -2.0; 1.5; zeros(p - 3)]
y = X * β_true + 0.5 * randn(rng, n)
λ = 20.0
f = LeastSquaresLoss(y, X)
g = NormL1(λ)
L = lipschitz(f) # ‖X‖₂², the Lipschitz constant of ∇f, by Lanczos419.0142557272451ISTA: β ← prox{g/L}(β - ∇f(β)/L). `valueand_prox!` returns g at the new point, so tracking the objective costs no extra pass.
function ista(f, g, L, p; iters = 2000)
β, grad = zeros(p), zeros(p)
obj = Float64[]
for _ in 1:iters
gradient!(grad, f, β)
β .-= grad ./ L
gβ = value_and_prox!(β, g, β, 1 / L)
push!(obj, f(β) + gβ)
end
β, obj
endista (generic function with 1 method)FISTA: the same step taken at an extrapolated point z
function fista(f, g, L, p; iters = 500)
β, βold, z, grad = zeros(p), zeros(p), zeros(p), zeros(p)
t = 1.0
for _ in 1:iters
gradient!(grad, f, z)
copyto!(βold, β)
β .= z .- grad ./ L
prox!(β, g, β, 1 / L)
tnew = (1 + sqrt(1 + 4t^2)) / 2
z .= β .+ ((t - 1) / tnew) .* (β .- βold)
t = tnew
end
β
endfista (generic function with 1 method)ADMM on the split β = z: β ← prox{f/ρ}(z - u), z ← prox{g/ρ}(β + u), u ← u + β - z
function admm(f, g, p; ρ = 200.0, iters = 1000)
β, z, u, v = zeros(p), zeros(p), zeros(p), zeros(p)
for _ in 1:iters
v .= z .- u
prox!(β, f, v, 1 / ρ)
v .= β .+ u
prox!(z, g, v, 1 / ρ)
u .+= β .- z
end
z
endadmm (generic function with 1 method)the largest violation of the optimality conditions of the lasso: ∇f(β)ᵢ = -λ sign(βᵢ) where βᵢ ≠ 0, and |∇f(β)ᵢ| ≤ λ where βᵢ = 0
function kkt(f, λ, β)
grad = gradient(f, β)
maximum(i -> iszero(β[i]) ? max(abs(grad[i]) - λ, 0.0) : abs(grad[i] + λ * sign(β[i])), eachindex(β))
end
β_ista, obj = ista(f, g, L, p)
β_fista = fista(f, g, L, p)
β_admm = admm(f, g, p)
for (name, β) in (("ISTA", β_ista), ("FISTA", β_fista), ("ADMM", β_admm))
println(rpad(name, 6), "objective ", round(f(β) + g(β), digits = 6), ", nonzeros ",
count(!iszero, β), ", KKT violation ", round(kkt(f, λ, β), sigdigits = 2))
end
println("first coefficients: ", round.(β_fista[1:4], digits = 3), " (truth ", β_true[1:4], ")")
@assert all(diff(obj) .<= 1e-9) # ISTA decreases the objective
@assert kkt(f, λ, β_ista) < 1e-6 && kkt(f, λ, β_fista) < 1e-6 && kkt(f, λ, β_admm) < 1e-6
@assert norm(β_ista - β_fista) < 1e-6 && norm(β_admm - β_fista) < 1e-6
@assert findall(!iszero, β_fista) == 1:3 # the support of β_true
@assert L ≈ opnorm(X)^2ISTA objective 152.266543, nonzeros 3, KKT violation 8.2e-14
FISTA objective 152.266543, nonzeros 3, KKT violation 7.1e-14
ADMM objective 152.266543, nonzeros 3, KKT violation 9.2e-14
first coefficients: [2.905, -1.903, 1.413, 0.0] (truth [3.0, -2.0, 1.5, 0.0])the gradient mapping (β - prox_{g/L}(β - ∇f(β)/L)) L is zero at the solution: a stopping rule that works for any f and g
@assert norm(gradient_mapping(f, g, β_fista, 1 / L)) < 1e-8