Mục lục
- Mục tiêu bài học
- Vấn đề của hold-out split
- Cross-Validation — ý tưởng K-Fold
- K-Fold với sklearn
- Chọn K thế nào
- Stratified K-Fold cho classification
- Group K-Fold — tránh group leak
- TimeSeriesSplit cho dữ liệu thời gian
- Repeated K-Fold — giảm variance của CV score
- cross_validate vs cross_val_score
- CV + Pipeline để tránh data leak
- Nested CV khi tune hyperparameter
- CV vs Train/Val/Test split
- scoring parameter và custom scorer
- Pitfall thường gặp
- Cost và parallel với n_jobs
- Code Python — Iris end-to-end
- Bài tập thực hành
- Bài tiếp theo
Mục tiêu bài học
Sau bài này, bạn sẽ:
- Hiểu vì sao 1 lần hold-out split chưa đủ để đánh giá model, đặc biệt khi dataset nhỏ.
- Triển khai K-Fold CV với
cross_val_score, đọcmean ± stdđúng nghĩa. - Chọn đúng strategy cho từng loại bài toán:
StratifiedKFold,GroupKFold,TimeSeriesSplit,RepeatedKFold. - Kết hợp CV với
Pipelineđể preprocessing không leak qua fold. - Hiểu khi nào cần Nested CV, khi nào hold-out là đủ.
- Dùng
cross_validateđể lấy nhiều metric + train score + thời gian fit.
Vấn đề của hold-out split
Bài 6 đã giới thiệu train/test split 80/20. Cách này hoạt động tốt khi data rất lớn, nhưng có ba điểm yếu khi dataset cỡ vừa hoặc nhỏ:
- Score phụ thuộc cái split nào. Đổi
random_state→ accuracy có thể nhảy từ 0.91 xuống 0.83. Không biết nên báo cáo con số nào. - Variance lớn khi data nhỏ. Với 200 sample, test set 40 sample chỉ cần lệch vài sample khó là điểm tụt mạnh. Một con số đơn lẻ không phản ánh ổn định.
- Lãng phí data. 20% nằm trong test không được dùng để train. Với dataset nhỏ, mất từng sample đã đáng kể.
Ví dụ minh hoạ với Iris (150 sample):
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
X, y = load_iris(return_X_y=True)
for seed in [0, 1, 2, 3, 4]:
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.2, random_state=seed)
model = LogisticRegression(max_iter=1000).fit(Xtr, ytr)
print(seed, round(model.score(Xte, yte), 3))
Output thực tế (giá trị có thể đổi theo version sklearn) cho ra accuracy dao động khoảng 0.93–1.00 chỉ vì đổi seed. Báo cáo "accuracy 1.00" là sai lệch.
Cross-Validation giải quyết bằng cách train nhiều lần trên nhiều split khác nhau và lấy thống kê.
Cross-Validation — ý tưởng K-Fold
K-Fold CV chia toàn bộ data thành \(K\) phần (fold) gần đều nhau, sau đó lặp \(K\) lần:
- Lần \(i = 1, \dots, K\): lấy fold thứ \(i\) làm test set, \(K-1\) fold còn lại làm train.
- Fit model trên train, đo metric trên test → lưu score \(s_i\).
Sau \(K\) lần ta có \(K\) score. Thông tin báo cáo gồm 2 con số:
- Mean: \(\bar s = \frac{1}{K} \sum_{i=1}^{K} s_i\) — ước lượng performance trung bình.
- Std: \(\sigma_s\) — độ lệch chuẩn giữa các fold, đo ổn định của model trên các split khác nhau.
Mỗi sample đều xuất hiện đúng 1 lần trong test (qua \(K\) fold) và \(K-1\) lần trong train. Không lãng phí data, không phụ thuộc 1 split duy nhất.
CV không tạo ra model "tốt hơn"; nó tạo ra ước lượng performance đáng tin cậy hơn. Sau khi đã chọn được model bằng CV, model cuối thường được fit lại trên toàn bộ data train (xem mục 12).
K-Fold với sklearn
API chính nằm trong sklearn.model_selection:
from sklearn.model_selection import KFold, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris
X, y = load_iris(return_X_y=True)
model = LogisticRegression(max_iter=1000)
scores = cross_val_score(model, X, y, cv=5, scoring="accuracy")
print(scores)
print(f"{scores.mean():.3f} ± {scores.std():.3f}")
Khi truyền cv=5 với bài toán classification, sklearn auto dùng StratifiedKFold (xem mục 6). Để khoá thành K-Fold thuần (không stratify), khai báo splitter rõ ràng:
kf = KFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(model, X, y, cv=kf, scoring="accuracy")
Lưu ý ba tham số của KFold:
n_splits— số fold K.shuffle=True— trộn data trước khi chia. Nếu data đang sort theo label (vd Iris được sort theo class), không shuffle sẽ tạo fold lệch hoàn toàn. Mặc địnhshuffle=False.random_state— chỉ có hiệu lực khishuffle=True; cần để reproduce.
Có thể inspect các split trực tiếp:
for fold, (train_idx, test_idx) in enumerate(kf.split(X)):
print(fold, len(train_idx), len(test_idx))
Chọn K thế nào
Không có K tối ưu tuyệt đối; chọn theo cỡ data và ngân sách tính:
- K = 5 hoặc K = 10: mặc định trong đa số paper và sklearn. Cân bằng giữa variance của ước lượng và chi phí (5 hoặc 10 lần train).
- K = n (Leave-One-Out, LOOCV): dataset rất nhỏ (vài chục sample). Mỗi lần test 1 sample. Ước lượng nearly-unbiased nhưng variance cao và chi phí n lần train. Sklearn có
LeaveOneOutriêng. - K = 3 hoặc nhỏ hơn: dataset rất lớn (10⁶+ sample). Mỗi fold train đã rất lâu, giảm K để tiết kiệm. Variance của estimate thấp vì test fold lớn.
Trade-off cơ bản: K lớn → mỗi train fold gần với full dataset → bias thấp nhưng variance cao (các train set rất giống nhau) và chi phí cao. K nhỏ → ngược lại.
Trong thực tế production code, K=5 là điểm khởi đầu hợp lý. Tăng lên 10 khi cần ước lượng chắc hơn và còn ngân sách thời gian.
Stratified K-Fold cho classification
Với classification, K-Fold thuần có thể tạo fold lệch tỉ lệ class. Vd dataset 90% class 0, 10% class 1; nếu chia ngẫu nhiên không stratify, có fold gặp toàn class 0, fold khác lại có 30% class 1 — score giữa các fold biến động vì lý do không liên quan đến model.
StratifiedKFold đảm bảo tỉ lệ class trong mỗi fold xấp xỉ tỉ lệ class của dataset gốc:
from sklearn.model_selection import StratifiedKFold
import numpy as np
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
for i, (tr, te) in enumerate(skf.split(X, y)):
print(i, np.bincount(y[te]))
Mỗi fold sẽ có distribution class gần như nhau. Bài 42 (Class Imbalance) sẽ bàn sâu hơn về imbalanced data.
Sklearn quy ước: nếu task là classifier và cv là một số nguyên, cross_val_score tự dùng StratifiedKFold. Với regressor hoặc khi truyền splitter cụ thể, không có auto-stratify.
Khuyến nghị: với mọi bài classification, mặc định dùng StratifiedKFold. Khi imbalanced rõ rệt (vd 99% vs 1%), càng phải dùng để fold không bị thiếu class hiếm.
Group K-Fold — tránh group leak
Nhiều dataset có cấu trúc nhóm: nhiều record cùng thuộc một group. Ví dụ:
- Y tế: nhiều lần đo của cùng 1 bệnh nhân.
- Speech: nhiều câu của cùng 1 speaker.
- Hình ảnh: nhiều ảnh của cùng 1 user.
Nếu để record cùng group rơi vào cả train lẫn test, model có thể học "vân tay" của group đó thay vì pattern khái quát → score test cao giả tạo. Đây là dạng data leak khó phát hiện.
GroupKFold đảm bảo mỗi group chỉ xuất hiện ở train hoặc test, không cả hai:
from sklearn.model_selection import GroupKFold, cross_val_score
groups = patient_id # 1 array cùng độ dài với X, giá trị là group id
gkf = GroupKFold(n_splits=5)
scores = cross_val_score(model, X, y, groups=groups, cv=gkf, scoring="accuracy")
Phải truyền groups qua cross_val_score, không chỉ qua gkf.split. Quên truyền là kết quả vẫn chạy nhưng không group-safe.
Sklearn còn có StratifiedGroupKFold (từ sklearn 1.0+) kết hợp cả stratify class và bảo toàn group — dùng khi cần cả hai.
TimeSeriesSplit cho dữ liệu thời gian
Với time series, không được shuffle: train phải luôn ở quá khứ, test ở tương lai. Trộn lẫn sẽ làm model "nhìn thấy tương lai" → score giả tạo, deploy mới biết sai.
TimeSeriesSplit chia data theo thứ tự thời gian, mỗi fold mở rộng train, test là đoạn kế tiếp:
from sklearn.model_selection import TimeSeriesSplit
tss = TimeSeriesSplit(n_splits=5)
for i, (tr, te) in enumerate(tss.split(X)):
print(i, tr[:5], "...", tr[-5:], "->", te[:5])
Cấu trúc fold:
- Fold 1: train [0..n/6], test [n/6..2n/6].
- Fold 2: train [0..2n/6], test [2n/6..3n/6].
- ... train tăng dần, test luôn ở phía sau.
Tham số quan trọng:
n_splits— số fold.max_train_size— giới hạn độ dài train (rolling window thay vì expanding window).gap— khoảng cách giữa train và test (tránh leak qua autocorrelation gần).test_size— số sample mỗi test fold.
Yêu cầu: X phải được sort theo thời gian trước khi truyền vào.
Repeated K-Fold — giảm variance của CV score
Bản thân CV score cũng có variance: chạy K-Fold với 2 random_state khác nhau, mean có thể khác chút. Khi cần so sánh 2 model rất sát nhau (chênh 0.01 accuracy), variance này có thể quyết định kết luận.
RepeatedKFold chạy K-Fold nhiều lần với seed khác nhau, sau đó pool tất cả score:
from sklearn.model_selection import RepeatedKFold, RepeatedStratifiedKFold
rkf = RepeatedKFold(n_splits=5, n_repeats=10, random_state=42)
scores = cross_val_score(model, X, y, cv=rkf)
print(f"{scores.mean():.3f} ± {scores.std():.3f} (n={len(scores)})")
Kết quả: 50 score (5 × 10), mean ổn định hơn nhiều so với chạy K-Fold 1 lần. Tương tự có RepeatedStratifiedKFold cho classification.
Chi phí: K × n_repeats lần train. Chỉ dùng khi thực sự cần ước lượng chắc — vd benchmark cho paper, hoặc khi 2 model có CV score quá sát nhau.
cross_validate vs cross_val_score
cross_val_score đơn giản: 1 metric, trả về array điểm test. Khi cần nhiều thông tin hơn, dùng cross_validate:
from sklearn.model_selection import cross_validate
scoring = ["accuracy", "f1_macro", "roc_auc_ovr"]
cv_result = cross_validate(
model, X, y,
cv=5,
scoring=scoring,
return_train_score=True,
return_estimator=False,
n_jobs=-1,
)
for key in cv_result:
if key.startswith("test_") or key.startswith("train_"):
print(key, cv_result[key].mean().round(3))
print("fit_time", cv_result["fit_time"].mean().round(3))
print("score_time", cv_result["score_time"].mean().round(3))
Khi nào dùng cross_validate:
- Cần nhiều metric cùng lúc (accuracy + F1 + ROC-AUC).
- Cần
train_scoređể chẩn đoán overfit (so sánh vớitest_score, bài 38 đã bàn). - Cần đo
fit_time/score_timeđể so sánh chi phí model. - Cần
return_estimator=Trueđể inspect model của từng fold (vd lấycoef_trung bình).
CV + Pipeline để tránh data leak
Đây là điểm sai phổ biến nhất khi mới dùng CV. Pattern sai:
# SAI — scaler fit trên TOÀN BỘ X, kể cả phần sẽ làm test fold
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
scores = cross_val_score(LogisticRegression(), X_scaled, y, cv=5)
Vì scaler đã thấy distribution của toàn bộ data trước khi CV chia fold, mean/std của test fold đã rỉ vào train. CV score sẽ cao hơn thực tế deploy.
Pattern đúng — bọc preprocessing và model trong Pipeline, truyền pipeline vào CV:
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
pipe = Pipeline([
("scaler", StandardScaler()),
("model", LogisticRegression(max_iter=1000)),
])
scores = cross_val_score(pipe, X, y, cv=5, scoring="accuracy")
print(f"{scores.mean():.3f} ± {scores.std():.3f}")
Với pattern này, mỗi fold:
- CV cắt train fold và test fold.
pipe.fit(X_train_fold, y_train_fold)— scaler fit chỉ trên train fold.pipe.score(X_test_fold, y_test_fold)— scaler chỉtransformtrên test fold.
Không có chỗ nào scaler "thấy" test fold trước khi đánh giá. Bài 14 đã giới thiệu Pipeline; CV là một trong những lý do chính khiến Pipeline gần như bắt buộc trong workflow ML production.
Quy tắc đơn giản: bất kỳ bước nào có fit phải nằm trong Pipeline trước khi vào CV. Bao gồm scaler, encoder, imputer, PCA, feature selection, SMOTE, target encoder.
Nested CV khi tune hyperparameter
Khi vừa tune hyperparameter, vừa muốn ước lượng performance, một lần CV là chưa đủ. Bởi vì:
- Bạn thử nhiều bộ hyperparameter, chọn bộ có CV score cao nhất.
- CV score đã được dùng để chọn — nó không còn là ước lượng unbiased của model cuối.
- Tương tự việc overfit trên test set khi tune lặp đi lặp lại.
Nested CV giải quyết bằng 2 vòng CV lồng nhau:
- Inner CV (vd 3-fold) — dùng để tune hyperparameter trên train fold của vòng ngoài.
- Outer CV (vd 5-fold) — dùng để đo performance của "thuật toán cộng với cách tune", trên data outer test fold mà inner CV chưa thấy.
from sklearn.model_selection import GridSearchCV, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
pipe = Pipeline([
("scaler", StandardScaler()),
("model", LogisticRegression(max_iter=1000)),
])
param_grid = {"model__C": [0.01, 0.1, 1.0, 10.0]}
inner_cv = 3
outer_cv = 5
search = GridSearchCV(pipe, param_grid, cv=inner_cv, scoring="accuracy", n_jobs=-1)
nested_scores = cross_val_score(search, X, y, cv=outer_cv, scoring="accuracy")
print(f"Nested CV: {nested_scores.mean():.3f} ± {nested_scores.std():.3f}")
Khi nào cần Nested CV:
- Báo cáo performance cho paper / report có yêu cầu chặt chẽ.
- So sánh nhiều thuật toán với tune riêng cho từng cái.
- Dataset nhỏ và không có hold-out test riêng.
Khi không cần Nested CV:
- Có test set độc lập riêng (giữ kín, chỉ dùng 1 lần ở cuối) — dùng CV trên train+val, eval trên test.
- Sản phẩm sẽ retrain định kỳ, performance "production" được monitor sau deploy.
Chi phí Nested CV = inner × outer × số combination hyperparameter. Bài 40 sẽ deep về Grid Search; bài 41 thêm Random Search và Bayesian.
CV vs Train/Val/Test split
Bài 6 dạy split 3 phần. Bài này dạy CV. Khi nào dùng cái nào:
- Train / Val / Test split: dataset đủ lớn (vd 10⁴+ sample/class), ngân sách tính bị giới hạn, hoặc workflow đã có pipeline tách biệt (Kaggle với public/private LB cũng tương tự). Một split là đủ ổn định.
- K-Fold CV: dataset nhỏ/vừa, cần ước lượng performance ổn định với CI rõ ràng, cần tune hyperparameter mà không có val set riêng.
- Kết hợp: phổ biến nhất trong production. Giữ riêng một test set kín ngay từ đầu. Trên phần còn lại, làm CV để tune và chọn model. Cuối cùng, fit model đã chọn trên toàn bộ train+val và đánh giá 1 lần duy nhất trên test.
Lý do của pattern kết hợp: CV chống bias trong quá trình chọn model, hold-out test giữ ước lượng cuối thực sự unseen.
scoring parameter và custom scorer
Tham số scoring nhận chuỗi tên metric (xem danh sách đầy đủ ở sklearn.metrics.get_scorer_names()). Một số tên hay dùng:
- Classification:
"accuracy","precision","recall","f1","roc_auc","average_precision","log_loss". - Multi-class:
"f1_macro","f1_micro","f1_weighted","roc_auc_ovr","roc_auc_ovo". - Regression:
"r2","neg_mean_squared_error","neg_root_mean_squared_error","neg_mean_absolute_error".
Vì sao có tiền tố "neg_": sklearn convention là "higher is better" cho mọi scorer. MSE/MAE/RMSE bản thân là "lower is better" nên được phủ định để khớp convention.
Custom scorer khi metric chưa có sẵn:
from sklearn.metrics import make_scorer
import numpy as np
def asymmetric_loss(y_true, y_pred):
# Phạt false negative gấp 5 lần false positive
diff = y_pred - y_true
return np.where(diff < 0, -5 * diff, diff).mean()
scorer = make_scorer(asymmetric_loss, greater_is_better=False)
scores = cross_val_score(model, X, y, cv=5, scoring=scorer)
Hai tham số make_scorer hay quên:
greater_is_better=True/False— sklearn dùng để biết tối ưu theo hướng nào (vd GridSearch). Nếu hàm là loss, đểFalse; sklearn sẽ tự đảo dấu.needs_proba=Truehoặcneeds_threshold=True— khi metric cần xác suất (predict_proba) hoặc decision function thay vìpredictthô.
Pitfall thường gặp
- Preprocessing không qua Pipeline: fit scaler/encoder trên toàn bộ data trước CV → leak. Mục 11 đã chỉ rõ.
- Shuffle time series: dùng
KFold(shuffle=True)hoặcStratifiedKFoldtrên dữ liệu có thứ tự thời gian. Phải dùngTimeSeriesSplit. - Group leak: data có cấu trúc group nhưng dùng K-Fold thuần. Phải
GroupKFoldvà truyềngroups. - K quá ít trên dataset nhỏ: K=3 với 60 sample → test fold chỉ 20 sample, variance giữa các fold rất cao. Tăng K hoặc dùng
RepeatedKFold. - Quên shuffle với data đã sort: Iris original sort theo class; K-Fold không shuffle sẽ tạo fold gần như đơn-class. Luôn
shuffle=True(hoặc dùngStratifiedKFoldauto-stratify). - So sánh 2 model bằng chênh lệch mean nhỏ hơn std: chênh 0.005 mà std mỗi model là 0.02 → không có ý nghĩa thống kê. Cần thêm test (vd paired t-test) hoặc
RepeatedKFold. - Báo cáo CV score như test score cuối cùng: nếu CV đã được dùng để chọn model / tune hyperparameter, đây là số "biased optimistic". Cần Nested CV hoặc hold-out test riêng.
- Dùng
scoring="neg_mean_squared_error"rồi quên đảo dấu: in ra số âm và tưởng model tệ. Nhớ-scores.mean()khi report.
Cost và parallel với n_jobs
K-Fold cần K lần fit. Nếu mỗi fit tốn 30 giây, 5-fold là 2.5 phút, 10-fold là 5 phút. Nested CV với 4 hyperparameter combinations và 3×5 fold là 60 lần fit — 30 phút.
Các fold độc lập nhau nên parallel được:
scores = cross_val_score(pipe, X, y, cv=5, scoring="accuracy", n_jobs=-1)
n_jobs=-1 dùng toàn bộ CPU core. n_jobs=4 dùng đúng 4 process. n_jobs=1 tuần tự (mặc định).
Lưu ý:
- Mỗi process load 1 bản copy của data → tốn RAM. Với dataset rất lớn (>vài GB), parallel có thể OOM. Cân nhắc giảm
n_jobs. - Một số estimator nội bộ đã parallel (
RandomForestClassifier(n_jobs=-1)). Lồng 2 lớp parallel có thể oversubscribe CPU. Thường để parallel ở 1 lớp (CV hoặc model, không cả hai). - Trên Windows, parallel với
joblibcó thể chậm khởi tạo. Trên Linux nhanh hơn nhờ fork.
Code Python — Iris end-to-end
Một workflow đầy đủ: K-Fold, Stratified, Pipeline + CV, multi-metric:
import numpy as np
from sklearn.datasets import load_iris
from sklearn.model_selection import (
KFold, StratifiedKFold, cross_val_score, cross_validate
)
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
X, y = load_iris(return_X_y=True)
# 1. K-Fold thuần (không stratify) — cần shuffle vì Iris sort theo class
kf = KFold(n_splits=5, shuffle=True, random_state=42)
scores_kf = cross_val_score(
LogisticRegression(max_iter=1000), X, y, cv=kf, scoring="accuracy"
)
print(f"KFold: {scores_kf.mean():.3f} ± {scores_kf.std():.3f}")
# 2. Stratified K-Fold — giữ tỉ lệ class
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores_skf = cross_val_score(
LogisticRegression(max_iter=1000), X, y, cv=skf, scoring="accuracy"
)
print(f"StratifiedKFold: {scores_skf.mean():.3f} ± {scores_skf.std():.3f}")
# 3. So sánh class distribution trong test fold
print("\nClass distribution mỗi test fold:")
print(" KFold: ", [np.bincount(y[te]) for _, te in kf.split(X)])
print(" StratifiedKFold: ", [np.bincount(y[te]) for _, te in skf.split(X, y)])
# 4. Pipeline + CV — pattern chuẩn để không leak
pipe = Pipeline([
("scaler", StandardScaler()),
("model", LogisticRegression(max_iter=1000)),
])
scores_pipe = cross_val_score(pipe, X, y, cv=5, scoring="accuracy", n_jobs=-1)
print(f"\nPipeline CV: {scores_pipe.mean():.3f} ± {scores_pipe.std():.3f}")
# 5. Multi-metric với cross_validate
cv_result = cross_validate(
pipe, X, y,
cv=5,
scoring=["accuracy", "f1_macro"],
return_train_score=True,
n_jobs=-1,
)
print("\nMulti-metric CV:")
print(f" test_accuracy: {cv_result['test_accuracy'].mean():.3f} "
f"± {cv_result['test_accuracy'].std():.3f}")
print(f" train_accuracy: {cv_result['train_accuracy'].mean():.3f} "
f"± {cv_result['train_accuracy'].std():.3f}")
print(f" test_f1_macro: {cv_result['test_f1_macro'].mean():.3f}")
print(f" fit_time avg: {cv_result['fit_time'].mean():.4f}s")
Quan sát điển hình khi chạy: KFold và StratifiedKFold cho mean rất gần nhau trên Iris (vì 3 class cân bằng 50/50/50), nhưng std của Stratified thường thấp hơn chút. Train accuracy cao hơn test accuracy ~0.01–0.03 là dấu hiệu fit tốt, không overfit nặng (xem bài 38).
Bài tập thực hành
Bài 1. Trên dataset sklearn.datasets.load_breast_cancer, chạy 5-Fold CV cho 3 model: LogisticRegression, RandomForestClassifier(n_estimators=100), SVC(kernel="rbf"). In mean ± std accuracy và F1 cho mỗi model. Model nào tốt nhất? Chênh lệch có lớn hơn std không?
Bài 2. Tạo dataset imbalanced với make_classification(weights=[0.95, 0.05], n_samples=1000, random_state=0). Chạy:
- 5-Fold CV không stratify (
KFold(shuffle=True)) — in bincount mỗi test fold. - 5-Fold Stratified CV — in bincount mỗi test fold.
So sánh distribution. So sánh F1 (không phải accuracy) của 2 cách. Stratified ổn định hơn ở đâu?
Bài 3. Dataset có cấu trúc group sau:
import numpy as np
rng = np.random.default_rng(0)
n_groups = 20
samples_per_group = 10
X = rng.normal(size=(n_groups * samples_per_group, 4))
# Mỗi group có offset riêng để model "nhớ" group là cheat được
group_offsets = rng.normal(scale=2.0, size=n_groups)
X = X + np.repeat(group_offsets, samples_per_group).reshape(-1, 1)
y = (X[:, 0] + rng.normal(scale=0.5, size=len(X)) > 0).astype(int)
groups = np.repeat(np.arange(n_groups), samples_per_group)
Chạy 2 CV với RandomForestClassifier: KFold(5, shuffle=True) và GroupKFold(5). So sánh score. Cái nào tin được? Giải thích vì sao.
Bài 4. Trên dataset Bài 1, viết Pipeline gồm StandardScaler + LogisticRegression. Chạy 2 lần CV để so sánh:
- Phiên bản LEAK:
scaler.fit_transform(X)trước, rồicross_val_score(LogReg, X_scaled, y, cv=5). - Phiên bản ĐÚNG:
cross_val_score(pipe, X, y, cv=5)với pipe = StandardScaler + LogReg.
Chênh lệch trên dataset này có thể nhỏ, nhưng giải thích vì sao về nguyên lý phiên bản LEAK là sai.
Bài 5 (nâng cao). Setup Nested CV cho dataset Bài 1:
- Inner CV = 3-fold StratifiedKFold tune
C ∈ {0.01, 0.1, 1, 10}choLogisticRegression. - Outer CV = 5-fold StratifiedKFold đánh giá performance.
So sánh Nested CV score với CV score "phẳng" (tune và đánh giá trên cùng 1 CV). Nested CV thường thấp hơn — giải thích.
Bài tiếp theo
Bài 40: Grid Search — có CV làm khung đánh giá, bài tiếp tận dụng nó để quét hyperparameter một cách có hệ thống: GridSearchCV, param_grid với cú pháp __ cho Pipeline, best_params_, refit, và khi nào Grid Search không còn khả thi.
Tài liệu tham khảo
- scikit-learn — User Guide: Cross-validation
- scikit-learn — KFold API reference
- scikit-learn — StratifiedKFold API reference
- scikit-learn — GroupKFold API reference
- scikit-learn — TimeSeriesSplit API reference
- scikit-learn — RepeatedKFold API reference
- scikit-learn — cross_val_score API reference
- scikit-learn — cross_validate API reference
- scikit-learn — Example: Nested versus non-nested cross-validation
- scikit-learn — The scoring parameter and make_scorer
