ℓ1 没有闭式解,但有两条完全不同的路:坐标下降 400 轮、近端梯度 800 步,参数差 5.29e-10。 近端梯度的收敛过程:10 步距离 0.8170,50 步 0.0450,200 步 0.0005,800 步 0.0000。 两种算法都靠同一个动作:软阈值。它是整个稀疏学习的核心算子。
5.1 建立了「ℓ1 会把系数压成零」这件事,但没给算法。 这一节补上:一个一维子问题的闭式解,加上两种把它串起来的策略。 为什么值得单独一节?因为在工程上,选哪个解法决定了你的代码能不能跑在真实规模上。 坐标下降适合稀疏、可并行、实现简单;近端梯度适合稠密、批量、易加约束。
一、为什么不能直接梯度下降
ℓ1 项在零点不可导,梯度下降在那里没有定义。 绕过它的办法有两条:用次梯度(收敛很慢,而且不会真的到零)、或者换一种更新方式。 次梯度法的问题是「抖」:它在零点附近来回振荡,永远停不下来。 你会得到一堆 1e-5 量级的小数,而不是精确的零——那就白做了稀疏。 所以真正的算法必须让「归零」成为一个明确的操作,而不是极限行为。 软阈值就是这样一个操作:它显式地输出 0。
二、坐标下降:一次只解一个坐标
思路:固定其余所有系数,只优化第 j 个。 这时候目标函数变成一个一维问题,而且它有闭式解——就是软阈值。 推导的关键一步:把残差里第 j 列的贡献单独拎出来,剩下的部分当成常数。 一维问题写成「二分之一乘列平方和乘 (w 减 rho 除以平方和) 的平方,加上 λ 乘 |w|」, 它的最小值就是 soft(rho, λ) 除以列平方和。 这个式子里 rho 是「第 j 列与当前残差的内积」,列平方和是固定的、可以预计算。 所以每次更新只花一次内积的时间。 50 个坐标扫一遍叫一轮,Demo 跑了 400 轮。 实现上还有一个技巧:增量维护残差。 更新第 j 个系数前把它旧值的影响加回残差,更新后再减掉新值的影响。 这样每步是 O(n) 而不是 O(nb),快一个数量级。 很多人写坐标下降慢,就是漏了这一步。 坐标下降的另一个好处是天然适合稀疏。 大量系数是零的时候,加回再减掉的影响都是零,等于白算——可以跳过。 所以它在「n 大、b 大、解很稀疏」的场景里几乎是最快的选择。
三、近端梯度:走一步,再掐一次
思路:把目标拆成两部分——光滑的残差平方和、不光滑的 ℓ1。 对光滑那部分走一步梯度下降,然后对结果做软阈值,把不光滑那部分「投影」回去。 这叫近端梯度,ℓ1 的版本有个专门名字叫 ISTA。 步长不用试:取 1 除以 A 的最大奇异值的平方,这是理论上保证收敛的上界。 这一点比朴素梯度下降省事得多——后者要试步长,试错了就发散。 收敛速度是 O(1/k),比坐标下降慢,但它每步只做两次矩阵乘,向量化程度极高。 所以「b 很大、机器有 BLAS 加速」时它反而更快。 还有个加速版叫 FISTA,加一个动量项,把 O(1/k) 提到 O(1/k 平方),代价是多存一组旧值。 两种算法最后落在同一点,这本身就是一个重要的事实。 它证明「ℓ1 最小二乘的解是唯一的」——虽然不可导,但它是严格凸的(当 A 列满秩时), 所以不管走哪条路,终点只有一个。
四、实测:两条路,一个终点
| 近端梯度步数 | 与坐标下降解的距离 | 训练 MSE |
|---|---|---|
| 10 | 0.8170 | 0.240 |
| 50 | 0.0450 | 0.036 |
| 200 | 0.0005 | 0.035 |
| 800 | 0.0000 | 0.035 |
| 看这张表的收敛节奏:10 步还很远,50 步已经八九不离十,200 步就稳了。 | ||
| 从 50 步到 200 步,距离掉了两个数量级,训练误差只动了 0.001。 | ||
| 这说明「参数的精度」和「预测的精度」不是一回事——预测对参数误差不敏感。 | ||
| 所以实践中不必追求 1e-10 的参数精度,1e-4 往往就够了。 | ||
| 反过来,如果你要的是「选对了哪些变量」, | ||
| 那就得很准——因为边界上的系数会在 1e-8 和 0 之间反复横跳,非零个数会跳变。 | ||
| 两种算法的参数差 5.29e-10,训练误差差 0.000000。 | ||
| 这个结果值得多想一层:它不是「巧合」,而是「凸问题的解唯一」的直接体现。 |
五、该用哪个
坐标下降:b 在万级以内、解稀疏、要自己加各种约束(比如非负、分组)。 它的循环是纯 Python 的话会慢,但每一步都极便宜,而且容易加「某些系数不许动」这类限制。 近端梯度:b 很大、要 GPU 或 BLAS、要批量处理多个 λ。 它的每一步是两次矩阵乘,天然向量化; 而且「热启动」很方便——上一个 λ 的解直接当这一个 λ 的初值,整条路径一次扫完。 两件事都别忘:先标准化,再求解。 ℓ1 罚的是绝对值之和,量纲大的特征天然吃亏少、量纲小的被压得狠。 不标准化直接用,等于给每个特征悄悄设了不同的 λ。
六、动手 Demo:两种解法:坐标下降 400 轮、近端梯度 800 步,参数差 5.3e-10
同一份 60 点、50 维的数据,λ 固定为 1.0。 一路用坐标下降跑 400 轮,另一路用近端梯度跑 800 步,比较两者的解、训练误差,并记录近端梯度在 10、 50、200、800 步时的位置。
L = float(np.linalg.norm(_X, 2) ** 2) # 步长 = 1 / 最大奇异值的平方
lam = 1.0
w = np.zeros(_D)
for k in range(800):
g = _X.T @ (_X @ w - _Y) # 光滑那部分的梯度
w = soft(w - g / L, lam / L) # 走一步,再做软阈值
实跑输出:
--------------------------------------------------------------------
两种解法:坐标下降 400 轮、近端梯度 800 步,参数差 5.3e-10
--------------------------------------------------------------------
问题 l1 约束最小二乘,λ 等于 1.0
坐标下降 每轮扫 50 个系数,每个都解一个一维软阈值问题
近端梯度 先走一步梯度,再对结果做软阈值
步长 近端梯度取 1 除以 A 的最大奇异值平方,不用试
两者参数差 5.29e-10
近端梯度步数 与坐标下降解的距离 训练 MSE
-------------------------------------------
10 0.8170 0.240
50 0.0450 0.036
200 0.0005 0.035
800 0.0000 0.035
**两种完全不同的算法,最后的参数差 5.29e-10,训练误差差 0.000000。**
**这说明 l1 最小二乘虽然是凸的、有唯一最优,但它是不可导的,所以没有闭式解。**
**「没有闭式解」不等于「解不唯一」——这是两件事,别混。**
**坐标下降的关键动作是那个软阈值:|rho| 小于 λ 就直接归零,大于就减掉 λ。**
**这一步是整个稀疏学习的核心算子,后面所有 l1 方法都在反复用它。**
**近端梯度是另一个思路:把「梯度下降」和「软阈值」拆成两步交替做。**
**它的步长可以按最大奇异值算出来,不需要试——这一点比朴素梯度下降省事。**
**两者的差别在工程上:坐标下降每次只动一个坐标,天然适合稀疏(大量系数为 0,跳过);近端梯度每次动全部坐标,适合大规模稠密问题。**
要说明:**坐标下降我跑了 400 轮,近端梯度 800 步,都是固定次数、不是跑到收敛。**
**所以两者差 5.3e-10 说明「都到位了」,不说明「谁更快」。**
近端梯度只有两行,而坐标下降有十行。
这是两者最直观的差别:近端梯度的逻辑简单到几乎不需要解释,代价是收敛慢一些。
L 为什么要用最大奇异值的平方?
因为梯度 Lipschitz 常数就是它。
步长取 1 除以它,能保证每步都不越过最优点——这是收敛的理论保证,不是经验值。
用 np.linalg.norm(_X, 2) 算谱范数,内部走的是 SVD,比手算特征值稳。
软阈值的阈值是 λ 除以 L,不是 λ。
因为这一步的更新量被 L 除过了,罚项也得跟着除。
写成 soft(w - g / L, lam / L) 是最不容易错的形式——两个除同时出现,一眼能看出配对。
注意 10 步时距离还有 0.8170,说明从零开始的头几步基本没用。
这不是算法差,是因为初始点在全零,梯度方向把所有权重一起往上推,而软阈值又把大部分摁回去,
两股力打架。
热启动(用上一个 λ 的解当初始点)能把这个阶段直接跳过。
两者参数差 5.29e-10 这个数,其实还受「400 轮」和「800 步」这两个上限的限制。
如果两个都跑到机器精度,差距会更小。
反过来说:它们在这里已经都收敛了,再跑也没有意义。
要说明:500 维以上的场景我才建议用近端梯度,50 维用坐标下降更省事。
所以这个 Demo 的规模其实偏小,它的目的是「验证两条路通向同一点」,不是「比谁快」。
30 秒实验
- 把近端梯度的步数从 800 改成 20,再看参数差。20 步的解,非零个数和 800 步的一样吗?
- 给坐标下降加一个「热启动」:用 λ 等于 2 的解当 λ 等于 1 的初值,看轮数能省多少。 这解释了为什么 lasso 的整条路径比单个 λ 快得多。
- 把 λ 改成 0.01(几乎无罚),再比两种算法。它们的差距会变大还是变小?为什么?
常见误区
- 「没有闭式解就是解不唯一」。 错。ℓ1 最小二乘是凸的,列满秩时还是严格凸的,解唯一。 「不可导」影响的是「怎么算」,不是「算出来是什么」。 Demo 里两种算法的参数差 1e-10,就是解唯一的直接证据。
- 「坐标下降一次只动一个变量,肯定收敛很慢」。 不一定。 它每一步都取到该坐标上的精确最优,所以「扫一轮」的进展往往比梯度下降「走一步」大得多。 在稀疏问题里,它经常是最快的。
- 「步长随便设,跑久一点总能收敛」。 近端梯度的步长有理论上界,超过就发散。 坐标下降不需要步长,这是它的一大优势——少一个要调的东西。
- 「先不标准化,反正 l1 会自己挑」。 不标准化等于给不同量纲的特征设了不同的有效 λ,选出来的变量会系统性偏向「数值大」的那些。 这是纯伪影,不是信号。
自测三问(点开对答案)
1. 坐标下降为什么每次只更新一个系数还能收敛到全局最优? —— **因为目标函数是凸的,而且「固定其余坐标、只优化一个」这个子问题有精确解。** **每步都会让目标函数不增,而凸函数在坐标方向上反复取最优,一定会到达全局最优。** **这个结论叫坐标下降的收敛性,前提是目标函数可分离得足够好。** 2. 近端梯度的步长为什么可以「算出来」而不是试出来? —— **因为光滑部分的梯度是 Lipschitz 连续的,常数就是 A 的最大奇异值的平方。** **步长取 1 除以这个常数,能保证每步的二次上界成立,从而单调下降。** **这是理论保证,不是调出来的经验值。** 3. 如果 b 是 100 万维,你会选哪个算法? —— **近端梯度(或者它的加速版 FISTA),甚至随机版本的 SGD。** **因为它的每步都是向量运算,能吃满 BLAS;** **而坐标下降要循环 100 万次,纯 Python 会慢到不可用。** **不过在 100 万维里解通常极稀疏,坐标下降如果只扫非零坐标,** **反而可能更快——所以在工程上两条路都要试。**算法有了,接下来问:这些被选中的变量,有多少是真的?下一节把「选变量」这件事量化:精确率、 召回率、以及和相关系数法的对比。
- ☐ (本节完,下一节 5.3 5.3)