Machine Learning: Ridge Regression from An Introduction to Statistical Learning, Chapter 6

Especially to TT

Nguyen Chi Dung

Giới thiệu

Bài này hướng dẫn các bạn thực hiện hồi quy Ridge một cách chi tiết. Nội dung của bài thuộc chương 6 cuốn An Introduction to Statistical Learning của James et al. (2013). Sách các bạn có thể download miễn phí ở đây.

#-----------------------
# Gọi các gói cần thiết: 
#-----------------------
rm(list = ls())
library(ISLR)
data("Hitters")
library(tidyverse)
library(magrittr)

#------------------------
#  Tiền xử lí số liệu
#------------------------

# Bỏ dữ liệu trống: 
Hitters %<>% na.omit()
# Điều tra qua dữ liệu: 
Hitters %>% str()
## 'data.frame':    263 obs. of  20 variables:
##  $ AtBat    : int  315 479 496 321 594 185 298 323 401 574 ...
##  $ Hits     : int  81 130 141 87 169 37 73 81 92 159 ...
##  $ HmRun    : int  7 18 20 10 4 1 0 6 17 21 ...
##  $ Runs     : int  24 66 65 39 74 23 24 26 49 107 ...
##  $ RBI      : int  38 72 78 42 51 8 24 32 66 75 ...
##  $ Walks    : int  39 76 37 30 35 21 7 8 65 59 ...
##  $ Years    : int  14 3 11 2 11 2 3 2 13 10 ...
##  $ CAtBat   : int  3449 1624 5628 396 4408 214 509 341 5206 4631 ...
##  $ CHits    : int  835 457 1575 101 1133 42 108 86 1332 1300 ...
##  $ CHmRun   : int  69 63 225 12 19 1 0 6 253 90 ...
##  $ CRuns    : int  321 224 828 48 501 30 41 32 784 702 ...
##  $ CRBI     : int  414 266 838 46 336 9 37 34 890 504 ...
##  $ CWalks   : int  375 263 354 33 194 24 12 8 866 488 ...
##  $ League   : Factor w/ 2 levels "A","N": 2 1 2 2 1 2 1 2 1 1 ...
##  $ Division : Factor w/ 2 levels "E","W": 2 2 1 1 2 1 2 2 1 1 ...
##  $ PutOuts  : int  632 880 200 805 282 76 121 143 0 238 ...
##  $ Assists  : int  43 82 11 40 421 127 283 290 0 445 ...
##  $ Errors   : int  10 14 3 4 25 7 9 19 0 22 ...
##  $ Salary   : num  475 480 500 91.5 750 ...
##  $ NewLeague: Factor w/ 2 levels "A","N": 2 1 2 2 1 1 1 2 1 1 ...
##  - attr(*, "na.action")=Class 'omit'  Named int [1:59] 1 16 19 23 31 33 37 39 40 42 ...
##   .. ..- attr(*, "names")= chr [1:59] "-Andy Allanson" "-Billy Beane" "-Bruce Bochte" "-Bob Boone" ...
Hitters %>% dim()
## [1] 263  20
# Chuyển hóa thành  ma trận thông tin đầu vào: 
u <- Hitters %>% 
  model.matrix(Salary ~ ., data = .)
# u là một dạng "ma trận" với 263 dòng và 20 cột: 
u %>% dim()
## [1] 263  20
u %>% str()
##  num [1:263, 1:20] 1 1 1 1 1 1 1 1 1 1 ...
##  - attr(*, "dimnames")=List of 2
##   ..$ : chr [1:263] "-Alan Ashby" "-Alvin Davis" "-Andre Dawson" "-Andres Galarraga" ...
##   ..$ : chr [1:20] "(Intercept)" "AtBat" "Hits" "HmRun" ...
##  - attr(*, "assign")= int [1:20] 0 1 2 3 4 5 6 7 8 9 ...
##  - attr(*, "contrasts")=List of 3
##   ..$ League   : chr "contr.treatment"
##   ..$ Division : chr "contr.treatment"
##   ..$ NewLeague: chr "contr.treatment"
# Loại ra cột đầu tiên của u để có ma trận thông
# tin đầu vào: 
x <- u[, -1]
x %>% dim()
## [1] 263  19
# Biến phụ thuộc (tức Salary): 
y <- Hitters %>% pull(Salary)

