Nếu bạn từng chuẩn hoá dữ liệu trên toàn bộ bảng rồi mới chia train/test, từng chọn siêu tham số bằng cách nhìn điểm trên tập kiểm tra, hoặc từng quên áp đúng phép tiền xử lý khi đưa mô hình vào sản phẩm, thì bạn đã gặp đúng những vấn đề mà scikit-learn sinh ra để giải quyết bằng ba công cụ: Pipeline, cross-validation và GridSearchCV. Bài này xây bài toán dự đoán khách hàng rời bỏ dịch vụ (dữ liệu giả lập) từ đầu đến cuối: xử lý giá trị thiếu, mã hoá biến phân loại, chuẩn hoá biến số, đánh giá bằng cross-validation phân tầng, tìm siêu tham số, rồi chứng minh bằng số liệu thật rằng rò rỉ dữ liệu có thể biến nhiễu thuần tuý thành mô hình "đạt" AUC 0,87.

Bài tiếp nối Hồi quy tuyến tính và gradient descent: tự cài đặt bằng NumPy, nơi ta thấy vì sao phải chuẩn hoá đặc trưng, và Machine learning là gì: bài toán, dữ liệu và quy trình một dự án AI, nơi mô tả quy trình tổng thể. Dữ liệu bảng cho scikit-learn được làm sạch bằng kỹ thuật ở Làm sạch dữ liệu crawl bằng pandas: chuẩn hoá, khử trùng lặp, kiểu dữ liệu. Môi trường chạy scikit-learn trong bài: Python 3.10, scikit-learn 1.1.3, pandas 1.5, NumPy 1.23 (bản ổn định tại thời điểm viết). Các API dùng trong bài (Pipeline, ColumnTransformer, GridSearchCV, get_feature_names_out) đều có trong scikit-learn 1.1.

Vì sao cần Pipeline trong scikit-learn

Theo tài liệu Pipelines and composite estimators, Pipeline mang lại ba lợi ích: tiện lợi và đóng gói (chỉ gọi fit và predict một lần cho cả chuỗi bước), chọn tham số chung (tìm kiếm lưới trên tham số của mọi bước cùng lúc) và an toàn (tránh rò rỉ thống kê từ dữ liệu kiểm tra vào mô hình khi cross-validation, vì cùng các mẫu huấn luyện được dùng để học cả bộ biến đổi lẫn bộ dự đoán). Mọi bước trừ bước cuối phải là transformer (có fit và transform); bước cuối có thể là mô hình dự đoán.

Nói cách khác, Pipeline biến "quy trình" thành một đối tượng duy nhất có thể tuần tự hoá, kiểm thử và triển khai. Đối tượng đó cũng là đơn vị mà cross-validation và tuning làm việc, nên mọi phép học (điền thiếu, chuẩn hoá, mã hoá, chọn đặc trưng) tự động chỉ nhìn vào phần huấn luyện của mỗi fold.

Dữ liệu: khách hàng rời bỏ dịch vụ (giả lập)

Ta sinh 1.500 khách hàng với ba biến số (số tháng sử dụng, cước hàng tháng, số cuộc gọi hỗ trợ), hai biến phân loại (gói cước, hình thức thanh toán), nhãn roi_bo nhị phân, và cố ý gài 90 giá trị thiếu ở cuoc_hang_thang và goi_cuoc. Công thức logit nằm ngay trong đoạn code để bạn đối chiếu với hệ số học được ở cuối bài. Dữ liệu hoàn toàn giả lập, không chứa thông tin cá nhân.

import numpy as np
import pandas as pd

rng = np.random.default_rng(7)
n = 1500
df = pd.DataFrame({
    "so_thang_su_dung": rng.integers(1, 60, n),
    "cuoc_hang_thang": rng.normal(250_000, 90_000, n).clip(50_000),
    "so_cuoc_goi_ho_tro": rng.poisson(1.5, n),
    "goi_cuoc": rng.choice(["co_ban", "tieu_chuan", "cao_cap"], n, p=[.5, .35, .15]),
    "thanh_toan": rng.choice(["chuyen_khoan", "the", "tien_mat"], n),
})
# nhãn sinh từ một hàm logistic: xác suất rời bỏ = 1 / (1 + exp(-logit))
logit = (-1.0 - 0.04 * df.so_thang_su_dung + 0.5 * df.so_cuoc_goi_ho_tro
         + 0.000004 * df.cuoc_hang_thang + (df.goi_cuoc == "co_ban") * 0.6
         + (df.thanh_toan == "tien_mat") * 0.4)
