Danh sách bài viết

Bài 39: Cross-Validation — K-Fold để đánh giá ổn định

Cross-Validation chia data thành K fold, train K lần và tính mean ± std để có ước lượng performance ổn định: K-Fold, Stratified, Group, TimeSeriesSplit, Repeated, cross_validate, Pipeline tránh data leak, Nested CV cho hyperparameter, scoring và custom scorer.

24/05/2026
14 phút đọc
0 lượt xem
1

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, đọc mean ± 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.
2

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ê.

3

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).

4

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 định shuffle=False.
  • random_state — chỉ có hiệu lực khi shuffle=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))
5

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ó LeaveOneOut riê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.

6

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.

7

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.

8

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.

9

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.

10

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ới test_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ấy coef_ trung bình).
11

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)

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:

  1. CV cắt train fold và test fold.
  2. pipe.fit(X_train_fold, y_train_fold) — scaler fit chỉ trên train fold.
  3. pipe.score(X_test_fold, y_test_fold) — scaler chỉ transform trê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.

12

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.

13

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.

14

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=True hoặc needs_threshold=True — khi metric cần xác suất (predict_proba) hoặc decision function thay vì predict thô.
15

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ặc StratifiedKFold trên dữ liệu có thứ tự thời gian. Phải dùng TimeSeriesSplit.
  • Group leak: data có cấu trúc group nhưng dùng K-Fold thuần. Phải GroupKFold và truyền groups.
  • 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ùng StratifiedKFold auto-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.
16

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 joblib có thể chậm khởi tạo. Trên Linux nhanh hơn nhờ fork.
17

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).

18

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)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ồi cross_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} cho LogisticRegression.
  • 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.

19

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.