Back to Cvxpy

Sparse signal recovery

doc/source/examples/dnlp/sparse_signal_recovery.rst

1.9.25.2 KB
Original Source

Sparse signal recovery

This example is adapted from Cederberg, Zhang, Nobel, and Boyd, "Disciplined Nonlinear Programming <https://stanford.edu/~boyd/papers/pdf/dnlp.pdf>__\ ".

In this example, the goal is to recover a sparse signal :math:x_0 \in \mathbf{R}^n from a given measurement vector :math:y = Ax_0, where :math:A \in \mathbf{R}^{m \times n} (with :math:m < n) is a known sensing matrix [26]. A common heuristic based on convex optimization is to minimize the :math:\ell_1 norm of :math:x subject to :math:Ax = y. An alternative approach based on nonconvex optimization is to minimize the sum of the square roots of the absolute values of the entries of :math:x, which tends to promote sparsity more aggressively. This leads to the problem

.. math::

\begin{array}{ll} \mbox{minimize} & \sum_{i=1}^{n} \sqrt{|x_i|} \ \mbox{subject to} & Ax = y, \end{array}

with variable :math:x. This problem is DNLP-compliant since the objective is L-convex.

DNLP specification. The code specifying this problem is given below. For this example, we use Knitro's interior-point method as the solver, because Ipopt failed to solve this problem reliably. The issue likely arises from the fact that the objective function gradient becomes infinite as any entry of x approaches zero, so no KKT point exists for the canonicalized problem.

We consider a simulation with signal dimension n = 100, where we vary the number of measurements m from 60 to 80, and the cardinality of the true signal :math:x_0 from 30 to 50. The positions of the nonzero entries of :math:x_0 are sampled from a uniform distribution, with the nonzero values chosen as :math:\mathcal{N}(0, 25) random variables. The entries of A are sampled from a standard normal distribution. We say that the recovery is successful if the relative error :math:\|\hat{x} - x_0\|_2 / \|x_0\|_2 is less than :math:10^{-2}, where :math:\hat{x} is the recovered signal. To estimate the probability of successful recovery for each pair of number of measurements and signal cardinality, we repeat the simulation 100 times and compute the fraction of successful recoveries.

.. code:: python

import matplotlib.pyplot as plt import numpy as np import numpy.linalg as LA import cvxpy as cp from matplotlib.ticker import PercentFormatter

rng = np.random.default_rng(0) n, T = 100, 100 m = list(range(60, 81, 2)) k = list(range(30, 51, 2))

RECOVERY_TOL = 1e-2 probabilities_noncvx = np.zeros((len(m), len(k))) probabilities_cvx = np.zeros((len(m), len(k)))

for time in range(T): print(f"Trial {time+1} of {T}") for kk in k: x0 = np.zeros((n,)) ind = rng.permutation(n) ind = ind[0:kk] x0[ind] = rng.standard_normal((kk,)) * 5

     for mm in m:
           A = rng.standard_normal((mm, n))
           y = A @ x0
           
           # nonconvex recovery
           x = cp.Variable((n, ))
           prob = cp.Problem(cp.Minimize(cp.sum(cp.sqrt(cp.abs(x)))), [A @ x == y])
           prob.solve(nlp=True, solver=cp.KNITRO, verbose=False, algorithm=1)
           x_noncvx_recovery = x.value      
  
           # convex recovery
           x = cp.Variable((n, ))
           prob = cp.Problem(cp.Minimize(cp.norm1(x)), [A @ x == y])
           prob.solve(solver=cp.CLARABEL)
           x_cvx_recovery = x.value

           # were the recoveries successful?
           noncvx_success = LA.norm(x_noncvx_recovery - x0, 2) / LA.norm(x0, 2) <= RECOVERY_TOL
           cvx_success = LA.norm(x_cvx_recovery - x0, 2) / LA.norm(x0, 2) <= RECOVERY_TOL
           probabilities_noncvx[m.index(mm), k.index(kk)] += int(noncvx_success)
           probabilities_cvx[m.index(mm), k.index(kk)] += int(cvx_success)

probabilities_cvx /= T probabilities_noncvx /= T

We see below that the nonconvex approach is more has a higher probability of recovery than the convex approach. Left: Approach based on nonconvex optimization. Right: Approach based on convex optimization.

.. code:: python

fig, axes = plt.subplots(1, 2, figsize=(12, 5), dpi=150, sharey=True, gridspec_kw={'wspace': 0.05})

im1 = axes[0].imshow(probabilities_noncvx, interpolation="none") axes[0].set_xticks(range(len(k))); axes[0].set_xticklabels(k) axes[0].set_yticks(range(len(m))); axes[0].set_yticklabels(m) axes[0].set_xlabel("cardinality", fontsize=12) axes[0].set_ylabel("number of measurements", fontsize=12)

im2 = axes[1].imshow(probabilities_cvx, interpolation="none") axes[1].set_xticks(range(len(k))); axes[1].set_xticklabels(k) axes[1].set_yticks(range(len(m))); axes[1].set_yticklabels(m) axes[1].set_xlabel("cardinality", fontsize=12) axes[1].set_ylabel("number of measurements", fontsize=12)

Shared colorbar on top

Shared colorbar placed in a fixed figure slot (doesn't change subplot sizes)

cax = fig.add_axes([0.3, 0.94, 0.4, 0.03]) # [left, bottom, width, height] in figure coords cbar = fig.colorbar(im1, cax=cax, orientation='horizontal') cbar.ax.xaxis.set_major_formatter(PercentFormatter(xmax=1.0)) fig.subplots_adjust(top=0.85)

.. image:: sparse_signal_recovery_files/convex_vs_nonconvex.png