使用 `glmnet` 的岭回归给出的系数与我通过 "textbook definition" 计算的系数不同?

Ridge regression with `glmnet` gives different coefficients than what I compute by "textbook definition"?

我是 运行 使用 glmnet R 包进行岭回归。我注意到我从 glmnet::glmnet 函数获得的系数与我通过定义计算系数(使用相同的 lambda 值)获得的系数不同。有人可以解释一下为什么吗?

数据(两者:响应 Y 和设计矩阵 X)被缩放。

library(MASS)
library(glmnet)

# Data dimensions
p.tmp <- 100
n.tmp <- 100

# Data objects
set.seed(1)
X <- scale(mvrnorm(n.tmp, mu = rep(0, p.tmp), Sigma = diag(p.tmp)))
beta <- rep(0, p.tmp)
beta[sample(1:p.tmp, 10, replace = FALSE)] <- 10
Y.true <- X %*% beta
Y <- scale(Y.true + matrix(rnorm(n.tmp))) # Y.true + Gaussian noise

# Run glmnet 
ridge.fit.cv <- cv.glmnet(X, Y, alpha = 0)
ridge.fit.lambda <- ridge.fit.cv$lambda.1se

# Extract coefficient values for lambda.1se (without intercept)
ridge.coef <- (coef(ridge.fit.cv, s = ridge.fit.lambda))[2:(p.tmp+1)]

# Get coefficients "by definition"
ridge.coef.DEF <- solve(t(X) %*% X + ridge.fit.lambda * diag(p.tmp)) %*% t(X) %*% Y

# Plot estimates
plot(ridge.coef, type = "l", ylim = range(c(ridge.coef, ridge.coef.DEF)),
     main = "black: Ridge `glmnet`\nred: Ridge by definition")
lines(ridge.coef.DEF, col = "red")

如果你阅读?glmnet,你会看到高斯响应的惩罚objective函数是:

1/2 * RSS / nobs + lambda * penalty

如果使用脊线惩罚1/2 * ||beta_j||_2^2,我们有

1/2 * RSS / nobs + 1/2 * lambda * ||beta_j||_2^2

成正比
RSS + lambda * nobs * ||beta_j||_2^2

这和我们平时在教科书上看到的岭回归是不一样的:

RSS + lambda * ||beta_j||_2^2

你写的公式:

##solve(t(X) %*% X + ridge.fit.lambda * diag(p.tmp)) %*% t(X) %*% Y
drop(solve(crossprod(X) + diag(ridge.fit.lambda, p.tmp), crossprod(X, Y)))

为课本成绩;对于 glmnet 我们应该期望:

##solve(t(X) %*% X + n.tmp * ridge.fit.lambda * diag(p.tmp)) %*% t(X) %*% Y
drop(solve(crossprod(X) + diag(n.tmp * ridge.fit.lambda, p.tmp), crossprod(X, Y)))

因此,教科书使用惩罚最小二乘法,但glmnet使用惩罚均方误差

请注意,我没有将您的原始代码用于 t()"%*%"solve(A) %*% b;使用 crossprodsolve(A, b) 效率更高!请参阅最后的 跟进 部分。


现在我们来做一个新的比较:

library(MASS)
library(glmnet)

# Data dimensions
p.tmp <- 100
n.tmp <- 100

# Data objects
set.seed(1)
X <- scale(mvrnorm(n.tmp, mu = rep(0, p.tmp), Sigma = diag(p.tmp)))
beta <- rep(0, p.tmp)
beta[sample(1:p.tmp, 10, replace = FALSE)] <- 10
Y.true <- X %*% beta
Y <- scale(Y.true + matrix(rnorm(n.tmp)))

# Run glmnet 
ridge.fit.cv <- cv.glmnet(X, Y, alpha = 0, intercept = FALSE)
ridge.fit.lambda <- ridge.fit.cv$lambda.1se

# Extract coefficient values for lambda.1se (without intercept)
ridge.coef <- (coef(ridge.fit.cv, s = ridge.fit.lambda))[-1]

# Get coefficients "by definition"
ridge.coef.DEF <- drop(solve(crossprod(X) + diag(n.tmp * ridge.fit.lambda, p.tmp), crossprod(X, Y)))

# Plot estimates
plot(ridge.coef, type = "l", ylim = range(c(ridge.coef, ridge.coef.DEF)),
     main = "black: Ridge `glmnet`\nred: Ridge by definition")
lines(ridge.coef.DEF, col = "red")

请注意,我在调用cv.glmnet(或glmnet)时设置了intercept = FALSE。这比它在实践中的影响具有更多的概念意义。从概念上讲,我们的教科书计算没有截距,因此我们希望在使用 glmnet 时删除截距。但实际上,由于您的 XY 是标准化的,因此截距的理论估计值为 0。即使使用 intercepte = TRUEglment 默认值),您也可以检查估计值截距为 ~e-17(数值为 0),因此其他系数的估计不会受到显着影响。另一个答案只是显示这个。


跟进

As for the using crossprod and solve(A, b) - interesting! Do you by chance have any reference to simulation comparison for that?

t(X) %*% Y会先转置X1 <- t(X),然后X1 %*% Y,而crossprod(X, Y)不会转置。 "%*%"DGEMM 的包装,用于 op(A) = A, op(B) = B,而 crossprodop(A) = A', op(B) = B 的包装。同样 tcrossprod 用于 op(A) = A, op(B) = B'.

crossprod(X) 的主要用途是 t(X) %*% X;类似地,tcrossprod(X) 对应 X %*% t(X),在这种情况下,DSYRK instead of DGEMM is called. You can read the first section of 是出于原因和基准。

请注意,如果 X 不是方阵,则 crossprod(X)tcrossprod(X) 的速度并不相同,因为它们涉及不同数量的浮点运算,您可以阅读

的边注

关于solvel(A, b)solve(A) %*% b,请阅读

的第一节

加上哲园的有趣post,又做了一些实验,看看用截距也能得到同样的结果,如下:

# ridge with intercept glmnet
ridge.fit.cv.int <- cv.glmnet(X, Y, alpha = 0, intercept = TRUE, family="gaussian")
ridge.fit.lambda.int <- ridge.fit.cv.int$lambda.1se
ridge.coef.with.int <- as.vector(as.matrix(coef(ridge.fit.cv.int, s = ridge.fit.lambda.int)))

# ridge with intercept definition, use the same lambda obtained with cv from glmnet
X.with.int <- cbind(1, X)
ridge.coef.DEF.with.int <- drop(solve(crossprod(X.with.int) + ridge.fit.lambda.int * diag(n.tmp, p.tmp+1), crossprod(X.with.int, Y)))

ggplot() + geom_point(aes(ridge.coef.with.int, ridge.coef.DEF.with.int))

# comupte residuals
RSS.fit.cv.int <- sum((Y.true - predict(ridge.fit.cv.int, newx=X))^2) # predict adds inter
RSS.DEF.int <- sum((Y.true - X.with.int %*% ridge.coef.DEF.with.int)^2)

RSS.fit.cv.int
[1] 110059.9
RSS.DEF.int
[1] 110063.4