#----------------------------------
#    Thực  hiện Ridge Regression
#----------------------------------

# Tạo ra một khoảng lambda từ 10^10 đến 10^-4: 

lambda_grid <- 10^seq(7, -4, length = 100)

# Thực hiện Ridge Regression: 
library(glmnet)
ridge1 <- glmnet(x, y, 
                 # chọn alpha = 0 cho Ridge. 
                 # Nếu alpha = 1 sẽ là Lasso: 
                 alpha = 0, 
                 # Chuẩn  hóa dữ liệu trước khi thực hiện: 
                 standardize = TRUE, 
                 # Với 100 giá trị lambda: 
                 lambda = lambda_grid)
# Ứng với một  lambda sẽ có 20 hệ số hồi quy (tính cả hệ số chặn): 
he_so_hq <- coef(ridge1)
# Ví dụ, hệ số hồi quy ứng với lambda = 10^7 là: 
he_so_hq[, 1]
##   (Intercept)         AtBat          Hits         HmRun          Runs 
##  5.357373e+02  5.443222e-05  1.974423e-04  7.955694e-04  3.338681e-04 
##           RBI         Walks         Years        CAtBat         CHits 
##  3.526612e-04  4.150630e-04  1.697604e-03  4.673161e-06  1.719785e-05 
##        CHmRun         CRuns          CRBI        CWalks       LeagueN 
##  1.296898e-04  3.449997e-05  3.560316e-05  3.766565e-05 -5.789555e-04 
##     DivisionW       PutOuts       Assists        Errors    NewLeagueN 
## -7.806370e-03  2.179882e-05  3.560422e-06 -1.661300e-05 -1.144934e-04
# Lambda thứ 50 đó có  giá trị: 
ridge1$lambda[50]
## [1] 35.93814
# Các hệ số hồi quy ứng  với lambda thứ 50: 
hq <- he_so_hq[, 50]
hq
##   (Intercept)         AtBat          Hits         HmRun          Runs 
##  6.407070e+01 -5.026628e-01  2.320295e+00 -1.396185e+00  1.101672e+00 
##           RBI         Walks         Years        CAtBat         CHits 
##  7.703019e-01  3.016126e+00 -7.692944e+00  2.554182e-03  1.199480e-01 
##        CHmRun         CRuns          CRBI        CWalks       LeagueN 
##  6.607760e-01  2.553854e-01  2.387831e-01 -2.098318e-01  4.973103e+01 
##     DivisionW       PutOuts       Assists        Errors    NewLeagueN 
## -1.209431e+02  2.577556e-01  1.444918e-01 -3.506943e+00 -1.386494e+01
# Có thể tính l2 norm tương ứng: 
l2_norm <- sqrt(sum((hq[-1])^2))
l2_norm 
## [1] 131.8458
# Hàm predict() có thể được sử dụng để tính các hệ 
# số hồi quy với một lambda bất kì. Như ta biết hồi quy
# OLS chỉ là một trường hợp riêng của Ridge khi lambda = 0: 

