C++实现迭代软阈值算法:压缩感知信号重建原理与性能分析

1. 项目概述:从稀疏信号到高效重建

信号重建,或者说信号恢复,是信号处理领域一个经典又充满活力的研究方向。简单来说,我们常常面临一个困境:如何从远少于信号本身维度的、不完整的观测数据中,高精度地还原出原始信号?这个问题在医学成像(如CT、MRI)、天文观测、图像压缩、无线通信等场景中无处不在。传统方法在数据量不足时往往无能为力,但“压缩感知”理论的提出,为我们打开了一扇新的大门。它指出,只要信号本身在某个变换域(如傅里叶变换、小波变换)是“稀疏”的,即大部分系数为零或接近零,那么我们就可以用远低于奈奎斯特采样率的观测数据,近乎完美地重建信号。

这个项目的核心,就是实现压缩感知理论中一个非常经典且强大的重建算法——迭代软阈值算法。ISTA算法以其简洁的迭代形式和坚实的收敛性保证,成为了稀疏信号重建领域的基石。很多更高级的算法,如快速迭代软阈值算法、近端梯度下降法等,都是在它的思想上发展而来。因此,深入理解并用C++实现ISTA,不仅是对压缩感知理论的一次绝佳实践,更是掌握一系列优化算法的敲门砖。本文将带你从零开始,拆解ISTA的数学原理,用C++一步步实现它,并对其计算性能进行深入分析,探讨在不同稀疏度、观测维度下的表现,以及如何通过代码级优化来提升效率。无论你是信号处理方向的学生,还是对高性能数值计算感兴趣的开发者,这篇文章都将提供一份可直接复现的“实战手册”。

2. 核心算法原理与设计思路拆解

2.1 问题建模:从线性系统到稀疏约束

我们首先将信号重建问题形式化。假设我们有一个我们想要恢复的原始高维信号x(维度为n),但我们无法直接观测到它。我们只能通过一个已知的、维度更低的观测矩阵A(大小为m x n, 且m < n)对其进行线性投影,得到一个观测向量y(维度为m)。这个过程可以表示为:

y = A * x + e

其中,e代表观测过程中不可避免的噪声。我们的目标是从已知的yA中,反推出未知的x。显然,由于m < n,这是一个欠定方程组,有无数多解。压缩感知的巧妙之处在于引入了“稀疏性”先验:我们假设x本身,或者其在某个变换域Ψ下的表示θ = Ψ * x是稀疏的,即θ中只有少数几个元素非零。

为了使问题可解,我们将重建任务转化为一个优化问题:寻找一个解x,它既要尽可能好地拟合观测数据y(即A*x接近y),又要满足稀疏性约束。最常用的数学模型是LASSO或基追踪去噪问题:

min_x (1/2) * ||y - A*x||_2^2 + λ * ||Ψ*x||_1

这里,第一项(1/2) * ||y - A*x||_2^2是数据保真项,衡量重建信号与观测数据的误差,使用L2范数的平方。第二项λ * ||Ψ*x||_1是正则化项,用于促进解的稀疏性,使用L1范数。参数λ > 0用于平衡这两项:λ越大,解越稀疏,但对数据的拟合可能变差;λ越小,拟合越好,但解可能不够稀疏。L1范数之所以能诱导稀疏性,是因为它在零点不可导,其等高线是“菱形”,与L2范数的“圆形”等高线相比,更易与目标函数等值线在坐标轴(即某些分量为零)上相交。

2.2 ISTA算法:近端梯度下降的直观体现

直接求解上述含L1范数的优化问题并不容易,因为L1项不可导。迭代软阈值算法提供了一种优雅的迭代解决方案。它的核心思想可以看作是“近端梯度下降”的一个特例。

我们考虑一个更一般的问题:min_x f(x) + g(x),其中f(x)是光滑可导的(在我们的问题里是数据保真项),g(x)可能不可导但具有简单的结构(这里是L1范数)。ISTA的迭代格式为:

x^(k+1) = prox_{t*g} ( x^(k) - t * ∇f(x^(k)) )

其中,t是步长,∇ff的梯度,prox是近端算子。对于我们的L1正则化问题,f(x) = (1/2)||y-Ax||_2^2,其梯度为∇f(x) = A^T(Ax - y)。而L1范数的近端算子就是著名的软阈值函数。

