Chuyển đến nội dung chính

Chọn Lọc Đặc Trưng trong R với Gói Boruta

Thực hành chọn lọc đặc trưng trong R: khám phá thuật toán Boruta, một wrapper quanh thuật toán phân loại Random Forest, và cách triển khai!
· 10 phút đọc

Khám phá cùng AI

Dữ liệu có số chiều cao, xét theo số lượng đặc trưng, ngày càng phổ biến trong các bài toán machine learning. Để trích xuất thông tin hữu ích từ khối lượng dữ liệu lớn này, bạn cần áp dụng các kỹ thuật thống kê để giảm nhiễu hoặc dữ liệu dư thừa. Lý do là bạn thường không cần dùng mọi đặc trưng hiện có để huấn luyện mô hình. Bạn có thể cải thiện mô hình bằng cách chỉ đưa vào những đặc trưng không tương quan và không trùng lặp. Đây chính là lúc chọn lọc đặc trưng phát huy vai trò quan trọng. Nó không chỉ giúp huấn luyện mô hình nhanh hơn mà còn giảm độ phức tạp, giúp dễ diễn giải hơn và cải thiện độ chính xác, độ chính xác dự đoán hay recall, tùy theo thước đo hiệu năng bạn sử dụng.

Trong bài hướng dẫn này, bạn sẽ tìm hiểu các khái niệm sau:

  • Đầu tiên, bạn sẽ tìm hiểu thêm về chọn lọc đặc trưng: khi nào nên dùng và các phương pháp sẵn có để chọn ra những đặc trưng quan trọng nhất cho mô hình!

  • Tiếp theo, bạn sẽ được giới thiệu về thuật toán Boruta. Bạn sẽ thấy cách sử dụng nó để thực hiện tìm kiếm top-down các đặc trưng liên quan bằng cách so sánh độ quan trọng của thuộc tính gốc với độ quan trọng đạt được ngẫu nhiên (ước tính bằng các bản sao xáo trộn của chúng) và dần loại bỏ các đặc trưng không liên quan.

  • Bạn cũng sẽ lướt qua bộ dữ liệu sẽ được dùng để chọn lọc đặc trưng. Bạn sẽ thấy cách bù khuyết giá trị thiếu dễ dàng với gói Amelia.

  • Cuối cùng, bạn sẽ tìm hiểu thêm về gói Boruta, công cụ để chạy thuật toán này.

Chọn Lọc Đặc Trưng

Nhìn chung, khi muốn giảm số chiều của dữ liệu, bạn thường gặp các phương pháp như Phân tích Thành phần Chính (PCA), Phân rã Giá trị Kỳ dị, v.v. Tự nhiên sẽ nảy sinh câu hỏi: vì sao còn cần các phương pháp chọn lọc đặc trưng khác? Điểm cần lưu ý là những kỹ thuật này là các cách chọn lọc đặc trưng không giám sát: chẳng hạn PCA dùng phương sai của dữ liệu để tìm các thành phần. Các kỹ thuật này không xét đến mối quan hệ giữa giá trị đặc trưng và nhãn mục tiêu hay giá trị mục tiêu. Ngoài ra, chúng còn đi kèm một số giả định (như phân phối chuẩn), đòi hỏi phải biến đổi dữ liệu trước khi áp dụng. Những ràng buộc này không phù hợp với mọi loại dữ liệu.

Nhìn chung có ba nhóm phương pháp chọn lọc đặc trưng:

  • Phương pháp Lọc (Filter): thường dùng như bước tiền xử lý. Việc chọn đặc trưng độc lập với bất kỳ thuật toán học máy nào. Thay vào đó, các đặc trưng được chọn dựa trên điểm số trong các kiểm định thống kê về tương quan với biến đầu ra. Một số phương pháp lọc phổ biến gồm các thước đo tương quan (Pearson, Spearman, khoảng cách), kiểm định Chi-bình phương, ANOVA, Fisher’s Score, v.v.

  • Phương pháp Wrapper: bạn thử dùng một tập con đặc trưng và huấn luyện mô hình với chúng. Dựa trên kết quả từ mô hình trước, bạn quyết định thêm hoặc bớt đặc trưng khỏi tập con. Forward Selection, Backward Elimination là vài ví dụ.

  • Phương pháp Nhúng (Embedded): các thuật toán có cơ chế chọn lọc đặc trưng tích hợp sẵn. Hồi quy LASSO là một ví dụ.

