A lasso path by coordinate descent
A lasso path by coordinate descent, with warm starts and an active set.
minimize ‖y - Xβ‖² / 2 + λ ‖β‖₁ for a decreasing sequence of λOne coordinate step minimizes over βⱼ alone: with the residual r = y - Xβ and aⱼ = ‖Xⱼ‖², the new value is the proximal operator of the penalty, with step 1/aⱼ, at the point βⱼ + Xⱼ'r / aⱼ. prox_entry computes it without writing β, so a coordinate that stays at zero is never written, and a SparseIterate stores only the coordinates that have been nonzero: its stored entries are the active set. kkt_violation tells whether a coordinate at zero should join.
using ProximalBase, LinearAlgebra, Random
rng = Xoshiro(2)
n, p = 100, 300
X = randn(rng, n, p)
β_true = zeros(p)
β_true[[5, 50, 120, 200, 260]] = [2.0, -1.5, 1.0, -1.0, 0.8]
y = X * β_true + 0.3 * randn(rng, n)
a = [sum(abs2, view(X, :, j)) for j in 1:p] # coordinate curvatures ‖Xⱼ‖²300-element Vector{Float64}:
89.98138413168286
104.80733871029449
77.1542177046276
90.67467270132737
94.0829477069477
91.85243397389704
91.32431552281932
96.91562736831847
102.91332221149068
83.07024709204346
⋮
82.75757990637281
101.9098275445456
102.5409472135332
89.26310551684875
115.98237742481042
78.75102932852973
112.32344202391447
75.39095234255147
109.64091573823575one pass over the coordinates js; returns the largest change of a coordinate
function sweep!(β, r, X, a, g, js)
δ = 0.0
for j in js
old = β[j]
z = old + dot(view(X, :, j), r) / a[j]
new = prox_entry(g, z, j, 1 / a[j])
if new != old # write only what changes
β[j] = new
axpy!(old - new, view(X, :, j), r) # keep r = y - Xβ
δ = max(δ, abs(new - old) * sqrt(a[j]))
end
end
δ
endsweep! (generic function with 1 method)solve for one λ from the current β: sweep the active set to convergence, then check every coordinate; repeat while some coordinate at zero violates optimality
function solve!(β, r, X, a, g; tol = 1e-10)
grad = similar(r, size(X, 2))
while true
while sweep!(β, r, X, a, g, collect(activecoordinates(β))) > tol end
mul!(grad, X', r)
grad .*= -1 # ∇f(β) = -X'r
violators = [j for j in 1:size(X, 2) if !isactive(β, j) && kkt_violation(g, grad, j) > 1e-9]
isempty(violators) && return β
sweep!(β, r, X, a, g, violators)
end
end
λmax = norm(X' * y, Inf) # the smallest λ with β = 0
λs = λmax .* exp.(range(0, log(0.02), length = 25))
β = SparseIterate(p)
r = copy(y)
path = Vector{Float64}[]
for λ in λs
solve!(β, r, X, a, NormL1(λ))
push!(path, Vector(β))
println("λ = ", rpad(round(λ, digits = 2), 8), " nonzeros ", rpad(count(!iszero, β), 4),
" stored ", nnz(β))
endλ = 217.65 nonzeros 0 stored 0
λ = 184.91 nonzeros 1 stored 1
λ = 157.1 nonzeros 1 stored 1
λ = 133.47 nonzeros 2 stored 2
λ = 113.4 nonzeros 2 stored 2
λ = 96.34 nonzeros 2 stored 2
λ = 81.85 nonzeros 4 stored 4
λ = 69.54 nonzeros 4 stored 4
λ = 59.08 nonzeros 5 stored 5
λ = 50.19 nonzeros 5 stored 5
λ = 42.64 nonzeros 5 stored 5
λ = 36.23 nonzeros 5 stored 5
λ = 30.78 nonzeros 5 stored 5
λ = 26.15 nonzeros 5 stored 5
λ = 22.22 nonzeros 5 stored 5
λ = 18.88 nonzeros 5 stored 5
λ = 16.04 nonzeros 5 stored 5
λ = 13.62 nonzeros 6 stored 6
λ = 11.58 nonzeros 7 stored 7
λ = 9.83 nonzeros 9 stored 9
λ = 8.36 nonzeros 13 stored 13
λ = 7.1 nonzeros 16 stored 18
λ = 6.03 nonzeros 20 stored 20
λ = 5.12 nonzeros 25 stored 25
λ = 4.35 nonzeros 29 stored 31the optimality conditions at every λ of the path
f = LeastSquaresLoss(y, X)
for (λ, βλ) in zip(λs, path)
grad = gradient(f, βλ)
@assert all(abs(grad[i] + λ * sign(βλ[i])) < 1e-6 for i in findall(!iszero, βλ))
@assert all(abs(grad[i]) <= λ + 1e-6 for i in findall(iszero, βλ))
end
@assert iszero(path[1]) # λmax gives β = 0
@assert Set(findall(!iszero, path[12])) == Set([5, 50, 120, 200, 260])
@assert nnz(β) < p # most coordinates were never stored