因此,ISTA的一次迭代包含两个清晰步骤:

  1. 梯度下降步:沿着数据保真项梯度的反方向走一步,得到中间变量z = x^(k) - t * A^T(A*x^(k) - y)。这一步旨在减小拟合误差。
  2. 近端映射步(软阈值):对中间变量z的每一个分量应用软阈值函数S。这一步旨在施加稀疏性约束。

软阈值函数S_λ(z_i)的定义为:S_λ(z_i) = sign(z_i) * max(|z_i| - λ, 0)

这个函数非常直观:它将输入值z_i向零“收缩”。如果|z_i|小于阈值λ,则直接置为零;否则,将其绝对值减去λ,并保留原来的符号。这正是“软阈值”名称的由来,它以一种连续、可导(在非零点)的方式实现了“置零”操作,比简单的硬阈值(小于阈值置零,否则不变)在理论上性质更好。

将两步合并,我们就得到了ISTA的标准迭代公式:x^(k+1) = S_{λt} ( x^(k) - t * A^T(A*x^(k) - y) )

2.3 算法参数与收敛性考量

在实现之前,有几个关键参数需要仔细选择:

  • 步长t:为了保证算法收敛,步长t需要满足0 < t < 2 / L,其中L∇f(x)的利普希茨常数。对于我们的二次函数f(x)L等于观测矩阵A的最大奇异值的平方,即||A^T A||_2。一个保守且常用的安全选择是t = 1 / L。我们可以通过计算A^T*A的谱范数(最大特征值)来估计L,或者更简单地,在实现中采用回溯直线搜索来自适应地确定每一步的t
  • 正则化参数λλ的选择至关重要,它直接控制重建结果的稀疏度和保真度。没有绝对最优值,它依赖于噪声水平||e||_2和信号的稀疏度。一个经验法则是λ与噪声水平成正比。在实践中,常采用交叉验证或基于噪声估计的准则(如斯坦无偏风险估计)来选取。对于演示和性能分析,我们通常会测试一个λ的取值范围。
  • 停止准则:迭代何时结束?常见准则有:1) 迭代次数达到预设最大值;2) 相邻两次迭代解的变化小于某个容差,即||x^(k+1) - x^(k)||_2 / ||x^(k)||_2 < tol;3) 目标函数值下降量小于容差。通常结合使用1和2。

注意:ISTA的收敛速度是次线性的,即O(1/k)。这意味着在迭代后期,进展会变得缓慢。这是其理论性质决定的。如果需要更快的速度,可以考虑其加速版本FISTA,它通过引入一个巧妙的动量项,将收敛速度提升到O(1/k^2)。但在本文中,我们将聚焦于基础ISTA的实现与分析,理解其本质。

3. C++实现:从公式到高效代码

3.1 环境准备与核心类设计

我们将采用面向对象的方式来组织代码,这样结构更清晰,也便于后续扩展(例如,很容易修改为FISTA)。我们将主要依赖标准库和线性代数库。为了性能和多维数组操作的便利,我强烈推荐使用Eigen库。它是一个纯头文件的C++模板库,提供高性能的矩阵运算,语法接近MATLAB,非常直观。

首先,定义一个ISTAReconstructor类,它将封装所有与重建相关的参数、数据和方法。

// ISTAReconstructor.h #ifndef ISTA_RECONSTRUCTOR_H #define ISTA_RECONSTRUCTOR_H #include <Eigen/Dense> #include <functional> class ISTAReconstructor { public: using VectorXd = Eigen::VectorXd; using MatrixXd = Eigen::MatrixXd; // 构造函数:传入观测矩阵A,观测向量y ISTAReconstructor(const MatrixXd& A, const VectorXd& y); // 设置算法参数 void setLambda(double lambda); void setStepSize(double t); // 固定步长模式 void enableBacktracking(double beta = 0.5); // 启用回溯直线搜索 void setMaxIterations(int max_iter); void setTolerance(double tol); // 核心重建函数 VectorXd reconstruct(const VectorXd& x_init = VectorXd::VectorXd()); // 可指定初始解 // 获取内部状态信息 int getIterations() const { return iterations_; } const std::vector<double>& getObjectiveHistory() const { return objective_history_; } bool isConverged() const { return converged_; } private: // 输入数据 MatrixXd A_; VectorXd y_; int m_, n_; // A_的维度: m x n // 算法参数 double lambda_ = 0.1; double step_size_ = 1.0; // 初始步长,用于固定步长模式 bool use_backtracking_ = false; double backtrack_beta_ = 0.5; // 回溯收缩因子 (0,1) int max_iterations_ = 1000; double tolerance_ = 1e-6; // 内部状态 double lipschitz_constant_; // A^T*A的谱范数估计 std::vector<double> objective_history_; int iterations_ = 0; bool converged_ = false; // 核心辅助函数 double computeObjective(const VectorXd& x) const; VectorXd computeGradient(const VectorXd& x) const; void softThreshold(VectorXd& vec, double threshold) const; double backtrackingLineSearch(const VectorXd& x, const VectorXd& grad, const VectorXd& direction) const; }; #endif // ISTA_RECONSTRUCTOR_H