df["roi_bo"] = (rng.random(n) < 1 / (1 + np.exp(-logit))).astype(int)
# gài giá trị thiếu: 90 dòng ngẫu nhiên ở mỗi cột
for c in ["cuoc_hang_thang", "goi_cuoc"]:
    df.loc[rng.choice(n, 90, replace=False), c] = np.nan
X, y = df.drop(columns="roi_bo"), df["roi_bo"]
scikit-learn 1.1.3
Kích thước: (1500, 5) | tỉ lệ rời bỏ: 0.465
Giá trị thiếu:
cuoc_hang_thang    90
goi_cuoc           90

Việc đầu tiên với scikit-learn, trước mọi thao tác học, là tách tập kiểm tra và để yên. Theo hướng dẫn Cross-validation, học tham số của một hàm dự đoán rồi kiểm tra trên đúng dữ liệu đó là một sai lầm phương pháp luận. Ta dùng stratify=y để giữ nguyên tỉ lệ nhãn ở cả hai phần:

X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, stratify=y, random_state=42)
train: (1200, 5) test: (300, 5)

ColumnTransformer: mỗi nhóm cột một cách xử lý

Biến số cần điền thiếu (median) rồi chuẩn hoá; biến phân loại cần điền thiếu (giá trị phổ biến nhất) rồi one-hot. ColumnTransformer gán từng chuỗi biến đổi cho từng nhóm cột, rồi ghép kết quả. Tham số handle_unknown="ignore" để khi gặp loại chưa từng thấy lúc dự đoán, mô hình không báo lỗi mà gán toàn số 0.

so = ["so_thang_su_dung", "cuoc_hang_thang", "so_cuoc_goi_ho_tro"]
pl = ["goi_cuoc", "thanh_toan"]
pre = ColumnTransformer([
    ("so", make_pipeline(SimpleImputer(strategy="median"), StandardScaler()), so),
    ("pl", make_pipeline(SimpleImputer(strategy="most_frequent"),
                         OneHotEncoder(handle_unknown="ignore")), pl),
])
pipe = Pipeline([("pre", pre), ("clf", LogisticRegression(max_iter=1000))])

Quy ước đặt tên của scikit-learn bước__tham_số (hai dấu gạch dưới) cho phép truy cập tham số lồng nhau ở bất kỳ tầng nào; đây cũng là cách khai báo lưới tìm kiếm:

Tên tham số dạng bước__tham_số: ['pre__so__simpleimputer__strategy', 'pre__pl__onehotencoder__handle_unknown', 'clf__C']

Chuỗi pre__so__simpleimputer__strategy đi qua bước pre (ColumnTransformer), nhánh so, bộ điền thiếu simpleimputer (tên tự sinh bởi make_pipeline từ tên lớp, viết thường) và cuối cùng là tham số strategy.

Cross-validation phân tầng trong scikit-learn: ước lượng đáng tin hơn

Một lần chia train/validation trong scikit-learn cho điểm số phụ thuộc vào cách chia may rủi. K-fold cross-validation chia tập huấn luyện thành k phần, huấn luyện trên k−1 phần, đánh giá trên phần còn lại, rồi lấy trung bình. Với bài toán phân loại, dùng StratifiedKFold để mỗi fold giữ tỉ lệ các lớp gần bằng toàn bộ; tài liệu scikit-learn minh hoạ việc giữ tỉ lệ lớp bằng ví dụ 45 mẫu âm và 5 mẫu dương. Với lớp hiếm như vậy, chia ngẫu nhiên không phân tầng có thể tạo fold có rất ít mẫu dương hoặc không có, còn StratifiedKFold bảo đảm mỗi fold có đủ cả hai lớp. Nếu truyền cv=5 cho một bộ phân loại, scikit-learn đã tự dùng StratifiedKFold (không xáo trộn); ta khai báo tường minh để đặt shuffle=True và random_state.

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
sc = cross_val_score(pipe, X_tr, y_tr, cv=cv, scoring="roc_auc")
res = cross_validate(pipe, X_tr, y_tr, cv=cv,
                     scoring=["roc_auc", "average_precision", "f1"],
                     return_train_score=True)
