Confusion matrix, precision, recall và F1
Xem thửGiao Hàng Anh Tí muốn cảnh báo sớm: ngay khi khách đặt đơn, nếu mô hình đoán đơn sẽ giao trễ so với giờ hứa (is_late), tổng đài gọi cho khách trước và ưu tiên điều phối shipper. Mỗi cuộc gọi tốn khoảng 8.000 đồng công tổng đài. Một đơn trễ mà không ai báo trước tốn khoảng 50.000 đồng: voucher xin lỗi, thời gian xử lý khiếu nại và nguy cơ mất khách (số liệu ước lượng của nhóm vận hành, mô phỏng).
Minh huấn luyện một mô hình logistic regression và báo cáo: accuracy 91,9% trên 10.000 đơn kiểm thử. Bình, trưởng nhóm vận hành, hỏi lại hai câu: "Trong các đơn mô hình bảo sẽ trễ, bao nhiêu đơn trễ thật? Và trong các đơn trễ thật, mô hình bỏ sót bao nhiêu?"
Trước khi đọc tiếp, hãy dự đoán: con số 91,9% trả lời được câu nào trong hai câu của Bình? Và nếu hạ ngưỡng cảnh báo từ 0,5 xuống 0,3, tổng chi phí sẽ tăng hay giảm?
Sau bài này, bạn có thể
- Dựng confusion matrix cho một mô hình phân loại, gọi đúng tên bốn ô TP, FP, FN, TN và đọc đúng thứ tự trong scikit-learn.
- Tính precision, recall, F1 bằng tay từ bốn ô, và giải thích mỗi con số trả lời câu hỏi nghiệp vụ nào.
- Gắn chi phí cho từng loại sai, so sánh các ngưỡng bằng tổng chi phí thay vì bằng accuracy hay F1.
Cần biết trước: bài Logistic regression và ngưỡng quyết định (Chương 7) và bài Metric hồi quy: MAE, RMSE, MAPE và chi phí của sai số (bài trước).
Mức độ: Cơ bản · Thời gian: khoảng 20 phút · Từ khóa: confusion matrix, precision, recall, F1, ngưỡng, chi phí
Bốn kết cục của một lần dự đoán
Với bài toán hai lớp, ta gọi lớp ta quan tâm là lớp dương (positive), ở đây là "đơn trễ". Mỗi đơn kiểm thử có một nhãn thật (trễ hay không) và một dự đoán (cảnh báo hay không). Hai câu hỏi có/không cho bốn kết cục:
| Thật sự trễ | Thật sự đúng giờ | |
|---|---|---|
| Mô hình cảnh báo | TP (true positive): báo đúng | FP (false positive): báo nhầm |
| Mô hình im lặng | FN (false negative): bỏ sót | TN (true negative): im lặng đúng |
Bảng đếm bốn ô này là confusion matrix (ma trận nhầm lẫn). Cách nhớ tên: chữ thứ hai (P hoặc N) là điều mô hình nói, chữ thứ nhất (T hoặc F) là mô hình nói đúng hay sai. FP là "nói dương, nói sai".
Mô hình tư duy của bài: hai loại sai có giá khác nhau. Ở Giao Hàng Anh Tí, một FP tốn một cuộc gọi 8.000 đồng; một FN tốn 50.000 đồng. Accuracy (tỉ lệ dự đoán đúng) cộng TP và TN rồi chia cho tổng, nên nó coi một FP và một FN là như nhau, và không trả lời được câu nào của Bình.
Precision và recall: hai câu hỏi của Bình
Câu thứ nhất của Bình là precision (độ chính xác của cảnh báo): trong các đơn bị cảnh báo, bao nhiêu đơn trễ thật.
precision = TP / (TP + FP)
Câu thứ hai là recall (độ phủ, còn gọi là sensitivity hay TPR): trong các đơn trễ thật, mô hình bắt được bao nhiêu.
recall = TP / (TP + FN)
Hai công thức có cùng tử số TP nhưng khác mẫu số. Precision nhìn theo hàng "mô hình cảnh báo" của bảng trên; recall nhìn theo cột "thật sự trễ". Precision thấp nghĩa là tổng đài gọi nhiều cuộc thừa; recall thấp nghĩa là nhiều khách bị trễ mà không ai báo.
Khi cần một con số gộp cả hai, người ta dùng F1, là trung bình điều hòa (harmonic mean) của precision và recall:
F1 = 2 × precision × recall / (precision + recall)
Trung bình điều hòa bị kéo về phía số nhỏ hơn: precision 0,9 và recall 0,1 cho F1 chỉ 0,18, trong khi trung bình cộng là 0,5. F1 cao buộc cả hai phải cao. Nhưng F1 coi precision và recall quan trọng như nhau, tức là coi FP và FN cùng giá, điều không đúng ở Giao Hàng Anh Tí.
Ngưỡng quyết định đổi FN lấy FP
Logistic regression cho mỗi đơn một xác suất trễ. Mô hình chỉ thành "cảnh báo" hay "im lặng" khi ta chọn một ngưỡng (threshold): xác suất từ ngưỡng trở lên thì cảnh báo. Hạ ngưỡng làm nhiều đơn bị cảnh báo hơn: một số đơn trễ chuyển từ FN sang TP (recall tăng), đồng thời một số đơn đúng giờ chuyển từ TN sang FP (precision thường giảm). Ngưỡng 0,5 không có gì đặc biệt; nó chỉ là mặc định của predict().
Animation dưới đây đưa 10.000 đơn kiểm thử qua mô hình rồi rải vào bốn ô. Chế độ đầu dùng ngưỡng 0,5; bấm nút biến thể để thử ngưỡng 0,3. Hãy theo dõi ô FN và dòng chi phí.
Đang tải Animation…
Ở ngưỡng 0,5, 489 đơn trễ bị bỏ sót và chúng chiếm 24,45 triệu trong 27,0 triệu đồng chi phí. Hạ ngưỡng xuống 0,3 thêm 298 cuộc gọi thừa nhưng bớt 234 đơn bỏ sót, nên tổng chi phí giảm còn 17,7 triệu. F1 gần như đứng yên (0,771 và 0,785), vì F1 không biết FN đắt hơn FP gấp hơn sáu lần.
Làm cùng mình
Bước 1: dựng confusion matrix và tính tay ba metric.
orders = load_orders()
X, y = make_features(orders), orders["is_late"].to_numpy()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
model = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000)).fit(X_train, y_train)
proba = model.predict_proba(X_test)[:, 1]
pred = proba >= 0.5
# Thứ tự ravel() với nhãn False, True là tn, fp, fn, tp
tn, fp, fn, tp = confusion_matrix(y_test, pred).ravel()
print(confusion_matrix(y_test, pred))
print(f"TP {tp} FP {fp} FN {fn} TN {tn} total {tp + fp + fn + tn}")
precision = tp / (tp + fp)
recall = tp / (tp + fn)
f1 = 2 * precision * recall / (precision + recall)
print(f"by hand precision {precision:.3f} recall {recall:.3f} F1 {f1:.3f}")
print(f"sklearn precision {precision_score(y_test, pred):.3f} recall {recall_score(y_test, pred):.3f}"
f" F1 {f1_score(y_test, pred):.3f}")
print(f"accuracy {(tp + tn) / len(y_test):.3f} late share {y_test.mean():.3f}")
| Dòng code | Máy làm gì | Vì sao |
|---|---|---|
stratify=y | Giữ tỉ lệ đơn trễ như nhau ở tập học và tập kiểm thử | Lớp dương chỉ chiếm 18,6%, chia ngẫu nhiên có thể lệch |
make_pipeline(StandardScaler(), ...) | Chuẩn hóa rồi mới học | Fit chuẩn hóa trên tập học, đúng như Chương 6 |
predict_proba(X_test)[:, 1] | Lấy cột xác suất của lớp True | Cột 0 là xác suất lớp False |
confusion_matrix(y_test, pred).ravel() | Trải ma trận 2 × 2 thành 4 số | Thứ tự là tn, fp, fn, tp, không phải tp đứng trước |
tp / (tp + fp) | Precision tính tay | Đối chiếu với precision_score để chắc hiểu đúng |
Kết quả [Đã chạy] Python 3.12, scikit-learn 1.9, pandas 3.1, NumPy 2.5, CPU x86_64, ngày 04/11/2026:
[[7819 324]
[ 489 1368]]
TP 1368 FP 324 FN 489 TN 7819 total 10000
by hand precision 0.809 recall 0.737 F1 0.771
sklearn precision 0.809 recall 0.737 F1 0.771
accuracy 0.919 late share 0.186
Đọc ma trận scikit-learn: hàng là nhãn thật (hàng trên là False, đúng giờ), cột là dự đoán (cột trái là False, im lặng). Vì vậy ô trên trái là TN 7.819, ô dưới phải là TP 1.368. Đây là quy ước của scikit-learn; nhiều sách vẽ ngược lại, nên luôn kiểm tra trước khi đọc.
Trả lời hai câu của Bình: precision 0,809 nghĩa là cứ 10 cảnh báo có khoảng 8 đơn trễ thật; recall 0,737 nghĩa là mô hình bắt được khoảng 74% đơn trễ, bỏ sót 489 đơn. Accuracy 0,919 cao chủ yếu vì 81,4% số đơn đúng giờ và mô hình im lặng đúng với phần lớn chúng.
Bước 2: so ba ngưỡng bằng tổng chi phí.
COST_FP, COST_FN = 8_000, 50_000
orders = load_orders()
X, y = make_features(orders), orders["is_late"].to_numpy()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
proba = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000)).fit(X_train, y_train).predict_proba(X_test)[:, 1]
print("thr TP FP FN precision recall F1 cost (VND)")
for threshold in [0.5, 0.3, 0.2]:
tn, fp, fn, tp = confusion_matrix(y_test, proba >= threshold).ravel()
p, r = tp / (tp + fp), tp / (tp + fn)
cost = COST_FP * fp + COST_FN * fn
print(f"{threshold:<4} {tp:5} {fp:5} {fn:5} {p:.3f} {r:.3f} {2 * p * r / (p + r):.3f} {cost:,}")
Kết quả [Đã chạy] Python 3.12, scikit-learn 1.9, pandas 3.1, NumPy 2.5, CPU x86_64, ngày 04/11/2026:
thr TP FP FN precision recall F1 cost (VND)
0.5 1368 324 489 0.809 0.737 0.771 27,042,000
0.3 1602 622 255 0.720 0.863 0.785 17,726,000
0.2 1684 893 173 0.653 0.907 0.760 15,794,000
Hạ ngưỡng từ 0,5 xuống 0,2, recall tăng từ 0,737 lên 0,907, precision giảm từ 0,809 xuống 0,653. F1 cao nhất ở 0,3 (0,785), nhưng chi phí thấp nhất ở 0,2 (15,8 triệu đồng). Nếu chọn ngưỡng theo F1, Giao Hàng Anh Tí mất thêm khoảng 1,9 triệu đồng trên mỗi 10.000 đơn. Bài 5 của chương sẽ tính thẳng ngưỡng tối ưu từ bảng chi phí.
Bước 3: một lỗi im lặng, đảo thứ tự tham số. Các hàm metric của scikit-learn nhận (y_true, y_pred) theo đúng thứ tự đó. Đảo lại thì code vẫn chạy.
y_true = np.array([1, 1, 1, 1, 0, 0, 0, 0, 0, 0])
y_pred = np.array([1, 1, 0, 0, 1, 0, 0, 0, 0, 0])
print(f"correct precision {precision_score(y_true, y_pred):.3f} recall {recall_score(y_true, y_pred):.3f}")
# Đảo thứ tự tham số: không có lỗi nào được báo, nhưng hai con số đổi chỗ cho nhau
print(f"swapped precision {precision_score(y_pred, y_true):.3f} recall {recall_score(y_pred, y_true):.3f}")
Kết quả [Đã chạy] Python 3.12, scikit-learn 1.9, pandas 3.1, NumPy 2.5, CPU x86_64, ngày 04/11/2026:
correct precision 0.667 recall 0.500
swapped precision 0.500 recall 0.667
Precision và recall đổi chỗ cho nhau mà không có cảnh báo nào. Lý do: precision là TP chia cho số đơn "được gọi là dương"; khi đảo tham số, "được gọi là dương" trở thành nhãn thật, đúng mẫu số của recall. Cách phòng: luôn gọi bằng tên tham số, precision_score(y_true=..., y_pred=...), và tính tay một lần trên ví dụ nhỏ như trên.
Áp dụng vào Giao Hàng Anh Tí
Báo cáo gửi Bình nên có bốn ô của confusion matrix bằng số đơn thật, precision và recall, rồi tổng chi phí ở từng ngưỡng ứng viên. Không đưa accuracy làm con số chính: tỉ lệ đơn trễ thay đổi theo mùa (bài 4 sẽ cho thấy mùa mưa trễ nhiều hơn), nên accuracy thay đổi ngay cả khi mô hình không đổi.
Bảng chi phí 8.000 và 50.000 đồng là đầu vào của nhóm vận hành, không phải của người làm mô hình. Việc của bạn là biến nó thành quyết định: ngưỡng nào, bao nhiêu cuộc gọi mỗi ngày, bao nhiêu đơn trễ vẫn lọt.
Hiểu lầm thường gặp
Hiểu lầm 1: "Accuracy 91,9% nghĩa là mô hình đúng 91,9% số lần cảnh báo." Sai ở chỗ accuracy tính trên mọi đơn, kể cả 7.819 đơn mô hình im lặng đúng. Tỉ lệ cảnh báo đúng là precision 0,809.
Hiểu lầm 2: "F1 cao nhất là ngưỡng tốt nhất." F1 ngầm cho FP và FN cùng giá. Ở bài này ngưỡng có F1 cao nhất (0,3) tốn hơn ngưỡng 0,2 khoảng 1,9 triệu đồng.
Hiểu lầm 3: "Ô trên trái của confusion_matrix là TP." Với nhãn False và True, scikit-learn xếp nhãn tăng dần nên ô trên trái là TN. ravel() trả về tn, fp, fn, tp.
| Triệu chứng | Nguyên nhân | Cách kiểm chứng | Cách sửa |
|---|---|---|---|
| Precision và recall "đổi chỗ" so với tính tay | Đảo y_true và y_pred | Tính tay trên 10 nhãn biết trước | Gọi hàm bằng tên tham số |
| Recall lớn hơn 1 hoặc TP âm khi tự tính | Gán nhầm thứ tự sau ravel() | In cả ma trận, so với tổng số đơn trễ | Dùng tn, fp, fn, tp = ...ravel() |
precision_score cảnh báo và trả về 0 | Mô hình không cảnh báo đơn nào (TP + FP = 0) | Đếm số dự đoán dương | Hạ ngưỡng, hoặc đặt zero_division có chủ đích |
Tự kiểm tra (có đáp án)
Câu 1 (giải thích). Precision và recall có cùng tử số TP. Vì sao hạ ngưỡng thường làm recall tăng nhưng precision giảm?
Đáp án: hạ ngưỡng làm thêm đơn bị cảnh báo. Mẫu số của recall (TP + FN, tổng đơn trễ thật) không đổi, còn TP tăng, nên recall tăng. Mẫu số của precision (TP + FP) tăng thêm cả các đơn đúng giờ bị báo nhầm, thường nhanh hơn TP, nên precision giảm.
Câu 2 (dự đoán). Một mô hình cảnh báo mọi đơn. Trên 10.000 đơn kiểm thử có 1.857 đơn trễ, precision, recall và chi phí là bao nhiêu?
Đáp án: recall 1,0 vì không bỏ sót đơn nào; precision 0,186 vì chỉ 1.857 trong 10.000 cảnh báo là đúng; chi phí 8.143 × 8.000 = 65,1 triệu đồng, đắt hơn mọi ngưỡng ở Bước 2.
Câu 3 (vận dụng). Mùa cao điểm, tổng đài chỉ gọi được 1.700 cuộc trên mỗi 10.000 đơn. Dựa vào bảng ở Bước 2, bạn chọn ngưỡng nào?
Đáp án: ngưỡng 0,5. Ngưỡng 0,3 cần 1.602 + 622 = 2.224 cuộc gọi, vượt khả năng; ngưỡng 0,5 cần 1.368 + 324 = 1.692 cuộc. Giới hạn năng lực là một ràng buộc, chọn ngưỡng có chi phí thấp nhất trong các ngưỡng thỏa ràng buộc.
Tự tay làm
Mở threshold_tradeoff.py, thêm ngưỡng 0,1 và 0,15 vào danh sách, và thêm một cột in số cuộc gọi (tp + fp). Trước khi chạy, dự đoán: chi phí ở 0,1 thấp hơn hay cao hơn ở 0,2?
Kết quả mong đợi để tự kiểm: ở 0,1, recall lên trên 0,95 nhưng số cuộc gọi vượt 3.000, và chi phí nằm gần mức của ngưỡng 0,2 (chênh lệch dưới 2 triệu đồng). Viết một câu giải thích vì sao chi phí gần như đi ngang quanh vùng ngưỡng này và sẽ tăng trở lại nếu hạ tiếp (gợi ý: mỗi lần hạ, số FN bớt đi ngày càng ít còn số FP thêm vào ngày càng nhiều).
Điều cần nhớ
- Confusion matrix đếm bốn kết cục: TP, FP, FN, TN. Trong scikit-learn,
ravel()trả về theo thứ tự tn, fp, fn, tp. - Precision trả lời "cảnh báo có đáng tin không", recall trả lời "có bỏ sót đơn trễ nào không"; accuracy không trả lời câu nào trong hai câu đó.
- Ngưỡng đổi FN lấy FP. Khi hai loại sai có giá khác nhau, chọn ngưỡng theo tổng chi phí, không theo F1.
- Bài tiếp theo xét trường hợp lớp dương rất hiếm, khi accuracy 98% có thể hoàn toàn vô dụng và cần đến ROC-AUC, PR-AUC.
Nguồn tham khảo
- scikit-learn,
confusion_matrix: https://scikit-learn.org/stable/modules/generated/sklearn.metrics.confusion_matrix.html - scikit-learn,
precision_recall_fscore_support: https://scikit-learn.org/stable/modules/generated/sklearn.metrics.precision_recall_fscore_support.html - scikit-learn, Metrics and scoring (mục Classification metrics): https://scikit-learn.org/stable/modules/model_evaluation.html
- Google Machine Learning Crash Course, Accuracy, precision, and recall: https://developers.google.com/machine-learning/crash-course/classification/accuracy-precision-recall
- AI Engineering from Scratch (MIT), phase 02, Model evaluation: https://github.com/rohitg00/ai-engineering-from-scratch/tree/main/phases/02-ml-fundamentals/09-model-evaluation