3.2 核心迭代过程的实现

接下来,我们实现核心的重建函数reconstruct。这里我将展示固定步长和带回溯直线搜索两种版本的关键部分。

// ISTAReconstructor.cpp (部分关键实现) #include “ISTAReconstructor.h” #include <iostream> #include <cmath> ISTAReconstructor::ISTAReconstructor(const MatrixXd& A, const VectorXd& y) : A_(A), y_(y) { m_ = A.rows(); n_ = A.cols(); if (m_ != y.size()) { throw std::invalid_argument(“Dimensions of A and y do not match!”); } // 估算利普希茨常数L = ||A^T A||_2 // 对于大型矩阵,精确计算最大特征值开销大。这里采用幂迭代法进行估计,或使用一个上界。 // 简单起见,可以先计算A^T*A的迹除以n作为初始估计的参考,但这不是严格上界。 // 更稳健的做法是在启用回溯搜索时,设置一个较大的初始步长t0,让回溯机制自动调整。 Eigen::SelfAdjointEigenSolver<MatrixXd> eigensolver(A.transpose() * A); if (eigensolver.info() != Eigen::Success) { // 如果特征分解失败(矩阵太大),采用一个启发式估计,例如 t = 1.0 lipschitz_constant_ = 1.0 / step_size_; // 假设初始step_size是合理的 std::cerr << “Warning: Could not compute Lipschitz constant exactly. Using backtracking is recommended.” << std::endl; } else { lipschitz_constant_ = eigensolver.eigenvalues().maxCoeff(); step_size_ = 0.99 * 2.0 / lipschitz_constant_; // 设置一个安全的固定步长 } } double ISTAReconstructor::computeObjective(const VectorXd& x) const { VectorXd residual = y_ - A_ * x; double fidelity = 0.5 * residual.squaredNorm(); double regularization = lambda_ * x.lpNorm<1>(); // L1 norm return fidelity + regularization; } VectorXd ISTAReconstructor::computeGradient(const VectorXd& x) const { // ∇f(x) = A^T (A x - y) return A_.transpose() * (A_ * x - y_); } void ISTAReconstructor::softThreshold(VectorXd& vec, double threshold) const { // 就地软阈值操作 for (int i = 0; i < vec.size(); ++i) { double value = vec(i); if (value > threshold) { vec(i) = value - threshold; } else if (value < -threshold) { vec(i) = value + threshold; } else { vec(i) = 0.0; } } } double ISTAReconstructor::backtrackingLineSearch(const VectorXd& x, const VectorXd& grad, const VectorXd& direction) const { double t = step_size_ * 2.0; // 从稍大的步长开始回溯 double fx = computeObjective(x); VectorXd x_new = x + t * direction; // 注意:ISTA的direction是 -gradient // 但我们的回溯条件需要计算,这里先按标准形式写,实际调用时会注意。 // 更标准的写法是专门为ISTA设计一个回溯函数,检查 surrogate optimality condition. // 简化版:回溯直到满足 f(x_new) <= f(x) + grad.dot(direction)*t + (1/(2*t))*||direction||^2 // 对于ISTA,更常用的是确保梯度步后的点经过软阈值后,目标函数下降。 // 这里实现一个简化的通用回溯: VectorXd grad_step = x - t * grad; VectorXd x_candidate = grad_step; softThreshold(x_candidate, lambda_ * t); // 对梯度步结果进行软阈值 double f_candidate = computeObjective(x_candidate); double linearized = fx + grad.dot(x_candidate - x) + (1.0/(2.0*t)) * (x_candidate - grad_step).squaredNorm(); while (f_candidate > linearized && t > 1e-14) { t *= backtrack_beta_; grad_step = x - t * grad; x_candidate = grad_step; softThreshold(x_candidate, lambda_ * t); f_candidate = computeObjective(x_candidate); linearized = fx + grad.dot(x_candidate - x) + (1.0/(2.0*t)) * (x_candidate - grad_step).squaredNorm(); } return t; } VectorXd ISTAReconstructor::reconstruct(const VectorXd& x_init) { VectorXd x; if (x_init.size() == n_) { x = x_init; } else { x = VectorXd::Zero(n_); // 默认从零向量开始 } VectorXd x_old; converged_ = false; iterations_ = 0; objective_history_.clear(); for (int k = 0; k < max_iterations_; ++k) { x_old = x; // 保存旧值用于收敛判断 objective_history_.push_back(computeObjective(x)); // 计算梯度 ∇f(x) VectorXd grad = computeGradient(x); double t_k = step_size_; // 当前迭代步长 if (use_backtracking_) { t_k = backtrackingLineSearch(x, grad, -grad); // direction = -grad } // ISTA核心迭代: x_new = S_{λt}(x - t * grad) VectorXd grad_step = x - t_k * grad; x = grad_step; // 先赋值给x,然后对x进行就地软阈值 softThreshold(x, lambda_ * t_k); iterations_++; // 检查收敛条件:相对变化小于容差 double diff_norm = (x - x_old).norm(); double x_norm = x_old.norm(); if (x_norm < 1e-12) x_norm = 1.0; // 避免除零 if (diff_norm / x_norm < tolerance_) { converged_ = true; std::cout << “ISTA converged after ” << iterations_ << “ iterations.” << std::endl; break; } } if (!converged_) { std::cout << “ISTA reached maximum iterations (” << max_iterations_ << “).” << std::endl; } objective_history_.push_back(computeObjective(x)); // 记录最终目标值 return x; }

