11import numpy as np
22
3+ from pylops .optimization .sparsity import _hardthreshold
34from pyproximal .ProxOperator import _check_tau
45from 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 ()
0 commit comments