Skip to content

Commit 72255ed

Browse files
Implement quadratic envelope of cardinality funtion
1 parent ed831c3 commit 72255ed

File tree

5 files changed

+117
-4
lines changed

5 files changed

+117
-4
lines changed

docs/source/api/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ Operators
7979
Log
8080
ETP
8181
Geman
82+
QuadraticEnvelopeCard
8283

8384

8485
Other operators

examples/plot_concave_penalties.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,9 @@ def compare_penalty_and_proximal_operator(penalty):
5555
# The Geman penalty
5656
geman = pyproximal.Geman(3, 1.2)
5757
compare_penalty_and_proximal_operator(geman)
58+
59+
60+
###############################################################################
61+
# The quadratic envelope of the l0-penalty
62+
f_mu = pyproximal.QuadraticEnvelopeCard(1.5)
63+
compare_penalty_and_proximal_operator(f_mu)
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import numpy as np
2+
3+
from pyproximal.ProxOperator import _check_tau
4+
from pyproximal import ProxOperator
5+
6+
7+
class QuadraticEnvelopeCard(ProxOperator):
8+
r"""Quadratic envelope of the :math:`\ell_0`-penalty.
9+
10+
The :math:`\ell_0`-penalty is also known as the *cardinality function*, and the
11+
quadratic envelope :math:`\mathcal{Q}(\mu\|\cdot\|_0)` of it is defined as
12+
13+
.. math::
14+
15+
\mathcal{Q}(\mu\|\cdot\|_0)(x) = \sum_i \left(\mu - \frac{1}{2}\max(0, \sqrt{2\mu} - |x_i|)^2\right)
16+
17+
where :math:`\mu \geq 0`.
18+
19+
Parameters
20+
----------
21+
mu : :obj:`float`
22+
Threshold parameter.
23+
24+
Notes
25+
-----
26+
The terminology *quadratic envelope* was coined in [1]_, however, the rationale has
27+
been used earlier, e.g. in [2]_. In a general setting, the quadratic envelope
28+
:math:`\mathcal{Q}(f)(x)` is defined such that
29+
30+
.. math::
31+
32+
\left(f(x) + \frac{1}{2}\|x-y\|_2^2\right)^{**} = \mathcal{Q}(f)(x) + \frac{1}{2}\|x-y\|_2^2
33+
34+
where :math:`g^{**}` denotes the bi-conjugate of :math:`g`, which is the l.s.c.
35+
convex envelope of :math:`g`.
36+
37+
There is no closed-form expression for :math:`\mathcal{Q}(f)(x)` given an arbitrary
38+
function :math:`f`. However, for certain special cases, such as in the case of the
39+
cardinality function, such expressions do exist.
40+
41+
The proximal operator is given by
42+
43+
.. math::
44+
45+
\prox_{\tau\mathcal{Q}(\mu\|\cdot\|_0)}(x) =
46+
\begin{cases}
47+
x_i, & |x_i| \geq \sqrt{2 \mu} \\
48+
\frac{x_i-\tau\sqrt{2\mu}\sgn(x_i)}{1-\tau}, & \tau\sqrt{2\mu} < |x_i| < \sqrt{2 \mu} \\
49+
0, & |x_i| \leq \tau\sqrt{2 \mu}
50+
\end{cases}
51+
52+
By inspecting the structure of the proximal operator it is clear that large values
53+
are unaffected, whereas smaller ones are penalized partially or completely. Such
54+
properties are desirable to counter the effect of *shrinking bias* observed with
55+
e.g. the :math:`\ell_1`-penalty. Note that in the limit :math:`\tau=1` this becomes
56+
the hard thresholding with threshold :math:`\sqrt{2\mu}`. It should also be noted
57+
that this proximal operator is identical to the Minimax Concave Penalty (MCP)
58+
proposed in [3]_.
59+
60+
.. [1] Carlsson, M. "On Convex Envelopes and Regularization of Non-convex
61+
Functionals Without Moving Global Minima", In Journal of Optimization Theory
62+
and Applications, 183:66–84, 2019.
63+
.. [2] Larsson, V. and Olsson, C. "Convex Low Rank Approximation", In International
64+
Journal of Computer Vision (IJCV), 120:194–214, 2016.
65+
.. [3] Zhang et al. "Nearly unbiased variable selection under minimax concave
66+
penalty", In the Annals of Statistics, 38(2):894–942, 2010.
67+
68+
"""
69+
70+
def __init__(self, mu):
71+
super().__init__(None, False)
72+
self.mu = mu
73+
74+
def __call__(self, x):
75+
return np.sum(self.elementwise(x))
76+
77+
def elementwise(self, x):
78+
return self.mu - 0.5 * np.maximum(0, np.sqrt(2 * self.mu) - np.abs(x)) ** 2
79+
80+
@_check_tau
81+
def prox(self, x, tau):
82+
r = np.abs(x)
83+
idx = r < np.sqrt(2 * self.mu)
84+
if tau >= 1:
85+
r[idx] = 0
86+
else:
87+
r[idx] = np.maximum(0, (r[idx] - tau * np.sqrt(2 * self.mu)) / (1 - tau))
88+
return r * np.sign(x)