3.3 关键实现细节与优化技巧

  1. 矩阵运算与内存Eigen库默认使用列优先存储,并且其表达式模板技术可以优化中间计算,避免不必要的临时变量。但在循环中频繁计算A_ * xA_.transpose() * residual仍然是主要开销。对于超大规模问题,需要考虑使用稀疏矩阵(Eigen::SparseMatrix)或专门的函数库,并可能利用并行计算。
  2. 软阈值函数的优化:上面实现的softThreshold函数是标量循环,对于大型向量可能不是最优的。Eigen提供了基于数组运算的向量化方式,可以写出更高效的无循环版本:
    void ISTAReconstructor::softThreshold(VectorXd& vec, double threshold) const { vec = (vec.array().abs() - threshold).max(0.0) * vec.array().sign(); }
    这个版本利用了Eigen的数组操作,通常比显式循环更快,因为它可能触发SIMD指令优化。
  3. 步长选择策略:固定步长简单,但需要估计L,估计不准可能导致收敛慢甚至发散。回溯直线搜索增加了每次迭代的计算量(需要多次计算目标函数),但保证了单调下降性和更稳健的收敛。对于不确定L的问题,建议启用回溯搜索,并将初始步长step_size_设得稍大一些(例如1.0或通过一次幂迭代粗略估计L的倒数)。
  4. 收敛判断:除了监测x的变化,还可以监测目标函数值的变化|f(x^(k+1)) - f(x^(k))| / |f(x^(k))|。有时目标函数值稳定了,解的变化可能还较大,或者反之。可以同时监控两者。
  5. 预热启动:如果需要进行多次重建(例如,扫描不同的λ值),可以将前一次重建的解作为下一次的初始值。由于解路径通常是连续的,这可以显著减少迭代次数。

4. 性能分析与实验设计

实现算法后,我们需要系统地评估其性能。性能分析主要围绕重建精度收敛速度计算效率三个维度展开。

4.1 实验数据生成与评估指标

为了进行可控的实验,我们需要模拟生成稀疏信号和观测数据。

