Chương 1·Bài 1.3

Data split và cross-validation

Để biết model đoán tốt trên dữ liệu mới, ta giữ riêng một phần dữ liệu không dùng để học, gọi là validation. Cách chia phải giống lúc dùng thật. Ví dụ model sẽ dùng cho học sinh ở một trường mới, còn mỗi học sinh có ba bài làm trong dữ liệu. Nếu chia ngẫu nhiên từng bài làm, cùng một học sinh sẽ có mặt ở cả train lẫn validation, và điểm validation sẽ cao hơn thực tế. Bài này học các cách chia (ngẫu nhiên, theo nhóm, theo thời gian) và cross-validation: chia nhiều lần rồi lấy trung bình để kết quả bớt phụ thuộc may rủi.

Trước hết, hỏi mẫu mới là ai

Trước khi chia, hỏi: lúc dùng thật, mẫu mới là gì? Đoán thêm một bài làm của học sinh đã quen khác với đoán cho một học sinh chưa từng thấy. Dự báo buổi sinh hoạt tuần sau khác với đoán một buổi lấy ngẫu nhiên trong cùng giai đoạn. Mỗi câu trả lời cần một cách chia riêng:

Tình huống cần mô phỏng Kiểu split thường phù hợp Điều cần kiểm
Mẫu độc lập, cùng phân phối Random holdout hoặc K-fold Không trùng bản ghi
Classification ít mẫu một lớp Stratified split/K-fold Số mẫu từng lớp đủ
Người, trường hoặc thiết bị chưa thấy Group split Group không xuất hiện ở hai phía
Dự đoán thời điểm tương lai Split theo thời gian Train đứng trước validation

Có bài cần cả hai yêu cầu cùng lúc. Ví dụ dự đoán tháng tới cho một trường mới: validation phải là những trường không có trong train, và phải nằm sau train về thời gian.

Chia ngẫu nhiên: năm người ở cả hai phía. Chia theo nhóm: mỗi người một phía.Random splitngười 0trùngngười 1trùngngười 2trùngngười 3trùngngười 4người 5trùngGroup splittrainvalidation
Chia ngẫu nhiên: năm người ở cả hai phía. Chia theo nhóm: mỗi người một phía.

Hỏi nhanh

Mô hình sẽ dùng để đoán cho những trường chưa có trong train. Mỗi học sinh có nhiều bản ghi. Ta nên để riêng dữ liệu validation theo đơn vị nào?

Stratified split giữ lớp, không giữ người

Classification có nhãn 1 là “cần gợi ý”, nhãn 0 là “chưa cần”. Nếu lớp 1 hiếm, chia ngẫu nhiên có thể tạo validation không có mẫu lớp 1. Khi ấy không đo được khả năng phát hiện lớp này, và một số metric còn không xác định. Chuyện này không hiếm: với 12 mẫu gồm ba nhãn 1, chọn ngẫu nhiên ba mẫu làm validation có (123)=220\binom{12}{3} = 220 cách, trong đó (93)=84\binom{9}{3} = 84 cách chỉ toàn nhãn 0. Xác suất validation không có nhãn 1 nào là 84/220≈0.3884/220 \approx 0.38.

Tệ hơn nữa là dữ liệu xếp sẵn theo lớp, như 800 dòng lớp A rồi 200 dòng lớp B: cắt theo vị trí thì một phía không có lớp B nào. Vì vậy, khi dữ liệu không có yếu tố thời gian, phải xáo trộn trước khi chia.

Stratified split chia sao cho tỉ lệ các lớp ở mỗi phần gần bằng tỉ lệ của cả bảng; với train_test_split chỉ cần thêm stratify=y. Nó chỉ lo tỉ lệ lớp: cùng một người vẫn có thể nằm ở cả hai phía. Nếu cần cả hai, scikit-learn có StratifiedGroupKFold.

Python
import numpy as np
from sklearn.model_selection import train_test_split

labels = np.array([1, 1, 1] + [0] * 9)
row_ids = np.arange(len(labels))
train_ids, valid_ids = train_test_split(
    row_ids, test_size=0.25, random_state=13, stratify=labels
)
print("Số mẫu train/valid:", len(train_ids), len(valid_ids))
print("Số nhãn 1 train/valid:", labels[train_ids].sum(), labels[valid_ids].sum())
print("Trùng hàng:", len(set(train_ids) & set(valid_ids)))

Validation có ba mẫu nên không thể có đúng 25% mẫu lớp 1; code giữ một mẫu lớp 1 ở validation, hai ở train. Khi chia thành nhiều phần, lớp hiếm nhất phải có ít nhất bằng số phần thì phần nào cũng có lớp ấy.

