Precision, Recall, F1 và ma trận nhầm lẫn cho trạm kiểm tra lỗi
Ma trận nhầm lẫn với NG là lớp dương, precision, recall, F1 bằng tay và scikit-learn, dịch sang escape, overkill, ppm và bẫy accuracy.
Học xong bài này, bạn sẽ
- Tính các chỉ số từ ma trận nhầm lẫn
- Chuyển sang ngôn ngữ nhà máy
- Không bị đánh lừa bởi accuracy
Nên học trước: Tiêu chí OK/NG, escape và overkill
Nội dung bài viết
- 1. Ma trận nhầm lẫn: NG là lớp dương
- 2. Các chỉ số và tên gọi trong nhà máy
- 3. Tính bằng scikit-learn và cái bẫy thứ tự nhãn
- 4. Nghịch lý accuracy khi dữ liệu mất cân bằng
- 5. Precision phụ thuộc tỉ lệ lỗi: phòng thí nghiệm khác dây chuyền
- 6. Từ điểm số đến quyết định: đường cong precision–recall
- 7. Ma trận nhầm lẫn nhiều lớp
- 8. Báo cáo cho nhà máy: số tuyệt đối và độ bất định
- 9. Sai lầm thường gặp
- 10. Bài tập
- Tóm tắt
"Mô hình đạt độ chính xác 99%" nghe rất ấn tượng, cho tới khi bạn biết dây chuyền chỉ có 1% hàng lỗi: một chương trình luôn trả lời "OK" cũng đạt 99%. Bài trước, "Tiêu chí OK/NG, escape và overkill", đã dạy ngôn ngữ của nhà máy: lọt lỗi, loại nhầm, ppm, chi phí mỗi ca. Bài này dạy ngôn ngữ của người làm học máy và thống kê: ma trận nhầm lẫn, precision, recall, F1, specificity, và quan trọng nhất là cách dịch qua lại giữa hai ngôn ngữ đó. Bạn sẽ cần kỹ năng này mỗi khi đọc báo cáo của nhà cung cấp phần mềm AI, so sánh hai phương án, hoặc viết tiêu chí nghiệm thu ở bài sau, "Đặc tả kỹ thuật và nghiệm thu FAT/SAT".
1. Ma trận nhầm lẫn: NG là lớp dương
Trong thống kê, lớp dương (positive) là thứ ta đang đi tìm: bệnh trong xét nghiệm, thư rác trong bộ lọc, và hàng lỗi (NG) trong trạm kiểm tra. "Dương" không có nghĩa là "tốt". Quy ước này quyết định tên gọi của mọi chỉ số, nên hãy viết rõ trong mọi báo cáo: lớp dương = NG.
| Trạm báo NG (dự đoán dương) | Trạm báo OK (dự đoán âm) | |
|---|---|---|
| Thật sự NG | TP (true positive): bắt đúng lỗi | FN (false negative): lọt lỗi, escape |
| Thật sự OK | FP (false positive): loại nhầm, overkill | TN (true negative): cho qua đúng |
Cách nhớ: chữ thứ hai (P/N) là điều trạm nói, chữ thứ nhất (T/F) cho biết trạm nói đúng hay sai. FN = trạm nói "âm" (OK) và nói sai, tức là một sản phẩm lỗi đã đi qua.
2. Các chỉ số và tên gọi trong nhà máy
| Chỉ số | Công thức | Câu hỏi nó trả lời | Tên trong nhà máy |
|---|---|---|---|
| Accuracy | (TP + TN) / tổng | Bao nhiêu phần quyết định là đúng? | "Độ chính xác" (dễ gây hiểu lầm) |
| Recall, sensitivity, TPR | TP / (TP + FN) | Trong các sản phẩm NG thật, bắt được bao nhiêu? | Tỉ lệ phát hiện; escape = 1 − recall |
| Specificity, TNR | TN / (TN + FP) | Trong các sản phẩm OK thật, cho qua đúng bao nhiêu? | overkill = 1 − specificity = FP / (FP + TN) |
| Precision, PPV | TP / (TP + FP) | Trong các sản phẩm bị báo NG, bao nhiêu là NG thật? | 1 − precision = tỉ lệ hàng tốt trong thùng NG (khối lượng kiểm lại) |
| NPV | TN / (TN + FN) | Trong các sản phẩm được cho qua, bao nhiêu là OK thật? | ppm lọt = (1 − NPV) × 10⁶ = FN / (FN + TN) × 10⁶ |
| F1 | 2·P·R / (P + R) = 2TP / (2TP + FP + FN) | Một con số gộp precision và recall | Hầu như không dùng khi báo cáo cho sản xuất |
Escape và overkill ở đây được tính theo đúng định nghĩa của bài "Tiêu chí OK/NG, escape và overkill": escape theo số NG thật, overkill theo số OK thật. Có nhà máy tính overkill theo tổng sản lượng (FP / tổng); hai cách cho con số gần nhau khi tỉ lệ lỗi nhỏ, nhưng phải thống nhất trước khi ký.
Áp dụng cho bài thực hành của trang: một trạm kiểm 10.000 sản phẩm, có 50 lỗi thật, phát hiện được 45, báo nhầm 120 sản phẩm tốt. Bước đầu tiên luôn là điền đủ bốn ô: TP = 45, FN = 50 − 45 = 5, FP = 120, TN = (10.000 − 50) − 120 = 9.830.
def chi_so(TP, FN, FP, TN):
"""Mọi chỉ số từ 4 ô của ma trận nhầm lẫn (lớp dương = NG)."""
tong = TP + FN + FP + TN
return {
"accuracy": (TP + TN) / tong,
"recall (tỉ lệ bắt lỗi)": TP / (TP + FN),
"specificity": TN / (TN + FP),
"precision": TP / (TP + FP),
"F1": 2 * TP / (2 * TP + FP + FN),
"escape = FN/(TP+FN)": FN / (TP + FN),
"overkill = FP/(FP+TN)": FP / (FP + TN),
"overkill theo tổng sản lượng": FP / tong,
"hàng tốt trong thùng NG": FP / (TP + FP),
}
TP, FN, FP = 45, 5, 120
TN = 10_000 - (TP + FN) - FP
print(f"TP = {TP}, FN = {FN}, FP = {FP}, TN = {TN}")
for ten, gia_tri in chi_so(TP, FN, FP, TN).items():
print(f"{ten:30s} {gia_tri:8.2%}")
print(f"ppm lọt theo hàng được giao: {FN / (FN + TN) * 1e6:.0f} ppm ({FN} lỗi trong {FN + TN} sản phẩm giao)")
TP = 45, FN = 5, FP = 120, TN = 9830
accuracy 98.75%
recall (tỉ lệ bắt lỗi) 90.00%
specificity 98.79%
precision 27.27%
F1 41.86%
escape = FN/(TP+FN) 10.00%
overkill = FP/(FP+TN) 1.21%
overkill theo tổng sản lượng 1.20%
hàng tốt trong thùng NG 72.73%
ppm lọt theo hàng được giao: 508 ppm (5 lỗi trong 9835 sản phẩm giao)
Đọc cho quản lý sản xuất, không cần nhắc tới F1:
- Trạm bắt 45/50 lỗi (recall 90%), tức escape 10%: 5 sản phẩm lỗi đã đi tiếp, khoảng 508 ppm trên hàng được giao.
- Trạm loại nhầm 120 sản phẩm tốt, overkill khoảng 1,2%. Trong thùng NG có 165 sản phẩm, 73% là hàng tốt (precision chỉ 27%): người kiểm lại phải xem 165 sản phẩm để tìm 45 lỗi thật.
- Accuracy 98,75% nghe rất tốt nhưng không nói lên điều nào trong hai điều trên.
3. Tính bằng scikit-learn và cái bẫy thứ tự nhãn
Trong dự án thật bạn có hai danh sách: nhãn thật (do QC xác nhận) và kết quả của trạm, mỗi phần tử một sản phẩm. scikit-learn tính mọi thứ từ hai danh sách đó, với điều kiện bạn nói rõ lớp dương:
import numpy as np
from sklearn.metrics import (accuracy_score, classification_report, confusion_matrix,
f1_score, precision_score, recall_score)
# Dựng lại 10.000 sản phẩm của bài thực hành (thứ tự không quan trọng)
y_that = np.array(["NG"] * 50 + ["OK"] * 9950)
y_tram = np.array(["NG"] * 45 + ["OK"] * 5 + ["NG"] * 120 + ["OK"] * 9830)
cm = confusion_matrix(y_that, y_tram, labels=["NG", "OK"]) # hàng = thật, cột = dự đoán
print("Ma trận nhầm lẫn (hàng: thật NG, OK; cột: báo NG, OK):")
print(cm)
(TP, FN), (FP, TN) = cm
print(f"TP={TP} FN={FN} FP={FP} TN={TN}")
print(f"accuracy {accuracy_score(y_that, y_tram):.4f}")
print(f"precision {precision_score(y_that, y_tram, pos_label='NG'):.4f}")
print(f"recall {recall_score(y_that, y_tram, pos_label='NG'):.4f}")
print(f"specificity {recall_score(y_that, y_tram, pos_label='OK'):.4f} (recall của lớp OK)")
print(f"F1 {f1_score(y_that, y_tram, pos_label='NG'):.4f}")
print(classification_report(y_that, y_tram, labels=["NG", "OK"], digits=4))
Ma trận nhầm lẫn (hàng: thật NG, OK; cột: báo NG, OK):
[[ 45 5]
[ 120 9830]]
TP=45 FN=5 FP=120 TN=9830
accuracy 0.9875
precision 0.2727
recall 0.9000
specificity 0.9879 (recall của lớp OK)
F1 0.4186
precision recall f1-score support
NG 0.2727 0.9000 0.4186 50
OK 0.9995 0.9879 0.9937 9950
accuracy 0.9875 10000
macro avg 0.6361 0.9440 0.7061 10000
weighted avg 0.9959 0.9875 0.9908 10000
Kết quả khớp với phần tính tay. Lưu ý classification_report in cả hai lớp: dòng NG là precision/recall với NG là lớp dương; dòng OK cho specificity (recall của OK) và NPV (precision của OK). Hai dòng macro avg và weighted avg sẽ được giải thích ở mục 7.
labels, scikit-learn sắp nhãn tăng dần, nên ma trận có dạng [[TN, FP], [FN, TP]], ngược với bảng ở mục 1. Còn precision_score mặc định coi nhãn 1 là lớp dương: nếu nhóm của bạn lỡ quy ước 1 = OK, mọi con số "precision, recall" trong báo cáo đều là của lớp OK, thường rất cao và rất sai lệch. Luôn truyền labels và pos_label một cách tường minh.from sklearn.metrics import confusion_matrix, precision_score
y_that = [1, 1, 0, 0, 0, 1, 0, 0] # 1 = NG, 0 = OK
y_tram = [1, 0, 0, 1, 0, 1, 0, 0]
print(confusion_matrix(y_that, y_tram)) # mặc định: thứ tự [0, 1]
tn, fp, fn, tp = confusion_matrix(y_that, y_tram).ravel()
print(f"tn={tn} fp={fp} fn={fn} tp={tp}")
print(confusion_matrix(y_that, y_tram, labels=[1, 0])) # thứ tự như bảng mục 1: NG trước
print("precision NG:", round(precision_score(y_that, y_tram, pos_label=1), 3),
"| precision nếu lỡ coi OK là lớp dương:", round(precision_score(y_that, y_tram, pos_label=0), 3))
[[4 1]
[1 2]]
tn=4 fp=1 fn=1 tp=2
[[2 1]
[1 4]]
precision NG: 0.667 | precision nếu lỡ coi OK là lớp dương: 0.8
4. Nghịch lý accuracy khi dữ liệu mất cân bằng
Dây chuyền thật có rất ít lỗi: 0,1–2% là phổ biến. Khi lớp NG hiếm, accuracy gần như chỉ phản ánh khả năng cho qua hàng OK. So sánh ba "trạm" trên 20.000 sản phẩm có 0,5% lỗi:
import numpy as np
from sklearn.metrics import accuracy_score, balanced_accuracy_score, matthews_corrcoef, recall_score
N, N_NG = 20_000, 100
y_that = np.array([1] * N_NG + [0] * (N - N_NG)) # 1 = NG
def tram(bat_duoc, loai_nham):
"""Kết quả một trạm: bắt `bat_duoc` NG đầu tiên, loại nhầm `loai_nham` OK đầu tiên."""
y = np.zeros(N, int)
y[:bat_duoc] = 1
y[N_NG:N_NG + loai_nham] = 1
return y
cac_tram = {"Luôn báo OK": tram(0, 0),
"Trạm A (nhạy)": tram(95, 398),
"Trạm B (dè dặt)": tram(60, 20)}
print("trạm | accuracy | recall | overkill | balanced acc | MCC")
for ten, y in cac_tram.items():
ovk = ((y == 1) & (y_that == 0)).sum() / (y_that == 0).sum()
print(f"{ten:16s} | {accuracy_score(y_that, y):8.2%} | {recall_score(y_that, y, zero_division=0):6.0%} |"
f" {ovk:8.2%} | {balanced_accuracy_score(y_that, y):12.3f} | {matthews_corrcoef(y_that, y):5.3f}")
trạm | accuracy | recall | overkill | balanced acc | MCC
Luôn báo OK | 99.50% | 0% | 0.00% | 0.500 | 0.000
Trạm A (nhạy) | 97.98% | 95% | 2.00% | 0.965 | 0.423
Trạm B (dè dặt) | 99.70% | 60% | 0.10% | 0.799 | 0.669
- Chương trình "luôn báo OK" đạt accuracy 99,5% mà không bắt được lỗi nào.
- Trạm B có accuracy cao hơn trạm A nhưng để lọt 40% lỗi; trạm A lọt 5% và đổi lại loại nhầm khoảng 2% hàng tốt. Chọn theo accuracy là chọn sai trong hầu hết nhà máy.
- Balanced accuracy = (recall + specificity) / 2 và MCC (hệ số tương quan Matthews, từ −1 đến 1, bằng 0 với trạm đoán bừa hoặc luôn báo một lớp) không bị đánh lừa bởi mất cân bằng: cả hai cho chương trình "luôn báo OK" điểm của một trạm vô dụng (0,5 và 0).
- Nhưng để ý: balanced accuracy xếp trạm A trên, còn MCC xếp trạm B trên, vì MCC tính cả precision (trạm A loại nhầm 398 sản phẩm tốt để bắt 95 lỗi). Không có một con số "đúng" cho mọi nhà máy: mỗi chỉ số gộp ngầm định một tỉ lệ chi phí giữa escape và overkill. Chỉ chi phí thật (bài trước) mới quyết định được A hay B; báo cáo cho sản xuất vẫn nên là cặp escape/overkill kèm số lượng tuyệt đối.
5. Precision phụ thuộc tỉ lệ lỗi: phòng thí nghiệm khác dây chuyền
Recall và specificity là tính chất của trạm: chúng chỉ phụ thuộc trạm đối xử với một sản phẩm NG (hoặc OK) ra sao. Precision thì phụ thuộc thêm tỉ lệ lỗi của đầu vào. Bộ ảnh thử của nhà cung cấp thường có số NG ngang số OK, nên precision trong báo cáo rất đẹp; đưa ra dây chuyền có 0,3% lỗi thì precision sụp đổ dù trạm không thay đổi gì:
RECALL, OVERKILL = 0.98, 0.01 # tính chất của trạm, đo được trên bộ thử
print("tỉ lệ lỗi | trên 10.000 sp: NG bắt được | OK bị loại nhầm | precision | hàng tốt trong thùng NG")
for ti_le in (0.5, 0.10, 0.02, 0.005, 0.001):
ng = 10_000 * ti_le
tp, fp = RECALL * ng, OVERKILL * (10_000 - ng)
print(f"{ti_le:9.1%} | {tp:27.0f} | {fp:15.0f} | {tp / (tp + fp):9.1%} | {fp / (tp + fp):10.1%}")
tỉ lệ lỗi | trên 10.000 sp: NG bắt được | OK bị loại nhầm | precision | hàng tốt trong thùng NG
50.0% | 4900 | 50 | 99.0% | 1.0%
10.0% | 980 | 90 | 91.6% | 8.4%
2.0% | 196 | 98 | 66.7% | 33.3%
0.5% | 49 | 100 | 33.0% | 67.0%
0.1% | 10 | 100 | 8.9% | 91.1%
Cùng một trạm (bắt 98% lỗi, loại nhầm 1% hàng tốt): trên bộ thử 50/50 precision 99%; ở dây chuyền 0,1% lỗi, cứ 11 sản phẩm bị loại thì 10 là hàng tốt. Hai hệ quả thực tế:
- Khi đọc báo cáo "precision 99%", hỏi ngay: bộ thử có tỉ lệ NG bao nhiêu? Hãy yêu cầu recall và overkill (hoặc specificity), rồi tự tính precision theo tỉ lệ lỗi thật của dây chuyền.
- Khối lượng kiểm lại thùng NG tính theo precision ở tỉ lệ lỗi thật. Một trạm "tốt" trên giấy vẫn có thể khiến người kiểm lại chán nản và bỏ qua thùng NG, như bài trước đã cảnh báo.
6. Từ điểm số đến quyết định: đường cong precision–recall
Mô hình học máy (và nhiều công cụ rule-based) không trả lời thẳng OK/NG mà cho một điểm số (score), rồi ngưỡng biến điểm số thành quyết định. Mỗi ngưỡng cho một ma trận nhầm lẫn khác nhau; đường cong precision–recall vẽ tất cả các cặp đó. Bài trước đã chọn ngưỡng theo chi phí; ở đây ta chọn theo một ràng buộc thường gặp trong đặc tả: "recall ≥ 99% trên tập kiểm định", rồi xem cái giá phải trả. Dữ liệu là điểm số mô phỏng:
import numpy as np
from sklearn.metrics import average_precision_score, precision_recall_curve, roc_auc_score
rng = np.random.default_rng(3)
def tao_bo(n_ok, n_ng):
"""Điểm số mô phỏng: hàng OK quanh 0,25, hàng NG quanh 0,70 (1 = chắc chắn NG)."""
diem = np.concatenate([rng.normal(0.25, 0.10, n_ok), rng.normal(0.70, 0.12, n_ng)])
return np.r_[np.zeros(n_ok, int), np.ones(n_ng, int)], np.clip(diem, 0, 1)
y_val, s_val = tao_bo(5000, 200) # tập kiểm định: dùng để CHỌN ngưỡng
y_test, s_test = tao_bo(5000, 200) # tập thử: chỉ dùng để BÁO CÁO
print(f"AP (diện tích dưới đường PR) = {average_precision_score(y_val, s_val):.3f},"
f" ROC AUC = {roc_auc_score(y_val, s_val):.4f}")
p, r, nguong = precision_recall_curve(y_val, s_val)
ok = np.where(r[:-1] >= 0.99)[0] # các ngưỡng còn giữ recall ≥ 99%
T = nguong[ok[-1]] # ngưỡng cao nhất thỏa ràng buộc → ít loại nhầm nhất
print(f"Ngưỡng chọn trên tập kiểm định: {T:.3f}")
for ten, y, s in (("kiểm định", y_val, s_val), ("thử", y_test, s_test)):
bao_ng = s >= T
tp, fn = (bao_ng & (y == 1)).sum(), (~bao_ng & (y == 1)).sum()
fp = (bao_ng & (y == 0)).sum()
print(f" tập {ten:9s}: bắt {tp}/{tp + fn} NG (recall {tp / (tp + fn):.1%}),"
f" loại nhầm {fp}/5000 OK (overkill {fp / 5000:.2%}), precision {tp / (tp + fp):.1%}")
AP (diện tích dưới đường PR) = 0.979, ROC AUC = 0.9983
Ngưỡng chọn trên tập kiểm định: 0.443
tập kiểm định: bắt 198/200 NG (recall 99.0%), loại nhầm 138/5000 OK (overkill 2.76%), precision 58.9%
tập thử : bắt 197/200 NG (recall 98.5%), loại nhầm 137/5000 OK (overkill 2.74%), precision 59.0%
- AP (average precision) và ROC AUC tóm tắt cả đường cong thành một con số, tiện để so sánh hai mô hình. Nhưng dây chuyền chạy ở một ngưỡng; thứ cần ghi vào đặc tả là recall và overkill tại ngưỡng đó.
- Ràng buộc recall ≥ 99% có giá: overkill khoảng 2,7%, precision chỉ khoảng 59% dù AUC gần 1. Ngưỡng chọn trên tập kiểm định thường cho kết quả hơi khác trên tập thử độc lập: ở đây recall trên tập thử chỉ còn 98,5%, dưới ràng buộc. Báo cáo con số của tập thử, không phải con số của tập đã dùng để chọn ngưỡng. Với 200 NG, mỗi NG bị lọt là 0,5% recall: số lượng mẫu NG giới hạn độ chính xác của mọi con số (mục 8).
7. Ma trận nhầm lẫn nhiều lớp
Khi trạm phân loại cả loại lỗi (xước, móp, bẩn), ma trận nhầm lẫn có một hàng và một cột cho mỗi lớp. Đường chéo là phân loại đúng; ô ngoài đường chéo cho biết lớp nào bị nhầm sang lớp nào:
import numpy as np
from sklearn.metrics import classification_report, confusion_matrix
LOP = ["OK", "xuoc", "mop", "ban"]
# (thật, dự đoán, số lượng): kết quả trên bộ thử 1.000 ảnh
DEM = [("OK", "OK", 880), ("OK", "ban", 15), ("OK", "xuoc", 5),
("xuoc", "xuoc", 34), ("xuoc", "ban", 4), ("xuoc", "OK", 2),
("mop", "mop", 25), ("mop", "xuoc", 3), ("mop", "OK", 2),
("ban", "ban", 27), ("ban", "xuoc", 3)]
y_that = np.array([t for t, d, n in DEM for _ in range(n)])
y_du_doan = np.array([d for t, d, n in DEM for _ in range(n)])
cm = confusion_matrix(y_that, y_du_doan, labels=LOP)
print("thật \\ dự đoán " + "".join(f"{l:>6s}" for l in LOP))
for ten, hang in zip(LOP, cm):
print(f"{ten:15s}" + "".join(f"{v:6d}" for v in hang))
print(classification_report(y_that, y_du_doan, labels=LOP, digits=3, zero_division=0))
# Gộp về câu hỏi của nhà máy: OK hay NG (mọi loại lỗi đều là NG)
that_ng, bao_ng = y_that != "OK", y_du_doan != "OK"
tp, fn = (that_ng & bao_ng).sum(), (that_ng & ~bao_ng).sum()
fp, tn = (~that_ng & bao_ng).sum(), (~that_ng & ~bao_ng).sum()
print(f"Gộp OK/NG: TP={tp} FN={fn} FP={fp} TN={tn} → escape {fn / (tp + fn):.1%}, overkill {fp / (fp + tn):.1%}")
print(f"Nhầm loại lỗi nhưng vẫn báo NG: {(that_ng & bao_ng & (y_that != y_du_doan)).sum()} ảnh")
thật \ dự đoán OK xuoc mop ban
OK 880 5 0 15
xuoc 2 34 0 4
mop 2 3 25 0
ban 0 3 0 27
precision recall f1-score support
OK 0.995 0.978 0.987 900
xuoc 0.756 0.850 0.800 40
mop 1.000 0.833 0.909 30
ban 0.587 0.900 0.711 30
accuracy 0.966 1000
macro avg 0.834 0.890 0.852 1000
weighted avg 0.974 0.966 0.968 1000
Gộp OK/NG: TP=96 FN=4 FP=20 TN=880 → escape 4.0%, overkill 2.2%
Nhầm loại lỗi nhưng vẫn báo NG: 10 ảnh
- Đọc theo hàng: 2 vết xước và 2 vết móp bị báo OK, đó là 4 escape thật sự. Đọc theo cột: 15 ảnh OK bị báo "bẩn", nguồn overkill lớn nhất; nên xem lại các ảnh này (bụi, vết dầu, phản xạ?).
- 10 ảnh bị nhầm loại lỗi (xước thành bẩn…) nhưng vẫn bị loại. Với quyết định OK/NG chúng vô hại; chúng chỉ quan trọng nếu loại lỗi dùng để truy nguyên nhân hoặc chọn thùng tái chế.
- macro avg lấy trung bình các lớp như nhau; weighted avg lấy trung bình theo số mẫu mỗi lớp nên bị lớp OK (900 mẫu) chi phối, luôn trông đẹp hơn. Với kiểm tra lỗi, hãy xem từng lớp và con số gộp OK/NG, đừng chỉ nhìn weighted avg.
8. Báo cáo cho nhà máy: số tuyệt đối và độ bất định
"Recall 90%" tính trên 50 mẫu NG khác hẳn "recall 90%" tính trên 5.000 mẫu. Hãy luôn ghi phân số (45/50) và, khi có thể, khoảng tin cậy. Khoảng Wilson là một cách tính đơn giản và ổn định cho tỉ lệ:
import math
def wilson(k, n, z=1.96):
"""Khoảng tin cậy Wilson (mặc định 95%) cho tỉ lệ k/n."""
p = k / n
tam = (p + z * z / (2 * n)) / (1 + z * z / n)
nua = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / (1 + z * z / n)
return tam - nua, tam + nua
for ten, k, n in (("recall bài thực hành", 45, 50), ("cùng recall, 10 lần mẫu", 450, 500),
("overkill bài thực hành", 120, 9950)):
lo, hi = wilson(k, n)
print(f"{ten:24s}: {k}/{n} = {k / n:6.2%}, khoảng tin cậy 95%: {lo:6.2%} – {hi:6.2%}")
recall bài thực hành : 45/50 = 90.00%, khoảng tin cậy 95%: 78.64% – 95.65%
cùng recall, 10 lần mẫu : 450/500 = 90.00%, khoảng tin cậy 95%: 87.06% – 92.33%
overkill bài thực hành : 120/9950 = 1.21%, khoảng tin cậy 95%: 1.01% – 1.44%
Với 50 mẫu NG, recall thật có thể chỉ khoảng 79% hoặc cao tới khoảng 96%: hai trạm có recall 88% và 92% trên 50 mẫu là không phân biệt được. Khi kết quả là 0 lần lọt, dùng quy tắc số 3 đã học ở bài trước (escape < 3/n với 95% tin cậy). Mẫu báo cáo ngắn gọn cho một đợt thử:
Bộ thử: 10.000 sản phẩm (50 NG đã xác nhận bởi QC, 9.950 OK), ngưỡng phiên bản v1.3, ngày …
Lớp dương: NG
Bắt lỗi: 45/50 (recall 90,0%; KTC 95%: 78,6–95,7%) → escape 5/50 = 10,0%
Loại nhầm: 120/9.950 (overkill 1,21%) → thùng NG: 165 sp, trong đó 120 sp tốt (73%)
Lọt ra hàng giao: 5/9.835 ≈ 508 ppm
Ghi chú: 5 sản phẩm lọt đều là lỗi loại … nằm ở vùng …; ảnh đính kèm.
9. Sai lầm thường gặp
- Không nói lớp dương là gì, hoặc dùng mặc định của thư viện với quy ước 1 = OK (mục 3).
- Chỉ báo accuracy hoặc chỉ báo F1. Sản xuất cần escape và overkill (và số lượng tuyệt đối).
- Mang precision từ bộ thử cân bằng ra dây chuyền (mục 5).
- Chọn ngưỡng và báo cáo trên cùng một tập dữ liệu: kết quả lạc quan hơn thực tế (mục 6).
- Lẫn đơn vị đếm: đếm theo ảnh, theo vùng ROI, theo lỗi hay theo sản phẩm? Một sản phẩm chụp bởi 4 camera là NG nếu bất kỳ ảnh nào báo NG, nên overkill theo sản phẩm lớn hơn overkill theo ảnh (bài tập 3). Ghi rõ đơn vị đếm trong đặc tả.
- Nhãn thật không đáng tin: nếu QC dán nhãn sai 2% bộ thử, mọi chỉ số đều sai theo. Mẫu sát giới hạn phải được kiểm và ký bởi người có thẩm quyền.
10. Bài tập
Bài 1 (thực hành của bài). Một trạm kiểm 10.000 sản phẩm, 50 lỗi thật, phát hiện 45, báo nhầm 120. Tính recall, precision, tỉ lệ escape và overkill (theo số OK thật và theo tổng sản lượng). Viết hai câu báo cáo cho quản lý sản xuất.
Xem đáp án
TP, FN, FP = 45, 50 - 45, 120
TN = 10_000 - 50 - FP
print(f"recall = {TP}/{TP + FN} = {TP / (TP + FN):.1%}")
print(f"precision = {TP}/{TP + FP} = {TP / (TP + FP):.1%}")
print(f"escape = {FN}/{TP + FN} = {FN / (TP + FN):.1%}")
print(f"overkill = {FP}/{FP + TN} = {FP / (FP + TN):.2%} (theo OK thật), {FP / 10_000:.2%} (theo tổng)")
recall = 45/50 = 90.0%
precision = 45/165 = 27.3%
escape = 5/50 = 10.0%
overkill = 120/9950 = 1.21% (theo OK thật), 1.20% (theo tổng)
Báo cáo: "Trạm bắt được 45/50 sản phẩm lỗi; 5 sản phẩm lỗi đã lọt (escape 10%). Trạm loại nhầm 120 sản phẩm tốt (khoảng 1,2%), nên trong 165 sản phẩm ở thùng NG chỉ 45 là lỗi thật; cần người kiểm lại thùng NG cho tới khi giảm được overkill."
Bài 2. Nhà cung cấp AI báo: trên bộ thử 500 OK và 500 NG, mô hình bắt 490 NG và loại nhầm 10 OK, "precision 98%, recall 98%". Dây chuyền của bạn có 0,3% lỗi và chạy 30.000 sản phẩm mỗi ca. Mỗi ca dự kiến có bao nhiêu sản phẩm trong thùng NG, bao nhiêu là hàng tốt, và precision thực tế bao nhiêu?
Xem đáp án
recall, overkill = 490 / 500, 10 / 500 # tính chất của mô hình, không phụ thuộc tỉ lệ lỗi
sp_ca, ti_le = 30_000, 0.003
ng = sp_ca * ti_le
tp, fp = recall * ng, overkill * (sp_ca - ng)
print(f"recall {recall:.0%}, overkill {overkill:.0%}")
print(f"Mỗi ca: {ng:.0f} NG thật, thùng NG có {tp + fp:.0f} sp ({tp:.0f} lỗi thật + {fp:.0f} hàng tốt)")
print(f"Precision thực tế: {tp / (tp + fp):.1%}")
recall 98%, overkill 2%
Mỗi ca: 90 NG thật, thùng NG có 686 sp (88 lỗi thật + 598 hàng tốt)
Precision thực tế: 12.8%
Overkill 2% trên 29.910 sản phẩm tốt là khoảng 600 sản phẩm tốt bị loại mỗi ca, gần gấp 7 lần số lỗi thật; precision thực tế chỉ khoảng 13%. Precision 98% trong báo cáo chỉ đúng với bộ thử 50/50. Cần đàm phán lại theo overkill (ví dụ ≤ 0,2%) chứ không theo precision.
Bài 3. Một sản phẩm được 4 camera chụp 4 mặt; sản phẩm là NG nếu bất kỳ ảnh nào báo NG. Mỗi camera có overkill 0,5% trên ảnh của sản phẩm tốt (giả sử độc lập). Overkill theo sản phẩm là bao nhiêu? Một lỗi chỉ nhìn thấy ở một mặt, camera mặt đó có recall 97%: recall theo sản phẩm là bao nhiêu?
Xem đáp án
Overkill theo sản phẩm = 1 − (1 − 0,005)⁴ ≈ 1,99%, gần gấp 4 lần overkill mỗi camera. Recall theo sản phẩm với lỗi chỉ thấy ở một mặt vẫn là 97% (cộng thêm một chút nếu camera khác loại nhầm đúng sản phẩm đó do overkill ngẫu nhiên, khoảng 0,03 × 0,015 ≈ 0,05%, không đáng kể). Bài học: ghép nhiều camera làm overkill cộng dồn còn recall không tăng; đặc tả phải nói rõ con số tính theo ảnh hay theo sản phẩm.
Tóm tắt
- Lớp dương = NG. Bốn ô: TP (bắt đúng), FN (escape), FP (overkill), TN (cho qua đúng); luôn điền đủ bốn ô trước khi tính.
- Recall = TP/(TP+FN), escape = 1 − recall; specificity = TN/(TN+FP), overkill = 1 − specificity; precision = TP/(TP+FP), 1 − precision = phần hàng tốt trong thùng NG; ppm lọt = FN/(FN+TN) × 10⁶.
- Dùng scikit-learn với
labelsvàpos_labeltường minh; nhãn 0/1 mặc định cho ma trận [[TN, FP], [FN, TP]]. - Accuracy vô nghĩa khi lỗi hiếm; precision phụ thuộc tỉ lệ lỗi đầu vào, còn recall và overkill thì không. Balanced accuracy, MCC tốt hơn để so sánh mô hình.
- Chọn ngưỡng trên tập kiểm định, báo cáo trên tập thử; ghi số tuyệt đối, khoảng tin cậy và đơn vị đếm (ảnh hay sản phẩm). Ma trận nhiều lớp: xem từng hàng/cột rồi gộp về OK/NG.
Bài tiếp theo: Đặc tả kỹ thuật và nghiệm thu FAT/SAT, nơi các chỉ số trong bài này trở thành tiêu chí nghiệm thu có chữ ký của hai bên.
Bài tập thực hành
- Một trạm kiểm 10.000 sản phẩm, 50 lỗi thật, phát hiện 45, báo nhầm 120: tính recall, precision, tỉ lệ escape và overkill.
Làm xong bài tập mới chuyển sang bài tiếp theo: tự viết code là cách duy nhất để nhớ lâu.


