Đánh giá mô hình: Accuracy, Precision, Recall, F1
Mục tiêu
Phần tiêu đề “Mục tiêu”Sau bài này, bạn sẽ:
- Nhận ra vì sao accuracy (độ chính xác) đánh lừa khi dữ liệu mất cân bằng (imbalanced data), và luôn so nó với một mô hình cơ sở (baseline) “luôn đoán lớp đa số”.
- Đọc được ma trận nhầm lẫn (confusion matrix), rồi tính precision, recall và F1 từ bốn ô TP, FP, FN, TN. Tự dẫn được vì sao F1 dùng trung bình điều hòa (harmonic mean).
- Hiểu đánh đổi precision–recall khi đổi ngưỡng quyết định (decision threshold), biết khi nào ưu tiên precision, khi nào ưu tiên recall, và chọn ngưỡng trên tập kiểm định.
- Đọc được đường cong precision–recall, đường cong ROC cùng AUC, và các dòng
macro avg,weighted avgtrongclassification_reportcủa scikit-learn.
Cần biết trước
Phần tiêu đề “Cần biết trước”- Hồi quy Logistic, thuộc phần Học máy (Đã học)
Trực giác
Phần tiêu đề “Trực giác”Một phòng khám thử một mô hình sàng lọc trên 20 bệnh nhân đã biết chắc kết quả xét nghiệm. Có 6 người có bệnh. Với mỗi người, mô hình đưa ra xác suất có bệnh . Bảng dưới xếp bệnh nhân theo giảm dần (dữ liệu tự tạo cho bài học).
| Bệnh nhân | Xác suất | Nhãn thật |
|---|---|---|
| 1 | 0.93 | 1 |
| 2 | 0.86 | 1 |
| 3 | 0.78 | 0 |
| 4 | 0.71 | 1 |
| 5 | 0.63 | 0 |
| 6 | 0.58 | 1 |
| 7 | 0.54 | 0 |
| 8 | 0.46 | 1 |
| 9 | 0.42 | 0 |
| 10 | 0.36 | 0 |
| 11 | 0.31 | 1 |
| 12 | 0.27 | 0 |
| 13 | 0.22 | 0 |
| 14 | 0.18 | 0 |
| 15 | 0.14 | 0 |
| 16 | 0.12 | 0 |
| 17 | 0.09 | 0 |
| 18 | 0.07 | 0 |
| 19 | 0.04 | 0 |
| 20 | 0.02 | 0 |
Một mô hình không làm gì vẫn được 70%
Phần tiêu đề “Một mô hình không làm gì vẫn được 70%”Cách chấm dễ nghĩ nhất là đếm số người đoán đúng. Quy ước: ai có thì đoán “có bệnh”. Bệnh nhân 1 tới 7 được đoán có bệnh, 13 người còn lại đoán không bệnh. Đối chiếu với cột , mô hình đúng 15 trên 20 người, tức 75%.
Nghe khá ổn. Giờ thử một “mô hình” chẳng cần học gì: luôn đoán không bệnh. Nó đúng với cả 14 người không bệnh, nên được . Mô hình thật chỉ hơn nó 5 điểm phần trăm.
Tệ hơn, mô hình “luôn đoán không bệnh” không phát hiện được người bệnh nào. Nó vô dụng cho việc sàng lọc, mà vẫn được 70%. Nếu bệnh hiếm hơn, chẳng hạn 1 trên 100 người, nó được tới 99%.
Không phải lỗi nào cũng như nhau
Phần tiêu đề “Không phải lỗi nào cũng như nhau”Con số 75% còn giấu một chuyện nữa: mô hình sai kiểu gì. Năm người bị đoán sai gồm hai loại:
- Bệnh nhân 3, 5, 7 không có bệnh mà bị báo có bệnh. Họ phải đi xét nghiệm thêm, tốn tiền và lo lắng.
- Bệnh nhân 8 và 11 có bệnh mà bị báo không. Họ về nhà, bệnh không được chữa sớm.
Với sàng lọc bệnh, lỗi thứ hai đắt hơn nhiều. Accuracy đếm hai loại lỗi như nhau.
Thêm nữa, con số 0.5 là do ta chọn. Hạ nó xuống 0.3 thì bệnh nhân 8 và 11 được bắt, nhưng thêm bệnh nhân 9, 10 bị báo nhầm. Mỗi ngưỡng cho một bộ lỗi khác.
Bài toán của bài này là: đo chất lượng một mô hình phân loại sao cho thấy được nó mắc loại lỗi nào, kể cả khi một lớp hiếm hơn hẳn lớp kia, và chọn ngưỡng hợp với chi phí của từng loại lỗi.
Lý thuyết
Phần tiêu đề “Lý thuyết”Từ xác suất tới nhãn: ngưỡng quyết định
Phần tiêu đề “Từ xác suất tới nhãn: ngưỡng quyết định”Gọi là xác suất mô hình gán cho mẫu thứ thuộc lớp dương. Để ra nhãn dự đoán , ta so nó với một ngưỡng quyết định :
Hàm predict của scikit-learn dùng ngưỡng 0.5 (đúng ra là dự đoán dương khi ; chỉ khác ở mẫu có đúng bằng 0.5), nhưng không có gì bắt buộc phải dùng 0.5. Mô hình học ra ; còn là quyết định của bạn, tùy chi phí của từng loại lỗi. Mọi chỉ số trong các mục dưới (trừ AUC) đều tính sau khi đã chọn , nên đổi là đổi chỉ số.
Ma trận nhầm lẫn
Phần tiêu đề “Ma trận nhầm lẫn”So nhãn dự đoán với nhãn thật , mỗi mẫu rơi vào đúng một trong bốn ô:
| Nhãn thật | Dự đoán dương () | Dự đoán âm () |
|---|---|---|
| Dương () | TP: dương tính thật | FN: âm tính giả |
| Âm () | FP: dương tính giả | TN: âm tính thật |
- TP (true positive, dương tính thật): có bệnh, đoán có bệnh.
- FN (false negative, âm tính giả): có bệnh, đoán không. Đây là ca bỏ sót.
- FP (false positive, dương tính giả): không bệnh, đoán có bệnh. Đây là ca báo nhầm.
- TN (true negative, âm tính thật): không bệnh, đoán không.
Chữ thứ hai (P hoặc N) là điều mô hình đoán. Chữ thứ nhất (T hoặc F) cho biết lời đoán đó đúng hay sai. Bảng này gọi là ma trận nhầm lẫn (confusion matrix).
Với 20 bệnh nhân và : TP gồm bệnh nhân 1, 2, 4, 6; FP gồm 3, 5, 7; FN gồm 8 và 11; mười một người còn lại là TN.
| Nhãn thật | Dự đoán dương | Dự đoán âm |
|---|---|---|
| Dương (6 người) | TP = 4 | FN = 2 |
| Âm (14 người) | FP = 3 | TN = 11 |
Tổng theo hàng là số mẫu thật của mỗi lớp, không đổi dù ngưỡng ra sao. Tổng theo cột là số dự đoán của mỗi loại, đổi theo ngưỡng.
Accuracy
Phần tiêu đề “Accuracy”Accuracy là tỉ lệ mẫu đoán đúng, tức hai ô trên đường chéo chia cho tổng:
Mô hình “luôn đoán không bệnh” có TP = 0, FP = 0, FN = 6, TN = 14, nên accuracy là . Phần lớn điểm của accuracy đến từ ô TN, ô của lớp đông người. Lớp càng lệch, ô TN càng lấn át, và accuracy càng ít nói về lớp dương.
Precision và recall
Phần tiêu đề “Precision và recall”Hai chỉ số sau chỉ nhìn vào lớp dương, mỗi chỉ số trả lời một câu hỏi.
Precision: trong những người mô hình báo có bệnh, bao nhiêu phần thật sự có bệnh? Ta chia TP cho cả cột “dự đoán dương”:
Recall (còn gọi là độ nhạy, sensitivity, hay tỉ lệ dương tính thật, true positive rate): trong những người thật sự có bệnh, mô hình bắt được bao nhiêu phần? Ta chia TP cho cả hàng “dương thật”:
Hai công thức có cùng tử số TP, chỉ khác mẫu số. Precision bị kéo xuống bởi báo nhầm (FP). Recall bị kéo xuống bởi bỏ sót (FN).
Với lớp âm cũng có một chỉ số giống recall, gọi là độ đặc hiệu (specificity): trong những người không bệnh, bao nhiêu phần được đoán đúng là không bệnh.
Trong y khoa, xét nghiệm thường được mô tả bằng cặp độ nhạy và độ đặc hiệu. Trong học máy, cặp precision và recall hay gặp hơn.
Với mô hình “luôn đoán không bệnh”: recall , lộ ngay chỗ accuracy 70% che đi. Precision của nó là : không có dự đoán dương nào nên không định nghĩa được. scikit-learn khi đó in cảnh báo và trả về 0; tham số zero_division đổi giá trị trả về đó.
F1: gộp precision và recall thành một số
Phần tiêu đề “F1: gộp precision và recall thành một số”Một con số thì dễ so mô hình hơn hai. Cách dễ nghĩ nhất là lấy trung bình cộng , với là precision và là recall. Nhưng xem ngưỡng : chỉ bệnh nhân 1 được báo có bệnh, nên và . Trung bình cộng là khoảng , cao hơn cả ở ngưỡng 0.5, dù mô hình bỏ sót 5 trên 6 người bệnh.
F1 dùng trung bình điều hòa (harmonic mean): lấy trung bình của các nghịch đảo, rồi nghịch đảo lại.
Vế phải có được bằng cách quy đồng , rồi nghịch đảo cả hai vế.
Vì sao nghịch đảo lại giúp? Số nhỏ có nghịch đảo lớn. Với , lấn át , nên trung bình của các nghịch đảo lớn và nhỏ. Ở :
F1 bị kéo về phía số nhỏ hơn. Muốn F1 cao, cả precision lẫn recall đều phải cao.
Thay và bằng công thức theo TP, FP, FN, ta còn được một dạng chỉ dùng ba ô của ma trận:
Dòng đầu thay và bằng nghịch đảo của hai phân số ở mục trước. Dòng thứ hai cộng hai phân số cùng mẫu . Nghịch đảo lại:
Ở : . Dạng này không có ô TN, nên một biển âm tính thật không làm F1 đẹp lên như với accuracy. Nó còn tính được cả khi không có dự đoán dương nào: TP = 0 thì , trong khi precision là .
F-beta. Khi một loại lỗi đắt hơn loại kia, có thể dùng :
cho lại . coi recall quan trọng hơn (ví dụ cho sàng lọc bệnh), coi precision quan trọng hơn (ví dụ ). Trong scikit-learn là fbeta_score.
Đánh đổi precision–recall theo ngưỡng
Phần tiêu đề “Đánh đổi precision–recall theo ngưỡng”Giờ đổi ngưỡng trên 20 bệnh nhân. Ô code thứ hai in đủ các ngưỡng từ 0.1 tới 0.9; dưới đây là vài ngưỡng tiêu biểu.
| Ngưỡng | TP | FP | FN | TN | Accuracy | Precision | Recall | F1 |
|---|---|---|---|---|---|---|---|---|
| 0.30 | 6 | 5 | 0 | 9 | 0.750 | 0.545 | 1.000 | 0.706 |
| 0.50 | 4 | 3 | 2 | 11 | 0.750 | 0.571 | 0.667 | 0.615 |
| 0.70 | 3 | 1 | 3 | 13 | 0.800 | 0.750 | 0.500 | 0.600 |
| 0.80 | 2 | 0 | 4 | 14 | 0.800 | 1.000 | 0.333 | 0.500 |
| 0.95 | 0 | 0 | 6 | 14 | 0.700 | không xác định | 0.000 | 0.000 |
Nâng thì có ít người bị báo có bệnh hơn:
- Recall không bao giờ tăng. Mẫu chỉ có thể rời nhóm dự đoán dương, nên TP chỉ giữ nguyên hoặc giảm, trong khi mẫu số luôn là 6.
- Precision thường tăng, nhưng không chắc. FP giảm, nhưng TP cũng có thể giảm. Từ lên , người rời nhóm là bệnh nhân 11, một người có bệnh, nên precision giảm từ xuống .
- Accuracy có thể tăng trong khi mô hình tệ đi với người bệnh. Ở , accuracy là , cao hơn ở , mà recall chỉ còn : bỏ sót 4 trên 6 người bệnh.
Không có ngưỡng nào tốt nhất cho mọi bài toán. Câu hỏi đúng là: loại lỗi nào đắt hơn?
- Ưu tiên recall khi bỏ sót đắt. Sàng lọc bệnh: báo nhầm chỉ tốn một lần xét nghiệm thêm, còn bỏ sót có thể làm lỡ việc chữa sớm. Hạ ngưỡng. Phát hiện gian lận thẻ cũng thường theo hướng này.
- Ưu tiên precision khi báo nhầm đắt. Lọc thư rác: để lọt một thư rác chỉ hơi phiền, còn đẩy nhầm thư công việc vào thư mục rác thì người dùng có thể mất thư quan trọng. Nâng ngưỡng.
Chọn ngưỡng cũng là chọn một siêu tham số (hyperparameter). Vì vậy hãy chọn nó trên tập kiểm định, hoặc bằng kiểm định chéo (cross-validation) như bài Overfitting và Underfitting, rồi chấm tập test một lần với ngưỡng đã chốt. Chọn ngưỡng cho F1 đẹp nhất trên tập test là lỗi “chọn mô hình bằng tập test” của bài Tổng quan về Học máy. Ô code thứ ba làm đúng quy trình này. scikit-learn còn có sẵn TunedThresholdClassifierCV, tự chọn ngưỡng bằng kiểm định chéo.
Đường cong precision–recall và ROC
Phần tiêu đề “Đường cong precision–recall và ROC”Bảng ở trên mới là vài ngưỡng. Quét mọi ngưỡng và nối các điểm lại, bạn được một đường cong. Có hai cách vẽ phổ biến.
Đường cong precision–recall (precision–recall curve) đặt recall ở trục ngang, precision ở trục dọc. Mỗi điểm là một ngưỡng. Mô hình tốt có đường đi sát góc trên bên phải: recall cao mà precision vẫn cao. Mức của mô hình đoán bừa là một đường ngang ở độ cao bằng tỉ lệ lớp dương, ở đây .
Đường cong ROC (receiver operating characteristic) đặt ở trục ngang tỉ lệ dương tính giả (false positive rate, FPR), tức tỉ lệ người không bệnh bị báo nhầm:
và ở trục dọc là recall (tỉ lệ dương tính thật). Hạ ngưỡng từ 1 xuống 0, đường đi từ góc (không báo ai) tới góc (báo tất cả). Đoán bừa cho đường chéo.
AUC (area under the curve) là diện tích dưới đường cong ROC, từ 0 tới 1. Nó có một nghĩa dễ nhớ: chọn ngẫu nhiên một người bệnh và một người không bệnh, AUC là xác suất mô hình cho người bệnh cao hơn. Với 20 bệnh nhân có cặp như vậy. Đếm từ bảng: bệnh nhân 4 xếp dưới 1 người không bệnh (bệnh nhân 3), bệnh nhân 6 dưới 2 người, bệnh nhân 8 dưới 3, bệnh nhân 11 dưới 5. Tổng cộng 11 cặp xếp sai, nên
AUC chỉ phụ thuộc thứ tự của các , không phụ thuộc ngưỡng. Nó hợp để so hai mô hình trước khi chọn ngưỡng. Khi lớp dương rất hiếm, ô TN khổng lồ làm FPR luôn nhỏ, nên đường ROC trông đẹp hơn thực tế. Khi đó đường precision–recall cho thấy rõ hơn mô hình làm tốt tới đâu với lớp dương.
Nhiều lớp: trung bình macro, micro và weighted
Phần tiêu đề “Nhiều lớp: trung bình macro, micro và weighted”Với lớp (ví dụ phân loại chữ số 0 tới 9, bài Hồi quy Softmax), ta tính precision, recall, F1 cho từng lớp bằng cách coi lớp đó là dương, mọi lớp khác là âm. Rồi gộp con số theo một trong ba cách:
- Macro: trung bình cộng của con số, lớp nào cũng nặng như nhau. Một lớp hiếm làm tệ sẽ kéo điểm macro xuống rõ.
- Weighted (có trọng số): trung bình có trọng số theo số mẫu thật của mỗi lớp (cột
supportcủa scikit-learn). Lớp đông chiếm phần lớn điểm. - Micro: cộng dồn TP, FP, FN của mọi lớp rồi mới tính một lần. Khi mỗi mẫu có đúng một nhãn, precision, recall và F1 kiểu micro đều bằng accuracy.
Muốn biết mô hình có bỏ rơi lớp hiếm không, hãy nhìn macro. Ô code thứ ba cho thấy hai cách gộp cách nhau xa đến đâu.
Giới hạn
Phần tiêu đề “Giới hạn”- Dữ liệu ít thì chỉ số nhảy mạnh. Với 6 người bệnh, mỗi ca bắt thêm hay bỏ sót làm recall đổi . Hãy coi các chỉ số trên 20 bệnh nhân là để minh họa.
- Chỉ số không biết chi phí thật. F1 coi FP và FN nặng như nhau; chỉ chỉnh tỉ lệ đó một cách thô. Nếu biết chi phí từng loại lỗi bằng tiền, hãy chọn ngưỡng làm tổng chi phí nhỏ nhất.
- Chỉ số theo ngưỡng không cho biết có đáng tin không. Nếu xác suất được hiệu chỉnh (calibrated) tốt, thì trong 100 ca mô hình báo , khoảng 90 ca là dương thật. Bài này không đi vào chuyện đó.
- Đo được mất cân bằng chưa phải là sửa được nó. Các cách xử lý dữ liệu mất cân bằng (lấy mẫu lại, đặt trọng số lớp) nằm ở bài Xử lý dữ liệu.
Minh họa
Phần tiêu đề “Minh họa”Hình dưới đặt 20 bệnh nhân lên một trục xác suất từ 0 tới 1, mỗi người một chấm, tô màu theo nhãn thật (có bệnh hoặc không). Thanh trượt đặt ngưỡng từ 0 tới 1, bước 0.05; một vạch dọc trên trục cho thấy ngưỡng, mọi chấm ở bên phải vạch (kể cả chấm nằm đúng trên vạch) được đoán có bệnh. Bên cạnh là ma trận nhầm lẫn 2×2 và bốn số accuracy, precision, recall, F1, đổi ngay khi bạn kéo. Hãy thử:
- Lúc đầu : TP = 4, FP = 3, FN = 2, TN = 11. Accuracy 0.750, precision 0.571, recall 0.667, F1 0.615. Hai chấm “có bệnh” nằm bên trái vạch: đó là hai ca bỏ sót.
- Kéo xuống : cả 6 chấm có bệnh đều sang bên phải vạch, recall 1.000. Đổi lại FP tăng lên 5 và precision giảm còn 0.545. F1 lên 0.706.
- Kéo lên : không còn ca báo nhầm nào, FP = 0, precision 1.000. Nhưng recall chỉ còn 0.333, và accuracy lại tăng lên 0.800: accuracy thưởng cho việc bỏ sót người bệnh, vì lớp không bệnh đông hơn.
- Kéo lên : không còn ai bị báo có bệnh. Precision hiện ”—” vì là , recall và F1 bằng 0, còn accuracy là 0.700, đúng bằng mô hình “luôn đoán không bệnh”.
- Có bệnh (y = 1)
- Không bệnh (y = 0)
- Vạch ngưỡng t
| Nhãn thật | Dự đoán dương | Dự đoán âm |
|---|---|---|
| Dương (6) | TP = 4 | FN = 2 |
| Âm (14) | FP = 3 | TN = 11 |
Không có dự đoán dương nào: precision là 0/0, không xác định.
20 bệnh nhân tự tạo, 6 người có bệnh và 14 người không bệnh, đặt theo xác suất có bệnh mà mô hình dự đoán. Người có bệnh phần lớn có xác suất cao, nhưng hai nhóm xen nhau trong khoảng từ 0.3 tới 0.8.
Ví dụ code
Phần tiêu đề “Ví dụ code”Ba ô dưới đây chạy độc lập: ô nào cũng tự import và tự tạo dữ liệu. Hai ô đầu chép đúng bảng 20 bệnh nhân. Ô chạy đầu tiên lâu hơn một chút, vì trình duyệt phải tải thêm scikit-learn.
Ô đầu tiên đặt ngưỡng , đếm bốn ô của ma trận nhầm lẫn bằng NumPy, tính bốn chỉ số bằng tay, rồi đối chiếu với các hàm trong sklearn.metrics. (p >= t).astype(int) biến dãy đúng/sai thành dãy 1/0. (y_pred == 1) & (y_true == 1) đánh dấu những mẫu thỏa cả hai điều kiện, và np.sum đếm chúng.
Ma trận nhầm lẫn ở ngưỡng 0.5
import numpy as npfrom sklearn.metrics import (accuracy_score, confusion_matrix, f1_score, precision_score, recall_score)
# 20 bệnh nhân (dữ liệu minh họa, tự tạo), xếp theo xác suất giảm dần.# y_true: nhãn thật (1 = có bệnh). p: xác suất có bệnh mô hình dự đoán.y_true = np.array([1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0])p = np.array([0.93, 0.86, 0.78, 0.71, 0.63, 0.58, 0.54, 0.46, 0.42, 0.36, 0.31, 0.27, 0.22, 0.18, 0.14, 0.12, 0.09, 0.07, 0.04, 0.02])
t = 0.5 # ngưỡng quyết địnhy_pred = (p >= t).astype(int) # dự đoán dương khi p >= t
# Đếm bốn ô của ma trận nhầm lẫn.tp = np.sum((y_pred == 1) & (y_true == 1))fp = np.sum((y_pred == 1) & (y_true == 0))fn = np.sum((y_pred == 0) & (y_true == 1))tn = np.sum((y_pred == 0) & (y_true == 0))print(f"TP = {tp}, FP = {fp}, FN = {fn}, TN = {tn}")
accuracy = (tp + tn) / len(y_true)precision = tp / (tp + fp)recall = tp / (tp + fn)f1 = 2 * precision * recall / (precision + recall) # trung bình điều hòaprint(f"by hand: acc = {accuracy:.3f}, P = {precision:.3f}, " f"R = {recall:.3f}, F1 = {f1:.3f}")
# Đối chiếu với scikit-learn.print(confusion_matrix(y_true, y_pred)) # hàng: nhãn thật 0, 1print(f"sklearn: acc = {accuracy_score(y_true, y_pred):.3f}, " f"P = {precision_score(y_true, y_pred):.3f}, " f"R = {recall_score(y_true, y_pred):.3f}, " f"F1 = {f1_score(y_true, y_pred):.3f}")
# Mô hình "luôn đoán không bệnh": accuracy vẫn cao.print(f"always 0: acc = {np.mean(y_true == 0):.3f}")Lần chạy đầu cần tải Python, khoảng 10 MB.
Dòng đầu cho đúng ma trận ở phần Lý thuyết: TP = 4, FP = 3, FN = 2, TN = 11. Bốn chỉ số tính tay và tính bằng scikit-learn trùng nhau: accuracy 0.750, precision 0.571, recall 0.667, F1 0.615. Dòng cuối là mô hình “luôn đoán không bệnh”, accuracy 0.700.
Để ý cách confusion_matrix của scikit-learn xếp ô: hàng là nhãn thật, cột là nhãn dự đoán, và lớp 0 đứng trước. Vì vậy ô trên trái 11 là TN, ô dưới phải 4 là TP. Bảng trong bài đặt lớp dương lên trước, nên hai bảng ngược thứ tự nhau.
Ô thứ hai quét ngưỡng từ 0.1 tới 0.9, in precision, recall, F1 ở mỗi ngưỡng, rồi vẽ hai đường cong. precision_recall_curve và roc_curve tự thử mọi ngưỡng cần thiết, nên chỉ cần đưa vào xác suất , không phải nhãn đã cắt ngưỡng.
Quét ngưỡng, đường cong PR và ROC
import matplotlib.pyplot as pltimport numpy as npfrom sklearn.metrics import (f1_score, precision_recall_curve, precision_score, recall_score, roc_auc_score, roc_curve)
# 20 bệnh nhân (dữ liệu minh họa, tự tạo), xếp theo xác suất giảm dần.y_true = np.array([1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0])p = np.array([0.93, 0.86, 0.78, 0.71, 0.63, 0.58, 0.54, 0.46, 0.42, 0.36, 0.31, 0.27, 0.22, 0.18, 0.14, 0.12, 0.09, 0.07, 0.04, 0.02])
# Quét ngưỡng từ 0.1 tới 0.9, bước 0.1.print(" t P R F1")for t in np.round(np.arange(0.1, 0.95, 0.1), 2): y_pred = (p >= t).astype(int) P = precision_score(y_true, y_pred, zero_division=0) # 0/0 thì ghi 0 R = recall_score(y_true, y_pred) F = f1_score(y_true, y_pred) print(f"{t:.2f} {P:.3f} {R:.3f} {F:.3f}")
# Hai đường cong: mỗi điểm trên đường ứng với một ngưỡng.prec, rec, _ = precision_recall_curve(y_true, p)fpr, tpr, _ = roc_curve(y_true, p)auc = roc_auc_score(y_true, p)print(f"ROC AUC = {auc:.3f}")
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 4))ax1.plot(rec, prec, marker="o")ax1.axhline(y_true.mean(), ls="--", c="gray") # mức đoán ngẫu nhiênax1.set(xlabel="recall", ylabel="precision", title="Precision-recall")ax2.plot(fpr, tpr, marker="o", label=f"model (AUC = {auc:.2f})")ax2.plot([0, 1], [0, 1], ls="--", c="gray", label="random")ax2.set(xlabel="false positive rate", ylabel="recall (TPR)", title="ROC")ax2.legend()plt.tight_layout()plt.show()Lần chạy đầu cần tải Python, khoảng 10 MB.
Đọc bảng theo từng cột:
- Recall đứng ở 1.000 tới ngưỡng 0.3, rồi giảm dần còn 0.167 ở ngưỡng 0.9. Nó không bao giờ tăng khi ngưỡng tăng.
- Precision đi từ 0.375 lên 1.000, nhưng không đều: có những đoạn gần như đứng yên (0.545, 0.556, 0.571 ở ba ngưỡng 0.3, 0.4, 0.5).
- F1 cao nhất ở ngưỡng 0.3, khoảng 0.706, nghĩa là trên bộ dữ liệu này, hạ ngưỡng xuống dưới 0.5 có lợi.
ROC AUC = 0.869 khớp phép đếm 73 trên 84 cặp ở phần Lý thuyết. Trên hình bên trái, đường precision–recall có dạng răng cưa: mỗi lần ngưỡng đi qua một người không bệnh, precision tụt xuống; đi qua một người bệnh, recall nhích sang phải và precision nhích lên. Với dữ liệu lớn, các răng cưa nhỏ lại và đường mượt hơn.
Ô thứ ba làm với dữ liệu mất cân bằng thật hơn: 2 000 mẫu do make_classification sinh ra, chỉ khoảng 5% thuộc lớp dương. Dữ liệu được chia thành ba tập train, kiểm định và test (60%, 20%, 20%); stratify giữ tỉ lệ lớp ở mỗi tập. Ô so ba thứ: DummyClassifier luôn đoán lớp đa số, hồi quy logistic với ngưỡng mặc định, và hồi quy logistic với ngưỡng chọn trên tập kiểm định.
Dữ liệu mất cân bằng và chọn ngưỡng
import numpy as npfrom sklearn.datasets import make_classificationfrom sklearn.dummy import DummyClassifierfrom sklearn.linear_model import LogisticRegressionfrom sklearn.metrics import classification_report, f1_score, recall_scorefrom sklearn.model_selection import train_test_split
# 2000 mẫu, chỉ khoảng 5% thuộc lớp dương (lớp 1).X, y = make_classification(n_samples=2000, n_features=10, n_informative=4, weights=[0.95, 0.05], class_sep=2.0, random_state=0)# Chia ba tập: train 60%, kiểm định 20%, test 20%, giữ tỉ lệ lớp.X_tmp, X_test, y_tmp, y_test = train_test_split( X, y, test_size=0.2, stratify=y, random_state=0)X_tr, X_val, y_tr, y_val = train_test_split( X_tmp, y_tmp, test_size=0.25, stratify=y_tmp, random_state=0)
# Mô hình "ngốc": luôn đoán lớp đa số (lớp 0).dummy = DummyClassifier(strategy="most_frequent").fit(X_tr, y_tr)d_pred = dummy.predict(X_test)print(f"dummy: acc = {np.mean(d_pred == y_test):.3f}, " f"R = {recall_score(y_test, d_pred):.3f}")
# Hồi quy logistic với ngưỡng mặc định 0.5 (predict).model = LogisticRegression().fit(X_tr, y_tr)print(classification_report(y_test, model.predict(X_test), digits=3))
# Chọn ngưỡng trên tập KIỂM ĐỊNH (không phải tập test).p_val = model.predict_proba(X_val)[:, 1]ts = np.round(np.arange(0.05, 0.95, 0.05), 2)f1s = [f1_score(y_val, (p_val >= t).astype(int)) for t in ts]best_t = ts[int(np.argmax(f1s))]print(f"best threshold on validation: {best_t:.2f}")
# Chấm một lần trên tập test với ngưỡng đã chọn.p_test = model.predict_proba(X_test)[:, 1]y_hat = (p_test >= best_t).astype(int)print(classification_report(y_test, y_hat, digits=3))Lần chạy đầu cần tải Python, khoảng 10 MB.
Tập test có 400 mẫu, 22 mẫu dương (cột support).
- Mô hình “ngốc” có accuracy 0.945 mà recall 0.000: không bắt được mẫu dương nào.
- Hồi quy logistic ở ngưỡng 0.5 có accuracy 0.960, chỉ hơn mô hình ngốc 1.5 điểm phần trăm. Dòng của lớp
1nói rõ hơn nhiều: precision 1.000 nhưng recall 0.273, tức chỉ bắt được 6 trên 22 mẫu dương. Mô hình quá thận trọng. - Ngưỡng chọn trên tập kiểm định là 0.20 (ngưỡng cho F1 cao nhất trên tập kiểm định). Chấm một lần trên tập test: recall lên 0.500 (11 trên 22), precision còn 0.647, F1 của lớp
1tăng từ 0.429 lên 0.564. Accuracy thì gần như đứng yên, thậm chí giảm nhẹ từ 0.960 xuống 0.958. Nhìn accuracy thôi, bạn sẽ không thấy mô hình đã bắt được gần gấp đôi số mẫu dương.
Hai dòng cuối mỗi bảng là hai cách gộp. Ở ngưỡng 0.5, F1 kiểu macro avg là 0.704, còn weighted avg là 0.949. Cách weighted cho lớp 0 (378 mẫu) gần như toàn bộ trọng số, nên che mất F1 0.429 của lớp hiếm. Cách macro cho hai lớp nặng như nhau, nên lộ ra điểm yếu đó.
Lỗi hay gặp
Phần tiêu đề “Lỗi hay gặp”Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Tóm tắt và đọc thêm
Phần tiêu đề “Tóm tắt và đọc thêm”Ý chính:
- Accuracy , với là số mẫu, dễ đánh lừa khi một lớp hiếm: mô hình “luôn đoán lớp đa số” đã đạt tỉ lệ của lớp đó (70% với 20 bệnh nhân, 94.5% ở ô code thứ ba). Luôn so với mô hình cơ sở này.
- Ma trận nhầm lẫn tách lỗi thành báo nhầm (FP) và bỏ sót (FN). Precision phạt báo nhầm, recall phạt bỏ sót. là trung bình điều hòa, bị kéo về số nhỏ hơn.
- Mô hình cho xác suất , còn ngưỡng là lựa chọn của bạn. Nâng thì recall không tăng, precision thường tăng. Ưu tiên recall khi bỏ sót đắt (sàng lọc bệnh), precision khi báo nhầm đắt (lọc thư rác). Chọn trên tập kiểm định, không trên tập test.
- Đường cong precision–recall và ROC quét mọi ngưỡng. AUC là xác suất một mẫu dương được xếp trên một mẫu âm; khi lớp dương rất hiếm, đường precision–recall nói thật hơn. Với nhiều lớp, macro cho mọi lớp nặng như nhau, weighted theo số mẫu, micro bằng accuracy khi mỗi mẫu có một nhãn.
Đọc thêm:
- Vũ Hữu Tiệp (2017). Machine Learning cơ bản, Bài 33: Các phương pháp đánh giá một hệ thống phân lớp. Tiếng Việt, đủ các chủ đề của bài: ma trận nhầm lẫn, ROC và AUC, precision–recall, F1, micro và macro. Bài giữ nguyên tên tiếng Anh precision, recall như bài này.
- Google (không ghi năm). Machine Learning Crash Course: Accuracy, recall, precision, and related metrics và ROC and AUC. Ngắn, có ví dụ khi nào nên ưu tiên chỉ số nào, và nghĩa “xếp hạng” của AUC.
- scikit-learn developers (2007–2026). scikit-learn 1.8 User Guide: Metrics and scoring, mục 3.4.4 (classification metrics). Định nghĩa chính xác của
average='macro','micro','weighted'vàzero_division. Xem thêm mục 3.3 Tuning the decision threshold: vì sao ngưỡng 0.5 hiếm khi tối ưu, vàTunedThresholdClassifierCV. - Fawcett, T. (2006). An introduction to ROC analysis. Pattern Recognition Letters 27(8), 861–874. Bài nhập môn kinh điển về đường cong ROC và AUC. Bài dùng “tp rate”, “fp rate” thay cho recall và FPR.
- Saito, T., Rehmsmeier, M. (2015). The Precision-Recall Plot Is More Informative than the ROC Plot When Evaluating Binary Classifiers on Imbalanced Datasets. PLOS ONE 10(3), e0118432. Vì sao đường precision–recall hợp hơn ROC khi lớp dương hiếm.
Trình duyệt đang chặn lưu dữ liệu. Tiến độ sẽ mất khi bạn tải lại trang.
Nội dung theo giấy phép CC BY-NC-SA 4.0. Mã nguồn theo giấy phép MIT.Giới thiệu