Ridge、Lasso 与因子合成
本章目录
学完后,你应能独立完成
- 推导 Ridge、特征方向收缩和库参数换算
- 推导一维 Lasso 并理解坐标下降
- 从尺度、共线和时间验证选择正则强度
前置建议:线性回归
改变一个条件,观察发生什么
浏览器内即时教学计算;Python 实验结果另列于代码区。图形与数值一起核对。
在同一目标上定义惩罚
本课程用平均平方损失 SSE/(2n),Ridge 的惩罚为 λ‖β‖²/2,截距不罚。λ 的大小只有在目标归一化与特征尺度明确后才有意义。scikit-learn Ridge 的 SSE+alpha‖β‖²约定对应 alpha=nλ;Lasso 的平均损失约定对应 alpha=λ,不能在两者之间照抄一个数。
- 中心化消去截距。
- 梯度为 X_cᵀ(X_cβ−y_c)/n+λβ。
- 令梯度为零得到线性系统。
- λ>0 时斜率目标严格凸;代码使用 solve,不显式求逆。
收缩不是给每个系数同乘一个数
设 X_c=UDVᵀ。沿第 j 个右奇异向量方向,拟合值相对 OLS 的收缩比例为 d_j²/(d_j²+nλ)。小奇异值方向更难稳定估计,因此受到更强的收缩。原始坐标中的一个系数是多个方向的叠加,其绝对值不保证随 λ 单调下降。
λ→∞ 时斜率趋零,含截距预测趋训练目标均值。Ridge 引入偏差,可能换来更大的方差下降;是否改善未来误差需要验证,并非一个普遍优于 OLS 的定理。
Lasso 的零来自次梯度条件
一维中心化后,令 s=xᵀx/n>0、c=xᵀy/n。去掉常数项,目标是 sβ²/2−cβ+λ|β|。在 β=0 处,L1 的次梯度包含一个区间;只要 |c|≤λ,零就可以满足最优条件。L2 的零处梯度只有零,通常只是连续收缩。
- β>0 时解为 (c−λ)/s,但必须满足 c>λ。
- β<0 时解为 (c+λ)/s,但必须满足 c<−λ。
- 剩余的 |c|≤λ 对应 β=0。
- 多维只有正交设计能一次逐坐标阈值;一般需反复重算其他列残差。
坐标下降与 Elastic Net
更新第 j 个系数时,先把该列旧贡献加回残差,计算它与部分残差的相关,再做软阈值,最后减去新贡献。这就把多维问题拆成条件一维问题。收敛检查可先用参数变化;更严格实现还会检查对偶间隙或 KKT 条件。
相关因子下 Lasso 可以任意选择可替代列,相同列时还可能多解。Elastic Net 加入 L2,使相关组更稳定,并在正 L2 条件下改善唯一性。稀疏不是因果识别,入选也不是可靠的选后显著性证明。
尺度与先验是建模选择
对原系数用同样惩罚时,不同单位会影响同一预测贡献的惩罚大小。训练折内标准化是一种常用的公平量纲设计,不是数学上存在解的必要条件。标准化均值、方差必须来自当前训练折。
高斯误差与高斯斜率先验下可以得到 Ridge 的 MAP;在固定方差、线性高斯条件下后验均值与 MAP 相同。Laplace 先验对应 Lasso 的 MAP,但一般不等于后验均值。先验解释需声明误差、方差与尺度条件。
在时间折中选 λ
每个开发折先用成熟过去标签拟合填补与缩放,再训练候选 λ,最后评分未来日期。窗长、因子子集和标准化方式也属于选择压力。不要看最终测试后再换 λ,然后继续称测试独立。
页面实验使用正交、标准化设计的解析解,以便看清收缩与置零。相关因子的真实路径由下方 Python 坐标下降计算;不能把这张解析图直接解释为任意因子组的路径。
从算法到一次真实运行
代码在本地 Python 中执行,HTML 内展示参考实现和本轮运行结果。请先独立完成练习,再展开完整参考文件。
def ols_fit(X, y):
"""X excludes intercept. Return intercept, slopes, rank of centered X."""
X, y = np.asarray(X, float), np.asarray(y, float)
if X.ndim != 2 or y.shape != (len(X),) or len(X) == 0:
raise ValueError("Expected nonempty X(n,p) and y(n)")
if not np.isfinite(X).all() or not np.isfinite(y).all():
raise ValueError("Resolve nonfinite data first")
mean = X.mean(axis=0)
coef, _, rank, _ = np.linalg.lstsq(X-mean, y-y.mean(), rcond=None)
return float(y.mean()-mean@coef), coef, int(rank)
def ridge_fit(X, y, lam):
"""SSE/(2n) + lam*||beta||^2/2; do not penalize intercept."""
if not np.isfinite(lam) or lam < 0:
raise ValueError("lam must be nonnegative and finite")
if lam == 0:
b, coef, _ = ols_fit(X, y)
return b, coef
X, y = np.asarray(X, float), np.asarray(y, float)
ols_fit(X, y) # Validate the input; not used as the regularized solution.
mean = X.mean(0)
xc, yc = X-mean, y-y.mean()
coef = np.linalg.solve(xc.T@xc + len(X)*lam*np.eye(X.shape[1]), xc.T@yc)
return float(y.mean()-mean@coef), coef
def soft_threshold(value, lam):
return np.sign(value)*np.maximum(np.abs(value)-lam, 0)
def lasso_fit(X, y, lam, iterations=2000, tol=1e-9):
"""Centered cyclic coordinate descent; SSE/(2n)+lam*sum(abs(beta))."""
if not np.isfinite(lam) or lam < 0:
raise ValueError("Invalid penalty")
X, y = np.asarray(X, float), np.asarray(y, float)
ols_fit(X, y)
mean, target_mean = X.mean(0), y.mean()
xc, yc = X-mean, y-target_mean
coef = np.zeros(X.shape[1]); residual = yc.copy()
scale = np.mean(xc*xc, axis=0)
converged = False
for step in range(iterations):
old = coef.copy()
for j in range(len(coef)):
residual += xc[:, j]*coef[j]
c = xc[:, j]@residual/len(X)
coef[j] = soft_threshold(c, lam)/scale[j] if scale[j] > 0 else 0
residual -= xc[:, j]*coef[j]
if np.max(np.abs(coef-old)) < tol:
converged = True
break
return float(target_mean-mean@coef), coef, converged"""Standalone synthetic teaching example. Run locally with Python, not in the HTML."""
import json
import numpy as np
import pandas as pd
from scipy.special import expit
def clean(value):
if isinstance(value, dict): return {str(k):clean(v) for k,v in value.items()}
if isinstance(value,(list,tuple,np.ndarray)): return [clean(v) for v in value]
if isinstance(value,(np.integer,)): return int(value)
if isinstance(value,(float,np.floating)): return float(value) if np.isfinite(value) else None
if isinstance(value,(np.bool_,)): return bool(value)
return value
def print_result(result):
print(json.dumps(clean(result),ensure_ascii=False,allow_nan=False))
def ols_fit(X, y):
"""X excludes intercept. Return intercept, slopes, rank of centered X."""
X, y = np.asarray(X, float), np.asarray(y, float)
if X.ndim != 2 or y.shape != (len(X),) or len(X) == 0:
raise ValueError("Expected nonempty X(n,p) and y(n)")
if not np.isfinite(X).all() or not np.isfinite(y).all():
raise ValueError("Resolve nonfinite data first")
mean = X.mean(axis=0)
coef, _, rank, _ = np.linalg.lstsq(X-mean, y-y.mean(), rcond=None)
return float(y.mean()-mean@coef), coef, int(rank)
def ridge_fit(X, y, lam):
"""SSE/(2n) + lam*||beta||^2/2; do not penalize intercept."""
if not np.isfinite(lam) or lam < 0:
raise ValueError("lam must be nonnegative and finite")
if lam == 0:
b, coef, _ = ols_fit(X, y)
return b, coef
X, y = np.asarray(X, float), np.asarray(y, float)
ols_fit(X, y) # Validate the input; not used as the regularized solution.
mean = X.mean(0)
xc, yc = X-mean, y-y.mean()
coef = np.linalg.solve(xc.T@xc + len(X)*lam*np.eye(X.shape[1]), xc.T@yc)
return float(y.mean()-mean@coef), coef
def soft_threshold(value, lam):
return np.sign(value)*np.maximum(np.abs(value)-lam, 0)
def lasso_fit(X, y, lam, iterations=2000, tol=1e-9):
"""Centered cyclic coordinate descent; SSE/(2n)+lam*sum(abs(beta))."""
if not np.isfinite(lam) or lam < 0:
raise ValueError("Invalid penalty")
X, y = np.asarray(X, float), np.asarray(y, float)
ols_fit(X, y)
mean, target_mean = X.mean(0), y.mean()
xc, yc = X-mean, y-target_mean
coef = np.zeros(X.shape[1]); residual = yc.copy()
scale = np.mean(xc*xc, axis=0)
converged = False
for step in range(iterations):
old = coef.copy()
for j in range(len(coef)):
residual += xc[:, j]*coef[j]
c = xc[:, j]@residual/len(X)
coef[j] = soft_threshold(c, lam)/scale[j] if scale[j] > 0 else 0
residual -= xc[:, j]*coef[j]
if np.max(np.abs(coef-old)) < tol:
converged = True
break
return float(target_mean-mean@coef), coef, converged
if __name__ == "__main__":
from sklearn.linear_model import Ridge, Lasso
rng = np.random.default_rng(42)
X = rng.normal(size=(60,3)); X[:,2] = .95*X[:,0]+.1*X[:,2]
X = (X-X.mean(0))/X.std(0)
y = 2 + X@np.array([1.5,0.,.5])+rng.normal(0,.2,len(X))
lam=.05
b,w = ridge_fit(X,y,lam)
reference = Ridge(alpha=len(X)*lam).fit(X,y)
np.testing.assert_allclose(w,reference.coef_,atol=1e-10)
bl,wl,converged = lasso_fit(X,y,lam)
lasso = Lasso(alpha=lam,tol=1e-10,max_iter=10000).fit(X,y)
np.testing.assert_allclose(wl,lasso.coef_,atol=1e-7)
print_result({"lambda":lam,"ridge_alpha":len(X)*lam,"ridge_coef":w,
"lasso_coef":wl,"lasso_converged":converged,
"ridge_reference_max_error":abs(w-reference.coef_).max(),
"lasso_reference_max_error":abs(wl-lasso.coef_).max()})
查看本轮本地运行记录 · 2026-10-12
{
"lambda": 0.05,
"ridge_alpha": 3.0,
"ridge_coef": [
1.0348052927178804,
-0.004242126423485157,
0.9766647161195813
],
"lasso_coef": [
1.372627212578412,
0.0,
0.6391351448868879
],
"lasso_converged": true,
"ridge_reference_max_error": 2.6645352591003757e-15,
"lasso_reference_max_error": 9.572640990995751e-08
}先回答,再看提示和答案
n=80、λ=.1,Ridge 的 alpha 应是多少?
一级提示
把整式乘 2n。
参考答案与条件
8。这里目标采用 SSE/(2n)+λ‖β‖²/2;换了目标约定要重新推。
能把 OLS 系数一次软阈值当作一般 Lasso 解吗?
一级提示
先写出目标、信息集和所需条件。
参考答案与条件
一般不能。列不正交时每个坐标依赖其他列残差,需要迭代优化。
Ridge 的每个原坐标系数都会单调收缩吗?
一级提示
先写出目标、信息集和所需条件。
参考答案与条件
不保证。收缩在奇异向量方向具有清晰比例,原坐标是方向混合,可能非单调。
把知识迁移到一个新任务
实现 Ridge 与 Lasso 坐标下降;分别用常数列、重复列、不同单位的列测试。比较 coef 的稳定性和同一留出集合的误差,不凭一次系数入选宣布因子有效。
这里保存学习笔记,不在浏览器执行 Python。A/B/C 由你按独立完成程度填写。
参考与进一步阅读
在线文档可能比本地固定版本更新;API 行为以已验证代码和版本记录为准。
掌握状态与复习
关掉资料独立完成后记 A;提示后完成记 B;需要重学记 C。
记录尽量保存在当前浏览器,建议定期导出备份。