[CV 5-fold] ROC AUC từng fold: [0.708 0.707 0.754 0.731 0.768] | TB 0.733 ± 0.024
  train_roc_auc            0.740
  test_roc_auc             0.733
  test_average_precision   0.714
  test_f1                  0.642

Hai điều cần đọc từ kết quả scikit-learn này. Thứ nhất, điểm giữa các fold dao động từ 0,707 đến 0,768; một lần chia đơn lẻ có thể cho bạn con số nằm ở bất kỳ đâu trong khoảng đó, nên báo cáo "trung bình ± độ lệch chuẩn" mới trung thực. Thứ hai, AUC huấn luyện (0,740) gần AUC kiểm tra (0,733): mô hình tuyến tính đơn giản trên dữ liệu này không overfit, vấn đề nằm ở chỗ dữ liệu có nhiễu cao chứ không phải mô hình quá phức tạp. Hàm cross_validate của scikit-learn khác cross_val_score ở chỗ cho phép nhiều metric cùng lúc và trả về thời gian fit/score; điểm huấn luyện mặc định tắt, ta bật bằng return_train_score=True để chẩn đoán overfitting.

Rò rỉ dữ liệu: thí nghiệm với nhiễu thuần tuý

Rò rỉ dữ liệu (data leakage) khi dùng scikit-learn xảy ra khi thông tin không có sẵn lúc dự đoán lọt vào quá trình huấn luyện. Trang Common pitfalls and recommended practices nhấn mạnh phải tách train/test trước mọi bước tiền xử lý và không bao giờ đưa dữ liệu kiểm tra vào fit hay fit_transform. Để thấy mức độ tác hại, ta tạo dữ liệu mà ta biết chắc không có tín hiệu nào: 100 mẫu, 2.000 đặc trưng ngẫu nhiên, nhãn ngẫu nhiên. AUC đúng phải quanh 0,5.

from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import make_pipeline

rng2 = np.random.default_rng(0)
Xn = rng2.normal(size=(100, 2000)); yn = rng2.integers(0, 2, 100)
# SAI: chọn 20 đặc trưng "tốt nhất" trên toàn bộ dữ liệu rồi mới cross-validate
Xs = SelectKBest(f_classif, k=20).fit_transform(Xn, yn)
leak = cross_val_score(LogisticRegression(max_iter=1000), Xs, yn, cv=5, scoring="roc_auc")
# ĐÚNG: đưa bước chọn đặc trưng vào Pipeline
ok_pipe = make_pipeline(SelectKBest(f_classif, k=20), LogisticRegression(max_iter=1000))
good = cross_val_score(ok_pipe, Xn, yn, cv=5, scoring="roc_auc")
[Rò rỉ] dữ liệu nhiễu thuần tuý, 100 mẫu x 2000 đặc trưng
  chọn đặc trưng TRƯỚC khi CV (sai): AUC TB = 0.867
  chọn đặc trưng TRONG Pipeline     : AUC TB = 0.578

Cách làm sai báo AUC 0,867 trên dữ liệu không chứa thông tin gì. Bước chọn đặc trưng đã "nhìn" nhãn của mọi mẫu, kể cả các mẫu về sau nằm trong fold kiểm tra, nên nó chọn đúng những cột tình cờ tương quan với nhãn. Khi nằm trong Pipeline, bước đó chỉ học trên phần huấn luyện của từng fold và kết quả giảm về 0,578, gần mức ngẫu nhiên (với 100 mẫu thì con số này vẫn dao động, không nên kỳ vọng đúng 0,5).

Đây cũng là tinh thần của ví dụ trong tài liệu scikit-learn với 200 mẫu và 10.000 đặc trưng ngẫu nhiên; vấn đề này được phân tích kinh điển trong sách The Elements of Statistical Learning (Hastie, Tibshirani, Friedman), mục 7.10.2, về việc chọn đặc trưng trước khi cross-validation. Bài học: mọi bước có fit đều phải ở trong Pipeline, kể cả chọn đặc trưng, giảm chiều, cân bằng lớp.

Tuning siêu tham số bằng GridSearchCV của scikit-learn