Hỏi nhanh

Mỗi học sinh có nhiều bài làm trong dữ liệu. Ta dùng stratified split. Cách chia này bảo đảm được điều gì?

Group split: kiểm giao của tập người

Code dưới tạo sáu học sinh, mỗi người ba bản ghi. groups đi cùng hàng để chỉ ra người sở hữu bản ghi; không cần đưa mã người vào feature. Trước tiên nhìn một random split, rồi kiểm ba vòng GroupKFold.

Python
from sklearn.model_selection import GroupKFold

groups = np.repeat(np.arange(6), 3)
X_group = np.zeros((18, 1))                 # chỉ dùng để minh họa chỉ số
y_group = np.repeat([0, 1, 0, 1, 0, 1], 3)
all_ids = np.arange(18)
train_ids, valid_ids = train_test_split(
    all_ids, test_size=1/3, random_state=13
)
shared = set(groups[train_ids]) & set(groups[valid_ids])
print("Random split, người trùng:", sorted(map(int, shared)))

group_cv = GroupKFold(n_splits=3)
for fold, (train_ids, valid_ids) in enumerate(
    group_cv.split(X_group, y_group, groups), start=1
):
    shared = set(groups[train_ids]) & set(groups[valid_ids])
    assert not shared
    print(f"Fold {fold}: valid groups={sorted(map(int, set(groups[valid_ids])))}, trùng={len(shared)}")

Chia ngẫu nhiên làm năm trong sáu người có bài ở cả hai phía. Với GroupKFold, mỗi vòng không có người nào chung giữa train và validation. Để ý: phải kiểm giao của tập người, không chỉ giao của chỉ số hàng, vì hai hàng khác nhau vẫn có thể của cùng một người.

GroupKFold nhận thêm mảng groups và bảo đảm mỗi nhóm chỉ nằm ở một phía. Nhóm có thể là mọi ảnh của một bệnh nhân, mọi bản ghi của một học sinh. Nếu để người đã thấy lẫn vào validation, điểm sẽ cao hơn thực tế: model nhận ra người quen chứ chưa chắc làm tốt với người lạ. Vài trường hợp khác cũng cần gom nhóm:

  • Cùng một ảnh lưu thành nhiều bản: cắt, đổi cỡ, đổi tên file. Gom các bản này vào một nhóm.
  • Các khung hình liền nhau của một video gần như giống hệt nhau: chia theo video, không xáo từng khung.

Chia theo nhóm có thể làm tỉ lệ nhãn giữa các fold lệch nhau, nhất là khi ít nhóm hoặc có nhóm rất lớn, nên hãy in số mẫu và số nhãn của từng fold để biết.

Hỏi nhanh

Bộ ảnh bài làm viết tay có nhiều ảnh là bản cắt, bản xoay nhẹ, hoặc bản chụp lại từ cùng một tờ giấy. Chia ngẫu nhiên theo ảnh thì validation đúng 99%, nhưng gặp bài của lớp khác chỉ đúng 80%. Ta nên sửa cách chia thế nào?

Thời gian: quá khứ học, tương lai chấm

Muốn dự báo buổi tuần sau, model chỉ được học từ những buổi đã diễn ra trước đó. Nếu xáo trộn rồi chia, train có thể chứa các tuần nằm sau validation: model được học từ tương lai, điều không thể có lúc dùng thật.

Train luôn đứng trước; bỏ một tuần sát ranh giới rồi chấm hai tuần sau.Fold 1gapFold 2gapFold 3gap01234567891011tuần
Train luôn đứng trước; bỏ một tuần sát ranh giới rồi chấm hai tuần sau.

TimeSeriesSplit chia theo thời gian, mỗi vòng train dài thêm. Code dưới dùng 12 tuần đã sắp thứ tự, mỗi lần validation hai tuần; gap=1 bỏ một tuần sát ranh giới giữa train và validation. Ở bài thật, gap dài bao nhiêu phải tính theo lúc nào nhãn được biết; mục sau có ví dụ.

Python
from sklearn.model_selection import TimeSeriesSplit

weeks = np.arange(12)
time_cv = TimeSeriesSplit(n_splits=3, test_size=2, gap=1)
for fold, (train_ids, valid_ids) in enumerate(time_cv.split(weeks), start=1):
    assert weeks[train_ids].max() < weeks[valid_ids].min()
    print(f"Fold {fold}: train={train_ids.tolist()}, valid={valid_ids.tolist()}")

