Constraints on separable penalties
Separable penalties with constraints.
Constrained(f, C) is a convex separable f restricted to a box, a hyperplane, a halfspace or a simplex C, with its proximal operator. On a box it clamps the proximal operator of f; on a hyperplane ⟨a, x⟩ = b it is prox_{γf}(x - γνa) for the multiplier ν that meets the constraint. Three problems:
- regression on compositional data, whose coefficients sum to zero, with the lasso penalty on that hyperplane;
- the lasso with coefficients in [0, 1], by coordinate descent, since a separable penalty on a box stays separable;
- a soft selection of the k largest of n scores, by the projection onto the capped simplex {z : 0 ≤ z ≤ 1, Σᵢ zᵢ = k}.
using ProximalBase, LinearAlgebra, Random
rng = Xoshiro(16)
function fista(f, g, p; iters = 5000)
L = lipschitz(f)
β, β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
β
end
centered(A) = A .- sum(A; dims = 1) ./ size(A, 1)centered (generic function with 1 method)The lasso on compositional data
Each row of W holds the proportions of p parts, positive and summing to one, as in microbiome or household-budget data. The response depends on the log proportions, y = log.(W) β + ε with Σⱼ βⱼ = 0 (Lin, Shi, Feng and Li, 2014). The constraint makes the fit invariant to the scale of each row: scaling row i by cᵢ adds log cᵢ to each of its predictors, and so log cᵢ Σⱼ βⱼ = 0 to its fit. An intercept, unpenalized, is removed by centering.
n, p = 100, 30
W = exp.(randn(rng, n, p))
W ./= sum(W; dims = 2)
β_true = [1.0; -0.8; 0.6; -0.5; -0.3; zeros(p - 5)] # sums to zero
y = log.(W) * β_true .+ 0.2 .* randn(rng, n)
λ = 0.07
f = (1 / n) * LeastSquaresLoss(centered(y), centered(log.(W)))
g = Constrained(NormL1(λ), IndHyperplane(1, 0))
β = fista(f, g, p)
println("zero-sum lasso: support ", findall(!iszero, β), ", Σβ = ", sum(β))
@assert abs(sum(β)) < 1e-12 && norm(gradient_mapping(f, g, β, 1 / lipschitz(f))) < 1e-9
@assert findall(!iszero, β) ⊇ 1:5zero-sum lasso: support [1, 2, 3, 4, 5, 22, 24], Σβ = 6.522560269672795e-16the optimality conditions: ∇f(β)ⱼ + ν + λ sign(βⱼ) = 0 where βⱼ ≠ 0, and |∇f(β)ⱼ + ν| ≤ λ where βⱼ = 0, for one multiplier ν
grad = gradient(f, β)
S = findall(!iszero, β)
ν = -grad[S[1]] - λ * sign(β[S[1]])
@assert all(abs(grad[j] + ν + λ * sign(β[j])) < 1e-8 for j in S)
@assert all(abs(grad[j] + ν) <= λ + 1e-8 for j in 1:p if iszero(β[j]))scaling the rows of W leaves the estimate unchanged, up to the accuracy of FISTA; the estimate of the lasso without the constraint changes
c = exp.(2 .* randn(rng, n))
fc = (1 / n) * LeastSquaresLoss(centered(y), centered(log.(c .* W)))
βc = fista(fc, g, p)
β1, β1c = fista(f, NormL1(λ), p), fista(fc, NormL1(λ), p)
println("rows rescaled: zero-sum lasso moves by ", norm(βc - β), ", the lasso by ", norm(β1c - β1))
@assert norm(βc - β) < 1e-6 && norm(β1c - β1) > 1e-3rows rescaled: zero-sum lasso moves by 1.816849917175604e-10, the lasso by 0.050387801891688455The lasso on [0, 1] by coordinate descent
NormL1 on the box 0 ≤ β ≤ 1 is separable: prox_entry clamps the soft thresholding of each coordinate, so coordinate descent with exact residuals solves the problem one coefficient at a time.
X = randn(rng, n, p)
β_true = [2.0; 1.5; 0.5; zeros(p - 3)] # the first two exceed the bound
y = X * β_true + 0.3 * randn(rng, n)
f = (1 / n) * LeastSquaresLoss(y, X)
g = Constrained(NormL1(0.05), IndBox(0, 1))
β = SparseIterate(p)
r = copy(y) # the residual y - Xβ
a = [sum(abs2, view(X, :, j)) / n for j in 1:p] # the curvature of f along coordinate j
for sweep in 1:1000
change = 0.0
for j in 1:p
old = β[j]
v = old + dot(view(X, :, j), r) / (n * a[j]) # the minimizer of f along j
new = prox_entry(g, v, j, 1 / a[j]) # soft thresholding, then the box
if new != old
β[j] = new
r .-= (new - old) .* view(X, :, j)
change = max(change, abs(new - old))
end
end
change < 1e-13 && break
end
βf = fista(f, g, p)
println("lasso on [0, 1]: β[1:4] = ", round.(Array(β)[1:4], digits = 4), ", ", count(!iszero, β), " nonzero, ",
"FISTA differs by ", norm(Array(β) - βf))
@assert all(0 .<= β .<= 1) && β[1] == β[2] == 1 # the first two sit on the bound
@assert norm(Array(β) - βf) < 1e-8
@assert norm(gradient_mapping(f, g, Array(β), 1 / lipschitz(f))) < 1e-9lasso on [0, 1]: β[1:4] = [1.0, 1.0, 0.254, 0.0], 10 nonzero, FISTA differs by 6.714243124023615e-15A soft selection of the k largest scores
The projection of t s onto the capped simplex weights the items between 0 and 1, with total weight k. For small t it spreads the weight; as t grows it becomes the indicator of the k largest scores.
k = 3
s = randn(rng, 10)
C = Constrained(IndBox(0, 1), IndHyperplane(1, k))
topk = sortperm(s; rev = true)[1:k]
for t in (0.5, 2.0, 1000.0)
z = prox(C, t .* s)
println("t = ", t, ": weights ", round.(z, digits = 3))
@assert all(0 .<= z .<= 1) && sum(z) ≈ k
end
z = prox(C, 1000 .* s)
@assert z[topk] == ones(k) && count(!iszero, z) == kt = 0.5: weights [0.141, 0.0, 0.475, 0.334, 0.119, 0.185, 0.0, 1.0, 0.0, 0.746]
t = 2.0: weights [0.0, 0.0, 0.783, 0.217, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0]
t = 1000.0: weights [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0]