pyproximal/proximal/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
Log Logarithmic
2929
ETP Exponential-type penalty
3030
Geman Geman penalty
31+
QuadraticEnvelopeCard The quadratic envelope of the cardinality function
3132
3233
"""
3334

@@ -51,9 +52,10 @@
5152
from .Log import *
5253
from .ETP import *
5354
from .Geman import *
55+
from .QuadraticEnvelope import *
5456

5557
__all__ = ['Box', 'Simplex', 'Intersection', 'AffineSet', 'Quadratic',
5658
'Euclidean', 'EuclideanBall', 'L0Ball', 'L1', 'L1Ball', 'L2',
5759
'L2Convolve', 'L21', 'L21_plus_L1', 'Huber', 'Nuclear',
5860
'NuclearBall', 'Orthogonal', 'VStack', 'Nonlinear', 'SCAD',
59-
'Log', 'ETP', 'Geman']
61+
'Log', 'ETP', 'Geman', 'QuadraticEnvelopeCard']

pytests/test_concave_penalties.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
import numpy as np
44

55
from pyproximal.utils import moreau
6-
from pyproximal.proximal import ETP, Geman, Log, SCAD
6+
from pyproximal.proximal import ETP, Geman, Log, SCAD, QuadraticEnvelopeCard
77

8-
par1 = {'nx': 10, 'sigma': 1., 'a': 2.1, 'gamma': 0.5, 'dtype': 'float32'} # even float32
9-
par2 = {'nx': 11, 'sigma': 2., 'a': 3.7, 'gamma': 5.0, 'dtype': 'float64'} # odd float64
8+
par1 = {'nx': 10, 'sigma': 1., 'a': 2.1, 'gamma': 0.5, 'mu': 0.5, 'dtype': 'float32'} # even float32
9+
par2 = {'nx': 11, 'sigma': 2., 'a': 3.7, 'gamma': 5.0, 'mu': 1.5, 'dtype': 'float64'} # odd float64
1010

1111

1212
@pytest.mark.parametrize("par", [(par1), (par2)])
@@ -79,3 +79,19 @@ def test_Geman(par):
7979
# Check proximal operator
8080
tau = 2.
8181
assert moreau(geman, x, tau)
82+
83+
84+
@pytest.mark.parametrize("par", [(par1), (par2)])
85+
def test_QuadraticEnvelopeCard(par):
86+
"""QuadraticEnvelopeCard penalty and proximal/dual proximal
87+
"""
88+
np.random.seed(10)
89+
fmu = QuadraticEnvelopeCard(mu=par['mu'])
90+
# Quadratic envelope of the l0-penalty
91+
x = np.random.normal(0., 10.0, par['nx']).astype(par['dtype'])
92+
expected = np.linalg.norm(par['mu'] - 0.5 * np.maximum(0, np.sqrt(2 * par['mu']) - np.abs(x)) ** 2, 1)
93+
assert fmu(x) == pytest.approx(expected)
94+
95+
# Check proximal operator
96+
tau = 0.25
97+
assert moreau(fmu, x, tau)

0 commit comments

Comments
 (0)