Trong bài này, bạn sẽ dùng một phương pháp wrapper có sẵn trong R thông qua gói Boruta.

Thuật Toán Boruta

Boruta là một wrapper xây dựng quanh thuật toán phân loại random forest. Nó cố gắng nắm bắt tất cả các đặc trưng quan trọng, thú vị trong dữ liệu của bạn đối với biến đầu ra.

  • Đầu tiên, nó nhân đôi bộ dữ liệu và xáo trộn giá trị ở từng cột. Những giá trị này được gọi là đặc trưng bóng (shadow features). * Sau đó, nó huấn luyện một bộ phân loại, chẳng hạn Random Forest, trên bộ dữ liệu. Bằng cách này, bạn có được ý niệm về độ quan trọng — thông qua Mean Decrease Accuracy hoặc Mean Decrease Impurity — cho từng đặc trưng trong dữ liệu. Điểm càng cao, đặc trưng càng quan trọng.
  • Tiếp theo, thuật toán kiểm tra với mỗi đặc trưng thật của bạn xem liệu độ quan trọng của chúng có cao hơn hay không. Tức là, đặc trưng đó có Z-score cao hơn Z-score lớn nhất của các đặc trưng bóng hay không. Nếu có, điều này được ghi nhận vào một vector. Đây được gọi là một lần “hit”. Sau đó, nó tiếp tục lặp khác. Sau số vòng lặp định sẵn, bạn sẽ có một bảng các “hit”. Nhớ rằng: Z-score là số độ lệch chuẩn mà một điểm dữ liệu cách trung bình; để biết thêm, nhấp vào đây.
  • Ở mỗi vòng lặp, thuật toán so sánh Z-score của các bản sao xáo trộn với Z-score của đặc trưng gốc để xem liệu đặc trưng gốc có tốt hơn hay không. Nếu có, thuật toán đánh dấu đặc trưng đó là quan trọng. Về bản chất, thuật toán đang kiểm định độ quan trọng của đặc trưng bằng cách so với các bản sao xáo trộn ngẫu nhiên, giúp tăng độ vững. Việc này được thực hiện đơn giản bằng cách so sánh số lần một đặc trưng vượt trội hơn các đặc trưng bóng bằng phân phối nhị thức.

feature selection R boruta

  • Nếu một đặc trưng không ghi nhận “hit” nào trong, ví dụ, 15 vòng lặp, bạn loại bỏ nó và xóa khỏi ma trận gốc. Sau số vòng lặp định sẵn — hoặc khi tất cả đặc trưng đã được xác nhận hoặc loại bỏ — bạn dừng lại.

Thuật Toán Boruta trong R

Hãy dùng thuật toán Boruta trên một trong những bộ dữ liệu phổ biến nhất: Bank Marketing. Dữ liệu này mô tả các chiến dịch marketing trực tiếp (gọi điện) của một ngân hàng Bồ Đào Nha. Mục tiêu phân loại là dự đoán khách hàng có đăng ký tiền gửi có kỳ hạn hay không.

Mẹo: đừng quên xem mô tả chi tiết các đặc trưng tại đây.

read_file <- read.csv('./bank_bank.csv',header=TRUE,sep=';',stringsAsFactors = F) #read csv into a dataframe
str(read_file)

## 'data.frame':    4521 obs. of  17 variables:
##  $ age      : int  30 33 35 30 59 35 36 39 41 43 ...
##  $ job      : chr  "unemployed" "services" "management" "management" ...
##  $ marital  : chr  "married" "married" "single" "married" ...
##  $ education: chr  "primary" "secondary" "tertiary" "tertiary" ...
##  $ default  : chr  "no" "no" "no" "no" ...
##  $ balance  : int  1787 4789 1350 1476 0 747 307 147 221 -88 ...
##  $ housing  : chr  "no" "yes" "yes" "yes" ...
##  $ loan     : chr  "no" "yes" "no" "yes" ...
##  $ contact  : chr  "cellular" "cellular" "cellular" "unknown" ...
##  $ day      : int  19 11 16 3 5 23 14 6 14 17 ...
##  $ month    : chr  "oct" "may" "apr" "jun" ...
##  $ duration : int  79 220 185 199 226 141 341 151 57 313 ...
##  $ campaign : int  1 1 1 4 1 2 1 2 2 1 ...
##  $ pdays    : int  -1 339 330 -1 -1 176 330 -1 -1 147 ...
##  $ previous : int  0 4 1 0 0 3 2 0 0 2 ...
##  $ poutcome : chr  "unknown" "failure" "failure" "unknown" ...
##  $ y        : chr  "no" "no" "no" "no" ...

