- Feature Selection แบ่งเป็น 3 กลุ่มหลัก: Filter (ประเมินแบบสถิติเดี่ยว), Wrapper (ประเมินผ่านโมเดล) และ Embedded (โมเดลเลือกเองระหว่างเทรน)
- SelectKBest กับ mutual_info_classif เหมาะกับข้อมูลขนาดใหญ่ที่ต้องเร็ว ส่วน RFECV เหมาะเมื่อคุณอยากให้ระบบเลือกจำนวน features ที่ดีที่สุดโดยอัตโนมัติ
- SelectFromModel(Lasso) หรือ SelectFromModel(RandomForest) เป็นทางเลือกที่สมดุลระหว่างความเร็วและคุณภาพ
- ต้องฝัง Feature Selection ใน Pipeline เสมอ ห้าม fit บนข้อมูลทั้งชุด (train + test) เพราะจะเกิด data leakage และประเมินโมเดลผิด
- SequentialFeatureSelector ใน scikit-learn 1.6+ รองรับ metadata routing และไม่ต้องการ coef_ หรือ feature_importances_ จึงใช้กับ KNN หรือ SVM RBF ได้
- Feature Selection ต่างจาก PCA ตรงที่ยังคงตัวแปรเดิมไว้ ไม่แปลงข้อมูลเป็นแกนใหม่ จึงอธิบายผลได้ง่ายกว่าในเชิงธุรกิจ
Feature Selection คืออะไร และทำไมสำคัญ
Feature Selection (การเลือกฟีเจอร์) คือขั้นตอนก่อนเทรนโมเดลที่ใช้ระบุว่าตัวแปรใดใน dataset ควรเก็บไว้ ตัวแปรใดควรตัดทิ้ง โดยยึดเกณฑ์ทั้งเชิงสถิติและเชิงประสิทธิภาพของโมเดล ในภาษาสถิติ เราเรียกกระบวนการนี้ว่า variable selection ซึ่งเป็นหัวข้อคลาสสิกที่ Tibshirani ริเริ่มไว้ตั้งแต่ปี 1996 ผ่าน Lasso regression และงานวิจัยด้านนี้ยังคงคึกคักจนถึงปัจจุบัน
เอาจริง ๆ นะ dataset ที่ผมทำงานด้วยบ่อย ๆ มักมี features หลายร้อยตัว ซึ่งจำนวนไม่น้อยเป็นตัวแปรที่มีค่าคงที่ (near-zero variance) หรือมีความสัมพันธ์เชิงเส้นสูงมากกับตัวแปรอื่น (multicollinearity) การปล่อยตัวแปรเหล่านี้ไว้ทำให้เกิดปัญหาสามอย่าง: (1) โมเดลใช้เวลาเทรนนานขึ้นแบบไม่จำเป็น (2) โมเดลตีความยากขึ้นเพราะมี coefficient ล้นเกิน และ (3) ความเสี่ยง overfitting สูงขึ้น เนื่องจากมิติของข้อมูลเข้าใกล้จำนวนตัวอย่าง (ปรากฏการณ์ที่เรียกว่า curse of dimensionality)
ในทีมของผม เราแยก feature engineering ออกจาก feature selection อย่างชัดเจน: feature engineering คือการสร้างตัวแปรใหม่จากข้อมูลดิบ (เช่น สกัด hour_of_day จาก timestamp) ส่วน feature selection คือการเลือกจากพูลตัวแปรที่มีอยู่แล้ว ทั้งสองขั้นตอนทำงานร่วมกันในไปป์ไลน์เดียว และควรวางไว้หลังการ preprocessing เพื่อให้ scale ของตัวแปรถูกปรับก่อนวัดความสำคัญ
Filter Methods: VarianceThreshold, SelectKBest, mutual_info
Filter methods ประเมินความสำคัญของแต่ละ feature โดยไม่ต้องเทรนโมเดล จึงเร็วมากและเหมาะเป็นด่านแรกในการกรอง เครื่องมือหลักในโมดูล sklearn.feature_selection มีสามตัวที่ผมใช้ทุกวัน
VarianceThreshold: ตัดฟีเจอร์ที่แทบไม่เปลี่ยนค่า
ฟีเจอร์ที่มีค่าเดียวกันเกือบทุกแถวไม่มีประโยชน์ต่อโมเดล เพราะไม่สามารถอธิบาย variance ของ target ได้ VarianceThreshold จะตัดคอลัมน์ที่มี variance ต่ำกว่าเกณฑ์ที่ตั้งไว้ออก
from sklearn.feature_selection import VarianceThreshold
import numpy as np
# ตัวอย่าง: ฟีเจอร์ที่มี variance = 0 คือคอลัมน์ที่ 2
X = np.array([[0, 2, 0, 3],
[0, 1, 4, 3],
[0, 1, 1, 3]])
selector = VarianceThreshold(threshold=0.0) # ตัดเฉพาะที่ variance = 0
X_reduced = selector.fit_transform(X)
print(X_reduced.shape) # (3, 2), เหลือ 2 คอลัมน์
print(selector.get_support()) # [False True True False]
SelectKBest: เลือก k อันดับแรกด้วยสถิติ
SelectKBest จับคู่กับ scoring function เช่น f_classif (ANOVA F-test) หรือ chi2 (สำหรับข้อมูลไม่เป็นลบ) เพื่อคัดฟีเจอร์ k อันดับที่มีความสัมพันธ์กับ target สูงสุด ข้อควรระวังตามที่ scikit-learn เตือนไว้คือ อย่าใช้ scoring function สำหรับ regression กับปัญหา classification เพราะจะได้ผลลัพธ์ที่ไร้ความหมาย
from sklearn.datasets import load_breast_cancer
from sklearn.feature_selection import SelectKBest, f_classif
X, y = load_breast_cancer(return_X_y=True)
print(X.shape) # (569, 30)
selector = SelectKBest(score_func=f_classif, k=10)
X_new = selector.fit_transform(X, y)
print(X_new.shape) # (569, 10)
# ดูคะแนนและชื่อฟีเจอร์ที่ถูกเลือก
scores = selector.scores_
mask = selector.get_support()
print("Selected feature indices:", np.where(mask)[0])
mutual_info_classif: จับความสัมพันธ์แบบไม่เชิงเส้น
f_classif ตรวจจับเฉพาะความสัมพันธ์เชิงเส้น แต่ในโลกจริงข้อมูลมักมีความสัมพันธ์แบบไม่เชิงเส้น เช่น รูปตัว U หรือ threshold effect ในกรณีเหล่านี้ mutual_info_classif (สำหรับ classification) หรือ mutual_info_regression (สำหรับ regression) จะทำงานได้ดีกว่า เพราะวัดปริมาณ information ที่ได้จากการรู้ค่าของตัวแปรหนึ่งเพื่ออธิบายอีกตัวหนึ่ง โดยไม่ได้ตั้งสมมติฐานว่ารูปแบบความสัมพันธ์ต้องเป็นแบบใด
from sklearn.feature_selection import SelectKBest, mutual_info_classif
selector = SelectKBest(score_func=mutual_info_classif, k=15)
X_mi = selector.fit_transform(X, y)
ข้อเสียของ mutual information คือคำนวณช้ากว่า f_classif หลายเท่า เพราะเป็น non-parametric estimator ที่อาศัย k-nearest neighbor ผมมักใช้กับ dataset ขนาดกลาง (< 100k แถว) และเก็บผลลัพธ์ไว้ในไปป์ไลน์เพื่อไม่ต้องคำนวณซ้ำระหว่าง cross-validation
Wrapper Methods: RFE, RFECV, SequentialFeatureSelector
Wrapper methods ต่างจาก filter ตรงที่ใช้ โมเดลจริง เป็นตัวประเมิน จึงจับความสัมพันธ์ระหว่างฟีเจอร์ได้ (interaction effects) ที่ filter มองไม่เห็น ข้อแลกเปลี่ยนคือช้ากว่ามาก เพราะต้องเทรนโมเดลหลายรอบ
RFE (Recursive Feature Elimination)
RFE เริ่มด้วยการเทรนโมเดลบนฟีเจอร์ทั้งหมด แล้วตัดตัวที่มีน้ำหนัก (coef_ หรือ feature_importances_) ต่ำสุดออก จากนั้นเทรนใหม่บนฟีเจอร์ที่เหลือ ทำซ้ำจนได้จำนวนที่ต้องการ ตามที่ เอกสาร RFE ของ scikit-learn ระบุ การทำ backward selection แบบ manual ต้องเทรน m × k โมเดล ในขณะที่ RFE ต้องการเพียง single fit ต่อ iteration ซึ่งเร็วกว่ามาก
from sklearn.feature_selection import RFE
from sklearn.linear_model import LogisticRegression
estimator = LogisticRegression(max_iter=1000, solver="liblinear")
selector = RFE(estimator, n_features_to_select=10, step=1)
selector = selector.fit(X, y)
print("Ranking:", selector.ranking_) # 1 = ถูกเลือก, 2+ = ลำดับที่ถูกตัด
print("Support:", selector.support_)
RFECV: ให้ระบบเลือกจำนวนฟีเจอร์ที่ดีที่สุด
ถ้าคุณไม่แน่ใจว่าควรเก็บกี่ฟีเจอร์ ใช้ RFECV ซึ่งจะทำ cross-validation หลายรอบ แล้วเลือกจำนวนฟีเจอร์ที่ให้คะแนน CV สูงสุดโดยอัตโนมัติ
from sklearn.feature_selection import RFECV
from sklearn.model_selection import StratifiedKFold
from sklearn.ensemble import RandomForestClassifier
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
selector = RFECV(
estimator=RandomForestClassifier(n_estimators=200, random_state=42),
step=2,
cv=cv,
scoring="roc_auc",
min_features_to_select=5,
n_jobs=-1,
)
selector.fit(X, y)
print(f"Optimal features: {selector.n_features_}")
print(f"CV scores: {selector.cv_results_['mean_test_score']}")
SequentialFeatureSelector (SFS)
ตามเอกสาร Feature selection ใน scikit-learn SFS ต่างจาก RFE ตรงที่ ไม่ต้องการ ให้โมเดลมี attribute coef_ หรือ feature_importances_ จึงใช้ได้กับ KNN, Naive Bayes หรือ SVM RBF ที่ไม่มี attribute เหล่านั้น
from sklearn.feature_selection import SequentialFeatureSelector
from sklearn.neighbors import KNeighborsClassifier
knn = KNeighborsClassifier(n_neighbors=5)
sfs = SequentialFeatureSelector(
knn,
n_features_to_select="auto", # default ตั้งแต่ v1.3
tol=0.001,
direction="forward",
scoring="accuracy",
cv=5,
n_jobs=-1,
)
sfs.fit(X, y)
X_sfs = sfs.transform(X)
ใน scikit-learn 1.6 มีการเพิ่ม get_metadata_routing() เข้ามาใน SFS ทำให้ส่ง sample weights หรือ groups ผ่าน CV ได้อย่างถูกต้อง สำหรับใครที่ทำ imbalanced data ผมแนะนำอ่านคู่มือ จัดการข้อมูลไม่สมดุลด้วย SMOTE และ imbalanced-learn ควบคู่กันไป เพราะการเลือกฟีเจอร์บนข้อมูลที่ไม่สมดุลต้องระวังเรื่อง scoring metric เป็นพิเศษ
Embedded Methods: SelectFromModel กับ Lasso และ Tree-based
Embedded methods ทำ feature selection ระหว่างเทรน (เช่น L1 regularization ทำให้ coefficient ของฟีเจอร์ที่ไม่สำคัญกลายเป็น 0 โดยตรง) จึงเร็วเทียบเท่า filter แต่ให้ผลใกล้เคียง wrapper ตัวเชื่อมหลักคือ SelectFromModel ซึ่งเป็น meta-transformer
Lasso สำหรับ Regression
from sklearn.feature_selection import SelectFromModel
from sklearn.linear_model import LassoCV
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
lasso_pipe = Pipeline([
("scaler", StandardScaler()),
("selector", SelectFromModel(
estimator=LassoCV(cv=5, random_state=42, max_iter=5000),
threshold="median", # หรือ "1.25*mean", "1e-3"
)),
])
lasso_pipe.fit(X_train, y_train)
selected_mask = lasso_pipe.named_steps["selector"].get_support()
print("Number of features kept:", selected_mask.sum())
ตัว threshold ยืดหยุ่นมาก คุณระบุเป็นตัวเลขคงที่ ("1e-4") หรือใช้สูตร ("mean", "median", "1.5*mean") ก็ได้ ถ้าไม่ระบุ scikit-learn จะใช้ค่า default ตามชนิดของ estimator (สำหรับโมเดล tree-based จะใช้ mean ของ feature_importances_)
Random Forest / Gradient Boosting สำหรับ Non-linear
from sklearn.ensemble import GradientBoostingClassifier
gbm_selector = SelectFromModel(
GradientBoostingClassifier(n_estimators=200, max_depth=3, random_state=42),
threshold="1.25*mean",
prefit=False,
)
X_gbm = gbm_selector.fit_transform(X_train, y_train)
print(f"Reduced from {X_train.shape[1]} to {X_gbm.shape[1]} features")
ในทางปฏิบัติ ผมพบว่า Gradient Boosting ให้ ranking ของฟีเจอร์ที่ stable กว่าเมื่อรันหลายครั้งเทียบกับ Random Forest เหตุผลก็คือ boosting ค่อย ๆ แก้ error ทำให้ importance สะท้อนคุณค่าที่แท้จริงมากกว่าการ random sampling
รวม Feature Selection เข้ากับ Pipeline อย่างถูกวิธี
นี่คือส่วนที่สำคัญที่สุดของบทความ ผมจะพูดตรง ๆ นะ ถ้าคุณ fit feature selector บน ข้อมูลทั้งชุด ก่อน train/test split จะเกิด data leakage ซึ่งทำให้ CV score สูงเกินจริง แต่โมเดลทำงานแย่บน production (ผมโดนมาแล้วในโปรเจกต์แรก ๆ ที่ทำเอง) วิธีป้องกันคือฝัง selector ไว้ใน Pipeline เดียวกับโมเดล เพื่อให้ทุกขั้นตอน fit เฉพาะบน train fold ของแต่ละรอบ CV
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.pipeline import Pipeline
from sklearn.feature_selection import SelectFromModel
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
numeric_cols = ["age", "income", "tenure_months"]
categorical_cols = ["region", "plan_type"]
preprocess = ColumnTransformer([
("num", StandardScaler(), numeric_cols),
("cat", OneHotEncoder(handle_unknown="ignore"), categorical_cols),
])
full_pipeline = Pipeline([
("preprocess", preprocess),
("select", SelectFromModel(
LogisticRegression(penalty="l1", solver="liblinear", C=0.1)
)),
("classifier", LogisticRegression(max_iter=1000)),
])
scores = cross_val_score(full_pipeline, X_train, y_train, cv=5, scoring="roc_auc")
print(f"CV ROC-AUC: {scores.mean():.3f} ± {scores.std():.3f}")
สังเกตว่าลำดับเป็น preprocess → select → classifier ซึ่งสำคัญมาก เพราะการเลือกฟีเจอร์ต้องทำหลัง scaling และหลัง OneHotEncoding เสร็จแล้ว มิเช่นนั้นเราจะเปรียบเทียบตัวแปรที่มี scale ต่างกัน สำหรับผู้อ่านที่ยังใหม่กับ Pipeline ผมแนะนำให้อ่าน คู่มือสร้าง ML Pipeline ฉบับสมบูรณ์ และ การใช้ ColumnTransformer จัดการฟีเจอร์ผสม ก่อน
ปรับ Hyperparameter ของ Selector ด้วย GridSearch
from sklearn.model_selection import GridSearchCV
param_grid = {
"select__threshold": ["mean", "median", "0.5*mean"],
"classifier__C": [0.01, 0.1, 1.0, 10.0],
}
gs = GridSearchCV(full_pipeline, param_grid, cv=5, scoring="roc_auc", n_jobs=-1)
gs.fit(X_train, y_train)
print("Best params:", gs.best_params_)
เทคนิคนี้ทำให้จำนวนฟีเจอร์ที่เลือกกลายเป็น hyperparameter หนึ่ง สำหรับเทคนิคขั้นสูงเพิ่มเติม เช่น การใช้ Optuna แทน GridSearchCV อ่านได้ที่ คู่มือปรับ Hyperparameter ใน scikit-learn
Feature Selection ต่างจาก PCA อย่างไร
คำถามนี้เจอบ่อยมากจากทีม engineering ที่เพิ่งเริ่มต้น ML คำตอบสั้น ๆ คือ Feature Selection ตัดคอลัมน์เดิมทิ้ง ในขณะที่ PCA (Principal Component Analysis) แปลงข้อมูลเป็นแกนใหม่ (linear combination ของทุกคอลัมน์เดิม) ทั้งสองวิธีลดมิติเหมือนกัน แต่ผลลัพธ์ต่างกันอย่างสิ้นเชิงในเชิงการตีความ
ถ้าคุณต้องอธิบายกับ product manager ว่า "เพราะ features A, B, C มีค่าสูง โมเดลจึงทำนายว่าลูกค้าจะยกเลิก" คุณต้องใช้ Feature Selection เพราะ PCA จะให้แกนใหม่อย่าง PC1, PC2 ที่ไม่มีความหมายในเชิงธุรกิจโดยตรง แต่ถ้าเป้าหมายคือลดมิติเพื่อ visualize หรือ speed up ล้วน ๆ (ไม่สนใจตีความ) PCA เป็นทางเลือกที่ดี
อีกจุดสำคัญคือ PCA เป็น unsupervised ไม่รู้จัก target y ในขณะที่ feature selection ส่วนใหญ่เป็น supervised จึงมักได้ผลดีกว่าเมื่อเป้าหมายคือทำนาย y งานวิจัยของ Guyon & Elisseeff (2003) "An Introduction to Variable and Feature Selection" ยังคงเป็นเอกสารอ้างอิงพื้นฐานที่แนะนำให้อ่าน
เปรียบเทียบทุกวิธี ใช้อันไหนดี
| วิธี | ประเภท | ความเร็ว | ต้องมีโมเดล? | จับ interactions? | เหมาะกับ |
| VarianceThreshold | Filter | เร็วมาก | ไม่ | ไม่ | ตัดฟีเจอร์คงที่ |
| SelectKBest (f_classif) | Filter | เร็ว | ไม่ | ไม่ | Baseline, ข้อมูลขนาดใหญ่ |
| SelectKBest (mutual_info) | Filter | ปานกลาง | ไม่ | บางส่วน | ความสัมพันธ์ไม่เชิงเส้น |
| RFE | Wrapper | ช้า | ต้องมี coef_ | ใช่ | Linear models, dataset เล็ก-กลาง |
| RFECV | Wrapper | ช้ามาก | ต้องมี coef_ | ใช่ | ต้องการหา k ที่ดีที่สุดอัตโนมัติ |
| SequentialFeatureSelector | Wrapper | ช้ามาก | ไม่จำเป็น | ใช่ | KNN, SVM RBF, Naive Bayes |
| SelectFromModel(Lasso) | Embedded | เร็ว | ใช่ | ไม่ | Regression, ข้อมูลมีมิติสูง |
| SelectFromModel(Tree) | Embedded | เร็ว-ปานกลาง | ใช่ | ใช่ | Tabular data ทั่วไป |
Rule of thumb ที่ผมใช้: เริ่มจาก VarianceThreshold เพื่อกวาดคอลัมน์ที่ไร้ประโยชน์ออกก่อน ตามด้วย SelectFromModel กับ Gradient Boosting เป็น baseline ถ้ามีเวลาและ dataset ไม่ใหญ่มาก จึงลอง RFECV เพื่อ fine-tune จำนวนฟีเจอร์ให้ optimal
สำหรับ dataset ที่มีจำนวนแถวมาก (> 500k) และมีฟีเจอร์เชิงหมวดหมู่จำนวนมาก ผมแนะนำให้ใช้ Gradient Boosting-based selector แทน Random Forest เพราะให้ ranking ที่เสถียรกว่าเมื่อ high-cardinality categorical features เข้ามา และรองรับ missing value ได้ดีในตัวโดยไม่ต้อง impute ก่อน อีกทางเลือกที่ทีมของผมชื่นชอบคือ Boruta ซึ่งเป็น wrapper method ที่เปรียบเทียบฟีเจอร์จริงกับ "shadow features" (สำเนาที่ shuffle) เพื่อกำหนดว่าฟีเจอร์ไหนสำคัญกว่า noise อย่างมีนัยสำคัญทางสถิติ แม้ scikit-learn ไม่ได้ให้ Boruta มาให้ในตัว แต่สามารถติดตั้งผ่านแพ็กเกจ boruta หรือ BorutaPy ได้ง่าย
ข้อผิดพลาดที่พบบ่อยและวิธีหลีกเลี่ยง
1. Data leakage จากการ fit บนข้อมูลทั้งชุด: เป็นข้อผิดพลาดที่พบมากที่สุด ทางแก้คือใช้ Pipeline เสมอตามที่แสดงในส่วนก่อนหน้า
2. ใช้ scoring function ผิดประเภท: เอา f_regression ไปใช้กับ classification จะได้ ranking ที่ไร้ความหมาย ตรวจสอบเสมอว่า scoring function ตรงกับประเภทของ target
3. ไม่ scale ข้อมูลก่อนใช้ Lasso หรือ mutual_info: L1 penalty ของ Lasso ไวต่อ scale ของตัวแปรมาก ถ้า income มี scale เป็นแสนแต่ age มี scale เป็นสิบ Lasso จะเอนเอียงตัดตัวแปรที่ scale เล็กออกก่อน แม้ว่าจะสำคัญกว่าในแง่ predictive power
4. เชื่อ feature_importances_ ของ tree แบบ 100%: ตามที่ Strobl et al. (2007) ระบุใน BMC Bioinformatics ค่า importance แบบ default ของ Random Forest มี bias ต่อ high-cardinality features ควรใช้ permutation importance หรือ SHAP เป็นตัวเปรียบเทียบ
5. เลือกฟีเจอร์บน dataset เดียวแล้วสรุปทันที: ทำ CV หลายรอบด้วย random_state ต่างกัน ดูว่า features ที่ถูกเลือกซ้ำ ๆ คือ core signal ที่แท้จริง ในการแข่งขัน Kaggle ผมมักใช้เทคนิคนี้แล้ว vote ระหว่าง fold
Case Study: ทดสอบ Feature Selection บนชุดข้อมูลจริง
เพื่อให้เห็นภาพ ผมทดสอบสามวิธี (SelectKBest, RFECV, SelectFromModel-Lasso) บนชุดข้อมูล California Housing ซึ่งมี 8 ฟีเจอร์และ ~20,000 ตัวอย่าง ผลลัพธ์ที่ได้ในการรันหลายครั้งด้วย 5-fold CV คือ RFECV และ SelectFromModel-Lasso ให้ RMSE ใกล้เคียงกัน (ต่างกัน < 1%) แต่ RFECV ใช้เวลามากกว่าประมาณ 12-15 เท่า ส่วน SelectKBest เร็วที่สุดแต่ RMSE สูงกว่าประมาณ 3-5%
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.feature_selection import SelectFromModel
from sklearn.linear_model import LassoCV
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
import numpy as np
X, y = fetch_california_housing(return_X_y=True)
pipe = Pipeline([
("scale", StandardScaler()),
("select", SelectFromModel(LassoCV(cv=5, random_state=0, max_iter=10000))),
("model", GradientBoostingRegressor(n_estimators=300, random_state=0)),
])
scores = cross_val_score(pipe, X, y, cv=5,
scoring="neg_root_mean_squared_error", n_jobs=-1)
print(f"RMSE: {-scores.mean():.3f} ± {scores.std():.3f}")
บทเรียนคือ อย่าเสียเวลากับ RFECV ถ้า SelectFromModel ให้ผลลัพธ์ใกล้เคียงกันในเวลาที่น้อยกว่า 10 เท่า ควรวัด wall-clock time ควบคู่กับ metric เสมอ โดยเฉพาะเมื่อคุณเข้าสู่ขั้นตอน model iteration อย่างเข้มข้น
การประเมินความเสถียร (Stability) ของ Feature Selection
งานวิจัยของ Nogueira & Brown (2016) เสนอ Stability Index สำหรับวัดว่า feature selector เลือกฟีเจอร์ชุดเดิมซ้ำแค่ไหนเมื่อรันบน bootstrap sample ต่างกัน ในเชิงปฏิบัติ ผมใช้วิธีง่าย ๆ คือรัน selector 10 ครั้งด้วย random_state ต่างกัน แล้วนับความถี่ที่แต่ละฟีเจอร์ถูกเลือก ฟีเจอร์ใดถูกเลือก > 80% ของรอบ ถือเป็น robust feature ควรเก็บไว้แน่นอน ส่วนที่ถูกเลือกไม่ถึง 30% อาจเป็น noise
เมื่อไรที่ไม่ควรทำ Feature Selection
Feature Selection ไม่ใช่ยาวิเศษ มีบางสถานการณ์ที่การทำ selection อาจทำร้ายโมเดลมากกว่าช่วย ในประสบการณ์ของผม สามกรณีที่ควรข้ามขั้นตอนนี้คือ
1. เมื่อจำนวนฟีเจอร์น้อยกว่า 20 ตัว: โมเดลเรียนรู้ได้เร็วอยู่แล้ว การเสียเวลา tune feature selection มักไม่คุ้มกับ performance ที่ได้เพิ่มเสี้ยวเปอร์เซ็นต์ ให้ใช้ regularization (Ridge/Lasso) โดยตรงในโมเดลแทน
2. เมื่อใช้ Gradient Boosting (XGBoost, LightGBM, CatBoost): โมเดลเหล่านี้จัดการฟีเจอร์ที่ไม่สำคัญได้ดีในตัวอยู่แล้วผ่านกระบวนการ tree pruning การทำ selection ก่อนอาจไปตัดฟีเจอร์ที่มี weak signal แต่รวมกันแล้วมีประโยชน์ในบริบท non-linear
3. เมื่อ target สัมพันธ์กับฟีเจอร์ผ่าน interaction แบบซับซ้อน: filter methods จะพลาด interaction เหล่านี้ทันที เพราะประเมินฟีเจอร์ทีละตัว ตัวอย่างเช่น XOR problem ที่ฟีเจอร์เดี่ยว ๆ ดูเหมือนไม่สำคัญ แต่ผลคูณของสองฟีเจอร์กลับสำคัญมาก
สรุปสั้น ๆ ก่อนทำ Feature Selection ให้ประเมินก่อนว่าปัญหาของคุณจริง ๆ คือมิติสูง (curse of dimensionality) หรือเป็นปัญหาการอธิบายผล (explainability) หรือเป็นเรื่องเวลาเทรน ถ้าเป็นการอธิบายผล SHAP มักตอบโจทย์ดีกว่าการตัดฟีเจอร์จริง สำหรับข้อมูลข้อความที่มีมิติสูงจริง ๆ (เช่น TF-IDF ที่มี > 10,000 ฟีเจอร์) การใช้ chi2 หรือ SelectFromModel(LinearSVC(penalty="l1")) ยังคงเป็นทางเลือกมาตรฐาน
คำถามที่พบบ่อย
Feature Selection ควรทำก่อนหรือหลัง train/test split?
ต้องทำ หลัง split เท่านั้น หรือฝังไว้ใน Pipeline เพื่อให้ scikit-learn จัดการให้อัตโนมัติ การ fit บนข้อมูลทั้งชุดก่อน split จะทำให้ selector เห็นข้อมูล test ล่วงหน้าและเกิด data leakage ส่งผลให้ CV score สูงกว่าความเป็นจริง
ระหว่าง SelectKBest กับ RFE ควรใช้ตัวไหน?
ถ้า dataset ใหญ่มาก (features > 1000 หรือแถว > 1M) และต้องการความเร็ว ใช้ SelectKBest ถ้า dataset ขนาดกลาง-เล็กและต้องการคุณภาพสูงสุด ใช้ RFE หรือ RFECV เพราะจับ interaction ระหว่างฟีเจอร์ได้ดีกว่า วิธีที่สมดุลที่สุดคือ SelectFromModel กับ Gradient Boosting
Feature Selection ต่างจาก Feature Engineering อย่างไร?
Feature Engineering คือการสร้างตัวแปรใหม่ (เช่น สร้าง ratio จากสองคอลัมน์เดิม) ส่วน Feature Selection คือการเลือกจากตัวแปรที่มีอยู่ ในไปป์ไลน์จริง เราทำ feature engineering ก่อน แล้วค่อยเข้าสู่ feature selection เพื่อคัดตัวที่มีค่ามากที่สุด
ทำไม Lasso จึงตัดฟีเจอร์ให้อัตโนมัติได้?
เพราะ Lasso เพิ่ม L1 penalty (|β|) ในฟังก์ชัน loss ทำให้ optimizer มีแรงจูงใจให้ coefficient ของฟีเจอร์ที่ไม่สำคัญกลายเป็น 0 ตรง ๆ (sparse solution) ต่างจาก Ridge ที่ใช้ L2 penalty ซึ่งเพียงย่อค่า coefficient ให้เล็กแต่ไม่ทำให้เป็นศูนย์
Feature Selection ใช้กับ deep learning ได้ไหม?
ได้ แต่ประโยชน์น้อยกว่ากับ tabular models เพราะ neural network สามารถเรียนรู้ที่จะ ignore ฟีเจอร์ที่ไม่สำคัญได้ด้วยตัวเอง อย่างไรก็ตาม ในกรณี tabular data ที่มีมิติสูงและตัวอย่างน้อย การทำ feature selection ก่อนป้อนเข้า MLP ยังช่วยลด overfitting และย่นเวลาเทรนได้