Siêu tham số (hyperparameter) là các tham số không học từ dữ liệu mà ta đặt trước, như C của logistic regression (độ mạnh regularization, C càng nhỏ càng mạnh). Theo tài liệu Tuning the hyper-parameters, GridSearchCV thử mọi tổ hợp trong lưới còn RandomizedSearchCV lấy mẫu một số tổ hợp cố định từ không gian tham số. Vì tìm kiếm của scikit-learn chạy trên Pipeline, ta tinh chỉnh được cả bước tiền xử lý, ở đây là chiến lược điền thiếu mean hay median.

grid = {"clf__C": [0.01, 0.1, 1, 10, 100],
        "pre__so__simpleimputer__strategy": ["mean", "median"]}
gs = GridSearchCV(pipe, grid, cv=cv, scoring="roc_auc", n_jobs=-1)
gs.fit(X_tr, y_tr)
print(gs.best_params_, gs.best_score_)
print(gs.score(X_te, y_te))
[GridSearchCV] best_params_: {'clf__C': 0.1, 'pre__so__simpleimputer__strategy': 'median'}
[GridSearchCV] best CV AUC: 0.734
 param_clf__C param_pre__so__simpleimputer__strategy  mean_test_score  rank_test_score
         0.1                                 median         0.733942                1
         0.1                                   mean         0.733859                2
        0.01                                   mean         0.733677                3
        0.01                                   median         0.733551                4
[Tập test, chạm 1 lần] AUC = 0.790

Có ba điểm đáng chú ý. Điểm một: bốn cấu hình đứng đầu chênh nhau chưa tới 0,0004 AUC, nhỏ hơn rất nhiều so với độ lệch chuẩn giữa các fold (0,024). Nghĩa là trên dữ liệu này, mean hay median, C=0.01 hay 0.1 là các cấu hình không phân biệt được bằng cross-validation; đừng diễn giải "median thắng" như một phát hiện. (Đây là cách đọc thực dụng, không phải kiểm định thống kê, vì các cấu hình dùng chung các fold.)

Điểm hai: GridSearchCV mặc định refit=True, tức sau khi chọn tổ hợp tốt nhất nó huấn luyện lại trên toàn bộ tập train, và gs.score dùng mô hình đó.

Điểm ba: điểm trên tập kiểm tra (0,790) cao hơn điểm cross-validation (0,734). Tập kiểm tra chỉ có 300 mẫu nên chính nó cũng dao động; chênh lệch này là do may rủi của một lần chia chứ không chứng tỏ mô hình tốt hơn dự kiến. Ta chỉ chạm vào tập kiểm tra đúng một lần, sau khi mọi lựa chọn đã chốt; nếu lặp lại "xem test, sửa, xem test", tập kiểm tra trở thành tập validation và con số mất giá trị. Tài liệu khuyến nghị chia thành development set (đưa vào GridSearchCV) và evaluation set để tính chỉ số cuối.

Đổi cả mô hình trong lưới và dùng RandomizedSearchCV

Vì bước clf cũng là một tham số của Pipeline, ta có thể đặt nhiều loại mô hình vào cùng một lần tìm kiếm bằng một danh sách các từ điển, mỗi từ điển ứng với một mô hình và các tham số riêng của nó. Với tham số liên tục như C, hãy khai báo một phân phối (ở đây là loguniform của SciPy) thay vì liệt kê vài giá trị; RandomizedSearchCV sẽ lấy mẫu từ phân phối đó, và tăng n_iter là đủ để thử thêm giá trị mới, không cần dựng lại lưới. Đây cũng là hướng tài liệu khuyến nghị cho tham số liên tục.

space = [
    {"clf": [LogisticRegression(max_iter=1000)], "clf__C": loguniform(1e-3, 1e2)},
    {"clf": [RandomForestClassifier(random_state=0, n_jobs=-1)],
     "clf__n_estimators": [100, 200, 300], "clf__max_depth": [3, 5, 8, None]},
]
rs = RandomizedSearchCV(pipe2, space, n_iter=12, cv=cv, scoring="roc_auc", random_state=0)
rs.fit(X_tr, y_tr)
[RandomizedSearchCV] mô hình thắng: LogisticRegression | CV AUC 0.734
  tham số: {'clf': 'LogisticRegression', 'clf__C': 0.030737817853626094}
  AUC tập test: 0.789
  điểm CV tốt nhất theo từng mô hình:
    LogisticRegression       0.734 (6 cấu hình đã thử)
    RandomForestClassifier   0.718 (6 cấu hình đã thử)