// 生成一个长度为n,稀疏度为k的随机稀疏信号(非零位置随机,值服从高斯分布) VectorXd generateSparseSignal(int n, int k) { VectorXd x = VectorXd::Zero(n); std::vector<int> indices(n); std::iota(indices.begin(), indices.end(), 0); std::shuffle(indices.begin(), indices.end(), std::default_random_engine()); std::normal_distribution<double> dist(0.0, 1.0); std::random_device rd; std::mt19937 gen(rd()); for (int i = 0; i < k; ++i) { x(indices[i]) = dist(gen); } return x; } // 生成随机高斯观测矩阵 (m x n) MatrixXd generateGaussianMeasurementMatrix(int m, int n) { MatrixXd A = MatrixXd::Random(m, n); // 通常会对A的每一列进行归一化,使其L2范数为1,有利于数值稳定性 for (int i = 0; i < n; ++i) { A.col(i).normalize(); } return A; } // 添加高斯白噪声 VectorXd addGaussianNoise(const VectorXd& vec, double sigma) { VectorXd noisy = vec; std::normal_distribution<double> dist(0.0, sigma); std::random_device rd; std::mt19937 gen(rd()); for (int i = 0; i < vec.size(); ++i) { noisy(i) += dist(gen); } return noisy; }

评估指标

  1. 重建误差:使用相对L2误差||x_reconstructed - x_true||_2 / ||x_true||_2。这是最直接的精度度量。
  2. 信噪比改善:如果原始观测y有噪声,可以计算输入信噪比和重建信号与真实信号之间的信噪比。
  3. 支持集恢复率:对于严格稀疏信号,可以计算算法正确识别出的非零位置(支持集)的比例。这衡量了稀疏模式恢复的能力。
  4. 收敛曲线:记录每次迭代后的目标函数值或重建误差,绘制随迭代次数变化的曲线,直观观察收敛速度。
  5. 运行时间:记录算法达到收敛或最大迭代次数所需的时间。对于性能分析,需要区分不同问题规模(n,m,k)下的时间消耗。

4.2 性能测试场景设计

我们将设计以下几组实验来全面分析ISTA的性能:

实验1:稀疏度k的影响固定信号长度n=512,观测数m=256(压缩比50%),生成不同稀疏度k(如10, 20, 40, 60, 80)的信号。使用相同的λ(例如0.01 *max(abs(A^T y)),这是一个启发式初始值)和停止准则。观察随着k增大(信号越来越不稀疏),重建误差和所需迭代次数的变化。预期结果是,在k远小于m时,重建效果很好;当k接近甚至超过m时,重建会失败。

实验2:观测数量m(压缩比)的影响固定n=512,k=20,改变观测数m(如128, 192, 256, 320, 384),即压缩比从25%到75%。分析重建精度随观测数据量增加而提升的趋势。理论上存在一个相变边界,当m大于某个与kn相关的阈值时,成功重建的概率会急剧上升。

实验3:正则化参数λ的影响固定n=512,m=256,k=20,生成含噪观测(例如信噪比20dB)。在一个范围内(如1e-41,对数尺度)扫描λ。对于每个λ,运行ISTA并计算重建误差和重建信号的稀疏度(非零元素个数)。绘制“误差-λ”曲线和“稀疏度-λ”曲线。这条L形曲线能帮助我们直观地理解λ如何权衡拟合误差与稀疏性,并帮助我们选择接近拐点的λ值。

实验4:算法配置对比对比固定步长ISTA与带回溯直线搜索的ISTA。在相同问题设置下,比较两者的收敛迭代次数和总运行时间。回溯搜索通常迭代次数更少,但每次迭代成本更高。这个实验可以指导我们在实际中如何选择配置。

实验5:与基准算法对比可以与最速下降法(只优化数据保真项,无L1正则化)、正交匹配追踪等贪婪算法进行简单对比,突出L1正则化在欠定问题中诱导稀疏解的优势。

4.3 性能分析结果解读与可视化

使用像Python的Matplotlib或C++的gnuplot将实验结果可视化是关键。

  • 收敛曲线图:X轴为迭代次数,Y轴为对数坐标下的目标函数值或重建误差。可以清晰看到ISTA的次线性收敛特征——初期下降快,后期平缓。回溯搜索版本可能表现出更稳定的下降。
  • 相变图:以稀疏度k/m为X轴,观测数m/n为Y轴,用颜色表示成功重建的概率或平均误差。这张图能直观展示压缩感知的理论恢复边界。
  • 正则化路径图:展示不同λ下,解向量x中各个分量值的变化。可以看到,随着λ增大,越来越多的分量被“压缩”为零。