Hãy dùng hàm summary() để tóm tắt các thống kê mô tả phổ biến của các đặc trưng trong dữ liệu.

summary(read_file)

##       age            job              marital           education        
##  Min.   :19.00   Length:4521        Length:4521        Length:4521       
##  1st Qu.:33.00   Class :character   Class :character   Class :character  
##  Median :39.00   Mode  :character   Mode  :character   Mode  :character  
##  Mean   :41.17                                                           
##  3rd Qu.:49.00                                                           
##  Max.   :87.00                                                           
##    default             balance        housing              loan          
##  Length:4521        Min.   :-3313   Length:4521        Length:4521       
##  Class :character   1st Qu.:   69   Class :character   Class :character  
##  Mode  :character   Median :  444   Mode  :character   Mode  :character  
##                     Mean   : 1423                                        
##                     3rd Qu.: 1480                                        
##                     Max.   :71188                                        
##    contact               day           month              duration   
##  Length:4521        Min.   : 1.00   Length:4521        Min.   :   4  
##  Class :character   1st Qu.: 9.00   Class :character   1st Qu.: 104  
##  Mode  :character   Median :16.00   Mode  :character   Median : 185  
##                     Mean   :15.92                      Mean   : 264  
##                     3rd Qu.:21.00                      3rd Qu.: 329  
##                     Max.   :31.00                      Max.   :3025  
##     campaign          pdays           previous         poutcome        
##  Min.   : 1.000   Min.   : -1.00   Min.   : 0.0000   Length:4521       
##  1st Qu.: 1.000   1st Qu.: -1.00   1st Qu.: 0.0000   Class :character  
##  Median : 2.000   Median : -1.00   Median : 0.0000   Mode  :character  
##  Mean   : 2.794   Mean   : 39.77   Mean   : 0.5426                     
##  3rd Qu.: 3.000   3rd Qu.: -1.00   3rd Qu.: 0.0000                     
##  Max.   :50.000   Max.   :871.00   Max.   :25.0000                     
##       y            
##  Length:4521       
##  Class :character  
##  Mode  :character  
##                    
##                    
##  

Hàm summary() đưa ra số đo khuynh hướng trung tâm cho các đặc trưng liên tục như mean, median, các phân vị, v.v. Nếu có đặc trưng phân loại trong tập dữ liệu, bạn cũng sẽ thấy Class và Mode của chúng.

Giờ hãy chuyển các đặc trưng phân loại sang kiểu factor:

convert <- c(2:5, 7:9,11,16:17)
read_file[,convert] <- data.frame(apply(read_file[convert], 2, as.factor))
str(read_file)

## 'data.frame':    4521 obs. of  17 variables:
##  $ age      : int  30 33 35 30 59 35 36 39 41 43 ...
##  $ job      : Factor w/ 12 levels "admin.","blue-collar",..: 11 8 5 5 2 5 7 10 3 8 ...
##  $ marital  : Factor w/ 3 levels "divorced","married",..: 2 2 3 2 2 3 2 2 2 2 ...
##  $ education: Factor w/ 4 levels "primary","secondary",..: 1 2 3 3 2 3 3 2 3 1 ...
##  $ default  : Factor w/ 2 levels "no","yes": 1 1 1 1 1 1 1 1 1 1 ...
##  $ balance  : int  1787 4789 1350 1476 0 747 307 147 221 -88 ...
##  $ housing  : Factor w/ 2 levels "no","yes": 1 2 2 2 2 1 2 2 2 2 ...
##  $ loan     : Factor w/ 2 levels "no","yes": 1 2 1 2 1 1 1 1 1 2 ...
##  $ contact  : Factor w/ 3 levels "cellular","telephone",..: 1 1 1 3 3 1 1 1 3 1 ...
##  $ day      : int  19 11 16 3 5 23 14 6 14 17 ...
##  $ month    : Factor w/ 12 levels "apr","aug","dec",..: 11 9 1 7 9 4 9 9 9 1 ...
##  $ duration : int  79 220 185 199 226 141 341 151 57 313 ...
##  $ campaign : int  1 1 1 4 1 2 1 2 2 1 ...
##  $ pdays    : int  -1 339 330 -1 -1 176 330 -1 -1 147 ...
##  $ previous : int  0 4 1 0 0 3 2 0 0 2 ...
##  $ poutcome : Factor w/ 4 levels "failure","other",..: 4 1 1 4 4 1 2 4 4 1 ...
##  $ y        : Factor w/ 2 levels "no","yes": 1 1 1 1 1 1 1 1 1 1 ...

