Como dice el título, estoy tratando de replicar los resultados de glmnet linear usando el optimizador LBFGS de la biblioteca lbfgs
. Este optimizador nos permite añadir un término regularizador L1 sin tener que preocuparnos por la diferenciabilidad, siempre que nuestra función objetivo (sin el término regularizador L1) sea convexa.
El problema de regresión lineal de la red elástica en el papel glmnet viene dada por $$\min_{\beta \in \mathbb{R}^p} \frac{1}{2n}\Vert \beta_0 + X\beta - y \Vert_2^2 + \alpha \lambda \Vert \beta\Vert_1 + \frac{1}{2}(1-\alpha)\lambda\Vert\beta\Vert^2_2$$ donde $X \in \mathbb{R}^{n \times p}$ es la matriz de diseño, $y \in \mathbb{R}^p$ es el vector de observaciones, $\alpha \in [0,1]$ es el parámetro de la red elástica, y $\lambda > 0$ es el parámetro de regularización. El operador $\Vert x \Vert_p $ denota la norma Lp habitual.
El siguiente código define la función y luego incluye una prueba para comparar los resultados. Como puede ver, los resultados son aceptables cuando alpha = 1
pero están muy lejos para valores de alpha < 1.
El error empeora a medida que pasamos de alpha = 1
a alpha = 0
como muestra el siguiente gráfico (la "métrica de comparación" es la distancia euclidiana media entre las estimaciones de los parámetros de glmnet y lbfgs para una ruta de regularización determinada).
Bien, aquí está el código. He añadido comentarios siempre que ha sido posible. Mi pregunta es: ¿Por qué mis resultados son diferentes a los de glmnet
para los valores de alpha < 1
? Está claro que tiene que ver con el término de regularización L2, pero por lo que veo, he implementado este término exactamente como en el artículo. Cualquier ayuda será muy apreciada.
library(lbfgs)
linreg_lbfgs <- function(X, y, alpha = 1, scale = TRUE, lambda) {
p <- ncol(X) + 1; n <- nrow(X); nlambda <- length(lambda)
# Scale design matrix
if (scale) {
means <- colMeans(X)
sds <- apply(X, 2, sd)
sX <- (X - tcrossprod(rep(1,n), means) ) / tcrossprod(rep(1,n), sds)
} else {
means <- rep(0,p-1)
sds <- rep(1,p-1)
sX <- X
}
X_ <- cbind(1, sX)
# loss function for ridge regression (Sum of squared errors plus l2 penalty)
SSE <- function(Beta, X, y, lambda0, alpha) {
1/2 * (sum((X%*%Beta - y)^2) / length(y)) +
1/2 * (1 - alpha) * lambda0 * sum(Beta[2:length(Beta)]^2)
# l2 regularization (note intercept is excluded)
}
# loss function gradient
SSE_gr <- function(Beta, X, y, lambda0, alpha) {
colSums(tcrossprod(X%*%Beta - y, rep(1,ncol(X))) *X) / length(y) + # SSE grad
(1-alpha) * lambda0 * c(0, Beta[2:length(Beta)]) # l2 reg grad
}
# matrix of parameters
Betamat_scaled <- matrix(nrow=p, ncol = nlambda)
# initial value for Beta
Beta_init <- c(mean(y), rep(0,p-1))
# parameter estimate for max lambda
Betamat_scaled[,1] <- lbfgs(call_eval = SSE, call_grad = SSE_gr, vars = Beta_init,
X = X_, y = y, lambda0 = lambda[2], alpha = alpha,
orthantwise_c = alpha*lambda[2], orthantwise_start = 1,
invisible = TRUE)$par
# parameter estimates for rest of lambdas (using warm starts)
if (nlambda > 1) {
for (j in 2:nlambda) {
Betamat_scaled[,j] <- lbfgs(call_eval = SSE, call_grad = SSE_gr, vars = Betamat_scaled[,j-1],
X = X_, y = y, lambda0 = lambda[j], alpha = alpha,
orthantwise_c = alpha*lambda[j], orthantwise_start = 1,
invisible = TRUE)$par
}
}
# rescale Betas if required
if (scale) {
Betamat <- rbind(Betamat_scaled[1,] -
colSums(Betamat_scaled[-1,]*tcrossprod(means, rep(1,nlambda)) / tcrossprod(sds, rep(1,nlambda)) ), Betamat_scaled[-1,] / tcrossprod(sds, rep(1,nlambda)) )
} else {
Betamat <- Betamat_scaled
}
colnames(Betamat) <- lambda
return (Betamat)
}
# CODE FOR TESTING
# simulate some linear regression data
n <- 100
p <- 5
X <- matrix(rnorm(n*p),n,p)
true_Beta <- sample(seq(0,9),p+1,replace = TRUE)
y <- drop(cbind(1,X) %*% true_Beta)
library(glmnet)
# function to compare glmnet vs lbfgs for a given alpha
glmnet_compare <- function(X, y, alpha) {
m_glmnet <- glmnet(X, y, nlambda = 5, lambda.min.ratio = 1e-4, alpha = alpha)
Beta1 <- coef(m_glmnet)
Beta2 <- linreg_lbfgs(X, y, alpha = alpha, scale = TRUE, lambda = m_glmnet$lambda)
# mean Euclidean distance between glmnet and lbfgs results
mean(apply (Beta1 - Beta2, 2, function(x) sqrt(sum(x^2))) )
}
# compare results
alpha_seq <- seq(0,1,0.2)
plot(alpha_seq, sapply(alpha_seq, function(alpha) glmnet_compare(X,y,alpha)), type = "l", ylab = "Comparison metric")
@hxd1011 Probé tu código, aquí hay algunas pruebas (hice algunos ajustes menores para que coincida con la estructura de glmnet - nota que no regularizamos el término de intercepción, y las funciones de pérdida deben ser escaladas). Esto es para alpha = 0
, pero puedes probar con cualquier alpha
- los resultados no coinciden.
rm(list=ls())
set.seed(0)
# simulate some linear regression data
n <- 1e3
p <- 20
x <- matrix(rnorm(n*p),n,p)
true_Beta <- sample(seq(0,9),p+1,replace = TRUE)
y <- drop(cbind(1,x) %*% true_Beta)
library(glmnet)
alpha = 0
m_glmnet = glmnet(x, y, alpha = alpha, nlambda = 5)
# linear regression loss and gradient
lr_loss<-function(w,lambda1,lambda2){
e=cbind(1,x) %*% w -y
v= 1/(2*n) * (t(e) %*% e) + lambda1 * sum(abs(w[2:(p+1)])) + lambda2/2 * crossprod(w[2:(p+1)])
return(as.numeric(v))
}
lr_loss_gr<-function(w,lambda1,lambda2){
e=cbind(1,x) %*% w -y
v= 1/n * (t(cbind(1,x)) %*% e) + c(0, lambda1*sign(w[2:(p+1)]) + lambda2*w[2:(p+1)])
return(as.numeric(v))
}
outmat <- do.call(cbind, lapply(m_glmnet$lambda, function(lambda)
optim(rnorm(p+1),lr_loss,lr_loss_gr,lambda1=alpha*lambda,lambda2=(1-alpha)*lambda,method="L-BFGS")$par
))
glmnet_coef <- coef(m_glmnet)
apply(outmat - glmnet_coef, 2, function(x) sqrt(sum(x^2)))
0 votos
No estoy seguro de que tu pregunta sea sobre el tema (creo que podría serlo, ya que es sobre la técnica de optimización subyacente), y realmente no puedo comprobar tu código ahora, pero
lbfgs
plantea una cuestión sobre laorthantwise_c
argumento sobreglmnet
equivalencia.0 votos
El problema no es realmente con
lbfgs
yorthantwise_c
como cuandoalpha = 1
la solución es casi exactamente la misma conglmnet
. Tiene que ver con el lado de la regularización L2, es decir, cuandoalpha < 1
. Creo que hacer algún tipo de modificación en la definición deSSE
ySSE_gr
debería arreglarlo, pero no estoy seguro de cuál debería ser la modificación - hasta donde yo sé, esas funciones están definidas exactamente como se describen en el documento de glmnet.0 votos
Esto puede ser más una pregunta de stackoverflow, de programación.
3 votos
Pensé que tenía más que ver con la optimización y la regularización que con el código en sí, y por eso lo publiqué aquí.
1 votos
Para una pregunta de optimización pura, scicomp.stackexchange.com también es una opción. Sin embargo, no estoy seguro de si las preguntas específicas sobre el idioma están en el tema. (por ejemplo, "hacer esto en R")
0 votos
¿está seguro de que la ecuación básica es correcta? ¿Por qué el componente L2 tiene un coeficiente de 1/2?
0 votos
Esa es la fórmula que aparece en el artículo de glmnet (enlace en cuestión). Estoy tratando de replicar los resultados de glmnet
0 votos
@aginensky Se divide por 2 porque al derivar en $\beta$ se anula muy bien.