实操心得:在测量运行时间时,务必确保计时只包含核心迭代循环,排除数据生成和初始化时间。对于C++,可以使用<chrono>库的高精度时钟。此外,为了获得稳定的时间数据,应多次运行(例如10次)取平均,并关闭调试模式和编译器优化的一致性(通常使用-O2-O3优化等级进行性能测试)。

5. 常见问题、调试技巧与扩展方向

5.1 实现与调试中的常见坑点

  1. 算法不收敛或发散

    • 首要怀疑对象是步长t太大。检查利普希茨常数L的计算是否正确。一个快速的诊断方法是:在迭代开始时,计算t是否满足t < 2/L。如果不确定,立即启用回溯直线搜索,这是解决发散问题最有效的方法。
    • 检查梯度计算是否正确。一个常用的梯度检查方法是:对于随机点x0和随机方向d,计算函数值差分[f(x0 + h*d) - f(x0 - h*d)] / (2h)和梯度点乘∇f(x0).dot(d),当h很小时(如1e-5),两者应非常接近。
    • 观测矩阵A的条件数可能过大。尝试对A的列进行归一化,这有助于改善问题的条件数。
  2. 重建结果全零或稀疏度极高

    • 这通常是正则化参数λ设置过大的标志。λ的作用是惩罚非零项,λ太大,软阈值函数会将几乎所有分量置零。需要减小λ。可以参考λ_max = ||A^T y||_∞,当λ >= λ_max时,最优解就是零向量。因此,合理的λ通常远小于λ_max
  3. 重建结果稀疏度不够,充满很多小值

    • λ设置过小,对稀疏性的惩罚力度不够。需要增大λ。观察解的L1范数,如果它很大,而数据保真项误差很小,就是典型的欠正则化。
  4. 运行速度慢

    • 性能瓶颈几乎总是在矩阵-向量乘法A*xA^T*r上。确保你使用的是优化过的线性代数库(如Eigen,并启用编译器优化-O3 -march=native)。
    • 如果A是结构化的(例如,部分傅里叶矩阵),可以编写专门的函数来实现快速乘法,而不是构造出完整的矩阵A
    • 考虑使用更快的算法变种,如FISTA。ISTA的O(1/k)收敛在后期确实很慢。

5.2 算法扩展与变种

  1. FISTA (Fast ISTA):这是ISTA最著名的加速版本。它引入了一个额外的辅助序列y^(k),其迭代格式为:

    x^(k) = S_{λt}(y^(k) - t * ∇f(y^(k))) t_{k+1} = (1 + sqrt(1 + 4*t_k^2)) / 2 y^(k+1) = x^(k) + ((t_k - 1)/t_{k+1}) * (x^(k) - x^(k-1))

    它通过一个巧妙的动量项,将收敛速度提升到O(1/k^2),实际加速效果非常显著,通常只需ISTA十分之一不到的迭代次数。

  2. 带权重L1正则化:有时我们知道信号不同分量的稀疏先验概率不同,可以使用加权L1范数Σ w_i |x_i|,其中w_i是权重。这只需要修改软阈值函数中的阈值,将固定的λ变为λ * w_i

  3. 处理复值信号:在通信、雷达等领域,信号通常是复值的。ISTA可以推广到复数域,此时L1范数定义为||x||_1 = Σ sqrt(real(x_i)^2 + imag(x_i)^2),软阈值操作也需要作用于复数的幅度。

  4. 非凸正则化:L1范数是L0范数最紧的凸松弛,但为了获得更稀疏的解,有时会使用非凸正则化项,如Lp范数(0<p<1)。此时,近端算子不再是软阈值,而是一种广义的硬阈值/非线性收缩算子,算法会变得更复杂,但可能得到更好的稀疏恢复效果。

这个基于C++的迭代软阈值算法实现与性能分析项目,不仅让你掌握了压缩感知信号重建的一个核心工具,更带你深入理解了稀疏优化问题的求解思路。从数学公式推导,到高效的C++代码实现,再到系统性的性能评估与调试,整个过程是信号处理与数值计算交叉领域一次完整的工程实践。当你看到算法成功从少量观测中恢复出原始稀疏信号的那组曲线时,你会对“从信息中挖掘信息”这一信号处理的核心使命有更深刻的体会。试着去调整参数,观察相变,实现FISTA加速,甚至应用到一段真实的音频或图像数据上,你会发现这片天地广阔而有趣。