Skip to content

Commit 225eb9c

Browse files
Tutorial: Hankel matrix recovery
1 parent a5c2ee6 commit 225eb9c

File tree

11 files changed

+595
-8
lines changed

11 files changed

+595
-8
lines changed

docs/source/api/index.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Orthogonal projections
2020
AffineSetProj
2121
BoxProj
2222
EuclideanBallProj
23+
HankelProj
2324
HyperPlaneBoxProj
2425
IntersectionProj
2526
L0BallProj
@@ -62,6 +63,7 @@ Convex
6263
Box
6364
Euclidean
6465
EuclideanBall
66+
Hankel
6567
Huber
6668
Intersection
6769
L0
@@ -88,6 +90,7 @@ Non-Convex
8890
Geman
8991
Log
9092
QuadraticEnvelopeCard
93+
QuadraticEnvelopeCardIndicator
9194
SCAD
9295

9396

@@ -100,7 +103,7 @@ Matrix-only
100103
Nuclear
101104
NuclearBall
102105
SingularValuePenalty
103-
106+
QuadraticEnvelopeRankL2
104107

105108
Other
106109
^^^^^

docs/source/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@
140140
"sgn": r"\operatorname{sgn}",
141141
"argmin": r"\operatorname*{argmin}",
142142
"diag": r"\operatorname{diag}",
143+
"rank": r"\operatorname{rank}",
143144
}
144145
}
145146
}

pyproximal/projection/Hankel.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import numpy as np
2+
from scipy.linalg import hankel
3+
4+
5+
class HankelProj:
6+
r"""Hankel matrix projection.
7+
8+
Solves the least squares problem
9+
10+
.. math::
11+
\min_{X\in\mathcal{H}} \|X-X_0\|_F^2
12+
13+
where :math:`\mathcal{H}` is the set of Hankel matrices.
14+
15+
Notes
16+
-----
17+
The solution to the above-mentioned least squares problem is given by a Hankel matrix,
18+
where the (constant) anti-diagonals are the average value along the corresponding
19+
anti-diagonals of the original matrix :math:`X_0`.
20+
"""
21+
22+
def __call__(self, X):
23+
m, n = X.shape
24+
ind = hankel(np.arange(m, dtype=np.int32), m - 1 + np.arange(n, dtype=np.int32))
25+
mean_values = np.bincount(ind.ravel(), weights=X.ravel()) / np.bincount(ind.ravel())
26+
return mean_values[ind]