Vì các điểm dữ liệu sẽ được xáo trộn để tạo đặc trưng bóng và Z-score được tính cho từng điểm, việc xử lý giá trị thiếu hoặc trống trước khi dùng gói boruta là quan trọng, nếu không sẽ phát sinh lỗi.

(Không) may là bộ dữ liệu này không có hai dạng giá trị trên. Tuy nhiên, vì mục đích học tập, bạn sẽ chủ động thêm một số NA vào dữ liệu.

Hãy gieo giá trị thiếu vào tập dữ liệu bằng hàm prodNA(). Bạn có thể dùng hàm này bằng cách cài gói missForest.

Nhớ rằng bạn có thể dùng install.packages() để cài các gói còn thiếu khi cần!

library(missForest)

# Generate 5% missing values at random
bank.mis <- prodNA(read_file, noNA = 0.05)  

Bạn có thể gọi lại hàm summary() trên data frame mới để xem số lượng NA được thêm vào, nhưng hãy thử sáng tạo hơn một chút!

Hãy trực quan hóa mức độ thiếu dữ liệu bằng đoạn mã ggplot2 sau:

library(reshape2)
library(ggplot2)
library(dplyr)
ggplot_missing <- function(x){

  x %>% 
    is.na %>%
    melt %>%
    ggplot(data = .,
           aes(x = Var2,
               y = Var1)) +
    geom_raster(aes(fill = value)) +
    scale_fill_grey(name = "",
                    labels = c("Present","Missing")) +
    theme_minimal() + 
    theme(axis.text.x  = element_text(angle=45, vjust=0.5)) + 
    labs(x = "Variables in Dataset",
         y = "Rows / observations")
}
ggplot_missing(bank.mis)

Các đường trắng trên biểu đồ cho thấy một cách trực quan rằng bạn đã gieo giá trị thiếu ở mọi đặc trưng, nhưng bạn thấy viết hàm ggplot_missing kia cũng khá tốn công chứ? Gói Amelia trong R, mà bạn sẽ dùng ở phần sau, cung cấp một cách vẽ tương tự chỉ với một dòng lệnh:

library(Amelia)
missmap(bank.mis)

Hãy tự thử nhé!

Bù Khuyết Giá Trị Thiếu với Amelia

Bạn có thể bù khuyết giá trị thiếu theo nhiều cách như thay bằng mean, median hoặc mode (với đặc trưng phân loại), nhưng ở đây hãy dùng một gói mạnh khác để bù khuyết là Amelia.

Amelia lấy m mẫu bootstrap và áp dụng thuật toán EMB (kỳ vọng–tối đa hóa kết hợp bootstrap) cho từng mẫu. Các ước lượng mean và phương sai ở mỗi mẫu sẽ khác nhau. Cuối cùng, bộ ước lượng đầu tiên được dùng để bù khuyết tập giá trị thiếu thứ nhất bằng hồi quy, bộ thứ hai cho tập thứ hai, v.v. Bù khuyết đa lần giúp giảm thiên lệch và tăng hiệu quả. Ngoài ra, nó hỗ trợ bù khuyết song song trên CPU đa lõi.

Nó có 3 tham số quan trọng:

  • m: số bộ dữ liệu đã bù khuyết cần tạo.
  • idvars: giữ nguyên các biến ID và những biến bạn không muốn bù khuyết.
  • noms: liệt kê các biến định danh (nominal) tại đây.
