当数据科学家面对高维数据集时,传统线性回归往往会陷入"维度诅咒"的困境。想象一下,你手头有1000个特征,但只有100个样本——这种情况下,最小二乘法就像用渔网捞针,不仅效率低下,还容易捕捉到大量噪声。这正是LASSO回归和ISTA算法大显身手的舞台。
在机器学习实践中,我们经常遇到设计矩阵列相关性高或样本量远小于特征数的情况。这类矩阵被称为"病态矩阵",其最小二乘解对微小扰动异常敏感。
让我们用NumPy创建一个故意设计的病态系统:
python复制import numpy as np
np.random.seed(42)
# 构造病态设计矩阵
n_samples, n_features = 50, 100
X = np.random.randn(n_samples, n_features)
# 人为制造列相关性(病态的核心原因)
X[:, 10] = X[:, 9] + 0.01 * np.random.randn(n_samples)
# 生成稀疏真实解(仅5%非零)
w_true = np.zeros(n_features)
nonzero_indices = np.random.choice(n_features, 5, replace=False)
w_true[nonzero_indices] = 3 * np.random.randn(5)
# 生成观测值并添加噪声
y = X @ w_true + 0.1 * np.random.randn(n_samples)
此时若直接使用最小二乘:
python复制w_ls = np.linalg.pinv(X) @ y
print(f"LS解与真实解的误差:{np.linalg.norm(w_ls - w_true):.4f}")
典型输出可能显示误差高达300%以上,且解完全失去稀疏性。这就是我们需要ℓ₁正则化的根本原因。
LASSO问题的目标函数:
minimize ½‖Xw - y‖₂² + λ‖w‖₁
其中λ控制稀疏度强度。与岭回归(ℓ₂正则化)相比,LASSO的ℓ₁惩罚项具有选择性收缩特性:
| 正则化类型 | 解特性 | 计算复杂度 | 特征选择能力 |
|---|---|---|---|
| ℓ₂ (岭回归) | 稠密解 | O(n³) | 无 |
| ℓ₁ (LASSO) | 稀疏解 | 依赖算法 | 优秀 |
提示:当λ足够大时,LASSO会将某些系数精确压缩为零,这是特征选择的数学基础
迭代收缩阈值算法(ISTA)的核心在于将复杂的ℓ₁优化分解为简单的迭代步骤,每次迭代包含标准的梯度下降和特殊的软阈值操作。
ISTA的迭代公式:
w⁽ᵏ⁺¹⁾ = S_{λt}[w⁽ᵏ⁾ - t∇f(w⁽ᵏ⁾)]
其中:
Python实现软阈值函数:
python复制def soft_threshold(x, threshold):
return np.sign(x) * np.maximum(np.abs(x) - threshold, 0)
步长t的选择直接影响收敛速度。理论上,t应小于等于1/L,其中L = σₘₐₓ(XᵀX)是最大奇异值。实践中可采用:
python复制# 计算Lipschitz常数
L = np.linalg.norm(X.T @ X, 2) # 谱范数(最大奇异值)
step_size = 1 / L
# 或者使用更鲁棒的线搜索方法
def line_search(w, X, y, lambda_, max_iter=10):
grad = X.T @ (X @ w - y)
t = 1.0
for _ in range(max_iter):
w_new = soft_threshold(w - t * grad, t * lambda_)
if np.linalg.norm(X @ w_new - y) < np.linalg.norm(X @ w - y):
return t
t *= 0.5
return t
python复制def ista(X, y, lambda_, max_iter=1000, tol=1e-4):
n_samples, n_features = X.shape
w = np.zeros(n_features)
L = np.linalg.norm(X.T @ X, 2) # Lipschitz常数
t = 1 / L
for k in range(max_iter):
grad = X.T @ (X @ w - y) # 计算梯度
w_new = soft_threshold(w - t * grad, t * lambda_)
# 检查收敛条件
if np.linalg.norm(w_new - w) < tol:
break
w = w_new
return w
λ控制模型复杂度,选择方法包括:
网格搜索+交叉验证:
python复制from sklearn.model_selection import cross_val_score
lambdas = np.logspace(-4, 0, 20)
scores = []
for l in lambdas:
w = ista(X, y, l)
score = -cross_val_score(LinearRegression(), X, y,
scoring='neg_mean_squared_error').mean()
scores.append(score)
best_lambda = lambdas[np.argmin(scores)]
基于理论的方法:
可视化路径分析:
python复制lambda_range = np.logspace(-3, 1, 50)
coefs = []
for l in lambda_range:
coefs.append(ista(X, y, l))
coefs = np.array(coefs)
plt.figure(figsize=(10,6))
for i in range(n_features):
plt.semilogx(lambda_range, coefs[:, i])
plt.xlabel('Lambda')
plt.ylabel('Coefficient value')
plt.title('LASSO Path')
FISTA通过引入动量项将收敛速度从O(1/k)提升到O(1/k²):
python复制def fista(X, y, lambda_, max_iter=1000, tol=1e-4):
w = np.zeros(X.shape[1])
z = w.copy()
t = 1 / np.linalg.norm(X.T @ X, 2)
t_prev = 1
for k in range(max_iter):
grad = X.T @ (X @ z - y)
w_new = soft_threshold(z - t * grad, t * lambda_)
# FISTA特有的动量更新
t_new = (1 + np.sqrt(1 + 4 * t_prev**2)) / 2
z = w_new + ((t_prev - 1)/t_new) * (w_new - w)
if np.linalg.norm(w_new - w) < tol:
break
w, t_prev = w_new, t_new
return w
当特征维度极高时(如n_features > 1e5),可采用稀疏矩阵存储和计算:
python复制from scipy import sparse
# 转换为CSR格式稀疏矩阵
X_sparse = sparse.csr_matrix(X)
# 修改梯度计算为稀疏友好形式
grad = X_sparse.T @ (X_sparse @ w - y) # 自动使用稀疏矩阵乘法
虽然sklearn提供了Lasso实现,但自定义ISTA在某些场景更具优势:
| 特性 | 自定义ISTA | sklearn.Lasso |
|---|---|---|
| 算法透明度 | 完全可控 | 黑箱 |
| 超大规模数据 | 需优化实现 | 优化较好 |
| 自定义停止条件 | 灵活 | 固定 |
| 并行计算 | 需手动实现 | 自动 |
| 特征标准化 | 需手动处理 | 自动 |
实际项目中,建议先用sklearn基准测试,再针对特殊需求考虑自定义实现。