Random forest của scikit-learn không thắng: điểm CV tốt nhất của nó là 0,718, thấp hơn 0,734 của logistic regression (mỗi mô hình được thử 6 trong 12 cấu hình lấy mẫu). Điều này hợp lý vì nhãn do ta sinh từ một hàm logistic, nên mô hình tuyến tính khớp đúng cấu trúc thật; trên dữ liệu thực tế bạn sẽ không biết trước điều đó, và đó chính là lý do phải so sánh bằng cross-validation thay vì đoán. Một nguyên tắc rút ra: bắt đầu bằng mô hình đơn giản làm baseline, chỉ chuyển sang mô hình phức tạp khi CV chứng minh có lợi.

Đọc mô hình cuối cùng của Pipeline scikit-learn

Vì tiền xử lý nằm trong Pipeline scikit-learn, ta lấy tên đặc trưng sau biến đổi bằng get_feature_names_out() rồi đọc hệ số, bằng cách ghép với coef_ của bước clf:

[Hệ số logistic regression, đặc trưng đã chuẩn hoá/one-hot]
  so__so_thang_su_dung                     -0.606
  so__so_cuoc_goi_ho_tro                   +0.585
  pl__goi_cuoc_co_ban                      +0.351
  pl__goi_cuoc_tieu_chuan                  -0.283
  so__cuoc_hang_thang                      +0.261
  pl__thanh_toan_tien_mat                  +0.133

Chiều và độ lớn khớp với cách ta sinh dữ liệu: dùng lâu thì ít rời bỏ, gọi hỗ trợ nhiều và gói cơ bản thì dễ rời bỏ hơn. Hệ số học được nằm trên đặc trưng đã chuẩn hoá nên không so trực tiếp được với hệ số trong công thức sinh dữ liệu (nằm trên thang gốc).

Muốn đối chiếu, nhân hệ số thật với độ lệch chuẩn của cột: -0,04 × 16,9 ≈ -0,68 cho số tháng sử dụng, 0,5 × 1,19 ≈ +0,59 cho số cuộc gọi hỗ trợ, 0,000004 × 90.562 ≈ +0,36 cho cước hàng tháng.

Các giá trị học được (-0,606, +0,585, +0,261) cùng chiều và cùng bậc độ lớn; phần chênh, rõ nhất ở cước hàng tháng, đến từ regularization (C=0.1) co hệ số về 0 và từ nhiễu lấy mẫu của 1.200 dòng. Vì biến số đã chuẩn hoá, so sánh độ lớn giữa các biến số với nhau là hợp lệ. Hệ số của one-hot thì so với các nhóm còn lại của cùng cột. Đừng diễn giải hệ số như quan hệ nhân quả: mô hình chỉ nói biến nào đi cùng nhãn.

scikit-learn trong thực tế: những mẫu dùng lại được

  • Một đối tượng để triển khai: với scikit-learn, lưu cả Pipeline (ví dụ bằng joblib.dump) thay vì lưu riêng scaler và mô hình, để lúc dự đoán không thể áp sai thứ tự hay sai tham số. Chỉ nạp file mô hình từ nguồn tin cậy vì pickle có thể chạy mã tuỳ ý khi nạp.
  • Cột theo kiểu dữ liệu: với bảng nhiều cột, dùng make_column_selector(dtype_include="number") thay vì liệt kê tay; tài liệu ColumnTransformer mô tả cách chọn cột theo dtype hay mẫu tên.
  • Bỏ qua một bước khi tìm kiếm: đặt bước tiền xử lý thành "passthrough" trong lưới để kiểm tra xem bước đó có thật sự giúp ích không (ví dụ có hoặc không chuẩn hoá).
  • Phân tầng và nhóm: dữ liệu theo thời gian dùng TimeSeriesSplit, dữ liệu có nhiều dòng cùng một khách hàng dùng GroupKFold; cả hai được tài liệu cross-validation đề cập. Xáo trộn ngẫu nhiên ở những trường hợp này gây rò rỉ.
  • Nhiều metric: trong scikit-learn dùng scoring dạng danh sách cùng refit="roc_auc" khi cần theo dõi nhiều chỉ số; bài sau về đánh giá mô hình đi sâu vào chọn metric đúng cho dữ liệu lệch.