Dùng số liệu của tuần trước làm feature, như số đăng ký tuần trước, là được, vì lúc dự báo tuần này đã biết số đó. Điều cần kiểm là: feature hay nhãn của train có chứa thông tin mà lúc dự báo validation chưa thể biết không.

Hai lưu ý: TimeSeriesSplit không tự đọc cột ngày, nên phải sắp các hàng theo thời gian trước. Và nó chia theo số hàng, nên nếu các hàng cách nhau không đều, mỗi phần validation có thể dài ngắn khác nhau về thời gian; khi đó nên tự chia theo mốc ngày.

Hỏi nhanh

Nhãn của mỗi hàng là tổng số người đến trong ba tuần tiếp theo. Ta chỉ bảo đảm ngày bắt đầu của mọi hàng train sớm hơn validation. Như vậy đã đủ chưa?

Nhãn là tổng ba tuần sau, nên hàng train sát ranh giới đã nhìn vào validation.0123456789101112trainvalidationhàng tuần 6nhãn: tổng tuần 7–9
Nhãn là tổng ba tuần sau, nên hàng train sát ranh giới đã nhìn vào validation.

K-fold và dự đoán out-of-fold

K-fold cross-validation chia dữ liệu thành KK phần bằng nhau, gọi là fold. Làm KK vòng: mỗi vòng giữ một fold để chấm và học trên K−1K - 1 fold còn lại. Sau KK vòng, mẫu nào cũng được chấm đúng một lần, bởi model chưa học nó.

  • So với chia một lần, kết quả ổn định hơn vì dùng hết dữ liệu để chấm, nhất là khi dữ liệu ít.
  • Đổi lại phải huấn luyện KK lần; với model lớn và dữ liệu nhiều, người ta thường chỉ chia một lần.
  • KK bằng số mẫu gọi là leave-one-out (LOOCV): mỗi vòng giữ riêng đúng một mẫu, nn mẫu thì nn vòng.

Điểm cuối thường là trung bình điểm của các fold:

sˉ=1K∑k=1Ksk.\bar s=\frac1K\sum_{k=1}^{K}s_k.

Một cách khác là gộp dự đoán của mọi fold rồi tính điểm một lần. Hai cách cho kết quả khác nhau khi các fold không bằng nhau, hoặc với metric như F1 (bài 1.5); nên ghi rõ đang báo cách nào.

Code dưới tạo dữ liệu phân loại giả 96 mẫu, bốn feature, chia ba fold stratified. Model LogisticRegression trả về xác suất thuộc lớp 1 (bài 2.3 sẽ học model này). Ghép dự đoán của ba vòng lại được một dự đoán cho mỗi mẫu, đều do model chưa học mẫu đó tạo ra; gọi là dự đoán out-of-fold (OOF).

Python
from sklearn.datasets import make_classification
from sklearn.model_selection import StratifiedKFold
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score

X, y = make_classification(
    n_samples=96, n_features=4, n_informative=3,
    n_redundant=0, random_state=13,
)
cv = StratifiedKFold(n_splits=3, shuffle=True, random_state=13)
oof_probability = np.full(len(y), np.nan)
times_scored = np.zeros(len(y), dtype=int)
fold_scores = []
for fold, (train_ids, valid_ids) in enumerate(cv.split(X, y), start=1):
    model = LogisticRegression(max_iter=500).fit(X[train_ids], y[train_ids])
    probability = model.predict_proba(X[valid_ids])[:, 1]
    oof_probability[valid_ids] = probability       # ghi về đúng vị trí gốc
    times_scored[valid_ids] += 1
    score = accuracy_score(y[valid_ids], probability >= 0.5)
    fold_scores.append(score)
    print(f"Fold {fold}: n={len(valid_ids)}, accuracy={score:.3f}")
assert np.all(times_scored == 1)
assert np.isfinite(oof_probability).all()
print("OOF shape:", oof_probability.shape)
print(f"Mean fold accuracy={np.mean(fold_scores):.3f}")
print(f"Pooled OOF accuracy={accuracy_score(y, oof_probability >= 0.5):.3f}")

[:, 1] lấy cột xác suất của lớp 1. Điều quan trọng là code ghi dự đoán vào đúng vị trí valid_ids của mảng OOF, chứ không nối các đoạn theo thứ tự fold; nối như vậy thì dự đoán bị lệch sang hàng khác. Mảng times_scored kiểm rằng không hàng nào bị bỏ sót hay chấm hai lần.

