Bỏ qua để đến nội dung

Đánh giá mô hình: Accuracy, Precision, Recall, F1

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 avg trong classification_report của scikit-learn.

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 pp. Bảng dưới xếp bệnh nhân theo pp giảm dần (dữ liệu tự tạo cho bài học).

Bệnh nhânXác suất ppNhãn thật yy
10.931
20.861
30.780
40.711
50.630
60.581
70.540
80.461
90.420
100.360
110.311
120.270
130.220
140.180
150.140
160.120
170.090
180.070
190.040
200.020

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ó p≥0.5p \ge 0.5 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 yy, 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 14/20=70%14/20 = 70\%. 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%.

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.

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 p(i)p^{(i)} là xác suất mô hình gán cho mẫu thứ ii thuộc lớp dương. Để ra nhãn dự đoán y^(i)\hat{y}^{(i)}, ta so nó với một ngưỡng quyết định tt:

y^(i)={1neˆˊu p(i)≥t0neˆˊu p(i)<t\hat{y}^{(i)} = \begin{cases} 1 & \text{nếu } p^{(i)} \ge t \\ 0 & \text{nếu } p^{(i)} < t \end{cases}

Hàm predict của scikit-learn dùng ngưỡng 0.5 (đúng ra là dự đoán dương khi p>0.5p > 0.5; chỉ khác ở mẫu có pp đú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 pp; còn tt 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 tt, nên đổi tt là đổi chỉ số.

So nhãn dự đoán y^\hat{y} với nhãn thật yy, mỗi mẫu rơi vào đúng một trong bốn ô:

Nhãn thậtDự đoán dương (y^=1\hat{y} = 1)Dự đoán âm (y^=0\hat{y} = 0)
Dương (y=1y = 1)TP: dương tính thậtFN: âm tính giả
Âm (y=0y = 0)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à t=0.5t = 0.5: 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ậtDự đoán dươngDự đoán âm
Dương (6 người)TP = 4FN = 2
Âm (14 người)FP = 3TN = 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 là tỉ lệ mẫu đoán đúng, tức hai ô trên đường chéo chia cho tổng:

Accuracy=TP+TNTP+TN+FP+FN=4+1120=0.75\text{Accuracy} = \frac{\text{TP} + \text{TN}}{\text{TP} + \text{TN} + \text{FP} + \text{FN}} = \frac{4 + 11}{20} = 0.75

Mô hình “luôn đoán không bệnh” có TP = 0, FP = 0, FN = 6, TN = 14, nên accuracy là 14/20=0.7014/20 = 0.70. 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.

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”:

Precision=TPTP+FP=44+3≈0.571\text{Precision} = \frac{\text{TP}}{\text{TP} + \text{FP}} = \frac{4}{4 + 3} \approx 0.571

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”:

Recall=TPTP+FN=44+2≈0.667\text{Recall} = \frac{\text{TP}}{\text{TP} + \text{FN}} = \frac{4}{4 + 2} \approx 0.667

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.

Specificity=TNTN+FP=1111+3≈0.786\text{Specificity} = \frac{\text{TN}}{\text{TN} + \text{FP}} = \frac{11}{11 + 3} \approx 0.786

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 =0/6=0= 0/6 = 0, lộ ngay chỗ accuracy 70% che đi. Precision của nó là 0/00/0: 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ề đó.

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 (P+R)/2(P + R)/2, với PP là precision và RR là recall. Nhưng xem ngưỡng t=0.9t = 0.9: chỉ bệnh nhân 1 được báo có bệnh, nên P=1/1=1.0P = 1/1 = 1.0 và R=1/6≈0.167R = 1/6 \approx 0.167. Trung bình cộng là khoảng 0.5830.583, 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.

1F1=12(1P+1R)⟹F1=2PRP+R\frac{1}{F_1} = \frac{1}{2}\left(\frac{1}{P} + \frac{1}{R}\right) \quad\Longrightarrow\quad F_1 = \frac{2PR}{P + R}

Vế phải có được bằng cách quy đồng 1P+1R=P+RPR\frac{1}{P} + \frac{1}{R} = \frac{P + R}{PR}, 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 R≈0.167R \approx 0.167, 1/R=61/R = 6 lấn át 1/P=11/P = 1, nên trung bình của các nghịch đảo lớn và F1F_1 nhỏ. Ở t=0.9t = 0.9:

F1=2⋅1.0⋅0.1671.0+0.167≈0.286F_1 = \frac{2 \cdot 1.0 \cdot 0.167}{1.0 + 0.167} \approx 0.286

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 PP và RR 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:

1F1=12(TP+FPTP+TP+FNTP)=2 TP+FP+FN2 TP\begin{aligned} \frac{1}{F_1} &= \frac{1}{2}\left(\frac{\text{TP} + \text{FP}}{\text{TP}} + \frac{\text{TP} + \text{FN}}{\text{TP}}\right) \\ &= \frac{2\,\text{TP} + \text{FP} + \text{FN}}{2\,\text{TP}} \end{aligned}

Dòng đầu thay 1/P1/P và 1/R1/R 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 TP\text{TP}. Nghịch đảo lại:

F1=2 TP2 TP+FP+FNF_1 = \frac{2\,\text{TP}}{2\,\text{TP} + \text{FP} + \text{FN}}

Ở t=0.5t = 0.5: F1=8/(8+3+2)=8/13≈0.615F_1 = 8/(8 + 3 + 2) = 8/13 \approx 0.615. 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ì F1=0F_1 = 0, trong khi precision là 0/00/0.

F-beta. Khi một loại lỗi đắt hơn loại kia, có thể dùng FβF_\beta:

Fβ=(1+β2) PRβ2P+RF_\beta = \frac{(1 + \beta^2)\,P R}{\beta^2 P + R}

β=1\beta = 1 cho lại F1F_1. β>1\beta > 1 coi recall quan trọng hơn (ví dụ F2F_2 cho sàng lọc bệnh), β<1\beta < 1 coi precision quan trọng hơn (ví dụ F0.5F_{0.5}). Trong scikit-learn là fbeta_score.

Giờ đổi ngưỡng tt 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 ttTPFPFNTNAccuracyPrecisionRecallF1
0.3065090.7500.5451.0000.706
0.50432110.7500.5710.6670.615
0.70313130.8000.7500.5000.600
0.80204140.8001.0000.3330.500
0.95006140.700không xác định0.0000.000

Nâng tt 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ố TP+FN\text{TP} + \text{FN} 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ừ t=0.30t = 0.30 lên 0.350.35, 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ừ 0.5450.545 xuống 0.5000.500.
  • Accuracy có thể tăng trong khi mô hình tệ đi với người bệnh. Ở t=0.80t = 0.80, accuracy là 0.8000.800, cao hơn ở t=0.5t = 0.5, mà recall chỉ còn 0.3330.333: 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.

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 6/20=0.36/20 = 0.3.

Đườ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:

FPR=FPFP+TN=1−Specificity\text{FPR} = \frac{\text{FP}}{\text{FP} + \text{TN}} = 1 - \text{Specificity}

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 (0,0)(0, 0) (không báo ai) tới góc (1,1)(1, 1) (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 pp cao hơn. Với 20 bệnh nhân có 6×14=846 \times 14 = 84 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=84−1184≈0.869\text{AUC} = \frac{84 - 11}{84} \approx 0.869

AUC chỉ phụ thuộc thứ tự của các pp, 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 KK 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 KK con số theo một trong ba cách:

  • Macro: trung bình cộng của KK 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 support củ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.

  • 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 1/6≈0.1671/6 \approx 0.167. 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; FβF_\beta 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 pp 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 p=0.9p = 0.9, 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.

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 tt 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ử:

  1. Lúc đầu t=0.50t = 0.50: 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.
  2. Kéo xuống t=0.30t = 0.30: 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.
  3. Kéo lên t=0.80t = 0.80: 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.
  4. Kéo lên t=0.95t = 0.95: không còn ai bị báo có bệnh. Precision hiện ”—” vì là 0/00/0, 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”.
Kéo ngưỡng t. Mỗi chấm là một bệnh nhân; chấm nằm trên vạch hoặc bên phải vạch được đoán có bệnh. Ma trận nhầm lẫn và bốn chỉ số bên dưới đổi theo.
  • Có bệnh (y = 1)
  • Không bệnh (y = 0)
  • Vạch ngưỡng t
Ma trận nhầm lẫn ở t = 0.50. Hàng là nhãn thật, cột là dự đoán.
Nhãn thậtDự đoán dươngDự đoán âm
Dương (6)TP = 4FN = 2
Âm (14)FP = 3TN = 11
Accuracy15/200.750
Precision4/70.571
Recall4/60.667
F18/130.615

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.

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 t=0.5t = 0.5, đế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 np
from 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 định
y_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òa
print(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, 1
print(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 pp, không phải nhãn đã cắt ngưỡng.

Quét ngưỡng, đường cong PR và ROC

import matplotlib.pyplot as plt
import numpy as np
from 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ên
ax1.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 np
from sklearn.datasets import make_classification
from sklearn.dummy import DummyClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, f1_score, recall_score
from 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 1 nó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 1 tă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 đó.

Câu 1/4 Có 1 000 giao dịch thẻ, trong đó 10 giao dịch gian lận (lớp dương). Một mô hình luôn đoán "hợp lệ". Accuracy và recall của nó là bao nhiêu?
Câu 2/4 Một mô hình có precision 0.90.9 và recall 0.10.1. F1 của nó gần với số nào nhất?
Câu 3/4 Bạn nâng ngưỡng quyết định từ 0.50.5 lên 0.70.7 (dự đoán dương khi p≥tp \ge t). Điều nào luôn đúng?
Câu 4/4 Bạn làm bộ lọc thư rác, lớp dương là "thư rác". Thư bị lọc sẽ vào thư mục rác, người dùng hầu như không mở. Bạn nên ưu tiên chỉ số nào khi chọn ngưỡng?

Ý chính:

  • Accuracy =(TP+TN)/m= (\text{TP}+\text{TN})/m, với mm 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 =TP/(TP+FP)= \text{TP}/(\text{TP}+\text{FP}) phạt báo nhầm, recall =TP/(TP+FN)= \text{TP}/(\text{TP}+\text{FN}) phạt bỏ sót. F1=2PR/(P+R)=2TP/(2TP+FP+FN)F_1 = 2PR/(P+R) = 2\text{TP}/(2\text{TP}+\text{FP}+\text{FN}) là trung bình điều hòa, bị kéo về số nhỏ hơn.
  • Mô hình cho xác suất pp, còn ngưỡng tt là lựa chọn của bạn. Nâng tt 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 tt 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:

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