Đa phần trong chúng ta khi thực hiện feature-selection đều sử dụng "SelectFromModel" (một module của Scikit-learn). Thường thì công việc ta sẽ làm như sau:
- Bạn chọn một mô hình dự đoán (ta sẽ gọi nó là
WhatevBoost
) - Thực hiện fit
WhatevBoost
với tất cả feature - Trích xuất những feature quan trọng từ
WhatevBoost
- Loại bỏ tất cả những feature có threshold thấp hơn mong muốn và giữ lại những feature còn lại
Quá trình này nghe có vẻ hợp lý phải không Câu trả lời sẽ làm bạn bất ngờ đấy
Trong bài viết này, ta sẽ cùng kiểm nghiệm xem cách làm này có thật sự hiệu quả không nhé
Mô phỏng dữ liệu
Trong bài viết, ta sẽ sử dụng dữ liệu mô phỏng. Đây là một cách tiếp cận tốt để thực hiện các thử nghiệm mà đa phần chúng ta ít khi sử dụng. Vậy thì tập dữ liệu mô phỏng trông sẽ như nào nhỉ Trước tiên, ta sẽ tạo các biến độc lập (hay các feature), một phần trong số các biến này liên quan đến biến mục tiêu (được gọi là y
), phần còn sẽ là nhiễu (noise). Mục tiêu là tìm ra một phương pháp chọn các feature nhằm xác định xem feature nào liên quan tới y
và feature nào là nhiễu. Ta sẽ tạo một ma trận feature gồm 16 feature độc lập như sau:
feature_names = [ 'linear', # 1 'nonlinear_square', # 2 'nonlinear_sin', # 3 'interaction_1', # 4 'interaction_2', # 5 'interaction_3', # 6 'noise_1', # 7 'noise_2', # 8 'noise_3', # 9 'noise_4', # 10 'noise_5', # 11 'noise_6', # 12 'noise_7', # 13 'noise_8', # 14 'noise_9', # 15 'noise_10' # 16
]
Dựa vào tên thì chắc hẳn bạn đã đoán được vai trò của các feature đối với y
rồi nhỉ 6 feature đầu tiên có mối quan hệ với y
, 10 feature còn lại là nhiễu.
Dạng hàm của y
phải đủ phức tạp để bao gồm một số tác động không tầm thường tồn tại trong tự nhiên giữa các biến. Đặc biệt, ta cần xét đến các mối quan hệ như sau:
- Linear
- Non linear
- Interactions
Ngoài ra, mối quan hệ giữa x
và y
nên là non-deteministic, do đó một hệ số sai số, được gọi là được thêm vào. Ta có thể xây dựng một hàm y
như sau:
Ta có thể chuyển sang code python như sau:
def X2y(X, with_error = True): # functional form of the dependence between y and X y_star = X['linear'] + X['nonlinear_square'] ** 2 + np.sin(3 * X['nonlinear_sin']) + (X['interaction_1'] * X['interaction_2'] * X['interaction_3']) # add random error called epsilon (this will be used for creating y) if with_error: np.random.seed(0) epsilon = np.random.normal(0, .1, len(y_star)) return y_star + epsilon # do not add error (this will be used for prediction) else: return y_star
Okay! Giờ ta sẽ bắt đầu gen X và y, sau đó chia thành tập train và tập test.
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split # make X and y
np.random.seed(0)
X = pd.DataFrame(np.random.normal(size = (20_000, len(feature_names))), columns = feature_names)
y = X2y(X, with_error = True) # make X_trn, X_tst, y_trn, y_tst
X_trn, X_tst, y_trn, y_tst = train_test_split(X, y, test_size = .5, random_state = 0)
Bật chế độ "SelectFromModel"
Giờ ta đã có trong tay data rồi, công việc tiếp theo sẽ là chọn một số model dự đoán và thử chạy chúng. Ta sẽ lấy thử 8 model sau:
from sklearn.dummy import DummyRegressor
from sklearn.linear_model import LinearRegression
from sklearn.neighbors import KNeighborsRegressor
from sklearn.svm import SVR
from sklearn.ensemble import RandomForestRegressor
from xgboost import XGBRegressor
from lightgbm import LGBMRegressor # dictionary of models that will be used for comparison
models = { 'DummyRegressor': DummyRegressor(), 'LinearRegression': LinearRegression(), 'KNeighborsRegressor': KNeighborsRegressor(n_neighbors = int(np.sqrt(len(X_trn)))), 'SupportVectorRegressor': SVR(C = .1), 'RandomForestRegressor': RandomForestRegressor(max_depth = 5), 'XGBRegressor': XGBRegressor(max_depht = 5), 'LGBMRegressor': LGBMRegressor(num_leaves = 10), 'UnbeatableRegressor': UnbeatableRegressor()
}
Nhìn đoạn code trên, bạn có thể sẽ hỏi UnbeatableRegressor
là gì vậy? Ta gọi đó là một model hoàn hảo bởi vì bản chất đây chính là công thức hàm y
mà ta đã xây dựng ở trên, đó chính là công thức hàm ban đầu nên chắc chắn không thể sai đi đâu được rồi
Để có một regressor thích hợp ta cần bọc hàm trong 1 class:
# define a sklearn compatible wrapper for our data generating function
class UnbeatableRegressor(): def __init__(self): pass def fit(self, X, y): pass def predict(self, X): return np.array(X2y(X, with_error = False)) def score(self, X, y): return mean_absolute_error(y, self.predict(X))
Tiếp theo ta sẽ fit các model với dữ liệu training và thử xem kết quả mean absolute error sẽ như nào nhé
from sklearn.metrics import mean_absolute_error
from eli5.sklearn import PermutationImportance mae = pd.DataFrame(columns = ['train', 'test'])
fi = pd.DataFrame(columns = feature_names) for model_name in list(models.keys()): # fit model models[model_name].fit(X_trn, y_trn) # compute mean absolute error of model in train and test set mae.loc[model_name,:] = [mean_absolute_error(y_trn, models[model_name].predict(X_trn)), mean_absolute_error(y_tst, models[model_name].predict(X_tst))] # compute feature importances of model try: feature_importances_ = models[model_name].feature_importances_ except: feature_importances_ = PermutationImportance(models[model_name], cv = 'prefit', n_iter = 3).fit(X_trn, y_trn).feature_importances_ fi.loc[model_name, :] = feature_importances_ / feature_importances_.sum() fi.fillna(0, inplace = True)
Kết quả ta thu được như sau:
Các model ML như XGBoost
và LightGBM
thể hiện performance ngon hơn các model còn lại mặc dù 2 model này vẫn còn cách khá xa mức tối thiểu error về mặt lý thuyết (thu được từ "model" UnbeatableRegressor). Okay! Giờ ta sẽ tập trung xem xem feature nào là quan trọng, việc này được thực hiện bởi "SelectFromModel".
Tầm quan trọng của một feature là một khái niệm khá khó nắm bắt trong học máy, có nghĩa là không có cách tính toán cụ thể nào. Mặc dù vậy, ý tưởng ta sử dụng sẽ khá trực quan: Định lượng mức quan trọng của 1 feature bằng cách xem xem nó đóng góp như nào với độ chính xác của mô hình.
Feature có giá trị 0% khi không ảnh hưởng đến độ chính xác của mô hình theo bất kỳ cách nào (vì vậy nó có thể bị loại bỏ). Feature có giá trị 100% khi là feature duy nhất ảnh hưởng đến các dự đoán. Trong các ứng dụng thực tế, các feature không bao giờ có mức quan trọng là 0% hoặc 100%, chúng thường nằm ở giữa 2 giá trị này.
Tuy nhiên, vì ta chỉ có 16 feature và 10 trong số chúng (noise) không có tác động gì với y
nên ta có thể mong đợi rằng mức độ quan trọng của 10 feature này là 0%. 6 feature còn lại sẽ chia sẻ mức độ quan trọng với model
Hmm! Cơ mà 0% có vẻ là một con số cực đoan. Để an toàn hơn thì ta sẽ lấy giá trị 1%, tức là ta sẽ bỏ những feature có mức độ quan trọng dưới 1%. Giá trị threshold này khá ngẫu nhiên nhưng khi xét với chỉ 16 feature, nó có vẻ khá hợp lý khi cho rằng feature có mức độ quan trọng dưới 1% là không hữu ích.
Ảnh dưới cho ta thấy được mức độ quan trọng của các feature trong 8 model. Các feature có mức độ quan trọng dưới 1% được đánh dấu là "[DROP]" ở gần tên.
Đáng ngạc nhiên là một trong những mô hình tệ nhất - KNeighborsRegressor - là mô hình duy nhất đoán đúng tất cả các feature liên quan (tuy nhiên, ta để ý rằng feature nonlinear_sin ở trạng thái "hấp hối" , vì mức độ quan trọng của nó là 1,17%, giá trị threshold chỉ cần khác đi một chút có thể làm feature này bị drop ).
Model có mức độ quan trọng của feature gần với giá trị thực nhất (UnbeatableRegressor) là LGBMRegressor, tuy nhiên, ta quan sát thấy có nhiều noise feature được đánh giá có mức quan trọng quá cao theo nghĩa là giá trị của chúng trên 1%.
Qua thực nghiệm trên, ta có một nhận xét quan trọng là một model tốt khi đánh giá theo performance không nhất thiết là model tốt khi đánh giá theo việc lựa chọn feature. Hay nói một cách khác, một model xuất sắc không đảm bảo cho việc lựa chọn feature thành công Ngoài ra, thực nghiệm này bị nhạy cảm với việc chọn threshold: một threshold khác sẽ cho một kết quả hoàn toàn khác.
Vì những lý do này, chúng ta hãy thử tìm kiếm một giải pháp thay thế. Một cách tiếp cận hay ho đó là sử dụng Boruta.
Sử dụng Boruta cho trích chọn feature
Boruta là thuật toán được sử dụng cho việc lựa chọn feature được đề xuất từ năm 2010. Ý tưởng đằng sau Boruta thật sự rất thông minh. Trong giới hạn bài viết này, mình sẽ giới hạn các kiến thức chuyên sâu trong Boruta và sử dụng thuật toán này bằng cách import một thư viện Python có sẵn.
Boruta là một wrapper. Ta sẽ wrap một model random forest với max_depth = 5
như sau:
!pip install Boruta
from boruta import BorutaPy # instantiate random forest
forest = RandomForestRegressor(n_jobs = -1, max_depth = 5) # fit boruta
boruta_selector = BorutaPy(forest, n_estimators = 'auto', random_state = 0)
boruta_selector.fit(np.array(X_trn), np.array(y_trn)) # store results
boruta_ranking = boruta_selector.ranking_
selected_features = np.array(feature_names)[boruta_ranking <= 2]
Bạn có thể sẽ tự hỏi "tại sao lại là RandomForestRegressor
? Trong thực nghiệm trước, đây là một trong model có performance tệ nhất mà nhỉ?". Đây chính là vẻ đẹp của Boruta: Mặc dù wrap một model yếu như random forest nhưng nó vẫn có thể khắc phục được điều này.
Output của Boruta là một feature ranking giúp bạn có thể chia nhỏ các feature thành 3 loại:
- Ranking 1: Confirmed features (các feature này mang một số tín hiệu liên quan đến biến mục tiêu, vì vậy chúng nên được giữ lại)
- Ranking 2: Tentative features (Boruta do dự về những feature này, sự lựa chọn là tùy thuộc vào bạn)
- Ranking 3 hoặc cao hơn: Rejected features (Các feature trong ranking này là nhiễu)
Ta sẽ visualize ranking của các feature như sau:
Boruta thật sự hoạt động tốt. Thuật toán này phân loại đúng 6 feature có nghĩa và 10 feature là noise như ta mong đợi.
Tổng kết
Trong bài viết này, chúng ta đã sử dụng dữ liệu mô phỏng để chứng minh rằng ngay cả các mô hình phức tạp như Xgboost hoặc LightGBM có thể không phải là lựa chọn tốt khi sử dụng cho lựa chọn tfeature. Để thay thế, bài viết đã đề xuất một package Python khá mạnh nhưng chưa được biết đến có tên là Boruta, thuật toán này đã mang lại kết quả mong muốn. Nếu bạn muốn tìm hiểu thuật toán Boruta hoạt động như nào, hãy tiếp tục theo dõi bài viết tiếp theo nhé.