Replikowanie wyników regresji liniowej glmnet przy użyciu ogólnego optymalizatora

10

Jak mówi tytuł, próbuję powielić wyniki z glmnet linear przy użyciu optymalizatora LBFGS z biblioteki lbfgs. Ten optymalizator pozwala nam dodać termin regulatora L1 bez martwienia się o różnicę, o ile nasza funkcja celu (bez terminu regulatora L1) jest wypukła.

Problem regresji liniowej siatki elastycznej w papierze glmnet podaje

minβRp12)nβ0+Xβ-y2)2)+αλβ1+12)(1-α)λβ2)2)
gdzie XRn×p jest macierzą projektową, yRp jest wektorem obserwacji , α[0,1] to parametr elastycznej siatki, a λ>0 to parametr regularyzacji. Operator xp oznacza zwykłą normę Lp.

Poniższy kod definiuje funkcję, a następnie zawiera test służący do porównania wyników. Jak widać, wyniki są dopuszczalne, gdy alpha = 1, ale są daleko do wartości alpha < 1.błędu gorzej jak idziemy alpha = 1do alpha = 0, jak pokazuje poniższa działki (dalej „porównanie metryczny” jest średnią odległość euklidesowa pomiędzy oszacowaniami parametrów w glmnet i lbfgs dla danej ścieżki regularyzacji).

wprowadź opis zdjęcia tutaj

Okej, więc oto kod. Dodałem komentarze tam, gdzie to możliwe. Moje pytanie brzmi: dlaczego moje wyniki różnią się od wyników glmnetdla alpha < 1? Wyraźnie ma to związek z terminem regularyzacji L2, ale o ile wiem, wdrożyłem ten termin dokładnie tak, jak opisano w artykule. Każda pomoc będzie mile widziana!

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 Próbowałem twojego kodu, oto kilka testów (zrobiłem kilka drobnych poprawek, aby pasowały do ​​struktury glmnet - uwaga: nie regulujemy terminu przechwytywania, a funkcje utraty muszą być skalowane). To jest alpha = 0, ale możesz spróbować dowolne alpha- wyniki nie pasują.

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)))
użytkownik3294195
źródło
Nie jestem pewien, czy twoje pytanie dotyczy tematu (myślę, że tak, ponieważ dotyczy to podstawowej techniki optymalizacji) i nie mogę teraz naprawdę sprawdzić twojego kodu, ale lbfgspodnosi kwestię orthantwise_cargumentu dotyczącego glmnetrównoważności.
Firebug
Problem nie jest tak naprawdę związany z, lbfgsi orthantwise_ctak jak wtedy alpha = 1, rozwiązanie jest prawie takie samo glmnet. Ma to związek ze stroną regularyzacji L2 rzeczy, tj alpha < 1. Kiedy . Myślę, że dokonanie jakiejś modyfikacji definicji SSEi SSE_grpowinno to zostać naprawione, ale nie jestem pewien, jaka powinna być modyfikacja - o ile wiem, funkcje te są zdefiniowane dokładnie tak, jak opisano w dokumencie glmnet.
user3294195,
To może być bardziej przepełnienie stosu, pytanie programistyczne.
Matthew Gunn
3
Myślałem, że ma to więcej wspólnego z optymalizacją i regulacją niż z samym kodem, dlatego opublikowałem go tutaj.
user3294195
1
W przypadku czysto optymalizacyjnego pytania opcja scicomp.stackexchange.com jest również dostępna. Nie jestem jednak pewien, czy pytania dotyczące konkretnego języka są tam na ten temat? (np. „zrób to w R”)
GeoMatt22,

Odpowiedzi:

11

wersja tl; dr:

Cel domyślnie zawiera współczynnik skalowania , gdzie jest standardowym odchyleniem próbki.s^=sre(y)sre(y)

Dłuższa wersja

Jeśli przeczytasz drobny druk dokumentacji glmnet, zobaczysz:

Zauważ, że funkcją celu dla „gaussa” jest

               1/2  RSS/nobs + lambda*penalty,                  

a dla innych modeli tak jest

               -loglik/nobs + lambda*penalty.                   

Zauważ też, że dla „Gaussa” „glmnet” standaryzuje y, aby mieć wariancję jednostkową przed obliczeniem sekwencji lambda (a następnie unormuje uzyskane współczynniki); jeśli chcesz powielać / porównywać wyniki z innym oprogramowaniem, najlepiej podać znormalizowany y.

Oznacza to, że celem jest właściwie a ten glmnet zgłasza .

12)ny/s^-Xβ2)2)+λαβ1+λ(1-α)β2)2),
β~=s^β

Teraz, kiedy używasz czystego lasso ( ), wówczas unormalizacja glmnet oznacza, że ​​odpowiedzi są równoważne. Z drugiej strony, z czystym grzbietem, musisz skalować karę o współczynnik , aby ścieżka się zgadzała, ponieważ dodatkowy czynnik wyskakuje z kwadratu w kary . Dla pośredniego nie ma łatwego sposobu na skalowanie kary współczynników w celu odtworzenia wyniku.~ P 1 / sα=1β~1/s^glmnets^2)αglmnets

Kiedy skaluję by mieć wariancję jednostek, znajduję ywprowadź opis zdjęcia tutaj

co wciąż nie pasuje dokładnie. Wydaje się, że wynika to z dwóch rzeczy:

  1. Sekwencja lambda może być zbyt krótka, aby algorytm cyklicznego zniżania współrzędnych na początku był w pełni zbieżny.
  2. W danych nie ma terminu błędu ( regresji wynosi 1).R2)
  3. Zwróć też uwagę na błąd w kodzie, pod warunkiem, że zajmuje lambda[2]on początkowe dopasowanie, ale tak powinno być lambda[1].

Po skorygowaniu elementów 1-3 otrzymuję następujący wynik (chociaż YMMV w zależności od losowego materiału siewnego):

wprowadź opis zdjęcia tutaj

Andrew M.
źródło