predict(ridge1, s = 0, type = "coefficients", exact = TRUE)
## 20 x 1 sparse Matrix of class "dgCMatrix"
##                         1
## (Intercept)  163.93623646
## AtBat         -1.97340819
## Hits           7.39040885
## HmRun          3.99960564
## Runs          -2.22703202
## RBI           -0.93669742
## Walks          6.20539846
## Years         -3.56606365
## CAtBat        -0.17721984
## CHits          0.20837873
## CHmRun         0.03122581
## CRuns          1.38009798
## CRBI           0.72134189
## CWalks        -0.79770449
## LeagueN       63.39218815
## DivisionW   -116.97962810
## PutOuts        0.28199310
## Assists        0.37409929
## Errors        -3.41730413
## NewLeagueN   -25.86532851
# Có thể kiểm tra lại với chú ý rằng có một sự sai 
# khác nhỏ bởi vì cách thức làm tròn số của hàm glmnet(): 
ols <- Hitters %>% lm(Salary ~., data = .)
ols$coefficients
##  (Intercept)        AtBat         Hits        HmRun         Runs 
##  163.1035878   -1.9798729    7.5007675    4.3308829   -2.3762100 
##          RBI        Walks        Years       CAtBat        CHits 
##   -1.0449620    6.2312863   -3.4890543   -0.1713405    0.1339910 
##       CHmRun        CRuns         CRBI       CWalks      LeagueN 
##   -0.1728611    1.4543049    0.8077088   -0.8115709   62.5994230 
##    DivisionW      PutOuts      Assists       Errors   NewLeagueN 
## -116.8492456    0.2818925    0.3710692   -3.3607605  -24.7623251
# Có thể xem qua mức độ sai khác: 
100*(predict(ridge1, s = 0, type = "coefficients", exact = TRUE) / ols$coefficients)
## 20 x 1 sparse Matrix of class "dgCMatrix"
##                     1
## (Intercept) 100.51050
## AtBat        99.67348
## Hits         98.52870
## HmRun        92.35081
## Runs         93.72202
## RBI          89.63938
## Walks        99.58455
## Years       102.20717
## CAtBat      103.43140
## CHits       155.51701
## CHmRun      -18.06411
## CRuns        94.89743
## CRBI         89.30717
## CWalks       98.29141
## LeagueN     101.26641
## DivisionW   100.11158
## PutOuts     100.03568
## Assists     100.81658
## Errors      101.68247
## NewLeagueN  104.45436
#--------------
#  Tính MSE: 
#--------------
# Tính các giá trị ước lượng khi lambda = 0: 
ridge_dubao <- predict(ridge1, s = 0, newx = x)
# Viết hàm tính MSE: 
mse <- function(dubao, thuc) {
  return(mean((dubao - thuc)^2))
}

# Tính MSE: 
mse(ridge_dubao, y)
## [1] 92024.73
# So với cách chúng ta thực hiện OLS thì
# có một  sai khác nhỏ do cách thức làm 
# tròn khác nhau: 
mse(ols$fitted.values, Hitters$Salary)
## [1] 92017.87
#-------------------------------------------------
# Chúng ta có thể viết một hàm hình ảnh hóa sự
# Biến đổi của các hệ số hồi quy (trừ hệ số chặn)
# khi lambda thay đổi. 
#-------------------------------------------------

plot_coef <- function(ridge_obj, ...) {
  heso <- coef(ridge_obj)
  cac_hs <- heso@Dimnames[[1]]
  gia_tri <- heso@x
  df1 <- data.frame(Coef = rep(cac_hs, length(lambda_grid)), Value = gia_tri)
  tg <- expand.grid(1:nrow(heso), lambda_grid)
  df1$Lambda <- tg$Var2
    df1 %>% filter(Coef != "(Intercept)") %>% 
    ggplot(aes(log(Lambda), Value, color = Coef)) + 
    geom_line(show.legend = FALSE) + 
    theme_minimal() + 
    labs(title = "Coefficients V.s Lambda")
}
# Rồi sử dụng hàm vừa viết: 
plot_coef(ridge1)

# Hoặc sử dụng hàm sẵn có (nhưng xấu): 
plot(ridge1, xvar = "lambda", label = T)

#------------------------------------------------
#  Khảo sát biến đổi của MSE khi lambda thay đổi
#------------------------------------------------

# Thực hiện 10 folds cross validation. Nhớ rằng có
# 10*100 = 1000 mô hình được chạy: 
set.seed(1707)
k_folds <- cv.glmnet(x, y, 
                     alpha = 0, 
                     lambda = lambda_grid, 
                     nfolds = 10)


# Viết hàm hình  ảnh hóa biến đổi của MSE khi lambda thay đổi: 

mse_lambda <- function(ridge_cv, ...) {
  mse_lam <- data.frame(Lambda = ridge_cv$lambda, 
                        MSE = ridge_cv$cvm)
  mse_lam %>% ggplot(aes(log(Lambda), MSE)) + 
    geom_line() + geom_point(color = "blue", alpha = 0.3) +
    geom_point(data = mse_lam %>% filter(MSE == min(MSE)), color = "red", size = 3) + 
    labs(title = "MSE V.s Lambda", 
         subtitle = "Note: The red point corresponds to the smallest value of the MSE") + 
    theme_minimal() 
}

# Sử dụng hàm vừa viết: 
mse_lambda(k_folds)

