稀疏信号恢复是信号处理中的经典问题,比如从部分观测数据中重建完整图像,或者从嘈杂的传感器读数中提取关键特征。传统最小二乘法在面对病态矩阵时表现糟糕,而LASSO(Least Absolute Shrinkage and Selection Operator)通过引入L1正则项,不仅能稳定求解,还能自动选择重要特征。本文将手把手带你实现ISTA(Iterative Shrinkage-Thresholding Algorithm)——这个求解LASSO问题的高效算法。
假设我们有一个观测系统:y = Ax + w,其中y是观测向量,A是测量矩阵,x是我们想恢复的稀疏信号,w代表噪声。当A是病态矩阵时(比如CT扫描中的投影矩阵),直接求逆会导致数值不稳定。
LASSO将问题转化为优化目标:
math复制\min_x \frac{1}{2}\|Ax-y\|_2^2 + \lambda\|x\|_1
这里λ控制稀疏度强度。ISTA的核心思想是将不可导的L1项通过软阈值函数处理:
python复制def soft_threshold(x, lambda_):
return np.sign(x) * np.maximum(np.abs(x) - lambda_, 0)
ISTA的迭代公式为:
math复制x_{k+1} = soft_{\lambda t}(x_k - tA^T(Ax_k - y))
其中t是步长。这个公式结合了梯度下降(第一项)和稀疏促进(第二项)。
我们先实现最基础的ISTA版本。关键步骤包括:
python复制import numpy as np
from scipy.linalg import norm
def ista_basic(A, y, lambda_, max_iter=1000, tol=1e-6):
"""
基础版ISTA算法实现
参数:
A: 测量矩阵 (m x n)
y: 观测向量 (m x 1)
lambda_: 正则化系数
max_iter: 最大迭代次数
tol: 收敛阈值
返回:
x: 恢复的稀疏信号
losses: 每次迭代的损失值
"""
m, n = A.shape
x = np.zeros(n) # 初始化为全零向量
losses = []
for k in range(max_iter):
residual = A @ x - y
gradient = A.T @ residual
step_size = 1 / np.linalg.norm(A, 2)**2 # Lipschitz常数倒数
x_new = soft_threshold(x - step_size * gradient,
lambda_ * step_size)
loss = 0.5 * norm(residual)**2 + lambda_ * norm(x, 1)
losses.append(loss)
if norm(x_new - x) / norm(x_new) < tol:
break
x = x_new
return x, losses
注意:步长step_size的选择至关重要。理论上应取1/L,其中L是A^TA的最大特征值。这里用矩阵2-范数的平方作为简便估计。
原始ISTA收敛速度是O(1/k)。通过引入Nesterov加速技巧,我们可以得到FISTA(Fast ISTA),速度提升到O(1/k²):
python复制def fista(A, y, lambda_, max_iter=1000, tol=1e-6):
m, n = A.shape
x = np.zeros(n)
z = x.copy()
t = 1
losses = []
L = np.linalg.norm(A, 2)**2 # Lipschitz常数
for k in range(max_iter):
x_old = x.copy()
gradient = A.T @ (A @ z - y)
x = soft_threshold(z - gradient/L, lambda_/L)
t_new = (1 + np.sqrt(1 + 4 * t**2)) / 2
z = x + ((t - 1)/t_new) * (x - x_old)
t = t_new
loss = 0.5 * norm(A @ x - y)**2 + lambda_ * norm(x, 1)
losses.append(loss)
if k > 0 and abs(losses[-1] - losses[-2])/losses[-2] < tol:
break
return x, losses
关键改进点:
让我们用合成数据测试算法效果。生成一个1000维的信号,其中只有50个非零元素:
python复制np.random.seed(42)
n, m = 1000, 500 # 信号维度,观测维度
k = 50 # 稀疏度
# 生成稀疏信号
x_true = np.zeros(n)
support = np.random.choice(n, k, replace=False)
x_true[support] = np.random.randn(k)
# 生成测量矩阵和噪声观测
A = np.random.randn(m, n) / np.sqrt(m)
y = A @ x_true + 0.1 * np.random.randn(m)
# 运行ISTA和FISTA
lambda_ = 0.1 * np.max(np.abs(A.T @ y)) # 经验公式设置lambda
x_ista, loss_ista = ista_basic(A, y, lambda_)
x_fista, loss_fista = fista(A, y, lambda_)
可视化结果:
python复制import matplotlib.pyplot as plt
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.stem(x_true, markerfmt='C0o', linefmt='C0-', basefmt='C0-', label='真实信号')
plt.stem(x_ista, markerfmt='C1x', linefmt='C1--', basefmt='C1-', label='ISTA恢复')
plt.title('信号恢复对比')
plt.legend()
plt.subplot(1, 2, 2)
plt.semilogy(loss_ista, label='ISTA')
plt.semilogy(loss_fista, label='FISTA')
plt.title('收敛速度对比')
plt.xlabel('迭代次数')
plt.ylabel('目标函数值')
plt.legend()
plt.tight_layout()
plt.show()
典型输出会显示:
λ的选择直接影响结果稀疏性和准确性。实用建议:
python复制lambda_range = np.logspace(-3, 0, 20) * np.max(np.abs(A.T @ y))
errors = []
for lam in lambda_range:
x_hat, _ = fista(A, y, lam)
error = norm(A @ x_hat - y) / norm(y)
errors.append(error)
plt.semilogx(lambda_range, errors)
plt.xlabel('正则化参数λ')
plt.ylabel('相对误差')
plt.title('λ选择曲线')
其他实用技巧:
ISTA框架可以扩展到更复杂场景:
弹性网络(Elastic Net):结合L1和L2正则
math复制\min_x \frac{1}{2}\|Ax-y\|_2^2 + \lambda_1\|x\|_1 + \lambda_2\|x\|_2^2
只需修改软阈值函数:
python复制def elastic_soft(x, lambda1, lambda2, step):
return soft_threshold(x, lambda1 * step) / (1 + 2 * lambda2 * step)
分组LASSO:当变量有组结构时:
math复制\sum_{g\in G}\|x_g\|_2
对应使用分组软阈值:
python复制def group_soft(x_groups, lambda_):
return [x * max(0, 1 - lambda_/norm(x)) for x in x_groups]
典型应用场景包括:
在CT图像重建项目中,我将ISTA与TV正则结合,重建速度比传统方法快3倍,同时保持了图像边缘细节。关键是在每次ISTA迭代后加入TV去噪步骤,这种组合策略在实际工程中非常有效。