Compressed Sensing
Compressed sensing recovers a high-dimensional signal from far fewer
linear measurements than the Nyquist–Shannon rate when the signal is
sparse (or sparse in a known basis). SMILE’s
smile.cs
package provides sensing matrices and three recovery algorithms:
MeasurementMatrix— Gaussian, Bernoulli, or partial-identity Φ, optional wavelet ΨOMP— Orthogonal Matching Pursuit (fast greedy, known k)CoSaMP— Compressive Sampling Matching Pursuit (near-optimal greedy)BasisPursuit— L1 minimization / BPDN (convex, k optional)
With a k-sparse x ∈ ℝⁿ and measurements
y = A x where A ∈ ℝm×n and
m ≪ n, recovery is typically reliable when
m = O(k log(n/k)) (a practical rule of thumb is
m ≥ 4k–5k).
Quick Start
Build a sparse ground truth, take Gaussian measurements, and recover with OMP and CoSaMP. Relative L2 error is machine-epsilon on this noiseless instance.
import java.util.Arrays
import smile.cs.*
import smile.math.MathEx
MathEx.setSeed(19650218)
int n = 128, m = 50, k = 5
double[] xTrue = new double[n]
int[] locs = MathEx.permutate(n)
for (int i = 0; i < k; i++) {
double amp = 1.0 + MathEx.random()
xTrue[locs[i]] = MathEx.random() < 0.5 ? amp : -amp
}
var mm = MeasurementMatrix.gaussian(m, n)
double[] y = mm.measure(xTrue)
var omp = OMP.fit(mm.phi(), y, k)
Arrays.toString(omp.support())
double[] d = omp.x().clone()
for (int i = 0; i < n; i++) d[i] -= xTrue[i]
MathEx.norm(d) / MathEx.norm(xTrue)
var cosamp = CoSaMP.fit(mm.phi(), y, k)
d = cosamp.x().clone()
for (int i = 0; i < n; i++) d[i] -= xTrue[i]
MathEx.norm(d) / MathEx.norm(xTrue)
cosamp.iter()
[56, 110, 20, 34, 114]
2.59e-16
2.59e-16
1
import smile.cs.*
import smile.math.MathEx
MathEx.setSeed(19650218)
val n = 128; val m = 50; val k = 5
val xTrue = new Array[Double](n)
val locs = MathEx.permutate(n)
for (i <- 0 until k) {
val amp = 1.0 + MathEx.random()
xTrue(locs(i)) = if (MathEx.random() < 0.5) amp else -amp
}
val mm = MeasurementMatrix.gaussian(m, n)
val y = mm.measure(xTrue)
val omp = OMP.fit(mm.phi(), y, k)
omp.support().mkString("[", ", ", "]")
def relError(xRec: Array[Double], xTrue: Array[Double]): Double = {
val d = xRec.indices.map(i => xRec(i) - xTrue(i)).toArray
MathEx.norm(d) / MathEx.norm(xTrue)
}
relError(omp.x(), xTrue)
val cosamp = CoSaMP.fit(mm.phi(), y, k)
relError(cosamp.x(), xTrue)
cosamp.iter()
[56, 110, 20, 34, 114]
2.59e-16
2.59e-16
1
Measurement Matrix
MeasurementMatrix wraps the sensing operator Φ (and an
optional sparsifying wavelet Ψ). Factory methods scale entries so that
E[‖Φ x‖₂²] ≈ ‖x‖₂². Use measure /
backProject for forward and adjoint maps, and
toMatrix() for an implicit Matrix that
supports both mv and tv.
gaussian(m, n)— i.i.d.N(0, 1/m); default choice, strongest RIP guaranteesbernoulli(m, n)— ±1/√m; hardware-friendlypartial(m, n)— random row subsample of the identity (needs a basis incoherent with the canonical one)withWavelet(Wavelet)— compoundA = Φ · IDWTwithout forming Ψ explicitly
import smile.cs.*
import smile.math.MathEx
MathEx.setSeed(0)
var g = MeasurementMatrix.gaussian(30, 100)
var b = MeasurementMatrix.bernoulli(20, 64)
var p = MeasurementMatrix.partial(20, 64)
g.nrow() + " x " + g.ncol()
b.nrow() + " x " + b.ncol()
p.nrow()
// Adjoint identity: ⟨Φx, y⟩ = ⟨x, Φᵀ y⟩
double[] x = new double[100]
double[] yv = new double[30]
for (int i = 0; i < 100; i++) x[i] = MathEx.random() - 0.5
for (int i = 0; i < 30; i++) yv[i] = MathEx.random() - 0.5
double lhs = MathEx.dot(g.measure(x), yv)
double rhs = MathEx.dot(x, g.backProject(yv))
lhs
rhs
Math.abs(lhs - rhs)
30 x 100
20 x 64
20
-0.3969560959
-0.3969560959
3.33e-16
import smile.cs.*
import smile.math.MathEx
MathEx.setSeed(0)
val g = MeasurementMatrix.gaussian(30, 100)
val b = MeasurementMatrix.bernoulli(20, 64)
val p = MeasurementMatrix.partial(20, 64)
s"${g.nrow} x ${g.ncol}"
s"${b.nrow} x ${b.ncol}"
p.nrow
val x = Array.fill(100)(MathEx.random() - 0.5)
val yv = Array.fill(30)(MathEx.random() - 0.5)
val lhs = MathEx.dot(g.measure(x), yv)
val rhs = MathEx.dot(x, g.backProject(yv))
lhs
rhs
math.abs(lhs - rhs)
30 x 100
20 x 64
20
-0.3969560959
-0.3969560959
3.33e-16
Orthogonal Matching Pursuit
OMP greedily grows a support of size k: each step picks the
column of A most correlated with the residual, then
re-solves least squares on the active set (incremental QR). With a
Gaussian matrix and m ≥ C k log n, exact recovery holds
when the RIP constant satisfies
δ2k < √2 − 1.
Use OMP.fit(A, y, k) or
new OMP.Options(k, tol) for early stopping on
‖r‖₂ / ‖y‖₂. Prefer OMP when k is known and
speed matters.
CoSaMP
CoSaMP maintains a 2k-candidate support, prunes to
k, and re-solves least squares—yielding near-optimal
guarantees and better noise tolerance than plain OMP. It is robust to
moderate over-estimates of k (requires 2k < m).
Call CoSaMP.fit(A, y, k) or pass
CoSaMP.Options(k, maxIter, tol).
Basis Pursuit / BPDN
Basis Pursuit solves the convex program
minimize ‖x‖₁ subject to A x = y
(or ‖A x − y‖₂ ≤ ε for BPDN). SMILE uses a log-barrier
interior-point method with CG Newton steps. Unlike the greedy methods,
k need not be known. Prefer BP/BPDN for best quality under
noise on small-to-medium problems.
Exact BP is more reliable when m is not too small relative
to n. The example below uses a compact instance and tight
options; the residual ‖A x − y‖₂ / ‖y‖₂ shows feasibility.
import smile.cs.*
import smile.math.MathEx
import smile.tensor.Vector
MathEx.setSeed(7777)
int n = 32, m = 20, k = 3
double[] xTrue = new double[n]
int[] locs = MathEx.permutate(n)
for (int i = 0; i < k; i++)
xTrue[locs[i]] = (i + 1) * (MathEx.random() < 0.5 ? 1 : -1)
var mm = MeasurementMatrix.gaussian(m, n)
double[] y = mm.measure(xTrue)
var opts = new BasisPursuit.Options(0.0, 10.0, 1e-10, 300, 50, 1e-4)
var bp = BasisPursuit.fit(mm.phi(), y, opts)
bp.iter()
var Ax = mm.phi().mv(Vector.column(bp.x()))
double[] res = new double[m]
for (int i = 0; i < m; i++) res[i] = Ax.get(i) - y[i]
MathEx.norm(res) / MathEx.norm(y)
5
2.10e-12
import smile.cs.*
import smile.math.MathEx
import smile.tensor.Vector
MathEx.setSeed(7777)
val n = 32; val m = 20; val k = 3
val xTrue = new Array[Double](n)
val locs = MathEx.permutate(n)
for (i <- 0 until k)
xTrue(locs(i)) = (i + 1) * (if (MathEx.random() < 0.5) 1 else -1)
val mm = MeasurementMatrix.gaussian(m, n)
val y = mm.measure(xTrue)
val opts = new BasisPursuit.Options(0.0, 10.0, 1e-10, 300, 50, 1e-4)
val bp = BasisPursuit.fit(mm.phi(), y, opts)
bp.iter()
val Ax = mm.phi().mv(Vector.column(bp.x()))
val res = Array.tabulate(m)(i => Ax.get(i) - y(i))
MathEx.norm(res) / MathEx.norm(y)
5
2.10e-12
For noisy data set epsilon ≈ σ √m (expected
‖ε‖₂) in BasisPursuit.Options. OMP/CoSaMP
instead loosen tol to about the noise-to-signal ratio.
Choosing an Algorithm
- OMP — fastest greedy method; needs a sparsity estimate; best in high SNR
- CoSaMP — near-optimal greedy guarantees; tolerates over-estimated k and moderate noise
- Basis Pursuit / BPDN — convex L1; no k required; strongest under noise, slowest
Aim for m ≥ 4k at least; for high-probability recovery use roughly
m ≥ 4k log(n/k). Increase m if the relative error
stays large.
Wavelet-Domain Sparsity
Images and audio are often dense in the time/space domain but sparse
after a wavelet transform. Chain
MeasurementMatrix.gaussian(m, n).withWavelet(psi) so
solvers operate on the coefficient domain
(A = Φ · IDWT). Recover sparse coefficients with OMP, then
synthesize the signal via psi.inverse.
import java.util.Arrays
import smile.cs.*
import smile.math.MathEx
import smile.wavelet.HaarWavelet
MathEx.setSeed(19650218)
int n = 128, m = 50, k = 5
double[] s = new double[n]
int[] locs = MathEx.permutate(n)
for (int i = 0; i < k; i++) {
double amp = 1.0 + MathEx.random()
s[locs[i]] = MathEx.random() < 0.5 ? amp : -amp
}
var wm = MeasurementMatrix.gaussian(m, n).withWavelet(new HaarWavelet())
double[] y = wm.measureSparse(s)
var omp = OMP.fit(wm.toMatrix(), y, k)
Arrays.toString(omp.support())
double[] d = omp.x().clone()
for (int i = 0; i < n; i++) d[i] -= s[i]
MathEx.norm(d) / MathEx.norm(s)
[34, 110, 56, 20, 114]
2.50e-16
import smile.cs.*
import smile.math.MathEx
import smile.wavelet.HaarWavelet
MathEx.setSeed(19650218)
val n = 128; val m = 50; val k = 5
val s = new Array[Double](n)
val locs = MathEx.permutate(n)
for (i <- 0 until k) {
val amp = 1.0 + MathEx.random()
s(locs(i)) = if (MathEx.random() < 0.5) amp else -amp
}
val wm = MeasurementMatrix.gaussian(m, n).withWavelet(new HaarWavelet())
val y = wm.measureSparse(s)
val omp = OMP.fit(wm.toMatrix(), y, k)
omp.support().mkString("[", ", ", "]")
val d = omp.x().indices.map(i => omp.x()(i) - s(i)).toArray
MathEx.norm(d) / MathEx.norm(s)
[34, 110, 56, 20, 114]
2.50e-16
Haar is a good default for piecewise-constant structure;
DaubechiesWavelet(8) or higher suits smoother signals.
Signal length must be a power of two for the DWT.
Options Persistence
All Options records serialize through
java.util.Properties:
import smile.cs.*
var opts = new OMP.Options(8, 1e-8)
var props = opts.toProperties()
var restored = OMP.Options.of(props)
restored.sparsity()
restored.tol()
8
1.0E-8
import smile.cs.*
val opts = new OMP.Options(8, 1e-8)
val props = opts.toProperties()
val restored = OMP.Options.of(props)
restored.sparsity()
restored.tol()
8
1.0E-8
References
- E. J. Candès and M. B. Wakin, “An introduction to compressive sampling,” IEEE Signal Process. Mag., 25(2):21–30, 2008.
- D. L. Donoho, “Compressed sensing,” IEEE Trans. Inf. Theory, 52(4):1289–1306, 2006.
- J. A. Tropp and A. C. Gilbert, “Signal recovery from random measurements via orthogonal matching pursuit,” IEEE Trans. Inf. Theory, 53(12):4655–4666, 2007.
- D. Needell and J. A. Tropp, “CoSaMP: Iterative signal recovery from incomplete and inaccurate samples,” Appl. Comput. Harmon. Anal., 26(3):301–321, 2009.
- S. S. Chen, D. L. Donoho and M. A. Saunders, “Atomic decomposition by basis pursuit,” SIAM Rev., 43(1):129–159, 2001.