pyproximal/projection/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
NuclearBallProj Projection onto a Nuclear Ball
1414
IntersectionProj Projection onto an Intersection of sets
1515
AffineSetProj Projection onto an Affine set
16+
HankelProj Projection onto the set of Hankel matrices
1617
1718
"""
1819

@@ -24,8 +25,9 @@
2425
from .Nuclear import *
2526
from .Intersection import *
2627
from .AffineSet import *
28+
from .Hankel import *
2729

2830

2931
__all__ = ['BoxProj', 'HyperPlaneBoxProj', 'SimplexProj', 'L0BallProj',
3032
'L1BallProj', 'EuclideanBallProj', 'NuclearBallProj',
31-
'IntersectionProj', 'AffineSetProj']
33+
'IntersectionProj', 'AffineSetProj', 'HankelProj']

pyproximal/proximal/Hankel.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import numpy as np
2+
from pyproximal.ProxOperator import _check_tau
3+
from pyproximal import ProxOperator
4+
from pyproximal.projection import HankelProj
5+
6+
7+
class Hankel(ProxOperator):
8+
r"""Hankel proximal operator.
9+
10+
Proximal operator of the Hankel matrix indicator function.
11+
12+
Parameters
13+
----------
14+
dim : :obj:`tuple`
15+
Dimension of the Hankel matrix.
16+
17+
Notes
18+
-----
19+
As the Hankel Operator is an indicator function, the proximal operator corresponds to
20+
its orthogonal projection (see :class:`pyproximal.projection.HankelProj` for
21+
details).
22+
23+
"""
24+
def __init__(self, dim):
25+
super().__init__(None, False)
26+
self.dim = dim
27+
self.hankel_proj = HankelProj()
28+
29+
def __call__(self, x):
30+
X = x.reshape(self.dim)
31+
return np.allclose(X, self.hankel_proj(X))
32+
33+
@_check_tau
34+
def prox(self, x, tau):
35+
X = x.reshape(self.dim)
36+
return self.hankel_proj(X).ravel()

pyproximal/proximal/QuadraticEnvelope.py

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import numpy as np
22

3+
from pylops.optimization.sparsity import _hardthreshold
34
from pyproximal.ProxOperator import _check_tau
45
from pyproximal import ProxOperator
56

@@ -21,6 +22,10 @@ class QuadraticEnvelopeCard(ProxOperator):
2122
mu : :obj:`float`
2223
Threshold parameter.
2324
25+
See Also
26+
--------
27+
QuadraticEnvelopeCardIndicator: Quadratic envelope of the indicator function of :math:`\ell_0`-penalty
28+
2429
Notes
2530
-----
2631
The terminology *quadratic envelope* was coined in [1]_, however, the rationale has
@@ -57,6 +62,8 @@ class QuadraticEnvelopeCard(ProxOperator):
5762
that this proximal operator is identical to the Minimax Concave Penalty (MCP)
5863
proposed in [3]_.
5964
65+
References
66+
----------
6067
.. [1] Carlsson, M. "On Convex Envelopes and Regularization of Non-convex
6168
Functionals Without Moving Global Minima", In Journal of Optimization Theory
6269
and Applications, 183:66–84, 2019.
@@ -86,3 +93,226 @@ def prox(self, x, tau):
8693
else:
8794
r[idx] = np.maximum(0, (r[idx] - tau * np.sqrt(2 * self.mu)) / (1 - tau))
8895
return r * np.sign(x)
96+
97+
98+
class QuadraticEnvelopeCardIndicator(ProxOperator):
99+
r"""Quadratic envelope of the indicator function of the :math:`\ell_0`-penalty.
100+
101+
The :math:`\ell_0`-penalty is also known as the *cardinality function*, and the
102+
indicator function :math:`\mathcal{I}_{r_0}` is defined as
103+
104+
.. math::
105+
106+
\mathcal{I}_{r_0}(\mathbf{x}) =
107+
\begin{cases}
108+
0, & \mathbf{x}\leq r_0 \\
109+
\infty, & \text{otherwise}
110+
\end{cases}
111+
112+
Let :math:`\tilde{\mathbf{x}}` denote the vector :math:`\mathbf{x}` resorted such that the
113+
sequence :math:`(\tilde{x}_i)` is non-increasing. The quadratic envelope
114+
:math:`\mathcal{Q}(\mathcal{I}_{r_0})` can then be written as
115+
116+
.. math::
117+
118+
\mathcal{Q}(\mathcal{I}_{r_0})(x) =
119+
\frac{1}{2k^*}\left(\sum_{i>r_0-k^*}|\tilde{x}_i|\right)^2
120+
- \frac{1}{2}\left(\sum_{i>r_0-k^*}|\tilde{x}_i|^2
121+
122+
where :math:`r_0 \geq 0` and :math:`k^* \leq r_0`, see [3]_ for details. There are
123+
other, equivalent ways, of expressing this penalty, see e.g. [1]_ and [2]_.
124+
125+
Parameters
126+
----------
127+
r0 : :obj:`int`
128+
Threshold parameter.
129+
130+
See Also
131+
--------
132+
QuadraticEnvelopeCard: Quadratic envelope of the :math:`\ell_0`-penalty
133+
134+
Notes
135+
-----
136+
The terminology *quadratic envelope* was coined in [1]_, however, the rationale has
137+
been used earlier, e.g. in [2]_. In a general setting, the quadratic envelope
138+
:math:`\mathcal{Q}(f)(x)` is defined such that
139+
140+
.. math::
141+
142+
\left(f(x) + \frac{1}{2}\|x-y\|_2^2\right)^{**} = \mathcal{Q}(f)(x) + \frac{1}{2}\|x-y\|_2^2
143+
144+
where :math:`g^{**}` denotes the bi-conjugate of :math:`g`, which is the l.s.c.
145+
convex envelope of :math:`g`.
146+
147+
There is no closed-form expression for :math:`\mathcal{Q}(f)(x)` given an arbitrary
148+
function :math:`f`. However, for certain special cases, such as in the case of the
149+
indicator function of the cardinality function, such expressions do exist.
150+
151+
The proximal operator does not have a closed-form, and we refer to [1]_ for more details.
152+
Note that this is a non-separable penalty.
153+
154+
References
155+
----------
156+
.. [1] Carlsson, M. "On Convex Envelopes and Regularization of Non-convex
157+
Functionals Without Moving Global Minima", In Journal of Optimization Theory
158+
and Applications, 183:66–84, 2019.
159+
.. [2] Larsson, V. and Olsson, C. "Convex Low Rank Approximation", In International
160+
Journal of Computer Vision (IJCV), 120:194–214, 2016.
161+
.. [3] Andersson et al. "Convex envelopes for fixed rank approximation", In
162+
Optimization Letters, 11:1783–1795, 2017.
163+
164+
"""
165+
166+
def __init__(self, r0):
167+
super().__init__(None, False)
168+
self.r0 = r0
169+
170+
def __call__(self, x):
171+
if x.size <= self.r0 or np.count_nonzero(x) <= self.r0:
172+
return 0
173+
xs = np.sort(np.abs(x))[::-1]
174+
sums = np.cumsum(xs[::-1])
175+
sums = sums[-self.r0:] / np.arange(1, self.r0 + 1)
176+
tmp = np.diff(sums) > 0
177+
k_star = np.argmax(tmp)
178+
if k_star == 0 and not tmp[k_star]:
179+
k_star = self.r0 - 1
180+
return 0.5 * ((k_star + 1) * sums[k_star] ** 2 - np.sum(xs[self.r0-k_star-1:] ** 2))
181+
182+
@_check_tau
183+
def prox(self, y, tau):
184+
rho = 1 / tau
185+
if rho <= 1:
186+
return _hardthreshold(y, tau)
187+
if y.size <= self.r0:
188+
return y
189+
190+
r = np.abs(y)
191+
theta = np.sign(y)
192+
id = np.argsort(-r, kind='quicksort')
193+
rsort = r[id]
194+
idinv = np.zeros_like(id)
195+
idinv[id] = np.arange(r.size)
196+
rnew = np.concatenate((rsort[:self.r0], rho * rsort[self.r0:]))
197+
198+
if rho * rsort[self.r0] < rsort[self.r0 - 1]:
199+
x = rnew
200+
x = x[idinv]
201+
x = x * theta
202+
else:
203+
j = np.min(np.where(rnew <= rnew[self.r0])[0])
204+
l = np.max(np.where(rnew >= rnew[self.r0 - 1])[0])
205+
z = np.sort(rnew[j:l + 1])[::-1]
206+
z1 = z[0]
207+
for z2 in z[1:]:
208+
s = (z1 + z2) / 2
209+
temp = np.where(rnew <= s)[0]
210+
j1 = np.min(temp)
211+
temp = np.where(rnew >= s)[0]
212+
l1 = np.max(temp)
213+
sI = (rho * sum(rsort[j1:l1 + 1])) / ((self.r0 - j1) * rho + (l1 + 1 - self.r0) * 1)
214+
if z2 <= sI <= z1:
215+
x = np.concatenate((np.maximum(rnew[:self.r0], sI), np.minimum(rnew[self.r0:], sI)))
216+
x = x[idinv]
217+
x = x * theta
218+
break
219+
z1 = z2
220+
221+
return (rho * y - x) / (rho - 1)
222+
223+
224+
class QuadraticEnvelopeRankL2(ProxOperator):
225+
r"""Quadratic envelope of the rank function with an L2 misfit term.
226+
227+
The penalty :math:`p` is given by
228+
229+
.. math::
230+
231+
p(X) = \mathcal{R}_{r_0}(X) + \frac{1}{2}\|X - M\|_F^2
232+
233+
where :math:`\mathcal{R}_{r_0}` is the quadratic envelope of the hard-rank function.
234+
235+
Parameters
236+
----------
237+
dim : :obj:`tuple`
238+
Size of input matrix :math:`X`.
239+
r0 : :obj:`int`
240+
Threshold parameter, encouraging matrices with rank lower than or equal to r0.
241+
M : :obj:`numpy.ndarray`
242+
L2 misfit term (must be the same size as the input matrix).
243+
244+
See Also
245+
--------
246+
SingularValuePenalty: Proximal operator of a penalty acting on the singular values
247+
QuadraticEnvelopeCardIndicator: Quadratic envelope of the indicator function of :math:`\ell_0`-penalty
248+
249+
Notes
250+
-----
251+
The proximal operator solves the minimization problem
252+
253+
.. math::
254+
\argmin_Z \mathcal{R}_{r_0}(Z) + \frac{1}{2}\|Z - M\|_F^2 + \frac{1}{2\tau}\| Z - X \|_F^2
255+
256+
which is a convex-concave min-max problem, see [1]_ for details.
257+
258+
References
259+
----------
260+
.. [1] Larsson, V. and Olsson, C. "Convex Low Rank Approximation", In International
261+
Journal of Computer Vision (IJCV), 120:194–214, 2016.
262+
263+
"""
264+
265+
def __init__(self, dim, r0, M):
266+
super().__init__(None, False)
267+
self.dim = dim
268+
self.r0 = r0
269+
self.M = M.copy()
270+
self.penalty = QuadraticEnvelopeCardIndicator(r0)
271+
272+
def __call__(self, x):
273+
X = x.reshape(self.dim)
274+
eigs = np.linalg.eigvalsh(X.T @ X)
275+
eigs[eigs < 0] = 0 # ensure all eigenvalues at positive
276+
return np.sum(self.penalty(np.sqrt(eigs))) + 0.5 * np.linalg.norm(X - self.M, 'fro')
277+
278+
@_check_tau
279+
def prox(self, x, tau):
280+
rho = 1 / tau
281+
P = x.reshape(self.dim)
282+
283+
Y = (self.M + rho * P) / (1 + rho)
284+
U, yk, Vh = np.linalg.svd(Y, full_matrices=False)
285+
n = yk.size
286+
r = np.concatenate((yk[:self.r0], (1 + rho) * yk[self.r0:]))
287+
ind = np.argsort(r, kind='quicksort')
288+
p = r[ind]
289+
290+
a = (n - self.r0) / rho
291+
b = (rho + 1) / rho * np.sum(yk[self.r0:])
292+
293+
# Base case
294+
zk = yk.copy()
295+
zk[self.r0:] = (1 + rho) * yk[self.r0:]
296+
297+
for k, ii in enumerate(ind):
298+
299+
if ii < self.r0:
300+
a = a + (rho + 1) / rho
301+
b = b + (rho + 1) / rho * yk[ii]
302+
else:
303+
a = a - 1 / rho
304+
b = b - (rho + 1) / rho * yk[ii]
305+
306+
if a == 0:
307+
continue
308+
309+
s = b / a
310+
311+
if p[k] <= s <= p[k + 1]:
312+
zk = np.maximum(s, yk)
313+
zk[self.r0:] = np.minimum(s, (1 + rho) * yk[self.r0:])
314+
break
315+
316+
Z = np.dot(U * zk, Vh)
317+
X = P + (self.M - Z) / rho
318+
return X.ravel()

pyproximal/proximal/SingularValuePenalty.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class SingularValuePenalty(ProxOperator):
2020
----------
2121
dim : :obj:`tuple`
2222
Size of matrix :math:`\mathbf{X}`.
23-
penalty : :class:`pyproximal.ProxOperator`
23+
penalty : :obj:`pyproximal.ProxOperator`
2424
Function acting on the singular values.
2525
2626
Notes

0 commit comments

Comments
 (0)