# MSE nhỏ nhất và lambda tương ứng là: 
min(k_folds$cvm)
## [1] 115907.2
k_folds$lambda.min
## [1] 5.994843
# Các hệ số hồi quy Ridge lúc này (tương ứng với lambda thứ 57) là: 
he_so_hq[, which.min(k_folds$cvm)]
##   (Intercept)         AtBat          Hits         HmRun          Runs 
##  1.407607e+02 -1.466739e+00  5.118644e+00  1.203639e-01 -6.251881e-03 
##           RBI         Walks         Years        CAtBat         CHits 
##  2.154588e-01  4.933163e+00 -1.083019e+01 -4.208203e-02  1.939445e-01 
##        CHmRun         CRuns          CRBI        CWalks       LeagueN 
##  7.466971e-01  6.051086e-01  3.404316e-01 -5.562237e-01  6.074272e+01 
##     DivisionW       PutOuts       Assists        Errors    NewLeagueN 
## -1.234518e+02  2.779227e-01  2.710992e-01 -3.814888e+00 -2.736373e+01
# Với giá trị lambda tối ưu ở trên thì MSE cho train data là: 
dubao <- predict(ridge1, s = k_folds$lambda.min, newx = x)
mse(dubao, y)
## [1] 93931.71
# So sánh với OLS: 
mse(ols$fitted.values, y)
## [1] 92017.87

MSE của OLS thấp hơn cho thấy rằng hồi quy OLS tạo ra những giá trị dự báo tốt hơn so với Ridge. Tuy nhiên lưu ý rằng điều này chỉ mới đúng cho CHỈ MỘT bộ dữ liệu. Để so sánh khả năng dụ báo của hai mô hình chúng ta cần một cách tiếp cận khác chính xác hơn: lần lượt cho hai mô hình này dự báo với, chẳng hạn, 100 mẫu khác nhau rồi so sánh MSE trung bình của 100 lần chạy này:

MSE_ridge <- c()
MSE_ols <- c()
for (i in 1:100) {
  set.seed(i)
  train <- Hitters %>% sample_frac(0.5)
  test <- dplyr::setdiff(Hitters, train)
  # Chuẩn bị dữ  liệu cho Ridge Rigression: 
  x_ridge_train <- train %>% model.matrix(Salary ~., data = .)
  x_ridge_train <- x_ridge_train[, -1]
  y_ridge_train <- train$Salary
  
  x_ridge_test <- test %>% model.matrix(Salary ~., data = .)
  x_ridge_test <- x_ridge_test[, -1]
  y_ridge_test <- test$Salary
  # Thực  hiện Ridge Regression: 
  ridge2 <- glmnet(x_ridge_train, y_ridge_train, 
                   alpha = 0, standardize = TRUE, 
                   lambda = k_folds$lambda.min)
  # Dự báo từ Ridge: 
  dubao <- predict(ridge2, s = k_folds$lambda.min, newx = x_ridge_test)
  mse_ridge <- mse(dubao, y_ridge_test)
  MSE_ridge <- c(MSE_ridge, mse_ridge)
  
  # Thực hiện OLS: 
  ols2 <- train %>% lm(Salary ~., data = .)
  # Dự báo từ OLS: 
  dubao <- predict(ols2, test %>% select(-Salary))
  mse_ols <- mse(dubao, test$Salary)
  MSE_ols <- c(MSE_ols,  mse_ols)
}

df_ketqua <- data.frame(Model = c(rep("Ridge", 100), rep("OLS", 100)),  
                        MSE = c(MSE_ridge, MSE_ols))

# Hình ảnh hóa dữ liệu: 
df_ketqua %>% ggplot(aes(Model, MSE)) + geom_boxplot()

# Hình ảnh này chỉ ra dấu  hiệu rằng Ridge tạo ra các giá trị dự báo
# sát hơn với giá trị thực tế. Chúng ta có thể đưa ra bằng chứng  thống 
# kê cho sự khác biệt trong  dự báo của hai mô hình bằng kiểm định t: 
t.test(df_ketqua$MSE ~ df_ketqua$Model)
## 
##  Welch Two Sample t-test
## 
## data:  df_ketqua$MSE by df_ketqua$Model
## t = 2.6479, df = 195.8, p-value = 0.008761
## alternative hypothesis: true difference in means is not equal to 0
## 95 percent confidence interval:
##   2036.413 13923.688
## sample estimates:
##   mean in group OLS mean in group Ridge 
##            133205.4            125225.3