library(Amelia)
amelia_bank <- amelia(bank.mis, m=3, parallel = "multicore",noms=c('job','marital','education','default','housing','loan','contact','month','poutcome','y'))

## -- Imputation 1 --
## 
##   1  2  3  4  5  6
## 
## -- Imputation 2 --
## 
##   1  2  3  4  5  6
## 
## -- Imputation 3 --
## 
##   1  2  3  4  5

Để truy cập các data frame đã bù khuyết, bạn có thể dùng cách lấy tập con sau:

amelia_bank$imputations[[1]]

Để xuất bộ dữ liệu đã bù khuyết ra tệp csv, dùng:

write.amelia(amelia_bank, file.stem = "imputed_bank_data_set")

Gói R Boruta

Bây giờ hãy dùng thuật toán Boruta trên một trong các bộ dữ liệu đã bù khuyết. Bạn có thể dùng gói Boruta để thực hiện:

library(Boruta)
set.seed(111)
boruta.bank_train <- Boruta(y~., data = amelia_bank$imputations[[1]], doTrace = 2)
print(boruta.bank_train)

## Boruta performed 99 iterations in 18.97234 mins.
##  10 attributes confirmed important: age, contact, day, duration,
## housing and 5 more;
##  3 attributes confirmed unimportant: education, job, marital;
##  3 tentative attributes left: balance, campaign, default;

Boruta đưa ra kết luận về mức độ quan trọng của các đặc trưng trong dữ liệu. Nhiều đặc trưng đã được phân loại là quan trọng hoặc không quan trọng, nhưng bạn thấy vẫn có một số được xếp vào nhóm “tạm thời” (tentative).

Điều này nghĩa là gì?

Các đặc trưng tạm thời có độ quan trọng quá sát với đặc trưng bóng tốt nhất của chúng, khiến Boruta không thể đưa ra quyết định với độ tin cậy mong muốn trong số lượt chạy Random Forest mặc định.

Vậy bạn nên làm gì?

Bạn có thể cân nhắc tăng tham số maxRuns nếu còn đặc trưng tạm thời. Tuy nhiên, lưu ý rằng bạn cũng có thể cung cấp giá trị cho các tham số mtry và ntree, sẽ được truyền vào hàm randomForest(). Tham số đầu cho phép chỉ định số biến được lấy mẫu ngẫu nhiên làm ứng viên ở mỗi lần chia, còn tham số sau chỉ định số cây bạn muốn xây dựng. Khi đặt các đối số này, bộ phân loại Random Forest sẽ hội tụ tại giá trị tối thiểu của lỗi out-of-bag.

Nhớ rằng lỗi out-of-bag là ước lượng đo sai số dự đoán của các bộ phân loại dùng bootstrap aggregation để lấy mẫu con cho huấn luyện. Đây là sai số dự đoán trung bình trên mỗi mẫu huấn luyện X, chỉ sử dụng các cây không chứa X trong mẫu bootstrap của chúng.

Ngoài ra, bạn cũng có thể đặt đối số doTrace thành 1 hoặc 2 để nhận báo cáo tiến độ.

Gói boruta cũng có hàm TentativeRoughFix(), dùng để lấp đầy các quyết định còn thiếu bằng cách so sánh đơn giản median Z-score của đặc trưng với median Z-score của đặc trưng bóng quan trọng nhất:

#take a call on tentative features
boruta.bank <- TentativeRoughFix(boruta.bank_train)
print(boruta.bank)

## Boruta performed 99 iterations in 18.97234 mins.
## Tentatives roughfixed over the last 99 iterations.
##  12 attributes confirmed important: age, campaign, contact, day,
## default and 7 more;
##  4 attributes confirmed unimportant: balance, education, job,
## marital;

Boruta đã hoàn thành nhiệm vụ: phân loại thành công từng đặc trưng là quan trọng hay không quan trọng.

Bạn có thể vẽ biểu đồ tầm quan trọng biến bằng cách gọi plot(boruta.bank). Tuy nhiên, nhãn trục x sẽ nằm ngang, trông không gọn mắt.

