GridSearchCV เหมาะกับ search space เล็ก (≤ 100 ชุดค่า) หรือเมื่อต้องการผลลัพธ์ที่ทำซ้ำได้ 100% สำหรับงานวิจัยและ regulatory audit
RandomizedSearchCV คือค่าเริ่มต้นที่ปลอดภัยสำหรับงานโปรดักชันส่วนใหญ่ เร็วกว่า Grid 5-20 เท่าโดยสูญเสียคุณภาพแค่ 0-2%
Optuna 4.x ใช้ Tree-structured Parzen Estimator (TPE) พร้อม pruning ทำให้ค้นหา hyperparameter ได้เร็วและคุณภาพดีที่สุดสำหรับโมเดล deep learning, XGBoost, LightGBM
scikit-learn 1.6 เพิ่ม HalvingGridSearchCV และ HalvingRandomSearchCV ที่ตัดผู้แพ้ตั้งแต่ต้น (ลดเวลาได้ 3-10 เท่า)
การใช้ n_jobs=-1 ร่วมกับ Pipeline และ cross_val_score ช่วยลดเวลาในสภาพแวดล้อมโปรดักชันได้มหาศาล แต่ต้องระวัง memory leak บนโมเดลใหญ่
วัด cost-per-trial ไม่ใช่แค่ accuracy เพราะบางครั้งโมเดลที่ดีกว่า 0.3% แต่ใช้ตอน inference นานขึ้น 4 เท่าก็ไม่คุ้มที่จะปล่อยจริง
หัวข้อในบทความ
Hyperparameter tuning คืออะไรและทำไมต้องสนใจ
ตารางเปรียบเทียบ GridSearchCV vs RandomizedSearchCV vs Optuna
GridSearchCV: เมื่อไหร่ที่ยัง make sense
RandomizedSearchCV: ทางสายกลางที่ใช้จริง
Optuna: เครื่องมือเลือกของงานโปรดักชัน
HalvingGridSearchCV: ตัวเลือกใหม่ใน scikit-learn 1.6
วิธีเลือกอัลกอริทึมให้เหมาะกับงาน
กับดักที่เจอตอนใช้จริงในโปรดักชัน
Hyperparameter tuning คืออะไรและทำไมต้องสนใจ
Hyperparameter คือพารามิเตอร์ที่ผู้สร้างโมเดลกำหนดก่อนการฝึก เช่น n_estimators, max_depth ของ RandomForest หรือ learning_rate, num_leaves ของ LightGBM ต่างจาก parameter ปกติ (เช่นน้ำหนักของ neural network) ที่โมเดลเรียนรู้เอง การปรับ hyperparameter ให้ดีอาจเพิ่ม accuracy ได้ 2-15% โดยไม่ต้องเปลี่ยนโมเดลหรือ feature engineering เลย
ในโปรดักชัน ผมใช้กฎง่ายๆ คือ "ห้ามใช้ค่า default ของ XGBoost/LightGBM ไปยิง production" เพราะค่า default ของ learning_rate=0.3 ใน XGBoost ถูกออกแบบให้ converge เร็วในการทดลอง ไม่ได้ปรับมาเพื่อ generalization สำหรับชุดข้อมูลของคุณ ทุกๆ โปรเจกต์ ML ของผมที่ส่งขึ้นโปรดักชันต้องผ่านขั้นตอน hyperparameter tuning อย่างน้อยหนึ่งรอบ
scikit-learn จัดเตรียมคลาส GridSearchCV และ RandomizedSearchCV เป็นมาตรฐานมานานกว่า 10 ปี ขณะที่ Optuna framework อย่างเป็นทางการ ที่พัฒนาโดย Preferred Networks กลายเป็นเครื่องมือที่ทีม ML ส่วนใหญ่เลือกใช้ในปี 2025-2026 เพราะ API ง่ายและประสิทธิภาพดี
ตารางเปรียบเทียบ GridSearchCV vs RandomizedSearchCV vs Optuna
ก่อนจะลงรายละเอียด นี่คือสรุปสั้นๆ ที่ผมใช้เป็น cheat sheet เวลาตัดสินใจในทีม:
คุณสมบัติ GridSearchCV RandomizedSearchCV Optuna 4.x
กลยุทธ์ค้นหา Exhaustive (ทุกชุดค่า) สุ่มจาก distribution Bayesian (TPE)
ความเร็ว (relative) 1x (ช้าสุด) 5-20x 20-100x
คุณภาพผลลัพธ์ ดีถ้า grid ครอบคลุม ดี (ภายใต้จำนวน trial เท่ากัน) ดีที่สุด
Pruning (ยกเลิก trial ที่แย่) ไม่มี ไม่มี มี (MedianPruner, Hyperband)
Conditional search space ทำไม่ได้ ทำไม่ได้ ทำได้ (define-by-run)
Multi-objective optimization ไม่รองรับ ไม่รองรับ รองรับ (NSGA-II)
การทำซ้ำได้ (Reproducibility) 100% มี seed ก็ทำซ้ำได้ มี seed ก็ทำซ้ำได้
Dashboard / visualization ไม่มี ไม่มี มี (optuna-dashboard)
เหมาะกับ Search space เล็ก, audit Production ทั่วไป Deep learning, GBDT
หมายเหตุ: ตัวเลขความเร็วเป็นตัวอย่างจาก benchmark ภายในทีมเราบน XGBoost classifier ขนาดกลาง (50K rows, 30 features) ผลลัพธ์อาจต่างกันตาม search space และโมเดล
GridSearchCV: เมื่อไหร่ที่ยัง make sense
GridSearchCV ค้นหาทุกชุดค่าที่เป็นไปได้ใน hyperparameter grid ถ้าคุณมี n_estimators 3 ค่า, max_depth 4 ค่า และ learning_rate 5 ค่า มันจะรัน 3 × 4 × 5 = 60 ครั้ง คูณด้วยจำนวน CV folds (เช่น 5) ก็ 300 trial เครื่องของผมที่บ้านยังพอรันได้ในไม่กี่ชั่วโมง
ผมเลือก GridSearchCV เฉพาะกรณีต่อไปนี้: (1) งานที่ต้องส่งให้ regulatory ตรวจสอบและต้อง reproduce ได้ 100% โดยไม่ต้องอธิบาย stochastic process, (2) โมเดลเล็กที่ฝึกเร็ว เช่น Logistic Regression, Linear SVM ที่ trial เดียวใช้เวลาไม่ถึง 1 วินาที, (3) ตอนทำ research paper ที่ผู้รีวิวอาจถามว่า "คุณค้นหาทุกความเป็นไปได้หรือยัง"
นี่คือตัวอย่างที่ใช้ GridSearchCV กับ Pipeline ถ้ายังไม่คุ้นเคยกับ Pipeline ของ scikit-learn แนะนำให้อ่าน คู่มือสร้าง scikit-learn Pipeline ฉบับสมบูรณ์ ก่อน:
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import GridSearchCV, StratifiedKFold
from sklearn.datasets import load_breast_cancer
X, y = load_breast_cancer(return_X_y=True)
pipe = Pipeline([
("scaler", StandardScaler()),
("clf", RandomForestClassifier(random_state=42, n_jobs=1)),
])
param_grid = {
"clf__n_estimators": [100, 300, 500],
"clf__max_depth": [None, 5, 10, 20],
"clf__min_samples_split": [2, 5, 10],
}
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
grid = GridSearchCV(
pipe,
param_grid,
cv=cv,
scoring="roc_auc",
n_jobs=-1,
verbose=1,
refit=True,
)
grid.fit(X, y)
print(f"Best ROC-AUC: {grid.best_score_:.4f}")
print(f"Best params: {grid.best_params_}")
สังเกตว่าผมตั้ง n_jobs=1 ใน RandomForestClassifier แต่ตั้ง n_jobs=-1 ใน GridSearchCV เพราะถ้าตั้ง -1 ทั้งสองที่จะเกิด nested parallelism ที่ทำให้ CPU contention และช้าลงจริง ผมเคยเจอเคสที่ training ช้าลง 3 เท่าเพราะตั้งผิด
RandomizedSearchCV: ทางสายกลางที่ใช้จริง
RandomizedSearchCV สุ่มชุดค่าจาก distribution ที่คุณกำหนด ไม่ต้องครบทุก combination. งานวิจัยคลาสสิกของ Bergstra & Bengio (2012) เรื่อง random search สำหรับ hyperparameter tuning พิสูจน์ว่าใน high-dimensional search space การสุ่ม 60 ครั้งให้ผลใกล้เคียงกับ grid search 1000+ ครั้ง เพราะ hyperparameter ส่วนใหญ่ไม่ sensitive เท่ากันทุกตัว
นี่คือ default ที่ผมแนะนำสำหรับโปรเจกต์ใหม่ ใช้กับ scipy distribution ได้:
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint, uniform, loguniform
param_distributions = {
"clf__n_estimators": randint(100, 800),
"clf__max_depth": randint(3, 30),
"clf__min_samples_split": randint(2, 20),
"clf__min_samples_leaf": randint(1, 10),
"clf__max_features": uniform(0.3, 0.7),
}
random_search = RandomizedSearchCV(
pipe,
param_distributions=param_distributions,
n_iter=60,
cv=cv,
scoring="roc_auc",
n_jobs=-1,
random_state=42,
verbose=1,
)
random_search.fit(X, y)
print(f"Best ROC-AUC: {random_search.best_score_:.4f}")
เคล็ดลับที่ผมใช้บ่อยคือ loguniform(1e-4, 1e-1) สำหรับ learning_rate เพราะการกระจายแบบ log ครอบคลุมหลาย order of magnitude ดีกว่า uniform ที่จะมีค่าส่วนใหญ่กระจุกอยู่ปลายบน
เคล็ดลับ: ตั้ง n_iter ให้ ≈ 10 × จำนวน hyperparameter เป็นจุดเริ่มต้น ถ้าโมเดลซับซ้อน (เช่น XGBoost ที่มี 10+ knobs) ก็เริ่ม 100 trial แล้วค่อยขยาย
Optuna: เครื่องมือเลือกของงานโปรดักชัน
Optuna 4.x ใช้ Tree-structured Parzen Estimator (TPE) ซึ่งเรียนรู้จาก trial ที่ผ่านมาเพื่อเดาว่าค่าไหนน่าจะดี ต่างจากการสุ่มตรงที่ trial ที่ 100 ของ Optuna มีโอกาสได้คะแนนสูงกว่า trial ที่ 100 ของ RandomizedSearchCV อย่างมีนัยสำคัญ
ฟีเจอร์ที่ทำให้ผมเปลี่ยนมาใช้ Optuna เกือบทุกโปรเจกต์คือ pruning . Optuna ดู intermediate score ระหว่างการฝึก แล้วยกเลิก trial ที่ดูจะไม่ดีตั้งแต่ early stage โดยอัตโนมัติ บน XGBoost ที่มี early_stopping_rounds ฟีเจอร์นี้ลดเวลารวมได้ 40-60%
import optuna
from sklearn.model_selection import cross_val_score
import xgboost as xgb
def objective(trial):
params = {
"n_estimators": trial.suggest_int("n_estimators", 100, 1000),
"max_depth": trial.suggest_int("max_depth", 3, 12),
"learning_rate": trial.suggest_float("learning_rate", 1e-3, 0.3, log=True),
"subsample": trial.suggest_float("subsample", 0.5, 1.0),
"colsample_bytree": trial.suggest_float("colsample_bytree", 0.5, 1.0),
"reg_alpha": trial.suggest_float("reg_alpha", 1e-8, 10.0, log=True),
"reg_lambda": trial.suggest_float("reg_lambda", 1e-8, 10.0, log=True),
"tree_method": "hist",
"random_state": 42,
"n_jobs": 1,
}
model = xgb.XGBClassifier(**params)
scores = cross_val_score(
model, X, y, cv=cv, scoring="roc_auc", n_jobs=-1
)
return scores.mean()
study = optuna.create_study(
direction="maximize",
sampler=optuna.samplers.TPESampler(seed=42),
pruner=optuna.pruners.MedianPruner(n_warmup_steps=10),
)
study.optimize(objective, n_trials=100, n_jobs=1, show_progress_bar=True)
print(f"Best ROC-AUC: {study.best_value:.4f}")
print(f"Best params: {study.best_params}")
สิ่งที่ Optuna ทำได้แต่ scikit-learn search ทำไม่ได้คือ conditional search space , เช่นถ้า trial เลือก kernel='rbf' ก็เพิ่ม gamma เข้าไป แต่ถ้าเลือก 'linear' ไม่ต้อง สิ่งนี้สำคัญมากเวลาเทียบหลายโมเดลในการค้นหาเดียว ดู เอกสาร Optuna เรื่อง define-by-run API สำหรับรายละเอียด
HalvingGridSearchCV: ตัวเลือกใหม่ใน scikit-learn 1.6
ตั้งแต่ scikit-learn 1.0 มี HalvingGridSearchCV และ HalvingRandomSearchCV เป็น experimental มาตลอด พอถึงเวอร์ชัน 1.6 (มกราคม 2026) ก็ stabilize เต็มตัว ใช้กลยุทธ์ successive halving : เริ่มทุก candidate ด้วย training set น้อยๆ แล้วเอาครึ่งล่างทิ้งทุกรอบ จนเหลือผู้ชนะคนเดียว
from sklearn.experimental import enable_halving_search_cv # noqa
from sklearn.model_selection import HalvingRandomSearchCV
halving = HalvingRandomSearchCV(
pipe,
param_distributions=param_distributions,
n_candidates=100,
factor=3,
resource="n_samples",
min_resources=200,
cv=cv,
scoring="roc_auc",
n_jobs=-1,
random_state=42,
)
halving.fit(X, y)
จากการทดสอบของผมบน dataset 500K rows, HalvingRandomSearchCV เร็วกว่า RandomizedSearchCV ปกติ 4-6 เท่าโดยที่ best score ต่างกันไม่ถึง 0.5% เป็นตัวเลือกที่ดีถ้าคุณต้องการอยู่ใน ecosystem ของ scikit-learn 100% โดยไม่อยากเพิ่ม dependency Optuna
วิธีเลือกอัลกอริทึมให้เหมาะกับงาน
นี่คือ decision tree ที่ผมใช้ในทีม:
โมเดลฝึกเร็วมาก (< 5 วินาทีต่อ trial) และมี hyperparameter ≤ 4 ตัว? ใช้ GridSearchCV ไปเลย เพราะ debug ง่าย ผลลัพธ์ทำซ้ำได้
ต้องการคำตอบที่ใช้ได้เร็ว ไม่ยอมเพิ่ม dependency ใหม่? ใช้ RandomizedSearchCV เพราะเป็น built-in กับ scikit-learn ทำงานกับ Pipeline ได้ทันที
โมเดล GBDT (XGBoost, LightGBM, CatBoost) หรือ deep learning? ใช้ Optuna เพราะ pruning ลดเวลาได้มหาศาล และ TPE ดีกว่าการสุ่ม
Search space เปลี่ยนตามเงื่อนไข เช่น เปรียบเทียบหลายโมเดลในการค้นหาเดียว? ต้อง Optuna เท่านั้น เพราะ scikit-learn ไม่รองรับ conditional space
ต้อง optimize หลาย objective พร้อมกัน (เช่น accuracy + latency)? Optuna กับ NSGA-II sampler
สำหรับการเตรียมข้อมูลก่อนเข้าโมเดล อ่านเพิ่มเติมที่ คู่มือจัดการข้อมูลหายไปใน pandas 3.0 และ 10 เทคนิคเร่งความเร็ว pandas DataFrame เพราะการ tune model ที่กิน I/O ช้าก็ไม่ช่วยอะไรมาก
กับดักที่เจอตอนใช้จริงในโปรดักชัน
ผ่านมาหลายปีที่ผม deploy โมเดล ML ขึ้น production มีกับดักห้าข้อที่เจอซ้ำๆ และอยากเตือนทีมที่กำลังจะลงสนามจริง
1. CV strategy ผิดพลาด ทำให้ tune แล้ว overfit
ใช้ StratifiedKFold สำหรับ classification, ใช้ TimeSeriesSplit สำหรับข้อมูล time-series อย่าเอา KFold ธรรมดามาใช้กับข้อมูลที่ unbalanced หรือมี time dependency ผมเคยเห็นทีมหนึ่งใช้ KFold กับข้อมูลการคลิกของผู้ใช้ที่ได้ ROC-AUC 0.92 ในการ tune แต่พอ deploy จริงเหลือ 0.71 เพราะ data leakage ข้ามเวลา
2. ลืม fit imputer/scaler ภายใน CV fold
ห่อทุก preprocessing step ใน Pipeline ให้หมด ห้าม fit StandardScaler บน full training set ก่อนเข้า GridSearchCV เพราะ statistics ของ test fold จะ leak เข้า training fold ทำให้ score ปลอม
3. n_jobs nested parallelism
คำเตือน: ถ้าตั้ง n_jobs=-1 ทั้งใน estimator (เช่น RandomForest) และใน search class คุณจะได้ thread storm ที่ทำให้ช้าลงและ memory พุ่ง บน Linux การ fork process ของ joblib บนโมเดลขนาดใหญ่ยังอาจทำให้ memory ใช้สูงกว่าคาด 2-3 เท่า
4. ใช้ score เดียวตัดสินใจ ไม่ดู latency/cost
ในงาน production ML ผมใช้ multi-objective ของ Optuna เพื่อ optimize ทั้ง accuracy และ inference latency พร้อมกัน บางครั้งโมเดล 200 trees ที่ AUC 0.891 ดีกว่าโมเดล 800 trees ที่ AUC 0.894 เพราะ latency ต่ำกว่า 4 เท่าและ cost-per-prediction ต่ำกว่า ลองคำนวณ p99 latency budget ของ serving endpoint ก่อนเริ่ม tune ทุกครั้ง
5. ไม่ persist study object
ใน Optuna ใช้ storage="sqlite:///study.db" ตอน create_study เพื่อบันทึก trial ทั้งหมดลงไฟล์ ถ้า process ตายระหว่าง trial 80 ของ 100 คุณสามารถ resume ได้โดยไม่ต้องเริ่มใหม่ ใน เอกสาร scikit-learn เรื่อง model selection ก็แนะนำใช้ joblib dump เก็บ search object เช่นกัน
คำถามที่พบบ่อย
Optuna ดีกว่า GridSearchCV ตลอดหรือไม่?
ไม่ตลอด ถ้า search space เล็กกว่า 50 combination และโมเดลฝึกเร็ว GridSearchCV ให้ผลลัพธ์ที่ทำซ้ำได้ 100% และเขียนโค้ดสั้นกว่า Optuna ดีกว่าตอน search space ใหญ่ มีหลาย hyperparameter หรือใช้โมเดลที่ฝึกนาน
ควรตั้ง n_iter เท่าไหร่ใน RandomizedSearchCV?
เริ่มจาก n_iter = 10 × จำนวน hyperparameter เป็นค่าตั้งต้น ถ้ามี 5 hyperparameter ลอง 50 trial ก่อน ถ้าผลยังไม่ดีขึ้น (best score คงที่ 10+ trial ล่าสุด) ค่อยเพิ่มเป็น 100-200 trial
HalvingGridSearchCV ใช้ทดแทน Optuna ได้หรือไม่?
ใช้ได้ในงานทั่วไป แต่ Optuna ยังได้เปรียบที่ TPE sampler ฉลาดกว่าและรองรับ conditional space กับ multi-objective ถ้าอยากอยู่ใน scikit-learn 100% Halving เป็นตัวเลือกที่ดี ถ้ายอมเพิ่ม dependency Optuna คุ้มกว่า
ทำไม cross_val_score ของผมช้ามากเวลาตั้ง n_jobs=-1?
มักเกิดจาก nested parallelism. กล่าวคือ estimator ภายใน (เช่น RandomForest, XGBoost) ก็ใช้ multi-threading อยู่แล้ว เมื่อ CV ห่อด้วย n_jobs=-1 อีกชั้น CPU ถูกแย่งกันใช้จนช้าลง แก้โดยตั้ง n_jobs=1 ในตัว estimator แล้วให้ CV เป็นตัว parallelize
ควรใช้ score อะไรในการ tune โมเดล imbalanced classification?
อย่าใช้ accuracy เพราะ class ส่วนใหญ่จะครองคะแนน ใช้ roc_auc, average_precision หรือ f1_macro ตามจุดประสงค์ ถ้าสนใจ recall ของ minority class ใช้ recall_macro ในงาน fraud detection ผมมักใช้ average_precision เพราะ sensitive ต่อ rare positive class กว่า ROC-AUC