p-value = 0.008761 < 5% chỉ ra rằng có sự khác biệt về khả năng dự báo của hai cách tiếp cận. Cụ thể hơn, dự báo từ Ridge Regression tạo ra MSE trung bình trên 100 mẫu thử nghiệm là thấp hơn só với MSE trung bình từ cách tiếp cận truyền thống OLS.

Một ví dụ về ứng dụng của Ridge Regression: Dự báo giá nhà ở thành phố Boston

Trong mục này chúng ta sẽ sử dụng bộ số liệu BostonHousing tích hợp cùng gói mlbench. Mục tiêu là dự báo giá nhà (biến được dự báo là medv). Bộ dữ liệu này từng được sử dụng ở một số Research Paper. Một trong số đó là nghiên cứu có tên Hedonic prices and the demand for clean air.

Dưới đây chỉ minh họa một số điểm cơ bản của sử dụng Ridge Regression. Các phân tích phức tạp hơn để ngỏ cho người đọc.

# Gọi dữ liệu và chuẩn bị dữ liệu: 
library(mlbench)
data("BostonHousing")
x <- BostonHousing %>% model.matrix(medv ~., data = .)
x <- x[, -1]
y <- BostonHousing %>% pull(medv)
#  Tạo một khoảng của lambda: 
lambda_grid <- 10^seq(10, -4, length = 100)

# Tìm lambda tối ưu: 
set.seed(29)
k_folds_boston <- cv.glmnet(x, y, 
                            alpha = 0, 
                            lambda = lambda_grid, 
                            nfolds = 10)

# Hinh ảnh hóa lambda tối ưu: 
mse_lambda(k_folds_boston)

# Giá trị tối  ưu của lambda: 
k_folds_boston$lambda.min
## [1] 0.09326033
# Đánh giá biến đổi  của  các hệ số hồi quy: 
ridge_boston <- glmnet(x, y, 
                       alpha =  0, 
                       standardize = TRUE, 
                       lambda = lambda_grid)

plot_coef(ridge_boston)

# Các hệ số hồi quy tương ứng với lambda tối ưu: 
coef(ridge_boston)[, which.min(k_folds_boston$cvm)]
##   (Intercept)          crim            zn         indus         chas1 
##  3.464325e+01 -1.032801e-01  4.336313e-02  4.452949e-03  2.748370e+00 
##           nox            rm           age           dis           rad 
## -1.658536e+01  3.866241e+00 -3.439613e-04 -1.412878e+00  2.675841e-01 
##           tax       ptratio             b         lstat 
## -1.050984e-02 -9.336343e-01  9.282917e-03 -5.158660e-01
# Sử dụng Ridge Regression  cho dự báo: 
boston_dubao <- predict(ridge_boston, s = k_folds_boston$lambda.min, x)
# Có thể xem 6 giá  trị dự bao đầu tiên: 
boston_dubao %>% head()
##          1
## 1 30.11737
## 2 25.01593
## 3 30.57416
## 4 28.68584
## 5 28.04339
## 6 25.30989
# Giá trị thực tế là: 
y %>% head()
## [1] 24.0 21.6 34.7 33.4 36.2 28.7
# Tính MSE: 
mse(boston_dubao, y)
## [1] 21.91857
# So với OLS truyền  thống: 
ols_boston <- BostonHousing %>% lm(medv ~., data = .)
# MSE dựa trên một tình huống (một  bộ dữ liệu) thì 
# OLS tạo ra thấp hơn. Lưu ý rằng Ridge Regression 
# là mô hình có xu hướng underfitting. Nghĩa là dự báo
# trên Train Data thì tệ hơn khi dự báo trên Test Data. 
mse(ols_boston$fitted.values, y)
## [1] 21.89483

References

  1. Harrison, D. and Rubinfeld, D.L. (1978). Hedonic prices and the demand for clean air. Journal of Environmental Economics and Management, 5, 81–102.

  2. Hoerl, A.E. and Kennard, R. (1970). Ridge regression: Biased estimation for nonorthogonal problems. Technometrics, 12: 55-67.

  3. James, G., Witten, D., Hastie, T., & Tibshirani, R. (2013). An Introduction to Statistical Learning. New York: Springer.