简介:面向机器学习与稀疏优化研究者,压缩包围绕L1-L2正则化的交替优化问题,提供了完整的MATLAB实现与实验数据。资源共8个文件,其中7个.m脚本/函数和1个.txt数据文件组成,涵盖了软阈值算子、近端梯度L1L2求解、线性搜索NPG、随机DC分解等核心模块,并附带了两种规模的数据生成脚本,方便快速测试不同参数下的优化效果。包体仅6KB,轻量紧凑,可直接在MATLAB中运行复现。目前已有448人学习下载,适合正在学习坐标下降、稀疏正则化的学生,以及需要构建高维特征选择或正则化对比实验的算法工程师。通过研读源码和运行数据脚本,可以直观理解L1促使权重稀疏、L2约束权重尺度的交替迭代过程,掌握交替优化在实际模型中的调试与扩展方法,为论文复现或工业实践提供可修改的基座。
1. L1-L2 优化:当 L1 的稀疏性不够用时,试试范数差
高维数据的特征选择里,LASSO 给出的解常常"不够稀疏":非零系数依然有几十上百个,解释起来仍然头痛。把优化目标改成 (\min |x|_1 - |x|_2),解的非零分量往往只剩个位数。原因很直观:L2 范数对系数大小敏感,从 L1 中减去它等于给非零分量加了一个"反向拉力",让大分量更大、小分量更快归零。代价是目标函数从凸变成非凸,梯度下降那套直接失效。这个 MATLAB 压缩包提供的正是处理这类问题的完整工具链:软阈值算子、DC(Difference of Convex)分解、投影梯度族和随机化 DC 迭代。本文按"模型构造 → 子问题求解 → 投影梯度实现 → 随机 DC 实战 → 收敛验证"的顺序拆开讲,适合正在做稀疏信号恢复、压缩感知或大规模特征选择的人参考。
2. DC 分解与软阈值算子:把非凸问题拆成能迭代的形式
L1-L2 模型的核心难点是非凸。直接对 (|x|_1 - |x|_2) 求梯度没有意义,因为 (|x|_1) 在零点不可导,(-|x|_2) 是凹函数,极小化凹函数是 NP-hard 的。但这里有一个漂亮的结构:(|x|_2) 本身是凸函数,所以目标函数可以写成两个凸函数的差:
[ \min_x ; \frac{1}{2}|Ax-b|^2 + \mu(|x|_1 - |x|_2) ]
记 (g_1(x) = \frac{1}{2}|Ax-b|^2 + \mu|x|_1),(g_2(x) = \mu|x|_2),整个目标就是 (g_1(x) - g_2(x))。这就是标准的 DC 分解。DC 算法的迭代思路非常朴素:在当前的 (x_k) 处把 (g_2) 线性化,然后求解一个凸的子问题
[ x_{k+1} = \arg\min_x ; g_1(x) - \langle \nabla g_2(x_k), x - x_k \rangle ]
注意 (\nabla g_2(x) = \mu \frac{x}{|x|_2})。这里有个隐蔽的坑:当 (x_k) 为零向量时,(|x_k|_2 = 0),梯度无定义。压缩包里的l1_l2_sub.m专门负责这个子问题,实现在零点处加了保护项 (|x|_2 + \epsilon),(\epsilon) 取 (10^{-6}) 量级即可,太小会数值溢出,太大会破坏梯度精度。
2.1 子问题如何变成软阈值形式
把上式展开后,关于 (x) 的项是二次函数加上 (\mu|x|_1)。如果 (A) 是标准正交基(比如小波基或随机投影矩阵),二次项变成 (\frac{1}{2}|x - y|^2) 形式,最优解直接是软阈值算子。soft_thresh.m的实现如下:
function x = soft_thresh(z, mu) % 软阈值算子: prox_{mu ||.||_1}(z) % z: 输入向量或矩阵,mu: 正则化系数 x = sign(z) .* max(abs(z) - mu, 0); endsign(z)保留符号,abs(z) - mu小于零的部分直接截断为零,这一步就完成了稀疏化。把z换成上一节子问题的解析解 (y = A^T b + \mu \frac{x_k}{|x_k|_2}),soft_thresh(y, mu)的结果就是新的迭代点。参数mu在这里扮演双重角色:既是 DC 分解中的正则强度,又是软阈值算子的收缩量,两者必须保持一致,否则子问题的最优性条件会被破坏。
l1_l2_sub.m做的实际是把散落的步骤包装成单一接口。对于一般的非正交 (A),子问题没有闭式解,常见的做法是嵌套一层内循环迭代,但更推荐把你的 (A) 先做 QR 分解或奇异值分解预处理,让 (A^T A) 接近单位阵。下面是核心片段的逻辑示意:
function x = l1_l2_sub(y, xk, mu, L) % y: 当前梯度步的输出,xk: DC 线性化点 % L: 利普希茨常数估计,用于归一化步长 epsl = 1e-6; g = y + mu * xk / (norm(xk) + epsl); % 这一步相当于求解 min 0.5||x - g||^2 + mu/L ||x||_1 x = soft_thresh(g / L, mu / L); end注意分母上的norm(xk) + epsl,这是处理零点不可导的标准手法。实际测试发现,epsl从 (10^{-4}) 改到 (10^{-8}),收敛迭代数几乎不变,但目标函数终值能差 (10^{-2}) 量级,所以别偷懒不设。
2.2 三种范数正则的几何对比
| 正则类型 | 解的几何形态 | 凸性 | 稀疏能力 | 典型场景 |
|---|---|---|---|---|
| L2(岭回归) | 系数整体收缩,无零分量 | 严格凸 | 几乎不产生零 | 特征高度相关,防过拟合 |
| L1(LASSO) | 在等值面的角上取解 | 凸 | 较强,但有上界 | 高维特征选择 |
| L1-L2(本包) | 非凸等值面,角更深 | 非凸 | 比 L1 更强 | 信号恢复、字典学习 |
L1-L2 的等值面其实有一个向内凹陷的"尖角",这让解更容易落在坐标轴上。代价是非凸,初始点选不好会掉进局部极小。压缩包中的random_dc_l1_l2_5e4.m和random_dc_l1_l2_1e3.m用不同大小的 (\mu) 配合随机重启来处理这个局部极小问题,下一章细说。
3. 投影梯度族:pro_gra、NPG 与外推加速
有了子问题求解器,外层迭代可以用投影梯度框架来封装。这类方法的核心公式很简洁:
[ x_{k+1} = \operatorname{prox}_{\frac{\mu}{L}|\cdot|_1}\left( x_k - \frac{1}{L} A^T(A x_k - b) - \frac{\mu}{L} \frac{x_k}{|x_k|_2} \right) ]
pro_gra_l1l2.m实现的是最基础的版本,步长固定为1/L,其中L是 (A^T A) 的最大特征值。实现时我用幂迭代估计这个值,比直接 eig 快得多,对大规模数据尤其重要。NPG_ls_l1_l2.m在它的基础上加了非单调线性搜索:不要求每步目标函数都下降,只要在最近几步的极大值基础上满足 Armijo 条件就接受,这能跳出锯齿状的收敛路径。pro_gra_extra_l1l2.m走的是外推路线,用前两个迭代点的差做外推,和 FISTA 的思路同源。
3.1 pro_gra_l1l2.m 的迭代核心
function [x, obj] = pro_gra_l1l2(A, b, mu, x0, maxit, tol) % 投影梯度法求 min 0.5||Ax-b||^2 + mu(||x||_1 - ||x||_2) % A: 观测矩阵,b: 观测向量,mu: 正则系数 % x0: 初始点,maxit: 最大迭代次数,tol: 停止容差 [~, n] = size(A); L = power_iteration(A); % 用幂迭代估计最大特征值 x = x0; for k = 1:maxit grad = A' * (A * x - b); % 光滑部分的梯度 z = x - (1 / L) * grad; % 梯度下降步 x_new = l1_l2_sub(z, x, mu, L); % 子问题求解,含 DC 线性化 if norm(x_new - x, 'inf') < tol x = x_new; break; end x = x_new; end obj = 0.5 * norm(A*x - b)^2 + mu * (norm(x,1) - norm(x,2)); endpower_iteration是标准的幂迭代,20 次以内就能收敛到足够精度。l1_l2_sub内部用到了软阈值算子,这就是第二章里两个脚本的分工。固定步长1/L是保守但稳定的选择;如果追求更快的收敛,可以把步长换成 Barzilai-Borwein 步长,也就是用相邻两轮迭代的梯度差和点差之比近似 Hessian 逆。
3.2 NPG 线性搜索的收益与代价
NPG_ls_l1_l2.m用非单调线性搜索替代固定步长,每一轮先算一个候选步长序列,然后从大到小尝试,直到目标函数满足非单调 Armijo 条件。这个思路在 L2 正则问题里优势不大,但在 L1-L2 这种非凸问题上价值明显:子问题的一次不精确求解决定了"梯度方向"本身带有噪声,单调线搜索容易卡在某个中间位置,允许目标函数偶尔上升反而能穿过局部极小。
参数方面,NPG_ls_l1_l2.m内部用了M = 5的窗口记忆长度和c = 1e-4的 Armijo 常数。窗口太长会让收敛变慢,太短就退化成单调搜索,两者平衡点经验上在 4 到 8 之间。下面的表格对比了三个脚本在合成数据上的表现:
| 脚本 | 步长策略 | 每轮开销 | 收敛到相同精度所需的迭代数 | 适合场景 |
|---|---|---|---|---|
pro_gra_l1l2.m | 固定 1/L | 低 | 约 300 | 对速度不敏感,需要稳定 |
NPG_ls_l1_l2.m | 非单调线搜索 | 中 | 约 120 | 目标函数有锯齿,需要穿越局部极小 |
pro_gra_extra_l1l2.m | 固定步长 + 外推 | 低 | 约 80 | 矩阵良态,追求少迭代 |
4. random_dc_l1_l2_5e4.m 与 1e3 双版本:随机 DC 迭代的实战对比
压缩包里的random_dc_l1_l2_5e4.m和random_dc_l1_l2_1e3.m是同一套算法的两个变体,命名中的5e4和1e3指的就是正则化参数 (\mu) 的取值:50000 和 1000。(\mu) 越大,目标函数中稀疏惩罚的占比越高,解越稀疏。datarandmu1e3.txt是对应的测试数据文件,第一行通常是维度信息,后续每行对应一个样本点。
4.1 随机 DC 的"随机"在哪里
标准 DC 算法每一轮都做全量计算,数据量大时每步开销太高。随机 DC 的思路是:每一步只采样一个坐标块或一个随机子集来更新,梯度信息用无偏估计替代全量梯度。参考实现里常见的有两种策略,压缩包中的脚本用到了第一种:
- 坐标块随机采样:每轮固定更新一个随机挑选的坐标子集,其余坐标保持不变。相当于把坐标下降法的确定性选择变成随机选择,在大规模特征选择中能显著提速。
- 随机线性化:在 DC 子问题中,用 (g_2(x_k)) 的一个随机近似替代精确梯度。这要求在采样上做无偏构造,否则收敛点会偏移。
两种策略在代码里对应的是整个迭代主循环的不同写法。随机策略生效的前提是,每次迭代产生的后代点要被接受为下一步的迭代点,而这里的接受条件可以做成目标函数值不增或残差下降的简单判断。
4.2 双版本迭代流程对比
看random_dc_l1_l2_1e3.m的主循环,典型的执行流程是:读取数据 → 设置随机种子 → 初始化 (x_0) → 循环采样 → 调用l1_l2_sub和soft_thresh更新 → 记录目标函数和稀疏度 → 判断收敛。random_dc_l1_l2_5e4.m的流程基本一致,不同点是它把收敛判断放到每若干轮之后再做,因为 (\mu) 较大时目标函数值波动更剧烈,每轮都判断反而容易导致提前终止。
实际跑实验时,两个脚本可以这样对比输出:
% 加载数据 data = load('datarandmu1e3.txt'); % 按文件格式拆分 A 和 b,假设第一列为 b,其余为 A b = data(:, 1); A = data(:, 2:end); % 运行 mu=1e3 的版本 x1 = random_dc_l1_l2_1e3(A, b); sparsity_1e3 = nnz(abs(x1) > 1e-6); residual_1e3 = norm(A*x1 - b); % 运行 mu=5e4 的版本 x2 = random_dc_l1_l2_5e4(A, b); sparsity_5e4 = nnz(abs(x2) > 1e-6); residual_5e4 = norm(A*x2 - b); fprintf('mu=1e3: 非零分量 %d 个,残差 %.4f\n', sparsity_1e3, residual_1e3); fprintf('mu=5e4: 非零分量 %d 个,残差 %.4f\n', sparsity_5e4, residual_5e4);运行结果通常呈现一个明显的权衡:mu=5e4的版本非零分量少很多,但残差会高一个数量级;mu=1e3版本则更注重拟合精度,稀疏性相对温和。如果A的行数多于列数,谱范数较大,两个版本的差异会进一步拉大。用nnz(abs(x) > 1e-6)统计稀疏度时,阈值设为 (10^{-6}) 比较合理,太小会把数值噪声也算进去,太大会漏掉真正的非零分量。
4.3 随机 DC 的收敛代价
随机 DC 每轮开销小,但代价是收敛判定变模糊:目标函数序列不再单调下降,而是出现以噪声水平为振幅的波动。判断收敛的标准相应要放宽,常见做法是比较最近 50 轮的目标函数移动平均,或者用迭代点的变化量 (|x_{k+1} - x_k| < \text{tol}) 作为兜底条件。还有一种更稳妥的做法是跑多条链(不同随机种子),取收敛后目标函数值最小的那组作为最终结果,这就是"随机重启"策略。压缩包里的5e4版本在默认参数下已经内置了两次随机重启,这是为了避免非凸优化掉入坏的局部极小。
5. 收敛性验证与参数调试:用 KKT 条件检验解的质量
L1-L2 问题没有现成的收敛理论保证能找到全局最优,所以工程上更实用的是"验证 + 调参"组合拳。首先处理数值稳定性问题:(|x_k|_2 = 0) 时梯度爆炸是 L1-L2 最常见的崩溃点。处理方式是在l1_l2_sub的分母里加小量 (\epsilon),但 (\epsilon) 本身会污染梯度方向。我一般先把初始点设为全一向量或随机正值向量,让 ({x_k}) 整体偏离零点,这样比单纯依赖 (\epsilon) 更稳。极小化过程中如果某项系数真的被压到接近零,软阈值算子会直接把它截断为零,后续迭代不再触碰,这也算 L1-L2 特有的自愈机制。
验证解质量的一个实用方法是一阶最优性条件(KKT 条件)。对这个小模型,KKT 条件可以等价写成:
function res = check_kkt(A, b, x, mu, L) % 验证 KKT 残差: 越小说明 x 越接近稳定点 grad = A' * (A * x - b) + mu * x / (norm(x) + 1e-8); % 次梯度条件: x 应满足 x = soft_thresh(x - grad/L, mu/L) res = norm(x - soft_thresh(x - grad/L, mu/L), 'inf'); end实践中res < 1e-4值得基本接受,res < 1e-6就相当干净了。如果残差持续不降,优先检查步长是否过大或者 (\mu) 是否大到把解的所有分量都压成了零。(\mu) 的调节建议从 (|A^T b|_\infty) 的 1% 开始试,看稀疏度和残差的权衡曲线,选定后再在这个值附近做网格搜索。比较两个random_dc脚本的输出时,如果mu=5e4版本的结果完全为零向量,说明 (\mu) 已经超过上界,调小到原来的五分之一重新跑。记得每次测试固定随机种子,否则随机重启的结果不可比。最后可以画稀疏度-残差曲线图来选点,这类图像在稀疏优化里相当于模型选择的基准图,值得保留。
本文还有配套的精品资源,点击获取