Lựa chọn đặc trưng (feature selection) là một giai đoạn quan trọng đối với các nhà khoa học dữ liệu và kỹ sư học máy. Việc chọn lựa đúng các đặc trưng không chỉ giúp cải thiện hiệu suất mô hình mà còn sâu sắc hơn vào việc hiểu rõ dữ liệu, cấu trúc tiềm ẩn của chúng, từ đó hỗ trợ tối ưu hóa mô hình và thuật toán.
Mục tiêu chính của việc lựa chọn đặc trưng bao gồm:
- Giảm số lượng đặc trưng và chiều dữ liệu, giúp mô hình tổng quát hóa tốt hơn, giảm thiểu hiện tượng quá khớp (overfitting).
- Nâng cao khả năng diễn giải và hiểu biết về các đặc trưng cũng như mối quan hệ giữa chúng.
Trong thực tế, việc đạt được cả hai mục tiêu này cùng lúc thường không dễ dàng chỉ với một phương pháp duy nhất. Nhiều khi, chúng ta có xu hướng chọn phương pháp quen thuộc nhất hoặc tiện lợi nhất, thường chỉ tập trung vào việc giảm chiều mà bỏ qua mục tiêu hiểu dữ liệu.
Phần lớn các tài liệu về học máy ít khi đề cập sâu về lựa chọn đặc trưng, vì nó thường được coi là một khía cạnh phụ của quá trình xây dựng mô hình. Bài viết này sẽ trình bày một số phương pháp lựa chọn đặc trưng phổ biến, dựa trên các ví dụ minh họa bằng thư viện Scikit-learn, cùng với phân tích ưu nhược điểm của từng phương pháp.
1. Loại Bỏ Đặc Trưng Có Phương Sai Thấp
Đây là phương pháp lựa chọn đặc trưng đơn giản nhất. Nếu một đặc trưng chỉ có hai giá trị (ví dụ: 0 và 1) và 95% các mẫu đều nhận giá trị 1, thì đặc trưng đó có thể không mang nhiều thông tin. Nếu 100% các mẫu nhận cùng một giá trị, đặc trưng đó hoàn toàn vô nghĩa. Phương pháp này chỉ áp dụng trực tiếp cho các biến rời rạc. Đối với biến liên tục, cần phải rời rạc hóa trước khi áp dụng. Tuy nhiên, trong thực tế, ít khi có đặc trưng nào mà hơn 95% các giá trị trùng nhau, làm cho phương pháp này tuy đơn giản nhưng không thực sự hiệu quả. Nó có thể được sử dụng như một bước tiền xử lý để loại bỏ các đặc trưng ít biến đổi, sau đó kết hợp với các phương pháp phức tạp hơn.
2. Lựa Chọn Đặc Trưng Đơn Biến (Univariate Feature Selection)
Các phương pháp lựa chọn đặc trưng đơn biến đánh giá từng đặc trưng riêng lẻ và mối quan hệ của nó với biến phản hồi (biến mục tiêu). Dựa trên điểm số này, các đặc trưng kém quan trọng sẽ bị loại bỏ. Đối với bài toán hồi quy và phân loại, các kỹ thuật như kiểm định Chi-bình phương hoặc kiểm định Anova có thể được sử dụng.
Ưu điểm của phương pháp này là tính đơn giản, dễ triển khai và dễ hiểu, thường mang lại cái nhìn tốt về dữ liệu (mặc dù không phải lúc nào cũng tối ưu cho việc cải thiện hiệu suất mô hình). Có nhiều biến thể và cải tiến của phương pháp này.
2.1 Hệ số Tương Quan Pearson
Hệ số tương quan Pearson là một trong những cách đơn giản nhất để đánh giá mối quan hệ tuyến tính giữa một đặc trưng và biến phản hồi. Giá trị của hệ số này nằm trong khoảng [-1, 1], với -1 biểu thị tương quan nghịch hoàn hảo, +1 biểu thị tương quan thuận hoàn hảo, và 0 biểu thị không có mối quan hệ tuyến tính.
Hàm pearsonr từ scipy.stats có thể tính toán cả hệ số tương quan và p-value:
import numpy as np
from scipy.stats import pearsonr
np.random.seed(0)
so_mau = 300
du_lieu_goc = np.random.normal(0, 1, so_mau)
print(f"Tiếng ồn thấp: {pearsonr(du_lieu_goc, du_lieu_goc + np.random.normal(0, 1, so_mau))}")
print(f"Tiếng ồn cao: {pearsonr(du_lieu_goc, du_lieu_goc + np.random.normal(0, 10, so_mau))}")
Trong ví dụ trên, khi tiếng ồn thấp, tương quan rất mạnh và p-value rất nhỏ. Hàm f_regression của Scikit-learn cũng rất tiện lợi để tính p-value cho nhiều đặc trưng cùng lúc, thường được sử dụng trong pipeline.
Một hạn chế rõ ràng của hệ số tương quan Pearson là nó chỉ nhạy cảm với các mối quan hệ tuyến tính. Nếu mối quan hệ là phi tuyến tính, ngay cả khi hai biến có mối quan hệ một-một chặt chẽ, hệ số Pearson vẫn có thể gần bằng 0.
import numpy as np
from scipy.stats import pearsonr
bien_x = np.random.uniform(-1, 1, 100000)
print(f"Pearson correlation giữa x và x^2: {pearsonr(bien_x, bien_x**2)[0]}")
Kết quả sẽ gần 0, cho thấy Pearson không phát hiện được mối quan hệ phi tuyến tính mạnh mẽ này. Để tránh những kết luận sai lệch, đặc biệt là với các trường hợp như Anscombe's quartet, việc trực quan hóa dữ liệu là rất quan trọng.
2.2 Thông Tin Tương Hỗ Cực Đại (Maximal Information Coefficient - MIC)
Thông tin tương hỗ (Mutual Information) là một thước đo mạnh mẽ hơn Pearson, có khả năng phát hiện cả mối quan hệ phi tuyến tính. Công thức kinh điển của thông tin tương hỗ là:
$$I(X;Y) = \sum_{y \in Y} \sum_{x \in X} p(x,y) \log \left( \frac{p(x,y)}{p(x)p(y)} \right)$$
Tuy nhiên, việc sử dụng thông tin tương hỗ trực tiếp cho lựa chọn đặc trưng không phải lúc nào cũng tiện lợi vì:
- Nó không phải là một độ đo chuẩn hóa và không thể so sánh kết quả giữa các tập dữ liệu khác nhau.
- Đối với biến liên tục, việc tính toán không dễ dàng (X và Y là các tập hợp, x và y là các giá trị rời rạc), thường yêu cầu rời rạc hóa biến, và kết quả rất nhạy cảm với cách rời rạc hóa.
Thư viện minepy cung cấp chức năng tính MIC, giúp khắc phục một số vấn đề trên, nhưng vẫn có những tranh cãi về tính ổn định thống kê của MIC trong một số trường hợp.
2.3 Tương Quan Khoảng Cách (Distance Correlation)
Tương quan khoảng cách ra đời để khắc phục điểm yếu của hệ số tương quan Pearson. Trong ví dụ về $X$ và $X^2$, ngay cả khi Pearson bằng 0, chúng ta không thể kết luận rằng hai biến độc lập (có thể có mối quan hệ phi tuyến). Nhưng nếu tương quan khoảng cách bằng 0, chúng ta có thể khẳng định hai biến đó độc lập.
Gói energy (trong R) hoặc các triển khai tương tự trong Python (ví dụ: Python gist) cung cấp chức năng này. Ví dụ trong R:
#R-code
x = runif (1000, -1, 1)
dcor(x, x**2)
# [1] 0.4943864
Mặc dù MIC và tương quan khoảng cách có khả năng phát hiện mối quan hệ phi tuyến, Pearson vẫn không thể thay thế khi mối quan hệ gần tuyến tính. Thứ nhất, Pearson nhanh hơn trong tính toán, điều quan trọng với dữ liệu lớn. Thứ hai, Pearson có phạm vi giá trị [-1, 1], biểu thị cả chiều (dương/âm) và cường độ của mối quan hệ, trong khi MIC và tương quan khoảng cách chỉ có phạm vi [0, 1]. Tuy nhiên, Pearson chỉ hiệu quả khi mối quan hệ là đơn điệu.
2.4 Lựa Chọn Đặc Trưng Đơn Biến Dựa Trên Mô Hình
Phương pháp này sử dụng trực tiếp thuật toán học máy dự kiến của bạn để xây dựng một mô hình dự đoán cho từng đặc trưng và biến phản hồi. Hệ số tương quan Pearson có thể được xem là trường hợp đặc biệt của hệ số hồi quy chuẩn hóa trong hồi quy tuyến tính. Nếu mối quan hệ là phi tuyến tính, bạn có thể sử dụng các phương pháp dựa trên cây (như Cây quyết định, Rừng ngẫu nhiên) hoặc các mô hình tuyến tính mở rộng. Các phương pháp dựa trên cây dễ sử dụng vì chúng mô hình hóa tốt các mối quan hệ phi tuyến tính và không yêu cầu nhiều tinh chỉnh. Cần chú ý đến vấn đề quá khớp, do đó nên giới hạn độ sâu của cây và sử dụng kỹ thuật kiểm định chéo.
Ví dụ sử dụng Random Forest Regressor của Scikit-learn trên tập dữ liệu giá nhà Boston:
from sklearn.model_selection import ShuffleSplit, cross_val_score
from sklearn.datasets import load_boston
from sklearn.ensemble import RandomForestRegressor
import numpy as np
# Tải tập dữ liệu giá nhà Boston
du_lieu_boston = load_boston()
dac_trung = du_lieu_boston["data"]
bien_muc_tieu = du_lieu_boston["target"]
ten_dac_trung = du_lieu_boston["feature_names"]
rf_model = RandomForestRegressor(n_estimators=20, max_depth=4, random_state=42)
diem_so = []
for i in range(dac_trung.shape[1]):
cv_splitter = ShuffleSplit(n_splits=3, test_size=0.3, random_state=42)
diem = cross_val_score(rf_model, dac_trung[:, i:i+1], bien_muc_tieu, scoring="r2", cv=cv_splitter)
diem_so.append((round(np.mean(diem), 3), ten_dac_trung[i]))
print("Điểm số đặc trưng (R^2 trung bình) được sắp xếp giảm dần:")
print(sorted(diem_so, reverse=True))
Trong khi lựa chọn đặc trưng đơn biến đánh giá độc lập từng đặc trưng, một phương pháp lựa chọn đặc trưng phổ biến khác là dựa trên mô hình học máy toàn diện hơn. Một số thuật toán học máy tự thân đã có cơ chế đánh giá mức độ quan trọng của đặc trưng, hoặc có thể dễ dàng áp dụng cho nhiệm vụ này, ví dụ như mô hình hồi quy, SVM, cây quyết định, rừng ngẫu nhiên. Các phương pháp này đôi khi được gọi là "wrapper methods" vì mô hình lựa chọn đặc trưng được "gói" cùng với mô hình học máy chính, trái ngược với "filter methods" như các phương pháp đơn biến.
3. Lựa Chọn Đặc Trưng Dựa Trên Mô Hình
Phần này sẽ giới thiệu cách sử dụng các hệ số từ mô hình hồi quy để chọn đặc trưng. Các đặc trưng quan trọng thường có hệ số lớn hơn trong mô hình, trong khi các đặc trưng không liên quan đến biến đầu ra sẽ có hệ số gần 0. Với dữ liệu ít nhiễu hoặc số lượng mẫu lớn hơn đáng kể so với số đặc trưng, và nếu các đặc trưng tương đối độc lập, ngay cả một mô hình hồi quy tuyến tính đơn giản cũng có thể hoạt động rất tốt.
from sklearn.linear_model import LinearRegression
import numpy as np
np.random.seed(0)
kich_thuoc = 5000
# Tập dữ liệu với 3 đặc trưng độc lập
dac_trung = np.random.normal(0, 1, (kich_thuoc, 3))
# Biến mục tiêu Y = X0 + 2*X1 + nhiễu
bien_muc_tieu = dac_trung[:,0] + 2*dac_trung[:,1] + np.random.normal(0, 2, kich_thuoc)
mo_hinh_lr = LinearRegression()
mo_hinh_lr.fit(dac_trung, bien_muc_tieu)
# Hàm hỗ trợ hiển thị mô hình tuyến tính đẹp mắt
def hien_thi_mo_hinh_tuyen_tinh_dep(he_so_list, ten_dac_trung=None, sap_xep=False):
if ten_dac_trung is None:
ten_dac_trung = [f"X{i}" for i in range(len(he_so_list))]
danh_sach_cap = list(zip(he_so_list, ten_dac_trung))
if sap_xep:
danh_sach_cap = sorted(danh_sach_cap, key=lambda x: -abs(x[0]))
return " + ".join(f"{round(hs, 3)} * {ten}" for hs, ten in danh_sach_cap)
print(f"Mô hình tuyến tính: {hien_thi_mo_hinh_tuyen_tinh_dep(mo_hinh_lr.coef_)}")
Trong ví dụ này, mặc dù có nhiễu, mô hình lựa chọn đặc trưng vẫn phản ánh tốt cấu trúc dữ liệu tiềm ẩn, vì bài toán này rất phù hợp với mô hình tuyến tính (tất cả các mối quan hệ đều tuyến tính và các đặc trưng độc lập).
Trong nhiều bộ dữ liệu thực tế, thường có nhiều đặc trưng có mối tương quan với nhau (multicollinearity). Điều này có thể làm cho mô hình trở nên không ổn định, với những thay đổi nhỏ trong dữ liệu có thể dẫn đến sự biến động lớn trong các hệ số của mô hình. Điều này gây khó khăn cho việc diễn giải và dự đoán của mô hình. Ví dụ, nếu mô hình thực sự là Y = X1 + X2, nhưng X1 và X2 có mối quan hệ tuyến tính chặt chẽ (X1 ≈ X2), thì do nhiễu, mô hình học được có thể là Y = 2X1 hoặc Y = -X1 + 3X2, thay vì Y = X1 + X2.
Ví dụ dưới đây minh họa tác động của các đặc trưng tương quan khi sử dụng hồi quy tuyến tính:
from sklearn.linear_model import LinearRegression
import numpy as np
kich_thuoc = 100
np.random.seed(seed=5)
nguon_goc_x = np.random.normal(0, 1, kich_thuoc)
x1 = nguon_goc_x + np.random.normal(0, .1, kich_thuoc)
x2 = nguon_goc_x + np.random.normal(0, .1, kich_thuoc)
x3 = nguon_goc_x + np.random.normal(0, .1, kich_thuoc)
muc_tieu_y = x1 + x2 + x3 + np.random.normal(0,1, kich_thuoc)
cac_dac_trung = np.array([x1, x2, x3]).T
mo_hinh_lr = LinearRegression()
mo_hinh_lr.fit(cac_dac_trung, muc_tieu_y)
print(f"Mô hình tuyến tính: {hien_thi_mo_hinh_tuyen_tinh_dep(mo_hinh_lr.coef_)}")
Tổng các hệ số gần bằng 3, cho thấy khả năng dự đoán của mô hình khá tốt. Tuy nhiên, nếu diễn giải trực tiếp các hệ số, có thể thấy X3 có ảnh hưởng dương mạnh mẽ, trong khi X1 có ảnh hưởng âm, mặc dù thực tế tất cả các đặc trưng đều có ảnh hưởng như nhau đến biến mục tiêu. Phương pháp tương tự cũng có thể áp dụng cho các mô hình tuyến tính khác như hồi quy logistic.
3.1 Chuẩn Hóa (Regularization)
Chuẩn hóa là kỹ thuật thêm các ràng buộc hoặc thành phần phạt vào hàm mất mát của mô hình để ngăn chặn quá khớp và cải thiện khả năng tổng quát hóa. Hàm mất mát từ $E(X,Y)$ sẽ trở thành $E(X,Y) + \alpha ||w||$, trong đó $w$ là vector hệ số của mô hình, $||\cdot||$ thường là chuẩn L1 hoặc L2, và $\alpha$ là một siêu tham số điều khiển cường độ chuẩn hóa. Khi áp dụng cho mô hình tuyến tính, chuẩn hóa L1 và L2 còn được gọi là Lasso và Ridge.
3.2 Chuẩn Hóa L1 (Lasso Regression)
Chuẩn hóa L1 thêm chuẩn L1 của vector hệ số $w$ làm thành phần phạt. Vì thành phần phạt này không bằng 0, nó buộc các hệ số của các đặc trưng yếu trở thành 0. Do đó, chuẩn hóa L1 thường tạo ra các mô hình rất "thưa" (sparse), nghĩa là nhiều hệ số $w$ sẽ bằng 0. Đặc tính này làm cho chuẩn hóa L1 trở thành một phương pháp lựa chọn đặc trưng rất hiệu quả.
Ví dụ dưới đây chạy Lasso trên tập dữ liệu giá nhà Boston, với tham số $\alpha$ được tối ưu thông qua tìm kiếm lưới (grid search):
from sklearn.linear_model import Lasso
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import load_boston
du_lieu_boston = load_boston()
bo_chuan = StandardScaler()
cac_dac_trung_sc = bo_chuan.fit_transform(du_lieu_boston["data"])
bien_muc_tieu = du_lieu_boston["target"]
ten_dac_trung = du_lieu_boston["feature_names"]
mo_hinh_lasso = Lasso(alpha=.3, random_state=42)
mo_hinh_lasso.fit(cac_dac_trung_sc, bien_muc_tieu)
print(f"Mô hình Lasso: {hien_thi_mo_hinh_tuyen_tinh_dep(mo_hinh_lasso.coef_, ten_dac_trung, sort=True)}")
Bạn có thể thấy nhiều hệ số đặc trưng bằng 0. Nếu tiếp tục tăng giá trị $\alpha$, mô hình sẽ càng trở nên thưa hơn, với nhiều hệ số đặc trưng hơn nữa trở thành 0.
3.3 Chuẩn Hóa L2 (Ridge Regression)
Chuẩn hóa L2 thêm chuẩn L2 của vector hệ số vào hàm mất mát. Do thành phần phạt L2 là bình phương của các hệ số, nó có nhiều điểm khác biệt so với L1, đáng chú ý nhất là L2 có xu hướng làm cho các giá trị hệ số trở nên đều đặn hơn. Đối với các đặc trưng tương quan, điều này có nghĩa là chúng sẽ nhận được các hệ số tương tự nhau hơn. Quay lại ví dụ Y = X1 + X2, giả sử X1 và X2 có tương quan mạnh. Nếu sử dụng L1, hình phạt sẽ giống nhau (ví dụ: $2\alpha$) cho cả mô hình Y = X1 + X2 và Y = 2X1. Nhưng với L2, mô hình đầu tiên có hình phạt là $2\alpha$, trong khi mô hình thứ hai là $4\alpha$. Điều này cho thấy khi tổng các hệ số là hằng số, hình phạt nhỏ nhất khi các hệ số bằng nhau, giải thích tại sao L2 làm các hệ số có xu hướng tương tự nhau.
Chuẩn hóa L2 tạo ra một mô hình ổn định hơn cho việc lựa chọn đặc trưng, không giống như L1, nơi các hệ số có thể dao động do những thay đổi nhỏ trong dữ liệu. Do đó, L2 và L1 mang lại giá trị khác nhau; L2 hữu ích hơn cho việc hiểu đặc trưng: các đặc trưng có khả năng biểu diễn mạnh mẽ sẽ có hệ số khác 0.
Hãy xem lại ví dụ về 3 đặc trưng tương quan mạnh, chạy 10 lần với các hạt giống ngẫu nhiên khác nhau để quan sát sự ổn định của L1 và L2:
from sklearn.linear_model import Ridge
from sklearn.metrics import r2_score
import numpy as np
kich_thuoc = 100
# Chạy phương pháp 10 lần với các hạt giống ngẫu nhiên khác nhau
for i in range(10):
print(f"Hạt giống ngẫu nhiên {i}")
np.random.seed(seed=i)
nguon_goc_x = np.random.normal(0, 1, kich_thuoc)
x1 = nguon_goc_x + np.random.normal(0, .1, kich_thuoc)
x2 = nguon_goc_x + np.random.normal(0, .1, kich_thuoc)
x3 = nguon_goc_x + np.random.normal(0, .1, kich_thuoc)
muc_tieu_y = x1 + x2 + x3 + np.random.normal(0, 1, kich_thuoc)
cac_dac_trung = np.array([x1, x2, x3]).T
mo_hinh_lr = LinearRegression()
mo_hinh_lr.fit(cac_dac_trung, muc_tieu_y)
print(f"Mô hình tuyến tính: {hien_thi_mo_hinh_tuyen_tinh_dep(mo_hinh_lr.coef_)}")
mo_hinh_ridge = Ridge(alpha=10)
mo_hinh_ridge.fit(cac_dac_trung, muc_tieu_y)
print(f"Mô hình Ridge: {hien_thi_mo_hinh_tuyen_tinh_dep(mo_hinh_ridge.coef_)}")
print("-" * 30)
Bạn có thể thấy rằng các mô hình hồi quy tuyến tính (hệ số) thu được từ các tập dữ liệu khác nhau có sự khác biệt đáng kể. Tuy nhiên, đối với mô hình chuẩn hóa L2 (Ridge), các hệ số trong kết quả rất ổn định, ít khác biệt, đều gần bằng 1, phản ánh cấu trúc nội tại của dữ liệu.
4. Rừng Ngẫu Nhiên (Random Forest)
Rừng ngẫu nhiên nổi tiếng với độ chính xác cao, khả năng chịu lỗi tốt và dễ sử dụng, khiến nó trở thành một trong những thuật toán học máy phổ biến nhất hiện nay. Rừng ngẫu nhiên cung cấp hai phương pháp lựa chọn đặc trưng chính: giảm độ tạp trung bình (Mean Decrease Impurity) và giảm độ chính xác trung bình (Mean Decrease Accuracy).
4.1 Giảm Độ Tạp Trung Bình (Mean Decrease Impurity)
Một rừng ngẫu nhiên bao gồm nhiều cây quyết định. Mỗi nút trong cây quyết định là một điều kiện về một đặc trưng, nhằm phân chia tập dữ liệu thành hai phần dựa trên biến phản hồi. Độ tạp (impurity) được sử dụng để xác định nút tối ưu; đối với bài toán phân loại, thường dùng Gini impurity hoặc Information Gain, còn đối với hồi quy, thường dùng phương sai hoặc khớp bình phương tối thiểu. Khi huấn luyện cây quyết định, có thể tính toán mức độ mỗi đặc trưng làm giảm độ tạp của cây. Đối với một rừng ngẫu nhiên, chúng ta có thể tính trung bình mức độ giảm độ tạp do mỗi đặc trưng gây ra trên tất cả các cây, và sử dụng giá trị này làm điểm số quan trọng của đặc trưng.
from sklearn.datasets import load_boston
from sklearn.ensemble import RandomForestRegressor
# Tải tập dữ liệu giá nhà Boston
du_lieu_boston = load_boston()
dac_trung = du_lieu_boston["data"]
bien_muc_tieu = du_lieu_boston["target"]
ten_dac_trung = du_lieu_boston["feature_names"]
rf_model = RandomForestRegressor(random_state=42)
rf_model.fit(dac_trung, bien_muc_tieu)
print("Các đặc trưng được sắp xếp theo điểm số quan trọng (Gini Importance):")
diem_quan_trong = sorted(zip(map(lambda x: round(x, 4), rf_model.feature_importances_), ten_dac_trung),
reverse=True)
print(diem_quan_trong)
Điểm số đặc trưng ở đây thực chất là Gini Importance. Khi sử dụng phương pháp dựa trên độ tạp, cần nhớ rằng: 1) Phương pháp này có thể có thiên vị, ưu tiên các biến có nhiều loại giá trị hơn. 2) Đối với nhiều đặc trưng có mối tương quan, bất kỳ đặc trưng nào trong số đó đều có thể được chọn làm chỉ báo (đặc trưng ưu việt). Khi một đặc trưng được chọn, mức độ quan trọng của các đặc trưng tương quan khác sẽ giảm mạnh vì độ tạp đã được giảm đáng kể bởi đặc trưng đã chọn. Điều này có thể dẫn đến hiểu lầm, cho rằng đặc trưng được chọn đầu tiên rất quan trọng trong khi các đặc trưng tương quan khác thì không, mặc dù thực tế chúng có ảnh hưởng tương đương đến biến phản hồi (tương tự như Lasso). Các phương pháp chọn ngẫu nhiên đặc trưng một phần nào đó giảm nhẹ vấn đề này, nhưng không giải quyết hoàn toàn.
Ví dụ dưới đây với X0, X1, X2 là ba biến tương quan mạnh, và biến mục tiêu là tổng của chúng (không nhiễu):
import numpy as np
from sklearn.ensemble import RandomForestRegressor
kich_thuoc = 10000
np.random.seed(seed=10)
nguon_goc_x = np.random.normal(0, 1, kich_thuoc)
x0 = nguon_goc_x + np.random.normal(0, .1, kich_thuoc)
x1 = nguon_goc_x + np.random.normal(0, .1, kich_thuoc)
x2 = nguon_goc_x + np.random.normal(0, .1, kich_thuoc)
cac_dac_trung = np.array([x0, x1, x2]).T
muc_tieu_y = x0 + x1 + x2
rf_model = RandomForestRegressor(n_estimators=20, max_features=2, random_state=42)
rf_model.fit(cac_dac_trung, muc_tieu_y)
print(f"Điểm số cho X0, X1, X2: {list(map(lambda x: round(x,3), rf_model.feature_importances_))}")
Khi tính toán độ quan trọng của đặc trưng, bạn có thể thấy X1 có độ quan trọng cao hơn X2 gấp 10 lần, nhưng thực tế chúng có độ quan trọng như nhau. Ngay cả với dữ liệu lớn, không nhiễu và sử dụng 20 cây để chọn ngẫu nhiên, vấn đề này vẫn tồn tại. Cần lưu ý rằng sự không ổn định trong việc đánh giá các đặc trưng tương quan không chỉ là đặc điểm riêng của rừng ngẫu nhiên mà còn xảy ra ở hầu hết các phương pháp lựa chọn đặc trưng dựa trên mô hình.
4.2 Giảm Độ Chính Xác Trung Bình (Mean Decrease Accuracy / Permutation Importance)
Một phương pháp lựa chọn đặc trưng phổ biến khác là trực tiếp đo lường ảnh hưởng của từng đặc trưng đến độ chính xác của mô hình. Ý tưởng chính là xáo trộn thứ tự các giá trị của một đặc trưng cụ thể và sau đó đo lường tác động của sự thay đổi này đến độ chính xác của mô hình. Rõ ràng, đối với các biến không quan trọng, việc xáo trộn thứ tự sẽ không ảnh hưởng nhiều đến độ chính xác của mô hình, nhưng đối với các biến quan trọng, việc xáo trộn sẽ làm giảm đáng kể độ chính xác của mô hình.
Phương pháp này không được cung cấp trực tiếp trong Scikit-learn, nhưng rất dễ triển khai. Dưới đây là ví dụ trên tập dữ liệu giá nhà Boston:
from sklearn.model_selection import ShuffleSplit
from sklearn.metrics import r2_score
from collections import defaultdict
from sklearn.ensemble import RandomForestRegressor
from sklearn.datasets import load_boston
import numpy as np
du_lieu_boston = load_boston()
cac_dac_trung = du_lieu_boston["data"]
bien_muc_tieu = du_lieu_boston["target"]
ten_dac_trung = du_lieu_boston["feature_names"]
rf_model = RandomForestRegressor(random_state=42)
diem_so_hoan_vi = defaultdict(list)
# Kiểm định chéo điểm số trên nhiều lần chia ngẫu nhiên dữ liệu
cv_splitter = ShuffleSplit(n_splits=100, test_size=0.3, random_state=42)
for train_idx, test_idx in cv_splitter.split(cac_dac_trung):
X_train, X_test = cac_dac_trung[train_idx], cac_dac_trung[test_idx]
Y_train, Y_test = bien_muc_tieu[train_idx], bien_muc_tieu[test_idx]
rf_model.fit(X_train, Y_train)
do_chinh_xac_goc = r2_score(Y_test, rf_model.predict(X_test))
for i in range(X_test.shape[1]):
X_test_xao_tron = X_test.copy()
np.random.shuffle(X_test_xao_tron[:, i])
do_chinh_xac_xao_tron = r2_score(Y_test, rf_model.predict(X_test_xao_tron))
diem_so_hoan_vi[ten_dac_trung[i]].append((do_chinh_xac_goc - do_chinh_xac_xao_tron) / do_chinh_xac_goc)
print("Các đặc trưng được sắp xếp theo điểm số quan trọng (Mean Decrease Accuracy):")
ket_qua_sap_xep = sorted([(round(np.mean(score), 4), feat) for feat, score in diem_so_hoan_vi.items()], reverse=True)
print(ket_qua_sap_xep)
Trong ví dụ này, các đặc trưng LSTAT và RM có ảnh hưởng lớn đến hiệu suất mô hình; việc xáo trộn giá trị của chúng làm giảm hiệu suất mô hình lần lượt 73% và 57%. Lưu ý rằng, mặc dù chúng ta huấn luyện mô hình trên tất cả các đặc trưng để đánh giá tầm quan trọng của từng đặc trưng, điều này không có nghĩa là loại bỏ một hoặc một vài đặc trưng quan trọng sẽ làm giảm mạnh hiệu suất mô hình, vì các đặc trưng tương quan khác vẫn có thể bù đắp và giữ cho hiệu suất mô hình tương đối ổn định.
5. Hai Thuật Toán Lựa Chọn Đặc Trưng Nâng Cao
Các thuật toán này được gọi là "nâng cao" vì chúng được xây dựng trên nền tảng của các phương pháp lựa chọn đặc trưng dựa trên mô hình (như hồi quy hoặc SVM), bằng cách xây dựng mô hình trên các tập con khác nhau của dữ liệu và tổng hợp kết quả để xác định điểm số cuối cùng của đặc trưng.
5.1 Chọn Lọc Ổn Định (Stability Selection)
Chọn lọc ổn định là một phương pháp tương đối mới, kết hợp lấy mẫu lại (subsampling) với một thuật toán lựa chọn (có thể là hồi quy, SVM, hoặc các phương pháp tương tự). Ý tưởng chính là chạy thuật toán lựa chọn đặc trưng trên nhiều tập con của dữ liệu và đặc trưng, lặp đi lặp lại. Sau đó, tổng hợp kết quả, ví dụ bằng cách đếm tần suất một đặc trưng được coi là quan trọng (số lần được chọn làm đặc trưng quan trọng chia cho số lần tập con chứa đặc trưng đó được kiểm tra). Lý tưởng nhất, các đặc trưng quan trọng sẽ có điểm số gần 100%. Các đặc trưng yếu hơn sẽ có điểm số khác 0, và các đặc trưng vô ích nhất sẽ có điểm số gần 0.
Scikit-learn cung cấp triển khai chọn lọc ổn định thông qua RandomizedLasso và RandomizedLogisticRegression.
from sklearn.linear_model import RandomizedLasso
from sklearn.datasets import load_boston
import numpy as np
du_lieu_boston = load_boston()
# Dữ liệu sẽ được tự động chuẩn hóa bởi triển khai của sklearn
cac_dac_trung = du_lieu_boston["data"]
bien_muc_tieu = du_lieu_boston["target"]
ten_dac_trung = du_lieu_boston["feature_names"]
rlasso_model = RandomizedLasso(alpha=0.025, random_state=42)
rlasso_model.fit(cac_dac_trung, bien_muc_tieu)
print("Các đặc trưng được sắp xếp theo điểm số chọn lọc ổn định:")
ket_qua_sap_xep = sorted(zip(map(lambda x: round(x, 4), rlasso_model.scores_), ten_dac_trung), reverse=True)
print(ket_qua_sap_xep)
Trong ví dụ trên, ba đặc trưng hàng đầu có điểm số 1.0, cho thấy chúng luôn được chọn là hữu ích (điểm số có thể bị ảnh hưởng bởi tham số chuẩn hóa alpha, nhưng RandomizedLasso của Scikit-learn có thể tự động chọn alpha tối ưu). Các đặc trưng tiếp theo có điểm số giảm dần nhưng không quá đột ngột, khác với phương pháp Lasso thuần túy và Random Forest. Chọn lọc ổn định hữu ích cho cả việc hiểu dữ liệu và chọn ra các đặc trưng chất lượng: các đặc trưng tốt sẽ không bị loại bỏ hoàn toàn (điểm số bằng 0) chỉ vì có các đặc trưng tương tự hoặc liên quan, điều này khác với Lasso. Trong nhiều bộ dữ liệu và môi trường, chọn lọc ổn định thường là một trong những phương pháp có hiệu suất tốt nhất cho nhiệm vụ lựa chọn đặc trưng.
5.2 Loại Bỏ Đặc Trưng Đệ Quy (Recursive Feature Elimination - RFE)
Ý tưởng chính của Loại bỏ đặc trưng đệ quy là liên tục xây dựng một mô hình (ví dụ: SVM hoặc mô hình hồi quy) và chọn ra các đặc trưng tốt nhất (hoặc tệ nhất) – có thể dựa trên hệ số. Các đặc trưng đã chọn sẽ được loại bỏ khỏi tập dữ liệu hiện tại, và quá trình này lặp lại trên các đặc trưng còn lại cho đến khi tất cả các đặc trưng đã được duyệt qua. Thứ tự các đặc trưng bị loại bỏ chính là thứ tự xếp hạng của chúng. Do đó, đây là một thuật toán tham lam nhằm tìm kiếm tập con đặc trưng tối ưu.
Sự ổn định của RFE phụ thuộc nhiều vào mô hình cơ sở được sử dụng trong các lần lặp. Ví dụ, nếu RFE sử dụng hồi quy tuyến tính thông thường không được chuẩn hóa, RFE sẽ không ổn định; nếu sử dụng Ridge (hồi quy tuyến tính có chuẩn hóa Ridge), RFE sẽ ổn định. Scikit-learn cung cấp lớp RFE và RFECV (RFE với kiểm định chéo) để xếp hạng đặc trưng.
from sklearn.feature_selection import RFE
from sklearn.linear_model import LinearRegression
from sklearn.datasets import load_boston
import numpy as np
du_lieu_boston = load_boston()
cac_dac_trung = du_lieu_boston["data"]
bien_muc_tieu = du_lieu_boston["target"]
ten_dac_trung = du_lieu_boston["feature_names"]
# Sử dụng hồi quy tuyến tính làm mô hình cơ sở
mo_hinh_lr = LinearRegression()
# Xếp hạng tất cả các đặc trưng, tiếp tục loại bỏ cho đến đặc trưng cuối cùng
rfe_selector = RFE(mo_hinh_lr, n_features_to_select=1, step=1)
rfe_selector.fit(cac_dac_trung, bien_muc_tieu)
print("Các đặc trưng được sắp xếp theo thứ hạng (RFE):")
ket_qua_sap_xep = sorted(zip(map(float, rfe_selector.ranking_), ten_dac_trung))
print(ket_qua_sap_xep)
6. So Sánh Thực Nghiệm Các Phương Pháp Lựa Chọn Đặc Trưng
Phần này sẽ so sánh các phương pháp đã đề cập trên tập dữ liệu hồi quy Friedman #1. Dữ liệu được tạo ra từ công thức:
$$Y = 10 \sin(\pi X_0 X_1) + 20(X_2 - 0.5)^2 + 10 X_3 + 5 X_4 + \epsilon$$
Trong đó, $X_0$ đến $X_4$ được tạo từ phân phối đơn biến, và $\epsilon$ là nhiễu phân phối chuẩn $N(0,1)$. Tập dữ liệu gốc còn chứa 5 biến nhiễu $X_5, ..., X_9$ hoàn toàn độc lập với biến mục tiêu. Chúng ta sẽ thêm 4 biến bổ sung $X_{10}, ..., X_{13}$ là các biến tương quan với $X_0, ..., X_3$ thông qua công thức $f(x) = x + N(0, 0.01)$, tạo ra hệ số tương quan lớn hơn 0.999. Tập dữ liệu này giúp đánh giá hiệu suất của các phương pháp lựa chọn đặc trưng khi đối mặt với các đặc trưng tương quan và phi tuyến tính.
Tiếp theo, chúng ta sẽ chạy tất cả các phương pháp lựa chọn đặc trưng trên dữ liệu này và chuẩn hóa điểm số của mỗi phương pháp để chúng nằm trong khoảng [0, 1]. Đối với RFE, vì nó cung cấp thứ tự chứ không phải điểm số, chúng ta sẽ đặt điểm số cho 5 đặc trưng tốt nhất là 1, và các đặc trưng còn lại có điểm số phân bố đều từ 0 đến 1.
from sklearn.linear_model import (LinearRegression, Ridge, Lasso, RandomizedLasso)
from sklearn.feature_selection import RFE, f_regression
from sklearn.preprocessing import MinMaxScaler
from sklearn.ensemble import RandomForestRegressor
import numpy as np
from minepy import MINE
from collections import defaultdict
np.random.seed(0)
kich_thuoc = 750
# Bài toán hồi quy "Friedman #1"
cac_dac_trung = np.random.uniform(0, 1, (kich_thuoc, 14))
bien_muc_tieu = (10 * np.sin(np.pi * cac_dac_trung[:,0] * cac_dac_trung[:,1]) +
20 * (cac_dac_trung[:,2] - .5)**2 +
10 * cac_dac_trung[:,3] +
5 * cac_dac_trung[:,4] +
np.random.normal(0, 1, kich_thuoc))
# Thêm 4 biến tương quan bổ sung (tương quan với X0-X3)
cac_dac_trung[:,10:] = cac_dac_trung[:,:4] + np.random.normal(0, .025, (kich_thuoc,4))
ten_dac_trung = [f"X{i}" for i in range(1,15)]
diem_xep_hang = defaultdict(dict)
def chuan_hoa_xep_hang(diem_so, ten_dac_trung_list, dao_nguoc=False):
minmax_scaler = MinMaxScaler()
diem_so_chuan_hoa = minmax_scaler.fit_transform(np.array([diem_so]).T).T[0]
if dao_nguoc:
diem_so_chuan_hoa = 1 - diem_so_chuan_hoa # Để thứ hạng thấp nhất có điểm cao nhất
return {ten: round(diem, 2) for ten, diem in zip(ten_dac_trung_list, diem_so_chuan_hoa)}
# 1. Hồi quy tuyến tính
mo_hinh_lr = LinearRegression(normalize=True)
mo_hinh_lr.fit(cac_dac_trung, bien_muc_tieu)
diem_xep_hang["Hồi quy tuyến tính"] = chuan_hoa_xep_hang(np.abs(mo_hinh_lr.coef_), ten_dac_trung)
# 2. Ridge Regression
mo_hinh_ridge = Ridge(alpha=7)
mo_hinh_ridge.fit(cac_dac_trung, bien_muc_tieu)
diem_xep_hang["Ridge"] = chuan_hoa_xep_hang(np.abs(mo_hinh_ridge.coef_), ten_dac_trung)
# 3. Lasso Regression
mo_hinh_lasso = Lasso(alpha=.05)
mo_hinh_lasso.fit(cac_dac_trung, bien_muc_tieu)
diem_xep_hang["Lasso"] = chuan_hoa_xep_hang(np.abs(mo_hinh_lasso.coef_), ten_dac_trung)
# 4. Stability Selection (RandomizedLasso)
mo_hinh_rlasso = RandomizedLasso(alpha=0.04, random_state=42)
mo_hinh_rlasso.fit(cac_dac_trung, bien_muc_tieu)
diem_xep_hang["Ổn định"] = chuan_hoa_xep_hang(np.abs(mo_hinh_rlasso.scores_), ten_dac_trung)
# 5. RFE (loại bỏ cho đến khi còn 5 đặc trưng, chúng sẽ có điểm bằng nhau)
# Sử dụng xếp hạng để tạo điểm, thứ hạng thấp nhất (1) được điểm cao nhất (1)
rfe_selector = RFE(mo_hinh_lr, n_features_to_select=5, step=1)
rfe_selector.fit(cac_dac_trung, bien_muc_tieu)
diem_xep_hang["RFE"] = chuan_hoa_xep_hang(list(map(float, rfe_selector.ranking_)), ten_dac_trung, dao_nguoc=True)
# 6. Random Forest (Gini Importance)
rf_model = RandomForestRegressor(random_state=42)
rf_model.fit(cac_dac_trung, bien_muc_tieu)
diem_xep_hang["Random Forest"] = chuan_hoa_xep_hang(rf_model.feature_importances_, ten_dac_trung)
# 7. Correlation (F-regression cho hồi quy)
f_scores, p_values = f_regression(cac_dac_trung, bien_muc_tieu, center=True)
diem_xep_hang["Tương quan"] = chuan_hoa_xep_hang(f_scores, ten_dac_trung)
# 8. MIC
mine_calculator = MINE()
mic_diem_so = []
for i in range(cac_dac_trung.shape[1]):
mine_calculator.compute_score(cac_dac_trung[:,i], bien_muc_tieu)
mic_diem_so.append(mine_calculator.mic())
diem_xep_hang["MIC"] = chuan_hoa_xep_hang(mic_diem_so, ten_dac_trung)
# Tính điểm trung bình
diem_trung_binh = {}
for ten_dtrung in ten_dac_trung:
diem_trung_binh[ten_dtrung] = round(np.mean([diem_xep_hang[phuong_phap][ten_dtrung] for phuong_phap in diem_xep_hang.keys()]), 2)
diem_xep_hang["Trung bình"] = diem_trung_binh
phuong_phap_list = sorted(list(diem_xep_hang.keys()))
# In tiêu đề bảng
print("\t" + "\t".join(phuong_phap_list))
# In dữ liệu
for ten_dtrung in ten_dac_trung:
print(f"{ten_dtrung}\t" + "\t".join(map(str, [diem_xep_hang[phuong_phap][ten_dtrung] for phuong_phap in phuong_phap_list])))
Từ kết quả trên, chúng ta có thể rút ra một số nhận xét thú vị:
- Tương quan (và các phương pháp đơn biến khác như F-regression) đánh giá độc lập từng đặc trưng. Do đó, điểm số của $X_0, ..., X_3$ và $X_{10}, ..., X_{13}$ (các biến tương quan) là rất gần nhau, và các biến nhiễu $X_4, ..., X_9$ có điểm số gần bằng 0 như dự kiến. Vì biến $X_2$ có mối quan hệ phi tuyến (bậc hai), hầu hết các phương pháp đều không phát hiện được mối quan hệ này (ngoại trừ MIC). Phương pháp này hữu ích để đo mối quan hệ tuyến tính giữa đặc trưng và biến phản hồi, nhưng không hiệu quả lắm trong việc chọn các đặc trưng chất lượng để cải thiện khả năng tổng quát hóa của mô hình, vì tất cả các đặc trưng quan trọng đều được chọn hai lần.
- Lasso có khả năng chọn ra một số đặc trưng chất lượng, đồng thời đưa các hệ số của các đặc trưng khác về 0. Nó rất hữu ích khi cần giảm số lượng đặc trưng, nhưng không tốt cho việc hiểu dữ liệu (ví dụ: trong bảng kết quả, $X_{10}, X_{11}, X_{12}, X_{13}$ có điểm số 0, mặc dù thực tế chúng có mối quan hệ mạnh mẽ với biến mục tiêu).
- MIC đối xử công bằng với các đặc trưng, tương tự như hệ số tương quan, và quan trọng hơn, nó có thể phát hiện mối quan hệ phi tuyến giữa $X_2$ và biến phản hồi.
- Kết quả xếp hạng của Random Forest (dựa trên độ tạp) rất rõ ràng, điểm số của các đặc trưng giảm mạnh sau vài đặc trưng đứng đầu. Từ bảng, đặc trưng đứng thứ ba có điểm số thấp hơn đặc trưng đứng đầu bốn lần. Các thuật toán lựa chọn đặc trưng khác không giảm mạnh như vậy.
- Ridge phân bổ đều các hệ số hồi quy cho các biến tương quan. Từ bảng, $X_{10}, ..., X_{13}$ và $X_0, ..., X_3$ có điểm số rất gần nhau.
- Chọn lọc ổn định thường là một phương pháp hữu ích cả trong việc hiểu dữ liệu và chọn ra các đặc trưng chất lượng. Điều này được thể hiện rõ trong bảng kết quả. Giống như Lasso, nó tìm thấy các đặc trưng có hiệu suất tốt ($X_0, X_1, X_3, X_4$), đồng thời các biến có mối tương quan mạnh với chúng cũng nhận được điểm số cao.