Vì vậy, bạn sẽ đặt nhãn đặc trưng theo phương dọc trên trục x, như trong đoạn mã sau:

plot(boruta.bank, xlab = "", xaxt = "n")
lz<-lapply(1:ncol(boruta.bank$ImpHistory),function(i)
boruta.bank$ImpHistory[is.finite(boruta.bank$ImpHistory[,i]),i])
names(lz) <- colnames(boruta.bank$ImpHistory)
Labels <- sort(sapply(lz,median))
axis(side = 1,las=2,labels = names(Labels),
at = 1:ncol(boruta.bank$ImpHistory), cex.axis = 0.7)

Nhãn trục y Importance biểu thị Z-score của mỗi đặc trưng trong bộ dữ liệu đã xáo trộn.

Các boxplot màu xanh lam tương ứng với Z-score nhỏ nhất, trung bình và lớn nhất của đặc trưng bóng, trong khi boxplot màu đỏ và xanh lục lần lượt biểu diễn Z-score của các đặc trưng bị loại và được xác nhận. Như bạn thấy, các boxplot đỏ có Z-score thấp hơn Z-score lớn nhất của đặc trưng bóng, đó chính là lý do chúng bị xếp vào nhóm không quan trọng.

Bạn có thể xác nhận độ quan trọng của các đặc trưng bằng cách gõ:

getSelectedAttributes(boruta.bank, withTentative = F)

##  [1] "age"      "default"  "housing"  "loan"     "contact"  "day"     
##  [7] "month"    "duration" "campaign" "pdays"    "previous" "poutcome"

bank_df <- attStats(boruta.bank)
print(bank_df)

##              meanImp  medianImp     minImp    maxImp   normHits  decision
## age       11.4236197 11.3760979  8.4250222 15.518420 1.00000000 Confirmed
## job        0.0741753  0.3002281 -1.7651336  1.566687 0.01010101  Rejected
## marital    1.8891283  2.0043568 -1.0276720  4.804499 0.22222222  Rejected
## education  1.5969540  1.6188117 -1.6836346  4.629572 0.28282828  Rejected
## default    2.3721979  2.3472820 -0.1434933  5.044653 0.50505051 Confirmed
## balance    2.3349682  2.3214378 -0.8098151  5.567993 0.51515152  Rejected
## housing    8.4147808  8.4384240  4.7392059 10.404609 1.00000000 Confirmed
## loan       4.1872186  4.2797591  2.0325838  6.263155 0.87878788 Confirmed
## contact   18.9482180 18.9757719 16.0937657 22.121461 1.00000000 Confirmed
## day        9.5645192  9.5828766  6.1842233 13.495442 1.00000000 Confirmed
## month     24.1475736 24.2067940 20.0621966 27.200679 1.00000000 Confirmed
## duration  71.5232213 71.1785055 64.3941499 78.249830 1.00000000 Confirmed
## campaign   2.6221456  2.6188180 -0.4144493  4.941482 0.65656566 Confirmed
## pdays     26.5650528 26.7123730 23.7902945 29.067476 1.00000000 Confirmed
## previous  20.9569022 20.9703991 18.7273357 23.117672 1.00000000 Confirmed
## poutcome  28.5166889 28.4885934 25.9855974 31.527154 1.00000000 Confirmed

Bạn có thể dễ dàng kiểm chứng kết quả vì đặc trưng duration được xếp quan trọng nhất, điều này đã được đề cập trong mô tả dữ liệu (nhấp vào đây) nếu bạn đọc kỹ!

Kết Luận

Xong rồi! Bạn đã lọc ra các đặc trưng quan trọng nhất từ tập dữ liệu chỉ với vài dòng lệnh. Nhờ đó bạn đã giảm nhiễu cho dữ liệu, điều rất có lợi cho bất kỳ bộ phân loại nào khi gán nhãn cho một quan sát. Huấn luyện mô hình trên các đặc trưng quan trọng này chắc chắn sẽ cải thiện hiệu năng mô hình — cũng chính là mục tiêu của việc chọn lọc đặc trưng ngay từ đầu!

Nếu bạn muốn xem các tài liệu tham khảo được dùng để xây dựng hướng dẫn này, hãy xem danh sách sau:

Chủ đề

Liên quan