Khi nào không nên dùng Pipeline scikit-learn hay tuning rộng

  • Tuning quá nhiều trên dữ liệu nhỏ: lưới tìm kiếm càng lớn, điểm cao nhất càng có khả năng là may mắn. Với 1.200 mẫu và chênh lệch 0,0004 như trên, tuning chỉ tốn thời gian; nếu cần ước lượng hiệu năng trung thực sau khi tuning, dùng nested cross-validation (xem ví dụ Nested versus non-nested cross-validation).
  • Tiền xử lý nặng, không phải của scikit-learn: biến đổi chuỗi, ảnh, hay đặc trưng từ API ngoài thường tách khỏi Pipeline (dựng sẵn và lưu), miễn là không dùng thống kê của tập kiểm tra.
  • Dữ liệu lớn không vừa bộ nhớ: ngoài scikit-learn thuần, cân nhắc estimator có partial_fit (như SGDClassifier) hoặc công cụ khác, vì Pipeline cùng cross-validation nhân bản dữ liệu qua các fold.
  • Mục tiêu chỉ là khám phá: với scikit-learn, nếu chỉ muốn nhìn dữ liệu nhanh, Pipeline đầy đủ là thừa; nhưng ngay khi con số nào đó đi vào báo cáo, hãy chuyển sang Pipeline.

Lỗi thường gặp

  1. Fit scaler hoặc imputer trên toàn bộ dữ liệu trước khi chia: đưa vào Pipeline.
  2. Chọn đặc trưng trước cross-validation: thí nghiệm ở trên cho thấy AUC 0,867 trên nhiễu thuần tuý.
  3. Dùng tập kiểm tra để chọn siêu tham số: hãy dùng cv_results_ và chỉ chạm tập kiểm tra một lần.
  4. Kết luận từ chênh lệch nhỏ hơn độ lệch chuẩn giữa các fold: báo cáo cả trung bình lẫn độ lệch chuẩn.
  5. Quên stratify khi nhãn lệch, hoặc quên shuffle khi dữ liệu sắp xếp theo nhãn; mặc định không xáo trộn.
  6. Tên tham số sai trong lưới: clf__C chứ không phải C; dùng pipe.get_params().keys() để tra.
  7. Không ghim phiên bản thư viện: tham số mặc định và giá trị cảnh báo thay đổi giữa các bản; ghi requirements.txt (có sẵn trong code/s7-11) để chạy lại được kết quả.
  8. Quên handle_unknown="ignore" ở OneHotEncoder nên lỗi khi gặp loại mới ở sản phẩm.

Checklist

  • Tách tập kiểm tra có stratify trước mọi thao tác học.
  • Đặt toàn bộ bước có fit vào một scikit-learn Pipeline kèm ColumnTransformer.
  • Đánh giá bằng cross-validation phân tầng của scikit-learn, báo cáo trung bình ± độ lệch chuẩn và điểm huấn luyện.
  • Tuning bằng scikit-learn GridSearchCV hoặc RandomizedSearchCV trên Pipeline; so sánh chênh lệch với độ lệch chuẩn.
  • Bắt đầu bằng baseline đơn giản; chỉ tăng độ phức tạp khi CV chứng minh có lợi.
  • Chạm tập kiểm tra một lần; lưu cả Pipeline và ghim phiên bản thư viện.

Mã nguồn đầy đủ nằm trong code/s7-11 (pipeline_demo.py, output.txt, requirements.txt); chạy bằng python pipeline_demo.py trong venv. Mọi số liệu là output thật chạy với Python 3.10, scikit-learn 1.1.3, pandas 1.5.1, NumPy 1.23.4 (ghim trong requirements.txt); dữ liệu giả lập, hạt giống cố định nên chạy lại sẽ cho cùng kết quả (khác biệt nhỏ ở chữ số cuối có thể xuất hiện nếu đổi phiên bản BLAS hoặc thư viện). Tiến trình dùng khoảng 150 MB RAM.

Bài tiếp theo

Đánh giá mô hình phân loại: precision, recall, ROC và dữ liệu lệch. Ta vừa dùng ROC AUC, average precision và F1 gần như chỉ để so sánh; bài sau đi sâu vào ý nghĩa từng chỉ số, vì sao accuracy đánh lừa khi dữ liệu lệch và cách chọn ngưỡng quyết định theo chi phí nghiệp vụ.