Mỗi fold chấm một phần; OOF ghép về đúng vị trí, mỗi mẫu một lần.Fold 1Fold 2Fold 3OOF023
Mỗi fold chấm một phần; OOF ghép về đúng vị trí, mỗi mẫu một lần.
StratifiedKFold 3 fold của bài trên 96 mẫu: mỗi fold, model chưa thấy các ô viền cam sẽ đoán chúng; xác suất (xanh 0, cam 1) ghi về đúng vị trí cho đủ cột OOF.

Với TimeSeriesSplit, các tuần đầu chỉ làm train, không bao giờ được chấm, nên mảng OOF có những ô trống. Đánh dấu hàng nào đã được chấm và chỉ tính điểm trên những hàng đó; không lấy dự đoán trên train để lấp chỗ trống.

Hỏi nhanh

Một đội dùng điểm OOF để chọn feature, rồi lại dùng nó để chọn giữa nhiều mô hình. Điểm OOF của phương án thắng còn được coi là điểm test độc lập không?

Khi đi thi

  • Hỏi trước mẫu mới là ai: một hàng mới, một người mới hay một thời điểm sau. Từ đó chọn chia ngẫu nhiên, theo nhóm hay theo thời gian.
  • Dùng cùng các fold cho mọi phương án đem so.
  • In kiểm từng fold: số mẫu, tỉ lệ lớp, giao của các nhóm, khoảng thời gian.

Tóm tắt

  • Chia dữ liệu theo cách model sẽ được dùng, không chọn cách cho điểm cao nhất.
  • Stratified giữ tỉ lệ lớp; chia theo nhóm để validation toàn người hoặc trường mới; chia theo thời gian để học quá khứ, chấm tương lai.
  • Dự đoán OOF phải ghi vào đúng chỉ số hàng.
  • Cross-validation để chọn model; test riêng để chấm sau khi đã chọn.

Kiểm tra cuối bài

Câu 1

Mô hình sẽ dùng cho những người chưa có trong dữ liệu, mà mỗi người có nhiều bản ghi. Ta nên chia dữ liệu thế nào?

Câu 2

Hai cách làm trên cùng một bộ dữ liệu.

A: chia 70% train, 30% test. Lặp 50 vòng “train, xem điểm test, chỉnh mô hình”, rồi báo điểm test cao nhất là 94.2%.

B: chia 60% train, 20% validation, 20% test. Chỉnh mô hình theo validation, rồi chấm test đúng một lần, được 91.8%.

Con số nào đáng tin hơn để ước lượng mô hình làm tốt đến đâu trên dữ liệu mới?

Đề thiNOAI 2026 Singapore · Vòng sơ loại · Câu 303

Câu 3

Train có học sinh của trường 1 và 2. Validation có học sinh của trường 2 và 3. Không có học sinh nào ở cả hai phía. Cách chia này có kiểm được khả năng đoán cho trường mới không?

Câu 4

Lớp hiếm chỉ có 4 mẫu. Ta muốn chia thành 5 fold validation không chồng lên nhau, fold nào cũng có ít nhất một mẫu của lớp hiếm. Làm được không?

Câu 5

Dữ liệu dự báo có cột time, nhưng các hàng đang bị xáo trộn. Trước khi dùng TimeSeriesSplit, ta phải làm gì để fold nào cũng học quá khứ, đoán tương lai?

Câu 6

Hai fold validation có 10 và 30 mẫu, accuracy lần lượt là 0.6 và 0.8. Gộp cả 40 dự đoán OOF lại thì accuracy là bao nhiêu?

Câu 7

Bài phân loại nhị phân. Mỗi fold cho ra predict_proba(X_valid)[:, 1], và các fold không lấy hàng theo thứ tự. Ta ghi các dự đoán vào mảng OOF thế nào để mỗi số nằm đúng hàng của nó?

Câu 8

Với TimeSeriesSplit, các hàng đầu tiên chưa bao giờ nằm trong validation. Ở những hàng ấy, mảng OOF nên ghi gì?

Câu 9

Câu nào mô tả đúng K-fold cross-validation cho linear regression với một feature xx?

Phỏng theoCMU 10-601 · Practice Problems, Exam 1 · Q3.4

Câu 10

Ta dự báo lúc 8 giờ sáng thứ Hai. Feature A là tổng số đăng ký của tuần trước. Feature B là tổng số người đến trong chính tuần đang dự báo. Cả hai đều có sẵn trong bảng lịch sử, và lúc 8 giờ ta đã biết A. Được dùng feature nào?

Đọc thêm